简单说下越界访问的调试方法

时间:2022-01-17 15:28:00

越界访问的一般提示是 “access violation reading location 0xXXXXXXXX”,翻译过来就是“内存越界访问”。


这个提示和一般提示的不同之处是,程序不会停在越界访问的错误行上,

而是会停在分配或释放内存的行上,于是就需要我们自己去找到越界行。


那要怎么找到越界行呢?这里说一个较为实际的办法:


首先根据 函数调用栈 (Call Stack) 这个工具,找到内存越界访问的那个函数。


例如:

  

f()
{
#define MAXLIM 100
#define MAXLEN 10

int *p = (int *)calloc(MAXLEN, sizeof(int));
int *q = (int *)calloc(MAXLIM, sizeof(int));

int i;
for(i = 0; i < MAXLIM; i++)
p[i] = 1;

free(q);
free(p); // 程序将在执行这一行后提示一个对话框,同时 Output 窗口中会输出一行 “access violation reading location 0xXXXXXXXX”
}

然后按照下述法则调试代码,保证可以找到内存越界访问:


1. 注释 (Comment) 大量连续源代码,只保留成对的 *alloc 和 free,运行看是否还提示越界。然后进行下一步。

2. 若越界访问,那么注释更大部分源代码。重复此步,直至无越界访问。越界访问行必然在最后两次注释的增加注释里。

    若无越界访问,那么注释更小部分源代码。重复此步,直至越界访问。越界访问行必然在最后两次注释的减少注释里。


下面我们试试这个办法。


首先注释大量代码。我们把整个函数都注释了,只保留两个宏定义:


f()
{
#define MAXLIM 100
#define MAXLEN 10

//int *p = (int *)calloc(MAXLEN, sizeof(int));
//int *q = (int *)calloc(MAXLIM, sizeof(int));

//int i;
//for(i = 0; i < MAXLIM; i++)
//p[i] = 1;

//free(q);
//free(p);
}

这样子显然没有任何问题。于是我们注释更小部分代码:


f()
{
#define MAXLIM 100
#define MAXLEN 10

int *p = (int *)calloc(MAXLEN, sizeof(int));
int *q = (int *)calloc(MAXLIM, sizeof(int));

//int i;
//for(i = 0; i < MAXLIM; i++)
//p[i] = 1;

free(q);
free(p);
}

调试发现也没有问题。那么再注释更小部分的代码。for 循环,虽然不容易用“直到换行符的注释(//)”,注释,但是还是可以用“从/*符到*/符的注释”,注释。


f()
{
#define MAXLIM 100
#define MAXLEN 10

int *p = (int *)calloc(MAXLEN, sizeof(int));
int *q = (int *)calloc(MAXLIM, sizeof(int));

int i;
for(i = 0; i < MAXLIM; i++)
/*p[i] = 1*/;

free(q);
free(p);
}

调试后依然没有问题。那么继续减小注释范围。这时发现已经没有可以注释的了,那么就不注释,也就是原来代码了。


原来代码运行必然会提示越界访问的。于是可以肯定地说,越界访问出现在 "p[i] = 1;" 上。正是这个索引 i 超过了允许访问的界限。


于是我们要比较一下 i 的访问范围和 p 的允许范围。


i 的访问范围是 0 ... MAXLIM - 1

p 的允许范围是 0 ... MAXLEN - 1


而 MAXLEN < MAXLIM ,于是越界。这时原因查出,只需根据需要,减小赋值范围或增大分配内存即可。


f()
{
#define MAXLIM 100
#define MAXLEN 10

int *p = (int *)calloc(MAXLEN, sizeof(int));
int *q = (int *)calloc(MAXLIM, sizeof(int));

int i;
for(i = 0; i < MAXLIM; i++)
p[i] = 1;

free(q);
free(p);
}


其实从 free 能够释放内存这里可以知道,对已分配内存其实像字符串一样已经做过标记,所以释放时才能够知道需要释放到哪儿。


赋值时候不会自动检测是否越界访问,也是为了效率提升。如果一定需要检测,我们可以自己用 assert 宏进行实现:


f()
{
#define MAXLIM 100
#define MAXLEN 10

int *p = (int *)calloc(MAXLEN, sizeof(int));
int *q = (int *)calloc(MAXLIM, sizeof(int));

int i;
for(i = 0; i < MAXLIM; i++)
{
assert(i < MAXLEN);
p[i] = 1;
}

free(q);
free(p);
}