☰
8KB RAM单片机上实现AES128:查表法、ECB/CBC与资源取舍
2026/10/26 11:06:37 网站建设 项目流程

简介:AES128 ECB、CBC模式加密解密函数面向单片机与嵌入式开发者,提供一套以C语言编写的对称加密实现参考,可用于资源受限设备的数据保护、通信报文加解密等场景。压缩包共2个文件,包含1个C源码文件和1个头文件,整体约5KB,体量轻巧,源码与接口声明配套,便于阅读、移植或按工程需要裁剪。实现覆盖ECB与CBC两种工作模式,并涉及PKCS7Padding补齐与去填充处理,同时包含密钥扩展、分块加密及解密等核心环节,有助于理解相同明文在不同模式下产生不同密文结果的机制,以及初始向量在CBC中的作用。代码还兼顾嵌入式环境下的资源占用与接口集成,适合作为低资源设备加解密模块的参考实现。已有7335人学习或下载,可用于对照验证模式差异、填充逻辑与基本加解密流程。

1. 一块 8KB RAM 的板子要加密通信:AES128 在单片机上的真实取舍

前阵子给一块 Flash 64KB、RAM 8KB 的板子加通信加密,报文里带设备编号和一组标定参数,串口抓包直接就是明文。现成的加密库塞进来固件就满了,最后用的是一份两百行上下的 AES128 C 实现:密钥 16 字节、分组 16 字节、10 轮迭代,只依赖一张 256 字节的 S 盒常量表,ECB 模式和 CBC 模式各一对读写函数,配 PKCS7Padding 补齐算法把任意长度报文补到 16 的整数倍。

这份资源适合谁?做单片机串口、RS485、LoRa 报文加密的,给 Flash 里的配置参数做混淆存储的,或者要跟网关约定一套轻量加密协议的嵌入式开发者。它不负责密钥协商,也不防重放,ECB 模式下相同明文块还会出相同密文,这些边界在动手前就得想明白,别指望一份加解密函数包打天下。

2. 工程结构与资源账:查表法 AES128 的 Flash/RAM 怎么算

拿到这类嵌入式加密源码,我第一件事不是看算法,是先算账:代码占多少 Flash、常驻多少 RAM、栈峰值多深。AES128 本身不复杂,翻车基本都翻在资源上。

2.1 四层分层:从对外 API 到单条轮函数

整个实现按职责切成四层,调用方向是单向的,自下而上:

最底层是轮函数层,sub_bytes、shift_rows、mix_columns、add_round_key四个函数,加上它们的逆变换,全部只操作一个 16 字节的状态数组。这层是纯算法,跟模式、跟填充、跟你用哪颗芯片都没关系。

往上是分组层,aes_encrypt_block和aes_decrypt_block,一个 16 字节块进、一个 16 字节块出,内部把 10 轮循环走完。这层是对外的原子操作,也是将来接硬件加密外设时唯一要替换的函数。

再往上是模式层,ECB 和 CBC 的封装,负责把任意长度的数据切成 16 字节一块、处理 CBC 的链式异或、维护 IV。这层只调分组层,不碰轮函数。

最上面是填充层,pkcs7_pad和pkcs7_unpad,把「业务报文长度」翻译成「16 的整数倍长度」。业务代码通常只碰模式层和填充层,轮函数一个字都不用改。

这么分层不是学院派洁癖。我之前一个项目后面要从 CBC 换成 CTR 模式,因为分层清楚,只补了一个计数器块的生成函数和两个封装函数,轮函数和填充一行没动,半天就切完了。反过来如果把异或、轮循环、填充全糊在一个函数里,改模式等于重写。

2.2 文件清单与各自职责

一份能直接塞进工程的实现,我一般拆成这么几个文件:

文件作用关键内容
aes128.h对外接口上下文结构体、块大小/密钥长度宏、四个加解密函数原型、返回值枚举
aes128.c算法主体S 盒常量表、轮函数、分组加解密、ECB/CBC、PKCS7 填充
aes128_cfg.h编译期开关是否编入解密、是否编入 CBC、是否用外部预置轮密钥
test_aes.cPC 侧自测标准测试向量、打印比对
main.c板级调用示例从串口收报文、补齐、加密、发送

