内核雏形

简介: 内核雏形

引言

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

整体设计

  • 在前面的章节当中,我们做过很多实验,这些实验全部都放在了 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 内存空间任意使用了,这里先有个印象如果以后内核莫名其妙的出问题了,可以查看一下是否是内核程序过大了
目录
相关文章
|
JavaScript API 虚拟化
20个基于DPDL开源项目,带你冲破内核瓶颈(上)
20个基于DPDL开源项目,带你冲破内核瓶颈
|
26天前
|
存储 Linux 开发者
探索操作系统的内核——从理论到实践
操作系统是计算机科学的核心,它像一位默默无闻的指挥官,协调着硬件和软件之间的复杂关系。本文将深入操作系统的心脏——内核,通过直观的解释和丰富的代码示例,揭示其神秘面纱。我们将一起学习进程管理、内存分配、文件系统等关键概念,并通过实际代码,体验内核编程的魅力。无论你是初学者还是有经验的开发者,这篇文章都将带给你新的视角和知识。
|
4月前
|
存储 人工智能 算法
操作系统的演化之路:从单一任务到多任务处理
【8月更文挑战第12天】 在计算机科学的历史长河中,操作系统作为硬件与软件之间的桥梁,其发展经历了由简单到复杂、由单一到多元的转变。本文旨在探究操作系统如何实现从执行单个任务到同时管理多个任务的飞跃,并分析这一变革对现代计算技术的影响。通过回顾操作系统的关键发展阶段,我们将理解多任务处理机制的起源和优化过程,以及它如何塑造了今天的数字世界。
|
7月前
|
算法 搜索推荐 开发工具
探索代码的奥秘:技术感悟与实践探索操作系统的心脏:内核
【5月更文挑战第31天】在数字世界的编织中,每一行代码都承载着创造者的智慧和汗水。本文将带你深入编程的核心,揭示那些隐藏在日常开发实践中的技术真谛。从算法的精妙到系统的架构,我们将一同探讨如何通过技术提升效率,解决问题,并在这个过程中获得个人成长。 【5月更文挑战第31天】本文深入剖析了操作系统的核心组件——内核,探讨了其设计哲学、功能职责以及在现代计算环境中的重要性。通过分析内核的工作原理和它如何与硬件、软件交互,我们将揭示这个隐藏在用户界面背后的力量之源。
|
3月前
|
存储 安全 Linux
操作系统的心脏:内核探秘
在数字世界的庞大机器中,操作系统扮演着至关重要的角色,而其核心—内核则如同这台机器的心脏。本文将深入浅出地剖析操作系统内核的设计哲学、功能组成以及它如何管理硬件资源和提供系统服务。我们将一同探索进程调度、内存管理、文件系统等关键组件,并通过实例了解它们是如何协同工作以确保系统的高效与稳定。无论你是技术新手还是资深开发者,这篇文章都将为你打开一扇了解操作系统深邃世界的大门。
46 3
|
3月前
|
存储 安全 算法
探索操作系统的心脏:内核架构与机制的深度剖析
本文旨在深入探讨操作系统的核心——内核,揭示其架构设计与运行机制的内在奥秘。通过对进程管理、内存管理、文件系统、设备控制及网络通信等关键组件的细致分析,展现内核如何高效协调计算机硬件与软件资源,确保系统稳定运行与性能优化。文章融合技术深度与通俗易懂的表述方式,旨在为读者构建一幅清晰、立体的内核运作全景图。
86 0
|
4月前
|
人工智能 安全 物联网
操作系统的演变之旅:从单一任务到多任务处理
【8月更文挑战第14天】在数字时代的浪潮中,操作系统作为计算机的核心,经历了翻天覆地的变化。本文将探讨操作系统从简单到复杂的发展过程,特别是如何从最初的单任务处理进化到现代的多任务并行处理。我们将穿越时间的长河,见证操作系统如何应对不断增长的计算需求,并分析这一演变对用户体验和系统性能的深远影响。
|
6月前
|
安全 算法 Linux
探秘操作系统:从内核到用户界面的奥秘
【6月更文挑战第10天】本文将带领读者深入探索操作系统的多维世界,从其最基础的内核设计到丰富多彩的用户界面,我们将一窥这些软件巨匠如何支撑起现代计算的基石。不同于常规的技术解读,我们将通过故事化的叙述,揭示操作系统如何响应硬件、管理资源以及提供用户体验,展现它们不仅仅是代码和算法的集合,更是人类智慧与创造力的结晶。
|
缓存 网络协议 Linux
20个基于DPDL开源项目,带你冲破内核瓶颈(中)
20个基于DPDL开源项目,带你冲破内核瓶颈(中)
|
6月前
|
存储 物联网 调度
探秘操作系统的心脏:内核
【6月更文挑战第6天】本文将深入探讨操作系统的核心——内核。我们将从内核的定义和功能出发,逐步揭示其在操作系统中的重要性。接着,我们将详细解析内核的主要组成部分,包括进程管理、内存管理、文件系统等。最后,我们将探讨内核的发展趋势,以及它在未来操作系统中可能扮演的角色。

热门文章

最新文章