るくすの日記 ~ Out_Of_Range ~

主にプログラミング関係

QEMUのなかみ(QEMU internals) part2

前回(part1)rkx1209.hatenablog.com
の続きです。

part2では仮想IRQ,チップセット,仮想IO,TCGを見ていきます。
多分part2で終わりです。(時間があればまたいつかpart3とか書いてみたいですね...)

ではまず仮想IRQから見ていきます。

6.仮想IRQ
QEMUにおいてIRQはIRQState構造体で表されます。
(hw/core/irq.c)

struct IRQState {
    Object parent_obj;

    qemu_irq_handler handler;
    void *opaque;
    int n;
};

nがIRQ番号でhandlerがIRQ#nに対応するハンドラです。またIRQStateは(include/hw/irq.h)でqemu_irqにtypedefされているため以降はqemu_irqと呼びます。
では前回見ていたマシン初期化のメイン部分であるpc_init1の続きから見て行きましょう。

(hw/i386/pc_piix.c: pc_init1)

static void pc_init1(MachineState *machine,
                     const char *host_type, const char *pci_type)
{
    ...
    ISABus *isa_bus;
    ...
    qemu_irq *gsi;
    qemu_irq *i8259;
    qemu_irq smi_irq;
    GSIState *gsi_state;
    ...
    gsi_state = g_malloc0(sizeof(*gsi_state)); [*1]
    ...
    else {
        gsi = qemu_allocate_irqs(gsi_handler, gsi_state, GSI_NUM_PINS); [*2]
    }      
    isa_bus_irqs(isa_bus, gsi); [*6]
    ...  
    else {
        i8259 = i8259_init(isa_bus, pc_allocate_cpu_irq());[*7]
    }
    ...
    for (i = 0; i < ISA_NUM_IRQS; i++) {
        gsi_state->i8259_irq[i] = i8259[i]; [*8]
    }
    
    ...
      
}

[*1]でGSIStateを確保しています。GSIはGlobal System Interruptsの略でqemu_irqを置いておくグローバルなコンテキストです。
[*2]で#0 ~ #GSI_NUM_PINSまでのIRQを初期化していきます。

(hw/core/irq.c)

qemu_irq *qemu_extend_irqs(qemu_irq *old, int n_old, qemu_irq_handler handler,
                           void *opaque, int n)
{
    qemu_irq *s;
    int i;

    if (!old) {
        n_old = 0;
    }
    s = old ? g_renew(qemu_irq, old, n + n_old) : g_new(qemu_irq, n);
    for (i = n_old; i < n + n_old; i++) {
        s[i] = qemu_allocate_irq(handler, opaque, i); [*3]
    }
    return s;
}

qemu_irq *qemu_allocate_irqs(qemu_irq_handler handler, void *opaque, int n)
{
    return qemu_extend_irqs(NULL, 0, handler, opaque, n); 
}

qemu_irq qemu_allocate_irq(qemu_irq_handler handler, void *opaque, int n)
{
    struct IRQState *irq;

    irq = IRQ(object_new(TYPE_IRQ)); [*4]
    irq->handler = handler;
    irq->opaque = opaque;
    irq->n = n;

    return irq;
}

[*3]でそれぞれのIRQを初期化しています。具体的には[*4]でTYPE_IRQ型のオブジェクトをnewしていますね。ではqemu_allocate_irqsに渡されているハンドラ、gsi_handlerを見てみます。

(hw/i386/pc.c)

void gsi_handler(void *opaque, int n, int level)
{
    GSIState *s = opaque;

    DPRINTF("pc: %s GSI %d\n", level ? "raising" : "lowering", n);
    if (n < ISA_NUM_IRQS) {
        qemu_set_irq(s->i8259_irq[n], level);[*5]
    }
    qemu_set_irq(s->ioapic_irq[n], level);
}

IRQナンバーがISAの範囲で扱えるなら[*5]で割り込みを発生させています。扱えない範囲はAPICで割り込んでいます。

(hw/core/irq.c)

void qemu_set_irq(qemu_irq irq, int level)
{
    if (!irq)
        return;

    irq->handler(irq->opaque, irq->n, level);
}

qemu_set_irqは指定されたirqのハンドラを呼び出すだけです。これで割り込みが発生します。簡単ですね。

ではpc_init1に戻りましょう。
[*6]のisa_bus_irqs(hw/isa/isa-bus.c)でISAバスにgsiを設定しています。PCIバスのように4ピンからコンフィグレーションを通してIRQに対応させる必要は無いのでこのように素直な代入で済んでいます。[*7]でisa_busに接続されるi8259を初期化しています。(isa_busについては次の章で説明します)

(hw/intc/i8259.c: i8259_init)

qemu_irq *i8259_init(ISABus *bus, qemu_irq parent_irq)
{
    qemu_irq *irq_set;
    DeviceState *dev;
    ISADevice *isadev;
    int i;

    irq_set = g_new0(qemu_irq, ISA_NUM_IRQS);

	/* Init i8259(master) */
    isadev = i8259_init_chip(TYPE_I8259, bus, true); [*9]
    dev = DEVICE(isadev);
    qdev_connect_gpio_out(dev, 0, parent_irq);
    for (i = 0 ; i < 8; i++) {
        irq_set[i] = qdev_get_gpio_in(dev, i); 
    }

    isa_pic = dev;
	/* Init i8259(slave) */
    isadev = i8259_init_chip(TYPE_I8259, bus, false); [*10]
    dev = DEVICE(isadev);

    qdev_connect_gpio_out(dev, 0, irq_set[2]); [*11]
    for (i = 0 ; i < 8; i++) {
        irq_set[i + 8] = qdev_get_gpio_in(dev, i);
    }

    slave_pic = PIC_COMMON(dev);
	/* irq_set is GPIO of i8259 */
    return irq_set;
}

[*9][*10]でそれぞれi8259のマスター、スレーブを初期化し、[*11]ではdevのunnamed-gpio-out[0]プロパティ、すなわちIRQの出力をマスターのirq_set[2]に設定しています。これでカスケード接続が完了しました。またここでirq_setに設定されるハンドラはpc_allocate_cpu_irq(hw/i386/pc.c)で指定されているpic_irq_requestです。

(hw/i386/pc.c: pic_irq_request)

static void pic_irq_request(void *opaque, int irq, int level)
{
    CPUState *cs = first_cpu;
    X86CPU *cpu = X86_CPU(cs);

    DPRINTF("pic_irqs: %s irq %d\n", level? "raise" : "lower", irq);
    ...
    else {
        if (level) {
            cpu_interrupt(cs, CPU_INTERRUPT_HARD);
        } else {
            cpu_reset_interrupt(cs, CPU_INTERRUPT_HARD);
        }
    }
}

(translate-all.c: cpu_interrupt)

void cpu_interrupt(CPUState *cpu, int mask)
{
    cpu->interrupt_request |= mask;
    cpu->tcg_exit_req = 1;
}

cpu_interruptはCPUStateにハード割り込みが発生した事を通知しTCGの実行を中止するよう要求します。(TCGについては後ほど説明します)
最後に[*8]にてgsi_stateのqemu_irqに上で設定した物を代入していきます。これでqemu_set_irqが実行された場合、gsi_stateを通してpic_irq_requestが呼び出される事になります。

