コードとデータの分離(実行モデルとフォン・ノイマン)
なぜプログラムは「ただのデータ」として配れるのか。フォン・ノイマン型の実行モデルを原理から押さえると、JITも自己書き換えもセキュリティ対策も一本の線で理解できる。
- 1.フォン・ノイマン型では命令とデータが同じメモリ・同じバスに同居し、CPUはプログラムカウンタが指す番地から命令を順に読んで実行します。
- 2.実行はフェッチ・デコード・実行の繰り返しで、分岐やジャンプはプログラムカウンタを書き換えることに帰着します。
- 3.命令もデータとして書けるため自己書き換えやJITが可能になる一方、命令とデータのバスを分けたハーバード型は両者を同時アクセスできる利点を持ちます。
「コードはデータである」という出発点
プログラムをファイルとして配布でき、コンパイラがプログラムを生成し、デバッガがプログラムの中身をバイト列として覗ける——これらすべてが成り立つのは、命令もまた数値データとしてメモリに格納されているからです。この設計を フォン・ノイマン型(プログラム内蔵方式) と呼びます。命令列をあらかじめメモリに置き、CPU がそれを1つずつ取り出して実行する、という発想です。
要点は2つあります。1つはプログラム内蔵——命令を配線ではなくメモリ上のデータとして持つこと。もう1つは命令とデータの同居——命令もデータも同じアドレス空間・同じメモリ・同じバスを共有することです。後者ゆえに、命令はバイト列として読み書きでき、ある領域を「コード」と見るか「データ」と見るかは、CPU がそこをどう扱うか次第になります。番地で値を指し示すという見方の基礎はポインタと参照も参照してください。
フェッチ-デコード-実行の繰り返し
CPU の動作は、見かけ上どれほど複雑でも、次の単純なサイクルの反復に還元できます。
loop:
IR = MEM[PC] # フェッチ: PCが指す番地から命令を読む
PC = PC + 命令長 # PCを次の命令へ進める(分岐前のデフォルト)
op = decode(IR) # デコード: 命令を解釈しオペランドを取り出す
execute(op) # 実行: 演算・メモリ操作・PC書き換えなど
goto loop
ここで主役となるのが プログラムカウンタ(PC) です。PC は「次に実行する命令の番地」を保持するレジスタで、フェッチのたびに命令長ぶん自動的に進みます。つまり何もしなければプログラムは番地の昇順に直線実行される——これが制御フローの土台です。
- IR(命令レジスタ) はフェッチした命令そのものを一時保持します。
- デコード は命令のビット列から「何をする命令か」「どのレジスタ/番地を使うか」を割り出す段階です。
- 実行 は ALU での計算、メモリの読み書き、そして場合によっては PC の書き換えを行います。
if 文・ループ・関数呼び出し・例外——制御フローを変えるあらゆる仕組みは、最終的に「PC に別の番地を代入する」操作に帰着します。条件分岐は「条件が真なら PC をジャンプ先に、偽ならそのまま」という分岐命令1つで実現されます。高水準の制御構文が機械語ではすべて PC 操作に落ちる、という対応を押さえると見通しが良くなります。
命令とデータが同居することの帰結
命令とデータを同じメモリに置く設計は、強力な能力と固有の制約を同時にもたらします。
能力の側——命令はデータなので、プログラムが実行時に命令列を生成してそのまま実行できます。これが JIT コンパイル の原理的な土台です。ソースやバイトコードを実行中に機械語へ翻訳し、生成したコードへ PC を飛ばす。フォン・ノイマン型でなければそもそも成立しません(コンパイルとインタプリタ)。同じ原理の極端な例が次節の自己書き換えコードです。
制約の側——命令とデータが1本のバスを共有するため、命令フェッチとデータアクセスを同時に行えません。1サイクルで「命令を読む」か「データを読む」かのどちらか一方しか通せない。この構造的ボトルネックは フォン・ノイマン・ボトルネック と呼ばれ、CPU とメモリの速度差が広がった現代では深刻です。
現代の CPU は、L1 キャッシュを命令用(I-cache)とデータ用(D-cache)に物理的に分ける構成を採ります。これは外から見れば単一メモリのフォン・ノイマン型でありながら、キャッシュ階層の内部だけハーバード型に近づける「変形ハーバード(modified Harvard)」です。命令とデータのアクセス競合を緩める実務的な解であり、なぜキャッシュ設計が性能を左右するかはメモリレイアウトとデータ局所性で扱っています。
自己書き換えコード
命令がデータなら、実行中のプログラムが自分自身の命令を書き換えることも原理上できます。これが 自己書き換えコード(self-modifying code) です。かつては実行時に最適な命令を埋め込む高速化手段として使われ、現代でも JIT は本質的に「コードを書いてから実行する」という意味で自己生成コードの一種です。
しかし利点と裏腹に、扱いは厄介です。
- キャッシュ整合性 — 書き換えはデータとして D-cache に書かれますが、CPU は古い命令を I-cache やパイプラインの先読み段に保持し続けます。そのため
icache flushや命令同期バリアで明示的に整合を取らないと、書き換え前の命令が実行されかねません。 - セキュリティと W^X — 命令がデータとして書ける性質は、攻撃者がデータを命令にすり替えるコードインジェクションの温床でもあります。対策として、メモリ領域を「書き込み可能」か「実行可能」のどちらか一方に限る W^X(Write XOR Execute) が広く採られます。JIT はこの制約下で、生成中は書き込み可能・実行直前に実行可能へと保護属性を切り替える運用を取ります。
実行時にコードを組み立てる発想全般はメタプログラミングとマクロとも地続きですが、あちらが主にコンパイル時の生成を扱うのに対し、自己書き換えは実行時の命令メモリ書き換えである点が決定的に異なります。
フォン・ノイマン型とハーバード型
命令とデータを「同居させる」か「分離する」かが、2つのアーキテクチャを分けます。
| 観点 | フォン・ノイマン型 | ハーバード型 |
|---|---|---|
| 命令とデータの格納 | 同一メモリに同居 | 別々のメモリに分離 |
| バス | 命令・データで共有(1本) | 命令用・データ用で独立 |
| 同時アクセス | 命令フェッチとデータ参照は排他 | 両方を同時に実行可能 |
| 自己書き換え/JIT | 原理的に可能 | 原理的に困難(命令メモリは書きにくい) |
| 代表例 | 汎用CPU(x86 / ARM の外部モデル) | 多くのDSP・組込みMCU(AVRなど) |
ハーバード型は命令バスとデータバスを分けることで、命令フェッチとデータアクセスを同時に行える——フォン・ノイマン・ボトルネックを構造的に回避します。信号処理のように決まったループを高速に回す DSP で好まれるのはこのためです。反面、命令メモリは書き換えを前提としないため、プログラムを実行時に生成・自己書き換えする柔軟性は乏しくなります。
紛らわしいのは、ここでいう命令/データの分離がハードウェアの分離である点です。同じ「コードとデータの分離」でも、OS が W^X でページ単位に実行可否を分けるのは保護属性による論理的な分離であり、メモリやバスを物理的に分けるわけではありません。フォン・ノイマン型の上に W^X を載せる、というのが現代の汎用機の実像です。両者を混同しないでください。
汎用 CPU の多くは、外から見ればフォン・ノイマン型(単一アドレス空間)でありながら、内部キャッシュは命令・データを分ける変形ハーバード型、という二層構造を取ります。柔軟性(自己生成コードを許す単一メモリ)と性能(競合を避ける分離キャッシュ)を両取りする折衷です。なお、命令を取り出して解釈し PC を進めるこのサイクルは、バイトコードを回す仮想マシンのインタプリタループとも相似形です(仮想マシンとバイトコード実行)。
まとめ
フォン・ノイマン型実行モデルの核心は、命令をデータとしてメモリに内蔵し、プログラムカウンタが指す番地から1命令ずつフェッチ・デコード・実行する点にあります。分岐や呼び出しはすべて PC の書き換えに還元され、命令とデータが同居するからこそ JIT や自己書き換えという「コードを生成して走らせる」芸当が成立します。その代償が、命令とデータが1本のバスを奪い合うフォン・ノイマン・ボトルネックであり、命令/データを分離するハーバード型はこれを構造的に避ける代わりに柔軟性を手放します。現実の CPU は、外はフォン・ノイマン型・内は分離キャッシュという折衷で両者の良いとこ取りを図っています。「コードとデータは本来同じバイト列であり、それをどう扱うかが層ごとに違う」——この視点が、実行モデルとセキュリティと最適化を一本の線でつなぎます。
プログラミング Article
コードとデータの分離(実行モデルとフォン・ノイマン)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
フォン・ノイマン
比較で見る軸
難易度: advanced / カテゴリ: プログラミング / タグ数: 5
導入後に効く点
実行はフェッチ・デコード・実行の繰り返しで、分岐やジャンプはプログラムカウンタを書き換えることに帰着します。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- プログラミング
- タグ数
- 5
判断チェックリスト
- 自社の用途が「フォン・ノイマン / 実行モデル」に近いか確認する。
- 強みである「フォン・ノイマン型では命令とデータが同じメモリ・同じバスに同居し、CPUはプログラムカウンタが指す番地から命令を順に読んで実行します。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。