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)