7.仮想チップセット
さて次は仮想チップセットです。念の為ハードウェアの全体像part1とは違うバージョンで載せておきます。

f:id:RKX1209:20151120203217p:plain

INTEL 440FX PCISET 82441FX PCI AND MEMORY CONTROLLER (PMC) AND 82442FX DATA BUS
ACCELERATOR (DBX)より
http://download.intel.com/design/chipsets/datashts/29054901.pdf

ではpc_init1に戻って今度はi440fxを初期化している部分を見て行きましょう。

(hw/i386/pc_piix.c: pc_init1)

  if (pci_enabled) {
	/* hosttype: TYPE_I440FX_PCI_HOST_BRIDGE "i440FX-pcihost"
   	   pci_type: TYPE_I440FX_PCI_DEVICE "i440FX" 
	   gsi: IRQ
	   system_memory: main memory */
        pci_bus = i440fx_init(host_type,
                              pci_type,
                              &i440fx_state, &piix3_devfn, &isa_bus, gsi,
                              system_memory, system_io, machine->ram_size,
                              pcms->below_4g_mem_size,
                              pcms->above_4g_mem_size,
                              pci_memory, ram_memory);[*1]
    } 

host_typeというのは上図にもあるようにCPUのバスとPCIバスをつなぐためのPCI Host Bridgeを表しています。またpci_typeはつなぐ先のPCIを表しています。

(hw/pci-host/piix.c)

PCIBus *i440fx_init(const char *host_type, const char *pci_type,
                    PCII440FXState **pi440fx_state,
                    int *piix3_devfn,
                    ISABus **isa_bus, qemu_irq *pic,
                    MemoryRegion *address_space_mem,
                    MemoryRegion *address_space_io,
                    ram_addr_t ram_size,
                    ram_addr_t below_4g_mem_size,
                    ram_addr_t above_4g_mem_size,
                    MemoryRegion *pci_address_space,
                    MemoryRegion *ram_memory)
{
    DeviceState *dev;
    PCIBus *b;
    PCIDevice *d;
    PCIHostState *s;
    PIIX3State *piix3;
    PCII440FXState *f;
    unsigned i;
    I440FXState *i440fx;

    dev = qdev_create(NULL, host_type); [*2]
    s = PCI_HOST_BRIDGE(dev);[*3]
    b = pci_bus_new(dev, NULL, pci_address_space,
                    address_space_io, 0, TYPE_PCI_BUS); [*4]
    s->bus = b; [*5]
    object_property_add_child(qdev_get_machine(), "i440fx", OBJECT(dev), NULL); [*6]
    qdev_init_nofail(dev); [*7]

    d = pci_create_simple(b, 0, pci_type); [*8]
    *pi440fx_state = I440FX_PCI_DEVICE(d);
    f = *pi440fx_state;
    f->system_memory = address_space_mem;
    f->pci_address_space = pci_address_space;
    f->ram_memory = ram_memory;

    i440fx = I440FX_PCI_HOST_BRIDGE(dev);
    i440fx->pci_info.w32.begin = below_4g_mem_size;

    /* setup pci memory mapping */
    pc_pci_as_mapping_init(OBJECT(f), f->system_memory,
                           f->pci_address_space); [*9]

    ...

    else {
        PCIDevice *pci_dev = pci_create_simple_multifunction(b,
                             -1, true, "PIIX3"); [*10]
        piix3 = PIIX3_PCI_DEVICE(pci_dev); 
        pci_bus_irqs(b, piix3_set_irq, pci_slot_get_pirq, piix3,
                PIIX_NUM_PIRQS); [*11]
        pci_bus_set_route_irq_fn(b, piix3_route_intx_pin_to_irq); [*12]
    }
    piix3->pic = pic; [*13]
    *isa_bus = ISA_BUS(qdev_get_child_bus(DEVICE(piix3), "isa.0")); [*14]

    *piix3_devfn = piix3->dev.devfn;
    ...
    return b;
}

[*2]qdev_createでは新しいDeviceStateを確保します。DeviceStateは自分が接続されているバスの情報をparentとして持ちます。第1引数にはそのparentに指定するBusStateを渡しますがこの場合NULLなのでデフォルトのルートバスがparentになります。
また第2引数には作成するQdevのタイプ名を指定します。(ここではi440FX-pcihost)

[*3]でPCIHostBridgeClassのインスタンスであるPCIHostStateに解釈しています。(part1で説明したQOMのポリモーフィズムです) PCIHostBridgeClass(include/hw/pci/pci_host.h)はSysBusDeviceClass(include/hw/sysbus.h)を継承しています。

[*4]のpci_bus_newでPCIBusClass(include/hw/pci/pci_bus.h)のインスタンスであるPCIBusを初期化しています。

(hw/pci/pci.c: pci_bus_new)

PCIBus *pci_bus_new(DeviceState *parent, const char *name,
                    MemoryRegion *address_space_mem,
                    MemoryRegion *address_space_io,
                    uint8_t devfn_min, const char *typename)
{
    PCIBus *bus;

    bus = PCI_BUS(qbus_create(typename, parent, name));
    pci_bus_init(bus, parent, name, address_space_mem,
                 address_space_io, devfn_min);
    return bus;
}

parentはバスが生えている親デバイス(DeviceState)です。BusStateは親にDeviceStateを持ち、DeviceStateも親にBusStateを持っています。つまりこれらは交互につながっていく設計になっています。(まあボードの回路図を考えればそうなるのが自然ですね)

[*5]でホストブリッジにPCIBusをつないでいます。

[*6]ではobject_property_add_childで"i440fx"プロパティを追加しコンテナにつないでいます。
そういえばpart1の2章QOMの所でコンテナの説明をしていなかったのでこれについては少し詳しく見て行きましょう

(qom/object.c)

void object_property_add_child(Object *obj, const char *name,
                               Object *child, Error **errp)
{
    Error *local_err = NULL;
    gchar *type;
    ObjectProperty *op;
    ...
    type = g_strdup_printf("child<%s>", object_get_typename(OBJECT(child)));

    op = object_property_add(obj, name, type, object_get_child_property, NULL,
                             object_finalize_child_property, child, &local_err);
    ...
    child->parent = obj;

out:
    g_free(type);
}

まあほとんどobject_property_addのwrapperなのですが、最後にchildをobjの子として繋ぎ直しています。
え、Objectって親子関係なんてあったけとなると思いますが、これはQOMのコンテナ(と呼んで良いのかな)で使用する時のみ使われる関係です。コンテナは重要なインスタンス木構造でつないでおくものでルートはobject_get_root()で取得できます。
qdev_get_machineを見てみると分かります。

(hw/core/qdev.c)

Object *qdev_get_machine(void)
{
    static Object *dev;

    if (dev == NULL) {
        dev = container_get(object_get_root(), "/machine");
    }

    return dev;
}

コンテナの木構造はファイルパスのようにアクセスします。ここではroot/machineオブジェクトを取得している訳です。

つまり[*6]はroot/machineオブジェクト(-machineで指定したMachineClassです)の"i440fx"プロパティにdevオブジェクトをつなぎroot/machine/i440fxを作成しています。

