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

11

编译以下代码时:

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时做错了什么?


3
在调用_AMD64 ABI_的_C_库例程之前,您可能存在堆栈未对齐到16字节边界的潜在问题。当number为4个字节时,mov rsi,[number]将8个字节从number移动到RSI。也许您想要使用mov esi,[number] - Michael Petch
2
仅仅因为一个答案被接受并不意味着它没有问题。如果在 main: 之后添加 push rbp 并在 ret 之前添加 pop rbp(这应该解决对齐问题),会发生什么情况?将 mov rsi,[number] 更改为 mov esi,[number]。GDB或任何调试器都是理想的工具。如果调试器让你感到害怕,那么汇编可能不是你应该编程的东西。 - Michael Petch
1
推送 rbp 和弹出 rbp 可以解决问题。非常感谢! - user3057544
1
Ubuntu 18.04(Bionic Beaver)64位 - user3057544
1
EDB调试器现在不是已经可以在18.04的宇宙软件源中获取了吗?去看看吧,它可能有更简单的用户界面,虽然它不像GDB那样强大和多功能。如果你只是计划用短代码片段学习汇编基础知识,那么EDB应该足够了。(如果它还没有在软件源中,你就需要从源代码[从Github]编译,但这也不完全容易)。 - Ped7g
显示剩余2条评论
1个回答

16

在函数的开头/结尾使用sub rsp, 8/add rsp, 8将栈重新对齐到16字节,然后再执行call。或者更好地推荐,可以像push rdx/pop rcx这样推入/弹出一个虚拟寄存器,或者另外一个需要保存的调用保留寄存器,比如RBP。需要注意的是,从函数入口到任何call包括所有的push和sub rsp,RSP的总改变量应该是8的奇数倍。也就是说,对于整数n,总共需要8 + 16*n个字节。

在函数入口时,由于call已经推入了一个8字节的返回地址,所以RSP距离16字节对齐还有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,如何实现这一点并不重要。注意,并不是所有的函数在不遵守这个规则的情况下都会崩溃,但ABI要求和保证这样做。


即使AL == 0,gcc为glibc scanf生成的代码现在也依赖于16字节的栈对齐。

它似乎在__GI__IO_vfscanf中的某个地方自动向量化复制了16个字节,这是常规的scanf在将其寄存器参数溢出到栈后调用的函数。1 (像scanffscanf等这样的libc入口点共享一个大型的实现作为后端)。

我下载了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桌面上也会出现这个问题。

使用GDB,在您的程序上运行set env LD_LIBRARY_PATH /tmp/bionic-libc/lib/x

<code>   │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                                                                          │
</code>

使用movq+movhps进行加载,movaps进行存储,将两个8字节对象复制到堆栈中。但由于堆栈未对齐,movaps [rbp-0x470],xmm0 出现错误。

我没有获取调试版本来找出C源代码的哪个部分变成了这个样子,但该函数是用C编写并由启用优化的GCC编译的。GCC一直允许这样做,但仅最近它变得聪明起来,以更好地利用SSE2。


脚注1:printf/ scanf如果使用AL!=0一直需要16字节对齐,因为gcc对可变参数函数的代码生成使用 test al,al / je 通过对齐存储完整的16字节XMM寄存器 xmm0..7。 __m128i可以作为可变参数函数的参数,不仅仅是double,gcc不检查函数是否实际读取任何16字节的FP参数。


非常有趣。openSuSE在没有对齐的情况下没有问题(gcc 4.8.5),但Arch确实会出现SegFaults(gcc 8.1.1)。确保16字节对齐工作正常。 - David C. Rankin
@DavidC.Rankin:这只是最近在Arch上发生的变化。 - Peter Cordes
PS:jmp scanf 到尾部调用(类似于调用 scanf/ret)当然要求 RSP%16==8,而不是对齐,以复制预期的函数输入布局。而且一如既往的,仅在 RSP 指向自己的返回地址时 jmp 尾调用才有效,因此 scanf 将获得该返回地址。所以只有从已经使用 16 字节 RSP 对齐调用的函数中才能进行尾调用。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接