(H8移植編第5回)ブートローダーの解説をしよう

2009/09/06

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

前回作成したブートローダーだけど, 前回はちょろっと動作させただけでろくに解説していなかったので, ちょっと解説しよう.

まずブートローダーについてなのだけど,リンカスクリプトに以下の行が 追加されている.

        buffer(rwx)     : o = 0xffdf20, l = 0x002000 /*   8kb */
さらにデータ領域とBSS領域は,上記 buffer 領域に割り当てられるように変更 してある.

これはどーいうことかというと,まあ本当はシリアルからプログラムをダウンロード しながらロード先のメモリ上に直接展開できればいいのだけど,面倒なので, いったんRAM上のバッファにぜんぶダウンロードして,で,ELFヘッダを解析して ロード先にコピーするという構成にしてある.

で,プログラムをロードする先はRAMの先頭付近にしておきたいので, RAMの終端付近にデータ領域とBSSを置いてブートローダーはそこで動作する, というメモリ配置にしてある.バッファはブートローダー内で

#define BUFSIZE 4096
static unsigned char loadbuf[BUFSIZE];
のようにして文字配列として定義してあるので,BSSに配置される. これが上記の buffer 領域に置かれることになる. loadbufのサイズが4KBなので,buffer 領域は余裕をみて8KBとして定義している.

いったんバッファ上に置いておいて,さらにロード先に展開することを考えると, loadbuf[]の最大サイズは,RAMの16KBの半分の8KBとなる.(loadbuf[]を12KBにすれば 12KBまでのプログラムをダウンロードできるが,残りRAMは4KBしかないので展開先が 無くて意味がない.よってRAMサイズの半分が,バッファサイズの最大となる.

このためこのブートローダーは現状,8KBまでのプログラムしかロードできない. あとロードされるプログラムは,RAMの先頭付近を使うようにリンカスクリプトで メモリ配置する必要がある.

あと serial.c には,バイナリデータの送受信用に serial_getb(), serial_putb() を 新規作成した.これらは改行コードの変換を行わずに生データを送受信する. 前回はここで従来の serial_getc(), serial_putc() を使っていたために 改行コードが変換されてしまってチェックサムとかが壊れてしまう,というのに ハマッたわけだ.

あと uuencode 形式のデコードのために,uudecode.c を追加してある. xmodem.c からは USE_UUENCODE の定義の有無で,uudecode を呼び出すか, 生データとして扱うかが切り替わるようになっている.シリアルから データをダウンロードしながらデコードを行えるように,uudecode.c は ストリームデータを処理できるような書き方をしてある. (同様に書けば,ELF形式をダウンロードしながらヘッダ解析してロード先に 直接展開,ということもできると思う)

あとELF形式の解析のために elf.c が追加されている.ELF形式はそれなりに複雑な フォーマットだけど,ここでやってるのはELFヘッダのチェックをしてから プログラムヘッダを見て展開することだけなので,超簡単に書けている. ELF形式について詳しくは この記事の第2回を参照してほしい.

main.c は,putval(), putxval() に実は負の値を渡すと無限ループに陥ってしまう という問題があったので,引数をunsignedとして受け取るように修正. あと run コマンドを追加して,ダウンロードしたプログラムをロード先に展開して 実行するように機能追加してある.

次に,ダウンロードされるプログラムのほうについて. まあ現状では入力された単語をエコーするだけの機能しかなくて, 実はブートローダーを流用して書いてある.

シリアルまわりとかスタートアップはブートローダーから持ってきたそのまま. スタートアップ(startup.s)ではスタックポインタを設定しているので, サンプルプログラムを起動するとスタックは設定されなおすことになる (ブートローダーが使用していたスタックを引続き利用するわけではない). ライブラリ(lib.c)もそのまま.異なるのはリンカスクリプトとmain.cなのだけど, main.cは使わない関数に蓋をして,単語をエコーするだけに書き換えただけなので たいした変更は無い.あ,あとデータ領域の展開とBSSのクリアはブートローダー側で やっているので,main.c の init() からは削除してある.

大きな変更があるのはリンカスクリプトだ.以下,ブートローダーと ロードされるサンプルプログラムでの,リンカスクリプトの変更点.

--- kzload/ld.scr	Fri Sep  4 00:16:17 2009
+++ os/ld.scr	Fri Sep  4 00:16:35 2009
@@ -4,44 +4,38 @@
 
 MEMORY
 {
-	vectors(r)	: o = 0x000000, l = 0x000100 /* top of ROM */
-	rom(rx)		: o = 0x000100, l = 0x07ff00 /* 512kb */
-	ram(rwx)	: o = 0xffbf20, l = 0x004000 /*  16kb */
-	buffer(rwx)	: o = 0xffdf20, l = 0x002000 /*   8kb */
+	/* reserve 256bytes space for ELF header */
+	ram(rwx)	: o = 0xffbf20 + 0x100, l = 0x004000 - 0x100 /* 16kb */
 	stack(rw)	: o = 0xffff00, l = 0x000010 /* end of RAM */
 }
 
 SECTIONS
 {
-	.vectors : {
-		vector.o(.data)
-	} > vectors
-
 	.text :	{
 		_text_start = . ;
 		*(.text)
 		_etext = . ;
-	} > rom
+	} > ram
 
 	.rodata : {
 		_rodata_start = . ;
 		*(.strings)
 		*(.rodata)
 		_erodata = . ;
-	} > rom
+	} > ram
 
-	.data : AT(_erodata) {
+	.data : {
 		_data_start = . ;
 		*(.data)
 		_edata = . ;
-	} > buffer
+	} > ram
 
 	.bss : {
 		_bss_start = . ;
 		*(.bss)
 		*(COMMON)
 		_ebss = . ;
-	} > buffer
+	} > ram
 
 	_end = . ;
 
まずロード時ていうかリンク時の注意なのだけど,ELF形式はリンカによって作成される 際に,先頭にELFヘッダが付加される.で,このELFヘッダがテキスト領域の直前に 配置&ロードされるようにプログラムヘッダが作成される.

これはロードされたプログラム側で,自身のELFヘッダを参照できるようにするためだと 思われる(あくまで想像だけど,もしかしたら共有ライブラリがらみで参照が必要 なのかもしれない)のだが,RAMの先頭(0xffbf20)から利用するようにふつうに書くと, RAM領域の先頭として指定された 0xffbf20 というアドレスよりも少し前にELFヘッダが はみ出して配置されてしまう.いろいろやってみたのだけどこれをうまく削ったり 調整したりする方法が見つからなくて,しかたがないので対策として,RAM領域の 先頭256バイトをELFヘッダ用に空けるようにしてある. (今思ったのだけど,ブートローダがプログラムヘッダを見て展開するときに, RAM領域内でないデータは展開しないようにすればいいだけだね.まあでもいいや)

ということで,サンプルプログラムのほうではRAMは先頭 0xffbf20 付近から利用する. RAMの終端のほうはブートローダーが利用しているので,ブートローダーからロードした プログラムに実行が移るまでは,上書きすることはできない.つまり,RAM終端付近に なにかを配置するようにリンカスクリプトを書いてはいけない.

割り込みベクタはいまのところとくに上書きしていないので,リンカスクリプトから 削除.あとテキスト,リードオンリーデータ,データ,BSSをすべてRAM上に配置する ように変更してある.

まあ解説はこんなとこかな.ざざっと説明しただけだけど.


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