【问题标题】:x64 nasm: pushing memory addresses onto the stack & call functionx64 nasm:将内存地址推入堆栈并调用函数
【发布时间】:2012-10-17 00:25:42
【问题描述】:

我对 Mac 上的 x64-assembly 还很陌生,所以我对在 64 位中移植一些 32 位代码感到困惑。
该程序应该通过 C 标准库中的 printf 函数简单地打印出一条消息。
我从这段代码开始:

section .data
    msg db 'This is a test', 10, 0    ; something stupid here

section .text
    global _main
    extern _printf

_main:
    push    rbp
    mov     rbp, rsp       

    push    msg
    call    _printf

    mov     rsp, rbp
    pop     rbp
    ret

以这种方式用 nasm 编译它:

$ nasm -f macho64 main.s

返回以下错误:

main.s:12: error: Mach-O 64-bit format does not support 32-bit absolute addresses

我已尝试修复该问题,将代码更改为:

section .data
    msg db 'This is a test', 10, 0    ; something stupid here

section .text
    global _main
    extern _printf

_main:
    push    rbp
    mov     rbp, rsp       

    mov     rax, msg    ; shouldn't rax now contain the address of msg?
    push    rax         ; push the address
    call    _printf

    mov     rsp, rbp
    pop     rbp
    ret

使用上面的nasm 命令编译得很好,但现在在将带有gcc 的目标文件编译为实际程序时出现警告:

$ gcc main.o
ld: warning: PIE disabled. Absolute addressing (perhaps -mdynamic-no-pic) not
allowed in code signed PIE, but used in _main from main.o. To fix this warning,
don't compile with -mdynamic-no-pic or link with -Wl,-no_pie

因为这是一个警告而不是错误,所以我执行了a.out 文件:

$ ./a.out
Segmentation fault: 11

希望有人知道我做错了什么。

