临时接手BIOS相关代码的验收与修改工作,一头雾水555,花了快一个月时间才大体上有了认识,然后搭好一个简单的模拟环境。

 

一 UEFI与TianoCore简介

“统一的可扩展固件接口”(Unified Extensible Firmware Interface),是一个旨在代替传统Bios,统一操作系统与底层固件之间的连接层的公开标准,各大厂商基于此标准进行了自己的实现。UEFI的主要用途(个人理解)是在主板接上电后,进行硬件的检查和初始化,然后引导操作系统启动,并为运行中的操作系统提供服务。

 

图1 UEFI启动流程

TianoCore是由Intel开源的一个UEFI实现,可以用于UEFI的学习和研究。下文对Windows10系统下的EDK2(uEFI Development Kit Ⅱ)模拟环境搭建方法进行叙述。

主要有两种搭建模拟环境的方式,Nt32功能包可以直接编译产生一个可执行程序(Windows下是.exe文件),运行后就启动一个模拟的UEFI界面。这种方式可以很方便的调试程序,但无法对模拟的硬件平台进行修改,比如只能在32位下运行。还有一种方法是通过OVMF功能包编译UEFI固件镜像(.fd文件),然后在QEMU模拟器中运行,通过模拟器的自带功能来修改环境硬件配置。

 

二 Nt32模拟环境安装配置

2.1环境信息

操作系统:Windows Professional 10.0.17763.678

CPU:Intel Core i5-8265U

内存:8G

 

2.2安装步骤

  1. 安装windows SDK(10.0.18362.0):https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk
  2. 安装Visual Studio 2015 Community(或者其他C编译器):https://my.visualstudio.com/Downloads?q=visual%20studio%202015&wt.mc_id=o~msft~vscom~older-downloads 需要登录MS账号并加入Dev Essentials计划。
  3. 安装IASL编译器:https://acpica.org/downloads/binary-tools
  4. 下载EDK2 UDK2018:https://github.com/tianocore/edk2/tree/vUDK2018

解压后将所有文件放到C:/MyWorkspace下

  1. 下载安装NASM、PYTHON27、OPENSSL。链接:http://www.nasm.us/

 https://www.python.org/http://wiki.overbyte.eu/arch/openssl-1.1.0g-win32.zip

可以下载cx_Freeze-4.3.4.win-amd64-py2.7并放到PYTHON27目录下的Scripts下。不下载会在编译过程中提示WARNING但不影响UDK2018环境的搭建。将这三项都安装到C盘目录下。然后在系统环境变量PATH中添加C:\NASM和C:\Openssl。新建环境变量PYTHON_HOME,值为C:\Python27。

  1. 下载https://github.com/openssl/openssl/archive/OpenSSL_1_1_0g.zip,然后解压到C:\MyWorkspace\CryptoPkg\Library\OpensslLib中,重命名为openssl。在加密库中添加openssl源码。
    1. 直接下载已编译好的运行工具base tools。从https://github.com/tianocore/edk2-BaseTools-win32 下载并解压到C:\MyWorkspace\BaseTools\Bin,将文件夹重命名为Win32,注意忽略此步会在接下来的搭建中遇到报错环境变量PYTHON_HOME的问题。
    2. 打开CMD,运行命令:

cd C:\MyWorkspace

edksetup --nt32

初始化UEFI的运行环境,运行后会在C:\MyWorkspace\Conf目录下生成三个配置文件,打开target.txt,将其中的TOOL_CHAIN_TAG的值修改为VS2015x86(所使用的编译器,可以参考同目录下tools_def.txt中的名称)。

Nt32Pkg(Nt32Pkg / Nt32Pkg.dsc)仿真平台需要Windows头文件。--nt32选项会檢測Microsoft Visual Studio是否安裝並執行它的setup命令,例如vsvars32.bat,从而包含Windows头文件。

  1. 运行命令:

build

开始安装。

 

图3

  1. 运行命令:

build run

或者打开C:\MyWorkspace\Build\NT32IA32\DEBUG_VS2015x86\IA32文件夹下的SecMain.exe

启动UEFI的Nt32模拟环境。

 

