ARM体系结构
⭐ 关联知识点:指令集
计算机的指令集一般可分为4种:复杂指令集(CISC)、精简指令集(RISC) 、显式并行指令集 (EPIC)和超长 指 令 字 指 令 集(VLIW)。嵌入式用的是RISC指令集,RISC指令集相对于CISC指令集,主要有以下特点
●Load/Store架构,CPU不能直接处理内存中的数据,要先将内存中的数据Load(加载)到寄存器中才能操作,然后将处理结果Store(存储)到内存中。
●固定的指令长度,单周期指令。
●倾向于使用更多的寄存器来存储数据,而不是使用内存中的堆栈,效率更高。ARM指令集虽然属于RISC,但是和RISC相比,有一些差异如下:
●ARM有桶型移位寄存器,单周期内可以完成数据的各种移位操作。
●并不是所有的ARM指令都是单周期的。
●ARM有16位的Thumb指令集,是32位ARM指令集的压缩形式,提
高了代码密度。
●条件执行:通过指令组合,减少了分支指令数目,提高了代码
密度。
●增加了DSP、SIMD/NEON等指令。
⭐
关联知识点:用户态、内核态、中断
ARM处理器有多种工作模式,应用程序正常运行时,ARM处理器工作在用户模式,当程序运行出错或有中断发生时,ARM处理器就会切换到对应的特权工作模式。用户模式运行不了特权指令,需要切换到特权模式下才能运行。在ARM处理器中,除了用户模式是普通模式,剩下的几种工作模式都属于特权模式。
在ARM处理器内部,除了基本的算术运算单元、逻辑运算单元、浮点运算单元和控制单元,还有一系列寄存器,包括各种通用寄存器、状态寄存器、控制寄存器,用来控制处理器的运行,保存程序运行时
的各种状态和临时结果。
ARM处理器中的寄存器可分为通用寄存器和专用寄存器两种。
寄存器R0~R12属于通用寄存器,除了FIQ工作模式,在其他工作模式下这些寄存器都是共用、共享的:
- R0~R3通常用来传递函数参数
- R4~R11用来保存程序运算的中间结果或函数的局部变量等,R12常用来作为函数调用过程中的临时寄存器。
ARM处理器有多种工作模式,除了这些在各个模式下通用的寄存器,还有一些寄存器在各自的工作模式下是独立存在的,如R13、R14、R15、CPSP、SPSR寄存器,在每个工作模式下都有自己单独的寄存器。
- R13寄存器又称为堆栈指针寄存器,用来维护和管理函数调用过程中的栈帧变化,R13总是指向当前正在运行的函数的栈帧,一般不能再用作其他用途。
- R14寄存器又称为链接寄存器,在函数调用过程中主要用来保存上一级函数调用者的返回地址。
- 寄存器R15又称为程序计数器(PC),保存的地址中取的,每取一次指令,PC寄存器的地址值自动增加。CPU一条一条不停地取指令,程序也就源源不断地一直运行下去。在ARM三级流水线中,PC指针的值等于当前正在运行的指令地址+8,后续的32位处理器虽然流水线的级数不断增加,但为了简化编程,PC指针的值继续延续了这种计算方式。
- 当前处理器状态寄存器(Current Processor State Register CPSR)主要用来表征当前处理器的运行状态。除了各种状态位、标志位,CPSR寄存器里也有一些控制位,用来切换处理器的工作模式和中断使能控制。
在每种工作模式下,都有一个单独的程序状态保存寄存器(Saved Processor State Register,SPSR)。当ARM处理器切换工作模式或发生异常时,SPSR用来保存当前工作模式下的处理器现场,即将CPSR寄存器的值保存到当前工作模式下的SPSR寄存器。当ARM处理器从异常返回时,就可以从SPSR寄存器中恢复原先的处理器状态,切换到原来的工作模式继续运行。
在ARM所有的工作模式中,有一种工作模式比较特殊,即FIQ模式。为了快速响应中断,减少中断现场保护带来的时间开销,在FIQ工作模式下,ARM处理器有自己独享的R8~R12寄存器。
ARM汇编指令
一个完整的ARM指令通常由操作码+操作数组成,指令的编码格式如下。
<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:第二个可选操作数,灵活使用第二个操作数可以提
高代码效率。
存储访问指令
ARM指令集属于RISC指令集,RISC处理器采用典型的加载/存储体系结构,CPU无法对内存里的数据直接操作,只能通过Load/Store指令来实现:当需要对内存中的数据进行操作时,要首先将这个数据从内存加载到寄存器,然后在寄存器中对数据进行处理,最后将结果重新存储到内存中。
ARM处理器属于冯·诺依曼架构,程序和数据都存储在同一存储器上,内存空间和I/O空间统一编址,ARM处理器对程序指令、数据、I/O空间中外设寄存器的访问都要通过Load/Store指令来完成。
LDR R1,[RO]; 将R中的值作为地址,将该地址上的数据保存到R1
STR R1,[RO]; 将R中的值作为地址,将R1中的值存储到这个内存地址;每次读写一字节,LDR/STR默认每次读写4字节
LDRB/STRB; 批量加载/存储指令,在一组寄存器和一片内存之间传输数据
SWPR1,R1,[R0]; 将R1与R中地址指向的内存单元中的数据进行交换
SWPR1,R2,[R0]; 将[R0]存储到R1,将R2写入[R0]这个内存存储单元LDM/STM
LDR/STR、LDM/STM两对指令经常使用。
LDR/STR指令是ARM汇编程序中使用频率最高的一对指令。
LDM/STM指令常用来加载或存储一组寄存器到一片连续的内存,通过和堆栈格式符组合使用,LDM/STM指令还可以用来模拟堆栈操作。
将一组寄存器入栈,或者从栈中弹出一组寄存器。
LDMFD SP!,{R0-R2,R14}; 将内存栈中的数据依次弹出到R14,R2,R1,RO
STMFD SP!{RO-R2,R14}; 将R,R1,R2,R14依次压入内存栈
ARM还专门提供了PUSH和POP指令来执行栈元素的入栈和出栈操作。
PUSH {R0-R2,R14} ;将 R0、R1、R2、R14依次压入栈
POP {R0-R2,R14} ;将栈中的数据依次弹出到R14、R2、R1、R0
在一个堆栈内存结构中,如果堆栈指针SP总是指向栈顶元素,那么这个栈就是满栈;如果堆栈指针SP指向的是栈顶元素的下一个空闲的存储单元,那么这个栈就是空栈。
每入栈一个元素,栈指针SP都会往栈增长的方向移动一个存储单元。如果栈指针SP从高地址往低地址移动,那么这个栈就是递减栈;如果栈指针SP从低地址往高地址移动,那么这个栈就是递增栈。
数据传送指令
LDR/STR指令用来在寄存器和内存之间输送数据。想要在寄存器之间传送数据,则可以使用MOV指令。
MOV {cond} {s} Rd, operand2
MVN {cond} {S} Rd, operand2
如果想要在寄存器之间传送数据,则可以使用MOV指令。MVN指令用来将操作数operand2按位取反后传送到目标寄存器Rd。
MOV R1,#1 ;将立即数1传送到寄存器R1中
MOV R1, RO ;将R寄存器中的值传送到R1寄存器中
MOV PC, LR ;子程序返回
MVN RO,#xFF ;将立即数 0xFF 取反后赋值给 R
MVN RO, R1 ;将R1寄存器的值取反后赋值给R
算术逻辑运算指令
算术运算指令包括基本的加、减、乘、除,逻辑运算指令包括与、或、非、异或、清除等
ADD {cond} {S} Rd, Rn, operand2 ;加法
ADC {cond} {S} Rd, Rn, operand2 ;带进位加法
SUB {cond} {S} Rd, Rn, operand2 ;减法
AND {cond} {S} Rd, Rn, operand2 ;逻辑与运算
ORR {cond} {S} Rd, Rn, operand2 ;逻辑或运算
EOR {cond} {S} Rd, Rn, operand2 ;异或运算
BIC {cond} {S} Rd, Rn, operand2 ;位清除指令
算术逻辑运算指令的基本使用方法及说明如下
ADD R2,R1,#1 ; R2=R1+1
ADC R1,R1,#1 ; R1=R1+1+C(其中C为CPSR寄存器中进位)
SUB R1, R1, R2 ; R1=R1-R2
SBC R1,R1,R2 ;R1=R1-R2-C
AND RO,RO,#3 ; 保留 R的 bit0和 1,其余位清除
ORR RO,RO,#3 ; 置位 R0的 bit0 和 bit1
EOR RO,RO,#3 ; 反转 R0中的 bit 和 bit1
BIC RO,RO,#3 ; 清除 R0中的 bit0和 bit1
操作数:operand2详解
.......
ARM寻址方式
不同的ARM指令又有不同的寻址方式,比较常见的寻址方式有寄存器寻址、立即寻址、寄存器偏移寻址、寄存器间接寻址、基址寻址、多寄存器寻址、相对寻址等.
寄存器寻址
寄存器寻址比较简单,操作数保存在寄存器中,通过寄存器名就可以直接对寄存器中的数据进行读写.
MOV R1, R2 ;将寄存器R2中的值传送到R1
ADD R1,R2,R3 ;运行减法运算R2-R3,并将结果保存到R1中
立即数寻址
ADD R1,R1,#1 ;将R1寄存器中的值加1,并将结果保存到R1中
ADDR1,R1,#16,20; R1=R1+立即数16循环右移20位
MOV R1,#xFF ;将十六进制常数0xFF写到R1寄存器中
MOV R1,#12 ; 将十进制常数12放到R1寄存器中
寄存器偏移寻址
寄存器间接寻址
寄存器间接寻址主要用来在内存和寄存器之间传输数据。寄存器中保存的是数据在内存中的存储地址,通过这个地址就可以在寄存器和内存之间传输数据。C语言中的指针操作,在汇编层次其实就是使用寄存器间接寻址实现的。
LDRR1,[R2]; 将R2中的值作为地址,取该内存地址上的数据,保存到R1
STRR1,[R2]; 将R2中的值作为地址,将R1寄存器的值写入该内存地址
基址寻址
基址寻址其实也属于寄存器间接寻址。两者的不同之处在于,基址寻址将寄存器中的地址与一个偏移量相加,生成一个新地址,然后基于这个新地址去访问内存.
LDRR1,[FP,#2] ;将FP中的值加2作为新地址,取该地址上的值保存到R1
LDR R1,[FP,#2]! ; FP=FP+2,然后将FP指定的内存单元数据保存到R1中
LDRR1,[FP,R0] ;将FP+R0作为新地址,取该地址上的值保存到 R1
LDR R1,[FP,RO,LSL#2];将FP+R0<<2作为新地址,读取该内存地址上的值保存到 R1
LDR R1,[FP],#2 ;将FP中的值作为地址,读取该地址上的值保存到R1,然后 FP 中的值加 2
STRR1,[FP,#-2] ;将FP中的值减 2,作为新地址,将 R1中的值写入该地址
STR R1,[FP],#-2] ;将FP中的值作为地址,将R1中的值写入此地址,然后FP中的值减2
基址寻址一般用在查表、数组访问、函数的栈帧管理等场合。根据偏移量的正负,基址寻址又可以分为向前索引寻址和向后索引寻址.
多寄存器寻址
STM/LDM指令就属于多寄存器寻址,一次可以传输多个寄存器的值。
LDMIA SP!,{R0-R2,R14};将内存栈中的数据依次弹出到 R14、R2、R1、RO
STMDB SP!,{RO-R2,R14};将RO、R1、R2、R14 依次压入栈
LDMFD SP!,{R0-R2,R14};将内存栈中的数据依次弹出到 R14、R2、R1、R0
STMFDSP!,{RO-R2,R14};将R、R1、R2、R14 依次压入栈
在多寄存器寻址中,用大括号{}括起来的是寄存器列表,寄存器之间用逗号隔开,如果是连续的寄存器,还可以使用连接符连接,如R0-R3,就表示R0、R1、R2、R3这4个寄存器。LDM/STM指令一般和IA、IB、DA、DB组合使用,分别表示Increase After、Increase Before、Decrease After、Decrease Before。LDM/STM指令也可以和FD、ED、FA、EA组合使用,用于堆栈操作。
相对寻址
相对寻址属于基址寻址,只不过是基址寻址的一种特殊情况。
以PC指针作为基地址进行寻址,以指令中的地址差作为偏移,两者相加后得到的就是一个新地址,然后可以对这个地址进行读写操作。
ARM中的B、BL、ADR指令都是采用相对寻址的。
很多与位置无关的代码,如动态链接共享库,其在汇编代码层次的实现其实也是采用相对寻址的。程序中使用相对寻址访问的好处是不需要重定位,将代码加载到内存中的任何地址都可以直接运行。
ARM伪指令
ARM伪指令不是ARM指令集中定义的标准指令,而是为了编程方便,编译器厂商自定义的一些辅助指令。在程序编译时,这些伪指令会被翻译为一条或多条ARM标准指令。常见的ARM伪指令主要有4个:ADR、ADRL、LDR、NOP。
LDR伪指令
LDR伪指令的主要用途是将一个32位的内存地址保存到寄存器中 。
RISC指令的特点是单周期指令,指令的长度固定。在一个32位的系统中,一条指令通常是32位的,指令中包括操作码和操作数。
指令中的操作码和操作数共享32位的存储空间:一般前面的操作码要占据几个比特位,剩下来的留给操作数的编码空间就小于32位。当编译器遇到MOV R0,#0x30008000这条指令时,后面的操作数是32位,编译器就无法对这条指令进行编码了。为了解决这个难题,编译器提供了一个LDR伪指令来完成上面的功能。
LDR不是普通的ARM加载指令,而是一个伪指令。为了与ARM指令集中的加载指令LDR区别开来,LDR伪指令中的操作数前一般会有一个等于号=,用来表示该指令是个伪指令。通过LDR伪指令,编译器就解决了向一个寄存器传送32位的立即数时指令无法编码的难题。
LDR R,=0x30008000; 有=号的就是伪指令,将立即数x30008000送到R0
LDR RO, =LOOP ;将标号LOOP表示的地址送到R0
LDR RO,[R1] ;R1中的值作为地址,将该地址上的值送到R0
LDR RO,LOOP ;将标号LOOP表示的内存地址上的数据送到R0
ADR伪指令
START
ADR R0, LOOP ; 将标号 LOOP 对应的地址加载到寄存器 R0 中
B END ; 跳转到 END 标签,结束程序
LOOP
NOP ; 一个空操作,作为示例使用
B LOOP ; 无限循环
ADR R0, LOOP:这一行使用了 ADR
伪指令,它将标号 LOOP
的地址加载到寄存器 R0
中。ADR
指令会计算当前指令地址与标号 LOOP
之间的偏移量,然后通过相对寻址将 LOOP
的地址加载到 R0
中。ADR伪指令的功能与LDR伪指令类似,将基于PC相对偏移的地址值读取到寄存器中。ADR为小范围的地址读取伪指令,底层使用相对寻址来实现,因此可以做到代码与位置无关。
编译器在编译ADR伪指令时,会首先计算出当前正在执行的ADR伪指令地址与标号LOOP之间的地址偏移OFFSET,然后使用ARM指令集中的一条标准指令代替。
ARM汇编程序设计
ARM汇编程序格式
ARM汇编程序是以段(section)为单位进行组织的。在一个汇编文件中,可以有不同的section,分为代码段、数据段等,各个段之间相互独立,一个ARM汇编程序至少要有一个代码段。可以使用AREA伪操作来标识一个段的起始、段名和段的读写属性。
AREA COPY, CODE, READONLY ; 当前段属性为代码段,只读,段名为 COPY
ENTRY
START
LDR R0, =SRC ; 将源地址 SRC 加载到寄存器 R0 中
LDR R1, =DST ; 将目标地址 DST 加载到寄存器 R1 中
MOV R2, #10 ; 将值 10 加载到寄存器 R2 中
LOOP
LDR R3, [R0], #4 ; 从 R0 地址加载一个字到 R3 中,并将 R0 增加 4
STR R3, [R1], #4 ; 将 R3 中的值存储到 R1 指向的地址,并将 R1 增加 4
SUBS R2, R2, #1 ; 将 R2 减 1,并设置条件标志
BNE LOOP ; 如果 R2 不为 0,则跳转回 LOOP
AREA COPYDATA, DATA, READWRITE ; 数据段,读写权限,段名为 COPYDATA
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
在汇编程序中,使用分号;来注释代码
符号与标号
使用符号来标识一个地址、变量或数字常量。当用符号来标识一个地址时,这个符号通常又被称为标号。符号的命名规则和C语言的标识符命名规则一样:由字母、数字和下画线组成,符号的开头不能使用数字,但标号除外。标号比较任性,标号的开头不仅可以是数字,甚至整个标号可以是一个纯数字。
局部标号
局部标号用于标记程序中的特定位置,以便在该位置附近进行跳转或引用。与全局标号不同,局部标号的作用域通常仅限于当前段或当前范围内,不会影响到其他代码段。局部标号常常使用数字(如 0
, 1
, 2
等)来命名,并通过特定的格式来引用。
作用域有限:局部标号的作用域只在当前段内有效。这意味着同一个局部标号可以在不同的段中重复使用,而不会产生冲突。
命名方式简单:局部标号通常使用数字命名,如 0
, 1
, 2
等,这使得它们在短距离跳转和循环结构中非常方便。
引用格式:引用局部标号时,使用 %
符号加上 B
(向后搜索)或 F
(向前搜索)来确定跳转方向。
如果未指定 F
或 B
,编译器会默认先向后搜索,如果找不到,再向前搜索。
MOV R0, #10 ; 将值 10 存入寄存器 R0 中
0 SUBS R0, R0, #1 ; 将 R0 减 1,并设置条件标志
BNE %B0 ; 如果 R0 不为零,则跳转回局部标号 0
伪操作
伪操作(也称为伪指令)是在汇编语言中使用的一类特殊指令,用来辅助编写和组织汇编代码。与真正的机器指令不同,伪操作并不会直接转换为机器码,而是为汇编器提供了指示,用于控制汇编过程、数据定义、段定义以及程序结构管理等功能。伪操作主要用于提高代码的可读性和结构化,让程序员更方便地编写和管理汇编程序。
符号定义:使用伪操作定义变量、常量或符号。例如,可以使用 EQU
指令将一个符号绑定到一个数值或表达式上。
数据定义:用于定义数据段中的变量和常量。例如,DCD
、DCB
、SPACE
等伪操作用来在内存中分配特定的数据空间。
程序结构控制:用于控制汇编程序的流程和组织。例如,AREA
用于定义段(section),ENTRY
用于指定程序的入口点,EXPORT
和 IMPORT
用于声明全局符号和导入其他模块中的符号。
模块化编程:伪操作可以用于定义汇编子程序,并通过伪操作声明全局符号,以便在主程序或其他程序中调用这些子程序。
以下是一些常用的伪操作及其使用方法:
AREA:定义一个段或区域。
AREA MyCode, CODE, READONLY
DCD:定义一个32位常量。
DCD 1, 2, 3, 4
DCB:定义一个字节常量。
DCB 'A', 'B', 'C'
EQU:定义一个符号常量。
MyValue EQU 10
EXPORT:将符号声明为全局符号,允许其他模块访问。
EXPORT MyFunction
IMPORT:导入外部模块中的符号。
IMPORT ExternalFunction
ENTRY:指定程序的入口点。
ENTRY
SPACE:在内存中分配一定数量的字节。
SPACE 16 ;分配16字节
伪操作的作用
伪操作的主要作用是帮助汇编器组织和处理汇编代码,而不是转换成机器码。它们在汇编代码的预处理中起到重要作用,使代码更加灵活和易于维护。例如,使用 EXPORT
和 IMPORT
伪操作,可以轻松实现模块化编程,使得不同的汇编程序模块可以相互调用。
C语言和汇编语言混合编程
ATPCS规则
ATPCS的全称是ARM-Thumb Procedure Call Standard,核心内容就是定义ARM子程序调用的基本规则及堆栈的使用约定等。
如ATPCS规定了ARM程序要使用满递减堆栈,入栈/出栈操作要使用STMFD/LDMFD指令,只要所有的程序都遵循这个约定,ARM程序的格式也就统一了,编写的ARM程序也就可以在各种各样的ARM处理器上运行。
子程序间要通过寄存器R0~R3(可记作a0~a3)传递参数,当参数个数大于4时,剩余的参数使用堆栈来传递。
●子程序通过R0~R1返回结果。
●子程序中使用R4~R11(可记作v1~v8)来保存局部变量。
●R12作为调用过程中的临时寄存器,一般用来保存函数的栈帧
基址,记作FP。
●R13作为堆栈指针寄存器,一般记作SP。
●R14作为链接寄存器,用来保存函数调用者的返回地址,记作LR。
●R15作为程序计数器,总是指向当前正在运行的指令,记作PC。
在ARM平台下,无论是C程序,还是汇编程序,只要大家遵守ARM子程序之间的参数传递和调用规则,就可以很方便地在一个C程序中调用汇编子程序,或者在一个汇编程序中调用C程序。
AREA SumFunc, CODE, READONLY
EXPORT sum ; 声明sum为全局函数,使其可以被C代码调用
sum PROC
;输入:R0 和 R1 为两个加数
;输出:R0 为和
ADD R0, R0, R1 ; 执行加法操作,将结果存储到R0中
BX LR ; 返回调用者
ENDP
END
#include <stdio.h>
extern int sum(int a, int b); // 声明汇编函数
int main() {
int result = sum(5, 7); // 调用汇编函数sum
printf("The result is: %d\n", result); // 打印结果
return 0;
}
无条件跳转:BX
指令将控制权转移到指定的地址(寄存器中的值)。
在C程序中内嵌汇编代码
为了能在C程序中内嵌汇编代码,ARM编译器在ANSI C标准的基础上扩展了一个关键字__asm。通过这个关键字,可以在C程序中内嵌ARM汇编代码。
int src[10] = {1,2,3,4,5,6,7,8,9};
int dst[10] = {0};
int data_copy_c(void){
for(int i = 0; i < 10; i++)
dst[i] = src[i];
return 0;
}
int data_copy_asm(void){
__asm
{
LDR R0, =src
LDR R1, =dst
MOV R2, #10
LOOP:
LDR R3, [R0], #4
STR R3, [R1], #4
SUBS R2, R2, #1
BNE LOOP
}
}
在汇编程序中调用C程序
在汇编程序中同样也可以调用C程序。在调用的时候,我们要注意根据ATPCS规则来完成参数的传递,并配置好C程序传递参数和保存局部变量所依赖的堆栈环境,然后使用BL指令直接跳转即可。
GNU ARM汇编语言
........