x86汇编bootloader切换保护模式修改段寄存器致QEMU崩溃问题咨询
问题1解答
段寄存器里存储的不是GDT表项的指针,也不是单纯的索引,而是段选择子,结构拆分如下:
- 最低2位:RPL(请求特权级),0为内核级,3为用户级
- 第2位(索引从0开始计数):TI位,0表示从GDT中查表,1表示从LDT中查表
- 高13位:GDT/LDT表项的索引,CPU会按索引*8的偏移量从描述符表中读取对应表项
问题2解答
为什么mov gs, ax会触发崩溃
进入保护模式后,所有段寄存器的加载规则都会发生变化:此时往段寄存器写入的值会被CPU当做段选择子解析,不能再像实模式一样直接塞段地址。你给ax赋值的是0xb000,CPU解析这个值的时候,高13位是0xb000 >> 3 = 0x1600,远超过你GDT的实际表项数量(总共仅定义了3个GDT表项,合法索引最大为2),直接触发段不存在异常。你没有安装中断描述符表处理异常,CPU会触发三重故障直接重启,表现为QEMU循环崩溃。
不管是ds/es/fs/gs/ss哪个段寄存器,在保护模式下写入非法选择子都会触发异常,不存在附加段寄存器可以随便写的规则。
为什么mov [0xa000:0000], byte 0x01会编译报错
这种直接写立即数作为段前缀的语法是实模式下的特有写法,nasm在处理32位保护模式代码时不支持该语法。保护模式下的段基址是提前定义在GDT表项中的,指令里的段前缀只能指定段寄存器,不能直接写段地址。
补充:你当前的GDT定义的是基址为0的平坦数据段,直接访问
0xA0000这个虚拟地址就对应VGA缓冲区的物理地址,不需要修改任何段寄存器,直接写mov byte [0xA0000], 0x01就能实现和实模式下相同的像素绘制效果。
问题3解答
现有GDT相关代码的问题
- 没有刷新CS寄存器:你设置CR0的保护模式位之后,必须立即执行一次远跳转加载代码段选择子到CS寄存器,否则CS里还是实模式的旧值,后续执行32位代码会发生不可预知的错误。你现在用的近跳转
jmp cstart不会修改CS的值,正确写法是jmp CODE:cstart,这是核心问题之一。 - 没有指定32位汇编模式:你进入保护模式后还是默认使用
bits 16的汇编模式,nasm会把后续代码编译成16位机器码,在32位保护模式下执行会出错,需要在cstart标签前加bits 32指定汇编模式。 - 没有初始化SS段寄存器:你注释了
mov ss, eax的代码,虽然手动设置了esp,但SS还是实模式的旧值,后续只要用到栈操作就会触发异常。 - A20线开启逻辑不可靠:仅用
int 0x15的方式开启A20线兼容性很差,部分旧机型/模拟器不支持该BIOS调用,建议补充键盘控制器开启A20的备用逻辑。
优化方向
- 固定使用平坦内存模型,代码段和数据段的基址都设为0、限长4GB,这样所有虚拟地址和物理地址一一对应,开发C语言的时候不需要考虑段转换问题。
- 可以提前预留TSS段、用户态代码/数据段的表项位置,方便后续扩展多任务、用户态功能。
- 把GDT的定义用宏封装,避免手动写二进制位出错。
内容的提问来源于stack exchange,提问作者Jack_of_all_spades
相关产品推荐
相关产品推荐

