みけCATのにっき(仮)
つれづれなるまゝに、日くらし、PCにむかひて、心に移りゆくよしなし事を、そこはかとなく書きつくれば、あやしうこそものぐるほしけれ。
(本当か!?)
出典

インラインアセンブリとスタックの罠

アバター
みけCAT
記事: 6734
登録日時: 15年前
住所: 千葉県
連絡を取る:

インラインアセンブリとスタックの罠

投稿記事 by みけCAT » 12年前

前の日記で紹介した、アセンブラ入門の卒業試験
「MicrosoftのVisual C++、Ver6.0を持っていない貧乏人は諦めろバーカ」(解釈)とかいうふざけたことを言っている筆者に対抗するため、
独自のUIを作成し、画像処理機能の作成に取り組んでいます。

「4分割」の実装をしているとき、どうしてもレジスタだけでは変数が足りなくなり、スタックを使おうとしたのですが…
何かおかしい。スタックをいじらなければ普通に動くのに、スタックの操作とメモリ操作をすると動作が止まる。
push/popではなく、%espを直接いじっても同様。
OllyDbgで調べてみると、どうも拡張インラインアセンブリのパラメータとして渡しているメモリの値がおかしいことがわかる。
(今回はソースコードをアセンブリで書いているため、OllyDbgにそのままの意味で表示され、該当する場所がわかりやすい。
といっても、入力と出力が逆だったり、表現が長かったりして、少し見ずらいが。)
なして?

実験しましょう。
検証環境
Windows Vista Home Premium SP2 32ビット
Intel(R) Core(TM)2 Duo T8100 @2.10GHz 2.10GHz
RAM 4.00GB
gcc 4.7.2

CODE:

#include 

int main(void) {
	int a;
	int* p;
	int b[3]={127,65537,11113};
	__asm__ volatile (
		"mov %2,%%ecx\n\t"
		"lea %2,%%edx\n\t"
		"mov %%ecx,%0\n\t"
		"mov %%edx,%1\n\t"
	: "=m"(a),"=m"(p)
	: "m"(b[1])
	: "%ecx","%edx");
	printf("%p %d\n",p,a);
	__asm__ volatile (
		"push %%eax\n\t"
		"mov %2,%%ecx\n\t"
		"lea %2,%%edx\n\t"
		"pop %%eax\n\t"
		"mov %%ecx,%0\n\t"
		"mov %%edx,%1\n\t"
	: "=m"(a),"=m"(p)
	: "m"(b[1])
	: "%ecx","%edx");
	printf("%p %d\n",p,a);
	return 0;
}
前半がpush/popを使わない時のパラメータの値とアドレスを取得するコード、
後半がpush/popを使った時のパラメータの値とアドレスを取得するコードです。
実行結果(最適化なしと-O2を試しましたが、実行結果は変わりませんでした)

CODE:

0022FF18 65537
0022FF14 127
じぇじぇじぇ!やっぱり値もアドレスもずれてる!

では、もう少し単純な実験を。

CODE:

#include 

int main(void) {
	void* a;
	void* b;
	void* c;
	a=b=c=NULL;
	__asm__ volatile (
		"mov %%esp,%%eax\n\t"
		"push %%eax\n\t"
		"mov %%esp,%%ebx\n\t"
		"pop %%eax\n\t"
		"mov %%esp,%%ecx\n\t"
		"mov %%eax,%0\n\t"
		"mov %%ebx,%1\n\t"
		"mov %%ecx,%2\n\t"
	: "=m"(a),"=m"(b),"=m"(c)
	::"%eax","%ebx","%ecx");
	printf("%p\n%p\n%p\n",a,b,c);
	putchar('\n');
	a=b=c=NULL;
	__asm__ volatile (
		"mov %%esp,%0\n\t"
		"push %%eax\n\t"
		"mov %%esp,%1\n\t"
		"pop %%eax\n\t"
		"mov %%esp,%2\n\t"
	: "=m"(a),"=m"(b),"=m"(c)
	::);
	printf("%p\n%p\n%p\n",a,b,c);
	putchar('\n');
	a=b=NULL;
	__asm__ volatile (
		"mov %%esp,%0\n\t"
		"push %%eax\n\t"
		"lea %1,%%ebx\n\t"
		"pop %%eax\n\t"
		"mov %%ebx,%0\n\t"
	: "=m"(a),"=m"(b)
	::"%ebx");
	printf("%p\n%p\n",a,&b);
	return 0;
}
1.push/popによって%espの値が変わることの確認
2.push/popによって%espの値が変わることの確認(パラメータのメモリに直接入れようとする)
3.pushしたときのパラメータのメモリのアドレスと、元の変数のアドレスを比較

実行結果

CODE:

0022FF00
0022FEFC
0022FF00

0022FF00
00000000
0022FF00

0022FF14
0022FF18
1.pushすると%espが4減り、popすると%espが4増える
2.%espがずれている時は、うまくパラメータのメモリに代入できない
3.%espがずれると、パラメータのメモリのアドレスもずれる
なして?

では、実際に出力されたアセンブリを見てみましょう。

CODE:

	.file	"push_esp_test.c"
	.def	___main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
LC0:
	.ascii "%p\12%p\12%p\12\0"
LC1:
	.ascii "%p\12%p\12\0"
	.text
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
LFB6:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	pushl	%ebx
	andl	$-16, %esp
	subl	$32, %esp
	.cfi_offset 3, -12
	call	___main
	movl	$0, 20(%esp)
	movl	20(%esp), %eax
	movl	%eax, 24(%esp)
	movl	24(%esp), %eax
	movl	%eax, 28(%esp)
