プログラマーのためのCPU入門 読書メモ

非常に分かりやすく要点が整理されていて良い本だった。 特にアセンブリで書かれた実験用サンプルコード 1 があったのが嬉しかった。 概念として知ってはいても動かしてみると腑に落ちる感覚がある。 ここでは自分に宛てて雑多なメモを残しておく。 アセンブリ記法 AT&T記法よりもIntel記法の方がわかりやすい。これは自分も感じる。 Intel記法はdestination が左側にくる記法。 gdb向けに~/.gdbinit内でset disassembly-flavor intel とやっとくと良いかもね。 命令実行にかかるサイクル数の計測 add、mul、mov命令にかかるサイクル数を統計的に計測できる 2 。 大量に同一の命令を実行することで統計的に値を出していく。 さらにそれらの命令間で真のデータ依存を持たせることで、 スーパースカラやスーパーパイプラインの影響を排除し1命令ずつ実行していく。 分岐予測 分岐命令のメモリアドレス → ジャンプ先のメモリアドレスをテーブルにキャッシュしておく。 そして再度同じ分岐命令が発行された時にジャンプ先を予測するのが基礎的なアイデア。 関数からの復帰の場合にはreturn address専用のテーブルにキャッシュしておく。 条件を伴う分岐の場合には”100010”のようなビットマップの構造で過去数回分の結果を保持しておいてそれとマッチさせることで予測する。 あるいは対象の命令とメモリアドレスの観点で近くにある他の条件分岐の結果も考慮に入れる場合もある。 精度は95%程度、普通のCPUキャッシュヒット率は97〜%くらいなのでちょっと低め。 投機実行 条件分岐予測によってアウトオブオーダー実行することを投機実行と呼ぶ。 まあ本質的にはこれがアウトオブオーダー実行の一番の目的になる。 つまり、基本ブロック(条件分岐などで区切られた命令列)の領域を超えた命令のアウトオブオーダー実行できるため。 キャッシュコヒーレンシ SMPにおけるキャッシュコヒーレンシはMSIプロトコルとその派生によって実現されている。 アイデアとしてはCPU0がメモリに書き込んだとき、同一アドレスを持つキャッシュライン(64バイトの集まり)が CPU0以外に登場すればそこにinvalidateフラグを立てるというもの。 つまりCPU0以外のCPUがそのメモリアドレスにアクセスする場合には必ずキャッシュミスが起こり、 それに伴い最新の値を主記憶から取得できるようにしている。 Memory consistency 特にマルチコアでメモリアクセスがある時に、メモリアクセスの順番が入れ替わることで意図しない結果を引き起こすので、それの対策としてMemory Consistencyを考えないといけない。 Memory consistencyとは、要するにアウトオブオーダー実行に制約を加えること。 とはいえインオーダー実装だとしてもメモリの仕組み(複数バンクとか?)によっては順序の入れ替えが発生しうるので注意。 x86だとTSOモデル?を元にしている。 x86ではstoreやload命令のメモリアクセス順は普通入れ替わらないが、異なるメモリ属性領域にある場合には入れ替えが起こる可能性がある。 x86 SSEではlfenceやsfence命令が入った。 Linuxのbarrier()マクロ、GCCのmemory clobber、C言語のvolatile修飾子はここでいうメモリ順序を保証しないことに注意する。 これは複雑な領域なので、プログラミング言語の提供するチャネルのような抽象化されたものを使った方が良い。 ordering_unexpected.S 3 でメモリの前段にあるストアバッファに起因するメモリアクセス順の入れ替わりを再現できる。 アトミック操作 キャッシュコヒーレンシプロトコルのMESIにおけるE(排他状態)を使うと実現できる。 LL/SC命令で囲った領域もアトミックな操作ができる。 実際にはSC命令の時にLL命令で読み出したメモリアドレスに書き込みがあれば失敗・なければ成功となる。 なので、LL/SC命令のブロックを成功するまで繰り返せば良い。 もちろん性能を確実に引き出すのは難しい。 単一のプロセッサであっても時分割されるとアトミック操作は必要。 現代的なCPU 現代的なCPUとしてRISC-VアーキテクチャのBOOM(The Berkeley Out-of-Order RISC-V Processor) 4 がある。...

May 18, 2023

Intel NUC 12 Pro で仮想環境をつくる

開発機として Intel NUC 12 Pro 1 を買って Proxmox VE 2 を入れてみた。 これまでは古い Thinkpad を開発機としていたので、快適になったかなと思う。 スペックはこんな感じ。 メモリとディスクは重視したいところだったので多めにした。 一方でコア数はそこまで拘らないのでi3のモデルとした。 vPro 搭載モデルも検討したが、流通量が少なそうなこと、仮想化すればリモートでの管理操作の頻度は減るだろうということ、から見送った。 NUC:Intel NUC12WSHi3(Core i3-1220P) 電源コード:サンワサプライ KB-DM3S-1 メモリ:Team SO-DIMM DDR4 3200MHz PC4-25600 32GBx2 ストレージ:Crucial CT2000P3PSSSD8JP 2TB M.2 PCIe4.0 元々はESXi 8を使おうかと思っていたが、 “Shutting down firmware services” のエラーに苦しみ結局解決できず諦めることにした。 ちなみにPコア・Eコアが混在することによる問題もいくつか報告されているが、 それに対する知見はそれなりに広まっていて 3 4 私の環境でも同じように解決できた。 ESXi 7についてはいくつか 12 世代 Intel NUC での動作事例が見つかったが、コミュニティドライバ?が必要なので面倒に感じてしまい諦めた。 残りの候補として cockpit-machines と Proxmox VE を考えていた。 この中でもできるだけ手軽に使いたかったので Proxmox VE を選択した。 Proxmox VEのインストール手順については特筆するものはなく、 ダイアログにしたがってやっていけば自然にインストールできた。 まだまだ触れていないが、第一印象はなかなか快適で良さそうだ。 Intel NUC12WSHi3 ↩︎...

May 1, 2023

LUA4-U3-AGTE-NBK ドライバのインストール

特権コンテナで遊んでいたら /lib/modules 配下を壊してしまったようだ。 よくわからないがこのマシンから外部に出ていくときに、疎通したりしなかったりする。 イーサネットをUSBタイプAとして受けるアダプタ BUFFALO LUA4-U3-AGTE-NBK 1 を使っていて、 それに対応するドライバが怪しい。 ログはこんな感じ。 ubuntu2004thinkpad:~$ dmesg IPv6: ADDRCONF(NETDEV_CHANGE): enx9096f349a025: link becomes ready usb 3-1: USB disconnect, device number 45 ax88179_178a 3-1:1.0 enx9096f349a025: unregister 'ax88179_178a' usb-0000:00:14.0-1, ASIX AX88179 USB 3.0 Gigabit Ethernet ax88179_178a 3-1:1.0 enx9096f349a025: Failed to read reg index 0x0002: -19 ax88179_178a 3-1:1.0 enx9096f349a025: Failed to write reg index 0x0002: -19 ax88179_178a 3-1:1.0 enx9096f349a025 (unregistered): Failed to write reg index 0x0002: -19 ax88179_178a 3-1:1....

March 31, 2023

vhost-userのネゴシエーション

