2 Star 5 Fork 4

稀风 / KOS

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
内核雏形.md 15.73 KB
一键复制 编辑 原始数据 按行查看 历史
稀风 提交于 2023-04-16 21:30 . 优化代码

引言

  • 前面讲了那么多章节,都只能说是做铺垫,从本章开始,我们正式开始实现操作系统

整体设计

  • 在前面的章节当中,我们做过很多实验,这些实验全部都放在了 loader.asm 文件中,这么做显然是不合适的。loader 的功能就应该只有加载内核并跳转到内核执行

  • 操作系统整体规划设计

    操作系统整体设计

  • 新建 “KOS” 文件夹,从今以后实现的代码都放在这个文件夹中

  • 新建 “bootloader” 文件夹,将我们前面实现过的代码 boot.asmloader.asm 放入其中, make 一下再运行,一切 OK。

用 python 替代 Makefile

  • 从今天起,将抛弃 Makefile 文件,改用 python 实现编译脚本,python 用起来简单快速
  • 先提供个基础版本:Build.py ,仅单纯的将 Makefile 翻译成 python 脚本
  • 其中关键点:os.system 函数可以将字符串转化成命令执行,比如
    import os
    os.system("ls -a") # 显示当前目录下的所有文件
  • 运行 python 文件
    python Build.py
  • 得到最终的 a.img ,bochs 运行一下

优化遗留问题

  • boot.asm 中读取硬盘中数据到内存中时,读取的扇区数是我们写死的 20 个扇区
    ; 将硬盘扇区 2 中的数据读入到内存  0x900 处
    mov eax, 0x02
    mov bx, 0x900
    mov cx, 20
    call rd_disk_to_mem
  • 现在我们来解决这个遗留问题
  • 回想一下,硬盘的第二个扇区我们是不是并没有使用,现在正好把它用上,我们把 loader.bin 文件所占的山区数算出来,填充到第二个扇区的前两个字节中,然后在 boot.asm 中读取第二个扇区,以获得 loader.bin 的扇区数。这样子就能实现自适应 loader.bin 大小了
  • 优化后的 python 脚本:Build.py
  • boot.asm 中改动处:
    ; 将硬盘扇区 1 中的数据读入到内存  0x700 处
    mov eax, 0x01
    mov bx, 0x700
    mov cx, 1
    call rd_disk_to_mem
    
    ; 将硬盘扇区 2 中的数据读入到内存  0x900 处
    mov eax, 0x02
    mov bx, 0x900
    mov cx, [0x700]
    call rd_disk_to_mem
  • 重新编译
    python Build.py
  • 接下来需要反汇编调试了,这次是对 boot.asm 进行反汇编
    ndisasm -o 0x7c00 boot.bin  > boot.txt
  • 打开 boot.txt 文件,在 mov cx, [0x700] 执行完成后地址(0x7c4a)处打断点
    <bochs:1> b 0x7c4a
    <bochs:2> c
    ...
    <bochs:3> xp 0x700
    [bochs]:
    0x00000700 <bogus+       0>:    0x00000009
    <bochs:4> reg
    eax: 0x00000002 2
    ecx: 0x00000009 9
    edx: 0x000001f0 496
    ebx: 0x00000900 2304
    esp: 0x00007c00 31744
    ebp: 0x00000000 0
    esi: 0x00000001 1
    edi: 0x00000001 1
    eip: 0x00007c4a
    eflags 0x00000016: id vip vif ac vm rf nt IOPL=0 of df if tf sf zf AF PF cf
  • 通过 xp 查看内存和 reg 查看 cx 寄存器的值都为 9,即 loader.bin 所占的扇区数。说明我们优化成功了

bootloader 代码优化

  • bootloader 分为 boot 和 loader 两个部分,我们把它们需要重复使用相同函数拆分出来,放到 common.asm
  • 相同函数功能有 print 和 rd_disk_to_mem。两者都需要打印函数,boot 需要加载 loader,而 loader 需要加载内核。所以 rd_disk_to_mem 函数也是需要重复的
  • 按照 C 语言的思想,我们把属性定义等单独放到头文件中 inc.asm
  • 使用时需要包含文件
    %include "./bootloader/inc.asm"
    %include "./bootloader/common.asm"
  • 优化后的 boot.asm
  • loader.asm 优化,删除不需要的东西,仅保留开启保护模式并跳转到 kernel 的功能
  • 开启保护模式后再由保护模式跳转到 kernel,利用平坦模式跳转到实际指定的物理地址处
  • 由于以后我们以后在低特权级情况下也可能进行 I/O 操作,所以要改一下 IOPL=3
    pushf
    pop eax 
    or eax, 0x3000 ; bit12-bit13:11b
    push eax
    popf
  • 最终改动后的 loader.asm
  • 由于还没实现 kernel,所以暂时先不跳转到 kernel
    ; jmp dword FLAT_MODE_SELECTOR:KERNEL_START_ADDR

