以下の内容はhttps://www.valinux.co.jp/blog/entry/20260326より取得しました。


新Linuxカーネル解読室 - Linuxの起動 ~ARM64編~ (1)

「Linuxカーネル2.6解読室」(以降、旧版)出版後、Linuxには多くの機能が追加され、エンタープライズ領域をはじめとする様々な場所で使われるようになりました。
それに伴いコードが肥大かつ複雑化し、多くのエンジニアにとって解読不能なブラックボックスとなっています。
世界中のトップエンジニア達の傑作であるLinuxカーネルにメスを入れ、ブラックボックスをこじ開けて、時に好奇心の赴くままにカーネルの世界を解読する「新Linuxカーネル解読室」プロジェクト。

本稿では、ARM64版Linuxの起動処理について、Linuxカーネルv6.8のコードをベースに解説します。

執筆者:高橋 浩和

※ 「新Linuxカーネル解読室」連載記事一覧はこちら


はじめに

ARM64用Linuxの起動の仕組みを、Linuxカーネルの初期化処理から、initプロセスとinitramfsを用いた初期化の仕組みまで解説します。 説明にあたっては、CPUアーキテクチャはARMv8.2-A、ディストリビューションはUbuntu 24.04LTSであることを前提にします。

今回の記事では、カーネルエントリから始まりinitプロセスを起動するところまでを説明します。

1. Linux起動処理の全体の流れ

Bootプログラム

Bootプログラムが、Linuxカーネルイメージ/デバイスツリー/initramfsイメージをメモリ上に展開し、カーネルエントリ(Linuxカーネルイメージの先頭)に制御を移します。この時、デバイスツリーをロードしたメモリアドレスをx0レジスタに設定してLinuxカーネルに渡します。initramfsイメージ(initrd.img)をロードしたアドレスは、Bootプログラムがデバイスツリーに登録する必要があります。カーネル起動引数も同様にデバイスツリー内に登録しておく必要があります。

LinuxカーネルイメージにはもうひとつのカーネルエントリであるEFIエントリがあります。これについては、「11. EFI対応」の節で説明します。

カーネルエントリの処理

Bootプログラムは、ロードしたLinuxカーネルイメージの先頭に制御を移します。 Linuxカーネルは1つのコア(Bootコア)のみで動作を開始します(primary_entry関数)。 ARM64 Linuxカーネルは、実行モードがEL2またはEL1、物理アドレスモード(MMUオフ)で呼び出されることを前提に実装されています。ARM64のCPUモードについては、「おわりに」節で簡単に説明しています。

primary_entry関数はアセンブリコードで記述されており、CPUコアの初期化を行ないます。そのうち下記の処理が重要です。

  1. 仮想空間を立ち上げる(MMUオン)
  2. Virtualization Host Extensions (VHE)を有効にする*1
  3. start_kernel関数呼び出し

カーネル基本機能の初期化

スレッドを動かすために必要となるカーネルデータとハードウェアの初期化を行ないます(start_kernel関数)。下記に、重要な初期化処理のフローを載せます。

start_kernel
   ├── setup_arch     // ARM64アーキテクチャ依存の初期化
   │       ├── setup_machine_fdt    // 暫定メモリアロケータ準備
   │       ├── arm64_memblock_init     // 暫定メモリアロケータ準備
   │       ├── paging_init    // 全物理メモリをカーネル空間にマップ
   │       └── unflatten_device_tree    // バイナリ形式のデバイスツリー情報をツリー形式に展開
   ├── vfs_caches_init    // VFSの初期化。この処理の延長で空のtmpfsをrootfsとしてマウントします。
   ├── mm_core_init    // メモリ管理機能の初期化
   ├── sched_init    // プロセススケジューラの初期化
   ├── init_IRQ    // 割り込み機能の初期化
   ├── init_timers     // タイマ機能の初期化
   ├── hrtimers_init    // ハイレゾタイマ機能の初期化
   ├── console_init    // コンソール初期化
   ├── fork_init    // ここまで初期化処理を行ってきたコンテキストがpid 0タスク(swapper)のコンテキストとなる
   └── arch_call_rest_init
            └── rest_init
                     ├── pid 1のスレッド生成、全てのユーザモードスレッドの始祖となる。
                     ├── kthreaddスレッド生成、全てのカーネルスレッドの親となる。
                     └── cpu_startup_entry      // idle状態になる

