| /* |
| * Copyright (c) 2018-2019 Maxime Villard, All rights reserved. |
| * |
| * NetBSD Virtual Machine Monitor (NVMM) accelerator for QEMU. |
| * |
| * This work is licensed under the terms of the GNU GPL, version 2 or later. |
| * See the COPYING file in the top-level directory. |
| */ |
| |
| #include "qemu/osdep.h" |
| #include "cpu.h" |
| #include "exec/address-spaces.h" |
| #include "exec/ioport.h" |
| #include "qemu/accel.h" |
| #include "sysemu/nvmm.h" |
| #include "sysemu/cpus.h" |
| #include "sysemu/runstate.h" |
| #include "qemu/main-loop.h" |
| #include "qemu/error-report.h" |
| #include "qapi/error.h" |
| #include "qemu/queue.h" |
| #include "migration/blocker.h" |
| #include "strings.h" |
| |
| #include "nvmm-accel-ops.h" |
| |
| #include <nvmm.h> |
| |
| struct AccelCPUState { |
| struct nvmm_vcpu vcpu; |
| uint8_t tpr; |
| bool stop; |
| |
| /* Window-exiting for INTs/NMIs. */ |
| bool int_window_exit; |
| bool nmi_window_exit; |
| |
| /* The guest is in an interrupt shadow (POP SS, etc). */ |
| bool int_shadow; |
| }; |
| |
| struct qemu_machine { |
| struct nvmm_capability cap; |
| struct nvmm_machine mach; |
| }; |
| |
| /* -------------------------------------------------------------------------- */ |
| |
| static bool nvmm_allowed; |
| static struct qemu_machine qemu_mach; |
| |
| static struct nvmm_machine * |
| get_nvmm_mach(void) |
| { |
| return &qemu_mach.mach; |
| } |
| |
| /* -------------------------------------------------------------------------- */ |
| |
| static void |
| nvmm_set_segment(struct nvmm_x64_state_seg *nseg, const SegmentCache *qseg) |
| { |
| uint32_t attrib = qseg->flags; |
| |
| nseg->selector = qseg->selector; |
| nseg->limit = qseg->limit; |
| nseg->base = qseg->base; |
| nseg->attrib.type = __SHIFTOUT(attrib, DESC_TYPE_MASK); |
| nseg->attrib.s = __SHIFTOUT(attrib, DESC_S_MASK); |
| nseg->attrib.dpl = __SHIFTOUT(attrib, DESC_DPL_MASK); |
| nseg->attrib.p = __SHIFTOUT(attrib, DESC_P_MASK); |
| nseg->attrib.avl = __SHIFTOUT(attrib, DESC_AVL_MASK); |
| nseg->attrib.l = __SHIFTOUT(attrib, DESC_L_MASK); |
| nseg->attrib.def = __SHIFTOUT(attrib, DESC_B_MASK); |
| nseg->attrib.g = __SHIFTOUT(attrib, DESC_G_MASK); |
| } |
| |
| static void |
| nvmm_set_registers(CPUState *cpu) |
| { |
| CPUX86State *env = cpu_env(cpu); |
| struct nvmm_machine *mach = get_nvmm_mach(); |
| AccelCPUState *qcpu = cpu->accel; |
| struct nvmm_vcpu *vcpu = &qcpu->vcpu; |
| struct nvmm_x64_state *state = vcpu->state; |
| uint64_t bitmap; |
| size_t i; |
| int ret; |
| |
| assert(cpu_is_stopped(cpu) || qemu_cpu_is_self(cpu)); |
| |
| /* GPRs. */ |
| state->gprs[NVMM_X64_GPR_RAX] = env->regs[R_EAX]; |
| state->gprs[NVMM_X64_GPR_RCX] = env->regs[R_ECX]; |
| state->gprs[NVMM_X64_GPR_RDX] = env->regs[R_EDX]; |
| state->gprs[NVMM_X64_GPR_RBX] = env->regs[R_EBX]; |
| state->gprs[NVMM_X64_GPR_RSP] = env->regs[R_ESP]; |
| state->gprs[NVMM_X64_GPR_RBP] = env->regs[R_EBP]; |
| state->gprs[NVMM_X64_GPR_RSI] = env->regs[R_ESI]; |
| state->gprs[NVMM_X64_GPR_RDI] = env->regs[R_EDI]; |
| #ifdef TARGET_X86_64 |
| state->gprs[NVMM_X64_GPR_R8] = env->regs[R_R8]; |
| state->gprs[NVMM_X64_GPR_R9] = env->regs[R_R9]; |
| state->gprs[NVMM_X64_GPR_R10] = env->regs[R_R10]; |
| state->gprs[NVMM_X64_GPR_R11] = env->regs[R_R11]; |
| state->gprs[NVMM_X64_GPR_R12] = env->regs[R_R12]; |
| state->gprs[NVMM_X64_GPR_R13] = env->regs[R_R13]; |
| state->gprs[NVMM_X64_GPR_R14] = env->regs[R_R14]; |
| state->gprs[NVMM_X64_GPR_R15] = env->regs[R_R15]; |
| #endif |
| |
| /* RIP and RFLAGS. */ |
| state->gprs[NVMM_X64_GPR_RIP] = env->eip; |
| state->gprs[NVMM_X64_GPR_RFLAGS] = env->eflags; |
| |
| /* Segments. */ |
| nvmm_set_segment(&state->segs[NVMM_X64_SEG_CS], &env->segs[R_CS]); |
| nvmm_set_segment(&state->segs[NVMM_X64_SEG_DS], &env->segs[R_DS]); |
| nvmm_set_segment(&state->segs[NVMM_X64_SEG_ES], &env->segs[R_ES]); |
| nvmm_set_segment(&state->segs[NVMM_X64_SEG_FS], &env->segs[R_FS]); |
| nvmm_set_segment(&state->segs[NVMM_X64_SEG_GS], &env->segs[R_GS]); |
| nvmm_set_segment(&state->segs[NVMM_X64_SEG_SS], &env->segs[R_SS]); |
| |
| /* Special segments. */ |
| nvmm_set_segment(&state->segs[NVMM_X64_SEG_GDT], &env->gdt); |
| nvmm_set_segment(&state->segs[NVMM_X64_SEG_LDT], &env->ldt); |
| nvmm_set_segment(&state->segs[NVMM_X64_SEG_TR], &env->tr); |
| nvmm_set_segment(&state->segs[NVMM_X64_SEG_IDT], &env->idt); |
| |
| /* Control registers. */ |
| state->crs[NVMM_X64_CR_CR0] = env->cr[0]; |
| state->crs[NVMM_X64_CR_CR2] = env->cr[2]; |
| state->crs[NVMM_X64_CR_CR3] = env->cr[3]; |
| state->crs[NVMM_X64_CR_CR4] = env->cr[4]; |
| state->crs[NVMM_X64_CR_CR8] = qcpu->tpr; |
| state->crs[NVMM_X64_CR_XCR0] = env->xcr0; |
| |
| /* Debug registers. */ |
| state->drs[NVMM_X64_DR_DR0] = env->dr[0]; |
| state->drs[NVMM_X64_DR_DR1] = env->dr[1]; |
| state->drs[NVMM_X64_DR_DR2] = env->dr[2]; |
| state->drs[NVMM_X64_DR_DR3] = env->dr[3]; |
| state->drs[NVMM_X64_DR_DR6] = env->dr[6]; |
| state->drs[NVMM_X64_DR_DR7] = env->dr[7]; |
| |
| /* FPU. */ |
| state->fpu.fx_cw = env->fpuc; |
| state->fpu.fx_sw = (env->fpus & ~0x3800) | ((env->fpstt & 0x7) << 11); |
| state->fpu.fx_tw = 0; |
| for (i = 0; i < 8; i++) { |
| state->fpu.fx_tw |= (!env->fptags[i]) << i; |
| } |
| state->fpu.fx_opcode = env->fpop; |
| state->fpu.fx_ip.fa_64 = env->fpip; |
| state->fpu.fx_dp.fa_64 = env->fpdp; |
| state->fpu.fx_mxcsr = env->mxcsr; |
| state->fpu.fx_mxcsr_mask = 0x0000FFFF; |
| assert(sizeof(state->fpu.fx_87_ac) == sizeof(env->fpregs)); |
| memcpy(state->fpu.fx_87_ac, env->fpregs, sizeof(env->fpregs)); |
| for (i = 0; i < CPU_NB_REGS; i++) { |
| memcpy(&state->fpu.fx_xmm[i].xmm_bytes[0], |
| &env->xmm_regs[i].ZMM_Q(0), 8); |
| memcpy(&state->fpu.fx_xmm[i].xmm_bytes[8], |
| &env->xmm_regs[i].ZMM_Q(1), 8); |
| } |
| |
| /* MSRs. */ |
| state->msrs[NVMM_X64_MSR_EFER] = env->efer; |
| state->msrs[NVMM_X64_MSR_STAR] = env->star; |
| #ifdef TARGET_X86_64 |
| state->msrs[NVMM_X64_MSR_LSTAR] = env->lstar; |
| state->msrs[NVMM_X64_MSR_CSTAR] = env->cstar; |
| state->msrs[NVMM_X64_MSR_SFMASK] = env->fmask; |
| state->msrs[NVMM_X64_MSR_KERNELGSBASE] = env->kernelgsbase; |
| #endif |
| state->msrs[NVMM_X64_MSR_SYSENTER_CS] = env->sysenter_cs; |
| state->msrs[NVMM_X64_MSR_SYSENTER_ESP] = env->sysenter_esp; |
| state->msrs[NVMM_X64_MSR_SYSENTER_EIP] = env->sysenter_eip; |
| state->msrs[NVMM_X64_MSR_PAT] = env->pat; |
| state->msrs[NVMM_X64_MSR_TSC] = env->tsc; |
| |
| bitmap = |
| NVMM_X64_STATE_SEGS | |
| NVMM_X64_STATE_GPRS | |
| NVMM_X64_STATE_CRS | |
| NVMM_X64_STATE_DRS | |
| NVMM_X64_STATE_MSRS | |
| NVMM_X64_STATE_FPU; |
| |
| ret = nvmm_vcpu_setstate(mach, vcpu, bitmap); |
| if (ret == -1) { |
| error_report("NVMM: Failed to set virtual processor context," |
| " error=%d", errno); |
| } |
| } |
| |
| static void |
| nvmm_get_segment(SegmentCache *qseg, const struct nvmm_x64_state_seg *nseg) |
| { |
| qseg->selector = nseg->selector; |
| qseg->limit = nseg->limit; |
| qseg->base = nseg->base; |
| |
| qseg->flags = |
| __SHIFTIN((uint32_t)nseg->attrib.type, DESC_TYPE_MASK) | |
| __SHIFTIN((uint32_t)nseg->attrib.s, DESC_S_MASK) | |
| __SHIFTIN((uint32_t)nseg->attrib.dpl, DESC_DPL_MASK) | |
| __SHIFTIN((uint32_t)nseg->attrib.p, DESC_P_MASK) | |
| __SHIFTIN((uint32_t)nseg->attrib.avl, DESC_AVL_MASK) | |
| __SHIFTIN((uint32_t)nseg->attrib.l, DESC_L_MASK) | |
| __SHIFTIN((uint32_t)nseg->attrib.def, DESC_B_MASK) | |
| __SHIFTIN((uint32_t)nseg->attrib.g, DESC_G_MASK); |
| } |
| |
| static void |
| nvmm_get_registers(CPUState *cpu) |
| { |
| CPUX86State *env = cpu_env(cpu); |
| struct nvmm_machine *mach = get_nvmm_mach(); |
| AccelCPUState *qcpu = cpu->accel; |
| struct nvmm_vcpu *vcpu = &qcpu->vcpu; |
| X86CPU *x86_cpu = X86_CPU(cpu); |
| struct nvmm_x64_state *state = vcpu->state; |
| uint64_t bitmap, tpr; |
| size_t i; |
| int ret; |
| |
| assert(cpu_is_stopped(cpu) || qemu_cpu_is_self(cpu)); |
| |
| bitmap = |
| NVMM_X64_STATE_SEGS | |
| NVMM_X64_STATE_GPRS | |
| NVMM_X64_STATE_CRS | |
| NVMM_X64_STATE_DRS | |
| NVMM_X64_STATE_MSRS | |
| NVMM_X64_STATE_FPU; |
| |
| ret = nvmm_vcpu_getstate(mach, vcpu, bitmap); |
| if (ret == -1) { |
| error_report("NVMM: Failed to get virtual processor context," |
| " error=%d", errno); |
| } |
| |
| /* GPRs. */ |
| env->regs[R_EAX] = state->gprs[NVMM_X64_GPR_RAX]; |
| env->regs[R_ECX] = state->gprs[NVMM_X64_GPR_RCX]; |
| env->regs[R_EDX] = state->gprs[NVMM_X64_GPR_RDX]; |
| env->regs[R_EBX] = state->gprs[NVMM_X64_GPR_RBX]; |
| env->regs[R_ESP] = state->gprs[NVMM_X64_GPR_RSP]; |
| env->regs[R_EBP] = state->gprs[NVMM_X64_GPR_RBP]; |
| env->regs[R_ESI] = state->gprs[NVMM_X64_GPR_RSI]; |
| env->regs[R_EDI] = state->gprs[NVMM_X64_GPR_RDI]; |
| #ifdef TARGET_X86_64 |
| env->regs[R_R8] = state->gprs[NVMM_X64_GPR_R8]; |
| env->regs[R_R9] = state->gprs[NVMM_X64_GPR_R9]; |
| env->regs[R_R10] = state->gprs[NVMM_X64_GPR_R10]; |
| env->regs[R_R11] = state->gprs[NVMM_X64_GPR_R11]; |
| env->regs[R_R12] = state->gprs[NVMM_X64_GPR_R12]; |
| env->regs[R_R13] = state->gprs[NVMM_X64_GPR_R13]; |
| env->regs[R_R14] = state->gprs[NVMM_X64_GPR_R14]; |
| env->regs[R_R15] = state->gprs[NVMM_X64_GPR_R15]; |
| #endif |
| |
| /* RIP and RFLAGS. */ |
| env->eip = state->gprs[NVMM_X64_GPR_RIP]; |
| env->eflags = state->gprs[NVMM_X64_GPR_RFLAGS]; |
| |
| /* Segments. */ |
| nvmm_get_segment(&env->segs[R_ES], &state->segs[NVMM_X64_SEG_ES]); |
| nvmm_get_segment(&env->segs[R_CS], &state->segs[NVMM_X64_SEG_CS]); |
| nvmm_get_segment(&env->segs[R_SS], &state->segs[NVMM_X64_SEG_SS]); |
| nvmm_get_segment(&env->segs[R_DS], &state->segs[NVMM_X64_SEG_DS]); |
| nvmm_get_segment(&env->segs[R_FS], &state->segs[NVMM_X64_SEG_FS]); |
| nvmm_get_segment(&env->segs[R_GS], &state->segs[NVMM_X64_SEG_GS]); |
| |
| /* Special segments. */ |
| nvmm_get_segment(&env->gdt, &state->segs[NVMM_X64_SEG_GDT]); |
| nvmm_get_segment(&env->ldt, &state->segs[NVMM_X64_SEG_LDT]); |
| nvmm_get_segment(&env->tr, &state->segs[NVMM_X64_SEG_TR]); |
| nvmm_get_segment(&env->idt, &state->segs[NVMM_X64_SEG_IDT]); |
| |
| /* Control registers. */ |
| env->cr[0] = state->crs[NVMM_X64_CR_CR0]; |
| env->cr[2] = state->crs[NVMM_X64_CR_CR2]; |
| env->cr[3] = state->crs[NVMM_X64_CR_CR3]; |
| env->cr[4] = state->crs[NVMM_X64_CR_CR4]; |
| tpr = state->crs[NVMM_X64_CR_CR8]; |
| if (tpr != qcpu->tpr) { |
| qcpu->tpr = tpr; |
| cpu_set_apic_tpr(x86_cpu->apic_state, tpr); |
| } |
| env->xcr0 = state->crs[NVMM_X64_CR_XCR0]; |
| |
| /* Debug registers. */ |
| env->dr[0] = state->drs[NVMM_X64_DR_DR0]; |
| env->dr[1] = state->drs[NVMM_X64_DR_DR1]; |
| env->dr[2] = state->drs[NVMM_X64_DR_DR2]; |
| env->dr[3] = state->drs[NVMM_X64_DR_DR3]; |
| env->dr[6] = state->drs[NVMM_X64_DR_DR6]; |
| env->dr[7] = state->drs[NVMM_X64_DR_DR7]; |
| |
| /* FPU. */ |
| env->fpuc = state->fpu.fx_cw; |
| env->fpstt = (state->fpu.fx_sw >> 11) & 0x7; |
| env->fpus = state->fpu.fx_sw & ~0x3800; |
| for (i = 0; i < 8; i++) { |
| env->fptags[i] = !((state->fpu.fx_tw >> i) & 1); |
| } |
| env->fpop = state->fpu.fx_opcode; |
| env->fpip = state->fpu.fx_ip.fa_64; |
| env->fpdp = state->fpu.fx_dp.fa_64; |
| env->mxcsr = state->fpu.fx_mxcsr; |
| assert(sizeof(state->fpu.fx_87_ac) == sizeof(env->fpregs)); |
| memcpy(env->fpregs, state->fpu.fx_87_ac, sizeof(env->fpregs)); |
| for (i = 0; i < CPU_NB_REGS; i++) { |
| memcpy(&env->xmm_regs[i].ZMM_Q(0), |
| &state->fpu.fx_xmm[i].xmm_bytes[0], 8); |
| memcpy(&env->xmm_regs[i].ZMM_Q(1), |
| &state->fpu.fx_xmm[i].xmm_bytes[8], 8); |
| } |
| |
| /* MSRs. */ |
| env->efer = state->msrs[NVMM_X64_MSR_EFER]; |
| env->star = state->msrs[NVMM_X64_MSR_STAR]; |
| #ifdef TARGET_X86_64 |
| env->lstar = state->msrs[NVMM_X64_MSR_LSTAR]; |
| env->cstar = state->msrs[NVMM_X64_MSR_CSTAR]; |
| env->fmask = state->msrs[NVMM_X64_MSR_SFMASK]; |
| env->kernelgsbase = state->msrs[NVMM_X64_MSR_KERNELGSBASE]; |
| #endif |
| env->sysenter_cs = state->msrs[NVMM_X64_MSR_SYSENTER_CS]; |
| env->sysenter_esp = state->msrs[NVMM_X64_MSR_SYSENTER_ESP]; |
| env->sysenter_eip = state->msrs[NVMM_X64_MSR_SYSENTER_EIP]; |
| env->pat = state->msrs[NVMM_X64_MSR_PAT]; |
| env->tsc = state->msrs[NVMM_X64_MSR_TSC]; |
| |
| x86_update_hflags(env); |
| } |
| |
| static bool |
| nvmm_can_take_int(CPUState *cpu) |
| { |
| CPUX86State *env = cpu_env(cpu); |
| AccelCPUState *qcpu = cpu->accel; |
| struct nvmm_vcpu *vcpu = &qcpu->vcpu; |
| struct nvmm_machine *mach = get_nvmm_mach(); |
| |
| if (qcpu->int_window_exit) { |
| return false; |
| } |
| |
| if (qcpu->int_shadow || !(env->eflags & IF_MASK)) { |
| struct nvmm_x64_state *state = vcpu->state; |
| |
| /* Exit on interrupt window. */ |
| nvmm_vcpu_getstate(mach, vcpu, NVMM_X64_STATE_INTR); |
| state->intr.int_window_exiting = 1; |
| nvmm_vcpu_setstate(mach, vcpu, NVMM_X64_STATE_INTR); |
| |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static bool |
| nvmm_can_take_nmi(CPUState *cpu) |
| { |
| AccelCPUState *qcpu = cpu->accel; |
| |
| /* |
| * Contrary to INTs, NMIs always schedule an exit when they are |
| * completed. Therefore, if window-exiting is enabled, it means |
| * NMIs are blocked. |
| */ |
| if (qcpu->nmi_window_exit) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /* |
| * Called before the VCPU is run. We inject events generated by the I/O |
| * thread, and synchronize the guest TPR. |
| */ |
| static void |
| nvmm_vcpu_pre_run(CPUState *cpu) |
| { |
| CPUX86State *env = cpu_env(cpu); |
| struct nvmm_machine *mach = get_nvmm_mach(); |
| AccelCPUState *qcpu = cpu->accel; |
| struct nvmm_vcpu *vcpu = &qcpu->vcpu; |
| X86CPU *x86_cpu = X86_CPU(cpu); |
| struct nvmm_x64_state *state = vcpu->state; |
| struct nvmm_vcpu_event *event = vcpu->event; |
| bool has_event = false; |
| bool sync_tpr = false; |
| uint8_t tpr; |
| int ret; |
| |
| qemu_mutex_lock_iothread(); |
| |
| tpr = cpu_get_apic_tpr(x86_cpu->apic_state); |
| if (tpr != qcpu->tpr) { |
| qcpu->tpr = tpr; |
| sync_tpr = true; |
| } |
| |
| /* |
| * Force the VCPU out of its inner loop to process any INIT requests |
| * or commit pending TPR access. |
| */ |
| if (cpu->interrupt_request & (CPU_INTERRUPT_INIT | CPU_INTERRUPT_TPR)) { |
| cpu->exit_request = 1; |
| } |
| |
| if (!has_event && (cpu->interrupt_request & CPU_INTERRUPT_NMI)) { |
| if (nvmm_can_take_nmi(cpu)) { |
| cpu->interrupt_request &= ~CPU_INTERRUPT_NMI; |
| event->type = NVMM_VCPU_EVENT_INTR; |
| event->vector = 2; |
| has_event = true; |
| } |
| } |
| |
| if (!has_event && (cpu->interrupt_request & CPU_INTERRUPT_HARD)) { |
| if (nvmm_can_take_int(cpu)) { |
| cpu->interrupt_request &= ~CPU_INTERRUPT_HARD; |
| event->type = NVMM_VCPU_EVENT_INTR; |
| event->vector = cpu_get_pic_interrupt(env); |
| has_event = true; |
| } |
| } |
| |
| /* Don't want SMIs. */ |
| if (cpu->interrupt_request & CPU_INTERRUPT_SMI) { |
| cpu->interrupt_request &= ~CPU_INTERRUPT_SMI; |
| } |
| |
| if (sync_tpr) { |
| ret = nvmm_vcpu_getstate(mach, vcpu, NVMM_X64_STATE_CRS); |
| if (ret == -1) { |
| error_report("NVMM: Failed to get CPU state," |
| " error=%d", errno); |
| } |
| |
| state->crs[NVMM_X64_CR_CR8] = qcpu->tpr; |
| |
| ret = nvmm_vcpu_setstate(mach, vcpu, NVMM_X64_STATE_CRS); |
| if (ret == -1) { |
| error_report("NVMM: Failed to set CPU state," |
| " error=%d", errno); |
| } |
| } |
| |
| if (has_event) { |
| ret = nvmm_vcpu_inject(mach, vcpu); |
| if (ret == -1) { |
| error_report("NVMM: Failed to inject event," |
| " error=%d", errno); |
| } |
| } |
| |
| qemu_mutex_unlock_iothread(); |
| } |
| |
| /* |
| * Called after the VCPU ran. We synchronize the host view of the TPR and |
| * RFLAGS. |
| */ |
| static void |
| nvmm_vcpu_post_run(CPUState *cpu, struct nvmm_vcpu_exit *exit) |
| { |
| AccelCPUState *qcpu = cpu->accel; |
| X86CPU *x86_cpu = X86_CPU(cpu); |
| CPUX86State *env = &x86_cpu->env; |
| uint64_t tpr; |
| |
| env->eflags = exit->exitstate.rflags; |
| qcpu->int_shadow = exit->exitstate.int_shadow; |
| qcpu->int_window_exit = exit->exitstate.int_window_exiting; |
| qcpu->nmi_window_exit = exit->exitstate.nmi_window_exiting; |
| |
| tpr = exit->exitstate.cr8; |
| if (qcpu->tpr != tpr) { |
| qcpu->tpr = tpr; |
| qemu_mutex_lock_iothread(); |
| cpu_set_apic_tpr(x86_cpu->apic_state, qcpu->tpr); |
| qemu_mutex_unlock_iothread(); |
| } |
| } |
| |
| /* -------------------------------------------------------------------------- */ |
| |
| static void |
| nvmm_io_callback(struct nvmm_io *io) |
| { |
| MemTxAttrs attrs = { 0 }; |
| int ret; |
| |
| ret = address_space_rw(&address_space_io, io->port, attrs, io->data, |
| io->size, !io->in); |
| if (ret != MEMTX_OK) { |
| error_report("NVMM: I/O Transaction Failed " |
| "[%s, port=%u, size=%zu]", (io->in ? "in" : "out"), |
| io->port, io->size); |
| } |
| |
| /* Needed, otherwise infinite loop. */ |
| current_cpu->vcpu_dirty = false; |
| } |
| |
| static void |
| nvmm_mem_callback(struct nvmm_mem *mem) |
| { |
| cpu_physical_memory_rw(mem->gpa, mem->data, mem->size, mem->write); |
| |
| /* Needed, otherwise infinite loop. */ |
| current_cpu->vcpu_dirty = false; |
| } |
| |
| static struct nvmm_assist_callbacks nvmm_callbacks = { |
| .io = nvmm_io_callback, |
| .mem = nvmm_mem_callback |
| }; |
| |
| /* -------------------------------------------------------------------------- */ |
| |
| static int |
| nvmm_handle_mem(struct nvmm_machine *mach, struct nvmm_vcpu *vcpu) |
| { |
| int ret; |
| |
| ret = nvmm_assist_mem(mach, vcpu); |
| if (ret == -1) { |
| error_report("NVMM: Mem Assist Failed [gpa=%p]", |
| (void *)vcpu->exit->u.mem.gpa); |
| } |
| |
| return ret; |
| } |
| |
| static int |
| nvmm_handle_io(struct nvmm_machine *mach, struct nvmm_vcpu *vcpu) |
| { |
| int ret; |
| |
| ret = nvmm_assist_io(mach, vcpu); |
| if (ret == -1) { |
| error_report("NVMM: I/O Assist Failed [port=%d]", |
| (int)vcpu->exit->u.io.port); |
| } |
| |
| return ret; |
| } |
| |
| static int |
| nvmm_handle_rdmsr(struct nvmm_machine *mach, CPUState *cpu, |
| struct nvmm_vcpu_exit *exit) |
| { |
| AccelCPUState *qcpu = cpu->accel; |
| struct nvmm_vcpu *vcpu = &qcpu->vcpu; |
| X86CPU *x86_cpu = X86_CPU(cpu); |
| struct nvmm_x64_state *state = vcpu->state; |
| uint64_t val; |
| int ret; |
| |
| switch (exit->u.rdmsr.msr) { |
| case MSR_IA32_APICBASE: |
| val = cpu_get_apic_base(x86_cpu->apic_state); |
| break; |
| case MSR_MTRRcap: |
| case MSR_MTRRdefType: |
| case MSR_MCG_CAP: |
| case MSR_MCG_STATUS: |
| val = 0; |
| break; |
| default: /* More MSRs to add? */ |
| val = 0; |
| error_report("NVMM: Unexpected RDMSR 0x%x, ignored", |
| exit->u.rdmsr.msr); |
| break; |
| } |
| |
| ret = nvmm_vcpu_getstate(mach, vcpu, NVMM_X64_STATE_GPRS); |
| if (ret == -1) { |
| return -1; |
| } |
| |
| state->gprs[NVMM_X64_GPR_RAX] = (val & 0xFFFFFFFF); |
| state->gprs[NVMM_X64_GPR_RDX] = (val >> 32); |
| state->gprs[NVMM_X64_GPR_RIP] = exit->u.rdmsr.npc; |
| |
| ret = nvmm_vcpu_setstate(mach, vcpu, NVMM_X64_STATE_GPRS); |
| if (ret == -1) { |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| static int |
| nvmm_handle_wrmsr(struct nvmm_machine *mach, CPUState *cpu, |
| struct nvmm_vcpu_exit *exit) |
| { |
| AccelCPUState *qcpu = cpu->accel; |
| struct nvmm_vcpu *vcpu = &qcpu->vcpu; |
| X86CPU *x86_cpu = X86_CPU(cpu); |
| struct nvmm_x64_state *state = vcpu->state; |
| uint64_t val; |
| int ret; |
| |
| val = exit->u.wrmsr.val; |
| |
| switch (exit->u.wrmsr.msr) { |
| case MSR_IA32_APICBASE: |
| cpu_set_apic_base(x86_cpu->apic_state, val); |
| break; |
| case MSR_MTRRdefType: |
| case MSR_MCG_STATUS: |
| break; |
| default: /* More MSRs to add? */ |
| error_report("NVMM: Unexpected WRMSR 0x%x [val=0x%lx], ignored", |
| exit->u.wrmsr.msr, val); |
| break; |
| } |
| |
| ret = nvmm_vcpu_getstate(mach, vcpu, NVMM_X64_STATE_GPRS); |
| if (ret == -1) { |
| return -1; |
| } |
| |
| state->gprs[NVMM_X64_GPR_RIP] = exit->u.wrmsr.npc; |
| |
| ret = nvmm_vcpu_setstate(mach, vcpu, NVMM_X64_STATE_GPRS); |
| if (ret == -1) { |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| static int |
| nvmm_handle_halted(struct nvmm_machine *mach, CPUState *cpu, |
| struct nvmm_vcpu_exit *exit) |
| { |
| CPUX86State *env = cpu_env(cpu); |
| int ret = 0; |
| |
| qemu_mutex_lock_iothread(); |
| |
| if (!((cpu->interrupt_request & CPU_INTERRUPT_HARD) && |
| (env->eflags & IF_MASK)) && |
| !(cpu->interrupt_request & CPU_INTERRUPT_NMI)) { |
| cpu->exception_index = EXCP_HLT; |
| cpu->halted = true; |
| ret = 1; |
| } |
| |
| qemu_mutex_unlock_iothread(); |
| |
| return ret; |
| } |
| |
| static int |
| nvmm_inject_ud(struct nvmm_machine *mach, struct nvmm_vcpu *vcpu) |
| { |
| struct nvmm_vcpu_event *event = vcpu->event; |
| |
| event->type = NVMM_VCPU_EVENT_EXCP; |
| event->vector = 6; |
| event->u.excp.error = 0; |
| |
| return nvmm_vcpu_inject(mach, vcpu); |
| } |
| |
| static int |
| nvmm_vcpu_loop(CPUState *cpu) |
| { |
| struct nvmm_machine *mach = get_nvmm_mach(); |
| AccelCPUState *qcpu = cpu->accel; |
| struct nvmm_vcpu *vcpu = &qcpu->vcpu; |
| X86CPU *x86_cpu = X86_CPU(cpu); |
| CPUX86State *env = &x86_cpu->env; |
| struct nvmm_vcpu_exit *exit = vcpu->exit; |
| int ret; |
| |
| /* |
| * Some asynchronous events must be handled outside of the inner |
| * VCPU loop. They are handled here. |
| */ |
| if (cpu->interrupt_request & CPU_INTERRUPT_INIT) { |
| nvmm_cpu_synchronize_state(cpu); |
| do_cpu_init(x86_cpu); |
| /* set int/nmi windows back to the reset state */ |
| } |
| if (cpu->interrupt_request & CPU_INTERRUPT_POLL) { |
| cpu->interrupt_request &= ~CPU_INTERRUPT_POLL; |
| apic_poll_irq(x86_cpu->apic_state); |
| } |
| if (((cpu->interrupt_request & CPU_INTERRUPT_HARD) && |
| (env->eflags & IF_MASK)) || |
| (cpu->interrupt_request & CPU_INTERRUPT_NMI)) { |
| cpu->halted = false; |
| } |
| if (cpu->interrupt_request & CPU_INTERRUPT_SIPI) { |
| nvmm_cpu_synchronize_state(cpu); |
| do_cpu_sipi(x86_cpu); |
| } |
| if (cpu->interrupt_request & CPU_INTERRUPT_TPR) { |
| cpu->interrupt_request &= ~CPU_INTERRUPT_TPR; |
| nvmm_cpu_synchronize_state(cpu); |
| apic_handle_tpr_access_report(x86_cpu->apic_state, env->eip, |
| env->tpr_access_type); |
| } |
| |
| if (cpu->halted) { |
| cpu->exception_index = EXCP_HLT; |
| qatomic_set(&cpu->exit_request, false); |
| return 0; |
| } |
| |
| qemu_mutex_unlock_iothread(); |
| cpu_exec_start(cpu); |
| |
| /* |
| * Inner VCPU loop. |
| */ |
| do { |
| if (cpu->vcpu_dirty) { |
| nvmm_set_registers(cpu); |
| cpu->vcpu_dirty = false; |
| } |
| |
| if (qcpu->stop) { |
| cpu->exception_index = EXCP_INTERRUPT; |
| qcpu->stop = false; |
| ret = 1; |
| break; |
| } |
| |
| nvmm_vcpu_pre_run(cpu); |
| |
| if (qatomic_read(&cpu->exit_request)) { |
| #if NVMM_USER_VERSION >= 2 |
| nvmm_vcpu_stop(vcpu); |
| #else |
| qemu_cpu_kick_self(); |
| #endif |
| } |
| |
| /* Read exit_request before the kernel reads the immediate exit flag */ |
| smp_rmb(); |
| ret = nvmm_vcpu_run(mach, vcpu); |
| if (ret == -1) { |
| error_report("NVMM: Failed to exec a virtual processor," |
| " error=%d", errno); |
| break; |
| } |
| |
| nvmm_vcpu_post_run(cpu, exit); |
| |
| switch (exit->reason) { |
| case NVMM_VCPU_EXIT_NONE: |
| break; |
| #if NVMM_USER_VERSION >= 2 |
| case NVMM_VCPU_EXIT_STOPPED: |
| /* |
| * The kernel cleared the immediate exit flag; cpu->exit_request |
| * must be cleared after |
| */ |
| smp_wmb(); |
| qcpu->stop = true; |
| break; |
| #endif |
| case NVMM_VCPU_EXIT_MEMORY: |
| ret = nvmm_handle_mem(mach, vcpu); |
| break; |
| case NVMM_VCPU_EXIT_IO: |
| ret = nvmm_handle_io(mach, vcpu); |
| break; |
| case NVMM_VCPU_EXIT_INT_READY: |
| case NVMM_VCPU_EXIT_NMI_READY: |
| case NVMM_VCPU_EXIT_TPR_CHANGED: |
| break; |
| case NVMM_VCPU_EXIT_HALTED: |
| ret = nvmm_handle_halted(mach, cpu, exit); |
| break; |
| case NVMM_VCPU_EXIT_SHUTDOWN: |
| qemu_system_reset_request(SHUTDOWN_CAUSE_GUEST_RESET); |
| cpu->exception_index = EXCP_INTERRUPT; |
| ret = 1; |
| break; |
| case NVMM_VCPU_EXIT_RDMSR: |
| ret = nvmm_handle_rdmsr(mach, cpu, exit); |
| break; |
| case NVMM_VCPU_EXIT_WRMSR: |
| ret = nvmm_handle_wrmsr(mach, cpu, exit); |
| break; |
| case NVMM_VCPU_EXIT_MONITOR: |
| case NVMM_VCPU_EXIT_MWAIT: |
| ret = nvmm_inject_ud(mach, vcpu); |
| break; |
| default: |
| error_report("NVMM: Unexpected VM exit code 0x%lx [hw=0x%lx]", |
| exit->reason, exit->u.inv.hwcode); |
| nvmm_get_registers(cpu); |
| qemu_mutex_lock_iothread(); |
| qemu_system_guest_panicked(cpu_get_crash_info(cpu)); |
| qemu_mutex_unlock_iothread(); |
| ret = -1; |
| break; |
| } |
| } while (ret == 0); |
| |
| cpu_exec_end(cpu); |
| qemu_mutex_lock_iothread(); |
| |
| qatomic_set(&cpu->exit_request, false); |
| |
| return ret < 0; |
| } |
| |
| /* -------------------------------------------------------------------------- */ |
| |
| static void |
| do_nvmm_cpu_synchronize_state(CPUState *cpu, run_on_cpu_data arg) |
| { |
| nvmm_get_registers(cpu); |
| cpu->vcpu_dirty = true; |
| } |
| |
| static void |
| do_nvmm_cpu_synchronize_post_reset(CPUState *cpu, run_on_cpu_data arg) |
| { |
| nvmm_set_registers(cpu); |
| cpu->vcpu_dirty = false; |
| } |
| |
| static void |
| do_nvmm_cpu_synchronize_post_init(CPUState *cpu, run_on_cpu_data arg) |
| { |
| nvmm_set_registers(cpu); |
| cpu->vcpu_dirty = false; |
| } |
| |
| static void |
| do_nvmm_cpu_synchronize_pre_loadvm(CPUState *cpu, run_on_cpu_data arg) |
| { |
| cpu->vcpu_dirty = true; |
| } |
| |
| void nvmm_cpu_synchronize_state(CPUState *cpu) |
| { |
| if (!cpu->vcpu_dirty) { |
| run_on_cpu(cpu, do_nvmm_cpu_synchronize_state, RUN_ON_CPU_NULL); |
| } |
| } |
| |
| void nvmm_cpu_synchronize_post_reset(CPUState *cpu) |
| { |
| run_on_cpu(cpu, do_nvmm_cpu_synchronize_post_reset, RUN_ON_CPU_NULL); |
| } |
| |
| void nvmm_cpu_synchronize_post_init(CPUState *cpu) |
| { |
| run_on_cpu(cpu, do_nvmm_cpu_synchronize_post_init, RUN_ON_CPU_NULL); |
| } |
| |
| void nvmm_cpu_synchronize_pre_loadvm(CPUState *cpu) |
| { |
| run_on_cpu(cpu, do_nvmm_cpu_synchronize_pre_loadvm, RUN_ON_CPU_NULL); |
| } |
| |
| /* -------------------------------------------------------------------------- */ |
| |
| static Error *nvmm_migration_blocker; |
| |
| /* |
| * The nvmm_vcpu_stop() mechanism breaks races between entering the VMM |
| * and another thread signaling the vCPU thread to exit. |
| */ |
| |
| static void |
| nvmm_ipi_signal(int sigcpu) |
| { |
| if (current_cpu) { |
| AccelCPUState *qcpu = current_cpu->accel; |
| #if NVMM_USER_VERSION >= 2 |
| struct nvmm_vcpu *vcpu = &qcpu->vcpu; |
| nvmm_vcpu_stop(vcpu); |
| #else |
| qcpu->stop = true; |
| #endif |
| } |
| } |
| |
| static void |
| nvmm_init_cpu_signals(void) |
| { |
| struct sigaction sigact; |
| sigset_t set; |
| |
| /* Install the IPI handler. */ |
| memset(&sigact, 0, sizeof(sigact)); |
| sigact.sa_handler = nvmm_ipi_signal; |
| sigaction(SIG_IPI, &sigact, NULL); |
| |
| /* Allow IPIs on the current thread. */ |
| sigprocmask(SIG_BLOCK, NULL, &set); |
| sigdelset(&set, SIG_IPI); |
| pthread_sigmask(SIG_SETMASK, &set, NULL); |
| } |
| |
| int |
| nvmm_init_vcpu(CPUState *cpu) |
| { |
| struct nvmm_machine *mach = get_nvmm_mach(); |
| struct nvmm_vcpu_conf_cpuid cpuid; |
| struct nvmm_vcpu_conf_tpr tpr; |
| Error *local_error = NULL; |
| AccelCPUState *qcpu; |
| int ret, err; |
| |
| nvmm_init_cpu_signals(); |
| |
| if (nvmm_migration_blocker == NULL) { |
| error_setg(&nvmm_migration_blocker, |
| "NVMM: Migration not supported"); |
| |
| if (migrate_add_blocker(nvmm_migration_blocker, &local_error) < 0) { |
| error_report_err(local_error); |
| error_free(nvmm_migration_blocker); |
| return -EINVAL; |
| } |
| } |
| |
| qcpu = g_new0(AccelCPUState, 1); |
| |
| ret = nvmm_vcpu_create(mach, cpu->cpu_index, &qcpu->vcpu); |
| if (ret == -1) { |
| err = errno; |
| error_report("NVMM: Failed to create a virtual processor," |
| " error=%d", err); |
| g_free(qcpu); |
| return -err; |
| } |
| |
| memset(&cpuid, 0, sizeof(cpuid)); |
| cpuid.mask = 1; |
| cpuid.leaf = 0x00000001; |
| cpuid.u.mask.set.edx = CPUID_MCE | CPUID_MCA | CPUID_MTRR; |
| ret = nvmm_vcpu_configure(mach, &qcpu->vcpu, NVMM_VCPU_CONF_CPUID, |
| &cpuid); |
| if (ret == -1) { |
| err = errno; |
| error_report("NVMM: Failed to configure a virtual processor," |
| " error=%d", err); |
| g_free(qcpu); |
| return -err; |
| } |
| |
| ret = nvmm_vcpu_configure(mach, &qcpu->vcpu, NVMM_VCPU_CONF_CALLBACKS, |
| &nvmm_callbacks); |
| if (ret == -1) { |
| err = errno; |
| error_report("NVMM: Failed to configure a virtual processor," |
| " error=%d", err); |
| g_free(qcpu); |
| return -err; |
| } |
| |
| if (qemu_mach.cap.arch.vcpu_conf_support & NVMM_CAP_ARCH_VCPU_CONF_TPR) { |
| memset(&tpr, 0, sizeof(tpr)); |
| tpr.exit_changed = 1; |
| ret = nvmm_vcpu_configure(mach, &qcpu->vcpu, NVMM_VCPU_CONF_TPR, &tpr); |
| if (ret == -1) { |
| err = errno; |
| error_report("NVMM: Failed to configure a virtual processor," |
| " error=%d", err); |
| g_free(qcpu); |
| return -err; |
| } |
| } |
| |
| cpu->vcpu_dirty = true; |
| cpu->accel = qcpu; |
| |
| return 0; |
| } |
| |
| int |
| nvmm_vcpu_exec(CPUState *cpu) |
| { |
| int ret, fatal; |
| |
| while (1) { |
| if (cpu->exception_index >= EXCP_INTERRUPT) { |
| ret = cpu->exception_index; |
| cpu->exception_index = -1; |
| break; |
| } |
| |
| fatal = nvmm_vcpu_loop(cpu); |
| |
| if (fatal) { |
| error_report("NVMM: Failed to execute a VCPU."); |
| abort(); |
| } |
| } |
| |
| return ret; |
| } |
| |
| void |
| nvmm_destroy_vcpu(CPUState *cpu) |
| { |
| struct nvmm_machine *mach = get_nvmm_mach(); |
| AccelCPUState *qcpu = cpu->accel; |
| |
| nvmm_vcpu_destroy(mach, &qcpu->vcpu); |
| g_free(cpu->accel); |
| } |
| |
| /* -------------------------------------------------------------------------- */ |
| |
| static void |
| nvmm_update_mapping(hwaddr start_pa, ram_addr_t size, uintptr_t hva, |
| bool add, bool rom, const char *name) |
| { |
| struct nvmm_machine *mach = get_nvmm_mach(); |
| int ret, prot; |
| |
| if (add) { |
| prot = PROT_READ | PROT_EXEC; |
| if (!rom) { |
| prot |= PROT_WRITE; |
| } |
| ret = nvmm_gpa_map(mach, hva, start_pa, size, prot); |
| } else { |
| ret = nvmm_gpa_unmap(mach, hva, start_pa, size); |
| } |
| |
| if (ret == -1) { |
| error_report("NVMM: Failed to %s GPA range '%s' PA:%p, " |
| "Size:%p bytes, HostVA:%p, error=%d", |
| (add ? "map" : "unmap"), name, (void *)(uintptr_t)start_pa, |
| (void *)size, (void *)hva, errno); |
| } |
| } |
| |
| static void |
| nvmm_process_section(MemoryRegionSection *section, int add) |
| { |
| MemoryRegion *mr = section->mr; |
| hwaddr start_pa = section->offset_within_address_space; |
| ram_addr_t size = int128_get64(section->size); |
| unsigned int delta; |
| uintptr_t hva; |
| |
| if (!memory_region_is_ram(mr)) { |
| return; |
| } |
| |
| /* Adjust start_pa and size so that they are page-aligned. */ |
| delta = qemu_real_host_page_size() - (start_pa & ~qemu_real_host_page_mask()); |
| delta &= ~qemu_real_host_page_mask(); |
| if (delta > size) { |
| return; |
| } |
| start_pa += delta; |
| size -= delta; |
| size &= qemu_real_host_page_mask(); |
| if (!size || (start_pa & ~qemu_real_host_page_mask())) { |
| return; |
| } |
| |
| hva = (uintptr_t)memory_region_get_ram_ptr(mr) + |
| section->offset_within_region + delta; |
| |
| nvmm_update_mapping(start_pa, size, hva, add, |
| memory_region_is_rom(mr), mr->name); |
| } |
| |
| static void |
| nvmm_region_add(MemoryListener *listener, MemoryRegionSection *section) |
| { |
| memory_region_ref(section->mr); |
| nvmm_process_section(section, 1); |
| } |
| |
| static void |
| nvmm_region_del(MemoryListener *listener, MemoryRegionSection *section) |
| { |
| nvmm_process_section(section, 0); |
| memory_region_unref(section->mr); |
| } |
| |
| static void |
| nvmm_transaction_begin(MemoryListener *listener) |
| { |
| /* nothing */ |
| } |
| |
| static void |
| nvmm_transaction_commit(MemoryListener *listener) |
| { |
| /* nothing */ |
| } |
| |
| static void |
| nvmm_log_sync(MemoryListener *listener, MemoryRegionSection *section) |
| { |
| MemoryRegion *mr = section->mr; |
| |
| if (!memory_region_is_ram(mr)) { |
| return; |
| } |
| |
| memory_region_set_dirty(mr, 0, int128_get64(section->size)); |
| } |
| |
| static MemoryListener nvmm_memory_listener = { |
| .name = "nvmm", |
| .begin = nvmm_transaction_begin, |
| .commit = nvmm_transaction_commit, |
| .region_add = nvmm_region_add, |
| .region_del = nvmm_region_del, |
| .log_sync = nvmm_log_sync, |
| .priority = MEMORY_LISTENER_PRIORITY_ACCEL, |
| }; |
| |
| static void |
| nvmm_ram_block_added(RAMBlockNotifier *n, void *host, size_t size, |
| size_t max_size) |
| { |
| struct nvmm_machine *mach = get_nvmm_mach(); |
| uintptr_t hva = (uintptr_t)host; |
| int ret; |
| |
| ret = nvmm_hva_map(mach, hva, max_size); |
| |
| if (ret == -1) { |
| error_report("NVMM: Failed to map HVA, HostVA:%p " |
| "Size:%p bytes, error=%d", |
| (void *)hva, (void *)size, errno); |
| } |
| } |
| |
| static struct RAMBlockNotifier nvmm_ram_notifier = { |
| .ram_block_added = nvmm_ram_block_added |
| }; |
| |
| /* -------------------------------------------------------------------------- */ |
| |
| static int |
| nvmm_accel_init(MachineState *ms) |
| { |
| int ret, err; |
| |
| ret = nvmm_init(); |
| if (ret == -1) { |
| err = errno; |
| error_report("NVMM: Initialization failed, error=%d", errno); |
| return -err; |
| } |
| |
| ret = nvmm_capability(&qemu_mach.cap); |
| if (ret == -1) { |
| err = errno; |
| error_report("NVMM: Unable to fetch capability, error=%d", errno); |
| return -err; |
| } |
| if (qemu_mach.cap.version < NVMM_KERN_VERSION) { |
| error_report("NVMM: Unsupported version %u", qemu_mach.cap.version); |
| return -EPROGMISMATCH; |
| } |
| if (qemu_mach.cap.state_size != sizeof(struct nvmm_x64_state)) { |
| error_report("NVMM: Wrong state size %u", qemu_mach.cap.state_size); |
| return -EPROGMISMATCH; |
| } |
| |
| ret = nvmm_machine_create(&qemu_mach.mach); |
| if (ret == -1) { |
| err = errno; |
| error_report("NVMM: Machine creation failed, error=%d", errno); |
| return -err; |
| } |
| |
| memory_listener_register(&nvmm_memory_listener, &address_space_memory); |
| ram_block_notifier_add(&nvmm_ram_notifier); |
| |
| printf("NetBSD Virtual Machine Monitor accelerator is operational\n"); |
| return 0; |
| } |
| |
| int |
| nvmm_enabled(void) |
| { |
| return nvmm_allowed; |
| } |
| |
| static void |
| nvmm_accel_class_init(ObjectClass *oc, void *data) |
| { |
| AccelClass *ac = ACCEL_CLASS(oc); |
| ac->name = "NVMM"; |
| ac->init_machine = nvmm_accel_init; |
| ac->allowed = &nvmm_allowed; |
| } |
| |
| static const TypeInfo nvmm_accel_type = { |
| .name = ACCEL_CLASS_NAME("nvmm"), |
| .parent = TYPE_ACCEL, |
| .class_init = nvmm_accel_class_init, |
| }; |
| |
| static void |
| nvmm_type_init(void) |
| { |
| type_register_static(&nvmm_accel_type); |
| } |
| |
| type_init(nvmm_type_init); |