逆アセンブルで遊んでみる(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()」というコードは、ポインタ値を返していただけで、データの格納自体は、呼出し先関数内で行われていた…ということですね。

しかし、アセンブリコードの解析って、忍耐力がモノを言いますね…。