This is a mini-tutorial of sorts for getting started with gdb on the Switch, with the target audience being people who want to mod and/or reverse games, with not prerequisite knowledge of gdb. The goal will be to walk you through some of the basic workflows needed to use a debugger on the Switch, while being brief enough for skimming for when you forget things. If some part is unclear, your OS doesn't have install instructions, or you feel part of your workflow should be added here, feel free to comment any additions. (If you only need a quick reference [Jump to the Appendix](https://gist.github.com/jam1garner/c9ba6c0cff150f1a2480d0c18ff05e33#appendix)) ## Installing GDB First off you'll need a version of GDB compatible with aarch64. This can be obtained via either a distribution of `aarch64-none-elf-gdb` or `gdb-multiarch`. I personally use `gdb-multiarch`, however users of devkitpro or Windows users may find it easier to aquire an aarch64-specific GDB.
aarch64-none-else-gdb Install (Click to expand) ### `aarch64-none-else-gdb` Install Install [devkitpro](https://devkitpro.org/wiki/Getting_Started) if you haven't already. 1. Install aarch64 gdb: ``` dkp-pacman -Syu devkitA64-gdb ``` 2. Ensure devkitA64 is added to path. The path should look something like this: * Linux - `/opt/devkitpro/devkitA64/bin` * Windows - `C:/devkitpro/devkitA64/bin`
gdb-multiarch Install (Click to expand) ### `gdb-multiarch` Install #### Ubuntu ``` sudo apt-get install -y gdb-multiarch ``` #### Windows (Requires MSYS2 with msys2 pacman in PATH) ``` pacman -Syu mingw-w64-x86_64-gdb-multiarch ``` #### Arch Linux ``` pacman -Syu gdb-multiarch ```
## Setting Up The On-Console GDB Stub 1. Ensure your version of [Atmosphere](https://github.com/Atmosphere-NX/Atmosphere/releases) is at least version `1.2.4` (System Settings → System → "Current version: ... | AMS X.Y.Z | ..." under the 'System Update' button) 2. Modify `sd:/atmosphere/config/system_settings.ini` (or create it if it doesn't exist) and ensure the following values are set accordingly: ```ini [atmosphere] enable_htc = u8!0x0 enable_standalone_gdbstub = u8!0x1 ``` 3. Reboot your Switch **Note:** this portion is subject to change if Atmosphere ever removes the standalone GDB stub. If so this section will need to be updated for Atmosphere's Starlink once it is ready for use, which will also enable debugging the Switch over USB. ## Connecting GDB to a game First find your Switch IP (System Settings → Internet → "IP Address") and ensure the computer you're using to connect to. Anywhere you see `` should be replaced with your Switch's IP, for example `:22225` becomes `192.168.0.5:22225` if your Switch's IP is `192.168.0.5`. (Note: your Switch will need to be on the same network as your computer for this to work) 1. Open a terminal window and run GDB: ``` gdb-multiarch ``` or ``` aarch64-none-elf-gdb ``` 2. Run the following GDB command to connect to your Switch (note: `(gdb)` is the prompt, you should only type/copy the part after it) ``` (gdb) target extended-remote :22225 ``` 3. Tell the Switch GDB stub you're about to run an application ``` (gdb) monitor wait application ``` 4. Ignore any "packet error" messages and start up your game 5. Your game will freeze and gdb will output "`Attach to 0xNN`", where `0xNN` is the process ID of your game. 6. Finish attaching to the game (replace 0xNN with your process ID): ``` (gdb) attach 0xNN ``` 7. If everything worked you should now see something akin to the following: ``` Attaching to process 146 warning: Application: No such file or directory. 0x0000001040200000 in ?? () => 0x0000001040200000: 02 00 00 14 b 0x1040200008 ``` ## Actually Using GDB
Terms to Know (Click to expand) * Breakpoint - a location in the code to stop execution for debugging when the location's code is about to run * Register - a named temporary storage location provided by the CPU used for all calculations * Watchpoint - a location in memory to stop execution when it's accessed, basically a breakpoint but for data instead of code * Program Counter - a value representing the position in the code that is currently executing * Pointer - a memory address which "points" to some data of a given type * Endian - the byte order of a number stored in memory. The Switch is a little endian system, so numbers stored in memory have their bytes reversed. * Module - an executable loaded into memory. (`main`, nnsdk, NROs, `subsdk`s, etc) * Offset - a location relative to the start of something, in the case of debugging usually the start of the relevant module. * Section - a portion of a module with a given purpose. The most relevant sections are `.text` (the code), `.data` (global writable data), and `.rodata` (read-only global data). * Heap - miscellaneous memory from which the game can request a section of memory (allocation) and then later return it (deallocation/freeing memory) * Thread - multiple bits of code can run at the same time, each individual bit of code being its own "thread" of execution * Symbol - a label which points to a given function or value. For example `nn::os::StartThread` (an nnsdk function for starting a thread) or `nnMain` (where the `main` executable of the game starts execution) * Stack - the storage location for local variables in memory, each function reserves some memory on the top of the stack (by subtracting from the 'stack pointer') then at the end of the function returns the memory (by adding to the stack pointer) * Arguments - functions can take "arguments" or values which are passed to it. The passing of arguments uses a standard set of registers (calling convention) so we can know which registers map to which argument (for example `X0` will be the first integer/pointer argument)
The general process of debugging will be finding a point in the code in which we would like to stop execution and observe the values stored in either registers or in memory (usually in memory pointed to by the registers at the point in time). Typically this is done by locating potentially relevant code in GHIDRA/IDA and then setting a breakpoint at the offset in our module where we want to observe the system state. For example if we have the following function in GHIDRA: ``` nnMain XREF[1]: Entry Point(*) 71003071a0 ff c3 00 d1 sub sp,sp,#0x30 71003071a4 fe 0b 00 f9 str x30,[sp, #local_20] 71003071a8 f4 4f 02 a9 stp x20,x19,[sp, #local_10] 71003071ac e0 63 00 91 add x0,sp,#0x18 71003071b0 b0 7b 00 94 bl nn::fs::QueryMountRomCacheSize undefined QueryMountRomCacheSize ``` Let's say we want to see the value of the stack pointer (sp) at the start of the `nnMain` function. First off we need to get the offset of `nnMain`. For the [GHIDRA Switch Loader](https://github.com/Adubbz/Ghidra-Switch-Loader), our game will be loaded at `0x7100000000`, so to get the offset from our disassebly above we can just chop of the `71` part of the address. So our offset of nnMain (relative to the start of our binary) is `0x3071a0`. ### Setting A Breakpoint (The Basic Workflow) 1. First up we need to locate the start of our module: ``` (gdb) monitor get info ``` ``` Process: 0x92 (Application) Program Id: 0x01007ec013abc000 Application: 1 Hbl: 0 Layout: Alias: 0x2146400000 - 0x31463fffff Heap: 0x31cec00000 - 0x334ebfffff Aslr: 0x0008000000 - 0x7fffffffff Stack: 0x7c21400000 - 0x7ca13fffff Modules: 0x1040200000 - 0x1040201fff nnrtld.elf 0x1040204000 - 0x10558dbfff Spel2.nss 0x1056a48000 - 0x1056fd7fff nnSdk.elf ``` 2. The relevant module here is `Spel2.nss` (this will be named after your game) and so the load address of it is `0x1040204000`, so lets set a breakpoint relative to it: ``` b *
+ ``` filling in our module adddress/offset we get: ``` (gdb) b *0x1040204000+0x3071a0 ``` 3. Now we want to continue (allow the game to start running so it can reach the point where it hits our breakpoint) ``` (gdb) c ``` 4. Our breakpoint should get hit a few seconds after the above is run. Let's print out the next 5 instructions so we know we're at the right place: ``` (gdb) x/5i $pc => 0x104050b1a0: sub sp, sp, #0x30 0x104050b1a4: str x30, [sp, #16] 0x104050b1a8: stp x20, x19, [sp, #32] 0x104050b1ac: add x0, sp, #0x18 0x104050b1b0: bl 0x104052a070 ``` 5. Now lets print out our stack pointer: ``` (gdb) p/x $sp ``` 6. And since our current instruction modifies the stack pointer, lets see that in action. First up let's "step" to the next instruction: ``` (gdb) si ``` 7. And finally let's print the stack pointer again (same command as above), and it is `0x30` smaller than before (`sub sp,sp,#0x30`) ### More Useful Commands #### Printing a String Print a null-terminated string pointed to by the register $x0: ``` p (char*)$x0 ``` #### Set a Watchpoint Break if the process writes to the integer at offset `+0x158620de`: ``` watch *(int*)0x1040204000+0x158620de ``` Break if the process reads the integer from offset `+0x158620de`: ``` rwatch *(int*)0x1040204000+0x158620de ``` Break if the process accesses (reads from or writes to) the integer at offset `+0x158620de`: ``` awatch *(int*)0x1040204000+0x158620de ``` #### List the Mapped Memory Regions ``` monitor get mappings ``` #### Print a Backtrace Print the call stack of which functions called the current function: ``` bt ``` ## Appendix |Short|Long|Info|Example| |-|-|-|-| |`b`|`break`| Set a breakpoint at a given point, common form: `b *
`, where `
` can be an expression |`b *0x1234+0x5`| |`c`|`continue`| Resume (or start) execution | | |`p`|`print`| Print a value. | print sp as hex: `p/x $sp`, print x0 as string: `p (char*)$x0`| |`x`|| Print memory at a location ([More Info](https://visualgdb.com/gdbreference/commands/x)) | print 4 64-bit numbers at x0: `x/4g`, print the first 6 bytes on the stack: `x/6b $sp` | | `si` || Step a single instruction || | `bt` || Print a backtrace || || `watch` | Write-only watchpoint | `watch *(char*)$x5` | || `rwatch` | Read-only watchpoint | `rwatch *(void**)$x5` | || `awatch` | Watchpoint | `awatch *(int*)$sp` | | `mon` | `monitor`| Switch-specific gdb commands | `monitor help` | || `x/Ni ` | Disassemble `N` instructions at location `` | print the next 3 instructions: `x/3i $pc` | || `target extended-remote` | Attach to a remote target. Atmosphere uses port 22225. | `target extended-remote 192.168.1.31:22225` | || `attach` | Attach to a given process ID | `attach 0x33` | || `info break` | List breakpoints || || `info watch` | List watchpoints || || `info os processes` | List all running processes || ### Switch-Only Monitor Commands |Command|Info| |-|-| |`monitor get info`| Get process info, address space layout, and information on modules | |`monitor get mappings`| Get all memory mappings | |`monitor wait application`| Causes the stub to wait for an application to be launched. The next application will be started suspended until the user runs `attach [pid]` | |`monitor wait homebrew`| Causes the stub to wait for homebrew to be launched | |`monitor wait {program id}`| Causes the stub to wait for a given program id to be launched then haults it until its pid is attached to | ## Other Tips ### Print Disassembly Automatically Set the following in your GDB init file (`~/.gdbinit`) where `~` is your user's home directory: ``` set disassemble-next-line on show disassemble-next-line ``` this will have it print out the current instruction whenever you hit a breakpoint, making it easier to understand what's going on. ### GDB TUI (Terminal UI) GDB TUI isn't super amazing, but sometimes it's a nice choice and I'm sure some people swear by it. To active it use the command `layout asm` in a compatible version of gdb in ANSI compatible terminals: ![image](https://user-images.githubusercontent.com/8260240/141351730-c5d49502-b9cb-47a7-8b20-20528685d612.png) Or to display registers use `layout regs`: ![image](https://user-images.githubusercontent.com/8260240/141352076-ed1a8998-19a2-479c-98bc-dd89d915f9e1.png)