xv6-armv7(3)

警察から呼び出し状が来た。Webに悪い事(spam)を書いているから、怪しい人フラグが立ったの かなと思ったら、違った。

そろそろ、運転免許証の更新時期だから、忘れんようにお願いします案内だった。無線局の 更新依頼案内と言い、行政はやる事をやってるね。感心々。まあ、税金を徴収されてるんで 当然のサービスと言えば、それまでだけど。

免許証の更新時に、よれよれおじさんになってないか、眼はちゃんと見えるかって検査が 有ったはず。目は幾つあればいいんだっけ? そりゃ、2つに決まってるだろ。3つとか1つ じゃ大変じゃん。違う違う、視力のリミットって幾つだって事。

調べたら、両目で0.7、単眼で0.3ってのが合格ラインみたいだ。パイロットは、って調べて みたらパイロットになるために必要な視力は?航空身体検査の合格条件とか。 が出てきた。無線従事者の免許も必要なんだな。そりゃそうだ、管制官とお話するには、無線機を 使うんで当然か。

それはさておき、視力ってどういう定義なの? 視力1.0の基準はなに?とか らしい。そうすると、アフリカの現地人の脅威の視力10.0とか、逆に免許のリミット0.7とか は、どうやって算出するの?

基準の輪が見えなかったら、輪に近づいていく。仮に3mの所で見えたとすると、3/5って事で、 視力0.6って判定。逆に10mの所で見えれば、10/5で、視力2.0って事になる。 実際は、近寄ったり離れたりしての判定では面倒なので、輪の大きさを相対的に変えて、判定するそうだ。

これらを調べている時、 視角って言葉が出てきた。 視角繋がりで、 「太陽と五円玉」とか 「五円玉の話」~五円玉の穴から満月は見えるか? に行きあたったよ。

5m離れた所で、1分の角度(60分で1度)なら、大きさは幾つ? lispで確認

> (define (d2r d) (div (mul d (acos 0)) 90))
(lambda (d) (div (mul d (acos 0)) 90))

> (mul (tan (d2r (div 60))) 5000)
1.454441

d2r関数は、度をラジアンに変換するやつ。結果は1.45mmって事で合ってるね。

余談だけど、角度θが微小である場合、tanθ又はsinθの値はθとほぼ同じってのを、突然思い出したぞ。

> (tan (d2r (div 60)))
0.0002908882168703159
> (d2r (div 60))
0.0002908882086657216
> (sin (d2r (div 60)))
0.0002908882045634246

本当だったな。余談終了。

今の話は、分の世界の人間相手だけど、望遠鏡だと、更に細かい秒の世界の話になる。 望遠鏡のカタログの読み方~有効径、集光力、極限等級、分解能、有効最高倍率 で、知識を得て、盗撮用の望遠鏡を買うんじゃねーぞ。ああ、またサツに眼付けられるな。

そうそう、角度を度分秒という60進法で表す方法、コンピュータにとっちゃ煩わしいと、 ググル様あたりの地図の座標では廃止してるね。それに変わって、度以下を10進の少数点表示法を 使ってる。

こういう単位系は世の基本。国際的にはSI単位なんだけど、かたくなにそれを拒否してる国が ある。アメリカとかエゲレスとかの英語圏。

この間、クロ現で、女性の痩せすぎに注意しましょうなんてのをやってた。BMIで18以下の モデルさんは雇わないとか。BMIって、身長/体重/体重 身長はメートル、体重はキログラムで 測った時、即BMIになるんだけど、アメリカあたりだと、身長はフィートとインチ、体重はポンドかな。 そのままじゃ、即BMIにならない。 面倒な事は止めて、自然に任せましょ、てんで、肥満が蔓延中ですよ。

英語圏がメートル法に乗り遅れたのには訳があるとか。昔、フランス革命が成功して、単位を 一新しようてんで、地球の大きさを元にメートルの長さを決めた。そこからの誘導で、重さも 決めた。庶民には、これだけあれば十分。

メートル法を広めようってんで、フランスが中心になって世界に呼びかけた。その時、フランスと イギリスは戦争中。で、イギリスを中心とする英語圏の国(いわゆる大英帝国系)は、その会議に招待されなかった。 かくして、英語圏は、メートル法に乗り遅れ、今更変えられよーーーってなった。

