3. 基本语法规则(一):禁止使用动态内存分配(malloc/free)、禁止使用标准库中的危险函数(如gets、strcpy)
好,咱们今天聊点硬核的。动态内存分配和那些危险的标准库函数,在航天软件里,基本就是「禁区」。你可能在桌面程序里用惯了 malloc 和 free,觉得挺方便。但在航天器上,这一套行不通。
为什么?我跟你讲个真实案例。有一次我在评审一个飞控模块的代码,发现里面用了 malloc 来分配一个临时缓冲区。当时我就问开发者:「如果分配失败怎么办?」他愣了一下说:「应该不会吧?」——你看,这就是问题所在。「应该不会」在航天领域,就是最大的隐患。
3.1 为什么禁止动态内存分配?
说白了,动态内存分配有三个致命伤:
- 不确定性:你不知道
malloc什么时候会失败。内存碎片、堆空间耗尽,都可能让系统在关键时刻崩溃。 - 不可预测的执行时间:
malloc和free的执行时间不是固定的。它可能因为内存碎片而变得很慢。航天系统对实时性要求极高,这种不确定性是不能接受的。 - 内存泄漏风险:你想想看,在长达数年的任务周期里,如果有一处
free漏掉了,内存就会一点点被吃掉。最终系统会因内存耗尽而宕机。
我曾经在一个老项目中看到过这样的代码:
// ❌ 禁止使用
void process_data(int size) {
char *buffer = (char*)malloc(size);
if (buffer == NULL) {
// 处理错误?但往往处理不了
return;
}
// ... 使用 buffer
free(buffer);
}
这段代码看起来没什么问题,对吧?但你想过没有:如果 malloc
3.2 正确的做法:静态分配
那不用 malloc,我们用什么?答案是:静态分配。在编译时就确定好所有内存的大小和位置。
// ✅ 推荐做法:静态分配
#define MAX_BUFFER_SIZE 1024
static char buffer[MAX_BUFFER_SIZE]; // 编译时就分配好了
void process_data(void) {
// 直接使用 buffer,不用担心分配失败
// buffer 的大小是固定的,不会溢出
}
你可能会问:「那如果数据大小不确定怎么办?」嗯,这个问题问得好。我的建议是:取最大值。航天系统在设计阶段就应该能估算出所有可能的数据规模。如果实在估算不了,那就说明设计本身有问题。
3.3 禁止使用危险的标准库函数
接下来聊聊标准库里的那些「定时炸弹」。像 gets、strcpy、sprintf 这些函数,在航天软件里是绝对不能用的。
为什么?因为它们不检查缓冲区边界。你想想看,如果输入的数据比缓冲区大,会发生什么?缓冲区溢出!轻则数据被覆盖,重则程序崩溃,甚至被攻击者利用。
我记得有一次,一个同事在代码里用了 strcpy,结果输入字符串比目标缓冲区长了 10 个字节。程序没崩溃,但后续的逻辑全乱了。查了两天才找到原因——就是这 10 个字节的溢出,把相邻的变量给覆盖了。
3.4 危险函数清单与替代方案
下面这张表,我建议你打印出来贴在工位上:
| 危险函数 | 风险 | 安全替代方案 |
|---|---|---|
gets() |
不检查缓冲区大小,极易溢出 | fgets()(指定最大读取长度) |
strcpy() |
不检查目标缓冲区大小 | strncpy() 或 strlcpy() |
strcat() |
不检查目标缓冲区剩余空间 | strncat() 或 strlcat() |
sprintf() |
不检查输出缓冲区大小 | snprintf()(指定最大输出长度) |
vsprintf() |
同 sprintf |
vsnprintf() |
scanf() |
不检查输入缓冲区大小 | 使用 fgets() + sscanf() 并指定宽度 |
3.5 安全编码示例
咱们来看一个对比。这是错误用法:
// ❌ 禁止使用
char name[32];
gets(name); // 如果输入超过31个字符,直接溢出
char full_name[64];
strcpy(full_name, name); // 如果 name 超过63个字符,溢出
char message[128];
sprintf(message, "Hello, %s!", name); // 如果 name 很长,message 可能溢出
这是正确用法:
// ✅ 推荐做法
#define NAME_MAX 31
#define FULL_NAME_MAX 63
#define MESSAGE_MAX 127
char name[NAME_MAX + 1]; // +1 留给字符串结束符 '\0'
if (fgets(name, sizeof(name), stdin) != NULL) {
// 去掉可能存在的换行符
name[strcspn(name, "\n")] = '\0';
}
char full_name[FULL_NAME_MAX + 1];
strncpy(full_name, name, FULL_NAME_MAX);
full_name[FULL_NAME_MAX] = '\0'; // 确保字符串以 '\0' 结尾
char message[MESSAGE_MAX + 1];
snprintf(message, sizeof(message), "Hello, %s!", name);
3.6 避坑指南
我曾经犯过一个错误,觉得 strncpy 是安全的,就没检查返回值。结果发现 strncpy 在源字符串比目标缓冲区长的时候,不会自动添加字符串结束符。这就导致后续的字符串操作全部越界。
所以,使用 strncpy 后,一定要手动加 '\0'。这是很多新手容易忽略的地方。
另外,snprintf 也不是万能的。它的返回值是「如果缓冲区足够大,应该写入的字符数」。如果返回值大于等于缓冲区大小,说明输出被截断了。你最好检查一下这个返回值,并做相应处理。
assert(strlen(full_name) < FULL_NAME_MAX);。这样在测试阶段就能发现问题,而不是等到上天了才出故障。
3.7 小结
今天咱们聊了两个核心规则:
- 禁止动态内存分配:用静态分配代替,所有内存在编译时确定。
- 禁止危险标准库函数:用带边界检查的安全替代函数。
这两条规则,是航天软件安全的基石。你想想看,如果连内存安全都保证不了,那后面的所有逻辑都是空中楼阁。下一章咱们会继续聊基本语法规则,包括指针的使用规范和类型转换的陷阱。到时候见。