Linux/GNU toolchain 环境下的UEFI开发和调试

Linux/GNU toolchain 环境下的UEFI开发和调试

随着UEFI的广泛使用和开源硬件平台的推广,Linux下如何开发和调试越来越重要,UEFI编译工具链对GCC和Clang的支持也越来越完备。现在,GCC编译结果无论从生成的Image大小和执行效率都和Visual Studio编译结果相差无几,而GCC的免费和开源无疑使之更具魅力,也成为了许多项目尤其是面向Maker的项目必须要支持的编译工具链。

因为历史沿袭和实际市场需求的缘故, Windows及微软toolchain ,即Visual Studio C编译器,WinDDK和微软 ACPI asl 编译器等,曾经是UEFI固件开发的主要工作平台。但多年前EdkII发端之时,多平台多编译器的支持从一开始就纳入了架构,设计和实现中。现在EdkII的基本工具,所有核心Package均可在多个平台上编译,简要的UEFI模拟环境也提供了Windows和Linux版。厂商,社区和个人开发者能在自己偏好的环境里,方便地进行UEFI开发。不同操作系统里的独特功能和工具也为UEFI开发提供了很多启发和创新。这次专门说说Linux和GNU toolchain,包括环境设置,构建,调试方法和模拟器使用。

严格地说,GNU toolchain并不限于在Linux上使用,其他POSIX系统和Windows也能使用,但这次我们只说最原生的方法,即Linux + GNU toolchain。

内容安排用试错的方法推进,逐步完成开发。从无预设的基本Linux环境开始,按照“碰到错误-》解释-》解决-》继续”的步骤循环。

实例环境是Ubuntu 14.04 Server,安装时的额外包选项只选择了OpenSSH server。假定读者了解基本的Linux使用和Ubuntu的包管理。截至2017年3月,Ubuntu16.04 + GCC5的环境可以顺利构建多数EdkII 的package,但是运行起来尚不保证功能完备。

环境设置

首先从EdkII官方github地址tianocore/edk2下载source tree。

EdkII基本工具包括build程序,Flash FD,DSC文件处理,缺省配置项管理等等涉及UEFI或者EdkII 专门概念的专门工具,代码放在BaseTools目录下的。因为是基本工具,需要先行构建。

对一个给定的tree,这些工具只需构建一次。如果他们的可执行文件被删除,或者在新tree里开始工作,那么这个步骤需要重新执行一次。

make -C BaseTools

第一个错误应该会是make未找到,使用Ubuntu提供的包即可。安装make后再次运行 make -C BaseTools,后面还会有几次类似的“Command not found”的错误,原因和解决方法也类似。

总结下来,在缺省安装的Ubuntu 14.04上,如下包是构建BaseTools必须的,以后可以一次安装完毕。

apt-get install make gcc uuid-dev g++

现代发行版都会自动判断依赖关系,自动提示/安装关联的package,就不列出了。

扫描二维码关注公众号,回复: 11941912 查看本文章

make, gcc, g++ 都是广泛使用的构建和编译工具,为EdkII工具使用也无特别之处。 uuid是EdkII工具里的GenFv需要的,解决编译它时“GenFvInternalLib.c:24:23: fatal error: uuid/uuid.h: No such file or directory ”错误的。了解了这些OS package需求的来龙去脉,那即便换做其他发行版,打包名字不同也可以应对了。

然后设置EdkII构建环境:

source ./edksetup.sh

在当前shell上下文中,执行edksetup.sh。这个步骤主要完成workspace的设置,包括环境变量,查找路径等。如果开启了新的终端,这一步还需要执行一次。

在其他EdkII文档或指南中,此步骤也常做“. edksetup.sh”。注意第一个“点”和其后的空格,目前看来,在引导性文档中,这个步骤使用点操作符这种标注值得商榷,因为不够显著,有时甚至会带来混乱。我们的实例使用Ubuntu,缺省shell为bash,所以如果不考虑不同发行版差异和不同shell实现的话,点操作符和source命令功能一样,可认为互为同义词。

