「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
#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を使った時のパラメータの値とアドレスを取得するコードです。
実行結果(最適化なしと-O2を試しましたが、実行結果は変わりませんでした) じぇじぇじぇ!やっぱり値もアドレスもずれてる!
では、もう少し単純な実験を。
#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;
}
2.push/popによって%espの値が変わることの確認(パラメータのメモリに直接入れようとする)
3.pushしたときのパラメータのメモリのアドレスと、元の変数のアドレスを比較
実行結果 1.pushすると%espが4減り、popすると%espが4増える
2.%espがずれている時は、うまくパラメータのメモリに代入できない
3.%espがずれると、パラメータのメモリのアドレスもずれる
なして?
では、実際に出力されたアセンブリを見てみましょう。
.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
そして、3番目の実験のコードを見ると、%1に相当するところに24(%esp)とそのまま書かれています!
じぇじぇじぇ!これでは、ずれて当たり前ですね。
でも、これを回避するのは難しそうです。
しかし、原因がわかったらこっちのもんです。
与えられたパラメータを最初に全部メモリに乗せてしまい、その後%espをいじるようにすればいいのです!
今回の場合
__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の方を取るべきだと思います。(個人の感想です)