栈上的局部存储

文章讲述了在C代码中,当函数调用时局部变量无法全部存储在寄存器,需借助内存和栈帧进行数据传递的过程。通过实例分析了汇编代码中的栈帧设置和参数传递,强调理解原理的重要性,而不是死记硬背特定结构。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这个例子是在过程调用的时候对局部变量使用了“&”,并且寄存器不足够存放所有的本地数据,这时候局部数据必须放到内存中。笔记中函数比较简单,不过个人觉得这个例子比较有代表性。笔记中汇编代码的操作数是S(源)在前,D(目的)在后。最近挺喜欢Linux的,笔记也用的gcc汇编,返回地址是8字节。这个可以比较"玩具化"一点有助于我这种新手理解,毕竟现阶段我的目的不是去写,以后会用MASM,微软vs自带的那个。

一、为了方便,笔记中代码标注中的rsp代表栈顶,即栈指针%rsp,R[[%rsp]]这个地址值。为了简化理解,也假设call_proc的栈帧很简单,没有寄存器保存的部分等等。

二、例子的C代码部分

被调用过程proc代码如下

void proc(long a1, long *a1p,
		   int a2, int *a2p,
		   short a3, short *a3p,
		   char a4, char *a4p)
{
	*a1p += a1;
	*a2p += a2;
	*a3p += a3;
	*a4p += a4;
}

过程call_proc代码如下

long call_proc()
{
	long  x1 = 1; int  x2 = 2;
	short x3 = 3; char x4 = 4;
	proc(x1, &x1, x2, &x2, x3, &x3, x4, &x4);
	return (x1+x2)*(x3-x4);
}

因为在调用函数call_proc中调用了proc,而proc需要8个参数,寄存器不足以存储这么多参数,并且对局部变量x1,x2,x3,x4使用了&,因为必须为他们产生地址。这个时候就需要使用栈帧来处理这些问题。结合汇编代码来分析。

三、调用函数生成的汇编代码

#这段主要工作就是给proc设置参数	
call_proc:
	subq $32,%rsp     		#先分配32字节栈帧,栈向前了,旧rsp-新rsp=32
	movq $1, 24(%rsp)		#存储1&x1中,这个值占64位,即rsp+24指向的字节到rsp+31指向的字节这个空间
	movl $2, 20(%rsp)		#存储2&x2中,这个值占32位,即rsp+20指向的字节到rsp+23指向的字节这个空间
	movw $3, 18(%rsp)		#存储3&x3中,这个值占16位,即rsp+18指向的字节到rsp+19指向的字节这个空间
	movb $4, 17(%rsp)		#存储4&x4中,这个值占8位,即rsp+17指向的这个字节
	leaq 17(%rsp), %rax		#创建&x4,存入寄存器%rax中
	movq %rax, 8(%rsp)		#设置&x4作为参数8,这个值占64位它在rsp+8指向的字节到rsp+15指向的字节这个空间
							#这里注意一下现在栈帧中rsp+16指向的这个字节是空着的
	movl $4, (%rsp)			#设置4作为参数7,其实rsp+1指向的字节到rsp+7指向的字节这个7个字节属于空位
	leaq 18(%rsp), %r9		#设置&x3作为参数6
	movl $3, %r8d			#设置3作为参数5
	leaq 20(%rsp), %rcx		#设置&x2作为参数4
	movl $2, %edx			#设置2作为参数3
	leaq 24(%rsp), %rsi		#设置&x1作为参数2
	movl $1, %edi			#设置1作为参数1

分配的32字节空间大致意思如下,画了一个粗糙的图
每一个空白格子代表1个字节
调用函数call_proc的栈帧草图是这样的,下图中的返回地址不是call proc以后push进栈的那个返回地址,而是call_proc上一层的返回地址,画的不是特别好哈哈。当然也是假设没有寄存器保存的部分。
在这里插入图片描述
接着上面的call_proc的汇编代码

#下面就要开始调用proc了	
call_proc:
	call proc     				#调用proc后,返回地址会被push进栈,所以此时rsp会-8
