EMS 知识库
首页 / knowledge/代码解析/common

md5.c

源文件路径: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 行)
Transformstatic(头文件却按全局声明)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 行)

  1. mdi = (i[0] >> 3) & 0x3F(第 100 行)算出当前分组内已用字节数——用位计数右移 3 位得到字节数,取低 6 位得到分组内偏移。
  2. 位计数更新(第 103-106 行):
    • 若 i[0] + (inLen << 3) 发生回绕(结果小于原 i[0]),则 i[1]++;
    • i[0] += inLen << 3;i[1] += inLen >> 29(补上 inLen 左移 3 位时溢出 32 位的高位)。
  3. 逐字节 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 行)

  1. 先把当前位计数写入局部 in[14]、in[15](第 129-130 行)作为长度尾的低/高 32 位。
  2. mdi = (i[0] >> 3) & 0x3F,计算填充长度 padLen = (mdi < 56) ? (56 - mdi) : (120 - mdi)(第 136 行),保证填充后长度 ≡ 56 mod 64。
  3. MD5Update(mdContext, (char*)PADDING, padLen)(第 137 行)——复用 MD5Update 完成填充,代价是 PADDING 必须为 char* 强制转型(丢弃 const),同时 padLen ≤ 64 正好不超过 PADDING[64] 的长度。
  4. 把 mdContext->in[0..55] 按小端组装进局部 in[0..13],与步骤 1 的 in[14]/in[15] 拼成完整 16 字分组,调用 Transform(第 140-142 行)。此处依赖「填充后恰好只剩最后一个分组在缓冲里」这一不变量。
  5. 把 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. 对外接口

本文件导出(供谁调用)

本文件调用的外部函数/变量

6. 依赖关系

7. 备注与风险点

  1. 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 却漏改头文件。
  2. 强转丢弃 const:第 137 行 MD5Update(mdContext, (char*)PADDING, padLen)——PADDING 是 static unsigned char,转成 char* 是为适配 MD5Update 的非 const 形参。若将来给 MD5Update 加上 const,需要回头清理此处。
  3. 依赖 32 位平台:UINT4 = unsigned long int。在 LP64(unsigned long 为 8 字节)平台上 MD5_CTX.buf 会变 8 字节、i[2] 位计数语义错乱、ROTATE_LEFT 的 32-n 移位也不成立,算法会静默算错。移植到 PC 端工具时需改成 uint32_t。
  4. MD5Final 无返回值:摘要通过 mdContext->digest 输出,调用方必须记得读该字段;接口没有「未初始化就 Final」的防护,若 MD5Init 未调用则结果无意义。
  5. MD5Update 的 inLen 语义依赖无符号回绕:第 103 行的进位检测 (i[0] + (inLen << 3)) < i[0] 依赖 UINT4 无符号运算的回绕语义。若把 UINT4 改成有符号类型,该判断失效。
  6. 无长度上限检查、无自检向量:本文件没有任何内置测试向量(RFC 1321 的 ""、"abc" 等期望摘要),工程内也无对应单元测试,改动后无法快速回归验证。
  7. 原始实现未做工程化改造:文件保留 1990 年的注释与版权头,缩进为 2 空格、宏名的自定义风格(PADDING 全大写却用于「补充填充」这一语义描述),与工程其余文件的 4 空格、l_u8_/g_t_ 匈牙利命名风格完全不同;MD5Final 里 in[14]/in[15] 的赋值(第 129-130 行)虽正确但极易误读为「先写长度再填充」的顺序错误。
  8. PADDING 命名易与 PKCS7 填充混淆:本文件的 PADDING 只服务 MD5 的「0x80 + 若干 0」填充规则,与 common/aes.c 中被注释掉的 PKCS7 填充无关,两处命名相同语义不同。