ARM架构与C语言(韦东山)学习笔记(1)-C语言的本质


前言

学习B站韦东山的ARM架构与C语言的学习笔记。


一、C语言的简单操作

#include "stdio.h"
int main(){
    int a=1;
    a++;
    return 0;
}

为了完成a++这一步,经过了三个步骤:
①读ADDR:CPU读取a这个变量在内存中的地址,得到其中的数据,就是a的值data。CPU读取data后,把data保存到CPU内部的存储结构-寄存器,比如R0中去。

②CPU中的计算单元ALU完成对R0的值的累加操作。

③写ADDR:把R0的值写入a这个变量在内存中的地址。

二、CPU如何知道要执行指令

1.引入FLASH

将代码烧写进FLASH里面,当一上电时,CPU便取出FLASH里的指令执行,如下图以a++为例:
在这里插入图片描述
a++;实际上转化为了汇编语言,首先:
①LDR R0,[addrA]:读地址A的数据到寄存器R0,即加载Load
②ADD R0,#1:寄存器R0的值加一
③STR R0,[addrA]:写R0的值到地址A,即保存Store
使用FLASH是因为他属于ROM,是掉电任然可以保存数据的,所以不会使指令丢失。

KEIL生成的.HEX文件,该文件会被烧写进单片机的FLASH中。

2.cortex-m3内核下寄存器

在这里插入图片描述
(1)通用寄存器(R0-R12):用于调用指令进行数据操作。
(2)堆栈指针寄存器(SP):栈寄存器,用作堆栈指针功能,实现数据的先进后出。
(3)连接寄存器(LR):返回地址寄存器,调用子程序时将返回的地址直接存储在连接寄存器中。
(4)程序计数寄存器(PC):用于存放下一条执行的指令的地址。
(5)特殊功能寄存器组分为三类:1.程序状态字寄存器:用于记录ALU标志执行状态,以及当前正服务的中断号。2.中断屏蔽寄存器:除能中断。3.控制寄存器:定义特权状态,并决定使用哪一个堆栈指针。

三、变量是什么

1.变量——能变的量

变量的最大特点,就是能读能写,这个特性决定了其位于内存中。

变量在内存中存储,是因为内存具有较大的存储容量和较高的读写速度,可以方便地存储和读取程序中的数据。相比之下,寄存器容量较小,只能存储少量数据,而且读写速度较快,但是访问寄存器的速度比访问内存的速度更快。因此,程序中的变量通常会先被存储在内存中,然后被加载到寄存器中进行操作,以提高程序的运行效率。而FLASH中存储的是程序代码和常量数据,不适合存储变量数据,FLASH虽然可读可写,但要对其进行写操作比较复杂。

2.全局变量与局部变量

全局变量:在函数外部定义的变量称为全局变量,它的作用域是从定义它的位置开始,到文件结束为止。全局变量可以被整个程序访问,因此它的生命周期也很长,直到程序结束才会被销毁。全局变量的值可以在程序的任何地方修改,因此需要谨慎使用,避免造成不必要的错误。

局部变量:在函数内部定义的变量称为局部变量,它的作用域仅限于定义它的函数内部。每次函数被调用时,局部变量都会被重新初始化,函数执行完毕后,局部变量的值就会被销毁。局部变量的作用域只在函数内部,因此可以避免变量名冲突和不必要的全局变量。
看代码:

#include "stdio.h"
int add_val(volatile int v){
    volatile int a=321;
    v=v+a;
    return v;
}
int main(){
    static volatile int s_a=1;
    volatile int b;
    b=add_val(s_a);
    return 0;
}

代码段用到了两个关键字,这里进行说明:

①static关键字

对局部变量使用static:在函数内部使用static修饰的变量称为静态局部变量,该变量只会被初始化一次,不会随着函数的退出而销毁,仍然会保持其值,下次函数被调用时,该变量的值不会被重置。静态局部变量的作用域只在函数内部,不会影响其他函数。

对全局变量使用static:在函数外部使用static修饰的变量称为静态全局变量,该变量只能被本文件中的函数访问,其他文件无法访问该变量。静态全局变量的作用域只在本文件内部,不会影响其他文件。

对函数使用static:在函数外部使用static修饰的函数称为静态函数,该函数只能被本文件中的其他函数调用,其他文件无法调用该函数。静态函数的作用域只在本文件内部,不会影响其他文件。

