電源ONからブートローダ起動までを追いかける

計算機科学に対しての興味は尽きない。
年々知りたいことが増えていくのに全然追いつけない。

というわけで懲りもせず、最近のイケてるエンジニアには見向きもされない地味なところを追いかけてます。
やっぱりローレベルなところが好きみたいです。

スポンサーリンク

おさらい

ここであらためて、PCがどうやって起動するのかについておさらい。

アーキテクチャ等によって色々あるけど、一般的なPC-AT互換機の場合
RESET(電源ボタン) → BIOS/UEFI → ブートローダ → Linux Kernel
といった順序で基準します。

電源を入れるとまず、BIOSが実行されます。BIOSは、MBR (Master Boot Record) 領域に書き込まれているブートローダを起動。ブートローダは、カーネルを読み込み実行します。以上。

まぁ、この全体の流れはなんとなくわかる。でもこれじゃあ何も理解したことにならないので、もう少し深掘りしてみます。

電源ボタンからBIOSまで

電源が投入されて、+12VDC +5VDC +3.3VDC が規定(たいていの場合、定格の95%)を満たすと、
ATX電源からMotherBoardに PWR_OK が送られます。

この信号を受けると、MotherBoardは CPU を起動し、レジスタに初期値をロードします。
この初期値から、リセットベクタのアドレスが割り出されて、リセットベクタに置かれたJMP命令の飛び先に配置されたBIOSが起動します。

このあたりのプロセスについて掘り下げる余地はまだまだありそうだけど、
ハードの話になっちゃいそうなので、まぁひとまずは良いでしょう。

BIOSからブートローダまで

BIOSはマザーボードなどに格納されているプログラムです。
Basic I/O System という名前が示す通り、基本的なハードウェアの入出力の制御ができます。

BIOSが起動すると、POST (Power On Self Test)と呼ばれるハードウェアのチェック等を行ないます。
POSTが問題なく終了すれば、起動デバイスのMBRからブートローダを読み込み、制御を移します。

実際には、MBRに置けるのは 512 バイトまでなので、MBRからさらに別のどこかに置いてあるブートローダプログラムを実行して、そこからKernelを起動します。

MBRにはブートデバイスの最初の512バイトが割り当てられています。

最後の2バイトはBIOSに対してこのデバイスがbootableであることを知らせるための、ブートシグニチャになっています。

ブートシグニチャは 0x55 0xaa です。

この2バイトが設定されていなければ、「No bootable device」 とか
「起動可能なディスクが見つかりません」といったメッセージが出て、起動が止まることになります。

このブートセクタが読み込まれるアドレスですが、0x00007c00 から 0x00007dff と決められています。

ということで、実際に見てみます。

ブートローダ擬き(何もしない)を作ってみる

ブートローダの動きを理解するために、NASMでMBRから起動するプログラムを作ってみます。

まずはブートシグニチャなし版。

USE16
ORG    0x7C00
times  512-($-$$) DB 0

コンパイルしたら、qemuで起動させてみましょう。

$nasm -f bin boot.asm
$qemu-system-x86_64 boot

いろいろなデバイスでの起動を試した痕跡があります。そして最後に「No Bootable Device」と出ました。

ブートシグニチャあり版を作ってみます。
最後の2バイトを 0x55 0xAA にしただけです。

USE16
ORG    0x7C00
times  510-($-$$) DB 0
DB     0x55
DB     0xAA

今度はエラーメッセージは出なくなりました。

このままではよくわかりませんので何か文字を出してみます。

文字を表示するには、BIOSが用意してくれた割り込み処理を使います。

AH = 0x0E
AL = 出力する文字
BH = Page番号
BL = 文字の色
各レジスタをセットしたら、INT 0x10 をコールします。

愚直に、Hello! と出力させてみます。

USE16
ORG    0x7C00
 
MOV    ah, 0x0E
MOV    bh, 0x00
MOV    bl, 0x07
 
MOV    al, 'H'
int    0x10
MOV    al, 'e'
int    0x10
MOV    al, 'l'
int    0x10
MOV    al, 'l'
int    0x10
MOV    al, 'o'
int    0x10
MOV    al, '!'
int    0x10
 
times  510-($-$$) DB 0
DB     0x55
DB     0xAA

ということで、ここまでで終わりにしようと思いましたが、これではあまりにもアレなので、もう少しまじめに書いてみます。

USE16
ORG 0x7c00
 
msg:
  DB      0x0a, 0x0a
  DB      "Hello World!"
  DB      0
 
entry:
  MOV     SI, msg
  MOV     AH, 0x0e
  MOV     BH, 0x00
  MOV     BL, 0x07
 
cput:
  MOV     AL, [SI]
  ADD     SI, 1
  CMP     AL, 0
  JE      sleep
  INT     0x10
  JMP     cput
 
sleep:
  HLT
  JMP     sleep
 
fill:
  times  510-($-$$) DB 0
  DB     0x55
  DB     0xAA
$nasm -f bin boot.asm
$qemu-system-x86_64 boot

まとめ


MBRに置かれたプログラムは、0x00007c00 番地から始まり、最後の2バイトが 0x55 0xaa の512バイトの機械語の塊である。

ということがわかりました。

付録 & 次回予告?

リアルモード時のメモリマップはこうなっています。

0x00000000 - 0x000003FF - Real Mode Interrupt Vector Table
0x00000400 - 0x000004FF - BIOS Data Area
0x00000500 - 0x00007BFF - Unused
0x00007C00 - 0x00007DFF - Bootloader
0x00007E00 - 0x0009FFFF - Unused
0x000A0000 - 0x000BFFFF - Video RAM (VRAM) Memory
0x000B0000 - 0x000B7777 - Monochrome Video Memory
0x000B8000 - 0x000BFFFF - Color Video Memory
0x000C0000 - 0x000C7FFF - Video ROM BIOS
0x000C8000 - 0x000EFFFF - BIOS Shadow Area
0x000F0000 - 0x000FFFFF - System BIOS

ここから、どうにかカーネルをロードしたいのですが、
未使用領域 0x00000500 – 0x00007BFF をいっぱいいっぱい使っても 638k バイトしかありません。
リアルモードでは 0x0010FFEF までしかアクセスできないので、そこから先に広がるメモリ領域にカーネルを置けません。

ということで、32bitとか64bit使えるモードへ移行する必要が出てきます。
ということで、次は移行してカーネルをロードするまでを追いかけたいです。

参考

PhoenixBIOS 4.0
Programmer’s Guilde
http://www.msc-technologies.eu/fileadmin/documentpool/Support-Center/Archive/70-PISA/PISA-P/02-Manual/BIOS%20Programmer’s%20Guide%20v10.pdf?file=&ref=

INTEL 80386
PROGRAMMER’S REFERENCE MANUAL 1986
http://css.csail.mit.edu/6.858/2014/readings/i386.pdf

Minimal Intel Architecture Boot Loader
Bare Bones Functionality Required for Booting an Intel Architecture Platform
https://www.cs.cmu.edu/~410/doc/minimal_boot.pdf

The Net Wide Assembler : NASM
http://www.nasm.us/doc/nasmdoci.html

スポンサーリンク

フォローする