计算机组成原理 —— 函数栈帧的创建切换传参

我们之前学习了指令的一些相关内容,我们今天来看利用这些指令,如何实现函数栈帧的创建切换

虚拟地址空间

在这之前我们要稍微复习一下虚拟地址空间

https://blog.csdn.net/qq_67693066/article/details/134044905

不得不拿出我之前在Linux中画的图了:
在这里插入图片描述
我们有关程序调用的运行的信息会放在栈区
在这里插入图片描述

栈区(Stack)是进程虚拟地址空间的一个重要组成部分,主要用于存储函数调用期间的信息以及函数内的局部变量。栈区的特点是按照后进先出(LIFO, Last In First Out)的原则工作,这意味着最后压入栈的数据将是第一个被弹出的数据。
以下是栈区中通常存放的内容:

  1. 局部变量
  • 函数内部声明的局部变量通常会存储在栈中。这些变量在其作用域内有效,在函数返回时会被销毁。
  1. 函数调用帧(Process Activity Record)
  • 每次函数调用时,都会创建一个新的栈帧来存储该函数的相关信息,包括:
    • 参数值:传递给函数的参数。
    • 返回地址:函数调用完成后应返回的代码位置。
    • 局部变量:函数内部声明的变量。
    • 临时数据:函数执行过程中产生的临时数据。
    • 保存的寄存器状态:为了防止函数调用期间破坏寄存器的值,一些寄存器会在进入函数之前被保存在栈上。
  1. 返回值
  • 在某些架构中,函数的返回值也可能暂时存储在栈上,尤其是在返回值较大或不适合寄存器的情况下。
  1. 函数调用开销
  • 函数调用所需的额外开销,比如栈帧指针和基址指针的设置。
    栈区的主要特点有:
  • 栈区的空间相对较小,因为它的大小通常是固定的或有限制的。
  • 栈区的内存分配和释放非常快速,是由编译器自动完成的。
  • 栈区通常是从高地址向低地址方向增长(向下生长栈)。

由于栈区的内存是自动管理和回收的,开发者不需要显式地分配和释放内存,这使得栈区非常适合存储那些生命周期短暂且大小已知的对象。然而,如果在栈上分配过大的数据结构或递归调用层次过深,则可能导致栈溢出(Stack Overflow),这是一种常见的编程错误

常见寄存器

在 Intel x86 架构下,有一些常用的寄存器用于执行各种操作。这些寄存器分为通用寄存器、段寄存器、指令指针寄存器以及其他特殊用途的寄存器。以下是 x86 架构中的一些主要寄存器及其用途:

通用寄存器

这些寄存器通常用于存储数据和中间结果,也可以作为索引或基址寄存器。

32位寄存器
  • eax (accumulator):累加器寄存器,通常用于算术运算的结果。
  • ebx (base):基址寄存器,用于指向数据结构。
  • ecx (counter):计数寄存器,通常用于循环计数。
  • edx (data):数据寄存器,经常用于乘法和除法运算。
  • esi (source index):源索引寄存器,用于指向源数据。
  • edi (destination index):目标索引寄存器,用于指向目标数据。
  • esp (stack pointer):堆栈指针寄存器,指向当前堆栈顶部。
  • ebp (base pointer):基址指针寄存器,用于指向当前函数调用帧的底部。

处了edi和esi之外,剩下的寄存器大家要熟悉。

16位寄存器
  • ax, bx, cx, dx, si, di, sp, bp:32位寄存器的低16位部分。
  • ah, bh, ch, dh:32位寄存器的高8位部分。
64位寄存器

在 x86-64 架构中,除了上述寄存器扩展到了 64 位之外,还引入了一些新的寄存器:

  • rax, rbx, rcx, rdx, rsi, rdi, rsp, rbp:32位寄存器的64位版本。
  • r8 - r15:额外的通用寄存器。

段寄存器

这些寄存器用于存储段选择符,通常与指针寄存器一起使用来计算内存地址。

  • cs (code segment):代码段寄存器,指向当前执行的代码段。
  • ds (data segment):数据段寄存器,指向数据段。
  • ss (stack segment):堆栈段寄存器,指向堆栈段。
  • es (extra segment):附加段寄存器,可以指向任何段。
  • fs (extra segment):附加段寄存器,可以指向任何段。
  • gs (extra segment):附加段寄存器,可以指向任何段。

指令指针寄存器

  • eip (instruction pointer):指令指针寄存器,指向当前正在执行的指令地址。
  • rip (64-bit instruction pointer):在 x86-64 架构中,这是 64 位的指令指针寄存器。

标志寄存器

  • eflags (flags register):包含各种标志位,如 CF(进位标志)、PF(奇偶标志)、AF(辅助进位标志)、ZF(零标志)、SF(符号标志)、TF(陷阱标志)、IF(中断标志)、DF(方向标志)、OF(溢出标志)。

其他特殊寄存器

  • cr0 - cr4 (control registers):控制寄存器,用于控制 CPU 的各种特性。
  • dr0 - dr7 (debug registers):调试寄存器,用于支持调试功能。
  • mmx0 - mmx7, xmm0 - xmm15, ymm0 - ymm15:多媒体寄存器,用于 SIMD 操作。

这些寄存器在编写汇编语言程序时非常重要,因为它们直接影响着程序的行为和性能。了解这些寄存器的功能可以帮助你更有效地编写和优化程序。

