3. 基本语法规则(一):禁止使用动态内存分配(malloc/free)、禁止使用标准库中的危险函数(如gets、strcpy)

好,咱们今天聊点硬核的。动态内存分配和那些危险的标准库函数,在航天软件里,基本就是「禁区」。你可能在桌面程序里用惯了 mallocfree,觉得挺方便。但在航天器上,这一套行不通。

为什么?我跟你讲个真实案例。有一次我在评审一个飞控模块的代码,发现里面用了 malloc 来分配一个临时缓冲区。当时我就问开发者:「如果分配失败怎么办?」他愣了一下说:「应该不会吧?」——你看,这就是问题所在。「应该不会」在航天领域,就是最大的隐患。

3.1 为什么禁止动态内存分配?

说白了,动态内存分配有三个致命伤:

  • 不确定性:你不知道 malloc 什么时候会失败。内存碎片、堆空间耗尽,都可能让系统在关键时刻崩溃。
  • 不可预测的执行时间mallocfree 的执行时间不是固定的。它可能因为内存碎片而变得很慢。航天系统对实时性要求极高,这种不确定性是不能接受的。
  • 内存泄漏风险:你想想看,在长达数年的任务周期里,如果有一处 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 禁止使用危险的标准库函数

接下来聊聊标准库里的那些「定时炸弹」。像 getsstrcpysprintf 这些函数,在航天软件里是绝对不能用的。

为什么?因为它们不检查缓冲区边界。你想想看,如果输入的数据比缓冲区大,会发生什么?缓冲区溢出!轻则数据被覆盖,重则程序崩溃,甚至被攻击者利用。

我记得有一次,一个同事在代码里用了 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 小结

今天咱们聊了两个核心规则:

  • 禁止动态内存分配:用静态分配代替,所有内存在编译时确定。
  • 禁止危险标准库函数:用带边界检查的安全替代函数。

这两条规则,是航天软件安全的基石。你想想看,如果连内存安全都保证不了,那后面的所有逻辑都是空中楼阁。下一章咱们会继续聊基本语法规则,包括指针的使用规范和类型转换的陷阱。到时候见。