2009年1月8日木曜日

CPU から I/O 空間 (タイマー) へのアクセス

QEMU では、特定のアドレスにコールバックが結び付けてあって、I/O 空間にアクセスする。

具体的には、I/O メモリにアクセスすると cpu_register_io_memory() に渡したコールバックが実行される。
/* mem_read と mem_write は、byte にアクセスするための関数 (mem_*[0])、
word にアクセスするための関数 (mem_*[1])、そして double word にアクセス
するための関数の配列。関数は NULL 関数ポインタで省略することができる。
登録済みの関数は、後で動的に変更される可能性がある。
もし io_index が非ゼロならば、対応する I/O ゾーンが変更される。もしゼロならば、
新しい I/O ゾーンがアロケートされる。戻り値は cpu_register_physical_memory()
とともに使用することができる。もしエラーの場合は -1 が返る。*/

int cpu_register_io_memory(int io_index,
CPUReadMemoryFunc **mem_read, CPUWriteMemoryFunc **mem_write,
void *opaque);
つまり、全てのメモリアクセスについて、通常のメモリアクセスと I/O メモリのアクセスを判定して、適切に扱われないといけない。

QEMU は JIT 実行を行うので、コールバックを呼び出すためのコードを生成する必要がある。

そのやり方が、けっこう面倒なことをしているのでメモ。

まず、論文を読む。だいたいのことが書いてある。

QEMU, a Fast and Portable Dynamic Translator

現在のリリース最新版 QEMU (0.9.1) の JIT は、
  1. ターゲットマシン命令 (のマイクロオペレーション) をエミュレートする C コード片を書く
  2. GCC でコンパイルして、ホストマシン向けのオブジェクトファイルを作る (dyngen は ELF/PE-COFF/MACH-O に対応)
  3. オブジェクトファイルを dyngen で解析し、ターゲットマシン命令からマイクロオペレーション (ホストマシン命令列) トランスレータが include するヘッダを自動生成する
  • 命令に対応する関数のオブジェクトファイル中の位置などを記録したヘッダ
  • オブジェクトファイルの対応位置から命令列をバッファにコピーするコードが生成されたヘッダ (トランスレータ本体)
という、非常に斬新なやり方で実現されている。

(しかし、そのせいで、GCC の 3.x でしかちゃんと動かないという縛りができてしまっている。trunk のヘッドでは、Tiny Code Generator (TCG) という新しい JIT に差し替えられている)

ここのところが、プリプロセッサやトランスレータを駆使したコードの自動生成が行われまくるので、わかりにくい。

QEMU のビルドディレクトリの、例えば arm-softmmu などの下に自動生成されたヘッダファイル (op.h/gen-op.h/opc.h) が存在するので、確認。

ARM の store 命令を x86 命令列に JIT する時には、gen_op_stl_kernel などが呼ばれて、オペコード列が作られる (gen-op.h)。
static inline void gen_op_stl_kernel(void)
{
*gen_opc_ptr++ = INDEX_op_stl_kernel;
}

オペコード列から、最終的な x86 命令列が作られる。その対応表が opc.h。

opc.h には
...
DEF(stl_user, 0, 80)
...
DEF(stl_kernel, 0, 72)
のようになっている。stl_kernel は、store long の kernel モード時の命令に対応する。72 は、オブジェクトファイル中の関数のサイズ。
これらはトランスレータ本体 translater.c の中で使われる。
enum {
#define DEF(s, n, copy_size) INDEX_op_ ## s,
#include "opc.h"
#undef DEF
NB_OPS,
};

#include "gen-op.h"

そして最終的に op.h では
case INDEX_op_stl_kernel: {
extern void op_stl_kernel();
extern char __stl_mmu;
memcpy(gen_code_ptr, (void *)((char *)&op_stl_kernel+0), 72);
*(uint32_t *)(gen_code_ptr + 46) = (long)(&__stl_mmu) - (long)(gen_code_ptr + 46) + 0 -4;
gen_code_ptr += 72;
}
break;

のように、オブジェクトファイルからバッファの gen_code_ptr 位置に 72 バイトコピーし、___stl_mmu() 関数呼び出しのアドレスを差し替えている。

つまり、バッファの JIT 済みのコードの先頭にジャンプして実行された時、___ldl_mmu() が呼ばれて、その中でメモリの種類の判定や、コールバックの呼び出しなどが行われている。

___stl_mmu なんていう関数の定義は grep しても見つからない。プリプロセッサマクロで関数名が自動生成されている。MMUSUFFIX は softmmu_header.h で定義される。

本体は softmmu_template.h にある。その名のとおりのテンプレートで、アクセスする型のサイズや MMU の種類に応じて関数名が作られる。glue はプリプロセッサでシンボルをくっつけているだけ (foo ## 1 == glue(foo, 1) == foo1)
void REGPARM(2) glue(glue(__st, SUFFIX), MMUSUFFIX)(target_ulong addr,
DATA_TYPE val,
int mmu_idx)
{
target_phys_addr_t physaddr;
target_ulong tlb_addr;
void *retaddr;
int index;

index = (addr >> TARGET_PAGE_BITS) & (CPU_TLB_SIZE - 1);
redo:
tlb_addr = env->tlb_table[mmu_idx][index].addr_write;
if ((addr & TARGET_PAGE_MASK) == (tlb_addr & (TARGET_PAGE_MASK | TLB_INVALID_MASK))) {
physaddr = addr + env->tlb_table[mmu_idx][index].addend;
if (tlb_addr & ~TARGET_PAGE_MASK) {
/* IO access */
if ((addr & (DATA_SIZE - 1)) != 0)
goto do_unaligned_access;
retaddr = GETPC();
glue(io_write, SUFFIX)(physaddr, val, tlb_addr, retaddr);
} else if (((addr & ~TARGET_PAGE_MASK) + DATA_SIZE - 1) >= TARGET_PAGE_SIZE) {
do_unaligned_access:
retaddr = GETPC();
#ifdef ALIGNED_ONLY
do_unaligned_access(addr, 1, mmu_idx, retaddr);
#endif
glue(glue(slow_st, SUFFIX), MMUSUFFIX)(addr, val,
mmu_idx, retaddr);
} else {
/* aligned/unaligned access in the same page */
#ifdef ALIGNED_ONLY
if ((addr & (DATA_SIZE - 1)) != 0) {
retaddr = GETPC();
do_unaligned_access(addr, 1, mmu_idx, retaddr);
}
#endif
glue(glue(st, SUFFIX), _raw)((uint8_t *)(long)physaddr, val);
}
} else {
/* the page is not in the TLB : fill it */
retaddr = GETPC();
#ifdef ALIGNED_ONLY
if ((addr & (DATA_SIZE - 1)) != 0)
do_unaligned_access(addr, 1, mmu_idx, retaddr);
#endif
tlb_fill(addr, 1, mmu_idx, retaddr);
goto redo;
}
}
static 関数の glue(io_write, SUFFIX)(physaddr, val, tlb_addr, retaddr); が呼ばれて (io_writel(...))、最終的に io_write[][](...) の形で、関数ポインタがコールされる。

0 件のコメント:

コメントを投稿