二进制FD生成过程推演
1.obj二级制目标文件的生成
每个带有inf文件的模块,Build时都会先生成一个对应的GNUMakefile文件,里面定义了该模块的Build规则。我们以ArmPkg/Drivers/CpuDxe模块为例。通过其GNUMakefile分析二进制文件生成规则。
1.1.1.obj的生成规则
模块的每个源文件都会编译生成一个xx.obj目标文件。
在执行完build后,每个模块对应的输出目录下会有GnuMakefile文件,从中我们可以看到模块的xx.obj目标文件构建规则。
主要是基于gcc编译器。
1.1.2. 编译工具和Flag
我们看下GnuMakefile中的定义:
生成了各个未经链接的二级制目标文件。Flag中使用了“-fno-pic”,看来是要生成位置无关码。
1.1.3 编译规则的定义
定义在:BaseTools/Conf/build_rule.template#149文件。
125 <OutputFile>
126 $(OUTPUT_DIR)(+)${s_dir}(+)${s_base}.obj
127
128 <Command.MSFT, Command.INTEL>
129 "$(CC)" /Fo${dst} $(DEPS_FLAGS) $(CC_FLAGS) $(INC) ${src}
130
131 <Command.GCC, Command.RVCT>
132 # For RVCTCYGWIN CC_FLAGS must be first to work around pathing issues
133 "$(CC)" $(DEPS_FLAGS) $(CC_FLAGS) -c -o ${dst} $(INC) ${src}
134
135 <Command.XCODE>
136 "$(CC)" $(DEPS_FLAGS) $(CC_FLAGS) -o ${dst} $(INC) ${src}
137
138 [C-Code-File.BASE.AARCH64,C-Code-File.SEC.AARCH64,C-Code-File.PEI_CORE.AARCH64,C-Code-File.PEIM.AARCH64,C-Code-File.BASE.ARM,C-Code-File.SEC.ARM,C-Code-File.PEI_CORE.ARM,C-Code-File.PEIM.ARM]
139 <InputFile>
140 ?.c
141
142 <ExtraDependency>
143 $(MAKE_FILE)
144
145 <OutputFile>
146 $(OUTPUT_DIR)(+)${s_dir}(+)${s_base}.obj
147
148 <Command.GCC, Command.RVCT>
149 "$(CC)" $(CC_FLAGS) $(CC_XIPFLAGS) -c -o ${dst} $(INC) ${src}
这里就定义了obj文件的生成规则,使用命令“ “$(CC)” $(CC_FLAGS) $(CC_XIPFLAGS) -c -o ${dst} $(INC) ${src}”
我们这里$(CC)使用的是gcc-4.8,看下flag的定义。
在文件:BaseTools/Conf/tools_def.template
1903 DEFINE GCC48_ALL_CC_FLAGS = DEF(GCC_ALL_CC_FLAGS) -ffunction-sections -fdata-sections -DSTRING_ARRAY_NAME=$(BASE_NAME)Strings
...
1912 DEFINE GCC48_ASM_FLAGS = DEF(GCC_ASM_FLAGS)
1913 DEFINE GCC48_ARM_ASM_FLAGS = $(ARCHASM_FLAGS) $(PLATFORM_FLAGS) DEF(GCC_ASM_FLAGS) -mlittle-endian
1914 DEFINE GCC48_AARCH64_ASM_FLAGS = $(ARCHASM_FLAGS) $(PLATFORM_FLAGS) DEF(GCC_ASM_FLAGS) -mlittle-endian
1915 DEFINE GCC48_ARM_CC_FLAGS = $(ARCHCC_FLAGS) $(PLATFORM_FLAGS) DEF(GCC_ARM_CC_FLAGS) -fstack-protector -mword-relocations
1916 DEFINE GCC48_ARM_CC_XIPFLAGS = DEF(GCC_ARM_CC_XIPFLAGS)
1917 DEFINE GCC48_AARCH64_CC_FLAGS = $(ARCHCC_FLAGS) $(PLATFORM_FLAGS) -mcmodel=large DEF(GCC_AARCH64_CC_FLAGS)
1918 DEFINE GCC48_AARCH64_CC_XIPFLAGS = DEF(GCC_AARCH64_CC_XIPFLAGS)
这些编译和链接选项也可以在INF文件或dsc文件的[BuildOptions]中设置。会对此默认模板的配置覆盖。
28 [BuildOptions]
29 RELEASE_*_*_CC_FLAGS = -D MDEPKG_NDEBUG
30 *_*_*_CC_FLAGS = -D DISABLE_NEW_DEPRECATED_INTERFACES
31
2 dll静态库文件的生成
xx.dll 文件是所有obj文件链接成的静态库文件,也是elf可执行文件的一种。注意:xx.dll文件实际上是ELF格式的文件,它带有调试信息,它是静态库文件。
我们经常使用readelf和objdump 工具去解析它,获取一些信息。当程序运行panic后,dump出栈帧之后,我们也利用addr2line工具从dll文件获取执行失败的offset对应的函数名称。
还是看模块的GnuMakefile。
2.1 xx.dll 可执行文件的生成规则
从多个*.obj到xx.dll文件的过程,经过了三步:
- 先将
*.obj
处理成静态库文件*.lib
。 - 采用$(DLINK)表示的链接器链接静态库文件
*.lib
,输出xx.dll。 - 采用$(OBJCOPY) 工具对xx.dll文件进行处理,去除了一些非必须的段。输出到xx.dll。
2.2 DLINK链接处理
看下链接选项:
这里指定了链接脚本:
-W1,--script=BaseTools/Scripts/GccBase.lds
我们看下链接脚本的定义:
/*BaseTools/Scripts/GccBase.lds*/
13 SECTIONS {
14
15 /*
16 * The PE/COFF binary consists of DOS and PE/COFF headers, and a sequence of
17 * section headers adding up to PECOFF_HEADER_SIZE bytes (which differs
18 * between 32-bit and 64-bit builds). The actual start of the .text section
19 * will be rounded up based on its actual alignment.
20 */
21 . = PECOFF_HEADER_SIZE;
22
23 .text : ALIGN(CONSTANT(COMMONPAGESIZE)) {
24 *(.text .text.* .stub .gnu.linkonce.t.*)
25 *(.rodata .rodata.* .gnu.linkonce.r.*)
26 *(.got .got.*)
27
28 /*
29 * The contents of AutoGen.c files are mostly constant from the POV of the
30 * program, but most of it ends up in .data or .bss by default since few of
31 * the variable definitions that get emitted are declared as CONST.
32 * Unfortunately, we cannot pull it into the .text section entirely, since
33 * patchable PCDs are also emitted here, but we can at least move all of the
34 * emitted GUIDs here.
35 */
36 *:AutoGen.obj(.data.g*Guid)
37 }
38
39 /*
40 * The alignment of the .data section should be less than or equal to the
41 * alignment of the .text section. This ensures that the relative offset
42 * between these sections is the same in the ELF and the PE/COFF versions of
43 * this binary.
44 */
45 .data ALIGN(ALIGNOF(.text)) : ALIGN(CONSTANT(COMMONPAGESIZE)) {
46 *(.data .data.* .gnu.linkonce.d.*)
47 *(.bss .bss.*)
48 }
49
50 .eh_frame ALIGN(CONSTANT(COMMONPAGESIZE)) : {
51 KEEP (*(.eh_frame))
52 }
53
54 .rela (INFO) : {
55 *(.rela .rela.*)
56 }
57
58 .hii : ALIGN(CONSTANT(COMMONPAGESIZE)) {
59 KEEP (*(.hii))
60 }
61
62 /*
63 * Retain the GNU build id but in a non-allocatable section so GenFw
64 * does not copy it into the PE/COFF image.
65 */
66 .build-id (INFO) : { *(.note.gnu.build-id) }
67
68 /DISCARD/ : {
69 *(.note.GNU-stack)
70 *(.gnu_debuglink)
71 *(.interp)
72 *(.dynsym)
73 *(.dynstr)
74 *(.dynamic)
75 *(.hash .gnu.hash)
76 *(.comment)
77 }
78 }
79
看来,这里在链接时使用 /DISCARD/ strip掉了*(.comment)
、*(.dynstr)
、 *(.dynsym)
等不需要的section。但是.strtab、.symtab、*.debug
等段还在dll文件中。这点我们可以通过readelf 分析xx.dll来证实。
注意:依然这里已经strip掉了不需要的调试信息,那么$(OBJCOPY) 就不需要再去strip了吧。
2.3 DLINK链接规则的定义
还是在BaseTools/Conf/build_rule.template文件。
静态库文件的xx.dll的生成。
289 [Static-Library-File]
290 <InputFile>
291 *.lib
292
293 <ExtraDependency>
294 $(MAKE_FILE)
295
296 <OutputFile>
297 $(DEBUG_DIR)(+)$(MODULE_NAME).dll
298
299 <Command.MSFT, Command.INTEL>
300 "$(DLINK)" /OUT:${dst} $(DLINK_FLAGS) $(DLINK2_FLAGS) $(DLINK_SPATH) @$(STATIC_LIBRARY_FILES_LIST)
301 "$(DLINK)" /OUT:${dst} $(DLINK_FLAGS) $(DLINK_SPATH) @$(STATIC_LIBRARY_FILES_LIST)
302
303 <Command.CLANGPDB>
304 "$(DLINK)" /OUT:${dst} $(DLINK_FLAGS) $(DLINK_SPATH) @$(STATIC_LIBRARY_FILES_LIST) $(DLINK2_FLAGS)
305
306 <Command.GCC>
307 "$(DLINK)" -o ${dst} $(DLINK_FLAGS) -Wl,--start-group,@$(STATIC_LIBRARY_FILES_LIST),--end-group $(CC_FLAGS) $(DLINK2_FLAGS)
308 "$(OBJCOPY)" $(OBJCOPY_FLAGS) ${dst}
309
310 <Command.RVCT>
311 "$(DLINK)" $(DLINK_FLAGS) -o ${dst} $(DLINK_SPATH) --via $(STATIC_LIBRARY_FILES_LIST) $(DLINK2_FLAGS)
312
313 <Command.RVCTCYGWIN>
314 #$(STATIC_LIBRARY_FILES_LIST) has wrong paths for cygwin
315 "$(DLINK)" $(DLINK_FLAGS) -o ${dst} $(DLINK_SPATH) $(STATIC_LIBRARY_FILES) $(DLINK2_FLAGS)
316
317 <Command.XCODE>
318 "$(DLINK)" $(DLINK_FLAGS) -o ${dst} $(DLINK_SPATH) -filelist $(STATIC_LIBRARY_FILES_LIST) $(DLINK2_FLAGS)
PEI, DXE,和Application, 有不同的生成规则。
我们看下DLINK_FLAGS和DLINK2_FLAGS的定义,在BaseTools/Conf/tools_def.template
#BaseTools/Conf/tools_def.template
1919 DEFINE GCC48_ARM_DLINK_FLAGS = DEF(GCC_ARM_DLINK_FLAGS) -Wl,--oformat=elf32-littlearm
1920 DEFINE GCC48_ARM_DLINK2_FLAGS = DEF(GCC_DLINK2_FLAGS_COMMON) -Wl,--defsym=PECOFF_HEADER_SIZE=0x220
1921 DEFINE GCC48_AARCH64_DLINK_FLAGS = DEF(GCC_AARCH64_DLINK_FLAGS)
1922 DEFINE GCC48_AARCH64_DLINK2_FLAGS = DEF(GCC_DLINK2_FLAGS_COMMON) -Wl,--defsym=PECOFF_HEADER_SIZE=0x228
1923 DEFINE GCC48_ARM_ASLDLINK_FLAGS = DEF(GCC_ARM_ASLDLINK_FLAGS) -Wl,--oformat=elf32-littlearm
1924 DEFINE GCC48_AARCH64_ASLDLINK_FLAGS = DEF(GCC_AARCH64_ASLDLINK_FLAGS)
1925 DEFINE GCC48_ASLCC_FLAGS = DEF(GCC_ASLCC_FLAGS)
同样,支持在模块的INF文件和pkg的dsc文文件通过 [BuildOptions] 块修改。
137 # Force PE/COFF sections to be aligned at 4KB boundaries to support NX protection
138 [BuildOptions.common.EDKII.DXE_DRIVER, BuildOptions.common.EDKII.DXE_CORE, BuildOptions.common.EDKII.UEFI_DRIVER, BuildOptions.common.EDKII.UEFI_APPLICATION]
139 #MSFT:*_*_*_DLINK_FLAGS = /ALIGN:4096
140 #GCC:*_*_*_DLINK_FLAGS = -z common-page-size=0x1000
141
2.4 objcopy对xx.dll文件处理
前面我们介绍xx.dll文件生成规则时,xx.dll可执行文件带有很多调试信息,虽然DISCARD了一些,但是还有.debug、.symtab、.strtab等段,占据很大Size。objcopy工具对生成的xx.dll可执行文件再处理一次,再strip一些段。
我们从GNUMakefile看下$(OBJCOPY) 的定义。
看来只是用“echo”打印了下,并没有使用aarch64-linux-gnu-objcopy工具来进行处理。如果是在ELF目标文件中,会采用如下命令来去除掉调试信息,只保留必要的段。
aarch64-linux-gnu-objcopy --strip-unneeded -R .eh_frame xx.dll
实际上,通过readelf 解析xx.dll证明,它的符号信息(debug、.symtab、.strtab等)还在xx.dll中。没有使用objcopy工具strip调试信息。
我们看下BaseTools/Conf/tools_def.template中的定义:
1854 ####################################################################################
1855 # GCC Common
1856 ####################################################################################
1857
1858 *_*_*_OBJCOPY_PATH = echo
1859 *_*_*_OBJCOPY_FLAGS = objcopy not needed for
1860 *_*_*_SYMRENAME_PATH = echo
1861 *_*_*_SYMRENAME_FLAGS = Symbol renaming not needed for
1862 DEBUG_*_*_OBJCOPY_ADDDEBUGFLAG = --add-gnu-debuglink=$(DEBUG_DIR)/$(MODULE_NAME).debug
1863 RELEASE_*_*_OBJCOPY_ADDDEBUGFLAG =
1864 NOOPT_*_*_OBJCOPY_ADDDEBUGFLAG = --add-gnu-debuglink=$(DEBUG_DIR)/$(MODULE_NAME).debug
可见,这里并没有真正使用objcopy工具。
3 动态库 xx.efi 文件的生成
xx.efi被认为是一个动态库,查看它的头,是一个“MD-DOS”可执行文件。它的内容实际上是PE32, TE(精简版PE), PE32+中的一种。
3.1 efi文件的生成规则
继续看GnuMakefile中的生成规则:
无非就是经过如下几个处理步骤:
- 使用objcopy --strip-unneeded 处理xx.dll文件,strip掉不必须的段。实际上并没有做。
- 使用objcopy工具,添加回一些$(OBJCOPY_ADDDEBUGFLAG)指定的debug相关的段。实际上没有做。
- 调用GenFw工具将xx.dll 静态库处理成动态库xx.efi。
这里重点是:GenFw转换工具对dll文件做了什么。
3.2 GenFw 转换工具
GnuMakefile中的定义:
GENFW_FLAGS =
GENFW = GenFw
MODULE_TYPE =DXE_DRIVER
命令展开就是:
GenFw -e DXE_DRIVER -o xx.efi xx.dll
最后,生成了两个文件:xx.efi和xx.txt。 xx.txt中记录了文件的Size和时间戳。
我们可以通过readpe命令来分析该:xx.efi文件。
$readpe -S xx.efi
通过readpe -S 查看该文件中保留了哪些段。
看到只剩下:.text(代码段)、.data(数据段)、.reloc(重定位表) 这几个必需的段,实际还有.bss段只是没数据,其余段没有了。
看来GenFw工具将静态库文件xx.dll ELF格式的文件变成MD_DOS格式的了。而且xx.efi中的段和xx.dll的段很不一样,它只有.text(代码段)、.data(数据段)、.reloc(重定位表) 这几个段。
那么生成的xx.efi到底是PE32、TE(精简版PE)还是PE32+格式的呢?
我们通过readpe -H命令读取所有的header看下。
readpe -H xx.efi
原来是PE32+格式的。
3.3 GenFw工具的定义
GenFw工具编译后生成在:BaseTools/Source/C/GenFw/bin下。
我们分析时将它导出到环境变量,就可以在任意位置使用该命令了。
export PATH=/.../BaseTools/Source/C/GenFw/bin:$PATH
GenFw工具的源码在:BaseTools/Source/C/GenFw
TODO:
GenFw工具对xx.dll文件究竟做了哪些处理和转换,有兴趣可以从BaseTools/Source/C/GenFw/GenFw.c文件main函数开始阅读。
3.3 efi文件中符号表去哪了
我们之前分析时,生成的静态库xx.dll中包含了.symtab、.strtab、.debug等调试段,但是经过GenFw工具处理后变成PE32+文件,看不到这些段了。难道是GenFw工具将这些段剔除了吗?
经过运行实践,并不是!字符串表.strtab就没有剔除,要不然就无法打印字符串常量。
-
通过“readpe -A”命令查看xx.efi文件的内容。
“Data directiories”,是个资源目录。表里面放着两类资源:
- IMAGE_DIRECTORY_ENTRY_BASERELOC, 重定位表, 存放它的虚拟地址Offset和Size
- IMAGE_DIRECTORY_ENTRY_DEDBUG, 调试信息。这应该也是一张表,这里放的是它的虚拟地址Offset和Size。
由此可见,efi文件种是带着调试信息的。但是怎么只有28字节呢?
-
通过“pestr -s” 命令,查看程序中各个段种的字符串。实际上就是dump 文件的字符串表示.strtab
该命令会显示xx.efi中所有字符串。
pestr -s xx.efi
会显示.text和.data段种都有很多字符串信息。使用“-o” 参数不仅显示字符串,还显示每个字符串在efi文件中的offset。
pestr -o xx.efi
发现只有字符串表.strtab中部分内容,没有符号表.symtab。
我们还是再看下原来的xx.dll文件。使用“objdump -h xx.dll”查看ELF文件的头信息(GenFw就是根据这里的头信息遍历ELF 段的,这里没有就不会处理)。竟然没有符串表.strtab和符号表.symtab的头,而使用“readelf -t ”能看到.strtab和.symtab的信息。
怀疑Gcc工具不支持。(实际上验证,确实如此。)无论怎么弄,ELF的header中都不会有.strtab和.symtab段。
objdump -h xx.dll结果:
readelf -t xx.dll结果:
如果Gcc工具链没有将.strtab和.symtab放在文件header表的话,GenFw应该不会把它们放在相应的debug表(IMAGE_DIRECTORY_ENTRY_DEDBUG)。怀疑使用GenFw工具将ELF格式转换成PE32格式的过程中会导致ELF中一些段的内容丢失。
查看GenFw源码:
///BaseTools/Source/C/GenFw/Elf64Convert.c
ScanSections64 (
861 //
862 // Make room for .debug data in .data (or .text if .data is empty) instead of
863 // putting it in a section of its own. This is explicitly allowed by the
864 // PE/COFF spec, and prevents bloat in the binary when using large values for
865 // section alignment.
866 //
867 if (SectionCount > 0) {
868 mDebugOffset = DebugRvaAlign(mCoffOffset);
869 }
870 mCoffOffset = mDebugOffset + sizeof(EFI_IMAGE_DEBUG_DIRECTORY_ENTRY) +
871 sizeof(EFI_IMAGE_DEBUG_CODEVIEW_NB10_ENTRY) +
872 strlen(mInImageName) + 1;
873
874 mCoffOffset = CoffAlign(mCoffOffset);
875 if (SectionCount == 0) {
876 mDataOffset = mCoffOffset;
877 }
878
通过注释可以看到:只是将.debug 段放在了.data或.text段,再PE32中不会开辟单独的段放这些特殊段的信息。
但是我们查看xx.dll文件,并没有名叫“.debug”的段。
3.4GenFw命令
GenFw工具的使用,自行查看使用方法。
GenFw --help