背景:有一个在 U-Boot 阶段做系统全量自更新的需求,要制作系统的全量固件包(U-Boot.img、kerlen.img、rootfs.img)。大体分为三个主要部分:U-Boot-shell 脚本编写、打包各镜像为一个固件包、固件包的加密和解密
一、U-Boot-shell 脚本
1. 一些概念
既然是在uboot阶段进行全量更新,那么更新的业务逻辑脚本自然要能在uboot下运行,就得编写uboot-shell script(一般我们提起shell脚本就想到是linux下的shell脚本),这里有一些概念需要厘清:shell指命令解释器,而shell有很多种,linux下有自己的shell,最常用的就是我们所熟知的bash,那自然uboot也有自己的命令解释器,一般有两种。
U-Boot 主要使用以下两个 shell 解释器:
1.hush shell
hush 是U-Boot中默认的命令解释器,他支持基本的脚本功能,如变量、条件语句、循环等。
hush 提供了比传统的简单命令行解释器更强大的功能,使得编写复杂启动脚本成为可能
在U-Boot 源码中,hush的实现位于common目录下的cli_hush.c
2.minimal commmand line interpreter(CLI)
这是一个非常基础的命令解释器,仅提供最基本的命令解释功能,不具有复杂脚本的功能,在uboot 源码中位于common目录下的cli.c文件中
一般 U-Boot 默认使用第二种简单的 shell 解释器
2. 编写一个uboot脚本
以上概念理解清楚后就可以编写自己的U-Boot-shell 脚本了,比较简单,就像平时在uboot控制台输入uboot命令一样,只不过把这些命令依次写到一个文件中即可,在文件中写入要执行的业务逻辑(核心就是要把固件包中的哪一段写入到flash分区的哪一个分区),这里我们创建一个文件updata.cmd,文件内容如下:
#设置你的分区信息,视自己情况而定
setenv mtdids nand0=nand
setenv mtdparts 'mtdparts=nand:1024k@0(boot0)ro,3072k@1048576(uboot)ro,1024k@4194304(secure_storage)ro,-(sys)'
saveenv#解密各镜像,如果有加密的话
decode uboot.img
decode kernel.img
decode rootfs.img
#将解密好的各镜像写入到对应分区,写入的方法和地址addr视自己情况而定
flash write addr1 uboot size
flash write addr2 kernel size
flash write addr3 rootfs size
3. 制作可供uboot执行的脚本——scr
到这里,一个基本的更新逻辑就写完了,那么这个脚本能直接丢到内存中让uboot去运行吗,答案是不可以的,一般我们在linux系统下写的脚本直接通过bash + 文件名就可以执行,但在uboot中,必须要先通过一个工具mkimage,将文件制作为一个scr文件后(其实就是在文件前加了一些头部信息),这个scr文件才能被uboot直接执行。mkimage工具在uboot源码中自带,位于tools目录下。制作scr文件时在linux下输入以下指令即可。
mkimage -A arm -O linux -T script -C none -a 0 -e 0 -n "U-Boot Script" -d script.txt script.scr
-A 指定目标处理器架构类型为 arm。
-O 指定生成的镜像是用于 Linux 系统的
-T 指定镜像类型是script,意为是一个脚本镜像
-a 指定镜像的入口地址,默认为0
-e 指定镜像的实际加载地址,默认为0
-n 指定生成的镜像名称为"U-Boot Script",一个描述性的名称,感觉没什么用
-d 指定源文件
script.txt 原始脚本文件
script.scr 生成的带头部信息的可直接由uboot执行的脚本文件(要的就是这个)
选项的含义可以使用命令 mkimage -h 查看
其实在输入指令时,简单输入以下指令即可,不必指定每一项参数
mkimage -C none -A arm -T script -d updata.cmd updata.scr
当这个scr文件制作好后,就可以将这个文件下载到内存中,uboot控制台输入source + 内存addr即可以执行这个文件。其中source为uboot专门执行脚本的一个指令
二、打包各镜像为一个固件包
核心就一条思想:在linux下,利用 cat 指令将更新逻辑脚本和各个镜像拼接起来即可。
拼接顺序为:updata.scr + uboot.img + kernel.img + rootfs.img
#!/bin/bash
#make uboot script
mkimage -C none -A arm -T script -d ./updata_ubi.cmd ./updata.scr#cp file to current path
cp ~/a8replace/out/a40i_c/p3-spinand-ubi/pack_out/boot_package.fex ./uboot.img
cp ~/a8replace/out/a40i_c/p3-spinand-ubi/buildroot/boot.img ./kernel.img
cp ~/a8replace/out/a40i_c/p3-spinand-ubi/buildroot/rootfs.ubifs ./rootfs.ubifs#encode image
此处省略#bytes alignment
script_size=$(stat --format=%s updata.scr)
remainder=$((script_size % 1024))
if [ $remainder -ne 0 ]; then
padding=$((1024 - remainder))
dd if=/dev/zero bs=1 count=$padding >> updata.scr
fi#make firmware
cat updata.scr uboot_encode.img kernel_encode.img rootfs_encode.ubifs > allwinner_updata_packet.bin
注意:这里有一个坑,在进行拼接时,要考虑到字节对齐问题,如若不然,内存中镜像写入flash时可能导致uboot异常或镜像写入后无法启动。因为ARM架构下默认是打开字节对齐优化的,因为这可以提高内存的访问效率。在 U-Boot 的 start.s 启动文件中我们可以看到:
这里的 orr r0, r0, #0x00000002 @ set bit 1 (--A-) Align 就是打开字节对齐的开关,可以看到默认是打开的。
如果要关闭的话更改此行为 bic r0, r0, #0x00000002 @ 清除 bit 1 (--A-) 关闭字节对齐优化
那么我们是关闭字节优化吗,当然不是,这虽然解决了内存中镜像写入flash时可能导致uboot异常或镜像写入后无法启动的问题,但这是以损失系统运行效率为代价的,绝不推荐这样做,那怎么避免这个问题呢,答案是我们在制作固件包时就对齐好地址。一般来说更新逻辑脚本(updata.scr)后紧接的是第一个镜像(uboot.img),字节对齐就是要求uboot.img的起始地址为整数地址,如42000800等类似的地址,我这里是按照1024字节对齐的,所以我在脚本中把updata.scr后补0,使其为1024字节的整数倍。之后再紧跟uboot.img。
三、固件包的加密和解密
1. 是否要加密
固件包是否要加密,这取决于你的安全需求,我这里的固件包是要加密的。既然要加密,首先就要考虑用哪种方式去做加密,是对称加密还是非对称加密、是整个固件加密还是只加密某一段。这取决于你的具体需求。同时还应考虑到加密算法的选取,如果加密强度太大,则下位机解密花费的代价太大。总之一句话,要综合自己的需求选择合适的加密算法和加密方式。
2.TEA加密
我这里采用的是对称加密算法中的TEA加密算法,简单、高效,适用于嵌入式类的资源受限场景。TEA算法全称是Tiny Encryption Algorithm由剑桥大学计算机实验室的David Wheeler和Roger Needham于1994年发明。它是对称加密中的一种分组(分块)密码算法,其明文密文块为64比特(8字节),密钥长度为128比特(16字节)。该算法使用了一个神秘常数δ来参与加密,它来源于黄金比率,以保证每一轮加密都不相同。但δ的精确值似乎并不重要,这里取其近似值即可 TEA 把它定义为 δ=「(√5 - 1)/231」(也就是程序中的 0×9E3779B9)。TEA算法利用不断增加的Delta(黄金分割率)近似值作为变化,使得每轮的加密是不同,该加密算法的迭代次数可以改变,建议的迭代次数为32轮
TEA加密的原理:是把输入分成两组,分别是v[0],v[1],绿色方格为做加法,红色圆圈为做异或,可以看出,用密钥k[0],k[1]加密后,把两个数做一次置换,再加密一次,这样经过多轮加密以后就可以通过简单的算法把两个数变得很复杂,满足加密算法混乱和扩散的特性。
以下是采用TEA加密,对一个文件进行加、解密的简单demo。
上位机加密
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
// TEA加密模块
void tea_encrypt(uint32_t *v, uint32_t *k)
{
uint32_t v0 = v[0], v1 = v[1];
uint32_t sum = 0;
uint32_t delta = 0x9e3779b9; // A constant
for (int i = 0; i < 32; i++)
{
sum += delta;
v0 += ((v1 << 4) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1]);
v1 += ((v0 << 4) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3]);
}
v[0] = v0;
v[1] = v1;
}
int main(int argc, char *argv[])
{
if (argc != 4)
{
fprintf(stderr, "Usage: %s <input file> <output file> <key>\n", argv[0]);
return 1;
}
const char *input_file = argv[1];
const char *output_file = argv[2];
const char *key_str = argv[3];
// 读取输入文件
FILE *in = fopen(input_file, "rb");
if (!in)
{
perror("Failed to open input file");
return 1;
}
// 创建输出文件
FILE *out = fopen(output_file, "wb");
if (!out)
{
perror("Failed to open output file");
fclose(in);
return 1;
}
// 将字符串密钥转换为4个32位整数
uint32_t key[4];
sscanf(key_str, "%08X%08X%08X%08X", &key[0], &key[1], &key[2], &key[3]);
// 缓冲区用于存储8字节的数据块
uint32_t data[2];
size_t bytes_read;
while ((bytes_read = fread(data, sizeof(uint32_t), 2, in)) > 0)
{
if (bytes_read == 1)
{
// 如果只读到了4字节,用0填充剩余部分
data[1] = 0;
}
// 加密数据
tea_encrypt(data, key);
// 写入加密后的数据
fwrite(data, sizeof(uint32_t), 2, out);
}
// 关闭文件
fclose(in);
fclose(out);
printf("Encryption completed.\n");
return 0;
}
上位机解密
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
// TEA解密
void tea_decrypt(uint32_t *v, uint32_t *k) {
uint32_t v0 = v[0], v1 = v[1];
uint32_t sum = 0xC6EF3720; // delta * 32
uint32_t delta = 0x9e3779b9;
for (int i = 0; i < 32; i++)
{
v1 -= ((v0 << 4) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3]);
v0 -= ((v1 << 4) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1]);
sum -= delta;
}
v[0] = v0;
v[1] = v1;
}
int main(int argc, char *argv[])
{
if (argc != 4)
{
fprintf(stderr, "Usage: %s <encrypted file> <decrypted file> <key>\n", argv[0]);
return 1;
}
const char *encrypted_file = argv[1];
const char *decrypted_file = argv[2];
const char *key_str = argv[3];
// 读取加密文件
FILE *in = fopen(encrypted_file, "rb");
if (!in)
{
perror("Failed to open encrypted file");
return 1;
}
// 创建解密文件
FILE *out = fopen(decrypted_file, "wb");
if (!out)
{
perror("Failed to open decrypted file");
fclose(in);
return 1;
}
// 将字符串密钥转换为4个32位整数
uint32_t key[4];
sscanf(key_str, "%08X%08X%08X%08X", &key[0], &key[1], &key[2], &key[3]);
// 缓冲区用于存储8字节的数据块
uint32_t data[2];
size_t bytes_read;
while ((bytes_read = fread(data, sizeof(uint32_t), 2, in)) > 0)
{
if (bytes_read == 1)
{
// 如果只读到了4字节,用0填充剩余部分
data[1] = 0;
}
// 解密数据
tea_decrypt(data, key);
// 写入解密后的数据
fwrite(data, sizeof(uint32_t), 2, out);
}
// 关闭文件
fclose(in);
fclose(out);
printf("Decryption completed.\n");
return 0;
}
通过diff指令(比较两个文件内容是否相同)可以看出加密解密是否成功
注意:TEA加密时,待加密数据一定要8是字节的整数倍,否则就要做补零处理,这样在解密时也需要将补的0重新去掉,会带来很多麻烦。推荐只对镜像的一部分做加密,这样不论镜像大小再怎么变,我们加密段的数据大小始终为8字节的整数倍。