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

时间:2011-10-27 06:39:44

标签: c gcc assembly

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

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

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
...

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

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

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

3 个答案:

答案 0 :(得分:86)

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

  

-falign-functions
  -falign-functions=n

     

将函数的开头与大于n的下一个2次幂对齐,跳过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   

答案 1 :(得分:13)

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

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

  

11.5代码对齐

     

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

     

[...]

     

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

答案 2 :(得分:6)

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