C函数调用过程

2023-05-16


这几天在看GCC Inline Assembly,在C代码中通过asm或__asm__嵌入一些汇编代码,如进行系统调用,使用寄存器以提高性能能,需要对函数调用过程中的堆栈帧(Stack Frame)、CPU寄存器、GCC inlie assembly等了如指掌。现在看看函数调用过程吧。


1. Linux 进程虚拟地址空间

以32位操作系统为例,下面是Linux进程地址空间布局:



32位虚拟地址空间的高1GB的空间是留给操作系统内核的,栈由高地址到低地址向下增长,堆由低地址到高地址向上增长。

C中如 malloc 等分配的内存在堆中分配。初始化了的静态变量和全局变量放在Data段中。未初始化的全局变量和局部静态变量放在Bss段中,更准确的说是在Bss段为它们预留了空间。非静态局部变量是在函数调用过程中暂存在栈上的。


2. 函数的堆栈帧

栈在程序运行中具有举足轻重的地位。最重要的,栈保存了一个函数调用所需要的维护信息,被称为堆栈帧(Stack Frame),一个函数(被调函数)的堆栈帧一般包括下面几个方面的内容:

(1) 函数参数,默认调用惯例情况下从右向左的顺序依次把参数压入栈中。由函数调用方执行。

(2) 函数的返回地址,即调用方调用此函数(如call func1)的下一条指令的地址。函数调用方(call指令)执行。

(3) 保存调用方函数的EBP寄存器,即将调用方函数的EBP压入堆栈,并令EBP指向此栈中的地址:pushl %ebp; movl %esp, %ebp。由被调函数执行。

(4) 上下文:保存在函数调用过程中需要保持不变的寄存器(函数调用方的),如ebx,esi,edi等。由被调函数执行。

(5) 临时变量,如非静态局部变量。

下面是一个函数的堆栈帧结构图:


压入函数参数和返回地址的过程是由函数调用方在调用函数之前将其压入栈中,每个函数执行后首先要执行的就是把函数调用方的EBP寄存器压入栈中,之后是在栈上开辟一些空间存放局部变量,最后把要保存的寄存器压入栈中。i386标准函数进入和退出的指令序列的基本形式如下:(GCC 内联汇编描述)

pushl %ebp
movl %esp, %ebp
subl $x, %esp
[pushl reg1]
...
[pushl regn]
函数实际内容
[popl regn]
...
[popl reg1]
movl %ebp, %esp
opol %ebp
ret

3. 例子

下面通过代码分析一下吧:

/* stackFrame.c*/                                                                                                            
#include <stdio.h>

void func1(int a, int b);
void func2(int a, int b, int c);

int main(void )
{
    int a = 1;
    func1(a, 2);
    return 0;
}

void func1(int a, int b)
{
    int  a1 = 3;
    char b1 = '4' - '0'; //4
    func2(a1, b1, 5);
}

void func2(int a, int b, int c)
{
    char a1 = 'a';
}

汇编后的汇编代码如下,gcc -o stackFrame.s -S stackFrame.c

    .file   "funcstack.c"                                                                                                    
    .text
.globl main
    .type   main, @function
main:
    pushl   %ebp
    movl    %esp, %ebp
    andl    $-16, %esp
    subl    $32, %esp
    movl    $1, 28(%esp)
    movl    $2, 4(%esp)
    movl    28(%esp), %eax
    movl    %eax, (%esp)
    call    func1
    movl    $0, %eax                //函数返回值送往eax
    leave
    ret
    .size   main, .-main
.globl func1
    .type   func1, @function
