《ORANGE’S:一个操作系统的实现》读书笔记(十一)内核雏形(五)

        上一篇文章完成了将控制权交给内核,使我们的操作系统内核开始运行了。这篇文章开始记录内核扩充,我们目前的 esp、GDT 等工作还是在Loader中完成的,为了方便控制,我们要把它们放进内核中才好,现在就来做这项工作。

切换堆栈和 GDT

        有一点需要先说明,我们现在可以使用C语言了,只要可以用C,我们就尽量避免使用汇编,毕竟C语言比汇编用起来要方便很多。

        首先,我们先来切换堆栈和GDT,内核 kernel.asm 代码进行重新编写,具体代码如下所示。

; 编译链接方法
; nasm -f elf kernel.asm -o kernel.o
; nasm -f elf string.asm -o string.o
; gcc -m32 -c start.c -o start.o
; ld -m elf_i386 -Ttext 0x30400 -s kernel.o string.o start.o -o kernel.bin

SELECTOR_KERNEL_CS  equ 8

; 导入函数
extern  cstart
; 导入全局变量
extern  gdt_ptr

[SECTION .bss]
StackSpace      resb    2 * 1024
StackTop:       ; 栈顶

[section .text] ; 代码在此

global _start   ; 导出 _start

_start:
    ; 把 esp 从 LOADER 挪到 KERNEL
    mov esp, StackTop   ; 堆栈在 bss 段中

    sgdt    [gdt_ptr]   ; cstart() 中将会用到 gdt_ptr
    call    cstart      ; 在此函数中改变了gdt_ptr,让它指向新的GDT
    lgdt    [gdt_ptr]   ; 使用新的GDT

    jmp SELECTOR_KERNEL_CS:csinit
csinit:     ; 这个跳转指令强制使用刚刚初始化的结构
    push    0
    popfd   ; Pop top of stack into EFLAGS

    hlt

        在这段代码中,用4条语句就完成了切换堆栈和更换GDT的任务。其中,StockTop定义在 .bss 段中,堆栈大小为2KB。操作GDT时用到的gdt_ptr是一个全局变量,cstart是一个全局函数,它们定义在start.c中。start.c 代码如下所示。 

#include "type.h"
#include "const.h"
#include "protect.h"

PUBLIC void* memcpy(void* pDst, void* pSrc, int iSize);

PUBLIC u8 gdt_ptr[6];   /* 0~15:Limit  16~47:Base */
PUBLIC DESCRIPTOR gdt[GDT_SIZE];

PUBLIC void cstart()
{
    /* 将 LOADER 中的 GDT 复制到新的 GDT 中 */
    memcpy(&gdt,                               /* New GDT */
           (void*)(*((u32*)(&gdt_ptr[2]))),    /* Base  of Old GDT */
           *((u16*)(&gdt_ptr[0])) + 1          /* Limit of Old GDT */
        );
    /* gdt_ptr[6] 共 6 个字节:0~15:Limit  16~47:Base。用作 sgdt/lgdt 的参数。*/
    u16* p_gdt_limit = (u16*)(&gdt_ptr[0]);
    u32* p_gdt_base  = (u32*)(&gdt_ptr[2]);
    *p_gdt_limit = GDT_SIZE * sizeof(DESCRIPTOR) - 1;
    *p_gdt_base  = (u32)&gdt;
}

        函数cstart()首先把位于 Loader 中的原GDT复制到新的GDT,然后把gdt_ptr中的内容换成新的GDT的基地址和界限。复制GDT使用的是函数memcpy,这个函数就是我们在 Loader 中使用的MemCpy,只不过在这里将函数名做了更改,这次我们把它的函数体放进string.asm中,这样也便于以后进行工具函数的扩展。string.asm代码如下所示。 

[SECTION .text]
; 导出函数
global  memcpy

; void* memcpy(void* es:pDest, void* ds:pSrc, int iSize);
memcpy:
    push    ebp
    mov ebp, esp

    push    esi
    push    edi
    push    ecx

    mov edi, [ebp + 8]      ; Destination
    mov esi, [ebp + 12]     ; Source
    mov ecx, [ebp + 16]     ; Counter
.1:
    cmp ecx, 0              ; 判断计数器
    jz  .2                  ; 计数器为零时跳出

    mov al, [ds:esi]        ; ┓
    inc esi                 ; ┃
                            ; ┣ 逐字节移动
    mov byte [es:edi], al   ; ┃
    inc edi                 ; ┛

    dec ecx                 ; 计数器减一
    jmp .1                  ; 循环
.2:
    mov eax, [ebp + 8]      ; 返回值

    pop ecx
    pop edi
    pop esi
    mov esp, ebp
    pop ebp

    ret                     ; 函数结束,返回
