嵌入式与Linux软件工程素养:学习资料与实践路线

嵌入式与Linux软件工程素养:学习资料与实践路线

写出一段能够工作的代码并不难,困难的是让代码在资源不足、并发竞争、硬件异常、需求变更和多年维护之后,依然安全、清晰并且容易验证。

对于C、C++、Linux Kernel和RTOS开发者,所谓“工程素养”并不是背诵更多语法,而是逐渐形成一套稳定的判断方式:

1
2
3
4
5
6
场景
-> 约束
-> 风险
-> 可选方案
-> Trade-off
-> 工程选择

本文整理一套长期学习路线:

1
2
3
4
5
6
标准与规范
-> 成熟源码
-> Git历史
-> Patch Review
-> 真实Bug复盘
-> 个人工程原则库

它的目标不是把所有规范从头背到尾,而是提高代码品味、工程判断力和Review能力。

1. 资料应该怎样使用

不同资料解决的问题不同:

资料类型 主要回答的问题
安全编码标准 哪些写法可能产生未定义行为、越界、溢出或资源错误?
嵌入式编码规范 怎样让固件代码更清楚、更可移植、更不容易误用?
C++设计指南 怎样表达所有权、生命周期、接口约束和资源管理?
Linux开发文档 一个可维护、可Review、可合入的Kernel Patch应该是什么样?
Git历史和邮件列表 当前代码为什么这样设计?哪些方案被拒绝过?
Bug复盘 怎样把一次故障转化为可复用的设计原则?

不要试图一次性读完所有资料。更有效的方式是:先建立索引,遇到真实问题时查规则,再回到成熟代码与历史讨论中验证理解。

2. SEI CERT C Coding Standard

2.1 资料入口

在线索引适合按规则搜索,PDF适合建立整体框架。链接和在线内容可能随站点调整,引用具体规则时还应记录所用版本和访问日期。

2.2 重点目录

CERT C按问题类别组织规则,常见目录包括:

1
2
3
4
5
6
7
8
9
10
11
PRE:预处理
DCL:声明和初始化
EXP:表达式
INT:整数
ARR:数组
STR:字符串
MEM:内存管理
FIO:文件I/O
CON:并发
POS:POSIX
ERR:错误处理

对嵌入式、Linux和RTOS开发,建议优先阅读:

  1. MEM:分配、释放、对象生命周期和非法访问;
  2. ERR:失败路径、返回值和错误传播;
  3. INT:整数转换、截断、溢出和有符号性;
  4. EXP:求值顺序、副作用和未定义行为;
  5. ARR:边界、长度计算和指针关系;
  6. CON:数据竞争、同步和可见性。

2.3 怎样用于Review

例如:

1
2
record = AmbaLink_Malloc(size);
record->seq = seq;

Review时不要只说“这里最好判断一下”。应该结构化分析:

1
2
3
4
5
6
资源获取是否可能失败?
失败以后当前代码会发生什么?
调用者能否处理错误?
对象由谁释放?
部分初始化失败时怎样回滚?
错误路径是否会泄漏其他资源?

CERT C帮助你把个人偏好变成可解释的安全理由。

3. Barr Group Embedded C Coding Standard

Barr Group的资料更贴近MCU、RTOS、BSP和Driver开发,强调一致性、可读性、可移植性以及降低固件缺陷。

可以把两类资料这样搭配:

1
2
3
4
5
CERT C
-> 关注安全性、边界、未定义行为和错误条件

Barr Group
-> 关注嵌入式团队日常编码方式和可维护性

规范不是越多越好。团队应该挑选适用规则,说明例外条件,并通过静态检查、Review和测试让规则真正落地。

4. C++ Core Guidelines

即使主要工作仍是C或Kernel,C++ Core Guidelines对理解接口、所有权和生命周期也很有价值。建议重点关注:

1
2
3
4
5
6
7
8
Interfaces
Classes
Resource management
RAII
Error handling
Concurrency
Performance
Templates

阅读时不要停在“什么时候使用class”。更重要的问题是:

1
2
3
4
5
6
谁拥有对象?
借用关系能持续多久?
资源释放是否与控制流绑定?
失败后对象处于什么状态?
接口是否让非法状态难以表达?
并发访问契约在哪里?

RAII的核心不是“使用智能指针”这么简单,而是让资源生命周期与对象作用域形成可验证的关系。

5. Linux Kernel官方开发文档

5.1 Kernel Development Process

它帮助理解Maintainer、邮件列表、Patch提交、Review和版本迭代。Kernel开发不是“代码能编译就提交”,而是一套公开论证与共同维护过程。

5.2 Coding Style

