kexecの実装を調べる(ARM編)

2014/03/26linux::ARMimport

kexecを追う

ユーザランドでは、kexec-toolsを用いる。仕組みがわかっていれば、
システムコールを自前で叩いて準備も出来そう。

実際の処理はアーキテクチャ依存部にて行われる。
ここではARMについて調査をすすめる。

FILE: kernel/kexec.c

SYSCALL_DEFINE4(kexec_load, unsigned long, entry, unsigned long, nr_segments,
		struct kexec_segment __user *, segments, unsigned long, flags)
{
...
			result = kimage_normal_alloc(&image, entry,
							nr_segments, segments);
...
		result = machine_kexec_prepare(image);
...
		for (i = 0; i < nr_segments; i++) {
			result = kimage_load_segment(image, &image->segment[i]);
...
		kimage_terminate(image);
kernel imageやinitrdのイメージを物理ページへ転送して起動準備。

load image用メモリ確保(kernel/kexec.c)

static int kimage_normal_alloc(struct kimage **rimage, unsigned long entry,
				unsigned long nr_segments,
				struct kexec_segment __user *segments)
{
...
	image->control_code_page = kimage_alloc_control_pages(image,
					   get_order(KEXEC_CONTROL_PAGE_SIZE));

image->control_code_page には、連続した4096page=16MBの物理連続空間が割り当てられる。
以下の順序で関数が呼ばれて取得できる。この領域はreboot用につかう。
struct page *kimage_alloc_control_pages(struct kimage *image,
					 unsigned int order)
{
	struct page *pages = NULL;

	switch (image->type) {
	case KEXEC_TYPE_DEFAULT:
		pages = kimage_alloc_normal_control_pages(image, order);
		break;
DEFAULTとCRASH時の2種類あり。今はDEFAULTを追う。
static struct page *kimage_alloc_normal_control_pages(struct kimage *image,
							unsigned int order)
{

		pages = kimage_alloc_pages(GFP_KERNEL, order);
...
		if ((epfn >= (KEXEC_CONTROL_MEMORY_LIMIT >> PAGE_SHIFT)) ||
			      kimage_is_destination_range(image, addr, eaddr)) {
			list_add(&pages->lru, &extra_pages);
			pages = NULL;
		}
	} while (!pages);
...
	kimage_free_page_list(&extra_pages);
物理的に連続するページを確保して、転送先アドレスとの重複チェックを行う。
該当する場合は改めて取得を試みる。最後に未使用分は開放する。



範囲チェック(FILE: arch/arm/kernel/machine_kexec.c)

int machine_kexec_prepare(struct kimage *image)
...
memblock_is_region_memory(current_segment->mem,current_segment->memsz)
現在のkernelが把握している memory空間の有効範囲が指示されているかをチェックしているようだ。

ロード

static int kimage_load_segment(struct kimage *image,
				struct kexec_segment *segment)
{
...
	case KEXEC_TYPE_DEFAULT:
		result = kimage_load_normal_segment(image, segment);

static int kimage_load_normal_segment(struct kimage *image,
					 struct kexec_segment *segment)
{


FILE:kernel/reboot.c

SYSCALL_DEFINE4(reboot, int, magic1, int, magic2, unsigned int, cmd,
		void __user *, arg)
{
#ifdef CONFIG_KEXEC
	case LINUX_REBOOT_CMD_KEXEC:
		ret = kernel_kexec();
kexecシステムコールで保持したイメージデータは、
rebootシステムコールのKEXEC要求により発動する。


FILE: kernel/kexec.c

/*
 * Move into place and start executing a preloaded standalone
 * executable.  If nothing was preloaded return an error.
 */
int kernel_kexec(void)
{
	{
		kexec_in_progress = true;
		kernel_restart_prepare(NULL);
		migrate_to_reboot_cpu();
		printk(KERN_EMERG "Starting new kernel\n");
		machine_shutdown();
	}

	machine_kexec(kexec_image);

ドライバ・ユーザヘルパ?の終了をしつつ(kernel_restart_prepare(NULL))、
CPU0のみ稼働状態(ほかは停止へ遷移させる)
FILE: kernel/reboot.c
void kernel_restart_prepare(char *cmd)
{
	blocking_notifier_call_chain(&reboot_notifier_list, SYS_RESTART, cmd);
	system_state = SYSTEM_RESTART;
	usermodehelper_disable();
	device_shutdown();
}


FILE: arch/arm/kernel/machine_kexec.c
void machine_kexec(struct kimage *image)
{
...
	unsigned long reboot_entry = (unsigned long)relocate_new_kernel;
...
	/* copy our kernel relocation code to the control code page */
	reboot_entry = fncpy(reboot_code_buffer,
			     reboot_entry,
			     relocate_new_kernel_size);
★imageで渡されたロードイメージをページ単位で保存し、
 そのPFNをLookUpTableとして管理している。
 そのポインタを受け取って、指定された物理アドレスへイメージを
 転送するアセンブラコード本体を、コレでコピーする。
 コードはentry pointへのjumpも含まれている。
...
	printk(KERN_INFO "Bye!\n");

	if (kexec_reinit)
		kexec_reinit();

	soft_restart(reboot_entry_phys);
}

FILE: arch/arm/kernel/relocate_kernel.S
ENTRY(relocate_new_kernel)
...
	/* Jump to relocated kernel */
	mov lr,r1
	mov r0,#0
	ldr r1,kexec_mach_type
	ldr r2,kexec_boot_atags
 ARM(	mov pc, lr	)
 THUMB(	bx lr		)

	.align

	.globl kexec_start_address
kexec_start_address:
	.long	0x0

	.globl kexec_indirection_page
kexec_indirection_page:
	.long	0x0

	.globl kexec_mach_type
kexec_mach_type:
	.long	0x0

	/* phy addr of the atags for the new kernel */
	.globl kexec_boot_atags
kexec_boot_atags:
	.long	0x0

ENDPROC(relocate_new_kernel)

FILE: arch/arm/kernel/process.c
void soft_restart(unsigned long addr)
{
	u64 *stack = soft_restart_stack + ARRAY_SIZE(soft_restart_stack);
	/* Disable interrupts first */
...
	/* Disable the L2 if we're the last man standing. */
...
	/* Change to the new stack and continue with the reset. */
	call_with_stack(__soft_restart, (void *)addr, (void *)stack);
FILE: arch/arm//lib/call_with_stack.S
ENTRY(call_with_stack)
	str	sp, [r2, #-4]!
	str	lr, [r2, #-4]!

	mov	sp, r2
	mov	r2, r0
	mov	r0, r1

	adr	lr, BSYM(1f)
	mov	pc, r2

1:	ldr	lr, [sp]
	ldr	sp, [sp, #4]
	mov	pc, lr
ENDPROC(call_with_stack)
ARM-EABI仕様にしたがい、r0にLUTへのポインタ(PFN/dest-adr/end-mark/next-LUTの入った先頭ページ)、r1に関数アドレス、r2にスタック(static変数)が渡されてくる。
ここで使われるスタック領域が気がかりであり、旧kernelのstatic変数領域なのだが、
ロードイメージがその領域を侵食しない保証がないのではなかろうか。
control_iamgeの後半から使うとか、予め1枚用意しておくなどしたほうが良い気はする。

まとめ

以上より、以下のことが解った。
  • 新しいイメージの保存先は、転送先アドレスにぶつからないように作られている。
  • 渡されたイメージは、任意の物理アドレスにコピーされ、entryへのjumpも行われる。\→kernel imageに限らず任意の領域に任意のデータをおけそう。
  • 最終的にcpu0/割り込み全部禁止状態で、entryへ飛ぶ
  • reboot前の処理として、なにか作りこみたい場合は、以下の関数ポインタへ適切な関数へのアドレスをセットする。\設定方法は、mach-*のあたりで、直接代入するしかなさそうだ(mach-kirkwoodのみ該当@3.14-rc8)。
    void (*kexec_reinit)(void);
    
kexecに失敗するようならば、以下の観点が必要。
cpu0以外が生きている状態で切り替えようとしていないか。
通常のpower on resetと同じ状態にするのが望ましい。
これはペリフェラルに関しても同じことが言える。power managementを有効化して、
ドライバはpower save有効と指示されれば、ペリフェラルを極力寝かせる。
状態としても、ほぼPOR直後と同じ状態にしておくことが望ましい。
ソフト的な状態保持は影響を受けないので、
制御レジスタ・外部デバイスの設定値が効いてくるだろう。

もしくは、初期化処理で、既に初期化された状態でも正常に
動作するような実装であれば良い。