スレッドによる初期化

pid 1のスレッドはkernel_init関数を実行します。カーネル基本機能の初期化が完了しているため、物理メモリアロケータ、タイマー処理など活用した初期化処理を行なうことができます。

kernel_init
   ├── kthreaddスレッド生成完了を待ち合わせる。この後の処理でカーネルスレッド生成を行なうため。
   ├── kernel_init_freeable
   │       ├── workqueue_init      // workqueue初期化
   │       ├── do_pre_smp_initcalls      // early_initcallで登録された初期化関数実行
   │       ├── smp_init      // マルチコアの起動
   │       ├── do_basic_setup    
   │       │       └──do_initcalls     // 各種機能やデバイスドライバの初期化関数呼び出し
   │       │               └── populate_rootfs     // vfs_caches_initで用意したrootfsの中にinitramfsを展開する
   │       └── console_on_rootfs      // /dev/consoleを標準入出力、エラー出力用にopen
   ├── free_initmem      // カーネル初期化処理専用の関数や変数が置かれていたメモリ領域を解放
   └── initプロセスを起動

initプロセスによる初期化

initramfsをrootとして、initプロセスが起動します。 initプロセスは、本当のrootデバイスをmountするための準備を行ないます。 必要となるデバイス用のドライバモジュールのカーネルへの組み込み、RAIDやLVMの構築などを行なった後、rootファイルシステムの切替えを行ないます。 その後、本当のrootデバイス上にあるsystemdに処理を引き継ぎます。

systemdによる初期化

systemdは、様々なサービスの立ち上げを行ないます。systemdが参照するスクリプトには依存関係が記述されており、systemdは依存関係が壊れない範囲で並列に処理を実行します。

2. 仮想記憶の立ち上げ

物理アドレスモードで動作を開始したLinuxカーネルは、仮想記憶モードに切り替えながら立ち上がってきます。仮想空間の生成は数段階に分けて行われます。

仮想空間の生成

最終的に必要とするカーネル仮想メモリ空間を即座に生成することはできず、暫定的なカーネル仮想メモリ空間を経由して、少しずつ仮想空間を生成します。

下記にカーネル仮想空間構築のフローを説明します。 カーネル仮想空間構築時にデバイスツリーをマップする空間も作成するのですが、それについては「3. デバイスツリー」の節で説明します。

  1. 物理アドレスモード

    物理アドレスモードでprimary_entryから実行を開始します。

  2. カーネル領域の暫定マップ(第一段階)

    __primary_switch関数にてカーネルイメージがロードされている物理アドレス領域を、仮想アドレスと物理アドレスが同じアドレスになるようマップした同一マッピング空間(下図の① )を生成します(Identity Mapと呼びます)。その後、MMUを有効にします。MMUを有効にした直後から仮想空間上に見えている__primary_switch関数の命令を実行するようになります。

  3. カーネル領域の暫定マップ(第二段階)

    カーネルコンパイル時に割り付けを想定している仮想アドレス空間(下図の② )にも同じメモリ域をマップします。新たにマップした仮想空間上に見える__primary_switched関数に制御を移し、そこからstart_kernel関数を呼び出します。

  4. 物理メモリ領域の正式マップ

    暫定マップの段階では、カーネル領域のマップのみを行ないました。暫定マップを行なう段階では、搭載メモリの情報がないためです。 start_kernel関数から呼び出されるARM64依存初期化関数setup_archの延長で、全物理メモリ領域をカーネル仮想空間に線形マップします(paging_init関数)。搭載物理メモリ領域の情報はデバイスツリーから取得します。

