调试段错误和指针问题

(gdb) print  x
$1 = 0x0

  对于程序员新手,调试与指针相关的错误简直就是恶梦。“段错误(core dumped)”是个非常模糊的错误信息,更糟糕的是,当奇怪的bug出现但是这个bug不会引起段错误--但可能导致内存以一种意想不到的方式被重写。

  但是发现指针引发的问题比你想像中要简单。这些段错误最终会成为一些很容易发现的错误,使用一些特殊的工具,比如Valgrind , 甚至于发现缓存溢出都变得简单了。

  这个手册假定你对指针有个最基本的了解。系统中需要有调试器,比如GDB,或者是类似GDB这样的调试工具,这样就便于理解下面的示例了。最后,为了便于寻找缓存溢出以及其它的无效内存使用,你最好熟悉Valgrind, 尽管下面的例子都用不到它。

什么是段错误

  当你的程序运行的时候,它会存取内存的某一部分。

  首先,函数中的局部变量;这些变量存储在栈stack中。

  其次,有些内存是在运行时分配的(C中的malloc, C++中的new),存储在堆heap(你也许可能听过“自由存储”)中。

  你的程序仅仅允许接触到上面所提到的内存空间。任何超出以上区域的存取都会引发段错误。段错误通常被称为segfaults。

  

  有4种常见的错误会引发段错误:引用NULL;引用一个未初始化的指针;引用一个已经被释放的指针(delete, in C++)或者是超出范围(比如函数中的数组);写到数组以外的地方。

  第5种引发段错误的原因是递归函数使用了所有的栈空间。有些系统中,会产生“栈溢出”报告;在另一些系统中,仅仅是另一种形式的段错误。

  调试这些错误的方法都是一样的:将核心文件(core file)载入GDB,然后backtrace,移动到你的代码范围内,并列出引发段错误的代码行。

  例如,运行在一个linux系统,下面有个示例: 

% gdb example core
  使用一个叫core的核心文件载入名为example的程序。核心文件包含GDB重建执行状态的所有信息。

  当载入gdb,能取得如下信息:

Some copyright info

Core was generated by 'example'.
Program terminated with signal 11, Segmentation fault.

Some information about loading symbols
#0  0x0804838c in foo() at t.cpp:14
4               *x = 3;

  因此,执行在第4行的函数foo()处停止,这一行是将数字3分配到指针x所指向的位置。我们已经知道问题出在哪里以及涉及到哪个指针。

