【问题标题】:Why does GCC pad functions with NOPs?为什么 GCC 使用 NOP 填充功能?
【发布时间】:2011-12-16 06:46:14
【问题描述】:

我使用 C 语言已经有一段时间了,最​​近才开始接触 ASM。当我编译一个程序时:

int main(void)
  {
  int a = 0;
  a += 1;
  return 0;
  }

objdump 反汇编有代码,但在 ret 之后没有:

...
08048394 <main>:
 8048394:       55                      push   %ebp
 8048395:       89 e5                   mov    %esp,%ebp
 8048397:       83 ec 10                sub    $0x10,%esp
 804839a:       c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%ebp)
 80483a1:       83 45 fc 01             addl   $0x1,-0x4(%ebp)
 80483a5:       b8 00 00 00 00          mov    $0x0,%eax
 80483aa:       c9                      leave  
 80483ab:       c3                      ret    
 80483ac:       90                      nop
 80483ad:       90                      nop
 80483ae:       90                      nop
 80483af:       90                      nop
...

据我所知 nops 什么都不做,因为在 ret 之后甚至不会被执行。

我的问题是:为什么要打扰? ELF(linux-x86) 不能与任何大小的 .text 部分(+main) 一起使用吗?

我很感激任何帮助,只是想学习。

【问题讨论】:

  • 那些 NOP 会继续吗?如果它们停在80483af,那么可能是填充将下一个函数对齐到 8 或 16 个字节。
  • no 在 4 个 nops 之后,它进入一个函数:__libc_csu_fini
  • 如果 NOP 是由 gcc 插入的,那么我认为它不会只使用 0x90,因为有很多 NOP 的大小变量来自 1-9 bytes(如果使用 gas syntax,则为 10)

标签: c assembly gcc memory-alignment


【解决方案1】:

据我所知,指令在 cpu 中流水线化,不同的 cpu 块(加载器、解码器等)处理后续指令。在执行RET 指令时,很少有下一条指令已加载到 cpu 管道中。这是一个猜测,但您可以从这里开始挖掘,如果您发现(可能是安全的NOPs 的具体数量,请分享您的发现。

【讨论】:

  • @ninjalj:嗯?这个问题是关于 x86 的问题,它是流水线的(正如 mco 所说)。许多现代 x86 处理器还推测性地执行“不应该”执行的指令,可能包括这些 nop。也许您打算在其他地方发表评论?
  • @DavidCary:在 x86 中,这对程序员来说是完全透明的。错误猜测的推测性执行指令只会丢弃其结果和效果。在 MIPS 上,根本没有“推测”部分,总是执行分支延迟槽中的指令,程序员必须填充延迟槽(或者让汇编器来做,这可能会导致 nop s)。
  • @ninjalj:是的,错误猜测的推测性执行操作和非对齐指令的影响是透明的,因为它们对输出数据值没有影响。但是,它们都对程序的时序有影响,这可能是 gcc 将 nops 添加到 x86 代码的原因,这就是原始问题所要求的。
  • @DavidCary:如果是这个原因,你只会在有条件的跳转之后看到它,而不是在无条件的 ret 之后。
  • 这不是原因。间接跳转的回退预测(在 BTB 未命中时)是下一条指令,但如果那是非指令垃圾,则建议的停止错误推测的优化是像 ud2int3 这样总是出错的指令,所以前面-例如,end 知道停止解码,而不是将潜在昂贵的div 或虚假的 TLB 未命中负载馈送到管道中。这在函数末尾的 ret 或直接 jmp 尾调用之后不需要。
【解决方案2】:

这样做是为了将下一个函数与 8、16 或 32 字节边界对齐。

来自 A.Fog 的“优化汇编语言中的子程序”:

11.5 代码对齐

大多数微处理器以对齐的 16 字节或 32 字节块的形式获取代码。如果一个重要的子程序入口或跳转标签恰好在一个 16 字节块的末尾附近,那么微处理器在获取该代码块时只会得到几个有用的代码字节。在解码标签之后的第一条指令之前,它可能也必须获取接下来的 16 个字节。这可以通过将重要的子程序条目和循环条目对齐 16 来避免。

[...]

对齐一个子程序条目就像放置尽可能多的一样简单 无 ' 在子例程入口之前根据需要使地址可被 8、16、32 或 64 整除。

【讨论】:

  • 这是 25-29 字节之间的差异(对于 main),您是在说更大的东西吗?和文本部分一样,通过readelf我发现它是364字节?我还注意到 _start 上有 14 个 nops。为什么“as”不做这些事情?我是菜鸟,抱歉。
  • @olly:我见过开发系统对已编译的机器代码执行全程序优化。如果函数 foo 的地址是 0x1234,那么恰好使用该地址接近文字 0x1234 的代码最终可能会生成像 mov ax,0x1234 / push ax / mov ax,0x1234 / push ax 这样的机器代码,然后优化器可以将其替换为 mov ax,0x1234 / push ax / push ax。请注意,优化后的函数不能重定位,因此消除指令会提高执行速度,但不会提高代码大小。
【解决方案3】:

首先,gcc 并不总是这样做。 padding由-falign-functions控制,由-O2-O3自动开启:

-falign-functions
-falign-functions=n

将函数的开头与大于n 的下一个二次幂对齐,最多跳过n 字节。例如, -falign-functions=32 将函数与下一个 32 字节边界对齐,但 -falign-functions=24 将仅与下一个 32 字节边界对齐 如果这可以通过跳过 23 个字节或更少来完成。

-fno-align-functions-falign-functions=1 是等价的,表示函数不会对齐。

一些汇编程序仅在 n 是 2 的幂时才支持此标志;在 这种情况下,四舍五入。

如果 n 未指定或为零,则使用与机器相关的默认值。

在 -O2、-O3 级别启用。

这样做可能有多种原因,但 x86 上的主要原因可能是这样的:

大多数处理器在对齐的 16 字节或 32 字节块中获取指令。有可能 有利于将关键循环条目和子例程条目对齐 16 以最小化 代码中 16 字节边界的数量。或者,确保进入关键循环或子程序后的前几条指令中没有 16 字节边界。

(引自“优化汇编中的子程序 语言”,Agner Fog 着。)

编辑:这是一个演示填充的示例:

// align.c
int f(void) { return 0; }
int g(void) { return 0; }

当使用 gcc 4.4.5 和默认设置编译时,我得到:

align.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <f>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   b8 00 00 00 00          mov    $0x0,%eax
   9:   c9                      leaveq 
   a:   c3                      retq   

000000000000000b <g>:
   b:   55                      push   %rbp
   c:   48 89 e5                mov    %rsp,%rbp
   f:   b8 00 00 00 00          mov    $0x0,%eax
  14:   c9                      leaveq 
  15:   c3                      retq   

指定-falign-functions 给出:

align.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <f>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   b8 00 00 00 00          mov    $0x0,%eax
   9:   c9                      leaveq 
   a:   c3                      retq   
   b:   eb 03                   jmp    10 <g>
   d:   90                      nop
   e:   90                      nop
   f:   90                      nop

0000000000000010 <g>:
  10:   55                      push   %rbp
  11:   48 89 e5                mov    %rsp,%rbp
  14:   b8 00 00 00 00          mov    $0x0,%eax
  19:   c9                      leaveq 
  1a:   c3                      retq   

【讨论】:

  • 我没有使用任何 -O 标志,简单的“gcc -o test test.c”。
  • @olly:我已经在 64 位 Ubuntu 上使用 gcc 4.4.5 对其进行了测试,在我的测试中,默认情况下没有填充,并且使用 -falign-functions 进行填充。
  • @aix:我在 centOS 6.0(32 位)上,没有任何标志有填充。有人要我转储完整的“objdump -j .text -d ./test”输出吗?
  • 在进一步测试中,当我将其编译为对象时:“gcc -c test.c”。没有填充,但是当我链接时:“gcc -o test test.o”会出现。
  • @olly:该填充由链接器插入,以满足可执行文件中main 之后的函数的对齐要求(在我的情况下,该函数是__libc_csu_fini)。
猜你喜欢
  • 2023-04-02
  • 1970-01-01
  • 2020-01-16
  • 2016-02-26
  • 2011-08-19
  • 2019-03-30
  • 2020-01-17
  • 2020-01-01
相关资源
最近更新 更多