セキュリティ対策

Linuxカーネルを起動する度にカーネルが割り付くアドレスが変わるように設定することにより、攻撃耐性を上げることができます(CONFIG_RANDOMIZE_BASEコンフィギュレーション)。 この機能を使うには、カーネルイメージがリロケータブルオブジェクトとしてコンパイルされている必要があります(--picオプションを指定)。

「カーネル領域の暫定マップ(第二段階)」を行なうときに、乱数(RNDR_EL0レジスタ)を用いてカーネル割り付けアドレスを決定します。 カーネルオブジェクトは遠い距離にあるデータ参照や関数呼び出しを行なうときに間接参照用のテーブルを用いますが、このテーブルは.relaセクションに登録されています。 Linuxカーネルは「カーネル領域の暫定マップ(第二段階)」を行なうときに.relaセクションに登録されているテーブルを、カーネル割り付けアドレスに対応するように全て修正します(__relocate_kernel関数)。

3. デバイスツリー

ACPIを搭載するハイエンドサーバを除き、殆どのARM64搭載システムでは、デバイスツリーを用いてハードウェアの構成情報をLinuxカーネルに渡します。 デバイスツリーには、搭載CPUの情報、搭載メモリの情報、各種搭載コントローラの構成情報が載っています。更にソフトウエアに対する設定情報などもここに記述することができます。Linuxカーネル起動引数や、initrd.img用に確保したメモリ域の情報などもここに記述します。

暫定マップ(第一段階)時のデバイスツリー

暫定マップ(第一段階)の仮想空間を生成するとき、カーネル空間の直後にデバイスツリーが置かれたメモリ域をマップします。 カーネル空間は仮想アドレスと物理アドレスが一致するようにマップ(Identity Map)していますが、デバイスツリー域は一致していません。

現在のARM64 Linuxカーネルは、カーネル割り付けアドレスを決定する時に利用する乱数発生源をRNDR_EL0レジスタではなく、デバイスツリーに記述された値に変更することができます。この時、暫定マップ(第一段階)時のデバイスツリーを通して情報を取得します。

暫定マップ(第二段階)時のデバイスツリー

start_kernel関数の初期にて、デバイスツリーをあらかじめ決められたFixmap領域と呼ばれる固定アドレスにマップします(early_fdt_map関数)。 「物理メモリ領域の正式マップ」が行なわれるまでの間、この空間をデバイスツリーアクセスに利用します。

正式マップ時のデバイスツリー

物理メモリ領域全てをカーネル空間にマップすると、その中にデバイスツリーのメモリ領域も見えるようになります。この後のデバイスツリーのアクセスは、この空間を通して行ないます。

デバイスツリーアクセス

デバイスツリーの情報アクセスを効率化するために、ブートプログラムがロードしたバイナリ形式のデバイスツリーを、カーネル初期化の途中でツリー形式のデータ構造へ変換しています(unflatten_device_tree関数)。

Linuxカーネルは、デバイスツリー情報にアクセスするための標準関数群を用意しています。カーネルの各機能やデバイスドライバは、デバイスツリーの記述を参照しつつ初期処理を進めます。

4. 物理メモリ管理

Linuxカーネルの初期化処理の開始時点では、まだ物理メモリ管理機能が動いていません。 Linuxカーネル標準のメモリアロケータ(Buddyシステム)が使えるようになるまで、暫定的なメモリアロケータmemblockを利用します。*2 Linuxカーネル標準のメモリ管理機能の初期化が完了すると、Linuxカーネル標準のメモリ管理機能がメモリ管理を引き継ぎます。

memblockの構造

単純なメモリ領域管理で空きメモリ管理を実現しています。 管理には、固定長配列のリストを用います。

リスト名 用途
memblock.memory 搭載されている物理メモリを登録するリスト
memblock.reserve 予約済み(使用中)な物理メモリ域を登録するリスト

memblockの初期化