不要只把它当缩进手册。更值得关注的是:

  • 函数应保持清晰并只承担可理解的职责;
  • 错误处理怎样使用goto集中释放资源;
  • typedef何时会隐藏重要类型信息;
  • 注释为什么应解释“为什么”,而不是复述代码;
  • API返回值怎样表达成功、失败和部分结果。

代码风格的价值,是降低团队阅读和Review时的认知切换成本。

5.3 Submitting Patches

重点学习:

1
2
3
4
5
6
一个Patch只做一件逻辑完整的事情
Patch应尽可能小,但不能破坏可验证性
Commit Message解释问题、影响和Why
Fixes标签要指向引入问题的提交
Signed-off-by表达开发者证书声明
根据Review发送v2、v3并记录修改

5.4 Submit Checklist

它适合作为提交前检查框架。团队还可以在此基础上增加自己的硬件矩阵、压力测试、静态分析和回归要求。

6. 不要只读当前源码:学习Git历史

源码告诉你“现在是什么”,历史经常告诉你“为什么变成这样”。

常用命令:

1
2
3
4
5
6
git blame path/to/file.c
git show <commit>
git log -p -- path/to/file.c
git log -L :function_name:path/to/file.c
git log -S'keyword' -- path/
git log -G'regex' -- path/

它们回答的问题不同:

命令 适用问题
git blame 当前某行主要来自哪个提交?
git show 该提交改了什么,Commit Message怎样解释?
git log -p 文件怎样逐步演化?
git log -L 一个函数的历史怎样变化?
git log -S 哪个提交增加或删除了某段精确文本?
git log -G 哪些diff匹配某个正则模式?

推荐调查流程:

1
2
3
4
5
6
7
看到一段奇怪代码
-> 确认当前版本和配置
-> git blame找到提交
-> git show阅读diff与Why
-> 查看前后相关提交
-> 到lore阅读Review讨论
-> 用当前约束重新评价设计

需要注意:git blame只显示最后修改者,不等于最初设计者,也不等于责任归属。

7. Linux Patch与邮件列表

7.1 lore.kernel.org

lore可以看到Patch原始邮件、Reviewer意见、Maintainer取舍以及v1到vN的演进。它特别适合回答:

1
2
3
4
5
为什么当前API这样设计?
曾经有哪些替代方案?
Reviewer担心什么边界?
某个字段为什么被删除或重新初始化?
修复是局部补丁还是接口问题?

例如研究DMAEngine、DW AXI DMAC、direction、cyclic DMA或slave DMA时,应把源码、Git提交和dmaengine邮件列表结合起来。

7.2 Patchwork

Patchwork更适合浏览Patch Series、版本和处理状态。状态只能帮助导航,最终技术理由仍应阅读邮件线程和实际合入提交。

8. Linux Kernel文档总入口

遇到具体子系统时,优先寻找官方文档和代码内Documentation,例如:

1
2
3
4
5
6
7
8
9
10
DMAEngine
Scheduler
Memory Management
Driver API
Locking
RCU
Tracing
Perf
eBPF
DeviceTree

推荐证据优先级:

1
2
3
4
5
目标版本源码与Documentation
-> 对应Git提交
-> 邮件列表Review
-> Maintainer文档和工具输出
-> 高质量二手文章

二手文章适合建立入口,但版本相关的实现结论必须回到目标源码验证。

9. 四阶段学习路线

9.1 第一阶段:代码安全

1
SEI CERT C + Barr Group Embedded C

优先建立:NULL与错误返回检查、长度和数组边界、整数溢出、资源生命周期、完整错误路径以及并发安全。

实践方式:每周选择一个真实Bug,用一条CERT规则和一条团队工程规则解释它。

9.2 第二阶段:Linux工程规范

1
Coding Style + Submitting Patches + Submit Checklist

练习把一个大修改拆成逻辑清楚的Patch Series;为每个Commit Message写清问题、用户影响、根因、修复理由和测试。

9.3 第三阶段:真实源码设计

1
Current Source + Git History + Documentation

每次阅读函数先画调用位置、对象关系和状态变化,再追历史。不要只问“怎样实现”,还要问“哪些约束迫使它这样实现”。

9.4 第四阶段:Patch Review

1
Commit -> lore thread -> v1/v2/v3差异 -> 最终合入版本

重点观察Maintainer真正关心的内容:

  • API是否容易误用;
  • 状态生命周期是否明确;
  • 是否产生stale state;
  • 锁粒度和上下文是否正确;
  • 所有错误路径是否闭合;
  • ABI或用户可见行为是否改变;
  • 是否引入不必要复杂度;
  • 测试是否真正覆盖失败模式。

10. 建立自己的工程原则库

每解决一个真实Bug,固定记录:

1
2
3
4
5
6
7
8
9
现象
-> 触发条件
-> 直接原因
-> Root Cause
-> 违反的工程原则
-> 正确修复
-> Containment与观测改进
-> Regression
-> 可推广边界