はじめに gokvmをvhost-userに対応させるにあたり、初期化部分について調査したのでメモとして残しておく。 QEMUのドキュメントVhost-user Protocol 1 に詳しくまとまっているが、 実際に動かしてみないことには分からない部分(例外処理だったり、リクエストの順番だったり、ログの落ち方だったり) もあると思うので動かしてみた。 色々試行錯誤してみたものの、結局 QEMU と DPDK の2つだけで手軽に試すことができた。 ここでは QEMU をサーバモード、DPDK をクライアントモードで動かした。 サーバモードが vhost-user 用の Unix Domain Socket の生成、クライアントがその Socket への接続を担当する。 dpdk-skeleton のビルド DPDK にはいくつかサンプルプログラムが用意されているが、ここでは dpdk-skeleton を利用することにした。 私の使っているディストリビューションではそれは DPDK パッケージに同梱されていなかったので、以下でビルドした。 $ git clone git@github.com:DPDK/dpdk.git $ cd dpdk $ meson setup -Dexamples=skeleton build $ cd build $ ninja $ file ./examples/dpdk-skeleton ./examples/dpdk-skeleton: ELF 64-bit LSB shared object, x86-64, ... 仮想マシンの起動 軽量なVMイメージであるCirrosをQEMUからブートさせた。 ここでpath=$HOME/vhost-net0 がUnix Domain Socket に対応する。 logfile=$HOME/vhost-net0....

March 8, 2023

自作VMM u-rootベースのinitrd

はじめに gokvm開発 1 2 3 4 5 6 の続き。 前回までに紹介したとおり virtio-blk と virtio-net に対応したことで、仮想マシンが外部とIOを通してやり取りができるようになった。 今回は initrd を busybox ベースから u-root ベースへと変更したので、それについて述べていく。 0d89a47f u-rootベースの initrd の導入 Go言語で作られたVMMには、同じくGo言語で書かれた initrd が相応しいのではないかということで、Pull Requestをもらった。 1コマンドで成果物を生成でき、busyboxと比べると手順が少なく簡単な印象を受けた。 cb504d85 u-rootベースのinitrdをデフォルトとする u-rootによるinitrdをしばらく触ってみると自分のやりたいことはこなせるだろうという感触を持ったので、デフォルトとした。ただ、busyboxでは特に意識せずできていたことが u-root ではできないことがあった。例えば以下のもの。 ctrl-lやctrl-eでシェル内カーソル移動を行うために、clearやticコマンドに加えて terminfo ファイルが必要だった。 ゲストの起動時に、NIC・ファイルシステムの初期化やHTTPサーバの起動のために、それを記載したスクリプトファイルを /bin/uinit に配置したが、デーモンが途中でkillされるような挙動になってしまった。init関連の挙動に対する自分の理解が甘いのだと思う。しょうがないのでワークアラウンドとして .bashrc に記載した。 終わりに この他にもいくつかリファクタリングを実施した。 今回はVMMらしい変更はなかった。今後はマイグレーションをやっていきたい。 KVMを使ったVMMを自作してLinuxを起動するまでの記録 ↩︎ KVMを使ったVMMを自作してLinuxを起動するまでの記録2 ↩︎ KVMを使った自作VMMのSMP対応 ↩︎ 自作VMMの PCI デバイス対応 ↩︎ 自作VMM の virtio-net 対応 ↩︎ 自作VMM の virtio-blk 対応 ↩︎

June 13, 2022

自作VMM の virtio-blk 対応

はじめに gokvm開発 1 2 3 4 5 の続き。 前回の virtio-net 対応に引き続いて、virtio-blk に対応した。 virt queueのデータ構造や挙動はそのまま流用できる。 この辺り Virtio はうまく設計されているなと感動する。 7389ff59 カーネルコンパイルオプションの調整 ゲストカーネルからファイルシステムを経由してブロックIOを実現するにあたって、以下のオプションを有効にした。 CONFIG_VIRTIO_BLK=y CONFIG_XFS_FS=y CONFIG_EXT3_FS=y CONFIG_EXT4_FS=y 4f4bbb78 virtio-blkの実装 さて、それでは本題である virtio-blk の実装に移っていく。 virtio-blk の挙動は virtio-net のものとほとんど同じなので、もし前回のブログを読んでいなければ、そちらを先に読むことをお勧めする。 差分はキュー数とdescripterテーブルのエントリが指す先のデータ構造だけである。 virtio-net では送受信のため2つのキューを必要としたが、virtio-blk の場合には 1つのキューで読み書きを実現する。 これはディスクへの読み書きはどちらもOS側からの発行となるため、外部割り込みを受ける必要がないためである。 descripterテーブルエントリが指すデータ構造は、以下のように3つのエントリがLinked Listの要領で繋がっている 6 。 1つ目のエントリが指すデータ構造は blkReq であり、typeフィールドが1なら書き込み、0なら読み込みを意味する。 sectorフィールドがディスクの先頭からのオフセットを意味する。 1セクタは512バイトなので、仮想ディスク用ファイルの sector x 512 バイト目から読み書きすることを意味する。 type blkReq struct { typ uint32 _ uint32 sector uint64 } 2つ目のエントリが実データを指す。ここに実際に読み書きしたいデータをバイナリで格納する。 3つ目のエントリがステータスである。エラーが発生した場合には0以外の数値を書き込む。 その他 Virt Queue の初期化方法や Avail Ring、Used Ring の使い方は virtio-net と全く同じ。...

