Introduction to Debugging on RISC-V (using gdb)

Introduction to Debugging on RISC-V

Welcome to a debugging tutorial! This notebook will help you understand how to examine and debug programs running on your RISC-V system.

What is Debugging?

Debugging is the process of finding and fixing errors in computer programs. Think of it like being a detective - you’re looking for clues about why your program isn’t working as expected.

What You’ll Learn

  1. How to check what debugging tools are available on your system

  2. How to examine a compiled program

  3. Basic debugging commands to understand what’s happening inside your program

  4. How to step through code and inspect variables

Step 1: Check What Debugging Tools Are Available

Let’s first see what debugging tools you have installed on your RISC-V system. if you want to use gdb sudo apt-get install gdb

!which gdb || echo "GDB not found"
!which objdump || echo "objdump not found"
!which readelf || echo "readelf not found"
!which llvm-dwarfdump || echo "llvm-dwarfdump not found"

/usr/bin/gdb
/usr/bin/objdump
/usr/bin/readelf
/usr/local/bin/llvm-dwarfdump

Step 2: Examine Your Compiled Program

Before we start debugging, let’s look at the structure of your compiled program. This helps us understand what’s inside the executable file.

# Let's see what type of file we're working with
!file hello_riscv

hello_riscv: ELF 64-bit LSB pie executable, UCB RISC-V, RVC, double-float ABI, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-riscv64-lp64d.so.1, for GNU/Linux 4.15.0, not stripped

# View the sections of the executable
!readelf -S hello_riscv || !objdump -h hello_riscv || echo "Neither readelf nor objdump available"

There are 28 section headers, starting at offset 0x1aa8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000000270  00000270
       0000000000000021  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000000294  00000294
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .gnu.hash         GNU_HASH         00000000000002b8  000002b8
       0000000000000024  0000000000000000   A       4     0     8
  [ 4] .dynsym           DYNSYM           00000000000002e0  000002e0
       00000000000000c0  0000000000000018   A       5     2     8
  [ 5] .dynstr           STRTAB           00000000000003a0  000003a0
       000000000000007d  0000000000000000   A       0     0     1
  [ 6] .gnu.version      VERSYM           000000000000041e  0000041e
       0000000000000010  0000000000000002   A       4     0     2
  [ 7] .gnu.version_r    VERNEED          0000000000000430  00000430
       0000000000000030  0000000000000000   A       5     1     8
  [ 8] .rela.dyn         RELA             0000000000000460  00000460
       00000000000000c0  0000000000000018   A       4     0     8
  [ 9] .rela.plt         RELA             0000000000000520  00000520
       0000000000000030  0000000000000018  AI       4    20     8
  [10] .plt              PROGBITS         0000000000000550  00000550
       0000000000000040  0000000000000010  AX       0     0     16
  [11] .text             PROGBITS         0000000000000590  00000590
       00000000000000d0  0000000000000000  AX       0     0     4
  [12] .rodata           PROGBITS         0000000000000660  00000660
       000000000000002a  0000000000000000   A       0     0     4
  [13] .eh_frame_hdr     PROGBITS         000000000000068c  0000068c
       0000000000000014  0000000000000000   A       0     0     4
  [14] .eh_frame         PROGBITS         00000000000006a0  000006a0
       000000000000002c  0000000000000000   A       0     0     8
  [15] .preinit_array    PREINIT_ARRAY    0000000000001dd0  00000dd0
       0000000000000008  0000000000000008  WA       0     0     1
  [16] .init_array       INIT_ARRAY       0000000000001dd8  00000dd8
       0000000000000008  0000000000000008  WA       0     0     8
  [17] .fini_array       FINI_ARRAY       0000000000001de0  00000de0
       0000000000000008  0000000000000008  WA       0     0     8
  [18] .dynamic          DYNAMIC          0000000000001de8  00000de8
       00000000000001e0  0000000000000010  WA       5     0     8
  [19] .got              PROGBITS         0000000000001fc8  00000fc8
       0000000000000028  0000000000000008  WA       0     0     8
  [20] .got.plt          PROGBITS         0000000000001ff0  00000ff0
       0000000000000020  0000000000000008  WA       0     0     8
  [21] .data             PROGBITS         0000000000002010  00001010
       0000000000000008  0000000000000000  WA       0     0     8
  [22] .bss              NOBITS           0000000000002018  00001018
       0000000000000008  0000000000000000  WA       0     0     1
  [23] .comment          PROGBITS         0000000000000000  00001018
       000000000000008f  0000000000000001  MS       0     0     1
  [24] .riscv.attributes RISCV_ATTRIBUTE  0000000000000000  000010a7
       0000000000000066  0000000000000000           0     0     1
  [25] .symtab           SYMTAB           0000000000000000  00001110
       0000000000000618  0000000000000018          26    46     8
  [26] .strtab           STRTAB           0000000000000000  00001728
       0000000000000278  0000000000000000           0     0     1
  [27] .shstrtab         STRTAB           0000000000000000  000019a0
       0000000000000105  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), p (processor specific)

