Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

URL: https://www.qemu.org/

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-generate

Erstellen 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/app
cd app

Verwendung von git

Das Repository klonen

git clone https://github.com/rust-embedded/cortex-m-quickstart app
cd app

Und 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 = false

Keines 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 app

Oder 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-eabi

Da 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-headers

Bitte beachten Sie:

  • --bin app ist eine vereinfachte Schreibweise für die Überprüfung der Binärdatei unter target/$TRIPLE/debug/app
  • --bin app kompiliert 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: 18

Mit cargo-size lässt sich die Größe der Linker-Abschnitte der Binärdatei anzeigen.

cargo size --bin app --release -- -A

Wir 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              14570

Eine Auffrischung zum Thema ELF-Linker-Abschnitte

  • .text enthält die Programmbefehle
  • .rodata enthält konstante Werte wie Zeichenfolgen
  • .data enthält statisch zugewiesene Variablen, deren Anfangswerte nicht Null
  • Die Datei .bss enthält außerdem statisch zugewiesene Variablen, deren Anfangswerte sind Null
  • .vector_table ist ein nicht standardmäßiger Abschnitt, in dem wir den Vektor speichern (interrupt) table
  • Die Abschnitte .ARM.attributes und .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-hex
HINWEIS: 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 hello

Die 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/hello

Da 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/hello
Hello, 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 $?
0

Schauen 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/hello

Dieser 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 Funktion main erreicht, 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/hello

HINWEIS: 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 :3333
Remote 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:473 möglicherweise Warnungen wie die folgenden ausgibt:

core::num::bignum::Big32x40::mul_small () at src/libcore/num/bignum.rs:254 src/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 main

Dadurch 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 QEMU

Wir möchten einen Haltepunkt direkt vor „Hello, world!“ setzen, das sich in Zeile 13 befindet. Dazu verwenden wir den Befehl break:

break 13

Wir können gdb nun mit dem Befehl continue anweisen, bis zu unserer Hauptfunktion weiterzulaufen:

continue
Continuing.

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.

next
16          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