April 12, 2022

自作VMM の virtio-net 対応

はじめに gokvm開発 1 2 3 4 の続き。 最近の一連の開発によって、gokvm 上のVMに virtio-net によって仮想NICを提供することができた。 ネットワーキングのサポートは当初の目標の一つだったので、達成感がある。 この対応によって gokvm 上のVMはホスト(あるいはソフトウェアスイッチを経由して外部)との間で通信できるようになった。 WEBサーバを提供したり、SSHでログインできたり、と出来ることの幅が広がる大きな変更だと思う。 例によって、重要なコミットを抜き出して振り返りたい。 c5217550 Virt Queue データ構造の追加 そもそも Virt Queue とは何なのか。 Virt Queue は ゲスト・ホスト間におけるデータのやり取りに使うリング構造のキューを意味する。 例えば送受信でそれぞれ1つのキューを使うナイーブな virtio-net の場合には、 送受信それぞれ1つの Virt Queue (全体で合わせて2つのVirt Queue)が必要になる。 もちろん、マルチキューをサポートする場合やコントロールキューをサポートする場合には、さらに Virt Queue が必要になる。 1つの Virt Queue は Descripter Table、Avail Ring、Used Ring から構成される。 取り扱いたいデータのアドレスと長さを1つのディスクリプタとしてまとめ、それをテーブル状に並べたものが Descripter Table である。 Avail Ring と Used Ring は似ていて、どちらもディスクリプタのIDをゲスト・ホスト間で 伝え合うために利用される。 方向も決まっていて、Avail Ring がゲストからホスト宛、Used Ring がホストからゲスト宛 となる。 ちなみに virtio 仕様 5 の中では、ゲストをdriver、ホストをdevice と表現している。...

March 18, 2022

自作VMMの PCI デバイス対応

はじめに gokvm開発 1 2 3 の続き。 gokvm 上のVMからPCIデバイスを取り扱えるよう開発を進めてきた。 道のりは長いだろうが、最終的には virtio-net を経由して、VMと外部の間でIP疎通を取りたい。 現時点では virtio-net デバイスをゲストカーネルのネットワークインターフェイスとして認識させることができたので、 ひとまずそこまでのログを残しておく。 やったことを大きく分けると、(1) ゲストのLinuxカーネルに対してvirtio-netデバイスをPCIデバイスとして認識させ、 (2) virtio-netデバイス初期化を完了させることでネットワークインターフェイスとして登録させることの2点。 virt queue上の操作やパケットのやり取りについては、この記事には含まれない。 例によって、コミット単位で実装の経過を残しておく。 fc02176d lspciコマンドの追加 busyboxにはlspciコマンドが同梱されているが、pci.ids ファイル 4 が存在しない。 pci.ids はベンダIDやデバイスIDなどの数値と、それに対応する文字列が組になっているようなファイルである。 このファイルがあれば、人間に読みやすいフォーマットで出力できる。 後々のデバッグをスムーズに進めたいので、対応させておいた。 e126392e PCI Config空間に対するIOエミュレーション カーネルがPCIデバイスを認識するための重要なフェーズ。 PCI Config 空間を読む方法はいくつかあるようだが、ここではタイプ1 5 と呼ばれる方法でアクセスした。 ここで使われるIOポートのアドレスは以下の通り。 0xcf8:アドレスレジスタに対応する。バス番号、デバイス番号、Function番号、PCI Config 空間内のオフセットに対応する。 0xcfc ~ 0xcff:データに対応する。 アドレスレジスタは32bit幅で以下のように解釈される。 位置 内容 Bit 31 Enable Bit Bit 30-24 Reserved Bit 23-16 Bus Number Bit 15-11 Device Number Bit 10-8 Function Number Bit 7-0 Register Offset ざっくり PCI Config 空間のあるオフセットにあるデータを読みたいときは次のような手続きになる。...

