ラベル UNIX の投稿を表示しています。 すべての投稿を表示
ラベル UNIX の投稿を表示しています。 すべての投稿を表示

2023年11月22日水曜日

UNIX V7 カーネル 

 ブートはどうにかなってきたので、そろそろカーネル本体に手を出していきたいのですが、どこから手を付けていいのか、正直途方に暮れています。

カーネルのコードは /usr/sys/ 以下にあり、/usr/sys/conf/ で make すれば、カーネルがビルドできるようです。その中の makefile を見る限るでは、l.s, mch.s, c.c あたりから見ていけば良さそうです。


この辺りは、カーネルのか解説本を見てもあまり参考になりません。解説本には、メモリ管理やプロセス管理のコードの説明がされていますが、移植を前提とした解説ではないので。この辺りは、PDP-11のハードに依存したコードが多いのも難しいところです。

暫くこの辺りのコードを眺めながら、突破口を探すことにします。

2023年7月9日日曜日

UNIX V7のブート 4

PDP-11が起動し最初にディスクから読み出されるコードは、ディスクの先頭に書かれているブートプログラムです。そのソースコードは、/usr/mdec/rpuboot.s です。UNIX V6だと、このプログラムがUNIXカーネルをディスクから読み出して実行するのですが、V7では、/bootを読み込んで実行します。そしてbootがUNIXカーネルを読み込んで実行します。bootのソースコードは、/usr/src/cmd/standaloneにあります。

ブートプログラムはアセンブリ言語で書かれています。コードのメインの部分を見ていきます。

/ now start reading the inodes
/ starting at the root and
/ going through directories
1:
	mov	$names,r1
	mov	$2,r0			/ ルートディレクトリのinode番号
1:
	clr	bno
	jsr	pc,iget			
	tst	(r1)			/ namesの終端か
	beq	1f
2:
	jsr	pc,rmblk		/ 
		br start		/ rmblkが正常の終了の場合はスキップ
	mov	$buf,r2			/ r2 = buf[]
3:
	mov	r1,r3			/ r3 = names[]
	mov	r2,r4			/ r4 = buf[]
	add	$16.,r2			/ r2 = 次のディレクトリエントリ
	tst	(r4)+			/ inodeは空か
	beq	5f
4:
	cmpb	(r3)+,(r4)+	/ ファイル名の比較
	bne	5f
	cmp	r4,r2			/ 
	blo	4b
	mov	-16.(r2),r0		/ r0 = inode番号
	add	$14.,r1			/ 
	br	1b
5:
	cmp	r2,$buf+512.	/ 
	blo	3b				/ buf[]の最後でない
	br	2b				/ buf[]の最後

/ read file into core until
/ a mapping error, (no disk address)
1:
	clr	r1				/ r1 = 0
1:
	jsr	pc,rmblk
		br 1f
	mov	$buf,r2
2:
	mov	(r2)+,(r1)+
	cmp	r2,$buf+512.
	blo	2b
	br	1b
1:
	clr	r1

6−9行で、ルートディレクトリのinodeを読み込んでいます。igetは、r0で指定した番号のinodeをinode[]にコピーします。

13行は、rmblkでルートディレクトリをbuf[]に読み込みます。15行目からは、1エントリごとにファイル名とnames[]を比較します。

一致した場合は、27行でinode番号をr0に読み込みます。r1に14を足して8行目に飛び、igetでファイル名が一致したファイルのinodeを読み込みます。

11行でファイル名が終わっていることを確認し、38行目からinodeのdi_addr[]に沿ってファイルをメモリーに読み込んでいきます。


次のリストは igetです。

/ get the inode specified in r0
iget:
	add	$15.,r0
	mov	r0,r5
	ash	$-3.,r0			/ r0 = (r0 + 15) / 8
	bic	$!17777,r0		/ r0 &= 0x01fff
	mov	r0,dno
	clr	r0
	jsr	pc,rblk
	bic	$!7,r5			/ r5 &= 0x0007
	ash	$6,r5			/ r5 *= 64
	add	$buf,r5			/ r5 += buf
	mov	$inod,r4
1:
	mov	(r5)+,(r4)+
	cmp	r4,$inod+64.
	blo	1b
	rts	pc

