CPU
 Computer >> コンピューター >  >> ハードウェア >> CPU

CPUの隠れた力を引き出す:CPUID命令でシステム情報を取得する方法

CPUの隠れた力を引き出す:CPUID命令でシステム情報を取得する方法

ブートローダーやカーネルを開発する際、ハードウェアとソフトウェアの互換性を確保し、パフォーマンスを最大限に引き出すためには、基盤となるアーキテクチャを深く理解することが不可欠です。

その中でも、システム情報を照会・取得するためにエンジニアが利用できる強力なツールの一つがCPUID命令です。本記事では、CPUID命令の基本概念から、実際のコードによる実装方法までを段階的に解説します。

CPUID命令とは?

CPUID命令は、現代のx86およびx86-64プロセッサの心臓部に組み込まれている低レベル命令です。ソフトウェアからプロセッサ自身や、そのサポート機能に関する情報を直接照会できるようにするものです。

この命令を呼び出すことで、プロセッサのモデルやファミリー、内部キャッシュのサイズ、SIMDやハードウェア仮想化などの対応機能といった情報を収集できます。これにより、パフォーマンスの最適化や、サポート状況に応じた機能の動的な有効化・無効化が可能になります。

ブートローダーやカーネルの開発者にとって、プロセッサがどのような機能を備えているか(ハードウェア仮想化、キャッシュサイズ、SIMD命令など)を事前に把握することは、システムを効率的に動作させ、異なるCPU間でのコード互換性を確保するうえで極めて重要です。CPUID命令を活用すれば、実行環境のプロセッサに応じてカーネルの挙動を動的に調整できます。

この記事では、CPUID命令がシステムで利用可能かどうかを確認する方法、その仕組み、そして呼び出しによって得られる情報について学びます。

前提知識と必要な環境

  • アセンブリ言語の基礎知識(本記事の例ではFASMを使用)
  • オペレーティングシステム/カーネルに関する基礎知識
  • GDBなどの低レベルデバッグツール、またはQEMUなどのハードウェアエミュレータを使い、さまざまなプラットフォームでブートローダー/カーネルをテストできる環境

ステップ1:CPUID命令の利用可否を確認する

CPUID命令を実行する前に、プロセッサがその命令をサポートしているかどうかを確認することが重要です。すべてのCPUがこの機能を持つとは限らないからです。以下のコードは、EFLAGSレジスタのIDビット(ビット21)を変更してテストすることで、CPUID命令の利用可否を判定します。

以下はwiki.osdev.orgより引用した、EFLAGSレジスタの各ビットを示した図です。

CPUの隠れた力を引き出す:CPUID命令でシステム情報を取得する方法

プロセッサがこのビットの切り替えを許可していればCPUIDはサポートされており、許可しなければサポートされていません。検出の仕組みは以下の通りです。

※多くの方は「リアルモードでは32ビットレジスタにアクセスできない」と考えていますが、それは誤りです。実際にはすべての32ビットレジスタが使用可能です。

cpuid_check:
 pusha ; save state
 pushfd ; Save EFLAGS
 pushfd ; Store EFLAGS
 xor dword [esp],0x00200000 ; Invert the ID bit in stored EFLAGS
 popfd ; Load stored EFLAGS (with ID bit inverted)
 pushfd ; Store EFLAGS again (ID bit may or may not be inverted)
 pop eax ; eax = modified EFLAGS (ID bit may or may not be inverted)
 xor eax,[esp] ; eax = whichever bits were changed
 popfd ; Restore original EFLAGS
 and eax,0x00200000 ; eax = zero if ID bit can't be changed, else non-zero
 cmp eax,0x00
 je .cpuid_instruction_not_is_available
.cpuid_instruction_is_available:
 ;handle CPUID exists
.cpuid_instruction_not_is_available:
 ;handle CPUID isn't supported
.cpuid_check_end:
 popa ; restore state
 ret

