Vengineerの戯言

人生は短いけど、長いです。人生を楽しみましょう!

Apple M1で Linux をロードするまでのステップはどうなっているのかを調べてみた

@Vengineerの戯言 : Twitter
SystemVerilogの世界へようこそすべては、SystemC v0.9公開から始まった 

はじめに

昨日のブログ、「Apple M1でLinuxがnativeに動くようになってきた眺めてみた」では、Apple M1 で Linux が native で動くようになったのをソースコードを眺めてみました。ポイントとなる2点、1)、CPU の enable_methodと2)、割り込みコントローラ(AIC)、について深堀しました。P.S として、macOS (iOS/iPadOS)では、Page sizeが16KBだったので、Linuxではどのようになっているかを調べてみたら、iommu では Page size が 16KB になっているのを確認できました。このことにより、Apple M1 で Linux がまともに動くようになったら、Intel CPU よりも速くなると思います(GPUも対応しないといけないので、Intel CPUのLinuxを超えるにはかなり先になるとは思います)

vengineer.hatenablog.com

今日のブログでは、Linuxが動く前について調べていきます。

ARM64 で Linux が立ち上がるまでのステップ

一般的なARM64なCPUコア(2コア-8コア)を搭載したSoCでは、ARM64なCPUコアは Cortex-AシリーズのCPUです。また、SoCの中には Cortex-AシリーズのCPUだけでなく、Cortex-Mシリーズのマイコンを搭載しています。このCortex-Mシリーズのマイコンは、Cortex-AシリーズのCPUを起動する前の初期設定や LinuxのPSCI(省エネ)の処理などをしています。

このようなSoCで Linux が立ち上がるまでのステップは、次のようになっています。

  • Cortex Mシリーズのマイコンが立ち上がり、いろいろな初期設定をする。この時、Cortex-AシリーズのCPUのリセット後のアクセスするアドレスを設定し、Cortex Mシリーズのマイコンが Cortex-AシリーズのCPU(1コアだけ)をリセットから解除する
  • Cortex AシリーズのCPUコア(1コアだけ)がリセット解除後、設定されたアドレスからプログラムをロードし、次のようなステップで処理を行います。
    • ARM Trauted Firmware (EL1/EL2/EL3) を実行する (EL3が u-Boot をメモリにロードし、u-Boot の先頭アドレスにジャンプする)
    • u-Boot を実行する (u-Boot が Linux:Image と dtb をメモリにロードし、Linux:Imageの先頭アドレスにジャンプする)
    • Linux を実行する

Apple M1 で Linux が立ち上がるまでのステップ

Apple M1では、一般的なARM64なCPUコアを搭載のSoCと違って、

などは利用していません。その代わりに、corellium は、preloader-m1 というプログラムを使って、Linux (Imageとdtb)をメモリにロードして、Linuxを起動しています。

では、preloader-m1 というプログラムは、どのようにして起動されるのでしょうか? preloader-m1 の README.txt には kmutilというコマンドで preloader-m1 (test.machoというファイル)をつぎのようにして、HDに登録するようです。

  kmutil configure-boot -c test.macho -v /Volumes/Macintosh HD/

kmutil コマンドは、Installing a Custom Kernel Extensionに書いてあるようなので、こちらを読んでください。

kmutil コマンドでHDに登録される test.macho は、preloader-m1 の Makefile の中を見てみると、

linux.macho: machopack preboot.bin Image apple-m1-j274.dtb
	./machopack $@ preboot.bin@0x803040000 Image@0x803080000 apple-m1-j274.dtb@0x803060000

のように、macopack コマンドで、preboot.bin、Image、apple-m1-j274.dtb から linux.macho (上の test.macho になるのかな)を生成しています。
macopack コマンドは、このディレクトリの machopack.c から生成するっぽいです。

preloader-m1 の中身

ここでは、preloader-m1 の中身を追っていきます。ただし、preloader-m1 のポイントを見ていきます。

上記の macopack コマンドで linux.macho ファイルにパックした preboot.bin、Image、apple-m1-j274.dtb の中の preboot.bin が Linux (Imageとdtbファイル)を起動するためのプログラムです。preboot.bin は、

