美文网首页
图解Linux是如何进行函数调用的?

图解Linux是如何进行函数调用的?

作者: this_is_for_u | 来源:发表于2021-02-15 09:56 被阅读0次

开篇依旧先提出几个问题:

  1. 进程虚拟地址空间是如何分布的?

  2. 函数调用的栈帧结构是什么样子?

  3. 函数调用涉及到的寄存器都起了什么作用?

  4. 函数参数是如何传递的?传递顺序如何?

  5. 函数的返回值是如何传递的?

如果您对上述问题有些困惑,请继续往下看吧!

进程的内存布局

如图:

image.png

高地址的一部分空间会分配给内核,称为内核空间,剩下的内存空间给用户使用,称为用户空间。

用户空间中有几个主要的内存区域:

  • 栈:用于维护函数调用的上下文,离开了栈,函数调用就没法实现,栈通常在用户空间的最高地址处分配,通常有数兆字节的大小。

  • 堆:堆用来容纳程序动态分配的内存区域,程序中malloc或new分配的内存就来自堆里。堆通常存在于栈的下方(低地址方向),在某些时候,堆也可能没有固定统一的存储区域,堆一般比栈大很多,可以有百兆甚至几G的大小。

  • 动态链接库映射区:这个区域用于映射装载的动态链接库,Linux下如果可执行文件依赖其它共享库,那系统就会在这个区域分配相应空间,并将共享库装入该空间。

  • 可执行文件映像:存储着可执行文件在内存里的映像,由装载器在装载时将可执行文件的内存读取或映射到这里。

  • 保留区:保留区并不是一个单一的内存区域,而是堆内存中受到保护而禁止访问的内存区域的总称,例如在大多数操作系统里,极小的地址通常都是不允许访问的,如NULL,通常C语言将无效地址赋值为0也是出于这个考虑,因为0地址正常情况下不可能有有效的可访问数据。

函数调用的栈帧结构

我们都知道函数调用都是以栈帧为单位,机器通常用栈来传递函数参数、保存返回地址、保存寄存器(即函数调用的上下文)及存储本地局部变量等。

一个单独的栈帧结构如图所示:

image.png

为单个函数调用分配的那部分栈称为栈帧,栈帧的边界由两个指针界定:寄存器%ebp为帧指针,指向当前栈帧的起始处,通常较为固定;寄存器%esp为栈指针,指向当前栈帧的栈顶位置,当程序执行时,栈指针可以移动,因此大多数数据的访问都是相对于帧指针的。

一次函数调用的栈帧图如下:


image.png

寄存器使用约定

直接看图:

image.png

图片来源于网络,侵权删

上图表达的应该已经很清楚啦,简单示例解释一下,函数调用需要传递参数时,第一个参数存到%edi里,第二个参数会存到%esi里,如果有返回值会存到%eax里,这里如果是64位的返回值,会使用%rax。

函数的调用约定

这里主要涉及三种约定:

  • 函数参数的传递顺序和方式:这里可以有很多参数传递方式,栈传递和寄存器传递,函数的调用方将参数压入栈中,函数自己再从栈中将参数取出,需要规定压栈的顺序,是从左到右,还是从右到左,有的也使用寄存器传递,这都需要约定好。

  • 栈的维护方式:在函数将参数压栈后,函数体会被调用,此后需要将被压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致,这个弹出的工作可以是由函数的调用方完成还是函数本身来完成需要约定好。

  • 名字修饰策略:为了链接的时候对调用约定进行区分,需要对函数本身的名字进行修饰,不同的调用约定有不同的名字修饰策略。一般都是前面加个下划线。

C语言默认的调用约定是cdecl方式,可以通过attribute___((cdecl))标明使用cdecl约定,其实还有其它一些调用约定,如图:


image.png

函数的返回值传递

这里有几种情况:

4字节:当函数返回值是4个字节会通过%eax寄存器作为通道,函数将返回值存储在%eax中,返回后函数的调用方再读取%eax。

5-8个字节:通过rax寄存器作为通道。

大于8个字节:以如下代码举例:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="c++" cid="n58" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">struct A {
// ...大于8字节
};
A func() {
A b;
return b;
}
A x = func();</pre>

返回值传递方式如图:


image.png
  1. 调用函数首先在栈上额外开辟一片空间,作为临时对象(temp)

  2. temp作为隐藏参数传递给被调用函数

  3. 函数将数据拷贝给temp,同时%eax为指向temp的指针

  4. 返回返回后将%eax指向的temp拷贝回被赋予的对象

返回值类型的尺寸太大导致函数返回时,会开辟一段区域作为中介,返回值对象会被拷贝两次,而C++在有些情况下会做返回值优化,减少拷贝的次数,具体可以看我之前的文章:

参考资料

https://blog.csdn.net/slvher/article/details/8831885

https://blog.csdn.net/slvher/article/details/8831983

https://www.cnblogs.com/alantu2018/p/8465904.html

https://mp.weixin.qq.com/s/fpf4qRRLN3wVDUrWka3HfQ

https://mp.weixin.qq.com/s/j7SKtrMCmYs6g8yH75OH4A

https://www.sec4.fun/2018/05/29/stack/

https://murphypei.github.io/blog/2019/01/linux-heap

https://cloud.tencent.com/developer/article/1515763

《程序员的自我修养:链接装载与库》

相关文章

  • 图解Linux是如何进行函数调用的?

    开篇依旧先提出几个问题: 进程虚拟地址空间是如何分布的? 函数调用的栈帧结构是什么样子? 函数调用涉及到的寄存器都...

  • NIO 看破也说破(一)- Linux/IO 基础

    Tips:Linux底层通过文件的方式实现IOJava等高级语言是通过syscall对Linux系统函数进行调用来...

  • Linux中的sleep、usleep、nanosleep、po

    在进行Linux C/C++编程时,可调用的sleep函数有好多个,那么究竟应当调用哪一个了?下表列出了这几个函数...

  • 递归为什么会导致栈溢出

    下面来看一下,函数是如何调用的,在函数A里调用函数B,我们称A为调用者函数,B是被调用函数。每一次函数的调用,都会...

  • 跨系统调用总结

    windows调用linux 使用putty的plink进行 linux调用windows 使用telnet+ps...

  • 数据结构(五)--递归

    数据结构(五)--递归 首先思考一个问题,当A函数调用B函数,A函数是如何调用B函数的? 在一个函数调用期间,调用...

  • python 杂记

    进程 fork()函数Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用...

  • QT调用外部程序

    1、通过调用Linux C 函数 system("calc.exe"); 2、通过QProcess 阻塞调用 ...

  • 递归调用

    什么是递归调用 递归调用就是在本函数中连续不断地对自身函数进行调用。 递归调用注意点 递归调用函数要有明确的某一或...

  • Linux(C/C++)下的文件操作open、fopen与fre

    Linux(C/C++)下的文件操作open、fopen与freopen open是linux下的底层系统调用函数...

网友评论

      本文标题:图解Linux是如何进行函数调用的?

      本文链接:https://www.haomeiwen.com/subject/mcxuxltx.html