aes128_cfg.h这种开关文件在 PC 上显得多余,但在单片机上很值钱。只做上报加密、不需要本地解密的设备,把解密分支全部条件编译掉,逆 S 盒和逆轮函数就不进固件,Flash 能省下将近一半。我吃过一次亏:一个 32KB Flash 的小板子,明明只用加密方向,链接出来超了 800 字节,翻半天才发现整份逆变换都编进去了。

2.3 RAM 与 Flash 的真实开销

这里的数字是估算量级,跟内核位宽、编译器优化等级、是否开-Os都有关系,但比例关系是准的:

项目32 位 MCU 查表法8 位 MCU 查表法纯有限域计算法
S 盒 + 逆 S 盒常量512 字节 Flash512 字节 Flash0
代码体积约 1.5~2KB约 3~4KB多 0.5~1KB
上下文常驻 RAM192 字节192 字节192 字节
单次调用栈峰值60~80 字节60~80 字节略高

192 字节的常驻 RAM 来自轮密钥 176 字节加 IV 16 字节。这是这份实现里最扎眼的一笔开销,8KB RAM 的板子占掉 2.3%,还算能接受;2KB RAM 的芯片就要认真盘算。

三个取舍展开说。第一个是查表还是算。AES 的 S 盒本质是 GF(2^8) 上的乘法逆元加仿射变换,纯算也能出结果,但要在 8 位机上跑几百次循环,单块耗时能差五到十倍,Flash 是省了 512 字节,CPU 时间赔进去更多。通信加密这种非高频操作,我倾向查表,但表必须加const放 Flash。

第二个是轮密钥预计算还是在线扩展。预计算就是初始化时算好 176 字节存进上下文,之后每块直接取;在线扩展则每加密一块从头推一遍密钥。后者省 176 字节 RAM,代价是每块多一次密钥扩展的开销,块数少的时候还行,连续加密长报文就明显慢了。设备固定密钥、报文短,选在线扩展;要做双向通信、密钥可能换,选预计算。

第三个是 IV 放不放上下文。放进去意味着每次加密调用会改写上下文里的 IV,第二次调用自动续上链,这对分段发送很方便,但也埋了个坑——同一个上下文重复用之前没重置 IV,前后两次数据就串链了。这个坑在第 5 章细说。

提示:如果只做加密方向,把逆 S 盒相关代码用条件编译关掉,很多链接器对未被引用的static const数组是能剔除的,但别赌,显式关掉更稳。

2.4 先在 PC 上用标准向量跑通,再烧板子

我不建议直接在目标板上调加密。串口打印十六进制、对着示波器数时序,出了问题根本分不清是算法错还是串口错。正确顺序是先在本机用标准工具链编一份可执行文件,喂 FIPS-197 官方向量,输出对得上再交叉编译。

# 本机编译,-Wall -Wextra 把可疑写法全暴露出来 gcc -O2 -Wall -Wextra -std=c99 aes128.c test_aes.c -o aes_test # 跑起来看输出,对比 FIPS-197 附录里的标准结果 ./aes_test

-Wall -Wextra一定要加。这份代码里大量uint8_t位移和异或,隐式整型提升会悄悄把结果扩成int,赋值回去截断时如果编译器有告警,多半就是哪里少写了强制转换。我见过一次xtime()少写(uint8_t)转换,x << 1里最高位溢出到int的高位,某些编译器上碰巧结果是对的,换一家编译器就全错——这种玄学问题排查起来能耗掉一整天。

测试用的向量就两条即可覆盖加密和解密:

/* test_aes.c —— 两条标准向量,改代码前后各跑一次 */ #include <stdio.h> #include <string.h> #include "aes128.h" static const uint8_t KEY[16] = { 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07, 0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f }; static const uint8_t PT[16] = { 0x00,0x11,0x22,0x33,0x44,0x55,0x66,0x77, 0x88,0x99,0xaa,0xbb,0xcc,0xdd,0xee,0xff }; static const uint8_t CT[16] = { 0x69,0xc4,0xe0,0xd8,0x6a,0x7b,0x04,0x30, 0xd8,0xcd,0xb7,0x80,0x70,0xb4,0xc5,0x5a }; int main(void) { aes128_ctx_t ctx; uint8_t out[16], back[16]; aes128_set_key(&ctx, KEY); /* 扩展轮密钥 */ aes128_ecb_encrypt(&ctx, PT, out); /* 加密 */ printf("cipher %s\n", memcmp(out, CT, 16) == 0 ? "OK" : "FAIL"); aes128_ecb_decrypt(&ctx, CT, back); /* 解密 */ printf("decrypt %s\n", memcmp(back, PT, 16) == 0 ? "OK" : "FAIL"); return 0; }