func1:
    pushl   %ebp                //压入函数调用方的EBP,Old EBP
    movl    %esp, %ebp          //令当前EBP指向栈顶,此时的栈顶指向Old EBP
    subl    $40, %esp           //在栈上开辟一些空间,存放局部变量等
    movl    $3, -16(%ebp)       //func1中整型变量a1的存储位置, ebp-16
    movb    $4, -9(%ebp)        //func1中字符型变量b1的存储位置,ebp-9
    movsbl  -9(%ebp), %eax      //将b1送入eax
    movl    $5, 8(%esp)         //将调用func2的函数参数5压入栈,esp+8
    movl    %eax, 4(%esp)       //将参数b1压入栈,            esp+4
    movl    -16(%ebp), %eax     //将a1送入eax
    movl    %eax, (%esp)        //将参数a1压入栈,            esp+0
    call    func2               //调用func2,下一调指令的返回地址入栈(EIP入栈),内部完成,无汇编代码
    leave
    ret                         //将返回地址送往EIP
    .size   func1, .-func1
.globl func2
    .type   func2, @function
func2:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $16, %esp
    movb    $97, -1(%ebp)
    leave
    ret
    .size   func2, .-func2
    .ident  "GCC: (GNU) 4.4.4 20100726 (Red Hat 4.4.4-13)"
    .section    .note.GNU-stack,"",@progbits 

4. 例子

代码如下:

#include <stdio.h> 

void test1(int a, int b);
void test2(int a, int b, int c);

int main(void)
{
    test1(1, 2);
    return 0;
}

void test1(int a, int b)
{
    char *ebp = NULL;

    asm("movl %%ebp, %0\n\t"
        :"=r"(ebp));

    printf("test1: ebp = %p\n", ebp); //test1函数的ebp
    test2(3, 4, 5);
}

void test2(int a, int b, int c)
{
    int *ebp = NULL;

    asm("movl %%ebp, %0\n\t"
        :"=r"(ebp));

    printf("test2: *ebp = %p\n", (char *)(*ebp)); //test2的ebp指向的内容(old EBP),是test1的EBP
    printf("*(ebp + 4)  = %d\n", *((char *)ebp + 4));  //返回地址
    printf("*(ebp + 8)  = %d\n", *((char *)ebp + 8));  //第一个参数 3
    printf("*(ebp + 12) = %d\n", *((char *)ebp + 12)); //第二个参数 4
    printf("*(ebp + 16) = %d\n", *((char *)ebp + 16)); //第三个参数 5
}     

结果如下:



本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C函数调用过程 的相关文章

