UNIX V6コードリーディング 備忘録
この記事はIS18er Advent Calendarの13日目の記事として書かれました。
最近学科でこの本を読むゼミをやっています。
はじめてのOSコードリーディング ~UNIX V6で学ぶカーネルのしくみ (Software Design plus)
- 作者: 青柳隆宏
- 出版社/メーカー: 技術評論社
- 発売日: 2013/01/09
- メディア: 単行本(ソフトカバー)
- 購入: 56人 クリック: 1,959回
- この商品を含むブログ (29件) を見る
というわけで,ゼミで得られた知見をつらつらと書きたいところ…なのですが,ハイライトは他のゼミのメンバーがいい感じにまとめてくれたのと,全てを網羅的に解説するのは重すぎるということがあり,またOSの仕組みの理解云々の以前にC言語の書き方が古かったりアセンブリの読み方が分からなかったりという本質でない点で割と苦労していたことがあったため,主にこれから同じ本を読もうとしている人(と自分)に向けて,分かったことをメモ代わりに書いておこうと思いました。
C言語に関して
pre K&R特有のもの
本の巻末の付録に書いてあることと同じですが…
演算代入
現代のC言語ではa += 1
となるようなものは,pre K&Rではa =+ 1
のように書いていました。
ただこれだと,例えば現代でいうa -= 1
が書きたい時にa =- 1
となるわけですが,マイナス記号の後ろにスペースを入れないとa = -1
との区別がなくなってしまうという不備があるため,後に修正されたということです。
無名構造体
無名構造体で宣言された要素は任意の変数に対して割り当てられるというものです。 この仕組みを使うと,この記事の「無名構造体」の部分で説明されているような便利なことができたりします。
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
dpadd
とdpcmp
は,巻末の付録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 b
はa
をb
に代入します。 :
で終わるものがラベル- 関数は先頭に
_
がついて_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 1f
のf
というのが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 |
システムコールを出す |
参考リンクなど
本の巻末にも沢山載っているのですが,なるべくそれ以外に関して,個人的に有用だと思ったものを紹介していこうと思います。
ソースコード類
- UNIX OPERATING SYSTEM SOURCE CODE LEVEL SIX
- GitHub上のソースコード
- ソースコード内を検索する時などはPDFよりは便利かも??
- The Unix Tree
Reference / Manual類
- A COMMENTARY ON THE SIXTH EDITION UNIX OPERATING SYSTEM
- 俗に「Lions本」とも呼ばれるもの。昔アメリカの大学でOSの教科書として使われていたものです。
- アセンブラやpre K&R Cの解説もきちんとされており,これさえ見ておけば大丈夫という感じです。
- 慶応大学のアセンブリの資料
- ニーモニックの説明がちゃんと載っていて良いです。
ブログ類
- Lions' Commentary on UNIX 読書会
- コードリーディングの著者(青柳さん)が参加していた読書会の記録です。色々なリソースに飛べます。
- 読書会のメモ(やる気のないはてだ)
- コードリーディングの著者の青柳氏のブログです。基本的には本と同じことが書いてありますが,関数呼び出しの遷移図などが参考になることもあります。
- 2238クラブのサイト
- 断片的ではありますがUNIX V6関連の解説があります。
おわりに
まだゼミは続いているので,今後も何か新しい発見があったら追記していきたいと思います。
2017/12/19 unsignedがないことについて追記しました