Timeline
Timeline
2025-11-23
- init
This article introduces various debugging techniques for QEMU, including source code debugging using gdb and vscode, remote debugging using the built-in gdbserver, and observing guest states through the logging system. Furthermore, this article discusses in detail the usage of the tracing tool, the declaration and addition process of trace-events, and the tracing architecture supporting multiple backends, providing comprehensive guidance for tracking QEMU internal function execution and performance tuning.
Environment
1 | wget https://download.qemu.org/qemu-10.1.2.tar.xz |
Create .clangd
1 | CompileFlags: |
Source code debugging
Debugging QEMU source code does not require the same process as debugging the kernel, where you need to start it first and then attach via gdb; QEMU itself can be executed directly locally. (Note: enable-debug is required during compilation)
gdb
You can debug directly using gdb
1 | $ gdb --args ./build/riscv64-softmmu/qemu-system-riscv64 \ |
vscode
.vscode/launch.json
1 | { |
Remote debugging
QEMU has a built-in gdbserver that can control the guest processor. Users can connect via any gdb client.
The specific operation commands are given below:
1 | $QEMU $QEMU_ARGS -s -S |
-s: Start the gdbstub and set the port number to 1234-S: Make QEMU stop at the first instruction of the guest and wait for the gdb client to connect.
If you want to specify the debugging port number, you can use the following command:
1 | $QEMU $QEMU_ARGS -gdb tcp::<your-port> -S |
Then use the gdb corresponding to the architecture to connect to the QEMU gdbstub:
1 | $ARCH-gdb $BINARY -ex "target remote localhost:1234" |
Log debugging
QEMU has a flexible logging system that makes it very convenient to observe various states of the guest (instruction flow, interrupts, exceptions, system calls).
The basic command format is given below:
1 | $QEMU $QEMU_ARGS -d <log-type,...> -D <log-file-name> |
-d: Specify the type of log, multiple can be used, separated by,commas-D: Specify the file path for outputting the log; if this parameter is not added, it defaults to outputting to the command line terminal
You can use the following command to view the currently supported log types:
1 | $QEMU -d ? |
We list some commonly used combinations.
- If we want to observe how TCG translates instructions, we can use the following command:
1 | $QEMU $QEMU_ARGS -d in_asm,op,out_asm -D tcg.log |
- If we want to observe the state of the CPU (register values, interrupts/exceptions), we can use the following command:
1 | $QEMU $QEMU_ARGS -d exec,cpu,int -D cpu.log |
- If you want to obtain the precise instruction flow executed by the CPU, you need to set each TB to contain only one instruction, you can use the following command:
1 | $QEMU $QEMU_ARGS --accel tcg,one-insn-per-tb=on -d exec,cpu,int -D cpu.log |
Tracing events
Reference:
QEMU has a very useful debugging tool called tracing, which can be used to track the execution of QEMU internal functions, as well as for performance tuning. For example, to track the memory access of a guest program, the read and write records of QEMU’s memory_region can be printed out, as long as the corresponding trace-event is registered.
Quick start
In QEMU’s startup options, add the trace parameter to specify the events to be tracked. Here, tracking memory region access events is taken as an example:
1 | $ qemu-system-riscv64 -M virt --trace "memory_region_ops_*" # * represents the preceding character as the matching object |
We can find the mr-related trace-event in the system/trace-events file of the QEMU source code:
1 |
|
If you want to enable multiple trace-events, you just need to append them in the startup options--trace <name>。
To avoid lengthy parameters, you can record the trace-events to be tracked in a configuration file and then load it:
1 | echo "memory_region_ops_*" >/tmp/events |
At the same time, tracing also supports outputting to a file. Let’s modify the QEMU command above:
1 | qemu-system-riscv64 -M --trace events=/tmp/events,file=/tmp/event.log ... |
If you don’t want to enable it in the QEMU startup options, we can also dynamically enable it in the QEMU monitor, which is more flexible. The operation is as follows:
1 | $ qemu-system-riscv64 -M virt -monitor stdio -S -display none |
Another benefit of using tracing in the monitor is that you can use the Tab key to complete commands, so you don’t have to painstakingly look up the trace-events supported by each component from the source code.
can also use
info trace-eventscommand queries the supported trace events.Or use
trace-filecommand outputs the trace log to a file.
tracing supports multiple backends, and uses QEMU’s log as the backend by default.
Introduction to trace-events
In every level of the QEMU source code directory, you can add a trace-events file. You just need to declare its relative path in the top-level meson.build directory, and then you can add custom trace-events to it:
1 | if have_system |
These trace-events files in the subdirectories need to be processed by tracetool to automatically generate trace code.
During the QEMU build process, each trace-events file will be processed by the tracetool script, automatically generating trace-related code under the /trace/ path, which mainly includes the following files:
1 | - trace-<子目录名>.c |
Here represents replacing ‘/’ in the subdirectory path with ‘_’ .
Example
accel/kvm/trace-events
1 | trace/trace-accel_kvm.c |
The various trace-events files will be merged into a single trace-events-all file,trace/trace-events-all
This file will also be installed to the /usr/share/qemu directory. This merged file will be used by the simpletrace.py script provided by QEMU to subsequently analyze trace records in the simple trace data format.
In the source code directory, the generated files are not directly included; instead, the local trace.h file is referenced via #include, without any subdirectory path prefix。
For example, io/channel-buffer.c would reference it like this:
1 |
Additionally, we must manually create the io/trace.h file,
and include the corresponding trace/trace-.h file in it, which is generated in :
1 | echo '#include "trace/trace-io.h"' > io/trace.h |
It is worth noting: although trace.h can be introduced from outside the subdirectory where the source file is located, it is generally not recommended to do so.
It is strongly recommended that all trace-events be declared directly in the subdirectory where they are used. The only exception is that some shared trace events are defined in the trace-events file in the top-level directory.
The trace files generated in the top-level directory will have the trace/trace-root prefix, rather than just trace , to avoid ambiguity between the trace.h in the current directory and the files in the top-level directory.
Example
1 | src/ |
After building:
1 | build/trace/ |
Add a new trace-event
Adding a new trace-event only requires two steps:
- Declare the trace-event in the trace-events file of the corresponding directory
- Add the function call for this event in the target source code that needs to be debugged.
Taking tracing QEMU memory allocation and deallocation as an example, the format of a trace-event is as follows:
1 | qemu_vmalloc(size_t size, void *ptr) "size %zu ptr %p" |
Each event declaration will start with the event name, followed by parameters, and finally a format string for pretty printing。
The format string should reflect the types defined in the trace event. tracing only supports basic scalar types (char, int, long), and does not support floating-point types (float, double)
Pay special attention to int64_t and uint64_t types use PRId64 and PRIu64 respectively, which ensures portability between 32-bit and 64-bit platforms.
The format string must not end with a newline character. It is the backend’s responsibility to adjust the line ending for correct logging.
Once the trace-event is defined, call it directly from the target source code, as shown in the example below:
1 |
|
If there are multiple trace events in a function, you shouldadd a unique identifier at the end of the nameto distinguish them.
In some cases, it may be necessary to perform relatively complex calculations to generate values that are only used as parameters for trace functions. In such cases, you can use the following function to protect this calculation logic:
1 | trace_event_get_state_backends() |
When an event is disabled at compile time or runtime, the related calculations will be skipped. If the event is disabled at the compile stage, this check will not incur any performance overhead.
The example code is as follows.
1 |
|
The following situations are very suitable for debugging using tracing:
- Tracking state changes in the code. Key points in the code usually involve state changes, such as start, stop, allocation, release, etc. State changes are ideal trace events because they help understand the system execution process.
- Tracking guest operations. Guest I/O accesses (such as reading device registers) are good trace events and can be used to analyze guest interaction behavior.
- Use associated fields to understand the context of a single line of trace output. For example, track the pointer returned by malloc and its usage as a parameter for free, so that malloc and free operations can be matched. Trace events without context have limited practical value.
Introduction to trace backends
QEMU’s tracing adopts a front-end/back-end separated design, supporting multiple backends. In addition to the log mentioned above, it also supports the lighter-weight simple, as well as ftrace and dtrace.
We can also add more backend support through the tracetool script.
To enable different backends, you can use the following QEMU compilation command:
1 | ./configure --enable-trace-backends=simple,dtrace |
By running./configure --helpto view all supported backends. If no backend is explicitly selected, the configuration will default to the log backend.
Analyzing trace files
Let’s take the simple backend as an example. This backend writes binary trace logs to a file through an independent thread, which has lower overhead compared to the log backend.
At the same time, the QEMU source repository provides Python scripts for offline trace file analysis. Although the functionality may not be as powerful as specific platform or third-party trace backends, it is portable and requires no special library dependencies.
To format using the simpletrace.py script, you need the trace-events-all file and the binary trace file:
1 | ./scripts/simpletrace.py <trace-events-all> <trace-log> |
You must ensure that the trace-events-all file used is the same as the one generated when building QEMU, otherwise the trace event declarations may have changed, leading to inconsistent output.
Reference:

