インラインアセンブラでCPUID

インテル系(x86)アーキテクチャのcpuid命令を使用して、
プロセッサがサポートするリニアアドレスと物理アドレスのビット幅を取得してみる。


プログラムがメモリにアクセスするためには
CPUのメモリ管理機構を経由することになる。
その中でアドレス変換が行われる。
↓こんな感じ

論理アドレス(セグメント:オフセット)→ [セグメテーション機構]
    → リニアアドレス→ [ページング機構] → 物理アドレス

最近のCPUは64bitだとか言われているけれど、
64bitのリニアアドレスや物理アドレスが使えるわけではない。
実際にサポートされているアドレスのビット幅は、
最近のIntel64対応プロセッサならば cpuid という命令で取得できる。

さっそくやってみよう。

  • 環境


インテルのマニュアルによると

  1. eaxレジスタに0x80000008をセットしてcpuid命令を実行すると・・
  2. eaxレジスタにリニア/物理アドレスのビット幅が設定される

と、ある。
ちなみに最初に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   0x1ebd 
 eflags         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   0x1ebf 
 eflags         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モードと互換モード・・・たぶん。
他の動作モード(リアルモードやプロテクトモード)でも値変わらないのかなー。
そのうち試そう。