Jailhouse Hypervisor启用流程详解:从用户工具到VM Exit处理

概述

本文档详细分析Jailhouse hypervisor的完整启用流程,从用户空间工具开始,到最终进入VMX non-root模式的全过程。特别关注arch_entry函数的调用链以及VM Exit处理地址的设置机制。

1. 完整调用链分析(基于实际代码)

1.1 用户空间启动流程

A. 用户工具入口

// tools/jailhouse.c - main()函数
int main(int argc, char *argv[])
{
    if (argc < 2)
        help(argv[0], 1);

    if (!strcmp(argv[1], "enable"))
        return cell_management(argc, argv, JAILHOUSE_ENABLE);
    // ... 其他命令处理
}

B. cell_management函数

// tools/jailhouse.c - cell_management()
static int cell_management(int argc, char *argv[], unsigned int command)
{
    struct jailhouse_cell_create cell_create;
    // ... 参数处理 ...

    switch (command) {
    case JAILHOUSE_ENABLE:
        // 读取系统配置
        config = read_config(argv[2]);
        if (!config)
            return -1;

        // 打开jailhouse设备文件
        fd = open("/dev/jailhouse", O_RDWR);
        if (fd < 0) {
            perror("opening /dev/jailhouse");
            return -1;
        }

        // 执行enable ioctl
        err = ioctl(fd, JAILHOUSE_ENABLE, config);
        break;
    }
    return err;
}

1.2 内核驱动处理流程

A. ioctl处理入口

// driver/main.c - jailhouse_ioctl()
static long jailhouse_ioctl(struct file *file, unsigned int ioctl,
                            unsigned long arg)
{
    long err;

    switch (ioctl) {
    case JAILHOUSE_ENABLE:
        err = jailhouse_cmd_enable((struct jailhouse_system __user *)arg);
        break;
    // ... 其他ioctl处理
    }
    return err;
}

B. enable命令处理

// driver/main.c - jailhouse_cmd_enable()
static int jailhouse_cmd_enable(struct jailhouse_system __user *arg)
{
    const struct jailhouse_header *header;
    int err;

    // ... 参数验证和内存分配 ...

    header = (struct jailhouse_header *)hypervisor_mem;

    // 关键:在所有CPU上执行enter_hypervisor
    err = smp_call_function(enter_hypervisor, (void *)header);
    if (!err)
        err = enter_hypervisor((void *)header);

    return err;
}

C. 关键的enter_hypervisor函数

// driver/main.c - enter_hypervisor()
static void enter_hypervisor(void *info)
{
    struct jailhouse_header *header = info;
    unsigned int cpu = smp_processor_id();
    int (*entry)(unsigned int);
    int err;

    // 计算hypervisor入口地址
    entry = header->entry + (unsigned long)hypervisor_mem;

    if (cpu < header->max_cpus)
        // 关键调用:跳转到hypervisor的arch_entry
        err = entry(cpu);
    else
        err = -EINVAL;

    // 注意:正常情况下这里不会执行到,因为entry()不会返回
    // 如果执行到这里,说明hypervisor启动失败
}

1.3 Hypervisor入口点分析

A. arch_entry汇编入口

# hypervisor/arch/x86/entry.S
.code64

.globl arch_entry
arch_entry:
    cli

    # 保存Linux的寄存器状态,稍后会被读取到linux_reg[]数组,详见后文
    push %rbp        # 第一个压入
    push %rbx        # 第二个压入
    push %r12        # 第三个压入
    push %r13        # 第四个压入
    push %r14        # 第五个压入
    push %r15        # 最后压入,位于栈顶

    # 保存Linux的栈指针到per_cpu结构:cpu_data->linux_sp
    mov %rsp,PERCPU_LINUX_SP(%rsi)

    # 切换到hypervisor的栈
    mov PERCPU_STACK_END(%rsi),%rsp

    # 调用C语言的entry函数
    call entry

    # 如果entry返回(错误情况),恢复Linux状态并返回
    mov PERCPU_LINUX_SP(%rsi),%rsp

    pop %r15         # 恢复顺序与压栈顺序相反
    pop %r14
    pop %r13
    pop %r12
    pop %rbx
    pop %rbp

    sti
    ret

B. C语言entry函数

// hypervisor/setup.c - entry()
int entry(unsigned int cpu_id)
{
    struct per_cpu *cpu_data;
    int err;

    // 获取当前CPU的数据结构
    cpu_data = per_cpu(cpu_id);

    // 早期初始化
    err = init_early(cpu_id);
    if (err)
        return err;

    // CPU初始化
    err = cpu_init(cpu_data);
    if (err)
        return err;

    // 关键:激活虚拟化,这个函数不会返回!
    arch_cpu_activate_vmm();
}

