i386ボード(もどき)
最近は暑いので散歩は、早朝か夜になってからしてる。夜散歩してたら目の前を照らす 懐中電灯の光の中で、何やらのたうち回る細長い物を発見した。
近寄って見ると、蛇の赤ちゃんでした。体長60cmぐらい、割り箸ぐらいの太さでした。旨く 歩けない?のか、さかんにくねくねしています。まだ歩行練習中なんですかね。お母さん蛇 が助けにくるかと思って暫く見てましたが、一向に現れません。そんな所でくねくねしてると 車に轢かれちゃうよ。尻尾を持って、持ち上げてみた。逆上がりして腕に巻きついてくる 体力もないみたいで、振り子のように揺れるだけ。かわいそうになって、そっと 草むらに置いてあげました。
他にも、みみずがくねくねしてたり、牛蛙がひっくり返ってぱたぱたしてたりするんで、 この時期の散歩には懐中電灯が必需品です。
早朝散歩でちょっと季節はずれの犬に遭遇しました。人間界ではとっくの昔に衣替えが 終わってますが、その犬は今が衣替えの真っ最中でした。ええ、冬毛が途中まで抜けて 夏毛になってる犬です。冬毛がしっかりと背中に残ってました。
余り外に出る事がないので、季節の感覚を失ってしまったのでしょうかね。普通は春から 夏になる時期が抜け替わりの季節と思うんですけど。。まあ、わんちゃんにも個性があるって 事でしょうかね。
asm --> runのサイクルを早く回そう
前回、アセンブル結果をemacsで処理して命令を取り出し、それを元にアプリケーションに 仕立てあげていた。こういう決まりきった事はコンピュータにやらせろと思うんだ。特にemacsって 所が癌だなあ。しょうがない、伝家の宝刀を抜くか。
[sakae@cdr ~/hand]$ as -al z.as GAS LISTING z.as page 1 1 ### Sample for FreeBSD system call 2 .text 3 .equ BASE, 0x8050000 ## offset1(elf load address) 4 0000 00000000 .org 0x54 ## offset2(for skip elf and prog header size) 4 00000000 4 00000000 4 00000000 4 00000000 5 .globl _start 6 _start: 7 0054 CC int3 ## call gdb 8 9 0055 680D0000 push $len ## write(fd, buf_adr, len) 9 00 10 005a 8D057A00 lea msg + BASE, %eax 10 0508 11 0060 50 push %eax 12 0061 6A01 push $1 13 0063 6A00 push $0 ## dummy for syscall 14 0065 B8040000 mov $4, %eax 14 00 15 006a CD80 int $0x80 16 006c 83C410 add $16, %esp 17 18 006f 6A00 push $0 ## exit(exit_code) 19 0071 6A00 push $0 ## dummy for syscall 20 0073 B8010000 mov $1, %eax 20 00 21 0078 CD80 int $0x80 22 msg: 23 007a 48656C6C .ascii "Hello world!\n" 23 6F20776F 23 726C6421 23 0A 24 .equ len, .-msg
asにリスティングswを付ければ、昔懐かしいやつが出てくる。けど、これじゃ正規表現で 処理するには不向きだ。(命令やデータ部分が次行送りになってるから!)やっぱり、objdump の方が正規表現向きの出力をしてくれるね。rubyで一行野郎して、Makefileにまとめちゃえ。
ちょっと話の本筋からそれるけど、上のアセンブラコードにちょっとした仕掛けが。。。 前回、プログラムのスタート番地を切りのよい番地にしておこうとして、細工したけどうまく いかなかった。どうも、ファイル自身が、0x8050000からロードされる(OSの)仕様っぽい。 するとオフセットの0x54が生じてしまうため、実アドレスに戻すのに苦労する。 そこで、アドレスの計算は、lea命令を使うとして、adr = msg + offset1 + offset2 と なるようにした。こうしておけば、リスティングのアドレス部に0x8050000を加えるだけで 実アドレスに変換出来る。また、文字列のサイズもasに計算させるようにした。
objdump -d a.out | ruby -n -e 'puts $1 if $_ =~ / ... /' > rom
とか、素敵な一行野郎を書いたんだけど、残念ながらMakefileの中ではエラーになって しまって使えない。(rubyとmakeが、$_ を取り合いしてしまい、その結果、$_ に、/usr/bin/make なんてのが入ってくる。) しょうがないので、やむなくrubyの部分はスクリプト化したよ。
#!/usr/local/bin/ruby # pick instraction's (or data) from objdump format # Usage: objdump -d a.out | ./pick.rb while ln = gets if ln =~ /:\t(([0-9a-f][0-9a-f] )+) +/ puts $1 end end
お次はMakefileね。コピペすると先頭のTABがSPACEに化けちゃうから修正してね。
## make application foo from z.as foo: z.as as z.as objdump -d a.out | ./pick.rb > rom cat rom | ./has > foo chmod 755 foo clean: rm -f foo a.out rom foo.core
objdumpのファイル名はデフォで、a.outなんで、省略出来るけど分かりやすさの為あえて 記述してます。また、中間ファイルのromも省いて、一本のパイプにも出来るけど、rubyの 正規表現フィルターがちゃんと動いているか検証出来るように、2本のパイプにしました。 こんな風に使います。(ここを読んでくれている人には、釈迦に説法ですね)
[sakae@cdr ~/hand]$ make as z.as objdump -d a.out | ./pick.rb > rom cat rom | ./has > foo chmod 755 foo [sakae@cdr ~/hand]$ ./foo Trace/BPT trap: 5 (コアダンプ)
嗚呼、gdbに落ちるように設定してたわい。ちと、修正。
[sakae@cdr ~/hand]$ vi z.as : "z.as" 24 行, 408 文字 書込み [sakae@cdr ~/hand]$ make as z.as objdump -d a.out | ./pick.rb > rom cat rom | ./has > foo chmod 755 foo [sakae@cdr ~/hand]$ ./foo Hello world! [sakae@cdr ~/hand]$ make `foo' is up to date. [sakae@cdr ~/hand]$ make clean rm -f foo a.out rom foo.core
これで、開発スピードも劇的て向上する事でしょう。よかった、よかった。でもちょっと、 Makefileが不恰好だなあ。何故って、ソースファイル等が決め打ちだもん。この際だから ちょっと修正しておくかな。
## make application TARGET from SRC TARGET = foo SRC = z.as $(TARGET): $(SRC) as $(SRC) objdump -d | ruby -n -e 'puts $$1 if /:\t(([0-9a-f][0-9a-f] )+) +/' >rom cat rom | ./has > $(TARGET) chmod 755 $@ clean: rm -f $(TARGET) a.out rom $(TARGET).core
結局いろいろ修正しちゃったわい。一行野郎も埋め込んじゃったし。。 何に何かと言うと。
まず、ruby内のif文で、$_ は省略出来るね。すっかり忘れてた。そんれから、put $1 だと makeの変数として解釈してしまって、NULL になってしまうんだ。よって解釈しないように エスケープしてあげた。また、chmodの所は、最初 $(TARGET) って指定してたんだけど、 これだと、やはりNULLになってしまうので、特殊なmake用の変数名を書いてあげた。makeの 文法って地雷満載だな。
おかげて、make -p なんて言う余計なのも覚えちゃった。そしてmakeの変数の中に面白い ものを発見。これって、GNUへのパロディーなんでしょうかね?
AR = ar .FreeBSD = true unix = We run FreeBSD, not UNIX.
i386ボード(もどき)
とまあ、i386なFreeBSDでもH8ボード並みな事が出来る事が判明した。これはひとえに ELFの力かも知れない。そんなELFに感謝を込めて KernelのObject構造 なんて言う、ダエモン仲間を見つけたので、ありがたく使わせていただく。 更にアセンブラーの資料として アンティーク・アセンブラ~Antique Assembler も、あげておこう。
また、先の開発環境では、romに書き込むのは2kまでだけど、実際はどのくらいまで書ける? これは、手っ取り早く、Solarisに有った pmapで確認出来る。きっとFreeBSDにも有る だろうと思って探したら、/usr/ports/sysutils/pmap が、それだった。
[sakae@cdr ~/hand]$ sudo pmap 4377 4377: /usr/home/sakae/hand/foo Address Kbytes RSS Shared Priv Mode Mapped File 08050000 4 4 - 4 rwx /usr/home/sakae/hand/foo BFBE0000 128 8 - 128 rwx [ anon ] -------- ------- ------- ------- ------- Total Kb 132 12 0 132
へぇー、4kまではOKなのか。スタックは128kね。両エリア共、読み書き実行が可能になってる。 正に、i386ボードってとこだな。それもZERO円だ。あっ、このパソコン女房が出資してて、 正式には通販発注端末です。おいらはそのパソコンのroot見習い中ですよ。
と、まあ、ひょんな事から無料ボードとその開発環境を手に入れた訳であるが、最後に一つ 注意をば(アホなおいらがはまったので)
Makefile中に埋め込んである一行野郎は、アドレス情報を一切見ていません。命令なりデータの たぐいは、最満充填でromに吐き出されます。ゆえに、アセンブラソース中で、命令とデータ エリアを分離しておこうなんて気になって、途中に .org を入れても無駄です。
KOZOSでgdbを読む
前回はKOZOSの第3回目の所で思わぬ伏兵(ctimeが落ちる)が現れて、足踏みしてしまった。 とても難題に思えるので、取り合えず先に進んでみる。
KOZOSでgdbを使えるようにしようと言う奮闘記なんだけど、gdbの姿がかいま見られて非常に 為になった。作者様は、emacs love な方で、emacs画面のスナップショットがページに 貼り付けてあったりして、おお御同輩と思わず叫んでしまった。よって、おいらも .emacsに 次のようなgdb対応を仕込んでみた。
(setq gdb-many-windows t) (setq gdb-use-separate-io-buffer t) (setq gud-tooltip-echo-area nil)
3行目はGUIで使うと効果がありそうだけど、まあ普段CUI端末のおいらでももしもの為の 保険って事です。起動すると、画面がいきなり6つに割れます。
左上段が、いわゆるgdbの画面。中段がソース画面。下段は、スタックフレーム。右上段が、 ローカル変数(自動更新されて便利)。中段は、ターゲットアプリの表示欄。下段は、 ブレークポイントの設定状況。これだけあれば、ほぼ十分だ。 なかなかやるな、emacsさんよ。
で、不思議な事に、第3回で動いていなかった、日時表示のスレッドがまともに動いてる。 どういうこっちゃ? スレッドは難しいなあ。大学の授業でも取り上げられているよ。
後でじっくり読む事にして、KOZOSのgdbI/Fの実装を読んでて、むらむらと悪戯心が。。。 gdb上から独自のブレークポインタの設定をやってみようと。。まあ、無駄と言えば無駄な んだが、ゆとりも必要って事で。題材は、i386ボードのそれ。
(gdb) x/10i $pc 0x8050055: push $0xd 0x805005a: lea 0x805007a,%eax 0x8050060: push %eax 0x8050061: push $0x1 0x8050063: push $0x0 0x8050065: mov $0x4,%eax 0x805006a: int $0x80 0x805006c: add $0x10,%esp 0x805006f: push $0x0 0x8050071: push $0x0 (gdb) set $save = {char}0x8050061 (gdb) p/x $save $1 = 0x6a (gdb) set {char}0x8050061 = 0xcc (gdb) c Continuing. Program received signal SIGTRAP, Trace/breakpoint trap. 0x08050062 in ?? () (gdb) set {char}0x8050061 = $save (gdb) set $pc = 0x8050061 (gdb) c Continuing. Hello world! Program exited normally.
0x8050061番地にBPを張って、contし、今止まった所から再びcontする模様です。BP命令は、0xcc なんだけど、それを書き込む前に、元あった命令をdebuggerの中に待避しておく。そしてSIGTRAPで 止まったら、元の命令に復帰して、PCも何喰わぬ顔をして元に戻しておく。これが、BPの一連の 動作になるんだな。1バイトの読み書き方法が分かっただけ得ってもんだ。
ローダーを追いかける
KOZOSの発表会の時、作者様は、ELFフォーマットを解析してそれをロードするCのプログラムが 、100行程度で書けるよとさりげなくおっしゃっていた。どんなコードか鑑賞してみたよ。 どうやら肝はこの部分(私に取って)っぽい。一部抜粋させて頂く。
/* セグメント単位でのロード */ static int elf_load_program(struct elf_header *header) { int i; struct elf_program_header *phdr; for (i = 0; i < header->program_header_num; i++) { /* プログラム・ヘッダを取得 */ phdr = (struct elf_program_header *) ((char *)header + header->program_header_offset + header->program_header_size * i); if (phdr->type != 1) /* ロード可能なセグメントか? */ continue; memcpy((char *)phdr->physical_addr, (char *)header + phdr->offset, phdr->file_size); memset((char *)phdr->physical_addr + phdr->file_size, 0, phdr->memory_size - phdr->file_size); } return 0; }
elfのヘッダーを受け取り、そこからプログラムセグメントを実アドレスへ転送する部分だ。 elfヘッダーを見れば、プログラムヘッダーが幾つあるか分かるので、その個数分、forで回す。 それぞれのphdrの場所を割り出し、そのプログラムセグメントをメモリーにロードするか否かを 決定。ロードするなら、実アドレスの何処へ、今のファイルの何処から、どれぐらいのサイズを 転送するかを割り出して転送してる。
作者さんも、H8移植4回目で述べておられるように、『実際に自分で作ってみないと気が付かない,逆に言えば自分で作ってみれば気がつくこと,というのはあるもんだなあ,と.』 には、激しく同意します。
そんじゃ、おいらも。。とは、簡単にいかないよなーー。i386ボードのローダーはどうなって いるん? ソースの森に分け入って(迷子になって)みるかな。
当たりを付けて、ローダーってカーネルの一部だろうな。
[sakae@cdr ~]$ cd /sys/kern [sakae@cdr /sys/kern]$ ls *elf* imgact_elf.c imgact_elf32.c imgact_elf64.c link_elf.c link_elf_obj.c [sakae@cdr /sys/kern]$ ls -l *elf* -rw-r--r-- 1 root wheel 39907 10 25 2009 imgact_elf.c -rw-r--r-- 1 root wheel 1535 10 25 2009 imgact_elf32.c -rw-r--r-- 1 root wheel 1535 10 25 2009 imgact_elf64.c -rw-r--r-- 1 root wheel 39257 10 25 2009 link_elf.c -rw-r--r-- 1 root wheel 35555 10 25 2009 link_elf_obj.c
リンクってリンク関係だろうから違うだろうな。そうすると imgact*ってのが怪しそうだけど 32とか64って多分経由してるだろうから、見るべきは、imgact_elf.c かな。
/* * Load the file "file" into memory. It may be either a shared object * or an executable. * * The "addr" reference parameter is in/out. On entry, it specifies * the address where a shared object should be loaded. If the file is * an executable, this value is ignored. On exit, "addr" specifies * where the file was actually loaded. * * The "entry" reference parameter is out only. On exit, it specifies * the entry point for the loaded file. */ static int __elfN(load_file)(struct proc *p, const char *file, u_long *addr, u_long *entry, size_t pagesize) : for (i = 0, numsegs = 0; i < hdr->e_phnum; i++) { if (phdr[i].p_type == PT_LOAD && phdr[i].p_memsz != 0) { /* Loadable segment */ prot = 0; if (phdr[i].p_flags & PF_X) prot |= VM_PROT_EXECUTE; if (phdr[i].p_flags & PF_W) prot |= VM_PROT_WRITE; if (phdr[i].p_flags & PF_R) prot |= VM_PROT_READ; if ((error = __elfN(load_section)(vmspace, imgp->object, phdr[i].p_offset, (caddr_t)(uintptr_t)phdr[i].p_vaddr + rbase, phdr[i].p_memsz, phdr[i].p_filesz, prot, pagesize)) != 0) goto fail; /* * Establish the base address if this is the * first segment. */ if (numsegs == 0) base_addr = trunc_page(phdr[i].p_vaddr + rbase); numsegs++; } } :
なんとなく、KOZOSのそれと似てるなあ(って、お前は、パターンマッチングに長けたPlologか?) で、これって何処から呼ばれているんだろう? 探してみたけど、特定できんかった。 追いかけてみたいな。