当前位置: 首页 > news >正文

01:C语言的本质

C语言的本质

  • 1、ARM架构与汇编
  • 2、局部变量初始化与空间分配
    • 2.1、局部变量的初始化
    • 2.1、局部变量数组初始化
  • 3、全局变量/静态变量初始化化与空间分配
  • 4、堆空间
  • 5、函数

1、ARM架构与汇编

ARM简要架构如下:CPU,ARM(能读能写),Flash(能读,写比较麻烦)。
在这里插入图片描述

2、局部变量初始化与空间分配

2.1、局部变量的初始化

CPU寄存器如下
在这里插入图片描述
CPU中的特殊寄存器
SP:栈空间地址指针
LR:正在执行的函数返回地址
PC:保存Flash的代码段的机器码地址,保存CPU正在执行的机器码地址。

执行如下代码时,单片机内部是怎样执行操作的?

int main()
{volatile int a = 10;volatile int b = 20;a = a+b;return 0;
}

C语言代码被编译为单片机能识别的机器码后,烧录进入单片机的Flash的代码段
       在这里插入图片描述

如下为c代码转换的汇编码和机器码

0x08000138 B50C      PUSH     {r2-r3,lr}5:     volatile int a = 10; 
0x0800013A 200A      MOVS     r0,#0x0A
0x0800013C 9001      STR      r0,[sp,#0x04]6:     volatile int b = 20; 
0x0800013E 2014      MOVS     r0,#0x14
0x08000140 9000      STR      r0,[sp,#0x00]7:     a = a+b; 
0x08000142 E9DD1000  LDRD     r1,r0,[sp,#0]
0x08000146 4408      ADD      r0,r0,r1
0x08000148 9001      STR      r0,[sp,#0x04]8:         return 0; 
0x0800014A 2000      MOVS     r0,#0x009: } 
0x0800014C BD0C      POP      {r2-r3,pc}
常见的汇编指令:
PUSH:压栈,一般情况将CPU的寄存器压入RAM栈空间例如:PUSH  {r2-r3,lr}。表示将lr,r3,r2压入栈空间
MOVS:赋值,给CPU的寄存器赋值例如:MOVS  r0,#0x0A。表示给r0寄存器赋值0x0A
STR:写入数据4个字节,将CPU的寄存器数据写入栈空间里面例如:STR   r0,[sp,#0x00]。表示将r0的数据写入地址为sp + 0x00的空间
STRB:写入数据1个字节
LDRD:读取8个字节数据,将栈空间的数据读取到CPU的寄存器里面例如:LDRD  r1,r0,[sp,#0]。表示将sp+0x00地址的数据读取到r0,将sp+0x04地址数据读取到r1
LDR:读取4个字节的数据
ADD:做加法, 例如:ADD   r0,r0,r1。表示将r0 = r0 + r1
SUB:做减法例如:SUB   sp,sp,#0x68。表示将sp = sp - 0x68
POP:出栈,将CPU的寄存器退出栈空间,用于栈空间的释放。例如:POP {r2-r3,pc}。表示将r2,r3,pc对应的栈空间释放。

PUSH {r2-r3,lr}。表示依次将寄存器lr,r3,r2中的数据压入栈的空间里面。而压栈的同时,sp也会随着压栈而改变。
在这里插入图片描述

【注】lr寄存器里面的数据是返回地址,即在执行main函数之前,将ENDP的地址保存在lr中。
在这里插入图片描述
如图:PUSH {r2-r3,lr}此汇编对应的机器码为0x08000014 B50C,当单片机执行完此机器码后,lr,r3,r2的寄存器的值被保存到RAM的栈区空间里面。而sp(栈空间地址光标)会指向地址0x2000 FFF4。
【注】此时的r2和r3寄存器的值为空。
在这里插入图片描述

volatile int a = 10对应的汇编:MOVS r0, #0x0A。表示将0x0A移入r0寄存器
                 STR r0, [sp,#0x04]。表示将r0的数据写入(sp + 0x04)的地址存储空间。sp = 0x2000 FFF4,则sp + 4 = 0x2000 FFF8。所以将r0的数据写入到栈空间的r3的位置。
在这里插入图片描述在这里插入图片描述在这里插入图片描述

【注】0x2000 FFF8为什么代表r3的位置,而不是代表r2的位置喃?一般情况下一个存储空间是以较小的那个地址表示的
在这里插入图片描述

在这里插入图片描述


volatile int b = 20对应的汇编:MOVS r0, #0x14。表示将0x0A移入r0寄存器
                 STR r0, [sp,#0x00]。表示将r0的数据写入(sp + 0x00)的地址存储空间。sp = 0x2000 FFF4,则sp + 0 = 0x2000 FFF4。所以将r0的数据写入到栈空间的r2的位置。
在这里插入图片描述

在这里插入图片描述在这里插入图片描述


a = a + b对应的汇编:LDRD r1, r0, [sp,#0]。从栈区读取2个数据到r0,r1寄存器中。读取的起始地址为sp + 0 = 0x2000 FFF4。(r0接收地址sp + 0x00空间的数据,r1接收地址sp + 0x04空间的数据)即将b/0x14读取到r0,将a/0x0A读取到r1。
             ADD r0, r0, r1。表示将r1的数据加上r0的数据赋值r0。即r0 = 0x14 + 0x0A = 0x1E
            STR r0, [sp,#0x04]。表示将r0的数据写入(sp + 0x04)的地址存储空间。sp = 0x2000 FFF4,则sp + 0x04 = 0x2000 FFF8。所以将r0的数据写入到栈空间的r3的位置。

在这里插入图片描述

在这里插入图片描述
最终调试结果如下:
在这里插入图片描述



return 0;对应的汇编:MOVS r0,#0x00。表示将r0寄存器的数据清零。

栈的回收对应的汇编:POP {r2-r3,pc}。从栈中恢复寄存器 r2、r3 和 pc所对应栈空间的值,并且会自动调整栈指针 sp。最终sp指向0x20010000。表示之前使用的栈空间被回收。
【注】①低标号寄存器在栈空间对应低地址。进栈出栈都是。所以r2在栈空间的下面。②压栈时,先压进去sp在向下移动;出栈时,先出栈,sp在向上移动。

综上:c语言的代码本质就是被编译器转换后的机器码(指令),然后存储在Flash的代码段里面。一个机器码占用2个字节的空间。上面的main函数的代码转换为机器码占用空间地址为:0x08000138(PUSH开始) ~0x0800014C(POP结束),共占用20字节。main函数的地址就是机器码的首地址:0x08000138

2.1、局部变量数组初始化

执行如下代码时,单片机内部是怎样执行操作的?

int main()
{volatile int a = 10;volatile char b[100];b[99] = 20;return 0;
}

如下为c代码转换的汇编码和机器码

0x08000014 B09A      SUB      sp,sp,#0x6817:     volatile int a = 10; 18:     volatile char b[100]; 
0x08000016 200A      MOVS     r0,#0x0A
0x08000018 9019      STR      r0,[sp,#0x64]19:     b[99] = 20; 
0x0800001A 2014      MOVS     r0,#0x14
0x0800001C F88D0063  STRB     r0,[sp,#0x63]20:         return 0; 
0x08000020 2000      MOVS     r0,#0x00

SUB sp,sp,#0x68。表示sp = sp - 0x68。则sp = 0x2000 FFFC - 0x68 = 0x2000 FF98。其中0x68 = 104。则表示在栈区开辟了104个字节
在这里插入图片描述
在这里插入图片描述

3、全局变量/静态变量初始化化与空间分配

#include "main.h"volatile int g_a = 123;//全局变量
int main()
{static volatile int g_b = 321;//静态变量volatile int a = 10;volatile int b = 20;a = a+b;g_b = g_a + g_b;return 0;
}

如上代码包含g_a全局变量,g_b静态变量。如下为c代码转换的汇编码和机器码

0x08000154 B50C      PUSH     {r2-r3,lr}7:     volatile int a = 10; 
0x08000156 200A      MOVS     r0,#0x0A
0x08000158 9001      STR      r0,[sp,#0x04]8:     volatile int b = 20; 
0x0800015A 2014      MOVS     r0,#0x14
0x0800015C 9000      STR      r0,[sp,#0x00]9:     a = a+b; 
0x0800015E E9DD1000  LDRD     r1,r0,[sp,#0]
0x08000162 4408      ADD      r0,r0,r1
0x08000164 9001      STR      r0,[sp,#0x04]10:     g_b = g_a + g_b; 
0x08000166 4804      LDR      r0,[pc,#16]  ; @0x08000178
0x08000168 6800      LDR      r0,[r0,#0x00]
0x0800016A 4904      LDR      r1,[pc,#16]  ; @0x0800017C
0x0800016C 6809      LDR      r1,[r1,#0x00]
0x0800016E 4408      ADD      r0,r0,r1
0x08000170 4902      LDR      r1,[pc,#8]  ; @0x0800017C
0x08000172 6008      STR      r0,[r1,#0x00]11:         return 0; 
0x08000174 2000      MOVS     r0,#0x0012: } 
0x08000176 BD0C      POP      {r2-r3,pc}

综上:并未有机器码和汇编代码来初始化全局变量和静态变量。那么在内存中他们是怎样被初始化赋值的喃?
答案:将全局变量和局部变量需要被初始化的值保存在Flash的数据段里面。有多少个数据,在数据段里面就有多少个数据
在这里插入图片描述
有了数据,那全局变量和局部变量的内存又在哪里喃?又怎样将数据给到全局变量和局部变量喃?

答案:全局变量和静态变量依旧保存在RAM的里面,但不在是栈区。全局变量/静态变量由编译器分配的存储空间,不再是像局部变量由代码指令分配。如下图所示:Linker(链接器):将0x0800 0000的空间与0x2000 0000的空间链接在一起。

在这里插入图片描述
如上图:R/O base:0x0800 0000。表示的是Flash的数据段的起始地址。
在这里插入图片描述

R/W base:0x0200 0000。表示的是RAM中保存全局变量和静态变量的起始地址。
在这里插入图片描述
综上:
①全局变量/局部静态变量赋值和栈里面的局部变量不同,全局变量是先占用低地址空间,而局部变量是先占用高地址空间。

②全局变量是通过copy函数,将Flash里面的数据复制到全局变量和静态变量的内存里面。
③当 main 函数执行完毕时,虽然栈上的局部变量会被销毁,但是全局变量不会受到影响。全局变量在整个程序运行期间都存在,直到程序退出时才会被操作系统回收

【注】copy函数在启动文件里面,由程序员编写,且在调用main函数之前。调用完copy函数后在执行main函数。全局变量在程序启动时分配内存和初始化值,并在整个程序运行期间都保持有效。
在这里插入图片描述

综上为有初始值的全局变量和静态变量的内存分配情况(简称为:RW段),那若没有初始值/初始化为0的全局变量。依然会在Flash的数据段将数据0保存起来吗?显然浪费内存空间。

答案:没有初始值和初始值为0的全局变量,在Flash的数据段里面并未保存数据。但是编译器会在RAM里面给这些变量分配存储空间(简称:ZI段)。在调用main函数之间,调用memset函数将这些变量的存储空间清零。

4、堆空间

综上:①RAM中存在栈区:用于存储局部变量、函数参数、返回地址等。栈内存是自动管理的,随着函数调用和返回而分配和释放。②RAM也存在全局变量/静态局部变量区域。③RAM还存在堆区:堆区由用户调用mallo函数分配和管理,调用free函数进行释放。
在这里插入图片描述
堆区的空间不能在栈区里面分配。因为栈区空间会随着函数的结束而释放,是用户不可控制的。而堆区是不会随着函数的结束而释放。除非main函数终止。

而堆空间可以是全局变量区域。因为都是不会随着函数的结束而释放。除非main函数终止。

在这里插入图片描述

5、函数

综上:c语言的代码本质就是被编译器转换后的机器码(指令),然后存储在Flash的代码段里面。一个机器码占用2个字节的空间。上面的main函数的代码转换为机器码占用空间地址为:0x08000138 ~0x0800014C,共占用20字节。main函数的地址就是机器码的首地址:0x08000138

接下来继续分析如下的代码:

int add_value(volatile int b)
{volatile int c = 20;return c + b;
}int main()
{volatile int a = 10;add_value(a);return 0;
}

生成的机器码和汇编代码如下:

0x08000138 B501      PUSH     {r0,lr}
0x0800013A B081      SUB      sp,sp,#0x045:     volatile int c = 20; 
0x0800013C 2014      MOVS     r0,#0x14
0x0800013E 9000      STR      r0,[sp,#0x00]6:     return c + b; 
0x08000140 E9DD0100  LDRD     r0,r1,[sp,#0]
0x08000144 4408      ADD      r0,r0,r17: } 8:  9: int main() 
0x08000146 BD0C      POP      {r2-r3,pc}10: { 
0x08000148 B508      PUSH     {r3,lr}11:     volatile int a = 10; 
0x0800014A 200A      MOVS     r0,#0x0A
0x0800014C 9000      STR      r0,[sp,#0x00]12:     add_value(a); 
0x0800014E 9800      LDR      r0,[sp,#0x00]
0x08000150 F7FFFFF2  BL.W     0x08000138 add_value13:         return 0; 
0x08000154 2000      MOVS     r0,#0x0014: } 
0x08000156 BD08      POP      {r3,pc}

在这里插入图片描述
综上:main函数的机器码地址0x08000148(PUSH) ~ 0x08000156(POP) 。add_value函数的机器码地址0x08000138(PUSH) ~ 0x08000146(POP)。即:main函数的地址为0x08000148 ,add_value函数的地址为0x08000138 。

在这里插入图片描述


http://www.mrgr.cn/news/82700.html

相关文章:

  • 深入探讨 Android 中的 AlarmManager:定时任务调度及优化实践
  • 第1章:Go语言入门
  • 算法:两个升序单链表的合并
  • 【Linux】揭开套接字编程的神秘面纱(上)
  • 【多线程初阶篇¹】线程理解| 线程和进程的区别
  • Postgresql中clog与xid对应关系计算方法(速查表)
  • 深入探索 Kubernetes:从基础概念到实战运维
  • LLM - 使用 LLaMA-Factory 部署大模型 HTTP 多模态服务 教程 (4)
  • 多模态论文笔记——CogVLM和CogVLM2
  • 毕业项目推荐:基于yolov8/yolov5的行人检测识别系统(python+卷积神经网络)
  • 【Unity3D】UGUI Canvas画布渲染流程
  • TP8 前后端跨域访问请求API接口解决办法
  • 基于海思soc的智能产品开发(camera sensor的两种接口)
  • 【Vim Masterclass 笔记05】第 4 章:Vim 的帮助系统与同步练习(L14+L15+L16)
  • 【C++】B2104 矩阵加法
  • 【MyBatis-Plus 进阶功能】开发中常用场景剖析
  • Markdown中流程图的用法
  • 【C++】P5732 【深基5.习7】杨辉三角
  • 【C++】B2103 图像相似度
  • 算法设计与分析期末
  • 【第二部分--Python之基础】05 类与对象
  • STC单片机 IAP在线升级功能的使用介绍
  • [SMARTFORMS] 输出文本变量绑定
  • 我用Ai学Android Jetpack Compose之Button
  • 算法题(26):最后一个单词的长度
  • Nexus Message Transaction Services(MTS)