1.4 VMX激活流程

A. 架构特定的VMM激活

// hypervisor/arch/x86/setup.c - arch_cpu_activate_vmm()
void __attribute__((noreturn)) arch_cpu_activate_vmm(void)
{
    unsigned int cpu_id = this_cpu_id();

    // 切换到私有栈映射
    asm volatile(
        "add %0,%%rsp"
        : : "g" (LOCAL_CPU_BASE - (unsigned long)per_cpu(cpu_id)));

    // 撤销完整的per_cpu访问权限
    paging_map_all_per_cpu(cpu_id, false);

    // 最终调用:激活虚拟化
    vcpu_activate_vmm();
}

B. VMX虚拟化激活

// hypervisor/arch/x86/vmx.c - vcpu_activate_vmm()
void __attribute__((noreturn)) vcpu_activate_vmm(void)
{
    // 启用VMX操作
    write_cr4(read_cr4() | X86_CR4_VMXE);

    // 进入VMX操作模式
    if (vmxon(this_cpu_data()->vmxon_region))
        goto vmxon_failed;

    // 加载VMCS
    if (vmclear(this_cpu_data()->vmcs) ||
        vmptrld(this_cpu_data()->vmcs))
        goto vmcs_failed;

    // 关键:进入guest模式,执行VMLAUNCH
    asm volatile(
        "mov (%%rax),%%r15; "
        "mov 0x8(%%rax),%%r14; "
        // ... 恢复更多寄存器 ...
        "vmlaunch; "
        // 如果VMLAUNCH失败,会执行到这里
        "jmp vmx_entry_failure"
        : : "a" (&this_cpu_data()->guest_regs)
        : "memory");

    // 这里不应该执行到
    __builtin_unreachable();
}

2. VM Exit处理地址设置分析

2.1 VMCS中HOST_RIP的设置

A. vmcs_setup函数中的关键设置

// hypervisor/arch/x86/vmx.c - vmcs_setup()
static bool vmcs_setup(struct per_cpu *cpu_data)
{
    unsigned long val;
    bool ok = true;

    // ... 其他VMCS字段设置 ...

    // 关键:设置VM Exit时的RIP地址
    ok &= vmcs_write64(HOST_RIP, (unsigned long)vmx_vmexit);

    // 设置VM Exit时的RSP
    ok &= vmcs_write64(HOST_RSP,
                       (unsigned long)cpu_data + sizeof(*cpu_data));

    // 设置VM Exit时的CR3(页表基址)
    ok &= vmcs_write64(HOST_CR3,
                       paging_hvirt2phys(cpu_data->pg_structs.root_table));

    return ok;
}

B. vmcs_setup的调用时机

// hypervisor/arch/x86/vmx.c - vcpu_init()
int vcpu_init(struct per_cpu *cpu_data)
{
    // ... 其他初始化 ...

    // 设置VMCS - 这里设置了VM Exit处理地址
    if (!vmcs_setup(cpu_data))
        return trace_error(-EIO);

    return 0;
}

// 调用链:
// entry() -> cpu_init() -> arch_cpu_init() -> vcpu_init() -> vmcs_setup()
// 这发生在arch_cpu_activate_vmm()之前

2.2 VM Exit处理汇编代码

A. vmx_vmexit汇编入口

# hypervisor/arch/x86/vmx-vmexit.S
.code64

.globl vmx_vmexit
vmx_vmexit:
    # VM Exit时硬件会跳转到这里
    # 此时已经在VMX root模式,使用hypervisor的栈

    # 保存guest的通用寄存器
    push %rax
    push %rcx
    push %rdx
    push %rbx
    push %rbp
    push %rsi
    push %rdi
    push %r8
    push %r9
    push %r10
    push %r11
    push %r12
    push %r13
    push %r14
    push %r15

    # 调用C语言的VM Exit处理函数
    mov %rsp, %rdi  # 传递寄存器结构指针
    call vcpu_handle_exit

    # 恢复guest的通用寄存器
    pop %r15
    pop %r14
    pop %r13
    pop %r12
    pop %r11
    pop %r10
    pop %r9
    pop %r8
    pop %rdi
    pop %rsi
    pop %rbp
    pop %rbx
    pop %rdx
    pop %rcx
    pop %rax

    # 返回guest模式
    vmresume

    # 如果vmresume失败,跳转到错误处理
    jmp vmx_entry_failure

B. C语言VM Exit处理函数

