使用 GNU 汇编器在 x86_64 中调用 printf

Posted

技术标签:

【中文标题】使用 GNU 汇编器在 x86_64 中调用 printf【英文标题】:Calling printf in x86_64 using GNU assembler 【发布时间】:2016-11-15 01:39:49 【问题描述】:

我使用 AT&T 语法编写了一个程序,用于 GNU 汇编器:

            .data
format:   .ascii "%d\n"  

            .text
            .global main  
main:
            mov    $format, %rbx
            mov    (%rbx), %rdi
            mov    $1, %rsi
            call     printf
            ret

我使用 GCC 来组装和链接:

gcc -o main main.s

我用这个命令运行它:

./main

当我运行程序时,我遇到了段错误。通过使用 gdb,它显示 printf 未找到。我试过“.extern printf”,它不起作用。有人建议我应该在调用printf之前存储堆栈指针并在RET之前恢复,我该怎么做?

【问题讨论】:

您应该真的阅读 SysV x86-64 ABI。乍一看,您在调用之前未对齐堆栈,您没有将 %rax 归零,您没有为正确的参数使用正确的寄存器,而且我怀疑您在不应该取消引用 format 时。 【参考方案1】:

此代码存在许多问题。 Linux 使用的AMD64 System V ABI 调用约定需要做一些事情。它要求在 CALL 之前堆栈至少 16 字节(或 32 字节)对齐:

输入参数区域的末尾应对齐在 16(32,如果 __m256 是 在堆栈上传递)字节边界。

C 运行时调用您的main 函数后,堆栈未对齐8,因为返回指针被CALL 放置在堆栈上。要重新对齐到 16 字节边界,您可以简单地将 PUSH any 通用寄存器放入堆栈并在最后 POP 将其关闭。

调用约定还要求 AL 包含用于变量参数函数的向量寄存器的数量:

%al 用于表示传递给需要可变参数数量的函数的向量参数的数量

printf是可变参数函数,所以需要设置AL。在这种情况下,您不会在向量寄存器中传递任何参数,因此您可以将 AL 设置为 0。

当 $format 指针已经是地址时,您还可以取消引用它。所以这是错误的:

mov  $format, %rbx
mov  (%rbx), %rdi 

这会获取格式的地址并将其放在RBX中。然后,您将 RBX 中该地址的 8 个字节放入 RDI 中。 RDI 需要是一个指向字符串的指针,而不是字符本身。这两行可以替换为:

lea  format(%rip), %rdi

这使用 RIP 相对寻址。

您还应该 NUL 终止您的字符串。您可以在 x86 平台上使用 .asciz,而不是使用 .ascii

您的程序的工作版本可能如下所示:

# global data  #
    .data
format: .asciz "%d\n"
.text
    .global main
main:
  push %rbx
  lea  format(%rip), %rdi
  mov  $1, %esi           # Writing to ESI zero extends to RSI.
  xor %eax, %eax          # Zeroing EAX is efficient way to clear AL.
  call printf
  pop %rbx
  ret

其他建议/建议

您还应该从 64 位 Linux ABI 中了解到,调用约定还需要您编写的函数来尊重某些寄存器的保留。寄存器列表及是否保留如下:

任何在 中写着 Yes 的寄存器都保留 函数调用列是您必须确保在函数中保留的列。函数 main 与任何其他 C 函数一样。


如果您知道字符串/数据是只读的,您可以将它们放在.rodata 部分中,使用.section .rodata 而不是.data


在 64 位模式下:如果您的目标操作数是 32 位寄存器,CPU 会将寄存器零扩展至整个 64 位寄存器。这样可以节省指令编码的字节数。


您的可执行文件可能被编译为与位置无关的代码。您可能会收到类似于以下内容的错误:

在制作共享对象时,不能使用针对符号`printf@@GLIBC_2.2.5'的重定位R_X86_64_PC32;用 -fPIC 重新编译

要解决此问题,您必须以这种方式调用外部函数 printf

call printf@plt 

这通过Procedure Linkage Table (PLT)调用外部库函数

【讨论】:

我已经重新组织了我的代码并且它可以工作。我以为是import printf的问题,没必要。非常感谢。 在这个特定的代码示例中,因为只有一个调用并且它出现在末尾,所以可以 JMP 到 printf 而不是 CALL 并使用 PUSH 消除堆栈对齐/流行音乐。这超出了我的回答范围,但可以随时查看 TAIL CALL optimizations 上的文献 优秀的建议。未来的旅行者,另请参阅:nickdesaulniers.github.io/blog/2014/04/18/… 我编辑的更改日志消息包含重要点。 . ABI 文档链接已损坏。其他更改是可选的,因为xor %eax,%eax 是将 AL 或 RAX 设置为零的最佳方法(因此说可变参数函数查看%rax 而不是%al 并无害处),其余的只是额外的细节/ cmets 因为无论如何都需要编辑来修复 ABI 链接。【参考方案2】:

您可以查看从等效的 c 文件生成的汇编代码。 使用 test.c 运行 gcc -o - -S -fno-asynchronous-unwind-tables test.c

#include <stdio.h>
int main() 
   return printf("%d\n", 1);

这会输出汇编代码:

        .file   "test.c"
        .section        .rodata
.LC0:
        .string "%d\n"
        .text
        .globl  main
        .type   main, @function
main:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    $1, %esi
        movl    $.LC0, %edi
        movl    $0, %eax
        call    printf
        popq    %rbp
        ret
        .size   main, .-main
        .ident  "GCC: (GNU) 6.1.1 20160602"
        .section        .note.GNU-stack,"",@progbits

这为您提供了一个调用 printf 的汇编代码示例,您可以随后对其进行修改。


与您的代码相比,您应该修改两件事:

%rdi 应该指向格式,你不应该未引用 %rbx,这可以通过 mov $format, %rdi 来完成 printf 有可变数量的参数,那么你应该添加mov $0, %eax

应用这些修改会得到类似的结果:

    .data
format: .ascii "%d\n"  
.text
    .global main  
main:
  mov  $format, %rdi
  mov  $1, %rsi
  mov  $0, %eax
  call printf
  ret

然后运行它打印:

1

【讨论】:

您修改后的代码在调用printf 之前没有16 字节对齐堆栈。它可能适用于许多场景,但不是全部。在您的函数main 启动后推送任何 64 位寄存器并在最后恢复它会使事情保持一致。 64 位 Linux ABI 需要至少 16 字节对齐(如果将 256 位向量传递给函数,则需要 32 字节对齐)。在函数调用之前,堆栈需要 16(或 32)字节对齐。在 CALL 指令将控制权转移给一个函数(main 与其他 C 函数一样)后,返回地址被放置在堆栈上,使其错位 8。 @MichaelPetch:我试图给出一个修改最少的工作代码,否则 gcc 生成的程序集更好。 不幸的是,在这种情况下,16 字节对齐是幸运的。我怀疑在 -O2 或更高级别的优化级别上,它实际上会删除 PUSH/POP/RET,然后对 printf 进行尾部调用 JMP。在这种情况下,仍然保持对齐(没有额外的 PUSH/POP),因为 JMP 不像 CALL 那样在堆栈上放置返回地址。 Michael 是正确的:如果您要求 gcc 优化(通过使用 -O3),gcc 会发出最佳代码:godbolt.org/g/sX5yCe。它使用jmp 进行尾调用,因此堆栈对齐方式与进入main 时保持相同。它还使用xor%al 归零,而不是使用效率较低的mov。当然,它会将字符串常量放入.rodata,而不是.data。使用编译器输出作为优化的起点是一个不错的计划,但前提是您从-O2-O3 输出开始!否则你可能比编译器做得更差。

以上是关于使用 GNU 汇编器在 x86_64 中调用 printf的主要内容,如果未能解决你的问题,请参考以下文章

x86_64 Linux 上定义的 ioctl 系统调用的用户空间包装器在哪里?

x86_64 汇编 Linux 系统调用混淆

如何在 GCC (x86_64) 中使用内联汇编进行相对跳转/调用

为啥 linux multiarch 使用 x86_64-linux-gnu 而不是 lib64?

在 x86_64 汇编问题中添加双精度

32位系统调用表入口点如何映射到x86_64中的SYSCALL_DEFINE