mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
1276 字
3 分钟
md5从原理到cpp实现
2026-09-12

md5哈希算法原理与实现#

一、唠嗑#

刚刚跑完步回来,藏了一周的情绪貌似都被发泄了出去,跑步还是这么神奇啊哈哈哈。

说起来,md5经常在逆向里见到她,但我只认得她个大概,每次见到她时我总会思考很久。她的特征,她的样貌这些我都不了解。唯一能记得的,只有向她说一句hello时,她总是会回我一句5d41402abc4b2a76b9719d911017c592。我不理解她的意思,姑且就当作连认识她的机会都没有吧。

即便如此,现在我还是很想了解她,想知道关于她的故事。和现实里一样,我私底下会记得很多人的名字,很多关于他们的事情,但是他们连我的名字都喊不出来,哈哈。

二、原理#

a

我们先来看看这种图,理清一下md5的主要过程。

嗯。。。

**主要分为“预处理”——“分块”——“迭代压缩”——“输出摘要”**四个部分。看样子还是压缩算法。

预处理#

咱们来看看预处理部分,那个长条分成了三段,分别是“消息”,“填充”,“消息长度”。

  • 消息:就是我们要哈希的原始消息,也就是数据。
  • 填充:在消息后面填充个“1”然后一直补“0”,知道原始消息长度+填充的长度+最后那个64比特的“用于记录原始消息的长度(也就是下面那位)”。填充完满足公式(长度+64)%512==0
  • 消息长度:填充末尾追加的8字节也就是64比特的存储,用于记录原始消息的长度。注意这个消息长度是小端序!

预处理要满足的核心公式:L×512 比特=N×32 比特

也就是最终的长度要是512比特(或者说64字节)的整数倍!如果用字来计算(也就是32比特)那也可以说成32比特**的整数倍,且这个倍数必须是 16 的整数倍。**因为md5的预处理后的数据也可以被拆分成16个字来看待。

分块#

预处理的消息被切分成了L个512比特的块。

迭代压缩#

这里是MD5的核心步骤。图里的Hmd5是压缩函数

  • IV:是初始向量(128 位的初始值),也就是那四个魔数。

    • 0x67452301

    • 0xefcdab89

    • 0x98badcfe

    • 0x10325476

  • CVq(Chaining Variable,链接变量):第 q 轮压缩后的 128 位中间结果。

输出摘要#

经过L轮压缩后得到的128位,或者通常被转换为32位16进制字符串(每 4 位二进制对应 1 位十六进制).

三、详细实现#

既然原理我们知道了,现在就来谈如何实现的吧。

我们从应用层入手,一步步拆解md5源码的cpp实现。

我直接在代码里注释了编号,不然篇幅太长,回来看这里的代码再对应会有些麻烦。

std::string md5(const std::string& input) {
MD5Context ctx; // 1 这里声明了一个上下文,我们看看做了什么
md5Update(ctx, reinterpret_cast<const uint8_t*>(input.data()), input.size());
//2.这里调用了Update函数,传入了ctx上下文等,具体看下面分析。
uint8_t digest[16]; //这里声明了无符号16字节数组,也就是哈希的结果
md5Final(ctx, digest); //4.参数是上下文和结果数组,进去看看。
// 转十六进制字符串
std::ostringstream oss;
for (int i = 0; i < 16; i++) {
oss << std::hex << std::setw(2) << std::setfill('0')
<< (int)digest[i];
}
return oss.str();
}

1.MD5Context上下文#

struct MD5Context {
uint32_t state[4]; // A, B, C, D 四个寄存器
uint64_t bitCount; // 原始消息的总比特数(最后要写进填充末尾的)
uint8_t buffer[64]; // 缓冲区,临时缓存没达到63字节的数据。
size_t bufferLen; // buffer 中已有多少字节
MD5Context() { init(); }
void init() {
// 四个魔数(RFC 1321 定义),注意这里是数值,不是内存字节
state[0] = 0x67452301; // A
state[1] = 0xefcdab89; // B
state[2] = 0x98badcfe; // C
state[3] = 0x10325476; // D
bitCount = 0;
bufferLen = 0;
}
};

2.md5Update#

md5Update(ctx, reinterpret_cast<const uint8_t*>(input.data()), input.size());

传入的参数有ctx上下文

reinterpret_cast<const uint8_t*>(input.data())这里在做一个类型转换,我们传入的是input.data(),这是个const char *类型,也就是原始消息(字符串底层字节数组的指针)。

reinterpret_cast作用是转换类型,将原来的const char *转成了const uint8_t*无符号8位整型。为什么这么转?原来的char类型是有符号的,可能会有负数,转换后则为0~255。

第三个参数input.size()则是原始消息的长度。

顺便来贴一个cpp转换类型常用的四个api,跟本节内容无关,单纯补充一下知识哈。

