2023年11月22日水曜日

UNIX V7 カーネル 

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

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


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

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

2023年9月2日土曜日

NEC V60 入手

 Z80,000やZ80320と同じように入手を諦めていたのですが、たまたまネットで見つけ、ちょっと高価でしたが購入してしまいました。


同じ所でV70も販売していたのですが、V70はほとんど資料がネット上になく、入手しても走らすのは無理です。私はレトロCPUコレクターではないので、手に入れたら動かしたい派です。とは言え、入手したまま引き出しにしまったままのCPUが結構たまってきていますが。

このCPUはかなり高機能で、以下のような特徴があります。

  • 32本の32bit汎用レジスタ 
  • 浮動小数点演算命令
  • V20/V30エミュレーション
  • デマンドページングをサポートできるMMU

    詳しくは、Wikipedia https://en.wikipedia.org/wiki/NEC_V60 を参照して下さい。
    データシートがあるサイトも上げておきます。http://mess.redump.net/datasheets/nec

    V60は32bitCPUなのですが、モトローラのMC68000と同じように、データバスは 16bitになっています。アドレスバスも24bitで、MC68020のような完全な32bit CPUとはなっていません。そのため、いままで弄ってきたZ8001と同じくらいの配線量で動かせるはずです。この点が自分には結構重要で、Z8000のボードを何枚か作ってきていますが、はんだ付けやラッピングで配線して作るのは、16bitが限界だと感じています。

    趣味に使える時間は限られるので、Z8000をいじりつつ気分転換がてら少しずつ進めたていきたいと思います。


    2023年8月3日木曜日

    UNIX V7のブート 5

     ブートローダの2段目は、C言語とアセンブリ言語で書かれたbootで、そのソースコードは、/usr/src/cmd/standaloneにあります。M.sにあるstartから実行が開始され、MMUの設定とコードの移動が行われたのち、C言語でかかれたmain()に実行が移ります。

    / set kernel I+D to physical 0 and IO page
            clr     r1              ! start from 0x00000
            mov     $77406,r2       ! 127 blocks R/W
            mov     $KISA0,r3
            mov     $KISD0,r4
            jsr     pc,setseg
            mov     $IO,-(r3)
            clr     r1
            mov     $KDSA0,r3
            mov     $KDSD0,r4
            jsr     pc,setseg
            mov     $IO,-(r3)
    
    / set user I+D to physical 64K (words) and IO page
            mov     $4000,r1        ! start from 0x20000
            mov     $UISA0,r3
            mov     $UISD0,r4
            jsr     pc,setseg
            mov     $IO,-(r3)
            mov     $4000,r1
            mov     $UDSA0,r3
            mov     $UDSD0,r4
            jsr     pc,setseg
            mov     $IO,-(r3)
    
    / enable map
            mov     $65,SSR3        / 22-bit map ! Split I+D for Kernel and User
            bit     $20,SSR3
            beq     1f              ! Check if 22-bit actived
            mov     $3,MSCR
    1:
            mov     $30340,PS
            inc     SSR0            ! Enable MMU

    ブートローダは、1-24行でカーネルとユーザのIとD空間用にMMUを設定したのち、MMUを有効にします。この段階ではブートローダは カーネルモードで実行されているので、PSWを現在カーネル、以前をユーザに32行で設定しています。

    このままでは、ローダ自身がじゃまでUNIXカーネルを読み込めないので、ユーザ空間に移動させます。

    / copy program to user I space
            mov     $_end,r0
            asr     r0
            clr     r1
    1:
            mov     (r1),-(sp)
            mtpi    (r1)+
            sob     r0,1b
    
    
    / continue execution in user space copy
            mov     $140004,sp      ! 0xc004
            clr     *$KDSA6
            mov     $140340,-(sp)   ! PSW:0b1100000011100000
            mov     $user,-(sp)     ! Return address
            rtt                     ! Return from trap

    7行目のmtpi命令は、以前いたモードのI空間にデータをスタック経由でデータを書き込みます。繰り返し命令を使って、カーネルモードから以前のユーザモードのI空間にローダのコードがコピーされます。

    14-16行目は、スタックにカーネルからユーザモードに戻るようなPSWと、userのアドレスを積み、rtt命令でトラップからの復帰を装ってモード切替とジャンプを実行しています。

    この後は、C言語で書かれた main()に実行が移ります。

    2023年7月29日土曜日

    Z8001MBex その1

    新しくZ8001CPUボードを作製しました。今までは、ラッピングワイヤを直接はんだ付けして配線していましたが、今回初めて、ユニバーサル基板にピンヘッダをたてワイヤラッピングで配線してみました。この方法は、ピンヘッダとICソケットとの間を配線するのが単調で根気がいります。とくにPLCCソケットは苦痛でした。ラッピングでの配線はやっていて楽しいのですが、間違いに気づいたときは修正が大変です。ほどきたい配線の上にすでに配線されていると、それもほどかないといけません。一度ほどくとワイヤが傷んでしまっているので、2回目巻きなおすと折れたりするので気を使います。年で眼も弱ってきているので、そろそろPCBが作れるようにならないとだめですね。


    今回のボードは、MMUを搭載しCP/M-8000以外のOSも移植することを考えています。初代のボードでは、Z8010 MMUを使ってメモリー管理ができるように考えていたのですが、何故かうまく動作せず。結局、CPLDでセグメントアドレスを変換するロジックを組んで、CP/M-8000が動作するようにしました。しかし、この方法ではアドレス変換が固定で融通が利かず、CP/M-8000以外のOSを動かすのは難しくなります。

    新しいボードでは、Z8010を使うことは諦めて、MMUを自前で組むことにしました。これに先立って、Z8002 CPUを使ったボードで、MMUを自作できそうか簡単な回路を組んで試してあったのですが、実際に作ってみると配線量の多さからかなり苦戦しました。MMU以外は、最初のボードとほぼ同じです。

    自作MMUの仕様は下のようになっています。

    • 高速SRAMを使ってページテーブルを構成する
    • ページテーブルをメモリ空間に置く
    • ページサイズ 2Kバイト
    • 論理アドレス空間 1Mバイト(16セグメント)
    • 物理アドレス空間 1Mバイト
    • 命令とデータ空間を分離可能
    • システムとノーマルで空間を分離可能
    手持ちで256kビットの高速SRAMを持っていたので、これを使ってページテーブルを構成しました。試験的に作ったZ8002ボードでは、I/O空間にページテーブルを置いたのですが、Z8001ではメモリ空間に置くことにしました。ページテーブルはSRAMの一部しか使わないので、未使用の部分が通常のメモリとして使えます。
    また、メモリ空間だとブロック転送命令が使えるので、ページテーブルのコピーや移動が短いコードで行なえます。

    ページサイズとアドレス空間のサイズは適当に決めたもので、これが最適なのかはわかりません。欲張ると配線量が増えるので、自作するには、これ位がちょうど良いかなと思っています。

    命令とデータ空間の分離は、CP/M-8000を動かすためには必要な機能です。また、Z8001には8086のように8ビット命令がないため、コードが大きくなりがちです。64kバイトのセグメントサイズに縛られるZ8000では、データを別空間に置ける機能はあったほうが良さそうです。

    システムとノーマルで空間を分離する機能は、UNIXのようなOSを移植するために付けておきました。一応、CP/M-8000でも使われているようなのですが、役には立っていないようです。

    今回は次のような機能は見送っています。
    • ページを無効に設定できる
    • ページを書き込み禁止に設定できる
    • ページにアクセスがあったか記録する
    最初の2つは、あとからでも実装できると思います。しかし、ページアクセスの記録は、別のメモリを積むか、高速SRAMにアドレス変換後に書き込む回路を追加する必要があり、実現が難しいので諦めています。

    すでに、CP/M-8000の移植は完了しています。初代のボードとほとんど同じなので、MMUの初期化を追加し、初代のボードと同じセグメントアドレス変換になるよう合わせれば動きました。

    そして今、UNIX V7を移植しようと挑んでます。

    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 
    ざっと眺めてみて正直どこから手を付ければ良いのか、ちょっと悩みます。少しでも成果が見えて、進んでいる実感がないと継続は難しいですからね。やはり、ブートローダあたりからかなあ。