xv6-armv7 (6)
『世界史で学べ!地政学』(祥伝社)なんて本を読んでみた。書いたのは予備校で世界史を 教えておられる講師さん。
オイラーの学生時代に世界史なんて有ったっけかな? 古代オリエント文明とかエジプト 文明なんて言葉が出て来るから、きっと習ったんだろうな。日本史にしてもそうだけど、 ずっとずっと昔の事は丁寧に教えるくせに、何故か近代のやつは、時間切れで自分で 勉強してねって事だったように記憶してる。
先生がそこはかとなく、近代のやばい事は避けておきたいって気持ちが働いていたんでは と何となく疑いのマナコ。
この本は、そんなのをぶっ飛ばせって具合にブルブリと近代の話が出て来る。まるで、 あの池上さんが連日講義してる感。分かり易い。
地政は地勢から生まれる。回りが海か? 陸続きか?によって国の政治、衰退が変わって くるよ。言い換えれば、陸軍と海軍に考えの違い。日本でも戦前は対立してたね。
300ページの中で、アメリカ帝国、中国の悪魔、朝鮮半島、東南アジア、インド、ロシア、 ヨーロッパ、中東、アフリカと盛りだくさん。3回ぐらい読まないと頭に入ってこないよ。 これが理解出来たら、今のニュースがもっと面白くなるね。
著者の個人ブログ、 もぎせかブログ館も、これから定期的に 眺めてみよう。
doas
OpenBSD5.8が出て、早速sudo(8)を廃止してdoas(1)を導入 の試用が行われていた。シンプルな事UNIXなごとし。決してLinuxでは真似出来ないだろう。 このdoasをOpenBSD5.7にバックポート出来るか試してみよう。
ソースは、http://bxr.su/OpenBSD/usr.bin/doas/に 一式置いてある。で、コンパイルすると、pledgeが無いぞと言われる。無ければしょうがないので、 無理を承知で、その部分をコメントアウト。そしたら、コマンドが出来上がった。
走らせると、doas.confが無いと言われる。適当に/etc/doas.confを書いた。
permit nopass sakae as root cmd su
英語だなあ。sakaeさんをrootとみなして、パスワード無しでsuコマンドを実行するのを 許可します。まるで$Mの直情的翻訳だな。これで走らせると、
[ob: doas]$ ./doas su -l doas: failed to set user context for target
suの-lは、フルログインをシュミレートするオプションだそうだ。(初めて知った) で、どこで落ちてるか確認。
// if (pledge("stdio rpath id exec", NULL) == -1) // err(1, "pledge"); if (setusercontext(NULL, pw, target, LOGIN_SETGROUP | LOGIN_SETPRIORITY | LOGIN_SETRESOURCES | LOGIN_SETUMASK | LOGIN_SETUSER) != 0) errx(1, "failed to set user context for target");
pledgeってのは、manを引いても5.7には無い。5.8で新しく追加されたシステムコールだろうか? 使い方を想像するに、pledgeを発行した後に続く命令に特権を付与するって事かな。setusercontextは 調べてみると、LOGIN_CAP(3)に載ってた。
ああ、pledgeを解説してる人が居た。 なるほど、宣誓すんのか。それを外れるとエラー。宣誓して使える機能を絞っていくとな。
[ob: doas]$ grep ple doas.c | grep if if (pledge("stdio rpath getpw proc exec id", NULL) == -1) if (pledge("stdio rpath getpw exec id", NULL) == -1) if (pledge("stdio rpath id exec", NULL) == -1) if (pledge("stdio rpath exec", NULL) == -1) if (pledge("stdio exec", NULL) == -1)
最後の状態で、cmdを実行するとな。
pledge (2) 親分のおめがねにかなって、システムコールが新設されたとな。Linux逝ってよしってのが OpenBSD界隈の合言葉だからなあ。
NetBSD 7.0
見事にdoasは失敗してしまったので、5.8は後日のお楽しみに取っておいて、先に出た 「NetBSD 7.0」リリース、Luaカーネルスクリプティングをサポート を、ものの順番として早速入れてみた。以前からの懸念事項、man した時に、リンクすべき ライブラリィー名が表示されるかは、されてた。うろ覚えが解消出来てスッキリ。
SIN(3) Library Functions Manual SIN(3) NAME sin, sinf -- sine function LIBRARY Math Library (libm, -lm) SYNOPSIS #include <math.h> double sin(double x); :
pkg_addする時の、取り寄せ先は、下記のように設定。
export PKG_PATH=ftp://ftp2.jp.netbsd.org/pub/pkgsrc/packages/NetBSD/i386/7.0/All
なんだけど、emacsとかの大物は用意されていない。自分でportsからコンパイルして入れろってか? そんな事してたら、日が暮れてしまう。
x64の方は充実してるんで、みなさん64Bit系に移行しちゃったのね。寂しい限りだ。 CentOSと言い、軒並み32Bitユーザーに辛く当たるようになって、オイラー肩身が狭いよ。 まあ、スマホも64Bitの時代だからしょうがないのかな。
Thumb
ARMにはThumbと呼ばれるモードがある。16Bitの縮退モード。メモリーけちけち作戦の時に 使う。これはARMの主戦場、組み込み分野でメモリーが贅沢に取れない場合、メモリーを 切り詰める為、命令体系を16Bitにするのだ。
32Bitで一つの命令を保持してたのを止め、16Bitで一つの命令を保持する。どこかに犠牲が 出て来る。それは何処か? 16個自由に使えていたレジスタが8個までしか使えない等だ。 こうして、命令を圧縮するんだ。
組み込みしない人は関係ないじゃん。今じゃ、ねーちゃんが持ってるスマホでも、メモリー たっぷり積んでいるぞ。じゃ、何故残っている?
それは、こういう悪さをする為。 Linux ARM用のシェルコードを書いてみる
なかなか知恵が働きますなあ。(違ぅ)
sh
あなたの知らない>|と<>の使い方 なんてのを見てたら、結局ソース嫁取れば、いい事あるぞに落ち着くのね。 リダイレクトに的を絞って読んでましたけど、sh全体はどうよ?
身近なものを読んでみるか。xv6-armv7にもユーザーランドにちゃんとshellが鎮座してる。
sakae@uB:~/xv6-armv7/src$ wc usr/sh.c 494 1080 9901 usr/sh.c
500行にも満たない、かわいいやつ。どうなってるか? ちょいと開いてみる。
// Parsed command representation #define EXEC 1 #define REDIR 2 #define PIPE 3 #define LIST 4 #define BACK 5
冒頭付近にあった宣言。パースしてこれだけの種類に分類するんだな。EXECってのは普通の コマンド起動。REDIRはリダイレクト関係かな。そしてパイプ系があって、LISTってのは、 リストって言うぐらいだから括弧で括って一まとめにする系統か。最後はバックグラウンド 実行かな。shellの基本機能が実現されてると推測。更に見てくと、
struct pipecmd { int type; struct cmd *left; struct cmd *right; };
パースした結果が構造体に格納されるとな。昔、ユニマガのテクニック本を読んだ時、パース してコマンド木を作るって説明されてたけど、同じ考えだな。
ちと、実行してみる。
$ grep control UNIX | wc 3 173 1138 $ grep 1993 UNIX | wc 2 191 1198 $ (grep control UNIX; grep 1993 UNIX) | wc 5 364 2336
UNIXって言うテキストから、それぞれの語句でgrepして、行数を表示。次は、その2つの 語句の検索結果をリストにまとめて、wcしてみた。結果は合ってるな。次は、
$ echo 123456789 > zz $ echo abc >> zz $ wc zz & 2 2 10 zz zombie!
ちょいとリダイレクト系とバックグランド系を使ってみた。バックグランド系は、終了しても 引き取ってくれる人が居ないので、ゾンビになるんか。そりゃそうだ、バックグランドで起動 したやつの終了をwaitで待ってたら、次のプロンプトを出せないものね。
$ cat zz abc 56789
リダイレクトで、追加したはずが、上書きになってるぞ! これは、どうした事だ。gdbの 出番かな。
(gdb) symbol-file usr/_sh Reading symbols from usr/_sh...done. (gdb) b redircmd Breakpoint 1 at 0x4dc: file sh.c, line 212.
ユーザーランドのコマンドを実行するには、上のようにシンボルテーブルを切り替えてから BPを設定します。なお、コマンド名の頭にアンダーバーが付いているのは、debugの便を図る ためにコマンド単体でelfファイルを残しているからです。(ユーザーランドのコマンドは、 まとめてfs.imgにし、それをデータとしてkernel.elfに押し込んでいて、gdbからは直接触れない)
Breakpoint 1, redircmd (subcmd=0xcfa8, file=0x21cb "zz\n", efile=0x21cd "\n", mode=513, fd=1) at sh.c:212 : (gdb) p *cmd $3 = { type = 2, cmd = 0xcfa8, file = 0x21cb "zz\n", efile = 0x21cd "\n", mode = 513, fd = 1 }
続いて、これらのパースされたのの実行系 runcmdにBPを置いて追ってみます。
82 case REDIR: 83 rcmd = (struct redircmd*)cmd; 84 close(rcmd->fd); 85=> if(open(rcmd->file, rcmd->mode) < 0){ 86 printf(2, "open %s failed\n", rcmd->file); 87 exit(); 88 } 89 runcmd(rcmd->cmd); 90 break;
rcmd->fd は、1、すなわち標準出力。それをクローズして、今度は、渡ってきたファイルで オープン。こうすると、コマンドが標準出力だと思って出力すると、それはファイルへの 書き込みに変更されちゃう。こうして準備をしておいて、 再び、runcmdを呼ぶ。ああ、さりげなく再帰してるね。
70 switch(cmd->type){ 71 default: 72 panic("runcmd"); 73 74 case EXEC: 75 ecmd = (struct execcmd*)cmd; 76 if(ecmd->argv[0] == 0) 77 exit(); 78=> exec(ecmd->argv[0], ecmd->argv); 79 printf(2, "exec %s failed\n", ecmd->argv[0]); 80 break;
これが、普通のコマンドの実行部分。execに渡る引数は
(gdb) p ecmd->argv[0] $6 = 0x21c0 "echo" (gdb) p ecmd->argv[1] $7 = 0x21c5 "XY"
shに与えたコマンドは
$ echo XY >> zz
だから、再帰の基底部分を実行してるんだな。大体仕組みと、追いかけ方が分かったよ。
でも、追加してる積もりが上書きになってるのは何故?
struct cmd* parseredirs(struct cmd *cmd, char **ps, char *es) { int tok; char *q, *eq; while(peek(ps, es, "<>")){ tok = gettoken(ps, es, 0, 0); if(gettoken(ps, es, &q, &eq) != 'a') panic("missing file for redirection"); switch(tok){ case '<': cmd = redircmd(cmd, q, eq, O_RDONLY, 0); break; case '>': cmd = redircmd(cmd, q, eq, O_WRONLY|O_CREATE, 1); break; case '+': // >> cmd = redircmd(cmd, q, eq, O_WRONLY|O_CREATE, 1); break; } } return cmd; }
出力へのリダイレクトも追加モードのリダイレクトも、同じモードになってるぞ。ちょっと 本物のOSで確かめてみるか。
[ob: z]$ ktrace sh -c "echo 123456789 > zz" [ob: z]$ kdump : 24542 sh CALL open(0x817bed98,0x601<O_WRONLY|O_CREAT|O_TRUNC>,0666<S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH>) 24542 sh NAMI "zz" 24542 sh RET open 3 24542 sh CALL fcntl(1,F_DUPFD_CLOEXEC,0xa) 24542 sh RET fcntl 10/0xa 24542 sh CALL dup2(3,1) 24542 sh RET dup2 1 24542 sh CALL close(3) 24542 sh RET close 0 24542 sh CALL write(1,0x8286ef08,0xa) 24542 sh GIO fd 1 wrote 10 bytes "123456789 " 24542 sh RET write 10/0xa 24542 sh CALL dup2(10,1) 24542 sh RET dup2 1 24542 sh CALL close(10) 24542 sh RET close 0 :
[ob: z]$ ktrace sh -c "echo abc >> zz" [ob: z]$ kdump : 2438 sh CALL open(0x84174ba8,0x209<O_WRONLY|O_APPEND|O_CREAT>,0666<S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH>) 2438 sh NAMI "zz" 2438 sh RET open 3 2438 sh CALL fcntl(1,F_DUPFD_CLOEXEC,0xa) 2438 sh RET fcntl 10/0xa 2438 sh CALL dup2(3,1) 2438 sh RET dup2 1 2438 sh CALL close(3) 2438 sh RET close 0 2438 sh CALL write(1,0x83ed1608,0x4) 2438 sh GIO fd 1 wrote 4 bytes "abc " 2438 sh RET write 4 2438 sh CALL dup2(10,1) 2438 sh RET dup2 1 2438 sh CALL close(10) 2438 sh RET close 0
注目はopen時のフラグ
O_WRONLY | O_CREAT | O_TRUNC ;; for > O_WRONLY | O_APPEND | O_CREAT ;; for >>
ファイルをクリア(O_TRUNC)するか、ファイルに追加(O_APPEND)するかが分かれ目になって いるんだな。追加って、オープンした時に、ファイルポインターをファイルの最後に移動 しておけばいいのかな。ちょいと、man 2 open して調べてみる。
O_APPEND The file is opened in append mode. Before each write(2), the file offset is positioned at the end of the file, as if with lseek(2).
書き込みの前に、ファイルポインターをファイルの最後尾に移動しとくとな。そうなってるか、 sys_write()にBPを貼って確認してみる。(カーネル側に在るんで、シンボルテーブルを、kernel.elfに 切り替えてから)
90int sys_write(void) 91{ 92 struct file *f; 93 int n; 94 char *p; 95 96 if(argfd(0, 0, &f) < 0 || argint(2, &n) < 0 || argptr(1, &p, n) < 0) { 97 return -1; 98 } 99 100=> return filewrite(f, p, n);
(gdb) p *f $1 = { type = FD_INODE, ref = 1, readable = 0 '\000', writable = 1 '\001', pipe = 0x0, ip = 0xc00ac5ac <icache+212>, off = 0 } (gdb) p p $2 = 0x3f8b "X\001" (gdb) p n $3 = 1
offってのが、ファイルポインターだ。ファイルの先頭から書いてるよ。そして書くべき文字列として、 "XY"を指定したんだけど、コンソール出力相当なので、1文字づつ書き出そうとしてるんだな。 こういう非効率には眼をつぶりましょう。
(gdb) p *f->ip $5 = { dev = 1, inum = 19, ref = 1, flags = 2, type = 2, major = 0, minor = 0, nlink = 1, size = 10, addrs = {466, 0 <repeats 12 times>} }
これはiノード構造体。ファイルサイズが10Byteってなってるから、これを見て、ポインターを 動かしてしまえば良いのかな。ファイルを切り詰めるには、ここをゼロにするのか。 こういった事を組み込めば、上でみた、O_TRUNCとO_APPENDが実現出来そう。 後、最後の項に、addrsってあるけど、これはDISK内で512Byte単位で数えた、ブロック番号だ。
ああ、どうでもいいけど、上で出てきたUNIXって文書、元ネタは、 History and Timeline これを図にするとこうなる 。Linuxってのは、unixと互換性のあるもどきOSなのね。(何を今更)
console
BSDの元となったOSは、uv7って事で良く知られている。uv6のライオン本には、ttyなんて 言葉が出て来る。近代的なxv6では、さすがにその言葉は消え失せて、consoleなんてのに 変わってる。
shもお世話になってるぞ。下記はmainの冒頭付近。
// Assumes three file descriptors open. while((fd = open("console", O_RDWR)) >= 0){ if(fd >= 3){ close(fd); break; } } // Read and run input commands. while(getcmd(buf, sizeof(buf)) >= 0){
取りあえずのお約束で、ファイルディスクリプタを3つ開いてる。読み書き自由ってのは、 少々雑だけど、試し実装だから許そう。あれ? 一つ疑問、consoleっってファイルに対して 読み書きして良いのか?それにこのファイルを作成していないぞ。 今までの常識では通用しなさそう。
$ ls console console 3 18 0
最初の3は、stat.hを参照すると、T_DEV って事でDeviceファイルだった。ちなみに2はレギュラーファイル。1は ディレクトリィーと定義されてた。デバイスファイルのサイズってゼロなんですね。存在する事に 意義が在るって事だ。
consoleは、usr/init.cの中で作ってた。
if(open("console", O_RDWR) < 0){ mknod("console", 1, 1); open("console", O_RDWR); } dup(0); // stdout dup(0); // stderr
失敗する事を期待してconsoleファイルを開いて、それからデバイスファイルとして作成。 出来たやつを開くと、fdはゼロのstdin相当。続いてdupを2回行って、stdoutとstderr用 とするとな。失敗を期待ってのは、確実にデバイスファイルを作成させる方策なんだな。 (同名のレギュラーファイルやdirが無い事を確認)
続いて、getcmdで文字列を取り込んで、それを解析 してってのの永久ループが始まる。
ここで出てきた、getcmdは
int getcmd(char *buf, int nbuf) { printf(2, "$ "); memset(buf, 0, nbuf); gets(buf, nbuf); if(buf[0] == 0) // EOF return -1; return 0; }
プロンプトを出してから、getsで一行読み取りかな。getsは、共通ライブラリィって事で、 ulib.cに実装されてる。printfは、printf.cってライブラリィにまとめられているよ。 その作りはBSDのそれと似てる。やっぱり直系の子孫だね。glibのわけわかめとは違って素直。 ソース読むならBSD系に限ると思うぞ。ハロワ攻略本でも、そこはかとなくそんな雰囲気を かもし出していた。 そんじゃgetsの中核部分を見ると、
for(i=0; i+1 < max; ){ cc = read(0, &c, 1); if(cc < 1) break; buf[i++] = c; if(c == '\n' || c == '\r') break; }
一文字づつ読み込んでいるんですね。readシステムコールの登場です。行末文字を検出して ループを抜けてるね。
sys_readにBPを貼って監視してると、コンソールで一行入力してリターンを叩いた時にbreakがかかる。 それはいいんだけど、コンソールで一文字キーインした時に文字表示されてるぞ。 それはどういう理屈?
console.cをつらつら見て行くと、consoleintrなんてのが眼に入ってきた、コンソール割り込みかな。 そこにBPを置いてgdbを継続。コンソールからキーインしたらbreakした。
Breakpoint 1, consoleintr (getc=0xc0028bf0 <uartgetc>) at console.c:173 173 acquire(&input.lock); (gdb) bt #0 consoleintr (getc=0xc0028bf0 <uartgetc>) at console.c:173 #1 0xc0028c80 in isr_uart (tf=0xc0011f60, idx=5) at device/uart.c:82 #2 0xc0029440 in pic_dispatch (tp=0xc0011f60) at device/gic.c:247 #3 0xc002776c in irq_handler (r=0xc0011f60) at trap.c:25 #4 0xc0027610 in trap_irq () #5 0x00004fb0 in ?? ()
デバドラのuartから通知されたんだな。こういう下回りをボトムハーフって言うそうだ。 それに対して、カーネル内の上層部をトップハーフって 言うとか。こういう隠語で、会話出来るようになりたいものだ。
この処理ルーチンの引数名はgetcで、実体はuartgetcっての。こういう人を惑わす、もとえ 上の層の人にも読んで貰えるような工夫がされてるね。uart.cをちゃんと読もうとしたら、 ハードの仕様書を読まないとならないね。デバドラ屋さんは、ハードとソフトの両刀使いだぞ。
consoleintrをつらつらと見て行くと、以前にやった、CTL+Pで、プロセスリストを出したり、 一行末梢とか一文字末梢とか、簡単なクックモードが実現されてる。 更には、wakeupが組み込まれていて、上位にデータが整った事を知らせている。
wakuupとかsleepで、旨くプロセスが切り替わって行くように促している訳だ。
415void sleep(void *chan, struct spinlock *lk) 416{ : 436 // Go to sleep. 437 proc->chan = chan; 438 proc->state = SLEEPING; 439 sched(); : } 452// Wake up all processes sleeping on chan. The ptable lock must be held. 453static void wakeup1(void *chan) 454{ 455 struct proc *p; 456 457 for(p = ptable.proc; p < &ptable.proc[NPROC]; p++) { 458 if(p->state == SLEEPING && p->chan == chan) { 459 p->state = RUNNABLE; 460 } 461 } 462}
上記はproc.cに定義されてた。sleepの引数chanは、chanが整うまでお休み(別のプロセスに CPUを明け渡す)しますって意味。例えば、データが一行入力されるまでとか、diskからの 読み出しが完了するとか、時期未定な事象を期待。
それに対して、要求が整った時に、sleepしてるプロセスを起こすためのトリガーをかける のは、wakeupの役目だ。
今日の読み物
Linux on z Systems 向けインライン・アセンブリーの基礎
Linux on z Systems 向けインライン・アセンブリーの高度な機能
IBM z Systems の IBM XL C/C++ コンパイラーとインライン・アセンブリーを使用してパフォーマンスを向上させる
z Systemの石ってPower何とかってやつ? まあ細けい事はどうでも良い。野次馬になって 幅を広げておけば、何かの時に役に立つだろう。