機械語とは、CPU が命令を実行する仕組み

回 3-5 道 3 コンピュータの中

機械語は、CPU が直接読んで実行する命令を、数の並び (バイト列) で書いたものです。CPU は、プログラムカウンタが指す番地から命令を取り出し、何の命令かを解読し、実行して次の番地へ進む、という手順を繰り返します。c = a + b の 1 行は、この頁の模型では LOAD、LOAD、ADD、STORE の 4 命令になり、本物の CPU でも数個の命令に分かれます。

1 行のコードと、CPU が実際に進める命令の数を切り分けられるようになります。

1 行の足し算は、CPU にとって何個の仕事か

c = a + b の 1 行は、CPU (コンピュータの計算をする部品) にとって 1 個の仕事ではなく、読み込み、足し算、書き出しの数個の命令です。

CPU は、変数の名前も = の記号も読めません。読めるのは、番地 (メモリの中の場所の番号) に並んだ数だけです。その数の並びが機械語 (CPU が直接読んで実行する命令の列) です。この回の終わりには、1 行が命令の何個になるかを数え、1 命令ずつ追えるようになります。

CPU はどうやって命令を実行するのか

CPU は、メモリから命令を 1 つ取り出し、何の命令かを解読し、実行し、次の番地へ進む、という同じ手順を繰り返します。

機械語は数の並び

機械語は、命令を数で書いたものです。1 つの命令は「何をするか」と「何に対してするか」の組でできています。前の部分をオペコード (命令の種類を表す数)、後ろの部分をオペランド (命令の相手、たとえば番地を表す数) と呼びます。

この回では、説明のために小さな機械の模型を使います。命令は 1 つ 2 バイトで、1 バイト目がオペコード、2 バイト目がオペランドです。オペコードの表は次の通りです。この表は模型のために決めた物で、本物の CPU の番号ではありません。

オペコード (16 進)名前すること
10LOAD A番地の中身をレジスタ A に読み込む
11LOAD B番地の中身をレジスタ B に読み込む
20ADD A,BA と B を足して A に入れる
30STORE AA の中身を番地に書き出す
01HALT止まる

レジスタは、CPU の中にある小さな置き場で、計算はここに入れた値どうしで行います。模型のメモリは 16 バイトで、番地 10 に a、11 に b、12 に c を置きます。c = a + b を命令に直し、さらに数の並びに直します。

動かす c = a + b を機械語にする

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

10 0a LOAD A 10
11 0b LOAD B 11
20 00 ADD A,B 0
30 0c STORE A 12
01 00 HALT 0
10 0a 11 0b 20 00 30 0c 01 00
10 バイト

1 行が 1 命令で、左の 2 つの 16 進数がその命令の機械語です。足し算そのものは LOAD A、LOAD B、ADD A,B、STORE A の 4 命令で、最後に止まるための HALT を足しています。下から 2 行目の 10 0a 11 0b … が、模型にとってのプログラムの正体で、名前も = も残っていません。

取り出す、解読する、実行する、次へ進む

CPU は、プログラムカウンタ (次に読む命令の番地を覚えている置き場、PC と略す) を使って、命令を 1 つずつ進めます。手順は次の 4 つで、これを命令の数だけ繰り返します。

段すること模型で言うと
取り出すPC が指す番地から命令を読むmem[pc] と mem[pc + 1] を読む
解読する何の命令かを見分けるオペコードが 10、11、20、30、01 のどれかを見る
実行する命令のとおりに値を動かすレジスタやメモリを書き換える
次へ進むPC を次の命令へ進めるPC に 2 を足す

下の図を 1 段ずつ進めると、PC が 100 番地から順に命令を指し、A と B の中身が変わります。図の LOAD は番地を読まずに数をそのまま入れるので、模型とは番地の付け方が違いますが、4 つの段の繰り返しは同じです。

CPU は同じ4手を繰り返す

命令もデータも、同じメモリに数として並んでいます。メモリは作業のあいだだけ使う置き場で、プログラムを動かすときに、保存先から命令とデータが読み込まれます。

メモリは作業中だけ広げる机で、保存する場所は引き出しの SSD のほうだという図

アセンブリ言語は機械語に名前を付けた物

アセンブリ言語は、機械語の命令 1 つ 1 つに、人が読める短い名前を付けて書く言葉です。上の模型の LOAD A や ADD A,B がそれに当たります。アセンブリ言語を機械語の数に直すことをアセンブル、直す道具をアセンブラと呼びます。

プログラムの言葉は、CPU の命令に近いほど低水準、人の考え方に近いほど高水準と呼ばれます。

言葉例1 行が表す物
機械語10 0aCPU の命令 1 つ
アセンブリ言語LOAD A 10CPU の命令 1 つ (名前で書く)
高水準言語c = a + b命令いくつか分の仕事

C 言語のような高水準言語の 1 行を、何個の命令に分けるかを決めるのは、コンパイラ (プログラム全体を前もって機械語に訳す道具) です。

gcc が hello.c を全文訳して、何度でも動かせる hello を作る図

本物の CPU では何個の命令になるか

本物の CPU でも、1 行の足し算は数個の命令に分かれ、何個になるかは CPU の種類で変わります。次の C 言語のファイルを、手元の Mac (uname -m が arm64 と答える機械) でコンパイルしました。

add.c

int a = 5, b = 3, c = 0;

void f(void) {
    c = a + b;
}

道具は Apple clang 17.0.0 (clang-1700.6.4.2) と同じ版の objdump です。cc -O1 -S add.c はアセンブリ言語を出します。関数の中身の行だけを抜き、字下げを空白にしました。

cc -O1 -S の結果 (arm64)

adrp x8, _a@PAGE
ldr  w8, [x8, _a@PAGEOFF]
adrp x9, _b@PAGE
ldr  w9, [x9, _b@PAGEOFF]
add  w8, w9, w8
adrp x9, _c@PAGE
str  w8, [x9, _c@PAGEOFF]
ret

この回では、関数から戻る ret は数えず、c = a + b のための命令だけを数えます。arm64 では 7 命令です。adrp (変数のある場所の大まかな番地を作る命令) が 3 回、ldr (読み込み) が 2 回、add (足し算) が 1 回、str (書き出し) が 1 回です。模型の LOAD、LOAD、ADD、STORE に、番地を作る 3 命令が加わった形です。

同じファイルを cc -O1 -c add.c で機械語の入った add.o にし、objdump -d add.o で中を見ました。32 字に収めるため、空白を詰め、行の後ろの注釈を外しています。

objdump -d の結果 (arm64)

 0: 90000008 adrp x8, 0x0
 4: b9400108 ldr w8, [x8]
 8: 90000009 adrp x9, 0x0
 c: b9400129 ldr w9, [x9]
10: 0b080128 add w8, w9, w8
14: 90000009 adrp x9, 0x0
18: b9000128 str w8, [x9]
1c: d65f03c0 ret

左から、番地 (16 進)、機械語、それをアセンブリ言語に戻した物で、1 対 1 で並んでいます。番地が 4 ずつ進むので、どの命令も 4 バイトです。0x0 のように番地が 0 のままなのは、add.o が組み立て途中の部品で、変数の本当の番地は最後にまとめるときに埋まるからです。

真ん中の 90000008 は、4 バイトを 1 つの数として見た値です。メモリに並ぶ順は、objdump -s でバイトのまま見ると分かります。

objdump -s の結果 (1 行を 2 行に折った)

0000 08000090 080140b9
     09000090 290140b9
0010 2801080b 09000090
     280100b9 c0035fd6

90000008 は、メモリでは 08、00、00、90 の順に並んでいます。下の桁のバイトから先に置く並べ方で、エンディアンの回で扱う話です。

同じ add.c を、Intel の CPU などで使われる x86_64 向けに cc -O1 -fomit-frame-pointer -target x86_64-apple-macos で作ると、命令が変わります。

cc -S の結果 (x86_64)

movl _b(%rip), %eax
addl _a(%rip), %eax
movl %eax, _c(%rip)
retq

objdump -d の結果 (x86_64)

 0: 8b 05 00 00 00 00 movl
 6: 03 05 00 00 00 00 addl
 c: 89 05 00 00 00 00 movl
12: c3                retq

x86_64 では、retq を除いて 3 命令です。addl がメモリの a を読みながら足すので、読み込みが 1 つ少なく済みます。命令の長さも 6 バイトと 1 バイトでそろっていません。同じ 1 行でも、機械語の数の並びは CPU の種類ごとに全く違います。

書いた所c = a + b の命令の数1 命令の長さ
この頁の模型42 バイト
arm64 (上の cc)74 バイト
x86_64 (上の cc)36 バイト (retq は 1)

動かして確かめる

模型の CPU を Python で書き、上で作った機械語を 1 命令ずつ進めます。メモリは bytearray (0〜255 の数を並べた入れ物) で 16 バイト持ち、レジスタ A と B は 8 ビットです。

動かす 1 命令ずつ進める

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

0 10 0a LOAD A 5 0
2 11 0b LOAD B 5 3
4 20 00 ADD A,B 8 3
6 30 0c STORE A 8 3
8 01 00 HALT
c = 8