さて[*7]のqdev_init_nofail(hw/core/qdev.c)でdevのrealizedプロパティをtrueに設定しています。realizedプロパティはpart1でも説明しましたがデバイスが起動される際trueに設定されます。
ちなみにinitのような名前でなくreazliedという名前がついているのは恐らくQMP(QEMU Machine Protocol)などを利用したデバイスのホットプラグ時に呼び出されるからだと思われます。(プラグに気付くという意味合いでしょうかね)
i440fxブリッジのrealizedコールバック関数については後で見ていきます。

[*8]ではi440fxチップ本体の初期化を行っています。また[*9]のpc_pci_as_mapping_init(hw/i386/pc.c)ではpci_address_spaceをsystem_memoryの子としてつないでいます。(part1の仮想メモリの説明を見てください)

[*10]ではPIIX3Stateを初期化しています。上図ではpiix4となっていますが正確にはpiix3に近くコード中では3の扱いになっています。

[*11]のpci_bus_irqs(hw/pci/pci.c)でPCIBusのset_irqにpiix3_set_irqを設定しています。PICはPCIBusを通じてCPUへとつながっているためバスへ割り込みハンドラを設定しているのだと思われます。

[*12]ではバスにpiix3_route_intx_pin_to_irqという関数を設定していますが、これは一体何なのでしょうか。

(hw/pci-host/piix.c: piix3_route_intx_pin_to_irq)

static PCIINTxRoute piix3_route_intx_pin_to_irq(void *opaque, int pin)
{
    PIIX3State *piix3 = opaque;
    int irq = piix3->dev.config[PIIX_PIRQC + pin];
    PCIINTxRoute route;

    if (irq < PIIX_NUM_PIC_IRQS) {
        route.mode = PCI_INTX_ENABLED;
        route.irq = irq;
    } else {
        route.mode = PCI_INTX_DISABLED;
        route.irq = -1;
    }
    return route;
}

piix3->dev.configは各PCIデバイスのPCIコンフィグレーション空間です。念の為仕様図を載せておきます。

f:id:RKX1209:20151120203247p:plain

Wikipedia:PCI configuration spaceより
https://en.wikipedia.org/wiki/PCI_configuration_space
config[PIIX_PIRQC + pin]により上図のInterrupt Lineから対応するIRQ番号を取り出しているわけです。

[*13]では6章でgsi_handlerを設定したqemu_irqの配列を設定し、[*14]でISAバスを初期化しています。ISAバスはpiix3のchildバスとなっています。

8.仮想IO
では次に仮想IOについて見ていきます。ここでは仮想デバイスとしてフロッピーディスクドライバを見ていきます。(VENOM脆弱性で有名になったあれです)

(hw/block/fdc.c)

static const MemoryRegionOps fdctrl_mem_ops = {
    .read = fdctrl_read_mem, [*3]
    .write = fdctrl_write_mem,
    .endianness = DEVICE_NATIVE_ENDIAN,
};

static void sysbus_fdc_initfn(Object *obj)
{
    SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
    FDCtrlSysBus *sys = SYSBUS_FDC(obj);
    FDCtrl *fdctrl = &sys->state;

    memory_region_init_io(&fdctrl->iomem, obj, &fdctrl_mem_ops, fdctrl,
                          "fdc", 0x08); [*2]
    sysbus_init_mmio(sbd, &fdctrl->iomem);
}
...
static const TypeInfo sysbus_fdc_info = {
    .name          = "sysbus-fdc",
    .parent        = TYPE_SYSBUS_FDC,
    .instance_init = sysbus_fdc_initfn, [*1]
    .class_init    = sysbus_fdc_class_init,
};

static void fdc_register_types(void)
{
    ...
    type_register_static(&sysbus_fdc_info); 
    ...
}
type_init(fdc_register_types)

いつものtype_initがありこれでフロッピークラスを初期化しています。[*1]でコンストラクタとしてsysbus_fdc_initfnを設定しています。コンストラクタ内では[*2]においてMMIOを初期化しています。MMIOはAddressSpaceとしてaddress_space_io(exec.c)を持ちその下にポート番号をアドレスとしたMemoryRegionがつながっていきます。(この辺りのデータ構造はpart1の5.SoftMMUをもう一度見なおしてください)
さてこのMemoryRegionがMMIOである場合、readおよびwriteのフック関数が呼び出されます。
part1でみたaddress_space_lduw_internalをもう一度見てみましょう。
前回は省略していましたがMMIOを処理する部分があります。

(exec.c: address_space_lduw_internal)

static inline uint32_t address_space_lduw_internal(AddressSpace *as,
                                                   hwaddr addr,
                                                   MemTxAttrs attrs,
                                                   MemTxResult *result,
                                                   enum device_endian endian)
{
    uint8_t *ptr;
    uint64_t val;
    MemoryRegion *mr;
    hwaddr l = 2;
    hwaddr addr1;
    MemTxResult r;
    bool release_lock = false;

    rcu_read_lock();
    mr = address_space_translate(as, addr, &addr1, &l,
                                 false);
    if (l < 2 || !memory_access_is_direct(mr, false)) {
       ....

        /* I/O case */
        r = memory_region_dispatch_read(mr, addr1, &val, 2, attrs); [*4]
	....
    } 
}

MMIOの場合[*4]でフック関数を呼び出しています。
フロッピーディスクの場合このフック関数readは[*3]のfdctrl_read_memになります。

(hw/block/fdc.c)

static uint32_t fdctrl_read (void *opaque, uint32_t reg)
{
    FDCtrl *fdctrl = opaque;
    uint32_t retval;

    reg &= 7;
    switch (reg) {
    ...
    case FD_REG_FIFO:
        retval = fdctrl_read_data(fdctrl); [*5]
        break;
    }

    return retval;
}

static uint64_t fdctrl_read_mem (void *opaque, hwaddr reg,
                                 unsigned ize)
{
    return fdctrl_read(opaque, (uint32_t)reg);
}

[*5]でデータレジスタからデータを読み込んでいます。
(フロッピーディスクコントローラの仕様はwikiなどを参照してください。)

(hw/block/fdc.c)

static uint32_t fdctrl_read_data(FDCtrl *fdctrl)
{
    FDrive *cur_drv;
    uint32_t retval = 0;
    uint32_t pos;
    ...
    pos = fdctrl->data_pos;
    pos %= FD_SECTOR_LEN;

    switch (fdctrl->phase) {
    case FD_PHASE_EXECUTION:
        if (pos == 0) {
	   ...
            if (blk_read(cur_drv->blk, fd_sector(cur_drv), fdctrl->fifo, 1)
                < 0) { [*6]
                FLOPPY_DPRINTF("error getting sector %d\n",
                               fd_sector(cur_drv));
                /* Sure, image size is too small... */
                memset(fdctrl->fifo, 0, FD_SECTOR_LEN);
            }
        }
    }
    retval = fdctrl->fifo[pos];
    FLOPPY_DPRINTF("data register: 0x%02x\n", retval);

    return retval;
}