軍需関係とか航空系は、もうヤード・ポンド系が永久に残るんでしょうな。

armな石は知恵遅れ?

この間からやってるxv6のarm版、Fedoraでも動くかと思ってやってみた。

[sakae@fedora src]$ make
make -C tools
make[1]: Entering directory '/home/sakae/xv6-armv7/src/tools'
make[1]: Nothing to be done for 'all'.
make[1]: Leaving directory '/home/sakae/xv6-armv7/src/tools'
make -C usr
make[1]: Entering directory '/home/sakae/xv6-armv7/src/usr'
arm-linux-gnu-gcc -march=armv7-a -mtune=cortex-a15 -fno-pic -static -fno-builtin -fno-strict-aliasing -Wall -Werror -I. -g  -iquote ../   -c -o cat.o cat.c
arm-linux-gnu-ld -L. -N -e main -Ttext 0 -o _cat cat.o ulib.o usys.o printf.o umalloc.o  -L ../ /usr/lib/gcc/arm-linux-gnueabi/5.1.1/libgcc.a
/usr/lib/gcc/arm-linux-gnueabi/5.1.1/libgcc.a(_dvmd_lnx.o): 関数 `__aeabi_ldiv0' 内:
/builddir/build/BUILD/gcc-5.1.1-20150618/arm-linux-gnu/arm-linux-gnueabi/libgcc/../../../gcc-5.1.1-20150618/libgcc/config/arm/lib1funcs.S:1349: `raise' に対する定義されていない参照です
Makefile:30: recipe for target '_cat' failed
make[1]: *** [_cat] Error 1
rm cat.o
make[1]: Leaving directory '/home/sakae/xv6-armv7/src/usr'
Makefile:69: recipe for target 'build/fs.img' failed
make: *** [build/fs.img] Error 2

結果は、見た通りにコンパイルエラーですよ。いや、コンパイルエラーと言うよりもリンクエラー。 libgccの中のldiv0内で使ってる、raiseが未定義ですって。一体どゆ事? それにlibgccって何よ?

Makefileを見ると、こんな興味深い事が

# link the libgcc.a for __aeabi_idiv. ARM has no native support for div
LIBS = $(LIBGCC)

ARMな石は、知恵遅れで、割り算が出来ません。そんな不幸な石を支援する為にGNUが乗りだし ました。libgcc.aの中に、ソフトで実現した割り算が入ってるから、それ使うよとな。

昔から、加減乗除出来る事が一種のステータスだったはずなのに、ARMの人はその努力を 怠った。どうせ組み込みにしか使わない石だから、複雑な割り算回路は載せるのさぼちゃえ、 そうすれば、ZEROで割り算しようとして、怒り出す回路も不要と考えたんだな。 納得です。

で、Frdoraのlibgccは、割り算をソフトでやるのは実現したけど、どうやら連携が悪くて、 怒りだす真似をさぼったみたいだな。

divmod:	mov  r2, #0        // r0 = r0/r1,   r1 = r0%r1
1:	add  r2, r2, #1    // inc r2
	subs r0, r0, r1    // r0 = r0 - r1
	bge  1b
	add  r1, r0, r1
	sub  r0, r2, #1
	bx lr

小学生並みの割り算を実現。割る数をr1に割られる数をr0に入れてコール。負になるまで、 割る数を割られる数から引いていく。その度にカウンターr2を増やす。負になったら、余りの 数と商を調整。余りはr1に商はr0に得られる。

