嵌入式 SHA-256 完全实现(附原码)(无 uint64_t,减少栈使用)

原码见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)

  1. 先附加一个 0x80 字节;

  2. 再附加若干个 0x00,使得总长度 ≡ 56 (mod 64);

  3. 最后附加 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.hsha256.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 计算。

相关推荐
wuminyu2 小时前
专家视角看Java的线程是如何run起来的过程
java·linux·c语言·jvm·c++
进击的小头2 小时前
第12篇:嵌入式核心外设科普:ADC_DAC模拟前端接口原理与典型应用
单片机·嵌入式硬件
码农的神经元2 小时前
2026 MathorCup 选题建议:A/B/C/D/E 题到底怎么选?
c语言·开发语言·数学建模
CHANG_THE_WORLD2 小时前
PE文件解析器详细文档
stm32·单片机·嵌入式硬件
Z文的博客2 小时前
SLCAN工程搭建与实现教程(下)
stm32·单片机·嵌入式·can
聆风吟º3 小时前
【C标准库】深入理解C语言strcmp函数:字符串比较的核心用法
c语言·开发语言·库函数·strcmp
weixin_446023563 小时前
C语言入门:发展历程与编程应用
c语言·基础知识·发展历程·语法结构·编程应用
老师用之于民3 小时前
【DAY39】Linux 驱动开发关键技术研究:设备树、Input 子系统与 I2C 通信
单片机·嵌入式硬件
良木生香3 小时前
【C++初阶】:泛型编程的代表作---C++初阶模板
c语言·开发语言·数据结构·c++·算法