大白话函数调用过程

大白话函数的调用过程

写了很多年代码了,却说不上来当我调用一次函数,是怎么样一个过程,今天研究一下。
先看几个概念:

寄存器:

先来看寄存器。CPU 本身只负责运算,不负责储存数据。数据一般都储存在内存之中,CPU 要用的时候就去内存读写数据。CPU 还自带了寄存器(register),用来储存最常用的数据。也就是说,那些最频繁读写的数据(比如循环变量),都会放在寄存器里面,CPU 优先读写寄存器,再由寄存器跟内存交换数据。

先对几个常用寄存器有点印象,具体的去百度这里不展开。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uO0cCLct-1606811000766)(/uploads/sqwfw/images/m_d48f3f8a7537ae6ebc847faba0587eaf_r.png)]

栈 stack

栈读我另外一篇文章
https://blog.csdn.net/yanchendage/article/details/110201400

栈帧

函数调用经常是嵌套的,a调用b,b有调用c,在同一时刻,堆栈中会有多个函数的信息。每个未完成运行的函数占用一个独立的连续区域,称作栈帧(Stack Frame)。栈帧是堆栈的逻辑片段,当调用函数时逻辑栈帧被压入堆栈, 当函数返回时逻辑栈帧被从堆栈中弹出。栈帧存放着函数参数,局部变量及恢复前一栈帧所需要的数据等。

这点注意点,我们平时总说调用函数会开辟栈,其实严格说是栈帧

步入正题

写一段代码
int add_a_and_b(int a, int b) {
   return a + b;
}

int main() {
   return add_a_and_b(2, 3);
}
翻译成汇编语言是
_add_a_and_b:
   push   %ebx
   mov    %eax, [%esp+8] 
   mov    %ebx, [%esp+12]
   add    %eax, %ebx 
   pop    %ebx 
   ret  

_main:
   push   3
   push   2
   call   _add_a_and_b 
   add    %esp, 8
   ret
什么是汇编语言

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pEJMf8cq-1606811000779)(/uploads/sqwfw/images/m_05aa2c1723739f29e6bf3cd068c0907b_r.png)]

通俗点我们写的c、java、go、php乱七八糟的都是高级语言,但是这东西计算机根本就不认识,最后需要把你写的代码翻译成计算机认识的低级语言他才能执行,这个低级语言就是汇编语言。

谁来翻译的?编译器

简单讲,编译器就是将“一种语言(通常为高级语言)”翻译为“另一种语言(通常为低级语言)”的程序

下面开始一步一步的分析!

你看到上面翻译的这堆乱七八糟的东西了吧,什么push,mov,add,pop这堆,这叫cpu指令。你写的程序就是按照这一个一个指令来执行的,你仔细看看那个 add %eax, %ebx,猜一猜好像是在给cpu下a+b的命令,是不是呢?咱一步一步来。

首先代码在哪里执行的?

操作系统为了执行这段代码开辟了一个进程,每个进程都有4g的虚拟地址空间,每个进程的 4G 地址空间中,最高 1G 都是一样的,即内核空间。剩余的 3G 是自己可以用的用户空间,这里可以参考我的另外一篇文章.
https://blog.csdn.net/yanchendage/article/details/110393617
总之为什么用虚拟地址空间是因为:

  • 让每个进程拥有了相同的、独立内存空间,相互之间不会干扰
  • 读写内存更安全。由于系统和MMU的限制,使得进程无法操作到其他进程的数据。
  • 不连续的物理空间可以映射成连续的虚拟地址空间
  • 进程分配的内存空间只有在实际使用时,才会触发缺页异常来分配实际物理空间,从而最大程度减少了内存空间的浪费。

如下图可以看出分成了上下两部分。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0PJQwpgc-1606811000786)(/uploads/sqwfw/images/m_9448df1025da0b5a17582ebc1c0c5065_r.png)]

