嵌入式C语言学习记录(一) —— ARM指令集与作用域
version : v1.0 「2022.9.11」 最后补充
author: Y.Z.T.
摘要: 记录汇总自己在嵌入式开发过程中 学习的一些零散知识
简介: 简单汇总,方便自己查看
(👇 第二部分)
嵌入式C语言学习记录(二) —— GNU拓展语法与C语言补充
(👇 PDF 版)
PDF 文件
1️⃣ string.h 库函数
下面是头文件 string.h 中定义的函数:
序号 | 函数 | 描述 |
---|---|---|
1 | void *memchr(const void *str, int c, size_t n |
在参数 str 所指向的字符串的前 n 个字节中搜索第一次出现字符 c(一个无符号字符)的位置。 |
2 | int memcmp(const void *str1, const void *str2, size_t n) |
把 str1 和 str2 的前 n 个字节进行比较。 |
3 | void *memcpy(void *dest, const void *src, size_t n) |
从 src 复制 n 个字符到 dest。 |
4 | void *memmove(void *dest, const void *src, size_t n) |
另一个用于从 src 复制 n 个字符到 dest 的函数。 |
5 | void *memset(void *str, int c, size_t n) |
复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。 |
6 | char *strcat(char *dest, const char *src) |
把 src 所指向的字符串追加到 dest 所指向的字符串的结尾。 |
7 | char *strncat(char *dest, const char *src, size_t n) |
把 src 所指向的字符串追加到 dest 所指向的字符串的结尾,直到 n 字符长度为止。 |
8 | char *strchr(const char *str, int c) |
在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置。 |
9 | int strcmp(const char *str1, const char *str2) |
把 str1 所指向的字符串和 str2 所指向的字符串进行比较。 |
10 | int strncmp(const char *str1, const char *str2, size_t n) |
把 str1 和 str2 进行比较,最多比较前 n 个字节。 |
11 | int strcoll(const char *str1, const char *str2) |
把 str1 和 str2 进行比较,结果取决于 LC_COLLATE 的位置设置 |
12 | char *strcpy(char *dest, const char *src) |
把 src 所指向的字符串复制到 dest。 |
13 | char *strncpy(char *dest, const char *src, size_t n) |
把 src 所指向的字符串复制到 dest,最多复制 n 个字符。 |
14 | size_t strcspn(const char *str1, const char *str2) |
检索字符串 str1 开头连续有几个字符都不含字符串 str2 中的字符。 |
15 | char *strerror(int errnum) |
从内部数组中搜索错误号 errnum,并返回一个指向错误消息字符串的指针。 |
16 | size_t strlen(const char *str) |
计算字符串 str 的长度,直到空结束字符,但不包括空结束字符。 |
17 | char *strpbrk(const char *str1, const char *str2) |
检索字符串 str1 中第一个匹配字符串 str2 中字符的字符,不包含空结束字符。也就是说,依次检验字符串 str1 中的字符,当被检验字符在字符串 str2 中也包含时,则停止检验,并返回该字符位置。 |
18 | char *strrchr(const char *str, int c) |
在参数 str 所指向的字符串中搜索最后一次出现字符 c(一个无符号字符)的位置。 |
19 | size_t strspn(const char *str1, const char *str2) |
检索字符串 str1 中第一个不在字符串 str2 中出现的字符下标。 |
20 | char *strstr(const char *haystack, const char *needle) |
在字符串 haystack 中查找第一次出现字符串 needle(不包含空结束字符)的位置。 |
21 | char *strtok(char *str, const char *delim) |
分解字符串 str 为一组字符串,delim 为分隔符。 |
22 | size_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指令通常是由操作码 + 操作数
1 | <opcode> {<cond> {s} <Rd>,<Rn> {,<operand2>}} |
格式说明:
- 使用
<>
标起来的是必选项,使用{ }
标起来的是可选项。 <opcode>
是二进制机器指令的操作码助记符,如MOV
、ADD
这些汇编指令都是操作码的指令助记符。- cond : 执行条件,
ARM
为减少分支跳转指令个数,允许类似BEQ
、BNE
等形式的组合指令。 - S: 是否影响
CPSR
寄存器中的标志位,如SUBS指令会影响CPSR寄存器中的N、Z、C、V
标志位,而SUB
指令不会。 - Rd :目标寄存器。
- Rn :第一个操作数的寄存器。
- operand2 :第二个可选操作数,灵活使用第二个操作数可以提高代码效率。
2.1.1 操作数2(operand2)
操作数operand2在汇编程序中经常出现的两种格式如下:
1 | #constant ; 操作数是一个立即数 |
注意:
在第二种格式中,通过{, shift}
可选项,我们还可以通过多种移位或循环移位的方式,构建更加灵活的操作数。可选项{, shift}
可以选择的移位方式如下:
1 | #constant,n ;将立即数 constant循环右移n位 |
示例:
1 | ADD R3, R2, R1, LSL #3 ;R3=R2+R1<<3 |
2.2 基本指令
2.2.1 储存访问指令
2.2.1.1 [LDR
指令]
格式 : LDR{条件} 目的寄存器,<存储器地址>;
作用 : LDR指令用于从存储器中将一个32位的字数据传送到目的寄存器中。该指令通常用于从存储器中读取32位的字数据到通用寄存器,然后对数据进行处理。当程序计数器PC作为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而可以实现程序流程的跳转。
示例:
1 | LDR R0,[R1] ;将存储器地址为R1的字数据读入寄存器R0。 |
{!}为可选后缀,若选用该后缀,则当数据传送完毕之后,将最后的地址写入基址寄存器,否则基址寄存器的内容不改变。
2.2.1.2 [STR
指令]
注意: LDR/STR
指令是ARM
汇编中使用频率最高的一对指令
格式 : STR{条件} 源寄存器,<存储器地址>;
作用 : STR指令用于从源寄存器中将一个32位的字数据传送到存储器中。
示例:
1 | STR R0,[R1],#8 ;将R0中的字数据写入以R1为地址的存储器中,并将新地址R1+8写入R1。 |
2.2.1.3 [LDRB
指令]
格式 : LDR{条件}B 目的寄存器,<存储器地址>
作用 : LDRB指令用于从存储器中将一个8位的字节数据传送到目的寄存器中,同时将寄存器的高24位清零。
该指令通常用于从存储器中读取8位的字节数据到通用寄存器,然后对数据进行处理。当程序计数器PC作为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而可以实现程序流程的跳转。
示例:
1 | LDRB R0,[R1] ;将存储器地址为R1的字节数据读入寄存器R0,并将R0的高24位清零。 |
2.2.1.4 [STRB
指令]
格式 : STR{条件}B 源寄存器,<存储器地址>
作用 : STRB指令用于从源寄存器中将一个8位的字节数据传送到存储器中。该字节数据为源寄存器中的低8位。
示例:
1 | STRB R0,[R1] ;将寄存器R0中的字节数据写入以R1为地址的存储器中。 |
2.2.1.5 [LDRH / STRH
指令]
格式:
- STR{条件}H 源寄存器,<存储器地址>
- LDR{条件}H 目的寄存器,<存储器地址>
作用 :
- STRH指令用于从源寄存器中将一个16位的半字数据传送到存储器中。该半字数据为源寄存器中的低16位。
- LDRH指令用于从存储器中将一个16位的半字数据传送到目的寄存器中,同时将寄存器的高16位清零。
2.2.1.6 [LDM / STM
指令]
格式 : LDM(或STM){条件}{类型} 基址寄存器{!},寄存器列表{∧}
作用 :
- 批量加载/储存指令 , 在一组寄存器和内存间传输数据
- 常与堆栈格式组合使用 . 以模拟堆栈操作
不同类型的堆栈:
示例:
1 | LDMFD SP!,{R0-R2,R14} ;将内存栈中的数据依次弹出到R14、R2、R1、R0 |
注意:
- 每入栈一个元素, 栈指针
SP
都会往栈增长的方向移动一个存储单元. - 栈是先入后出(FILO) , 所以
STMFD
指令会根据{ }
中的寄存器列表 ,从左到右压入栈 LDMFD
指令出栈时,顺序相反
入栈与出栈:
注意:
也可以通过PUSH
/POP
指令来执行栈元素的入栈和出栈操作
1 | PUSH {R0-R2, R14} ;将RO、R1、R2、R14依次压入栈, |
2.2.2 数据传送指令
2.2.2.1 [MOV
指令]
格式: MOV{条件}{S} 目的寄存器,源操作数
作用 :
- 是把一个寄存器的值(要能用立即数表示)赋给另一个寄存器,或者将一个常量赋给寄存器,将后边的量赋给前边的量。
MOV
指令中,条件缺省时指令无条件执行;- {S}用来表示是否影响
CPSR
寄存器的值,如MOVS
指令就会影响寄存器CPSR
的值,而MOV
则不会
示例:
1 | MOV R1,R0 ;将寄存器R0的值传送到寄存器R1 |
2.2.2.2 [MVN
指令]
格式 : MVN{条件}{S} 目的寄存器,源操作数
作用 :
- MVN指令用来将操作数operand2按位取反后传送到目标寄存器Rd
- 与MOV指令不同的是 ,在传送之前就按位取反了
示例:
1 | MVN R0,#0xFF ;将立即数0xFF取反后赋值给R0 |
2.2.3 算术逻辑运算指令
2.2.3.1 算术指令
算术指令包括 基本的加、减、乘、除
格式:
1 | ADD {cond} {S} Rd, Rn, operand2 ;加法 |
示例:
1 | ADD R0,R1,R2 ; R0 = R1 + R2 |
2.2.3.2 逻辑指令
逻辑运算指令包括 (与、或、非、异或、清除等
格式:
1 | AND {cond} {S} Rd, Rn, operand2 ;逻辑与运算(常用于屏蔽操作数1 中的某些位 ) |
示例:
1 | AND R0,R0,#3 ; 该指令保持R0的0、1位,其余位清零。 ( R0 = R0 & 0011 ) |
2.2.4 比较指令
2.2.4.1 [CMP
直接比较指令]
格式 : CMP{条件} 操作数1,操作数2
作用 :
CMP
指令用于把一个寄存器的内容和另一个寄存器的内容或立即数进行比较 , 同时影响CPSR寄存器的N、Z、C、V标志位- 该指令进行一次减法运算,但不存储结果,只更改条件标志位。标志位表示的是操作数1与操作数2的关系(大、小、相等)
CPSR寄存器标志位:
示例:
1 | CMP R1,R0 ;将寄存器R1的值与寄存器R0的值相减,并根据结果设置CPSR的标志位 |
注意:
-
比较指令的运行结果Z=1时,表示运算结果为零,两个数相等;
-
N=1表示运算结果为负,
-
N=0表示运算结果为非负,即运算结果为正或者为零。
2.2.4.2 [CMN
负数比较指令]
格式: CMN{条件} 操作数1,操作数2
作用: CMN指令用于把一个寄存器的内容和另一个寄存器的内容或立即数取反后进行比较,同时更新CPSR中条件标志位的值。
示例:
1 | CMN R0, #1 ;将立即数取负,然后比较大小 |
2.2.5 跳转指令
2.2.5.1 [B label
指令]
格式 : B {cond} Label
作用:
- 程序无条件跳转到标号Label处执行,
- 它是 24 位有符号数,左移两位后有符号扩展为 32 位. 表示跳转范围[0 , 32MB]
- 无条件跳转指令B主要用在循环 , 分支结构
示例:
1 | B Label ;程序无条件跳转到标号Label处执行 |
2.2.5.2 [BL
带链接的跳转]
格式 : BL{条件} 目标地址
作用 :
- 但跳转之前,会在寄存器
RL
(即R14
)中保存PC的当前内容- BL指令一般用在函数调用的场合
示例:
1 | 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组合在一起使用,构成一个循环程序结构。
1 | AREA COPY,CODE,READOLY |
2.3 伪指令
伪指令有点类似C语言中的预处理命令,在程序编译时,这些伪指令会被翻译为一条或多条ARM标准指令。常见的ARM伪指令主要有4个: ADR、ADRL、LDR、NOP,
它们的使用示例如下:
1 | ADR RO, LOOP ;将标号LOOP的地址保存到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 段共享同一段存储单元。
使用示例:
1 | AREA Init , CODE , READONLY ;该伪指令定义了一个代码段,段名为 Init ,属性为只读。 |
2.4.2 [ALIGN
]
语法格式: [ALIGN { 表达式 { ,偏移量 }} ]
作用 : 地址对齐
- ALIGN 伪指令可通过添加填充字节的方式,使当前位置满足一定的对齐方式。
- 其中,表达式的值用于指定对齐方式,可能的取值为2的幂,如 1 、2 、4 、8 、16 等。
- 若未指定表达式,则将当前位置对齐到下一个字的位置。
- 偏移量也为一个数字表达式,若使用该字段,则当前位置的对齐方式为:2的表达式次幂+偏移量。
使用示例:
1 | AREA Init,CODE ,READONLY,ALIEN=3 ;指定后面的指令为 8 字节对齐。 |
2.4.3 [CODE16
/ CODE32
]
语法格式: CODE16 (或 CODE32 )
作用 :
- CODE16 伪指令通知编译器,其后的指令序列为 16 位的 Thumb 指令。
- CODE32 伪指令通知编译器,其后的指令序列为 32 位的 ARM 指令。
- 在使用 ARM 指令和 Thumb 指令混合编程的代码里,可用这两条伪指令进行切换,但注意他们只通知编译器其后指令的类型,并不能对处理器进行状态的切换。
使用示例:
1 | AREA Init ,CODE ,READONLY |
2.4.4 [ENTRY
]
语法格式 : ENTRY
作用:
- ENTRY 伪指令用于指定汇编程序的入口点。
- 在一个完整的汇编程序中至少要有一个 ENTRY (也可以有多个)
- 当有多个 ENTRY 时,程序的真正入口点由链接器指定),但在一个源文件里最多只能有一个 ENTRY (可以没有)。
使用示例:
1 | AREA Init , CODE , READONLY |
2.4.5 [END
]
语法格式 : END
作用: END 伪指令用于通知编译器已经到了源程序的结尾。
使用示例:
1 | AREA Init , CODE , READONLY |
3️⃣ 作用域
3.1 头文件的内容
- 函数原型
- 使用
#define
和const
定义的符号常量 - 结构声明
- 类声明
- 模版声明
- 内联函数
注意: 不要将函数定义和变量声明放在头文件中,容易导致重定义。 除非该函数是内联函数
3.2 #inlcude
中“ ” 和 < >的区别
3.3 内存在程序中保留的时间
- 自动储存:局部变量、函数参数 在程序开始执行其所属的函数和代码块时被创建, 在执行完函数和代码块时,内存被释放
- 静态储存:全局变量、
staic
关键字定义的局部变量和全局变量 。在整个程序运行过程中都存在。 - 动态存储: 用
new
分配的内存会一直存在,直到使用delete
关键字将其释放。 也称为堆
3.4 作用域和链接
- 作用域: 值在上面范围内能看到这个(函数/变量 ),描述了名称在文件的多大范围内可见。
- 链接性: 描述了名称在不同单元键的共享
局部变量作用域: 作用域只在定义它的代码块中
全局变量作用域: 作用域为定义位置到文件结尾
自动变量作用域: 作用域为局部
函数体作用域: 整个类或整个名称空间,但不能是局部的
[例如: 局部变量和函数形参的 储存持续性为自动,作用域为局部; 没有链接性。 当程序开始执行时,为该变量分配内存,当函数结束时,这些变量会消失。]
注意:
3.5 静态储存的链接性
3.5.1 静态变量的链接性:
- 外部链接性: 可在其他文件中访问
- 内部链接性: 只能在当前文件中访问
- 无链接性: 只能在当前函数或代码块中访问
静态变量的数量在程序运行期间是不变的, 编译器会分配固定的内存块来储存所有的静态变量,这些变量在程序
执行期间一直存在。
1 | int a = 0; // 静态持续变量,连接性为外部 |
特别注意: 变量c是储存在静态数据区的,会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。
即使funct
函数没有被执行,c变量也是留在内存中的,而变量d则会消失。
3.5.2 静态变量的初始化特性
所有静态持续变量在初始化时都会被初始化为0 ,称为零初始化 。
在静态数据区,内存中所有的字节默认值都是 0x00
,某些时候这一特点可以减少程序员的工作量。比如初始化一个稀疏矩阵,我们可以一个一个地把所有元素都置 0,然后把不是 0 的几个元素赋值。如果定义成静态的,就省去了一开始置 0 的操作。再比如要把一个字符数组当字符串来用,但又觉得每次在字符数组末尾加 \0
太麻烦。如果把字符串定义成静态的,就省去了这个麻烦,因为那里本来就是 \0
。