kernel

  • bootloader 的代码优化工作暂时就到这里吧,接下来我们来实现 kernel 部分,等 kernel 实现后再实现 loader 加载并跳转到 kernel

  • 创建 "init" 文件夹并在其中创建 "kentry.asm" 和 "main.c" 两个文件夹

  • 有了前面讲的 C 与汇编混合编程知识,这里就不做详细介绍了

  • kentry.asm 内容如下:

    [section .text]
    global _start
    extern main
    
    _start:
        mov ax, 8   ; 没啥实际意义,仅用于断点调试,便于查看
        mov ax, 9   ; 没啥实际意义,仅用于断点调试,便于查看
        call main
        jmp $
  • main.c 内容如下:

    void main(void)
    {
        while(1);
    }
  • 编译 kentry.asm

    nasm -f elf32 kentry.asm -o kentry.o
  • 编译 main.c【注意必须加 '-nostdinc', 不需要使用系统自带的库及头文件】

    gcc -m32 -nostdinc -c main.c -o main.o
  • 链接,-Ttext是链接时将初始地址重定向为 0xB000【注意链接时 kentry.o 必须在最前面】

    ld -Ttext 0xB000 -s -m elf_i386 -o kernel.out  kentry.o main.o
  • 我们最终链接生成可执行程序 kernel.out,但是这个程序是 linux 使用的 elf 格式的,不能直接加载进内存执行,CPU 只认识代码和数据,无法正确执行 elf 可执行程序。于是我们需要提取 elf 中的代码段和数据段(删除 elf 文件格式信息)

    objcopy -O binary kernel.out kernel.bin
  • 接下来使用 dd 命令将 kernel.bin 写入 a.img 中

  • 为了自动化,我们把 kernel.bin 所占的扇区数存入 a.img 扇区 1 的第 3-4 个字节中(共 2 个字节)。kernel.bin 数据接着写到 loader.bin 数据的下一个扇区即可

  • 原先创建虚拟硬盘固定大小 60M,现在也改成自动计算

  • 改动后的 Build.py

  • 有了 kernel.bin 程序, loader 就可以加载并跳转到 kernel 了

    ; 将硬盘扇区 1 中的数据读入到内存  0x700 处
    mov eax, 0x01
    mov bx, 0x700
    mov cx, 1
    call rd_disk_to_mem
    
    ; 将硬盘扇中 kernel 数据读入到内存  0xB000 处
    mov eax, 0
    mov ax, [0x700]    ; 0x700 处存的是 loader.bin 所占的扇区数,再 +2 找到 kernel 起始扇区 
    add ax, 2
    mov bx, KERNEL_START_ADDR
    mov cx, [0x702]
    call rd_disk_to_mem
  • 编译运行一下,发现程序崩溃了,查找了半天,原来是平坦模式段描述符要具有可执行属性

  • 改完以后以为可以了,结果还是崩溃,原来是读扇区 1 到内存 0x700 与栈使用的内存冲突了,我们也可以使用 boot 时使用的栈空间

    ; mov sp, 0x900	; 设置栈
    mov sp, 0x7c00	; 设置栈
  • loader 最终程序:loader.asm

  • 断点调试,验证一下

    <bochs:1> b 0xb000
    <bochs:2> c
    ...
    (0) [0x0000b000] 0010:0000b000 (unk. ctxt): mov ax, 0x0008            ; 66b80800
    <bochs:3> s
    Next at t=16770437
    (0) [0x0000b004] 0010:0000b004 (unk. ctxt): mov ax, 0x0009            ; 66b80900
  • 在 kernel 入口地址 0xb000 处打断点,单步执行,从调试信息看其执行的汇编指令就是我们在 kentry.asm 中实现的汇编代码

  • 辛苦了那么久,程序终于走到了 main

  • 最后让我们欣赏一下当前程序运行效果

    内核雏形

