Renode という QEMU に似たオープンソースのエミュレータを試しています。
前回 は、バイナリファイルが動かない原因を解析しましたが、分かりませんでした。
もう少し深く解析するために、今回は、Renode をソースからビルドして、問題個所の特定と対策までやっていきます。
それでは、やっていきます。
参考文献
はじめに
「QEMUを動かす」の記事一覧です。良かったら参考にしてください。
QEMUを動かすの記事一覧
まず、Renode の公式サイトは以下です。
https://renode.io/
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;
}
VectorTableOffset = checked((uint)value);
}
}
if(pcNotInitialized)
{
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;
}
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 でデバッグしてみようと思います。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。