现在的位置: 首页 > 综合 > 正文

函数调用的汇编实现浅析

2013年11月23日 ⁄ 综合 ⁄ 共 4755字 ⁄ 字号 评论关闭

一, 引子
       对于汇编的理解是思维的一个跳变, 指令语法的理解对语义的理解似乎毫无
帮助, 就好象我对设计模式的模糊理解使我看代码的时候总是困惑于其如此实现
的原因. 这也使我想起看英文书的时候, 经常是每一个单词都认识, 但组合在一起
却不知所云. 其理相通罢.
      局部变量的存储及子过程的调用均是用栈实现的,  这个数据结构被证明是最
有效的, 栈的实现如此重要以至CPU对它提供了内置的支持,  这里提到的CPU是IA32,
实际上它包括两种微架构, 但这不是我现在关心的, 因为这里提到的约定对于这两种微
架构来说是共同的. 几个重要的约定是:
      地址编号从下往上增长, 比特位从右往左增长. 如果超过一个字节的数据, 低
字节放在低地址中, 此所谓 "little endian". 如:
31            23         15            7                0
+-----------+---------+----------+-------------+ 0Ch
|               |            |             |                 |
+-----------+---------+----------+-------------+ 08h
|               |            |             |                 |
+-----------+---------+----------+-------------+ 04h
|               |            |    12h    |      34h     |
+-----------+---------+----------+-------------+ 00h
      如果 00h 处存放的是一个字的话, 那么它的值为 1234h. 初看起来, 这样的表达方式
让人不是太习惯, 虽然我们通常将地址想象成一个一维的序列, 但我们必须要理解这样
的方式, 因为在CPU眼中, 数据是按照这种方式获取的,  这有助于我们对数据对齐的理解.
 
二, 代码
     一个很简单但比较典型的程序, 包含函数调用, 赋值, 运算, 返回.
 int add (int i1, int i2) {
  int val1 = i1;
  int val2 = i2;

  static int val3 = 0x20;
  ++val3;

  return val1 + val2 + val3;
 }

 int main () {
  int ret = 0x10;
  ret = add (0x1, 0x2);

  return 0;
 }
 

//////////////////////////////////////////////////////////////////////////////////////

三, 指令解析
    不同的编译器, 代码会略有不同, 但主线是不变的, 这没有唯一答案, 我需要了解
的只是它的思路.
 1, esp和ebp
      SS拥有当前进程运行堆栈的选择符(selector), 而 esp 则永远指向栈的顶端,
 请注意, 栈的顶端是栈中地址最小的地方, 记住地址是从下往上增长的.
  我们已有基址和指向栈中元素的指针, 为何还需要ebp? 设想 esp 总是在
 不断变化, 我如何用正确的偏移引用栈中的局部变量和参数呢? 于是ebp
 被用来作为当前活动记录 (也叫栈帧) 的基指针, 它永远指向函数返回地址
 即调用者的下一条指令的地址, 和被调用函数中第一个局部变量的中间, 当然它
 是一个双字 (32位). 
 
2, call
      call 指令调用一个过程, 但它有一个小动作, 在参数入栈以后, 被调用函数
 执行 之前, 它会将当前函数的下一条指令地址, 即EIP的值压入. 

 3, 调用约定 ( calling convention)
      在 c/c++ 中, 函数的默认调用约定为 cdecl, 它约定参数从右到左入栈, 由调用
 者清理堆栈, 所谓清理, 即调整ESP的值, 使得原来的局部数据不再属于栈. 当然
 由此也可以理解 stdcall 的工作方式, 不过换成被调函数自己清理罢了. 
 
4, retn
     这称之为 "near return", 在执行这个操作的时候, ESP已经指向函数的返回地址,
 ret 指令将弹出这个地址, 并将它存入 EIP, "near return" 意指 CS 的值没有变化, 而
 "far return" 将修改 CS 的值. 
 
5, 静态变量
     就象我早已经知道的, 静态变量是在一个全局存储区, 不过那只是在概念中
 远没有看见代码这么直观, 在下面的代码中就可以看到, 这个变量并不在栈中出
 现, 事实上, 如果我不对它进行一下操作, 甚至感觉不到它的存在. 这就是为什么
 静态变量总能保持它的值的秘密.

四, 代码解析
 _main:
                push    ebp                               ;保存原来的ebp
                mov     ebp, esp                        ;esp, ebp指向栈顶
                push    ecx                                ;int ret 只有一个局部变量, 存在ecx中.
                mov     [ebp - 4], 10h                 ;ret = 0x10 赋值
                push    2                                    ;要调用add了, 先将参数压栈
                push    1                                    ;
                call     add                                  ;调用, 先将EIP压栈
                add     esp, 8                              ;清理堆栈, 两个参数, 共8个字节
                mov     [ebp - 4], eax                  ;返回值在eax中, 将它赋给 ret
                xor     eax, eax                           ;将 eax 清 0, main返回值是0
                mov     esp, ebp                         ;将main的局部数据弹出
                pop     ebp                                 ;恢复ebp的值, 同时esp正好指向main的返回地址
                retn                                           ;将返回地址存入EIP, 转移流程.

/////////////////////////////////////////////////////////////////////////////////////////////////////////////
 add:
                 push    ebp                                ;这两行代码同main中的相同
                 mov     ebp, esp                         ;
                 sub     esp, 8                              ;有两个局部变量, 留出8个字节空间
                 mov     eax, [ebp+8]                   ;用eax中转, 将 i1 的值赋给val1
                 mov     [ebp-8], eax                    ;  val1 = i1;
                 mov     ecx, [ebp+0Ch]                ;用ecx中转, 将 i2 的值赋给val2
                 mov     [ebp-4], ecx                    ;  val2 = i2;
                 mov     edx, dword_407030         ; static int val3 静态变量并没有被压入栈
                 add     edx, 1                              ;
                 mov     dword_407030, edx          ; val3 = val3 +1;
                 mov     eax, [ebp-8]                     ;
                 add     eax, [ebp-4]                      ;
                 add     eax, dword_407030            ; val1 + val2 + val3 返回值放在在eax中
                 mov     esp, ebp                           ;丢弃局部数据
                 pop     ebp                                  ;这两行代码与main中的相同
                 retn                                            ;

 main 的 stack frame:
 +-------------------+<--------------high address
 |main返回地址    |
 +-------------------+
 |      ebp             |<-----------------ebp for main
 +-------------------+
 |      int ret          |         ebp-4
 +-------------------+
 |      int i2           |          ebp+0Ch
 +-------------------+
 |      int i1           |          ebp+8
 +-------------------+
 |  add返回地址     |
 +-------------------+
 |       ebp            |<------------------ebp for add
 +-------------------+
 |      int val2        |          ebp-4
 +-------------------+
 |      int val1        |          ebp-8
 +-------------------+
五, 尾声
      顺便提一下的是在 stdcall 调用约定中, 被调用函数, 如此处的add, 返回语句变为 "ret 8" ,
它在弹出返回地址后, 自己将ESP的指加了8个字节, 此所谓被调用者清理堆栈.
 虽然现实世界远比此复杂, 但这是一个好的开始, 很多看起来高不可攀的东西, 其实
对于我们来说, 也许就是差一把进入大门的钥匙.

抱歉!评论已关闭.