(H8移植編第4回)ブートローダーでプログラムをロードしてみる

2009/09/03

あなたは 人目のお客様です.

今回は,いよいよブートローダーでプログラムをロードして,実行してみよう. 最終的にやりたいのは,ブートローダーでの起動後に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 形式を利用したいならば以下のようにすればよい. デフォルトでは 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.cは92行,uudecode.cは48行だ.ちなみにXMODEMの処理である xmodem.c は96行. ELFとかXMODEMとかUUENCODE実装とかってなんか難しそうなイメージあるけど, 特定の処理に限定すればこんなもんで済むもんです.これくらいならちょっと読めば 誰でも十分に理解できるし,じゃあモトローラSフォーマットに対応してみようとか BinHexにも対応しようとか思ったときに,なんか簡単にできそうな気がするよね.

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

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

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

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

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


メールは kozos(アットマーク)kozos.jp まで