方法は次のとおりです:
.file "test.c"
元のソース ファイル名 (デバッガーが使用)。
.section .rodata
.LC0:
.string "Hello world!"
ゼロで終わる文字列がセクション「.rodata」に含まれています (「ro」は「読み取り専用」を意味します。アプリケーションはデータを読み取ることができますが、データに書き込もうとすると例外がトリガーされます)。
.text
次に、コードが入る「.text」セクションに書き込みます。
.globl main
.type main, @function
main:
「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)
の単語 andl
より前にスタックの一番上にあった単語です .このコードはその単語 (ちなみに、これはリターン アドレスである必要があります) を取得し、再度プッシュします。この種の関数は、16 バイトでアラインされたスタックを持つ関数からの呼び出しで得られるものをエミュレートします。私の推測では、この push
引数のコピー シーケンスの名残です。関数はスタック ポインターを調整したため、関数の引数をコピーする必要があります。これにはスタック ポインターの古い値からアクセスできます。ここでは、関数の戻りアドレスを除いて、引数はありません。この単語は使用されないことに注意してください (繰り返しますが、これは最適化されていないコードです)。
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
であることを確認します 引数を含む単語を指します。ここでは $.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
最初の関数オペコードで設定されました。これにより、%esp
への変更がキャンセルされます 、 andl
を含む .
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 ではない) には、実行可能スタックが必要です。最新のプロセッサでは、カーネルは実行不可能なスタック (誰かがスタック上にあるデータをコードとして実行しようとすると例外をトリガーするスタック) を作成できます。コードをスタックに置くことは、バッファ オーバーフローを悪用する一般的な方法であるため、これを「セキュリティ機能」と見なす人もいます。このセクションでは、実行可能ファイルは「非実行可能スタックと互換性がある」とマークされ、カーネルは喜んでそれを提供します。
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
これらの命令は最初のブロックとは逆で、ある種のスタック操作のものです。常に実行も
@Thomas Pornin
の補足です。 の答えです。
.LC0
文字列リテラルなどのローカル定数.LFB0
ローカル関数の開始.LFE0
ローカル関数の終了、
これらのラベルのサフィックスは数字で、0 から始まります。
これは gcc アセンブラーの規則です。