// hypervisor/arch/x86/vmx.c - vcpu_handle_exit()
void vcpu_handle_exit(struct per_cpu *cpu_data)
{
    struct guest_regs *guest_regs = &cpu_data->guest_regs;
    u32 reason = vmcs_read32(VM_EXIT_REASON);
    u32 *stats = cpu_data->public.stats;

    // 根据VM Exit原因进行分发处理
    switch (reason & EXIT_REASON_MASK) {
    case EXIT_REASON_EXCEPTION_NMI:
        vmx_handle_exception_nmi();
        return;
    case EXIT_REASON_CPUID:
        vcpu_handle_cpuid();
        return;
    case EXIT_REASON_VMCALL:
        vcpu_handle_hypercall();
        return;
    case EXIT_REASON_CR_ACCESS:
        stats[JAILHOUSE_CPU_STAT_VMEXITS_CR]++;
        if (vmx_handle_cr())
            return;
        break;
    case EXIT_REASON_MSR_READ:
        if (vcpu_handle_msr_read())
            return;
        break;
    case EXIT_REASON_MSR_WRITE:
        if (vcpu_handle_msr_write())
            return;
        break;
    case EXIT_REASON_EPT_VIOLATION:
        stats[JAILHOUSE_CPU_STAT_VMEXITS_MMIO]++;
        if (vcpu_handle_mmio_access())
            return;
        break;
    // ... 其他VM Exit原因处理
    }

    // 如果没有处理成功,打印错误信息
    panic_printk("FATAL: Unhandled VM exit, reason %d\n", reason);
    panic_stop();
}

3. 关键时序和内存状态变化

3.1 完整时序图

sequenceDiagram
    participant User as 用户工具
    participant Driver as Jailhouse驱动
    participant Entry as arch_entry
    participant Setup as hypervisor/setup.c
    participant VMX as hypervisor/arch/x86/vmx.c
    participant Linux as Linux Root Cell

    Note over User,Linux: Jailhouse启用完整流程

    User->>Driver: jailhouse enable config.cell
    Driver->>Driver: jailhouse_cmd_enable()
    Driver->>Driver: smp_call_function(enter_hypervisor)

    Note over Driver: 在每个CPU上执行

    Driver->>Entry: entry = header->entry + hypervisor_mem
    Driver->>Entry: entry(cpu_id) - 调用arch_entry

    Note over Entry: hypervisor/arch/x86/entry.S

    Entry->>Entry: 保存Linux寄存器状态
    Entry->>Entry: 切换到hypervisor栈
    Entry->>Setup: call entry - C语言函数

    Note over Setup: hypervisor/setup.c

    Setup->>Setup: init_early() - 早期初始化
    Setup->>Setup: cpu_init() - CPU初始化
    Setup->>VMX: arch_cpu_init() -> vcpu_init()

    Note over VMX: 关键的VMCS设置

    VMX->>VMX: vmcs_setup()
    VMX->>VMX: vmcs_write64(HOST_RIP, vmx_vmexit)

    Note over VMX: VM Exit地址已设置完成

    Setup->>VMX: arch_cpu_activate_vmm()
    VMX->>VMX: vcpu_activate_vmm()
    VMX->>VMX: vmxon() + vmclear() + vmptrld()
    VMX->>Linux: VMLAUNCH - 进入VMX non-root模式

    Note over Linux: Linux现在运行在VMX non-root模式<br/>成为root cell

    Linux->>VMX: 某个操作触发VM Exit
    Note over VMX: 硬件自动跳转到HOST_RIP<br/>(vmx_vmexit地址)
    VMX->>VMX: vmx_vmexit汇编处理
    VMX->>VMX: vcpu_handle_exit() C函数
    VMX->>Linux: vmresume返回guest模式

3.2 内存和执行模式变化

A. 启用前的状态

Linux执行状态:
- 执行模式:Ring 0 (kernel mode)
- 虚拟化:VMX关闭或VMX root模式
- 内存访问:直接物理内存访问
- 栈:Linux内核栈

B. arch_entry执行期间

Hypervisor初始化状态:
- 执行模式:Ring 0 (kernel mode)
- 虚拟化:VMX开启,准备VMCS
- 内存访问:hypervisor页表
- 栈:hypervisor栈
- 关键操作:设置HOST_RIP = vmx_vmexit

C. VMLAUNCH后的状态

Linux (Root Cell) 状态:
- 执行模式:Ring 0 (kernel mode)
- 虚拟化:VMX non-root模式 (guest mode)
- 内存访问:通过EPT控制
- 栈:原Linux内核栈
- VM Exit处理:硬件跳转到vmx_vmexit