January 24, 2022

KVMを使った自作VMMのSMP対応

はじめに gokvm開発 1 2 の近況報告。 これまでは1つの仮想CPUにしか対応していなかった。 マルチCPUのためSMP(Symmetric Multiprocessing)対応させたいと思い立ってから2~3週間くらい試行錯誤し、無事実装することができた。 自分の知る限り KVM でVMMを作ってみたという取り組みを探す中で、 具体的にSMP対応とはどのような実装なのか解説されている資料がなかなか見つからなかった。 稚拙な記事ではあるけれど、今後自作VMMに挑戦する方にこの記事が役に立てば嬉しい。 例によってコミット単位に開発の経過を紹介していく。もちろん実際にはもっともっと泥臭い実装から始めていて、 何度もgit rebaseを繰り返しながら、最終的に解説できるよう粒度を調整したので、コミットのタイムスタンプはあてに出来ない。 #34 vCPUスレッドを複数生成 プルリクエストをいただいた。まずはioctl(fd,KVM_CREATE_VCPU,...)でvCPUを複数生成できるよう変更する。 その後vCPUごとに個別のスレッドを生成して各vCPUごとに独立してioctl(fd,KVM_RUN,...)を発行する。 vCPUはライフタイム全体を通して、同一のスレッドからioctlを発行する必要がある。 Go言語の場合にはスレッドの代わりにgoroutineを使うことが多いので、 runtime.LockOSThread()を呼び出してgoroutineとスレッドを静的に関連づけた。 ce22a91 struct mpf_intel の実装 vCPUがカーネルに認識されるためにはIntel MultiProcessor Specification 3 に従ったデータ構造を認識させる必要がある。 このデータ構造はLinuxカーネルの中で struct mpf_intel のPhysPtrが指す先 struct mpc_table に対応する。 コード 4 を読むと、チェックサム・バーション・マジックナンバーを読み取ることができたので、 仕様書は斜め読みしかしていない。 このデータ構造はどこに配置すれば良いのか。仕様書を読むとExtended BIOS Data Area (EBDA)の最初の1KB以内とあるのでそこに配置することにした。 EBDAは典型的に 0x0009FC00 に置かれる 5 ようなので、それに倣った。 a. In the first kilobyte of Extended BIOS Data Area (EBDA), or b. Within the last kilobyte of system base memory (e....

November 25, 2021

BusyboxベースのミニマルなLinux環境を作りQEMUで起動

