私はしばらく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)で動作しませんか?
どんなことでも学んでみてください。
まず、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
これは、次の関数を8、16、または32バイトの境界で整列させるために行われます。
A.Fogによる「アセンブリ言語でのサブルーチンの最適化」から:
11.5コードの整列
ほとんどのマイクロプロセッサは、整列された16バイトまたは32バイトのブロックでコードをフェッチします。重要なサブルーチンエントリまたはジャンプラベルが16バイトブロックの終わり近くにある場合、マイクロプロセッサは、そのコードブロックをフェッチするときに数バイトの有用なコードしか取得しません。ラベルの後の最初の命令をデコードする前に、次の16バイトもフェッチする必要がある場合があります。これは、重要なサブルーチンエントリとループエントリを16ずつ揃えることで回避できます。
[...]
サブルーチンエントリの整列は、アドレスを必要に応じて8、16、32、または64で割り切れるように、サブルーチンエントリの前に必要な数のNOPを配置するのと同じくらい簡単です。
私が覚えている限り、命令はCPUでパイプライン化され、さまざまなCPUブロック(ローダー、デコーダーなど)が後続の命令を処理します。 RET
命令が実行されているとき、次のいくつかの命令はすでにCPUパイプラインにロードされています。推測ではありますが、ここから掘り始めることができます。発見できたら(おそらく、安全なNOP
sの具体的な数は)、調査結果を共有してください。