概述
好久没写文章记录自己在成长路上的脚步,以至于已经忘记了自己走了多久,回首才发现留下的记忆着实少,写此文章以此勉励自己。
本文涉及的知识点有:
1.什么是栈?
2.栈帧为何物?
3.什么叫帧地址
4.函数返回地址与入口地址以及如何获取
5.函数的链接地址与加载地址(后续讲解)
6.x86汇编指令的大致认识
7.函数调用过程中栈帧的变化
9.gdb如何单步调试汇编、打印栈帧、查看寄存器等
10.形参数多余6个寄存器是,函数做哪些过程(x86_64规定只有6个寄存器来存储参数)
1.什么是栈?
栈有的地方也叫堆栈,它不是栈和堆的合称。
简单说,栈是一种LIFO(先进后出)的数据结构,它支持两种基本操作push和pop,push将数据压入栈中,pop将栈中的数据弹出并存储到指定的寄存器或者内存中。
注:此数据结构正好满足函数的调用过程:父函数在前,子函数在后;返回时,子函数先返回,父函数后返回
栈的生长方向:
栈向上生长:执行push指令后sp所指地址增大;
栈向下生长:执行push指令后sp所指地址减小;
51的栈是向高地址增长,INTEL的8031、8032、8048、8051系列使用向高地址增长的栈;但同样是INTEL,在x86系列中全部使用向低地址增长的栈。其他公司的CPU中除ARM的结构提供向高地址增长的栈选项外,多数都是使用向低地址增长的栈。
堆一般是向上增长(比如x86栈向下增长,堆还是向上增长)
另外需要注意的一点是堆栈指针(sp)所指向的存储单元是否已经保存有数据,可以分成两种情况,分别为“满堆栈”和“空堆栈”.这并不意味着堆栈是满的或者是空的,而是说当前sp指向的单元是否有有效数据的情况
满堆栈:
sp指向最后压入栈的有效数据项,称为满堆栈,这种堆栈的入栈操作要先将SP先调整然后再写入数据
空堆栈:
另外一种sp指向下一个待压入数据的空位置(SP指向的位置没有有效数据),称为空堆栈,这种堆栈的操作先写入数据再调整sp
2.栈帧为何物?
栈帧(函数帧),也就是stack frame,其本质就是一种栈,C语言中,每个栈帧对应着一个未运行完的函数,这种栈专门用于保存函数调用过程中的各种信息(参数,返回地址,局部变量等)。栈帧有栈顶和栈底之分,其中栈顶的地址最低,栈底的地址最高,SP(栈指针)就是一直指向栈顶的。
在x86-32bit中,我们用 %ebp 指向栈底,也就是基址指针;用 %esp 指向栈顶,也就是栈指针(64bit的为%rbp %rsp)
IA32寄存器与x86-64寄存器的区别 - Broglie - 博客园(IA32和x86_64寄存器简单对照表)
下面描述借鉴网络博客描述,只作为参考(以后面实际分析过程为主)
ESP(Extended Stack Pointer)为扩展栈指针寄存器,是指针寄存器的一种,用于存放函数栈顶指针。
EBP(Extended Base Pointer),扩展基址指针寄存器,也被称为帧指针寄存器,用于存放函数栈底指针。
一般来说,我们将 %ebp 到 %esp 之间区域当做栈帧(也有人认为该从函数参数开始,不过这不影响分析)。并不是整个栈空间只有一个栈帧,每调用一个函数,就会生成一个新的栈帧。在函数调用过程中,我们将调用函数的函数称为“调用者(caller)”,将被调用的函数称为“被调用者(callee)”。在这个过程中,1)“调用者”需要知道在哪里获取“被调用者”返回的值;2)“被调用者”需要知道传入的参数在哪里,3)返回的地址在哪里。同时,我们需要保证在“被调用者”返回后,%ebp, %esp 等寄存器的值应该和调用前一致。因此,我们需要使用栈来保存这些数据。
3.x86汇编指令简单介绍
X86汇编快速入门 - iosJohnson - 博客园
寄存器理解 及 X86汇编入门 - JokerJason - 博客园
https://cs.brown.edu/courses/cs033/docs/guides/x64_cheatsheet.pdf
CS107 Guide to x86-64
Guide to x86 Assembly
mov和movl 的区别:
l,w,b是ATT汇编语言(Assembly Language)中用来表达操作属性的限定符
l是长字(4字节),
w是双字
b是唯一字节
加在命令的后边
关于网络很多关于x86汇编的文章把指令的操作数关系写反,在此不做讨论
Intel x86 Function-call Conventions - Assembly View
下面截图来源《x86 Assembly Language RM.pdf》,本文分析以下面为主理解
callq指令采用一个操作数,该函数的地址被调用。它将返回地址(当前值为%rip,这是调用之后的下一条指令)压入堆栈,然后跳转到被调用函数的地址。该retq指令将返回地址从堆栈中弹出到中%rip,从而从保存的返回地址处恢复
函数调用前将六个参数赋值到寄存器%rdi,%rsi,%rdx,%rcx,%r8,和%r9(任何额外的参数压入堆栈),然后执行该call指令
leaveq和retq中的q是指64位操作数。
leaveq相当于:
movq %rbp, %rsp
popq %rbp
retq相当于:
popq %rip
注意leaveq跟开头是对应的:
push %rbp
mov %rsp,%rbp
有些指令集也把它叫做enterq。
而与retq对应的是callq,相当于:
pushq %rip
jmpq addr
4.函数实例分析
x86 Disassembly/Functions and Stack Frames - Wikibooks, open books for an open world
C语言函数调用栈(一) - clover_toeic - 博客园
x86-64 规定只有6个寄存器来存参数,那 C 函数为什么还能超过6个参数呢?
int func0(int x ,int y ,int z)
{
int a, b,c;
a = 11;
b = 4;
c = 22;
printf("0 = %p %p %pn", __builtin_return_address(0),
__builtin_frame_address(0),__builtin_frame_address(1));
return 0;
}
int main(int argc, char const *argv[])
{
int x1 =1,y1=2,z1=3;
func0(x1,y1,z1);
return 0;
}
objdump反汇编用法示例:
-d:将代码段反汇编
-S:将代码段反汇编的同时,将反汇编代码和源代码交替显示,编译时需要给出-g,即需要调试信息。
-C:将C++符号名逆向解析。
-l:反汇编代码中插入源代码的文件名和行号。
-j section:仅反汇编指定的section。可以有多个-j参数来选择多个section
查看当前程序栈的内容: x/10x $sp-->打印stack的前10个元素
查看当前程序栈的信息: info frame----list general info about the frame
查看当前程序栈的参数: info args---lists arguments to the function
查看当前程序栈的局部变量: info locals---list variables stored in the frame
查看当前寄存器的值:info registers(不包括浮点寄存器) info all-registers(包括浮点寄存器)
查看当前栈帧中的异常处理器:info catch(exception handlers)
//只截取main函数与fun0函数分析:
0000000000400526 <func0>:
#include <stdio.h>
int func0(int x ,int y ,int z)
{
400526: 55 push %rbp
400527: 48 89 e5 mov %rsp,%rbp
40052a: 48 83 ec 20 sub $0x20,%rsp
40052e: 89 7d ec mov %edi,-0x14(%rbp)
400531: 89 75 e8 mov %esi,-0x18(%rbp)
400534: 89 55 e4 mov %edx,-0x1c(%rbp)
int a, b,c;
a = 11;
400537: c7 45 f4 0b 00 00 00 movl $0xb,-0xc(%rbp)
b = 4;
40053e: c7 45 f8 04 00 00 00 movl $0x4,-0x8(%rbp)
c = 22;
400545: c7 45 fc 16 00 00 00 movl $0x16,-0x4(%rbp)
printf("0 = %p %p %pn", __builtin_return_address(0),
__builtin_frame_address(0),__builtin_frame_address(1));
40054c: 48 8b 45 00 mov 0x0(%rbp),%rax
400550: 48 89 c1 mov %rax,%rcx
400553: 48 89 ea mov %rbp,%rdx
400556: 48 8b 45 08 mov 0x8(%rbp),%rax
40055a: 48 89 c6 mov %rax,%rsi
40055d: bf 34 06 40 00 mov $0x400634,%edi
400562: b8 00 00 00 00 mov $0x0,%eax
400567: e8 94 fe ff ff callq 400400 <printf@plt>
return 0;
40056c: b8 00 00 00 00 mov $0x0,%eax
}
400571: c9 leaveq
400572: c3 retq
0000000000400573 <main>:
int main(int argc, char const *argv[])
{
400573: 55 push %rbp
400574: 48 89 e5 mov %rsp,%rbp
400577: 48 83 ec 20 sub $0x20,%rsp
40057b: 89 7d ec mov %edi,-0x14(%rbp)
40057e: 48 89 75 e0 mov %rsi,-0x20(%rbp)
int x1 =1,y1=2,z1=3;
400582: c7 45 f4 01 00 00 00 movl $0x1,-0xc(%rbp)
400589: c7 45 f8 02 00 00 00 movl $0x2,-0x8(%rbp)
400590: c7 45 fc 03 00 00 00 movl $0x3,-0x4(%rbp)
func0(x1,y1,z1);
400597: 8b 55 fc mov -0x4(%rbp),%edx
40059a: 8b 4d f8 mov -0x8(%rbp),%ecx
40059d: 8b 45 f4 mov -0xc(%rbp),%eax
4005a0: 89 ce mov %ecx,%esi
4005a2: 89 c7 mov %eax,%edi
4005a4: e8 7d ff ff ff callq 400526 <func0>
return 0;
4005a9: b8 00 00 00 00 mov $0x0,%eax
4005ae: c9 leaveq
4005af: c3 retq
函数返回地址:__builtin_return_address,一般指子函数执行完成后,需要返回到父函数对应位置继续执行的地址()。
函数入口地址:函数名,执行函数的入口地址
函数帧地址:__builtin_frame_address
Return Address (Using the GNU Compiler Collection (GCC))
gcc xx.c -g
gdb xx.out
>start
下面开始分析函数调用过程:
从下图可知进入main函数后rip指向了main局部变量开始赋值的位置(start为c语言级别调试,无法控制main在此之前的汇编)
si单步调试汇编指令
下面为main函数局部变量在栈中的定义过程
调用fun0之前进行形参准备
最后
以上就是高挑板凳为你收集整理的深入理解C函数调用过程及函数栈帧(20220302)的全部内容,希望文章能够帮你解决深入理解C函数调用过程及函数栈帧(20220302)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复