对构建目标的选择也是在这一步完成的,edksetup.sh脚本当然是不知道我们期望的构建目标的,只是生成一个模板供参考。所以打开生成的构建目标配置文件加以修改:

nano Conf/target.txt

在这次的实例里,我们打算构建EFI Shell,它的实现放在ShellPkg目录,所以target.txt 里按下面内容修改:

ACTIVE_PLATFORM        = ShellPkg/ShellPkg.dsc
TARGET_ARCH            = IA32 X64
TOOL_CHAIN_TAG         = GCC48

其他内容可以保持不变。

目前主流x86平台平台均支持64位所以我们在TARGET_ARCH中加入X64。

在Conf/tools_def.txt里,有EdkII支持的所有toolchain的详细信息。每个toolchain由一个关键字表示,比如VS2015, GCC45, CLANG38等,TOOL_CHAIN_TAG的赋值只能是这些关键字中的一个。如果开发者需要支持一个新的toolchain,则至少需要修改BaseTools/Conf里的build_rule.template和tools_def.template,他们会在edksetup步骤中被复制到Conf目录。

目前EdkII支持的GCC版本从4.4到5.x,对应的toolchain关键字是GCC44, GCC45, … GCC5。实例平台Ubuntu 14.04所带gcc版本为4.8,所以TOOL_CHAIN_TAG选择GCC48。

在target.txt设置好构建目标后,环境设置就完成了。

构建

使用EdkII工具‘build’开始构建

build

此工具即为BaseTools构建出来的,edksetup.sh设置过查找路径因而可以直接使用。

和之前设置环境中的情况类似,也会碰到工具找不到的错误,比如nasm。解决方法亦类似,安装Ubuntu提供的nasm包即可。

安装完后继续,build会成功完成。按照EdkII开发的惯例,最终shell.efi文件放在Build目录ShellPkg对应的目录里。

build命令从上一步提到的target.txt读取设置,也可以让build忽略target.txt中的内容,直接给build 命令传递参数,指定构建目标。

build -t GCC48 -a IA32 -a X64 -p ShellPkg/ShellPkg.dsc -b 

此命令会忽略target.txt中的内容,按照传递的参数工作。当然此处给的参数使build结果与使用target.txt一样。

实际的项目开发中通常会涉及更多工程细节,为了照顾各种专门需要并方便工程师工作,实际上会使用各种自动化脚本/工具作为主干,他们调用前述的basetool检查/构建,edksetup和build工具,这时候给build工具传递参数而不是使用target.txt就比较方便。

调试

调试以Intel Target Probe (ITP)工具为例,解释如何搭建环境用ITP在真实硬件上实现gcc编译结果的符号调试,对象是前面构建的shell.efi。

ITP是Intel的调试器硬件套装,支持从Pre-Silicon到OS到应用各层次的软件调试。除了可以用于UEFI程序,还可以调试Linux内核和VxWorks,完备支持各代Intel架构处理器的功能特性,寄存器,多处理器等。

套装的软件侧,提供两个调试器前端, Platform Debug Toolkit和Intel Source Debugger。Intel Source Debugger使用较为方便,就以这个作为本节实例。后面用ITP泛指Intel Source Debugger或套装。

目前UEFI开发者要使用ITP对GNU toolchain编译结果做符号调试,还需要再做一些工作。

1。调试信息格式

截至ITP版本16,其只支持DWARF格式version 3,而gcc 4.8是按照DWARF version 4生成调试信息的,所以需要修改EdkII gcc编译选项让其生成DWARF 3格式的调试信息。

修改toolchain配置模板文件

nano BaseTools/Conf/tools_def.template

找到对应GCC48的选项 DEBUG_GCC48_IA32_CC_FLAGS和DEBUG_GCC48_X64_CC_FLAGS,修改为以下内容:

DEBUG_GCC48_IA32_CC_FLAGS = DEF(GCC48_IA32_CC_FLAGS) -Os -gdwarf-3
DEBUG_GCC48_X64_CC_FLAGS  = DEF(GCC48_X64_CC_FLAGS) -Os -gdwarf-3