Step 3: Starting a Debugging Session

Now let’s start a debugging session with GDB (if available).

# open program with gdb
!gdb -batch -ex "info functions" hello_riscv 2>/dev/null || echo "GDB not available or failed to run"

All defined functions:

Non-debugging symbols:
0x0000000000000570  __libc_start_main@plt
0x0000000000000580  puts@plt
0x0000000000000590  _start
0x00000000000005b2  load_gp
0x00000000000005be  deregister_tm_clones
0x00000000000005e0  register_tm_clones
0x000000000000060c  __do_global_dtors_aux
0x0000000000000644  frame_dummy
0x0000000000000648  main

Step 4: Basic Debugging Commands

Let’s explore some useful debugging commands. If you don’t have GDB, we’ll use other tools to examine your program.

# List the functions in your program
!nm hello_riscv 2>/dev/null || !objdump -t hello_riscv 2>/dev/null || echo "Could not list functions"

0000000000000294 r __abi_tag
0000000000002020 B __BSS_END__
0000000000002018 B __bss_start
0000000000002018 b completed.0
                 w __cxa_finalize@GLIBC_2.27
0000000000002010 D __DATA_BEGIN__
0000000000002010 D __data_start
0000000000002010 W data_start
00000000000005be t deregister_tm_clones
000000000000060c t __do_global_dtors_aux
0000000000001de0 d __do_global_dtors_aux_fini_array_entry
0000000000002010 D __dso_handle
0000000000001de8 a _DYNAMIC
0000000000002018 D _edata
0000000000002020 B _end
0000000000000644 t frame_dummy
0000000000001dd8 d __frame_dummy_init_array_entry
00000000000006c8 r __FRAME_END__
0000000000001fc8 a _GLOBAL_OFFSET_TABLE_
0000000000002810 A __global_pointer$
000000000000068c r __GNU_EH_FRAME_HDR
0000000000000660 R _IO_stdin_used
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 U __libc_start_main@GLIBC_2.34
00000000000005b2 t load_gp
0000000000000648 T main
0000000000000550 a _PROCEDURE_LINKAGE_TABLE_
                 U puts@GLIBC_2.27
00000000000005e0 t register_tm_clones
0000000000002018 D __SDATA_BEGIN__
0000000000000590 T _start
0000000000002018 D __TMC_END__

# Disassemble the main function to see the assembly code
!objdump -d hello_riscv | grep -A 20 "<main>" 2>/dev/null || echo "Could not disassemble main function"

0000000000000648 <main>:
 648:	1141                	addi	sp,sp,-16
 64a:	e406                	sd	ra,8(sp)
 64c:	00000517          	auipc	a0,0x0
 650:	01850513          	addi	a0,a0,24 # 664 <_IO_stdin_used+0x4>
 654:	f2dff0ef          	jal	580 <puts@plt>
 658:	4501                	li	a0,0
 65a:	60a2                	ld	ra,8(sp)
 65c:	0141                	addi	sp,sp,16
 65e:	8082                	ret

