アドレス空間とアドレッシングモード ─ 物理/仮想/I/Oマップ
ポインタやMMIOが「どこを指すか」を原理から掴めます。物理/仮想/I/O空間の分け方とカーネル分割、ベース+変位などの有効アドレス計算まで一気通貫で理解できます。
- 1.アドレス空間は番地の集合で、物理アドレス空間にはDRAMだけでなくMMIOやROMがメモリマップで割り付けられる。x86はI/O命令で別系統のポート空間も持つが、現代のデバイスは大半がMMIO側に寄る。
- 2.仮想アドレス空間はプロセスごとに独立で、ページテーブルとMMU/TLBが物理へ変換する。上位をカーネル、下位をユーザーに分けて固定写像し、特権ビットで保護する。
- 3.アドレッシングモードは即値・レジスタ・ベース+変位・インデックス・スケールなどがあり、有効アドレスはベース+インデックス×スケール+変位で計算してから変換とアクセスに渡される。
アドレス空間とは番地の集合である
アドレス空間とは、ある主体が指し示せる番地(アドレス)の全体集合です。n ビットのアドレスを使えば 0 から 2^n - 1 までの 2^n 個の番地を区別でき、これが空間のサイズになります。32 ビットなら 4 GiB、48 ビットなら 256 TiB です。ここで重要なのは、アドレス空間は「メモリの容量」ではなく「区別できる番地の範囲」だという点です。番地の先に DRAM があるとは限らず、デバイスのレジスタや何も存在しない穴(ホール)であることもあります。
主体が誰かによって空間は分かれます。CPU コアが命令で扱う仮想アドレス空間、メモリコントローラやデバイスがバス上で見る物理アドレス空間、そして x86 が持つ独立したI/O ポート空間です。これらは別々の番地体系で、変換や写像によって結び付けられます。
物理アドレス空間とメモリマップ
物理アドレス空間は、CPU の外側(バス)に出た番地が指す世界です。ここには DRAM がまるごと割り付けられますが、それだけではありません。同じ番地体系の中に、デバイスのレジスタ・ROM(ファームウェア)・フレームバッファなどが区間を割り当てられて同居します。この割り付けの設計図をメモリマップと呼びます。
物理アドレス空間(例: 下位4GiB付近)
0x0000_0000 ┌────────────┐
│ DRAM │ ← 通常の主記憶
0x8000_0000 ├────────────┤
│ (ホール) │ ← 何もない/予約
0xC000_0000 ├────────────┤
│ MMIO │ ← デバイスレジスタ
0xFEE0_0000 │ Local APIC │
0xFFFF_0000 │ Boot ROM │ ← リセットベクタ
0xFFFF_FFFF └────────────┘
DRAM が 4 GiB あっても、その一部の番地が MMIO に取られていると、その範囲の DRAM は隠れて見えなくなります(メモリホール)。これが昔の 32 ビット機で「4 GB 積んでも 3.x GB しか使えない」現象の正体です。
MMIO とポートマップド I/O
CPU がデバイスのレジスタを読み書きする方式は、大きく2系統あります。
| 項目 | メモリマップドI/O (MMIO) | ポートマップドI/O (PMIO) |
|---|---|---|
| 番地体系 | 物理アドレス空間に同居 | 独立したI/O空間 |
| アクセス命令 | 通常のload/store | 専用命令(IN/OUT) |
| 空間サイズ | アドレス幅ぶん広大 | x86は64KiBと狭い |
| キャッシュ | uncacheable指定が必須 | そもそも対象外 |
| 採用 | Arm/RISC-V等ほぼ全て | x86の一部レガシ |
メモリマップド I/O は、デバイスレジスタを物理アドレス空間の一区画に写像し、普通の load/store でアクセスします。専用命令が要らず、ポインタ経由で扱えるのが利点で、Arm や RISC-V はこの方式に統一しています。
ポートマップド I/O は x86 固有で、メモリとは別の 16 ビット幅(0〜65535)のポート空間を持ち、IN/OUT 命令でのみアクセスします。バス上では専用の信号でメモリアクセスと区別されます。歴史的経緯で残るものの、PCIe など現代のデバイスは設定レジスタもデータ転送も MMIO に寄せており、PMIO は縮小傾向です。
デバイスレジスタは「読むたびに値が変わる」「書くと副作用が起きる」ため、CPU が値をキャッシュしたり投機的に先読みしたり、書き込みをまとめたりすると正しく動きません。そのため MMIO 領域は uncacheable(または write-combining)に設定し、メモリオーダリングも厳しく扱う必要があります。メモリコンシステンシモデルで扱う順序保証が、ここで実害として効いてきます。
仮想アドレス空間とカーネル/ユーザー分割
仮想アドレス空間は、各プロセスに独立して与えられる番地の集合です。プロセスはあたかも空間全体を独占しているかのように振る舞え、互いのアドレスは衝突しません。CPU が命令で扱う番地はすべて仮想で、MMU がページ単位でこれを物理へ変換します。詳細は仮想メモリとTLBの内部で扱いますが、ここでは空間の構成に注目します。
64 ビット機でも実装するのは全 64 ビットではなく、典型的には 48 ビット(256 TiB)や 57 ビット程度です。この空間を上位と下位に二分し、上位を全プロセス共通のカーネル空間、下位を各プロセス固有のユーザー空間に割り当てるのが定石です。
仮想アドレス空間(x86-64, 48bit実装の例)
0x0000_0000_0000_0000 ┌──────────────┐
│ ユーザー空間 │ ← プロセス固有
│ (コード/ヒープ/ │ 下位アドレス
│ スタック) │
0x0000_7FFF_FFFF_FFFF └──────────────┘
(非正準アドレスの穴)
0xFFFF_8000_0000_0000 ┌──────────────┐
│ カーネル空間 │ ← 全プロセス共通
0xFFFF_FFFF_FFFF_FFFF └──────────────┘
カーネル空間を高位に固定写像しておくと、システムコールや割り込みで特権モードへ入ったとき、ページテーブルを切り替えずにカーネルのコードとデータへ到達できます。ユーザーコードがカーネル領域を触ろうとすると、ページテーブルエントリの特権ビット(U/S)により保護違反となり、ページフォルトが上がります。実装上は 48 ビットの有効ビットを最上位まで符号拡張した正準(canonical)アドレスだけが有効で、中間の値はアクセス自体が例外になります。これにより上位・下位の二領域がきれいに分離されます。
アドレッシングモードと有効アドレス
命令がオペランドの在りかを指定する方法がアドレッシングモードです。CPU は指定から**有効アドレス(EA: effective address)**を計算し、それを仮想アドレスとして変換・アクセスに渡します。代表的なモードを示します。
| モード | オペランド指定 | 有効アドレス | 用途例 |
|---|---|---|---|
| 即値 | 命令内の定数 | (番地でなく値) | 定数の加算 |
| レジスタ | レジスタ番号 | (番地でなくレジスタ) | レジスタ間演算 |
| 直接 | 命令内の絶対番地 | その番地 | グローバル変数 |
| レジスタ間接 | レジスタ=番地 | (R) | ポインタ参照 |
| ベース+変位 | R+定数 | (R) + disp | 構造体メンバ |
| インデックス+スケール | B+I×s+disp | (B) + (I)×s + disp | 配列要素 |
| PC相対 | PC+変位 | (PC) + disp | 位置独立コード |
即値とレジスタは番地計算を伴わず、オペランドが命令やレジスタの中にそのまま在ります。直接は絶対番地を命令に埋め込みます。実務で多用されるのはベース+変位で、ベースレジスタに構造体の先頭ポインタ、変位にメンバのオフセットを置けば、構造体->メンバ が一発で表せます。
配列アクセスに効くのがインデックス+スケールです。x86-64 の最も一般的な形は次の計算をハードウェアの**アドレス生成ユニット(AGU)**が1サイクルで行います。
有効アドレス = ベース + インデックス × スケール + 変位
EA = (B) + (I) * s + disp ; s ∈ {1, 2, 4, 8}
たとえば int 配列[i](要素 4 バイト)なら、ベースに配列先頭、インデックスに i、スケールに 4 を置けば arr + i*4 が一命令で求まります。スケールが要素サイズに対応するため、添字をバイト数へ変換する乗算が不要になります。PC 相対はプログラムカウンタを基準にした変位で、コードをどの番地に置いても正しく動く位置独立コード(PIC)や、近傍の定数・分岐先の参照に使われます。RISC では命令長を一定に保つため即値の幅が限られ、大きな定数は上位/下位に分けて2命令で合成します。
CISC(x86)は1命令に複雑な EA 計算(ベース+インデックス×スケール+変位)を許し、命令密度を稼ぎます。RISC(Arm/RISC-V)はロードストア型で、メモリ参照を load/store に限定し、モードもベース+変位中心に絞ってデコードを単純化します。どこまでをハードのアドレス計算に任せるかは、命令セットアーキテクチャの設計思想そのものです。
計算から実アクセスまでの流れ
一つのメモリ参照命令が実行される道筋を追うと、3つの空間がどう連なるかが見えます。
- 有効アドレス計算: AGU がベース+インデックス×スケール+変位を加算し、仮想アドレスを得る。
- アドレス変換: その仮想アドレスを TLB/ページテーブルで物理アドレスへ変換し、同時に特権・読み書き権限を検査する。
- 空間の振り分け: 物理アドレスがメモリマップ上どの区画かで、DRAM・MMIO・ROM のどれへ向かうかが決まる。MMIO ならキャッシュを迂回してデバイスへ届く。
この一貫した流れがあるからこそ、プログラマは「ポインタを足し引きする」だけで、その先が主記憶でもデバイスレジスタでも同じ構文で扱えます。x86 の PMIO だけは2の変換を経ず、専用命令で I/O 空間へ直接向かう例外的な経路をたどります。
「アドレス空間サイズ=2のアドレス幅乗(番地数)であって搭載メモリ量ではない」「MMIO は通常のload/storeで物理空間に同居、PMIO はIN/OUT命令で独立空間」「仮想空間は上位カーネル/下位ユーザーに分割し特権ビットで保護」「有効アドレス=ベース+インデックス×スケール+変位」は頻出です。MMIO 領域は uncacheable にする理由(副作用と最新値の保証)も問われます。
まとめ
- アドレス空間は番地の集合であり、物理空間には DRAM だけでなく MMIO・ROM がメモリマップで同居する。x86 はこれと別に I/O ポート空間を持つが、現代は MMIO 主流。
- 仮想アドレス空間はプロセスごとに独立し、上位をカーネル・下位をユーザーに固定写像して特権ビットで保護する。CPU が扱う番地はすべて仮想で、MMU が物理へ変換する。
- アドレッシングモードは即値・レジスタ・ベース+変位・インデックス×スケールなどがあり、有効アドレスは「ベース+インデックス×スケール+変位」で計算してから変換・アクセスへ渡される。
メモリへ届いた後の挙動はメモリコントローラのスケジューリングが、デバイスへの大量転送がアドレス空間とどう関わるかはDMAとIOMMUの原理が掘り下げます。
CPU/メモリ/ディスク Article
アドレス空間とアドレッシングモード ─ 物理/仮想/I/Oマップを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
アドレス空間
比較で見る軸
難易度: advanced / カテゴリ: CPU/メモリ/ディスク / タグ数: 5
導入後に効く点
仮想アドレス空間はプロセスごとに独立で、ページテーブルとMMU/TLBが物理へ変換する。上位をカーネル、下位をユーザーに分けて固定写像し、特権ビットで保護する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- CPU/メモリ/ディスク
- タグ数
- 5
判断チェックリスト
- 自社の用途が「アドレス空間 / 仮想メモリ」に近いか確認する。
- 強みである「アドレス空間は番地の集合で、物理アドレス空間にはDRAMだけでなくMMIOやROMがメモリマップで割り付けられる。x86はI/O命令で別系統のポート空間も持つが、現代のデバイスは大半がMMIO側に寄る。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。