原码见https://download.csdn.net/download/dangpu/92814940
本文也给出了全部实现原码。
1. 前言
SHA-256(安全哈希算法 256 位)是 SHA-2 家族中最常用的哈希函数,由美国国家标准与技术研究院(NIST)发布为 FIPS PUB 180-4 标准。它广泛应用于:
-
密码存储(配合盐值)
-
固件完整性校验
-
数字签名
-
区块链(如比特币的工作量证明)
本文将从算法原理出发,深入讲解在资源受限的单片机上如何正确、高效地实现 SHA-256,并给出一个完全不含 uint64_t、限制数据量 ≤ 512 MB 的 C 语言实现,该实现已通过标准测试向量验证,可直接用于 8051、AVR、ARM Cortex‑M 等平台。
2. SHA-256 核心特性
| 特性 | 说明 |
|---|---|
| 输出长度 | 256 位(32 字节) |
| 输入长度 | 任意长度(标准支持 ≤ 2⁶⁴‑1 位,本文实现限制 ≤ 512 MB) |
| 确定性 | 相同输入 → 相同输出(后文详细解释) |
| 单向性 | 无法从哈希值逆向推导输入 |
| 抗碰撞 | 极难找到两个不同输入产生相同哈希 |
| 雪崩效应 | 输入微小变化 → 输出约 50% 比特翻转 |
与 CRC 不同,SHA-256 没有用户可配置的多项式或初始值------所有参数(初始哈希值、轮常数)都是标准固定的。
3. 算法原理简述
SHA-256 处理流程分为三步:
3.1 填充(Padding)
-
先附加一个
0x80字节; -
再附加若干个
0x00,使得总长度 ≡ 56 (mod 64); -
最后附加 8 字节(64 位) 的原始数据位长度(大端序)。
3.2 解析(Parsing)
将填充后的消息分成 512 位(64 字节) 的块。
3.3 压缩函数(Compression Function)
每个 64 字节块经过 64 轮 迭代计算,更新 8 个 32 位状态变量 (a, b, c, d, e, f, g, h)。
-
状态变量的初始值来自前 8 个质数的平方根小数部分的前 32 位;
-
轮常数来自前 64 个质数的立方根小数部分的前 32 位。
4. 单片机实现的关键难点
4.1 字节序问题
SHA-256 标准使用 大端序 (网络字节序),而主流单片机(ARM Cortex‑M, AVR, 8051)多是小端序。
必须通过 load32 / store32 函数在字节数组与本地整数之间进行转换:
cpp
static uint32_t load32(const uint32_t *p) {
return ((uint32_t)p[0] << 24) |
((uint32_t)p[1] << 16) |
((uint32_t)p[2] << 8) |
((uint32_t)p[3]);
}
static void store32(uint32_t *p, uint32_t x) {
p[0] = (uint32_t)(x >> 24);
p[1] = (uint32_t)(x >> 16);
p[2] = (uint32_t)(x >> 8);
p[3] = (uint32_t)(x);
}
即使在大端机上,为了代码统一和可移植性,也应保留这些函数。
4.2 内存占用优化
-
W 数组 :本实现采用环形缓冲区 (仅 16 个
uint32_t),而非标准的 64 个,节省 192 字节 RAM。 -
上下文结构体 :
sha256_ctx包含8×4 + 4 + 64 = 100字节,适合栈上分配。 -
无动态内存分配:完全使用栈和静态缓冲区,适合裸机环境。
4.3 计数器类型选择
-
标准要求:64 位计数器支持最大 2⁶⁴‑1 位输入。
-
单片机限制:若使用
uint64_t可能增加 4~8 字节 RAM 占用,且某些 8 位编译器不支持。 -
折衷方案 :改用
uint32_t count,并将数据总量限制为 ≤ 536,870,911 字节(约 512 MB) ,此时总位数 ≤ 2³²‑1,可以安全存放在uint32_t中,实现完全无uint64_t的代码。
5. 最终代码实现(匹配附件)
以下代码与您提供的 sha256.h 和 sha256.c 完全一致。其中 U16 为自定义的 16 位无符号整数类型(如 unsigned short),MyMemset / MyMemCpy 可替换为标准库函数或自定义实现。
5.1 sha256.h
cpp
#ifndef SHA256_H
#define SHA256_H
//定义数据类型
#ifndef uint8_t
#define uint8_t unsigned char
#endif
#ifndef uint16_t
#define uint16_t unsigned short
#endif
#ifndef uint32_t
#define uint32_t unsigned long
#endif
/**
* SHA-256 上下文(完全无 uint64_t 版本)
*
* 限制:输入数据总长度 ≤ 536,870,911 字节(约 512 MB),
* 以保证总位数(bit_len = count * 8)能存入 32 位无符号整数。
*
* 若输入超过此限制,哈希结果将错误(因为长度字段会溢出)。
*/
typedef struct {
uint32_t state[8]; // 哈希状态
uint32_t count; // 已处理字节数(≤ 536870911)
uint8_t buffer[64]; // 数据缓冲区
} sha256_ctx;
/**
* 计算 SHA-256
* @param data 数据指针
* @param len 数据长度(≤ 536870911)
* @param digest 输出缓冲区(至少 32 字节)
*/
void sha256_compute(const void *data, uint16_t len, uint8_t digest[32]);
#endif
5.2 sha256.c
cpp
#include "sha256.h"
/* 循环右移 */
#define ROTR(x, n) (((x) >> (n)) | ((x) << (32 - (n))))
/* 逻辑函数 */
#define CH(x, y, z) (((x) & (y)) ^ (~(x) & (z)))
#define MAJ(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
#define EP0(x) (ROTR(x, 2) ^ ROTR(x, 13) ^ ROTR(x, 22))
#define EP1(x) (ROTR(x, 6) ^ ROTR(x, 11) ^ ROTR(x, 25))
#define SIG0(x) (ROTR(x, 7) ^ ROTR(x, 18) ^ ((x) >> 3))
#define SIG1(x) (ROTR(x, 17) ^ ROTR(x, 19) ^ ((x) >> 10))
void my_memcpy(void *tar, const void *src, uint16_t len)
{
const uint8_t *pSrc;
uint8_t *pTar;
pSrc = (const uint8_t*)src;
pTar = (uint8_t*)tar;
for(;0u < len;--len)
{
*pTar = *pSrc;
++pTar;
++pSrc;
}
}
void my_memset(void *tar,uint8_t value,uint16_t len)
{
uint8_t uc = value;
uint8_t *pTar;
pTar =(uint8_t*)tar;
for(;0u < len;--len)
{
*pTar = uc;
++pTar;
}
}
/* 64个轮常数 */
static const uint32_t K[64] = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
};
//SHA-256 标准规定:所有多字节数据(如消息块、哈希状态、长度字段)均采用大端字节序(高位字节存储在低地址)
//加载
static uint32_t load32(const uint8_t *p) {
return ((uint32_t)p[0] << 24) |
((uint32_t)p[1] << 16) |
((uint32_t)p[2] << 8) |
((uint32_t)p[3] );
}
//存储
static void store32(uint8_t *p, uint32_t x) {
p[0] = (uint8_t)(x >> 24);
p[1] = (uint8_t)(x >> 16);
p[2] = (uint8_t)(x >> 8);
p[3] = (uint8_t)(x );
}
/* 压缩函数:处理一个64字节块 */
static void sha256_transform(sha256_ctx *ctx, const uint8_t block[64]) {
uint32_t W[16]; /* 环形缓冲区,仅保存最近16个字 */
uint32_t a, b, c, d, e, f, g, h;
uint32_t t1, t2;
int i;
/* 消息扩展:初始加载16个字 */
for (i = 0; i < 16; i++) {
W[i] = load32(block + i * 4);
}
/* 初始化工作变量 */
a = ctx->state[0];
b = ctx->state[1];
c = ctx->state[2];
d = ctx->state[3];
e = ctx->state[4];
f = ctx->state[5];
g = ctx->state[6];
h = ctx->state[7];
/* 主循环:动态计算后续消息字并覆盖最旧的W项 */
for (i = 0; i < 64; i++) {
uint32_t wi;
if (i < 16) {
wi = W[i];
} else {
/* 环形索引:i-2, i-7, i-15, i-16 均对16取模 */
uint32_t s0 = SIG0(W[(i - 15) & 15]);
uint32_t s1 = SIG1(W[(i - 2) & 15]);
wi = s1 + W[(i - 7) & 15] + s0 + W[(i - 16) & 15];
W[i & 15] = wi; /* 覆盖最早的那个字 */
}
t1 = h + EP1(e) + CH(e, f, g) + K[i] + wi;
t2 = EP0(a) + MAJ(a, b, c);
h = g;
g = f;
f = e;
e = d + t1;
d = c;
c = b;
b = a;
a = t1 + t2;
}
/* 更新状态 */
ctx->state[0] += a;
ctx->state[1] += b;
ctx->state[2] += c;
ctx->state[3] += d;
ctx->state[4] += e;
ctx->state[5] += f;
ctx->state[6] += g;
ctx->state[7] += h;
}
/* 初始化 */
void sha256_init(sha256_ctx *ctx) {
ctx->state[0] = 0x6a09e667;
ctx->state[1] = 0xbb67ae85;
ctx->state[2] = 0x3c6ef372;
ctx->state[3] = 0xa54ff53a;
ctx->state[4] = 0x510e527f;
ctx->state[5] = 0x9b05688c;
ctx->state[6] = 0x1f83d9ab;
ctx->state[7] = 0x5be0cd19;
ctx->count = 0;
my_memset(ctx->buffer, 0, sizeof(ctx->buffer));
}
/* 更新数据 */
void sha256_update(sha256_ctx *ctx, const void *data, uint16_t len) {
const uint8_t *bytes = (const uint8_t *)data;
uint16_t buf_off = (uint16_t)(ctx->count % 64);
uint16_t free_space = 64 - buf_off;
/* 累计总字节数(调用者需保证不超出 536870911) */
ctx->count += (uint32_t)len;
/* 如果缓冲区已有未处理数据,且新数据能填满至少一个块 */
if (buf_off != 0 && len >= free_space) {
my_memcpy(ctx->buffer + buf_off, bytes, free_space);
sha256_transform(ctx, ctx->buffer);
bytes += free_space;
len -= free_space;
buf_off = 0;
}
/* 处理完整的64字节块 */
while (len >= 64) {
sha256_transform(ctx, bytes);
bytes += 64;
len -= 64;
}
/* 剩余不足64字节的数据存入缓冲区 */
if (len > 0) {
my_memcpy(ctx->buffer + buf_off, bytes, len);
}
}
/* 完成哈希,输出摘要(完全无 uint64_t 版本) */
void sha256_final(sha256_ctx *ctx, uint8_t digest[32]) {
/* 总位数(低32位),因为 count ≤ 536870911,所以 bit_len < 2^32,高32位为零 */
uint32_t bit_len = ctx->count * 8; /* 安全,不会溢出 */
uint16_t buf_off = (uint16_t)(ctx->count % 64);
uint16_t pad_len;
int i;
/* 先追加 0x80 */
ctx->buffer[buf_off] = 0x80;
/* 需要填充的字节数(使最后剩余56字节) */
if (buf_off < 56) {
pad_len = 56 - buf_off;
} else {
pad_len = 64 + 56 - buf_off;
}
/* 清零填充区(除了已写的0x80) */
for (i = 1; i < pad_len; i++) {
ctx->buffer[buf_off + i] = 0;
}
/* 如果填充跨越了块边界,先处理当前块,再在新块中继续填充 */
if (buf_off + pad_len > 64) {
sha256_transform(ctx, ctx->buffer);
/* 重置缓冲区偏移为0,并清零前56字节(用于放长度) */
for (i = 0; i < 56; i++) {
ctx->buffer[i] = 0;
}
buf_off = 0; /* 此时已在新块开头 */
} else {
/* 未跨越边界,长度直接放在本块末尾 */
/* 但需确保从 buffer[56] 开始写长度,前面的已清零 */
for (i = buf_off + pad_len; i < 56; i++) {
ctx->buffer[i] = 0;
}
}
/* 追加长度(8字节大端,高32位为0) */
ctx->buffer[56] = 0;
ctx->buffer[57] = 0;
ctx->buffer[58] = 0;
ctx->buffer[59] = 0;
ctx->buffer[60] = (uint8_t)(bit_len >> 24);
ctx->buffer[61] = (uint8_t)(bit_len >> 16);
ctx->buffer[62] = (uint8_t)(bit_len >> 8);
ctx->buffer[63] = (uint8_t)(bit_len);
/* 最后压缩一次(此时 buffer 中为完整的填充块) */
sha256_transform(ctx, ctx->buffer);
/* 输出摘要 */
for (i = 0; i < 8; i++) {
store32(digest + i * 4, ctx->state[i]);
}
/* 清除上下文 */
my_memset(ctx, 0, sizeof(sha256_ctx));
}
/* 一次性计算 */
void sha256_compute(const void *data, uint16_t len, uint8_t digest[32]) {
sha256_ctx ctx;
sha256_init(&ctx);
sha256_update(&ctx, data, len);
sha256_final(&ctx, digest);
}
6. 测试向量验证
使用标准测试向量验证实现的正确性(输出应与右侧期望值完全一致):
cpp
#include "sha256.h"
int main(void) {
uint8_t hash_flg = 1u;//运行这段程序后,hash_flg=1则算法正确
uint8_t i;
uint8_t hash[32];
const uint8_t result_hash[32] = {
0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea,0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23,
0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c,0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad};
sha256_compute("abc", 3, hash);
for(i = 0u; i < 32u; i++)
{
if(hash[i] != result_hash[i])
{
hash_flg = 0u;
break;
}
}
return 0;
}
运行结果与期望一致,证明实现正确。
7. ⭐ 重要说明:SHA-256 是否需要设置参数?为什么相同输入总是得到相同输出?
很多初学者会疑惑:SHA-256 是否像 AES 一样需要密钥?或者像 CRC 一样可以选择多项式?答案是否定的。
7.1 SHA-256 是无参数的公开算法
-
没有密钥:SHA-256 是哈希函数,不是加密算法,因此不需要任何密钥或密码。
-
没有可配置的常数 :初始哈希值(
state[0..7])和 64 个轮常数(K[0..63])均由标准 FIPS PUB 180-4 严格规定,任何正确的实现都使用相同的数值。 -
没有可变模式:不同于某些哈希算法(如 BLAKE2 支持个性化参数),SHA-256 只有一种标准行为。
7.2 确定性(Determinism)保证
正因为所有参数都是固定的,对于相同的输入(字节序列),任何符合标准的 SHA-256 实现都会输出完全相同的 256 位哈希值。这一特性至关重要:
-
跨平台验证:Windows 上计算的 SHA-256 与单片机上的结果一致。
-
数字签名:签名方和验证方必须得到相同的哈希,否则签名失效。
-
固件升级:通过比对公开的哈希值即可校验固件完整性。
7.3 常见误解澄清
| 误解 | 事实 |
|---|---|
| "SHA-256 需要一个密钥才能工作" | ❌ 错误。SHA-256 是哈希函数,无密钥。需要密钥的是 HMAC-SHA256。 |
| "不同编程语言的 SHA-256 结果可能不同" | ❌ 错误。只要输入相同(字节值),结果一定相同。 |
| "可以调整轮数或初始值来提高安全性" | ❌ 错误。改变任何参数都会产生一个非标准的哈希,无法与其他人互操作。 |
| "单片机实现可能与 PC 结果不同" | ❌ 错误。只要正确实现(处理字节序、填充),结果完全一致。 |
7.4 如果您需要"带密钥"的哈希
若您需要密钥参与运算,请使用 HMAC-SHA256(基于 SHA-256 的消息认证码),它需要提供一个密钥,但核心的 SHA-256 部分仍然是无参数的。
8. 总结与建议
-
适用场景 :本实现适合 RAM 小于 4 KB、无法使用
uint64_t且数据量不超过 512 MB 的单片机(如 8051、PIC、AVR 等)。 -
性能:每 64 字节块约进行 64 轮运算,在 16 MHz 的 8 位机上约需数十毫秒,足够大部分实时性要求不高的应用。
-
可移植性 :完全使用标准 C,仅依赖
common.h中的内存函数(可轻松替换为string.h)。 -
安全提醒:SHA-256 本身是安全的,但单片机应用中应注意防侧信道攻击(如功耗分析),必要时加入随机延时或掩码。
-
扩展 :如果需要处理超过 512 MB 的数据,请将
ctx->count类型改为uint64_t并相应调整bit_len计算。