zoukankan      html  css  js  c++  java
  • CPU阿甘:函数调用的秘密

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

    本文源地址: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。他会随着指令的运行,指向函数帧的最后的地址,像这样:现在这个指令来了:

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

    “把esp的值赋给ebp”
    你看看,是不是新的函数帧生成了?只不过现在只有一行数据。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嘛)

    这又是在干嘛?

    这其实就相当于把 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
    ",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
    ",sum);

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

    函数调用,关键就是:

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

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

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

    “把esp的值赋给ebp”

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

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

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

  • 相关阅读:
    简单算法之插入排序(二)
    简单算法之选择排序(一)
    使用iptables为docker容器动态添加端口映射
    CentOS7出现Unit iptables.service could not be found
    linux系统下使用xampp 丢失mysql root密码【xampp的初始密码为空】
    centos6 安装docker
    Elasticsearch安装配置问题
    Elasticsearch技术解析与实战--shard&replica机制
    Elasticsearch聚合问题
    elasticsearch-head的使用
  • 原文地址:https://www.cnblogs.com/tgycoder/p/6066456.html
Copyright © 2011-2022 走看看