FreeBSD で system call
『ご飯ですよーー』『はーい』と、いつもの夕食の一駒。夏仕様の炬燵テーブルの前に あぐらをかこうと思ったおいらは、テーブルに手を置いたのはいいんだけど、ちとタイミングが 狂って、左手の親指を先についちゃったんだ。ひょっとしてつき指したかな? でも、何とも なかったみたい。
翌朝、起きてからがまずかったねぇ。レフティーなおいらは歯を磨くのは左手なんだけど、 歯ブラシの枝を持てないのだ。親指を中心に手のひらが痛くてだめなんだ。ぎこちなく右手を 登場させたよ。食パンの袋はいつも、両手で端を持って開いているんだけど、左手が痛くて 駄目。牛乳パックも開けない。ぎこちなく右手に持った鋏で切り開いたよ。
それから、チャリはハンドルをかろうじて持てたけど、ブレーキ操作は出来なかった。 風呂場で洗面器を持てんかった。 他にも不便なことがいっぱい。これって、年寄りになって不自由になった時のシュミレーション をやっているみたいで、暗澹たる気持ちになったよ。あー、歳は取りたくないねぇと実感 した数日でした。
ハンドアセンブル
そろそろ出るか? OpenSolarisっつう事で界隈をうろうろしてるんだけど、何の気配も ないねぇ。もう1年経つよ。技術的な理由で、まだ出せないって言うのならいいけど、 政治的な理由ってんじゃ見切りを付けちゃうぞ。
そんな訳で、散歩中に思わぬものを発見しましただ。 やっぱりSunが好きに、Solaris でハンドアセンブル なんて いうとっても素敵が記事が出てた。 対象の石が例の電卓上がりの変化自在の可変長命令を扱うって所が、超ニッチな世界だ。 酔狂と言うか、、、(以下自粛)
昔初めて触れたコンピュータは16Bitのミニコンで命令数は僅か20個ぐらいしか無いから 命令も覚えちゃうよな。メモリーの何処に命令を書き込むかを、フロントパネルにあるSWで 指定。そして一度読み出し(こうすると、アドレスレジスターがセットされる)続いて 書き込みたい命令をハンドアセンブルしてSWにセット。そして書き込み。次の番地の 命令をセットしてから、次の番地に書き込みのボタンを押す。こうして、脳内アセンブラを フル活動してデータを入れる。走らせたい番地をSWにセットしてスタート。
運がよければ正しく実行されるし、ハンドアセンブルに失敗したりすると、暴走して 運が悪ければメモリー内容が破壊されるので、またいちからやり直し。よくやったなあ。勿論本物の アセンブラーを使って出力した紙テープを読ませる事も可能。その為には読み込みプログラム (ブートローダー)をこれまた手で入れる。13命令あるんだけど、やっているうちに手が 覚えてしまって、上の空で入れられるようになった。懐かしい思い出だ。
こちらでも読んで、ハンドアセンブル 出来るようになろうかな。x86アセンブリ言語入門。 それより手を動かせ、かな。ブログにあったやつをFreeBSDでやってみる。nasmはportsになってた。 逆アセンブラーも付いているんだ。豪華仕様だけど、書き方がintel風って所が混乱に拍車を かけるな。余り使わないようにしよう。脳内記憶領域が少ないからね。
で、掲載されてたexeにしちゃうのを動かすと。。
[sakae@cdr ~/hand]$ echo -n 6A00 B801000000 0F05 | ./has > foo [sakae@cdr ~/hand]$ chmod 755 foo [sakae@cdr ~/hand]$ ./foo ELF binary type "0" not known. -bash: ./foo: バイナリファイルを実行できません [sakae@cdr ~/hand]$ file foo foo: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, corrupted section header size
やっぱりそのままでは動かんわ。Solaris用(Linux用でもあるのかな)じゃね。SVRのエミュレーション 機能でもonにしてればひょっとして動くんだろうけど。きっと、カーネルの再コンパイルが 必要になるだろうね。もう少し楽な手を考える。
/* * Usage: * # gcc has.c -o has * # echo -n 6A00 B801000000 0F05 | ./has > foo * # chmod +x foo * # ./foo */ #include <stdio.h> #include <elf.h> #include <ctype.h> #define PROGRAM_SIZE 2048 int main() { int size = 0; unsigned char prog[PROGRAM_SIZE] = {0}; unsigned int ehdr_size = sizeof(Elf32_Ehdr); unsigned int phdr_size = sizeof(Elf32_Phdr); Elf32_Ehdr ehdr = { {ELFMAG0, ELFMAG1, ELFMAG2, ELFMAG3, ELFCLASS32, ELFDATA2LSB, EV_CURRENT, ELFOSABI_FREEBSD}, ET_EXEC, EM_386, EV_CURRENT, 0x8050000 + ehdr_size + phdr_size, // e_entry ehdr_size, // e_phoff 0, // e_shoff 0, // e_flags ehdr_size, // e_ehsize phdr_size, // e_phentsize 1, // e_phnum 0, // e_shentsize 0, // e_shnum SHN_UNDEF // e_shstrndx }; // 52bytes Elf32_Phdr phdr = { PT_LOAD, // p_type 0, // p_offset 0x8050000, // p_vaddr NULL, // p_paddr 0, // p_filesz 0, // p_memsz PF_R + PF_W + PF_X, // p_flags 0x10000 // p_align }; // 32bytes enum {HIGH, LOW}; // nibble int c, v, nibble = HIGH; while((c = fgetc(stdin)) != EOF) { if(size >= PROGRAM_SIZE) { perror("input too large."); break; } if (isxdigit(c)){ c = toupper(c); v = c > '9' ? c - 'A' + 10 : c - '0'; if(nibble == HIGH) { prog[size] = v << 4; nibble = LOW; } else { prog[size] += v; size++; nibble = HIGH; } } } phdr.p_filesz = phdr.p_memsz = size + ehdr_size + phdr_size; write(1, &ehdr, ehdr_size); write(1, &phdr, phdr_size); write(1, prog, size); exit(0); }
ちょっとFreeBSD用にELFヘッダーを調整してみた。 また、16進数への変換がごちゃごちゃしてたので、ちょっと整理した。テーブルを引く ようにした方が良かったかしらん。まあいいや。で、 FreeBSDの魂植え込みの参考にしたのは、/usr/include/sys/elf_common.h 。elf(5)の 方が良かったかも。 このファイルのマシンタイプを見てると面白いな。
#define EM_NDR1 57 /* Denso NDR1 microprocessor. */ #define EM_ME16 59 /* Toyota ME16 processor. */ #define EM_PJ 91 /* picoJava. */
自動車関連があったり、Javaを冠してる石が有ったり。MITのSchemeチップはさすがに 登録されてなかったわい。
[sakae@cdr ~/hand]$ file foo foo: ELF 32-bit LSB executable, Intel 80386, version 1 (FreeBSD), statically linked, corrupted section header size [sakae@cdr ~/hand]$ readelf -h foo ELF Header: Magic: 7f 45 4c 46 01 01 01 09 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - FreeBSD ABI Version: 0 Type: EXEC (Executable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x8050054 Start of program headers: 52 (bytes into file) Start of section headers: 0 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 1 Size of section headers: 0 (bytes) Number of section headers: 0 Section header string table index: 0 [sakae@cdr ~/hand]$ readelf -l foo Elf file type is EXEC (Executable file) Entry point 0x8050054 There are 1 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x000000 0x08050000 0x00000000 0x0005d 0x0005d RWE 0x10000 [sakae@cdr ~/hand]$ readelf -S foo There are no sections in this file.
fileコマンドを叩くだけで、ちゃんとセクションが無い事を教えてくれるんだ。
[sakae@cdr ~/hand]$ ./foo Illegal instruction: 4 (コアダンプ)
またcoreのお出ましだよ。
[sakae@cdr ~/hand]$ gdb -q foo foo.core (no debugging symbols found)...Core was generated by `foo'. Program terminated with signal 4, Illegal instruction. #0 0x0805005b in ?? () (gdb) x/5i 0x8050054 0x8050054: push $0x0 0x8050056: mov $0x1,%eax 0x805005b: syscall 0x805005d: add %al,(%eax) 0x805005f: add %al,(%eax) (gdb) x/20b 0x8050054 0x8050054: 0x6a 0x00 0xb8 0x01 0x00 0x00 0x00 0x0f 0x805005c: 0x05 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x8050064: 0x00 0x00 0x00 0x00
データとしてはちゃんと登録(ロード)されてるし、命令列もただexitのシステムコールを 呼び出している雰囲気。ここから先は、システムコールのFreeBSD流呼び出し方法を調べナイトな。
FreeBSDでsystemcall
FreeBSDでsystemcallなんてのを見つけた。int 0x80が 入り口なんだ。引数は右から順にスタックへ積んでから、戻り番地用のエリアもstack上に 確保。そして、eaxレジにシステムコール番号を入れるのか。そんな面倒な事はマニュアル 調べるの大変だな。asを下請けにしちゃえ。
.text .globl _start _start: push $6 ## write(1,"Hello\n",6) push $msg push $1 push $0 ## dummy for return mov $4, %eax int $0x80 push $0 ## exit(0) push $0 ## dummy for return mov $1, %eax int $0x80 msg: .ascii "Hello\n"
標準出力へ Hello するアセンブラです。こやつのソースをrom.c(あれ、アセンブラだから rom.sだろう)とすれば
as rom.c
すると、a.outが出来上がる。
[sakae@cdr ~/hand]$ objdump -d a.out a.out: file format elf32-i386-freebsd Disassembly of section .text: 00000000 <_start>: 0: 6a 06 push $0x6 2: 68 1d 00 00 00 push $0x1d 7: 6a 01 push $0x1 9: 6a 00 push $0x0 b: b8 04 00 00 00 mov $0x4,%eax 10: cd 80 int $0x80 12: 6a 00 push $0x0 14: 6a 00 push $0x0 16: b8 01 00 00 00 mov $0x1,%eax 1b: cd 80 int $0x80 0000001d <msg>: 1d: 48 dec %eax 1e: 65 gs 1f: 6c insb (%dx),%es:(%edi) 20: 6c insb (%dx),%es:(%edi) 21: 6f outsl %ds:(%esi),(%dx) 22: 0a .byte 0xa
こやつの命令部分だけ(msgの部分もね)を取り出してromとか適当なファイルに保存する。 で、命令だけを取り出すのは、正規表現ですかね? 命令サイズが可変長という糞な仕様 なので、そんな面倒な事は諦めた。正規表現 Love な、あの人ならきっと書いてくれるだろう。
こういう時は、emacsの矩形切り取りが使えるな。C-x r k で切り取ってから、C-x r y で貼り付けるんだ。なかなか良い機能だ事。嗚呼それから、objdumpもemancsの上で、shell-command を使うと、簡単に展開出来る。
で、上記のromは残念ながらそのままでは使えない。アセンブラ出力を見れば分かる通り 0番地から出力されている。問題になるのは、msgのアドレスだ。これを修正しておかないと 明後日の場所からデータを読み出してしまう。(asには、.loc とかのディレクティブが 無いのかなあ? 後で調べてみよう)
具体的には、_start が、0x8050054 から始まるので(既出のELFエントリーポイント参照) 、オフセットとして加えてやる。さあ、16進数の足し算ですよ。gdbにやらせちゃえ。
[sakae@cdr ~/hand]$ gdb -q (gdb) p/x 0x8050054 + 0x1d $1 = 0x8050071
こやつを、リトルエンディアンで表現してあげる。結果は、68 71 00 05 08 下位桁から パッチしてくって、人間にとっちゃ面倒だ。やっぱり糞な石だなあ。
[sakae@cdr ~/hand]$ cat rom | ./has >foo
上記コマンドで、romにELFヘッダー(と、プログラムヘッダー)を結合します。そして実行属性を 付けてあげて
[sakae@cdr ~/hand]$ ./foo Hello [sakae@cdr ~/hand]$ hd foo 00000000 7f 45 4c 46 01 01 01 09 00 00 00 00 00 00 00 00 |.ELF............| 00000010 02 00 03 00 01 00 00 00 54 00 05 08 34 00 00 00 |........T...4...| 00000020 00 00 00 00 00 00 00 00 34 00 20 00 01 00 00 00 |........4. .....| 00000030 00 00 00 00 01 00 00 00 00 00 00 00 00 00 05 08 |................| 00000040 00 00 00 00 77 00 00 00 77 00 00 00 07 00 00 00 |....w...w.......| 00000050 00 00 01 00 6a 06 68 71 00 05 08 6a 01 6a 00 b8 |....j.hq...j.j..| 00000060 04 00 00 00 cd 80 6a 00 6a 00 b8 01 00 00 00 cd |......j.j.......| 00000070 80 48 65 6c 6c 6f 0a |.Hello.| 00000077 [sakae@cdr ~/hand]$ ls -l foo -rwxr-xr-x 1 sakae kuma 119 Jun 16 11:19 foo*
やっと出来ましたねぇ。小さい小さいやつが。Binary Hack本でもチャレンジしてるけど(97頁) それより断然小さいよ。努力の甲斐がありました。ダイエットは一日にして成らず。
アセンブラのディレクティブ調べてみた。 .locなんてお里が知れちゃうなぁ。.orgが正解。 但し、.org 0x8050054 なんて大きな数値を指定しちゃうとa.outがとんでもない事(肥大化) しちゃうので、.org 0x0054 にしておくのが吉。どうせ上位桁の 0x805 は固定なんだから。
00000054 <_start>: 54: 6a 06 push $0x6 56: 68 71 00 00 00 push $0x71
ちゃんと下位2Bytes分は、71 00 と出てる。上位2Btytesは機械的に、05 08と 書き換えるだけで済んじゃう。これで、gdbに計算させなくてもよくなる。
call gdb
上で出てきた、push命令、同じ命令なのに命令サイズが違う。いやらしい! そのからくりは 如何にと思ってマニュアルを紐解いてみた。そしたら、即値の種類によって変化自在とか。 よく見ると、命令の接頭語も違う。命令の動作を説明するために擬似コードが載って いるんだけど、その複雑な事よ!
折角開いたマニュアルなのでパラパラ見てたら、int n なんてのが目に入ってきた。 ソフトウェア割り込みなんですね。FreeBSDでもシステムコールする時に使われてる。 この命令の特殊な物として、int3 ってのが有り、こやつはこの手の命令で唯一の1バイト命令とか。 debuggerのBreakを実現するのに最適でっせ なんて説明されてた。そっか、それなら きっとgdbも使ってるだろうな。これはもう試してみる鹿。
実はここに行き着く前に、先に作ったfooをgdbしてみようと実験してみたんだが、 gdbの動作に必須のセクションが無いため、途中で止める事が出来なかったんだ。
また、コードを書く事になるけど、上でやったようなパッチを宛てる事は避けたいので ちょっと知恵を付けてみた。そう、msgのアドレスをプログラムの中で計算させちゃえってね。 それと、システムコール後のスタック調整もちゃんと行ってあげる事にした。
.text .equ BASE, 0x8050054 ## Real start address .globl _start _start: int3 ## Call gdb (TRAP instraction) push $6 ## write(1,"Hello\n",6) mov $BASE, %eax addl $msg, %eax ## calc real msg: address push %eax push $1 push $0 ## dummy for return movl $4, %eax int $0x80 addl $16, %esp push $0 ## exit(0) push $0 ## dummy for return movl $1, %eax int $0x80 msg: .ascii "Hello\n"
実スタートアドレスは固定になってるので、そいつを .equ で宣言しといて、そいつを eaxに持ってくる。そしたら、それにオフセット分のmsgを加えてから、eaxをpush。ちょっと まどろっこしいな。こういう場合、LEAなんて命令は使えるのだろうか? 後で調べて おこう。そんじゃ、折角なんで、アセンブル結果を開陳。
[sakae@cdr ~/hand]$ objdump -d a.out : 00000000 <_start>: 0: cc int3 1: 6a 06 push $0x6 3: b8 54 00 05 08 mov $0x8050054,%eax 8: 05 27 00 00 00 add $0x27,%eax d: 50 push %eax e: 6a 01 push $0x1 10: 6a 00 push $0x0 12: b8 04 00 00 00 mov $0x4,%eax 17: cd 80 int $0x80 19: 83 c4 10 add $0x10,%esp 1c: 6a 00 push $0x0 1e: 6a 00 push $0x0 20: b8 01 00 00 00 mov $0x1,%eax 25: cd 80 int $0x80 00000027 <msg>: 27: 48 dec %eax :
そんじゃ、早速実行。
[sakae@cdr ~/hand]$ ./foo Trace/BPT trap: 5 (コアダンプ)
普通に実行すれば、コアダンプしちゃって、知らない人は、このアプリ壊れてるって 誤解するに違いない。ひひひ、隠れ蓑になるな。
[sakae@cdr ~/hand]$ gdb -q foo (no debugging symbols found)...(gdb) run Starting program: /usr/home/sakae/hand/foo warning: shared library handler failed to enable breakpoint Program received signal SIGTRAP, Trace/breakpoint trap. 0x08050055 in ?? ()
一番最初の命令は、まあ、gdbに制御を渡せっていう事だから、制御がgdbにきた。 後は、普通にデバックしてけば良い。
(gdb) disp/i $pc 1: x/i $pc 0x8050055: push $0x6 (gdb) ni 0x08050057 in ?? () 1: x/i $pc 0x8050057: mov $0x8050054,%eax (gdb) 0x0805005c in ?? () 1: x/i $pc 0x805005c: add $0x27,%eax (gdb) 0x08050061 in ?? () 1: x/i $pc 0x8050061: push %eax (gdb) p/x $eax $1 = 0x805007b (gdb) ni 0x08050062 in ?? () 1: x/i $pc 0x8050062: push $0x1 (gdb) 0x08050064 in ?? () 1: x/i $pc 0x8050064: push $0x0 (gdb) 0x08050066 in ?? () 1: x/i $pc 0x8050066: mov $0x4,%eax (gdb) 0x0805006b in ?? () 1: x/i $pc 0x805006b: int $0x80 (gdb) Hello 0x0805006d in ?? () 1: x/i $pc 0x805006d: add $0x10,%esp (gdb) p/x $eax $2 = 0x6 (gdb) c Continuing. Program exited normally.
msgのアドレスもちゃんと計算されてる。そしてシステムコールを実行すると、ちゃんと Helloを表示してる。writeの返り値は、出力したバイトサイズで、これは、eaxに載る約束だ。
ブレークポイントをセットする時、普通にやるとセット出来ない。break *adrのように、*を 付けるのが味噌だ。
Program received signal SIGTRAP, Trace/breakpoint trap. 0x08050055 in ?? () (gdb) x/10i $pc 0x8050055: push $0x6 0x8050057: mov $0x8050054,%eax 0x805005c: add $0x27,%eax 0x8050061: push %eax 0x8050062: push $0x1 0x8050064: push $0x0 0x8050066: mov $0x4,%eax 0x805006b: int $0x80 0x805006d: add $0x10,%esp 0x8050070: push $0x0 (gdb) b 0x805006b No symbol table is loaded. Use the "file" command. (gdb) i b No breakpoints or watchpoints. (gdb) b *0x0805006d Breakpoint 1 at 0x805006d (gdb) i b Num Type Disp Enb Address What 1 breakpoint keep y 0x0805006d (gdb) c Continuing. Hello Breakpoint 1, 0x0805006d in ?? ()
アドレス計算に使うlea命令を調べてみた。下記のように使えば良いみたい。(BASEの方を 演算子の左側にもってきてしまうと、正しい計算をしてくれないという罠があるので注意。)
lea msg + BASE, %eax
で、そのアセンブル結果。6バイト命令って最長なんですかね?
3: 8d 05 77 00 05 08 lea 0x8050077,%eax
そのうちに拡張されて1024バイト/命令ぐらいの、VLIW(Very Long Instraction Word)が出てきて ギネス認定されたりして。世界一複雑な命令ですってね。
おまけ
上記で、世界一小さい(かも知れない)Hello が出来た。(言っとくけど、LLとかだと もっとちちゃいよと言うのは無しね。、裏方にどえらい大きいのが控えてますから。rubyとか だと、どのくらい? bin/rubyが5kで、libruby18が936kだったよ。)
エントリーポイントがちと半端な値になってる。(0x8050054) どうでもいいけど、きりの いい数字の方がいいんでないかい。たとえば、0x8000000 とか。そうすれば、上記のパッチ宛も ちと楽になる。
has.cの部分で、
0x8050000 + ehdr_size + phdr_size, // e_entry
で、生数字(2箇所に有り)をいじってやる。ehdr_sizeは 52で phdr_sizeは、32となっているから
(gdb) p/x 0x8000000 - 52 - 32 $2 = 0x7ffffac
この数字に付け替えて、hasを作り直しておけばOK。 でも、8の下に0が6つも付くなんて おいらに入力させたら、絶対に個数を間違えるよなあ。まあこの辺は、FreeBSDがカーネルが やってくれから、どうでもいいんだけど。。
ちょいと悪戯っ気を出して、上記のパッチを宛てなくてもいい領域にロードするように してみたら、見事にSegv(ヌルポ)を喰らってしまったわい。なかなか楽出来ないね。
西田さんの所で紹介されてるモジュールを 使うと、更に更に大幅ダイエットが可能だったりします。すげぇー。 small そして、ゴルフしてる人もいるようで、 ELFでGolfなんてのもありました。 やっぱり、FreeBSDなんて言う伝統的なコースじゃなくて、Linuxなんていう何処にもあるコース が使われているんだなぁ。(あっ、石投げないでね。)