デバイスツリーから搭載メモリ情報を取得し、memblockに登録します(memblock_add関数)。 既に利用しているメモリ領域(カーネル、デバイスツリー、initrd.img域)を予約します(memblock_reserve関数)。

start_kernel
   └── setup_arch
           ├── setup_machine_fdt
           │       ├── memblock_reserve     // デバイスツリー領域の予約
           │       ├── early_init_dt_scan
           │       │      └── memblock_add     // デバイスツリーに定義された全メモリをmemblockに追加
           │       └── "Machine model: Raspberry Pi 5 Model B Rev 1.0" 表示
           ├── arm64_memblock_init
           │        ├── memblock_reserve    // initrd.img領域を予約
           │        └── memblock_reserve    // カーネル領域を予約
           └── paging_init   //全物理メモリのカーネル空間へのマップ

メモリの利用

setup_arch関数呼び出し後、カーネルの初期化関数群は、下記の関数を利用してメモリ確保、解放が行なえるようになります。

関数名 用途
memblock_alloc メモリ確保
memblock_free メモリ解放

標準メモリ管理への移行

Linuxカーネル標準のメモリアロケータの初期化が完了した時点で、メモリ管理をmemblockからLinuxカーネル標準のメモリアロケータに切り替えます(memblock_free_all関数)。

start_kernel
    ├── setup_arch
    │ 
    :     
    └── mm_core_init    //カーネルメモリアロケータの初期化
             ├── build_all_zonelists
             ├── page_alloc_init_cpuhp
             ├── mem_init
             │       └── memblock_free_all    // memblockの空きメモリをカーネルメモリアロケータ(Buddyシステム)に移動
             ├── "Memory: 7729952K/8384512K available (15232K kernel code, 2730K rwdata, 9628K rodata, 6656K init, 895K bss, 326880K reserved, 327680K cma-reserved)" 出力
             ├── kmem_cache_init
             └── vmalloc_init

この後の処理では、alloc_pages関数やkmalloc関数などの、カーネル開発者には馴染みのあるメモリ確保関数を使うことができます。

カーネル初期化処理利用メモリ

関数やデータの属性として、カーネル初期化時のみ利用しカーネル起動後には参照しないデータや関数に下記属性を指定します。 これら属性を指定したデータや関数は、.init.から始まる特別なセクションに配置されます。 Linuxカーネルは初期化処理完了後、.init.から始まるセクションのメモリ領域を解放します(free_initmem関数)。解放したメモリ域は、Linuxカーネル標準の物理メモリ管理機構(Buddyシステム)に、空きメモリとして登録されます。

属性 配置セクション
__init .init.text
__initdata .init.data
__initconst .init.rodata

またメモリ量としては少ないですが、後述する各種initcalls()や__setup()で指定した関数を登録するテーブルの領域も解放されます。

5. マルチコアの起動

マルチコアの起動は、smp_init関数にて行います。 デバイスツリーに登録されたCPUコアの起動を試みます。

kernel_init
   ├── kernel_init_freeable
   :        ├── do_pre_smp_initcalls      // early_initcallで登録された初期化関数実行
   :        ├── smp_init      // マルチコアの起動
            :        ├── idle_threads_inits     // 各コア用のidleスレッドを用意
                     ├── cpuhp_threads_init      // 各コア用にcpu hotplug用スレッド生成
                     ├── "smp: Bringing up secondary CPUs ..." 出力
                     ├── bringup_nonboot_cpu
                     ├── "smp: Brought up 1 node, 4 CPUs" 出力
                     └── smp_cpus_done
                             └──  "Total of 4 processors activated." 出力

bringup_nonboot_cpu関数は、対象のコア固有の管理データを用意した後に、実際にコアの起動を行ないます。

cpuhp_hp_statesテーブルにコアが状態遷移する時に実行する関数が登録されています。 bringup_nonboot_cpu関数はコアを起動する時に、CPUHP_OFFLINE状態からCPUHP_BRINGUP_CPU状態のエントリに登録されているコア起動時の初期化関数を順番に呼び出します。 環境が必要とする場合は、このテーブルのエントリに追加で初期化関数を登録することもできます。

