Linux-ARM汇编及ARM片内寄存器

简介

汇编语言在嵌入式驱动开发中通常只会用到很小的一部分,主要用于处理特定的底层操作和性能优化。大部分驱动程序仍然是用高级语言(如C语言)编写的,而汇编语言则是为了在某些特殊需求下提供更高级别的控制和优化手段。因此学习ARM汇编语言可帮助嵌入式工程师更好地理解底层硬件、进行性能优化、调试和故障排除以及移植和优化现有代码。

在靠近硬件的编程部分,通常使用 C/C++ 来实现,这是因为 C 足够底层,提供了直接对内存的精准操控,这是某些上层语言所不具备的功能,而应用程序员写代码本质上都是在内存中对各种数据进行处理。但在贴近硬件的一侧,还有很多 C 语言所做不到的事,需要汇编语言来实现,比如:获取当前栈指针、操作 CPU 内部的状态寄存器,此时就需要直接使用汇编来实现更灵活的操作。

汇编和 C 语言各有着特点:C 语言有更好的开发效率,而执行效率比汇编要低;而汇编在编码速度、代码可读性、可维护性等方面的缺陷是非常明显的,而汇编带来的执行效率的提升也被硬件的高速发展稀释,所以只有在必要情况下,才会使用汇编语言来实现功能,所说的必要情况通常是两种情况:

  • C 语言做不了但是汇编能做的事:最典型的就是系统的 bootloader 部分,C 语言的执行是依赖于一定的执行环境的,而这个环境需要更底层的汇编语言给它创造,所以操作系统的 bootloader 的 early start 部分由汇编实现,除了这些还有很多硬件强相关的操作都需要汇编来做,比如进程切换时、中断的初始化。
  • 对执行效率有极高要求的情况,最典型的情况在于系统执行非常频繁的部分,因为执行非常频繁,很小的效率提升叠加起来也要重视。

系统级芯片例如:Cortex-A系列芯片刚一上电 SP 指针还没初始化,C 环境还没准备好,所以肯定是不能运行 C 代码的,此时就必须先用汇编语言设置好 C 语言的运行环境,比如初始化 DDR、设置 SP指针等等,这段就属于U-boot,bootloader操作。然后等汇编把 C 运行环境设置好了以后才可以运行 C 代码。所以 Cortex-A 一开始肯定是跑的汇编代码。其实STM32也一样的,一开始也是汇编,以 STM32F103 为例,启动文件startup_stm32f10x_hd.s 就是汇编文件,只是这个文件 ST已经写好了,我们不用去修改。

Cortex-A7处理器的9种运行模式:

Linux-ARM汇编及ARM片内寄存器_第1张图片

USR、SVC和IRQ最为常用。 

ARM-A寄存器

要读懂ARM汇编,首先就必须对ARM片内寄存器有一个基础的认知(很重要,因为汇编的命令一般都是在操作CPU的片内寄存器),在ARM32架构下,CPU提供使用了一组通用寄存器、程序状态寄存器、浮点寄存器。

通用寄存器:

共有16个32位的通用寄存器,用于存储数据和中间计算结果。它们通常被表示为R0到R15。

  • R0至R12:通用寄存器,用于存储临时数据、计算结果和函数参数等。这些寄存器在函数调用中可能会被破坏,所以在函数调用之前需要保存它们的值。

  • R13(SP,堆栈指针寄存器):堆栈指针寄存器用于指示当前活跃进程或函数的栈顶位置,存储栈的地址信息。

  • R14(LR,链接寄存器):链接寄存器用于存储函数调用的返回地址。当一个函数被调用时,程序的返回地址会被保存在R14中,从而在函数执行完毕后可以跳回到调用函数的位置。

  • R15(PC,程序计数器寄存器):程序计数器寄存器保存当前正在执行的指令地址,即下一条要执行的指令的地址。当指令执行完毕后,程序计数器会自动递增,指向下一条指令的地址。

程序状态寄存器:

当前程序状态寄存器(CPSR):用于存储当前执行模式的状态以及控制指令执行的条件。它包含了许多与程序状态相关的信息,例如程序运行模式、条件码等。CPSR寄存器可以用于控制程序的执行和处理异常情况。

程序状态保存寄存器(SPSR):用于保存异常或中断发生时的先前程序状态,包括条件码、处理器模式等。

浮点寄存器:

ARM处理器还提供了一组浮点寄存器(F0-F31),用于浮点运算和浮点数据存储。

Linux-ARM汇编及ARM片内寄存器_第2张图片

Cortex-A7 有 9 种运行模式,每一种运行模式都有一组与之对应的寄存器组。每一种模式可见的寄存器包括 15 个通用寄存器(R0~R14)、两个程序状态寄存器和一个程序计数器 PC。在这些寄存器中,有些是所有模式所共用的同一个物理寄存器,有一些是各模式自己所独立拥有的,各个模式所拥有的寄存器如下表:
Linux-ARM汇编及ARM片内寄存器_第3张图片

