こんにちは。技術統括部の前田です。いつもCTF関連の記事しか書いていないのでそれを期待されていた方はごめんなさい。今回は違います。
この記事で紹介するのは、QEMUにオレオレデバイスを追加する記事を書いたら面白いんじゃないか、と思って始めた物語が、序盤で壮大な沼にハマった話です。Raspberry PiをQEMUで動かすという解説記事自体はあるものの、真似しても全然動かないんですよね。特に4BモデルをQEMU上で動かす方法を解説する希少な記事になっているかもしれません。
そして、近日開催の学生向けイベントのお知らせがあります。少々お付き合いください。
[PR]
2025年8月21日から8月22日にかけて、学生向けイベント『Binary Exploitation Workshop for Students』を開催します!
CDIで開催されているトレーニング『Binary Exploitation Fundamentals』をベースにカスタムしたコンテンツで、主にスタックベースのバッファオーバーフローに対する攻撃を取り扱います。 新しい技術を学びたい方はもちろん、CDIのエンジニアとお話したい方の参加もお待ちしております!
詳細はこちらからご確認ください。

[/PR]
それでは始めましょう。
はじめに
QEMU、便利ですよね。
でも、QEMUってドキュメントが乏しく、「起動オプションは誰かのパクり、実際のデバイスとは違うけど動いてるからOK!」みたいな経験をしたことがある人は多いでしょう。
QEMUを理解すれば、そんな不満から開放されます。されたいです。この記事ではその第一歩として、QEMUの周辺デバイスのエミュレータを作る方法を紹介します。
QEMUに実装されているraspi4bマシンには、ハードウェア乱数生成器のエミュレーションが不足しています。入出力が簡単で実装がしやすいので、この記事ではこれを題材に実装を進めていきます。
QEMUでraspi4bマシンを使えるようにする
皆さんはQEMUでRaspberry Pi OSを動かしたことはありますか?QEMUのArch64(とARMも)エミュレーションでは、仮想マシンのモデル定義として各種Raspberry Piが収録されています。
$ qemu-system-aarch64 -M help
...
raspi0 Raspberry Pi Zero (revision 1.2)
raspi1ap Raspberry Pi A+ (revision 1.1)
raspi2b Raspberry Pi 2B (revision 1.1)
raspi3ap Raspberry Pi 3A+ (revision 1.0)
raspi3b Raspberry Pi 3B (revision 1.2)
raspi4b Raspberry Pi 4B (revision 1.5)
...
QEMUでRaspberry Piを試せるなら便利じゃないか、と思うかもしれません。
しかし、QEMU上で動作させるときにはRaspberry Piならではの特徴(GPIOなど)はほとんど消失してしまいます。Web上の記事でRaspberry Pi OSをQEMUで動かす例では、結局汎用モデルのvirtを使って動かすことが多いようです。
そちらのほうが制約が少なく資料も豊富なので現実的ですね。ではraspi4bは何が不便なのかというと……、きっとこれからわかると思います。
しかし、今回の目的は「QEMUのデバイスを作ってみる」です。どうにかしてraspi4bを使わなければいけません。この道のりが意外に険しく、本質のデバイス作成より重くなってしまいました。できるだけOS起動までは端折って説明しますが、長いことには変わりありません。デバイス作成から読みたい方はこちらから次の章までジャンプしてください。
1. 起動に必要なファイルを揃える
まずは、通常のRaspberry PiでSDカードにインストールするのと同じイメージファイルをダウンロードします。
https://www.raspberrypi.com/software/operating-systems/
今回はRaspberry Pi OSの64bit Lite版を使います。ファイルはxz圧縮されているので展開し、空き領域を確保するためにqemu-img等で適当な大きさにしておきましょう。というかそもそも2のn乗じゃないと怒られます。
unxz -k 2025-05-13-raspios-bookworm-arm64-lite.img.xz
qemu-img resize 2025-05-13-raspios-bookworm-arm64-lite.img 8G
実機ではGPUから起動して、GPUがファイルシステムからカーネルを取り出しますが、QEMUではその方法は真似できません。Linuxカーネルを指定して直接起動します。次のURLからkernel8.img
もダウンロードしましょう。OSイメージ内にも入っているのでそこから抜き出してもOKです。
https://github.com/raspberrypi/firmware/tree/master/boot
同じ理由でLinuxが起動時に使うDevice Treeファイルも不足しています。上記URLからbcm2711-rpi-4-b.dtb
もダウンロードしてください。
QEMUも必要ですね。最初はOSのパッケージマネージャに入っているものを使ってください。実装パートではQEMU 10.0.2を使います
2. QEMUの起動オプションを組む
これ、むずいですよね。特にデバイス周り。コマンドラインで作業していると大変なので、シェルスクリプトに書きながら進めるのがおすすめです。
qemu-system-aarch64 -M raspi4b -m 2G -smp 4 \
-kernel kernel8.img \
-dtb bcm2711-rpi-4-b.dtb \
-drive if=sd,format=raw,file=2025-05-13-raspios-bookworm-arm64-lite.img \
-nographic \
-serial mon:stdio \
-no-reboot \
-usb \
-device usb-kbd \
-device usb-net,netdev=net0 -netdev user,id=net0 \
-append "root=/dev/mmcblk1p2 rootwait"
「おまじない」じみていますが、特筆すべき点はUSB周りとLinuxの起動引数-append
です。
USB経由ではキーボードとネットワークデバイスの2つを接続します。ネットワーク接続を実現する方法はいくつもあると思いますが、この方法が一番簡単だと思います。それでもDevice Treeの修正が必要になるので大変ではありますが……。
Linuxの起動引数には起動するためのルートパーティションを指定しています。これも本来であればGPU側の設定ファイル(/boot/config.txt
)に書くはずの内容です。
それでは起動してみましょう。
$ ./run.sh
qemu-system-aarch64: warning: bcm2711 dtc: brcm,bcm2711-pcie has been disabled!
qemu-system-aarch64: warning: bcm2711 dtc: brcm,bcm2711-rng200 has been disabled!
qemu-system-aarch64: warning: bcm2711 dtc: brcm,bcm2711-thermal has been disabled!
qemu-system-aarch64: warning: bcm2711 dtc: brcm,bcm2711-genet-v5 has been disabled!
(シーン)
困った。シリアルコンソールがうまく認識できておらず、出力が表示されていないようです。様々な試行錯誤の後、シリアルコンソールの出力は出てくるようになったものの、一方で入力はできない状態になりました。
また、試行錯誤の中で-nographic
を外したときに、グラフィック経由で画面が出ていることに気づきました。変な環境で作業していると忘れがちですが、Raspberry Piは普通画面に映して使います。しかし、残念ながらここでもキーボード入力が全く効きません。