[*6]でセクタ番号およびBlockBackendを指定してブロック読み込みを行っています。
ここからはフロッピーのデータ転送部分を扱うブロックデバイスドライバの処理を見ていきます。

(block/block-backend.c: blk_read)

int blk_read(BlockBackend *blk, int64_t sector_num, uint8_t *buf,
             int nb_sectors)
{
    ...
    return bdrv_read(blk->bs, sector_num, buf, nb_sectors); 
}


(block/io.c)

static int bdrv_rw_co(BlockDriverState *bs, int64_t sector_num, uint8_t *buf,
                      int nb_sectors, bool is_write, BdrvRequestFlags flags)
{
    QEMUIOVector qiov;
    struct iovec iov = {
        .iov_base = (void *)buf,
        .iov_len = nb_sectors * BDRV_SECTOR_SIZE,
    };

    if (nb_sectors < 0 || nb_sectors > BDRV_REQUEST_MAX_SECTORS) {
        return -EINVAL;
    }

    qemu_iovec_init_external(&qiov, &iov, 1);
    return bdrv_prwv_co(bs, sector_num << BDRV_SECTOR_BITS,
                        &qiov, is_write, flags); [*7]
}

int bdrv_read(BlockDriverState *bs, int64_t sector_num,
              uint8_t *buf, int nb_sectors)
{
    return bdrv_rw_co(bs, sector_num, buf, nb_sectors, false, 0);
}

QEMUIOVectorにIOリクエストを設定し、[*7]でbdrv_prwv_coを呼び出しています。

(block/io.c)

static int bdrv_prwv_co(BlockDriverState *bs, int64_t offset,
                        QEMUIOVector *qiov, bool is_write,
                        BdrvRequestFlags flags)
{
    Coroutine *co;
    RwCo rwco = {
        .bs = bs,
        .offset = offset,
        .qiov = qiov,
        .is_write = is_write,
        .ret = NOT_DONE,
        .flags = flags,
    };
    ...
    else {
        AioContext *aio_context = bdrv_get_aio_context(bs);

        co = qemu_coroutine_create(bdrv_rw_co_entry); [*8]
        qemu_coroutine_enter(co, &rwco); [*9]
        while (rwco.ret == NOT_DONE) {
            aio_poll(aio_context, true);
        }
    }
    return rwco.ret;
}

[*8]でコルーチンを作成しています。QEMUはcallback hellを回避するために独自にコルーチンを実装しており、[*9]のenterでbdrv_rw_co_entryコルーチンに飛び、yieldが呼ばれると戻ってきます。

(block/io.c)

static void coroutine_fn bdrv_rw_co_entry(void *opaque)
{
    RwCo *rwco = opaque;

    if (!rwco->is_write) {
        rwco->ret = bdrv_co_do_preadv(rwco->bs, rwco->offset,
                                      rwco->qiov->size, rwco->qiov,
                                      rwco->flags);
    }
    ...
}
...
static int coroutine_fn bdrv_co_do_preadv(BlockDriverState *bs,
    int64_t offset, unsigned int bytes, QEMUIOVector *qiov,
    BdrvRequestFlags flags)
{
    BlockDriver *drv = bs->drv;
    BdrvTrackedRequest req;
    ...
    ret = bdrv_aligned_preadv(bs, &req, offset, bytes, align,
                              use_local_qiov ? &local_qiov : qiov,
                              flags); [*10]
    ...
    return ret;
}

bdrv_co_do_preadvは与えれたブロックIO要求のアドレスのアライメントを調整した後[*10]のbdrv_aligned_preadvを呼び出すだけです。

static int coroutine_fn bdrv_aligned_preadv(BlockDriverState *bs,
    BdrvTrackedRequest *req, int64_t offset, unsigned int bytes,
    int64_t align, QEMUIOVector *qiov, int flags)
{
    BlockDriver *drv = bs->drv;
    int ret;

    int64_t sector_num = offset >> BDRV_SECTOR_BITS;
    unsigned int nb_sectors = bytes >> BDRV_SECTOR_BITS;

    ...

    /* Forward the request to the BlockDriver */
    if (!bs->zero_beyond_eof) {
        ret = drv->bdrv_co_readv(bs, sector_num, nb_sectors, qiov);
    } else {
        /* Read zeros after EOF */
        int64_t total_sectors, max_nb_sectors;

        total_sectors = bdrv_nb_sectors(bs);
        if (total_sectors < 0) {
            ret = total_sectors;
            goto out;
        }

        max_nb_sectors = ROUND_UP(MAX(0, total_sectors - sector_num),
                                  align >> BDRV_SECTOR_BITS);
	...
	else if (max_nb_sectors > 0) {

            ret = drv->bdrv_co_readv(bs, sector_num, max_nb_sectors,
                                     &local_qiov); [*11]
        }
	...
    }
}

[*11]のBlockDriver->bdrv_co_readvには-driverオプションで指定したqemuのimg形式によって指定される関数が違います。例えばqcow2(block/qcow2.c)ではqcow2_co_readvなどが指定されています。qcow2の細かい仕様に関しては省略します。


9.TCG
ではいよいよQEMUの中心処理であるTCG(Tiny Code Generator)の処理を見ていきます。前回の4.仮想CPUの[*15]を見てください。このtcg_exec_all()というのがTCGの実行開始関数です。ではこのtcg_exec_all関数から見て行きましょう。

(cpus.c)

static int tcg_cpu_exec(CPUState *cpu)
{
    ...
    ret = cpu_exec(cpu);[*2]
    ...
    return ret;
}

static void tcg_exec_all(void)
{
    ...
    for (; next_cpu != NULL && !exit_request; next_cpu = CPU_NEXT(next_cpu)) {
        CPUState *cpu = next_cpu;

        qemu_clock_enable(QEMU_CLOCK_VIRTUAL,
                          (cpu->singlestep_enabled & SSTEP_NOTIMER) == 0);

        if (cpu_can_run(cpu)) {
            r = tcg_cpu_exec(cpu); [*1]
	    ...
        } else if (cpu->stop || cpu->stopped) {
            break;
        }
    }
}

[*1]で各vcpuごとにtcg_cpu_execを実行しています。tcg_cpu_execでは[*2]でcpu_execが呼び出されており、ここからがメインの処理となります。

(cpu-exec.c)

