LinuxのARMv7でのMMU faultを追いかける

2014/12/21 linux::ARM

Linux(ARMv7系)のメモリ管理を追ってみる

Linux Advent Calendar 2014の21日目の記事です。

kernel sourceの追いかけかた

前回のcopy_to_user()のfault処理実装を見たときに、__do_kernel_fault()あたりから先はちらっと見たので、そもそもメモリ管理全般はどうなっているのだろう、と気になって仕方が無くなる。
とりあえず下から覗いていくのが低レイヤー民(そんなものはない)のたしなみだろう、ということで、最近メジャーなARMv7アーキテクチャをターゲットとして調査する。

kernelのメモリ管理の基本と、ARMアーキテクチャでの実装を確認し、faultハンドラを追いかけていこう。

追いかけ方は人それぞれかもしれないけれど、Makefileにtagsがターゲットとして存在しているので、マクロやプリプロセッサが多いので、適当なターゲットでkernel sourceをbuildしておき、tagsも作っておくと楽ができる。マクロで関数を生成しているところもあるので、すべてタグジャンプできるとは限らないので注意も必要だ...
迷ったらバイナリを逆アセンブルするとかすればヒントにもなろう。
それでもだめならgrepするなり舐めるなりするといい(?)。

メモリ管理の基本

gorman氏のUnderstanding the Linux Virtual Memory Managerを見ればだいたい判るような気がしてきた。こんなトコロ読むよりも有意だと思う(´・ω・)

ココを参照されている諸兄は、すでにカーネル空間とユーザ空間とかの存在は把握されていると思います。
で、Virtual AddressをPhysical Addressに変換する機能はARMv7Aの場合はMMUが物理メモリを参照して勝手に変換しちゃう。
ということまで理解しているとしましょう。

Linuxの場合、基本的に4kByte/pageを使うことにしてあるので、第二レベル変換テーブルはsmall sectionが選択されています。
必然的に、第一レベル変換はSection Baseの変換テーブルを用いることになります。
Section baseの場合は、1MiBごとに第二レベル変換テーブルの先頭アドレスが置かれます。

順序が逆になりますが、Linuxのメモリ管理テーブルについて文字だけでまとめます。
論理アドレスを上位ビットからテーブル引きをして、PGD(Page Global Directory ),
PUD(Page Upper Directory), PMD(Page Middle Directory), PTE(Page Table Entries )の順に
4段階でページテーブルをインデックスします。
先に示した資料ではPUDが無いので、どこかで追加されたのでしょう。

linux/mm/配下は、この概念をベースにして実装されており、アーキテクチャに依存する
アドレス空間の差異を吸収できる仕組みになっています。
Linuxの論理-物理変換管理テーブルが定義されており、汎用性を持った記述になっています。
アーキテクチャ依存部でうまくコードを共通化しているのでそのあたりも見ていきましょう。
1. VAを元に PGDから PUDの先頭アドレスを得る。
2. PUDとVAから PMDの先頭アドレスを得る(arm32ではPUDは未使用,PGD=>PMDとなっている)
3. PMDとVAから PTEの先頭アドレスを得る(PGDがPMDをインデックスしている、と見る)。
4. PTEとVAから 該当するページの物理アドレスや属性や状態を得る。

arm32/LPAEなしでは、pgdレベル1変換テーブル、PMDがレベル2変換テーブル、
pteがレベル2変換テーブルの該当アドレスにあたるSmall page要素になります。
PUD,PMDの取得は、pud_offset()とpmd_offset()で実装されますが、pgd,pudをそのまま返しています。
関連するシンボルは以下のとおりで、ビット幅をセットします。
symbol value
PAGE_SHIFT12(4k)
PMD_SHIFT21
SECTION_SHIFT20
SUPERSECTION_SHIFT20
下位12bitはページ単位になるので、変換テーブルによって指される最小単位となります。
Cortex-Aシリーズになると、不要な気がしますが、過去の経緯でPGDは2GiB空間をインデックスするように設計されています。
accessed/dirty bitがpteに存在しないので それをエミュレートするため, とありますが,
ここは消化しきれていません. arm依存部でも ARMv4,v5,v7,v8と差異はそこそこありそうです。


ARMv7Aの場合

ここで、LAPEなしと仮定して、以下を参照する(LAPEありなら03level)。

