我是靠谱客的博主 高挑板凳,最近开发中收集的这篇文章主要介绍深入理解C函数调用过程及函数栈帧(20220302),觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

        好久没写文章记录自己在成长路上的脚步,以至于已经忘记了自己走了多久,回首才发现留下的记忆着实少,写此文章以此勉励自己。

本文涉及的知识点有:

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)所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(52)

评论列表共有 0 条评论

立即
投稿
返回
顶部