浅色字体的是与 User 模式所共有的寄存器,蓝绿色背景的是各个模式所独有的寄存器。 

程序状态寄存器CPSR

所有的处理器模式都共用一个 CPSR 物理寄存器,因此 CPSR 可以在任何模式下被访问。CPSR寄存器包含了条件标志位、中断禁止位、当前处理器模式标志等一些状态位以及一些控制位。

所有的处理器模式都共用一个 CPSR 必然会导致冲突,为此,除了 User 和 Sys 这两个模式以外,其他 7 个模式每个都配备了一个专用的物理状态寄存器,叫做 SPSR(备份程序状态寄存器),当特定的异常中断发生时, SPSR 寄存器用来保存当前程序状态寄存器(CPSR)的值,当异常退出以后可以用 SPSR 中保存的值来恢复 CPSR。User 和 Sys 这两个模式不是异常模式,所以并没有配备 SPSR,因此不能在 User 和Sys 模式下访问 SPSR,会导致不可预知的结果。SPSR 是 CPSR 的备份,因此 SPSR 和CPSR 的寄存器结构相同。

N(bit31):当两个补码表示的 有符号整数运算的时候, N=1 表示运算对的结果为负数, N=0表示结果为正数。
Z(bit30): Z=1 表示运算结果为零, Z=0 表示运算结果不为零,对于 CMP 指令, Z=1 表示进行比较的两个数大小相等。
C(bit29):在加法指令中,当结果产生了进位,则 C=1,表示无符号数运算发生上溢,其它情况下 C=0。在减法指令中,当运算中发生借位,则 C=0,表示无符号数运算发生下溢,其它情况下 C=1。对于包含移位操作的非加/减法运算指令, C 中包含最后一次溢出的位的数值,对于其它非加/减运算指令, C 位的值通常不受影响。
V(bit28): 对于加/减法运算指令,当操作数和运算结果表示为二进制的补码表示的带符号数时, V=1 表示符号位溢出,通常其他位不影响 V 位。
Q(bit27): 仅 ARM v5TE_J 架构支持,表示饱和状态, Q=1 表示累积饱和, Q=0 表示累积不饱和。
IT[1:0](bit26:25): 和 IT[7:2](bit15:bit10)一起组成 IT[7:0],作为 IF-THEN 指令执行状态。
J(bit24): 仅 ARM_v5TE-J 架构支持, J=1 表示处于 Jazelle 状态,此位通常和 T(bit5)位一起表示当前所使用的指令集,如下表:

J T 描述
0 0 ARM
0 1 Thumb
1 1 ThumbEE
1 0 Jazelle

GE[3:0](bit19:16): SIMD 指令有效,大于或等于。
IT[7:2](bit15:10): 参考 IT[1:0]。
E(bit9): 大小端控制位, E=1 表示大端模式, E=0 表示小端模式。
A(bit8): 禁止异步中断位, A=1 表示禁止异步中断。
I(bit7): I=1 禁止 IRQ, I=0 使能 IRQ。
F(bit6): F=1 禁止 FIQ, F=0 使能 FIQ。
T(bit5): 控制指令执行状态,表明本指令是 ARM 指令还是 Thumb 指令,通常和 J(bit24)一起表明指令类型,参考 J(bit24)位。
M[4:0]: 处理器模式控制位,含义如下表:
Linux-ARM汇编及ARM片内寄存器_第4张图片

数据类型

ARM32架构,使用下面的后缀表示数据大小。如果没有后缀,汇编器假设操作数是unsigned word类型。

后缀 数据类型 大小
B Byte 8 位
H Halfword 16 位
W WORD 32 位
- Double Word 64 位
SB Signed Byte 8 位
SH Signed Halfword 16 位
SW Signed Word 32 位
- Double Word 64 位

ARM汇编基本指令

我们要编写的是 ARM 汇编,编译使用的 GCC 交叉编译器,所以我们的汇编代码要符合 GNU 语法。GNU 汇编语法适用于所有的架构,并不是 ARM 独享的, GNU 汇编由一系列的语句组成,
每行一条语句,每条语句有三个可选部分,如下:

label: instruction @ comment

label 即标号,表示地址位置,有些指令前面可能会有标号,这样就可以通过这个标号得到指令的地址,标号也可以用来表示数据地址。label 后面的“:”,任何以“:”结尾的标识符都会被识别为一个标号。
instruction 即指令,也就是汇编指令或伪指令。
@符号,表示后面的是注释,跟 C 语言里面的“/*”和“*/”一样,在 GNU 汇编文件中我们也可用“/*”和“*/”来注释。comment 就是注释内容。

标号和注释可以去掉,指令不可去掉。汇编语言是一种低级语言,以特定的指令格式存在

每个指令都由操作码(Opcode)和操作数(Operand)组成。操作码指定要执行的操作,例如加法、乘法、跳转等,而操作数则提供操作所需的数据。

