2.4 locore. s の関数と用語 (i386/machdep. c の init386 ( ) を参照 ) 。 %eax / * ページの残りを計算 * / [ Fi1e : /usr/src/kerne1/kern/i386/10core . s , line : 587 ] 2 : 3 : movl andl movl subl cmpl movl subl movl shrl cld; movl andl rep ; cmpl Jne %edi , $NBPG , %eax , 3f %ebx , 0/oecx , %ecx , 0/oecx $NBPG-I , %eax %edx %ebx 0/oecx %ebx 0/oecx lb $ 効 , %ebx movsb $ 3 , %ecx %edx, %ecx rep ; movsl $ 2 , %ecx / * ページ内の断片を移動 * / / * まだ残りがあるか ? * / 断片が転送されるサイズは、ページの残りの量に制限されるので、大量のデータを転送する場合、こ の関数はページのプロテクションの条件が違っていないか、各ページの境界でチェックを行いながら転 送を繰り返す。次に、その断片において可能な限り多くのワードを転送し ( m 。 vsl 命令によって 4 バイ トごとの転送を行う ) 、余りが出たら movsb 命令によって 1 バイトずつ転送する。 まだデータが残っていたら、最初に戻って、次のページアドレスのページテープルエントリを見てプ ロテクションをチェックし、処理を繰り返す。 [ Fi1e : /usr/src/kerne1/kern/i386/10core . s , line : 6 の 7 ] 9 : popl popl popl XO て 1 movl movl ret popl popl popl movl xorl movl %ebx %edi %eax, %eax 4(%esp) , %edx %eax , PMD-ONFAULT (%edx) %ebx %edi 0/oesi / * フォールト検出、エラーを返す * / 4(%esp) , %edx %eax , %eax %eax , PMD-ONFAULT (%edx) 67
第 2 章アセンブリ言語によるエントリとプリミティブ ( i386/locore. s) ◆ 2.4.7.1 ssdtosd の実装 [ Fi1e : /usr/src/kerne1/kern/i386/10core . s , ENTRY (ssdtosd) line : 518 ] pushl movl movl shll movl r011 movb movb movl movw andl 0r1 movl movl movl popl ret % ebx 8(%esp) , %ecx 8(%ecx) , %ebx $ 16 , %ebx %edx $ 16 , %edx %dh , %bl %dl , %bh $ 8 , %ebx 4(%ecx) , %eax %ax , %dx $ の x 朝ののの , %eax %eax , %ebx 12(%esp) , %ecx %edx, (%ecx) %ebx, 4(%ecx) %ebx 286 形式のディスクリプタとの互換性を守るため、 386 以降のプロセッサは、ディスクリプタの内容を 再定義しないで、この古い形式のディスクリプタの最後にフィールドを追加することによって拡張して いる。その結果、ビットフィールドが複雑に交錯していて、ソフトウェアで使いやすい形式にはなって いない。初期化中の統一性を保っために、これらのフィールドはソフトウェア形式で作られ、この関数 で使う前に変換する。 この関数はローテーション命令の組み合わせによって、 286 形式に追加された " 拡張 " ワードフィール ドを整列させる。 386BSD は、ソフトウェアでは " 合理的な " ディスクリプタ形式で統一し、使用前に変 換する ( 使用例は、 i386/machdep ・ c にある )。 この関数は C 言語でも書けるが、それでも複雑さに変わりはない。つまり、 C で書いても読みやすさ や効率が向上するわけではない。 2.4.8 copyout とは何か copyout は、カーネルプログラムからユーザープロセスにデータの断片を渡すのに使用される、カー ネル関数である。この関数は 386 専用で、 486 では、より高速なインラインアセンプル版が使われる。さ らにインライン版では、 386 のカーネルモードにおけるライトプロテクトの欠如に甘んじる必要もない。 copyout は、データの断片の内容をカーネルからユーザー空間に転送する前に、その断片が書かれる ューザーアドレス空間が存在し、かっ書き込み可能であることを確認する。この処理が不可能だと判断 したら、この関数はエラー ( EFAULT ) を返す。そうでなければ 0 を返して、転送の実行中にエラーが 64
2.4 locore. s の関数と用語 qswtch は、実行中のプロセスのほかにも、自己の再スケジュールを要求しているプロセスが 1 つだ け存在しているケースを対象に最適化されているが、ほかに実行すべきプロセスが存在しなければ、自 分自身を " 実行 " しなければならない。 ◆ 2.4.12.1 qswtch の実装 [ Fi1e : /usr/src/kerne1/kern/i386/10core . s , ・ 925 ] line . ENTRY ( qswt ch ) PRO C ENTRY incl xorl xchgl pu sh1 pu shl pu shl cli _cnt + V_SWTCH / * " 古い " プロセスを記憶しつつ、現在のプロセスに不在のマークを付ける * / %eax, %eax %eax, _curproc %eax %ebx 0/oesi swtc 五と同じく、 qswtch もコンテクスト切替のカウントアップを行い、カレントプロセス ( cu て p て oc [ Fi1e : /usr/src/kerne1/kern/i386/10core . s , line : 944 ] このコードは、できるだけ swt ch に似せて書かれている。 クに保存し、この要求の処理に使われる 2 つのレジスタの内容も保存する。次に割り込みを禁止する。 をクリアして、現在のところ実行中のプロセスが存在しないことを示す。前のプロセスポインタをスタッ andl movzbl shrl btsl sh11 addl movl movl movl movl movl JIIIP $&MDP_RESCHED , PMD_FLAGS (%eax) P-PR 工 (%eax) , %edx $ 2 , %edx %edx , whichqs $ 3 , %edx $-qs , %edx %edx, P-L 工 NK(%eax) P_RLINK(%edx) , %ecx %ecx , P-RLINK(%eax) %eax, P-RLINK(%edx) %eax , P-L 工 NK(%ecx) swtch—selq / * キューの full ビットをセット * / / * キューヘッダを探す * / / * キューの末尾にプロセスをリンク * / このプロセスに対して、もう再スケジューリング要求が行われないことを示すために、マシン依存の プロセス定義の中の、再スケジューリングおよびシグナルのフラグをクリアする。 BerleleyUNIX の初 期のバージョンと異なり、再スケジューリングとシグナルの処理は、スレッド / プロセスのコンテクス 81
第 2 章アセンブリ言語によるエントリとプリミティブ ( i386/Iocore. s) を調べて、そのアドレス空間が書き込み可能であることを確認する。 [ Fi1e : /usr/src/kerne1/kern/i386/10core . s , line : 792 ] s ahf Jne 7f / * バッファ容量不足 ? * / こで、さらに別のチェックをおこなう。これは、文字列の最後を示すヌルバイトに遭遇する前に、転 送先の文字列バッフアの空間がなくなってしまったかどうかのテストである。その場合、このルーチン が終了する 3 つめの理由となる。 4 : 8 : [ Fi1e : /usr/src/kerne1/kern/i386/10core . s , line : 795 ] / * ヌルを見つけた場合、正常終了 * / %eax, %eax / * 処理の終了 * / popl popl popl movl movl %ebx %edi o,/oesi 4(%esp) , %edx $ の , PMD-ONFAULT (%edx) / * 転送カウンタを返す必要があるか ? * / movl cmpl subl movl movl ret 2Ø(%esp) , %edx $ の , %edx If %ecx, 16(%esp) 16(%esp) , %ecx %ecx, (%edx) / * lencopied * / / * Size * / 最後のヌルまで文字列の転送が正しくおこなわれた通常の場合、戻り値 0 によって成功を示す。レジ スタをリストアし、フォールトべクタをクリアする。 転送カウンタが要求されていた場合 * 訳注 2 は、処理されなかった部分と要求されなかった部分との差 によって計算される。そうでなければ、転送カウントに関しては何も返さない。 [ Fi1e:/usr/src/kerne1/kern/i386/10core. s, line: 815 ] 7 : 9 : 76 * 訳注 2 movl Jmp movl $ENAMETOOLONG, %eax / * ューザープロセスの容量不足 * / 8b $EFAULT , %eax / * フォールト検出によるエラー * / le Ⅱ copied が 0 ではないとき。
2.4 locore. s の関数と用語 [ FiIe : /usr/src/kerne1/kern/i386/10core . s, line : 26 の ] movl shrl movl call / * カ movl subl s て 1 addl movl ca11 %edi , %ecx $PGSHIFT, %ecx $PG_VI PG_KR , %edx %eax, %eax rfillkpt / * 使用する仮想アドレス空間の量 * / / * read-only ( 486 用 ) * / / * このアドレス ( 0 ) から開始 * / ーネルのデータベージ -data, bss, 初期状態を含む * / %esi, o,/oecx %edi , %ecx $PGSHIFT , %ecx $UPAGES + 土 + NSYSMAP , $PG_VIPG_KW , %edx rfillkpt / * 残りのデータベージ * / %ecx / * 初期状態も含める * / / * read—write * / / * I/O メモリをマップする * / movl ca11 movl movl movl movl ca11 $—atdevphys—KERNBASE , reloc %ebx, (%eax) $ の xl の一の xa %ecx $PG_V ー PG_KW , %edx $ の xa ののの , %eax f i 1 lkpt %eax / * 相対 pte べースを記録 * / / * 隙間にマップされるべージ * / / * read—write * / / * 隙間のべースアドレスから開始 * / 次にページテープルそのものを、カーネルの text ページを先頭に構築する。カーネルプログラムその ものが text と data の 2 つにわかれているので、カーネル text を読み出し専用にするために、そのアド レス空間に対応するべージテープルのエントリを read ー only に設定する。 386 はカーネルモードベージ テープルのライトプロテクトビットを認識しないので、これが影響を与えるのは 486 以降のプロセッサ だけである。 カーネルの text ページが占有する空間は、アドレス 0 から edi レジスタの内容までの範囲である。 れがカウンタレジスタ ( ecx) に移され、左シフトによってページ数になる。カーネルページテープルに 適切な数のページテープルエントリを記入するために、 rfillkpt サプルーチンを呼び出す。ページテー プルエントリの属性ビットには edx レジスタの値、 0 から始まる物理アドレスには eax レジスタの値が 設定される。 rfillkpt は、 PTE(page table entry) 自身のアドレスと、そこに割り当てられている物理ページの ための論理アドレス変換を行うことによって、一群のページテープルの記入を行う。なぜなら、カーネ ルの text セグメントの中で参照されるアドレスは、このメモリの中の隙間を通過するだけでなく、実際 のページテープルそのものを通過する可能性があるからだ ( ページテープルが隙間で二分されている可 能性がある ) 。隙間はカーネルがロードされるマシンのそれぞれで異なるので、この隙間がどこにでき るかを知る方法がないし、カーネルがどこで隙間に重なるかも、カーネルの大きさによって異なる。 引き続いてカーネルの data ページも同様に初期化するが、 text のように属性に read-only を設定す
第 2 章アセンブリ言語によるエントリとプリミティブ ( i386/locore. s) [ Fi1e : /usr/src/kerne1/kern/i386/10core. s , line : 851 ] 32 個の実行キューすべてをチェックして、プロセスを含んでいるもの ( fu Ⅱキュー ) がないか探す ( も / * 空でない ( fu Ⅱ ) キューを探す * / なければ、 idle * / キューの full ステータスをクリア * / / * すでにクリアされていたら別のを探す * / / * swtch() が終わるまで割り込み禁止 * / キューヘッド index を保存 * / / * 対応するキューを選択 * / キューヘッドボインタを保存 * / [ FiIe : /usr/src/kerne1/kern/i386/Iocore . s , line : 87 ] キューの先頭 ( キューヘッダ ) を探して、それをレジスタに保存する。 ( おもにクロック ) による実行キューの奪い合いを防止する。次に、クリアしたキュービットに対応する 調べたキューが現在は空になっていることは確実なので、こで割り込みを禁止して、他の割り込み を探す。 いまキューから抜き出されたところなのだ。この場合はラベル s ch ー selq に戻って、次の fu Ⅱキュー スは、システムの別の部分 ( たとえば schedcpu ( ) : 第 8 章の kern/synch. c 参照 ) によって、たった クリアする。しかし、それ以前にクリアされていたら、一種の競争的な状態にある。つまり、そのプロセ キューを見つけたら、その最後のエントリを取り出す可能性があるので、キューの fu Ⅱステータスを ラベル swtch-selq から再入する。 は実行可能なプロセスが出現するまでプロセッサは何もしないで待ち続ける。プロセスに戻るときは、 もしⅶ ichqs 変数のビットがまったくセットされていなければ、 idle ルーチンに分岐する。 中で最も高い優先順位のものを探すのに、ビットスキャン命令を使っている。 しキューに何かが入っていれば、この変数のビットが 1 つ真になっている ) 。ビットがセットされている swtch—selq : bsfl btrl Jnc cli movl shll addl movl %eax , —whichqs swtch—selq %eax, %ebx $ 3 , %eax $-qs , %eax %eax, %esi —whichqs , %eax ldle movl movl movl movl movl P-L 工 NK(%eax) , %ecx P-LINK(%ecx) , %edx %edx, P-LINK(%eax) P-RLINK(%ecx) , %eax %eax, P-RLINK(%edx) 次に、このキューに並んでいる最初のプロセスをキューから外す。それには、キューヘッダに繋がっ ている双方向連結リストからプロセスを外し、そのプロセスの次に並んでいたプロセスを、そのキュー ヘッドに繋がる最初のプロセスに変える。このようにして、プロセスは厳密な FIFO 形式によって実行 キューから消費される。もしこの実行キューにエントリが 1 つしかなかったら、そのキューヘッダは自 78
第 2 章 アセンブリ言語によるエントリとプリミティブ ( i386/locore. s) ca11 popl movl testl Jne movl movl movw movl ret —trapwrite %edx —curproc , %ecx %eax , %eax copyout-faultl 8(%esp) , %edx 4(%esp) , %eax %ax, (%edx) %eax, %eax / * trapwrite(addr) * / / * 壊れたレジスタをリストア * / / * OK でなければ、リターンする * / %eax, PMD-ONFAULT(%ecx) アドレスに対応する PTE を調べて、保護されている有効なページであるかどうかチェックする。この 場合、ライトフォールトは trapwrite ( ) によってシミュレートされる。もし工ラーがあれば、フォー ルトルーチンが直接呼び出され、この転送は中止される。アドレスを示す引数の値をスタックから取り 出し、ユーザープロセスの中のハーフワードを書き換える。このときメモリマップが有効になっていな ければ、透過的なページフォールトが起きるだろう。 書き換えを終えたあとは、フォールトべクタをクリアし、成功を示す 0 を返す。上記の明示的なフォー ルトや、暗黙に行われる透過的なフォールトが発生した場合は、そのフォールト機構によって関数の処 理が中止され、戻り値 EFAULT が返される。 ◆ 2.4.9.3 copyout ー 1 の実装 [ Fi1e : /usr/src/kerne1/kern/i386/10core. s , ENTER (copyout-l) line : movl movl movl cmpl _curproc ,%ecx $copyout-faultl , 8(%esp) ,%edx $-PTmap , %edx copyout faultl PMD_ONFAULT (%ecx) 698 ] / * フォールトに備えて * / / * ユーザープロセスの外か * / カレントプロセスのフォールトべクタは、これらのルーチンが共用する copyout フォールト処理に設 定される。フォールトが発生し得るのは、バイトを書き換える命令だけだ。それが、この関数の要点であ る。このアドレスが、ユーザーのアドレス空間の中にあるかどうかを判定する。そうでなければフォー ルトルーチンを直接呼び出して、この関数を終了する。 [ Fi1e : /usr/src/kerne1/kern/i386/10core . s , line : 7 の 5 ] shrl andb movl $ 工 DXSHIFT, %edx / * pte アドレスを計算 * / $Øxfc , %dl -PTmap(%edx) , %edx
2.4 locore. s の関数と用語 きれば、早くもページフォールトが発生してしまう。そこで、ある種の printf ( ) の呼び出しのように 現実的に使われるよりも多くの引数領域を先に予約しておく。 [ FiIe : /usr/src/kerneI/kern/i386/10core . s , line : 318 ] movl subl movl subl sh11 addl movl addl movl _Crtat , %eax $ の xfe a 必ののの , %eax -atdevphys , %edx -KPTphys , %edx $PGSHIFT—2 , %edx $KERNBASE , %edx / * pte の物理アドレスを取得 * / / * pte の仮想べースアドレスを引く * / / * 仮想アドレスを保存 * / %edx, %eax, %edx, atdevbase %edx Crtat コンソールのフレームバッフアアドレスとして使うメモリのべースアドレスを、隙間の ( 新しい ) 仮 想アドレスに対応して調節する。これは、コンソール初期化前のマシン初期化ルーチン実行時からでも カーネルの printf() ルーチンを使えるようにというので使われてきた、コンソール初期化処理の遺物 である。これはデバッガに便利なように挿入されている小細工に過ぎず、今後のバージョンではなくな るだろう。 [ FiIe : /usr/src/kerne1/kern/i386/10core . s , line : 339 ] movl movl subl XO て 1 cld rep stosb $—end, %ecx / * カーネルイメージの終点 * / $—edata, %edi %edi , %ecx %eax , %eax / * カーネルの最初のスタックをクリア * / movl movl xorl cld rep stosl $(UPAGES * NBPG)/4, %ecx -procøpaddr , %edi %eax, %eax BSS (BIank Storage Segment) 領域は、 -edata から始まり、 -end で終わるカーネルの未初期化デー タである。この BSS とカーネルの最初のスタックの両方を、準備のために 0 クリアする。ほとんどの PC はメモリを 0 クリアするので、この処理に意味があるのは、メモリが最初から 0 クリアされていな い場合 ( たとえば DOS のようなオペレーティングシステムからプートした場合 ) に限られる。 BSS をク リアしないプートストラップが 386BSD に使われることもあるので、 BSS は私たちのプートストラップ 55
第 2 章アセンブリ言語によるエントリとプリミティブ ( i386 / loco 「 e. s ) るのではなく、 read/write 属性を設定する。これらはカーネル text のあとに続く物理アドレスにアロ ケートされるが、その理由は、 eax レジスタが rfillkpt サプルーチンによって、連続するメモリペー ジおよびページテープルエントリを参照するように更新されているからだ。これらのエントリに対応す るカーネル data 領域には、初期値のあるデータ、初期値がなく 0 クリアされている bss 領域、そして初 期化時のマッピング状態が含まれる。 カーネルの data ページを初期化したあとは、ドライバがアクセスできるように、隙間 * 訳注 1 そのもの をマップする。 atdevphys 変数から ebx レジスタにロードされた開始物理アドレスは、隙間をマップ する仮想記憶領域に割り当てられたカーネルページテープルエントリの相対物理アドレスが入っている。 そこで、隙間を参照するこれらのページテープルエントリを追加するために、 fillkpt 関数が呼び出さ れる。このケースでは、て fillkpt 関数は使えない。これは隙間を避けようとするので、マップしよう としている物理メモリ領域を飛び越えてしまうからだ。 この時点で、カーネルのページテープルはすべて初期化された。これでカーネルページテープルのア ドレスと大きさがわかったので、ページテープルディレクトリを作ることができる。 [ Fi1e : /usr/src/kerne1/kern/i386/10core . s , line : 287 ] / * カ 1ea movl lea lea movl movl movl call call movl movl lea ca11 (UPAGES + 1) *NBPG(%esi) , %eax / * カーネルページテープルの * / / * 論理アドレス * / $PG_VI PG_KW , %edx $ 1 , %ecx %esi , %ebx rfillkpt / * read—write * / / * 工ントリ 1 個を * / / * ページディレクトリの最初に * / ーネルのページテープルのために一群のエントリをインストール * / (UPAGES + 1) *NBPG(%esi) , 0/oeax $NSYSMAP , %ecx SYSPDROFF*4 (%esi) , rf illkpt %esi, %eax $ 1 , %ecx PDRPDROFF*4(%esi) , rfillkpt %ebx / * %ebx / * / * カーネルページテープルの * / 論理アドレス * / ディレクトリエントリ数 * / カーネルプログラム用の * / ページディレクトリへのオフセット * / ページディレクトリを 1 個のページテープル * / として再帰的にマップしながらエントリを * / インストールする * / proc 0 の PTD の論理アドレス * / 工ントリ 1 個を * / カーネルエントリの下に * / ページテープルディレクトリの一番最初のエントリは、メモリの最下位にある 4M バイトを参照する。 このエントリはカーネルページテープルを指し、ディレクトリエントリの属性を書き込み可能 (read write) として初期化する。ページディレクトリの要素を初期化するのに rfillkpt 関数を使用するの * 訳注 1 すなわち I/O メモリのこと。 52
第 2 章アセンブリ言語によるエントリとプリミティブ ( i386 / loco 「 e. s ) [ Fi1e : /usr/src/kerne1/kern/i386/10core . s , line : 228 ] movl ca11 movl movl call movl %esi , %eax reIOC %eax, %edx $-Kerne1PTD—KERNBASE , reloc %edx, (%eax) / * KernelPTD の仮想アドレス * / movl addl movl ca11 movl %esi , %edx $KERNBASE , %edx $_VAKerne1PTD—KERNBASE , て eloc %edx, (%eax) %eax %eax / * プロセス 0 の pcb / スタックの論理アドレス * / lea lea movl ca11 movl NBPG(%esi) , %eax KERNBASE (%eax) , %edx $-procØpaddr ー KERNBASE , %eax %edx, (%eax) て eloc / * KerneIPT の論理アドレス * / lea movl ca11 movl (UPAGES + i) *NBPG(%esi) , $_KPTphys ー KERNBASE , reIOC %ebx, (%eax) %ebx %eax カーネルのページテープルエントリの物理アドレスが、それに対応する Kerne1PTD 変数に格納され る。 VAKerne1PTD には、ページテープルディレクトリの仮想アドレスが入る。これらのアドレスは、検 死 ( postmortem ) デバッグ時にページテープルを解読するのに便利である * 19 。また、プロセス 0 の仮 想空間のアドレスが paddr フィールドに書かれているが、これは pr 。 c の . p ー addr フィールドを介して カーネル初期化ルーチンに渡される。 最後にページテープルの物理アドレスを、変数 KPTphys に記録する。このアドレスを他のアドレスか ら必すしも推測できないのは、べースメモリと拡張メモリの間の隙間を超えて広がっているカーネルの 場合、他の再配置とは違うことがあるからだ。そうなるのは、たとえばページテープルの位置が物理メ モリの開始点 ( 境界 ) に重なっているようなケースだ * 20 。 * 19 * 20 50 これらは物理メモリダンプの中から初期ページテープルディレクトリを探し当てるのに使われる。これに よって、カーネルプログラムやスタック、変数の解析を始める前に、カーネルのアドレス変換情報を取得 できる。ユーザーモードの libutil オプジェクトライプラリと kvn 関数を参照してほしい。 カーネルのその他の部分はべースメモリにあるが、ページテープルが、ちょうど拡張メモリの最初から始 まるとき ( べースメモリを占めるカーネルイメージの終点のすぐあとに配置された場合 ) 。