crash工具源码分析

【摘要】

crash工具用于解析kdump文件,可用于内核crash事后分析。本文的产生是因为需要移植kdump、 crash工具到mips64架构,

本着方便后人的原则,顺带将crash流程分析整理,以备查阅。本文分析对象为截止2014年8月发布的最新版本crash-7.0.7。

一、编译框架

为什么要分析编译框架?因为标准crash工具不支持交叉编译。crash只支持两种运行方法,一种是host和target都是一个cpu架构,另外一种是host和target为不同的cpu架构,但是两个程序都只能在host上编译出来,然后执行。说简单点,打个比方,crash支持:1)x86架构编译出x86格式的程序,这个程序只能解析x86内核死机保存的dump文件;2)x86架构编译出x86格式的程序,这个程序只能解析arm内核死机保存的dump文件。cgel的环境决定了我们非交叉编译不可,所以就得硬着头皮分析改造 crash的编译框架。

crash编译的入口在源码根目录Makefile的all:

all: make_configure
	@./configure ${CONF_TARGET_FLAG} -p "RPMPKG=${RPMPKG}" -b
	@make --no-print-directory gdb_merge

我们这里提前把crash的大体编译流程概括下:先执行make_configure将configure.c编译成configure程序;再执行configure程序,把Makefile里的一些全局变量赋值;接着进入gdb_merge流程,该流程会先编译出gdb库,再同crash的源码编译出来的对象链接在一起,成为最终的可执行程序。

为了支持交叉编译,我们主要对Makefile和configure.c分别做了手脚,主体思想就是千方百计修改编译工具为交叉工具链,以及修改gdb的configure参数为交叉编译参数等。具体的修改可以查看svn上的代码提交记录。

下面是一条编译的例子:

make target=mips64 CROSS_COMPILE=mips64_gcc4.1.2_glibc2.5.0/bin/mips64-unknown-linux-gnu-

configure.c的流程:

configure.c的main函数里,会根据target=xxx,来设置参数。

	case 't':
			target_data.target_as_param = optarg;
			break;

以mips64为例,则target_data.target_as_param= mips64

接着configure解析-b,

	case 'b':
			build_configure(sp);
			break;
build_configure里, 调用get_current_configuration,
根据当前编译工具链,设置target架构。

#ifdef __mips64
        target_data.target = MIPS64;
