通过自定义升级程序,更直观的理解ota升级原理。
一、模拟计算hash,验证签名,判断激活分区,并通过dd命令,写入对应分区
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <openssl/sha.h>
#include <json-c/json.h>// 1. 检测当前激活分区(A或B)
char get_active_slot() {FILE *fp = fopen("/proc/mounts", "r");if (!fp) return 'A'; // 默认A分区char line[256];while (fgets(line, sizeof(line), fp)) {if (strstr(line, "system_a")) {fclose(fp);return 'A';}if (strstr(line, "system_b")) {fclose(fp);return 'B';}}fclose(fp);return 'A';
}// 2. 计算文件SHA256哈希
void calculate_sha256(const char *file_path, char *sha_str) {unsigned char sha_hash[SHA256_DIGEST_LENGTH];SHA256_CTX sha_ctx;SHA256_Init(&sha_ctx);FILE *fp = fopen(file_path, "rb");if (!fp) {strcpy(sha_str, "");return;}unsigned char buffer[4096];size_t bytes_read;while ((bytes_read = fread(buffer, 1, sizeof(buffer), fp)) > 0) {SHA256_Update(&sha_ctx, buffer, bytes_read);}fclose(fp);SHA256_Final(sha_hash, &sha_ctx);// 转换为字符串for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {sprintf(sha_str + (i * 2), "%02x", sha_hash[i]);}sha_str[SHA256_DIGEST_LENGTH * 2] = '\0';
}// 3. 验证升级包签名(简化示例)
int verify_signature(const char *manifest_path) {// 实际应使用OpenSSL验证签名,此处简化为返回成功printf("验证签名: 成功\n");return 0;
}// 4. 写入分区
int write_partition(const char *img_path, const char *part_name) {char cmd[256];snprintf(cmd, sizeof(cmd), "dd if=%s of=/dev/disk/by-name/%s bs=4M status=progress", img_path, part_name);printf("执行命令: %s\n", cmd);return system(cmd);
}// 5. 设置misc分区标志
int set_misc_flag(const char *flag) {int fd = open("/dev/disk/by-name/misc", O_WRONLY);if (fd < 0) {perror("打开misc分区失败");return -1;}ssize_t bytes_written = write(fd, flag, strlen(flag));close(fd);if (bytes_written != strlen(flag)) {perror("写入misc标志失败");return -1;}printf("写入misc标志: %s\n", flag);return 0;
}// 主函数:OTA升级流程
int main(int argc, char *argv[]) {if (argc != 2) {fprintf(stderr, "用法: %s <ota_package_url>\n", argv[0]);return 1;}const char *ota_url = argv[1];// 步骤1:检测当前激活分区char current_slot = get_active_slot();char target_slot = (current_slot == 'A') ? 'B' : 'A';printf("当前分区: %c, 目标分区: %c\n", current_slot, target_slot);// 步骤2:下载OTA包(简化为本地文件)printf("下载升级包: %s\n", ota_url);// 实际应使用libcurl等工具下载,此处假设已下载到本地const char *ota_package = "ota_package.zip";system("unzip -q ota_package.zip -d ota_temp"); // 解压到临时目录// 步骤3:解析manifest并验证struct json_object *manifest = json_object_from_file("ota_temp/manifest.json");if (!manifest) {fprintf(stderr, "解析manifest失败\n");return 1;}// 验证签名if (verify_signature("ota_temp/manifest.json") != 0) {fprintf(stderr, "签名验证失败\n");return 1;}// 步骤4:写入目标分区struct json_object *partitions;json_object_object_get_ex(manifest, "partitions", &partitions);int n = json_object_array_length(partitions);for (int i = 0; i < n; i++) {struct json_object *part = json_object_array_get_idx(partitions, i);const char *name = json_object_get_string(json_object_object_get(part, "name"));const char *img = json_object_get_string(json_object_object_get(part, "image"));const char *expected_sha = json_object_get_string(json_object_object_get(part, "sha256"));// 拼接目标分区名(如boot -> boot_b)char target_part[32];snprintf(target_part, sizeof(target_part), "%s_%c", name, target_slot);// 校验镜像哈希char actual_sha[65];char img_path[64];snprintf(img_path, sizeof(img_path), "ota_temp/%s", img);calculate_sha256(img_path, actual_sha);if (strcmp(actual_sha, expected_sha) != 0) {fprintf(stderr, "%s 哈希校验失败\n", img);return 1;}// 写入分区if (write_partition(img_path, target_part) != 0) {fprintf(stderr, "写入%s失败\n", target_part);return 1;}}// 步骤5:设置启动标志,重启char misc_flag[128];snprintf(misc_flag, sizeof(misc_flag), "target_slot=%c\nboot_retry_count=3\n", target_slot);if (set_misc_flag(misc_flag) != 0) {fprintf(stderr, "设置misc标志失败\n");return 1;}printf("升级准备完成,重启中...\n");system("reboot");return 0;
}
二、rk3568上开启ota,ab分区
将每个分区的hash写入json,打包进入下载包。
写一个ota下载,验证进程。
通过fuse里或某个只读分区,读出公钥的哈希,计算升级包里的公钥哈希并与前面的哈希对比
验证公钥的安全,然后对镜像包的哈希进行解密,获得哈希,并计算升级包的镜像分区哈希,进行对比
通过则升级。
写入完后,还需要读写出来,进行比较,失败则重新刷写3次为止。
这里还需要做一个超时判断,若超时则失败,回滚到a分区,
断电保护:写入分区时建议分块写入并记录日志,断电电后可从断点续传;
四、参考demo
build_ota_package.sh,打包升级包的脚本,打包为zip
#!/bin/bash
# 生成OTA升级包(包含镜像、manifest.json和签名)# 配置
OTA_DIR="ota_temp"
OTA_PACKAGE="ota_v1.1.zip"
TARGET_SLOT="B" # 目标分区(实际应根据当前版本动态设置)# 创建临时目录
rm -rf $OTA_DIR
mkdir -p $OTA_DIR# 复制镜像文件(假设已编译好)
cp ../build/boot.img $OTA_DIR/
cp ../build/system.img $OTA_DIR/
cp ../build/vendor.img $OTA_DIR/# 生成manifest.json
cat > $OTA_DIR/manifest.json << EOF
{"version": "v1.1","target_slot": "$TARGET_SLOT","partitions": [{"name": "boot","image": "boot.img","sha256": "$(sha256sum $OTA_DIR/boot.img | awk '{print $1}')"},{"name": "system","image": "system.img","sha256": "$(sha256sum $OTA_DIR/system.img | awk '{print $1}')"},{"name": "vendor","image": "vendor.img","sha256": "$(sha256sum $OTA_DIR/vendor.img | awk '{print $1}')"}],"signature": "dummy_signature" # 实际应替换为RSA签名
}
EOF# 生成签名(示例:使用openssl生成RSA签名)
# openssl dgst -sha256 -sign private.key -out $OTA_DIR/manifest.sig $OTA_DIR/manifest.json# 打包为OTA包
cd $OTA_DIR && zip -r ../$OTA_PACKAGE ./* && cd ..echo "OTA包生成完成: $OTA_PACKAGE"
rm -rf $OTA_DIR
ota_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <openssl/sha.h>
#include <json-c/json.h>// 1. 检测当前激活分区(A或B)
char get_active_slot() {FILE *fp = fopen("/proc/mounts", "r");if (!fp) return 'A'; // 默认A分区char line[256];while (fgets(line, sizeof(line), fp)) {if (strstr(line, "system_a")) {fclose(fp);return 'A';}if (strstr(line, "system_b")) {fclose(fp);return 'B';}}fclose(fp);return 'A';
}// 2. 计算文件SHA256哈希
void calculate_sha256(const char *file_path, char *sha_str) {unsigned char sha_hash[SHA256_DIGEST_LENGTH];SHA256_CTX sha_ctx;SHA256_Init(&sha_ctx);FILE *fp = fopen(file_path, "rb");if (!fp) {strcpy(sha_str, "");return;}unsigned char buffer[4096];size_t bytes_read;while ((bytes_read = fread(buffer, 1, sizeof(buffer), fp)) > 0) {SHA256_Update(&sha_ctx, buffer, bytes_read);}fclose(fp);SHA256_Final(sha_hash, &sha_ctx);// 转换为字符串for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {sprintf(sha_str + (i * 2), "%02x", sha_hash[i]);}sha_str[SHA256_DIGEST_LENGTH * 2] = '\0';
}// 3. 验证升级包签名(简化示例)
int verify_signature(const char *manifest_path) {// 实际应使用OpenSSL验证签名,此处简化为返回成功printf("验证签名: 成功\n");return 0;
}// 4. 写入分区
int write_partition(const char *img_path, const char *part_name) {char cmd[256];snprintf(cmd, sizeof(cmd), "dd if=%s of=/dev/disk/by-name/%s bs=4M status=progress", img_path, part_name);printf("执行命令: %s\n", cmd);return system(cmd);
}// 5. 设置misc分区标志
int set_misc_flag(const char *flag) {int fd = open("/dev/disk/by-name/misc", O_WRONLY);if (fd < 0) {perror("打开misc分区失败");return -1;}ssize_t bytes_written = write(fd, flag, strlen(flag));close(fd);if (bytes_written != strlen(flag)) {perror("写入misc标志失败");return -1;}printf("写入misc标志: %s\n", flag);return 0;
}// 主函数:OTA升级流程
int main(int argc, char *argv[]) {if (argc != 2) {fprintf(stderr, "用法: %s <ota_package_url>\n", argv[0]);return 1;}const char *ota_url = argv[1];// 步骤1:检测当前激活分区char current_slot = get_active_slot();char target_slot = (current_slot == 'A') ? 'B' : 'A';printf("当前分区: %c, 目标分区: %c\n", current_slot, target_slot);// 步骤2:下载OTA包(简化为本地文件)printf("下载升级包: %s\n", ota_url);// 实际应使用libcurl等工具下载,此处假设已下载到本地const char *ota_package = "ota_package.zip";system("unzip -q ota_package.zip -d ota_temp"); // 解压到临时目录// 步骤3:解析manifest并验证struct json_object *manifest = json_object_from_file("ota_temp/manifest.json");if (!manifest) {fprintf(stderr, "解析manifest失败\n");return 1;}// 验证签名if (verify_signature("ota_temp/manifest.json") != 0) {fprintf(stderr, "签名验证失败\n");return 1;}// 步骤4:写入目标分区struct json_object *partitions;json_object_object_get_ex(manifest, "partitions", &partitions);int n = json_object_array_length(partitions);for (int i = 0; i < n; i++) {struct json_object *part = json_object_array_get_idx(partitions, i);const char *name = json_object_get_string(json_object_object_get(part, "name"));const char *img = json_object_get_string(json_object_object_get(part, "image"));const char *expected_sha = json_object_get_string(json_object_object_get(part, "sha256"));// 拼接目标分区名(如boot -> boot_b)char target_part[32];snprintf(target_part, sizeof(target_part), "%s_%c", name, target_slot);// 校验镜像哈希char actual_sha[65];char img_path[64];snprintf(img_path, sizeof(img_path), "ota_temp/%s", img);calculate_sha256(img_path, actual_sha);if (strcmp(actual_sha, expected_sha) != 0) {fprintf(stderr, "%s 哈希校验失败\n", img);return 1;}// 写入分区if (write_partition(img_path, target_part) != 0) {fprintf(stderr, "写入%s失败\n", target_part);return 1;}}// 步骤5:设置启动标志,重启char misc_flag[128];snprintf(misc_flag, sizeof(misc_flag), "target_slot=%c\nboot_retry_count=3\n", target_slot);if (set_misc_flag(misc_flag) != 0) {fprintf(stderr, "设置misc标志失败\n");return 1;}printf("升级准备完成,重启中...\n");system("reboot");return 0;
}
verify_new_system.c,重启后进行校验是否能正常启动。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>// 检查关键服务是否启动
int check_critical_services() {// 检查sshd服务if (system("pgrep sshd > /dev/null 2>&1") != 0) {printf("sshd服务未启动\n");return -1;}// 检查应用程序if (system("pgrep my_app > /dev/null 2>&1") != 0) {printf("应用程序my_app未启动\n");return -1;}return 0;
}// 设置misc分区标志
int set_misc_flag(const char *flag) {int fd = open("/dev/disk/by-name/misc", O_WRONLY);if (fd < 0) {perror("打开misc分区失败");return -1;}ssize_t bytes_written = write(fd, flag, strlen(flag));close(fd);return (bytes_written == strlen(flag)) ? 0 : -1;
}// 检测当前启动的分区
char get_current_slot() {FILE *fp = fopen("/proc/mounts", "r");if (!fp) return 'A';char line[256];while (fgets(line, sizeof(line), fp)) {if (strstr(line, "system_a")) {fclose(fp);return 'A';}if (strstr(line, "system_b")) {fclose(fp);return 'B';}}fclose(fp);return 'A';
}int main() {char current_slot = get_current_slot();printf("当前启动分区: %c,开始验证...\n", current_slot);// 验证系统状态if (check_critical_services() != 0) {printf("系统验证失败,准备回滚\n");char flag[64];snprintf(flag, sizeof(flag), "boot_failed=%c\n", current_slot);set_misc_flag(flag);system("reboot"); // 重启后U-Boot会回滚return 1;}// 验证成功,标记分区为稳定printf("系统验证成功,更新分区状态\n");char flag[128];snprintf(flag, sizeof(flag), "slot_successful=%c\ntarget_slot=%c\n", current_slot, current_slot);set_misc_flag(flag);return 0;
}
有了ab分区,在uboot启动时,运行一段时间后,如出现崩溃,可以在uboot失败多次,尝试回滚到a区
总结,rk和nvidia的方案,是有些区别的,nvidia是可以更新uboot,kernel,system,因为nvidia把uboot分出去了
到cboot,cboot代替uboot。rk的芯片是没有cboot的,所以uboot是不能更新的。