PDP7-UNIX(Unics) とは何か

現在入手できる最も古い UNIX のソースコードが PDP7-UNIX あるいは UNIXEditionZero と呼ばれるもの。 つまり Ken Thompson が Multics を揶揄して Unics と命名したアレなのだがそう呼ばないのは何か大人の事情でもあるんですかね(邪推)。

不鮮明なプリントしか現存してなかったようで The Unix Heritage SocietyPDF形式 でしか見れなかったのだがいつの間にか OCR でソースコード復元し詳細なコメントをつけたバージョンが github/DoctorWkt/pdp7-unix から入手可能な上に実機で動くそうだ、いい時代だなぁ。

V1 から V7 までの UNIX(TM) はほぼ BSDL の Caldera Lisence だし V0 についても Caldera が権利を有してると思うのだが、リポジトリは GPLv3 である。 これは tools 以下にある perl で書かれた

  • a7out … PDP-7 ユーザーモードシミュレータ 直接 PDP-7 実行ファイルが動かせる
  • as7 … PDP-7 アセンブラ
  • ccov7 … PDP-7 カバレッジ
  • fsck7 … PDP-7 UNIX ファイルシステム整合性チェック
  • mkfs7 … PDP-7 UNIX ファイルシステム作成

などのツールが GPLv3 だからと思われ、いやむしろそっち読む方が楽しそうだなオイ!?

まぁこっちのコード読むのはまた機会があればにして、この最初期の UNIX において時刻がどうやって管理されていたかを読んでいこうと思う。

まずは資料を用意する

PDP-7 に関するドキュメントは ここ に大量に転がってるので安心、いい時代だなぁ。

今回はこの 2 つをパラ見すればなんとかなるんじゃねえかなぁ(無謀)!

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

チェケラしたリポジトリの src/sys 以下には

$ ls src/sys/
cas.in    NOTES.md  s2.s  s4.s  s6.s  s8.s  sop.s   trysys.s
maksys.s  s1.s      s3.s  s5.s  s7.s  s9.s  sysmap

というあまりに素っ気ないファイルが転がってるが、ざっと何やってるかの紹介。

sop.s

PDP-7 の命令アドレスとニーモニックを対応づけるファイル、 詳しくは前述の PDP-7 SYMBOLIC ASSEMBLER PROGRAMMING MANUAL を見るといい。

1 "** 01-s1.pdf page 62
2 " sop
3 
4 dac = 0040000			" MEM: deposit AC
5 jms = 0100000			" MEM: jump to subroutine

sysmap

ラベルとメモリアドレスを対応づけるファイル。

1 "** 01-s1.pdf page 57 -- system assembly map
2 .        004671 r
3 .ac	 004102 r
4 .chown	 000426 r
5 .close	 000725 r

ざっと中身をみるとラベルの命名規則は

  • .* … システムコール
  • s.* … システムブロック
  • u.* … ユーザーブロック
  • i.* … インフォメーションブロック
  • d.* … ディレクトリブロック

となってるっぽい、カーネル弱者男性でもそれくらい判るよバカヤロウ。

i.* だけピンと来ないかもしれんが i-node の i といえばはたと膝を鬱に違いない、鬱になってどうするいや冬はどうしてもね。

maksys.s

コンパイルしたカーネルをディスクに書込むツールのようである。 すでにマジックに a.out の文字が見える。

46 a.out:
47    <a.>;<ou>;<t 040;040040

ただし存在しない過去記事 a.out フォーマットを読み解く(その2) で解説したオブジェクトフォーマット a.out 形式とは違う事には注意。

trysys.s

コメント皆無なので困るのだが、どうやら maksys.s で書込んだカーネルをディスクから起動するためのブートローダーっぽい。 実機で PDP-7 UNIX を動かす動画の 1:10 あたりに登場するブートローダーのコードとはだいぶ異なるんだけど

10    iof		// PIC: interrupts off
11    caf		// CPU: clear all flags
...
26    jmp 0100		// jump kernel entry point

あたりのコードが一致してるのでそう判断した。

cas.in

Makefile を追いかけると src/cmd/cas.s で character table を生成するためのソースのようだが詳細不明。

s[連番].s

