linux glibc scanf从未对齐RSP的函数调用时出现分段错误

7lrncoxx  于 2022-11-28  发布在  Linux
关注(0)|答案(1)|浏览(127)

编译以下代码时:

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

它运行,打印:**输入一个数字:**但随后崩溃并打印:分段错误(核心转储)
所以printf工作正常,但scanf不是。我用scanf so做错了什么?

idv4meu8

idv4meu81#

在函式的开头/结尾使用sub rsp, 8/add rsp, 8,在函式执行call之前,将堆栈重新对齐至16个字节。

或者更好地压入/弹出一个虚拟寄存器,例如push rdx/pop rcx,或者一个调用保留寄存器,如RBP,你实际上无论如何都想保存。**你需要对RSP的总改变是8的奇数倍,包括所有压入和sub rsp,**从函数入口到任何call
即整数x1M8 N1 x的x1M7 N1 x字节。
在函数入口处,RSP距离16字节对齐还有8字节,因为call推送了一个8字节的返回地址。参见Printing floating point numbers from x86-64 seems to require %rbp to be savedmain and stack alignmentCalling 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在你call一个函数之前。你怎么做并不重要。(如果你不这样做,并不是所有的函数都会崩溃,但是ABI确实要求/保证了这一点。)

**gcc的glibc scanf代码生成现在依赖于16字节堆栈对齐

即使当AL == 0**.
它似乎在__GI__IO_vfscanf中的某个地方自动矢量化复制了16个字节,常规的scanf在将其寄存器参数溢出到stack 1后调用它。(许多类似的调用scanf的方法共享一个大的实现,作为scanffscanf等各种libc入口点的后端。)
我下载了Ubuntu 18.04的libc 6二进制包:https://packages.ubuntu.com/bionic/amd64/libc6/download并提取了文件(用7z x blah.debtar xf data.tar,因为7z知道如何提取很多文件格式)。
我可以用LD_LIBRARY_PATH=/tmp/bionic-libc/lib/x86_64-linux-gnu ./bad-printf来重现你的bug,而且它也可以用我的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编写的,由愚者编译并启用了优化。愚者一直被允许这样做,但直到最近它才变得足够聪明,以这种方式更好地利用了SSE 2。
脚注一:带有AL != 0的printf / scanf始终要求16字节对齐,因为gcc的可变函数的代码生成使用test al,al / je来溢出带有对齐存储的完整16字节XMM寄存器xmm0..7。__m128i可以是可变函数的参数,而不仅仅是double,并且gcc不检查该函数是否实际读取过任何16字节的FP参数。

相关问题