(声明:以下均为个人理解陈述,如有错误,还请指出)
函数栈帧
前言
在学习完函数与数组部分知识后,我们常会遇见因数组越界导致死循环和函数无限递归导致栈溢出等问题。这里我们就从汇编代码入手,去理解我们所编制的代码是如何运行的,从而顺解问题。
内存地址
首先,我们得了解一个概念,信息是需要储存的。计算机是无法直接与我们对话获取信息的,因此,科学家们构造了计算机语言。我们所编制的代码通过层层转化为二进制语言,供计算机读取。
那信息要如何储存呢?计算机只能读懂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
到这,我对函数栈帧的理解就陈述完了 。
希望对你有帮助!
码文不易 点点赞