程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)

Rockchip RK3588 - Rockchip Linux Recovery updateEngine源码分析

----------------------------------------------------------------------------------------------------------------------------

开发板 :ArmSoM-Sige7开发板
eMMC64GB
LPDDR48GB
显示屏 :15.6英寸HDMI接口显示屏
u-boot2017.09
linux5.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_RECOVERYBR2_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框架有深入了解的话,你就很容易明白软件包recoveryRockchipBuildroot扩展的软件包,用来解决系统升级的问题。

有关如何在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告诉编译器在当前目录中查找头文件;
  • -fPICGCC和其他兼容编译器的选项,表示生成位置无关的代码,用于动态链接库。这是因为动态链接库在编译时无法确定加载地址,因此需要生成位置无关的代码;
  • -lpthread:表示链接pthread库,即POSIX线程库,用于多线程编程;
  • -lcurl:表示链接libcurl库,这是一个支持多协议的URL传输库,用于实现网络操作;
  • -lssl:表示链接OpenSSLSSL库,提供了安全套接层 (SSL) 和传输层安全性 (TLS) 的实现;
  • -lcrypto:表示链接OpenSSL的加密库,提供了各种加密算法的实现,例如哈希函数、对称加密和非对称加密等;
  • -lbz2:表示链接libbz2库,这是用于处理Bzip2压缩格式的库,提供了文件压缩和解压缩的功能。

这些选项通常用于构建需要多线程、网络操作、安全套接层以及数据压缩支持的应用程序或库。在编译时,它们告诉编译器在链接阶段需要使用这些特定的外部库。

(2) RECOVERY_MAKE_ENV:这里设置为TARGET_MAKE_ENVTARGET_MAKE_ENV是在Buildroot中用来定义传递给 make 命令的环境变量;

TARGET_MAKE_ENVpackage/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图像处理、DRMzlib支持;
  • 由于我们开启了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_CMDSmake <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操作,同时并传递了CCCFLAGS变量。

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_CMDSmake <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等)、openrcsystemd单元安装初始脚本的操作;

$(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),会覆盖MakefileCFLAGS相关配置:

其中:

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,其依赖recoveryupdateEngine

all: $(PROM) $(UPDATE_ENGINE)
.PHONY : all

在执行make命令时,如果未指定目标,默认执行第一个目标,即all

接下来我们需要寻找all依赖recoveryupdateEngine的生成规则。

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_partitionRK_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有该分区镜像信息(通过升级名称名称查找匹配的分区镜像),更新升级命令中的offsetsize,即设置成分区镜像在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有该分区镜像信息,更新升级命令中的offsetsize,即设置成分区镜像在update.img文件中的偏移和大小;
  • 遍历每一个升级命令,对于打了标记的升级命令,依次执行如下操作;
    • 执行升级操作,即执行升级命令的cmd函数,比如recovery升级命令调用flash_normal(_url,update_cmd[5])进行分区镜像文件的烧录;
    • 除了parameterbootloader升级命令外,执行分区校验操作,即校验烧录到系统的分区镜像(/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.imgbuildroot系统);

#  -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~0x073171bootloader分区镜像移除最后180个字节之后的数据,一共471308 个字节;
  • 第三部分0x073172~0x073225bootloader分区镜像最后180个字节,一共180个字节;
  • 第四部分0x073226~0x33e08229:存放update.raw.img,即原始固件,一共869879812个字节;
  • 第五部分0x33e0822A~0x33e08249:其中最后32个字节,为MD5校验数据;

实际上我们的update.img文件大小为870351434102+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.imgbuildroot系统);

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.imgupdate.img的偏移。

此外该函数,还将bootloader分区镜像的偏移和大小直接设置为rkimage_head.dwBootOffsetrkimage_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.imgpcmd->offsetpcmd->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系统;

  • commandboot-recovery,则进入recovery系统;
  • command为空,则进入normal系统;

(2) recovery:在设备进入recovery系统中,可以读取misc分区中recovery部分的内容,从而执行不同的动作或升级分区固件,或擦除用户分区数据,或其他操作等等;misc分区的结构组成详见下图;

下面以rk3588平台使用的misc分区为例,以二进制形式打开misc.img文件,在距文件开始位置偏移16K16384 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-radioupdate-hboot)后,bootloader将执行结果写入此处;
  • recovery:此字段用于normalrecovery系统之间的通信,比如如果期望linux系统重启进入recovery系统,需要将该参数设置为recovery\n--update_package=${savepath}
  • needupdaterecovery系统下需要升级的分区,即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.crktools.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.clog.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_MAXLOG_VERBOSELOG_DEBUGLOG_INFOLOG_WARNLOG_ERROR

5.3 download.c

download.c文件从文件名字就可以看出来,这个文件主要实现的就是文件下载功能。

5.3.1 download_file

download_file函数用于从url下载文件,函数接收两个参数:

  • url:需要下载的文件地址,可以是远端url路径,也可以是本地路径;
  • output_filename:下载的文件保存路径;

函数主要流程如下:

  • 如果urlhttp开头,则从远端使用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升级开发指南

[3] 嵌入式Linux开发之Makefile

posted @ 2024-06-22 11:12  大奥特曼打小怪兽  阅读(959)  评论(0编辑  收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步