Linux内核开发二:GDB调试

在 Linux 下开发中,遇到程序功能异常或者奔溃的时候,就需要借助调试工具对程序进行检查。GDB

 

是 GNU 开源组织发布的一个强大的 Linux 下的命令行程序调试工具。-g 标志是对程序进行调试性编译时常用的一个选项,必须在编译链接每个需要调试的源文件时都加上这个选项。

 

1、GDB 调试可执行文件

 

通常情况下,gdb 可调试程功能错误的可执行文件,如排序不完整序……程序奔溃(core)的可执行文件,

 

如信号 11SIGSEGV 导致的段错误…..

 

启动 gdb开始调试可执行程序的格式为:gdb 可执行文件(带路径)

 

运行程序:使用 run 命令来执行这个程序,可简写成 r。如果调试带参数的程序,在 run 命令中给出的所有参数都将作为程序的参数传递给程序。如果还没有设置断点的话,程序就一次执行完或者直接奔溃。

 

断点调式: break 行号或函数名,就会指定行或进入此函数时设置断点,当执行 run 后,程序在第一个断点处暂停,以后每次执行都断点处程序都会停下来。

 

可以用 info break 查看当前程序设置的所有断点的详细信息,在进入某个函数之后使用 finish 命令可一次性退出该函数的运行。

 

cont 或 continue 命令继续执行程序直到它遇到下一个断点,next 命令是逐行执行每一行代码。

 

使用 delete 命令删除指定的断点,但通常只是使用 disable 将某个断点禁用,需要的时候再激活就可以了。使用 commands 命令可以在调试过程中打补丁修改程序而不需要改变程序源代码重新编译,具体方法是将某个断点与相应操作结合起来,在程序运行至该断点时不是停下来,而是执行 commands 命令定制的一套指令。如先 commands 2,再 set variable n=n+1,然后 cont,最后 end 保存退出,就是在程序运行至断点 2 处时,将变量 n 加 1,然后继续运行至下一断点。

 

查看函数堆栈:键入 backtrace/bt  where 命令,就会看到程序崩溃时或暂停时的堆栈信息,就是当前所有已调用函数的列表,包括此时的参数取值和行号。

 

检查变量:用调试器检查参数参数、局部变量和全局数据的内容,print 命令的作用就是显示出程序奔溃或暂停时变量和表达式的值。

 

列出程序源代码: gdb 中使用 list 命令默认会打印出围绕当前位置前后的 10 行代码,也可以给 list

 

命令提供一个行号或函数名作为参数,他将显示指定位置前后的源代码。

 

终止当前调试和退出 gdb使用 kill 命令可终止当前的调试,而使用 quit 命令是直接退出 gdb

 

2 、GDB 调试 core 文件

 

2.1 程序 core 掉的原因

 

core 意为核心,在 Linux 中指的是操作系统的内核。通常说程序产生 core 或者进程 core 了指的就是程序运行中出现异常,Linux 内核就会检测到异常向并向该程序发送信号终止当前进程,还可以将程序奔溃的当前状态倒出到 core 文件中,此文件包含了程序运行时的内存、寄存器状态,堆栈指针,内存管理信息等,以便进一步分析。可以说所有的进程 core 都是由于信号直接导致的。

 

很多信号都会导致程序发生 core 异常终止并且产生 core 文件,如用户按下 ctrl + c 组合键发送 SINGINT 信号就会导致进程发生 core 异常终止,但是此信号并不产生 core 文件;按下 Ctrl + \组合键发送

 

SINQUIT 信号也会导致进程 core;不同类型之间的指针之间进行类型转换会导致地址对不齐发送 SIGBUS 信号,导致进程 core 产生 core 文件;向进程发送 SIGK ILL 信号会导致进程发生 core 异常终止,但是也不会产生 core 文件。但是进程发生 core 最主要的原因还是程序因为段错误而接收到 SIGSEGV 信号。

 

所谓的段错误就是就是内存错误,大多是指访问的内存超出了系统所给这个程序的内存空间,一旦程序发生了越界访问,操作系统内核就会产生相应的异常保护,于是 segmentation fault 就出现了。造成段错误的具体原因根据以往的经验总结有:但这些问题麻烦的是并不是必现的。

 

1)内存访问越界,也称为踩内存 a) 由于使用错误的下标,导致数组访问越界

void main()

 

{

 

int a[5] = {0,1,2,3,4} , *p ; p = a ; //p 是数组首地址,*p 就是第一个元素

printf ( “%d\n ” ,*(p + 4*sizeof(int)) ; //p+4 就已经是指向最后一个元素了

 

}

 

b) 使用 strcpystrcatsprintf 等字符串操作函数,将目标字符串写爆等数组或指针越界,应该使用 strncpy

 

