BIOSとは?
BIOS(Basic Input/Output System)は、PCの電源を入れた直後に最初に動作するソフトウェアです。CPUやメモリ、ディスクなどの基本的なハードウェアを初期化し、最終的にはOSを起動するための「準備」を行います。今回はこのBIOSの一例として、オープンソースの「SeaBIOS」を使って、その動作の流れを追っていきます。
「起動直後にCPUが最初に実行する命令」から始まり、「周辺機器の初期化」や「割り込みベクタの構築」、そして「ブートの実行」まで、起動の全体像を俯瞰していきます。
※いきなり全てを読み解こうとするとコードの迷宮で迷子になってしまうため、一里塚となる様な要のコードに絞り紹介します。
以下のリンクから全コードが閲覧可能です。
https://github.com/coreboot/seabios
流れ
CPU起動直後
↓
リセットベクタ (0xFFFFFFF0)
↓
ljmp → BIOS ROMの entry_post
↓
maininit() → POST
↓
デバイス初期化 → 割り込みベクタ初期化
↓
do_boot → ブート順選定
↓
boot_disk() → INT 13hでMBRを読む
↓
call_boot_entry() → MBRに制御移行(0x7C00)
↓
OSのローダー起動
実装
【リセットベクタ】起動直後:0xFFFFFFF0の実行
0xFFFFFFF0から実行してPOST処理へ跳びます。
//・・・(略)・・・
ORG 0xfff0 // Power-up Entry Point
.global reset_vector
reset_vector:
ljmpw $SEG_BIOS, $entry_post
//・・・(略)・・・
初期化
周辺機器の初期化を行っています。
特に目を引くのは画面表示に関わるVGAの初期化です。
static void
maininit(void)
{
//・・・・中略・・・・
// ハードウェア初期化を開始
if (threads_during_optionroms())
device_hardware_setup();
// VGAオプションROMの実行
vgarom_setup(); // VGA BIOSの初期化
sercon_setup(); // シリアルコンソールの設定
enable_vga_console(); // VGAコンソールの有効化
//・・・・中略・・・・
// ブート前の最終準備
prepareboot(); // ブートプロセスの準備
//・・・・中略・・・・
// INT 19hでブートプロセスを開始
startBoot(); // ブートローダーの起動
}
ブート順の選択と実行(CD/USB等)
CDやUSBからOSを起動したい場合にBIOSから優先順位を変更するあれですね。
/* 次のブート方法を決定し、それを使用してブートを試みる */
static void
do_boot(int seq_nr)
{
//・・・・中略・・・・
// 指定されたBEV(Boot Execution Vector)タイプでブートを実行
struct bev_s *ie = &BEV[seq_nr];
switch (ie->type) {
case IPL_TYPE_FLOPPY: // フロッピーディスクからのブート
printf("Booting from Floppy...\n");
boot_disk(0x00, CheckFloppySig); // ドライブ00h, 署名チェックあり
break;
case IPL_TYPE_HARDDISK: // ハードディスクからのブート
printf("Booting from Hard Disk...\n");
boot_disk(0x80, 1); // ドライブ80h
break;
case IPL_TYPE_CDROM: // CD-ROMからのブート
boot_cdrom((void*)ie->vector);
break;
//・・・・中略・・・・
case IPL_TYPE_HALT: // ブート失敗時の処理
boot_fail();
break;
}
//・・・・中略・・・・
}
ブートローダの呼び出し(MBR実行) ※HDDの場合
① 読み込むセグメントの指定(0x7C00)
② INT 13h を使って MBR を読み込み
③ 読み込み失敗のチェック
④ 0x55AA の確認
⑤ セキュリティ記録(TPM、無視可)
⑥ ジャンプ先アドレス(CS:IP)を作成
⑦ far call でMBRのコードを実行
※0x7C00と55AAに関しては私の拙い説明よりも、他にうまく説明しているサイトや本がたくさんあるので調べてみてください。
歴史的な理由でブートローダはメモリの0x7C00に読み込まれます。ブートローダは512バイトと決まっており、満たない場合は0で埋め最後にバイトに55AAが書き込まれます。最後の2バイトが55AAでないとブートローダと見做されず起動されないのです。
// ディスク(フロッピーまたはハードディスク)からブートする関数
static void
boot_disk(u8 bootdrv, int checksig)
{
// BIOS伝統のMBR読み込み先セグメント:0x07C0(→ 実際のアドレスは 0x07C0:0000 = 0x7C00)
u16 bootseg = 0x07c0;
// BIOS割り込み呼び出しのためのレジスタ構造体
struct bregs br;
memset(&br, 0, sizeof(br)); // 初期化(全部0クリア)
// 割り込み許可、ドライブ番号の設定
br.flags = F_IF; // 割り込み有効
br.dl = bootdrv; // 対象のブートドライブ(例:0x80 は HDD)
// 読み込み先のセグメント設定
br.es = bootseg; // ES:BX の ES 部分
// BIOS割り込み INT 13h の読み込み命令を指定
br.ah = 0x02; // AH=2:読み込み命令
br.al = 0x01; // AL=1:1セクタだけ読む
br.cl = 0x01; // セクタ番号(CHとCLでセクタ指定 → 最初のセクタ)
// BIOS割り込み INT 13h を実行(ディスクからMBR読み込み)
call16_int(0x13, &br);
// 読み込みに失敗した場合(CF=1)
if (br.flags & F_CF) {
printf("Boot failed: could not read the boot disk\n\n");
return;
}
// MBRの目印を確認(checksigフラグが立っていれば)
if (checksig) {
struct mbr_s *mbr = (void*)0; // MBR構造体のポインタ(仮に 0 番地とする)
// MBRの末尾2バイトが 0xAA55 であるか確認(ブート可能ディスクの証)
if (GET_FARVAR(bootseg, mbr->signature) != MBR_SIGNATURE) {
printf("Boot failed: not a bootable disk\n\n");
return;
}
}
tpm_add_bcv(bootdrv, MAKE_FLATPTR(bootseg, 0), 512);
// bootseg:bootip を「正規化」する処理(セグメントとオフセットを分離)
u16 bootip = (bootseg & 0x0fff) << 4; // セグメントの下12bitをシフトしてオフセットに変換
bootseg &= 0xf000; // セグメントとオフセットに分離(0x07C0:0000 → 0x0000:7C00 形式に変換)
// 実際に MBR のコード(0x7C00)へ移り、制御を渡す
call_boot_entry(SEGOFF(bootseg, bootip), bootdrv);
}
#define MBR_SIGNATURE 0xaa55
BIOS割り込みのためのIVT(割り込みベクタテーブル)初期化
割り込み番号,実装されたコードのアドレスを登録していきます。
0x10:画面処理
0x1A:時刻処理
等
ivt_init(void)
{
dprintf(3, "init ivt\n");
int i;
for (i=0; i<256; i++)
SET_IVT(i, FUNC16(entry_iret_official));
//・・・・中略・・・・
// Initialize software handlers.
SET_IVT(0x02, FUNC16(entry_02));
SET_IVT(0x05, FUNC16(entry_05));
SET_IVT(0x10, FUNC16(entry_10));
SET_IVT(0x11, FUNC16(entry_11));
//・・・・中略・・・・
割り込みベクタテーブルとは何か
BIOS割り込みの説明
BIOS割り込みを使用すると int+割り込み番号 でBIOSの機能を呼び出せます。
(BIOS割り込みとは簡単に言うとBIOSが用意した関数です。呼び出すだけで色々な処理が簡単に書けます。)
割り込みを利用しないと制御したいハードウェア毎に固有のアドレスやポート番号を直接指定して操作しなくてはなりません。
文字表示 int 0x10
例えば画面上に文字を表示した場合は 0x10 を使用して以下のように書けます。
mov ah, 0x0E ; テキスト表示機能
mov al, 'X' ; 表示する文字
int 0x10 ; BIOS画面処理を呼び出し
mov ax, 0xB800
mov es, ax
mov di, 0 ; 左上の位置
mov ah, 0x0F ; 白地(明るい灰色)
mov al, 'A'
stosw ; AXをES:DIに書き込み(A+属性)
キーボードの値取得 INT 0x16
mov ah, 0x00 ; キー入力待機機能
int 0x16 ; BIOSキーボード割り込み
mov al, [key] ; ALにASCIIコードが入る
wait_key:
in al, 0x64 ; キーボードコントローラの状態
test al, 0x01 ; データ有無チェック
jz wait_key ; データなしならループ
in al, 0x60 ; ポート0x60から直接読み取り
時刻取得 int 0x1A
mov ah, 0x02 ; 時刻取得機能
int 0x1A ; RTC割り込み
; CH=時間, CL=分, DH=秒
mov al, 0x04 ; 時間のレジスタ番号
out 0x70, al ; CMOSアドレスポート
in al, 0x71 ; CMOSデータポート
mov [hour], al ; バイナリ形式で取得
割り込みベクタテーブルの本質
割り込みが発生したとき、「どのアドレスへ制御を移すか」をあらかじめ登録しておく仕組みが割り込みベクタテーブルです(索引のようなもの)。
たとえば 0x〇〇 番の割り込みが実行されたら、予め指定されたアドレスへ跳びます。
跳んだ先のアドレスには、対応する機能を実現するためのコードが予め用意されています。
MBRの説明
PCの起動時、BIOSはハードディスクの最初のセクタ(512バイト)を読み込みます。この先頭セクタが「MBR」と呼ばれ、BIOSはこれを0x0000:0x7C00(リニアで0x7C00)にロードして、そこから実行を始めます。
512バイトという制限があるため、より大きなプログラム(例えばOSのカーネルなど)を読み込む場合は、jmp命令やBIOSのディスク読み込み機能(INT 13hなど)を使って他のセクタにジャンプし、続きのコードを読み込みます。
[BITS 16]
[ORG 0x7C00] ; BIOSがMBRをロードする位置に合わせる
start:
cli ; 割り込み禁止
;・・・・中略・・・・
hang:
jmp hang ; 無限ループ(BIOSに制御を戻さない)
;・・・・中略・・・・
times 510 - ($ - $$) db 0 ; MBRは512バイト固定。残りを0で埋める
dw 0xAA55 ; ; BIOSが「ブート可能」と認識するための目印!
最後に
調べながら書いたので間違っているところや不明瞭なところがあると思います。
この記事にあまりにも時間を掛けすぎたので一旦あげますが、これから詳しく
seabiosを読み込んでいく予定ですので近々第二弾を上げると思います。
今回の記事は多少ふわっとしていますが、第二弾ではもっと詳しく解説します!