程序项目代做,有需求私信(vue、React、Java、爬虫、电路板设计、嵌入式linux等)

Rockchip RK3588 - Rockchip Linux Recovery recovery源码分析

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

开发板 :NanoPC-T4开发板
eMMC16GB
LPDDR34GB
显示屏 :15.6英寸HDMI接口显示屏
uboot2017.09
linux4.19
----------------------------------------------------------------------------------------------------------------------------

在《Rockchip RK3588 - Rockchip Linux Recovery updateEngine源码分析》中我们对updateEngine源码进行了深入分析,并提到updateEngine 升级命令由两部分组成;

  • normal系统下的升级:升级recovery分区,并修改misc分区,重启系统;
  • recovery系统下的升级:系统重启之后,会根据misc分区存放的字段来判断将要引导的系统是normal系统还是recovery系统,这里会进入到recovery系统,进行剩余分区的升级;

其中normal系统下的升级是通过updateEngine可执行程序完成的;

// 1. 在normal系统,updateEngine升级recovery分区;升级完函数就返回了,并不会升级其它分区
main(argc, argv)
	MiscUpdate(image_url, partition, save_path)		
		//  对需要执行的升级命令打标记(这里标记了parameter、recovery升级命令)
		RK_ota_set_partition(0x040000)   
		// 进行parameter、recovery分区升级
		RK_ota_start(handle_upgrade_callback, handle_print_callback)
		// 往misc偏移16k位置写入recovery信息,这样系统重启后会进入recovery系统执行剩余分区的升级
		set_bootloader_message(&msg)
		
	// 2. 触发系统重启
	system(" echo b > /proc/sysrq-trigger ")

recovery系统下的升级,是通过recovery可执行程序实现,具体命令如下:

/usr/bin/recovery --update_package=/userdata/update.img

recovery二进制bin程序部会根据编译配置调用updateEngine或者rkupdate进行升级,本章的目标就是对recovery源码进行分析,实验基于《Rockchip RK3399 - 从零开始制作recovery系统》中移植的recovery系统

一、recovery目标分析

1.1 目标recovery

定位到<Rockchip Linux SDK>/external/recovery目录下的Makefile文件,找到目标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结尾的文件作为依赖文件。

1.2 编译recovery

在《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源码分析

recovery程序的入口为recovery.c文件。recovery.c代码比较长,如下所示:

点开查看详情
int main(int argc, char **argv)
{
    bool bSDBoot    = false;
    bool bUDiskBoot = false;
    const char *sdupdate_package = NULL;
    const char *usbupdate_package = NULL;
    int previous_runs = 0;
    const char *send_intent = NULL;
    const char *update_package = NULL;
    const char *encrypted_fs_mode = NULL;
    int wipe_data = 0;
    int wipe_all = 0;
    int pcba_test = 0;  // add for pcba test
    int toggle_secure_fs = 0;
    int arg;
    bool isrkdebug = false;
    int log_level = LOG_DEBUG;
    encrypted_fs_info encrypted_fs_data;
    struct timeval start_time, end_time;
    long long elapsed_time;

    gettimeofday(&start_time, NULL);
    
    // 2.1 获取命令行参数:该函数的目的是从多个来源获取命令行参数,并将这些参数存储在argv数组中,同时更新misc分区
    get_args(&argc, &argv);
    strcpy(systemFlag, "false");
    
    // 2.2 解析命令行参数:解析命令行参数
    while ((arg = getopt_long(argc, argv, "", OPTIONS, NULL)) != -1) {
        switch (arg) {
        case 'p':
            previous_runs = atoi(optarg);
            break;
        case 's':
            send_intent = optarg;
            break;
        case 'u':
            update_package = optarg;
            break;
        case 'w':
            wipe_data = 1;
            break;
        case 'a':
            wipe_all = 1;
            break;
        case 'e':
            encrypted_fs_mode = optarg;
            toggle_secure_fs = 1;
            break;
        case 't':
            ui_show_text(1);
            break;
        case 'f':
            pcba_test = 1;
            break;  // add for pcba test
        case 'r':
            isrkdebug = true;
            break;
        case 'i':
            gr_set_rotate(atoi(optarg));
            break;
        case '?':
            LOGE("Invalid command argument\n");
            continue;
        }
    }
    
    // 2.3 重定位标准输出和标准错误,并输出版本信息
    time_t start = time(NULL);
    if ((access("/.rkdebug", F_OK) != 0) && (isrkdebug != true)) {
        // If these fail, there's not really anywhere to complain...
        if (freopen(TEMPORARY_LOG_FILE, "a", stdout) == NULL) {
            LOGW("freopen stdout error");
        }
        setbuf(stdout, NULL);
        if (freopen(TEMPORARY_LOG_FILE, "a", stderr) == NULL) {
            LOGE("freopen stderr error");
        }
        setbuf(stderr, NULL);
    }
    
    printf("\n");
    printf("*********************************************************\n");
    printf("            ROCKCHIP recovery system                     \n");
    printf("*********************************************************\n");
    printf("**** version : %s ****\n", recovery_version);
    
    LOGI("Starting recovery on %s\n", ctime(&start));
    while (access(coldboot_done, F_OK) != 0) {
        LOGI("coldboot not done, wait...\n");
        sleep(1);
    }

#ifndef RecoveryNoUi
    LOGI("Recovery System have UI defined.\n");
#endif

    // 2.4 ui相关
    ui_init();
    ui_set_background(BACKGROUND_ICON_INSTALLING);
    
    // 2.5 获取/etc/fstab中定义的系统启动时要挂在的文件系统,同时将要挂载的文件系统数量保存到全局变量num_volumes
    load_volume_table();
    
    // 2.6 获取获取MMC、SD、SDIO等类型的设备节点路径
    setFlashPoint();
    
    // 2.7 启动方式判定
    bSDBoot = is_boot_from_SD();
    bUDiskBoot = is_boot_from_udisk();
    
    if (bSDBoot || bUDiskBoot) {
        char imageFile[64] = {0};
        if (bSDBoot) {
            if (is_sdcard_update()) {
                strlcpy(imageFile, EX_SDCARD_ROOT, sizeof(imageFile));
                strlcat(imageFile, "/sdupdate.img", sizeof(imageFile));
                if (access(imageFile, F_OK) == 0) {
                    sdupdate_package = strdup(imageFile);
                    bSDBootUpdate = true;
                    ui_show_text(1);
                    LOGI("sdupdate_package = %s\n", sdupdate_package);
                }
            }
        }
    
        if (bUDiskBoot) {
            if (is_udisk_update()) {
                strlcpy(imageFile, EX_UDISK_ROOT, sizeof(imageFile));
                strlcat(imageFile, "/sdupdate.img", sizeof(imageFile));
                if (access(imageFile, F_OK) == 0) {
                    usbupdate_package = strdup(imageFile);
                    bUdiskUpdate = true;
                    ui_show_text(1);
                    LOGI("usbupdate_package = %s\n", usbupdate_package);
                }
            }
        }
    }
    
    // 函数什么也没有做,直接返回0
    device_recovery_start();
    
    // 输出命令:LOG_INFO: Command: "recovery" "--update_package=/userdata/update.img"
    LOGI("Command:");
    for (arg = 0; arg < argc; arg++) {
        printf(" \"%s\"", argv[arg]);
    }
    printf("\n");
    
    // update_package="/userdata/update.img"
    if (update_package) {
        // For backwards compatibility on the cache partition only, if
        // we're given an old 'root' path "CACHE:foo", change it to
        // "/cache/foo".  不会进入
        if (strncmp(update_package, "CACHE:", 6) == 0) {
            int len = strlen(update_package) + 10;
            char* modified_path = malloc(len);
            strlcpy(modified_path, "/cache/", len);
            strlcat(modified_path, update_package + 6, len);
            LOGI("(replacing path \"%s\" with \"%s\")\n",
                 update_package, modified_path);
            update_package = modified_path;
        }
    }
    printf("\n");
    
    int status = INSTALL_SUCCESS;
    
    // 不会进入
    if (toggle_secure_fs) {
		......
    } else if (update_package != NULL) {  // 进入 2.8 升级固件检测
        int i, ret = 0;
        const char* binary = "/usr/bin/rkupdate";

        // 确保/oem、/userdata路径没有被挂载
        rockchip_partition_check();
    
        for (i = 0; i < 5; i++) {
            // 确保/userdata/update.img已经被挂载
            if (!ensure_path_mounted(update_package)) {
                LOGI("mounted %s Success.\n", update_package);
                break;
            }
            LOGW("mounted %s Failed. retry %d\n", update_package, i + 1);
            sleep(1);
        }
        if (i != 5) {
            LOGI(">>>rkflash will update from %s\n", update_package);
#ifdef USE_RKUPDATE
            status = do_rk_update(binary, update_package);
#endif
#ifdef USE_UPDATEENGINE   // 走这里 2.9 执行剩余分区的升级功能
            const char* updateEnginebin = "/usr/bin/updateEngine";
            status = do_rk_updateEngine(updateEnginebin, update_package);
#endif
            if (status == INSTALL_SUCCESS) {
                strcpy(systemFlag, update_package);
                /* update success, delete update.img. */
                if (access(update_package, F_OK) == 0)
                    remove(update_package);
                ui_print("update.img images success!\n");
            } else {
                ui_print("update.img images failed!\n");
            }
        } else {
            LOGE("mounted %s Failed.\n", update_package);
            ui_print("mounted %s Failed.\n", update_package);
        }

        if (status != INSTALL_SUCCESS) ui_print("Installation aborted.\n");
        ui_print("update.img Installation done.\n");
        //ui_show_text(0);
    } else if (sdupdate_package != NULL) {
		......
    
    } else if (usbupdate_package != NULL) {
       .......
    } else if (wipe_data) {
       .......
    } else if (wipe_all) {
       .......
    } else if (pcba_test) {
        //pcba test todo...
        printf("------------------ pcba test start -------------\n");
        exit(EXIT_SUCCESS); //exit recovery bin directly, not start pcba here, in rkLanuch.sh
        return 0;
    } else {
        if (argc == 1) { // No command specified
            if (!bSDBootUpdate && !bUdiskUpdate && ui_text_visible())
                prompt_and_wait();
            finish_recovery(NULL);
            reboot(RB_AUTOBOOT);
            return 0;
        }
        status = INSTALL_ERROR;  // No command specified
    }
    
    if (status != INSTALL_SUCCESS) ui_set_background(BACKGROUND_ICON_ERROR);
    if (status != INSTALL_SUCCESS) {
        LOGE("\n Install fail! \n");
        if (!bSDBootUpdate && !bUdiskUpdate && ui_text_visible())
            prompt_and_wait();
    }
    
    if (sdupdate_package != NULL && bSDBootUpdate) {
       .......
    } else if (usbupdate_package && bUdiskUpdate) {
       .......
    }
    
    // Otherwise, get ready to boot the main system...
    // 2.10 进行一些收尾工作
    finish_recovery(send_intent);
    gettimeofday(&end_time, NULL);
    
    elapsed_time = (end_time.tv_sec - start_time.tv_sec) * 1000LL +
                   (end_time.tv_usec - start_time.tv_usec) / 1000LL;
    LOGI("recovery usage time:%lld ms\n", elapsed_time);
    ui_print("Rebooting...\n");
    LOGI("Reboot...\n");
    ui_show_text(0);
    fflush(stdout);
    sync();
    // 系统重启
    reboot(RB_AUTOBOOT);
    return EXIT_SUCCESS;
}

2.1 获取命令行参数

首先调用get_args函数,该函数的目的是从多个来源获取命令行参数,并将这些参数存储在 argv 数组中,同时更新misc分区;

// command line args come from, in decreasing precedence:
//   - the actual command line
//   - the bootloader control block (one per line, after "recovery")
//   - the contents of COMMAND_FILE (one per line)
static void
get_args(int *argc, char ***argv)
{
    // 1. misc偏移16k位置存放的是recovery信息,因此recovery系统重启后会加载recovery信息
    struct bootloader_message boot;
    memset(&boot, 0, sizeof(boot));
    get_bootloader_message(&boot);  // this may fail, leaving a zeroed structure

    // 输出命令:Boot command: boot-recovery
    if (boot.command[0] != 0 && boot.command[0] != 255) {
        LOGI("Boot command: %.*s\n", (int)sizeof(boot.command), boot.command);
    }

    // 输出状态
    if (boot.status[0] != 0 && boot.status[0] != 255) {
        LOGI("Boot status: %.*s\n", (int)sizeof(boot.status), boot.status);
    }

    // --- if arguments weren't supplied, look in the bootloader control block
    // 2. 如果argc小于或等于1(即没有提供命令行参数),函数会尝试从boot.recovery中提取参数
    if (*argc <= 1) {
        // 首先确保boot.recovery字符串以空字符结尾,然后使用strtok将boot.recovery字符串分割成以换行符\n为界的多个部分
        // \n对应的ASCII值是0x0a
        // boot.recovery="recovery\n--update_package=/userdata/update.img\n"
        boot.recovery[sizeof(boot.recovery) - 1] = '\0';  // Ensure termination
        // arg=”recovery”
        // boot.recovery="recovery\0--update_package=/userdata/update.img\n"
        const char *arg = strtok(boot.recovery, "\n");
        // 满足条件
        if (arg != NULL && !strcmp(arg, "recovery")) {      
            *argv = (char **) malloc(sizeof(char *) * MAX_ARGS);
            // 保存参数arg
            (*argv)[0] = strdup(arg);
            for (*argc = 1; *argc < MAX_ARGS; ++*argc) {
                // 获取下一个子字符串
		        // arg=”--update_package=/userdata/update.img”
        		// boot.recovery="recovery\0--update_package=/userdata/update.img\0"
                if ((arg = strtok(NULL, "\n")) == NULL) break;
                (*argv)[*argc] = strdup(arg);
            }
            LOGI("Got arguments from boot message\n");
        } else if (boot.recovery[0] != 0 && boot.recovery[0] != 255) {
            LOGE("Bad boot message\n\"%.20s\"\n", boot.recovery);
        }
    }

    // --- if that doesn't work, try the command file
    if (*argc <= 1) {
 		......
    }

    // --> write the arguments we have back into the bootloader control block
    // always boot into recovery after this (until finish_recovery() is called)
    // 3. 设置参数
    // 设置boot.command="boot-recovery"
    // 设置boot.recovery="recovery"
    strlcpy(boot.command, "boot-recovery", sizeof(boot.command));
    strlcpy(boot.recovery, "recovery\n", sizeof(boot.recovery));
    int i;
    // 合并参数,最后boot.recovery="recovery\n--update_package=/userdata/update.img\n"
    for (i = 1; i < *argc; ++i) {
        strlcat(boot.recovery, (*argv)[i], sizeof(boot.recovery));
        strlcat(boot.recovery, "\n", sizeof(boot.recovery));
    }
    //  往misc偏移16k位置写入recovery信息
    set_bootloader_message(&boot);
}

有关struct bootloader_message参考《Rockchip RK3588 - Rockchip Linux Recovery updateEngine源码分析》第4.4节。比如misc分区偏移16k的数据如下:

10000000: 62 6f 6f 74 2d 72 65 63 6f 76 65 72 79 00 00 00    boot-recovery...
10000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
10000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
10000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
10000040: 72 65 63 6f 76 65 72 79 0a 2d 2d 75 70 64 61 74    recovery.--updat
10000050: 65 5f 70 61 63 6b 61 67 65 3d 2f 75 73 65 72 64    e_package=/userd
10000060: 61 74 61 2f 75 70 64 61 74 65 2e 69 6d 67 0a 00    ata/update.img..
10000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
10000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
10000090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
......
10000340: 00 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
......

2.2 解析命令行参数

由于recovery是通过传参来实现固件升级的,因此不难猜出recovery是基于命令行参数的程序。

接着使用了getopt_long函数来解析命令行参数,并根据参数的不同执行相应的操作。

static const struct option OPTIONS[] = {
    { "send_intent", required_argument, NULL, 's' },
    { "update_package", required_argument, NULL, 'u' },
    { "wipe_data", no_argument, NULL, 'w' },
    { "wipe_all", no_argument, NULL, 'a' },
    { "set_encrypted_filesystems", required_argument, NULL, 'e' },
    { "show_text", no_argument, NULL, 't' },
    { "factory_pcba_test", no_argument, NULL, 'f' },
    { "rkdebug", no_argument, NULL, 'r'},
    { "ui_rotation", required_argument, NULL, 'i'},
    { NULL, 0, NULL, 0 },
};

while ((arg = getopt_long(argc, argv, "", OPTIONS, NULL)) != -1) {
	switch (arg) {
	case 'p':
		previous_runs = atoi(optarg);
		break;
	case 's':
		send_intent = optarg;
		break;
	case 'u':  // --update_package=/userdata/update.img
		update_package = optarg;
		break;
	case 'w':
		wipe_data = 1;
		break;
	case 'a':
		wipe_all = 1;
		break;
	case 'e':
		encrypted_fs_mode = optarg;
		toggle_secure_fs = 1;
		break;
	case 't':
		ui_show_text(1);
		break;
	case 'f':
		pcba_test = 1;
		break;  // add for pcba test
	case 'r':
		isrkdebug = true;
		break;
	case 'i':
		gr_set_rotate(atoi(optarg));
		break;
	case '?':
		LOGE("Invalid command argument\n");
		continue;
	}
}

getopt_long 这是一个用于获取命令行选项的函数。它可以处理长选项,并与短选项配合使用。

  • argcargv是标准的命令行参数;
  • OPTIONS是定义选项的字符串或结构体(通常包含短选项及其参数信息);
  • NULL 是指不使用长选项的返回值。

由于(*argv)[0]="recovery"(*argv)[1]="--update_package=/userdata/update.img",因此会进入u分支,设置update_package="/userdata/update.img"

2.3 输出版本信息

此外如果创建了/.rkdebug文件,则将将标准输出和标准错误重定向到一个临时日志文件/tmp/recovery.log,,以便进行调试和记录;

if ((access("/.rkdebug", F_OK) != 0) && (isrkdebug != true)) {
	// If these fail, there's not really anywhere to complain...
	if (freopen(TEMPORARY_LOG_FILE, "a", stdout) == NULL) {
		LOGW("freopen stdout error");
	}
	setbuf(stdout, NULL);
	if (freopen(TEMPORARY_LOG_FILE, "a", stderr) == NULL) {
		LOGE("freopen stderr error");
	}
	setbuf(stderr, NULL);
}

接着main函数输出当前recovery版本信息,比如:

*********************************************************
            ROCKCHIP recovery system
*********************************************************
**** version : V1.0.1-g28f720bc5-240524-dirty ****
LOG_INFO: Starting recovery on Sun Oct  6 09:04:09 2024

如果没有配置RecoveryNoUi,输出:

LOG_INFO: Recovery System have UI defined.

2.4 ui相关

2.4.1 ui_init

ui_init函数定义在ui.c文件;

void ui_init(void)
{
    int ret = 0;
    ret = gr_init();
    if (ret)
        return;

    ev_init((ev_callback)input_callback, NULL);

    text_col = text_row = 0;
    text_rows = gr_fb_height() / CHAR_HEIGHT;
    if (text_rows > MAX_ROWS) text_rows = MAX_ROWS;
    text_top = 1;

    text_cols = gr_fb_width() / CHAR_WIDTH;
    if (text_cols > MAX_COLS - 1) text_cols = MAX_COLS - 1;

    int i;
    for (i = 0; BITMAPS[i].name != NULL; ++i) {
        int result = res_create_display_surface(BITMAPS[i].name, BITMAPS[i].surface);
        if (result < 0) {
            if (result == -2) {
                LOGI("Bitmap %s missing header\n", BITMAPS[i].name);
            } else {
                LOGE("Missing bitmap %s\n(Code %d)\n", BITMAPS[i].name, result);
            }
            *BITMAPS[i].surface = NULL;
        }
    }

    pthread_t t;
    pthread_create(&t, NULL, progress_thread, NULL);
    pthread_create(&t, NULL, input_thread, NULL);
}
2.4.2 ui_set_background

ui_set_background函数定义在ui.c文件;

void ui_set_background(int icon)
{
    if (!gr_draw)
        return;

    pthread_mutex_lock(&gUpdateMutex);
    gCurrentIcon = gBackgroundIcon[icon];
    update_screen_locked();
    pthread_mutex_unlock(&gUpdateMutex);
}

2.5 加载/etc/fstab

load_volume_table函数用于获取/etc/fstab中定义的系统启动时要挂在的文件系统,同时将要挂载的文件系统数量保存到全局变量num_volumes

rk3399 login: recovery filesystem table
=========================
  0 (null) /tmp ramdisk (null) (null) (null)
  1 /dev/root / ext2 rw,noauto 0 1
  2 proc /proc proc defaults 0 0
  3 devpts /dev/pts devpts defaults,gid=5,mode=620,ptmxmode=0666 0 0
  4 tmpfs /dev/shm tmpfs mode=1777 0 0
  5 tmpfs /tmp tmpfs mode=1777 0 0
  6 tmpfs /run tmpfs mode=0755,nosuid,nodev 0 0
  7 sysfs /sys sysfs defaults 0 0
  8 configfs /sys/kernel/config configfs defaults 0 0
  9 debugfs /sys/kernel/debug debugfs defaults 0 0

load_volume_table函数定义在roots.c

