http://dev.10086.cn/cmdn/bbs/redirect.php?fid=48&tid=2433&goto=nextoldset
开发人员有必要理解CE系统启动过程。首先回顾一下系统怎样建立起来的。微软工具链生成.exe和.dll文件。这些文件都包含了Portable Executable格式,简称PE格式。它们的结构都是一样的: 1、 是一种common object文件格式的扩展 2、 有导入、导出表 3、 头部有入口点,是开始执行的地方。 操作系统都是由编译器生成的,一个exe(nk.exe)不会连接到任何外部的库或者DLL。当这个文件执行时候,系统中还没有任何东西。Exe需要具有一个已知的头部(PE),来决定程序入口点。因而CPU能知道从那里开始执行。 另外,PE文件可以按序排列,所以可以XIP(execute in place)。这意味着,加入文件的数据放在某一个虚拟地址,不需要改变情况下,程序代码可以访问和使用这个地址中的数据。例如,使用微软的链接器,把内核代码文件放到虚拟地址0x80000000。那么程序入口地址会放到exe的文件中,执行时候就能依靠地址,跳到真正的代码段执行。如果函数foo是放在0x80001000的,foo中又调用了函数bar,bar地址在0x80005000。那么会有一段结构直接保存在代码中,去调用地址0x80005000。如下,虚线是函数代码的分割线。 假如内核的exe文件改变的地址,bar函数也会跟着移动。Foo函数的调用地址,现在指向不对了,需要指向新的地址。
上图是内核exe文件从0x80000000移到0x80050000,foo函数内的调用地址就不对了。 进程当加载到真正的地址空间后,修改exe和dll文件的动作,称为——修正。普通的exe文件允许程序修正地址的记录,不修正前地址都是错误的。所以CE内核exe在加载到特定地址前,会做地址修正。ROMIMAGE程序在生成系统镜像文件前(nk.bin),会修正内核的exe和某些dll的地址。 最后,我们得到一个修正后的exe——nk.exe,系统内核的一部分。这个exe和其他exe、dll一样,有程序入口点。执行前,系统的bootloader会把镜像文件放到正确的地址中。下面我们来看看bootloader如何在镜像中,找到nk.exe和它的入口点。
Nk.exe是CE6内核的唯一部分,包含了OAL和系统启动的模板流程。这个流程主要的部分,操作系统内核的所有进程、线程和内存管理放到kernel.dll中。这个dll也是经过ROMIMAGE修正过运行的虚拟地址了。这就是说至少有2个可执行模块,我们需要找到存放的地址和入口点。入口点的地址在exe和dll中,但在镜像中怎样找到exe和dll呢。 CE镜像有一个重要的结构体,通过ROMIMAGE生成的,叫Table Of Contents,简称TOC。TOC保存了系统的指针和数据。在镜像文件开头附近,有一个标志,内容是CECE(0x44424442)。这个标志后面就存放着TOC的偏移值,那么bootloader和其他程序可以通过TOC找到镜像相关的信息。这个偏移值在OAL中定义了一个全局指针pTOC来保存,ROMIMAGE可以使用这个指针来找到和填充TOC的内容。编译时候,nk.exe的pTOC变量是0xFFFFFFFF,当生产nk.bin时候,ROMIMAGE会做以下处理: 1、 加载nk.exe,然后修正 2、 生产TOC内容,找到镜像文件存放TOC的地方 3、 找到pTOC指针,确认是指向0xFFFFFFFF 4、 把pTOC的指针,执行真正TOC所在位置 那么当nk.exe开始运行时候,就知道在那里能找到TOC。再根据TOC的内容,找到镜像其他部分。 ROMIMAGE通过bib文件,获取系统镜像的地址分布。Config.bib有2个重要部分,RAMIMAGE和RAM。下面是例子: NK 0x80070000 0x02000000 RAMIMAGE RAM 0x82070000 0x01E7F000 RAM 这是告诉ROMIMAGE该怎样做,系统镜像在地址0x80070000,可读写的内存地址在0x82070000。根据这些信息,就能知道那里可以加载模块运行,然后建立TOC内容。为了让内核运行起来,TOC也会存放这RAM的信息。下图是内存中内核放置的示意图:
操作系统要运行,还需要bootloader做以下工作: 1、 把镜像放到内存的正确地方 2、 找到CECE标记 3、 使用TOC指针,找到TOC 4、 在TOC中,找到nk.exe的地址 5、 扫描exe文件,找到入口点(通过PE) 6、 跳到入口点地址,开始执行 Nk.exe运行时: 1、 建立和打开虚拟内存映射 2、 收集kernel.dll运行需要的信息 3、 使用pTOC找到kernel.dll 4、 找到kernel.dll入口点 5、 把收集到的信息,传入kernel.dll的入口点 不同的处理器在启动过程不太相同,ARM和X86的CPU有不同的虚拟内存管理器(MMU)。但是大体的流程是相同的。当nk.exe运行前,系统有些条件是一致的: 1、 所有的cache是关闭的 2、 在config.bib配置的RAMIMAGE和RAM段,物理上可访问的,可读的。 3、 虚拟地址是预先确定好的 4、RAM无需额外操作,就可以写入。 以上是任何系统启动前的先决条件。内核运行是独立的,不会依赖运行前的bootloader配置的虚拟内存。当内核运行时,nk.exe首先是计算OEMAddressTable中的物理地址。OEMAddressTable是静态定义了虚拟地址和物理地址的映射。Nk.exe知道: 1、 所属的虚拟内存 2、 所属的物理内存 3、 OEMAddressTable的虚拟地址空间 一个简单公式,计算内核OEMAddressTable的物理地址: NK:
hysicalBase + (NK::Virtual OEMAddressTable – NK::Virtual Base) è NK Physical OEMAddressTable OEMAddressTable的格式: ... 根据以上表格的信息,nk.exe可以通过MMU设置虚拟内存的映射关系。虚拟内存使用OEMAddressTable中的数据,并且使其生效,然后Nk.exe转换为可执行的虚拟地址。 注意的是,所有在RAM中的模块都还没初始化。不管RAM初始化后的数据是多少,初始化数据都还保存在镜像文件中(data段的数据)。对数据的读写,必须要把镜像的真实数据内容,复制到RAM中,才允许使用。那么nk.exe如何知道数据段在镜像那个位置呢,通过TOC。 TOC不但列出了镜像中,各个模块的开始地址,还描述了各个模块的读写指针。从系统镜像复制到RAM的动作称为——copy entries。Nk.exe在访问读写变量之前,需要copy entries到RAM中。指针pTOC就必须是有效的,如何保证pTOC是有效呢。pTOC是只读变量,在镜像文件创建时,ROMIMAGE就会把pTOC写入。保存pTOC的介质不是RAM,在使用pTOC前,不需要复制到RAM中。Nk.exe有函数把所有的相关信息复制到RAM,称为KernelRelocate。这是一个简单的过程,只是遍历一个表格内的结构体,然后把虚拟内存内容复制出来。当这个动作结束后,nk.exe的变量才能像其他程序一样,可以被正常的访问。
这时,我们才有真正可以工作的程序,像之前提到那样可以执行、调用函数、读写内存。这还不是线程、进程或任何系统的对象,但是所有东西都放到已知的地方,在系统高端地址开始执行时候,可以使用到。 虚拟内存有很大的弹性,CE保留了一些虚拟地址段,只给系统内核使用。大小为4K页面的虚拟内存,在0xFFFE0000以上的高端地址空间中,保留起来。内核映射了一些物理地址到这些页面,用来保存全局动态的数据。它们一部分用来MMU的内存映射,一部分保留用来做内核态和中断的堆栈,最重要是一部分保留作为Kernel Data Page。根据内核版本,保留不同的页面大小。Nk.exe直接可以访问和初始化这些页面。 Nk.exe的3个重要数据 1、 pTOC的备份 2、 OEMAddressTable的地址 3、OEMInitGolbals函数的地址 前2项内容保存在Kernel Data Page中,任何代码知道这个页的地址,就可以找到系统镜像的内容和基本的虚拟映射关系。最后一项信息比较特殊,nk.exe使用一次后就传递给kernel.dll了。放置方式如下:
现在Kernel Data Page被初始化了,虚拟内存也激活了,可以跳入到微软的kernel.dll中入口了。记住,我们通过TOC找到镜像的kernel.dll,同时也可以找到其他模块的入口。即使Nk.exe知道如何把Kernel Data Page放到虚拟内存中,但kernel.dll不知道确认它自己运行位置。因此,我们需要把Kernel Data Page的虚拟地址传递给kernel.dll的入口。 跳转完成后,开始执行内核代码。入口点获取了Kernel Data Page的地址,因此通过TOC可以获取任何系统镜像的信息。内核开始做一些准备工作和临界区,确保它是Kernel Data Page当前唯一使用者。 Kernel.dll有一个静态函数和数据表,编译时候作为dll的一个静态数据结构体,称为NKGlobals。由于kernel.dll被ROMIMAGE修正过,运行在特定的地址中,所以运行时NKGlobals的指针也会被修改成正确的地址。这些函数指针中,如SetLastError()和NKwvsprintfW(),内核允许它们直接调用。但内核并不清楚这些函数其实在kernel.dll中,接着内核会被告知这部分的函数和数据,其实是在kernel.dll中。 Kernel.dll通过OEMInitGlobals,把NKGlobals的地址传回nk.exe。流程如下:
如上,OEMInitGlobals函数保存了一个指向OMEGlobals结构体的指针。这个结构体是内核能够其他功能函数的关键。Kernel.dll模块确立后,可以被任何一种结构的处理器运行(如x86、ARM等)。Nk.exe提取了这类处理器的特有部分,提供给平台,来确保系统的运行(xcale或OAMP,它们与ARM有些微差别)。OMEGlobals的组成与NKGlobals类似,有以下成员:
- PFN_InitDebugSerial(), PFN_WriteDebugByte(), PFN_ReadDebugByte()
- PFN_SetRealTime(), PFN_GetRealTime(), PFN_SetAlarmTime()
- PFN_Ioctl()