这段测试的价值在于它把「算法对不对」和「板子上有没有跑通」两件事解耦了。两个OK出来以后,算法部分就与你无关了,后面再出问题只可能在数据搬运、串口协议或者填充约定上,排查范围直接缩小一大半。CBC 模式再补一条 NIST SP 800-38A 的向量,密钥是2b7e1516...、IV 是000102...、首块密文7649abac8119b246cee98e9b12e9197d,同样能验证链式异或有没有写反。

3. 核心代码拆解:从密钥扩展到 CBC 链式加解密的完整链路

算法主体部分我按调用顺序拆。从上下文结构开始,到密钥扩展,到单块加解密,最后到 ECB/CBC 的封装,每一段都给完整可抄的代码,再讲清楚参数和易错点。

3.1 上下文结构与密钥扩展

上下文只放两样东西:轮密钥和 IV。模式不放进结构体,用哪个模式由调哪个函数决定,这样接口更直白,也不会出现上下文标志位和函数调用打架的情况。

/* aes128.h 片段 */ #define AES_BLOCK_SIZE 16u #define AES128_KEY_SIZE 16u #define AES128_RK_SIZE 176u /* (10 轮 + 1) × 16 字节 */ typedef struct { uint8_t rk[AES128_RK_SIZE]; /* 轮密钥,预计算后常驻 */ uint8_t iv[AES_BLOCK_SIZE]; /* CBC 用,ECB 模式忽略 */ } aes128_ctx_t; void aes128_set_key(aes128_ctx_t *ctx, const uint8_t key[AES128_KEY_SIZE]); void aes128_set_iv(aes128_ctx_t *ctx, const uint8_t iv[AES_BLOCK_SIZE]);

AES128 的密钥扩展把 16 字节原始密钥摊成 11 组、每组 16 字节的轮密钥,共 176 字节。第 0 组就是原密钥,第 i 组由第 i-1 组推出,每 4 个 4 字节字要做一次 RotWord、SubWord 和轮常数异或。

/* GF(2^8) 上乘 2:左移一位,溢出位与 0x1B 异或 */ static uint8_t xtime(uint8_t x) { return (uint8_t)((x << 1) ^ ((x >> 7) * 0x1B)); } static void aes_key_expand(uint8_t rk[AES128_RK_SIZE], const uint8_t key[AES128_KEY_SIZE]) { uint8_t i, rcon = 0x01, t[4]; for (i = 0; i < 16; i++) rk[i] = key[i]; /* W[0..3] 直接来自密钥 */ for (i = 4; i < 44; i++) { /* 共 44 个 4 字节字 */ /* 取前一个字 */ t[0] = rk[(i - 1) * 4 + 0]; t[1] = rk[(i - 1) * 4 + 1]; t[2] = rk[(i - 1) * 4 + 2]; t[3] = rk[(i - 1) * 4 + 3]; if ((i & 0x03) == 0) { /* 每 4 个字做一次变换 */ uint8_t tmp = t[0]; /* RotWord:循环左移一字节 */ t[0] = t[1]; t[1] = t[2]; t[2] = t[3]; t[3] = tmp; t[0] = sbox[t[0]]; /* SubWord:逐字节查 S 盒 */ t[1] = sbox[t[1]]; t[2] = sbox[t[2]]; t[3] = sbox[t[3]]; t[0] ^= rcon; /* 异或轮常数 */ rcon = xtime(rcon); /* 下一轮的常数 */ } /* 当前字 = 前 4 个字 ^ 变换结果 */ rk[i * 4 + 0] = rk[(i - 4) * 4 + 0] ^ t[0]; rk[i * 4 + 1] = rk[(i - 4) * 4 + 1] ^ t[1]; rk[i * 4 + 2] = rk[(i - 4) * 4 + 2] ^ t[2]; rk[i * 4 + 3] = rk[(i - 4) * 4 + 3] ^ t[3]; } }

