Linuxのユーザプロセスのメモリマップについて

  • 16
    Like
  • 0
    Comment

はじめに

Linuxのユーザプロセスのセグメントマップ周辺の知識が、意外とあやふやな箇所があったので確認したいと思ったこと、その他にも諸々の目的があったので、今回はこの点をまとめました。
なお、特に記載のない場合、以下に従います。

項目
Linuxバージョン 4.9
CPUアーキテクチャ arm
実行形式ファイル ELF32

.text, .data, ユーザ空間のスタック

.text, .dataの仮想アドレス空間生成はexec()の処理の一環で行われます。
ポイントになるのは、以下do_execveat_common()で呼び出している2つの関数bprm_mm_init()とexec_binprm()です。

fs/exec.c
/*
 * sys_execve() executes a new program.
 */
static int do_execveat_common(int fd, struct filename *filename,
                  struct user_arg_ptr argv,
                  struct user_arg_ptr envp,
                  int flags)
{
    /* 略 */
    retval = bprm_mm_init(bprm);
    if (retval)
        goto out_unmark;
    /* 略 */
    retval = exec_binprm(bprm);
    if (retval < 0)
        goto out;

まず最初にbprm_mm_init()を見ます。

fs/exec.c
static int bprm_mm_init(struct linux_binprm *bprm)
{
    /* 略 */
    /* 実行形式ファイル依存の初期化。今回見るのはこっち */
    err = __bprm_mm_init(bprm);
    if (err)
        goto err;

    return 0;

実質的な処理は、__bprm_mm_init()で行っています。コードを見ましょう。
実は、__bprm_mm_init()では、ユーザ空間スタックの仮想アドレス空間構築に必要なパラメータを作ります。

ユーザ空間スタック

fs/exec.c
static int __bprm_mm_init(struct linux_binprm *bprm)
{
    bprm->vma = vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
    /* 略 */
    /*
     * Place the stack at the largest stack address the architecture
     * supports. Later, we'll move this to an appropriate place. We don't
     * use STACK_TOP because that can depend on attributes which aren't
     * configured yet.
     */
    BUILD_BUG_ON(VM_STACK_FLAGS & VM_STACK_INCOMPLETE_SETUP);
    vma->vm_end = STACK_TOP_MAX;
    vma->vm_start = vma->vm_end - PAGE_SIZE;
    vma->vm_flags = VM_SOFTDIRTY | VM_STACK_FLAGS | VM_STACK_INCOMPLETE_SETUP;
    vma->vm_page_prot = vm_get_page_prot(vma->vm_flags);
    INIT_LIST_HEAD(&vma->anon_vma_chain);

    err = insert_vm_struct(mm, vma);
    if (err)
        goto err;

    mm->stack_vm = mm->total_vm = 1;

繰り返しですが、ここで実施しているのは、ユーザ空間スタック向けの仮想アドレス空間のパラメータを作ることです。この時点では、自プロセスに該当仮想アドレス空間の設定は反映されません。

STACK_TOP_MAX - ユーザスタックの底のアドレス -

vm_end(ユーザ空間スタックの末尾領域)に設定しているSTACK_TOP_MAXは以下の定義となっています。

arch/arm/include/asm/processor.h
#ifdef __KERNEL__
#define STACK_TOP   ((current->personality & ADDR_LIMIT_32BIT) ? \
             TASK_SIZE : TASK_SIZE_26)
#define STACK_TOP_MAX   TASK_SIZE
#endif
arch/arm/include/asm/memory.h
#ifdef CONFIG_MMU

/*
 * TASK_SIZE - the maximum size of a user space task.
 * TASK_UNMAPPED_BASE - the lower boundary of the mmap VM area
 */
#define TASK_SIZE       (UL(CONFIG_PAGE_OFFSET) - UL(SZ_16M))

PAGE_OFFSETとは、カーネルイメージが置かれる領域の先頭仮想アドレスです。厳密には異なるのですが、イメージの容易さを優先させる場合、カーネル空間の開始アドレスと考えても差し支えありません。
その定義の名前から、意味が想像しにくいので注意です。
armの場合、その値はKconfigで決まります。(ここで決まった値が、CONFIG_PAGE_OFFSETになります)。

arch/arm/Kconfig
config PAGE_OFFSET
    hex  
    default PHYS_OFFSET if !MMU 
    default 0x40000000 if VMSPLIT_1G
    default 0x80000000 if VMSPLIT_2G
    default 0xB0000000 if VMSPLIT_3G_OPT
    default 0xC0000000

なお、この文章内では、PAGE_OFFSETの値を0xc0000000とします。

話を戻します。これまで見てきたことから、STACK_TOP_MAXの値は、0xc0000000 - 16Mbyte = 0xbf000000 になります。
また、スタックのサイズは暫定的に4kbyteとしています。

.text, .data, .bss

次にexec_binprm()を見ます。

fs/exec.c

static int exec_binprm(struct linux_binprm *bprm)
{
    /* 略 */
    ret = search_binary_handler(bprm);
    /* 略 */
fs/exec.c

int search_binary_handler(struct linux_binprm *bprm)
{
    /* 略 */
 retry:
    read_lock(&binfmt_lock);
    list_for_each_entry(fmt, &formats, lh) {
        if (!try_module_get(fmt->module))
            continue;
        read_unlock(&binfmt_lock);
        bprm->recursion_depth++;
        retval = fmt->load_binary(bprm);
    /* 略 */

実質的な処理は、search_binary_handler()のfmt->load_binary()にあるようです。
結論から書くと、ELF32の場合、fs/binfmt_elf.cに実装されているload_elf_binary()が呼ばれます。
※このあたり、調べてみたい方は、fs/exec.c内を「formats」で検索してください。そうすると、formatsリストにバイナリフォーマットを登録する関数があるはずです。そこを足がかりにして調べてみるとはかどるでしょう。

elfバイナリについて

次に、load_elf_binary()を確認します。少し長いですが、もともとこの関数は400行以上あるのでそこはご勘弁を。適時コメントを追加しています。
また、コードを読む際、ELFフォーマットのプログラムヘッダの具体例があると助けになると思うので、おまけで掲載します。

プログラムヘッダの具体例
fyoshida-ThinkPad-X201% readelf -l a

Elf ファイルタイプは EXEC (実行可能ファイル) です
エントリポイント 0x102c0
8 個のプログラムヘッダ、始点オフセット 52

プログラムヘッダ:
  タイプ       オフセット 仮想Addr   物理Addr   FileSiz MemSiz  Flg Align
  EXIDX          0x000498 0x00010498 0x00010498 0x00008 0x00008 R   0x4
  PHDR           0x000034 0x00010034 0x00010034 0x00100 0x00100 R E 0x4
  INTERP         0x000134 0x00010134 0x00010134 0x00019 0x00019 R   0x1
      [Requesting program interpreter: /lib/ld-linux-armhf.so.3]
  LOAD           0x000000 0x00010000 0x00010000 0x004a4 0x004a4 R E 0x10000
  LOAD           0x0004a4 0x000204a4 0x000204a4 0x00118 0x0011c RW  0x10000
  DYNAMIC        0x0004b0 0x000204b0 0x000204b0 0x000e8 0x000e8 RW  0x4
  NOTE           0x000150 0x00010150 0x00010150 0x00044 0x00044 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
  /* 以下略 */
fs/binfmt_elf.c
static int load_elf_binary(struct linux_binprm *bprm)
{
    /* 略 */
    /* 
     * わかりにくいが、bprm->mmはflush_old_exec()でcurrent(自プロセス)のmmに反映される。
     * よって、ここで先に設定したユーザ空間スタックが可視になる
     */
    /* Flush all traces of the currently running executable */
    retval = flush_old_exec(bprm);
    /* 略 */
    /* この中でスタック絡みのパラメータを設定している。この中で、直値でexpand_size=128kbyteとしている。*/
    /* Do this so that we can load the interpreter, if need be.  We will
       change some of these later */
    retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
                 executable_stack);
    /* 略 */ 
    /* 属性PT_LOADのセグメントに仮想アドレスを割り当てる */
    for(i = 0, elf_ppnt = elf_phdata;
        i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
        int elf_prot = 0, elf_flags;
        unsigned long k, vaddr;
        unsigned long total_size = 0;

        if (elf_ppnt->p_type != PT_LOAD)
            continue;

        /* 略 */

        if (elf_ppnt->p_flags & PF_R)
            elf_prot |= PROT_READ;
        if (elf_ppnt->p_flags & PF_W)
            elf_prot |= PROT_WRITE;
        if (elf_ppnt->p_flags & PF_X)
            elf_prot |= PROT_EXEC;

        elf_flags = MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE;

        vaddr = elf_ppnt->p_vaddr;
        if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) {
            elf_flags |= MAP_FIXED;
        } else if (loc->elf_ex.e_type == ET_DYN) {
            /* 略。今回は動的リンクのコードは扱わない */
        }

        /* 
         * 見つけた.text, .dataについて、自プロセスにメモリマップを行い、仮想アドレス空間を生成する。
         * elf_map()の中ではvm_mmap()を呼ぶことがメイン。
         */
        error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
                elf_prot, elf_flags, total_size);
        /* 略 */
        /* 
         * このループの末尾近辺は、
         * elf_bss, elf_brk, start_code, end_code, start_data, end_dataの値を設定する処理
         */
    }

    /* 略 */

    /* BSS領域を作る。.data領域がアドレスの下位に向かって伸びていく */
    retval = set_brk(elf_bss, elf_brk);
    /* 略 */
    set_binfmt(&elf_format);
    /* 略 */
    /* start_code, end_codeは.text領域の開始アドレス、末尾アドレス */
    current->mm->end_code = end_code;
    current->mm->start_code = start_code;
    /* start_code, end_codeは.data領域の開始アドレス、末尾アドレス */
    current->mm->start_data = start_data;
    current->mm->end_data = end_data;
    /* スタック */
    current->mm->start_stack = bprm->p;
    /* 略 */

なお、以下の点は注意が必要です。

  • .textだけでなく、.dataについてもMAP_PRIVATE(共有しない), MAP_DENYWRITE(保護属性でWriteが指定された場合でも、ひとまず書込み禁止でマップ), MAP_EXECUTABLE(実行権を設定可能。実行権を即つけるわけではない)としているところは注意してください。
  • ここで行うのは、自プロセスの仮想アドレス空間に.textと.dataの仮想アドレス空間を挿入することです。この時点で物理メモリとのひも付けは行いません。それはデマンドページングで行われます。

動的ライブラリがロードされるベースアドレス

また、突然ですが、動的ライブラリがロードされる領域について、以下の定義値があります。動的ライブラリが実行されたときなどにこの仮想アドレス空間にロードされるようです。

arch/arm/include/asm/elf.h
/* This is the location that an ET_DYN program is loaded if exec'ed.  Typical
   use of this is to invoke "./ld.so someprog" to test out a new version of
   the loader.  We need to make sure that it is out of the way of the program
   that it will "exec", and that there is sufficient room for the brk.  */

#define ELF_ET_DYN_BASE (TASK_SIZE / 3 * 2)

ここまでのまとめ

ここまで書いたことをまとめると、メモリマップはおおよそ以下のとおりです。
なお、ここまで見てきた通り、メモリマップはconfigによってかなり異なります。よって、仕事や開発でメモリマップについて知りたい場合、この文書に書かれたことをベースに各自ご確認をお願いします。

memmap2.jpg

カーネルスタック

プロセスがカーネル空間のコードを実行する際に使われるスタック(カーネルスタック)は、スレッドごと(task_structで管理されるインスタンスごと)に生成されます。
生成される場所はfork()の一処理です。

kernel/fork.c
static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
{
    struct task_struct *tsk;
    unsigned long *stack;

    /* 略 */
    tsk = alloc_task_struct_node(node);
    /* 略 */

    stack = alloc_thread_stack_node(tsk, node);
    /* 略 */

    /* 単にコピー元のtask_struct origの内容をtskに複写しているだけ */
    err = arch_dup_task_struct(tsk, orig);

    /* 略 */
    tsk->stack = stack;
    /* 略 */

    setup_thread_stack(tsk, orig);
    /* 略 */
    set_task_stack_end_magic(tsk);
    /* 略 */

alloc_task_struct_node()は、kmem_cache_alloc_node()経由でtask struct構造体を割り当てるだけです。

kernel/fork.c
static unsigned long *alloc_thread_stack_node(struct task_struct *tsk, int node)
{
#ifdef CONFIG_VMAP_STACK
    /* 
     * 今回は対象外だが、x86, ia64ではデフォルトでこちらが有効になるようだ。 
     * もちろんdistributionによっては異なってくるかもしれない。
     */
#else
    /* 今回はこちら */
    struct page *page = alloc_pages_node(node, THREADINFO_GFP,
                         THREAD_SIZE_ORDER);

    return page ? page_address(page) : NULL;
#endif
}

ちなみに、THREAD_SIZE_ORDERは以下のとおりです。よって、2ページ分のページを要求していることになります。
ここで割り当てた領域がカーネルスタックになります。

arch/arm/include/asm/thread_info.h
#define THREAD_SIZE_ORDER   1
#define THREAD_SIZE     (PAGE_SIZE << THREAD_SIZE_ORDER)

ちなみに、1ページのサイズは、極めて標準的に4kbyteです。

arch/arm/include/asm/page.h
#define PAGE_SHIFT      12
#define PAGE_SIZE       (_AC(1,UL) << PAGE_SHIFT)

setup_thread_stack(), set_task_stack_end_magic()を経て、カーネルスタックの状態は以下のようになります。
(詳しくは、setup_thread_stack(), set_task_stack_end_magic()を読んでください)

memmap1.jpg

最後に

ざっくりと書きましたが、メモリがらみの機能設計をしたり、不具合解析をする際に、メモリマップのイメージができることは重要です。
また、armだけでなく、x86などの異なるアーキテクチャのメモリマップを調査する際にも参考になると思います。

参考文献

Linuxカーネル解読室
Linkers&Loaders