preboot.bin: preboot.elf
	$(AA64)objcopy -Obinary $^ $@

preboot.elf: preboot.o preboot-c.o tunable.o dtree-dict.o dtree.o adtree.o libc.o printf.o unscii-16.o preboot.ld
	$(AA64)gcc -Wl,-T,preboot.ld -Wl,--build-id=none -nostdlib -o $@ preboot.o preboot-c.o tunable.o dtree-dict.o dtree.o adtree.o printf.o libc.o unscii-16.o

のように、生成されています。

preboot.bin プログラムは、以下のように、preboot.o (preboot.S) の entry から始まります。

    .align 8
.global entry
entry:
    b start

entry から直ぐに、start にジャンプしています。

start:
    mov x24, x0				<= x0 には、bootargsのアドレス(loader_mainの第3引数)

になっていて、x0 レジスタの値を x24 に設定しています。x0 レジスタの値は、preboot.bin プログラムを起動した時の第一引数の値になります。この第一引数は、下記の iphone_boot_args構造体へのポインタになっています。なんでこのような構造体になっているかはここでは説明しません。

struct iphone_boot_args {
    uint16_t revision;
    uint16_t version;
    uint64_t virt_base;
    uint64_t phys_base;
    uint64_t mem_size;
    uint64_t top_of_kernel;
    struct {
        uint64_t phys, display, stride;
        uint64_t width, height, depth;
    } video;
    uint32_t machine_type;
    uint64_t dtree_virt; 
    uint32_t dtree_size;
    char cmdline[BOOT_LINE_LENGTH];
};