cast用途安全性典型场景
static_cast编译期能确定的类型转换较安全数值类型互转、基类↔派生类指针
dynamic_cast运行期带检查的转换最安全多态类型间的向下转换
const_cast去掉/加上 constvolatile危险修改 const 变量的极少数场景
reinterpret_cast重新解释比特位最危险指针↔整数、不相关指针类型互转

ok,言归正传,我们来看update函数的实现。

static void md5Update(MD5Context& ctx, const uint8_t* data, size_t len) {
ctx.bitCount += (uint64_t)len * 8; // 累加比特数
// 先填满之前没处理完的 buffer
while (len > 0) {
size_t space = 64 - ctx.bufferLen; //由于是无符号,所以space右边的计算结果如果是负数,那他就会变成特别大的数。这里计算的是缓冲区还有多少空位。
size_t copy = (len < space) ? len : space;//本次最多能拷多少字节。取“剩余数据量”和“缓冲区空位”的较小值。
std::memcpy(ctx.buffer + ctx.bufferLen, data, copy);//把数据拷贝进缓冲区。从 buffer 的 bufferLen 位置开始写,拷 data数据的 copy长度 字节。
ctx.bufferLen += copy;//更新已经处理的长度
data += copy; //移动data指针
len -= copy; //剩余数据量减少
// buffer 满了,处理一个块
if (ctx.bufferLen == 64) {
md5Transform(ctx.state, ctx.buffer);
ctx.bufferLen = 0;
}
}
}

总结一下这个函数:它的主要作用就是流式接受数据,并将数据存放在缓冲区,缓冲区每满64字节就处理一次。如果没满64字节,则将多余的数据扔在buffer缓冲区里。

接下来我们看md5Transform(ctx.state, ctx.buffer);这个函数做了什么。

3.md5Transform#

这个函数开始用到那些魔数以及常量了,我先贴一下。

// ============================================================
// MD5 核心常量
// ============================================================
// 每轮左移位数(RFC 1321 定义)
static const uint32_t S[64] = {
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, // 第1轮
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, // 第2轮
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, // 第3轮
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21 // 第4轮
};
// 正弦表 T[i] = floor(2^32 * |sin(i+1)|),RFC 1321 给出固定值
static const uint32_t K[64] = {
0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391
};
// ============================================================
// 小端序读写工具
// ============================================================
// 注意:MD5 规定所有 32 位字按小端序从字节流读取/写入。
// 这里不用 memcpy + 强制转换,而是显式按字节拼装,
// 这样在大端机器上也能得到正确结果(跨平台安全)。
// 从 4 字节按小端序读出一个 uint32_t
static inline uint32_t read_le32(const uint8_t* p) {
return (uint32_t)p[0]
| ((uint32_t)p[1] << 8)
| ((uint32_t)p[2] << 16)
| ((uint32_t)p[3] << 24);
}
// 把一个 uint32_t 按小端序写入 4 字节
static inline void write_le32(uint8_t* p, uint32_t v) {
p[0] = (uint8_t)(v & 0xff);
p[1] = (uint8_t)((v >> 8) & 0xff);
p[2] = (uint8_t)((v >> 16) & 0xff);
p[3] = (uint8_t)((v >> 24) & 0xff);
}
// 循环左移
static inline uint32_t rotl(uint32_t x, uint32_t n) {
return (x << n) | (x >> (32 - n));
}

下面这个函数可以用一张图概括

11

原来的A,B,C,D分别变成了D,经过各种计算后的A+原来的B,B,C

所以最后要说有变化的值只有一个,也就是新的B,其他值都是沿用的上轮的。

flowchart TD S1["1. 非线性函数<br/>F = f(B, C, D)<br/>(每轮 f 不同)"] --> S2["2. 求和<br/>F = F + A + K[i] + M[g]"] S2 --> S3["3. 寄存器轮转<br/>A←D, D←C, C←B"] S3 --> S4["4. 循环左移 + 加到 B<br/>B = B + rotl(F, S[i])"] S4 --> S1 style S1 fill:#dbeafe,stroke:#1d4ed8,color:#10243e style S2 fill:#fef3c7,stroke:#b45309,color:#3d2410 style S3 fill:#f3e8ff,stroke:#7e22ce,color:#2e1065 style S4 fill:#d1fae5,stroke:#15803d,color:#123524
上一轮的 state(CV_{q-1})
├──→ 复制出 A, B, C, D
│ │
│ ▼
64 轮搅乱
│ │
│ ▼
│ 新的 A, B, C, D
│ │
▼ ▼
state ←─── state += A/B/C/D
CV_q(下一轮的输入)
// ============================================================
// 处理一个 512 位(64 字节)块
// ============================================================
static void md5Transform(uint32_t state[4], const uint8_t block[64]) {
uint32_t M[16];
// 按小端序把 64 字节拆成 16 个 32 位字
for (int i = 0; i < 16; i++) {
M[i] = read_le32(block + i * 4); //这里的block也就是分块处理的buffer。
//这个函数是用来构建M数组,将buffer里的数据变成小端序16个32位字。
}
//获取四个IV
uint32_t A = state[0];
uint32_t B = state[1];
uint32_t C = state[2];
uint32_t D = state[3];
// 64 步主循环
for (int i = 0; i < 64; i++) {
uint32_t F;
int g; // M 的下标
if (i < 16) {
// 第 1 轮:F = (B & C) | (~B & D)
F = (B & C) | (~B & D);
g = i;
} else if (i < 32) {
// 第 2 轮:F = (D & B) | (~D & C)
F = (D & B) | (~D & C);
g = (5 * i + 1) % 16;
} else if (i < 48) {
// 第 3 轮:F = B ^ C ^ D
F = B ^ C ^ D;
g = (3 * i + 5) % 16;
} else {
// 第 4 轮:F = C ^ (B | ~D)
F = C ^ (B | ~D);
g = (7 * i) % 16;
}
// 核心运算:F = F + A + K[i] + M[g]; A = D; D = C; C = B;
// B = B + rotl(F, S[i])
F = F + A + K[i] + M[g];
A = D;
D = C;
C = B;
B = B + rotl(F, S[i]); //循环左移
}
//每整完64轮就更新IV
// 累加回 state
state[0] += A;
state[1] += B;
state[2] += C;
state[3] += D;
}

