araki tech

for developers including me

はじめてのOSコードリーディング個人メモ【PDP-11/40 アセンブリ編】

はじめてのOSコードリーディング個人メモ【アセンブリ編】

PDP-11/40におけるアセンブリ構文

PDP11のアセンブリは、その他のアセンブリと似ているところもあるが、初見だとなかなか読めず苦戦するのでまとめます。

なお、これからまとめる内容は、Lions本と呼ばれる有名なOSの教科書を参考にしています。

(リンク以外のオレンジの字は私の独り言です。読み進めたら解決できるかも、な部分。)

なお、この記事の対象者は主に下記の本を読んでいる人向けです。

 

おさらい

PSWと汎用レジスタについて

PDP11のニーモニック(命令の種類) について見ていく前に、PSWと汎用レジスタの構成についてまとめます。

これを理解していないと、ニーモニックの意味が理解できません。

はじめてのOSコードリーディング個人メモ【アセンブリ編】

算術演算 (ビット演算)

演算
& (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, BABのこと。1 + 212 もオペランドと言える。
アドレッシングモード レジスタへの参照の仕方。(r0)4(r1)などのような表記がそれ。

アドレッシングモード一覧

アドレッシングモードとは、レジスタを参照したときにそれをどう扱うかを決定するモードです。

rNは汎用レジスタで、実際にはr1r2のような表記になります。

基本

表記 説明
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がオペランドへのポインタのアドレスとする。

r6r7はそれぞれ、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 rNr(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が偶数ならば結果はrNr(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を使用した分岐になっています。