bobuhiro11's diary

x86エミュレータ開発記録 (3)

19 Aug 2018
[x86]

mpmain()scheduler()swtch()とプロセス切り替えできるまで、 エミュレータの開発が進んだ。 xv6のコードproc.cの334行目sti命令をずっと発行していて、どうやらループに入っているようなので、 ここでスケジューリング、コンテキストスイッチ周りをまとめたい。 切り替え先のプロセスは、切り替えられた直後forkret()iinit()関数内にて、IOを行っている。 ここで、スリープ状態に入り、割り込みを待っているようだ。

proc構造体

xv6では各プロセスはproc構造体で管理される。 contextには、コンテキストスイッチのためのレジスタを格納する。 CSなどセグメントレジスタは、プロセス間で共通なので、保存する必要がない。 EAX、ECS、EDXなどは関数呼び出し時に自動的にスタックに保存されるため、contextに含めない。 最初のユーザプロセスは、userinit関数とallocproc関数で作成する。 proc構造体のコメントには、最初のユーザプロセスのデータをメモしておく。 トラップフレームは、ハードウェアとtrapasm.Sによりスタック上に積まれるもので、 trap関数へと引き渡される。 forkret関数のreturn先は、trapretに設定する。

struct proc {
  uint sz;                     // Size of process memory (bytes)
  pde_t* pgdir;                // Page table
  char *kstack;                // このプロセスのカーネルスタックの底
  enum procstate state;        // プロセスの状態:EMBRYO
  int pid;                     // PID:1
  struct proc *parent;         // 親プロセス:NULL
  struct trapframe *tf;        // トラップフレーム
  struct context *context;     // このプロセスのコンテキスト
  void *chan;                  // If non-zero, sleeping on chan
  int killed;                  // If non-zero, have been killed
  struct file *ofile[NOFILE];  // Open files
  struct inode *cwd;           // カレントディレクトリ:/
  char name[16];               // プロセス名:initcode
};

struct context {
  uint edi;
  uint esi;
  uint ebx;
  uint ebp;
  uint eip; // forkret
};

struct trapframe {
  // registers as pushed by pusha
  uint edi, esi, ebp, oesp, ebx, edx, ecx, eax;

  // rest of trap frame
  ushort gs, padding1, fs, padding2, es, padding3, ds, padding4;
  uint trapno;

  // below here defined by x86 hardware
  uint err;
  uint eip;
  ushort cs;
  ushort padding5;
  uint eflags;

  // below here only when crossing rings, such as from user to kernel
  uint esp;
  ushort ss;
  ushort padding6;
};

struct {
  struct spinlock lock;
  struct proc proc[NPROC];
} ptable;

カーネルスタック

proc->kstackの中身を図示しておく。 swtchでは、espproc->contextとして、edi、esi、ebx、ebpをpopする。 そして、最後にret命令で、eipを復元する。

(lower address)
 ------------------- 
|                   | <= proc->kstack
|                   |
 ------------------- 
 ------------------- 
| edi               | <= proc->context
| esi               |
| ebx               |
| ebp               |
| eip = forkret     |
 ------------------- 
 ------------------- 
|                   | <= trapret(関数ポインタ)
 ------------------- 
 ------------------- 
|                   | <= proc->tf
 ------------------- 
                      <= proc->kstack + KSTACKSIZE(4096)
(upper address)
 

参考


はじめて読む486の読書メモ

04 Aug 2018
[x86]

初めて読む486を読んだので、気になってことをメモしておく。 エミュレータを作っているので、参考になる。 全体を通して、図が分かりやすいので、図を見るだけでも勉強になる。

セグメント

  • EIPから命令を読み出すときにはCSレジスタの指すセグメントが使われ、命令実行時のメモリアクセスにはDSレジスタの指すセグメントが使われる。また、PUSH、POP、CALL、RETなどスタックに関する操作には、SSレジスタの指すセグメントが使われる。スタックは下位アドレス方向に伸びるため、スタックセグメントのリミットは例外的に下限を指定する。
  • すべてのメモリアクセスのたびにセグメントディスクリプタを参照するのは、無駄が多いので、CPU内にセグメントディスクリプタ自体のキャッシュ(セグメントディスクリプタキャッシュ)を持つ。
  • プロテクトモードでは、CSレジスタが16ビットセグメントを指していれば、オペランドサイズとアドレスサイズは16ビットになる。同様に、32ビットセグメントでは、それらのサイズは32bitになる。
  • セグメントごとにDPL(Descriptor Privilege Level)で特権レベルを指定する。現在実行中のセグメントのDPLによって、CPL(Current Privilege Level)が決まる。CPLはCSレジスタの下位2bit、DPLはセグメントディスクリプタで設定される。ジャンプ先セグメントのDPLがCPLより高い場合には、直接ジャンプすることはできず、コールゲートを経由する必要がある。

