《老码识途》读书笔记:第一章--欲向码途问大道,锵锵bit是吾刀(中)

   3、函数调用和局部变量

要研究函数的调用过程,先来看下面的一段代码:

 1 int Add(int x, int y)
 2 {
 3     int sum;
 4     sum = x + y;
 5     return sum;
 6 }
 7
 8 void main()
 9 {
10     int z;
11     z = Add(1, 2);
12     printf("z = %d\n", z);
13 }

对于 z = Add(1, 2); 这一句,我们可以看到其汇编代码和机器码如下:

1 z = Add(1, 2);
2
3 00413762     6a 02                push  2
4 00413764     6a 01                push  1
5 00413766     e8 7a da ff ff       call  004111e5
6 0041376B     83 c4 08             add   esp, 8
7 0041376E     89 45 18             mov   dword ptr [ebp-8], eax

上述指令表明主函数将跳转到内存地址004111e5来进行Add函数的调用执行。在机器码中,如果已知e8代表的是call指令,那后面的四个字节代表什么呢?call指令采用的是相对偏移量寻址,也就是是通过 基址 + 偏移量 的方式来获取最终的目的地址的。根据对mov指令进行分析后的经验,知道小端机内存中的7a da ff ff 代表 0xffffda7a 。但是00413766加上0xffffda7a得到的结果与目的地址004111e5相去甚远。因此还要考虑0xffffda7a有可能为负数。在计算机中有符号数的第一位为1则说明该数为负数,显然按照这样来看0xffffda7a是一个负数,与其对应的相反数正数为0x2586。如果用call指令的地址00413766减去2586,得到的结果为0X4111e0,与真正的目的地之间相差5个字节。通过观察发现call指令的机器码长度刚好为5个字节,因此即可得出最终的结论:

  x86系列CPU的call指令的寻址方式为:用与call指令相关的偏移量定位跳转到的地址

  偏移量计算如下:偏移量 = 跳转到的地址 - call指令后一条指令的起始地址

进行函数调用的时候,要使用到两个寄存器ESP和EIP,它们保存着与函数跳转和返回相关的信息。通过在函数调用过程中对两个寄存器的值进行观察,可知EIP中存储的是call跳转到的指令的地址004111E5。而ESP中存储的则是0X00116E60这个内存地址,显然,它并不是call指令的返回地址0x0041376b。再根据该内存地址去获取保存在其中的值,得到的结果为从该地址开始往高地址依次数四个字节的值分别为 6b 37 41 00,即0x0041376b,正好是call指令返回的的地址。由此可知:

  call指令将返回地址保存在内存中,而且ESP寄存器指向了该内存。call指令相当于以下两条指令的组合:

  push  返回地址

  jmp   函数入口地址

调用函数的时候有时还需要给函数传递参数,在上面的 z = Add(1, 2) 这一赋值语句对应的几句汇编代码中,与传递参数相关的是下面的两句:

1 00413762    6a 02                 push  2
2 00413764    6a 01                 push  1

这两句汇编代码的作用是将两个参数按照从右至左的顺序压入到内存栈中。在此有必要说明一下,内存中栈的栈顶内存地址保存在ESP寄存器中。由于栈在内存中是从高地址向低地址扩展的,因此每次压入一个参数,ESP寄存器中的值指向的内存地址都会按照一定的字节数减少相应的值。在压入第一个参数2之前,ESP寄存器中的值为0x00116e6c。压入参数2之后,ESP寄存器的值变为0x00116e68,此时ESP寄存器指向的是参数2存放在内存中的地址。再压入参数3之后,ESP寄存器的值减少为为0x00116e64,指向参数1存放在内存中的地址。在执行call指令之后,函数的返回地址被压栈,ESP寄存器的值又减少了4个字节,变成0x00116e60。

