~主にプログラミング関係この広告は、90日以上更新していないブログに表示しています。QEMUTweetVMMの高速化について学ぶ過程でKVMのコードを読んだので、メモ代わりに内部構造の解説記事を書きました。)も合わせてご参照ください。本記事はある程度システムプログラムに慣れており、VT-xや仮想化の基本アーキテクチャは知っている物として進めます。1.qemu-kvm,kvmの初期化では早速見て行きましょう。KVMの初期化の入り口となるqemu-kvmサイドから見ていきます。(ちなみに現在qemu-kvmはqemu本家に統合されておりconfigを変えることでkvmを有効化する仕様になっています)qemuは/dev/kvmを通してKVMとやり取りを行います。全体的なアーキテクチャは以下のような感じです。http://vm360degree.com/2011/09/24/2011-red-hat-forum-%E0%B8%95%E0%B8%AD%E0%B8%99%E0%B8%97%E0%B8%B5%E0%B9%88-1-%E2%80%93-kvm-kernel-base-virtual-machine-%E0%B8%84%E0%B8%B7%E0%B8%AD-%E0%B8%AD%E0%B8%B0%E0%B9%84%E0%B8%A3/より実際の初期化コードは以下のようになります。0;}[*1]でMachineClassからAccelStateにアクセスしています。KVMState(kvm-all.c)はAccelStateを継承しておりこれもポリモーフィズムですね。internalsに載っています)さて[*2]で/dev/kvmをopenしていますがこれは上でも述べたように、qemuがカーネルモジュールであるkvm.koとやり取りするためのインタフェースとして使用されます。[*3]でKVMの必要とするCPUの数を調べています。中身はkvm_ioctl(kvm-all.c)を使って/dev/kvmにioctlでKVM_CHECK_EXTENSIONリクエストを飛ばしています。qemu-kvmはkvm_ioctlを利用してこのようなリクエストを頻繁に送ります。これらリクエストはKVMのAPIとして定義されています。(KVMのAPIはDocumentation/virtual/kvm/api.txtにマニュアルがあります)[*4]でKVM_CREATE_VMを発行しKVM用のVMを作成するよう依頼します。[*5]では作成したVMのfdをvmfdに代入しています。VMはKVMにおいてkvm-vmという名前のanonymous-inodeとして管理されており[*4]ではそのfdが返り値となっているわけです。次にKVMサイドの初期化処理を見て行きましょう。[*6]arch/x86/kvm/vmx.cはIntelのVT-Xを扱うKVMモジュールで、[*6]でvmx_initをカーネルモジュールの初期化関数として登録しています。[*7]のkvm_initの第1引数にvmx_x86_opsのアドレスを渡しています。vmx_x86_opsはkvm_x86_ops構造体で、これはx86特有の操作を関数としてまとめた物です。0;...}[*8]でkvm_devをmiscdeviceとして登録しています。miscdeviceはメジャー番号が10に固定されマイナー番号で識別されるキャラクタデバイスでここにkvmという名前のデバイスを登録します。(これが/dev/kvmデバイスとなるわけです)kvm_devは以下のように定義されています。r;}[*9]のKVM_CREATE_VMを見てください。これは/dev/kvmにKVM_CREATE_VMがioctlで発行された場合に処理される箇所です。QEMUサイドの初期化で発行されていましたね。ここではkvm_dev_ioctl_create_vmを呼び出しVMを作成しています。r;}[*10]でVMを作成した後[*11]で"kvm-vm"という名前のanonymous-inodeを作成しています。これがVMの実体であり、上でも説明したようにqemu-kvmからアクセスする際はこのinodeのfdを通して行います。noop_llseek,};となっており、VMに対するioctlはkvm_vm_ioctlで処理されます。[*10]のkvm_create_vmについて見て行きましょう。kvm;...}[*12]のkvm_arch_alloc_vm(include/linux/kvm_host.h)で新たなkvm構造体(include/linux/kvm_host.h)を確保しています。kvm構造体はゲストごとにそれぞれ一つずつ存在します。[*13]のkvm_alloc_memslots(arch/x86/kvm/kvm_main.c)でメモリスロットをそれぞれ確保し初期化しています。[*14]ではkvm_io_busを初期化しています。notifierフックハンドラを登録する関数です。notifierはゲストのページテーブルが指すページテーブルエントリがホストOSによってスワップ(イン|アウト)された際にゲストへ通知するために呼ばれます。これはゲストOS側のカーネルが知らない間にホスト側でpteがスワップアウトされた場合、ゲスト側のメモリ管理が破綻してしまうのを防ぐためです。次にkvmにおけるvcpuの初期化を見ていきます。)の4.仮想CPUにもあるqemu_init_vcpuの処理を見てみましょう。internalsでは[*16]のqemu_tcg_init_vcpuを見ていきましたが、kvmが有効な場合[*17]でqemu_kvm_start_vcpuが呼び出されるため今回はこちらを見ます。qemu_kvm_start_vcpuでは[*18]でqemu_kvm_cpu_thread_fnを実行するvcpuスレッドを作成し、vcpuがキックインされるまで[*19]で待ちます。qemu_kvm_cpu_thread_fnを見てみましょう。NULL;}[*20]のkvm_init_vcpuでvcpuの初期化を行います。ret;}[*21]でKVM_CREATE_VCPUを発行し[*22]でCPUStateのkvm_fdに代入しています。vcpuの初期化が終われば[*23]でcreatedにtrueを設定しシグナルで通知します。これでKVMにおける/dev/kvmデバイス,vCPU,VMの初期化が終わりました。まとめると/dev/kvm(キャラクタデバイス)KVM自体の制御に使われるインターフェースqemu-kvm側からはkvm_ioctlでアクセスvCPU(anonymous-inode)KVMのvCPUに対する制御を行うインターフェースqemu-kvm側からはkvm_vcpu_ioctlでアクセスioctlのハンドラはKVM側のkvm_vcpu_fopsが用いられる。VM(anonymous-inode)KVM自体の制御に使われるインターフェースqemu-kvm側からはkvm_vm_ioctlでアクセスioctlのハンドラはKVM側のkvm_vm_fopsが用いられる。メイン処理を見て行きましょう。[*24]のkvm_cpu_execがKVMの中心処理です。2.仮想CPU(vCPU)kvm_cpu_execはVMEnterしてからVMExitするまでの一回分の処理を担当します。ret;}[*1]でKVM_RUNを発行しKVMにVMEnterを行うよう指示しています。vcpuに対してioctlが発行された場合、KVM側では以下のような処理が行われます。r;}[*2]がKVM_RUNのフック関数です。実際の処理はこのkvm_arch_vcpu_ioctl_runに投げています。r;}[*3]のvcpu_runで実際にゲストへenterします。r;}[*4]のvcpu_enter_guestがコア部分の処理です。r;}[*5]ではvcpuに届いた各リクエストを処理します。リクエストとはVMEnter前にするべき処理をvcpuに依頼する機構で、有名な例だとkvm-clockが時計の更新をVMEnter前に行う際このリクエストを使用します。[*6]のrunはarch/x86/kvm/vmx.c内でvmx_vcpu_runに設定されています。[*13]...}[*7]でホストレジスタのうちVT-xの退避対象になっていないレジスタをスタック上に退避しています。次に[*8]ですでに一度VMEnterしたかどうか確認し、初めてであれば[*9]でVMLAUNCH命令を、二度目以降なら[*10]でVMRESUME命令を実行しています。[*11]でゲストのレジスタをvcpu.arch.regsからロードした後、処理をゲストへ移します。[*12]ではVMExit後、ゲストのレジスタをvcpu.arch.regsに退避しホストのレジスタを復帰しています。[*13]ではVMCSからVM_EXIT_REASONを読み出します。これによりVM_EXIT要因に合わせたエミュレートを行います。続いて仮想I/O処理を見ていきます。[*14],[*15]のkvm_handle_io,address_space_rwが、I/O処理が理由でVMExitした際の処理になります。仮想I/OKVMにおける仮想IOはqemu-kvmに全て任せています。そのため処理は非常に簡単です。kvm_handle_ioは以下のようになっています。}}[*1]でaddress_space_rwを呼び出しています。これはqemuのaddress_space_ioというMemoryRegionを使用したIO処理です。internals参照)またのKVM_EXIT_MMIOの処理でもaddress_space_memoryをMemoryRegionとしたaddress_space_rwが呼ばれておりqemu側に丸投げしています。このように仮想IO処理はqemu-kvmに移譲されています。仮想IRQ(APIC)KVMは高速化のため、一部のハードウェアをqemu-kvmではなく独自にエミュレートしています。具体的にはPIC,PIT,(I/O|Local)APICです。-no-kvm-pitなどのオプションをつける事でqemuにエミュレートを任せることができますが、せっかくなのでKVM独自のエミュレーションの実装を見て行きましょう。APICから見ていきます。1章の[*25]を見てください。machine_kernel_irqchip_allowedはkvmのIRQチップエミュレーションを利用する場合trueが返り、その後kvm_irqchip_createにてIRQチップの初期化を行っています。[*]}[*1]でKVMに対しKVM_CREATE_IRQCHIPを発行しています。これでKVM側のIRQチップが初期化されます。...}[*2]がKVM_CREATE_IRQCHIPを受け取った際の処理です。[*3]のkvm_ioapic_initでioapicを仮想デバイスとして初期化しています。ret;}[*4]のkvm_ioapic_resetでioapicを初期化し、[*5]でioapic->dev->opsで表されるデバイスの操作メソッドをioapic_mmio_opsに設定しています。0;}[*6]で指定されたバスに[*7]で新たに確保したkvm_io_busをコピーしています。[*8]のkvm_io_bus_insert_devでは新たに作成したkvm_io_busにデバイスをつないでいます。0;}[*9]でkvm_io_rengaeを新たに追加している事が分かります。これでAPICデバイスの登録は完了しました。最後に[*10]のkvm_vcpu_request_scan_ioapicで全てのvCPUに対してKVM_REQ_SCAN_IOAPICリクエストを送信しています。(これらリクエストの処理は後ほど説明します)では戻って次に[*11]のkvm_setup_default_irq_routingを見てみましょう。[*14]...++ue;}...kvm_arch_irq_routing_update(kvm);[*15]...}[*12]でkvm_irq_routing_entryのリストであるkvm_irq_routing_tableを作成しています。kvm_irq_routing_entryはGSIからIRQへの対応(ルーティング)を表しており、[*13],[*14]で引数として渡されたkvm_irq_routing_entryのリストをnewに設定していきます。これでIRQルーティングの登録は完了です。最後に[*15]で全てのvCPUにKVM_REQ_SCAN_IOAPICリクエストを送信しています。これで仮想APICの初期化は完了しました。仮想メモリ(EPT)では次にアドレス空間の仮想化機構であるEPTがKVMでどのように扱われているか見ていきます。EPTはゲスト物理アドレス(GPA)からホスト物理アドレス(HPA)への変換を行う4段ページテーブルです。Violationが発生しVMExitする仕組みです。Violation発生時はhandle_ept_violation関数が実行されます。[*2]}[*1]でVMCSからGPAを取得した後[*2]でページフォルトを処理します。r;}[*3]のarch.mmu.page_faultにはtdp_page_faultが設定されています。r;}EPTの処理は単純で[*4]でまずGPAをゲストページフレーム(GFN)に変換した後、[*5]のtry_async_pfでGFNをホストページフレーム(PFN)に変換します。entryを新たに作成してページフォルト処理は終了です。)があるため、ここでは処理概要だけ述べます。(適時、こちらのページと照らし合わせながら読んで頂ければと思います。fault(以下APF)を実行します。そもそもAPFとはゲストOSが""ホストOSによって""スワップアウトされた空間にアクセスした際、これはゲストOSにとって透過的なためゲスト内でスワップインの間のI/O待ち時間を、別のプロセスに割り当てられないという不平等な状態を解消するための機構です。このような状態を解消するためにAPFではAPF要因という変数をホスト-ゲスト間の共有メモリに置いてスワップアウトされている事をゲストに通知し、スワップインI/O処理はworkqueueにまかせて再びVMEnterします。ちなみにこのメモリ空間の通知にはMSRを使用します。ゲスト側(Linuxカーネルの場合ですが)はAPF用のページフォルトハンドラdo_async_page_faultを使いAPF要因を読んだ後、適時他プロセスにCPUリソースを明け渡します。ゲストのLinuxカーネルに準仮想化的なアプローチを施し、ハイパーバイザと協調する事で、このようなプロセスディスパッチを行っているわけですね。スワップインの処理はこれで良いとして、では実際にどのようなホスト物理アドレス(HPA)に変換してページを割り当てるのか見て行きましょう。[*8]}アドレス変換は先ほどのtry_async_pf内で行われています。[*7]でGFNをmemslotに変換します。中身は[*8]のsearch_memslotsです。search_memslotsによりGFNに該当するmemslotsを検索します。kvm_memslots->npagesが変換対象のGFNです。これらの範囲をkvm_memslots->userspace_addrを開始アドレスとする範囲に写像する事でGFNをHVAに変換します。(qemuのsoftmmuと同じですね)[*9]のkvm_memslotsを使った実際の変換処理を見てみましょう。[*12]}まずは[*10]でGFNをHVAに変換しています。実際の処理は[*11]の__gfn_to_hva_memslotです。PAGE_SIZE;}見たままですが、上で述べた写像を行っていますね。HVAが出せたので次は[*12]のhva_to_pfnでPFNに変換して完了です。pfn;}Linuxカーネルハッカーな方にはお馴染みの関数がありますね。このPFNへの変換はホストOSの処理を殆ど流用しているだけなためLinuxカーネル内APIがいろいろ使われています。[*13]のfast処理は内部で__get_user_pages_fastを呼び出しているだけです。[*14]のslowも似たような物で、[*15]もaddrから赤黒木を辿ってVMAを取得するだけです。KVMの解説に専念するためこの辺りのカーネル内APIは既知とします。これでPFNへの変換が完了しました。実際にこのPFNをEPTテーブルにマップする[*6]の__direct_mapの処理を見て行きましょう。emulate;}[*16]のfor_each_shadow_entryマクロを使い、shadowページテーブルの各レベルを走査していきます。[*17]で現在見ているレベルがマッピングを指定されたレベルの場合、[*18]のmmu_set_spte->set_pteでpfnをエントリーに設定します。[*19]ではマッピングを指定されていないレベル、つまりミドルテーブル(level4~2)がまだエントリー上にない場合[*20]で新たにエントリーを作成し[*21]でつないでいます。おわりにvCPUに始まりKVMの基本的な構造を見てきました。本記事で説明した部分以外でも例えばkvm-clockなどの準仮想化クロックやVFIOなどまだまだKVMには様々な機構が備わっています。これらについてもいつか書けたら良いなーと思いつつとりあえず今回はここまでにしておきます。カーネルエクスプロイト入門記事part2です。)月別アーカイブはてなブログをはじめよう!RKX1209さんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。