ARM汇编指令的格式,ARM指令一般结构使用的是 三地址码 , 它的格式如下:

{}  {S}  ,,
  • : 为指令。代表具体的操作,如ADD、SUB、MOV等。

  • {}: 条件码,可选项,用于指定在特定条件下执行指令。例如:EQ表示等于、NE表示不等于等等。如果未指定条件码,则指令将在所有条件下执行。

  • {S}: 更新标志位,状态码位(2位)。可选项,表示是否更新程序状态寄存器中的标志位。如果包含S,则执行指令后,程序状态寄存器中的标志位将被更新;如果没有S,则不会更新标志位。

  • :为指令操作目的寄存器。目标寄存器。

  • :为指令第一源操作数。源寄存器。

  • 数据处理指令,表示用于执行位移或移位操作的操作数。

处理器内部数据传输指令

使用处理器做的最多事情就是在处理器内部来回的传递数据(寄存器间的)。

常见的操作有:
①、将数据从一个寄存器传递到另外一个寄存器。
②、将数据从一个寄存器传递到特殊寄存器,如 CPSR 和 SPSR 寄存器。
③、将立即数传递到寄存器。

数据传输常用的指令有三个:MOV、MRS 和 MSR。

指令 目的 描述
MOV R0 R1 将 R1 里面的数据复制到 R0 中。
MRS R0 CPSR 将特殊寄存器 CPSR 里面的数据复制到 R0 中。
MSR CPSR R1 将 R1 里面的数据复制到特殊寄存器 CPSR 里中。

MOV指令,可以把一个立即数或者寄存器值拷贝到另一个寄存器中。

MOV指令,后面添加标识数据类型的字母,确定传输的类型和如何传输数据。如果要按照字节、半字进行操作的话可以在指令“MOV”后面加上 B 或 H,比如按字节操作的指令就是 MOVB,按半字操作的指令就是MOVH。如果没有指定,汇编器假定为word。

ARM中,用 #表示立即数,这些立即数必须小于等于16位。如果大于16位,就会使用LDR指令代替。大部分的ARM指令,目的寄存器在左,源寄存器在右。(STR例外)。

MOV  r0, #3            @立即数传递到寄存器 r0
MOV  r1, r0            @寄存器 r0 拷贝到寄存器 r1	
MOVEQ r2,r1    
@如果此MOVEQ指令前一句比较指令的操作结果(通常是用SUB或CMP指令进行比较)满足"相等"条件,那么将寄存器r1的值移动到寄存器r2中

MRS 指令,用于将特殊寄存器(如 CPSR 和 SPSR)中的数据传递给通用寄存器,要读取特殊寄存器的数据只能使用 MRS 指令!使用示例如下:

MRS  R0, CPSR       @将特殊寄存器 CPSR 里面的数据传递给 R0

MSR 指令,用来将通用寄存器的数据传递给特殊寄存器,也就是写特殊寄存器,写特殊寄存器只能使用 MSR!使用示例如下:

MSR  CPSR, R0            @将 R0 中的数据复制到特殊寄存器CPSR 中

存储器/内存访问指令

搬运寄存器与内存间的数据。我们用汇编编写驱动的时候最常用的就是LDR和STR这两条指令。

ARM 不能直接访问存储器,比如 RAM 中的数据,I.MX6ULL 中的寄存器就是 RAM 类型的,我们用汇编来配置 I.MX6ULL 寄存器的时候需要借助存储器访问指令,一般先将要配置的值写入到 Rx(x=0~12)通用寄存器中,然后借助存储器访问指令将 Rx(x=0~12) 中的数据写入到 I.MX6ULL 寄存器中。读取 I.MX6ULL 寄存器也是一样的,只是过程相反。

通用寄存器:ARM架构提供的一组通用寄存器,通常用于存储临时数据、计算结果和函数调用中的参数。这些寄存器具有高速访问速度和直接访问性,可以被CPU直接读写。通用寄存器一般用于处理器内部的运算和数据传输。

I.MX6ULL寄存器:I.MX6ULL是一款基于ARM架构的嵌入式处理器。它具有一组特殊的寄存器,用于配置和控制硬件设备,如外设接口、时钟、中断控制器等。这些寄存器一般位于特定的地址空间中,通过存储器访问指令才能读写。特殊寄存器的访问需要借助特定的指令来完成。

通用寄存器主要用于处理器内部的运算和数据传输,而I.MX6ULL寄存器则用于配置和控制硬件设备。通用寄存器具有直接访问性能,而I.MX6ULL寄存器需要通过存储器访问指令间接进行访问。

常用的存储器访问指令有两种:LDR 和STR。(单寄存器的读写指令)

LDR和STR指令拷贝寄存器和内存之间的数据。

LDR把目的寄存器 Rd作为第一个参数,要读的内存地址作为第二个参数 Ra

STR把源寄存器 Rs 作为第一个参数,要写的内存地址作为第二个参数 Ra

