gcc x86-32堆栈对齐并调用printf

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了gcc x86-32堆栈对齐并调用printf相关的知识,希望对你有一定的参考价值。

据我所知,x86-64要求堆栈在调用之前是16字节对齐,而gcc with -m32 doesn't require this for main

我有以下测试代码:

.data
intfmt:         .string "int: %d
"
testint:        .int    20

.text
.globl main

main:
    mov     %esp, %ebp
    push    testint
    push    $intfmt
    call    printf
    mov     %ebp, %esp
    ret

as --32 test.S -o test.o && gcc -m32 test.o -o test构建。我知道syscall写存在,但据我所知它不能打印整数和浮动printf的方式。

进入main后,堆栈上有一个4字节的返回地址。然后天真地解释这个代码,两个push调用各自在堆栈上放置4个字节,因此调用需要另一个4字节值推送对齐。

这是gas和gcc生成的二进制文件的objdump:

0000053d <main>:
 53d:   89 e5                   mov    %esp,%ebp
 53f:   ff 35 1d 20 00 00       pushl  0x201d
 545:   68 14 20 00 00          push   $0x2014
 54a:   e8 fc ff ff ff          call   54b <main+0xe>
 54f:   89 ec                   mov    %ebp,%esp
 551:   c3                      ret    
 552:   66 90                   xchg   %ax,%ax
 554:   66 90                   xchg   %ax,%ax
 556:   66 90                   xchg   %ax,%ax
 558:   66 90                   xchg   %ax,%ax
 55a:   66 90                   xchg   %ax,%ax
 55c:   66 90                   xchg   %ax,%ax
 55e:   66 90                   xchg   %ax,%ax

我对生成的推送指令非常困惑。

  1. 如果按下两个4字节值,如何实现对齐?
  2. 为什么要推0x2014而不是0x14?什么是0x201d?
  3. call 54b甚至实现了什么? hd的输出与objdump匹配。为什么这在gdb中有所不同?这是动态链接器吗?

B+>│0x5655553d <main>                       mov    %esp,%ebp                      │
   │0x5655553f <main+2>                     pushl  0x5655701d                     │
   │0x56555545 <main+8>                     push   $0x56557014                    │
   │0x5655554a <main+13>                    call   0xf7e222d0 <printf>            │
   │0x5655554f <main+18>                    mov    %ebp,%esp                      │
   │0x56555551 <main+20>                    ret  

关于实际执行二进制文件时所发生的事情的资源是值得赞赏的,因为我不知道实际发生了什么以及我读过的教程没有涵盖它。我正在通过How programs get run: ELF binaries阅读。

答案

i386 System V ABI在call之前保证/需要16字节堆栈对齐,就像我在你回答的链接中所说的那样。 (除非您正在调用私有帮助程序函数,在这种情况下,您可以组成自己的对齐规则,arg传递以及哪些寄存器被该函数破坏。)

如果违反此ABI要求,则允许函数崩溃或行为异常,但不是必需的。例如scanf在x86-64 Ubuntu glibc(由最近的gcc编译)最近才开始这样做:scanf Segmentation faults when called from a function that doesn't change RSP

函数可以依赖于堆栈对齐的性能(对齐doubledoubles数组以避免访问时的缓存行拆分)。

通常,函数依赖于堆栈对齐以确保正确性的唯一情况是在编译时使用SSE / SSE2,因此它可以使用16字节对齐所需的加载/存储来复制结构或数组(movapsmovdqa),或实际在本地数组上自动矢量化循环。

我认为Ubuntu不会使用SSE编译他们的32位库(除了使用运行时调度的memcpy之类的函数),因此他们仍然可以使用像Pentium II这样的古老CPU。 x86-64系统上的多树库应该采用SSE2,但是使用4字节指针时,32位函数不太可能有16字节结构要复制。

无论如何,无论什么原因,显然你的32位构建的glibc中的printf实际上并不依赖于16字节堆栈对齐的正确性,因此即使你的堆栈不对齐也不会出错。


为什么要推0x2014而不是0x14?什么是0x201d?

0x14(十进制20)是该位置的内存中的值。它将在运行时加载,因为你使用的是push r/m32,而不是push $20(或像.equ testint, 20testint = 20那样的汇编时间常数)。

您使用gcc -m32创建一个PIE(位置无关可执行文件),它在运行时重新定位,因为这是Ubuntu的gcc上的默认值。

0x2014是相对于文件开头的偏移量。如果在运行程序后在运行时进行反汇编,则会看到一个真实的地址。

call 54b也是如此。它是对PLT的调用(它靠近文件/文本段的开头,因此是低地址)。

如果你用objdump -drwC反汇编,你会看到符号重定位信息。 (我也喜欢-Mintel,但要注意它是MASM,而不是NASM)。

您可以链接gcc -m32 -no-pie以制作经典的位置相关可执行文件。我肯定建议特别是对于32位代码,特别是如果你正在编译C,使用gcc -m32 -no-pie -fno-pie来获取非PIE代码以及链接到非PIE可执行文件。 (有关PIE的更多信息,请参阅32-bit absolute addresses no longer allowed in x86-64 Linux?。)

以上是关于gcc x86-32堆栈对齐并调用printf的主要内容,如果未能解决你的问题,请参考以下文章

使用 GCC 但没有使用 Clang 的堆栈帧太大(过度对齐?)

为啥 gcc 4.x 在调用方法时默认为 linux 上的堆栈保留 8 个字节?

针对不同缓冲区大小的不同内存对齐

GCC - 如何重新对齐堆栈?

即使没有对齐,GCC 分配的堆栈空间也比本地所需的更多。空间有啥用?

如何在 GCC 的 32 字节边界处对齐堆栈?