使用 GNU 汇编程序在 x86_64 中调用 printf
Calling printf in x86_64 using GNU assembler
我使用 AT&T 语法编写了一个程序用于 GNU assembler:
.data
format: .ascii "%d\n"
.text
.global main
main:
mov $format, %rbx
mov (%rbx), %rdi
mov , %rsi
call printf
ret
我将 GCC 用于 assemble 和 link 以及:
gcc -o main main.s
我运行它用这个命令:
./main
当我 运行 程序出现段错误时。通过使用 gdb,它说 printf
未找到。我试过“.extern printf”,它不起作用。有人建议我应该在调用 printf
之前存储堆栈指针并在 RET 之前恢复,我该怎么做?
这段代码有很多问题。 Linux 使用的 AMD64 System V ABI 调用约定需要一些东西。它要求在 CALL 之前堆栈至少对齐 16 字节(或 32 字节):
The end of the input argument area shall be aligned on a 16 (32, if __m256 is
passed on stack) byte boundary.
在 C 运行时调用您的 main
函数后,堆栈错位 8,因为 return 指针被 呼叫。要重新对齐到 16 字节边界,您可以简单地 PUSH any 通用寄存器到堆栈和 POP最后关闭。
调用约定还要求AL包含用于可变参数函数的向量寄存器的数量:
%al is used to indicate the number of vector arguments passed to a function requiring a variable number of arguments
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 , %esi # Writing to ESI zero extends to RSI.
xor %eax, %eax # Zeroing EAX is efficient way to clear AL.
call printf
pop %rbx
ret
其他Recommendations/Suggestions
您还应该从 64 位 Linux ABI 了解到,调用约定还需要您编写的函数来遵守某些寄存器的保存。寄存器列表及是否保留如下:
任何在 Preserved 中显示 Yes
的寄存器
function calls 列是您必须确保在整个函数中保留的那些。函数 main
与任何其他 C 函数一样。
如果您知道 strings/data 是只读的,您可以将它们放在 .rodata
部分,并使用 .section .rodata
而不是 .data
在 64 位模式下:如果您的目标操作数是 32 位寄存器,则 CPU 会将寄存器零扩展到整个 64 位寄存器。这可以节省指令编码的字节数。
您的可执行文件可能被编译为与位置无关的代码。您可能会收到类似于以下内容的错误:
relocation R_X86_64_PC32 against symbol `printf@@GLIBC_2.2.5' can not be used when making a shared object; recompile with -fPIC
要解决此问题,您必须这样调用外部函数 printf
:
call printf@plt
调用外部库函数
您可以查看从等效的 c 文件生成的汇编代码。
运行 gcc -o - -S -fno-asynchronous-unwind-tables test.c
与 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 , %esi
movl $.LC0, %edi
movl [=11=], %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 [=15=], %eax
应用这些修改将得到如下结果:
.data
format: .ascii "%d\n"
.text
.global main
main:
mov $format, %rdi
mov , %rsi
mov [=12=], %eax
call printf
ret
然后 运行 它打印:
1
我使用 AT&T 语法编写了一个程序用于 GNU assembler:
.data
format: .ascii "%d\n"
.text
.global main
main:
mov $format, %rbx
mov (%rbx), %rdi
mov , %rsi
call printf
ret
我将 GCC 用于 assemble 和 link 以及:
gcc -o main main.s
我运行它用这个命令:
./main
当我 运行 程序出现段错误时。通过使用 gdb,它说 printf
未找到。我试过“.extern printf”,它不起作用。有人建议我应该在调用 printf
之前存储堆栈指针并在 RET 之前恢复,我该怎么做?
这段代码有很多问题。 Linux 使用的 AMD64 System V ABI 调用约定需要一些东西。它要求在 CALL 之前堆栈至少对齐 16 字节(或 32 字节):
The end of the input argument area shall be aligned on a 16 (32, if __m256 is passed on stack) byte boundary.
在 C 运行时调用您的 main
函数后,堆栈错位 8,因为 return 指针被 呼叫。要重新对齐到 16 字节边界,您可以简单地 PUSH any 通用寄存器到堆栈和 POP最后关闭。
调用约定还要求AL包含用于可变参数函数的向量寄存器的数量:
%al is used to indicate the number of vector arguments passed to a function requiring a variable number of arguments
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 , %esi # Writing to ESI zero extends to RSI.
xor %eax, %eax # Zeroing EAX is efficient way to clear AL.
call printf
pop %rbx
ret
其他Recommendations/Suggestions
您还应该从 64 位 Linux ABI 了解到,调用约定还需要您编写的函数来遵守某些寄存器的保存。寄存器列表及是否保留如下:
任何在 Preserved 中显示 Yes
的寄存器
function calls 列是您必须确保在整个函数中保留的那些。函数 main
与任何其他 C 函数一样。
如果您知道 strings/data 是只读的,您可以将它们放在 .rodata
部分,并使用 .section .rodata
而不是 .data
在 64 位模式下:如果您的目标操作数是 32 位寄存器,则 CPU 会将寄存器零扩展到整个 64 位寄存器。这可以节省指令编码的字节数。
您的可执行文件可能被编译为与位置无关的代码。您可能会收到类似于以下内容的错误:
relocation R_X86_64_PC32 against symbol `printf@@GLIBC_2.2.5' can not be used when making a shared object; recompile with -fPIC
要解决此问题,您必须这样调用外部函数 printf
:
call printf@plt
调用外部库函数
您可以查看从等效的 c 文件生成的汇编代码。
运行 gcc -o - -S -fno-asynchronous-unwind-tables test.c
与 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 , %esi
movl $.LC0, %edi
movl [=11=], %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 [=15=], %eax
应用这些修改将得到如下结果:
.data
format: .ascii "%d\n"
.text
.global main
main:
mov $format, %rdi
mov , %rsi
mov [=12=], %eax
call printf
ret
然后 运行 它打印:
1