关键词:
c语言过程栈机制详解
先声明,下面的图来自于B站up主——九曲阑干 的视频课程,讲计算机组成原理十分简练通透!想学CSAPP的小伙伴可以看书+看他的视频!本文来自对视频内容的整理加上一些自己个人的逻辑和理解。
过程栈在c/c++中起着很重要的角色,为了更深入地理解一个c/c++程序是如何运作的、程序core时我们该怎么查找问题,弄清楚过程栈机制都是很有必要的!
一、 汇编语言基础
1. c程序从源文件到执行的过程
图1 c/c++程序从源文件都可执行文件的过程
记得在很久以前的笔记放过一张这个图,相信每一个c/c++程序员起码都对这张图了如指掌。今天我们讨论的话题是这个过程的第一个中间产物:汇编代码,编译器的输出结果。
2. 看懂基本的汇编语言
一条基本的汇编语言有两部分组成:操作码和操作数;而操作数又分为3种:立即数、寄存器、内存引用。
下面是一条基本的汇编指令:
mov %rdi,%rax
-
操作码
操作码代表指令的类型,常见的有:mov、add、sub、xor、push、pop、ret。。。
有些需要两个操作数,而有些只需要一个。
在实际汇编代码中经常看到操作码后面加上q、l后缀,比如
movq
、movl
等。这些后缀代表待操作的操作数长度:b(byte),操作数占1B;w(word),操作数占2B;l(long),操作数占4B;q,操作数占8B。 -
操作数
操作数,即指令操作的数据,分为3种:立即数,寄存器,内存引用。
立即数,比如:
$1
。寄存器,比如:
%rdi
。下面详细介绍64位cpu的寄存器组织。内存引用,比如:
(%rdi)
。我理解 类似于c语言中的指针,括号里的寄存器存放一个内存地址,去内存中查找相应地址的内容。 -
寄存器
寄存器是汇编语言中存储变量、完成逻辑最常用的高速存储器了。不同cpu设计规范有着不同的寄存器组织。常见的64位cpu——x86-64处理器设计规范中有16个通用寄存器。其中8个在实现过程栈逻辑中常用的,了解以下这些暂时够用啦:
图2 cpu向汇编语言提供的寄存器组织(64位)
由上图,这些寄存器的低32位都是32位cpu的遗留,而如果想只使用高32位,需要这样表达,如:%rsi
的高32位是%esx
;%rdi
的高32位是%edx
。
二、 从汇编角度分析过程栈机制
1. 返回地址的压入与弹出(call、ret指令)
几乎所有高级语言都会有 函数 的概念,通过函数对可以复用的逻辑进行封装,可以使得程序书写更加简明规范。几乎任何程序员都知道函数调用的逻辑:当前函数调用另一函数时,当前函数将被“挂起”,转而执行被调用函数;待执行完毕后,会返回到刚刚执行的函数语句位置继续向下执行。 但这个逻辑在语言底层是怎么实现的呢?这是我们要搞懂的问题。下面以c/c++语言为例介绍一下经过编译后的汇编语言是如何实现过程调用逻辑的:
int func(int a,int b,int* c)
*c=a+b;
return *c;
int main()
int aa=1;
int bb=2;
int* pc;
int cc=func(aa,bb,pc);
写一个简单的调用,看看汇编之后的是什么样:
_Z4funciiPi:
pushq %rbp
省略一些...
popq %rbp
(执行完func函数后,ret指令即可跳转回原函数)
ret
main:
省略一些...
(这里call,即跳转到func函数)
call _Z4funciiPi
省略一些...
ret
可见,上述函数调用的逻辑是用call和ret指令实现的。下面分别剖析一下call和ret指令的具体作用:
-
call指令:
- 把被调用函数第一条指令的地址放入程序指令寄存器
%rip
中,之后下一条指令将会按照%rip
中地址执行。 - 把 返回地址 压入栈中。即,执行完被调用函数后接下来一条指令的地址,也就是原函数的下一条指令地址。
- 把被调用函数第一条指令的地址放入程序指令寄存器
-
ret指令:
- 将返回地址放入程序指令寄存器
%rip
中,之后下一条指令将会按照%rip
中地址执行。 - %rsp上移8B,将返回地址弹出。
如此就可以借助栈空间来完成函数的调用逻辑了!
- 将返回地址放入程序指令寄存器
2. 函数参数和自动变量的存储与传递
2.1函数参数和自动变量的存储
为了实现函数特定的功能,往往需要传入传出一些参数。原则上说,如果寄存器足够那么就不需要借助栈资源来存放传入参数或者自动变量,别忘了栈空间实际上是内存,所以寄存器的读写速度一定是比栈更快的。下面来看看实际情况:
-
向函数传入一个int:
movl %edi, -4(%rsp)
可见在没有更高级别优化的前提下,向函数内传入一个参数也会占用栈资源。(跟用不用没关系)
-
函数内声明一个自动变量并使用它:
int func() int a; return a;//使用(返回)了为初始化的变量
相关的汇编如下:
movl -4(%rsp), %eax popq %rbp
pop指令还没介绍,暂时可以这样理解上面的汇编:把
-4(%rsp)
(当前栈指针低4B)存放的内容赋给%eax
。pop会将%eax
中的内容保存到当前栈帧外的寄存器%rbp
中,作为返回值供上一级函数使用。可见,在没有更高级别优化的前提下,声明一个自动变量也会占用栈空间。当然了,这种写法犯了一个低级错误,返回的内容将是随机的,通过汇编代码更能理解为什么是这样了。因为我们根本没有清理-4(%rsp)
位置的内容。而且没有任何机制可以自动清理栈的内容(将其置初始值什么的。。),都要依赖下次使用时自己手动初始化。可见,无论是函数参数还是自动变量,没有高级别优化的前提下都会占用栈空间。
2.2 函数参数的传递
我们看看函数参数是怎么传递的?方法也很简单,”栈是死的,寄存器是活的“——尽管栈有严格的机制保护其空间的使用,传递参数貌似很麻烦;但寄存器比栈的机制更为灵活,不受这种规则限制。因此,不论是参数的传入还是返回,都会依靠**”惯例寄存器“**先将其保存,然后再放到栈里使用。
下面是按照使用惯例常用寄存器的用途:
图3 部分惯例寄存器的用途
可见在使用惯例中,%rdi
、%rsi
、%rdx
、%rcx
分别用来存放传入函数的第1、2、3、4个参数。与传入类似,%rax
用于返回栈内的变量。进入或返回目标函数后,接下来要做的事就是到这些惯例寄存器中取得内容,将其放到栈内,或者做其他事情即可!而%rbp
和%rbx
是被调用保存的寄存器,弹栈压栈操作可以借助这两个寄存器操作。
看到这里你可能会提出疑问,如果传入的参数个数不止4个怎么办?或者传入/返回的变量是一个巨大类型的拷贝,8B寄存器放不下怎么办?
对于传入参数过多的函数,就得不能依靠寄存器来作为函数之间的暂存了,只能使用栈空间来传递了。对于传入的变量过大且需要拷贝的情况,先依靠寄存器保存其地址,再将地址中的内容拷贝到栈里;而返回值就比较特殊了,还记得c++中的RVO优化吗?尽管看起来像是触发了返回值拷贝,但其实并没有真的拷贝,效率提高了不少。
3. 寄存器值的保存和恢复(push、pop指令)
了解参数是怎么在栈之间传递的,接下来就可以看看这种传递是靠什么实现的:
-
push指令:
push?顾名思义,就是把元素压栈嘛!push指令也是这样:
int func(int a) //...
考虑最简单的情况,相关的汇编代码如下,
pushq %rbp
pushq指令做了2件事:
- 将栈指针
%rsp
向低地址移动8B(因为是q嘛!) - 将寄存器
%rbp
的内容存入%rsp
指向的栈空间中。
也就是说,上面一条pushq指令,相当于下面2条:
subq $8,%rsp movq %rbp,(%rsp)
为什么是往低地址呢?回忆一下操作系统的知识:虚拟地址空间的分布,执行过程中,栈是从高地址向低地址生长的嘛。
- 将栈指针
-
pop指令:
同样,就是把元素从栈中弹出:
int func(int a) //... return aa;
返回 相关的汇编代码如下,
popq %rbx ret
与pushq对应,popq指令做了2件事:
- 将栈指针
%rbp
指向的内容移到%rbx
寄存器中,供外层函数使用。 - 将栈指针
%rsp
向高地址移动8B(因为是q嘛!)
一条popq指令,相当于下面2条:
movq (%rsp),%rbx addq $8,%rsp
pop之后就可以执行ret指令返回了。可见,在退栈时,真的没有任何机制把栈空间的内容复原的操作。因此,初始化内存的任务交给了下一次的栈调用。
- 将栈指针
c语言函数调用完整过程(代码片段)
C语言函数调用详细过程函数调用是步骤如下:按照调用约定传参调用约定是调用方(Caller)和被调方(Callee)之间按相关标准对函数的某些行为做出是商议,其中包括下面内容:传参顺序:是从左往右传还是从右往左传参方式:是用寄... 查看详情
函数栈帧详解(代码片段)
...下就是编译过程的最终产品是可执行程序——由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机的内存中,因此每条指令都有特定的内存地址。在生成可执行程序时程序已经变成了供机器识别的... 查看详情
函数栈帧详解(代码片段)
...下就是编译过程的最终产品是可执行程序——由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机的内存中,因此每条指令都有特定的内存地址。在生成可执行程序时程序已经变成了供机器识别的... 查看详情
面试常见之jvm垃圾回收机制gc详解(代码片段)
作为Java语言最重要的特性之一的自动垃圾回收机制,也是基于JVM实现的。那么,自动垃圾回收机制到底是如何实现的呢?1.GC是干啥的?进行资源的回收1.1.对于C/C++而言对于C/C++语言是没有GC机制的... 查看详情
c语言链栈及基本操作(包含入栈和出栈)保姆级详解(代码片段)
...链表中,每个数据元素的添加过程如下图所示:C语言实现代码为://链表中的节点结构typedefstructlineStack intdata; structlineStack*next; lineStack; //stac 查看详情
c语言链栈及基本操作(包含入栈和出栈)保姆级详解(代码片段)
...链表中,每个数据元素的添加过程如下图所示:C语言实现代码为://链表中的节点结构typedefstructlineStack intdata; structlineStack*next; lineStack; //stac 查看详情
预处理过程详解(代码片段)
前言上一篇文章说到,这将是博主更新的最后一篇关于C语言知识点的博客,也确实,因为这篇文章是讲解的c的预处理,也是c语言最后的一部分知识了,还是老话,博主的所有文章几乎篇幅都比较长,大家可以根据目录进行选择性观看,同... 查看详情
新手向c语言实现特殊的数据结构——栈(代码片段)
目录一、易错接口详解1.1栈的初始化1.2栈的销毁1.3入栈1.4出栈二、简单接口的实现2.1有效数据个数2.2返回栈顶数据三、头文件的引用,结构体和函数的定义一、易错接口详解1.1栈的初始化涉及到结构体的定义,请先跳转... 查看详情
c语言网络编程—内核协议栈收包/发包流程(代码片段)
目录文章目录目录关键技术DMAsk_buff结构体NetdriverRx/TxRingBufferBufferDescriptorTableNAPI收包机制网卡多队列内核协议栈收包/发包流程概览内核协议栈收包流程详解驱动程序层(数据链路层)VLAN协议族LinuxBridge子系统网络协议层... 查看详情
c语言函数递归(详解)(代码片段)
文章目录函数递归什么是递归?递归的俩个必要条件代码引例1栈溢出(StackOverflow)合理使用递归代码引例3代码引例4解释要合理使用递归结束语函数递归程序调用自身的编程技巧称为递归recursion)函数自己调用自... 查看详情
hadoop详解——hdfs的命令,执行过程,java接口,原理详解。rpc机制(代码片段)
HDFS是Hadoop的一大核心,关于HDFS需要掌握的有:分布式系统与HDFS、HDFS的体系架构和基本概念、HDFS的shell操作、Java接口以及常用的API、Hadoop的RPC机制、远程debugDistributed FileSystem数据量越来越多,在一个操作系统管理的... 查看详情
c语言sizeof与strlen详解(附大量笔试题题解过程)(代码片段)
前言:相信很多初学者都对被sizeof和strlen搞得晕晕的,相信看完这篇文章,你对这二者的认识将提升一个档次!已经了解sizeof和strlen的大佬们就当复习吧,哈哈哈~目录一.sizeof()详解二.strlen()详解strlen()的模拟... 查看详情
c语言之函数调用及栈帧分析(代码片段)
一.前言 每一次函数调用都是一个过程。这个过程我们通常称之为:函数的调用过程。这个过程要为函数开辟栈空间,用于本次函数的调用中临时变量的保存,现场保护。这块栈空间就是函数栈帧。实验代码:#inc... 查看详情
十图详解tensorflow数据读取机制(附代码)(代码片段)
...习材料。今天这篇文章就以图片的形式,用最简单的语言,为大家详细解释一下TensorFlow的数据读取机制,文章的最后还会给出实战代码以供参考。TensorFlow读取机制图解首先需要思考的一个问题是& 查看详情
tcpip协议栈的心跳丢包重传连接超时机制实例详解(代码片段)
目录1、问题概述2、TCPIP协议栈的心跳机制2.1、TCP中的ACK机制2.2、TCPIP协议栈的心跳机制说明2.3、修改TCPIP协议栈的默认心跳参数3、libwebsockets开源库中的心跳机制使用的就是TCPIP协议栈的心跳机制4、TCPIP丢包重传机制5、使用非阻... 查看详情
c语言的函数栈帧究竟是什么?你知道吗?(代码片段)
内容导读1.寄存器2.函数栈帧2.1函数栈帧的概述2.2函数栈帧创建过程2.2.1被调用的main函数2.2.2函数栈帧创建与销毁的过程前面的话:作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!博主的... 查看详情
4-6:tcp协议之连接管理机制(三次握手四次挥手详解)(代码片段)
文章目录一:TCP三次握手过程和状态变迁(1)三次握手过程和状态变迁过程详解(2)为什么必须要三次握手?A:只有三次握手才可以阻止重复历史连接的初始化(主要原因)B:同步双方初... 查看详情
《c语言杂记》详解extern“c“(代码片段)
在嵌入式开发过程中,你是否经常看到类似下面的代码。#ifdef__cplusplusextern"C"#endif……#ifdef__cplusplus#endif下面我们就来深入剖析。很明显#ifdef/#endif、#ifndef/#endif用于条件编译,#ifdef_cplusplus/#endif_cplusplus——表示如果... 查看详情