5
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

1972年頃の歴史的Cコンパイラを動かす

C言語は1972年に開発が始まった。この1972年ごろのCコンパイラのソースコードがテープに残されていて、そこから読み出したファイルがインターネットに公開されている。本稿では、47年ほど前のこの状態のC言語を体感することを目的として、Cコンパイラを動かしてみることとする。

Apout の導入

C言語はDEC社の16bitミニコンピュータであるPDP-11で動いていたUNIXの高級言語として開発された。この最初期のCコンパイラは移植性(portability)がなく、PDP-11でしか動作しなかった。そのため、まずPDP-11シミュレータを準備する。

PDP-11 Unixのユーザモードエミュレータを行うアプリとしてApoutを導入する。

DoctorWkt/Apoutのソースをcloneしてきて、単にmakeすればよい。
しかしデフォルトではEMUV1というマクロが無効化されているので、これを有効にしないと後述のunix v1が動作しないことに注意すること。

% git clone --depth=1 https://github.com/DoctorWkt/Apout.git
% cd Apout
% make APOUT_OPTIONS='-DEMU211 -DEMUV1 -DNATIVES -DRUN_V1_RAW'

以下、ここで作ったapoutのあるフォルダにパスが通っているものとする。

使用方法はapoutを引数なしで実行すれば使い方が表示されるので、すぐに了解できる。単にPDP-11の実行ファイルを後ろに続けるだけである

% apout
Usage: apout pdp11_binary
% 

PDP-11 unix v1 の取得

下記からダウンロードできる。

これを適当なディレクトリに展開する。中身は単なる / 以下を tar ball にしただけである。なので配下のバイナリファイルはapoutを介して使用することとなる。

% apout bin/echo Hello
Hello
% 

もし上のように実行して Apout not compiled to support 2nd Edition binaries というエラーが表示された場合、Apout のビルドに失敗している。APOUT_OPTIONSを再確認した上でmake cleanmakeしてapoutを作り直すこと。

last1120c 版Cコンパイラ

1972-1973年頃の歴史的Cコンパイラのソースコードは、ベル研究所にあるデニス・リッチーのページにおいてある。

しかし、ここには「鶏と卵」の問題がある。この歴史的コンパイラは、Cの原始的な方言で書かれている。現存するgccといったコンパイラではソースコードを解析できない(エラーとなる)。しかし、初期のコンパイラのバイナリも救出されたため、我々はこの歴史的Cコンパイラを実行できるようになったのである。

ただ、この問題を解決するのに簡便な方法がgithubに上がっているので、ここではその手法を使う。

last1120c 版Cコンパイラの生成

ここには mak というシェルスクリプトがあって、これを使えばビルドが可能だ。$APOUT_ROOTの値を環境に応じて変更する必要がある。

+++ mak.orig    2000-01-10 18:49:01.000000000 +0900
--- mak 2019-12-15 00:08:20.745133838 +0900
@@ -4,7 +4,7 @@
 # match the location where you unpacked
 # the 2nd Edition UNIX binaries
 #
-APOUT_ROOT=/usr/local/src/V1
+APOUT_ROOT=/path/to/V1
 export APOUT_ROOT
 #
 cc="apout $APOUT_ROOT/bin/cc"

このように変更後に、シェルスクリプト mak を実行する。

% sh mak
% 

実際には mak の実行において大量のエラーが出たが、これで実行ファイルが作られたので特に気にしないものとする。

last1120c 版Cコンパイラのインストール

ccは、/usr/lib/c0/usr/lib/c1を内部的に読んでいる仕組みになっている。c0は構文解析で、c1はコードジェネレータだ。いま mak の実行によって c0, c1, cc が作られているが、このうち c0, c1 を unix v1 の該当箇所に入れる。同様に ccbin ディレクトリに入れても良い。

% extern APOUT_ROOT=/path/to/V1
% chmod +w $APOUT_ROOT/usr/lib
% mv $APOUT_ROOT/usr/lib/c0 $APOUT_ROOT/usr/lib/c0_orig
% mv $APOUT_ROOT/usr/lib/c1 $APOUT_ROOT/usr/lib/c1_orig
% cp -p c0 c1 $APOUT_ROOT/usr/lib

last1120c 版Cコンパイラの動作確認

簡単にいわゆる Hello, world で動作確認する。

hello.c
main(argc, argv)
char argv[][]; {
  extern printf;
  printf("Hello, World.\n");
  return(0);
}

ここで以下の点に注意すること。

  • このCコンパイラはファイル名が8文字しか受け付けないため短いファイル名とすること。例えば、hello1.cはOKだが、hello12.cは9文字のためエラーとなる。
  • この時代のC言語には#includeがなかったので、#include <stdio.h>を書いてはならない。
    • かわりにmainから参照している関数(やグローバル変数)の宣言を関数冒頭に記載する必要がある
    • ただリンカが勝手に解決してくれるのでextern宣言をしなくても良かったりする。
  • この時代のC言語にはint main(int argc, char argv[][])といった原型形式での書き方はなかった。非原型形式(いわゆるK&R スタイル)の関数定義/宣言で書く必要がある。
    • 引数型を書いていないのはintを意味する。
    • このサンプルの場合は、引数を省略して書いても良い。
  • このCコンパイラはreturn 0;と書いてはならない。return文の戻り値を括弧で囲まなければならない。

