CPU阿甘:函数调用的秘密
个人感言:真正的知识是深入浅出的,码农翻身” 公共号将苦涩难懂的计算机知识,用形象有趣的生活中实例呈现给我们,让我们更好地理解。感谢“码农翻身” 公共号,感谢你们的成果,谢谢你们的分享。
我是CPU阿甘,今天要讲一讲函数调用的秘密,这个确实有点复杂,想透彻的理解机器代码层面的函数调用不容易。 我也是从无数的指令中悟出这个函数调用的秘密的, 所以慢慢来,不要急。 放松心情,慢慢的品味,你可能需要多看几遍才能明白。 但是你一旦理解了,绝对物超所值,因为你会了解到汇编,寄存器,指针,以及他们在一起到底是怎么工作的。
首先, 一个程序的有指令都老老实实的放在内存的一个地方,这个地方是Linux老大分配的,我干涉不了,但是这些指令都是我打电话通知硬盘,让他给运输到内存的。
然后Linux老大就会告诉我程序的入口点,其实就是第一条指令的存放地址,我就打电话问内存要这个指令,取到指令以后就开始执行。这些指令当中无非有这么几类:
- 把数据从内存加载我的寄存器里什么?(寄存器就是CPU内部的一个临时的数据存储空间了);
- 对寄存器的数据进行运算,例如把两个寄存器的数加起来;
- 把我寄存器的数据再写到内存里。
但是我一旦遇到像这样的指令。
当把寄存器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阿甘:函数调用的秘密的更多相关文章
- CPU阿甘
本系列文章全部摘选自"码农翻身"公众号,仅供个人学习和分享之用.文章会给出原文的链接地址,希望不会涉及到版权问题. 个人感言:真正的知识是深入浅出的,码农翻身" 公共号将 ...
- 【转载】CPU阿甘
原文:CPU阿甘 前言 上帝为你关闭了一扇门,就一定会为你打开一扇窗这句话来形容我最合适不过了.我是CPU, 他们都叫我阿甘, 因为我和<阿甘正传>里的阿甘一样, 有点傻里傻气的.上帝 ...
- CPU阿甘之烦恼
转自“码农翻身”公共号,原文地址CPU阿甘之烦恼 总结:(程序加载到内存运行的演变过程) 内存存放程序.OS负责加载程序到内存.CPU负责运行内存中的程序 1.串行:加载一个完整程序到内存,CPU运行 ...
- 网络编程基础【day10】:我是一个进程(三)
本节内容 1.引子 2.进程的诞生 3.线程 4.争吵 一.引子 我听说我的祖先们生活在专用计算机里, 一生只帮助人类做一件事情,比说微积分运算 了.人口统计了 .生成密码.甚至通过织布机印花 ! ...
- JVM学习(2)——技术文章里常说的堆,栈,堆栈到底是什么,从os的角度总结
俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及到的知识点总结如下: 堆栈是栈 JVM栈和本地方法栈划分 Java中的堆,栈和c/c++中的堆,栈 数据结构层面的堆,栈 os层面 ...
- Javascript:一个屌丝的逆袭
HTML负责结构, CSS负责展示, 而我(加上AJAX, JSON) 负责逻辑.于是前端编程三剑客形成了. http://mp.weixin.qq.com/s?__biz=MzAxOTc0NzExN ...
- 理解 Memory barrier
理解 Memory barrier(内存屏障) 发布于 2014 年 04 月 21 日2014 年 05 月 15 日 作者 name5566 参考文献列表:http://en.wikipedia. ...
- Casual Note of OS
20170104 冯诺依曼计算机(遵循冯诺依曼结构设计的计算机:存储器.运算器.控制器.输入设备.输出设备)之前也有计算机,不过在那之前的计算机是专用的,不可编程,只能干特定的事情没法干其他事.与之前 ...
- java 面试大全
一.CoreJava 部分: 基础及语法部分: 1.面向对象的特征有哪些方面? [基础] 答:面向对象的特征主要有以下几个方面: 1)抽象:抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地 ...
随机推荐
- Ext.Net 学习随笔 001 安装Ext.Net
Ext.Net版本:4.1.0 Ext.Net官网:ext.net Ext.Net官方演示:mvc.ext.net Ext.Net MVC Example 下载:github.com/extnet/E ...
- JSBinding+Bridge.NET:Unity游戏热更新方案
老版本链接如下:http://www.cnblogs.com/answerwinner/p/4469021.html 新用户不要再使用老版本了. 新版本 JSBinding 将抛弃 SharpKit ...
- maven-deploy失败
昨天遇到的问题,mavne项目执行deploy的时候,出错.提示 Return code is: 401, ReasonPhrase: Unauthorized. -> [Help 1] 很直白 ...
- [原] wmic: Invalid XSL format (or) file name错误解决方法
之前运行wmic命令正确,今天在服务器上出现Invalid XSL format (or) file name的提示,搜索了一下,在这里找到了答案: http://www.ctkn.net/2011/ ...
- MYSQL 判断一个时间段是否在另一个时间段内。
[1 CREATE TABLE #B 2 ( 3 MeetingRoom int, 4 BeginTime datetime, 5 EndTime datetime6 ) 7 insert into ...
- LB(Load balance)负载均衡集群--{LVS-[NAT+DR]单实例实验+LVS+keeplived实验} 菜鸟入门级
LB(Load balance)负载均衡集群 LVS-[NAT+DR]单实例实验 LVS+keeplived实验 LVS是Linux Virtual Server的简写,意即Linux虚拟服务器,是一 ...
- Play with docker 1.12
Docker v1.12 brings in its integrated orchestration into docker engine. Starting with Docker 1.12, w ...
- Rails ---> routes.rb 详解
理解路由的目的 看懂routes.rb文件中的代码 使用经典的hash风格或者现在比较流行的Restful风格构造你自己的路径 断定一个路径会映射到哪一个controller和action 路由的双重 ...
- 【转】JSch - Java实现的SFTP(文件下载详解篇)
上一篇讲述了使用JSch实现文件上传的功能,这一篇主要讲述一下JSch实现文件下载的功能.并介绍一些SFTP的辅助方法,如cd,ls等. 同样,JSch的文件下载也支持三种传输模式:OVERWRI ...
- 用户输入函数--raw_input、input
1.raw_input python2.7用户输入字符串的话用raw_input.如果使用input输入字符串的话需要先把字符串放到变量中才可,但是用input输入数字的话是可以直接输入的,所以说在p ...