左から PC の値、取り出した 2 バイト、解読した名前、実行したあとの A と B です。PC は 0、2、4、6、8 と 2 ずつ進み、ADD で A が 8 になり、STORE で番地 12 の c が 8 になりました。HALT を除いて 4 回の繰り返しです。

次に、+ が増えると命令がいくつになるかを、1 行を模型の命令に直す道具で数えます。

動かす 1 行を命令に直して数える

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

c = a + b → 4
   LOAD A a
   LOAD B b
   ADD A,B
   STORE A c
d = a + b + c → 6
   LOAD A a
   LOAD B b
   ADD A,B
   LOAD B c
   ADD A,B
   STORE A d
e = a → 2
   LOAD A a
   STORE A e

右辺の名前が 1 つ増えるごとに、LOAD B と ADD A,B の 2 命令が増えます。最初の LOAD A と最後の STORE A は 1 回ずつなので、右辺に名前が n 個あれば 2 × n 命令です。名前が 2 個の c = a + b は 4、3 個の d = a + b + c は 6、1 個の e = a は 2 です。

前提が崩れると何が起きるか

CPU は、読んだ数が正しい命令かを疑いません。解読できれば実行し、できなければ止まり、器の幅を超えた値は上の桁を捨てます。

壊れた命令と、ずれた PC

模型を関数にまとめ、ADD を ff に書き換える、HALT を 00 で消す、PC を 1 から始める、の 3 つを試します。

動かす 壊れた命令とずれた PC

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

ff は命令に無い c = 0
00 は命令に無い c = 8
0a は命令に無い c = 0

1 つ目は、番地 4 の ff を解読できずに止まり、c は 0 のままです。図で「103番地に壊れた命令を置く」を入れても、ff ff ff ff を解読できずに止まります。

2 つ目は、c に 8 を書いたあと番地 8 へ進み、00 を命令として読もうとして止まりました。CPU はプログラムの終わりを知らず、止まれの命令が無ければ、その先の数も命令として読みます。

3 つ目は、PC が 1 つずれただけで、LOAD A の相手の番地だった 0a をオペコードとして読んでいます。同じバイト列でも、どこから読むかで意味が変わります。

8 ビットのレジスタからあふれる

模型のレジスタは 8 ビットなので、0〜255 しか入りません。足した結果が 256 以上になると、9 桁目がこぼれて捨てられます。模型では & 0xFF (下の 8 ビットだけを残す計算) でそれを表しています。

動かす 8 ビットのレジスタで足す

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

5 + 3 → 8 000001000
200 + 50 → 250 011111010
200 + 100 → 44 100101100
255 + 1 → 0 100000000

右の 9 桁は、足した結果を 9 ビットで書いた物です。200 + 100 の 300 は 9 桁目が捨てられ、残りの 00101100 で 44 になりました。命令は 1 つも壊れていないのに、値が器に入るという前提が崩れただけで答えが変わります。

崩れる前提をまとめると、次の通りです。

  • メモリの中身は正しい命令だ、という前提
  • PC は命令の頭を指し、止まる命令がある、という前提
  • 計算の結果はレジスタに入る、という前提。8 ビットなら 256 以上はこぼれる

仕事でどう確かめるか

仕事では、気になる 1 行を実際にコンパイルし、出てきた命令を数えて確かめます。

1 つ目は、cc -O1 -S でアセンブリ言語を出すことです。遅い 1 行が何個の命令で、メモリへの読み書きが何回あるかが分かります。最適化の強さを変えると命令の数も変わるので、本番と同じ設定で出します。

2 つ目は、objdump -d で機械語とアセンブリ言語を並べることです。落ちた記録に番地が出ていれば、その番地の命令を探せます。

3 つ目は、どの CPU 向けかを確かめることです。手元で file add.o add86.o を打つと、Mach-O 64-bit object arm64 と Mach-O 64-bit object x86_64 と出ました。

Python の dis で見えるのは CPU の命令ではありません。手元の Python 3.14 で dis.dis("c = a + b") を動かすと、次の物が出ました (空白を詰めた)。

Python 3.14 の dis の結果

0 RESUME 0
1 LOAD_NAME 0 (a)
  LOAD_NAME 1 (b)
  BINARY_OP 0 (+)
  STORE_NAME 2 (c)
  LOAD_CONST 0 (None)
  RETURN_VALUE

LOAD、LOAD、足し算、STORE と並ぶ形は模型と同じですが、これは CPython (標準の Python) の解釈役が読むバイトコードです。公式の文書は、バイトコードは実装の詳細で、版の間で変わりうると書いています。そのため頁の中では動かしていません。

