手把手教你构建 C 语言编译器.参考.1 -- 程序内存分布与函数调用

写在前面

参考链接

(1) 内存分布 https://www.linuxidc.com/Linux/2017-02/141094.htm

(2) 函数调用 https://www.jianshu.com/p/594357dff57e

本文绝大部分参考以上链接,结合我们要做的C语言编译器,我会对链接中的内容进行精简。

本章目的

(1) 大致了解C程序在运行时的内存分布情况;

(2) 大致了解C程序中调用函数时,汇编指令中做了哪些骚操作。

平台及语言

Visual Studio 2017,windows 7,C/C++

Visual Studio 版本不要求一样,只要能编译C++,能调试就可以了。

1. 内存分布

以下分析只是一个简要版,这里并无意于涵盖所有的C程序执行时的内存分布情况:
在这里插入图片描述

  • text: 代码段,存储机器码
  • data: 初始化数据段,初始化的全局变量,初始化静态变量(static)
  • bbs: 未初始化数据段,未初始化的全局变量,未初始化的静态变量(全0)
  • heap: 堆,malloc,new/realloc,calloc等操作分配的空间
  • stack: 栈,局部变量,由编译器自动分配和释放

需要注意的是:地址的增长方向,特别是栈,栈底是高地址,栈顶是低地址

2. 函数调用

  • PC: 程序计数器,代表下一条指令的地址

在Visual Studio 2017 x86 debug,windows平台下, 使用的是EIP,表示本条指令的地址。

  • ESP: 栈顶指针(base pointer)
  • EBP: 栈底指针(stack pointer)

注意:

(1) ESP和EBP是成对的,可简单理解为 当前执行的函数 对应的有效的栈顶和栈底;

(2) 栈顶指针是有效的,即push入栈的时候,是先ESP- -,然后再 move ESP, XXX。

大致步骤(main中调用function函数),简单起见,我们调用函数时,使用立即数参数,代码如下:

int function(int a, int b)
{
	int m = 0x12345;
	
	return m + a + b;
}

int main()
{
	int sum = 0;
	sum = function(1, 2);

	return 0;
}

我们在 sum = function(1, 2); 这里下一个断点,使用 “调试-反汇编” 功能。

2.1 main 中调用 function

(1) 参数(1, 2)压栈 ( 目前平台是倒序入栈的, 变长参数的这里先不讨论了 )
在这里插入图片描述
(2) 返回地址压栈,跳转
在这里插入图片描述
此时我们可以看到 ESP指针指向的值是 01281fd8,正是返回地址。

注意:
在Windows中,call指令相当于: {PUSH EIP +5, JMP XXX,} 。之所以这里 是 EIP + 5,是因为指令 "call xxxxxxxh"占用5个字节的内存,如下所示(VS可以查看指令长度):
在这里插入图片描述

2.2 function中的操作

2.2.1 刚刚进入函数

注意,此时已经进入函数中,这是在函数function中执行的
在这里插入图片描述
(1) { push ebp },保存上一层函数的ebp;

(2) { mov ebp, esp }, 更新ebp,此时 ebp,esp指向同一个位置,可以发现其实ebp指向的就是上一层函数的ebp;

(3) { sub esp, 0c0h },给函数的临时变量预留位置,操作临时变量的值(或者地址)。

2.2.2 函数体

在这里插入图片描述
可以忽略接下来的那些 "push ebx, push esi"等汇编指令,这个目前和我们讲的没有关系,我们直接看函数体中的汇编指令。

这里执行函数自己的操作,加减乘除等,也就是function里面的

return m + a + b;

当然通过汇编指令,可以知道,这里是先计算 m + a + b。并且把得到的值0x00012348 放到 eax 中.

同时我们可以看见临时变量m的存储的地方就是在栈中,其值为 0x12345。

所以这里再次强调:

(1) 函数参数存放在栈中;

(2) 函数中的临时变量,也存放在栈中。