#endif
一般情况下,在x86_64 的host上编译一个x86_64的binary(但可以识别mips64),那么target_data.target = X86_64;
,如果是交叉工具链编译,则 target_data.target = MIPS64;
接着set_initial_target并没有做什么事。 再后面,初始化target_data.host与target一样:
target_data.host = target_data.target;
这样host和target都是mips64了,后面会根据target_data.target_as_param,来修改target
(也就是说,target是编译命令行指定,host是工具链的架构,因为crash一般支持的是在具体
架构下面直接编译而非交叉编译,工具链的架构也就是运行环境的架构)。
	if (target_data.target_as_param) {
			if ((target_data.target == X86_64) &&
			(name_to_target((char *)target_data.target_as_param) == MIPS64)) {
			/*
			 *  Build an MIPS64 crash binary on an X86_64 host.
			 */
			target_data.target = MIPS64;
target修正完毕后,会尝试对target_data.initial_gdb_target进行赋值。
在set_initial_target函数里,target_data.initial_gdb_target = UNKNOWN;
由于gdb没解压,文件crash.target不存在,因此不会对target_data.initial_gdb_target进行修正。

接着就是修正target_data的各种值:

strcat(target_data.program, "D");
strcpy(target_data.release, buf);
修正完毕后,回到build_configure,根据之前的结果,来计算各参数值:

	case MIPS64:
                target = TARGET_MIPS64;
                target_CFLAGS = TARGET_CFLAGS_MIPS64;
		gdb_conf_flags = GDB_TARGET_MIPS64_ON_X86_64;
                break;
接着打开Makefile,并新建一个Makefile.new,作为后续要写的Makefile
makefile_setup(&fp1, &fp2);

接下来,将Makefile里的各个变量赋值,其中比较重要的是这几个变量:

TARGET=, TARGET_CFLAGS=, GDB_CONF_FLAGS=, LDFLAGS=

                     while (fgets(buf, 512, fp1)) {
		if (strncmp(buf, "TARGET=", strlen("TARGET=")) == 0)
			fprintf(fp2, "%s\n", target);
                else if (strncmp(buf, "TARGET_CFLAGS=",
			strlen("TARGET_CFLAGS=")) == 0)
                       	fprintf(fp2, "%s%s%s\n", target_CFLAGS,
				cflags ? " " : "", cflags ? cflags : "");
		else if (strncmp(buf, "GDB_CONF_FLAGS=",
			strlen("GDB_CONF_FLAGS=")) == 0)
			fprintf(fp2, "%s\n", gdb_conf_flags);
		else if (strncmp(buf, "GDB_FILES=",strlen("GDB_FILES=")) == 0)
			fprintf(fp2, "%s\n", sp->GDB_FILES);
		else if (strncmp(buf, "GDB_OFILES=",strlen("GDB_OFILES=")) == 0)
                        fprintf(fp2, "%s\n", sp->GDB_OFILES);
		else if (strncmp(buf, "GDB_PATCH_FILES=",strlen("GDB_PATCH_FILES=")) == 0)
                        fprintf(fp2, "%s\n", sp->GDB_PATCH_FILES);
		else if (strncmp(buf, "GDB_FLAGS=",strlen("GDB_FLAGS=")) == 0)
                        fprintf(fp2, "%s\n", sp->GDB_FLAGS);
		else if (strncmp(buf, "GPL_FILES=", strlen("GPL_FILES=")) == 0)
			fprintf(fp2, "GPL_FILES=%s\n", strcmp(sp->GPL, "GPLv2") == 0 ? 
				"COPYING" : "COPYING3");
                else if (strncmp(buf, "GDB=", strlen("GDB=")) == 0) {
                        fprintf(fp2, "%s\n", sp->GDB);
                        sprintf(target_data.gdb_version, "%s", &sp->GDB[4]);
		} else if (strncmp(buf, "LDFLAGS=", strlen("LDFLAGS=")) == 0) {
                       	fprintf(fp2, "LDFLAGS=%s\n", ldflags ? ldflags : "");
		} else
			fprintf(fp2, "%s", buf);

	}




写完后,将makefile.new改成makefile。

makefile_create:
if (system("mv Makefile.new Makefile") != 0) 
configure程序执行完毕后,Makefile里的变量已经正确赋值,然后进入gdb_merge。

gdb_merge: force
	  make --no-print-directory gdb_unzip; 
        @echo "${LDFLAGS} -lz -ldl -rdynamic" > ${GDB}/gdb/mergelibs
	@echo "../../${PROGRAM} ../../${PROGRAM}lib.a" > ${GDB}/gdb/mergeobj
	  (cd ${GDB}; ./configure ${GDB_CONF_FLAGS} --with-separate-debug-dir=/usr/lib/debug \
	    --with-bugurl="" --with-expat=no --with-python=no; \
	  make --no-print-directory CRASH_TARGET=${TARGET};  
        echo ${TARGET} > crash.target) 
前面configure的时候,我们知道GDB_CONF_FLAGS的值被修正过,来源是:

ldflags = get_extra_flags("LDFLAGS.extra", NULL);
	cflags = get_extra_flags("CFLAGS.extra", NULL);
	gdb_conf_flags = get_extra_flags("GDBFLAGS.extra", gdb_conf_flags);
这几个文件暂时都为空。为了交叉编译,我们需要给上面的几个参数赋值。

	ldflags = adjust_ldflags(ldflags);
	target_CFLAGS = adjust_target_cflags(target_CFLAGS);
	gdb_conf_flags = adjust_gdb_conf_flags(gdb_conf_flags);
解压gdb并打补丁后,进入gdb目录编译,因为没有指定编译目标,默认从all开始。

all:
	@: $(MAKE); $(unstage)
	@r=`${PWD_COMMAND}`; export r; \
	s=`cd $(srcdir); ${PWD_COMMAND}`; export s; \
	  $(MAKE) $(RECURSE_FLAGS_TO_PASS) all-host all-target \
	&& :
即执行all-host和all-target。

.PHONY: all-host

all-host: maybe-all-bfd
all-host: maybe-all-opcodes
all-host: maybe-all-bison
all-host: maybe-all-cgen
all-host: maybe-all-dejagnu
all-host: maybe-all-etc
all-host: maybe-all-fastjar
all-host: maybe-all-fixincludes
all-host: maybe-all-flex
all-host: maybe-all-gprof
all-host: maybe-all-intl
all-host: maybe-all-tcl
all-host: maybe-all-itcl
all-host: maybe-all-libdecnumber
all-host: maybe-all-libgui
all-host: maybe-all-libiberty
all-host: maybe-all-libiconv
all-host: maybe-all-m4
all-host: maybe-all-readline
all-host: maybe-all-sid
all-host: maybe-all-sim
all-host: maybe-all-texinfo
all-host: maybe-all-gdb
all-host: maybe-all-expect
all-host: maybe-all-guile
all-host: maybe-all-tk
all-host: maybe-all-libtermcap
all-host: maybe-all-utils
all-host: maybe-all-gnattools

.PHONY: all-target

all-target: maybe-all-target-libmudflap
all-target: maybe-all-target-libssp
all-target: maybe-all-target-newlib
all-target: maybe-all-target-libbacktrace
all-target: maybe-all-target-libquadmath
all-target: maybe-all-target-libgfortran
all-target: maybe-all-target-libobjc
all-target: maybe-all-target-libgo
all-target: maybe-all-target-libtermcap
all-target: maybe-all-target-winsup
all-target: maybe-all-target-libgloss
all-target: maybe-all-target-libffi
all-target: maybe-all-target-libjava
all-target: maybe-all-target-zlib
all-target: maybe-all-target-boehm-gc
all-target: maybe-all-target-rda
all-target: maybe-all-target-libada
all-target: maybe-all-target-libitm
all-target: maybe-all-target-libatomic
以maybe-all-gdb为例:

.PHONY: all-gdb maybe-all-gdb
maybe-all-gdb:
TARGET-gdb=all
maybe-all-gdb: all-gdb
all-gdb: configure-gdb
	@: $(MAKE); $(unstage)
	@r=`${PWD_COMMAND}`; export r; \
	s=`cd $(srcdir); ${PWD_COMMAND}`; export s; \
	$(HOST_EXPORTS)  \
	(cd $(HOST_SUBDIR)/gdb && \
	  $(MAKE) $(BASE_FLAGS_TO_PASS) $(EXTRA_HOST_FLAGS) $(STAGE1_FLAGS_TO_PASS)  \
		$(TARGET-gdb))
即进入$(HOST_SUBDIR)/gdb执行$(TARGET-gdb)
也就是进入gdb子目录,执行make all。

all: gdb$(EXEEXT)
	@$(MAKE) -s $(FLAGS_TO_PASS) DO=all "DODIRS=`echo $(SUBDIRS) | sed 's/testsuite//'`" subdir_do
makefile的-s参数是禁止输出
即先执行gdb$(EXEEXT),最后执行subdir_do
先看subdir_do,如果子目录有testsuite,则进入编译:

subdir_do: force
	@for i in $(DODIRS); do \
		case $$i in \
		$(REQUIRED_SUBDIRS)) \
			if [ ! -f ./$$i/Makefile ] ; then \
				echo "Missing $$i/Makefile" >&2 ; \
				exit 1 ; \
			fi ;; \
		esac ; \
		if [ -f ./$$i/Makefile ] ; then \
			if (cd ./$$i; \
				$(MAKE) $(FLAGS_TO_PASS) $(DO)) ; then true ; \
			else exit 1 ; fi ; \
		else true ; fi ; \
	done
再来看最重要的gdb$(EXEEXT):

CC_LD=$(CC)
gdb$(EXEEXT): gdb.o $(LIBGDB_OBS) $(ADD_DEPS) $(CDEPS) $(TDEPLIBS)
	@rm -f gdb$(EXEEXT)
	@(cd ../..; make --no-print-directory GDB_FLAGS=-DGDB_7_6 library)//回到crash的makefile
	$(CC_LD) $(INTERNAL_LDFLAGS) $(WIN32LDAPP) \
		-o $(shell /bin/cat mergeobj) $(LIBGDB_OBS) \
		$(TDEPLIBS) $(TUI_LIBRARY) $(CLIBS) $(LOADLIBES) $(shell /bin/cat mergelibs)

all_object_files = gdb.o $(LIBGDB_OBS) gdbtk-main.o \
	test-cp-name-parser.o
LIBGDB_OBS的组成
LIBGDB_OBS= $(COMMON_OBS) $(TSOBS) $(ADD_FILES) init.o

COMMON_OBS = $(DEPFILES) $(CONFIG_OBS) $(YYOBJ) \
	version.o \
	annotate.o \
	addrmap.o \
	auto-load.o auxv.o \
	agent.o \
	bfd-target.o \
	blockframe.o breakpoint.o break-catch-sig.o \
	findvar.o regcache.o cleanups.o \
	charset.o continuations.o corelow.o disasm.o dummy-frame.o dfp.o \
	source.o value.o eval.o valops.o valarith.o valprint.o printcmd.o \
	block.o symtab.o psymtab.o symfile.o symmisc.o linespec.o dictionary.o \
	infcall.o \
	infcmd.o infrun.o \
这里我们看到,他首先会生成gdb.o,接着一些必要的$(LIBGDB_OBS),
再就是$(ADD_DEPS) $(CDEPS) $(TDEPLIBS),

这些gdb的相关依赖执行完毕后,会继续回去执行crash的library。

library: make_build_data ${OBJECT_FILES}
	ar -rs ${PROGRAM}lib.a ${OBJECT_FILES}
而OBJECT_FILES就是各个crash的源码目标文件,编译完crash后,将目标文件打包成crashlib.a
最后gdb执行完library目标后,会生成crash,这就是:

$(CC_LD) $(INTERNAL_LDFLAGS) $(WIN32LDAPP) \
		-o $(shell /bin/cat mergeobj) $(LIBGDB_OBS) \
		$(TDEPLIBS) $(TUI_LIBRARY) $(CLIBS) $(LOADLIBES) $(shell /bin/cat mergelibs)
来完成的。

之前在gdb_merger依赖的前面,有两句:

@echo "${LDFLAGS} -lz -ldl -rdynamic" > ${GDB}/gdb/mergelibs
@echo "../../${PROGRAM} ../../${PROGRAM}lib.a" > ${GDB}/gdb/mergeobj
即把../../crash(空格) ../../crashlib.a 写入mergeobj
把-lz -ldl -rdynamic写入mergelibs
这样,上句就变成:

cc -o ../../crash   ../../crashlib.a $(LIBGDB_OBS) -lz -ldl -rdynamic
至此,crash可执行程序编译成功。

二、代码流程

2.1vmcore_list--访问宕机内核的关键结构

crash的工作原理,是解析捕获内核/proc/vmcore导出的dump文件(这里先不考虑makedumpfile的压缩)。因此,需要分析捕获内核的/proc/vmcore的工作原理。作为桥梁,/proc/vmcore就是通过解析捕获内核里携带的宕机内核内存位置信息,来访问宕机内核的内存数据。

下面,先以/proc/vmcore的读取流程来看,如何获取宕机内核的数据。

/proc/vmcore的读取是靠read_vmcore函数,该函数入参offset,读指定长度的数据:

start = map_offset_to_paddr(*fpos, &vmcore_list, &curr_m);
read_from_oldmem(buffer, tsz, &start, 1);

其中,start变量可以看出,是物理地址。 也就是说,map_offset_to_paddr将偏移转换成了宕机内核里的物理地址。

再深入map_offset_to_paddr就可以知道,关键转换就是:遍历vmcore_list里的vmcore成员,

找到包含一个vmcore成员,满足vmcore.offset<offset<(vmcore.offset+size),

再返回(vmcore.paddr+offset-vmcore.offset)得到物理地址。

最后调用read_from_oldmem从物理地址读出数据。也即,捕获内核的vmcore.paddr和vmcore.offset有一一对应关系,可以互相反推。

因此,我们需要看,vmcore.offset和vmcore.paddr是怎么赋值的?

2.2 vmcore_list的生成

vmcore_list的生成,是捕获内核启动时,通过:

parse_crash_elf_headers->parse_crash_elf64_headers来生成的:

static int __init parse_crash_elf64_headers(void)
{
        /*  将PT_NOTE段信息加入vmcore_list  */
        rc = merge_note_headers_elf64(elfcorebuf, &elfcorebuf_sz, &vmcore_list);
        /* 将PT_LOAD段信息加入vmcore_list */
	rc = process_ptload_program_headers_elf64(elfcorebuf, elfcorebuf_sz,
							&vmcore_list);
        /*调整vmcore_list的每个成员的偏移*/
        set_vmcore_list_offsets_elf64(elfcorebuf, &vmcore_list);
}

可以看出,先根据宕机内核传递过来的elfcore地址,进行elf解析,将PT_NOTE和PT_LOAD

段的头信息,以struct vmcore的形式加入到vmcore_list链表。 而vmcore信息代表了宕机内核的内存布局,该结构有三个重要的成员,paddr,size,以及offset。因为用户态想要达到的目标是,根据/proc/vmcore导出的文件,只要给出一个文件内的offset,就可以得到该offset对应宕机内核的哪个物理地址paddr,size则说明,每个vmcore代表了一段宕机内核的内存区间。

     再回到前面的parse_crash_elf64_headers函数,逐句分析。

merge_note_headers_elf64:

处理PT_NOTE段。尝试把所有note段合并成一个段(每个cpu有一个note属性的programmhead,这些programm head挨个存放,每个programm指向连续的多个note。其实连续的只有两个note,第一个note是实际负载,由crash_save_cpu保存的寄存器现场等,第二个note是空的,表示结束)。

static int __init merge_note_headers_elf64(char *elfptr, size_t *elfsz,
						struct list_head *vc_list)
{
        int i, nr_ptnote=0;
        ehdr_ptr = (Elf64_Ehdr *)elfptr;
	phdr_ptr = (Elf64_Phdr*)(elfptr + sizeof(Elf64_Ehdr));
        //遍历所有的programm header,找到PT_NOTE programe header
        for (i = 0; i < ehdr_ptr->e_phnum; i++, phdr_ptr++) {
            if (phdr_ptr->p_type != PT_NOTE)
			continue;
                //遍历该programm header里所有连续存放的note_load负载
                //每个note_load段负载的组成为,一个Elf64_Nhdr,后面接实际note数据
                //有多个连续的note_load组成一个programm header
                for (j = 0; j < max_sz; j += sz) {
			if (nhdr_ptr->n_namesz == 0)
				break;
			sz = sizeof(Elf64_Nhdr) +
				((nhdr_ptr->n_namesz + 3) & ~3) +
				((nhdr_ptr->n_descsz + 3) & ~3);
			real_sz += sz;
			nhdr_ptr = (Elf64_Nhdr*)((char*)nhdr_ptr + sz);
		}
      new->paddr = phdr_ptr->p_offset;
                new->size = real_sz;
                //每找到一个pt_note属性的programme header,
                //就把这个programm header指向的连续note_load段用一个vmcore表示,
                //并添加到vmcore链表中
                //size为该programe header里所有note的大小
                list_add_tail(&new->list, vc_list);
                phdr_sz += real_sz;
        }
        //新建立一个PT_NOTE属性的programm header,将所有的NOTE属性programm header都归一到此
        phdr.p_type    = PT_NOTE;
	phdr.p_flags   = 0;
        //第一个内核最先传递过来的elf信息,结构如下:
        //[elf_header][programm_head_note_cpu0]...[programm_head_note_cpum]
        //[programm_head_note_load0]..[programm_head_note_loadn]
        //(可能还有其他段)[note_0_a]...[note_0_x](可能还有其他段)[note_m_a]...[note_m_x][load_0]  ...       [load_n]
        //note段归并后,只剩一个programm_head_note_all,即删除了从programm_head_note_cpu0            //到programm_head_note_cpux
        //因此第一个note段note_0_a的偏移要减少nr_ptnote-1个program head长度
	note_off = sizeof(Elf64_Ehdr) +
			(ehdr_ptr->e_phnum - nr_ptnote +1) * sizeof(Elf64_Phdr);
        //原elf.p_offset段的值是物理地址,这里改成了文件内偏移
	phdr.p_offset  = note_off;  //notice: 1
	phdr.p_vaddr   = phdr.p_paddr = 0;
        //更新大小
	phdr.p_filesz  = phdr.p_memsz = phdr_sz;
	phdr.p_align   = 0;
        //将第一个programm_head_note_cpu0替换为新note段的值
	/* Add merged PT_NOTE program header*/
	tmp = elfptr + sizeof(Elf64_Ehdr);
	memcpy(tmp, &phdr, sizeof(phdr));
	tmp += sizeof(phdr);
        //整体前移nr_ptnote - 1个programm_head
	/* Remove unwanted PT_NOTE program headers. */
	i = (nr_ptnote - 1) * sizeof(Elf64_Phdr);
	*elfsz = *elfsz - i;
        //要往前挪动的长度是原长度减去一个elf head和一个note属性的programm_head长度。
	memmove(tmp, tmp+i, ((*elfsz)-sizeof(Elf64_Ehdr)-sizeof(Elf64_Phdr)));

	/* Modify e_phnum to reflect merged headers. */
	ehdr_ptr->e_phnum = ehdr_ptr->e_phnum - nr_ptnote + 1;
}


结论:merge_note_headers_elf64执行完毕,elf段的note段被合并为一个,并且更新note head的p_offset字段为合并后的note段文件偏移。

为什么要合并note段呢?因为之前添加的vmcore成员,offset都是挨着的,

也就是说,当read /proc/vmcore>dump之后,之前的note段在dump文件里都会挨着,

那么就需要一个新的note段来指示,这就是新note段的来历。

process_ptload_program_headers_elf64:
处理 PT_LOAD 段:
//LOAD段的起始偏移
vmcore_off = sizeof(Elf64_Ehdr) +
			(ehdr_ptr->e_phnum) * sizeof(Elf64_Phdr) +
			phdr_ptr->p_memsz; /* Note sections */

for (i = 0; i < ehdr_ptr->e_phnum; i++, phdr_ptr++) {
            if (phdr_ptr->p_type != PT_LOAD)
			continue;
		new->paddr = phdr_ptr->p_offset;
                new->size = phdr_ptr->p_memsz; //PT_LOAD
		list_add_tail(&new->list, vc_list);
        
                //最重要的是,这里会把elf各段的p_offset字段更新为文件内偏移
                /* Update the program header offset. */
		phdr_ptr->p_offset = vmcore_off; 
		vmcore_off = vmcore_off + phdr_ptr->p_memsz;
}
结论:vmcore.paddr = elf.p_offset
            然后elf.p_offset被改成了文件偏移。
set_vmcore_list_offsets_elf64:

设定vmcore.offset的赋值,也就是各个段在文件中的偏移。这个是人为来设定的值。根据每个段对应区间占用的内存(注意,不是段头大小)来算的:

	/* Skip Elf header and program headers. */
	vmcore_off = sizeof(Elf64_Ehdr) +
			(ehdr_ptr->e_phnum) * sizeof(Elf64_Phdr);

	list_for_each_entry(m, vc_list, list) {
		m->offset = vmcore_off;
		vmcore_off += m->size;
	}

从上面代码可以看出,谁先加入vmcore链表,谁的offset就在前面,我们可以从前几步看出,先加的是每cpunote段,再是各个load段。

为便于理解,再补充一下宕机内核传递过来的elf信息。elf信息是vmcore_list的核心,正确的理解kdump elf格式是理解/proc/vmcore的关键。

网上有一篇文章对用户态coredump的格式解析,写的很好,coredump格式与kdump格式差不多,这里将其部分分析摘抄如下,以备参考:

Core文件的整体布局如下图所示,它与普通ELF文件的差别是多了一个特定的PT_NOTE类型的段,用于存放线程信息和寄存器信息。





下图是是一个在mips平台上生成的Coredump文件的头部的十六进制数据。

crash工具源码分析_第1张图片

文件开始是一个ELF Header,该头的结构定义为:

typedef struct elf32_hdr{
  unsigned char e_ident[EI_NIDENT];    // 16字节标识
  Elf32_Half e_type;
  Elf32_Half e_machine;
  Elf32_Word e_version;
  Elf32_Addr e_entry;
  Elf32_Off e_phoff;    // Program Headers的文件偏移地址
  Elf32_Off e_shoff;
  Elf32_Word e_flags;
  Elf32_Half e_ehsize;    // ELF Header头结构的大小
  Elf32_Half e_phentsize;    // 每个Program Header信息描述占用的大小
  Elf32_Half e_phnum;    // Program Header的数量
  Elf32_Half e_shentsize;
  Elf32_Half e_shnum;
  Elf32_Half e_shstrndx;
} Elf32_Ehdr;
根据该结构,解析Coredump文件的头信息如下图所示。
crash工具源码分析_第2张图片

e_type为0x04(ET_CORE),表示这是一个core文件;e_phoff为0x0034,

表示Program Headers信息从文件的0x34地址开始;

e_ehsize为0x34,表示此ELF Header文件头占用的字节为0x34;

e_phentsize为0x20(32字节),表示每个Program Header占用的大小为32字节;

e_phnum为0x23,表示这个core文件共含有35个segment段;

根据e_phoff信息,Program Headers从0x34字节开始, 即紧跟在ELF Header之后开始。

再来分析Program Headers信息,根据ELF Header,

每个Program Header占据的字节数为32字节,说明这是一个32位的Elf32_Phdr

(对应的还有64位的Elf64_Phdr,其size要大一些),Elf32_Phdr的定义如下:

typedef struct elf32_phdr{
  Elf32_Word p_type;    // segment的类型
  Elf32_Off p_offset;    // segment数据在文件中的偏移
  Elf32_Addr p_vaddr;    // segment加载到内存中的虚拟地址
  Elf32_Addr p_paddr;
  Elf32_Word p_filesz;    // segment数据在文件中的数据大小
  Elf32_Word p_memsz;    // segment数据在内存中占据的大小
  Elf32_Word p_flags;    // segment的属性标志
  Elf32_Word p_align;
} Elf32_Phdr;
根据这个定义,先分析第一个Program Header的信息,如下图所示。
crash工具源码分析_第3张图片

第1个Segment的p_type为0x04(PT_NOTE),表示这是一个描述note信息的段;

p_offset为0x494,表示note信息段从文件的0x494字节开始;

p_filesz为0x358,表示note信息共有856字节。

由于note信息是辅助信息段,在原程序中并不存在于内存中,所以其p_vaddr,p_memsz,p_flags等均为0。


紧接着的是第二个Program Header信息(从地址0x34+0x20=0x54开始):


第2个Segment的p_type为0x01(PT_LOAD),这是一个可加载的段;

p_offset为0x1000,表示该段从文件的0x1000字节开始;p_vaddr为0x400000,

表示该段在原程序的内存中的虚拟地址为0x400000

(对mips而言,此地址是主程序的加载地址);

p_filesz为0,表示该段在文件中的大小为0,

如前所述,由于该段是一个代码段,所以Core文件并没有保存其内容,因此该段在Core文件中的数据长度为0,

调试时需要原程序的ELF档加载到0x400000地址才能分析此程序;

p_memsz为0x10000,表示该段在原内存中占据4096的大小(即该段占据0x400000~0x40FFFF的地址空间);

p_flags为0x05,即PF_R | PF_X,表示该段是一个只读可执行的代码段。

2.3 kdump的elf格式

首先我们要搞清楚一个问题,宕机内核传递给捕获内核的elf头是谁提供的?

起初,我怀疑是直接根据宕机内核vmlinux的elf头信息得到的。下面是一个vmlinux的elf信息:

[root@localhost kernel_imx]# readelf -l vmlinux

Elf file type is EXEC (Executable file)
Entry point 0x1000000
There are 5 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000200000 0xffffffff81000000 0x0000000001000000
                 0x00000000008a7000 0x00000000008a7000  R E    200000
  LOAD           0x0000000000c00000 0xffffffff81a00000 0x0000000001a00000
                 0x0000000000130000 0x0000000000130000  RW     200000
  LOAD           0x0000000000e00000 0x0000000000000000 0x0000000001b30000
                 0x0000000000013a40 0x0000000000013a40  RW     200000
  LOAD           0x0000000000f44000 0xffffffff81b44000 0x0000000001b44000
                 0x00000000001ba000 0x000000000043b000  RWE    200000
  NOTE           0x0000000000785758 0xffffffff81585758 0x0000000001585758
                 0x0000000000000024 0x0000000000000024         4

其中offset的值,很明显不是对应的内存物理地址。那这到底是怎么回事?

很多内核镜像都是被strip掉elf头信息后再加载的,所以捕获内核得到的elf头信息,

一定还有其他别的什么来历。答案是kexec-tool用户态工具生成的。这个是关键!

生成elf信息的代码在kexec/crashdump-elf.c文件的FUNC函数。

我们看看这个起着联系第一个内核和第二个内核纽带的函数,是如何生成elf信息的,将该函数裁剪如下:

   nr_cpus = sysconf(_SC_NPROCESSORS_CONF);
	sz = sizeof(EHDR) + (nr_cpus + has_vmcoreinfo) * sizeof(PHDR) +
	     ranges * sizeof(PHDR);
        bufp = xmalloc(sz);
	memset(bufp, 0, sz);
        /* Setup ELF Header*/
	elf = (EHDR *) bufp;
        先来构造PT_NOTE段,一个cpu的信息就是一个note段
        /* PT_NOTE program headers. One per cpu */
        for (i = 0; i < nr_cpus; i++) {
                ///sys/devices/system/cpu/cpu%d/crash_notes
                get_note_info(i, ¬es_addr, ¬es_len);
                phdr = (PHDR *) bufp;
		bufp += sizeof(PHDR);
		phdr->p_type	= PT_NOTE;
		phdr->p_offset  = phdr->p_paddr = notes_addr;
		phdr->p_filesz	= phdr->p_memsz	= notes_len;
        }

上面的代码里详细解释了elf文件的生成过程。可以看出,elf文件主要由PT_NOTE和PT_LOAD段组成。

先来看PT_NOTE段的作用。捕获内核如何获取宕机内核死机时的寄存器现场?

答案是,在内核crash dump时,会把死机信息存放到PT_NOTE段里。

这样捕获内核起来后,解析PT_NOTE段,就能根据物理地址找到宕机内核存放死机现场的寄存器了。

kexec tool在上面的代码里相当于预留了一个elf段,里面填的是宕机内核存放现场数据crash_notes的地址

(用sys/devices/system/cpu/cpu%d/crash_notes来保存),以及crash_notes的长度。

那么宕机内核会往crash_data填写什么值呢?代码在​crash_save_cpu:

void crash_save_cpu(struct pt_regs *regs, int cpu)
{
        struct elf_prstatus prstatus;
	u32 *buf;
        buf = (u32*)per_cpu_ptr(crash_notes, cpu);

        //*(struct pt_regs *)&prstatus.pr_reg= *regs;
	elf_core_copy_kernel_regs(&prstatus.pr_reg, regs);

        //memcpy(buf,&prstatus,sizeof(prstatus));,拷贝到crash_notes
        //buf+=sizeof(prstatus);后移指针
	buf = append_elf_note(buf, KEXEC_CORE_NOTE_NAME, NT_PRSTATUS,
		      	      &prstatus, sizeof(prstatus));

        //添加一个空长度的note(name长度为0,n_descsz长度为0,n_type为0),作为结束标志
	final_note(buf);
}

这一步执行完后,elf头信息里就加了PT_NOTE段的内容。

再来看PT_LOAD段的生成。

代码数据段:

 if (elf_info->kern_size && !xen_present()) {
		phdr = (PHDR *) bufp;
		bufp += sizeof(PHDR);
		phdr->p_type	= PT_LOAD;
		phdr->p_flags	= PF_R|PF_W|PF_X;
		phdr->p_offset	= phdr->p_paddr = elf_info->kern_paddr_start;
		phdr->p_vaddr	= elf_info->kern_vaddr_start;
		phdr->p_filesz	= phdr->p_memsz	= elf_info->kern_size;
		phdr->p_align	= 0;
		(elf->e_phnum)++;
	}

接下来就是重要的各个内存段了,这主要是靠kexec-tool解析/proc/iomem里的System RAM:

 for (i = 0; i < ranges; i++, range++) {
		unsigned long long mstart, mend;
		if (range->type != RANGE_RAM)
			continue;
		mstart = range->start;
		mend = range->end;
		if (!mstart && !mend)
			continue;
		phdr = (PHDR *) bufp;
		bufp += sizeof(PHDR);
		phdr->p_type	= PT_LOAD;
		phdr->p_offset	= mstart;
                phdr->p_paddr = mstart;
                phdr->p_vaddr = phys_to_virt(elf_info, mstart);
		if (mstart == info->backup_src_start
		    && (mend - mstart + 1) == info->backup_src_size)
			phdr->p_offset	= info->backup_start;
、            phdr->p_filesz	= phdr->p_memsz	= mend - mstart + 1;
		(elf->e_phnum)++;
	}

最后得到的elf的头如下(注意,这是elf头,还不包括真正的note段数据、load段数据。

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  NOTE           0x0000000000000190 0x0000000000000000 0x0000000000000000
                 0x0000000000000d14 0x0000000000000d14         0
  LOAD           0x0000000000000ea4 0xffffffffc0100000 0x0000000000100000
                 0x000000000452b1ef 0x000000000452b1ef  RWE    0
  LOAD           0x000000000452c093 0xc000000000000000 0x0000000000000000
                 0x0000000000100000 0x0000000000100000  RWE    0
  LOAD           0x000000000462c093 0xc000000000100000 0x0000000000100000
                 0x0000000007f00000 0x0000000007f00000  RWE    0
  LOAD           0x000000000c52c093 0xc00000000c000000 0x000000000c000000
                 0x0000000003effe00 0x0000000003effe00  RWE    0
  LOAD           0x000000001042be93 0xc000000020000000 0x0000000020000000
                 0x000000009ffffe00 0x000000009ffffe00  RWE    0

总结:

1)宕机传递给捕获内核的elf信息,包含note段、load段,每个段是一个elf结构,其p_offset为物理地址,p_memsz为该段大小,p_paddr为物理地址;

2)在捕获内核初始化执行vmcore_init->parse_crash_elf_headers之后,捕获内核的内存里,

