UNIX V6コードリーディング 備忘録

この記事はIS18er Advent Calendarの13日目の記事として書かれました。

最近学科でこの本を読むゼミをやっています。

はじめてのOSコードリーディング ~UNIX V6で学ぶカーネルのしくみ (Software Design plus)

はじめてのOSコードリーディング ~UNIX V6で学ぶカーネルのしくみ (Software Design plus)

というわけで,ゼミで得られた知見をつらつらと書きたいところ…なのですが,ハイライトは他のゼミのメンバーがいい感じにまとめてくれたのと,全てを網羅的に解説するのは重すぎるということがあり,またOSの仕組みの理解云々の以前にC言語の書き方が古かったりアセンブリの読み方が分からなかったりという本質でない点で割と苦労していたことがあったため,主にこれから同じ本を読もうとしている人(と自分)に向けて,分かったことをメモ代わりに書いておこうと思いました。

C言語に関して

pre K&R特有のもの

本の巻末の付録に書いてあることと同じですが…

演算代入

現代のC言語ではa += 1となるようなものは,pre K&Rではa =+ 1のように書いていました。 ただこれだと,例えば現代でいうa -= 1が書きたい時にa =- 1となるわけですが,マイナス記号の後ろにスペースを入れないとa = -1との区別がなくなってしまうという不備があるため,後に修正されたということです。

無名構造体

無名構造体で宣言された要素は任意の変数に対して割り当てられるというものです。 この仕組みを使うと,この記事の「無名構造体」の部分で説明されているような便利なことができたりします。

moraprogramming.hateblo.jp

unsigned型が存在しない

pre K&Rにはunsigned型が存在しないため,unsignedで表現したいものについてはポインタで表現していたようです。 例えば,map構造体のm_size

2515    struct map
2516    {
2517        char *m_size;  // 空き領域のサイズ
2518        char *m_addr;  // 空き領域のアドレス
2519    };

ino.h中のinode構造体のi_size1

5605    struct    inode
5606    {
5607        int    i_mode;
5608        char   i_nlink;
5609        char   i_uid;
5610        char   i_gid;
5611        char   i_size0;
5612        char   *i_size1;
5613        int    i_addr[8];
5614        int    i_atime[2];
5615        int    i_mtime[2];
5616    };

なども,unsignedがなかったためにやむなくchar*を使っているのではないかと考えられます。

post K&Rではあるものの知らなかった書き方

ソースコードの中には,一応現代でも存在してはいますが私は知らなかった書き方をしている部分があったため,メモしておきます。

関数宣言のしかた

関数宣言の最初の部分を見てみると,こんな感じの書かれ方をしているんですが,知らないと一瞬戸惑います。

malloc(mp, size)
struct map *mp;
{
    /* 関数の定義 */
}

これは古いスタイルの関数宣言の書き方で,1行目で関数名と引数の宣言をし,2行目で引数の型を指定しています。 つまり,現在では

int hoge(int a, int b, int c)
{
    /* 関数の定義 */
}

のように書くところを昔は

int hoge(a, b, c)    // 宣言子
int a, b, c;         // パラメータの宣言リスト
{
    /* 関数の定義 */
}

といった感じで書いていたというだけの話のようです。

キーワード等

extern

includeしているヘッダーファイルの中では定義されていないglobal変数を使う時に,それが存在することを示すためのもの(普通に今でも使われていると思いますが個人的に知らなかったのでメモ)。

register

C言語のキーワードで,変数の宣言時に

register [型名] hoge;

といった感じで使い,「この変数は(多用されるため)CPUのレジスタに置いておいおくことを推奨する」という意味です。コンパイラはこの指示を無視することもできます。最近非推奨になったらしいです。

V6に用意されているUtility関数

dpadd, dpcmp

dpadddpcmpは,巻末の付録Bでも紹介がありますが,16bitアーキテクチャの中で32bit整数を扱いたい時に使われます。conf/m40.sまたはconf/m45.sアセンブリによる実装があります(m40.sがPDP-11/40用でm45.sがPDP-11/45およびPDP-11/70用です)。

それぞれの働きを説明すると,dpaddは,第1引数である32bit整数に第2引数の16bit整数を足す処理をします。桁上りの処理も行います。dpcmpは,第1, 2引数の表している32bit整数から,第3, 4引数の表す32bit整数を引いた値を返します。

雰囲気を現代のC言語で実装してみるとおそらくこんな感じになります(本来ならshort型を使うべきですが面倒なのでintにしています)。

void dpadd(int a[], int b)
{
    if ((a[1] + b) > 0xffff) { // 桁上りが発生する場合の処理
        a[1] += b - 0x10000;
        a[0]++;
    } else {
        a[1] += b;
    }
}

int dpcmp(int a1, int a2, int b1, int b2)
{
    int a = (a1 << 16) + a2;
    int b = (b1 << 16) + b2;
    return a - b;
}

ldiv

