1. 最简单的代码:
//// test1.c
int main(){
return 1;
}
编译、反汇编:
gcc test1.c
gdb ./a.out
(gdb) disassemble main
0x08048344
0x08048348
0x0804834b
0x0804834e
0x0804834f
0x08048351
0x08048352
0x08048357
0x08048358
0x08048359
0x0804835c
常用指令解释 :
CALL 指令 :
用来调用一个函数或过程,此时,下一条指令地址会被压入堆栈,以备返回时能恢复执行下条指令。
RET 指令 :
用来从一个函数或过程返回,之前 CALL 保存的下条指令地址会从栈内弹出到 EIP 寄存器中,程序转到 CALL 之前下条指令处执行
ENTER 指令 :
建立当前函数的栈框架,即相当于以下两条指令:
pushl %ebp
movl %esp,%ebp
LEAVE 指令 :
释放当前函数或者过程的栈框架,即相当于以下两条指令:
movl ebp esp
popl ebp
2. 函数间的调用代码:
假如函数A 调用函数B ,函数B 调用函数C :
///// test2.c
void c(){}
void b(){c();}
void a(){b();}
int main(){
a();
return 1;
}
编译、反汇编:
gcc test1.c
gdb ./a.out
(gdb) disassemble main
Dump of assembler code for function main:
0x0804835d
0x08048361
0x08048364
0x08048367
0x08048368
0x0804836a
0x08048370
0x08048375
0x08048376
0x08048377
0x0804837a
End of assembler dump.
(gdb) disassemble a
Dump of assembler code for function a:
0x08048353 : push %ebp
0x08048354 : mov %esp,%ebp
0x08048356 : call 0x8048349
0x0804835b : pop %ebp
0x0804835c : ret
End of assembler dump.
(gdb) disassemble b
Dump of assembler code for function b:
0x08048349 : push %ebp
0x0804834a : mov %esp,%ebp
0x0804834c : call 0x8048344
0x08048351 : pop %ebp
0x08048352 : ret
End of assembler dump.
(gdb) disassemble c
Dump of assembler code for function c:
0x08048344
0x08048345
0x08048347
0x08048348
End of assembler dump.
函数调用栈的状态:
+-------------------------+----> 高地址
| EIP (Main 函数返回地址) |
+-------------------------+
| EBP (Main 函数的EBP) | --+ <------ 当前函数A 的EBPA ( 即SFP 框架指针)
+-------------------------+ +-->offsetA
| A 中的局部变量 | --+ <------ESP 指向函数A 新分配的局部变量, 局部变量可以通过EBPA-offsetA 访问
+-------------------------+
| Arg .( 函数B 的参数) | --+ <------ B 函数的参数可以由B 的EBPB+offsetB 访问
+-------------------------+ +--> offsetB
| EIP (A 函数的返回地址) | |
+-------------------------+ --+
| EBP (A 函数的EBP) |<--+ <------ 当前函数B 的EBPB ( 即SFP 框架指针)
+-------------------------+
| B 中的局部变量 |
+-------------------------+
| Arg .( 函数C 的参数) |
+-------------------------+
| EIP (B 函数的返回地址) |
+-------------------------+
| EBP (B 函数的EBP) | --+ <------ 当前函数C 的EBPC ( 即SFP 框架指针)
+-------------------------+
| C 中的局部变量 |
| .......... | <------ ESP 指向函数C 新分配的局部变量
+-------------------------+----> 低地址
函数被调用时 :
1) EIP/EBP 成为新函数栈的边界
函数被调用时,返回时的 EIP 首先被压入堆栈;创建栈框架时,上级函数栈的 EBP 被压入堆栈,与 EIP 一道行成新函数栈框架的边界
2) EBP 成为栈框架指针 SFP ,用来指示新函数栈的边界
栈框架建立后, EBP 指向的栈的内容就是上一级函数栈的 EBP ,可以想象,通过 EBP 就可以把层层调用函数的栈都回朔遍历一遍,调试器就是利用这个特性实现 backtrace 功能的
3) ESP 总是作为栈指针指向栈顶,用来分配栈空间
栈分配空间给函数局部变量时的语句通常就是给 ESP 减去一个常数值,例如,分配一个整型数据就是 ESP-4
4) 函数的参数传递和局部变量访问可以通过 SFP 即 EBP 来实现
由于栈框架指针永远指向当前函数的栈基地址,参数和局部变量访问通常为如下形式:
+8+xx(%ebp) ; 函数入口参数的的访问
-xx(%ebp) ; 函数局部变量访问
3 含局部变量时:
int main(){
int a = 3;
int b = 5;
return 1;
}
(gdb) disassemble main
Dump of assembler code for function main:
0x08048344
0x08048348
0x0804834b
0x0804834e
0x0804834f
0x08048351
0x08048352
0x08048355
0x0804835c
0x08048363
0x08048368
0x0804836b
0x0804836c
0x0804836d
0x08048370
End of assembler dump.
通过反汇编代码对程序运行时的寄存器和栈的观察和分析,可以得出局部变量在栈中的访问和分配及释放方式:
1. 局部变量的分配,可以通过 esp 减去所需字节数
sub $0x10,%esp
2. 局部变量的释放,可以通过 esp 加上已分配的字节
add $0x10,%esp
3. 局部变量的访问,可以通过 ebp 减去偏移量
movl $0x3,-0x8(%ebp)
4. 函数调用时有参数
int func(int m, int n)
{
return m+n;
}
int main(){
int a = 3;
int b = 5;
int c = 0;
c = func(a, b);
return c;
}
(gdb) disassemble main
Dump of assembler code for function main:
0x0804834f
0x08048353
0x08048356
0x08048359
0x0804835a
0x0804835c
0x0804835d
0x08048360
0x08048367
0x0804836e
0x08048375
0x08048378
0x0804837c
0x0804837f
0x08048382
0x08048387
0x0804838a
0x0804838d
0x08048390
0x08048391
0x08048392
0x08048395
End of assembler dump.
(gdb) disassemble func
Dump of assembler code for function func:
0x08048344
0x08048345
0x08048347
0x0804834a
0x0804834d
0x0804834e
End of assembler dump.
参数的访问,可以通过 ebp 加上减去偏移量:
mov 0xc(%ebp),%eax
add 0x8(%ebp),%eax