はじめてのOSコードリーディング個人メモ【PDP-11/40 アセンブリ編】
PDP-11/40におけるアセンブリ構文
PDP11のアセンブリは、その他のアセンブリと似ているところもあるが、初見だとなかなか読めず苦戦するのでまとめます。
なお、これからまとめる内容は、Lions本と呼ばれる有名なOSの教科書を参考にしています。
(リンク以外のオレンジの字は私の独り言です。読み進めたら解決できるかも、な部分。)
なお、この記事の対象者は主に下記の本を読んでいる人向けです。
おさらい
PSWと汎用レジスタについて
PDP11のニーモニック(命令の種類) について見ていく前に、PSWと汎用レジスタの構成についてまとめます。
これを理解していないと、ニーモニックの意味が理解できません。
算術演算 (ビット演算)
演算 | 例 |
& (AND) |
1100 & 1001 = 1000 |
| (OR) |
1100 | 1001 = 1101 |
! / ~ (NOT) |
~(1100) = 0011 |
^ (XOR) |
1100 ^ 1001 = 0101 |
>> (右シフト) |
1100 >> 1 = 0110 |
<< (左シフト) |
0011 << 2 = 1100 |
上記演算した結果、オーバーフローしたら PSWの Vフラグが立つといったことが起こります。
用語整理
用語 | 意味 |
ニーモニック | 元々オペコードと呼ばれる数字の羅列を命令としてCPUは扱っているが、それを人間にわかりやすく短い文字列としたもの。(例: mov ) |
オペランド | 被演算子。mov A, B のA とB のこと。1 + 2 の 1 と2 もオペランドと言える。 |
アドレッシングモード | レジスタへの参照の仕方。(r0) や4(r1) などのような表記がそれ。 |
アドレッシングモード一覧
アドレッシングモードとは、レジスタを参照したときにそれをどう扱うかを決定するモードです。
rN
は汎用レジスタで、実際にはr1
やr2
のような表記になります。
基本
表記 | 説明 |
rN |
rN を参照して、それをオペランドとする。 |
(rN) |
rN を参照して、それをアドレスとしてオペランドを取得する。 |
(rN)+ |
rN を参照して、それをアドレスとしてオペランドを取得する。その後rN の内容をインクリメントする。 |
*(rN)+ |
rN を参照して、それをオペランドへのポインタのアドレスとする。その後rN の内容を 2 だけインクリメントする。 |
-(rN) |
実行前にrN の内容をデクリメントし、その値をアドレスとしてオペランドを取得する。 |
*-(rN) |
実行前にrN の内容を 2 だけデクリメントし、それをオペランドへのポインタのアドレスとする。 |
X(rN) |
rN + X がオペランドのアドレスとする。X は任意の数値が入る (例: 4(r3) )。 |
*X(rN) |
rN + X がオペランドへのポインタのアドレスとする。 |
r6
とr7
はそれぞれ、sp
(スタックポインタ) と pc
(プログラムカウンタ) と表記される場合もあります。
またsp
のアドレッシングモードは一部異なります。
spのアドレッシングモード
表記例 | 説明 |
(sp) |
スタックのトップをオペランドとする。 |
(sp)+ |
スタックのトップをオペランドとする。実行後にそれをポップする。 |
*(sp)+ |
スタックのトップをオペランドのアドレスとする。実行後にそれをポップする。 |
-(sp) |
値をスタックにプッシュする。 |
X(sp) |
スタックのトップからX 番目をオペランドとする。 |
*X(sp) |
スタックのトップからX 番目をオペランドのアドレスとする。 |
その他
表記例 | 説明 |
$n |
n をそのまま使う。 |
*$n |
n をアドレスとして使う。 |
ニーモニック一覧
ここからは、[src] / [dest]
は任意のオペランド (値やレジスタ) が入ると思ってください。
[label]
には飛ぶ先のラベル (例: 1f
)などが入ります。
ちなみに、nf
はForwardで次のnラベルに飛ぶという意味で、nb
はBackwardなので、前のnラベルに飛ぶという意味です。
演算後に状態フラグをいじるもの
先に、状態フラグ N, Z, V, C
を書き換えるニーモニックをまとめます。
これのあとに条件分岐のニーモニックが基本続きます。
ニーモニック | 意味 |
bit [src], [dest] |
[src] と[dest] でAND演算をする。その結果をもとに状態フラグを書き換える。 |
cmp [src], [dest] |
[src] と[dest] を比較し状態フラグを書き換える。例えば[src] が[dest] より小さい場合、Nに1が立つ (= [src] - [dest] )。等価であれば Zに1が立つ。 |
tst [src] |
[src] をもとに状態フラグを書き換える。 |
条件分岐
下記は後に出てくるjmp
とは違い、飛べる範囲が -128 ~ 127
と決まっているようです。
ニーモニック | 意味 |
beq [label] |
結果が0 (Z=1 )のときに[label] へ飛ぶ。(Branch if equalsの略) |
bec [label] |
C=0 ならば[label] へ飛ぶ。 |
bne [label] |
結果が非0 (Z=0 )のときに[label] へ飛ぶ。(Branch if not equalsの略) |
bge [label] |
比較結果が大きいもしくは等しい(e.g. N=0 && V=0 ) ときに[label] へ飛ぶ。(Branch if greather than or equal toの略) |
ble [label] |
比較結果が小さいもしくは等しい(e.g. N != V ) ときに[label] へ飛ぶ。(Branch if less than or equal toの略) |
bhi [label] |
結果が高い (e.g. C=0 && Z=0 )のとき[label] へ飛ぶ。(Branch if higherの略: “高い” という表現がわかりにくい) |
bhis [label] |
結果が高い、もしきは等価 (e.g. C=0 )のとき[label] へ飛ぶ。(Branch if higherの略: “高い” という表現がわかりにくい) |
blo [label] |
結果が低い (e.g. C=1 )のとき[label] へ飛ぶ。(Branch if lowerの略) |
br [label] |
無条件に[label] へ飛ぶ。 |
sob rN, [label] |
rN をデクリメントし、その結果が非0であれば、[label] へ飛ぶ。このとき[label] は2b のような戻る動作とする。 |
ジャンプ
ニーモニック | 意味 |
jmp [dest] |
[dest] に無条件で飛ぶ。 |
jsr [src], [dest] |
任意のレジスタ ([src] ) をスタックのトップにおいて、サブルーチン ([dest] )に飛ぶ。このとき [src] にサブルーチンから戻ったときの次の命令のアドレスが格納される。通常、 pc を渡すことが多く、rts pc で元の位置に戻る。(例 /sys/conf/m40.s#L492: jsr pc,copsu ) |
その他
ニーモニック | 意味 |
adc [dest] |
Cビットの中身を[dest] に加算する。 |
add [src], [dest] |
[src] を[dest] に加算する。 |
ash $n, [src] |
[src] の値を$n だけ左シフトする。(例: ash $2, r2 … r2の値を2bit左シフトする) |
ashc $n, [src] |
[src][src+1] を$n だけ左シフトする。(連鎖算術シフト) |
asl [dest] |
[dest] を1つ左シフトする。 |
asr [dest] |
[dest] を1つ右シフトする。 |
bic [src], [dest] |
[src] の1が立っている桁に対応した[dest] の桁を0にクリアする。 |
bis [src], [dest] |
[src] と[dest] をXOR演算し、結果を[dest] に格納する。 |
clc |
Cフラグをクリアする。 |
clr [dest] |
[dest] を0にする。 |
dec [dest] |
[dect] をデクリメントする。 |
div [src], rN |
rN とr(N+1) (このときN は偶数) に格納された32bitの2の補数表現された整数を[src] で除算する。rN に商を、r(N+1) に余りを格納する。 |
inc [dest] |
[dect] をインクリメントする。 |
mfpi [dest] |
“以前の” アドレス空間で指定されたワードを現在のスタックにプッシュする。 (Move From Previous Instruction space の略) (「”以前の” アドレス空間で指定されたワード」がよくわからない。以前のモードの仮想アドレス空間を指している?) (このあたりは “割り込み” の解説で解決するかも) |
mtpi [dest] |
“以前の” アドレス空間で指定されたワードに、現在のスタックからポップした値を格納する。 (Move To Previous Instruction space の略) |
mov [src], [dest] |
[src] の値を[dest] に格納する。 |
mul [src], rN |
[src] とrN の積を取る。N が偶数ならば結果はrN とr(N+1) に格納される。 |
reset |
UnibusのINIT行を10ミリ秒に設定する。全てのデバイスコントローラを再初期化するということ。 |
ror [dest] |
[dest] の全てのビットを1つ右回転させる。ここで言う”回転”とは、最上位ビットと最下位ビットが連結して、数珠状になっているとみて、ビットをシフトすることを指す。 ただし、0ビット目はCフラグに、以前のCフラグの値が15ビット目に入る。 |
rts [dest] |
サブルーチンから戻る。rN からpcを読み込み、スタックからrN を読み込む。 |
rtt |
割り込み または トラップから戻る。pcとpsをスタックから読み込む。 |
sbc [dest] |
[dest] からキャリービットを引く。 |
sub [src], [dest] |
[dest] から[src] を引く。 |
swab [dest] |
[dest] の上位ビットと下位ビットをスワップ する。 |
wait |
ハードウェア割り込みが発生するまで、プロセッサをアイドル状態にし、Unibusを解放する。 |
例: fork.s
以上を踏まえて試しに、PDP-11/40のアセンブリを読んでみます。
.globl _fork, cerror, _par_uid / _forkの公開と、他のグローバル変数、関数の使用を宣言
_fork:
mov r5,-(sp) / r5の値をスタックに積む
mov sp,r5 / スタックポインタをr5に格納
sys fork / カーネルのfork()をコール
br 1f / 前方の1:に飛ぶ
bec 2f / もしC=0ならば前方の2:に飛ぶ
jmp cerror / 飛べなかった場合は、cerror (/source/s4/cerror.s)に飛ぶ
1:
mov r0,_par_uid / r0の値を _par_uidに格納
clr r0 / r0 をクリア
2:
mov (sp)+,r5 / スタックをPOPし、その値をr5に格納
rts pc / プログラムカウンタを返す
.bss
_par_uid: .=.+2
7,8行目に2種類の条件分岐が存在し、「br
あるならbec
に到達しなくない?」と一瞬思ったのですが、fork()
内で親プロセスはpcを1命令分進めているので、親プロセスはbec
に到達するということですね (書籍にも書いてありました)。
また、sp
と(sp)
、(sp)-
と+(sp)
の違いをしっかり理解していることが大事ですね。
ちなみに書籍にも書いてある通り、fork()
で何かしらエラーがあればCフラグに1が立つようになっているので、bec
を使用した分岐になっています。