簡単な C プログラムによって生成されるアセンブリ コードを理解する



gdb の逆アセンブラを使用して検査することにより、単純な C プログラムのアセンブリ レベルのコードを理解しようとしています。


以下は C コードです:


#include <stdio.h>
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
}
void main() {
function(1,2,3);
}

以下は、両方の main の逆アセンブリ コードです。 と function


gdb) disass main
Dump of assembler code for function main:
0x08048428 <main+0>: push %ebp
0x08048429 <main+1>: mov %esp,%ebp
0x0804842b <main+3>: and $0xfffffff0,%esp
0x0804842e <main+6>: sub $0x10,%esp
0x08048431 <main+9>: movl $0x3,0x8(%esp)
0x08048439 <main+17>: movl $0x2,0x4(%esp)
0x08048441 <main+25>: movl $0x1,(%esp)
0x08048448 <main+32>: call 0x8048404 <function>
0x0804844d <main+37>: leave
0x0804844e <main+38>: ret
End of assembler dump.
(gdb) disass function
Dump of assembler code for function function:
0x08048404 <function+0>: push %ebp
0x08048405 <function+1>: mov %esp,%ebp
0x08048407 <function+3>: sub $0x28,%esp
0x0804840a <function+6>: mov %gs:0x14,%eax
0x08048410 <function+12>: mov %eax,-0xc(%ebp)
0x08048413 <function+15>: xor %eax,%eax
0x08048415 <function+17>: mov -0xc(%ebp),%eax
0x08048418 <function+20>: xor %gs:0x14,%eax
0x0804841f <function+27>: je 0x8048426 <function+34>
0x08048421 <function+29>: call 0x8048340 <[email protected]>
0x08048426 <function+34>: leave
0x08048427 <function+35>: ret
End of assembler dump.

次のことについて回答を求めています :



  1. アドレッシングの仕組み、つまり (main+0) 、 (main+1) 、 (main+3)

  2. 主に、$0xfffffff0,%esp が使用されている理由

  3. 関数で %gs:0x14,%eax , %eax,-0xc(%ebp) が使用されている理由

  4. 誰かが を段階的に説明してくれれば、それは大歓迎です。


答え:


main+0 などの「奇妙な」アドレスの理由 、 main+1main+3main+6 などは、各命令が可変バイト数を占めるためです。例:


main+0: push %ebp

は 1 バイトの命令なので、次の命令は main+1 にあります .一方、


main+3: and $0xfffffff0,%esp

は 3 バイトの命令なので、その後の次の命令は main+6 にあります。 .


そして、コメントで movl の理由を尋ねるので は可変バイト数を取るようですが、その説明は次のとおりです。


命令の長さは、オペコードだけに依存するわけではありません (movl など ) だけでなく、オペランドのアドレッシング モードも 同様に(オペコードが操作しているもの)。私はあなたのコードを特にチェックしていませんが、


movl $0x1,(%esp)

オフセットが含まれていないため、命令はおそらく短くなります - esp を使用するだけです アドレスとして。一方、次のようなもの:


movl $0x2,0x4(%esp)

movl $0x1,(%esp) のすべてが必要です あり、さらに オフセット 0x4 の余分なバイト .


実際、これが意味することを示すデバッグ セッションです:


Microsoft Windows XP [Version 5.1.2600]
(C) Copyright 1985-2001 Microsoft Corp.
c:\pax> debug
-a
0B52:0100 mov word ptr [di],7
0B52:0104 mov word ptr [di+2],8
0B52:0109 mov word ptr [di+0],7
0B52:010E
-u100,10d
0B52:0100 C7050700 MOV WORD PTR [DI],0007
0B52:0104 C745020800 MOV WORD PTR [DI+02],0008
0B52:0109 C745000700 MOV WORD PTR [DI+00],0007
-q
c:\pax> _

オフセットのある 2 番目の命令は、オフセットがない最初の命令と実際には異なることがわかります。これは 1 バイト長く (オフセットを保持するために 4 バイトではなく 5 バイト)、実際には別のエンコーディング c745 を持っています。 c705 の代わりに .


また、最初と 3 番目の命令を 2 つの異なる方法でエンコードできることもわかりますが、基本的には同じことを行います。



