操作系统的引导

	#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 (;;) {}
}

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值