void load_volume_table()
{
    int alloc = 2;
    // 保存块设备信息
    device_volumes = malloc(alloc * sizeof(Volume));

    // Insert an entry for /tmp, which is the ramdisk and is always mounted.
    device_volumes[0].mount_point = "/tmp";
    device_volumes[0].fs_type = "ramdisk";
    device_volumes[0].device = NULL;
    device_volumes[0].device2 = NULL;
    device_volumes[0].option = NULL;
    device_volumes[0].dump = NULL;
    device_volumes[0].pass = NULL;
    num_volumes = 1;

    // 打开/etc/fstab文件
    FILE* fstab = fopen("/etc/fstab", "r");
    if (fstab == NULL) {
        LOGE("failed to open /etc/fstab (%d)\n", errno);
        return;
    }

    char buffer[1024];
    char file_system[1024];
    char mount_point[1024];
    char fs_type[1024];
    char option[1024];
    char dump[1024];
    char pass[1024];
    char device[1024];
    int i;
    // 从文件中读取一行
    while (fgets(buffer, sizeof(buffer) - 1, fstab)) {
        // 将读取的行数据依次解析到file_system、mount_point、fs_type、option等
        i = sscanf(buffer, "%s %s %s %s %s %s", file_system,
                   mount_point, fs_type, option, dump, pass);
        if (file_system[0] == '#') continue;
        //printf("load_volume_table file_system:%s, mount_point:%s, fs_type:%s, option:%s, dump:%s, pass:%s\n", file_system, mount_point, fs_type, option, dump, pass);
        /* HACK: Convert *LABEL to "by-name" symlink */
        // 如果配置的LABEL=xxx,则设置device="/dev/block/by-name/xxxx"
        if (strstr(file_system, "LABEL="))
            snprintf(device, sizeof(device), "/dev/block/by-name/%s",
                     strstr(file_system, "LABEL=") + strlen("LABEL="));
        else
            // 负责直接设置device=file_system
            strcpy(device, file_system);

        // 一行6个参数
        if (i == 6) {
            // 动态扩容
            while (num_volumes >= alloc) {
                alloc *= 2;
                device_volumes = realloc(device_volumes, alloc * sizeof(Volume));
            }
            device_volumes[num_volumes].mount_point = strdup(mount_point);
            device_volumes[num_volumes].fs_type = strdup(fs_type);
            device_volumes[num_volumes].option = strdup(option);
            device_volumes[num_volumes].dump = strdup(dump);
            device_volumes[num_volumes].pass = strdup(pass);
            device_volumes[num_volumes].device = strdup(device);;
            device_volumes[num_volumes].device2 = NULL;
            ++num_volumes;
        } else {
            LOGE("skipping malformed recovery.fstab line: %s\n", buffer);
        }
    }

    fclose(fstab);

    printf("recovery filesystem table\n");
    printf("=========================\n");
    for (i = 0; i < num_volumes; ++i) {
        Volume* v = &device_volumes[i];
        printf("  %d %s %s %s %s %s %s\n", i, v->device, v->mount_point, v->fs_type, v->option, v->dump, v->pass);
    }
    printf("\n");
}

2.6 setFlashPoint

setFlashPoint函数定义在rktools.c,用于获取MMCSDSDIO等类型的设备节点路径;

void setFlashPoint()
{
    // 判断是不是MTD设备(Nor/Nand Flash设备),对于SD、eMMC不属于MTD设备
    if (!isMtdDevice())   // 进入
        // 等待设备节点/dev/block/by-name/misc可用
        wait_for_device(MISC_PARTITION_NAME_BLOCK);

    // 通过读取/sys/bus/mmc/devices/目录中的条目来查找SD或eMMC设备,并更新result_point
    init_sd_emmc_point();
    
    // 设置环境变量emmc_point_name="/dev/mmcblk2"
    setenv(EMMC_POINT_NAME, result_point[MMC], 1);    
    
    // 判断SD类型设备是否存在,设备节点为/dev/mmcblk0
    if (access(result_point[SD], F_OK) == 0)
        // 设置环境变量sd_point_name_2
        setenv(SD_POINT_NAME_2, result_point[SD], 1);
    
    // /dev/mmcblk0p1
    char name_t[22];
    if (strlen(result_point[SD]) > 0) {        
        strcpy(name_t, result_point[SD]);
        strcat(name_t, "p1");
    }
    if (access(name_t, F_OK) == 0)
        // 设置环境变量sd_point_name
        setenv(SD_POINT_NAME, name_t, 1);

    // emmc_point_name
    LOGI("emmc_point is %s\n", getenv(EMMC_POINT_NAME));
    // sd_point_name
    LOGI("sd_point is %s\n", getenv(SD_POINT_NAME));
    // sd_point_name_2
    LOGI("sd_point_2 is %s\n", getenv(SD_POINT_NAME_2));
}

函数执行完,输出信息大致如下;

LOG_INFO: devices is not MTD.
LOG_INFO: emmc_point is /dev/mmcblk2    # SDIO类型设备,在这里对应的是板载eMMC
LOG_INFO: sd_point is (null)
LOG_INFO: sd_point_2 is (null)
2.6.1 wait_for_device

wait_for_device用于 确保某个文件(例如设备节点,挂载点或配置文件)可用;

static void wait_for_device(const char* fn)
{
    int tries = 0;
    int ret;
    struct stat buf;
    do {
        ++tries;
        // 检查文件状态
        ret = stat(fn, &buf);
        // 回非零值,表示路径不可用	
        if (ret) {
            LOGI("stat %s try %d: %s\n", fn, tries, strerror(errno));
            sleep(1);
        }
    } while (ret && tries < 10);
    if (ret) {
        LOGI("failed to stat %s\n", fn);
    }
}
2.6.2 init_sd_emmc_point

init_sd_emmc_point通过读取/sys/bus/mmc/devices/目录中的条目来查找SDeMMC设备,并更新result_point

static const char *point_items[] = {
    "/dev/mmcblk0",     // MMC类型设备对应的设备节点
    "/dev/mmcblk1",     // SD类型设备对应的设备节点
    "/dev/mmcblk2",     // SDIO类型设备对应的设备节点
    "/dev/mmcblk3",     // SDcombo类型设备对应的设备节点
};

enum type {
    MMC,            // MMC类型
    SD,             // SD类型
    SDIO,           // SDIO类型
    SDcombo,        // .....
};

static const char *typeName[] = {
    "MMC",
    "SD",
    "SDIO",
    "SDcombo",
};

/**
 *  设置flash 节点
 */
static char result_point[4][20] = {'\0'}; //0-->MMC, 1-->SD, 2-->SDIO, 3-->SDcombo

// 读取dir目录下的filename文件
int readFile(DIR* dir, char* filename)
{
    char name[30] = {'\0'};
    int i;

    // 构建文件路径:mmc0:0001/type
    // 构建文件路径:mmc1:0001/type
    // 构建文件路径:mmc2:0001/type
    strcpy(name, filename);
    strcat(name, "/type");
    
    // 打开文件
    int fd = openat(dirfd(dir), name, O_RDONLY);
    if (fd == -1) {
        LOGE("Error: openat %s error %s.\n", name, strerror(errno));
        return -1;
    }
    
    // 读取文件内容到resultBuf中
    char resultBuf[10] = {'\0'};
    if (read(fd, resultBuf, sizeof(resultBuf)) < 1) {
        return -1;
    }
    
    // 将换行符替换为字符串结束符
    for (i = 0; i < strlen(resultBuf); i++) {
        if (resultBuf[i] == '\n') {
            resultBuf[i] = '\0';
            break;
        }
    }
    
    // 比较读取的内容与预定义的类型名称typeName,如果匹配则返回索引
    for (i = 0; i < 4; i++) {
        if (strcmp(typeName[i], resultBuf) == 0) {
            //printf("type is %s.\n", typeName[i]);
            return i;
        }
    }

    LOGE("Error:no found type!\n");
    return -1;
}

// 通过读取/sys/bus/mmc/devices/目录中的条目来查找SD或eMMC设备
void init_sd_emmc_point()
{
    DIR* dir = opendir("/sys/bus/mmc/devices/");
    // 目录存在
    if (dir != NULL) {
        struct dirent* de;
        // 循环读取目录中的每个条目,比如mmc0:0001、mmc1:0001、mmc2:0001
        while ((de = readdir(dir))) {
            // 排除当前目录 (.) 和父目录 (..)
            if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0 )
                continue;
            //if (de->d_type == 4)    //dir
            //    printf("dir name : %s \n", de->d_name);

            // 检查目录项名称是否以mmc开头,以确定是否是mmc设备。	
            if (strncmp(de->d_name, "mmc", 3) == 0) {
                //printf("find mmc is %s.\n", de->d_name);
                char flag = de->d_name[3];
                int ret = -1;
                // 读取dir目录下的mmc0:0001/type文件,获取设备类型
                ret = readFile(dir, de->d_name);
                // 设备类型存在
                if (ret != -1) {
                    // mmc0:0001/type -> result_point[SDIO]="/dev/mmcblk2"
                    // mmc1:0001/type -> result_point[SD]="/dev/mmcblk1"
                    // mmc2:0001/type -> result_point[MMC]="/dev/mmcblk0"
                    strcpy(result_point[ret], point_items[flag - '0']);
                } else {
                    // 无效
                    strcpy(result_point[ret], "");
                }
            }
        }
    }
    closedir(dir);
}