次に、フレームバッファになんかを書き込んでいます。x24 が上記の構造体のポインタなので、x24 + 0x048 の値 [x24, #0x40] が width, x24 + 0x48 の値 [x24, #0x48]が height になり、fill_rect にジャンプしています。

    mov x0, #0
    mov x1, #0
    ldr x2, [x24, #0x40]		<= 上記の width
    ldr x3, [x24, #0x48]		<= 上記の height
    mov w4, #0x0000001f
    bl fill_rect				<= Frame buffer に描画

fill_rect は、次のようになっています。x0 (0), x1 (0), x2 (width), x3(height), w4(0x1f) になっています。x8 に phys, x9 に stride になり、x8 (phys) から始まるメモリに x2(width)*x3(height) の範囲に w4(x12, 0x1f)を書き込んでいます。フレームバッファに、0x1f を書き込んでいるんだと思います。

fill_rect:
    ldr x8, [x24, #0x28]
    ldr x9, [x24, #0x38]
    mov w12, w4
    mul x10, x1, x9
    add x8, x8, x0, lsl #2
    add x8, x8, x10
    mov x10, x3
1:  mov x11, x2
2:  str w12, [x8], #4
    sub x11, x11, #1
    cbnz x11, 2b
    add x8, x8, x9
    sub x8, x8, x2, lsl #2
    sub x10, x10, #1
    cbnz x10, 1b
    ret

fill_rect の後は、UART の初期化 です。

    mov x1, #0x35200000
    orr x1, x1, #0x200000000	<= x1 : 0x2_3520_0000

x1 に設定している 0x2_3520_0000 は、apple-m1-j274.dtsのchosenで指定されている値と同じで、UARTのレジスタのアドレスになります。

   chosen {
        bootargs = "earlycon=apple_uart,0x235200000 console=tty0";
    };

apple-m1-j274.dts の uart0 のアドレスを見ると、0x235200000 になってるので確認できます。

       uart0: serial@235200000 {
            compatible = "apple,uart";
            reg = <0x2 0x35200000 0x0 0x4000>;
            interrupts = <0 605 4>;
            clocks = <&refclk24mhz>;
            clock-names = "refclk";
            index = <0>;
        };

その後に、uartのレジスタの初期設定を行っています。

    mov x0, #3
    str w0, [x1]				<= 0x2_3520_0000に、0x3 (REG_ULCON_WORD_8)を write
    mov x0, #5
    str w0, [x1, #4]			<= 0x2_3520_0004に、0x5 (REG_UCON_TX_MODE_PIO|REG_UCON_RX_MODE_PIO)を write
    str wzr, [x1, #0xc]		<= 0x2_3520_000cに、0x0 を write
    mov x0, #0xc
    str w0, [x1, #28]			<= 0x2_3520_001cに、0xc を write
    mov x0, #0x3
    str w0, [x1, #8]			<= 0x2_3520_0008に、0x3 (REG_UFCON_TXF_RESET|REG_UFCON_RXF_RESET)を write
    mov x0, #0x1
    str w0, [x1, #0xc]		<= 0x2_3520_000cに、0x1 (REG_UMCON_RTS)を write

コメントに追記した () 内のマクロ値は、Linuxのuartドライバ(apple-uart.c)の中で次のように定義しています。

#define REG_ULCON                       0x000
#define REG_UCON                        0x004
#define REG_UFCON                       0x008
#define REG_UMCON                       0x00c
#define REG_UTRSTAT                     0x010
#define REG_UERSTAT                     0x014
#define REG_UFSTAT                      0x018
#define REG_UMSTAT                      0x01c

UARTの初期化の後では、0x2_3d2b_0000 (0x2_3d2b_001c)へに 0x0 を書いているのですが、このアドレスが何かまではこの時点ではわかっていません。

    mov x0, #0x3d2b0000
    orr x0, x0, #0x200000000	<= x0 : 0x2_3d2b_0000
    str wzr, [x0, #0x1c]		<= 0x2_3d2b_001cに、0x0 を write

ここまでにUARTの初期化が終わったので、Imageとdtbをメモリに読み、load_mainにて dtb内の情報から rvbar の値を設定します。

    mov x0, #0x800000000
    orr x0, x0, #0x80000000		<= x0 : 0x8_8000_0000  (Memory)
    sub x0, x0, #16
    mov sp, x0				<= sp : 0x8_7fff_ffe0	 (Stack)
    adr x0, smpentry			<= x0smpentry のアドレスを設定
    add x0, x0, #0x20000		<= x0 : smpentry + 0x2_0000 (dtbが入っている)
    mov x1, x24				<= x1 : x0 (struct iphone_boot_args *bootargs)
    adr x2, smpentry			<= x2 : smpentry
    adr x3, rvbar			<= x3 : rvbar
    bl loader_main			load_main(x0, x1, x2, x3)

loader_main は、preloader-m1/preboot-c.c at main · corellium/preloader-m1 · GitHub で定義されています。

void loader_main(
void *linux_dtb, 
struct iphone_boot_args *bootargs, 
uint64_t smpentry, 
uint64_t rvbar)

loader_mainの最後に次のような感じで Image と linux_dtb をメモリに書き込みます。

    printf("Loader complete, relocating kernel...\n");
    dt_write_dtb(linux_dt, linux_dtb, 0x20000);
    dt_free(linux_dt);

loader_main に戻ってきたら、途中に、ちょこっと何かやっていて、cpuinit にて、CPUコアを初期化します。

    bl cpuinit

CPUコアの初期化が終わったら、いよいよ、Linux にジャンプします。Linux (Image)の先頭アドレスは、x18(0x8_8008_0000)です。Linuxへの第一引数(x0)は、dtbが書き込まれている先頭アドレス(0x8_8006_0000)です。最後に、br x18 で、Linuxにジャンプします。

    mov x0, #0x800000000	<= ここから Linux にジャンプするコード
    orr x0, x0, #0x80000000
    add x0, x0, #0x60000		<= x0 0x8_8006_0000 (dtbのアドレス)
    mov x1, #0				<= x1, x2, x3 は、引数だけど、0を設定
    mov x2, #0
    mov x3, #0
    add x18, x0, #0x20000	<= x18 : 0x8_8008_0000 (Imageにjump)
    br x18					(だたし、Makefileでは、0x803080000 になっている)

おわりに

上記のように、preboot.bin の中で、初期化し、引数として渡された iphone_boot_args の情報から、Linux (Imageとdtb) をメモリにロードし、dtb を読み込んだアドレスを x0 (第一引数)として、Linux(Image)を起動していました。

感想

よくここまで、リバースしたものです。