linux - 从不对齐 RSP 的函数调用时,glibc scanf 出现段错误

标签 linux assembly nasm x86-64 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

使用:

nasm -f elf64 example.asm -o example.o
gcc -no-pie -m64 example.o -o example

然后运行

./example

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

所以 printf 工作正常但 scanf 不行。 我对 scanf 做错了什么?

最佳答案

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

或者更好地压入/弹出一个虚拟寄存器,例如push rdx/pop rcx,或者你实际上想要保存的调用保留寄存器,如 RBP。 您需要对 RSP 的总更改是 8 的奇数倍,计算所有推送和sub rsp从函数入口到任何调用。< br/> 即 8 + 16*n 字节为整数 n

在函数入口处,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 时,您过去可以避免违反。但现在不是了。

另见 Why does the x86-64 / AMD64 System V ABI mandate a 16 byte stack alignment?

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


gcc 的 glibc scanf 代码生成现在取决于 16 字节堆栈对齐
即使 AL == 0

它似乎在 __GI__IO_vfscanf 中的某处自动向量化复制了 16 个字节,在将其寄存器参数溢出到堆栈1scanf 调用>。 (调用 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 来重现你的错误,结果也是 glibc 2.27-3 系统在我的 Arch Linux 桌面上。

使用 GDB,我在您的程序上运行它并set env LD_LIBRARY_PATH/tmp/bionic-libc/lib/x86_64-linux-gnu 然后运行。使用 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 编写的,并由启用了优化的 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。

关于linux - 从不对齐 RSP 的函数调用时,glibc scanf 出现段错误,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51070716/

相关文章:

linux - sed 在每 4 行的末尾添加逗号

video - 汇编 x86 视频模式

linux - malloc 和免费的 x86 NASM + 编译

swift - 如何解释汇编调用堆栈中的快速(或一般)闭包?

c++ - 是否可以在 C++ 结构中编写静态程序集数据?

assembly - 带累加器的 NASM 范围宏

linux - 将非 PIC 对象链接到具有 PIC 对象的可执行文件是否有效

linux - 如何使用 Cinnamon 中的键盘快捷键将窗口捕捉到屏幕中间?

linux - 无法从 ubuntu 完全卸载 Postgres

linux - ARM 汇编添加