~Linux C_2/3/11_GDB DEBUG

Outline

  1. printf 调试
  2. assert 调试
  3. gdb 调试
  4. 异常处理

 

 

====程序格式(风格)的变换:indent ====

indent -kr -i8 main.c -kr选项表示K&R风格,-i8表示缩进8个空格的长度。如果没有指定-nut选项,则每8个缩进空格会自动用一个Tab代替。

 

====printf调试====

程序中除了一目了然的Bug之外都需要一定的调试手段来分析到底错在哪。到目前为止我们的调试手段只有一种:根据程序执行时的出错现象假设错误原因,然后在代码中适当的位置插入printf,执行程序并分析打印结果,如果结果和预期的一样,就基本上证明了自己假设的错误原因,就可以动手修正Bug了,如果结果和预期的不一样,就根据结果做进一步的假设和分析。

printf("%s, %d\n", __func__, __LINE__);

C语言函数不定参数实现方式

    • va_list, 指向参数列表
    • va_start, 设置参数列表的起始位置
    • va_arg, 取参数
    • va_end, 析构

 

#include <stdio.h>  
#include <stdarg.h>  
#include <string.h>  
  
int sumi(int c, ...)  
{  
    va_list argptr;  
    va_start(argptr, c);      // 初始化变元指针  

    int sum = c;  

    c = va_arg(argptr, int);  // 作为下一个参数类型的参数类型,返回不定参数  

    printf("%d\n", c);        // c保存第一个不定参数  
    while(0 != c)             // 末尾特定字符指示结束  
    {  
        sum = sum + c;  
        c = va_arg(argptr, int);  
    }  

    va_end(argptr);
    return sum;  
}  

double sum_series(int num, ...)  
{  
    double sum = 0.0, t;  

    va_list argptr;  
    va_start(argptr, num);    // 初始化变元指针  

    while (num--)  
    {  
        t = va_arg(argptr, double);  
        sum = sum + t;  
    }  

    va_end(argptr);  
    return sum;  
}  

int main()  
{  
    int i = sumi(1,2,3,4,5,6,7,8,9,0);  

    printf("%d\n", i);  

    double d = sum_series(1.1, 1.2, 1.3, 1.4);  
    printf("%f\n", d);  
  
    return 0;  
}  

OUTPUT: 

2
45
1.200000

 

但是注意不能使用:

总之,va_arg(ap,type)中的type绝对不能为以下类型:
——char、signed char、unsigned char
——short、unsigned short
——signed short、short int、signed short int、unsigned short int
——float

 

 

====assert调试====

Ref: assert()断言函数,用于在调试过程中捕捉程序错误

在大部分编译器下,assert() 是一个宏;
在少数的编译器下,assert() 就是一个函数。
 
原则:被检测的表达式最好不要太复杂
被除数不能为 0,所以我们加入了 assert() 来检测错误。
  assert(n != 0); //写作 assert(n) 更加简洁
 
 
原则:不要用会改变环境的语句作为断言的表达式。
  assert(++i <= 100);
 