; memcpy 结束

        函数cstart()中除了用到memcpy定义在其它文件之外,还用到了一些新定义的类型、结构体和宏,这些内容分别定义在了type.h、const.h以及protect.h。这三个头文件代码如下所示。

        代码 const.h。

#ifndef _ORANGES_CONST_H_
#define _ORANGES_CONST_H_

/* 函数类型 */
#define PUBLIC          /* PUBLIC is the opposite of PRIVATE */
#define PRIVATE static  /* PRIVATE x limits the scope of x */

/* GDT 和 IDT 中描述符的个数 */
#define GDT_SIZE    128

#endif

        代码 type.h。 

#ifndef _ORANGES_TYPE_H_
#define _ORANGES_TYPE_H_

typedef unsigned int u32;
typedef unsigned short u16;
typedef unsigned char u8;

#endif

        u8、u16、u32分别代表8位、16位、32位的数据类型。定义它们可以增加我们代码的可读性。

        代码 protect.h。

#ifndef _ORANGES_PROTECT_H_
#define _ORANGES_PROTECT_H_

/* 存储段描述符/系统段描述符 */
typedef struct s_descriptor     /* 共 8 个字节 */
{
    u16 limit_low;              /* Limit */
    u16 base_low;               /* Base */
    u8  base_mid;               /* Base */
    u8  attr1;                  /* P(1) DPL(2) DT(1) TYPE(4) */
    u8  limit_high_attr2;       /* G(1) D(1) 0(1) AVL(1) LimitHigh(4) */
    u8  base_high;              /* Base */
}DESCRIPTOR;

#endif

        记录到这里,可能大家对代码已经有了一个大概的了解,毕竟C语言多少是要好理解一点的。因为书上这里也没有写得特别清楚,说的是参考Minix来做,代码有时仅仅是奉行了拿来主义,那作为读者的我来说,更是拿来主义了,只要能写功能就可以了,其它固定的代码模式怎么写就直接拿来用了,如果以后对知识理解的更加深入了,可能就理解了。

        说明一点的是,在kernel.asm内核代码中,将显示字符“K”的代码去掉了。同时,loader.asm中显示“P”字符的代码也删除了。毕竟我们当时显示字符的目的是看运行是否正确,是否运行到了那里,是作为调试信息使用的。现在我们的代码运行没有问题,它们也就没有存在的必要了。

        好了,我们现在开始进行编译链接:

# 因为删除了显示字符“P”的代码,所以Loader需要重新编译
nasm loader.asm -o loader.bin
# 内核编译链接
nasm -f elf kernel.asm -o kernel.o
nasm -f elf string.asm -o string.o
gcc -m32 -c start.c -o start.o
ld -m elf_i386 -Ttext 0x30400 -s kernel.o string.o start.o -o kernel.bin

        编译链接完成后,将loader.bin和kernel.bin拷贝到软件文件fd.img中,运行,查看运行效果。 

        运行成功,不过屏幕上没有提示信息,看起来有些别扭,总感觉好像少了点什么,是不是程序出问题了?为了证明程序没有问题,我们现在给它添加一个字符串信息的展示,为了以后使用方便,我们将展示字符串的功能编写成一个函数,这样以后需要的时候只用调用就可以了。我们把显示字符串的函数放在一个新的文件kliba.asm中,代码如下所示。 

[SECTION .data]
disp_pos dd 0

[SECTION .text]

; 导出函数
global  disp_str

; void disp_str(char * info);
disp_str:
    push ebp
    mov ebp, esp

    mov esi, [ebp + 8]  ; pszInfo
    mov edi, [disp_pos]
    mov ah, 0Fh
.1:
    lodsb
    test al, al
    jz .2
    cmp al, 0Ah ; 换行
    jnz .3
    push eax
    push ebx
    mov eax, edi
    mov bl, 160
    div bl
    and eax, 0FFh
    inc eax
    mov bl, 160
    mul bl
    mov edi, eax
    pop ebx
    pop eax
    jmp .1
.3:
    mov [gs:edi], ax
    add edi, 2
    jmp .1
.2:
    mov [disp_pos], edi

    pop ebp
    ret

        使用方法和mempy是一样的,需要在C语言代码中声明一下才可以使用。现在我们就可以在cstart()函数中添加打印字符串的代码了。需要注意的是,由于变量disp_pos被初始化为0,所以如果直接打印字符的话,字符会出现在屏幕的左上角,会覆盖掉现有的显示内容,于是在输出时添加了许多回车(\n),以便让字符串越过已经打印的区域。 

......
PUBLIC void disp_str(char * pszInfo);
......

PUBLIC void cstart()
{
    disp_str("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n-----\"cstart\" begins-----\n");
    ......
}

        现在再来进行编译,这次只用编译kliba.asm和start.c就可以了,不过链接时需要添加kliba.asm编译的文件。 

