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が無ければ取得するし、エラー判定になれば呼ばれず返る。
OK キャンセル 確認 その他