web-dev-qa-db-ja.com

GCCパッドがNOPで機能するのはなぜですか?

私はしばらく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)で動作しませんか?

どんなことでも学んでみてください。

77
olly

まず、gccは常にこれを行うわけではありません。パディングは -falign-functions によって制御され、-O2および-O3によって自動的にオンになります。

-falign-functions
-falign-functions=n

関数の先頭をnより大きい2のべき乗に揃え、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による「アセンブリ言語でのサブルーチンの最適化」から引用)

edit:次に、パディングを示す例を示します。

// 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   
86
NPE

これは、次の関数を8、16、または32バイトの境界で整列させるために行われます。

A.Fogによる「アセンブリ言語でのサブルーチンの最適化」から:

11.5コードの整列

ほとんどのマイクロプロセッサは、整列された16バイトまたは32バイトのブロックでコードをフェッチします。重要なサブルーチンエントリまたはジャンプラベルが16バイトブロックの終わり近くにある場合、マイクロプロセッサは、そのコードブロックをフェッチするときに数バイトの有用なコードしか取得しません。ラベルの後の最初の命令をデコードする前に、次の16バイトもフェッチする必要がある場合があります。これは、重要なサブルーチンエントリとループエントリを16ずつ揃えることで回避できます。

[...]

サブルーチンエントリの整列は、アドレスを必要に応じて8、16、32、または64で割り切れるように、サブルーチンエントリの前に必要な数のNOPを配置するのと同じくらい簡単です。

13
hamstergene

私が覚えている限り、命令はCPUでパイプライン化され、さまざまなCPUブロック(ローダー、デコーダーなど)が後続の命令を処理します。 RET命令が実行されているとき、次のいくつかの命令はすでにCPUパイプラインにロードされています。推測ではありますが、ここから掘り始めることができます。発見できたら(おそらく、安全なNOPsの具体的な数は)、調査結果を共有してください。

6
mco