linux下core dump配置,以及google breakpad详解

 

前言:google breakpad

          google就是牛逼啊,google专门除了一套工具来控制生成core文件,名称叫google-breakpad, 本人有幸研读了其源代码,发现采用信号注册的方式,捕获core信号,并通过父子进程的模式使用pipe管道,实现了子进程对父进程内存的访问,牛逼吧!!!   而且还能够打印不同内存区域的堆栈,等等,感觉比linux默认的core机制牛逼!

         3年前,由于项目的需要,本人和另外一个同事,将C++的google-breakpad改成C实现版,并应用到项目中了。

         呵呵     在此顶google!!!

 

1 Linux core dump详解

      A core dump is the recorded state of the working memory of a computer program at a specific time, generally when the program has terminated abnormally (crashed). In practice, other key pieces of program state are usually dumped at the same time, including the processor registers, which may include the program counter and stack pointer, memory management information, and other processor and operating system flags and information. The name comes from the once-standard memory technology core memory. Core dumps are often used to diagnose or debug errors in computer programs.

      On many operating systems, a fatal error in a program automatically triggers a core dump, and by extension the phrase "to dump core" has come to mean, in many cases, any fatal error, regardless of whether a record of the program memory is created.

1.1 配置


在linux平台下,设置core dump文件生成的方法:

1)关闭: 在终端中输入ulimit -c 如果结果为0,说明当程序崩溃时,系统并不能生成core dump。

2) 开启:使用ulimit -c unlimited命令,开启core dump功能,并且不限制生成core dump文件的大小。如果需要限制,加数字限制即可。ulimit -c 1024

3) 修改core文件名生成规则:默认情况下,core dump生成的文件名为core,而且就在程序当前目录下。新的core会覆盖已存在的core。通过修改/proc/sys/kernel/core_uses_pid文件,可以将进程的pid作为作为扩展名,生成的core文件格式为core.xxx,其中xxx即为pid

4) 修改生成路径和文件名:通过修改/proc/sys/kernel/core_pattern可以控制core文件保存位置和文件格式。

centos下的core_pattern文件默认内容是:|/usr/libexec/abrt-hook-ccpp %s %c %p %u %g %t e   ,   我们可以进行下面的替换

例如:将所有的core文件生成到/corefile目录下,文件名的格式为core-命令名-pid-时间戳. echo "/root/corefile/core-%e-%p-%t" > /proc/sys/kernel/core_pattern

 

1.2  如何产生 core 文件


要素一,必须有信号产生:

       从上面的信号定义和说明可以看出,进程中止前肯定会产生信号,然后内核根据信号的类型来决定是否要产生 core 文件

要素二,编译器支持:

       要产生 core 文件,编译器必须支持把当前进程的镜像以某种格式 dump 到一个文件中,常见的比如 gcc/g++ 的 -g 选项

要素三,环境参数支持:

      通过 ulimit –a 查看 core file size 是否为 0 ,如果为 0 则不能产生 core 文件。

      通过 ulimit –c unlimited 可以系统能支持的产生足够大的 core 文件,也可以设置为具体值。

特别说明:

      core 文件的产生不是 POSIX.1 所属部分,而是很多 UNIX/Linux 版本的实现特征。

 

1.3  Unix/Linux 对信号的处理方式
 

信号类别:

$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL
 5) SIGTRAP      6) SIGABRT      7) SIGBUS       8) SIGFPE
 9) SIGKILL     10) SIGUSR1     11) SIGSEGV     12) SIGUSR2
13) SIGPIPE     14) SIGALRM     15) SIGTERM     16) SIGSTKFLT
17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU
25) SIGXFSZ     26) SIGVTALRM   27) SIGPROF     28) SIGWINCH
29) SIGIO       30) SIGPWR      31) SIGSYS      34) SIGRTMIN
35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3  38) SIGRTMIN+4
39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12
47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14
51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10
55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7  58) SIGRTMAX-6
59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

 

1.4  Unix/Linux 对信号的处理方式(无序)

Name

Description

ISO C

SUS

FreeBSD 5.2.1

Linux 2.4.22

Mac OS X 10.3

Solaris 9

