个人感言:真正的知识是深入浅出的,码农翻身” 公共号将苦涩难懂的计算机知识,用形象有趣的生活中实例呈现给我们,让我们更好地理解。感谢“码农翻身” 公共号,感谢你们的成果,谢谢你们的分享。

本文源地址:http://mp.weixin.qq.com/s?__biz=MzAxOTc0NzExNg==&mid=2665513039&idx=1&sn=381c1b8c7f86906c4838050b8c1db2bb&scene=21#wechat_redirect

我是CPU阿甘,今天要讲一讲函数调用的秘密,这个确实有点复杂,想透彻的理解机器代码层面的函数调用不容易。

我也是从无数的指令中悟出这个函数调用的秘密的, 所以慢慢来,不要急。 放松心情,慢慢的品味,你可能需要多看几遍才能明白。

但是你一旦理解了,绝对物超所值,因为你会了解到汇编,寄存器,指针,以及他们在一起到底是怎么工作的。

首先, 一个程序的有指令都老老实实的放在内存的一个地方,这个地方是Linux老大分配的,我干涉不了,但是这些指令都是我打电话通知硬盘,让他给运输到内存的。 
然后Linux老大就会告诉我程序的入口点,其实就是第一条指令的存放地址,我就打电话问内存要这个指令,取到指令以后就开始执行。这些指令当中无非有这么几类:

  1. 把数据从内存加载我的寄存器里什么?(寄存器就是CPU内部的一个临时的数据存储空间了);
  2. 对寄存器的数据进行运算,例如把两个寄存器的数加起来;
  3. 把我寄存器的数据再写到内存里。

但是我一旦遇到像这样的指令。

当把寄存器ebp的值压到栈里去,我就知道好戏要上场了,函数调用就会开始。

我们这些x86体系的机器有个特点,就是每个函数调用都会创建一个所谓的“帧”
哈哈, 不要被这些术语吓坏, 其实帧也就是我哥们内存中的一段连续的空间而已。像这样:

多个函数帧在内存里排起来, 就像一个先进后出的栈一样,不过,这个栈不像我们常见的栈,栈底在下面。相反,这个栈的栈底在上面,是从上往下生长的 (或者说是从高地址向低地址生长的)。
内存经常向我抱怨:"阿甘,你知道吗,每次我看到这个栈,都有一种真气逆行的感觉,半天都调整不过来 " 
但内存不知道,我有一个叫ebp的特殊寄存器,一直会指向当前函数在一个栈的开始地址。我还有另外一个特殊寄存器,叫做esp。他会随着指令的运行,指向函数帧的最后的地址,像这样:aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkJDQzA1MTVGNkE2MjExRTRBRjEzODVCM0Q0NEVFMjFBIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkJDQzA1MTYwNkE2MjExRTRBRjEzODVCM0Q0NEVFMjFBIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QkNDMDUxNUQ2QTYyMTFFNEFGMTM4NUIzRDQ0RUUyMUEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QkNDMDUxNUU2QTYyMTFFNEFGMTM4NUIzRDQ0RUUyMUEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6p+a6fAAAAD0lEQVR42mJ89/Y1QIABAAWXAsgVS/hWAAAAAElFTkSuQmCC" alt="" data-src="http://mmbiz.qpic.cn/mmbiz/KyXfCrME6UJrqjFwY47XjzDDkP8ichuyDxIvVUCdwfpuWqPicRZSjFbkNs4RIAMsUE1fwRkwNLgvR2NTfSN1M4HQ/0?wx_fmt=png" data-ratio="0.49764150943396224" data-w="424" />现在这个指令来了:

“把寄存器ebp的值压到栈里去”

“把esp的值赋给ebp”
aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkJDQzA1MTVGNkE2MjExRTRBRjEzODVCM0Q0NEVFMjFBIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkJDQzA1MTYwNkE2MjExRTRBRjEzODVCM0Q0NEVFMjFBIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QkNDMDUxNUQ2QTYyMTFFNEFGMTM4NUIzRDQ0RUUyMUEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QkNDMDUxNUU2QTYyMTFFNEFGMTM4NUIzRDQ0RUUyMUEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6p+a6fAAAAD0lEQVR42mJ89/Y1QIABAAWXAsgVS/hWAAAAAElFTkSuQmCC" alt="" data-src="http://mmbiz.qpic.cn/mmbiz/KyXfCrME6UJrqjFwY47XjzDDkP8ichuyDlIgA4yibVTCia9AKT4dSd3tYqRiahplyKnoeFSbwHHkcgj2JotX9U74Uw/0?wx_fmt=png" data-ratio="0.5170454545454546" data-w="528" />你看看,是不是新的函数帧生成了?只不过现在只有一行数据。ebp和esp指向同一地址。函数帧的第一行的地址是800,里边的内容是1000,也就是上个函数帧的地址。
注意,我们每次操作的是4个字节,所以原来esp 的地址是804,现在变成了800我又问内存要下一条指令:

“把esp 的值减去24”

下面几条指令是这样的:

“把10放到ebp 减去4的地址” (其实就是796嘛);

“把20放到ebp减去8的地址” (其实就是792嘛)。

你们知道这是干什么吗? 我想了好久才明白这是干嘛, 这其实就是在分配函数的局部变量啊我猜源代码应该是这样的:int x = 10;int y = 20;在我看来, x, y 只是变量, 他们叫什么根本不重要, 重要的是他们的值和地址!下面几条指令很有意思:

“把地址796作为数据放到 esp指向的地址” (其实就是776嘛)

“把地址792作为数据放到 esp+4指向的地址” (其实就是780嘛)

aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkJDQzA1MTVGNkE2MjExRTRBRjEzODVCM0Q0NEVFMjFBIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkJDQzA1MTYwNkE2MjExRTRBRjEzODVCM0Q0NEVFMjFBIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QkNDMDUxNUQ2QTYyMTFFNEFGMTM4NUIzRDQ0RUUyMUEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QkNDMDUxNUU2QTYyMTFFNEFGMTM4NUIzRDQ0RUUyMUEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6p+a6fAAAAD0lEQVR42mJ89/Y1QIABAAWXAsgVS/hWAAAAAElFTkSuQmCC" alt="" data-src="http://mmbiz.qpic.cn/mmbiz/KyXfCrME6UJrqjFwY47XjzDDkP8ichuyDI1OI6HtIH7zh7kdAfN68T06zZHmomJmMiafKuXu5zicPFScv3Q03Ox5A/0?wx_fmt=png" data-ratio="0.7877358490566038" data-w="424" />这又是在干嘛?

这其实就相当于把 x 的指针 &x和 y 的指针 &y ,放到了特定的地方, 准备着要做什么事情 , 可能要调用函数了。

所以,所谓的指针就是地址而已。

我猜程序员写的代码应该是这样:int x = 10;int y = 20;int sum= add(&x, &y); 接下来的指令是这样:

“调用函数 add”
我看到这样的函数就需要特别小心, 因为我必须要找到 add函数返回以后的那条指令的地址, 把它也压到栈里去。

int x = ;
int y = ;
int sum = add(&x, &y);
printf("the sum is %d\n",sum);// 假设这条指令的地址是100

注意啊, 把函数调用结束的以后的返回地址100压入栈以后, esp 也发生变化了, 指向了772的位置我会找到函数Add 的指令,继续执行

“把寄存器ebp的值压到栈里去”

“把esp的值赋给ebp”

“把寄存器ebx的值压入栈”

你看每个函数的开始指令都是这样, 我猜这应该是一种约定吧这里额外把ebx这个寄存器压入栈, 是因为ebx可能被上个函数使用, 但是在add函数中也会用 , 为了不破坏之前的值, 只有先委屈一下暂时放到内存里吧。

接下来的指令是:

“把ebp 加8的数据取出来放到 edx 寄存器” (ebp+8 不就是地址776嘛,其中存放的是&x的地址,这就是取参数了)
“把ebp 加12的数据取出来放到 ecx 寄存器” (ebp+12 不就是地址780嘛, 其中存放的是&y的地址)

注意啊,现在edx的值是796,ecx的值是792,但他们仍然不是真正的数据,而是指针(地址)!
“把edx 指向的内存地址(796)的数据取出来,放到ebx 寄存器”
“把ecx 指向的内存地址(792)的数据取出来,放到eax寄存器” 
此时此刻,终于取到了真正的值,ebx = 10,eax = 20你晕了没有?

如果你到此已经晕了,建议你再读一遍。

我想源代码应该非常的简单,就是这样:

int add(int *xp , int *yp)
{
int x = *xp; int y = *yp;
....
}

“把ebx 和 eax 的值加起来,放到 eax寄存器中”

这个指令我最擅长做了。接下来的指令也很关键, add 函数已经调用完成, 准备返回了

“把esp 指向的数据弹出的ebx寄存器”

“把esp 指向的数据弹出到ebp寄存器”

你看add 函数帧已经消失了,或者换句话说,add 函数帧的数据还在内存里,只是我们不在关心了!

接下来的指令非常的关键:

“返回”

我就会取出那个返回地址,也就是 100,去这里找指令接着执行其实就是这条语句:

 printf("the sum is %d\n",sum);

问你一个问题,sum的值在那里保存着呢? 对,是在eax寄存器里 !
搞定了,看着很复杂,其实看透了也挺简单吧。

函数调用,关键就是:

(1)把参数和返回地址准备好;

(2)然后大家都遵循约定, 每次新函数都要建立新的函数帧:

“把寄存器ebp的值压到栈里去”

“把esp的值赋给ebp”

(3) 函数调用完了,重置 ebp 和esp,让他们重新指向调用着的栈帧。

“码农翻身” 公共号 : 由工作15年的前IBM架构师创建,分享编程和职场的经验教训。

长按二维码, 关注码农翻身

