版权声明:本文为CSDN博主「ashimida@」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lidan113lidan/article/details/119789654更多内容可关注微信公众号
模块的编译流程在<Kbuild系统源码分析(二)—./Makefile>等文章中已有提及,这里主要梳理思路并记录之前分析中没有记录的一些细节,这里仅以arm64平台内部模块的编译,目标为modules(编译所有模块),CONFIG_MODULES=y,且CONFIG_MODVERSIONS=y的情况为例,后面提及的模块默认指的是这种情况下的模块. 模块的编译规则如下:
modules: $(vmlinux-dirs) $(if $(KBUILD_BUILTIN),vmlinux) modules.builtin
$(Q)$(AWK) '!x[$$0]++' $(vmlinux-dirs:%=$(objtree)/%/modules.order) > $(objtree)/modules.order
@$(kecho) ' Building modules, stage 2.';
$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost
$(Q)$(CONFIG_SHELL) $(srctree)/scripts/modules-check.sh
模块规则中所有的.o文件都是在依赖项$(vmlinux-dirs)中编译的,其记录的是参与编译的各个子目录,最终会通过make -f ./scripts/Makefile.build的方式到子目录中编译子目录Makefile中指定的所有obj-m等目标,进而生成模块的.o文件,并完成一些其他操作(直到Makefile.modpost执行前),这些步骤为模块编译时的stage1,之后规则命令中会调用 make -f ./scripts/Makefile.modpost来完成.o => .ko的生成,这一步称为stage2.
下面以模块编译时真正的执行顺序来分析模块编译的流程,这里只提取关键步骤(其中 A => B代表B是A的依赖,* C 代表A的命令中有C, -代表子项, =代表变量展开):
1. Stage1:
1.1.$(vmlinux-dirs)
|=> prepare
| => prepare-objtool arm64平台没有重要的objtool,可以忽略
| => prepare0
| |=> archprepare
| | => archheaders //平台相关头文件,arm64为空
| | => archscripts //平台相关工具,arm平台为空,x86有relocs
| | => prepare1
| | * 创建并删除$(MODVERDIR) ,默认是 ./tmp_versions, 此目录存放了一堆文本文件 *.mod,记录编译阶段所有有export符号的模块名,
| | 后续会通过modpost批量处理
| | => scripts
| | => scripts/basic目录的编译,主要编译出fixdep工具,此工具的作用是对gcc输出的依赖项做一些整理,让其不要太冗长
| | => scripts/dtc目录的编译
| | * $(Q)$(MAKE) $(build)=scripts //这里默认编译的没有包含上面两个目录,编译的工具包括(都是host端的)
| | - bin2c
| | - kallsyms
| | - pnmtolo
| | - conmakehash
| | - recordmcount
| | - sortextable
| | - asn1_compiler
| | - sign-file //此工具用于给模块签名
| | - extract-cert //此工具用于从pem文件中提取签名
| | - insert-sys-cert
| | - build_unifdef
| | - gcc-plugins/ //子目录,gcc的plugin目标是加到这里的
| | - genksyms/ //子目录,目前只包括genksyms一个工具
| | - selinux/ //子目录,目前包括mdp genheaders两个工具
| |* $(MAKE) $(build)=scripts/mod //编译出modpost工具
| |* $(Q)$(MAKE) $(build)=. //时钟等相关目标,pass
|* $(Q)$(MAKE) $(build)=$@ need-builtin=1
$(vmlinux-dirs)中的目录是依次构建的,对于每个子目录都执行以上的命令,假设某次编译子目录subdir,实际上是执行了
make -f ./scripts/Makefile.build obj=subdir need-builtin=1
在./scripts/Makefile.build中引入了subdir下的Kbuild/Makefile文件,然后解析其中的obj-y/m,lib-y/m,always,extra-y,subdir-ym等设置,最后执行默认编译:
__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \
$(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \
$(subdir-ym) $(always)
@:
这里仅讨论对于模块obj-m中目标的编译,用户输入的obj-m一共存在三种情况,在./Makefile.build中分别对应三种规则:
* 单目标:
用户输入的obj-m中的单目标全部记录在$(single-used-m)变量中,单目标中用户指定的x.o的源码就是单独的一个x.c,所以此类目标有一个静态规则如下:
## 在$(single-used-m)中的所有目标的规则定义都是 $(obj)/%.o: $(src)/%.c
$(single-used-m): $(obj)/%.o: $(src)/%.c $(recordmcount_source) $(objtool_dep) FORCE
$(call cmd,force_checksrc)
$(call if_changed_rule,cc_o_c)
@{ echo $(@:.o=.ko); echo $@; \
$(cmd_undef_syms); } > $(MODVERDIR)/$(@F:.o=.mod)
* 复合目标:
用户输入的obj-m中的复合目标全部都记录在$(multi-used-m)中,复合目标指的是用户在obj-m中指定了x.o为目标,但此目标包含多个源码文件,故在subdir的Makefile中用户还通过了如x-objs/x-y/x-m指定了其他依赖的*.o文件,这三个变量中的目标与源码一一对应,而obj-m中的x.o不对应任何源码,而是最终这些.o 通过ld -r输出的结果.
复合目标也有用一个静态规则,如下:
$(multi-used-m): FORCE
$(call if_changed,link_multi-m)
@{ echo $(@:.o=.ko); echo $(filter %.o,$^); \
$(cmd_undef_syms); } > $(MODVERDIR)/$(@F:.o=.mod)
## 这里输出复合目标真正以来的单目标,类似 x.o : $(x-objs) $(x-y) $(x-m)
$(call multi_depend, $(multi-used-m), .o, -objs -y -m)
复合目标生成了x.o : $(x-objs) $(x-y) $(x-m)依赖项,而这些依赖项的规则则是通用规则:
$(obj)/%.o: $(src)/%.c $(recordmcount_source) $(objtool_dep) FORCE
$(call cmd,force_checksrc)
$(call if_changed_rule,cc_o_c)
* 子目录:
子目录中的模块的编译实际上和当前目录的思路完全一致,就是一个递归调用,不予讨论:
PHONY += $(subdir-ym)
## 这里是重启子目录编译,是否buildin取决于当前子目录是否在subdir-obj-y中也有记录,若有则代表obj-y也指定了要编译此目录
## 设置need-builtin,这个和KBUILD_BUINTIN不一样,设置了不代表所有obj-y都要编译,而只是在变量计算时有用
$(subdir-ym):
$(Q)$(MAKE) $(build)=$@ need-builtin=$(if $(findstring $@,$(subdir-obj-y)),1)
这里需要注意的是,前面说的规则都是针对.c文件的规则,若有.s文件则参考./Makefile.build中.s文件的对应规则. 根据以上规则整理可知:
1) 对于单目标(*.o),其依赖项和编译步骤包括:
=> $(src)/%.c //依赖的源码文件,必须存在
=> $(recordmcount_source) //这里是一个pl脚本和几个头文件,pass
|=> $(objtool_dep) //工具依赖,pass
|* $(call cmd,force_checksrc) //默认调用sparse,做源码检查,这是linux默认的源码检查工具
|* $(call if_changed_rule,cc_o_c) //这里调用rule_cc_o_c编译单目标,其中包含7个子命令:
| |* $(call cmd,checksrc) //这里也是sparse源码检查,和前面的区别在于输入参数不同,则有可能不检查
| |* $(call cmd_and_fixdep,cc_o_c)
| | * $(CC) $(c_flags) -c -o *.o *.c //这里使用c_flags直接编译.c文件->.o,由于c_flags中设置了-Wp,-MD,$(depfile) 这里同时会生成当前编译依赖的头文件-> *.d
| | * scripts/basic/fixdep $(depfile) $@ '$(make-cmd)' > $(dot-target).cmd; //调用fixdep对*.d文件做修正,结果输出到 *.cmd,
| | //此函数会先写入 cmd_$(target) := make-cmd, 然后写入依赖项删除生成的*.d
| |* $(call cmd,gen_ksymdeps) //在开启了CONFIG_TRIM_UNUSED_KSYMS时,会将符号信息也输入到*.cmd中(没细看,先pass)
| |* $(call cmd,checkdoc) //这个应该是从源码中找到文档信息(若有)并输出的,先pass
| |* $(call cmd,objtool) //pass
| |* $(call cmd,modversions_c) //此命令仅在CONFIG_MODVERSIONS=y时才有效,否则cmd_modversions_c为空,在=y时,其主要作用是为当前编译的目标
| | //文件中所有EXPORT_SYMBOL(x)定义的x确定变量__crc_x的值,也就是确定每一个模块导出函数的CRC(这里的导出和符
| | //号表的动态链接符号表中的导出函数不同,后者是dll/exe文件,由ld来决定哪些函数导出;前者是目标文件(.o),由
| | //用户手动指定哪些函数导出),细节参考注1.
| |* $(call cmd,record_mcount) //应该是ftrace计数的,先pass
|* @{ echo $(@:.o=.ko); echo $@; $(cmd_undef_syms); } > $(MODVERDIR)/$(@F:.o=.mod)
当前单目标的信息会被输出到 ./.tmp_versions/*.mod文件中,其内容如:
net/netfilter/xt_nat.ko
net/netfilter/xt_nat.o
(如果开启了CONFIG_TRIM_UNUSED_KSYMS(裁剪所有未使用的导出符号),那么还会将当前模块的未定义符号输出到此文件中)
2) 复合目标(*.o)依赖于多个单目标,这些单目标和前面的单目标规则基本一致,复合目标规则展开后就多一步:
=> $(src)/%.c
=> $(recordmcount_source)
=> $(objtool_dep)
* $(call cmd,force_checksrc)
* $(call if_changed_rule,cc_o_c)
* $(call if_changed,link_multi-m) //这里主要是将复合目标中所有的单目标链接为这个复合目标(-r生成的还是目标文件)
//cmd_link_multi-m = $(LD) $(ld_flags) -r -o $@ $(filter %.o,$^) $(cmd_secanalysis)
* @{ echo $(@:.o=.ko); echo $(filter %.o,$^); $(cmd_undef_syms); } > $(MODVERDIR)/$(@F:.o=.mod)
复合目标在./.tmp_versions/*.mod中的输出和单目标类似,只不过第二行输出的是所有复合目标,如:
drivers/net/wireless/ti/wlcore/wlcore.ko
drivers/net/wireless/ti/wlcore/main.o drivers/net/wireless/ti/wlcore/cmd.o drivers/net/wireless/ti/wlcore/io.o drivers/net/wireless/ti/wlcore/event.o ......
1.2.vmlinux
=> scripts/link-vmlinux.sh //已存在文件
=> autoksyms_recursive //若开启了CONFIG_TRIM_UNUSED_KSYMS这里才有代码,其目的是去除没用到的内核和模块导出函数
//内核和模块通过EXPORT_SYMBOL系列函数导出的符号,并不是所有符号在每次编译中都用到了,这取决于当前内核使用了哪些模块
//而此config的作用就是将所有没有用到的EXPORT_SYMBOL drop掉,这会为编译器(LTO)提供更多优化可能,减少二进制大小和提高安
//全性,但如果需要编译out-of-tree模块的话(内核不再重编),则不要开启此选项,否则有可能找不到符号.
|=> $(vmlinux-deps) // = KBUILD_VMLINUX_OBJS(:= $(head-y) $(init-y) $(core-y) $(libs-y2) $(drivers-y) $(net-y) $(virt-y))
| // += KBUILD_VMLINUX_LIBS(:= libs-y1)
| // += arch/xxx/kernel/vmlinux.lds
| => $(vmlinux-dirs) //注意这里是$(vmlinux-deps)中的每个元素都依赖整个$(vmlinux-dirs)
| // = init-y/m core-y/m drivers-y/m net-y/m libs-y/m virt-y,这里的目录名都没有结尾的/
| * $(Q)$(MAKE) $(build)=$@ need-builtin=1 //调用./script/Makefile.build 构建$(vmlinux-dirs)中的每一个子目录,
| //vmlinux.lds是在这里生成的, modules.order也是在这里生成的,见注3.
|* +$(call if_changed,link-vmlinux) //vmlinux的构建,参考注2.
| * scripts/link-vmlinux.sh $(LD) $(KBUILD_LDFLAGS) $(LDFLAGS_vmlinux) //这里负责vmlinux的构建,参考注2:
| * $(if $(ARCH_POSTLINK), $(MAKE) -f $(ARCH_POSTLINK) $@, true) //arm64平台没有Makefile.postlink,故这里不执行
1.3.modules.builtin
此目标实际上是一个本地文件,在所有子目录都存在此文件(递归生成的),最终父文件中包含了子文件的所有内容,故./modules.builtin中记录了所有信息。
此文件中记录的是那些在Makefile中使用obj-$(CONFIG_XXX)标记,且最终CONFIG_XXX=y的目标,因为CONFIG_XXX也可能=m(编译成ko),所以其名字叫做modules.builtin,意思是模块中被builtin到内核的部分,其递归生成逻辑比较简单就不展开了,文件内容如下(实际上就是一个个builtin的模块的列表,只不过结尾写成了ko):
kernel/arch/arm64/crypto/sha1-ce.ko
kernel/arch/arm64/crypto/sha2-ce.ko
kernel/arch/arm64/crypto/ghash-ce.ko
......
1.4.输出modules.order信息
* 对于某个目录中的非子目录来说,modules.order信息记录的是这些目标(obj-m中的.o,最终以扩展名.ko的形式记录)在obj-m中出现的顺序.
* 对于某个目录中的子目录来说,父目录的Modules.order会直接包含子目录中Modules.order中的所有信息(obj-y中的目录要放到递归中,因为子目录中可能有obj-m指定的目标)
所以对于最终内核源码根目录的modules.order文件,其中记录的是所有目标(ko)出现的编译顺序(全部都是ko,没有别的).
2.stage2
stage2一共有两步操作,其中最关键的是调用Makefile.modpost生成模块的ko:
$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost
$(Q)$(CONFIG_SHELL) $(srctree)/scripts/modules-check.sh
2.1.$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost
没指定目标的情况下Makefile.modpost中的默认目标为_modpost,其本身只有依赖没有命令:
_modpost
=> __modpost
=> $(modules:.ko=.o) //获取 ./tmp_versions/*.mod中记录的ko对应的.o目标文件名,这些目标都是在stage1编译出来的本地文件,
//故在这里并没有为这些.o文件设置额外规则(若不存在则编译报错),
* $(call cmd,modpost) $(wildcard vmlinux) //这里找到所有*.mod中所有ko对应的.o文件和vmlinux(若有)作为输入;
//最终为其中的每个*.o文件输出一个*.mod.c文件,同时将所有这些模块和内核vmlinux(若有)的符号信息
//记录到$(objtree)/Module.symvers中
=> $(modules) //$(modules)中记录的是*.mod中全部的ko名,其拥有静态规则如下:
$(modules): %.ko :%.o %.mod.o FORCE
+$(call if_changed,ld_ko_o)
=> %.o //这个就是stage1编译出的*.o
=> %.mod.o //%.mod.o的规则就是简单的编译 %.mod.c,而这个文件是在前面$(call cmd,modpost)中生成的
* cmd_ld_ko_o //使用LD -r 将 *.o 和 *.mod.o链接为 *.ko,这里也是最终ko的 生成
//执行ARCH_POSTLINKE makefile(若有)
2.2 $(Q)$(CONFIG_SHELL) $(srctree)/scripts/modules-check.sh
此脚本主要检查当前所有编译的模块中是否有重名模块(基于module.order)
3. 一些细节
3.1 $(call cmd,modversions_c)
cmd_modversions_c = \
if $(OBJDUMP) -h $@ | grep -q __ksymtab; then \
$(call cmd_gensymtypes_c,$(KBUILD_SYMTYPES),$(@:.o=.symtypes)) \
> $(@D)/.tmp_$(@F:.o=.ver); \
\
$(LD) $(KBUILD_LDFLAGS) -r -o $(@D)/.tmp_$(@F) $@ \
-T $(@D)/.tmp_$(@F:.o=.ver); \
mv -f $(@D)/.tmp_$(@F) $@; \
rm -f $(@D)/.tmp_$(@F:.o=.ver); \
fi
内核模块是通过EXPORT_SYMBOL系列宏来导出符号的(包括GPL兼容符号),在内核或其他模块的标准操作流程中只能使用EXPORT_SYMBOL导出的函数,这里先讨论CONFIG_MODVERSIONS开启的情况,此时对于函数x,EXPORT_SYMBOL(x)实际包含三步操作:
- 生成一个____ksymtab+x段,并添加符号 __ksymtab_x的定义
- 生成一个__ksymtab_strings段(公用的段名,和x无关),并添加一个字符串__kstrtab_x="x"的定义
- 一个____kcrctab+x段,并添加符号 __crc_x的声明(但并没有定义,因为此符号要保存的是符号x的CRC)
而在每个目标文件编译后,都会立刻执行cmd_modversions_c命令,此命令的作用是在目标文件中定义每个EXPORT_SYMBOL导出的变量的CRC,也就是上面所有的__crc_x,其主要包括两步操作:
1) cmd_gensymtypes_c:
其主要作用是解析目标文件对应的源码文件,对其中每个EXPORT_SYMBOL系列函数标记的函数计算出一个CRC值,最终记录到.tmp_*.ver文件中,其格式如下注释:
# These mirror gensymtypes_S and co below, keep them in synch.
## 以blk-core为例,这里展开通常为($(1)通常为空):
## $(CPP) -E -D__GENKSYMS__ $(c_flags) block/blk-core.c | scripts/genksyms/genksyms -r /dev/null > block/.tmp_blk-core.ver;
## 此命令先通过$(CPP) -E 输出了预处理后的blk-core.c文件(应该是.i文件了,这里的__GENKSYMS__是将__CRC_SYMBOL定义为空,因为这里没用上)
## 之后将预处理结果交给genksyms,后者会识别源码中所有EXPORT_SYMBOL的符号,并为其生成一个CRC,最终将结果记录到对应的.tmp_blk-core.ver文件中,如:
## __crc_blk_queue_flag_set = 0x564adbd5;
## __crc_blk_queue_flag_clear = 0x52d55223;
## __crc_blk_queue_flag_test_and_set = 0x5474d529;
## __crc_blk_queue_flag_test_and_clear = 0xb029eaf1;
cmd_gensymtypes_c = \
$(CPP) -D__GENKSYMS__ $(c_flags) $< | \
scripts/genksyms/genksyms $(if $(1), -T $(2)) \
$(patsubst y,-R,$(CONFIG_MODULE_REL_CRCS)) \
$(if $(KBUILD_PRESERVE),-p) \
-r $(firstword $(wildcard $(2:.symtypes=.symref) /dev/null))
2) 通过$(LD) -r -T .tmp_*.ver *.o -o xxx为目标文件*.o加入CRC函数的定义
根据前面可知,EXPORT_SYMBOL(x)中只是声明了x的crc符号 __crc_x,但没有真正的定义,故在当前编译出的目标文件中,所有的__crc_x函数都是UND的.
而前面的cmd_gensymtypes_c又刚好将这些函数的CRC计算并保存到.tmp_*.ver中,符号名同样是__crc_x,.tmp_v*.ver中存储的格式就是ld链接脚本定义符号的格式,故通过$(LD) -T 指定.tmp_*.ver作为链接脚本,重新链接目标文件为新的目标文件,即可解决目标文件中__crc_x未定义的问题,链接后函数的__crc_x符号类型为ABS,符号值就是.tmp_*.ver中对应符号的CRC值,如:
826: 00000000bd9074b1 0 NOTYPE GLOBAL DEFAULT ABS __crc_blk_finish_plug
827: 000000000df7b943 0 NOTYPE GLOBAL DEFAULT ABS __crc_direct_make_request
在CONFIG_MODVERSIONS未开启的情况下,EXPORT_SYMBOL(x)并不生成____kcrctab+x段,也不产生__crc_x的声明,同时命令cmd_modversions_c也为空,故模块中不会引入任何EXPORT_SYMBOL的crc信息,.tmp_*.ver链接脚本也不会生成,如下:
./include/linux/export.h
#ifdef CONFIG_MODVERSIONS
...
#define __CRC_SYMBOL(sym, sec) \
asm(" .section \"___kcrctab" sec "+" #sym "\", \"a\" \n" \
" .weak __crc_" #sym " \n" \
" .long __crc_" #sym " \n" \
" .previous \n");
#else
#define __CRC_SYMBOL(sym, sec)
#endif
#define ___EXPORT_SYMBOL(sym, sec) \
extern typeof(sym) sym; \
__CRC_SYMBOL(sym, sec) \
static const char __kstrtab_##sym[] __attribute__((section("__ksymtab_strings"), used, aligned(1))) = #sym; \
__KSYMTAB_ENTRY(sym, sec)
./scripts/Makefile.build
ifdef CONFIG_MODVERSIONS
cmd_modversions_c = \
if $(OBJDUMP) -h $@ | grep -q __ksymtab; then \
$(call cmd_gensymtypes_c,$(KBUILD_SYMTYPES),$(@:.o=.symtypes)) \
> $(@D)/.tmp_$(@F:.o=.ver); \
\
$(LD) $(KBUILD_LDFLAGS) -r -o $(@D)/.tmp_$(@F) $@ \
-T $(@D)/.tmp_$(@F:.o=.ver); \
mv -f $(@D)/.tmp_$(@F) $@; \
rm -f $(@D)/.tmp_$(@F:.o=.ver); \
fi
endif
3.2 scripts/link-vmlinux.sh $(LD) $(KBUILD_LDFLAGS) $(LDFLAGS_vmlinux)
此脚本负责 vmlinux.o, modules.builtin.modinfo, .tmp_vmlinux1/2, .tmp_kallsyms1/2.o, vmlinux, System.map的生成,其中只有vmlinux是在Makefile脚本中存在目标的,其他均没有对应的规则(vmlinux.o是中间产物,在Makefile中并没有vmlinux.o这个目标,在模块编译中有也不是用来生成vmlinux.o的),此脚本的大体流程如下:
1. 生成vmlinux.o
objects="--whole-archive ${KBUILD_VMLINUX_OBJS} --no-whole-archive --start-group ${KBUILD_VMLINUX_LIBS} --end-group"
${LD} ${KBUILD_LDFLAGS} -r -o ${1} ${objects}
这里LD的参数会将$(KBUILD_VMLINUX_OBJS)中所有归档文件中的所有.o文件都链接到vmlinux.o中,并在$(KBUILD_VMLINUX_LIBS)中循环查找依赖库(最终真的不需要的不链接).
要注意,这里没有指定链接脚本,最终生成的vmlinux.o同样也是一个目标文件。
2. 检查vmlinux.o符号是否正确,生成Modules.symvers
${MAKE} -f "${srctree}/scripts/Makefile.modpost" vmlinux.o
这里最终调用modpost工具处理vmlinux.o,此工具是用作CRC处理的(见3.4),这里会将vmlinux.o中所有的CRC信息输出到$(objtree)/Modules.symvers中(但不输出对应的*.mod,因为只有对于非[*/]vmlinux[.o]文件才会输出对应的*.mod):
- 如果后面没有编译模块,那么这里的CRC就是最终内核的所有CRC信息了
- 若后面编译了模块,那么会根据vmlinux和模块信息生成一个新的Modules.symvers.
3. 提取vmlinux.o中的.modinfo段
${OBJCOPY} -j .modinfo -O binary vmlinux.o modules.builtin.modinfo
若vmlinux.o中存在.modinfo(里面存的是如 name=, depend=等字段),则将其单独提取到文件modules.builtin.modinfo中,没有则不提取.
4. 生成.tmp_vmlinux1和.tmp_kallsyms1.o
## 在链接时,链接参数--whole-archive --no-whole-archive 内包裹的归档文件(*.a)中的所有目标均需要被打包到输出文件中
## --start-group --end-group内包裹的归档文件(*.a)是真正的库,但会被循环查找未定义符号,没有使用到的最终不会打包
objects="--whole-archive ${KBUILD_VMLINUX_OBJS} --no-whole-archive --start-group ${KBUILD_VMLINUX_LIBS} --end-group "
## 这里LD没有指定 -pie/-shared等参数,最终编译出来的是一个static executable文件
## .tmp_vmlinux1: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, BuildID[sha1]=d12cba6bd8ebfe3f61f4f3467dd23830bb0baeab, with debug_info, not stripped
${LD} ${KBUILD_LDFLAGS} ${LDFLAGS_vmlinux} -o .tmp_vmlinux1 -T xxx/vmlinux.lds ${objects}
## nm查看最终static exe中的所有符号,-n是按照符号地址排序
## scripts/kallsyms接收标准输入,将结果以汇编的形式输出到 ./tmp_kallsyms1.S,kallsym会过滤并记录符号表中的符号,并将这些符号信息记录到新的符号
## kallsyms_offset, kallsyms_relative_base, kallsyms_num_syms, kallsyms_names, kallsyms_markers, kallsyms_token_table, kallsyms_token_index 中,
## 这些新的符号(变量)最终被输出到了./tmp_kallsyms1.S中,[]内的是scripts/kallsyms的参数,与内核配置项相关
${NM} -n .tmp_vmlinux1 | scripts/kallsyms [--all-symbols/--absolute-percpu/--base-relative] > ./tmp_kallsyms1.S
## CC -c 是将./tmp_kallsyms1.S文件编译为目标文件.tmp_kallsyms1.o
${CC} ${aflags} -c -o .tmp_kallsyms1.o ./tmp_kallsyms1.S
5. 生成.tmp_vmlinux2和.tmp_kallsyms2.o
其流程和.tmp_vmlinux1, .tmp_kallsyms1.o大体是类似的:
## 这里的objects比上面多了一个 .tmp_kallsyms1.o,也就是说4中的.tmp_kallsyms1.o文件在此步骤中也参与了链接
objects="--whole-archive ${KBUILD_VMLINUX_OBJS} --no-whole-archive --start-group ${KBUILD_VMLINUX_LIBS} --end-group .tmp_kallsyms1.o"
## 同样的链接脚本和配置项,最终生成.tmp_vmlinux2,和.tmp_vmlinux1的区别仅在于新增了目标文件 .tmp_kallsyms1.o
${LD} ${KBUILD_LDFLAGS} ${LDFLAGS_vmlinux} -o .tmp_vmlinux2 -T xxx/vmlinux.lds ${objects}
## 同样的符号表再次解析,这里又会生成kallsyms_offset, kallsyms_relative_base, kallsyms_num_syms, kallsyms_names, kallsyms_markers, kallsyms_token_table,
## kallsyms_token_index符号,和4的唯一区别是,这些符号中的内容包含这些符号自身
${NM} -n .tmp_vmlinux2 | scripts/kallsyms [--all-symbols/--absolute-percpu/--base-relative] > ./tmp_kallsyms2.S
## 同样将新的符号信息输出到./tmp_kallsyms2.S
${CC} ${aflags} -c -o .tmp_kallsyms2.o ./tmp_kallsyms2.S
6. 若.tmp_kallsyms2.o和 .tmp_kallsym1.o大小不一样则要重复上面步骤做一个 .tmp_kallsyms3.o
7. 链接最终的vmlinux
## 这里的objects多的是.tmp_kallsyms2.o
objects="--whole-archive ${KBUILD_VMLINUX_OBJS} --no-whole-archive --start-group ${KBUILD_VMLINUX_LIBS} --end-group .tmp_kallsyms2.o"
## 同样的链接脚本和配置项,最终生成vmlinux
${LD} ${KBUILD_LDFLAGS} ${LDFLAGS_vmlinux} -o vmlinux -T xxx/vmlinux.lds ${objects}
8. 生成System.map
System.map实际上是读取了vmlinux的符号表,并过滤了:
- [aNUw],分别代表本地绝对符号,调试符号,未定义全局符号,局部弱符号
- __crc_,所有__crc_开头的符号不记录到System.map中(记录到Module.symvers中了)
- $[adt],所有$a,$d,$t符号都被忽略了,这些是标记代码数据段开始的符号,对于arm的nm来说,正常dx都不输出,所有没有,x86才会输出才会过滤
- 这里.L是有问题的,错误的过滤掉了*.L的文件,社区代码写错了
## $NM -n vmlinux | grep -v '\( [aNUw] \)\|\(__crc_\)\|\( \$[adt]\)\|\( .L\)' > System.map
mksysmap vmlinux System.map
实际上内核看到的kallsyms和System.map并没有直接关系,只能说二者都源自vmlinux的符号表,System.map有自己的过滤方式,而kallsyms则是通过./script/kallsyms工具来过滤编译并链接到内核的。
梳理一下4-6的流程,这个三个步骤实际上的目的就是要将最终二进制符号表中的部分符号打包到内核中,因为:
- 静态链接符号表本身是运行时无关的(不载入内存),也是有可能被strip掉的,所以其肯定不能作为最终内核的符号表
- 动态链接符号表的生成是LD控制的(如默认局部符号不输出),也就是说从静态链接符号表中提取哪些符号放到动态链接符号表,是要满足LD的规则,我们只能通过LD的部分参数粗粒度的来做一些控制,通常无法满足要求。所以动态链接符号表(以及相关段)在内核链接脚本中就直接给discard了:
./arch/arm64/kernel/vmlinux.lds.s
SECTIONS
{
/DISCARD/ : {
ARM_EXIT_DISCARD(EXIT_TEXT)
ARM_EXIT_DISCARD(EXIT_DATA)
EXIT_CALL
*(.discard)
*(.discard.*)
## vmlinux不指定-pie链接参数这里本来也没有,指定了则discard
*(.interp .dynamic)
*(.dynsym .dynstr .hash .gnu.hash)
*(.eh_frame)
}
......
所以社区采取的做法是自己向内核打包一份符号表,其来源就是nm -n 获取的静态符号表,其中通过./script/kallsyms工具过滤,最终过滤的结果打包到内核代码中,这样就可以实现自己制定的一份内核符号表了,这个整体流程如下图:
其中:
- .tmp_vmlniux1/2/3, vmlinux都是 static executable,也就是静态链接可执行文件
- .tmp_kallsyms1/2/3.s 都是汇编文件
- .tmp_kallsyms1/2/3.o 都是汇编文件通过CC -c编译出的目标文件
- ./script/kallsyms工具的作用是过滤当前符号,其输入是从标准输入中获取的,这也是为什么使用前需要通过nm -n从静态链接可执行文件中获取其符号信息.
./script/kallsyms中的大体逻辑是:
1) 忽略符号:
- 长度> KSYM_NAME_LEN的符号都忽略
- 特别指定忽略绝对符号(A/a)__kernel_syscall_via_break/__kernel_syscall_via_epc/__kernel_sigtramp/__gp,其他绝对符号都保留
- 未定义符号(U/u)和arm的 $a/x/d/t符号都忽略
- $开头的符号和调试符号(N/n)都忽略
- 修改源码可添加要忽略的符号,包括:
- 特别指定要忽略的符号
- 特定前缀开头的符号
- 特定前缀结尾的符号
- 如果没开all_symbols(CNFIG_KALLSYMS_ALL),那么所有非text和inittext段的符号都忽略
2) 结果输出:
最终结果是以汇编格式输出的,所有的内容实际上全都定义在几个符号内了,这些符号是:kallsyms_offset, kallsyms_relative_base, kallsyms_num_syms, kallsyms_names, kallsyms_markers, kallsyms_token_table,kallsyms_token_index.
符号表输出的整体逻辑:
1. 将所有目标${KBUILD_VMLINUX_OBJS}和其需要的库${KBUILD_VMLINUX_LIBS}链接为 .tmp_vmlinux1;
2. 从.tmp_vmlinux1中提取静态链接符号表并通过kallsyms过滤,结果输出到 .tmp_kallsyms1.o,.tmp_kallsyms1.o中包含了内核所有的符号表,理论上将其链接到内核中就可以了,但此符号表的内容中不包含新生成的kallsyms_offset等符号的信息,如果就这样将.tmp_kallsyms1.o链接到最终内核,那么最终内核只能在其静态链接符号表中看到kallsyms_offset等信息,而无法在运行时动态查找kallsym_offset等的符号信息.
3. 所以需要在此链接将.tmp_kallsyms1.o + .tmp_vmlinux1 => .tmp_vmlinux2, 然后再次通过kallsyms生成符号表目标文件 .tmp_kallsyms2.o
.tmp_kallsyms2.o和.tmp_kallsyms1.o的区别在于,前者在内容中包括kallsyms_offset等符号的符号信息,而后者的内容中不包括(只在静态链接符号表中包括)kallsym_offset等符号的信息.
4. 正常来说将.tmp_kallsym2.o实际上已经包含了所有需要的符号信息了,包括目标中的符号信息和内核符号表符号的信息,直接链接到vmlinux中就可以了,但有个问题就是:
* .tmp_kallsyms2.o内容中关于kallsyms_offset的地址信息是来自于.tmp_vmlinux2(=目标+.tmp_kallsyms1.o)
* 而最终vmlinux内容中关于kallsyms_offset的地址信息,应该是来源于其自身(=目标 +.tmp_kallsyms2.o)
* 当.tmp_kallsyms1.o和.tmp_kallsyms2.o大小相等时,这个kallsyms_offset的地址信息是相同的,但当二者大小不同时,vmlinux想记录自身kallsyms_offset的信息,但实际上记录的是.tmp_vmlinux2中的kallsyms_offset的信息,这就会导致二者不一致.
* 所以如果二者大小不相同的话,还需要再做一个.tmp_vmlinux3,然后从中重新提取 .tmp_kallsyms3.o
- .tmp_kallsyms1.o和.tmp_kallsyms2.o的大小大概率是相同的(因为二者内容区别仅仅是增加了kallsyms_offset等几个符号)
- 而.tmp_kallsyms2.o和.tmp_kallsyms3.o的大小一定是相同的(因为二者中的符号个数是一样多的,区别仅在于地址不一样)
- 所以这里的.tmp_kallsyms2/3.o不用再比较了,二者大小肯定是一样的,.tmp_kallsym3.o中的符号链接到vmlinux中肯定也能代表vmlinux自身的符号
这也是为什么vmlniux中最终能通过kallsyms_offset等符号找到自身符号表的原因,因为这些符号表信息是从静态链接符号表中提取的,通过./script/kallsyms过滤后特意重新打包到vmlinux中的。
这里最后再说一下System.map和vmlinux代码中符号表的区别:
- System.map中的符号表是通过 $NM -n vmlinux | grep -v '\( [aNUw] \)\|\(__crc_\)\|\( \$[adt]\)\|\( .L\)' > System.map 过滤的,其过滤的内容只包括__CRC_,局部符号,段标记符号,本地绝对符号,调试符号,未定义全局符号,局部弱符号等。
- vmlnux中内置的符号表则有可能只记录了text/inittext段的符号,或者其他的规则定义的符号
- 只能说System.map和vmlinux中符号表二者的符号来源相同,都可以认为是从最终vmlinux中来的(对于vmlinux中符号表是等同于从vmlinux中来的),但二者的过滤器不同,导致二者记录的符号内容不同,二者不是子集的关系,应该是互有交集,总体上System.map会更全一些.
3.3 vmlinux.lds、modules.order的生成
$(vmlinux-dirs): prepare
$(Q)$(MAKE) $(build)=$@ need-builtin=1
此规则调用./scripts/Makefile.build负责子目录的构建, 其中:
* vmlinux.lds是$(vmlinux-dirs)中传过来的目标之一,其是通过如下规则构建的
./scripts/Makefile.build
$(obj)/%.lds: $(src)/%.lds.S FORCE
$(call if_changed_dep,cpp_lds_S)
* modules.order是_build的依赖项:
./scripts/Makefile.lib
## obj-y中只取目录,obj-m中的全要
modorder := $(patsubst %/,%/modules.order, $(filter %/, $(obj-y)) $(obj-m:.o=.ko))
./scripts/Makefile.build
ifdef CONFIG_MODULES
modorder-target := $(obj)/modules.order
endif
__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \
$(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \
$(subdir-ym) $(always)
@:
## 如果m是.modules.order文件,则cat其中的ko,如果m不是,则直接记录文件(ko)名
modorder-cmds = \
$(foreach m, $(modorder), \
$(if $(filter %/modules.order, $m), \
cat $m;, echo kernel/$m;))
$(modorder-target): $(subdir-ym) FORCE
$(Q)(cat /dev/null; $(modorder-cmds)) > $@
也就是说:
* 对于modorder中的非目录对象,./modules.order中直接记录其名字(通常是*.ko)
* 对于modorder中的目录对象, ./modules.order记录其子目录中的modules.order到当前文件
3.4 $(call cmd,modpost) $(wildcard vmlinux)
## shell执行后,从./tmp_versions中读取所有ko列表(对应obj-m指定的目标名,不包括-objs等)
MODLISTCMD := find $(MODVERDIR) -name '*.mod' | xargs -r grep -h '\.ko$$' | sort -u
## 根据配置,确定当前modpost命令的参数
modpost = scripts/mod/modpost \
$(if $(CONFIG_MODVERSIONS),-m) \
$(if $(CONFIG_MODULE_SRCVERSION_ALL),-a,) \
$(if $(KBUILD_EXTMOD),-i,-o) $(kernelsymfile) \
$(if $(KBUILD_EXTMOD),-I $(modulesymfile)) \
$(if $(KBUILD_EXTRA_SYMBOLS), $(patsubst %, -e %,$(KBUILD_EXTRA_SYMBOLS))) \
$(if $(KBUILD_EXTMOD),-o $(modulesymfile)) \
$(if $(CONFIG_SECTION_MISMATCH_WARN_ONLY),,-E) \
$(if $(KBUILD_MODPOST_WARN),-w)
## Makefile flags
MODPOST_OPT=$(subst -i,-n,$(filter -i,$(MAKEFLAGS)))
## 调用modpost生成Modules.version文件
cmd_modpost = $(MODLISTCMD) | sed 's/\.ko$$/.o/' | $(modpost) $(MODPOST_OPT) -s -T -
modpost参数定义如下:
- -m: 若指定了-m选项,则输出的mod.c文件中中会多一个____versions[]数组,其中记录着模块的CRC值
- -i xxx/Module.symvers: 指定-i参数和一个文件,那么modpost会认为此文件中记录的是内核的符号信息(通常是$(objtree)/Module.symvers),从此文件中读入的所有符号都会被标记为kenrel符号(kernel代表内核vmlinux,或内核intree模块中的符号)
- -o xxx/Modules.symvers: 指定-i参数和一个文件,那么modpost最后会将其中所有的符号信息都输出到此文件中.
- -I xxx/Module.symvers: 指定-I参数和一个文件, 那么modpost会认为此文件中记录的是非内核(外部模块)中的符号信息,这些信息同样也会被读入当前modpost工具中做后续分析(通常此module.symvers是此模块前一次编译输出的module.symvers)
- -e :若当前模块依赖于其他外部模块中的符号,那么需要此选项来指定其他模块
modpost的功能有两项:
1.其可以为所有模块目标文件(*.o)生成对应的.mod.c文件,后者用于ko的编译
2.其可以为外部模块或内核生成Module.symvers文件
根据使用方式的不同,具体的操作也不同,在内核中modpost一共有三种使用方式:
1) 内核(vmlinux)编译时
在内核编译vmlinux的过程中(link-svmlinux.sh),生成vmlinux.o后就会为其调用modpost,会先将内核built-in中所有的导出符号输出到./Module.symvers文件中,后面若有内部模块编译(2),则在覆盖重写此文件即可
//link-svmlinux.sh
${MAKE} -f "${srctree}/scripts/Makefile.modpost" vmlinux.o
2) 内部模块编译时
内部模块编译时,不论是单个模块还是所有模块编译,最终调用Makefile.modpost时都会做相同的操作:
modules: $(vmlinux-dirs) $(if $(KBUILD_BUILTIN),vmlinux) modules.builtin
......
@$(kecho) ' Building modules, stage 2.';
$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost
......
%.ko: %.o
$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost
这里最终的命令都类似于:
//$(call cmd,modpost) $(wildcard vmlinux)
find .tmp_versions -name '*.mod' | xargs -r grep -h '\.ko$' | sort -u | sed 's/\.ko$/.o/' |scripts/mod/modpost -o ./Module.symvers -s -T - [vmlinux]
也就是说所有./.tmp_versions/*.mod中存在的ko对应的.o文件和vmlinux(若有)都会作为输入;最终会为每个*.o文件输出一个*.mod.c文件,同时将所有这些模块和内核vmlinux(若有)的符号信息记录到$(objtree)/Module.symvers中.
3) 外部模块编译时
外部模块编译实际上和内部模块编译的命令是一样的,只不过modpost的参数不同
modules: $(module-dirs)
@$(kecho) ' Building modules, stage 2.';
$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost
最终命令类似:
## $(call cmd,modpost) $(wildcard vmlinux)
find $(KBUILD_EXTMOD)/.tmp_versions -name '*.mod' | xargs -r grep -h '\.ko$' | sort -u | sed 's/\.ko$/.o/' | \
scripts/mod/modpost -i ./Module.symvers -o $(KBUILD_EXTMOD)/Module.symvers -s -T - [vmlinux]
也就是说所有$(KBUILD_EXTMOD)/.tmp_versions/*.mod中存在的ko对应的.o文件(外部模块的.o和内核、外部模块的Module.symver, 以及内核vmlinux(若有)都作为输入;最终会为所有外部模块的*.o生成对应的*.mod.o,并将所有非内核符号导出信息输出到外部模块的Module.symver文件中.