Default action

SIGABRT

abnormal termination (abort )

terminate+core

SIGALRM

timer expired (alarm )

 

terminate

SIGBUS

hardware fault

 

terminate+core

SIGCANCEL

threads library internal use

 

 

 

 

 

ignore

SIGCHLD

change in status of child

 

ignore

SIGCONT

Continue stopped process

 

continue/ignore

SIGEMT

hardware fault

 

 

terminate+core

SIGFPE

arithmetic exception

terminate+core

SIGFREEZE

checkpoint freeze

 

 

 

 

 

ignore

SIGHUP

hangup

 

terminate

SIGILL

illegal instruction

terminate+core

SIGINFO

status request from keyboard

 

 

 

 

ignore

SIGINT

terminal interrupt character

terminate

SIGIO

asynchronous I/O

 

 

terminate/ignore

SIGIOT

hardware fault

 

 

terminate+core

SIGKILL

termination

 

terminate

SIGLWP

threads library internal use

 

 

 

 

 

ignore

SIGPIPE

write to pipe with no readers

 

terminate

SIGPOLL

pollable event (poll )

 

XSI

 

 

terminate

SIGPROF

profiling time alarm (setitimer )

 

XSI

terminate

SIGPWR

power fail/restart

 

 

 

 

terminate/ignore

SIGQUIT

terminal quit character

 

terminate+core

SIGSEGV

invalid memory reference

terminate+core

SIGSTKFLT

coprocessor stack fault

 

 

 

 

 

terminate

SIGSTOP

stop

 

stop process

SIGSYS

invalid system call

 

XSI

terminate+core

SIGTERM

termination

terminate

SIGTHAW

checkpoint thaw

 

 

 

 

 

ignore

SIGTRAP

hardware fault

 

XSI

terminate+core

SIGTSTP

terminal stop character

 

stop process

SIGTTIN

background read from control tty

 

stop process

SIGTTOU

background write to control tty

 

stop process

SIGURG

urgent condition (sockets)

 

ignore

SIGUSR1

user-defined signal

 

terminate

SIGUSR2

user-defined signal

 

terminate

SIGVTALRM

virtual time alarm (setitimer )

 

XSI

terminate

SIGWAITING

threads library internal use

 

 

 

 

 

ignore

SIGWINCH

terminal window size change

 

 

ignore

SIGXCPU

CPU limit exceeded (setrlimit )

 

XSI

terminate+core/ignore

SIGXFSZ

file size limit exceeded (setrlimit )

 

XSI

terminate+core/ignore

SIGXRES

resource control exceeded

 

 

 

 

 

ignore

 

1.5   core 文件的局限


core 文件提供了当前进程的所有线程的堆栈信息,但是也有他的局限性。

(1)由于 core 是对当前进程地址空间的镜像,所以 core 文件一般比较巨大,特别是针对服务器程序。这样如果服务器程序自动重启几次,可能就会导致磁盘空间占满

(2)另外,由于 core 文件巨大,不删除的话占用大量磁盘空间,下载下来又比较费时。

(3)对于缓冲区溢出导致的 coredump ,进程的调用堆栈已经被覆盖破坏了, core 文件显示的堆栈信息往往错误

(4)一些信号导致进程崩溃,但是不产生 core 文件,比如 SIGPIPE

(5)只能在进程中止时才可以产生 core, 不能实时产生。
 

1.6 堆栈打印
1.6.1. 如何打印堆栈


1 、通过backtrace 和backtrace_symbols 或backtrace_symbols_fd 实现当前线程的堆栈打印。

堆栈打印的一个简单示例:

     #include
     #include
     #include
    
     /* Obtain a backtrace and print it to stdout. */
     void print_trace (void)
     {
       void *array[10];
       size_t size;
       size_t i;
       size = backtrace (array, 10);
       printf ("Obtained %zd stack frames./n", size);
       for (i = 0; i < size; i++)
          printf ("% p  /n",   array  [i]);
     }

     /* A dummy function to make the backtrace more interesting. */
     void dummy_function (void)
     {
       print_trace ();
     }

     int main (void)
     {
       dummy_function ();
       return 0;
     }

