编译以下代码时:
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
它运行,打印: enter a number: 但随后崩溃并打印: Segmentation fault (core dumped)
所以printf工作正常,但扫描不行 . 我怎么在scanf上做错了?
1 回答
Use sub rsp, 8 / add rsp, 8 at the start/end of your function 在函数执行
call
之前将堆栈重新对齐到16个字节 .或者更好地推/弹一个虚拟寄存器,例如
push rdx
/pop rcx
,或者保存/恢复像RBP这样的保持呼叫的寄存器 .在函数输入时,RSP与16字节对齐相距8个字节,因为
call
推送了一个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 args时违反规定 . 但不是了 .gcc's code-gen for glibc scanf now depends on 16-byte stack alignment even when AL == 0 .
它似乎在
__GI__IO_vfscanf
中的某处自动向量化复制16个字节,在将其寄存器args溢出到stack1之后定期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的位置看起来像这样:因此,它将两个8字节对象复制到堆栈,
movq
movhps
加载,movaps
存储 . 但由于堆栈未对齐,movaps [rbp-0x470],xmm0
故障 .我没有 grab 调试版本来确切地知道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 .