2.2.3 函数返回

在这里插入图片描述
(1) { add esp, 0C0h }, 这里面是临时变量,add 指令直接让esp往回移动;

题外话:有些函数会返回函数内临时变量的指针,函数执行完后,该指针指向的值已经不可预测了。因为esp往回移动后,该内存视为空闲,之后会被赋何值,不可预料。所以大家不要这么做。

(2) { mov esp, ebp }, 恢复esp 为上一层函数的esp;

(3) { pop ebp },恢复ebp。出栈赋值给 ebp(ebp指向的正是上一层函数时的ebp);

(4) ret ,相当于 pop EIP,在2.1.(2)中,我们将 01281fd8 进栈,此处重新赋值EIP,指向main中的下一条指令地址。

2.3 main中 调用函数后返回

在这里插入图片描述
(1) { add esp, 8 },栈顶是函数参数,这里直接把esp往回移动 8个字节(两个int型参数)。这样也解释了参数其实就是一种拷贝,如果不是指针或者引用,不会对原来的值产生影响;

(2) { move dword ptr[sum], eax },之前的function的返回值0x00012348 放到了eax中,这里将其赋值给 num;

(插一段,我们的C编译器也是把“函数结果以及每个表达式的结果”都放在eax 中。)

2.4 栈示意图

估计上面仅仅是文字和图片,还是会让大家有点糊涂。下面添加一下main 调用function时的栈示意图:在这里插入图片描述

3. 总结

本章节我们主要复习了一下C程序运行时的内存分布情况,再者简单了解了一下C程序调用函数时汇编指令的一些操作。

这为我们后面构建C语言编译器打下了一定的基础。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 学习51单片机和C语言编程,可以帮助我们更深入地理解嵌入式系统的原理和工作方式。对于初学者来说,掌握一份适合自己的学习资料非常重要。 要学习51单片机-C语言版,可以阅读《手把手你学51单片机-C语言版pdf》这本电子书,这本书内容丰富,讲解详细,配合实例编程,非常适合初学者自学。以下是学习本书的几个关键点: 第一,掌握基本的硬件知识,包括单片机的结构和特性,尤其是各种寄存器的作用和配置方法。 第二,了解C语言编程基础,尤其是语法、数据类型、运算符、控制结构、函数等,这是编写单片机程序的基础。 第三,通过实例编程加强对知识的理解和运用能力。例如,可以尝试写一些简单的IO控制、定时器中断、串口通讯等程序。 第四,可以搭配相应的开发板和开发环境进行实践学习。例如,可以使用STC89C51开发板和Keil或SDCC开发环境。 总之,《手把手你学51单片机-C语言版pdf》这本电子书是一个不错的学习资料,但也需要具备一定的基础知识和耐心,可以结合其他资料和实践不断提高自己的能力。 ### 回答2: 学习51单片机-c语言版, 需要基础的C语言编程知识。在学习前,先要熟悉C语言的数据类型、循环、判断及函数等语法结构,并掌握C语言的编写方法。 在学习51单片机-c语言版之前,需要准备好学习环境,如下载并安装Keil软件, 安装并关联好相应的单片机模拟器。Keil软件中有类似于记事本的编辑窗口用来编写C语言代码, 以及编译,调试和下载程序到单片机等功能。 在学习时,可以选择一些简单的例程开始学习,逐步理解其代码逻辑,了解基本的寄存器操作和中断等知识。可以从LED灯等简单的实验开始,逐渐增加难度和功能的复杂度。 同时,可以参考一些权威的学习资料如《单片机原理与应用》、《51单片机学习与应用》等相关书籍,或结合网络资源进行学习。在学习过程中,需要勤加练习,多编写代码进行实践,同时多与他人交流学习体会和技术问题。通过坚持不断的学习和练习,便可以逐步掌握51单片机-c语言版编程技巧,提高自己的单片机应用开发能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值