Moving esp
into ebp
is done as a debugging aid and in some cases for exception handling. ebp
is often called the frame pointer. With this in mind, think of what happens if you call several functions. ebp
points to a block of memory where you pushed the old ebp
, which itself points to another saved ebp
, etc. Thus, you have a linked list of stack frames. From these, you can look at the return addresses (which are always 4 bytes above the frame pointer in the stack frame) to find out what line of code called a stack frame in question. The instruction pointer can tell you the location of current execution. This allows you to generate a stacktrace which is useful for debugging by showing the flow of execution throughout a program.
As a practical example consider the following code:
void foo();
void bar();
void baz();
void quux();
void foo() {
bar();
}
void bar() {
baz();
quux();
}
void baz() {
//do nothing
}
void quux() {
*(int*)(0) = 1; //SEGFAULT!
}
int main() {
foo();
return 0;
}
This generates the following assembly (with Debian gcc 4.7.2-4 gcc -m32 -g test.c
, snipped):
080483dc <foo>:
80483dc: 55 push %ebp
80483dd: 89 e5 mov %esp,%ebp
80483df: 83 ec 08 sub $0x8,%esp
80483e2: e8 02 00 00 00 call 80483e9 <bar>
80483e7: c9 leave
80483e8: c3 ret
080483e9 <bar>:
80483e9: 55 push %ebp
80483ea: 89 e5 mov %esp,%ebp
80483ec: 83 ec 08 sub $0x8,%esp
80483ef: e8 07 00 00 00 call 80483fb <baz>
80483f4: e8 07 00 00 00 call 8048400 <quux>
80483f9: c9 leave
80483fa: c3 ret
080483fb <baz>:
80483fb: 55 push %ebp
80483fc: 89 e5 mov %esp,%ebp
80483fe: 5d pop %ebp
80483ff: c3 ret
08048400 <quux>:
8048400: 55 push %ebp
8048401: 89 e5 mov %esp,%ebp
8048403: b8 00 00 00 00 mov $0x0,%eax
8048408: c7 00 01 00 00 00 movl $0x1,(%eax)
804840e: 5d pop %ebp
804840f: c3 ret
08048410 <main>:
8048410: 55 push %ebp
8048411: 89 e5 mov %esp,%ebp
8048413: 83 e4 f0 and $0xfffffff0,%esp
8048416: e8 c1 ff ff ff call 80483dc <foo>
804841b: b8 00 00 00 00 mov $0x0,%eax
8048420: c9 leave
8048421: c3 ret
Note that leave
is the same as:
mov %ebp, %esp
pop %ebp
With this in mind, and the standard-ish C calling convention on x86, we know that the stack at the segfault is going to look like:
- top of main's stack frame
- stack space for main - in this case, enough to align on 16 bytes
0x0804841b
return address for call foo
- pointer to
1.
- stack space for foo
0x080483e7
return address for call bar
- pointer to
4.
- stack space for bar
0x080483f9
return address for call quux
- pointer to
7.
- stack space for quux
The instruction pointer will be 0x08048408
. ebp
will point to 10.
.
At this point, the processor generates an exception, which the operating system processes. It then sends SIGSEGV
to the process, which obligingly terminates and dumps core. You then bring up the core dump in gdb with gdb -c core
, and you type in file a.out
and bt
, and it gives you in response:
#0 0x08048408 in quux () at test.c:20
#1 0x080483f9 in bar () at test.c:12
#2 0x080483e7 in foo () at test.c:7
#3 0x0804841b in main () at test.c:24
#0
is generated from the instruction pointer. Then, it goes to ebp
(10), looks at the previous item on the stack (9), and generates #1
. It follows ebp
(i.e. mov %ebp, (%ebp)
) to (7), and looks 4 bytes above that (6) to generate #2
. It finally follows (7) to (4) and looks at (3) to generate #3
.
Note: This is but one way of doing such stack tracing. GDB is very, very smart, and can perform the stack trace even when you use -fomit-frame-pointer
. However, in a very basic implementation this is probably the simplest way to generate a stack trace.