FILE:arch/arm/include/asm/pgtable-2level.h
/*
 * Hardware-wise, we have a two level page table structure, where the first
 * level has 4096 entries, and the second level has 256 entries.  Each entry
 * is one 32-bit word.  Most of the bits in the second level entry are used
 * by hardware, and there aren't any "accessed" and "dirty" bits.
 *
 * Linux on the other hand has a three level page table structure, which can
 * be wrapped to fit a two level page table structure easily - using the PGD
 * and PTE only.  However, Linux also expects one "PTE" table per page, and
 * at least a "dirty" bit.
 *
 * Therefore, we tweak the implementation slightly - we tell Linux that we
 * have 2048 entries in the first level, each of which is 8 bytes (iow, two
 * hardware pointers to the second level.)  The second level contains two
 * hardware PTE tables arranged contiguously, preceded by Linux versions
 * which contain the state information Linux needs.  We, therefore, end up
 * with 512 entries in the "PTE" level.
 *
 * This leads to the page tables having the following layout:
 *
 *    pgd             pte
 * |        |
 * +--------+
 * |        |       +------------+ +0
 * +- - - - +       | Linux pt 0 |
 * |        |       +------------+ +1024
 * +--------+ +0    | Linux pt 1 |
 * |        |-----> +------------+ +2048
 * +- - - - + +4    |  h/w pt 0  |
 * |        |-----> +------------+ +3072
 * +--------+ +8    |  h/w pt 1  |
 * |        |       +------------+ +4096
 *
 * See L_PTE_xxx below for definitions of bits in the "Linux pt", and
 * PTE_xxx for definitions of bits appearing in the "h/w pt".
 *
 * PMD_xxx definitions refer to bits in the first level page table.
 *
 * The "dirty" bit is emulated by only granting hardware write permission
 * iff the page is marked "writable" and "dirty" in the Linux PTE.  This
 * means that a write to a clean page will cause a permission fault, and
 * the Linux MM layer will mark the page dirty via handle_pte_fault().
 * For the hardware to notice the permission change, the TLB entry must
 * be flushed, and ptep_set_access_flags() does that for us.
 *
 * The "accessed" or "young" bit is emulated by a similar method; we only
 * allow accesses to the page if the "young" bit is set.  Accesses to the
 * page will cause a fault, and handle_pte_fault() will set the young bit
 * for us as long as the page is marked present in the corresponding Linux
 * PTE entry.  Again, ptep_set_access_flags() will ensure that the TLB is
 * up to date.
 *
 * However, when the "young" bit is cleared, we deny access to the page
 * by clearing the hardware PTE.  Currently Linux does not flush the TLB
 * for us in this case, which means the TLB will retain the transation
 * until either the TLB entry is evicted under pressure, or a context
 * switch which changes the user space mapping occurs.
 */
#define PTRS_PER_PTE            512
#define PTRS_PER_PMD            1
#define PTRS_PER_PGD            2048

#define PTE_HWTABLE_PTRS        (PTRS_PER_PTE)
#define PTE_HWTABLE_OFF         (PTE_HWTABLE_PTRS * sizeof(pte_t))
#define PTE_HWTABLE_SIZE        (PTRS_PER_PTE * sizeof(u32))

/*
 * PMD_SHIFT determines the size of the area a second-level page table can map
 * PGDIR_SHIFT determines what a third-level page table entry can map
 */
#define PMD_SHIFT               21
#define PGDIR_SHIFT             21

#define PMD_SIZE                (1UL << PMD_SHIFT)
#define PMD_MASK                (~(PMD_SIZE-1))
#define PGDIR_SIZE              (1UL << PGDIR_SHIFT)
#define PGDIR_MASK              (~(PGDIR_SIZE-1))

FILE: arch/arm/include/asm/pgtable.h
/* to find an entry in a page-table-directory */
#define pgd_index(addr)		((addr) >> PGDIR_SHIFT)  /// 21bit@LAPEなし

#define pgd_offset(mm, addr)	((mm)->pgd + pgd_index(addr))

/* to find an entry in a kernel page-table-directory */
#define pgd_offset_k(addr)	pgd_offset(&init_mm, addr)


static inline pte_t *pmd_page_vaddr(pmd_t pmd)
{
	return __va(pmd_val(pmd) & PHYS_MASK & (s32)PAGE_MASK);
}

