私はこれに対してgcc-Sを実行しました:
int main()
{
printf ("Hello world!");
}
そして私はこのアセンブリコードを手に入れました:
.file "test.c"
.section .rodata
.LC0:
.string "Hello world!"
.text
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $20, %esp
movl $.LC0, (%esp)
call printf
addl $20, %esp
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
.size main, .-main
.ident "GCC: (GNU) 4.3.0 20080428 (Red Hat 4.3.0-8)"
.section .note.GNU-stack,"",@progbits
この出力を理解したいと思います。誰かがこの出力を理解する上でいくつかのポインタを共有できますか、または誰かがこれらの行/行のグループのそれぞれに対してコメントをマークして、それが何をするのかを説明できれば素晴らしいでしょう。
ここにそれがどうなるか:
_ .file "test.c"
_
元のソースファイル名(デバッガーによって使用されます)。
_ .section .rodata
.LC0:
.string "Hello world!"
_
ゼロで終了する文字列がセクション「.rodata」に含まれています(「ro」は「読み取り専用」を意味します。アプリケーションはデータを読み取ることができますが、データに書き込もうとすると例外が発生します)。
_ .text
_
次に、コードの行き先である「.text」セクションに書き込みます。
_.globl main
.type main, @function
main:
_
「メイン」と呼ばれるグローバルに表示される関数を定義します(他のオブジェクトファイルがそれを呼び出すことができます)。
_ leal 4(%esp), %ecx
_
レジスタ_%ecx
_に値_4+%esp
_を格納します(_%esp
_はスタックポインタです)。
_ andl $-16, %esp
_
_%esp
_は、16の倍数になるようにわずかに変更されています。一部のデータ型(Cのdouble
および_long double
_に対応する浮動小数点形式)では、メモリを使用するとパフォーマンスが向上します。アクセスは16の倍数のアドレスにあります。これはここでは実際には必要ありませんが、最適化フラグ(_-O2
_...)なしで使用すると、コンパイラは非常に多くの一般的な役に立たないコード(つまりコード)を生成する傾向があります。これは場合によっては役立つかもしれませんが、ここでは役に立ちません)。
_ pushl -4(%ecx)
_
これは少し奇妙です。その時点で、アドレス-4(%ecx)
のWordは、andl
の前にスタックの一番上にあったWordです。コードはそのWord(ちなみにリターンアドレスである必要があります)を取得し、再度プッシュします。この種のエミュレートは、16バイトに整列されたスタックを持つ関数からの呼び出しで得られるものをエミュレートします。私の推測では、このPush
は引数コピーシーケンスの残骸です。関数はスタックポインタを調整しているため、スタックポインタの古い値からアクセスできる関数の引数をコピーする必要があります。ここでは、関数の戻りアドレス以外に引数はありません。このWordは使用されないことに注意してください(ただし、これは最適化されていないコードです)。
_ pushl %ebp
movl %esp, %ebp
_
これは標準の関数プロローグです。_%ebp
_を保存し(変更しようとしているため)、スタックフレームを指すように_%ebp
_を設定します。その後、_%ebp
_を使用して関数の引数にアクセスし、_%esp
_を再び解放します。 (はい、引数がないので、これはその関数には役に立ちません。)
_ pushl %ecx
_
_%ecx
_を保存します(_%esp
_をandl
の前の値に復元するには、関数の終了時に必要になります)。
_ subl $20, %esp
_
スタックに32バイトを予約します(スタックが「ダウン」することを忘れないでください)。そのスペースは、printf()
への引数を格納するために使用されます(4バイト[ポインター]を使用する単一の引数があるため、これはやり過ぎです)。
_ movl $.LC0, (%esp)
call printf
_
引数をprintf()
に「プッシュ」します(つまり、_%esp
_が引数を含むWordを指していることを確認します。ここでは、_$.LC0
_は、の定数文字列のアドレスです。 rodataセクション)。次に、printf()
を呼び出します。
_ addl $20, %esp
_
printf()
が戻ると、引数に割り当てられたスペースが削除されます。このaddl
は、上記のsubl
が行ったことをキャンセルします。
_ popl %ecx
_
_%ecx
_(上にプッシュ)を回復します。 printf()
はそれを変更した可能性があります(呼び出し規約は、関数が終了時に復元せずに変更できるレジスタを記述します。_%ecx
_はそのようなレジスタの1つです)。
_ popl %ebp
_
関数エピローグ:これにより、_%ebp
_が復元されます(上記の_pushl %ebp
_に対応)。
_ leal -4(%ecx), %esp
_
_%esp
_を初期値に復元します。このオペコードの効果は、値_%esp
_を_%ecx-4
_に格納することです。 _%ecx
_は最初の関数オペコードで設定されました。これにより、andl
を含む_%esp
_への変更がキャンセルされます。
_ ret
_
関数の終了。
_ .size main, .-main
_
これにより、main()
関数のサイズが設定されます。アセンブリ中の任意の時点で、「_.
_」は「現在追加しているアドレス」のエイリアスです。ここに別の命令を追加すると、「_.
_」で指定されたアドレスに移動します。したがって、ここでの「_.-main
_」は、関数main()
のコードの正確なサイズです。 _.size
_ディレクティブは、その情報をオブジェクトファイルに書き込むようにアセンブラに指示します。
_ .ident "GCC: (GNU) 4.3.0 20080428 (Red Hat 4.3.0-8)"
_
GCCは、その行動の痕跡を残すのが大好きです。この文字列は、オブジェクトファイル内の一種のコメントとして終了します。リンカはそれを削除します。
_ .section .note.GNU-stack,"",@progbits
_
GCCが、コードが実行不可能なスタックに対応できると記述している特別なセクション。これは通常のケースです。一部の特別な使用法(標準Cではない)には、実行可能スタックが必要です。最新のプロセッサでは、カーネルは実行不可能なスタック(誰かがスタック上にあるデータをコードとして実行しようとすると例外をトリガーするスタック)を作成できます。スタックにコードを置くことはバッファオーバーフローを悪用する一般的な方法であるため、これは「セキュリティ機能」と見なされる人もいます。このセクションでは、実行可能ファイルは「実行不可能なスタックと互換性がある」とマークされ、カーネルはそれ自体を喜んで提供します。
これが@Thomas Pornin
の答えの補足です。
.LC0
ローカル定数、例:文字列リテラル。.LFB0
ローカル関数の開始、.LFE0
ローカル関数の終了、これらのラベルのサフィックスは数字で、0から始まります。
これはgccアセンブラの規則です。
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $20, %esp
これらの命令はcプログラムでは比較されません。常にすべての関数の先頭で実行されます(ただし、コンパイラー/プラットフォームによって異なります)。
movl $.LC0, (%esp)
call printf
このブロックは、printf()呼び出しに対応します。最初の命令は、その引数( "hello world"へのポインター)をスタックに配置してから、関数を呼び出します。
addl $20, %esp
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
これらの命令は最初のブロックの反対であり、ある種のスタック操作のものです。常に実行されます