Linux 对下面3g用户空间进程地址空间有个标准布局,地址空间中由各个不同的内存段组成 (Memory Segment),主要的内存段如下:

  • 程序段 (Text Segment):可执行文件代码的内存映射

  • 数据段 (Data Segment):可执行文件的已初始化全局变量的内存映射

  • BSS段 (BSS Segment):未初始化的全局变量或者静态变量(用零页初始化)

  • 堆区 (Heap) : 存储动态内存分配,匿名的内存映射

  • 栈区 (Stack) : 进程用户空间栈,由编译器自动分配释放,存放函数的参数值、局部变量的值等

  • 映射段(Memory Mapping Segment):任何内存映射文件

    看到了吧,有一个醒目的 ***程序段 (Text Segment)***,代码就放在这里。

开始执行main函数

首先我们从main函数看起,这是程序的第一个函数对吧,我们从各种文章中都能到过一句话。调用函数会开辟栈帧。现在在栈区 (Stack) 为main函数开辟了一段空间。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AIKMGJD5-1606811000790)(/uploads/sqwfw/images/m_93e17e0e4c446cca835958e123094045_r.png)]

为什么栈底在上面,栈顶在下面,是因为栈在内存空间中是从上到下的,不懂的去看看我写的栈。

不管你干什么你申请空间肯定是要告诉别人你要多少对吧,现实里你跟人家租房子你肯定也是跟中介说我要一个20平米的房子,栈帧也一样,栈帧用的是EBP、和ESP这两个东西表示空间大小的。
ESP(Stack Pointer)是堆栈指针寄存器,存放执行函数对应栈帧的栈顶地址(也是系统栈的顶部),且始终指向栈顶;EBP(Base Pointer)是栈帧基址指针寄存器,存放执行函数对应栈帧的栈底地址。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7kGznURB-1606811000792)(/uploads/sqwfw/images/m_4092c329841143acca016d0822d058a8_r.png)]

他也是有单位来限制他的,栈帧的边界由栈帧基地址指针EBP和堆栈指针ESP界定(指针存放在相应寄存器中)。EBP指向当前栈帧底部(高地址),在当前栈帧内位置固定;ESP指向当前栈帧顶部(低地址),当程序执行时ESP会随着数据的入栈和出栈而移动。因此函数中对大部分数据的访问都基于EBP进行。
为更具描述性,以下称EBP为帧基指针, ESP为栈顶指针,并在引用汇编代码时分别记为%ebp和%esp。

栈帧里都有什么东西呢?

总结一句话栈帧存放着函数参数,局部变量及恢复前一栈帧所需要的数据等。

调用add_a_and_b(2, 3)函数

main函数内部调用了add_a_and_b函数。执行到这一行的时候,操作系统也会为add_a_and_b函数在**栈区 (Stack)**新建一个栈帧,也就是说,此时同时存在两个帧:main和add_a_and_b。一般来说,调用栈有多少层,就有多少帧。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qr0rnFoP-1606811000795)(/uploads/sqwfw/images/m_a26f3c56cbb3bbb627ec9ec9b86f28c0_r.png)]

看到这里是不是稍微有点感觉了?烧脑的来了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vwWmvc2c-1606811000798)(/uploads/sqwfw/images/m_e4e817879f277f107b849507b9bfe4f8_r.png)]

卧槽这幅图什么玩意?咱一步一步来。

首先每一块都是什么

这幅图分成四个部分pre stack frame(上一个栈帧),红色部分(main函数的栈帧),蓝色部分(add_a_and_b函数的栈帧),next stack frame(下一这个栈帧)。

接着每一块里都有什么

lcocal variables ,argument,return address都代表什么呢?上面说的,函数参数,局部变量及返回地址。函数参数和局部变量好理解,返回地址是什么?下一条待执行指令的地址。比如说当你代码执行到了add_a_and_b(2, 3),底层编译器会翻译成汇编语言, “call _add_a_and_b” 这条指令,下一条指令就是“add %esp, 8”,也不难理解,当main调用add_a_and_b执行完毕后,main要知道执行到哪条指令接着执行吧。