#调用结束返回到call_proc,进行了rsp+8,此时rsp又回到了调用前的值		
	movslq 20%rsp), %rdx  	#读取到x2的值,并将其符号扩展到long
	addq   24%rsp), %rdx  	#这步是在计算 x1+x2
	movswl 18%rsp),%eax  	#读取x3,并进行符号扩展到int
	movsbl 17%rsp),%ecx  	#读取x4,并进行符号扩展到int
	subl   %ecx,     %eax  	#这步是计算x3-x4
	cltq            	    	#将%eax符号扩展%rax
	imulq  %rdx,	  %rax		#计算(x1+x2)*(x3-x4)
	addq   $32,		  %rsp		#清栈
	ret							#return
		

到此call_proc就结束了

四、扩展下proc被调用时程序计数器pc跳转到proc代码的部分

#call_proc为被调用的proc做的参数准备工作
#参数1%rdi中  (64)
#参数2%rsi中  (64)
#参数3%edx中  (32)
#参数4%rcx中  (64)
#参数5%r8w中  (16)
#参数6%r9中   (64) 由于寄存器放不开了,就要把参数7和参数8放入栈中
#参数7 在 R[%rsp]+8  (8)
#参数8 在 R[%rsp]+16 (64) 这里因为call之后返回地址被压入栈中,栈指针向前了8字节(64位)
#所以对于参数7和参数8的地址相对于之前都要加8
proc :
	movq 16%rsp), %rax	#先取得参数8
	addq %rdi,		(%rsi)	#进行*a1p += a1 对应的就是*(&x1) 和 x1的操作 
	addl %edx,		(%rcx)	#进行*a2p += a2 对应的就是*(&x2) 和 x2的操作
	addw %r8w,		(%r9)	#进行*a3p += a3 对应的就是*(&x3) 和 x3的操作
	movl 8(%rsp),	 %edx	#取得参数7
	addb %dl,		(%rax)	#进行*a4p += a4 对应的就是*(&x4) 和 x4的操作
	ret
#调用proc结束,通过call proc时候push进栈的那个返回地址程序计数器pc回到了call_proc代码中call proc后面那个指令,也就是说把此时栈顶的返回地址pop %rip