状態 コア起動初期化関数 説明
CPUHP_OFFLINE なし
CPUHP_CREATE_THREADS smpboot_create_threads 起動するコア用のカーネルスレッド(ksoftirqd、irq_workなど)を生成
CPUHP_PERF_PREPARE perf_event_init_cpu 起動するコア用の性能測定イベント用データ構造初期化
CPUHP_RANDOM_PREPARE random_prepare_cpu 起動するコア用の乱数発生器を初期化
CPUHP_WORKQUEUE_PREP workqueue_prepare_cpu 起動するコア用のworkerスレッドを生成する
CPUHP_HRTIMERS_PREPARE hrtimers_prepare_cpu 起動するコアのハイレゾタイマ管理構造を初期化する
CPUHP_SMPCFD_PREPARE smpcfd_prepare_cpu 他コアに関数実行させるためのデータ構造初期化
CPUHP_RELAY_PREPARE relay_prepare_cpu 起動するコアのrelayfs用データ構造初期化
CPUHP_RCUTREE_PREP rcutree_prepare_cpu 起動するコアのrcuデータ構造初期化
CPUHP_TIMERS_PREPARE timers_prepare_cpu 起動するコアのローカルタイマ管理構造を初期化する
CPUHP_BRINGUP_CPU bringup_cpu 実際のコア起動要求を出す

最後に呼び出されるbringup_cpu関数は、物理的にコアの起動要求を出します。 PSCI*3をサポートするファームウェアを搭載している環境では、PSCIのインターフェイスを通してコアの起動要求を行ないます。コア起動要求を行なう時、コアの起動アドレス(物理アドレス)として、secondary_entry関数のアドレスを指定します。 デバイスツリーのenable-methodプロパティには"psci"が指定されています。

                cpu@1 {
                        device_type = "cpu";
                        compatible = "arm,cortex-a76";
                        reg = <0x100>;
                        enable-method = "psci";
                            :
                };

新たに起動したコア(セカンダリコア)は、secondary_entry関数から動作を開始します。

secondary_entry
     ├── CPUコア初期化、MMU on
     └── secondary_start_kernel
             ├── 起動したコア用のデータ構造初期化
             ├── "CPU1: Booted secondary processor 0x0000000100 [0x414fd0b1]" 表示
             └── cpu_startup_entry     // idle状態になる

6. initcalls

Linuxカーネルが動作するハードウェアや環境に合わせて、デバイスドライバやサブシステムの初期化関数を登録することができます。 Linuxカーネルは、これら初期化関数を登録する汎用の仕組みを用意しています。

下記テーブルにあるマクロ で登録した関数は、initcall専用のセクションに登録されます。 Linuxカーネルは、初期化時にこのセクションに登録された関数群を呼び出します。

do_initcallsで呼び出される初期化関数には優先度があり、優先度の高いものから順番に呼び出されます。 次の表において、上段にあるマクロで登録した関数が先に呼び出されます。

初期化関数登録マクロ 呼び出し関数 呼び出しタイミング
console_initcall(fn) console_initcalls start_kernel関数にて、基本機能の初期化時に呼び出す
early_initcall(fn) pre_smp_initcalls kernel_init関数にて、マルチコア起動直前に呼び出す
pure_initcall(fn) do_initcalls kernel_init関数にて、マルチコア起動後に呼び出す
core_initcall(fn) do_initcalls 同上
core_initcall_sync(fn) do_initcalls 同上
postcore_initcall(fn) do_initcalls 同上
postcore_initcall_sync(fn) do_initcalls 同上
arch_initcall(fn) do_initcalls 同上
arch_initcall_sync(fn) do_initcalls 同上
subsys_initcall(fn) do_initcalls 同上
subsys_initcall_sync(fn) do_initcalls 同上
fs_initcall(fn) do_initcalls 同上
fs_initcall_sync(fn) do_initcalls 同上
rootfs_initcall(fn) do_initcalls 同上
device_initcall(fn) do_initcalls 同上
device_initcall_sync(fn) do_initcalls 同上
late_initcall(fn) do_initcalls 同上
late_initcall_sync(fn) do_initcalls 同上