int cpu_exec(CPUState *cpu)
{
    CPUClass *cc = CPU_GET_CLASS(cpu);
#ifdef TARGET_I386
    X86CPU *x86_cpu = X86_CPU(cpu);
    CPUArchState *env = &x86_cpu->env;
#endif
    int ret, interrupt_request;
    TranslationBlock *tb;
    uint8_t *tc_ptr;
    uintptr_t next_tb;
    SyncClocks sc;
    ...
    current_cpu = cpu;    
    ...

    for(;;) {
        if (sigsetjmp(cpu->jmp_env, 0) == 0) {
	   ...
            next_tb = 0;
            for(;;) {
                interrupt_request = cpu->interrupt_request;
                if (unlikely(interrupt_request)) {
                    if (cc->cpu_exec_interrupt(cpu, interrupt_request)) { [*3]
                        next_tb = 0;			
                    }
		    ...
                }
		...
                tb_lock();
                tb = tb_find_fast(cpu); [*4]
		...
                if (next_tb != 0 && tb->page_addr[1] == -1) {
                    tb_add_jump((TranslationBlock *)(next_tb & ~TB_EXIT_MASK),
                                next_tb & TB_EXIT_MASK, tb); [*5]
                }
                tb_unlock();
                if (likely(!cpu->exit_request)) {
                    trace_exec_tb(tb, tb->pc);
                    tc_ptr = tb->tc_ptr;
                    cpu->current_tb = tb;
                    next_tb = cpu_tb_exec(cpu, tc_ptr); [*6]
                    cpu->current_tb = NULL;
		    ...
            } /* for(;;) */
	    ...
        } 
    } /* for(;;) */

    return ret;
}
||< 

[*3]で割り込みリクエストが来ている場合cpu_exec_interruptを実行します。ゲストがx86の場合これは(target-i386/cpu.c)のtype_initを通してx86_cpu_exec_interruptが設定されています。

(target-i386/seg_helper.c: x86_cpu_exec_interrupt)
>|c|
bool x86_cpu_exec_interrupt(CPUState *cs, int interrupt_request)
{
    X86CPU *cpu = X86_CPU(cs);
    CPUX86State *env = &cpu->env;
    bool ret = false;
    ...
    else if ((interrupt_request & CPU_INTERRUPT_HARD) &&
                   (((env->hflags2 & HF2_VINTR_MASK) &&
                     (env->hflags2 & HF2_HIF_MASK)) ||
                    (!(env->hflags2 & HF2_VINTR_MASK) &&
                     (env->eflags & IF_MASK &&
                      !(env->hflags & HF_INHIBIT_IRQ_MASK))))) {
            int intno;
	    ...
            intno = cpu_get_pic_interrupt(env); [*7]
	    ...
            do_interrupt_x86_hardirq(env, intno, 1); [*8]
            /* ensure that no TB jump will be modified as
               the program flow was changed */
            ret = true;
        }
	...
    }

    return ret;
}

[*7]でPICCommonState(include/hw/isa/i8259_internal.h)のirq_baseを使ってIRQ番号からINT番号へ変換しています。[*8]で実際に割り込みを実行します。

(target-i386/seg_helper.c)

static void do_interrupt_all(X86CPU *cpu, int intno, int is_int,
                             int error_code, target_ulong next_eip, int is_hw)
{
    CPUX86State *env = &cpu->env;

    if (env->cr[0] & CR0_PE_MASK) {
        {
            do_interrupt_protected(env, intno, is_int, error_code, next_eip,
                                   is_hw); [*9]
        }
    }
    ...
}
..
void do_interrupt_x86_hardirq(CPUX86State *env, int intno, int is_hw)
{
    do_interrupt_all(x86_env_get_cpu(env), intno, 0, 0, 0, is_hw);
}

プロテクトモードが有効な場合[*9]でdo_interrupt_protectedを呼び出します。

(target-i386/seg_helper.c: do_interrupt_protected)

static void do_interrupt_protected(CPUX86State *env, int intno, int is_int,
                                   int error_code, unsigned int next_eip,
                                   int is_hw)
{
    SegmentCache *dt;
    target_ulong ptr, ssp;
    int type, dpl, selector, ss_dpl, cpl;
    int has_error_code, new_stack, shift;
    uint32_t e1, e2, offset, ss = 0, esp, ss_e1 = 0, ss_e2 = 0;
    uint32_t old_eip, sp_mask;
    int vm86 = env->eflags & VM_MASK;
    ...
    dt = &env->idt;
    ptr = dt->base + intno * 8;
    e1 = cpu_ldl_kernel(env, ptr); [*10]
    e2 = cpu_ldl_kernel(env, ptr + 4); 
    /* check gate type */
    type = (e2 >> DESC_TYPE_SHIFT) & 0x1f;
    ...
    dpl = (e2 >> DESC_DPL_SHIFT) & 3; [*11]
    cpl = env->hflags & HF_CPL_MASK;
    ...
    selector = e1 >> 16;
    offset = (e2 & 0xffff0000) | (e1 & 0x0000ffff);
    ...
    if (!(e2 & DESC_C_MASK) && dpl < cpl) {
        /* to inner privilege */
        get_ss_esp_from_tss(env, &ss, &esp, dpl, 0); [*12]
	...
        new_stack = 1;
        sp_mask = get_sp_mask(ss_e2);
        ssp = get_seg_base(ss_e1, ss_e2);
    }
    ...

    shift = type >> 3;

     else {
        PUSHW(ssp, esp, sp_mask, cpu_compute_eflags(env)); [*13]
        PUSHW(ssp, esp, sp_mask, env->segs[R_CS].selector);
        PUSHW(ssp, esp, sp_mask, old_eip);
        if (has_error_code) {
            PUSHW(ssp, esp, sp_mask, error_code);
        }
    }    ...

    if (new_stack) {
        ...
        ss = (ss & ~3) | dpl;
        cpu_x86_load_seg_cache(env, R_SS, ss,
                               ssp, get_seg_limit(ss_e1, ss_e2), ss_e2);
    }
    SET_ESP(esp, sp_mask);

    selector = (selector & ~3) | dpl;
    cpu_x86_load_seg_cache(env, R_CS, selector,
                   get_seg_base(e1, e2),
                   get_seg_limit(e1, e2),
                   e2); [*16]
    env->eip = offset; [*17]
}

[*10]でIDTの示すゲートディスクリプタの上位4byteおよび下位4byteを読み込んでいます。[*11]ではディスクリプタで示されるDPL(Descriptor Privilege Level)および現特権レベルのCPL(Current Privilege Level)を設定し、DPL < CPLな場合、つまり割り込みによって権限が上がる場合[*12]でTSSから新しいスタックの情報を読みだしています。そして[*13]で現在のeflags,cs,eipを退避し割り込みの準備が完了します。
(この辺りの仕様はintelのSDMを読むか、詳解linuxカーネルを持っている方はp.152あたりに載っていますので適時参照してください)
[*13]のPUSHWを少し詳しく見てみましょう。

(target-i386/seg_helper.c)

#define PUSHW_RA(ssp, sp, sp_mask, val, ra)                      \
    {                                                            \
        sp -= 2;                                                 \
        cpu_stw_kernel_ra(env, (ssp) + (sp & (sp_mask)), (val), ra); \
    }

#define PUSHW(ssp, sp, sp_mask, val) PUSHW_RA(ssp, sp, sp_mask, val, 0)

実質的な処理はcpu_stw_kernel_raです。はい、すごく不穏な名前の関数ですね。定義はこちらになります。

(include/exec/cpu_ldst_template.h)

static inline void
glue(glue(glue(cpu_st, SUFFIX), MEMSUFFIX), _ra)(CPUArchState *env,
                                                 target_ulong ptr,
                                                 RES_TYPE v, uintptr_t retaddr)
{
    int page_index;
    target_ulong addr;
    int mmu_idx;
    TCGMemOpIdx oi;

    addr = ptr;
    page_index = (addr >> TARGET_PAGE_BITS) & (CPU_TLB_SIZE - 1);
    mmu_idx = CPU_MMU_INDEX;
    if (unlikely(env->tlb_table[mmu_idx][page_index].addr_write !=
                 (addr & (TARGET_PAGE_MASK | (DATA_SIZE - 1))))) {
        oi = make_memop_idx(SHIFT, mmu_idx);
        glue(glue(helper_ret_st, SUFFIX), MMUSUFFIX)(env, addr, v, oi,
                                                     retaddr); [*14]
    } else {
        uintptr_t hostaddr = addr + env->tlb_table[mmu_idx][page_index].addend;
        glue(glue(st, SUFFIX), _p)((uint8_t *)hostaddr, v); [*15]
    }
}

