为什么要学习这么底层的东西,因为能帮助我们分析core文件,从而定位production上的问题。往往我们production或者QA的机器上没有安装GDB,也没有源代码。学会一点基本的汇编知识配合mdb就能帮上大忙。
先看一个core stack:
-bash-3.00$ g++ -o Test Test.cpp
-bash-3.00$ ./Test
Segmentation Fault (core dumped)
-bash-3.00$ mdb core
Loading modules: [ libc.so.1 ld.so.1 ]
> $C
080476fc _Z9FunctionCPc+0×16(80509dc)
08047718 _Z9FunctionBPc+0×14(80509dc)
08047734 _Z9FunctionAPc+0×14(80509dc)
0804776c main+0x2b(1, 8047790, 8047798)
08047784 _start+0×80(1, 8047870, 0, 8047877, 80478b9, 804795c)
初看一下,_start->main->FunctionA->FunctionB->FunctionC。程序core在FunctionC中。一点都不错,不过而实际上mdb打印出来的call stack的意义是:
_start+0×80是调用main的返回地址,main+0x2b是调用FunctionC的返回地址,_Z9FunctionAPc+0×14是调用FunctionB的返回地址,_Z9FunctionBPc+0×14是调用FunctionC的返回地址,那_Z9FunctionCPc+0×16呢?哈哈,_Z9FunctionCPc+0×16是当前指令地址,在这里表示导致程序core dump的指令地址。
结合反汇编和当前各寄存器的值,可以定位程序崩溃的直接原因:
> _Z9FunctionCPc+0×16::dis
_Z9FunctionCPc+0×16: movl $0×64,(%eax)
..
movl $0×64,(%eax)试图把立即数100赋值给%eax所指向的地址空间。而这个时候%eax的值是:
> $r
%cs = 0x003b %eax = 0×00000000
所以造成访问违规而程序崩溃。
为什么mdb能够打印程序调用栈,程序的栈又是张什么样的呢?
先看源代码,在看栈的情况:
int FunctionC(char* str){
char *my = str;
int *p = 0;
*p = 100;
return 0;
}
int FunctionB(char* str){
char *my = str;
FunctionC(my);
return 0;
}
int FunctionA(char* str){
char *my = str;
FunctionB(my);
return 0;
}
int main(){
char *str="arrowpig";
FunctionA(str);
return 0;
}
> 080476f8,10/nap
–>低地址 [栈是从高地址往低地址长的,所以实际的调用顺序应该从下往上看]
0x80476f8:
0x80476f8: 0 –>FunctionB的局部变量: int *p = 0;
0x80476fc: 0×8047718 –>FunctionC的上级函数EBP
0×8047700: _Z9FunctionBPc+0×14 –>调用FunctionC的返回地址
0×8047704: 0x80509dc –>FunctionC的参数
0×8047708: 0 – >斜体部分似乎是gcc添加的函数调用
0x804770c: 0xfefb0cb8
0×8047710: 1
0×8047714: 0x80509dc –>FunctionB的局部变量: char* my=str;
0×8047718 : 0×8047734 –>FunctionB的上级函数(也就是FunctionA)的EBP
0x804771c: _Z9FunctionAPc+0×14 –>调用FunctionB的返回地址
0×8047720: 0x80509dc –>FunctionB的参数
0×8047724: libgcc_s.so.1`__register_frame_info_bases+0xe –>斜体部分似乎是gcc添加的函数调用
0×8047728: 0x80508a1
0x804772c: 0
0×8047730: 0x80509dc –>FunctionA的局部变量
0×8047734 : 0x804776c –>FunctionA的上级函数(也就是main)的EBP
0×8047738: main+0x2b –>调用FunctionA的返回地址
0x804773c: 0x80509dc –>FunctionA的参数
–>高地址
我们先回忆一下大学时代学汇编语言时的几个基本概念:
- 栈是用来存放临时数据的,后进先出。栈的增长方式是从高地址向低地址增长。
- EBP是栈基址指针,永远指向栈底(高地址);ESP是栈指针,永远指向栈顶(低地址)。
- CALL指令用来调用一个函数。CALL将下一条指令地址压栈,这样函数返回时才能执行下一条指令。
- RET用来从函数返回,之前CALL压栈的返回地址从栈中弹出赋给EIP,程序转到CALL前的下一条指令继续运行。
- ENTER建立当前函数的栈框架stack frame。
- LEAVE释放当前函数的栈框架。
Enter 相当于:
pushl %ebp
movl %esp,%ebp
Leave相当于:
movl %ebp,%esp
popl %ebp
好,现在我们看一下FunctionA的反汇编代码:
> _Z9FunctionAPc::dis
_Z9FunctionAPc: pushl %ebp –>保存上级函数的栈基址EBP
_Z9FunctionAPc+1: movl %esp,%ebp –>完成当前函数的Stack Frame的建立
_Z9FunctionAPc+3: subl $0×10,%esp –>分配16字节用于存放局部变量
_Z9FunctionAPc+6: movl 0×8(%ebp ),%eax –> 使用%ebp的正偏移量访问函数参数
_Z9FunctionAPc+9: movl %eax,-0×4(%ebp) –> 使用%ebp的负偏移量访问局部变量
_Z9FunctionAPc+0xc: pushl -0×4(%ebp) –> 下级函数的参数压栈
_Z9FunctionAPc+0xf: call -0x2d <_Z9FunctionBPc> –>调用下级函数
_Z9FunctionAPc+0×14: addl $0×4,%esp –>C的调用规范由主调函数负责从堆栈清理参数
_Z9FunctionAPc+0×17: movl $0×0,%eax –>%eax存放函数返回值: return 0;
_Z9FunctionAPc+0x1c: leave –> 释放 stack frame
_Z9FunctionAPc+0x1d: ret –> 弹出返回地址,从而继续执行
- 使用EBP我们可以找到上级函数的栈基址,调试器就是通过它打印call stack的。
- 使用subl $0xXX, %esp 分配局部变量空间。
- 使用%ebp的正偏移量访问函数参数。
- 使用%ebp的负偏移量访问局部变量。
- 函数返回值存在%eax中。
比如我们想看一下FunctionA的局部变量的值,我们知道这是一个字符串:
> 0x80509dc/S
0x80509dc: arrowpig
最后,我摘抄一下网上一篇名为:《x86汇编语言学习手记》里面的一张图作为参考:
假如函数A调用函数B,函数B调用函数C ,则函数栈框架及调用关系如下图所示:
+———————-+—-> 高地址
| EIP (上级函数返回地址) |
+———————-+
+–> | EBP (上级函数的EBP) | –+ <—- 当前函数A的EBP (即STP框架指针)
| +———————-+ +–>偏移量A
| | Local Variables | |
| | ………. | –+<—ESP指向函数A新分配的局部变量,局部变量可以通过A的ebp-偏移量A访问
| f +———————-+
| r | Arg n(函数B的第n个参数)|
| a +———————-+
| m | Arg .(函数B的第.个参数) |
| e +———————-+
| | Arg 1(函数B的第1个参数) |
| o +———————-+
| f | Arg 0(函数B的第0个参数) | –+ <—— B函数的参数可以由B的ebp+偏移量B访问
| +———————-+ +–> 偏移量B
| A | EIP (A函数的返回地址) | |
| +———————-+ –+
+—| EBP (A函数的EBP) |<–+ <—— 当前函数B的EBP (即STP框架指针)
+———————-+ |
| Local Variables | |
| ………. | | <—— ESP指向函数B新分配的局部变量
+———————-+ |
| Arg n(函数C的第n个参数) | |
+———————-+ |
| Arg .(函数C的第.个参数) | |
+———————-+ +–> frame of B
| Arg 1(函数C的第1个参数) | |
+———————-+ |
| Arg 0(函数C的第0个参数) | |
+———————-+ |
| EIP (B函数的返回地址) | |
+———————-+ |
+–> | EBP (B函数的EBP) |—+ <—— 当前函数C的EBP (即STP框架指针)
| +———————-+
| | Local Variables |
| | ………. | <—— ESP指向函数C新分配的局部变量
| +———————-+—-> 低地址