浅谈Core文件分析

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可简写为plist可简写为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);

dateAndTimeElementstartdateandtime都是长度为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;

这就导致pTCAPInfopMsg->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)

在的33core 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    

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 Linux 中,当一个进程遇到致命错误时,它会生成一个 core 文件,也称为核心转储文件。这个文件包含了进程的内存映像、寄存器状态等信息,可以用于分析进程崩溃的原因。 下面是一些常见的分析 core 文件的方法: 1. gdb 调试器 gdb 是一个强大的命令行调试器,可以用来分析 core 文件。简单的做法是通过命令行运行 `gdb <可执行文件> <core 文件>`,然后使用 gdb 的命令来查看内存、寄存器、堆栈等信息。 2. objdump 工具 objdump 工具可以用来分析 ELF 格式的可执行文件core 文件。使用 `objdump -f <core 文件>` 命令可以查看 core 文件的基本信息,使用 `objdump -s <core 文件>` 命令可以查看 core 文件的内存映像。 3. readelf 工具 readelf 工具也可以用来分析 ELF 格式的可执行文件core 文件。使用 `readelf -a <core 文件>` 命令可以查看 core 文件的基本信息,使用 `readelf -x <section> <core 文件>` 命令可以查看特定节的内容。 4. addr2line 工具 addr2line 工具可以将内存地址转换为源代码行号。使用 `addr2line -e <可执行文件> <内存地址>` 命令可以查看特定内存地址对应的源代码行号。 以上是一些常见的分析 core 文件的方法,但是对于复杂的程序崩溃,仍然需要耐心分析。同时,还可以使用其他工具和技术,如 Valgrind、strace、perf 等,来进一步分析进程崩溃的原因。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值