【问题标题】:gcc x86-32 stack alignment and calling printfgcc x86-32 堆栈对齐和调用 printf
【发布时间】:2018-09-12 03:47:51
【问题描述】:

据我所知,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 构建。我知道系统调用 write 存在,但据我所知,它不能像 printf 那样打印整数和浮动。

进入main后,栈上有一个4字节的返回地址。然后天真地解释这段代码,两个 push 调用每个都将 4 个字节放在堆栈上,所以 call 需要另一个 4 字节的值 push 才能对齐。

这里是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。

【问题讨论】:

  • mov %esp, %ebp 不保存/恢复调用者的%ebp 是不好的,很容易在主返回后导致段错误。
  • 用gcc -O1 -fverbose-asm -S 编译你的C 代码以获得汇编代码。阅读相关的x86 ABI 规范
  • @BasileStarynkevitch 我正在为学习目的编写程序集。我想尽可能多地保留我的原始程序集,我担心-O1 会摆脱它们。
  • 然后删除-O1。请注意,汇编程序是由gcc 从一些foo.c 源代码生成 到gcc -fverbose-asm -S foo.c 之后的foo.s(您可以添加-O1)。我确实提到了 C 代码(不是汇编程序)以了解 gcc 正在生成什么样的汇编程序代码
  • @BasileStarynkevitch 我仍然不确定我是否理解。我使用as 创建目标文件和gcc 创建可执行文件,根本没有C 源代码?

标签: gcc x86 linker gdb


【解决方案1】:

i386 System V ABI确实保证/要求在 call 之前进行 16 字节堆栈对齐,就像我在您链接的答案顶部所说的那样。 (除非您正在调用私有辅助函数,在这种情况下,您可以制定自己的对齐规则、参数传递以及该函数的哪些寄存器被破坏。)

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

函数可能依赖于堆栈对齐来提高性能(对齐 double 或 doubles 的数组以避免在访问它们时缓存行拆分)。

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

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

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


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

0x14(十进制 20)是该位置内存中的值。它将在运行时加载,因为您使用了push r/m32,而不是push $20(或像.equ testint, 20 或testint = 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?。)

【讨论】:

  • 动态链接过程中是否会替换 0x2014 和 0x201d 值?我使用的是默认的 Ubuntu 18.04 和 printf 附带的任何东西。
  • @qwr:是的,你制作了一个 PIE 可执行文件,因为这是最近 Ubuntu 上的 gcc 默认值。
  • 感谢您对 PIE 的引用,这正是我想要的。
猜你喜欢
  • 2017-01-04
  • 2021-03-03
  • 2021-12-16
  • 1970-01-01
  • 1970-01-01
  • 2014-03-11
  • 2011-08-24
  • 2011-02-15
  • 1970-01-01
相关资源
最近更新 更多