土日の勉強ノート

AI、機械学習、最適化、Pythonなどについて、技術調査、技術書の理解した内容、ソフトウェア/ツール作成について書いていきます

QEMUに似たRenodeをソースからビルドする

Renode という QEMU に似たオープンソースのエミュレータを試しています。

前回 は、バイナリファイルが動かない原因を解析しましたが、分かりませんでした。

もう少し深く解析するために、今回は、Renode をソースからビルドして、問題個所の特定と対策までやっていきます。

それでは、やっていきます。

参考文献

GDBハンドブック

GDBハンドブック

Amazon

はじめに

「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; // 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(スタックポインタ)に、正しい値が設定されました。

対策方法は手抜きでしたが、無事に動かすことが出来ました。

バイナリファイルから構築したELFファイルで動かせた
バイナリファイルから構築したELFファイルで動かせた

おわりに

今回は、Renode をソースからビルドして、バイナリファイルから構築した ELFファイルが動作しない原因を解析しました。

無事に原因は分かり、手抜きですが、対策は効果があり、解決しました。

次回は、VSCode でデバッグしてみようと思います。

最後になりましたが、エンジニアグループのランキングに参加中です。

気楽にポチッとよろしくお願いいたします🙇

今回は以上です!

最後までお読みいただき、ありがとうございました。