What a return address is and why you might need to find one
A return address is a memory location that tells a program where to go next after a function finishes running. When your code calls a function, the processor saves the return address — the spot right after the function call — so it knows where to resume once the function ends. In most cases, this happens invisibly and correctly. But if you're debugging a crash, a hang, or unexpected behavior, you may need to see what return address is actually stored on the stack.
GDB (the GNU Debugger) is a tool that lets you pause a running program, inspect its memory, and trace where it's going. Finding the return address with GDB means looking at the stack — the part of memory where return addresses and local variables live — and reading the raw value that tells the processor where to jump back to.
Key Takeaways
- The return address sits on the stack, and you can see it by running info frame or x/a $sp in GDB to inspect the memory at the stack pointer.
- The backtrace command shows you the call stack in human-readable form, which is usually faster than hunting for raw addresses.
- On x86-64 systems, the return address is typically stored at the top of the stack (the address pointed to by the stack pointer register).
- If you need the raw hex address rather than the function name, use x/a $sp to print what's stored at the current stack pointer.
Using backtrace to see the call stack
The simplest way to find where your program will return to is the backtrace command (or bt for short). When you pause the debugger at any point — by hitting a breakpoint or pressing Ctrl+C — type backtrace and GDB will print the entire chain of function calls that led to where you are now.
Each line shows a frame number, the function name, the file and line number, and the arguments passed to that function. The frame at the top (frame 0) is where you are right now. The frame above it is the function that called the current function — and that's where the return address points. This is almost always what you actually want to know.
For example, if you see:
#0 crash_here () at main.c:15 #1 0x00005555555546b2 in main () at main.c:20
The return address from crash_here points to line 20 in main, which is where main called crash_here. GDB has already translated the raw address into a human-readable location.
Reading the return address directly from the stack
If you need the actual hex address rather than the function name, you can inspect the stack pointer directly. In GDB, type x/a $sp (examine as address at stack pointer). On most systems, this will print the return address in hex.
The x command means "examine memory." The /a flag tells GDB to interpret the bytes as an address and try to match it to a symbol (function name) if possible. The $sp register holds the address of the top of the stack, where the return address usually sits on x86-64 and ARM systems.
You can also use info frame to see detailed information about the current stack frame, including the return address. Type info frame and look for the line that says "saved pc" — that's the return address.
Understanding stack layout on different architectures
Where the return address lives depends on your processor. On x86-64 (Intel and AMD 64-bit), the return address is pushed onto the stack by the call instruction, so it sits at the address pointed to by $sp. On ARM and ARM64, the return address is stored in the link register ($lr) instead, so you would inspect that register directly with print $lr.
If you're not sure which architecture you're debugging, type info registers in GDB and look at what registers are available. If you see $rsp and $rax, you're on x86-64. If you see $sp and $r0 through $r15, you're likely on ARM.
Stepping through code to watch return addresses change
Another way to understand return addresses is to step through your code one instruction at a time. Use stepi (step instruction) to execute one machine instruction, then check the stack pointer and return address after each step. When you hit a call instruction, the return address will be pushed onto the stack. When you hit a ret instruction, the return address will be popped off and the processor will jump to it.
This is slower than using backtrace, but it shows you exactly what's happening at the hardware level. It's useful if you're trying to understand why a return address is wrong or corrupted.
Finding corrupted return addresses
If a program crashes with a segmentation fault or jumps to a nonsensical address, the return address may be corrupted. In GDB, run backtrace and look for frames that show an address instead of a function name, or frames that jump to an obviously wrong location. You can also compare the return address shown in backtrace with the raw hex value from x/a $sp — if they don't match, something has overwritten the stack.
Buffer overflows, uninitialized pointers, and stack corruption are common causes. Once you've found the corrupted return address, work backward through your code to find where the stack was written to incorrectly.
Frequently Asked Questions
How do I set a breakpoint and then check the return address?
Type break function_name to set a breakpoint, then run your program with run. When the breakpoint hits, type backtrace or info frame to see the return address. You can also use x/a $sp to see the raw hex value.
What does "saved pc" mean in the info frame output?
The "saved pc" (program counter) is the return address — the memory location the processor will jump to when the current function returns. It's the same value you see in the backtrace output, just displayed in a different format.
Can I modify the return address while debugging?
Yes. You can use set $sp = value or set $lr = value to change the return address, but this is dangerous and rarely needed. Changing the return address will cause the program to jump to a different location when the function returns, which usually causes a crash unless you know exactly what you're doing.
Why does backtrace show question marks or ?? instead of function names?
This usually means the return address points to memory that doesn't have debugging symbols. The address is valid, but GDB can't match it to a function name. Recompile your program with the -g flag to include debugging symbols, or check whether you're debugging a stripped binary.