[*14]ではtlb_tableにキャッシュが無い場合helper_be_st_name(softmmu_tmeplate.h)を利用してHVAに変換し、値を書き込んでいます。(これはpart1のSoftMMUの所で見ました)
もしすでにキャッシュがある場合はaddendを使ってHVAに変換した後、[*15]のstw_pを呼び出します。stw_pは(include/exec/cpu-all.h)においてstw_le_pの別名となっています。
(stw_le_pというのはstore word little endian pointerの略を意図していると思われます)

(include/qemu/bswap.h)

static inline void stw_he_p(void *ptr, uint16_t v)
{
    memcpy(ptr, &v, sizeof(v));
}

static inline void stw_le_p(void *ptr, uint16_t v)
{
    stw_he_p(ptr, le_bswap(v, 16));
}

結局memcpyですね。これでHVAに値を書き込んでいます。
do_interrupt_protectedに戻り[*16]のcpu_x86_load_seg_cache(target-i386/cpu.h)ではCSレジスタ(これはSegmentCacheで表されます)を更新しています。そして[*17]でeipをセグメントレジスタ内のオフセットに更新し割り込みは終了です。これで割り込みハンドラに制御が移ります。

さて割り込み処理が終わったのでTCGに戻りましょう。[*4]のtb_find_fastはTranslationBlockをキャッシュから探し、ない場合はさらにtb_find_slowを呼び出します。

(cpu-exec.c)

static inline TranslationBlock *tb_find_fast(CPUState *cpu)
{
    CPUArchState *env = (CPUArchState *)cpu->env_ptr;
    TranslationBlock *tb;
    target_ulong cs_base, pc;
    int flags;

    cpu_get_tb_cpu_state(env, &pc, &cs_base, &flags);
    tb = cpu->tb_jmp_cache[tb_jmp_cache_hash_func(pc)];
    if (unlikely(!tb || tb->pc != pc || tb->cs_base != cs_base ||
                 tb->flags != flags)) {
        tb = tb_find_slow(cpu, pc, cs_base, flags);
    }
    return tb;
}

そもそもTranslationBlockとはゲストの命令列をTCGを通してホストの命令列に変換した際、これらを分岐命令ごとに分けた物で、命令列の基本単位ブロックのような物です。
これらブロックはそれぞれプロローグおよびエピローグで挟まれた形で実行されますが、分岐命令を実行していくたびに、実行パスにそって接続されていきます。これをBlock Chainingと言います。

f:id:RKX1209:20151120203305p:plain

QEMU: Architecture and Internals Lecture for the Embedded Systems Course CSD, University of Creteより
http://www.csd.uoc.gr/~hy428/reading/qemu-internals-slides-may6-2014.pdf
さてではtb_find_slowの実装を見て行きましょう。

(cpu-exec.c)

static TranslationBlock *tb_find_slow(CPUState *cpu,
                                      target_ulong pc,
                                      target_ulong cs_base,
                                      uint64_t flags)
{
    TranslationBlock *tb;
    ...
    /* if no translated code available, then translate it now */
    tb = tb_gen_code(cpu, pc, cs_base, flags, 0); [*18]

    /* we add the TB in the virtual pc hash table */
    cpu->tb_jmp_cache[tb_jmp_cache_hash_func(pc)] = tb; [*19]
    return tb;
}

[*18]でTranslationBlock(以下TBと呼びます)を作成し、[*19]でキャッシュにためます。ではこのtb_gen_codeを見てみます。

(translate-all.c)

TranslationBlock *tb_gen_code(CPUState *cpu,
                              target_ulong pc, target_ulong cs_base,
                              int flags, int cflags)
{
    CPUArchState *env = cpu->env_ptr;
    TranslationBlock *tb;
    tb_page_addr_t phys_pc, phys_page2;
    target_ulong virt_page2;
    tcg_insn_unit *gen_code_buf;
    int gen_code_size, search_size;
    ...
    phys_pc = get_page_addr_code(env, pc);[*20]
    if (use_icount) {
        cflags |= CF_USE_ICOUNT;
    }

    tb = tb_alloc(pc); [*21]
    ...
    gen_code_buf = tcg_ctx.code_gen_ptr;
    tb->tc_ptr = gen_code_buf;
    tb->cs_base = cs_base;
    tb->flags = flags;
    tb->cflags = cflags;

    gen_intermediate_code(env, tb); [*22]
    ...
    tcg_ctx.tb_jmp_offset = NULL;
    tcg_ctx.tb_next = tb->tb_next;
    ...
    tcg_ctx.code_gen_ptr = (void *)
        ROUND_UP((uintptr_t)gen_code_buf + gen_code_size + search_size,
                 CODE_GEN_ALIGN);
    ...
    gen_code_size = tcg_gen_code(&tcg_ctx, gen_code_buf); [*34]
    ....
    /* check next page if needed */
    virt_page2 = (pc + tb->size - 1) & TARGET_PAGE_MASK;
    phys_page2 = -1;
    if ((pc & TARGET_PAGE_MASK) != virt_page2) {
        phys_page2 = get_page_addr_code(env, virt_page2);
    }
    tb_link_page(tb, phys_pc, phys_page2);
    return tb;
}

[*20]では前回のSoftMMUで見たget_page_addr_codeを使ってpcをHVAに変換しています。
[*21]で新しいTBを確保した後、[*22]のgen_intermediate_codeで実際にマイクロコードを作成してTBに格納しています。

(target-i386/translate.c: gen_intermediate_code)

void gen_intermediate_code(CPUX86State *env, TranslationBlock *tb)
{
    X86CPU *cpu = x86_env_get_cpu(env);
    CPUState *cs = CPU(cpu);
    DisasContext dc1, *dc = &dc1;
    target_ulong pc_ptr;
    uint64_t flags;
    target_ulong pc_start;
    target_ulong cs_base;
    int num_insns;
    int max_insns;
    ...
    cpu_T[0] = tcg_temp_new(); [*23]
    cpu_T[1] = tcg_temp_new();
    cpu_A0 = tcg_temp_new();
    ...
    for(;;) {
        pc_ptr = disas_insn(env, dc, pc_ptr); [*24]
        /* stop translation if indicated */
        if (dc->is_jmp) 
            break;
	...	
    }    
}    

[*23]ではマイクロコード中で使用するTCG用の汎用レジスタを二つ、アドレス格納用のレジスタを一つ作成しています。
[*24]でいよいよpc_ptr番地から始まるゲストのマシン語をマイクロコードに変換していきます。

(target-i386/translate.c: disas_insn)

/* convert one instruction. s->is_jmp is set if the translation must
   be stopped. Return the next pc value */
