xv6-armv7 (5)

『AIは心を持てるのか』(日経BP社)なんて本を読んだ。これで何冊目なんだろう。この系統の本。 ブームを煽って、CPUの利用を促す戦略。これもひょっとして、尼のEC戦略に乗せられて、 本を書けば売れ末世なんかな。

最近は富が1%の人に集中しちゃってる。これは資本主義のなせる業。富が富を生む循環に 入ってしまった為だ。こんな事は許されるのかって、いつぞやのTVで池上先生がまとめてた。

これを打破するには、政府に超協力なAIマシンを装備させ、全体が豊かになるように、AIに 采配してもらえばいいんでないかい。AIじゃないオイラーが思うには、遺産相続禁止に しちゃえばいいと思うぞ。そうすれば、金儲けする意味無くなるじゃん。これは、ちと浅はかな 考えかな?

上で超協力って誤変換しちゃったけど、本意は超強力ね。でも、よく考えたら超協力ってのも ある意味間違いじゃないね。世の中の趨勢がそういう方向を目指してる。たとえば飛行機。 各種のコンピュータが搭載されてて、AIってかある程度の自動化は実現されてるけど、最後の 判断は人に任せてる。

そんな事が如実に分かる取り組みがなされるようだ。今まで、人間の棋士とコンピュータが 対戦するってのが話題をさらってた。曰く、コンピュータは人を越えられるか?って構図 だったけど、今度は、協力しあったら、パワーがブーストされるんじゃねぇって事。

電王戦タッグマッチが 始まるらしい。どうなるかなあ?要注目。あっ、これも面白い記事だな。 電王戦と独機墜落から考える人工知能の今後 そして、情報処理学会からは、 コンピュータ将棋プロジェクトの終了宣言 なんてのが出された。高らかな勝利宣言でしょうか。それとも、AIは遊びじゃないのよーーー、 もっと儲かる口を見つけろーーと、産業界から圧力がかかったから? いろいろ考えてみよう。

自動車が馬の延長ではなく、飛行機が鳥の延長ではなかったように、
コンピューター将棋も人間の思考の延長ではなかっただけですが、
応用が利かず、研究の隘路にはまったように感じました。

案外、真実は上記のような事かも知れないな。

かのデカルトさんも言ってるじゃないですか。『我思う、ゆえに我あり』って。AIにこの心が 分かるのでしょうか? おっと、心は今の所未定義じゃったわい。

富って事で、ちと書いてみたけど、こちらは 貧困についてグーグル先生に聞いてみた そうです。ぐぐる先生が提示したものが世の中の全てか? 都合の悪い事は隠してしまえ 引力が、どこからか放射されていないか? 疑ってかかれ。相手は人口知能だぞ。

fedoraのあれ

って、何ちゅうタイトルだ。fedoraでxv6を動かした時エラーで落ちる。落ちた時に 出してくれるスタック状況には再現性がある。

starting xv6 for ARM...
cpu0: panic: popcli - interruptible
15: 0x0
 :
6: 0x0
5: 0xc0020750  ;; after sti() in popcli()
4: 0xc0025e74  ;; if (first) in forkret()
3: 0xc00262c0  ;; after popcli() in release()
2: 0xc00206c0  ;; if (--cpu->ncli < 0) in popcli()
1: 0xc0021c68  ;; after show_callstk() in panic()

これ、マシン語のアドレスなんで、kernel.asmをgrepして、何処に居たか調べておいた。

panicするとarm.c/show_callstk()で、stackの中を辿ってくれるんだけど、そのルーチンが 面白い。まるでgdbのbacktraceコマンドみたい(多分)。

