-
Notifications
You must be signed in to change notification settings - Fork 28
Debugging
There can be multiple reasons for an application not to be supported by HermiTux. The first one is: missing syscall implementation. Please see this issue on GitHub to understand how to detect such events, and how to implement a new system call in HermiTux.
Another reason is: because we only partially support some system calls (and other things in the ABI such as the ELF auxiliary vector content), this can lead to undefined behavior. It's a little bit more tricky to detect and find the root cause. This generally ends up in memory corruption and leads either to a page fault or a general protection fault. To solve these issue debugging is needed as we need to understand what lead to the fault. A very important thing that helps is to enable the verbose mode, which can give you information about the fault. Here is an example of the log I obtain when I voluntarily generate a page fault using this example program:
[0.000][0:1][ERROR] Page Fault Exception (14) on core 0 at cs:ip = 0x8:0x400276, fs = 0x6015d8, gs = 0, rflags 0x11246, task = 1, addr = 0, error = 0x2 [ supervisor data write not present ]
[0.000][0:1][ERROR] rax 0, rbx 0xb5a6b0, rcx 0x400cd3, rdx 0, rbp 0x1, rsp 0xb5a650 rdi 0x4, rsi 0xb5a590, r8 0x601040, r9 0xb5a61f, r10 0x1, r11 0x1202, r12 0xb5a6c0, r13 0x400266, r14 0xb5ab60, r15 0xc0408
[0.000][0:1][ERROR] Heap 0x606000 - 0x607000
This contains multiple helpful things:
- the instruction pointer (here
0x400276
), can be used to identify the faulty instruction. It is useful to useobjdump --source
to decompile the program (there is amake objdump
target for that in the template makefile) and look at the corresponding instruction. For a static binary, one can directly used the instruction pointer's value, but remember that with dynamically compiled programs there is a fixed offset to add to whatever addressobjdump
will give you (currently that offset is fixed and is0x400000
). You can also use addr2line to get the C code line that triggered the fault. If the value of%rip
is located under0x200000
, you need to disassemble the kernel, otherwise disassemble the application. - the register values
Enable the verbose mode by setting the HERMIT_VERBOSE
environment variable to 1 or the VERBOSE
variable to 1 in the
template Makefile.
Note that this feature is not super stable and it may be required to fall back on "manual" debugging with
objdump
as explained above.
-
As they live in the same address space, it is possible to conjointly debug the guest application as well as the kernel. On the contrary to regular unikernels, with HermiTux the kernel and the application reside in different binaries. It is possible to load symbols from multiple binaries using the gdb command
add-symbol-file
: this is how we add the application symbols after calling gdb with the kernel binary as parameter. To load the application symbols we also need to instruct gdb of the location in the virtual address space for the application text section. The application is loaded at 0x400000 but the.text
section may be located a bit further in the address space. We can get that information usingreadelf -S
and look at the.text
location. Seetools/hermitux-gdb
for a gdb wrapper that is doing all of this automatically so that the user can fully transparently call it as he would call gdb. -
Adding and removing breakpoints is supported.
-
Inspecting memory for checking the value of a variable or printing the stack is supported. Memory writes, for example to modify a variable at runtime is also supported.
-
Inspecting and modifying the registers value is supported (for example one needs to have access to the value of
%rsp
to print the stack. -
Single step execution is supported, both at the C code line as well as assembly instruction level (
layout asm
works). -
The hypervisor automatically breaks on the first guest instruction (in a similar fashion as qemu
-S
parameter), making boot process debugging possible, including the part written in assembly. -
Hardware breakpoints are supported (uhyve can access and modify the VCPU state)
-
Watchpoint and conditional breakpoints work
-
Interrupting the VM while it executes is not supported. It could probably be done but would involve a complete redesign of the GDB stub as a thread separated from uhyve execution flow (in order to received GDB asynchronous interrupt requests
-
Multi-threading and dynamically compiled binaries are unfortunately not supported for now.
We provide a gdb wrapper (tools/hermitux-gdb
) for quick and easy joint
debugging of application and kernel. First, edit this script and set the
HERMITUX_BASE
variable to point on HermiTux codebase root folder on your
filesystem.
Next, launch an HermiTux application with the HERMIT_DEBUG
envirnonment
variable set to 1
:
$ HERMIT_DEBUG=1 HERMIT_ISLE=uhyve HERMIT_TUX=1 path/to/proxy path/to/linux/binary
proxy: Waiting for a debugger, see tools/hermitux-gdb for info.
In another terminal use the wrapper:
tools/hermitux-gdb path/to/linux/binary
First let's look at the exact virtual address the linux binary text section:
readelf -SW path/to/linux/binary
# ...
[ 6] .text PROGBITS 0000000000400490 # ...
# ...
Here we can see that .text
starts at 0x400490
. Launch the HermiTux
application with debug mode enabled:
$ HERMIT_DEBUG=1 HERMIT_ISLE=uhyve HERMIT_TUX=1 path/to/proxy path/to/linux/binary
proxy: Waiting for a debugger, see tools/hermitux-gdb for info.
In another terminal start gdb with the HermiTux kernel as parameter and add the linux binary symbols based on the relevant virtual address, then connect to the remote target:
gdb path/to/hermitux/kernel
(gdb) add-symbol-file path/to/linux/binary 0x400490
(gdb) target remote :1234
x/100xw $rsp
Hermitux support GDB debugging of both application and kernel through the remote
gdb capabilities. Most of the code was leveraged from solo5/ukvm, see the files
with gdb
in their names here:
https://github.com/Solo5/solo5/tree/master/ukvm
Implementing support for a gdb remote target is relatively easy. In our case, the target is the uhyve hypervisor. It communicates with a gdb client using a well defined protocol: https://sourceware.org/gdb/onlinedocs/gdb/Remote-Protocol.html
This protocol is implemented using a simple socket connection between uhyve and the gdb client.
To be supported by gdb, the target must be able to:
- Communicate with the client on a debug exception (ex: breakpoint reached).
With uhyve, a debug exception within the guest results in a trap to the
hypervisor with
KVM_EXIT_DEBUG
status, thus we can send a notification to the gdb client over the socket connection. - Read and modify the CPU registers: this is doable from the hypervisor using
KVM_SET_REGS
,KVM_SET_SREGS
,KVM_GET_REGS
,KVM_GET_SREGS
. - Read and modify the guest virtual address space. This is also doable from uhyve as the guest memory is also mapped in the hypervisor address space.
Some modifications were necessary to adapt the ukvm gdb stub to uhyve. Ukvm is expecting to run the solo5 unikernel, which differs from HermiTux in the way the virtual address space is layed out. When gdb request some guest memory access (for example inspecting a variable or setting a breakpoint), the target needs to translate a guest virtual address into a virtual address within the hypervisor virtual address space. This involve computing the corresponding guest physical address as the hypervisor only has visibility over the guest physical memory. With solo5, there is a direct mapping between guest virtual and guest physical memory, so the translation is easy:
guest_physical = guest_virtual + offset
Where offset
is a constant equal to the starting address of the big hypervisor
buffer correspondign to the guest physical memory.
With HermiTux, it is a bit more complicated as while static memory is directly mapped (eveyrhing that comes from the kernel & binary ELF sections), dynamic memory such as kernel and application stacks, kernel and application heaps, are mapped using regular 4-level page tables.
Thus, with HermiTux, in order to compute the guest physical address from the
virtual one, we use the KVM feature KVM_TRANSLATE
which walks from the
hypervisor the guest page tables.