调用proc结束,可以看到proc不需要额外分配栈帧空间。利用call_proc通过call proc指令调用proc的时候push进栈的那个返回地址,程序计数器pc回到了call_proc代码中call proc后面那个指令,{ movslq 20(%rsp), %rdx #读取到x2的值,并将其符号扩展到long }这里,也就是说此时栈顶的返回地址已经被pop %rip了,当然rsp也随之进行了+8。

总结

当寄存器不足够存放所有的本地数据,这时局部数据要放到内存中,这样才能为被调用过程实现传递数据。有很多函数其实根本用不上栈帧,比如一些叶子程序当寄存器足够处理本地所有变量的时候,并且这个过程没有调用其他过程。学习计算机系统的时候个人感觉没必要去死记一些栈帧的结构,要理解搞清楚原理是最重要的。很多东西实现起来都是活的并不是一成不变的,理解了栈帧的结构自然就熟记在心。晚一些整理下x86寄存器的局部存储,主要关于那些被调用者保存寄存器。

<think>嗯,用户想了解C++的顺序存储结构的实现和原理。首先,我得回忆一下的基本概念,是后进先出(LIFO)的结构,顺序存储通常用数组来实现。那顺序存储结构具体是怎么实现的呢?可能需要定义一个结构体或类,包含一个数组和顶指针。根据提供的引用,比如引用[2]提到结构体中有top和data数组,引用[4]和[5]中的代码示例,应该用数组存储元素,top指示顶位置。 然后,我需要解释的操作,比如初始化、入、出、判断空等。比如初始化时,top可能设为-1或0,这取决于实现方式。入操作要先检查是否已满,否则top加一并存入数据。出则是检查是否为空,然后取出数据并调整top。引用[5]中的PushStack函数展示了如何将元素压入顶,并处理满的情况。 接下来,原理部分需要说明顺序存储的优缺点,比如连续内存带来的访问效率高,但容量固定可能导致溢出。动态扩容可能是一个优化点,但用户的问题可能更关注基础实现。引用[3]提到的STL的使用,但用户问的是顺序存储的实现,所以可能需要对比STL中的stack和手动实现的区别。 用户提供的引用中有多个代码示例,比如引用[4]的SeqStack类,使用数组和top指针,以及相关的成员函数。需要解释这些代码的结构,比如构造函数初始化top,isEmpty判断top是否为初始值,push和pop的操作逻辑。同时,要注意错误处理,比如满或空时的处理方式,比如返回错误码或抛出异常。 可能还需要比较顺序和链式的不同,但用户的问题集中在顺序存储,所以可以简要提到。另外,应用场景方面,如函数调用、表达式求值等,引用[2]和[3]提到的应用,可以结合这些例子说明原理的实际用途。 要确保回答结构清晰,分步骤解释实现步骤,并结合代码示例。同时,要检查引用的正确性,比如引用[1]虽然提到顺序存储结构,但主要讲的是抽象实现,可能和的具体实现相关度不高,而引用[2][4][5]更直接相关。需要正确引用这些资料来支持回答。 最后,生成的相关问题可能需要围绕的实现细节、操作的时间复杂度、优化方法、应用场景等方面展开,帮助用户进一步学习。</think>### C++的顺序存储结构实现及原理 是一种**后进先出(LIFO)**的线性数据结构,其顺序存储结构通过**数组**实现连续内存分配。以下是核心实现步骤与原理分析: --- #### 一、顺序存储结构定义 的顺序存储结构包含两个关键属性: 1. **数据存储数组**:用于存放元素,例如`int data[MAXSIZE]`。 2. **顶指针**:记录当前顶位置(通常用整型变量`top`表示)。 **代码示例**(引用[2][4]): ```cpp typedef struct { int top; // 顶指针 int data[MAXSIZE]; // 存储数据的数组 } SeqStack; ``` --- #### 二、核心操作实现 1. **初始化** 将顶指针初始化为`-1`(表示空): ```cpp void initStack(SeqStack &S) { S.top = -1; // 空状态 } ``` 2. **入(Push)** 将元素压入顶,需先检查是否已满: ```cpp bool Push(SeqStack &S, int value) { if (S.top >= MAXSIZE - 1) return false; // 满,操作失败[^5] S.data[++S.top] = value; // 顶指针先+1,再存入数据 return true; } ``` 3. **出(Pop)** 弹出顶元素,需先检查是否为空: ```cpp bool Pop(SeqStack &S, int &value) { if (S.top == -1) return false; // 空,操作失败[^5] value = S.data[S.top--]; // 取出元素后,顶指针-1 return true; } ``` 4. **获取顶元素(Peek)** ```cpp int getTop(SeqStack &S) { if (S.top == -1) throw "Empty Stack"; return S.data[S.top]; } ``` 5. **判断空** ```cpp bool isEmpty(SeqStack &S) { return (S.top == -1); } ``` --- #### 三、实现原理与特点 1. **内存连续性** 元素在内存中连续存储,支持$O(1)$时间复杂度访问顶元素[^1]。 2. **固定容量限制** 数组大小需预先定义(如`MAXSIZE`),可能因容量不足导致**溢出**(需动态扩容优化)[^4]。 3. **操作效率** - 入/出操作时间复杂度:$O(1)$(无需移动其他元素)。 - 空间复杂度:$O(n)$(n为的最大容量)。 --- #### 四、与STL的对比 STL的`std::stack`默认基于**双队列**实现,但可通过模板参数指定底层容器(如数组): ```cpp #include <stack> std::stack<int, std::vector<int>> arrStack; // 基于动态数组的[^3] ``` --- #### 五、典型应用场景 1. **函数调用**:保存函数返回地址和局部变量[^2]。 2. **表达式求值**:处理括号匹配、运算符优先级(如逆波兰表达式)。 3. **撤销操作**:通过记录操作历史(如文本编辑器)[^3]。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值