Linux Core 文件详细介绍

2022-03-17 language

Core 文件又称为 Core Dump 文件,对于线上的服务而言,也就意味着进程异常;而且,如果进程占用内存很大,但是 dump 到磁盘上,也会花很长时间。

当然,Core 虽然会终止掉当前进程,但是也会保留下第一手的现场数据,包括了进程被终止时内存、CPU寄存器等信息,可以供后续开发人员进行调试。

接下来,看看 MySQL 中 Core 文件的处理。

简介

core file logo

在开发一个程序时,程序可能会在运行过程中异常终止或者崩溃,这时操作系统就会把程序挂掉时的内存状态记录下来,并写入一个叫做 Core 的文件中,这种行为就叫做 Core Dump 操作,通过这个文件可以方便的进行调试。

在使用半导体作为内存的材料前,人类使用的是线圈作为内存的材料,线圈叫做 Core,用线圈制作的内存就是 Core Memory。

除了内存信息之外,还有些关键的程序运行状态也会同时 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.

开启CoreDump

可以使用命令 ulimit 开启,也可以在程序中通过 setrlimit 系统调用开启;先介绍下前者配置方式。

----- 查看配置,如果为0,则说明未开启
$ ulimit -c

----- 设置转储文件大小,单位是blocks(KB),unlimited表示不限
# ulimit -c unlimited

----- 设置转储文件大小为100KB
# ulimit -c 100

当设置为 unlimited 时,则表示不限制内核转储文件的大小,发生问题时所有的内存都将转储到文件中;对于大量消耗内存的程序可以限制转储文件的大小。

如果要持久化,可以修改 /etc/security/limits.conf 文件即可,参考如下示例。

#<domain>      <type>  <item>         <value>
    *           soft    core          unlimited

默认生成的 core 文件保存在可执行文件所在目录下,文件名为 core;当然,也可以通过如下方式进行设置。

----- 添加PID后缀
# echo 1 > /proc/sys/kernel/core_uses_pid

----- 设置输出目录,格式为core-命令名-PID-时间戳
# echo "/tmp/core-%e-%p-%t" > /proc/sys/kernel/core_pattern

常见参数:
   %t: 设置文件转储时的 unix 时间,从 1970.1.1 0:00:00 开始的秒数。
   %e: 执行的命令名。
   %p: 被转储进程的 PID 。
   %u: 被转储进程的真实用户 ID ,也即 UID 。
   %g: 被转储进程的真实组 ID ,也即 GID 。
   %s: 引发转储的信号编号。
   %h: 主机名,同 uname(2) 返回的 nodename 。
   %c: 转储文件大小的上限,2.6.24 以后可以使用。

设置完 core_pattern 之后,core_user_pid 会无效,也可以通过 sysctl 进行设置。

# cat /etc/sysctl.conf
kernel.core_pattern = /var/%e-%p-%t.core
kernel.core_user_pid = 0
# sysctl -p

测试示例

可以在程序执行期间发送 SIGSEGV(11) 信号,也即 Ctrl+\ 快捷键。

----- 使用Ctrl+\退出程序,产生core dump
$ sleep 100
^\Quit (core dumped)

----- 或者发送SIGSEGV(11)信号
# kill -s SIGSEGV $$
# kill -11 <pid>

接着看一个简单的示例程序。

$ cat << EOF > create_core.c
int a (int *p)
{
	int y = *p;
	return y;
}
int main (void)
{
	int *p = NULL;
	*p = 0;         // 访问0地址,发生Segmentation fault错误
	return a(p);
}
EOF
$ gcc -Wall -g create_core.c -o create_core
$ ./create_core
Segmentation fault (core dumped)

也可以通过 API 进行设置,不过此时编译之后,在运行时需要 root 权限。

#include <stdio.h>
#include <unistd.h>
#include <sys/resource.h>
#define CORE_SIZE   1024 * 1024 * 500
int main(void)
{
	struct rlimit rlmt;

	if (getrlimit(RLIMIT_CORE, &rlmt) == -1)
		return -1;
	printf("Before set rlimit CORE dump current is:%d, max is:%d\n",
		(int)rlmt.rlim_cur, (int)rlmt.rlim_max);

	rlmt.rlim_cur = (rlim_t)CORE_SIZE;
	rlmt.rlim_max  = (rlim_t)CORE_SIZE;
	if (setrlimit(RLIMIT_CORE, &rlmt) == -1)
		return -1;
	if (getrlimit(RLIMIT_CORE, &rlmt) == -1)
		return -1;
	printf("After set rlimit CORE dump current is:%d, max is:%d\n",
		(int)rlmt.rlim_cur, (int)rlmt.rlim_max);

	int *ptr = NULL;  // 测试非法内存,产生core文件
	*ptr = 10;
	return 0;
}