保存了一个vmcore_list链表,每个链表成员是vmcore。vmcore根据第1步的结果,保存了偏移地址对应物理地址的关系。

也就是说,给出偏移,可以得到物理地址。并且,第一个内核传递给第二个内核的elf load属性段,

其p_offset被改为了本段在文件内的偏移,但p_paddr保持不变(note段没有必要保留p_addr);

3)cat/proc/vmcore将数据导出到一个二进制文件里,包括第一个内核传递的elf头信息也被导出;

4)crash解析二进制文件,根据elf信息里的偏移p_offset和物理地址p_paddr的关系,

根据内核虚拟地址得到物理地址,再得对应文件偏移。

再根据偏移读出文件数据,也就是原宕机内核的物理内存数据。

2.4 crash关键流程分析

好,走到这里,才算是真正进入crash源码分析了。读者是不是已经没耐心了?

请再坚持一会,现在打开crash-7.0.7的源码工程。

本节,我们主要考察crash的两个重要接口的实现,第一个是readmem函数,

第二个是bt回溯。

2.4.1 readmem


先来看readmem的原型:

int
readmem(ulonglong addr, int memtype, void *buffer, long size,
	char *type, ulong error_handle)
该函数从指定addr开始的地址,读取size长度的数据,存放到buffer中。其中,type

