Android 编译介绍
- 1. boot.img 编译
- 1.1 boot.img 组成
- 1.1.1 linux 内核镜像文件介绍
- 1.2 mkbootimg 工具介绍
- 1.2.1 unpack_bootimg 工具介绍
- 1.2.2 kernel编译
- 1.2.3 dtb 反编译
- 1.2.4 Makefile 编译注意事项
- 1.2.4.1 ko 快速编译
- 1.2.4.2 Makefile 添加打印方法
- 1.2.4.3 Makefile 编译工具链确认
- 1.2.4.4 Makefile不同的赋值符号
- 1.2.4.5 Makefile外部模块头文件的检索
- 1.2.4.6 Makefile 编译选项
- 1.2.4.7 Makefile 通配符 wildcard
1. boot.img 编译
1.1 boot.img 组成
Android 产品中,内核格式是Linux标准的zImage,根文件系统采用ramdisk格式。这两者在Android下是直接合并在一起取名为boot.img,会放在一个独立分区当中。这个分区格式是Android自行制定的格式。
Android开发时,最标准的做法是重新编译于内核和根文件系统,然后调用Android给的命令行文件mkbootimg来打包。
常用的就是打包和解包工具:unpackbootimg 和 mkbootimg
1.1.1 linux 内核镜像文件介绍
linux内核镜像几种形式:vmlinux、vmlinux.bin、vmlinuz、zImage、bzImage。
(1) vmlinux:这是编译linux内核直接生成的原始镜像文件,它是由静态链接之后生成的可执行文件,文件类型是linux接受的可运行文件格式之中的一个(ELF、COFF 或 a.out),但是它一般不作为最终的镜像使用,不能直接boot启动,用于生成vmlinuz,可以debug的时候使用。
(2) vmlinuz:vmlinuz是可引导的,压缩的linux内核,“vm”代表的“virtual memory”。
vmlinuz是vmlinux经过gzip和objcopy(*)制作出来的压缩文件。vmlinuz不仅是一个压缩文件,并且在文件的开头部分内嵌有gzip解压缩代码。所以不能用gunzip 或 gzip –dc解包vmlinuz。是可以引导boot启动内核的最终镜像。vmlinuz通常被放置在/boot目录,/boot目录下存放的是系统引导需要的文件,同时vmlinuz文件解压出的vmlinux不带符号表的目标文件,所以一般 /boot 目录下会带一个符号表 System.map 文件。
vmlinuz是一个统称。有两种详细的表现形式:zImage和bzImage(big zImage)。
(3) zImage:这是小内核的旧格式,有指令make zImage生成,仅适用于只有640K以下内存的linux kernel文件。
(4) bzImage: big zImage,需要注意的是这个 bz 与 bzip 没有任何关系,适用于更大的 linux kernel 文件,bzImage是1M以上。现代处理器的 linux 镜像有一部分是生成 bzImage 文件。
(5) vmlinux.bin:与vmlinux相同,但采用可启动的原始二进制文件格式。丢弃所有符号和重定位信息。通过 objcopy -O binary vmlinux vmlinux.bin 从vmlinux生成。
(6) uImage 是在zImage的头部添加了64字节信息,包含了内核版本,加载位置,生成时间,大小信息等,这个头信息描述了该镜像文件的类型、加载位置、生成时间、大小等信息。但是新的 uboot 已经支持了 zImage 启动!所以已经很少用到 uImage 了,除非你用的很古老的 uboot。
使用“ make”、“ make all”、“ make zImage”这些命令就可以编译出 zImage 镜像,具体内容见 arch/arm/Makefile。
1.2 mkbootimg 工具介绍
boot.img 编译时主要相关Makefile文件
include build/make/core/main.mk
build/core/Makefileincluding out_vnd/soong/xxx.mk 通过build/core/Makefile中添加打印可以找到mkbootimg对应的参数:
ifeq ($(BOARD_USES_RECOVERY_AS_BOOT),true)
$(foreach b,$(INSTALLED_BOOTIMAGE_TARGET), $(eval $(call add-dependency,$(b),$(call bootimage-to-kernel,$(b)))))
$(INSTALLED_BOOTIMAGE_TARGET): $(recoveryimage-deps)
$(call pretty,"Target boot image from recovery: $@")
$(call build-recoveryimage-target, $@, $(PRODUCT_OUT)/$(subst .img,,$(subst boot,kernel,$(notdir $@))))
endif # BOARD_USES_RECOVERY_AS_BOOT
$(INSTALLED_RECOVERYIMAGE_TARGET): $(recoveryimage-deps)
$(call build-recoveryimage-target, $@, \
$(if $(filter true, $(BOARD_EXCLUDE_KERNEL_FROM_RECOVERY_IMAGE)),, $(recovery_kernel)))
示例如下:
out/host/linux-x86/bin/mkbootimg \
--kernel out/target/product/$(project_name)/kernel \
--ramdisk out/target/product/$(project_name)/ramdisk-recovery.img \
--cmdline "bootopt=xxx buildvariant=user" \
--base 0x60000000 --board JUST-TEST-EXAMLE \
--dtb out_vnd/target/product/$(project_name)/dtb.img \
--os_version 12 \
--os_patch_level 2022-09-16 \
--kernel_offset 0x00060000 \
--ramdisk_offset 0x20000000 \
--tags_offset 0x08000000 \
--header_version 2 \
--dtb_offset 0x08000000 \
--output out/target/product/$(project_name)/boot.img
1.2.1 unpack_bootimg 工具介绍
下面命令把boot.img解包,所有文件输出到out目录下
unpack_bootimg --boot_img out/target/product/mt6853/boot.img --out boot
boot magic: ANDROID!
kernel_size: 13447872
kernel load address: 0x40060000
ramdisk size: 15558924
ramdisk load address: 0x62100000
second bootloader size: 0
second bootloader load address: 0x00000000
kernel tags load address: 0x49a80000
page size: 2048
os version: 12.0.0
os patch level: 2021-09
boot image header version: 2
product name: CY-X7866-T773-A
command line args: bootopt=64S3,32N2,64N2 buildvariant=userdebug
additional command line args:
recovery dtbo size: 0
recovery dtbo offset: 0x0000000000000000
boot header size: 1650
dtb size: 170514
dtb address: 0x0000000057b80000
参考连接
编译指定目录(mmm)命令,只要有XXX.mk文件都可以编译:
mmm packages/apps/setting/ 2>&1 | tee build.log
1.2.2 kernel编译
编译命令:(-B、-j是可选项,-B表示强制编译,-j表示开的线程数,进行快速编译)
mmm kernel-4.19:kernel -j16 2>&1 | tee kernel.log
mmm kernel-4.19:kernel-menuconfig (生成的.config 在out\target\product\[project]\obj\KERNEL_OBJ)
mmm kernel-4.19:clean-kernel
1.2.3 dtb 反编译
当前驱动代码一般都是在probe函数中解析dts设备树节点来进行驱动注册,所以如何确认编译生成的dtb中是否包含自己所需要的设备节点就很重要,主要分为以下几点来进行确认:
1)找到dtc 工具所在的目录
./../kernel/prebuilts/kernel-build-tools/linux-x86/bin/dtc
- 找到所生成的dtb所在目录
/.../obj/KERNEL_OBJ/arch/arm64/boot/dts/mediatek/x7820_t773.dtb
3)使用dtc工具将 x7820_t773.dtb 生成 A_dts.dts 方便查看
/.../linux-x86/bin/dtc -I dtb -O dts -o A_dts.dts x7820_t773.dtb
note: 使用 dtc -I dts -O dtb -o B_dtb.dtb A_dts.dts 可以将 A_dts 生成B_dtb.dtb
1.2.4 Makefile 编译注意事项
1.2.4.1 ko 快速编译
很多时候我们只需要将某个驱动编译成一个 .ko,然后 adb push 进去,在insmod 即可,并不需要全部编译kernel或者boot.img。这个时候就需要自己写个Makefile,然后直接make即可,在make 编译之前需要 source build/env.sh lunch xxx来配置好环境, Makefile的配置可以参考下面内容:
ifneq ($(KERNELRELEASE),)
KBUILD_EXTRA_SYMBOLS=$(PWD)/Module.symvers
ccflags-y := -I$(PWD)/linux -I$(PWD)/common
ccflags-y += -I$(PWD)/../linux/include/kernel
obj-m := test.o
test_demo-objs := common/test1.o linux/test2.o \
linux/test3.o
else
PWD ?= $(shell pwd)
CLANG_TRIPLE=aarch64-linux-gnu-
MAKE=$(ANDROID_BUILD_TOP)/kernel/build/build-tools/path/linux-x86/make
CROSS_COMPILE=$(ANDROID_BUILD_TOP)/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9/bin/aarch64-linux-android-
KERNELDIR ?= $(ANDROID_BUILD_TOP)/out_vnd/target/product/x6820_h773/obj/KERNEL_OBJ/
LD=$(ANDROID_BUILD_TOP)/kernel/prebuilts-master/clang/host/linux-x86/clang-r416183b/bin/ld.lld
CC=$(ANDROID_BUILD_TOP)/kernel/prebuilts-master/clang/host/linux-x86/clang-r416183b/bin/clang
.PHONY: modules clean
modules:
$(MAKE) CC=$(CC) CLANG_TRIPLE=$(CLANG_TRIPLE) ARCH=arm64 LD=$(LD) CROSS_COMPILE=$(CROSS_COMPILE) \
-C $(KERNELDIR) -I./linux/ M=$(PWD) modules
clean:
@rm -rf *.o *.order *.symvers *.mod *.mod.* .*.o.cmd .*.mod.o.cmd .*.ko.cmd\
.tmp_versions *.ko *.symversions
endif
在当前文件夹下make 的时候,KERNELRELEASE 是没有定义的,就会走 else 分支,给一些编译工具宏进行赋值,然后去 KERNELDIR 根目录下去编译,根目录是top目录,会定义KERNELRELEASE,再次执行到这里就会直接 obj-m 指定要编译的模块即。
1.2.4.2 Makefile 添加打印方法
在使用make命令进行编译的时候经常会遇到需要打印出某些变量的的信息,来定位问题,在Makefile中可以使用下面几种方式来进行添加打印:
SRC := ./src/test1.c
SRC += ./src/test2.c
INC := ./inc/test.h
$(info SRC = $(SRC))
$(warning INC = $(INC))
# 注意:执行到error会直接stop退出
$(error INC = $(INC))
test:
@echo SRC=$(SRC)
@echo INC=$(INC)
$(info SRC = $(SRC))
$(warning INC = $(INC))
#注意:执行到 error 会直接 stop 退出
$(error INC = $(INC))
1.2.4.3 Makefile 编译工具链确认
在编译模块的时候,我们需要确认当前使用的工具链所在得路径,比如GCC, Clang等,可以再 kernel 目录下的Android.mk
中添加打印即可看到相关信息,如:
kernel-4.14/Android.mk
...
ifeq ($(wildcard $(TARGET_PREBUILT_KERNEL)),)
KERNEL_MAKE_DEPENDENCIES := $(shell find $(KERNEL_DIR) -name .git -prune -o -type f | sort)
KERNEL_MAKE_DEPENDENCIES := $(filter-out %/.git %/.gitignore %/.gitattributes,$(KERNEL_MAKE_DEPENDENCIES))
$(TARGET_KERNEL_CONFIG): PRIVATE_DIR := $(KERNEL_DIR)
$(TARGET_KERNEL_CONFIG): $(KERNEL_CONFIG_FILE) $(LOCAL_PATH)/Android.mk
$(TARGET_KERNEL_CONFIG): $(KERNEL_MAKE_DEPENDENCIES)
$(info ######## schan ############)
$(warning) mkdir -p $(dir $@)
$(warning $(PREBUILT_MAKE_PREFIX)$(MAKE) -C $(PRIVATE_DIR) $(KERNEL_MAKE_OPTION) $(KERNEL_DEFCONFIG))
$(BUILT_DTB_OVERLAY_TARGET): $(KERNEL_ZIMAGE_OUT)
.KATI_RESTAT: $(KERNEL_ZIMAGE_OUT)
$(KERNEL_ZIMAGE_OUT): PRIVATE_DIR := $(KERNEL_DIR)
$(KERNEL_ZIMAGE_OUT): $(TARGET_KERNEL_CONFIG) $(KERNEL_MAKE_DEPENDENCIES)
$(warning) mkdir -p $(dir $@)
$(warning $(PREBUILT_MAKE_PREFIX)$(MAKE) -C $(PRIVATE_DIR) $(KERNEL_MAKE_OPTION))
...
然後在源碼目錄下執行make bootimage -j16 2>&1 | tee build.log 編譯即可,編譯完成后查看build.log,可以發現類似下面
的内容:
/.../prebuilts/build-tools/linux-x86/bin/make -j32 -C kernel-4.14 O=/.../obj/KERNEL_OBJ \
ARCH=arm64 CROSS_COMPILE=aarch64-linux-androidkernel- CLANG_TRIPLE=aarch64-linux-gnu- LD=ld.lld \
LD_LIBRARY_PATH=/.../prebuilts/... \
NM=llvm-nm OBJCOPY=llvm-objcopy CC=clang ROOTDIR=/.../android_src \
PATH=/.../prebuilts/clang/host/linux-x86/clang-r383902/bin:...
上面打印出来的信息 “-C kernel-4.1.4” 其中 -C 表示进入到kernel-4.14目录执行(解析)其中的makefile, 所以这个时候就是去编译kernel了。
1.2.4.4 Makefile不同的赋值符号
= 是最基本的赋值
:= 是覆盖之前的值
?= 是如果没有被赋值过就赋予等号后面的值
+= 是添加等号后面的值
其中 = 和 := 都是给变量赋值,他们之间的区别是什么呢?
代码在make的时候,是将整个 Makefile 展开之后再去决定变量的值,因此获取的变量的值会是它最终的赋值。
但是使用 := 符号的话,获得的变量值就是它当时的变量值。
举个例子说明:
x=kitty
y=hello $(x)
x=mickey
最终 y 的值是 hello mickey;
而如果是下面的情况
x:=kitty
y:=hello $(x)
x:=mickey
则 y 的最终值是 hello kitty
1.2.4.5 Makefile外部模块头文件的检索
当编译的目标模块依赖多个头文件时,kbuild 对头文件的搜索位置有如下规定:
(1) 直接放置在 Makefile 同级的目录下,在编译时当前目录会被添加到头文件搜索目录。
(2) 放置在系统目录,这个系统目录是源代码目录中的 include。
(3) 与通用的 Makefile 一样,使用 -I$(DIR) 来指定,不同的是,代表编译选项的变量是固定的,为 ccflag,因为当前 Makefile 相当于是镶嵌到 Kbuild 系统中进行编译,所以必须遵循 Kbuild 中的规定.
一般的用法是这样的:
ccflags-y += -I$(DIR)/include
1.2.4.6 Makefile 编译选项
在之前的版本中,编译的选项由 EXTRA_CFLAGS, EXTRA_AFLAGS 和 EXTRA_LDFLAGS 修改成了 ccflags-y asflags-y 和 ldflags-y.
(1) ccflags-y asflags-y 和 ldflags-y
这三个变量的值分别对应编译、汇编、链接时的参数。
同时,所有的 ccflags-y asflags-y 和 ldflags-y 这三个变量只对有定义的 Makefile 中使用,简而言之,这些flag在Makefile树中不会有继承效果,Makefile之间相互独立。
(2) subdir-ccflags-y, subdir-asflags-y
这两个编译选项与ccflags-y和asflags-y效果是一致的,只是添加了subdir-前缀,意味着这两个编译选项对本目录和所有的子目录都有效。
1.2.4.7 Makefile 通配符 wildcard
wildcard 用来明确表示通配符。因为在 Makefile 里,变量实质上就是 C/C++ 中的宏,也就是说,如果一个表达式如 objs = *.o ,则 objs 的值就是 *.o ,而不是表示所有的 .o 文件。
如果要使用通配符,那么就要使用 wildcard 来声明 * 这个符号,使 * 符号具有通配符的功能。
如下:
建立两个 *.c 文件,如 test1.c 和 test2.c ,Makefile 如下:
src = $(wildcard *.c /test/src/*.c)
all:
@echo $(src)
运行make后会输出 test1.c test2.c 和 、test/src 目录下的所有 C 文件。