如何在带图形支持的微内核运行时切换16位实模式与32位保护模式?
嘿,刚接触汇编就敢搞带图形支持的微内核,勇气可嘉!我来一步步帮你拆解实模式和32位保护模式切换的核心逻辑,结合你的i686-elf交叉编译环境和内联GAS语法来实现,尽量讲得直白好懂~
实模式↔32位保护模式切换的核心思路与实现
你要调用的中断0x10确实只能在16位实模式下工作,所以核心逻辑就是:先从32位保护模式切回实模式,调用中断设置图形模式,再切回保护模式。下面分环节给你讲清楚,附适配你环境的代码示例。
一、切换前的准备:实模式专用GDT
保护模式用的是32位平坦段GDT,但实模式需要16位的段描述符,所以得先定义一个简化的实模式GDT:
#include <stdint.h> // GDT条目结构体(按x86规范打包) struct gdt_entry { uint16_t limit_low; uint16_t base_low; uint8_t base_mid; uint8_t access; uint8_t granularity; uint8_t base_high; } __attribute__((packed)); // GDT指针结构体 struct gdt_ptr { uint16_t limit; uint32_t base; } __attribute__((packed)); struct gdt_entry real_mode_gdt[3]; struct gdt_ptr real_mode_gdtr; // 初始化实模式GDT void init_real_mode_gdt() { // 空描述符(必须放在第一个位置) real_mode_gdt[0] = (struct gdt_entry){0}; // 16位代码段:基址0,限长0xFFFF,特权级0,可执行可读 real_mode_gdt[1] = (struct gdt_entry){ 0xFFFF, 0x0000, 0x00, 0x9F, 0x00, 0x00 }; // 16位数据段:基址0,限长0xFFFF,特权级0,可写可读 real_mode_gdt[2] = (struct gdt_entry){ 0xFFFF, 0x0000, 0x00, 0x93, 0x00, 0x00 }; real_mode_gdtr.limit = sizeof(real_mode_gdt) - 1; real_mode_gdtr.base = (uint32_t)&real_mode_gdt; }
二、完整切换流程的内联GAS实现
下面是封装好的函数,实现“保护模式→实模式→调用中断0x10→切回保护模式”的完整流程:
void switch_to_real_mode_and_set_video(uint8_t video_mode) { cli(); // 先关中断,切换过程不能被打断 // 加载实模式GDT asm volatile ( "lgdt %0\n\t" : : "m"(real_mode_gdtr) ); // 关闭保护模式:把CR0寄存器的PE位(第0位)清零 uint32_t cr0; asm volatile ( "movl %%cr0, %0\n\t" "andl $0xFFFFFFFE, %0\n\t" "movl %0, %%cr0\n\t" : "=r"(cr0) : : "memory" ); // 远跳刷新流水线,进入16位实模式 asm volatile ( "ljmp $0x08, $1f\n\t" // 0x08是实模式GDT的代码段选择子 ".code16\n\t" // 告诉GAS接下来是16位汇编 "1:\n\t" // 初始化实模式段寄存器 "movw $0x10, %%ax\n\t" // 0x10是实模式GDT的数据段选择子 "movw %%ax, %%ds\n\t" "movw %%ax, %%es\n\t" "movw %%ax, %%fs\n\t" "movw %%ax, %%gs\n\t" "movw %%ax, %%ss\n\t" // 设置实模式栈(选0x7000,避开内核初始化区域) "movl $0x7000, %%esp\n\t" // 调用中断0x10设置视频模式:AH=00是设置模式,AL传模式号 "movb $0x00, %%ah\n\t" "movb %0, %%al\n\t" "int $0x10\n\t" // 开始切回32位保护模式 "cli\n\t" "movl %%cr0, %%eax\n\t" "orl $0x01, %%eax\n\t" // 把CR0的PE位设为1,开启保护模式 "movl %%eax, %%cr0\n\t" "ljmp $0x08, $2f\n\t" // 远跳回32位代码段(假设你保护模式代码段选择子是0x08) ".code32\n\t" // 切回32位汇编模式 "2:\n\t" // 恢复保护模式的段寄存器 "movl $0x10, %%eax\n\t" // 假设你保护模式数据段选择子是0x10 "movl %%eax, %%ds\n\t" "movl %%eax, %%es\n\t" "movl %%eax, %%fs\n\t" "movl %%eax, %%gs\n\t" "movl %%eax, %%ss\n\t" : : "r"(video_mode) : "eax", "ebx", "ecx", "edx", "memory" // 声明被破坏的寄存器,避免GCC优化出错 ); sti(); // 重新开启中断 }
三、避坑关键提醒
- 一定要用
.code16/.code32伪指令:GAS需要明确知道当前是16位还是32位汇编,不然指令会编译错误 - 远跳不能省:CPU的指令流水线会缓存旧模式的指令,远跳能强制刷新,避免执行错误代码
- 实模式栈要选对:实模式下栈不能超过64KB(段寄存器左移4位+栈指针不能超过0xFFFF),还要避开内核或应用的内存区域
- 备选简化方案:如果不需要动态切换图形模式,不如在内核初始化的实模式阶段就设置好图形模式,这样就不用来回切换了,更省心
内容的提问来源于stack exchange,提问作者Sky Wilshaw
相关产品推荐
相关产品推荐