指定addr是虚拟地址还是物理地址。例如:

buf =(char*)malloc(1024);
readmem(C000000018000000,buf,1024,KVADDR,NULL)
从内核虚拟地址0xc000000018000000开始读取1024直接到buf中。

这个是如何做到的,在2.1节我们已经简要介绍过。下面详细分析。
这个读取的流程,正好和vmcore文件被导出的过程互逆。
现在我们只有一个vmcore的导出文件,已知的是用户给的内核态虚拟地址,需要看这个内核虚拟地址,对应vmcore里的哪个位置的内容。
首先,内核态虚拟地址,可以通过kvtop得到其在第一个内核里的物理地址;接着,物理地址
和文件的偏移,可以在vmcore_list里找到对应关系。 由于vmcore_list是内核里才能访问的,那么
crash这个用户态工具,有没有保存一份vmcore_list的拷贝呢?

crash里对应的这个结构,叫做vmcore_data。我们来看看这个vmcore_data是如何生成的。
首先,cat /proc/vmcore的时候,调用的是read_vmcore,该函数的最开头,如果发现要读的文件偏移,

小于一个elf头的长度,则会把头一个内核提供的elf信息,copy_to_user返回给用户。代码注释写的很清楚:

 /* Read ELF core header */
	if (*fpos < elfcorebuf_sz) {
		tsz = elfcorebuf_sz - *fpos;
		if (buflen < tsz)
			tsz = buflen;
		if (copy_to_user(buffer, elfcorebuf + *fpos, tsz))
			return -EFAULT;
		buflen -= tsz;
		*fpos += tsz;
		buffer += tsz;
		acc += tsz;
	}