4. 关键代码文件和函数总结

4.1 调用链中的关键文件

文件 关键函数 作用
tools/jailhouse.c main(), cell_management() 用户空间入口
driver/main.c jailhouse_cmd_enable(), enter_hypervisor() 内核驱动处理
hypervisor/arch/x86/entry.S arch_entry 汇编入口点
hypervisor/setup.c entry(), init_early(), cpu_init() Hypervisor初始化
hypervisor/arch/x86/vmx.c vcpu_init(), vmcs_setup(), vcpu_activate_vmm() VMX设置和激活
hypervisor/arch/x86/vmx-vmexit.S vmx_vmexit VM Exit汇编处理

4.2 VM Exit地址设置的关键点

A. 设置位置

// 文件:hypervisor/arch/x86/vmx.c
// 函数:vmcs_setup()
// 代码:vmcs_write64(HOST_RIP, (unsigned long)vmx_vmexit);

B. 设置时机

调用顺序:
entry() -> cpu_init() -> arch_cpu_init() -> vcpu_init() -> vmcs_setup()
时机:在arch_cpu_activate_vmm()之前完成

C. 处理地址

# 文件:hypervisor/arch/x86/vmx-vmexit.S
# 函数:vmx_vmexit
# 作用:所有VM Exit的统一入口点

5. 设计精妙之处

5.1 无缝切换机制

5.2 预设VM Exit处理

5.3 确定性的执行流程

6. Guest RIP设置的关键机制

6.1 Guest RIP的赋值位置和过程

您的问题非常关键!让我详细解释Guest RIP是如何设置的:

A. Linux栈帧信息的保存

// hypervisor/arch/x86/setup.c - arch_cpu_init()
int arch_cpu_init(struct per_cpu *cpu_data)
{
    // ... 其他初始化 ...

    // 关键:从Linux栈中读取寄存器信息
    // 这些是arch_entry调用时Linux保存在栈上的寄存器
    for (n = 0; n < NUM_ENTRY_REGS; n++)
        cpu_data->linux_reg[n] =
            ((unsigned long *)cpu_data->linux_sp)[n];

    // 关键:读取Linux的返回地址作为Guest RIP
    cpu_data->linux_ip =
        ((unsigned long *)cpu_data->linux_sp)[NUM_ENTRY_REGS];

    // ... 其他处理 ...
}

B. arch_entry汇编中的栈帧布局

# hypervisor/arch/x86/entry.S - arch_entry
arch_entry:
    cli

    # 保存Linux的寄存器到栈上(按照原始代码顺序)
    push %rbp        # 第一个压入
    push %rbx        # 第二个压入
    push %r12        # 第三个压入
    push %r13        # 第四个压入
    push %r14        # 第五个压入
    push %r15        # 最后压入,位于栈顶

    # 此时栈的布局(从高地址到低地址):
    # [rsp+48]: 返回地址 (调用arch_entry的下一条指令)  这就是linux_ip!
    # [rsp+40]: %rbp  (linux_reg[0] - 第一个压入的寄存器)
    # [rsp+32]: %rbx  (linux_reg[1])
    # [rsp+24]: %r12  (linux_reg[2])
    # [rsp+16]: %r13  (linux_reg[3])
    # [rsp+8]:  %r14  (linux_reg[4])
    # [rsp+0]:  %r15  (linux_reg[5] - 最后压入的寄存器,位于栈顶)

    # 保存Linux栈指针
    mov %rsp,PERCPU_LINUX_SP(%rsi)  # 保存到cpu_data->linux_sp

    # 切换到hypervisor栈并调用entry()
    mov PERCPU_STACK_END(%rsi),%rsp
    call entry

    # 如果entry()返回(错误情况),恢复Linux状态
    mov PERCPU_LINUX_SP(%rsi),%rsp
    pop %r15         # 恢复顺序与压栈顺序相反(后进先出)
    pop %r14
    pop %r13
    pop %r12
    pop %rbx
    pop %rbp

    sti
    ret              # 返回到linux_ip指向的地址

C. Guest RIP写入VMCS

// hypervisor/arch/x86/vmx.c - vmcs_setup()
static bool vmcs_setup(void)
{
    struct per_cpu *cpu_data = this_cpu_data();
    // ... 其他VMCS设置 ...

    // 关键:将linux_ip设置为Guest RIP
    ok &= vmcs_write64(GUEST_RIP, cpu_data->linux_ip);

    // 同时设置Guest RSP
    ok &= vmcs_write64(GUEST_RSP, cpu_data->linux_sp +
                       (NUM_ENTRY_REGS + 1) * sizeof(unsigned long));

    // ... 其他设置 ...
}

