前言
在汽车工业、航空航天、医疗设备等高安全性和高可靠性要求的嵌入式系统领域,软件的质量至关重要。代码中的细微缺陷都可能导致严重的后果。为了解决嵌入式 C 语言开发中常见的安全性和可靠性问题,汽车产业软件可靠性协会 (MISRA) 发布了 MISRA C 编码标准。
1. MISRA C 概述
MISRA C (Motor Industry Software Reliability Association C) 是汽车工业 C 编码标准的缩写,由 MISRA 协会发布。其目标是为嵌入式系统中的 C 语言开发提供一套严格的编码规范,旨在:
- 提升代码可靠性 (Reliability): 减少因编码错误导致的程序缺陷,提高系统运行的稳定性。
- 提升代码可读性 (Readability): 统一代码风格,使代码更易于理解和维护,降低维护成本。
- 提升代码可移植性 (Portability): 减少对特定编译器或硬件平台的依赖,增强代码在不同环境下的适应性。
- 提升代码可维护性 (Maintainability): 规范的代码结构和风格,降低代码维护和升级的难度。
- 提升代码安全性 (Safety): 避免潜在的安全漏洞,保障系统运行的安全。
MISRA C:2012 是该标准的第三版,于 2012 年发布,是对之前版本的重要升级和完善。它整合了之前的 AMD1、TC1 修订内容,并持续进行更新以适应新的技术发展和安全挑战。虽然 MISRA C 标准并不能 100% 保证程序零缺陷,但它能显著降低因编程错误引入问题的风险,是提升嵌入式系统软件质量的有效手段。
2. MISRA C:2012 的规则体系
MISRA C:2012 标准并非简单的代码风格指南,而是一套严谨的规则体系,它包含 指令 (Directives) 和 规则 (Rules) 两大类。
2.1 指令 (Directives)
指令 (Directives) 主要关注代码的组织、构建和文档等方面,旨在建立良好的开发流程和代码管理规范。MISRA C:2012 标准中共包含 16 条指令,下面将逐一介绍:
-
Dir 1.1 实现定义行为文档化:
- 要求: 应该用文档记录并了解程序输出依赖的任何实现定义行为。
- 解释: C 语言标准中有一些行为是“实现定义”的 (implementation-defined),这意味着这些行为的具体实现由编译器或平台决定。不同的编译器或平台可能会对这些行为有不同的实现,导致代码的行为在不同环境下不一致。为了确保代码的可移植性和可预测性,应该将程序依赖的任何实现定义行为记录在文档中,并确保开发团队充分理解这些行为。
- 示例:
- 整数类型的大小 (int, long 等) 在不同平台上可能是不同的。
- 有符号整数的右移操作可能是算术右移 (保留符号位) 或逻辑右移 (不保留符号位)。
- 结构体成员的对齐方式可能因编译器和平台而异。
-
Dir 2.1 零编译错误:
- 要求: 所有源文件必须没有任何编译错误。
- 解释: 这是代码质量最基本的要求。存在编译错误的代码无法生成可执行程序,更谈不上功能和性能。在开发过程中,应该及时修复所有编译错误,确保代码始终处于可编译状态。
-
Dir 3.1 需求可追溯性:
- 要求: 所有代码应该可以追溯至文件化的需求。
- 解释: 可追溯性是指代码和需求之间建立清晰的对应关系。良好的可追溯性可以帮助开发人员理解代码的设计意图,验证代码是否满足需求,并在需求变更时快速定位受影响的代码。
- 示例:
- 可以在代码注释中注明该代码段对应的需求编号或需求描述。
- 可以使用需求管理工具来维护代码和需求之间的映射关系。
-
Dir 4.1 运行时故障最小化:
- 要求: 运行时故障必须最小化。
- 解释: 运行时故障是指程序在运行过程中发生的错误,例如空指针解引用、数组越界、除零错误等。这些故障会导致程序崩溃或产生不可预测的结果。在代码设计阶段就应考虑各种可能的错误情况,并采取措施预防运行时故障的发生,例如进行输入验证、边界检查、错误处理等。
-
Dir 4.2 所有汇编语言的使用应当用文档记录
- 要求: 建议所有汇编的使用应当用文档记录。
- 解释: 虽然在某些情况下, 为了提高性能或访问底层硬件, 可能需要使用汇编语言。 但是, 汇编语言的可读性, 可维护性和可移植性都比较差。 因此,MISRA C建议详细记录汇编代码的用途, 功能和接口, 以便其他开发人员理解和维护。
-
Dir 4.3 Assembly language shall be encapsulated and isolated
- 要求: 汇编语言必须封装、隔离。
- 解释: 为了减少汇编语言对代码可维护性和可移植性的影响, 应该将汇编代码封装在独立的函数或模块中, 并与 C 代码隔离。 这样可以限制汇编代码的作用范围, 降低代码的复杂性, 并方便后续的维护和移植。
-
Dir 4.4 Sections of code should not be 'commented out'
- 要求: 建议代码部分不应当注释掉。
-
解释:
-
不合规代码示例:
/* x = y + z; // 这段代码暂时不需要 */
-
合规代码示例:
#if 0 x = y + z; // 使用预处理指令 #endif
解释: 不应该用
// ...
或者/* ... */
注释代码。而应该用#ifdef ...#endif
等预编译指令。
-
-
Dir 4.5 Identifiers in the same namespace with overlapping visibility should be typographically unambiguous
- 要求: 建议同一命名空间中, 具有重叠可见性的标识符, 必须在排版上毫不含糊。
- 解释: 类似于C++名称遮掩问题。例如, 全局变量不要与局部变量重名。
-
Dir 4.6 typedefs that indicate size and signedness should be used in place of the basic numerical types
- 要求: 建议指示大小, 符号的typedefs(类型定义), 应当用来替代基本的数字类型。
- 解释: 例如, typedef定义的uint32_t, 用来替代32位无符号整型。
-
Dir 4.7 If a function returns error information, then that error information shall be tested
-
要求: 如果一个函数返回错误信息, 那么错误信息应答被测试。
-
解释:许多库函数和自定义函数会通过返回值来指示错误状态。 调用这些函数后, 应该检查返回值, 并进行相应的错误处理。 忽略错误信息可能导致程序在错误状态下继续运行, 产生不可预测的结果。
-
示例:
FILE *fp = fopen("myfile.txt", "r"); if (fp == NULL) { // 处理文件打开失败的情况 perror("fopen failed"); return -1; }
-
-
Dir 4.8 If a pointer to a structure or union is never dereferenced within a translation unit, then the implementation of the object should be hidden
要求:建议如果一个指向struct或union的指针, 在翻译单元内从未被解引用, 则应该隐藏该对象的实现。
-
非合规代码示例:
// A.h struct A { uint32_t id; uint8_t name[32]; }; //test.c #include <A.h> // 不必要 void foo(struct A *p){ //没有解引用 }
-
合规代码示例:
// A.h struct A { uint32_t id; uint8_t name[32]; }; // test.c // 正确用法:前向声明 struct A; void foo(struct A *p){ //没有解引用 }
解释: 如果结构体A实现如下,如果test.c中, 从未对指向A类型对象的指针进行解引用, 也就没有访问其成员。此时, 在test.c文件中应该使用前向声明, 而不应该使用
"#include <A.h>"
。 -
-
Dir 4.9 A function should be used in preference to a function-like macro where they are interchangeable
- 要求: 建议在函数、函数类的宏可以相互替换的地方, 应优先使用函数。
- 解释: 因为函数会在编译期做类型检查, 更安全。
-
Dir 4.10 Precautions shall be taken in order to prevent the contents of a header file being included more than once
-
要求: 应当采取预防措施, 防止头文件的内容被多次包含。
-
解释: 通常, 使用
"#ifndef ... #endif"
。也可以使用"#pragma once"
, 不过想要编译器支持。 -
示例:
// my_header.h #ifndef MY_HEADER_H #define MY_HEADER_H // 头文件内容 #endif
-
-
Dir 4.11 The validity of values passed to library functions shall be checked
-
要求: 传递给库函数的值的有效性, 应当被检查。
-
解释: 因为一些库函数有严格的限制域, 需要检查:
许多数学函数(
中的math函数), 如:
(1) 负数不允许传递给sqrt, log函数。
(2) fmod第二个参数不应为0。当非小写字母的参数传递给函数toupper时(类似有tolower), 一些实现能产生非预期结果。
中的字符测试函数, 在传递无效值时, 表现出未定义行为。如, isalnum, isalpha, islower等。 给abs函数传递最值负数时, 表现出未定义行为。最小负数值转换成整数值, 由于位宽限制, 无法正确转换。
-
示例:
#include <math.h> #include <stdio.h> int main() { double x = -1.0; if (x >= 0.0) { double result = sqrt(x); printf("sqrt(x) = %fn", result); } else { printf("Error: Cannot compute square root of a negative number.n"); } return 0; }
-
-
Dir 4.12 Dynamic memory allocation shall not be used
- 要求: 不应该使用动态内存分配。
- 解释: 例如, 不应该使用malloc/free进行动态内存分配。 动态内存分配(malloc, free等)可能导致内存泄漏, 碎片, 以及难以预测的程序行为。 在安全关键的嵌入式系统中, 应该避免使用动态内存分配。
-
Dir 4.13 Functions which are designed to provide operations on a resource should be called in an appropriate sequence
- 要求: 建议设计用来提供操作资源的函数, 应当以合适的序列进行调用。
- 解释: 例如, 某个硬件模块的操作, 应当遵循一定顺序, 以符合硬件资源特性。 例如, 对于文件操作, 应该按照打开 -> 读/写 -> 关闭的顺序进行。
2.2 规则 (Rules)
规则 (Rules) 则更侧重于 C 语言编码的具体细节,针对 C 语言的各种特性和潜在的陷阱,提出了详细的编码约束。
2.2.1 标准 C 环境 (Standard C environment)
-
Rule 1.1 (强制): 标准 C 语法和约束: 程序不得违反标准C语法和约束,不应超出实现的翻译限制。
- 解释: 程序必须只使用C语言特性及其库, 除非使用语言扩展, 否则程序不应: 1) 包含任何违反本标准中描述的语言语法行为; 2) 包含任何违反本标准规定的限制行为。
- 示例:
- 语法行为:不支持写const变量。
- 语言扩展:一些C90编译器提供
__inline
关键字声明inline函数。许多编译器支持使用一些关键字定义对象位置, 如__zpage
,__near
,__far
。
-
Rule 1.2 (强制): 避免语言扩展
-
非合规代码示例 (使用 GNU C 扩展):
// 使用 GNU C 扩展的语句表达式 int max(int a, int b) { return ({ int _a = a; int _b = b; _a > _b ? _a : _b; }); } // 使用 GNU C 扩展的零长度数组 struct my_struct { int data_len; char data[]; // 零长度数组 (非标准 C) }; // GNU C 扩展的 typeof int x = 10; typeof(x) y = 20; // y 的类型与 x 相同 (int) //GNU C 扩展, 指定初始化器 int arr[10] = { [2] = 10, [5] = 20 }; //GNU C扩展, case 范围 switch (value) { case 1 ... 10: // 处理 1 到 10 之间的值 // ... break; // ... }
-
合规代码示例 (使用标准 C):
// 标准 C 代码 int max(int a, int b) { return (a > b) ? a : b; } // 使用指针和动态内存分配代替零长度数组(MISRA C不允许动态分配, 这里仅做标准C演示) struct my_struct { int data_len; char *data; // 指针 }; // 标准 C 没有 typeof,需要明确类型 int x = 10; int y = 20; //标准C 初始化 int arr[10] = {0, 0, 10, 0, 0, 20, 0, 0, 0, 0}; //标准C switch 语句 switch (value) { case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: case 9: case 10: // ... break; // ... }
-
规则解释及益处:
语言扩展虽然在某些情况下可以提供便利或提高性能,但它们通常是特定于编译器的,会导致代码的可移植性降低。如果将代码移植到不支持这些扩展的编译器或平台,就需要进行大量的修改。遵循 Rule 1.2,坚持使用标准 C 特性,可以最大程度地保证代码的可移植性。
-
-
Rule 1.3 (强制): 避免未定义和未指定行为:
- 要求: 不应出现未定义或关键的未指定行为。
- 解释:
- 未定义行为 (Undefined behavior): 指 C 语言标准没有规定其行为的操作。当程序执行到未定义行为时,可能会发生任何情况,包括程序崩溃、产生错误的结果、正常运行等。
- 未指定行为 (Unspecified behavior): 指 C 语言标准规定了可能的行为,但具体选择哪种行为由编译器实现决定。未指定行为本身不一定是错误,但它可能导致代码的行为在不同编译器或平台下不一致。
- 关键的未指定行为是指那些可能导致严重后果或安全漏洞的未指定行为。
- 示例:
- 未定义行为:
- 数组越界访问。
- 空指针解引用。
- 除以零。
- 有符号整数溢出。
- 修改字符串字面量。
- 在信号处理函数中访问 volatile 对象 (除非该对象是 volatile sig_atomic_t 类型)。
- 未指定行为:
- 函数参数的求值顺序。
- 表达式中子表达式的求值顺序 (除非有明确的序列点)。
- printf 函数中格式化字符串中 %s 对应的参数不是指向字符串的指针。
- 未定义行为:
2.2.2 未使用代码 (Unused code)
-
Rule 2.1 (强制): 不可达代码
-
非合规代码示例:
void foo() { int a = 1; if (a > 0) { printf("a is positiven"); } else { // 这段代码永远不会被执行 printf("a is non-positiven"); } return; //return 之后的语句永远不会被执行 printf("Unreachable coden"); }
-
合规代码示例:
void foo() { int a = 1; if (a > 0) { printf("a is positiven"); } }
-
规则解释及益处:
不可达代码是指在程序执行过程中永远不会被执行到的代码。它通常是由于逻辑错误、条件判断错误或冗余代码导致的。不可达代码的存在会增加代码的复杂性,降低可读性,并可能掩盖潜在的逻辑错误。
-
-
Rule 2.2 (强制): 死代码
-
非合规代码示例:
void bar() { int x = 10; int y = 20; x = x + y; // x 的值被计算,但后续没有使用, 也不会对外部造成影响 printf("y = %dn", y); } int compute(){ int result = 1; result * 2; //计算结果没有被使用, 也不会对外部造成影响 return result; }
-
合规代码示例:
void bar() { int y = 20; printf("y = %dn", y); } int compute(){ int result = 1; return result * 2; }
-
规则解释及益处:
死代码是指被执行但其结果不会对程序的输出或后续计算产生任何影响的代码。它可能是未使用的变量赋值、未使用的表达式计算等。死代码的存在会浪费计算资源,增加代码的复杂性,并可能干扰代码优化。
-
-
Rule 2.3 (建议): A project should not contain unused type declarations
-
要求: 建议项目不应包含未使用类型声明。
-
解释: 如果一个类型定义但未使用, 审核人不清楚该类型是否冗余, 或者遗留的未使用错误。
-
非合规代码示例:
typedef struct { int x; int y; } Point; // Point 类型未被使用 void foo() { // ... }
-
合规代码示例:
// 删除未使用的类型声明, 或者在后续的代码中使用它 void foo() { // ... }
-
-
Rule 2.4 (建议): A project should not contain unused tag declarations
-
要求: 建议项目不应包含未使用标记声明。
-
非合规代码示例:
struct MyStruct; // 声明了标记 MyStruct,但从未定义或使用 void foo() { // ... }
-
合规代码示例:
// 删除未使用的标记声明,或者定义并使用它 void foo() { // ... }
-
-
Rule 2.5 (建议): A project should not contain unused macro declarations
- 要求: 建议项目不应包含未使用宏定义声明。
-
非合规代码示例:
#define MAX_VALUE 100 // 定义了宏 MAX_VALUE,但从未使用 void foo() { // ... }
- 合规代码示例:
// 删除未使用的宏定义,或者在后续的代码中使用它 void foo() { // ... }
-
Rule 2.6 (建议): A function should not contain unused label declarations
-
要求: 建议函数不应包含未使用标签声明。
-
非合规代码示例:
void foo() { int x = 10; start: // 定义了标签 start,但从未被 goto 语句引用 x++; }
-
合规代码示例:
// 删除未使用的标签,或者添加 goto 语句引用它 void foo() { int x = 10; x++; }
-
-
Rule 2.7 (建议): There should be no unused parameters in functions
-
要求: 建议函数中, 不应有未使用参数。
-
解释: 如果确实定义了未使用的参数, 为了确保函数兼容性, 可以形参名省略。
-
非合规代码示例:
void foo(int x, int y) { // 参数 y 未被使用 printf("x = %dn", x); }
-
合规代码示例:
void foo(int x, int /*y*/) { // 省略未使用参数的名称, 使用注释说明 printf("x = %dn", x); } // 或者, 如果 y 参数在未来可能会被使用, 可以添加 (void)y; 语句 void foo(int x, int y) { (void)y; // 明确表示 y 参数未被使用 printf("x = %dn", x); }
-
2.2.3 注释 (Comments)
-
*Rule 3.1 (强制): The character sequences `/
and
//` shall not be used within a comment**-
要求: 字符序列
/*
和//
不应在注释中使用。 -
解释: 在注释中嵌套使用注释符号可能导致注释提前结束, 或产生意外的注释效果。
-
非合规代码示例:
/* * This is a comment. * // This is a nested comment, which is not allowed. */ /* This is a comment /* with a nested comment */ , which is not allowed. */
-
合规代码示例:
/* * This is a comment. * This is NOT a nested comment. */
-
-
Rule 3.2 (强制): Line-splicing shall not be used in
//
comments-
要求: 不得在注释
//
中使用续行符()。 -
解释: 在
//
注释中使用续行符可能导致意外的代码被注释掉, 降低代码的可读性。 -
非合规代码示例:
// This is a comment int x = 10; // 这行代码会被意外注释掉
-
合规代码示例:
// This is a comment. int x = 10;
-
2.2.4 字符集和词汇约定 (Character sets and lexical conventions)
-
Rule 4.1 (强制): Octal and hexadecimal escape sequences shall be terminated
-
要求: 8进制和16进制高级转义序列应终止。
-
解释: 如果8进制或16进制转义序列后跟其他转义序列, 可能出现混淆。如, 字符常量
x1f
由1个字符组成(表示ASCII 16进制值为1f的单元分隔符US), 而x1g
由2个字符x1
(表示ASCII 16进制值为1的标题开始SOH)和g组成。 -
非合规代码示例:
const char *str = "x1F"; // 16 进制转义序列未终止 printf("%sn", str); // 输出结果可能不符合预期
-
合规代码示例:
const char *str = "x1F "; // 使用空字符终止 16 进制转义序列 // 或者 const char *str = "x1F" ""; // 使用空字符串字面量终止 // 或者 const char *str = "x1Fx67"; // 使用另一个转义序列终止 printf("%sn", str);
-
规则解释及益处:
如果 16 进制或 8 进制转义序列后面紧跟其他字符,可能会导致编译器错误地解析转义序列,产生意外的字符值。为了避免这种混淆,应该使用明确的方式终止转义序列,例如使用空字符
-