PDP-11のブートストラップ

年の瀬も押し迫った頃、なぜかPDP-11のブートストラップが気になってしまったので、simhとPDP-11のプロセッサハンドブックを見ながら、調べてみた。

今回は実機のイメージがあった方が理解しやすいと思い、Wikipediaから写真引用。これは2基のDECtapユニット(TU56)を装備したPDP-11/40である。TU56はブロックアドレッシング可能なテープで、144K 18 bit wordの記憶容量を持っていた。ブロックサイズは256 18 bit word。余談だが、TU11という9トラックのテープユニットもあった。こちらはPCの外部記憶媒体にカセットテープを使っていた頃を思い出すなぁ。また、この頃のハードディスクはプラッタとヘッダが分離されていて、プラッタのみを取り替えることができた。これがDECpack(RK05)で、ヘッダがある方のコントローラはRK11と呼ばれた。容量は2.5MBだった。写真はこちらとか、探せばいろいろ見つかる。そして、本エントリの最後でも触れるが、一番下に見えるプロセッサコンソールのスイッチを使って、ブートストラップローダを手入力することもできた。

まず、Lions' commentaryの6章冒頭を読むと、UNIXが起動するまでには、次のような流れになることがわかる。

  1. (ROM上の)ブートストラップローダプログラム
  2. (システムディスクのブロック#0以降に格納された)ローダプログラム
  3. ファイルシステム上の)UNIXカーネル

ここでは順にブートストラップローダ、uboot、UNIXカーネルと呼ぶことにするが、説明の都合上、実行順とは逆のUNIXカーネルから追っていくことにする。

UNIXカーネルは0番地から始まる低位アドレスにロードし、0番地から実行を始めることになる。ソースコードでは507行目の「br 1f」が最初に実行される命令だ。1fはラベルなので、実際の機械語は「br 40」相当になる。

0507  . = 0^.
0508    br 1f

このUNIXカーネルをシステムディスクから読み込んで、制御を移すローダがubootである。正確にはubootはファイルシステムからカーネルを読み込むローダで、生のディスクやテープからカーネルをロードするmbootやtbootというプログラムも存在したようだが、ここでは割愛する。ubootのソースコードは/usr/source/mdec以下にある。コンパイル済みのプログラムは/usr/mdecに格納される。RKハードディスクドライブ用のubootのファイル名は、rkubootだ。rkubootはmkfsでディスクの先頭に書き込まれるようだ。rkubootをodしてみればわかるが、先頭の「000407」が0番地に書き込まれる。これは「br .+7」で020番地に飛ぶことを意味する。

# od < rkuboot
0000000 000407 000676 000000 000000 000000 000000 000000 000001
0000020 012706 137000 010601 005000 020701 103012 021027 000407
0000040 001002 012700 000020 012021 020127 140000 103774 000116
0000060 005020 020006 103775 012705 137620 012700 000100 004715
0000100 012702 136046 010201 004767 000346 020027 000012 001412
0000120 020027 000057 001402 110021 000766 020201 001764 062702
0000140 000016 000760 012702 136046 012700 000001 005067 176702
0000160 004767 000112 005712 001430 004767 000152 000711 012701
0000200 135040 010203 010104 062701 000020 005724 001411 122324
0000220 001007 020401 103774 016100 177760 062702 000016 000746
0000240 020127 136040 103756 000750 005002 004767 000070 000555
0000260 012701 135040 012122 020127 136040 103774 000766 062700
0000300 000037 010005 072027 177774 004767 000132 042705 177760
0000320 072527 000005 062705 135040 012704 135000 012524 020427
0000340 135030 103774 000207 062716 000002 016700 176504 005267
0000360 176500 032767 010000 175430 001007 006300 016000 135010
0000400 001022 162716 000002 000207 005046 110016 105000 000300
0000420 006300 016000 135010 001765 004767 000012 012600 006300
0000440 016000 135040 001756 010067 176412 000475 135040 177400
0000460 105737 177560 002375 016700 040110 042700 177600 020027
0000500 000101 103405 020027 000132 101002 062700 000040 020027
0000520 000015 001002 012700 000012 020027 000012 001005 012700
0000540 000015 004715 012700 000012 105767 040030 100375 010067
0000560 040024 000207 117600 000000 001403 004715 005216 000772
0000600 062716 000002 042716 000001 000207 005000 021027 000407
0000620 001004 016020 000020 020006 103774 012746 137000 005007
0000640 000733 000706 000747 016701 176212 005000 071027 000014
0000660 072027 000004 050100 012701 177412 010011 016741 177554
0000700 016741 177552 012741 000005 105711 100376 000207

