OS自作入門 -Advent15-

NO IMAGE

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 020x0002を表している。多少聞いたことがあるがここでビッグエンディアン、リトルエンディアンという話が出てくる。

ビッグエンディアンとリトルエンディアン
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()の呼び出しから戻ってきた時におかしなコードを実行して暴走しないように無限ループで止めているというもの。


今日の分はおしまい。結構時間かかったな。ここまで深いレイヤーは久しぶりだ。あとまともにこの辺りを学習するのは初めてだ。なかなか興味深い。コンパイラの最適化って結構色々やってそうではある。