@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を超えるにはかなり先になるとは思います)
今日のブログでは、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 が立ち上がるまでのステップは、次のようになっています。
Apple M1 で Linux が立ち上がるまでのステップ
Apple M1では、一般的なARM64なCPUコアを搭載のSoCと違って、
- ARM Trusted Firmware
- u-Boot
などは利用していません。その代わりに、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 <= x0 に smpentry のアドレスを設定 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)を起動していました。
感想
よくここまで、リバースしたものです。