工程管理

  • 目前工程中一个 .h 文件都没有,我们创建一个名为 “include” 的文件夹,头文件都放在该文件夹中,再创建一个头文件 “common.h” 放到 “include” 目录下,这样,整个工程所需的基本文件类型就都有了, “common.h” 内容如下
    #ifndef __COMMON_H_
    #define __COMMON_H_
    
    typedef unsigned char      U08;
    typedef unsigned short     U16;
    typedef unsigned int       U32;
    
    typedef char               S08;
    typedef short              S16;
    typedef int                S32;
    
    #endif
  • 在 main.c 中包含 common.h 文件
    #include <common.h>
    
    S32 main(void)
    {
        while(1);
        return 0;
    }
  • 因为新增了头文件 common.h,所以编译肯定是报错的。解决办法是 gcc 编译时加 "-I" 指定头文件路径,如果有多个头文件路径,可以多次使用 "-I",示例如下:
    gcc -Iinclude_dir1 -Iinclude_dir2 -c main.c -o main.o
  • 目前我们的编译脚本 Build.py 直接包含了所有的工程文件,当文件总数较少的时候,这种方式比较简单快捷,但当后期工程文件逐渐增加时,且我们想对其中某一部分功能实现选择性编译时,这种方式就不太科学了
  • 现在提倡模块化设计思想:我们设计一种配置文件,每个目录下都包含一个这样的配置文件,该配置文件只负责三件事,一是包含当前目录下的文件夹,二是包含当前目录下的源文件,三是包含当前目录下源文件所包含的头文件路径
  • 我们给这个配置文件取名 “BUILD.json”,文件内容使用 json 格式,因为 json 是一种通用格式,所以可以使用别人已实现的库对 json 文件进行解析。当然,你也可以自定义格式,只不过文件解析就必须自己实现了
  • 整个工程组织结构如下:
    KOS
     |--- BUILD.json
     |--- include
     |      |--- common.h
     |--- bootloader
     |      |--- BUILD.json
     |      |--- boot.asm
     |      |--- common.asm
     |      |--- inc.asm
     |      |--- loader.asm
     |--- init
     |      |--- BUILD.json
     |      |--- kentry.asm
     |      |--- main.c
  • 最外层 BUILD.json 内容如下:
    {
        "dir" : [
            "bootloader", 
            "init"
            ],
    
        "src" : [
            ],
        
        "inc" : [
            ]
    }
  • "dir" 中包含的是当前目录下的文件夹
  • bootloader 目录下的 BUILD.json 内容如下:
    {
        "dir" : [
            ],
    
        "src" : [
            "boot.asm",
            "loader.asm"
            ],
    
        "inc" : [
            ]
    }
  • "src" 中包含的是当前目录下的源文件
  • init 目录下的 BUILD.json 内容如下:
    {
        "dir" : [
            ],
    
        "src" : [
            "kentry.asm",
            "main.c"
            ],
        
        "inc" : [
            "include"
            ]
    }
  • "inc" 中包含的是当前目录下源文件所包含的头文件路径,注意这个路径是相对于编译脚本 “Build.py” 的路径
  • 实现解析 BUILD.json 文件的函数:Parse_BUILD_CFG,该函数通过递归遍历路径 path 下的所有的 BUILD.json 文件,并将遍历后的结果以 '源文件文件名':['源文件所在路径', ['源文件所包含的头文件路径1', '源文件所包含的头文件路径1']] 这种形式的数据添加到字典 project 中,函数内容具体如下:
    def Parse_BUILD_CFG(path):
        JsonPathName = os.path.join(path, BUILD_CFG)
        # 以只读方式打开文件 JsonPathName
        with open(JsonPathName, 'r') as f:
            # 读取文件内容到 json_text  
            json_text = f.read()
            # 将 json_text 内容转为 python 字典 json_dict
            json_dict = json.loads(json_text)
            # 遍历字典 json_dict
            for key in json_dict:
                if key == 'src':    # 如果是源文件,则将 '源文件文件名':['源文件所在路径', ['源文件所包含的头文件路径1', '源文件所包含的头文件路径1']]
                                    # 这种形式的数据添加到字典 project 中
                    if json_dict[key]:
                        for item in json_dict[key]:
                            project[item] = [path, json_dict['inc']]
                            
                elif key == 'dir':  # 如果是文件夹,则进入该文件夹内,递归调用 Parse_BUILD_CFG 
                    if json_dict[key]:
                        for item in json_dict[key]:
                            new_path = os.path.join(path, item)
                            Parse_BUILD_CFG(new_path)
                elif key == 'inc': # 如果是头文件路径,则不处理
                    pass
                
                else:
                    print("Invalid key")
    
        return project
  • 定义一个全局 project 字典,调用 Parse_BUILD_CFG 函数后,我们把 project 中的内容打印出来
    root_path = ''
    project = Parse_BUILD_CFG(root_path)
    # 打印 project
    for item in project:
        str_print = item + ': ' + str(project[item])
        print(str_print)
  • 打印内容如下(从左到右依次是源文件名,源文件所在路径,源文件包含的头文件所在路径,其中路径都是为 Build.py 所在路径的相对路径):
    boot.asm: ['bootloader', []]
    loader.asm: ['bootloader', []]
    kentry.asm: ['init', ['include']]
    main.c: ['init', ['include']]
  • 好了,我们想要的整个工程信息都在 project 字典中了,接下来就是读取 project 字典,获取想要的信息,编译,链接等
  • Build.py 修改过程就不过多介绍了,我也是利用 print 打印一点一点的调试出来的,整个过程只是繁琐一点而已。改动后的代码如下 Build.py
  • 由于 Build.py 做了一定的调整,boot.asm 和 loader.asm 中包含文件处也要改动一下
    ; %include "./bootloader/inc.asm"
    ; %include "./bootloader/common.asm"
    %include "inc.asm"
    %include "common.asm"
  • 至此,整个工程的基本雏形已经展现出来了