static target_ulong disas_insn(CPUX86State *env, DisasContext *s,
                               target_ulong pc_start)
{
    int b, prefixes;
    int shift;
    TCGMemOp ot, aflag, dflag;
    int modrm, reg, rm, mod, op, opreg, val;
    target_ulong next_eip, tval;
    int rex_w, rex_r;

    s->pc = pc_start;
    ...
    b = cpu_ldub_code(env, s->pc);
    s->pc++;
   /* now check op code */
 reswitch:
    switch(b) {
        ...
        case 0x8b: /* mov Ev, Gv */
	    ot = mo_b_d(b, dflag);
	    modrm = cpu_ldub_code(env, s->pc++); [*25]
	    reg = ((modrm >> 3) & 7) | rex_r;

	    gen_ldst_modrm(env, s, modrm, ot, OR_TMP0, 0); [*26]
	    gen_op_mov_reg_v(ot, reg, cpu_T[0]); [*31]
	    break;
	...
	          
		 /* port I/O */
        case 0xe4:
        case 0xe5:
             ot = mo_b_d32(b, dflag);
             val = cpu_ldub_code(env, s->pc++);
             tcg_gen_movi_tl(cpu_T[0], val); 
             gen_check_io(s, ot, pc_start - s->cs_base,
                     SVM_IOIO_TYPE_MASK | svm_is_rep(prefixes)); 
             tcg_gen_movi_i32(cpu_tmp2_i32, val); [*32]
             gen_helper_in_func(ot, cpu_T[1], cpu_tmp2_i32); [*33]
             gen_op_mov_reg_v(ot, R_EAX, cpu_T[1]); [*34]
             break;
	...
    }    
    return s->pc;
}

エミュレータあるあるのとてつもなく長いswitch文です。あまりにも長いのでMOV命令とportI/Oで使用するIN命令のみピックアップします。
まずMOV r16/32 r/m16/32命令から見ていきましょう。[*25]でModR/Mバイトを取り出しています。ここにオペランドの情報が入っています。(バイナリアンな人なら日常的に見ているかもしれませんね)
次に[*26]においてtcgのload命令を作成しています。

(target-i386/translate.c: gen_ldst_modrm)

static void gen_ldst_modrm(CPUX86State *env, DisasContext *s, int modrm,
                           TCGMemOp ot, int reg, int is_store)
{
    int mod, rm;

    mod = (modrm >> 6) & 3;
    rm = (modrm & 7) | REX_B(s);
    ...
    else {
        gen_lea_modrm(env, s, modrm); [*27]
	...
        else {
            gen_op_ld_v(s, ot, cpu_T[0], cpu_A0); 
	    ...
        }
    }
}

is_storeはfalseであるため[*27]でcpu_A0にディスプレースメントなどを計算したメモリのアドレスを設定し、gen_op_ld_vが呼び出されます。
(target-i386/translate.c: gen_op_ld_v)

static inline void gen_op_ld_v(DisasContext *s, int idx, TCGv t0, TCGv a0)
{
    tcg_gen_qemu_ld_tl(t0, a0, s->mem_index, idx | MO_LE);
}

またtcg_gen_qemu_ld_tlは(tcg/tcg-op.h)においてtcg_gen_qemu_ld_i32の別名としてdefineされています。

(tcg/tcg-op.c)

static void gen_ldst_i32(TCGOpcode opc, TCGv_i32 val, TCGv addr,
                         TCGMemOp memop, TCGArg idx)
{
    TCGMemOpIdx oi = make_memop_idx(memop, idx);
    ...
    if (TCG_TARGET_REG_BITS == 32) {
        tcg_gen_op4i_i32(opc, val, TCGV_LOW(addr), TCGV_HIGH(addr), oi);
    } else {
        tcg_gen_op3(&tcg_ctx, opc, GET_TCGV_I32(val), GET_TCGV_I64(addr), oi);
    }
#endif
}
...
void tcg_gen_qemu_ld_i32(TCGv_i32 val, TCGv addr, TCGArg idx, TCGMemOp memop)
{
    memop = tcg_canonicalize_memop(memop, 0, 0);
    gen_ldst_i32(INDEX_op_qemu_ld_i32, val, addr, memop, idx);
}

tcg_gen_op4i_i32は(tcg/tcg-op.h)でtcg_genop4を呼び出しています。

(tcg/tcg-op.h)

static inline void tcg_gen_op4_i32(TCGOpcode opc, TCGv_i32 a1, TCGv_i32 a2,
                                   TCGv_i32 a3, TCGv_i32 a4)
{
    tcg_gen_op4(&tcg_ctx, opc, GET_TCGV_I32(a1), GET_TCGV_I32(a2),
                GET_TCGV_I32(a3), GET_TCGV_I32(a4));
}

(tcg/tcg-op.c: tcg_gen_op4)

void tcg_gen_op4(TCGContext *ctx, TCGOpcode opc, TCGArg a1,
                 TCGArg a2, TCGArg a3, TCGArg a4)
{
    int pi = ctx->gen_next_parm_idx;

    tcg_debug_assert(pi + 4 <= OPPARAM_BUF_SIZE);
    ctx->gen_next_parm_idx = pi + 4;
    ctx->gen_opparam_buf[pi + 0] = a1; [*28]
    ctx->gen_opparam_buf[pi + 1] = a2;
    ctx->gen_opparam_buf[pi + 2] = a3;
    ctx->gen_opparam_buf[pi + 3] = a4;

    tcg_emit_op(ctx, opc, pi); [*29]
}

[*28]で4つの引数をTCGContextに格納した後[*29]でtcg_emit_opを呼び出しgen_op_bufにTCGOpとして追加しています。[*30]

(tcg/tcg-op.c: tcg_emit_op)

static void tcg_emit_op(TCGContext *ctx, TCGOpcode opc, int args)
{
    int oi = ctx->gen_next_op_idx;
    int ni = oi + 1;
    int pi = oi - 1;

    ctx->gen_last_op_idx = oi;
    ctx->gen_next_op_idx = ni;

    ctx->gen_op_buf[oi] = (TCGOp){ [*30]
        .opc = opc,
        .args = args,
        .prev = pi,
        .next = ni
    }; 
}

追加されたTCGOpがどのようにホストのコードに変換されるかについては後ほど見ていきます。ではdisas_insnまで戻り、[*31]のgen_op_mov_reg_vではcpu_T[0]を宛先のregにmovする命令を生成しています。これも先程と同じように内部でtcg_emit_opを使い命令バッファにためています。

さてでは次にport IOのIN命令を見て行きましょう。
[*32]でcpu_T[0]にポート番号をmovしています。そして[*33]のgen_helper_in_funcからgen_helper_inlを呼び出しています。

static void gen_helper_in_func(TCGMemOp ot, TCGv v, TCGv_i32 n)
{
    switch (ot) {
    case MO_8:
        gen_helper_inb(v, cpu_env, n);
        break;
    case MO_16:
        gen_helper_inw(v, cpu_env, n);
        break;
    case MO_32:
        gen_helper_inl(v, cpu_env, n);
        break;
    default:
        tcg_abort();
    }
}