モジュール可能なサブシステムがカーネルに静的に組み込まれた場合、モジュールの初期化関数指定マクロmodule_initは、device_initcallに置き換えられます。

汎用initcallで登録された初期化関数では、workqueueを使った非同期での初期化処理、他コア上のスレッドによる初期化処理などを記述することが可能です。

start_kernel
   ├── setup_arch    
   ├── console_init    // console_initcallで登録された初期化関数呼び出し
   ├── fork_init    
   └── arch_call_rest_init
            └── rest_init
                     └── pid 1のスレッド生成、全てのユーザモードスレッドの始祖となる。

kernel_init
   ├── kernel_init_freeable
   │       ├── do_pre_smp_initcalls      // early_initcallで登録された初期化関数実行
   │       ├── smp_init      // マルチコアの起動
   │       └── do_basic_setup    
   │               └──do_initcalls      // その他汎用initcallで登録された初期化関数呼び出し
   └── initプロセスを起動

7. カーネル起動引数

Linuxカーネルの起動時に引数を渡すことで、カーネルの動作を動的に変更することが可能です。 ARM64 Linuxカーネルの場合、デバイスツリーの chosen ノード内にある bootargs プロパティにこれらを記述し、カーネルへ渡します。

chosen {
        bootargs = "reboot=w console=ttyAMA0,115200 rootfstype=ext4 rootwait";
        stdout-path = "serial10:115200n8";
             :
             :
};

カーネル起動引数に対応するセットアップ・ハンドラ

Linuxカーネルには、特定の起動引数文字列に対応する関数を登録し、実行する仕組みがあります。この関数は一般にセットアップ・ハンドラ(Setup Handler)と呼ばれます。 また、この仕組みを応用して、カーネル内の変数に初期値を直接代入する機構も提供されています。

マクロ名 用途 呼び出しタイミング
__setup(str, fn) 引数 str に対応するハンドラ fn を登録
early_param(str, fn) 早期解析が必要な引数 str に対応するハンドラ fn 登録 可能な限り早い段階で呼び出す
module_param(name, type, perm) 指定した type 型変数 name に引数の値を直接代入する __setupで登録したハンドラと同じタイミングで処理される
start_kernel
   ├── setup_arch     // ARM64アーキテクチャ依存の初期化
   │       ├── setup_machine_fdt 
   │       ├── parse_early_param      // early_paramで登録した関数実行
   │       ├── arm64_memblock_init
   │       ├── paging_init 
   │       └── unflatten_device_tree  
   ├── parse_early_param     // setup_archから呼び出されていない場合、ここでearly_paramで登録した関数実行
   ├── parse_args("Booting kernel", ...)      // __setupで登録した関数実行
   :
   :

8. コンソール設定

標準コンソール

コンソールとして利用可能なコントローラのデバイスドライバ(UART等)は、console_initcall マクロを使って登録されます。

console_initcall(univ8250_console_init);

ただし、登録されたすべてのデバイスが有効になるわけではありません。起動引数の console= で指定されたデバイスのみが、正式な出力先として選択されます。

console= を解釈するハンドラ console_setup は、以下のように定義されています。

__setup("console=", console_setup);

__setupで登録されていますが、"console="の解釈は特別にparse_early_param関数で行なうようになっています。 本来 __setup は parse_args("Booting kernel", ...) で処理されるものですが、console= は特別です。 起動後少しでも早い段階で出力先を確定させるため、early_param と同等のタイミングで優先的に解析されるようハードコーディングされています。 なお、console_setup 自体はデバイスの初期化(有効化)は行わず、「どのデバイスを優先して使うか」の予約のみを行います。実際に文字が出力されるのは、その後の console_init 関数が完了した後となります。

earlycon

