インラインアセンブラでCPUID
インテル系(x86)アーキテクチャのcpuid命令を使用して、
プロセッサがサポートするリニアアドレスと物理アドレスのビット幅を取得してみる。
プログラムがメモリにアクセスするためには
CPUのメモリ管理機構を経由することになる。
その中でアドレス変換が行われる。
↓こんな感じ
論理アドレス(セグメント:オフセット)→ [セグメテーション機構] → リニアアドレス→ [ページング機構] → 物理アドレス
最近のCPUは64bitだとか言われているけれど、
64bitのリニアアドレスや物理アドレスが使えるわけではない。
実際にサポートされているアドレスのビット幅は、
最近のIntel64対応プロセッサならば cpuid という命令で取得できる。
さっそくやってみよう。
- 環境
インテルのマニュアルによると
と、ある。
ちなみに最初にeaxに設定しておく値によっては、
これ以外にもCPUに関する様々な情報が取得可能。
出力(eaxレジスタ)の内訳はこんな感じ。
ビット | 内容 |
---|---|
7-0 | 物理アドレスのビット数 |
15-8 | リニアアドレスのビット数 |
31-16 | 予約済み(0埋め) |
結果をprintfで簡単に出すためにインラインアセンブラで書いてみよう。
インラインアセンブラは使った事がないので勉強も兼ねて。
ソース(cpuid.c)
#include<stdio.h> int main() { unsigned int cpuid, result, physical_address_bits, linear_address_bits; cpuid = 0x80000008; __asm__ ("cpuid": "=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; }
__asm__の行がインラインアセンブラ。
Cのオペランドを使ってアセンブラの命令が書けることがインラインアセンブラの魅力らしい。
gccのインラインアセンブラは
__asm__ (アセンブラテンプレート : 出力オペランド : 入力オペランド : ワークレジスタ);
と言うフォーマットをしている。
詳細はGCCのマニュアル参照。
とりあえずここでは「アセンブラ部分の実行前にeaxレジスタにcpuid(変数の方)の値を入れて、実行後にはeaxレジスタの値をresultに入れてね」ということが書いてあるんだなー程度で。
コンパイル
$ gcc --save-temps -o cpuid cpuid.c
どんなアセンブラが吐かれたのか見るために
一時ファイルを残すオプション --save-temps を指定。
cpuid.s がアセンブラコード。
cpuid.sから抜粋
1: _main: 2: pushq %rbp 3: movq %rsp, %rbp 4: subq $16, %rsp 5: movl $-2147483640, -4(%rbp) 6: movl -4(%rbp), %eax 7: cpuid 8: movl %eax, -8(%rbp)
- 1行目からmain()開始
- 2〜3行目は呼び出された側の関数でのお決まりのスタック操作
- 4行目でローカル変数用の領域をスタック上に確保。Intel系アーキテクチャではスタックはだいたい仮想メモリ空間の上の方に配置されていて、メモリ下位方向に成長する。rspレジスタはスタック領域の下端を指しているのでこれをビヨーンと引き下げて領域を確保している。(sizeof(int) * ローカル変数4個 = 16byte)
- 5行目。-4(%rbp)はcpuid用の領域、-2147483640は0x80000008の事なので、ここは cpuid = 0x80000008 の部分。
- 6行目。cpuidの値をeaxレジスタに設定。
- 7行目。cpuid実行。
- 8行目。eaxレジスタの値をresultに設定。(-8(%rbp)はresult用の領域)
おおお。想定通りなアセンブラになってる。
ちなみにできたバイナリは
$ file ./cpuid ./cpuid: Mach-O 64-bit executable x86_64
64bit版。64bit版はどのあたりが64bit的なのかはまたいずれ・・・。
実行結果(()内は10進数)
$ ./cpuid result = 0x3024 physical_address_bits = 0x24, (36) linear_address_bits = 0x30, (48)
うちのCPUではこんな感じでしたー。
ついでに扱えるメモリ容量をビット数から計算すると
32bit版でも試す
特にアーキテクチャ指定なしでコンパイルすると64bit版バイナリができる。
ターゲットアーキテクチャにi386を指定して
32bit版バイナリを作成して実行してみるとどうなるか。
$ gcc --save-temps -arch i386 -o cpuid cpuid.c
確認
$ file ./cpuid ./cpuid: Mach-O executable i386
32ビット版だな。
実行
$ ./cpuid Bus error
(゜∀゜;)
もしかしたらアドレス上限が4GBになったりするのかなと期待していたのに。
Bus erorrってなんや!!。
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 ()
そういえば、cpuid命令の出力レジスタはeaxだけではなかった。
ebx,ecx,edxも出力レジスタとなる。
0x80000008を指定した場合はeax以外のレジスタは実行後0に設定される。
cpuid実行直前のレジスタ状態
eax 0x80000008 -2147483640 ecx 0xbffff8fc -1073743620 edx 0x0 0 ebx 0x1eb2 7858 esp 0xbffff8b0 0xbffff8b0 ebp 0xbffff8d8 0xbffff8d8 esi 0x0 0 edi 0x0 0 eip 0x1ebd 0x1ebdeflags 0x282 642 cs 0x1b 27 ss 0x23 35 ds 0x23 35 es 0x23 35 fs 0x0 0 gs 0xf 15
ebxとecxには何か0以外の値が入っている。
cpuid実行直後のレジスタの状態
eax 0x3024 12324 ecx 0x0 0 edx 0x0 0 ebx 0x0 0 esp 0xbffff8b0 0xbffff8b0 ebp 0xbffff8d8 0xbffff8d8 esi 0x0 0 edi 0x0 0 eip 0x1ebf 0x1ebfeflags 0x282 642 cs 0x1b 27 ss 0x23 35 ds 0x23 35 es 0x23 35 fs 0x0 0 gs 0xf 15
確かにebx,ecxの値が0に変わっている。
これがこの後実行されるprintfに何か悪影響を及ぼした模様。
ワークレジスタにebx,ecx,edx等を指定しても
実行時エラーやコンパイルエラーでうまく行かなかった。
ebxレジスタの値を”asm”実行前に退避しておいて、
実行後に復帰させるようにするとエラーは無くなった。
cpuid.c(修正版抜粋)
__asm__ ("pushl %%ebx\n\t" "cpuid\n\t" "popl %%ebx\n\t": "=a"(result): "0"(cpuid));
- なぜ64bit版ではエラーにならなかったのか
- なぜebxなのか。
と言うのはまた別のお話。たぶん呼出規約が絡んでる。
実行結果
$ ./cpuid result=0x3024 physical_address_bits=0x24, (36) linear_address_bits=0x30, (48)
アドレスのビット幅は64bit版と変わらず。
実行中のプログラムが32bit版か64bit版かには関係ないのか。
今回試したのはcpuの動作モードで言うとIA-32eモードの
サブモードで、64bitモードと互換モード・・・たぶん。
他の動作モード(リアルモードやプロテクトモード)でも値変わらないのかなー。
そのうち試そう。