こちらもconf/m40.sまたはconf/m45.sに実装されているものです。機能としては普通に割り算の商を求めるもので,ldiv(a, b)a / bと同じということになります。 ではなぜ/演算子を使わなかったのか?となりますが,どうやら古いCでは負の整数を含んだ割り算をしたときの商の取り方がプロセッサ依存だったためということみたいです(ソース)。

アセンブリについて

さて次はアセンブリについてです。コードリーディングの本にはアセンブリを読まねばならない箇所がいくつかあるため,そこを読むために必要と思われることを書きます。

なお記事を書いている途中に判明したことですが,このあたりの話はLions本のP4〜7にきちんとまとめられているようなので,参照してみてください。

基本的な文法

  • AT&T記法で書かれています。よって,例えばmov a bab代入します。
  • :で終わるものがラベル
  • 関数は先頭に_がついて_hoge:のようなラベルになる
  • .から始まるものがロケーションカウンタ
  • /以降はコメント

Numeric Label

UNIXアセンブリで使われているラベルには(大抵1や2など1桁の)数字が名前になっているものが存在します。これがbranch命令などでのオペランドになっていることがしばしばあるのですが,その時の分岐先の指定方法がややトリッキーなので記しておきます。

第5章の割り込みの説明で出てくるcallを例に取ります(4桁の行番号の振り方はUNIX OPERATING SYSTEM SOURCE CODE LEVEL SIXに準じています)。

0776   call:
0777      mov     PS,-(sp)
0778   1:
0779      mov     r1,-(sp)
0780      mfpi    sp
0781      mov     4(sp),-(sp)
0782      bic     $!37,(sp)
0783      bit     $30000,PS
0784      beq     1f
0785      jsr     pc,*(r0)+
0786   2:
0787      bis     $340,PS
0788      tstb    _runrun
0789      beq     2f
0790      bic     $340,PS
0791      jsr     ps,_swtch
0792      br      2b
0793   2:
0794      tst     (sp)+
0795      mtpi    sp
0796      br      2f
0797   1:
0798      bis     $30000,PS
0799      jsr     pc,*(r0)+
0800      cmp     (sp)+,(sp)+
0801   2:
0802      mov     (sp)+,r1
0803      tst     (sp)+
0804      mov     (sp)+,r0
0805      rtt

さて,例えば0784行目のbeq 1fって,どこに飛ぶのでしょうか。まず,このオペランドの 1はラベル1:で指定されている場所を表しているので,0778行目か0797行目のどちらかということになります。では一体どっちなのかというと,0797行目の方に分岐します。これは実はbeq 1ffというのがforwardを表しているので,「すぐ前方にある1:のラベルに飛べ」という命令になっているんですね。逆に仮にもしここがbeq 1bであったならば,bはbackwardのbの意で「すぐ後方にある1:のラベルに飛べ」となるため,0778行目の方に飛びます。

他の部分についても同様で,例えば0792行目のbr 2bであれば0786行目に飛びますし,0789行目のbeq 2fは0793行目(0801行目ではない)に飛びます。

このように,Numeric Labelをbranch命令のオペランドに取る時は,「数字n + fまたはb」の形式になっており,fならばすぐ前方のn:ラベルへ,bならばすぐ後方のn:ラベルへ飛びます。何も知らないで見るとこれは16進数か?と思ってしまうんですが,そうではないことに注意です。

レジスタのアドレッシング・モード

頻出の表記についてのみまとめてみます。

書き方 意味
reg レジスタregの値を参照
(reg) レジスタregのさすアドレスにある値を参照
(reg)+ (reg)したあと,regをインクリメント
-(reg) regをデクリメントしたあと,(reg)する
n(reg) regがさす値 + n(数字)」がさすアドレスにある値を参照

他の*(reg)などの表記に関してはあまり頻出でないため割愛しました(Lions本を見て下さい)。

本に出てくるニーモニック

下の「参考リンク」でも紹介している資料を参考にしました。本に出てくるものを中心にまとめてみます。

ニーモニック 意味
bec Branch on Error Clear
beq Branch if EQual (zero)
bic src dest dest |= ~src (BIt Clear) dest中のsrcビットを消す。
bis src dest dest |= src (BIt Set) dest中のsrcビットを立てる。
bit src dest dest & src (BIt Test) dest中のsrcビットが立っているか見る。
bne Branch if Not Equal (zero)
br loc locに無条件分岐
jsr reg dest regをスタックに積んでからdestに飛ぶ(Jump to SubRoutine)
mov src dest dest src 代入
rts 関数の呼び出し元に戻る(ReTurn from Subroutine)
rtt スタックの先頭2wordをPSW, PCに格納する(ReTurn from Trap)
sys システムコールを出す

参考リンクなど

本の巻末にも沢山載っているのですが,なるべくそれ以外に関して,個人的に有用だと思ったものを紹介していこうと思います。

ソースコード

Reference / Manual類

ブログ類

おわりに

まだゼミは続いているので,今後も何か新しい発見があったら追記していきたいと思います。

2017/12/19 unsignedがないことについて追記しました