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なんていう何処にもあるコース が使われているんだなぁ。(あっ、石投げないでね。)