strncatsnprintf 等函数防止读写越界。

 

void test()

 

{

 

char str_dest[10] ; //不一定奔溃,但最好应该改为 11

 

char* str_src = ”0123456789” ; // strlenstr_src=10

 

strcpy ( str_dest , str_src ) ; //strcpy 会自动添加个’\0’

 

}

 

C)使用 strlen  sizeof 计算字符串长度问题时没有考虑最后的’\0’,没有加 1 和减 1,造成数组越界。

 

2)非法指针a)访问不存在的内存地址,就是对未初始化的 NULL 空指针读写。 void main()

 

{

 

int *ptr = NULL; *ptr = 100;

 

}

 

b) 使用野指针,野指针不是 NULL 指针,它只是指向了个未知的内存地址,一旦这块内存有重要的数

 

据,就会有被野指针存取的潜在危险。野指针主要有两个来源:指针在声明时没有初始化,既没有用具体的地址值初始化也没有用个变量初始化也没有动态内存初始化;指针在 free 或 delete 掉之后首先会变为野

 

指针,所以需要置为 NULL,之后就是空指针。堆内存 free 释放掉归还内存池以后就变成野指针,就不能再访问了,栈内存在函数结束时就释放,所以函数不能返回局部变量的地址,但是可以返回局部变量本省,因为这仅仅是构造了个临时变量。

 

void main()

 

{

 

short *ptr ; //是野指针,应该(short*) 0x32451234 &num  malloc

 

short var = 0x20 ; //变量在栈里,地址是&var

 

*ptr = var ; (错误) //把野指针指向的地址不变,此地址赋值为 0x20 是危险的。

 

ptr = &var; (正确) //原来野指针指向变量 var,就不再是野指针了。

 

*ptr = 0x30 ;(正确) //和上一行搭配等同 var=0x30,单独使用和第 5 行一样是野指针

 

}

 

c) 访问只读的内存地址

 

void main()

 

{

 

char *ptr = “test” ; //ptr 本身在栈区,指向只读区,可 strcmp 比大小

 

strcpy ( str , “TEST” ); //改值必定段错误,=是改指针本身不是改值

 

char *s1 = (char*)malloc(10*sizeof(char)); //使用 strcpy 该值,使用 strcmp 比大小


 

char s1[] = “abc” ; //使用 strcpy 赋值,使用 strcmp 比大小

 

char s3[] = malloc(sizeof(char)); //初始化就已经非法了

 

}

 

3)栈溢出

 

不要使用大的局部变量或数组(因为局部变量都分配在栈上),而系统栈一般是比较小的,这样容易造成堆栈溢出,破坏系统的栈和堆结构,导致段错误。当程序确实需要大数组时,可以设置为静态变量或全局变量。

 

#define SIZE 1 * 1024 * 1024 void main()

 

{

 

int arr[SIZE] ; //定义超大数组,在 ulimit –s 输出比较小的时候会段错误 int i ;

for ( i = 0 ; i< SIZE ; ++i ){ arr[i] = i ; }

 

}

 

2.2 core 文件的生成、名称和路径

 

我们经常会疑惑有时进程执行最后明显提示 segmentation fault,但是却没有 core dump ,而且是使用了允许产生 core 文件的信号导致程序 down 掉,这是由于系统设置的原因。ulimit 命令用于显示和设置 shell 启动进程所占用的资源,可以控制的各项资源记录在/etc/security/limis.conf :

 

ulimit –a 用来显示当前系统的各项资源设置 ulimit -c <core 文件上限>用以显示和设置 core 文件的大小,单位为 block 块。

ulimit -f <文件大小>用以显示和设置当前系统文件的最大值,单位 block 块。 ulimit - n <打开文件数目>用以显示和设置每个进程允许同时打开文件的最大个数。 ulimit -s <系统栈大小>指定进程系统栈空间的上限,单位为 KB

 

使用 ulimit –c 查看若输出为 0,就表示 core 文件的最大值被设置为 0block,就是关闭 core 文件的产生。通常是使用 ulimit –c unlimited 将 core 文件的大小设置为不受限制。如果指定了具体大小,但是生成的信息超过此大小,将会被裁剪,最终生成一个不完整的 core 文件,在调试此 core 文件的时候,gdb

会提示错误。

 

1)保持/proc/sys/kernel/core_pattern 文件为系统默认不变,使用/proc/sys/kernel/core_uses _pid

 