#ifndef CONFIG_HIGHPTE
#define __pte_map(pmd)		pmd_page_vaddr(*(pmd))
#define __pte_unmap(pte)	do { } while (0)
#else
#endif
#define pte_index(addr)		(((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
#define pte_offset_kernel(pmd,addr)	(pmd_page_vaddr(*(pmd)) + pte_index(addr))
#define pte_offset_map(pmd,addr)	(__pte_map(pmd) + pte_index(addr))
#define pte_unmap(pte)			__pte_unmap(pte)

#define pte_pfn(pte)		((pte_val(pte) & PHYS_MASK) >> PAGE_SHIFT)
#define pfn_pte(pfn,prot)	__pte(__pfn_to_phys(pfn) | pgprot_val(prot))

#define pte_page(pte)		pfn_to_page(pte_pfn(pte))
#define mk_pte(page,prot)	pfn_pte(page_to_pfn(page), prot)

#define pte_clear(mm,addr,ptep)	set_pte_ext(ptep, __pte(0), 0)

#define pte_none(pte)		(!pte_val(pte))
#define pte_present(pte)	(pte_val(pte) & L_PTE_PRESENT)
#define pte_write(pte)		(!(pte_val(pte) & L_PTE_RDONLY))
#define pte_dirty(pte)		(pte_val(pte) & L_PTE_DIRTY)
#define pte_young(pte)		(pte_val(pte) & L_PTE_YOUNG)
#define pte_exec(pte)		(!(pte_val(pte) & L_PTE_XN))
#define pte_special(pte)	(0)
以上がソースを追うのに必要な情報(だったはず)。

具体的に変換テーブルはどこに..?

見ていて有用なのがデバッグ用の関数やコメント。
Oopsを吐くときに使用されている show_pte()から依存部の実装を追える。

FILE: arch/arm/mm/fault.c
/*
 * This is useful to dump out the page tables associated with
 * 'addr' in mm 'mm'.
 */
