本回开始讲解一个比较有意思的部分,操作系统kernel被bootloader加载并运行的过程。操作系统中的进程管理、内存管理、存储管理等重要内容都是在操作系统Kernel中实现。但本回主要是为了体现kernel被加载到内存被执行的过程,不是讲解kernel具体功能的实现。因此kernel会很简单,本质就是一个C语言编写的小kernel,功能就是一个while无限循环。经过这一步过后,后续的开发就可以使用C语言来开发,但是为了跨越这一步,需要经过曲折的路程。操作系统kernel包含两部分,一部分是x86_64汇编编写的kernel header,用于是解析C语言编译的kernel core,即Linux ELF格式文件。然后将kernel core的代码、数据部分复制到其运行地址处;第二部分为kernel core,操纵系统具体功能的实现部分。操作系统kernel整体加载与运行的逻辑如下图图1所示:
图1
图中①由上一回已经讲述过具体的过程,这里只是强调一下现阶段所有操作都是运行在实模式下。图中②③是bootloader加载操作系统kernel header与core到内存。bootloader运行后会设置CPU相关处理器进入保护模式。这一部分包括创建保护模式使用的代码段、数据段GDT,打开A20线,设置CR0寄存器PE位,最后使用段间跳转指令jmp进入保护模式如图2所示。需要注意的是现在还未打开分页功能,只是进入段管理模式的保护模式。进入保护模式后,开始创建iae-32模式所需要的页表相关内容,包括4级表头、页目录指针表、页目录表、页表。处理器已经进入64位时代,基本都采用4级或5级页表模式,但这与32位时代经典的3级页表模式类似,区别在于具体的各个表项的属性不同,表项为 图2
8字节,不再是4字节。本系列操作系统实验中只使用了物理机内存6M以内的空间,而且分页管理中的物理页大小为2M。开启分页前为了平滑过渡到分页内存管理,设置内存地址0~6M范围内通过段式内存管理访问与分页内存管理访问到的物理内存是一样的,这个可以根据具体的情况进行处理。由于iae-32 64位模式下操作系统kernel访问的虚拟机地址是在扩高地址0xffff800000000000以上,因此需要建立虚拟地址空间0xffff800000000000~0xffff800000600000到物理机内存0~6M。开启分页管理的相关汇编代码如图3所示,其中省略了4级表头、页目录指针表相关的设置代码,但是不影响总体流程的理解。
由于进入保护模式以及开启分页管理之后,无法直接调用BIOS提供的中断服务。那怎么样像第五回所述那样读取硬盘数据和显示信息呢?这个是有办法的,可以使用in、out IO指令读取硬盘的端口从而进行数据的读取。而显示数据更为简单,可以按照显存的相关规范向显存中mov数据即可,因为物理内存中某一段内存已经分配并映射为显存。这一部分主要是涉及硬盘、显示器的接口规范,原理与实现比较简单,只是编写汇编相对比较繁琐,各位看官可查阅相关资料。
图3
现在到了一个历史性转折点,前期准备工作都已完成,怎么从iae-32保护模式进入iae-32 64位长模式呢? 其实我们在图1中创建的第三个代码段段描述符就是为进入iae-32 64位长模式做准备,这个段描述的长模式位已经置位,当通过该段描述符跳转运行代码时处理器则进入iae-32 64位长模式。操作系统kernel header和core都是运行在64位扩高地址以上,因此在当前系统栈中依次压入上述64位代码段段描述符选择子、kernel header地址,模拟段间跳转栈中的数据。最后使用段间跳转返回指令retf弹出上述段描述选择子与kernel header地址,开始进入kernel header运行。
Kernel header的主要有两个作用,一是解析kernel core的ELF文件,将其代码段迁移至0x00100000处,如图1中的④;二是编译kernel core的时候指定运行的起始虚拟地址为0xFFFF8000100000,因此需要设置kernel core的运行地址位于0xFFFF8000000000之上。Kernel header解析并复制kernel core的汇编代码如图4所示。图4中的步骤1是根据图5所示ELF header的数据规
图4
图 5
范解析出Program header表的起始地址,Program header每个表项的大小,Program表项数目,从而遍历代码段、数据段等可加载到内存运行的数据。图4中的步骤2是判断该Program header表项所指向的ELF段是否是可加载到内存运行的有效数据。如果是,图4中的步骤3根据图6中的规范开始从该Program header表项解析出ELF段在ELF文件中的偏移、加载的内存虚拟地址以及段数据大小。进而将从kernel core ELF文件中的kernel core的代码段、数据段复制到Program header表项所指出的内存虚拟地址处。图4中的mem_cpy函数执行具体数据复制操作,该函数本质就是从栈中获取ELF段在文件中偏移、基于CORE_PHY_ADDR地址处的起始地址,复制目的地址即为加载的内存虚拟机地址,然后使用设置使用rep movsb复制数据。图7展示本回中的kernel core ELF文件各个ELF段的信息,各位可以重点关注标识的kernel core代码段信息。
图6
图7
Kernel header复制完kernel core的数据后,通过jmp指令将接力棒交给kernel core. kernel core将在虚拟地址0xFFFF8000100000处运行,该虚拟地址映射的物理内存地址为0x100000。图8所示右上角为本回的kernel core C程序,下面为kernle core编译后的汇编指令。整个kernel core的虚拟机地址空间从0xFFFF8000100000开始编址,代码大小一共6字节,这些与图7中的信息相匹配。
图8
图9为整个bochs虚拟机启动后在屏幕输出的内容,显示处理器从开启到进入iae-32模式一些关键阶段信息,这些信息输出都是在bootloader中实现。由于kernel core只是一个无限循环,没有向屏幕输出任何信息,这里大家可以思考一下这个kernel core C程序能否调用bootloader中实现的汇编打印输出函数向显示器输出信息呢?因此需要进入bochs虚拟机进行调试,查看处理器相关寄存器内容,以确认处理器是否运行的为我们的kernel core程序。图10为我们在bochs虚拟机中调试的结果,虚拟机中运行指令的汇编与图8中的汇编一样。
图10中的1显示运行的虚拟地址为0xFFFF8000100000,图中的2显示kernel core在0xFFFF8000100004处不断的自循环,这完全符合我们的预期。经过这一
图9
图 10
回后,相信大家对操作系统内核不在那么恐惧,它的本质就是一个C语言程序。只是真实的操作系统内核运行在内核态,代码数量庞大,控制着计算机的硬件,为用户态程序提供运行环境。后续我们就可以基于这个小型kernel core不断的丰富其功能,让它具有一个小型操作系统kernel该有的功能。
======================================================================
各位看官如果对本系列有兴趣,大家一起学习交流,可以加一下该系列微信公众号,鼓励继续写的动力。声明此公众号是个人申请用于交流学习底层技术,不会涉及其他商业行为。
微信公众号链接: 第六回-操作系统Kernel加载与运行
======================================================================