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
-
How to check what debugging tools are available on your system
-
How to examine a compiled program
-
Basic debugging commands to understand what’s happening inside your program
-
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):
-
break main- Set a breakpoint at the main function -
run- Start the program -
next- Execute the next line of code -
step- Step into a function -
print variable- Show the value of a variable -
backtrace- Show the function call stack -
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
-
Start simple: Add print statements to your code to track execution flow
-
Use a debugger: When available, it’s the most powerful tool
-
Understand compilation: Make sure you compile with debugging symbols (
-gflag) -
Read error messages: They often tell you exactly what’s wrong
-
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:
-
How to check what debugging tools are available on your RISC-V system
-
How to examine the structure of an executable file
-
Basic commands to understand what’s happening in your program
-
Alternative approaches when full debuggers aren’t available
To continue learning:
-
Recompile your program with
gcc -g hello_riscv.c -o hello_riscvto include debugging information -
Practice with GDB (if available) using the commands we discussed
-
Add simple print statements to your code to trace execution
-
Learn to read assembly code to understand what happens at the lowest level
Happy debugging!