图4

 

Nt32模拟环境安装成功!

在此环境中可以学习EFI SHELL的功能和设置细节,测试写好的小程序。若要进行更深度的开发工作则可使用UDK源码中的OVMF包。安装OVMF能够获得一个运行在QEMU模拟器中的完整UEFI固件镜像。

2.3遇到的问题

 

  1. Rebuild过程中py脚本报编码错误:遇到此种有明确位置提示的报错,首先根据报错信息去分析,报错源头是print语句,报错信息类型是UnicodeEncodeError,可以确认是在打印编译信息时出现了编码错误。

图5

我们先找到NmakeSubdirs.py文件,然后对报错的print(message),修改message对象的编码格式(.encode(encoding='UTF-8')),通常使用UTF-8格式即可。

 

  1. 报找不到CYGWIN_HOME环境变量的WARNING,因为UDK默认编译器是Cygwin,所以编译时会先去寻找其路径,可以无视,实际上我们使用了VS进行编译。对于找不到其他变量地址的WARNING,可以检查一下是否已经在系统环境变量中进行正确的设置。

 

图6

  1. 大量“无法解析的外部符号”报错。我对配置文件进行了修改,然后又用新的UDK源码覆盖了原来的文件夹,再次编译时就出现了此错误。无法确定是哪些文件不匹配导致的问题,因此我将MyWorkspace文件夹下的所有文件都删除,然后按照前文的步骤重新安装,没有出现错误。

 

 

 

图7

  1. 负责解析FDF文件的GenFds工具(实际上是封装为.exe的Python脚本)报错,可能与操作系统版本以及Windows SDK的版本有关。
     

图8