技巧:生产环境,采用NDEBUG使断言失效。
#ifdef NDEBUG
#define assert(e) ((void)0)
#else
#define assert(e)  \
    ((void) ((e) ? ((void)0) : __assert (#e, __FILE__, __LINE__)))
#endif

 

Ref: 在C语言中用ASSERT调试的八个技巧

  • 技巧1:记住ASSERT的定义
  • 技巧2:使用ASSERT验证函数的先决条件(入口参数有效性检查)  
  • 技巧3:使用ASSERT验证函数的后置条件(出口数据有效性检查)
  • 技巧4:不要把ASSERT用于错误处理
  • 技巧5:ASSERT仅对开发有意义,不能用于生产
  • 技巧6:不允许断言有副作用
  • 技巧7:断言应该占代码的1%至3%
  • 技巧8:将断言用作 可执行代码 注释

 

 

====gdb调试====

01. 一起开始

本章我们介绍一种很强大的调试工具gdb,可以完全操控程序的运行,使得程序就像你手里的玩具一样,叫它走就走,叫它停就停,并且随时可以查看程序中所有的内部状态,比如各变量的值、传给函数的参数、当前执行的代码行等。掌握了gdb的用法之后,调试手段就更加丰富了。但要注意,即使调试手段丰富了,调试的基本思想仍然是

  “分析现象 --> 假设错误原因 --> 产生新的现象去验证假设”

 

这样一个循环,根据现象如何假设错误原因,以及如何设计新的现象去验证假设,这都需要非常严密的分析和思考,如果因为手里有了强大的工具就滥用而忽略了分析过程,往往会治标不治本地修正Bug,导致一个错误现象消失了但Bug仍然存在,甚至是把程序越改越错。

本章通过初学者易犯的几个错误实例来讲解如何使用gdb调试程序,在每个实例后面总结一部分常用的gdb命令。

在编译时要加上-g选项,生成的可执行文件才能用gdb进行源码级调试:

$ gcc -g main.c -o main
$ gdb main //这个main已是一个具有gdb功能的可执行文件

GNU gdb 6.8-debian
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.

观察下文件大小:

-rwxr-xr-x 1 root root 6381 08-30 16:33 main1 //具有gdb功能的可执行文件明显要大些
-rwxr-xr-x 1 root root 4901 08-30 17:17 main2

结论:-g选项的作用是在可执行文件中加入源代码的信息,比如可执行文件中第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证gdb能找到源文件。

 

02. 简单的命令尝试

(gdb) help //查看命令的类别

List of classes of commands:

aliases -------------- Aliases of other commands
breakpoints ---------- Making program stop at certain points
data ----------------- Examining data
files ---------------- Specifying and examining files
internals ------------ Maintenance commands
obscure -------------- Obscure features
running -------------- Running the program
stack ---------------- Examining the stack
status --------------- Status inquiries
support -------------- Support facilities
tracepoints ---------- Tracing of program execution without stopping the program
user-defined --------- User-defined commands

Type "help" followed by a class name for a list of commands in that class.
Type "help all" for the list of all commands.
Type "help" followed by command name for full documentation.
Type "apropos word" to search for commands related to "word".
Command name abbreviations are allowed if unambiguous.

==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== (gdb) help files //进一步查看某一类别中有哪些命令 Specifying and examining files. List of commands: add-shared-symbol-files -- Load the symbols from shared objects in the dynamic linker's link map add-symbol-file -- Load symbols from FILE add-symbol-file-from-memory -- Load the symbols out of memory from a dynamically loaded object file cd -- Set working directory to DIR for debugger and program being debugged core-file -- Use FILE as core dump for examining memory and registers directory -- Add directory DIR to beginning of search path for source files edit -- Edit specified file or function exec-file -- Use FILE as program for getting contents of pure memory file -- Use FILE as program to be debugged forward-search -- Search for regular expression (see regex(3)) from last line listed generate-core-file -- Save a core file with the current state of the debugged process list -- List specified function or line ...
==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== (gdb) list 1 //从第一行开始列出源代码 1 #include <stdio.h> 2 3 int add_range(int low, int high) 4 { 5 int i, sum; 6 for (i = low; i <= high; i++) 7 sum = sum + i; 8 return sum; 9 } 10 (gdb) l add_range //列出该函数 1 #include <stdio.h> 2 3 int add_range(int low, int high) 4 { 5 int i, sum; 6 for (i = low; i <= high; i++) 7 sum = sum + i; 8 return sum; 9 } 10
==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== $ gdb main //从main开始调试 ... (gdb) start Breakpoint 1 at 0x80483ad: file main.c, line 14. Starting program: /home/akaedu/main main () at main.c:14 14 result[0] = add_range(1, 10); //gdb停在main函数中“变量定义之后”的第一条语句处等待我们发命令 (gdb)

==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== (gdb) next //看下一条执行指令 15 result[1] = add_range(1, 100); (gdb) (直接回车) //表示重复之前的命令 16 printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]); (gdb) (直接回车) result[0]=55 result[1]=5105 17 return 0;
==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== (gdb) start //重新开始debug The program being debugged has been started already. Start it from the beginning? (y or n) y Breakpoint 2 at 0x80483ad: file main.c, line 14. Starting program: /home/akaedu/main main () at main.c:14 14 result[0] = add_range(1, 10); (gdb) step //钻入add_range函数 add_range (low=1, high=10) at main.c:6 6 for (i = low; i <= high; i++)

 

03. 基础调试

1。查看栈桢:backtrace
2。选择栈桢:frame [数字]
3。局部变量:info locals
4。任意变量:print [变量名]

(gdb) backtrace //查看函数调用的栈帧
#0 add_range (low=1, high=10) at main.c:6 //add_range的 栈帧编号为0; 可见当前的add_range函数是被main函数调用的,main传进来的参数是low=1, high=10。
#1 0x080483c1 in main () at main.c:14 //main 函数的 栈帧编号为1。


(gdb) info locals //查看add_range函数局部变量的值:
i = 0
sum = 0


(gdb) frame 1 //选择1号栈帧
#1 0x080483c1 in main () at main.c:14
14 result[0] = add_range(1, 10);
(gdb) info locals 
result = {0, 0, 0, 0, 0, 0, 134513196, 225011984, -1208685768, -1081160480, 
...
-1208623680} 

(gdb) print sum //打印出任意存在的变量名。
$1 = 3 //这里的$1表示gdb保存着这些中间结果,$后面的编号会自动增长,在命令中可以用$1、$2、$3等编号代替相应的值。

==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== (gdb) finish //用finish命令让程序一直运行到从当前函数返回为止 Run till exit from #0 add_range (low=1, high=10) at main.c:6 0x080483c1 in main () at main.c:14 14 result[0] = add_range(1, 10); Value returned is $2 = 55

 

04. 显示变量 display

(gdb) display sum //每次都显示 sum 的值 1: sum = -1208103488
(gdb) n 9 scanf("%s", input);
1: sum = 0

(gdb)
 123
10 for (i = 0; input[i] != '\0'; i++)
1: sum = 0

undisplay 1 //跟踪号是1,取消它的跟踪显示

 

 

 

调试之Boss级招数:

(1) set var sum=?
(2) print var=?

(gdb) set var sum=0 //可以立即改一变量的值,可以继续调试,看是否还有其他的BUG。
(gdb) finish
Run till exit from #0 add_range (low=1, high=100) at main.c:6
0x080483db in main () at main.c:15
15 result[1] = add_range(1, 100);
Value returned is $4 = 5050

16 printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]);
(gdb) (直接回车)
result[0]=55
result[1]=5050
17 return 0;


(gdb) print result[2]=33 //后面需要跟的是表达式
$5 = 33 //返回值就是改变的变量值
(gdb) print printf("result[2]=%d\n", result[2]) //printf是个有点特殊的函数,返回值是打印出的字符数。
result[2]=33
$6 = 13

 

  (3) 反汇编、寄存器查看:

(gdb) start
...
main () at main.c:14
14 foo(2, 3);

(gdb) s foo (a
=2, b=3) at main.c:9 9 return bar(a, b);
(gdb) s bar (c
=2, d=3) at main.c:3 3 int e = c + d;
(gdb) disassemble
//反汇编当前函数或者指定的函数 Dump of assembler code for function bar: 0x08048394 <bar+0>: push %ebp 0x08048395 <bar+1>: mov %esp,%ebp 0x08048397 <bar+3>: sub $0x10,%esp 0x0804839a <bar+6>: mov 0xc(%ebp),%edx 0x0804839d <bar+9>: mov 0x8(%ebp),%eax 0x080483a0 <bar+12>: add %edx,%eax 0x080483a2 <bar+14>: mov %eax,-0x4(%ebp) 0x080483a5 <bar+17>: mov -0x4(%ebp),%eax 0x080483a8 <bar+20>: leave 0x080483a9 <bar+21>: ret End of assembler dump.
(gdb) si
//一条指令一条指令地单步调试 0x0804839d 3 int e = c + d; (gdb) si 0x080483a0 3 int e = c + d; (gdb) si 0x080483a2 3 int e = c + d; (gdb) si 4 return e; (gdb) si 5 }
(gdb) bt #
0 bar (c=2, d=3) at main.c:5 #1 0x080483c2 in foo (a=2, b=3) at main.c:9 #2 0x080483e9 in main () at main.c:14
(gdb) info registers
//显示所有寄存器的当前值 eax 0x5 5 ecx 0xbff1c440 -1074674624 edx 0x3 3 ebx 0xb7fe6ff4 -1208061964 esp 0xbff1c3f4 0xbff1c3f4 ebp 0xbff1c404 0xbff1c404 esi 0x8048410 134513680 edi 0x80482e0 134513376 eip 0x80483a8 0x80483a8 <bar+20> eflags 0x200206 [ PF IF ID ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51
(gdb) x
/20 $esp 0xbff1c3f4: 0x00000000 0xbff1c6f7 0xb7efbdae 0x00000005 0xbff1c404: 0xbff1c414 0x080483c2 0x00000002 0x00000003 0xbff1c414: 0xbff1c428 0x080483e9 0x00000002 0x00000003 0xbff1c424: 0xbff1c440 0xbff1c498 0xb7ea3685 0x08048410 0xbff1c434: 0x080482e0 0xbff1c498 0xb7ea3685 0x00000001 (gdb)

 

 

 

====断点====

****设置断点****

(gdb) break 9 //在第九行设置断点,参数也可以是函数名
Breakpoint 2 at 0x80483bc: file main.c, line 9.

和continue命令(简写为c)配合使用,连续运行而非单步运行,程序到达断点会自动停下来。

(gdb) continue
Continuing.
input=123 //程序阶段性输出

Breakpoint 2, main () at main.c:9 //断点信息
9 scanf("%s", input); //断点后的函数
1: sum = 123 //显示的变量

****查看断点****

(gdb) info breakpoints
Num Type Disp Enb Address What
2 breakpoint keep y 0x080483c3 in main at main.c:9 //断点2 位置
breakpoint already hit 1 time 3 breakpoint keep y 0x08048411 in main at main.c:12 //断点3 位置

 

****删除断点****

(gdb) delete breakpoints 2 //删掉断点
(gdb) i breakpoints 
Num Type Disp Enb Address What
3 breakpoint keep y 0x08048411 in main at main.c:12

****暂停断点****

(gdb) disable breakpoints 3 //暂停断点
(gdb) info breakpoints 
Num Type Disp Enb Address What
3 breakpoint keep n(状态) 0x08048411 in main at main.c:12

(gdb) enable 3 //启用断点
(gdb) info breakpoints 
Num Type Disp Enb Address What
3 breakpoint keep y(状态) 0x08048411 in main at main.c:12

(gdb) delete breakpoints //删除断点
Delete all breakpoints? (y or n) y

(gdb) info breakpoints
No breakpoints or watchpoints.

****条件断点****

(gdb) break 9 if sum != 0 //if [条件表达式]
Breakpoint 5 at 0x80483c3: file main.c, line 9.

(gdb) info breakpoints 
Num Type Disp Enb Address What
5 breakpoint keep y 0x080483c3 in main at main.c:9
stop only if sum != 0

(gdb) run //重新从程序开头连续运行
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/akaedu/main 
123
input=123

Breakpoint 5, main () at main.c:9
9 scanf("%s", input);
1: sum = 123

 

 

====观察点====

**** 内存状态 ****

(gdb) x/13b input //x命令打印指定存储单元的内容。7b是打印格式,b表示每个字节一组,7表示打印7组2: i = 3
2: i = 3
1: sum = 123
0xbfffe727: 49 50 51 0 0 123 0 0
0xbfffe72f: 0 3 0 0 0 

    上述观察数据可以看出变量在内存中的方式。

 

**** 设置状态点 ****

(gdb) watch input[5] //观察点是当程序访问某个存储单元时中断。
Hardware watchpoint 2: input[5]

(gdb) info watchpoints 
Num Type Disp Enb Address What
2 hw watchpoint keep y input[5]

 

**** 到达状态点 ****

(gdb) continue
Continuing.
Hardware watchpoint 2: input[5]

Old value = 0 '\0'
New value = 1 '\001'
0x0804840c in main () at main.c:11
11 for (i = 0; input[i] != '\0'; i++)

 

 

====段错误====

$ gdb main
...
(gdb) r
Starting program: /home/akaedu/main 
123

Program received signal SIGSEGV, Segmentation fault.
0xb7e1404b in _IO_vfscanf () from /lib/tls/i686/cmov/libc.so.6 //段错误出现的原始调用

(gdb) bt
#0 0xb7e1404b in _IO_vfscanf () from /lib/tls/i686/cmov/libc.so.6
#1 0xb7e1dd2b in scanf () from /lib/tls/i686/cmov/libc.so.6 //通过以上调用发现程序中的出错函数位置

 

 

 

====调错实战==== 

**** 1 ****

目的:多一个数组,改变了“未初始化”变量的在栈中的位置。

#include<stdio.h>

int add_range(int low, int high)
{
  int i, sum;
  for (i = low; i <= high; i++)
  sum = sum + i;
  return sum;
}

int main(void)
{
  int a = 0, b;
  a = add_range(1, 10);
  b = add_range(1, 100);
  printf("a = %d\n"  //C99的风格,比较有用,但慎用。
      "b = %d\n",
       a, b); 
  return 0;
}

Result:
a = 134518299
b = 134523349

结论: 结果是完全的乱码,在于sum在被调函数中没有初始化,所以sum的值是不定的。
          而且sum初次的值也不是默认为0。


#include<stdio.h>

int add_range(int low, int high)
{
  int i, sum;
  for (i = low; i <= high; i++)
    sum = sum + i;
  return sum;
}

int main(void)
{
  int result[100]; //关键就是这里数组的定义,长度影响着 i, sum 的栈的位置。
  result[0] = add_range(1, 10);
  result[1] = add_range(1, 100);
  printf("result[0] = %d\n"
           "result[1] = %d\n",
            result[0], result[1]); 
  return 0; 
} 

Result:
result[0] = 55
result[0] = 5105

 

 

**** 2 ****

案例:数组越界问题,尤其是“用户输入”时;

#include <stdio.h>

int main(void)
{
    int sum = 0, i = 0;
    char input[5]; //只能容纳五个字符,注意是包括:\0

    while (1) {
      sum = 0;
      scanf("%s", input); //如果这里输入了五个

   for (i = 0; input[i] != '\0'; i++)     sum = sum*10 + input[i] - '0'; //数组没有越界检测,这里需要注意的是:数组越界了之后会覆盖掉哪些数据。
  printf("input=%d\n", sum);   } return 0; } Result: 12345 input=12345090 //出现异常

(gdb) x
/13b input //x命令打印指定存储单元的内容。7b是打印格式,b表示每个字节一组,7表示打印7组2: i = 3 2: i = 3 1: sum = 123 0xbfffe727: 49 50 51 0 0 123 0 0 0xbfffe72f: 0 3 0 0 0

分析:
  上述观察数据可以看出变量在内存中的方式,如果输入的数据过大就会把“123"这个数据覆盖掉,就相当于123变成0了,然后就产生了程序出错。
  覆盖的123的位置仍然会随着for循环的继续而sum改变着自己,for的结束条件就成了遇到下一个0,相当于\0。

 

 

**** 3 ****

注意:C语言的数组没有越界判断,自己要当心点

#include <stdio.h>

int main(void)
{
  int sum = 0, i = 0;
  char input[5];

  scanf("%s", input);
  for (i = 0; input[i] != '\0'; i++) { //这一句就是导致BUG的根本原因。
    if (input[i] < '0' || input[i] > '9') {
      printf("Invalid input!\n");
      sum = -1;
      break;
    }
    sum = sum*10 + input[i] - '0';
  }
  printf("input=%d\n", sum);
  return 0;
}

Result:
1234567890abcdef
Invalid input!
input=-1
Segmentation fault

gdb指出,如果某个函数的局部变量发生访问越界,有可能并不立即产生段错误,而是在函数返回时产生段错误

原因:

  该程序有时发生段错误有时却不发生。因为输入不同的字符串,导致的覆盖sum变量而留下的隐患——什么时候会碰到\0而使for循环停止。

 

 

**** 4 ****

趣味:scan函数竟然通过变量载体的类型carry输入内容

#include<stdio.h>

int main(void)
{
  int sum = 50; 
  scanf("%d", &sum);
  printf("%d\n", sum);
  return 0;
}

Result:
12345asldfkjaslfkjaslfj
12345

scanf函数是个有点特殊的函数,上述实验可以发现内部的%d导致不识别后面的字符串。

这也从侧面说明了一个问题:
  scanf("%d", sum);
当函数变成这种错误的形式,为什么输入字符串会不出现段错误——因为字符串就根本没有写入sum。

 

 

 

==== C的异常处理 ====

C++异常处理不是什么问题。

C的异常处理是个技巧问题。

异常简述(一):C语言中的异常处理机制

为以下的内容做铺垫:

--------------------------------------------------------------------------------------------------------------

1.直接终止程序(自杀)

2.返回一个错误的值,附加错误码

3.返回一个合法的值,让程序处于某种非法的状态

4.调用一个预先准备好在出现"错误"的情况下使用的函数.(函数作为参数,回调函数)

5、通过暴力的方式解决 exit(), abort()

6、使用goto语句

7、使用setjmp()与longjmp()

--------------------------------------------------------------------------------------------------------------

 

思路:若出了问题,就longjmp到setjmp的起始位置。

#include<stdio.h>
#include<setjmp.h>
 
jmp_buf mark;
 
int Div(int a,int b){
    if(b==0){
        longjmp(mark,1);  //会使state = 1
    }
    return a/b;
}
int main(){
    int State = setjmp(mark);   //保存寄存器相关信息,初始值为0 -- 返回值代表了某寄存机上的值
    if(State==0){
        Div(4,0);
    }else{
        switch(State){
            case 1:
                printf("除0异常!\n");
        }
    }
    return 0;
}

 

 

 

以上取自于老博客:

http://hi.baidu.com/kebey2004/item/fa6392d333717f2838f6f7fe

http://hi.baidu.com/kebey2004/item/a910e58d3f888156e73d19fe

http://hi.baidu.com/kebey2004/item/b1fe8dffe1a82cc30cd1c8ff

http://hi.baidu.com/kebey2004/item/f8ae322b7b934f0f73863eff

posted @ 2012-11-14 11:13  郝壹贰叁  阅读(431)  评论(0编辑  收藏  举报