为什么GCC垫可与NOP一起使用?


80

我已经使用C了很短的时间,最近才开始涉足ASM。当我编译程序时:

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

objdump反汇编具有代码,但在重新输入后会提示:

...
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个字节。
Mysticial 2011年

在4个nops之后不返回到函数:__libc_csu_fini
olly

1
如果NOP是由gcc插入的,那么我认为它不会仅使用0x90,因为有许多NOP的大小变量在1-9字节之间(如果使用gas语法
则为

Answers:


89

首先,gcc并不总是这样做。填充由控制-falign-functions,由-O2和自动打开-O3

-falign-functions
-falign-functions=n

将函数的开头对齐到下一个大于2的2的幂n,并跳至n字节。例如,-falign-functions=32将函数对齐到下一个32字节的边界,但是-falign-functions=24仅当可以跳过23个字节或更少的字节才能对齐时,才 将函数对齐到下一个32字节的边界。

-fno-align-functions-falign-functions=1是等效的,表示功能将不对齐。

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

如果未指定n或为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
我没有使用任何-O标志,简单的是“ gcc -o test test.c”。
olly

1
@olly:我已经在64位Ubuntu上使用gcc 4.4.5对其进行了测试,并且在我的测试中默认情况下没有填充,并且使用填充-falign-functions
NPE

@aix:我使用的是centOS 6.0(32位),没有任何标志进行填充。有人要我转储完整的“ objdump -j .text -d ./test”输出吗?
olly

1
在进一步测试中,当我将其编译为对象时:“ gcc -c test.c”。没有填充,但是当我链接:“ gcc -o test test.o”时,它会出现。
olly

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

15

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

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

11.5代码对齐

大多数微处理器以对齐的16字节或32字节块来获取代码。如果一个重要的子例程条目或跳转标签恰好在16字节块的末尾附近,则微处理器在获取该代码块时将仅获得一些有用的代码字节。它可能还必须获取下一个16个字节,然后才能解码标签后的第一条指令。可以通过将重要的子例程条目和循环条目对齐16来避免这种情况。

[...]

对齐子例程条目就像在子例程条目之前放置所需数量的NOP一样简单,以根据需要使地址可被8、16、32或64整除。


25-29个字节(用于main)之间是有区别的,您是否在谈论更大的东西?像文本部分一样,通过readelf我发现它是364个字节?我还注意到_start上有14个点。为什么这些事情不按“原样”进行?我是菜鸟,很抱歉。
olly

@olly:我已经看到了开发系统,它们对编译的机器代码执行整个程序的优化。如果函数的地址foo为0x1234,那么恰好在紧邻字面0x1234的位置使用该地址的代码可能最终生成机器代码mov ax,0x1234 / push ax / mov ax,0x1234 / push ax,然后优化器可以将其替换为mov ax,0x1234 / push ax / push ax。请注意,在进行此类优化后不得重定位函数,因此消除指令可以提高执行速度,但不能提高代码大小。
2015年

5

据我所知,指令在cpu中流水线化,不同的cpu块(加载器,解码器等)处理后续指令。当RET正在执行的指令,几下一指令已经被加载到CPU流水线。这是一个猜测,但是您可以在这里开始挖掘,如果您发现了(也许NOP安全的s的特定数量,请分享您的发现。


@ninjalj:嗯?这个问题是关于x86的,它是流水线的(如mco所说)。许多现代的x86处理器还推测性地执行“不应”执行的指令,可能包括这些nop。也许您想在其他地方发表评论?
大卫·卡里

3
@DavidCary:在x86中,对程序员完全透明。错误猜测的推测执行指令只会丢弃其结果和效果。在MIPS上,根本没有“推测性”部分,分支延迟槽中的指令始终会执行,并且程序员必须填充延迟槽(或让汇编器执行,这可能会导致nops)。
ninjalj 2013年

@ninjalj:是的,因为错误地猜测推测性执行的操作和未对齐的指令对输出数据值没有影响,所以它们是透明的。但是,它们都对程序的时间有影响,这可能是gcc向x86代码添加nops的原因,这就是最初的问题。
大卫·卡里

1
@DavidCary:如果是这个原因,那么您只会在有条件的跳转之后看到它,而不是在无条件的之后看到它ret
ninjalj 2013年

1
这不是为什么。下一条指令是间接跳转的回退预测(针对BTB未命中),但是,如果这是非指令垃圾,则建议的用于阻止错误推测的优化是一条类似ud2int3总是出错的指令,因此前端知道停止解码例如,将潜在的昂贵div或虚假的TLB缺失负载馈入管道。在函数末尾进行ret或直接jmp进行尾调用后,不需要此操作。
彼得·科德斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.