web-dev-qa-db-ja.com

C hello worldのアセンブリ出力の各行の意味は何ですか?

私はこれに対して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

この出力を理解したいと思います。誰かがこの出力を理解する上でいくつかのポインタを共有できますか、または誰かがこれらの行/行のグループのそれぞれに対してコメントをマークして、それが何をするのかを説明できれば素晴らしいでしょう。

34
Mohammed

ここにそれがどうなるか:

_        .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ではない)には、実行可能スタックが必要です。最新のプロセッサでは、カーネルは実行不可能なスタック(誰かがスタック上にあるデータをコードとして実行しようとすると例外をトリガーするスタック)を作成できます。スタックにコードを置くことはバッファオーバーフローを悪用する一般的な方法であるため、これは「セキュリティ機能」と見なされる人もいます。このセクションでは、実行可能ファイルは「実行不可能なスタックと互換性がある」とマークされ、カーネルはそれ自体を喜んで提供します。

61
Thomas Pornin

これが@Thomas Porninの答えの補足です。

  • .LC0ローカル定数、例:文字列リテラル。
  • .LFB0ローカル関数の開始、
  • .LFE0ローカル関数の終了、

これらのラベルのサフィックスは数字で、0から始まります。

これはgccアセンブラの規則です。

18
Eric Wang
    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

これらの命令は最初のブロックの反対であり、ある種のスタック操作のものです。常に実行されます

3
BlackBear