各行の命令が果たす役割を順に見ていきましょう。

  • pusha:すべての汎用レジスタを保存し、処理終了時に元の状態へ復元できるようにします。
  • pushfd:現在のEFLAGSレジスタの状態を保存します。
  • pushfd:EFLAGSのコピーをさらにスタックに退避します。
  • xor dword [esp], 0x00200000:XOR演算により、退避したEFLAGSのIDビット(21)を反転します。
  • popfd:IDビットを反転した状態のEFLAGSを読み込みます。
  • pushfd:その状態のEFLAGSを再度スタックに退避します(IDビットが反転できたかどうかはこの時点では未確定)。
  • pop eax:退避しておいたEFLAGSの値をEAXレジスタに取り出します。
  • xor eax, [esp]:XOR演算の結果、EAXには「変化があったビット」だけが残ります。
  • popfd:元のEFLAGSを復元します。
  • and eax, 0x00200000:AND演算で他のビットをマスクし、21番目のビット(IDビット)のみを抽出します。この結果、EAXには0x00200000(IDビットが変化した=CPUID対応)または0x00(変化なし=非対応)のいずれかが入ります。
  • cmp eax, 0x00:CMP命令で直前の結果を判定します。EAXが0x00ならIDビットを変更できなかったことになり、プロセッサはCPUID命令をサポートしていません。ゼロ以外であればIDビットの反転に成功しており、CPUID命令が利用可能です。

CPUの機能フラグを取得する

CPUID命令は、EAXレジスタに設定する値によって異なる情報を返します。

mov eax, 0x1
cpuid

EAXに1を設定して呼び出すと、CPUIDはEDX(およびECX)にビットフィールド形式で機能フラグを返します。以下は主な定義の一覧です。なお、CPUブランドによって各ビットの意味が異なる場合があります(出典:wiki.osdev.org/CPUID)。

enum {
 CPUID_FEAT_ECX_SSE3 = 1 << 0,
 CPUID_FEAT_ECX_PCLMUL = 1 << 1,
 CPUID_FEAT_ECX_DTES64 = 1 << 2,
 CPUID_FEAT_ECX_MONITOR = 1 << 3,
 CPUID_FEAT_ECX_DS_CPL = 1 << 4,
 CPUID_FEAT_ECX_VMX = 1 << 5,
 CPUID_FEAT_ECX_SMX = 1 << 6,
 CPUID_FEAT_ECX_EST = 1 << 7,
 CPUID_FEAT_ECX_TM2 = 1 << 8,
 CPUID_FEAT_ECX_SSSE3 = 1 << 9,
 CPUID_FEAT_ECX_CID = 1 << 10,
 CPUID_FEAT_ECX_SDBG = 1 << 11,
 CPUID_FEAT_ECX_FMA = 1 << 12,
 CPUID_FEAT_ECX_CX16 = 1 << 13,
 CPUID_FEAT_ECX_XTPR = 1 << 14,
 CPUID_FEAT_ECX_PDCM = 1 << 15,
 CPUID_FEAT_ECX_PCID = 1 << 17,
 CPUID_FEAT_ECX_DCA = 1 << 18,
 CPUID_FEAT_ECX_SSE4_1 = 1 << 19,
 CPUID_FEAT_ECX_SSE4_2 = 1 << 20,
 CPUID_FEAT_ECX_X2APIC = 1 << 21,
 CPUID_FEAT_ECX_MOVBE = 1 << 22,
 CPUID_FEAT_ECX_POPCNT = 1 << 23,
 CPUID_FEAT_ECX_TSC = 1 << 24,
 CPUID_FEAT_ECX_AES = 1 << 25,
 CPUID_FEAT_ECX_XSAVE = 1 << 26,
 CPUID_FEAT_ECX_OSXSAVE = 1 << 27,
 CPUID_FEAT_ECX_AVX = 1 << 28,
 CPUID_FEAT_ECX_F16C = 1 << 29,
 CPUID_FEAT_ECX_RDRAND = 1 << 30,
 CPUID_FEAT_ECX_HYPERVISOR = 1 << 31,
 CPUID_FEAT_EDX_FPU = 1 << 0,
 CPUID_FEAT_EDX_VME = 1 << 1,
 CPUID_FEAT_EDX_DE = 1 << 2,
 CPUID_FEAT_EDX_PSE = 1 << 3,
 CPUID_FEAT_EDX_TSC = 1 << 4,
 CPUID_FEAT_EDX_MSR = 1 << 5,
 CPUID_FEAT_EDX_PAE = 1 << 6,
 CPUID_FEAT_EDX_MCE = 1 << 7,
 CPUID_FEAT_EDX_CX8 = 1 << 8,
 CPUID_FEAT_EDX_APIC = 1 << 9,
 CPUID_FEAT_EDX_SEP = 1 << 11,
 CPUID_FEAT_EDX_MTRR = 1 << 12,
 CPUID_FEAT_EDX_PGE = 1 << 13,
 CPUID_FEAT_EDX_MCA = 1 << 14,
 CPUID_FEAT_EDX_CMOV = 1 << 15,
 CPUID_FEAT_EDX_PAT = 1 << 16,
 CPUID_FEAT_EDX_PSE36 = 1 << 17,
 CPUID_FEAT_EDX_PSN = 1 << 18,
 CPUID_FEAT_EDX_CLFLUSH = 1 << 19,
 CPUID_FEAT_EDX_DS = 1 << 21,
 CPUID_FEAT_EDX_ACPI = 1 << 22,
 CPUID_FEAT_EDX_MMX = 1 << 23,
 CPUID_FEAT_EDX_FXSR = 1 << 24,
 CPUID_FEAT_EDX_SSE = 1 << 25,
 CPUID_FEAT_EDX_SSE2 = 1 << 26,
 CPUID_FEAT_EDX_SS = 1 << 27,
 CPUID_FEAT_EDX_HTT = 1 << 28,
 CPUID_FEAT_EDX_TM = 1 << 29,
 CPUID_FEAT_EDX_IA64 = 1 << 30,
 CPUID_FEAT_EDX_PBE = 1 << 31
};

