md5.c
MD5Init,第 82-91 行)4.2 数据追加(MD5Update,第 93-120 行)4.3 收尾(MD5Final,第 122-151 行)4.4 压缩函数(Transform,第 155-251 行)4.5 与看门狗和升级流程的配合时序5. 对外接口6. 依赖关系7. 备注与风险点源文件路径:
common/md5.c所属模块:common(通用工具库) 代码行数:258
1. 文件职责
本文件是 RSA Data Security 1990 年版 MD5 消息摘要算法的参考 C 实现(RLR/SRD/AJ/BSK/JT 版本),在 EMS 固件中提供唯一的哈希计算能力。它被驱动的场景有两类:网络层用它派生动态登录密码与校验云端下发的服务器配置(net/ems.c、net/server.c),升级流程用它比对固件镜像摘要以判断 Boot / EMS / 各设备固件是否需要刷写(net/ems_boot_handle.c、net/ems_download.c、net/upd_download.c)。本文件处在「校验/派生」环节,由调用者所在任务同步调用,自身不创建任务、不等待信号量、不使用动态内存,全部状态放在调用方栈上的 MD5_CTX 里,因此天然可重入(同一上下文不可并发使用)。大文件哈希时由调用方穿插 IWDG_Feed() 维护看门狗。
2. 关键数据结构与宏
| 名称 | 类型 | 说明 | |
|---|---|---|---|
PADDING[64] | static unsigned char 数组 | 填充模板:首字节 0x80,其余 63 字节为 0(第 39-48 行)。注意命名为 PADDING,与第 137 行局部变量 padLen 区分 | |
MD5_CTX | 结构体(来自 md5.h) | i[2] 位计数、buf[4] 中间状态、in[64] 分组缓冲、digest[16] 摘要 | |
UINT4 | 类型别名(md5.h) | unsigned long int,32 位无符号 | |
F(x,y,z) | 宏函数 | 第 1 轮选择函数 `(x&y) | (~x&z)`(第 51 行) |
G(x,y,z) | 宏函数 | 第 2 轮多数/选择函数 `(x&z) | (y&~z)`(第 52 行) |
H(x,y,z) | 宏函数 | 第 3 轮奇偶函数 x^y^z(第 53 行) | |
I(x,y,z) | 宏函数 | 第 4 轮函数 `y^(x | ~z)`(第 54 行) |
ROTATE_LEFT(x,n) | 宏函数 | 循环左移 `((x)<<(n)) | ((x)>>(32-(n)))`(第 57 行) |
FF/GG/HH/II(a,b,c,d,x,s,ac) | 宏函数 | 4 轮各 16 步的复合运算:a += Fn(b,c,d) + x + ac; a = ROTL(a,s); a += b(第 61、66、71、76 行) | |
S11..S14 | 宏常量 | 第 1 轮循环左移位数 7 / 12 / 17 / 22(第 160-163 行) | |
S21..S24 | 宏常量 | 第 2 轮循环左移位数 5 / 9 / 14 / 20(第 182-185 行) | |
S31..S34 | 宏常量 | 第 3 轮循环左移位数 4 / 11 / 16 / 23(第 204-207 行) | |
S41..S44 | 宏常量 | 第 4 轮循环左移位数 6 / 10 / 15 / 21(第 226-229 行) |
FF/GG/HH/II 各 16 处调用点内的 ac 参数是 RFC 1321 规定的 64 个正弦常数(如 3614090360、3905402710 …),以十进制字面量内联写入,无符号后缀,依赖 UINT4 的无符号运算回绕。
3. 函数清单
| 函数名 | 作用域 | 说明 |
|---|---|---|
MD5Init | 全局 | 位计数清零,buf[0..3] 写入 4 个魔数初值 0x67452301 / 0xefcdab89 / 0x98badcfe / 0x10325476(第 82 行) |
MD5Update | 全局 | 累加位计数并把数据填入 64 字节分组缓冲,满一组即调用 Transform(第 93 行) |
MD5Final | 全局 | 记录位长度 → 填补至 56 mod 64 → 追加长度 → 最后一次 Transform → 把 buf[] 小端拆入 digest[16](第 122 行) |
Transform | static(头文件却按全局声明) | MD5 单分组压缩函数,64 步 FF/GG/HH/II 后把结果累加回 buf[0..3](第 155 行) |
共 4 个函数(3 个全局 + 1 个 static)。本文件没有函数指针、没有回调、没有中断服务程序。
4. 核心逻辑解析
本文件无状态机、无定时器、无 uCOS-II 原语,是一次性同步计算。核心逻辑分四段。
4.1 初始化(MD5Init,第 82-91 行)
i[0] = i[1] = 0 // 已处理位数清零
buf[0..3] = 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476 // RFC1321 魔数
in[64] 与 digest[16] 不显式清零:in 由 mdi 索引写入、不足部分由 PADDING 补齐,digest 由 MD5Final 全量覆盖,因此无未初始化读取。
4.2 数据追加(MD5Update,第 93-120 行)
mdi = (i[0] >> 3) & 0x3F(第 100 行)算出当前分组内已用字节数——用位计数右移 3 位得到字节数,取低 6 位得到分组内偏移。- 位计数更新(第 103-106 行):
- 若
i[0] + (inLen << 3)发生回绕(结果小于原i[0]),则i[1]++; i[0] += inLen << 3;i[1] += inLen >> 29(补上inLen左移 3 位时溢出 32 位的高位)。
- 若
- 逐字节
in[mdi++] = *inBuf++,当mdi == 0x40(64 字节满组)时把 64 字节按小端组装成 16 个UINT4(第 115 行),调用Transform(buf, in),并mdi = 0。
支持任意次分段调用(工程内 net/server.c:2049-2058 连续 6 次 MD5Update 即为该用法的实例)。
4.3 收尾(MD5Final,第 122-151 行)
- 先把当前位计数写入局部
in[14]、in[15](第 129-130 行)作为长度尾的低/高 32 位。 mdi = (i[0] >> 3) & 0x3F,计算填充长度padLen = (mdi < 56) ? (56 - mdi) : (120 - mdi)(第 136 行),保证填充后长度 ≡ 56 mod 64。MD5Update(mdContext, (char*)PADDING, padLen)(第 137 行)——复用MD5Update完成填充,代价是PADDING必须为char*强制转型(丢弃const),同时padLen ≤ 64正好不超过PADDING[64]的长度。- 把
mdContext->in[0..55]按小端组装进局部in[0..13],与步骤 1 的in[14]/in[15]拼成完整 16 字分组,调用Transform(第 140-142 行)。此处依赖「填充后恰好只剩最后一个分组在缓冲里」这一不变量。 - 把
buf[0..3]各自按小端拆成 4 字节依次写入digest[0..15](第 145-150 行)。因此digest就是 RFC 1321 文本摘要的字节序列。
4.4 压缩函数(Transform,第 155-251 行)
a,b,c,d = buf[0..3]
Round1: 16 × FF(a,b,c,d, in[k], S1x, AC) k 顺序 0..15
Round2: 16 × GG(a,b,c,d, in[k], S2x, AC) k 顺序 1,6,11,0,5,10,15,4,9,14,3,8,13,2,7,12
Round3: 16 × HH(a,b,c,d, in[k], S3x, AC) k 顺序 5,8,11,14,1,4,7,10,13,0,3,6,9,12,15,2
Round4: 16 × II(a,b,c,d, in[k], S4x, AC) k 顺序 0,7,14,5,12,3,10,1,8,15,6,13,4,11,2,9
buf[0..3] += a,b,c,d
四轮的消息字索引序列与 RFC 1321 完全一致(第 164-245 行),64 个加法常数亦为规范值。宏 FF/GG/HH/II 内部用大括号包裹成语句块(属「do-while(0) 的替代写法」),因此调用时末尾带分号,在 if/else 中使用不会有悬挂分支问题。
4.5 与看门狗和升级流程的配合时序
固件 MD5 计算属耗时操作,net/ems_download.c:142、net/upd_download.c:397 的调用方在每 1024 字节或整体计算前后调用 IWDG_Feed()。也就是说:本文件内部不做任何喂狗,看门狗维护完全由调用方负责,若将来新增长哈希调用点需同步补上喂狗,否则会触发独立看门狗复位。
5. 对外接口
本文件导出(供谁调用)
- 函数:
MD5Init、MD5Update、MD5Final(Transform实际为static,不导出) - 调用方:
net/ems.c:389-391——动态登录密码派生(取digest[12..15])net/server.c:2049-2058——服务器配置校验(对 6 段字符串分段喂入,比对 32 位 HEX)net/ems_boot_handle.c:101、:125、:169——Boot 新旧镜像及 MCU flash 内容校验net/ems_download.c:127-157——分块读取固件并累进哈希net/upd_download.c:397-399——SDRAM 中整份设备固件哈希
本文件调用的外部函数/变量
- 无外部函数调用。仅使用 C 语言内建运算符与整形算术;不 include 任何系统头(第 35 行仅
#include "md5.h")。
6. 依赖关系
- 依赖:
common/md5.h(唯一 include,第 35 行)。不依赖chip.h、includes.h或任何 uCOS-II 接口,因此可脱离 RTOS 单独测试。 - 被依赖:
net/ems.c、net/ems_boot_handle.c、net/ems_download.c、net/inv_download.c(只 include 头文件,未调用)、net/server.c、net/upd_download.c。工程 Keil 目标user/EMS_3_0.uvprojx中包含md5.c,参与编译。
7. 备注与风险点
Transform的static与头文件原型矛盾:第 155 行定义为static void Transform(unsigned long int *buf, unsigned long int *in),而md5.h:54声明为全局void Transform(UINT4 *buf, UINT4 *in);。因为全工程无外部引用(grep -rn "\bTransform\b"在common/md5.*之外零命中),链接期未暴露冲突,但头文件原型是死声明。原始 RLR 参考实现中Transform是全局函数,此处被改成static却漏改头文件。- 强转丢弃
const:第 137 行MD5Update(mdContext, (char*)PADDING, padLen)——PADDING是static unsigned char,转成char*是为适配MD5Update的非 const 形参。若将来给MD5Update加上 const,需要回头清理此处。 - 依赖 32 位平台:
UINT4=unsigned long int。在 LP64(unsigned long为 8 字节)平台上MD5_CTX.buf会变 8 字节、i[2]位计数语义错乱、ROTATE_LEFT的32-n移位也不成立,算法会静默算错。移植到 PC 端工具时需改成uint32_t。 MD5Final无返回值:摘要通过mdContext->digest输出,调用方必须记得读该字段;接口没有「未初始化就 Final」的防护,若MD5Init未调用则结果无意义。MD5Update的inLen语义依赖无符号回绕:第 103 行的进位检测(i[0] + (inLen << 3)) < i[0]依赖UINT4无符号运算的回绕语义。若把UINT4改成有符号类型,该判断失效。- 无长度上限检查、无自检向量:本文件没有任何内置测试向量(RFC 1321 的
""、"abc"等期望摘要),工程内也无对应单元测试,改动后无法快速回归验证。 - 原始实现未做工程化改造:文件保留 1990 年的注释与版权头,缩进为 2 空格、宏名的自定义风格(
PADDING全大写却用于「补充填充」这一语义描述),与工程其余文件的 4 空格、l_u8_/g_t_匈牙利命名风格完全不同;MD5Final里in[14]/in[15]的赋值(第 129-130 行)虽正确但极易误读为「先写长度再填充」的顺序错误。 PADDING命名易与 PKCS7 填充混淆:本文件的PADDING只服务 MD5 的「0x80 + 若干 0」填充规则,与common/aes.c中被注释掉的 PKCS7 填充无关,两处命名相同语义不同。