001主引导扇区
一、主导扇区代码
[bits 16]mov ax,3
int 0x10mov ax,0
mov dx,ax
mov es,ax
mov ss,ax
mov sp,0x7c00mov ax,0xb800
mov ds,ax
mov byte [0],'H'jmp $times 510-($-$$) db 0
db 0x55,0xaa
这里没有使用org 0x7c00 是因为在elf
这是一个典型的 16 位实模式下的引导扇区(Boot Sector)汇编代码。它被设计成一个独立运行的程序,由 BIOS 加载到内存地址 0x7c000x7c000x7c00 处执行。
这段代码非常简洁,主要完成了 清屏、堆栈初始化 和 屏幕输出一个字符 的功能。
💻 汇编代码逐行解析
1. 模式设定和清屏
| 代码 | 汇编指令 | 作用 | 详解 |
|---|---|---|---|
[bits 16] | 伪指令 | 设置代码为 16 位模式 | 告诉汇编器(如 NASM),后续的代码要按照 16 位 CPU 的规则(如寄存器大小、指令长度)进行编译。 |
mov ax,3 | 移动数据 | 准备调用 BIOS 视频服务 3 号功能。 | AXAXAX 寄存器用于传递功能号。AHAHAH (高 8 位) 设为 0x000x000x00(视频服务)或 0x030x030x03(文本模式)。这里 AX=3AX=3AX=3 表示进入 80×2580 \times 2580×25 彩色文本模式。 |
int 0x10 | 软件中断 | 调用 BIOS 视频服务。 | 刷新屏幕,进入指定的文本模式。这相当于执行了清屏操作。 |
2. 寄存器和堆栈初始化
这段代码是为了在代码执行前,清理和设置所有段寄存器以及堆栈指针,以建立一个稳定的运行环境。
| 代码 | 汇编指令 | 作用 | 详解 |
|---|---|---|---|
mov ax,0 | 移动数据 | 将 AXAXAX 寄存器清零。 | |
mov dx,ax | 移动数据 | 将 DXDXDX 寄存器清零。 | |
mov es,ax | 移动数据 | 将 附加段寄存器(ES) 清零(ES=0ES=0ES=0)。 | |
mov ss,ax | 移动数据 | 将 堆栈段寄存器(SS) 清零(SS=0SS=0SS=0)。 | SS:SPSS:SPSS:SP 定义了堆栈的位置。如果 SS=0SS=0SS=0,则堆栈在内存的低地址区域。 |
mov sp,0x7c00 | 移动数据 | 设置 堆栈指针(SP)。 | 由于代码本身被加载到 0x7c000x7c000x7c00,将堆栈指针也设为 0x7c000x7c000x7c00 意味着堆栈从引导代码的起始处向低地址增长,防止覆盖引导代码。 |
3. 屏幕输出操作
这段代码使用 内存映射 I/O 的方式,直接向 PC 内存中的显存写入数据,以在屏幕上显示字符。
| 代码 | 汇编指令 | 作用 | 详解 |
|---|---|---|---|
mov ax,0xb800 | 移动数据 | 将 彩色文本模式显存的起始地址 放入 AXAXAX。 | 0xB80000xB80000xB8000 是显存的物理起始地址。在实模式下,段寄存器要用 0xB8000xB8000xB800 来表示这个地址。 |
mov ds,ax | 移动数据 | 将 数据段寄存器(DS) 设置为 0xB8000xB8000xB800。 | 之后的内存访问指令将使用 DSDSDS 寄存器作为段地址,指向显存。 |
mov byte [0],'H' | 移动数据 | 将字符 ‘H’ 写入显存的第一个位置。 | DS:0DS:0DS:0(即 0xB800:00000xB800:00000xB800:0000)是屏幕左上角第一个字符的位置。写入 ‘H’ (一个字节) 会让它显示在屏幕上。注意: 完整的显存单元是 2 字节(字符 + 属性),这里只写了字符,属性字节将保持不变(可能为 0x000x000x00 或之前的残留值)。 |
4. 停止执行
| 代码 | 汇编指令 | 作用 | 详解 |
|---|---|---|---|
jmp $ | 跳转指令 | 无限循环。 | $ 表示当前指令的地址。这行代码让 CPU 不断地跳转回自身,使程序停在这里,直到外部干预(如重启或关机)。这是引导扇区在完成任务后常见的停止方式。 |
5. 填充和签名(MBR 格式要求)
| 代码 | 汇编指令 | 作用 | 详解 |
|---|---|---|---|
times 510-($-$$) db 0 | 伪指令 | 填充零字节直到第 510 字节。 | $$ 是文件起始地址,$ 是当前地址。($-$$) 计算了当前代码的总长度。这个指令用 0x000x000x00 填充剩余空间,确保总长度是 510 字节。 |
db 0x55,0xaa | 定义字节 | 写入 MBR 签名。 | 这是 主引导记录(MBR) 格式要求的 结束标志。BIOS 会检查这最后两个字节是否为 0x550x550x55 和 0xAA0xAA0xAA。如果不是,BIOS 会认为这不是一个有效的引导扇区,并拒绝加载它。 |
总结
这段代码是一个完整的、最小化的 16 位 MBR 引导程序,它的功能是:
- 初始化 AX=3AX=3AX=3 并调用 INT0x10INT 0x10INT0x10 清屏。
- 设置段寄存器和堆栈指针。
- 通过直接操作 0xB8000xB8000xB800 处的显存,在屏幕左上角显示字符 ‘H’。
- 进入无限循环停止执行。
- 用 0x000x000x00 填充到 510 字节,并在最后写入 0x55AA0x55AA0x55AA 签名。
二、如何调试和编写Makefile文件
打开vscode中左侧第四个的debug文件点一下,修改lanch.json文件,先别问为什么将我的文件复制进去就好了,刚开始不需要将时间浪费在配置环境上面
{"version": "0.2.0","configurations": [{"name": "Debug EcpOS (QEMU)","type": "cppdbg","request": "launch","program": "${workspaceFolder}/build/kernel.elf","cwd": "${workspaceFolder}","MIMode": "gdb","miDebuggerPath": "/usr/bin/gdb","miDebuggerServerAddress": "localhost:1234","stopAtEntry": true,"setupCommands": [{"description": "Enable pretty-printing for gdb","text": "-enable-pretty-printing","ignoreFailures": true}]}]
}
Makefile文件的编写
一般就是预处理,汇编,链接,提取纯二进制代码
# 工具链定义
NASM := nasm
GCC := gcc
LD := ld
OBJCOPY := objcopy
QEMU := qemu-system-i386# 编译选项
# 使用 -f elf32 来生成 ld 可以处理的对象文件
AFLAGS := -f elf32 -g -F dwarf
LDFLAGS := -m elf_i386 -nostdlib# 目录和源文件
BUILD_DIR := build
SRC_DIR_BOOT := boot# 源文件
STAGE1_SRC := $(SRC_DIR_BOOT)/stage1.asm# 目标文件
# 1. 对象文件 (.o)
STAGE1_OBJ := $(BUILD_DIR)/boot/stage1.o
# 2. ELF 可执行文件 (用于调试)
KERNEL_ELF := $(BUILD_DIR)/kernel.elf
# 3. 纯二进制文件 (用于镜像)
STAGE1_BIN := $(BUILD_DIR)/boot/stage1.bin
# 4. 最终磁盘镜像
IMG := ecpos.img# 伪目标
.PHONY: all clean run rebuild debug# 默认目标
all: $(IMG)# --- 编译和链接规则 ---# 汇编 stage1.asm 为对象文件 (.o)
$(STAGE1_OBJ): $(STAGE1_SRC)@echo "Assembling $< to $@..."@mkdir -p $(dir $@)$(NASM) $(AFLAGS) $< -o $@# 链接最终的 ELF 文件
# 这是 GDB 进行调试时需要加载的文件
$(KERNEL_ELF): $(STAGE1_OBJ)@echo "Linking $@..."@mkdir -p $(dir $@)$(LD) $(LDFLAGS) -Ttext 0x7c00 $^ -o $@# 从 ELF 文件中提取纯二进制代码
$(STAGE1_BIN): $(KERNEL_ELF)@echo "Extracting binary from $< to $@..."@mkdir -p $(dir $@)$(OBJCOPY) -O binary $< $@# --- 镜像和运行规则 ---# 创建最终的磁盘镜像
$(IMG): $(STAGE1_BIN)@echo "Creating disk image $(IMG)..."# 确保文件大小正好是 512 字节,并带有 MBR 签名cat $^ > $@@echo "Image created successfully: $(IMG) (`stat -c '%s' $@` bytes)"# 运行 QEMU
run: $(IMG)@echo "Booting with QEMU..."$(QEMU) -drive format=raw,file=$(IMG),if=floppy -boot order=a# 以调试模式运行 QEMU
# 注意:debug 依赖于 all,确保在调试前所有文件都已正确生成
debug: all@echo "Booting with QEMU in debug mode (waiting for GDB on localhost:1234)..."$(QEMU) -cpu 486 -s -S -drive format=raw,file=$(IMG),if=floppy -boot order=a# --- 清理规则 ---# 强制重新编译
rebuild: clean all# 清理所有生成的文件
clean:@echo "Cleaning up..."-rm -rf $(BUILD_DIR) $(IMG)
🛠️ 1. 工具链和编译选项 (Toolchain & Flags)
这部分定义了使用的工具和参数。
| 变量 | 值 | 变化及作用 |
|---|---|---|
AFLAGS | -f elf32 -g -F dwarf | 保持不变。告诉 NASM 生成 32 位 ELF 格式对象文件,并包含 调试信息。 |
LDFLAGS | -m elf_i386 -nostdlib | 移除了 -Ttext 0x7c00。现在 LDFLAGS 只负责指定链接格式和禁用标准库。 |
$(LD) ... -Ttext 0x7c00 $^ -o $@ | (见 KERNEL_ELF 规则) | Ttext 地址设置被移到了 KERNEL_ELF 规则的命令行中。这样做使 LDFLAGS 更通用,将特定地址的设置留给具体的目标规则。 |
📦 2. 文件和目录定义 (Files & Directories)
这部分定义了构建过程中的输入、中间和输出文件,与前一个版本保持一致。
| 目标文件 | 阶段 | 格式 | 依赖关系 (用于构建) |
|---|---|---|---|
STAGE1_OBJ | 汇编阶段 | ELF 32-bit (.o) | stage1.asm |
KERNEL_ELF | 链接阶段 | ELF 32-bit | stage1.o |
STAGE1_BIN | 提取阶段 | Pure Binary (.bin) | kernel.elf |
IMG | 镜像阶段 | Raw Image (.img) | stage1.bin |
🏗️ 3. 编译和链接规则 (Build Rules)
这部分定义了从汇编代码到纯二进制文件的转换过程。
规则 1: 汇编 (STAGE1_OBJ)
$(STAGE1_OBJ): $(STAGE1_SRC)$(NASM) $(AFLAGS) $< -o $@
- 使用
NASM将汇编源码 (stage1.asm) 汇编为对象文件 (stage1.o)。
规则 2: 链接 (KERNEL_ELF) (关键变更)
$(KERNEL_ELF): $(STAGE1_OBJ)$(LD) $(LDFLAGS) -Ttext 0x7c00 $^ -o $@
- 功能: 使用
LD将对象文件链接成 ELF 文件。 - 关键点:
-Ttext 0x7c00明确放在了命令行中,确保了代码段(.text)的起始虚拟地址被设定为 0x7c000x7c000x7c00,这是 BIOS 加载 MBR 引导扇区的约定地址。
规则 3: 提取纯二进制 (STAGE1_BIN)
$(STAGE1_BIN): $(KERNEL_ELF)$(OBJCOPY) -O binary $< $@
- 使用
OBJCOPY从包含调试信息的 ELF 文件中剥离所有元数据,得到一个 纯粹的、可直接执行的机器码文件 (stage1.bin)。
💾 4. 镜像和运行规则 (Image & Run)
这部分定义了如何打包二进制文件和在模拟器中运行。
规则 4: 创建最终镜像 ($(IMG)) (关键变更)
$(IMG): $(STAGE1_BIN)cat $^ > $@
- 功能: 创建最终的
ecpos.img文件。 - 关键点: 这个规则非常简单地使用了
cat $^ > $@(即cat stage1.bin > ecpos.img)。 - 与上一个版本的区别:
- 旧版本: 使用
dd先创建 512 字节空白文件,再用dd覆盖。这保证了文件大小是 恰好 512 字节。 - 新版本: 使用
cat直接复制stage1.bin。
- 旧版本: 使用
- 隐含要求: 为了让这个
cat规则能正常工作,您的stage1.asm源码中 必须 包含times 510-($-$$) db 0和db 0x55,0xaa这两条伪指令,以确保stage1.bin自身恰好是 512 字节,并且带有正确的 MBR 签名。
规则 5/6: 运行 (run) 和调试 (debug)
run: 使用 QEMU 正常启动镜像。debug: 使用 QEMU 启动,并通过-s -S在localhost:1234开启 GDB 调试服务器并暂停 CPU,等待开发者连接 GDB 进行调试。
总结
这个 Makefile 建立了一个完整的引导程序开发流程:汇编 → 链接定位地址 → 剥离为纯代码 → 创建镜像 → 模拟运行/调试。
如何进行调试
在当前的文件夹中,输入make debug 在程序中打下端点,点击调试按钮
在左侧的watch中可以查看寄存器的值
输入下面指令可以查看内存值
-exec x/1c 0xb8000 # 以字符显示第一个字节,应该为 'H'
-exec x/2bx 0xb8000 # 以十六进制显示字符和属性,例: 0x48 0x07
-exec x/16bx 0xb8000 # 查看更多字节