在调试时可以通过 gdb program core-file 调试,当然编译时,需要加上调试信息 -g

$ gdb core_demo core_demo.core.24816
...
Core was generated by './core_demo'.
Program terminated with signal 11, Segmentation fault.
#0  0x080483cd in func (p=0x0) at core_demo.c:5
5       int y = *p;
(gdb)  where                        # 或者backtrace
#0  0x080483cd in func (p=0x0) at core_demo.c:5
#1  0x080483ef in main () at core_demo.c:12
(gdb) info frame
Stack level 0, frame at 0xffd590a4:
 eip = 0x80483cd in func (core_demo.c:5); saved eip 0x80483ef
 called by frame at 0xffd590c0
 source language c.
 Arglist at 0xffd5909c, args: p=0x0
 Locals at 0xffd5909c, Previous frame's sp is 0xffd590a4
 Saved registers:
  ebp at 0xffd5909c, eip at 0xffd590a0

从上面可以看出,可以还原 core_demo 执行时的场景,使用 where 或者 backtrace 查看当前程序调用函数栈帧,来定位 core dump 的文件行,还可以查看寄存器、变量等信息。

也可以通过如下方式查看。

$ gdb -c core_demo.core.24816 core_demo
$ gdb core_demo
(gdb) core core_demo.core

debuginfo

一般线上生成的文件会删除 Debug、符号表信息,但是一旦有问题了,例如发生了 CoreDump ,那么就需要使用符号表了。

使用 file test 命令查看时,会显示改文件为 not stripped ,当通过 nm test 查看时会发现一堆的符号,或者通过 readelf -S test 查看。

为了能够使用 gdb 跟踪调试程序,需要在编译期使用 -g 选项;而对于系统库或是 Linux 内核,使用 gdb 调试或使用 systemtap 探测时,还需要安装相应的 debuginfo 包。

例如 glibc 及它的 debuginfo 包。

# yum --enablerep=base-debuginfo install glibc-debuginfo

$ rpm -qa | grep glibc
glibc-2.17-157.el7_3.1.x86_64
glibc-debuginfo-2.17-157.el7_3.1.x86_64

接下来,我们看看 debuginfo 包中包含了那些信息,该包是如何制作的,而且 glibcdebuginfo 是如何关联起来的。

包含信息

首先看看 glibc-debuginfo 包中包含有什么内容。

$ rpm -ql glibc-debuginfo
/usr/lib/debug
/usr/lib/debug/.build-id
/usr/lib/debug/.build-id/00
/usr/lib/debug/.build-id/00/98d7f56d263e087a1abad592e81e3e79e26652
/usr/lib/debug/.build-id/00/98d7f56d263e087a1abad592e81e3e79e26652.debug
... ...
/usr/lib/debug/lib64
/usr/lib/debug/lib64/ld-2.17.so.debug
/usr/lib/debug/lib64/ld-linux-x86-64.so.2.debug
/usr/lib/debug/lib64/libBrokenLocale-2.17.so.debug
/usr/lib/debug/lib64/libBrokenLocale.so.1.debug
... ...
/usr/src/debug/glibc-2.17-c758a686
/usr/src/debug/glibc-2.17-c758a686/argp
/usr/src/debug/glibc-2.17-c758a686/argp/argp-ba.c
/usr/src/debug/glibc-2.17-c758a686/argp/argp-eexst.c
/usr/src/debug/glibc-2.17-c758a686/argp/argp-fmtstream.c
... ...

可以看出 glibc-debuginfo 大致有三类文件:

  1. 存放在 /usr/lib/debug/ 下的 .build-id/nn/nnn...nnn.debug 文件,文件名是 hash key 。
  2. 存放在 /usr/lib/debug/ 下的其它 *.debug 文件,其文件名,是库文件名+.debug 后缀。
  3. glibc 的源代码。

