寄存器和函数调用栈介绍

什么是寄存器

通俗的理解:

寄存器就是你的口袋。身上只有那么几个,只装最常用或者马上要用的东西。

内存就是你的背包。有时候拿点什么放到口袋里,有时候从口袋里拿出点东西放在背包里。

辅存就是你家里的抽屉。可以放很多东西,但存取不方便。

寄存器的种类

        操作系统有32位和64位之分,因此寄存器的位数也不一样:

32位和62位的差异:

  • 32位有8个通用寄存器,64位有16个通用寄存器
  • 32位的8个寄存器名称都是r开头;64位的16个寄存器名称都是e开头

寄存器的逻辑结构

        以RAX为例,寄存器的逻辑结构如下图所示:

        RAX寄存器是一个64位寄存器,那么它可以存储一个64位的数据。RAX寄存器是在64位处理器中工作的,它的上一代处理器是32位处理器,而32位处理器的上一代是16位处理器。为了保证兼容,使之前的程序的可以在64位处理器上运行,使得RAX寄存器包含了32位、16位、8位寄存器。

        RAX寄存器可分为一个可独立使用的32位寄存器:EAX(32位);32位寄存器还可以分为一个16位的AX寄存器;AX还可以分为两个独立使用的8位寄存器:AH(8位~15位为高8位)、AL(0位~7位为低8位)。

寄存器的用途

        每个寄存器都有自己的用途,下面是32位和64位寄存器的用途说明:

32位寄存器:

  • EAX:累加寄存器(Accumulator),操作数和结果数据累加器,保存返回值
  • EBX:基地址寄存器(Base),DS段的数据指针,在内存寻址时存放基地址
  • ECX:计数寄存器(Counter),字符串和循环操作的计数器
  • EDX:数据寄存器(Data),保存乘法形成的部分结果或者除法之前部分被除数
  • ESI:字符串操作的源(Source Index)指针,SS段的数据指针
  • EDI:字符串操作的目标(Destination Index)指针,ES段的数据指针
  • EBP:基址指针(Base pointer),存放栈底指针
  • ESP:堆栈(Stack pointer)指针,存放栈顶指针

此外,CPU内还有6个段寄存器、1个指令寄存器:

CPU内部的段寄存器:

  • CS——代码段寄存器(Code Segment Register),其值为代码段的段值;
  • DS——数据段寄存器(Data Segment Register),其值为数据段的段值;
  • ES——附加段寄存器(Extra Segment Register),其值为附加数据段的段值;
  • SS——堆栈段寄存器(Stack Segment Register),其值为堆栈段的段值;
  • FS——附加段寄存器(Extra Segment Register),其值为附加数据段的段值;
  • GS——附加段寄存器(Extra Segment Register),其值为附加数据段的段值。

指令寄存器:

  • EIP:(Instruction Pointer)是存放下一条指令的内存地址

64位寄存器:

  • RAX:返回值
  • RBX:被调用者保存
  • RCX:第四个参数
  • RDX:第三个参数
  • RSI:第四个参数
  • RDI:第三个参数
  • RBP:存放栈底指针
  • RSP:存放栈顶指针
  • R8:第5个参数
  • R9:第6个参数
  • R10~R15:被调用者保存
  • R8~R15属于普通寄存器,支持拆分,但是拆分的寄存器在命名规则上与特殊功能寄存器有所不同。32位拆分寄存器以D作为后缀(DWORD),16位寄存器以W作为后缀(WORD),8位则以B作为后缀(BYTE)。

常用的汇编指令

