逆アセンブルで遊んでみる(9)構造体変数の返却
構造体変数をreturnする関数の内部処理を解析する。です。
1 struct d { 2 int num1; 3 int num2; 4 int num3; 5 }; 6 7 struct d func() 8 { 9 struct d d1; 10 11 d1.num1 = 0x7FFFFFFF; 12 d1.num2 = 0x8FFFFFFF; 13 d1.num3 = 0x9FFFFFFF; 14 15 return d1; 16 } 17 18 int main() 19 { 20 struct d d1; 21 22 d1 = func(); 23 24 return 0; 25 }
080483ec <func>: int num2; int num3; }; struct d func() { 80483ec: 55 push %ebp 80483ed: 89 e5 mov %esp,%ebp 80483ef: 83 ec 10 sub $0x10,%esp struct d d1; d1.num1 = 0x7FFFFFFF; 80483f2: c7 45 f4 ff ff ff 7f movl $0x7fffffff,-0xc(%ebp) d1.num2 = 0x8FFFFFFF; 80483f9: c7 45 f8 ff ff ff 8f movl $0x8fffffff,-0x8(%ebp) d1.num3 = 0x9FFFFFFF; 8048400: c7 45 fc ff ff ff 9f movl $0x9fffffff,-0x4(%ebp) return d1; 8048407: 8b 45 08 mov 0x8(%ebp),%eax 804840a: 8b 55 f4 mov -0xc(%ebp),%edx 804840d: 89 10 mov %edx,(%eax) 804840f: 8b 55 f8 mov -0x8(%ebp),%edx 8048412: 89 50 04 mov %edx,0x4(%eax) 8048415: 8b 55 fc mov -0x4(%ebp),%edx 8048418: 89 50 08 mov %edx,0x8(%eax) } 804841b: 8b 45 08 mov 0x8(%ebp),%eax 804841e: c9 leave 804841f: c2 04 00 ret $0x4 08048422 <main>: int main() { 8048422: 55 push %ebp 8048423: 89 e5 mov %esp,%ebp 8048425: 83 ec 14 sub $0x14,%esp struct d d1; d1 = func(); 8048428: 8d 45 f4 lea -0xc(%ebp),%eax 804842b: 89 04 24 mov %eax,(%esp) 804842e: e8 b9 ff ff ff call 80483ec <func> 8048433: 83 ec 04 sub $0x4,%esp return 0; 8048436: b8 00 00 00 00 mov $0x0,%eax } 804843b: c9 leave 804843c: c3 ret 804843d: 66 90 xchg %ax,%ax 804843f: 90 nop
まず、main関数から見ていきます。
main() 構造体の領域確保
8048425: 83 ec 14 sub $0x14,%esp
20バイト分の自動変数領域を確保します。この時点での、スタックの内容は以下。
ebp | offset | Stack | esp |
---|---|---|---|
-0x14 | (空き) | ← | |
-0x10 | (空き) | ||
-0x0C | (空き) | ||
-0x08 | (空き) | ||
-0x04 | (空き) | ||
→ | -0x00 | Saved EBP | |
+0x04 | Saved EIP | ||
+0x08 | … | ||
… | main()呼出元のスタックベース |
8048428: 8d 45 f4 lea -0xc(%ebp),%eax 804842b: 89 04 24 mov %eax,(%esp) 804842e: e8 b9 ff ff ff call 80483ec <func>
lea命令は、指定アドレスに格納された値ではなく、”アドレスそのもの”を転送するので、eaxレジスタを媒介して、「アドレス-0xc(%ebp)」(以下図★印のアドレス)という値を、espレジスタのポイント先に格納しています。内容が空の領域のアドレスをなぜ保存するのか?というと、次に呼び出す関数内で、その空領域に、何か値が格納されて返されるのだろう…と推測されます。
ebp | offset | Stack | esp |
---|---|---|---|
-0x14 | ★のアドレス | ← | |
-0x10 | (空き) | ||
★-0x0C | (空き) | ||
-0x08 | (空き) | ||
-0x04 | (空き) | ||
→ | -0x00 | Saved EBP | |
+0x04 | Saved EIP | ||
+0x08 | … | ||
… | main()呼出元のスタックベース |
最後に、func関数を呼び出しています。
func() プロローグ
80483ec: 55 push %ebp 80483ed: 89 e5 mov %esp,%ebp 80483ef: 83 ec 10 sub $0x10,%esp
上の命令を実行した時点で、スタックは以下のようになります。
ebp | offset | Stack | esp |
---|---|---|---|
-0x10 | (空き) | ← | |
-0x0C | (空き) | ||
-0x08 | (空き) | ||
-0x04 | (空き) | ||
→ | +0x00 | Saved EBP | |
+0x04 | Saved EIP | ||
+0x08 | ★のアドレス | ||
+0x0C | (空き) | ||
★+0x10 | (空き) | ||
+0x14 | (空き) | ||
+0x18 | (空き) | ||
+0x1C | Saved EBP | ||
+0x20 | Saved EIP | ||
+0x24 | … | ||
… | main()呼出元のスタックベース |
func() ローカル変数領域への値の代入
d1.num1 = 0x7FFFFFFF; 80483f2: c7 45 f4 ff ff ff 7f movl $0x7fffffff,-0xc(%ebp) d1.num2 = 0x8FFFFFFF; 80483f9: c7 45 f8 ff ff ff 8f movl $0x8fffffff,-0x8(%ebp) d1.num3 = 0x9FFFFFFF; 8048400: c7 45 fc ff ff ff 9f movl $0x9fffffff,-0x4(%ebp)
空いた領域(構造体変数の領域)に、各メンバ変数への値を代入しています。
ebp | offset | Stack | esp |
---|---|---|---|
-0x10 | (空き) | ← | |
-0x0C | ★7F FF FF FF | ||
-0x08 | ★8F FF FF FF | ||
-0x04 | ★9F FF FF FF | ||
→ | +0x00 | Saved EBP | |
+0x04 | Saved EIP | ||
+0x08 | ●のアドレス | ||
+0x0C | (空き) | ||
●+0x10 | (空き) | ||
+0x14 | (空き) | ||
+0x18 | (空き) | ||
+0x1C | Saved EBP | ||
+0x20 | Saved EIP | ||
+0x24 | … | ||
… | main()呼出元のスタックベース |
func() ローカル変数領域への値の代入
8048407: 8b 45 08 mov 0x8(%ebp),%eax 804840a: 8b 55 f4 mov -0xc(%ebp),%edx 804840d: 89 10 mov %edx,(%eax) 804840f: 8b 55 f8 mov -0x8(%ebp),%edx 8048412: 89 50 04 mov %edx,0x4(%eax) 8048415: 8b 55 fc mov -0x4(%ebp),%edx 8048418: 89 50 08 mov %edx,0x8(%eax)
この部分、ちょっとややこしいですね…。上の命令を実行した時点で、スタックは以下のようになっています。
ebp | offset | Stack | esp |
---|---|---|---|
-0x10 | (空き) | ← | |
-0x0C | 7F FF FF FF | ||
-0x08 | 8F FF FF FF | ||
-0x04 | 9F FF FF FF | ||
→ | +0x00 | Saved EBP | |
+0x04 | Saved EIP | ||
+0x08 | ●のアドレス | ||
+0x0C | (空き) | ||
●+0x10 | ★7F FF FF FF | ||
+0x14 | ★8F FF FF FF | ||
+0x18 | ★9F FF FF FF | ||
+0x1C | Saved EBP | ||
+0x20 | Saved EIP | ||
+0x24 | … | ||
… | main()呼出元のスタックベース |
何をやっているかというと、ローカルのスタックフレーム内に確保した、構造体変数のメンバ変数の値を、呼出元のスタックフレーム内にコピーしたいわけです。呼出元に、構造体の値を渡したいわけですから。で、どこにコピーするか?という位置情報は、eaxレジスタに格納されたアドレスを基準に、メンバ変数num1、num2、num3のそれぞれの場所にオフセット指定でコピーしているわけです。
が、そもそも、そのベースアドレスはどこか?という位置情報は、現スタックベースから+0x08の位置に格納されているわけです。この位置情報は、func関数の冒頭で格納されましたし、領域の実体は、呼出元(main関数)で確保されました。
func() エピローグ処理
804841b: 8b 45 08 mov 0x8(%ebp),%eax 804841e: c9 leave 804841f: c2 04 00 ret $0x4
関数を抜ける前に、構造体変数のコピー先の位置情報(0x8(%ebp))を、eaxレジスタに格納して、呼出元(main())に返却しています。あとはお決まりの終了処理ですね。
ebp | offset | Stack | esp |
---|---|---|---|
-0x10 | (空き) | ← | |
-0x0C | 7F FF FF FF | ||
-0x08 | 8F FF FF FF | ||
-0x04 | 9F FF FF FF | ||
→ | +0x00 | Saved EBP | |
+0x04 | Saved EIP | ||
+0x08 | ★●のアドレス | ||
+0x0C | (空き) | ||
●+0x10 | 7F FF FF FF | ||
+0x14 | 8F FF FF FF | ||
+0x18 | 9F FF FF FF | ||
+0x1C | Saved EBP | ||
+0x20 | Saved EIP | ||
+0x24 | … | ||
… | main()呼出元のスタックベース |
なるほど、構造体の実体をreturnする場合、構造体の領域は呼出元で確保→その領域に呼出先で値をセット→呼出先から、セットされた領域の位置情報をeaxレジスタ経由で取得…という流れですね。「d1 = func()」というコードは、ポインタ値を返していただけで、データの格納自体は、呼出し先関数内で行われていた…ということですね。
しかし、アセンブリコードの解析って、忍耐力がモノを言いますね…。