pre K&R Cコンパイラ
UNIX V6のソースコードを読むときにネックになるものの一つがK&R以前のC言語の仕様である。これをpre K&Rと呼ぶ。マニュアル(「C Reference Manual」)も存在するので、興味のある人はどうぞ。ちなみにUNIX V7では、後にも触れるポータブルCコンパイラを使って書き直された。Lions本がV7ベースだったらもっと読みやすかったのにね。
と言っても仕方ないので、pre K&Rの世界に飛び込んでみよう。さっそくコードを見ていく。まずはmalloc、mfreeで空き領域管理に使われてる構造体mapの定義とその実体である。
0141 #define CMAPSIZ 100 0142 #define SMAPSIZ 100 0203 int coremap[CMAPSIZ]; 0204 int swapmap[SMAPSIZ]; 2515 struct map 2516 { 2517 char *m_size; /* サイズ */ 2518 char *m_addr; /* アドレス */ 2519 };
いろいろツッコミどころがあるけど、せっかくmap構造体を定義しているのに、struct map coremap[XXX]ではない。sizeof(struct map)は4バイトになるので、coremapとswapmapのエントリ数は50になる*1。
さらにm_sizeやm_addrの型がなぜchar *なのか?という疑問が湧き起こる。m_sizeはintでよさそうだし、m_addrもポインタではなく(16bitの)アドレスである。
ここで前述のマニュアルを見てもらいたいのだが*2、pre K&Rにはunsignedがないのだ。ポインタは当然unsignedなので、unsignedを使いたい場合はポインタ(char *)を使っていたのである。いや、なんとも。
わかりやすいところでは、こんなコードがある。
6326: max(a, b) 6327: char *a, *b; 6328: { 6329: 6330: if(a > b) 6331: return(a); 6332: return(b); 6333: }
続いて、putcharの冒頭を少し引用する。
2386 putchar(c) 2387 { 2388 register rc, s; 2389 rc = c; 2390 if(SW->integ == 0) 2391 return; 2392 while((KL->xsr&0200) ==0) 2393 ;
SWやKLにはそれぞれコンソールスイッチレジスタ、KL11シリアルラインコントローラのアドレスが格納されている。これらは8ビット表記だ。ここでPDP-11のアーキテクチャについて少し補足しておこう。PDP-11のシステムバスであるUNIBUSには、CPU、メモリ、周辺機器がフラットにつながっている*3。UNIBUSは18ビットのアドレス空間を持ち、周辺機器もそのアドレス空間にマッピングされる。いわゆるメモリマップドIOである。
0165 #define KL 0177560 0166 #define SW 0177570
それでは、2390行の「SW->integ == 0」などは何を意味しているのだろうか? それに、SWは構造体でもないのに、「->integ」とアクセスできるの? 不思議だ。これを今風に解釈すると、「*(int *)SW == 0」になる。そして、こう書かねばならない理由はpre K&Rにはキャストがないからだ。そのため、メモリアドレスをむりやり構造体としてアクセスしている。つまり、SW->integだと0177560番地から2バイトを読み込む、KL->xsrだと0177564番地*4からとなる。
ではintegはどこで定義されているのかというと、次のようになっている。このような無名構造体は今ならコンパイルエラーになるけど、pre K&RではOKだった。
0175 struct { int integ; }; 2313 struct { 2314 int rsr; 2315 int rbr; 2316 int xsr; 2317 int xbr; 2318 };
さらに気持ち悪いというか、注意が必要なのは、構造体のメンバの名前空間が単一である点だ。なので、上記のような無名構造体?があちこちに出現する。読む方も大変だけど、書く方もメンバ名が重複しないようにするのは大変だったろうね。
まぁ、この辺は慣れの問題だと思うので、頑張って読み進めよう*5。
さて、pre K&Rの挙動を深追いしたいのであれば、Cコンパイラのソースコードもあるので参照できる。その入り口としてのメモを残しておこう。
コンパイラドライバccのソースコードは/usr/source/s1/cc.cに、狭義のCコンパイラ(CソースコードからPDP-11のアセンブリを出力する)は/usr/source/c以下に存在する。Cコンパイラは基本的に2パスになっていて、c0*.[cs]が1パス目、c1*.[cs]が2パス目になる。そして-Oオプション時に実行されるオプティマイザがc2*.[cs]である。実行バイナリは/lib以下にコピーされ、ccから呼ばれる。また、/lib/fc*という実行バイナリも存在するが、これはどうも浮動小数点エミュレーションをするときに用いられる。また、プリプロセッサは独立したプログラムではなく、cc内部で実行するようだ。
*1:終端を示すために1エントリは必ず消費されるので、利用可能なのは49エントリとなる。プロセス数の上限(NPROC)が50なので、NPROC-1プロセスがスワップアウトできるだけのswapmapはあるのね。coremapエントリ数とNPROCの関係は? 次回読書会の課題かな。
*2:ソースコードを追っかけたい場合は/usr/source/c以下に存在する。ただしyacc以前のコードなので、ちょっと文法を追っかけるのは面倒か。まぁ、キーワードを調べるだけならgrepで十分で、c00.cの先頭の配列kwtab[]でキーワードを定義している。unsignedもshortもないね。
*3:後にUNIBUSはIO専用のバスになる。
*4:当時の日本人は「いななごろし」といった語呂合わせでレジスタのアドレスを覚えたという。
*5:より突っ込んだ事情を知りたい場合は、USENIX98のStephen C Johnsonによる招待講演「C and the AT&T Unix Port -- A Personal History」がお勧め。同氏はベル研でポータブルCコンパイラ(PCC)、yacc、lintを実装したことで知られている。当時はLinusも在籍していたTransmetaに居たんだね。余談だけど、PCCのメンテナンスは続いていて、x86でも動くし、C99にも対応しているそうな。(Anders Magnusson 版 PCC)