Rockchip RK3588 - Rockchip Linux Recovery updateEngine源码分析
----------------------------------------------------------------------------------------------------------------------------
开发板 :ArmSoM-Sige7
开发板
eMMC
:64GB
LPDDR4
:8GB
显示屏 :15.6
英寸HDMI
接口显示屏
u-boot
:2017.09
linux
:5.10
----------------------------------------------------------------------------------------------------------------------------
本文内容基于Rockchip Linux SDK
,如果你对Rockchip Linux SDK
不了解的前提下,请先阅读以下两篇文章:
一、Recovery
模式
1.1 简介
Rockchip Linux
平台支持两种启动方案,Recovery
模式和Linux A/B
模式:
Recovery
模式,设备上有一个单独的分区(recovery
)用于升级操作;Linux A/B
模式,设备上有两套固件,可切换使用。
这两种启动模式各有优缺点,用户根据需求选择使用。本篇博客主要针对Recovery
模式进行深入剖析。
1.1.1 Recovery
模式概述
Recovery
模式是在设备上多一个recovery
分区,该分区由kernel + dtb + ramdisk
组成,主要用于升级操作。
u-boot
会根据misc
分区存放的字段来判断将要引导的系统是Normal
系统还是Recovery
系统。
由于系统的独立性,所以Recovery
模式能保证升级的完整性,即升级过程被中断,如异常掉电,升级仍然能继续执行。
优点:
- 能保证升级的完整性;
缺点:
- 系统多了一个分区,该分区仅用于升级;
- 升级过程必须重启进入
Recovery
系统,不能在Normal
系统直接进行升级。
注意:实际测试发现,如果不升级recovery
分区,是可以直接在Normal
系统升级其它分区的;但是直接在Normal
系统下升级如果出现升级中断可能会造成系统损坏,比如Normal
系统运行的根文件系统损坏,这样就Normal
系统就无法正常进入了。
1.1.2 升级方式
Rockchip Linux
平台有两套升级方案代码;
升级方案 | 升级方案代码路径 | 是否支持Recovery启动模式升级 | 是否支持A/B启动模式升级 | 简介 |
---|---|---|---|---|
updateEngine | external/recovery/update_engine |
支持 | 支持 | RV1126/RV1109平台使用 实际测试也适用于RK3588 |
rkupdate | external/rkupdate | 支持 | 不支持 | 其它平台使用 |
updateEngine
源码:位于目录 external/recovery/update_engine
,生成updateEngine
二进制bin
程序,解析update.img
固件中各个分区数据,并执行对各分区升级的关键程序序;
rkupdate
源码:位于目录 external/rkupdate
,生成rkupdate
二进制bin
程序,解析update.img
固件中各个分区数据,并执行对各分区升级的关键程序;
external/recovery
:位于目录 external/recovery
,生成recovery
二进制bin
程序,其中recovery
二进制bin
程序部会根据编译配置调用updateEngine
或者rkupdate
进行升级。
1.2 配置recovery
准备工作:请参考《Rockchip RK3588 - Rockchip Linux SDK
编译》下载SDK
。
1.2.1 设置环境变量
进入buildroot
目录,执行如下命令:
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot$ sudo -s source envsetup.sh
选择一个平台的recovery
配置,这里选择rockchip_rk3588_recovery
,文件rockchip_rk3588_recovery_defconfig
位于configs
目录下,内容如下:
#include "base/base.config"
#include "base/recovery.config" // 位于configs/rockchip/base/recovery.config
#include "chips/rk3588_aarch64.config"
执行结果如下:
Top of tree: /work/sambashare/rk3588/armsom/armsom-rk3588-bsp
Pick a board:
.......
57. rockchip_rk3588_recovery
Which would you like? [1]: 57
make: 进入目录“/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot”
GEN /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/Makefile
/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/build/parse_defconfig.sh /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/configs/rockchip_rk3588_recovery_defconfig /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/.config.in
Parsing defconfig: /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/configs/rockchip_rk3588_recovery_defconfig
Using configs/rockchip/base/kernel.config as base #基准配置
Merging configs/rockchip/fs/e2fs.config
Merging configs/rockchip/base/common.config
Merging configs/rockchip/base/base.config
Merging configs/rockchip/base/kernel.config
Merging configs/rockchip/fs/e2fs.config
Merging configs/rockchip/base/common.config
Merging configs/rockchip/fs/vfat.config
Merging configs/rockchip/base/recovery.config #recovery配置
Merging configs/rockchip/chips/rk3588.config
Value of BR2_ROOTFS_OVERLAY is redefined by configs/rockchip/chips/rk3588.config:
Previous value: BR2_ROOTFS_OVERLAY="board/rockchip/common/base"
Modify value: BR2_ROOTFS_OVERLAY+="board/rockchip/rk3588/fs-overlay/"
New value: BR2_ROOTFS_OVERLAY="board/rockchip/common/base board/rockchip/rk3588/fs-overlay/"
Merging configs/rockchip/chips/rk3588_aarch64.config
Merging /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/configs/rockchip_rk3588_recovery_defconfig
#
# merged configuration written to /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/.config.in (needs make)
#
BR2_DEFCONFIG='' KCONFIG_AUTOCONFIG=/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/buildroot-config/auto.conf KCONFIG_AUTOHEADER=/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/buildroot-config/autoconf.h KCONFIG_TRISTATE=/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/buildroot-config/tristate.config BR2_CONFIG=/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/.config HOST_GCC_VERSION="9" BASE_DIR=/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery SKIP_LEGACY= CUSTOM_KERNEL_VERSION="5.10" BR2_DEFCONFIG=/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/configs/rockchip_rk3588_recovery_defconfig /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/buildroot-config/conf --defconfig=/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/.config.in Config.in
#
# configuration written to /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/.config
#
make: 离开目录“/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot”
生成.config
配置文件,位于output/rockchip_rk3588_recovery
目录。
1.2.2 功能配置
进行recovery
配置:
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot$ sudo make menuconfig
1.2.2.1 开启updateEngine
升级方式
配置如下选项:
Target packages --->
Hardware Platforms --->
[*] Rockchip Platform --->
Rockchip BSP packages --->
[*] Rockchip recovery for linux # BR2_PACKAGE_RECOVERY
[ ] No UI for recovery # BR2_PACKAGE_RECOVERY_NO_UI
[ ] Linux AB bool control # BR2_PACKAGE_RECOVERY_BOOTCONTROL
Linux A/B bringup features. (successful_boot) --->
(X) successful_boot # BR2_PACKAGE_RECOVERY_SUCCESSFUL_BOOT
( ) retry time # BR2_PACKAGE_RECOVERY_RETRY
choice the update bin of recovery. (updateEngine) --->
() rkupdate # BR2_PACKAGE_RECOVERY_USE_RKUPDATE,如果选中自动选中recovery bin
(*) updateEngine # BR2_PACKAGE_RECOVERY_USE_UPDATEENGINE、如果选中自动选中updateEngine bin
[*] recovery bin # BR2_PACKAGE_RECOVERY_RECOVERYBIN
-*- updateEngine bin # BR2_PACKAGE_RECOVERY_UPDATEENGINEBIN
[ ] Enable static # BR2_PACKAGE_RECOVERY_STATIC
注意:配置源码位于package/rockchip/recovery/Config.in
。
1.2.2.2 开启rkupdate
升级方式
配置如下选项:
Target packages --->
Hardware Platforms --->
[*] Rockchip Platform --->
Rockchip BSP packages --->
[*] Rockchip rkupdate for linux # BR2_PACKAGE_RKUPDATE,rkupdate升级方式
[ ] update signature firmware (NEW)
[ ] simulate abnormal power off during updating fw (NEW)
[ ] Enable static (NEW)
注意:配置源码位于package/rockchip/rkupdate/Config.in
。
1.2.2.3 保存配置
最终生成配置文件output/rockchip_rk3588_recovery/.config
,包含配置项:
#updateEngine升级方式配置项
BR2_PACKAGE_RECOVERY=y #开启升级相关功能
BR2_PACKAGE_RECOVERY_SUCCESSFUL_BOOT=y #引导方式为successful,升级直至成功启动
BR2_PACKAGE_RECOVERY_USE_UPDATEENGINE=y #使用新升级程序,不配置则默认使用原有升级流程
BR2_PACKAGE_RECOVERY_RECOVERYBIN=y #开启recovery bin 文件
BR2_PACKAGE_RECOVERY_UPDATEENGINEBIN=y #编译新升级程序
#rkupdate升级方式配置项
BR2_PACKAGE_RKUPDATE=y
我们需要将以上这些配置追加到configs/rockchip/base/recovery.config
文件中(注意:BR2_PACKAGE_RECOVERY
、BR2_PACKAGE_RKUPDATE
默认已经配置),这样默认就会开启这些配置。
注意:对于rockchip_rk3588_defconfig
配置文件,是不可以配置这些的。
二、updateEngine in & mk
分析
接下来我们来研究《配置recovery
》小节中我们添加了如下这些配置项究竟实现了什么功能;
#updateEngine升级方式配置项
BR2_PACKAGE_RECOVERY=y #开启升级相关功能
BR2_PACKAGE_RECOVERY_SUCCESSFUL_BOOT=y #升级直至成功启动
BR2_PACKAGE_RECOVERY_USE_UPDATEENGINE=y #使用新升级程序,不配置则默认使用原有升级流程
BR2_PACKAGE_RECOVERY_RECOVERYBIN=y #开启recovery bin 文件
BR2_PACKAGE_RECOVERY_UPDATEENGINEBIN=y #编译新升级程序
注意:本节仅仅分析updateEngine
升级方式。
假如你对Buildroot
框架有深入了解的话,你就很容易明白软件包recovery
是Rockchip
为Buildroot
扩展的软件包,用来解决系统升级的问题。
有关如何在Buildroot
系统自定义软件包可以参考:《Rockchip RK3588 - Rockchip Linux SDK Buildroot
文件系统构建》。
在为Buildroot
自定义软件包时,在软件包目录下通常为引入.in
和.mk
文件;
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot$ ll package/rockchip/recovery/
-rw-r--r-- 1 root root 1596 6月 9 12:58 Config.in
-rw-r--r-- 1 root root 81 6月 9 12:58 recovery.hash
-rw-r--r-- 1 root root 2400 6月 9 12:58 recovery.mk
-rwxr-xr-x 1 root root 273 6月 9 12:58 S40recovery*
2.1 Config.in
我们首先从Config.in
文件入手,该文件实际上就是定义make menuconfig
支持的配置选项;
config BR2_PACKAGE_RECOVERY
bool "Rockchip recovery for linux"
depends on BR2_TOOLCHAIN_HAS_THREADS # libpthread-stubs 此配置项依赖于BR2_TOOLCHAIN_HAS_THREADS(其开启的情况下才可用)
select BR2_PACKAGE_LIBDRM # 如果启动,自动选择启用以下列出的其他软件包和库
select BR2_PACKAGE_LIBPNG
select BR2_PACKAGE_LIBPTHREAD_STUBS
select BR2_PACKAGE_LIBZ
select BR2_PACKAGE_DOSFSTOOLS
select BR2_PACKAGE_DOSFSTOOLS_FSCK_FAT
select BR2_PACKAGE_DOSFSTOOLS_MKFS_FAT
select BR2_PACKAGE_E2FSPROGS
select BR2_PACKAGE_E2FSPROGS_RESIZE2FS
select BR2_PACKAGE_LIBCURL
select BR2_PACKAGE_OPENSSL
select BR2_PACKAGE_BZIP2
help
Rockchip recovery for linux.
if BR2_PACKAGE_RECOVERY
config BR2_PACKAGE_RECOVERY_NO_UI # 不开启UI
bool "No UI for recovery"
help
No UI for recovery
config BR2_PACKAGE_RECOVERY_BOOTCONTROL # Linux A/B模式
bool "Linux AB bool control"
choice # 接下来的配置项将是一个选择项,用户可以从其中选择一个选项
default BR2_PACKAGE_RECOVERY_SUCCESSFUL_BOOT # 默认选项
prompt "Linux A/B bringup features."
config BR2_PACKAGE_RECOVERY_SUCCESSFUL_BOOT
bool "successful_boot" # 直至成功启动
config BR2_PACKAGE_RECOVERY_RETRY # 重试次数
bool "retry time"
endchoice
choice # 升级方式选择
default BR2_PACKAGE_RECOVERY_USE_RKUPDATE # 默认使用rkupdate
prompt "choice the update bin of recovery."
config BR2_PACKAGE_RECOVERY_USE_RKUPDATE # recovery bin
bool "rkupdate"
select BR2_PACKAGE_RECOVERY_RECOVERYBIN
config BR2_PACKAGE_RECOVERY_USE_UPDATEENGINE # updateEngine bin
bool "updateEngine"
select BR2_PACKAGE_RECOVERY_UPDATEENGINEBIN
endchoice
config BR2_PACKAGE_RECOVERY_RECOVERYBIN
bool "recovery bin"
config BR2_PACKAGE_RECOVERY_UPDATEENGINEBIN
bool "updateEngine bin"
config BR2_PACKAGE_RECOVERY_STATIC
bool "Enable static"
default y if BR2_STATIC_LIBS
select BR2_PACKAGE_LIBCURL_STATIC
select BR2_PACKAGE_LIBDRM_STATIC
select BR2_PACKAGE_LIBPNG_STATIC
endif
比如我们勾选了updateEngine
,由于选中该配置项的同时,会默认选中updateEngine bin
配置项;
因此在生成的output/rockchip_rk3588_recovery/.config
配置文件会配置:
BR2_PACKAGE_RECOVERY_USE_UPDATEENGINE=y
BR2_PACKAGE_RECOVERY_UPDATEENGINEBIN=y
2.2 recovery.mk
buildroot
编译recovery
所需要的设置recovery.mk
,包括源码位置、安装目录、权限设置等。
################################################################################
#
# Rockchip Recovery For Linux
#
################################################################################
RECOVERY_VERSION = develop
RECOVERY_SITE = $(TOPDIR)/../external/recovery
RECOVERY_SITE_METHOD = local
RECOVERY_LICENSE = ROCKCHIP
RECOVERY_LICENSE_FILES = NOTICE
RECOVERY_CFLAGS = $(TARGET_CFLAGS) -I. \
-fPIC \
-lpthread \
-lcurl \
-lssl \
-lcrypto \
-lbz2
RECOVERY_MAKE_ENV = $(TARGET_MAKE_ENV)
RECOVERY_DEPENDENCIES += libpthread-stubs util-linux libcurl openssl
ifeq ($(BR2_PACKAGE_RECOVERY_NO_UI),y)
RECOVERY_MAKE_ENV += RecoveryNoUi=true
else
RECOVERY_CFLAGS += -lpng -ldrm -lz -lm -I$(STAGING_DIR)/usr/include/libdrm
RECOVERY_DEPENDENCIES += libpng libdrm libzlib
endif
# For static link with libcurl
ifeq ($(BR2_PACKAGE_RTMPDUMP)$(BR2_PACKAGE_RECOVERY_STATIC),yy)
RECOVERY_CFLAGS += -lrtmp
RECOVERY_DEPENDENCIES += rtmpdump
endif
ifeq ($(BR2_PACKAGE_RECOVERY_USE_RKUPDATE),y)
RECOVERY_CFLAGS += -DUSE_RKUPDATE=ON
endif
ifeq ($(BR2_PACKAGE_RECOVERY_USE_UPDATEENGINE),y)
RECOVERY_CFLAGS += -DUSE_UPDATEENGINE=ON
endif
ifeq ($(BR2_PACKAGE_RECOVERY_SUCCESSFUL_BOOT),y)
RECOVERY_CFLAGS += -DSUCCESSFUL_BOOT=ON
endif
ifeq ($(BR2_PACKAGE_RECOVERY_RETRY),y)
RECOVERY_CFLAGS += -DRETRY_BOOT=ON
endif
ifeq ($(BR2_PACKAGE_RECOVERY_STATIC),y)
RECOVERY_CFLAGS += -static
endif
define RECOVERY_BUILD_CMDS
$(RECOVERY_MAKE_ENV) $(MAKE) -C $(@D) \
CC="$(TARGET_CC)" CFLAGS="$(RECOVERY_CFLAGS)"
endef
ifeq ($(BR2_PACKAGE_RECOVERY_RECOVERYBIN),y)
define RECOVERYBIN_INSTALL_TARGET
$(INSTALL) -D -m 755 $(@D)/recovery $(TARGET_DIR)/usr/bin/
mkdir -p $(TARGET_DIR)/res/images
cp $(@D)/res/images/* $(TARGET_DIR)/res/images/
endef
define RECOVERY_INSTALL_INIT_SYSV
$(INSTALL) -D -m 755 $(RECOVERY_PKGDIR)/S40recovery \
$(TARGET_DIR)/etc/init.d/S40recovery
endef
endif
ifeq ($(BR2_PACKAGE_RECOVERY_BOOTCONTROL), y)
define BOOTCONTROLBIN_INSTALL_TARGET
$(INSTALL) -D -m 755 $(@D)/update_engine/S99_bootcontrol \
$(TARGET_DIR)/etc/init.d/
endef
endif
ifeq ($(BR2_PACKAGE_RECOVERY_UPDATEENGINEBIN),y)
define UPDATEENGINEBIN_INSTALL_TARGET
$(INSTALL) -D -m 755 $(@D)/updateEngine $(TARGET_DIR)/usr/bin/
endef
endif
define RECOVERY_INSTALL_TARGET_CMDS
$(RECOVERYBIN_INSTALL_TARGET)
$(UPDATEENGINEBIN_INSTALL_TARGET)
$(BOOTCONTROLBIN_INSTALL_TARGET)
endef
$(eval $(generic-package))
2.2.1 约定配置
首先映入眼帘的是一些约定的配置:
RECOVERY_VERSION
:定义了源码的版本号为develop
;RECOVERY_SITE
:定义了源码下载地址,这里指定为<SDK>/external/recovery
;RECOVERY_SITE_METHOD
:定义了源码下载的方式,该处指定为本地获取(local
);RECOVERY_LICENSE
:设置许可证类型为ROCKCHIP
;RECOVERY_LICENSE_FILES
:设置许可证文件NOTICE
。
2.2.2 编译选项
接下来的是编译选项和依赖关系配置:
(1) RECOVERY_CFLAGS
:编译选项配置,包括开启线程支持、网络支持、SSL
加密等;
RECOVERY_CFLAGS = $(TARGET_CFLAGS) -I. \
-fPIC \
-lpthread \
-lcurl \
-lssl \
-lcrypto \
-lbz2
其中:
-I
告诉编译器在当前目录中查找头文件;-fPIC
是GCC
和其他兼容编译器的选项,表示生成位置无关的代码,用于动态链接库。这是因为动态链接库在编译时无法确定加载地址,因此需要生成位置无关的代码;-lpthread
:表示链接pthread
库,即POSIX
线程库,用于多线程编程;-lcurl
:表示链接libcurl
库,这是一个支持多协议的URL
传输库,用于实现网络操作;-lssl
:表示链接OpenSSL
的SSL
库,提供了安全套接层 (SSL
) 和传输层安全性 (TLS
) 的实现;-lcrypto
:表示链接OpenSSL
的加密库,提供了各种加密算法的实现,例如哈希函数、对称加密和非对称加密等;-lbz2
:表示链接libbz2
库,这是用于处理Bzip2
压缩格式的库,提供了文件压缩和解压缩的功能。
这些选项通常用于构建需要多线程、网络操作、安全套接层以及数据压缩支持的应用程序或库。在编译时,它们告诉编译器在链接阶段需要使用这些特定的外部库。
(2) RECOVERY_MAKE_ENV
:这里设置为TARGET_MAKE_ENV
,TARGET_MAKE_ENV
是在Buildroot
中用来定义传递给 make
命令的环境变量;
TARGET_MAKE_ENV
在package/Makefile.in
中有定义:
TARGET_MAKE_ENV = PATH=$(BR_PATH)
而BR_PATH
又定义在Makefile
中:
......
# Check if the current Buildroot execution meets all the pre-requisites.
# If they are not met, Buildroot will actually do its job in a sub-make meeting
# its pre-requisites, which are:
# 1- Permissive enough umask:
# Wrong or too restrictive umask will prevent Buildroot and packages from
# creating files and directories.
# 2- Absolute canonical CWD (i.e. $(CURDIR)):
# Otherwise, some packages will use CWD as-is, others will compute its
# absolute canonical path. This makes harder tracking and fixing host
# machine path leaks.
# 3- Absolute canonical output location (i.e. $(O)):
# For the same reason as the one for CWD.
# Remove the trailing '/.' from $(O) as it can be added by the makefile wrapper
# installed in the $(O) directory.
# Also remove the trailing '/' the user can set when on the command line.
override O := $(patsubst %/,%,$(patsubst %.,%,$(O)))
# Make sure $(O) actually exists before calling realpath on it; this is to
# avoid empty CANONICAL_O in case on non-existing entry.
CANONICAL_O := $(shell mkdir -p $(O) >/dev/null 2>&1)$(realpath $(O))
BASE_DIR := $(CANONICAL_O) # 值为<SDK>/buildroot/output/rockchip_rk3588_recovery
HOST_DIR := $(BASE_DIR)/host
# Quotes are needed for spaces and all in the original PATH content.
BR_PATH = "$(HOST_DIR)/bin:$(HOST_DIR)/sbin:$(PATH)"
实际上HOST_DIR=<SDK>/buildroot/output/rockchip_rk3588_recovery/host
(其中<SDK>
表示SDK
所在全路径),因此不难得到:
TARGET_MAKE_ENV = PATH=<SDK>/buildroot/output/rockchip_rk3588_recovery/host/bin;<SDK>/buildroot/output/rockchip_rk3588_recovery/host/sbin;$(PATH)
其中在<SDK>/buildroot/output/rockchip_rk3588_recovery/host/bin
目录下包含了交叉编译工具的。
(3) RECOVERY_DEPENDENCIES
:定义依赖项,包括线程库、工具库、网络库和加密库等,这样在编译recovery
软件包的时候才会构建和安装依赖包。
2.2.3 条件编译配置
紧跟着的就是一些条件编译配置项;
ifeq ($(BR2_PACKAGE_RECOVERY_NO_UI),y)
RECOVERY_MAKE_ENV += RecoveryNoUi=true # 关闭UI
else # 开启UI 添加编译选项和依赖项 走这里
RECOVERY_CFLAGS += -lpng -ldrm -lz -lm -I$(STAGING_DIR)/usr/include/libdrm
RECOVERY_DEPENDENCIES += libpng libdrm libzlib
endif
# For static link with libcurl 静态链接,该选项未开启
ifeq ($(BR2_PACKAGE_RTMPDUMP)$(BR2_PACKAGE_RECOVERY_STATIC),yy)
RECOVERY_CFLAGS += -lrtmp
RECOVERY_DEPENDENCIES += rtmpdump
endif
ifeq ($(BR2_PACKAGE_RECOVERY_USE_RKUPDATE),y)
RECOVERY_CFLAGS += -DUSE_RKUPDATE=ON # 不走这里
endif
ifeq ($(BR2_PACKAGE_RECOVERY_USE_UPDATEENGINE),y)
RECOVERY_CFLAGS += -DUSE_UPDATEENGINE=ON # 走这里
endif
ifeq ($(BR2_PACKAGE_RECOVERY_SUCCESSFUL_BOOT),y)
RECOVERY_CFLAGS += -DSUCCESSFUL_BOOT=ON # 走这里
endif
ifeq ($(BR2_PACKAGE_RECOVERY_RETRY),y)
RECOVERY_CFLAGS += -DRETRY_BOOT=ON # 未开启
endif
ifeq ($(BR2_PACKAGE_RECOVERY_STATIC),y)
RECOVERY_CFLAGS += -static # 静态链接,未开启
endif
比如:
- 根据
BR2_PACKAGE_RECOVERY_NO_UI
的设置决定是否启用UI
(用户界面),这里我们未设置BR2_PACKAGE_RECOVERY_NO_UI
,则在编译选项以及依赖项中添加PNG
图像处理、DRM
、zlib
支持; - 由于我们开启了
BR2_PACKAGE_RECOVERY_USE_RKUPDATE
,因此添加编译选项RECOVERY_CFLAGS += -DUSE_RKUPDATE=ON
; - 由于我们开启了
BR2_PACKAGE_RECOVERY_SUCCESSFUL_BOOT
,因此添加编译选项RECOVERY_CFLAGS += -DSUCCESSFUL_BOOT=ON
。
2.2.4 CMD
命令之BUILD
接着是定义一系列的编译命令,这些_CMD
结尾的变量会在buildroot
框架编译的时候执行,用于给源码的Makefile
传递编译选项和链接选项,调用源码的Makefile
;
define RECOVERY_BUILD_CMDS
$(RECOVERY_MAKE_ENV) $(MAKE) -C $(@D) \
CC="$(TARGET_CC)" CFLAGS="$(RECOVERY_CFLAGS)"
endef
RECOVERY_BUILD_CMDS
在make <pkg>
的build
阶段被执行;
$(RECOVERY_MAKE_ENV) $(MAKE) -C $(@D) CC="$(TARGET_CC)" CFLAGS="$(RECOVERY_CFLAGS)"
2.2.4.1 RECOVERY_MAKE_ENV
$(RECOVERY_MAKE_ENV)
: 通过前面的分析,我们已经了解到该变量被设置为:
RECOVERY_MAKE_ENV = $(TARGET_MAKE_ENV)
2.2.4.2 MAKE
$(MAKE)
: 定义在package/Makefile.in
,表示调用make
命令,它会执行Makefile
;
ifndef MAKE
MAKE := make
endif
2.2.4.3 -C $(@D)
-C $(@D)
: 这是make
的一个选项,-C
选项后跟目录,表示到子目录下执行子目录的Makefile
,顶层Makefile
中的export
变量还有make
的变量时可以传递给子目录中的Makefile
的;$(@D)
是一个自动变量,表示当前目标文件所在的目录,也就是recovery
源码所在目录(具体路径为<SDK>/buildroot/output/rockchip_rk3588_recovery/build/recovery-develop
),我们我们可以知道该条语句实际上就是到recovery
源码目录执行make
操作,同时并传递了CC
、CFLAGS
变量。
2.2.4.4 TARGET_CC
CC="$(TARGET_CC)"
: TARGET_CC
定义在package/Makefile.in
,它指定了gcc
编译器的路径;
ifeq ($(BR2_TOOLCHAIN_BUILDROOT),y) # 使用内部工具链
TARGET_VENDOR = $(call qstrip,$(BR2_TOOLCHAIN_BUILDROOT_VENDOR)) # 走这里,去除BR2_TOOLCHAIN_BUILDROOT_VENDOR值前后空格,设置为buildroot
else
TARGET_VENDOR = buildroot
endif
# Compute GNU_TARGET_NAME
GNU_TARGET_NAME = $(ARCH)-$(TARGET_VENDOR)-$(TARGET_OS)-$(LIBC)$(ABI) # 延时变量,在使用的时候才确定
# FLAT binary format needs uclinux, except RISC-V 64-bits which needs
# the regular linux name.
ifeq ($(BR2_BINFMT_FLAT):$(BR2_RISCV_64),y:)
TARGET_OS = uclinux
else
TARGET_OS = linux # 走这里
endif
ifeq ($(BR2_TOOLCHAIN_USES_UCLIBC),y)
LIBC = uclibc
else ifeq ($(BR2_TOOLCHAIN_USES_MUSL),y)
LIBC = musl
else
LIBC = gnu # 走这里
endif
......
ifeq ($(BR2_TOOLCHAIN_BUILDROOT),y) # 使用内部工具链
TARGET_CROSS = $(HOST_DIR)/bin/$(GNU_TARGET_NAME)- # 走这里
else
TARGET_CROSS = $(HOST_DIR)/bin/$(TOOLCHAIN_EXTERNAL_PREFIX)-
endif
TARGET_CC = $(TARGET_CROSS)gcc
此外,在output/rockchip_rk3588_recovery/.config
中配置了:
#
# Target options
#
BR2_aarch64=y
......
#
# Toolchain
#
BR2_TOOLCHAIN=y
# BR2_TOOLCHAIN_PREFER_CLANG is not set
BR2_TOOLCHAIN_USES_GLIBC=y
BR2_TOOLCHAIN_BUILDROOT=y
# BR2_TOOLCHAIN_EXTERNAL is not set
#
# Toolchain Buildroot Options
#
BR2_TOOLCHAIN_BUILDROOT_VENDOR="buildroot"
BR2_TOOLCHAIN_BUILDROOT_GLIBC=y
BR2_TOOLCHAIN_BUILDROOT_LIBC="glibc"
在Makefile
中配置了:
Strip off the annoying quoting
ARCH := $(call qstrip,$(BR2_ARCH)) # 去除BR2_ARCH值前后空格,设置为aarch64
因此不难得出:
GNU_TARGET_NAME=aarch64-buildroot-linux-gnu
TARGET_CROSS=<SDK>/buildroot/output/rockchip_rk3588_recovery/host/bin/aarch64-buildroot-linux-gnu-
CC=$(TARGET_CC)=<SDK>/buildroot/output/rockchip_rk3588_recovery/host/bin/aarch64-buildroot-linux-gnu-gcc
因此,我们得出CC
实际上即使指定了gcc
编译器路径。
2.2.4.5 RECOVERY_CFLAGS
CFLAGS="$(RECOVERY_CFLAGS)"
: 这是一个将编译标志传递给make
的变量赋值语句;RECOVERY_CFLAGS
是一个变量,它存储着特定于recovery
构建的编译标志,这里被设置为:
RECOVERY_CFLAGS = $(TARGET_CFLAGS) -I. \
-fPIC \
-lpthread \
-lcurl \
-lssl \
-lcrypto \
-lbz2 \
-lpng -ldrm -lz -lm -I$(STAGING_DIR)/usr/include/libdrm \
-DUSE_UPDATEENGINE=ON \
-DSUCCESSFUL_BOOT=ON
2.2.4.6 build
之后
我们知道buildroot
软件包在编译时会将源码拷贝到output/rockchip_rk3588_recovery/build
目录下,recovery
软件包对应的目录为recovery-develop
:
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot$ ll output/rockchip_rk3588_recovery/build/recovery-develop/
-rw-r--r-- 1 root root 6236 6月 9 12:58 bootloader.c
-rw-r--r-- 1 root root 4543 6月 9 12:58 bootloader.h
-rw-r--r-- 1 root root 9296 6月 18 01:48 bootloader.o
-rwxr-xr-x 1 root root 1072 6月 18 01:48 .build.sh*
-rw-r--r-- 1 root root 423 6月 9 12:58 ChangeLog.md
-rw-r--r-- 1 root root 3882 6月 9 12:58 common.h
-rwxr-xr-x 1 root root 158 6月 18 01:48 .configure.sh*
-rw-r--r-- 1 root root 1887 6月 9 12:58 default_recovery_ui.c
-rw-r--r-- 1 root root 2616 6月 18 01:48 default_recovery_ui.o
-rwxr-xr-x 1 root root 795 6月 18 01:48 .deploy.sh*
-rw-r--r-- 1 root root 9170 6月 9 12:58 encryptedfs_provisioning.c
-rw-r--r-- 1 root root 1533 6月 9 12:58 encryptedfs_provisioning.h
-rw-r--r-- 1 root root 0 6月 18 01:48 .files-list-host.txt
-rw-r--r-- 1 root root 0 6月 18 01:48 .files-list-images.txt
-rw-r--r-- 1 root root 0 6月 18 01:48 .files-list-staging.txt
-rw-r--r-- 1 root root 364 6月 18 01:48 .files-list-target.txt
-rw-r--r-- 1 root root 500 6月 18 01:48 .files-list.txt
drwxr-xr-x 2 root root 4096 6月 9 12:58 hooks/
-rw-r--r-- 1 root root 889 6月 9 12:58 install.h
-rwxr-xr-x 1 root root 11556 6月 9 12:58 LICENSE*
-rw-r--r-- 1 root root 2250 6月 9 12:58 Makefile
drwxr-xr-x 2 root root 4096 6月 18 01:48 minui/
drwxr-xr-x 2 root root 4096 6月 18 01:48 minzip/
drwxr-xr-x 2 root root 4096 6月 18 01:48 mtdutils/
-rw-r--r-- 1 root root 10695 6月 9 12:58 NOTICE
-rw-r--r-- 1 root root 1546 6月 9 12:58 noui.c
-rwxr-xr-x 1 root root 126768 6月 18 01:48 recovery*
-rw-r--r-- 1 root root 49 6月 18 01:48 recovery_autogenerate.h
-rw-r--r-- 1 root root 43859 6月 9 12:58 recovery.c
-rw-r--r-- 1 root root 143360 6月 18 01:48 recovery-develop.tar
-rw-r--r-- 1 root root 42096 6月 18 01:48 recovery.o
-rw-r--r-- 1 root root 3175 6月 9 12:58 recovery_ui.h
-rw-r--r-- 1 root root 1269 6月 9 12:58 recovery_version.h
drwxr-xr-x 3 root root 4096 6月 9 12:58 res/
-rw-r--r-- 1 root root 6011 6月 9 12:58 rktools.c
-rw-r--r-- 1 root root 1308 6月 9 12:58 rktools.h
-rw-r--r-- 1 root root 10680 6月 18 01:48 rktools.o
-rw-r--r-- 1 root root 6779 6月 9 12:58 rkupdate.c
-rw-r--r-- 1 root root 8368 6月 18 01:48 rkupdate.o
-rw-r--r-- 1 root root 14072 6月 9 12:58 roots.c
-rw-r--r-- 1 root root 1835 6月 9 12:58 roots.h
-rw-r--r-- 1 root root 16728 6月 18 01:48 roots.o
-rw-r--r-- 1 root root 42629 6月 9 12:58 safe_iop.c
-rw-r--r-- 1 root root 24520 6月 9 12:58 safe_iop.h
-rw-r--r-- 1 root root 3776 6月 18 01:48 safe_iop.o
-rw-r--r-- 1 root root 4274 6月 9 12:58 sdboot.c
-rw-r--r-- 1 root root 872 6月 9 12:58 sdboot.h
-rw-r--r-- 1 root root 6472 6月 18 01:48 sdboot.o
-rw-r--r-- 1 root root 0 6月 18 01:48 .stamp_built
-rw-r--r-- 1 root root 0 6月 18 01:48 .stamp_configured
-rw-r--r-- 1 root root 0 6月 18 01:48 .stamp_installed
-rw-r--r-- 1 root root 0 6月 18 01:48 .stamp_rsynced
-rw-r--r-- 1 root root 0 6月 18 01:48 .stamp_target_installed
-rw-r--r-- 1 root root 1840 6月 9 12:58 strlcat.c
-rw-r--r-- 1 root root 1536 6月 18 01:48 strlcat.o
-rw-r--r-- 1 root root 1699 6月 9 12:58 strlcpy.c
-rw-r--r-- 1 root root 1320 6月 18 01:48 strlcpy.o
-rwxr-xr-x 1 root root 3506 6月 18 01:48 .target_install.sh*
-rw-r--r-- 1 root root 16670 6月 9 12:58 ui.c
-rw-r--r-- 1 root root 17904 6月 18 01:48 ui.o
drwxr-xr-x 2 root root 4096 6月 18 01:48 update_engine/
-rwxr-xr-x 1 root root 94688 6月 18 01:48 updateEngine*
-rwxr-xr-x 1 root root 984 6月 18 01:48 .update.sh*
-rwxr-xr-x 1 root root 4731 6月 9 12:58 usbboot.c*
-rwxr-xr-x 1 root root 922 6月 9 12:58 usbboot.h*
-rw-r--r-- 1 root root 6768 6月 18 01:48 usbboot.o
2.2.5 CMD
命令之INSTALL
ifeq ($(BR2_PACKAGE_RECOVERY_RECOVERYBIN),y)
define RECOVERYBIN_INSTALL_TARGET # 用于安装recovery、res/images
$(INSTALL) -D -m 755 $(@D)/recovery $(TARGET_DIR)/usr/bin/
mkdir -p $(TARGET_DIR)/res/images
cp $(@D)/res/images/* $(TARGET_DIR)/res/images/
endef
define RECOVERY_INSTALL_INIT_SYSV #用于安装S40recovery启动脚本
$(INSTALL) -D -m 755 $(RECOVERY_PKGDIR)/S40recovery \
$(TARGET_DIR)/etc/init.d/S40recovery
endef
endif
ifeq ($(BR2_PACKAGE_RECOVERY_BOOTCONTROL), y) # 不走这里
define BOOTCONTROLBIN_INSTALL_TARGET
$(INSTALL) -D -m 755 $(@D)/update_engine/S99_bootcontrol \
$(TARGET_DIR)/etc/init.d/
endef
endif
ifeq ($(BR2_PACKAGE_RECOVERY_UPDATEENGINEBIN),y)
define UPDATEENGINEBIN_INSTALL_TARGET # 用于安装updateEngine
$(INSTALL) -D -m 755 $(@D)/updateEngine $(TARGET_DIR)/usr/bin/
endef
endif
define RECOVERY_INSTALL_TARGET_CMDS
$(RECOVERYBIN_INSTALL_TARGET) # 安装rkupdate
$(UPDATEENGINEBIN_INSTALL_TARGET) # 安装updateEngine
$(BOOTCONTROLBIN_INSTALL_TARGET) # 未定义
endef
RECOVERY_INSTALL_TARGET_CMDS
在make <pkg>
的install
阶段被执行;
$(RECOVERYBIN_INSTALL_TARGET)
$(UPDATEENGINEBIN_INSTALL_TARGET)
$(BOOTCONTROLBIN_INSTALL_TARGET)
2.2.5.1 RECOVERYBIN_INSTALL_TARGET
$RECOVERYBIN_INSTALL_TARGET
:它包含了一系列安装recovery
模块相关文件的操作,包括安装 recovery
文件到 /usr/bin/
目录,并复制 res/images/
目录下的文件到目标系统的相同位置;
$(INSTALL) -D -m 755 $(@D)/recovery $(TARGET_DIR)/usr/bin/
: 这行命令使用了makefile
中定义的INSTALL
变量,以及自动变量$(@D)
和预定义变量TARGET_DIR
。它的作用是将源码recovery
编译出来的recovery
文件安装到目标系统的/usr/bin/
目录下,并设置相应的权限;mkdir -p $(TARGET_DIR)/res/images
: 这行命令创建了目标系统下的/res/images
目录,其中-p
选项表示如果目录不存在则递归创建;cp $(@D)/res/images/* $(TARGET_DIR)/res/images/
: 这行命令将源代码中res/images/
目录下的所有文件复制到目标系统的相同目录下。
注意:INSTALL
定义在package/Makefile.in
文件:
INSTALL := $(shell which install || type -p install)
INSTALL
其值等于/usr/bin/install
,/usr/bin/install
是一个命令行工具,用于复制文件并设置文件的权限和属性。它通常用于将文件从一个位置复制到另一个位置,并且可以设置文件的权限、所有者和组;
TARGET_DIR
定义在Makefile
文件;
BASE_TARGET_DIR := $(BASE_DIR)/target
.......
ifeq ($(BR2_PER_PACKAGE_DIRECTORIES),y)
HOST_DIR = $(if $(PKG),$(PER_PACKAGE_DIR)/$($(PKG)_NAME)/host,$(call qstrip,$(BR2_HOST_DIR)))
TARGET_DIR = $(if $(ROOTFS),$(ROOTFS_$(ROOTFS)_TARGET_DIR),$(if $(PKG),$(PER_PACKAGE_DIR)/$($(PKG)_NAME)/target,$(BASE_TARGET_DIR)))
else # 走这里
HOST_DIR := $(call qstrip,$(BR2_HOST_DIR)) # BR2_HOST_DIR="$(BASE_DIR)/host"
TARGET_DIR = $(if $(ROOTFS),$(ROOTFS_$(ROOTFS)_TARGET_DIR),$(BASE_TARGET_DIR))$(if $($(PKG)_OEM_INSTALL),/oem)
ifeq ($(BR2_PACKAGE_OEM),y) # 不走这里
$(eval $(foreach pkg,$(call qstrip,$(BR2_PACKAGE_OEM_PACKAGES)), \
$(eval $(call UPPERCASE,$(pkg))_OEM_INSTALL := y)$(sep) \
$(eval OEM_DEPENDENCIES += $(pkg))$(sep)))
endif
endif
由于未定义ROOTFS
变量,所以TARGET_DIR=$(BASE_TARGET_DIR)
,也就是$(BASE_DIR)/target
,而BASE_DIR
前面已经介绍过,值为<SDK>/buildroot/output/rockchip_rk3588_recovery
,因此:
TARGET_DIR=<SDK>/buildroot/output/rockchip_rk3588_recovery/target
因此如果在recovery
软件包编译安装完成后,我们可以在<SDK>/buildroot/output/rockchip_rk3588_recovery/target/usr/bin
中看到recovery
可执行文件;
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot$ ll output/rockchip_rk3588_recovery/target/usr/bin/recovery
-rwxr-xr-x 1 root root 103496 6月 18 01:48 output/rockchip_rk3588_recovery/target/usr/bin/recovery*
同时在<SDK>/buildroot/output/rockchip_rk3588_recovery/target/res/images
目录下看到一些资源文件:
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot$ ll output/rockchip_rk3588_recovery/target/res/images/
-rwxr-xr-x 1 root root 10398 6月 18 01:48 icon_error.png*
-rwxr-xr-x 1 root root 11493 6月 18 01:48 icon_installing.png*
-rw-r--r-- 1 root root 1919 6月 18 01:48 indeterminate1.png
-rw-r--r-- 1 root root 1912 6月 18 01:48 indeterminate2.png
-rw-r--r-- 1 root root 1917 6月 18 01:48 indeterminate3.png
-rw-r--r-- 1 root root 1912 6月 18 01:48 indeterminate4.png
-rw-r--r-- 1 root root 1902 6月 18 01:48 indeterminate5.png
-rw-r--r-- 1 root root 1914 6月 18 01:48 indeterminate6.png
-rw-r--r-- 1 root root 361 6月 18 01:48 progress_empty.png
-rw-r--r-- 1 root root 286 6月 18 01:48 progress_fill.png
2.2.5.2 UPDATEENGINEBIN_INSTALL_TARGET
$UPDATEENGINEBIN_INSTALL_TARGET
:安装 updateEngine
文件到 /usr/bin/
目录;
$(INSTALL) -D -m 755 $(@D)/updateEngine $(TARGET_DIR)/usr/bin/
这行命令使用了makefile
中定义的INSTALL
变量,以及自动变量$(@D)
和预定义变量TARGET_DIR
。它的作用是将源码recovery
编译出来的updateEngine
可执行文件安装到目标系统的/usr/bin/
目录下,并设置相应的权限;
因此如果在recovery
软件包编译安装完成后,我们可以在<SDK>/buildroot/output/rockchip_rk3588_recovery/target/usr/bin
中看到updateEngine
可执行文件;
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot$ ll output/rockchip_rk3588_recovery/target/usr/bin/updateEngine
-rwxr-xr-x 1 root root 80544 6月 18 01:48 output/rockchip_rk3588_recovery/target/usr/bin/updateEngine*
2.2.5.3 RECOVERY_INSTALL_INIT_SYSV
在Buildroot
中,使用 INSTALL_INIT_SYSV
列出了为类似systemV
的初始系统(busybox、sysvinit
等)、openrc
或systemd
单元安装初始脚本的操作;
$(INSTALL) -D -m 755 $(RECOVERY_PKGDIR)/S40recovery $(TARGET_DIR)/etc/init.d/S40recovery
这行命令使用了makefile
中定义的INSTALL
变量,以及自动变量$(@D)
和预定义变量TARGET_DIR
。它的作用是将S40recovery
脚本安装到目标系统的/etc/init.d
目录下,并设置相应的权限;
S40recovery
脚本内容如下:
#!/bin/sh
#
# Start Rockchip recovery...
#
case "$1" in
start)
echo "starting recovery... "
touch /dev/.coldboot_done
/usr/bin/recovery &
;;
stop)
killall recovery
;;
*)
echo "Usage: $0 {start|stop}"
exit 1
;;
esac
exit 0
我们知道内核启动的第一个用户级进程是init
,那么聪init
进程是如何触发/etc/init.d
下服务的执行的呢;
(1) init
进程执行会读取/etc/inittab
文件,然后执行inittab
中的sysinit
所指的脚本;我们读取/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/target/etc/inittab
文件内容:
# /etc/inittab
#
# Copyright (C) 2001 Erik Andersen <andersen@codepoet.org>
#
# Note: BusyBox init doesn't support runlevels. The runlevels field is
# completely ignored by BusyBox init. If you want runlevels, use
# sysvinit.
#
# Format for each entry: <id>:<runlevels>:<action>:<process>
#
# id == tty to run on, or empty for /dev/console
# runlevels == ignored
# action == one of sysinit, respawn, askfirst, wait, and once
# process == program to run
# Startup the system
::sysinit:/bin/mount -t proc proc /proc
::sysinit:/bin/mount -o remount,rw /
::sysinit:/bin/mkdir -p /dev/pts /dev/shm
::sysinit:/bin/mount -a
::sysinit:/bin/mkdir -p /run/lock/subsys
::sysinit:/sbin/swapon -a
null::sysinit:/bin/ln -sf /proc/self/fd /dev/fd
null::sysinit:/bin/ln -sf /proc/self/fd/0 /dev/stdin
null::sysinit:/bin/ln -sf /proc/self/fd/1 /dev/stdout
null::sysinit:/bin/ln -sf /proc/self/fd/2 /dev/stderr
::sysinit:/bin/hostname -F /etc/hostname
# now run any rc scripts
::sysinit:/etc/init.d/rcS
# Put a getty on the serial port
::respawn:-/bin/sh # ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100 # GENERIC_SERIAL
# Stuff to do for the 3-finger salute
#::ctrlaltdel:/sbin/reboot
# Stuff to do before rebooting
::shutdown:/etc/init.d/rcK
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
因此会执行/etc/init.d/rcS
这个脚本;
(2) /etc/init.d/rcS
脚本会依次执行/etc/init.d
目录下以S
命令的脚本文件;
#!/bin/sh
# Start all init scripts in /etc/init.d
# executing them in numerical order.
#
for i in /etc/init.d/S??* ;do
# Ignore dangling symlinks (if any).
[ ! -f "$i" ] && continue
case "$i" in
*.sh)
# Source shell script for speed.
(
trap - INT QUIT TSTP
set start
. $i
)
;;
*)
# No sh extension, so fork subprocess.
$i start
;;
esac
done
我们查看/etc/init.d
目录;
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/target$ ls etc/init.d/
rcK S00mountall.sh S02klogd S05async-commit.sh S20urandom S40recovery S99input-event-daemon
rcS S01syslogd S02sysctl S10udev S40network S50usbdevice
其中S40recovery
就是编译recovery
软件包是安装的脚本。
注意:更多细节可以参考《从init
进程逐步到/etc/init.d
,整体分析Openwrt
的软件启动机制》。
由于rockchip_rk3588_recovery_defconfig
配置编译出来的recovery.img
安装了S40recovery
服务,因此不难猜测,当烧录了包含该镜像的系统进入recovery
系统将会执行S40recovery
脚本进行固件的升级,并输出类似如下的日志:
starting recovery...
LOG_INFO: devices is not MTD.
LOG_INFO: Boot command: boot-recovery
LOG_INFO: Got arguments from boot message
LOG_INFO: devices is not MTD.
同样,如果我们的根文件系统rootfs.img
如果安装了S40recovery
服务,将会导致系统陷入系统固件升级的死循环中,不停的重启。
2.2.6 $(eval $(generic-package))
($eval$(generic-package
)) 最核心的就是这个东西了,一定不能够漏了,不然源码不会被编译,这个函数就是把整个.mk
构建脚本,通过Buildroot
框架的方式,展开到Buildroot/
目录下的Makfile
中,生成的构建目标。
2.3 Makefile
经过前面对recovery.mk
的分析,我们已经知道recovery
的源码位于本地<SDK>/external/recovery
目录;
zhengyang@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp$ ll external/recovery/
-rw-r--r-- 1 root root 6236 6月 9 12:58 bootloader.c
-rw-r--r-- 1 root root 4543 6月 9 12:58 bootloader.h
-rw-r--r-- 1 root root 423 6月 9 12:58 ChangeLog.md
-rw-r--r-- 1 root root 3882 6月 9 12:58 common.h
-rw-r--r-- 1 root root 1887 6月 9 12:58 default_recovery_ui.c
-rw-r--r-- 1 root root 9170 6月 9 12:58 encryptedfs_provisioning.c
-rw-r--r-- 1 root root 1533 6月 9 12:58 encryptedfs_provisioning.h
drwxr-xr-x 2 root root 4096 6月 9 12:58 hooks/
-rw-r--r-- 1 root root 889 6月 9 12:58 install.h
-rwxr-xr-x 1 root root 11556 6月 9 12:58 LICENSE*
-rw-r--r-- 1 root root 2250 6月 9 12:58 Makefile
drwxr-xr-x 2 root root 4096 6月 9 12:58 minui/
drwxr-xr-x 2 root root 4096 6月 9 12:58 minzip/
drwxr-xr-x 2 root root 4096 6月 9 12:58 mtdutils/
-rw-r--r-- 1 root root 10695 6月 9 12:58 NOTICE
-rw-r--r-- 1 root root 1546 6月 9 12:58 noui.c
-rw-r--r-- 1 root root 43859 6月 9 12:58 recovery.c
-rw-r--r-- 1 root root 3175 6月 9 12:58 recovery_ui.h
-rw-r--r-- 1 root root 1269 6月 9 12:58 recovery_version.h
drwxr-xr-x 3 root root 4096 6月 9 12:58 res/
-rw-r--r-- 1 root root 6011 6月 9 12:58 rktools.c
-rw-r--r-- 1 root root 1308 6月 9 12:58 rktools.h
-rw-r--r-- 1 root root 6779 6月 9 12:58 rkupdate.c
-rw-r--r-- 1 root root 14072 6月 9 12:58 roots.c
-rw-r--r-- 1 root root 1835 6月 9 12:58 roots.h
-rw-r--r-- 1 root root 42629 6月 9 12:58 safe_iop.c
-rw-r--r-- 1 root root 24520 6月 9 12:58 safe_iop.h
-rw-r--r-- 1 root root 4274 6月 9 12:58 sdboot.c
-rw-r--r-- 1 root root 872 6月 9 12:58 sdboot.h
-rw-r--r-- 1 root root 1840 6月 9 12:58 strlcat.c
-rw-r--r-- 1 root root 1699 6月 9 12:58 strlcpy.c
-rw-r--r-- 1 root root 16670 6月 9 12:58 ui.c
drwxr-xr-x 2 root root 4096 6月 9 12:58 update_engine/
-rwxr-xr-x 1 root root 4731 6月 9 12:58 usbboot.c*
-rwxr-xr-x 1 root root 922 6月 9 12:58 usbboot.h*
首先我们需要了解源码的Makefile
是怎么样的,其内容如下;
点击查看代码
PROJECT_DIR := $(shell pwd)
CC = gcc
PROM = recovery
UPDATE_ENGINE = updateEngine
all: $(PROM) $(UPDATE_ENGINE)
.PHONY : all
OBJ = recovery.o \
default_recovery_ui.o \
rktools.o \
roots.o \
bootloader.o \
safe_iop.o \
strlcpy.o \
strlcat.o \
rkupdate.o \
sdboot.o \
usbboot.o \
mtdutils/mounts.o \
mtdutils/mtdutils.o \
mtdutils/rk29.o \
minzip/DirUtil.o \
update_engine/log.o
ifdef RecoveryNoUi
OBJ += noui.o
else
OBJ += ui.o\
minzip/Hash.o \
minzip/SysUtil.o \
minzip/Zip.o \
minui/events.o \
minui/graphics.o \
minui/resources.o \
minui/graphics_drm.o
endif
CFLAGS += -I$(PROJECT_DIR) -I/usr/include -I/usr/include/libdrm/ -lc -DUSE_UPDATEENGINE=ON
ifdef RecoveryNoUi
CFLAGS += -lpthread -lbz2
else
CFLAGS += -lz -lpng -ldrm -lpthread -lcurl -lcrypto -lbz2
endif
UPDATE_ENGINE_OBJ = mtdutils/mounts.o \
mtdutils/mtdutils.o \
mtdutils/rk29.o \
update_engine/rkbootloader.o \
update_engine/download.o \
update_engine/flash_image.o \
update_engine/log.o \
update_engine/main.o \
update_engine/md5sum.o \
update_engine/rkimage.o \
update_engine/rktools.o \
update_engine/rkboot.o \
update_engine/crc.o \
update_engine/update.o \
update_engine/do_patch.o
# build in buildroot, it need change work directory
recovery_version:
cd $(PROJECT_DIR)/../../../../../external/recovery && \
COMMIT_HASH=$$(git rev-parse --verify --short HEAD) && \
GIT_COMMIT_TIME=$$(git log -1 --format=%cd --date=format:%y%m%d) && \
GIT_DIRTY=$$(git diff-index --quiet HEAD -- || echo "-dirty") && \
commit_info=-g$${COMMIT_HASH}-$${GIT_COMMIT_TIME}$${GIT_DIRTY} && \
cd $(PROJECT_DIR) && \
echo "#define GIT_COMMIT_INFO $${commit_info}" > recovery_autogenerate.h
$(PROM): $(OBJ)
$(CC) -o $(PROM) $(OBJ) $(CFLAGS)
$(UPDATE_ENGINE): $(UPDATE_ENGINE_OBJ)
$(CC) -o $(UPDATE_ENGINE) $(UPDATE_ENGINE_OBJ) $(CFLAGS)
%.o: %.cpp
$(CC) -c $< -o $@ $(CFLAGS)
%.o: %.c recovery_version
$(CC) -c $< -o $@ $(CFLAGS)
clean:
rm -rf $(OBJ) $(PROM) $(UPDATE_ENGINE_OBJ) $(UPDATE_ENGINE)
install:
mkdir -p $(DESTDIR)/res/images $(DESTDIR)/usr/bin
install -D -m 755 $(PROJECT_DIR)/recovery $(DESTDIR)/usr/bin/
install -D -m 755 $(PROJECT_DIR)/updateEngine $(DESTDIR)/usr/bin/
cp $(PROJECT_DIR)/res/images/* $(DESTDIR)/res/images/
这段脚本写的比较简单,在编译recovery
软件包的build
阶段,会执行如下命令;
$(RECOVERY_MAKE_ENV) $(MAKE) -C $(@D) CC="$(TARGET_CC)" CFLAGS="$(RECOVERY_CFLAGS)"
在前文分析,我们已经知道:
RECOVERY_MAKE_ENV
变量值被设置为$(TARGET_MAKE_ENV)
,即PATH=环境变量路径
;MAKE
变量值被设置为make
;CC
变量值被设置为$(TARGET_CC)
;CFLAGS
变量值被设置为$(RECOVERY_CFLAGS)
,会覆盖Makefile
中CFLAGS
相关配置:
其中:
TARGET_CC = <SDK>/buildroot/output/rockchip_rk3588_recovery/host/bin/aarch64-buildroot-linux-gnu-gcc
RECOVERY_CFLAGS = $(TARGET_CFLAGS) -I. \
-fPIC \
-lpthread \
-lcurl \
-lssl \
-lcrypto \
-lbz2 \
-lpng -ldrm -lz -lm -I$(STAGING_DIR)/usr/include/libdrm \
-DUSE_UPDATEENGINE=ON \
-DSUCCESSFUL_BOOT=ON
2.3.1 变量定义
脚本开始定义了一些变量:
PROJECT_DIR
:既时变量,值在定义时就确定,为当前工作路径;CC
:延时变量,值在使用的时候才确定,由于我们在RECOVERY_BUILD_CMDS
编译命令中指定了CC
,因此该值会被设置为$(TARGET_CC)
;PROM
:延时变量,值在使用的时候才确定,这里设置为recovery
;UPDATE_ENGINE
:延时变量,值在使用的时候才确定,这里设置为updateEngine
;
2.3.2 目标all
第一个目标为伪目标all
,其依赖recovery
、updateEngine
;
all: $(PROM) $(UPDATE_ENGINE)
.PHONY : all
在执行make
命令时,如果未指定目标,默认执行第一个目标,即all
。
接下来我们需要寻找all
依赖recovery
、updateEngine
的生成规则。
2.3.3 目标recovery
OBJ = recovery.o \
default_recovery_ui.o \
rktools.o \
roots.o \
bootloader.o \
safe_iop.o \
strlcpy.o \
strlcat.o \
rkupdate.o \
sdboot.o \
usbboot.o \
mtdutils/mounts.o \
mtdutils/mtdutils.o \
mtdutils/rk29.o \
minzip/DirUtil.o \
update_engine/log.o
ifdef RecoveryNoUi
OBJ += noui.o # 不走这里
else
OBJ += ui.o\ # 走这里
minzip/Hash.o \
minzip/SysUtil.o \
minzip/Zip.o \
minui/events.o \
minui/graphics.o \
minui/resources.o \
minui/graphics_drm.o
endif
CFLAGS += -I$(PROJECT_DIR) -I/usr/include -I/usr/include/libdrm/ -lc -DUSE_UPDATEENGINE=ON
ifdef RecoveryNoUi
CFLAGS += -lpthread -lbz2 # 不走这里
else
CFLAGS += -lz -lpng -ldrm -lpthread -lcurl -lcrypto -lbz2 # 走这里
endif
$(PROM): $(OBJ)
$(CC) -o $(PROM) $(OBJ) $(CFLAGS)
可以看到目标recovery
是由若干个.o
文件通过aarch64-buildroot-linux-gnu-gcc
编译器链接生成的可执行文件。
而.o
文件实际上是由.c
文件通过aarch64-buildroot-linux-gnu-gcc
编译器编译生成的。
# build in buildroot, it need change work directory
recovery_version:
cd $(PROJECT_DIR)/../../../../../external/recovery && \
COMMIT_HASH=$$(git rev-parse --verify --short HEAD) && \
GIT_COMMIT_TIME=$$(git log -1 --format=%cd --date=format:%y%m%d) && \
GIT_DIRTY=$$(git diff-index --quiet HEAD -- || echo "-dirty") && \
commit_info=-g$${COMMIT_HASH}-$${GIT_COMMIT_TIME}$${GIT_DIRTY} && \
cd $(PROJECT_DIR) && \
echo "#define GIT_COMMIT_INFO $${commit_info}" > recovery_autogenerate.h
%.o: %.cpp
$(CC) -c $< -o $@ $(CFLAGS)
%.o: %.c recovery_version
$(CC) -c $< -o $@ $(CFLAGS)
其中:%.o
表示所有以.o
结尾的文件作为目标文件,%.cpp
表示所有以.c
结尾的文件作为依赖文件。
2.3.4 目标updateEngine
UPDATE_ENGINE_OBJ = mtdutils/mounts.o \
mtdutils/mtdutils.o \
mtdutils/rk29.o \
update_engine/rkbootloader.o \
update_engine/download.o \
update_engine/flash_image.o \
update_engine/log.o \
update_engine/main.o \
update_engine/md5sum.o \
update_engine/rkimage.o \
update_engine/rktools.o \
update_engine/rkboot.o \
update_engine/crc.o \
update_engine/update.o \
update_engine/do_patch.o
$(UPDATE_ENGINE): $(UPDATE_ENGINE_OBJ)
$(CC) -o $(UPDATE_ENGINE) $(UPDATE_ENGINE_OBJ) $(CFLAGS)
同recovery
,可以看到目标updateEngine
是由若干个.o
文件通过aarch64-buildroot-linux-gnu-gcc
编译器链接生成的可执行文件。
而.o
文件实际上是由.c
文件通过aarch64-buildroot-linux-gnu-gcc
编译器编译生成的。
2.3.5 目标clean
伪目标clean
用于执行清理工作;
clean:
rm -rf $(OBJ) $(PROM) $(UPDATE_ENGINE_OBJ) $(UPDATE_ENGINE)
2.3.6 目标install
伪目标install
用于执行安装工作;
install:
mkdir -p $(DESTDIR)/res/images $(DESTDIR)/usr/bin
install -D -m 755 $(PROJECT_DIR)/recovery $(DESTDIR)/usr/bin/
install -D -m 755 $(PROJECT_DIR)/updateEngine $(DESTDIR)/usr/bin/
cp $(PROJECT_DIR)/res/images/* $(DESTDIR)/res/images/
2.4 编译
在《Rockchip RK3588 - Rockchip Linux SDK Buildroot
文件系统构建》中介绍过buildroot
编译命令;
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot$ sudo make -j8
或者使用如下命令单独编译recovery
软件包:
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot$ sudo make recovery-dirclean
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot$ sudo make recovery
其中recovery
编译日志如下:
点击查看代码
>>> recovery develop Syncing from source dir /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/../external/recovery
rsync -au --chmod=u=rwX,go=rX --exclude .svn --exclude .git --exclude .hg --exclude .bzr --exclude CVS /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/../external/recovery/ /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/recovery-develop
>>> recovery develop Configuring
>>> recovery develop Building
PATH="/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/host/bin:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/host/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin" /usr/bin/make -C /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/recovery-develop CC="/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/host/bin/aarch64-buildroot-linux-gnu-gcc" CFLAGS="-D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 -Os -g0 -D_FORTIFY_SOURCE=1 -I. -fPIC -lpthread -lcurl -lssl -lcrypto -lbz2 -lpng -ldrm -lz -lm -I/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/host/aarch64-buildroot-linux-gnu/sysroot/usr/include/libdrm -DUSE_UPDATEENGINE=ON -DSUCCESSFUL_BOOT=ON"
make[1]: 进入目录“/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/recovery-develop”
cd /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/recovery-develop/../../../../../external/recovery && \
COMMIT_HASH=$(git rev-parse --verify --short HEAD) && \
GIT_COMMIT_TIME=$(git log -1 --format=%cd --date=format:%y%m%d) && \
GIT_DIRTY=$(git diff-index --quiet HEAD -- || echo "-dirty") && \
commit_info=-g${COMMIT_HASH}-${GIT_COMMIT_TIME}${GIT_DIRTY} && \
cd /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/recovery-develop && \
echo "#define GIT_COMMIT_INFO ${commit_info}" > recovery_autogenerate.h
/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/host/bin/aarch64-buildroot-linux-gnu-gcc -c recovery.c -o recovery.o -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 -Os -g0 -D_FORTIFY_SOURCE=1 -I. -fPIC -lpthread -lcurl -lssl -lcrypto -lbz2 -lpng -ldrm -lz -lm -I/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/host/aarch64-buildroot-linux-gnu/sysroot/usr/include/libdrm -DUSE_UPDATEENGINE=ON -DSUCCESSFUL_BOOT=ON
......
/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/host/bin/aarch64-buildroot-linux-gnu-gcc -o updateEngine mtdutils/mounts.o mtdutils/mtdutils.o mtdutils/rk29.o update_engine/rkbootloader.o update_engine/download.o update_engine/flash_image.o update_engine/log.o update_engine/main.o update_engine/md5sum.o update_engine/rkimage.o update_engine/rktools.o update_engine/rkboot.o update_engine/crc.o update_engine/update.o update_engine/do_patch.o -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 -Os -g0 -D_FORTIFY_SOURCE=1 -I. -fPIC -lpthread -lcurl -lssl -lcrypto -lbz2 -lpng -ldrm -lz -lm -I/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/host/aarch64-buildroot-linux-gnu/sysroot/usr/include/libdrm -DUSE_UPDATEENGINE=ON -DSUCCESSFUL_BOOT=ON
make[1]: 离开目录“/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/recovery-develop”
>>> recovery develop Installing to target
/usr/bin/install -D -m 755 /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/recovery-develop/recovery /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/target/usr/bin/
mkdir -p /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/target/res/images
cp /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/recovery-develop/res/images/* /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/target/res/images/
/usr/bin/install -D -m 755 /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/recovery-develop/updateEngine /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/target/usr/bin/
/usr/bin/install -D -m 755 package/rockchip/recovery//S40recovery /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/target/etc/init.d/S40recovery
在日志中输出了recovery
软件包构建的各个阶段的信息,可以验证我们之前的分析是否正确。比如编译命令:
PATH="/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/host/bin:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/host/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin" /usr/bin/make -C /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/recovery-develop CC="/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/host/bin/aarch64-buildroot-linux-gnu-gcc" CFLAGS="-D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 -Os -g0 -D_FORTIFY_SOURCE=1 -I. -fPIC -lpthread -lcurl -lssl -lcrypto -lbz2 -lpng -ldrm -lz -lm -I/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/host/aarch64-buildroot-linux-gnu/sysroot/usr/include/libdrm -DUSE_UPDATEENGINE=ON -DSUCCESSFUL_BOOT=ON"
三、updateEngine
源码分析
由于updateEngine
可执行文件是由 update_engine
目录以及mtdutils
目录下若干.c
文件编译而成,程序的入口为update_engine/main.cpp
文件。
main.c
代码比较长,如下所示:
点击查看代码
static const struct option engine_options[] = {
{ "update", optional_argument, NULL, 'u' },
{ "version_url", required_argument, NULL, 'v' + 'u' },
{ "image_url", required_argument, NULL, 'i' + 'u'},
{ "check", required_argument, NULL, 'c' },
{ "misc", required_argument, NULL, 'm' },
{ "misc_custom", required_argument, NULL, 'd' },
{ "partition", required_argument, NULL, 'p' },
{ "reboot", no_argument, NULL, 'r' },
{ "help", no_argument, NULL, 'h' },
{ "pipefd", required_argument, NULL, 'p' + 'f' },
{ "savepath", required_argument, NULL, 's'},
{ "version", no_argument, NULL, 'v'},
{ "rkdebug", no_argument, NULL, 'k'},
{ "ui_rotation", required_argument, NULL, 'o'},
{ NULL, 0, NULL, 0 },
};
int main(int argc, char *argv[])
{
LOGI("*** update_engine: %s ***.\n", update_engine_version);
int arg;
char *image_url = NULL;
char *version_url = NULL;
char *misc_func = NULL;
char *save_path = NULL;
char *partition = NULL;
char *custom_define = NULL;
bool is_update = false;
bool is_reboot = false;
int pipefd = -1;
while ((arg = getopt_long(argc, argv, "", engine_options, NULL)) != -1) {
switch (arg) {
case 'u': is_update = true; if (optarg != NULL) is_sdboot = true; continue;
case 'c': version_url = optarg; continue;
case 'm': misc_func = optarg; continue;
case 'p': partition = optarg; continue;
case 's': save_path = optarg; continue;
case 'r': is_reboot = true; continue;
case 'v' + 'u': version_url = optarg; continue;
case 'i' + 'u': image_url = optarg; continue;
case 'p' + 'f': pipefd = atoi(optarg); continue;
case 'h': display(); break;
case 'd': custom_define = optarg; continue;
case 'v': LOGI("*** update_engine: %s ***.\n", update_engine_version); break;
case 'k': rkdebug = true; break;
case 'o': ui_rotation = optarg; break;
case '?':
LOGE("Invalid command argument\n");
continue;
}
}
if (pipefd != -1) {
cmd_pipe = fdopen(pipefd, "wb");
setlinebuf(cmd_pipe);
}
if (is_update) {
if (optarg && (strstr(optarg, "usb") != NULL || strstr(optarg, "udisk") != NULL)) {
is_usbboot = true;
is_sdboot = false;
LOGI("*** will upgrade firmware from udisk ***");
}
if (is_sdboot || is_usbboot) {
int res = 0x3FFC00; //默认升级的分区
LOGI("%s-%d: is %s update.\n", __func__, __LINE__, is_usbboot ? "usbboot" : "sdboot");
if (partition != NULL) {
res = strtol(partition + 2, NULL, 16);
}
RK_ota_set_url(image_url, save_path);
if ( !RK_ota_set_partition(res) ) {
LOGE("ota file is error.\n");
return -1;
}
if (version_url != NULL) {
if (!RK_ota_check_version(version_url) ) {
LOGE("you shouldn't update the device.\n");
return -1;
}
}
RK_ota_start(handle_upgrade_callback, handle_print_callback);
} else {
LOGI("%s-%d: is ota update\n", __func__, __LINE__);
if (MiscUpdate(image_url, partition, save_path) == 0) {
m_status = RK_UPGRADE_FINISHED;
}
}
} else if (misc_func != NULL) {
if (strcmp(misc_func, "now") == 0) {
if (setSlotSucceed() == 0) {
m_status = RK_UPGRADE_FINISHED;
}
} else if (strcmp(misc_func, "other") == 0) {
if (setSlotActivity() == 0) {
m_status = RK_UPGRADE_FINISHED;
}
} else if (strcmp(misc_func, "wipe_userdata") == 0) {
if (wipe_userdata(0) == 0) {
m_status = RK_UPGRADE_FINISHED;
}
} else if (strcmp(misc_func, "update") == 0) {
if (MiscUpdate(image_url, partition, save_path) == 0) {
m_status = RK_UPGRADE_FINISHED;
}
} else if (strcmp(misc_func, "display") == 0) {
miscDisplay();
} else {
LOGE("unknow misc cmdline : %s.\n", misc_func);
return 0;
}
} else if (custom_define != NULL) {
if (strcmp(custom_define, "read") == 0) {
if (readCustomMiscCmdline())
return -1;
} else if (strcmp(custom_define, "write") == 0) {
if (writeCustomMiscCmdline())
return -1;
} else if (strcmp(custom_define, "clean") == 0) {
if (cleanCustomMiscCmdline())
return -1;
} else {
LOGI("Not supported\n");
return m_status;
}
m_status = RK_UPGRADE_FINISHED;
}
// 触发系统重启
if (is_reboot && (m_status == RK_UPGRADE_FINISHED)) {
sync();
//reboot(RB_AUTOBOOT);
if (system(" echo b > /proc/sysrq-trigger ") == -1)
return -1;
}
return m_status;
}
3.1 解析命令行参数
由于updateEngine
是通过传参来实现固件升级的,因此不难猜出updateEngine
是基于命令行参数的程序。
main
函数首先输出当前update_engine
版本信息,比如:
LOG_INFO: *** update_engine: V1.0.1-g28f720bc5-240524-dirty ***
接着使用了getopt_long
函数来解析命令行参数,并根据参数的不同执行相应的操作。
其中-h
,调用display
输出支持的命令信息:
void display(void)
{
LOGI("--misc=now Linux A/B mode: Setting the current partition to bootable.\n");
LOGI("--misc=other Linux A/B mode: Setting another partition to bootable.\n");
LOGI("--misc=update Recovery mode: Setting the partition to be upgraded.\n");
LOGI("--misc=display Display misc info.\n");
LOGI("--misc=wipe_userdata Format data partition.\n");
LOGI("--misc_custom= < op > Operation on misc for custom cmdline");
LOGI(" op: read Read custom cmdline to /tmp/custom_cmdline");
LOGI(" write Write /tmp/custom_cmdline to custom area");
LOGI(" clean clean custom area");
LOGI("--update Upgrade mode.\n");
LOGI("--partition=0x3FFC00 Set the partition to be upgraded.(NOTICE: OTA not support upgrade loader and parameter)\n");
LOGI(" 0x3FFC00: 0011 1111 1111 1100 0000 0000.\n");
LOGI(" uboot trust boot recovery rootfs oem\n");
LOGI(" uboot_a uboot_b boot_a boot_b system_a system_b.\n");
LOGI(" 100000000000000000000000: Upgrade loader\n");
LOGI(" 010000000000000000000000: Upgrade parameter\n");
LOGI(" 001000000000000000000000: Upgrade uboot\n");
LOGI(" 000100000000000000000000: Upgrade trust\n");
LOGI(" 000010000000000000000000: Upgrade boot\n");
LOGI(" 000001000000000000000000: Upgrade recovery\n");
LOGI(" 000000100000000000000000: Upgrade rootfs\n");
LOGI(" 000000010000000000000000: Upgrade oem\n");
LOGI(" 000000001000000000000000: Upgrade uboot_a\n");
LOGI(" 000000000100000000000000: Upgrade uboot_b\n");
LOGI(" 000000000010000000000000: Upgrade boot_a\n");
LOGI(" 000000000001000000000000: Upgrade boot_b\n");
LOGI(" 000000000000100000000000: Upgrade system_a\n");
LOGI(" 000000000000010000000000: Upgrade system_b\n");
LOGI(" 000000000000001000000000: Upgrade misc\n");
LOGI(" 000000000000000100000000: Upgrade userdata\n");
LOGI("--reboot Restart the machine at the end of the program.\n");
LOGI("--version_url=url The path to the file of version.\n");
LOGI("--image_url=url Path to upgrade firmware.\n");
LOGI("--savepath=url save the update.img to url.\n");
LOGI("--version the version of updateEngine\n");
LOGI("--rkdebug Log output to serial port\n");
LOGI("--ui_rotation UI rotation,has 4 angles(0-3).\n");
}
我们在使用updateEngine
中最常使用的命令格式如下:
updateEngine --image_url=http://192.168.0.200:8080/recovery/update.img --misc=update --savepath=/userdata/update.img --reboot
因此我们就以该命令为例,进行分析;
--image_url
:进入case 'i' + 'u'
分支,因此设置变量image_url='http://192.168.0.200:8080/recovery/update.img'
;--misc
:进入case 'm'
分支,因此设置变量misc_func='update'
;--savepath
:进入case 's'
分支,因此设置变量save_path='/userdata/update.img'
;--reboot
:进入case 'r'
,因此设备变量is_reboot=true
。
3.2 misc
分支
由于设置了misc
,所以进入misc_func
相关分支,并执行MiscUpdate
函数;
if (strcmp(misc_func, "now") == 0) {
if (setSlotSucceed() == 0) {
m_status = RK_UPGRADE_FINISHED;
}
} else if (strcmp(misc_func, "other") == 0) {
if (setSlotActivity() == 0) {
m_status = RK_UPGRADE_FINISHED;
}
} else if (strcmp(misc_func, "wipe_userdata") == 0) {
if (wipe_userdata(0) == 0) {
m_status = RK_UPGRADE_FINISHED;
}
} else if (strcmp(misc_func, "update") == 0) { // 走这里
if (MiscUpdate(image_url, partition, save_path) == 0) {
m_status = RK_UPGRADE_FINISHED;
}
} else if (strcmp(misc_func, "display") == 0) {
miscDisplay();
} else {
LOGE("unknow misc cmdline : %s.\n", misc_func);
return 0;
}
3.3 MiscUpdate
MiscUpdate
函数同样位于main.c
文件,函数有三个参数,依次传入:
url
:即--image_url
指定的值;update_partition
:即--partition
指定的值;save_path
:即--savepath
指定的值;
函数原型如下:
void handle_upgrade_callback(void *user_data, RK_Upgrade_Status_t status)
{
if (status == RK_UPGRADE_FINISHED) {
LOGI("rk ota success.\n");
setSlotActivity();
}
m_status = status;
LOGI("rk m_status = %d.\n", m_status);
}
void handle_print_callback(char *szPrompt)
{
if (cmd_pipe != NULL) {
fprintf(cmd_pipe, "ui_print %s\n", szPrompt);
}
}
static int MiscUpdate(char *url, char *update_partition, char *save_path)
{
int partition;
char *savepath = NULL;
int slot = -1;
if (url == NULL) {
// 如果没有传入URL,则可以去查找是否有存在
LOGE("MiscUpdate URL must be set.\n");
return -1;
}
// 从cmdline获取从哪里引导
slot = getCurrentSlot();
// 没有传入要升级的分区,默认升级
if (update_partition == NULL) {
// recovery模式
if (slot == -1) {
// recovery mdoe
// uboot/trust/boot/recovery/rootfs/oem
partition = 0X3F0000; // 0011 1111 0000 0000 0000 0000
} else { // linux A/B模式
// A/B mdoe
// uboot_a/uboot_b/boot_a/boot_b/system_a/system_b
partition = 0XFC00;
}
} else {
partition = strtol(update_partition + 2, NULL, 16);
}
// 可以看到如果save_path为空,其值设置为url
if (save_path == NULL) {
savepath = url;
} else {
savepath = save_path;
}
// 保存升级固件源路径和目标路径到全局变量_url和_save_path
RK_ota_set_url(url, savepath);
LOGI("url = %s.\n", url);
LOGI("[%s:%d] save path: %s\n", __func__, __LINE__, savepath);
// If it's recovery mode, upgrade recovery in normal system. 进入这里
if (slot == -1 && !is_sdboot && !is_usbboot) {
// 在normal系统,升级recovery分区;升级完函数就返回了,并不会升级其他分区
if (partition & 0x040000) {
LOGI("update recovery in normal system.\n");
// 将recovery分区标志位清除
partition = partition & 0xFBFFFF;
// upgrade recoery in normal system 对需要执行的升级命令打标记(这里标记了parameter、recovery升级命令)
if (!RK_ota_set_partition(0x040000)) {
LOGE("ota file is error.\n");
return -1;
}
// 进行parameter、recovery分区升级
RK_ota_start(handle_upgrade_callback, handle_print_callback);
if (m_status != RK_UPGRADE_FINISHED) {
return -1;
}
// 往misc偏移16k位置写入recovery信息,这样系统重启后会进入recovery系统执行剩余分区的升级
struct bootloader_message msg;
memset(&msg, 0, sizeof(msg));
char recovery_str[] = "recovery\n--update_package=";
strcpy(msg.command, "boot-recovery");
sprintf(msg.recovery, "%s%s\n", recovery_str, savepath);
// 写入剩余需要升级的分区
memcpy(msg.needupdate, &partition, 4);
if (true == rkdebug) {
strcat(msg.recovery, "--rkdebug\n");
}
if (NULL != ui_rotation) {
strcat(msg.recovery, "--ui_rotation=");
strcat(msg.recovery, ui_rotation);
msg.recovery[strlen(msg.recovery)] = '\n';
}
set_bootloader_message(&msg);
return 0;
}
} else if (slot == 0) {
LOGI("In A system, now upgrade B system.\n");
partition = partition & 0x155ff;
} else if (slot == 1) {
LOGI("In B system, now upgrade A system.\n");
partition = partition & 0x1a9ff;
}
// 对需要执行的升级命令打标记
if (!RK_ota_set_partition(partition)) {
LOGE("ota file is error.\n");
return -1;
}
// 升级分区
RK_ota_start(handle_upgrade_callback, handle_print_callback);
if (m_status != RK_UPGRADE_FINISHED) {
return -1;
}
return 0;
}
如果我们执行的是如下升级命令:
updateEngine --image_url=http://192.168.0.200:8080/recovery/update.img --misc=update --savepath=/userdata/update.img --reboot
由于未指定升级分区,并且使用的是recovery
模式,默认会升级uboot/trust/boot/recovery/rootfs/oem
分区。升级顺序可以分为两部分;
normal
系统下的升级:升级recovery
分区,并修改misc
分区,接着MiscUpdate
函数返回true
;由于升级命令指定了reboot
参数,main
函数会执行系统重启命令;recovery
系统下的升级:系统重启之后,会根据misc
分区存放的字段来判断将要引导的系统是normal
系统还是recovery
系统,这里会进入到recovery
系统,由于recovery.img
中的ramdisk
根文件系统安装了/etc/init.d/S40recovery
服务,该服务会在系统启动时执行,即执行:/usr/bin/recovery
进行剩余分区的升级。
注意:这里有一点需要补充,如果我们指定了需要升级的分区,并且需要升级的分区中没有recovery
分区,那么会在normal
系统下升级所有分区;换句话说只有需要升级的分区中包含了recovery
分区,那么升级过程才会包含两部分。
3.3.1 normal
系统下的升级
在normal
模式,升级recovery
分区:依次调用RK_ota_set_partition
和RK_ota_start
方法;
(1) 调用RK_ota_set_partition(0x040000)
对需要执行的升级命令打标记,由于只升级recovery
分区,所以函数入参为0x40000
;该函数:
- 会对命令数组
update_cmd
中的parameter
/recovery
升级命令的need_update
成员打标记,即设置为true
; - 设置
parameter
升级命令的dest_path
为/dev/block/by-name/gpt
; - 设置
recovery
升级命令的dest_path
为/dev/block/by-name/recovery
;
(2) 调用RK_ota_start(handle_upgrade_callback, handle_print_callback)
方法执行打了标记的升级命令,有关该函数的细节我们在后面单独介绍;
- 调用
download_file(_url, _save_path)
函数从_url
下载升级固件,文件下载成功设置_url=_save_path
; - 调用
RK_ota_set_partition(-1)
函数获取升级固件(存储路径为_url
)的信息; - 遍历每一个升级命令,对于打了标记的升级命令同时升级固件中存在该分区镜像,依次执行升级命令的
cmd
函数,实现分区镜像文件的烧录;parameter
:代码中会跳过该分区的烧录;recovery
:该分区镜像文件烧录会执行;
注意:这里有一点我们需要提一下,比如我们的升级命令是recovery
,那么是如果在升级固件中找到该分区的镜像数据呢?实际上是通过解析固件中的头部信息可以获取到到固件包含的各个分区镜像信息,这些分区镜像信息中就有分区名称,recovery
升级命令就是查找分区名称为recovery
的镜像文件,如果找不到就不会执行该升级命令。
(3) 调用set_bootloader_message(&msg)
往misc
分区偏移16k
位置写入recovery
信息,这样系统重启会进入recovery
系统;
3.3.2 recovery
系统下的升级
由于在recovery
系统下执行的是/usr/bin/recovery
二进制文件,该文件由源码external/recovery
编译所得,由于我们配置了-DUSE_UPDATEENGINE=ON
,因此recovery
调用updateEngine
实现其它分区的升级,其过程大致如下;
(1) 调用RK_ota_set_partition(partition)
对需要执行的升级命令打标记,partition
值来自misc
分区bootloader_message
结构体的needupdate
参数;该函数:
- 会对命令数组
update_cmd
中的parameter
/uboot
/trust
/boot
/rootfs
/oem
升级命令的need_update
成员打标记,即设置为true
; - 设置
parameter
升级命令的dest_path
为/dev/block/by-name/gpt
; - 设置
uboot
升级命令的dest_path
为/dev/block/by-name/uboot
; - 设置
trust
升级命令的dest_path
为/dev/block/by-name/trust
; - 设置
boot
升级命令的dest_path
为/dev/block/by-name/boot
; - 设置
rootfs
升级命令的dest_path
为/dev/block/by-name/rootfs
; - 设置
oem
升级命令的dest_path
为/dev/block/by-name/oem
;
(2) 调用RK_ota_start(handle_upgrade_callback, handle_print_callback)
方法执行打了标记的升级命令;
- 调用
download_file(_url, _save_path)
函数从_url
下载升级固件,文件下载成功设置_url=_save_path
; - 调用
RK_ota_set_partition(-1)
函数获取升级固件(存储路径为_url
)的信息; - 遍历每一个升级命令,对于打了标记的升级命令同时升级固件中存在该分区镜像,依次执行升级命令的
cmd
函数,实现分区镜像文件的烧录;parameter
:代码中会跳过该分区的烧录;uboot
:该分区镜像文件烧录会执行;trust
:升级固件中没有该分区镜像,因此烧录不会执行;boot
:该分区镜像文件烧录会执行;rootfs
:该分区镜像文件烧录会执行;oem
:该分区镜像文件烧录会执行;
(3) 系统重启会进入normal
系统;
有关recovery
的细节我们需要单独介绍。
3.4 update.c
update.c
文件用于实现固件的的升级操作。
3.4.1 UPDATE_CMD
UPDATE_CMD
定义在update.h
文件,用于对指定分区进行升级操作;
typedef int (*update_func)(char *src_path, void* pupdate_cmd);
typedef struct {
char name[32]; // 升级命令名称(分区镜像名称)
bool need_update; // 是否执行该命令升级
bool is_ab; // Linux A/B模式
long long size; // 用于存放分区镜像文件在update.img中所占数据大小
long long offset; // 用于存放分区镜像文件在update.img中的偏移
long long flash_offset; // 目标分区节点起始偏移
char dest_path[100]; // 目标地址,这里存放的是目标分区设备节点名称,比如/dev/block/by-name/recovery;由于在linux中一切设备皆文件,因此对该设备节点进行读写就是读写该分区
bool skip_verify; // 是否跳过MD5校验
update_func cmd; // 执行分区镜像烧录的函数
} UPDATE_CMD, *PUPDATE_CMD;
比如在update.c
中定义了如下分区的升级命令;
// 升级命令数组,分别用于升级不同的分区
UPDATE_CMD update_cmd[] = {
{"bootloader", false, false, 0, 0, 0, "", false, flash_bootloader},
{"parameter", false, false, 0, 0, 0, "", false, flash_parameter},
{"uboot", false, false, 0, 0, 0, "", false, flash_normal},
{"trust", false, false, 0, 0, 0, "", false, flash_normal},
{"boot", false, true, 0, 0, 0, "", false, flash_normal},
{"recovery", false, false, 0, 0, 0, "", false, flash_normal},
{"rootfs", false, true, 0, 0, 0, "", false, flash_normal},
{"oem", false, false, 0, 0, 0, "", false, flash_normal},
{"uboot_a", false, false, 0, 0, 0, "", false, flash_normal},
{"uboot_b", false, false, 0, 0, 0, "", false, flash_normal},
{"boot_a", false, false, 0, 0, 0, "", false, flash_normal},
{"boot_b", false, false, 0, 0, 0, "", false, flash_normal},
{"system_a", false, false, 0, 0, 0, "", false, flash_normal},
{"system_b", false, false, 0, 0, 0, "", false, flash_normal},
{"misc", false, false, 0, 0, 0, "", false, flash_normal},
{"userdata", false, false, 0, 0, 0, "", false, flash_normal},
};
3.4.2 RK_ota_set_url
RK_ota_set_url
用于保存升级固件源路径和目标路径到全局变量_url
和_save_path
;
#define DEFAULT_DOWNLOAD_PATH "/tmp/update.img"
static char * _url = NULL;
static char const * _save_path = NULL;
static char _url_dir[128];
void RK_ota_set_url(char *url, char *savepath)
{
LOGI("start RK_ota_url url [%s] save path [%s].\n", url, savepath);
if ( url == NULL ) {
LOGE("RK_ota_set_url : url is NULL.\n");
return ;
}
if (savepath == NULL) {
_save_path = DEFAULT_DOWNLOAD_PATH;
} else {
_save_path = savepath;
}
LOGI("save image to %s.\n", _save_path);
_url = url;
sprintf(_url_dir, "%s", _url);
dirname(_url_dir);
}
该函数会输出url
以及save path
信息,比如:
LOG_INFO: start RK_ota_url url [http://192.168.0.200:8080/recovery/update.img] save path [/userdata/update.img].
LOG_INFO: save image to /userdata/update.img.
3.4.3 RK_ota_set_partition
RK_ota_set_partition
根据传入参数不同,执行不同的行为;
如果入参值为-1
:
- 解析固件
update.img
头部信息,解析原始固件update.raw.img
头部信息,存储到rkimage_hdr
(包含了各个分区镜像的名称、偏移和大小等信息); - 升级命令匹配,并且升级固件
update.img
有该分区镜像信息(通过升级名称名称查找匹配的分区镜像),更新升级命令中的offset
、size
,即设置成分区镜像在update.img
文件中的偏移和大小;
如果入参非-1
:
- 根据
partition
位信息对需要执行的升级命令进行标记;
RK_ota_set_partition
函数如下;
点击查看代码
bool RK_ota_set_partition(int partition)
{
//000000000000000000000000: 没有升级分区 0x000000
//100000000000000000000000: 升级bootloader分区 0x800000
//010000000000000000000000: 升级parameter分区 0x400000
//001000000000000000000000: 升级uboot分区 0x200000
//000100000000000000000000: 升级trust分区 0x100000
//000010000000000000000000: 升级boot分区 0x80000
//000001000000000000000000: 升级recovery分区 0x40000
//000000100000000000000000: 升级rootfs分区 0x20000
//000000010000000000000000: 升级oem分区 0x10000
//000000001000000000000000: 升级uboot_a分区 0x8000
//000000000100000000000000: 升级uboot_b分区 0x4000
//000000000010000000000000: 升级boot_a分区 0x2000
//000000000001000000000000: 升级boot_b分区 0x1000
//000000000000100000000000: 升级system_a分区 0x800
//000000000000010000000000: 升级system_b分区 0x400
//000000000000001000000000: 升级misc分区,sdboot使用 0x200
//000000000000000100000000: 升级userdata分区 0x100
// 计算数组长度16
int num = sizeof(update_cmd) / sizeof(UPDATE_CMD);
LOGI("[%s:%d] num [%d]\n", __func__, __LINE__, num);
// 获取镜像文件信息
if (partition == -1) {
// 设置目标分区大小
RKIMAGE_HDR rkimage_hdr;
// 解析固件update.img头部信息,解析原始固件update.raw.img头部信息,存储到rkimage_hdr(包含了各个分区镜像的偏移和大小)
if ( analyticImage(_url, &rkimage_hdr) != 0) {
LOGE("analyticImage error.\n");
return false;
}
// 遍历每一个升级命令
for (int i = 0; i < num; i++) {
// 对于打了标记需要执行的升级命令
if ( update_cmd[i].need_update || is_sdboot || is_usbboot) {
// 取消标记
update_cmd[i].need_update = false;
// 遍历升级固件update.img中每一个分区镜像信息
for (int j = 0; j < rkimage_hdr.item_count; j++) {
// 升级命令匹配,并且升级固件update.img有该分区镜像信息;更新升级命令中的offset、size
if (strcmp(rkimage_hdr.item[j].name, update_cmd[i].name) == 0) {
LOGI("found rkimage_hdr.item[%d].name = %s.\n", j, update_cmd[i].name);
// 分区镜像偏移4GB,分为两部分存储
if (rkimage_hdr.item[j].file[50] == 'H') {
// 高位值,单位为4GB
update_cmd[i].offset = *((DWORD *)(&rkimage_hdr.item[j].file[51]));
update_cmd[i].offset <<= 32;
// 低位值,不足4GB的部分
update_cmd[i].offset += rkimage_hdr.item[j].offset;
LOGI("offset more than 4G, after adjusting is %lld.\n", update_cmd[i].offset);
} else {
update_cmd[i].offset = rkimage_hdr.item[j].offset;
}
// 分区镜像大于4GB,分为两部分存储
if (rkimage_hdr.item[j].file[55] == 'H') {
// 高位值,单位为4GB
update_cmd[i].size = *((DWORD *)(&rkimage_hdr.item[j].file[56]));
update_cmd[i].size <<= 32;
// 低位值,不足4GB的部分
update_cmd[i].size += rkimage_hdr.item[j].size;
LOGI("size more than 4G, after adjusting is %lld.\n", update_cmd[i].size);
} else {
update_cmd[i].size = rkimage_hdr.item[j].size;
}
if (is_sdboot || is_usbboot) {
update_cmd[i].flash_offset = (long long)rkimage_hdr.item[j].flash_offset * SECTOR_SIZE;
}
// 设置为true
update_cmd[i].need_update = true;
continue ;
}
}
}
}
// 非sd卡/usb启动
if (!is_sdboot && !is_usbboot) {
// 遍历每一个升级命令
for ( int i = 0; i < num; i++ ) {
// 对于没有打标记的升级命令
if (*update_cmd[i].dest_path && (update_cmd[i].need_update == false)) {
unsigned char len = strlen(update_cmd[i].name);
// 对于recovery模式,这里并不会进入
if (update_cmd[i].name[len - 2] == '_' && (update_cmd[i].name[len - 1] == 'a' || update_cmd[i].name[len - 1] == 'b')) {
char slot_find = (update_cmd[i].name[len - 1] == 'a') ? 'b' : 'a';
......
}
}
}
}
// for ( int i=0; i<num; i++ ) {
// printf ( "[%s:%d] update_cmd[%d].name [%s] dest path [%s] flash offset [%#llx] offset [%#llx] size [%#llx] \n",
// __func__, __LINE__, i, update_cmd[i].name, update_cmd[i].dest_path, update_cmd[i].flash_offset, update_cmd[i].offset, update_cmd[i].size);
// }
return true;
}
// 遍历每一个升级命令, 对需要执行升级操作的命令进行标记
for (int i = 0; i < num; i++) {
// For OTA and SD update MUST read gpt from update***.img
// 首次循环,执行paramert升级命令;之后的循环根据partition位映射升级相应分区
if ( (partition & 0x800000 || is_sdboot || is_usbboot || (strcmp(update_cmd[i].name, "parameter") == 0) ) ) {
LOGI("need update %s.\n", update_cmd[i].name);
update_cmd[i].need_update = true;
// sd卡/usb启动特殊处理
if (is_sdboot || is_usbboot) {
memset(update_cmd[i].dest_path, 0, sizeof(update_cmd[i].dest_path) / sizeof(update_cmd[i].dest_path[0]));
if (strcmp(update_cmd[i].name, "parameter") == 0) {
sprintf(update_cmd[i].dest_path, "%s/gpt", _url_dir);
} else {
sprintf(update_cmd[i].dest_path, "%s/%s", _url_dir, update_cmd[i].name);
}
} else { // 走这里
// 如果是parameter升级,设置升级的分区的名称
if (strcmp(update_cmd[i].name, "parameter") == 0) {
sprintf(update_cmd[i].dest_path, "/dev/block/by-name/gpt");
} else {
// 判断是不是MTD设备(Nor/Nand Flash设备),对于SD、eMMC不属于MTD设备
if (!isMtdDevice()) { // 进入
// 设置各个升级命令 => 目标分区名称 /dev/block/by-name/分区名
sprintf(update_cmd[i].dest_path, "/dev/block/by-name/%s", update_cmd[i].name);
} else {
if ( update_cmd[i].need_update && (mtd_scan_partitions() > 0) ) {
const MtdPartition *mtdp = mtd_find_partition_by_name(update_cmd[i].name);
if (mtdp) {
sprintf(update_cmd[i].dest_path, "/dev/mtd%d", mtdp->device_index);
LOGI("need update %s ,.dest_path: %s.\n", update_cmd[i].name, update_cmd[i].dest_path);
}
} else {
sprintf(update_cmd[i].dest_path, "/dev/block/by-name/%s", update_cmd[i].name);
}
}
}
}
}
partition = (partition << 1);
}
return true;
}
3.3.4 RK_ota_start
RK_ota_start
对命令数组update_cmd
中打了标记的命令所指向的分区进行升级,函数接收参数:
RK_upgrade_callback
:升级状态回调函数,第二个参数为状态枚举值RK_Upgrade_Status_t
;RK_print_callback
:升级状态回调函数,参数是一个字符指针;
这两个函数在我看来没多大区别,都是用来输出升级状态信息的。
RK_ota_start
函数比较复杂,主要可以分为以下几个流程;
- 调用
download_file(_url, _save_path)
函数从_url
下载升级固件,文件下载成功设置_url=_save_path
;- 如果是从远端下载的文件则会保存到
_save_path
; - 否则则认为文件位于本地
_url
,并不会保存到_save_path
;因此本地升级时,需要将_save_path
设置成和_url
一样的路径;
- 如果是从远端下载的文件则会保存到
- 调用
RK_ota_set_partition(-1)
函数获取升级固件_url
的信息;- 解析固件
update.img
头部信息,解析原始固件update.raw.img
头部信息,存储到rkimage_hdr
(包含了各个分区镜像的偏移和大小); - 升级命令匹配,并且升级固件
update.img
有该分区镜像信息,更新升级命令中的offset
、size
,即设置成分区镜像在update.img
文件中的偏移和大小;
- 解析固件
- 遍历每一个升级命令,对于打了标记的升级命令,依次执行如下操作;
- 执行升级操作,即执行升级命令的
cmd
函数,比如recovery
升级命令调用flash_normal(_url,update_cmd[5])
进行分区镜像文件的烧录; - 除了
parameter
和bootloader
升级命令外,执行分区校验操作,即校验烧录到系统的分区镜像(/dev/block/by-name/
分区名)和分区镜像文件(位于update.img
偏移offset
长度size
的数据)的MD5
值是否一样;
- 执行升级操作,即执行升级命令的
函数原型如下:
点击查看代码
void RK_ota_start(RK_upgrade_callback cb, RK_print_callback print_cb)
{
LOGI("start RK_ota_start.\n");
processvalue = 95;
cb(NULL, RK_UPGRADE_START);
//确认升级路径
if (_url == NULL) {
LOGE("url is NULL\n");
cb(NULL, RK_UPGRADE_ERR);
return ;
}
// 1. 获取文件
int res = download_file(_url, _save_path);
// 如果文件系在成功,__url修改为文件的保存路径
if (res == 0) {
_url = (char *)_save_path;
} else if (res == -1) {
LOGE("download_file error.\n");
cb(NULL, RK_UPGRADE_ERR);
return ;
}
// 2. 获取文件信息
if (!RK_ota_set_partition(-1)) {
LOGE("RK_ota_set_partition failed.\n");
cb(NULL, RK_UPGRADE_ERR);
return ;
}
STRUCT_PARAM_ITEM param_item[20] = {0};
long long gpt_backup_offset = -1;
memset(param_item, 0, sizeof(param_item));
flash_register_partition_data(param_item, &gpt_backup_offset);
// 是不是MTD设备
int is_mtd_flag = isMtdDevice();
// 3. 烧录文件到分区并校验
int num = sizeof(update_cmd) / sizeof(UPDATE_CMD);
char prompt[128] = {0};
for (int i = 0; i < num; i++ ) {
// 处理需要升级的分区
if (update_cmd[i].need_update) {
if (update_cmd[i].cmd != NULL) {
LOGI("now write %s to %s.\n", update_cmd[i].name, update_cmd[i].dest_path);
sprintf(prompt, "[%s] upgrade start...\n", update_cmd[i].name);
print_cb(prompt);
// 如果是misc、parameter升级命令,直接跳过
if (!is_sdboot && !is_usbboot &&
( (strcmp(update_cmd[i].name, "misc") == 0) ||
(strcmp(update_cmd[i].name, "parameter") == 0) )) {
LOGI("ingore misc.\n");
continue;
}
// 下载固件到分区
LOGI("update_cmd.flash_offset = %lld.\n", update_cmd[i].flash_offset);
// 执行升级命令的升级函数cmd,比如recovery => flash_normal(_url,update_cmd[i])
if (update_cmd[i].cmd(_url, (void*)(&update_cmd[i])) != 0) {
LOGE("update %s error.\n", update_cmd[i].dest_path);
sprintf(prompt, "[%s] upgrade fail\n", update_cmd[i].name);
print_cb(prompt);
cb(NULL, RK_UPGRADE_ERR);
return ;
} else {
sprintf(prompt, "[%s] upgraede success!\n", update_cmd[i].name);
print_cb(prompt);
}
// sd卡启动
if (is_sdboot) {
if (ota_recovery_cmds(update_cmd[i].flash_offset, update_cmd[i].dest_path)) {
LOGE("write recovery cmds to %s failed.\n", CMD4RECOVERY_FILENAME);
cb(NULL, RK_UPGRADE_ERR);
return ;
}
LOGI("not check in sdboot (sdcard).\n");
continue;
} else if (is_usbboot) { // usb启动
if (ota_recovery_cmds(update_cmd[i].flash_offset, update_cmd[i].dest_path)) {
LOGE("write recovery cmds to %s failed.\n", CMD4RECOVERY_UDISK_FILENAME);
cb(NULL, RK_UPGRADE_ERR);
return ;
}
LOGI("not check in usb storage (udisk).\n");
continue;
}
// parameter和bootloader先不校验
if (strcmp(update_cmd[i].name, "parameter") == 0 || strcmp(update_cmd[i].name, "bootloader") == 0) {
LOGI("not check parameter and loader.\n");
continue;
}
// 校验分区 即烧录到eMMC分区的镜像和原镜像分区MD5校验值是否一致,如果不一样说明烧录的有问题
if (!update_cmd[i].skip_verify &&
comparefile(update_cmd[i].dest_path, _url,
update_cmd[i].flash_offset,
update_cmd[i].offset, update_cmd[i].size)) {
LOGI("check %s ok.\n", update_cmd[i].dest_path);
} else if (!update_cmd[i].skip_verify) {
LOGE("check %s failed.\n", update_cmd[i].dest_path);
cb(NULL, RK_UPGRADE_ERR);
return ;
}
}
}
}
/*
* Fix if update_xxx.img not found some A/B partition image.
*/
if (is_sdboot || is_usbboot) {
for (int i = 0; i < num; i++) {
if ( (!update_cmd[i].need_update) || (update_cmd[i].cmd == NULL)) {
continue;
}
unsigned char len = strlen(update_cmd[i].name);
// rocovery模式不会进入
if (update_cmd[i].name[len - 2] == '_' && (update_cmd[i].name[len - 1] == 'a' || update_cmd[i].name[len - 1] == 'b') ) {
......
}
}
// write gpt backup
/*
* char gpt_backup_img_path[100] = {0};
* sprintf(gpt_backup_img_path, "%s/%s", _url_dir, GPT_BACKUP_FILE_NAME);
* if (ota_recovery_cmds(gpt_backup_offset, gpt_backup_img_path)) {
* LOGE("write gpt backup to recovery cmds failed (%s).\n", gpt_backup_img_path);
* cb(NULL, RK_UPGRADE_ERR);
* return ;
* }
*/
}
// 4. 升级完成
LOGI("RK_ota_start is ok!");
processvalue = 100;
cb(NULL, RK_UPGRADE_FINISHED);
print_cb((char *)"updateEngine upgrade OK!\n");
/* We're successful upgraded, Remove the done_file(see flash_image.cpp) if exist. */
{
char done_file[256];
snprintf(done_file, 256, "%s.done", _url);
unlink(done_file);
}
}
四、镜像及烧录相关
4.1 rkimage.c
rkimage.c
文件主要用于解析rockchip
升级固件update.img
的内容。
4.1.1 analyticImage
analyticImage
函数用于解析升级固件,获取固件头部信息,函数接收两个参数:
filepath
:固件文件在系统的存储位置,比如/userdata/update.img
;phdr
:用于存储解析后的原始固件update.raw.img
头部信息。
函数原型如下:
点击查看代码
// defineHeader.h
typedef enum { // 每个枚举常量的赋值决定了其在内存中所占的大小,占用4个字节
RKNONE_DEVICE = 0,
RK27_DEVICE = 0x10,
RKCAYMAN_DEVICE,
RK28_DEVICE = 0x20,
RK281X_DEVICE,
RKPANDA_DEVICE,
RKNANO_DEVICE = 0x30,
RKSMART_DEVICE,
RKCROWN_DEVICE = 0x40,
RK29_DEVICE = 0x50,
RK292X_DEVICE,
RK30_DEVICE = 0x60,
RK30B_DEVICE,
RK31_DEVICE = 0x70,
RK32_DEVICE = 0x80
} ENUM_RKDEVICE_TYPE;
.......
// rkimage.h
#pragma pack(1)
typedef struct { // 时间 一共占用7个字节
USHORT usYear; // 2个字节
BYTE ucMonth; // 1个字节
BYTE ucDay; // 1个字节
BYTE ucHour; // 1个字节
BYTE ucMinute; // 1个字节
BYTE ucSecond; // 1个字节
} STRUCT_RKTIME, *PSTRUCT_RKTIME;
typedef struct tagRKIMAGE_ITEM { // 分区镜像信息 112个字节
char name[PART_NAME]; // 分区名称 32个字节 比如parameter
char file[RELATIVE_PATH]; // 镜像文件名称 64个字节 比如parameter.txt
unsigned int offset; // 偏移 4个字节
unsigned int flash_offset; // 4个字节
unsigned int usespace; // 4个字节
unsigned int size; // 分区镜像大小 4个字节
} RKIMAGE_ITEM, *PRKIMAGE_ITEM;
typedef struct tagRKIMAGE_HDR { // 原始固件update.raw.img头部信息 3724个字节
unsigned int tag; // 4个字节
unsigned int size; // 4个字节
char machine_model[MAX_MACHINE_MODEL]; // 64个字节
char manufacturer[MAX_MANUFACTURER]; // 60个字节
unsigned int version; // 4个字节
int item_count; // 4个字节
RKIMAGE_ITEM item[MAX_PACKAGE_FILES]; // 112*32个字节
} RKIMAGE_HDR, *PRKIMAGE_HDR;
typedef struct { // 固件update.img头部信息 102个字节
UINT uiTag; //标志,固定为0x57 0x46 0x4B 0x52 4个字节
USHORT usSize; // 结构体大小 2个字节
DWORD dwVersion; //Image 文件版本 4个字节
DWORD dwMergeVersion; //打包工具版本 4个字节
STRUCT_RKTIME stReleaseTime; //生成时间 7个字节
ENUM_RKDEVICE_TYPE emSupportChip; //使用芯片 4个字节
DWORD dwBootOffset; //bootloade分区镜像偏移 4个字节
DWORD dwBootSize; //bootloade分区镜像大小 4个字节
DWORD dwFWOffset; //原始固件偏移(指的是update.raw.img)4个字节
DWORD dwFWSize; //原始固件大小(指的是update.raw.img)4个字节
BYTE reserved[61]; //预留空间,用于存放不同固件特征 61个字节
} STRUCT_RKIMAGE_HEAD, *PSTRUCT_RKIMAGE_HEAD;
......
#pragma pack()
// rkimage.c
// 解析固件,获得固件头部信息
int analyticImage(const char *filepath, PRKIMAGE_HDR phdr)
{
// 用于存存放update.img除了MD5校验数据外的文件大小
long long ulFwSize;
STRUCT_RKIMAGE_HEAD rkimage_head;
// 存放MD5校验数据,一共32个字节
unsigned char m_md5[32];
// 打开升级固件文件update.img
int fd = open(filepath, O_RDONLY);
if (fd < 0) {
LOGE("Can't open %s\n", filepath);
return -2;
}
// 1. image 头部信息读取
if (read(fd, &rkimage_head, sizeof(STRUCT_RKIMAGE_HEAD)) != sizeof(STRUCT_RKIMAGE_HEAD)) {
LOGE("Can't read %s\n(%s)\n", filepath, strerror(errno));
close(fd);
return -2;
}
// 判断预留空间偏移14、15是不是HI,即升级固件update.img偏移0x37、0x38
if ((rkimage_head.reserved[14] == 'H') && (rkimage_head.reserved[15] == 'I')) {
// 如果原始固件update.raw.img大小大于4GB,0x39开始的4个字节存放的是原始固件update.raw.img大小/4GB的(即单位是4GB)
ulFwSize = *((DWORD *)(&rkimage_head.reserved[16]));
ulFwSize <<= 32;
ulFwSize += rkimage_head.dwFWOffset;
ulFwSize += rkimage_head.dwFWSize;
} else {
// 原始固件update.raw.img在update.img中的偏移 + 原始固件update.raw.img大小
ulFwSize = rkimage_head.dwFWOffset + rkimage_head.dwFWSize;
}
// 计算原始固件update.raw.img的大小
rkimage_head.dwFWSize = ulFwSize - rkimage_head.dwFWOffset;
// 输出固件update.img头部信息
display_head(&rkimage_head);
// 2. 固件md5 校验
long long fileSize;
int nMd5DataSize;
// 定位文件末尾,获取文件大小fileSize
fileSize = lseek64(fd, 0L, SEEK_END);
// 文件大小fileSize减去ulFwSize,即update.img除了MD5校验数据外的文件大小
nMd5DataSize = fileSize - ulFwSize;
if (nMd5DataSize >= 160) {
LOGE("md5 : not support sign image.\n");
//sign image
//m_bSignFlag = true;
//m_signMd5Size = nMd5DataSize-32;
//fseeko64(m_pFile,ulFwSize,SEEK_SET);
//fread(m_md5,1,32,m_pFile);
//fread(m_signMd5,1,nMd5DataSize-32,m_pFile);
} else {
// 定位到文件末尾向前32个字节,并尝试读取这32个字节数据到m_md5数组中
lseek64(fd, -32, SEEK_END);
if ( read(fd, m_md5, 32) != 32) {
LOGE("lseek failed.\n");
close(fd);
return -2;
}
}
// 3. image 地址信息读取 将文件指针移动到rkimage_head.dwFWOffset指定的位置,即读取原始固件update.raw.img头部信息
if (lseek64(fd, rkimage_head.dwFWOffset, SEEK_SET) == -1) {
LOGE("lseek failed.\n");
close(fd);
return -2;
}
if (read(fd, phdr, sizeof(RKIMAGE_HDR)) != sizeof(RKIMAGE_HDR)) {
LOGE("Can't read %s\n(%s)\n", filepath, strerror(errno));
close(fd);
return -2;
}
// 校验标签:0x46414B52
if (phdr->tag != RKIMAGE_TAG) {
LOGE("tag: %x\n", phdr->tag);
LOGE("Invalid image\n");
close(fd);
return -3;
}
// 偏移0x80 0x81
if ((phdr->manufacturer[56] == 0x55) && (phdr->manufacturer[57] == 0x66)) {
USHORT *pItemRemain;
pItemRemain = (USHORT *)(&phdr->manufacturer[58]);
phdr->item_count += *pItemRemain;
}
// 由于phdr存放的各个分区镜像的偏移都是相对原始固件update.raw.img,因此调整各个分区镜像的偏移配置为相对固件update.img,即偏移+update.raw.img在update.img的偏移
if (rkimage_head.dwFWOffset) {
adjustFileOffset(phdr, rkimage_head.dwFWOffset, rkimage_head.dwBootOffset, rkimage_head.dwBootSize);
}
// 输出原始固件update.raw.img头部信息
display_hdr(phdr);
close(fd);
#if 1
if (!compareMd5sum((char*)filepath, m_md5, 0, fileSize - 32)) {
LOGE("Md5Check update.img fwSize:%ld", fileSize - 32);
return -1;
}
#endif
LOGI("analyticImage ok.\n");
return 0;
}
4.1.1.1 解析update.img
头信息
这里我们可以以二进制方式打开我们编译的统一固件update.img
(buildroot
系统);
# -e little-endian dump (incompatible with -ps,-i,-r).
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp$ xxd -b -e -l 102 output/update/Image/update.img
00000000: 57464b52 00000066 00000100 07e80200 RKFWf...........
00000010: 0b0e1e06 35383824 00006633 0731c000 ....$8853f....1.
00000020: 07322600 d9500400 00000033 00000100 .&2...P.3.......
00000030: 00000000 00000000 00000000 00000000 ................
00000040: 00000000 00000000 00000000 00000000 ................
00000050: 00000000 00000000 00000000 00000000 ................
00000060: 00000000 0000 ......
以该文件为例,我们以小端方式(大多数ARM
处理器在默认情况下采用小端序:数据的低位字节存储在低地址,高位字节存储在高地址)来解析,解析到的STRUCT_RKIMAGE_HEAD
内容如下:
typedef struct { // 102个字节
UINT uiTag; //标志,固定为0x57 0x46 0x4B 0x52 4个字节,偏移0x00开始:0x52 0x4B 0x46 0x57
USHORT usSize; // 结构体大小 2个字节,偏移0x04开始:0x66 0x00,解析出来为102
DWORD dwVersion; //Image 文件版本 4个字节,偏移0x06开始:0x00 0x00 0x00 0x01
DWORD dwMergeVersion; //打包工具版本 4个字节,偏移0x0A开始:0x00 0x00 0x00 0x02
STRUCT_RKTIME stReleaseTime; //生成时间 7个字节,偏移0x0E开始:0xE8 0x07 0x06 0x1E 0x0E 0x0B 0x24, 解析生时间就是2024-06-30 14:11:24
ENUM_RKDEVICE_TYPE emSupportChip; //使用芯片 4个字节,偏移0x15开始:0x38 0x38 0x35 0x33,解析成字符串就是3588
DWORD dwBootOffset; //bootloade分区镜像偏移 4个字节,偏移0x19开始:0x66 0x00 0x00 0x00,解析成数字就是102
DWORD dwBootSize; //bootloade分区镜像大小 4个字节,偏移0x1D开始:0x0c 0x31 0x07 0x00,解析成数字就是471308
DWORD dwFWOffset; //原始固件update.raw.img偏移 4个字节,偏移0x21开始:0x26 0x32 0x07 0x00,解析成数字就是471590
DWORD dwFWSize; //原始固件update.raw.img大小 4个字节,偏移0x25开始:0x04 0x50 0xD9 0x33,解析成数字就是869879812,output/update/Image/update.raw.img大小
BYTE reserved[61]; //预留空间,用于存放不同固件特征 61个字节
} STRUCT_RKIMAGE_HEAD, *PSTRUCT_RKIMAGE_HEAD;
因此我们大概可以了解到update.img
的文件结构:
- 第一部分
0x00~0x65
:固件头信息,一共102
个字节; - 第二部分
0x66~0x073171
:bootloader
分区镜像移除最后180
个字节之后的数据,一共471308
个字节; - 第三部分
0x073172~0x073225
:bootloader
分区镜像最后180
个字节,一共180
个字节; - 第四部分
0x073226~0x33e08229
:存放update.raw.img
,即原始固件,一共869879812
个字节; - 第五部分
0x33e0822A~0x33e08249
:其中最后32
个字节,为MD5
校验数据;
实际上我们的update.img
文件大小为870351434
,102+471308+180+869879812+32=870351434
;
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp$ ll output/update/Image/update.*
-rw-r--r-- 1 root root 870351434 6月 30 14:11 output/update/Image/update.img
-rw-r--r-- 1 root root 869879812 6月 30 14:11 output/update/Image/update.raw.img
bootloader
分区镜像文件为MiniLoaderAll.bin
,其指向u-boot/rk3588_spl_loader_v1.13.112.bin
,大小为471488
,比471308
多了180
个字节;
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp$ ll u-boot/rk3588_spl_loader_v1.13.112.bin
-rw-r--r-- 1 root root 471488 6月 18 01:48 u-boot/rk3588_spl_loader_v1.13.112.bin
update.img
中第二部分+第三部分合在一起就是rk3588_spl_loader_v1.13.112.bin
文件,我们可以通过如下命令来证实:
# 查看rk3588_spl_loader_v1.13.112.bin最后180个字节
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp$ xxd -b -e -s 471308 -l 180 u-boot/rk3588_spl_loader_v1.13.112.bin
0007310c: 0c6526d5 54ecb1cb 39e54545 ff777bd5 .&e....TEE.9.{w.
0007311c: 38c8c09b 4a6ee54e 439f42bb 045ed0d1 ...8N.nJ.B.C..^.
0007312c: 3e000f78 3c07bc06 642a4dc9 cf7743b9 x..>...<.M*d.Cw.
0007313c: 68f8d793 7bb93800 8af3aae8 8b279647 ...h.8.{....G.'.
0007314c: c4c44544 26a1e0ed 37dd6f02 01c69287 DE.....&.o.7....
0007315c: e6a01403 da6c2baa 357598e5 ccbf2c3a .....+l...u5:,..
0007316c: 5f6227b5 9b2adcd0 d74da2d7 d17a6572 .'b_..*...M.rez.
0007317c: e7983a17 b68e573c c93432a0 cdcad16e .:..<W...24.n...
0007318c: 6ebd9581 a65c840e 5f420ccb 3ee016c8 ...n..\...B_...>
0007319c: 8e32f3ac 25025644 1eafcddf b659361d ..2.DV.%.....6Y.
000731ac: d5d15844 6bc304ea 936f8d91 4527281c DX.....k..o..('E
000731bc: 9445a116 ..E.
# 查看update.img第三部分180个字节
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp$ xxd -b -e -s 0x073172 -l 180 output/update/Image/update.img
00073172: 0c6526d5 54ecb1cb 39e54545 ff777bd5 .&e....TEE.9.{w.
00073182: 38c8c09b 4a6ee54e 439f42bb 045ed0d1 ...8N.nJ.B.C..^.
00073192: 3e000f78 3c07bc06 642a4dc9 cf7743b9 x..>...<.M*d.Cw.
000731a2: 68f8d793 7bb93800 8af3aae8 8b279647 ...h.8.{....G.'.
000731b2: c4c44544 26a1e0ed 37dd6f02 01c69287 DE.....&.o.7....
000731c2: e6a01403 da6c2baa 357598e5 ccbf2c3a .....+l...u5:,..
000731d2: 5f6227b5 9b2adcd0 d74da2d7 d17a6572 .'b_..*...M.rez.
000731e2: e7983a17 b68e573c c93432a0 cdcad16e .:..<W...24.n...
000731f2: 6ebd9581 a65c840e 5f420ccb 3ee016c8 ...n..\...B_...>
00073202: 8e32f3ac 25025644 1eafcddf b659361d ..2.DV.%.....6Y.
00073212: d5d15844 6bc304ea 936f8d91 4527281c DX.....k..o..('E
00073222: 9445a116 ..E.
4.1.1.2 解析update.raw.img
头信息
这里我们可以以二进制方式打开原始固件update.raw.img
(buildroot
系统);
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp$ xxd -b -e -l 3724 output/update/Image/update.raw.img
00000000: 46414b52 33d95000 334b5220 00383835 RKAF.P.3 RK3588.
00000010: 00000000 00000000 00000000 00000000 ................
00000020: 00000000 00000000 30200000 00003730 .......... 007..
00000030: 00000000 00000000 00000000 00000000 ................
00000040: 00000000 00000000 334b5220 00383835 ........ RK3588.
00000050: 00000000 00000000 00000000 00000000 ................
00000060: 00000000 00000000 00000000 00000000 ................
00000070: 00000000 00000000 00000000 00000000 ................
00000080: 00000000 01000000 0000000a 6b636170 ............pack 0x8c - 0xfb
00000090: 2d656761 656c6966 00000000 00000000 age-file........
000000a0: 00000000 00000000 00000000 6b636170 ............pack
000000b0: 2d656761 656c6966 00000000 00000000 age-file........
000000c0: 00000000 00000000 00000000 00000000 ................
000000d0: 00000000 00000000 00000000 00000000 ................
000000e0: 00000000 00000000 00000000 00000800 ................
000000f0: ffffffff 00000001 000000e2 61726170 ............para 0xfc - 0x16b parameter分区
00000100: 6574656d 00000072 00000000 00000000 meter...........
00000110: 00000000 00000000 00000000 61726170 ............para
00000120: 6574656d 78742e72 00000074 00000000 meter.txt.......
00000130: 00000000 00000000 00000000 00000000 ................
00000140: 00000000 00000000 00000000 00000000 ................
00000150: 00000000 00000000 00004000 00001000 .........@......
00000160: 00000000 00000001 00000227 746f6f62 ........'...boot 0x16c - 0x1db bootloader分区
00000170: 64616f6c 00007265 00000000 00000000 loader..........
00000180: 00000000 00000000 00000000 696e694d ............Mini
00000190: 64616f4c 6c417265 69622e6c 0000006e LoaderAll.bin...
000001a0: 00000000 00000000 00000000 00000000 ................
000001b0: 00000000 00000000 00000000 00000000 ................
000001c0: 00000000 00000000 00000000 00001800 ................
000001d0: ffffffff 000000e7 000731c0 6f6f6275 .........1..uboo 0x1dc - 0x24b uboot分区
000001e0: 00000074 00000000 00000000 00000000 t...............
000001f0: 00000000 00000000 00000000 6f6f6275 ............uboo
00000200: 6d692e74 00000067 00000000 00000000 t.img...........
00000210: 00000000 00000000 00000000 00000000 ................
00000220: 00000000 00000000 00000000 00000000 ................
00000230: 00000000 00000000 00002000 00075000 ......... ...P..
00000240: 00004000 00000800 00400000 6373696d .@........@.misc 0x24c - 0x2bb misc分区
00000250: 00000000 00000000 00000000 00000000 ................
00000260: 00000000 00000000 00000000 6373696d ............misc
00000270: 676d692e 00000000 00000000 00000000 .img............
00000280: 00000000 00000000 00000000 00000000 ................
00000290: 00000000 00000000 00000000 00000000 ................
000002a0: 00000000 00000000 00002000 00475000 ......... ...PG.
000002b0: 00006000 00000018 0000c000 746f6f62 .`..........boot 0x2bc - 0x32b boot分区
000002c0: 00000000 00000000 00000000 00000000 ................
000002d0: 00000000 00000000 00000000 746f6f62 ............boot
000002e0: 676d692e 00000000 00000000 00000000 .img............
000002f0: 00000000 00000000 00000000 00000000 ................
00000300: 00000000 00000000 00000000 00000000 ................
00000310: 00000000 00000000 00020000 00481000 ..............H.
......
或者直接查看update.img
偏移0x073226
处的内容,两者内容是一致的;
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp$ xxd -b -e -s 0x073226 -l 3724 output/update/Image/update.img
以该文件为例,我们以小端方式(大多数ARM
处理器在默认情况下采用小端序:数据的低位字节存储在低地址,高位字节存储在高地址)来解析,解析到的PRKIMAGE_HDR
内容如下:
typedef struct tagRKIMAGE_HDR { // 原始固件update.raw.img头部信息 364个字节
unsigned int tag; // 4个字节 偏移0x00开始:0x52 0x4b 0x41 0x46
unsigned int size; // 4个字节 偏移0x04开始:0x00 0x50 0xd9 0x33,解析出来为869879808
char machine_model[MAX_MACHINE_MODEL]; // 64个字节 偏移0x08开始 解析成字符串:RK3588
char manufacturer[MAX_MANUFACTURER]; // 60个字节 偏移0x48开始 解析成字符串:RK3588
unsigned int version; // 4个字节 偏移0x84开始 0x00 0x00 0x00 0x01
int item_count; // 4个字节 偏移0x88开始 0x0a 0x00 0x00 0x00
RKIMAGE_ITEM item[MAX_PACKAGE_FILES]; // 112*32个字节 偏移0x8C开始
} RKIMAGE_HDR, *PRKIMAGE_HDR;
4.1.2 display_head
display_head
函数用于输出固件update.img
的头部信息;
static void display_head(PSTRUCT_RKIMAGE_HEAD pHead)
{
LOGD("uiTag = %x.\n", pHead->uiTag);
LOGD("usSize = %x.\n", pHead->usSize);
LOGD("dwVersion = %x.\n", pHead->dwVersion);
UINT btMajor = ((pHead->dwVersion) & 0XFF000000) >> 24;
UINT btMinor = ((pHead->dwVersion) & 0X00FF0000) >> 16;
UINT usSmall = ((pHead->dwVersion) & 0x0000FFFF);
LOGD("btMajor = %x, btMinor = %x, usSmall = %02x.\n", btMajor, btMinor, usSmall);
LOGD("dwBootOffset = %x.\n", pHead->dwBootOffset);
LOGD("dwBootSize = %x.\n", pHead->dwBootSize);
LOGD("dwFWOffset = %x.\n", pHead->dwFWOffset);
LOGD("dwFWSize = %x.\n", pHead->dwFWSize);
}
4.1.3 adjustFileOffset
由于phdr
是解析原始固件update.raw.img
头部信息得到的,其成员item
数组存放的各个分区镜像的偏移都是相对原始固件update.raw.img
,因此调整各个分区镜像的偏移配置为相对固件update.img
;
phdr->item[i].offset += offset
即+update.raw.img
在update.img
的偏移。
此外该函数,还将bootloader
分区镜像的偏移和大小直接设置为rkimage_head.dwBootOffset
、rkimage_head.dwBootSize
。
adjustFileOffset
函数原型如下;
void adjustFileOffset(PRKIMAGE_HDR phdr, int offset, int loader_offset, int loader_size)
{
for (int i = 0; i < phdr->item_count; i++) {
// bootloader分区镜像信息
if ( strcmp(phdr->item[i].name, "bootloader") == 0) {
phdr->item[i].offset = loader_offset;
phdr->item[i].size = loader_size;
continue ;
}
phdr->item[i].offset += offset;
}
}
4.1.4 display_hdr
display_head
函数用于输出原始固件update.raw.img
的头部信息;
static void display_hdr(PRKIMAGE_HDR phdr)
{
//unsigned int tag;
//unsigned int size;
//char machine_model[MAX_MACHINE_MODEL];
//char manufacturer[MAX_MANUFACTURER];
//unsigned int version;
//int item_count;
//RKIMAGE_ITEM item[MAX_PACKAGE_FILES];
LOGD("tag = %d\n", phdr->tag);
LOGD("size = %d\n", phdr->size);
LOGD("machine_model = %s\n", phdr->machine_model);
LOGD("manufacturer = %s\n", phdr->manufacturer);
LOGD("version = %d\n", phdr->version);
LOGD("item = %d.\n", phdr->item_count);
for (int i = 0; i < phdr->item_count; i++) {
LOGI("================================================\n");
display_item(&(phdr->item[i]));
}
}
4.2 flash_image.c
flash_image
文件主要实现了分区镜像烧录的功能。
4.2.1 flash_normal
flash_normal
用于从源文件src_path
中读取指定范围内的数据(偏移pcmd->offset
、大小pcmd->size
),并将其烧录到目标地址pcmd->dest_path
;
这里以recovery
镜像烧写为例进行介绍:
src_path
为固件update.img
的存放地址,比如/userdata/update.img
,pcmd->offset
和pcmd->size
用于从update.img
固件中查找到recovery
镜像文件;pcmd->dest_path
为烧录的目标地址,值为/dev/block/by-name/recovery
;flash_normal
函数首先会读取升级状态文件(比如/userdata/update.img.done
),如果该文件包含>recovery<
字符串,则认为recovery
镜像已经烧录过,直接返回;- 调用
do_patch_rkimg(src_path, pcmd->offset, pcmd->size, pcmd->dest_path, dst_file)
:其中dst_file=/userdata/update.img.recovery
,这个函数代码太长了,我也就没有仔细阅读了; - 如果
do_patch_rkimg
返回0则调用block_write(src_path, pcmd->offset, pcmd->size,pcmd->flash_offset, pcmd->dest_path)
从src_path
(比如/userdata/update.img
)中读取指定范围内的数据,并将其写入到目标文件pcmd->dest_path
(比如/dev/block/by-name/recovery
)的指定位置 recovery
镜像烧写完毕,会记录recovery
分区的升级状态,比如升级成功会往/userdata/update.img.done
文件(文件不存在则创建)写入>recovery<
字符串;- 删除
/userdata/update.img.recovery
文件。
flash_normal
函数原型如下:
点击查看代码
int flash_normal(char *src_path, void *pupdate_cmd)
{
LOGI("%s:%d start.\n", __func__, __LINE__);
PUPDATE_CMD pcmd = (PUPDATE_CMD)pupdate_cmd;
int ret = 0;
// sd卡启动 或者 不是MTD设备
if (is_sdboot || !isMtdDevice()) { // 进入
//block
char dst_file[256];
/* record the already upgraded img name,
* so it can continue on upgrading after unexpected shutdown/reboot
*/
char done_file[256];
/* The pattern writes to done_file, e.g.
* >uboot<
* >boot<
* >rootfs<
*/
char pattern[64];
int fd;
off_t size, len;
char buf[512];
struct stat dst_stat;
LOGI("%s:%d, diff check for %s\n", __func__, __LINE__, pcmd->name);
// dst_file=/userdata/update.img.recovery
snprintf(dst_file, 256, "%s.%s", src_path, pcmd->name);
// done_file=/userdata/update.img.done
snprintf(done_file, 256, "%s.done", src_path);
// 打开一个用于记录固件升级状态的文件(/userdata/update.img.done)
if ((fd = open(done_file, O_RDWR | O_CREAT | O_DSYNC, 0)) < 0 ||
// size保存文件大小
(size = lseek(fd, 0, SEEK_END) < 0) ||
// 设置读取偏移为0
(lseek(fd, 0, SEEK_SET) < 0)) {
LOGE("open %s failed, upgrading abort!\n", done_file);
return -1;
}
memset(buf, 0, 512);
len = 0;
// 读取/userdata/update.img.done文件内容,保存到buf
while (size > 0 && len != size) {
ssize_t val;
// 读取size-len长度内容,存放到buf+len
if ((val = read(fd, buf + len, size - len)) < 0) {
close(fd);
LOGE("read %s failed: %s\n", done_file, strerror(errno));
return val;
}
len += val;
}
snprintf(pattern, 64, ">%s<", pcmd->name);
/* If this partition already upgraded,
* the old image (in flash) could be broken,
* it's fine to skip it. 如果在文件内容中找到了特定的升级模式,比如>recovery<字符串,则认为已经升级过recovery分区,直接返回
*/
if (strstr(buf, pattern) != NULL) {
close(fd);
return 0;
}
/* Otherwise, we shall look inside to make sure:
* do_patch_rkimg() can generate a corrent new image even if
* machine could lost-power/crash during upgrading
* By doing this, do_patch_rkimg() will checking:
* - if dst_file already exist
* - md5sum is correct, then we may reboot during writing-to-flash
* >> do not do patch again.
* - md5sum is wrong, then we may reboot during do_patch_rkimg()
* >> the old image (in flash) good enough as is, do patch again.
* - if dst_file is not exist
* - this is normal case, we're safe to continue
*/
if (stat(dst_file, &dst_stat) == 0) // 获取文件状态信息
LOGI("file %s exist for <%s>, unexpected detected, trying to continue\n",
dst_file, pcmd->name);
ret = do_patch_rkimg(src_path, pcmd->offset, pcmd->size,
pcmd->dest_path, dst_file);
if (ret == 0) {
// 从src_path(比如:/userdata/update.img)中读取指定范围内的数据,并将其写入到目标文件dest_path(比如/dev/block/by-name/recovery)的指定位置
ret = block_write(src_path, pcmd->offset, pcmd->size,
pcmd->flash_offset, pcmd->dest_path);
} else if (ret > 0) {
// 从dst_file(比如:/userdata/update.img.recovery)中读取指定范围内的数据,并将其写入到目标文件dest_path(比如/dev/block/by-name/recovery)的指定位置
ret = block_write(dst_file, 0, ret,
pcmd->flash_offset, pcmd->dest_path);
//TODO: do not skip diff image verify
pcmd->skip_verify = true;
} else {
return ret;
}
lseek(fd, 0, SEEK_END);
// 记录固件升级状态,向/userdata/update.img.done文件写入>recovery<
if (write(fd, pattern, strlen(pattern)) != strlen(pattern)) {
LOGW("write len error");
}
close(fd);
// 执行同步操作,确保文件写入完成后再删除文件。
sync();
/* Make sure that dst file written to flash before unlink it,删除/userdata/update.img.recovery文件 */
unlink(dst_file);
sync();
} else {
//mtd
LOGI("pcmd->flash_offset = %lld.\n", pcmd->flash_offset);
ret = mtd_write(src_path, pcmd->offset, pcmd->size, pcmd->flash_offset, pcmd->dest_path);
}
return ret;
}
4.2.2 do_patch_rkimg
do_patch_rkimg
函数位于do_patch.c
文件;
点击查看代码
/* Return:
* -1 patching error,
* 0 not a diff img,
* >0 the new image size
* dst_file size if patch successfully
*/
int do_patch_rkimg(const char *img, ssize_t offset, ssize_t size,
const char *blk_dev, const char *dst_file)
{
#define TAIL_SIZE 80
#define TID_HEAD 0
#define TID_NAME 1
#define TID_OLD_SIZE 2
#define TID_NEW_SIZE 3
#define TID_MD5SUM 4
#define TOKENS 5
#define MAGIC_TAIL "DIFF"
#define MD5SUM_LEN 32
// For tail parsing.
// Tail size is 80 bytes as like,
// "DIFF:%-15s:%-12s:%-12s:%-32s:"
// $name $old_size $new_size $md5sum
int fd_img;
const char *split = ": "; //space is a split char too
char tail[TAIL_SIZE];
ssize_t len, ret;
ssize_t oldsize, newsize;
char *saveptr, *str, *name, *md5sum, *token[TOKENS];
int j;
// For patching
FILE * f = NULL, * cpf = NULL, * dpf = NULL, * epf = NULL;
BZFILE * cpfbz2, * dpfbz2, * epfbz2;
int cbz2err, dbz2err, ebz2err;
int fd;
ssize_t bzctrllen, bzdatalen;
unsigned char header[32], buf[8];
unsigned char *old = NULL, *new_ptr = NULL;
off_t oldpos, newpos;
off_t ctrl[3];
off_t lenread;
off_t i;
struct stat old_stat, dst_stat;
if ((fd_img = open(img, O_RDONLY, 0)) < 0) {
LOGE("open %s failed\n", img);
return -1;
}
if (lseek(fd_img, offset + size - TAIL_SIZE, SEEK_SET) < 0) {
LOGE("%s: lseek to: %ld failed: %s\n", img,
offset + size - TAIL_SIZE, strerror(errno));
close(fd_img);
return -1;
}
len = 0;
while (len != TAIL_SIZE) {
ret = read(fd_img, tail + len, TAIL_SIZE - len);
if (ret < 0) {
LOGE("read %s tail err\n", img);
close(fd_img);
return -1;
}
len += ret;
}
close(fd_img);
tail[TAIL_SIZE - 1] = '\0';
for (j = 0, str = tail; j < TOKENS; j++, str = NULL) {
token[j] = strtok_r(str, split, &saveptr);
if (token[j] == NULL)
break;
}
name = token[TID_NAME];
md5sum = token[TID_MD5SUM];
/* When unexpected reboot during patching/writing happened,
* if dst_file is in correct state, then old image may already broken
*/
if (stat(dst_file, &dst_stat) == 0 &&
compareMd5sum(dst_file, (unsigned char *)md5sum, 0, dst_stat.st_size)) {
LOGI("Recovery from unecptected reboot successfully.");
return dst_stat.st_size;
}
/* If dst_file exist but md5sum is wrong, old image file is clean, hopefully */
//check tail magic, return 0 if not exist
if (j == 0 || strncmp(MAGIC_TAIL, token[TID_HEAD], strlen(MAGIC_TAIL)) != 0) {
LOGW("Not a diff image, ret = %ld\n", ret);
return 0;
}
LOGI("This is a diff image, patching...\n");
if (j != TOKENS ||
(oldsize = strtol(token[TID_OLD_SIZE], &saveptr, 10)) == 0 ||
(errno == ERANGE && (oldsize == LONG_MAX || oldsize == LONG_MIN)) ||
saveptr == token[TID_OLD_SIZE] ||
(newsize = strtol(token[TID_NEW_SIZE], &saveptr, 10)) == 0 ||
(errno == ERANGE && (newsize == LONG_MAX || newsize == LONG_MIN)) ||
saveptr == token[TID_NEW_SIZE] ||
strlen(token[TID_MD5SUM]) != MD5SUM_LEN) {
LOGE("Bad Tail header of bsdiff patch\n");
return -1;
}
//TODO: check dst_file dir size, return -1 if space too small.
/* Open patch file */
if ((f = fopen(img, "r")) == NULL) {
LOGE("fopen %s err\n", img);
return -1;
}
if (fseeko(f, offset, SEEK_SET)) {
LOGE("fseeko %s err\n", img);
fclose(f);
return -1;
}
/*
File format:
0 8 "BSDIFF40"
8 8 X
16 8 Y
24 8 sizeof(newfile)
32 X bzip2(control block)
32+X Y bzip2(diff block)
32+X+Y ??? bzip2(extra block)
with control block a set of triples (x,y,z) meaning "add x bytes
from oldfile to x bytes from the diff block; copy y bytes from the
extra block; seek forwards in oldfile by z bytes".
*/
/* Read header */
if (fread(header, 1, 32, f) < 32) {
LOGE("Read header err\n");
fclose(f);
return -1;
}
fclose(f);
f = NULL;
/* Check for appropriate magic */
if (memcmp(header, "BSDIFF40", 8) != 0) {
LOGE("Bad header, Corrupt patch\n");
return -1;
}
/* Read lengths from header */
bzctrllen = offtin(header + 8);
bzdatalen = offtin(header + 16);
newsize = offtin(header + 24);
if ((bzctrllen < 0) || (bzdatalen < 0) || (newsize <= 0)) {
LOGE("Bad header len, Corrupt patch\n");
return -1;
}
/* re-open patch file via libbzip2 at the right places */
if ((cpf = fopen(img, "r")) == NULL)
return -1;
if (fseeko(cpf, offset + 32, SEEK_SET)) {
LOGE("fseeko(%s, %lld) err\n", img, (long long)(32 + offset));
goto cleanup;
}
if ((cpfbz2 = BZ2_bzReadOpen(&cbz2err, cpf, 0, 0, NULL, 0)) == NULL) {
LOGE("BZ2_bzReadOpen, bz2err = %d, err\n", cbz2err);
goto cleanup;
}
if ((dpf = fopen(img, "r")) == NULL)
goto cleanup;
if (fseeko(dpf, offset + 32 + bzctrllen, SEEK_SET)) {
LOGE("fseeko(%s, %lld) err\n", img,
(long long)(offset + 32 + bzctrllen));
goto cleanup;
}
if ((dpfbz2 = BZ2_bzReadOpen(&dbz2err, dpf, 0, 0, NULL, 0)) == NULL) {
LOGE("BZ2_bzReadOpen, bz2err = %d, err\n", dbz2err);
goto cleanup;
}
if ((epf = fopen(img, "r")) == NULL) {
LOGE("fopen(%s) err\n", img);
goto cleanup;
}
if (fseeko(epf, offset + 32 + bzctrllen + bzdatalen, SEEK_SET)) {
LOGE("fseeko(%s, %lld) err\n", img,
(long long)(offset + 32 + bzctrllen + bzdatalen));
goto cleanup;
}
if ((epfbz2 = BZ2_bzReadOpen(&ebz2err, epf, 0, 0, NULL, 0)) == NULL) {
LOGE("BZ2_bzReadOpen, bz2err = %d\n", ebz2err);
goto cleanup;
}
if (((fd = open(blk_dev, O_RDONLY, 0)) < 0) ||
((old = (unsigned char *)mmap(NULL, oldsize, PROT_READ,
MAP_SHARED | MAP_POPULATE, fd, 0)) == MAP_FAILED)) {
LOGE("open %s err\n", blk_dev);
goto cleanup;
}
close(fd);
fd = -1;
/* mmap the new file */
if (((fd = open(dst_file, O_CREAT | O_TRUNC | O_RDWR, 0666)) < 0) ||
(lseek(fd, newsize - 1, SEEK_SET) != (newsize - 1)) ||
(write(fd, "E", 1) != 1) ||
(lseek(fd, 0, SEEK_SET) != 0) ||
((new_ptr = (unsigned char *)mmap(NULL, newsize, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0)) == MAP_FAILED)) {
LOGE("mmap %s err\n", dst_file);
goto cleanup;
}
close(fd);
fd = -1;
oldpos = 0; newpos = 0;
while (newpos < newsize) {
/* Read control data */
for (i = 0; i <= 2; i++) {
lenread = BZ2_bzRead(&cbz2err, cpfbz2, buf, 8);
if ((lenread < 8) || ((cbz2err != BZ_OK) &&
(cbz2err != BZ_STREAM_END))) {
LOGE("Read control data: Corrupt patch\n");
goto cleanup;
}
ctrl[i] = offtin(buf);
};
/* Sanity-check */
if (newpos + ctrl[0] > newsize) {
LOGE("Sanity-check: Corrupt patch\n");
goto cleanup;
}
/* Read diff string */
lenread = BZ2_bzRead(&dbz2err, dpfbz2, new_ptr + newpos, ctrl[0]);
if ((lenread < ctrl[0]) ||
((dbz2err != BZ_OK) && (dbz2err != BZ_STREAM_END))) {
LOGE("Read diff string: Corrupt patch\n");
goto cleanup;
}
/* Add old data to diff string */
for (i = 0; i < ctrl[0]; i++)
if ((oldpos + i >= 0) && (oldpos + i < oldsize))
new_ptr[newpos + i] += old[oldpos + i];
/* Adjust pointers */
newpos += ctrl[0];
oldpos += ctrl[0];
/* Sanity-check */
if (newpos + ctrl[1] > newsize) {
LOGE("Sanity-check: Corrupt patch\n");
goto cleanup;
}
/* Read extra string */
lenread = BZ2_bzRead(&ebz2err, epfbz2, new_ptr + newpos, ctrl[1]);
if ((lenread < ctrl[1]) ||
((ebz2err != BZ_OK) && (ebz2err != BZ_STREAM_END))) {
LOGE("Read extra string: Corrupt patch\n");
goto cleanup;
}
/* Adjust pointers */
newpos += ctrl[1];
oldpos += ctrl[2];
};
/* Clean up the bzip2 reads */
BZ2_bzReadClose(&cbz2err, cpfbz2);
BZ2_bzReadClose(&dbz2err, dpfbz2);
BZ2_bzReadClose(&ebz2err, epfbz2);
fclose(cpf);
fclose(dpf);
fclose(epf);
munmap(new_ptr, newsize);
munmap(old, oldsize);
sync();
//check md5sum
if (!compareMd5sum(dst_file, (unsigned char *)md5sum, 0, newsize))
return -1;
LOGI("Diff patch apply successfully for %s, size: %ld\n",
name, newsize);
return newsize;
cleanup:
if (new_ptr != NULL)
munmap(new_ptr, newsize);
if (old != NULL)
munmap(old, oldsize);
if (fd >= 0)
close(fd);
if (cpf != NULL)
fclose(cpf);
if (dpf != NULL)
fclose(dpf);
if (epf != NULL)
fclose(epf);
return -1;
}
4.2.3 block_write
block_write
函数用于从一个源文件 (src_path
) 中读取指定范围内的数据,并将其写入到目标文件 (dest_path
) 的指定位置,函数接收5个参数:
-
src_path
:源文件的路径名,update.img
镜像在系统的存储路径; -
offset
:从源文件中读取数据的起始偏移量;这里会传入需要升级的分区镜像文件在update.img
中的偏移; -
size
:从源文件中读取的数据大小,这里会传入需要升级的分区镜像文件的大小; -
flash_offset
:目标文件起始偏移; -
dest_path
:目标文件;这里会传入升级的分区在linux
系统中对应的设备节点名称,比如/dev/block/by-name/recovery
;
点击查看代码
static int block_write(char *src_path, long long offset, long long size, long long flash_offset, char *dest_path)
{
LOGI("block_write src %s dest %s.\n", src_path, dest_path);
int fd_dest = 0, fd_src = 0;
long long src_offset = 0, dest_offset = 0;
long long src_remain, dest_remain;
int src_step, dest_step;
long long src_file_offset = 0;
long long read_count, write_count;
char data_buf[BLOCK_WRITE_LEN] = {0};
// 1. 以只读方式打开源文件 (src_path)
fd_src = open(src_path, O_RDONLY);
if (fd_src < 0) {
LOGE("Can't open %s\n", src_path);
return -2;
}
src_offset = offset;
dest_remain = src_remain = size;
dest_step = src_step = BLOCK_WRITE_LEN;
// 2. 设置源文件的读取起始位置为offset
if (lseek64(fd_src, src_offset, SEEK_SET) == -1) {
close(fd_src);
LOGE("lseek64 failed (%s:%d).\n", __func__, __LINE__);
return -2;
}
src_file_offset = src_offset;
// dest_offset = flash_offset;
// This step is going to write (src_path: sdupdate.img) to the file which is partition data (e.g. uboot)
// So dest_offset is 0.
dest_offset = 0;
// 3. 以读写方式打开目标文件 (dest_path)
fd_dest = open(dest_path, O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd_dest < 0) {
close(fd_src);
LOGE("Can't open %s\n", dest_path);
return -2;
}
// 4. 设置目标文件的写入起始位置为0
if ( lseek64(fd_dest, dest_offset, SEEK_SET) == -1 ) {
LOGE("(%s:%d) lseek64 failed(%s).\n", __func__, __LINE__, strerror(errno));
close(fd_src);
close(fd_dest);
return -2;
}
// 5. 每次从源文件读取数据并写入到目标文件,直到完成指定的读取大小 (size)
while (src_remain > 0 && dest_remain > 0) {
memset(data_buf, 0, BLOCK_WRITE_LEN);
read_count = src_remain > src_step ? src_step : src_remain;
if (read(fd_src, data_buf, read_count) != read_count) {
close(fd_dest);
close(fd_src);
LOGE("Read failed(%s):(%s:%d)\n", strerror(errno), __func__, __LINE__);
return -2;
}
src_remain -= read_count;
src_file_offset += read_count;
write_count = dest_remain > dest_step ? dest_step : dest_remain;
if (write(fd_dest, data_buf, write_count) != write_count) {
close(fd_dest);
close(fd_src);
LOGE("(%s:%d) write failed(%s).\n", __func__, __LINE__, strerror(errno));
return -2;
}
dest_remain -= write_count;
}
// 6. 强制将目标文件的数据写入磁盘
fsync(fd_dest);
close(fd_dest);
close(fd_src);
return 0;
}
4.3 md5sum.c
4.3.1 checkdata
checkdata
用于从指定文件中读取指定范围的数据,计算其MD5
值,并将结果存储在 out_md5sum
中,函数接收4个参数:
dest_path
:目标文件的路径;out_md5sum
:用于存储计算出的MD5
值的缓冲区;offset
:从文件的起始位置开始的偏移量,指定从文件的哪个位置开始读取数据;checkSize
:要读取并计算MD5
的数据大小;
函数原型如下:
#include <openssl/md5.h>
......
bool checkdata(const char *dest_path, unsigned char *out_md5sum, long long offset, long long checkSize)
{
MD5_CTX ctx;
unsigned char md5sum[16];
char buffer[512];
int len = 0;
int ret;
// 1. 以二进制方式打开目标分区设备节点
FILE *fp = fopen(dest_path, "rb");
if (fp == NULL) {
LOGE("open file failed %s", dest_path);
return -1;
}
// 定位到偏移量offset
ret = fseeko64(fp, offset, SEEK_SET);
if (ret < 0) {
LOGE("%s:%d fseeko64 fail", __func__, __LINE__);
return false;
}
// 始化MD5上下文
MD5_Init(&ctx);
long long readSize = 0;
int step = 512;
// 循环读取文件内容:通过循环,每次最多读取step大小的数据块(这里是512字节),直到checkSize为 0
while (checkSize > 0) {
readSize = checkSize > step ? step : checkSize;
// 每次读取readSize长度到buffer
if (fread(buffer, 1, readSize, fp) != readSize) {
LOGE("fread error.\n");
return false;
}
checkSize = checkSize - readSize;
// 每次读取数据后,使用MD5_Update(&ctx, buffer, readSize) 更新MD5上下文
MD5_Update(&ctx, buffer, readSize);
// 将buffer清零以确保数据的正确性和安全性
memset(buffer, 0, sizeof(buffer));
}
// 完成MD5计算,并将结果存储在md5sum数组中
MD5_Final(md5sum, &ctx);
fclose(fp);
LOGI("new md5:");
for (int i = 0; i < 16; i++) {
printf("%02x", md5sum[i]);
}
printf("\n");
//change
if (out_md5sum != NULL) {
memset(out_md5sum, 0, 16);
memcpy(out_md5sum, md5sum, 16);
}
LOGI("MD5Check is ok of %s\n", dest_path);
return 0;
}
4.3.2 comparefile
comparefile
用于比较两个文件在指定范围内数据是否一样。其分别计算目标文件和源文件在指定范围内数据的MD5
值,并进行比较。
函数接收5个参数:
dest_path
:目标文件;这里会传入升级的分区在linux
系统中对应的设备节点名称,比如/dev/block/by-name/recovery
;source_path
:源文件,update.img
镜像在系统的存储路径;dest_offset
:目标文件起始偏移;source_offset
:源文件数据起始偏移;这里会传入需要升级的分区镜像文件在update.img
中的偏移;checkSize
:需要计算MD5
值的数据长度;这里会传入需要升级的分区镜像文件的大小;
函数原型如下:
bool comparefile(const char *dest_path, const char *source_path, long long dest_offset, long long source_offset, long long checkSize)
{
unsigned char md5sum_source[16];
unsigned char md5sum_dest[16];
// MTD设备进入
if (isMtdDevice()) {
checkdata_mtd(dest_path, md5sum_dest, dest_offset, checkSize);
} else { // 走这里
// 计算目标文件在指定范围内数据的MD5值
checkdata(dest_path, md5sum_dest, dest_offset, checkSize);
}
// 计算源文件在指定范围内数据的MD5值
checkdata(source_path, md5sum_source, source_offset, checkSize);
// 比较这16个字节是否一致,不相等返回false
for (int i = 0; i < 16; i++) {
if (md5sum_dest[i] != md5sum_source[i]) {
LOGE("MD5Check is error of %s\n", dest_path);
return false;
}
}
return true;
}
4.4 rkbootloader.c
rkbootloader
文件用于对misc
分区进行读写。
4.4.1 misc
分区介绍
misc
分区是一个没有文件系统的分区,用于存放一些引导配置参数,现有结构如下;
偏移地 | 作用 |
---|---|
2k | Linux A/B 分区引导信息 |
4k | 格式化命令 |
16k | Recovery系统与Normal系统通信,存放bootloader_message结构体的内容 |
misc
分区的概念来源于Android
系统,Linux
系统中常用来作为系统升级时或者恢复出厂设置时使用。
misc
分区的读写:misc
分区在以下情况下会被读写。
(1) uboot
:设备加电启动时,首先启动uboot
,在uboot
中会读取misc
分区的内容。根据misc
分区中command
命令内容决定是进入normal
系统还是recovery
系统;
command
为boot-recovery
,则进入recovery
系统;command
为空,则进入normal
系统;
(2) recovery
:在设备进入recovery
系统中,可以读取misc
分区中recovery
部分的内容,从而执行不同的动作或升级分区固件,或擦除用户分区数据,或其他操作等等;misc
分区的结构组成详见下图;
下面以rk3588
平台使用的misc
分区为例,以二进制形式打开misc.img
文件,在距文件开始位置偏移16K
(16384 Byte
)字节位置处开始,存放bootloader_message
结构体的内容;
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp$ xxd -b -e -s 16384 -l 1088 output/firmware/misc.img
4.4.2 struct bootloader_message
struct bootloader_message
定义在rkbootloader.h
文件,定义了用于在Flash
中进行通信的数据块结构,主要用于引导加载程序(bootloader
)和Linux
系统之间的通信,特别是用于恢复和固件更新的目的;
/* Bootloader Message
*
* This structure describes the content of a block in flash
* that is used for recovery and the bootloader to talk to
* each other.
*
* The command field is updated by linux when it wants to
* reboot into recovery or to update radio or bootloader firmware.
* It is also updated by the bootloader when firmware update
* is complete (to boot into recovery for any final cleanup)
*
* The status field is written by the bootloader after the
* completion of an "update-radio" or "update-hboot" command.
*
* The recovery field is only written by linux and used
* for the system to send a message to recovery or the
* other way around.
*
* The systemFlag field is used for the system to send a message to recovery.
*/
struct bootloader_message {
char command[32];
char status[32];
char recovery[768];
char needupdate[4];
char systemFlag[252];
};
其中:
command
:如果期望linux
系统重启进入recovery
系统,需要将该命令设置为boot-recovery
;status
:在执行某些命令(如update-radio
或update-hboot
)后,bootloader
将执行结果写入此处;recovery
:此字段用于normal
与recovery
系统之间的通信,比如如果期望linux
系统重启进入recovery
系统,需要将该参数设置为recovery\n--update_package=${savepath}
;needupdate
:recovery
系统下需要升级的分区,即partition
参数;systemFlag
:系统标志位;
4.4.3 get_bootloader_message
/**
* 往misc 偏移16k位置处读取recovery信息
*/
int get_bootloader_message(struct bootloader_message *out)
{
return readMisc((char *)out, BOOTLOADER_MESSAGE_OFFSET_IN_MISC, sizeof(struct bootloader_message));
}
4.4.4 set_bootloader_message
set_bootloader_message
用于往misc
偏移16k
位置写入recovery
信息;
static int writeMisc_block(char *buf, int offset, int size)
{
// 写文件/dev/block/by-name/misc
FILE* f = fopen(MISC_PARTITION_NAME_BLOCK, "wb");
if (f == NULL) {
LOGE("Can't open %s\n(%s)\n", MISC_PARTITION_NAME_BLOCK, strerror(errno));
return -1;
}
// 定位到offset
fseek(f, offset, SEEK_SET);
// 写操作
int count = fwrite(buf, sizeof(char), size, f);
if (count != size) {
LOGE("Failed writing %s\n(%s)\n", MISC_PARTITION_NAME_BLOCK, strerror(errno));
return -1;
}
if (fclose(f) != 0) {
LOGE("Failed closing %s\n(%s)\n", MISC_PARTITION_NAME_BLOCK, strerror(errno));
return -1;
}
return 0;
}
static int writeMisc(char *buf, int offset, int size)
{
if (isMtdDevice()) {
return writeMisc_mtd(buf, offset, size);
} else { // 走这里
return writeMisc_block(buf, offset, size);
}
}
/**
* 往misc 偏移16k位置写入recovery信息
*/
int set_bootloader_message(const struct bootloader_message *in)
{
// 文件偏移16*1024
return writeMisc((char*)in, BOOTLOADER_MESSAGE_OFFSET_IN_MISC, sizeof(struct bootloader_message));
}
五、辅助工具相关
5.1 rktools.c
rktools.c
和rktools.h
文件是一些常用工具方法的实现。
5.1.1 getCurrentSlot
getCurrentSlot
函数用于判断Linux
的启动模式,Recovery
模式或者Linux A/B
模式;
/**
* 从cmdline 获取从哪里引导
* 返回值:
* 0: a分区
* 1: b分区
* -1: recovery 模式
*/
int getCurrentSlot()
{
char cmdline[CMDLINE_LENGTH];
int fd = open("/proc/cmdline", O_RDONLY);
if (read(fd, (char*)cmdline, CMDLINE_LENGTH) < 1) {
close(fd);
return -1;
}
close(fd);
char *slot = strstr(cmdline, "android_slotsufix");
if (slot == NULL) slot = strstr(cmdline, "androidboot.slot_suffix");
if (slot != NULL) {
slot = strstr(slot, "=");
if (slot != NULL && *(++slot) == '_') {
slot += 1;
LOGI("Current Mode is '%c' system.\n", (*slot == 'a') ? 'A' : 'B');
if ((*slot) == 'a') {
return 0;
} else if ((*slot) == 'b') {
return 1;
}
}
}
LOGI("Current Mode is recovery.\n");
return -1;
}
通过读取命令行参数是否拥有android_slotsufix
或者slot_suffix
参数,以及参数内容来区分哪种启动模式。
此外,该函数会输出当前的启动模式信息,比如:
LOG_INFO: Current Mode is recovery.
5.1.2 isMtdDevice
通过读取命令行参数storagemedia
,判断启动设备是不是MTD
设备;
- 如果未指定命令行参数
storagemedia
,默认不是MTD
设备; - 如果命令行指定了
storagemedia
参数;- 判定参数是否包含
mtd
,如果是则认为是MTD
设备; - 判断参数是否包含
sd
,并且存在/proc/mtd
文件,如果文件中写有mtd
分区信息,则认为是MTD
设备;
- 判定参数是否包含
具体源码如下:
#define MTD_PATH "/proc/mtd"
//判断是MTD还是block设备
bool isMtdDevice()
{
char param[2048];
int fd, ret;
char *s = NULL;
fd = open("/proc/cmdline", O_RDONLY);
ret = read(fd, (char*)param, 2048);
close(fd);
s = strstr(param, "storagemedia");
if (s == NULL) {
LOGI("no found storagemedia in cmdline, default is not MTD.\n");
return false;
} else {
// 在字符串s中查找第一个出现的 = 符号,并返回该符号及其后面的字符串的指针
s = strstr(s, "=");
if (s == NULL) {
LOGI("no found storagemedia in cmdline, default is not MTD.\n");
return false;
}
// 跳过=符号本身
s++;
// 跳过可能存在的空格,直到遇到非空格字符为止
while (*s == ' ') {
s++;
}
// 如果字符串s开头的三个字符是mtd
if (strncmp(s, "mtd", 3) == 0 ) {
LOGI("Now is MTD.\n");
return true;
} else if (strncmp(s, "sd", 2) == 0) { // 如果字符串s开头的两个字符是sd
LOGI("Now is SD.\n");
// 判断是否可以访问/proc/mtd,如果path存在且mode设置为F_OK,则返回0
if ( !access(MTD_PATH, F_OK) ) {
fd = open(MTD_PATH, O_RDONLY);
ret = read(fd, (char*)param, 2048);
close(fd);
// 判断是否存在mtd分区 比如:mtd0: 00040000 00020000 "u-boot"
s = strstr(param, "mtd");
if (s == NULL) {
LOGI("no found mtd.\n");
return false;
}
LOGI("Now is MTD.\n");
return true;
}
}
}
LOGI("Current device is not MTD\n");
return false;
}
对于ArmSoM-Sige7
开发板而言,最常用的就是SD
卡/eMMC
启动,并没有使用过任何Flash
启动,因此函数这里使用返回false
。
由于MTD
设备更多内容可以参考《linux
驱动移植-Nand Flash ONFI
标准和MTD
子系统》。
5.2 日志实现
log.c
和log.h
文件用于实现日志记录的功能。
5.2.1 log.h
该头文件建立了一个具有多个日志级别的日志框架,并提供了宏,使得在C
程序中可以方便地记录不同级别的日志消息。
// Copyright 2013 Google Inc. All Rights Reserved.
//
// Log - Platform independent interface for a Logging class
//
#ifndef update_engine_CORE_LOG_H_
#define update_engine_CORE_LOG_H_
// Simple logging class. The implementation is platform dependent.
typedef enum {
LOG_ERROR,
LOG_WARN,
LOG_INFO,
LOG_DEBUG,
LOG_VERBOSE,
LOG_MAX
} LogPriority;
// Enable/disable verbose logging (LOGV).
// This function is supplied for cases where the system layer does not
// initialize logging. This is also needed to initialize logging in
// unit tests.
void InitLogging(LogPriority level);
void Log(const char* file, int line, LogPriority level, const char* fmt, ...);
// Log APIs
#define LOGE(...) Log(__FILE__, __LINE__, LOG_ERROR, ##__VA_ARGS__)
#define LOGW(...) Log(__FILE__, __LINE__, LOG_WARN, ##__VA_ARGS__)
#define LOGI(...) Log(__FILE__, __LINE__, LOG_INFO, ##__VA_ARGS__)
#define LOGD(...) Log(__FILE__, __LINE__, LOG_DEBUG, ##__VA_ARGS__)
#define LOGV(...) Log(__FILE__, __LINE__, LOG_VERBOSE, ##__VA_ARGS__)
#endif // update_engine_CORE_LOG_H_
5.2.2 log.c
log.c
文件提供了两个方法:
InitLogging
:日志初始化函数,用于设置日志级别;Log
:数用于实际记录日志;它接收日志的文件名file
、行号line
、日志级别level
、以及格式化字符串fmt
和可变参数列表;
log.c
代码如下:
// Copyright 2013 Google Inc. All Rights Reserved.
//
// Log - implemented using the standard Android logging mechanism
/*
* Qutoing from system/core/include/log/log.h:
* Normally we strip ALOGV (VERBOSE messages) from release builds.
* You can modify this (for example with "#define LOG_NDEBUG 0"
* at the top of your source file) to change that behavior.
*/
#include "log.h"
#include <stdio.h>
#include <stdarg.h>
#define LOG_BUF_SIZE 1024
static int LOG_LEVEL = LOG_DEBUG;
void InitLogging(LogPriority level)
{
if (level > 0)
LOG_LEVEL = level;
}
void Log(const char* file, int line, LogPriority level, const char* fmt, ...)
{
if (level > LOG_LEVEL) {
return;
}
va_list ap;
char buf[LOG_BUF_SIZE];
va_start(ap, fmt);
vsnprintf(buf, LOG_BUF_SIZE, fmt, ap);
va_end(ap);
switch (level) {
case LOG_ERROR:
printf("LOG_ERROR: %s", buf);
break;
case LOG_WARN:
printf("LOG_WARN: %s", buf);
break;
case LOG_INFO:
printf("LOG_INFO: %s", buf);
break;
case LOG_DEBUG:
printf("LOG_DEBUG: %s", buf);
break;
default :
break;
}
}
日志输出的默认级别为LOG_LEVEL
,这里将高于以及等于该级别的日志会被输出,日志级别从低到高依次为:LOG_MAX
、LOG_VERBOSE
、LOG_DEBUG
、LOG_INFO
、LOG_WARN
、LOG_ERROR
。
5.3 download.c
download.c
文件从文件名字就可以看出来,这个文件主要实现的就是文件下载功能。
5.3.1 download_file
download_file
函数用于从url
下载文件,函数接收两个参数:
url
:需要下载的文件地址,可以是远端url
路径,也可以是本地路径;output_filename
:下载的文件保存路径;
函数主要流程如下:
-
如果
url
以http
开头,则从远端使用wget
下载文件,并保存到output_filename
指定的路径; -
否则则认为文件位于本地,函数直接返回;
download_file
函数原型如下:
#include <stdio.h>
#include <curl/curl.h>
#include <string.h>
#include <stdbool.h>
#include "log.h"
.....
int download_file(char *url, char const *output_filename)
{
// 判断url是否是以http开头,即从远端采用http协议下载文件
if (strncmp(url, "http", 4) != 0) { // 非http开头,则认为文件位于本地
LOGI("where the file is local.\n");
return -2;
}
CURL *curl;
CURLcode res;
FILE *outfile;
char const *progress_data = "* ";
down_processvalue = -1;
// 初始化CURL对象
curl = curl_easy_init();
if (curl) {
// 打开指定的output_filename文件以写入二进制数据
outfile = fopen(output_filename, "wb");
// 设置CURL选项
curl_easy_setopt(curl, CURLOPT_URL, url); // 设置下载的URL地址
curl_easy_setopt(curl, CURLOPT_WRITEDATA, outfile); // 设置写入数据的文件指针为 outfile
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, my_write_func); // 设置自定义的写入回调函数my_write_func,用于处理接收到的数据。
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, false); // 设置为false,允许显示下载进度
curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, my_progress_func); // 设置进度回调函数my_progress_func,用于更新下载进度
curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, progress_data); // 设置进度数据为progress_data,这里可能用于自定义进度显示的标志
// 执行下载操作
res = curl_easy_perform(curl);
if (res != CURLE_OK)
LOGE("curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
// 关闭文件outfile和清理CURL对象
fclose(outfile);
/* always cleanup */
curl_easy_cleanup(curl);
}
if (res != CURLE_OK) {
LOGE("download Error.\n");
return -1;
}
return 0;
}
在设置CURL
选项的过程中,有两个回调函数:
- 写入回调函数
my_write_func
,用于处理接收到的数据; - 进度回调函数
my_progress_func
,用于更新下载进度。
5.3.2 my_write_func
my_progress_func
函数用于处理接收到的数据,函数接收4个参数:
ptr
:指向要写入数据的内存块的指针,通常是一个void*
类型的指针;size
:要写入的每个数据项的字节数(大小),通常使用sizeof()
运算符来获取数据项的大小,例如sizeof(int)
或sizeof(struct YourStruct)
;nmemb
:要写入的数据项的数量,即数据块中包含多少个size
大小的数据项。stream
:指向FILE
对象的指针,该FILE
对象标识了要写入数据的文件或者一个内存流。
函数返回值size_t
表示实际写入的数据项数量,如果写入成功,返回值通常等于nmemb
,如果失败,则返回一个小于nmemb
的值。
函数原型如下:
size_t my_write_func(void *ptr, size_t size, size_t nmemb, FILE *stream)
{
return fwrite(ptr, size, nmemb, stream);
}
5.3.3 my_progress_func
my_progress_func
函数用于输出下载进度,函数原型如下;
int my_progress_func(char *progress_data,
double t, /* dltotal */
double d, /* dlnow */
double ultotal,
double ulnow)
{
processvalue = ulnow / ultotal * 100 / 110;
//LOGI("ultotal is %f, ulnow is %f, t is %f, d is %f\n", ultotal, ulnow, t, d);
if (down_processvalue != (int)(d / t * 100)) {
down_processvalue = (int)(d / t * 100);
LOGI("down_processvalue is %d%\n", down_processvalue);
}
return 0;
}
参考文章
[1] Rockchip Linux updateEngine
升级方案介绍
[2] Rockchip Linux Recovery
升级开发指南