1.NOP :交换指令,操作数为90,什么也不干
2.PUSH x:将x压入栈,esp先减再压数据
3.POP x: 将栈顶数据弹出到x中,esp递增
4.CALL x:将call指令下一条指令压入栈中,改变eip的值为x
5.RET: 将栈顶数据弹出到eip 若为RET X 表示esp再增加x字节
6.MOV  目的, 源:将源的值传给目的
7.LEA x1,x2:将x2的地址载入到x1
8.ADD x1,x2:x1=x1+x2,指令的计算结果将影响eflags寄存器
9.SUB x1,x2:x1=x1-x2,同add,不能同时是内存操作数
10. jmp x: 将eip的值改为x。 jmp -2意味着jmp指令的无限循环
11. jcc x:当条件满足时跳转到x
12. cmp x1, x2: 用于比较两个操作数的大小
13. test x1, x2:通过计算两个操作数的逻辑与位运算实现比较操作,根据结果设置eflags
14. AND x1,x2: x1 = x1&x2
15. OR x1,x2: x1 = x1|x2
16. XOR x1,x2: x1 = x1^x2 常用于清零,效率高xor eax eax
17. not x1: x1 = ~x1 
18. SHL x1,x2:x1=x1<<x2逻辑左移,右边补0,   SAL:算术左移
19. SHR x1,x2:x1=x1>>x2 逻辑右移,左边补0,  SAR:算术右移,左边补符号
20. IMUL/MUL: imul实现有符号数的乘法运算,mul是无符号乘法
	imul r/m32 eax=eax*r/m*32
	imul reg,r/m reg=reg*r/m32
	imul reg,r/m32,immediate reg=r/m32*immediate
21. IDIV/DIV: div用于实现无符号数的除法运算
	idiv ecx: eax / ecx 商放在eax,余数放在edx 
	div ax, r/m8 ; ax 除以 r/m8, al是商,ah是余数
	div eax,r/m32; eax除以r/m32,eax是商,edx是余数
	若被除数是32位数,在指令执行前edx被置为0
22. REP STOS; rep是前缀,表示重复执行某项操作
23. REP MOVS:重复执行movs操作
24. LEAVE: 用于子函数推出时清理栈的操作,ret和leave选择取决于编译器。  等价于mov  esp, ebp; pop ebp

函数调用栈的过程

什么是栈帧

        首先,什么是栈帧?引用百度百科:C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。从这句话中,可以提炼以下几点信息:

  • 栈帧是一块因函数运行而临时开辟的空间。
  • 每调用一次函数便会创建一个独立栈帧。
  • 栈帧中存放的是函数中的必要信息,如局部变量、函数传参、返回值等。
  • 当函数运行完毕栈帧将会销毁。

ESP,EBP,EAX寄存器

        栈帧中有2个重要的寄存器:esp和ebp。可以把esp和ebp看作是两个指针,ebp指向当前栈帧栈底,esp指向函数栈栈顶。

代码演示

演示代码:

#include <stdio.h>

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

int main()
{
	int a = 10;
	int b = 20;
	int c = 0;
	c = Add(a, b);
	return 0;
}

这段代码,我们在VS2022编译器上调试,调试进入Add函数后,我们就可以观察到函数的调用堆栈:

我们可以从中清晰的观察到,main 函数调用之前,是由 invoke_main 函数来调用 main 函数.

在 invoke_main 函数之前的函数调用我们暂时就不考虑了。

那我们可以确定,invoke_main 函数应该会有自己的栈帧,main 函数和 Add 函数也会维护自己的栈帧,每个函数栈帧都由 ebp 和 esp 来维护栈帧空间。

那接下来我们从 main 函数的栈帧创建开始讲解。

main函数反汇编代码

        我们在main函数内设置好断点,然后按F5启动调试。然后调出反汇编代码:

int main()
{
00E016E0  push        ebp  
00E016E1  mov         ebp,esp  
00E016E3  sub         esp,0E4h  
00E016E9  push        ebx  
00E016EA  push        esi  
00E016EB  push        edi  
00E016EC  lea         edi,[ebp+FFFFFF1Ch]  
00E016F2  mov         ecx,39h  
00E016F7  mov         eax,0CCCCCCCCh  
00E016FC  rep stos    dword ptr es:[edi]  
	int a = 10;
00E016FE  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
00E01705  mov         dword ptr [ebp-14h],14h  
	int c = 0;
00E0170C  mov         dword ptr [ebp-20h],0  
	c = Add(a, b);
00E01713  mov         eax,dword ptr [ebp-14h]  
00E01716  push        eax  
00E01717  mov         ecx,dword ptr [ebp-8]  
00E0171A  push        ecx  
00E0171B  call        00E010F0  
00E01720  add         esp,8  
00E01723  mov         dword ptr [ebp-20h],eax  
	return 0;
00E01726  xor         eax,eax  
}