3-6行でinode番号をinodeが存在しているブロック番号に変換し、9行でそのブロックをbuf[]に読み込んでいます。

10-12行で目的のinodeのbuf[]内でのオフセットを求め、13ー17行でinode[]にコピーしています。


次のrmblkは、bnoで指定されたブロックを、inode内のdi_addr[]に従ってbuff[]に読み込みます。

/ read a mapped block
/ offset in file is in bno.
/ skip if success, no skip if fail
/ the algorithm only handles a single
/ indirect block. that means that
/ files longer than 10+128 blocks cannot
/ be loaded.
rmblk:
	add	$2,(sp)			/ リターンアドレスを2増やす
	mov	bno,r0
	cmp	r0,$10.
	blt	1f
	mov	$10.,r0			/ 間接参照
1:
	mov	r0,-(sp)		/ 
	asl	r0				/ 
	add	(sp)+,r0		/ r0 = bno * 3 
	add	$addr+1,r0		/ r0 += di_addr + 1
	movb	(r0)+,dno	
	movb	(r0)+,dno+1	/ dno = ブロック番号の下位2バイト
	movb	-3(r0),r0	/ ブロック番号の上位1バイト
	bne	1f
	tst	dno
	beq	2f
1:
	jsr	pc,rblk			/ ブロック読み込み
	mov	bno,r0
	inc	bno
	sub	$10.,r0
	blt	1f				/ 直接参照ならリターン
	ash	$2,r0			/ r0 *= 4
	mov	buf+2(r0),dno	/ dno = ディスク番号の下位2バイト
	mov	buf(r0),r0		/ r0 = ディスク番号の上位2バイト
	bne	rblk
	tst	dno
	bne	rblk
2:
	sub	$2,(sp)
1:
	rts	pc

ブロック番号が0から9までは直接参照で10は間接参照になりますが、コメントにあるとおり、2重間接参照には対応していません。

13行でブロック番号10以上は、間接参照に対応するため10に固定しています。

15-18行で、ブロック番号からdi_addr[]内でのディスク番号下位2バイトのオフセットを求めています。

22-24行は、ブロック番号が0だった場合、rmblkを呼び出した直後のアドレスにリターンします。

26-30行で、ブロックを読み込んだ後、直接参照の場合はリターンします。

31行目からは間接参照の処理で、29行で10引かれたディスク番号を4倍しオフセットを求め、先に読み込んだブロックからブロック番号を取り出します。

34-36行で、番号が0でなければブロックを読み出します。

ブロックが正常に読み込まれた場合は、9行でリターンアドレスが+2されているので、rmblkを呼び出した次の命令をスキップします。


これで、UNIXが起動する第一段階を追いかけられました。次は、2段階目の/bootになります。

2023年7月2日日曜日

UNIX V7のブート 3

ディレクトリは、/usr/sys/h/dir.hで定義されているdirect構造体のリストです。


#ifndef	DIRSIZ
#define	DIRSIZ	14
#endif
struct	direct
{
	ino_t	d_ino;
	char	d_name[DIRSIZ];
};

direct構造体のサイズは16バイトで,、最初の2バイトはinode番号です。続く14バイトがファイル名です。


ルートディレクトリのファイルを見てみると、rl2tunixだとinode番号は0x007dだとわかります。i-node番号からディスクイメージでのアドレスへの変換は、次の式になります。

 (inode番号 - 1)×dinod構造体のサイズ (64バイト)+ inodeブロックの先頭アドレス(0x400)

計算すると、rl2tunixのi-nodeのアドレスは0x2300になります。


di_addr[0]~di_addr[9]は直接参照で、ブロック番号0x000da5, 0x000da8, .... , 0x000dc0となっています。di_addr[10]は間接参照で、ブロック番号0x0dc3にブロック番号のリストがあります。こちらは3バイトではなく、4バイト単位でブロック番号が書き込まれています。

92ブロックの間接参照のブロック番号が書かれています。直接参照と合わせると、102ブロックに渡ってrl2unixが書き込まれていることがわかります。

これでUNIXカーネルのファイルまでたどり着くことができました。次は、ブートプログラムを見ていきます。



2023年6月29日木曜日

UNIX V7のブート 2

