QEMU + GNU Debugger Basic Tutorial
#1
QEMU + GNU Debugger Basic Tutorial

Editor's NOTE: I'm very new to QEMU, GDB, and general assembling/linking. So if anybody has any improvements or corrections for this tutorial, please share them. Thank you.



Chapter 1: Intro

NOTE: Guide is for Linux only. Verified to work on Debian 10 & Debian 11.

Instead of using something like Dolphin to rig up an environment to test simulate PowerPC code/instructions, you can instead use the QEMU emulator with the GNU Debugger.

The QEMU and GNU programs support a wide variety of languages, thus you can use those programs for the following...
  • ARM 64-bit aka AAarch64 (for Nintendo Switch)
  • ARM 32-bit (for Starlet on Nintendo Wii)
  • PPC 64-bit (for Xbox360 & PS3)
  • PPC 32-bit (for Nintendo Gamecube, Broadway on Wii, & Wii U)

Keep in mind that the languages are "generic" for the most part. The Xbox360, PS3, Wii U, Wii, and Gamecube all use unique CPUs that have special/additional instructions & registers to their conventional counterparts. The Switch uses a "generic" Cortex-A57 CPU which uses the ARMv8-a (8.0) language.

The GNU Debugger can specify some CPUs. For example, you can specify PowerPC 32-bit CPU 750cl to try to mimic as closely as possible for Broadway. Regarding Nintendo Switch, you can specify its exact CPU (cortex-a57).

The great thing about QEMU+GDB is that you can test C code, not just basic Assembly files. The following guide will cover debugging a basic Hello World source written in C. Later on, a quick overview of debugging bare-bones Assembly files will also be covered.



Chapter 2: Software Installation

Update & Upgrade your System then Reboot

Code:
sudo apt-get update
sudo apt-get upgrade
sudo reboot


Install the GNU Compiler Software for the desired architecture

ARM 64 bit...
Code:
sudo apt-get install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu binutils-aarch64-linux-gnu-dbg 


ARM 32 bit...
Code:
sudo apt-get install gcc-arm-linux-gnueabihf binutils-arm-linux-gnueabihf binutils-arm-linux-gnueabihf-dbg


PPC 64 bit...
Code:
sudo apt-get install gcc-powerpc64-linux-gnu binutils-powerpc64-linux-gnu binutils-powerpc64-linux-gnu-dbg


PPC 32 bit...
Code:
sudo apt-get install gcc-powerpc-linux-gnu binutils-powerpc-linux-gnu binutils-powerpc-linux-gnu-dbg


Install QEMU Emulator & GNU Debugger
Code:
sudo apt-get install qemu-user qemu-user-static gdb-multiarch build-essential


NOTE: There are other qemu packages such as qemu and qemu-system, we only need user and user-static.



Chapter 3: C file creation and compilation

Create the following Hello World C file. Save it as hello_world.c

Code:
#include <stdio.h>
#include <stdlib.h>

int main() {
    puts("hello world");
    return EXIT_SUCCESS;
}

Compile an executable file from your C source

ARM64:
Code:
aarch64-linux-gnu-gcc -ggdb3 -o hello_world hello_world.c -static -mcpu=cortex-a57


ARM32:
Code:
arm-linux-gnueabihf-gcc -ggdb3 -o hello_world hello_world.c -static


NOTE: The tags of... "-mbig-endian -march=armv5te -mcpu=arm926ej-s" should be included, but I can't get the file to be compiled when these tags are applied. So if anyone is very familiar with GCC, please let me know how to remedy this.

PPC64:
Code:
powerpc64-linux-gnu-gcc -ggdb3 -o hello_world hello_world.c -static


PPC32:
Code:
powerpc-linux-gnu-gcc -ggdb3 -o hello_world hello_world.c -static -mcpu=750


NOTE: PPC64 and PPC32 default to big endian. Extra command tags for endianness are not required.

About command tags:
-o = Create object file (executable)
-ggdb3 = Use GNU Debugging symbols
-static = Use static libraries



Chapter 4. Run C file

Launch the file on QEMU

ARM64:
Code:
qemu-aarch64 -L /usr/aarch64-linux-gnu -g 1234 ./hello_world


ARM32:
Code:
qemu-arm -L /usr/arm-linux-gnueabihf -g 1234 ./hello_world


PPC64:
Code:
qemu-ppc64 -L /usr/powerpc64-linux-gnu -g 1234 ./hello_world


PPC32:
Code:
qemu-ppc -L /usr/powerpc-linux-gnu -g 1234 ./hello_world


About command tags:
-L /user/xxxxx = Choose which elf interpreter to use
-g xxxxx = Set port number for GDB connection