今作ったccでコンパイルをして、実行することができるはずだ。

% extern APOUT_ROOT=/path/to/V1
% apout ./cc hello.c
I
II
% apout ./a.out
Hello, World.
% 

もしここで、下記のようなエラーが出た場合はAPOUT_ROOTの設定が不正である可能性が高い。

  • ld: 認識できないオプション '-l' です などホストのldが動作してしまう場合→APOUT_ROOTが指定できていない可能性がある
  • Can't find /bin/ld というエラーがでて異常終了 → APOUT_ROOTに間違ったパスを指定している可能せいがある

補足: prestructc 版Cコンパイラ

構造体をCコンパイラ自身に導入する前のCコンパイラである prestructc というのもある。オリジナルには一部のソースが抜けているらしいが、今用意した unix 環境で動作するようにしたソースコードもあるので、それを利用できる。

なお、struct実装前と言われるため、まるで構造体が使えないように聞こえるが、あくまでlast1120cからprestructcの間に構造体を処理できるように実装してあって、Cコンパイラに構造体が導入されていないという意味である。

c00.cを比較するとstruct対応がなされていることが確認できよう。

@@ -41,11 +37,12 @@
        exit(1);
    }
    tmpfil = argv[3];
+   xdflg++;
    init("int", 0);
    init("char", 1);
    init("float", 2);
    init("double", 3);
-/* init("long", 4);  */
+   init("struct", 4);
    init("auto", 5);
    init("extern", 6);
    init("static", 7);

これもlast1120cと同様にコンパイラをビルドできるが、last1120cとprestructcの差分を確認していない(この時代のstructの記述方法など)ので、詳細は述べない。

last1120c プログラミング実例: FizzBuzz

引数を一つ取り1からその数字までカウントして、FizzBuzzする。なお、現代のC言語ではエラーとなるもの、もしくは誤りとなるものについては、コメントで記載している。

fizbuz.c
main(argc, argv)
char argv[][]; { /* XXX char *argv[] のかわりにこう書く */
  extern printf; /* XXX #include<stdio.h>の代わり */
  auto num, cpos;

  if (argc != 2) {
    printf("Usage: fizbuz NUMBER\n");
    return(1);
  }

  num = 0;
  cpos = 0;
  while (argv[1][cpos] != 0) {
    num =* 10; /* XXX 代入演算子が逆 */
    if (('0' <= argv[1][cpos]) & (argv[1][cpos] <= '9')) { /* XXX 論理演算子非対応 */
      num =+ argv[1][cpos] - '0'; /* XXX 代入演算子が逆 */
    } else {
      error("Error: Wrong number specified\n");
      return(1);
    }
    cpos++;
  }

  if (num <= 0) {
    error("Error: wrong NUMBER specified\n");
    return(1);
  }

  fizzbuzz(num);

  return(0);
}

fizzbuzz(num)
{
  extern printf; /* XXX #include<stdio.h>の代わり */
  auto i;

  i = 1;
  while (i <= num) {
    if (i % 15 == 0) {
      printf("FizzBuzz\n");
    } else if (i % 3 == 0) {
      printf("Fizz\n");
    } else if (i % 5 == 0) {
      printf("Buzz\n");
    } else {
      printf("%d\n", i);
    }
    i++;
  }
}

error(s, p1, p2) { /* XXX 可変長引数非対応 */
  extern printf, fout, flush; /* XXX #include<stdio.h>の代わり */
  int f;

  flush();
  f = fout;
  fout = 1; /* XXX 出力先のファイルディスクリプタの番号を直接変更 */
  printf(s, p1, p2);
  fout = f;
}

for文はまだない。while文・do-while文もしくはgotoを組み合わせてループを作る必要がある。

&&||といった論理演算子もまだない。かわりにビット演算子の&|を使う。

基本的に型はintしかないため、intの場合は書かない。これは関数戻り値の型を略記するとintになる、といった形などで現在も引きずっているC言語の仕様である(警告は出る)。main(),fizzbuzz(),error()のいずれも関数戻り値の型を略記しているため、intである。現代のC言語ではvoidを使うことで戻さないことを明示するが、voidキーワードがまだ存在しないのでどうにもならない。

興味深いのは可変長引数を対応していないのだが、関数の引数チェックを行っていないため可変長引数のような使い方をしても問題なくコンパイルがとおる。もっともこの仕様は現代のCコンパイラも引き継いでいる(警告は出る)。このため、関数プロトタイプ宣言といったものも不要であり、ファイルをまたいだ関数呼び出しの場合もextern printfと書くだけである。

おわりに

本稿では、1972年頃の歴史的Cコンパイラであるlast1120cの環境を準備し、実際にこれが解釈することができるコードを書いた。

C言語の引数をチェックしないことや、関数プロトタイプ宣言などが、最初期のC言語では気にしないでよく、それはそれでリーズナブルな形になっていることも体感できた。こういうものは、増築を繰り返した温泉旅館みたいに複雑になった末に発生したものであったということも理解できたので、もう少し広い心でCコンパイラを見ることができるだろうと思う。

参考文献

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
5
Help us understand the problem. What are the problem?