尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
C语言手写MD5算法:从原理到性能优化的完整实战
在项目里校验一份文件或者一段字符串的完整性第一反应基本都是找库调一下几行代码收工。但如果你真想知道消息摘要这类算法到底在做什么我建议把MD5用C语言完完整整手写一遍。MD5这个算法复杂度刚好卡在一个很舒服的位置数据填充、字节序、位运算、循环结构、状态轮换全都要处理但又不涉及大数运算和复杂的密钥编排非常适合当底层基本功的练手项目。我说几个你可能会关心的点它能做什么——对任意长度的输入产生固定128位指纹用于文件校验、缓存键计算、数据去重等非对抗场景它适合谁——正在啃网络协议和密码学基础的人、准备面试的人、想优化移植嵌入式摘要算法的人。这篇文章不会只贴一份能跑的代码而是把为什么这么写、性能瓶颈在哪、常见坑怎么避都讲透。1. MD5算法整体设计拆解从一条消息到128位指纹1.1 算法核心流程MD5本质上是把任意长度的输入通过补位、分块、压缩三步变成一个固定128位的输出。完整的处理流程可以用一句话概括先补出一个能被512整除的长度再按512位切成一个个消息块每个块进入同一个压缩函数把4个32位寄存器的状态反复搅动最后输出状态寄存器中的128位。具体拆开是这样计算原始消息的位长度比如消息有L位。在消息末尾补一个“1”位再补若干“0”位使得补完后的长度对512取模等于448。在最后追加64位的原始长度L追加时按小端字节序写入。这样整个消息就变成若干个512位64字节的消息块。初始化4个32位寄存器A0x67452301B0xefcdab89C0x98badcfeD0x10325476。每个消息块进入压缩函数对A、B、C、D进行64步非线性变换。所有块处理完把A、B、C、D拼接输出每个字同样按小端转成16个字节。这里有个很多人初学时容易糊涂的点填充是对512位64字节进行但实际写代码的时候大家更习惯按字节处理。因为一个字节是8位所以条件是“消息长度对64字节取模等于56字节”。补“1”位在字节层面就等于补一个0x80字节“0”位就是0x00字节最后再补8字节小端长度。别以为位操作和字节操作是两套方法本质上完全一样只是字节写法更好操作。第3步里的“原始长度”是整个消息的位长度不是字节长度而且必须用64位无符号整数表示。因为64位能表示的最大值是2^64-1个位这对应约2艾字节Exabyte的输入实际场景几乎不可能触达所以安全。1.2 为什么值得用C语言手写一遍有人会问MD5实现早就烂大街了网上随便一搜就有成型的代码为什么还要手写我的理由很实际网上流传的很多MD5代码质量参差不齐有的为了追求“短”牺牲了可读性有的把标准里公式写错了还能跑出正确结果——因为他把两个错误互相抵消了。你自己动手写一遍至少要搞清楚三件事状态寄存器在每一轮是怎么轮换的16个消息字是从哪里来的填充长度为什么是那样拼的。C语言是最适合干这事的语言。一方面它能直接操作uint32_t这种无符号整数位运算的行为完全可预测另一方面C没有隐式的字符串处理负担你被迫自己管理缓冲区这正好把MD5处理字节流的过程看得清清楚楚。相比之下用Python写MD5容易很多但动态类型和隐藏的整数溢出机制会把底层的字节语义掩盖掉你调试了半天可能还没意识到问题出在字节序上。还有一层考虑是嵌入式场景。很多需要计算校验和的设备CPU没有专门的指令甚至没有内存管理单元这时候每一条指令和每一块栈内存都要精打细算。一个用C写的、足够精简的MD5实现是可以直接硬移植过去的。我用C实现过一版最终代码加常量表也不到200行非常便于审查和移植。2. C语言核心实现结构体、Update与Transform2.1 上下文结构体与外部API先设计出对外接口。MD5支持流式处理意思是你可以一点一点喂数据喂完了再取摘要而不是一次性把整份数据怼进函数。流式处理需要保存中间状态所以要用一个上下文结构体。下面是常用且清晰的版本代码语言标记为c#include stdint.h #include string.h #include stdio.h typedef struct MD5_CTX { uint32_t state[4]; uint64_t total_len; uint8_t buffer[64]; } MD5_CTX; void md5_init(MD5_CTX *ctx); void md5_update(MD5_CTX *ctx, const void *data, size_t len); void md5_final(MD5_CTX *ctx, uint8_t digest[16]);这里total_len记录已经处理的总字节数buffer[64]是待处理的残块缓冲区。state[4]是A、B、C、D四个字的实时状态。init负责把状态设为标准初始值update负责吸收数据final负责做最后填充并产出16字节摘要。有人会把 total_len 设计成位长度那也OK但要注意每次 update 都要做 uint64_t 溢出处理。我的习惯是记录字节数最后统一乘8。由于无符号数乘法会发生回绕这个方案在极端输入下可能出问题但这需要处理2^61字节的数据普通项目用不上。如果你有一天真要哈希2EB以上的文件记得把计数拆成两个32位分别做进位。2.2 Transform主循环看懂步函数就不怕核心中的核心是压缩函数md5_transform。它接收当前state和一个64字节的消息块把state原地更新。算法里有64步每步根据轮数选择不同的非线性函数F、G、H、I。四个函数分别对应轮1F (B C) | (~B D)这一步也叫“如果B则C否则D”。轮2G (D B) | (~D C)把F里的角色换了一下。轮3H B ^ C ^ D三个字异或。轮4I C ^ (B | ~D)最后一个比较绕。每一个步长做的事是从16个消息字中取一个加上一个常数K加上上一步算出的F加上当前A做一次循环左移再加到B上然后A、B、C、D轮换位置。为提高效率我先把64字节消息块拆成16个uint32_t小端字存入数组w[16]。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 }; static const uint8_t S[64] { 7,12,17,22,7,12,17,22,7,12,17,22,7,12,17,22, 5,9,14,20,5,9,14,20,5,9,14,20,5,9,14,20, 4,11,16,23,4,11,16,23,4,11,16,23,4,11,16,23, 6,10,15,21,6,10,15,21,6,10,15,21,6,10,15,21 }; #define ROTL(x, n) (((x) (n)) | ((x) (32 - (n)))) static void md5_transform(uint32_t state[4], const uint8_t block[64]) { uint32_t w[16]; uint32_t a state[0], b state[1]; uint32_t c state[2], d state[3]; for (int i 0; i 16; i) { w[i] (uint32_t)block[i * 4] | ((uint32_t)block[i * 4 1] 8) | ((uint32_t)block[i * 4 2] 16) | ((uint32_t)block[i * 4 3] 24); } for (int i 0; i 64; i) { uint32_t f, g; if (i 16) { f (b c) | (~b d); g i; } else if (i 32) { f (d b) | (~d c); g (5 * i 1) 15; } else if (i 48) { f b ^ c ^ d; g (3 * i 5) 15; } else { f c ^ (b | ~d); g (7 * i) 15; } uint32_t t d; d c; c b; b ROTL(a f K[i] w[g], S[i]); a t; } state[0] a; state[1] b; state[2] c; state[3] d; }K表的来源是取角度绝对值的正弦函数再乘以2^32的整数部分看起来神秘实际上是用来生成一组“无规律”的扰动常数。如果你不想硬编码这64个常量可以在初始化时用fabs(sin(i1))*pow(2,32)现算但那样每次启动都要循环一遍不如直接预置常量表。消息字索引g的选取逻辑是MD5设计的一部分。轮1按输入顺序取字轮2做(5i1)%16轮3做(3i5)%16轮4做(7i)%16。代码里我用 15代替% 16因为输入是16个字按位与和取模结果一致但位运算通常更便宜。2.3 Update、Final与填充逻辑实现了transform之后剩下的就是组装。update要处理三种情况当前缓冲区已有部分数据、传入数据足够填满整个块、以及最后剩下的零头。void md5_init(MD5_CTX *ctx) { ctx-state[0] 0x67452301; ctx-state[1] 0xefcdab89; ctx-state[2] 0x98badcfe; ctx-state[3] 0x10325476; ctx-total_len 0; memset(ctx-buffer, 0, sizeof(ctx-buffer)); } void md5_update(MD5_CTX *ctx, const void *data, size_t len) { const uint8_t *p (const uint8_t *)data; size_t have (size_t)(ctx-total_len 63); ctx-total_len len; if (have) { size_t fill 64 - have; if (len fill) { memcpy(ctx-buffer have, p, len); return; } memcpy(ctx-buffer have, p, fill); md5_transform(ctx-state, ctx-buffer); p fill; len - fill; } while (len 64) { md5_transform(ctx-state, p); p 64; len - 64; } if (len) { memcpy(ctx-buffer, p, len); } } void md5_final(MD5_CTX *ctx, uint8_t digest[16]) { uint64_t bits ctx-total_len * 8; size_t have (size_t)(ctx-total_len 63); size_t pad (have 56) ? (56 - have) : (120 - have); uint8_t tail[128] {0}; tail[0] 0x80; for (int i 0; i 8; i) { tail[pad i] (uint8_t)(bits (8 * i)); } md5_update(ctx, tail, pad 8); for (int i 0; i 16; i) { digest[i] (uint8_t)(ctx-state[i 2] (8 * (i 3))); } }final里的填充逻辑如果当前缓冲区的已用字节数少于56就填充到56字节如果已经等于或超过56说明一整个块放不下最后的填充要先补齐到64再开一个新块填充到56。我定义tail数组为128字节保证两种情况都能覆盖。填充的8字节长度是小端写入的很多实现错就错在这一步写成大端结果全是abadgebag。输出摘要时也要注意小端。state[0]对应输出字节0到3state[1]对应字节4到7依次类推。取字节时i2是找第几个状态字i3是找该字里的第几个字节。3. 性能优化实战从能跑到跑得快3.1 先立个靶子基准怎么设很多人拿到实现就急吼吼地优化其实第一步应该是先确认“现在跑多快”。性能优化如果没有基准后面做的每件事都是猜。测吞吐量的标准方法准备一块足够大的内存或者一个真实文件比如256MB的随机数据循环调用md5_update最后用高精度时钟量耗时。别用什么system clock至少要读到微秒级。我自己用的方式是生成一个256MB的文件用一次性fread读进1MB缓冲区每读完一段就update一次然后看每秒能处理多少MB。之所以不用一次性读取整个文件是因为那会把内存拷贝的时间也算进去干扰了真实哈希吞吐。对MD5来说一次update处理64字节和一次update处理1MB吞吐差距可能有几倍。原因是循环调用、函数调用、残块合并处理都是有开销的。1MB缓冲比4KB缓冲少了上百次函数调用虽然每次调用本身不贵但积少成多。所以基准要明确测的是“大块连续输入”还是“零散小输入”两者的优化重点完全不同。3.2 源码级优化点逐个击破源码层面的优化有一个比较清晰的优先级。第一把ROTL从函数改成宏。ROTL如果写成函数每次64步都要调用64次即便编译器内联也不如直接展开来得稳。宏版本如#define ROTL(x,n) (((x)(n))|((x)(32-(n))))在C里是安全且可移植的。第二避免每步都做if判断。上面我给出的transform里64步循环每步都要判断i属于哪一轮这会带来流水线分支预测惩罚。性能敏感的实现会把64步展开成4个16步循环每个循环固定使用对应的非线性函数和消息字索引。分支开销在实际跑大文件时能占到总耗时的10%以上尤其对于乱序性较强的循环分支预测会频繁失败。拆开后代码会显得啰嗦但换来的是更稳定的吞吐。第三用宏辅助展开步进逻辑。每轮16步的步进结构完全一样只是非线性函数和取字索引不一样。你可以把这个模板抽出来#define MD5_STEP(f, a, b, c, d, w, k, s) do { \ uint32_t t d; \ d c; \ c b; \ b ROTL((a) (f) (k) (w), (s)); \ a t; \ } while (0)然后用它手动搭每轮16步。这种展开牺牲了代码可读性但能省去分支和循环计数器。不过我个人不会手动把64步全部写成一行一行因为编译器在开-O2以后往往也会做循环展开手动展开的收益在多数场景下有限。想让代码更容易预测的话优先做“四轮拆分”后面的事交给编译器。第四字节序转换。小端机器上从内存加载uint32_t再按位拼装是零成本的但大端机器上要倒腾字节。如果在x86这类小端平台跑尽量保持小端分解不要因为“移植性”把所有字都转成网络字节序再计算那是白白浪费时间。第五避免重复复制。update里如果不是必须不要再把缓冲区的情况弄乱。我的实现里每次只复制不足64字节的残块完整的块直接指向输入数据调用transform没有二次拷贝。这一点你看代码就能明白while (len 64)里transform直接吃的是p指向的原始数据。3.3 编译器、调用方式与大数据吞吐说完源码再谈编译和使用方式。我自己实测的时候同一个源代码只用-O0编译和用-O2 -marchnative编译吞吐可以差出1.5到2倍。因为-marchnative会让编译器根据当前CPU特性选择指令调度策略但有个麻烦是编译出的二进制不能随便拷到其他机器跑。如果你的程序要分发到不同CPU的服务器上最好用保守的-O2部署机器统一再用-marchx86-64或者干脆默认值。调用方式上最需要注意的是输入缓冲区大小。读取文件时用4KB缓冲和用1MB缓冲哈希总耗时能差不少。4KB缓冲会产生大量小规模update调用而md5_update每次都要做残块判断和memcpy。用1MB缓冲后绝大部分update都能直接命中while (len 64)分支效率最高。我在某文件指纹工具里就是这么干的一开始用8KB缓冲后来改成1MB整体吞吐提升了约15%。还有一个很多人忽略的点文件大小比较小时磁盘IO和系统调用开销远大于哈希本身。比如哈希一个10字节的文件真正计算可能只要几十纳秒但打开文件、读取、关闭可能就要几十微秒。这时候纠结算法内部性能没有意义优化重点应该放在减少系统调用上比如用pread连续读、避免反复fseek。下表是我在一台普通4核CPU上跑同一份数据、不同优化状态的相对吞吐示意。数值只能说明趋势你自己的机器要重新测。优化状态相对吞吐备注O0编译单循环含分支约1.0倍基准最慢但最容易调试O2编译宏实现ROTL约1.7倍正常生产可用O2编译四轮拆分约2.2倍分支惩罚明显下降O3编译四轮拆分marchnative约2.8倍依赖编译器和CPU想继续提速可以看两条路一是把需要哈希的数据尽量排布在连续内存减少数据加载时的cache miss二是使用多线程并行处理多个独立消息但这只适用于一次要哈希很多文件的情况。单个文件内部的MD5链式结构是强依赖的上一个块的输出是下一个块的输入没法简单并行。4. MD5常见踩坑与问题排查实录4.1 五个我亲眼见过的错误这种手写算法最怕的不是逻辑难而是细节错得悄无声息。下面几个问题我都见过不止一次。第一个是填充长度忘乘8。有人写了uint64_t bits ctx-total_len;就少了个乘8。结果是任何超过两个字节的消息摘要都不对但空字符串和单字节消息可能正确非常迷惑。正确写法是ctx-total_len * 8。第二个是字节序混乱。分两种情况填充末尾的64位长度要小端写入最终摘要的四个状态字也要小端输出。我见过有人长度部分写了小端结果输出部分用大端或者反过来。更隐蔽的是加载消息字的时候用union直接转uint32_t这在x86上可能碰巧没问题但在大端机器上全错。用显式字节移位最稳。第三个是残留缓冲区长度判断错误。update里用ctx-total_len 63而不是自己维护一个单独变量能避免很多状态不同步问题。如果你同时用两个变量跟踪“缓冲区里有多少字节”和“总处理长度”很容易在边界条件下不一致尤其是跨多次update调用时。第四个是片段更新顺序错误。update里如果当前残块不为空应该先把残块填满再处理后续完整块。有人喜欢先把传入数据全部memcpy到一个大缓冲再一次性处理这样代码简单但内存开销大而且在final之后容易忘记清空大缓冲。第五个是用有符号数处理字节。int8_t在移位时会先转成有符号整型符号扩展会污染结果。所有涉及字节拼装的地方都应该用uint8_t或显式转成uint32_t再移位。下面整理成速查表问题现象可能原因解决方案常见向量几乎都对长输入偶尔错长度计数漏乘8或计数溢出检查total_len * 8是否有乘8空字符串正确任意非空输入错误消息字加载字节序反了统一用小端字节移位加载小数据正确大文件必出错update边界处理不正确用total_len 63统一判断残块在不同CPU上结果不同依赖了union对齐或未定义的移位行为改用显式移位和uint32_t类型数字全部正常但字符串打印乱码摘要输出时序错了摘要数组按小端逐字节解析4.2 用向量和对照实现自检手写完成之后第一步必须跑标准测试向量。这三个向量基本够用空字符串、字符串abc、一百万个字母a。MD5的参考值如下输入参考摘要空字符串d41d8cd98f00b204e9800998ecf8427eabc900150983cd24fb0d6963f7d28e17f72a重复1,000,000次7707d6ae4e027c70eea2a935c2296f21如果这三个向量全对基本可以确定核心逻辑没问题。接下来要做的是随机对照测试随机生成不同长度的二进制数据比如长度从0到5000字节随机取喂给你的实现和一个成熟的摘要库比较输出是否一致。这里要注意比较的对象本身必须是对的所以我建议先用标准库验证再拿自己的实现做批量测试。第二轮测试是性能测试时要跑的不只是验证正确性。造一个256MB的二进制文件分别用4KB、64KB、1MB的缓冲区读取并update记录三种配置下的耗时。你会发现4KB和1MB之间有明显差距这能帮你判断要不要修改使用层的缓冲区大小。最后再做一次“增量化对照”。把一个100KB的消息分三次update进同一个上下文和一次性update进去两者摘要必须完全一致。这个测试特别容易暴露残块缓冲区的拼接错误任何一处少复制或多复制都会导致对不上。5. MD5的边界认识与项目选型经验5.1 它不安全但也不是不能用MD5在1996年被发现存在碰撞风险后来攻击者已经能够构造出两个内容不同但MD5完全相同的文件。选择前缀攻击也已经实用化攻击者可以在一个合法文件后追加内容让它和另一个恶意文件的MD5一致。这意味着凡是“对抗恶意输入”的场景MD5都不能再用了。但把MD5放在非对抗场景里它依然是相当好用的完整性工具。比如下载包校验目的只是检测传输过程中文件有没有被损坏而不是防一个黑客篡改。又比如缓存键把一组参数序列化后取MD5作为key只要不发生恶意构造碰撞概率低到可以忽略。再比如数据去重和分块指纹这些场景里你并不假设输入方会刻意制造碰撞。我的态度是了解MD5实现MD5但不要在安全敏感链路上依赖MD5。如果代码里出现MD5第一时间要想到它是不是只负责“发现意外错误”而不是“防止恶意篡改”。5.2 要迁移时如何替换如果项目里正用MD5做安全相关用途替换方案的选择要看你的成本。最平滑的是SHA-256虽然摘要从16字节变成32字节但算法同样是Merkle-Damgard结构代码骨架和MD5非常相似手写实现、性能调优的思路几乎可以平移。唯一要注意的是SHA-256的分组大小、初始向量、旋转位数、扩展消息调度都不同不能只改常量就糊弄过去。更现代的选择是BLAKE2它比SHA-256快且在软件实现上有优势。BLAKE2的API设计和MD5也很接近都是init/update/final三段式迁移成本不算高。不过如果你的项目还涉及HMAC或者签名校验那就不是简单替换一个摘要函数的问题需要整个密码学库一起换。如果你只是想把MD5从“文件校验”升级成“更强文件校验”SHA-256或BLAKE2都行。但在动手之前先确定你的数据到底需不需要抗碰撞攻击。很多项目里的MD5只是给内部文件去重做个索引硬换成SHA-256会白白增加计算时间却不会带来任何实质安全性提升。5.3 我在实际项目中的选择我自己的文件指纹工具里保留了MD5但只用于非安全场景包管理模块的缓存键、文件去重、调试日志里的消息指纹。安全相关的模块比如登录态token和数字签名验证全部走了SHA-256。这个分工执行了很多年一直稳稳当当。手写这版MD5代码最大的收获反而不是性能而是把循环结构彻底看透了。我能立刻判断一个已有的MD5实现有没有做无谓分支、有没有错误地重复拷贝数据、有没有在小端机器上做多余转换。这种底层素养靠调库是练不出来的。如果你也想完整走一遍这个流程我建议先把这个实现跑通再按四轮拆分的思路重写一遍transform最后测一轮吞吐对比。你会发现实现一遍比背十遍规范都管用。
RELATED

