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