10.1 DMA direction stale

1
2
3
4
5
现象:channel被不同client复用后,direction仍保留上一次operation状态。

原则:long-lived object的可变状态必须有明确生命周期,不能依赖旧值恰好正确。

改进:每个prep path显式建立本次operation所需状态;复用边界重置;测试不同方向交替使用。

10.2 malloc未判空

1
2
3
4
5
现象:分配失败后立即解引用。

原则:所有可能失败的资源获取都必须拥有可执行的失败路径。

改进:检查结果、传播错误、回滚已获取资源,并测试内存不足注入。

10.3 RTOS临时线程快速创建销毁

1
2
3
4
5
现象:短生命周期线程高频创建销毁,触发调度器、TCB或资源回收边界问题。

原则:高频工作优先复用长期worker和队列,资源生命周期应与负载模式匹配。

改进:改为固定worker、明确停止协议,并对资源耗尽和并发退出做压力测试。

原则库不是“永远正确的格言”。每条原则都应记录适用条件、反例和Trade-off,避免从一个Bug过度推广。

11. 一套固定的代码Review问题

以后阅读或Review代码,可以依次问:

11.1 需求与约束

1
2
3
4
这段代码解决什么问题?
运行在哪种上下文:进程、中断、原子上下文还是RTOS task?
时间、内存、功耗和实时性约束是什么?
哪些输入来自不可信边界?

11.2 所有权与生命周期

1
2
3
4
谁创建、拥有、借用和释放资源?
对象能否被并发销毁?
异步回调返回时对象还存在吗?
复用对象时哪些字段必须重置?

11.3 错误与恢复

1
2
3
4
每个可能失败的操作都检查了吗?
部分成功后怎样回滚?
超时后如何取消,迟到回调怎样处理?
错误码会不会被覆盖或错误转换?

11.4 并发与状态

1
2
3
4
哪些字段由哪把锁或原子规则保护?
多核可见性和内存顺序是什么?
回调、IRQ和普通线程能否同时访问?
是否存在lost wakeup、double completion或stale state?

11.5 接口与可维护性

1
2
3
4
5
接口是否让非法状态容易表达?
调用者是否容易漏掉必要步骤?
命名是否揭示语义而非实现细节?
能否用更小、更清楚的状态机实现?
日志和trace是否能支持现场诊断?

12. 怎样把资料转化为能力

只收藏链接不会自动产生工程判断力。建议为每个主题执行一个小闭环:

1
2
3
4
5
6
7
8
选择一个真实问题
-> 阅读一条规范
-> 找一个成熟实现
-> 追一个相关提交
-> 阅读一次Review讨论
-> 写一个最小测试或故障注入
-> 形成一条个人原则
-> 在下一次Review中主动使用

例如研究“错误路径漏释放”:

  1. 阅读CERT C资源管理和错误处理规则;
  2. 在Kernel源码中找使用goto清理的驱动probe函数;
  3. 用Git历史查一次泄漏修复;
  4. 阅读Reviewer为什么要求集中清理;
  5. 为自己的模块注入第N步失败;
  6. 检查所有资源是否逆序释放;
  7. 把结论加入Review Checklist。

13. 推荐的长期节奏

不需要每天阅读大量文档,可以保持稳定节奏:

1
2
3
4
每周:精读1条规则 + Review 1个真实函数
每两周:追踪1个提交及其邮件讨论
每月:完整复盘1个Bug并更新原则库
每季度:选择1个子系统做源码、历史、实验闭环

衡量进步的标准不是收藏了多少资料,而是能否:

  • 在代码写下之前识别风险;
  • 在Review时解释问题的后果与边界;
  • 用证据说明设计取舍;
  • 为失败路径设计可执行测试;
  • 把一次Bug转化为团队以后不会重复犯的机制。

14. 总结

嵌入式与Linux软件工程素养来自多种证据的长期结合:

1
2
3
4
5
6
7
CERT C告诉你哪些行为危险
Barr Group帮助建立嵌入式编码习惯
C++ Core Guidelines训练所有权和接口设计
Kernel Documentation给出社区工程规范
Git History解释设计演化
lore和Patchwork展示真实Review取舍
Bug复盘把知识转化为个人与团队原则

最终目标不是记住“什么时候用inline、UINT32、class或汇编”,而是面对一个具体场景时,能够清楚说明:

当前约束是什么,有哪些方案,每种方案牺牲什么,为什么这个选择最合适,以及失败后系统如何安全恢复。


嵌入式与Linux软件工程素养:学习资料与实践路线
https://cj0510.github.io/2026/10/22/Linux Kernel/Debug/嵌入式-Linux-软件工程素养学习资料与实践路线/
作者
CJ1018
发布于
2026年10月22日
许可协议
CC BY-NC-SA 4.0