编译以下代码时:
global main
extern printf, scanf
section .data
msg: db "Enter a number: ",10,0
format:db "%d",0
section .bss
number resb 4
section .text
main:
mov rdi, msg
mov al, 0
call printf
mov rsi, number
mov rdi, format
mov al, 0
call scanf
mov rdi,format
mov rsi,[number]
inc rsi
mov rax,0
call printf
ret
使用:
nasm -f elf64 example.asm -o example.o
gcc -no-pie -m64 example.o -o example
然后运行
./example
运行,打印:输入数字: 但随后崩溃并打印: Segmentation fault (core dumped)
所以 printf 工作正常但 scanf 不行。 我对 scanf 做错了什么?
在函数的开始/结束处使用
sub rsp, 8
/ add rsp, 8
在函数执行 call
. 之前将堆栈重新对齐到 16 个字节
或者更好地推送/弹出虚拟寄存器,例如
push rdx
/ pop rcx
,或者你实际上想要保存的像 RBP 这样的呼叫保留寄存器。 您需要对 RSP 的总更改是 8 的奇数倍,计算所有推送和 sub rsp
, 从函数入口到任何 call
.8 + 16*n
字节为整数 n
.
在函数入口处,RSP 距离 16 字节对齐有 8 个字节,因为
call
推送了一个 8 字节的返回地址。请参阅从 x86-64 打印浮点数似乎需要保存 %rbp,
main 和堆栈对齐,以及使用 GNU 汇编程序在 x86_64 中调用 printf。这是一个 ABI 要求,当 printf 没有任何 FP args 时,您过去可以避免违反。但现在不是了。
另请参阅为什么 x86-64 / AMD64 System V ABI 要求 16 字节堆栈对齐?
换句话说,
RSP % 16 == 8
在功能入口上,你需要在你RSP % 16 == 0
一个功能之前确保call
。你如何做到这一点并不重要。 (如果你不这样做,并不是所有的功能都会崩溃,但 ABI 确实需要/保证它。)
gcc 的 glibc scanf 代码生成现在取决于 16 字节堆栈对齐
即使当
AL == 0
.
它似乎在
__GI__IO_vfscanf
中的某处自动矢量化复制了 16 个字节,在将其寄存器参数溢出到堆栈1后,常规
scanf
调用。 (调用 scanf 的许多类似方法共享一个大实现作为各种 libc 入口点的后端,如 scanf
、fscanf
等)
我下载了 Ubuntu 18.04 的 libc6 二进制包:https://packages.ubuntu.com/bionic/amd64/libc6/download 并解压文件(使用
7z x blah.deb
和 tar xf data.tar
,因为 7z 知道如何解压很多文件格式)。
我可以用
LD_LIBRARY_PATH=/tmp/bionic-libc/lib/x86_64-linux-gnu ./bad-printf
重现你的错误,而且它在我的 Arch Linux 桌面上用系统 glibc 2.27-3 证明了。
使用 GDB,我在你的程序上运行了它,然后做了
set env LD_LIBRARY_PATH /tmp/bionic-libc/lib/x86_64-linux-gnu
然后run
。使用 layout reg
,反汇编窗口在收到 SIGSEGV 时看起来像这样:
│0x7ffff786b49a <_IO_vfscanf+602> cmp r12b,0x25 │
│0x7ffff786b49e <_IO_vfscanf+606> jne 0x7ffff786b3ff <_IO_vfscanf+447> │
│0x7ffff786b4a4 <_IO_vfscanf+612> mov rax,QWORD PTR [rbp-0x460] │
│0x7ffff786b4ab <_IO_vfscanf+619> add rax,QWORD PTR [rbp-0x458] │
│0x7ffff786b4b2 <_IO_vfscanf+626> movq xmm0,QWORD PTR [rbp-0x460] │
│0x7ffff786b4ba <_IO_vfscanf+634> mov DWORD PTR [rbp-0x678],0x0 │
│0x7ffff786b4c4 <_IO_vfscanf+644> mov QWORD PTR [rbp-0x608],rax │
│0x7ffff786b4cb <_IO_vfscanf+651> movzx eax,BYTE PTR [rbx+0x1] │
│0x7ffff786b4cf <_IO_vfscanf+655> movhps xmm0,QWORD PTR [rbp-0x608] │
>│0x7ffff786b4d6 <_IO_vfscanf+662> movaps XMMWORD PTR [rbp-0x470],xmm0 │
所以它将两个 8 字节的对象复制到堆栈中,
movq
+ movhps
加载和 movaps
存储。但是由于堆栈未对齐,movaps [rbp-0x470],xmm0
故障。
我没有抓取调试版本来找出 C 源代码的哪一部分变成了这个,但该函数是用 C 编写的,并由启用了优化的 GCC 编译。 GCC 一直被允许这样做,但直到最近它才变得足够聪明,可以通过这种方式更好地利用 SSE2。
脚注 1:带有
AL != 0
的 printf / scanf 始终需要 16 字节对齐,因为 gcc 的可变参数函数代码生成使用 test al,al / je 溢出完整的 16 字节 XMM regs xmm0..7 并对齐存储在那种情况。 __m128i
可以是可变参数函数的参数,而不仅仅是 double
,并且 gcc 不会检查该函数是否实际读取任何 16 字节的 FP args。