首页 文章

glibc scanf从不对齐RSP的函数调用时的分段错误

提问于
浏览
2

编译以下代码时:

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 回答

  • 4

    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 savedmain and stack alignmentCalling 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入口点的后端,如 scanffscanf 等)

    我下载了Ubuntu 18.04的libc6二进制包: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 重新编写您的错误,而且我的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 故障 .

    我没有 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 .

相关问题