透彻的理解机器代码层面的函数调用

描述

我是CPU阿甘, 上次我给大家承诺过,要讲一讲函数调用的秘密, 这个确实有点复杂, 想透彻的理解机器代码层面的函数调用不容易。

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

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

首先, 一个程序一条一条的指令都的老老实实的放在内存的一个地方,这个地方是Linux老大分配的, 我干涉不了, 但是这些指令都是我打电话给硬盘, 让他给运输到内存的。 然后Linux老大就会告诉我程序的入口点, 其实就是第一条指令的存放地址,  我就打电话问内存要这个指令, 取到指令以后就开始执行。这些指令当中无非有这么几类:1. 把数据从内存加载我的寄存器里什么? 你不知道啥是寄存器?  寄存器就是我内部的一个临时的数据存储空间了2. 对寄存器的数据进行运算, 例如把两个寄存器的数加起来3. 把我寄存器的数据再写到内存里但是我一旦遇到像这样的指令。 "把寄存器ebp的值压到栈里去“我就知道好戏要上场了, 函数调用就会开始。 我们这些x86体系的机器有个特点,就是每个函数调用都会创建一个所谓的“帧”哈哈, 不要被这些术语吓坏, 其实帧也就是我哥们内存中的一段连续的空间而已。像这样: 

汇编

汇编

现在这个指令来了:"把寄存器ebp的值压到栈里去“"把esp的值赋给ebp"

汇编

"把esp 的值减去24”

汇编

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

汇编

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

汇编

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

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

我猜程序员写的代码应该是这样:int x = 10;int y = 20;int sum= add(&x, &y); 接下来的指令是这样:“调用函数 add”我看到这样的函数就需要特别小心, 因为我必须要找到 add函数返回以后的那条指令的地址, 把它也压到栈里去。int x = 10;int y = 20;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寄存器”

汇编

"返回"我就会取出那个返回地址, 也就是 100, 去这里找指令接着执行其实就是这条语句: printf("the sum is %d\n",sum);问你一个问题, sum的值在那里保存着呢? 对, 是在eax寄存器里 !搞定了,看着很复杂, 其实看透了也挺简单吧。 函数调用,关键就是(1)把参数和返回地址准备好, (2)然后大家都遵循约定, 每次新函数都要建立新的函数帧:   "把寄存器ebp的值压到栈里去“    "把esp的值赋给ebp"(3) 函数调用完了, 重置 ebp 和esp ,让他们重新指向调用着的栈帧。好了,今天就到此为止 , 把我也累坏了,  主人又要关机了, 留一个问题吧: C语言编译,链接以后直接就是机器码, 那函数调用的操作都是上面讲的。 但是对于Python, Ruby 这样的解释型语言, 或者对于java 这样的有虚拟机的语言, 他们的函数调用是什么样的?  和上面讲的有什么关系?

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分