当使用 gdb 调试时,需要在机器码与源代码之间,建立起映射关系,这就需要三个信息:

  • 机器码:可执行文件、动态链接库,例如上面的 /lib64/libc-2.18.so
  • 源代码:显然就是 glibc-debuginfo 中,包含的 *.c*.h 等源文件;
  • 映射关系:也就是保存在 *.debug 文件中的信息。

如何生成

通过 gcc -g 编译时,默认机器码与源代码的映射关系会与可执行程序、动态链接库合并在一起;但是这样就导致文件特别大,而对于普通用户来说是不需要的。

正是了为解决这个问题,在 Linux 上的各种程序和库,在生成 RPM 时,就已经把 debuginfo 单独的抽取出来,因此形成了独立的 debuginfo 包。

$ cat << EOF > foobar.c
#include <stdio.h>
int main(void)
{
	printf("Hello World!!!\n");
	return 0;
}
EOF

----- 其中参数-ggdb生成gdb格式调试信息
$ gcc -ggdb foobar.c -o foobar
----- 查看段信息,有一堆的.debug段
$ readelf -S foobar

----- 删除.debug段的信息,符号表和字符串还在,调试仍可以查看符号信息
$ strip --strip-debug foobar
----- 同时删除.symtab和.strtab,默认的操作
$ strip --strip-all foobar

----- 创建一个包含debuginfo的文件
$ objcopy --only-keep-debug foobar foobar.debug
----- 添加一个包含路径文件的.gnu_debuglink section,注意执行时文件必须存在
$ objcopy --add-gnu-debuglink=foobar.debug foobar

----- 或者一步到位
$ eu-strip foobar -f foobar.debug

----- 查看新添加的.gnu_debuglink section 
$ objdump -s -j .gnu_debuglink test

----- 清除原执行文件中的调试信息,如下两个操作相同
$ objcopy --strip-debug foobar
$ strip --strip-debug foobar

----- 此时尝试加载调试符号时会报错
$ gdb foobar
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-94.el7
... ...
Reading symbols from /tmp/foobar...(no debugging symbols found)...done.
(gdb)

----- 当然,现在可以指定gdb需要加载的debuginfo即可,如下两者相同
$ gdb foobar -s foobar.debug
$ gdb
(gdb) file foobar
(gdb) symbol-file foobar.debug

显然,gdb 现在无法找到调试信息了;我们需要告诉 gdb 如何正确地找到它对应的 debug 文件,也就是上述的 foobar.debug 文件。

对于 Linux 下的 ELF(Executable and Linkable Format) 格式文件,可以通过一个 .gnu_debuglink 段来保存信息,可通过 objcopy --add-gnu-debuglink 添加。

----- 添加一个包含路径文件的.gnu_debuglink section
$ objcopy --add-gnu-debuglink=foobar.debug foobar

----- 查看.gnu_debuglink section
$ objdump -s -j .gnu_debuglink foobar
foobar:     file format elf64-x86-64
Contents of section .gnu_debuglink:
 0000 666f6f62 61722e64 65627567 00000000  foobar.debug....
 0010 ba8924f6

----- 现在可以直接调试了
$ gdb foobar
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-94.el7
... ...
Reading symbols from /tmp/test/foobar...Reading symbols from /tmp/test/foobar.debug...done.
done.
(gdb)

上面的 objcopy 是把 foobar.debug 的文件名以及这个文件的 CRC 校验码,写到了.gnu_debuglink 这个 ELF 的头部值中,但是并没有告诉 foobar.debug 所在的路径。

搜索路径

在 Linux 中,在编译时会根据时间戳生成 build-id,并保存到 gnu.build-id section 中;可以通过如下命令查看 "gnu.build-id" section 信息。

$ readelf -n foobar
$ readelf -t foobar |grep build-id
$ readelf --wide --sections foobar |grep build
$ objdump -s -j .note.gnu.build-id foobar

而 gdb 默认会搜索指定目录 (show debug-file-directory) 下与 build-id 关联的 .debug 文件。默认 gdb 搜索的文件名为 NN/N...N.debug, 前两个 NN 就是 build-id 前两位,后面的 N...N 则是 build-id 的剩余部分,只是改了个文件名而已。

gdb 则是通过下面的顺序查找 foobar.debug 文件:

1. <global debug directory>/.build-id/nn/nnnn...nnnn.foobar.debug
2. <the path of foobar>/foobar.debug
3. <the path of foobar>/.debug/foobar.debug
4. <global debug directory>/<the patch of foobar>/foobar.debug