最後にブートストラップローダに戻ろう。ブートストラップローダもテープやハードディスクごとに異なる。ここはsimhのソースコードから探ってみたい。simhでUNIX v6を起動する場合、「boot rk0」のように起動デバイスを引数にbootコマンドを実行する。実はこれ、simhの内部ではデバイスごとのブートROMを実行している。例えば、RKディスクの場合は、pdp11_rk.cのboot_rom[]がその内容である。次に示す通りの短いコードだしコメントもあるので、読むのはそれほど難しくないはずだ。ユニット番号はbootコマンドの引数を見て修正される。このboot_romを実行することで、ディスクの#0ブロック、つまりrkubootを0番地以降にコピーして、そこにジャンプしている。これでrkubootに制御が移る。

余談だが、PCを0にセットするために、clr命令を使っている。これはPCが汎用レジスタだからこそできる芸当である。もちろん様々なアドレッシングモードも使える。例えば、2行目の「0012706」だが、「01」はmov命令のオペコードである。mov命令は2オペランド命令で、「01 ss dd」の形式である(ssはソースレジスタ、ddはデスティネーションレジスタ)。そしてオペランドの前半3ビットはアドレッシングモード、後半3ビットはレジスタの指定になる。この場合、ssの「27」は、モード2のr7、つまりPCを示す。PCのモード2は即値モードで、次のワードBOOT_STARTがソースになる。続いてddの「06」はモード0のr6で、デスティネーションはr6になる。ここまで読んだ方は気付くと思うが、PDP-11の命令セットは8進数と相性がよく、8進数で書かれると素直に読むことができる。この辺は初期のUNIXにも影響を与えていたはずで、UNIX v6のコードに8進数がよく登場するのはこのためだと思う。

static const uint16 boot_rom[] = {
    0042113,                        /* "KD" */
    0012706, BOOT_START,            /* MOV #boot_start, SP */
    0012700, 0000000,               /* MOV #unit, R0        ; unit number */
    0010003,                        /* MOV R0, R3 */
    0000303,                        /* SWAB R3 */
    0006303,                        /* ASL R3 */
    0006303,                        /* ASL R3 */
    0006303,                        /* ASL R3 */
    0006303,                        /* ASL R3 */
    0006303,                        /* ASL R3 */
    0012701, 0177412,               /* MOV #RKDA, R1        ; csr */
    0010311,                        /* MOV R3, (R1)         ; load da */
    0005041,                        /* CLR -(R1)            ; clear ba */
    0012741, 0177000,               /* MOV #-256.*2, -(R1)  ; load wc */
    0012741, 0000005,               /* MOV #READ+GO, -(R1)  ; read & go */
    0005002,                        /* CLR R2 */
    0005003,                        /* CLR R3 */
    0012704, BOOT_START+020,        /* MOV #START+20, R4 */
    0005005,                        /* CLR R5 */
    0105711,                        /* TSTB (R1) */
    0100376,                        /* BPL .-2 */
    0105011,                        /* CLRB (R1) */
    0005007                         /* CLR PC */
    };

では、simhを使って、ブートROMからUNIXの起動までを追ってみよう。ブートROMのエントリは02002番地なので、ここにブレークポイントをはる。ステップ実行すれば、上記のブートROMが実行されていく様子がわかるだろう。

sim> set cpu 11/40
sim> att rk0 v6root
:
sim> br 02002
sim> boot rk0

