我是老温,一名热爱学习的嵌入式工程师,关注我,一起变得更加优秀!
上周,大伙在办公室安安静静写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%的数组越界、野指针、未被初始化的变量,编译器都会进行编译警告
458