我们首先先看一些UEFI 的框架图:

uefi 启动esxi_框架


无论是从功能划分还是各个文件划分,我们都可以看到这种模块话思想。模块话是现在软件的主流,这样能让不同的人独立开发自己的模块,通过统一的接口添加整合到一个firmware file里。

我们uefi firmware 模块,是通过几个core 联系到一起的,这几个core 包括(SEC core, PEI core, Dxe core, SMM core)。如果单单从软件角度讲想学习UEFI 架构,那么这几个core 是很值得学习的,因为他们说架构核心。当然这是指软件方面的,因为还有各种外围设备,总线协议,等等要去学习或者了解。

在uefi 世界里,efi 文件我们是不会陌生的,这些文件就是围绕各种core 身边然后组成一个完整uefi firmware 。所以也有必要了解一下efi。efi 文件就是一个小的module ,是一种可执行文件类型。比如Microsoft PE/COFF , Linux EFL 等等。这些可执行文件类型都有相似之处,应为都是同一种文件类型发展演化出来的。
efi 文件里面包的文件类型可能是PE32, TE(精简版PE), PE32+中的一种。efi 文件作为可以被动态载入然后被执行,那么其中的文件类型可以选择linux 下.so 和 windows .dll。 相比so 文件,dll 文件重定向更简单,效率可能会高点,但是dll 会比.so 文件占用更多的空间。intel 选择了dll 文件类型,可能有他们的考量吧。

介绍EFI 这种能被动态load 然后还能被执行的image 来讲,我们首先先来热身一下,通过一个例子,我们可能会比列出各种spec ,贴代码强多了。通过例子来思考可能会有带入感,再去看spec,就会很有感觉。

  1. 我们首先先实现一个c 程序a.c 和一个经过编译过的可执行文件b.bin。在a.c 里去动态加载到内存里,然后把控制权交给b.bin

这个问题首先我们是要练习如果在一个程序里去加载另外的程序并执行它,所以我们不考虑增加线程或者进程去实现它。

下面是我的实现思路(linux )

  1. 这个linux 下比较好实现,其实windows 下思路一样,只不过函数名字不一样而已。linux 有一个mmap函数它会把其他文件映射到当前进程的虚拟空间里。并且映射的地址空间可以设置成可读,可写,可执行。所以知道这个mmap function 其他就迎刃而解了。下面是伪代码:
//a.c
handle = open(b.bin)
// address 是映射文件到内存返回的虚拟地址
address = mmap(handle,可读,可写,可执行)
//把控制权交给b.bin
jmp/call  address  
--------------------------------
b.c
// b.bin 就是一个打印hello world 的可执行文件
char str[] = "hello world\n"
write (4, 1, str, strlen(str));
  1. 思路就是这样,看起来a.c 不难,b.c 更是简单,但是把这两个合在一块能在屏幕上打印一个hello world 还是有点细节要注意的。(赶紧去写一个吧,看看能不能打印出来)。
  2. 如果自己写的执行有点问题可以接着往下看看,我直接就贴代码了。
.section .data
output:
  .asciz "Hello World\n"
length:
  .int .-output

.section .text
.global _start
_start:

  movl $4, %eax
  movl $1, %ebx
  movl $output, %ecx
  movl length, %edx
  int $0x80
  movl $1, %eax
  movl $0, %ebx
  int $0x80  
; 这个就是那个只打印hello world 的源文件 hello.s,
; 这里是用的汇编调用int 0x80 的function 4 也就是 write。

编译hello.s :
as hello.s -o hello.o
ld hello.o -T test.ld -o hello.elf
objcoby -j .text -O binary hello.elf hello.bin
第一条,把hello.s 编译成目标文件hello.o
第二条,把hello.o 链接成一个可执行的文件hello.elf 其中test.ld 是我写的一个链接脚本主要是把链接的的Section 从0 地址开始,并且把数据段和代码段合并成一个Segment。
第三条, 主要是避免还得多了解elf 文件格式而浪费时间,我这边只输出一个没有文件格式的可执行文件.bin (当然在linux 下也可以用nasm 去编译,会省去第二条和第三条)

hello.s 还是很简单的,但是有一个细节地方

movl $output, %ecx

这句话意思是把output 的地址给到ecx寄存器,那么问题来了,当我们编译的时候这个地址编译器会用0x00000000先代替,链接的时候链接器会根据代码实际地址进行修正。链接器是如何知道哪个地方要修正?修正成多少?
第一个问题,链接器会根据目标文件里的可重定向表,去修正要重定向的值。
第二个问题,修正成多少(这里有很多规则,不详细讲解,具体请参考elf 文件相关文档),简单理解是由链接器决定的,配置的话可以下参数,也可以写一个链接器脚本(相当于我们.fdf 文件)

先来看一下hello.s 汇编之后的目标文件

uefi 启动esxi_linux_02

我们可以看到代码段a 行,f 行,那两段给寄存器填的值都是0,也就是说这两个值是要重定向的。我们也看一下重定向表长啥样:

uefi 启动esxi_框架_03

这张图,我们可以看到编译器要告诉它人,.text 需要重定向,两个地方,分别是.text 段偏移0x0b 位置,和 0x12 的位置,填什么值,后面VALUE 字段有讲。

于是乎,链接器知道哪里需要修改了,好了它去执行链接(这篇文章例子是用的静态链接,linux 动态链接因为和主题无关,略去),看看静态链接之后可执行文件长什么样。

uefi 启动esxi_uefi_04

看到了a,f 行,分别给寄存器的值修正成0x24,和0x31, 因为我们编译的时候把base address 设成0 了。仔细一想,我们如果把hello.bin 在a.c 里load 到虚拟地址0开始的位置,那么这段代码就不用修正(这叫固定加载,之后看efi 加载的时候会看到,固定下载相关的代码)。但是这样只能load 在固定位置显示不太合理。所以我们还得手动修改a,f 行的修正值,举例,如果我们的hello.bin被load在0x10000处,那么output地址0x10024,
length 地址就是0x10031,我们的任务就是让代码段偏移0x0b,0x12地方的里值 + 0x10000。(这个例子就和uefi 代码里load efi image 的时候很像了,如果这个例子弄明白,关于efi 文件加载,执行,估计只要贴两个图,就明白怎么回事了,根本不用看代码翻spec,信不?)

主要分析过程结束。看看代码的执行结果

uefi 启动esxi_框架_05


hello.bin 被load 在虚拟地址0x50000000 位置,我们看看0x0B 和 0x12 位置被修改成了0x50000000 + 0x24和0x50000000 + 0x31, 最后打印出hello world。

代码链接:
app