inodeブロックを見ていきます。inodeブロックは、ディスクイメージでは0x0400から始まっています。
inodeの構造は、/usr/sys/h/ino.h の dinode構造体で定義されています。この構造体のサイズは64バイトです。
 struct dinode
{
	unsigned short	di_mode;/* mode and type of file */
	short	di_nlink;    	/* number of links to file */
	short	di_uid;      	/* owner's user id */
	short	di_gid;      	/* owner's group id */
	off_t	di_size;     	/* number of bytes in file */
	char  	di_addr[40];	/* disk block addresses */
	time_t	di_atime;   	/* time last accessed */
	time_t	di_mtime;   	/* time last modified */
	time_t	di_ctime;   	/* time created */
};
#define	INOPB	8	/* 8 inodes per block */
/*
 * the 40 address bytes:
 *	39 used; 13 addresses
 *	of 3 bytes each.
 */
};
inode番号0は未使用で、ルートディレクトリのinode番号は2なので、ルートのinodeの位置は、ディスクイメージでは0x0440からです。


ルートディレクトリのファイルの位置は、0x044cからのdi_addr[]にブロック番号がリストされています。このアドレスは、下のコメントのように3バイトで表され、上位1バイト+下位2バイトで記録されています。最初の3バイトは0x0002d4で1ブロック512バイトなので、0x5a800からファイルのデータが保存されています。
アスキーダンプにunixの文字列が見られます。SIMHでルートを ls してみた結果が下で、対応していることがわかります。

2023年6月25日日曜日

UNIX V7のブート 1


ブートローダの移植から始めようとしているわけですが、ディスクドライブからカーネルを読み込まないといけないので、まずはファイルシステムを理解する必要があります。この辺りは参考になりそうな書籍やネット上の情報があるので助かります。

技術評論社から出ている「はじめてのOSコードリーディング」は、UNIX V6の説明ですが、基本はV7とほぼ同じなので参考になります。

コードだけを読んで理解するのは難しいので、ディスクイメージと照らし合わせながら理解していきます。SIMHのサイトからUNIX V7のSoftware Kitesをダウンロードし、ZIPファイルに含まれているunix_v7_rl.dskを使います。

http://simh.trailing-edge.com/kits/uv7swre.zip 

このディスクイメージのブロックサイズは512バイトです。

先頭のブロックはブートブロックで、システム起動時に最初に読み込まれるブートプログラムが格納されています。ディスクからカーネルを読み出すコードですが、とりあえずは手をつけずに置いておきます。

ブートブロックに続く1ブロック、ディスクイメージのダンプリストで0x0200から0x03FFまでがスーパーブロックになります。



スーパーブロックの構造の定義は、/usr/sys/h/filsys.h にある filsys 構造体です。NICFREEなどの定数は param.h に定義されてます。

 /*
 * Structure of the super-block
 */
struct	filsys {
	unsigned short s_isize;	/* size in blocks of i-list */
	daddr_t	s_fsize;   	/* size in blocks of entire volume */
	short  	s_nfree;   	/* number of addresses in s_free */
	daddr_t	s_free[NICFREE];/* free block list */
	short  	s_ninode;  	/* number of i-nodes in s_inode */
	ino_t  	s_inode[NICINOD];/* free i-node list */
	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 */
	time_t 	s_time;    	/* last super block update */
	/* remainder not maintained by this version of the system */
	daddr_t	s_tfree;   	/* total free blocks*/
	ino_t  	s_tinode;  	/* total free inodes */
	short  	s_m;       	/* interleave factor */
	short  	s_n;       	/* " " */
	char   	s_fname[6];	/* file system name */
	char   	s_fpack[6];	/* file system pack name */
};
これらデータは、ブートには直接関係ないのですが、ファイルシステムの構造が含まれています。
0x0200 : s_isize(2バイト)は、inodeブロックのブロック数です。PDP-11はリトル(ミドル)エンディアンなので、0x02D2で722ブロックになります。

0x0202: s_fsize(4バイト)は、ディスクドライブのブロック数です。上位の2バイト、下位の2バイトの順に並んでいます。0x00004650なので18000ブロックです。

スーパーブロックの次のブロックは、inodeブロックでinodeのリストです。このディスクイメージでは、722ブロック続きます。

