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何とかってやつ? まあ細けい事はどうでも良い。野次馬になって 幅を広げておけば、何かの時に役に立つだろう。