跟我一起写操作系统

    返回首页    发表留言
本文作者:李德强
          第一节 复制内核
 
 

        作为一个操作系统,最重要的内容就是内存管理。我们知道当CPU加电之后,会载入BISO程序并运行,当BIOS程序运行完成之后就会将引导设备的第一个扇区内容载入到地址为0x7c00的内存当中。当我们使用软盘引导和硬盘引导时,仅仅一个扇区大小的程序是完全不够的。也就是说,我们的操作系统不可能只有512个字节这么小,随着后续功能的逐步完善,我们的操作系统内核程序将不断的扩大。那么当引导设备第一个扇区的程序被载入0x7c00处时,这部分程序的工作就是要将剩余的程序从第二个扇区开始全部载入到内存当中,使我们的操作系统内核被完整载入和执行。

        现在我们就来一起学习如何在实时模式下写程序来载入剩余的扇区程序。关于硬盘引导与软盘引导时的做法一样,只不过调用的设备号不同罢了。在实时模式下读写软盘和硬盘可以直接调用BISO中断。关于中断的相关知识这里不再做详细的说明,相信读者在学到这里时,汇编、C语言相关的准备知识也已经掌握了。好的,下面我们先看一下我们的第一个扇区被载入到内存的0x7c00之后,内存中的各个区域都存放的是什么内容:



 

        作为一个操作系统,它为其它可执行的程序提供运行的平台,为每一个程序分配相关的资源,并合理的,有效的管理内存空间。所以我们要让我们的系统内核占用内存中最有利的位置,也就是把地址为0x0的内存起始区,做为我们系统内核的起点。但是,问题来了:我们需要通过调用BISO中断程序来将我们事先存放在软盘上的程序载入到0x0处,但是一但这里的内存被内核占据了,原来存放在这里的BISO中断向量就会被内核覆盖掉。也就是说,只要我们读入一个扇区并将其载入到0x0处时,就再也不能使用BIOS中断程序了。所以为了能够有效的载入系统内核,我们采用了一种折衷的方法,就是将内核程序从软盘读入,并载入到0x7c00的后续内存地址中,然后再将内核程序复制到0x0起始区。

        细心的读者可能就会发现另外一个问题:从0x7c00开始复制程序到0x0处,如果我们的程序大小超过了0x7c00个字节,那么,在0x7c00处的程序也将在复制时被覆盖,那么复制程序则会变成一段无效的代码。也就是说,在程序自我复制到0x0时,如果内核程序大小超过了0x7c00,内存就会发生程序代码的错误覆盖,内核程序则会异常。

        我们先将软盘中的程序载入到0x7c00之后的内存区域,从0x7c00到0x8ffff,共有0x88400大小的存储空间,也就是545KB。这对于我们的操作系统内核已经足够了。为了避免上面提到的问题,我们把内核程序分为两部分:boot引导程序和kernel内核程序。引导程序处于0x7c00,内核程序处于0x9c00。引导程序负责将自身复制到0x90000处。在跳转到0x9000处开始将处于0x9c00地址的内核程序复制到0x0处。具体的过程如下图:



 

        现在我们来具体实现这部分功能。为了让我们的开发工程看起来更加直观,有条理性,我们先将工程分为3个部分:boot引导程序、kernel系统内核程序和shell外壳程序。 shell外壳程序我们暂时先不去管它,只是先创建boot、kernel、shell和include这4个文件夹。如图所示:



 

        对于内核程序kernel来说,我们的操作系统的核心功能都要在这里面来实现,但目前我们还没有用到它,在这里我们只是在kernel中创建了一个叫做kernel.c的C语言的源代码文件,并在include里创建了它相应的C语言头文件kernel.h,如下图:



 

        在kernel.h和kernel.c中只是写了一个简单的空函数,叫作start_kernel,它具体的功能我们会在后续的章节来慢慢的扩充和完善。现在的它什么功能都没有,代码如下:

include/kernel/kernel.h

#ifndef _INCLUDE_KERNEL_KERNEL_H_
#define _INCLUDE_KERNEL_KERNEL_H_

int start_kernel(int argc, char **args);

#endif /* INCLUDE_KERNEL_KERNEL_H_ */
kernel/kernel.c

#include <kernel/kernel.h>