2023年6月24日土曜日

UNIX V7の移植に挑む

PCCをZ8000に対応できそうな目処がたってきたので、UNIXの移植に手をつけていきます。何年越しのプロジェクトやねん、って感じですが、PCCの作業にすこし飽きてきたのと、実際のコードをコンパイルしながらデバッグしていった方が現実的な気がしてきたきたからです。途中で挫折しそうな気がしますが、気長にやっていきます。なにせ趣味(暇つぶし)ですから。

まず第一歩として、githubの unix-history-repo からV7のソースコードをクローンしました。

$ git clone git@github.com:dspinellis/unix-history-repo.git -b Research-V7-Snapshot-Development --depth 1 
ざっと眺めてみて正直どこから手を付ければ良いのか、ちょっと悩みます。少しでも成果が見えて、進んでいる実感がないと継続は難しいですからね。やはり、ブートローダあたりからかなあ。

2020年12月26日土曜日

Z8000にPortable C Compilerを

かなりブログの投稿をさぼっていたのですが、PCCをZ8000に移植(対応?)させようと格闘しています。 現在挑んでいるのは、最新のPCC1.1.0ではなく、70年代のV7 UNIXに含まれていたANSI C 以前のPCCです。 
作業を単純化するため、ノンセグメントモードだけに対応しているので、実質Z8002用です。 過去には、i8086, MC68000 と共にZ8000版も有ったようなのですが、ネット上を探し回りましたが見つからず、現在では失われてしまっているようです。 まあ、40年もまえのことなので仕方がないですね。  
現在は、アーキテクチャの似ているPDP-11版をベースに、Z8000のコードを吐けるよう段階的に書き換えていっています。  

下は引数を2つ取って足し算をして返すだけの関数をコンパイルした例です。 

 C言語
add(a, b) int a, b;
{
        a += b;
        return a;
}
Z8000アセンブリ
	.data
	.text
	.globl	_add
_add:
	call	csv
	sub	r15,#F1
	ld	r2,4(r14)
	add	r2,6(r14)
	ld	4(r14),r2
	ld	r2,4(r14)
	jp	cret
	jp	cret
	.F1 = 0.
	.data
最適化は全くされていないので無駄が多いコードですが、正しくアセンブリを出力できるようになってきています。
コンパイラの作業が終わっても、次はアセンブラとリンカが必要なので、まだまだ時間が掛かりそうで気が遠くなります。いつか公開できる日が来ることを祈りつつ。

2020年7月30日木曜日

Z8002にMMUを その2

今回作るMMUは、高速SRAMをマッピングテーブに使った回路になり、下の図のような構成になります。


Z8002のアドレス下位12ビットは、そのまま物理アドレスの下位12bitとしてメモリに渡します。Z8002のアドレス上位4bitは高速SRAMのアドレスに入れ、読み出されるデータ8bitを物理アドレスの上位8bitとします。これで合計20bit( 1Mバイト分)のアドレスに変換します。

アドレス変換を行うには、高速SRAMにマッピングデータを書き込む必要があるので、マルチプレクサとバッファをつけます。高速SRAMは0xC000からのI/O空間に置きアクセスします。マルチプレクサは、変換する仮想アドレスの上位4ビットとI/Oアクセスするアドレスを切り替えるために使います。バッファは、高速SRAMをデータバスに接続するために必要です。

本当は、アドレスの上位4ビットをバイパスするために、高速SRAMとメモリの間にさらにマルチプレクサを追加したいのですが、部品と配線量が増えるので見送ります。これはCPUリセット後に高速SRAMを初期化するまでの間、MMUを無効にしておくために必要なのですが、単純に高速SRAMを非アクティブにしおいて データバスをプルダウンすることにします。このため上位8ビットが0に固定され、MMUが初期化されるまでの間はメモリの先頭の4kバイトしかアクセスできなくなってしまいます。

いま気づいている点は、
  • ページがマップされていない状態を設定できない
  • ページごとに書き込み禁止を設定できない
  • ページに書き込みがあったか記録できない
などですが、実現するには高速SRAMを追加する必要があります。これらの機能がどれくらい必要なのか判断がつかないので、MMUは最小限の構成にしておいて、とりあえず動かすことを優先します。