すでに多くの方が似たような取り組みを行っていてブログ記事 1 2 3 として丁寧にまとめられているように、 やはりこういった環境を手元にさっと作れることの意味は大きいと思う。 ここではざっくりとした仕組みを記録しておく。 成果物をスクリプトとしてまとめGithubにあげている。 特徴 CentOS6、CentOS7、Ubuntu20.04などメジャーなディストリビューション向けカーネルのビルドに対応しているので、実務よりの応用ができる Busyboxを使ってユーザランドをメモリ上に展開するので、起動のたびにピュアでミニマルな環境を作れる SSHログインや外部ネットワーク疎通が可能なので、他システムとの連携が絡む動作を検証しやすい GDBを使ったデバッグによってカーネル内部のデータを参照できる 現時点ではx86/64のみに対応している カーネルのビルド カーネルのビルドは端的に言えば、ビルド設定を.config ファイルに記述し、makeコマンドを叩くことに対応する。.config はテキストファイルなので適用なエディタでも編集できるが、専用のコマンド(make oldconfig、make defconfig、make menuconfig など)が用意されているので、それを使うことが多い。カーネルはアップストリームのものと各ディストリビューションが手を加えたものがあるがここでは以下の全てのカーネルをビルドできるよう環境を整えた。 upstream (kernel v2.6.39) centos6 (kernel v2.6.32-754.35.1.el6) centos7 (kernel v3.10.0-1160.13.1.el7) ubuntu20.04 (kernel v5.4.0-65.73) 最近のGCCで古いカーネルをコンパイルするのは難儀なので、カーネルバージョンごとにビルド専用Dockerイメージを用意した。例えばカーネルv2.6.39はCentOS6のビルド環境を使ってビルドすることにした。 FROM ghcr.io/buddying-inc/centos68:latest RUN sed -i "s|#baseurl=|baseurl=|g" /etc/yum.repos.d/CentOS-Base.repo \ && sed -i "s|mirrorlist=|#mirrorlist=|g" /etc/yum.repos.d/CentOS-Base.repo \ && sed -i "s|http://mirror\.centos\.org/centos/\$releasever|https://vault\.centos\.org/6.10|g" /etc/yum.repos.d/CentOS-Base.repo RUN yum install -y gcc perl glibc-static kernel kernel-devel \ autoconf zlib-devel zlib-static openssl-static openssl-devel 上のDockerfileをもとに buildenv-v2....

July 7, 2021

KVMを使ったVMMを自作してLinuxを起動するまでの記録 2

2021/2/24 WSL2 サポート 4f6b785 WSL2(Windows Subsystem for Linux 2)のUbuntu 20.04で gokvm を実行すると、 IOポート 0x64 への出力が無限に繰り返され、Initプロセスの起動まで到達しなかった。 どうやら PS/2 キーボード周りの挙動が原因のようだ。 kvmtool では in (0x61) に対して 0x20 を返している 1 のでそれを踏襲する形で対応した。 IOポート 0x61は NMI (Non-Maskable Interrupt)のステータスとコントロールレジスタとして使われているようだ 2。 このステータスレジスタの内容を調べると、bit 5はmirrors timer 2 output condition を意味するが、 これ以上は解釈できず。 理解できていない部分はあるが、結果として WSL2 での ゲストVMの起動もできるようになった。 0061 r KB controller port B control register (ISA, EISA) system control port for compatibility with 8255 bit 7 parity check occurred bit 6 channel check occurred bit 5 mirrors timer 2 output condition bit 4 toggles with each refresh request bit 3 channel check status bit 2 parity check status bit 1 speaker data status bit 0 timer 2 gate to speaker status 出典:XT, AT and PS/2 I/O port addresses...

March 3, 2021

KVMを使ったVMMを自作してLinuxを起動するまでの記録