int start_kernel(int argc, char **args)
{
        return 0;
}

 

        前面所说的boot程序通过调用BIOS中断程序来读入扇区数据并载入到指定的内存地址中,具体方法就是调用BISO的0x13号中断,这个中断过程有8个传入参数(使使用CPU的16位寄存器来传入参数),分别为:

  • ah:读写方式:2为读;3为写。

  • al:读写扇区数。

  • ch:磁道号。

  • cl:扇区号。

  • dh:柱面号。

  • dl:驱动器号:0为软驱A;1为软驱B;硬盘驱动器号为0x80。

  • es:读写扇区对应的内存段地址。

  • bx:读写扇区对应的内存偏移地址。

        根据这个BISO磁盘中断说明,我们就可以使用汇编程序来调用0x13号中断读取软盘中的扇区数据并载入到指定的位置。需要注意的是,软盘的柱面号、磁道号都是从0开始的;而扇区号则从1开始。由于第1个扇区已经被BISO程序载入到了0x7c00所以我们要从第2个扇区开始读取并载入到0x7c00 + 0x200 = 0x7e00处,并载入17个扇区。我们的boot程序占用10个扇区(大小为0x2000个字节),虽然boot程序现在还没有这么大,但为了以后的扩充,10个扇区空间也足够boot程序使用了。kernel程序目前只占用1个扇区,但我们还是载入剩下的7个扇区。具体的代码如下:

//读到es:bx所表示的 0x7e00处
movw $0x7e0, %ax
movw %ax, %es
xorw %bx, %bx
//0x2为读扇区
movb $0x2, %ah
//读入扇区数
movb $0x11, %al
//磁道号
movb $0x0, %ch
//扇区号
movb $0x2, %cl
//柱面号
movb $0x0, %dh
//驱动器号,软驱A为0,软驱B为1
movb $0x0, %dl
//调用BISO磁盘中断
int $0x13

        将剩余的程序读入之后就要将boot程序自身复制到0x90000处,在这里我们写了一个汇编过程,在执行前要使用push保存现场,在返回前要使用pop恢复现场,代码如下:

//将0x7c00处的boot程序copy到0x90000处
_load_boot:
        //保存现场
        pushw       %ax
        pushw       %bx
        pushw       %cx
        pushw       %dx
        pushw       %es
        //将es和di设置为0x90000
        xorw        %ax, %ax
        movw        $0x9000, %ax
        movw        %ax, %es
        xorw        %ax, %ax
        movw        %ax, %di
        //将ds和si设置为0x7c00
        xorw        %ax, %ax
        movw        $0x7c0, %ax
        movw        %ax, %ds
        xorw        %ax, %ax
        movw        %ax, %si
        //将cx设置成启动程序大小
        movw        $0x2000, %cx
        cld
        //循环拷贝启动程序到0x90000
        rep         movsb %ds:(%si), %es:(%di)
        //恢复现场
        popw        %es
        popw        %dx
        popw        %cx
        popw        %bx
        popw        %ax
        retl
_load_boot_end:     nop

        接下来,为了将内核程序由0x9c00处拷贝到0x0处,boot程序需要进行一次跳转,跳转到段地址为0x9000处,也就是跳转至刚刚被复制到0x90000的boot程序里执行:

        //将0x7c00处的boot程序copy到0x90000处
        calll        _load_boot
        //跳转到0x90000处来执行程序
        ljmp        $0x9000, $_copy_kernel

        下面的_copy_kernel实际上就是boot程序的一部分。这里要说明的是,它已经被拷贝到了0x90000处,再通过ljmp之后cs变为0x9000,ip变为$_copy_kernel。也就说明CPU已经转入0x90000处的boot程序继续执行:

//已经被拷贝到了0x90000处的过程
_copy_kernel:
        //将es和di设置为0x0
        xorw        %ax, %ax
        movw        %ax, %es
        xorw        %ax, %ax
        movw        %ax, %di
        //将ds和si设置为0x9c00
        xorw        %ax, %ax
        movw        $0x9c0, %ax
        movw        %ax, %ds
        xorw        %ax, %ax
        movw        %ax, %si
        //将cx设置成kernel程序大小
        movw        $0x200, %cx
        cld
        //循环拷贝kernel程序到0x0处
        rep         movsb %ds:(%si), %es:(%di)
//永不停歇的循环
_loop:
        jmp                _loop

        通过软驱引导系统是很早以前启动计算机的方式。软驱容量小,速度慢。现在,软驱已经被主流设备淘汰。现在引导计算机的设备除了使用硬盘之外,通常是采用光驱或usb大容量存储设备。我们后续的教程都会采用光驱的方式来引导我们的计算机。采用光驱引导还有一个明显的好处,就是BISO启动之后直接将光盘中多个扇区的引导程序载入内存(在前面章节中讲到的在光盘制作时可以设定BISO启动之后载入光盘的扇区数)。也就是说我们并无需使用BISO的0x13号中断来载入剩余扇区,而是直接可以使用内存中已经被载入的boot引导程序和kernel内核程序。本节的源代码还是使用了软驱引导方式。后续章节则会一直使用光驱引导方式,而不会再编写软驱引导的相关代码和例子。另外,Makefile文件也做了相应的修改,将编译的各项命令和参数定义为相关变量,如下:

#工程名称
PROJECT = lidqos
BUILD_PATH = build
MKDIR = mkdir
BOOT = boot
KERNEL = kernel
MKIMG = mkimg
PSHELL = shell
#命令
#编译c程序
CC = gcc
#连接oject文件到elf可执行文件
LD = ld
#将elf可执行文件转为纯机器码的bin文件
OC = objcopy
#将elf可执行文件反汇编到S文件
OD = objdump
#制作iso软盘或光盘文件
DD = dd
#gcc参数,编译32位代码
GC_PARAMS = -fno-builtin -Iinclude -c -m32 -std=c99
GC_B_PARAMS = $(GC_PARAMS)
GC_K_PARAMS = $(GC_PARAMS)
GC_S_PARAMS = $(GC_PARAMS)
#ld连接参数,
LD_BOOT_PARAMS = -m elf_i386 -Ttext 0x0
#ld连接内核参数
LD_KERNEL_PARAMS = -m elf_i386 -Ttext 0x0
#ld连接shell参数
LD_PSHELL_PARAMS = -m elf_i386 -Ttext 0x0
#转bin文件参数
OC_PARAMS = -O binary
#转16位bin程序参数
OD_PARAMS_16 = -S -D -m i8086
#转16位at&t汇编
OD_PARAMS_16_ATT = $(OD_PARAMS_16) -M att
#转16位intel汇编
OD_PARAMS_16_INTEL = $(OD_PARAMS_16) -M intel
#转32位bin程序参数
OD_PARAMS_32 = -S -D -m i386
#转32位at&t汇编
OD_PARAMS_32_ATT = $(OD_PARAMS_32) -M att
#转32位intel汇编
OD_PARAMS_32_INTEL = $(OD_PARAMS_32) -M intel
#dd参数一个扇区大小为512字节
DD_PARAMS = bs=512

        将工程的boot程序、kernel内核程序和shell外壳程序分别编译,链接,反汇编并烧录在软盘文件中:

###################################################################################
#                创建文件夹        软盘img文件        启动程序                系统内核
all:                $(MKDIR)        $(MKIMG)        $(BOOT)                $(KERNEL)                        
###################################################################################

#创建文件夹
$(MKDIR):
        … …
###################################################################################

#软盘img文件
$(MKIMG):
        … …
###################################################################################

#启动程序
$(BOOT):
        … …
###################################################################################

#系统内核
$(KERNEL):
        … …
###################################################################################        

#外壳程序
$(PSHELL):
        … …
###################################################################################

        源代码的下载地址为:

https       https://github.com/magicworldos/lidqos.git
git         git@github.com:magicworldos/lidqos.git
subverion   https://github.com/magicworldos/lidqos
branch      v0.4

    返回首页    返回顶部
#1楼  我是重新学的雷锋V5  于 2018年03月29日17:41:46 发表
 
文中“boot程序占用10个扇区(大小为0x2000个字节)。。。”,我想博主写的时候也乱套了吧,应该是boot程序占用0x10个扇区吧,也就是十六个,外加kernel部分用一个扇区,所以正好是17个扇区,也就没有后边的boot还剩7个扇区了吧,我的理解是boot模块占用16(0x10)扇区,kernel需要一个扇区,因为已经有一个扇区加载进入内存,所以说以程序中读取另外的扇区数应该是16而不是17吧,但是多加载进来也无伤大雅,不知道我这么理解对不对?
#2楼  李德强  于 2018年03月30日07:06:16 发表
 
感谢您的支持和细心!这里的意思是前17个扇区存放了boot(1-10扇区)和kernel(11-17扇区),后续的扇区作为文件系统存放系统文件。

如果没记错的话应该是这样,这里的程序有几年没碰了,有点忘记了,如果有错误欢迎批评指正!
#3楼  匿名  于 2019年10月17日22:03:50 发表
 
bios程序在高端地址吧,在0xA0000~0xFFFFF之间,图上画的应该有点问题。
#4楼  大山  于 2021年01月22日22:03:06 发表
 
一. BIOS帮我们加载1扇区, 1扇区里面的代码加载2到17扇区, 其中2到16扇区是boot代码, 加上1扇区, 所以1到16是boot代码,然后拷贝至0x90000. 17扇区是kernel代码, 拷贝至0x00000

二. 文中“boot程序占用10个扇区(大小为0x2000个字节)。。。”应该是占用0x10个扇区,就是16扇区.

17个扇区存放了boot(1-16扇区)和kernel(17扇区)
  看不清?点击刷新

 

  Copyright © 2015-2023 问渠网 辽ICP备15013245号