在写setup时遇到的问题与思考
在写setup时遇到的问题与思考
首先遇到这个问题需要搞明白什么是setup。
在编写操作系统的过程中,我们首先要写汇编程序,然后将我们的代码编译为二进制之后写入软盘(或者硬盘),然后由BIOS将软盘(或者硬盘)中的内容加载到内存当中(0x7c00处,为什么是这里看另一篇文章)并执行,这样我们的操作系统内核就运行起来了。
但是这样我们面临一个问题。我们是将编译后的二进制文件(boot.o)写入的位置是0磁盘0磁道1扇区的位置,而且要求最后两个字节是0x55aa。(这是约定俗成的,不然谁知道上哪里去找你的代码呢?而且有这样的特征才能够被识别为一个操作系统)这样就有一个限制,因为一个扇区大小只有512字节,最后两个字节还必须要求是0x55aa,那么我们就只有510个字节的大小可以用了,这显然是不够我们来写一个操作系统内核的。这种情况改怎么办呢?
答案是我们写一个setup,按照与boot相同的方式写到内存当中就行了,当然不是一个随便的位置,而是参考实模式下1MB的内存布局表
发现有两个空闲的可用区域,这就是我们可以放的地方,只要将setup加载到这两个位置即可。
BIOS完成任务之后会将CPU控制权移交给boot,所以只要在boot中写一个跳转到setup的逻辑,就能够摆脱512字节的限制了。
以下是我的实现代码(setup只是输出了一个"Hello World")
; boot.asm
[org 0x7c00] ; 指定程序加载地址为 0x7C00(BIOS 加载引导扇区的地址)
[bits 16] ; 16 位实模式section .text ; 定义代码段
global _start ; 全局入口标签_start:; 保存启动驱动器号mov [boot_drive], dl ; 将 dl(启动驱动器号)保存到内存变量 boot_drive; 初始化段寄存器xor ax, ax ; 将 ax 清零(ax = 0)mov ds, ax ; 设置数据段寄存器 ds = 0mov es, ax ; 设置附加段寄存器 es = 0mov ss, ax ; 设置栈段寄存器 ss = 0mov sp, 0x7c00 ; 设置栈指针 sp = 0x7C00; 打印 "Booting..."mov si, boot_msg ; 将 boot_msg 的地址加载到 si 寄存器call print_string ; 调用 print_string 函数; 加载 setup 到内存 0x90000mov ah, 0x02 ; BIOS 中断功能号 ah = 0x02(读扇区)mov al, 1 ; al = 1(读取 1 个扇区)mov ch, 0 ; ch = 0(柱面号 0)mov cl, 2 ; cl = 2(扇区号 2)mov dh, 0 ; dh = 0(磁头号 0)mov dl, [boot_drive] ; dl = 启动驱动器号(从内存加载)mov bx, 0x9000 ; bx = 0x9000(目标段地址)mov es, bx ; es = bx(设置目标段地址)xor bx, bx ; bx = 0(偏移量清零)int 0x13 ; 调用 BIOS 中断 0x13(读磁盘)jc disk_error ; 如果 CF 标志置位(错误),跳转到 disk_error; 跳转到 setupjmp 0x9000:0x0000 ; 远跳转到 0x9000:0x0000print_string:mov ah, 0x0e ; ah = 0x0e(BIOS 打印字符功能)
.loop:lodsb ; 从 ds:si 加载字节到 al,并递增 sicmp al, 0 ; 比较 al 和 0(检查字符串结束)je .done ; 如果 al = 0,跳转到 .doneint 0x10 ; 调用 BIOS 中断 0x10(打印字符)jmp .loop ; 跳转回 .loop 继续处理
.done:ret ; 返回disk_error:mov si, error_msg ; 将 error_msg 地址加载到 sicall print_string ; 调用 print_stringjmp $ ; 死循环boot_msg db 'Booting...', 0 ; 定义字符串 "Booting...",以 0 结尾
error_msg db 'Disk read error!', 0 ; 定义错误字符串
boot_drive db 0 ; 定义变量存储启动驱动器号times 510-($-$$) db 0 ; 填充到 510 字节
dw 0xaa55 ; 引导扇区签名
; setup.asm
[org 0x0000] ; 假设加载到 0x90000,偏移从 0 开始
[bits 16] ; 16 位实模式section .text
global _start_start:; 初始化段寄存器mov ax, 0x9000 ; ax = 0x9000(段地址)mov ds, ax ; ds = 0x9000mov es, ax ; es = 0x9000mov ss, ax ; ss = 0x9000mov sp, 0x1000 ; sp = 0x1000(栈指针); 打印 "Hello, World! from setup"mov si, hello_msg ; si = hello_msg 地址call print_string ; 调用 print_stringjmp $ ; 死循环print_string:mov ah, 0x0e ; ah = 0x0e(BIOS 打印字符功能)
.loop:lodsb ; 从 ds:si 加载字节到 al,并递增 sicmp al, 0 ; 比较 al 和 0je .done ; 如果 al = 0,跳转到 .doneint 0x10 ; 调用 BIOS 中断 0x10jmp .loop ; 继续循环
.done:ret ; 返回hello_msg db 'Hello, World! from setup', 0 ; 定义字符串
其实最重要的就是知道各种寄存器,然后语句就容易理解了(问GPT吧,这个还是自己哪里不会问哪里)
最后是我遇到的问题:
一开始我是将
boot_msg db 'Booting...', 0 ; 定义字符串 "Booting...",以 0 结尾
error_msg db 'Disk read error!', 0 ; 定义错误字符串
boot_drive db 0 ; 定义变量存储启动驱动器号
三行前面加了一个[section .data]
数据段声明,希望将它们放到数据段当中,但是报错了。为什么会出现这样的问题呢?
因为分段是通过section
或者segment
语句将代码和数据分组,在现代的程序当中,比如说ELF格式,链接器会将这些段映射到内存的不同区域上。但是在引导扇区(boot)当中,使用的是NASM的-f bin格式编译成的纯二进制文件,而不是ELF等格式,-f bin是不支持多段链接的,所有的代码都必须连续存储的。实模式下的地址计算都是相当于0x7c00的偏移计算的(因为这个程序是从0x700开始的),如果进行了段分离之后,会导致mov si,boot_msg这些语句找不到正确的位置,从而导致程序出错。