汇编指令push,mov,call,pop,leave,ret建立与释放栈的过程

栈内的数据

栈在汇编层面是辅助实现函数调用的,每个函数调用过程在栈中被抽象成一帧 ,在老式的32位CPU架构IA32中还有固定寄存器指向当前帧底部(下图中的0x100000f4,0x100000d8)。每帧内保存的数据为当前函数的局部变量,当前函数内调用其他函数的参数,寄存器状态(调用者与被调用者依据约定分别负责保存与恢复部分寄存器),上一帧底部地址(x86-64取消),当前函数返回后的指令地址,各组别数据的长度不定顺序固定。
在这里插入图片描述

高级语言中的栈结构有数据后进先出的性质,永远都是操作栈最顶层的数据。在汇编指令层面有一个**%rsp寄存器总是保存着栈顶的内存地址**,以此抽象出一个栈指针指向栈顶数据,利用汇编指令push与pop完成数据的入栈与出栈,push与pop负责将%rsp保存的内存地址中的数据移到目标地址,并对%rsp内的地址进行加减操作。

push 源数据
//push指令可以分解为两条更基本的汇编指令:
//--sub $指针移动长度 %rsp		//栈指下移
//--mov 源数据 (%rsp)	//推入数据至栈顶

pop 目标地址
//pop指令等同于:
//--mov (%rsp)目标地址		//数据出栈
//--add $指针移动长度 %rsp    //指针上移

在这里插入图片描述
对栈内数据的访问依靠指针的相对位置,如根据帧指针的相对位置寻找当前函数参数(IA32),根据栈指针的相对位置获取当前局部变量。
与高级语言只可访问栈内数据不同,编译器可读写栈外数据,例如在栈外(指针下方)的位置先写入数据后再移动栈指针,在x86-64指令结构中,甚至还可对相对于栈指针地址低128位的内存空间进行读写。相对于编程语言中的栈,汇编层面限制较差,不是那么纯粹。

栈空间的分配与回收

调用一个函数不一定100%会对栈进行分配,出于速度与性能cpu会优先考虑只用寄存器组完成函数的运行,但如果涉及下面的情况则一般会进行栈的分配:
1,寄存器组无法储存所有的函数局部变量。(当前函数或与它调用的函数累计)
2,有些局部变量是数组或结构。
3,需要计算局部变量的内存地址。
4,有调用另一个函数且参数超过6个。

举例,假如有函数声明和调用如下:

func1(a,b)
	...
	func2(2,2)
	...
	return
end

func2(a,b)
	...
	return
end

main()
	func1(1,1)
end

以上代码在一个老式的IA32的栈中的分配时机大概图示:

在这里插入图片描述
以帧的角度看,调用一个函数既涉及当前帧的空间分配也涉及下一帧的空间分配。在当前帧,编译器需要分别push进两个参数和指令返回地址。在下一帧中push进上一帧的起始地址(x86-64不需要了),再根据函数内具体情况继续分配空间。
在语言层面,调用函数既会触发上面的操作,在汇编层面涉及以下几条指令:

call func1
push 源数据
//push等同于sub与mov的组合
//--sub $指针移动长度 %rsp
//--mov 源数据 相对栈指针的内存地址

call指令会完成以下3个动作,1,更改PC(%rip寄存器,负责指向下一条指令的地址)为func1函数的第一条相关指令的地址。2,将栈指针向下移动8Bytes(x64)。3,向栈中推入指令call func1的下一条指令的内存地址。
如上文所说,push指令等同于subq+moveq,它们三个的使用与顺序比较灵活,编译器可自行决定。

在代码中,当一个函数return或所有代码行运行结束则会跳回调用该函数的下一行代码处,所有局部变量都会释放。在汇编层面(32位指令架构)中由以下三条指令实现:

mov 帧指针内保存的帧底地址 %rsp	//栈指针上移动到帧指针处
pop 帧指针寄存器				//将上一帧帧底地址出栈
ret							//将指令返回地址出栈

等同于:

leave
ret

在x86-64中由于取消了帧指针,可以更简洁的化为以下两条指令:

add %rsp 偏移值  //栈指针上移到上一帧帧顶
ret

也等同于

leave 
ret

ret相当于pop %rip,将返回地址出栈到程序计数器。leave指令相当于对mov,pop(IA32)或add(x86-64)包了一层(除了可恢复栈指针外好像还可以恢复被调用者寄存器状态)。总而言之,编译器对运行时栈的分配与回收就是依靠栈帧指针的上下移动来完成(x86-64只依赖栈指针)。
在这里插入图片描述


参考:
深入理解计算机系统 第二版,第三版 R.E.Bryant


维护日志:
2020-1-12:重写

  • 17
    点赞
  • 65
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
下面是一个使用C语言编写的简单虚拟机,实现了call指令、ret指令、push指令和pop指令,以及主函数调用函数的过程: ```c #include <stdio.h> #define MAX_STACK_SIZE 100 int stack[MAX_STACK_SIZE]; int stack_pointer = 0; int program_counter = 0; void push(int value) { if (stack_pointer < MAX_STACK_SIZE) { stack[stack_pointer++] = value; } else { printf("Stack overflow!\n"); } } int pop() { if (stack_pointer > 0) { return stack[--stack_pointer]; } else { printf("Stack underflow!\n"); return -1; // 或者你可以返回一个特殊的值来表示为空 } } void called_function() { int a = 10; int b = 20; int result = a + b; printf("Result: %d\n", result); } void execute_instruction(int instruction) { switch (instruction) { case 1: // call指令 push(program_counter + 1); // 将下一条指令的地址压入中 program_counter = called_function; // 跳转到called_function函数的入口 break; case 2: // ret指令 program_counter = pop(); // 从中弹出返回地址 break; default: printf("Unknown instruction!\n"); break; } } int main() { int instructions[] = {1, 2}; // call指令、ret指令 int num_instructions = sizeof(instructions) / sizeof(int); while (program_counter < num_instructions) { int current_instruction = instructions[program_counter]; execute_instruction(current_instruction); program_counter++; } return 0; } ``` 在这个示例代码中,我们定义了一个简单的虚拟机,使用C语言实现了call指令、ret指令、push指令和pop指令。我们使用一个整型数组来表示一系列的指令,然后通过遍历数组来执行每条指令。execute_instruction函数根据指令的不同,执行相应的操作。在主函数中,我们定义了一个包含call指令和ret指令的指令数组,并通过循环遍历执行每条指令。 请注意,这只是一个简单的示例,实际的虚拟机实现可能更为复杂,取决于具体的需求和设计。此示例主要用于演示如何使用C语言编写一个简单的虚拟机,并实现call指令、ret指令、push指令和pop指令以及主函数调用函数的过程

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值