由于参数存储在内存栈中,因此被调用的函数要获得参数,就必须借助esp寄存器中的值。由于参数之上还压入了函数的返回地址,且每个内存地址长度都为四个字节,因此第一个参数的内存地址即为esp+4,而第二个参数的内存地址为esp+8,以此类推。但是由于esp的值会随着栈的变化而变化,且难保在函数执行过程中不会改变栈的当前状态,因此还需要另外一个寄存器ebp(扩展基址指针寄存器)来暂时存放esp寄存器中的值。但是在函数层层嵌套时,内层函数执行完毕退出后如果不改变ebp寄存器的值而让外层函数继续使用的话就会出现不可预知的错误情况。因此在每次调用一个函数时,要先将当前ebp的值push入栈保存起来,然后才将当前esp的值存入ebp寄存器中。内层函数执行完毕之后,要将函数执行之前保存的ebp寄存器的值出栈并恢复到ebp寄存器中,外层函数就可以继续使用ebp的值了。要注意的是,由于ebp压栈后esp的值又减小了4,所以在将esp的值赋给ebp后,第一个参数的内存地址应该为ebp+8,同时第二个参数的内存地址应该为ebp+0ch(即12),以此类推。

为了验证上面的结论,先来看看下面的这一段代码:

 1 int Add(int x, int y)
 2 {
 3
 4     00411430         push    ebp
 5     00411431         mov     ebp, esp
 6     00411433         sub     esp, 0cch
 7     00411439         push    ebx
 8     0041143a         push    esi
 9     0041143b         push    edi
10     0041143c         lea     edi, [ebp+ffffff34h]
11     00411442         mov     ecx, 33h
12     00411447         mov     eax, 0cccccccch
13     0041144c         rep     stos dword ptr es:[edi]
14
15     int sum;
16     sum = x + y;
17     0041144e         mov     eax, dword ptr [ebp+8]
18     00411451         add     eax, dword ptr [ebp+0ch]
19 }

观察上面的代码可知,跳转到Add函数之后,在执行第一条语句之前程序预先做了一连串的准备工作。先将ebp的值压栈,然后将esp的值赋给ebp。在后面的语句中,获取参数x是通过内存地址ebp+8,而获取参数y则是通过内存地址ebp+12,与之前的结论相同。

在函数的执行过程中可能还会使用到用户定义的局部变量,如下面的代码中就使用到了局部变量sum:

int Add(int x, int y)
{int sum;sum = x + y;
}

在VS2008中反汇编得到与该函数中的赋值语句相对应的三条汇编语句如下:

1  0041144e         mov     eax, dword ptr [ebp+8]
2  00411451         add     eax, dword ptr [ebp+0ch]
3  00411454         mov     dword ptr [ebp-8], eax

在最后一条汇编语句中,将两个参数相加得到的值保存在了地址为ebp-8的内存空间中。这是因为局部变量的特点与参数一样,都是当函数调用完毕就不再使用,所以仿效参数将其分配在栈上。但是栈上方已经被参数和返回地址等使用,因此只能使用栈更低地址的空间,每分配一个局部变量都要进行一次压栈。但是要注意的是,压栈一次的话地址应该为ebp-4而非上面见到的ebp-8。事实上,在VC 6.0编译器中反汇编得到的代码如下:

1  00401038         mov     eax, dword ptr [ebp+8]
2  0040103b         add     eax, dword ptr [ebp+0ch]
3  0040103e         mov     dword ptr [ebp-4], eax

此处的局部变量地址确为ebp-4。造成这种不同的原因主要是在VS2008中为了防止溢出攻击而采用的StackGaurd溢出攻击防护机制,在ebp和局部变量的地址之间空出了四个字节。

函数执行完毕就要返回调用的地方,由之前的叙述可知call指令已经将其后指令的地址压栈保存,因此可以使用该地址进行返回。函数返回要用到返回指令ret,ret指令的介绍如下:

ret指令:将栈顶保存的地址弹入指令寄存器EIP,相当于"pop eip",从而让程序跳转到该地址。执行ret指令后,寄存器EIP(存储了被弹出的栈顶地址)和ESP(在32位x86中加4)的值有变化

转载于:https://www.cnblogs.com/ZJAJS/archive/2013/03/08/2949162.html

