Loading [MathJax]/extensions/tex2jax.js

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にあります。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/ 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です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/ 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[]に読み込みます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/ 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構造体のリストです。


1
2
3
4
5
6
7
8
#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カーネルのファイルまでたどり着くことができました。次は、ブートプログラムを見ていきます。