2 、编译时必须加上 -rdynamic –ldl 选项。
3 、如果要打印崩溃线程的堆栈信息,则必须在信号处理函数中打印堆栈。注意要通过 setjmp/longjmp 或 sigsetjmp/siglongjmp 来跳转,否则会陷于死循环。因为信号是软件中断,处理完后会返回到发生异常的地方继续执行,这样又会重新产生异常。
4 、我写了一个 coredump 组件,支持打印崩溃线程的堆栈信息和寄存器变量,捕捉 SIGINT 、 SIGTERM 、 SIGFPE 、 SIGABRT SIGSEGV 等信号引起的异常崩溃,打印信息输出到一个文本文件。该组件将会放到 SGDP 中供项目组选用。具体使用方法请看 readme 和 example 。

1.6.2.  打印堆栈的优缺点

优点:

      1 、不依赖 core 文件。
      2 、可以打印当前崩溃线程的堆栈调用信息。
      3 、输出到文件占用的磁盘空间很小。

缺点:

1、  只能打印当前线程的堆栈信息。
2、  只能打印堆栈调用信息。
3、  不能打印当前线程的所有变量值。
4、  输出函数名没有 gdb 的 core 文件可读性好。
5、  对于 abort 异常定位,需要和 nm 等代码分析工具配合使用。
 

1.7.  GCC/G++ 对内存错误的处理


GCC 对内存错误的检查,在不同版本都有一定的体现。
GCC 对内存错误的检查主要依靠 3 个选项: -fstack-check -fbounds-check -fstack-protector-all
其中:

-fstack-check 的准确性较差,特别是在代码执行了 O2 以上的优化后。
-fbounds-check 可以执行数组边界错误,主要是检查对数组赋值越界。
-fstack-protector-all 可以防止缓冲区溢出攻击,也可以利用此选项来检查代码是否有缓冲区溢出的错误。

GCC 4.1 以前的版本支持 -fstack-check -fbounds-check , GCC 4.1 以后版本支持 -fstack-check -fbounds-check -fstack-protector-all 。


GCC 4.1 以前的版本对于缓冲区溢出检查没有很好的解决方案,如果调用堆栈被破坏的不多还有可能定位出错函数的位置,否则只是一个错误的堆栈信息甚至无信息。
-fstack-protector-all 选项加上后会在标准输出上打印当前崩溃线程的堆栈调用信息,但是有时也不准确,同时产生 core 文件, core 文件中的出错堆栈信息相对比较准确性。


1.8.  SGDP 的 coredump 组件

我们自己开发了一个 Linux 版打印进程崩溃堆栈信息的 coredump 组件,放在 SGDP 的工具里面。

coredump 组件支持打印崩溃线程的堆栈信息和寄存器变量,捕捉SIGINT 、SIGTERM 、SIGFPE 、SIGABRT 和SIGSEGV 等信号引起的异常崩溃,打印信息输出到一个文本文件。

使用方法:

          1 、代码中包含coredump.h 。

          2 、在main 函数开始的地方调用setup_sigcoredump() 。

 

2  google breakpad详解


2.1  项目介绍

      breakpad 是一个包含一系列库文件和工具的开源工具包,使用它可以帮助我们对程序的崩溃进行一系列的后续处理,如现场的保存(core dump),及事后分析(重建call stack)等,它提供了非常有效且易用的工具来帮助开发者处理程序的异常崩溃。该项目由Google所开发维护并开源,代码托管在Google code上。

      breakpad具有跨平台的特性,支持window,linux,mac三大平台,可以运行于一系列架构的cpu上。现在已经被广泛运用在google的一系列产品及其它公司的桌面程序上,如chrome,piscal,firefox等。 

      在进程不中止的情况下可以产生 core 文件,该 core 文件可以使用 gdb 调试,对于多线程调试有重要意义。
在产生 core 文件时会挂起所有线程,所以是线程安全的。


项目链接: https://chromium.googlesource.com/breakpad/breakpad

git clone https://chromium.googlesource.com/breakpad/breakpad

 

./configure 
make

如果报错:

src/client/linux/crash_generation/crash_generation_client.cc:40:51: fatal error: third_party/lss/linux_syscall_support.h: No such file or directory

git第三依赖:

git clone https://chromium.googlesource.com/linux-syscall-support src/third_party/lss

处理符号文件

      为了产生有用的堆栈跟踪, Breakpad需要你把二进制中的调试符号转成text-format symbol files. 首先,编译时必须用-g 包含调试符号。其次编译这个工具dump_syms ,这个在上边make的时候一起生成好了。

$ google-breakpad/src/tools/linux/dump_syms/dump_syms ./test > test.sym

      像下面这样搞,就是搞出个特定的目录来,目录名是head命令 查看 test.sym第一行得出来的,然后把test.sym搞进去,最好复制。

$ head -n1 test.sym
MODULE Linux x86_64 6EDC6ACDB282125843FD59DA9C81BD830 test
$ mkdir -p ./symbols/test/6EDC6ACDB282125843FD59DA9C81BD830
$ mv test.sym ./symbols/test/6EDC6ACDB282125843FD59DA9C81BD830


symbolstore.py 这个 Mozilla 的脚本能搞定上边几步。

Usage: symbolstore.py

例:./symbolstore.py     ./tools/linux/dump_syms/dump_syms     symbols             test  

处理minidump来生成堆栈信息

还是用工具minidump_stackwalk 来搞

google-breakpad/src/processor/minidump_stackwalk minidump.dmp ./symbols
 

 

下面主要介绍一下其在linux下结构。

breakpad主要包括三大主件:

   1) client.

   2) symbol dumper.

   3) processor.

2.2 原理简介

breakpad抓取dump的方式和一般我们抓取dump的方式不一样。在breakpad的wiki上有一幅图可以很好的概括他的原理。

linux下core dump配置,以及google breakpad详解_第1张图片

 

2.3 google_breakpad::StackwalkerX86 Class Reference

linux下core dump配置,以及google breakpad详解_第2张图片

 

 

2.4 各个模块

2.4.1  CLIENT MODULE.

  client模块作为一个静态库将会与使用者的程序编译在一块。它的主要作用是在程序崩溃后,接管程序的异常处理,具体来说,它主要做了两方面的事情。

   a)响应程序崩溃时接收到的signal,包括:SIGSEGV,SIGABRT,SIGFPE,SIGILL,SIGBUS. (另外两个SIGSTOP,SIGKILL无法处理)

   b) 获取程序崩溃那一刻的运行时信息,保存为一个mini dump格式的文件(可以想像为一个特殊格式的coredump)

  下图描述了client module中几个类的关系及工作的流程:

  linux下core dump配置,以及google breakpad详解_第3张图片

  如上,breakpad通过提供信号处理函数来响应程序的崩溃,然后在信号处理函数中,保存程序崩溃时的现场信息,在这里有几点需要说明:

  a) 程序崩溃后,整个进程空间已经处于一个不稳定的状态,在这样的不稳定状态下,再进行内存的分配,和调用动态库里的函数,都是不安全的。之所以说不稳定状态,主要是指如果当前的崩溃是由SIGSEGV引起的,那么此时程序的内存可能已经被破坏了(heap,stack,数据段),而引用动态库是需要查找一堆数据段里的表项(plt,got)的,因此很可能没法加载动态库,至于不能malloc那是基本同理的,heap可能已经被破坏了,malloc内部维护的数据未必还正常,也就无法保证还能进行malloc。

  b) 信号处理函数会clone出一个新的进程,dump core这件事情则是在这个clone出来的子进程中进行的,子进程通过ptrace来与父进程进行交互,从而读取父进程的相关信息。

  c) breakpad的exception handling有两种模式,一种是in process,一种是out of process.但在Linux平台上,暂时只有in process这种模式。in process的实现相对简单些,流程很清楚明了,但out of process相对就复杂了。

      按照开发者的设计意想,是为每一个登陆Linux的登陆用户起一个deamon程序运行于后台,当该用户的其它程序崩溃后,崩溃程序通过与这个deamon程序进行交互,从而保存core dump. 其中,交互方面将通过socket按照client/server的模式进行。但是至今为止,这种模式还未可用。

 

    在上图中,MinidumpWriter这个类是一个包装,提供一些与write dump相关的接口函数给上层的函数处理函数进行调用,真正与linux process相关的操作在LinuxPtraceDump这个类中进行。client 程序崩溃之后中,dump出来的内容主要包含以下几个内容:

  a) 各个线程相关的运行时信息。如 stack pointer,context,mapping等,以进程为单位保存在一个数组中(list)

  b) 当前进程的各种内存映射(Mapping)

  c) 用户指定的内存区域。 (application-provided memory regions)

  d) 异常信息。(crash address, signal,thread id, context)

  e) 当前系统的信息 (cpu , os info)

  上述的内容会以minidump的格式组织起来保存为文件,minidump格式是一种简单流格式,由微软所设计,具体可看下图:

  linux下core dump配置,以及google breakpad详解_第4张图片