所で、こんな割り算、何処で使ってるの? 臭いのはcatを作る時にリンクしてるprintf。 ざっと、出来上がったcatを見ると、

    }while((x /= base) != 0);
     9d0:       e51b3038        ldr     r3, [fp, #-56]  ; 0x38
     9d4:       e51b0018        ldr     r0, [fp, #-24]
     9d8:       e1a01003        mov     r1, r3
     9dc:       eb000191        bl      1028 <__aeabi_uidiv>
     9e0:       e1a03000        mov     r3, r0

こういう使い方をされてましたね。10進もしくは16進数で表示するルーチン。そうすると、 catとかでは特に必要ではないけど、標準ライブラリィーをリンクするっていうノリなんだな。 気がついてウブのmakeログを見たら、同じものをリンクしてたよ。

ユーザーランドを攻略しようと思ったら、 ulib.o usys.o printf.o umalloc.o相当をも見とけ。 ulibは基本文字列関数、usysはシステムコールの定義、umallocはKernighan and Ritchieの C語バイブル本に出てたmalloc/free関数。こいつぁ面白い。morcoreの中でsbrkを使って、 カーネルからメモリーを貰ってくるくだりがあるんだけど、今なら壁のあちら側を見放題 ですよ。

実際にdivmodを使う

早速上のdivmodを使ってみる。usr/test.cを書くんだけど、今回はアセンブラーとC語の 合わせ技になる。アセンブラーで書いたのをC語から呼ぶにはどうするか? ABIの規約が あるはず。引数はどうやって渡す。壊していいレジスターはどれだとか。 ARM ABI 規約を見ておk。

sakae@uB:~/xv6-armv7/src$ cat usr/test.c
#include "types.h"
#include "stat.h"
#include "user.h"

int div(int, int);
int mod(int, int);

int main(int argc, char *argv[]) {
    printf(1, "%d\n", div(atoi(argv[1]), atoi(argv[2])) );
    printf(1, "%d\n", mod(atoi(argv[1]), atoi(argv[2])) );
    exit();
}

divとmodの型宣言は、user.hの中にでも押し込んでおくのが正しい態度だけど。

sakae@uB:~/xv6-armv7/src$ cat usr/divmod.S
.globl div
div:    mov  r2, #0        // r0 = r0/r1,   r1 = r0%r1 
1:      add  r2, r2, #1
        subs r0, r0, r1
        bge  1b
        sub  r0, r2, #1
        bx lr

.globl mod
mod:    mov  r2, #0
1:      add  r2, r2, #1
        subs r0, r0, r1
        bge  1b
        add  r0, r0, r1
        bx lr

後は、usr/MakefileのULIBにdivmod.oを追加し、UPROGSに_testを追加。make clean; makeすると 下記のように実行出来る。

ああ、C語の関数の引数は左からr0,r1 ...のように割付られるのだった。 戻り値はr0に入れる。これがABIの約束。余り沢山の引数を渡そうとすると、しょーがねぇなあ、 スタックに積んでやるわいとなる。何から何までスタックに積む糞石仕様とは違うので注意。

スタックに積むって、メモリーへの読み書きなんで遅くなるのは必定だぞ。レジスターが沢山ある石の 面目が保たれていますなあ。だから、スタックへ積まなければ引数を渡せないなんてのは ちとおかしな設計だ。もしそんな関数になったら分割せいと言う警告と思えとな。

$ test 10 2
5
0
$ test 10 3
3
1
$ test 2 5
0
2
$ test -10 3
0
0

マイナスを含んだ割り算は出来ない、少々知恵遅れの演算装置が出来上がりました。 ちゃんとやろうとしたら、intをuintに直して演算し、後で符号を考慮するって事を やらないと駄目だな。それにZEROで割ろうとすると暴走と言うかループするだろうし。

これをprintfに適用するとコンパイルエラーは無くなるだろうけど、kernel本体の方でもdivmodを 使ってるんで、それを割り出して対処しないと、Frdoraでは動かんな。面倒な事はヤメ。

システムコールを追加する

で、あこがれののシステムコールの追加です。これ一度やってみたかったのよ。昔のBSDマガジン だったかで得意気に記事になってた覚えがあるけど、コンパイル時間が長大になるので、指を 咥えて見てたんだ。今なら、数秒でコンパイルは終わるから、気軽に出来るだろう。

仕様はどうする。ありきたりだけど、割り算で商を求めるシステムコール。uv6だかの記事を見てた時に、 足し算の例題に上がってた。カーネル側に手を入れるのは勿論だけど、ユーザーランド側にも手を 入れなければならない。壁の両側から攻めて、壁を通りぬける訳だ。

一応、上のみっともない失敗を踏まえて、システムコール名は、udivにしておこう。 ZEROで割ろうとしたら、エラー(-1)を返す事にする。

そんじゃ、ユーザーランドのテストコードの方から。先にテストコードを書くのが常識 みたいですから。

sakae@uB:~/xv6-armv7/src/usr$ cat sysdiv.c
#include "types.h"
#include "stat.h"
#include "user.h"

int main(int argc, char *argv[]) {

        int a,b,c;

        a = atoi(argv[1]);
        b = atoi(argv[2]);
        c = udiv(a, b);

        if (c < 0){
                printf(1, "Divide by ZERO\n");
        } else {
                printf(1, "%d\n", c);
        }
    exit();
}

後は、user.hにプロトタイプ宣言追加。usys.Sにシステムコール名を追加。ユーザーランド側の変更は以上。 そして、テストプログラムのコンパイルが通るように、カーネル側のsyscall.hにシステムコール番号22を追加。

$ sysdiv 10 3
prefetch abort at: 0xe59f1024 (reason: 0x0)
r14_svc: 0xe59f1024
   spsr: 0xa00000d3
     r0: 0xc7fe4fb8
     r1: 0xa
     r2: 0x16
     r3: 0xe59f1024
     r4: 0x3
     r5: 0x0
     r6: 0x0
     r7: 0x0
     r8: 0x0
     r9: 0x0
    r10: 0x0
    r11: 0xc7fe4fa4
    r12: 0x0
     pc: 0xe59f1024

上記のはらわたは、trap.cの中のdump_trapframeが出していたぞ。

(gdb) bt
#0  dump_trapframe (tf=0xc7ffefb8) at trap.c:136
#1  0xc00277e0 in iabort_handler (r=0xc7ffefb8) at trap.c:72
#2  0xc00275e8 in trap_iabort ()
#3  0xe59f1024 in ?? ()

trap_iabortってのは、trap_asm.Sに収録されてたから、底の底の部分だな。これ以上 深入りはよそう。まあ、指定したSWIの番号に相当する飛び先が無かったって事だな。

カーネル側に追加

lib/div.S に機械語を追加。場所はまあ、適地かな。そして、syscall.cにシステムコールの 所在を追加。実際の仕事を登録するのは、どこでもいいんだろうけど、みた所sysproc.cに より集まっていたので、そこに追加。では、早速試運転。

$ sysdiv 10 2
Divide by ZERO

あれ? 2はZEROじゃないんだけどな。テストコードの間違いと言うより、カーネル側の問題 だろうね。

 26int sys_udiv(void) {
 27  int a,b;
 28=>if (argint(1, &b) == 0){
 29    return -1;
 30  }
 31  argint(0, &a);
 32  return div(a, b);
 33}
 34
 35int sys_kill(void)
 36{
 37    int pid;
 38
 39    if(argint(0, &pid) < 0) {
 40        return -1;
 41    }
 42
 43    return kill(pid);
 44}

ユーザーランド側から渡ってきた引数をargintで取り出してって、sys_killを真似て書いたんだ。 で、引数がZEROならエラーで帰る。あれ? 引数の取り出し変数名の前に付いているアンパサントは何だ?

argintの成功/失敗を比較しちゃってるな。多分。argintの中に入ってみると(syscall.c)

 48// Fetch the nth (starting from 0) 32-bit system call argument.
 49// In our ABI, r0 contains system call index, r1-r4 contain parameters.
 50// now we support system calls with at most 4 parameters.
 51int argint(int n, int *ip)
 52{
 53=>  if (n > 3) {
 54        panic ("too many system call parameters\n");
 55    }
 56
 57    *ip = *(&proc->tf->r1 + n);
 58
 59    return 0;
 60}

教育用OSのため、しっかりと説明がありますなあ。この時のトラップフレームを確認すると

(gdb) p *proc->tf
$2 = {
  sp_usr = 16316,
  lr_usr = 80,
  r14_svc = 2144,
  spsr = 2147483728,
  r0 = 22,
  r1 = 10,
  r2 = 2,
  r3 = 0,
  r4 = 2,
  r5 = 0,
  r6 = 0,
  r7 = 0,
  r8 = 0,
  r9 = 0,
  r10 = 0,
  r11 = 16348,
  r12 = 0,
  pc = 2144
}

システムコール番号22、引数は10と2って事でちゃんと渡ってきてるじゃん。ついでに、argintの 周辺を見回しておくと、argstrとargptrの2種類が用意されてた。周辺観光が済んで本題です。

int sys_udiv(void) {
  int a,b;
  if (argint(0, &a) < 0 || argint(1, &b) < 0){
    return -1;
  }
  if (b == 0){
    return -2;
  }
  return div(a, b);
}

引数の取り出しにもマナーが有るようで、成功/失敗を必ずチェックしてました。これ防衛策だな。 そして、システムコールの第二引数がZEROかチェック。ここでやるのが正解なのか、divの 中で行うのが流儀なのかは、知りませんが、この場合はdivはアセンブラーなので、面倒な 事はC語って方針です。

$ sysdiv 12 3
4
$ sysdiv 10 0
Divide by ZERO

一応、目的達成。

最後に、syscall.cに登録した登録簿と、それのハンドリングを見ておく。

static int (*syscalls[])(void) = {
        [SYS_fork]    sys_fork,
        [SYS_exit]    sys_exit,
           :
        [SYS_close]   sys_close,
        [SYS_udiv]    sys_udiv,
};

void syscall(void)
{
    int num;
    int ret;

    num = proc->tf->r0;

    //cprintf ("syscall(%d) from %s(%d)\n", num, proc->name, proc->pid);

    if((num > 0) && (num <= NELEM(syscalls)) && syscalls[num]) {
        ret = syscalls[num]();

        // in ARM, parameters to main (argc, argv) are passed in r0 and r1
        // do not set the return value if it is SYS_exec (the user program
        // anyway does not expect us to return anything).
        if (num != SYS_exec) {
            proc->tf->r0 = ret;
        }
    } else {
        cprintf("%d %s: unknown sys call %d\n", proc->pid, proc->name, num);
        proc->tf->r0 = -1;
    }
}
(gdb) p syscalls
$1 = {0x0, 0xc00272c4 <sys_fork>, 0xc00272dc <sys_exit>, 0xc00272f4 <sys_wait>, 0xc00\
271b4 <sys_pipe>, 0xc0026418 <sys_read>, 0xc0027390 <sys_kill>, 0xc0027064 <sys_exec>\
, 0xc00265c8 <sys_fstat>, 0xc0026fa8 <sys_chdir>, 0xc00263a4 <sys_dup>, 0xc00273dc <s\
ys_getpid>, 0xc0027404 <sys_sbrk>, 0xc0027478 <sys_sleep>, 0xc0027544 <sys_uptime>, 0\
xc0026c8c <sys_open>, 0xc00264b8 <sys_write>, 0xc0026ee0 <sys_mknod>, 0xc0026884 <sys\
_unlink>, 0xc0026640 <sys_link>, 0xc0026e64 <sys_mkdir>, 0xc0026558 <sys_close>, 0xc0\
02730c <sys_udiv>}

上のコード中に出て来るNELEMって何だと思って調べたら、

defs.h:#define NELEM(x) (sizeof(x)/sizeof((x)[0]))

自動配列サイズ検出器なのね。こういう技は覚えておくと徳になるな。

配列の初期化

上の、syscalls配列で、面白い技が披露されてた。曰く、syscalls[0]は宣言してないのに 勝手に挿入(0x0)されてるぞ。それに、初期化の文の中で [n]ってのが使われている。

宣言事態が怪しげ

static int (*syscalls[])(void) = {

ってのは、引数無しで返り値がintの関数の集まりですよ。しかも、エレメントは幾つ 有るか分からんて事だな。いわば、関数のポインター宣言だ。

戻り値の型 (*ポインタ名)(引数リスト);
int        (*f)         (int n);

そして、それぞれのエレメントの番号を鍵括弧で前置するとな。そうしておくと、 コンパイル時に、順番通りに並べかえてくれるとな。

以上の検証の為、無理を承知で、文字列で同じ事をやってみた。実験する舞台は、FreeBSD10.1です。 なぜFreeBSDにしたか、ccがFreeBSD clang version 3.4.1を使っていたから。GNUが勝手な 拡張をやってて、それを知らないで使うとロックインされますからね。

#include <stdio.h>

#define NELEM(x) (sizeof(x)/sizeof((x)[0]))
#define san 3
#define ni  2
#define ici 1
#define yon 4
#define rok 6

char (*hoge[])(void) = {
        [rok] "666",
        [san] "333",
        [ici] "111",
        [yon] "444",
        [2]   "222",
};

int main(){
        int i;
        for (i = 1; i< NELEM(hoge); i++){
                printf("%s\n", hoge[i]);
        }
}

コンパイルすると、いろいろ警告が出てくるけど、一応バイナリーは作られる。警告で象徴的なのは、これ。

[sakae@fb10 ~/z]$ cc -g t.c
t.c:11:15: warning: use of GNU 'missing =' extension in designator
      [-Wgnu-designator]
        [rok] "666",
              ^
              =

ほらね、GNUの拡張戦略が効いてますでしょ。あの陣営はLinuxを取り込んで、わが世の春だからなあ。

[sakae@fb10 ~/z]$ gdb -q a.out
(gdb) p hoge
$1 = {0, 0x804862c <.rodata>, 0x8048630 <.rodata+4>, 0x8048634 <.rodata+8>,
  0x8048638 <.rodata+12>, 0, 0x804863c <.rodata+16>}
(gdb) p/x  *0x804863c
$3 = 0x363636

ASCII表現だけど、666が格納されてる。走らせると

(gdb) run
Starting program: /usr/home/sakae/z/a.out
111
222
333
444
(null)
666

ちゃんと未定義な所はnullって断りを入れてくれた。同じ事をウブでやると

sakae@uB:~/t$ gdb -q a.out
Reading symbols from a.out...done.
(gdb) p hoge
$1 = {0x0, 0x80484e0, 0x80484e4, 0x80484e8, 0x80484ec, 0x0, 0x80484f0}

gdbの表示も違うな。6番目を検査してみると

(gdb) p/x *0x80484f0
$2 = 0x363636

この表現は同じ、ちゃんと666が格納されてるって言ってきた。走らせると

(gdb) run
Starting program: /home/sakae/t/a.out
111
222
333
444

Program received signal SIGSEGV, Segmentation fault.

エレメントの1から4まで、表示。5番目は登録無しを表示しようとして、SEGV。 旨く使うと省プログラミングになるな。

上のテストコードは、かなりこじつけだけど、普通にテストするのなら、配列の初期化の 所を次のようにする。

char *hoge[] = {
        [rok] "666",
        [3]    "333",
        [ici] "111",
        [yon] "444",
        [ni]  "222",
};

そうすれば、gdbも素直に

(gdb) p hoge
$1 = {0x0, 0x804862c "111", 0x8048630 "222", 0x8048634 "333", 0x8048638 "444",
  0x0, 0x804863c "666"}
(gdb) p (char *)0x804863c
$2 = 0x804863c "666"

使えるぞ。で、乗り掛けた船。OpenBSDのあの人は、どういう反応をするだろう? 一応、ご機嫌を 伺っておくか。

(gdb) p hoge
$1 = {0, 0x20000001 <__fini+536868097>, 0x20000005 <__fini+536868101>,
  0x20000009 <__fini+536868105>, 0x2000000d <__fini+536868109>, 0,
  0x20000011 <__fini+536868113>}
(gdb) run
Starting program: /home/sakae/z/a.out
111
222
333
444

Program received signal SIGSEGV, Segmentation fault.
strlen (str=0x0) at /usr/src/lib/libc/string/strlen.c:39
39              for (s = str; *s; ++s)

同じgccだけど挙動が違いますなあ。エレメントは珍しい事に奇数番地に配置されてます。 今までの常識を打ち破る快挙ですよ。 これもASLR の一端を狙ったものでしょうか? いろいろやってみると、奇数限定って訳でもなく、奇遇 な感じですね。じやなくて、文字列のリテラルは、0x20000001から隙間無く詰められていくので、 文字数の奇偶によって変わるって結論でした。

そして、SEGVになっても、どこを回っていたか追跡してくれている。しかも ライブラリィ内のソース表示付き。

奇数番地から配置って、確信犯(褒め言葉)なんですかね。いやー、いろいろあって面白い!!!

ASLR(Adress Space Layout Randomization)

ASLRがあっても出来るreturn-to-libc攻撃

ASLR回避技術とChromeのsandboxの突破exploit

x86 Linux シェルコード作成

セキュア・プログラミング講座