_add_a_and_b:
   push   %ebx
   mov    %eax, [%esp+8] 
   mov    %ebx, [%esp+12]
   add    %eax, %ebx 
   pop    %ebx 
   ret  

_main:
   push   3
   push   2
  call   _add_a_and_b  //开辟栈帧,记录下一条指令的地址
   add    %esp, 8
   ret
执行main函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aqamxUky-1606811000799)(/uploads/sqwfw/images/m_46afdb672acd66f4c7a54e3f6bfc6bc0_r.png)]

看图中粉红色部分,这部分对应的是main主调函数,从上往下看

  1. EBP:先有一个栈基,这段栈帧的开始

  2. caller-saved registers(主调函数保存寄存器)
    %eax(累加(Accumulator)寄存器)
    %edx(数据(Data)寄存器)
    %ecx(计数器(Counter)寄存器)
    为主调函数保存寄存器,当函数调用时,若主调函数希望保持这些寄存器的值,则必须在调用前显式地将其保存在栈中,被调函数可以覆盖这些寄存器,而不会破坏主调函数所需的数据。也就是a调用b之前先把几个寄存器的值保存起来以防b更改,这就是我们常说的保存函数调用的上下文。

  3. Local Variables:局部变量入栈

  4. 将参数按照调用约定依次入栈,可以看到先是arg2,后是arg1,为什么不是先arg1入栈再arg2入栈我也没明白。查百科是因为函数调用约定。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ybfYRwgI-1606811000802)(/uploads/sqwfw/images/m_ccea5b4cc8f495732a1d04889b2c0264_r.png)]

  5. return address,然后将指令指针EIP入栈以保存主调函数的返回地址(EIP 指令指针(Instruction Pointer)寄存器:总是指向下一条指令的地址; 所有已执行的指令都被它指向过),这个就是前面说过的返回地址吧。

main函数调用add_a_and_b函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NEyU6wbc-1606811000803)(/uploads/sqwfw/images/m_889990fe50fc76b15f9dcea5c35678c4_r.png)]

看图中蓝色部分,这部分对应的是add_a_and_b被调函数,从上往下看

  1. EBP:当前EBP寄存器的值还是main的栈基,ESP寄存器的值还是main的栈顶,为了保存mainEBP的值,把EBP寄存器的值取出来并入栈,那add_a_and_b的EBP是什么呢?因为main的结尾是add_a_and_b的开始,所以把ESP寄存器的值赋值给EBP,那add_a_and_b的ESP是什么呢?ESP的值会变化,始终指向当前函数栈帧的栈顶。这样以add_a_and_b的EBP基准,向上(栈底方向)可获取主调函数的返回地址,向下(栈顶方向)能获取被调函数的局部变量值。
  2. callee-saved registers(被调函数保存寄存器)
    %ebx (基址(Base)寄存器)
    %esi(来源索引(Source Index)寄存器)
    %edi(目的索引(Destination Index)寄存器)
    为被调函数保存寄存器,被调函数在覆盖这些寄存器的值时,必须先将寄存器原值压入栈中保存起来,并在函数返回前从栈中恢复其原值,因为主调函数可能也在使用这些寄存器。
  3. Local Variables:局部变量入栈
add_a_and_b函数调用结束并返回
  1. 当前的EBP寄存器的值为add_a_and_b栈基,ESP寄存器的值为add_a_and_b栈顶,当调用完成时我们都听过一句话函数执行完毕销毁栈空间,那么是怎么实现的呢?将EBP寄存器的值赋给ESP寄存器,使ESP再次指向被调函数栈底以释放局部变量,看没看见?有点收缩意思,再将已压栈的主调函数帧基指针弹出到EBP寄存器,此时EBP寄存器又变成了main的栈基,ESP又变成了main的栈顶。
  2. 最后将main栈帧中保存的return address出栈,也就是返回将EIP的值改为下一条应该执行的指令。在main栈帧中调用add_a_and_b入栈的参数用完了也应该销毁,所以ESP继续上移越过参数,最终回到函数调用前的状态。

这就是一个函数完整的调用过程!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值