Breakpoint, PC: 002002 (MOV #2000,SP)
sim> s

Breakpoint, PC: 002006 (MOV #0,R0)

simhのコマンドとして最低限覚えておけばよいのは、e(xamine)、s(tep)、c(cont)、br(eak)、nobr(eak)、q(uit)ぐらいかな。レジスタやメモリの内容を知るにはeコマンドを使う。PCやr0が見たければ、「e pc」とか「e r0」、メモリは範囲して可能で、「e 2000-2100」とか。

では、ubootへ遷移する箇所を見ていこう。ステップ実行を続けるか、「clr pc」のアドレスにブレークポイントをはるかして、PCを進める。そして、0番値あたりのメモリを覗いてみよう。最初はゼロだったが、さきほどのブートROMにより何かが書き込まれている。そう、これがubootなのである。前述のodの内容と見比べて欲しい。

sim> br 2070
sim> c

Breakpoint, PC: 002070 (CLR PC)
sim> e 0-40
0:	000407
2:	000676
4:	000000
6:	000000
10:	000000
12:	000000
14:	000000
16:	000001
20:	012706
22:	137000
24:	010601
26:	005000
30:	020701
32:	103012
34:	021027
36:	000407
40:	001002

ステップ実行を続ければ、ubootに遷移するのがわかる。

sim> s

Step expired, PC: 000000 (BR 20)
sim> s

Step expired, PC: 000020 (MOV #137000,SP)

さて、次はUNIXカーネルのエントリである0番地にブレークポイントをはって、実行を継続する。予想通りに行けば、ubootによってディスクからカーネルが0番地以降に読み込まれるはずだ*1

さてはて。おなじみの@プロンプトが出た。これはubootの仕事だったんだね。で、カーネルのファイル名を入力すると、ビンゴ! ブレークポイントに引っかかった。ここからカーネルのお仕事の始まりである。

sim> nobr 2002,2070
sim> br 0
sim> c
@unix

Breakpoint, PC: 000000 (BR 40)

今日は、もう一息頑張ってみよう。PDP-11にはプロセッサコンソールがあるので、ブートROMがなくても、ブートストラッププログラムを手入力して実行できた。simhでもdコマンドでそれをエミュレートしている。d命令は第一引数のメモリアドレスに、第二引数の内容を書き込むという意味だ。「boot rk0」の代わりに、次のステップでRKドライブのユニット#0番限定だが、ちゃんとubootをロードして、UNIXを立ち上げることができる。

; boot rk0
d 100000 012700  ;   mov #rkda, r0
d 100002 177412  ;
d 100004 005040  ;   clr -(r0)
d 100006 010040  ;   mov r0, -(r0)
d 100010 012740  ;   mov #5, -(r0)
d 100012 000005  ;
d 100014 105710  ; 1: tstb (r0)
d 100016 002376  ;   bge 1b
d 100020 005007  ;   clr pc
d pc 100000
go

年末、年始の時間があるときに、PDP-11の機械語アセンブリと戯れてみるというのも乙かもしれない。

(追記:2010-12-31)ubootを0番地から書き込んで、そこからブートすると聞いて、普通、a.outでもそれは無理じゃない?何か細工しているのでは?と気付いた方はなかなかするどい*2
実は「0407」というbr命令はa.out形式のマジックナンバー(a.outにもいくつかバージョンがあるので、正確にはOMAGICと呼ばれる)なのである。a.outヘッダは固定長なので、このbr命令はテキストセグメントの先頭にジャンプすることを意味する。ということで容量的にも制限のあるブートストラップローダがまじめなa.outローダを書かなくてもどうにかなっているのである。ちなみにubootの方はちゃんとa.out形式を解釈して、カーネルをロードしているようだ。

*1:ここで自分自身を上書きすることになるが、その問題をどうやって回避しているかは、あとで調べる。

*2:x86ブートローダを作ったことがある人は、objcopyを使ってELFからバイナリ形式に変換したりしなかっただろうか。