在ARM指令集中,用方括号[ ]括起来的表示内存地址。

内存地址[Ra]中存储的数据存储到寄存器Rd中 LDR  Rd, [Ra]
将寄存器Rs中的数据存储到内存地址[Ra]中 STR  Rs, [Ra]

LDR 主要用于从存储加载数据到寄存器 Rx 中,LDR 也可以将一个立即数加载到寄存器 Rx中,LDR 加载立即数的时候要使用“=”,而不是“#”。在嵌入式开发中,LDR 最常用的就是读取 CPU 的寄存器值,比如 I.MX6ULL 有个寄存器 GPIO1_GDIR,其地址为 0X0209C004,现在读取这个寄存器中的数据(读取RAM中的数据),代码如下:

LDR   R0, =0X0209C004      @将寄存器地址 0X0209C004 加载到 R0 中
LDR   R1, [R0]             @读取R0中的地址中的数据到 R1 寄存器中

LDR是从存储器读取数据,STR就是将数据写入到存储器中,同样以I.MX6UL寄存器GPIO1_GDIR 为例,现在我们要配置寄存器 GPIO1_GDIR 的值为 0X2000002(往RAM中写数据),代码如下:

 I.MX6ULL 有个寄存器 GPIO1_GDIR,其地址为 0X0209C004
LDR  R0, =0X0209C004      @将寄存器地址 0X0209C004 加载到 R0 中
LDR	 R1, =0X20000002 	  @R1 保存要写入到寄存器的值,即 R1=0X20000002
STR	 R1, [R0] 		      @将R1寄存器中的值存储到R0寄存器所指向的存储器地址中

LDR / STR是以字(word)进行读取操作

LDRB / STRB:按字节(Byte)进行读取操作

LDRH / STRH:按半字(Half word)进行读取操作

LDRSB :有符号的字节加载

LDRSH:有符号的半字加载

多寄存器的读写指令(LDM / STM)

LDM一般语法:LDM[地址操作模式]  R0,{R3-R9}

解释:R0寄存器中的数据传输到R3到R9寄存器中。该指令将R0中的数据依次存储到R3到R9中的寄存器。具体传输方式可以通过地址操作模式来设置。

汇编语言中,感叹号(!)用于间接寻址。比如指令LDM R0!,{R3-R9},感叹号表示使用间接寻址模式。意味着R0寄存器中存储的值是内存地址,指令将从该地址读取连续的字数据,然后存储到R3到R9这些寄存器中。感叹号表示使用寄存器中的值作为地址而不是直接的数值地址。具体地,这条指令会先将R0寄存器的值(作为内存地址)的内容读取到R3寄存器中,然后R0的值会自动增加字(4个字节),接着将下一个内存地址的内容读取到R4寄存器中,依此类推,直到R9寄存器。