QEMU & GDB need to run on a port. You can have multiple instances of QEMU+GDB programs running, but they cannot all use the same port.

At this moment you will notice that the terminal command to run QEMU is not doing anything...

[Image: gdb00.png]

This is exactly what you want to see. QEMU is waiting on the GNU Debugger to be launched. On a second terminal, launch the Debugger using the follow terminal command. Do NOT close/exit the first terminal!

ARM64:

Code:
gdb-multiarch -q --nh \
  -ex 'set architecture arm64' \
  -ex 'set sysroot /usr/aarch64-linux-gnu' \
  -ex 'file hello_world' \
  -ex 'target remote localhost:1234' \
  -ex 'break main' \
  -ex continue \
  -ex 'layout split' \
  -ex 'layout next' \ 
  -ex 'layout regs'

 
ARM32:

Code:
gdb-multiarch -q --nh \
  -ex 'set architecture arm' \
  -ex 'set sysroot /usr/arm-linux-gnueabihf' \
  -ex 'file hello_world' \
  -ex 'target remote localhost:1234' \
  -ex 'break main' \
  -ex continue \
  -ex 'layout split' \
  -ex 'layout next' \
  -ex 'layout regs'

 
PPC64:

Code:
gdb-multiarch -q --nh \
  -ex 'set architecture ppc64' \
  -ex 'set sysroot /usr/powerpc64-linux-gnu' \
  -ex 'file hello_world' \
  -ex 'target remote localhost:1234' \
  -ex 'break main' \
  -ex continue \
  -ex 'layout split' \
  -ex 'layout next' \
  -ex 'layout regs'

 
PPC32:

Code:
gdb-multiarch -q --nh \
  -ex 'set architecture ppc' \
  -ex 'set sysroot /usr/powerpc-linux-gnu' \
  -ex 'file hello_world' \
  -ex 'target remote localhost:1234' \
  -ex 'break main' \
  -ex continue \
  -ex 'layout split' \
  -ex 'layout next' \
  -ex 'layout regs'

 
About ex tags:
  • set architecture is self explanatory
  • set sysroot is for setting the directory that contains the targeted libraries, this must match what elf interpreter you used in the qemu command
  • file is self explanatory
  • target remote machine:portnumber is to tell gdb what machine and port QEMU is running on. Ofc port number used here must match what was used in the qemu terminal command
  • break main is to set a breakpoint on the main function
  • continue is to tell gdb to go ahead and run the program, do NOT breakpoint it at the very first assembly instruction
  • layout split will split the terminal into two halves where you can see more information simultaneously
  • layout regs tells gdb to place GPRs + some SPRs into the upper half of the split layout

Since break main and continue are applied, this tells GDB to run the program, and stop at the first instruction at the main function.
 
Notice how the port number in the GDB terminal command matches what was used in the QEMU terminal command.



Chapter 5: Basic GNU Debugging Commands, Stepping Thru the C File

At this point your C program is paused at 'main' waiting for further actions. Your GDB should look like this. Registers may or may not be available when you first boot GDB (we will address this shortly). For the picture below in my example, you will see our registers aren't available yet. You will also see we are at the start of our C program.

[Image: gdb01.png]

GDB comes with a large set of Debugging Commands, here's a quick list of useful ones~
 
GNU Debugging Commands:
  • step = step C program by 1 line (in Assembly view, this will step execution by 1 instruction)
  • stepi = step execution by 1 instruction (for Assembly view only)
  • nexti = step the very next instruction below, and bypass branches and function calls when encountered
  • break [function name] = set an instruction breakpoint at a function
  • step = step to next line in C program (for assembly files, this will do the same as stepi)
  • next = allow program to run til next instruction breakpoint
  • quit = quit
  • delete = delete all instruction breakpoints
  • info vector = list FPRs as Vector data first, then as Float data
  • layout next = swap to different view (C vs Assembly)
  • layout prev = swap to your previous view


Okay so we have GDB running, let's practice some commands. We can switch to Assembly view of our C file by using this command..

Code:
layout next


[Image: gdb02.png]

Great. Let's swap back to C view using this command...

Code:
layout prev


[Image: gdb03.png]

If registers aren't available upon boot, this can always be remedied via just 1 step (or 1 stepi when debugging a view of Assembly). The registers can now be seen. We can use the step command to step one line of C code, like this...

[Image: gdb04.png]

We can keep using the step command until you will see that the C source becomes unavailable. This is because our program has completed all execution. We can now use quit to exit GDB. Press Y when prompted.

[Image: gdb05.png]