and $0xfffffff0,%esp 命令は esp を強制する方法です 特定の境界にいること。これは、変数の適切な位置合わせを保証するために使用されます。最新のプロセッサでの多くのメモリ アクセスは、アラインメント規則 (4 バイトの値を 4 バイト境界にアラインする必要があるなど) に従っている場合、より効率的になります。これらのルールに従わない場合、一部の最新のプロセッサではエラーが発生することさえあります。


この指示の後、 esp が保証されます および前の値以下である 16 バイト境界に整列。



gs: プレフィックスは単に gs を使用することを意味します デフォルトではなくメモリにアクセスするためのセグメント レジスタ。


命令 mov %eax,-0xc(%ebp) ebp の内容を取得することを意味します レジスター、12 を減算 (0xc ) そして eax の値を入れます



コードの説明を再。あなたの function 関数は基本的に 1 つの大きなノーオペレーションです。生成されるアセンブリは、前述の %gs:14 を使用するいくつかのスタック フレームの破損チェックと共に、スタック フレームのセットアップとティアダウンに限定されます。


その場所から値をロードします (おそらく 0xdeadbeef のようなものです) ) をスタック フレームに挿入し、ジョブを実行してから、スタックをチェックして破損していないことを確認します。


この場合、その仕事は何もありません。したがって、表示されるのは機能管理に関するものだけです。


スタックのセットアップは function+0 の間で発生します と function+12 .その後はすべて eax で戻りコードを設定しています 破損チェックを含むスタック フレームの解体。


同様に、main スタック フレームのセットアップで構成され、function のパラメータをプッシュします 、function を呼び出しています 、スタック フレームを破棄して終了します。


以下のコードにコメントが挿入されました:


0x08048428 <main+0>:    push   %ebp                 ; save previous value.
0x08048429 <main+1>: mov %esp,%ebp ; create new stack frame.
0x0804842b <main+3>: and $0xfffffff0,%esp ; align to boundary.
0x0804842e <main+6>: sub $0x10,%esp ; make space on stack.
0x08048431 <main+9>: movl $0x3,0x8(%esp) ; push values for function.
0x08048439 <main+17>: movl $0x2,0x4(%esp)
0x08048441 <main+25>: movl $0x1,(%esp)
0x08048448 <main+32>: call 0x8048404 <function> ; and call it.
0x0804844d <main+37>: leave ; tear down frame.
0x0804844e <main+38>: ret ; and exit.
0x08048404 <func+0>: push %ebp ; save previous value.
0x08048405 <func+1>: mov %esp,%ebp ; create new stack frame.
0x08048407 <func+3>: sub $0x28,%esp ; make space on stack.
0x0804840a <func+6>: mov %gs:0x14,%eax ; get sentinel value.
0x08048410 <func+12>: mov %eax,-0xc(%ebp) ; put on stack.
0x08048413 <func+15>: xor %eax,%eax ; set return code 0.
0x08048415 <func+17>: mov -0xc(%ebp),%eax ; get sentinel from stack.
0x08048418 <func+20>: xor %gs:0x14,%eax ; compare with actual.
0x0804841f <func+27>: je <func+34> ; jump if okay.
0x08048421 <func+29>: call <_stk_chk_fl> ; otherwise corrupted stack.
0x08048426 <func+34>: leave ; tear down frame.
0x08048427 <func+35>: ret ; and exit.


%gs:0x14 の理由だと思います 上記から明らかかもしれませんが、念のため、ここで詳しく説明します。


この値 (センチネル) を使用して現在のスタック フレームに配置するため、スタック上に作成された 20 バイト配列に 1024 バイトを書き込むなど、関数内の何かがばかげたことを行う場合、またはあなたの場合:


char buffer1[5];
strcpy (buffer1, "Hello there, my name is Pax.");

その後、センチネルが上書きされ、関数の最後のチェックでそれが検出され、失敗関数を呼び出して通知され、他の問題を回避するためにおそらく中止されます。


0xdeadbeef を配置した場合 スタックに追加され、これは別のものに変更され、次に xor 0xdeadbeefje のコードで検出されるゼロ以外の値を生成します


関連するビットはここで言い換えられています:


          mov    %gs:0x14,%eax     ; get sentinel value.
mov %eax,-0xc(%ebp) ; put on stack.
;; Weave your function
;; magic here.
mov -0xc(%ebp),%eax ; get sentinel back from stack.
xor %gs:0x14,%eax ; compare with original value.
je stack_ok ; zero/equal means no corruption.
call stack_bad ; otherwise corrupted stack.
stack_ok: leave ; tear down frame.