我们将R0中存储的值作为一个地址,用来指示要访问的内存位置。具体地,R0寄存器中存储的值会被认为是一个内存单元的地址(比如:当R0寄存器中存储的值是0x2000,直接把这个0x200当作一个内存地址而不是一个值,当用LDR指令来访问内存时,指令可以写为 “LDR R1, [R0]”,表示从0x2000地址处读取数据,并将读取的结果存储到R1寄存器中。这里的R0寄存器中存储的值0x2000实际上是访问内存的目标地址。

通过指令中的间接寻址操作(如使用感叹号"!"),将该地址与指令操作相结合,从存储器中读取或写入数据。通过使用寄存器存储内存地址,我们可以灵活地根据需要动态指定访问的内存位置,而不仅仅局限于使用固定的常数地址。这种间接寻址机制经常处理连续的数据块、访问数据结构或进行动态内存分配等情况。

地址操作模式:

数据块摸式:IA(传输后地址加4),IB(传送前地址加4),DA(传送后地址减4),DB(传送前地址减4)。

堆栈模式:EA(空递减堆栈),FD(满递减堆栈),ED(空递增堆栈),FA(满递增堆栈)。

STM一般语法:STM[地址操作符] R0,{R3-R9}

R3-R9寄存器中的数据写道R0所存的地址中去,比如R0里存的0x00,那么R3-R9往R0写数据时候就从0x00这个地址开始写。

数据交换指令

SWP:内存和寄存器字交换。

一般语法:SWP Rd,Rn1,[Rn2]。具体操作过程如下:

[Rn2]->Rd,Rn1->[Rn2]:把内存的数据读到Rd寄存器,再把Rn1寄存器的数据存到内存里面。Rd就相当于一个临时寄存器。

SWP R1,R1,[Rn2]:直接交换。

SWPB:内存和寄存器字节交换,语法同上。

ARM寻址模式 

LDR和STR支持多种寻址模式。比如:LDR能够加载一个32位的文本值(或绝对地址)到寄存器。ARM架构的汇编指令集中并没有直接从内存地址拷贝数据的单一指令。为了实现从内存中拷贝数据,首先需要将源地址和目标地址加载到相应的寄存器中,然后使用寄存器间接寻址的方式进行数据传输。例如:要从一个内存地址拷贝数据到另一个内存地址,可以按照以下步骤执行:

  1. 将源地址加载到一个寄存器,比如将源地址加载到R0寄存器:LDR R0, =source_address

  2. 将目标地址加载到另一个寄存器,比如将目标地址加载到R1寄存器:LDR R1, =destination_address

  3. 使用寄存器间接寻址方式,将R0寄存器指向的内存地址中的数据读取到R2寄存器中:LDR R2, [R0]

  4. 使用寄存器间接寻址方式,将R2寄存器中的数据存储到R1寄存器指向的内存地址中:STR R2, [R1]

文本/立即数寻址

LDR Rd, =0xABCD1234

ADD r0,r0,#0x3f。r0+0x3f后赋值给r0

绝对地址 LDR Rd, =label
寄存器间接寻址

LDR Rd, [Ra]

ADD r0,r1,r2。r1+r2之后赋值给r0。

基址地址寻址

LDR Rd, [Ra, #4]。Ra地址加上一个立即数后新地址的值再赋值到Rd里面去。

LDR Rd, [Ra],#4。Ra指向的地址的值加上立即数4赋值给Rd。

[AABBCC]代表里面包含的变量AABBCC的地址。

先索引-寄存器 LDR Rd, [Ra, Ro]。
先索引-立即数&Writeback LDR Rd, [Ra, #4]!
先索引-寄存器&Writeback LDR Rd, [Ra, Ro]!
后索引-立即数 LDR Rd, [Ra], #4
后索引-寄存器  LDR Rd, [Ra], Ro

寄存器移位寻址:ADD r3,r2,r1,LSL  #2。r1左移两个立即数,就是左移两位,再加上r2之后赋值给r3.

多寄存器寻址:LDMIA r0,{r1,r2,r3,r4}

压栈与出栈指令

栈一些概念

栈:是一种后进先出(LIFO)的数据结构,主要用来存储函数调用历史以及局部变量。

满堆栈:当前地址指针指向栈最上面地址的时候,称此栈为满堆栈。

空堆栈:当前地址指针指向栈最上面的上一个地址的时候,称此栈为空堆栈。

递增堆栈:低地址像高地址压栈(进栈)时候,栈指针从低地址向高地址移动,栈空间逐渐增长,称为递增堆栈。

递减堆栈:高地址向低地址压栈(进栈)时候,栈指针从高地址向低地址移动,栈空间逐渐减小,称为递减堆栈。

PUSH 和 POP指令

我们通常会在 A 函数中调用 B 函数,当 B 函数执行完以后再回到 A 函数继续执行。要想在跳回 A 函数以后代码能够接着正常运行,那就必须在跳到 B 函数之前将当前处理器状态保存起来(就是保存 R0~R15 这些寄存器值),当 B 函数执行完成以后再用前面保存的寄存器值恢复R0~R15 即可。保存 R0~R15 寄存器的操作就叫做现场保护,恢复 R0~R15 寄存器的操作就叫做恢复现场。在进行现场保护的时候需要进行压栈(入栈)操作,恢复现场就要进行出栈操作。压栈的指令为 PUSH,出栈的指令为 POP,PUSH 和 POP 是一种多存储和多加载指令,即可以一次操作多个寄存器数据,他们利用当前的栈指针 SP 来生成地址,PUSH 和 POP 的用法如下表:

                        PUSH                  入栈(压栈)指令:将寄存器列表存入栈中。
                        POP                         出栈指令:从栈中恢复寄存器列表。

假如我们现在要将 R0~R3 和 R12 这 5 个寄存器压栈,当前的 SP 指针指向 0X80000000,处理器的堆栈是向下增长的,使用的汇编代码如下:

PUSH 	{R0~R3, R12} 	@将 R0~R3 和 R12 按顺序压栈

如下图:压栈后的堆栈模型。 由高地址向低地址增长

Linux-ARM汇编及ARM片内寄存器_第5张图片

 出栈的话就是使用如下代码:

POP   {LR}          @先恢复 LR寄存器
POP   {R0~R3,R12}   @再恢复 R0~R3,R12

跳转和跳转指令

有多种跳转操作,比如:
①、直接使用跳转指令 B、BL、BX 等。
②、直接向 PC 寄存器(保存当前正在执行的指令地址的寄存器)里面写入数据。(MOV PC,[R],MOV向PC赋值的方式)
上述两种方法都可以完成跳转操作,但是一般常用的还是 B、BL 或 BX,用法如下:

B        无条件跳转到 label    

BX      跳转并切换状态,间接跳转,跳转到存放于 Rm 中的地址处,并且切换指令集

BL      跳转到标号地址,并将返回地址保存在 LR 中。

BLX    BL+BX指令的组合,跳转到 Rm 指定的地址,并将返回地址保存在 LR 中,切换指令集。

重点来看一下 B 和 BL 指令,这两个是用的最多的,如果要在汇编中进行函数调用使用的就是 B 和 BL 指令:

B 指令

是最简单的跳转指令,B 指令会将 PC 寄存器的值设置为跳转目标地址, 一旦执行 B 指令,ARM 处理器就会立即跳转到指定的目标地址。如果要调用的函数不会再返回到原来的执行处,那就可以用 B 指令。

 _start:

 	ldr   sp,=0X80200000 	@设置栈指针
	b  main 				@跳转到 main 函数

BL 指令

BL 指令相比 B 指令,在跳转之前会在寄存器 LR(R14)中保存当前 PC 寄存器值,所以可以通过将 LR 寄存器中的值重新加载到 PC 中来继续从跳转之前的代码处运行,这是子程序调用一个基本但常用的手段。

比如 Cortex-A 处理器的 IRQ 中断服务函数都是汇编写的,主要用汇编来实现现场的保护和恢复、获取中断号等。但是具体的中断处理过程都是 C 函数,所以就会存在汇编中调用 C 函数的问题。而且当 C 语言版本的中断处理函数执行完成以后是需要返回到 IRQ 汇编中断服务函数,因为还要处理其他的工作,一般是恢复现场。这个时候就不能直接使用B 指令了,因为 B 指令一旦跳转就再也不会回来了,这个时候要使用 BL 指令。

1 	push 	{r0, r1} 			@保存 r0,r1
2 	cps 	#0x13 				@进入 SVC 模式,允许其他中断再次进去
3   bl 		system_irqhandler 	@加载 C 语言中断处理函数到 r2 寄存器中
4 	cps 	#0x12 				@进入 IRQ 模式
5 	pop 	{r0, r1}
6 	str 	r0, [r1, #0X10] 	@中断执行完成,写 EOIR

@第3行就是BL跳转去执行 C 语言版的中断处理函数,当处理完成以返回来继续执行后面的程序,所以使用 BL 指令

汇编算数操作

数学操作(嵌入式开发中最常会用的就是加减指令,乘除基本用不到)

MOV:数据传送(寄存器之间,不去操作内存)

MVN:数据取反传送(MVN r0,#0X1f,将0X1f取反之后传到r0里)

ADD:加(ADD Rd,Rn,Op2:把Rn和Op2相加存入到Rd里面去)

ADC:带进位的加(ADD Rd,Rn,Op2:Rn和Op2相加再加上进位标识符C的值之后存入到Rd里面去)

SUN:减(SUB Rd,Rn,Op2:把Rd=Rn-Op2)

SBC:带进位的减

RSB:翻转减(Rd=Op2-Rn)

RSC:带进位的翻转减

MUL:32位乘法(MUL Rd,Rm,Rs:Rd=Rm*Rs)保留低位,超过32位的部分舍掉,超2的32次方的。

MLA:32位累加乘法(MUL Rd,Rm,Rs,Rn:Rd=Rm*Rs+Rn)

UMULL:64位无符号乘法((RdLo,RdHi)=Rm*Rs,两个32位寄存器保存相乘后的值,小端模式存储,低位在前)

逻辑操作

AND:逻辑与(AND Rd,Rn,Op2:Rd=Rn&Op2。AND Rd,Rn,#3:保留0位和1位,其余位全部丢掉)

BIC:位清0(BSC Rd,Rn,Op2:Op2指定将Rn的哪个位清0。BSC Rd,Rn,#0x1101,把0位,1位,3位清0)

EOR:逻辑异或(EOR Rd,Rn,Op2:Rd=Rn ^ Op2)

ORR:逻辑或(ORR Rd,Rn,Op2:Rd=Rn | Op2)

比较操作

CMP:比较(CMP Rn,Op2:比较寄存器Rn中的值和操作数Op2,实际上就是Rn-Op2。

根据比较结果,会更新条件码寄存器(CPSR 中的条件标志位),其中最常用的条件标志位包括:

  • Z (Zero):如果比较结果为零,将设置为1。
  • N (Negative):如果比较结果为负数,将设置为1。
  • C (Carry):如果无符号操作中发生了借位,将设置为1。
  • V (Overflow):如果有符号操作导致溢出,将设置为1。

CMN:负数比较(CNN Rn,Op2,实际上就是Rn-(-Op2)

CMN指令将 Rn 寄存器的值与操作数 Op2 进行相反数取反后的减法运算,然后根据运算结果更新条件码寄存器(CPSR)中的条件标志位,包括:

  • Z (Zero):如果比较结果为零,将设置为1。
  • N (Negative):如果比较结果为负数,将设置为1。
  • C (Carry):如果无符号操作中发生了进位,将设置为1。
  • V (Overflow):如果有符号操作导致溢出,将设置为1。

TEQ:测试相等(TEQ Rn,Op2)

"TEQ"指令执行 Rn 寄存器的值与 Op2 进行异或运算(相等为0,不等为1),并根据运算结果更新条件码寄存器(CPSR)中的条件标志位。具体的条件标志位可能有:

  • Z (Zero):如果运算结果为零,也就是Rn与Op2相等,将设置为1。
  • N (Negative):如果运算结果为负数(带符号位为1),将设置为1。
  • C (Carry):不使用TEQ指令。
  • V (Overflow):不使用TEQ指令。

TST:测试(TST Rn,Op2)TST指令可以用于测试特定位是否被置位或清零。

"TST"指令执行 Rn 寄存器的值与 Op2 进行按位与运算,并根据运算结果更新条件码寄存器(CPSR)中的条件标志位。具体的条件标志位可能有:

  • Z (Zero):如果运算结果为零,将设置为1。
  • N (Negative):如果运算结果为负数(带符号位为1),将设置为1。
  • C (Carry):不使用TST指令。
  • V (Overflow):不使用TST指令。

ARM汇编伪指令

常见伪指令

AREA        声明区域段,数据段,代码区等等。

CODE16,CODE32        声明以下是16位指令还是32位指令。

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

EQU        类似于C语言中的#define,用于为程序中的常量,标号等定义一个等效的字符名称。

EXPORT        用于在程序中声明一个全局的标号,该标号可在其他的文件中引用。可用.global代替

GET        用于将一个源文件包含到当前的源文件中,并将被包含的源文件在当前位置进行汇编处理。类似于C语言中#include。

.byte        定义单字节数据,比如 .byte 0x12。
.short       定义双字节数据,比如 .short 0x1234。
.long        定义一个 4 字节数据,比如 .long 0x12345678。
.equ         赋值语句,格式为:.equ 变量名,表达式,比如 .equ num, 0x12,表示 num=0x12。
.align       数据字节对齐,比如 .align 4 表示 4 字节对齐。
.end         表示源文件结束。
.global     定义一个全局符号,格式为:.global symbol,比如 .global _start。

汇编程序的默认入口标号是_start,不过我们也可在链接脚本中使用 ENTRY 来指明其它的入口点,下面的代码就是使用_start 作为入口标号:

.global _start

_start:
ldr r0, =0x12      @r0=0x12

控制伪指令

IF  ELSE  ENDIF        条件语句

WHILE  WEND         循环语句

MACRO  MEND        宏定义

有关于汇编伪指令的内容很多,这里仅记录了一部分。

混合编程

C语言和汇编混合起来一起用。

为何要混合编程?

  1. 性能优化:汇编语言是一种底层语言,可以直接操作硬件和寄存器。相比之下,C语言是一种高级语言,更易于使用和理解。通过在关键部分使用汇编语言,可以更精确地控制代码的执行,优化性能,并提高系统的响应速度和效率。

  2. 节省资源:嵌入式设备通常具有有限的处理能力和内存大小。使用汇编语言可以直接操作硬件资源,更有效地利用资源,减少内存占用和代码大小。

  3. 特定功能实现:某些特定的功能或操作可能更适合用汇编语言来实现。例如,处理中断、访问特定寄存器、执行复杂的算法等。通过使用汇编语言,可以直接编写高效的代码来实现这些功能。

  4. 系统控制:汇编语言可以提供更细粒度的系统控制,如处理器的状态和标志位、中断响应等。通过与C语言结合使用,可以实现更高级别的控制和逻辑,同时使用汇编语言来处理关键的系统级操作。

常用到的汇编语言与C语言混合编程方式:

  1. 在C代码中嵌入汇编指令;
  2. C程序调用外部汇编文件。
  3. 汇编调用C;
1.C语言内联汇编

C 中嵌入汇编代码由 gcc 编译器实现的,使用由编译器提供的 asm 或者 __asm__ 关键字,这两者没有任何区别,然后将需要执行的汇编指令使用("")包含起来,对应的汇编指令就会被执行。如下C中嵌入汇编代码:

void func(void)
{
    ...
    asm("mov r1,r0");
    __asm__("mov r2,r1");
    ...
}
void mystrcpy(char* src,char*dest)//源字符串src的值拷贝到目标字符串dest中去
{
    char ch;
    __asm__
    {
    loop:                        @label
        LDRB ch,[src],#1         
        STRB ch,[dest],#1
        CMP ch,#0
        BNE loop
    }
}
int main()//调用my_strcpy
{
    char *a="hello world"
    char b[64];
    my_strcpy(a,b);
}

上述代码段中,内联的汇编代码解释:

  1. 首先LDRB按照字节加载,间接寻址[src]字符串的地址(字符串的首地址),从此地址里面读一个字节出来,然后地址加一递增(#1)存到ch里面。
  2. 然后STRB将ch中的数据写到[dest]的地址下,dest的地址也是不断加一递增(#1)。
  3. CMP比较ch和0的值。
  4. B是跳转。
  5. NE是条件操作符:不相等。
  6. BEN:上面比较语句CMP比较的两个值不相等时跳转回 loop 标签位置,直到LDRB加载[src]中字符串最后的0才结束循环跳转。

限制条件

  1. 不能直接向PC寄存器赋值,程序跳转要使用B或者BL指令;
  2. 在使用物理寄存器时,不要使用过于复杂的C表达式,避免物理寄存器冲突;
  3. R12和R13可能被编译器用来存放中间编译结果,计算表达式值时可能把R0-R3、R12及R14用于子程序调用,因此避免直接使用这些物理寄存器。
2.C程序调用外部汇编文件

通过函数调用的方式从C代码中调用汇编程序。外部汇编文件需要按照汇编语言的语法进行编写,并使用适当的约定来传递参数和返回值。如下两端代码:C程序调用外部的汇编文件。

//C文件中的代码
extern int my_asm_function(int a, int b);//extern 声明来引入外部的汇编函数,并在C代码中调用

int main() {
    int result = my_asm_function(3, 4);
    // ...
    return 0;
}
@汇编文件
global my_asm_function

section .text
my_asm_function:
    ; 汇编代码实现
    ; ...
    ret

在上述示例中,通过使用 extern 声明来引入外部的汇编函数,并在C代码中调用。然后,在汇编文件中,使用 global 来声明汇编函数的全局可见性,并在 .text 段中编写汇编代码。最后,通过 ret 返回执行结果。ret 指令:从子程序中返回到调用者处继续执行代码。

再看如下示例(字符串拷贝):

extern void my_strcpy(char *src,char*dest);//extern 声明来引入外部的汇编函数,并在C代码中调用

int main()//调用my_strcpy
{
    char *a="hello world"
    char b[64];
    my_strcpy(a,b);
}
my_strcpy        @label
loop             @label
    LDRB R4,[R0],#1
    CMP R4,#0
    BEQ over
    STRB R4,[R1],#1
    B loop
over
2.汇编调用C语言

使用外部函数调用:汇编代码和C代码是分开的,通过汇编代码中的函数调用指令来调用C语言函数。在汇编文件中,需要使用extern关键字来声明所调用的C语言函数。然后,在汇编代码中直接使用函数名调用所声明的C语言函数。

#include 

void my_c_function() {
    printf("Hello, World!");
}
section .text
    global _start

extern my_c_function    //extern关键字来声明所调用的C函数
extern printf

_start:
    call my_c_function    //在汇编代码中使用call指令来调用C语言中的my_c_function函数。
    //退出程序
    mov     eax, 1
    xor     ebx, ebx
    int     0x80

再看如下示例:

C语言实现函数。比如:最常见的,调用C语言的入口函数-main函数。

.global cFun:外部声明C语言函数

BL函数名。比如:BL main。BL指令:跳转执行main函数,执行完跳转回来。

int cFun(int a,int b,int c)//C语言实现一个简单的加法运算函数
{
    return a+b+c;
}
.global cFun   @ 外部声明C语言函数

.section .data
    @ 如果有数据需要定义,可以在这里声明

.section .text
    @ 这里放置可执行的代码

start
    MOV R0,#1
    MOV R1,#2
    MOV R2,#3
    BL cFun    //cFun此函数的返回结果也存在R0里面
    MOV R4,R0

C语言和汇编语言之间的参数传递是通过对应的用R0-R3寄存器来进行传递,即R0传递第一个参数,R1传递第二个参数,多于4个时借助栈完成,函的返回值通过R0来传递。这个规定叫作ATPCS。比如:汇编要传到C中的值,默认先传到R0-R3寄存器里面,若R0-R3存不下了,就借用栈,用LR指向一个地址来存储。

volatile 关键字

在 C 语言中,对于共享的全局变量,强调使用 volatile 关键字,以避免编译器自作主张地对数据进行优化(通常是寄存器缓存缓存)而导致的问题。

在嵌入汇编中,同样存在优化问题,对于某些嵌入汇编指令,它可能没有输入输出,编译器会认为它对整个程序并不起任何作用,从而将这条指令"优化"(通常是删除),volatile 和 __volatile__ 关键字就可以防止这种事情的发生,比如下面这条指令:

asm __volatile__ ("mov r0,r0");

这种优化并非存在统一的标准,而是由编译器实现,对于不同的编译器完全可能有不同的实现。

PS:提醒自己的一个小概念,总是记混,写此篇博客的时候所看的视频中又有提到,顺便再记录一下。

大小端存储

比如:int a=16777220,化为十六进制是 0x01 00 00 04,01属于高字节,04属于低字节

内存想象成笔记本:从第一页往后是从低地址到高地址,而最后一页往前是高地址到低地址。

大端存储:低字节也就是数据的低位保存在内存中的高地址,高字节也就是数据的高位保存在内存中的低地址。

小端存储:数据的低位保存在内存中的低地址,数据的高位保存在内存中的高地址。

Linux-ARM汇编及ARM片内寄存器_第6张图片

你可能感兴趣的:(linux,arm开发,汇编)