harib03a:ソースコード分割
- ソースコードの分割をしているだけなので省略
harib03b: Makefileのルールの一般規則化
harib03c: ヘッダファイル整備
- ここもヘッダファイルの話をしているだけなので、読むだけで。 ただヘッダファイの利便性が今まで一番よく伝わってきた。実際にコードが短くなると 「ほほー」ってなるなあ。また、なぜこれを使うのかわからなくなった時に戻ってきたい。
harib03c: GDT / GDTR / LGDT
DAY5の最後の説明の続き。
_load_gdtr: ; void load_gdtr(int limit, int addr);
MOV AX,[ESP+4] ; limit値をAXレジスタに格納
MOV [ESP+6],AX ; AXレジスタに格納したlimit値を
LGDT [ESP+6]
RET
関数名になっているGDTRは、GDT(global descriptor table)のアドレスを特殊な格納するレジスタという説明だった。 48bitの容量を持っているらしい。
GDTRレジスタへの値の代入はMOV命令を使うことができない。(そういう仕様らしい) 代わりに、 LGDT というような形でLGDT命令に特定のメモリ番地を渡すと、 LGDTがそこから先頭8byte=48bitを読み込んで、GDTRレジスタに格納してくれる
GDTRレジスタの下位2byteは、Limitを表すというのは下図の ‘16-Bit Table Limit’ の所だろう。 次の「GDTの有効バイト数 -1」 は説明が省略されすぎていて、ちょっとわからない。。
http://slideplayer.com/slide/9243430/
void load_gdtr(int limit, int addr)が呼び出された時、 「ESPレジスタに格納のアドレス + 4」にlimit値が、 「ESPレジスタに格納のアドレス + 8」にアドレスが、 それぞれ格納されている。
このうち、limit値のほうを、 ESP + 4 ⇒ AX ⇒ ESP + 6 ⇒ LGDT ⇒ GDTR という順番で渡しているようなコードに見える。
全体的に説明についていけていないが、この理解で進める。
harib03c: dsctbl.c
bootpack.h
/* dsctbl.c */
struct SEGMENT_DESCRIPTOR {
short limit_low, base_low; /* base addressをshort型のlow、char型のmid、highに3分割している。理由はintel80286時代との互換性とのこと。深追いはやめておく */
char base_mid, access_right;
char limit_high, base_high; /* limitはセグメントの大きさを表す。この説明は5章終わりの段階でほしかった。。
};
dsctbl.c
void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
{
if (limit > 0xfffff) {
ar |= 0x8000; /* G_bit = 1 */
limit /= 0x1000;
}
sd->limit_low = limit 0xffff;
sd->base_low = base 0xffff;
sd->base_mid = (base >> 16) 0xff;
sd->access_right = ar 0xff;
sd->limit_high = ((limit >> 16) 0x0f) | ((ar >> 8) 0xf0);
sd->base_high = (base >> 24) 0xff;
return;
}
ここの説明はdiagramなしでの理解がきついので、他のサイトからもってきた。
ざっくりとした絵としては、こっちのほうがシンプルでわかりやすいか。
要点だけ、
- セグメント番地(上図の BaseAddress )は32bit。Intel80286との互換性のため、low / mid / highの3つに分割した。
- Limit(上図の Limit )はセグメントサイズを表す。セグメント属性内のGビットを立てる(値を1にする)と、単位がbyte単位⇒ページ単位に変わる
- セグメント属性(上図のAccess、他にarと言ったりaccess rightと言ったり)は、Intel80286では存在せずi386の機能拡張で追加された。(なので拡張アクセス権と言ったりする) Limitのところで述べたGビットとは別に、メモリセグメントのモードを決める?Dセグメントというものがある。 値=0の場合は16bit、値=1の場合は32bitで動作。D=1で使うのが一般的。 セグメント属性の下位8bitを使って、メモリ空間のカテゴリー化を行っている。 カテゴリーの類型は以下の通り。
- システム専用 RW- ※X不可
- システム専用 R-X ※W不可
- アプリケーション用 RW- ※X不可
- アプリケーション用 R-X ※W不可
- CPUはprotection ringsという動作モードに応じて付与する権限を変えるアーキテクチャを採用している。 上記項番2のメモリ領域に展開されたプログラムを実行するときは、システムモード(ring 0)で起動する。 上記項番4のメモリ領域に展開されたプログラムが実行されたときは、アプリケーションモード( ring 3 )で起動する。 アプリケーションモードで動作時に項番1/2にアクセスしようとすると、怒られる。(segmentation faultとか?)
harib03d: PCI初期化
ここまでGDT/IDTの説明を延々としてきたのは、割り込み処理をしたかったからとのこと。
ここからは、PICの割り込み処理の説明ををしていく。 PIC(割り込みコントローラ)は、PCI(拡張バス)とは別ものらしい。名前がややこしい。
PICの概要:
- 8個の割り込み信号(IRQ = interrupt request)を1つの割り込み信号にまとめる
- CPUに直接つながるマスタPICとその下につながるスレーブPICの2種がある
[f:id:smatsuzaki:20191130123707p:plain]
上記を踏まえて以下のコードを概観していく。
/* 割り込み関係 */
#include "bootpack.h"
void init_pic(void)
/* PICの初期化 */
{
io_out8(PIC0_IMR, 0xff ); /* 全ての割り込みを受け付けない */
io_out8(PIC1_IMR, 0xff ); /* 全ての割り込みを受け付けない */
io_out8(PIC0_ICW1, 0x11 ); /* エッジトリガモード */
io_out8(PIC0_ICW2, 0x20 ); /* IRQ0-7は、INT20-27で受ける */
io_out8(PIC0_ICW3, 1 << 2); /* PIC1はIRQ2にて接続 */
io_out8(PIC0_ICW4, 0x01 ); /* ノンバッファモード */
io_out8(PIC1_ICW1, 0x11 ); /* エッジトリガモード */
io_out8(PIC1_ICW2, 0x28 ); /* IRQ8-15は、INT28-2fで受ける */
io_out8(PIC1_ICW3, 2 ); /* PIC1はIRQ2にて接続 */
io_out8(PIC1_ICW4, 0x01 ); /* ノンバッファモード */
io_out8(PIC0_IMR, 0xfb ); /* 11111011 PIC1以外は全て禁止 */
io_out8(PIC1_IMR, 0xff ); /* 11111111 全ての割り込みを受け付けない */
return;
}
- PICはCPUから見ると周辺装置のため、OUT命令(io_out8)を使う ※画面に描画するためにVRAMを操作するのに使っていた命令
- コード中の PIC0 がマスターPIC、 PIC1 がスレーブPCI
- PIC0 , PIC1の各レジスタのポートマッピングはincludeされている、bootpack.hにて定義されているがわかりにくい。。
/* int.c */ void init_pic(void); #define PIC0_ICW1 0x0020 #define PIC0_OCW2 0x0020 #define PIC0_IMR 0x0021 #define PIC0_ICW2 0x0021 #define PIC0_ICW3 0x0021 #define PIC0_ICW4 0x0021 #define PIC1_ICW1 0x00a0 #define PIC1_OCW2 0x00a0 #define PIC1_IMR 0x00a1 #define PIC1_ICW2 0x00a1 #define PIC1_ICW3 0x00a1 #define PIC1_ICW4 0x00a1
コードを細かく読んでいく
io_out8(PIC0_IMR, 0xff ); /* 全ての割り込みを受け付けない */
io_out8(PIC1_IMR, 0xff ); /* 全ての割り込みを受け付けない */
第一引数で使われている、IMRレジスタは「interrupt mask register」という8bit のレジスタであり、 bitの各桁がIRQ×8個に対応、ビットが立っている(1が入っている)と割り込み信号が無視されるようになる。
0xff は「1111 1111」なので、これで8bit全てが1になる = 割り込みが無効化される。
io_out8(PIC0_ICW1, 0x11 ); /* エッジトリガモード */ io_out8(PIC0_ICW2, 0x20 ); /* IRQ0-7は、INT20-27で受ける */ io_out8(PIC0_ICW3, 1 << 2); /* PIC1はIRQ2にて接続 */ io_out8(PIC0_ICW4, 0x01 ); /* ノンバッファモード */ io_out8(PIC1_ICW1, 0x11 ); /* エッジトリガモード */ io_out8(PIC1_ICW2, 0x28 ); /* IRQ8-15は、INT28-2fで受ける */ io_out8(PIC1_ICW3, 2 ); /* PIC1はIRQ2にて接続 */ io_out8(PIC1_ICW4, 0x01 ); /* ノンバッファモード */
ICWは「initial control word」=「初期化制御データ」 ICWは1~4があって、合計で4バイトのデータになる、らしい。
ICW1,3,4は飛ばして、重点的に説明されているICW2番のみ見ていく。 ICW2は、割り込み番号(アセンブラで INT xxx と書くと、何番のIRQが対応して動くか)をPICから指定する。
具体的には、PIC0_ICW2 ( 0x0021 ) に、io_out命令で0x20を入れると、 それがIRQ0に対応の割り込み番号と認識され、以降IRQ1は0x21、IRQは0x22というように インクリメントされて番号が付与されていく模様。 ここで番号を指定しておくことで、後段の割り込みハンドラ実装時に、 割り込み番号で制御対処のIRQポートを指定できるようになる、らしい。
これでマッピングはざっくり以下のようになる
harib03e: 割り込みハンドラ作成
0dで作ったPIC操作用のコードを元に、割り込みハンドラを実装していく。
マウスはIRQ12, キーボードはIRQ1なので、 03dで作成のマッピングに従い、INT 0x2c, INT 0x20でそれぞれ操作する。
割り込みハンドラは、dsctbl.cに登録する(21-24行目)
void init_gdtidt(void)
{
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT;
struct GATE_DESCRIPTOR *idt = (struct GATE_DESCRIPTOR *) ADR_IDT;
int i;
/* GDTの初期化 */
for (i = 0; i <= LIMIT_GDT / 8; i++) {
set_segmdesc(gdt + i, 0, 0, 0);
}
set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, AR_DATA32_RW);
set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);
load_gdtr(LIMIT_GDT, ADR_GDT);
/* IDTの初期化 */
for (i = 0; i <= LIMIT_IDT / 8; i++) {
set_gatedesc(idt + i, 0, 0, 0);
}
load_idtr(LIMIT_IDT, ADR_IDT);
/* IDTの設定 */
set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32); /* idtのアドレス+オフセット0x21のアドレスに、 (int) asm_inthandler21 を登録 */
set_gatedesc(idt + 0x27, (int) asm_inthandler27, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
return;
}
set_gatedescの引数を見ていく。
第1引数で設定されたアドレスは、 GATE_DESCRIPTOR構造体へのポインタになる。 つまり、「ここの番地に登録の構造体に0x21の設定を入れますよ」となる。
第2引数の (int) asm_inthandler21 がなぜ、(メモリアドレスの)オフセットに代入させるのかわからず苦悶していたが、 こちらの方は関数ポインタではないかと推測していた。関数名をintに変換すると、関数へのポインタになる? なるほどそうかも。。
第3引数の ‘2 * 8’ は、最終的に ’gd→selector’に代入されている。 セレクタはharib02gで書いた通り、CPUがメモリセグメントを探すときのインデックスとして機能する。 10進数2は、2進数 0010になる。 他方、10進数16は、1110
セグメントセレクタの下位3bitは、別の用途に使われ4bit目以降がIndex値として使われる。
なので、10進数16 = 1110、でセグメント番号2番を指し示すことになるらしい。
ディスクリプタ・テーブルのエントリ1個分は8バイトであるので、 「8*Index」で、ディスクリプタ・テーブルの先頭アドレスからのオフセット位置を示します。 http://d.hatena.ne.jp/yz2cm/20140502/1399012324
らしいので、セグメント番号1番 = オフセットなし、セグメント番号2番 = (8 * 1)分オフセット ということなのだろう。
第4引数の ‘AR_INTGATE32’ は、set_gatedescの引数 ‘ar’ として取得され、‘gd->access_right = ar 0xff;’ される。 値は、‘AR_INTGATE32’ の値は、bootpack.hで以下のように定義されている。
#define AR_INTGATE32 0x008e
0x008e = 1000 1110 0xff = 1111 1111 でAND演算される。
結果的にアクセス権に「このメモリは割り込み処理用に使いますよ!」という属性を付与することになるらしい。
AND演算というか、ビット演算をする意義については、あとで以下を読もう。。
【C言語入門】ビット演算子、シフト演算子の使い方(使い道も解説)
登録のために呼び出されている関数はやはり、dsctbl.c内で記述されている
void set_gatedesc(struct GATE_DESCRIPTOR *gd, int offset, int selector, int ar)
{
gd->offset_low = offset 0xffff;
gd->selector = selector;
gd->dw_count = (ar >> 8) 0xff;
gd->access_right = ar 0xff;
gd->offset_high = (offset >> 16) 0xffff;
return;
}
ここまでやると、各IRQへの割り込みに応じて asm_inthandler_xx_ が起動されるようになる。 このアセンブラ関数に対応する記述は、以下に書かれている。
14 GLOBAL _asm_inthandler21, _asm_inthandler2c
15 EXTERN _inthandler21, _inthandler2c
94 _asm_inthandler21:
103 CALL _inthandler21
ここの説明は以下ブログを参照。
CALLの説明
これは関数を呼び出す命令です。今回は、naskfunc.nasの中にかかれていない関数を呼び出さなくてはいけないので、 最初にEXTERN命令を使って、「今からこんな名前のラベルを使うけど、これは他のソースにあるからね~、間違いはないんだよー」とnaskに知らせています ということで EXTERN は CALL用に書かれているみたいです。C言語の関数をアセンブラ内での使用ためのものらしいです。 http://d.hatena.ne.jp/kobapan/20090501/1241132705
最後に、実際に呼び出されているアセンブラの関数の解説。
_asm_inthandler21:
PUSH ES ; ADD ESP, -4 / MOV [SS:ESP],ES , なのでESPレジスタの指し示すアドレスを-4し、そこにESの値を格納
PUSH DS
PUSHAD
MOV EAX,ESP
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler21 ; int.c内の void inthandler21(int *esp) を呼び出す
POP EAX ; MOV EAX, [SS:ESP] , ADD ESP,4 EAXレジスタに格納していたアドレスを戻したのちに、ESPレジスタの指し示すアドレスを+4する
POPAD
POP DS
POP ES
IRETD ; 割り込み終了時には、return = RET命令ではなく、IRETD命令を実行する必要があるとのこと
スタックの要点はざっくりと以下。
- スタックはFILO型のバッファ
- PUSHはバッファ上にデータを1つ乗せる
- POPはバッファ上からデータを1つ取り出す
また、PUSHDは以下の命令を合体させたもので、POPADはそのPOP版とのこと。
PUSH EAX
PUSH ECX
PUSH EDX
PUSH EBX
PUSH ESP
PUSH EBP
PUSH ESI
PUSH EDI
というわけで処理の流れとしては、
- 各レジスタの値をひたすらESPレジスタの持つスタックバッファに避難させていく
- DSとESレジスタにSSの値を代入後、C言語の関数の _inethandler21 を実行
- その後、レジスタの値をスタックからひたすらPOPして戻していく
- 最後にIRETD
という感じ。
項番2で呼び出される、 _inethandler21 は引数にESPレジスタを取っているが、 本章では特に気にしなくてOKとのこと。
C言語の型毎のビット桁数 / キャスト(明示的な型変換) / ビット演算
ビット演算を今まであまり理解していなかったので、メモ。
AND演算は、16bit maskでビット桁数を抜き取るために使うらしい。