OS自作入門 -Advent14-

NO IMAGE

Step4 シリアル経由でファイルを転送する 後半

この記事でstep4は3つめの記事になる。おそらくあと一回で終わるだろう。新しい知識の上に新しい知識を重ねるので振り返ることが多くて理解に時間がかかるな。続きにいくとしよう。Step4を通して、ブートローダーとは何なのか、どのような手順でOSを起動するまで持っていくのか、それに必要なブートローダーの機能、シリアル経由でファイルを転送する仕組み、xmodemの実装を行った。

さて、これで準備は整った。シリアル経由でファイルを転送し、ブートローダーに対してファイルを渡してあげよう。

プログラム実行

今回はkz_xmodemを使って転送を行うことにした。ダウンロードして解凍してmake。
ちなみにソースコードはGithub kz_xmodem.cかな。

ファイル転送

いつも通り実行ファイルの作成を行い、転送、実行モードに切り替えてシリアル接続まで行う。

make
make image
# 書き込みモードに切り替えて
make write
# 読み込みモードに切り替えて
sudo cu -l /dev/tty.usbserial-XXXXXXXX -s 9600
kzload (kozos boot loader) started.
kzload>

こうなる。おぉ、これは自分で打った記憶がある。なるほど。さてここでloadというコマンドを打つと以下のように待ち状態になる。

kzload> aaa
unknown.
kzload> load
# これでファイル転送待ちになる

本来であればここで子プロセスを立ち上げてファイルを送信するというやり方が本には載っている。が、kz_modemの方は少しやり方が違うらしい。

  1. 一旦リセットボタン押してリセット
  2. 上記kzload>の接続は切る(~.と入力するとconnectionが切れる)
  3. ../../tools/bin/kz_xmodem defines.h /dev/tty.usbserial-XXXXXXXXと入力

こんな感じでcompleteの表示が出ればOK!この後にリセットボタンを押さずにcuで接続する。

sudo cu -l /dev/tty.usbserial-XXXXXXXX -s 9600
kz_load> dump
# size: 100
# 23 69 66 6e 64 65 66 20  5f 44 45 46 49 4e 45 53
# 5f 48 5f 49 4e 43 4c 55  44 45 44 5f 0a 23 64 65
# 66 69 6e 65 20 5f 44 45  46 49 4e 45 53 5f 48 5f
# 49 4e 43 4c 55 44 45 44  5f 0a 0a 23 64 65 66 69
# 6e 65 20 4e 55 4c 4c 20  28 28 76 6f 69 64 20 2a
# 29 30 29 0a 23 64 65 66  69 6e 65 20 53 45 52 49
# 41 4c 5f 44 45 46 41 55  4c 54 5f 44 45 56 49 43
# 45 20 31 0a 0a 74 79 70  65 64 65 66 20 75 6e 73
# 69 67 6e 65 64 20 63 68  61 72 20 20 75 69 6e 74
# 38 3b 0a 74 79 70 65 64  65 66 20 75 6e 73 69 67
# 6e 65 64 20 73 68 6f 72  74 20 75 69 6e 74 31 36
# 3b 0a 74 79 70 65 64 65  66 20 75 6e 73 69 67 6e
# 65 64 20 6c 6f 6e 67 20  20 75 69 6e 74 33 32 3b
# 0a 0a 23 65 6e 64 69 66  0a 1a 1a 1a 1a 1a 1a 1a
# 1a 1a 1a 1a 1a 1a 1a 1a  1a 1a 1a 1a 1a 1a 1a 1a
# 1a 1a 1a 1a 1a 1a 1a 1a  1a 1a 1a 1a 1a 1a 1a 1a

こんな感じになるだろう。(今はdefines.hファイルをloadしているので小さめ)

転送結果の確認

上記の内容とhexdump -C defines.hをローカルで出力した内容の比較を行う。

hexdump -C defines.h
# 00000000  23 69 66 6e 64 65 66 20  5f 44 45 46 49 4e 45 53  |#ifndef _DEFINES|
# 00000010  5f 48 5f 49 4e 43 4c 55  44 45 44 5f 0a 23 64 65  |_H_INCLUDED_.#de|
# 00000020  66 69 6e 65 20 5f 44 45  46 49 4e 45 53 5f 48 5f  |fine _DEFINES_H_|
# 00000030  49 4e 43 4c 55 44 45 44  5f 0a 0a 23 64 65 66 69  |INCLUDED_..#defi|
# 00000040  6e 65 20 4e 55 4c 4c 20  28 28 76 6f 69 64 20 2a  |ne NULL ((void *|
# 00000050  29 30 29 0a 23 64 65 66  69 6e 65 20 53 45 52 49  |)0).#define SERI|
# 00000060  41 4c 5f 44 45 46 41 55  4c 54 5f 44 45 56 49 43  |AL_DEFAULT_DEVIC|
# 00000070  45 20 31 0a 0a 74 79 70  65 64 65 66 20 75 6e 73  |E 1..typedef uns|
# 00000080  69 67 6e 65 64 20 63 68  61 72 20 20 75 69 6e 74  |igned char  uint|
# 00000090  38 3b 0a 74 79 70 65 64  65 66 20 75 6e 73 69 67  |8;.typedef unsig|
# 000000a0  6e 65 64 20 73 68 6f 72  74 20 75 69 6e 74 31 36  |ned short uint16|
# 000000b0  3b 0a 74 79 70 65 64 65  66 20 75 6e 73 69 67 6e  |;.typedef unsign|
# 000000c0  65 64 20 6c 6f 6e 67 20  20 75 69 6e 74 33 32 3b  |ed long  uint32;|
# 000000d0  0a 0a 23 65 6e 64 69 66  0a                       |..#endif.|
# 000000d9

