应用程序调试总结

总结一下对应用程序出现segment fault时的基础和调试方法,知识来自debug hacks一书

环境,x86 32位linux

一.基础

1.熟悉参数的传递方式。

  在进入被调用函数之前,程序会按照参数,返回地址,fp指针(帧指针),被调用函数的局部变量,的次序压栈。

  源码:

  #include <stdio.h>

  int fun(int a,char c)

  {

    printf("%d\n%c\n",a,c);

    return a;

  }

  int main()

  {

    fun(1,‘a‘);

    return 0;

  }

  使用gdb调试该程序:

  在函数名前加上*号,程序遇到断点时,会卡在函数汇编语言层次的开头。如果不加*,会停在函数的第一句话。

  函数在跳转之前会把需要传递的变量和返回地址压入栈,而剩余的变量由被调用函数压栈。所以此时,sp指针指向的是返回地址,另一个我们知道栈是向下增长的,所以sp+4就是压入的第2个参数(a),sp+8是压入的第1个参数(c),如下图。

  

2.core文件的生成

  一般linux系统默认是不生成core文件的,可以通过ulimit -c查看。如果显示0,则调用ulimit -c unlimited 设置为没有上限,当然也可以设置一个具体的值,单位为blocks。

  注意:必须确保有权限在该目录下生成core文件,因为我们很多工作的时候是将本地文件挂载到linux服务器上或者虚拟机上,如果不是有权限的用户登录的话,是不会在该目录下生成core文件的,或者生成的core文件大小为0。

3.gdb的常用命令

  可以查看我的上一篇总结。

二.调试实践

1.栈溢出

  源码:

#include <stdio.h>
int fun()
{
int a = 10;
fun();
printf("%d\n",a);
return 1;
}

int main(int argc,char **argv)
{
fun();
return 0;
}

  发生段错误,利用生成的core文件,查看sp的指针大小

可以看到sp=0xbf45a000;

再查看各个段的大小,使用i files命令,虽然看不出哪个段是stack,ps:不知道为何无法上传图片。那我就打字了。

  如下:

Local core dump file:
`/root/core‘, file type elf32-i386.
0x0084e000 - 0x0084e000 is load1
0x009a1000 - 0x009a1000 is load2
0x009a2000 - 0x009a4000 is load3
0x009a4000 - 0x009a5000 is load4
0x009a5000 - 0x009a8000 is load5
0x00d68000 - 0x00d69000 is load6
0x00d87000 - 0x00d87000 is load7
0x00da2000 - 0x00da3000 is load8
0x00da3000 - 0x00da4000 is load9
0x08048000 - 0x08048000 is load10
0x08049000 - 0x0804a000 is load11
0x0804a000 - 0x0804b000 is load12
0xb775e000 - 0xb775f000 is load13
0xb776d000 - 0xb776f000 is load14
0xbf45a000 - 0xbfe5a000 is load15

可以看出0xbf45a000 属于段15,明显已经位于了这个段的末尾,因为sp自减时并不检查sp是否超过了范围,当访问时才会知道这个地址是否合法,所以可以确定是栈溢出。

  很多大型的程序,当程序抛出段错误的信号时,会有处理程序接收这个信号,但是这个时候栈上已经没有空间了,是不可能让这个处理函数正常结束的,所以需要提前为这个函数申请好栈空间,确保能把当时的情形保留下来,可以使用sigaltstack函数在堆上申请备用栈。具体的用法请man一下

2.返回地址被修改

  返回地址被修改的情况很多,根据之前的栈空间压栈顺序,如果被调用函数的局部数组越界就可以将返回地址覆盖,导致段错误的发生,这是一种。重点是我们要怎么知道发生了返回地址被修改,而且此时的局部变量也可能是不正确的,很难调试。一般来讲如果发生返回地址被修改,bt中的信息会是这样的。

  我们知道正常情况下,应该是显示函数名称而不是问号,(如果修改之后的地址还是指向某个函数的话,那就只能一步步查看下去,是否存在这么一个调用顺序)。此时是可以确定返回地址被修改了的。

  具体将一个如果是数组越界导致的返回地址被修改的情形。

  源码:

#include <stdio.h>
#include <string.h>
char names[] = "book cat dog building vagetable curry";
void fun()
{
char buf[5];
strcpy(buf,names);
}

int main(int argc,char **argv)
{
fun();
return 0;
}

调试过程:首先查看当前运行在哪句话上。

可以看出当前运行到了ret这句话,也就是返回,那么看下sp中的值是多少。

这步有些多余,就是堆栈信息中的下一帧地址。

因堆栈信息目前怀疑的是返回地址被修改,所以查看esp中的内容,先用字符串的形式查看里面的内容

比较明显可以看出现在堆栈中的信息就是book cat dog building vagetable curry 显然是一个字符串,搜索这个字符串被引用的地方,可以发现就在源代码的第8行,复制字符串时超出了数组的长度。

3.利用监视点检测非法内存访问

  这个我在linux系统中无法复现出,因为越界之后的地址值是非法的,模拟出这个情况比较困难。所以这边就语言描述下。

  源程序:

  

int data[2]= {1,2};

int calc(void)
{
return -7;
}

int main()
{
int index = calc();
data[index] = 0x0a;
data[index+1] = 0x08;

printf("ssssss\n");
return 0;
}

错误发生在printf那句话中。通过查看堆栈找到main函数中的返回地址,而在这个返回地址之前的语句可能就导致了这个段错误,然后查看到之前的语句中有一句call跟踪该语句,最终会跳转到一个指针中的地址,而实际上这个指针中的地址就是0x08,也就是被程序中的语句所修改了,那么重点就在怎么确定是这句话导致的错误。

既然知道了这个指针所指向的地址,那么就可以在这个地址值出设置监视点,当这个地址处的值被修改时gdb就会停住,运行时会发现就是printf的前一句话,也就是找到了原因所在。

4.双重释放指针导致的bug

  这种错误我觉得可以设置监视点或者断点的方式,利用gdb的脚本,打印出free时的堆栈信息,然后查看哪个地址有被多重释放。

  另一种方法是利用env MALLOC_CHECK_=1 ./a.out 来运行程序,但有的情况下不指定环境变量,在双重释放指针时也会打印出堆栈信息,反而加了环境变量没有打印出堆栈信息。但个人觉得这只是说明原因是双重释放,还是坚持前一种方法,找到释放的两个位置,只保留一个释放点。

5.死锁

  当造成死锁时,先使用ps命令查看下线程状态,如果状态是S的话,就有可能说明是死锁了。

  这个时候再使用gdb attch上去,查看各个线程的堆栈,看卡在哪一个线程中。

  然后再利用gdb设置断点和脚本,打印出同一把锁被操作的过程。下面看个例子

   源码:

  

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int cnt = 0;
void cnt_reset(void)
{
pthread_mutex_lock(&mutex);
cnt = 0;
pthread_mutex_unlock(&mutex);
}

void *th(void *p)
{
while(1){
pthread_mutex_lock(&mutex);
if(cnt > 2)
cnt_reset();
else
cnt++;
pthread_mutex_unlock(&mutex);

printf("%d\n",cnt);
sleep(1);
}
}

int main()
{
pthread_t id;
pthread_create(&id,0,th,0);
pthread_join(id,0);

return 0;
}

  运行结果:

[[email protected]: deadlock]./a.out
1
2
3

发现程序不跑了,根据程序接下来应该打印出0。

[[email protected]: deadlock]ps -x | grep a.out
Warning: bad ps syntax, perhaps a bogus ‘-‘? See http://procps.sf.net/faq.html
26418 pts/9 Sl+ 0:00 ./a.out

可以看出程序现在处于睡眠状态,那么使用gdb attch上去,查看是哪一个线程在睡眠或者说导致了死锁。

可以看出主线程是处在睡眠中,在等待子线程的结束,而子线程睡眠在了等待锁的释放上,那么现在问题就在于为什么锁是在哪一步或者哪个线程先拿到了,而导致当前线程拿不到锁。

使用gdb重新调试程序,并且在加锁和释放锁的位置设置断点,打印出堆栈,可以发现前面一直都是加锁解锁对应的,而在最后一对打印中两个操作都是加锁

根据这个堆栈信息可以知道th函数先加了一次锁,然后th函数本身调用了cnt_reset函数,该函数再一次加锁导致了死锁。

所以现在就找到原因了。

这是一个较为简洁的例子,我在工作中遇到过一次较为麻烦的问题,如下:多线程之间对于一个数据结构的访问,需要首先拿到保护该结构的锁,问题出在了当某一个线程拿到锁之后还没有释放锁,该线程就被杀死了,而此时其他线程就再也无法获取到该锁,导致所有线程堵死。同样通过上述方式可以找到原因。

6.死循环

  这个情况我自己模仿书上的例子,创建了一个类似的例子

  源码:

  

#include <stdio.h>

int fun(char *p,int len)
{
while(len > 0){
int version = *(int *)p;
int msgtype = *(int *)(p+sizeof(int));
int length = *(int *)(p+sizeof(int)+sizeof(int));
/*do something*/
len = len - length;
p = p + length;
}
}

int main()
{
char p[100];
int len = 0;
int version = 1;
int type = 10;
int length = 0;
memset(p,0,100);
memcpy(p,&version ,4);
memcpy(&p[4],&type,4);
memcpy(&p[8],&length,4);
length = 10;
memcpy(&p[12],&version ,4);
memcpy(&p[16],&type,4);
memcpy(&p[20],&length,4);

fun(p,30);
return 0;
}

fun函数是用来解析消息的一个函数。有些类似于tcp,是基于流的方式来解析数据包。

但是现在在运行时发生了死循环。即执行程序之后就不会退出。

gdb attach上该进程之后,发现是在fun函数里面,那么查看源码知道fun就只有一个循环。那么现在使用debug版本的可执行程序,单步调试该程序。

可以发现,消息体的长度一直为0,这个问题导致了,一直在解析同一个消息。那么问题就确定了,发送的消息长度有问题,所以在函数中解析到长度字段时,应该比较长度字段至少大于多少。

三。总结

  首先要熟练运用gdb中的各种工具,包括查看寄存器,堆栈,断点,监视点和脚本等。

  一般来讲调试过程是,收集信息,包括现象和dump信息。分析dump信息,复现bug,修复bug。

  栈溢出:结合sp和程序map信息。

  返回地址被修改:堆栈异常基本属于返回地址被修改,将sp中的内容打印出来,以各种方式打印,字符型或者十六进制等等。可能会发现比较眼熟的结果打印,比如明显是一个字符串,这时候对错误的定位就很容易了。

  非法内存访问:某个跳转地址是存放在一个指针中的,这个指针中的值被修改了,也就导致了后续的跳转出现了非法。这个时候可以在这个指针上设置监视点,打印访问该监视点时的堆栈。

  双重释放:还是利用监视点或者断点,确定哪两次释放。

  死锁:同上,确定哪两步拿锁冲突。

  死循环:确定当前死循环位置,最好使用debug版本单步调试。

  

时间: 2024-10-07 16:51:57

应用程序调试总结的相关文章

微信小程序调试之【不在以下合法域名列表中】

在微信小程序中进行网络通信,只能和指定的域名进行通信.目前,微信小程序提供如下四种类型的网络请求. 普通HTTPS请求(wx.request) 上传文件(wx.uploadFile) 下载文件(wx.downloadFile) WebSocket通信(wx.connectSocket) 目前,无论上述哪一种请求方式,都有次数限制.本文记录的是,我在使用豆瓣论坛API请求时,遇到的错误截图及解决办法.简记如下: 一.问题截图 小程序调试中,遇到如下错误提示: "xxx不在以下合法域名列表中 ,请参

.NET应用程序调试—原理、工具、方法

阅读目录: 1.背景介绍 2.基本原理(Windows调试工具箱..NET调试扩展SOS.DLL.SOSEX.DLL) 2.1.Windows调试工具箱 2.2..NET调试扩展包,SOS.DLL.SOSEX.DLL 2.3.调试系统的基本流程及架构(.NETDAC概念.mscordacwks.dll) 2.4.VisualStudio中集成扩展调试(更加细粒度的调试程序) 3.调试程序类型(客户端程序.服务端程序) 4.调试方式及场景 4.1.本机调试(Attach Process,调试器启动

Chisel辅助iOS 应用程序调试,MusicApp模仿酷狗4.0 UI框架

本文转载至 http://www.cocoachina.com/ios/20140825/9446.html Chisel Chisel集合了大量的LLDB 命令来辅助iOS 应用程序调试,并支持添加本地和自定义的命令.以下是其中所包含的一些命令,并对其适用于iOS还是OS X进行了区分: M13ProgressSuite 该项目包含了多种不同的风格的进程指示图,比如普通圆环形.分段圆形加载.圆形饼图加载以及条形加载等等,比如其中UINavigationBar的进程动画非常像苹果的Messag

Linux下C/C++程序调试基础(GCC,G++,GDB,CGDB,DDD)

在写程序的时候,经常会遇到一些问题,比如某些变量计算结果不是我们预期的那样,这时我们需要对程序进行调试.本文主要介绍调试C/C++在Linux操作系统下主要的调试工具. 在Linux下写程序,C/C++主要的编译器有GCC/G++,ICC等,像我等穷码农,最喜欢GCC了,很大原因是他免费!所以,我们以GCC/G++为例介绍主要的调试工具. 分以下几个内容介绍: 1.调试之前的工作 2.选择调试工具 3.调试步骤 点我,请帮我投一票! 调试之前的工作 编译器在编译阶段需要产生可供调试的代码,才能被

程序员突围-程序调试分析(序)

-从实践到思考,痛苦的煎熬 其实算算,工作一年了,从大学毕业至今,接触编程已经五年了,但是真正的编程感觉还没有开始,从大一开始接触C语言,陆续接触c++,java,C#等等,现在感悟到了一点,编程语言学那么多有什么用呢?其实把一门编程语言学精了,学透了,其他的是触类旁通的(底层的C语言和C++可能有点例外),下面我会说一下我的经历,我感觉可能是大多数学习编程人的必经的阶段,让大家对编程的抵触少一些,然后想想一个我这样的白痴都能慢慢的开始程序调试,程序分析,你们绝对比我强的,下篇文章才会进入我的程

程序员突围-程序调试分析(一) 我从菜鸟进化的感悟

程序员突围-程序调试分析(一) 我从菜鸟进化的感悟 在说程序调试分析之前,我们还是了解一些基本的概念性的东西(在下现在从事java,因而都已java为例) 1. bug的分类 根据程序的阶段和MSDN和看过的一些书籍的分析,bug分为编译错误,运行时错误和逻辑的错误 (1)  编译错误 一般初学者犯错比较多的地方,编译错误,说白了就是程序在从java编译成.class文件时出现了问题,这个问题的现象比较明显,比如说语句写的有问题,那么对于这类问题的解决方法是什么呢,翻翻书,翻翻API(翻阅API

php程序调试: xdebug的配置

如何在phpeclipse中像调试Java程序一样调试php呢? XDebug的版本很多,打开http://xdebug.org/index.php,把网站仔细看一下,你会发现有句"If you don't know which one you need, please refer to the custom installation instructions.",非常好,打开这个页面,按照要求将phpinfo()的内容copy & paste到form里面,然后按一下&quo

杂【第一天】包括eclipse常见操作,程序调试模式

观看传智播客视频笔记,感谢 eclipse的常见操作: 1.当即热版本低于编译器版本是,会出现bad Vresion number in class file的错误: 2.快捷键: alt+/:模板键 ctrl+1:快速修复 ctrl+shift+o:导包 设置代码阿保存的时候自动格式化:windows->首选项->Java->Editor->save Actions 代码移动:alt+上下键 重置视图:window->reset perspective... 3.典型的字节

No.14 程序调试

程序排错与程序开发是一个不可分割的整体,广义的程序开发包括程序调试,要正视程序排错 1. 程序的可调试性 添加注释 使用log log4j:辅助程序调试:记录程序的运行日志 logger及其继承关系 只有当方法的优先级高于/等于logger的优先级时才会输出 如果程序没有显示地指定某个logger的level,将继承离他最近的祖先的level 将logger的名字与它所在的类的全类名相同 logger继承体系与类的继承体系相同 当控制某个包内的调试信息时,设置该变  以该 包名 为 名字的 lo

程序调试时出现的错误

1.         程序调试时出现的错误: a)         逻辑错误:服务器端的代码. i.              因为服务器端的代码一般是静态的强类型语言,编译器会矫正一些拼写错误. ii.              服务器端的代码出现错误之后,一般可以通过编译器调试代码解决. iii.              服务器端编程要学会使用快捷键,增加编程效率,减少出错概率. b)         拼写细节错误:xml配置文件,html文档流,CSS渲染文件,sql脚本,代码中的字符串