添加的部分为红色的“-gdwarf-3”,指示gcc按照DWARF version 3的格式生成调试信息。

此处修改的文件为BaseTools Conf模板,通常不推荐修改Conf模板文件。但因为我们需要修改全局设置,保证-gdwarf3在即便清空目录下也能起效,故修改此处。

对Shell的入口做简单修改,使其等待。此步骤是为方便这里的演示,不是必须的。

 DEBUG((EFI_D_ERROR, "shell waiting...\n"));
  CpuDeadLoop();

之后重新构建Shell。推荐清除已有的Build目录做clean build。

rm –rf Build
rm Conf/*
source ./edksetup.sh
build -t GCC48 -a IA32 -a X64 -p ShellPkg/ShellPkg.dsc -b DEBUG

注意上一步不要删除Conf目录,只删除其中的文件。因为我们修改了模板文件,edksetup这一步无论是否在在新的终端都不能省略,这样修改过的模板会被复制到工作目录下的Conf目录,build时引用的gcc 选项就会是我们修改过的新选项。

2。目录和二进制文件

Intel Source Debugger目前只有Windows版,所以调试前需要设置一下环境。

我们称完成gcc build的Linux机器为电脑A,运行ITP的Windows电脑为B。

ITP需要A上构建出来的Build目录下的符号文件,以及用来显示的源文件。故可以设置共享,或者将Linux机器A上的工作目录完整地复制到运行ITP的B电脑上(或者ITP能访问的其他Windows机器上再行共享),包括EdkII源代码和生成的Build目录。

任何A与B电脑上最终binary或者源文件的不同步,都会在调试时带来混乱,请一定注意每次build后,同步文件之后再开始用ITP调试。

3。使用ITP

按照通常步骤启动连接ITP并halt下来,现在Shell应该在CpuDeadLoop()处循环等待。

因调试信息中记录的所有目录文件信息都是Linux A机器的,需要将其映射到Windows B机器上已同步的工作目录。

在ITP中如下设置:

本例A机上的工作目录为 /home/bowen/wechat/edk2,复制到B机器的 e:\a\wechat。ITP会自行处理目录分隔符,无需担心此处的Windows格式的斜杠。

之后使用通常的方法“loadthis”尝试查看CPU此刻在执行的代码,会看到如下消息:

xdb> efi loadthis
INFO: Searching backwards from 0x00000000BAFAA302 to 0x00000000BAEAA302 for PE/COFF header (semantics=MEM range=0x00100000)
INFO: Found PE/COFF module at 0x00000000BAF97000 - 0x00000000BB08A220 Entrypoint: 0x00000000BAF97240 (size: 995872 bytes)
ERROR: Unknown error when executing load command, issue the following command manually to get more error info:
LOAD /NOLOAD /OFFSET = 0xbaf97000 OF "E:\a\wechat\Build\Shell\DEBUG_GCC48\X64\ShellPkg\Application\Shell\Shell\DEBUG\Shell.efi"

可以看到,ITP知晓应该到映射过的B机器目录中尝试寻找并加载符号了,但EdkII的build过程并未将信息放在Shell.efi中,需要再执行一条命令。

复制上面消息的最后一行,但是修改最后的文件扩展名为debug,修改后将其作为命令执行:

LOAD /NOLOAD /OFFSET = 0xbaf97000 OF "E:\a\wechat\Build\Shell\DEBUG_GCC48\X64\ShellPkg\Application\Shell\Shell\DEBUG\Shell.debug"

上面修改的部分为蓝色的debug。意即让ITP从上述目录加载Shell.debug文件。执行后即有:

ITP成功加载符号并显示对应的源代码。此后的调试和任何UEFI或者说软件的调试就没有本质区别了。

4。解释

对符号文件的生成/放置,最好的理解方法是查看build.log或者EdkII build的配置文件。看看编译器/工具按哪些设置做了什么事情。

查看build过程的配置

nano BaseTools/Conf/build_rule.template

查找[Dynamic-Library-File]一节下面的<Command.GCC, Command.GCCLD>部分。

简要地说,下面两行完成的事情是将目标文件复制为.debug文件,之后再复制其到对应module的目录下:

$(CP) ${src} $(DEBUG_DIR)(+)$(MODULE_NAME).debug
#省略部分
-$(CP) $(DEBUG_DIR)(+)$(MODULE_NAME).debug $(BIN_DIR)(+)$(MODULE_NAME_GUID).debug

以上让.debug文件成为符号调试信息的载体,ITP就可以使用了。

因为现在是演示的缘故,我们的宗旨是以尽可能少的文件改动,来完成要演示的功能。其实Dynamic-Library-File这一节里的其他语句还有优化的空间,比如让build过程更省时一些,也可以让ITP从别的文件加载调试信息,这些留给感兴趣的诸位自行探索了。

模拟运行

模拟器是一种快速便捷的调试验证手段。Windows上有NT32,Linux也可以使用OVMF加qemu。OVMF 需要和模拟器/虚拟机配合使用。

首先构建OVMF。

build -t GCC48 -a IA32 -a X64 -p OvmfPkg/OvmfPkgIa32X64.dsc

这个dsc的选择是为了对应实际平台中最常见的情形:PEI是32位,DXE是64位的实际情况。欢迎诸位自行尝试另外两个OVMF dsc的构建。构建过程中如果缺少iasl编译器,安装发行版的iasl包即可。

OVMF的构建结果是ovmf.fd,这就是供qemu使用的BIOS。缺省情况下放在Build/Ovmf3264/DEBUG_GCC48/FV/ OVMF.fd。

启动qemu,提供bios参数为刚才构建的OVMF.fd

qemu-system-x86_64 --bios Build/Ovmf3264/DEBUG_GCC48/FV/OVMF.fd -nographic

因为我们没有提供虚拟磁盘,也是缺省固件配置,系统启动后,会开始尝试网络启动(iPXE boot),因为这是缺省的第一个启动选项,可以用 Ctrl + C 停止网络启动尝试,回到efi shell。如果网络启动不成功,还是会回到efi shell。

如果不需要模拟网络,可以给qemu加参数“-net none”,这样会直接启动到efi shell。

回到setup:

根据开发者主机配置,启动事件可能在几秒到几十秒不等。

在目前的不少文档中,启动qemu都没有加“-nographic”参数,这是假定开发者登陆GUI环境,有X存在,这个假定有点宽泛。搭建一台Linux机器A后,从别的Linux机器使用ssh或者从Windows使用Putty登陆机器A也是很通常的实际情况,在这种情况下从ssh终端启动qemu因为不能提供X,会碰到“Could not initialize SDL(No available video device) - exiting”错误,无法启动。

所以上面的命令用了-nographic参数,适合在终端(比如SSH, Ctrl+Fx)而不是GUI环境的终端模拟器下工作的情况。当然,任何和图形相关的行为就不会出现了,比如启动logo。

从当前qemu退出需要另外启动终端连接,用ps找到qemu的PID,然后kill PID即可。

对不需要操作真实硬件的UEFI程序,qemu是非常方便快捷的目标平台。

后记

如何将一个在Visual Studio下编译运行良好的UEFI工程转化为GCC工程呢?简单来说分下面几个步骤:

  1. TOOL_CHAIN_TAG改变为GCCxx
  2. 将所有宏汇编程序转换为AT&T语法汇编或者nasm汇编。
  3. 检查函数调用约定(calling convention ),确保声明和定义一致。特别注意带有EFI_API的接口。
  4. Prebuild和Postbuild的tool也要转化成Linux版(或用wine)。
  5. *.bat文件转化为*.sh文件
  6. 消除所有发现的错误和Warning。

接下来就是上机调试了,到了这里万里长征终于走了一半。接下来还会面对不少问题,尤其2)和3)中的错误和粗心会耗费大量时间,希望大家能善用“调试”部分的内容,助力快速找到并改正错误。

猜你喜欢

转载自blog.csdn.net/f2157120/article/details/108289938