上图中,_MINIDUMP_DIRECTOR就是各种流,流的类型不同,它所包含的数据形式就有所不同。

具体各个流的数据格式是怎样的,可以参考一下MSDN的说明:

http://msdn.microsoft.com/en-us/library/windows/desktop/ms680394(v=vs.85).aspx

2.4.2  SYMBOL DUMPER.

      这个模块主要是用来从可执行程序中提取与符号相关的信息,并保存为一种特定格式的文件

      为什么要提取符号信息? 前面介绍的client模块在程序崩溃时保存了一个core dump文件,但这个core dump出于简单及实用考虑,保存的都些二进制的东西,只通过这些二进制,我们根本无法重建出可读的call stack. 因此symbol dumper就是用来产生一个可与core dump配合起来使用的符号文件。

      编译非release版本的程序时(如,gcc 开了-g选项),编译器通常会将带有符号相关的信息以某种格式(DWARF,STABS)组织起来,存放在可执行文件的某个段位里。breakpad的symbol dumper就是要从这些段位里提取出对它有用的信息。     

下面具体来说一下这个symbol file:

1)symbol file中全部内容都是ascii文本。

2)symbol file的内容以行单位,每一行称作一条记录,每条记录中有多个字段,每个字段以空格分开。

3)每条记录的开头是一个串字符,这个字符标记这条记录是什么类型的记录。但Line record除外,这种类型的记录,默认省略掉标记符,也就是如果有一行没有标记类型,这一行就是一个Line record.

4)记录中有些字段是10进制或16进制的字符串,16进制也没有以0x开头,要分清某个数字具体是哪种进制,就要看这些数字是在哪种记录里,属于哪个字段,这些都是规定死了的。

     记录的类型主要有以下几种:

  • MODULE:    这种记录用来描述当前这个可执行文件。这条记录是symbol file的第一条记录。

  • FILE:     这种记录用来记录源文件,包含有文件名及路径信息。这个类型的记录会被分配一个整形符号来作标记,然后在别的记录中可能会引用它。

  • FUNC:     这种记录用来描述一个函数,包含函数名,函数在可执行文件中的地址等信息。

  • 行记录:     这种记录用来描述,一个给定范围的机器指令对应哪个源文件的哪一行。行记录总是跟在FUNC记录后面,从而描述每个函数里的指令对应在源码里的位置。

  • PUBLIC:   这种记录用来描述每一个链接符号的地址,如汇编函数里的各个入口点。

  • STACK WIN: 这种记录用来描述函数调用时,函数帧(stack frame)的布局。有了这个记录,给定一特定的函数帧F,就可以找到哪个函数帧调用了F。
  • STACK CFI:CFI, 就是Call Frame Info,这种记录用来表述当执行到某条指令的时候,怎样去查看当前的函数调用栈。

      上面主要讲了symbol file中的内容是怎样组织的,这里并不管其中的symbol是来自DWARF,还是STABS。这也正是breakpad定制自己的symbol格式的意义所在。