随机推荐

  • 编译升级OpenSSL报错:Can‘t locate IPC/Cmd.pm

    错误信息 span class token punctuation span root 64 dowhere openssl 3 0 3 span class token punctuation span span class token
  • 安装Paddlepaddle2.4.2以后导入模块报错: ImportError: libcudart.so.10.2: cannot open shared object file: No su

    问题描述 在使用 python3 8 安装 paddle 以后 xff0c 测试导入 paddle 报错 创建环境并安装 span class token builtin class name source span activate pa
  • 人工智能导论考前整理-思考题部分

    以下均为个人观点 xff08 也参考资料得到 侵删 xff09 xff0c 仅供参考 xff0c 可考试前用 我们学校的考点是基本都在上面了 xff08 考题雷达嘿嘿 xff09 xff0c 不同学校不太一样 xff0c 以下是我之前考前边
  • 自抗扰控制器中扩张状态观测器的设计

    状态观测器是根据系统的输入输出来确定系统内部状态变量的装置 xff0c 它的示意图如下 xff1a 在自抗扰控制器的设计过程中 xff0c 我们通常把未知的干扰都用 f f f 来表示 xff0c 如果利用状态观测器可以将未知的干扰观测出来
  • 前端页面小图标不显示问题

    这个问题困扰了我好久 xff0c 主要报错是Origin 39 http localhost 39 is therefore not allowed access 等 起因是我引入bootstrap框架后后端页面的一些小图标不显示 xff0
  • Thinkphp审核功能的实现

    审核功能经过几个小时的奋战终于完成了 xff0c 现在我就与广大网友分享我的成果 我定义未审核为 1 xff0c 审核通过为1 xff0c 审核不通过为0 下面请看HTML代码 lt div class 61 34 table respon
  • Thinkphp修改密码的实现

    密码修改是开发中很基础的一个功能 密码修改的HTML代码如下 span span lt form method 61 post class 61 form horizo ntal action 61 gt span span span st
  • 二维数组的输入和输出

    二维数组我知道的有两种方法 第一种方法是平时常见的方法 xff0c 用两个循环 xff0c 例如 for i 61 0 i lt 61 n 1 i 43 43 for j 61 0 j lt 61 n 1 j 43 43 cin gt gt
  • 怎样把网站前端页面扒取

    在网上经常看到一些很好看的页面 xff0c 这些页面其实都可以把代码扒取下来的 xff0c 可以用浏览器的另存为 xff0c 也有一些相应的软件 浏览器扒取 以火狐为例 右键鼠标点击网页另存为 然后保存即可 软件扒取 这种扒取的软件有很多种
  • 算法题1

    假设有这样一个国家 xff0c 其法律规定当公民月收入为x时 xff0c 若x gt 1 则每月应当缴纳的税金为x的因数中除了x之外的最大值 同时该国法律允许公民将月收入分成若干部分 每部分均为整数 xff0c 要求每部分收入都大于1 xf
  • 3.4日期处理

    include lt iostream gt include lt cstdio gt 平年和闰年的每月的天数 int month 13 2 61 0 0 31 31 28 29 31 31 30 30 31 31 30 30 31 31
  • 关于STL和Boost的理解

    xff11 xff0e STL STL是standard Template Library即标准模板库的英文缩写 xff0c 是惠普实验室开发的一系列软件的统称 从根本上讲 STL是一些 容器 的集合 xff0c 这些容器有list vec
  • Ubuntu 各版本号和名称对照

    版本开发代号中译发布日期支持结束时间内核版本桌面版服务器版4 10Warty Warthog多疣的疣猪2004 10 202006 04 302 6 85 04Hoary Hedgehog白发的刺猬2005 04 082006 10 312
  • 【无标题】安装ROS E: 无法定位软件包 ros-melodic-desktop-full

    一 遇到问题 二 可能的原因和解决方法 1 源换一下 xff1a xff08 1 xff09 我是看这位大佬的 5条消息 记录 解决Ubuntu安装ros报错E Unable to locate package ros kinetic de
  • 无线通信原理及协议栈(ZigBee、蓝牙等)解析

    1 天线 说起无线电通信 xff0c 不可不提起天线 在无线电设备中 xff0c 用来辐射和接收无线电波的装置称为天线 在发射端 xff0c 发射机产生的已调制的高频振荡电流 xff08 能量 xff09 经馈电 xff08 指被控制装置向
  • 串口Serial连接方式

    串口Serial连接方式 1 协议终端选择Serial 2 会话选项 xff0c 选择 串行 3 进入电脑 设备管理器 xff0c 查看USB Serial Port以及端口设置 串行选项根据端口设置配置 确定并连接即可
  • tcp/ip 协议栈实现2-socket文件系统

    core initcall sock init net socket c static int init sock init void int err Initialize sock SLAB cache sk init Initializ
  • VMware虚拟机安装Windows11(无需设置TPM密码)

    VMware虚拟机安装Windows11 xff08 无需设置TPM密码 xff09 注意 xff1a 需要新版VMware xff0c 目前小白的版本为 16 2 3 一 新建虚拟机向导 1 新建虚拟机 点击菜单栏文件 新建虚拟机 2 配
  • ROS相关:使用rospy 编写ros程序并使用rosbag存储数据

    为什么使用rospy ROS支持C 43 43 和Python xff0c 由于ROS的底层是由C 43 43 编写 xff0c 因此大多数的ROS程序都使用C 43 43 xff0c 但是Python语言接口简单 xff0c 更容易编写
  • C函数调用过程

    这几天在看GCC Inline Assembly xff0c 在C代码中通过asm或 asm 嵌入一些汇编代码 xff0c 如进行系统调用 xff0c 使用寄存器以提高性能能 xff0c 需要对函数调用过程中的堆栈帧 xff08 Stack