nasm -f elf kliba.asm -o kliba.o
gcc -m32 -c start.c -o start.o
ld -m elf_i386 -Ttext 0x30400 -s kernel.o string.o kliba.o start.o -o kernel.bin

        编译链接完成后,将kernel.bin拷贝到软件文件fd.img中,运行,查看运行效果。

        可以看到,我们打印的字符串出现在了屏幕上。 

整理我们的文件夹

        做到这里,相信你和我一样,感觉还不错,文件越来越多,系统也开始像那么回事了。但是出现了一个问题,所有的文件都放在了一个文件夹下,包括我们编译生成的文件和虚拟机用到的文件,看起来很乱,找到某个文件都需要花点时间。看样子,我们是时候将目录整理一下了。

        目录整理就比较简单了,可以按照个人的习惯进行整理。在这里,我使用书上给出的目录结构。

  • boot.asm和loader.asm放在单独的目录/boot中,包括它们所需要的头文件也放在该目录下;
  • klib.asm和string.asm放在/lib中,以库的形象出现;
  • type.h、const.h和protect.h放在/include目录中;
  • kernel.asm和start.c放在/kernel里面。

        这样一来,结构就比较清晰了,我们来看一下现在的目录结构。

Makefile

        随着源代码的增多,编译链接它们的命令也在逐渐增多,就像我们刚刚的编译链接命令就有5行。如果每一次都是这样编译链接的话,那真是太麻烦了,而且,现在文件已经分开放置在不同的文件夹下,要编译它们无疑变得更加困难。所以,我们现在准备使用Makefile。

        Makefile的内容是很多的,在Linux环境下使用GNU make来构建和管理自己的工程,是很常见的。所以在这里并不记录关于Makefile的具体用法,只记录一些常用的内容。首先先来看一个简单的Makefile文件,我们把它放在目录 /boot 下,用来编译boot.bin和loader.bin。Makefile文件内容如下所示。

# Makefile for boot

# Programs, flags, etc.
ASM = nasm
ASMFLAGS = -I include/

# This Program
TARGET = boot.bin loader.bin

# All Phony Targets
.PHONY : everything clean all

# Default starting position
everything : $(TARGET)

clean:
    rm -f $(TARGET)

all: clean everything

boot.bin : boot.asm include/load.inc include/fat12hdr.inc
        $(ASM) $(ASMFLAGS) -o $@ $<

loader.bin : loader.asm include/load.inc include/fat12hdr.inc include/pm.inc include/lib.inc
        $(ASM) $(ASMFLAGS) -o $@ $<

        在Makefile中,以字符“#”开头的行是注释,“=”用来定义变量。这里,ASM和ASMFLAGS就是两个变量,要注意的是,使用它们的时候要用$(ASM)和$(ASMFLAGS)。其实,看明白了这两点,Makefile整体上也就明白了一半。.PHONY这个关键字我们暂时不管,先来看一下Makefile的最重要的语法: 

target : prerequisites
         command

        上面这样的形式代表两层意思: 

  1. 要想得到 target,需要执行命令 command。
  2. target 依赖 prerequisites,当 prerequisites 中至少有一个文件比 target 文件新时,command才被执行。

        比如这个Makefile文件的最后两行,含义就是:

  1. 要想得到loader.bin,需要执行“$(ASM) $(ASMFLAGS) -o $@ $<”。
  2. loader.bin依赖一下文件:
  • loader.asm
  • include/load.inc
  • include/pm.inc
  • include/fat12hdr.inc
  • include/lib.inc

        当它们中至少有一个比loader.bin新时,command才被执行。

        接下来我们来看“$(ASM) $(ASMFLAGS) -o $@ $<”的含义:

  • $@代表 target;
  • $<代表 prerequisites 的第一个名字。

        联系前面我们说过的$(ASM)和$(ASMFLAGS),这个命令行等价于:

nasm -o loader.bin loader.asm

        在Makefile中我们注意到,除了boot.bin和loader.bin两个文件后面有冒号,everything、clean、all后面也有冒号,可是它们3个并不是文件,而是动作名称。如果运行“make clean”,将会执行“rm -f $(TARGET)”,即“rm -f boot.bin loader.bin”。

        all后面跟着的是clean和everything,这表明如果执行“make all”,clean和everything所表示的动作将分别执行。下图就是make all的执行结果。

刚才我们忽略的关键字 .PHONY,其实是表示它后面的名字并不是文件,而是一种行为的标号。