When you have finally exited GDB, take a look at your terminal that was running QEMU. You will see that the QEMU process has been terminated.

[Image: gdb06.png]

NOTE: Be sure to properly exit GDB (via the quit command) or else you will need to use a different port next time you run QEMU.



Chapter 6: GDB Memory Commands

Before we dive into debugging an Assembly file, lets go over how to view memory on the GNU Debugger. Viewing memory is a bit complicated, you cannot (afaik) view memory live on a separate layout or terminal.

Memory command template:
x/nfu addr

x = all gdb memory commands must start with a lower case x.

n = The count of how many units to display. Default value is 1.

f = Display format. Default is x (for hex). d is for signed decimal. u is for unsigned decimal. o is for octal. f is for float.

u = unit type. b for bytes. h for halfwords. w for words. g for doublewords. Default value is w (words).

addr = memory address

If nfu all are set to default values (which would also be the case if they were all omitted), then a the slash (/) is NOT needed in the memory command.

Example memory command showing 4 hex words at address 0x1234C
x/4xw 0x1234C

Instead of having to fill in an address, you can instead reference a register using the "$" symbol.

Example memory command showing 2 hex words located at the Stack Pointer:
x/2xw $sp



Chapter 7: Create Executable file from Bare-bones Assembly

Instead of writing the file in C, we can use an Assembly file without any standard libraries. Delete your original hello_world executable, so we can make a new one via Assembly.

Choose the following assembly source that you want to use and save it as hello_world.s. Be sure the "s" is lowercase. The source will use the emulated computer's system calls (via QEMU) to printf a message.

ARM 64bit:
Code:
.section .text
    .global _start

_start:
/* syscall write(int fd, const void *buf, size_t count) */
    mov x0, #1    
    ldr x1, =msg
    ldr x2, =len
    mov w8, #64 /*Syscall number for write fo ARM64*/
    svc #0

/* syscall exit(int status) */
    mov x0, #0
    mov w8, #93 /*Syscall number for exit for ARM64*/
    svc #0

msg:
    .asciz "Hello, ARM64!\n"
    len = . - msg

ARM 32bit:
Code:
.section .text
    .global _start

_start:
/* syscall write(int fd, const void *buf, size_t count) */
    mov r0, #1
    ldr r1, =msg
    ldr r2, =len
    mov r7, #4 /*Syscall number for write for ARM32*/
    svc #0

/* syscall exit(int status) */
    mov r0, #0
    mov r7, #1 /*Syscall number for exit for ARM32*/
    svc #0

msg:
    .asciz "Hello, ARM32!\n"
    len = . - msg

PPC 64bit:
Code:
.section .text
    .global _start
.section ".opd","aw"
    .align 3
_start:
    .quad   ._start,.TOC.@tocbase,0
    .previous
    .global  ._start
._start:
/* syscall write(int fd, const void *buf, size_t count) */
    li 3, 1      
    lis 4, msg@highest
    ori 4,4, msg@higher
    rldicr 4, 4, 32, 31
    oris 4, 4, msg@h
    ori 4, 4, msg@l
    li 5, len
    li 0, 4 /*Syscall number for write for PPC64*/
    sc
/* syscall exit(int status) */
    li 3, 1
    li 0, 1 /*Syscall number for exit for PPC64*/
    sc
   
msg:
    .asciz "Hello, PPC64!\n"
    len = . - msg

PPC 32bit:
Code:
.section .text
    .global _start

_start:
/* syscall write(int fd, const void *buf, size_t count) */
    li 3, 1
    lis 4, msg@ha
    addi 4, 4, msg@l
    li 5, len
    li 0, 4 /*Syscall number for write for PPC32*/
    sc

/* syscall exit(int status) */
    li 3, 0
    li 0, 1 /*Syscall number for exit for PPC32*/
    sc

msg:
    .asciz "Hello, PPC32!\n"
    len = . - msg

--

Side note: View Chapter 9 for more info about syscalls

To assemble the source into an executable, the 2 following terminal commands are required...

ARM64:

Code:
aarch64-linux-gnu-as -mcpu=cortex-a57 hello_world.s -o hello_world.o
aarch64-linux-gnu-ld hello_world.o -o hello_world


ARM32:

Code:
arm-linux-gnueabihf-as -march=armv5te -mcpu=arm926ej-s -mbig-endian hello_world.s -o hello_world.o
arm-linux-gnueabihf-ld -EB hello_world.o -o hello_world


PPC64:

Code:
powerpc64-linux-gnu-as -mregnames hello_world.s -o hello_world.o
powerpc64-linux-gnu-ld hello_world.o -o hello_world


PPC32:

Code:
powerpc-linux-gnu-as -mregnames -m750cl hello_world.s -o hello_world.o
powerpc-linux-gnu-ld hello_world.o -o hello_world


NOTE: PPC64 and PPC32 default to big endian. Extra tags for endianness are not required.



Chapter 7: Launch file, Stepping Instructions

You will notice that the QEMU and GDB terminal commands have been tweaked since we are now using an Assembly file.

Launch QEMU~

ARM 64-bit:
Code:
qemu-aarch64 -g 1234 ./hello_world


ARM 32-bit:
Code:
qemu-arm -g 1234 ./hello_world


PPC 64-bit:
Code:
qemu-ppc64 -g 1234 ./hello_world


PPC 32-bit:
Code:
qemu-ppc -g 1234 ./hello_world


Launch GNU Debugger in a second terminal~

ARM64:

Code:
gdb-multiarch -q --nh \
  -ex 'set architecture aarch64' \
  -ex 'file hello_world' \
  -ex 'target remote localhost:1234' \
  -ex 'layout split' \
  -ex 'layout regs'


ARM32:

Code:
gdb-multiarch -q --nh \
  -ex 'set architecture arm' \
  -ex 'file hello_world' \
  -ex 'target remote localhost:1234' \
  -ex 'layout split' \
  -ex 'layout regs'


PPC64:

Code:
gdb-multiarch -q --nh \
  -ex 'set architecture ppc64' \
  -ex 'file hello_world' \
  -ex 'target remote localhost:1234' \
  -ex 'layout split' \
  -ex 'layout regs'


PPC32:

Code:
gdb-multiarch -q --nh \
  -ex 'set architecture ppc' \
  -ex 'file hello_world' \
  -ex 'target remote localhost:1234' \
  -ex 'layout split' \
  -ex 'layout regs'

 
Registers may or not be available at this moment.
 
At this point you can start instruction stepping (via the stepi command). Let's step just one instruction...

[Image: gdb07.png]

Fyi, when you step, any register(s) that are changed by the stepped instruction will become highlighted (except in this case of initially making the Registers available). Similar concept to how registers will change to a red font in the Dolphin Emulator.

Stepping is cool enough, but lets view some memory. Let's take a look at the first 4 word values (as hexadecimal) of the Stack. Use the following command...

Code:
x/4xw $sp


[Image: gdb08.png]

Sweet! Feel free to play around with this to get a better feel.

Please NOTE the GDB will not allow you to step thru the actual system calls themselves. When you type stepi on sc/svc you will be navigated to the two instructions ahead of said sc/svc. System calls are emulated and cannot be customized. More on syscalls in Chapter 9.



Chapter 8: Alternative Method for Assembly Files

Alternatively, you can do Assembly Files with just one terminal command. You will have to change the lowercase "s" in hello_world.s to be Capitalized (hello_world.S). However with this method, you are more limited on cpu and architecture specification.

ARM 64-bit:
Code:
aarch64-linux-gnu-gcc -ggdb3 -nostdlib -o hello_world -static hello_world.S


ARM 32-bit:
Code:
arm-linux-gnueabihf-gcc -ggdb3 -nostdlib -o hello_world -static hello_world.S


PPC 64-bit:
Code:
powerpc64-linux-gnu-gcc -ggdb3 -nostdlib -o hello_world -static hello_world.S


PPC 32-bit:
Code:
powerpc-linux-gnu-gcc -ggdb3 -nostdlib -o hello_world -static hello_world.S


-nostdlib = Do not include any libraries that are not entirely present in the source file(s).



Chapter 9: Syscall Tables

This chapter is present to address the use of syscalls in the barebones Assembly Source examples. Syscalls allow the user to handle tasks such as console input/output, memory allocation, and file management via bare bones assembly. QEMU cannot emulate custom syscalls. Every CPU Architecture has built-in syscalls with a unique syscall table. QEMU will emulate these. 

If you are familiar with tinkering with ISFS/IOS for Wii Files, then adapting to syscalls is very easy. They essentially operate the same (you use a file-open syscall to get an fd, and use the fd for future file based syscalls).

You can search around on Google and easily find the syscall table for your desired architecture.

I may expand this chapter/section in this future for a quick tut on using these syscalls with various examples.



Chapter 10: Final Note

You may get unknown errors when quitting GDB. This will usually occur if you quit after you have called the sc/svc for the exit status when stepping. Simply press Y to quit the session and press N to deny core file creation on GDB.
Reply


Messages In This Thread
QEMU + GNU Debugger Basic Tutorial - by Vega - 11-14-2022, 01:24 AM

Forum Jump:


Users browsing this thread: 2 Guest(s)