关闭

汇编级Call Stack的构成

552人阅读 评论(0) 收藏 举报

为什么要学习这么底层的东西,因为能帮助我们分析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的参数
–>高地址

 

 

我们先回忆一下大学时代学汇编语言时的几个基本概念:

  1. 栈是用来存放临时数据的,后进先出。栈的增长方式是从高地址向低地址增长。
  2. EBP是栈基址指针,永远指向栈底(高地址);ESP是栈指针,永远指向栈顶(低地址)。
  3. CALL指令用来调用一个函数。CALL将下一条指令地址压栈,这样函数返回时才能执行下一条指令。
  4. RET用来从函数返回,之前CALL压栈的返回地址从栈中弹出赋给EIP,程序转到CALL前的下一条指令继续运行。
  5. ENTER建立当前函数的栈框架stack frame。
  6. 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                      –> 弹出返回地址,从而继续执行

  1. 使用EBP我们可以找到上级函数的栈基址,调试器就是通过它打印call stack的。
  2. 使用subl $0xXX, %esp 分配局部变量空间。
  3. 使用%ebp的正偏移量访问函数参数。
  4. 使用%ebp的负偏移量访问局部变量。
  5. 函数返回值存在%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新分配的局部变量
|     +———————-+—-> 低地址

 

 

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:78668次
    • 积分:1441
    • 等级:
    • 排名:千里之外
    • 原创:60篇
    • 转载:0篇
    • 译文:8篇
    • 评论:16条
    最新评论