我们刚才已经运行了make all了,其实直接运行make也是可以的,这时make程序会从第一个名字所代表的动作开始执行。在本例中,第一个标号是everything,所以make和make everything是一样的,下面的过程清楚的表示了这一点:

        由于make会自动比较目标和源文件的新旧程度,所以如果运行一个make之后立即运行另一个的话,make程序不会做任何事情,因为所有文件都是新的,不需要生成什么。

        至此,这个Makefile文件就已经没有什么地方需要进行说明的了。其实,make程序的原则就是由果寻因,先看要生成什么,再找生成它需要的条件。

        好了,我们现在只需要对这个Makefile文件进行改造和扩充,就可以用它来编译链接整个操作系统的工程了。我们首先把Makefile挪到/boot的父目录中,然后对其进行修改,修改后的内容如下所示。

# Makefile for Orange'S

# Entry point of Orange's
# It must have the same value with 'KernelEntryPointPhyAddr' in load.inc!
ENTRYPOINT = 0x30400

# Offset of entry point in kernel file
# It depends on ENTRYPOINT
ENTRYOFFSET = 0x400

# Programs, flags, etc.
ASM             = nasm
DASM            = ndisasm
CC              = gcc
LD              = ld
ASMBFLAGS       = -I boot/include/
ASMKFLAGS       = -I include/ -f elf
CFLAGS          = -I include/ -m32 -c -fno-builtin
LDFLAGS         = -m elf_i386 -s -Ttext $(ENTRYPOINT)
DASMFLAGS       = -u -o $(ENTRYPOINT) -e $(ENTRYOFFSET)

# This Program
ORANGESBOOT     = boot/boot.bin boot/loader.bin
ORANGESKERNEL   = kernel.bin
OBJS            = kernel/kernel.o kernel/start.o lib/kliba.o lib/string.o
DASMOUTPUT      = kernel.bin.asm

# All Phony Targets
.PHONY : everything final image clean realclean disasm all buildimg

# Default starting position
everything : $(ORANGESBOOT) $(ORANGESKERNEL)
all : realclean everything
final : all clean
image : final buildimg

clean:
    rm -f $(OBJS)

realclean:
    rm -f $(OBJS) $(ORANGESBOOT) $(ORANGESKERNEL)

disasm:
    $(DASM) $(DASMFLAGS) $(ORANGESKERNEL) > $(DASMOUTPUT)

# We assume that "fd.img" exists in current folder
buildimg :
    dd if=boot/boot.bin of=fd.img bs=512 count=1 conv=notrunc
    sudo mount -o loop fd.img /mnt/floppy/
    sudo cp -fv boot/loader.bin /mnt/floppy/
    sudo cp -fv kernel.bin /mnt/floppy/
    sudo umount /mnt/floppy/

boot/boot.bin : boot/boot.asm boot/include/load.inc boot/include/fat12hdr.inc
    $(ASM) $(ASMBFLAGS) -o $@ $<

boot/loader.bin : boot/loader.asm boot/include/load.inc boot/include/fat12hdr.inc boot/include/pm.inc boot/include/lib.inc
    $(ASM) $(ASMBFLAGS) -o $@ $<

$(ORANGESKERNEL) : $(OBJS)
    $(LD) $(LDFLAGS) -o $(ORANGESKERNEL) $(OBJS)

kernel/kernel.o : kernel/kernel.asm
    $(ASM) $(ASMKFLAGS) -o $@ $<

kernel/start.o : kernel/start.c include/type.h include/const.h include/protect.h
    $(CC) $(CFLAGS) -o $@ $<

lib/kliba.o : lib/kliba.asm
    $(ASM) $(ASMKFLAGS) -o $@ $<

lib/string.o : lib/string.asm
    $(ASM) $(ASMKFLAGS) -o $@ $<

        可以看到,因为目录层次的原因,我们把GCC的选项也增加了对头文件目录的指定“-I include”。这个Makefile虽然比原来长出不少,但是并不算难。不过,它的功能却十分强大,通过make disasm我们可以反汇编内核到一个文件。甚至于,通过make buildimg或者make image,我们可以直接把引导扇区、loader.bin和kernel.bin写入虚拟软盘。既然功能这么强大,那我们就来试一下它的效果,输入make image,执行情况如下图所示。 

        这可真是太省事了,只需要一个命令,make程序就按照预先设定的步骤将所有工作搞定。

        接下来,我们修改一下start.c,在cstart()的最后再添加一行打印字符串的程序,再运行make,查看一下运行效果。代码start.c如下所示。

PUBLIC void cstart()
{
    disp_str("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n-----\"cstart\" begins-----\n");
    ......
    disp_str("-----\"cstart\" ends-----\n");
}

        运行成功,我们新添加的字符串输出了。这样表明我们的Makefile运行正常,这真是太好了,今后我们重新编译链接的速度就会大大加快,而且像引导扇区、复制内核文件这样的工作也可以交给make来完成,甚至我们也可以根据自己的需要自由添加功能,这感觉真是不错。 

公众号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值