First Edition UNIX(V1 UNIX)

ターゲットが PDP-7 から PDP-11 になったことで完全にコードが書き直されている。 アセンブラで開発する以上避けられない問題だがこれは非常に負担が大きいよね。

それもあって Ken Thompson は機種依存しないプログラミング言語 B を作り一部のコマンドの実装に使っていたのだけど、かなり限定的なものだった。

そしてカーネルは機種依存コードが多いのでいまだ全てアセンブラである。

前回同様に OCR でソースコード復元し詳細なコメントをつけたバージョンが github/DoctorWkt/unix-jun72 から利用可能となっている(以前は Google Code で公開してた)。

ただしこれ V1 UNIX のカーネルに V2 UNIX のユーザーランドというチャンポンのようである。 そもそも C コンパイラがあるんだがプログラミング幻語 C の登場は V2 UNIX のはずである。

どうも断片的にしかコードが現存していないみたいね、バージョン管理じゅーよー。 当時ベル研には 1972 年から SCCS(Source Code Control System) が存在してたけども IBM System/370 上で動くもので UNIX への移植はまだだったのである。

ところでこいつも実は tools 以下の方が面白いんじゃないか説あると思います。

  • apout … PDP-11 上の V1/V7 UNIX/2.11BSD のバイナリエミュレータ
  • disaout … PDP-11 ディスアセンブラ
  • mkfs … V1 UNIX ファイルシステム作成

ただ apout は俺のメイン環境である Cygwin だと足りないシステムコールなどがあり修正量ちょいと多めであった。 プラスして python3 で動かなくなったスクリプトを対策した差分をご用意したので使ってくれ、まぁいまだに Cygwin 使ってる人間がいるのか謎ではあるが。

足りないものはなるべく代替機能に置換えたつもりだけど欠けたままのもいくつか。 まぁ V1 UNIX のビルドに as(1) 動かす程度なら問題無さそうだし気にせんでええ。

まずは資料を用意する

PDP-11 の資料も ここ に大量に転がってるけれど、多過ぎてどれ読めばいいやらである。

ベストセラー機なので、学生向けの「 やさしくスッキリわかるこれからはじめる独習 PDP-11 超入門 」みたいな冊子があるからそれ読めば完全理解できそうではある。

だがいまさら PDP-11 のお仕事なんて老朽化した原子力発電所とかプラントとかやべーところだから胃が牛の数だけあっても神経持たんのである。

カーネルソースコードの構成

カーネルソースは pages 以下にあるのだが、OCRしたテキストそのまんまで詠む用では無さげ。

そんでトップディレクトリで make all を実行すると build/ 以下に

  • 不鮮明な印刷による OCR の認識ミス
  • 元コードのバグなんかの修正

の差分が当たったカーネルソースが生成されるもよう。

