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

use*_*544 2 linux assembly x86-64 nasm calling-convention

编译以下代码时:

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
Run Code Online (Sandbox Code Playgroud)

使用:

nasm -f elf64 example.asm -o example.o
gcc -no-pie -m64 example.o -o example
Run Code Online (Sandbox Code Playgroud)

然后运行

./example
Run Code Online (Sandbox Code Playgroud)

它运行,打印:输入数字: 但随后崩溃并打印: 分段错误(核心已转储)

因此,printf可以正常工作,而scanf则不能。我对scanf有什么错呢?

Pet*_*des 5

在函数的开始/结尾使用sub rsp, 8/ add rsp, 8将函数重新对齐到16个字节,然后函数执行a call

或者最好压入/弹出一个虚拟寄存器,例如push rdx/ pop rcx,或保存/恢复一个调用保留的寄存器,如RBP。

在函数输入时,RSP与16字节对齐方式相距8字节,因为call推送了8字节的返回地址。请参阅从x86-64打印浮点数似乎需要保存%rbp主对齐和堆栈对齐,以及使用GNU汇编器在x86_64中调用printf。这是一个ABI要求,您过去可以在没有printf的FP arg的情况下避免违反。但是没有更多了。


gcc的glibc scanf的代码生成现在依赖于16字节的堆栈对齐方式,即使在时也是如此AL == 0

似乎已经在__GI__IO_vfscanf其中某个位置自动向量化了16个字节,scanf在将其寄存器args溢出到堆栈1之后会进行常规调用。(许多类似的方式来调用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                                                                          ?
Run Code Online (Sandbox Code Playgroud)

因此,它使用movq+ 将两个8字节对象复制到堆栈中以movhps进行加载和movaps存储。但是随着堆栈未对齐,出现了movaps [rbp-0x470],xmm0故障。

我没有找到确切的C语言源代码的调试版本,但是该函数是用C编写的,并由启用了优化功能的GCC编译。一直允许GCC这样做,但直到最近才使GCC变得足够聪明,可以通过这种方式更好地利用SSE2。


脚注1:printf / scanf AL != 0始终需要16字节对齐,因为在这种情况下,gcc可变参数功能的代码生成使用test al,al / je来溢出完整的16字节XMM reg xmm0..7与对齐的存储区。 __m128i可以是可变参数函数的参数,而不仅仅是double,并且gcc不会检查该函数是否实际读取任何16字节FP args。