標準のコンソールドライバが立ち上がる(console_init)よりもさらに前、カーネル起動の極初期からログを出力するための仕組みが earlycon です。 earlycon の初期化ハンドラ param_setup_earlycon は、次のように early_param マクロを使って定義されています。

early_param("earlycon", param_setup_earlycon);

実際の各UARTコントローラ用ドライバは、以下のようなマクロで個別に定義されます。param_setup_earlycon関数は、引数で指定された名前に一致するドライバ(例:ns16550)を探し出し、そのセットアップ関数を呼び出します(この場合は、early_serial8250_setup関数)。

OF_EARLYCON_DECLARE(ns16550, "ns16550", early_serial8250_setup);

early_serial8250_setup などの関数は、ポーリングモード(割り込みを使わない単純な転送)で動作するようにコントローラを最小限の設定で初期化します。 これにより、early_param 解析直後から printk による文字列出力が可能になります。ただし、一文字ずつ送信完了を待つ必要があるため、システム全体の起動速度に対しては一定のオーバーヘッドが生じるという側面もあります。

コンソール有効化の前

Linuxカーネルはコンソール出力とは別に、カーネル内に確保したログバッファにも出力文字列を書き込んでいます。 これはコンソールが利用可能になる前から行っています。コンソールの初期化が完了したタイミングで、ログバッファに溜まっていた出力文字列を一気にコンソールに吐き出します。

9. initramfs

rootfsのマウント

VFSの初期化処理の延長で、rootfsがマウントされます。initramfsとtmpfsが有効になっているカーネルでは、tmpfsがrootfsとしてmountされます。 ただし、この時点でmountしたrootfsの中身は空です。ファイルは何も置かれていません。

start_kernel
   ├── vfs_caches_init_early
   │      ├── dcache_init_early
   │      └── inode_init_early
   └── vfs_caches_init
           └── mnt_init
                   ├── shmem_init
                   ├── init_root_fs
                   └── init_mount_tree
                           ├── vfs_kern_mount(&rootfs_fs_type, ...)       //rootfs_fs_typeにはtmpfsが登録されている
                           │      └── fs_context_for_mount
                           │               └── alloc_fs_context
                           │                        └── rootfs_init_fs_context
                           │                                └── shmem_init_fs_context  //tmpfsの初期化。shmemとコードを共有している。
                           └── set_fs_root      // カレントスレッドのrootfsとして登録


file_system_type rootfs_fs_type = {
    .name      = "rootfs",
    .init_fs_context = rootfs_init_fs_context,    //tmpfsが組み込まれている時は、shmem_init_fs_contextを呼び出す
    .kill_sb         = kill_litter_super, 
}

initramfsイメージの展開

Linuxカーネルはデバイスツリーの情報から、initrd.imgが置かれているメモリ領域の場所を知ります。

chosen {
                bootargs = "reboot=w coherent_pool=1M 8250.nr_uarts=1 pci=pcie_bus_safe";
                stdout-path = "serial10:115200n8";
                linux,initrd-start = <0x00000000 0x80000000>;
                linux,initrd-end   = <0x00000000 0x81000000>;
 };

initcallsの一つとして、populate_rootfs関数が登録されています。

rootfs_initcall(populate_rootfs);

この関数はrootfs中にinitrd.img(cpio+圧縮形式)を展開する処理を行ないます。 initrd.imgの展開処理はworkqueueを利用して非同期に実行します。

実行が始まると、"Trying to unpack rootfs image as initramfs..." が出力され、完了するとinitrd.imgが置かれていたメモリ領域を解放することを知らせる "Freeing initrd memory: 63656K" が出力されます。

kernel_init
   ├── kernel_init_freeable
   │       ├── do_basic_setup    
   │       │       └── do_initcalls      // 各種機能やデバイスドライバの初期化関数呼び出し
   │       │                └── populate_rootfs      //tmpfsの中にinitramfsを展開する
   │       ├── wait_for_initramfs     // initramfsの展開完了待ち合わせ
   │       └── console_on_rootfs      // /dev/consoleを標準入出力、エラー出力用にopen
   ├── free_initmem      // カーネル初期化処理専用の関数や変数が置かれていたメモリ領域を解放
   └── initプロセスを起動