そんで今回の攻略対象がこのやる気ねえ命名のファイル群である。 こいつらこそがカーネルソースコードなのだ。

現代なら機能毎にファイル分割するのだが、当時はそれこそテープやパンチカードの時代なので、記憶媒体的にはこの方が適しているのだと思われる、適当書いてるけど。

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

さっきの sysmap でゼロ番地を探してみた。

211 orig	 000000 r

orig というラベルが見つかったので、ここからプログラムが開始されるんだろうなとこれまた根拠の無いアタリをつける、連番ファイルの最初だし多分あってると思う(適当)。

 3 .. = 0
 4 t = 0
 5 orig:
 6    hlt				" overwritten with interrupt return addr
 7    jmp pibreak			" dispatch to interrupt processing
 8 
 9 . = orig+7			" real time (60Hz) clock
10    -1				" -1 will cause "clock overflow" on next tick
11 				" Overflow is checked by "clsf" instr
12 				" in PI service (pibreak) routine,
13 				" reset to -1 after tick handling
14 				" results in an interrupt every "jiffy"

いきなり .. って何だよ!で挫折しそうだが大丈夫か?

マニュアル読んでもどこにも記述なくて頭抱えたのだが、前出の as7 のコメントに書かれていた。

61 # http://minnie.tuhs.org/cgi-bin/utree.pl?file=V3/man/manx/as.1
62 # ".." is the relocation constant and is added to each relocatable
63 #    reference.  On a PDP-11 with relocation hardware, its value is 0; on
64 #    most systems without protection, its value is 40000(8).
65 
66 # PLB: "relocatable" values are flagged with $RELATIVE
67 
68 # start with the location counter at zero
69 # predefine syscall and opcodes as variables
70 %Var = (
71     '.'    => $BASE,
72     '..'   => 4096,		# output base addr?

なるへどうやら DEC 謹製の assembler ではなく PDP-7 UNIX の as(1) の方言のようである、そっちかよ! 要は GOT(Global Offset Table) みたいなもんちゅうことでええんかな。

しかし . の方に関しては sysmap にも

2 .        004671 r

と定義されとるしもう何が何だかである。

さらにorig+7 って何だよこっちも! おまけに -1 も意味わかんねーよ!

リファレンスマニュアル流し読みしたら、おぼろげながら見えてきたんです 76 という数字が(構文)。

以下は CHAPTER 4 INPUT/OUTPUT CONTROL AND INTERFACE の

  • Figure 4-2 Bit Assignment for Input-Output Transfer Instruction (iot)
  • Figure 4-5 Input-Output Status Instruction - Bit Assignment

を悪魔合体させたもの。

Mnemonic Instruction Code Operation
iot      700000           input/output transfer
iors     700314           Input/Output read status, The contents of
                          given flags replace the contents of the as-
                          sined AC bit.
--- Operation Code ---
X[ 0] <- Program Interrupt On
X[ 1] <- Tape Reader Flag
X[ 2] <- Tape Punch Flag
--- Sub-Device Selection ---
X[ 3] <- Keyboard Input Flag
X[ 4] <- Type-Out Flag
X[ 5] <- Display Flag
--- Device Selection ---
X[ 6] <- Clock Overflow Flag
 [ 7] <- Clock Enabled
X[ 8] <- Magnetic Tape Interrupt 
X[ 9] <- Assignable
 [10] <- Assignable
 [11] <- Assignable
--- Sub-Device Selection ---
 [12] <- Assignable
X[13] <- Assignable
---
 [14] <- If Bit Is a 1: Clear AC at event time 1, Assignable
X[15] <- If Bit Is a 1: Transfer an IOT pulse at event time 3, Assignable
X[16] <- If Bit Is a 1: Transfer an IOT pulse at event time 2, Assignable
 [17] <- If Bit Is a 1: Transfer an IOT pulse at event time 1

X - Program Interrupt Connected

今の orig0 なので orig+70+7 だと思います だからこそ 7 だと思います(構文)。 せや! Clock Enable で 7 から -1 すれば 6 すなわち Clock Overflow Flag なんや(ガンギマリ)!

いわゆるひとつの MMIO(memory-mapped I/O) ってやつで RTC(Real-Time Clock) にアクセスしてるんかこれ、いやコメントなければ一生理解できなかっただろうが。

つまり RTC で周期的にクロック割込発生させてるコードがここなんじゃねえかな、Clock Enable 叩くと一定時間経過後に Clock Overflow Flag の割り込みがかかるって寸法よ。

RTC についてはリファレンスマニュアル 4-16 REAL TIME CLOCK に記述があるけども、1/60 秒毎に割込を発生させるもよう、つまり 60Hz ちゅうこと。

そんで pibreak というラベルが実質クロック割込ハンドラなのでは?という推理、ロードして 10 行でこれって出会って 4 秒で合体より展開はええな!?

pibreak ラベルを読む

pi とは priority interrupt 優先割込すなわち esr 版 ジャーゴンファイルセックス>(超えられない壁)プログラミング って揶揄されてるアレ、なお現代日本の弱男ナードすなわちデボチカとのインアウトなんて㍉も可能性無かった孤独老人の最優先度割込は SNS で三次元女性叩きである、地獄なんやな。

3 .ac	 004102 r
4 pibreak:		" priority interrupt break processing "chain"
5    dac .ac		" save interrupt AC
6 	"** CROSSED OUT....

計算を止めてより優先度の高いタスクに譲るため、AC レジスタの値を .ac ラベルの記憶領域に退避してる、でいいんかなこれ。

リファレンスマニュアル読むと

  • AC … ACcumulator 積算レジスタ

と演算レジスタの一種とあるけど本当にこれだけ退避すれば足りるんすかね、スタックポインタ的なものなのなんですかね(たぶんまだそんなものは存在しない)。

時刻関連のコードを探す

ええい今回の目的はカーネル完全に理解したではないので(興味ねえし)、近道探す。

sysmap を読むと time(2) システムコールなるものを発見した。 前述のとおりシステムコールは .* で開始するラベルなので .time がそれである。

37 .time	 000615 r

さっそく実装を読む。

185 	" time system call returns line (mains) frequency ticks
186 	" high order bits returned in AC, low order in MQ
187 	" s.tim is located in "system" block (written to disk)
188 	" so this is a running count of uptime since first boot!
189 	" at 60Hz, 36 bits would last 36+ years!
190 .time:
191    lac s.tim			" load high order bits
192    dac u.ac			" return in AC
193    lac s.tim+1			" load low order bits
194    dac u.mq			" return in MQ
195    jmp sysexit
7 lac = 0200000			" MEM: load AC

やってることは

  • s.tim を AC レジスタに読み込んで u.ac に書き込む
  • s.tim+1 を AC レジスタに読み込んで u.mq に書き込む

こんだけ。

PDP-7 は今ではあまり一般的ではない 18bit ワードマシンで s.time には記憶域を 2 ワードつまり 36bit 確保している。 なのでシステムコールの戻り値は u.acs.time つまり 上位 18bit と u.mqs.time+1 で下位 18bit をコピーしてるわけ。

そもそも u.acu.mq についてはそれぞれ AC および MQ レジスタを退避するための記憶域のようである。

んで MQ レジスタについてはリファレンスマニュアルに

  • MQ … Multiplier-Quotien 乗数・商レジスタ

とあるけど、こいつ読み書きするには

45 lacq = 0641002			" EAE: load AC with MQ
...
49 lmq = 0652000			" EAE: load MQ from AC

という命令を使って一度 AC レジスタを経由する必要があるみたい。

ただ上記のコードにおいて MQ レジスタはまったく無関係なので、単にシステムコールの戻り値にも流用されてるって感じ。

再び pibreak ラベルを読む

さて s.tim が現在時刻情報ということが time(2) から判明した。 改めて pibreak を追っていくと

4 pibreak:		" priority interrupt break processing "chain"
...
28 1: clsf			" clock overflow (60hz tick increment of loc 7 (-1))
29    jmp 1f		"  no
...
33    isz s.tim+1		" increment low order tick count
34    skp			"  no overflow, skip high order word increment
35    isz s.tim		"   low order overflowed, increment high order count
36 			"   ("never" skips: 36 bits overflows every 36 years!)
12 isz = 0440000			" MEM: increment and skip if zero
...
63 clsf = 0700001			" CLK: skip if overflow

clsforig で開始した RTC がオーバーフローしたかのチェックである。 オーバーフローしていたら iszs.tim+1 つまり下位 18bit を加算、こちらもオーバーフローなら s.tim つまり上位 18bit を加算する。

ハイ、ここっすね時計の針を進めているのは。

s.time は 秒単位ではない

ここまでの出てきたコードのコメントにネタばらし書いてあったし、すでにお気づきの人もいるだろう(誰も読んでいない)。

この頃の UNIX の time(2) が返す値は 今の POSIX time(3) のような 1970 年 1 月 1 日 00:00:00 UTC いわゆる Epoch からの「秒数」で現在時刻を表したものじゃないんだよね。

そう s.time は 1/60 秒毎にオーバーフローする RTC の割込で +1 すると思います、だからこそ s.time は 1/60 秒毎にカウントアップすると思います(構文)。

強いて N で例えるならば tick(9) の間隔で刻まれる time_uptime(9) みたいなもんである。

そのためコメントにもある通り

  • PDP-7 なら 132560 日つまり 36 年と 2 ヶ月
  • PDP-11 のような 16bit アーキテクチャなら 32bit で 828 日つまり 2年と3ヶ月

で値がターンオーバーするのである。

そもそもコードのどこ探しても RTC から現在時刻を読込んでるところが存在しないっぽい。 よって PDP7-UNIX で現在時刻を知る方法は無いということ。

それならばこの時代 date(1) が存在しないのも当然である。 さっきの動画で date(1) 叩いてるからあるもんだと思ってたら、 src/other/wktdate.s は新規に書いたコードなのである、そしてマニュアルの BUGS に

NAME              date ‐ print the date

SYNOPSIS           date

DESCRIPTION       The current date is printed to the second.

FILES             ‐‐

SEE ALSO          ‐‐

DIAGNOSTICS       ‐‐

BUGS              It is always 1970.

OWNER             wkt

と書いてあり、起動したら 1970-01-01 00:00:00 UTC から開始となってる無理矢理な実装なわけですな。

そういえばこの件については N の time(3) にもちゃんと書いてあるのに最近気づいたゾ。

HISTORY
     A time() function appeared in Version 2 AT&T UNIX.  It returned a 32-bit
     value measuring sixtieths of a second, leading to rollover every 2.26
     years.  In Version 6 AT&T UNIX, the precision of time() was changed to
     seconds, allowing 135.6 years between rollovers.

V2 UNIX より以前からセクション 3 つまりライブラリ関数 と 2 すなわちシステムコールの違いはあれど、存在はしたので内容は不正確ですなこれ。 ただまぁ 1/60 秒単位でカウントすることと頻繁にロールオーバーが発生することについては正しい記述である。

ところで覚えてる人間は世界で 0 人の、存在しない過去記事 なぜ localtime(3) には、ポインタを渡すのか? を思い出してほしい、この記事内で time(2) が現代的な UNIX と同様に秒単位の値を返すと勘違いした記述があった事に謝罪したい(結論自体は変わらないのだが)。

えーと HARAKIRI でいいですか、痛いのヤダからスゥと意識が飛ぶ首吊りで勘弁願いたいところである、というか究極の鎮痛剤であるショットガンいいっすか。

結論

PDP7-UNIX ではそもそも現在時刻なんて管理してねーよという大変に心温まるお話である。

つーかそもそも PDP-7 の RTC ってカレンダークロック機能つまりバッテリー保護された現在時刻なんて持ってないんだなこれ。 マニュアル読んでも該当しそうな命令が存在しないし。

次回

もう完全に飽きてるのだけど、次はプログラミング幻語 C で書き直される V4 UNIX あたりの解説になると思われるが、冗長にも続けて V1 UNIX の解説がはじまり結局は結論おんなじじゃねえか!になる可能性も無きしもあらず。

NTP の話に繋げるのに前振り長過ぎるなぁと自分でも思うのだが、これくらいやらないと David L. Mills 教授から見た UNIX の現在時刻管理ってピンと来ないと思うんだよね。