/APP
 # 8 "push_esp_test.c" 1
	mov %esp,%eax
	push %eax
	mov %esp,%ebx
	pop %eax
	mov %esp,%ecx
	mov %eax,28(%esp)
	mov %ebx,24(%esp)
	mov %ecx,20(%esp)
	
 # 0 "" 2
/NO_APP
	movl	20(%esp), %ecx
	movl	24(%esp), %edx
	movl	28(%esp), %eax
	movl	%ecx, 12(%esp)
	movl	%edx, 8(%esp)
	movl	%eax, 4(%esp)
	movl	$LC0, (%esp)
	call	_printf
	movl	$10, (%esp)
	call	_putchar
	movl	$0, 20(%esp)
	movl	20(%esp), %eax
	movl	%eax, 24(%esp)
	movl	24(%esp), %eax
	movl	%eax, 28(%esp)
/APP
 # 22 "push_esp_test.c" 1
	mov %esp,28(%esp)
	push %eax
	mov %esp,24(%esp)
	pop %eax
	mov %esp,20(%esp)
	
 # 0 "" 2
/NO_APP
	movl	20(%esp), %ecx
	movl	24(%esp), %edx
	movl	28(%esp), %eax
	movl	%ecx, 12(%esp)
	movl	%edx, 8(%esp)
	movl	%eax, 4(%esp)
	movl	$LC0, (%esp)
	call	_printf
	movl	$10, (%esp)
	call	_putchar
	movl	$0, 24(%esp)
	movl	24(%esp), %eax
	movl	%eax, 28(%esp)
/APP
 # 33 "push_esp_test.c" 1
	mov %esp,28(%esp)
	push %eax
	lea 24(%esp),%ebx
	pop %eax
	mov %ebx,28(%esp)
	
 # 0 "" 2
/NO_APP
	movl	28(%esp), %eax
	leal	24(%esp), %edx
	movl	%edx, 8(%esp)
	movl	%eax, 4(%esp)
	movl	$LC1, (%esp)
	call	_printf
	movl	$0, %eax
	movl	-4(%ebp), %ebx
	leave
	.cfi_restore 5
	.cfi_restore 3
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
LFE6:
	.def	_printf;	.scl	2;	.type	32;	.endef
	.def	_putchar;	.scl	2;	.type	32;	.endef
printfの引数の並びから、インラインアセンブリ外では28(%esp)がa、24(%esp)がb、20(%esp)がcに相当すると推測できます。
そして、3番目の実験のコードを見ると、%1に相当するところに24(%esp)とそのまま書かれています!
じぇじぇじぇ!これでは、ずれて当たり前ですね。
でも、これを回避するのは難しそうです。

しかし、原因がわかったらこっちのもんです。
与えられたパラメータを最初に全部メモリに乗せてしまい、その後%espをいじるようにすればいいのです!
今回の場合

CODE:

	__asm__ volatile (
		"mov %0,%%eax\n\t"
		"mov %1,%%ebx\n\t"
		"mov %2,%%ecx\n\t"
		"mov %3,%%edx\n\t"
		"sub $32,%%esp\n\t"
		"mov %%eax,16(%%esp)\n\t"
		"mov %%ebx,20(%%esp)\n\t"
		"mov %%ecx,24(%%esp)\n\t"
		"mov %%edx,28(%%esp)\n\t"
		/* 略 16(%%esp)を%0、20(%%esp)を%1、24(%%esp)を%2、28(%%esp)を%3として使う */
		"add $32,%%esp\n\t"
	: /* no output */
	: "m"(pDIB),"m"(pOut),"m"(width),"m"(height)
	: "%eax","%ebx","%ecx","%edx","%esi","%edi"
	);
という感じになりました。
今回はパラメータ(変数)に対して値を書き込むことはしなかったのですが、
書き込む必要があるときは、アドレスをleaでメモリに書き込み、アクセスするときにそのアドレスを一旦レジスタに書き戻す、
という処理が必要になりそうです。

【追記】
同じコードで実験したところ、Ideone.comではずれてしまいましたが、codepadではずれませんでした。
しかし、codepadはIdeone.comに比べて、標準入力を指定できない、(C言語の場合)math.hの三角関数が使えないなど劣っている点もあります。
それに、codepadではうまく動いても、どうせ環境依存です。(そもそもインラインアセンブリ自体環境依存ですが)
http://ideone.com/kyHh9T
http://ideone.com/nIfolQ
http://codepad.org/Fo7vdx5d
http://codepad.org/zbhgian1

【さらに追記】
gccのバージョンは、
ローカル : 4.7.2
Ideone : 4.8.1 (system("gcc -v");で確認)
codepad : 4.1.2 (systemが使えないようなので、aboutページで確認)
のようです。
古いコンパイラの方が直感的な挙動をするというのは、不思議ですね。
それとも、巫女ぐにょのように特殊なバージョンのつけ方をしていて、4.8.1より4.1.2の方が新しいのでしょうか?
しかし、gcc 4.1.2はVine Linuxにも搭載されていますが、OpenMPが使えない残念な子です。
どう考えても、インラインアセンブリの安定性よりOpenMPの方を取るべきだと思います。(個人の感想です)
最後に編集したユーザー みけCAT on 2013年9月08日(日) 09:58 [ 編集 2 回目 ]
理由: サイトのコンパイラのバージョンを追加

コメントはまだありません。