当前文件是指定义该静态变量的源代码文件(.c文件或.cpp文件等)。在该源文件中,静态变量可以在任何函数中被访问,但在其他文件中定义的函数无法访问该变量。这是因为静态变量的作用域被限制在当前源文件范围内,其他源文件无法访问该范围内的变量。

②volatile关键字

volatile是C语言中的一个关键字,用于告诉编译器在编译过程中不要对该变量进行优化,确保每次访问该变量时都从内存中读取数据或者将数据写入内存中,避免编译器对变量进行优化,导致变量的值不符合预期

在多线程或者中断处理程序中,当一个变量被多个线程或者中断处理程序共享时,为了保证数据的一致性,需要使用volatile关键字。因为多个线程或者中断可能会同时访问该变量,如果不使用volatile关键字,编译器可能会对变量进行优化,导致数据的不一致性。

3.函数解析

这里用到了KEIL与STM32的STLINK调试方式,编译并调试程序,可以看到:
在这里插入图片描述

1.volatile int a

对这个无法被编译器优化的局部变量a,会被暂时的保存在栈里。那么什么叫栈呢?

栈(Stack)是一种数据结构,它是一种只能在一端进行插入和删除操作的线性表。栈按照“先进后出”的原则进行操作,即最后插入的元素最先被删除。向栈中插入元素的操作称为“入栈”(Push),从栈中删除元素的操作称为“出栈”(Pop)。

在计算机中,栈通常是指程序运行时使用的一段内存区域,用于存储函数调用时的局部变量、函数参数、返回地址等信息。每当一个函数被调用时,就会在栈上分配一段空间存储这些信息;当函数返回时,这些信息会被弹出栈,栈的指针回到上一个函数的栈顶,继续执行上一个函数。因为栈的特性是后进先出,所以函数调用时,新的函数会先被压入栈中,等到执行完后才会被弹出。
在STM32F103中,存储器结构如下:
在这里插入图片描述在这里插入图片描述
所以开辟栈空间,显然是在内部SRAM区,边界为0x20000000~0x3FFFFFFF基地址0x20000000。SRAM区要存放全局变量、静态变量等,也要开辟一块栈空间来存放局部变量,因此,程序员可以编程来设定栈的起始地址,以此来区别开其他变量。

2.编译器的作用

编译器(Compiler)是一种将源代码翻译成目标代码的程序。它可以将程序员编写的源代码(如C、C++、Java等)转换成计算机能够理解和执行的机器码。编译器通常由多个模块组成,包括预处理器、词法分析器、语法分析器、语义分析器、优化器和代码生成器等。

编译器的主要作用包括:

将源代码转换成目标代码:编译器可以将程序员编写的源代码翻译成计算机能够理解和执行的机器码。这个过程包括词法分析、语法分析、语义分析、优化和代码生成等多个阶段。
优化目标代码:编译器可以对生成的目标代码进行优化,消除冗余代码、提高代码执行效率等,以使程序更快地运行。
检查代码的正确性:编译器可以检查源代码中的语法错误、类型错误等,以提高程序的正确性和可靠性。
提供调试信息:编译器可以在生成的目标代码中添加调试信息,以便程序员在调试程序时进行分析和排错。

也就是说,编译器功能十分强大,我们写的代码会被转化为机器码供CPU处理。

3. 局部变量的分配与初始化

知识点一:
SP(Stack Pointer):栈指针寄存器,指向栈顶地址。在程序执行过程中,栈是用来存储函数调用时的局部变量、函数参数、返回地址等信息的重要数据结构。栈指针SP指向栈顶,栈的大小由程序设定。
LR(Link Register):链接寄存器,用来存储跳转指令的返回地址。当函数被调用时,LR寄存器会保存函数返回时的地址,以便函数执行完毕后返回到正确的地址。
PC(Program Counter):程序计数器,用来存储下一条要执行的指令的地址。在程序执行过程中,PC不断更新,指向下一条要执行的指令,从而使程序能够顺序执行。

知识点二:
char:1字节
short:2字节
int:4字节
long:4字节或8字节(取决于编译器和操作系统)
float:4字节
double:8字节
long double:8字节或16字节(取决于编译器和操作系统)
指针类型:4字节(32位系统)或8字节(64位系统)

修改main函数,添加一个字符型数组,并赋值。