相关推荐

前后端分离课表管理系统实战:SpringBoot+Vue+MyBatis全栈开发

前后端分离课表管理系统实战:SpringBoot+Vue+MyBatis全栈开发

我最早接到这个课表管理系统的需求时,项目方只给了一句话——把学校的排课、调课、查课搬到线上。当时的技术方案其实已经比较固定了:前后端分离架构,后端用SpringBootMyBatisMySQL,前端用Vue,这也是目前JavaWeb毕业设…

📅 2026/10/9 8:17:38
RabbitMQ实战全解:Java后端必备的消息队列异步解耦与可靠性指南

RabbitMQ实战全解:Java后端必备的消息队列异步解耦与可靠性指南

搞Java后端的人,迟早会碰消息队列。而RabbitMQ,几乎可以说是你入行后第一个绕不开的MQ。我最早接触RabbitMQ是在一个订单系统的重构项目里,那时候线程池加数据库轮询已经快撑不住了,每天凌晨的峰值流量能把库连接池打满。后来把Ra…

📅 2026/10/9 8:17:38
Ubuntu 24.04 conda环境Python服务开机自启:Ollama依赖管理实践

Ubuntu 24.04 conda环境Python服务开机自启:Ollama依赖管理实践

搞了一台Ubuntu 24.04的机器做本地大模型应用,推理服务用Ollama跑,业务逻辑是放在conda虚拟环境里的Python程序。需求很简单:机器重启后,两个东西都要自动起来,而且Python程序必须在Ollama之后启动。听起来挺常规&…

