开发x86_64操作系统时切换至用户态触发GPF错误的解决咨询
x86_64内核切换至Ring3用户态触发GPF错误排查
问题背景
开发基于x86架构的64位操作系统,已实现包含Null、内核代码段、内核数据段、用户代码段、用户数据段、TSS的GDT。编写switch_to_user_mode函数尝试切换至低半内存的Ring3用户空间,但执行iret指令时触发通用保护错误(GPF,中断13),栈内容显示压入的切换参数正确,但切换未完成。
相关代码
switch_to_user_mode函数
__attribute__((naked, noreturn)) void switch_to_user_mode(uint64_t stack_addr, uint64_t code_addr) { asm volatile( "cli\n" // 禁用中断 "mov $0x23, %%ax \n" // 加载用户态数据段选择子(Ring3) "mov %%ax, %%ds \n" "mov %%ax, %%es \n" "mov %%ax, %%fs \n" "mov %%ax, %%gs \n" "push $0x23 \n" // 压入用户态SS选择子 "push %[stack] \n" // 压入用户栈指针 "pushf \n" // 压入RFLAGS "pop %%rax \n" // 将RFLAGS加载到RAX "or $0x200, %%rax \n" // 设置IF位,切换后自动启用中断 "push %%rax \n" // 压入更新后的RFLAGS "push $0x1B \n" // 压入用户态CS选择子 "push %[entry] \n" // 压入用户程序入口地址 "iret \n" // 中断返回至用户态 : : [stack] "r" (stack_addr), [entry] "r" (code_addr) : "rax" ); }
用户态初始化与调用代码
uint64_t create_user_function() { void *user_code = (void *) uheap_alloc(0x1000); // 无限循环机器码 uint8_t user_program[] = { 0xeb, 0xfe // jmp $ }; printf("user_code or user_programe is null\n"); memcpy(user_code, (void*)&user_program, sizeof(user_program)); return (uint64_t)user_code; // 返回用户态可访问的函数指针 } void init_user_mode(){ uint64_t code_addr = (uint64_t) create_user_function(); page_t *code_page = get_page(code_addr, 0, (pml4_t *)get_cr3_addr()); code_page->rw = 0; // 设为只读 code_page->nx = 0; // 设为可执行 code_page->user = 1; // 设为用户可访问 uint64_t stack_base_addr = ((uint64_t) uheap_alloc(STACK_SIZE)); for(uint64_t addr = stack_base_addr; addr < stack_base_addr + STACK_SIZE; addr += 0x1000){ page_t *_page = get_page(addr, 0, (pml4_t *)get_cr3_addr()); _page->rw = 1; // 设为可读写 _page->nx = 1; // 设为不可执行 _page->user = 1; // 设为用户可访问 } uint64_t stack_top_addr = stack_base_addr + STACK_SIZE; // 设置栈顶 flush_tlb_all(); printf("Starting Switching to the user mode: code addr.- %x, stack addr.- %x\n", code_addr, stack_top_addr); switch_to_user_mode(stack_top_addr, code_addr); }
错误输出
[Info] Interrupt Based System Call initialized! GDT Entry 0x0: Base=0x0 Limit=0x0 Access=0x0 Flags=0x0 GDT Entry 0x8: Base=0x0 Limit=0xFFFF Access=0x9A Flags=0xA GDT Entry 0x10: Base=0x0 Limit=0xFFFF Access=0x93 Flags=0xA GDT Entry 0x1B: Base=0x0 Limit=0xFFFF Access=0xFA Flags=0xA GDT Entry 0x23: Base=0x0 Limit=0xFFFF Access=0xF2 Flags=0xA GDT Entry 0x28: Base=0x8059A060 Limit=0x67 Access=0x8B Flags=0x0 Current stack address: 0xFFFF80007FF41FC0 Current rip address: 0xFFFFFFFF8000EF0A Current rflags address: 0x286 Starting Switching to the user mode: code addr.- 0x1000, stack addr.- 0x7000 General protection fault (pushes an error code) recieved interrupt: 13 Error Code: 0x0 CS: 0x8, RIP : 0xFFFFFFFF8000EA61 Stack (rsp = 0xFFFF80007FF41F70) Contents(First 26) : [0xFFFF80007FF41F70] = 0x1000 [0xFFFF80007FF41F78] = 0x1B [0xFFFF80007FF41F80] = 0x292 [0xFFFF80007FF41F88] = 0x7000 [0xFFFF80007FF41F90] = 0x23 [0xFFFF80007FF41F98] = 0xFFFFFFFF8000EBCD [0xFFFF80007FF41FA0] = 0x7FF28030 [0xFFFF80007FF41FA8] = 0x7000 [0xFFFF80007FF41FB0] = 0x3000 [0xFFFF80007FF41FB8] = 0x7FF28008 [0xFFFF80007FF41FC0] = 0x1000 [0xFFFF80007FF41FC8] = 0x7000 [0xFFFF80007FF41FD0] = 0xFFFF80007FF41FF0 [0xFFFF80007FF41FD8] = 0xFFFFFFFF800080A1 [0xFFFF80007FF41FE0] = 0x0 [0xFFFF80007FF41FE8] = 0xFEBD5000 [0xFFFF80007FF41FF0] = 0x0 [0xFFFF80007FF41FF8] = 0x0 [0xFFFF80007FF42000] = 0x800000015CD00037 [0xFFFF80007FF42008] = 0xFFFF [0xFFFF80007FF42010] = 0x0 [0xFFFF80007FF42018] = 0x0 [0xFFFF80007FF42020] = 0x0 [0xFFFF80007FF42028] = 0x0 [0xFFFF80007FF42030] = 0x0 [0xFFFF80007FF42038] = 0x0 System Halted!
排查与解决方法
1. 检查TSS配置与加载
x86_64下从Ring0切换至Ring3时,CPU依赖TSS保存内核栈信息(RSP0),若TSS未正确配置会触发GPF:
- 确认已通过
ltr指令加载TSS选择器(GDT中TSS条目为0x28,需执行ltr 0x28)。 - 确保TSS的
RSP0字段被设置为当前内核栈指针(调用switch_to_user_mode前,将%rsp的值写入TSS的RSP0区域)。
2. 修正RFLAGS压入指令
64位模式下,pushf指令实际压入的是32位EFLAGS并零扩展为64位,虽然不影响IF位,但建议使用pushfq明确压入完整的64位RFLAGS,避免潜在的栈帧对齐或寄存器位问题:
将pushf替换为pushfq,对应的pop %%rax无需修改,因为pop %%rax会读取64位值。
3. 检查页表全层级的User位设置
当前代码仅设置了页表项(PT级)的user=1,但上级页表(PML4、PDPT、PD)的user位也必须设为1,否则CPU会判定该内存区域为内核态专属,访问时触发GPF:
- 修改
get_page相关逻辑,确保用户态内存对应的所有层级页表项(从PML4到PT)的user位都被置1。
4. 验证用户栈的映射完整性
用户栈顶地址为0x7000,需确保栈的所有页(从stack_base_addr到stack_top_addr)都被正确分配并映射物理内存,且权限设置正确(rw=1、nx=1、user=1)。可添加日志输出确认uheap_alloc返回的地址是否有效,以及所有页表项是否正确修改。
5. 确认GDT加载正确性
虽然GDT条目输出显示段描述符配置正确,但需确保已通过lgdt指令正确加载GDT,且GDT的基址和限长设置无误。
内容的提问来源于stack exchange,提问作者baponkar
相关产品推荐
相关产品推荐

