インラインアセンブラでCPUIDその2
前回のエントリ( d:id:teru_kusu:20100203 )で次のようなコードをx86_64用のコードにコンパイルすると実行しても問題ないが、i386用のコードにコンパイルすると実行時にBus Errorと言うメッセージと共にクラッシュすると言う現象が発生。
この原因を今回は調べてみる。
現象のおさらい
Cソース(cpuid.c)
#include<stdio.h> int main() { unsigned int cpuid, result, physical_address_bits, linear_address_bits; cpuid = 0x80000008; __asm__ ("cpuid\n\t" : "=a"(result): "0"(cpuid)); physical_address_bits = result & 0xFF; linear_address_bits = (result >> 8) & 0xFF; printf("result=0x%x\n", result); printf("physical_address_bits=0x%x, (%d)\n", physical_address_bits, physical_address_bits); printf("linear_address_bits=0x%x, (%d)\n", linear_address_bits, linear_address_bits); return 0; }
gcc --save-temps -arch i386 -g -o cpuid.s cpuid.c
実行
$ ./cpuid Bus error
GDBで実行
(gdb) run Starting program: cpuid Reading symbols for shared libraries +. done Program received signal EXC_BAD_ACCESS, Could not access memory. Reason: KERN_PROTECTION_FAILURE at address: 0x000000aa 0x91030caa in __vfprintf () (gdb) bt #0 0x91030caa in __vfprintf () #1 0x9104e9bf in vfprintf_l () #2 0x91099790 in printf () #3 0x00001eec in main ()
で、ebxレジスタをインラインアセンブラの前後で値を保持する用にソースを変更するとエラーは出なくなった。
__asm__ ("pushl %%ebx\n\t" "cpuid\n\t" "popl %%ebx\n\t": "=a"(result): "0"(cpuid));
解決編
アセンブラを出力してみる。
gcc -S -arch i386 -o cpuid.s cpuid.c
こんなアセンブラが吐かれた。
cpuid.s
1: .cstring 2: LC0: 3: .ascii "result=0x%x\12\0" 4: .align 2 5: LC1: 6: .ascii "physical_address_bits=0x%x, (%d)\12\0" 7: .align 2 8: LC2: 9: .ascii "linear_address_bits=0x%x, (%d)\12\0" 10: .text 11: .globl _main 12: _main: 13: pushl %ebp 14: movl %esp, %ebp 15: pushl %ebx 16: subl $36, %esp 17: call L3 18: "L00000000001$pb": 19: L3: 20: popl %ebx 21: movl $-2147483640, -12(%ebp) 22: movl -12(%ebp), %eax 23: cpuid 24: 25: movl %eax, -16(%ebp) 26: movzbl -16(%ebp),%eax 27: movl %eax, -20(%ebp) 28: movl -16(%ebp), %eax 29: shrl $8, %eax 30: andl $255, %eax 31: movl %eax, -24(%ebp) 32: movl -16(%ebp), %eax 33: movl %eax, 4(%esp) 34: leal LC0-"L00000000001$pb"(%ebx), %eax 35: movl %eax, (%esp) 36: call _printf 37: movl -20(%ebp), %eax 38: movl %eax, 8(%esp) 39: movl -20(%ebp), %eax 40: movl %eax, 4(%esp) 41: leal LC1-"L00000000001$pb"(%ebx), %eax 42: movl %eax, (%esp) 43: call _printf 44: movl -24(%ebp), %eax 45: movl %eax, 8(%esp) 46: movl -24(%ebp), %eax 47: movl %eax, 4(%esp) 48: leal LC2-"L00000000001$pb"(%ebx), %eax 49: movl %eax, (%esp) 50: call _printf 51: movl $0, %eax 52: addl $36, %esp 53: popl %ebx 54: leave 55: ret 56: .subsections_via_symbols
よく見ると17〜20行目で、20行目を指してるeip(インストラクションポインタ)の値をebxに格納して、48行目ではebx+相対アドレスでprintf用のフォーマット文字列のアドレスをprintfに渡そうとしている。
ところが23行目のcpuidがebxを0にしてしまったからprintfが不正なアドレスにアクセスして落ちていたぽい。
検証してみよう。
逆アセンブルしたところ。
(gdb) disas main Dump of assembler code for function main: 0x00001ea6: push %ebp 0x00001ea7 : mov %esp,%ebp 0x00001ea9 : push %ebx 0x00001eaa : sub $0x24,%esp 0x00001ead : call 0x1eb2 0x00001eb2 : pop %ebx 0x00001eb3 : movl $0x80000008,-0xc(%ebp) 0x00001eba : mov -0xc(%ebp),%eax 0x00001ebd : cpuid 0x00001ebf : mov %eax,-0x10(%ebp) 0x00001ec2 : movzbl -0x10(%ebp),%eax 0x00001ec6 : mov %eax,-0x14(%ebp) 0x00001ec9 : mov -0x10(%ebp),%eax 0x00001ecc : shr $0x8,%eax 0x00001ecf : and $0xff,%eax 0x00001ed4 : mov %eax,-0x18(%ebp) 0x00001ed7 : mov -0x10(%ebp),%eax 0x00001eda : mov %eax,0x4(%esp) 0x00001ede : lea 0xaa(%ebx),%eax 0x00001ee4 : mov %eax,(%esp) 0x00001ee7 : call 0x1f36
ebxにeipが読み込まれるところを確認。
(gdb) b *0x00001eb3 Breakpoint 1 at 0x1eb3 (gdb) run Starting program: /Users/teru/Documents/low_level/cpuid Reading symbols for shared libraries +. done Breakpoint 1, 0x00001eb3 in main () (gdb) i reg ebx ebx 0x1eb2 7858 (gdb)
main+12のアドレスである0x1eb2がebxに設定されている。
次にcpuid命令の前後でのebxの値を確認。
(gdb) b *0x1ebd Breakpoint 2 at 0x1ebd (gdb) c Continuing. Breakpoint 2, 0x00001ebd in main () (gdb) i reg ebx ebx 0x1eb2 7858 (gdb) si 0x00001ebf in main () (gdb) i reg ebx ebx 0x0 0
cpuid命令によってebxが0に設定されている。
次にprintf()に渡す文字列のアドレスを確認。
このアドレスはmain+56の行で計算され、eaxに格納されているのでeaxの値も確認する。
(gdb) b *0x1ee4 Breakpoint 3 at 0x1ee4 (gdb) c Continuing. Breakpoint 3, 0x00001ee4 in main () (gdb) i reg eax ebx eax 0xaa 170 ebx 0x0 0
本来ならばeaxの値は 0x1eb2 + 0xaa であるべきなのに、ebxが0になってしまっているので 0 + 0xaa = 0xaa になってしまっている。
さらに続けると・・・
(gdb) c Continuing. Program received signal EXC_BAD_ACCESS, Could not access memory. Reason: KERN_PROTECTION_FAILURE at address: 0x000000aa 0x91030caa in __vfprintf ()
アドレス0x000000aaへの不正なメモリアクセスを行ったと言うエラー。
このアドレスは上のeaxの値(=フォーマット文字列のアドレス)0xaaと一致している。
というわけでエラーの原因は予想通り。
で、ebxを本来の値をキープするようにしてやればエラーは解消したと。
実はもうひとつの解決法として、ソースは変更なしでコンパイルオプションに -fdynamic-no-pic をつけると言う手もある。
$ gcc -mdynamic-no-pic -arch i386 -o cpuid cpuid.c $ ./cpuid result=0x3024 physical_address_bits=0x24, (36) linear_address_bits=0x30, (48)
pic・・・何かキーワードっぽいのが出てきた。
続きはまた今度。