crash会根据dump文件里的elf信息,在内存里生成vmcore_data链表,

这个和内核里生成vmcore_list的过程基本一致。

vmcore_data初始化好后,crash就可以根据vmcore_data读一个内核态虚拟地址内容。

函数流程简述如下:

readmem(sp->value, KVADDR, local,
                        size, symbol, FAULT_ON_ERROR);
kvtop(CURRENT_CONTEXT(), addr, &paddr, 0);
READMEM(fd, bufptr, cnt, 
		    (memtype == PHYSADDR) || (memtype == XENMACHADDR) ? 0 : addr, paddr)
#define READMEM  pc->readmem
read_kdump->read_netdump:

		for (i = offset = 0; i < nd->num_pt_load_segments; i++) {
			pls = &nd->pt_load_segments[i];
			if ((paddr >= pls->phys_start) &&
			    (paddr < pls->phys_end)) {
				offset = (off_t)(paddr - pls->phys_start) +
					pls->file_offset;
				break;
			}
		}
                lseek(nd->ndfd, offset, SEEK_SET) 
                read(nd->ndfd, bufptr, cnt);
可以看出,最关键的流程就是根据vmcore_data->pt_load_segments里的file_offset和
phys_start对应关系,把物理地址转换为文件偏移。
那vmcore_data是如何生成的呢?我们在代码里搜索vmcore_data,找到其生成的地方,is_netdump函数。