ディスクリプタ

  • GDTには、セグメントディスクリプタのほかに、TSS、コールゲート、タスクゲート、トラップゲート、割り込みゲートなど複数のシステムオブジェクトが格納される。
  • 各種ゲートディスクリプタにもDPLを設定しておき、ゲートを呼び出すときに、CPLとDPLを比較する。CPLがゲートのDPL以上であれば、ゲートの先のセグメントのDPLによらず、そのセグメントにジャンプできる。
  • LDTを使うときは、GDT内にLDTを指すディスクリプタを作成して、そのセレクタ値をLDTRに設定する。

割り込み

  • 割り込み番号は0x00~0xFFまで全256種類。
  • 割り込みディスクリプタテーブル(IDT)には、ゲートディスクリプタを並べておく。割り込み番号がIDT内オフセットとなる。ゲートの種類は、割り込みゲート、トラップゲート、タスクゲートの3種類。
  • ふつうRET命令は単純にCS,EIPをPOPするだけだが、POPしたCSのDPLがCPLよりも低ければ、続けてSSとESPもPOPする。
  • 低特権レベルからコールゲートを介してOSの関数を呼び出したとき、引数は低特権レベルのスタックにしか存在しないため、OSから簡単に扱えない。対策として、コールゲートのコピーカウントというパラメタに、引数のバイト数/4を設定しておき、自動的に引数をOS側のスタックにコピーさせる。戻るときは、オペランド付きRET命令を使い、OSのスタックをインクリメントさせる。

I/O

  • IOポートはディスクリプタがないので、DPLを持たないが、代わりにIOポート専用の特権レベルIOPRL(I/O Privilege Level)がある。IOPRLは、EFLAGSレジスタで設定される。また、TSS内のIO許可マップで制御することもできる。

その他

  • クロックダブラーと呼ばれるクロック周波数を2倍にする技術があった。486DX2やODP(Over Drive Processor)で使われていた。
  • OSが利用するシステムレジスタはGDTR、IDTR、LDTR、TR、CR0~CR3。
  • DR0~DR7を操作することでブレークポイントを仕掛けて、デバッグできる。
  • アドレスバスとデータバスは32ビット、IOアドレスバスは16ビット。
  • オペランドサイズとアドレスサイズはそれぞれ32ビットあるいは16ビットで解釈される。どちらもプレフィックス(0x66あるいは0x67)をつけることでサイズを変更できる。
  • 現在のタスクが使っているTSSのセレクト値は、TR(Task Register)に保持する。
  • 同特権レベルでのCALL命令であれば、CSとEIPをPUSHして、ジャンプするだけ。異なる特権レベルへ移るためのコールゲートへのCALL命令であれば、スタックを切り替えてから、SS、ESP、CS、EIPをPUSHする。

x86エミュレータ開発記録 (2)

21 Jul 2018
[x86]

xv6のページングについてまとめておきたい。 カーネルmain関数では、ページテーブルやページディレクトリの初期化が2度行われる。

1度目のページング機構の初期化は、 仮想アドレスの下位4MB 0x0 ~ 0x400000 をそのまま物理アドレス 0x0 ~ 0x400000 にマッピングする。 また、0x80000000をオフセットとした仮想アドレス 0x80000000 ~ 0x80400000 についても、 同じく物理アドレス 0x0 ~ 0x400000 にマッピングする。 このページングはカーネル初期化時にのみ利用される。 CR4のPage Size Extension(4bit目)を有効にすることで、 ページサイズを4MBに拡張している(スーパーページ)。 スーパーページでは、ページテーブルは利用せずページディレクトリだけを使う。 仮想アドレスの上位10ビットがページディレクトリのインデックス、 下位10ビットが4MBページ内のオフセットに対応する。 つまり、1段のページング。 kinit1関数では、カーネルの終端から4MB(0x801154a8 ~ 0x80400000)のページに対してkfreeを実行し、 freelist(空きメモリをつなげたlinked list)に繋いでいく。

続いて、2度目の初期化では、 4MBを超える仮想アドレスを対象としている。 こちらは、スーパーページは使わないので、ページサイズは4KBのままで、 ページディレクトリとページテーブルのどちらも使う。つまり、2段のページング。 kinit2関数で、0x80400000 ~ 0x8e000000のページをfreelistに繋ぐ。

