一个变量越界引起的灾难
1.前言
一般地,对于内存块访问(如数组、程序员动态分配的内存块、系统从堆上分配的内存块),通过“下标”形式访问时,如果稍有不留意,对于末尾地址的访问处理不当,则会发生程序异常,轻则导致当然应用程序(进程)异常退出,重则导致整个系统瘫痪。如果是在嵌入式系统里发生,如裸机程序,或者多线程的实时系统(RTOS)中,基本会导致整个系统程序异常退出(死机)。这对于用户来说是“灾难”性事的、不可接受的。
对于变量的越界,大多情况下不会导致程序异常,只是业务功能或者底层控制未能达到预期功能。然而,在特殊情况下,依然会导致“灾难”性发生,下面是调用别人代码时发生的一个案例,直接导致系统“假死”。
2.案例分析
在此之前未有遇到过类似情况,举此例子的目的不是为了讨伐别人的错误,而是作为自己一个总结和反思,提高自己的编程水平和思维觉悟。源代码不直接上,关键处理函数思路如下。
uint8_t fun_ctrl(uint16_t size)
{
uint8_t i;
for(i = 0; i < size;i++)
{
/*process*/
}
return 0;
}
2.2代码分析
对于上述函数,存在非常大的问题。入口函数参数是无符号16位数“uint16_t”类型,而函数内部循环变量则为无符号8位数“uint8_t”类型。调用该函数时,如果传入实参变量范围在“uint8_t”内,则完全达到预期功能。但如果,实参变量超出“uint8_t”范围,那么该函数将陷入“死循环”状态,从而导致程序(系统)“假死”,虽然此时系统仍在运行,但当前函数严重占用CPU资源导致其他任务几乎无法获取CPU资源去执行,而这种情况出现则是“灾难性”的。
造成的原因,稍加分析比较易发现,因为函数内部计数变量为8位,值范围为0—255,而传入的实参类型为16位且范围超出255时,计数变量超出类型限制范围(255)后,则归零再执行计数,如此往复,陷入死循环。
在各类应用中,如果调用了上述类似函数,则有几类情况:
1)嵌入式单片机裸机程序中,调用该函数,由于是单一任务执行,其他任务(功能)将永远无法执行,陷入死循环。
2)实时操作系统下,函数被其中一任务线程调用,如果是抢占式内核,并且应用程序采用抢占式调度方式处理,并且该线程是优先级为最高,则其他任何任务无法获取CPU资源,即无法执行相应功能,系统瘫痪。
3)实时操作系统下,函数被其中一任务线程调用,应用程序采用时间片轮询的方式处理,其他任务线程依然能获取CPU资源并执行,但当前任务线程其他功能则无法实现。
4)如果在驱动层,则导致的后果更加不堪设想,并且不易定位bug。
5)如果该函数控制的是相关数据传输,比如串口/CAN向外传输数据,则连接的相关外设也可能因为接收大数据流导致导致严重占用资源甚至异常。
2.1代码改正
1)原则上,只需要将函数内部循环计数变量修改为与入口形参类型一致即可。
2)关于此类相关的处理函数,理应在内部增加条件对比,条件不成了则返回相应错误码。
3)增加函数入口参数说明,以及返回错误码涵义。
/**
* @brief
* @param
* @retval
*/
uint8_t fun_ctrl(uint16_t size)
{
uint16_t i;
for(i = 0; i < size;i++)
{
/*process*/
}
return 0;
}
/**
* @brief
* @param
* @retval
*/
uint8_t fun_ctrl(uint16_t size)
{
uint8_t i;
if(size > 255)
return -1;
for(i = 0; i < size;i++)
{
/*process*/
}
return 0;
}
3.总结
1)考虑函数入口参数类型,是否兼容8位机、16位机、32位机等,考虑到嵌入式内存吃紧问题,多数情况下采用8位(uint8_t)类型;入口参数与函数内临时变量相互调用时,保证类型一致。
2)函数内部应对入口参数的有效值作检查,如变量值是否超出限制范围、指针(包括函数指针)是否为空,如指针为空,调用时则导致程序异常崩溃。相关条件不成立应返回相应错误码。
3)模块开发之间,增加函数接口说明,包括注意参数限制、非常参数可能导致的后果以及返回错误码详细涵义等。
4)如没有相关错误码返回,bug出现时则不便于定位,或者寻找bug方向不对,降低效率。如上述案例中,遇到时,程序员第一反应很大可能认为是堆栈溢出问题。