一.   Linux 下 cordump 机制

Linux 系统下存在一种 coredump 机制。

当程序运行过程中异常终止或崩溃,Linux 操作系统会将程序当时的堆栈等内存状态信息记录下来,会在指定文件下生成一个 coredump 相关的 Log 文件。软件开发人员可以通过对 coredump文件进行分析,即可定位到导致程序运行崩溃的 bug

当程序访问的内存超过了系统给定的内存空间,就会产生 Segmentation fault(core dumped),所以,一般产生段错误的原因有如下:

访问了不存在的内存地址,访问了系统保护的内存地址,数组或堆空间访问越界,,多线程程序使用了线程不安全的函数。多线程读写的数据未加锁保护等等一系列内存非法访问操作。

二.  coredump 文件定位方法

根据上一篇配置 coredump 机制的方法,成功配置好之后。就可以运行代码,当程序运行中崩溃时即可产生相应的 coredump 文件。

 在进行Coredump分析时,以下是一些基本的技巧:

  • 查看函数调用栈: 使用 bt 命令可以查看崩溃时的函数调用栈,找到出错的函数。
  • 查看变量值: 使用 info 命令可以查看 寄存器值等,找到出错的变量。
  • 使用gdb调试时进行更深入的分析: GDB提供了很多命令,比如 p(打印变量值)、x(查看内存)、watch(设置变量监视器)等,可以帮助开发者进行更深入的分析。熟练掌握这些命令可以提高分析效率。
  • 使用符号表: 如果程序是使用编译器编译的,那么可以使用符号表来查看函数名、变量名等信息。使用以下命令生成符号表(以下命令针对 c++):
g++ -g <program_name>.cpp -o <program_name>

其中,-g选项表示生成符号表。生成符号表后,可以在gdb调试中查看函数名、变量名等信息。

这里为了方便起见,不更改 coredump 文件的生成路径,即生成的 coredump 文件会与可执行程序在同一目录下,文件默认叫 core 的文件。

下面以一段测试代码说明一下,Linux 环境下 使用 gdb 调试器对 coredump 文件进行分析调试。

代码如下:

#include <stdio.h>
#include <string.h>
#include <unistd.h>

int fun(void)
{
    printf("---fun()\n");
    char buffer[3] = {0}; 
    memcpy(buffer, "Hello", 6);  
    printf("---buffer: %s\n", buffer);  
    return 0;
}

int main(void)
{
    printf("---main()---\n");   
    int pid = 0;
    pid = getpid();
    printf("pid %d\n", pid);
    fun(); 
    printf("---End main\n");
    return 0;
}

1. 编译运行。操作如下:

编译代码。输入 gcc -g main.c -o main.out 命令

运行程序。输入 ./main.out 命令。如下所示:

wangtian@wangtian-virtual-machine:~/Code_Learns/C_Learns/debug/debug2$ ./main.out 
---main()---
pid 3814
---fun()
---buffer: Hello
*** stack smashing detected ***: terminated
已放弃 (核心已转储)

当程序出现段错误时崩溃时,可查看程序当前目录下是否生成 coredump 相关文件。如下所示:

wangtian@wangtian-virtual-machine:~/Code_Learns/C_Learns/debug/debug2$ ls -l
总用量 180
-rw------- 1 wangtian wangtian 393216 12月 13 16:55 core
drwxrwxr-x 2 wangtian wangtian   4096 11月 25 22:24 debug
-rw-rw-r-- 1 wangtian wangtian     90 10月 28 13:32 debug.c
-rw-rw-r-- 1 wangtian wangtian     81 10月 20 18:12 debug.h
-rw-rw-r-- 1 wangtian wangtian   5688 12月 13 16:55 debug.o
-rw-rw-r-- 1 wangtian wangtian    356 12月 13 16:55 main.c
-rw-rw-r-- 1 wangtian wangtian   7144 12月 13 16:55 main.o
-rwxrwxr-x 1 wangtian wangtian  21144 12月 13 16:55 main.out

2.  gdb 调试 coredump 文件

查看 coredump 文件。输入" objdump -t +coredump文件名" 命令:

wangtian@wangtian-virtual-machine:~/Code_Learns/C_Learns/debug/debug2$  objdump -t core
core:     文件格式 elf64-x86-64
SYMBOL TABLE:
无符号

可以看到,coredump 文件是不带符号表的。所以调试时,需要同时加上 可执行程序文件。

输入命令 gdb main.out core 进行调试,如下所示:

wangtian@wangtian-virtual-machine:~/Code_Learns/C_Learns/debug/debug2$ gdb main.out core

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from main.out...
[New LWP 4272]
Core was generated by `./main.out'.
Program terminated with signal SIGABRT, Aborted.
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
50	../sysdeps/unix/sysv/linux/raise.c: 没有那个文件或目录.

可以看到,程序收到 signal SIGABRT 后崩溃了。

输入 bt (backtrace 缩写) 命令,可以查看下程序崩溃时的堆栈信息,如下所示:

(gdb) bt
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1  0x00007ff8d6f5c859 in __GI_abort () at abort.c:79
#2  0x00007ff8d6fc729e in __libc_message (action=action@entry=do_abort, 
    fmt=fmt@entry=0x7ff8d70f108f "*** %s ***: terminated\n")
    at ../sysdeps/posix/libc_fatal.c:155
#3  0x00007ff8d7069aea in __GI___fortify_fail (
    msg=msg@entry=0x7ff8d70f1077 "stack smashing detected") at fortify_fail.c:26
#4  0x00007ff8d7069ab6 in __stack_chk_fail () at stack_chk_fail.c:24
#5  0x000056183642c243 in fun () at main.c:12
#6  0x000056183642c28c in main () at main.c:20

以上的堆栈信息,显示 __stack_chk_fail () 错误。可以看到调用 fun() 函数过程中发生了栈问题。

__stack_chk_fail () 打印信息说明发生了缓冲区溢出,即数组越界问题。

从这段栈信息中并不能直观的定位到出现问题的地方,因为出现问题的地方是之前的数组越界,但是这里的栈信息已经执行到主函数 main中, 特别是在大项目中更是无法准确的定位出错的地方。
越界写坏内存,什么时候会 crash 取决于我们写坏的内存什么时候被用到,有时候会离犯罪现场特别远。所以,导致定位这样情况的崩溃问题比较麻烦。

在发生__stack_chk_fail () 错误的附近函数 fun() 中第11 行打断点,如下所示:

(gdb) b main.c:11
Breakpoint 1 at 0x56183642c22a: file main.c, line 11.
(gdb) info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000056183642c22a in fun at main.c:11
(gdb) r
Starting program: /home/wangtian/Code_Learns/C_Learns/debug/debug2/main.out 
---main()---
pid 3868
---fun()
---buffer: Hello

Breakpoint 1, fun () at main.c:11
11	    return 0;
(gdb) n
12	}
(gdb) 
*** stack smashing detected ***: terminated

Program received signal SIGABRT, Aborted.
__GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
50	../sysdeps/unix/sysv/linux/raise.c: 没有那个文件或目录.
(gdb) 

初步可以定位到,是在函数 fun() 内部发生了数组越界问题。目前还不能定位到具体某一行。

canary 栈溢出保护机制的具体原理: gcc 编译器默认开启了 canary 栈保护机制。canray 栈保护机制是专门针对栈溢出攻击涉及的一中保护机制。由于栈溢出攻击的主要目标是通过溢出覆盖函数栈高位的返回地址,因此,其思路是在函数开始执行前,即返回地址前写入一个字长的随机数据(canary),在函数返回前校验该值是否被改变,如果改变则认为是栈溢出,程序直接终止,以此来防止信息泄露。


总结:

以上一系列的操作,说明通过 gdb 对 数据越界问题的调试定位,这种方法不是太好用。

原因:越界写坏内存,什么时候会 crash 取决于我们写坏的内存什么时候被用到,有时候会离犯罪现场特别远。所以,导致定位这样情况的崩溃问题比较麻烦。后续文章再针对数组越界问题定位方法进行新的总结。

Logo

更多推荐