<global debug directory> 默认为 /usr/lib/debug/;可以通过 set/show debug-file-directory 命令来设置或查看这个值。

(gdb) show debug-file-directory
(gdb) set debug-file-directory PATH

假设 foobarBuild ID3bda624ab466b7725faaf5cde424a5674a741c5d,可将 foobar.debug 文件移动到 /usr/lib/debug/.build-id/3b/da624ab466b7725faaf5cde424a5674a741c5d.debug

foobar.debug 默认会采用 DWARF 4 格式来保存调试信息,可以通过 readelf -w foobar.debug 来查看 DWARF 的内容;详见 DWARF Debugging Information Format Version 4

如下是放置到默认路径下的一个示例:

----- 查看二进制文件的BuildID,并添加默认目录下
$ readelf -n test | grep Build
    Build ID: e380efb0fe7873bcc96506035e8640a365b29ef4
$ mkdir -p /usr/lib/debug/.build-id/e3
$ mv test.debug /usr/lib/debug/.build-id/e3/80efb0fe7873bcc96506035e8640a365b29ef4.debug

----- 查看当前debuginfo默认搜索目录,可通过set debug-file-directory path重新指定
(gdb) show debug-file-directory
The directory where separate debug symbols are searched for is "/usr/lib/debug".  
----- 会自动加载debuginfo文件
(gdb) file test

生成Marker探针

通过 gcc -g 命令,所有函数名都会自动的生成相应的 debuginfo,供 systemtap 进行探测, 其局限性在于,只能收集到函数调用的初始时刻、以及函数返回的结束时刻的上下文信息。

为了解决这个问题,又提出了一种新方法 Compiled-in instrumentation,可以把探针安插到指定的某行代码中,从而可以收集到那行代码执行时的上下文信息,这种探针被称为 Marker 探针。

编写 Marker 探针,示例如下:

#include <sys/sdt.h>
DTRACE_PROBE(provider, name)
DTRACE_PROBE4(provider, name, arg1, arg2, arg3, arg4)

写好 Marker 探针并成功编译后,可以使用下面的 systemtap 指令来查看 Marker 探针是否生效。

stap -L 'process("/path/to/foobar").mark("*")'

详细可以参考 Adding User Space Probing to an Application (heapsort example)

其它

----- 查找core文件,以及文件类型
# find $HOME -name "core*"
/home/oli/core.6440
# file core
core:      ELF 32-bit LSB core file Intel 80386, version 1 (SYSV), SVR4-style

----- 查看是那个进程信息
# strings core.6440 | head

可以在 core_pattern 中加入管道符,然后调用用户程序,例如将转储文件压缩。

# echo "|/usr/local/sbin/core_helper %e %p %t" > /proc/sys/kernel/core_pattern

$ cat /usr/local/sbin/core_helper
#!/bin/sh
exec gzip - > /var/$1-$2-$3.core.gz
$ gunzip -c /var/xxx-xxx-xxx.core.gz > ~/xxx-xxx-xxx.core

可以将 ulimit -S -c unlimited > /dev/null 2>&1 使用户登陆时即可以设置转储功能。默认内核会转储共享内存,可以设置排除共享内存。

MySQL

对于一般进程,要让进程崩溃时能生成 core file 用于调试,只需要设置 rlimitcore file size > 0 即可;比如,用在 ulimit -c unlimited 时启动程序。

对 MySQL 来说,由于 core file 中会包含表空间的数据,所以默认情况下为了安全,mysqld 捕获了 SEGV 等信号,崩溃时并不会生成 core file,需要在 my.cnf 或启动参数中加上 core-file

[mysqld]
core_file = ON

但是即使做到了以上两点,在 mysqld crash 时可能还是无法 core dump

如果程序通过 seteuid()/setegid() 系统调用,改变了进程的有效用户或组,则在默认情况下系统不会为这些进程生成 CoreDump 。简单来说,如果你当初是以用户 A 运行了某个程序,但在 ps 里看到的这个程序的用户却是 B 的话,那么这些进程就是调用了 seteuid 了。

对于 MySQL 来说,无论通过什么用户利用 mysqld_safe 启动,mysqld 的有效用户始终是 mysql 用户;为了能让 MySQL 生成 core dump,需要设置 /proc/sys/fs/suid_dumpable 文件的内容改为 1

之后,就可以用 kill -SEGVmysqld 崩溃,测试一下能不能正常产生 core file 了。

参考