本篇的主要目的是想通過分析Makefile,了解openwrt編譯過程。着重關注以下幾點:
0. openwrt目錄結構
- 主Makefile的解析過程,各子目錄的目標生成。
- kernel編譯過程
- firmware的生成過程
- 軟件包的編譯過程
openwrt目錄結構
官方源下載速度太度,我從github上clone了openwrt的代碼倉庫。
git clone https://github.com/openwrt-mirror/openwrt.git
上圖是openwrt目錄結構,其中第一行是原始目錄,第二行是編譯過程中生成的目錄。各目錄的作用是:
-
tools - 編譯時需要一些工具, tools里包含了獲取和編譯這些工具的命令。里面是一些Makefile,有的可能還有patch。每個Makefile里都有一句
$(eval $(call HostBuild))
,表示編譯這個工具是為了在主機上使用的。 -
toolchain - 包含一些命令去獲取kernel headers, C library, bin-utils, compiler, debugger
-
target - 各平台在這個目錄里定義了firmware和kernel的編譯過程。
-
package - 包含針對各個軟件包的Makefile。openwrt定義了一套Makefile模板,各軟件參照這個模板定義了自己的信息,如軟件包的版本、下載地址、編譯方式、安裝地址等。
-
include - openwrt的Makefile都存放在這里。
-
scripts - 一些perl腳本,用於軟件包管理。
-
dl - 軟件包下載后都放到這個目錄里
-
build_dir - 軟件包都解壓到build_dir/里,然后在此編譯
-
staging_dir - 最終安裝目錄。tools, toolchain被安裝到這里,rootfs也會放到這里。
-
feeds -
-
bin - 編譯完成之后,firmware和各ipk會放到此目錄下。
main Makefile
openwrt根目錄下的Makefile是執行make命令時的入口。從這里開始分析。
world:
ifndef ($(OPENWRT_BUILD),1)
# 第一個邏輯
...
else
# 第二個邏輯
...
endif
上面這段是主Makefile的結構,可以得知:
- 執行make時,若無任何目標指定,則默認目標是world
- 執行make時,無參數指定,則會進入第一個邏輯。如果執行命令
make OPENWRT_BUILD=1
,則直接進入第二個邏輯。
編譯時一般直接使用make V=s -j5
這樣的命令,不會指定OPENWRT_BUILD變量
第一個邏輯
override OPENWRT_BUILD=1
export OPENWRT_BUILD
更改了OPENWRT_BUILD變量的值。這里起到的作用是下次執行make時,會進入到第二邏輯中。
toplevel.mk中的 %:: 解釋world目標的規則。
prereq:: prepare-tmpinfo .config
@+$(MAKE) -r -s tmp/.prereq-build $(PREP_MK)
@+$(NO_TRACE_MAKE) -r -s $@
%::
@+$(PREP_MK) $(NO_TRACE_MAKE) -r -s prereq
@( \
cp .config tmp/.config; \
./scripts/config/conf --defconfig=tmp/.config -w tmp/.config Config.in > /dev/null 2>&1; \
if ./scripts/kconfig.pl '>' .config tmp/.config | grep -q CONFIG; then \
printf "$(_R)WARNING: your configuration is out of sync. Please run make menuconfig, oldconfig or defconfig!$(_N)\n" >&2; \
fi \
)
@+$(ULIMIT_FIX) $(SUBMAKE) -r $@
執行 make V=s
時,上面這段規則簡化為:
prereq:: prepare-tmpinfo .config
@make -r -s tmp/.prereq-build
@make V=ss -r -s prereq
%::
@make V=s -r -s prereq
@make -w -r world
可見其中最終又執行了prereq和world目標,這兩個目標都會進入到第二邏輯中。
第二邏輯
首先就引入了target, package, tools, toolchain這四個關鍵目錄里的Makefile文件
include target/Makefile
include package/Makefile
include tools/Makefile
include toolchain/Makefile
這些子目錄里的Makefile使用include/subdir.mk里定義的兩個函數來動態生成規則,這兩個函數是subdir和stampfile
stampfile
拿target/Makefile舉例:
$(eval \((call stampfile,\)(curdir),target,prereq,.config))
會生成規則:
target/stamp-prereq:=$(STAGING_DIR)/stamp/.target_prereq
$$(target/stamp-prereq): $(TMP_DIR)/.build .config
@+$(SCRIPT_DIR)/timestamp.pl -n $$(target/stamp-prereq) target .config || \
make $$(target/flags-prereq) target/prereq
@mkdir -p $$$$(dirname $$(target/stamp-prereq))
@touch $$(target/stamp-prereq)
$$(if $(call debug,target,v),,.SILENT: $$(target/stamp-prereq))
.PRECIOUS: $$(target/stamp-prereq) # work around a make bug
target//clean:=target/stamp-prereq/clean
target/stamp-prereq/clean: FORCE
@rm -f $$(target/stamp-prereq)
所以可以簡單的看作: $(eval \((call stampfile,\)(curdir),target,prereq,.config)) 生成了目標 $(target/stamp-prereq)
- 對於target分別生成了:$(target/stamp-preq), $(target/stamp-copile), $(target/stamp-install)
- toolchain : $(toolchain/stamp-install)
- package : \((package/stamp-preq),\)(package/stamp-cleanup), \((package/stamp-compile),\)(package/stamp-install)
- tools : $(tools/stamp-install)
subdir
subdir這個函數寫了一大堆東西,看起來很復雜 。
$(call subdir, target) 會遍歷下的子目錄,執行 make -C 操作。這樣就切入子目錄中去了。
目錄變量
幾個重要的目錄路徑:
- KERNEL_BUILD_DIR
build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/linux-ramips_mt7620a/linux-3.14.18
- LINUX_DIR
build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/linux-ramips_mt7620a/linux-3.14.18
- KDIR
build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/linux-ramips_mt7620a
- BIN_DIR
bin/ramips
Makefile中包含了rules.mk, target.mk等.mk文件,這些文件中定義了許多變量,有些是路徑相關的,有些是軟件相關的。這些變量在整個Makefile工程中經常被用到,
- TARGET_ROOTFS_DIR
build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2
- BUILD_DIR
build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2
- STAGING_DIR_HOST
staging_dir/toolchain-mipsel_24kec+dsp_gcc-4.8-linaro_uClibc-0.9.33.2
- TARGET_DIR
build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/root-ramips
kernel 編譯:
target/linux/ramips/Makefile: $(eval $(call BuildTarget))
target/linux/Makefile : export TARGET_BUILD=1
include/target.mk:
ifeq ($(TARGET_BUILD),1)
include $(INCLUDE_DIR)/kernel-build.mk
BuildTarget?=$(BuildKernel)
endif
BuildKernel是include/kernel-build.mk定義的一個多行變量,其中描述了如何編譯內核, 主要關注其中install規則的依賴鏈:
$(KERNEL_BUILD_DIR)/symtab.h: FORCE
rm -f $(KERNEL_BUILD_DIR)/symtab.h
touch $(KERNEL_BUILD_DIR)/symtab.h
+$(MAKE) $(KERNEL_MAKEOPTS) vmlinux
...
$(LINUX_DIR)/.image: $(STAMP_CONFIGURED) $(if $(CONFIG_STRIP_KERNEL_EXPORTS),$(KERNEL_BUILD_DIR)/symtab.h) FORCE
$(Kernel/CompileImage)
$(Kernel/CollectDebug)
touch $$@
install: $(LINUX_DIR)/.image
+$(MAKE) -C image compile install TARGET_BUILD=
1. 觸發make vmlinux命令生成vmlinux: install --> $(LINUX_DIR)/.image --> $(KERNEL_BUILD_DIR)/symtab.h --> `$(MAKE) $(KERNEL_MAKEOPTS) vmlinux`
2. 對vmlinux做objcopy, strip操作: $(LINUX_DIR)/.image --> $(Kernel/CompileImage) --> $(call Kernel/CompileImage/Default) --> $(call Kernel/CompileImage/Default)
$(KERNEL_CROSS)objcopy -O binary $(OBJCOPY_STRIP) -S $(LINUX_DIR)/vmlinux $(LINUX_KERNEL)$(1)
--> build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/linux-ramips_mt7620a/vmlinux
$(KERNEL_CROSS)objcopy $(OBJCOPY_STRIP) -S $(LINUX_DIR)/vmlinux $(KERNEL_BUILD_DIR)/vmlinux$(1).elf
--> build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/linux-ramips_mt7620a/vmlinux.elf
$(CP) $(LINUX_DIR)/vmlinux $(KERNEL_BUILD_DIR)/vmlinux.debug
--> build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/linux-ramips_mt7620a/vmlinux.debug
生成firmware
firmware由kernel和rootfs兩個部分組成,要對兩個部分先分別處理,然后再合並成一個.bin文件。先看一下這個流程。
"target/linux/ramips/image/Makefile" 文件中的最后一句:$(eval $(call BuildImage))
,將BuildImage展開在這里。BuildImage定義在 include/image.mk 文件中,其中定義了數個目標的規則。
define BuildImage
compile: compile-targets FORCE
**$(call Build/Compile)**
install: compile install-targets FORCE
...
$(call Image/BuildKernel) ## 處理vmlinux
...
$(call Image/mkfs/squashfs) ## 生成squashfs,並與vmlinux合並成一個.bin文件
...
endef
處理vmlinux: Image/BuildKernel
target/linux/ramips/image/Makefile:
define Image/BuildKernel
cp $(KDIR)/vmlinux.elf $(BIN_DIR)/$(VMLINUX).elf
cp $(KDIR)/vmlinux $(BIN_DIR)/$(VMLINUX).bin
$(call CompressLzma,$(KDIR)/vmlinux,$(KDIR)/vmlinux.bin.lzma)
$(call MkImage,lzma,$(KDIR)/vmlinux.bin.lzma,$(KDIR)/uImage.lzma)
cp $(KDIR)/uImage.lzma $(BIN_DIR)/$(UIMAGE).bin
ifneq ($(CONFIG_TARGET_ROOTFS_INITRAMFS),)
cp $(KDIR)/vmlinux-initramfs.elf $(BIN_DIR)/$(VMLINUX)-initramfs.elf
cp $(KDIR)/vmlinux-initramfs $(BIN_DIR)/$(VMLINUX)-initramfs.bin
$(call CompressLzma,$(KDIR)/vmlinux-initramfs,$(KDIR)/vmlinux-initramfs.bin.lzma)
$(call MkImage,lzma,$(KDIR)/vmlinux-initramfs.bin.lzma,$(KDIR)/uImage-initramfs.lzma)
cp $(KDIR)/uImage-initramfs.lzma $(BIN_DIR)/$(UIMAGE)-initramfs.bin
endif
$(call Image/Build/Initramfs)
endef
lzma壓縮內核
build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/linux-ramips_mt7620a/ 目錄中:
lzma e vmlinux -lc1 -lp2 -pb2 vmlinux.bin.lzma
MkImage
build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/linux-ramips_mt7620a/ 目錄中:
mkimage -A mips -O linux -T kernel -C lzma -a 0x80000000 -e 0x80000000 -n "MIPS OpenWrt Linux-3.14.18" -d vmlinux.bin.lzma uImage.lzma
copy
VMLINUX:=$(IMG_PREFIX)-vmlinux --> openwrt-ramips-mt7620a-vmlinux
UIMAGE:=$(IMG_PREFIX)-uImage --> openwrt-ramips-mt7620a-uImage
cp $(KDIR)/uImage.lzma $(BIN_DIR)/$(UIMAGE).bin
把uImage.lzma復制到bin/ramips/目錄下:
cp $(KDIR)/uImage.lzma bin/ramips/openwrt-ramips-mt7620a-uImage
制作squashfs,生成.bin: $(call Image/mkfs/squashfs)
define Image/mkfs/squashfs
@mkdir -p $(TARGET_DIR)/overlay
$(STAGING_DIR_HOST)/bin/mksquashfs4 $(TARGET_DIR) $(KDIR)/root.squashfs -nopad -noappend -root-owned -comp $(SQUASHFSCOMP) $(SQUASHFSOPT) -processors $(if $(CONFIG_PKG_BUILD_JOBS),$(CONFIG_PKG_BUILD_JOBS),1)
$(call Image/Build,squashfs)
endif
mkdir -p $(TARGET_DIR)/overlay
mkdir -p build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/root-ramips/overlay
mksquashfs4
$(STAGING_DIR_HOST)/bin/mksquashfs4 $(TARGET_DIR) $(KDIR)/root.squashfs -nopad -noappend -root-owned -comp $(SQUASHFSCOMP) $(SQUASHFSOPT) -processors $(if $(CONFIG_PKG_BUILD_JOBS),$(CONFIG_PKG_BUILD_JOBS),1)
制作squashfs文件系統,生成root.squashfs:
mksquashfs4 root-ramips root.squashfs -nopad -noappend -root-owned -comp gzip -b 256k -p '/dev d 755 0 0' -p '/dev/console c 600 0 0 5 1' -processors 1
$(call Image/Build,squashfs)
在 target/linux/ramips/image/Makefile 中:
define Image/Build
$(call Image/Build/$(1))
dd if=$(KDIR)/root.$(1) of=$(BIN_DIR)/$(IMG_PREFIX)-root.$(1) bs=128k conv=sync
$(call Image/Build/Profile/$(PROFILE),$(1))
endef
- dd if=\((KDIR)/root.squashfs of=\)(BIN_DIR)/$(IMG_PREFIX)-root.squashfs bs=128k conv=sync
dd if=build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/linux-ramips_mt7620a/root.squashfs of=bin/ramips/openwrt-ramips-mt7620-root.squashfs bs=128k conv=sync
- \((call Image/Build/Profile/\)(PROFILE),squashfs)
target/linux/ramips/mt7620a/profiles/00-default.mk, 中調用 Profile 函數:$(eval $(call Profile,Default))
include/target.mk 中定義了 Profile 函數, 其中令 PROFILE=Default
define Image/Build/Profile/Default
$(call Image/Build/Profile/MT7620a,$(1))
...
endef
規則依賴序列如下:
$(call Image/Build/Profile/$(PROFILE),squashfs)
--> $(call BuildFirmware/Default8M/squashfs,squashfs,mt7620a,MT7620a)
--> $(call BuildFirmware/OF,squashfs,mt7620a,MT7620a,8060928)
--> $(call MkImageLzmaDtb,mt7620a,MT7620a)
--> $(call PatchKernelLzmaDtb,mt7620a,MT7620a)
--> $(call MkImage,lzma,$(KDIR)/vmlinux-mt7620a.bin.lzma,$(KDIR)/vmlinux-mt7620a.uImage)
--> $(call MkImageSysupgrade/squashfs,squashfs,mt7620a,8060928)
其中的主要步驟:
- 復制: cp $(KDIR)/vmlinux $(KDIR)/vmlinux-mt7620a
- 生成dtb文件: $(LINUX_DIR)/scripts/dtc/dtc -O dtb -o $(KDIR)/MT7620a.dtb ../dts/MT7620a.dts
- 將內核與dtb文件合並:$(STAGING_DIR_HOST)/bin/patch-dtb $(KDIR)/vmlinux-mt7620a $(KDIR)/MT7620a.dtb
- 使用lzma壓縮:\((call CompressLzma,\)(KDIR)/vmlinux-mt7620a,$(KDIR)/vmlinux-mt7620a.bin.lzma)
- 將lzma壓縮后的文件經過mkimage工具處理,即在頭部添加uboot可識別的信息。
接下來就是合並生成firmware固件了:
MkImageSysupgrade/squashfs, squashfs, mt7620a,8060928
cat vmlinux-mt7620a.uImage root.squashfs > openwrt-ramips-mt7620-mt7620a-squashfs-sysupgrade.bin
--> 制作squashfs bin文檔, 並確認它的大小 < 8060928 才是有效的,否則報錯。
總結: 整個流程下來,其實最煩索的還是對內核生成文件vmlinux的操作,經過了objcopy, patch-dtb, lzma, mkimage 等過程生成一個uImage,再與mksquashfs工具制作的文件系統rootfs.squashfs合並。