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

时间:2018-09-12 03:47:51

标签: gcc x86 linker gdb

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

我有以下测试代码:

.data
intfmt:         .string "int: %d\n"
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的方式打印ints并浮动。

进入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

1 个答案:

答案 0 :(得分:2)

i386 System V ABI做保证/要求call之前必须进行16字节的堆栈对齐,就像我在回答的开头回答的那样。 (除非您要调用私有帮助器函数,否则可以编写自己的对齐,arg传递规则以及该函数的寄存器被破坏。)

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

函数的性能可能取决于堆栈对齐方式(对齐doubledouble的数组以避免访问时发生高速缓存行拆分)。

通常,唯一的函数依赖于正确性的堆栈对齐的情况是编译为使用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?。)