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

本文源地址: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. IntelliJ IDEA 在网页修改数据,但是在浏览器刷新的时候,不能读取到修改之后的数据

    使用IntelliJ IDEA 在网页修改数据,但是在浏览器刷新的时候,不能读取到修改之后的数据? 解决办法:tomcat配置中,On frame deactivation属性选择Update cla ...

  2. English—句子

    1. So far so good.   目前为止,一切都好. 2. Be my guest.     请便.别客气. 3. You're the boss.    听你的. 4.I've heard ...

  3. zabbix问题处理

    工作的时候回遇到各种各样的问题. 今天遇到一个关于zabbix的问题. "Zabbix agent on host.name is unreachable for 5 minutes&quo ...

  4. JSBinding+Bridge.NET:Unity游戏热更新方案

    老版本链接如下:http://www.cnblogs.com/answerwinner/p/4469021.html 新用户不要再使用老版本了. 新版本 JSBinding 将抛弃 SharpKit ...

  5. vm centos 网络配置

    安装Centos系统,查看网络配置. 输入命令:ifconfig 127.0.0.1 要开启网络 进入ifcfg-eth0文件. 输入命令:vi /etc/sysconfig/network-scri ...

  6. 转:linux lsof命令详解

    简介 lsof(list open files)是一个列出当前系统打开文件的工具.在linux环境下,任何事物都以文件的形式存在,通过文件不仅仅可以访问常规数据,还可以访问网络连接和硬件.所以如传输控 ...

  7. JQuery实现列表中复选框全选反选功能封装

    我们在做列表的时候经常会遇到全选,反选进行批量处理问题,例如: 我当时就是简单的实现了,然后想封装到公共的js中,封装的太烂,不好意思贴出来了(就是把实现代码之间放到公共js中,然后每个页面都用固定的 ...

  8. ng-if ng-show ng-hide 的区别

    angularjs ng-if ng-show ng-hide区别 在使用anularjs开发前端页面时,常常使用ng-show.ng-hide.ng-if功能来控制页面元素的显示或隐藏,那他们之间有 ...

  9. 免费提供UG、ProE二次开发、定制化开发服务

    免费提供UG.ProE二次开发,定制开发服务. 拥有六年UG.ProE二次开发经验,相关项目经验. 从事过智能设计.计算机图形学相关研究. 联系方式: QQ:1787326383 微信号:begtos ...

  10. Android framework完整源码下载

    包括cpp等native代码.可zip打包下载. https://github.com/android/platform_frameworks_base/branches/stale Android线 ...