Error[8]: Undefined offset: 347, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

嵌入式C语言学习记录(一) —— ARM指令集与作用域

version : v1.0 「2022.9.11」 最后补充

author: Y.Z.T.


摘要: 记录汇总自己在嵌入式开发过程中 学习的一些零散知识

简介: 简单汇总,方便自己查看


(👇 第二部分)

嵌入式C语言学习记录(二) —— GNU拓展语法与C语言补充




⭐️ 目录

文章目录



1️⃣ string.h 库函数

下面是头文件 string.h 中定义的函数:

序号函数描述
1void *memchr(const void *str, int c, size_t n在参数 str 所指向的字符串的前 n 个字节中搜索第一次出现字符 c(一个无符号字符)的位置。
2int memcmp(const void *str1, const void *str2, size_t n)str1str2 的前 n 个字节进行比较。
3void *memcpy(void *dest, const void *src, size_t n)从 src 复制 n 个字符到 dest
4void *memmove(void *dest, const void *src, size_t n)另一个用于从 src 复制 n 个字符到 dest 的函数。
5void *memset(void *str, int c, size_t n)复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。
6char *strcat(char *dest, const char *src)src 所指向的字符串追加到 dest 所指向的字符串的结尾。
7char *strncat(char *dest, const char *src, size_t n)src 所指向的字符串追加到 dest 所指向的字符串的结尾,直到 n 字符长度为止。
8char *strchr(const char *str, int c)在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置。
9int strcmp(const char *str1, const char *str2)str1 所指向的字符串和 str2 所指向的字符串进行比较。
10int strncmp(const char *str1, const char *str2, size_t n)str1str2 进行比较,最多比较前 n 个字节。
11int strcoll(const char *str1, const char *str2)str1str2 进行比较,结果取决于 LC_COLLATE 的位置设置
12char *strcpy(char *dest, const char *src)src 所指向的字符串复制到 dest
13char *strncpy(char *dest, const char *src, size_t n)src 所指向的字符串复制到 dest,最多复制 n 个字符。
14size_t strcspn(const char *str1, const char *str2)检索字符串 str1 开头连续有几个字符都不含字符串 str2 中的字符。
15char *strerror(int errnum)从内部数组中搜索错误号 errnum,并返回一个指向错误消息字符串的指针。
16size_t strlen(const char *str)计算字符串 str 的长度,直到空结束字符,但不包括空结束字符。
17char *strpbrk(const char *str1, const char *str2)检索字符串 str1 中第一个匹配字符串 str2 中字符的字符,不包含空结束字符。也就是说,依次检验字符串 str1 中的字符,当被检验字符在字符串 str2 中也包含时,则停止检验,并返回该字符位置。
18char *strrchr(const char *str, int c)在参数 str 所指向的字符串中搜索最后一次出现字符 c(一个无符号字符)的位置。
19size_t strspn(const char *str1, const char *str2)检索字符串 str1 中第一个不在字符串 str2 中出现的字符下标。
20char *strstr(const char *haystack, const char *needle) 在字符串 haystack 中查找第一次出现字符串 needle(不包含空结束字符)的位置。
21char *strtok(char *str, const char *delim)分解字符串 str 为一组字符串,delim 为分隔符。
22size_t strxfrm(char *dest, const char *src, size_t n)根据程序当前的区域选项中的 LC_COLLATE 来转换字符串 src 的前 n 个字符,并把它们放置在字符串 dest 中。



2️⃣ ARM汇编指令集


CPSR寄存器标志位:


2.1 ARM汇编指令

一个完整的ARM指令通常是由 *** 作码 + *** 作数

<opcode> {<cond> {s} <Rd>,<Rn> {,<operand2>}}

格式说明:



2.1.1 *** 作数2(operand2)

*** 作数operand2在汇编程序中经常出现的两种格式如下:

#constant		;  *** 作数是一个立即数
Rm{,shift}		; 使用寄存器值作为 *** 作数

注意:

在第二种格式中,通过{, shift}可选项,我们还可以通过多种移位或循环移位的方式,构建更加灵活的 *** 作数。可选项{, shift}可以选择的移位方式如下:

#constant,n 	;将立即数 constant循环右移n位
ASR #			;算术右移n位, n的取值范围: [1,32]
LSL #n			;逻辑左移n位, n的取值范围: [0,31]
LSR #n			;逻辑右移n位, n的取值范围: [1,32]
ROR #n			;向右循环移n位, n的取值范围: [1,31]
RRX				;向右循环移1位,带扩展
type Rs			;仅在ARM中可用,其中type指ASP、LSL、LSR、ROR, Rs是提供位移量的寄存器名称

示例:

ADD R3, R2, R1, LSL #3		;R3=R2+R1<<3
ADD R3, R2, R1, LSL R0		;R3=R2+R1<



2.2 基本指令 2.2.1 储存访问指令 2.2.1.1 [LDR指令]

格式 : LDR{条件} 目的寄存器,<存储器地址>;

作用 : LDR指令用于从存储器中将一个32位的字数据传送到目的寄存器中。该指令通常用于从存储器中读取32位的字数据到通用寄存器,然后对数据进行处理。当程序计数器PC作为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而可以实现程序流程的跳转。

示例:

LDR  R0,[R1]               ;将存储器地址为R1的字数据读入寄存器R0。
LDR  R0,[R1,R2]           ;将存储器地址为R1+R2的字数据读入寄存器R0。
LDR  R0,[R1,#8]          ;将存储器地址为R1+8的字数据读入寄存器R0。
LDR  R0,[R1,R2] !        ;将存储器地址为R1+R2的字数据读入寄存器R0,并将新地址R1+R2写入R1。
LDR  R0,[R1,#8] !       ;将存储器地址为R1+8的字数据读入寄存器R0,并将新地址R1+8写入R1。
LDR  R0,[R1],R2          ;将存储器地址为R1的字数据读入寄存器R0,并将新地址R1+R2写入R1。
LDR  R0,[R1,R2,LSL#2]! ;将存储器地址为R1+R2×4的字数据读入寄存器R0,并将新地址R1+R2×4写入R1。
LDR  R0,[R1],R2,LSL#2   ;将存储器地址为R1的字数据读入寄存器R0,并将新地址R1+R2×4写入R1。

{!}为可选后缀,若选用该后缀,则当数据传送完毕之后,将最后的地址写入基址寄存器,否则基址寄存器的内容不改变。




2.2.1.2 [STR指令]

注意: LDR/STR指令是ARM汇编中使用频率最高的一对指令

格式 : STR{条件} 源寄存器,<存储器地址>;

作用 : STR指令用于从源寄存器中将一个32位的字数据传送到存储器中。

示例:

STR R0,[R1],#8  	;将R0中的字数据写入以R1为地址的存储器中,并将新地址R1+8写入R1。
STR R0,[R1,#8] 		;将R0中的字数据写入以R1+8为地址的存储器中。




2.2.1.3 [LDRB 指令]

格式 : LDR{条件}B 目的寄存器,<存储器地址>

作用 : LDRB指令用于从存储器中将一个8位的字节数据传送到目的寄存器中,同时将寄存器的高24位清零。
该指令通常用于从存储器中读取8位的字节数据到通用寄存器,然后对数据进行处理。当程序计数器PC作为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而可以实现程序流程的跳转。

示例:

LDRB  R0,[R1]        ;将存储器地址为R1的字节数据读入寄存器R0,并将R0的高24位清零。
LDRB  R0,[R1,#8]    ;将存储器地址为R1+8的字节数据读入寄存器R0,并将R0的高24位清零。



2.2.1.4 [STRB 指令]

格式 : STR{条件}B 源寄存器,<存储器地址>

作用 : STRB指令用于从源寄存器中将一个8位的字节数据传送到存储器中。该字节数据为源寄存器中的低8位。

示例:

STRB  R0,[R1]      ;将寄存器R0中的字节数据写入以R1为地址的存储器中。
STRB  R0,[R1,#8]  ;将寄存器R0中的字节数据写入以R1+8为地址的存储器中。



2.2.1.5 [LDRH / STRH 指令]

格式:

  • STR{条件}H 源寄存器,<存储器地址>
  • LDR{条件}H 目的寄存器,<存储器地址>

作用 :

  • STRH指令用于从源寄存器中将一个16位的半字数据传送到存储器中。该半字数据为源寄存器中的低16位。
  • LDRH指令用于从存储器中将一个16位的半字数据传送到目的寄存器中,同时将寄存器的高16位清零。



2.2.1.6 [LDM / STM 指令]

格式 : LDM(或STM){条件}{类型} 基址寄存器{!},寄存器列表{∧}

作用 :

  • 批量加载/储存指令 , 在一组寄存器和内存间传输数据
  • 常与堆栈格式组合使用 . 以模拟堆栈 *** 作

不同类型的堆栈:


示例:

LDMFD SP!,{R0-R2,R14}    ;将内存栈中的数据依次d出到R14、R2、R1、R0
STMFD SP!,{R0-R2,R14}	 ;将R0、R1、R2、R14依次压入内存栈

注意:

  • 每入栈一个元素, 栈指针SP都会往栈增长的方向移动一个存储单元.
  • 栈是先入后出(FILO) , 所以STMFD指令会根据{ }中的寄存器列表 ,从左到右压入栈
  • LDMFD 指令出栈时,顺序相反

入栈与出栈:


注意:

也可以通过PUSH/POP 指令来执行栈元素的入栈和出栈 *** 作

PUSH {R0-R2, R14}	;将RO、R1、R2、R14依次压入栈,
POP {R0-R2,R14} 	;将中的数据依次d出到R14、R2、R1、R0



2.2.2 数据传送指令 2.2.2.1 [MOV指令]

格式: MOV{条件}{S} 目的寄存器,源 *** 作数

作用 :

  • 是把一个寄存器的值(要能用立即数表示)赋给另一个寄存器,或者将一个常量赋给寄存器,将后边的量赋给前边的量。
  • MOV指令中,条件缺省时指令无条件执行;
  • {S}用来表示是否影响CPSR寄存器的值,如MOVS指令就会影响寄存器CPSR的值,而MOV则不会

示例:

MOV R1,R0   		;将寄存器R0的值传送到寄存器R1
MOV PC,R14   		;将寄存器R14的值传送到PC,常用于子程序返回
MOV R1,R0,LSL#3    ;将寄存器R0的值左移3位后传送到R1(即乘8)
MOVS PC, R14	     ;将寄存器R14的值传送到PC中,返回到调用代码并恢复标志位

2.2.2.2 [MVN 指令]

格式 : MVN{条件}{S} 目的寄存器,源 *** 作数

作用 :

  • MVN指令用来将 *** 作数operand2按位取反后传送到目标寄存器Rd
  • 与MOV指令不同的是 ,在传送之前就按位取反了

示例:

MVN R0,#0xFF   ;将立即数0xFF取反后赋值给R0
MVN R0,R1 	    ;将R1寄存器的值取反后赋值给R0



2.2.3 算术逻辑运算指令 2.2.3.1 算术指令

算术指令包括 基本的加、减、乘、除

格式:

ADD {cond} {S} Rd, Rn, operand2 ;加法
ADC {cond} {S} Rd, Rn, operand2 ;带进位加法
SUB {cond} {S} Rd, Rn, operand2 ;减法
SBC {cond} {S} Rd, Rn, operand2 ;带借位减法

示例:

ADD  R0,R1,R2           ; R0 = R1 + R2
ADD  R0,R1,#256         ; R0 = R1 + 256
ADD  R0,R2,R3,LSL#1     ; R0 = R2 + (R3 << 1)

ADC  R1,R1,#1			 ; R1 = R1 + 1 + C (其中C为CPSR寄存器中的进位) 

SUB  R0,R1,R2           ; R0 = R1 - R2
SUB  R0,R1,#256         ; R0 = R1 - 256
SUB  R0,R2,R3,LSL#1     ; R0 = R2 - (R3 << 1)

;BC指令用于把操作数1减去操作数2,再减去CPSR中的C条件标志位的反码,并将结果存放到目的寄存器中。
SUBS  R0,R1,R2           ;R0 = R1 - R2 - !C,并根据结果设置CPSR的进位标志位



2.2.3.2 逻辑指令

逻辑运算指令包括 (与、或、非、异或、清除等

格式:

AND {cond} {S} Rd, Rn, operand2 ;逻辑与运算(常用于屏蔽 *** 作数1 中的某些位 )
ORR {cond} {S} Rd, Rn, operand2 ;逻辑或运算(常用于设置 *** 作数1的某些位。)
EOR {cond} {S} Rd, Rn, operand2 ;异或运算(常用于反转 *** 作数1的某些位。)
BIC {cond} {S} Rd, Rn, operand2 ;位清除运算

示例:

AND  R0,R0,#3           ; 该指令保持R0的0、1位,其余位清零。   ( R0 = R0 & 0011 )
ORR  R0,R0,#3           ; 该指令设置R0的0、1位,其余位保持不变。( R0 = R0 | 0011)
EOR  R0,R0,#3           ; 该指令反转R0的0、1位,其余位保持不变。( R0 = R0 ^ 0011)
BIC  R0,R0,#%1011       ; 该指令清除 R0 中的位 0、1、和 3,其余的位保持不变。



2.2.4 比较指令 2.2.4.1 [CMP直接比较指令]

格式 : CMP{条件} *** 作数1, *** 作数2

作用 :

  • CMP指令用于把一个寄存器的内容和另一个寄存器的内容或立即数进行比较 , 同时影响CPSR寄存器的N、Z、C、V标志位
  • 该指令进行一次减法运算,但不存储结果,只更改条件标志位。标志位表示的是 *** 作数1与 *** 作数2的关系(大、小、相等)

CPSR寄存器标志位:

示例:

CMP R1,R0     ;将寄存器R1的值与寄存器R0的值相减,并根据结果设置CPSR的标志位
CMP R1,#100  ;将寄存器R1的值与立即数100相减,并根据结果设置CPSR的标志位

注意:

  • 比较指令的运行结果Z=1时,表示运算结果为零,两个数相等;

  • N=1表示运算结果为负,

  • N=0表示运算结果为非负,即运算结果为正或者为零。



2.2.4.2 [CMN负数比较指令]

格式: CMN{条件} *** 作数1, *** 作数2

作用: CMN指令用于把一个寄存器的内容和另一个寄存器的内容或立即数取反后进行比较,同时更新CPSR中条件标志位的值。

示例:

CMN R0, #1			;将立即数取负,然后比较大小


2.2.5 跳转指令 2.2.5.1 [B label 指令]

格式 : B {cond} Label

作用:

  • 程序无条件跳转到标号Label处执行,
  • 它是 24 位有符号数,左移两位后有符号扩展为 32 位. 表示跳转范围[0 , 32MB]
  • 无条件跳转指令B主要用在循环 , 分支结构

示例:

B  Label  	;程序无条件跳转到标号Label处执行
CMP R1,#0  ;当CPSR寄存器中的Z条件码置位时,程序跳转到标号Label处执行
BEQ Label  


2.2.5.2 [BL 带链接的跳转]

格式 : BL{条件} 目标地址

作用 :

  • 但跳转之前,会在寄存器RL(即R14)中保存PC的当前内容
  • BL指令一般用在函数调用的场合

示例:

BL Label  ;当程序无条件跳转到标号Label处执行时,同时将当前的PC值保存到R14中
...		  ; 子程序返回后接着从此处继续执行


2.2.5.3 [BX 带状态切换的跳转]

格式 : BX{条件} 目标地址

作用 : BX指令跳转到指令中所指定的目标地址,目标地址处的指令既可以是ARM指令,也可以是Thumb指令。



2.2.5.4 [BLX 指令]

格式 : BX{条件} 目标地址

作用: BLX指令是BL指令和BX指令的综合 , 表示带链接和状态切换的跳转



2.2.6 条件执行指令

为了提高代码密度,减少ARM指令的数量,几乎所有的ARM指令都可以根据CPSR寄存器中的标志位,通过指令组合实现条件执行。

如:

  • 无条件跳转指令B,我们可以在后面加上条件码组成BEQ、BNE组合指令。
  • BEQ指令表示两个数比较,结果相等时跳转;
  • BNE指令则表示结果不相等时跳转

ARM 指令的条件码:


示例:

通过循环结构,我们可以实现数据块的搬运功能。我们可以将无条件跳转指令B和条件码NE组合在一起使用,构成一个循环程序结构。

AREA COPY,CODE,READOLY
	ENTRY
START
	LDR R0,=SRC	; 源地址
	LDR R1,=DST	; 目的地址
	MOV R2,=#10	; 复制循环次数
LOOP
	LDR R3,[R0],#4 ;
	STR R3,[R1],#4 ;
	SUBS R2,R2,#4  ; 
	BNE LOOP	   ;
	
AREA COPYDATA, DATA, READWRITE
SRC DCD 1,2,3,4,5,6,7,8,9,0
DST DCD 0,0,0,0,0,0,0,0,0,0
	END	


2.3 伪指令

伪指令有点类似C语言中的预处理命令,在程序编译时,这些伪指令会被翻译为一条或多条ARM标准指令。常见的ARM伪指令主要有4个: ADR、ADRL、LDR、NOP,它们的使用示例如下:

ADR RO, LOOP		;将标号LOOP的地址保存到R0寄存器中
ADRL RO, LOOP		;中等范围的地址读取
LDR R0, =9x30008000 ;将内存地址0x30008000赋值给R0(主要用途是将一个32位内存地址保存到寄存器中)
NOP					;空 *** 作,用于延时或插入流水线中暂停指令的运行(相当于"MOV R0 R0")

备注: 为什么要用LDR 伪指令将一个32位内存地址保存到寄存器中

  • 因为指令的长度一般都是固定的。在一个32位的系统中,一条指令通常是32位的,
  • 指令中包括 *** 作码和 *** 作数
  • 指令中的 *** 作码和 *** 作数共享32位的存储空间;
  • 一般前面的 *** 作码要占据几个比特位,剩下来的留给 *** 作数的编码空间就小于32位

ARM指令的编码格式:



2.4 伪 *** 作

为了编程方便,汇编器也定义了一些特殊的指令助记符,以方便对汇编程序做各种处理。如使用AREA来定义一个段(section) ,使用GBLA来定义一个数据,使用ENTRY来指定汇编程序的执行入口等,这些指令助记符统称为伪 *** 作。


常用伪 *** 作:



2.4.1 [AREA]

语法格式 : [语法格式:AREA 段名 属性 1 ,属性 2 ,…  ]

作用: AREA 伪指令用于定义一个代码段或数据段。其中,段名若以数字开头,则该段名需用 “ | ” 括起来,如 |1_test| 。


属性字段表示该代码段(或数据段)的相关属性,多个属性用逗号分隔。常用的属性如下:

  • CODE 属性:用于定义代码段,默认为 READONLY 。
  • DATA 属性:用于定义数据段,默认为 READWRITE 。
  • READONLY 属性:指定本段为只读,代码段默认为 READONLY 。
  • READWRITE 属性:指定本段为可读可写,数据段的默认属性为 READWRITE 。
  • ALIGN 属性:使用方式为ALIGN 表达式。在默认时,ELF(可执行连接文件)的代码段和数据段是按字对齐的,表达式的取值范围为 0 ~31,相应的对齐方式为2表达式次方。
  • COMMON 属性:该属性定义一个通用的段,不包含任何的用户代码和数据。各源文件中同名的 COMMON 段共享同一段存储单元。

使用示例:

AREA Init , CODE , READONLY     ;该伪指令定义了一个代码段,段名为 Init ,属性为只读。 


2.4.2 [ALIGN]

语法格式: [ALIGN { 表达式 { ,偏移量 }}   ]

作用 : 地址对齐

  • ALIGN 伪指令可通过添加填充字节的方式,使当前位置满足一定的对齐方式。
  • 其中,表达式的值用于指定对齐方式,可能的取值为2的幂,如 1 、2 、4 、8 、16 等。
  • 若未指定表达式,则将当前位置对齐到下一个字的位置。
  • 偏移量也为一个数字表达式,若使用该字段,则当前位置的对齐方式为:2的表达式次幂+偏移量。

使用示例:

AREA Init,CODE ,READONLY,ALIEN=3	;指定后面的指令为 8 字节对齐。      
....

.... 
END      


2.4.3 [CODE16 / CODE32]

语法格式: CODE16 (或 CODE32 )

作用 :

  • CODE16 伪指令通知编译器,其后的指令序列为 16 位的 Thumb 指令。
  • CODE32 伪指令通知编译器,其后的指令序列为 32 位的 ARM 指令。
  • 在使用 ARM 指令和 Thumb 指令混合编程的代码里,可用这两条伪指令进行切换,但注意他们只通知编译器其后指令的类型,并不能对处理器进行状态的切换。

使用示例:

AREA Init ,CODE ,READONLY            
....      
CODE32 				;通知编译器其后的指令为 32 位的 ARM 指令            
LDR R0,=NEXT+1 	  ;将跳转地址放入寄存器 R0      
BX R0 				;程序跳转到新的位置执行,并将处理器切换到 Thumb 工作状态      
....     
CODE16 				;通知编译器其后的指令为 16 位的 Thumb 指令            
NEXT LDR R3,=0x3FF            
....     
END 				;程序结束         


2.4.4 [ENTRY]

语法格式 :  ENTRY

作用:

  • ENTRY 伪指令用于指定汇编程序的入口点。
  • 在一个完整的汇编程序中至少要有一个 ENTRY (也可以有多个)
  • 当有多个 ENTRY 时,程序的真正入口点由链接器指定),但在一个源文件里最多只能有一个 ENTRY (可以没有)。

使用示例:

AREA Init , CODE , READONLY            
ENTRY 				;应用程序的入口点
.....   


2.4.5 [END]

语法格式 :  END

作用: END 伪指令用于通知编译器已经到了源程序的结尾。

使用示例:

AREA Init , CODE , READONLY            
......     
END 			;指定应用程序的结尾



3️⃣ 作用域
3.1 头文件的内容
  • 函数原型
  • 使用#defineconst定义的符号常量
  • 结构声明
  • 类声明
  • 模版声明
  • 内联函数

注意: 不要将函数定义和变量声明放在头文件中,容易导致重定义。 除非该函数是内联函数



3.2 #inlcude 中“ ” 和 < >的区别



3.3 内存在程序中保留的时间
  • 自动储存:局部变量、函数参数 在程序开始执行其所属的函数和代码块时被创建, 在执行完函数和代码块时,内存被释放
  • 静态储存:全局变量、staic关键字定义的局部变量和全局变量 。在整个程序运行过程中都存在。
  • 动态存储: 用new 分配的内存会一直存在,直到使用delete 关键字将其释放。 也称为堆



3.4 作用域和链接
  • 作用域: 值在上面范围内能看到这个(函数/变量 ),描述了名称在文件的多大范围内可见。
  • 链接性: 描述了名称在不同单元键的共享

局部变量作用域: 作用域只在定义它的代码块中

全局变量作用域: 作用域为定义位置到文件结尾

自动变量作用域: 作用域为局部

函数体作用域: 整个类或整个名称空间,但不能是局部的


[例如: 局部变量和函数形参的 储存持续性为自动,作用域为局部; 没有链接性。 当程序开始执行时,为该变量分配内存,当函数结束时,这些变量会消失。]




注意:



3.5 静态储存的链接性 3.5.1 静态变量的链接性:
  • 外部链接性: 可在其他文件中访问
  • 内部链接性: 只能在当前文件中访问
  • 无链接性: 只能在当前函数或代码块中访问

静态变量的数量在程序运行期间是不变的, 编译器会分配固定的内存块来储存所有的静态变量,这些变量在程序

执行期间一直存在。


 int a = 0;    // 静态持续变量,连接性为外部
 
 staic int b = 0; // 静态持续变量,连接性为内部
 
 void funct(void)
 {
     staic int c = 0;        //静态变量,无链接性
     int d = 0;              // 自动变量,无链接性
 }    

特别注意: 变量c是储存在静态数据区的,会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。

即使funct函数没有被执行,c变量也是留在内存中的,而变量d则会消失。



3.5.2 静态变量的初始化特性

所有静态持续变量在初始化时都会被初始化为0 ,称为零初始化 。

在静态数据区,内存中所有的字节默认值都是 0x00,某些时候这一特点可以减少程序员的工作量。比如初始化一个稀疏矩阵,我们可以一个一个地把所有元素都置 0,然后把不是 0 的几个元素赋值。如果定义成静态的,就省去了一开始置 0 的 *** 作。再比如要把一个字符数组当字符串来用,但又觉得每次在字符数组末尾加 volatile 太麻烦。如果把字符串定义成静态的,就省去了这个麻烦,因为那里本来就是 [+++] 。



3.6 变量储存方式总结



3.7 [+++] 限定符



)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 348, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

嵌入式C语言学习记录(一) —— ARM指令集与作用域

version : v1.0 「2022.9.11」 最后补充

author: Y.Z.T.


摘要: 记录汇总自己在嵌入式开发过程中 学习的一些零散知识

简介: 简单汇总,方便自己查看


(👇 第二部分)

嵌入式C语言学习记录(二) —— GNU拓展语法与C语言补充




⭐️ 目录

文章目录



1️⃣ string.h 库函数

下面是头文件 string.h 中定义的函数:

序号函数描述
1void *memchr(const void *str, int c, size_t n在参数 str 所指向的字符串的前 n 个字节中搜索第一次出现字符 c(一个无符号字符)的位置。
2int memcmp(const void *str1, const void *str2, size_t n)str1str2 的前 n 个字节进行比较。
3void *memcpy(void *dest, const void *src, size_t n)从 src 复制 n 个字符到 dest
4void *memmove(void *dest, const void *src, size_t n)另一个用于从 src 复制 n 个字符到 dest 的函数。
5void *memset(void *str, int c, size_t n)复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。
6char *strcat(char *dest, const char *src)src 所指向的字符串追加到 dest 所指向的字符串的结尾。
7char *strncat(char *dest, const char *src, size_t n)src 所指向的字符串追加到 dest 所指向的字符串的结尾,直到 n 字符长度为止。
8char *strchr(const char *str, int c)在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置。
9int strcmp(const char *str1, const char *str2)str1 所指向的字符串和 str2 所指向的字符串进行比较。
10int strncmp(const char *str1, const char *str2, size_t n)str1str2 进行比较,最多比较前 n 个字节。
11int strcoll(const char *str1, const char *str2)str1str2 进行比较,结果取决于 LC_COLLATE 的位置设置
12char *strcpy(char *dest, const char *src)src 所指向的字符串复制到 dest
13char *strncpy(char *dest, const char *src, size_t n)src 所指向的字符串复制到 dest,最多复制 n 个字符。
14size_t strcspn(const char *str1, const char *str2)检索字符串 str1 开头连续有几个字符都不含字符串 str2 中的字符。
15char *strerror(int errnum)从内部数组中搜索错误号 errnum,并返回一个指向错误消息字符串的指针。
16size_t strlen(const char *str)计算字符串 str 的长度,直到空结束字符,但不包括空结束字符。
17char *strpbrk(const char *str1, const char *str2)检索字符串 str1 中第一个匹配字符串 str2 中字符的字符,不包含空结束字符。也就是说,依次检验字符串 str1 中的字符,当被检验字符在字符串 str2 中也包含时,则停止检验,并返回该字符位置。
18char *strrchr(const char *str, int c)在参数 str 所指向的字符串中搜索最后一次出现字符 c(一个无符号字符)的位置。
19size_t strspn(const char *str1, const char *str2)检索字符串 str1 中第一个不在字符串 str2 中出现的字符下标。
20char *strstr(const char *haystack, const char *needle) 在字符串 haystack 中查找第一次出现字符串 needle(不包含空结束字符)的位置。
21char *strtok(char *str, const char *delim)分解字符串 str 为一组字符串,delim 为分隔符。
22size_t strxfrm(char *dest, const char *src, size_t n)根据程序当前的区域选项中的 LC_COLLATE 来转换字符串 src 的前 n 个字符,并把它们放置在字符串 dest 中。



2️⃣ ARM汇编指令集


CPSR寄存器标志位:


2.1 ARM汇编指令

一个完整的ARM指令通常是由 *** 作码 + *** 作数

<opcode> {<cond> {s} <Rd>,<Rn> {,<operand2>}}

格式说明:



2.1.1 *** 作数2(operand2)

*** 作数operand2在汇编程序中经常出现的两种格式如下:

#constant		;  *** 作数是一个立即数
Rm{,shift}		; 使用寄存器值作为 *** 作数

注意:

在第二种格式中,通过{, shift}可选项,我们还可以通过多种移位或循环移位的方式,构建更加灵活的 *** 作数。可选项{, shift}可以选择的移位方式如下:

#constant,n 	;将立即数 constant循环右移n位
ASR #			;算术右移n位, n的取值范围: [1,32]
LSL #n			;逻辑左移n位, n的取值范围: [0,31]
LSR #n			;逻辑右移n位, n的取值范围: [1,32]
ROR #n			;向右循环移n位, n的取值范围: [1,31]
RRX				;向右循环移1位,带扩展
type Rs			;仅在ARM中可用,其中type指ASP、LSL、LSR、ROR, Rs是提供位移量的寄存器名称

示例:

ADD R3, R2, R1, LSL #3		;R3=R2+R1<<3
ADD R3, R2, R1, LSL R0		;R3=R2+R1<



2.2 基本指令 2.2.1 储存访问指令 2.2.1.1 [LDR指令]

格式 : LDR{条件} 目的寄存器,<存储器地址>;

作用 : LDR指令用于从存储器中将一个32位的字数据传送到目的寄存器中。该指令通常用于从存储器中读取32位的字数据到通用寄存器,然后对数据进行处理。当程序计数器PC作为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而可以实现程序流程的跳转。

示例:

LDR  R0,[R1]               ;将存储器地址为R1的字数据读入寄存器R0。
LDR  R0,[R1,R2]           ;将存储器地址为R1+R2的字数据读入寄存器R0。
LDR  R0,[R1,#8]          ;将存储器地址为R1+8的字数据读入寄存器R0。
LDR  R0,[R1,R2] !        ;将存储器地址为R1+R2的字数据读入寄存器R0,并将新地址R1+R2写入R1。
LDR  R0,[R1,#8] !       ;将存储器地址为R1+8的字数据读入寄存器R0,并将新地址R1+8写入R1。
LDR  R0,[R1],R2          ;将存储器地址为R1的字数据读入寄存器R0,并将新地址R1+R2写入R1。
LDR  R0,[R1,R2,LSL#2]! ;将存储器地址为R1+R2×4的字数据读入寄存器R0,并将新地址R1+R2×4写入R1。
LDR  R0,[R1],R2,LSL#2   ;将存储器地址为R1的字数据读入寄存器R0,并将新地址R1+R2×4写入R1。

{!}为可选后缀,若选用该后缀,则当数据传送完毕之后,将最后的地址写入基址寄存器,否则基址寄存器的内容不改变。




2.2.1.2 [STR指令]

注意: LDR/STR指令是ARM汇编中使用频率最高的一对指令

格式 : STR{条件} 源寄存器,<存储器地址>;

作用 : STR指令用于从源寄存器中将一个32位的字数据传送到存储器中。

示例:

STR R0,[R1],#8  	;将R0中的字数据写入以R1为地址的存储器中,并将新地址R1+8写入R1。
STR R0,[R1,#8] 		;将R0中的字数据写入以R1+8为地址的存储器中。




2.2.1.3 [LDRB 指令]

格式 : LDR{条件}B 目的寄存器,<存储器地址>

作用 : LDRB指令用于从存储器中将一个8位的字节数据传送到目的寄存器中,同时将寄存器的高24位清零。
该指令通常用于从存储器中读取8位的字节数据到通用寄存器,然后对数据进行处理。当程序计数器PC作为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而可以实现程序流程的跳转。

示例:

LDRB  R0,[R1]        ;将存储器地址为R1的字节数据读入寄存器R0,并将R0的高24位清零。
LDRB  R0,[R1,#8]    ;将存储器地址为R1+8的字节数据读入寄存器R0,并将R0的高24位清零。



2.2.1.4 [STRB 指令]

格式 : STR{条件}B 源寄存器,<存储器地址>

作用 : STRB指令用于从源寄存器中将一个8位的字节数据传送到存储器中。该字节数据为源寄存器中的低8位。

示例:

STRB  R0,[R1]      ;将寄存器R0中的字节数据写入以R1为地址的存储器中。
STRB  R0,[R1,#8]  ;将寄存器R0中的字节数据写入以R1+8为地址的存储器中。



2.2.1.5 [LDRH / STRH 指令]

格式:

  • STR{条件}H 源寄存器,<存储器地址>
  • LDR{条件}H 目的寄存器,<存储器地址>

作用 :

  • STRH指令用于从源寄存器中将一个16位的半字数据传送到存储器中。该半字数据为源寄存器中的低16位。
  • LDRH指令用于从存储器中将一个16位的半字数据传送到目的寄存器中,同时将寄存器的高16位清零。



2.2.1.6 [LDM / STM 指令]

格式 : LDM(或STM){条件}{类型} 基址寄存器{!},寄存器列表{∧}

作用 :

  • 批量加载/储存指令 , 在一组寄存器和内存间传输数据
  • 常与堆栈格式组合使用 . 以模拟堆栈 *** 作

不同类型的堆栈:


示例:

LDMFD SP!,{R0-R2,R14}    ;将内存栈中的数据依次d出到R14、R2、R1、R0
STMFD SP!,{R0-R2,R14}	 ;将R0、R1、R2、R14依次压入内存栈

注意:

  • 每入栈一个元素, 栈指针SP都会往栈增长的方向移动一个存储单元.
  • 栈是先入后出(FILO) , 所以STMFD指令会根据{ }中的寄存器列表 ,从左到右压入栈
  • LDMFD 指令出栈时,顺序相反

入栈与出栈:


注意:

也可以通过PUSH/POP 指令来执行栈元素的入栈和出栈 *** 作

PUSH {R0-R2, R14}	;将RO、R1、R2、R14依次压入栈,
POP {R0-R2,R14} 	;将中的数据依次d出到R14、R2、R1、R0



2.2.2 数据传送指令 2.2.2.1 [MOV指令]

格式: MOV{条件}{S} 目的寄存器,源 *** 作数

作用 :

  • 是把一个寄存器的值(要能用立即数表示)赋给另一个寄存器,或者将一个常量赋给寄存器,将后边的量赋给前边的量。
  • MOV指令中,条件缺省时指令无条件执行;
  • {S}用来表示是否影响CPSR寄存器的值,如MOVS指令就会影响寄存器CPSR的值,而MOV则不会

示例:

MOV R1,R0   		;将寄存器R0的值传送到寄存器R1
MOV PC,R14   		;将寄存器R14的值传送到PC,常用于子程序返回
MOV R1,R0,LSL#3    ;将寄存器R0的值左移3位后传送到R1(即乘8)
MOVS PC, R14	     ;将寄存器R14的值传送到PC中,返回到调用代码并恢复标志位

2.2.2.2 [MVN 指令]

格式 : MVN{条件}{S} 目的寄存器,源 *** 作数

作用 :

  • MVN指令用来将 *** 作数operand2按位取反后传送到目标寄存器Rd
  • 与MOV指令不同的是 ,在传送之前就按位取反了

示例:

MVN R0,#0xFF   ;将立即数0xFF取反后赋值给R0
MVN R0,R1 	    ;将R1寄存器的值取反后赋值给R0



2.2.3 算术逻辑运算指令 2.2.3.1 算术指令

算术指令包括 基本的加、减、乘、除

格式:

ADD {cond} {S} Rd, Rn, operand2 ;加法
ADC {cond} {S} Rd, Rn, operand2 ;带进位加法
SUB {cond} {S} Rd, Rn, operand2 ;减法
SBC {cond} {S} Rd, Rn, operand2 ;带借位减法

示例:

ADD  R0,R1,R2           ; R0 = R1 + R2
ADD  R0,R1,#256         ; R0 = R1 + 256
ADD  R0,R2,R3,LSL#1     ; R0 = R2 + (R3 << 1)

ADC  R1,R1,#1			 ; R1 = R1 + 1 + C (其中C为CPSR寄存器中的进位) 

SUB  R0,R1,R2           ; R0 = R1 - R2
SUB  R0,R1,#256         ; R0 = R1 - 256
SUB  R0,R2,R3,LSL#1     ; R0 = R2 - (R3 << 1)

;BC指令用于把操作数1减去操作数2,再减去CPSR中的C条件标志位的反码,并将结果存放到目的寄存器中。
SUBS  R0,R1,R2           ;R0 = R1 - R2 - !C,并根据结果设置CPSR的进位标志位



2.2.3.2 逻辑指令

逻辑运算指令包括 (与、或、非、异或、清除等

格式:

AND {cond} {S} Rd, Rn, operand2 ;逻辑与运算(常用于屏蔽 *** 作数1 中的某些位 )
ORR {cond} {S} Rd, Rn, operand2 ;逻辑或运算(常用于设置 *** 作数1的某些位。)
EOR {cond} {S} Rd, Rn, operand2 ;异或运算(常用于反转 *** 作数1的某些位。)
BIC {cond} {S} Rd, Rn, operand2 ;位清除运算

示例:

AND  R0,R0,#3           ; 该指令保持R0的0、1位,其余位清零。   ( R0 = R0 & 0011 )
ORR  R0,R0,#3           ; 该指令设置R0的0、1位,其余位保持不变。( R0 = R0 | 0011)
EOR  R0,R0,#3           ; 该指令反转R0的0、1位,其余位保持不变。( R0 = R0 ^ 0011)
BIC  R0,R0,#%1011       ; 该指令清除 R0 中的位 0、1、和 3,其余的位保持不变。



2.2.4 比较指令 2.2.4.1 [CMP直接比较指令]

格式 : CMP{条件} *** 作数1, *** 作数2

作用 :

  • CMP指令用于把一个寄存器的内容和另一个寄存器的内容或立即数进行比较 , 同时影响CPSR寄存器的N、Z、C、V标志位
  • 该指令进行一次减法运算,但不存储结果,只更改条件标志位。标志位表示的是 *** 作数1与 *** 作数2的关系(大、小、相等)

CPSR寄存器标志位:

示例:

CMP R1,R0     ;将寄存器R1的值与寄存器R0的值相减,并根据结果设置CPSR的标志位
CMP R1,#100  ;将寄存器R1的值与立即数100相减,并根据结果设置CPSR的标志位

注意:

  • 比较指令的运行结果Z=1时,表示运算结果为零,两个数相等;

  • N=1表示运算结果为负,

  • N=0表示运算结果为非负,即运算结果为正或者为零。



2.2.4.2 [CMN负数比较指令]

格式: CMN{条件} *** 作数1, *** 作数2

作用: CMN指令用于把一个寄存器的内容和另一个寄存器的内容或立即数取反后进行比较,同时更新CPSR中条件标志位的值。

示例:

CMN R0, #1			;将立即数取负,然后比较大小


2.2.5 跳转指令 2.2.5.1 [B label 指令]

格式 : B {cond} Label

作用:

  • 程序无条件跳转到标号Label处执行,
  • 它是 24 位有符号数,左移两位后有符号扩展为 32 位. 表示跳转范围[0 , 32MB]
  • 无条件跳转指令B主要用在循环 , 分支结构

示例:

B  Label  	;程序无条件跳转到标号Label处执行
CMP R1,#0  ;当CPSR寄存器中的Z条件码置位时,程序跳转到标号Label处执行
BEQ Label  


2.2.5.2 [BL 带链接的跳转]

格式 : BL{条件} 目标地址

作用 :

  • 但跳转之前,会在寄存器RL(即R14)中保存PC的当前内容
  • BL指令一般用在函数调用的场合

示例:

BL Label  ;当程序无条件跳转到标号Label处执行时,同时将当前的PC值保存到R14中
...		  ; 子程序返回后接着从此处继续执行


2.2.5.3 [BX 带状态切换的跳转]

格式 : BX{条件} 目标地址

作用 : BX指令跳转到指令中所指定的目标地址,目标地址处的指令既可以是ARM指令,也可以是Thumb指令。



2.2.5.4 [BLX 指令]

格式 : BX{条件} 目标地址

作用: BLX指令是BL指令和BX指令的综合 , 表示带链接和状态切换的跳转



2.2.6 条件执行指令

为了提高代码密度,减少ARM指令的数量,几乎所有的ARM指令都可以根据CPSR寄存器中的标志位,通过指令组合实现条件执行。

如:

  • 无条件跳转指令B,我们可以在后面加上条件码组成BEQ、BNE组合指令。
  • BEQ指令表示两个数比较,结果相等时跳转;
  • BNE指令则表示结果不相等时跳转

ARM 指令的条件码:


示例:

通过循环结构,我们可以实现数据块的搬运功能。我们可以将无条件跳转指令B和条件码NE组合在一起使用,构成一个循环程序结构。

AREA COPY,CODE,READOLY
	ENTRY
START
	LDR R0,=SRC	; 源地址
	LDR R1,=DST	; 目的地址
	MOV R2,=#10	; 复制循环次数
LOOP
	LDR R3,[R0],#4 ;
	STR R3,[R1],#4 ;
	SUBS R2,R2,#4  ; 
	BNE LOOP	   ;
	
AREA COPYDATA, DATA, READWRITE
SRC DCD 1,2,3,4,5,6,7,8,9,0
DST DCD 0,0,0,0,0,0,0,0,0,0
	END	


2.3 伪指令

伪指令有点类似C语言中的预处理命令,在程序编译时,这些伪指令会被翻译为一条或多条ARM标准指令。常见的ARM伪指令主要有4个: ADR、ADRL、LDR、NOP,它们的使用示例如下:

ADR RO, LOOP		;将标号LOOP的地址保存到R0寄存器中
ADRL RO, LOOP		;中等范围的地址读取
LDR R0, =9x30008000 ;将内存地址0x30008000赋值给R0(主要用途是将一个32位内存地址保存到寄存器中)
NOP					;空 *** 作,用于延时或插入流水线中暂停指令的运行(相当于"MOV R0 R0")

备注: 为什么要用LDR 伪指令将一个32位内存地址保存到寄存器中

  • 因为指令的长度一般都是固定的。在一个32位的系统中,一条指令通常是32位的,
  • 指令中包括 *** 作码和 *** 作数
  • 指令中的 *** 作码和 *** 作数共享32位的存储空间;
  • 一般前面的 *** 作码要占据几个比特位,剩下来的留给 *** 作数的编码空间就小于32位

ARM指令的编码格式:



2.4 伪 *** 作

为了编程方便,汇编器也定义了一些特殊的指令助记符,以方便对汇编程序做各种处理。如使用AREA来定义一个段(section) ,使用GBLA来定义一个数据,使用ENTRY来指定汇编程序的执行入口等,这些指令助记符统称为伪 *** 作。


常用伪 *** 作:



2.4.1 [AREA]

语法格式 : [语法格式:AREA 段名 属性 1 ,属性 2 ,…  ]

作用: AREA 伪指令用于定义一个代码段或数据段。其中,段名若以数字开头,则该段名需用 “ | ” 括起来,如 |1_test| 。


属性字段表示该代码段(或数据段)的相关属性,多个属性用逗号分隔。常用的属性如下:

  • CODE 属性:用于定义代码段,默认为 READONLY 。
  • DATA 属性:用于定义数据段,默认为 READWRITE 。
  • READONLY 属性:指定本段为只读,代码段默认为 READONLY 。
  • READWRITE 属性:指定本段为可读可写,数据段的默认属性为 READWRITE 。
  • ALIGN 属性:使用方式为ALIGN 表达式。在默认时,ELF(可执行连接文件)的代码段和数据段是按字对齐的,表达式的取值范围为 0 ~31,相应的对齐方式为2表达式次方。
  • COMMON 属性:该属性定义一个通用的段,不包含任何的用户代码和数据。各源文件中同名的 COMMON 段共享同一段存储单元。

使用示例:

AREA Init , CODE , READONLY     ;该伪指令定义了一个代码段,段名为 Init ,属性为只读。 


2.4.2 [ALIGN]

语法格式: [ALIGN { 表达式 { ,偏移量 }}   ]

作用 : 地址对齐

  • ALIGN 伪指令可通过添加填充字节的方式,使当前位置满足一定的对齐方式。
  • 其中,表达式的值用于指定对齐方式,可能的取值为2的幂,如 1 、2 、4 、8 、16 等。
  • 若未指定表达式,则将当前位置对齐到下一个字的位置。
  • 偏移量也为一个数字表达式,若使用该字段,则当前位置的对齐方式为:2的表达式次幂+偏移量。

使用示例:

AREA Init,CODE ,READONLY,ALIEN=3	;指定后面的指令为 8 字节对齐。      
....

.... 
END      


2.4.3 [CODE16 / CODE32]

语法格式: CODE16 (或 CODE32 )

作用 :

  • CODE16 伪指令通知编译器,其后的指令序列为 16 位的 Thumb 指令。
  • CODE32 伪指令通知编译器,其后的指令序列为 32 位的 ARM 指令。
  • 在使用 ARM 指令和 Thumb 指令混合编程的代码里,可用这两条伪指令进行切换,但注意他们只通知编译器其后指令的类型,并不能对处理器进行状态的切换。

使用示例:

AREA Init ,CODE ,READONLY            
....      
CODE32 				;通知编译器其后的指令为 32 位的 ARM 指令            
LDR R0,=NEXT+1 	  ;将跳转地址放入寄存器 R0      
BX R0 				;程序跳转到新的位置执行,并将处理器切换到 Thumb 工作状态      
....     
CODE16 				;通知编译器其后的指令为 16 位的 Thumb 指令            
NEXT LDR R3,=0x3FF            
....     
END 				;程序结束         


2.4.4 [ENTRY]

语法格式 :  ENTRY

作用:

  • ENTRY 伪指令用于指定汇编程序的入口点。
  • 在一个完整的汇编程序中至少要有一个 ENTRY (也可以有多个)
  • 当有多个 ENTRY 时,程序的真正入口点由链接器指定),但在一个源文件里最多只能有一个 ENTRY (可以没有)。

使用示例:

AREA Init , CODE , READONLY            
ENTRY 				;应用程序的入口点
.....   


2.4.5 [END]

语法格式 :  END

作用: END 伪指令用于通知编译器已经到了源程序的结尾。

使用示例:

AREA Init , CODE , READONLY            
......     
END 			;指定应用程序的结尾



3️⃣ 作用域
3.1 头文件的内容
  • 函数原型
  • 使用#defineconst定义的符号常量
  • 结构声明
  • 类声明
  • 模版声明
  • 内联函数

注意: 不要将函数定义和变量声明放在头文件中,容易导致重定义。 除非该函数是内联函数



3.2 #inlcude 中“ ” 和 < >的区别



3.3 内存在程序中保留的时间
  • 自动储存:局部变量、函数参数 在程序开始执行其所属的函数和代码块时被创建, 在执行完函数和代码块时,内存被释放
  • 静态储存:全局变量、staic关键字定义的局部变量和全局变量 。在整个程序运行过程中都存在。
  • 动态存储: 用new 分配的内存会一直存在,直到使用delete 关键字将其释放。 也称为堆



3.4 作用域和链接
  • 作用域: 值在上面范围内能看到这个(函数/变量 ),描述了名称在文件的多大范围内可见。
  • 链接性: 描述了名称在不同单元键的共享

局部变量作用域: 作用域只在定义它的代码块中

全局变量作用域: 作用域为定义位置到文件结尾

自动变量作用域: 作用域为局部

函数体作用域: 整个类或整个名称空间,但不能是局部的


[例如: 局部变量和函数形参的 储存持续性为自动,作用域为局部; 没有链接性。 当程序开始执行时,为该变量分配内存,当函数结束时,这些变量会消失。]




注意:



3.5 静态储存的链接性 3.5.1 静态变量的链接性:
  • 外部链接性: 可在其他文件中访问
  • 内部链接性: 只能在当前文件中访问
  • 无链接性: 只能在当前函数或代码块中访问

静态变量的数量在程序运行期间是不变的, 编译器会分配固定的内存块来储存所有的静态变量,这些变量在程序

执行期间一直存在。


 int a = 0;    // 静态持续变量,连接性为外部
 
 staic int b = 0; // 静态持续变量,连接性为内部
 
 void funct(void)
 {
     staic int c = 0;        //静态变量,无链接性
     int d = 0;              // 自动变量,无链接性
 }    

特别注意: 变量c是储存在静态数据区的,会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。

即使funct函数没有被执行,c变量也是留在内存中的,而变量d则会消失。



3.5.2 静态变量的初始化特性

所有静态持续变量在初始化时都会被初始化为0 ,称为零初始化 。

在静态数据区,内存中所有的字节默认值都是 0x00,某些时候这一特点可以减少程序员的工作量。比如初始化一个稀疏矩阵,我们可以一个一个地把所有元素都置 0,然后把不是 0 的几个元素赋值。如果定义成静态的,就省去了一开始置 0 的 *** 作。再比如要把一个字符数组当字符串来用,但又觉得每次在字符数组末尾加 volatile 太麻烦。如果把字符串定义成静态的,就省去了这个麻烦,因为那里本来就是 。



3.6 变量储存方式总结



3.7 [+++] 限定符



)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
嵌入式C语言学习记录(一) —— ARM指令集与作用域_C_内存溢出

嵌入式C语言学习记录(一) —— ARM指令集与作用域

嵌入式C语言学习记录(一) —— ARM指令集与作用域,第1张

嵌入式C语言学习记录(一) —— ARM指令集与作用域

version : v1.0 「2022.9.11」 最后补充

author: Y.Z.T.


摘要: 记录汇总自己在嵌入式开发过程中 学习的一些零散知识

简介: 简单汇总,方便自己查看


(👇 第二部分)

嵌入式C语言学习记录(二) —— GNU拓展语法与C语言补充




⭐️ 目录

文章目录
  • 嵌入式C语言学习记录(一) —— ARM指令集与作用域
    • :one: string.h 库函数
    • :two: ARM汇编指令集
      • 2.1 ARM汇编指令
          • 2.1.1 *** 作数2(operand2)
      • 2.2 基本指令
        • 2.2.1 储存访问指令
          • 2.2.1.1 [`LDR`指令]
          • 2.2.1.2 [`STR`指令]
          • 2.2.1.3 [`LDRB` 指令]
          • 2.2.1.4 [`STRB` 指令]
          • 2.2.1.5 [`LDRH / STRH` 指令]
        • 2.2.2 数据传送指令
          • 2.2.2.1 [`MOV`指令]
          • 2.2.2.2 [`MVN` 指令]
        • 2.2.3 算术逻辑运算指令
          • 2.2.3.1 算术指令
          • 2.2.3.2 逻辑指令
        • 2.2.4 比较指令
          • 2.2.4.1 [`CMP`直接比较指令]
          • 2.2.4.2 [`CMN`负数比较指令]
        • 2.2.5 跳转指令
          • 2.2.5.1 [`B label` 指令]
          • 2.2.5.2 [`BL` 带链接的跳转]
          • 2.2.5.3 [`BX` 带状态切换的跳转]
          • 2.2.5.4 [`BLX` 指令]
        • 2.2.6 条件执行指令
      • 2.3 伪指令
      • 2.4 伪 *** 作
        • 2.4.1 [`AREA`]
        • 2.4.2 [`ALIGN`]
        • 2.4.3 [`CODE16` / `CODE32`]
        • 2.4.4 [`ENTRY`]
        • 2.4.5 [`END`]
    • :three: 作用域
      • 3.1 头文件的内容
      • 3.2 `#inlcude` 中“ ” 和 < >的区别
      • 3.3 内存在程序中保留的时间
      • 3.4 作用域和链接
      • 3.5 静态储存的链接性
        • 3.5.1 静态变量的链接性:
        • 3.5.2 静态变量的初始化特性
      • 3.6 变量储存方式总结
      • 3.7 `volatile` 限定符




1️⃣ string.h 库函数

下面是头文件 string.h 中定义的函数:

序号函数描述
1void *memchr(const void *str, int c, size_t n在参数 str 所指向的字符串的前 n 个字节中搜索第一次出现字符 c(一个无符号字符)的位置。
2int memcmp(const void *str1, const void *str2, size_t n)str1str2 的前 n 个字节进行比较。
3void *memcpy(void *dest, const void *src, size_t n)从 src 复制 n 个字符到 dest
4void *memmove(void *dest, const void *src, size_t n)另一个用于从 src 复制 n 个字符到 dest 的函数。
5void *memset(void *str, int c, size_t n)复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。
6char *strcat(char *dest, const char *src)src 所指向的字符串追加到 dest 所指向的字符串的结尾。
7char *strncat(char *dest, const char *src, size_t n)src 所指向的字符串追加到 dest 所指向的字符串的结尾,直到 n 字符长度为止。
8char *strchr(const char *str, int c)在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置。
9int strcmp(const char *str1, const char *str2)str1 所指向的字符串和 str2 所指向的字符串进行比较。
10int strncmp(const char *str1, const char *str2, size_t n)str1str2 进行比较,最多比较前 n 个字节。
11int strcoll(const char *str1, const char *str2)str1str2 进行比较,结果取决于 LC_COLLATE 的位置设置
12char *strcpy(char *dest, const char *src)src 所指向的字符串复制到 dest
13char *strncpy(char *dest, const char *src, size_t n)src 所指向的字符串复制到 dest,最多复制 n 个字符。
14size_t strcspn(const char *str1, const char *str2)检索字符串 str1 开头连续有几个字符都不含字符串 str2 中的字符。
15char *strerror(int errnum)从内部数组中搜索错误号 errnum,并返回一个指向错误消息字符串的指针。
16size_t strlen(const char *str)计算字符串 str 的长度,直到空结束字符,但不包括空结束字符。
17char *strpbrk(const char *str1, const char *str2)检索字符串 str1 中第一个匹配字符串 str2 中字符的字符,不包含空结束字符。也就是说,依次检验字符串 str1 中的字符,当被检验字符在字符串 str2 中也包含时,则停止检验,并返回该字符位置。
18char *strrchr(const char *str, int c)在参数 str 所指向的字符串中搜索最后一次出现字符 c(一个无符号字符)的位置。
19size_t strspn(const char *str1, const char *str2)检索字符串 str1 中第一个不在字符串 str2 中出现的字符下标。
20char *strstr(const char *haystack, const char *needle) 在字符串 haystack 中查找第一次出现字符串 needle(不包含空结束字符)的位置。
21char *strtok(char *str, const char *delim)分解字符串 str 为一组字符串,delim 为分隔符。
22size_t strxfrm(char *dest, const char *src, size_t n)根据程序当前的区域选项中的 LC_COLLATE 来转换字符串 src 的前 n 个字符,并把它们放置在字符串 dest 中。



2️⃣ ARM汇编指令集

  • R0~R3通常用来传递函数参数,
  • R4~R11用来保存程序运算的中间结果或函数的局部变量等,
  • R12常用来作为函数调用过程中的临时寄存器。
  • R13寄存器又称为堆栈指针寄存器(StackPointer, SP) ,用来维护和管理函数调用过程中的栈帧变化, R13总是指向当前正在运行的函数的栈帧,一般不能再用作其他用途。
  • R14寄存器又称为链接寄存器(Link Register, LR) ,在函数调用过程中主要用来保存上一级函数调用者的返回地址。寄存器
  • R15又称为程序计数器(Program Counter, PC) , CPU从内存取指令执行,就是默认从PC保存的地址中取的,每取一次指令, PC寄存器的地址值自动增加。
  • CPSR (当前处理器状态寄存器), 主要用来表征当前处理器的运行状态。除了各种状态位、标志位,CPSR寄存器里也有一些控制位,用来切换处理器的工作模式和中断使能控制。

CPSR寄存器标志位:


2.1 ARM汇编指令

一个完整的ARM指令通常是由 *** 作码 + *** 作数

<opcode> {<cond> {s} <Rd>,<Rn> {,<operand2>}}

格式说明:

  • 使用<>标起来的是必选项,使用{ }标起来的是可选项。
  • 是二进制机器指令的 *** 作码助记符,如MOVADD这些汇编指令都是 *** 作码的指令助记符。
  • cond : 执行条件, ARM为减少分支跳转指令个数,允许类似BEQBNE等形式的组合指令。
  • S: 是否影响CPSR寄存器中的标志位,如SUBS指令会影响CPSR寄存器中的N、Z、C、V标志位,而SUB指令不会。
  • Rd :目标寄存器。
  • Rn :第一个 *** 作数的寄存器。
  • operand2 :第二个可选 *** 作数,灵活使用第二个 *** 作数可以提高代码效率。


2.1.1 *** 作数2(operand2)

*** 作数operand2在汇编程序中经常出现的两种格式如下:

#constant		;  *** 作数是一个立即数
Rm{,shift}		; 使用寄存器值作为 *** 作数

注意:

在第二种格式中,通过{, shift}可选项,我们还可以通过多种移位或循环移位的方式,构建更加灵活的 *** 作数。可选项{, shift}可以选择的移位方式如下:

#constant,n 	;将立即数 constant循环右移n位
ASR #			;算术右移n位, n的取值范围: [1,32]
LSL #n			;逻辑左移n位, n的取值范围: [0,31]
LSR #n			;逻辑右移n位, n的取值范围: [1,32]
ROR #n			;向右循环移n位, n的取值范围: [1,31]
RRX				;向右循环移1位,带扩展
type Rs			;仅在ARM中可用,其中type指ASP、LSL、LSR、ROR, Rs是提供位移量的寄存器名称

示例:

ADD R3, R2, R1, LSL #3		;R3=R2+R1<<3
ADD R3, R2, R1, LSL R0		;R3=R2+R1<



2.2 基本指令 2.2.1 储存访问指令 2.2.1.1 [LDR指令]

格式 : LDR{条件} 目的寄存器,<存储器地址>;

作用 : LDR指令用于从存储器中将一个32位的字数据传送到目的寄存器中。该指令通常用于从存储器中读取32位的字数据到通用寄存器,然后对数据进行处理。当程序计数器PC作为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而可以实现程序流程的跳转。

示例:

LDR  R0,[R1]               ;将存储器地址为R1的字数据读入寄存器R0。
LDR  R0,[R1,R2]           ;将存储器地址为R1+R2的字数据读入寄存器R0。
LDR  R0,[R1,#8]          ;将存储器地址为R1+8的字数据读入寄存器R0。
LDR  R0,[R1,R2] !        ;将存储器地址为R1+R2的字数据读入寄存器R0,并将新地址R1+R2写入R1。
LDR  R0,[R1,#8] !       ;将存储器地址为R1+8的字数据读入寄存器R0,并将新地址R1+8写入R1。
LDR  R0,[R1],R2          ;将存储器地址为R1的字数据读入寄存器R0,并将新地址R1+R2写入R1。
LDR  R0,[R1,R2,LSL#2]! ;将存储器地址为R1+R2×4的字数据读入寄存器R0,并将新地址R1+R2×4写入R1。
LDR  R0,[R1],R2,LSL#2   ;将存储器地址为R1的字数据读入寄存器R0,并将新地址R1+R2×4写入R1。

{!}为可选后缀,若选用该后缀,则当数据传送完毕之后,将最后的地址写入基址寄存器,否则基址寄存器的内容不改变。




2.2.1.2 [STR指令]

注意: LDR/STR指令是ARM汇编中使用频率最高的一对指令

格式 : STR{条件} 源寄存器,<存储器地址>;

作用 : STR指令用于从源寄存器中将一个32位的字数据传送到存储器中。

示例:

STR R0,[R1],#8  	;将R0中的字数据写入以R1为地址的存储器中,并将新地址R1+8写入R1。
STR R0,[R1,#8] 		;将R0中的字数据写入以R1+8为地址的存储器中。




2.2.1.3 [LDRB 指令]

格式 : LDR{条件}B 目的寄存器,<存储器地址>

作用 : LDRB指令用于从存储器中将一个8位的字节数据传送到目的寄存器中,同时将寄存器的高24位清零。
该指令通常用于从存储器中读取8位的字节数据到通用寄存器,然后对数据进行处理。当程序计数器PC作为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而可以实现程序流程的跳转。

示例:

LDRB  R0,[R1]        ;将存储器地址为R1的字节数据读入寄存器R0,并将R0的高24位清零。
LDRB  R0,[R1,#8]    ;将存储器地址为R1+8的字节数据读入寄存器R0,并将R0的高24位清零。



2.2.1.4 [STRB 指令]

格式 : STR{条件}B 源寄存器,<存储器地址>

作用 : STRB指令用于从源寄存器中将一个8位的字节数据传送到存储器中。该字节数据为源寄存器中的低8位。

示例:

STRB  R0,[R1]      ;将寄存器R0中的字节数据写入以R1为地址的存储器中。
STRB  R0,[R1,#8]  ;将寄存器R0中的字节数据写入以R1+8为地址的存储器中。



2.2.1.5 [LDRH / STRH 指令]

格式:

  • STR{条件}H 源寄存器,<存储器地址>
  • LDR{条件}H 目的寄存器,<存储器地址>

作用 :

  • STRH指令用于从源寄存器中将一个16位的半字数据传送到存储器中。该半字数据为源寄存器中的低16位。
  • LDRH指令用于从存储器中将一个16位的半字数据传送到目的寄存器中,同时将寄存器的高16位清零。



2.2.1.6 [LDM / STM 指令]

格式 : LDM(或STM){条件}{类型} 基址寄存器{!},寄存器列表{∧}

作用 :

  • 批量加载/储存指令 , 在一组寄存器和内存间传输数据
  • 常与堆栈格式组合使用 . 以模拟堆栈 *** 作

不同类型的堆栈:


示例:

LDMFD SP!,{R0-R2,R14}    ;将内存栈中的数据依次d出到R14、R2、R1、R0
STMFD SP!,{R0-R2,R14}	 ;将R0、R1、R2、R14依次压入内存栈

注意:

  • 每入栈一个元素, 栈指针SP都会往栈增长的方向移动一个存储单元.
  • 栈是先入后出(FILO) , 所以STMFD指令会根据{ }中的寄存器列表 ,从左到右压入栈
  • LDMFD 指令出栈时,顺序相反

入栈与出栈:


注意:

也可以通过PUSH/POP 指令来执行栈元素的入栈和出栈 *** 作

PUSH {R0-R2, R14}	;将RO、R1、R2、R14依次压入栈,
POP {R0-R2,R14} 	;将中的数据依次d出到R14、R2、R1、R0



2.2.2 数据传送指令 2.2.2.1 [MOV指令]

格式: MOV{条件}{S} 目的寄存器,源 *** 作数

作用 :

  • 是把一个寄存器的值(要能用立即数表示)赋给另一个寄存器,或者将一个常量赋给寄存器,将后边的量赋给前边的量。
  • MOV指令中,条件缺省时指令无条件执行;
  • {S}用来表示是否影响CPSR寄存器的值,如MOVS指令就会影响寄存器CPSR的值,而MOV则不会

示例:

MOV R1,R0   		;将寄存器R0的值传送到寄存器R1
MOV PC,R14   		;将寄存器R14的值传送到PC,常用于子程序返回
MOV R1,R0,LSL#3    ;将寄存器R0的值左移3位后传送到R1(即乘8)
MOVS PC, R14	     ;将寄存器R14的值传送到PC中,返回到调用代码并恢复标志位

2.2.2.2 [MVN 指令]

格式 : MVN{条件}{S} 目的寄存器,源 *** 作数

作用 :

  • MVN指令用来将 *** 作数operand2按位取反后传送到目标寄存器Rd
  • 与MOV指令不同的是 ,在传送之前就按位取反了

示例:

MVN R0,#0xFF   ;将立即数0xFF取反后赋值给R0
MVN R0,R1 	    ;将R1寄存器的值取反后赋值给R0



2.2.3 算术逻辑运算指令 2.2.3.1 算术指令

算术指令包括 基本的加、减、乘、除

格式:

ADD {cond} {S} Rd, Rn, operand2 ;加法
ADC {cond} {S} Rd, Rn, operand2 ;带进位加法
SUB {cond} {S} Rd, Rn, operand2 ;减法
SBC {cond} {S} Rd, Rn, operand2 ;带借位减法

示例:

ADD  R0,R1,R2           ; R0 = R1 + R2
ADD  R0,R1,#256         ; R0 = R1 + 256
ADD  R0,R2,R3,LSL#1     ; R0 = R2 + (R3 << 1)

ADC  R1,R1,#1			 ; R1 = R1 + 1 + C (其中C为CPSR寄存器中的进位) 

SUB  R0,R1,R2           ; R0 = R1 - R2
SUB  R0,R1,#256         ; R0 = R1 - 256
SUB  R0,R2,R3,LSL#1     ; R0 = R2 - (R3 << 1)

;BC指令用于把操作数1减去操作数2,再减去CPSR中的C条件标志位的反码,并将结果存放到目的寄存器中。
SUBS  R0,R1,R2           ;R0 = R1 - R2 - !C,并根据结果设置CPSR的进位标志位



2.2.3.2 逻辑指令

逻辑运算指令包括 (与、或、非、异或、清除等

格式:

AND {cond} {S} Rd, Rn, operand2 ;逻辑与运算(常用于屏蔽 *** 作数1 中的某些位 )
ORR {cond} {S} Rd, Rn, operand2 ;逻辑或运算(常用于设置 *** 作数1的某些位。)
EOR {cond} {S} Rd, Rn, operand2 ;异或运算(常用于反转 *** 作数1的某些位。)
BIC {cond} {S} Rd, Rn, operand2 ;位清除运算

示例:

AND  R0,R0,#3           ; 该指令保持R0的0、1位,其余位清零。   ( R0 = R0 & 0011 )
ORR  R0,R0,#3           ; 该指令设置R0的0、1位,其余位保持不变。( R0 = R0 | 0011)
EOR  R0,R0,#3           ; 该指令反转R0的0、1位,其余位保持不变。( R0 = R0 ^ 0011)
BIC  R0,R0,#%1011       ; 该指令清除 R0 中的位 0、1、和 3,其余的位保持不变。



2.2.4 比较指令 2.2.4.1 [CMP直接比较指令]

格式 : CMP{条件} *** 作数1, *** 作数2

作用 :

  • CMP指令用于把一个寄存器的内容和另一个寄存器的内容或立即数进行比较 , 同时影响CPSR寄存器的N、Z、C、V标志位
  • 该指令进行一次减法运算,但不存储结果,只更改条件标志位。标志位表示的是 *** 作数1与 *** 作数2的关系(大、小、相等)

CPSR寄存器标志位:

示例:

CMP R1,R0     ;将寄存器R1的值与寄存器R0的值相减,并根据结果设置CPSR的标志位
CMP R1,#100  ;将寄存器R1的值与立即数100相减,并根据结果设置CPSR的标志位

注意:

  • 比较指令的运行结果Z=1时,表示运算结果为零,两个数相等;

  • N=1表示运算结果为负,

  • N=0表示运算结果为非负,即运算结果为正或者为零。



2.2.4.2 [CMN负数比较指令]

格式: CMN{条件} *** 作数1, *** 作数2

作用: CMN指令用于把一个寄存器的内容和另一个寄存器的内容或立即数取反后进行比较,同时更新CPSR中条件标志位的值。

示例:

CMN R0, #1			;将立即数取负,然后比较大小


2.2.5 跳转指令 2.2.5.1 [B label 指令]

格式 : B {cond} Label

作用:

  • 程序无条件跳转到标号Label处执行,
  • 它是 24 位有符号数,左移两位后有符号扩展为 32 位. 表示跳转范围[0 , 32MB]
  • 无条件跳转指令B主要用在循环 , 分支结构

示例:

B  Label  	;程序无条件跳转到标号Label处执行
CMP R1,#0  ;当CPSR寄存器中的Z条件码置位时,程序跳转到标号Label处执行
BEQ Label  


2.2.5.2 [BL 带链接的跳转]

格式 : BL{条件} 目标地址

作用 :

  • 但跳转之前,会在寄存器RL(即R14)中保存PC的当前内容
  • BL指令一般用在函数调用的场合

示例:

BL Label  ;当程序无条件跳转到标号Label处执行时,同时将当前的PC值保存到R14中
...		  ; 子程序返回后接着从此处继续执行


2.2.5.3 [BX 带状态切换的跳转]

格式 : BX{条件} 目标地址

作用 : BX指令跳转到指令中所指定的目标地址,目标地址处的指令既可以是ARM指令,也可以是Thumb指令。



2.2.5.4 [BLX 指令]

格式 : BX{条件} 目标地址

作用: BLX指令是BL指令和BX指令的综合 , 表示带链接和状态切换的跳转



2.2.6 条件执行指令

为了提高代码密度,减少ARM指令的数量,几乎所有的ARM指令都可以根据CPSR寄存器中的标志位,通过指令组合实现条件执行。

如:

  • 无条件跳转指令B,我们可以在后面加上条件码组成BEQ、BNE组合指令。
  • BEQ指令表示两个数比较,结果相等时跳转;
  • BNE指令则表示结果不相等时跳转

ARM 指令的条件码:


示例:

通过循环结构,我们可以实现数据块的搬运功能。我们可以将无条件跳转指令B和条件码NE组合在一起使用,构成一个循环程序结构。

AREA COPY,CODE,READOLY
	ENTRY
START
	LDR R0,=SRC	; 源地址
	LDR R1,=DST	; 目的地址
	MOV R2,=#10	; 复制循环次数
LOOP
	LDR R3,[R0],#4 ;
	STR R3,[R1],#4 ;
	SUBS R2,R2,#4  ; 
	BNE LOOP	   ;
	
AREA COPYDATA, DATA, READWRITE
SRC DCD 1,2,3,4,5,6,7,8,9,0
DST DCD 0,0,0,0,0,0,0,0,0,0
	END	


2.3 伪指令

伪指令有点类似C语言中的预处理命令,在程序编译时,这些伪指令会被翻译为一条或多条ARM标准指令。常见的ARM伪指令主要有4个: ADR、ADRL、LDR、NOP,它们的使用示例如下:

ADR RO, LOOP		;将标号LOOP的地址保存到R0寄存器中
ADRL RO, LOOP		;中等范围的地址读取
LDR R0, =9x30008000 ;将内存地址0x30008000赋值给R0(主要用途是将一个32位内存地址保存到寄存器中)
NOP					;空 *** 作,用于延时或插入流水线中暂停指令的运行(相当于"MOV R0 R0")

备注: 为什么要用LDR 伪指令将一个32位内存地址保存到寄存器中

  • 因为指令的长度一般都是固定的。在一个32位的系统中,一条指令通常是32位的,
  • 指令中包括 *** 作码和 *** 作数
  • 指令中的 *** 作码和 *** 作数共享32位的存储空间;
  • 一般前面的 *** 作码要占据几个比特位,剩下来的留给 *** 作数的编码空间就小于32位

ARM指令的编码格式:



2.4 伪 *** 作

为了编程方便,汇编器也定义了一些特殊的指令助记符,以方便对汇编程序做各种处理。如使用AREA来定义一个段(section) ,使用GBLA来定义一个数据,使用ENTRY来指定汇编程序的执行入口等,这些指令助记符统称为伪 *** 作。


常用伪 *** 作:



2.4.1 [AREA]

语法格式 : [语法格式:AREA 段名 属性 1 ,属性 2 ,…  ]

作用: AREA 伪指令用于定义一个代码段或数据段。其中,段名若以数字开头,则该段名需用 “ | ” 括起来,如 |1_test| 。


属性字段表示该代码段(或数据段)的相关属性,多个属性用逗号分隔。常用的属性如下:

  • CODE 属性:用于定义代码段,默认为 READONLY 。
  • DATA 属性:用于定义数据段,默认为 READWRITE 。
  • READONLY 属性:指定本段为只读,代码段默认为 READONLY 。
  • READWRITE 属性:指定本段为可读可写,数据段的默认属性为 READWRITE 。
  • ALIGN 属性:使用方式为ALIGN 表达式。在默认时,ELF(可执行连接文件)的代码段和数据段是按字对齐的,表达式的取值范围为 0 ~31,相应的对齐方式为2表达式次方。
  • COMMON 属性:该属性定义一个通用的段,不包含任何的用户代码和数据。各源文件中同名的 COMMON 段共享同一段存储单元。

使用示例:

AREA Init , CODE , READONLY     ;该伪指令定义了一个代码段,段名为 Init ,属性为只读。 


2.4.2 [ALIGN]

语法格式: [ALIGN { 表达式 { ,偏移量 }}   ]

作用 : 地址对齐

  • ALIGN 伪指令可通过添加填充字节的方式,使当前位置满足一定的对齐方式。
  • 其中,表达式的值用于指定对齐方式,可能的取值为2的幂,如 1 、2 、4 、8 、16 等。
  • 若未指定表达式,则将当前位置对齐到下一个字的位置。
  • 偏移量也为一个数字表达式,若使用该字段,则当前位置的对齐方式为:2的表达式次幂+偏移量。

使用示例:

AREA Init,CODE ,READONLY,ALIEN=3	;指定后面的指令为 8 字节对齐。      
....

.... 
END      


2.4.3 [CODE16 / CODE32]

语法格式: CODE16 (或 CODE32 )

作用 :

  • CODE16 伪指令通知编译器,其后的指令序列为 16 位的 Thumb 指令。
  • CODE32 伪指令通知编译器,其后的指令序列为 32 位的 ARM 指令。
  • 在使用 ARM 指令和 Thumb 指令混合编程的代码里,可用这两条伪指令进行切换,但注意他们只通知编译器其后指令的类型,并不能对处理器进行状态的切换。

使用示例:

AREA Init ,CODE ,READONLY            
....      
CODE32 				;通知编译器其后的指令为 32 位的 ARM 指令            
LDR R0,=NEXT+1 	  ;将跳转地址放入寄存器 R0      
BX R0 				;程序跳转到新的位置执行,并将处理器切换到 Thumb 工作状态      
....     
CODE16 				;通知编译器其后的指令为 16 位的 Thumb 指令            
NEXT LDR R3,=0x3FF            
....     
END 				;程序结束         


2.4.4 [ENTRY]

语法格式 :  ENTRY

作用:

  • ENTRY 伪指令用于指定汇编程序的入口点。
  • 在一个完整的汇编程序中至少要有一个 ENTRY (也可以有多个)
  • 当有多个 ENTRY 时,程序的真正入口点由链接器指定),但在一个源文件里最多只能有一个 ENTRY (可以没有)。

使用示例:

AREA Init , CODE , READONLY            
ENTRY 				;应用程序的入口点
.....   


2.4.5 [END]

语法格式 :  END

作用: END 伪指令用于通知编译器已经到了源程序的结尾。

使用示例:

AREA Init , CODE , READONLY            
......     
END 			;指定应用程序的结尾



3️⃣ 作用域
3.1 头文件的内容
  • 函数原型
  • 使用#defineconst定义的符号常量
  • 结构声明
  • 类声明
  • 模版声明
  • 内联函数

注意: 不要将函数定义和变量声明放在头文件中,容易导致重定义。 除非该函数是内联函数



3.2 #inlcude 中“ ” 和 < >的区别



3.3 内存在程序中保留的时间
  • 自动储存:局部变量、函数参数 在程序开始执行其所属的函数和代码块时被创建, 在执行完函数和代码块时,内存被释放
  • 静态储存:全局变量、staic关键字定义的局部变量和全局变量 。在整个程序运行过程中都存在。
  • 动态存储: 用new 分配的内存会一直存在,直到使用delete 关键字将其释放。 也称为堆



3.4 作用域和链接
  • 作用域: 值在上面范围内能看到这个(函数/变量 ),描述了名称在文件的多大范围内可见。
  • 链接性: 描述了名称在不同单元键的共享

局部变量作用域: 作用域只在定义它的代码块中

全局变量作用域: 作用域为定义位置到文件结尾

自动变量作用域: 作用域为局部

函数体作用域: 整个类或整个名称空间,但不能是局部的


[例如: 局部变量和函数形参的 储存持续性为自动,作用域为局部; 没有链接性。 当程序开始执行时,为该变量分配内存,当函数结束时,这些变量会消失。]




注意:



3.5 静态储存的链接性 3.5.1 静态变量的链接性:
  • 外部链接性: 可在其他文件中访问
  • 内部链接性: 只能在当前文件中访问
  • 无链接性: 只能在当前函数或代码块中访问

静态变量的数量在程序运行期间是不变的, 编译器会分配固定的内存块来储存所有的静态变量,这些变量在程序

执行期间一直存在。


 int a = 0;    // 静态持续变量,连接性为外部
 
 staic int b = 0; // 静态持续变量,连接性为内部
 
 void funct(void)
 {
     staic int c = 0;        //静态变量,无链接性
     int d = 0;              // 自动变量,无链接性
 }    

特别注意: 变量c是储存在静态数据区的,会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。

即使funct函数没有被执行,c变量也是留在内存中的,而变量d则会消失。



3.5.2 静态变量的初始化特性

所有静态持续变量在初始化时都会被初始化为0 ,称为零初始化 。

在静态数据区,内存中所有的字节默认值都是 0x00,某些时候这一特点可以减少程序员的工作量。比如初始化一个稀疏矩阵,我们可以一个一个地把所有元素都置 0,然后把不是 0 的几个元素赋值。如果定义成静态的,就省去了一开始置 0 的 *** 作。再比如要把一个字符数组当字符串来用,但又觉得每次在字符数组末尾加 volatile 太麻烦。如果把字符串定义成静态的,就省去了这个麻烦,因为那里本来就是 。



3.6 变量储存方式总结



3.7 限定符



欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/3002885.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-09-27
下一篇 2022-09-27

发表评论

登录后才能评论

评论列表(0条)

保存