Linux 0.01をUbuntu 10.10のQEMUで動かす
「Linux 0.01の設計と実装」という記事に触発されてLinux 0.01をUbuntu 10.10上のQEMUで動かしてみた。で、gcc 4.4.5ではコンパイルできないことがわかり、gdbでリモートデバッグした。(今回はPlan 9の「ぷ」の時も出てこないのであしからず。)
オリジナルの0.01は1991年にgcc 1.4で開発されていたと言うこともあり、今時のgccではコンパイルすら通らない。ということでAdbel Benamroucheの0.01-rmを使って楽をすることにした。0.01の雰囲気をてっとり早く知るには、生越さんの「Linusになろう! -- 君にも書けるOS kernel」がおすすめ。0.01を読むのは初めてだが、0.12あたりをbochsで動くようにして遊んでいたころの記憶がよみがえる。
0.01-rmを動かす方法は、冒頭で紹介したblogに詳しいので、ここでは(ubuntu 10.10の)gcc 4.4.5でコンパイルする場合の問題にしぼって述べる。
(問題1)
カーネルを展開しておもむろにmakeすると、インラインアセンブリのエラーに遭遇する。
$ make : ../include/asm/segment.h: Assembler messages: ../include/asm/segment.h:27: Error: bad register name `%sil'
問題のコードは次のようになっている。%silはx86_64で登場したレジスタだが、x86では当然そんなレジスタはないと怒られる。つまりレジスタの自動割当てがまずいので、制約を"r"から"q"に変更する。
__asm__ ("movb %0,%%fs:%1"::"r" (val),"m" (*addr));
(問題2)
makeがリンクまで進むが、シンボルの未解決でエラーになる。
ld -s -x -M -Ttext 0 -e startup_32 boot/head.o init/main.o \ kernel/kernel.o mm/mm.o fs/fs.o \ lib/lib.a \ -o tools/system > System.map kernel/kernel.o: In function `scrup': console.c:(.text+0x1e94): undefined reference to `columns' console.c:(.text+0x1f5c): undefined reference to `columns' kernel/kernel.o: In function `scrdown': console.c:(.text+0x1fb4): undefined reference to `columns'
コードを見てみると、columnsはstatic変数なのだが、どうやらgcc 4.1から4.4の間でインラインアセンブリから参照できなくなってしまったようだ。
__asm__ __volatile("cld\n\t" "rep\n\t" "movsl\n\t" "movl columns,%1\n\t" "rep\n\t" "stosw" :"=&a" (d0), "=&c" (d1), "=&D" (d2), "=&S" (d3) :"0" (0x0720), "1" ((lines-1)*columns>>1), "2" (SCREEN_START), "3" (origin) :"memory");
columnsをstaticじゃなくせばいいのだが、次のように直すこともできる。
__asm__ __volatile("cld\n\t" "rep\n\t" "movsl\n\t" "movl %[columns],%1\n\t" "rep\n\t" "stosw" :"=&a" (d0), "=&c" (d1), "=&D" (d2), "=&S" (d3) :"0" (0x0720), "1" ((lines-1)*columns>>1), "2" (SCREEN_START), "3" (origin), [columns] "r" (columns) :"memory");
これでカーネルがビルドできた!と喜んだのもつかの間、QEMUを起動してみるとシェルが立ち上がってこない。Ctrl+Alt+2を押してモニタに入ってEIPを見てみると、スケジューラの中でぐるぐるループしているようだ。。。
ということで、gdbを使ってリモートデバッグしてみる。そのためにデバッグオプション付きでカーネルを再コンパイルする。QEMUの起動時オプションに"-s -S"を指定すると、gdb stubが1234番ポートで待機し、かつ実行停止した状態で起動される。この組合せは便利だよね。詳細はこちらを参照。
$ gdb (gdb) target remote localhost:1234 (gdb) symbol-file tools/system (gdb)
しばらくステップ実行して様子見していると、だんだん問題が明らかになってきた。fork処理の際、copy_process関数でtask_struct構造体をカレントプロセス(親)から新規プロセス(子)へコピーし、各フィールドを更新するのだが、どうも子プロセスのtask_structの値がおかしいのだ。このコピー処理は次の何の変哲もない一行である。
*p = *current; /* NOTE! this doesn't copy the supervisor stack */
これでコピーが失敗しているので、memcpy(p, current, sizeof(struct task_struct))で置き換えてみると、何の問題もなく動いてしまった。
ん〜、ということでgcc 4.1.3と4.4.5が吐いたアセンブリを比較してみた。
gcc version 4.4.5 (Ubuntu/Linaro 4.4.4-14ubuntu5) .loc 1 74 0 movl current, %esi movl $141, %ecx movl %ebx, %edi .LVL27: rep movsl gcc version 4.1.3 20080704 (prerelease) (Ubuntu 4.1.2-29ubuntu1) .loc 1 74 0 movl $564, 8(%esp) movl current, %eax movl %eax, 4(%esp) movl %ebx, (%esp) call memcpy
4.1.3ではmemcpyを使い、4.4.5ではストリング命令を使っている。ぱっと見、間違っているようには見えないんだけど何がまずいのだろうか?
(追記)@_enamiさんにDFフラグの問題ではと指摘され、コピーの前にcld命令を追加したところ、正常に動作した。どこでDFフラグが立ったのかと調べてみたら、直前に読んでいるget_free_page関数だった。ということでこの関数を抜ける前にcld命令を実行することにした。
unsigned long get_free_page(void) { register unsigned long __res; __asm__ __volatile__("std ; repne ; scasw\n\t" "jne 1f\n\t" "movw $1,2(%%edi)\n\t" "sall $12,%%ecx\n\t" "movl %%ecx,%%edx\n\t" "addl %2,%%edx\n\t" "movl $1024,%%ecx\n\t" "leal 4092(%%edx),%%edi\n\t" "rep ; stosl\n\t" "movl %%edx,%%eax\n" "1:\n\t" "cld" :"=a" (__res) :"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES), "D" (mem_map+PAGING_PAGES-1) :"dx"); return __res; }
gccは関数のプロローグにはcldを挿入するが*1、今回のケースのように途中でstd命令をインラインアセンブリで入れられてしまうと、DFフラグのクリアができない。プログラマがちゃんとケアする必要があると言うことで、これはLinuxのバグじゃないかな。
とりあえずここまでのパッチをgithubgithubに置いた。物好きな人はどうぞ。
本当はMinix 3.1.8環境があるし、Linusになった気分でMinix上でLinux 0.01をクロスコンパイル!と思ったが、MinixのgccはELFじゃないし、ツールチェインをコンパイルするのが面倒なのであきらめた。気が向いたらやってみようかなぁ。
*1:-mcld、-mno-cldで制御可能。