Intel的汇编指令

Intel x86 架构下的汇编语言指令集非常丰富,涵盖了各种类型的指令,包括算术运算、逻辑运算、控制转移、输入输出操作、内存访问等等。下面是一些基本的 Intel x86 汇编指令的例子及其说明:

数据传送指令

  • mov (move): 将数据从一个寄存器或内存位置移动到另一个寄存器或内存位置。
    • mov eax, 5:将立即数 5 移动到寄存器 eax
    • mov ebx, [esi]:将 esi 指向的内存位置的数据移动到寄存器 ebx
    • mov [edi], eax:将寄存器 eax 中的数据移动到 edi 指向的内存位置。

算术指令

  • add (addition): 加法。
    • add eax, ebx:将 ebx 加到 eax 上。
  • sub (subtraction): 减法。
    • sub eax, ebx:从 eax 中减去 ebx
  • mul (multiply): 乘法。
    • mul ecx:将 eaxecx 相乘,结果的低 32 位存入 eax,高 32 位存入 edx
  • div (divide): 除法。
    • div edx:用 eax(低 32 位)和 edx(高 32 位)组成的 64 位数除以 edx,商存入 eax,余数存入 edx

逻辑指令

  • and: 逻辑与。
    • and eax, ebx:将 eaxebx 进行按位与运算。
  • or: 逻辑或。
    • or eax, ebx:将 eaxebx 进行按位或运算。
  • xor: 逻辑异或。
    • xor eax, ebx:将 eaxebx 进行按位异或运算。
  • not: 逻辑非。
    • not eax:对 eax 中的每一位取反。

控制转移指令

  • jmp (jump): 无条件跳转。
    • jmp label:跳转到标签 label 所指定的位置。
  • jcc (conditional jump): 条件跳转。
    • je label:如果 ZF(零标志)为 1,则跳转到 label
    • jne label:如果 ZF(零标志)不为 1,则跳转到 label
    • jg label:如果 CF(进位标志)和 ZF 都为 0,则跳转到 label
    • jl label:如果 SF(符号标志)和 OF(溢出标志)不同,则跳转到 label

内存访问指令

  • lea (load effective address): 计算有效地址并将其加载到寄存器中。
    • lea eax, [ebx + ecx * 4]:计算 ebx + ecx * 4 的有效地址,并将其存入 eax
  • push / pop: 堆栈操作。
    • push eax:将 eax 的值压入堆栈。
    • pop eax:从堆栈弹出一个值并将其存入 eax

其他常用指令

  • cmp (compare): 比较两个值,并设置标志位。
    • cmp eax, ebx:比较 eaxebx 的值,并根据结果设置标志位。
  • inc / dec: 对寄存器中的值进行加一或减一操作。
    • inc eaxeax 加 1。
    • dec ebxebx 减 1。

了解一些汇编指令可以帮助我们理解创建栈帧的过程。

函数栈帧创建

这里我们用VS2019来观察函数栈帧创建的过程,我们写这样的一段代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

int P(int x1,int x2)
{
	return x1 + x2;
}

int main()
{
	int a = 19, b = 90;

	int result = 0;

	result = P(19,90);

	return 0;
}

我们在main这打个断点,转到反汇编:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述我们先看前三行代码,我们画个图来表示:
在这里插入图片描述
首先我们push一下ebp,然后我们把esp中的值给了ebp:
在这里插入图片描述
在图中,我们可以这么表示:
在这里插入图片描述接下来进行了sub操作,将esp中的值减少了228:
在这里插入图片描述
在这里插入图片描述这时候esp到ebp所指的区域就是main函数的函数栈帧:
在这里插入图片描述

函数栈帧的切换

我们一直调试,调试到call这里:
在这里插入图片描述
这里的call指令表明我们要转移到另一个新的函数中去运行了,这个时候我们就要切换函数栈帧,这个时候,首先,我们原来的函数栈帧不能丢,这个时候,会把ebp指向的地址压入栈中
在这里插入图片描述

这个时候,我们点击逐语句,进入到P函数内部:
在这里插入图片描述
在这里插入图片描述
我们之前画图的时候已经将ebp的值压入了,这个时候esp中的值会赋值给ebp:
在这里插入图片描述之后又是sub,esp减:
在这里插入图片描述之后esp到ebp这段区域就是P的函数栈帧:
在这里插入图片描述如果之后我想切换回main函数的栈帧,我们首先将esp指回ebp,拿出里面的16251460,让ebp往这个地址去跳,然后esp-4就行了(按字节编址的)。

但是哦,我们切换回原来的main函数栈帧,我们肯定是想在原来的代码下继续往下执行,我们都知道我们有一个程序计数器PC(这里会称作IP计数器),所以我们在切换函数之前,会把原来的IP数压到栈中,方便我们知道在上一个函数栈帧里的代码运行到哪里了
在这里插入图片描述
在这里插入图片描述

函数返回:
在这里插入图片描述在这里插入图片描述

函数栈帧的传参

传参的话可以参考这一张图片:
在这里插入图片描述

全局变量和调用参数会占一个函数栈帧的两头,如果我们先访问参数,直接ebp+4,ebp+8就可以访问到:
在这里插入图片描述我们也可以自己调试一下验证一下参数的顺序:

在这里插入图片描述
我们发现先压的90,后压的19,跟我们预料的一样。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值