EIP 指令寄存器,其指向处理器下一条要执行的指令在内存的位置
ESP 栈指针寄存器
EBP 栈帧基地址寄存器
也就是说每一个函数所需要的栈帧,即每一个函数所对应活动记录的大小是固定的,由编译器在编译的时候指定。编译器在编译的时候就确定了当前函数所需要的栈空间的大小了。 最终表现为汇编代码中第一个字面常量。
函数结束的时候:
move ebp, esp
1 先将 ebp寄存器的值 赋值给 esp寄存器
pop ebp
2 此时pop 弹出来的值是 上一个栈帧的基准地址,pop到EBP寄存器中,此时 EBP寄存器保存了上一个栈帧的基准地址,即调用者栈帧的基准地址。即此时已经恢复了上一个栈帧的基准地址
函数返回的时候
pop eip
前面pop ebp 将 当时ESP栈指针寄存器指向的 内容(上一个栈帧基准地址) pop 到 EBP栈帧寄存器中。
此时 pop eip 将返回地址 弹出来到 EIP 指令寄存器,即函数返回,那么函数返回后,此时ESP 栈指针指向了 参数1的位置,即上一个栈帧的栈顶。被调函数的栈帧也被摧毁了。
pop 弹栈 弹的是ESP 栈顶指针指向的内存
当前栈帧起始地址:当前栈帧栈底
实验1 查看栈帧结构
#include <stdio.h>
/*
// EBP指针指向的内存空间 地址,当前栈帧基准地址
printf("ebp = %p\n", ebp);
// EBP指针指向的内存空间中 存储上一个栈帧的基准地址
printf("previous ebp = 0x%x\n", *((int*)ebp));
// EBP指针移位 +4 ---> 当前栈帧的返回地址
printf("return address = 0x%x\n", *((int*)(ebp + 4)));
// EBP指针移位 +8 ---> 上一个栈帧的 esp指针指向的内存地址
printf("previous esp = %p\n", ebp + 8);
*/
#define PRINT_STACK_FRAME_INFO() do \
{ \
char* ebp = NULL; \
char* esp = NULL; \
\
\
asm volatile ( \
"movl %%ebp, %0\n" \
"movl %%esp, %1\n" \
: "=r"(ebp), "=r"(esp) \
); \
\
printf("ebp = %p\n", ebp); \
printf("previous ebp = 0x%x\n", *((int*)ebp)); \
printf("return address = 0x%x\n", *((int*)(ebp + 4))); \
printf("previous esp = %p\n", ebp + 8); \
printf("esp = %p\n", esp); \
printf("&ebp = %p\n", &ebp); \
printf("&esp = %p\n", &esp); \
} while(0)
void test(int a, int b)
{
int c = 3;
printf("test() : \n");
PRINT_STACK_FRAME_INFO();
printf("&a = %p\n", &a);
printf("&b = %p\n", &b);
printf("&c = %p\n", &c);
}
void func()
{
int a = 1;
int b = 2;
printf("func() : \n");
PRINT_STACK_FRAME_INFO();
printf("&a = %p\n", &a);
printf("&b = %p\n", &b);
test(a, b);
}
int main()
{
printf("main() : \n");
PRINT_STACK_FRAME_INFO();
func();
return 0;
}
gdb 调试
栈帧结构 分析
mhr@ubuntu:~/Desktop/system/qianzhuan/15$ gdb
GNU gdb (Ubuntu 7.11-0ubuntu1) 7.11
Copyright (C) 2016 Free Software Foundation, Inc.
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 "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
Type "apropos word" to search for commands related to "word".
(gdb)
(gdb) shell gcc -g frame.c -o frame.out
frame.c: Assembler messages:
frame.c:30: Error: unsupported instruction `mov'
frame.c:31: Error: unsupported instruction `mov'
frame.c:44: Error: unsupported instruction `mov'
frame.c:45: Error: unsupported instruction `mov'
frame.c:56: Error: unsupported instruction `mov'
frame.c:57: Error: unsupported instruction `mov'
/*
在64位系统下去编译32位的目标文件,这样是非法的。
解决方案:
用”-m32”强制用32位ABI去编译,即可编译通过。
64位的Ubuntu如果执行X86平台32位编译,gcc -m32 -o x x.c会报错:fatal error: sys/cdefs.h: No such file or directory
sudo apt-get install libc6-dev-i386 安装32位库文件
*/
(gdb) shell gcc -g -m32 frame.c -o frame.out // 用”-m32”强制用32位ABI去编译
(gdb) file frame.out //载入可执行程序
Reading symbols from frame.out...done.
(gdb)
(gdb) start //执行可执行程序
Temporary breakpoint 1 at 0x80486fe: file frame.c, line 53.
Starting program: /home/mhr/Desktop/system/qianzhuan/15/frame.out
Temporary breakpoint 1, main () at frame.c:53
53 {
(gdb) break frame.c:35 //在 frame.c 第35行打上断点
Breakpoint 2 at 0x80485ad: file frame.c, line 35.
(gdb) (gdb) info breakpoints //查看断点信息 已经打上了
Num Type Disp Enb Address What
2 breakpoint keep y 0x080485ad in test at frame.c:35
(gdb) continue //执行到断点位置
Continuing.
main() :
ebp = 0xffffcf68
previous ebp = 0x0
return address = 0xf7e21647
previous esp = 0xffffcf70
esp = 0xffffcf50
&ebp = 0xffffcf54
&esp = 0xffffcf58
func() :
ebp = 0xffffcf48
previous ebp = 0xffffcf68
return address = 0x80487cc
previous esp = 0xffffcf50
esp = 0xffffcf20
&ebp = 0xffffcf34
&esp = 0xffffcf38
&a = 0xffffcf2c
&b = 0xffffcf30
test() :
//当前栈帧基准地址
ebp = 0xffffcf08
//上一个栈帧基准地址,可以看 func()函数的栈帧信息中的 栈帧基准地址,两者一致
previous ebp = 0xffffcf48
//当前栈帧的返回地址
return address = 0x80486d6
//上一个栈帧的esp指针指向的位置 即上一个栈帧的栈顶地址
previous esp = 0xffffcf10
esp = 0xffffcef0
&ebp = 0xffffcef4
&esp = 0xffffcef8
&a = 0xffffcf10 // test(a, b); 函数参数a地址
&b = 0xffffcf14// test(a, b); 函数参数b地址
&c = 0xffffcef0 //函数局部变量地址, 所以可以看出来 一个函数的栈帧空间中 参数数据 与 局部变量的存储 空间并不是连续的
Breakpoint 2, test (a=1, b=2) at frame.c:35
35 }
(gdb) info frame //查看当前栈帧信息
Stack level 0, frame at 0xffffcf10: //当前栈帧的起始地址(栈底地址) = 当前栈帧基准地址(0xffffcf08 ) + 8 = 上一个栈帧的栈顶位置
eip = 0x80485ad in test (frame.c:35); saved eip = 0x80486d6 //当前栈帧返回地址 所在空间的地址(0xffffcf0c ) = 当前栈帧基准地址(0xffffcf08 ) + 4
called by frame at 0xffffcf50
source language c.
Arglist at 0xffffcf08, args: a=1, b=2
Locals at 0xffffcf08, Previous frame's sp is 0xffffcf10 //函数调用前的栈顶指针指向的位置 ,即上一个栈帧(函数)的栈顶位置 = 当前栈帧基准地址(0xffffcf08 ) + 4
Saved registers:
ebp at 0xffffcf08, eip at 0xffffcf0c // eip 指针指向的空间 存储 当前函数(栈帧)的返回地址
(gdb) x /1wx 0xffffcf0c //查看 eip 指针指向的空间存储的数据 : 当前函数(栈帧)的返回地址
0xffffcf0c: 0x080486d6
(gdb)
(gdb) x /1wx 0xffffcf10 // 查看 上一个栈帧的栈顶位置(esp 指针指向的位置) 存储的数据
0xffffcf10: 0x00000001 // test(a, b); 函数调用的 最后一个参数b
(gdb) x /1wx 0xffffcf14
0xffffcf14: 0x00000002 // test(a, b); 函数调用的 倒数第二个参数a
(gdb)
以上的 栈帧结构分析 可以对应到这张结构图
参数入栈,前言,后续 分析
(gdb) shell objdump -S frame.out > frame.s //查看可执行程序的反汇编
...
void test(int a, int b)
{
804849b: 55 push %ebp
804849c: 89 e5 mov %esp,%ebp
804849e: 83 ec 18 sub $0x18,%esp
80484a1: 65 a1 14 00 00 00 mov %gs:0x14,%eax
80484a7: 89 45 f4 mov %eax,-0xc(%ebp)
80484aa: 31 c0 xor %eax,%eax
...
}
...
80485bf: c9 leave
80485c0: c3 ret
080485c1 <func>:
void func()
{
//前言
80485c1: 55 push %ebp // ebp 入栈
80485c2: 89 e5 mov %esp,%ebp // 移动 ebp 指针 指向 esp指向的地址处
80485c4: 83 ec 28 sub $0x28,%esp // $0x28 常量 即编译器设定的 栈帧空间大小。esp = esp - 0x28 。即移动esp指针到栈顶
80485c7: 65 a1 14 00 00 00 mov %gs:0x14,%eax
80485cd: 89 45 f4 mov %eax,-0xc(%ebp)
80485d0: 31 c0 xor %eax,%eax
int a = 1;
80485d2: c7 45 e4 01 00 00 00 movl $0x1,-0x1c(%ebp)
int b = 2;
80485d9: c7 45 e8 02 00 00 00 movl $0x2,-0x18(%ebp) //ebp 向低地址处 偏移24个字节
...
//函数调用 注意 call 指令会将返回地址 入栈
test(a, b);
80486c6: 8b 55 e8 mov -0x18(%ebp),%edx //将 -0x18(%ebp) 地址存储的数据 b 放到 edx寄存器
80486c9: 8b 45 e4 mov -0x1c(%ebp),%eax //将 -0x1c(%ebp) 地址存储的数据 a 放到 eax 寄存器
80486cc: 83 ec 08 sub $0x8,%esp //
80486cf: 52 push %edx // 参数 b 先入栈
80486d0: 50 push %eax //参数 a 后入栈
80486d1: e8 c5 fd ff ff call 804849b <test> // 跳转到 test()函数所在地 ,将返回地址入栈 继续执行
80486d6: 83 c4 10 add $0x10,%esp
}
...
//后续
80486eb: c9 leave
80486ec: c3 ret
可以由上面的调试过程 总结出 如下函数调用时,栈帧形成的过程:
80486c6: 8b 55 e8 mov -0x18(%ebp),%edx //将 -0x18(%ebp) 地址存储的数据 b 放到 edx寄存器
80486c9: 8b 45 e4 mov -0x1c(%ebp),%eax //将 -0x1c(%ebp) 地址存储的数据 a 放到 eax 寄存器
80486cf: 52 push %edx // 参数 b 先入栈
80486d0: 50 push %eax //参数 a 后入栈
//func() 调用 使用 call 指令调用 test(), 跳转到 test()函数所在地 ,将返回地址入栈
80486d1: e8 c5 fd ff ff call 804849b // 跳转到 test()函数所在地 ,将返回地址入栈 继续执行
时刻1 : func()调用 test():
时刻2 test() 函数开始 ,将 ebp 压入栈中
//将 ebp 压入栈中
804849b: 55 push %ebp
时刻3
80485c2: 89 e5 mov %esp,%ebp // 移动 ebp 指针 指向 esp指向的地址处
80485c4: 83 ec 28 sub $0x28,%esp // $0x28 常量 即编译器设定的 栈帧空间大小。esp = esp - 0x28 。即移动esp指针到栈顶
时刻4 test()帧栈形成