前言
上篇文章中,我们写了一个简单的 loader 作为被加载的演示对象。我们知道 loader 是用来加载 kernel 的,今天我们就编写 loader 代码完成这件事情。
实模式下的内存地址
我们计划把 kernel 加载到内存的 0x10000 地址处。
不过面临一个挑战,实模式下地址线为 20 位,寄存器只有 16 位,要想通过寄存器去构成这 20 位的主存地址,必须采取一种特殊的方式。
当指令要想访问某个内存地址时,它通常需要用下面的这种格式来表示:
(段基址:段内偏移)
其中第一个字段是段基址,它的值由段寄存器来提供(一般来说,段寄存器有 6 种,分别为 cs、ds、ss、es、fs、gs),其中 cs 不可以直接通过汇编指令赋值,其它可以。
最终:
物理地址 = (段基址 << 4) + 段内偏移
段基址
我们想要把 kernel 加载到 0x10000 处,就要计算段基址和段内偏移,可知段基址为 0x1000,段内偏移为 0x0。
所以就要给段基址寄存器赋值 0x1000,段偏移寄存器赋值 0x0。
代码
loader.S
.code16
.global _start
_start:
mov $0x1000, %ax
mov %ax, %ds
mov %ax, %ss
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
# 加载 loader 到内存
mov $0x0000, %bx # 要加载到的内存地址
mov $0x0003, %cx # ch:磁道号(0x00),cl:起始扇区号(0x03),(扇区 0x01 为 MBR, 扇区 0x02 为 loader)
mov $0x02, %ah # ah:读磁盘命令
mov $1, %al # al:读取的扇区数量,必须小于128,暂时设置成 1 个扇区
mov $0x0080, %dx # dh:磁头号,dl:驱动器号0x80(磁盘1)
int $0x13
# 跳转到 loader
jmp $0x1000, $0x0000
jmp .
我们可以通过 mov 指令,向 ds、ss、es、fs、gs 段寄存器赋值 0x1000,但是 cs 段基址寄存器就不可以使用 mov 指令赋值了,需要使用 jmp 指令。如 jmp $0x1000, $0x0000 就会将 cs 赋值为 0x1000。
start.S
.code16
.global _start
_start:
jmp .
kernel 的代码目前作为演示就写个简例。
不过注意要在链接时指定代码入口地址为 0x0000(为调试做准备)。
Makefile
OUTPUT=../_build/kernel
all:
mkdir -p ${OUTPUT}
as --32 -g -o ${OUTPUT}/start.o start.S
ld -m elf_i386 -Ttext=0x00000 ${OUTPUT}/start.o -o ${OUTPUT}/start.elf
objcopy -O binary ${OUTPUT}/start.elf ${OUTPUT}/kernel.bin
objdump -x -d -S ${OUTPUT}/start.elf > ${OUTPUT}/start_dis.txt
clean:
-rm ${OUTPUT}/*
调试
最终 loader 将 kernel 加载到内存的 0x10000 处,并跳转到该位置运行。
可以看到,段寄存器值全为 0x1000,段内偏移为 0x0(eip),该位置对应的汇编代码为 start.S:5 jmp .
。