使用GNU汇编程序在x86_64中调用printf

L's*_*rld 8 linux assembly gcc x86-64 gnu-assembler

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

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

            .text
            .global main  
main:
            mov    $format, %rbx
            mov    (%rbx), %rdi
            mov    $1, %rsi
            call     printf
            ret
Run Code Online (Sandbox Code Playgroud)

我使用GCC来组装和链接:

gcc -o main main.s

我用这个命令运行它:

./主要

当我运行程序时,我得到一个seg错误.通过使用gdb,它说printf没有找到.我试过".extern printf",但是没有用.有人建议我应该printfRET之前存储堆栈指针并在RET之前恢复,我该怎么做?

Mic*_*tch 20

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

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

C运行时调用main函数之后,堆栈未对齐8,因为返回指针被CALL放在堆栈上.重新调整到16字节边界你可以简单地PUSH 任何通用寄存器到堆栈和POP在结束其关闭.

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

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

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

当$ format指针已经是地址时,你也取消引用它.所以这是错的:

mov  $format, %rbx
mov  (%rbx), %rdi 
Run Code Online (Sandbox Code Playgroud)

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

lea  format(%rip), %rdi
Run Code Online (Sandbox Code Playgroud)

这使用RIP相对寻址.

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

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

# 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
Run Code Online (Sandbox Code Playgroud)

其他建议/建议

您还应该从64位Linux ABI中了解到,调用约定还需要您编写的函数以保护某些寄存器.登记册清单及是否应予保留如下:

在此输入图像描述

YesPreserved across Register列中显示的任何寄存器都是您必须确保在您的函数中保留的寄存器.功能main就像任何其他C功能一样.


如果你有一个字符串/数据,你知道会是只读的,你可以将它们放在.rodata段与.section .rodata而非.data


在64位模式下:如果您的目标操作数是32位寄存器,则CPU将在整个64位寄存器中对寄存器进行零扩展.这可以节省指令编码上的字节.


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

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

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

call printf@plt 
Run Code Online (Sandbox Code Playgroud)

这通过过程链接表(PLT)调用外部库函数


mpr*_*net 5

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

#include <stdio.h>
int main() {
   return printf("%d\n", 1);
}
Run Code Online (Sandbox Code Playgroud)

这输出汇编代码:

        .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
Run Code Online (Sandbox Code Playgroud)

这为您提供了一个调用 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
Run Code Online (Sandbox Code Playgroud)

然后运行它打印:

1

  • Michael 是正确的:如果您要求 gcc 优化(使用`-O3`),它会发出最佳代码:https://godbolt.org/g/sX5yCe。它使用 `jmp` 进行尾调用,因此堆栈对齐与进入 `main` 时保持相同。它还使用 `xor` 将 `%al` 归零,而不是使用效率较低的 `mov`。当然,它将字符串常量放在`.rodata` 中,而不是`.data`。使用编译器输出作为优化的起点是一个很好的计划,但前提是您从 `-O2` 或 `-O3` 输出开始!否则你可能会比编译器做得更糟。 (2认同)