C语言新手入门理解:函数栈帧——从汇编代码理解函数是如何创建、调用、销毁以及一些基础汇编指令的详细介绍。

(声明:以下均为个人理解陈述,如有错误,还请指出)


在这里插入图片描述

前言

   在学习完函数与数组部分知识后,我们常会遇见因数组越界导致死循环和函数无限递归导致栈溢出等问题。这里我们就从汇编代码入手,去理解我们所编制的代码是如何运行的,从而顺解问题。

内存地址

   首先,我们得了解一个概念,信息是需要储存的。计算机是无法直接与我们对话获取信息的,因此,科学家们构造了计算机语言。我们所编制的代码通过层层转化为二进制语言,供计算机读取。
  那信息要如何储存呢?计算机只能读懂0和1,即开与关。
  首先,计算机的CPU与内存之间是通过地址总线连接。每一根线都可以表示0或1,即每根线都能代表两种含义。对于32位的计算机,总数就有232种(4G)。
  那是不是每一根线都是一个地址呢?显然不是,通过查阅ASCII表,我们可知一共有128个字符,而每一个字符都是最基本的信息组成元素。如果我们要表示全,那必须需要8根线组成一个单元(八个二进制位)。于是,科学家就规定,一根线为一个bit,八个bit组成一个byte。最终,我们可以确定一个byte为一个地址单元,进行储存信息。
  从而进一步演化为我们所见的数据类型(char、short、int等)。
在这里插入图片描述

汇编指令

  这里涉及的汇编指令仅含下文我所讲的例子中涉及的汇编指令(完整解释各字符串含义在下文)。

xor

  xor:两个数进行逻辑异或运算,二进制位相同0,不同为1,结果赋值给第一个数。
在这里插入图片描述

mov

  mov:两个数进行赋值操作,将第二个数的值赋给第一个数。
在这里插入图片描述

lea

  lea:用于计算有效地址并将其加载到指定的寄存器中。这不是进行实际的内存引用,而是用于地址计算。
在这里插入图片描述

rep stos

  rep:重复其上面的指令。ecx的值是重复的次数。
  stos:将eax中的值拷贝到rdi指向的地址.
在这里插入图片描述

push与pop

  push:指令用于将一个寄存器或值压入栈中。栈是一种后进先出(Last In First Out)的数据结构,常用于保存函数参数、局部变量或者临时数据。
在这里插入图片描述

  pop:与push相反,将堆栈段中的一个字节弹出到某个寄存器或段寄存器或内存单元。
在这里插入图片描述

add与sub

  add:将两个数相加,并将结果存储在第一个操作数中。
在这里插入图片描述

  sub:将两个数相减,并将结果储存在第一个操作数中。
在这里插入图片描述

call与ret

  call:调用一个过程(函数),它将下一条指令的地址(也就是返回地址)压入栈中,并跳转到指定的过程地址去执行。
在这里插入图片描述
在这里插入图片描述

  ret:用于从一个过程返回。当执行RET指令时,它会从栈中弹出返回地址,并跳转到该地址继续执行。
在这里插入图片描述
在这里插入图片描述

test与 jl与jmp

  test:将两个操作数进行逻辑与运算,并根据运算结果设置相关的标志位。

  jl:若(有符号)结果小于判定值,跳转到目标地址执行指令。

  jmp:是无条件跳转指令,它告诉处理器无条件地将控制权转移给指定的地址。无论什么情况,jmp 指令后的指令都会被处理器忽略,并跳转到目标地址执行指令。
在这里插入图片描述

例题讲解(逐句讲解)

  下面以VS2022环境讲解一个简单的加法函数(不同编译器略有差异,但原理相同)

源码
#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	if (Add(a, b) >= 0)

		printf("和为非负数\n");

	else

		printf("和为负数\n");

	return 0;
}
反汇编(主函数部分)

以下讲解均以注释为主

栈指针
rbp

  rbp:保存的是栈中当前执行函数的基本地址,当前执行函数所有存储在栈上的数据都要靠 rbp 指针加上偏移量来读取。

rsp

  rsp:栈指针,它永远指向一个进程的栈顶。

int main()
//其实主函数是第一个被调用的 但这反汇码没显示 可以通过调试--窗口--调用栈堆查看
{
00007FF71BD919B0  push        rbp  
//压入main的基本地址
00007FF71BD919B2  push        rdi  
//压入一个寄存器,可以储存不同类型的数据
00007FF71BD919B3  sub         rsp,128h  
//申请空间 十六进制的128 个byte
00007FF71BD919BA  lea         rbp,[rsp+20h] 
//从栈顶往栈底数 十六进制的20 个byte 的位置 从新规定栈底基本地址
00007FF71BD919BF  lea         rdi,[rsp+20h]  
//rdi的地址就从栈底开始
00007FF71BD919C4  mov         ecx,12h  
00007FF71BD919C9  mov         eax,0CCCCCCCCh  
//eax为0CCCCCCCCh
00007FF71BD919CE  rep stos    dword ptr [rdi]  
//从rdi地址开始初始化12h个eax
00007FF71BD919D0  mov         rax,qword ptr [__security_cookie (07FF71BD9D000h)]  
//栈溢出保护机制,有兴趣可以搜一下(以下一些长英文函数均为一些保护机制,我也不多注释)
00007FF71BD919D7  xor         rax,rbp  
//通过异或检验是否栈溢出
00007FF71BD919DA  mov         qword ptr [rbp+0F8h],rax  
//运用指针记录cookie
00007FF71BD919E1  lea         rcx,[__C3EAF37F_test@c (07FF71BDA2008h)]  
00007FF71BD919E8  call        __CheckForDebuggerJustMyCode (07FF71BD9137Fh)  
//开辟栈帧,初始化安全cookie
	int a = 0;
00007FF71BD919ED  mov         dword ptr [a],0  
//运用指针取地址记录初始化a值
	int b = 0;
00007FF71BD919F4  mov         dword ptr [b],0  
//同理
	scanf("%d %d", &a, &b);
00007FF71BD919FB  lea         r8,[b]  
//将a b地址传入scanf函数
00007FF71BD919FF  lea         rdx,[a]  
00007FF71BD91A03  lea         rcx,[string "%d %d" (07FF71BD9AD70h)]  
//定义输入类型
00007FF71BD91A0A  call        scanf (07FF71BD9109Bh)  
//调用scanf函数 这里我就不补充scanf的反汇码了 太过复杂 但原理与Add类似
	if (Add(a, b) >= 0)
00007FF71BD91A0F  mov         edx,dword ptr [b]  
//同理 传输a b 指针
00007FF71BD91A12  mov         ecx,dword ptr [a]  
00007FF71BD91A15  call        Add (07FF71BD91357h)  
//调用Add函数 后文补充
00007FF71BD91A1A  test        eax,eax  
//按位与 检验Add返回值是否为空
00007FF71BD91A1C  jl          __$EncStackInitStart+6Dh (07FF71BD91A2Ch)  
//检验是否eax < 0 如果小于 则进行if下一步
		printf("和为非负数\n");
00007FF71BD91A1E  lea         rcx,[string "\xba\xcd\xce\xaa\xb7\xc7\xb8\xba\xca\xfd\n" (07FF71BD9AD78h)]  
//utf-8编码,但数据类型是字符串类型 可以查阅一下中文是如何翻译出来的
00007FF71BD91A25  call        printf (07FF71BD9119Fh)  
//调用printf函数 不探讨 按F11可以看
00007FF71BD91A2A  jmp         __$EncStackInitStart+79h (07FF71BD91A38h)  
//无条件跳过else
	else

		printf("和为负数\n");
00007FF71BD91A2C  lea         rcx,[string "\xba\xcd\xce\xaa\xb8\xba\xca\xfd\n" (07FF71BD9AD88h)]  
00007FF71BD91A33  call        printf (07FF71BD9119Fh)  //同理

	return 0;
00007FF71BD91A38  xor         eax,eax  
//清除eax中的值
}
00007FF71BD91A3A  mov         edi,eax  
//将edi值清零
00007FF71BD91A3C  lea         rcx,[rbp-20h]  
00007FF71BD91A40  lea         rdx,[__xt_z+2A0h (07FF71BD9AD40h)]  
00007FF71BD91A47  call        _RTC_CheckStackVars (07FF71BD91316h)  
//检查数据是否越界 前面两条 为call做准备
00007FF71BD91A4C  mov         eax,edi  
00007FF71BD91A4E  mov         rcx,qword ptr [rbp+0F8h]  
00007FF71BD91A55  xor         rcx,rbp  
00007FF71BD91A58  call        __security_check_cookie (07FF71BD911B8h)  
//开头类似 前面三条为call做准备
00007FF71BD91A5D  lea         rsp,[rbp+108h]  
//应该是栈顶与栈底重合
00007FF71BD91A64  pop         rdi  
//回收rdi
00007FF71BD91A65  pop         rbp  
//回收main的栈的基本地址  
00007FF71BD91A66  ret  
//返回main被调用位置
反汇编(Add函数部分)
#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>

int Add(int x, int y)
{
00007FF71BD917D0  mov         dword ptr [rsp+10h],edx  
00007FF71BD917D4  mov         dword ptr [rsp+8],ecx  
//将之前a b的指针赋值给Add中的指针 进行传址
00007FF71BD917D8  push        rbp  
00007FF71BD917D9  push        rdi  
//压入属于Add的函数栈底 与 rdi
00007FF71BD917DA  sub         rsp,0E8h  
//开辟 Add的内存空间
00007FF71BD917E1  lea         rbp,[rsp+20h]  
00007FF71BD917E6  lea         rcx,[__C3EAF37F_test@c (07FF71BDA2008h)]  
00007FF71BD917ED  call        __CheckForDebuggerJustMyCode (07FF71BD9137Fh)  
//检查是否栈溢出
	return x + y;
00007FF71BD917F2  mov         eax,dword ptr [y]  
00007FF71BD917F8  mov         ecx,dword ptr [x]  
//将传入地址所带的值 赋值给 寄存器
00007FF71BD917FE  add         ecx,eax  
//相加赋值给 ecx
00007FF71BD91800  mov         eax,ecx  
//传给 eax 对应 main 中 test
}
00007FF71BD91802  lea         rsp,[rbp+0C8h]  
//栈顶与栈底重合
00007FF71BD91809  pop         rdi  
00007FF71BD9180A  pop         rbp  
//回收 栈底与 rdi
00007FF71BD9180B  ret  
//返回主函数main

到这,我对函数栈帧的理解就陈述完了 。
希望对你有帮助!
码文不易 点点赞在这里插入图片描述

  • 32
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值