5.从零开始写LINUX内核--从实模式到保护模式的过渡实现
AT&T 语法增强版 Setup 程序开发:从实模式到保护模式的过渡实现(Ubuntu 20.04 全流程)
在操作系统启动流程中,Setup 程序承担着从实模式向保护模式过渡的关键任务。本文基于前期引导扇区与基础 Setup 程序框架,开发增强版 Setup 程序,实现硬件信息收集、GDT 配置及保护模式切换功能,完整覆盖从 BIOS 到内核加载前的初始化流程。
一、开发环境与工具链复用
1. 工具链依赖
沿用引导扇区开发的工具链(版本验证通过),确保环境一致性:
bash
# 工具链清单(同前期配置,无需重复安装)
sudo apt update && sudo apt install -y \
binutils gcc qemu-system-x86 qemu-utils \
util-linux xxd vim nano
2. 环境验证
通过前期编写的 check_boot_env.sh
脚本验证环境,确保 16 位汇编支持与工具完整性:
bash
chmod +x check_boot_env.sh
./check_boot_env.sh
显示 [SUCCESS] 开发环境就绪
即可进入开发。
二、增强版 Setup 程序设计与实现
增强版 Setup 程序需完成以下核心任务:
- 收集硬件信息(内存大小、显示参数、磁盘信息)
- 配置中断控制器与键盘控制器
- 构建全局描述符表(GDT)
- 从实模式切换到 32 位保护模式
1. 代码实现(setup.s)
asm
.code16 ; 初始运行于16位实模式
.text
.global _start_setup/* 常量定义 */
INITSEG = 0x9000 ; 硬件信息存储段地址
SETUPSEG = 0x9020 ; Setup程序自身段地址_start_setup:/* 初始化段寄存器(CS=SETUPSEG,同步DS/ES) */movw %cs, %axmovw %ax, %ds ; 数据段 = 代码段movw %ax, %es ; 附加段 = 代码段/* 打印启动信息:"setup is running [enhanced]" */movw $setup_msg, %axmovw %ax, %bp ; ES:BP 指向字符串movw $0x1301, %ax ; AH=0x13(字符串显示), AL=0x01(带属性+光标移动)movw $0x000C, %bx ; BH=0(页0), BL=0x0C(红底白字)movw $24, %cx ; 字符串长度movb $3, %dh ; 行号3movb $0, %dl ; 列号0int $0x10 ; 调用BIOS显示中断/* 阶段1:收集硬件信息并存储到INITSEG段 */call collect_hardware_info/* 阶段2:准备保护模式环境 */call prepare_protected_mode/* 阶段3:切换到保护模式并跳转 */call enter_protected_modehang:jmp hang ; 异常情况下的死循环/* 硬件信息收集函数 */
collect_hardware_info:/* 1. 收集光标位置(BIOS中断0x10, 功能0x03) */movb $0x03, %ahxorw %bh, %bh ; 页0int $0x10movw $INITSEG, %axmovw %ax, %dsmovw %dx, (0) ; 存储光标位置到0x9000:0000/* 2. 收集扩展内存大小(BIOS中断0x15, 功能0x88) */movb $0x88, %ahint $0x15movw %ax, (2) ; 存储到0x9000:0002(KB)/* 3. 收集显示模式信息(BIOS中断0x10, 功能0x0F) */movb $0x0F, %ahint $0x10movw %bx, (4) ; 存储显示页/颜色模式到0x9000:0004movw %ax, (6) ; 存储列数到0x9000:0006/* 4. 收集硬盘参数(从BIOS数据区复制) */; 复制第一硬盘参数(0x41*4=0x104)movw $0x0000, %axmovw %ax, %dsldsw (0x104), %si ; SI = 硬盘参数表地址movw $INITSEG, %axmovw %ax, %esmovw $0x0080, %di ; 目标地址:0x9000:0080movw $0x10, %cx ; 复制16字节repmovsb; 复制第二硬盘参数(0x46*4=0x118)movw $0x0000, %axmovw %ax, %dsldsw (0x118), %simovw $INITSEG, %axmovw %ax, %esmovw $0x0090, %di ; 目标地址:0x9000:0090movw $0x10, %cxrepmovsbret/* 保护模式环境准备函数 */
prepare_protected_mode:cli ; 关闭中断(避免切换期间中断干扰)/* 移动内核代码到0x0000:0000(假设内核在0x1000段) */movw $0x1000, %ax ; 源段起始地址
do_move:movw %ax, %es ; ES = 目标段(初始为0x1000)addw $0x1000, %ax ; 源段 = 目标段 + 0x1000cmpw $0x9000, %ax ; 移动到0x9000段为止jz end_movemovw %ax, %ds ; DS = 源段xorw %si, %si ; SI=0(源偏移)xorw %di, %di ; DI=0(目标偏移)movw $0x8000, %cx ; 复制0x8000字(64KB)repmovsw ; 批量移动jmp do_move
end_move:/* 配置8042键盘控制器(允许保护模式下键盘中断) */call empty_8042 ; 等待控制器空闲movb $0xD1, %al ; 命令:写输出端口outb %al, $0x64call empty_8042movb $0xDF, %al ; 允许A20地址线outb %al, $0x60call empty_8042/* 配置8259中断控制器(屏蔽所有中断,后续由内核启用) */movb $0x11, %al ; 初始化命令字ICW1outb %al, $0x20 ; 主8259outb %al, $0xA0 ; 从8259.word 0x00eb, 0x00eb ; 短延迟(替代nop)movb $0x20, %al ; 主8259向量基地址0x20outb %al, $0x21.word 0x00eb, 0x00ebmovb $0x04, %al ; 主8259级联标识outb %al, $0x21.word 0x00eb, 0x00ebmovb $0x01, %al ; 8086模式outb %al, $0x21.word 0x00eb, 0x00eboutb %al, $0xA1 ; 从8259配置(同上).word 0x00eb, 0x00ebmovb $0xFF, %al ; 屏蔽所有中断outb %al, $0x21outb %al, $0xA1ret/* 进入保护模式函数 */
enter_protected_mode:/* 加载GDT(全局描述符表) */movw $SETUPSEG, %axmovw %ax, %dslgdt gdt_48 ; 加载GDT描述符/* 设置CR0寄存器PE位(保护模式使能) */movl %cr0, %eaxorl $0x00000001, %eax ; 第0位=1movl %eax, %cr0/* 远跳转到保护模式代码(清空流水线) */.byte 0x66, 0xea ; 32位远跳转前缀.long 0x00000000 ; 偏移地址0.word 0x0008 ; 段选择子(GDT代码段)/* 8042控制器空闲等待函数 */
empty_8042:.word 0x00eb, 0x00eb ; 延迟inb $0x64, %al ; 读取状态寄存器testb $0x02, %al ; 检查输入缓冲区是否为空jnz empty_8042 ; 非空则继续等待ret/* 全局描述符表(GDT)定义 */
gdt:/* 空描述符(必须存在) */.word 0, 0, 0, 0/* 代码段描述符:基地址0,限长0xFFFFF,可读可执行,特权级0 */.word 0xFFFF ; 限长(低16位).word 0x0000 ; 基地址(低16位).byte 0x00 ; 基地址(中8位).byte 0b10011010 ; 访问位:P=1, DPL=0, 代码段, 可执行, 可读.byte 0b11001111 ; 标志位:G=1(4KB粒度), D=1(32位), 限长高4位.byte 0x00 ; 基地址(高8位)/* 数据段描述符:基地址0,限长0xFFFFF,可读可写,特权级0 */.word 0xFFFF.word 0x0000.byte 0x00.byte 0b10010010 ; 访问位:P=1, DPL=0, 数据段, 可写.byte 0b11001111.byte 0x00/* 视频段描述符:基地址0xB8000(VGA显存),限长0x07FF */.word 0x07FF.word 0x8000.byte 0x0B.byte 0b10010010 ; 数据段,可写.byte 0b00000000 ; 16位模式兼容.byte 0x00gdt_end:/* GDT描述符(供lgdt指令使用) */
gdt_48:.word gdt_end - gdt - 1 ; GDT限长.word gdt + SETUPSEG * 16, 0 ; GDT基地址(段地址转换为线性地址)/* 字符串定义 */
setup_msg:.ascii "setup is running [enhanced]"/* 填充至2048字节(4个扇区) */.fill 2048 - (.-_start_setup), 1, 0
三、镜像构建与验证流程
1. 修改 Makefile 适配增强版 Setup
沿用前期 Makefile 框架,无需修改(已兼容 setup.s 编译):
makefile
# 工具链定义(同前期)
AS := as --32
LD := ld -m elf_i386
LDFLAGS := -Ttext 0x0 -s --oformat binaryall: linux.imglinux.img: build bootsect setup./build > linux.imgbootsect: bootsect.o$(LD) $(LDFLAGS) -o $@ $<bootsect.o: bootsect.s$(AS) -o $@ $<setup: setup.o$(LD) $(LDFLAGS) -e _start_setup -o $@ $<setup.o: setup.s$(AS) -o $@ $<build: build.cgcc -o $@ $<clean:rm -f *.o bootsect setup build linux.imgrun:qemu-system-i386 -fda linux.img -boot a -vga std -debugcon stdio
2. 编译与生成镜像
bash
# 清理旧文件并重新编译
make clean && make
生成 linux.img
镜像文件,包含引导扇区(512 字节)和增强版 Setup 程序(2048 字节)。
3. 关键验证步骤
bash
# 1. 验证Setup程序大小(必须为2048字节)
ls -l setup | awk '{print "setup大小:" $5 "字节(应等于2048)"}'# 2. 查看GDT表定义(确保无语法错误)
objdump -s -j .text setup.o | grep -A 20 "gdt:"# 3. 验证引导扇区标志
xxd -s 510 -l 2 bootsect # 应显示0000200: aa55
四、QEMU 运行与调试配置
1. 基础运行命令
bash
make run # 或直接执行:qemu-system-i386 -fda linux.img -boot a -vga std
成功运行后,QEMU 窗口将显示:
- 第 1 行:
Setup has been loaded
(引导扇区输出) - 第 3 行:
setup is running [enhanced]
(增强版 Setup 输出) - 随后进入保护模式切换流程(屏幕可能短暂黑屏,属正常现象)
2. 调试模式运行
通过 QEMU 调试输出观察硬件信息收集过程:
bash
qemu-system-i386 -fda linux.img -boot a -vga std -debugcon stdio
调试信息将显示:
- BIOS 加载 Setup 程序的过程
- 硬盘参数读取结果
- 保护模式切换时的中断控制器配置日志
五、常见问题解决
保护模式切换后无响应
- 检查 GDT 基地址计算:
gdt_48
中基地址需转换为线性地址(SETUPSEG * 16 + gdt
) - 验证
lgdt
指令前ds
寄存器是否正确设置为SETUPSEG
- 确认远跳转指令的段选择子(
0x0008
)对应 GDT 中的代码段
- 检查 GDT 基地址计算:
硬件信息收集异常
- 内存大小读取失败:部分 BIOS 不支持
int 0x15
功能0x88
,可改用功能0xE820
兼容 - 硬盘参数为空:在虚拟机中需配置至少 1 个硬盘设备
- 内存大小读取失败:部分 BIOS 不支持
A20 地址线启用失败
- 检查 8042 控制器交互流程,确保
empty_8042
函数正确等待控制器空闲 - 替换延迟指令(
.word 0x00eb, 0x00eb
)为多个nop
指令(兼容部分硬件)
- 检查 8042 控制器交互流程,确保