作って分かる！
x86_64 機械語⼊⾨— OS レスで CPU と直接話す! —⼤神祐真 著

技術書典 7（2019 年秋）新刊

2019 年 9 ⽉ 22 ⽇

ver 1.0

■免責
本書は情報の提供のみを⽬的としています。
本書の内容を実⾏・適⽤・運⽤したことで何が起きようとも、それは実⾏・適⽤・運⽤した⼈⾃⾝の責任であり、著者や関係者はいかなる責任も負いません。
■商標
本書に登場するシステム名や製品名は、関係各社の商標または登録商標です。
また本書では、™、®、© などのマークは省略しています。

はじめに本書をお⼿に取っていただきありがとうございます。
本書では、⼀つ⼀つ構⽂を理解していくことで機械語を書いてみます。機械語は、ソフトウェアエンジニアの領域の⾔語としては低レイヤー中の低レイヤーで、これより下は無いってくらいですが、バイナリ列とはいえちゃんとパターンがあり、構⽂として捉えることができ、⼈の⼿で書くこともできます。

本書の構成
本書は以下の構成です。

• 第 1 章 環境構築とはじめてのプログラム◦ ループだけの簡単なプログラムを通して、環境構築をしながら、本書で機械語を理解していく流れを紹介します◦ 開発はテキストベースで⾏えるように簡単なスクリプトを使います◦ 実⾏環境は QEMU 上です。OS レスで直接動作させます。
◦ また、機械語を理解するための補助器具として、シェル上で⼿軽にアセンブ
ル・逆アセンブルするツールも⽤意しておきます∗ 以降の章ではこのツールも活⽤しながら、機械語表現が分からないときは適宜アセンブラからアセンブルしてみます*1• 第 2 章 基本的な演算◦ 基本的な演算命令 (四則演算、論理演算等) の機械語における構⽂を紹介します• 第 3 章 シリアルドライバを作る◦ シリアルドライバを作り、QEMU のシリアル出⼒画⾯に⽂字を表⽰してみます• 第 4 章 フレームバッファで画⾯描画◦ フレームバッファと呼ばれる領域へピクセル値を書き込むことで画⾯描画します• 第 5 章 キーボードドライバを作る◦ キーボードドライバを作り、キーボードからの⼊⼒を取得します• 付録 A 登場した機械語命令と構⽂の⼀覧*1 「それなら最初からアセンブラで書けば良いじゃん」は禁句です (笑)。まあ、この本の⽬的は機械語を勉強して書いてみることであって、何らかの別の⾔語から変換してきては意味がないので。

–i–

はじめに

◦ 登場した構⽂と命令を⼀覧にまとめています

開発環境・動作確認環境
筆者が開発や動作確認を⾏っている環境を以下に記載します。

• OS: Debian GNU/Linux 9.9 (stretch)• binutils: 2.28 (Debian stretch のバージョン)• QEMU: 2.8.1 (Debian stretch のバージョン)

本書の PDF 版/HTML 版やソースコードの公開場所についてこれまでの当サークルの同⼈誌同様、本書も PDF 版/HTML 版は以下のページで無料で公開しています。

• http://yuma.ohgami.jp
サンプルコードは以下の GitHub リポジトリで公開しています。

• https://github.com/cupnes/introduction_to_x86_64_machine_
language_samples

サンプルコードのリポジトリ内はディレクトリで分かれています。各ディレクトリが以降の各節あるいは各項で作るものです。節あるいは項の冒頭で「この節/項のサンプ
ルディレクトリは XX です。」と紹介しますので適宜参照してみてください。

– ii –

⽬次
はじめに
本書の構成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
開発環境・動作確認環境 . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
本書の PDF 版/HTML 版やソースコードの公開場所について . . . . . . . . . .
第1章

1.1
1.2

環境構築とはじめてのプログラム
本書で機械語を理解していく流れ . . . . . . . . . . . . . . . . . . . .
機械語構⽂の理解を補助するツール (objdump, as_dump, das_dump)
binutils をインストール . . . . . . . . . . . . . . . . . . . . . . . . . .
簡単なプログラムを逆アセンブルしてみる . . . . . . . . . . . . . . . . . .
⼩さなアセンブラコードを通して jmp 命令を理解 . . . . . . . . . . . . . .
その場で逆アセンブル. . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . .

構⽂まとめ (命令 1「jmp」)

1.3

テキストベースでバイナリを書く (t2b) . . . . . . . . . . . . . . . . .
16 進数が書かれたテキストをバイナリへ変換するコマンド (t2b) . . . . . . .
テキストで機械語コードを書いてみる . . . . . . . . . . . . . . . . . . . .
バイナリへ変換

1.4

1.5
1.6
第2章

2.1

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
VM 上で OS レスで実⾏する (QEMU, OVMF, poiboot) . . . . . . . .
バイナリフォーマットについて . . . . . . . . . . . . . . . . . . . . . . .
QEMU と OVMF をインストール . . . . . . . . . . . . . . . . . . . . .
poiboot を取得 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
実⾏できるようにファイルを配置 . . . . . . . . . . . . . . . . . . . . . .
QEMU で動作確認 . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
QEMU モニターで実⾏状況を確認する . . . . . . . . . . . . . . . . . . .
何もしない命令「nop」(命令 2) . . . . . . . . . . . . . . . . . . . . .
CPU を休ませる「hlt」(命令 3) . . . . . . . . . . . . . . . . . . . . .
基本的な演算データ格納命令 (mov)(命令 4) . . . . . . . . . . . . . . . . . . . . . .
汎⽤レジスタ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
即値をレジスタへ格納する機械語コードを⾒てみる

2.2. . . . . . . . . . . . .
他のレジスタへ格納する命令も⾒てみる . . . . . . . . . . . . . . . . . . .
構⽂: mov: 即値 (8bit) をレジスタ (8bit) へ格納 . . . . . . . . . . . . . .
構⽂: mov: 即値 (16bit) をレジスタ (16bit) へ格納 . . . . . . . . . . . . .
四則演算 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

– iii –

i
i
ii
ii
1
1
1
1
2
4
6
6

7
7
7
8

9
9
10
11
11
12
13

15
16
19
19
19
20
21
22
22

24

⽬次

和 (add)(命令 5) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
差 (sub)(命令 6) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
積 (imul)(命令 7). . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . .
論理演算 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
AND(命令 9) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
OR(命令 10) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
XOR(命令 11) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
NOT(命令 12) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
シフト演算 (命令 13-15) . . . . . . . . . . . . . . . . . . . . . . . . .
インクリメント/デクリメント (命令 16-17) . . . . . . . . . . . . . . .
商と剰余 (idiv)(命令 8)

2.3

2.4
2.5
第3章

3.1

シリアルドライバを作る
⽂字を送信して画⾯表⽰ . . . . . . . . . . . . . . . . . . . . . . . . .
シリアル通信を⾏うには. . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . .
動作確認 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
コントローラの状態を確認して送信する . . . . . . . . . . . . . . . . .
Line Status Register(LSR) . . . . . . . . . . . . . . . . . . . . . . . .
IO アドレス空間のレジスタ値取得「in」(命令 19) . . . . . . . . . . . . . .
フラグの待ち⽅「and」/「je」(命令 20) . . . . . . . . . . . . . . . . . .
フラグレジスタの動きを⾒てみる . . . . . . . . . . . . . . . . . . . . . .
LSR のビット 5 を待つ処理を実装 . . . . . . . . . . . . . . . . . . . . .
IO アドレス空間のレジスタへデータを書き込む「out」(命令 18)

3.2

3.3

受信も追加しエコーバックプログラムを作る . . . . . . . . . . . . . .
受信バッファにデータが来るのを待つ (命令 21「jne」/命令 22「pause」) . .
受信バッファのデータを取得する . . . . . . . . . . . . . . . . . . . . . .

mov 命令の新構⽂ (8 ビットレジスタから 8 ビットレジスタへコピー) . . . .
受信バッファから取得したデータを送信する . . . . . . . . . . . . . . . . .
動作確認 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

CR を受け取ったら LF を追加で送信するようにする (命令 23「cmp」) . . .
動作確認 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

第4章

4.1

フレームバッファで画⾯描画
フレームバッファのアドレスを取得する . . . . . . . . . . . . . . . . .
フレームバッファに関する情報の在処 . . . . . . . . . . . . . . . . . . . .

31
31
32
33
34

34
36
37
37
37
38
39

39
40
40
41
42
43

44
44
48
49
50
50
50
52

53
53

動作確認 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

53
54
55
56

画⾯の幅と⾼さを取得する . . . . . . . . . . . . . . . . . . . . . . . .

56

レジスタ間接アドレッシング. . . . . . . . . . . . . . . . . . . . . . . .

実装 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

4.2

24
26
27
30

– iv –

ディスプレースメント付きレジスタ間接アドレッシング. . . . . . . . . . .
. . . . . . . . . . . . . . . . . . .
実装 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
動作確認 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
画⾯へ 1 ピクセル描画してみる . . . . . . . . . . . . . . . . . . . . .
即値をレジスタ間接で書き込む . . . . . . . . . . . . . . . . . . . . . . .
実装 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
動作確認 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
画⾯を塗りつぶしてみる . . . . . . . . . . . . . . . . . . . . . . . . .
実装の流れ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
等しいか⼩さければジャンプ (命令 24「jbe」) . . . . . . . . . . . . . . .
実装 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
初出の構⽂について . . . . . . . . . . . . . . . . . . . . . . . . . . . .
動作確認 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ランダム⾊で塗りつぶしてみる . . . . . . . . . . . . . . . . . . . . .
rdrand 命令で乱数取得 (命令 25) . . . . . . . . . . . . . . . . . . . . . .
実装 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
動作確認 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
備考: DSP の有無による機械語の変化

4.3

4.4

4.5

第5章

キーボードドライバを作る

5.1

キー⼊⼒を受け取るまでの流れ . . . . . . . . . . . . . . . . . . . . .

5.2
5.3
5.4

ステータスレジスタ値を取得

5.5. . . . . . . . . . . . . . . . . . . . . .
キー⼊⼒値が無ければ先頭へ戻る . . . . . . . . . . . . . . . . . . . .
データレジスタからキー⼊⼒値を取得 . . . . . . . . . . . . . . . . . .

A.1

60
61
61

62
62
62
63
64
67

67
68
68
70

71
71
72
72
73

キー⼊⼒値が Break ならスキャンコードを抽出し、Make なら先頭へ

74

実装⽅針 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

74
75
75

実装 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

付録 A

60

戻る . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
⽐較結果が「⼩さい」ならジャンプ (命令 26「jb」) . . . . . . . . . . . . .

5.6
5.7

56
58
59
59

スキャンコードをレジスタ CL へコピーし、先頭へ戻る . . . . . . . .
動作確認 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
登場した機械語命令と構⽂の⼀覧
構⽂表記のキーワードについて . . . . . . . . . . . . . . . . . . . . .
rel_addr . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
reg_ofs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
reg_src_dst . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
reg_dst_src . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

–v–

76
77
79
79
79
79
79
80

⽬次

四則演算 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
add: 加算 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
sub: 減算 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
imul: 乗算 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
idiv: 除算 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

82

インクリメント/デクリメント . . . . . . . . . . . . . . . . . . . . . .
inc: インクリメント . . . . . . . . . . . . . . . . . . . . . . . . . . . .
dec: デクリメント . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

83

A.4

⽐較 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
cmp: 2 つのオペランドの差を評価 . . . . . . . . . . . . . . . . . . . . .

84

A.5

論理演算 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
and: 論理積 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
or: 論理和 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
xor: 排他的論理和 . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
not: 否定 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

84

ビットシフト . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
sal: 算術左シフト . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
sar: 算術右シフト . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
shr: 論理右シフト . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

85

読み出し/書き込み . . . . . . . . . . . . . . . . . . . . . . . . . . . .
mov: 第 1 から第 2 オペランドへコピー . . . . . . . . . . . . . . . . . . .
in: IO アドレス空間から読み出し . . . . . . . . . . . . . . . . . . . . . .
out: IO アドレス空間へ書き込み . . . . . . . . . . . . . . . . . . . . . .

86

CPU 状態制御 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
hlt: CPU をスリープ . . . . . . . . . . . . . . . . . . . . . . . . . . .
pause: ビジーループのヒント . . . . . . . . . . . . . . . . . . . . . . .
分岐 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
jmp: 無条件ジャンプ . . . . . . . . . . . . . . . . . . . . . . . . . . . .
jb: ⼩さい (Below) 場合にジャンプ . . . . . . . . . . . . . . . . . . . . .
jbe: ⼩さいか等しい (Below or Equal) 場合にジャンプ . . . . . . . . . . .
je: 等しい (Equal) 場合にジャンプ . . . . . . . . . . . . . . . . . . . . .
jne: 等しくない (Not Equal) 場合にジャンプ . . . . . . . . . . . . . . . .

88

その他 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
nop: 何もしない (No Operation) . . . . . . . . . . . . . . . . . . . . .
rdrand: 乱数取得 . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

89

A.2

A.3

A.6

A.7

A.8

A.9

A.10

おわりに

82
82
83
83
83
84
84
84
85
85
85
86
86
86
86
87
88
88
88

88
88
88
88
89
89
89
89

91

– vi –

参考情報

93
93
poiboot について . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
参考にさせてもらった情報 . . . . . . . . . . . . . . . . . . . . . . . . . . . .

– vii –

第1 章 環境構築とはじめてのプログラム

1.1 本書で機械語を理解していく流れ
本書では、C ⾔語やアセンブラをコンパイル/アセンブルした実⾏バイナリを調べる
ことで、機械語における各命令の書き⽅ (構⽂) を⼀つ⼀つ理解します。そして、理解した構⽂を使って⼩さな機械語プログラム (=実⾏バイナリ) を書いて実⾏してみます。
流れをまとめると以下の通りです。

1. 構⽂を理解する
2. 実⾏バイナリを書く
3. 実⾏する
この章では以降、上記の流れを、環境構築を⾏いながら簡単なプログラムを使って紹介します。

1.2 機械語構⽂の理解を補助するツール (objdump,
as_dump, das_dump)♣ binutils をインストール
本書では、実⾏バイナリから機械語構⽂を確認する際、「objdump」というコマンドを使って逆アセンブルを⾏います。逆アセンブル結果を機械語とアセンブリ⾔語で並べて表⽰してくれるため、機械語で書いた場合のバイナリ列 (すなわち構⽂) はどのような表現になるのかを確認するのに役⽴ちます。

objdump コマンドは「binutils」パッケージに⼊っています。まずはこのパッケージをインストールします。
$ sudo apt install binutils

以下のようにバージョンが表⽰できれば OK です。(バージョンは違っていても構い

–1–

第1章

環境構築とはじめてのプログラムません。)$ objdump -v
GNU objdump (GNU Binutils for Debian) 2.28
Copyright (C) 2017 Free Software Foundation, Inc.
This program is free software; you may redistribute it under the terms of
the GNU General Public License version 3 or (at your option) any later version››.
This program has absolutely no warranty.

♣ 簡単なプログラムを逆アセンブルしてみる
objdump を使ってみます。試しに C ⾔語で「無限ループだけのプログラム」を書いて、逆アセンブルしてみることにします。

C ⾔語のプログラムはリスト 1.1 の通りです。お好きなエディタで書いてみてくだい。
▼リスト 1.1: 011_jmp/loop.c
int main(void){
while (1);return 0;}

なお、このサンプルプログラムは、本書のサンプルプログラムをまとめているリポジ
トリ*1 内の「011_jmp」ディレクトリ内に置いています。
ここでは「loop.c」というファイル名で作成したとします。
これをコンパイルします。本書ではコンパイラには GCC を使⽤しますので、まだインストールされていなければインストールしてください。
$ sudo apt install gcc

以下のようにバージョンが表⽰できれば OK です。(本書で扱う範囲では、バージョンは違っていても構いません。)$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/6/lto-wrapper*1 サンプルリポジトリの URL は「はじめに」をご覧ください。

–2–

1.2 機械語構⽂の理解を補助するツール (objdump, as_dump, das_dump)

Target: x86_64-linux-gnu
Configured with: ../src/configure -v ・・・省略・・・
Thread model: posix
gcc version 6.3.0 20170516 (Debian 6.3.0-18+deb9u1)

それでは、コンパイルします。
「-g」を付けて、実⾏バイナリにデバッグ情報も付加しています。
$ gcc -g -o loop loop.c

これを逆アセンブルしてみます。
$ objdump -S loop | less・・・省略・・・

