使用调试器

关于调试器的第一个简明提示:总是使用调试器。

一个调试侦探故事

调试代码:

 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
29
30
31
32
33
34
35
36
37
38
#include <math.h>
#include <stdio.h> //size_t

typedef struct meanvar {double mean, var;} meanvar;

meanvar mean_and_var(const double *data){
    long double avg = 0,
          avg2 = 0;
    long double ratio;
    size_t count= 0;
    for(size_t i=0;  !isnan(data[i]); i++){
        ratio = count/(count+1);
        count ++;
        avg   *= ratio;
        avg2  *= ratio;
        avg   += data[i]/(count +0.0);
        avg2  += pow(data[i], 2)/(count +0.0);
    }
    return (meanvar){.mean = avg,
                    .var = avg2 - pow(avg, 2)}; //E[x^2] - E^2[x]
}

int main(){
    double d[] = { 34124.75, 34124.48,
                   34124.90, 34125.31,
                   34125.05, 34124.98, NAN};

    meanvar mv = mean_and_var(d);
    printf("mean: %.10g var: %.10g\n", mv.mean, mv.var*6/5.);

    double d2[] = { 4.75, 4.48,
                    4.90, 5.31,
                    5.05, 4.98, NAN};

    mv = mean_and_var(d2);
    mv.var *= 6./5;
    printf("mean: %.10g var: %.10g\n", mv.mean, mv.var);
}

使用CFLAGS=”-g -Wall -std=gnu11 -O3” make stddev编译代码,gdb stddev_bugged调试代码。

Q: 这个程序是做什么的? A: run命令运行程序。

1
2
3
4
(gdb) r
mean: 5687.496667 var: 194085710
mean: 0.83 var: 4.1334
[Inferior 1 (process 22734) exited normally]

Q: main函数中的代码符合我们的输出吗? A: list命令显示源代码

Q: 我们如何查看mean_and_var发生了什么? A: 我们想要程序在mean_and_var暂停,因此我们在那里设置一个断点:

1
2
(gdb) b mean_and_var
Breakpoint 1 at 0x400820: file stddev_bugged.c, line 16.

Q: data是我们想的那样吗? A: 我们可以通过print简写为p查看data:

1
2
(gdb) p *data
$2 = 34124.75

我们只得到第一个元素,但GDB有一个特殊的@语法打印数组元素。打印10个元素[LLDB: mem read -tdouble -c10 data]:

1
2
3
4
(gdb) p *data@10
$3 = {34124.75, 34124.480000000003, 34124.900000000001, 34125.309999999998, 
  34125.050000000003, 34124.980000000003, nan(0x8000000000000), 0, 
  4.9406564584124654e-324, 2.0732560687596022e-317}

注意表达式前面的星号,没有它,我们会得到10个地址。

Q: 这和main发送的匹配吗? A: 我们可以通过bt回溯:

1
2
3
(gdb) bt
#0  mean_and_var (data=data@entry=0x7fffffffdd50) at stddev_bugged.c:16
#1  0x0000000000400520 in main () at stddev_bugged.c:38

让我们看看data在frame 1是什么,首先切换到frame 1:

1
2
3
(gdb) f 1
#1  0x0000000000400520 in main () at stddev_bugged.c:38
38      meanvar mv = mean_and_var(d);

在这一层,data数组叫d:

1
2
3
(gdb) p *d@7
$5 = {34124.75, 34124.480000000003, 34124.900000000001, 34125.309999999998, 
  34125.050000000003, 34124.980000000003, nan(0x8000000000000)}

我们可以返回frame 0通过f 0或与栈关联的移动:

1
2
3
(gdb) down
#0  mean_and_var (data=data@entry=0x7fffffffdd50) at stddev_bugged.c:16
16  meanvar mean_and_var(const double *data){

注意up和down参考数字顺序。由bt产生的列表,数字最小的层在最上面,up走向数字大的层,down走向数字小的层。

Q: 这是线程的问题吗? A: 我们可以得到一个线程列表通过info threads[LLDB: thread list]:

1
2
3
4
(gdb) info threads
  Id   Target Id         Frame 
* 1    process 6125 "stddev_bugged" mean_and_var (
    data=data@entry=0x7fffffffdd50) at stddev_bugged.c:16

如果有多个线程,我们可以使用thread num(GDB)或thread select num(LLDB)切换过去。GDB用户可以在.gdbinit添加set print thread-events off关闭每一个线程烦人的通知。

Q: mean_and_var在做什么? A: 我们可以重复步进程序的下一行:

1
2
3
4
(gdb) n
18            avg2 = 0;
(gdb) n
16  meanvar mean_and_var(const double *data){

没有输入敲击回车重复上一个命令。还有其他的步进选项(snuc)。使用步进调试代码将会花费很多时间。且函数中有个循环,我们可以在循环中间设置一个断点:

1
2
(gdb) b 25
Breakpoint 2 at 0x400715: file stddev_bugged.c, line 25.

现在我们有2个断点,可以使用info break查看断点列表。

1
2
3
4
5
6
7
(gdb) info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00000000004006c0 in mean_and_var 
                                                   at stddev_bugged.c:16
    breakpoint already hit 1 time
2       breakpoint     keep y   0x0000000000400715 in mean_and_var 
                                                   at stddev_bugged.c:25

不再需要mean_and_var头部的断点了,可以禁止它[LLDB: break dis 1]:

1
(gdb) dis 1

你可以重新激活一个断点enable 1(GDB)或break enable 1(LLDB)。如果你不再需要一个断点可以删除它,del 1(GDB)或break del 1(LLDB)。