$ ls build/*.s
build/init.s  build/u1.s  build/u4.s  build/u7.s  build/ux.s
build/sh.s    build/u2.s  build/u5.s  build/u8.s
build/u0.s    build/u3.s  build/u6.s  build/u9.s

このうち sh.s と init.s はそれぞれ sh(1) と init(8) のコードなので無視してよろしい。

いちいちビルドしたくねえというものぐさは UNIX Archive に成形済ファイルあるのでそっち参照の事。

ux.s

 1 / ux -- unix
 2 
 3 systm:
 4 
 5 	.=.+2
 6 	.=.+128.
 7 	.=.+2
 8 	.=.+64.
 9 	s.time: .=.+4
10 	s.syst: .=.+4

要はカーネルで使う記憶域(変数)一覧である。

前回の PDP7-UNIX でいえば sysmap のうち記憶域のラベルを抜き出したものに近いのだが

  • ラベルでグループ分けしたり
  • アドレスを直値で書かずバイト単位のオフセットで書けたり

と 0 歳児が 1 歳児になって言葉のようなものを喋りはじめたかのようである。 あーうーいってるだけで何言ってんのかさっぱり判んねえんだけどな!

すでに s.time の文字がみえてるので先は短そうである、+4 つまり 4 バイト割り当てられている。

PDP-11 は 16bit ワードなので バイトは 8bit よって s.time は 32bit となる。

そういえば PDP-7 では + がワード単位だったのに PDP-11 ではバイト単位なのがプログラミング幻語 C を感じませんかね、よう知らんけど。

ところで sysmap にあったシステムコールのラベルはどこいった?と頭ひねる読者の方もいるだろう(鏡と会話)、答えは sop.s のニーモニック表と一緒に as(1) のビルトインとなり不要となった、である。

なんと as(1) は 2 パスなのだが最適化あるんかな、こっちは pass1

42 / register
43 
44 072270;000000;24;000000 /r0
45 072340;000000;24;000001 /r1
46 072410;000000;24;000002 /r2
...
70 / system calls
71 
72 021411;076400;01;0000001 /exit
73 023752;042300;01;0000002 /fork
74 070511;014400;01;0000003 /read

こっちは pass2

12 / register
13 
14 24;000000 /r0
15 24;000001 /r1
16 24;000002 /r2
...
40 / system calls
41 
42 01;0000001 /exit
43 01;0000002 /fork
44 01;0000003 /read

シンボルテーブル見つからんかったがコメントから動的に作ってるのかなこれ。 さすがに今読む気にならねえ…

u[連番].s

相変わらずやる気ねえ命名のファイル群、こいつらがカーネルソースコードである。

プログラムの開始地点を探す

PDP7-UNIX での豊富な経験(ペラッペラ)から安直に u[連番].s の先頭を開いてみた。

1 / u0 -- unix
2 
3 cold = 0 
4 orig = 0 . / orig = 0. relocatable
5 

またしても orig の文字が見えたので多分ここやろガハハ。

PDP-7 だとハードウェア I/O は 0 から 18 番固定で 7 だとか 6 だとか直値で叩いてたけれど、PDP-11 は Unibus という方式で先頭 8k バイトが MMIO(Memory-Mapped I/O) 領域となっているそうである。 詳しくは

を参照のこと。

6 rkda = 177412 / disk address reg            rk03/rk11
7 rkds = 177400 / driv status reg             rk03/rk11
8 rkcs = 177404 / control status reg          rk03/rk11
9 rcsr = 174000 / receiver status reg         dc-11
10 rcbr = 174002 / receiver buffer reg         dc-11
...

よって PDP-7 とは異なり自分で機器を列挙する必要があるわけである。

なお後期型になると Massbus というものも登場するそうだが生憎俺はバスマニアじゃないので無視する。

横浜市なのに市バスでなく神奈中バスが走る棄民の地。 さらに走る道路ときたら県内渋滞ワースト1位つーかおそらく都心抑えて日本一。 ラッシュ時は駅まで 1 - 2 時間で事故渋滞ともなれば 4 時間(通常 20 分)。 雨の日の混雑といったら朝の田園都市線すらソーシャルディスタンスってレベルの高圧縮率。 こんな僻地住んでるとバスは憎悪の対象なのである(突然 PTSD を発症する患者)。

もう少し下を読んでいくと RTC(Real-Time Clock) と思われる lks と名付けられたレジスタを発見。

 20 lks  = 177546 / clock status reg            kw11-l
...
135 / set devices to interrupt
136 
137 	mov    $100,*$lks / put 100 into clock status register;
138                            / enables clock interrupt

何の略かは不明であるが「Lealtime KlockS」という日本の底辺SIerでよく目撃するスペルミスの可能性が微粒子レベルで存在する、実は ken って名前もそれっぽいし日本人なのでは?(陰謀論者しぐさ)

前述の PDP-11 UNIBUS Proccessor Handbook の 2-33 を読むと

  • RTC は Line-time Clock という名前
  • そのレジスタ名が Clock Status Register (LKS)

となっとるので謎である、プロセッサ名も J-11 で無関係みたいだしなぁ。 DECお得意の i18n/l10n みたいな数略語 (numeronym)かもしれぬ。 k=1024 よって l1024s つまり l ではじまり s で終わる長さ 1026 の単語かもしれない。

さらに DEC 技術者の謎のこだわりを感じたのが lks の 0~15bit がそれぞれ意味する事の解説。

[15] <- UNUSED
...
[ 7] <- LINE CLOCK MONITOR
[ 6] <- LINE CLOCK INTERRUPT EN
...
[ 0] <- UNUSED

6 と 7bit 目が PDP-7 の RTC I/O の 6 と 7 番に対応してるのねこれ。

最初このコードって PDP-7 は 1/60 秒固定だけど PDP-11 は可変で 1/100 秒つまり 100Hz でタイマー割込を発生させるんかな?と思ったんだけどこれ 10 進でなく 8 進で 6bit 目をオンにしたってこと。

そしてクロック割込ハンドラは clock というラベルのもよう。

59  . = orig+60
...
64        clock;340 / clock interrupt vector       ; processor level 7

340 が processor level 7 ってのは PDP-11 の割込優先レベルには 0 から 7 まであり 最も優先度が高い割込という事ね。

clock ラベルを読む

じゃあクロック割込ハンドラのコード読むよ。

11 clock: / interrupt from 60 cycle clock
12        mov     r0,-(sp) / save r0
13        tst     *$lks / restart clock?
14        mov     $s.time+2,r0 / increment the time of day
15        inc     (r0)
16        bne     1f
17        inc     -(r0)

まず汎用レジスタの r0 を退避し RTC を再起動する。

次に r0s.time の下位 16bit を置いて加算しオーバーフローフラグ立ってたら -(r0) つまり s.time の上位 16bit を加算ちゅーこと。

なんかエンディアンおかしくね?って思ったそこのあなた(無)、これは PDP-11 endian というやつ。 リトルとビッグの混成で 0x01234567 は以下のバイト列になるのだ。

endian 0 1 2 3
big 0x01 0x23 0x45 0x67
little 0x67 0x45 0x23 0x01
pdp-11 0x23 0x01 0x67 0x45

はい、今回も s.time は 1/60 秒ごとに加算される値で 32bit だから 828 日つまり 2 年と 3 ヶ月でターンオーバーするってこと。

stime(2) システムコールが増えた

時刻を取得する time(2) に加えて時刻を変更する stime(2) が追加された、現在の settimeofday(2) 相当の機能ですな。

408 systime: / get time of year
409 	mov	s.time,4(sp)
410 	mov	s.time+2,2(sp) / put the present time on the stack
411 	br	sysret4
412 
413 sysstime: / set time
414 	tstb	u.uid / is user the super user
415 	bne	error4 / no, error
416 	mov	4(sp),s.time
417 	mov	2(sp),s.time+2 / set the system time
418 	br	sysret4

システムコールのプリフィクスが .* から sys* に変更になってる。 とはいえ PDP-7 でバイナリ互換無いから問題にはならんのである。

これで特権ユーザーによる時刻変更が可能になった、ようやっとまともな時計カレンダー機能を UNIX は獲得したのである。

date(1) の誕生

前回 PDP7-UNIX 上で動かした date(1) は後世に追加されたもので正しい現在時刻を表示できなかった。

しかし今度はちゃんと date(1) が実装されました。

NAME              date -- print the date

SYNOPSIS          date

DESCRIPTION       The current date is printed to the second.
NAME              sdate -- set date and time

SYNOPSIS          sdate mmddhhmm

DESCRIPTION       sdate adjusts the system's idea of the date and time. mm is
                  the month number; dd is the day number in the month; hh is
                  the hour number (24--hour system); mm is the minute number.
                  For example,

                               sdate 10080045

                  sets the date to Oct. 8, 12:45 AM.

マニュアルには sdate(1) って謎のコマンドが存在する。 だが実際に simh 上で V1 UNIX 動かしてみると date(1) で時刻変更が可能だし この sdate(1) を実行しようとすると

# sdate 02211920
No command
#

と怒られるので実装とドキュメントの不一致なんやな。 ユーザーランドは V1 と V2 のチャンポンだから V1 では sdate(1) が存在して V2 で date(1) に統合されたのかなぁ。

なるほど現在でも date(1) が date [MMDDhhmm[[CC]YY][.ss]] というクソ書式でマニュアル読まないと思い出せないのは V1 UNIX 時代からの互換性のためだったのか。

コードの解説はしないので読みたい人は読んでくれ。 なお libc おじさんとしては strftime(3) も無い時代に date(1) がアセンブラで必死にヒューマンリーダブルな形式に編集してるの涙ぐまし過ぎて飯が旨い。

代わりに日数から月を算出するためのテーブルのコードだけ貼る。

95 mtab:
96 	0
97 	31.
98 	59.
99 	90.
100 	120.
101 	151.
102 	181.
103 	212.
104 	243.
105 	273.
106 	304.
107 	334.

去年アレしたしもう 33-4 ネタもお寒い限りなのである。

結論

ついに V1 UNIX にして正しい現在時刻が取得できるようになり、壁掛時計や腕時計を見る必要が無くなったのである。

いやまぁやっぱり RTC に現在時刻を保存する機能が無いので、起動するたびに時刻再設定が必要になるけれどもな!

次回

V2 は UNIX Archive にカーネルソース(マスタード味)が存在しないので飛ばして V3 ですかね。 ついにカーネルもプログラミング眩語 C となる頃合いだし。

とはいえ本来の目的からすると蛇足もいいとこなんだよね。 それに C なら解説不要なくらい単純なコードなわけで。