补充

  • 后续的开发过程中遇到一个 BUG,我觉得放到这里提前说一下比较合适,什么 BUG 呢?
  • 我们找到 rd_disk_to_mem 函数,看一下下面的语句,其中 bx 为函数参数之一,表示将硬盘中的数据读取到 bx 内存地址处,mov [bx], ax 这条指令中我们能操作的内存访问是 [0, 0xFFFF],一旦内存操作超过这个范围,那就有问题了
    ...
    .go_on_read:
        in ax, dx
        mov [bx], ax
        add bx, 2
    loop .go_on_read
    ...
  • 由于 rd_disk_to_mem 是在 实模式下调用,bx 为 16 位,其最大值为 0xFFFF,我们把 kernel 程序读取到 0xB000 处,从代码上看即把 bx 赋值 0xB000,这时候就产生了一个问题,那就只能将 kernel 程序读取到 [0xB000, 0xFFFF] 这个内存范围,大概 19K 多一点,一旦 kernel 程序过大,那么肯定无法把 kernel 程序全部正确的读到内存中
    mov bx, KERNEL_START_ADDR ; 0xB000
    mov cx, [0x702]
    call rd_disk_to_mem
  • 临时解决方案如下,改变段基址 ds 的值为 0xb00, bx 为 0,mov [bx], ax 这条指令本质其实是 mov [ds:bx], ax,ds:bx = (ds<<4) + bx,这么做仅仅只是把 19K 的大小稍微扩大到 64K,不过对于目前的 kernel 程序代码量来说已经够用了,当然,也可以每当 bx 累加到超过 0xFFFF 的时候,使 ds += 0x1000,然后把 bx 清 0,这样子最多的内存操作空间就变成了 0xFFFF:0xFFFF = 1M,不过比较麻烦,64K 将就用吧
    ; 将硬盘扇中 kernel 数据读入到内存  0xB000 处
    mov eax, 0
    mov ax, [0x700]    ; 0x700 处存的是 loader.bin 所占的扇区数,再 +2 找打 kernel 起始扇区 
    add ax, 2
    mov cx, [0x702]
    mov dx, 0xb00
    mov ds, dx
    mov bx, 0x0
    call rd_disk_to_mem
    ; 需恢复 ds=0, 下面的程序需要 ds 为 0
    mov dx, 0x0
    mov ds, dx
  • 当然了,不管是简单的 64K 或者复杂改动增加到 1M(更精确的说是 1M 减去 0xB000),都不够大,不能很好的解决这个问题,真正解决问题要等到我们学习并实现了硬盘驱动代码,那时就是 4G 内存空间任意使用了,这里先有个印象如果以后内核莫名其妙的出问题了,可以查看一下是否是内核程序过大了
1
https://gitee.com/thin-wind/KOS.git
git@gitee.com:thin-wind/KOS.git
thin-wind
KOS
KOS
main

搜索帮助

53164aa7 5694891 3bd8fe86 5694891