ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从ff.c到invalid signature:嵌入式文件系统与固件签名全解析

从ff.c到invalid signature:嵌入式文件系统与固件签名全解析 前两天在 Gitee 上翻一个嵌入式开源项目仓库地址是 wuming/fatfs点进去看的是 ff.c 这个文件浏览器标题栏里挂着一串 signature8078a4ab63c88f9c690526bc05ffbacc。这串字符第一次看像是随机乱码实际上它是 Git 体系里的提交哈希也是代码托管平台用来标识文件精确版本的一种签名凭据。很多人扫一眼就过去了但这串 hash 背后牵扯的东西挺深代码怎么追溯、下载的文件怎么校验、固件签名怎么防篡改甚至和安全启动报错 invalid signature detected 都有关系。这篇文章就从这串哈希和 ff.c 切入把 FatFs 这个嵌入式文件系统、代码版本签名、固件安全校验这些事一次性捋清楚。不管你是刚接触单片机的学生还是做产品级固件开发的工程师都能从里面拿到能直接落地的经验。1. signature 哈希与 ff.c从一次仓库浏览说起1.1 这串 signature8078a4... 到底是什么在 Gitee 或者 GitHub 上打开一个文件页面时URL 里经常带一个 hash 参数页面标题或者浏览器标签就可能出现 signature8078a4ab63c88f9c690526bc05ffbacc 这样的东西。这串字符通常是 Git 的 commit SHA-1 值用来向服务器请求对应文件的那个精确历史版本。8078a4ab63c88f9c690526bc05ffbacc 是 40 位十六进制数可以把它理解为文件版本的身份证号。Git 的提交哈希是由当前提交的内容、父提交哈希、作者信息、时间戳等一起计算出来的哪怕只改动一个空格哈希也会完全不同。所以看到 signature8078a4...就能确定你看到的 ff.c 是仓库里的哪一个具体版本不会和其他人改过的同名文件混淆。这个机制对团队协作和代码审查非常重要。在 FatFs 这类 C 语言嵌入式项目里源码文件的版本差异往往是灾难性的。两个看起来一样的 ff.c可能一个打了长文件名补丁一个没有一个适配了 64GB 的 SDXC 卡一个只能读 2GB 的普通 SD 卡。pull 代码后如果不去核对 commit hash等编译报错或者读写文件出问题时定位问题的成本会高出好几倍。1.2 ff.c 在 FatFs 项目里扮演的角色FatFs 是 ChaN 开发的一套面向嵌入式系统的 FAT/exFAT 文件系统模块纯 C 编写不依赖操作系统可以跑在裸机上也可以跑在 RTOS 上。wuming/fatfs 这个仓库是一个典型的个人维护型仓库大概率是从上游源码 fork 或者按项目需求裁剪出来的里面最重要的文件就是 ff.c。ff.c 是整个 FatFs 库的核心实现文件。它包括了 FAT 文件系统的卷管理、目录项解析、文件分配表操作、文件读写缓冲、长文件名支持、exFAT 扩展、磁盘格式化等全部逻辑。拿一辆车来类比ff.c 是变速箱负责把“FAT 格式”这个传动逻辑转换成实际的行进动作而底层 diskio.c 是轮胎真正接触地面直接操作 SD 卡、U 盘或者 Flash 芯片。很多人把 FatFs 当成一个简单的文件读写库其实它的实现相当精巧。比如 f_open 时为了找到目标文件必须逐级解析目录项比较文件名、扩展名、属性、起始簇号f_read 时要根据当前簇号查找 FAT 表拿到下一个簇号处理跨簇读写和文件指针回绕f_write 时还要处理簇分配、FAT 表更新、目录项时间戳刷新。这些逻辑全部压缩在几千行的 ff.c 里所以理解 ff.c 的结构比单纯调用 API 重要得多。2. FatFs 的结构与 ff.c 的核心逻辑2.1 FatFs 为什么是嵌入式文件系统里的常青树十多年过去了FatFs 依然是嵌入式领域使用率最高的文件系统方案之一。原因其实很朴素FAT/exFAT 格式通用性极强。PC、相机、车载播放器、手机 OTG 都能直接识别SD 卡插到读卡器上Windows 和 macOS 都能直接读写。产品如果选择 ext4 或者自定义文件系统用户把 SD 卡拔出来插电脑上什么都看不到售后问题能烦死你。FatFs 的体积和裁剪能力也很出色。配置项集中在 ffconf.h 里用宏开关控制功能。一个只读、不带长文件名、不带格式化功能的最小配置ROM 占用可以压到 10KB 左右RAM 占用几百字节就够。对 Flash 和 RAM 资源紧张的单片机来说这是决定性优势。我见过不少团队在项目初期犹豫要不要上文件系统觉得用 Flash 驱动直接管理块设备就行。结果写到后面坏块管理、磨损均衡、日志覆盖、掉电恢复每一个都是大坑。FAT 是个很成熟的方案FatFs 把绝大部分细节都封装好了直接用反而省事。2.2 ff.c 里的核心 API 与它们背后的机制ff.c 对外暴露的 API 不多但每一个都有对应的工作机制。新手最常见的误区是照搬示例代码却不理解 API 的前置条件。API作用常用注意事项f_mount注册/注销一个工作区只是注册逻辑卷不一定会触发底层初始化需要配合 FF_FS_REENTRANT 等配置f_open打开或创建文件返回 FR_OK 才算成功打开模式FA_OPEN_EXISTING/FA_CREATE_ALWAYS 等决定文件是否存在时的行为f_read读取文件数据返回值小于请求字节数时要么到了文件尾要么底层读错误f_write写入文件数据短写时不要忽略返回值剩余的字节要自己处理缓冲和重试f_lseek移动文件指针配合 FF_USE_EXPAND 可以快速创建稀疏文件f_sync把缓存刷到磁盘必须在关键日志数据落盘后调用否则掉电会丢数据f_mkfs格式化逻辑卷格式化会破坏所有数据调用前必须做状态检查f_mount 是一个比较容易误用的函数。它不是“挂载 SD 卡”这个物理动作而是把一个 FATFS 结构体和逻辑卷编号绑定在一起。真正触发磁盘初始化和读取分区表通常是在第一次 f_open 或者其他操作时由 ff.c 内部的 mount_volume 完成的。想要立即访问磁盘可以在挂载后调用 f_getfree 来显式触发。f_write 的短写问题也值得注意。往 SD 卡写数据时如果底层返回错误或者人为中断f_write 返回的写入字节数会小于请求字节数。很多项目只在最后检查返回值结果中间部分的文件内容已经损坏了。我自己的习惯是写日志时每写一页就检查一次返回值宁可慢一点也不要事后花几天时间排查数据损坏。3. 从哈希到签名代码追溯与固件安全3.1 用 commit hash 追溯 ff.c 的代码变更拿到 signature8078a4ab63c88f9c690526bc05ffbacc 这样的 commit hash实际能做的事比你想象的多。比如最近有人报告 ff.c 里 fatfs 在处理 64GB 以上 exFAT 卡时格式化失败你就可以先用 git log 找出最近修改磁盘布局相关的提交再用 git show 8078a4a 查看具体改动。常用命令组合如下git show 8078a4ab63c88f9c690526bc05ffbacc --stat git show 8078a4a -- ff.c git blame ff.c -L 120,180 git log --oneline --follow -- ff.cgit blame 最适合定位某一行代码是谁在什么时候写的。比如看到 ff.c 里某个缓存对齐的宏特别奇怪先用 blame 找到提交再通过提交信息和代码注释理解当时的背景。我在做固件稳定性分析时这个方法帮我找到了不少上游已经踩过的坑。如果这个仓库是 fork 出来的commit hash 还能帮你对比上游。先给本地仓库添加上游远程然后 git fetch upstream再用 git merge-base 或者 git diff 找出本地分支相对上游的偏离程度评估是否需要同步更新。3.2 下载源码后如何校验文件完整性Gitee 或 GitHub 上某个仓库的 zip 包下载下来之后无法直接确认它是不是原封不动被打包的。尤其是嵌入式工程师经常从各种渠道拿源码群里传的压缩包、网盘链接、第三方转载。代码被植入恶意逻辑或者中间被改动的情况虽然不多但真要碰上一次就够受的。解决办法是校验哈希。Git 仓库里每个 commit 都有 SHA-1文件内容变了哈希必变另外发布方通常会提供 SHA-256 校验值下载后可以用工具比对Linux 下校验单个文件sha256sum ff.cWindows PowerShell 下用Get-FileHash .\ff.c -Algorithm SHA256我通常的做法是在 CI 脚本里加一个 checkout 后计算源码目录整体哈希的步骤再保存一份基准值。以后任何人改了源码、补丁没打全、文件被误替换CI 一下子就能抓出来。这个习惯对追求可复现构建的团队帮助很大。3.3 从 Git 哈希到 Secure Boot 的 invalid signature detectedGit 的 commit hash 本质上是一种完整性校验和防篡改手段只是它用的算法是对称的 SHA-1密钥信息不参与计算所以任何人都能伪造一个“作者名相同”的提交。安全的固件签名升级逻辑是一样的但加上了非对称密钥。热词里那条 invalid signature detected check secure boot policy指的是 UEFI Secure Boot 或类似安全启动机制在引导时发现引导加载程序、内核或驱动程序的数字签名与策略库不匹配。设备只信任由被授权私钥签名的代码如果签名无效启动流程直接中断避免恶意固件在系统最早期运行。这和 FatFs 关系很紧密。很多嵌入式产品把固件、配置、资源文件存放在 SD 卡或者 eMMC 上通过 FatFs 读取启动时校验固件镜像的签名。如果 SD 卡被换掉或者固件包在 PC 上被重新打包、签名丢了、哈希变了设备启动时就会报 invalid signature detected。搞嵌入式的人看到这个错误第一反应应该是“启动链路上的签名验证没过”而不是急着查文件系统代码。4. 手把手移植 FatFs从 diskio 到业务代码4.1 移植前要准备的文件与关键配置从一个新的 MCU 工程开始移植 FatFs其实没有想象的复杂。核心文件就这几个ff.hFatFs 对外的头文件包含 API 声明和数据类型定义ff.c核心实现ffconf.h功能配置diskio.h / diskio.c底层磁盘接口可选ffunicode.cUnicode 支持长文件名必需拿到 wuming/fatfs 这种仓库之后第一步是先把 ffconf.h 里的配置看清楚。常见要动的宏有#define FF_USE_LFN 2 // 长文件名支持0 关闭1 静态缓冲区2 动态分配 #define FF_FS_MINIMIZE 0 // 裁剪级别0 是全部功能 #define FF_USE_MKFS 1 // 格式化功能量产工具或恢复功能会用到 #define FF_USE_STRFUNC 1 // 字符串辅助函数 #define FF_FS_EXFAT 1 // exFAT 支持 #define FF_VOLUMES 1 // 逻辑卷数量FF_USE_LFN 建议直接开到 2用动态分配的方式RAM 占用可控中文文件名也能支持。如果只处理 ASCII 文件名开 0 可以省掉一部分代码。但要注意开关改动会影响 ff.c 的编译路径同一个源码不同配置编译出来的行为完全不一样。4.2 底层 diskio 接口的实现要点diskio.c 是移植工作的主战场。ff.c 不直接调用任何硬件函数它只依赖 diskio 层的 6 个接口DSTATUS disk_initialize(BYTE pdrv); DSTATUS disk_status(BYTE pdrv); DRESULT disk_read(BYTE pdrv, BYTE* buff, LBA_t sector, UINT count); DRESULT disk_write(BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count); DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void* buff);这里最容易踩的坑是 buff 的对齐。很多 SDIO 外设要求数据缓冲区 4 字节对齐甚至有些 DMA 控制器要求 32 字节对齐。ff.c 内部有一个 FF_MIN_SS 宏控制的内部扇区缓冲但传入 disk_read 的 buff 地址并不保证对齐到外设要求。所以实际驱动里强烈建议准备一个对齐好的中转缓冲区或者在 FatFs 配置里让内部缓冲区结构体对齐。还有超时和重试。SD 卡在擦写繁忙时会返回忙状态驱动必须做超时判断和重试否则偶尔出现卡车、掉数据。一位同事做的一款采集设备一开始 disk_read 没有重试机制结果读卡在某个特定簇位置偶尔返回错误最后查出来就是因为没有处理 SD 卡忙标志。4.3 一个完整的日志写入示例移植完成之后业务代码调用 FatFs 的套路大致如下。以给 SD 卡写一个日志文件为例FATFS fs; FIL file; FRESULT res; res f_mount(fs, , 1); // 挂载逻辑卷空字符串表示默认卷 if (res ! FR_OK) { printf(mount failed: %d\n, res); return -1; } res f_open(file, data.log, FA_OPEN_ALWAYS | FA_WRITE); if (res ! FR_OK) { printf(open failed: %d\n, res); return -1; } f_lseek(file, f_size(file)); // 定位到文件末尾实现追加 res f_write(file, hello\n, 6, bw); if (res ! FR_OK || bw ! 6) { printf(write failed\n); } f_sync(file); // 日志关键处要落盘 f_close(file); f_mount(NULL, , 0); // 卸载卷f_sync 平时可以隔一段时间调一次但关键日志、参数配置写入后必须调用。f_close 本身也会刷新缓冲但如果在 f_close 前断电数据就没了。嵌入式产品里这种问题被用户感知到的概率很低但一旦被用户抓到一次丢记录口碑就坏了。4.4 挂载失败怎么排查f_mount 返回 FR_NO_FILESYSTEM 是新手遇到最多的问题。这不一定是 FatFs 的问题更常见的是 SD 卡没有正确格式化或者底层读扇区读取失败。排查顺序我一般这样走先用底层接口 disk_read 直接读第 0 扇区能读出来再继续排查。读不出来就是物理层问题检查接线、SDIO 初始化、卡电压。看看第 0 扇区的最后两个字节是不是 0x55 0xAA。不是的话说明这不是一个标准 MBR 或引导扇区。如果 0 扇区是 MBR再解析 0x1BE 处的分区表找到第一个分区的起始 LBA确认分区类型是不是 0x0B、0x0C 或 0x07。FatFs 的 FF_USE_MKFS 打开后可以直接用 f_mkfs 重新格式化代价是数据全丢。另外检查 f_mount 的第三个参数 opt。设为 1 表示立即挂载挂载失败会直接返回错误设为 0 只是注册卷真正的挂载要等第一次文件操作时触发。很多代码里 f_mount 返回值已经被忽略导致后面 f_open 报错时完全不知道根因调试起来很绕。5. invalid signature detected 背后签名机制的完整链路5.1 Secure Boot 策略检查为什么会报签名错误Secure Boot 的核心思想是“信任链”。设备通电后最先执行的固件比如 ROM 代码或 BootROM验证下一级引导程序的签名验证通过才运行它下一级再验证下一级直到操作系统内核。每一级的策略库里存了一批受信任的公钥或者证书只有私钥签名的代码才能通过验证。当你看到 invalid signature detected check secure boot policy意味着在信任链的某个节点上当前要被加载的镜像签名和策略库不匹配。可能原因包括镜像被篡改、签名证书过期或吊销、策略库里没有对应的公钥、密钥数据库db/dbx配置错误等。这类问题不是 FatFs 造成的但如果你用 FatFs 从 SD 卡读取待校验的固件就需要同时拉通这两个模块来排查。在嵌入式 Linux 板卡上从 SD 卡启动系统是很常见的。SD 卡里的 bootloader、内核、设备树、rootfs 各自都可能被做 hash 或签名校验。只要卡上的内容和生成时的基准不一样启动就会中断。校验机制越严格对发布和部署流程的要求就越高。5.2 系统盘更换后为什么签名会失效热词里“系统盘更换”和 invalid signature detected 会同时出现这件事很好理解。系统的引导加载程序或者 UEFI 固件在做安全启动时会验证下一次启动所用的引导入口、启动文件甚至启动扇区。原来的系统盘上存有受信任的引导文件和对应的签名记录而你换了一块新硬盘或者新 SD 卡上面没有原厂签名的引导镜像或者分区 UUID、ESP 分区内容变了签名验证自然过不了。处理思路不是绕过校验而是重新建立可信关系备份原系统盘中的密钥、证书、签名工具和哈希基准值。新盘安装完系统后重新对 bootloader 和内核镜像做签名。在固件设置里把新盘对应的公钥注册进 Secure Boot 的数据库。验证启动链路的每一步确保最终策略库里只信任自己掌握私钥的签名。如果设备是量产产品系统盘更换频繁一定要把签名的产物、密钥的导入导出操作固化到工具链里。我用过不少开发板少数厂家会在烧录工具里内置自动生成密钥对和签名流程大部分还是靠工程师手工操作这也是量产现场最容易出错的一环。5.3 应用层签名错误给嵌入式开发的启发热词里还有两条微信支付提示用户态签名 signature 错误以及 unable to reset stream after calculating aws4 signature。这两个虽然属于应用层但和嵌入式固件签名有完全一致的底层逻辑签名是算法、密钥、数据、时间戳、随机数这几个要素共同作用的结果任何一边不匹配签名就失败。微信支付的签名错误多半是商户密钥填错、参与签名的参数集合顺序不一致或者编码格式不统一。AWS4 signature 报 unable to reset stream after calculating aws4 signature通常是因为签名计算时要读取请求体而请求流被消费了一次之后没有重置服务端拿到的数据为空或已偏移。本质上都是“签名数据和实际发送数据不一致”。放到嵌入式场景里对应的就是固件镜像在签名时使用的原始二进制和运行时读取到的二进制但凡有一个字节差异签名校验就会失败。所以我在工程里特别强调每次构建生成的产物要有哈希记录构建脚本和签名脚本使用同一个输入文件并且把哈希值写进构建日志。保证“签名什么就运行什么”是所有签名系统的死规矩。6. 我在实际项目里踩过的坑6.1 FatFs 相关的坑第一个坑是字节对齐。某款 STM32 的 SDIO 外设在开启 DMA 模式时要求数据缓冲区 4 字节对齐而 ff.c 内部的文件缓冲区是一个 char 数组地址不一定对齐到 4 字节。用了一段时间之后偶发出现 FR_DISK_ERR排查了很久才发现是对齐问题。后来直接在底层 disk_read 内部做了一次缓冲拷贝问题就消失了。第二个坑是长文件名和中文目录。配置里没有把 FF_USE_LFN 打开时FatFs 只能用 8.3 短文件名超过长度会返回 FR_INVALID_NAME。有一次在日志文件命名上用了时间戳加设备编号长度刚好超了文件一直创建不成功报错格式又不直观最终靠打印文件名长度才定位。想省心直接开 FF_USE_LFN 2并且注意把 ffconf.h 里的代码页设置成支持中文的编码比如 GBK 或 UTF-8。第三个坑是掉电保护。设备在写文件过程中突然断电FAT 表可能出现不一致表现为文件目录项还在但数据读不出来。FatFs 本身没有日志式文件系统那种很强的掉电恢复能力所以关键数据要在业务层做冗余写入或者双备份。比如存配置信息时先写一个副本校验校验值后再覆盖主文件。这套机制能让产品在意外断电场景下依然稳定。第四个坑是簇大小和性能。对 64GB 卡来说如果格式化时簇大小是 32KB读写的块就比较大频繁写小文件会浪费空间。反过来簇太小大文件读写时需要频繁跨簇更新 FAT 表性能会明显变差。量产前一定要根据场景做一次写满、读回、校验的性能测试再决定格式化参数。6.2 签名与版本管理相关的建议版本管理和签名校验这两个实践我是强烈建议从一开始就做好的。代码层面无论是自己维护 FatFs 还是使用上游源码都要记录你基于哪个 commit hash 做的修改。可以在工程根目录放一个 VERSION 文件里面写上游仓库地址、上游 commit hash、本地补丁描述。这样两三年后回头看代码还能快速定位和上游的差异也方便同步上游的 bug 修复。签名层面私钥永远不要进仓库。签名用的私钥单独放在签名服务器或者安全存储介质里公钥可以随固件发布或者预埋在设备里。换盘、升级、失败恢复这些场景都要跑一遍签名校验流程并且把校验结果输出到日志里。别等到现场出现 invalid signature detected 再去手动排查那种情况下的压力真的很大。最后再说一个我自己保留的习惯每次拿到一个新的 FatFs 版本先在本地用 git clone 拉下来再 git log 看提交历史然后用 sha256sum 记录所有源码文件的哈希。这个过程不超过十分钟却能让我对“这个版本从哪里来、改动过什么、能不能用”心里有数。代码世界里的任何一次“签名验证失败”大概率都是链条中间某个环节没有严格记录导致的。养成凡事留版本、留哈希、留签名的习惯能帮你省掉不少深夜排障的烦恼。
返回列表