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 无缝切换机制
arch_entry
保存Linux状态,确保可以无缝返回- VMLAUNCH后Linux从原来的位置继续执行
- 但现在运行在VMX non-root模式下
5.2 预设VM Exit处理
- 在激活VMX之前就设置好所有VM Exit处理地址
- 使用统一的
vmx_vmexit
入口处理所有类型的VM Exit - 通过
vcpu_handle_exit()
进行分发处理
5.3 确定性的执行流程
- 整个启用过程是确定性的,没有动态分配
- VM Exit处理路径预先确定,保证低延迟
- 符合Jailhouse实时系统的设计目标
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汇编函数保存在栈上的返回地址
- 写入VMCS:hypervisor/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事件,是其实现低延迟实时虚拟化的核心机制。
评论