OS自作入門 -Advent15-
Step4 シリアル経由でファイルを転送する 延長戦
意外と長くてstep4は4部構成になってしまった。今回はアセンブラについての詳細説明とまとめになりそうだ。
コンパイルしてアセンブラを見てみる
サンブルとしてlib.cにテスト用の関数#func
を定義してそれをmain.c追加した状態でコンパイルしてみる。
make
make image
../../tools/bin/h8300-elf-objdump -d kzload.elf > objdump-result.txt
この結果をobjdump-result.txtに置いておく。
解説
main部分
L614に_main
あ関数があり、L367に先ほど定義したテスト用の#func
関数がある。L624-L640を抜粋すると
624: 79 01 00 02 mov.w #0x2,r1 <= 引数を設定
628: 79 00 00 01 mov.w #0x1,r0 <= 引数を設定
62c: 5e 00 03 92 jsr @0x392:24 <= funcの呼び出し
という風に読める。jsr @0x392:24
に着目すると0x392とありfuncのアドレスが
00000392 <_func>:
なっているので多分呼び出しているんだろう。という感じ。またjsr
の前にある#0x2,r1
,#0x1,r0
はそれぞれ引数で、0x2
は数値の2、r1
はレジスタだと考えられる。なのでr0``r1
に値を代入した状態でjsr
命令を実行すれば良いということだ。
ニーモニックとオペコードの対応としては上記をみる限りmov => 79
である。このCPUだとオペランドは01 00 02
とかになっていて、最初の01
はレジスタ、それ以降の00 02
は0x0002
を表している。多少聞いたことがあるがここでビッグエンディアン、リトルエンディアンという話が出てくる。
ビッグエンディアンとリトルエンディアン
H8のようにバイト列をそのまま数値として取り扱うものをビッグエンディアン。
値を扱うときにバイト列をひっくり返してメモリ上に格納するCPUのことをリトルエンディアン。と呼ぶ。i386はリトルエンディアンらしい。
func部分
コードベースで解説を追加していこう。
392: 01 00 6d f6 mov.l er6,@-er7
-er7がER7のレジスタの値をデクリメントするという意味。
@がレジスタの値をアドレスとして利用するという意味。
なので@-er7でER7レジスタの値をデクリメントしてから
レジスタの値をアドレスとして利用するということになる。
@はポインタに近い概念かな。
デクリメント(減算)しているため、スタックを獲得する際には
アドレスの値が少なくなる方向にいくため下方伸長と呼ばれる。
er6は関数から戻る際にレジスタの値を戻す必要があるので、
ER6の内容をスタックに退避させている(movしている)
ER7はスタックポインタ。
396: 0f f6 mov.l er7,er6
スタックポインタの値をER6にコピー
ER6はフレームポインタと呼ばれる使い方をしている。
スタックフレームの先頭を指す。
398: 1b 97 subs #4,er7
ER7をさらに4byteだけ減算することで4byteの領域をスタック上に確保
39a: 09 10 add.w r1,r0
a + bの処理本体
39c: 6f e0 ff fe mov.w r0,@(0xfffe:16,er6)
加算結果はR0に格納されているが、自走変数cはスタック上に存在する。
フレームポインタを減算しcのアドレスを計算して結果を代入している。
@(0xfffe:16,er6)はER6の持つアドレスに対して定数値(0xfffe)を加算したアドレスにアクセス(@)します。
3a0: 6f 60 ff fe mov.w @(0xfffe:16,er6),r0
関数の戻り値はR0によって返されるので、スタック上に格納されている変数cの値を
R0に代入して値を返す準備をしている。
ここは実は無駄な処理で`volatile`を指定して最適化をさせていないためこうなる。
3a4: 0b 97 adds #4,er7
スタックポインタに4byteを加算する。
これで自動変数cのスタックを解放することになる。
3a6: 01 00 6d 76 mov.l @er7+,er6
スタック上に退避していたER6の値をER6に読み込むことでER6を元に戻す。
ロード後にスタックポインタであるER7は4byte加算される。
3aa: 54 70 rts
関数の呼び出し元に戻る。
jsrを呼ぶと現在実行中の次のアドレスがスタック上に自動保存され、
終端(ここ)でスタック上から戻り先アドレスを取得し戻っていく。
こんな流れになっている。 スタックの動きを軽く追ってみると
|@ER7|
^ jsrにより呼び出し元アドレスが格納される
↓
|@ER7|@-ER7|
^ このアドレスの先のメモリにER6のレジスタの値代入(現在のスタックフレームつまりmainフレーム)
↓ ER7のレジスタの値をER6にコピー(この関数の先頭のアドレスになる)
↓ 4byte確保
|@ER7|@-ER7|4byte|
↓ a+bの結果をR0に入れる
|@ER7|@-ER7|4byte|
^ ここにR0の値を入れる
↓
|@ER7|@-ER7|4byte|
^ ここからR0に読み込む(無駄部分)
↓ ER7に4byte加算(解放)
|@ER7|@-ER7|
^ ここの値(mainフレームの先頭のアドレス)をER6に読み込む
↓ @er7+の+の効果により4byte加算
|@ER7|
^ 元のアドレスに戻る(rts)
スタート・アップ
スタート・アップではスタックポインタの初期値の設定を行なっている。これをしないとc言語の関数呼び出しを正常に行うことができないらしい。start.sを抜粋すると
_start:
mov.l #_stack,sp
jsr @_main
ここでは_stack
と呼ばれるシンボルの値をsp
というレジスタに代入していることになる。spはスタックポインタでER7と同義。この_stack
はリンカ・スクリプト中に定義したスタック領域に配置されるため、スタックポインタはスタック領域が配置された位置に初期化されることになる。
1:
bra 1b
末尾にあるこの部分は、braが戻り先アドレスのスタックへの保存を行わないシンプルなジャンプ命令で、1bに飛べ。という命令になる。1bはこの表記が書かれたよりも前で1:という記述のうち近いものを指すようになっている。なのでこの場合は一行上の1:
になる。結果としては無限ループになっている。main()の呼び出しから戻ってきた時におかしなコードを実行して暴走しないように無限ループで止めているというもの。
今日の分はおしまい。結構時間かかったな。ここまで深いレイヤーは久しぶりだ。あとまともにこの辺りを学習するのは初めてだ。なかなか興味深い。コンパイラの最適化って結構色々やってそうではある。
- 前の記事
OS自作入門 -Advent14- 2017.12.14
- 次の記事
OS自作入門 -Advent16- 2017.12.16