通过百度查询,得知可以使用源码包中自带的备用版GenFds(.bat脚本)。将C:\MyWorkspace\BaseTools\Bin\Win32文件夹下的GenFds.exe更名为GenFds.LABZ,然后将BaseTools\BinWrappers\WindowsLike;添加到Path路径中,编译时就会优先从WindowsLike文件夹中寻找备用的GenFds工具。(此问题的解决可以参照http://www.lab-z.com/udk2018coming/https://github.com/tzz1996/Blog/blob/master/compile_udk2018_in_windows.md

 

三 OVMF固件编译及虚拟机环境配置

3.1环境信息

同上

3.2安装步骤

  1. 下载并安装QEMU:https://www.qemu.org/download/

根据自己的操作系统环境选择安装方式,这里我们选择下载qemu-w64-setup-20190815安装包(QEMU 4.1.0),安装到C:\Program Files\qemu。

  1. 如果之前没有进行过前述Nt32模拟环境的安装,则需要先执行其安装步骤的第1-7步。
  2. 打开CMD,运行命令:

cd C:\MyWorkspace

edksetup –nt32

设置环境变量

  1. 运行命令:

build clean

build -a X64 -p OvmfPkg\OvmfPkgX64.dsc

编译64位OVMF固件。

或者运行命令:

build -a IA32 -p OvmfPkg\OvmfPkgIa32.dsc

编译32位OVMF固件。

编译完成后的固件是C:\MyWorkspace\Build\OvmfIa32\DEBUG_VS2015x86\FV\OVMF.fd

  1. 通过QEMU命令运行编译好的固件并设置硬件参数,运行命令:

cd C:\Program Files\qemu

qemu-system-x86_64.exe -bios "OVMF.fd" -m 1024 -cpu "qemu64" -vga cirrus -serial vc -parallel vc -name "UEFI" -boot order=dc

参数说明

1. qemu-system-x86_64命令是结合自己的电脑构架使用的,因为本机使用的是Intel x64平台;

2. -boot d:表示从CDROM启动系统,因为虚拟盘中目前还没有系统,可以从CDROM中启动安装盘;

3. -cdrom:可以指定系统iso镜像名称;

4. -hda:指定之前创建的虚拟磁盘作为模拟环境的硬盘;

5. -m:指定了QEMU使用的系统内存大小,这里指定的是2G;

6. -bios:指定了QEMU运行的BIOS,默认使用的是seabios,这里使用了自己编译的OVMF.fd。

7. -smp,指定核数。

8. -serial stdio:指定io方式,可以定义为标准输入输出流。

 

输入命令后,可以看到qemu启动并加载了TianoCore,进入到EFI SHELL命令行界面。

 

图9

OVMF编译运行成功!

3.3遇到的问题

  1. build时报错:

 

图10

可以看到是python脚本在运行sql语句时报错,去查找MetaDataTable.py中的函数定义,看不出问题,因此在调用SqlCommand前添加一行打印语句print SqlCommand,查看打印输出。输出显示显示报错的是insert的语句,向8列的表插入7个值。解决思路:可以记录下该表的表名,在检测到要向该表插入数据时,检测拼接到SQL语句中的插入值的数量,并根据缺少的数据,额外拼接一个默认值到SQL语句中。

实际情况中,添加了打印语句后,该问题就不再出现了。

 

  1. 通过qemu启动后找不到fs0:盘符:

 

图11

可以使用qemu-img create -f qcow2 OS.img 20G 命令创建一个20g大的虚拟磁盘并在运行固件时加载,qcow2类型文件系统支持写时复制,加密,压缩以及VM快照。除此之外,如下类型也是被支持的:raw ,cloop ,cow,qcow,vmdk ,vdi ,vhdx,vpc 。

生成的虚拟盘中放入事先编译的.efi程序,就可以在挂载后运行。FS0就存在于虚拟磁盘中,它用来存放启动系统的GRUB文件(如果安装了系统),可以直接执行引导的.efi文件来启动系统

 

四 附录

4.1EFI SHELL命令表

Shell提供了丰富的内部命令。如果不清楚某个指令的用法,可以使用help。比如:help ifconfig,就可以查看ifconfig的帮助信息,单独输入help,则会显示所有的指令。另外,若不特殊说明,Shell内置命令的参数中的数值使用十六进制,且不区分大小写。

 

ver

展示shell的版本号

reconnect -r 

重新连接EFI驱动

alias

显示,创建,删除别名

dmem

显示内存目录

connect

连接驱动程序和设备

cp 

复制文件

date 

显示当前或者系统的日期

devices 

显示EFI驱动程序管理的设备

devtree

显示设备树

dh

显示设备句柄

disconnect 

断开驱动程序与设备的连接

dmpstore

管理UEFI NVRAM变量

drivers

显示设备驱动

drvcfg

调用驱动程序配置协议

drvdiag

调用驱动程序诊断协议

echo

关闭或者打开回显

edit

能够打开文件并且编辑它

eficompress

压缩文件

efidecompress

解压文件

hexedit

用十六进制编辑文件,块设备或者内存区域

ls

列出目录内容或者文件信息

map

显示挂载的磁盘

mem

显示memory信息

mode 

显示控制台输出设备的模样

mount 

在块设备上挂载文件

mv

移动一个文件从一个地方到另一个地方

touch

更新当前时间更新文件或者目录的文件

pci

显示PCI设备

rm 

删除文件

stall

停止处理器几微秒

 

4.2运行测试程序

编写HelloWorld程序。编译为.efi文件,然后在模拟环境中运行。

搭建好uefi开发环境之后,在MyWorkspace文件夹中建立一个文件夹ExamplePkg; ,然后在ExamplePkg文件夹中创建HelloWorld文件夹,Include文件夹,ExamplePkg.dec文件,ExamplePkg.dsc文件,buildx86.bat文件。

.dec文件中内容为:

 

[Defines]

  DEC_SPECIFICATION            = 0x00010006

  PACKAGE_NAME                   = ExamplePkg

  PACKAGE_GUID                    = A0D78D6-2CAF-414b-BD4D-B6762A894289

  PACKAGE_VERSION              = 1.01

 

[Includes]

  Include

 

[LibraryClasses]

 

.dsc文件中内容为:

 

[Defines]

  PLATFORM_NAME                        = Example

  PLATFORM_GUID                      = 587CE499-6CBE-43cd-94E2-186218569479

  PLATFORM_VERSION                     = 1.01

  DSC_SPECIFICATION                    = 0x00010006

  OUTPUT_DIRECTORY                     = Build/Example

  SUPPORTED_ARCHITECTURES    = IA32|IPF|X64|EBC|ARM|AARCH64

  BUILD_TARGETS                         = DEBUG|RELEASE

  SKUID_IDENTIFIER                      = DEFAULT

 

[LibraryClasses]

  UefiApplicationEntryPoint|MdePkg/Library/UefiApplicationEntryPoint/UefiApplicationEntryPoint.inf

  UefiLib|MdePkg/Library/UefiLib/UefiLib.inf 

  UefiBootServicesTableLib|MdePkg/Library/UefiBootServicesTableLib/UefiBootServicesTableLib.inf

  DebugLib|MdePkg/Library/UefiDebugLibStdErr/UefiDebugLibStdErr.inf

  BaseLib|MdePkg/Library/BaseLib/BaseLib.inf

  BaseMemoryLib|MdePkg/Library/BaseMemoryLib/BaseMemoryLib.inf

  DebugPrintErrorLevelLib|MdePkg/Library/BaseDebugPrintErrorLevelLib/BaseDebugPrintErrorLevelLib.inf

  MemoryAllocationLib|MdePkg/Library/UefiMemoryAllocationLib/UefiMemoryAllocationLib.inf

  DevicePathLib|MdePkg/Library/UefiDevicePathLib/UefiDevicePathLib.inf

  PrintLib|MdePkg/Library/BasePrintLib/BasePrintLib.inf

  PcdLib|MdePkg/Library/BasePcdLibNull/BasePcdLibNull.inf

  UefiRuntimeServicesTableLib|MdePkg/Library/UefiRuntimeServicesTableLib/UefiRuntimeServicesTableLib.inf

 

[Components]

  ExamplePkg/HelloWorld/HelloWorld.inf

.bat文件中内容为:

 

@call "C:\MyWorkSpace\edksetup.bat"

Build -t VS2015x86 -a IA32 -p ExamplePkg\ExamplePkg.dsc -m ExamplePkg\HelloWorld\HelloWorld.inf -b RELEASE

pause

 

HelloWorld文件夹中要两个文件,HelloWorld.c  HelloWorld.inf

.c文件中内容为

 

#include <Uefi.h>

#include <Library/UefiLib.h>

#include <Library/UefiApplicationEntryPoint.h>

 

EFI_STATUS EFIAPI UefiMain(

  IN EFI_HANDLE        ImageHandle,

  IN EFI_SYSTEM_TABLE  *SystemTable

)

{

_asm int 3

 Print(L"*************");

 Print(L"*HelloWorld*");

 Print(L"*************");

 return EFI_SUCCESS;

}

.inf文件中内容为

 

[Defines]

 INF_VERSION              = 0x00010006

  BASE_NAME               = HelloWorld

  FILE_GUID                  = 6987936E-ED34-44db-AE97-1FA5E4ED2117

  MODULE_TYPE             = UEFI_APPLICATION

  VERSION_STRING          = 1.01

  ENTRY_POINT             = UefiMain

 

[Sources]

  HelloWorld.c

 

[Packages]

  MdePkg/MdePkg.dec

  ExamplePkg/ExamplePkg.dec

 

[LibraryClasses]

  UefiApplicationEntryPoint

  UefiLib

 

因为要做的事情很简单只是打印hellowold所以Include文件夹中不需要创建文件。

其目录形式为:

ExamplePkg-->HelloWorld-->HelloWorld.c

ExamplePkg-->HelloWorld-->HelloWorld.inf

ExamplePkg-->Include

ExamplePkg-->buildx86.bat

ExamplePkg-->ExamplePkg.dec

ExamplePkg-->ExamplePkg.dsc

 

然后执行buildx86.bat

在MyWorkspace\build\Example\RELEASE_VS2015x86\IA32文件夹中的HellowWorld.efi文件就是编译生成的.efi应用,将该文件放到MyWorkSpace\Build\NT32IA32\DEBUG_VS2015x86\IA32文件夹下。

打开同目录下的SecMain.exe,在Shell中输入”fs0:”选择盘符,再输入helloworld.efi,然后回车就可以看到输出了。

 

图12