(gdb)list
1    void foo()
2    {
3         char *x = 0;
4         *x = 3;
5     }
6
7     int main()
8     {
9          foo();
10        return 0;
(gdb)
      
  这个例子有点太简单,我们很容易就能发现错误。指针x初始化为0,等同于NULL(事实上,NULL就代表0)。

  但假如没有这么明显该怎么做?仅仅输出指针的值可以得到结果。在这种情况下:

(gdb) print  x
$1 = 0x0
  打印出x表明它指向内存地址0x0(0x表明值是用16进制表示的,这种表示法一般用于打印内存地址)。地址0x0是无效的--事实上,它是NULL。如果你引用一个指向0x0的指针,你肯定会得到一个段错误,正如前面所示。


  如果你遇到一些更复杂的问题,例如在系统调用或库函数执行崩溃(或许是因为我们传递了一个未初始化的指针给fgets),我们需要找到在哪里调用了库函数以及是什么原因引起一个段错误。这里有一个调试过程的例子:

#0 0x40194f93 in strcat () from /lib/tls/libc.so.6
(gdb)
  这一次,段错误在strcat的内部发生。这能够说明库函数执行了一些错误的行为吗?不。这意味着我们可能传递了一些坏值到函数。为了调试它,我们需要看一下,传递了什么样的值到strcat。

因此,我们来看一下,什么样的函数调用导致了段错误。

(gdb) backtrace
#0  0x40194f93 in strcat () from /lib/tls/libc.so.6
#1  0x080483c9 in foo() () at t.cpp:6
#2  0x080483e3 in main () at t.cpp:11
(gdb)

  backtrace列出了在程序崩溃的时候有哪些函数被调用了。在这个实例中,foo是被main函数调用的。边上的数字(#0, #1, #2)表明了调用的顺序,从最近的到最长久的。

  

  为了能够看一下每个函数的状态(包裹在一个称为stack frame里),我们可以使用up 和down命令。当下,我们在strcat的栈框架(stack frame)中,这个栈框架包含strcat中的所有局部变量,因为它是栈最上面的函数。我们想往“上”移(向较大的数字上移);这与栈打印的顺序相反。

(gdb) up
#1  0x080483c9 in foo () () at t.cpp:6
6                strcat(x, "end");
(gdb)
  这有点儿作用--我们知道我们有个变量x和一个字符串常量。我们可能应该查看一下strcat函数来确保我们正确地取得参数的顺序。我们查了,问题出在x这里。

(gdb) print x
$1 = 0x0
  又是它:NULL指针。strcat()函数肯定是引用了一个NULL指针,尽管是个库函数。


  NULL通常很容易处理--当我们发现这个空指针时,我们知道,我们没有为它分配内存空间。这就是问题所在。一个常见的错误就是没有检查malloc的返回值,从而确定系统没有超出内存区域。另一个常见的错误是:假设一个调用了malloc  的函数不返回NULL,尽管这个函数是返回malloc的结果。注意在C++中,当你调用new 的时候,如果没有足够的内存空间,会抛出异常,bad_alloc。你的代码应该做好准备处理这种情况。

char * create_memory()
{ 
    char *x = malloc(10);
    if( x == NULL )
    {
         return NULL;
     }
     strcpy(x, "a string");
     return x;
}

void use_memory()
{
     char *new_memory = create_memory();
     new_memory[0] = 'A';
}
  在使用create_memory之前,我们通过检查确定malloc成功分配了内存空间,但是我们没有检查create_memory是否返回了一个有效的指针。直到你在真实的机器上运行你的代码的时候,这个bug才有可能出现,当你在低内存的情况下运行你的代码的时候。

引用求初始化的指针

  指出一个指针是否是未初始化的比指出一个指针是否为NULL要难一点。最好的方法来避免使用一个未初始化的指针就是当你声明一个指针的时候将它初始化为NULL(或者立刻初始化)。如果你确实使用了一个未分配空间的指针,你会被立刻告知。

  如果你在声明指针的时候未将其初始化为NULL,这个指针在以后可能会给你带来麻烦(记住在C或C++中非静态变量不会被自动初始化)。你可能需要指出0x4025e800是否是一个有效的内存。在GDB中通过打印刚才你所分配这个指针的地址。如果它们离得很近,你可能就是正确地分配了内存。当然,不是在所有的系统上都可以保证这个规则有效。

  在某些情况下,你的调试器依据存储在指针中的值来判断地址是否有效。例如,在下面的例子中,GDB表明,char *x,设置成指向内存地址"30",是不可存取的。

(gdb) print x
$1 = 0x1e <out of bounds>
(gdb) print *x
Cannot access memory at address 0xle
  从最开始的时候就将变量设置为NULL。


引用已释放的内存

  这是另一个很有趣的bug,因为你正在工作于看起来像是正确的内存地址上。最好的方法来处理这种情形就是再一次预防:当你释放了指针以后,将它设置为NULL。那样的话,如果你以后不再使用它,你会得到另一个“引用NULL”的bug,这个bug更容易追踪。

  另一种形式的bug,就是处理超过范围的内存。如果你声明了一个局部数组如下:

char * return_buffer()
{
    char x[10];
    strncpy(x, "a string", sizeof(x));
    return x;
}
  一旦函数返回,数组x就不再有效。这确实是个有趣的bug,因为当你在GDB中打印内存地址的时候,它看起来就像是有效的。事实上,你的代码可能在某些时候是能工作的。注意从函数返回的指针,如果那个指针给你带来麻烦,检查函数并查看一下,指针是否指向了函数内的局部变量。注意,如果返回一个使用malloc或者new分配的内存单元的指针,这是完全可以的,但是不能返回一个指向静态数组的指针(例如,char x[10])。

  像Valgrind这们的工具可以立刻帮助你查找到这些bug,因为它们监视内存确保它是有效的。如果内存无效,Valgrind会警告你。


写到数据的末尾

  通常,如果你写到数组的边界,引起段错误的那一行首先应该是数组存取。当然,有时候,你不会真正地引起一个段错误,当你写到数组之后的位置上,你仅仅是注意到某些变量值阶段性地变化着。这是个很难追踪的bug;一个选择就是设置你的调试器来追踪变量的变化,并且运行你的程序直到变量的值改变。你的调试器会在那条指令处停止,你就可以看一下程序的行为是否正常。

(gdb) watch [variable name]
Hardware watchpoint 1: [variable name]
(gdb) continue
...
Hardware watchpoint 1: [variable name]

Old value = [value1]
New value = [value2]
  当你处理动态分配的内存时,这个方法是很有用的。

栈溢出

  栈溢出问题同其它的指针相关的问题不是一种类型的。在这种情况下,程序中不需要一个单独的明确的指针;你仅仅需要一个递归函数。然而,这是关于段错误的一个准则,在某些系统上,栈溢出将报告成是段错误。

  为了在GDB中诊断出栈溢出,你只需要做一个backtrace:

(gdb) backtrace
#0  foo() () at t.cpp:5
#1  0x08048404 in foo() () at t.cpp:5
#2  0x08048404 in foo() () at t.cpp:5
#3  0x08048404 in foo() () at t.cpp:5
[...]
#20 0x08048404 in foo() () at t.cpp:5
#21 0x08048404 in foo() () at t.cpp:5
#22 0x08048404 in foo() () at t.cpp:5
---Type  to continue, or q  to quit---

  如果你发现一个函数被多次调用,这就是栈溢出的一个很好的提示。

  典型的,你需要分析递归函数确保基部是被正确覆盖的。例如,在计算factorial函数中:

int factorial(int n)
{
    // What about n < 0?
    if(n == 0)
    {
        return 1;
    }
    return factorial(n-1) * n;
}
  在这种情况下,基本的n被赋值为0已经被覆盖,但是如果n < 0 呢?在有效的输入,函数正常工作。但是如果是无效的输入例如-1?

  你也必须保证你的基本部分是可达到的。即使你有正确的base case。如果你没有正确地处理base case ,函数将永远不会终止。

int factorial(int n)
{
    if(n <= 0)
    {
        return 1;
    }
    // Ooops, we forgot to subtract 1 from n
    return factorial(n) * n;
}

总结

  段错误是非常讨厌的而且很难追踪当你初学编程的时候。随着时间的推移,你会发现段错误也就那几种类型,而且相对比较容易追踪。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
段错误是一种常见的错误类型,通常是由于访问无效的内存地址引起的。在调试 Linux 驱动程序时遇到段错误,可以按照以下步骤进行排查和解决: 1. 确认段错误的位置:可以使用调试工具(如 gdb)来定位段错误发生的位置。通过在代码中插入调试语句或设置断点,可以逐步执行程序并观察在哪个特定的代码行发生段错误。 2. 检查指针和数组:段错误通常与指针或数组的访问有关。确保您的指针已正确初始化并分配了内存。检查数组的索引是否超出了范围,并避免访问已释放的内存。 3. 检查内存分配和释放:如果您使用动态内存分配函数(如 malloc 或 kmalloc),请确保在使用完内存后进行适当的释放(free 或 kfree)。内存泄漏可能导致内存耗尽或无效的内存访问。 4. 检查函数参数和返回值:确保函数参数的类型和数量与函数声明匹配,并正确处理函数的返回值。传递无效的参数或对未初始化的返回值进行操作可能导致段错误。 5. 检查系统调用和驱动接口:如果您的驱动程序使用系统调用或与其他模块或设备进行交互,请确保正确处理错误情况和无效的参数。使用适当的错误处理机制,如返回适当的错误代码或设置错误标志。 6. 使用调试工具:除了 gdb 之外,还可以使用其他调试工具,如 Valgrind(用于检测内存错误)或 Ftrace(用于跟踪函数调用和内核事件)。这些工具可以帮助您更详细地分析和定位段错误。 7. 重现和记录问题:尝试重现段错误,并记录相关的环境、输入或操作。这将有助于更深入地分析问题并找到解决方案。 请注意,以上步骤只是一些常见的排查方法,具体的解决方案可能因问题的具体情况而异。如果问题仍然存在,请尝试搜索相关的文档、论坛或寻求其他开发者的帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值