为什么GCC使用NOP填充功能?

oll*_*lly 76 c assembly gcc

我已经和C一起工作了一段时间,最近才开始进入ASM.当我编译一个程序时:

int main(void)
  {
  int a = 0;
  a += 1;
  return 0;
  }
Run Code Online (Sandbox Code Playgroud)

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

...
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
...
Run Code Online (Sandbox Code Playgroud)

从我学到的东西,什么都不做,因为ret之后甚至都不会被执行.

我的问题是:为什么要这么麻烦?ELF(linux-x86)无法使用任何大小的.text段(+ main)吗?

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

NPE*_*NPE 86

首先,gcc并不总是如此.填充由控制-falign-functions,这是由自动开启-O2-O3:

-falign-functions
-falign-functions=n

将函数的开头与下一个二次幂对齐大于n,跳过最多n字节.例如, -falign-functions=32将函数与下一个32字节边界-falign-functions=24对齐,但只有在跳过23个字节或更少的情况下才能对齐下一个32字节边界.

-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; }
Run Code Online (Sandbox Code Playgroud)

使用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   
Run Code Online (Sandbox Code Playgroud)

指定-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   
Run Code Online (Sandbox Code Playgroud)

  • @olly:该填充由链接器插入,以满足可执行文件中 `main` 后面的函数的对齐要求(在我的情况下,该函数是 `__libc_csu_fini`)。 (2认同)

ham*_*ene 13

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

来自A.Fog的"用汇编语言优化子程序":

11.5代码对齐

大多数微处理器在对齐的16字节或32字节块中获取代码.如果一个重要的子例程入口或跳转标签碰巧接近一个16字节块的末尾,则在获取该代码块时,微处理器将只获得一些有用的代码字节.在它可以解码标签之后的第一条指令之前,它也可能需要获取接下来的16个字节.通过将重要的子例程条目和循环条目对齐16可以避免这种情况.

[...]

对齐子例程条目就像在子例程输入之前根据需要放置尽可能多的NOP一样简单,以使地址可以根据需要被8,16,32或64整除.


mco*_*mco 6

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

  • @DavidCary:在x86中,这对程序员来说是完全透明的.错误猜测的推测性执行指令只会丢弃其结果和效果.在MIPS上,根本没有"推测"部分,分支延迟槽中的指令总是被执行,程序员必须填充延迟槽(或者让汇编器执行它,这可能会导致`nop` S). (3认同)
  • 这不是原因。间接跳转的回退预测(在 BTB 未命中时)是下一条指令,但如果这是非指令垃圾,则建议停止错误推测的优化是像“ud2”或“int3”这样总是出错的指令,因此前面-例如,end 知道停止解码,而不是向管道提供可能昂贵的“div”或虚假的 TLB 未命中负载。在函数末尾的“ret”或直接“jmp”尾调用之后不需要这样做。 (2认同)