int main(){
    static volatile int s_a=1;
    volatile int b = 456;
    volatile char name[100];
    name[0]='A';
    b=add_val(s_a);
    return 0;
}

(1)如果不对b和name进行赋值,那么编译器会聪明地不为其分配一块栈空间。
(2)

POP {r2-r3, pc}
MOVS r0, r0

POP {r2-r3, pc}这条指令的作用是从栈中弹出4个字节,并将它们分别存储到寄存器r2、r3和程序计数器(pc)中。这条指令通常用于函数返回时,从栈中恢复现场并跳回调用函数的位置。
MOVS r0, r0,这条指令的作用是将寄存器r0的值复制到寄存器r0中。这条指令看起来没有实际作用,但实际上它可以用来清除寄存器r0中的值,因为它将r0的值覆盖为原来的值,相当于不做任何事情。
2.

PUSH {lr}
SUB sp, sp, #0x68

PUSH {lr},这条指令的作用是将链接寄存器(lr)的值压入栈中。这条指令通常用于函数调用时,将函数返回地址保存到栈中,以便在函数执行完毕后返回到正确的位置。
SUB sp, sp, #0x68
这条指令的作用是将栈指针(sp)减去0x68,即在栈顶分配0x68个字节的空间。这条指令通常用于函数栈帧的分配,用于保存函数的局部变量和临时数据。int型变量占用4个字节,而100个字符构成的数组占用100个字节,0x68是10进制的104,因此CPU从栈顶地址如0x20010000减去4,即LR返回地址寄存器的值,然后再减去104,即分配了104个字节的栈空间给两个局部变量。
3.

MOV r0, #0x1C8
STR r0, {sp, #0x00}

MOV r0, #0x1C8
这条指令的作用是将立即数0x1C8(即456的十六进制表示)移动到寄存器r0中。这条指令通常用于将常量加载到寄存器中。

STR r0, {sp, #0x00}
这条指令的作用是将寄存器r0中的值存储到栈指针(sp)加上立即数0x00的地址中。这条指令通常用于将寄存器中的值存储到内存中,这里将r0中的值存储到栈中的第一个位置。就把b的值保存到了内存中的地址中去。

4.局部变量的释放

在这里插入图片描述
同理,执行add_val(volatile int v)函数时,CPU再开辟一块栈,把立即数移动到寄存器r0去,在把r0的值保存到sp指向的地址(sp指针指向的地址是变化的)+自身地址。
而v=v+a这一操作,使用了读-累加-写的操作,不过这里由于a是局部变量,而v是函数形参,相对来说比较复杂。最后,读取r0的值到sp指向的地址。
在这里插入图片描述
这里要注意,PUSH指令将寄存器中的值压入栈中,POP指令从栈中弹出数据并存储到寄存器中,因为执行add_val(volatile int v)函数,CPU会开辟一块栈空间,而函数结束后应该把栈释放掉,便利用pop指令,恢复现场,释放空间,sp寄存器也会指向主函数main保存好的地址处,等待以供下一个函数的调用。

四、cortex-m3的栈

cortex-m3的栈是向下生长的满栈。

向下生长的满栈是一种指在内存中分配的固定大小的栈空间,它从高地址向低地址生长。当栈空间用满时,就会发生栈溢出(Stack Overflow)错误,导致程序崩溃或行为异常。
在向下生长的满栈模型中,栈指针(Stack Pointer)指向当前栈顶,栈顶地址随着栈的使用而不断向下移动。当栈的使用超出了栈空间的大小时,栈顶会越过栈底,覆盖了其他的内存区域,从而导致栈溢出错误。
向下生长的满栈模型通常用于x86架构的计算机中,这种架构采用的是小端字节序(Little-Endian),即将低位字节存储在低地址处,高位字节存储在高地址处。因此,栈空间从高地址向低地址生长,更符合计算机的存储方式。
cortex-m3的栈是向下生长的满栈,是从上向下减小地址,sp寄存器先调整指针位置,再存入操作。
在这里插入图片描述

从PUSH开始,到POP结束。PUSH里先调整sp减4,存入r0。再调整sp减4,存入LR。而POP里,把返回的v的值给r2,sp自加4(就是弹出,指针指向上面的位置了),再把r0的值给r3,sp再自加四,最后把LR的值给PC,就是把主函数进入add函数时LR保存的地址给PC,让CPU进入main函数继续完成操作,sp再自加四,回到栈顶,释放掉了add函数的栈空间。

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值