(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

今回は,いよいよブートローダーでプログラムをロードして,実行してみよう.最終的にやりたいのは,ブートローダーでの起動後にOSをロードして実行することでOSに処理を渡すことなのだけど,とりあえずはブートローダーで起動して,簡単なコマンド受け付けのサンプルプログラムをロードして実行してみる.

まずロードするプログラムのオブジェクトフォーマットなのだけど,いろいろ考えたのだけどELF形式で行くことにした.というのは,まあぼくがELFの記事を書いたことがあって馴染み深くてよく知っているのと,ELFでできないことはそうそう無いので融通が効くこと,フォーマット解析がそんなに難しくないこと(ローダの実装がラクそう),標準的なフォーマットとして普通に使われていること,などが理由だ.ELFフォーマットについて詳細は,この記事の第2回をぜひ読んでほしい.かなり詳細に書いてある.(追記:この記事を書いている現段階では,モトローラSフォーマットを知った今となってはそっちにすればよかったかなと思います.展開もラクそうだし,あとロード先に直接展開ができるのでRAMを最大限まで使えるし.まあ対応は簡単そうなので,そのうち対応しよう)

で,ELFの実行形式をいったんバッファにロードしてから実行コマンドでロード(メモリ上の動作すべきアドレスに展開)し,実行を渡すようにブートローダーを書いてみたのだけど,いまいちうまく動かない.

まあこのへんでいろいろハマってしまいちょっと時間をくってしまった.最初のうちはロードしたプログラムの挙動がなんかおかしくて,スタックポインタの設定とかシリアルの設定とかXMODEMのチェックサム計算とかをいろいろ疑っていたのだけど,じつはそもそもシリアルの受信関数(serial_getc())の内部で改行コードの変換をしているので,バイナリデータが壊れてしまうという問題だった.あー間抜け.(たとえテキストデータだとしても,XMODEMのブロックナンバとかチェックサム部分が壊れてしまい,エラーになってしまう)

ということで実装したのが以下のような感じ.

上のリンクでは,ブートローダーと,ロードして実行させるサンプルプログラム(テキスト入力を受け付けて,それを表示するだけのもの)を配布している.kzload というフォルダがブートローダーで,osというフォルダがサンプルプログラムだ.(将来的にOSをロードするようにするつもりなので,osというフォルダ名にしている)

なので手順としては,以下のようになる.
  1. ブートローダーとサンプルプログラムを別々にビルドする.
  2. ブートローダーを h8write でフラッシュに焼き込む.
  3. ブートローダーを起動する.
  4. loadコマンドでXMODEMでのファイル受信に入り,サンプルプログラムのELFファイルをシリアル転送する.
  5. 転送完了したらrunコマンド(これは今回新設)でサンプルプログラムを実行する,
  6. サンプルプログラムが起動する.
なお上のプログラムでは,実はuuencode形式でのファイル転送もサポートしている.というのは,うまく転送できないのはバイナリファイルで転送しているからでは?と考えて,uuencode形式でファイル転送するようにして試したから,その名残り.まあ実は serial_getc() の改行コード変換が問題だったのでバイナリファイルを転送することに問題は無く,その後 uuencode 形式は使用しないように修正したが,もしも uuencode 形式を利用したいならば以下のようにすればよい.
  • xmodem.c の USE_UUENCODE という定義を有効にしてブートローダーをビルド.
  • サンプルプログラムのビルド時に ./make.sh image を行うことで hello.uu という uuencode 形式のファイルが生成されるので,それを転送する.
デフォルトでは USE_UUENCODE は無効になっているので,ELF形式をそのままシリアル転送すればよいのだけど,なんかうまくシリアル転送できない場合にはuuencode 形式での転送を試してみるといいかもしれない.

ちなみに今回この uuencode 形式にすることを試したときに,モトローラSフォーマットでもいいのでは? と思ってモトローラSフォーマットについて調べてみたのだけど,実は以下のような利点がある.(uuencode形式を選択したのは,単に実装がラクそうだったから)
  1. テキスト形式なので,回線が7bitだったらどうとかフロー制御のコードがあったらどうとか余計なことを考えなくていい.
  2. データを受信しながらそのままメモリ上に展開する,という処理に向いている.
とくに,今回ブートローダーを作っていて,「データをバッファ上に一度読み込んでからロードアドレスに展開するのではなく,データを読み込みながら展開先を調べて直接ロードできないか?」という疑問があった.というのは,いったんどこかのバッファに置いてからロード先にコピーするような動作だと,処理自体は楽なのだけど,まあワーク領域として倍のメモリを使うことになるので,今回のマイコンボードのようにRAMが少ない場合には,問題となるからだ.(結局,今はいったんバッファに置く実装になっているので,これは将来課題ではある)

で,上のような問題意識があったうえでフォーマットを調べていたからこそ今回気がついたことなのだが,モトローラSフォーマットというのは,データを読み込みながらロード先のメモリ上に直接展開するのに非常に向いている.というのは,ロード先のアドレスが先頭にあるので,まずアドレスを読んで,後続のデータをそのアドレスの指す先に配置していけばいいからだ.うーん,きっとこういうことを考えた上で作られたフォーマットなんだろうなあ.

ついでにいうと,これもそーいう問題意識で考えていたからこそ気がついたことなのだけど,ELF形式というのも,まあモトローラSフォーマットほどやりやすくは無いが,プログラムヘッダが先頭付近にあるため,読み込みながら直接展開,ということは原理的にはできる(プログラムヘッダがファイルの終端にあったりすると,最後まで読まないとロード先がわからないので,直接展開は原理的に不可能).

この記事からもわかるのだけど,ELF形式は先頭付近にプログラムヘッダ,末尾にセクションヘッダが配置されており,プログラムのロードはプログラムヘッダを参照して行われるが,リンク作業はセクションヘッダを参照して行われる.なぜこのような配置になっているのか今までとっても疑問だったのだけど,おそらくプログラムヘッダが先頭付近にあるのは上記のようにプログラムを読み込みしながら展開先に直接展開するためだ.あとセクションヘッダが終端にあるのは,これもおそらくだけど,プログラムのロード&実行にはセクションヘッダは必要無いので,サイズ節約のために実行形式から取り除きたい場合がある.この場合,ファイルの先頭付近にあると,そこを削除すると後続のデータのオフセットが変わってしまうため,様々なオフセット計算をやり直さなければならなくなってこれはそうとう面倒臭い.しかし終端にあれば,単にそれを取り除くだけでいいからだ,と思う.

で,難しい話はあとに回して,とりあえず実行してみよう.今までのおさらいも兼ねて,ビルドからひととおり説明する.

まず,ブートローダーとサンプルプログラムをビルドする.ブートローダーは今まで通り,以下でビルドできる.

% ./make.sh clean ; ./make.sh ; ./make.sh image

これで kzload.mot というファイルが生成される.ここまでは(ファイル名が kzload* に変わっているけど)前回通り.

次に,サンプルプログラムをビルドする.

% ./make.sh clean ; ./make.sh

上に書いたように,ファイル転送に uuencode 形式を使うなら,以下も実行しておく.

% ./make.sh image

ここまでで準備は完了.

次に,h8write を使ってブートローダーをフラッシュROMに転送する.実際には Makefile に書いてあるので,make write するだけでいい.

teapot# make write
../../h8write/h8write -3069 -f20 kzload.mot
H8/3069F is ready! 2002/5/20 Yukio Mituiwa.
writing
WARNING:This Line dosen't start with"S".
Address Size seems wrong
WARNING:This Line dosen't start with"S".
Address Size seems wrong
.......................................
EEPROM Writing is successed.
teapot#

これで完了.

cuでシリアル接続し,ディップスイッチを起動用に切替えてリセットボタンを押してブートローダーを起動する.あとでサンプルプログラムを転送するときにファイル指定しやすくするために,cuはサンプルプログラムをビルドしたディレクトリで起動するとよい.

teapot# cu -l /dev/cuad0
Connected
Hello World!
>

ブートローダーが起動して,コマンド入力待ちになる.コマンド待ちするので,ブートローダーっつうよりモニタっつったほうが適切かもしんない.

loadコマンドを実行して,XMODEMによるファイル転送待ちに入る.

teapot# cu -l /dev/cuad0
Connected
Hello World!
> load
(待ち状態)

前回説明したように,「~」「C」でコマンド指定に入り,XMODEM送信アプリとしてlsxを指定する.引数にはサンプルプログラムのELF形式である「hello」ファイルを指定する.(uuencode形式を利用する場合は,ここで「hello.uu」を指定する)

teapot# cu -l /dev/cuad0
Connected
Hello World!
> load
~CLocal command? lsx hello

Enterを押すと転送が始まる.

> load
~CLocal command? lsx hello
Sending hello, 15 blocks: Give your local XMODEM receive command now.
Bytes Sent: 2048 BPS:586

Transfer complete
eceive succeeded.
>



うまく転送できた.実はこの「うまく転送できるまで」でハマッていろいろな試行錯誤があったのだけど,まあうまくいったのでいいや.

今回,ブートローダーにrunコマンドというのを新規追加してある.runコマンドを実行すると,loadによってバッファ上に読み込んだファイルをELF形式とみなして解析し,ロード先のアドレスを調べてメモリ上に展開し,エントリポイントに処理を渡す(要するに,ロードして起動する).

runコマンドで実行してみよう.

> run
starting from entry point.boot succeed!
os>

おー,起動した.かなり感動.ここまで来るのにどれだけ苦労したことか...(いやH8への移植だけに関して言えばここ数日のことなのだけど,OS作りたいなあとむかし漠然と思っていたときから考えると,ホント,しみじみ思ってしまう)

てきとうにコマンドを打ってみる.

> run
starting from entry point.boot succeed!
os> run
run
os> test
test
os> dump
dump
os> load
load
os> aaa
aaa
os>

入力した文字列をそのまま返している.ひとまずちゃんと動いているようだ.runやloadを実行しても,ブートローダーでの動作が行われずにただ文字列をそのまま返しているので,サンプルプログラムに完全に処理が移っていることがわかる.

うーんちょっと感動.結局,ブートローダーが作れてしまった.まあものすごく簡単なものではあるが,きちんと動作して,他のプログラムをブートできている.

OS作りたいなあと漠然と思ってからいろいろ勉強したりしてきたけれど,実際作ろうとなったときにこーいうのがパパッと作れるというのは,やっぱしいままで勉強してきた甲斐があったなあというものだ.しみじみ.やっぱりなんでも基礎力が大事だね!

ちなみに今回のソースコードだけど,ブートローダーには前回に対して以下のファイルが新規追加されている.
  • ELF形式のファイルの解析と展開 (elf.c)
  • uudecode の復号 (uudecode.c)
ここで注目したいのはこれらのファイルサイズなのだけど,elf.cは92行,uudecode.cは48行だ.ちなみにXMODEMの処理である xmodem.c は96行.ELFとかXMODEMとかUUENCODE実装とかってなんか難しそうなイメージあるけど,特定の処理に限定すればこんなもんで済むもんです.これくらいならちょっと読めば誰でも十分に理解できるし,じゃあモトローラSフォーマットに対応してみようとかBinHexにも対応しようとか思ったときに,なんか簡単にできそうな気がするよね.

いま調べたら,ブートローダーぜんぶ含めても634行だった.まあエラー処理とかがかなりザルではあるが,ファイルを読んで展開するだけなら,こんなんで書けるということだ.ブートローダーってなんか難しいイメージあるけど,600行ちょいなら自分でも十分に読めるかな?作れるかな?って感じがするでしょ?OS作るとかブートローダー自作とかってなんか難しいイメージあるけど,あんま難しいこと考えずに作れば,これくらいでできちゃうもんなのよ.

あと今回思ったことだけど,ELF形式についてなのだけど,実際に自分で作ってみないと気が付かない,逆に言えば自分で作ってみれば気がつくこと,というのはあるもんだなあ,と.ELF形式のプログラムヘッダがなぜ先頭付近にあるのか?セクションヘッダがなぜ終端にあるのか?は,今回実際にいろいろ作ってみて(試行錯誤してみて)はじめて気がついたことだ.

よく,通り一辺倒の知識を得ただけで「この分野に関しては制覇したぜ!」みたいなこという人がいるよね.「C言語は完璧だぜ!」とか言っちゃう感じ.でもそーいうのって全然不十分で,そもそも本に載っている知識を得ただけで満足するだけじゃだめで,なぜそのような仕様になっているのか?なぜそーいう実装になっているのか?という製作者の考えとか気配りとか苦肉の策とかに気がつくようにならないと,その分野をマスターしたとはいえないなあ,と思う.こうこういう経緯で,歴史の流れでそうなってしまった,とかね.(まあ別に自分がELF形式をマスターしたぜ!という意味ではなく,自分に対する戒めです).

C言語なら,文法事項をマスターしただけでなく,なぜそのような言語仕様になっているのか?とかね.こーいうのは習って知るだけではなく,作られた経緯とか歴史とか実際の使われかたとかを勉強していく過程で,自分で自然と気がつけるようになりたいものだ.1を知ることで10を知る,って感じでね.

今日はとりあえず動いたので満足.サンプルプログラムの構成とか詳しい解説は次回にしよう.