• 正文
  • 相关推荐
申请入驻 产业图谱

太神奇了!代码里的局部变量居然会自己发生变化!

04/27 10:18
458
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

我是老温,一名热爱学习的嵌入式工程师,关注我,一起变得更加优秀!

上周,大伙在办公室安安静静写bug的时候,一位小伙伴突然说,“我了个去,局部变量居然会自己发生变化,你们快过来看一下!”

大家好奇地凑到那位小伙伴的工位前,然后看着仿真的变量值,它居然自己会哗啦啦地发生变化,而且每次变化的值都不一样。

这是一个让工程师头皮发麻的诡异问题,在那个软件模块里面,明明没有任何一句代码主动修改过那个局部变量,它的值居然莫名其妙地变化,有时候还会导致程序跑飞逻辑错乱。

大家七嘴八舌地讨论着,突然一位老鸟抛了一句,“会不会是内存违规操作的问题,看看是不是数组溢出或者指针滥用?”,这才引起了大家的注意~

经验丰富的嵌入式软件工程师都知道,局部变量是存放在栈(Stack)上面的,因为栈空间是一片有限的连续内存空间,如果操作不当的话,就会很容易发生内存越界或者栈溢出,从而直接“无故”篡改了相邻的局部变量。

借此机会,我网上学习了一下关于栈内存和局部变量的知识,总结了嵌入式开发里面最常见的几种局部变量无故变化的原因。

原因一:数组越界访问。(最常见)

数组越界访问(栈溢出),是在嵌入式写bug的时候,最常见的变量无故变化的元凶,很多工程师喜欢在函数内部定义局部数组,在访问的时候如果不注意数组下标管理,假如数组越界,就会直接覆盖相邻的局部变量。

以下是数组越界访问的代码示例。

#include <stdio.h>

void test(void)
{
    // 先在函数内部,定义两个相邻的局部变量
    int array[10] = {0};        // 一个局部数组
    int normal_value = 250;    // 固定为250
    //故意让数组越界访问:下标15远超数组长度10
    for(int i=0; i<=15; i++){
        array[i] = i;
    }
    // 输出:normal_value 被意外修改!
    printf("normal_value = %d\r\n", normal_value);
}

int main(void)
{
    test();
    return 0;
}

程序编译运行之后,normal_value没有任何的赋值代码,但它的输出结果不是100,而是一个随机或者固定的错误值。

导致这种现象的根本原因就是数组array越界写入了,从而覆盖了栈上面相邻的normal_value的内存值。

要解决数组越界其实也很简单,对数组下标进行严格检查即可,如以下代码所示,使用sizeof(array)/sizeof(array[0])计算数组长度。

int array[5] = {0};
int len = sizeof(array)/sizeof(array[0]); // 自动计算长度=15
// 数组下标严格限制在合法范围
for(int i=0; i<len; i++){
    array[i] = i;
}

原因二:野指针/空指针非法访问内存空间

指针可以说是嵌入式软件编程的一大利器,把指针用好,内存的使用效率和程序的执行效率非常高,但使用指针也很容易出错。

特别是对编程不熟悉的新手朋友,指针可以说是内存访问出错的重灾区,滥用未被初始化的野指针或者指针越界,就会随机修改任意的局部变量值。

void ptr_test(void)
{
    int data = 10;    // 定义一个普通局部变量
    int *p;           // 未被初始化的野指针
    
    *p = 100;  // 随机写入数值,大概率会篡改栈上变量
    printf("data = %d\r\n", data);
}

上述代码造成的现象就是,data值可能会随机变化,程序可能一会正常一会异常,非常难定位原因和调试。

要解决这种情况,在指针定义的时候必须进行初始化并为其分配内存空间,在使用指针之前判断是否为NULL,并且禁止使用未被初始化的指针变量。上述代码应该进行如下的修改。

void ptr_test(void)
{
    int data = 10;
    int *p = NULL;    //把指针初始化为NULL值
    int temp = 0;
    p = &temp;        // 指针指向合法变量地址
    if(p != NULL)     // 判断非空后使用
    {
        *p = 100;
    }
}

原因三:字符串越界拷贝,无结束符

在嵌入式编程里面,经常会用到字符串操作,如果字符串在定义的时候没有加结束符,或者使用strcpy相关的函数越界,程序就会疯狂操作栈内存,就会导致局部变量被篡改,出错的示例代码如下。

#include <stdio.h>
#include <string.h>
void str_test(void)
{
    int flag = 1;        // 控制标志位
    char buf[4] = {0};   // 定义一个数组,最多存3个有效字符+1个结束符
    // 错误师范!!!:写入4个字符,没有结束符!
    buf[0] = 'A';
    buf[1] = 'B';
    buf[2] = 'C';
    buf[3] = 'D';
    
    strcpy(buf, "ABCD");// 如果没有无结束符,strcpy 会一直向后拷贝,导致越界覆盖flag
    printf("flag = %d\r\n", flag);
}

上述代码运行之后,flag就会无故变成0值或者其他值,然后程序逻辑就会直接错乱。

要解决这种情况,就必须要保留字符串结束符的位置,并且优先使用strncpy这些安全的字符串操作函数,最好就是手动确保字符串以结尾。

char buf[4] = {0};
strncpy(buf, "ABCD", sizeof(buf)-1);// 安全拷贝:最多拷贝3个字符,自动留结束符
buf[sizeof(buf)-1] = '\0';// 手动补结束符,绝对安全

原因四:函数嵌套过深导致栈溢出

如果项目里面有使用到RTOS,我们在配置RTOS的时候设置的任务栈过小,则极有可能会在递归调用、深层嵌套、使用超大局部数组的时候,直接撑爆栈内存,从而篡改栈内存。

// 无终止条件的递归函数
void recursive_process(void){
    int a = 1;
    int b = 2;

    char big_bufffer[1024] = {0};// 直接定义超大局部数组,占用大量的栈空间
    recursive_process();// 无限递归调用
}

int main(void){
    recursive_process();
    return 0;
}

上述代码运行之后就会导致所有的局部变量全部变成乱码,然后程序会直接进入HardFault硬件错误中断。

解决方案就是避免使用无限递归,递归函数必须要有明确的终止条件,还有就是,不要在函数内部定义超大型数组,应该要改为全局变量或者静态变量(但也要注意内存的使用),并且也要减少函数的嵌套层级。

char big_buf[1024] = {0};//超大型缓冲区改为全局变量,不占用栈空间
void recursive_process(void)
{
    static int count = 0;
    count++;
    if(count > 10) return;//增加递归调用的终止条件
    recursive_process();
}

总结一下

在嵌入式C语言编程中,局部变量是绝对不会自己变化的,这些“自己变化”的神秘现象,都是栈内存空间被破坏而导致的,主要有以下因素导致栈内存空间被破坏。

数组越界访问、错误使用野指针或者空指针、字符串无结束符越界拷贝、递归调用或超大数组导致栈溢出。

以下这些调试技巧,可以帮助快速定位局部变量无故变化,

1-打印变量地址,可以使用printf("normal_value = %prn", &normal_value);来确认变量是否被相邻的数组或者指针覆盖。

2-设置调试断点,在调试器中给变量设置数据的断点,然后一旦变量被修改之后,程序立即停止运行,并且直接定位到修改代码。

3-对于GCC编译器,可以开启 -Wall -Wextra,90%的数组越界、野指针、未被初始化的变量,编译器都会进行编译警告

相关推荐