在函数的开头/结尾使用sub rsp, 8
/add rsp, 8
将栈重新对齐到16字节,然后再执行call
。或者更好地推荐,可以像push rdx
/pop rcx
这样推入/弹出一个虚拟寄存器,或者另外一个需要保存的调用保留寄存器,比如RBP。需要注意的是,从函数入口到任何call
,包括所有的push和sub rsp
,RSP的总改变量应该是8的奇数倍。也就是说,对于整数n
,总共需要8 + 16*n
个字节。
在函数入口时,由于call
已经推入了一个8字节的返回地址,所以RSP距离16字节对齐还有8字节的差距。请参阅Printing floating point numbers from x86-64 seems to require %rbp to be saved、main and stack alignment和Calling printf in x86_64 using GNU assembler等文章,这是ABI的要求,如果没有printf的FP参数,你可能可以不遵守这个要求。但是现在已经不行了。
另外请参阅Why does the x86-64 / AMD64 System V ABI mandate a 16 byte stack alignment?
换句话说,在函数入口时,RSP % 16 == 8
,在调用函数之前,必须确保RSP % 16 == 0
,如何实现这一点并不重要。注意,并不是所有的函数在不遵守这个规则的情况下都会崩溃,但ABI要求和保证这样做。
即使AL == 0
,gcc为glibc scanf生成的代码现在也依赖于16字节的栈对齐。
它似乎在__GI__IO_vfscanf
中的某个地方自动向量化复制了16个字节,这是常规的scanf
在将其寄存器参数溢出到栈后调用的函数。1 (像scanf
、fscanf
等这样的libc入口点共享一个大型的实现作为后端)。
我下载了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桌面上也会出现这个问题。
使用GDB,在您的程序上运行set env LD_LIBRARY_PATH /tmp/bionic-libc/lib/x
<code> │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 │
</code>
使用movq
+movhps
进行加载,movaps
进行存储,将两个8字节对象复制到堆栈中。但由于堆栈未对齐,movaps [rbp-0x470],xmm0
出现错误。
我没有获取调试版本来找出C源代码的哪个部分变成了这个样子,但该函数是用C编写并由启用优化的GCC编译的。GCC一直允许这样做,但仅最近它变得聪明起来,以更好地利用SSE2。
脚注1:printf/ scanf如果使用AL!=0
一直需要16字节对齐,因为gcc对可变参数函数的代码生成使用 test al,al / je 通过对齐存储完整的16字节XMM寄存器 xmm0..7。 __m128i
可以作为可变参数函数的参数,不仅仅是double
,gcc不检查函数是否实际读取任何16字节的FP参数。
number
为4个字节时,mov rsi,[number]
将8个字节从number移动到RSI。也许您想要使用mov esi,[number]
。 - Michael Petchmain:
之后添加push rbp
并在ret
之前添加pop rbp
(这应该解决对齐问题),会发生什么情况?将mov rsi,[number]
更改为mov esi,[number]
。GDB或任何调试器都是理想的工具。如果调试器让你感到害怕,那么汇编可能不是你应该编程的东西。 - Michael Petch