2020年7月12日日曜日

Z8002にMMUを その1

MMUについて考えていきます。

Z8001にはZ8010というMMUが準備されていますが、Z8002にはつなげられないため、必然的に自作することになります。今回はUNIX V6を動かそうとしているので、DECのPDP-11のメモリ管理機構を参考にします。ネットの情報と「はじめてのOSコードリーディング」を読んだところでは、ざっと下のような感じです。
  • 64kバイトの物理アドレスは、8つのセグメントに分割されている
  • セグメントの最大サイズは8kバイト
  • 各セグメントは、64バイト単位でサイズが可変できる
  • 各セグメントは、物理アドレスの64バイト境界にマップできる
  • カーネルとユーザで空間を分離できる
  • 命令とデータの空間が分離できる
  • セグメント毎に読み込み専用に指定できる
完全に同じものを作るのは難しそうなので、次のように単純化します。
  • セグメントサイズは固定
  • セグメントの物理アドレスへのマッピングはセグメントサイズ境界に揃える
  • 読み込み専用指定は実装しない
  • カーネルとユーザで空間は分離する
  • 命令とデータで空間は分離する
セグメントのサイズは、4kバイト固定にします。マッピングテーブルに高速SRAMを使ったアドレス変換回路を組むのですが、高速SRAMのデータバスは8ビットなので、セグメントサイズを4kバイトにすると、上位8ビット + 下位12ビットでちょうど1Mバイトのメモリを扱うことができます。

セグメントのマッピング位置をセグメントサイズ境界に揃えると、メモリの使用に無駄が多くなりそうですが、PDP-11のように細かくしようとするとセグメント情報を記録するマップが大きくなってしまいます。また、加算回路が必要になるためアドレスが確定するまでの遅延が大きくなり配線量も増えるので諦めます。その分、セグメントサイズを4kバイトに小さくすることにします。

読み込み専用指定は、テキストセグメントへの書き込み禁止などの保護のために使うのでしょうか。今回は設定できなくても特に問題はないと思うので省略します。

カーネルとユーザ空間の分離は、Z8000がシステムとノーマルモードを持っており、どちらで動作しているか信号を出しているので実装できそうです。

命令とデータの空間分離は、Z8000では64kバイトしかアドレス空間がないので、プロセスが使えるメモリを増やすために有用です。これも信号が出ているので実装してみます。

2020年6月27日土曜日

Zilog Z8002でUNIX

CP/M-8000の移植が終わってから何もやってなかったのですが、注文していたZ8002が届いたので再始動します。

このZ8002でUNIX V6を動かすのが次の目標です。

ZilogのZEUSとかOnyx SystemsのC8002とか、Z8000でUNIXを動かしている製品があったので、不可能ではないのは確かです。ネット検索してみましたが、趣味でZ8000でUNIXを動かした人はいないようなので、挑みがいもあります。CP/M-8000よりかなりハードルが高いのは確かですが、また1年か2年越しで達成したいと思います。なんせ趣味なので気長にゆっくり楽しめれば、それでOKです。
ちなみに、私は、Linuxは使いますがUNIXの経験は全くありません。OSの中身を勉強するにも良い機会になると期待しています。

CPUをZ8001からZ8002に切り替えた理由ですが、Z8001のセグメントモードはいろいろと効率が悪いと感じたからです。Z8000のアドレス空間は基本64kバイトです。Z8001ではセグメントを導入し、128セグメント×64kバイトで8Mバイトまで拡張しています。
Z8001では、1セグメントは64kバイトしかないのにアドレスを表すには4バイト必要だったり、即値アドレスを読み込むのに1サイクル余分に必要になったりと無駄が多い気がしてなりません。Z8000のアーキテクチャだと、これらの無駄がないノンセグメントなZ8002の方が素直で扱いやすい気がします。

UNIX V6を動かすに必要なメモリ容量ははっきりわからないのですが、V6が開発されていたPDP-11では256kバイトか4Mバイト扱えたそうなので、最低256kバイトのメモリは必要になりそうです。
64kバイト以上のメモリをZ8002で扱うには、アドレス空間を拡張するMMUを外付けする必要が出てくるので、まずはこの辺りから始めます。