10. initプロセス起動

kernel_init関数を実行してきたのはpid 1のスレッドです。 このスレッドは、/dev/consoleを標準入出力としてopenし(console_on_rootfs関数)、その後initramfs上にあるinitをexecします(run_init_process関数)。

kernel_init
   ├── kernel_init_freeable
   │       └── console_on_rootfs      // /dev/consoleを標準入出力、エラー出力用にopen
   └── try_to_run_init_process
           └── run_init_process        // initプロセスを起動(execする)

後の初期化処理は、initプロセスに任せます。

11. EFI対応

ARM64 Linuxカーネルイメージは、それ自体がEFIアプリケーションの形式(PE/COFF)に準拠しており、EFIファームウェアから直接ロード可能です*4。これはEFIスタブと呼ばれる機能によるものです。 EFI用のエントリから呼び出されたLinuxカーネル(EFIスタブ)は、起動引数*5に指定されたdtb=やinitrd=を解析し、デバイスツリーとinitrd.imgをUEFI Boot Services(ブートサービス)のAPIを利用してメモリ上に読み込みます。 その後、ExitBootServicesを呼び出してEFIのモードをブートサービスからランタイムサービスのモードに切り替え*6、標準のLinuxカーネルエントリであるprimary_entryを呼び出します。

おわりに

ARMv8.2-Aアーキテクチャで動くLinuxは、CPUのモードを以下のように利用しています。

ARMv8.2-Aは、4つの実行レベル(EL3、EL2、EL1、EL0)と、それらと直交するモードとしてsecure/non-secureというモードを用意しています。 secureと呼ばれるモードは、ファームウェアとTrusted Application(鍵管理などを行なう)のみが利用するため、Linuxカーネルからはあまり気にする必要はありません。 KVMを組み込んだLinuxカーネルはEL2で動かすことを前提としています。KVM上のゲストOSはEL1を利用します。ベアメタル上のOSとして動かす場合はEL1/EL2のどちらでも動作可能です。 また、過去の32bit ARMとの互換性のため、AArch32モードがサポートされています。AArch32モードを使うことによりゲストOSとして過去のARM32用Linuxカーネルを動かすこともできますし*7、ARM64用Linuxカーネル上でARM32用のアプリケーションを動かすこともできます。

Exception Level 用途
EL0 アプリケーション
EL1 Linuxカーネル
EL2 Linuxカーネル+KVM
EL3 ファームウェア

更に、ARMv8.2-AはVHE(Virtualization Host Extensions)と呼ばれる機能を備えており、この機能を有効にするとEL1用に作られたLinuxカーネルを、EL2モードの中で動かすことが可能となります。ARM64 Linuxカーネルは、VHEが使えるときはカーネル起動直後に有効にしています。

次回予告

initプロセス起動以降の初期化処理は、次の記事で解説します。少々お待ちください。

*1:EL1モード用に記述されたコードをEL2モードでも実行できるようになります。またVHEを有効にするとEL2でもEL1と同じ広さの仮想空間を使えるようになります。

*2:古くはbootmemという仕組みを使っていましたが、アーキテクチャ毎の別実装になっていました。Linux kernel v4.17において、これを汎用的なmemblockに切り替えました。

*3:Power State Coordination Interface: CPUコアやシステムの電源状態を制御するための標準インターフェイス

*4:残念ながら、手元で動かしているRaspberry Pi 5のファームウェアはEFI対応していません。

*5:EFIファームウェアには、EFIアプリケーションに引数を渡す機能があります。

*6:ハードウェアの制御権をファームウェアからLinuxカーネルへ引き渡すために必要です。

*7:CPUモデルによってはサポートしていません




以上の内容はhttps://www.valinux.co.jp/blog/entry/20260326より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14