起動するまでの長い道のり IPL編(5) CPU解説の巻

 前回まで実際のソースコードを元に不器用に説明したけど、いい加減CPUの説明抜きで話を進めるのが非常に辛いと分かったので、必要最低限解説しておこうと思う。

メモリ・レジスタ関連

メモリ・レジスタ

 一般的なCPUには全てメモリレジスタがある。メモリは言うまでもなくデータやプログラムを置く記憶領域で、レジスタは以前説明したとおり作業用の「手」だ。メモリから取り出した値を持ったり、値を加工するために持ったりする。非常に小さく、ほとんど1〜4バイトしかない。また、計算や処理には使わない実行制御用の特殊なレジスタもたくさん存在する。

プログラムカウンタ

 CPUはメモリに置かれたプログラムのコードを順次実行していく。そのために、メモリ上のどの命令を現在実行しているのかを覚えていなければならない。CPUにはそのための専用のレジスタがあり、プログラムカウンタと呼ばれる。486やPentiumなどのいわゆるx86系CPUでは、IPまたはEIPレジスタが該当する。jmp命令やcall命令で処理を別の場所に移す時は、このプログラムカウンタが書き換えられる。

スタック

 メモリ上にはスタックという領域(とデータ構造)が割り当てられる。これもメモリの一部で、スタックポインタという専用のレジスタで在り処を指定する。x86系CPUでは、SPまたはESPレジスタが該当する。

 スタックはデータ構造の用語として出てくるスタックと同じで、データ値をpush/popできる格納庫だ。pushによりデータを入れ、popにより一番上にあるデータを取り出す。popではスタックのどのデータを取り出すかは指定できず、とにかく今一番上にあるものが取り出される。次回のpopでは、最後に取り出したデータの下にあったものが取り出される。

 スタックは、関数の呼び出しを行う時や、ローカル変数の格納場所として用いられる。callを実行した時、その時点でのプログラムカウンタの値がスタックにpushされる。call先でretが実行されると、スタックの一番上から以前のプログラムカウンタの値が取り出され、元の処理に戻る。

IO・割り込み

 もしプログラムが単に実行されるだけで、外部入力やディスクのデータを参照しない場合は、上記要素が揃っていれば処理が行える。だが、大抵のプログラムはディスクからデータを読み込んでしかもユーザの入力に反応する。そのための仕組みとして、IO割り込みがある。

IO

 IOはデバイスによる外部入出力のための仕組みで、x86では専用のIO入出力命令か、メモリマップによりアクセスできる。IOもメモリのようにアドレスがあり、1バイト・2バイト・4バイトといった単位でデータの読み書きが行える。メモリマップとは、IOのアドレスをメモリのアドレスにマップして、メモリアクセスすることがそのままIOにアクセスすることになる仕組みだ。

割り込み

 割り込みとは、外部デバイスで何かが起きたときに通知される信号のことだ。例えばキーボードでキーが押された時や、マウスが移動した時に発生する。割り込みにはそれぞれ種類ごとに番号が振られていて、発生時には番号に応じた処理が呼び出される。このとき実行される処理のことを割り込みハンドラと呼ぶ。番号と割り込みハンドラの対応付けはプログラムで設定できる。また、プログラム実行中にエラーが起きた場合(0で除算した等)も割り込み(この場合は例外とも呼ばれる)で通知される。

OSの仕事

 CPUでの処理は、基本的には、IO操作で外部デバイスを操作し、割り込みハンドラで外部デバイスからの入力を受け取り、メモリとレジスタで処理を実行するという形になる。

 しかし実際はもっと複雑で、例えばディスクに書き込もうとしてもデバイスが処理中で書き込めない事がある。その場合、割り込みで後から書き込み可能になったという通知が届く。それでようやくデータをディスクに書き込める。その間、ディスクを操作しようとしたプログラムは停止させておかなければならない。ずっと待っているだけなのは無駄なので、別のプログラムに切り替えてそちらを実行したりする(いわゆるマルチタスク)。そういうデバイスや割り込みの管理・プログラムの切り替え等がOSの仕事になる。

x86でのレジスタ

 x86でのレジスタには以下のようなものがある。なお、これ以外にも特殊な用途のものがまだたくさんある。だが一般的なものは以下で網羅していると思う。

