QEMU
QEMU (von englisch „Quick Emulator“) ist eine freie Virtualisierungssoftware, die die gesamte Hardware eines Computers emuliert und durch die dynamische Übersetzung der Prozessorinstruktionen des Gastprozessors (englisch guest) in Instruktionen für den Wirtprozessor (englisch host) eine sehr gute Ausführungsgeschwindigkeit erreicht.
QEMU emuliert Systeme mit den folgenden Prozessorarchitekturen:
- 68K,
- Alpha,
- ARM (32- und 64-Bit),
- CHRIS,
- HPPA,
- LatticeMico32,
- m68K bzw. Coldfire,
- MicroBlaze,
- MIPS,
- Moxie,
- Nios2,
- OpenRISC,
- PC
- Power
- PowerNV
- PowerPC (32- und 64-Bit),
- RISC-V,
- S/390,
- SH-4,
- Sparc32/64,
- TILE-Gx,
- TriCore,
- Unicore,
- x86 (x86-32 und x86-64),
- Xtensa
(Stand 2026).
Wir beginnen mit der Entwicklung eines Programms für den LM3S6965, einen ARM Cortex-M3-Mikrocontroller. Wir haben diesen als unser erstes Ziel ausgewählt, da er mit QEMU emuliert werden kann, sodass Sie sich in diesem Abschnitt nicht mit der Hardware beschäftigen müssen und wir uns auf die Werkzeuge und den Entwicklungsprozess konzentrieren können.
WICHTIG In dieser Anleitung verwenden wir den Namen „app“ als Projektnamen. Wann immer Sie das Wort „app” sehen, sollten Sie es durch den Namen ersetzen, den Sie für Ihr Projekt gewählt haben. Alternativ können Sie Ihr Projekt auch „app“ nennen und so die Ersetzungen vermeiden.
Erstellen eines nicht standardmäßigen Rust-Programms
Wir werden die Projektvorlage cortex-m-quickstart verwenden, um daraus ein neues Projekt zu erstellen. Das erstellte Projekt enthält eine Minimalanwendung: einen guten Ausgangspunkt für eine neue Embedded-Rust-Anwendung. Darüber hinaus enthält das Projekt ein Verzeichnis examples mit mehreren separaten Anwendungen, die einige der wichtigsten Funktionen von Embedded Rust veranschaulichen.
Verwendung von cargo-generate
Installieren Sie zunächst cargo-generate
cargo install cargo-generateErstellen Sie anschließend ein neues Projekt
cargo generate --git https://github.com/knurling-rs/app-template Project Name: app
Creating project called `app`...
Done! New project created /tmp/appcd appVerwendung von git
Das Repository klonen
git clone https://github.com/rust-embedded/cortex-m-quickstart app
cd appUnd fülle dann die Platzhalter in der Datei Cargo.toml aus
[package]
authors = ["{{authors}}"] # "{{authors}}" -> "John Smith"
edition = "2018"
name = "{{project-name}}" # "{{project-name}}" -> "app"
version = "0.1.0"
# ..
[[bin]]
name = "{{project-name}}" # "{{project-name}}" -> "app"
test = false
bench = falseKeines von beiden verwenden
Laden Sie den neuesten Snapshot der Vorlage cortex-m-quickstart herunter und entpacken Sie ihn.
curl -LO https://github.com/rust-embedded/cortex-m-quickstart/archive/master.zip
unzip master.zip
mv cortex-m-quickstart-master app
cd appOder Sie navigieren zu cortex-m-quickstart, klicken auf die grüne Schaltfläche „Clone or download“ und anschließend auf „Download ZIP”.
Füllen Sie anschließend die Platzhalter in der Datei Cargo.toml aus, wie im zweiten Teil der Version „Verwendung von git beschrieben.
Programmübersicht
Der Einfachheit halber sind hier die wichtigsten Teile des Quellcodes in src/main.rs aufgeführt:
#![no_std]
#![no_main]
use panic_halt as _;
use cortex_m_rt::entry;
#[entry]
fn main() -> ! {
loop {
// Hier kommt Ihr Code hin.
}
}
Dieses Programm unterscheidet sich ein wenig von einem typischen Rust-Programm, schauen wir es uns also einmal genauer an.
#![no_std] gibt an, dass dieses Programm nicht mit der Standard-Crate std verknüpft wird. Stattdessen wird es mit deren Teilmenge verknüpft: der Crate core.
#![no_main] gibt an, dass dieses Programm nicht die Standard-main- Schnittstelle verwendet, die die meisten Rust-Programme nutzen. Der Hauptgrund (kein Wortspiel beabsichtigt) für die Verwendung von no_main ist, dass die Nutzung der main-Schnittstelle im no_std-Kontext „Nightly“ erfordert.
use panic_halt as _;. Diese Crate stellt einen panic_handler bereit, der das Verhalten des Programms im Panikfall definiert. Wir werden darauf im Kapitel In Panik geraten des Buches näher eingehen.
#[entry] ist ein Attribut des cortex-m-rt-Crate, das dazu dient, den Einstiegspunkt des Programms zu kennzeichnen. Da wir nicht die Standard-Schnittstelle main verwenden, benötigen wir eine andere Möglichkeit, den Einstiegspunkt des Programms anzugeben, und das wäre #[entry].
fn main() -> !. Unser Programm wird der einzige Prozess sein, der auf der Zielhardware läuft, deshalb soll es nicht beendet werden! Wir verwenden eine divergent function (das -> ! in der Funktionssignatur), um bereits zur Kompilierungszeit sicherzustellen, dass dies auch der Fall ist.
Cross-Kompilierung
Zunächst benötigen wir das Speicherlayout für den Ziel-Mikrocontroller, in unserem Fall den LM3S6965. Andernfalls schlägt die Verknüpfung des Images beim Build fehl. Erstellen Sie eine Datei mit dem Namen memory.x im Stammverzeichnis des Projekts und fügen Sie den folgenden Inhalt ein:
MEMORY
{
/* Hinweis: 1 K = 1 KiBi = 1024 bytes */
/* TODO Passen Sie diese Speicherbereiche an das Speicherlayout Ihres Geräts an. */
/* Diese Werte entsprechen dem LM3S6965, einem der wenigen Bausteine, die
QEMU emulieren kann */
FLASH : ORIGIN = 0x00000000, LENGTH = 256K
RAM : ORIGIN = 0x20000000, LENGTH = 64K
}
/* Hier wird der Aufrufstapel zugewiesen. */
/* Der Stapel ist vom Typ „vollständig absteigend“. */
/* Möglicherweise möchten Sie diese Variable verwenden, um den Aufrufstapel und
statische Variablen in verschiedenen Speicherbereichen zu lokalisieren.
Nachstehend ist der Standardwert aufgeführt */
/* _stack_start = ORIGIN(RAM) + LENGTH(RAM); */
/* Mit diesem Symbol können Sie den Speicherort des .text-Abschnitts anpassen. */
/* Wird dies weggelassen, wird der .text-Abschnitt direkt nach dem .vector_table-
Abschnitt platziert */
/* Dies ist nur bei Mikrocontrollern erforderlich, die bestimmte
Konfigurationsdaten direkthinter der Vektortabelle speichern */
/* _stext = ORIGIN(FLASH) + 0x400; */
/* Beispiel für die Zuweisung nicht initialisierter Variablen zu
benutzerdefinierten RAM-Speicherplätzen. */
/* Dies setzt voraus, dass Sie oben einen Bereich namens „RAM2“ definiert und
in den Rust-Quelldateien das Attribut `#[link_section = „.ram2bss“]` zu den
Daten hinzugefügt haben, die Sie dort platzieren möchten. */
/* Beachten Sie, dass der Bereich von der Laufzeitumgebung nicht auf Null
initialisiert wird! */
/* SECTIONS {
.ram2bss (NOLOAD) : ALIGN(4) {
*(.ram2bss);
. = ALIGN(4);
} > RAM2
} INSERT AFTER .bss;
*/
Der nächste Schritt besteht darin, das Programm für die Cortex-M3-Architektur cross zu kompilieren. Das geht ganz einfach mit dem Befehl cargo build --target $TRIPLE, sofern Sie wissen, wie das Kompilierungsziel ($TRIPLE) lauten soll. Glücklicherweise finden Sie die Antwort in der Datei .cargo/config.toml in der Vorlage:
tail -n6 .cargo/config.toml[build]
# Waehlen Sie EINES dieser Kompilierungsziele aus
# target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
target = "thumbv7m-none-eabi" # Cortex-M3
# target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU)
# target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
Für die Cross-Kompilierung für die Cortex-M3-Architektur müssen wir thumbv7m-none-eabi verwenden. Dieses Ziel wird bei der Installation der Rust-Werkzeuge nicht automatisch installiert. Jetzt wäre ein guter Zeitpunkt, dieses Ziel zu den Werkzeugen hinzuzufügen, falls Sie dies noch nicht getan haben:
rustup target add thumbv7m-none-eabiDa das Kompilierungsziel thumbv7m-none-eabi in Ihrer Datei .cargo/config.toml als Standard festgelegt wurde, haben die beiden folgenden Befehle dieselbe Wirkung:
cargo build --target thumbv7m-none-eabi
cargo buildÜberprüfen
Nun haben wir eine nicht-native ELF-Binärdatei in target/thumbv7m-none-eabi/debug/app. Wir können sie mit cargo-binutils untersuchen.
Mit cargo-readobj können wir die ELF-Header ausgeben, um zu überprüfen, ob es sich um eine ARM- Binärdatei handelt.
cargo readobj --bin app -- --file-headersBitte beachten Sie:
--bin appist eine vereinfachte Schreibweise für die Überprüfung der Binärdatei untertarget/$TRIPLE/debug/app--bin appkompiliert die Binärdatei bei Bedarf auch (neu)
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0x0
Type: EXEC (Executable file)
Machine: ARM
Version: 0x1
Entry point address: 0x405
Start of program headers: 52 (bytes into file)
Start of section headers: 153204 (bytes into file)
Flags: 0x5000200
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 2
Size of section headers: 40 (bytes)
Number of section headers: 19
Section header string table index: 18Mit cargo-size lässt sich die Größe der Linker-Abschnitte der Binärdatei anzeigen.
cargo size --bin app --release -- -AWir verwenden --release, um die optimierte Version zu überprüfen.
app :
section size addr
.vector_table 1024 0x0
.text 92 0x400
.rodata 0 0x45c
.data 0 0x20000000
.bss 0 0x20000000
.debug_str 2958 0x0
.debug_loc 19 0x0
.debug_abbrev 567 0x0
.debug_info 4929 0x0
.debug_ranges 40 0x0
.debug_macinfo 1 0x0
.debug_pubnames 2035 0x0
.debug_pubtypes 1892 0x0
.ARM.attributes 46 0x0
.debug_frame 100 0x0
.debug_line 867 0x0
Total 14570Eine Auffrischung zum Thema ELF-Linker-Abschnitte
.textenthält die Programmbefehle.rodataenthält konstante Werte wie Zeichenfolgen.dataenthält statisch zugewiesene Variablen, deren Anfangswerte nicht Null- Die Datei
.bssenthält außerdem statisch zugewiesene Variablen, deren Anfangswerte sind Null.vector_tableist ein nicht standardmäßiger Abschnitt, in dem wir den Vektor speichern (interrupt) table- Die Abschnitte
.ARM.attributesund.debug_*enthalten Metadaten und werden nicht beim Flashen der Binärdatei auf das Zielgerät geladen werden.
WICHTIG: ELF-Dateien enthalten Metadaten wie Debug-Informationen, sodass ihre Größe auf der Festplatte nicht genau den Speicherplatz widerspiegelt, den das Programm beanspruchen wird, wenn es auf ein Gerät geflasht wird. Verwenden Sie immer cargo-size, um zu überprüfen, wie groß eine Binärdatei tatsächlich ist.
Mit cargo-objdump lässt sich die Binärdatei disassemblieren.
cargo objdump --bin app --release -- --disassemble --no-show-raw-insn --print-imm-hexHINWEIS: Falls der obige Befehl die Fehlermeldung „Unbekanntes Befehlszeilenargument“ ausgibt, siehe den folgenden Fehlerbericht: https://github.com/rust-embedded/book/issues/269
HINWEIS Diese Ausgabe kann auf Ihrem System abweichen. Neuere Versionen von rustc, LLVM und Bibliotheken können unterschiedlichen Assemblercode erzeugen. Wir haben einige der Befehle gekürzt, um den Ausschnitt kurz zu halten.
app: file format ELF32-arm-little
Disassembly of section .text:
main:
400: bl #0x256
404: b #-0x4 <main+0x4>
Reset:
406: bl #0x24e
40a: movw r0, #0x0
< .. truncated any more instructions .. >
DefaultHandler_:
656: b #-0x4 <DefaultHandler_>
UsageFault:
657: strb r7, [r4, #0x3]
DefaultPreInit:
658: bx lr
__pre_init:
659: strb r7, [r0, #0x1]
__nop:
65a: bx lr
HardFaultTrampoline:
65c: mrs r0, msp
660: b #-0x2 <HardFault_>
HardFault_:
662: b #-0x4 <HardFault_>
HardFault:
663: <unknown>Ausführen
Als Nächstes schauen wir uns an, wie man ein eingebettetes Programm auf QEMU ausführt! Diesmal verwenden wir das hello-Beispiel, das tatsächlich etwas tut. Standardmäßig nutzt dieses Beispiel [defmt] und RTT, um Text auszugeben.
HINWEIS defmt ist eine Abhängigkeit eines Drittanbieters (d. h. keine Kernkomponente), die im Embedded-Rust-Ökosystem weit verbreitet ist.Um die von defmt im Host erzeugten Nachrichten lesen und entschlüsseln zu können, müssen wir die RTT-Transportausgabe auf „Semihosting“ umstellen. Bei Verwendung von echter Hardware erfordert dies eine Debug-Sitzung, bei Verwendung von QEMU funktioniert dies jedoch einfach so.
Stellen wir die Abhängigkeiten um:
cargo remove defmt-rtt
cargo add defmt-semihostingÖffnen Sie src/lib.rs und ersetzen Sie use defmt_rtt as _; durch use defmt_semihosting as _;.
Nun können wir das Beispiel kompilieren:
cargo build --bin helloDie ausgegebene Binärdatei befindet sich unter target/thumbv7m-none-eabi/debug/hello.
Um diese Binärdatei unter QEMU auszuführen, reicht in der Regel der folgende Befehl aus:
qemu-system-arm \
-cpu cortex-m3 \
-machine lm3s6965evb \
-nographic \
-semihosting-config enable=on,target=native \
-kernel target/thumbv7m-none-eabi/debug/helloDa wir in unserem Fall defmt verwenden, kann der Host die Ausgabe nicht dekodieren. Stattdessen benötigen wir ein Werkzeug von Ferrous Systems namens qemu-run:
git clone git@github.com:knurling-rs/defmt.git
cd defmt/qemu-run/
cargo run -- --machine lm3s6965evb ../qemu-rs/target/thumbv7m-none-eabi/debug/helloHello, world!Der Befehl sollte nach der Ausgabe des Textes erfolgreich beendet werden (Exit-Code = 0). Unter *nix können Sie dies mit dem folgenden Befehl überprüfen:
echo $?0Schauen wir uns diesen QEMU-Befehl einmal genauer an:
qemu-system-arm. Dies ist der QEMU-Emulator. Es gibt einige Varianten dieser QEMU-Binärdateien; diese hier führt eine vollständige System-Emulation von ARM-Rechnern durch, daher der Name.-cpu cortex-m3. Damit wird QEMU angewiesen, eine Cortex-M3-CPU zu emulieren. Durch die Angabe des CPU-Modells lassen sich einige Kompilierungsfehler erkennen: Wenn man beispielsweise ein Programm ausführt, das für den Cortex-M4F kompiliert wurde, der über eine Hardware-FPU verfügt, löst dies bei QEMU während der Ausführung einen Fehler aus.-machine lm3s6965evb. Damit wird QEMU angewiesen, das LM3S6965EVB zu emulieren, ein Evaluierungsboard, das einen LM3S6965-Mikrocontroller enthält.-nographic. Damit wird QEMU angewiesen, seine grafische Benutzeroberfläche nicht zu starten.-semihosting-config (..). Damit wird QEMU angewiesen, Semihosting zu aktivieren. Semihosting ermöglicht es dem emulierten Gerät unter anderem, die Host-Ausgabe (stdout), die Host-Fehlerausgabe (stderr) und die Host-Eingabe (stdin) zu nutzen sowie Dateien auf dem Host zu erstellen.-kernel $file. Damit wird QEMU mitgeteilt, welche Binärdatei auf der emulierten Maschine geladen und ausgeführt werden soll.
Das Eintippen dieses langen QEMU-Befehls ist viel zu mühsam! Wir können einen benutzerdefinierten Runner einrichten, um den Vorgang zu vereinfachen. In der Datei .cargo/config.toml gibt es einen auskommentierten Runner, der QEMU aufruft; entfernen wir die Auskommentierung:
head -n3 .cargo/config.toml[target.thumbv7m-none-eabi]
# Entferne den Kommentar hier, damit `cargo run` Programme auf QEMU ausfuehrt
runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"
Dieser Runner gilt nur für das Ziel thumbv7m-none-eabi, das unser Standard-Kompilierungsziel ist. Nun kompiliert cargo run das Programm und führt es auf QEMU aus:
cargo ru(
level: 2 + whole
)--release Compiling app v0.1.0 (file:///tmp/app)
Finished release [optimized + debuginfo] target(s) in 0.26s
Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target/thumbv7m-none-eabi/release/examples/hello`
Hello, world!Fehlerbehebung (Debugging)
Das Debuggen ist für die Embedded-Entwicklung von entscheidender Bedeutung. Schauen wir uns einmal an, wie es funktioniert.
Das Debuggen eines Embedded-Geräts erfolgt remote, da das Programm, das wir debuggen möchten, nicht auf dem Rechner läuft, auf dem das Debugger-Programm (GDB oder LLDB) ausgeführt wird.
Beim Remote-Debugging sind ein Client und ein Server beteiligt. In einer QEMU-Umgebung ist der Client ein GDB- (oder LLDB-)Prozess und der Server der QEMU-Prozess, auf dem auch das eingebettete Programm läuft.
In diesem Abschnitt verwenden wir das bereits kompilierte Beispiel hello.
Der erste Schritt beim Debuggen besteht darin, QEMU im Debugging-Modus zu starten:
qemu-system-arm \
-cpu cortex-m3 \
-machine lm3s6965evb \
-nographic \
-semihosting-config enable=on,target=native \
-gdb tcp::3333 \
-S \
-kernel target/thumbv7m-none-eabi/debug/examples/helloDieser Befehl gibt nichts auf der Konsole aus und blockiert das Terminal. Wir haben diesmal zwei zusätzliche Flags übergeben:
-gdb tcp::3333. Damit wird QEMU angewiesen, auf eine GDB-Verbindung über den TCP-Port 3333 zu warten.-S. Damit wird QEMU angewiesen, die Maschine beim Start einzufrieren. Ohne diese Option hätte das Programm das Ende der Funktionmainerreicht, bevor wir die Gelegenheit gehabt hätten, den Debugger zu starten!
Als Nächstes starten wir GDB in einem anderen Terminal und weisen es an, die Debug-Symbole des Beispiels zu laden:
gdb-multiarch -q target/thumbv7m-none-eabi/debug/examples/helloHINWEIS: Möglicherweise benötigen Sie anstelle von gdb-multiarch eine andere Version von gdb, je nachdem, welche Sie im Kapitel zur Installation installiert haben. Dies könnte auch arm-none-eabi-gdb oder einfach nur gdb sein.
Anschließend stellen wir innerhalb der GDB-Shell eine Verbindung zu QEMU her, das auf dem TCP-Port 3333 auf eine Verbindung wartet.
target remote :3333Remote debugging using :3333
Reset () at $REGISTRY/cortex-m-rt-0.6.1/src/lib.rs:473
473 pub unsafe extern "C" fn Reset() -> ! {Sie werden feststellen, dass der Prozess angehalten wurde und der Programmzähler auf eine Funktion namens Reset zeigt. Das ist der Reset-Handler: das, was Cortex-M-Kerne beim Booten ausführen.
Beachten Sie, dass gdb in manchen Konfigurationen anstelle der oben gezeigten Zeile
Reset () at $REGISTRY/cortex-m-rt-0.6.1/src/lib.rs:473möglicherweise Warnungen wie die folgenden ausgibt:
core::num::bignum::Big32x40::mul_small () at src/libcore/num/bignum.rs:254src/libcore/num/bignum.rs: No such file or directory.Das ist ein bekannter Fehler. Sie können diese Warnungen getrost ignorieren, da Sie sich höchstwahrscheinlich bei
Reset()befinden.
Dieser Reset-Handler ruft schließlich unsere Hauptfunktion auf. Lassen Sie uns den gesamten Weg dorthin mithilfe eines Haltepunkts und des Befehls continue überspringen. Um den Haltepunkt zu setzen, schauen wir uns zunächst mit dem Befehl list an, an welcher Stelle in unserem Code wir anhalten möchten.
list mainDadurch wird der Quellcode aus der Datei „examples/hello.rs“ angezeigt.
6 use panic_halt as _;
7
8 use cortex_m_rt::entry;
9 use cortex_m_semihosting::{debug, hprintln};
10
11 #[entry]
12 fn main() -> ! {
13 hprintln!("Hello, world!").unwrap();
14
15 // exit QEMUWir möchten einen Haltepunkt direkt vor „Hello, world!“ setzen, das sich in Zeile 13 befindet. Dazu verwenden wir den Befehl break:
break 13Wir können gdb nun mit dem Befehl continue anweisen, bis zu unserer Hauptfunktion weiterzulaufen:
continueContinuing.
Breakpoint 1, hello::__cortex_m_rt_main () at examples\hello.rs:13
13 hprintln!("Hello, world!").unwrap();Wir sind nun fast bei dem Code angelangt, der „Hello, world!“ ausgibt. Machen wir weiter mit dem Befehl next.
next16 debug::exit(debug::EXIT_SUCCESS);An dieser Stelle sollte auf dem Terminal, auf dem qemu-system-arm läuft, „Hello, world!“ angezeigt werden.
$ qemu-system-arm (..)
Hello, world!Ein erneuter Aufruf von next beendet den QEMU-Prozess.
next[Inferior 1 (Remote target) exited normally]Sie können die GDB-Sitzung nun beenden.
quit