CPU阿甘:函数调用的秘密的更多相关文章

  1. CPU阿甘

    本系列文章全部摘选自"码农翻身"公众号,仅供个人学习和分享之用.文章会给出原文的链接地址,希望不会涉及到版权问题. 个人感言:真正的知识是深入浅出的,码农翻身" 公共号将 ...

  2. 【转载】CPU阿甘

    原文:CPU阿甘  前言 上帝为你关闭了一扇门,就一定会为你打开一扇窗这句话来形容我最合适不过了.我是CPU, 他们都叫我阿甘, 因为我和<阿甘正传>里的阿甘一样,  有点傻里傻气的.上帝 ...

  3. CPU阿甘之烦恼

    转自“码农翻身”公共号,原文地址CPU阿甘之烦恼 总结:(程序加载到内存运行的演变过程) 内存存放程序.OS负责加载程序到内存.CPU负责运行内存中的程序 1.串行:加载一个完整程序到内存,CPU运行 ...

  4. 网络编程基础【day10】:我是一个进程(三)

    本节内容 1.引子 2.进程的诞生 3.线程 4.争吵 一.引子 我听说我的祖先们生活在专用计算机里, 一生只帮助人类做一件事情,比说微积分运算 了.人口统计了 .生成密码.甚至通过织布机印花 !   ...

  5. JVM学习(2)——技术文章里常说的堆,栈,堆栈到底是什么,从os的角度总结

    俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及到的知识点总结如下: 堆栈是栈 JVM栈和本地方法栈划分 Java中的堆,栈和c/c++中的堆,栈 数据结构层面的堆,栈 os层面 ...

  6. Javascript:一个屌丝的逆袭

    HTML负责结构, CSS负责展示, 而我(加上AJAX, JSON) 负责逻辑.于是前端编程三剑客形成了. http://mp.weixin.qq.com/s?__biz=MzAxOTc0NzExN ...

  7. 理解 Memory barrier

    理解 Memory barrier(内存屏障) 发布于 2014 年 04 月 21 日2014 年 05 月 15 日 作者 name5566 参考文献列表:http://en.wikipedia. ...

  8. Casual Note of OS

    20170104 冯诺依曼计算机(遵循冯诺依曼结构设计的计算机:存储器.运算器.控制器.输入设备.输出设备)之前也有计算机,不过在那之前的计算机是专用的,不可编程,只能干特定的事情没法干其他事.与之前 ...

  9. java 面试大全

    一.CoreJava 部分: 基础及语法部分: 1.面向对象的特征有哪些方面? [基础] 答:面向对象的特征主要有以下几个方面: 1)抽象:抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地 ...

随机推荐

  1. Get Jenkins job build queue length

    Jenkins API doesn’t provide the job build queue length. Hence, it seems we have to parse the html to ...

  2. 微信在IOS7下无法分享图片

    家里老大的iphone5在WWDC后第一时间升级了IOS7. 整体的UI风格和功能都很喜欢, 偶尔的crash还能接受. 但是最常用的软件之一微信,在IOS7下无法分享图片这点一直让她耿耿于怀. 从用 ...

  3. ibm v3700

    raid5总容量计算(n-1)*最小盘容量RAID0:N块盘组成,逻辑容量为N块盘容量之和:RAID1:两块盘组成,逻辑容量为一块盘容量:RAID3:N+1块盘组成,逻辑容量为N块盘容量之和:RAID ...

  4. Apache Tomcat 7 安装与配置

    下载 首先需要下载tomcat7的安装文件,地址如下: http://mirror.bit.edu.cn/apache/tomcat/tomcat-7/v7.0.69/bin/apache-tomca ...

  5. [linux]解决vsftpd 读取目录列表失败的问题

    使用第三方FTP软件filezilla进行登陆,出现如下错误:状态:    正在连接 192.168.1.6:21...状态:    连接建立,等待欢迎消息...响应:    220 (vsFTPd ...

  6. Laravel项目目录结构说明

    Laravel项目目录结构说明: |- vendor 目录包含你的 Composer 依赖模块及laravel框架. |- bootstrap 目录包含几个框架启动跟自动加载配置的文件. |- app ...

  7. Python 字符串格式化

    Python 字符串格式化 Python的字符串格式化有两种方式: 百分号方式.format方式 百分号的方式相对来说比较老,而format方式则是比较先进的方式,企图替换古老的方式,目前两者并存 一 ...

  8. Microsoft JScript 运行时错误: '$' 未定义

    在运行MVC Music Store时Visual Studio 捕捉到此错误: Microsoft JScript 运行时错误: '$' 未定义 可能是引用的Scripts 引用出错,检查当前文件中 ...

  9. Oracle补习班第十天

    Life without love is like a tree without blossoms or fruit. 缺少爱的生活就像从未开花结果的枯树 RMAN备份工具 crosscheck ba ...

  10. PL/SQLDeveloper导入导出Oracle数据库方法

    前一篇博客介绍了Navicat工具备份Oracle的方法,这篇博客介绍一下使用PL/SQL Developer工具导入导出Oracle数据库的方法. PL/SQL Developer是Oracle数据 ...