// Record the current call stack in pcs[] by following the call chain.
// In ARM ABI, the function prologue is as:
//              push    {fp, lr}
//              add             fp, sp, #4
// so, fp points to lr, the return address
void getcallerpcs (void * v, uint pcs[]) {
    uint *fp;
    int i;

    fp = (uint*) v;

    for (i = 0; i < N_CALLSTK; i++) {
        if ((fp == 0) || (fp < (uint*) KERNBASE) || (fp == (uint*) 0xffffffff)) {
            break;
        }

        fp = fp - 1;            // points fp to the saved fp
        pcs[i] = fp[1];         // saved lr
        fp = (uint*) fp[0];     // saved fp
    }
     :

このルーチンには、現在のフレームポインターが引数として渡ってくるんだけど、 どうやって取り出しているかと思ったら、asm.S内にひっそりと置かれていた。

# return the frame pointer for the current function
get_fp:
    MOV     r0, fp
    bx      lr

get_fp()って事で、呼び出されるサブルーチンなんだけど、実質は、返り値用のレジスター r0に fpの値を代入してるだけ。こういうのは、きっとC語の中にまぜこぜ出来ちゃうはず。

インライン アセンブル

前回やったasm構文、引数が大文字で始まっていたものだからてっきりマクロかと思っていた んだけど、いろいろ調べてみると、そうではなかった。C語にアセンブラーを埋め込むいわゆる、 インライン・センブリらしい。この際だと思って調べてみた。

inline assmble

GCC Inline Assembler

GCCでインラインアセンブリを使用 する方法と留意点等 for x86

ARMな石に特化した説明としては、 ARM GCC Inline Assembler Cookbook が、良く参照されてるみたい。論文でもそうだけど、良く参照されるのは優れた文献。 リンクが多数なされているのは佳きWeb。ググル様はこれを定式化して大儲けしたけど、 なんの事は無い。学術的な方法をインターネットに応用しただけじゃん。これで特許に なるんですかね。と、凡人が申しております。

最近、ググルの社提で有名だった、「Don't Be Evil」(悪をなすな)が、最近発足したググルの 持ち株会社の規範から消えたらしい。これはぐぐる帝国の邁進を意味しますよ。何か検索 する度に、何を考えているか筒抜けになる。そしてそれを金儲けに利用する。 裏に控えるは、ぐぐるXが持ってる膨大なコンピュータ資源。人口知能で何でも、おみとうし。 その上前をはねるのは、本当の帝国。そう、田舎単位を使って当たり前と思ってる国。 英語の席巻で、インターネットの世界を我が物にしたあの国ですな。

いかん、いかん、つい横道に逸れてしまった。gccに毒されないように、別の道も探っておこう。 そう、 llvm界隈では、どうなっているか? 余り資料は見当たらないけど、逃げ道って事で。。。

差を取って儲ける

これ、世間の常識。安い時に買って、高い時に売る。株屋さんじゃ無くても知ってる。 安いコスト(利息)で、資金調達、高い金利で金を貸す。おまけに、手数料でがっぽりが銀行。

差は大事だよーーて所で、 fedoraで、xv6-armv7が動かないのを追い駆けてみます。こういう場合は、ちゃんと動いて いる系と比べてみるのが手っ取り早い。そう、diffを取るのね。

まずは正常系なウブの入り口と出口

   |0xc0025a78 <forkret>    push   {r11, lr}                                   |
   |0xc0025a7c <forkret+4>  add    r11, sp, #4                                 |
B+>|0xc0025a80 <forkret+8>  movw   r0, #54800      ; 0xd610                    |
   |0xc0025a84 <forkret+12> movt   r0, #49162      ; 0xc00a                    |
   |0xc0025a88 <forkret+16> bl     0xc0025e58 <release>                        |
        :
   |0xc0025ab0 <forkret+56> bl     0xc0024204 <initlog>                        |
  >|0xc0025ab4 <forkret+60> pop    {r11, pc}                                   |

そして、fedora

   |0xc0025e5c <forkret>    str    r11, [sp, #-8]!                             |
   |0xc0025e60 <forkret+4>  str    lr, [sp, #4]                                |
   |0xc0025e64 <forkret+8>  add    r11, sp, #4                                 |
B+>|0xc0025e68 <forkret+12> movw   r0, #58696      ; 0xe548                    |
   |0xc0025e6c <forkret+16> movt   r0, #49162      ; 0xc00a                    |
   |0xc0025e70 <forkret+20> bl     0xc002629c <release>                        |
        :
   |0xc0025e98 <forkret+60> bl     0xc00244f0 <initlog>                        |
  >|0xc0025e9c <forkret+64> nop                    ; (mov r0, r0)              |
   |0xc0025ea0 <forkret+68> sub    sp, r11, #4                                 |
   |0xc0025ea4 <forkret+72> ldr    r11, [sp]                                   |
   |0xc0025ea8 <forkret+76> add    sp, sp, #4                                  |
   |0xc0025eac <forkret+80> pop    {pc}            ; (ldr pc, [sp], #4)        |

見た通りウブの方は非常に素直。入り口でr11(fp)とlr(サブルーチンの戻り先)をpushし、 fpの値を調整してる。出口も素直に、pushした値をpop(lrのpop先をpcに向けるってのは、 ARMの定番技法)して抜けてる。

それに対して、異常なfedoraの方は、異常に複雑な事をやって、墓穴を掘っている。

で、案の定、forkretを抜けた時に、何処へ飛んで行くか追ってみると、正常なウブは、

  >|0xc00275a8 <trapret>            ldm    sp, {sp, lr}^                       |
   |0xc00275ac <trapret+4>          add    sp, sp, #8                          |
   |0xc00275b0 <trapret+8>          ldmfd  sp!, {lr}                           |
   |0xc00275b4 <trapret+12>         ldmfd  sp!, {r2}                           |
   |0xc00275b8 <trapret+16>         msr    SPSR_fsxc, r2                       |
   |0xc00275bc <trapret+20>         ldm    sp!, {r0, r1, r2, r3, r4, r5, r6, r7|

異常なfedoraは、

  >|0xc0025e60 <forkret+4>  str    lr, [sp, #4]                                |
   |0xc0025e64 <forkret+8>  add    r11, sp, #4                                 |
B+ |0xc0025e68 <forkret+12> movw   r0, #58696      ; 0xe548                    |

forkretに舞い戻りしてますよ。

正常な方は、trapretで何処に戻るかと言うと、usr/init.cのmainに戻る。随分端折った 言い方だな。userinit()中で、initcodeがロードされる。スケジューラーがそれを検知と 言うか、実行可として検知。実行。それは、ユーザーランドのinit.cを実行する コードだ。

まだ、端折っているな。initcodeの中では、システムコールexecが 実行(ユーザーランドのinit.cの起動)されるのだ。システムコールの帰結として、trap_swiが 実行されてるので、それに対応するtrapretが巡って来たって訳。

ああ、userinit()の中では、初めて起動するユーザー側のプロセスが、ちゃんと動けるように 下記のトラップフレームをセットアップしてるね。

(gdb) p/x *p->tf
$1 = {
  sp_usr = 0x1000,        // user stack size?
  lr_usr = 0x0,
  r14_svc = 0xc002510c,   // lr for error_init
  spsr = 0x60000050,      // IRQ=enable & user-mode
  r0 = 0x0,
  r1 = 0x0,
  r2 = 0x0,
  r3 = 0x0,
  r4 = 0x0,
  r5 = 0x0,
  r6 = 0x0,
  r7 = 0x0,
  r8 = 0x0,
  r9 = 0x0,
  r10 = 0x0,
  r11 = 0x0,
  r12 = 0x0,
  pc = 0x0
}

修正するぞ

万策尽きた。だってarm用のgccが変なコードを吐くんだもん。まてまて、こういう時はgccの 挙動を変えてミレ。どうやって?

makefile.incを覗いたら、コンパイルの仕方が、-g 付きになってた。これに-Oを付けて、 吐き出すコードを変えてみればよい。で、-O1 から始めて-O2 ,,,やってみたら、-Os の時に、ちゃんと動き出した。 まあ、その前に一波乱があったんだけどね。

それは、exec.cのコンパイル中、pgdirって変数が初期化されていない状態で使われてるって エラーになっちゃうの。コードを見ると、エラーを検出してbadへ飛んできた所で使ってた。 正常ルートの時は、ちゃんと初期化してる。んな訳で、初期化を関数の最初でやってあげたら、 ちゃんとコンパイル出来た。

オプチマイザーレベルを変えると、エラーの検出度も変わるのね。たまにやってみると、隠れた BUGのあぶり出しに有効かも。

で、動いたって言って喜んでいては、血肉にならないので、どんな風にgccがコードを 吐いたか、眺めておく。以下、kernel.asmからの抜粋。

void forkret(void){
c002286c:       e92d4010        push    {r4, lr}
    static int first = 1;
    // Still holding ptable.lock from scheduler.
    release(&ptable.lock);
c0022870:       e59f0020        ldr     r0, [pc, #32]   ; c0022898 <forkret+0x2c>
c0022874:       eb0001ce        bl      c0022fb4 <release>
    if (first) {
c0022878:       e59f301c        ldr     r3, [pc, #28]   ; c002289c <forkret+0x30>
c002287c:       e5932004        ldr     r2, [r3, #4]
c0022880:       e3520000        cmp     r2, #0
c0022884:       08bd8010        popeq   {r4, pc}
        first = 0;
c0022888:       e3a02000        mov     r2, #0
c002288c:       e5832004        str     r2, [r3, #4]
        initlog();
    }
    // Return to "caller", actually trapret (see allocproc).
}
c0022890:       e8bd4010        pop     {r4, lr}
    if (first) {
        // Some initialization functions must be run in the context
        // of a regular process (e.g., they call sleep), and thus cannot
        // be run from main().
        first = 0;
        initlog();
c0022894:       eafffe1f        b       c0022118 <initlog>
c0022898:       c00a94c4        .word   0xc00a94c4
c002289c:       c0026000        .word   0xc0026000

ちら見、入り口と出口がウブ風になったな。こうなったらか、ちゃんと正しい所に 戻れて、動くようになったんか。

冒頭にある、static int first = 1; のコード相当が、消えてる。こんな事していいのか? >gcc。 よく考えたら、永遠に残る変数でっせ(static)宣言がある変数なんで、関数外で初期値を 設定しておける。だから、コード上からは消えた。

そして、if(first)が、末尾最適化(もどき)されて、この関数の出口方向へ行くのが先に来、initlog の関数呼び出しが、ジャンプ命令に置き換えられている。gcc賢いな。本当か? 末尾呼び出し最適化 (Tail Call Optimization)

forkretは何処から呼ばれる?

それは、allocproc()で、procの雛形を作った時に、セットされてた。

    // skip the push {fp, lr} instruction in the prologue of forkret.
    // This is different from x86, in which the harderware pushes return
    // address before executing the callee. In ARM, return address is
    // loaded into the lr register, and push to the stack by the callee
    // (if and when necessary). We need to skip that instruction and let
    // it use our implementation.
    p->context->lr = (uint)forkret+4;

注意書きにあるように、関数の入り口部(プロローグ)は飛ばして設定してる。これは、 ウブなgcc用だ。fedoraの場合は、もう一命令先を帰りアドレスに設定すればいいんで ないかい。やってみた。動いたぞ。

gccと言えども、同じコードを吐くわけではないのね。こういう違いはどこで発生するの? gccのバージョン違いかな?

[sakae@fedora xv6-armv7]$ arm-linux-gnu-gcc -v
組み込み spec を使用しています。
COLLECT_GCC=arm-linux-gnu-gcc
  :
スレッドモデル: single
gcc バージョン 5.1.1 20150618 (Red Hat Cross 5.1.1-3) (GCC)
sakae@uB:~/xv6-armv7/src$ arm-linux-gnueabi-gcc -v
Using built-in specs.
COLLECT_GCC=arm-linux-gnueabi-gcc
 :
Thread model: posix
gcc version 4.7.4 (Ubuntu/Linaro 4.7.4-2ubuntu1)

exec

ここで、最初のユーザーランドプロセスであるinitの起動を見ておく。initcodeから、 execがシステムコールされるはず。

(gdb) bt
#0  exec (path=0x1c "/init", argv=0xc7fdef08) at exec.c:27
#1  0xc002715c in sys_exec () at sysfile.c:473
#2  0xc00261cc in syscall () at syscall.c:154
#3  0xc0027724 in swi_handler (r=0xc7fdefb8) at trap.c:12
#4  0xc00275a8 in trap_swi ()
#5  0x00000010 in ?? ()

まず、お目当てのプログラム名から、nameiを使って、iノード番号を割り出し、それを 元に、elfのヘッダーを読み出す。マジックをみて、elfファイルである事を確認後、

 42    pgdir = 0;
 43
 44=>  if ((pgdir = kpt_alloc()) == 0) {
 45        goto bad;
 46    }
 47
 48    // Load program into memory.
 49    sz = 0;
 50
 51    for (i = 0, off = elf.phoff; i < elf.phnum; i++, off += sizeof(ph)) {
 52        if (readi(ip, (char*) &ph, off, sizeof(ph)) != sizeof(ph)) {
 53            goto bad;
 54        }
  :
 64        if ((sz = allocuvm(pgdir, sz, ph.vaddr + ph.memsz)) == 0) {
 65            goto bad;
 66        }
 67
 68        if (loaduvm(pgdir, (char*) ph.vaddr, ip, ph.off, ph.filesz) < 0) {
 69            goto bad;
 70        }
 71    }

メモリーページを用意。そして読み込み。続いて、

 76    // Allocate two pages at the next page boundary.
 77    // Make the first inaccessible.  Use the second as the user stack.
 78    sz = align_up (sz, PTE_SZ);
 79
 80    if ((sz = allocuvm(pgdir, sz, sz + 2 * PTE_SZ)) == 0) {
 81        goto bad;
 82    }
 83
 84    clearpteu(pgdir, (char*) (sz - 2 * PTE_SZ));
 85
 86    sp = sz;

BSS領域? と、スタック領域を用意。次は、argc、argv関係をstack上に用意。それが、main()の 引数になるように、proc内の変数領域にセット。(r0,r1)

最後の最後に、

124    // Commit to the user image.
125    oldpgdir = proc->pgdir;
126    proc->pgdir = pgdir;
127    proc->sz = sz;
128    proc->tf->pc = elf.entry;
129    proc->tf->sp_usr = sp;
130
131    switchuvm(proc);
132    freevm(oldpgdir);
133    return 0;

proc内の変数をセットして終了。なお、switchvvmは、

161// Switch to the user page table (TTBR0)
162void switchuvm (struct proc *p)
163{
164    uint val;
165
166=>  pushcli();
167
168    if (p->pgdir == 0) {
169        panic("switchuvm: no pgdir");
170    }
171
172    val = (uint) V2P(p->pgdir) | 0x00;
173
174    asm("MCR p15, 0, %[v], c2, c0, 0": :[v]"r" (val):);
175    flush_tlb();
176
177    popcli();
178}
179

ユーザー用のページテーブルに切り替えてるようです。(まだ、追いきれていない)

こうして、initが起動すると、そこからユーザーとのインターフェースになるshellが 起こされる事になる。

祝20周年 OpenBSD

年に2回、淡々と版を重ねるOpenBSDだが、今回は特に目出度いリリース。

20周年を迎えたOpenBSD、「OpenBSD 5.8」を公開

10月に入った所で、一部の成果物が上がっていた事は知っていたけど、肝心の本体が 未公開。親分のTheo de Raadt氏をめぐり内紛でも有ったのかと心配してたけど、 10月18日の佳き日を待ってたって訳ね。納得。

sudoコマンドがdoasコマンドに置き換えられたそうだけど、いつ試してみるかなあ。