如何调试程序

原文地址:

初学C语言/C++程序的编写时,可能经常会遇到程序崩溃的现象。一般来说,程序崩溃是由于内存操作不当引发的。但是具体来讲,由哪些原因可以导致程序崩溃呢?以及当程序崩溃时该如何找到错误的位置呢?本教程即是讲解这个问题。

本文的视频讲解在 C/C++学习指南(补充篇)- 单步调试 的第7,8节课。

一、程序崩溃的定位

先给出一个例子,该代码有致命bug,运行时将使程序崩溃。在VC中输入以下代码:

/////////////// 示例1 ////////////////////
#include 
#include 
struct Object
{
    int id;
    char name[32];
};
void show(Object* p)
{
    printf("Object [%d, %s] \n", p->id, p->name);
}
void test(int id, const char* name)
{
    Object* obj = NULL;
    show(obj); //<--空指针
}
int main()
{
    int aaa = 9801; // 未使用
    char* str = "127.0.0.1"; // 未使用

    int id = 123;
    const char* name = "shafa";
    test(id, name);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

* 按CTRL+F5运行 
显示程序已崩溃,如下图所示:

如何调试程序_第1张图片

这种提示意味着代码中存在严重bug,导致了程序崩溃。那么,怎么知道是哪儿出错了呢?

* 按F5启动调试 
黄色箭头指向的位置,就是出错的位置。在程序崩溃时,VC会自动地停在导致崩溃的那一行代码上, 
如何调试程序_第2张图片

注意两点: 
- 提示的错误为“未处理的异常 0XC000005,读取位置0x00000000时发生访问冲突”。以后凡是看到这种提示,表示错误的原因是“空指针”。 
- 在代码编辑器,黄色箭头已经指向了错误的行。

在界面上,点“中断” 
如何调试程序_第3张图片 
在界面上,点开“调用堆栈” 
这个窗口里可以直接观察到发生错误的时候、函数栈的各层函数的信息。( 如果没有显示这个窗口,可从菜单里 “调试 | 窗口 | 调用堆栈”里打开) 
如何调试程序_第4张图片


二、“调用堆栈”窗口的使用方法

“调用堆栈”窗口里可以观察到: 
- 函数的调用层次 :main() -> test(id, name) ->show(p) 
- 每一次函数里的局部变量(含参变量)的值 
- 全部变量的值

如何调试程序_第5张图片 
(*)从上到下,依次是函数的调用层次 
(*)每一行由以下信息组成 
Hello.exe!show(Object* p=0x00000000)行12+0xc字节 
模块名:Hello.exe 
函数名: show 
参数值:Object*p = 0x00000000 
位置:第12行 
可以发现,在main()函数之上还有一些东西,那些就是Windows应用程序的框架。 
(3)双击某个函数,可以看到这个函数内的局部变量的值 
如何调试程序_第6张图片 
注:显示的此时此刻(发生错误的时刻),函数栈上的各个层次的所有局部变量的值。观察它们的值,即可有助于程序员判断到底是哪儿写错了。


三、程序崩溃的原因分类

  1. 函数栈溢出 
    一个变量未初化、未赋值,就读取它的值。 
    ( 这属于逻辑问题,往往是粗心大意的导致的 )
  2. 函数栈溢出 
    (1)定义了一个体积太大的局部变量 
    (2)函数嵌套调用,层次过深(如无穷递归)
  3. 数组越界访问 
    访问数组元素时,下标越界
  4. 指针的目标对象不可用 
    (1)空指针 
    (2)野指针 
    • 指针未赋值
    • free/delete释放了的对象
    • 不恰当的指针强制转换

3.1 读取未赋值的变量

这种往往是疏忽大意造成的,因为逻辑错误非常明显。

//////////////// 示例 //////////////////
#include 
#include 
// 求两数的积
int multiply(int m, int n)
{
    return m * n;
}
int main()
{
    int a, b;
    int m = multiply(a, b);//<--这里有错
    printf("result: %d \n", m);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

按Ctrl + F5运行 
注意其错误提示的特征:“The variable is being used without being initialized”。 显然,a,b都没有初始值,而且也未赋值,那么multiply(a,b)毫无意义、不是正常的逻辑。 
如何调试程序_第7张图片 
按F5启动调试 
注意其错误特征:“The variable is being used without being initialized”。 
如何调试程序_第8张图片 
点“中断”, 
如何调试程序_第9张图片


3.2 函数栈溢出

以下两种情况会导致函数栈溢出: 
(1)定义了一个体积太大的局部变量 
(2)函数嵌套调用,层次过深(如无穷递归)

//////////////////// 示例 //////////////////////
#include 
#include 

// 局部变量的体积太大
void test()
{
    int buf[1024*1024*16];  // 这个变量体积太大
    printf("DO NOT define a very large buffer on the stack!");
    for(int i = 0; i<sizeof(buf)/sizeof(int); i++)
    {
        buf[i] = i;
    }
}
int main()
{
    test();
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

按CTRL+F5运行, 
如何调试程序_第10张图片 
按F5启动调试 
如何调试程序_第11张图片 
注意错误提示的特征:“未处理的异常:0Xc00000FD: Stack overflow” 
点“中断”,则黄色箭头停在出错位置。

如何调试程序_第12张图片 
在调用堆栈里点main, 
如何调试程序_第13张图片 
绿色箭头 表示,从第16行返回后,函数栈发生异常。 
结论:当变量体积太大时,应该用malloc或new来动态分配内存。

会导致函数栈溢出的另一种原因:函数递归调用,层次太深,没有终止条件。

///////////////// 示例 /////////////////
#include 
#include 
void a();
void b();

void a()
{
    printf("Calling a() ...\n");
    b();
}
void b()
{
    printf("Calling b() ...\n");
    a();
}
int main()
{
    a();
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

则运行时也会崩溃。


3.3 数组越界访问

#include 
#include 
void test(char* p)
{
    for(int i=0; i<5; i++)//<--这里有错
    {
        p[i] *= 10;
        printf("%d \n", p[i]);
    }
}
int main()
{
    char buf[4] = {1,2,3,4};
    test(buf);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

按CTRL + F5运行 
错误特征:” Stack around the variable was corrupted ” 
如何调试程序_第14张图片


3.4指针的目标对象不可用

请参考 C/C++学习指南(语法篇),第九章,9.5讲的视频讲解

指针指向的对象(内存)必须保证是有效的、可以访问的。 
分以下几种情况: 
(1)空指针 
(2)野指针 
- 指针未赋值 
- free/delete释放了的对象 
- 不恰当的指针强制转换

3.4.1 空指针

示例代码

#include 
#include 
struct Object
{
    int id;
    char name[32];
};
void show(Object* p)
{
    printf("Object [%d, %s] \n", p->id, p->name);
}
void test(int id, const char* name)
{
    Object* obj = NULL;
    show(NULL);//<--这里有错
}
int main()
{
    int id = 123;
    const char* name = "shafa";
    test(id, name);
    return 0;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

按CTRL+F5运行,显示程序已崩溃 
按F5启动调试 
错误特征:” 未处理的异常:0xC0000005:读取位置0x00000000时发生访问冲突 “

如何调试程序_第15张图片

3.4.2 野指针: 指针未赋值

////////////////// 示例代码 ///////////////
#include 
#include 
struct Object
{
    int id;
    char name[32];
};
void show(Object* p)
{
    printf("Object [%d, %s] \n", p->id, p->name);
}

int main()
{
    Object* p;
    show(p);//<--这里有错
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

错误特征: 
The variable is being used without being initialized

如何调试程序_第16张图片 
此时VC不能自动找到错误的精确位置,应该按照用单步调试的手段,逐步找到出错的行。

3.4.3 野指针: free/delete释放了的对象 
指针指向一个动态分配的对象,被free/delete释放之后,该指针不再可用。

#include 
#include 
#include 
struct Object
{
    int id;
    char name[32];
};

void show(Object* p)
{
    printf("Object [%d, %s] \n", p->id, p->name);
}
int main()
{
    Object* p = (Object*)malloc(sizeof(Object));
    p->id = 123;
    strcpy(p->name, "邵发");

    free(p); // p指向的内存被释放

    p->id = 12; //<--这里有错,不可再对其访问
    show(p);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

[参考习题 #145]

此时VC不能自动找到错误的精确位置,应该按照用单步调试的手段,逐步找到出错的行。

3.4.4 野指针:不恰当的指针强制转换 
关于指针强制转换的各种情况,请参考此文: 
指针类型的转换 http://tieba.baidu.com/p/4103000163

这样会导致各种类型的错误提示,并没有统一的错误特征。所以指针强制转时,必须要对它有足够的理解才能使用。

#include 
int main()
{
    int a = 10;
    double* p = (double*) &a; 
    *p = 123.345; // 程序崩溃
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9








2

用depends.exe查看exe依赖的dll及其版本号。

崩溃的时候在弹出的对话框按相应按钮进入调试,按Alt+7键查看Call Stack即“调用堆栈”里面从上到下列出的对应从里层到外层的函数调用历史。双击某一行可将光标定位到此次调用的源代码或汇编指令处,看不懂时双击下一行,直到能看懂为止。

你可能感兴趣的:(C++)