件控制 core 文件的文件名中是否添加 pid 作为扩展。文件内容为 1,表示添加 pid 作为扩展名,生成的 core 文件格式为 core.xxxx;为 0 则表示系统生成的 core 文件不带其他任何扩展名称,全部命名为 core,新的 core 文件生成将覆盖原来的 core 文件。生成的 core 文件与可执行文件运行命令的同一路径下。可通过以下命令修改此文件:echo "1"> /proc/sys/ kernel/core_uses_pid

 

2/proc/sys/kernel/core_pattern 可以控制 core 文件保存位置和文件名格式。可通过以下命令修改此文件:echo "/corefile/core-%e-%p-%s-%t"> core_pattern,这种格式就是将 core 文件统一生成到/corefile

 

目录下,产生的文件名为 core-命令名-pid-信号。以下是常用的参数列表:

 

%e - insert coredumping executable name into filename 添加命令名

 

%p - insert pid into filename 添加 pid

 

%u - insert current uid into filename 添加当前 uid

 

%s - insert signal that caused the coredump into the filename 添加导致产生 core 的信号

 

2.3 gdb 调试 core 文件


 

利用 GDB 调试 core 文件的固定的格式是 gdb 可执行文件(带路径) core 文件(带路径)。启动之后就会停留在程序奔溃的代码位置,可以看到是收到哪个信号导致了 core,输入 bt 命令就可以输出当前堆栈信息。


 

3 、内存泄露

 

C/C++语言的一大特色就是允许程序员在程序运行过程中动态地分配内存,New/Malloc 等分配操作将内存块取到工作空间,Delete/Free 等释放操作将内存块送回自由空间。这种机制给予了程序员使用内存的自由,但也增加了程序开发的危险性。内存泄露就是因为在程序运行中,人为动态分配的内存在使用完毕后没有被释放导致的,另一种常见情况就是那些丢失了访问路径的内存块,无法被使用也无法被释放,程序运行时间越长,占用内存越多,没有足够的内存可以分配,就会报内存泄露(memory leak),整个系统奔溃。内存泄露主要指的是堆内存的泄露,因为栈内存是程序自动分配自动回收的。

 

内存泄露的常见情况:

 

1)、Malloc/New 分配的内存在程序的全部执行路径中就没有相应的语句释放;用 new[]分配的数组只是用 delete 而不是使用 delete[],也会导致数组中其余元素占用的内存空间无法被释放。

 

 

 

 

 

 

 

2)、指针被重新赋值:

 

 

 

 

 

执行了指针重新赋值的语句后,oldArea 以前所执行的内存位置就变成了孤立的内存,它无法被释放,因为没有指向该位置的指针了,访问路径丢失了,这就导致了 10 个字节的内存泄露。如果在重新赋值之前,先 free 掉 oldArea 指向的 10 个字节理论上可以的不会内存泄露,但然后的赋值语句就是对野指针赋值。

 

3)、首先释放父块:

 

这种情况主要是针对包含指向动态分配的内存的指针型成员结构体变量,如果直接从结构体的第一个字节开始释放,则其中指针型成员所指向的内存位置就丢失了访问路径了,这就导致了内存泄露。所以对于结构体变量应首先遍历其中的子内存位置,并从这里开始释放,然后再遍历会父节点释放。

 

 

 

 

 

 

 

 

 

4)、动态内存的传递-返回值得不正确处理:

 

有时,某些函数会返回在本函数中动态分配的内存,跟踪该内存位置并正确地处理它就成了 calling 函

 

数的职责了。加入调用函数未接收该内存位置的返回地址,或者是接收之后没有释放也没有向其他函数传递,也会导致这段内存块的泄露。以下代码就是丢失了对 20 个字节的访问路径。


 

 

 

 

 

 

 

 

 

从用户使用程序的角度看,根本感觉不到内存泄露的存在,内存泄露本身不会产生什么危害,真正有


 

危害的内存泄露的堆积,如果堆积过多导致消耗系统所有的内存。程序运行速度会渐渐变慢,出现 CPU 资

 

源耗尽,进程 ID 耗尽等情况,最终系统奔溃。

 

检测内存泄露的关键是要能截获住对分配内存和释放内存的函数的调用。截获住这两个函数,就能够跟踪每一块内存的生命周期。比如,每当成功分配一块内存后,就把它的指针加入一个全局里 list 中;每当释放一块内存,再把它的指针从 list 中删除。这样,在程序运行过程中,list 中剩余的指针就是指向那些没有被释放的内存。当然程序结束运行,也就不存在内存泄露的问题了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值