真ん中の方が同じ構造している!転送ができてることが確認できた!

アセンブラ・プログラミング

この本の内容の多くはc言語で書かれているが一部アセンブラじゃないとどうしても書けない部分があるとのこと。それは「スタートアップ」「割込みの入口と出口」「スレッドのディスパッチ」の3箇所。読み方のコツは目的を意識することらしい。まぁ確かに自分の現時点の理解だとアセンブラってレジスタ周りの値をコピーしたり書き込んだり移動したりを連続で書いていくものっぽいので各行を理解したとしても全体として謎みたいなことにはなりそうである。

スタック

javaやってたときにスタック領域とかヒープ領域とか出て来たなぁ。という脱線はともかくここでは静的変数で全部定義するとメモリに常駐して無駄この上ないから関数内の一時的な変数とかはreturnで抜けた時に捨てられてメモリ領域は使い回されるようにしましょう。という話。ここでのやり方としては以下の通り。

  1. ある程度の容量の領域を確保
  2. 今現在どこの領域まで利用しているのかを示すポインタを用意する
  3. 自然変数(一時変数)が必要になった場合には、必要分だけポインタをずらす。
  4. ポインタをずらした分の領域は自由に利用可能
  5. さらに関数が呼び出されたらポインタを必要分だけずらす
  6. 関数から戻る時にはポインタを戻す。

という流れ。スタックを管理するために利用されるポインタをスタック・ポインタ、関数単位でスタック上に確保される領域をスタック・フレームと呼ぶ。これjavaとかrubyとかやっていると無限ループした時にstack overflowとか言うけどポインタそれ以上ズラせなくなっちゃった。みたいな状態なのかな。この辺りに載っているのが噛み砕いていて結構分かりやすかった。

アセンブラ

現時点のソースコードだとstartup.sが該当する。

CPUはメモリ上にある命令を順次実行していくが、メモリ上には数値しか保存できないため命令自体も0x01というような数値になっている。

実際の命令の種類はそんなに多くなく、「メモリからレジスタにデータを読み込む」「レジスタのデータをメモリに書き込む」「加算」などといったかなりローレベルな簡単なものの集まりである。ここでいうレジスタはstep2で出てきたコントローラの持つレジスタとは異なり、CPUの持つレジスタのことを指している。

最近のCPUは内部にレジスタを持っていて加減算の処理はこのレジスタに直結しているため、結局メモリ上にある値といえども一度レジスタにコピーして演算をする必要がある。なので例えば c = a + bの演算は、a => レジスタ1に読み込む、b => レジスタ2に読み込む、加算処理 => 結果をレジスタ3へ書き込む、レジスタ3の値を変数cのメモリに書き込む。という流れになっている。

このC言語で書かれている処理を上記4つの流れに置き換えるのがコンパイラの役割とのこと。

プログラム・カウンタ

上記のような演算の途中に使うようなレジスタは汎用レジスタと呼ばれる一方で、CPUが現在実行している命令のアドレスを指すものをプログラム・カウンタと呼ぶ。CPUはプログラム・カウンタで指す先のメモリの値を読み込んで命令として実行しているだけである。

ニーモニックとアセンブラ

遠い昔にどこかで聞いたことのある単語が出てきた。ニーモニック。。。アセンブラの命令部分をオペコード、引数部分をオペランドと呼ぶ。ここまでは知ってる。例えば0x01という数値はロード(メモリからレジストリに値を読み込む)を表し、引数としては1バイトのレジスタ番号、2バイトのアドレスを持つ。これを実行するとアドレスの指す一のメモリ上の値をレジスタにロードするという処理を実行することになる。

01 01 80 00

これは1番レジスタにアドレス0x8000の値をロードする。と読める

このどの数値がどのような命令として動作するのかという決まりを「命令セット」と呼ぶ。なるほど。いまいち命令セットってぼやっとした理解だったがそういうことか。当然H8とi386の命令セットは全然違う。

さて、ニーモニックとは0x01という数値だと人間にはわかりにくいので名前つけました。というものをニーモニックと呼ぶらしい。0x01はロードなのでld。とかね。なのでニーモニックを使うと上記の例は

ld r1, 0x0000

こんな感じになる。ただしこれはあくまでも人間用なのでこれを結局01 01 80 00という形にする必要があり、それをアセンブルすると呼び、その役割を担うものアセンブラと呼ぶ。アセンブラについてはCPUのマニュアルに記載があるとのこと。


今日はここまで。次回はmain.cにテストコードを埋めてアセンブラのコードを見ていくことになりそう。少しずつかけている知識が補完されていく感じがいいな。