3、对称加密算法移植(AES):AES算法原理回顾、C语言源码适配、内存对齐优化
好,咱们直接进入正题。AES这个算法,做嵌入式加密的兄弟应该都不陌生。但说实话,很多人只是调个库,真到了要移植到自家芯片上,各种坑就冒出来了。
我个人习惯,拿到一个新平台,第一件事不是写代码,而是先把算法原理再过一遍。为什么?因为只有理解了算法在干什么,你才知道怎么优化它。
3.1 AES算法原理回顾——别嫌啰嗦,这是基础
AES,全称Advanced Encryption Standard,说白了就是一套固定的数学变换。它把128位的数据(16字节)通过多轮操作变成密文。密钥长度可以是128、192或256位,轮数对应是10、12、14轮。
每一轮干四件事:
- SubBytes(字节代换):用一个S盒把每个字节替换掉。这个S盒是固定的,查表就行。
- ShiftRows(行移位):把状态矩阵的每一行循环左移。第一行不动,第二行移1个字节,第三行移2个,第四行移3个。
- MixColumns(列混合):对每一列做矩阵乘法。这一步计算量最大,也是优化的重点。
- AddRoundKey(轮密钥加):把当前状态和轮密钥做异或。简单,但必须做。
最后一轮没有MixColumns。嗯,这个细节很重要,写代码时别搞错了。
核心要点:AES的加解密流程是对称的,但解密时SubBytes、ShiftRows、MixColumns都要用逆操作。如果你只移植加密,那还好;如果加解密都要,工作量翻倍。
3.2 C语言源码适配——从标准库到嵌入式
网上能找到的AES源码很多,OpenSSL、mbedTLS、或者一些独立实现。但直接拿来用?我劝你三思。
我在项目中遇到过一个问题:从PC上移植一套AES代码到STM32上,编译通过,跑起来也正常。但一上量,发现某些批次芯片加密结果不对。查了两天,最后发现是memcpy在部分优化等级下行为异常。嵌入式环境下的标准库函数,有时候并不靠谱。
所以我的建议是:尽量自己实现关键操作,或者用芯片厂商提供的HAL库。
下面是一段精简的AES-128加密核心代码,我做了适配:
// AES-128 加密单块(16字节)
void aes_encrypt_block(uint8_t *state, const uint8_t *round_keys) {
uint8_t i;
// 初始轮密钥加
add_round_key(state, round_keys);
// 前9轮
for (i = 1; i < 10; i++) {
sub_bytes(state);
shift_rows(state);
mix_columns(state);
add_round_key(state, round_keys + i * 16);
}
// 最后一轮(无MixColumns)
sub_bytes(state);
shift_rows(state);
add_round_key(state, round_keys + 10 * 16);
}
你看,逻辑很清晰。但这里有个坑:sub_bytes和mix_columns的实现方式,直接影响性能。
我的经验:如果芯片有硬件CRC或DMA,可以考虑用它们辅助计算。但别过度依赖,毕竟不是所有平台都支持。
3.3 内存对齐优化——性能提升的关键
你想想看,嵌入式芯片的CPU访问内存,是按字(4字节)对齐的。如果数据没对齐,轻则多花几个时钟周期,重则直接触发异常。
我曾经在一个Cortex-M4项目上,AES加密一次要2.3ms。后来发现状态数组没做对齐,改完之后直接降到1.1ms。翻了一倍多!
怎么做?两个方法:
- 使用对齐属性:在定义数组时加上
__attribute__((aligned(4)))或__align(4)。 - 手动填充:如果编译器不支持,就自己用
uint32_t指针操作。
看代码:
// 方法1:使用GCC对齐属性
uint8_t state[16] __attribute__((aligned(4)));
// 方法2:使用uint32_t数组,天然对齐
uint32_t state_aligned[4]; // 16字节,4字节对齐
// 访问时强转
#define STATE_BYTE(i) ((uint8_t*)state_aligned)[i]
我个人更推荐方法2。为什么?因为mix_columns操作本质上是32位运算,用uint32_t数组可以直接操作,省去类型转换的开销。
注意:对齐优化后,一定要检查字节序(Endianness)。AES标准是大端模式,但很多嵌入式芯片是小端。我见过有人优化后加密结果不对,最后发现是字节序搞反了。
3.4 查表优化——空间换时间
如果你芯片的Flash够大,可以用查表法代替mix_columns的矩阵乘法。标准做法是预计算4个T表(Te0-Te3),每个表256个32位值。
这样一轮加密就变成了4次查表加4次异或,速度飞快。但代价是4KB的Flash占用。
// T表查表法示例(简化)
uint32_t t0 = Te0[state[0]] ^ Te1[state[5]] ^ Te2[state[10]] ^ Te3[state[15]];
// ... 类似处理其他列
嗯,这里要注意:T表是跟密钥长度相关的。如果你要支持AES-192和AES-256,需要多套表。我一般只做AES-128的查表优化,够用就行。
3.5 避坑指南——我踩过的雷
- 密钥扩展的时序:密钥扩展可以在初始化时做一次,然后存起来。别每次加密都重新算,那太傻了。
- 中断安全:如果加密过程被中断打断,状态数组可能被破坏。建议在加密前后关中断,或者用局部变量。
- 调试输出:我习惯在每轮结束后打印中间状态,跟标准向量对比。这样能快速定位哪一步出了问题。
总结一下:AES移植不难,但优化需要细心。内存对齐、查表、字节序,这三个点抓住了,性能就能上去。别贪多,先把128位版本跑通,再考虑扩展。
好,这一章就到这里。下一章我们会讲RSA算法的移植,那个更刺激,因为涉及大数运算。到时候咱们再聊。