(時間の溶ける音)
3. Device Treeをパッチする
様々なデバッグの結果、シリアルコンソールもUSBキーボードもDevice Treeが悪さをしているらしいことが判明しました。Device Treeは、ハードウェアの構成情報が記述されたファイルで、Linuxはこの情報を元にドライバーを割り当てています。
まず、シリアルコンソールの方は結論から言うとBluetoothを無効にすると正常に動作するようになりました。どうやらBluetoothのコントローラーと通信するためにシリアル接続が使われていて、カーネルから使おうとすると干渉してしまうようです。
Bluetoothの無効化などを一気にやってくれるDevice Treeの差分(Overlay)がなんと公式で提供されています。 disable-bt.dtbo
をダウンロードしておきましょう。
https://github.com/raspberrypi/firmware/blob/master/boot/overlays/disable-bt.dtbo
続いてUSBの話に移ります。
Raspberry Piは4BからUSBコントローラーがPCI Express経由での接続に変わりました。それに伴って、公式で配布されるDevice Treeでは、従来のコントローラーが無効になっています。一方でraspi4bマシンではPCI Expressに対応しておらず、従来形式のUSBコントローラーにのみ対応しています。

この微妙な齟齬によって、結果としてUSBコントローラーが存在しない状況になっていたわけです。これについても公式で従来のコントローラーを復活させるDevice Tree Overlayが提供されているのでダウンロードします。明らかにUSB-OTG用みたいな名前ですが、status = "disabled"
になっているUSBコントローラーをstatus = "okay"
に変更してくれます。
https://github.com/raspberrypi/firmware/blob/master/boot/overlays/dwc-otg-deprecated.dtbo
最後にダウンロードしたOverlayを適用します。
fdtoverlay -i bcm2711-rpi-4-b.dtb -o patched.dtb disable-bt.dtbo dwc-otg-deprecated.dtbo
ちなみに、実機であれば/boot/config.txt
にdtoverlay
という項目を追記すると自動でパッチしてくれます。でもパッチを担当するのはRaspberry PiのGPUなので、当然今回は動作しません。
それでは起動してみましょう。