【问题讨论】:

    标签: macos gcc assembly x86-64 nasm


    【解决方案1】:

    根据x86 64位指令集http://download.intel.com/products/processor/manual/325383.pdf的文档

    PUSH 仅接受 8、16 和 32 位立即数(但允许使用 64 位寄存器和寄存器寻址的内存块)。

    PUSH msg
    

    如果 msg 是 64 位立即地址,则不会像您发现的那样编译。


    您的 64 位库中定义的 _printf 调用约定是什么?

    它是期望堆栈中的参数还是使用寄存器中的参数的快速调用约定?由于 x86-64 提供了更多通用寄存器,因此更频繁地使用快速调用约定。

    【讨论】:

    • msg 是一个 32 位地址,所以问题不在于 PUSH。问题是 Mach-O 不允许 32 位绝对寻址。
    【解决方案2】:

    64 位 OS X ABI 完全符合 System V ABI - AMD64 Architecture Processor Supplement。它的代码模型与 Small position Independent Code Model (PIC) 非常相似,区别在于here。在该代码模型中,所有本地和小数据都使用 RIP 相对寻址直接访问。正如 Z boson 在 cmets 中所指出的,64 位 Mach-O 可执行文件的映像库超出了虚拟地址空间的前 4 GiB,因此push msg 不仅是放置msg 地址的无效方式在堆栈上,但这也是不可能的,因为PUSH 不支持 64 位立即数。代码应该类似于:

       ; this is what you *would* do for later args on the stack
    lea   rax, [rel msg]  ; RIP-relative addressing
    push  rax
    

    但在这种特殊情况下,根本不需要将值压入堆栈。 64 位调用约定要求前 6 个整数/指针参数在寄存器中传递 RDIRSIRDXRCXR8R9 ,完全按照这个顺序。前 8 个浮点或向量参数进入 XMM0XMM1、...、XMM7。只有在使用了所有可用的寄存器或存在不适合任何这些寄存器的参数(例如 80 位 long double 值)之后,才会使用堆栈。使用MOVQWORD 变体)而不是PUSH 执行 64 位立即推送。简单的返回值在RAX 寄存器中传回。调用者还必须为被调用者提供堆栈空间以保存一些寄存器。

    printf 是一个特殊函数,因为它接受可变数量的参数。调用此类函数时,AL(RAX 的低字节)应设置为浮点参数的数量,并在向量寄存器中传递。另请注意,对于位于代码 2 GiB 内的数据,首选RIP-relative 寻址。

    下面是gcc 在 OS X 上如何将printf("This is a test\n"); 转换为程序集:

        xorl    %eax, %eax             # (1)
        leaq    L_.str(%rip), %rdi     # (2)
        callq   _printf                # (3)
    
    L_.str:
        .asciz   "This is a test\n"
    

    (这是AT&T风格的程序集,左边是源,右边是目的地,寄存器名称以%为前缀,数据宽度编码为指令名称的后缀)

    (1) 处,零被放入AL (通过将整个RAX 归零以避免部分寄存器延迟),因为没有传递浮点参数。在(2),字符串的地址被加载到RDI。请注意该值实际上是与RIP 的当前值的偏移量。由于汇编器不知道这个值是什么,它会在目标文件中放置一个重定位请求。然后链接器会看到重定位并在链接时放置正确的值。

    我不是 NASM 大师,但我认为下面的代码应该可以做到:

    default rel             ; make [rel msg] the default for [msg]
    section .data
        msg:  db 'This is a test', 10, 0    ; something stupid here
    
    section .text
        global _main
        extern _printf
    
    _main:
        push    rbp                 ; re-aligns the stack by 16 before call
        mov     rbp, rsp       
    
        xor     eax, eax            ; al = 0 FP args in XMM regs
        lea     rdi, [rel msg]
        call    _printf
    
        mov     rsp, rbp
        pop     rbp
        ret
    

    【讨论】:

    • 函数应该从堆栈中删除参数还是调用者应该删除它们? (如果栈上有参数)
    • 调用者应该在调用后清理堆栈。
    • 您的回答并没有真正解释为什么 NASM 不会创建目标文件。原因是 Mac OS X 的限制。图像库在 OS X 上大于 2^32。OP 的原始代码将与 elf64 很好地组装。 mach-o-64-bit-format-does-not-support-32-bit-absolute-addresses-nasm
    • @Zboson,嗯,Mach-O 64 位不支持 32 位绝对地址从问题中的错误消息中可以看出,因此我可能认为没有必要解释这个问题进一步(或者我只是在工作一天后变得懒惰 - 更有可能)并直接给出了解决方案。不过,我关于符合 SysV AMD64 ABI 的声明并不完全正确。我将扩大段落并添加相应的引用。
    • @HristoIliev,我删除了对另一个问题的回答。您和我的回答现在完成了这个问题,所以另一个问题现在是重复的。
    【解决方案3】:

    尚无答案解释 NASM 报告的原因

    Mach-O 64-bit format does not support 32-bit absolute addresses
    

    NASM 不这样做的原因在 Agner Fog's Optimizing Assembly 手册中的 3.3 寻址模式 部分中的标题为64 位模式中的 32 位绝对寻址 小节中进行了解释他写道

    32 位绝对地址不能在 Mac OS X 中使用,其中地址大于 2^32 默认。

    这在 Linux 或 Windows 上不是问题。事实上,我已经在static-linkage-with-glibc-without-calling-main 展示了这个作品。 hello world 代码使用 32 位绝对寻址和 elf64 并且运行良好。

    @HristoIliev 建议使用 rip 相对寻址,但没有解释 Linux 中的 32 位绝对寻址也可以。事实上,如果你将lea rdi, [rel msg] 更改为lea rdi, [msg],它会在nasm -efl64 下组装并运行良好,但在nasm -macho64 下会失败

    像这样:

    section .data
        msg db 'This is a test', 10, 0    ; something stupid here
    
    section .text
        global _main
        extern _printf
    
    _main:
        push    rbp
        mov     rbp, rsp       
    
        xor     al, al
        lea     rdi, [msg]
        call    _printf
    
        mov     rsp, rbp
        pop     rbp
        ret
    

    您可以检查这是一个绝对的 32 位地址,而不是与 objdump 相对的地址。但是,重要的是要指出,首选方法仍然是翻录相对寻址。 Agner 在同一手册中写道:

    绝对没有理由对简单的内存操作数使用绝对地址。撕裂- 相对地址使指令更短,它们消除了在加载时重新定位的需要 时间,并且它们可以在所有系统中安全使用。

    那么什么时候在 64 位模式下使用 32 位绝对地址呢?静态数组是一个不错的选择。请参阅以下小节在 64 位模式下寻址静态数组。简单的情况是例如:

    mov eax, [A+rcx*4]
    

    其中 A 是静态数组的绝对 32 位地址。这在 Linux 上工作得很好,但在 Mac OS X 上你又不能这样做,因为默认情况下图像基数大于 2^32。要在 Mac OS X 上执行此操作,请参阅 Agner 手册中的示例 3.11c 和 3.11d。在示例 3.11c 中,您可以这样做

    mov eax, [(imagerel A) + rbx + rcx*4]
    

    您使用来自 Mach O __mh_execute_header 的外部引用来获取图像库。在示例 3.11c 中,您使用 rip 相对寻址并像这样加载地址

    lea rbx, [rel A]; rel tells nasm to do [rip + A]
    mov eax, [rbx + 4*rcx] ; A[i]
    

    【讨论】:

    • 我根本找不到可靠的信息来说明为什么 64 位 Mach-O 二进制文件的图像基数如此之高,但似乎在 x86-64 上 __PAGEZERO 段占据了整个前 4 GiB VM 的任何 32 位指针都是无效的(__PAGEZERO 既不可读也不可写或可执行)。这可能是检测旧程序代码的一种廉价方法,它假设sizeof(int) 等于sizeof(void *),只需使程序崩溃即可。
    • This answer 及其评论表明我的假设可能是正确的。
    • @HristoIliev,看起来你想明白了。我想知道这还有什么其他影响。这意味着静态数组的寻址模式 [绝对 32 位地址 + 索引] 在 OS X 上是不可能的。Evgeny Kluev 对我的问题stackoverflow.com/questions/25899395/… 的建议在 OS X 上是不可能的。一些指令/端口(例如端口 7 Haswell) 仅适用于这种简单的寻址模式。微操作融合也可能会受到影响stackoverflow.com/questions/26046634/…
    • 如果您打算使用 32 位绝对寻址将静态地址放入寄存器,请使用 mov edi, msg,就像 Linux 中的 gcc -fno-pie 一样。 这是 32 位绝对寻址比索引静态数组更广泛的使用。 (虽然它只节省代码大小,而不是指令。但mov reg,imm32 可以在比 LEA 更多的端口上运行。)
    猜你喜欢
    • 1970-01-01
    • 2021-11-02
    • 1970-01-01
    • 1970-01-01
    • 2017-02-24
    • 1970-01-01
    • 2011-10-15
    • 2015-04-11
    • 2012-11-02
    相关资源
    最近更新 更多