名称 サイズ 用途
AH 1バイト 汎用
AL 1バイト 汎用
AX 2バイト 汎用
EAX 4バイト 汎用
BH 1バイト 汎用
BL 1バイト 汎用
BX 2バイト 汎用。データ参照用アドレスとして使われる。
EBX 4バイト 汎用。データ参照用アドレスとして使われる。
CH 1バイト 汎用
CL 1バイト 汎用
CX 2バイト 汎用。主にループカウンタとして使われる。
ECX 4バイト 汎用。主にループカウンタとして使われる。
DH 1バイト 汎用
DL 1バイト 汎用
DX 2バイト 汎用。主にIOポインタとして使われる。
EDX 4バイト 汎用。主にIOポインタとして使われる。
SI 2バイト 汎用。ストリング命令の元アドレスとして使われる。
ESI 4バイト 汎用。ストリング命令の元アドレスとして使われる。
DI 2バイト 汎用。ストリング命令の先アドレスとして使われる。
EDI 4バイト 汎用。ストリング命令の先アドレスとして使われる。
SP 2バイト スタックポインタ
ESP 4バイト スタックポインタ
BP 2バイト スタック上のアドレスを指すベースポインタ
EBP 4バイト スタック上のアドレスを指すベースポインタ
IP 2バイト プログラムカウンタ
EIP 4バイト プログラムカウンタ
EFLAGS 4バイト フラグレジスタ
CS 2バイト コードセグメントレジスタ
DS 2バイト データセグメントレジスタ
SS 2バイト スタックセグメントレジスタ
ES 2バイト 追加のデータセグメントレジスタ
FS 2バイト 追加のデータセグメントレジスタ
GS 2バイト 追加のデータセグメントレジスタ

 A・B・C・Dの各汎用レジスタにはそれぞれにAH・AL・AX・EAXといった4種類がある。表の通りそれぞれサイズが違う。また、サイズが違うレジスタ同士で領域が重なっていて、ALレジスタに値を代入した場合はAX・EAXの値(最下位バイト)も変わる。逆にEAXに値を設定した場合もAX・AH・ALの値が変わる。2バイトと4バイトの2種類があるESI・EDIも同様だ。

 EBP・ESP・EIPに関しては、CPUのモード(リアルモードかプロテクトモードか)に応じて2バイトになるか4バイトになるかが決まる。

命令セット

 x86系のCPUが実行する命令について簡単に解説する。

命令の構造

 CPUに与えられる命令は、以下のような構造になっている。

オペコード オペランド1 オペランド2 ...オペランドn
movw・jmpなど命令の種類を表す。 命令の対象(レジスタや値)を指定する。 同じく命令の対象を指定する。 オペランドの数は命令により違う。

 たとえば「movw %ax, %bx」なら、オペコードはmovw、オペランド1はAXレジスタオペランド2はBXレジスタになる。movw命令はオペランド1の値をオペランド2にコピーする命令なので、AX→BXの処理が行われる。

 ちなみに、movwやjmpといったオペコードの名前のことをニーモニックと呼ぶ。

基本的な命令

 詳細はやはりIntelのマニュアルを見て欲しい。本当にごく簡単なものをごく簡単に説明する。カッコつきのニーモニックは「movw」といったようにサイズを表す文字が付加されることを示す。

ニーモニック 説明
mov(bwl) 値をコピーする。
lea(wl) アドレス値をレジスタに読み込む。
add(bwl) 値を加算する。
sub(bwl) 値を減算する。
mul(bwl) EAX(AL・AX)の値をオペランドの値で乗算する。
div(bwl) EAX(AL・AX)の値をオペランドの値で除算する。
neg(bwl) 値の符号を反転させる。
sal(bwl) 値を左ビットシフトする。符号を考慮する。
sar(bwl) 値を右ビットシフトする。符号を考慮する。
shl(bwl) 値を左ビットシフトする。符号は考慮しない。
shr(bwl) 値を右ビットシフトする。符号は考慮しない。
or(bwl) 値をOR演算する。
and(bwl) 値をAND演算する。
xor(bwl) 値をXOR演算する。
not(bwl) 値をビット反転する。
inc(bwl) 値を1加算する。
dec(bwl) 値を1減算する。
jmp 指定アドレスにジャンプする。
je 直前の計算で値が等しかった場合ジャンプする。
jne 直前の計算で値が等しくなかった場合ジャンプする。
jz 直前の計算でゼロになった場合ジャンプする。
jnz 直前の計算でゼロにならなかった場合ジャンプする。
jl 直前の計算で値がより小さい場合ジャンプする。
jle 直前の計算で値が以下の場合ジャンプする。
jnl 直前の計算で値がより小さくない場合ジャンプする。
jnle 直前の計算で値が以下ではない場合ジャンプする。
jg 直前の計算で値がより大きい場合ジャンプする。
jge 直前の計算で値が以上の場合ジャンプする。
jng 直前の計算で値がより大きくない場合ジャンプする。
jnge 直前の計算で値が以上ではない場合ジャンプする。
call 関数呼び出しを行う。
ret 関数内から呼び出し元に戻る。
iret 割り込みハンドラを終了させる。
push(wl) スタックに値を積む。
pop(wl) スタックから値を取り出す。
in(bwl) IOポートから入力を取り出す。
out(bwl) IOポートに出力する。
int 指定した番号の割り込みを発生させる
cli 割り込みを禁止する。
sti 割り込み禁止を解除する。
cld ストリング命令を低位アドレス→高位アドレスの方向にする。
std ストリング命令を高位アドレス→低位アドレスの方向にする。
nop 何もしない。

次回予告

 リアルモードでのセグメンテーションと割り込みについて解説する。

参考資料

詳解 Linuxカーネル 第3版

詳解 Linuxカーネル 第3版

はじめて読む486―32ビットコンピュータをやさしく語る

はじめて読む486―32ビットコンピュータをやさしく語る

x86アセンブラ入門―PC/ATなどで使われている80x86のアセンブラを習得 (TECHI―Processor)

x86アセンブラ入門―PC/ATなどで使われている80x86のアセンブラを習得 (TECHI―Processor)