Renode という QEMU に似たオープンソースのエミュレータを試しています。
前回 は、バイナリファイルが動かない原因を解析しましたが、分かりませんでした。
もう少し深く解析するために、今回は、Renode をソースからビルドして、問題個所の特定と対策までやっていきます。
それでは、やっていきます。
参考文献
はじめに
「QEMUを動かす」の記事一覧です。良かったら参考にしてください。
・第2回:STM32(ARM Cortex-M)をQEMUで動かす(ソースコード確認編)
・第3回:STM32(ARM Cortex-M)をQEMUで動かす(スタートアップルーチン編)
・第4回:STM32(ARM Cortex-M)をQEMUで動かす(リンカスクリプト編)
・第5回:STM32(ARM Cortex-M)のELFファイルの内容を確認する
・第6回:STM32(ARM Cortex-M)のELFファイル⇔バイナリの変換を行う
・第7回:STM32(ARM Cortex-M)のバイナリから構築したELFファイルをQEMUで動かす
・第8回:QEMUのビルドに必要なxpm(xPack Project Manager)について学ぶ
・第9回:QEMUをソースからビルドして動かす
・第10回:QEMUのソースコードを変更してSTM32の動作を変える
・第11回:QEMUに似たRenodeというOSSの組込みデバイスエミュレータを試す
・第12回:QEMUに似たRenodeでSTM32をGDBを使ってデバッグする
・第13回:QEMUに似たRenodeでSTM32をバイナリファイルで動かす
・第14回:QEMUに似たRenodeをソースからビルドする ← 今回
まず、Renode の公式サイトは以下です。
Renode の公式のドキュメントは以下です。
https://renode.readthedocs.io/en/latest/
また、GitHub は以下です。
https://github.com/renode/renode
今回は、Renode をソースからビルドします。
Renodeをソースからビルドする
ビルド手順は、Renodeのドキュメント に従います。
このビルド手順は、Ubuntu 20.04 で動作すると書かれていますが、私の環境は、VirtualBox の Ubuntu 22.04 です。
既に、Renode を一度でも動作させていれば、依存関係のパッケージはインストール済みだと思うので、そこは省略します。
ビルドのための依存パッケージをインストールします。
$ sudo apt update $ sudo apt install git automake autoconf libtool g++ coreutils policykit-1 libgtk2.0-dev uml-utilities gtk-sharp2 python3 python3-pip
続いて、Renode のソースコードを取得します。
$ git clone https://github.com/renode/renode.git
$ cd renode
ビルドスクリプトが準備されてるので、実行します。
$ ./build.sh Updating submodules... Submodule 'lib/AntShell' (https://github.com/antmicro/AntShell.git) registered for path 'lib/AntShell' Submodule 'lib/BigGustave' (https://github.com/antmicro/BigGustave.git) registered for path 'lib/BigGustave' Submodule 'lib/CxxDemangler' (https://github.com/antmicro/CxxDemangler) registered for path 'lib/CxxDemangler' Submodule 'lib/ELFSharp' (https://github.com/antmicro/elfsharp.git) registered for path 'lib/ELFSharp' Submodule 'lib/FdtSharp' (https://github.com/antmicro/FdtSharp.git) registered for path 'lib/FdtSharp' Submodule 'lib/InpliTftpServer' (https://github.com/antmicro/InpliTftpServer.git) registered for path 'lib/InpliTftpServer' Submodule 'lib/Migrant' (https://github.com/antmicro/Migrant.git) registered for path 'lib/Migrant' Submodule 'lib/Packet.Net' (https://github.com/antmicro/Packet.Net.git) registered for path 'lib/Packet.Net' Submodule 'lib/bc-csharp' (https://github.com/antmicro/bc-csharp.git) registered for path 'lib/bc-csharp' Submodule 'lib/cctask' (https://github.com/antmicro/cctask.git) registered for path 'lib/cctask' Submodule 'lib/options-parser' (https://github.com/antmicro/options-parser) registered for path 'lib/options-parser' Submodule 'lib/termsharp' (https://github.com/antmicro/termsharp.git) registered for path 'lib/termsharp' Submodule 'src/Infrastructure' (https://github.com/renode/renode-infrastructure.git) registered for path 'src/Infrastructure' Submodule 'tools/dts2repl' (https://github.com/antmicro/dts2repl.git) registered for path 'tools/dts2repl' (省略) Done building project "/home/daisuke/svn_/renode/renode/Renode.sln". Build succeeded. Warnings: /home/daisuke/svn_/renode/renode/Renode.sln (default targets) -> (Build target) -> /home/daisuke/svn_/renode/renode/src/Infrastructure/src/Emulator/Peripherals/Peripherals.csproj (default targets) -> /usr/lib/mono/xbuild/14.0/bin/Microsoft.CSharp.targets (CoreCompile target) -> /home/daisuke/svn_/renode/renode/src/Infrastructure/src/Emulator/Peripherals/Peripherals/DMA/STM32WBA_GPDMA.cs(472,40): warning CS0649: Field 'STM32WBA55_GPDMA.Channel.triggerOverrunInterruptEnable' is never assigned to, and will always have its default value null 1 Warning(s) 0 Error(s) Time Elapsed 00:05:30.6956520
5分半ぐらいで、エラーもなく、ビルドは完了しました。
ビルドのログを見ると、Microsoft とか、dll ファイルとかがたくさん出てきます。
ウィキペディアによると、Mono とは、.NET Framework 互換の環境を実現するオープンソースのプロジェクトらしいです。Renode は、ソースコードを見ると、C# で書かれているようです。
もう少し調べると、C# でコンパイルすると中間コードを出力してくれて、その出力ファイルは、Windows、Mono を導入した Linux のどちらでも動作するようです。そういう意味では、Java に似てますね。
ソースからビルドしたRenodeを動かす
今まで気づきませんでしたが、renode というファイルはシェルスクリプトでした。
renode --console で動かしたとき、実際は、mono /home/daisuke/svn_/renode/renode/output/bin/Release/Renode.exe --console というコマンドが発行されてました。
では、ビルドした Renode を動かしていきたいと思います。
$ ./renode --console Gtk-Message: 19:45:52.393: Failed to load module "canberra-gtk-module" 19:45:53.1851 [INFO] Loaded monitor commands from: /home/daisuke/svn_/renode/renode/scripts/monitor.py Renode, version 1.15.0.33179 (f830e634-202406021825) (monitor) mach create 19:46:11.5189 [INFO] System bus created. (machine-0) machine LoadPlatformDescription @platforms/boards/stm32f4_discovery-kit.repl 19:46:17.7711 [INFO] Reading cache 19:46:18.2495 [INFO] sysbus: Loaded SVD: /tmp/renode-25417/ede950a7-f709-40b5-8460-b2687e1efc1c.tmp. Name: STM32F40x. Description: STM32F40x. (machine-0) sysbus LoadELF @stm32f4discovery_sample.elf 19:46:26.3999 [INFO] sysbus: Loading segment of 11000 bytes length at 0x8000000. 19:46:26.4186 [INFO] sysbus: Loading segment of 360 bytes length at 0x8002AF8. 19:46:26.4187 [INFO] sysbus: Loading segment of 256 bytes length at 0x2001F700. (machine-0) machine StartGdbServer 3333 (machine-0) 19:46:30.3299 [INFO] machine-0: GDB server with all CPUs started on port :3333
いつもと同じ感じに起動しました。GDB 側を起動していきます。
$ arm-none-eabi-gdb stm32f4discovery_sample.elf (gdb) target remote :3333 Remote debugging using :3333 Reset_Handler () at ../startup.S:119 119 mrs r0, control
問題なく起動しました。Renode 側のログです。
(machine-0) 19:47:05.5616 [INFO] cpu: Guessing VectorTableOffset value to be 0x8000000. 19:47:05.5672 [INFO] cpu: Setting initial values: PC = 0x8000CF5, SP = 0x20020000. 19:47:05.5690 [INFO] machine-0: Machine started.
Renodeのソースから問題個所を特定する
Renode の仕組みは全く分かりませんが、とりあえず関係ありそうなキーワードで検索して、問題個所を特定していきます。
C# は全く分かりません(笑)。
Renodeのソースから問題個所を特定する
それっぽいところを見つけました。src/Infrastructure/src/Emulator/Cores/Arm-M/CortexM.cs の L307 あたりです。
STM32 は、先頭番地にスタックポインタの初期値を格納する必要があり、4番地にリセットハンドラのアドレスを格納する必要があります。それらしい関数名です。
最初の if文(if(!vtorInitialized && firstNotNullSection.HasValue))が真になれば、普通の ELFファイルを起動した場合に出ていたログが出るようです。また、次の if文(if(pcNotInitialized))で、実際に 0番地と 4番地から、値を取り出して、PC と SP に代入してそうです。
そこで、関数に入ってすぐのところで、ログを出力する行を追加しました(*** で囲ってるところ)。
private void InitPCAndSP() { var firstNotNullSection = machine.SystemBus.GetLookup(this).FirstNotNullSectionAddress; this.Log(LogLevel.Info, "*** vtorInitialized={0}, firstNotNullSection.HasValue={1} ***", vtorInitialized, firstNotNullSection.HasValue); if(!vtorInitialized && firstNotNullSection.HasValue) { if((firstNotNullSection.Value & (2 << 6 - 1)) > 0) { this.Log(LogLevel.Warning, "Alignment of VectorTableOffset register is not correct."); } else { var value = firstNotNullSection.Value; this.Log(LogLevel.Info, "Guessing VectorTableOffset value to be 0x{0:X}.", value); if(value > uint.MaxValue) { this.Log(LogLevel.Error, "Guessed VectorTableOffset doesn't fit in 32-bit address space: 0x{0:X}.", value); return; // Keep VectorTableOffset uninitialized in the case of error condition } VectorTableOffset = checked((uint)value); } } if(pcNotInitialized) { // stack pointer and program counter are being sent according // to VTOR (vector table offset register) var sysbus = machine.SystemBus; var pc = sysbus.ReadDoubleWord(VectorTableOffset + 4, this); var sp = sysbus.ReadDoubleWord(VectorTableOffset, this); if(sysbus.FindMemory(pc, this) == null || (pc == 0 && sp == 0)) { this.Log(LogLevel.Error, "PC does not lay in memory or PC and SP are equal to zero. CPU was halted."); IsHalted = true; return; // Keep PC and SP uninitialized in the case of error condition } this.Log(LogLevel.Info, "Setting initial values: PC = 0x{0:X}, SP = 0x{1:X}.", pc, sp); PC = pc; SP = sp; } }
この変更を加えた状態で、ビルドしてみます。
$ ./build.sh
(省略)
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:59.1223600
今度は1分以内にビルドが完了しました。差分でビルドできているようです。
デバッグログを入れたソースで動かしてみる
では、動かしていきます。
$ ./renode --console Gtk-Message: 20:34:02.720: Failed to load module "canberra-gtk-module" 20:34:03.4985 [INFO] Loaded monitor commands from: /home/daisuke/svn_/renode/renode/scripts/monitor.py Renode, version 1.15.0.36947 (f830e634-202406021825) (monitor) mach create 20:34:06.8539 [INFO] System bus created. (machine-0) machine LoadPlatformDescription @platforms/boards/stm32f4_discovery-kit.repl 20:34:11.3491 [INFO] Reading cache 20:34:11.8140 [INFO] sysbus: Loaded SVD: /tmp/renode-27280/e7022e0e-6168-41fa-9b36-ad26ffd4e5b3.tmp. Name: STM32F40x. Description: STM32F40x. (machine-0) sysbus LoadBinary @stm32f4discovery_sample_objcopy.bin 0x08000000 (machine-0) machine StartGdbServer 3333 20:34:30.1779 [INFO] machine-0: GDB server with all CPUs started on port :3333
ここまでは同じです。GDB を起動していきます。
$ arm-none-eabi-gdb stm32f4discovery_sample.elf (gdb) target remote :3333 Remote debugging using :3333 0x00000000 in ?? ()
Renode 側のログです。
(machine-0) 20:34:39.9079 [INFO] cpu: *** vtorInitialized=False, firstNotNullSection.HasValue=False *** 20:34:39.9149 [WARNING] sysbus: [cpu: 0x0] ReadDoubleWord from non existing peripheral at 0x4. 20:34:39.9150 [WARNING] sysbus: [cpu: 0x0] ReadDoubleWord from non existing peripheral at 0x0. 20:34:39.9172 [ERROR] cpu: PC does not lay in memory or PC and SP are equal to zero. CPU was halted. 20:34:39.9233 [INFO] machine-0: Machine started.
追加したログが出力されてますね、成功です。
普通の ELFファイルを起動した場合、追加したログは以下のようになりました。
20:50:46.8969 [INFO] cpu: *** vtorInitialized=False, firstNotNullSection.HasValue=True *** 20:50:46.8970 [INFO] cpu: Guessing VectorTableOffset value to be 0x8000000. 20:50:46.9032 [INFO] cpu: Setting initial values: PC = 0x8000CF5, SP = 0x20020000. 20:50:46.9051 [INFO] machine-0: Machine started.
firstNotNullSection.HasValue が異なっています。最初の if文に入れず、VectorTableOffset 変数に値が入らず、読み出しに失敗したんだと思います。
追加で、2番目の if文の前に、以下のようにログ出力を追加しました。
this.Log(LogLevel.Info, "*** pcNotInitialized={0}, VectorTableOffset={1} ***", pcNotInitialized, VectorTableOffset); if(pcNotInitialized)
まずは、普通の ELFファイルの方です。
(machine-0) 20:55:39.3453 [INFO] cpu: *** vtorInitialized=False, firstNotNullSection.HasValue=True *** 20:55:39.3454 [INFO] cpu: Guessing VectorTableOffset value to be 0x8000000. 20:55:39.3474 [INFO] cpu: *** pcNotInitialized=True, VectorTableOffset=134217728 *** 20:55:39.3511 [INFO] cpu: Setting initial values: PC = 0x8000CF5, SP = 0x20020000. 20:55:39.3528 [INFO] machine-0: Machine started.
続いて、バイナリファイルの方です。
(machine-0) 20:56:54.1127 [INFO] cpu: *** vtorInitialized=False, firstNotNullSection.HasValue=False *** 20:56:54.1129 [INFO] cpu: *** pcNotInitialized=True, VectorTableOffset=0 *** 20:56:54.1192 [WARNING] sysbus: [cpu: 0x0] ReadDoubleWord from non existing peripheral at 0x4. 20:56:54.1194 [WARNING] sysbus: [cpu: 0x0] ReadDoubleWord from non existing peripheral at 0x0. 20:56:54.1215 [ERROR] cpu: PC does not lay in memory or PC and SP are equal to zero. CPU was halted. 20:56:54.1285 [INFO] machine-0: Machine started.
予想した通り、ELFファイルの方は 0x8000000 が入っていますが、バイナリファイルの方は 0 のままです。
問題は、関数の先頭で、0x8000000番地が取得できていないことのようです(ソースの以下の場所)。
var firstNotNullSection = machine.SystemBus.GetLookup(this).FirstNotNullSectionAddress;
バイナリファイルが動かない理由と対策
では、上の FirstNotNullSectionAddress に、0x8000000 が格納されていないことが原因でした。では、FirstNotNullSectionAddress がどこで設定されるかを検索で探してみると、src/Infrastructure/src/Emulator/Main/Peripherals/Bus/SymbolLookup.cs の LoadELF関数で設定されていました。
LoadELF関数の先頭で、シンボルテーブルのセクションが存在するかどうかをチェックしています。どうやら、これが原因のようです(デバッグログを入れて、FirstNotNullSectionAddress を設定するまえに、return してしまっていることを確認しました)。
一方で、LoadBinary関数も見てみました(src/Infrastructure/src/Emulator/Main/Core/Extensions/FileLoaderExtensions.cs)。FirstNotNullSectionAddress に値を格納するところはありませんでした。LoadBinary関数は、Cortex-M に対応していないのかもしれません。
以上より、LoadBinary関数が使えないということは、バイナリファイルから再構築した ELFファイルで動かすしかありませんが、それを動かそうと思うと、シンボルテーブルのセクションを作らなければならないようです。
ソースコードを見ると、シンボルテーブルのセクションが存在すればいいだけなので、中身が適当なシンボルテーブルのセクションでも良さそうです。しかし、あまり意味があるとも思えないところです。
FirstNotNullSectionAddress は、0x8000000 以外の値が入ることは今のところなさそうなので、ソースコードを変更して、強制的に、0x8000000 を入れてしまえば良さそうです。
というわけで、以下のように、InitPCAndSP関数の先頭の先ほどログを追加したところに、代わりに、以下のように、1行追加します。
private void InitPCAndSP() { var firstNotNullSection = machine.SystemBus.GetLookup(this).FirstNotNullSectionAddress; firstNotNullSection = 0x08000000; if(!vtorInitialized && firstNotNullSection.HasValue)
ソースコードを変更したので、再度ビルドします。ビルドが完了したら早速動かしていきます。
$ ./build.sh $ ./renode --console Gtk-Message: 21:31:41.884: Failed to load module "canberra-gtk-module" 21:31:45.9419 [INFO] Loaded monitor commands from: /home/daisuke/svn_/renode/renode/scripts/monitor.py Renode, version 1.15.0.38713 (07559ce7-202406032107) (monitor) mach create 21:31:49.4158 [INFO] System bus created. (machine-0) machine LoadPlatformDescription @platforms/boards/stm32f4_discovery-kit.repl 21:32:00.4714 [INFO] Reading cache 21:32:02.5700 [INFO] sysbus: Loaded SVD: /tmp/renode-22954/fdbedcef-e10d-4227-a8f7-05dd4b415c41.tmp. Name: STM32F40x. Description: STM32F40x. (machine-0) sysbus LoadELF @stm32f4discovery_sample_bin2elf_entry.elf 21:32:06.5746 [INFO] sysbus: Loading segment of 11116 bytes length at 0x8000000. (machine-0) machine StartGdbServer 3333 21:32:10.1833 [INFO] machine-0: GDB server with all CPUs started on port :3333
続いて、GDB を起動します。
$ arm-none-eabi-gdb stm32f4discovery_sample_bin2elf_entry.elf (gdb) target remote :3333 Remote debugging using :3333 0x08000cf4 in ?? ()
リセットハンドラのアドレスで停止しています!一応、Renode 側のログです。
(machine-0) 21:32:19.4627 [INFO] cpu: Guessing VectorTableOffset value to be 0x8000000. 21:32:19.5230 [INFO] cpu: Setting initial values: PC = 0x8000CF5, SP = 0x20020000. 21:32:19.5410 [INFO] machine-0: Machine started.
PC(プログラムカウンタ)と SP(スタックポインタ)に、正しい値が設定されました。
対策方法は手抜きでしたが、無事に動かすことが出来ました。

おわりに
今回は、Renode をソースからビルドして、バイナリファイルから構築した ELFファイルが動作しない原因を解析しました。
無事に原因は分かり、手抜きですが、対策は効果があり、解決しました。
次回は、VSCode でデバッグしてみようと思います。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。