NanoPC-T4开发板为例(未接入SD卡):

pi@NanoPC-T4:~$ ls -l  /sys/bus/mmc/devices
total 0
lrwxrwxrwx 1 root root 0 Oct  6 14:41 mmc0:0001 -> ../../../devices/platform/fe310000.mmc/mmc_host/mmc0/mmc0:0001
lrwxrwxrwx 1 root root 0 Oct  6 14:41 mmc2:0001 -> ../../../devices/platform/fe330000.sdhci/mmc_host/mmc2/mmc2:0001
pi@NanoPC-T4:~$ cat  /sys/bus/mmc/devices/mmc0\:0001/type
SDIO
pi@NanoPC-T4:~$ cat  /sys/bus/mmc/devices/mmc2\:0001/type
MMC

函数执行完毕:

result_point[MMC]="/dev/mmcblk0"
result_point[SD]=""
result_point[SDIO]="/dev/mmcblk2"
result_point[SDcombo]=""    

如果插入SD卡:

pi@NanoPC-T4:~$ ls -l  /sys/bus/mmc/devices
total 0
lrwxrwxrwx 1 root root 0 Oct  6 14:41 mmc0:0001 -> ../../../devices/platform/fe310000.mmc/mmc_host/mmc0/mmc0:0001
lrwxrwxrwx 1 root root 0 Oct  6 15:02 mmc1:0001 -> ../../../devices/platform/fe320000.mmc/mmc_host/mmc1/mmc1:0001
lrwxrwxrwx 1 root root 0 Oct  6 14:41 mmc2:0001 -> ../../../devices/platform/fe330000.sdhci/mmc_host/mmc2/mmc2:0001

pi@NanoPC-T4:~$ cat  /sys/bus/mmc/devices/mmc1\:0001/type
SD

2.7 启动方式判定

接着通过读取命令行信息判断启动方式,用于检测是否从SD/USB启动;

bSDBoot = is_boot_from_SD();
bUDiskBoot = is_boot_from_udisk();

if (bSDBoot || bUDiskBoot) {
	char imageFile[64] = {0};
	if (bSDBoot) {
		if (is_sdcard_update()) {
			strlcpy(imageFile, EX_SDCARD_ROOT, sizeof(imageFile));
			strlcat(imageFile, "/sdupdate.img", sizeof(imageFile));
			if (access(imageFile, F_OK) == 0) {
				sdupdate_package = strdup(imageFile);
				bSDBootUpdate = true;
				ui_show_text(1);
				LOGI("sdupdate_package = %s\n", sdupdate_package);
			}
		}
	}

	if (bUDiskBoot) {
		if (is_udisk_update()) {
			strlcpy(imageFile, EX_UDISK_ROOT, sizeof(imageFile));
			strlcat(imageFile, "/sdupdate.img", sizeof(imageFile));
			if (access(imageFile, F_OK) == 0) {
				usbupdate_package = strdup(imageFile);
				bUdiskUpdate = true;
				ui_show_text(1);
				LOGI("usbupdate_package = %s\n", usbupdate_package);
			}
		}
	}
}

由于我们采用的eMMC启动方式,所以输出如下日志:

LOG_INFO: read cmdline
LOG_INFO: >>> Boot from non-SDcard
LOG_INFO: read cmdline
LOG_INFO: >>> Boot from non-U-Disk
2.7.1 is_boot_from_SD

is_boot_from_SD函数定义在sdboot.c

bool is_boot_from_SD(void)
{
    bool bSDBoot = false;
    char param[1024];
    int fd, ret;
    char *s = NULL;
    LOGI("read cmdline\n");
    memset(param, 0, 1024);

    // 读取命令行
    fd = open("/proc/cmdline", O_RDONLY);
    ret = read(fd, (char*)param, 1024);

    // 查找sdfwupdate字符串
    s = strstr(param, "sdfwupdate");
    if (s != NULL) {
        bSDBoot = true;
        LOGI(">>> Boot from SDcard\n");
    } else {
        bSDBoot = false;
        LOGI(">>> Boot from non-SDcard\n");
    }

    close(fd);
    return bSDBoot;
}
2.7.2 is_boot_from_udisk

is_boot_from_udisk函数定义在usbboot.c

bool is_boot_from_udisk(void)
{
    bool bUDisk = false;
    char param[1024];
    int fd, ret;
    char *s = NULL;
    LOGI("read cmdline\n");
    memset(param, 0, 1024);

    // 读取命令行
    fd = open("/proc/cmdline", O_RDONLY);
    ret = read(fd, (char*)param, 1024);

    // 查找usbfwupdate字符串
    s = strstr(param, "usbfwupdate");
    if (s != NULL) {
        bUDisk = true;
        LOGI(">>> Boot from U-Disk\n");
    } else {
        bUDisk = false;
        LOGI(">>> Boot from non-U-Disk\n");
    }

    close(fd);
    return bUDisk;
}

2.8 升级固件检测

接着就是升级固件的检测,这里包含两步检测:

  • 确保/oem/userdata路径没有被挂载;

  • 确保升级固件路径/userdata/update.img已经挂载;

源码如下:

int i, ret = 0;
const char* binary = "/usr/bin/rkupdate";

// 检测oem、userdata分区是否挂载
rockchip_partition_check();

for (i = 0; i < 5; i++) {
	//update_package="/userdata/update.im", 函数内部确保/userdata路径已经挂载
	if (!ensure_path_mounted(update_package)) {
		LOGI("mounted %s Success.\n", update_package);
		break;
	}
	LOGW("mounted %s Failed. retry %d\n", update_package, i + 1);
	sleep(1);
}

比如我们在使用NanoPC-T4开发板进行测试的时候,并没有在/etc/fstab中配置/oem/userdata路径的挂载信息;因此会输出如下信息:

# 输出下面这个错误是因为我们没有在/etc/fstab中配置/oem这个路径的挂载信息
LOG_ERROR: unknown volume for path [/oem]
LOG_ERROR:
 === umount oem fail ===
 # 输出下面这个错误是因为我们没有在/etc/fstab中配置/userdata这个路径的挂载信息
LOG_ERROR: unknown volume for path [/userdata]
LOG_ERROR:
 === umount userdata fail ===
LOG_INFO: check userdata/oem partition success ...
# /userdata路径挂载成功
LOG_INFO: mounted /userdata/update.img Success.

我们采用了另外一种方式,即在制作的recovery系统的/etc/inittab文件中配置了oemuserdata分区的的挂载命令;

# 用于挂载oem、userdata分区(如果分区存在的话)
::sysinit:/bin/mount -t ext4 -r /dev/block/by-name/oem /oem
::sysinit:/bin/mount -t ext4 /dev/block/by-name/userdata /userdata

所以后面在使用updateEngine命令进行剩余分区升级的时候,是可以找到升级固件/userdata/update.img的。

2.8.1 rockchip_partition_check

rockchip_partition_check函数用于确保/oem/userdata路径没有被挂载;

// rockchip partition check (e.g: oem/userdata....)
static void
rockchip_partition_check()
{
    // 确保/oem路径没有被挂载
    if (ensure_path_unmounted("/oem") != 0)
        LOGE("\n === umount oem fail === \n");

    // 确保/userdata路径没有被挂载
    if (ensure_path_unmounted("/userdata") != 0)
        LOGE("\n === umount userdata fail === \n");

    ui_print("check userdata/oem partition success ...\n");
    LOGI("check userdata/oem partition success ...\n");
}
2.8.2 ensure_path_unmounted