is_netdump就是将dump文件的elf信息归纳为vmcore_data的函数:

/*
 *  Determine whether a file is a netdump/diskdump/kdump creation, 
 *  and if TRUE, initialize the vmcore_data structure.
 */
int 
is_netdump(char *file, ulong source_query) 
{
fd = open(file, O_RDWR);
size = MIN_NETDUMP_ELF_HEADER_SIZE;
read(fd, eheader, size) ; //读取elf头

elf64 = (Elf64_Ehdr *)&eheader[0];//64位的elf头
size = (size_t)load64->p_offset;
tmp_elf_header = (char *)malloc(size)) ;
lseek(fd, 0, SEEK_SET) ;
read(fd, tmp_elf_header, size);

	nd->ndfd = fd;
	nd->elf_header = tmp_elf_header;
	nd->flags = tmp_flags;
	nd->flags |= source_query;
           nd->header_size = load64->p_offset;
                nd->elf64 = (Elf64_Ehdr *)&nd->elf_header[0];
		nd->num_pt_load_segments = nd->elf64->e_phnum - 1;
                //关键的段信息,包含offset与physical address的映射关系
               nd->pt_load_segments = (struct pt_load_segment *)
                    malloc(sizeof(struct pt_load_segment) *
                    nd->num_pt_load_segments)) ;
                 ////越过elf头,就是note head段
                nd->notes64 = (Elf64_Phdr *)
                    &nd->elf_header[sizeof(Elf64_Ehdr)];
                //越过elf头,越过note head段(经第二个内核启动时归并,只剩一个),跳到load head段,
                //load64指针类型: Elf64_Phdr *load64;
                nd->load64 = (Elf64_Phdr *)
                    &nd->elf_header[sizeof(Elf64_Ehdr)+sizeof(Elf64_Phdr)];
		
			nd->page_size = (uint)nd->load64->p_align;
                dump_Elf64_Ehdr(nd->elf64);
                dump_Elf64_Phdr(nd->notes64, ELFREAD);
		for (i = 0; i < nd->num_pt_load_segments; i++)
                        //解析每个段的信息
                	dump_Elf64_Phdr(nd->load64 + i, ELFSTORE+i);
                offset64 = nd->notes64->p_offset;//在kexec里这个值曾经是 crash_note的物理地址,
                //经第二个内核一折腾,就改成了dump文件的偏移
                for (tot = 0; tot < nd->notes64->p_filesz; tot += len) {
                        //直接跳到note段,依次解析之后每个字段nhdr的内容
                        if (!(len = dump_Elf64_Nhdr(offset64, ELFSTORE)))
				break;
                        offset64 += len;
                }

}
其中,dump_Elf64_Phdr就是把PT_LOAD添加到vmcore_data->pt_load_segments段:

static void 
dump_Elf64_Phdr(Elf64_Phdr *prog, int store_pt_load_data)
{
    pls = &nd->pt_load_segments[store_pt_load_data-1];
    pls->file_offset = prog->p_offset;
    pls->phys_start = prog->p_paddr; 
}
dump_Elf64_Nhdr是为了获取PT_NOTE段数据,这些数据主要是宕机内核的死机寄存器现场:

static size_t 
dump_Elf64_Nhdr(Elf64_Off offset, int store)
{
note = (Elf64_Nhdr *)((char *)nd->elf64 + offset);
switch (note->n_type)
	{
	case NT_PRSTATUS:
		netdump_print("(NT_PRSTATUS)\n");
		if (store) {
			if (!nd->nt_prstatus)
				nd->nt_prstatus = (void *)note;
			for (i = 0; i < NR_CPUS; i++) {
				if (!nd->nt_prstatus_percpu[i]) {
					nd->nt_prstatus_percpu[i] = (void *)note;
					nd->num_prstatus_notes++;
					break;
				}
			}
		}
        //将本次解析到的note段长度返回(包含一个nhdr头和开头的名字长度加实际数据负载即elf_prstatus长度)
  	len = sizeof(Elf64_Nhdr);
  	len = roundup(len + note->n_namesz, 4);
  	len = roundup(len + note->n_descsz, 4);

	return len;
}

2.4.2 bt回溯

在crash界面敲bt的时候,发生了什么。

void
cmd_bt(void)
{
        //执行bt不带任何参数,就走蓝字部分
        if (!args[optind]) {
		if (CURRENT_PID() && (bt->flags & BT_THREAD_GROUP)) {
			tgid = task_tgid(CURRENT_TASK());
			DO_THREAD_GROUP_BACKTRACE();
		} else {
			tc = CURRENT_CONTEXT(); //根据tt->current获取进程描述符
			DO_TASK_BACKTRACE();
		}
		return;
	}
}

#define CURRENT_CONTEXT() (tt->current)

#define DO_TASK_BACKTRACE() 					\
	{							\
	BT_SETUP(tc);						\
	if (!BT_REFERENCE_CHECK(bt))				\
		print_task_header(fp, tc, subsequent++);	\
	back_trace(bt);						\
	}

#define BT_SETUP(TC)                                          \
	clone_bt_info(&bt_setup, bt, (TC));         	      

void
clone_bt_info(struct bt_info *orig, struct bt_info *new,
	      struct task_context *tc)
{
	BCOPY(orig, new, sizeof(*new));
	new->stackbuf = NULL;
	new->tc = tc;
	new->task = tc->task;
	new->stackbase = GET_STACKBASE(tc->task);
	new->stacktop = GET_STACKTOP(tc->task);
}

上面的clone_bt_info准备好待回溯的栈信息后,就调用back_trace准备回溯了。

在分析crash的回溯算法之前,先看回溯的堆栈现场是如何准备出来的。

首先,back_trace,需要根据宕机内核发生异常时的那个进程,来获取当时的内核栈指针。

内核异常进程,就是由宏CURRENT_CONTEXT来指示,即,tt->current决定了是在哪个cpu回溯,那么这个tt是哪里赋值的呢?

答案是在crash初始化的task_init函数。tt是一个task 链表, 由task_init初始化。
struct task_table task_table = { 0 };
struct task_table *tt = &task_table;
void
task_init(void)
{
	please_wait("determining panic task");
	set_context(get_panic_context(), NO_PID);
}
//在get_panic_context中获取死机的那个线程
//接着以此线程地址为key,搜索tc(进程链表),
//找到对应的tc,并返回。
//所以重点在怎么找到死机的线程get_panic_context()
int
set_context(ulong task, ulong pid)
{
	int i;
	struct task_context *tc;
	int found;

	tc = FIRST_CONTEXT();

        for (i = 0, found = FALSE; i < RUNNING_TASKS(); i++, tc++) {
		if (task && (tc->task == task)) {
			found = TRUE;
			break;
		} else if (pid == tc->pid) {
			found = TRUE;
			break;
		}
        }

	if (found) {
		CURRENT_CONTEXT() = tc;
		return TRUE;
	} else {
		return FALSE;
	}

static ulong
get_panic_context(void)
{
    //首先非R状态的不关心,因为非R状态的肯定不是发生非法地址访问的进程
    //对所有的当前运行进程检查一下,如果是运行状态,但又不在hash_pid,则告警。
        for (i = 0; i < NR_CPUS; i++) {
                if (!(task = tt->active_set[i]))
			continue;
                if (!task_exists(task)) {
			error(WARNING);
                }
        if ((tc = panic_search())) {
		tt->panic_processor = tc->processor;
		return(tt->panic_task = tc->task);
	}
}
虽然我们很不想讨论panic_search,因为它很大,但不得不面对它。
我们先来想一下,如果是你来写这个函数,找出上个内核发生死机的
那个进程,你会怎么写?
一种方案是,首先根据dump文件的特殊性,来查找crash cpu,
再根据crash cpu获取运行的进程tt->active_set[crash cpu];
还有就是,找到crash cpu后,取得nhdr结构,得到死机时的sp,这个sp
就是内核栈,内核栈与thread_info是重合的,因此可以得到thread_info结构,
再得到task struct结构。

ulong 
get_netdump_panic_task(void)
{
                int crashing_cpu=-1;
                if (kernel_symbol_exists("crashing_cpu")) {
			get_symbol_data("crashing_cpu", sizeof(int), &i);
			crashing_cpu = i;
		}
                note64 = (Elf64_Nhdr *)
					nd->nt_prstatus_percpu[crashing_cpu];

                //ppc64走方案2
                if (nd->elf64->e_machine == EM_PPC64) {
			
			esp = *(ulong *)((char *)user_regs + 8);
			if (IS_KVADDR(esp)) {
				task = stkptr_to_task(esp);
				for (i = 0; task && (i < NR_CPUS); i++) {
					if (task == tt->active_set[i]) 
						return task;
				}
			}
		
		
                //x86走方案1
                if (nd->elf64->e_machine == EM_X86_64) {
			if ((crashing_cpu != -1) && (crashing_cpu <= kt->cpus))
				return (tt->active_set[crashing_cpu]);
		}
                //mips64走方案1
                if (nd->elf64->e_machine == EM_MIPS) {
			if ((crashing_cpu != -1) && (crashing_cpu <= kt->cpus))
				return (tt->active_set[crashing_cpu]);
		}
}

得到异常进程的栈后,回溯的核心是unwind_stack,这个是从内核的对应函数抄过来的。

不过,由于内核的unwind_stack可以直接访问内核虚拟地址,crash还需要对unwind_stack做少许修改,

用readmem来访问内核虚拟地址。对应mips的实现是mips64_unwind_stack。具体的函数这里就不贴了,

仅仅简要说明一下mips的堆栈回溯原理。mips的回溯,核心思想就是一层一层的找调用函数的ra,

 叶子函数的ra就是当前寄存器,非叶子函数的ra保存在函数栈里,下面用这个简单例子来作为本文的结尾:

func3()  
{  
}  
func2()  
{  
func3();  
}  
func1()  
{  
func2();  
}  
main()  
{  
 func3();  
}  
</pre><pre code_snippet_id="486572" snippet_file_name="blog_20141017_52_3660158" name="code" class="cpp">func3:                 //叶子函数,所以ra一直没有变化过,返回时可以jr ra来返回  
addi sp,sp,-24         //于是可以通过寄存器里的ra值得到上一级pc  
xxx  
jr ra  
  
func2:  
addi sp,sp,-52         //非叶子函数,如果要找到ra,就需要根据sp里的值来确认  
sw   ra,48(sp)         //根据当前sp的值,加上52是栈帧,接着减去(52-48),就得到ra保存的位置,实际上就是sp+48,取出ra得到func1里的某句地址  
jal  func3             //即上一层pc,jal会把pc的下一条指令放到ra里,再进行跳转,相对于jr直接跳转  
nop  
  
func1:                 //同上  
addi sp,sp,-48  
sw   ra,44(sp)  
jal  func2  
nop  







你可能感兴趣的:(Crash,kdump)