xtime里的(x >> 7) * 0x1B是替代分支的写法,最高位为 1 时(x >> 7)得 1,乘出来就是 0x1B,与左移结果异或完成约简。写成if (x & 0x80)判断也行,但乘法形式在多数编译器上会被展开成条件传送,没有跳转,8 位机上的分支预测开销就省了。

(i & 0x03) == 0这个判断容易被误写成i % 4 == 4或者(i & 0x03) == 4——下标从 4 开始,第一个需要变换的字就是 i=4,4 & 0x03等于 0,条件成立,逻辑没问题;但如果误写成与 4 比较,条件永远为假,扩展出来的轮密钥前面几轮看着正常、后面全是错的,症状是「加密能跑但结果对不上」,特别容易误判成 S 盒表抄错了。

rcon只有 10 个值,用累乘xtime推出比存一张表省 Flash,代价是每次多一次函数调用。轮密钥只算一次,这点开销无所谓。

3.2 单块加密:四步轮函数的完整实现

状态数组按列优先存放,s[4*c + r]表示第 c 列第 r 行。这个布局决定了 ShiftRows 里哪些下标要挪,写错一次就会得到「看起来像密文但解不开」的结果。

/* 字节替换:直接查表,一行搞定 */ static void sub_bytes(uint8_t s[16]) { uint8_t i; for (i = 0; i < 16; i++) s[i] = sbox[s[i]]; } /* 行移位:第 1/2/3 行分别循环左移 1/2/3 字节 */ static void shift_rows(uint8_t s[16]) { uint8_t t; /* 第 1 行:s[1] s[5] s[9] s[13] 左移 1 */ t = s[1]; s[1] = s[5]; s[5] = s[9]; s[9] = s[13]; s[13] = t; /* 第 2 行:左右互换即可 */ t = s[2]; s[2] = s[10]; s[10] = t; t = s[6]; s[6] = s[14]; s[14] = t; /* 第 3 行:左移 3 等价右移 1 */ t = s[3]; s[3] = s[15]; s[15] = s[11]; s[11] = s[7]; s[7] = t; } /* 列混淆:每列做 GF(2^8) 矩阵乘,用等价式省掉 4 次查表 */ static void mix_columns(uint8_t s[16]) { uint8_t i, a0, a1, a2, a3, t, u; for (i = 0; i < 4; i++) { a0 = s[i * 4 + 0]; a1 = s[i * 4 + 1]; a2 = s[i * 4 + 2]; a3 = s[i * 4 + 3]; t = (uint8_t)(a0 ^ a1 ^ a2 ^ a3); u = a0; s[i * 4 + 0] = (uint8_t)(a0 ^ t ^ xtime((uint8_t)(a0 ^ a1))); s[i * 4 + 1] = (uint8_t)(a1 ^ t ^ xtime((uint8_t)(a1 ^ a2))); s[i * 4 + 2] = (uint8_t)(a2 ^ t ^ xtime((uint8_t)(a2 ^ a3))); s[i * 4 + 3] = (uint8_t)(a3 ^ t ^ xtime((uint8_t)(a3 ^ u))); } } /* 轮密钥加:16 字节逐个异或 */ static void add_round_key(uint8_t s[16], const uint8_t *rk) { uint8_t i; for (i = 0; i < 16; i++) s[i] ^= rk[i]; } /* 单块加密:初始轮 + 9 轮完整轮 + 末轮(无列混淆) */ void aes128_encrypt_block(const uint8_t rk[AES128_RK_SIZE], const uint8_t in[16], uint8_t out[16]) { uint8_t s[16], r; for (r = 0; r < 16; r++) s[r] = in[r]; /* 拷进局部状态,保护输入 */ add_round_key(s, rk); /* 第 0 组轮密钥 */ for (r = 1; r <= 9; r++) { /* 第 1~9 轮 */ sub_bytes(s); shift_rows(s); mix_columns(s); add_round_key(s, rk + r * 16); } sub_bytes(s); /* 第 10 轮不做列混淆 */ shift_rows(s); add_round_key(s, r <p> <a href="https://download.csdn.net/download/hpyyzspy/12358683" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询