4.md5Final#

// ============================================================
// 收尾:填充 + 写入原始长度 + 输出 16 字节摘要
// 格式:[原始消息] + [0x80] + [若干 0x00] + [8 字节原始长度] == 64字节的整数倍
// 至少要填 1 字节(那个 0x80)。
// ============================================================
static void md5Final(MD5Context& ctx, uint8_t digest[16]) {
uint8_t padding[64];
std::memset(padding, 0, sizeof(padding));
padding[0] = 0x80; // 第一个填充字节是 0x80,这是默认的,其余是 0
// 记录填充前的比特数
uint64_t bits = ctx.bitCount;
// 需要填充的字节数:让 (bufferLen + 8) % 64 == 0
size_t padLen = (ctx.bufferLen < 56)
? (56 - ctx.bufferLen)
: (120 - ctx.bufferLen);//buffer里的数据比56字节大的话,就得寻找下一个可以填充的位置,这里是120字节,120+8=128,是64的整数倍
// 先填 0x80 和 0x00
md5Update(ctx, padding, padLen);
// 再把 64 位原始长度按小端序追加(8 字节)
uint8_t lenBytes[8];
for (int i = 0; i < 8; i++) {
lenBytes[i] = (uint8_t)((bits >> (8 * i)) & 0xff);
}
md5Update(ctx, lenBytes, 8);
// 把 A、B、C、D 按小端序输出成 16 字节
for (int i = 0; i < 4; i++) {
write_le32(digest + i * 4, ctx.state[i]);
}
}
原始消息
┌─────────────────────────────────────────────────┐
│ md5Update:流式处理 │
│ ├─ 每凑满 64 字节 → md5Transform 处理 │
│ ├─ 处理过的都是"完整块",无填充 │
│ └─ 剩下 < 64 字节的尾巴留在 buffer 里 │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ md5Final:专门处理最后的尾巴 │
│ ├─ 对 buffer 里的尾巴做填充: │
│ │ 0x80 + 若干 0x00 + 8 字节长度 │
│ ├─ 触发最后一次(或两次)md5Transform │
│ └─ 把最终 state 输出成 16 字节摘要 │
└─────────────────────────────────────────────────┘

5.输出摘要#

// 转十六进制字符串
std::ostringstream oss;
for (int i = 0; i < 16; i++) {
oss << std::hex << std::setw(2) << std::setfill('0')
<< (int)digest[i];
}
return oss.str();
//这里将digest里的数据转为16进制输出
uint8_t b = 0x0A; // 十进制 10
// ❌ 直接输出(uint8_t 本质是 unsigned char)
std::cout << b; // 输出的是换行符 '\n'(ASCII 10)
// ✅ 转成 int 输出
std::cout << (int)b; // 输出 "10"(十六进制下是 "a")
更现代就是
#include <format>
std::string hex;
for (int i = 0; i < 16; i++) {
hex += std::format("{:02x}", digest[i]);
}
return hex;

四、总结#

flowchart TD IN["任意长度的消息"] --> BLK["按 512 位分块处理"] BLK --> FULL["完整块:直接 64 轮搅拌<br/>状态 += 本轮结果"] FULL --> BLK BLK --> TAIL["最后不足 512 位的尾巴"] TAIL --> PAD["填充:0x80 + 若干 0<br/>+ 64 位小端长度"] PAD --> LAST["处理最后一块"] LAST --> OUT["输出 A B C D<br/>小端拼接 = 128 位摘要"]
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

md5从原理到cpp实现
https://fatdog.20060113.xyz/posts/md5/
作者
神秘大胖狗
发布于
2026-09-12
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00