今回は、2度目の初期化のところでバグがあったため、 kinit2内で不正なアドレスに書き込みを行っていた。 2段のページング機構でのページウォークを見直して、解決した。 CR3の上位20bitがページディレクトリのベース物理アドレスを指しているが、 そのアドレスはCR3 >> 20ではなくCR3 & 0xFFFFF000であることに注意する。 ページディレクトリエントリについても同様のことが言える。 つまり、ページディレクトリやページテーブルは4KBでアライメントされている。


x86エミュレータ開発記録 (1)

01 Jul 2018
[x86]

自作エミュレータで学ぶx86アーキテクチャ という本を読んだ。 その続きとして、主に教育向けに使われているx86 OSのxv6を完動させることを目標に、 自作エミュレータを拡張していこうと思う。 比較的長期間にわたって開発することになるので、一旦今の状況をまとめておきたい。 現状では、ブートローダによってカーネル本体をメモリ上に展開し、 カーネルのmain関数から実行を開始できる状態になっている。 より正確には、ブートローダからカーネルのmain関数へと進み、 どこかの地点でカーネルパニックが起きている状態である。 また、シリアルポート(I/O Port 0x3F8)のエミュレートをできるようになっているので、 カーネルから cprintf 関数や panic 関数を呼び出すことで、任意の文字列を出力できる。 このあたりまでくるとCPU自体のエミュレートは落ち着いてきて、 各種IOや割り込みの実装が中心になってきていると感じる。 ある程度までまとまったら、コードを公開したい。

拡張にあたって、まずは本のコードの写経から始めた。 足りない命令については、インテルの仕様書を参考に随時追加していった。 Mod R/MやSIBバイトの動作については、手当たり次第に実装していたが、 インテルの仕様書を見れば一発でわかる話だった。 例外的な動作が多く、例えば、SIBバイトによるアドレス指定で Base=5 としたときには、 レジスタでなく disp8disp32 を使うことになる。 開発初期は各命令の動作を検証するために、QEMUの動作を正として、以下のようにQEMUのレジスタをトレースするGDBスクリプトを用意した。 10万ステップにわたり、自作エミュレータとQEMUのレジスタが一致するかどうかテストしながら、開発を進めた。

target remote localhost:1234
set architecture i8086
set confirm off
break *0x7c00
c
set variable $i = <numstep>
while $i > 0
si
info registers
set variable $i -= 1
end
quit

xv6の動作についても、簡単にまとめておく。 xv6のイメージを指定し自作エミュレータを起動すると、 16bitモードで起動し、EIP=0x7c00 の状態からbootasm.Sのブートローダを開始する。 その後、32bitモードに移行して、Cコード bootmain.cへジャンプする。 bootmain.cでは、ディスクからELF形式のカーネル本体を読み込み、メモリ上に展開する。 main.cのカーネル本体では、 mpinit 関数や lapicinit 関数などから、各種デバイスの初期化を順に実行していく。 上から順番に初期化していれば良いので、わかりやすい。 コンテキストスイッチあたりまで話が進むとデバッグが大変になるんだろうと思う。

参考


Chef実践入門の読書メモ

21 Oct 2017
[chef]

Chefを利用する機会があり、体系的に知りたいと持って、Chef実践入門を読んだ。 自分用に、そのメモを残しておく。

概要

  • 管理サーバを介するかどうかによって、2種類の動作形態がある。1つ目は、管理サーバとしてChef Serverを介するサーバクライアントモデル。ローカル端末と、管理対象ノードの他に、Chef Serverを必要とする。ローカル端末・Chef Server間、Chef Server・ノード(Chef Client)間でやり取りする。ノード数の増加に強い。2つ目は、Chef Soloを使ったスタンドアロンモデル。Chef ServerもChef Clientも不要。小規模な環境で使われることが多い。いずれもRubyで実装されているので、gem install chef knife-solo berkshelfのように、gemによってインストールできる。
  • リポジトリ(キッチン)、クックブック、レシピという階層構造を持つ。リポジトリは、Gitなどでバージョン管理をする。site-cookbooksに自作のクックブック、cookbooksに外部クックブック、data_bagsにリポジトリ全体でグローバルなスコープのデータ、environmentsにdev/prodなどの環境情報、nodesにノードごとの設定情報(nodeオブジェクト)を配置する。また、Berksfileに依存する外部クックブック一覧を記述する。