由于main函数是由 invoke_main 函数调用的,所以我们这里先画出 invoke_main 函数的函数栈帧:

第一步:push ebp。把ebp压入栈中,esp-4

第二步:mov ebp, esp。把esp的值存入ebp(使ebp和esp同时指向栈顶)

第三步:sub esp, 0E4h。esp减去0E4h的大小,即esp下移0E4h。为main函数开辟栈空间

第四步~第六步:分别将ebx、esi、edi压栈,esp-4,esp-4,esp-4

  • push        ebx
  • push        esi 
  • push        edi

第七步~第十步

  • lea           edi,[ebp+FFFFFF1Ch]  
  • mov         ecx,39h  
  • mov         eax,0CCCCCCCCh  
  • rep stos   dword ptr es:[edi]  
  1. 先把 ebp+FFFFFF1Ch (等于ebp+0E4h)放到edi中
  2. 把 39h 放到 ecx中
  3. 把 0xCCCCCCCC 放在 eax 中
  4. 将 ebp+FFFFFF1Ch 到 ebp 这一段的内存的每个字节都初始化为 0xcccccccc

将这四步执行完后,发现main函数栈对应的这块内存全部被初始化为0xcccccccc:

所以,当我们打印未初始化的内存内容,会输出 “烫”这么一个奇怪的字,0xcccccccc的汉字编码就是“烫”。

那么现在main函数的函数栈帧就创建完毕了,接下来就是main函数中代码的执行。

第十一步:mov  dword ptr [ebp-8],0Ah。将0Ah(10)存储到ebp-8的地址处,ebp-8的位置就是变量a。

第十二步:mov  dword ptr [ebp-14h],14h。 将14h(20)存储到ebp-14h的地址处,ebp-14h的位置其实就是b变量:

第十三步:mov dword ptr [ebp-20h],0。将0存储到ebp-20h的地址处,ebp-20h的位置其实就是c变量:

第十四步~第十七步:调用Add函数(传参):Add(a, b)

  • mov         eax,dword ptr [ebp-14h]  
  • push        eax 
  • mov         ecx,dword ptr [ebp-8]  
  • push        ecx  
  1. 传递b(从右往左扫描)将ebp-14h处放的20放在eax寄存器中
  2. 将eax的值压栈,esp-4
  3. 传递a,将ebp-8处的10放在ecx寄存器中
  4. 将ecx的值压栈,esp-4

第十八步:调用Add函数:

call指令是要执行函数调用的,它会执行2个动作:

  1. 在执行call指令之前会把call指令的下一条add指令的地址进行压栈操作。这个操作是为了在函数调用结束后回到call指令的下一条指令add的地方,继续往后执行。我们可以看到call指令的下一条指令add的地址是:00E01720
  2. 保存下一步要执行的指令的地址。即:使eip寄存器的值为00E010F0

第十九步:这个时候我们按键盘上的F11进入Add函数:

Add函数的反汇编代码

再按一下F11,就可以跳转到Add函数。Add函数的反汇编代码如下:

int Add(int x, int y)
{
00E01690  push        ebp  
00E01691  mov         ebp,esp  
00E01693  sub         esp,0CCh  
00E01699  push        ebx  
00E0169A  push        esi  
00E0169B  push        edi  
00E0169C  lea         edi,[ebp+FFFFFF34h]  
00E016A2  mov         ecx,33h  
00E016A7  mov         eax,0CCCCCCCCh  
00E016AC  rep stos    dword ptr es:[edi]  
	int z = 0;
00E016AE  mov         dword ptr [ebp-8],0  
	z = x + y;
00E016B5  mov         eax,dword ptr [ebp+8]  
00E016B8  add         eax,dword ptr [ebp+0Ch]  
00E016BB  mov         dword ptr [ebp-8],eax  
	return z;
00E016BE  mov         eax,dword ptr [ebp-8]  
}
00E016C1  pop         edi  
00E016C2  pop         esi  
00E016C3  pop         ebx  
00E016C4  mov         esp,ebp  
00E016C6  pop         ebp  
00E016C7  ret  

第一步~第六步:为Add函数创建一块栈帧空间

  • push        ebp  
  • mov         ebp,esp  //让ebp指向esp
  • sub          esp,0CCh  
  • push        ebx  
  • push        esi  
  • push        edi  

为Add函数中创建栈帧的方法和main函数中是相似的,在栈帧空间的大小上略有差异而已。

  1. 将main函数的ebp压栈,esp-4
  2. 计算新的ebp,esp
  3. 将ebx,esi,edi寄存器的值保存

第七~十步:初始化add函数的栈帧空间

  • lea         edi,[ebp+FFFFFF34h]  
  • mov         ecx,33h  
  • mov         eax,0CCCCCCCCh  
  • rep stos    dword ptr es:[edi] 

第十一步:将0存储到ebp-8的地址处,ebp-8的位置就是变量z。

mov         dword ptr [ebp-8],0  

第十二、三步:计算z的值

  • mov         eax,dword ptr [ebp+8]  
  • add         eax,dword ptr [ebp+0Ch]  
  1. 将ebp+8地址处存放的值存入eax寄存器中,即把形参a存入eax
  2. 将ebp+12地址处的数字加到eax寄存器中,即把形参b加入eax

以上两步算出a+b的值,并把其存入了寄存器eax中。

第十二、三步:将eax的结果保存到ebp-8的地址处,其实就是放到z中

  • mov         dword ptr [ebp-8],eax  

第十四步:将ebp-8地址处存放的值放入eax中,其实就是把z的值存储到eax寄存器中,这里是想通过eax寄存器带回计算的结果,做函数的返回值。

  • mov         eax,dword ptr [ebp-8]

做完这一步我们的函数调用就将结束了,应该开始返回了。

函数栈帧的销毁

当函数调用要结束返回的时候,前面创建的函数栈帧也开始销毁。

那具体是怎么销毁的呢?我们来看一下反汇编代码。

第一步~第三步:

  1. 在栈顶弹出一个值,存放到edi中,esp+4
  2. 在栈顶弹出一个值,存放到esi中,esp+4
  3. 在栈顶弹出一个值,存放到ebx中,esp+4

第四步:让esp指向ebp,此时ebp和esp指向同一处

  • mov         esp,ebp  

第五步:弹出栈顶的值存放到ebp,栈顶此时的值恰好就是之前main函数的ebp。

  • pop         ebp

esp+4,此时恢复的main函数的栈帧维护,esp指向main函数栈帧的栈顶,ebp指向main函数栈帧的栈底:

第六步:ret指令会将栈顶数据弹出给eip,然后esp+4。

然后直接跳转到call指令的下一条指令的地址00E01720处,回到main函数,继续往下执行:

第一步:esp直接+8,相当于跳过了main函数中压栈的a和b

  • add         esp,8

第二步:把eax的值赋给ebp-20h地址所指向的空间,其实就是存储到main函数的c变量中,而此时eax中就是Add函数中计算的x和y的和,可以看出来,本次函数的返回值是由eax寄存器带回来的。程序是在函数调用返回之后,在eax中去读取返回值的。

  • mov         dword ptr [ebp-20h],eax

参考文献:

如何通俗地解释什么是寄存器? - 知乎 (zhihu.com)

函数栈帧详解_函数调用栈帧过程(带图详解)_码农菠萝的博客-CSDN博客

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值