提问者:小点点

GliBC scanf从不对齐RSP的函数调用时出现分割错误


编译以下代码时:

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有什么错?


共1个答案

匿名用户

使用sub rsp,8/在函数的开始/结束处添加rsp,8,以便在函数执行调用之前将堆栈重新对齐到16个字节。

或者更好地推送/pop一个虚拟寄存器,例如push rdx/pop rcx,或者像RBP这样您实际上想要保存的调用保留寄存器。您需要对RSP的总更改为8的奇数倍,计算所有推送和sub rsp,从函数条目到任何调用
8 16*n字节表示整数n

在函数输入中,RSP与16字节对齐相差8个字节,因为调用推送了一个8字节的返回地址。请参阅从x86-64打印浮点数似乎需要保存%rbp、主和堆栈对齐以及使用GNU汇编器在x86_64中调用printf。这是一个ABI的要求,当printf没有任何FP参数时,您曾经能够逃脱违反。但不再是了。

另请参阅为什么x86-64/AMD64 System VABI要求16字节堆栈对齐?

换句话说,函数输入上的RSP%16==8,在调用函数之前,您需要确保RSP%16==0。如何做到这一点并不重要。(如果你不这样做,并非所有函数都会真正崩溃,但ABI确实需要/保证它。)

gcc用于glibc scanf的code-gen现在依赖于16字节堆栈对齐
,即使AL==0

它似乎在__GI__IO_vfscanf中的某个地方自动向量化复制了16个字节,常规scanf在将其寄存器参数泄漏到堆栈1后调用这些字节。(许多类似的调用scanf的方法共享一个大实现作为各种libc入口点的后端,如scanffscanf等)

我下载了Ubuntu 18.04的libc6二进制包:https://packages.ubuntu.com/bionic/amd64/libc6/download并提取了文件(使用7z x blah. debtar xf data.tar,因为7z知道如何提取大量文件格式)。

我可以用重现你的bugLD_LIBRARY_PATH=/tmp/bionic-libc/lib/x86_64-linux-gnu./bad-printf,而且它在我的ArchLinux桌面上显示为系统glibc 2.27-3。

使用GDB,我在您的程序上运行它,并在设置envLD_LIBRARY_PATH /tmp/bionic-libc/lib/x86_64-linux-gnu,然后运行。使用布局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字节的对象复制到堆栈中,用movqmovhps加载和movaps存储。但是随着堆栈的错位,movaps[rbp-0x470], xmm0出现故障。

我没有抓住调试版本来找出C源代码的哪一部分变成了这个,但是这个函数是用C编写的,由启用优化的GCC编译。GCC一直被允许这样做,但是直到最近它才变得足够聪明,以这种方式更好地利用SSE2。

脚注1:带有AL!=0的printf/scanf总是需要16字节对齐,因为gcc的可变函数的code-gen使用test al, al/je来溢出完整的16字节XMM regs xmm0…7,在这种情况下,对齐存储。__m128i可以是可变函数的参数,而不仅仅是double,gcc不会检查该函数是否真的读取了任何16字节FPargs。