一、问题的引出
在Linux系统中,当内核发生panic的时候,我们可能希望能够保留内核的现场,就像当用户态程序异常的时候内核对应用程序的“吐核”一样(注意,不是吐槽)。但是应用程序的吐核是由内核来完成的,那么内核自己真正的吐自己该如何完成呢?
二、实现方法
这个实现是和kdump结合来实现的,这个实现本的思路就是在内核异常的时候用另一个内核来接班。这个新的内核来完成老内核的善终工作。正如长江后浪推前浪,前浪死在沙滩上一样。
但是老迈的内核依然盘踞在大家都需要用得位置,此时新的内核就没有办法使用原始的默认地址。当然新的内核也不能把自己的物理地址写死,这样不通用,看起来就太挫了。所以新的内核就要能够完成自己的重定位,也就是有随遇而安的能力。
三、386的实现
1、内核重定位表的生成
如果内核需要能够重定位,它就需要知道自己的重定位表,并且重定位表需要放在内核执行时需要知道的位置。因为内核加载入内存之后已经没有了ELF信息,所以这个位置就需要和内核真正使用的位置物理相邻,或者有一个结构来告诉内核自己到哪个地方找到自己需要完成的重定位。所以内核就需要在生成了自己的ELF格式的vmlinux之后,通过外部工具来生成这个内核所有的需要重定位的重定位项。这个其实和Windows的DLL和Linux下的so文件实现原理都是相同的,只是后者是在用户态由系统完成,这里需要内核自己独立完成而已。
在\linux-2.6.21\arch\i386\boot\compressed\Makefile
hostprogs-y := relocs
$(obj)/vmlinux: $(src)/vmlinux.lds $(obj)/head.o $(obj)/misc.o $(obj)/piggy.o FORCE
$(call if_changed,ld)
@:
$(obj)/vmlinux.bin: vmlinux FORCE 原始内核二进制格式文件生成
$(call if_changed,objcopy)
quiet_cmd_relocs = RELOCS $@ 这里根据vmlinux文件来生成这个文件中所有重定位项的表,内容重定向入make规则目标,对于重定位内核来说,这里为vmlinux.relocs文件。
cmd_relocs = $(obj)/relocs $< > $@;$(obj)/relocs --abs-relocs $<
$(obj)/vmlinux.relocs: vmlinux $(obj)/relocs FORCE 主机环境中relocs可执行文件的生成规则,由同目录下的relocs.c编译生成。
$(call if_changed,relocs)
vmlinux.bin.all-y := $(obj)/vmlinux.bin
vmlinux.bin.all-$(CONFIG_RELOCATABLE) += $(obj)/vmlinux.relocs如果使能了可重定位内核,则最终的vmlinux.bin.all中会添加vmlinux.relocs文件作为依赖。
quiet_cmd_relocbin = BUILD $@
cmd_relocbin = cat $(filter-out FORCE,$^) > $@ 这里承接vmlinux.bin.all的生成规则,也就是把它所有的依赖(除了为目标FORCE之外)全部写入同一个文件,这个文件名就是relocbin。从前面可以看到,由于vmlinux.bin是作为vmlinx.bin.all的第一个依赖,所以它在最终文件中也应该是考前的,而重定位表文件vmlinux.relocs文件则在内核文件之后,但是紧邻。
$(obj)/vmlinux.bin.all: $(vmlinux.bin.all-y) FORCE
$(call if_changed,relocbin)
linux-2.6.21\arch\i386\boot\compressed\relocs.c的实现比较简单,由于Makefile中没有指定--text选项,所以在emit_relocs函数中是通过二进制的格式逐个打印出来的。
2、内核加载时对重定位表位置的确定
在内核的连接脚本linux-2.6.21\arch\i386\boot\compressed\vmlinux.scr中,其中在链接脚本中定义了一个变量,这个变量也就是在运行时确定重定位表位置的一个变量。其定义为
SECTIONS
{
.data.compressed : {
input_len = .;压缩内核的开始逻辑地址,在可执行文件中,该位置放置了压缩之后内核文件的大小。。
LONG(input_data_end - input_data) input_data = .;
*(.data)
output_len = . - 4;这里设置了内核中代码段和数据段的长度,这个output_len表示压缩内核被解压缩之后所在的逻辑起始地址。
input_data_end = .; 压缩文件的结束地址,同样为逻辑地址。由于gzip文件的最后一个byte存放的是输入文件压缩前的大小,所以通过这个位置加上内核的装载位置可以知道压缩内核解压缩之后的大小。这个位置之所以重要,是由于对于可重定位的内核来说,这个位置向下是一组内核重定位表,下面将会用到这个说明。
}
}
内核默认配置大小linux-2.6.21\arch\i386\Kconfig
config PHYSICAL_START
hex "Physical address where the kernel is loaded" if (EMBEDDED || CRASH_DUMP)
default "0x100000"
然后看一下head.S中对这个重定位项的操作
.section ".text"
relocated:
/*
* Clear BSS
*/
xorl %eax,%eax eax寄存器清零。
leal _edata(%ebx),%edi 数据段结束开始
leal _end(%ebx), %ecx 到整个可执行文件结束,之间为BSS段。
subl %edi,%ecx BSS段大小放入ECX寄存器中,目标地址为edi,即从数据段结束开始,到可执行程序结束之间的所有内存清零,这里ebx保存的是程序在内存中的地址。
cld
rep
stosb
/*
* Setup the stack for the decompressor
*/
leal stack_end(%ebx), %esp
/*
* Do the decompression, and jump to the new kernel.. 这里进行内核的解压缩工作,这个解压缩之后的内存包含了内核和重定位表,并且重定位表紧邻着内核,在内核之后。
*/
movl output_len(%ebx), %eax
pushl %eax 参考前面的说明,这里将解压缩之后的vmlinux+vmlinux.relocs的结束位置放入EAX,然后压入堆栈,在执行完decompress_kernel之后,需要从堆栈中再取出这个值。。
pushl %ebp # output address
movl input_len(%ebx), %eax
pushl %eax # input_len
leal input_data(%ebx), %eax
pushl %eax # input_data
leal _end(%ebx), %eax
pushl %eax # end of the image as third argument
pushl %esi # real mode pointer as second arg
call decompress_kernel
addl $20, %esp
popl %ecx 将解压缩之后内核的结束地址放入 ECX寄存器中,在接下来重定位中将会遍历其中的所有重定位项,对内核中所有需要重定位的内容进行定位。
#if CONFIG_RELOCATABLE
/* Find the address of the relocations.
*/
movl %ebp, %edi
addl %ecx, %edi
/* Calculate the delta between where vmlinux was compiled to run
* and where it was actually loaded.
*/
movl %ebp, %ebx
subl $LOAD_PHYSICAL_ADDR, %ebx
jz 2f /* Nothing to be done if loaded at compiled addr. */
/*
* Process relocations.
*/
1: subl $4, %edi
movl 0(%edi), %ecx
testl %ecx, %ecx 重定位表最后以一个零结束,如果遇到零,则重定位完成。
jz 2f
addl %ebx, -__PAGE_OFFSET(%ebx, %ecx)将装载位置和编译位置的差值EBX添加到内核中所有需要重定位的位置,可能包括代码段和数据段。
jmp 1b
2:
#endif
/*
* Jump to the decompressed kernel.
*/
xorl %ebx,%ebx
jmp *%ebp 这里跳转到真正的内核起始地址,开始解压缩之后内核的执行。
3、新内核保留位置
static int __init parse_crashkernel(char *arg)
{
unsigned long size, base;
size = memparse(arg, &arg);
if (*arg == '@') {
base = memparse(arg+1, &arg);
/* FIXME: Do I want a sanity check
* to validate the memory range?
*/
crashk_res.start = base;
crashk_res.end = base + size - 1;
}
return 0;
}
early_param("crashkernel", parse_crashkernel);
也就是在内核启动的时候通过
crashkernel=size@addr的形式保留新内核即将使用的地址。
4、使用内存的保留
在linux-2.6.21\arch\i386\kernel\e820.c中
#ifdef CONFIG_KEXEC
request_resource(res, &crashk_res);
#endif
此处提前为新内核保留了指定的内存起始地址和大小。