Step 5: Debugging Session Examples

Let’s simulate a debugging session. We’ll show you what commands you would use in a real debugger.

Common GDB Commands (if available):

  1. break main - Set a breakpoint at the main function

  2. run - Start the program

  3. next - Execute the next line of code

  4. step - Step into a function

  5. print variable - Show the value of a variable

  6. backtrace - Show the function call stack

  7. quit - Exit GDB

Let’s try some of these:

# Try to run a simple debugging session
!echo -e "break main\nrun\nbacktrace\nquit" | gdb hello_riscv 2>/dev/null || echo "GDB not available for interactive debugging"

GNU gdb (Debian 15.1-1) 15.1
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "riscv64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from hello_riscv...
(No debugging symbols found in hello_riscv)
(gdb) Breakpoint 1 at 0x654
(gdb) Starting program: /home/david/develop/hello_riscv 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/riscv64-linux-gnu/libthread_db.so.1".

Breakpoint 1, 0x0000555555555654 in main ()
(gdb) #0  0x0000555555555654 in main ()
(gdb) A debugging session is active.

	Inferior 1 [process 571701] will be killed.

Quit anyway? (y or n) [answered Y; input not from terminal]

Step 6: Alternative Debugging Approaches

If you don’t have a full debugger, there are other ways to understand your program:

# Use strings command to see printable text in the executable
!strings hello_riscv | head -20

/lib/ld-linux-riscv64-lp64d.so.1
puts
__libc_start_main
__cxa_finalize
libc.so.6
GLIBC_2.27
GLIBC_2.34
_ITM_deregisterTMCloneTable
_ITM_registerTMCloneTable
Hello risc-v world, courtesy of clang
GCC: (Debian 14.2.0-19rockos1) 14.2.0
clang version 15.0.7 (https://github.com/llvm/llvm-project.git 8dfdcc7b7bf66834a761bd8de445840ef68e4d1a)
riscv
rv64i2p1_m2p0_a2p1_f2p2_d2p2_c2p0_zicsr2p0_zifencei2p0_zmmul1p0_zaamo1p0_zalrsc1p0
Scrt1.o
__abi_tag
$xrv64i2p1_m2p0_a2p1_f2p2_d2p2_c2p0_zicsr2p0_zifencei2p0_zmmul1p0_zaamo1p0_zalrsc1p0
load_gp
crtstuff.c
deregister_tm_clones

# Check what libraries the program depends on
!ldd hello_riscv 2>/dev/null || !objdump -p hello_riscv | grep NEEDED 2>/dev/null || echo "Could not list dependencies"

	linux-vdso.so.1 (0x00007fffbd18d000)
	libc.so.6 => /lib/riscv64-linux-gnu/libc.so.6 (0x00007fffbd006000)
	/lib/ld-linux-riscv64-lp64d.so.1 (0x00007fffbd18f000)

Step 7: Debugging Tips for Beginners

  1. Start simple: Add print statements to your code to track execution flow

  2. Use a debugger: When available, it’s the most powerful tool

  3. Understand compilation: Make sure you compile with debugging symbols (-g flag)

  4. Read error messages: They often tell you exactly what’s wrong

  5. Take small steps: Test frequently as you write code

Let’s check if your program was compiled with debugging symbols:

Conclusion

Debugging is an essential skill for all programmers. You’ve learned:

  1. How to check what debugging tools are available on your RISC-V system

  2. How to examine the structure of an executable file

  3. Basic commands to understand what’s happening in your program

  4. Alternative approaches when full debuggers aren’t available

To continue learning:

  1. Recompile your program with gcc -g hello_riscv.c -o hello_riscv to include debugging information

  2. Practice with GDB (if available) using the commands we discussed

  3. Add simple print statements to your code to trace execution

  4. Learn to read assembly code to understand what happens at the lowest level

Happy debugging!

1 Like