ensure_path_unmounted函数定义在roots.c文件中,该函数用于确保路径path没有被挂载。这里有一点需要注意:

  • 如果输入path="/oem",则判定的实际挂载点mount_point="/oem"
  • 如果输入path="/userdata/update.img,则判定的实际挂载点mount_point="/userdata"
  • 如果输入path="/userdata/recovery/last_log,则判定的实际挂载点mount_point="/userdata/recovery"

如果挂载了该路径文件系统,比如/dev/userdata设备挂载到/userdata,则取消挂载;

int ensure_path_unmounted(const char* path)
{
    // 检测/etc/fstab文件中配置的若干文件系统挂载点是否包含了path这个路,如果包含,则返回匹配的文件系统挂载信息
    Volume* v = volume_for_path(path);
    // 如果找不到匹配项,返回-1; 在使用NanoPC-T4开发板进行测试时走到这一步就会返回
    if (v == NULL) {
        LOGE("unknown volume for path [%s]\n", path);
        return -1;
    }
    
    // 如果文件系统类型为ramdisk,则返回-1
    if (strcmp(v->fs_type, "ramdisk") == 0) {
        // the ramdisk is always mounted; you can't unmount it.
        return -1;
    }

    // 读取/proc/mount文件获取当前系统上真实挂载的所有文件系统的信息
    int result;
    result = scan_mounted_volumes();
    if (result < 0) {
        LOGE("failed to scan mounted volumes\n");
        return -1;
    }

    // 从已经挂载的文件系统列表中查找挂载点为path`的文件系统
    const MountedVolume* mv =
        find_mounted_volume_by_mount_point(v->mount_point);
    // 未挂载
    if (mv == NULL) {
        // volume is already unmounted
        return 0;
    }

    // 如果mv文件系统已经挂载,取消挂载
    return unmount_mounted_volume(mv);
}

/etc/fstab: 这是一个静态文件,定义了系统启动时要挂载的文件系统,而 /proc/mounts 是动态的,反映当前的挂载状态。

通过检测/etc/fstab文件中配置的要挂载的文件系统,如果挂载点为path,则通过读取/proc/mount来判定该文件系统是否真正挂载,如果真正挂载了则进行卸载。

2.8.2.1 volume_for_path

volume_for_path定义在roots.c文件中,用于检测/etc/fstab文件中配置的若干文件系统挂载点是否包含了path这个路,如果包含,则返回匹配的文件系统挂载信息;

static char mount_point[256] = {0};
// 如果输入linkpath="/oem",则返回buf="/oem"
// 如果输入linkpath="/userdata/update.img",则返回buf="/userdata"
// 如果输入linkpath="/userdata/recovery/last_log",则返回buf="/userdata/recovery"
char * get_link_path(const char* linkpath, char * buf="/oem", int count)
{
    int i;
    int rslt;
    char path[256];

    // 安全复制linkpath并限制长度
    memset(path, 0, sizeof(path));
    strcpy(path, linkpath);
    
     // 查找最后一个'/'并将其替换为'\0',注意这里跳过了第一个字符
    for (i = strlen(linkpath); i > 0; i--) {
        if (path[i] == '/') {
            path[i] = '\0';
            break;
        }
    }

    // 读取符号链接
    rslt = readlink(path, buf, count - 1);
    if (rslt < 0 || (rslt >= count - 1)) {
        // printf("No link to path [%s]!!! \n", path);
        return NULL;
    }

    // 确保buf以'/'开头
    if (buf[0] != '/') {
        char tmp[256];
        memset(tmp, 0, sizeof(tmp));
        memcpy(tmp, buf, strlen(buf));

        memset(buf, 0, sizeof(buf));
        memcpy(buf + 1, tmp, strlen(tmp));

        buf[0] = '/';
    }

    // printf("buf = %s \n", buf);
    return buf;
}

// path传入/oem
// path传入/userdata
// 返回/etc/fstab中配置的文件系统挂载信息
Volume* volume_for_path(const char* path)
{
    int i;

    // 填充0
    memset(mount_point, 0, sizeof(mount_point));
    // 这里path指向的实际路径,保存到mount_point
    char* tmp = get_link_path(path, mount_point, 1024);

    // if ( tmp != NULL)
    //  printf(" ### get mount_ponit = %s ### \n", mount_point);
	// 遍历/etc/fstab中定义的文件系统挂载点和挂载选项
    for (i = 0; i < num_volumes; ++i) {
        // 获取第i个挂载信息
        Volume* v = device_volumes + i;
        int len = strlen(v->mount_point);
        // 判断/oem、/userdata目录是否挂载
        if (strncmp(path, v->mount_point, len) == 0 &&
            (path[len] == '\0' || path[len] == '/')) {
            //printf(" ===path = %s, v-mount_point = %s ===\n",path, v->mount_point);
            return v;
        } else {
            //add by chad.ma for symbol link file. eg. sdcard/ --->mnt/sdcard
            if (!tmp)
                continue;

            //printf(" # v->mount_point = %s\n", v->mount_point);
            if (strncmp(mount_point, v->mount_point, len) == 0 &&
                (path[len] == '\0' || path[len] == '/')) {
                return v;
            }
        }
    }
    return NULL;
}
2.8.2.1 scan_mounted_volumes

scan_mounted_volumes函数定义在mounts.c文件,通过读取/proc/mount文件获取当前系统上真实挂载的所有文件系统的信息;

typedef struct {
    MountedVolume *volumes;
    int volumes_allocd;
    int volume_count;
} MountsState;

static MountsState g_mounts_state = {
    NULL,   // volumes
    0,      // volumes_allocd
    0       // volume_count
};

int scan_mounted_volumes()
{
    char buf[2048];
    const char *bufp;
    int fd;
    ssize_t nbytes;

    if (g_mounts_state.volumes == NULL) {
        const int numv = 32;
        MountedVolume *volumes = malloc(numv * sizeof(*volumes));
        if (volumes == NULL) {
            errno = ENOMEM;
            return -1;
        }
        g_mounts_state.volumes = volumes;
        g_mounts_state.volumes_allocd = numv;
        memset(volumes, 0, numv * sizeof(*volumes));
    } else {
        /* Free the old volume strings.
         */
        int i;
        for (i = 0; i < g_mounts_state.volume_count; i++) {
            free_volume_internals(&g_mounts_state.volumes[i], 1);
        }
    }
    g_mounts_state.volume_count = 0;

    /* Open and read the file contents.
     * 读取/proc/mounts文件,获取真实挂载的文件系统信息
     */
    fd = open(PROC_MOUNTS_FILENAME, O_RDONLY);
    if (fd < 0) {
        goto bail;
    }
    nbytes = read(fd, buf, sizeof(buf) - 1);
    close(fd);
    if (nbytes < 0) {
        goto bail;
    }
    buf[nbytes] = '\0';

    /* Parse the contents of the file, which looks like:
     *
     *     # cat /proc/mounts
     *     rootfs / rootfs rw 0 0
     *     /dev/pts /dev/pts devpts rw 0 0
     *     /proc /proc proc rw 0 0
     *     /sys /sys sysfs rw 0 0
     *     /dev/block/mtdblock4 /system yaffs2 rw,nodev,noatime,nodiratime 0 0
     *     /dev/block/mtdblock5 /data yaffs2 rw,nodev,noatime,nodiratime 0 0
     *     /dev/block/mmcblk0p1 /sdcard vfat rw,sync,dirsync,fmask=0000,dmask=0000,codepage=cp437,iocharset=iso8859-1,utf8 0 0
     *
     * The zeroes at the end are dummy placeholder fields to make the
     * output match Linux's /etc/mtab, but don't represent anything here.
     */
    bufp = buf;
    while (nbytes > 0) {
        char device[64];
        char mount_point[64];
        char filesystem[64];
        char flags[128];
        int matches;

        /* %as is a gnu extension that malloc()s a string for each field.
         */
        matches = sscanf(bufp, "%63s %63s %63s %127s",
                         device, mount_point, filesystem, flags);

        if (matches == 4) {
            device[sizeof(device) - 1] = '\0';
            mount_point[sizeof(mount_point) - 1] = '\0';
            filesystem[sizeof(filesystem) - 1] = '\0';
            flags[sizeof(flags) - 1] = '\0';

            MountedVolume *v =
                &g_mounts_state.volumes[g_mounts_state.volume_count++];
            v->device = strdup(device);
            v->mount_point = strdup(mount_point);
            v->filesystem = strdup(filesystem);
            v->flags = strdup(flags);
        } else {
            printf("matches was %d on <<%.40s>>\n", matches, bufp);
        }

        /* Eat the line.
         */
        while (nbytes > 0 && *bufp != '\n') {
            bufp++;
            nbytes--;
        }
        if (nbytes > 0) {
            bufp++;
            nbytes--;
        }
    }

    return 0;

bail:
//TODO: free the strings we've allocated.
    g_mounts_state.volume_count = 0;
    return -1;
}
2.8.2.3 find_mounted_volume_by_mount_point

