PDP-11/40用のm40.sもしくはPDP-11/45用のm45.sで定義されているbackupは謎につつまれている。
まず、trap.cの2812行目セグメンテーション例外の処理でbackup()を呼び出している。 引数はu.u_ar0すなわち、context中に格納されているr0のアドレスのみである。 backupはC言語での名前で、アセンブラでは_backupであることに注意しよう。 1047行目からのbackupは_backupのサブルーチンである。 たいへんまぎらわしいのだが、文句を言っても始まらないので先に進もう。
Lions本の記述は第12章トラップとシステムコールの「ユーザーモードのトラップ」の項(訳書352ページ)にある。 2810行目あたりから見ると「ユーザーモードスタック領域を拡張するためにオペレーティングシステムの手助けが必要である場合を表している。アセンブラルーチンの``backup''はトラップをひきおこした命令が実行される前の状況を再構築するために使われる」とある。 直後には「``backup''プロシージャは自明ではなく、その理解のためはPDP11アーキテクチャの多様な側面を慎重に考慮する必要がある。興味のある読者に自分でおいかけてもらうよう残してある」、 さらに「PDP11/40のためのコメントにあるように、プロセッサーがすべての可能性を解明するのに十分な情報を残してくれないので、``backup''はかならずしもうまくいかないかもしれない」とつづく。
backupは、m40.sの長大な領域(Lions本では1008行目から1240行目までの)を占めている。 コメントだけひろってみると、「難しい部分」「11/40にないssr2レジスターをシミュレートする」という部分が目につく。 Lions本では2-5に「PDP11/40にはメモリ管理状態レジスタがふたつある」という説明があって、 あとのほうの「SR2はそれぞれの命令のフェッチ開始時に16bit仮想アドレスをロードする」と説明されている。 「11/40にこのレジスタはいったいあるのかないのか?」といった疑問がのこるが、 Lions本の説明は簡単すぎてよくわからない。
v6はメモリの使用量を極力減らす戦略をとっている。 そのひとつとして、ユーザーのスタック領域はあらかじめすべてを物理メモリに割り当てるのではなく、必要が生じた時のみ確保して割り当てる方式をとっている。 すなわち、実行中のユーザープログラムがスタックをアクセスしたが割り当てられていなかった場合、実行を中断してtrapルーチンに制御が移る。 trapルーチンがこの状況を検出するとメモリを割り当てをおこない、ユーザープログラムの実行を再開する。 PDP-11/45の場合は実行の再開に必要な情報をマシンが保持しており、backup()ルーチンによりこの情報を取得できる。 ところが、PDP-11/40の場合はかならずしもすべてのデータが揃わない。 そこで、さまざまな情報から推測しよう、というのがm40.sのbackup()の趣旨である。
このへんの理解に必要な情報のうちLions本に欠けている部分についてはメモリ管理機構の項に説明をおぎなった。 これを参照しながら、てはじめに、PDP-11/45用のm45.sを見ることにしよう。 こちらはわずか26行におさまっており、「難しい部分」が存在しない。
.globl _backup .globl _regloc _backup: mov 2(sp),r0 movb ssr+2,r1 jsr pc,1f movb ssr+3,r1 jsr pc,1f movb _regloc+7,r1 asl r1 add r0,r1 mov ssr+4,(r1) clr r0 2: rts pc 1: mov r1,-(sp) asr (sp) asr (sp) asr (sp) bic $!7,r1 movb _regloc(r1),r1 asl r1 add r0,r1 sub (sp)+,(r1) rts pc
これはm40.sの_backup先頭の7命令をのぞいたものとおなじである。 1:は簡単なサブルーチンなのでこれから見ていこう。
reglocはsystm.h(0237)で定義されており、実体はtrap.c(2677)にある。 その中身はreg.hで定義する配列で、コンテキスト中に目当てのレジスタが保存されているオフセットが入っている。 適当なレジスタについてreglocの値を引いて2倍して(ワードなので)r0の保存されているアドレスに加算すると、目的のアドレスが得られる。
引数として使われているr1の下3bitがレジスタ番号である。 目当てのレジスタの保存内容からr1の上5bitの値を引いて処理は終了する。 r0は不変である。
もういちど先頭から見直してみると、 uに保存されているレジスタについて以下の操作をおこなっていることがわかる。
ここまでわかったところで、m40の_backup先頭の共通しない部分をみてみる。 r2をpush/popしているのは_backupの返り値につかわれているからである。 backupはr2を返すが、これはssr+2とssr+3に代入される。 ただし、jflgが非0ならあとの処理はスキップする。 すなわち、この場合、補正はおこなわれない。
1008 /* -------------------------- */ 1009 .globl _backup 1010 /* -------------------------- */ 1011 .globl _regloc 1012 _backup: 1013 mov 2(sp),ssr+2 1014 mov r2,-(sp) 1015 jsr pc,backup 1016 mov r2,ssr+2 1017 mov (sp)+,r2 1018 movb jflg,r0 1019 bne 2f注意したいのはbackup()の引数をssr+2に保存しているところである。 単に使えるメモリアドレスというだけのことで、ssr+2を使う必然性はない。
ここで補正につかわれているssrについてみてみよう。 m40.sではSSR0がssrに、SSR2がssr+4に保存されている。 m45.sではさらにSSR1がssr+2に保存されている。 ということは、PDP-11/45に存在してPDP-11/40に存在しないのはSSR1で、 backupはSSR1に相当するデータを計算しているのである。 というわけで、コメント
1044 / hard part 1045 / simulate the ssr2 register missing on 11/40のssr2はssr1の誤りである。 これでようやく話の辻褄があった。
m40.sではSSR1相当の部分にデータを格納している。 ということはそこには普通のメモリー(に見えるもの)がある、ということでよいだろう。
「難しい部分」は実行が失敗した命令のオペコードを解析して、 適切な補正値を推測する。 これを理解するには、PDP-11のオペコードの体系を頭に入れておく必要がある。 PDP-11/40のハンドブックの付録に"NUMERICAL OP CODE LIST"というのがあって、そちらを見るとよりわかりやすい。
1237 .bss 1238 bflg: .=.+1 1239 jflg: .=.+1
r2はSSR1に相当する補正値を蓄積するのに使われる。それぞれのbyteについて、 下3bitがレジスター番号、上5bitが補正値であることに留意しよう。
下請けのsetreg(1197)は引数r0からr2(の下8bit)を計算する。 その際、r0のbit0-2をレジスター、bit3-5をアドレッシングモードとみなす。
モード | 補正値 |
自動加算 | 1 |
間接自動加算 | 2 |
自動減算 | -1 |
間接自動減算 | -2 |
1196 setreg: 1197 mov r0,-(sp) 1198 bic $!7,r0 1199 bis r0,r2 1200 mov (sp)+,r0 1201 ash $-3,r0 1202 bic $!7,r0 1203 movb 0f(r0),r0 1204 tstb bflg 1205 beq 1f 1206 bit $2,r2 1207 beq 2f 1208 bit $4,r2 1209 beq 2f 1210 1: 1211 cmp r0,$20 1212 beq 2f 1213 cmp r0,$-20 1214 beq 2f 1215 asl r0 1216 2: 1217 bisb r0,r2 1218 rts pc 1219 1220 0: .byte 0,0,10,20,-10,-20,0,0
下請けのfetchは引数および結果がr0である。 ユーザー空間の(r0)にあるワードを読んでr0に返す。 失敗した場合はr2をクリアしてr0に-1を返す。
1222 fetch: 1223 bic $1,r0 1224 mov nofault,-(sp) 1225 mov $1f,nofault 1226 mfpi (r0) 1227 mov (sp)+,r0 1228 mov (sp)+,nofault 1229 rts pc 1230 1231 1: 1232 mov (sp)+,nofault 1233 clrb r2 / clear out dest on fault 1234 mov $-1,r0 1235 rts pc
fetchを呼ぶときのr0にはSSR2から読み出した値が入っている。 すなわち、実行が失敗した命令の(ユーザー空間での)アドレスである。 この命令のオペコードの8進数表記XXxxxxのXXによりt00からt17に分岐する。
1047 backup: 1048 clr r2 / backup register ssr1 1049 mov $1,bflg / clrs jflg 1050 mov ssr+4,r0 1051 jsr pc,fetch 1052 mov r0,r1 1053 ash $-11.,r0 1054 bic $!36,r0 1055 jmp *0f(r0) 1056 0: t00; t01; t02; t03; t04; t05; t06; t07 1057 t10; t11; t12; t13; t14; t15; t16; t17 1058
さらに、オペコードが(8進数で)00Xxxxもしくは10Xxxxの場合は、 Xの値によりu0からu7に分岐する。
1059 t00: 1060 clrb bflg 1061 1062 t10: 1063 mov r1,r0 1064 swab r0 1065 bic $!16,r0 1066 jmp *0f(r0) 1067 0: u0; u1; u2; u3; u4; u5; u6; u7命令を仕分けして処理に振り分ける。 さいわい、コメントでおおよその見当がつく。 1099行目の
br setregは
jsr pc,setreg rts pcの略である。
1069 u6: / single op, m[tf]pi, sxt, illegal 1070 bit $400,r1 1071 beq u5 / all but m[tf], sxt 1072 bit $200,r1 1073 beq 1f / mfpi 1074 bit $100,r1 1075 bne u5 / sxt 1076 1077 / simulate mtpi with double (sp)+,dd 1078 bic $4000,r1 / turn instr into (sp)+ 1079 br t01 1080 1081 / simulate mfpi with double ss,-(sp) 1082 1: 1083 ash $6,r1 1084 bis $46,r1 / -(sp) 1085 br t01 1086ror:0060xx, rol:0061xx, asr:0062xx, asl:0063xx。 rorb:1060xx, rolb:1061xx, asrb:1062xx, aslb:1063xx。 sxt:0067xx。 ここまではu5に飛ばされる。 mfpi:0065xx, mtpi:0066xx。 mfpd:1065xx, mtpd:1066xx。106[47]xxは未使用。 mark:0064xxが無視されているのは要検討。 1078行目では命令を0026xxに書き換えて、ダブルオペランド処理をおこなうmovの処理ルーチンに渡す。 ここの26は(sp)+に相当。命令を変換して処理をまとめるのは賢い方法かもしれないがずいぶんな落とし穴。 1083行目も同様で、命令を65xx46に書き換えて、mov処理ルーチンに渡す。 46は-(sp)相当。
1087 u4: / jsr 1088 mov r1,r0 1089 jsr pc,setreg / assume no fault 1090 bis $173000,r2 / -2 from sp 1091 rts pc0004xxxはjsrで、10x4[0-3]xxはemt、10x4[4-7]xxはtrapである。
1093 t07: / EIS 1094 clrb bflg07xxxxにある拡張命令。 075040-076777は未使用。
1096 u0: / jmp, swab 1097 u5: / single op 1098 mov r1,r0 1099 br setreg000xxx: JMP, RTS, SPL, NOP, SWAB, BR等 100xxx: BPL, BMI 005xxx: 未定義 105xxx: CLRB, COMB, ...
bit15-12が命令の種類、bit11-6がソース、 bit5-0がデスティネーションの場合(xxSSDD)。 1117、1120行目でsetregを呼び出して、ソース、デスティネーションそれぞれについて 補正値を概算する。
1101 t01: / mov 1102 t02: / cmp 1103 t03: / bit 1104 t04: / bic 1105 t05: / bis 1106 t06: / add 1107 t16: / sub 1108 clrb bflg 1109 1110 t11: / movb 1111 t12: / cmpb 1112 t13: / bitb 1113 t14: / bicb 1114 t15: / bisb 1115 mov r1,r0 1116 ash $-6,r0 1117 jsr pc,setreg 1118 swab r2 1119 mov r1,r0 1120 jsr pc,setregSSとDDについて補正値を概算した結果がr2に入っている。 r1はもとの命令のまま。
1122 / if delta(dest) is zero, 1123 / no need to fetch source 1124 1125 bit $370,r2 1126 beq 1fソースの補正値が0ならソースを取ってくる必要がないのでそのまま終了。
1128 / if mode(source) is R, 1129 / no fault is possible 1130 1131 bit $7000,r1 1132 beq 1fレジスタモードであればfaultが起きるはずはない。
1134 / if reg(source) is reg(dest), 1135 / too bad. 1136 1137 mov r2,-(sp) 1138 bic $174370,(sp) 1139 cmpb 1(sp),(sp)+ 1140 beq t17174370は上下バイトの上5bitに対するマスク。 ソースとデスティネーションのレジスタが同じだとまずい。 何でt17なのか、という話もあるが要するにあきらめる。
1142 / start source cyclesource cycleを再現するのは、プロセッサの内部状態を復元するためか。
1143 / pick up value of reg 1144 1145 mov r1,r0 1146 ash $-6,r0 1147 bic $!7,r0 1148 movb _regloc(r0),r0 1149 asl r0 1150 add ssr+2,r0 1151 mov (r0),r0u構造体からソースレジスタの値を取ってくる。 ssr+2を使っているのでびっくりするが、単に仮置きされているだけである。
1153 / if reg has been incremented, 1154 / must decrement it before fetch 1155 1156 bit $174000,r2 1157 ble 2f 1158 dec r0 1159 bit $10000,r2 1160 beq 2f 1161 dec r0補正値がプラスならデクリメント、2のbitが立っていればさらにデクリメント。
1162 2: 1163 1164 / if mode is 6,7 fetch and add X(R) to R 1165 1166 bit $4000,r1 1167 beq 2f 1168 bit $2000,r1 1169 beq 2f 1170 mov r0,-(sp) 1171 mov ssr+4,r0 1172 add $2,r0 1173 jsr pc,fetch 1174 add (sp)+,r06はindex mode、7はindex deferred mode。 X(R)のXは命令の次のアドレスに入っている。
1175 2: 1176 1177 / fetch operand 1178 / if mode is 3,5,7 fetch * 1179 1180 jsr pc,fetch 1181 bit $1000,r1 1182 beq 1f 1183 bit $6000,r1 1184 bne fetch 1185 1: 1186 rts pc3はauto-increment deferred、5はauto-decrement deferred、7はindex deferred。 間接アドレッシングモードの場合は繰り返し適用してオペランドの値をr0に得る。 ここでせっかく取得した値は捨てられてしまう。 実際にアクセスできることを確認して、無限ループに陥るのを防いでいる、というのが一応の説明だろう。 ではデスティネーションのほうは確認しなくていいのか、という疑問は残る。
不正な命令など、backupによる復元が不可能な場合。
1188 t17: / illegal 1189 u1: / br 1190 u2: / br 1191 u3: / br 1192 u7: / illegal 1193 incb jflg 1194 rts pc単純にあきらめる。 陽に処理が記述されていない命令についてもひととおりチェックするとこの項はおしまい。 いくつか残った疑問はいずれそのうち解決することにしよう。