#include "boot.h"
// 16位代码,务必加上
.code16
.text
.global _start
.extern boot_entry
_start:
// 重置数据段寄存器
mov $0, %ax
mov %ax, %ds
mov %ax, %ss
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
// 根据https://wiki.osdev.org/Memory_Map_(x86)
// 使用0x7c00之前的空间作栈,大约有30KB的RAM,足够boot和loader使用
mov $_start, %esp
// 显示boot加载完成提示
mov $0xe, %ah
mov $'L', %al
int $0x10
// 加载loader,只支持磁盘1
// https://wiki.osdev.org/Disk_access_using_the_BIOS_(INT_13h)
read_loader:
mov $0x8000, %bx // 读取到的内存地址
mov $0x2, %cx // ch:磁道号,cl起始扇区号
mov $0x2, %ah // ah: 0x2读磁盘命令
mov $64, %al // al: 读取的扇区数量, 必须小于128,暂设置成32KB
mov $0x0080, %dx // dh: 磁头号,dl驱动器号0x80(磁盘1)
int $0x13
jc read_loader
// 跳转至c部分执行,再由c部分做一些处理
jmp boot_entry
// 原地跳转
jmp .
// 引导结束段
.section boot_end, "ax"
boot_sig: .byte 0x55, 0xaa
.global symbal的意思是symbal地址处的内容对文件外是可见的,也就是此文件外的程序也可以引用这个标号对应的位置。
_start标号处的代码就是做了一些简单的初始化。将一些寄存器全部置为0。esp寄存器指向栈底,这里是0000:7c00的位置向下都是作栈。这里用的为何是esp而不是sp呢,ss:sp指出栈地址的位置。而且汇编之后的代码,这一步也是0x7c00送到了esp寄存器。这里我觉得应该是作者的习惯吧。毕竟这个算是32位机了。地址实际上也是32位了。
然后利用bios中断指令显示一些基础的初始化工作完成。
然后接着是把加载器的代码加载到内存0000:8000这个位置上来。利用寄存器去读磁盘上的文件到内存,具体内容在注释。int 13中断主要用于各种与磁盘有关的操作。
最后还有个标准引导程序的结束标志。0x55,0xaa。
下一步就是到加载loader了。从jmp boot_entry开始执行。
下面是boot_entry的代码。
__asm__(".code16gcc");
#include "boot.h"
#define LOADER_START_ADDR 0x8000 // loader加载的地址
/**
* Boot的C入口函数
* 只完成一项功能,即从磁盘找到loader文件然后加载到内容中,并跳转过去
*/
void boot_entry(void) {
((void (*)(void))LOADER_START_ADDR)();
}
这里首先定义了一个地址,0000:0x8000,然后在c函数中将这个地址看成一个函数并且执行。这里需要强调的是,jmp boot_entry是个跳转指令,但其本身的地址是在reader_loader程序段下面,
具体代码如下
// 原地跳转
jmp .
7c2d: eb fe jmp 7c2d <read_loader+0x14>
7c2f: 90 nop
00007c30 <boot_entry>:
/**
* Boot的C入口函数
* 只完成一项功能,即从磁盘找到loader文件然后加载到内容中,并跳转过去
*/
void boot_entry(void) {
((void (*)(void))LOADER_START_ADDR)();
7c30: 66 b8 00 80 00 00 mov $0x8000,%eax
7c36: 66 ff e0 jmpl *%eax
可以看处C程序和汇编代码的地址是相邻的。接着就是跳到boot_entry处的代码开始执行。这个函数只完成了一项工作,就是到让loader执行。
下面是boot_entry处的代码,是一段简单的汇编程序。
// 16位代码,务必加上
.code16
.text
.extern loader_entry
.global _start
_start:
// 栈和段等沿用之前的设置,也可以重新设置
// 这里简单起见,就不做任何设置了
// 你可能会想,直接跳到loader_entry,但这样需要先知识loader_entry在哪儿
// boot没有这个能力做到,所以直接用汇编,以便_start处于整个bin文件开头,这样boot直接跳到开头就可以
jmp loader_entry
这个地方接着又跳转到了loader_entry处执行,且这个地址在外部,所以把它声明为.extern
然后loader_entry是一个c函数地址,在里面做了一些基础的引导之前的工作。
void loader_entry(void) {
show_msg("....loading.....\r\n");
detect_memory();
enter_protect_mode();
for(;;) {}
}
先是简单的展示一个字符串,表示正在加载
__asm__(".code16gcc");
#include "loader.h"
boot_info_t boot_info; // 启动参数信息
/**
* BIOS下显示字符串
*/
static void show_msg (const char * msg) {
char c;
// 使用bios写显存,持续往下写
while ((c = *msg++) != '\0') {
__asm__ __volatile__(
"mov $0xe, %%ah\n\t"
"mov %[ch], %%al\n\t"
"int $0x10"::[ch]"r"(c));
}
}
然后是检测可用内存块。
// 参考:https://wiki.osdev.org/Memory_Map_(x86)
// 1MB以下比较标准, 在1M以上会有差别
// 检测:https://wiki.osdev.org/Detecting_Memory_(x86)#BIOS_Function:_INT_0x15.2C_AH_.3D_0xC7
static void detect_memory(void) {
uint32_t contID = 0;
SMAP_entry_t smap_entry;
int signature, bytes;
show_msg("try to detect memory:");
// 初次:EDX=0x534D4150,EAX=0xE820,ECX=24,INT 0x15, EBX=0(初次)
// 后续:EAX=0xE820,ECX=24,
// 结束判断:EBX=0
boot_info.ram_region_count = 0;
for (int i = 0; i < BOOT_RAM_REGION_MAX; i++) {
SMAP_entry_t * entry = &smap_entry;
__asm__ __volatile__("int $0x15"
: "=a"(signature), "=c"(bytes), "=b"(contID)
: "a"(0xE820), "b"(contID), "c"(24), "d"(0x534D4150), "D"(entry));
if (signature != 0x534D4150) {
show_msg("failed.\r\n");
return;
}
// todo: 20字节
if (bytes > 20 && (entry->ACPI & 0x0001) == 0){
continue;
}
// 保存RAM信息,只取32位,空间有限无需考虑更大容量的情况
if (entry->Type == 1) {
boot_info.ram_region_cfg[boot_info.ram_region_count].start = entry->BaseL;
boot_info.ram_region_cfg[boot_info.ram_region_count].size = entry->LengthL;
boot_info.ram_region_count++;
}
if (contID == 0) {
break;
}
}
show_msg("ok.\r\n");
}
最后是进入32位保护模式。进入保护模式,需要开启第20根地址线,同时还要加载临时GDT表,开启寄存器的保护位。
这里是GDT表的具体格式
// GDT表。临时用,后面内容会替换成自己的
uint16_t gdt_table[][4] = {
{0, 0, 0, 0},
{0xFFFF, 0x0000, 0x9A00, 0x00CF},
{0xFFFF, 0x0000, 0x9200, 0x00CF},
};
/**
* 进入保护模式
*/
static void enter_protect_mode() {
// 关中断
cli();
// 开启A20地址线,使得可访问1M以上空间
// 使用的是Fast A20 Gate方式,见https://wiki.osdev.org/A20#Fast_A20_Gate
uint8_t v = inb(0x92);
outb(0x92, v | 0x2);
// 加载GDT。由于中断已经关掉,IDT不需要加载
lgdt((uint32_t)gdt_table, sizeof(gdt_table));
// 打开CR0的保护模式位,进入保持模式
uint32_t cr0 = read_cr0();
write_cr0(cr0 | (1 << 0));
// 长跳转进入到保护模式
// 使用长跳转,以便清空流水线,将里面的16位代码给清空
far_jump(8, (uint32_t)protect_mode_entry);
}
这里的代码汇编之后挺疑惑的,
00008000 <_start>:
_start:
// 栈和段等沿用之前的设置,也可以重新设置
// 这里简单起见,就不做任何设置了
// 你可能会想,直接跳到loader_entry,但这样需要先知识loader_entry在哪儿
// boot没有这个能力做到,所以直接用汇编,以便_start处于整个bin文件开头,这样boot直接跳到开头就可以
jmp loader_entry
8000: e9 1d 00 jmp 8020 <loader_entry>
00008003 <protect_mode_entry>:
.text
.global protect_mode_entry
.extern load_kernel
protect_mode_entry:
// 重新加载所有的数据段描述符
mov $16, %ax // 16为数据段选择子
8003: 66 b8 10 00 8e d8 mov $0xd88e0010,%eax
mov %ax, %ds
mov %ax, %ss
8009: 8e d0 mov %ax,%ss
mov %ax, %es
800b: 8e c0 mov %ax,%es
mov %ax, %fs
800d: 8e e0 mov %ax,%fs
mov %ax, %gs
800f: 8e e8 mov %ax,%gs
// 长跳转进入到32位内核加载模式中
jmp $8, $load_kernel
8011: ea b0 89 00 00 ljmp $0x0,$0x89b0
8016: 08 00 or %al,(%bx,%si)
8018: 66 90 xchg %eax,%eax
801a: 66 90 xchg %eax,%eax
801c: 66 90 xchg %eax,%eax
801e: 66 90 xchg %eax,%eax
00008020 <loader_entry>:
// 长跳转进入到保护模式
// 使用长跳转,以便清空流水线,将里面的16位代码给清空
far_jump(8, (uint32_t)protect_mode_entry);
}
void loader_entry(void) {
8020: 66 55 push %ebp
8022: 66 57 push %edi
while ((c = *msg++) != '\0') {
8024: 66 ba 19 8b 00 00 mov $0x8b19,%edx
可以看到<loader_entry>处的代码一大段都是在下面,但偏偏长跳转的代码在那个函数外面。
接着就是跳转到保护模式了,到protect_mode_emtry处的汇编代码处执行。
// 32位保护模式下的代码
.code32
.text
.global protect_mode_entry
.extern load_kernel
protect_mode_entry:
// 重新加载所有的数据段描述符
mov $16, %ax // 16为数据段选择子
mov %ax, %ds
mov %ax, %ss
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
// 长跳转进入到32位内核加载模式中
jmp $8, $load_kernel
这里开始地址就是32位的了。可以看到长跳转那个汇编也是将地址强制转换为了32位。
将一些段选择子都初始化为16。
接着进入内核模式。
从磁盘上开始加载内核。(磁盘文件都是我们写进去的,一开始的引导程序是在磁盘第一个区,加载到内存以后主导权就是在在内存的程序中了,然后执行这段程序,这段程序将读磁盘将loader_entry加载到内存8000处的位置,接着跳转到8000处的位置开始执行,也就是说磁盘第2到第2+64(由代码推断,实际可能没这么大),都是loader_entry的程序。接着loader_entry在8000处的代码继续执行,这些代码在完成内存检测、开启地址线、加载GDT表等工作之后进入了保护模式,进入保护模式的代码又做了一些工作,这里就是实行分页机制,加载磁盘内核,然后就进入了内核工程。)
/**
* 从磁盘上加载内核
*/
void load_kernel(void) {
// 读取的扇区数一定要大一些,保不准kernel.elf大小会变得很大
// 我就吃过亏,只读了100个扇区,结果运行后发现kernel的一些初始化的变量值为空,程序也会跑飞
read_disk(100, 500, (uint8_t *)SYS_KERNEL_LOAD_ADDR);
// 解析ELF文件,并通过调用的方式,进入到内核中去执行,同时传递boot参数
// 临时将elf文件先读到SYS_KERNEL_LOAD_ADDR处,再进行解析
uint32_t kernel_entry = reload_elf_file((uint8_t *)SYS_KERNEL_LOAD_ADDR);
if (kernel_entry == 0) {
die(-1);
}
// 开启分页机制
enable_page_mode();
((void (*)(boot_info_t *))kernel_entry)(&boot_info);
for (;;) {}
}