void show_pte(struct mm_struct *mm, unsigned long addr)
{
	pgd_t *pgd;

	if (!mm)
		mm = &init_mm;

	printk(KERN_ALERT "pgd = %p\n", mm->pgd);
	pgd = pgd_offset(mm, addr);
	printk(KERN_ALERT "[%08lx] *pgd=%08llx",
			addr, (long long)pgd_val(*pgd));
...
第一レベル変換テーブルが mm_structのメンバpgdであることがわかる。
引数mmがNULLの場合、すなわちタスクのmmが無い状態では、init_mmが基底になっていそうである*1

このinit_mmを探してみると以下に見つかる。

FILE:mm/init-mm.c
struct mm_struct init_mm = {
        .mm_rb          = RB_ROOT,
        .pgd            = swapper_pg_dir,
        .mm_users       = ATOMIC_INIT(2),
        .mm_count       = ATOMIC_INIT(1),
        .mmap_sem       = __RWSEM_INITIALIZER(init_mm.mmap_sem),
        .page_table_lock =  __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
        .mmlist         = LIST_HEAD_INIT(init_mm.mmlist),
        INIT_MM_CONTEXT(init_mm)
};
これ(swapper_pg_dir)の実態はどこにあるのか。
FILE:arch/arm/kernel/head.S
/*
 * swapper_pg_dir is the virtual address of the initial page table.
 * We place the page tables 16K below KERNEL_RAM_VADDR.  Therefore, we must
 * make sure that KERNEL_RAM_VADDR is correctly set.  Currently, we expect
 * the least significant 16 bits to be 0x8000, but we could probably
 * relax this restriction to KERNEL_RAM_VADDR >= PAGE_OFFSET + 0x4000.
 */

#define KERNEL_RAM_VADDR        (PAGE_OFFSET + TEXT_OFFSET)
#if (KERNEL_RAM_VADDR & 0xffff) != 0x8000
#error KERNEL_RAM_VADDR must start at 0xXXXX8000
#endif

#ifdef CONFIG_ARM_LPAE
        /* LPAE requires an additional page for the PGD */
#define PG_DIR_SIZE     0x5000
#define PMD_ORDER       3
#else
#define PG_DIR_SIZE     0x4000
#define PMD_ORDER       2
#endif

        .globl  swapper_pg_dir
        .equ    swapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZE

        .macro  pgtbl, rd, phys
        add     \rd, \phys, #TEXT_OFFSET - PG_DIR_SIZE
        .endm
MMUのテーブルウォークに使われる、レベル1変換テーブルをswapper_pg_dirに格納している。
しかし、2MiB/1stLvとして運用している(前引用のコメントどおり)。
PGD1MiB単位のブロック.. 1MiB x 4Ki個(12bit)のテーブルで 4GiBフルアドレッシング
PTE最終的に4KiBをインデックスして 256個(8bit)テーブルがあれば 1MiBになる。


pgd_offset_k()でインデックスを求めるが、これは2MiB単位でアドレスを求める。
pgdが2MiBになる理由が未だわからんなぁ。
テーブルは作ってしまうのに、どこに属性を置くのか...

タスクがある場合
SMPの場合は cpu固有情報は cpu_なんとか()という関数名で実装されているようである。
第一レベル変換テーブル、すなわちpgd配列の先頭ポインタを取得する関数を見てみると、
以下の実相となっていた。
FILE: arch/arm/include/asm/proc-fns.h
#ifdef CONFIG_ARM_LPAE
#else
#define cpu_get_pgd()   \
        ({                                              \
                unsigned long pg;                       \
                __asm__("mrc    p15, 0, %0, c2, c0, 0"  \
                         : "=r" (pg) : : "cc");         \
                pg &= ~0x3fff;                          \
                (pgd_t *)phys_to_virt(pg);              \
        })
#endif
意訳すると、"Translation Table Base Register 0"を取得する
(=current taskのpgd先頭アドレス(物理)を取得して、論理に変換する)と言えよう。


*1 : タスク生成時のメモリアロケーションを調べておく必要がありますなぁ

で?

MMU faultのハンドラは以下で、割り込みベクタまわりの処理から呼ばれてくる。
FILE: arch/mm/fault.c
static int __kprobes
do_page_fault(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
...
	tsk = current;
	mm  = tsk->mm;
...
	/*
	 * If we're in an interrupt or have no user
	 * context, we must not take the fault..
	 */
	if (in_atomic() || !mm)
		goto no_context;
...
	fault = __do_page_fault(mm, addr, fsr, flags, tsk);
....
	if (likely(!(fault & (VM_FAULT_ERROR | VM_FAULT_BADMAP | VM_FAULT_BADACCESS))))
		return 0;
...
	__do_user_fault(tsk, addr, fsr, sig, code, regs);
	return 0;

no_context:
	__do_kernel_fault(mm, addr, fsr, regs);
	return 0;

ユーザ空間

static int __kprobes
__do_page_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr,
		unsigned int flags, struct task_struct *tsk)
{
	struct vm_area_struct *vma;
	int fault;

	vma = find_vma(mm, addr);
	fault = VM_FAULT_BADMAP;
	if (unlikely(!vma))
		goto out;
	if (unlikely(vma->vm_start > addr))
		goto check_stack;

	/*
	 * Ok, we have a good vm_area for this
	 * memory access, so we can handle it.
	 */
good_area:
	if (access_error(fsr, vma)) {
		fault = VM_FAULT_BADACCESS;
		goto out;
	}

	return handle_mm_fault(mm, vma, addr & PAGE_MASK, flags);

check_stack:
	/* Don't allow expansion below FIRST_USER_ADDRESS */
	if (vma->vm_flags & VM_GROWSDOWN &&
	    addr >= FIRST_USER_ADDRESS && !expand_stack(vma, addr))
		goto good_area;
out:
	return fault;
}
ここはユーザ空間でのfaultで、タスクに割り当てられた空間かどうかをチェックしています。
find_vma()がそれで、mm_struct構造体メンバのmmapをスキャンします(curent->vmacache[]を先に見る)。
⇒タスクごとのメモリ管理、割り当てと解放、その他はこの変数を触っている個所を見ればよいと分かりました。

handle_mm_fault()はアーキテクチャに依存しないソースで実装されていますので、まずはここまで。
エラー発生時
SIGSEGVとかですね.. 引数sigに エラー判定結果を入れて呼ばれます。
/*
 * Something tried to access memory that isn't in our memory map..
 * User mode accesses just cause a SIGSEGV
 */
static void
__do_user_fault(struct task_struct *tsk, unsigned long addr,
		unsigned int fsr, unsigned int sig, int code,
		struct pt_regs *regs)

kernel空間

コンテキストが無かったり、ディスパッチ禁止(in_atomic()==true)の場合などに呼ばれます。ね。
/*
 * Oops.  The kernel tried to access some page that wasn't present.
 */
static void
__do_kernel_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr,
		  struct pt_regs *regs)
{
	/*
	 * Are we prepared to handle this kernel fault?
	 */
	if (fixup_exception(regs))
		return;

