内核雏形

简介: 内核雏形

引言

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

整体设计

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

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

用 python 替代 Makefile

  • 从今天起,将抛弃 Makefile 文件,改用 python 实现编译脚本,python 用起来简单快速
  • 先提供个基础版本:Build.py ,仅单纯的将 Makefile 翻译成 python 脚本
  • 其中关键点:os.system 函数可以将字符串转化成命令执行,比如

importos

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 中,函数内容具体如下:

defParse_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 内存空间任意使用了,这里先有个印象如果以后内核莫名其妙的出问题了,可以查看一下是否是内核程序过大了
目录
相关文章
|
2月前
|
机器学习/深度学习 人工智能 搜索推荐
未来操作系统发展趋势探究
随着科技的不断进步和应用领域的不断拓展,操作系统作为计算机系统的核心组成部分,也在不断演化与创新。本文将从未来操作系统的发展趋势出发,探讨人工智能、物联网和安全性等方面对操作系统的影响,展望未来操作系统的可能发展方向。
17 2
|
2月前
|
存储 人工智能 安全
打造未来:下一代操作系统的设计理念
随着技术的不断进步,对操作系统的需求也在不断变化。本文将探讨下一代操作系统的设计理念,包括其应具备的特性、面临的挑战以及可能的解决方案。我们将讨论如何通过创新的设计和技术,打造出能够满足未来需求的操作系统。
17 1
|
2月前
|
消息中间件 并行计算 网络协议
探秘高效Linux C/C++项目架构:让进程、线程和通信方式助力你的代码飞跃
探秘高效Linux C/C++项目架构:让进程、线程和通信方式助力你的代码飞跃
36 0
|
3月前
|
Ubuntu 网络协议 Linux
【操作系统】探究驱动奥秘:驱动程序设计的解密与实战
【操作系统】探究驱动奥秘:驱动程序设计的解密与实战
31 0
|
4月前
|
开发者 UED 智能硬件
请简要介绍一下鸿蒙操作系统的特点和优势。
请简要介绍一下鸿蒙操作系统的特点和优势。
66 0
|
5月前
|
程序员 Linux
不愧是华为内部的“操作系统学习笔记”,一篇说细节,一篇讲哲学
当然重要,身为程序员的我们,那更应该深刻理解和掌握操作系统,虽然我们日常 CURD 的工作中,即使不熟悉它们,也不妨碍我们写代码,但是当出现问题时,没有这些基础知识,你是无厘头的,根本没有思路下手,这时候和别人差距就显现出来了,可以说是程序员之间的分水岭。
|
6月前
|
运维 Java Unix
深入挖掘Linux内核源码:揭秘其惊人的架构和设计
深入挖掘Linux内核源码:揭秘其惊人的架构和设计
|
12月前
|
机器学习/深度学习 人工智能 自然语言处理
大模型的「狂飙时代」,以开源之力推动「新Linux底层操作系统」
大模型的「狂飙时代」,以开源之力推动「新Linux底层操作系统」
155 0
|
传感器 Linux 芯片
Linux驱动开发——内核I2C驱动
Linux驱动开发——内核I2C驱动
243 0
Linux驱动开发——内核I2C驱动
|
Rust 安全 Linux
一种设想:为linux建立一个微内核,融合OS内核与语言runtime设想
本文关键字:os之争。微内核,language based os,language on bearmetal not on os,华为鸿蒙,语言即OS,类脚本语言,把原生应用变语言模块。
602 0
一种设想:为linux建立一个微内核,融合OS内核与语言runtime设想