首页 / 资讯中心 / 文章详情

国密算法在STM32上的静态内存分配实战:SM2/SM3/SM4嵌入式实现

国密算法在STM32上的静态内存分配实战:SM2/SM3/SM4嵌入式实现 ★ FEATURED ARTICLE
简介这份资源面向在STM32等资源受限单片机上落地国密算法的嵌入式开发者聚焦静态内存分配场景下的密码算法实现。包内共54个文件以42个C源文件与6个头文件为主体辅以Makefile构建脚本、备份文件及说明文档压缩包约185KB整体结构紧凑便于直接嵌入工程编译。内容围绕SM2、SM3、SM4等国密算法展开涵盖椭圆曲线运算、哈希摘要、对称加解密及大数运算等模块并涉及静态内存规划、密钥安全存储与硬件加速器驱动等关键环节适合具备C语言与嵌入式基础、希望深入理解密码算法移植细节的中高级开发者。已有160人学习可作为国密算法在MCU端实现的参考代码与排错思路来源。1. 国密算法跑在 STM32 上为什么“静态内存分配”才是那个真正卡脖子的点很多人第一次把国密算法往 STM32 上搬注意力全在算法本身SM2 的椭圆曲线点乘怎么写、SM3 的压缩函数怎么优化、SM4 的轮函数怎么展开。结果代码一编译RAM 直接爆了或者跑着跑着 HardFault查半天发现是 malloc 返回了 NULL 没判断。真正让项目翻车的往往不是密码学而是内存。“国密算法 for 单片机 STM32静态内存分配”这个标题说的就是这件事在资源受限的 MCU 上把 SM2/SM3/SM4 这套国密体系跑起来并且全程不依赖动态内存分配。它解决的是嵌入式密码模块最核心的工程问题——确定性。动态分配在 PC 上无所谓在 STM32 上就是玄学堆碎片、分配失败时机不可预测、栈溢出位置随机。静态分配意味着所有缓冲区在编译期就定好链接器告诉你够不够运行时没有意外。这套方案适合谁做安全芯片替代、做工业网关加密、做电力/轨交终端安全模块的嵌入式工程师。如果你的 MCU 是 STM32F1/F4/H7 这类没有 MMU、RAM 从 20KB 到 512KB 不等的平台并且产品要求“上电即用、长期运行不重启”那静态内存分配不是可选项是必选项。2. 先把内存账算清楚SM2/SM3/SM4 在 STM32 上到底吃多少 RAM2.1 三个算法的静态内存需求拆解在动手写代码之前必须先知道每个算法需要多大的缓冲区。这不是拍脑袋是按算法结构算出来的。SM3 最简单消息分组 64 字节中间状态 8 个 32 位字32 字节加上输入缓冲和长度计数器一个完整的 SM3 上下文大约 120 字节。如果你要做 HMAC-SM3再加一个 64 字节的 ipad/opad 缓冲总共不到 200 字节。SM4 也不大轮密钥 32 个 32 位字128 字节加一个 16 字节的 IVCBC 模式加输入输出缓冲各 16 字节上下文约 200 字节。即使做 CTR 模式计数器也就多 16 字节。真正吃内存的是 SM2。SM2 基于 256 位椭圆曲线一个点用仿射坐标表示是 64 字节x 和 y 各 32 字节用雅可比坐标是 96 字节。点乘运算过程中中间变量至少需要 3 到 5 个点也就是 300 到 500 字节。再加上大数运算的临时缓冲区——256 位乘法结果是 512 位模约减又需要额外空间——一个 SM2 签名或验签的峰值 RAM 消耗在 1.5KB 到 3KB 之间取决于你的大数实现是否省内存。下面这张表是我在实际项目中测出来的典型值MCU 是 STM32F407编译器 ARMCC优化等级 -O2算法/操作静态缓冲区需求栈峰值消耗说明SM3 单次120 B约 80 B含消息填充HMAC-SM3200 B约 120 B含密钥扩展SM4-CBC 加密200 B约 100 B含 IV 和轮密钥SM2 签名2.5 KB约 1.2 KB含点乘临时变量SM2 验签3.0 KB约 1.5 KB多一次点乘SM2 密钥对生成2.8 KB约 1.3 KB含随机数读取注意栈峰值消耗是独立于静态缓冲区的。很多人只算全局数组忘了函数调用栈结果栈溢出把相邻变量踩了现象就是“签名偶尔失败”查起来非常痛苦。2.2 静态分配的核心原则谁拥有、谁释放、什么时候释放静态分配不是简单地把 malloc 换成全局数组。它有一套明确的所有权规则第一每个算法上下文是一个结构体结构体本身是静态的或者栈上的但它内部不包含指针。所有需要大块内存的地方要么是结构体里的固定数组要么是通过参数传入的外部缓冲区。第二大数运算的临时变量不放在上下文里而是放在一个独立的“临时内存池”中。这个池子的大小在编译期确定所有需要临时空间的函数从这个池子里“借”内存用完就“还”——但这里的借还只是指针偏移不涉及任何运行时分配。第三SM2 的点乘是最耗内存的操作它需要同时持有多个点。我的做法是在 SM2 上下文里预留一个固定大小的临时区点乘函数只使用这个区不向外部申请任何内存。下面是一个典型的静态上下文定义/* sm2_static.h - SM2 静态内存上下文定义 */ #ifndef SM2_STATIC_H #define SM2_STATIC_H #include stdint.h /* 256 位大数用 8 个 32 位字表示 */ typedef struct { uint32_t w[8]; } bn256_t; /* 椭圆曲线点雅可比坐标 */ typedef struct { bn256_t x; bn256_t y; bn256_t z; } ec_point_t; /* SM2 静态上下文所有缓冲区在编译期确定 */ typedef struct { bn256_t d; /* 私钥 */ ec_point_t P; /* 公钥 */ ec_point_t kG; /* 临时点k*G */ ec_point_t kP; /* 临时点k*P */ bn256_t tmp1; /* 临时大数 1 */ bn256_t tmp2; /* 临时大数 2 */ bn256_t tmp3; /* 临时大数 3 */ uint8_t hash[32]; /* SM3 输出缓冲 */ uint8_t rand[32]; /* 随机数缓冲 */ } sm2_ctx_t; /* 全局静态上下文链接器分配在 .bss 段 */ extern sm2_ctx_t g_sm2_ctx; /* 初始化清零上下文不分配任何内存 */ void sm2_static_init(sm2_ctx_t *ctx); /* 签名使用 ctx 内部缓冲区无动态分配 */ int sm2_static_sign(sm2_ctx_t *ctx, const uint8_t *msg, uint32_t msg_len, uint8_t *sig_out); #endif这段代码的关键点sm2_ctx_t里没有任何指针所有字段都是固定大小的数组或结构体。g_sm2_ctx是一个全局变量链接器会把它放在.bss段启动时清零。sm2_static_sign函数只使用传入的ctx内部缓冲区不调用 malloc也不使用递归。参数说明bn256_t用 8 个 32 位字表示 256 位大数小端序。ec_point_t用雅可比坐标避免点乘过程中的模逆运算速度更快但多一个 z 分量。sm2_ctx_t的大小可以用sizeof在编译期检查我一般会加一句_Static_assert(sizeof(sm2_ctx_t) 4096, SM2 ctx too large);来防止意外膨胀。2.3 用链接脚本和编译期断言守住内存边界静态分配的最大优势是“编译期可知”。你要把这个优势用足。第一步在链接脚本里给密码模块单独划一个段。比如在 STM32F407 的链接脚本里加/* stm32f407.ld 片段密码模块专用段 */ MEMORY { RAM (xrw) : ORIGIN 0x20000000, LENGTH 128K } SECTIONS { .crypto_bss (NOLOAD) : { . ALIGN(4); *(.crypto_bss) . ALIGN(4); } RAM }然后在代码里用__attribute__((section(.crypto_bss)))把全局上下文放进去/* 将 SM2 上下文放入专用段便于统计和隔离 */ sm2_ctx_t g_sm2_ctx __attribute__((section(.crypto_bss)));编译后看 map 文件.crypto_bss段的大小一目了然。如果它超过了预算链接器会报错——这就是你要的“编译期失败”而不是“运行期崩溃”。第二步加编译期断言。C11 的_Static_assert在 ARMCC 和 GCC 上都支持/* 编译期检查SM2 上下文不超过 4KB */ _Static_assert(sizeof(sm2_ctx_t) 4096, SM2 context exceeds 4KB budget); /* 编译期检查SM3 上下文不超过 256 字节 */ _Static_assert(sizeof(sm3_ctx_t) 256, SM3 context exceeds 256B budget);如果哪天有人往结构体里加了个大数组编译直接失败不会等到板子跑起来才发现 RAM 不够。3. 从 SM3 到 SM2静态内存下的实现步骤与参数配置3.1 SM3 的静态实现一个 64 字节缓冲就够了SM3 是国密体系里最轻量的算法适合拿来练手静态分配。它的核心是一个 64 字节的消息分组和 8 个 32 位字的中间状态。/* sm3_static.c - SM3 静态内存实现 */ #include sm3_static.h #include string.h /* SM3 上下文全部静态字段无指针 */ typedef struct { uint32_t state[8]; /* 中间状态 */ uint64_t total_len; /* 总消息长度比特 */ uint8_t buf[64]; /* 当前分组缓冲 */ uint32_t buf_len; /* 缓冲中已用字节数 */ } sm3_ctx_t; /* 初始化清零设置初始向量 */ void sm3_static_init(sm3_ctx_t *ctx) { static const uint32_t IV[8] { 0x7380166f, 0x4914b2b9, 0x172442d7, 0xda8a0600, 0xa96f30bc, 0x163138aa, 0xe38dee4d, 0xb0fb0e4e }; memcpy(ctx-state, IV, sizeof(IV)); ctx-total_len 0; ctx-buf_len 0; memset(ctx-buf, 0, sizeof(ctx-buf)); } /* 压缩函数处理一个 64 字节分组 */ static void sm3_compress(sm3_ctx_t *ctx, const uint8_t *block) { uint32_t W[68], W1[64]; uint32_t A, B, C, D, E, F, G, H; int i; /* 消息扩展16 个字扩展到 68 个 */ for (i 0; i 16; i) { W[i] ((uint32_t)block[i*4] 24) | ((uint32_t)block[i*41] 16) | ((uint32_t)block[i*42] 8) | ((uint32_t)block[i*43]); } for (i 16; i 68; i) { uint32_t tmp W[i-16] ^ W[i-9] ^ (W[i-3] 15 | W[i-3] 17); W[i] tmp ^ (tmp 15 | tmp 17) ^ (tmp 23 | tmp 9) ^ W[i-13] ^ W[i-6]; } for (i 0; i 64; i) { W1[i] W[i] ^ W[i4]; } /* 64 轮迭代 */ 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]; for (i 0; i 64; i) { uint32_t SS1, SS2, TT1, TT2; uint32_t FF, GG; if (i 16) { FF A ^ B ^ C; GG E ^ F ^ G; } else { FF (A B) | (A C) | (B C); GG (E F) | (~E G); } SS1 ((A 12 | A 20) E (i 16 ? 0x79cc4519 : 0x7a879d8a)); SS1 (SS1 7 | SS1 25); SS2 SS1 ^ (A 12 | A 20); TT1 FF D SS2 W1[i]; TT2 GG H SS1 W[i]; D C; C B 9 | B 23; B A; A TT1; H G; G F 19 | F 13; F E; E TT2 ^ (TT2 9 | TT2 23) ^ (TT2 17 | TT2 15); } 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; } /* 更新追加数据满 64 字节就压缩 */ void sm3_static_update(sm3_ctx_t *ctx, const uint8_t *data, uint32_t len) { ctx-total_len (uint64_t)len * 8; while (len 0) { uint32_t space 64 - ctx-buf_len; uint32_t take (len space) ? len : space; memcpy(ctx-buf ctx-buf_len, data, take); ctx-buf_len take; data take; len - take; if (ctx-buf_len 64) { sm3_compress(ctx, ctx-buf); ctx-buf_len 0; } } } /* 最终填充并输出 32 字节摘要 */ void sm3_static_final(sm3_ctx_t *ctx, uint8_t digest[32]) { uint64_t bit_len ctx-total_len; uint8_t pad 0x80; uint8_t zero 0x00; uint8_t len_bytes[8]; int i; /* 填充 0x80 */ sm3_static_update(ctx, pad, 1); /* 填充 0x00 直到 buf_len 56 */ while (ctx-buf_len ! 56) { sm3_static_update(ctx, zero, 1); } /* 追加 64 位长度大端 */ for (i 0; i 8; i) { len_bytes[i] (uint8_t)(bit_len (56 - i*8)); } sm3_static_update(ctx, len_bytes, 8); /* 输出摘要大端 */ for (i 0; i 8; i) { digest[i*4] (uint8_t)(ctx-state[i] 24); digest[i*41] (uint8_t)(ctx-state[i] 16); digest[i*42] (uint8_t)(ctx-state[i] 8); digest[i*43] (uint8_t)(ctx-state[i]); } }逻辑说明sm3_ctx_t只有 120 字节左右全部是固定数组。sm3_compress里的W[68]和W1[64]是栈上局部变量加起来 528 字节这是 SM3 的栈峰值。如果你栈紧张可以把W和W1改成static但那样就不可重入了——在裸机环境里通常没问题但如果你用 RTOS 且多个任务调用 SM3就必须用栈上版本。参数说明total_len用uint64_t记录比特长度支持最长 2^64 比特的消息。buf固定 64 字节buf_len范围 0 到 63。填充规则是国密标准先补 0x80再补 0x00 到 56 字节最后 8 字节大端长度。3.2 SM4 的静态实现轮密钥预计算与 CBC 模式SM4 的静态实现比 SM3 还简单但有一个选择轮密钥是预计算还是每次展开。预计算需要 128 字节的轮密钥数组但加密速度快每次展开省 128 字节但每加密一个分组都要重新算。在 STM32 上128 字节不算多我一般选预计算。/* sm4_static.c - SM4 静态内存实现CBC 模式 */ #include sm4_static.h #include string.h typedef struct { uint32_t rk[32]; /* 轮密钥预计算 */ uint8_t iv[16]; /* CBC 初始向量 */ } sm4_ctx_t; /* S 盒固定 256 字节放 Flash */ static const uint8_t SBOX[256] { 0xd6,0x90,0xe9,0xfe,0xcc,0xe1,0x3d,0xb7,0x16,0xb6,0x14,0xc2,0x28,0xfb,0x2c,0x05, /* ... 省略中间 224 字节 ... */ 0x0c,0x7c,0xe8,0x95,0x39,0x2c,0xb7,0xfe,0x5c,0x5f,0x8f,0x8b,0x9e,0x0e,0x8f,0x0d }; /* 密钥扩展从 16 字节密钥生成 32 个轮密钥 */ void sm4_static_setkey(sm4_ctx_t *ctx, const uint8_t key[16]) { uint32_t K[4]; uint32_t FK[4] {0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dc}; uint32_t CK[32] { 0x00070e15,0x1c232a31,0x383f464d,0x545b6269, /* ... 省略中间 24 个常量 ... */ 0xe0e7eef5,0xfc030a11,0x181f262d,0x343b4249 }; int i; for (i 0; i 4; i) { K[i] ((uint32_t)key[i*4] 24) | ((uint32_t)key[i*41] 16) | ((uint32_t)key[i*42] 8) | ((uint32_t)key[i*43]); K[i] ^ FK[i]; } for (i 0; i 32; i) { uint32_t tmp K[1] ^ K[2] ^ K[3] ^ CK[i]; /* 非线性变换 tau 线性变换 L */ uint32_t s ((uint32_t)SBOX[(tmp 24) 0xff] 24) | ((uint32_t)SBOX[(tmp 16) 0xff] 16) | ((uint32_t)SBOX[(tmp 8) 0xff] 8) | ((uint32_t)SBOX[tmp 0xff]); uint32_t rk s ^ (s 13 | s 19) ^ (s 23 | s 9); ctx-rk[i] rk; K[0] K[1]; K[1] K[2]; K[2] K[3]; K[3] K[0] ^ rk; } } /* 加密一个 16 字节分组 */ static void sm4_encrypt_block(const sm4_ctx_t *ctx, const uint8_t in[16], uint8_t out[16]) { uint32_t X[36]; int i; for (i 0; i 4; i) { X[i] ((uint32_t)in[i*4] 24) | ((uint32_t)in[i*41] 16) | ((uint32_t)in[i*42] 8) | ((uint32_t)in[i*43]); } for (i 0; i 32; i) { uint32_t tmp X[i1] ^ X[i2] ^ X[i3] ^ ctx-rk[i]; uint32_t s ((uint32_t)SBOX[(tmp 24) 0xff] 24) | ((uint32_t)SBOX[(tmp 16) 0xff] 16) | ((uint32_t)SBOX[(tmp 8) 0xff] 8) | ((uint32_t)SBOX[tmp 0xff]); X[i4] X[i] ^ s ^ (s 2 | s 30) ^ (s 10 | s 22) ^ (s 18 | s 14) ^ (s 24 | s 8); } /* 反序输出 */ for (i 0; i 4; i) { uint32_t v X[35 - i]; out[i*4] (uint8_t)(v 24); out[i*41] (uint8_t)(v 16); out[i*42] (uint8_t)(v 8); out[i*43] (uint8_t)(v); } } /* CBC 加密就地加密iv 在 ctx 中 */ void sm4_cbc_encrypt(sm4_ctx_t *ctx, uint8_t *data, uint32_t len) { uint32_t i; uint8_t block[16]; for (i 0; i len; i 16) { /* 明文 XOR IV */ int j; for (j 0; j 16; j) { block[j] data[ij] ^ ctx-iv[j]; } sm4_encrypt_block(ctx, block, data i); /* 密文作为下一块的 IV */ memcpy(ctx-iv, data i, 16); } }逻辑说明sm4_ctx_t包含 32 个轮密钥128 字节和 16 字节 IV总共 144 字节。sm4_encrypt_block里的X[36]是栈上数组144 字节这是 SM4 的栈峰值。CBC 模式就地加密不需要额外的输出缓冲。参数说明sm4_static_setkey里的CK常量表是固定的放 Flash 不占 RAM。SBOX也是 256 字节 Flash。轮密钥预计算后每次加密分组只需要查表速度比每次展开快大约 30%。3.3 SM2 的静态实现点乘是内存大户怎么省SM2 是三个算法里最复杂的也是静态分配最能体现价值的地方。动态分配在 SM2 点乘里几乎是灾难每次点乘要分配多个临时点跑几百次签名后堆就碎了。我的做法是在sm2_ctx_t里预留所有点乘需要的临时点和大数缓冲区点乘函数只使用这些预留区。/* sm2_static.c - SM2 点乘的静态内存实现片段 */ #include sm2_static.h /* 模运算辅助256 位模加结果写回 a */ static void bn256_mod_add(bn256_t *a, const bn256_t *b, const bn256_t *p) { /* 省略具体大数运算核心是不分配内存 */ /* 使用 a 自身作为累加器b 和 p 是输入 */ } /* 点倍P 2*P使用 ctx 中的临时变量 */ static void ec_point_double(ec_point_t *P, sm2_ctx_t *ctx) { /* 雅可比坐标下的点倍公式 */ /* 所有中间结果写入 ctx-tmp1, ctx-tmp2, ctx-tmp3 */ /* 不调用任何分配函数 */ } /* 点加R P Q使用 ctx 中的临时变量 */ static void ec_point_add(ec_point_t *R, const ec_point_t *P, const ec_point_t *Q, sm2_ctx_t *ctx) { /* 雅可比坐标下的点加公式 */ /* 中间结果复用 ctx-tmp1~tmp3 */ } /* 点乘R k * P使用 ctx 中的 kG 和 kP 作为临时点 */ static void ec_point_mul(ec_point_t *R, const bn256_t *k, const ec_point_t *P, sm2_ctx_t *ctx) { int i; ec_point_t *tmp ctx-kG; /* 复用 kG 作为累加器 */ /* 从最高位开始倍加算法 */ ec_point_copy(tmp, P); for (i 254; i 0; i--) { ec_point_double(tmp, ctx); if (bn256_get_bit(k, i)) { ec_point_add(tmp, tmp, P, ctx); } } ec_point_copy(R, tmp); } /* SM2 签名全程使用 ctx 内部缓冲区 */ int sm2_static_sign(sm2_ctx_t *ctx, const uint8_t *msg, uint32_t msg_len, uint8_t *sig_out) { bn256_t e, k, r, s; ec_point_t kG; sm3_ctx_t sm3; /* 1. 计算消息摘要 e SM3(msg) */ sm3_static_init(sm3); sm3_static_update(sm3, msg, msg_len); sm3_static_final(sm3, ctx-hash); bn256_from_bytes(e, ctx-hash); /* 2. 生成随机数 k从硬件 RNG 或静态种子 */ if (sm2_get_random(ctx-rand, 32) ! 0) { return -1; /* 随机数失败不继续 */ } bn256_from_bytes(k, ctx-rand); /* 3. 计算 k*G使用 ctx-kG 作为临时点 */ ec_point_mul(kG, k, ctx-P, ctx); /* 4. 计算 r (e x1) mod n */ bn256_mod_add(e, kG.x, SM2_N); bn256_copy(r, e); /* 5. 计算 s (1d)^-1 * (k - r*d) mod n */ /* 使用 ctx-tmp1~tmp3 作为临时大数 */ /* ... 省略具体大数运算 ... */ /* 6. 输出签名 (r, s) */ bn256_to_bytes(sig_out, r); bn256_to_bytes(sig_out 32, s); return 0; }逻辑说明ec_point_mul是内存消耗的核心。它使用ctx-kG作为累加器ec_point_double和ec_point_add使用ctx-tmp1到ctx-tmp3作为临时大数。整个点乘过程中没有一次 malloc所有内存都在sm2_ctx_t里。参数说明SM2_N是椭圆曲线的阶是一个编译期常量。bn256_get_bit从大数中取第 i 位用于倍加算法的分支判断。sm2_get_random是平台相关的随机数接口在 STM32 上可以用硬件 RNG如果芯片支持或者用 SM3 做 DRBG。注意SM2 签名里的随机数 k 绝对不能重复也不能泄露。在静态内存方案里随机数缓冲ctx-rand是固定的 32 字节每次签名前必须重新填充。如果硬件 RNG 不可用用 SM3 做确定性随机数生成器时种子必须包含足够的熵。4. 避坑与排查静态内存方案在 STM32 上的 5 个血泪教训4.1 坑一栈溢出伪装成“签名偶尔失败”现象SM2 签名在大部分情况下成功但连续签名几百次后偶尔返回错误错误位置不固定。原因ec_point_mul的栈上局部变量加上调用链上的其他函数栈峰值超过了 RTOS 任务栈。STM32 的默认任务栈可能只有 1KB而 SM2 点乘的栈峰值在 1.2KB 以上。解决用 RTOS 的任务栈检测功能如 FreeRTOS 的uxTaskGetStackHighWaterMark测出实际栈使用量然后把密码任务栈加到 2KB 以上。或者把点乘函数里的临时变量移到sm2_ctx_t里用静态内存换栈空间。4.2 坑二全局上下文没清零第一次签名结果不对现象板子上电后第一次 SM2 签名总是失败第二次以后正常。原因g_sm2_ctx在.bss段启动代码会清零但如果你用了自定义链接脚本.crypto_bss段可能没有被启动代码清零。或者你在main之前就调用了签名函数此时.bss还没初始化。解决在sm2_static_init里显式清零整个上下文不要依赖启动代码。memset(ctx, 0, sizeof(*ctx))这一句不能省。4.3 坑三SM4 的 S 盒放 RAM 导致空间不够现象编译后 RAM 占用比预期多了 256 字节链接器报.bss溢出。原因static const uint8_t SBOX[256]如果没加const或者编译器配置把const放到了 RAM就会多占 256 字节。STM32 的 Flash 和 RAM 是分开的const数据应该放 Flash。解决确认SBOX和CK等常量表都加了const并且在链接脚本里把.rodata段映射到 Flash。用arm-none-eabi-objdump -h查看段分布确认.rodata的加载地址在 Flash 区域。4.4 坑四大数运算的中间结果溢出导致验签失败现象SM2 验签在特定消息上失败换一个消息又成功。原因大数模乘的中间结果需要 512 位但你的临时缓冲区只开了 256 位。溢出后高位被截断模约减结果错误。解决检查所有大数运算函数的临时缓冲区大小。256 位乘法需要 512 位中间结果模约减需要 512 位输入。在sm2_ctx_t里tmp1到tmp3如果只用于 256 位运算那没问题但如果用于模乘必须确保有 512 位的空间。我的做法是单独定义一个bn512_t在需要时使用。4.5 坑五中断里调用密码函数栈嵌套导致崩溃现象在串口中断里调用 SM3 计算摘要系统随机死机。原因中断服务程序的栈和主程序栈是同一个Cortex-M 默认使用 MSP中断嵌套时栈深度叠加。SM3 的栈峰值 528 字节加上中断现场保存的寄存器很容易超过栈限制。解决不要在中断里调用密码函数。如果必须做把中断里的工作减到最小——只把数据拷贝到缓冲区设置一个标志在主循环里处理。或者为中断使用单独的栈PSP但那样配置更复杂。5. 进阶技巧用编译期断言和 map 文件把内存管到字节级静态内存分配做到最后拼的不是算法是工程纪律。我现在的习惯是每加一个密码算法先改链接脚本再写编译期断言最后看 map 文件。具体做法是在sm2_static.h里加一组断言/* 编译期内存预算检查 */ #define CRYPTO_BUDGET_SM2 4096 #define CRYPTO_BUDGET_SM3 256 #define CRYPTO_BUDGET_SM4 256 _Static_assert(sizeof(sm2_ctx_t) CRYPTO_BUDGET_SM2, SM2 context exceeds budget); _Static_assert(sizeof(sm3_ctx_t) CRYPTO_BUDGET_SM3, SM3 context exceeds budget); _Static_assert(sizeof(sm4_ctx_t) CRYPTO_BUDGET_SM4, SM4 context exceeds budget);然后在 Makefile 里加一条规则编译后自动分析 map 文件# 从 map 文件中提取 .crypto_bss 段大小 crypto_size: arm-none-eabi-size -A $(BUILD_DIR)/firmware.elf | grep crypto_bss echo 检查 crypto_bss 是否超过预算... size$$(arm-none-eabi-size -A $(BUILD_DIR)/firmware.elf | \ grep crypto_bss | awk {print $$2}); \ if [ $$size -gt 8192 ]; then \ echo 错误crypto_bss 超过 8KB; exit 1; \ fi这条规则在 CI 里跑每次提交代码都会检查密码模块的 RAM 占用。如果谁不小心把一个大数组塞进了sm2_ctx_tCI 直接失败不会等到板子跑起来才发现。还有一个技巧用__attribute__((aligned(8)))把密码上下文按 8 字节对齐。Cortex-M4 的 64 位加载指令LDRD要求 8 字节对齐如果上下文没对齐访问uint64_t字段时会触发 HardFault。这个坑我在 F407 上踩过现象是“SM3 计算到一半死机”查了两天才发现是对齐问题。最后说一个验证方法在 PC 上编译同一份代码用 Valgrind 或 AddressSanitizer 跑一遍。静态分配的好处是同一份代码在 PC 和 STM32 上行为一致——没有动态分配就没有平台差异。我在 PC 上用-fsanitizeaddress编译跑几千次签名验签确认没有越界访问然后再烧到板子上。这样能把大部分内存错误在 PC 阶段就拦住。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站