Lets take this small example code:
#include <stdio.h>
#include <stdlib.h>
int foo (int a)
{
return a ? a << 2: 1000;
}
int main()
{
printf("The result of foo(10) is %d\n", foo(10));
return EXIT_SUCCESS;
}
Once in assembly we get:
0000000000400506 <foo>:
400506: 55 push %rbp
400507: 48 89 e5 mov %rsp,%rbp
40050a: 89 7d fc mov %edi,-0x4(%rbp)
40050d: 83 7d fc 00 cmpl $0x0,-0x4(%rbp)
400511: 74 08 je 40051b <foo+0x15>
400513: 8b 45 fc mov -0x4(%rbp),%eax
400516: c1 e0 02 shl $0x2,%eax
400519: eb 05 jmp 400520 <foo+0x1a>
40051b: b8 e8 03 00 00 mov $0x3e8,%eax
400520: 5d pop %rbp
400521: c3 retq
0000000000400522 <main>:
400522: 55 push %rbp
400523: 48 89 e5 mov %rsp,%rbp
400526: bf 0a 00 00 00 mov $0xa,%edi
40052b: e8 d6 ff ff ff callq 400506 <foo>
400530: 89 c6 mov %eax,%esi
400532: bf d4 05 40 00 mov $0x4005d4,%edi
400537: b8 00 00 00 00 mov $0x0,%eax
40053c: e8 9f fe ff ff callq 4003e0 <printf@plt>
400541: b8 00 00 00 00 mov $0x0,%eax
400546: 5d pop %rbp
400547: c3 retq
400548: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
40054f: 00
Lets take a look at the following gdb
session:
(gdb) break *0x40052b
Breakpoint 1 at 0x40052b: file frame.c, line 13.
(gdb) break *0x400530
Breakpoint 2 at 0x400530: file frame.c, line 13.
We just set a breakpoint before and after the foo
procedure call.
(gdb) break foo
Breakpoint 3 at 0x40050d: file frame.c, line 7.
We set a breakpoint in the foo
procedure.
(gdb) run
Starting program: /home/fleury/tmp/tests/frame
Breakpoint 1, 0x000000000040052b in main () at frame.c:13
13 printf("The result of foo(10) is %d\n", foo(10));
We start the program and we hit the first breakpoint before the call to foo
.
(gdb) info frame
Stack level 0, frame at 0x7fffffffe0c0:
rip = 0x40052b in main (frame.c:13); saved rip = 0x7ffff7a54b45
source language c.
Arglist at 0x7fffffffe0b0, args:
Locals at 0x7fffffffe0b0, Previous frame's sp is 0x7fffffffe0c0
Saved registers:
rbp at 0x7fffffffe0b0, rip at 0x7fffffffe0b8
We asked information about the stack frame environment. We can notice that save rip = 0x7ffff7a54b45
(which is the return address of the main
procedure).
(gdb) continue
Continuing.
Breakpoint 3, foo (a=10) at frame.c:7
7 return a ? a << 2: 1000;
We continue the execution of the program and got stopped inside the foo
procedure (third breakpoint). Lets ask about the stack frame:
(gdb) info frame
Stack level 0, frame at 0x7fffffffe0b0:
rip = 0x40050d in foo (frame.c:7); saved rip = 0x400530
called by frame at 0x7fffffffe0c0
source language c.
Arglist at 0x7fffffffe0a0, args: a=10
Locals at 0x7fffffffe0a0, Previous frame's sp is 0x7fffffffe0b0
Saved registers:
rbp at 0x7fffffffe0a0, rip at 0x7fffffffe0a8
Note that saved rip = 0x400530
which is exactly the position of the next assembly instruction after the call foo
.
(gdb) continue
Continuing.
Breakpoint 2, 0x0000000000400530 in main () at frame.c:13
13 printf("The result of foo(10) is %d\n", foo(10));
We keep going in the execution and we reach the second breakpoint at the exit of the foo
procedure. Again, lets ask for the return address:
(gdb) info frame
Stack level 0, frame at 0x7fffffffe0c0:
rip = 0x400530 in main (frame.c:13); saved rip = 0x7ffff7a54b45
source language c.
Arglist at 0x7fffffffe0b0, args:
Locals at 0x7fffffffe0b0, Previous frame's sp is 0x7fffffffe0c0
Saved registers:
rbp at 0x7fffffffe0b0, rip at 0x7fffffffe0b8
It has been restored to the original value when we popped out of foo
.
In fact, the info frame
(shortened into i f
) also tell where to find the stored return address on the stack:
Saved registers:
rbp at 0x7fffffffe0b0, rip at 0x7fffffffe0b8
And, if you ask gdb
to display the content of 0x7fffffffe0b8
you should see 0x7ffff7a54b45
:
(gdb) print /x *0x7fffffffe0b8
$1 = 0x7ffff7a54b45
disassemble test_function
.call
instruction does two things: it jumps to the target address and it pushes the return address on the stack. After reaching your breakpoint, check the stack like you did in your example, then executestepi
in gdb, and check the stack again;esp
will have decreased by 4, and0x08048499
will be at the top of the stack.