📅 2026/10/9 8:17:38
MORE NEWS

更多资讯

📰

从零搭建算法刷题题单目录:知识域划分与复盘方法

1. 刷题这件事,为什么需要一份"题单目录"先说个扎心的现实:很多人在算法面试或者日常训练中,刷了三四百道题,一到真正需要输出的时候,脑子里还是一团浆糊。遇到新题就像碰到陌生人,感觉似曾相识&…

📰

OpenClaw与MCP集成实战:从配置到部署的完整指南

最近我一直在折腾 OpenClaw,越折腾越觉得它和 MCP 是天生一对。OpenClaw 是一个开放的个人 AI 代理框架,你可以把它装到电脑、服务器甚至手机 Termux 里,再给它接上各种大模型和外部工具;MCP 则是 Model Context Protocol&#xf…

📰

基于SpringBoot+Hadoop的农业环境管理平台搭建与答辩指南

每年到了这个节点,总有一大批人盯着同一个题目熬夜——"基于SpringBootHadoop的农业环境管理平台"。你可能就是其中一个,也可能只是刷到了这篇,不管哪种情况,我先把话说在前面:这个题目没有想象中那么可怕&a…

📰

告别无效刷题:如何搭建一份可复用的刷题题单目录

刷题这件事,最难的不是题目本身有多难,而是“不知道从哪道开始刷”。我自己带过十几个新人,也前后整理过好几版题库,一个很深的体会是:花一个周末把 刷题题单目录 搭起来,比闷头刷三个月都管用。题单目录…

📰

OpenClaw + MCP:从零部署到跑通的智能体实操指南

最近一个词在我的圈子里出现的频率高得吓人:MCP(Model Context Protocol)。紧跟在它后面被反复提到的,还有另一个开源项目——OpenClaw。如果你刷技术社区,肯定见过“用 MCP 连接一切”的说法,OpenClaw 则是…

📰

BUUCTF Web第二页实战:文件包含、伪协议与上传绕过全解析

打开BUUCTF的Web题列表,翻过第一页,大多数人第一次意识到自己的"新手期"结束了。第一页的题目很善良,SQL注入会告诉你注入点在哪,命令执行会留好回显,弱口令甚至把用户名直接写在注释里。可到了第二页&#…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