	/*
	 * No handler, we'll have to terminate things with extreme prejudice.
	 */
	bust_spinlocks(1);
	printk(KERN_ALERT
		"Unable to handle kernel %s at virtual address %08lx\n",
		(addr < PAGE_SIZE) ? "NULL pointer dereference" :
		"paging request", addr);

	show_pte(mm, addr);
	die("Oops", regs, fsr);
	bust_spinlocks(0);
	do_exit(SIGKILL);
}
カーネル空間でfaultが起きることは不具合以外にはレアケースだと考えられます。
基本的にカーネル自体はストレートマップされるので。ARMのようにメモリマップドIOで
ioremap()を忘れていたとか、範囲を間違ったとかならありえそうです。

あと、発生が予期できる個所に関しては、fixup_exception()で判定されます。
これは前回の記事へ。。。


ARMの資料をみる

Programmer's manualはぜひとも見ていただきたい。
Linux OSを意識した説明がしっかりと記述されていて、スタートアップコードや本記事の対応・実装説明についても記載があります.
■Cortex-A Series Programmer's manual
8.9.2 Emulation of dirty and accessed bits
Linux makes use of a bit associated with each page which marks whether the page is dirty (the page has been modified by the application and may therefore need to be written from memory to disk when the page is no longer needed).
This is not directly supported in hardware by ARM processors, and must therefore be implemented by kernel software.
When a page is first created, it is marked as read-only.
The first write to such a page (a clean page) will cause an MMU permission fault and the kernel data abort handler will be called.
The Linux memory management code will mark the page as dirty, if the page should indeed be writable, using the function handle_pte_fault() .
The page table entry is modified to make it allow both reads and writes and the TLB entry invalidated from the cache to ensure the MMU now uses the newly modified entry.
The abort handler then returns to re-try the faulting access in the application.
Linuxは、ページがダーティであるか否かをマークし、各ページに対応するビットを利用する。
ダーティである、とは、ページがアプリケーションによって変更されるか、そのページが必要ではなくなったときにメモリからディスクへ書き出す必要があることを示す。
ARMプロセッサでは、直接的にハードウェアでサポートされていない。したがってカーネルソフトウェアで実装される必要がある。
ページが最初に作られたとき、read-onlyとしてマークされる。
そのページ(クリーンなページ)に対して、最初の書き込みは、MMUのpermission faultの要因となり、カーネルのデータアボートハンドラが呼ばれる。
Linuxのメモリ管理コードは、そのページをdirtyと設定します。そのページが書き込み可能であるべきならば、handle_pte_fault()を使う。
ページテーブルエントリーは、読み書き可能に変更される。そして、TLBエントリは、MMUが今新しく変更されたエントリを使うため、キャッシュから無効化される。
その後、アボートハンドラは、アプリケーション内でfaultアクセスした命令を再実行するため、リターンする。
*訳注
handle_te_fault()は最終的によばれれるもので、PUD, PMDが無ければ取得するし、エラー判定になれば呼ばれず返る。

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

2014/03/26 linux::ARM

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直後と同じ状態にしておくことが望ましい。
ソフト的な状態保持は影響を受けないので、
制御レジスタ・外部デバイスの設定値が効いてくるだろう。

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

[ARM] compressed kernel

2013/10/20 linux::ARM

zImageの展開処理

調査対象は、【kernel 3.0.39 "Sneaky Weasel"】です。

例として、u-bootから起動する場合を考えます。
ここで上げるのは、とあるボードへの移植時に調査した値です。
ボード依存部の値は、デタラメに書いている可能性があるので、注意してください。


NOR bootの場合は、ROM上のコード実行が可能ですが、昨今の方式*1ではbootloaderがDRAM上に展開してから実行することが多いでしょう。

u-bootが読み込むファイル、uImageは、zImageにuImageヘッダがついたもの。
図にしてみると以下のイメージ。(AAでスミマセン)
+------------------------+
| u-boot header(64oct.)  |
+------------------------+
| zImage decompress code |
| .text                  |
| (with compressed data )|
| .got                   |
| .got.plt               |
| .bss                   |
+------------------------+
このデータの読み込みは、u-bootによって行われるため、任意のアドレスに展開されます。


ldscriptを見ると、ざっくりとこんな感じ。
  _text = .;
  .text : {}
  _etext = .;
  _got_start = .;
  .got			: { *(.got) }
  _got_end = .;
  .got.plt		: { *(.got.plt) }
  _edata = .;

  __bss_start = .;
  .bss			: { *(.bss) }
  _end = .;

