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回で動いていなかった、日時表示のスレッドがまともに動いてる。 どういうこっちゃ? スレッドは難しいなあ。大学の授業でも取り上げられているよ。

4-1 マルチスレッド

4-2 Non-blocking I/O

4-3 割り込み処理

4-4 同期

後でじっくり読む事にして、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か?) で、これって何処から呼ばれているんだろう? 探してみたけど、特定できんかった。 追いかけてみたいな。