``find_mounted_volume_by_mount_point函数定义在mounts.c文件,实用于从已经挂载的文件系统列表中查找挂载点为mount_point`的文件系统;

const MountedVolume *
find_mounted_volume_by_mount_point(const char *mount_point)
{
    // 真实已经挂载的文件系统列表
    if (g_mounts_state.volumes != NULL) {
        int i;
        // 遍历每一个挂载的文件系统,查找挂载点为mount_point的文件系统
        for (i = 0; i < g_mounts_state.volume_count; i++) {
            MountedVolume *v = &g_mounts_state.volumes[i];
            /* May be null if it was unmounted and we haven't rescanned.
             */
            if (v->mount_point != NULL) {
                if (strcmp(v->mount_point, mount_point) == 0) {
                    return v;
                }
            }
        }
    }
    return NULL;
}
2.8.2.4 unmount_mounted_volume

unmount_mounted_volume函数定义在mounts.c文件,用于取消文件系统volume的挂载;

int
unmount_mounted_volume(const MountedVolume *volume)
{
    /* Intentionally pass NULL to umount if the caller tries
     * to unmount a volume they already unmounted using this
     * function.
     * 等价于执行命令 umount <挂载点或设备> 
     */
    int ret = umount(volume->mount_point);
    if (ret == 0) {
        // 释放volume成员
        free_volume_internals(volume, 1);
        return 0;
    }
    return ret;
}
2.8.3 ensure_path_mounted

ensure_path_mounted函数定义在roots.c文件中,该函数用于确保路径path被挂载。这里有一点需要注意:

  • 如果输入path="/oem",则判定的实际挂载点mount_point="/oem"
  • 如果输入path="/userdata/update.img,则判定的实际挂载点mount_point="/userdata"
  • 如果输入path="/userdata/recovery/last_log,则判定的实际挂载点mount_point="/userdata/recovery"

这里实际传入的参数为/userdata/update.img

int ensure_path_mounted(const char* path)
{
    if (access(path, F_OK) == 0)
        return 0;

    // 检测/etc/fstab文件中配置的若干文件系统挂载点是否包含了path这个路,如果包含,则返回匹配的文件系统挂载信息
    // 同时设置mount_point="/userdata"
    Volume* v = volume_for_path(path);
    // 如果找不到匹配项,返回-1; 在使用NanoPC-T4开发板进行测试时走到这一步就会返回
    if (v == NULL) {
        if (mount_point[0] != 0) {
            // 读取/proc/mount文件获取当前系统上真实挂载的所有文件系统的信息;
            int result = scan_mounted_volumes();
            if (result < 0) {
                LOGE("failed to scan mounted volumes\n");
                return -1;
            }

            // 从已经挂载的文件系统列表中查找挂载点为path的文件系统
            // 由于我们在/etc/inittab配置了设备/dev/userdata的挂载点为/userdata,因此这里可以匹配mount_point="/userdata"
            const MountedVolume* mv =
                find_mounted_volume_by_mount_point(mount_point);
            if (mv) {
                // volume is already mounted
                LOGI("%s already mounted \n", path);
                return 0;
            }
        }

        LOGE("unknown volume for path [%s]\n", path);
        return -1;
    }
    
    // 如果文件系统类型为ramdisk,则返回0
    if (strcmp(v->fs_type, "ramdisk") == 0) {
        // the ramdisk is always mounted.
        return 0;
    }

    int result;
    
    // 读取/proc/mount文件获取当前系统上真实挂载的所有文件系统的信息
    result = scan_mounted_volumes();
    if (result < 0) {
        LOGE("failed to scan mounted volumes\n");
        return -1;
    }

    // 从已经挂载的文件系统列表中查找挂载点为path的文件系统
    const MountedVolume* mv =
        find_mounted_volume_by_mount_point(v->mount_point);
    if (mv) {
        // volume is already mounted
        return 0;
    }

    mkdir(v->mount_point, 0755);  // in case it doesn't already exist

    // /etc/fstab中查找的匹配项v,其描述的文件系统未挂载,则进行挂载操作
    if (strcmp(v->fs_type, "yaffs2") == 0) {
        ......
        return mtd_mount_partition(partition, v->mount_point, v->fs_type, 0);
    } else if (strcmp(v->fs_type, "ext4") == 0 ||
               strcmp(v->fs_type, "vfat") == 0 ||
               strcmp(v->fs_type, "ext2") == 0) {
        char *blk_device;
        blk_device = (char*)v->device;
        if (strcmp("/mnt/sdcard", v->mount_point) == 0) {
            blk_device = getenv(SD_POINT_NAME);
            if (blk_device == NULL) {
                setFlashPoint();
                blk_device = getenv(SD_POINT_NAME);
            }
            result = mount(blk_device, v->mount_point, v->fs_type,
                           MS_NOATIME | MS_NODEV | MS_NODIRATIME, "");
            if (result == 0) return 0;
        }
        // 进行挂载操作
        result = mount(v->device, v->mount_point, v->fs_type,
                       MS_NOATIME | MS_NODEV | MS_NODIRATIME, "");
        if (result == 0) return 0;

        if (v->device2) {
            LOGW("failed to mount %s (%s); trying %s\n",
                 v->device, strerror(errno), v->device2);
            result = mount(v->device2, v->mount_point, v->fs_type,
                           MS_NOATIME | MS_NODEV | MS_NODIRATIME, "");
            if (result == 0) return 0;
        }
        LOGE("failed to mount %s (%s)\n", v->mount_point, strerror(errno));
        return -1;
    }

    LOGE("unknown fs_type \"%s\" for %s\n", v->fs_type, v->mount_point);
    return -1;
}

2.9 updateEngine 升级分区

接着使用/usr/bin/updateEngine升级剩余分区;

const char* updateEnginebin = "/usr/bin/updateEngine";
status = do_rk_updateEngine(updateEnginebin, usbupdate_package);

函数do_rk_updateEngine定义在rkupdate.c文件中:

int do_rk_updateEngine(const char *binary, const char *path)
{
    LOGI("[%s] start with main.\n", __func__);
    char *update = "--update";
    char *update_sdboot = "--update=sdboot";
    int pipefd[2];
    if (pipe(pipefd) == -1) {
        LOGE("pipe error");
    }

    //updateEngine --update --image_url=path --partition=0x3a0000
    char *args[6];
    char args_1[20] = {0};
    char args_2[URL_MAX_LENGTH] = {0};
    char args_4[32] = {0};
    args[0] = (char* )binary;
    args[1] = args_1;
    sprintf(args[1], "--pipefd=%d", pipefd[1]);
    args[2] = args_2;
    sprintf(args[2], "--image_url=%s", path);

    if (bSDBootUpdate) {
        char path_second[64] = {0};
        char path_second_dir[64] = {0};

        sprintf(path_second_dir, "%s", path);
        dirname(path_second_dir);

        sprintf(path_second, "%s/update_ab.img", path_second_dir);
        if (access(path_second, F_OK) == 0) {
            sprintf(args[2], "--image_url=%s", path_second);
        } else {
            sprintf(path_second, "%s/update_ota.img", path_second_dir);
            if (access(path_second, F_OK) == 0) {
                sprintf(args[2], "--image_url=%s", path_second);
            }
        }
    }
    LOGI("SDcard update data: [%s]\n", args[2]);

    if (bSDBootUpdate) {
        args[3] = (char *)update_sdboot;
    } else {
        args[3] = (char *)update;
    }

    args[4] = args_4;
    args[5] = NULL;

    if (bSDBootUpdate) {
        // If SD boot, ignore --partition
        sprintf(args[4], "--partition=0x%s", "FFFF00");
    } else {
        struct bootloader_message boot;
        get_bootloader_message(&boot);
        sprintf(args[4], "--partition=0x%X", *((int *)(boot.needupdate)));
    }

    return start_main(binary, args, pipefd);

}

2.10 finish_recovery

finish_recovery主要进行一些收尾工作,比如:

  • 保存升级日志到/userdata/recovery/log/userdata/recovery/last_log

  • 清理misc分区偏移16k位置数据,使得下次启动进入normal系统;

比如我们在使用NanoPC-T4开发板进行测试的时候,输出如下信息:

LOG_INFO: finish_recovery Enter.....
# ensure_path_mounted("/userdata/recovery/log"),检测挂载点/userdata/recovery失败
LOG_ERROR: unknown volume for path [/userdata/recovery/log]
LOG_ERROR: Can't mount /userdata/recovery/log
LOG_ERROR: Can't open /userdata/recovery/log
# ensure_path_mounted("/userdata/recovery/last_log"),检测挂载点/userdata/recovery失败
LOG_ERROR: unknown volume for path [/userdata/recovery/last_log]
LOG_ERROR: Can't mount /userdata/recovery/last_log
LOG_ERROR: Can't open /userdata/recovery/last_log
LOG_INFO: devices is not MTD.
# ensure_path_mounted("/userdata/recovery/command"),检测挂载点/userdata/recovery失败
LOG_ERROR: unknown volume for path [/userdata/recovery/command]
LOG_WARN: Can't unlink /userdata/recovery/command
LOG_INFO: recovery usage time:71632 ms

源码如下:

// clear the recovery command and prepare to boot a (hopefully working) system,
// copy our log file to cache as well (for the system to read), and
// record any intent we were asked to communicate back to the system.
// this function is idempotent: call it as many times as you like.
static void
finish_recovery(const char *send_intent) // 参数为null
{
    // By this point, we're ready to return to the main system...
    // 不会进入
    if (send_intent != NULL) {
        // 打开/userdata/recovery/intent文件
        FILE *fp = fopen_path(INTENT_FILE, "w");
        if (fp == NULL) {
            LOGE("Can't open %s\n", INTENT_FILE);
        } else {
            fputs(send_intent, fp);
            check_and_fclose(fp, INTENT_FILE);
        }
    }

    LOGI("finish_recovery Enter.....\n");

    // Copy logs to cache so the system can find out what happened.
    // /userdata/recovery/log
    // 将临时日志文件/tmp/recovery.log的内容追加到/userdata/recovery/log
    copy_log_file(LOG_FILE, true);
    // 将临时日志文件/tmp/recovery.log的内容写到/userdata/recovery/last_log(覆盖写入)
    copy_log_file(LAST_LOG_FILE, false);
    // 修改文件权限
    chmod(LAST_LOG_FILE, 0640);

    // Reset to mormal system boot so recovery won't cycle indefinitely.
    // 如果升级成功,清空misc分区偏移16k位置数据,使得下次启动进入normal系统
    struct bootloader_message boot;
    memset(&boot, 0, sizeof(boot));
    strlcpy(boot.systemFlag, systemFlag, sizeof(boot.systemFlag));
    set_bootloader_message(&boot);

    // Remove the command file, so recovery won't repeat indefinitely.
    // path传入/userdata/recovery/command,则判定/userdata/recovery路径是否被挂在,很显然这里会返回-1
    if (ensure_path_mounted(COMMAND_FILE) != 0 ||
        (unlink(COMMAND_FILE) && errno != ENOENT)) {
        LOGW("Can't unlink %s\n", COMMAND_FILE);
    }

    sync();  // For good measure.
}
2.10.1 copy_log_file
// 将临时日志文件/tmp/recovery.log的内容追加/覆盖写入到指定的目标文件中
static void copy_log_file(const char* destination, int append)
{
    // 首先检测路径destination是否被挂载,只有被挂载了才会打开文件 
    FILE *log = fopen_path(destination, append ? "a" : "w");
    // 在使用NanoPC-T4开发板进行测试时走到这一步就会返回
    if (log == NULL) {
        LOGE("Can't open %s\n", destination);
    } else {
        // /tmp/recovery.log
        FILE *tmplog = fopen(TEMPORARY_LOG_FILE, "r");
        if (tmplog == NULL) {
            LOGE("Can't open %s\n", TEMPORARY_LOG_FILE);
        } else {
            // 如果是追加模式,则使用 fseek 将读取位置移动到tmplog_offset,确保从tmplog_offset位置开始复制
            if (append) {
                fseek(tmplog, tmplog_offset, SEEK_SET);  // Since last write
            }
            char buf[4096];
            while (fgets(buf, sizeof(buf), tmplog)) fputs(buf, log);
            if (append) {
                tmplog_offset = ftell(tmplog);
            }
            check_and_fclose(tmplog, TEMPORARY_LOG_FILE);
        }
        check_and_fclose(log, destination);
    }
}
2.10.2 fopen_path

这里首先检测路径path是否被挂载,只有被挂载了才会打开文件;

// open a given path, mounting partitions as necessary
static FILE*
fopen_path(const char *path, const char *mode)
{
    // 确保路径path被挂载
    // 方法第1次调用:path传入/userdata/recovery/log,则判定/userdata/recovery路径是否被挂在,很显然这里会返回-1
    // 方法第2次调用:path传入/userdata/recovery/last_log,则判定/userdata/recovery路径是否被挂在,很显然这里会返回-1
    if (ensure_path_mounted(path) != 0) {
        LOGE("Can't mount %s\n", path);
        return NULL;
    }

    // When writing, try to create the containing directory, if necessary.
    // Use generous permissions, the system (init.rc) will reset them.
    if (strchr("wa", mode[0])) dirCreateHierarchy(path, 0777, NULL, 1);

    FILE *fp = fopen(path, mode);
    return fp;
}
2.10.3 check_and_fclose
// close a file, log an error if the error indicator is set
static void
check_and_fclose(FILE *fp, const char *name)
{
    fflush(fp);
    if (ferror(fp)) LOGE("Error in %s\n(%s)\n", name, strerror(errno));
    fclose(fp);
}

参考文章:

[1] Rockchip RK3588 - Rockchip Linux Recovery updateEngine源码分析

[2] Rockchip RK3588 - Rockchip Linux Recovery updateEngine测试

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