*1 : NAND/SDから2nd boot programを読み出して、本当のローダを呼び出す。

事前準備

実際には、コードを読みながら参照していった結果です。

FILE: arch/arm/boot/compressed/Makefile
# Supply ZRELADDR to the decompressor via a linker symbol.
ifneq ($(CONFIG_AUTO_ZRELADDR),y)
LDFLAGS_vmlinux += --defsym zreladdr=$(ZRELADDR)
ZRELADDRは、以下で定義されます。
FILE: arch/arm/boot/Makefile
ZRELADDR    := $(zreladdr-y)
右辺は、SoC依存部にて定義されています。
FILE: arch/arm/mach-*/Makefile.boot
zreladdr-y	:= 0x40008000
params_phys-y	:= 0x40000100
initrd_phys-y	:= 0x40800000


zImage先頭から

エントリーポイントはここ。

bootloaderから飛んでくるところ

アドレスは任意。

FILE: ./boot/compressed/head.S
start:
 ※cache有効, TLBはVA-PA一対一対応, 

restart:
		.align	2
		.type	LC0, #object
LC0:
		.word	LC0					@ r1
		.word	__bss_start			@ r2
		.word	_end				@ r3
		.word	_edata				@ r6
		.word	input_data_end - 4	@ r10 (inflated size location)
		.word	_got_start			@ r11
		.word	_got_end			@ ip(r12)

		.word	.L_user_stack_end	@ sp

		.size	LC0, . - LC0

		/*
		 * We might be running at a different address.  We need
		 * to fix up various pointers.
		 */
		sub	r0, r0, r1		@ calculate the delta offset
		add	r6, r6, r0		@ _edata
		add	r10, r10, r0	@ inflated kernel size location
LC0(DRAMの位置)から、"LC0"の配置した位置(ldscriptでは先頭0、コード配置後のLC0の位置)を差っ引いて、r0に保持する。
圧縮カーネルの、展開後のサイズは、build systemによって、圧縮カーネルの後ろにLEで付与される。したがって、r10へloadする値は、input_data_endから4引いてある。(32bit決め打ちだね)
spも、relocate後のアドレスをセット。r10にsp+64kのアドレスをセット(malloc用に64k確保)(CONFIG_ZBOOT_ROM未定義の場合。定義時はコード本体がROMに入るから、_edataの後ろに配置する)

このイメージがロードされる位置がどこでも動けるようになっている。

展開後のkernel位置と、この展開コード、edata類の空間が重なっているかをチェックする。重複している場合、reset〜edataまでを、後ろからコピーする(memmove相当)。その後、コピーした後のrestartへjumpする。(これで重複しない位置で再度relocateする)

※ユーザモードで入ってきた場合、SVCで特権を得るようになってます。
ヴェクタ位置へのロードを期待しているようですが、必ずしもそうではないから、ちょっと不味そうな気がするネ。

relocate後

decompress処理のソフトと、圧縮イメージとを再配置して、kernel imageを展開できる状態になって、wont_overwriteへたどり着く。前述のように、最初のバイナリがロードされる位置によって、ここへ直接来ることもあれば、relocateすることもありますね。
wont_overwrite:
/*
 * If delta is zero, we are running at the address we were linked at.
 *   r0  = delta
 *   r2  = BSS start
 *   r3  = BSS end
 *   r4  = kernel execution address
 *   r7  = architecture ID
 *   r8  = atags pointer
 *   r11 = GOT start
 *   r12 = GOT end
 *   sp  = stack pointer
 */
・GOT書き換え(オフセット分加算して、飛び先がrelocate先のアドレスになるように)
・BSSをゼロクリア


本来のImage(vmlinux)へ

kernelの展開先アドレスを第一引数、spからsp+64kを第二〜三引数に渡して、第四引数にアーキテクチャIDをのせてcall。
void decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p, unsigned long free_mem_ptr_end_p, int arch_id)
展開が終わったら、cache clean、cache-offしてから、圧縮前のkernel先頭へ飛ぶ。


ARM boot

2013/10/19 linux::ARM
kernel bootからの処理をみてみましょう。
kernel先頭へは、u-bootやbare-boxから飛んできます。

そこはそれ。wiki contentsとして記事を置いておきます。
OK キャンセル 確認 その他