クックブックの実装

  • 同一構成の複数ノードを管理する場合には、ノードをまとめたroleを定義する。<repository>/roles/<role name>.jsonを作り、run_listなどを書いていく。また、Attributesを上書きすることもできる。default_attributesは定義されていないものを定義し、override_attributesは強制的に上書きするもの。
  • Attributeというkey-valueを管理する仕組みがある。自分で設定したり、システムから自動抽出することができる。自動抽出する場合には、ohaiコマンドの結果が使われる。慣習的に、ohaiで取得したの値のキーに関してはnode[:platform]のようにシンボルを利用し、Nodeオブジェクトで定義したキーに関しては、node["httpd"]["port"]のように文字列を利用する。Attributeは、<cookbook name>/attributes/defaults.rbにデフォルト値を定義できる。Attributeの優先順は、Nodeオブジェクト>ロール>Environment>レシピのAttribute>クックブックのAttribute。
  • cookbook_fileリソースは<cookbook name>/files以下のファイルを転送し、fileリソースはファイルを新規作成するために使われる。
  • ifconfigmountgem_packagegithttp_requestrouteruby_blockリソースなどがある。もし、これらのリソースを使っても管理できないものがあれば、scriptリソースを使う。ただ、not_ifonly_ifなどをうまく使って、冪等性を自分で保証する必要がある。
  • 設定ファイルの配置後に、notifies :reload, "service[httpd]"のようにして、サービスのリロードできる。あるいは、subscribes :reload "template[/etc/httpd.conf]"によって、逆方向で定義できる。どちらも動作は同じで、キューに詰め込まれてまとめて実行される。
  • if文でインデントが深くなり可読性が下がってしまう場合には、only_ifなどの条件付きアクションを使うと良い。
  • レシピに書かれたリソースはコンパイル後に収束のタイミングで実行されるが、その他のRubyコードはコンパイル時に実行される。収束のタイミングで実行されるRubyコードを書きたければ、ruby_blockリソースを使う。
  • 一連のリソースや処理は、Definitionとして、マクロのように実装できる。

Chef Solo

  • Cookbookを開発するサーバと、管理対象ノードが同一の場合には、knife cookbook create <cookbook name>によりクックブックを作成し、chef-solo -o <cookbook name>によりクックブックを実行する。こういう使い方はあまりしないのかな。
  • 開発サーバと管理対象ノードが異なる場合には、開発サーバでknife-soloを使う。knife-soloコマンドは、手元のクックブックをノードへ転送し、chef-soloを実行する。まず、開発サーバで、knife solo init .により、リポジトリを準備する。次に、knife solo prepare <hostname>により、管理対象ノードにche-soloをインストールする。続いて、knife cookbook create <cookbook name> -o <repository>/site-cookbooksで、クックブックを作成する。その後、クックブックを編集して、<repository>/nodes/<hostname>.jsonの中で{'run_list': ["recipe[<cookbook name>:<recipe name>]"]}と記述する。<recipe name>を省略すると、default.rbが参照される。最後に、knife solo cook <hostname>で、ノードをプロビジョニングする。なお、knife solo prepare <hostname>knife solo cook <hostname>をまとめたknife solo bootstrap <hostname>コマンドもある。また、chef-soloにのみ依存するものはknife soloを使い、それ以外のものはknifeを使う。

その他

  • Test Kitchenを使って、クックブックを統合テストできる。Vagrantなどで、複数のOSを立ち上げる。テストの実行は、クックブックの適用後に、surverspecによって行われる。実際にテストを適用せずに、振る舞いをテストしたければ、ChefSpecを利用する。
  • knife cookbook site search <cookbook name>で外部のクックブックを検索して、Berksfilecookbook '<cookbook>'追加する。その後、berksコマンドで、cookbooksディレクトリにダウンロード、展開される。
  • Opscode、Basecamp、RiotGames、aws、engineyard、pivotal-sproutなどがコミュニティクックブックを公開している。
  • Chef Serverは、新規clientの登録時に双方のvalidation.pemをチェックして、認証する。その後、クライアントに秘密鍵を発行する。
  • knife statusで最後にChef Clientが実行された時間を確認できる。
  • Chefの持つ冪等性と、緊急時のロールバックは相性が悪い。そのため、Chefによるアプリケーションのデプロイは理想的ではない。
  • クックブックの適用忘れを防ぐためには、Chef Soloを定期実行するか、Server/Client構成にして自動適用する。
  • knifeコマンドやohaiコマンドのプラグインは、指定のディレクトリにRubyコードを配置することで、簡単に実装できる。