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.
次のことについて回答を求めています :
- アドレッシングの仕組み、つまり (main+0) 、 (main+1) 、 (main+3)
- 主に、$0xfffffff0,%esp が使用されている理由
- 関数で %gs:0x14,%eax , %eax,-0xc(%ebp) が使用されている理由
- 誰かが を段階的に説明してくれれば、それは大歓迎です。
答え:
main+0
などの「奇妙な」アドレスの理由 、 main+1
、 main+3
、 main+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
0xdeadbeef
で je
のコードで検出されるゼロ以外の値を生成します
関連するビットはここで言い換えられています:
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.