Core文件的由来
由于计算机程序问题的发生是随机的,为了给定位问题提供一个接口,一些人发明了在发生问题后将内存中的进程相关信息格式化地保存在一个文件中的手段,以便于在发生问题而中断了进程的执行后,通过内存信息转储文件得到问题发生时的蛛丝马迹,获得进一步的定位信息。这个进程内存信息转储文件就被命名为core。简单来说,core dump说的是操作系统执的一个动作,当某个进程因为一些原(常见的是内存越界读写,访问空指针、野指针)被意外终止(crash)的时候,操作系统会将这个进程内存信息转储(dump)磁盘上,产生的文件就是core文件,一般以core.xxx形式命名。
常见Core Dump的Linux的标准信号
SIGSEGV:最常见的一种core dump信号量,其值为11。这个信号通常在进程访问了无效的地址,如第0页或代码页(不可读/不可写)等中的数据时发生。一般的,当进程被这个信号core dump时,可以考虑是否使用了空指针访问数据,或者使用了没有初始化的随机指针访问数据。另外调用栈溢出也会导致操作系统向进程发送这个信号。
SIGBUS:其值为10。当进程被这个信号core dump时,考虑是否存在不正确的强制类型转换导致应用程序启动了一次不对齐的总线访问。
SIGABRT/SIGIOT:这是一个软件触发的信号,其值为6。在C语言中,这个信号通常由abort()、assert()和raise()等函数引起,而在C++中,除了这些函数外,更多的时候是因为堆操作失败或没有被捕捉的throw语句抛出的异常引起的,需要重点考虑是否是new调用失败(内存不够分配而使得malloc()返回空指针)引起的。
SIGILL:其值为4。这个信号一般是因为处理器在指令流中发现了一条当前不被支持的指令引起的。首先需要考虑是否在不合适的平台上执行一个应用程序。
其次,如果执行的是在当前主机环境下编译的程序,那么需要向编译器或运行库提供商咨询,确定其编译器或运行库的当前版本是否存在问题。
最后,确认你在程序中嵌入的汇编指令或指令流中没有使用特权指令
SIGFPE:
其值为8。收到这个信号通常意味着被0除。但其它的情况也不能被忽略,如浮点溢出,包括上溢出和下溢出。这个问题需要考察主机系统中浮点单元表示数的范围。
SIGTRAP:其值是5。通常是调试程序动态插入到进程中的陷阱指令引起的。它一般会被调试器正确地解释。但程序员也可能为了支持排错而在程序中插入陷阱指令,而在排错结束后忘了删除那条陷阱指令引起。
SIGQUIT:其值为3,一般由于从关联终端上按下Ctrl+|引起的。
SIGSYS:
其值为12,通常是因为在调用系统功能时传递了无效的参数值引起的。发生这种情况时,需要查手册获得相关系统调用的参数值的有效范围。
GDB的使用
gdb命令的格式为:
gdb [options] [executable-file [core-file or process-id]]
常用的Options:
-cd=DIR 改变当前工作目录为指定目录
-dbx dbx的兼容模式.
-xdb xdb的兼容模式
-directory=DIR 指定源代码文件路径
-objectdir=DIR 指定目标文件路径
-exec=EXECFILE 指定可执行文件名
-tui 使用图形界面,窗口分上下两部分,上面的源代码,下面是执行部分
-version 打印一下gdb的软件版本号
-command=FILE 根据文件的命令内容执行gdb命令
使用gdb –help可以得到所有options的信息。
进入gdb环境后,一般命令都还有简写,例如print可简写为p,list可简写为l。以下是gdb调试环境中常用的命令:
1) set 修改变量的值,例如:
(gdb) set g_system=2
2) 用list命令列出所要跟踪的程序的代码, list 文件名:行号,例如:
(gdb) l test.c:200
3) print 打印表达式或者变量的值, print 变量/表达式
(gdb) p g_system
$1 = 9
4) bt 打印当前栈调用情况,同where,例如
(gdb) bt
#0 0xc020edf8 in _select_sys+0x10 () from /usr/lib/libc.2
#1 0xc021a328 in select+0xe8 () from /usr/lib/libc.2
(gdb) where
#0 0xc020edf8 in _select_sys+0x10 () from /usr/lib/libc.2
#1 0xc021a328 in select+0xe8 () from /usr/lib/libc.2
5)f (frame缩写) 选择跳某层堆栈:
(gdb) f 2
#2 0x322ed8 in TSCFFEAM::active (this=0x0, blockFlag=Wrong) at c_feam.C:816
6) up 回到上一层调用栈。例如接上例,返回上一层:
(gdb) up
#3 0x13a59c in TMEControl::mainLoop
7) down 同up相反,进入下一层调用栈。
(gdb) down
#2 0x322ed8 in TSCFFEAM::active (this=0x0, blockFlag=Wrong)
8) 用break命令设置断点,用法如下:
break 行号,例如:
break 100
9) 用run命令运行,用法如下:
run 可执行文件的运行所需的参数 (或者用set args 参数设置运行参数)
10) 如果是跟踪一个进程,设置好断点后,用continue命令,使程序继续运行。
11) 执行下一步用next命令
12) 单步跟进函数内部,用step命令
13) help 获得帮助,想要查某个命令用法,可以使用help 命令 获得。
14) quit 退出gdb
15)分析core文件,bt(where)、up、down、print命令是最常用的,core的信息基本都可以通过以上命令获得。
(disassemble)可以查看当前程序执行时的机器码,这个命令把当前当前内存中的指令dump出来。如下示例表示查看函数main的汇编代码。
(gdb) disassemble mian
Dump of assembler code for function main:
0x0804838c <main+0>: push %ebp
0x0804838d <main+1>: mov %esp,%ebp
0x0804838f <main+3>: sub $0x18,%esp
0x08048392 <main+6>: and $0xfffffff0,%esp
16)ptype 可以列出结构定义:
(gdb) ptype CTest
type = class CTest {
private:
char m_strTemp[40];
char *m_pszText; public:
CTest(void);
~CTest(void);
int SetText(char *);
void Print(void);
}
17)set print pretty on执行后print列出的结构会好看很多
18)Info scope 函数名可显示各个局部变量的大小及偏移值
(gdb) info scope CTest::SetText
Scope for CTest::SetText:
Symbol this is an argument at stack/frame offset -36, length 4.
Symbol pszTemp is an argument at stack/frame offset -40, length 4.
常见Core Dump原因
空指针使用
这是最常出现的core dump情况,在没有判断其是否为NULL的情况下就是用,就会出现core dump。
如使用new申请一块内存,没有判断是否申请成功就使用。一般情况下,内存申请失败是很少见的,如果真的是内存不足了,什么异常都有可能发生,此时core dump也是没办法的。但是很多情况下并不是内存不足,而是我们代码错误,此时core dump就应该避免。
例如,NEW(char[size]),因为某些异常分支,size被改成-1了,后又被转换成无符号数,变成很大的一个值(4G),此时申请内存可能失败,但不一定真的是内存不足,不判断结果就可能core dump。
下面是另一个例子:
sprintf(filePach, "%s/temp ", getenv("TELLIN_DIR"));
当运行环境中没有配置TELLIN_DIR这个环境变量时,getenv会返回一个NULL,而代码没有判断就使用了,自然就core dump了。
Printf参数错误
这也是一个指针错误的问题,但是因为形式较特殊,单独列出以引起注意。请看下面的代码:
SCPPrintf(SCF_ERROR, -1,
"Error in %s, %d, fp == NULL\n", __LINE__, __FILE__);
__LINE__和__FILE__分别定义为整形和字符串,因为疏忽,上面的代码把__LINE__和__FILE__两个变量的位置写反了,这样其实编译器就做了类型转换,当把__LINE__转换为%s时,其实就是把__LINE__的值转换为一个字符串指针用,如果__LINE__的值正好是一个非法地址(例如是一个很小值,指向操作系统保留的地址),则是指针非法使用,发生core dump。
数组越界
例如以下代码:
char startdateandtime[14];
char dateAndTimeElement[14];
…
startdateandtime被赋值为 20040728144900;
…
strcpy(dateAndTimeElement, startdateandtime);
…
dateAndTimeElement和startdateandtime都是长度为14字节的字符串数组,startdateandtime赋值为20040728144900之后,数组startdateandtime被完全使用,此时再进行strcpy操作,
而strcpy函数的定义如下:
strcpy() copies string src to dst including the terminating null character,
stopping after the null character has been copied.
可知strcpy是拷贝到null字符为止,因此就有可能从startdateandtime读取超过14字节的数据,越界写到dateAndTimeElement中,导致core dump。
地址不对齐
在有些机型上,例如SUN的机器,要求整形的地址必须对齐,例如必须是4的整数倍,如果不对齐会因为总线错误而core dump。例如有如下代码:
//头文件定义
char bmpheader[54]=
{
(char)0x42, (char)0x4d, (char)0xc0, (char)0x32, (char)0x00, (char)0x00, (char)0x00, (char)0x00,
(char)0x00, (char)0x00, (char)0x36, (char)0x00, (char)0x00, (char)0x00, (char)0x28, (char)0x00,
(char)0x00, (char)0x00, (char)0x40, (char)0x00, (char)0x00, (char)0x00, (char)0x40, (char)0x00,
(char)0x00, (char)0x00, (char)0x01, (char)0x00, (char)0x20, (char)0x00, (char)0x00, (char)0x00,
(char)0x00, (char)0x00, (char)0x00, (char)0x00, (char)0x00, (char)0x00, (char)0x12, (char)0x0B,
(char)0x00, (char)0x00, (char)0x12, (char)0x0B, (char)0x00, (char)0x00, (char)0x00, (char)0x00,
(char)0x00, (char)0x00, (char)0x00, (char)0x00, (char)0x00, (char)0x00
};
…
//获得图片的尺寸
*(int *)(&bmpheader[18]) = htonl (width); *(int *)(&bmpheader[22]) = htonl (height);
…
以上程序将bmpheader[18]和bmpheader[22]当作整形用,因为其地址不是4的整数倍,在SUN环境下,以上代码会core dump。如果使用memcpy对字节操作的方式,就可以避免core dump。
修改为如下,就不core dump:
int temp;
temp = htonl (width);
memcpy(&bmpheader[18],&temp,4);
temp = htonl (height);
memcpy(&bmpheader[22],&temp,4);
野指针
内存泄漏一般就是堆中申请的空间没有释放,内存泄漏如果一直持续下去,结果会导致core dump。因为通常操作系统分配内存时,将堆空间和栈空间分别从内存空间两端分配,如果内存不停的泄漏,堆使用的空间不停增长,会覆盖了栈的空间,最后指针异常引出core dump。这类core dump问题要长时间运行才能暴露,较难发现。
内存空间重复删除
两个指针指向同一内存空间,如果分别使用这两个指针对对同一内存空间进行删除操作,在某些机型(如IBM)上会导致core dump。例如有下代码:
//类定义
class TMsg
{public:
…
PTTCAPInfo pTCAPInfo; //包含一些原TMsg中如tc_flag等信息}
}
TMsg::~TMsg()
{ …
DELETE(pTCAPInfo);
DELETE(pMsgPara);
}
//执行如下代码
pMsg->pTCAPInfo = pTCAPInfo;
if(…)
{ DELETE (pTCAPInfo); //第一次删除
DELETE (pMsg); //第二次删除
return FALSE;
}
以上代码中,因为有pMsg->pTCAPInfo = pTCAPInfo;
这就导致pTCAPInfo和pMsg->pTCAPInfo = pTCAPInfo指向同一个地址。蓝色代码删除了pTCAPInfo,这样其所指向的内存空间被删除了。
在TMsg的析构函数中会删除pMsg->pTCAPInfo,而pMsg->TCAPInfo不为NULL的,导致同一内存空间被释放两次。这种情况在IBM的机器上会导致core dump。
vptr指针被破坏
vptr是一个类中指向虚函数表(virtual table)的指针,是C++中实现多态的一个方法。如果一个类定义了虚函数,则编译器会在这个类中增加一个指针,指向虚函数表。vptr在对象构造时被初始化,在虚函数调用是使用它来在虚函数表中找到要调用的确切函数。对程序员,vptr可以说是透明的,但是如果我们真的忽略了它的存在,也会犯下严重的错误。为了方便说明,请看如下简化代码:
#include <stdio.h>
#include <memory.h>
class A
{ private:
int i;
public:
virtual intgeti();
A()
{i = 1;}
};
int A::geti()
{return i;}
main()
{A tempa;
memset(&tempa, 0, sizeof(A));
A * pa;
pa = &tempa;
printf("tempa.geti: i = %d.\n",tempa.geti());
printf("pa->geti: i = %d.\n",pa->geti());
printf("Over.\n");
}
以上代码看似没有什么指针异常操作,但是执行结果:
$a.out
tempa.geti: i = 0.
Bus error (core dumped)
用gdb分析:
Core was generated by `a.out'.
Program terminated with signal 10, Bus error.
…
(gdb) where
#0 0x17b4 in ()
#1 0x27a0 in main () at mset.C:33
(gdb)
在的33行core dump。为什么使用tempa.geti()可以,使用pa->geti()就不行?这是因为使用tempa.geti(),已经明确的指定了对象,运行时可以得到确切的要调用函数,即(& tempa)->geti()。而使用pa->geti(),因为C++中pa指向的对象可以是类A,也可以是类A的子类,类A中定义的geti()是虚函数,如果是A的子类,运行时要转化为子类的geti(),因此在运行中要转换为确切的函数。这个转换是通过vptr实现的,即pa->geti()要转换为pa->vptr-> geti()。
但是因为之前对象tempa已经被野蛮地初始化了:
memset(&tempa, 0, sizeof(A)); (再罗嗦一句,这里的sizeof(A)是8而不是4,因为有vptr)
这样vptr也被memset了,变成了一个NULL指针。所以pa->vptr-> geti()变成了pa->NULL-> geti(),当然就会出现core dump。