QEMUっぽいIPアドレスが割り当たっていますね。
そして、えー、パスワードがわかりません。
4. ユーザー名とパスワードを設定する
昔のRaspberry Piと言えばpi:raspberryで簡単にログインできました。今はセキュリティの都合で、起動時にユーザーアカウントを作成する必要があります。Raspberry Pi Imagerがその担当らしいのですが、今回の用途には微妙に合わず使えませんでした。
一時的に-nographic
を外し、USBキーボードをエミュレーションしたうえで、グラフィック経由の画面からパスワードを設定しましょう。USBが有効になっててよかった。

以降は-nographic
を指定してグラフィックを無効化しても問題ありません。

疲れました。ここまでをもっと丁寧に書いたら別の記事になりそうですね。
乱数生成器の通信方法を理解する
QEMUでRaspberry Piが動作したので、次は乱数生成器の仕様を確認しましょう。
通常であれば、デバイスのデータシートなりを見つけて仕様や機能の理解をするところですが、Raspberry Piのハードウェア乱数生成器はデータシートが見つかりませんでした。
しかし、乱数生成器を「使う」ソフトウェアは簡単に入手できるので、それを元に理解を進めていきます。今回のターゲットが乱数生成で、かつ正しく実装するつもりがないからできていることかもしれません。
1. Device Treeを読む
バイナリ形式のDevice Treeは次のコマンドでテキストに変換できます。
dtc -I dtb -O dts bcm2711-rpi-4-b.dtb
/ {
soc {
...
rng@7e104000 {
compatible = "brcm,bcm2711-rng200";
reg = <0x7e104000 0x28>;
status = "okay";
phandle = <0x4a>;
};
...
}
}
乱数生成器っぽい項目を確認すると、compatible = "brcm,bcm2711-rng200"
と書かれています。Linuxはこの値を確認し、対応するデバイスドライバーを検索します。同じようにカーネルのソースコードをこの文字列で検索して、どのドライバーが使われるのかを調べましょう。
Linuxの本家のコードとRaspberry PiのLinuxではかなり差異があります。皆さんはRaspberry Piの方を読んでくださいね(1敗)。
https://github.com/raspberrypi/linux/blob/rpi-6.12.y/drivers/char/hw_random/iproc-rng200.c
2. デバイスドライバーを読む
どのアドレスが何のレジスターに割り当たっているのか、そのレジスターはどのように使われているのかを重点的に読みます。乱数生成器はデータの読み出しが一番大事なので、iproc_rng200_read
関数から読み始めるとよいでしょう。
static int iproc_rng200_read(struct hwrng *rng, void *buf, size_t max,
bool wait)
{
// ...
while ((num_remaining > 0) && time_before(jiffies, idle_endtime)) {
/* Is RNG sane? If not, reset it. */
status = ioread32(priv->base + RNG_INT_STATUS_OFFSET);
if ((status & (RNG_INT_STATUS_MASTER_FAIL_LOCKOUT_IRQ_MASK |
RNG_INT_STATUS_NIST_FAIL_IRQ_MASK)) != 0) {
if (num_resets >= MAX_RESETS_PER_READ)
return max - num_remaining;
iproc_rng200_restart(priv->base);
num_resets++;
}
/* Are there any random numbers available? */
if ((ioread32(priv->base + RNG_FIFO_COUNT_OFFSET) &
RNG_FIFO_COUNT_RNG_FIFO_COUNT_MASK) > 0) {
if (num_remaining >= sizeof(uint32_t)) {
/* Buffer has room to store entire word */
*(uint32_t *)buf = ioread32(priv->base +
RNG_FIFO_DATA_OFFSET);
buf += sizeof(uint32_t);
num_remaining -= sizeof(uint32_t);
} else {
/* Buffer can only store partial word */
uint32_t rnd_number = ioread32(priv->base +
RNG_FIFO_DATA_OFFSET);
memcpy(buf, &rnd_number, num_remaining);
buf += num_remaining;
num_remaining = 0;
}
/* Reset the IDLE timeout */
idle_endtime = jiffies + MAX_IDLE_TIME;
} else {
if (!wait)
/* Cannot wait, return immediately */
return max - num_remaining;
/* Can wait, give others chance to run */
usleep_range(min(num_remaining * 10, 500U), 500);
}
}
// ...
}
読むとおわかりかと思いますが、処理の大部分はデバイスの状態を管理するコードです。ソフトウェアでエミュレーションするときにはガン無視でも動く予感がします。
また、ioread32(priv->base + RNG_FIFO_DATA_OFFSET)
の部分が乱数を受け取っているコードですが、前後のFIFOの管理をするコードについては適切な値を返してあげる必要がありそうです。
嘘乱数生成器を実装する
ようやく実装パートです。100行程度しかありませんが、すべてを説明するには長すぎるので要所を絞って解説します。QEMUでRaspberry Piを動かすだけでこんなに掛かるはずでは……。
1. QEMUのプロジェクト全体を確認する
当たり前のことに見えますが、大きなプロジェクトほどどこに何が置いてあるかわからないものです。そして、新しく実装するものはどこに置いたらいいのかわかりにくいです。
古いRaspberry Pi用のbcm2835-rng.c
がhw/misc
以下にあるのでそれに倣って新しい乱数生成器のコードを作成しましょう。
- hw/misc/bcm2711-rng200.c
- include/hw/misc/bcm2711-rng200.h
ファイルを追加したら、hw/misc/meson.build
にその情報を足しましょう。
diff --git a/hw/misc/meson.build b/hw/misc/meson.build
index 6d47de4..bfd5aee 100644
--- a/hw/misc/meson.build
+++ b/hw/misc/meson.build
@@ -88,6 +88,7 @@ system_ss.add(when: 'CONFIG_RASPI', if_true: files(
'bcm2835_thermal.c',
'bcm2835_cprman.c',
'bcm2835_powermgt.c',
+ 'bcm2711-rng200.c',
))
system_ss.add(when: 'CONFIG_SLAVIO', if_true: files('slavio_misc.c'))
system_ss.add(when: 'CONFIG_ZYNQ', if_true: files('zynq_slcr.c'))
2. 最低限のデバイスを実装する
QEMUではQEMU Object Model (QOM)という独自のオブジェクト指向モデルが使われています。まずはその型定義をする必要があります。
#ifndef BCM2711_RNG200_H
#define BCM2711_RNG200_H
#include "hw/sysbus.h"
#include "qemu/typedefs.h"
#include "qom/object.h"
#define TYPE_BCM2711_RNG200 "bcm2711-rng200"
OBJECT_DECLARE_SIMPLE_TYPE(BCM2711RNG200State, BCM2711_RNG200)
struct BCM2711RNG200State {
SysBusDevice parent_obj;
MemoryRegion regs; // メモリ
FILE *rng_source; // 乱数生成を丸投げする先のFILE構造体
};
#endif
SysBusDevice
を継承したBCM2711RNG200State
を作成します。ここでSysBusDevice
とは何なのか。申し訳ありませんが筆者には能力不足で解説できません。
BCM2711RNG200State
はデバイスのインスタンスに相当する構造体です。複数の乱数生成器を使う場合は、BCM2711RNG200State
も複数個作られます。
ここでは省略していますが、Classという概念もあります。これはデバイス自体とそのインスタンス化方法を説明するための構造体です1。Classは1つしか作られません。例えばQEMUのオプションからデバイスのパラメータを設定する場合にはClassが必要になります。
3. 乱数生成器を実装する
Linux側のドライバーが動きそうな感じに実装したものがこちらです。不要な部分は徹底的に削っていて短めのコードなので、一気に出します。
|
|
コードは次の3つのパートに分けられます。
- メモリ操作に対するコールバック関数(10~44行目)
- デバイス初期化(bcm2711_rng200_init関数)
- 型情報の定義(55行目~67行目)
デバイス初期化の処理も、ほとんどメモリ操作の初期化といって差し支えありません。
まずはsysbus_init_mmio
で、このデバイスが使うMemoryRegionの情報を親クラスであるSysBusDeviceに登録します。こうすることで、QEMU側からSysBusDevice経由でメモリの存在を認識できるようになります。
続いて、memory_region_init_io
でMemoryRegionの初期化とコールバック関数の登録をします。ここで第4引数に指定したオブジェクトがコールバック関数のopaque
に設定されて呼び出されます。
メモリのコールバック関数はrng_read
とrng_write
の2つです。
動作に必要な最低限の内容しか実装していません。特にrng_write
はデバッグ用に用意しているだけです。
デバイスドライバーはRNG_FIFO_DATA_ADDR
から乱数を読み出しますが、その処理に到達するためには、FIFO管理のカウンターを適切に設定する必要があります。
RNG_TOTAL_BIT_COUNT_ADDR
とRNG_FIFO_COUNT_ADDR
にアクセスがあったときに255 (0xff)を返してごまかしています。
RNG_FIFO_DATA_ADDR
にアクセスしたときのデータは/dev/random
から4バイト読み込んだものを使います。
4. raspi4bマシンに乱数生成器を接続する
いよいよ完成した乱数生成器をraspi4bマシンに接続します。
まずはraspi4bのインスタンス定義に乱数生成器のメンバーを追加する必要があります。BCM2838PeripheralState
の中に定義を追加します。 Raspi4bMachineState
という構造体もありますが、乱数生成器はBCM2838
としての機能なのでこちらが適切でしょう。
`
diff --git a/include/hw/arm/bcm2838_peripherals.h b/include/hw/arm/bcm2838_peripherals.h
index 7ee1bd0..278837e 100644
--- a/include/hw/arm/bcm2838_peripherals.h
+++ b/include/hw/arm/bcm2838_peripherals.h
@@ -12,6 +12,7 @@
#include "hw/arm/bcm2835_peripherals.h"
#include "hw/sd/sdhci.h"
#include "hw/gpio/bcm2838_gpio.h"
+#include "hw/misc/bcm2711-rng200.h"
/* SPI */
#define GIC_SPI_INTERRUPT_MBOX 33
@@ -72,6 +73,8 @@ struct BCM2838PeripheralState {
UnimplementedDeviceState asb;
UnimplementedDeviceState clkisp;
+
+ BCM2711RNG200State rng;
};
struct BCM2838PeripheralClass {
次は、追加したrng
とBCM2838Peripheralを接続する部分を書きます。
他の部分を参考に見よう見まねで書いてますが、インスタンスの初期化、デバイスの登録、メモリ領域の登録をしています。&s_base->peri_mr
がこのペリフェラルのベースアドレスで、RNG_OFFSET
が乱数生成器のアドレスまでのオフセット値になっています。ちなみにRNG_OFFSET
は最初から用意されていました。
diff --git a/hw/arm/bcm2838_peripherals.c b/hw/arm/bcm2838_peripherals.c
index e28bef4..f938c8d 100644
--- a/hw/arm/bcm2838_peripherals.c
+++ b/hw/arm/bcm2838_peripherals.c
@@ -11,6 +11,7 @@
#include "qemu/module.h"
#include "hw/arm/raspi_platform.h"
#include "hw/arm/bcm2838_peripherals.h"
+#include "hw/misc/bcm2711-rng200.h"
#define CLOCK_ISP_OFFSET 0xc11000
#define CLOCK_ISP_SIZE 0x100
@@ -57,6 +58,9 @@ static void bcm2838_peripherals_init(Object *obj)
TYPE_OR_IRQ);
object_property_set_int(OBJECT(&s->dma_9_10_irq_orgate), "num-lines", 2,
&error_abort);
+
+ /* RNG */
+ object_initialize_child(obj, "rng", &s->rng, TYPE_BCM2711_RNG200);
}
static void bcm2838_peripherals_realize(DeviceState *dev, Error **errp)
@@ -194,6 +198,14 @@ static void bcm2838_peripherals_realize(DeviceState *dev, Error **errp)
/* BCM2838 RPiVid ASB must be mapped to prevent kernel crash */
create_unimp(s_base, &s->asb, "bcm2838-asb", BRDG_OFFSET, 0x24);
+
+ /* RNG */
+ if (!sysbus_realize(SYS_BUS_DEVICE(&s->rng), errp)) {
+ return;
+ }
+ memory_region_add_subregion(
+ &s_base->peri_mr, RNG_OFFSET,
+ sysbus_mmio_get_region(SYS_BUS_DEVICE(&s->rng), 0));
}
raspi4b.c
にはDevice Treeからbrcm,bcm2711-rng200
を持つノードを削除する処理が入っています。消す候補のリストからこれを削除すれば、乱数生成器とraspi4bの接続作業は完了です。
static void bcm2838_peripherals_class_init(ObjectClass *oc, void *data)
diff --git a/hw/arm/raspi4b.c b/hw/arm/raspi4b.c
index f6de103..261fe32 100644
--- a/hw/arm/raspi4b.c
+++ b/hw/arm/raspi4b.c
@@ -68,7 +68,6 @@ static void raspi4_modify_dtb(const struct arm_boot_info *info, void *fdt)
/* Temporarily disable following devices until they are implemented */
const char *nodes_to_remove[] = {
"brcm,bcm2711-pcie",
- "brcm,bcm2711-rng200",
"brcm,bcm2711-thermal",
"brcm,bcm2711-genet-v5",
};
動作確認
いざ。
1. ビルド
次のコマンドでビルドします。
mkdir build
cd build
../configure --target-list=aarch64-softmmu --enable-slirp
make -j$(nproc)
--enable-slirp
はQEMUのユーザーモードネットワークのために必要なのでほぼ必須です。その他のオプションはお好みで設定してください。
2. 実行
起動スクリプトのQEMUの指定をビルドしたバイナリに差し替えます。
path/to/qemu-system-aarch64 -M raspi4b -m 2G -smp 4 \
-kernel kernel8.img \
-dtb patched.dtb \
-drive if=sd,format=raw,file=2025-05-13-raspios-bookworm-arm64-lite.img \
-nographic \
-serial mon:stdio \
-no-reboot \
-usb \
-device usb-kbd
-device usb-net,netdev=net0 -netdev user,id=net0 \
-append "root=/dev/mmcblk1p2 rootwait"
乱数生成器はRaspberry Pi OSから/dev/hwrng
としてアクセスできます。rng-tools
を入れるとrngtest
というツールが使えるようになるので、使ってみましょう。
元が/dev/random
なので当たり前ですが、いい感じに動作していますね。
おわりに
この実験をするにあたって、ブログで解説するのにちょうどいいデバイスは何かを探してこの乱数生成器を見つけたわけですが、そもそものRaspberry Piの起動があまりにも大変でした。Raspberry Piをちゃんと動かすために使った時間の方が圧倒的に長かったです。
「IoT機器のイメージをQEMU上で動作させたい、でも特定のデバイスで引っかかって起動できない」ときに参考にしてもらえると嬉しいです2。