6.2 完整的地址传递链路

sequenceDiagram
    participant Linux as Linux内核
    participant Driver as Jailhouse驱动
    participant Entry as arch_entry
    participant Setup as arch_cpu_init
    participant VMX as vmcs_setup
    participant Guest as Guest执行

    Note over Linux,Guest: Guest RIP设置的完整流程

    Linux->>Driver: 调用enter_hypervisor()
    Driver->>Entry: call arch_entry
    Note over Entry: 保存Linux寄存器到栈
    Entry->>Entry: push %rbp, %rbx, %r12, %r13, %r14, %r15
    Entry->>Entry: 栈顶现在是返回地址(linux_ip)
    Entry->>Entry: 保存栈指针到linux_sp

    Entry->>Setup: call entry() -> arch_cpu_init()
    Setup->>Setup: linux_sp读取栈帧
    Setup->>Setup: linux_reg[0-5] = 保存的寄存器
    Setup->>Setup: linux_ip = 栈上的返回地址

    Note over Setup: 关键:linux_ip就是Linux的返回地址

    Setup->>VMX: vcpu_init() -> vmcs_setup()
    VMX->>VMX: vmcs_write64(GUEST_RIP, linux_ip)
    VMX->>VMX: vmcs_write64(GUEST_RSP, linux_sp + offset)

    Note over VMX: VMCS配置完成

    VMX->>Guest: VMLAUNCH
    Note over Guest: Linux从arch_entry的返回地址继续执行<br/>但现在运行在VMX non-root模式

6.3 关键理解点

A. linux_ip的本质

// linux_ip实际上是:
// 1. enter_hypervisor()函数中调用arch_entry时的返回地址
// 2. 也就是enter_hypervisor()中call指令的下一条指令地址
// 3. 这确保了Linux在VMLAUNCH后从正确的位置继续执行

// 具体来说:
// driver/main.c:enter_hypervisor()
err = entry(cpu);  // ← 这个call指令的下一条指令地址就是linux_ip
// 当VMLAUNCH后,Linux会从这里继续执行,但现在在VMX non-root模式

B. 为什么这样设计

// 1. 无缝切换:Linux感觉不到hypervisor的介入
// 2. 状态保持:所有寄存器状态都被完整保存和恢复
// 3. 返回机制:如果hypervisor启动失败,可以正常返回Linux
// 4. 简洁性:不需要复杂的状态同步机制

6.4 实际的内存地址示例

// 假设的地址示例:
// enter_hypervisor() 在地址 0xffffffffc0123456
// call entry(cpu) 指令在 0xffffffffc0123460
// 下一条指令在       0xffffffffc0123465  ← 这就是linux_ip

// VMLAUNCH后:
// Guest RIP = 0xffffffffc0123465
// Linux从这个地址继续执行,但现在在VMX non-root模式
// Linux完全不知道自己已经变成了guest

7. 总结

完整调用链:

用户工具 → 驱动ioctl → enter_hypervisor → arch_entry(汇编) → 
entry(C函数) → cpu_init → vcpu_init → vmcs_setup → 
arch_cpu_activate_vmm → vcpu_activate_vmm → VMLAUNCH

Guest RIP设置的关键点: - 赋值位置hypervisor/arch/x86/setup.c:arch_cpu_init()第154-155行 - 数据来源:arch_entry汇编函数保存在栈上的返回地址 - 写入VMCShypervisor/arch/x86/vmx.c:vmcs_setup()第539行 - 执行位置:Linux从enter_hypervisor()的返回地址继续执行

VM Exit地址设置: - 设置位置hypervisor/arch/x86/vmx.c:vmcs_setup()第514行 - 设置时机:在arch_cpu_activate_vmm()之前的初始化阶段 - 处理地址vmx_vmexit(定义在hypervisor/arch/x86/vmx-vmexit.S) - 处理机制:硬件自动跳转 → 汇编保存状态 → C函数分发处理 → 汇编恢复状态 → vmresume返回

设计的精妙之处: 这种设计实现了完全透明的虚拟化切换 - Linux完全不知道自己已经变成了guest,从原来的执行位置无缝继续,但现在运行在VMX non-root模式下,所有的特权操作都会被hypervisor拦截和处理。这是Jailhouse实现低开销、高性能虚拟化的核心机制。 这种设计确保了Jailhouse能够以最小的开销和最高的确定性来处理虚拟化切换和VM Exit事件,是其实现低延迟实时虚拟化的核心机制。