上記の主要なCPU機能について、簡単に解説します。

  • PCLMUL、AES:高速な暗号化・復号化を実現する暗号関連の命令セット
  • VMX、SMX:仮想マシンの実行を支えるハードウェア仮想化サポート
  • SSE3、SSSE3、SSE4.1、SSE4.2、AVX:マルチメディア処理・数値計算・ベクトル演算を高速化するSIMD命令セット
  • FMA:融合積和演算(Fused Multiply-Add)。浮動小数点演算の性能を向上させる
  • RDRAND:ハードウェア乱数生成器
  • X2APIC:マルチプロセッサ環境における高度な割り込み処理
  • PCID:コンテキストスイッチ時のメモリ管理を最適化する
  • FPU:数学演算を高速化するハードウェア浮動小数点ユニット
  • PAE:Physical Address Extension。4GBを超えるメモリのアドレッシングを可能にする
  • HTT:単一のCPUコアで複数スレッドを処理できるようにする(ハイパースレッディング)
  • PAT、PGE:キャッシュ制御やページマッピングを管理するメモリ管理機能
  • MMX、SSE、SSE2:マルチメディア処理向けの旧世代SIMD命令セット

CPUベンダー文字列を取得する

CPUのベンダー文字列(製造元を示す識別子)を取得したい場合は、CPUID命令を呼び出す前にEAXを0x0に設定します。

mov eax, 0x0
cpuid

ベンダー文字列は、IntelやAMDといったCPUベンダーが固有に持つ識別子です。代表的な例としては、Intelプロセッサなら「GenuineIntel」、AMDプロセッサなら「AuthenticAMD」があります。これは基本的にCPUの製造元を表すものです。

ベンダー文字列を取得できると、カーネルはCPUの製造元を特定できます。これは非常に有用です。というのも、同じ機能でも製造元によって実装の詳細が異なるケースがあるからです。また、ソフトウェアやドライバ側でも、CPUメーカーに応じて挙動を切り替えることで互換性を確保しやすくなります。

この使い方では、ベンダーID文字列はEBX、EDX、ECXの3つのレジスタに分割して返されます。これらをバッファへ順に書き込めば、全12文字の文字列を組み立てられます。