0000000000000660 <main>:
int main(void)
{
660:
55
661:
48 89 e5while (1);
664:
eb fe
666:
66 2e 0f 1f 84 00 00
66d:
00 00 00

push
mov

%rbp
%rsp,%rbp

jmp
nopw

664 <main+0x4>%cs:0x0(%rax,%rax,1)・・・省略・・・「-S」は「デバッグ情報があれば C のソースコードも併せて表⽰」するオプションです。
また、⾏数が多いので、スクロールできるようにパイプでページャ (less) に繋いでいます。
出⼒をスクロールしていくと、「int main(void)」があり、そこより下の⾏に main 関数の実装があります。
少し下の⾏に⾏くと「while (1);」があり、その直後の⾏が、無限ループを実現している機械語、およびアセンブラのコードです。該当の⾏のみ抜粋するとリスト 1.2 の通りです。
▼リスト 1.2: 無限ループの機械語およびアセンブラの抜粋
664:

eb fe

jmp

664 <main+0x4>

objdump の出⼒は、各⾏で機械語とアセンブラが対応しています。この場合、アセンブラが「jmp 664 <main+0x4>」で、その機械語が「eb fe」です。たった 2 バイト
で無限ループを実現できるのですね。

–3–

第1章

環境構築とはじめてのプログラム

⼀番左にコロン区切りで書かれている「664:」は実⾏バイナリ先頭からのバイト数です。「jmp 664」の「664」はこの値を⽰しており、jmp 命令⾃⾝と同じ場所にジャンプしてくるため、無限ループとなります。
そんなわけで、1 つ⽬の命令は「jmp」命令です。もう少し詳しく⾒てみます。

♣ ⼩さなアセンブラコードを通して jmp 命令を理解
それでは、たった 2 バイトの命令の中に「先頭から何バイト⽬」という絶対アドレス
が埋め込まれているのでしょうか？ このような無限ループを、例えば先頭から 0 バイ
ト⽬に配置すると機械語コードも変わるのでしょうか？
実験してみましょう。ループを jmp 命令で実現できることはわかっているので、こ
こでは 1 ⾏〜数⾏のアセンブラコードを機械語に変換してみることで、jmp 命令のアセ
ンブラの表現と機械語の対応を理解します。
アセンブリ⾔語で書かれたソースコードを機械語 (実⾏バイナリ) へ変換するにはア
センブラを使⽤します。ですが、たった 1 ⾏〜数⾏のコードで実験して⾒るためだけにわざわざコードを書いて、アセンブルして、実⾏バイナリを 16 進数でダンプする、というのは⾯倒です。本書では、それらを⾏うコマンドを、「as_dump」というシェルス
クリプトで⽤意しています。

as_dump はサンプルリポジトリの「tools」ディレクトリに⼊っています。
as_dump はアセンブルに「as コマンド (GNU アセンブラ)」を使⽤します。as はbinutils に⼊っているので、objdump を使⽤する際にインストールしていれば問題ありません。
それでは、as_dump を使って簡単なアセンブラを機械語コードに変換してみます。
jmp 命令のオペランド*2 にはラベルが使えます。そして、「今この場所」を⽰すラベ
ルには「.(ドット)」があり、「jmp .」で無限ループを実現できます。

as_dump を使って「jmp .」の 1 ⾏だけのアセンブラコードを機械語に変換してみます。
$ as_dump jmp .
/tmp/tmp.hdumfEdnIQ/a.out:

ファイル形式 elf64-x86-64

セクション .text の逆アセンブル:

0000000000000000 <.text>:
0:
eb fe

jmp

0x0

*2 命令へ与えるパラメータのこと。jmp 命令の場合、先程の「4」がオペランドに相当します。

–4–

1.2 機械語構⽂の理解を補助するツール (objdump, as_dump, das_dump)

このように、as_dump コマンドは引数にアセンブラを与えることで、1 ⾏のアセン
ブラをその場で機械語に変換し、結果を objdump による逆アセンブル結果として表⽰します。
結果に関して、jmp 命令以外には何もないアセンブラコードなので、jmp のオペラ
ンドが「0x0(先頭から 0 バイト⽬)」なのは意図通りです。ただ、今回の場合も機械語
コードは「eb fe」となりました。どうやら、機械語コードにはジャンプ先は相対アドレスで埋め込まれていそうです。それを逆アセンブラのツール (objdump) が逆アセンブルの際、よしなに絶対アドレスで表⽰してくれていたのでしょう。
それでは、「eb fe」のどこに相対アドレスが埋め込まれていたのでしょうか。今度はラベルを使って少し先へジャンプさせてみましょう。

as_dump を引数なしで実⾏すると⼊⼒を受け付ける状態になりますので、その場で2 ⾏のアセンブラコードを書きます。「Ctrl-D」で抜ければ、書いたアセンブラコード
を機械語へ変換します。
$ as_dump
jmp
loop
loop:
jmp
.
<Ctrl-Dで抜ける>/tmp/tmp.V4twRVogBv/a.out:

ファイル形式 elf64-x86-64

セクション .text の逆アセンブル:

0000000000000000 <loop-0x2>:
0:
eb 00

jmp

2 <loop>

0000000000000002 <loop>:
2:
eb fe

jmp

2 <loop>

2 つ⽬の jmp 命令の地点に「loop」というラベルを付けました。
1 ⾏⽬の jmp 命令は⾃⾝の命令の場所から⾃⾝の命令⻑である 2 バイト先にジャンプすることになります。その場合、機械語コードは「eb 00」になりました。⾃⾝の命令と同じ場所へジャンプする場合は、これまで確認した通り「eb fe」です。
どうやら、機械語コードの「eb」が「jmp」命令であることを⽰し、続く 2 バイト⽬がジャンプ先の相対アドレスを⽰しているようです。
また、2 バイト⽬に関して、「今この場所」へジャンプする際は「fe」で、「2 バイト⽬」へジャンプする際は「00」でした。「0xfe + 2 = 0x100」で、1 バイトからオーバーフローした分を捨てると「0x00」になることから、eb 命令 (jmp 命令) のオペランドは
0xfe を基準とした相対アドレスであることがわかります。

–5–

第1章

環境構築とはじめてのプログラム♣ その場で逆アセンブル
as_dump の逆に、その場で逆アセンブルする「das_dump」コマンドもサンプルディ
レクトリの tools ディレクトリに⼊れてあります。
少し追加で実験してみます。
先ほど、jmp 命令の機械語である「eb XX」命令のオペランドは、「fe」を基準として、それに⾜し合わせた数値の分がジャンプ先の相対的なアドレスになることを確認しました。
それでは、「fe」から値を引いた場合はどうなるのでしょうか？
例として、「fe」から 1 を減算した値である「fd」をオペランドとした「eb fd」を逆アセンブルしてみます。
$ das_dump eb fd/tmp/tmp.qQjOC52hIN/a.bin:

ファイル形式 binary

セクション .data の逆アセンブル:

0000000000000000 <.data>:
0:
eb fd

jmp

0xffffffffffffffff

das_dump コマンドも、使い⽅は as_dump コマンドと同様です。コマンドライン
引数に逆アセンブルしたい機械語を 1 バイトずつ半⾓スペース区切りで 16 進数で指定すると、それを objdump で逆アセンブルした結果を表⽰します。
逆アセンブルの結果、jmp のオペランドは「0xﬀﬀﬀﬀﬀﬀﬀﬀ」となりました。この値は2 の補数で「-1」を⽰します。「基準 fe から引いた値を指定するとその分だけマイナスの⽅向へジャンプする」ということだと分かります*3 。

♣ 構⽂まとめ (命令 1「jmp」)
相対アドレス指定でジャンプする jmp 命令の機械語コードとアセンブラの構⽂を表

1.1 にまとめます。
表 1.1 の「アセンブラ」の表記について、objdump では jmp のオペランドを実⾏バ
イナリ先頭からのバイト数あるいは相対アドレスで表⽰していましたが、⼀般的にアセ
ンブラを書く際はジャンプ先にラベルを置いて、jmp のオペランドにはラベルを指定します。
*3 プラス⽅向へのジャンプについて fe からの増分が 2 以上の場合、00 へラップ (1 周) するので、プラス⽅向へのジャンプの最⼤値とマイナス⽅向へのジャンプの最⼤値はどこかでぶつかります。それがどこなのかも das_dump で適当にオペランドを変えて逆アセンブルしてみることで確認できます。
興味があれば実験してみてください。

–6–1.3 テキストベースでバイナリを書く (t2b)▼表 1.1: 構⽂ 1: jmp: 相対アドレス指定でジャンプ
アセンブラ
機械語

jmp <ラベルあるいは".">eb fe+rel_addr

また「機械語」の欄の「+rel_addr」は相対的にジャンプさせたいバイト数を⾜し合わせる事を⽰しています。
以降では、新たな構⽂を⾒つける/あるいは紹介する度に、このような表にまとめます。

1.3 テキストベースでバイナリを書く (t2b)
ここまでで、jmp 命令の相対アドレス指定での構⽂を理解しました。
次は、理解した構⽂を使って、実際に動作する実⾏バイナリを作ってみます。

♣ 16 進数が書かれたテキストをバイナリへ変換するコマンド (t2b)
バイナリを書く際、⼀般的にはバイナリエディタを使うかと思うのですが、そもそも「バイナリを書く」事が⼀般的ではないためか、0 から書いていくような場合に使い勝⼿のよいバイナリエディタがなかなか無い印象です。
そこで、本書では、16 進数が羅列されたテキストファイルをバイナリへ変換するス
クリプト「t2b」を⽤意して、機械語でのコーディング作業はテキストエディタで⾏う
ことにします。

t2b もシェルスクリプトで、サンプルリポジトリの tools ディレクトリにあります。
標準の UNIX コマンドしか使っていないため、動作のために新たに必要なパッケー
ジは特にありません。

♣ テキストで機械語コードを書いてみる
それでは、コードを書いてみます。
前述の実験を少し変更し、2 つの jmp 命令を並べて、交互にジャンプするようにしてみました (リスト 1.3)。
内容は「sample.txt」というファイルで保存することにします。また、サンプルリポ
ジトリの中の、「011_jmp」ディレクトリの中にも置いてあります。

–7–

第1章

環境構築とはじめてのプログラム▼リスト 1.3: 011_jmp/sample.txt(機械語コードのみ)
eb 00
eb fc

なお、t2b は⾏頭・⾏中どちらでも、「#」以降の記述をコメントとして扱います。そのため、リスト 1.4 の様にコメントを付けることが可能です。機械語と「#」の間はタ
ブ⽂字でもスペースでもどちらでも構いません。
▼リスト 1.4: 011_jmp/sample.txt# 2つのjmp命令を交互に⾏き来する
eb 00# jmp +2
eb fc
# jmp -2

また、「:(コロン)」より左側の記述も無視するため、リスト 1.5 の様に先頭からのバイト数などを付け加えても構いません。
▼リスト 1.5: 011_jmp/sample.txt# 2つのjmp命令を交互に⾏き来する
0: eb 00
# jmp +2
2: eb fc
# jmp -2♣ バイナリへ変換
t2b を使⽤して以下のようにバイナリへの変換が⾏えます。
$ t2b sample.txt > code.bin

od コマンドでバイナリをダンプしてみると、意図したとおりに書かれていることを確認できます。
$ od -t x1 code.bin
0000000 eb 00 eb fc
0000004「-t x1」は 1 バイト区切りで表⽰するオプションです。
アドレスの⼤きい⽅へ 2 バイト先にジャンプする「eb 00」と、アドレスの⼩さい⽅
へ 2 バイト前へジャンプする「eb fc」が書かれており、意図通りのバイナリであることを確認できました。

–8–

1.4 VM 上で OS レスで実⾏する (QEMU, OVMF, poiboot)

1.4 VM 上で OS レスで実⾏する (QEMU, OVMF,
poiboot)♣ バイナリフォーマットについて
ここまでで機械語命令を格納したバイナリは⽣成できました。ただ、「実⾏バイナリ」とするにはヘッダが⾜りません。
「はじめに」でも説明した通り、本書では⼿で書いた実⾏バイナリを、QEMU 上で
OS レスで実⾏します。なお、QEMU にしろ実機にしろ、電源を⼊れてから (QEMUの場合はエミュレータを⽴ち上げてから) 最初に実⾏されるのは BIOS*4 です。ただ、本書で作るバイナリは BIOS のお作法に従ったものではないため、作ったバイナリをロードし実⾏を開始してくれる「ブートローダー」が必要です。本書では「poiboot」という独⾃のブートローダーを使⽤します。ブートローダーが関わるのはバイナリの実⾏
開始のみで、その後は OS レスで直接ハードウェアを制御します。
ブートローダー「poiboot」が実⾏できるバイナリフォーマットはシンプルで表 1.2の通りです。
▼表 1.2: poiboot が実⾏できるバイナリフォーマット
ヘッダ(先頭 16 バイト)本体

(17 バイト⽬以降)

bss 領域の先頭アドレス (8 バイト)
bss 領域のサイズ (8 バイト)
実⾏バイナリ本体 (機械語コード)※ poiboot はロード後、本体の先頭 (ヘッダの直後) へジャンプする

bss とは、C ⾔語での初期値なし (あるいは初期値 0) のグローバル変数や static 変数
等で、プログラム実⾏開始時にゼロで初期化しておくべき領域です。poiboot は実⾏バ
イナリ先頭の 16 バイトの内容から bss 領域のアドレスとサイズを確認し、本体の実⾏開始前にその領域をゼロで初期化します。

poiboot が事前にやることはこれだけです。初期化が完了したら、実⾏バイナリ本体の先頭へジャンプします。

bss については、ヘッダの「bss 領域のサイズ」が 0 の場合、poiboot は何もしないです。本書では特に bss 領域は使わないので、全て 0 の 16 バイトのバイナリを、機械語
コードの先頭にくっつければ良いだけです。

dd コマンドで全て 0 の 16 バイトのデータ「head.bin」を⽣成し、前項で作成した*4 現代は UEFI BIOS

–9–

第1章

環境構築とはじめてのプログラム「code.bin」と結合します。
$ dd if=/dev/zero of=head.bin bs=1 count=16
16+0 レコード⼊⼒
16+0 レコード出⼒
16 bytes copied, 0.000514814 s, 31.1 kB/s
$ cat head.bin code.bin > kernel.bin

poiboot は「kernel.bin」という名前の実⾏ファイルを認識するので、この名前で⽣成しました。

♣ QEMU と OVMF をインストール
以下のように QEMU をインストールします。
$ sudo apt install qemu-system-x86 ovmf

poiboot は、旧来の BIOS(レガシー BIOS) ではなく、「UEFI」という近年⼀般的に使われている BIOS を前提としているのですが、QEMU には UEFI のファームウェアが付属しないため、オープンソースの UEFI ファームウェアである「OVMF」を併せてインストールしています。

OVMF については、ホームディレクトリ直下に「OVMF」というディレクトリを作成し、そこへファームウェアバイナリを配置しておきます。
「ovmf」パッケージによってインストールされたファイルの⼀覧は以下のコマンドで確認できます。
$ dpkg -L ovmf/.
/usr
/usr/share/usr/share/OVMF/usr/share/OVMF/OVMF_CODE.fd/usr/share/OVMF/OVMF_VARS.fd/usr/share/doc/usr/share/doc/ovmf/usr/share/doc/ovmf/changelog.Debian.gz/usr/share/doc/ovmf/copyright/usr/share/ovmf/usr/share/ovmf/OVMF.fd/usr/share/qemu/usr/share/qemu/OVMF.fd「OVMF_CODE.fd」と「OVMF_VARS.fd」が本書で使⽤するファームウェアバイナリです。

– 10 –

1.4 VM 上で OS レスで実⾏する (QEMU, OVMF, poiboot)

以下のようにホームディレクトリ直下にディレクトリを作成し、ファイルをコピーしておいてください。
$ mkdir ~/OVMF$ cp /usr/share/OVMF/OVMF_CODE.fd ~/OVMF/$ cp /usr/share/OVMF/OVMF_VARS.fd ~/OVMF/♣ poiboot を取得
poiboot は GitHub 上で公開しています。
• poiboot - GitHub◦ https://github.com/cupnes/poiboot/
本書のブートローダーとして動作確認した時点は「x86_64_ml_20190922」でリリースしており、リリースページの URL は以下の通りです。

• Release x86_64_ml_20190922 - cupnes/poiboot◦ https://github.com/cupnes/poiboot/releases/tag/x86_64_ml_
20190922

poiboot の コ ン パ イ ル 済 み 実 ⾏ バ イ ナ リ は 、上 記 の リ リ ー ス ペ ー ジ に て「poiboot_x86_64_ml_20190922.zip」という zip アーカイブで取得できます。
ダウンロードし、展開しておいてください。以下の 2 つのファイルを使⽤します。
• poiboot.efi: poiboot 本体• poiboot.conf: 設定ファイル♣ 実⾏できるようにファイルを配置必要なものは全て⽤意できました。作成した実⾏バイナリを QEMU 上で実⾏するために、リスト 1.6 のようにファイルを配置してください。
▼リスト 1.6: QEMU で実⾏するためのファイル配置fs/├ efi/│ └ boot/│└ bootx64.efi├ kernel.bin└ poiboot.conf← poiboot.efi をリネームして配置

ここでは、「fs」という名前の作業ディレクトリへファイルを配置したとします。
この階層構造について簡単に説明すると、これは UEFI ファームウェア (QEMU 上
で実⾏する場合 OVMF) が実⾏ファイルを⾒つけるためのファイル配置です。UEFI は– 11 –

第1章

環境構築とはじめてのプログラム

レガシー BIOS とは違い、FAT ファイルシステムを認識できます。そして、起動ディ
スク上の FAT ファイルシステムに「efi/boot/bootx64.efi」という名前で実⾏ファイルが配置されていた場合、それを実⾏してくれます。

bootx64.efi は「poiboot」で す 。poiboot は FAT フ ァ イ ル シ ス テ ム 上 の「kernel.bin」を実⾏するため、作成した実⾏ファイルが実⾏される、という流れです。
作成した実⾏バイナリ「kernel.bin」が実⾏されるまでの流れをまとめると以下の通りです。

1. QEMU を起動する
2. UEFI ファームウェア (OVMF) が起動し efi/boot/bootx64.efi(poiboot) 実⾏
3. poiboot は kernel.bin(作成した実⾏バイナリ) を実⾏♣ QEMU で動作確認
ファイルを配置できたので、QEMU で実⾏してみます。

fs が存在するディレクトリと同じ階層で以下のコマンドを実⾏してください。
$ ls -d fs
fs ← 「fs」が⾒える階層(fsと同じ階層)で実⾏$ qemu-system-x86_64 \-m 4G -enable-kvm \-drive if=pflash,format=raw,readonly,file=$HOME/OVMF/OVMF_CODE.fd \-drive if=pflash,format=raw,file=$HOME/OVMF/OVMF_VARS.fd \-drive dir=fs,driver=vvfat,rw=on

KVM が無い環境の場合、「-enable-kvm」のオプションは外してください。
「-drive dir=fs,driver=vvfat,rw=on」のオプションで、前項で作成した「fs」
ディレクトリを FAT ファイルシステムとして扱うように指定しています。QEMU を実⾏する際のカレントディレクトリが fs が存在するディレクトリとは異なる場合、「dir=fs」の「fs」を良しなに変更してください。(相対パスあるいは絶対パスで指定すれば良いです。)
実⾏すると、GUI で QEMU の画⾯が表⽰されたかと思います。ただ、今回は何も画⾯に表⽰するプログラムではないので、⾒た⽬上は何も変化はありません。
この QEMU コマンドは今後⼿で打つにはしんどいくらいに⻑いので、「run_qemu」という名前でシェルスクリプト化してあります。他のツールと同様にサンプルリポジト
リの tools ディレクトリに⼊れてありますので、今後はこちらを使います。
先ほどのコマンドを「run_qemu」を使って実⾏する場合、以下の通りです。

– 12 –

1.4 VM 上で OS レスで実⾏する (QEMU, OVMF, poiboot)$ run_qemu fs

run_qemu は、引数に指定したディレクトリを、ブートローダーや実⾏バイナリが配置されたディレクトリとして使⽤します。
なお、QEMU は CUI で実⾏することもできます。QEMU に「-nographic」オプションを追加すると CUI で QEMU を起動できます。run_qemu は 2 つ⽬以降のコマンド
ライン引数を QEMU への追加オプションとして扱いますので、以下のようにオプションを追加すると、CUI モードで起動できます。
$ run_qemu fs -nographic♣ QEMU モニターで実⾏状況を確認する
QEMU には QEMU の実⾏状態を確認できる「QEMU モニター」という機能があります。

QEMU モニターの画⾯へ切り替えてみましょう。切り替え⽅は GUI/CUI のそれぞれ、表 1.3 の通りです。
▼表 1.3: QEMU モニターへの切り替え⽅法
GUI の場合
QEMU モニターへ切り替え
元の画⾯に戻るCUI の場合
QEMU モニターへ切り替え
元の画⾯に戻る
ヘルプ表⽰

Ctrl-Alt-2
Ctrl-Alt-1

Ctrl-a 押下のあと c キー押下
Ctrl-a 押下のあと c キー押下
Ctrl-a 押下のあと?キー押下CPU の演算やその結果は基本的に「レジスタ」という CPU 専⽤の⾼速な記憶領域に格納されます。「info registers」コマンドで現在の CPU のレジスタ値を確認できます。
QEMU 2.8.1 monitor - type 'help' for more information(qemu) info registers
RAX=0000000000110000 RBX=00000000bef67f98 RCX=0000000000000000 RDX=00000000000››00000
RSI=0000000000407180 RDI=00000000bfe9b018 RBP=00000000bff0eb10 RSP=00000000002››10000– 13 –

第1章

環境構築とはじめてのプログラム

R8 =00000000bff0e9dc R9 =0000000000000078 R10=00000000bfe6e080 R11=00000000e7d››c4657R12=00000000bef69540 R13=00000000bef69548 R14=00000000bff24b60 R15=00000000bef››6a018RIP=0000000000110002 RFL=00000002 [-------] CPL=0 II=0 A20=1 SMM=0 HLT=0...

GUI の際、出⼒が 1 画⾯に収まらず流れてしまった際は「Ctrl を押しながら上下キー」でスクロールできます。
ここで注⽬すべきは「RIP=」の値です。これは、今 CPU が実⾏している命令のアドレスです。poiboot は「kernel.bin」内の実⾏バイナリ本体 (ヘッダより後の 17 バイト⽬以降) を、メモリアドレス「0x110000」から配置するので、RIP が「0x110000」から⼤きく離れていると異常です。今回の場合、1 つ⽬の jmp 命令のアドレスは「0x110000」
で、2 つ⽬の jmp 命令のアドレスは「0x110002」となりますので、RIP がこのいずれかの値になっていれば正常です。

1 つ⽬と 2 つ⽬の jmp 命令を繰り返しているはずなので、何度か info registers
コマンドを繰り返すと「RIP」が「0x110000」になったり「0x110002」になったりします。
また、メモリの内容を表⽰させることもできます。例えば、実⾏バイナリ本体が配置されている 0x110000 以降の内容を逆アセンブルして命令 2 個分を表⽰させるには以下の通りです。
(qemu) x/2i 0x110000
0x0000000000110000: jmp
0x0000000000110002: jmp

0x110002
0x110000「x/2i」の「x」が「引数に指定されたメモリアドレスの内容を表⽰する」というコマ
ンドで、「/」以降の「2i」がフォーマット指定です。
「i」が「逆アセンブルしてアセン
ブラ表⽰」という表⽰形式を⽰しており、「2」はそれを「2 個分」表⽰することを⽰しています。なお、もちろん 16 進数等で表⽰させることもできます。詳しくは QEMUのドキュメントをご覧ください*5 。
このようなビジーループのプログラムを実⾏しているとホスト PC 側の CPU 使⽤率が上昇していきます。レジスタ値やメモリの内容を確認する際は⼀旦 QEMU の実⾏を「stop」コマンドで⽌めておくと良いです。「cont(短縮表記 c)」コマンドで再開できます。

*5 https://qemu.weilnetz.de/doc/qemu-doc.html#Commands– 14 –

1.5 何もしない命令「nop」(命令 2)<⼀時停⽌>(qemu) stop
<再開>
(qemu) cont
<再開(短縮表記)>(qemu) c

確認後、QEMU を終了させる際は、QEMU モニター上では「quit」コマンドで終了できます。GUI の場合はウィンドウの [X] ボタンでも構いません。また、CUI の場
合は「Ctrl-a のあと x」でも終了できます。
(qemu) quit

以上で、「機械語の構⽂理解」から「実⾏バイナリ作成」、「QEMU での動作確認」ま
でのフルセットの流れが完了です。今後は、命令の簡単さや章ごとのテーマによって、フルセットで都度確認したりはしないですが、気になる場合はここで確認したように
⾊々と実験してみてください。
この章では最後にあと 2 つ、無限ループプログラムに関連した命令を紹介します。

1.5 何もしない命令「nop」(命令 2)
今後のためにこの章ではあと 2 つ、簡単な命令をさくっと紹介します。
まずは、「nop」命令です。サンプルディレクトリは「012_nop」です。
nop 命令は「何もしない命令」です。前節では jmp 命令のジャンプ先の相対アドレ
スを⾊々と変えてみるために jmp 命令⾃体を間に⼊れてみたりしていましたが、そのような場合はこの命令を使うと良いです。

as_dump を使ってこの命令の機械語を確認します。
$ as_dump nop/tmp/tmp.MrEmEUlxOC/a.out:

ファイル形式 elf64-x86-64

セクション .text の逆アセンブル:

0000000000000000 <.text>:
0:
90

nop

– 15 –

第1章

環境構築とはじめてのプログラム

オペランドを取らない nop 命令のアセンブラ/機械語構⽂は表 1.4 の通りであると分かりました。
▼表 1.4: 構⽂ 2: nop: 何もしない
アセンブラ
機械語

nop
90

例えば、
「3 バイト分戻るジャンプ」を実験として試してみる際は、nop を使ってリス
ト 1.7 のように書けます。
▼リスト 1.7: 012_nop/sample.txt
0: 90
1: 90
2: 90
3: eb fb

# nop
# nop
# nop
# jmp -3

1.6 CPU を休ませる「hlt」(命令 3)
この章で紹介する最後の命令は「hlt」命令です。サンプルディレクトリは「013_hlt」
です。

hlt 命令はシャットダウン等の意味合いで使われる「halt」の略です。この命令⾃体
はシャットダウンという程ではなく、割り込みがあるまで CPU をスリープさせます。
本書で⾏うような OS レスのプログラミングの場合、戻る先が無いので、プログラムの最後では必ず無限ループを設置して処理を⽌めておく必要があります。その際、ビジーループだと CPU 負荷が上がってしまうので、hlt 命令で CPU を休ませるようにすると良いです。

hlt 命令も as_dump で機械語を確認します。
$ as_dump hlt/tmp/tmp.5dlph8ymMx/a.out:

ファイル形式 elf64-x86-64

セクション .text の逆アセンブル:

0000000000000000 <.text>:
0:
f4

hlt

– 16 –

1.6 CPU を休ませる「hlt」(命令 3)

オペランドを取らない hlt 命令のアセンブラ/機械語構⽂をまとめると表 1.5 の通りです。
▼表 1.5: 構⽂ 3: hlt: CPU をスリープさせる
アセンブラ
機械語

hlt
f4

例えばリスト 1.8 のような感じで使います。
▼リスト 1.8: 013_hlt/sample.txt# 何らかの処理
90
# nop
90
# nop
# 無限Halt
f4
# hlt
eb fd
# jmp -1

hlt 命令による CPU のスリープ状態からは、割り込み等で起床するので、完全にやることがなくなった際は hlt 命令を実⾏し続けるように jmp 命令でループを⼊れておきます。

– 17 –

第2 章 基本的な演算

この章では、基本的な演算命令の機械語における構⽂を紹介します。以降の章で使うことになる構⽂の紹介が主で、この章では章を通して作り上げるサンプルはありません。

CPU は、基本的にレジスタ上の値で演算を⾏うため、まず、任意のレジスタへ値を格納する⽅法を紹介し、その後、四則演算・論理演算等の基本的な演算の⽅法を紹介します。
なお、機械語コードは、オペランドのパターン (オペランドの数や種類) が変わると機械語のバイト列も変化します。この章では、以降の章でも使⽤するような代表的な構⽂のみ紹介します。アセンブラの構⽂も併せて紹介しますので、演算対象が異なる場合など、ここで紹介しない構⽂については、as_dump 等で確認してみてください。

2.1 データ格納命令 (mov)(命令 4)♣ 汎⽤レジスタ
まず、⼀般的に CPU 命令で使⽤するレジスタを紹介します。

CPU が汎⽤的にデータ格納先として利⽤できるレジスタが「汎⽤レジスタ」です。
QEMU モニターの info registers コマンドで表⽰されるレジスタの内、リスト 2.1のレジスタが汎⽤レジスタです。
▼リスト 2.1: QEMU モニターの info registers で表⽰されるレジスタ
RAX=0000000000110101 RBX=00000000bef67f02 RCX=0000000000000000 RDX=00000››00000000000
RSI=0000000000407180 RDI=00000000bfe9b018 RBP=00000000bff0eb10 RSP=00000››00000210000
R8 =00000000bff0e9dc R9 =0000000000000078 R10=00000000bfe6e080 R11=00000››000e7dc4657
R12=00000000bef69540 R13=00000000bef69548 R14=00000000bff24b60 R15=00000››000bef6a018

RAX から RDI、R8 から R15 までの計 16 個のレジスタが並んでいます。これらはそれぞれ 64 ビットの⼤きさがあります。なお、⼀部のレジスタは、64 ビット幅の中の– 19 –

第2章

基本的な演算⼀部分にのみアクセスすることが可能です。レジスタの下位 32 ビットへのアクセスや、
さらにその中の下位 16 ビットへのアクセス、その 16 ビットの内の上位/下位それぞれ
8 ビットずつへのアクセスといったことができます。アクセスできるビット幅には名前が付いており、⼀覧にすると表 2.1 の通りです。
▼表 2.1: 汎⽤レジスタ⼀覧
64bit 全体
RAX
RCX
RDX
RBX
RSP
RBP
RSI
RDI
R8
R9
R10
R11
R12
R13
R14
R15

下位 32bit

下位 16bit

下位 16bit の上位 8bit

下位 16bit の下位 8bit

EAX
ECX
EDX
EBX
ESP
EBP
ESI
EDI
R8D
R9D
R10D
R11D
R12D
R13D
R14D
R15D

AX
CX
DX
BX
SP
BP
SI
DI
-

AH
CH
DH
BH
-

AL
CL
DL
BL
-

8 ビットの場合のみ、16 ビットアクセス時の上位側 (H) と下位側 (L) とで別々にアクセスできます。16 ビット/32 ビットのアクセスの際は、「32 ビット幅の上位 16 ビット」や「64 ビット幅の上位 32 ビット」というアクセスはできません。
なお、「R8」から「R15」のレジスタ (32 ビットにおける「R8D」から「R15D」) は本
書では使⽤しないため特に紹介しません。使い⽅はその他のレジスタ同様なので、使ってみたい場合は各⾃で as_dump 等から機械語を確認してみてください。

♣ 即値をレジスタへ格納する機械語コードを⾒てみる
レジスタへの値の格納は「mov」命令で⾏えます。ここでは 8 ビットの即値*1 を 8
ビットのレジスタへ格納する命令を⾒てみます。mov 命令はバリエーションが多いの*1 値そのものを⽰すオペランドです。アセンブラでは先頭に「$」を付けると即値を表すオペランドになります。

– 20 –

2.1 データ格納命令 (mov)(命令 4)で、その他の構⽂は、必要になったらその都度確認することにします。
例えば「0x12」という 8 ビット即値を「AL」レジスタへ格納する場合、アセンブラ
としては以下のコードになります。
▼リスト 2.2:
mov$0x12, %al

as_dump を使って機械語コードを確認してみます。
$ as_dump mov \$0x12,%al/tmp/tmp.zHlktVD4V2/a.out:

ファイル形式 elf64-x86-64

セクション .text の逆アセンブル:

0000000000000000 <.text>:
0:
b0 12

mov

$0x12,%al「mov $0x12,%al」は機械語では「b0 12」であることが分かりました。2 バイト⽬の「12」がオペランドで指定した即値「0x12」です。この値を変えれば、AL へ格納する値を変更できます。
また、機械語の 1 バイト⽬の「b0」が「AL へ格納する」事を⽰しており、ここを変更することで別のレジスタへ値を格納できます。

♣ 他のレジスタへ格納する命令も⾒てみる
例えば、CL レジスタ・DL レジスタへ格納する命令のアセンブラと機械語は表 2.2の通りです。
▼表 2.2: mov: 即値 0x12 をレジスタ CL・DL へ格納
CL レジスタアセンブラ
機械語

mov $0x12, %cl
b1 12

DL レジスタアセンブラ
機械語

mov $0x12, %dl
b2 12

機械語の 1 バイト⽬に着⽬すると、AL の時は「b0」、CL の時は「b1」、DL の時は「b2」と、1 ずつ増えています。

– 21 –

第2章

基本的な演算このように、レジスタをオペランドに取る機械語は、レジスタを⽰すバイトを 1 ず
つ変化させることで別のレジスタを指定することができ、レジスタには並び順があります。

8 ビットレジスタの並び順は他の命令でも共通で以下の通りです。
▼表 2.3: 機械語における 8 ビットレジスタ並び順
reg_ofs
0
1
2
3
4
5
6
7

レジスタ

AL
CL
DL
BL
AH
CH
DH
BH

「reg_ofs」は、レジスタを⽰すオペランド部分のベース値に対するオフセットを⽰します。今回の場合ベースは「b0」で、そこにオフセット値を⾜すことで任意の 8 ビッ
トレジスタを選択できます。今後、レジスタをオペランドにとる構⽂の表現に使⽤します。

♣ 構⽂: mov: 即値 (8bit) をレジスタ (8bit) へ格納
以上をまとめると、構⽂としては表 2.4 の通りです。
▼表 2.4: 構⽂ 4: mov: 即値 (8bit) をレジスタ (8bit) へ格納
アセンブラ
機械語

mov 即値 (8bit), レジスタ (8bit)
b0+reg_ofs 即値 (8bit)♣ 構⽂: mov: 即値 (16bit) をレジスタ (16bit) へ格納
続いて、16 ビット即値を 16 ビットレジスタへ格納する場合も⾒てみます。
$ as_dump mov \$0x1234,%ax/tmp/tmp.eTaoyVyqMf/a.out:

ファイル形式 elf64-x86-64– 22 –

2.1 データ格納命令 (mov)(命令 4)

セクション .text の逆アセンブル:

0000000000000000 <.text>:
0: 66 b8 34 12

mov$0x1234,%ax

機械語「66 b8 34 12」の最後の 2 バイト「34 12」が即値 0x1234 を⽰しています。
「0x12」と「0x34」がひっくり返っているのは、Intel 命令のバイトオーダーがリトルエ
ンディアン*2 であるためです。そして、対象とするレジスタは 2 バイト⽬の「b8」が⽰しています。
なお、16 ビットレジスタの並び順は以下の通りです。
▼表 2.5: 16 ビットレジスタ並び順
reg_ofs
0
1
2
3
4
5
6
7

レジスタ

AX
CX
DX
BX
SP
BP
SI
DI

以上をまとめると構⽂としては以下の通りです。
▼表 2.6: 構⽂ 5: mov: 即値 (16bit) をレジスタ (16bit) へ格納
アセンブラ
機械語

mov 即値 (16bit), レジスタ (16bit)
66 b9+reg_ofs 即値 (16bit)

32 ビットと 64 ビットについては、即値をレジスタへ格納する機会が本書では無く、確認⽅法等もこれまで同様なので得に紹介しません。
ただ、それぞれのレジスタは他の命令で使うことがあるので、並び順だけ他のビットも合わせて紹介しておきます。

*2 バイトの並び順 (バイトオーダー) を、下の位から順に 1 バイトずつ並べること。

– 23 –

第2章

基本的な演算▼表 2.7: レジスタの並び順まとめ
reg_ofs
0
1
2
3
4
5
6
7

64bit
RAX
RCX
RDX
RBX
RSP
RBP
RSI
RDI

32bit
EAX
ECX
EDX
EBX
ESP
EBP
ESI
EDI

16bit
AX
CX
DX
BX
SP
BP
SI
DI

8bit
AL
CL
DL
BL
AH
CH
DH
BH

2.2 四則演算
それでは、続いて四則演算を機械語で記述する⽅法を紹介します。
四則演算の命令は、⼀部を除いて基本的に 2 つのオペランドを取り、第 1 オペランドと第 2 オペランドで演算した結果を第 2 オペランドのレジスタ (あるいはメモリアドレ
ス先) へ格納するという挙動になります。
分かりやすい代表的な例として、8 ビット即値と 8 ビットレジスタで計算する場合を中⼼に紹介します。その他のパターンも同様の⽅法で構⽂を確認できますので、興味があれば適宜確認してみてください。

♣ 和 (add)(命令 5)和を計算するのが add 命令です。2 つのオペランドを取り、第 1 オペランドと第 2 オ
ペランドの和を第 2 オペランドへ格納します。
例えば、8 ビットレジスタ AL へ即値 0x12 を加えた結果を AL へ格納する場合、ア
センブラでは「add $0x12,%al」となり、その機械語コードは以下の通りです。
$ as_dump add \$0x12,%al/tmp/tmp.Ebxte7x4HM/a.out:

ファイル形式 elf64-x86-64

セクション .text の逆アセンブル:

0000000000000000 <.text>:
0: 04 12add

$0x12,%al– 24 –

2.2 四則演算「12」は即値で、「04」が対象レジスタを決めるベース値に⾒えます。

CL レジスタの場合はどうなるのか⾒てみましょう。
$ as_dump add \$0x12,%cl/tmp/tmp.t62F0yZWGi/a.out:

ファイル形式 elf64-x86-64

セクション .text の逆アセンブル:

0000000000000000 <.text>:
0:
80 c1 12add

$0x12,%cl

機械語コードの構⽂ががらっと変わりました。
実は、「04 XX」という表現は、8 ビット即値を AL に加算する場合にだけ使えるもの
で、8 ビット即値と 8 ビットレジスタでの加算命令の「シンタックスシュガー」のようなものです。
▼表 2.8: 構⽂ 6: add: 即値 (8bit) を AL へ加算
アセンブラ
機械語

add 即値 (8bit), %al04 即値 (8bit)

AL 以外の 8 ビットレジスタへ 8 ビット即値を加算する場合の構⽂は以下の通りです。
▼表 2.9: 構⽂ 7: add: 即値 (8bit) をレジスタ (8bit) へ加算
アセンブラ
機械語

add 即値 (8bit), レジスタ (8bit)
80 c0+reg_ofs 即値 (8bit)

das_dump 逆アセンブルしてみると分かりますが、構⽂ 7 で AL レジスタを使うこともできるようです。
$ das_dump 80 c0 12/tmp/tmp.NLv4saxnSr/a.bin:

ファイル形式 binary

セクション .data の逆アセンブル:– 25 –

第2章

基本的な演算0000000000000000 <.data>:
0:
80 c0 12add

$0x12,%al

ただ、せっかく 1 バイト減らせる表現があるので、AL レジスタを使う際は構⽂ 6 の表現を使うほうが良いです。このように、命令によっては特定のパターンのみ命令のバ
イト数を減らす構⽂があります。

♣ 差 (sub)(命令 6)
差は「sub」命令です。
和 (add) と同様なので、さくっと構⽂をまとめます。

sub も add と同様に AL レジスタでのみ使える構⽂があります。以下の通りです。
▼表 2.10: 構⽂ 8: sub: 即値 (8bit) を AL レジスタから減算
アセンブラ
機械語

sub 即値 (8bit), %al2c 即値 (8bit)

例えば、8 ビット即値 0x12 を 8 ビットレジスタ AL から減算する場合、以下のようになります。
▼表 2.11: 例: sub: 0x12 を AL から減算
アセンブラ
機械語

sub $0x12, %al
2c 12

次に、AL 以外でも使える構⽂です。
▼表 2.12: 構⽂ 9: sub: 即値 (8bit) をレジスタ (8bit) から減算
アセンブラ
機械語

sub 即値 (8bit), レジスタ (8bit)
80 e8+reg_ofs 即値 (8bit)

例えば、8 ビット即値 0x12 を 8 ビットレジスタ CL から減算する場合、以下のようになります。

– 26 –

2.2 四則演算▼表 2.13: 例: sub: 0x12 を CL から減算
アセンブラ
機械語

sub $0x12, %cl
80 e9 12♣ 積 (imul)(命令 7)
オペランドが 1 つの場合オペランドが 1 つの指定ができます。オペランドが 1 つのパターンで指定できるのはレジスタ名で、例えば 8 ビットレジスタを指定した場合、「AL * 指定されたレジスタ→ AX」という命令になります。暗黙の内に AL レジスタとの掛け算を⾏い、結果を⼀回り⼤きい 16 ビットの AX レジスタへ格納します。単⼀オペランド時のレジスタ幅に対する挙動を表 2.14 にまとめます。
▼表 2.14: imul: 単⼀オペランドの挙動まとめ
レジスタ幅

挙動

8 ビット
16 ビット
32 ビット
64 ビットAL * オペランド → AX
AX * オペランド → DX:AXEAX * オペランド → EDX:EAXRAX * オペランド → RDX:RAX

オペランドが 16 ビット以上のレジスタの場合、積は上位ビットと下位ビットが Dのレジスタと A のレジスタに分けて格納されます。例えば、16 ビットの演算で積が「0x1234 5678」であった場合、「0x1234」が DX へ、「0x5678」が AX へ格納されます。
オペランドに 8 ビットレジスタを指定した場合、構⽂としては以下の通りです。
▼表 2.15: 構⽂ 10: imul: AL * レジスタ (8bit) を AX へ格納
アセンブラ
機械語

imul レジスタ (8bit)
f6 e8+reg_ofs

例えば、AL の⼆乗を計算するような場合、以下のようになります。

– 27 –

第2章

基本的な演算▼表 2.16: 例: imul: AL * AL を AX へ格納
アセンブラ
機械語

imul %al
f6 e8

オペランドが 2 つの場合オペランドが 2 つの場合の挙動は、和や差と同じで「第 1 オペランドと第 2 オペラン
ドの演算結果を第 2 オペランドへ格納」です。
ただし、第 1 オペランドに即値を使う構⽂はありません。
例えば、第 1 オペランドに「AX」を、第 2 オペランドに「CX」を指定した場合は、「AX * CX」の結果が「CX」へ格納されます。(単⼀オペランドの時のように「⼀回り⼤きなレジスタへ格納」とはなりません。)
構⽂としては以下の通りです。
▼表 2.17: 構⽂ 11: imul: 第 1 * 第 2 オペランド (16bit レジスタ) を第 2 オペランドへ
格納
アセンブラ
機械語

imul レジスタ (16bit,src), レジスタ (16bit,dst)
66 0f af c0+reg_dst_src「reg_dst_src」は「第 1 オペランド (src)」と「第 2 オペランド (dst)」のレジスタの組み合わせを⽰すオフセット値で、以下の通りです。
▼表 2.18: 16 ビットレジスタの reg_dst_src ⼀覧
dst/src
AX
CX
DX
BX
SP
BP
SI
DI

AX
0x00
0x08
0x10
0x18
0x20
0x28
0x30
0x38

CX
0x01
0x09
0x11
0x19
0x21
0x29
0x31
0x39

DX
0x02
0x0a
0x12
0x1a
0x22
0x2a
0x32
0x3a

BX
0x03
0x0b
0x13
0x1b
0x23
0x2b
0x33
0x3b

SP
0x04
0x0c
0x14
0x1c
0x24
0x2c
0x34
0x3c

BP
0x05
0x0d
0x15
0x1d
0x25
0x2d
0x35
0x3d

SI
0x06
0x0e
0x16
0x1e
0x26
0x2e
0x36
0x3e

DI
0x07
0x0f
0x17
0x1f
0x27
0x2f
0x37
0x3f

各列が「第 1 オペランド (src)」で、各⾏が「第 2 オペランド (dst)」を⽰す表です。

– 28 –

2.2 四則演算

例に挙げた「AX * CX を CX へ格納する」場合、以下のようになります。
▼表 2.19: 例: AX * CX を CX へ格納
アセンブラ

imul %ax, %cx
66 0f af c8

機械語

なお、imul 命令のオペランド 2 つの場合では、第 1・第 2 いずれにも 8 ビットレジ
スタを使⽤する構⽂はありません。
オペランドが 3 つの場合

imul はオペランドが 3 つの指定もできます。その場合、第 1 オペランドは「即値」、
第 2・第 3 オペランドが「レジスタ」での指定となり、「第 1 オペランドと第 2 オペラ
ンドの演算結果を第 3 オペランドへ格納」という挙動になります。
なお、第 2・第 3 オペランドのレジスタ幅に応じて第 1 オペランドで指定できる即値のビット数が異なります。まとめると表 2.20 の通りです。
▼表 2.20: オペランド 3 つの場合の即値・レジスタ幅の組み合わせまとめ
第 1 オペランド (即値)

第 2・第 3 オペランド (レジスタ)

8 ビット
8 ビット
8 ビット
16 ビット
32 ビット
32 ビット

16 ビット
32 ビット
64 ビット
16 ビット
32 ビット
64 ビット

例えば、第 1 オペランドに 8 ビット即値「0x12」を、第 2 オペランドに 16 ビットレ
ジスタ「AX」を、第 3 オペランドに 16 ビットレジスタ「CX」を指定した場合、「0x12* AX」の結果が「CX」へ格納されます。
構⽂としては以下の通りです。
▼表 2.21: 構⽂ 12: imul: 第 1 オペランド * 第 2 オペランドを第 3 オペランドへ格納
アセンブラ
機械語

imul 即値 (8bit), レジスタ (16bit,src), レジスタ (16bit,dst)
66 6b c0+reg_dst_src 即値 (8bit)– 29 –

第2章

基本的な演算例に挙げた「0x12 * AX を CX へ格納する」場合、以下のようになります。
▼表 2.22: 例: 0x12 * AX を CX へ格納
アセンブラ
機械語

imul $0x12, %ax, %cx
66 6b c8 12♣ 商と剰余 (idiv)(命令 8)
商と剰余を求める「idiv」命令の場合、「オペランドが 1 つ」のパターンの命令しかありません。単⼀オペランドでレジスタを指定することができ、指定したレジスタのビット幅に応じて表 2.23 の挙動となります。
▼表 2.23: idiv: 指定したレジスタのビット幅に対する挙動まとめ
オペランドレジスタ幅

挙動

8 ビット
16 ビット
32 ビット
64 ビット「AX/オペランド」の商を AL へ剰余を AH へ格納「DX:AX/オペランド」の商を AX へ剰余を DX へ格納「EDX:EAX/オペランド」の商を EAX へ剰余を EDX へ格納「RDX:RAX/オペランド」の商を RAX へ剰余を RDX へ格納

そして、オペランドが 8 ビットレジスタの場合の構⽂は以下の通りです。
▼表 2.24: 構⽂ 13: idiv: AX/レジスタ (8bit) の商を AL へ剰余を AH へ格納
アセンブラ
機械語

idiv レジスタf6 f8+reg_ofs

例えば、AX/AL の商を AL へ剰余を AH へ格納する場合、以下のようになります。
▼表 2.25: 例: AX/AX の商 (1) を AL へ剰余 (0) を AH へ格納
アセンブラ
機械語

idiv %al
f6 f8

なお、0 で割る演算を⾏った場合、CPU で「0 除算」の例外が発⽣します。本書では– 30 –

2.3 論理演算

例外時のジャンプ先 (ハンドラ) の設定を⾏っていないので、例外発⽣時の挙動は未定
義となりますので、ご注意ください*3 。

2.3 論理演算
続いて、論理演算の⽅法を AND・OR・XOR・NOT の 4 つについて紹介します。

♣ AND(命令 9)
オペランドの数は 2 つのパターンのみで、第 1 オペランドと第 2 オペランドの演算結果を第 2 オペランドへ格納します。

8 ビット即値と 8 ビットレジスタの AND の結果を 8 ビットレジスタへ格納する場合の構⽂を紹介します。
第 2 オペランドに指定するレジスタが AL の場合、1 バイト少ない機械語表現があり、構⽂は以下の通りです。
▼表 2.26: 構⽂ 14: and: 即値 (8bit) AND AL を AL へ格納
アセンブラ
機械語

and 即値 (8bit), %al
24 即値 (8bit)

例えば、8 ビット即値 0x12 と AL の AND を AL へ格納する場合、以下のようになります。
▼表 2.27: 例: 0x12 と AL の AND を AL へ格納
アセンブラ
機械語

and $0x12, %al
24 12

そして、第 2 オペランドのレジスタが AL 以外でも使える構⽂は表 2.28 の通りです。
例えば、即値 0x12 とレジスタ CL の AND をレジスタ CL へ格納する場合の機械語
コードは表 2.29 の通りです。

*3 QEMU 上なので試してみても何も被害は無いとは思います。何が書かれているか分からないところ

へジャンプして CPU がそこに書かれたバイト列を命令と解釈して突き進んで⾏きます。

– 31 –

第2章

基本的な演算▼表 2.28: 構⽂ 15: and: 即値 (8bit) AND レジスタ (8bit) をレジスタ (8bit) へ格納
アセンブラ
機械語

and 即値 (8bit), レジスタ (8bit)
80 e0+reg_ofs 即値 (8bit)▼表 2.29: 例: 0x12 と CL の AND を CL へ格納
アセンブラ

and $0x12, %cl
80 e1 12

機械語

♣ OR(命令 10)
or 命令も and 命令同様、オペランドの数は 2 つのパターンのみです。
「8 ビット即値 (第 1 オペランド) OR 8 ビットレジスタ (第 2 オペランド)」の結果を第 2 オペランドへ格納する構⽂を紹介します。

and 命令同様に、第 2 オペランドのレジスタが AL の場合には 1 バイト短い構⽂があります。
▼表 2.30: 構⽂ 16: or: 即値 (8bit) OR AL を AL へ格納
アセンブラ

or 即値 (8bit), %al0c 即値 (8bit)

機械語

例えば、8 ビット即値 0x12 とレジスタ AL の OR をレジスタ AL へ格納する場合は以下のようになります。
▼表 2.31: 例: 0x12 と AL の OR を AL へ格納
アセンブラ
機械語

or $0x12, %al
0c 12

そして、第 2 オペランドのレジスタが AL 以外でも使える構⽂は表 2.32 の通りです。
例えば、即値 0x12 とレジスタ CL の OR をレジスタ CL へ格納する場合の機械語
コードは表 2.33 の通りです。

– 32 –

2.3 論理演算▼表 2.32: 構⽂ 17: or: 即値 (8bit) OR レジスタ (8bit) をレジスタ (8bit) へ格納
アセンブラ
機械語

or 即値 (8bit), レジスタ (8bit)
80 c8+reg_ofs 即値 (8bit)▼表 2.33: 例: 0x12 と CL の OR を CL へ格納
アセンブラ
機械語

or $0x12, %cl
80 c9 12

♣ XOR(命令 11)
XOR もオペランドの数は 2 つのパターンのみです。
「8 ビット即値 (第 1 オペランド) XOR 8 ビットレジスタ (第 2 オペランド)」の結果
を第 2 オペランドへ格納する構⽂を紹介します。

and 命令・or 命令同様に、第 2 オペランドのレジスタが AL の場合には 1 バイト短い構⽂があります。
▼表 2.34: 構⽂ 18: xor: 即値 (8bit) XOR AL を AL へ格納
アセンブラ
機械語

xor 即値 (8bit), %al34 即値 (8bit)

例えば、8 ビット即値 0x12 とレジスタ AL の XOR をレジスタ AL へ格納する場合
は以下のようになります。
▼表 2.35: 例: 0x12 と AL の XOR を AL へ格納
アセンブラ
機械語

xor $0x12, %al
34 12

そして、第 2 オペランドのレジスタが AL 以外でも使える構⽂は表 2.36 の通りです。
例えば、即値 0x12 とレジスタ CL の XOR をレジスタ CL へ格納する場合の機械語
コードは表 2.37 の通りです。

– 33 –

第2章

基本的な演算▼表 2.36: 構⽂ 19: xor: 即値 (8bit) XOR レジスタ (8bit) をレジスタ (8bit) へ格納
アセンブラ

xor 即値 (8bit), レジスタ (8bit)
80 f0+reg_ofs 即値 (8bit)

機械語▼表 2.37: 例: 0x12 と CL の XOR を CL へ格納
アセンブラ

xor $0x12, %cl
80 f1 12

機械語

♣ NOT(命令 12)
NOT は、オペランドの数は 1 つのみです。
「オペランドで指定された 8 ビットレジスタの NOT をオペランドのレジスタへ格納する」場合、構⽂は以下の通りです。
▼表 2.38: 構⽂ 20: not: レジスタ (8bit) の NOT をレジスタ (8bit) へ格納
アセンブラ

not レジスタ (8bit)
f6 d0+reg_ofs

機械語

例えば、レジスタ AL の NOT をレジスタ AL へ格納する場合は以下のようになります。
▼表 2.39: 例: AL の NOT を AL へ格納
アセンブラ
機械語

not %al
f6 d0

2.4 シフト演算 (命令 13-15)
シフト演算の命令は以下の 4 つです。

• SAL: 算術左シフト• SAR: 算術右シフト– 34 –

2.4 シフト演算 (命令 13-15)• SHL: 論理左シフト• SHR: 論理右シフト
全てオペランドの数は 2 つです。
各命令について、オペランドを使って実際に⾏われる演算は以下の通りです。
▼表 2.40: 各シフト演算の挙動
sal
sar
shl
shr

第 1 オペランドの回数分、第 2 オペランドへ 2 を掛ける
第 1 オペランドの回数分、第 2 オペランドを 2 で割る (符号有)第 1 オペランドの回数分、第 2 オペランドへ 2 を掛ける
第 1 オペランドの回数分、第 2 オペランドを 2 で割る (符号無)sal 命令と shl 命令は挙動が同じで、どちらでアセンブラを書いても同じ機械語が⽣成されます。そのため、機械語として実質存在するシフト命令は 3 つです。
なお、シフト命令は、第 1 オペランドに即値あるいは CL レジスタしか指定できません。
「第 1 オペランドを即値 (8bit)、第 2 オペランドをレジスタ (8bit)」の場合、構⽂は
以下の通りです。(shl は sal と同⼀なので省略)▼表 2.41: 構⽂ 21-23: sal/sar/shr: 第 1 オペランド=即値 (8bit)、第 2 オペランド=レジスタ (8bit)
アセンブラ
機械語
アセンブラ
機械語
アセンブラ
機械語

sal 即値 (8bit), レジスタ (8bit)
c0 e0+reg_ofs 即値 (8bit)
sar 即値 (8bit), レジスタ (8bit)
c0 f8+reg_ofs 即値 (8bit)
shr 即値 (8bit), レジスタ (8bit)
c0 e8+reg_ofs 即値 (8bit)

例えば、AL レジスタに対して即値 3 で SAL/SAR/SHR を実施する場合の機械語
コードは表 2.42 の通りです。
前述の通り、算術左シフトと論理左シフトは挙動は同じなので、機械語としては同じ
バイナリ列になっています。

– 35 –

第2章

基本的な演算▼表 2.42: 例: AL に対して即値 3 で SAL/SAR/SHR を実施
アセンブラ
機械語
アセンブラ
機械語
アセンブラ
機械語
アセンブラ
機械語

sal $3,%alc0 e0 03
sar $3,%al
c0 f8 03
shl $3,%alc0 e0 03
shr $3,%al
c0 e8 03

2.5 インクリメント/デクリメント (命令 16-17)
インクリメント (inc) とデクリメント (dec) は共に取れるオペランドの数は⼀つのみで、構⽂も似たような感じです。

8 ビットレジスタをインクリメント/デクリメントする場合の構⽂は以下の通りです。
▼表 2.43: 構⽂ 24-25: inc/dec: レジスタ (8bit) をインクリメント/デクリメント
アセンブラ
機械語
アセンブラ
機械語

inc レジスタ (8bit)
fe c0+reg_ofs
dec レジスタ (8bit)
fe c8+reg_ofs

例えば、AL レジスタをインクリメント/デクリメントする場合は以下のようになります。
▼表 2.44: 例: AL をインクリメント/デクリメント
アセンブラ
機械語
アセンブラ
機械語

inc %alfe c0
dec %alfe c8– 36 –

第3 章 シリアルドライバを作る

デバッグ⽤にテキストの表⽰などが⾏えると便利です。ここではシリアル通信*1 のドライバを作成し、シリアル通信により⽂字の送受信を⾏うことで、QEMU のシリアル
通信⽤の画⾯で⽂字の表⽰と⼊⼒された⽂字の取得を⾏ってみます*2 。

3.1 ⽂字を送信して画⾯表⽰
それでは早速、シリアル通信による⽂字の送信を試してみましょう。この節のサンプ
ルディレクトリは「031_ser_tx」です。

♣ シリアル通信を⾏うにはシリアル通信には専⽤のコントロール IC(コントローラ) があります。コントローラにも CPU 同様にレジスタがあり、そのレジスタへ値を書き込んだり、逆に値を読み出したりすることで、シリアル通信でデータを送信したり受信したりできます。
外部の IC が持つレジスタは、基本的に「IO アドレス空間」というアドレス空間からアクセスします。これは、ポインタ等でアクセスするような通常のメモリのアドレス空間とは別で、「in」命令と「out」命令という専⽤の命令を使⽤して、そのアドレス空間
へアクセスします。
シリアルでのデータ送信は「THR(Transmitter Holding Register)」というレジスタで⾏います。IO アドレスなどについては表 3.1 の通りです*3 。
レジスタサイズが 1 バイトなので、1 ⽂字ずつ書き込み、1 ⽂字ずつ送信します。

*1 送信 (Tx) と受信 (Rx) をそれぞれ 1 本の信号線でやり取りする通信⽅式です。Windows では「COMポート」と呼ばれていたり、Linux では「シリアルポート (ttySx)」等と呼ばれているやつです。昔のPC には「D-sub9 ピン」と呼ばれるコネクタが搭載されていて、「RS-232C」規格のケーブルを使ってシリアル通信を⾏うことができました。
*2 ここはあくまでもシリアル通信による⽂字の送受信なので、キーボードドライバ⾃体は後の章で作成します。
*3 マシンには複数のシリアルポートが搭載されていたりするのですが、これは 1 つ⽬のシリアルポートのアドレスです。複数のシリアルポートを使う予定も無いので本書では常に 1 つ⽬のシリアルポートを使います。

– 37 –

第3章

シリアルドライバを作る▼表 3.1: THR についてIO アドレス
レジスタサイズ

0x03f81 バイト♣ IO アドレス空間のレジスタへデータを書き込む「out」(命令 18)
それでは、out 命令を使って、⽂字をシリアル通信で送ってみます。

1 バイトのデータ送信を⾏う際の out 命令の構⽂は以下の通りです。
▼表 3.2: 構⽂ 26: out: AL を DX が指すアドレス先へ書き込む
アセンブラ
機械語

out %al, %dxee

使⽤できるレジスタは固定です。今回の場合、「AL」に送信したい⽂字の ASCII コー
ドを格納し、「DX」へ THR の IO アドレス (0x03f8) を格納した状態で out 命令を実施することで、データ送信を⾏えます。

8 ビットレジスタと 16 ビットレジスタへそれぞれのビット幅の即値を格納する構⽂は 2 章で紹介しました。
再掲すると以下の通りです。
▼表 3.3: 構⽂ 4: mov: 即値 (8bit) をレジスタ (8bit) へ格納 (再掲)
アセンブラ
機械語

mov 即値 (8bit), レジスタ (8bit)
b0+reg_ofs 即値 (8bit)▼表 3.4: 構⽂ 5: mov: 即値 (16bit) をレジスタ (16bit) へ格納 (再掲)
アセンブラ
機械語

mov 即値 (16bit), レジスタ (16bit)
66 b9+reg_ofs 即値 (16bit)

AL レジスタの reg_ofs は「0」で、DX レジスタの reg_ofs は「2」なので、AL レジ
スタと DX レジスタへの値格納までを sample.txt へ記述するとリスト 3.1 の通りです。

– 38 –

3.2 コントローラの状態を確認して送信する▼リスト 3.1: 031_ser_tx/sample.txt# ASCII⽂字'A'(0x41)をシリアルポートへ送信
b0 41# mov $0x41,%al
66 ba f8 03# mov $0x03f8,%dx

ここでは⽂字 A(0x41) を送信してみることにしました。
そして、out 命令を使い、AL に格納した⽂字を DX が指す THR へ書き込んでみま
す (リスト 3.2)。
▼リスト 3.2: 031_ser_tx/sample.txt# ASCII⽂字'A'(0x41)をシリアルポートへ送信
b0 41# mov $0x41,%al
66 ba f8 03# mov $0x03f8,%dx
ee# out %al,%dx(追加)# 無限Halt
f4
eb fd

# hlt
# jmp -1

(追加)
(追加)
(追加)

out 命令の後に hlt を使った無限ループを設置しました。

♣ 動作確認kernel.bin を⽣成し、run_qemu スクリプトで実⾏してみてください。
シリアル送信した⽂字は、QEMU を GUI で起動した場合「Ctrl-Alt-3」キーでシリ
アル通信⽤の画⾯へ切り替えることで確認できます。CUI で起動した場合、標準でシリアル通信画⾯になっているのでそのまま表⽰されます。
⽂字「A」が表⽰されれば成功です。
$ run_qemu fs -nographicA

3.2 コントローラの状態を確認して送信する
前節では、コントローラの状態確認無しで THR へ書き込みを⾏っていました。THR
はコントローラが持つ送信バッファです。コントローラの側で送信バッファにデータを受け⼊れる準備ができていない時にこのバッファへ書き込みを⾏ってしまうと、最悪の場合、未送信のデータを上書きして消してしまう恐れがあります。
シリアル通信のコントローラには「送信バッファが空になった」事を⽰すフラグがあるため、ここではこのフラグを確認してから THR へ書き込むように変更してみます。

– 39 –

第3章

シリアルドライバを作る

この節のサンプルディレクトリは「032_ser_tx_chk_flg」です。

♣ Line Status Register(LSR)
コントローラのステータスは「Line Status Register(LSR)」というレジスタで確認できます。
▼表 3.5: LSR についてIO アドレス
レジスタサイズ

0x03fd1 バイト

LSR は各ビットがコントローラのステータスを⽰しており、各ビットの意味は以下の通りです。
▼表 3.6: LSR の各ビットについて
ビット

意味

7
6
5
4
3
2
1
0

受信バッファに何らかのエラーが発⽣
送信バッファが空で内部の送信処理も全て完了
送信バッファが空 (データ受け⼊れ可能)
ブレークシグナルを受信
フレームエラーパリティエラー
オーバーランエラー
受信バッファに 1 つ以上のデータがある

使うのはビット 5 です。このビットがセットされていれば送信バッファへデータ書き込みを⾏って問題ありません。

♣ IO アドレス空間のレジスタ値取得「in」(命令 19)
それでは、LSR の値を読み出す処理を追加してみます。
IO アドレス空間上のレジスタの値を取得する命令は「in」命令で、構⽂は表 3.7 の通りです。
試しに、単体のソースコード「read_lsr.txt」を作り、LSR の値を読み出してみます(リスト 3.3)。

– 40 –

3.2 コントローラの状態を確認して送信する▼表 3.7: 構⽂ 27: in: DX が指すレジスタ値を AL へ格納
アセンブラ
機械語

in %dx, %alec▼リスト 3.3: 032_ser_tx_chk_flg/read_lsr.txt# シリアルポートのステータス取得
66 ba fd 03# mov $0x03fd,%dxec# in %dx,%al# 無限Halt
f4
eb fd

# hlt
# jmp -1

リスト 3.3 を実⾏し、QEMU モニターでレジスタ値を確認すると以下の通りでした。
(qemu) info registers
RAX=0000000000110060 RBX=00000000bef67f98 RCX=0000000000000000 RDX=00000000000››003fd
RSI=0000000000407180 RDI=00000000bfe9b018 RBP=00000000bff0eb10 RSP=00000000002››10000
R8 =00000000bff0e9dc R9 =0000000000000078 R10=00000000bfe6e080 R11=00000000e7d››c4657

RAX レジスタの最下位 1 バイト (AL レジスタ) が 0x60 になっています。ビット 6とビット 5 がセットされているので、このステータス値なら、送信して問題無さそうです。

♣ フラグの待ち⽅「and」/「je」(命令 20)
新たに 2 つの命令を使ってビットがセットされるまで待つ処理を実装します。流れとしては以下の 2 ステップです。

1. and 命令: AL(LSR の値) と 0x20 で AND を取る (AL のビット 5 以外を全て 0にする)
2. je 命令: 1. の演算結果が 0 の場合、in 命令まで戻る (再度 LSR 取得)
新たに登場した「je」命令は、直前の演算結果に応じてジャンプするか否かが決まる「条件付きジャンプ命令」の 1 つです。CPU では、数値の演算を⾏うと、その結果に応じて CPU 内の「フラグレジスタ」と呼ばれるレジスタの各ビットが変化します。条件付きジャンプ命令は、このフラグレジスタの状態に応じてジャンプするか否かが決まる命令です。

– 41 –

第3章

シリアルドライバを作る

je は「Jump if Equal」の略です。関係するのはフラグレジスタの中の「ゼロフラグ」のビットで、je 命令の実⾏時、このビットがセットされていればオペランドで⽰した相対アドレスへジャンプし、ビットがセットされていなければ、ジャンプはせずに次の命
令へ進みます。
上記の 2 ステップの場合、ステップ 1 の AND によって、LSR のビット 5 がセットされていた場合のみステップ 2 で次の命令へ進むようになり、LSR のビット 5 がセットされていなければ LSR 値を取得する in 命令まで戻ることになります。

je 命令の構⽂は jmp 命令と同じで以下の通りです。
▼表 3.8: 構⽂ 28: je: ゼロフラグがセットされていればジャンプ
アセンブラ
機械語

je <ラベルあるいは".">74 fe+rel_addr♣ フラグレジスタの動きを⾒てみる
ここで、フラグレジスタの動きを⾒てみます。
例えば、al に 0 を格納し、0x20 と and を取るだけのコードを作ってみます (リスト

3.4)。
▼リスト 3.4: 032_ser_tx_chk_flg/zf.txt# 0x00と0x20のANDでゼロフラグを⽴てる
b0 00# mov $0x00,%al
24 20# and $0x20,%al# 無限Halt
f4
# hlt
eb fd
# jmp -1

実⾏し、QEMU モニターでレジスタ値を表⽰させてみます。
(qemu) info registers
RAX=0000000000110000 RBX=00000000bef67f98 RCX=0000000000000000 RDX=00000000000››00000
RSI=0000000000407180 RDI=00000000bfe9b018 RBP=00000000bff0eb10 RSP=00000000002››10000
R8 =00000000bff0e9dc R9 =0000000000000078 R10=00000000bfe6e080 R11=00000000e7d››c4657R12=00000000bef69540 R13=00000000bef69548 R14=00000000bff24b60 R15=00000000bef››6a018RIP=0000000000110005 RFL=00000046 [---Z-P-] CPL=0 II=0 A20=1 SMM=0 HLT=1– 42 –

3.2 コントローラの状態を確認して送信する「RFL=」に書かれているのがフラグレジスタの値です。すぐ右側の"[]"の中にどんなフラグが設定されているかが現れていて、"Z"が「ゼロフラグ」の設定を⽰しています。
ゼロフラグは演算結果がゼロだった場合に設定されるフラグです。今回の場合、0x00と 0x20 の AND は 0x00 になるため、このフラグが設定されました。
この状態で「je」命令を実⾏するとオペランドで指定した相対アドレスへジャンプします。
フラグレジスタの算術演算に関わるビット (下位 8 ビット) の⼀覧を紹介しておきま
す (表 3.9)。
▼表 3.9: フラグレジスタ⼀覧
ビット

意味

7
6
5
4
3
2
1
0

符号フラグ。演算結果がマイナスの場合にセットされる
ゼロフラグ。演算結果がゼロの場合セットされる
予約
調整フラグ。2 進化 10 進 (BCD) でのキャリー・ボローでセットされる
予約
パリティフラグ。演算結果最下位 8 ビットの 1 の数が偶数の場合セット予約
キャリーフラグ。キャリー・ボローでセットされる

フラグレジスタ⾃体は 64 ビット CPU の場合、64 ビットの⼤きさがあります。ここでは算術演算に関わるもののみ紹介しました。今後使⽤するビットは登場した際に改めて紹介します*4 。

♣ LSR のビット 5 を待つ処理を実装
以上をまとめると、前節の機械語コードにフラグ確認の処理を追加するとリスト 3.5のようになります。
▼リスト 3.5: 032_ser_tx_chk_flg/sample.txt# 追加(ここから)# シリアルポートのステータス取得
00: 66 ba fd 03 # mov $0x03fd,%dx
04: ec# in %dx,%al*4 フラグレジスタについて詳しくは SDM の Volume 1 か、Wikibooks の記事が分かりやすいです。共に本書末尾の「参考情報」で紹介しています。

– 43 –

第3章

シリアルドライバを作る# 送信バッファが空でなければステータス取得まで戻る
05: 24 20# and $0x20,%al
07: 74 fb# je -3# 追加(ここまで)# ASCII⽂字'A'(0x41)をシリアルポートへ送信
09: b0 41# mov $0x41,%al
0b: 66 ba f8 03 # mov $0x03f8,%dx
0f: ee# out %al,%dx# 無限Halt
10: f411: eb fd# hlt# jmp -1

実⾏結果は前節と変わりませんが、これでポーリングによるシリアル送信をちゃんと実装できました。

3.3 受信も追加しエコーバックプログラムを作る
この章の最後に、シリアルポートからの受信データの取得を⾏ってみます。そして、受信した⽂字をそのままシリアルポートへ送信することで、エコーバック処理*5 を実装してみます。
この節のサンプルディレクトリは「033_echoback」です。

♣ 受 信 バ ッ フ ァ に デ ー タ が 来 る の を 待 つ (命 令 21「jne」/命 令 22
「pause」)
まずは、受信バッファにデータが来るのを待ちます。
シリアルポートの受信バッファにデータがあるか否かもステータスレジスタ LSR で確認できます。受信バッファに 1 つ以上のデータがあれば、LSR のビット 0 がセットされます (表 3.10)。

sample.txt に受信バッファに 1 つ以上のデータが来るまで待つ処理を追加するとリスト 3.6 の通りです。

*5 ⼊⼒したキーに対応する⽂字がそのまま画⾯へ表⽰される処理。シリアルドライバの送受信の動作確認としてよく使われます。

– 44 –

3.3 受信も追加しエコーバックプログラムを作る▼表 3.10: LSR の各ビットについて (再掲)
ビット番号

意味

ビット 7

受信バッファに何らかのエラーが発⽣

ビット 6

送信バッファが空で内部の送信処理も全て完了

ビット 5

送信バッファが空 (データ受け⼊れ可能)

ビット 4

ブレークシグナルを受信

ビット 3

フレームエラー

ビット 2

パリティエラー

ビット 1

オーバーランエラー

ビット 0

受信バッファに 1 つ以上のデータがある▼リスト 3.6: 033_echoback/sample.txt# 追加(ここから)# シリアルポートのステータス取得
00: 66 ba fd 03 # mov $0x03fd,%dx
04: ec# in %dx,%al# 受信バッファが空であればpauseしステータス取得まで戻る
05: 24 01# and $0x01,%al
07: 75 04# jne +6
09: f3 90# pause0b: eb f7# jmp -7
# 追加(ここまで)# シリアルポートのステータス取得
14: 66 ba fd 03 # mov $0x03fd,%dx
18: ec# in %dx,%al# 送信バッファが空でなければステータス取得まで戻る# ・・・ 省略 ・・・「シリアルポートのステータス取得」のコードブロックはシリアル送信の際と同じものです。
「受信バッファが空であれば pause しステータス取得まで戻る」のコードブロックは、
これまでと少し異なります。
まず、新たに登場した「jne」命令は、「Jump if Not Equal」の略です。je 命令の逆で、「ゼロフラグがセットされていなければジャンプ」という挙動になります。
構⽂も je 命令等と同様で表 3.11 の通りです。

– 45 –

第3章

シリアルドライバを作る▼表 3.11: 構⽂ 29: jne: ゼロフラグがセットされていなければジャンプ
アセンブラ
機械語

jne <ラベルあるいは".">75 fe+rel_addr

そして、AL へ格納したステータス値と 0x01 で AND の結果に対して、以下の振る舞いを実装しています。

• AND の結果が 0x01 (ゼロフラグが 0)◦ jne 命令で 6 バイト先 (このコードブロックの直後) へジャンプ◦ 意図: 受信バッファにデータ来たのでループを抜ける• AND の結果が 0x00 (ゼロフラグが 1)◦ jne 命令でジャンプせず、次の命令へ進む◦「pause」命令を実⾏後、jmp 命令で 7 バイト前 (LSR 取得) へ戻る◦ 意図: 受信バッファにデータ無いので、pause して再度ステータス取得
ここでは、「ビジーループで待つの際は pause 命令を挟む」という事をするためにこのようなロジックにしています。
「pause」命令は、「ビジーループしている」事を CPU へヒントとして伝える命令で、ビジーループにこの命令を挟んでおくことで CPU の実⾏効率を改善する効果があります。(詳しくは後述のコラムにまとめています。)

pause 命令はオペランドが不要で、構⽂としては表 3.12 の通りです。
▼表 3.12: 構⽂ 30: pause: ビジーループのヒントを CPU へ伝える
アセンブラ
機械語

pause
f3 90

併せて、送信バッファが空になるのを待つ処理にも、pause を挟んでおくことにします (リスト 3.7)。
▼リスト 3.7: 033_echoback/sample.txt# ・・・省略・・・# 受信バッファのデータを取得
0d: 66 ba f8 03 # mov $0x03f8,%dx
11: ec# in %dx,%al
12: 88 c3# mov %al,%bl– 46 –

3.3 受信も追加しエコーバックプログラムを作る# シリアルポートのステータス取得
14: 66 ba fd 03 # mov $0x03fd,%dx
18: ec# in %dx,%al# 変更(ここから)# 送信バッファが空でなければpauseしステータス取得まで戻る
19: 24 20# and $0x20,%al
1b: 75 04# jne +6
1d: f3 90# pause
1f: eb f7# jmp -7
# 変更(ここまで)# ・・・省略・・・【コラム】メモリ順序違反と pause 命令
フラグ設定待ちのようなビジーループに pause 命令を⼊れておくと CPU の実⾏
効率低下を防ぐことができるのは、「メモリ順序違反 (memory order violation)」を防ぐことができるためです。
インテル等の CPU には「投機的実⾏」と呼ばれる機能があり、現在実⾏している命令よりも先の命令を CPU 内のバッファへ予め読み込み、場合によっては先に実⾏しておくことで、CPU の性能最適化を図っています。
ただ、条件付きジャンプ命令等、命令を評価し終えるまでどこへ分岐するか分からない命令があると、その先をどのように先読みするかが問題になります。
この時、インテル等の CPU では「過去にどちらのコードパスを多く通ったか」等の統計値を元に、分岐先を推測して先読みを続⾏します。
しかし、今回のように CPU としてはほとんどの時間を「フラグ設定待ち」で過ごしていると、命令先読みのバッファは「フラグ設定待ちの処理」の先読みで埋め尽くされ、それらの処理が予め実⾏される形で最適化が⾏われます。これは、「次もきっとフラグ待ちだろう」という推測で最適化されている状況です。
そのため、いざフラグが設定された時に、推測が外れ、先読みしたバッファの内
容や予め実⾏した処理もムダになり、実⾏効率が低下します。これを「メモリ順序違反」と呼びます。

pause 命令があると CPU がこのような誤った先読みをしなくなり、メモリ順序違反を防ぐことができます。
詳しくは、インテルの CPU マニュアルである「Intel 64 and IA-32 Architectures

Software Developer’s Manual(SDM)」の「Volume 2」の「4.3 INSTRUCTIONS– 47 –

第3章

シリアルドライバを作る(M-U)」にある pause 命令の解説をご覧ください。(本書末尾の「参考情報」にPDF が公開されているページの URL を載せています。)♣ 受信バッファのデータを取得する
次に受信バッファのデータを取得します。受信バッファのレジスタは「RBR(ReceiverBuﬀer Register)」です。
▼表 3.13: RBR についてIO アドレス
レジスタサイズ

0x03f81 バイト

RBR の IO アドレスは THR と同じです。同⼀のアドレスに対して、in 命令による読み出す時は「RBR」へ、out 命令による書き込み時は「THR」へ、となるように内部の回路が組まれています。

RBR からデータを読み出す処理を追加するとリスト 3.8 の通りです。
▼リスト 3.8: 033_echoback/sample.txt# シリアルポートのステータス取得
00: 66 ba fd 03 # mov $0x03fd,%dx
04: ec# in %dx,%al# 受信バッファが空であればpauseしステータス取得まで戻る
05: 24 01# and $0x01,%al
07: 75 04# jne +6
09: f3 90# pause0b: eb f7# jmp -7
# 追加(ここから)# 受信バッファのデータを取得
0d: 66 ba f8 03 # mov $0x03f8,%dx
11: ec# in %dx,%al
12: 88 c3# mov %al,%bl# 追加(ここまで)# シリアルポートのステータス取得
14: 66 ba fd 03 # mov $0x03fd,%dx
18: ec# in %dx,%al– 48 –

3.3 受信も追加しエコーバックプログラムを作る# 送信バッファが空でなければpauseしステータス取得まで戻る# ・・・ 省略 ・・・

in 命令の箇所では、受信バッファから AL レジスタへ受信データを格納しています。
AL レジスタはその他の in/out 命令の箇所でも使⽤するため、AL へ格納したデータはmov 命令で BL レジスタへコピーしています。

♣ mov 命令の新構⽂ (8 ビットレジスタから 8 ビットレジスタへコピー)
ここで、8 ビットレジスタから 8 ビットレジスタへ値をコピーする mov 命令の構⽂を紹介します。
▼表 3.14: 構⽂ 31: mov: 第 1 から第 2 オペランドへコピー (共に 8 ビットレジスタ)
アセンブラ
機械語

mov レジスタ (8bit,src), レジスタ (8bit,dst)
88 c0+reg_src_dst「reg_src_dst」については表 3.15 の通りです。
▼表 3.15: 8 ビットレジスタの reg_src_dst ⼀覧
src/dst
AL
CL
DL
BL
AH
CH
DH
BH

AL
0x00
0x08
0x10
0x18
0x20
0x28
0x30
0x38

CL
0x01
0x09
0x11
0x19
0x21
0x29
0x31
0x39

DL
0x02
0x0a
0x12
0x1a
0x22
0x2a
0x32
0x3a

BL
0x03
0x0b
0x13
0x1b
0x23
0x2b
0x33
0x3b

AH
0x04
0x0c
0x14
0x1c
0x24
0x2c
0x34
0x3c

CH
0x05
0x0d
0x15
0x1d
0x25
0x2d
0x35
0x3d

DH
0x06
0x0e
0x16
0x1e
0x26
0x2e
0x36
0x3e

BH
0x07
0x0f
0x17
0x1f
0x27
0x2f
0x37
0x3f

2 章の imul 命令で紹介した表とは異なり、各⾏が「第 1 オペランド (src)」で、各列が「第 2 オペランド (dst)」となっている点に注意してください。

– 49 –

第3章

シリアルドライバを作る♣ 受信バッファから取得したデータを送信する
受信バッファからデータを取得できたので、今度はそれを送信してみます (リスト

3.9)。
▼リスト 3.9: 033_echoback/sample.txt# ・・・ 省略 ・・・# 送信バッファが空でなければpauseしステータス取得まで戻る
19: 24 20# and $0x20,%al
1b: 75 04# jne +6
1d: f3 90# pause
1f: eb f7# jmp -7
# 追加・変更(ここから)# 受信バッファから取得したデータを送信
21: 88 d8# mov %bl,%al
23: 66 ba f8 03 # mov $0x03f8,%dx
27: ee# out %al,%dx# 先頭へ戻る
28: eb d6# jmp -0x28# 追加・変更(ここまで)

特に新たな要素はありません。受信したデータは BL レジスタにあるため、それをAL へコピーし、DX レジスタへ送信バッファアドレスを設定の上、out 命令でシリアルポートへ送信しています。

♣ 動作確認
これまで通り、ビルドし、動作確認してみてください。
⼊⼒したキーに対応する⽂字がシリアルの画⾯へ表⽰されます。
ただ、改⾏を⼊⼒すると、次の⾏へ移らずに⾏頭へ戻ってしまいます。
$ ../tools/run_qemu ../fs -nographic...
hogeo world↑"hello world"と⼊⼒後、改⾏の後、"hoge"と⼊⼒したが、次の⾏へ移らない。

♣ CR を受け取ったら LF を追加で送信するようにする (命令 23「cmp」)
これは、シリアルポートで受信する改⾏⽂字が CR(Carriage Return、復帰コード)のみであるためです。次の⾏への改⾏を⾏う LF(Line Feed) を送信していないため、改– 50 –

3.3 受信も追加しエコーバックプログラムを作る

⾏キーを⼊⼒した際に、⾏頭に移動するのみで次の⾏へ移動しなかったのでした。
受信バッファから取得したデータを送信後、受信したデータが CR(0x0d) であった場合、LF(0x0a) を追加で送信するようにします (リスト 3.10)。
▼リスト 3.10: 033_echoback/sample.txt# ・・・ 省略 ・・・# シリアルポートのステータス取得(*1)
14: 66 ba fd 03 # mov $0x03fd,%dx
18: ec# in %dx,%al# 送信バッファが空でなければpauseしステータス取得まで戻る
19: 24 20# and $0x20,%al
1b: 75 04# jne +6
1d: f3 90# pause
1f: eb f7# jmp -7
# 受信バッファから取得したデータを送信
21: 88 d8# mov %bl,%al
23: 66 ba f8 03 # mov $0x03f8,%dx
27: ee# out %al,%dx# 追加・変更(ここから)# 送信したデータがCR(0x0d)でなければ先頭まで戻る
28: 3c 0d# cmp $0x0d,%al
2a: 75 d4# jne -0x2a# LF(0x0a)を受信データとして(*1)まで戻る
2c: b3 0a# mov $0x0a,%bl
2e: eb e4# jmp -0x1a# 追加・変更(ここまで)

新たに「cmp」命令が登場しました。これはオペランドで指定した数値の⽐較を⾏う
命令です。挙動としては「第 2 オペランド (dst) を書き換えない sub 命令」です。sub
命令は第 2 オペランドから第 1 オペランド減算した結果を第 2 オペランドへ格納しま
すが、cmp 命令は減算まで⾏ってフラグレジスタのみ変化させた後、減算結果は捨てます。
構⽂としては、cmp 命令も sub 命令同様、レジスタとして AL が対象の場合は 1 バ
イト少ない構⽂があります (表 3.16)。

AL を含むその他のレジスタについては表 3.17 の通りです。

– 51 –

第3章

シリアルドライバを作る▼表 3.16: 構⽂ 32: cmp: 即値 (8bit) と AL を⽐較
アセンブラ
機械語

cmp 即値 (8bit), %al3c 即値 (8bit)▼表 3.17: 構⽂ 33: cmp: 即値 (8bit) とレジスタ (8bit) を⽐較
アセンブラ
機械語

cmp 即値 (8bit), レジスタ (8bit)
80 f8+reg_ofs 即値 (8bit)

追加した処理でやっていることとしては、送信処理後、cmp 命令を使⽤して、AL レ
ジスタに残っている送信済みの⽂字と 0x0d(CR) を⽐較し、等しくなかった場合は jne
命令で先頭まで戻り、等しかった場合は、以降の LF 送信処理へ進みます。

LF 送信処理は、BL へ 0x0a(LF) を格納し、送信のためのシリアルポートステータス
確認処理 (*1) までジャンプするだけです。

♣ 動作確認
この状態で動作確認してみると、改⾏まで反映されているはずです。
$ ../tools/run_qemu ../fs -nographic...
hello worldhoge– 52 –

第4 章 フレームバッファで画⾯描画「フレームバッファ」を使⽤してディスプレイへの描画を試してみます。ディスプレ
イの各ピクセルには、メモリ空間上に対応する領域があり、それが「フレームバッファ」と呼ばれる領域です。

poiboot では、kernel.bin へジャンプする際、UEFI で取得したフレームバッファ情
報を渡すようにしています。この章では poiboot から渡されたフレームバッファ情報を取得し、取得したフレームバッファ情報を使⽤して、画⾯へドットを描画したり、塗りつぶしたりしてみます。
フレームバッファの各ピクセルは「B(⻘)」・「G(緑)」
・「R(⾚)」
・「Reserved(予約領域)」の順に各 1 バイトずつ計 4 バイトで構成されています。フレームバッファ領域の最も若いアドレスが画⾯左上のピクセルに対応します。

4.1 フレームバッファのアドレスを取得する
まずは、フレームバッファ領域の先頭アドレスを取得してみます。
この節のサンプルディレクトリは「041_get_fb_addr」です。

♣ フレームバッファに関する情報の在処
フレームバッファに関する情報は、ブートローダー (poiboot) から渡されます。

poiboot では、フレームバッファに関する情報を「struct fb(リスト 4.1)」という構造でメモリ上に配置しています。そして、kernel.bin を実⾏する際、その先頭アドレス
を RSI レジスタ*1 へ格納した上で、kernel.bin へジャンプします。

*1 RSI レジスタは、関数の第 2 引数として使われるレジスタです。そのため、普通に C ⾔語などでカーネルを書く際は、kernel.bin の⼀番先頭の関数の第 2 引数で struct fb の先頭アドレスを取得できます。

– 53 –

第4章

フレームバッファで画⾯描画▼リスト 4.1: struct fb の構造
struct fb {/* フレームバッファの先頭アドレス */unsigned long long base;/* フレームバッファサイズ[バイト] */unsigned long long size;/* 画⾯の幅[ピクセル] */unsigned int hr;/* 画⾯の⾼さ[ピクセル] */unsigned int vr;};

リスト 4.1 のコメントに記載の通り、フレームバッファの先頭アドレスは、先頭のメンバー変数「base」に格納されています。
「先頭のメンバー変数のアドレス」は「構造体の先頭アドレス」と等しいので、RSI に格納されているアドレス (struct fb の先頭アドレス) は、メンバー変数 base のアドレスでもあります。そのため、RSI が指す先に「フレームバッファの先頭アドレス」があります。

♣ レジスタ間接アドレッシング「あるアドレスが指す先」のメモリを参照したい場合、例えば C ⾔語では「ポインタ」
がありますが、機械語 (およびアセンブラ) では「レジスタ間接アドレッシング」(あるいは単に「レジスタ間接」) というオペランド指定⽅法があります。
これは、命令のオペランドとして「レジスタに格納されたアドレスの指す先」を指定するものです。アセンブラでは「(% レジスタ名)」の様に書きます。ここでは、RSIに格納されたアドレス先のメモリ上のデータ (フレームバッファ先頭アドレス) を、64ビットレジスタ RAX へコピーしてみます。対応する mov 命令は表 4.1 の通りです。
▼表 4.1: mov 命令: RSI に格納されたアドレス先の内容を RAX へコピー
アセンブラ
機械語

mov (%rsi),%rax
48 8b 06

レジスタ間接で取得した値を 64 ビットレジスタへ格納する構⽂は表 4.2 の通りで、構⽂内の「reg_dst_src」は表 4.3 の通りです。
表 4.2 の備考について、レジスタ間接に使⽤するレジスタが RSP あるいは RBP の場合、機械語のパターンが少し変わります。例としては表 4.4 と表 4.5 の通りです。
なお、⼀部の命令では、レジスタ間接に 64 ビット幅以外のレジスタを使う命令もあったりしますが、本書で使わないため省略します。

– 54 –

4.1 フレームバッファのアドレスを取得する▼表 4.2: 構⽂ 34: mov: 第 1 オペランドの指す先を第 2 オペランドへ格納
アセンブラ
機械語
備考

mov (レジスタ (64bit)), レジスタ (64bit)
48 8b 00+reg_dst_src
レジスタ間接が RSP の場合、末尾に 0x24 追加
レジスタ間接が RBP の場合、3 バイト⽬に 0x40 加算、末尾に 0x00 追加▼表 4.3: 64 ビットレジスタの reg_dst_src
dst/(src)
RAX
RCX
RDX
RBX
RSP
RBP
RSI
RDI

(RAX)
0x00
0x08
0x10
0x18
0x20
0x28
0x30
0x38

(RCX)
0x01
0x09
0x11
0x19
0x21
0x29
0x31
0x39

(RDX)
0x02
0x0a
0x12
0x1a
0x22
0x2a
0x32
0x3a

(RBX)
0x03
0x0b
0x13
0x1b
0x23
0x2b
0x33
0x3b

(RSP)
0x04
0x0c
0x14
0x1c
0x24
0x2c
0x34
0x3c

(RBP)
0x05
0x0d
0x15
0x1d
0x25
0x2d
0x35
0x3d

▼表 4.4: 例: レジスタ間接のレジスタが RSP の場合
アセンブラ
機械語

mov (%rsp), %rax
48 8b 04 24▼表 4.5: 例: レジスタ間接のレジスタが RBP の場合
アセンブラ
機械語

mov (%rbp), %rax
48 8b 45 00♣ 実装
サンプルコードはリスト 4.2 の通りです。
▼リスト 4.2: 041_get_fb_addr/sample.txt# フレームバッファ先頭アドレスをRAXへ格納
00: 48 8b 06# mov (%rsi),%rax– 55 –

(RSI)
0x06
0x0e
0x16
0x1e
0x26
0x2e
0x36
0x3e

(RDI)
0x07
0x0f
0x17
0x1f
0x27
0x2f
0x37
0x3f

第4章

フレームバッファで画⾯描画# 無限Halt
03: f404: eb fd# hlt# jmp -1

フレームバッファの先頭アドレスを RAX へコピーした後、halt の無限ループで待機するようにしました。

♣ 動作確認QEMU で動作させて、QEMU モニターで RAX を⾒てみると、フレームバッファの先頭アドレスが格納されています。
(qemu) info registers
RAX=00000000c0000000 RBX=00000000bef67f98 RCX=0000000000000000 RDX=00000000000››00000
RSI=0000000000407180 RDI=00000000bfe9b018 RBP=00000000bff0eb10 RSP=00000000002››10000
R8 =00000000bff0e9dc R9 =0000000000000078 R10=00000000bfe6e080 R11=00000000e7d››c4657R12=00000000bef69540 R13=00000000bef69548 R14=00000000bff24b60 R15=00000000bef››6a018RIP=0000000000110004 RFL=00000002 [-------] CPL=0 II=0 A20=1 SMM=0 HLT=1

この場合、フレームバッファの先頭アドレスは「0xc0000000」であったと分かります。

4.2 画⾯の幅と⾼さを取得する
続いて、画⾯の幅と⾼さを取得します。
この節のサンプルディレクトリは「042_get_width_height」です。

♣ ディスプレースメント付きレジスタ間接アドレッシング
画⾯の幅と⾼さも struct fb の構造の中にあります。それぞれ、幅 (hr) は struct fbの先頭から 16(0x10) バイト⽬、⾼さ (vr) は 20(0x14) バイト⽬です。
これらのデータを参照する際、アドレスを格納したレジスタへオフセット分の値を都度⾜したり引いたりしても参照できるのですが、少し⾯倒です。
レジスタ間接アドレッシングには「ディスプレースメント (dsp)」と呼ばれるオフ
セット値を指定できるため、これを使うと「あるアドレスから N バイト先のデータ」へ

1 命令でアクセスできます。
ディスプレースメント付きの間接アドレスは、アセンブラでは「dsp(レジスタ– 56 –

4.2 画⾯の幅と⾼さを取得する名)」の様に書きます。「RSI に格納されたアドレスの 16(0x10) バイト先」であれ
ば「0x10(%rsi)」、「RSI に 格 納 さ れ た ア ド レ ス の 20(0x14) バ イ ト 先 」で あ れ ば「0x14(%rsi)」です。
幅 (hr) と⾼さ (vr) はそれぞれ 32 ビットです。ここでは、幅の値を 32 ビットレジス
タ EBX へ、⾼さの値を 32 ビットレジスタ ECX へコピーすることにします。それぞれのアセンブラと機械語コードは表 4.6 と表 4.7 の通りです。
▼表 4.6: mov 命令: 幅 (RSI 格納アドレスの 0x10 バイト先) を EBX へコピー
アセンブラ
機械語コードmov 0x10(%rsi),%ebx
8b 5e 10

▼表 4.7: mov 命令: ⾼さ (RSI 格納アドレスの 0x14 バイト先) を ECX へコピー
アセンブラ
機械語コードmov 0x14(%rsi),%ecx
8b 4e 14

それぞれ、末尾の 1 バイトがディスプレースメントです。

8 ビットのディスプレースメント付きレジスタ間接の値を 32 ビットレジスタへ格納
する構⽂は表 4.8 の通りで、構⽂内の「reg_dst_src」は表 4.9 の通りです。
▼表 4.8: 構⽂ 35: mov: DSP(8bit) 付きレジスタ間接の値をレジスタ (32bit) へ格納
アセンブラ
機械語
備考

mov dsp(レジスタ (64bit)), レジスタ (32bit)
8b 40+reg_dst_src dsp(8bit)
レジスタ間接が RSP の場合、3 バイト⽬に 0x24 追加

表 4.8 の備考に記載の通り、レジスタ間接のレジスタが RSP の場合、機械語のパターンが変わります。例としては表 4.10 の通りです。

– 57 –

第4章

フレームバッファで画⾯描画▼表 4.9: レジスタ間接とレジスタ (32bit) のオフセット値
dst/(src)
EAX
ECX
EDX
EBX
ESP
EBP
ESI
EDI

(RAX)
0x00
0x08
0x10
0x18
0x20
0x28
0x30
0x38

(RCX)
0x01
0x09
0x11
0x19
0x21
0x29
0x31
0x39

(RDX)
0x02
0x0a
0x12
0x1a
0x22
0x2a
0x32
0x3a

(RBX)
0x03
0x0b
0x13
0x1b
0x23
0x2b
0x33
0x3b

(RSP)
0x04
0x0c
0x14
0x1c
0x24
0x2c
0x34
0x3c

(RBP)
0x05
0x0d
0x15
0x1d
0x25
0x2d
0x35
0x3d

(RSI)
0x06
0x0e
0x16
0x1e
0x26
0x2e
0x36
0x3e

(RDI)
0x07
0x0f
0x17
0x1f
0x27
0x2f
0x37
0x3f

▼表 4.10: 例: レジスタ間接のレジスタが RSP の場合
アセンブラ
機械語

mov 0x12(%rsp), %rax
8b 44 24 12♣ 備考: DSP の有無による機械語の変化
こ こ で 、構 ⽂ 33 で 紹 介 し た DSP 無 し の 構 ⽂ で は 、レ ジ ス タ を ⽰ す バ イ ト は「00+reg_dst_src」という構⽂でした。今回の場合と⽐較すると、レジスタを⽰す
バイトに 0x40 を⾜すと「DSP 付き」の意味になり、末尾に追加した 1 バイトが DSPとして扱われるようです。
構⽂ 34 より、「0x12」の DSP 付きで RAX のレジスタ間接で取得した値を EAX へ格納する機械語は「8b 40 12」です。
試しに、2 バイト⽬から 0x40 を引き、3 バイト⽬の DSP を除いた「8b 00」を
das_dump で逆アセンブルしてみます。
$ das_dump 8b 00/tmp/tmp.pYcO1hU5Ag/a.bin:

ファイル形式 binary

セクション .data の逆アセンブル:

0000000000000000 <.data>:
0:
8b 00

mov

(%rax),%eax「8b 00」は DSP 無しに対応する命令だと分かりました。これより、「レジスタ間接– 58 –

4.2 画⾯の幅と⾼さを取得するの対象レジスタを⽰すバイトに 0x40 を⾜すと DSP 付きを⽰し、末尾の 1 バイトがDSP になる」と考えて良さそうです。
DSP 無しの構⽂ 33 の備考を改めて⾒てみると、『レジスタ間接のレジスタ (src) がRBP の場合、3 バイト⽬に 0x40 を⾜し、末尾に「00」を追加』とありました。これは、RBP には DSP 無しの機械語が無いので、DSP 無しのアセンブラからアセンブルすると、DSP を 0 とした機械語が⽣成されていたのですね。

♣ 実装
以上を踏まえて、前節の sample.txt に幅と⾼さの取得を追加してみます (リスト

4.3)。
▼リスト 4.3: 042_get_width_height/sample.txt# フレームバッファ先頭アドレスをRAXへ格納
00: 48 8b 06# mov (%rsi),%rax# 追加(ここから)# 画⾯の幅をEBXへ、⾼さをECXへ格納
03: 8b 5e 10# mov 0x10(%rsi),%ebx
06: 8b 4e 14# mov 0x14(%rsi),%ecx# 追加(ここまで)# 無限Halt
09: f40a: eb fd# hlt# jmp -1

♣ 動作確認QEMU 上で実⾏し、QEMU モニターでレジスタ値を確認すると、EBX と ECX にそれぞれ、フレームバッファの幅と⾼さが格納されています。
(qemu) info registers
RAX=00000000c0000000 RBX=0000000000000320 RCX=0000000000000258 RDX=00000000000››00000
RSI=0000000000407180 RDI=00000000bfe9b018 RBP=00000000bff0eb10 RSP=00000000002››10000
R8 =00000000bff0e9dc R9 =0000000000000078 R10=00000000bfe6e080 R11=00000000e7d››c4657R12=00000000bef69540 R13=00000000bef69548 R14=00000000bff24b60 R15=00000000bef››6a018RIP=000000000011000a RFL=00000002 [-------] CPL=0 II=0 A20=1 SMM=0 HLT=1

EBX に格納されている「0x320」は 10 進数で「800」、ECX に格納されている「0x258」– 59 –

第4章

フレームバッファで画⾯描画

は 10 進数で「600」です。QEMU のフレームバッファ解像度はデフォルトで 800x600なので、正常に取得できているとわかります。

4.3 画⾯へ 1 ピクセル描画してみる
前節までで、フレームバッファで画⾯描画を⾏うために必要な情報は揃いました。この節からはそれらを使って画⾯描画を⾏ってみます。まずは 1 ピクセルだけ画⾯に描画してみます。
この節のサンプルディレクトリは「043_draw_pixel」です。

♣ 即値をレジスタ間接で書き込む
前節までで確認したレジスタ間接による⽅法で、メモリ上にマップされたフレーム
バッファの領域へ書き込みを⾏います。
フレームバッファのピクセルフォーマットは「⻘ (B), 緑 (G), ⾚ (R), 予約 (RSV)」が各 1 バイトずつで、1 ピクセル当たり 4 バイトです。フレームバッファには画⾯左上から順にピクセルが並んでいるため、フレームバッファ先頭の 4 バイトに書き込みを⾏うと画⾯左上の 1 ピクセルを操作できます。
ここではフレームバッファ先頭の 4 バイトに即値で「B=0x00, G=0xﬀ, R=0x00,

RSV=0x00」を書き込んで、画⾯左上の 1 ピクセルを緑で塗ってみます。対応する機械
語コードは表 4.11 の通りです。
▼表 4.11: mov 命令: 画⾯左上 (0,0) を緑 (0x00,0xff,0x00,0x00) で塗る
アセンブラ
機械語コードmovl $0x0000ﬀ00,(%rax)
c7 00 00 ﬀ 00 00

即値をレジスタ間接で書き込むの場合、アセンブラ上では何ビットの書き込みかが分からない*2 ため、書き込みのビット幅を明⽰する必要があります。
ここでは、32 ビットアクセスであることを明⽰するため mov に接尾辞として"l"*3 を付けています。
*2 アセンブラでは即値で「0x0000ﬀ00」と書いていても操作するビット幅に応じて上の位のビットを良

しなに 0(あるいは符号ビット) で補完します。そのため、「0x0000ﬀ00(計 32 ビット)」と書いているからといって、「32 ビットの即値書き込み」の意思がアセンブラへ伝わるわけではないです。「mov$0x0000ﬀ00,(%rax)」でアセンブルしようとすると「サイズが分からない」といったエラーが出ます。
*3 他にも、8 ビットアクセスは"b"、16 ビットアクセスは"w"、64 ビットアクセスは"q"といった接尾辞があります。

– 60 –

4.3 画⾯へ 1 ピクセル描画してみる

機械語コードの末尾 4 バイトが書き込む即値データです。
構⽂をまとめると以下の通りです。
▼表 4.12: 構⽂ 36: mov: 即値 (32bit) をレジスタ間接で書き込む
アセンブラ
機械語
備考

movl 即値 (32bit), (レジスタ (64bit))
c7 00+reg_ofs 即値 (32bit)
レジスタ間接が RSP の場合、3 バイト⽬に 0x24 追加
レジスタ間接が RBP の場合、2 バイト⽬に 0x40 加算、3 バイト⽬に 0x00 追加♣ 実装
前節までの sample.txt へ、画⾯左上 (0,0) のピクセルを緑で塗りつぶす処理を追加してみます (リスト 4.4)。
▼リスト 4.4: 043_draw_pixel/sample.txt# フレームバッファ先頭アドレスをRAXへ# 画⾯の幅をEBXへ、⾼さをECXへ格納
00: 48 8b 06# mov (%rsi),%rax
03: 8b 5e 10# mov 0x10(%rsi),%ebx
06: 8b 4e 14# mov 0x14(%rsi),%ecx# 追加(ここから)# フレームバッファ先頭ピクセル(画⾯左上)を緑で塗る
09: c7 00 00 ff 00 00# movl $0x0000ff00,(%rax)# 追加(ここまで)# 無限Halt
0f: f410: eb fd# hlt# jmp -1

♣ 動作確認GUI モードで実⾏すると、たった 1 ピクセルなので⾒難いですが、⼀番左上の 1 ピ
クセルが緑で塗られていることを確認できます。
また、QEMU モニターでフレームバッファのデータをダンプすることでも確認できます。
(qemu) x/4bx 0xc0000000
00000000c0000000: 0x00 0xff 0x00 0x00

– 61 –

第4章

フレームバッファで画⾯描画

筆者の場合、フレームバッファのアドレスは 0xc0000000 だったので、そこから 1 バ
イトずつ、4 個分のデータを 16 進数でダンプしてみました。
「0x00(B) 0xﬀ(G) 0x00(R) 0x00(RSV)」と、意図通りにフレームバッファへ書き込みができていることを確認できます。

4.4 画⾯を塗りつぶしてみる
前節で画⾯の左上 (0,0) の 1 ピクセルを塗ることができました。フレームバッファ上の書き込み先を 1 ピクセル (4 バイト) ずつずらしながら、全ピクセル (幅 * ⾼さ) 分を書き込むことで、画⾯全体を単⾊で塗りつぶしてみます。
この節のサンプルディレクトリは「044_fill」です。

♣ 実装の流れjmp 命令を使うことでループを作ることはできるので「書き込み先を 4 バイトずつずらしながら、即値を書き込む」事はできそうです。
ただ、フレームバッファの領域を超えて書き込みを⾏ってしまうとフレームバッファ
外の領域のメモリ破壊につながります。そのため、レジスタ EDX を「書き込んだピク
セル数を管理するカウンタ」、レジスタ ESI を「画⾯の総ピクセル数 (幅 * ⾼さ)」とし、「EDX >= ESI」のときループを脱出するようにします。
ここで主に使⽤する命令が、数値⽐較を⾏う「cmp」命令と、「jbe」命令です。

♣ 等しいか⼩さければジャンプ (命令 24「jbe」)
jbe は「Jump if Below or Equal」の略です。既に登場した「je(Jump if Equal) 命令」等と同じく条件付きジャンプ命令の⼀種です。
対応するフラグレジスタはキャリーフラグとゼロフラグで、いずれかがセットされていた場合にジャンプします。
キャリーフラグは、算術演算の結果、レジスタサイズを超える桁上がり (キャリー) あ
るいはボロー (桁借り) が発⽣した場合にセットされます。今回の場合、キャリーフラ
グがセットされるのは「ESI - EDX が負となる場合」で、ゼロフラグがセットされるのは「ESI と EDX が等しい場合」なので、「EDX >= ESI」のときジャンプすることになります。

jbe 命令の構⽂は表 4.13 の通りです。

– 62 –

4.4 画⾯を塗りつぶしてみる▼表 4.13: 構⽂ 37: jbe: 等しいか⼩さければジャンプ
アセンブラ
機械語コードjbe <ラベルあるいは".">76 fe+rel_addr

末尾の 1 バイトがジャンプ先を決める相対アドレスで、jmp 命令等と同様、基準は0xfe です。

♣ 実装
前節までの sample.txt へ画⾯塗りつぶし処理を追加してみました (リスト 4.5)。
▼リスト 4.5: 044_fill/sample.txt# フレームバッファ先頭アドレスをRAXへ# 画⾯の幅をEBXへ、⾼さをECXへ格納
00: 48 8b 06# mov (%rsi),%rax
03: 8b 5e 10# mov 0x10(%rsi),%ebx
06: 8b 4e 14# mov 0x14(%rsi),%ecx# 追加・変更(ここから)# EDXを「カウンタ」、ESIを「画⾯ピクセル数(幅*⾼さ)」で初期化
09: 31 d2# xor %edx,%edx
0b: 89 ce# mov %ecx,%esi
0d: 0f af f3# imul %ebx,%esi# カウンタ(EDX)が画⾯ピクセル数(ESI)へ達したら「無限Halt」へジャンプ(*1)
10: 39 d6# cmp %edx,%esi
12: 76 0e# jbe +0x10# 現在のピクセルを緑で描画し次のピクセルへ移動
14: c7 00 00 ff 00 00# movl $0x0000ff00,(%rax)
1a: 48 83 c0 04# add $4,%rax# カウンタ(EDX)をインクリメントし(*1)へジャンプ
1e: ff c2# inc %edx
20: eb ee# jmp -0x10# 追加・変更(ここまで)# 無限Halt
22: f423: eb fd# hlt# jmp -1

初出の構⽂については後述しますが、やっていることとしてはコメントに記載のとおりです。

– 63 –

第4章

フレームバッファで画⾯描画気をつける点としては、1 ピクセルの描画を⾏っている以下の命令です。
▼リスト 4.6: 即値のバイトオーダーに注意# 現在のピクセルを緑で描画し次のピクセルへ移動
14: c7 00 00 ff 00 00# movl $0x0000ff00,(%rax)

機械語の末尾 4 バイトには、描画したいピクセル情報が B(0x00)・G(0xﬀ)・R(0x00)・

RSV(0x00) の順に並んでいますが、この命令⾃体は「32 ビット即値をレジスタ間接で書き込む」です。そのため、アセンブラでは、バイトオーダーをリトルエンディアンで並べると「B・G・R・RSV」の順になるように、「0x0000ﬀ00」という即値を指定しています。

♣ 初出の構⽂について
今回、
「xor」
・
「mov」
・
「imul」
・
「cmp」
・
「add」
・
「inc」で新しい構⽂がでてきましたので、紹介しておきます。

xor
32 ビットレジスタを使う構⽂が新しく登場しました。
▼表 4.14: 構⽂ 38: xor: レジスタ (32bit) 同⼠の XOR を第 2 オペランドへ格納
アセンブラ
機械語

xor レジスタ (32bit), レジスタ (32bit)
31 c0+reg_src_dst「reg_src_dst」は以下の通りです。
▼表 4.15: 32 ビットレジスタの reg_src_dst
src/dst
EAX
ECX
EDX
EBX
ESP
EBP
ESI
EDI

EAX
0x00
0x08
0x10
0x18
0x20
0x28
0x30
0x38

ECX
0x01
0x09
0x11
0x19
0x21
0x29
0x31
0x39

EDX
0x02
0x0a
0x12
0x1a
0x22
0x2a
0x32
0x3a

EBX
0x03
0x0b
0x13
0x1b
0x23
0x2b
0x33
0x3b

– 64 –

ESP
0x04
0x0c
0x14
0x1c
0x24
0x2c
0x34
0x3c

EBP
0x05
0x0d
0x15
0x1d
0x25
0x2d
0x35
0x3d

ESI
0x06
0x0e
0x16
0x1e
0x26
0x2e
0x36
0x3e

EDI
0x07
0x0f
0x17
0x1f
0x27
0x2f
0x37
0x3f

4.4 画⾯を塗りつぶしてみる

今回、xor は EDX レジスタをゼロクリアするために使⽤しています。mov 命令で即値 0 を格納しても良いのですが、xor を使⽤しているのは機械語のバイト数が 2 バイトと⼩さいからです。
試しに as_dump で確認してみます。
$ as_dump mov \$0,%edx
ファイル形式 elf64-x86-64/tmp/tmp.0Sw3Al6M9z/a.out:

セクション .text の逆アセンブル:

0000000000000000 <.text>:
0:ba 00 00 00 00

mov$0x0,%edx

mov 命令の場合、即値部分で 4 バイト使ってしまう 5 バイトの命令でした。
mov
mov も 32 ビットレジスタに関する構⽂が初出です。
▼表 4.16: 構⽂ 39: mov: 第 1 から第 2 オペランドへコピー (共に 32bit レジスタ)
アセンブラ
機械語

mov レジスタ (32bit), レジスタ (32bit)
89 c0+reg_src_dst「reg_src_dst」は XOR と同じです。

imul
imul も 32 ビットレジスタに関する構⽂です。
▼表 4.17: 構⽂ 40: imul: レジスタ (32bit) 同⼠の積を第 2 オペランドへ格納
アセンブラ
機械語

imul レジスタ (32bit), レジスタ (32bit)
0f af c0+reg_dst_src「reg_dst_src」は以下の通りです。
先に紹介した xor・mov とは⾏・列が異なるのでご注意ください。

– 65 –

第4章

フレームバッファで画⾯描画▼表 4.18: 32 ビットレジスタの reg_dst_src
dst/src
EAX
ECX
EDX
EBX
ESP
EBP
ESI
EDI

EAX
0x00
0x08
0x10
0x18
0x20
0x28
0x30
0x38

ECX
0x01
0x09
0x11
0x19
0x21
0x29
0x31
0x39

EDX
0x02
0x0a
0x12
0x1a
0x22
0x2a
0x32
0x3a

EBX
0x03
0x0b
0x13
0x1b
0x23
0x2b
0x33
0x3b

ESP
0x04
0x0c
0x14
0x1c
0x24
0x2c
0x34
0x3c

EBP
0x05
0x0d
0x15
0x1d
0x25
0x2d
0x35
0x3d

ESI
0x06
0x0e
0x16
0x1e
0x26
0x2e
0x36
0x3e

EDI
0x07
0x0f
0x17
0x1f
0x27
0x2f
0x37
0x3f

cmp
cmp も 32 ビットレジスタに関する構⽂です。
▼表 4.19: 構⽂ 41: cmp: レジスタ (32bit) 同⼠の⽐較
アセンブラ
機械語

cmp レジスタ (32bit), レジスタ (32bit)
39 c0+reg_src_dst「reg_src_dst」は、xor・mov と同じです。

add
add は 8 ビット即値を 64 ビットレジスタへ加算する構⽂が初出でした。
▼表 4.20: 構⽂ 42: add: 即値 (8bit) をレジスタ (64bit) へ加算
アセンブラ
機械語

add 即値 (8bit), レジスタ (64bit)
48 83 c0+reg_ofs 即値 (8bit)inc
inc も 32 ビットレジスタについてです。

– 66 –

4.5 ランダム⾊で塗りつぶしてみる▼表 4.21: 構⽂ 43: inc: レジスタ (32bit) をインクリメント
アセンブラ
機械語

inc レジスタ (32bit)
ﬀ c0+reg_ofs♣ 動作確認GUI で実⾏すると、画⾯が緑⼀⾊で塗りつぶされることが確認できます (図 4.1)。
(⽩黒印刷なので印刷上はわからないですが。。)▲図 4.1: 044_fill/sample.txt の実⾏結果

4.5 ランダム⾊で塗りつぶしてみる
各ピクセルを塗る⾊をランダムに決めることでカラーノイズを描画してみます。
この節のサンプルディレクトリは「045_color_noise」です。

– 67 –

第4章

フレームバッファで画⾯描画♣ rdrand 命令で乱数取得 (命令 25)
実は x86 CPU には乱数を取得する命令があります。それが「rdrand 命令」です。
rdrand はオペランドにレジスタを指定することで、そのレジスタへ乱数を格納してくれます。
ここでは、レジスタ EDI へ乱数を格納し、この 4 バイトレジスタの内容をピクセルデータとしてフレームバッファへ書き込んでみます。

rdrand 命令の今回使⽤する構⽂は以下の通りです。
▼表 4.22: 構⽂ 44: rdrand: 乱数をレジスタ (32bit) へ格納
アセンブラ
機械語

rdrand レジスタ (32bit)
0f c7 f0+reg_ofs♣ 実装
早速実装してみます (リスト 4.7)。
▼リスト 4.7: 045_color_noise/sample.txt# ・・・ 省略 ・・・# カウンタ(EDX)が画⾯ピクセル数(ESI)へ達したら「無限Halt」へジャンプ(*1)
10: 39 d6# cmp %edx,%esi
12: 76 0d# jbe +0x0f(変更)# 変更(ここから)# 現在のピクセルをランダム⾊で描画し次のピクセルへ移動
14: 0f c7 f7# rdrand %edi
17: 89 38# mov %edi,(%rax)# 変更(ここまで)
19: 48 83 c0 04 # add $4,%rax# カウンタ(EDX)をインクリメントし(*1)へジャンプ
1d: ff c2# inc %edx
1f: eb ef# jmp -0x0f(変更)# ・・・ 省略 ・・・

rdrand 命令で 32 ビットレジスタ EDI へ乱数を格納し、それを mov 命令でフレームバッファへ書き込むように変更しました。
なお、32 ビットレジスタをレジスタ間接で書き込む mov 命令は新しい構⽂なので、紹介しておきます。

– 68 –

4.5 ランダム⾊で塗りつぶしてみる▼表 4.23: 構⽂ 45: mov: レジスタ (32bit) をレジスタ間接で書き込む
アセンブラ
機械語
備考

mov レジスタ (32bit), (レジスタ (64bit))
89 00+reg_src_dst
レジスタ間接が RSP の場合、末尾に 0x24 追加
レジスタ間接が RBP の場合、2 バイト⽬に 0x40 加算、末尾に 0x00 追加「reg_src_dst」は以下の通りです。
▼表 4.24: 32 ビットレジスタ間接での reg_src_dst
src/(dst)
EAX
ECX
EDX
EBX
ESP
EBP
ESI
EDI

(RAX)
0x00
0x08
0x10
0x18
0x20
0x28
0x30
0x38

(RCX)
0x01
0x09
0x11
0x19
0x21
0x29
0x31
0x39

(RDX)
0x02
0x0a
0x12
0x1a
0x22
0x2a
0x32
0x3a

(RBX)
0x03
0x0b
0x13
0x1b
0x23
0x2b
0x33
0x3b

(RSP)
0x04
0x0c
0x14
0x1c
0x24
0x2c
0x34
0x3c

(RBP)
0x05
0x0d
0x15
0x1d
0x25
0x2d
0x35
0x3d

(RSI)
0x06
0x0e
0x16
0x1e
0x26
0x2e
0x36
0x3e

(RDI)
0x07
0x0f
0x17
0x1f
0x27
0x2f
0x37
0x3f

備考について、例えば EAX から RSP のレジスタ間接で書き込む場合、以下のようになります。
▼表 4.25: 例: EAX から RSP のレジスタ間接で書き込む
アセンブラ
機械語

mov %eax, (%rsp)
89 04 24

また、EAX から RBP のレジスタ間接で書き込む場合は以下のようになります。
▼表 4.26: 例: EAX から RBP のレジスタ間接で書き込む
アセンブラ
機械語

mov %eax, (%rbp)
89 45 00

– 69 –

第4章

フレームバッファで画⾯描画♣ 動作確認実⾏すると図 4.2 の様に、ランダム⾊の描画により、ノイズ画像のようなものが表⽰されます。(これも⽩黒印刷なのでわかりにくいですが)▲図 4.2: 045_color_noise/sample.txt の実⾏結果– 70 –

第5 章 キーボードドライバを作る

前章で画⾯描画が⾏えるようになりました。
この章ではキーボードドライバを作成することで、シリアル通信ではない⽅法でキー⼊⼒を取得できるようにします。
この章では 1 章を通して「051_get_scancode」のサンプルを作成します。

5.1 キー⼊⼒を受け取るまでの流れ
キーボードのキー⼊⼒は、キーボードコントローラ (KBC) という IC とやり取りすることで取得できます。KBC もレジスタを持っており、KBC のレジスタを参照する
ことで、⼊⼒されたキーの情報や、キーボードに関する現在のステータスを確認できます。

KBC のレジスタは IO アドレス空間に対応付けられています。そのため、in 命令/out
命令でレジスタを読み書きします。
キー⼊⼒ (スキャンコード) を取得する流れは、シリアル通信でデータ受信を⾏ったときと同様で、以下の通りです。

1. ステータスレジスタ値を取得
2. キー⼊⼒値が無ければ 1.(プログラム先頭) へ戻る
3. データレジスタからキー⼊⼒値を取得
4. キー⼊⼒値からキー押した (Make) かキーを離した (Break) かを判別 (希望の状態でなければ 1. へ戻る)
5. キー⼊⼒値からスキャンコードを取得
6. 1. へ戻る
今回は、キーから指を離した (Break) 時のスキャンコードを取得してみることにします。

– 71 –

第5章

キーボードドライバを作る

5.2 ステータスレジスタ値を取得
それでは、実装してみます。

KBC のステータスレジスタは IO アドレス空間の 0x0064 に割り当てられている 8
ビットのレジスタです。
例によって in 命令でレジスタ値を取得します。なお、今回のように IO アドレスが 8
ビット (1 バイト) に収まる場合、2 バイトで書ける構⽂があります。
▼表 5.1: 構⽂ 46: in: IO アドレス (1 バイト) 先から AL へ値を読み出す
アセンブラ

in 即値 (8bit), %ale4 即値 (8bit)

機械語

今回の場合、以下のようになります。
▼表 5.2: 例: in: IO アドレス (0x64) から AL へ読み出す
アセンブラ
機械語

in $0x64,%ale4 64なので、「1. ステータスレジスタ値を取得」の部分の機械語コードはリスト 5.1 の通りです。
▼リスト 5.1: 051_get_scancode/sample.txt# 1. ステータスレジスタ値を取得
00: e4 64# in $0x64,%al

5.3 キー⼊⼒値が無ければ先頭へ戻る
ステータスレジスタの最下位ビット (ビット 0) で KBC にキー⼊⼒値がある (=1) か無い (=0) かが分かります。
「フラグが⽴つまで待つ」系の処理で、コードブロックの構造としては「シリアル通信」の「受信待ち」の時と使う命令も同じです (リスト 5.2)。

– 72 –

5.4 データレジスタからキー⼊⼒値を取得▼リスト 5.2: 051_get_scancode/sample.txt# 1. ステータスレジスタ値を取得
00: e4 64# in $0x64,%al# 追加(ここから)# 2. キー⼊⼒値が無ければ先頭へ戻る# (pauseしながら待つ)
02: 24 01# and $0x01,%al
04: 75 04# jne +6 (⼊⼒が有れば進む)
06: f3 90# pause08: eb f6# jmp -0x08 (⼊⼒が無ければ戻る)# 追加(ここまで)

and 命令によるマスクで AL レジスタの最下位ビットだけ抽出し、その結果に応じて以下の挙動となります。

• and の結果が 0 (ビット 0 がセットされていない)◦ ゼロフラグがセットされる◦ jne はスルーし、pause した後、jmp で in 命令まで戻る• and の結果が 1 (ビット 0 がセットされている)◦ ゼロフラグがクリアされる◦ jne でこの命令から (この命令含む)6 バイト先へジャンプ (このコードブ
ロック直後)

5.4 データレジスタからキー⼊⼒値を取得
データレジスタの IO アドレスは 0x60 です。in 命令で取得し AL レジスタへ格納します。

sample.txt へ追記するとリスト 5.3 の通りです。
▼リスト 5.3: 051_get_scancode/sample.txt# 1. ステータスレジスタ値を取得
00: e4 64# in $0x64,%al# 2. キー⼊⼒値が無ければ先頭へ戻る# (pauseしながら待つ)
02: 24 01# and $0x01,%al
04: 75 04# jne +6 (⼊⼒が有れば進む)
06: f3 90# pause08: eb f6# jmp -0x08 (⼊⼒が無ければ戻る)– 73 –

第5章

キーボードドライバを作る# 追加(ここから)# 3. データレジスタからキー⼊⼒値を取得
0a: e4 60# in $0x60,%al# 追加(ここまで)

5.5 キー⼊⼒値が Break ならスキャンコードを抽出し、Make なら先頭へ戻る
データレジスタの 8 ビットの内容は以下の通りです。
▼表 5.3: データレジスタの内容
ビット

内容

7
0-6

Make(=0) か Break(=1) か
スキャンコード

Make 時のスキャンコードなら、ビット 7 は 0 なので、単にデータレジスタから取得
した 8 ビットをそのままスキャンコードとして使えば良いのですが、今回は Break 時のスキャンコードを取得したいので、ビット 7 がスキャンコードに含まれないように何らかの対処が必要です。

♣ 実装⽅針
今回は以下の 2 命令で実装してみます。

1. sub 命令: AL - 0x80 → AL
2. jb 命令 : 演算結果がマイナスなら先頭へジャンプ
1 つ⽬の sub 命令がミソです。AL のビット 7 がセットされていたか否かに応じて、以下の挙動になります。

• AL のビット 7 が 0 (Make)◦ 0x80 の減算結果はマイナスの値になる◦ AL には負値の何らかの値が格納される (使わないので気にしない)• AL のビット 7 が 1 (Break)◦ 0x80 の減算結果はプラスの値になる◦ AL にはビット 7 のみ 0 にした値が格納される (スキャンコードのみ抽出)– 74 –

5.5 キー⼊⼒値が Break ならスキャンコードを抽出し、Make なら先頭へ戻る

こうすることで、「スキャンコード部分の抽出」と「最上位ビットの状態確認」を同時に⾏うことができます。

♣ ⽐較結果が「⼩さい」ならジャンプ (命令 26「jb」)
なお、「jb」命令は初出なのでここで紹介します。
条件付きジャンプ命令の⼀種で、「Jump if Below」の略です。フラグレジスタとしてはキャリーフラグがセットされているとジャンプします。
構⽂は以下の通りです。
▼表 5.4: 構⽂ 47: jb: ⽐較結果が⼩さいならジャンプ
アセンブラ
機械語

jb <ラベルあるいは".">72 fe+rel_addr

キャリーフラグに応じてジャンプする命令でありながら「Jump if Below」という名前なのは、cmp 命令が内部的には減算によって⽐較を⾏っていることに由来していると思いますが、キャリーフラグはオーバーフローした場合にもセットされるので、add
命令等で桁溢れが起きてキャリーフラグがセットされた場合も、jb 命令はもちろんジャ
ンプします。

♣ 実装
sample.txt へ追記するとリスト 5.4 の通りです。
▼リスト 5.4: 051_get_scancode/sample.txt# 1. ステータスレジスタ値を取得
00: e4 64# in $0x64,%al# 2. キー⼊⼒値が無ければ先頭へ戻る# (pauseしながら待つ)
02: 24 01# and $0x01,%al
04: 75 04# jne +6 (⼊⼒が有れば進む)
06: f3 90# pause08: eb f6# jmp -0x08 (⼊⼒が無ければ戻る)# 3. データレジスタからキー⼊⼒値を取得
0a: e4 60# in $0x60,%al# 追加(ここから)# 4. キー⼊⼒値がBreakならスキャンコードを抽出し、Makeなら先頭へ戻る
0c: 2c 80# sub $0x80,%al– 75 –

第5章

キーボードドライバを作る

0e: 72 f0# 追加(ここまで)# jb -0x0e (Makeなら先頭へ戻る)

jb 命令 2 バイト⽬の相対アドレス表記はその他の jmp 命令と同じで、⾃分⾃⾝の命令位置が 0xfe です。今回の場合、sub 命令含め、jb より上に 14 バイトあるので、相対アドレス表記としては 0xf0 になります。

5.6 スキャンコードをレジスタ CL へコピーし、先頭へ戻る
特に意味はないのですが、⼀応、アウトプット代わりとして、取得したスキャンコー
ドを CL レジスタへコピーした上で、先頭へ戻ります (リスト 5.5)。
▼リスト 5.5: 051_get_scancode/sample.txt# 1. ステータスレジスタ値を取得
00: e4 64# in $0x64,%al# 2. キー⼊⼒値が無ければ先頭へ戻る# (pauseしながら待つ)
02: 24 01# and $0x01,%al
04: 75 04# jne +6 (⼊⼒が有れば進む)
06: f3 90# pause08: eb f6# jmp -0x08 (⼊⼒が無ければ戻る)# 3. データレジスタからキー⼊⼒値を取得
0a: e4 60# in $0x60,%al# 4. キー⼊⼒値がBreakならスキャンコードを抽出し、Makeなら先頭へ戻る
0c: 2c 80# sub $0x80,%al
0e: 72 f0# jb -0x0e (Makeなら先頭へ戻る)# 追加(ここから)# 5. スキャンコードをレジスタCLへコピー
10: 88 c1# mov %al,%cl# 6. 先頭へ戻る
12: eb ec# 追加(ここまで)# jmp -0x12– 76 –

5.7 動作確認

5.7 動作確認
QEMU の GUI 画⾯でキー⼊⼒をしながら QEMU モニターで CL レジスタの値を確認したいので、動作確認の際は、以下のように QEMU モニターだけ、実⾏したシェル上で起動するようにするとやりやすいです。そして、別途⽴ち上がった GUI のウィンドウの⽅でキー⼊⼒ (Make&Break) を⾏います。
$ ../tools/run_qemu ../fs -monitor stdio
vvfat ../fs chs 1024,16,63
QEMU 2.8.1 monitor - type 'help' for more information(qemu) info registers
RAX=000000000011001c RBX=00000000bef6ab18 RCX=0000000000000000 RDX=00000000000››00000# ↑起動直後、CLは0x00...
# '1'キーをMake&Break(qemu) info registers
RAX=000000000011001c RBX=00000000bef6ab18 RCX=0000000000000002 RDX=00000000000››00000# ↑CLにはスキャンコード0x02が格納された...
# '2'キーをMake&Break(qemu) info registers
RAX=000000000011001c RBX=00000000bef6ab18 RCX=0000000000000003 RDX=00000000000››00000# ↑CLにはスキャンコード0x03が格納された...
# '3'キーをMake&Break(qemu) info registers
RAX=000000000011001c RBX=00000000bef6ab18 RCX=0000000000000004 RDX=00000000000››00000# ↑CLにはスキャンコード0x04が格納された...
# 'a'キーをMake&Break(qemu) info registers
RAX=000000000011001c RBX=00000000bef6ab18 RCX=000000000000001e RDX=00000000000››00000# ↑CLにはスキャンコード0x1eが格納された...

スキャンコードは ASCII コードとは別で、スキャンコードセットには⼤きく 3 つの種類があります。スキャンコードについては例えば以下のページ等が詳しいです。

• 5. Scan Code Sets - PS/2 Keyboard - OSDev Wiki
◦ https://wiki.osdev.org/PS/2_Keyboard#Scan_Code_Sets– 77 –

付録

A 登場した機械語命令と構⽂の⼀覧

A.1 構⽂表記のキーワードについて
本⽂でも説明した通り、機械語の構⽂には、オフセットを⾜すことで対象のレジスタやアドレス等、細かい意味が変わる構⽂があります。
本書では、そのようなオフセットはキーワードを対応付けて、構⽂を表現しています。
ここでは、本書で登場したキーワードをまとめます。

♣ rel_addr
ジャンプ命令で使⽤するキーワードで、⾃⾝の命令の地点からのジャンプ先の相対的なバイト数を⽰します。

♣ reg_ofs
対象のレジスタを⽰すオフセット値です。
reg_ofs
0
1
2
3
4
5
6
7

64bit
RAX
RCX
RDX
RBX
RSP
RBP
RSI
RDI

32bit
EAX
ECX
EDX
EBX
ESP
EBP
ESI
EDI

16bit
AX
CX
DX
BX
SP
BP
SI
DI

8bit
AL
CL
DL
BL
AH
CH
DH
BH

♣ reg_src_dst
オペランドにレジスタが複数登場する場合、元 (src) と先 (dst) のレジスタの組み合
わせを⽰すオフセット値です。

– 79 –

付録 A 登場した機械語命令と構⽂の⼀覧

8 ビットレジスタ同⼠の場合
src/dst
AL
CL
DL
BL
AH
CH
DH
BH

AL
0x00
0x08
0x10
0x18
0x20
0x28
0x30
0x38

CL
0x01
0x09
0x11
0x19
0x21
0x29
0x31
0x39

DL
0x02
0x0a
0x12
0x1a
0x22
0x2a
0x32
0x3a

BL
0x03
0x0b
0x13
0x1b
0x23
0x2b
0x33
0x3b

AH
0x04
0x0c
0x14
0x1c
0x24
0x2c
0x34
0x3c

CH
0x05
0x0d
0x15
0x1d
0x25
0x2d
0x35
0x3d

DH
0x06
0x0e
0x16
0x1e
0x26
0x2e
0x36
0x3e

BH
0x07
0x0f
0x17
0x1f
0x27
0x2f
0x37
0x3f

EDX
0x02
0x0a
0x12
0x1a
0x22
0x2a
0x32
0x3a

EBX
0x03
0x0b
0x13
0x1b
0x23
0x2b
0x33
0x3b

ESP
0x04
0x0c
0x14
0x1c
0x24
0x2c
0x34
0x3c

EBP
0x05
0x0d
0x15
0x1d
0x25
0x2d
0x35
0x3d

ESI
0x06
0x0e
0x16
0x1e
0x26
0x2e
0x36
0x3e

EDI
0x07
0x0f
0x17
0x1f
0x27
0x2f
0x37
0x3f

32 ビットレジスタ同⼠の場合
src/dst
EAX
ECX
EDX
EBX
ESP
EBP
ESI
EDI

EAX
0x00
0x08
0x10
0x18
0x20
0x28
0x30
0x38

ECX
0x01
0x09
0x11
0x19
0x21
0x29
0x31
0x39

32 ビットレジスタと 64 ビットレジスタ間接の場合
src/(dst)
EAX
ECX
EDX
EBX
ESP
EBP
ESI
EDI

(RAX)
0x00
0x08
0x10
0x18
0x20
0x28
0x30
0x38

(RCX)
0x01
0x09
0x11
0x19
0x21
0x29
0x31
0x39

(RDX)
0x02
0x0a
0x12
0x1a
0x22
0x2a
0x32
0x3a

(RBX)
0x03
0x0b
0x13
0x1b
0x23
0x2b
0x33
0x3b

(RSP)
0x04
0x0c
0x14
0x1c
0x24
0x2c
0x34
0x3c

(RBP)
0x05
0x0d
0x15
0x1d
0x25
0x2d
0x35
0x3d

(RSI)
0x06
0x0e
0x16
0x1e
0x26
0x2e
0x36
0x3e

(RDI)
0x07
0x0f
0x17
0x1f
0x27
0x2f
0x37
0x3f

♣ reg_dst_src
こちらも、元 (src) と先 (dst) のレジスタの組み合わせを⽰すオフセット値で、オフ
セット値を増やしていった際の src と dst の関係が「reg_dst_src」とは異なるパターンです。
端的には、オフセット値を⾏⽅向に増やしていく表記をする際、「reg_dst_src」は、– 80 –

A.1 構⽂表記のキーワードについて

列が dst、⾏が src になります。

16 ビットレジスタ同⼠の場合
dst/src
AX
CX
DX
BX
SP
BP
SI
DI

AX
0x00
0x08
0x10
0x18
0x20
0x28
0x30
0x38

CX
0x01
0x09
0x11
0x19
0x21
0x29
0x31
0x39

DX
0x02
0x0a
0x12
0x1a
0x22
0x2a
0x32
0x3a

BX
0x03
0x0b
0x13
0x1b
0x23
0x2b
0x33
0x3b

SP
0x04
0x0c
0x14
0x1c
0x24
0x2c
0x34
0x3c

BP
0x05
0x0d
0x15
0x1d
0x25
0x2d
0x35
0x3d

SI
0x06
0x0e
0x16
0x1e
0x26
0x2e
0x36
0x3e

DI
0x07
0x0f
0x17
0x1f
0x27
0x2f
0x37
0x3f

EDX
0x02
0x0a
0x12
0x1a
0x22
0x2a
0x32
0x3a

EBX
0x03
0x0b
0x13
0x1b
0x23
0x2b
0x33
0x3b

ESP
0x04
0x0c
0x14
0x1c
0x24
0x2c
0x34
0x3c

EBP
0x05
0x0d
0x15
0x1d
0x25
0x2d
0x35
0x3d

ESI
0x06
0x0e
0x16
0x1e
0x26
0x2e
0x36
0x3e

EDI
0x07
0x0f
0x17
0x1f
0x27
0x2f
0x37
0x3f

32 ビットレジスタ同⼠の場合
dst/src
EAX
ECX
EDX
EBX
ESP
EBP
ESI
EDI

EAX
0x00
0x08
0x10
0x18
0x20
0x28
0x30
0x38

ECX
0x01
0x09
0x11
0x19
0x21
0x29
0x31
0x39

32 ビットレジスタと 64 ビットレジスタ間接の場合
dst/(src)
EAX
ECX
EDX
EBX
ESP
EBP
ESI
EDI

(RAX)
0x00
0x08
0x10
0x18
0x20
0x28
0x30
0x38

(RCX)
0x01
0x09
0x11
0x19
0x21
0x29
0x31
0x39

(RDX)
0x02
0x0a
0x12
0x1a
0x22
0x2a
0x32
0x3a

(RBX)
0x03
0x0b
0x13
0x1b
0x23
0x2b
0x33
0x3b

– 81 –

(RSP)
0x04
0x0c
0x14
0x1c
0x24
0x2c
0x34
0x3c

(RBP)
0x05
0x0d
0x15
0x1d
0x25
0x2d
0x35
0x3d

(RSI)
0x06
0x0e
0x16
0x1e
0x26
0x2e
0x36
0x3e

(RDI)
0x07
0x0f
0x17
0x1f
0x27
0x2f
0x37
0x3f

付録 A 登場した機械語命令と構⽂の⼀覧

64 ビットレジスタと 64 ビットレジスタ間接の場合
dst/(src)
RAX
RCX
RDX
RBX
RSP
RBP
RSI
RDI

(RAX)
0x00
0x08
0x10
0x18
0x20
0x28
0x30
0x38

(RCX)
0x01
0x09
0x11
0x19
0x21
0x29
0x31
0x39

(RDX)
0x02
0x0a
0x12
0x1a
0x22
0x2a
0x32
0x3a

(RBX)
0x03
0x0b
0x13
0x1b
0x23
0x2b
0x33
0x3b

(RSP)
0x04
0x0c
0x14
0x1c
0x24
0x2c
0x34
0x3c

(RBP)
0x05
0x0d
0x15
0x1d
0x25
0x2d
0x35
0x3d

(RSI)
0x06
0x0e
0x16
0x1e
0x26
0x2e
0x36
0x3e

A.2 四則演算♣ add: 加算
構⽂ 6
アセンブラ
機械語
構⽂ 7
アセンブラ
機械語
構⽂ 42
アセンブラ
機械語

add: 即値 (8bit) を AL へ加算
add 即値 (8bit), %al04 即値 (8bit)
add: 即値 (8bit) をレジスタ (8bit) へ加算
add 即値 (8bit), レジスタ (8bit)
80 c0+reg_ofs 即値 (8bit)
add: 即値 (8bit) をレジスタ (64bit) へ加算
add 即値 (8bit), レジスタ (64bit)
48 83 c0+reg_ofs 即値 (8bit)♣ sub: 減算
構⽂ 8
アセンブラ
機械語
構⽂ 9
アセンブラ
機械語

sub: 即値 (8bit) を AL レジスタから減算
sub 即値 (8bit), %al2c 即値 (8bit)
sub: 即値 (8bit) をレジスタ (8bit) から減算
sub 即値 (8bit), レジスタ (8bit)
80 e8+reg_ofs 即値 (8bit)– 82 –

(RDI)
0x07
0x0f
0x17
0x1f
0x27
0x2f
0x37
0x3f

A.3 インクリメント/デクリメント♣ imul: 乗算
構⽂ 10
アセンブラ
機械語
構⽂ 11
アセンブラ
機械語
構⽂ 12
アセンブラ
機械語
構⽂ 40
アセンブラ
機械語

imul: AL * レジスタ (8bit) を AX へ格納
imul レジスタ (8bit)
f6 e8+reg_ofs
imul: 第 1 * 第 2 オペランド (16bit レジスタ) を第 2 オペランドへ格納
imul レジスタ (16bit,src), レジスタ (16bit,dst)
66 0f af c0+reg_dst_src
imul: 第 1 オペランド * 第 2 オペランドを第 3 オペランドへ格納
imul 即値 (8bit), レジスタ (16bit,src), レジスタ (16bit,dst)
66 6b c0+reg_dst_src 即値 (8bit)
imul: レジスタ (32bit) 同⼠の積を第 2 オペランドへ格納
imul レジスタ (32bit), レジスタ (32bit)
0f af c0+reg_dst_src♣ idiv: 除算
構⽂ 13
アセンブラ
機械語

idiv: AX/レジスタ (8bit) の商を AL へ剰余を AH へ格納
idiv レジスタf6 f8+reg_ofs

A.3 インクリメント/デクリメント♣ inc: インクリメント
構⽂ 24
アセンブラ
機械語
構⽂ 43
アセンブラ
機械語

inc: レジスタ (8bit) をインクリメント
inc レジスタ (8bit)
fe c0+reg_ofs
inc: レジスタ (32bit) をインクリメント
inc レジスタ (32bit)
ﬀ c0+reg_ofs– 83 –

付録 A 登場した機械語命令と構⽂の⼀覧♣ dec: デクリメント
構⽂ 25
アセンブラ
機械語

dec: レジスタ (8bit) をデクリメント
inc レジスタ (32bit)
ﬀ c0+reg_ofs

A.4 ⽐較
♣ cmp: 2 つのオペランドの差を評価
構⽂ 32
アセンブラ
機械語
構⽂ 33
アセンブラ
機械語
構⽂ 41
アセンブラ
機械語

cmp: 即値 (8bit) と AL を⽐較
cmp 即値 (8bit), %al3c 即値 (8bit)
cmp: 即値 (8bit) とレジスタ (8bit) を⽐較
cmp 即値 (8bit), レジスタ (8bit)
80 f8+reg_ofs 即値 (8bit)
cmp: レジスタ (32bit) 同⼠の⽐較
cmp レジスタ (32bit), レジスタ (32bit)
39 c0+reg_src_dst

A.5 論理演算♣ and: 論理積
構⽂ 14
アセンブラ
機械語
構⽂ 15
アセンブラ
機械語

and: 即値 (8bit) AND AL を AL へ格納
and 即値 (8bit), %al
24 即値 (8bit)
and: 即値 (8bit) AND レジスタ (8bit) をレジスタ (8bit) へ格納
and 即値 (8bit), レジスタ (8bit)
80 e0+reg_ofs 即値 (8bit)– 84 –

A.6 ビットシフト♣ or: 論理和
構⽂ 16
アセンブラ
機械語
構⽂ 17
アセンブラ
機械語

or: 即値 (8bit) OR AL を AL へ格納
or 即値 (8bit), %al0c 即値 (8bit)
or: 即値 (8bit) OR レジスタ (8bit) をレジスタ (8bit) へ格納
or 即値 (8bit), レジスタ (8bit)
80 c8+reg_ofs 即値 (8bit)♣ xor: 排他的論理和
構⽂ 18
アセンブラ
機械語
構⽂ 19
アセンブラ
機械語
構⽂ 38
アセンブラ
機械語

xor: 即値 (8bit) XOR AL を AL へ格納
xor 即値 (8bit), %al34 即値 (8bit)
xor: 即値 (8bit) XOR レジスタ (8bit) をレジスタ (8bit) へ格納
xor 即値 (8bit), レジスタ (8bit)
80 f0+reg_ofs 即値 (8bit)
xor: レジスタ (32bit) 同⼠の XOR を第 2 オペランドへ格納
xor レジスタ (32bit), レジスタ (32bit)
31 c0+reg_src_dst♣ not: 否定
構⽂ 20
アセンブラ
機械語

not: レジスタ (8bit) の NOT をレジスタ (8bit) へ格納
not レジスタ (8bit)
f6 d0+reg_ofs

A.6 ビットシフト
ビットシフトは挙動としては表 A.20 の通りです。

sal と shl は挙動が同じなので、機械語も同じです。そのため、sal のみ紹介します。

– 85 –

付録 A 登場した機械語命令と構⽂の⼀覧▼表 A.1: 各シフト演算の挙動
sal
sar
shl
shr

第 1 オペランドの回数分、第 2 オペランドへ 2 を掛ける
第 1 オペランドの回数分、第 2 オペランドを 2 で割る (符号有)第 1 オペランドの回数分、第 2 オペランドへ 2 を掛ける
第 1 オペランドの回数分、第 2 オペランドを 2 で割る (符号無)♣ sal: 算術左シフト
構⽂ 21
アセンブラ
機械語

sal: 第 1 オペランド=即値 (8bit)、第 2 オペランド=レジスタ (8bit)
sal 即値 (8bit), レジスタ (8bit)
c0 e0+reg_ofs 即値 (8bit)♣ sar: 算術右シフト
構⽂ 22
アセンブラ
機械語

sar: 第 1 オペランド=即値 (8bit)、第 2 オペランド=レジスタ (8bit)
sar 即値 (8bit), レジスタ (8bit)
c0 f8+reg_ofs 即値 (8bit)♣ shr: 論理右シフト
構⽂ 23
アセンブラ
機械語

shr: 第 1 オペランド=即値 (8bit)、第 2 オペランド=レジスタ (8bit)
shr 即値 (8bit), レジスタ (8bit)
c0 e8+reg_ofs 即値 (8bit)

A.7 読み出し/書き込み♣ mov: 第 1 から第 2 オペランドへコピー
構⽂ 4
アセンブラ
機械語
構⽂ 31
アセンブラ
機械語

mov: 即値 (8bit) をレジスタ (8bit) へ格納
mov 即値 (8bit), レジスタ (8bit)
b0+reg_ofs 即値 (8bit)
mov: 第 1 から第 2 オペランドへコピー (共に 8 ビットレジスタ)
mov レジスタ (8bit,src), レジスタ (8bit,dst)
88 c0+reg_src_dst– 86 –

A.7 読み出し/書き込み
構⽂ 5
アセンブラ
機械語
構⽂ 39
アセンブラ
機械語
構⽂ 36
アセンブラ
機械語
備考

構⽂ 45
アセンブラ
機械語
備考

構⽂ 35
アセンブラ
機械語
備考
構⽂ 34
アセンブラ
機械語
備考

mov: 即値 (16bit) をレジスタ (16bit) へ格納
mov 即値 (16bit), レジスタ (16bit)
66 b9+reg_ofs 即値 (16bit)

mov: 第 1 から第 2 オペランドへコピー (共に 32bit レジスタ)
mov レジスタ (32bit), レジスタ (32bit)
89 c0+reg_src_dst
mov: 即値 (32bit) をレジスタ間接で書き込む
movl 即値 (32bit), (レジスタ (64bit))
c7 00+reg_ofs 即値 (32bit)
レジスタ間接が RSP の場合、3 バイト⽬に 0x24 追加
レジスタ間接が RBP の場合、2 バイト⽬に 0x40 加算、3 バイト⽬に 0x00 追加
mov: レジスタ (32bit) をレジスタ間接で書き込む
mov レジスタ (32bit), (レジスタ (64bit))
89 00+reg_src_dst
レジスタ間接が RSP の場合、3 バイト⽬に 0x24 追加
レジスタ間接が RBP の場合、2 バイト⽬に 0x40 加算、末尾に 0x00 追加
mov: DSP(8bit) 付きレジスタ間接の値をレジスタ (32bit) へ格納
mov dsp(レジスタ (64bit)), レジスタ (32bit)
8b 40+reg_dst_src dsp(8bit)
レジスタ間接が RSP の場合、3 バイト⽬に 0x24 追加
mov: 第 1 オペランドの指す先を第 2 オペランド (64 ビットレジスタ) へ格納
mov (レジスタ (64bit)), レジスタ (64bit)
48 8b 00+reg_dst_src
レジスタ間接が RSP の場合、末尾に 0x24 追加
レジスタ間接が RBP の場合、3 バイト⽬に 0x40 加算、末尾に 0x00 追加♣ in: IO アドレス空間から読み出し
構⽂ 46
アセンブラ
機械語
構⽂ 27
アセンブラ
機械語

in: IO アドレス (1 バイト) 先から AL へ値を読み出す
in 即値 (8bit), %ale4 即値 (8bit)
in: DX が指すレジスタ値を AL へ格納
in %dx, %alec– 87 –

付録 A 登場した機械語命令と構⽂の⼀覧♣ out: IO アドレス空間へ書き込み
構⽂ 26

out: AL を DX が指すアドレス先へ書き込む
out %al, %dx
ee

アセンブラ
機械語

A.8 CPU 状態制御
♣ hlt: CPU をスリープ
構⽂ 3
アセンブラ
機械語

hlt: CPU をスリープさせる
hltf4♣ pause: ビジーループのヒント
構⽂ 30
アセンブラ
機械語

pause: ビジーループのヒントを CPU へ伝える
pause
f3 90

A.9 分岐♣ jmp: 無条件ジャンプ
構⽂ 1
アセンブラ
機械語

jmp: 相対アドレス指定でジャンプ
jmp <ラベルあるいは".">eb fe+rel_addr♣ jb: ⼩さい (Below) 場合にジャンプ
構⽂ 47
アセンブラ
機械語

jb: ⽐較結果が⼩さいならジャンプ
jb <ラベルあるいは".">72 fe+rel_addr♣ jbe: ⼩さいか等しい (Below or Equal) 場合にジャンプ
構⽂ 37
アセンブラ
機械語コードjbe: 等しいか⼩さければジャンプ
jbe <ラベルあるいは".">76 fe+rel_addr– 88 –

A.10 その他

♣ je: 等しい (Equal) 場合にジャンプ
構⽂ 28

je: ゼロフラグがセットされていればジャンプ
je <ラベルあるいは".">74 fe+rel_addr

アセンブラ
機械語

♣ jne: 等しくない (Not Equal) 場合にジャンプ
構⽂ 29
アセンブラ
機械語

jne: ゼロフラグがセットされていなければジャンプ
jne <ラベルあるいは".">75 fe+rel_addr

A.10 その他♣ nop: 何もしない (No Operation)
構⽂ 2
アセンブラ
機械語

nop: 何もしない
nop90♣ rdrand: 乱数取得
構⽂ 44
アセンブラ
機械語

rdrand: 乱数をレジスタ (32bit) へ格納
rdrand レジスタ (32bit)
0f c7 f0+reg_ofs– 89 –

おわりに本書をお読みいただきありがとうございました！
機械語を⼿で書く、しかもそれをコンパイラ等も⼗分に在る x86_64 アーキテクチャ
で、という内容でしたが、いかがだったでしょうか。
「意外と書こうと思えば書けるな」と感じてもらえれば幸いです。
完全にネタ本なのですが、何か役に⽴つとすれば、⾼級な⾔語でも処理を書きながら「機械語に落とし込まれたら何バイトくらいかな」という機械語におけるサイズ感が⾝につく、とかでしょうか。ただ、機械語の 1 命令は⼩さいものでは 1 バイトから、⼤きいものでは 10 数バイトもあったりするし、同じ命令でもオペランドの違いだけでサイズが⼤きく変わってきたりすることもあるので、⾼級⾔語で処理を書きながら機械語の実装をエスパーするのは相当難易度⾼いかも。
。
やっぱり「何かの役に⽴つものでは無い」のですが。。機械語を書いて実⾏すること
で「通訳を介さずに CPU と直接話している感覚」とか、⾯⽩がってもらえれば嬉しいです。
あと、本書のサンプルはどれも、「フレームバッファへの書き込み」や「命令フェッチ」を除き、⼀切メモリアクセスをしないのもポイントです。ただ、これに関してはア
センブラで書いても同じことはできるのですが、少なくとも C ⾔語とか、スタックを使うバイナリを⾃動的に⽣成してしまう⾼級⾔語ではできないことです。ただ、スタックの仕組みを使わないことには、今後⼤きなプログラムを書いていくのは難しそうですが。
。

– 91 –

参考情報参考にさせてもらった情報• Intel 64 and IA-32 Architectures Software Developer Manuals◦ https://software.intel.com/en-us/articles/intel-sdm◦ Intel 公式の x86 CPU のソフトウェア開発マニュアルです◦ CPU の基本的なアーキテクチャやレジスタなどについては Volume 1 に書かれています◦ 各命令のリファレンスは Volume 2 に書かれており、全命令と全構⽂が網羅されています• WikiBooks「X86 アセンブラ/x86 アーキテクチャ」◦ https://ja.wikibooks.org/wiki/X86 アセンブラ/x86 アーキテクチャ◦ x86 CPU の基本的なアーキテクチャについてはこちらの記事が分かりやすいです。

◦ ただ、64 ビットに関する記載はありませんが。。
• 書籍「ハードウェアデザインシリーズ 12 パソコンのレガシィ I/O 活⽤⼤全」◦ 桑野 雅彦 著、CQ 出版社、2000 年◦ 本書で扱ったシリアルポートやキーボードコントローラなどは、今やレガシーなアーキテクチャで、レジスタの仕様等の詳細はネット上よりもこの書
籍が詳しいです◦ ただ、中古で買うしかない状況ではありますが。。

poiboot について
本書でブートローダーとして使⽤している「poiboot」は、筆者の過去の著作である
以下の同⼈誌の成果物に少し⼿を加えたものです。

• フルスクラッチで作る!UEFI ベアメタルプログラミング! パート 1、パート 2◦ ブートローダーを作る上で必要な UEFI を直接叩く⽅法を紹介
当サークルの同⼈誌は、すべて以下の URL から PDF 版/HTML 版は無料でダウン
ロード/閲覧できますので興味があればぜひ⾒てみてもらえると嬉しいです。

• http://yuma.ohgami.jp– 93 –

作って分かる！ x86_64 機械語⼊⾨
OS レスで CPU と直接話す!
2019 年 9 ⽉ 22 ⽇
著

者

⼤神祐真

印刷所

⽇光企画

ver 1.0© 2019 へにゃぺんて(powered by Re:VIEW Starter)