LoginSignup
14
11

More than 5 years have passed since last update.

Raspberry Pi2(Linux Kernel)のブートシーケンスを読む(その4) start_kernel()始めからカナリアコードまで

Last updated at Posted at 2016-03-27

前回

ENTRY(stext)でMMUをONにするまで(アーキテクチャ依存部を抜け出すまで)を説明しました。
今回から、アーキテクチャに依存しない処理になります。

start_kernel()の内容

以下に、start_kernel(init/main.c)の処理を全て記載します。
ここでの処理は、initプロセスが動き出すまでに「モジュールの初期化」、
「デバイスの初期化」、「割込みハンドラの管理」、「デバッグ用機能の初期化」などです。
この初期化を追う事によって、Linux Kernelがどのような機能を有するのかがある程度、理解できます。
初期化の対象が多いため、今回はboot_init_stack_canary()までを解読します。

init/main.c
asmlinkage __visible void __init start_kernel(void)
{
    char *command_line;
    char *after_dashes;

    /*
     * Need to run as early as possible, to initialize the
     * lockdep hash:
     */
    lockdep_init();
    set_task_stack_end_magic(&init_task);
    smp_setup_processor_id();
    debug_objects_early_init();

    /*
     * Set up the the initial canary ASAP:
     */
    boot_init_stack_canary();

    cgroup_init_early();

    local_irq_disable();
    early_boot_irqs_disabled = true;

/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */
    boot_cpu_init();
    page_address_init();
    pr_notice("%s", linux_banner);
    setup_arch(&command_line);
    mm_init_cpumask(&init_mm);
    setup_command_line(command_line);
    setup_nr_cpu_ids();
    setup_per_cpu_areas();
    smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */

    build_all_zonelists(NULL, NULL);
    page_alloc_init();

    pr_notice("Kernel command line: %s\n", boot_command_line);
    parse_early_param();
    after_dashes = parse_args("Booting kernel",
                  static_command_line, __start___param,
                  __stop___param - __start___param,
                  -1, -1, &unknown_bootoption);
    if (!IS_ERR_OR_NULL(after_dashes))
        parse_args("Setting init args", after_dashes, NULL, 0, -1, -1,
               set_init_arg);

    jump_label_init();

    /*
     * These use large bootmem allocations and must precede
     * kmem_cache_init()
     */
    setup_log_buf(0);
    pidhash_init();
    vfs_caches_init_early();
    sort_main_extable();
    trap_init();
    mm_init();

    /*
     * Set up the scheduler prior starting any interrupts (such as the
     * timer interrupt). Full topology setup happens at smp_init()
     * time - but meanwhile we still have a functioning scheduler.
     */
    sched_init();
    /*
     * Disable preemption - early bootup scheduling is extremely
     * fragile until we cpu_idle() for the first time.
     */
    preempt_disable();
    if (WARN(!irqs_disabled(),
         "Interrupts were enabled *very* early, fixing it\n"))
        local_irq_disable();
    idr_init_cache();
    rcu_init();

    /* trace_printk() and trace points may be used after this */
    trace_init();

    context_tracking_init();
    radix_tree_init();
    /* init some links before init_ISA_irqs() */
    early_irq_init();
    init_IRQ();
    tick_init();
    rcu_init_nohz();
    init_timers();
    hrtimers_init();
    softirq_init();
    timekeeping_init();
    time_init();
    sched_clock_postinit();
    perf_event_init();
    profile_init();
    call_function_init();
    WARN(!irqs_disabled(), "Interrupts were enabled early\n");
    early_boot_irqs_disabled = false;
    local_irq_enable();

    kmem_cache_init_late();

    /*
     * HACK ALERT! This is early. We're enabling the console before
     * we've done PCI setups etc, and console_init() must be aware of
     * this. But we do want output early, in case something goes wrong.
     */
    console_init();
    if (panic_later)
        panic("Too many boot %s vars at `%s'", panic_later,
              panic_param);

    lockdep_info();

    /*
     * Need to run this when irqs are enabled, because it wants
     * to self-test [hard/soft]-irqs on/off lock inversion bugs
     * too:
     */
    locking_selftest();

#ifdef CONFIG_BLK_DEV_INITRD
    if (initrd_start && !initrd_below_start_ok &&
        page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {
        pr_crit("initrd overwritten (0x%08lx < 0x%08lx) - disabling it.\n",
            page_to_pfn(virt_to_page((void *)initrd_start)),
            min_low_pfn);
        initrd_start = 0;
    }
#endif
    page_ext_init();
    debug_objects_mem_init();
    kmemleak_init();
    setup_per_cpu_pageset();
    numa_policy_init();
    if (late_time_init)
        late_time_init();
    sched_clock_init();
    calibrate_delay();
    pidmap_init();
    anon_vma_init();
    acpi_early_init();
#ifdef CONFIG_X86
    if (efi_enabled(EFI_RUNTIME_SERVICES))
        efi_enter_virtual_mode();
#endif
#ifdef CONFIG_X86_ESPFIX64
    /* Should be run before the first non-init thread is created */
    init_espfix_bsp();
#endif
    thread_info_cache_init();
    cred_init();
    fork_init();
    proc_caches_init();
    buffer_init();
    key_init();
    security_init();
    dbg_late_init();
    vfs_caches_init(totalram_pages);
    signals_init();
    /* rootfs populating might need page-writeback */
    page_writeback_init();
    proc_root_init();
    nsfs_init();
    cpuset_init();
    cgroup_init();
    taskstats_init_early();
    delayacct_init();

    check_bugs();

    acpi_subsystem_init();
    sfi_init_late();

    if (efi_enabled(EFI_RUNTIME_SERVICES)) {
        efi_late_init();
        efi_free_boot_services();
    }

    ftrace_init();

    /* Do the rest non-__init'ed, we're now alive */
    rest_init();
}

"asmlinkage"、"__visible"、"__init"マクロについて

まず、asmlinkage __visible void __init start_kernel(void)
"asmlinkage"、"__visible"、"__init"の役割についてです。

include/linux/linkage.c
#ifndef asmlinkage
#define asmlinkage CPP_ASMLINKAGE
#endif

#ifdef __cplusplus
#define CPP_ASMLINKAGE extern "C"
#else
#define CPP_ASMLINKAGE
#endif

asmlinkageは、gccによるコンパイル時に、指定した関数をC言語として取り扱うためのマクロです。
C++の関数として取り扱うと、オブジェクトファイル(機械語)段階で、
関数名に情報(引数の数、引数の型の情報)が追加されます。
ここで追加される情報は、「オーバーロードによって複数存在する同名関数の中から、
システムがどの関数をコールすれば良いのかを判断する際」に用いられます。
C言語は、関数名に追加情報が含まれる事を前提として動作しないため、
C++として関数をコンパイルしてしまった場合、リンクエラーが発生します。
このエラーの解決にasmlinkageマクロ(extern "C")が必要になります。
 

次に、__visibleの定義です。
このマクロは使用するコンパイラによって、定義が異なります。
この定義は#define __visible (空白)の場合か、以下のように定義されている場合です。

linux/include/compiler-gcc5.h
#define __visible __attribute__((externally_visible))

GCCの拡張機能である__attribute__は、引数の値によって関数、変数、型に対して様々な属性を付加できます。
externally_visibleは、gccのFunction Attributeで以下のように記載されています。

externally_visible
This attribute, attached to a global variable or function, nullifies the effect of the -fwhole-program >command-line option, so the object remains visible outside the current compilation unit.
If -fwhole-program is used together with -flto and gold is used as the linker plugin, externally_visible >attributes are automatically added to functions (not variable yet due to a current gold issue) that are >accessed outside of LTO objects according to resolution file produced by gold. For other linkers that >cannot generate resolution file, explicit externally_visible attributes are still necessary.

externally_visibleの役割は、gccコンパイルオプション"-fwhole_program(最適化オプション)"を無効にし、
fltoおよびgoldによるファイル間のリンクを行わせる事です。
コンパイラに詳しくないので理解が不十分な点がありますが、リンクの方法が二種類選択されている場合に、
片方の方法(fltoおよびgoldを利用する手法)を採用するという事でしょうか。後日、追記・修正します。
  
  
最後に、__init"の定義です。

kernel/include/linux/init.h
#define __init      __section(.init.text) __cold notrace

__section(.init.text)は、メモリ上の初期化関数用の領域に関数のデータを配置するための宣言です。
メモリのsectionは、関数を配置する領域、スタック用の領域、モジュール用の領域などの分け方があります。
RTOS(Real Time OS)のsection分けが規模も小さく、単純で分かりやすいので、気になる方は調査してください。
__coldは、__visibleと同じく、コンパイラによって定義が異なります。
この内容は#define __cold (空白)の場合か、以下のように定義されている場合です。

linux/include/compiler-gcc5.h
#define __cold          __attribute__((__cold__))

__attribute__で__coldが指定された場合の効果は、以下の通りです(一部抜粋)。

cold
The cold attribute on functions is used to inform the compiler that the function is unlikely to be
executed. The function is optimized for size rather than speed and on many targets it is placed into
special subsection of the text section
so all cold functions appears close together improving code
locality of non-cold parts of program.

ここでの説明を読む限り、__section(.init.text)と同じ役割を果たしているように思われます。
予想ですが、コンパイラのバージョンにより、__section()か__coldの片方のみが有効になるのではないかと。
そして、最後のnotraceの定義は以下の通りです。

include/linux/compiler.h
#define notrace __attribute__((no_instrument_function))

no_instrument_functionによって、関数の再帰呼び出しを防止します。
一般的に、__initで修飾された関数はメモリから削除されますが、
その役割を担っているマクロはnotraceなんでしょうか?
一つ一つマクロを調べましたが、判明しませんでした。

boot_init_stack_canary()までの処理内容について

init/main.c
asmlinkage __visible void __init start_kernel(void)
{
    char *command_line;
    char *after_dashes;

    /*
     * Need to run as early as possible, to initialize the
     * lockdep hash:
     */
    lockdep_init();
    set_task_stack_end_magic(&init_task);
    smp_setup_processor_id();
    debug_objects_early_init();

    /*
     * Set up the the initial canary ASAP:
     */
    boot_init_stack_canary();

まず、lockdep_init(kernel/locking/lockdep.c)では、ハッシュテーブルを初期化します。
lockdepの詳しい説明は、`Documentation/locking/lockdep-design.txt'に記載されています。
この文章を読むと、lockdepがLock-class(spinlockやmutex)の状態を
検証するためのデバッグツールである事が分かります。
Kconfigで取捨選択されそうな機能ではありますが、最初に初期化されているのが面白いですね。
(頻繁に使用される仕組みかもしれません。オーバーヘッドがそれなりに発生しそうな仕組みですけど)
 
 
次のset_task_stack_end_magic(&init_task)は、以下のように定義されています。

kernel/fork.c
void set_task_stack_end_magic(struct task_struct *tsk)
{
    unsigned long *stackend;

    stackend = end_of_stack(tsk);
    *stackend = STACK_END_MAGIC;    /* for overflow detection */
}
init/init/task.c
struct task_struct init_task = INIT_TASK(init_task);

引数であるinit_taskは、include/linux/sched.hで宣言され、
その実体は上記のように定義されています。
プロセスの実体として動的に生成・消滅するtask_struct構造体の中で、
init_taskは静的に割り当てられ、常にメモリ上にグローバル変数として存在します。
何故、このようなタスクが必要か必要なのか調査したところ、
循環式リストのアンカー・ポイントとしてinit_taskを利用するようです。
例:(始点)init_task > task_A > task_B > …… > (終点)init_task
そして、今回の場合は、end_of_stack(include/linux/sched.h)で、
スタックの上限値(アドレス)を返します。
 
 
次のsmp_setup_processor_id(init/main.c)は、処理がありません(コールされた後、直ぐリターンする)。
前回説明したアーキテクチャ依存部で、プロセッサ情報を取得したからでしょうか。
この関数の意図が不明です。
 
 
次のdebug_objects_early_initは、以下のように定義されています。

lib/debugobjects.c
struct debug_bucket {
    struct hlist_head   list;
    raw_spinlock_t      lock;
};
static struct debug_bucket  obj_hash[ODEBUG_HASH_SIZE];

void __init debug_objects_early_init(void)
{
    int i;

    for (i = 0; i < ODEBUG_HASH_SIZE; i++)
        raw_spin_lock_init(&obj_hash[i].lock);

    for (i = 0; i < ODEBUG_POOL_SIZE; i++)
        hlist_add_head(&obj_static_pool[i].node, &obj_pool);
}

raw_spin_lock_init(include/linux/spinlock.h)では、raw_spinlock_t構造体のメンバ変数を初期化します。
ここでスピンロックとは、マルチプロセッサシステムの同期問題を解決するための仕組みです。
他のプロセスがロックを掛けて使用している資源が解放されるまで、短いループを繰り返しながら待機します。
ここでのスピンロックは資源(変数)の読み書き用ではなく、
"lifetime debugging of objects(オブジェクトの生存期間のデバッグ)"に用いられるようです。
 
 
次のboot_init_stack_canary()は、以下のように定義されています。

arch/arm/include/asm/stackprotector.h
static __always_inline void boot_init_stack_canary(void)
{
    unsigned long canary;

    /* Try to get a semi random initial value. */
    get_random_bytes(&canary, sizeof(canary));
    canary ^= LINUX_VERSION_CODE;

    current->stack_canary = canary;
    __stack_chk_guard = current->stack_canary;
}

セキュリティ対策として、current(現在実行中のプロセス)のスタックに、カナリアコードを設定します。
一般的に、スタックには引数、リターンアドレスなどが格納されますが、
この内容を第三者が書き換える事ができれば、任意コードを実行する事ができます。
例えば、関数の処理が終了した場合はスタックからリターンアドレスを取り出しますが、
このアドレスが関係のない関数へのアドレスだった場合、システムが想定外の挙動を起こします。
これを防ぐために(スタックの書き換えを検知するために)、
カナリアコード(ランダム値)をスタックに仕込んで、スタックが書き換えられていないかを確認します。

今回、私はこの処理を読んで、初めてカナリアによるセキュリティ対策を知りました。
カナリアコードは、炭鉱作業などの現場で毒ガスの有無をチェックをする際に、
カナリアが毒気に反応してくれる事に由来するそうです。何か可哀想。

疑問点

・オブジェクトにスピンロックを使用する例。

次回

cgroupの初期化からページアドレスの初期化までを説明しています。

参考

Function Attributes - Using the GNU Compiler
linuxカーネルが提供するリストの使い方について

14
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
11