以下、具体的なサンプルコードを見ていきましょう。

ステップ1:バッファを用意する

まず、12バイト以上を格納できるバッファを作成します。

buffer: db 12 dup(0), 0xA, 0xD, 0

ステップ2:文字列出力関数を作成する

次に、文字列を画面へ出力するための関数を用意します。

このアセンブリコードは、文字列を1文字ずつ読み込み、BIOS割り込み0x10を使って画面に表示します。print関数は文字列を先頭から順に走査し、lodsb命令で1文字ずつALレジスタへロードします。

続いてprint_char関数が割り込み0x10を呼び出して文字を描画します。文字列の終端(ヌル終端文字)に到達すると、ループが終了します。

print_string:
 call print
 ret
print:
.loop: 
 lodsb ;read character to al and then increment
 cmp al ,0 ;check if we reached the end
 je .done ;we reached null terminator, finish
 call print_char ;print character
 jmp .loop ;jump back into the loop
.done:
 ret
print_char:
 mov ah, 0eh
 int 0x10
 ret

ステップ3:バッファに格納して表示する

最後に、pusha命令で現在のレジスタ状態を保存し、EAXに0x0を設定してcpuidを呼び出した後、EBX、EDX、ECXの内容をバッファへ書き込みます。その後、print_stringを呼び出せば、ベンダー文字列が画面に表示されます。

get_cpu_vendor:
 pusha
 mov eax, 0x0
 cpuid
 mov [buffer], ebx
 mov [buffer + 4], edx
 mov [buffer + 8], ecx
 mov si, buffer 
 call print_string
 popa
 ret

CPUID命令がEAXレジスタに渡された値に応じてどんな情報を返せるのか、さらに詳しく知りたい方は、以下のデータベースが参考になります。
x86-cpuid-db(GitLab)

まとめ

CPUID命令を理解し活用することで、幅広い種類のプロセッサに柔軟に対応できるブートローダーやカーネルを構築できます。命令の利用可否を正しく検出し、CPUの機能フラグ、キャッシュサイズ、サポートされる技術といった重要なシステム情報を取得できれば、パフォーマンスと互換性の両面で大きなメリットが得られます。

この記事を通じて、CPUID命令を自分のプロジェクトで試してみるための基礎知識とツールが揃ったはずです。ぜひ実際にコードを書いて、CPUの世界を深く掘り下げてみてください。

それでは、楽しいコーディングライフを!


  1. CPU温度を監視する方法|適正温度の目安と高温時の対処法

    PCのCPU温度をチェックすることは、車のオイル点検に似ています。毎日行う必要はありませんが、数ヶ月に一度は目を通しておきたい項目です。特に、ゲーミング向けの高性能CPUなどで日常的に高負荷をかけている方は、定期的な温度管理が重要になります。幸い、CPU温度の確認はとても簡単です。PCケースを開けて温度計を差し込む必要はありません。すべてのCPUにはデジタル温度センサーが内蔵されており、専用ソフトでその数値を読み取るだけで済みます。この記事では、CPUにとって健康的な温度範囲、温度の確認方法、そして温度が高すぎる場合の対処法について詳しく解説します。CPUの適正温度はどのくらい?CPUがアイド

  2. Intel K付きCPUと非Kモデルの違いを徹底解説!あなたのPC構築に最適なプロセッサーはどっち?

    (画像クレジット:Intel)はじめにPCを自作・アップグレードする際、プロセッサー選びは性能を左右する最重要ポイントです。IntelのCPUラインナップでは、「K」付きモデルと「K」なしモデルのどちらを選ぶかが大きな分かれ目となります。この2つのカテゴリーは、特にオーバークロックへの対応可否といった性能面で大きく異なり、カジュアルなユーザーからエンスージアスト、ゲーマーまで、ユーザーのタイプによって適した選択肢が変わってきます。本記事では、IntelのK付きCPUと非Kモデルの違いを詳しく解説し、あなたのニーズに最適なCPUを選ぶための判断材料を提供します。Intel K付きCPUと非Kモデ