我们经常遇到的问题估计是调试bug、该配置适配移植等,也就是说已经有可以编译运行的代码,我们找到其中需要修改的点进行修改就可以了。那什么是别人不太会的,但是又比较有用的知识,遇到一个系统性的问题需要用到的,那可能就是对编译过程的理解。本文就来介绍下这些奇怪的知识。
1. 顶层Makefile
顶层也就是uboot代码根目录下的Makefile。顶层Makefile会再去调用子目录里面的Makefile递归执行,其是一个树状结构,顶层Makefile就是树根。
1.1 版本号:
那么这个怎么最后运行的时候被打印出来了。是时候展现下真正的技术了,在顶层Makefile中。
VERSION = 2023
PATCHLEVEL = 07
SUBLEVEL = 02
UBOOTVERSION = $(VERSION)$(if $(PATCHLEVEL),.$(PATCHLEVEL)$(if $(SUBLEVEL),.$(SUBLEVEL)))$(EXTRAVERSION)
ubootrelease:
@echo "$(UBOOTVERSION)$$($(CONFIG_SHELL) $(srctree)/scripts/setlocalversion $(srctree))"
# Use sed to remove leading zeros from PATCHLEVEL to avoid using octal numbers
define filechk_version.h
(echo \#define PLAIN_VERSION \"$(UBOOTRELEASE)\"; \
echo \#define U_BOOT_VERSION \"U-Boot \" PLAIN_VERSION; \
echo \#define U_BOOT_VERSION_NUM $(VERSION); \
echo \#define U_BOOT_VERSION_NUM_PATCH $$(echo $(PATCHLEVEL) | \
sed -e "s/^0*//"); \
echo \#define CC_VERSION_STRING \"$$(LC_ALL=C $(CC) --version | head -n 1)\"; \
echo \#define LD_VERSION_STRING \"$$(LC_ALL=C $(LD) --version | head -n 1)\"; )
endef
$(version_h): include/config/uboot.release FORCE
$(call filechk,version.h)
version_h := include/generated/version_autogenerated.h
这样就生成了include/generated/version_autogenerated.h,在其中:
#define PLAIN_VERSION "2023.07.02-dirty"
#define U_BOOT_VERSION "U-Boot " PLAIN_VERSION
display_options--》display_options_get_banner--》display_options_get_banner_priv
char *display_options_get_banner_priv(bool newlines, const char *build_tag,
char *buf, int size)
{
int len;
len = snprintf(buf, size, "%s%s", newlines ? "\n\n" : "",
version_string);
display_options在启动的时候,启动函数数组序列中执行,common/board_f.c中
static const init_fnc_t init_sequence_f[] = {
env_init, /* initialize environment */
init_baud_rate, /* initialze baudrate settings */
serial_init, /* serial communications setup */
console_init_f, /* stage 1 init of console */
display_options, /* say that we are here */
display_text_info, /* show debugging info if required */
启动的流程以后再介绍,这里就是各种宏定义,代码生成,然后在c代码中使用。看似复杂,其实一点也不简单,还是挺有趣的。
1.2 编译命令
help:
@echo 'Cleaning targets:'
@echo ' clean - Remove most generated files but keep the config'
@echo ' mrproper - Remove all generated files + config + various backup files'
@echo ' distclean - mrproper + remove editor backup and patch files'
@echo ''
@echo 'Configuration targets:'
@$(MAKE) -f $(srctree)/scripts/kconfig/Makefile help
@echo ''
@echo 'Test targets:'
@echo ''
@echo ' check - Run all automated tests that use sandbox'
@echo ' pcheck - Run quick automated tests in parallel'
@echo ' qcheck - Run quick automated tests that use sandbox'
@echo ' tcheck - Run quick automated tests on tools'
@echo ' pylint - Run pylint on all Python files'
@echo ''
@echo 'Other generic targets:'
@echo ' all - Build all necessary images depending on configuration'
@echo ' tests - Build U-Boot for sandbox and run tests'
@echo '* u-boot - Build the bare u-boot'
@echo ' dir/ - Build all files in dir and below'
@echo ' dir/file.[oisS] - Build specified target only'
@echo ' dir/file.lst - Build specified mixed source/assembly target only'
@echo ' (requires a recent binutils and recent build (System.map))'
@echo ' tags/ctags - Generate ctags file for editors'
@echo ' etags - Generate etags file for editors'
@echo ' cscope - Generate cscope index'
@echo ' ubootrelease - Output the release version string (use with make -s)'
@echo ' ubootversion - Output the version stored in Makefile (use with make -s)'
@echo " cfg - Don't build, just create the .cfg files"
@echo " envtools - Build only the target-side environment tools"
@echo ''
@echo 'PyPi / pip targets:'
@echo ' pip - Check building of PyPi packages'
@echo ' pip_test - Build PyPi pakages and upload to test server'
@echo ' pip_release - Build PyPi pakages and upload to release server'
@echo ''
@echo 'Static analysers'
@echo ' checkstack - Generate a list of stack hogs'
@echo ' coccicheck - Execute static code analysis with Coccinelle'
@echo ''
@echo 'Documentation targets:'
@$(MAKE) -f $(srctree)/doc/Makefile dochelp
@echo ''
@echo ' make V=0|1 [targets] 0 => quiet build (default), 1 => verbose build'
@echo ' make V=2 [targets] 2 => give reason for rebuild of target'
@echo ' make O=dir [targets] Locate all output files in "dir", including .config'
@echo ' make C=1 [targets] Check all c source with $$CHECK (sparse by default)'
@echo ' make C=2 [targets] Force check of all c source with $$CHECK'
@echo ' make RECORDMCOUNT_WARN=1 [targets] Warn about ignored mcount sections'
@echo ' make W=n [targets] Enable extra gcc checks, n=1,2,3 where'
@echo ' 1: warnings which may be relevant and do not occur too often'
@echo ' 2: warnings which occur quite often but may still be relevant'
@echo ' 3: more obscure warnings, can most likely be ignored'
@echo ' Multiple levels can be combined with W=12 or W=123'
@echo ''
@echo 'Execute "make" or "make all" to build all targets marked with [*] '
@echo 'For further info see the ./README file'
- make O:指定编译输出目录
- make V=0|1:输出编译日志级别
- make C=2:检查所有的源码文件
- make M=dir:单独编译某个目录
1.3 编译相关
描述递归执行子目录的makefile进行编译,可以自己用AI解释下这个代码的含义
# Look for make include files relative to root of kernel src
#
# This does not become effective immediately because MAKEFLAGS is re-parsed
# once after the Makefile is read. It is OK since we are going to invoke
# 'sub-make' below.
MAKEFLAGS += --include-dir=$(CURDIR)
获取Host主机的架构和系统
HOSTARCH := $(shell uname -m | \
sed -e s/i.86/x86/ \
-e s/sun4u/sparc64/ \
-e s/arm.*/arm/ \
-e s/sa110/arm/ \
-e s/ppc64/powerpc/ \
-e s/ppc/powerpc/ \
-e s/macppc/powerpc/\
-e s/sh.*/sh/)
HOSTOS := $(shell uname -s | tr '[:upper:]' '[:lower:]' | \
sed -e 's/\(cygwin\).*/cygwin/')
export HOSTARCH HOSTOS
scripts/Kbuild.include脚本的使用:
# We need some generic definitions (do not try to remake the file).
scripts/Kbuild.include: ;
include scripts/Kbuild.include
这个脚本里面有很多变量,这些变量级别都是使用命令生成的
编译工具的设置:
AS = $(CROSS_COMPILE)as
# Always use GNU ld
ifneq ($(shell $(CROSS_COMPILE)ld.bfd -v 2> /dev/null),)
LD = $(CROSS_COMPILE)ld.bfd
else
LD = $(CROSS_COMPILE)ld
endif
CC = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
AR = $(CROSS_COMPILE)ar
NM = $(CROSS_COMPILE)nm
LDR = $(CROSS_COMPILE)ldr
STRIP = $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump
LEX = flex
YACC = bison
CROSS_COMPILE编译时传入的:make -C /home/song/arm/optee/build/../u-boot CROSS_COMPILE=" /home/song/arm/optee/build/../toolchains/aarch64/bin/aarch64-linux-gnu-"
2. make过程分析
2.1 编译目标
默认编译目标是_all,在根目录Makefile中:
# That's our default target when none is given on the command line
PHONY := _all
_all:
all: $(ALL-y)
# Timestamp file to make sure that binman always runs
.binman_stamp: $(INPUTS-y) FORCE
ifeq ($(CONFIG_BINMAN),y)
$(call if_changed,binman)
endif
@touch $@
all: .binman_stamp
# Always append INPUTS so that arch config.mk's can add custom ones
INPUTS-y += u-boot.srec u-boot.bin u-boot.sym System.map binary_size_check
# Add optional build target if defined in board/cpu/soc headers
ifneq ($(CONFIG_BUILD_TARGET),)
INPUTS-y += $(CONFIG_BUILD_TARGET:"%"=%)
endif
ifeq ($(CONFIG_INIT_SP_RELATIVE)$(CONFIG_OF_SEPARATE),yy)
INPUTS-y += init_sp_bss_offset_check
endif
ifeq ($(CONFIG_ARCH_ROCKCHIP)_$(CONFIG_SPL_FRAMEWORK),y_)
INPUTS-y += u-boot.img
endif
all 目标依赖$(INPUTS-y),包含 u-boot.srec、u-boot.bin、u-boot.sym、System.map、u-boot.cfg 和 binary_size_check 这几个文件。
最后编译出来的为u-boot.bin,我们看其编译依赖:
ifneq ($(EXT_DTB),)
u-boot-fit-dtb.bin: u-boot-nodtb.bin $(EXT_DTB)
$(call if_changed,cat)
else
u-boot-fit-dtb.bin: u-boot-nodtb.bin $(FINAL_DTB_CONTAINER)
$(call if_changed,cat)
endif
u-boot.bin: u-boot-fit-dtb.bin FORCE
$(call if_changed,copy)
目标 u-boot.bin 依赖于 u-boot-nodtb.bin,命令为$(call if_changed,copy) , 这 里 调 用 了 if_changed , if_changed 是 一 个 函 数 , 这 个 函 数 在scripts/Kbuild.include 中有定义
if_changed = $(if $(strip $(any-prereq) $(arg-check)), \
@set -e; \
$(echo-cmd) $(cmd_$(1)); \
printf '%s\n' 'cmd_$@ := $(make-cmd)' > $(dot-target).cmd, @:)
继续进行分析u-boot-nodtb.bin
u-boot-nodtb.bin: u-boot FORCE
$(call if_changed,objcopy_uboot)
$(BOARD_SIZE_CHECK)
u-boot: $(u-boot-init) $(u-boot-main) $(u-boot-keep-syms-lto) u-boot.lds FORCE
+$(call if_changed,u-boot__)
ifeq ($(CONFIG_KALLSYMS),y)
$(call cmd,smap)
$(call cmd,u-boot__) common/system_map.o
endif
u-boot-init := $(head-y)
u-boot-main := $(libs-y)
arch/arm/Makefile中定义了$(head-y)
head-y := arch/arm/cpu/$(CPU)/start.o
libs-y 都是 uboot 各子目录的集合
**libs-y += boot/
libs-y += cmd/
libs-y += common/
libs-$(CONFIG_OF_EMBED) += dts/
libs-y += env/
libs-y += lib/
libs-y += fs/
libs-y += net/
libs-y += disk/
libs-y += drivers/**
...
libs-y := $(patsubst %/, %/built-in.o, $(libs-y))
patsubst将 libs-y 中的“/”替换为”/built-in.o”,比如“drivers/dma/”就变为了“drivers/dma/built-in.o”,相当于将 libs-y 改为所有子目录中 built-in.o 文件的集合。那么 uboot-main 就等于所有子目录中 built-in.o 的集合。这个规则就相当于将以 u-boot.lds 为链接脚本,将arch/arm/cpu/armv8/start.o 和各个子目录下的 built-in.o 链接在一起生成 u-boot。
built-in.o 是怎么生成的?
以 drivers/gpio/built-in.o 为例,在drivers/gpio/目录下会有个名为.built-in.o.cmd 的文件,此文件内容如下:
cmd_drivers/gpio/built-in.o := rm -f drivers/gpio/built-in.o; /home/song/arm/optee/build/../toolchains/aarch64/bin/aarch64-linux-gnu-ar cDPrsT drivers/gpio/built-in.o
drivers/gpio/built-in.o 这个文件是使用aarch64-linux-gnu-ar命令生成的。
2.2 链接脚本u-boot.lds
在Makefile中定义如下
u-boot.lds: $(LDSCRIPT) prepare FORCE
$(call if_changed_dep,cpp_lds)
ifeq ($(wildcard $(LDSCRIPT)),)
LDSCRIPT := $(srctree)/arch/$(ARCH)/cpu/u-boot.lds
对应的文件为:arch/arm/cpu/armv8/u-boot.lds lds文件规定了编译后各种程序数据在内存中的布局,例如我们经常用到的堆栈也在其中定义,还有异常向量表的位置。具体不展开说了。
prepare是一系列prepare伪目标和动作的组合,完成编译前的准备工作
/home/song/arm/optee/build/../toolchains/aarch64/bin/aarch64-linux-gnu-ld.bfd -z noexecstack -pie --gc-sections -Bstatic --no-dynamic-linker -z notext --build-id=none -Ttext 0x00000000 -o u-boot -T u-boot.lds arch/arm/cpu/armv8/start.o --whole-archive arch/arm/cpu/built-in.o arch/arm/cpu/armv8/built-in.o arch/arm/lib/built-in.o board/emulation/common/built-in.o board/emulation/qemu-arm/built-in.o boot/built-in.o cmd/built-in.o common/built-in.o disk/built-in.o drivers/built-in.o drivers/usb/cdns3/built-in.o drivers/usb/common/built-in.o drivers/usb/dwc3/built-in.o drivers/usb/emul/built-in.o drivers/usb/eth/built-in.o drivers/usb/host/built-in.o drivers/usb/isp1760/built-in.o drivers/usb/mtu3/built-in.o drivers/usb/musb-new/built-in.o drivers/usb/musb/built-in.o drivers/usb/phy/built-in.o drivers/usb/ulpi/built-in.o env/built-in.o fs/built-in.o lib/built-in.o net/built-in.o --no-whole-archive -L /home/song/arm/optee/toolchains/aarch64/bin/../lib/gcc/aarch64-none-linux-gnu/11.3.1 -lgcc -Map u-boot.map; true
aarch64-linux-gnu-ld.bfd进行了链接。这个也是.u-boot.lds.cmd中的内容。
2.3 u-boot.bin生成
从目标文件出发看下cmd命令。.u-boot.bin.cmd中
cmd_u-boot.bin := cp u-boot-nodtb.bin u-boot.bin
.u-boot.bin.cmd 里面定义了一个变量:cmd_u-boot.bin,此变量的值为“cp u-boot-nodtb.bin,依赖关系为:
u-boot.bin --> u-boot-dtb.bin --> u-boot-nodtb.bin + dts/dt.dtb
u-boot.bin”,也就是拷贝一份 u-boot-nodtb.bin 文件,并且重命名为 u-boot.bin,这个就是 u-boot.bin的来源,来自于文件 u-boot-nodtb.bin。
cmd_u-boot-nodtb.bin := /home/song/arm/optee/build/../toolchains/aarch64/bin/aarch64-linux-gnu-objcopy --gap-fill=0xff -j .text -j .secure_text -j .secure_data -j .rodata -j .data -j __u_boot_list -j .rela.dyn -j .got -j .got.plt -j .binman_sym_table -j .text_rest -j .dtb.init.rodata -j .efi_runtime -j .efi_runtime_rel -O binary u-boot u-boot-nodtb.bin && { echo ' tools/relocate-rela '; tools/relocate-rela u-boot-nodtb.bin u-boot; } || { rm -f u-boot-nodtb.bin; false; }
aarch64-linux-gnu-objcopy将 ELF 格式的 u-boot 文件转换为二进制的 u-boot-nodtb.bin 文件.
文件.u-boot.cmd 用于生成 u-boot,使用链接工具arm-linux-gnueabihf-ld.bfd,将各个 builtin.o 文件链接在一起就形成了 u-boot 文件。uboot 在编译的时候会将同一个目录中的所有.c 文件都编译在一起,并命名为 built-in.o,相当于将众多的.c 文件对应的.o 文件集合在一起,这个就是 u-boot 文件的来源。
目标 all 除了 u-boot.bin 以外还有其他的依赖,比如 u-boot.srec 、u-boot.sym 、System.map、u-boot.cfg 和 binary_size_check 等等,这些依赖的生成方法和 u-boot.bin 很类似
编译除了生成u-boot.bin外,还有如下的东西:
- u-boot:编译出来的 ELF 格式的 uboot 镜像文件。
- u-boot.bin:编译出来的二进制格式的 uboot 可执行镜像文件。
- u-boot.cfg:uboot 的另外一种配置文件。
- u-boot.imx:u-boot.bin 添加头部信息以后的文件,NXP 的 CPU 专用文件。
- u-boot.lds:链接脚本。
- u-boot.map:uboot 映射文件,通过查看此文件可以知道某个函数被链接到了哪个地址上。
- u-boot.srec:S-Record 格式的镜像文件。
- u-boot.sym:uboot 符号文件。
- u-boot-nodtb.bin:和 u-boot.bin 一样,u-boot.bin 就是 u-boot-nodtb.bin 的复制文件。
.config文件是make meuconfig自动生成的。
对于u-boot.img和u-boot.bin的区别:
- u-boot.bin:是二进制编译的uboot引导加载程序, 很多image 文件的生成都需要依赖于它;
- u-boot.img:它是给u-boot.bin 加上0x40 Byte 长度的Header。 里面包含加载地址,运行地址,CRC等重要信息, 用来让它的加载程序识别;这个只有在配置CONFIG_SPL_FRAMEWORK时才有效;
芯片烧录选择u-boot.bin还是u-boot.img取决于设备的性质。目前许多SoC/CPU启动ROM都能够加载u-boot.img,读取文件的头部,加载u-boot.bin到内存中并最终执行它。
以上流程总结起来如下图:
参考:
后记:
编译有点记流水账的感觉,很多都是平时遇到有这么个东西,但是还一头雾水,追本溯源才知道原来这个定义或者初始决策藏在这里。