随手记——使用GDB定位内存越界问题

Posted 穿越临界点

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了随手记——使用GDB定位内存越界问题相关的知识,希望对你有一定的参考价值。

在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的无效信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。

1 问题引入

问题最开始引入是软件升级后压力了将近4个小时突然出现大面积任务异常。

2 问题分析定位

出现任务异常,首先检查堆栈调用信息。

查看堆栈调用信息发现是pBoardList指针内容被修改导致了地址访问异常。

pBoardList指针内容被修改的情况有两种:一种是代码逻辑确实对其进行了修改;一种是产生了内存越界访问。

后对代码查看,发现pBoardList在首次赋值之后是不会对其进行修改的,也就是说它是const变量。那么就只有第二种情况了——内存越界访问。


接下来就是查看到底是谁对其进行了改写。使用的工具是gdb-watch。

系统启动后将pBoardList变量添加到gdb watch中,当该变量发生变化时,gdb会停止该进程,然后backtrace就可以查看到当时的堆栈信息——问题发生时的第一现场——了。

具体操作步骤如下:

# 1.关闭看门狗(不是gdb指令,要使用自己单板上的指令)
watchdog_stop
# 2.进入gdb并atta进程/线程
gdb atta <pid>
# 3.关闭信号响应(此时已经进入gdb)
handle SIGPIPE noprint nostop
# 4.添加需要监控的变量
watch pBoardList
# 5.继续运行
continue

然后,就是一直压力等待复现了。等待了4个多小时……问题终于复现了。

然后在gdb中输入bt(注意输入bt前不要敲回车,因为gdb中直接敲回车会运行上一个指令)查看此时的堆栈信息。堆栈信息显示了pBoardList变量被改写时被调用的函数和代码行号——定位到下述代码。

/* 根据gdb定位到的问题源码 */
s32 g_s32BadPower[100] = {0}char writebuff[4096] = {0};
list_node_t *pBoardList = NULL;

s32 get_power(void)
{
    ...
	if (powerMw > 20000) 
    {
		...
        g_s32BadPower[g_u32BadValNum] = powerMw; /*gdb定位到了该行代码,就是该行产生了越界访问*/
        g_u32BadValNum++; /* 该变量一直加加,没有限制取值范围 */
    }
    ...
}

到这里问题点已经找到,但是还有一个疑问没有解决——为什么复现需要将近4个小时?


我们继续看上述源码。出问题的变量是pBoardList,越界的变量是g_s32BadPower,他们中间隔了一个writebuff,所以g_s32BadPower出错想要覆盖pBoardList必须先要跨过writebuff这座“大山”,因此,问题需要压力好长时间才可以复现。

使用gdb查看g_s32BadPower和pBoardList的地址,以及g_u32BadValNum的值之后,证实了上述的分析。


问题的分析到这里还没有完全结束~

就像悬疑案的推理一样,所有的案发现场都还原之后总是还要再问一个问题——作案动机是什么?

我们分析定位问题同样需要刨根究底,不是解决了就草草了结了(否则很容易遗留BUG),我们也需要再追问一句——问题是如何触发的?

上述的源码已经上线半年了也没有出现问题,为什么突然间问题就出现了呢?

该问题的罪魁祸首就是下面标注的代码:

s32 get_power(void)
{
    ...
	if (powerMw > 20000) /*罪魁祸首是这里,使用了魔法数字*/
    {
		...
        g_s32BadPower[g_u32BadValNum] = powerMw; 
        g_u32BadValNum++; 
    }
    ...
}

由于业务需求的增加,板卡的功率突然增加,超过了20W,因此就进入了异常分支,才暴露了这个问题。所以说,使用魔法数字是很危险的,最好使用关系式或算法代替,实在不行也要使用配置宏和配置变量,在相关业务变动时同时跟随变动,否则会严重影响系统的可维护性、可扩展性和可复用性。


3 问题解决

问题分析和定位就像是分析就像是在大海里搜寻掉落的银针,解决问题就好比打捞这个动作。

所以说问题解决一般是比较简单的,代码如下:

#define BAD_POW_MAX_NUM 100
s32 g_s32BadPower[BAD_POW_MAX_NUM] = {0}//char writebuff[4096] = {0}; 该变量无人使用,删除
list_node_type *pBoardList = NULL;

s32 get_power(void)
{
    ...
	if (powerMw > 该门限值可以使用统计学算法给出而不是写死)
    {
		...
        if(g_u32BadValNum >= BAD_POW_MAX_NUM) /*数组索引在使用前一定要判断取值范围*/
        {
            g_u32BadValNum = 0;
        }
        g_s32BadPower[g_u32BadValNum] = powerMw;
        g_u32BadValNum++;
    }
    ...
}

4 复盘

  1. 任何情况下,针对数组操作,一定要控制下标的范围,防止内存越界产生。
  2. 全局变量的内存越界,始作俑者一般就在它身边——临近的“数组”。
  3. 不要使用魔法数字,不要使用魔法数字,不要使用魔法数字。尽量使用关系式,实在不行也要使用宏定义并将其放到配置文件中并加好注释。
  4. 代码一定要在组内或部门内走查后再上线,不要觉得代码改动量少就能完全控制风险,老虎也有打盹的时候,单个人总会犯错,要依靠团队的力量。

恭喜你又坚持看完了一篇博客,又进步了一点点!如果感觉还不错就点个赞再走吧,你的点赞和关注将是我持续输出的哒哒哒动力~~

以上是关于随手记——使用GDB定位内存越界问题的主要内容,如果未能解决你的问题,请参考以下文章

随手记——使用内存池遇到的性能下降问题

随手记——使用内存池遇到的性能下降问题

为什么gdb core文件的时候无法定位出出问题的地方

2019-3-19记随手记面试

Asan快速定位内存越界内存泄漏

Asan快速定位内存越界内存泄漏