id:oraccha:20101101の手順を参考に、UNIX V6をSIMHで動かしてみました。分析のためファイルを取り出そうとしたのですが、TCP/IP以前のOSのためネットワークが使えるのか不明で、ディスクイメージからファイルを取り出す方法も分かりません。仕方ないのでカーネルのソースを読みながらファイルを取り出してみました。
読み込みツールはF#で実装しました。ブラウザ上から実行できるようにSilverlight化しました。id:n7shi:20101004で作成したZIP圧縮コードにより、ファイルシステム全体をZIPファイルとして保存することができます。
【追記:2015.11.12】ローカルで動かすGUI版もあります。(Silverlight非依存)
- リポジトリ ⇒ https://bitbucket.org/7shi/v6fs/
- GUI版ダウンロード ⇒ https://bitbucket.org/7shi/v6fs/downloads
- ディスクイメージ ⇒ http://minnie.tuhs.org/Archive/PDP-11/Distributions/research/Dennis_v6/
Silverlightで開く ⇒ http://7shi.net/v6fs/
以下、UNIX V6のC言語ソースを引用しながら簡単に説明します。ソースコードはタブで整形されていますが、レイアウトが崩れるのを防ぐためスペースに置換して引用しました。タブは8文字を想定しているようです。
構造
前掲の図がディスクイメージの構造です。左の番号はブロック番号で、1つのブロックは512バイトです。ブロックは現在セクタと呼ばれている概念と同じもののようです。最初のブロックはMBRと書きましたが、これはx86用語で、当時どう呼ばれていたかは不明です。IPLの機械語コードが生バイナリとして入っていて、0000〜0777(0x0000〜0x01FF)番地に読み込まれて実行されるようです。
スーパーブロック
MBRの次のブロックにはファイルシステムの情報が収められ、スーパーブロックと呼ばれています。filsys構造体として定義されています。
/usr/sys/filsys.h
/* * Definition of the unix super block. * The root super block is allocated and * read in iinit/alloc.c. Subsequently * a super block is allocated and read * with each mount (smount/sys3.c) and * released with unmount (sumount/sys3.c). * A disk block is ripped off for storage. * See alloc.c for general alloc/free * routines for free list and I list. */ struct filsys { int s_isize; /* size in blocks of I list */ int s_fsize; /* size in blocks of entire volume */ int s_nfree; /* number of in core free blocks (0-100) */ int s_free[100]; /* in core free blocks */ int s_ninode; /* number of in core I nodes (0-100) */ int s_inode[100]; /* in core free I nodes */ char s_flock; /* lock during free list manipulation */ char s_ilock; /* lock during I list manipulation */ char s_fmod; /* super block modified flag */ char s_ronly; /* mounted read-only flag */ int s_time[2]; /* current date of last update */ int pad[50]; };
PDP-11は16bitマシンのため、intは16bit符号あり整数型(int16_t)です。sizeof(filsys)は516バイトで、ブロック(512バイト)より大きいのですが、パディング(pad)が大雑把に定義されているだけで、実際に516バイト使われているわけではないようです。
ブロックやinodeのリストは書き込む際に必要となりますが、今回は読み込みだけを対象としているので、詳細は飛ばします。
in coreという表現がありますが、coreは今で言うメモリを指しているため、「メモリ上」という意味で使われているようです。ディスクイメージを見ると値が書き込まれているため、純粋にメモリ上だけに存在するわけではなく同期しているようです。
実際の読み込みはmain()から呼ばれるiinit()で行われています。日本語のコメントは私の追記です。
/usr/sys/ken/alloc.c
/* * iinit is called once (from main) * very early in initialization. * It reads the root's super block * and initializes the current date * from the last modified date. * * panic: iinit -- cannot read the super * block. Usually because of an IO error. */ iinit() { register *cp, *bp; (*bdevsw[rootdev.d_major].d_open)(rootdev, 1); /* rootdevを開く */ bp = bread(rootdev, 1); /* 1はブロック番号 */ cp = getblk(NODEV); /* メモリ確保, malloc(512)に相当 */ if(u.u_error) panic("iinit"); bcopy(bp->b_addr, cp->b_addr, 256); /* memcpy()に相当, 256はワード長 = 512バイト */ brelse(bp); /* 読み込んだブロックのデータを解放 */ mount[0].m_bufp = cp; mount[0].m_dev = rootdev; cp = cp->b_addr; /* 確保されたメモリの生ポインタを取り出す */ cp->s_flock = 0; cp->s_ilock = 0; cp->s_ronly = 0; time[0] = cp->s_time[0]; time[1] = cp->s_time[1]; }
s_flock, s_ilock, s_ronlyは読み込み後に初期化していることから、ディスク上では同期していないようです。
inode
スーパーブロックの次にはinode(アイノード)のリストが来ます。inodeというのはファイルに対するヘッダのようなものですが、ハードリンクにより複数のファイルから1つのinodeを参照することもあるため、1対1でファイルと対応しているわけではありません。そのためinodeにはファイル名の情報が含まれていません。
inode全体で使えるブロックサイズは filsys.s_isize で示されています。1ブロックが512バイトで、1つのinodeが32バイトであることから、ファイルシステム上のinode数の上限は s_isize * 16 個となります。
inodeは1から数えます(0は「なし」を意味します)。最初のinode(inode番号1)がルートディレクトリです。inodeの抽出はiget()で行われています。ldivは除算、lremは剰余です。
/usr/sys/ken/iget.c: iget()より抜粋・追記
ip = bread(dev, ldiv(ino+31,16)); /* (ino+31)/16 */ ip1 = ip->b_addr + 32*lrem(ino+31, 16); /* 32*((ino+31)%16) */
inode番号2の場合、ino=2, (2+31)/16=2, 32*((2+31)%16)=32 となり、ディスク上のブロック番号2の32バイト目に存在します。
構造体
inode構造体はino.hとinode.hの2箇所で定義されていますが、ino.hはディスク上の格納形式を示すためだけに含まれているヘッダで、実際にはコードから使用されていないようです。
/usr/sys/ino.h
/* * Inode structure as it appears on * the disk. Not used by the system, * but by things like check, df, dump. */ struct inode { int i_mode; char i_nlink; char i_uid; char i_gid; char i_size0; char *i_size1; int i_addr[8]; int i_atime[2]; int i_mtime[2]; }; /* modes */ #define IALLOC 0100000 #define IFMT 060000 #define IFDIR 040000 #define IFCHR 020000 #define IFBLK 060000 #define ILARG 010000 #define ISUID 04000 #define ISGID 02000 #define ISVTX 01000 #define IREAD 0400 #define IWRITE 0200 #define IEXEC 0100
構造体の個々のフィールドの意味は、inode.hの方に書いてあります。i_flagなどディスク上に存在しないヘッダが付いています。
/usr/sys/inode.h
struct inode { char i_flag; char i_count; /* reference count */ int i_dev; /* device where inode resides */ int i_number; /* i number, 1-to-1 with device address */ int i_mode; char i_nlink; /* directory entries */ char i_uid; /* owner */ char i_gid; /* group of owner */ char i_size0; /* most significant of size */ char *i_size1; /* least sig */ int i_addr[8]; /* device addresses constituting file */ int i_lastr; /* last logical block read (for read-ahead) */ } inode[NINODE];
i_modeの下位12ビットはいわゆるパーミッションで、chmodで使われる4桁の8進数(0755, 0644等)が格納されています。IFDIRのビットが立っていればディレクトリを示します。
i_addrにはディスク上のデータの位置がブロック番号で格納されています。ブロック番号が16bit整数で管理されていることから、サポートされるディスクサイズの上限が 65536 * 512 = 32MB となります。断片化をサポートするため複数のブロックが指定できるようになっています。9ブロック以上使用しているとi_addr[8]に入りきらなくなりますが、そのときの処理はデータブロックの節で説明します。
i_atime, i_mtimeはinode.hの方には含まれていませんが、それぞれ最終アクセス日時・更新日時を示します。1970年1月1日午前0時を起点とする秒数が32bitで格納されています。いわゆるUNIX時間です。intが16bitのため、32bitは2つのintで表現されています。上位・下位はPDP-11特有のミドルエンディアンで表現されています。
ファイルサイズはi_size0(上位), i_size1(下位)で示されます。これは全体で24bitの数値を示しているため、扱えるファイルサイズの上限は16MBとなります。i_size1はcharのポインタ型ですが、ポインタではなく16bit符号なし整数型(uint16_t)として使われています。id:oraccha:20101106によれば当時のC言語(pre K&R)には符号なし整数型がなかったため、ポインタで代用していたとのことです。
ミドルエンディアン
PDP-11は16bitをリトルエンディアンで表現しますが、32bitは上位ワードを先に置くミドルエンディアンと呼ばれる形式を使用しています。具体的には以下の通りです。
エンディアン | 0x12345678 |
---|---|
ビッグ | 12 34 56 78 |
リトル | 78 56 34 12 |
ミドル | 34 12 78 56 |
あくまで想像ですが、PDP-11のCPU上の実装というよりも、ビッグエンディアンの16bitマシンを念頭に書かれたC言語のコードをリトルエンディアンのマシンで動かした結果、ミドルエンディアンが発生したように感じます。
64bit以上はどう表現されるか気になる所ですが、UNIX V6のソースコードで64bitを扱っている箇所はないようです。
データブロック
inode.i_addrでデータブロックの番号が示されます。ブロック番号は1から始まるため、0は「なし」を意味します。連続したブロックでも1つずつ示されます。
使用しているブロックが9個以上でi_addr[8]に収まりきらない場合、imodeにILARGフラグが立ち、i_addrが指すブロックにはブロックマップが来ます。ブロックマップにはデータブロックの番号が1つずつ格納されています。ちなみにILARGのIはinodeの接頭辞で、LARGはlargeの略のようです。
【追記】id:takahirox:20110815:1313398103より。ILARGの場合、i_addr[0〜6]は上記説明の通りですが、i_addr[7]が指すブロックは「ブロックマップのブロックマップ」という二重ポインタとなっています。これによりディスク上限(65536 * 512 = 32MB)をやや上回る(7 * 256 * 512 + 256 * 256 * 512 = 32.875MB)サイズのファイルが理論上マッピング可能になりますが、サイズ自体が24bitのため16MB以上は使用不可となります。
8ブロック以内
- i_addr → データブロック
9ブロック以上
- i_addr[0〜6] → ブロックマップ → データブロック
- i_addr[7] → ブロックマップのブロックマップ → ブロックマップ → データブロック