このgen_helper_inlはDEF_HELPER_2(include/exec/helper-head.h)によってDEF_HELPER_2(inl, tl, env, i32);(target-i386/helper.h)と定義されています。
DEF_HELPER_2は実質的には(include/exec/helper-gen.h)のDEF_HELPER_FLAGS_2であり、tcg_gen_callNを呼び出しています。

(include/exec/helper-gen.h: DEF_HELPER_FLAGS_2)

#define DEF_HELPER_FLAGS_2(name, flags, ret, t1, t2)                    \
static inline void glue(gen_helper_, name)(dh_retvar_decl(ret)          \
    dh_arg_decl(t1, 1), dh_arg_decl(t2, 2))                             \
{                                                                       \
  TCGArg args[2] = { dh_arg(t1, 1), dh_arg(t2, 2) };                    \
  tcg_gen_callN(&tcg_ctx, HELPER(name), dh_retvar(ret), 2, args);       \
}

このtcg_gen_callN(tcg/tcg.c)は詳細は省きますが第二引数で与えられた関数をcallするマイクロコードを生成します。ここではHELPERマクロ(include/exec/helper-head.h)を用いたHELPER(name)すなわちhelper_inl関数になります。ではこのhelper_inl関数は何をしているかというと

(target-i386/misc_helper.c: helper_inl)

target_ulong helper_inl(CPUX86State *env, uint32_t port)
{
    ...
    return address_space_ldl(&address_space_io, port,
                             cpu_get_mem_attrs(env), NULL);
}

address_space_ioのport番地の値を読みだしています。これは8章の仮想IOでも説明したようにMemory Mapped IOを使って外部デバイスからのデータを読んでいるわけです。x86におけるin/out命令は内部的にMMIOとして扱われているのです。

さてではtb_code_genに戻って[*34]のtcg_gen_codeを見て行きましょう。この関数が上で作成したマイクロコードを実際のホストのマシン語に変換します。

(tcg/tcg.c)

int tcg_gen_code(TCGContext *s, tcg_insn_unit *gen_code_buf)
{
	
    ...
    for (oi = s->gen_first_op_idx; oi >= 0; oi = oi_next) {
        TCGOp * const op = &s->gen_op_buf[oi];
        TCGArg * const args = &s->gen_opparam_buf[op->args];
        TCGOpcode opc = op->opc;
	/* tcg_op_defs is defition of TCG operations(see tcg/tcg-opc.h) */
        const TCGOpDef *def = &tcg_op_defs[opc];
        uint16_t dead_args = s->op_dead_args[oi];
        uint8_t sync_args = s->op_sync_args[oi];

        switch (opc) {
        case INDEX_op_mov_i32:
        case INDEX_op_mov_i64:
            tcg_reg_alloc_mov(s, def, args, dead_args, sync_args);
            break;
        case INDEX_op_movi_i32:
        case INDEX_op_movi_i64:
            tcg_reg_alloc_movi(s, args, dead_args, sync_args);
            break;
	...
        default:
            tcg_reg_alloc_op(s, def, opc, args, dead_args, sync_args); [*35]
            break;
        }
	...
    }

    return tcg_current_code_size(s);
}

[*35]でTCGのオペコードから実際のホストの命令列を生成しています。

static void tcg_reg_alloc_op(TCGContext *s, 
                             const TCGOpDef *def, TCGOpcode opc,
                             const TCGArg *args, uint16_t dead_args,
                             uint8_t sync_args)
{
   for(k = 0; k < nb_iargs; k++) {
        i = def->sorted_args[nb_oargs + k];
        arg = args[i];
        arg_ct = &def->args_ct[i];
        ts = &s->temps[arg];
	...
	else if (ts->val_type == TEMP_VAL_CONST) {
            else {
                reg = tcg_reg_alloc(s, arg_ct->u.regs, allocated_regs);
                tcg_out_movi(s, ts->type, reg, ts->val); [*36]
                ts->val_type = TEMP_VAL_REG;
                ts->reg = reg;
                ts->mem_coherent = 0;
                s->reg_to_temp[reg] = arg;
            }
        }
    }
}

[*36]のtcg_out_moviで各ホストごとのmov命令をtcg_out関数(tcg/tcg.c)を使って書き出しています。(tcg/i386/tcg-targe.c)

static __attribute__((unused)) inline void tcg_out32(TCGContext *s, uint32_t v)
{
    if (TCG_TARGET_INSN_UNIT_SIZE == 4) {
        *s->code_ptr++ = v;
    } else {
        tcg_insn_unit *p = s->code_ptr;
        memcpy(p, &v, sizeof(v));
        s->code_ptr = p + (4 / TCG_TARGET_INSN_UNIT_SIZE);
    }
}

これら書きだしたホスト命令列は[*6]のcpu_tb_execによって実行が開始されます。
cpu_tb_exec(cpu-exec.c)はtcg_qemu_tb_exec(tcg/tcg.h)を呼び出します。

(tcg/tcg.h: tcg_qemu_tb_exec)

# define tcg_qemu_tb_exec(env, tb_ptr) \
    ((uintptr_t (*)(void *, void *))tcg_ctx.code_gen_prologue)(env, tb_ptr)

これはTCGContextのcode_gen_prologueを関数ポインタと解釈し呼び出しています。
このcode_gen_prologueはTCGにおけるプロローグを表しており、(accel.c)のtcg_initによって初期化されています。tcg_initはtcg_exec_initを呼び出します。


(translate-all.c: tcg_exec_init)

void tcg_exec_init(unsigned long tb_size)
{
    cpu_gen_init();
    page_init();
    code_gen_alloc(tb_size);
#if defined(CONFIG_SOFTMMU)
    tcg_prologue_init(&tcg_ctx); [*37]
#endif
}

[*37]がcode_gen_prologueの初期化関数です。

(tcg/tcg.c)

void tcg_prologue_init(TCGContext *s)
{
    size_t prologue_size, total_size;
    void *buf0, *buf1;

    /* Put the prologue at the beginning of code_gen_buffer.  */
    buf0 = s->code_gen_buffer;
    s->code_ptr = buf0; [*38]
    s->code_buf = buf0;
    s->code_gen_prologue = buf0;

    /* Generate the prologue.  */
    tcg_target_qemu_prologue(s); [*39]
    ...
}

[*38]でcode_ptrすなわち現在処理中の命令列のポインタをプロローグに設定しています。
[*39]のtcg_target_qemu_prologue(s)(tcg/tcg.c)でプロローグ,call TB,エピローグをcode_gen_prologueに書き込んでおり、これでTBにジャンプする命令ができました。


これでエミュレーションにおける基本的な要素は殆ど全て説明しました。

10.おわりに
part1から続きTCG,SoftMMU,IO,IRQ,QOMと見てきました。
ご存知の方は多いと思いますが、QEMUには他にもQEMUTimer,BH,vfio,ライブマイグレーション,virtio,QMP,TCI,JITコンパイラなどまだまだ数えきれない程の機能があります。
ですが本記事で見てきた基本的な所を抑えておけばその辺りを読むのも難しくはないと思います。興味のある方はどんどんhackしてみてください。

QEMU-internalはここまでです(多分)
読んでくださって有難うございました!