《老码识途》读书笔记:第一章(中)相关推荐

  1. 老码识途读书笔记 1

    知识点记录: 1.int 或指针类型的全局变量默认初始化为0,局部变量则为0xcccccccc.(win7 + vs2008 ) 2.内存溢出攻击即使用6个字节空间改变程序执行流程达到某种目的.话说当 ...

  2. 老码识途学习笔记(一)

    第一章 全局变量引发的故事 1 程序存储区 程序存储区一般有下列几段: 程序代码区(SECTION.txt ): 用来存放可执行文件的操作指令(二进制),也就是说是它是可执行程序在内存中的镜像.代码段 ...

  3. 《老码识途》读书笔记:第一章(上)

    <老码识途>读书笔记:第一章--欲向码途问大道,锵锵bit是吾刀(上)   1.赋值语句 对于全局变量赋值语句,例如下面这句: 1 int gi; 2 void main(int argc ...

  4. 《MAC OS X 技术内幕》读书笔记第一章:MAC OS X的起源

    <MAC OS X 技术内幕>读书笔记第一章:MAC OS X的起源 前言 1 System x.x系列 1.1System 1.0(1984年1月24日) 1.2System 2.x(1 ...

  5. 老码识途:从机器码到框架的系统观逆向修炼之路 pdf电子书

    重要提示尊敬的用户您好,由于老码识途:从机器码到框架的系统观逆向修炼之路pdf书受百度网盘影响无法做公共分享,只能私密分享,有不到之处请多多谅解! 百度网盘链接: http://pan.baidu.c ...

  6. Android群英传神兵利器读书笔记——第一章:程序员小窝——搭建高效的开发环境

    Android群英传神兵利器读书笔记--第一章:程序员小窝--搭建高效的开发环境 目录 1.1 搭建高效的开发环境之操作系统 1.2 搭建开发环境之高效配置 基本环境配置 基本开发工具 1.3 搭建程 ...

  7. linux鸟叔私房菜读后感,鸟叔的Linux私房菜 读书笔记 第一章

    目录dom 硬盘数学 第一章 计算机概论 知识点总结 计算机的定义为:接受使用者输入指令与资料,经由中央处理器的数学与逻辑单元运算处理后,以产生或储存成有用的资讯:程序 电脑的五大单元包括:输入单元. ...

  8. 流畅的python读书笔记-第一章Python 数据模型

    第一章 python数据类型 1 隐式方法 利用collections.namedtuple 快速生成类 import collectionsCard = collections.namedtuple ...

  9. 《MongoDB权威指南》读书笔记 第一章 简介

    第一章 1.面向文档的数据库,不是关系形数据库 2.面向文档的数据模型可使数据在多台服务器之间分割,平衡集群的数据和负载 3.具有的功能:索引.聚合.固定集合.文件存储 4.卓越的性能,把逻辑尽量交给 ...

  10. 深入理解计算机系统第四版_《深入理解计算机系统》读书笔记 —— 第一章 计算机系统漫游...

    本书第一章沿着一个程序的生命周期,简要地介绍一些逐步出现的关键概念.专业术语和组成部分. 一.信息就是位+上下文 在计算机系统中所有的信息都由一串比特来表示. 一串相同的比特(或者几个相同的字节)可以 ...

最新文章

  1. SpringMVC:注解@ControllerAdvice的工作原理
  2. php微信小程序物流进度推送,微信小程序 消息推送php服务器验证实例详解
  3. 扩展 OpenLayers.Layer.WMS 为自定义的瓦片浏览服务
  4. spark standalone集群安装及spark on yarn
  5. JVM调优总结(七)-典型配置举例1
  6. 200行代码实现视频人物实时去除
  7. [bzoj1717][Usaco2006 Dec]Milk Patterns 产奶的模式 (hash构造后缀数组,二分答案)
  8. 打印浏览器文章为pdf
  9. 最快理解使用CSS弹性盒子
  10. TcpSocket的Qt串口实现与QtSocket接收数据不完整处理方法
  11. 聚焦技术实战!MDCC 2016 移动开发者大会盛大开幕
  12. 爬虫,酷我音乐接口解析
  13. RabbitMQ使用规范
  14. 掌握命令结构,详解monkey命令
  15. 关于RxJava2.0你不知道的事
  16. 2020年金融科技创新项目总结
  17. 前端别再错过2022的金三银四了。。
  18. 服务器端返回的状态码是什么意思
  19. C# Base64编码、AES等编码加、解密
  20. 怎么聊微信才能让她喜欢你

热门文章

  1. Javascript php 异常捕获
  2. [英语]工作邮件中超实用的100句英文
  3. 数据库基础知识——互动百科
  4. PEP8 Python 编码规范
  5. JAVA线程池 之 Executors (一) 简介
  6. [洛谷P2370]yyy2015c01的U盘
  7. Emacs正则表达式+零宽断言/环视
  8. 转: Oracle AWR 报告 每天自动生成并发送邮箱
  9. opencv fillConvexPoly深究
  10. POJ 1149 PIGS(最大流)dinic模板注释