はじめに KVMを利用したナイーブで実験的なVMMを作ってみた。 ioctl で /dev/kvm を叩いて仮想マシンを作成し、その上でLinux Kernelとユーザプロセスを起動できる。 Kernelのデバイスドライバから認識できる程度の非常に簡素なシリアルコンソールのエミュレーションも実装したので、 ログインシェルから操作ができる。 一方で、ネットワーキングやディスクについては現時点ではまだサポートしていない。 最近はKVMを従来のような仮想マシンとしての使い方だけでなく、 マルチテナントなクラウド環境において分離レベルを強化するために、 Google gVisor 1 や Kata Containers 2、 Amazon Firecracker 3 をはじめとした コンテナやマイクロVMでの使い方が登場してきた。 今回作ったgokvmは標準ライブラリのみを使いGo言語で実装したもので、 全体で1,500行程度(ブログ記事作成時点)なので、 自分と同じようにKVMやLinuxのブートプロセスに興味のある方にはとっかかりとして役立つかなと思う。 コミットログを見ながら、何をどう実装したのかについて振り返ってみる。 2021/1/30 プロジェクト始動 632c6e0 最初のコミット。README.md、.gitignore、LICENSEファイルを配置しただけで、 特に特筆することはない。似たようなプロジェクト 4 5 や LWN.net の記事 6 を調べていた。 ミニマムな実装で Linux ユーザランドまでブートさせるようなものは見当たらなかった。 ざっと調べただけなので調査漏れがあるかも。 もともとはkvmtool 4 がその立ち位置だったのかもしれないが、 ちょっとコードが巨大に感じた。kvm-host.c 5 は250行程度のCのコードでkernelのブートができるが、 ユーザランドまでは到達できていないようだ。 2021/2/4 bzImage・initrdのビルドとKVMのラッパー実装 69e3ebb 動作確認用のbzImageとinitrdを make コマンドから生成できるようにした。 bzImageはLinux Kernel本体、initrd はメモリ上の一時的なファイルシステムに対応する。 Linux Kernel バージョンはプロジェクト開始時点で最新の 5.10 を使った。 make tinyconfig を実施したのち、make menuconfig を使って追加で必要なconfigを有効にした。 initrdは、Busyboxをベースとした。 Linux KernelとBusyboxの ....

February 18, 2021

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

あけましておめでとうございます。 x86のエミュレータの開発を続けてきましたが、半年が経過し開発速度もゆっくりになってきたので、 一旦このタイミングでコードを公開することにします。 https://github.com/bobuhiro11/tiny_x86_emu に置いているので、 興味があれば見てみてください。

January 4, 2019

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

先月、Go 1.11がリリースされ、WebAssemblyが実験的にサポートされるようになった。 開発中のエミュレータもGoで書かれているということで、せっかくなので、 WebAssemblyに移植した。 単純にGoをWebAssemblyにコンパイルすると標準出力としてブラウザのコンソールが使われるが、 divタグの中に出力したかったので、syscall/jsパッケージでDOMを操作した。 その他の部分は特に変更の必要がなかった。 https://bobuhiro11.net/tiny_x86_emu/に公開している。 まだ開発途中という状態で、ユーザプロセス実行に関する開発が滞っているので、そろそろ手をつけていきたい。

September 19, 2018

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

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では、espをproc->contextとして、edi、esi、ebx、ebpをpopする。 そして、最後にret命令で、eipを復元する。...

August 19, 2018

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

初めて読む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する。

August 4, 2018

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

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でアライメントされている。

July 21, 2018

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

自作エミュレータで学ぶx86アーキテクチャ という本を読んだ。 その続きとして、主に教育向けに使われているx86 OSのxv6を完動させることを目標に、 自作エミュレータを拡張していこうと思う。 比較的長期間にわたって開発することになるので、一旦今の状況をまとめておきたい。 現状では、ブートローダによってカーネル本体をメモリ上に展開し、 カーネルのmain関数から実行を開始できる状態になっている。 より正確には、ブートローダからカーネルのmain関数へと進み、 どこかの地点でカーネルパニックが起きている状態である。 また、シリアルポート(I/O Port 0x3F8)のエミュレートをできるようになっているので、 カーネルから cprintf 関数や panic 関数を呼び出すことで、任意の文字列を出力できる。 このあたりまでくるとCPU自体のエミュレートは落ち着いてきて、 各種IOや割り込みの実装が中心になってきていると感じる。 ある程度までまとまったら、コードを公開したい。 拡張にあたって、まずは本のコードの写経から始めた。 足りない命令については、インテルの仕様書を参考に随時追加していった。 Mod R/MやSIBバイトの動作については、手当たり次第に実装していたが、 インテルの仕様書を見れば一発でわかる話だった。 例外的な動作が多く、例えば、SIBバイトによるアドレス指定で Base=5 としたときには、 レジスタでなく disp8 や disp32 を使うことになる。 開発初期は各命令の動作を検証するために、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....

July 1, 2018