はじめての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を使用した分岐になっています。
