目次: Yocto
Yocto Scarthgap(5.0.1)のメモです。前回同様、コードを読んで依存関係を調べるのは大変時間が掛かるので、簡易的な調査手法として全ファイルを消してエラーを観察し、エラーの原因となるファイルを復活させて次ステップに進める手段を用います。
引き続きBitbakeを実行してエラーを見て、足りないファイルを元に戻していくいう方法で各パーツや設定の動作を見ていきたいと思います。
$ bitbake core-image-sato ERROR: OE-core's config sanity checker detected a potential misconfiguration. Either fix the cause of this error or at your own risk disable the checker (see sanity.conf). Following is the list of potential problems / advisories: DISTRO 'poky' not found. Please set a valid DISTRO in your local.conf
新たなディストリビューション(DISTRO, distribution)なる用語が出てきました。Yoctoのドキュメント(Creating Your Own Distribution - The Yocto Project)には特に用語の説明がないのですが、一般的なLinuxディストリビューションと同じ意味、つまり「Linuxカーネルと周辺ソフトウェアを1つにまとめたもの」を意味していると思われます。
ビルドディレクトリbuild/conf/local.confに下記の記述があり、デフォルトのディストリビューションはpokyとなります。
## build/conf/local.conf
...(略)...
# Default policy config
#
# The distribution setting controls which policy settings are used as defaults.
# The default value is fine for general Yocto project use, at least initially.
# Ultimately when creating custom policy, people will likely end up subclassing
# these defaults.
#
DISTRO ?= "poky"
ちなみに環境変数による設定は上書きできます。DISTROに限らず他の変数も同様です。
$ DISTRO=something bitbake core-image-sato ERROR: OE-core's config sanity checker detected a potential misconfiguration. Either fix the cause of this error or at your own risk disable the checker (see sanity.conf). Following is the list of potential problems / advisories: DISTRO 'something' not found. Please set a valid DISTRO in your local.conf
Yoctoのドキュメントによれば、conf/distroの下にある"distro名.conf"がdistribution configuration fileなので、poky用ならばconf/distro/poky.confが該当します。探すとmeta-poky/conf/distroの下にpoky.confがありました。このファイルも元に戻します。
$ bitbake core-image-sato ERROR: Unable to parse /home/katsuhiro/share/projects/oss/poky/bitbake/lib/bb/parse/parse_py/BBHandler.py Traceback (most recent call last): File "/home/katsuhiro/share/projects/oss/poky/bitbake/lib/bb/parse/parse_py/BBHandler.py", line 71, in inherit(files=['poky-sanity'], fn='configuration INHERITs', lineno=0, d=<bb.data_smart.DataSmart object at 0x7fb98c543910>, deferred=False): if not os.path.exists(file): > raise ParseError("Could not inherit file %s" % (file), fn, lineno) bb.parse.ParseError: ParseError in configuration INHERITs: Could not inherit file classes/poky-sanity.bbclass
再びクラスが見つからないと言われています。どうもpokyディストリビューションだけで使っているクラスがあるようですので、該当するファイルも合わせて元に戻します。
今回登場したファイル、ディレクトリは、
こんなとこ。次回はレシピを見ます。
目次: Linux
以前、Berkeley Bootloader(riskv-pkのbbl)を使用してRISC-V Linuxを立ち上げました(2019年2月26日の日記参照)が、今回はOpenSBIを使ってLinuxの起動を試みようと思います。
前回との違いはブートローダをbblからOpenSBIに変更したことです。
前回同様なので省略します。
前回同様にbusyboxと手作業でinitramfsを作りますが、ビルドディレクトリを使うように手順を変更しています。バージョンは1.35.0を使います(タグ名: 1_35_0)。
$ git clone https://git.busybox.net/busybox $ cd busybox $ mkdir build $ make \ O=build \ ARCH=riscv \ CROSS_COMPILE=riscv64-unknown-linux-gnu- \ menuconfig - Settings ---> [*] Build static binary (no shared libs) $ make \ O=build \ ARCH=riscv \ CROSS_COMPILE=riscv64-unknown-linux-gnu- \ all
ビルドに成功すると実行ファイルbusyboxが生成されるはずです。次にinitramfsを作成します。基本的な流れはディレクトリに必要なファイルを配置し、cpioで固めるだけです。
$ mkdir initramfs-work-riscv $ mkdir initramfs-work-riscv/root $ cd initramfs-work-riscv/root $ mkdir bin $ cp ../../build/busybox bin/ $ ln -s busybox bin/sh $ ln -s bin/busybox init $ mkdir dev $ sudo cp -a /dev/tty* dev/ $ find . | cpio --format=newc -o > ../initramfs.cpio
横着してホストマシンのデバイスファイルをコピーしてます。RISC-V Linux上で何か操作したければinit, sh以外のシンボリックリンクも作った方が便利です。
前回との違いは下記のとおりです。linux-nextでも手順は同じですが、linux-nextはたまにデグレして動作せず話がややこしくなるので、linux本線の少し古めのバージョンを使います。
バージョンは6.9を使います(タグ名: v6.9)。
$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git $ cd linux $ mkdir build $ make \ O=build \ ARCH=riscv \ CROSS_COMPILE=riscv64-unknown-linux-gnu- \ defconfig $ make \ O=build \ ARCH=riscv \ CROSS_COMPILE=riscv64-unknown-linux-gnu- \ menuconfig - General setup ---> (../busybox/initramfs-work-riscv/initramfs.cpio) Initramfs source file(s) $ make \ O=build \ ARCH=riscv \ CROSS_COMPILE=riscv64-unknown-linux-gnu- \ all
前回のようにexportで環境変数を設定しても良いですし、makeに逐一指定しても良いです。
ブートローダーにはOpenSBIを使います。バージョンは1.5を使います(タグ名: v1.5)。
$ git clone https://github.com/riscv-software-src/opensbi.git $ cd opensbi $ make \ CROSS_COMPILE=riscv64-unknown-linux-gnu- \ PLATFORM=generic \ FW_PAYLOAD_PATH=../linux/build/arch/riscv/boot/Image \ clean all
BBLもそうでしたけど、ブートローダーとカーネルは1つのバイナリになります。FW_PAYLOAD_PATHにはカーネルのバイナリ(vmlinuxではない)のパスを指定します。PLATFORMはSoCやボードの種類を指定します、QEMU向けの場合はgenericです。
エミュレータにはQEMUを使います。システムにインストールされているバージョンが古い場合があるので、ソースコードからビルドする方法も紹介します。QEMU実行時にオプション--netdev userを使うのであれば、QEMUをビルドする前にlibslirp-devをインストールしておく必要があります。
$ cd qemu $ mkdir build $ cd build $ ../configure \ --target-list=riscv32-softmmu,riscv32-linux-user,riscv64-softmmu,riscv64-linux-user \ --enable-debug \ --disable-docs $ ninja
バージョンはv9.0.2を使いました。target-listやdisable-docsはビルド時間の短縮のためで指定しなくても良いです。使わないアーキテクチャ用(ARMとか)のエミュレータがビルドされるためビルドに若干時間がかかります。enable-debugはQEMUをデバッグする可能性があれば指定してください。
ここまでの準備でブートローダーとLinuxカーネルの初期部分は確認できるはずですので、動作確認します。
$ cd qemu/build $ ./qemu-system-riscv64 \ -machine virt \ -nographic \ -bios none \ -chardev stdio,id=con,mux=on \ -serial chardev:con \ -mon chardev=con,mode=readline \ -smp 4 \ -kernel ../../opensbi/build/platform/generic/firmware/fw_payload.bin
うまくいっていればこんな感じになるはずです。
OpenSBI v1.5 ____ _____ ____ _____ / __ \ / ____| _ \_ _| | | | |_ __ ___ _ __ | (___ | |_) || | | | | | '_ \ / _ \ '_ \ \___ \| _ < | | | |__| | |_) | __/ | | |____) | |_) || |_ \____/| .__/ \___|_| |_|_____/|____/_____| | | |_| Platform Name : riscv-virtio,qemu Platform Features : medeleg Platform HART Count : 4 Platform IPI Device : aclint-mswi Platform Timer Device : aclint-mtimer @ 10000000Hz Platform Console Device : uart8250 Platform HSM Device : --- Platform PMU Device : --- Platform Reboot Device : syscon-reboot Platform Shutdown Device : syscon-poweroff Platform Suspend Device : --- Platform CPPC Device : --- Firmware Base : 0x80000000 Firmware Size : 357 KB Firmware RW Offset : 0x40000 Firmware RW Size : 101 KB Firmware Heap Offset : 0x4f000 Firmware Heap Size : 41 KB (total), 2 KB (reserved), 11 KB (used), 27 KB (free) Firmware Scratch Size : 4096 B (total), 416 B (used), 3680 B (free) Runtime SBI Version : 2.0 Domain0 Name : root Domain0 Boot HART : 0 Domain0 HARTs : 0*,1*,2*,3* Domain0 Region00 : 0x0000000000100000-0x0000000000100fff M: (I,R,W) S/U: (R,W) Domain0 Region01 : 0x0000000010000000-0x0000000010000fff M: (I,R,W) S/U: (R,W) Domain0 Region02 : 0x0000000002000000-0x000000000200ffff M: (I,R,W) S/U: () Domain0 Region03 : 0x0000000080040000-0x000000008005ffff M: (R,W) S/U: () Domain0 Region04 : 0x0000000080000000-0x000000008003ffff M: (R,X) S/U: () Domain0 Region05 : 0x000000000c400000-0x000000000c5fffff M: (I,R,W) S/U: (R,W) Domain0 Region06 : 0x000000000c000000-0x000000000c3fffff M: (I,R,W) S/U: (R,W) Domain0 Region07 : 0x0000000000000000-0xffffffffffffffff M: () S/U: (R,W,X) Domain0 Next Address : 0x0000000080200000 Domain0 Next Arg1 : 0x0000000082200000 Domain0 Next Mode : S-mode Domain0 SysReset : yes Domain0 SysSuspend : yes Boot HART ID : 0 Boot HART Domain : root Boot HART Priv Version : v1.12 Boot HART Base ISA : rv64imafdch Boot HART ISA Extensions : sstc,zicntr,zihpm,zicboz,zicbom,sdtrig,svadu Boot HART PMP Count : 16 Boot HART PMP Granularity : 2 bits Boot HART PMP Address Bits: 54 Boot HART MHPM Info : 16 (0x0007fff8) Boot HART Debug Triggers : 2 triggers Boot HART MIDELEG : 0x0000000000001666 Boot HART MEDELEG : 0x0000000000f0b509 [ 0.000000] Linux version 6.9.0 (katsuhiro@blackbird) (riscv64-unknown-linux-gnu-gcc (GCC) 14.1.0, GNU ld (GNU Binutils) 2.42.50.20240622) #1 SMP Tue Jul 16 18:44:37 JST 2024 [ 0.000000] random: crng init done [ 0.000000] Machine model: riscv-virtio,qemu [ 0.000000] SBI specification v2.0 detected [ 0.000000] SBI implementation ID=0x1 Version=0x10005 [ 0.000000] SBI TIME extension detected [ 0.000000] SBI IPI extension detected [ 0.000000] SBI RFENCE extension detected [ 0.000000] SBI SRST extension detected [ 0.000000] SBI DBCN extension detected [ 0.000000] efi: UEFI not found. ...
今後はOpenSBIやLinuxカーネルの動作を調べる予定です。
目次: Arduino
Debian Testingをアップデートしたところ先日ビルドしたKiCad 7.0が動かなくなり、今回はビルドすらできなくなりました。cmakeを実行するとTKIGESライブラリが見つからないと言われてしまいます。
(略) -- Found the following HarfBuzz libraries: -- HarfBuzz (required): /usr/lib/x86_64-linux-gnu/libharfbuzz.so -- Found HarfBuzz: /usr/include/harfbuzz (found version "8.3.0") -- Found Fontconfig: /usr/lib/x86_64-linux-gnu/libfontconfig.so (found version "2.15.0") -- Checking for module 'ngspice' -- Found ngspice, version 42 -- Found ngspice: /usr/include -- Found OCC: /usr/include/opencascade (found version "7.8.1") *** OpenCascade library missing *** Could not find a library for TKIGES at /usr/lib/x86_64-linux-gnu Verify your OpenCascade installation or pass CMake the library directory as '-DOCC_LIBRARY_DIR=<path>' CMake Error at cmake/FindOCC.cmake:192 (message): Call Stack (most recent call first): CMakeLists.txt:807 (find_package) -- Configuring incomplete, errors occurred!
エラーメッセージさん曰く、OpenCascadeのライブラリが見つからないとのこと。バージョンを確認するとDebian StableはOpenCascad 7.6.3、Debian TestingはOpenCascade 7.8.1でした。この間にlibTKIGES.soのライブラリ名が変わったようです。
OpenCascadeのソースコード(リポジトリへのリンク)のコミットログを見ていると、ライブラリ名が一気に変わった瞬間がありました。
commit bd651bbbd9e30fc5a73d32d11e0ea1a1821afd76 Author: dpasukhi <dpasukhi@opencascade.com> Date: Sun Nov 19 11:09:33 2023 +0000 0033531: Configuration - Rework DataExchange ToolKits organization Integrated DE plugin functionality. Reworked DE components: - TKDESTEP: Handling STEP file format. - TKDEOBJ: Handling OBJ file format. - TKDEIGES: Handling IGES file format. - TKDEGLTF: Handling GLTF file format. - TKDEVRML: Handling VRML file format. - TKDEPLY: Handling PLY file format. - TKDESTL: Handling STL file format. Reworked DE DRAW components: TKXSDRAWSTEP: Container for DE command to work with STEP. TKXSDRAWOBJ: Container for DE command to work with OBJ. TKXSDRAWIGES: Container for DE command to work with IGES. TKXSDRAWGLTF: Container for DE command to work with GLTF. TKXSDRAWVRML: Container for DE command to work with VRML. TKXSDRAWPLY: Container for DE command to work with PLY. TKXSDRAWSTL: Container for DE command to work with STL. TKXSDRAW rework to be base DRAW plugin to keep DE session and utils. Updated documentation Updated samples
TKIGES以外にも盛大に変わっていて、OpenCascadeを使っている人たちは全滅しているんじゃないかな?と思います。どこで変わったか調べるとv7.7.2とv7.8.0の間で変わったようですね。
$ git log V7_7_2..V7_8_0 | grep bd651b commit bd651bbbd9e30fc5a73d32d11e0ea1a1821afd76
コミットの中身を参考にしつつKiCadを修正します。
diff --git a/cmake/FindOCC.cmake b/cmake/FindOCC.cmake
index af249c9ce2..923ccb4c7a 100644
--- a/cmake/FindOCC.cmake
+++ b/cmake/FindOCC.cmake
@@ -45,7 +45,6 @@ set( OCC_LIBS
TKGeomAlgo
TKGeomBase
TKHLR
- TKIGES
TKLCAF
TKMath
TKMesh
@@ -55,18 +54,16 @@ set( OCC_LIBS
TKPrim
TKService
TKShHealing
- TKSTEP209
- TKSTEPAttr
- TKSTEPBase
- TKSTEP
- TKSTL
TKTObj
TKTopAlgo
TKV3d
- TKVRML
+ TKDESTL
+ TKDEVRML
TKXCAF
- TKXDEIGES
- TKXDESTEP
+ TKDE
+ TKDECascade
+ TKDEIGES
+ TKDESTEP
TKXMesh
TKXmlL
TKXml
OpenCascadeやKiCadの中身を全くわかっていないので、APIや実装が大きく変わっていたらお手上げでした。しかし幸いなことにライブラリ名が変わっただけなので、OpenCascadeのライブラリを探しているcmake/FindOCC.cmakeの修正だけで乗り切れました。
Debian TestingのKiCadは8.0系に移行しましたし、そろそろKiCad 7.0系を引きずるのをやめてバージョンアップを考えないといけないかなあ……?
目次: Linux
OpenSBIのブート部分を調べます。OpenSBIはいくつか動作モードがあるのですが、今回はペイロードを持ったタイプを調べます。対象のファイル名はbuild/platform/generic/firmware/fw_payload.binもしくは.elfです。
OpenSBIのエントリポイントは_startで、アセンブラ実装のコードです。
//opensbi/firmware/fw_base.S
_start
_try_lottery
_bss_zero
fw_save_info() // firmware/fw_payload.S
//何もしない
fw_platform_init() // platform/generic/platform.cだが独自の実装もある(k210など)
_scratch_init
//scratch領域を確保する、先頭アドレスはtpレジスタに格納される
fw_next_arg1() // firmware/fw_payload.S
// FW_PAYLOAD_FDT_ADDRが定義済みならFW_PAYLOAD_FDT_ADDRを返す
// FW_PAYLOAD_FDT_OFFSETが定義済みなら_fw_start + FW_PAYLOAD_FDT_OFFSETを返す
// いずれも未定義ならばa1レジスタの値を返す
// 結果はscratch->next_arg1に格納
fw_next_addr() // firmware/fw_payload.S
// payload_binのアドレスを返す
// FW_PAYLOAD_PATHが定義済みなら.incbin FW_PAYLOAD_PATH
// 未定義ならwfi命令の無限ループ
// 結果はscratch->next_addrに格納
fw_next_mode() // firmware/fw_payload.S
// PRV_Sを返す
// 結果はscratch->next_modeに格納
fw_options()
// 0を返す
// 結果はscratch->optionsに格納
_start_warm
// tpの値をmscratchレジスタへ
_reset_regs
// mscratchレジスタを第一引数(a0レジスタ)にしてsbi_init()を呼ぶ
sbi_init(struct sbi_scratch *scratch)
結構長いですが、ブートに必要な情報を扱っている部分を抜粋してコメントを付け加えました。
ブート処理で重要なデータがstruct sbi_platform platformです。これは各platformごとに定義されていて、QEMU用のバイナリであればgeneric/platform.cの定義が使われるはずです。fw_platform_init()関数にて初期化されます。
//opensbi/platform/generic/platform.c
struct sbi_platform platform = {
.opensbi_version = OPENSBI_VERSION,
.platform_version =
SBI_PLATFORM_VERSION(CONFIG_PLATFORM_GENERIC_MAJOR_VER,
CONFIG_PLATFORM_GENERIC_MINOR_VER),
.name = CONFIG_PLATFORM_GENERIC_NAME,
.features = SBI_PLATFORM_DEFAULT_FEATURES,
.hart_count = SBI_HARTMASK_MAX_BITS,
.hart_index2id = generic_hart_index2id,
.hart_stack_size = SBI_PLATFORM_DEFAULT_HART_STACK_SIZE,
.heap_size = SBI_PLATFORM_DEFAULT_HEAP_SIZE(0),
.platform_ops_addr = (unsigned long)&platform_ops
};
Cの構造体をアセンブラのコードから参照するときはちょっと面倒で、
こんな処理を行う必要があります。Cの構造体を書き換えるとアセンブラ側で定義しているオフセットとずれてしまう問題があるため、
//opensbi/include/sbi/sbi_platform.h
/**
* Prevent modification of struct sbi_platform from affecting
* SBI_PLATFORM_xxx_OFFSET
*/
_Static_assert(
offsetof(struct sbi_platform, opensbi_version)
== SBI_PLATFORM_OPENSBI_VERSION_OFFSET,
"struct sbi_platform definition has changed, please redefine "
"SBI_PLATFORM_OPENSBI_VERSION_OFFSET");
(...以下同様に続く...)
全てのオフセットマクロに対してアサーションが書かれています。sbi_platform構造体が頻繁に変わることはないでしょうし、メンバもせいぜい10個くらいなので、手書き&気合で乗り切れます。
こういうオフセットマクロがあまりにも多い場合は、Cのコードからオフセットマクロを自動生成する場合もあります。OSのコードでたまに見かけますね。
もうひとつ重要なデータはスクラッチ領域(データ型はstruct sbi_scratch)です。アセンブラのコード_scratch_initにて領域の確保と値の設定が行われ、Cのコードではscratch->aaaaのような形で参照します。
まず領域の確保ですが、OpenSBIのバイナリの終端部分から下記のようにスタックとヒープ領域が確保されます。
スクラッチ領域はスタック領域の末尾に確保されます。またtpレジスタはスクラッチ領域の先頭アドレスとなります。この値は後でsbi_init()関数の第一引数に渡すために使います。
OpenSBIからペイロード(今回はLinuxをペイロードにしています)へ制御を移す部分のコードを調べます。
sbi_init()
init_coldboot() //1コアだけ : lib/sbi/sbi_init.c
sbi_AAAAAA_init() //初期化関数の名前はこんな感じ
//
//初期化、メッセージ表示など
//
sbi_hsm_hart_start_finish()
hsm_start_ticket_release()
sbi_hart_switch_mode()
// mepc = next_addr : payload_binのアドレス
// a0 = arg0 = hartid
// a1 = arg1 = next_arg1 : FDTのアドレス
// mretでmepcに飛ばす、つまりペイロードであるLinuxに飛ぶ
init_warmboot() //他のコア : lib/sbi/sbi_init.c
...
いろんな処理を行ないますが、ペイロードへ制御を移す部分はsbi_hsm_hart_start_finish()とsbi_hart_switch_mode()関数です。
//opensbi/lib/sbi/sbi_hsm.c
void __noreturn sbi_hsm_hart_start_finish(struct sbi_scratch *scratch,
u32 hartid)
{
unsigned long next_arg1;
unsigned long next_addr;
unsigned long next_mode;
struct sbi_hsm_data *hdata = sbi_scratch_offset_ptr(scratch,
hart_data_offset);
if (!__sbi_hsm_hart_change_state(hdata, SBI_HSM_STATE_START_PENDING,
SBI_HSM_STATE_STARTED))
sbi_hart_hang();
next_arg1 = scratch->next_arg1;
next_addr = scratch->next_addr;
next_mode = scratch->next_mode;
hsm_start_ticket_release(hdata);
sbi_hart_switch_mode(hartid, next_arg1, next_addr, next_mode, false); //★制御を移す関数★
}
//opensbi/lib/sbi/sbi_hart.c
void __attribute__((noreturn))
sbi_hart_switch_mode(unsigned long arg0, unsigned long arg1,
unsigned long next_addr, unsigned long next_mode,
bool next_virt)
{
#if __riscv_xlen == 32
unsigned long val, valH;
#else
unsigned long val;
#endif
switch (next_mode) {
case PRV_M:
break;
case PRV_S:
if (!misa_extension('S'))
sbi_hart_hang();
break;
case PRV_U:
if (!misa_extension('U'))
sbi_hart_hang();
break;
default:
sbi_hart_hang();
}
val = csr_read(CSR_MSTATUS);
val = INSERT_FIELD(val, MSTATUS_MPP, next_mode);
val = INSERT_FIELD(val, MSTATUS_MPIE, 0);
#if __riscv_xlen == 32
if (misa_extension('H')) {
valH = csr_read(CSR_MSTATUSH);
valH = INSERT_FIELD(valH, MSTATUSH_MPV, next_virt);
csr_write(CSR_MSTATUSH, valH);
}
#else
if (misa_extension('H'))
val = INSERT_FIELD(val, MSTATUS_MPV, next_virt);
#endif
csr_write(CSR_MSTATUS, val);
csr_write(CSR_MEPC, next_addr);
if (next_mode == PRV_S) {
if (next_virt) {
csr_write(CSR_VSTVEC, next_addr);
csr_write(CSR_VSSCRATCH, 0);
csr_write(CSR_VSIE, 0);
csr_write(CSR_VSATP, 0);
} else {
csr_write(CSR_STVEC, next_addr);
csr_write(CSR_SSCRATCH, 0);
csr_write(CSR_SIE, 0);
csr_write(CSR_SATP, 0);
}
} else if (next_mode == PRV_U) {
if (misa_extension('N')) {
csr_write(CSR_UTVEC, next_addr);
csr_write(CSR_USCRATCH, 0);
csr_write(CSR_UIE, 0);
}
}
//★各引数に入っている値は下記の通り★
//arg0 = hartid
//arg1 = scratch->next_arg1
//next_addr = scratch->next_addr
//next_mode = scratch->next_mode
//next_virt = false
register unsigned long a0 asm("a0") = arg0;
register unsigned long a1 asm("a1") = arg1;
__asm__ __volatile__("mret" : : "r"(a0), "r"(a1));
__builtin_unreachable();
}
レジスタa0とa1に引数を入れ、mepcレジスタにペイロード先頭のアドレス(scratch->next_addr)を設定してmretします。
fw_next_addr() // firmware/fw_payload.S
// payload_binのアドレスを返す
// FW_PAYLOAD_PATHが定義済みなら.incbin FW_PAYLOAD_PATH
// 未定義ならwfi命令の無限ループ
// 結果はscratch->next_addrに格納
なぜscratch->next_addrにペイロード先頭のアドレスが入っているのか?については再掲するとともに、該当部分のソースコードを掲載します。
//opensbi/firmware/fw_payload.S
.section .entry, "ax", %progbits
.align 3
.global fw_next_addr
/*
* We can only use a0, a1, and a2 registers here.
* The next address should be returned in 'a0'.
*/
fw_next_addr:
lla a0, payload_bin //★payload_binのアドレスを返す★
ret
...
.section .payload, "ax", %progbits
.align 4
.globl payload_bin
payload_bin:
#ifndef FW_PAYLOAD_PATH //★FW_PAYLOAD_PATHが未定義ならwfi命令の無限ループ★
wfi
j payload_bin
#else
.incbin FW_PAYLOAD_PATH //★FW_PAYLOAD_PATHが定義済みならFW_PAYLOAD_PATHで指定されたファイルをこの場所に展開★
#endif
//opensbi/firmware/fw_base.S
/* Store next address in scratch space */
MOV_3R s0, a0, s1, a1, s2, a2
call fw_next_addr
REG_S a0, SBI_SCRATCH_NEXT_ADDR_OFFSET(tp) //★結果はscratch->next_addrに格納★
MOV_3R a0, s0, a1, s1, a2, s2
最後にFW_PAYLOAD_PATHマクロはどこから来るかを紹介しておきます。
#opensbi/firmware/objects.mk
firmware-bins-$(FW_PAYLOAD) += fw_payload.bin
ifdef FW_PAYLOAD_PATH
FW_PAYLOAD_PATH_FINAL=$(FW_PAYLOAD_PATH)
else
FW_PAYLOAD_PATH_FINAL=$(platform_build_dir)/firmware/payloads/test.bin
endif
firmware-genflags-$(FW_PAYLOAD) += -DFW_PAYLOAD_PATH=\"$(FW_PAYLOAD_PATH_FINAL)\"
ビルド時に指定したFW_PAYLOAD_PATH変数が、Makefile内で-Dオプションとしてコンパイラに渡されてマクロとして定義されます。シンプルですね。
次回以降はメモリマップやデバイスツリーについて調べようと思います。
目次: Linux
OpenSBIのブート部分を調べます。OpenSBIはいくつか動作モードがあるのですが、payloadを持ったタイプを調べます。対象のファイル名はbuild/platform/generic/firmware/fw_payload.binもしくは.elfです。
今回はメモリマップについて調べます。調べている途中で気づいたのですがOpenSBI 1.4と1.5で変わっている部分があるので、その話もしたいと思います。
/* opensbi/firmware/fw_payload.elf.ldS */
OUTPUT_ARCH(riscv)
ENTRY(_start)
SECTIONS
{
#include "fw_base.ldS"
...
/* opensbi/firmware/fw_base.ldS */
. = FW_TEXT_START;
/* Don't add any section between FW_TEXT_START and _fw_start */
PROVIDE(_fw_start = .);
...
バイナリのメモリマップを決めるのはリンカースクリプトfw_payload.elf.ldSです。fw_base.ldSは各モード共通で使われるリンカースクリプトです。FW_TEXT_STARTがメモリマップの開始アドレスとなります。
FW_TEXT_STARTが定義されている場所はMakefileで、
# opensbi/firmware/objects.mk
ifdef FW_TEXT_START
firmware-genflags-y += -DFW_TEXT_START=$(FW_TEXT_START)
else
firmware-genflags-y += -DFW_TEXT_START=0x0
endif
FW_TEXT_STARTが定義されていればその値を使い、未定義ならば0x0です。
QEMU向けにビルドする場合はPLATFORM=genericと指定しました。このときMakefileはplatform/generic/objects.mkが使用されますが、FW_TEXT_STARTの定義がOpenSBI 1.4と1.5で違います。
# platform/generic/objects.mk (OpenSBI 1.4)
# Blobs to build
FW_TEXT_START=0x80000000
FW_DYNAMIC=y
FW_JUMP=y
# platform/generic/objects.mk (OpenSBI 1.5)
# Blobs to build
FW_DYNAMIC=y
FW_JUMP=y
OpenSBI 1.4ではplatform/generic/objects.mkにてFW_TEXT_START=0x80000000が定義されていました。firmware-genflags-yに追加されるオプションは前者(-DFW_TEXT_START=0x80000000)です。
OpenSBI 1.5ではplatform/generic/objects.mkにてFW_TEXT_STARTが定義されないため、firmware-genflags-yに追加されるオプションは後者(-DFW_TEXT_START=0x0)です。
QEMUのアドレス0x0にはメモリがないのになぜこの設定で動作するか?ですが、OpenSBI 1.4ではアドレス固定でしたが、OpenSBI 1.5ではPIE(Position Independent Executable、位置独立実行形式)でビルドされるため、OpenSBI 1.5はロードアドレスがどこであっても動作します。
実験してみるとわかりますが、FW_TEXT_STARTを変な値に変えても動作します。OpenSBI 1.4同様にFW_TEXT_START=0x80000000にしても動作します。
が、変更する意味はあまりないでしょう。私が思いつく範囲だと、gdbでPIEのシンボルを読みたい場合はsymbol-file (file) -o 0x80000000のようにしますが、FW_TEXT_START=0x80000000にしておくとこのコマンドを一々入力せず済む、くらいです。他にも何かあれば教えていただけると嬉しいです。
メモリマップと言いつつエントリアドレスしか説明していなかったので、次回はエントリアドレス以降のメモリマップも調べたいと思います。
目次: Linux
OpenSBIのブート部分を調べます。OpenSBIはいくつか動作モードがあるのですが、payloadを持ったタイプを調べます。対象のファイル名はbuild/platform/generic/firmware/fw_payload.binもしくは.elfです。
今回はメモリマップについて調べます。
前回(2024年7月19日の日記参照)説明したとおり、バイナリのメモリマップを決めるのはリンカースクリプトfw_payload.elf.ldSです。FW_TEXT_STARTがメモリマップの開始アドレスですが、OpenSBI 1.5からはPIEになったためFW_TEXT_START=0x0です。したがって開始アドレスは0x0です。
リンカースクリプトを図に起こすとこんな感じです。
OpenSBIのgeneric向け、payload付きバイナリのメモリマップ
読み取り専用データ(.rodataセクションなど)に続く謎の隙間があります。これはPMP(Physical Memory Protection、RISC-Vのメモリ保護機構)の領域サイズを2のべき乗にするためにわざと空けている隙間です。
/* opensbi/firmware/fw_base.ldS */
/*
* PMP regions must be to be power-of-2. RX/RW will have separate
* regions, so ensure that the split is power-of-2.
*/
. = ALIGN(1 << LOG2CEIL((SIZEOF(.rodata) + SIZEOF(.text)
+ SIZEOF(.dynsym) + SIZEOF(.rela.dyn))));
PROVIDE(_fw_rw_start = .);
/* Beginning of the read-write data sections */
.data :
{
私の環境でビルドした場合は.textや.rodataセクションの合計サイズが0x31bc8でした。この値に最も近い2のべき乗の2^18 = 0x40000が選択されます。
読み書き用データ(.dataセクションなど)の後にも謎の隙間があります。これはペイロードを配置するアドレスがプラットフォームによって固定されているためです。
/* opensbi/firmware/fw_payload.ldS */
#ifdef FW_PAYLOAD_OFFSET
. = FW_TEXT_START + FW_PAYLOAD_OFFSET;
#else
. = ALIGN(FW_PAYLOAD_ALIGN);
#endif
.payload :
{
FW_PAYLOAD_OFFSETが定義されていれば、ペイロードの開始アドレスはFW_TEXT_START + FW_PAYLOAD_OFFSETです。OpenSBI 1.5ではFW_TEXT_STARTは0x0です。FW_PAYLOAD_OFFSETはplatformのMakefileで値が決まっていて、firmwareのMakefileでマクロとして定義されます。
# opensbi/platform/generic/objects.mk
ifeq ($(PLATFORM_RISCV_XLEN), 32)
# This needs to be 4MB aligned for 32-bit system
FW_PAYLOAD_OFFSET=0x400000
else
# This needs to be 2MB aligned for 64-bit system
FW_PAYLOAD_OFFSET=0x200000
endif
# opensbi/firmware/objects.mk
ifdef FW_PAYLOAD_OFFSET
firmware-genflags-$(FW_PAYLOAD) += -DFW_PAYLOAD_OFFSET=$(FW_PAYLOAD_OFFSET)
endif
以前(2024年7月19日の日記参照)_fw_endの後ろの領域をスタック領域やヒープ領域として使っていると説明しました、再掲すると、
この説明を読んで、領域を勝手に使ってよいのか?何か別の領域が続いているのではないか?と疑問に思われた方はするどいです。が、ご安心ください、読み書き用データとペイロードの間の隙間を使っています。
個人的にはFW_PAYLOAD_OFFSETが決め打ちなのが若干気になりますが、0x200000 = 2MBあれば、256hart分(256 x 4KB = 1MB)のスタック領域は余裕で確保できますし、読み取り専用+読み書き用データ領域のサイズもよほど変な実装を加えない限り1MBも行かないでしょう。たぶん……。
目次: Linux
OpenSBIのブート部分を調べます。OpenSBIはいくつか動作モードがあるのですが、payloadを持ったタイプを調べます。対象のファイル名はbuild/platform/generic/firmware/fw_payload.binもしくは.elfです。
今回はOpenSBIを見る前にQEMUの動作、デバイスツリーの扱いについて調べます。実行環境はQEMU RV64 virt machine(qemu-system-riscv64 -machine virt)です。突然QEMUの話をし始めた理由は、OpenSBIはQEMUからデバイスツリーを受け取るからです。OpenSBIを調べる前にQEMUがわかっていた方が理解しやすいです。
QEMUは-dtbオプションにてデバイスツリーファイルを明示的に指定できます。もし-dtbオプションを省略するとHW構成に従ってデバイスツリーを自動生成します。便利ですね。明示的に指定した場合、自動生成した場合いずれであってもQEMUのメモリの何処かにデバイスツリーのデータを展開します。
OpenSBIを起動するときはレジスタa0にhartid、レジスタa1にデバイスツリーのアドレスを渡す決まりがあります。
####opensbi/docs/firmware/fw.md#### OpenSBI Platform Firmwares ========================== OpenSBI provides firmware builds for specific platforms. Different types of firmwares are supported to deal with the differences between different platforms early boot stage. All firmwares will execute the same initialization procedure of the platform hardware according to the platform specific code as well as OpenSBI generic library code. The supported firmwares type will differ in how the arguments passed by the platform early boot stage are handled, as well as how the boot stage following the firmware will be handled and executed. The previous booting stage will pass information via the following registers of RISC-V CPU: * hartid via *a0* register * device tree blob address in memory via *a1* register. The address must be aligned to 8 bytes. (適当訳) OpenSBIは特定のプラットフォーム用のファームウェアビルドを提供します。初期ブートローダーでの 異なるプラットフォーム間の違いに対処するため、いろいろな種類のファームウェアがサポートされています。 全てのファームウェアはプラットフォーム固有のコードとOpenSBIの汎用ライブラリのコードに従って、 プラットフォームHWの同じ初期化処理を実行します(訳注: 何を言いたいかよくわからん……)。 サポートされているファームウェアの種類は、プラットフォームの初期ブートローダーから渡された引数が どのように処理されるかと、ファームウェアに続く起動段階がどのように処理され実行されるかによって異なります。 直前のブートローダーは、次のRISC-V CPUレジスタ経由で情報を渡します: * レジスタa0経由でhartid * レジスタa1経由でメモリ上のデバイスツリーブロブ(訳注: *.dtbのこと)のアドレス、アドレスは8バイトアラインが必要
QEMUの場合はブートROMコード(qemu-system-riscv64 -machine virtの場合はアドレス0x1000付近)がレジスタa1にアドレスを設定します。コードの逆アセンブルはこんな感じです。
0x1000: auipc t0,0x0 //t0 <- 0x1000 0x1004: addi a2,t0,40 //a2 <- 0x1028 0x1008: csrr a0,mhartid //a0 <- hartid 0x100c: ld a1,32(t0) //a1 <- 0x87e00000(アドレス0x1020の値) 0x1010: ld t0,24(t0) //t0 <- 0x80000000(アドレス0x1018の値) 0x1014: jr t0 //0x80000000にジャンプ(OpenSBIの先頭) 0x1000: 0x00000297 0x02828613 0xf1402573 0x0202b583 0x1010: 0x0182b283 0x00028067 0x80000000 0x00000000 0x1020: 0x87e00000 0x00000000 0x4942534f 0x00000000 0x1030: 0x00000002 0x00000000 0x80000000 0x00000000
QEMUはリセットすると必ずブートROMコードから実行開始しますから、常にレジスタa1に0x87e00000(-m 128MBの場合、メモリ量に依存してアドレスが変わる)を設定してからOpenSBIにジャンプすることがわかります。アドレス0x87e00000付近の内容をダンプすると、
0x87e00000: 0xedfe0dd0 0x201c0000 0x38000000 0x341a0000 0x87e00010: 0x28000000 0x11000000 0x10000000 0x00000000 0x87e00020: 0xec010000 0xfc190000 0x00000000 0x00000000 0x87e00030: 0x00000000 0x00000000 0x01000000 0x00000000 0x87e00040: 0x03000000 0x04000000 0x1d000000 0x02000000 0x87e00050: 0x03000000 0x04000000 0x11000000 0x02000000 0x87e00060: 0x03000000 0x0d000000 0x06000000 0x63736972 0x87e00070: 0x69762d76 0x6f697472 0x6d657100 0x03000000
なるほど、このデータはデバイスツリーですね……と思える人は相当デバイスツリー通です。私はわかりませんので、データが何者か調べるため先頭にある謎の値0xedfe0dd0に注目します。
デバイスツリーのドキュメント(5. Flattened Devicetree (DTB) Format)を見るとヘッダは下記のようになっています。先頭のmagicメンバーはマジックナンバーであり、ビッグエンディアンの0xd00dfeedでなければならないと記述されています。
struct fdt_header {
uint32_t magic;
uint32_t totalsize;
uint32_t off_dt_struct;
uint32_t off_dt_strings;
uint32_t off_mem_rsvmap;
uint32_t version;
uint32_t last_comp_version;
uint32_t boot_cpuid_phys;
uint32_t size_dt_strings;
uint32_t size_dt_struct;
};
メモリ上にあった謎の値0xedfe0dd0をビッグエンディアンで解釈すると0xd00dfeedとなり、struct fdt_headerのマジックナンバーと一致しました。このデータはきっとデバイスツリーでしょう。
まとめるとQEMU(qemu-system-riscv64 -machine virt)のブートROMコードは、
このような動作をします。
QEMUが自動生成したデバイスツリーをファイルにダンプする方法をメモしておきます。簡単に言えばQEMU Monitorでdumpdtb (file名)コマンドを実行すればダンプできます。
QEMU Monitorを使うにはいくつか手があります。グラフィック環境が使えるなら-nographicと-monオプションを外し、ウインドウから入力するのが一番早いでしょう。
# グラフィック環境が使えるとき ./qemu-system-riscv64 \ -machine virt \ -bios none \ -chardev stdio,id=con,mux=on \ -serial chardev:con \ -smp 4
こんなウインドウが表示されるはずです。
グラフィック環境がない場合はシリアル出力を一時的に無効化して、-monを標準入出力に振り向けると使えるようです。
# グラフィック環境が使えないとき ./qemu-system-riscv64 \ -machine virt \ -bios none \ -nographic \ -chardev stdio,id=con,mux=on \ -chardev null,id=none,mux=on \ -serial chardev:none \ -mon chardev=con,mode=readline \ -smp 4 QEMU 9.0.2 monitor - type 'help' for more information (qemu) dumpdtb virt.dtb info: dtb dumped to virt.dtb (qemu)
もっとスマートなやり方がありそうですけど……使えればよしとしましょう。
QEMUは起動時にデバイスツリーを自動生成もしくはファイルからロードしてくれることがわかりました。次回はOpenSBIでデバイスツリーをどう扱っているか調べたいと思います。
目次: Linux
OpenSBIのブート部分を調べます。OpenSBIはいくつか動作モードがあるのですが、payloadを持ったタイプを調べます。対象のファイル名はbuild/platform/generic/firmware/fw_payload.binもしくは.elfです。
今回はOpenSBIのデバイスツリー処理について調べます。対象はOpenSBI 1.5 PLATFORM=generic、実行環境はQEMU RV64 virt machine(qemu-system-riscv64 -machine virt)です。
の順で説明したいと思います。
OpenSBIがLinuxを起動するときは、どんなHWなのか伝えるためデバイスツリー(正確に言うとFlattened Devicetree Blob、ファイル名*.dtb)のアドレスを渡します。デバイスツリーのアドレスを渡している箇所のコードはこんな感じです。
//opensbi/lib/sbi/sbi_hart.c
void __attribute__((noreturn))
sbi_hart_switch_mode(unsigned long arg0, unsigned long arg1,
unsigned long next_addr, unsigned long next_mode,
bool next_virt)
{
//...略...
//★各引数に入っている値は下記の通り★
//arg0 = hartid
//arg1 = scratch->next_arg1 -> デバイスツリーのアドレス
//next_addr = scratch->next_addr -> mretのリターンアドレス
//next_mode = scratch->next_mode
//next_virt = false
register unsigned long a0 asm("a0") = arg0;
register unsigned long a1 asm("a1") = arg1;
__asm__ __volatile__("mret" : : "r"(a0), "r"(a1));
__builtin_unreachable();
}
起動ログからもLinuxに渡されている情報が確認できます。
(...略...) Domain0 Name : root Domain0 Boot HART : 0 Domain0 HARTs : 0*,1*,2*,3* Domain0 Region00 : 0x0000000000100000-0x0000000000100fff M: (I,R,W) S/U: (R,W) Domain0 Region01 : 0x0000000010000000-0x0000000010000fff M: (I,R,W) S/U: (R,W) Domain0 Region02 : 0x0000000002000000-0x000000000200ffff M: (I,R,W) S/U: () Domain0 Region03 : 0x0000000080040000-0x000000008005ffff M: (R,W) S/U: () Domain0 Region04 : 0x0000000080000000-0x000000008003ffff M: (R,X) S/U: () Domain0 Region05 : 0x000000000c400000-0x000000000c5fffff M: (I,R,W) S/U: (R,W) Domain0 Region06 : 0x000000000c000000-0x000000000c3fffff M: (I,R,W) S/U: (R,W) Domain0 Region07 : 0x0000000000000000-0xffffffffffffffff M: () S/U: (R,W,X) Domain0 Next Address : 0x0000000080200000 ★scratch->next_addr: mretのリターンアドレス★ Domain0 Next Arg1 : 0x0000000082200000 ★scratch->next_arg1: デバイスツリーのアドレス★ Domain0 Next Mode : S-mode ★scratch->next_mode: Supervisorモードで開始★ Domain0 SysReset : yes Domain0 SysSuspend : yes
次はscratch->next_arg1にデバイスツリーのアドレスを設定する仕組みです。このメンバにはfw_next_arg1()関数が返す値が格納されます。以前(2024年7月19日の日記参照)説明したペイロードの先頭アドレスを取得するfw_next_addr()と仕組みも名前も似ていますね。処理の概要は下記のとおりです。
_scratch_init //scratch領域を確保する、先頭アドレスはtpレジスタに格納される fw_next_arg1() // firmware/fw_payload.S // FW_PAYLOAD_FDT_ADDRが定義済みならFW_PAYLOAD_FDT_ADDRを返す // FW_PAYLOAD_FDT_OFFSETが定義済みなら_fw_start + FW_PAYLOAD_FDT_OFFSETを返す ★この実装が選択される★ // いずれも未定義ならばa1レジスタの値を返す // 結果はscratch->next_arg1に格納
プラットフォームgenericのMakefileを確認するとFW_PAYLOAD_FDT_OFFSETの定義があるので、2番目の実装が選択されます。
# opensbi/platform/generic/objects.mk
FW_JUMP_FDT_OFFSET=0x2200000
FW_PAYLOAD=y
ifeq ($(PLATFORM_RISCV_XLEN), 32)
# This needs to be 4MB aligned for 32-bit system
FW_PAYLOAD_OFFSET=0x400000
else
# This needs to be 2MB aligned for 64-bit system
FW_PAYLOAD_OFFSET=0x200000
endif
FW_PAYLOAD_FDT_OFFSET=$(FW_JUMP_FDT_OFFSET) ★これ★
# opensbi/firmware/objects.mk
ifdef FW_PAYLOAD_FDT_OFFSET
firmware-genflags-$(FW_PAYLOAD) += -DFW_PAYLOAD_FDT_OFFSET=$(FW_PAYLOAD_FDT_OFFSET)
endif
FW_PAYLOAD_FDT_OFFSETの定義は以上のとおりです。デバイスツリーの先頭アドレスを求めるコードはこんな感じです。
//opensbi/firmware/fw_payload.S
fw_next_arg1:
#ifdef FW_PAYLOAD_FDT_ADDR
li a0, FW_PAYLOAD_FDT_ADDR
#elif defined(FW_PAYLOAD_FDT_OFFSET)
lla a0, _fw_start
li a1, FW_PAYLOAD_FDT_OFFSET
add a0, a0, a1 //★_fw_start + FW_PAYLOAD_FDT_OFFSETの値を返すだけ★
#else
add a0, a1, zero
#endif
ret
ちなみに_fw_startは0x80000000、FW_PAYLOAD_FDT_OFFSETは0x2200000ですから、デバイスツリーの先頭アドレスは0x82200000となります。
OpenSBIのgeneric向け、payload付きバイナリのメモリマップ
OpenSBI全体のメモリマップを示すとこんな感じです。
結論からいうとOpenSBIがQEMUが配置したデバイスツリーを0x82200000にコピーします。コピーする理由が気になりますが、ざっと見た限り見当たりませんでした。PMPで保護をかけたいのかなあ?
QEMUのデバイスツリーについては前回(2024年7月23日の日記参照)説明したとおりです。QEMUは自動的にデバイスツリーを作成&配置する機能を持っていてアドレス0x87e00000に配置します(-m 128MBの場合、メモリ量に依存してアドレスが変わる)。メモリマップはこんな感じですね。
プラットフォームgeneric向けにおいて、デバイスツリーの実体は2つあります。デバイスツリーを配置する主体と方法を時系列順に並べると下記のようになります。
コピーを行うコードはこんな感じです。
//opensbi/firmware/fw_base.S
/*
* Relocate Flatened Device Tree (FDT)
* source FDT address = previous arg1
* destination FDT address = next arg1
*
* Note: We will preserve a0 and a1 passed by
* previous booting stage.
*/
beqz a1, _fdt_reloc_done
/* Mask values in a4 */
li a4, 0xff
/* t1 = destination FDT start address */
MOV_3R s0, a0, s1, a1, s2, a2
call fw_next_arg1 //★コピー先アドレスを得る(a0に格納される)★
add t1, a0, zero //★t1: コピー先アドレス★
MOV_3R a0, s0, a1, s1, a2, s2
beqz t1, _fdt_reloc_done //★コピー先アドレスが0ならコピー処理をスキップ★
beq t1, a1, _fdt_reloc_done //★アドレスがコピー元 = コピー先ならコピー処理をスキップ★
/* t0 = source FDT start address */
add t0, a1, zero //★t0: コピー元アドレス★
/* t2 = source FDT size in big-endian */
#if __riscv_xlen > 32
lwu t2, 4(t0)
#else
lw t2, 4(t0)
#endif
/* t3 = bit[15:8] of FDT size */
add t3, t2, zero
srli t3, t3, 16
and t3, t3, a4
slli t3, t3, 8
/* t4 = bit[23:16] of FDT size */
add t4, t2, zero
srli t4, t4, 8
and t4, t4, a4
slli t4, t4, 16
/* t5 = bit[31:24] of FDT size */
add t5, t2, zero
and t5, t5, a4
slli t5, t5, 24
/* t2 = bit[7:0] of FDT size */
srli t2, t2, 24
and t2, t2, a4
/* t2 = FDT size in little-endian */
or t2, t2, t3
or t2, t2, t4
or t2, t2, t5
/* t2 = destination FDT end address */
add t2, t1, t2
/* FDT copy loop */
ble t2, t1, _fdt_reloc_done
_fdt_reloc_again:
REG_L t3, 0(t0) //★デバイスツリーコピーのメインループ★
REG_S t3, 0(t1)
add t0, t0, __SIZEOF_POINTER__
add t1, t1, __SIZEOF_POINTER__
blt t1, t2, _fdt_reloc_again
_fdt_reloc_done:
ラベル_fdt_reloc_againの手前にある処理で、レジスタt0がコピー元アドレス(0x87e00000)、レジスタt1がコピー先アドレス(0x82200000)に設定され、_fdt_reloc_againのループにてコピーします。
OpenSBIのプラットフォームgenericがデバイスツリーのアドレス(レジスタa0)を参照する方法は少なくとも2つあって、レジスタa1の参照と、scratch->next_arg1を参照する方法です。時系列順に並べると、
プラットフォームによっても処理が違います。読んでいてややこしい部分の1つですね……。
デバイスツリーがどこからきてどこへ行くのか?がわかりました。次はデバイスツリーのアドレスを決めるもう1つの実装について調べます。
目次: Linux
OpenSBIのブート部分を調べます。OpenSBIはいくつか動作モードがあるのですが、payloadを持ったタイプを調べます。対象のファイル名はbuild/platform/generic/firmware/fw_payload.binもしくは.elfです。
今回はOpenSBIのデバイスツリー処理について調べます。対象はOpenSBI 1.5 PLATFORM=genericの改造版、実行環境はQEMU RV64 virt machine(qemu-system-riscv64 -machine virt)です。
以前(2024年7月24日の日記参照)説明したデバイスツリーの先頭アドレスを取得する処理の概要は下記のとおりです。
_scratch_init //scratch領域を確保する、先頭アドレスはtpレジスタに格納される fw_next_arg1() // firmware/fw_payload.S // FW_PAYLOAD_FDT_ADDRが定義済みならFW_PAYLOAD_FDT_ADDRを返す // FW_PAYLOAD_FDT_OFFSETが定義済みなら_fw_start + FW_PAYLOAD_FDT_OFFSETを返す // いずれも未定義ならばa1レジスタの値を返す ★改造してこの実装を使ってみる★ // 結果はscratch->next_arg1に格納
プラットフォームgenericを選択すると選択肢2番目の処理が使われます。が、MakefileからFW_PAYLOAD_FDT_OFFSETの定義を削除すると、3番目の処理を選択できます。
無理やり改造しているので全てが完璧に動作するかわからないですが、少なくともLinuxのブート部分まで到達しますから、OpenSBIの動作を調べるには十分でしょう。Makefileを下記のように改変します。
# opensbi/platform/generic/objects.mk
ifeq ($(PLATFORM_RISCV_XLEN), 32)
# This needs to be 4MB aligned for 32-bit system
FW_PAYLOAD_OFFSET=0x400000
else
# This needs to be 2MB aligned for 64-bit system
FW_PAYLOAD_OFFSET=0x200000
endif
FW_PAYLOAD_FDT_OFFSET=$(FW_JUMP_FDT_OFFSET) ★この行を削除する★
FW_PAYLOAD_FDT_ADDRもFW_PAYLOAD_FDT_OFFSETも未定義にしたので、3番目の処理が選択されます。
//opensbi/firmware/fw_payload.S
fw_next_arg1:
#ifdef FW_PAYLOAD_FDT_ADDR
li a0, FW_PAYLOAD_FDT_ADDR
#elif defined(FW_PAYLOAD_FDT_OFFSET)
lla a0, _fw_start
li a1, FW_PAYLOAD_FDT_OFFSET
add a0, a0, a1
#else
add a0, a1, zero //★a1レジスタの値を返すだけ★
#endif
ret
なぜa1レジスタにデバイスツリーの先頭アドレスが格納されるか?については、以前の(2024年7月23日の日記参照)QEMUのデバイスツリーの説明をご参照ください。
レジスタa1の値はすぐに壊れてしまいそうに思えますが、OpenSBIはレジスタa1の値をずっと維持し続けるため実装を工夫しています。
// opensbi/firmware/fw_base.S
/* Store next address in scratch space */
MOV_3R s0, a0, s1, a1, s2, a2 //★a0, a1, a2の値をs0, s1, s2に退避★
call fw_next_addr //★C言語の関数呼び出しでa0, a1, a2レジスタが壊れる★
REG_S a0, SBI_SCRATCH_NEXT_ADDR_OFFSET(tp)
MOV_3R a0, s0, a1, s1, a2, s2 //★a0, a1, a2の値を復帰★
C言語で実装した関数を呼び出すなど、レジスタa1の値が壊れるとわかっている場合は壊れる前に別レジスタに値を退避し、関数から戻ってきた後に復帰処理を行います。
この改造を行うとデバイスツリーのコピーは行われず、ペイロードにQEMUが生成&配置したデバイスツリーの先頭アドレスが渡されます。メモリマップはこんな感じです。
動作ログも確認しましょう。
Domain0 Name : root Domain0 Boot HART : 3 Domain0 HARTs : 0*,1*,2*,3* Domain0 Region00 : 0x0000000000100000-0x0000000000100fff M: (I,R,W) S/U: (R,W) Domain0 Region01 : 0x0000000010000000-0x0000000010000fff M: (I,R,W) S/U: (R,W) Domain0 Region02 : 0x0000000002000000-0x000000000200ffff M: (I,R,W) S/U: () Domain0 Region03 : 0x0000000080040000-0x000000008005ffff M: (R,W) S/U: () Domain0 Region04 : 0x0000000080000000-0x000000008003ffff M: (R,X) S/U: () Domain0 Region05 : 0x000000000c400000-0x000000000c5fffff M: (I,R,W) S/U: (R,W) Domain0 Region06 : 0x000000000c000000-0x000000000c3fffff M: (I,R,W) S/U: (R,W) Domain0 Region07 : 0x0000000000000000-0xffffffffffffffff M: () S/U: (R,W,X) Domain0 Next Address : 0x0000000080200000 Domain0 Next Arg1 : 0x0000000087e00000 ★0x82200000 → 0x87e00000に変化した★ Domain0 Next Mode : S-mode Domain0 SysReset : yes Domain0 SysSuspend : yes
QEMUが渡してきたアドレス0x87e00000がそのまま使われています。
デバイスツリーのもう1つの扱い方についてわかりました。しかしデバイスツリーを起動時に生成&配置できるのはQEMUのようなエミュレータだけです。実機ではできませんから、何か別の方法が採用されているはずです。次回は実機でのデバイスツリーの扱いについて調べたいと思います。
目次: Linux
OpenSBIのブート部分を調べます。OpenSBIはいくつか動作モードがあるのですが、payloadを持ったタイプを調べます。対象のファイル名はbuild/platform/generic/firmware/fw_payload.binもしくは.elfです。
今回はデバイスツリーについて調べます。対象はOpenSBI 1.5です。
QEMUの場合は、QEMU起動時にデバイスツリーがロードされ、ブートコードによってレジスタa1にアドレス(-m 128Mの場合0x87e00000、メモリ量オプションによって変わる)が設定されます。実機はこのような動作はしないので、メモリがまっさらな状態からOpenSBIとデバイスツリーをどう配置すれば動くか、その方法についても調べます。
QEMU向けの場合はfw_platform_init()がレジスタa1を参照しますので、ブート時にa1の設定が必要です。
//opensbi/firmware/fw_base.S
_bss_zero:
REG_S zero, (s4)
add s4, s4, __SIZEOF_POINTER__
blt s4, s5, _bss_zero
/* Setup temporary trap handler */
lla s4, _start_hang
csrw CSR_MTVEC, s4
/* Setup temporary stack */
lla s4, _fw_end
li s5, (SBI_SCRATCH_SIZE * 2)
add sp, s4, s5
/* Allow main firmware to save info */
MOV_5R s0, a0, s1, a1, s2, a2, s3, a3, s4, a4
call fw_save_info
MOV_5R a0, s0, a1, s1, a2, s2, a3, s3, a4, s4
#ifdef FW_FDT_PATH
/* Override previous arg1 */
lla a1, fw_fdt_bin
#endif
/*
* Initialize platform
* Note: The a0 to a4 registers passed to the
* firmware are parameters to this function.
*/
MOV_5R s0, a0, s1, a1, s2, a2, s3, a3, s4, a4
call fw_platform_init /* ★この関数がa1を参照する★ */
add t0, a0, zero
MOV_5R a0, s0, a1, s1, a2, s2, a3, s3, a4, s4
add a1, t0, zero
//opensbi/platform/generic/platform.c
/*
* The fw_platform_init() function is called very early on the boot HART
* OpenSBI reference firmwares so that platform specific code get chance
* to update "platform" instance before it is used.
*
* The arguments passed to fw_platform_init() function are boot time state
* of A0 to A4 register. The "arg0" will be boot HART id and "arg1" will
* be address of FDT passed by previous booting stage.
*
* The return value of fw_platform_init() function is the FDT location. If
* FDT is unchanged (or FDT is modified in-place) then fw_platform_init()
* can always return the original FDT location (i.e. 'arg1') unmodified.
*/
unsigned long fw_platform_init(unsigned long arg0, unsigned long arg1,
unsigned long arg2, unsigned long arg3,
unsigned long arg4)
{
const char *model;
void *fdt = (void *)arg1; //★ここでレジスタa1の値を使っている★
u32 hartid, hart_count = 0;
int rc, root_offset, cpus_offset, cpu_offset, len;
...
関数fw_platform_init()は_bss_zeroの後に呼ばれます。fw_next_arg1()を呼び出す前なのでFW_PAYLOAD_FDT_ADDRやFW_PAYLOAD_FDT_OFFSETを定義しても参照されません。
もしQEMUで既存のデバイスツリーを無視して、手動でロードする実験をしたい場合は、
このような設定をすると実験できます。コマンドを手動で実行するのは辛いので、スクリプトファイルを用意してsource (スクリプトファイル)コマンドで実行したほうが楽でしょう。
# load_dtb_opensbi.gdb restore virt.dtb binary 0x87f00000 thread 1 set $a1=0x87f00000 thread 2 set $a1=0x87f00000 thread 3 set $a1=0x87f00000 thread 4 set $a1=0x87f00000 restore build/platform/generic/firmware/fw_payload.bin binary 0x80000000 thread 1 set $pc=0x80000000 thread 2 set $pc=0x80000000 thread 3 set $pc=0x80000000 thread 4 set $pc=0x80000000
デバイスツリーを0x87f00000にロードして実行する例です。restoreコマンドでロードしているデバイスツリーのバイナリをQEMUから得る方法は前回(2024年月日の日記参照)の説明をご参照ください。
QEMUが起動時に用意するデバイスツリーとは異なるデバイスツリーをロードできることを確認するため、QEMUのデバイスツリーのmodelを少し書き換えたデバイスツリーvirt_mod.dtbを用意します。
/* virt.dts */
/dts-v1/;
/ {
#address-cells = <0x02>;
#size-cells = <0x02>;
compatible = "riscv-virtio";
model = "riscv-virtio,qemo"; /* ★★qemu → qemoに変更★★ */
DTSからDTBにするとき(その逆もできます)はデバイスツリーコンパイラdtcを使用します。
$ dtc -O dtb virt.dts > virt.dtb
QEMUを起動した後にデバッガーでQEMUにアタッチし、先ほど作成したGDBスクリプトにてロードとレジスタの設定を行います。
$ riscv64-unknown-linux-gnu-gdb (gdb) target remote :1234 Remote debugging using :1234 warning: No executable has been specified and target does not support determining executable automatically. Try using the "file" command. 0x0000000000001000 in ?? () (gdb) source load_dtb_opensbi.gdb Restoring binary file ../qemu/build/virt_mod.dtb into memory (0x87f00000 to 0x87f01c20) [Switching to thread 1 (Thread 1.1)] #0 0x0000000000001000 in ?? () [Switching to thread 2 (Thread 1.2)] #0 0x0000000000001000 in ?? () [Switching to thread 3 (Thread 1.3)] #0 0x0000000000001000 in ?? () [Switching to thread 4 (Thread 1.4)] #0 0x0000000000001000 in ?? () Restoring binary file build/platform/generic/firmware/fw_payload.bin into memory (0x80000000 to 0x81f51608) [Switching to thread 1 (Thread 1.1)] #0 0x0000000000001000 in ?? () [Switching to thread 2 (Thread 1.2)] #0 0x0000000000001000 in ?? () [Switching to thread 3 (Thread 1.3)] #0 0x0000000000001000 in ?? () [Switching to thread 4 (Thread 1.4)] #0 0x0000000000001000 in ?? () (gdb) continue Continuing.
最後のcontinueを実行するとQEMU側もログが出ます。例えばこんな感じです。
$ ./qemu-system-riscv64 -machine virt \ -bios none \ -nographic \ -chardev stdio,id=con,mux=on \ -serial chardev:con \ -mon chardev=con,mode=readline \ -smp 4 \ -s \ -S OpenSBI v1.5 ____ _____ ____ _____ / __ \ / ____| _ \_ _| | | | |_ __ ___ _ __ | (___ | |_) || | | | | | '_ \ / _ \ '_ \ \___ \| _ < | | | |__| | |_) | __/ | | |____) | |_) || |_ \____/| .__/ \___|_| |_|_____/|____/_____| | | |_| Platform Name : riscv-virtio,qemo ★★改造したmodel名になっている★★ Platform Features : medeleg Platform HART Count : 4 ...
OpenSBIがPlatform Nameを出しているログを見れば、先程デバイスツリーを改造したときに付けたmodel名が出力されているはずです。メモリ上にデバイスツリーやOpenSBIがロードされない実機だとしても、デバッガー経由でロードして起動することができそうです。