関わる実例

模型の CPU は命令を 1 つずつ順番に実行しますが、本物の CPU はそうではないことが、2018 年に Spectre と Meltdown という弱点として公表されました。

Spectre と Meltdown (2018 年 1 月公表)

Google の Project Zero は、この問題を 2017 年 6 月 1 日に Intel、AMD、ARM に報告したと書いています。Spectre の論文 (2018 年 1 月 3 日に arXiv に掲載) の要旨によると、今の CPU は速さのために分岐予測と投機的実行 (行き先を予想して先の命令を実行しておくこと) を使い、外れたら実行した分を捨てます。ところが捨てた実行が測れる跡を残し、Spectre はそれで秘密を漏らさせます。

Meltdown は、アプリと OS (基本ソフト) の間の隔たりを破るものだと公式の頁は書き、順番を入れ替えて実行する Intel の CPU は、1995 年以降のほぼすべて (2013 年より前の Itanium と Atom を除く) が影響を受けうるとしています。番号は Spectre が CVE-2017-5753 と CVE-2017-5715、Meltdown が CVE-2017-5754 です。

崩れた前提は「CPU は命令を順番に実行し、捨てた実行は何も残さない」です。先回りして読んだデータが CPU のキャッシュに残っていたことが、跡になりました。

よくある問い

機械語の良い所と困る所は何ですか

良い所は、CPU が訳さずにそのまま読めることです。困る所は、数の並びなので人には読みにくく、この頁の arm64 と x86_64 のように CPU の種類が違うとそのままでは動かないことです。

機械語の命令の一覧はありますか

CPU の種類ごとに、使える命令とその数の決まりがあり、これを命令セットと呼びます。arm64 と x86_64 は別の命令セットなので、同じ c = a + b が、この頁では 7 命令と 3 命令の別の数の並びになりました。この頁の objdump の出力に出た adrp、ldr、add、str、ret は、arm64 の命令セットのごく一部です。

Python は機械語に訳されるのですか

標準の Python (CPython) は、プログラムを CPU の機械語ではなく、バイトコードという CPython 用の命令に直し、それを解釈役が 1 つずつ実行します。C 言語は、コンパイラが前もって全体を CPU の機械語に訳します。

機械語を読める人がいるのはなぜですか

機械語はアセンブリ言語と 1 対 1 に対応するので、命令の表があれば読めます。仕事で読むのは、落ちた原因や遅い処理、セキュリティの弱点を調べるときで、objdump のような道具が名前に戻してくれます。

確認クイズ

答えだと思う選択肢を押すと、その場で正誤と理由が開きます。4 問すべてに正しく答えると、この回は修了で、石が緑に変わります (この端末のブラウザに残ります)。間違えた問いは、あとから解き直せます。

1 問目

このコードを動かすと何が出ますか。A は 8 ビットのレジスタを真似ています。

8 ビットで足す

A, B = 250, 10
A = (A + B) & 0xFF
print(A)

2 問目

この頁の模型で、d = a + b + c は何個の命令になりますか。止まる HALT は数えません。

3 問目

CPU が、PC の指す番地から命令を読んだところ、その数が命令の表にありませんでした。どの段で何が起きますか。

4 問目

アセンブリ言語と機械語の関係として正しいものはどれですか。

説明課題

次のことを、自分の言葉で 3 行にまとめてください。書いたら、下の模範解答を開いて比べてください。

  • c = a + b の 1 行が、CPU の命令の何個になるかと、その命令の名前
  • CPU がその命令を 1 つずつ進める手順

模範解答

自分の説明を書いてから開いて、比べてください。

この回の先の一歩

遅い処理の原因を探すとき、別の CPU 向けにプログラムを作り直すとき、落ちたプログラムの記録に番地とバイト列が並んでいるとき、1 行が何個の命令になるかを数える目が手がかりになります。

一次資料

どれも 2026年10月27日に開いて、題と中身を確かめました。

次へ

道 3 コンピュータの中の回

  1. 3-1OS の役目とシステムコールOS が間に立つ理由頁で読む
  2. 3-2プロセスとスレッドスケジューラ、プロセス間通信頁で読む
  3. 3-3メモリの階層とキャッシュ局所性、当たりと外れ、L1 から主記憶まで頁で読む
  4. 3-4取り合いと排他レースコンディション、Mutex頁で読む
  5. 3-5CPU が命令を実行する命令、レジスタ、クロックこの回

道 3 の頁へ戻るほかの道を見る