インラインアセンブラで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・・・何かキーワードっぽいのが出てきた。
続きはまた今度。