2.4.3 PROCESSOR MODULE.

     前面所介绍的两个模块,分别输出了coredump及symbol file.

     这里要介绍的processor模块,它的作用就是根据coredump及symbol file,构建出可读的call stack。 stackwalking 从MinidumpProcessor这个类开始,MinidumpProcessor::Process()这个函数以symbol file,minidump file为参数。
            linux下core dump配置,以及google breakpad详解_第5张图片

    需要说明的是,stack walking 是针对每一个线程进行的。minidump中保存了每个线程运行时的相关信息,这些信息都会在Process()函数中被提取出来。MinidumpMemoryRegion包含着线程的调用栈,MinidumpContext包含着线程的cpu context。 stackwalking 开始时,Stackwalker::Walk()根据不同的cpu,构建出当前线程的top frame,也就是函数调用的最顶一层。然后从top frame开始,对整个调用栈的栈帧进行解析。解析的过程,包含有几方面的内容:

  (a) 查找模块

       根据当前帧的eip(x86)来调用 CodeModules::GetModuleForAddress()返回当前frame所属的模块信息。

  (b) 定位符号

       前面找到模块后,找到只是二进制相关的信息。要找到这个模块相应的名字及模块里其它函数,变量的名字等,还需要用到之前symbole file.

       这里需要注意的是,symbol file可能不止一个,因此需要能够根据当前的模块来定位到,与这个模块相关的symbol file是哪个。SimpleSymbolSupplier这个类就是用来做这个事情,它会结合当前模块的信息,定位到与当前module相关的symbol file。

  (c) 查找符号

       前面一步找到了symbol file.这里就需要根据symbol file来输出具体的符号。

       SourceLineResolverInterface这个类的LoadModuleUsingMemoryBuffer() 用来把symbol file加载进内存,并解析。

       BasicSourceLineResolver这个类则是提供对外的接口,用于根据某个地址,查找出对应的符号名字,如,输入一个函数地址,返回函数的名字。

  (d) 查找出当前帧的调用帧

       当前帧解析完后,需要继续去解析调用当前帧的父帧。要做到这件事情,必须要有symbol file的支持。回忆一下,symbol file中有二种记录类型:stack win,stack cfi。

       这两种类型的记录完整的描述了各类函数调用的栈帧布局,因此借助这些记录理论上就可以找回当前帧的调用帧。

     SourceLineResolverInterface就是用来做这些事情。具体可以查看它的成员函数,FindWindowsFrameInfo()及FindCFIFrameInfo()。    

 

2.5 安装方法

      与标准的开源项目基本上差不多: ./configure 、 make 、 make install 。
      需要注意的是编译生成的库在当前目录和标准目录中都没有,具体需要查看 libcoredumper.la 描述信息,方法: vi libcoredumper.la 。
     否则有可能找不到动态库或静态库。

2.6 使用方法

1、  包含 src 目录下的 google/coredumper.h 。
2、  调用 GetCoreDump 或 WriteCoreDump 等系列接口生成 core 文件。
3、  在 coredumper.h 中大部分函数都有 man 帮助。
4、  特别注意的是 WriteCoreDump 实际上是调用 GetCoreDump ,语义上好像是有点问题,看接口名称是有点困惑,我是看代码才发现的。
5、  可以指定预定义的压缩器来生成压缩的 core 文件,也可以自定义压缩算法生成 core 文件。

3 总结

     工欲善其事必先利其器,对程序开发来说,尤其如此,好的工具常常能对我们的工作起到事半工倍的作用,而对于工具的使用我们不应仅仅满足于知道怎么用,知其然也要能知其所以然,学习和分析别人的工具是怎么做出来的,不仅能帮助我们更好地理解和使用这些工具,更重要的是能帮助我们开阔视野和增长知识。前文对breakpad在linux平台下的实现做了简单介绍,从中我们可以看出一个完善的工具链实现起来是一项浩大的工程,涉及到许多方方面面的知识,里面可以学习的东西很多,需要完善的东西也很多,breakpad作为一个开源项目,现在仍处在开发和完善的过程中,回馈开源的最好方式就是加入其中贡献你的力量,希望本文能对有兴趣的读者有帮助。

 

引用:

https://blog.csdn.net/forever_feng/article/details/4676420

http://r12f.com/posts/google-breakpad-1-introduction-with-windows/

http://brionas.github.io/2014/05/14/google-breakpad/

 

你可能感兴趣的:(C/C++,Linux)