嵌入式Linux之常用ARM汇编
在嵌入式开发中,汇编程序常常用于非常关键的地方,比如系统启动时的初始化,中断上下文的保存和恢复,对性能要求非常苛刻的函数等。
在S3C2440的数据手册中,对各种汇编指令的作用及使用方法都有详细说明,这里只对一些常用的汇编指令进行介绍。
一、ARM寄存器介绍
1.1 32位体系
ARM 处理器在32位体系下主要由以下寄存器组成:
- 寄存器 0 到寄存器 7 是通用寄存器并可以用做任何目的。不像 80x86 处理器那样要求特定寄存器被用做段地址访问,或者像 6502 那样把数学计算的结果放置到一个累加器中,ARM 处理器在寄存器使用上是高度灵活的。
- 寄存器 8 到 12 是通用寄存器,但是在切换到 FIQ 模式的时候,使用它们的影子(shadow)寄存器。
- 寄存器 13 典型的用做 OS 栈指针,但可被用做一个通用寄存器。这是一个操作系统问题,不是一个处理器问题,所以如果你不使用栈,只要你以后恢复它,你可以在你的代码中自由的占用(corrupt)它。每个处理器模式都有这个寄存器的影子寄存器。
- 寄存器 14 专职持有返回点的地址以便于写子例程。当你执行带连接的分支的时候,把返回地址存储到 R14 中。同样在程序第一次运行的时候,把退出地址保存在 R14 中。R14 的所有实例必须被保存到其他寄存器中(不是实际上有效)或一个栈中。这个寄存器在各个处理器模式下都有影子寄存器。一旦已经保存了连接地址,这个寄存器就可以用做通用寄存器了。
- 寄存器 15 是程序计数器。PC 是完全的 32 位宽,并只用做程序计数器。
-
两个 PSR - CPSR 是当前的程序状态寄存器(Current Program Status Register),而 SPSR 是保存的程序状态寄存器(Saved Program Status Register)。每个有特权的模式都有自己的 SPSR,可获得的 PSR 有:
CPSR_all - 当前的 SPSR_svc - 保存的,SVC(32) 模式 SPSR_irq - 保存的,IRQ(32) 模式 SPSR_abt - 保存的,ABT(32) 模式 SPSR_und - 保存的,UND(32) 模式 SPSR_fiq - 保存的,FIQ(32) 模式
你不能显式的指定把 CPSR 保存到哪个SPSR 中,比如 SPSR_fiq。而是必须变更到 FIQ 模式并接着保存到 SPSR。换句话说,你只能在你所在的模式中改变这个模式的 SPSR。
为更清晰一些... 提供下列图表(其中User26,...26是为了兼容26位体系):
User26 SVC26 IRQ26 FIQ26 User SVC IRQ ABT UND FIQ R0 ----- R0 ----- R0 ----- R0 -- -- R0 ----- R0 ----- R0 ----- R0 ----- R0 ----- R1 R1 ----- R1 ----- R1 ----- R1 -- -- R1 ----- R1 ----- R1 ----- R1 ----- R1 ----- R2 R2 ----- R2 ----- R2 ----- R2 -- -- R2 ----- R2 ----- R2 ----- R2 ----- R2 ----- R2 R3 ----- R3 ----- R3 ----- R3 -- -- R3 ----- R3 ----- R3 ----- R3 ----- R3 ----- R3 R4 ----- R4 ----- R4 ----- R4 -- -- R4 ----- R4 ----- R4 ----- R4 ----- R4 ----- R4 R5 ----- R5 ----- R5 ----- R5 -- -- R5 ----- R5 ----- R5 ----- R5 ----- R5 ----- R5 R6 ----- R6 ----- R6 ----- R6 -- -- R6 ----- R6 ----- R6 ----- R6 ----- R6 ----- R6 R7 ----- R7 ----- R7 ----- R7 -- -- R7 ----- R7 ----- R7 ----- R7 ----- R7 ----- R7 R8 ----- R8 ----- R8 R8_fiq R8 ----- R8 ----- R8 ----- R8 ----- R8 R8_fiq R9 ----- R9 ----- R9 R9_fiq R9 ----- R9 ----- R9 ----- R9 ----- R9 R9_fiq R10 ---- R10 ---- R10 R10_fiq R10 ---- R10 ---- R10 ---- R10 ---- R10 R10_fiq R11 ---- R11 ---- R11 R11_fiq R11 ---- R11 ---- R11 ---- R11 ---- R11 R11_fiq R12 ---- R12 ---- R12 R12_fiq R12 ---- R12 ---- R12 ---- R12 ---- R12 R12_fiq R13 R13_svc R13_irq R13_fiq R13 R13_svc R13_irq R13_abt R13_und R13_fiq R14 R14_svc R14_irq R14_fiq R14 R14_svc R14_irq R14_abt R14_und R14_fiq --------- R15 (PC / PSR) --------- --------------------- R15 (PC) --------------------- ----------------------- CPSR ----------------------- SPSR_svc SPSR_irq SPSR_abt SPSR_und SPSR_fiq
下面是你想知道的"模式",比如上面提及的"FIQ"模式。
- 用户模式,运行应用程序的普通模式。限制你的内存访问并且你不能直接读取硬件设备。
- 超级用户模式(SVC 模式),主要用于 SWI(软件中断)和 OS(操作系统)。这个模式有额外的特权,允许你进一步控制计算机。例如,你必须进入超级用户模式来读取一个插件(podule)。这不能在用户模式下完成。
- 外部中断模式(IRQ 模式),用来处理发起中断的外设。这个模式也是有特权的。导致 IRQ 的设备有键盘、 VSync (在发生屏幕刷新的时候)、IOC 定时器、串行口、硬盘、软盘、等等...
- 快速中断模式(FIQ 模式),用来处理发起快速中断的外设。这个模式是有特权的。导致 FIQ 的设备有处理数据的软盘,串行端口(比如在 82C71x 机器上的 A5000) 和 Econet。
-
异常终止模式(ABT 模式): 在一个数据或指令预取异常终止(abort)的时候进入的模式。
-
未定义模式(UND 模式): 在执行了一个未定义的指令的时候进入的模式。
ARM处理器的工作模式可以通过软件改变,也可以通过外部中断或异常处理来改变处理器的工作模式。大多数的应用程序运行在用户模式下,当处理器运行在用户模式下时,某些被保护的系统资源是不能被访问的。
除了软件切换工作模式外,还有没有其他方法呢?答案当然是有!通过异常中断的方式也可以进入相应的工作模式。例如,当IRQ中断发生时,处理器就进入外部中断模式(IRQ模式)。
IRQ 和 FIQ 之间的区别是对于 FIQ 你必须尽快处理你事情并离开这个模式。IRQ 可以被 FIQ 所中断但 IRQ 不能中断 FIQ。FIQ 不能调用 SWI。FIQ 还必须禁用中断。
当进行工作模式切换时,哪些寄存器的值需要保护呢?为什么快速中断模式比外部中断模式中断响应的速度要快?
假设ARM处理器处于用户模式执行程序,在程序执行过程中,发生了外部中断,则处理器进入外部中断模式(IRQ模式),从上图可以看出,用户模式和IRQ模式的寄存器R0-R12是公用的,也就是说,在进入IRQ模式之前,在用户模式下可能使用到了寄存器R0-R12。如果在IRQ模式中也需要使用,那么寄存器R0~R12中的值将被更改,在IRQ模式执行完中断处理程序,返回到用户模式后,寄存器R0-R12中的值被破坏了。因此,在IRQ模式中,使用寄存器R0-R12之前需要将其中的值保存,当从IRQ模式返回到用户模式时将原来的值恢复就可以避免上述问题。
从上图还可以看出,用户模式下的寄存器R13-R14和IRQ模式的寄存器R13_irq、R14_irq不是同一个寄存器,也就是说在每种工作模式中,这两个寄存器是独立的。因此,当从用户模式切换到IRQ模式时,不需要保存这两个寄存器的值。
此外,从上图还可以看到,用户模式和快速中断模式公用的寄存器是R0-R7,在FIQ模式中,R8_fiq~R14_fiq是独立的。因此,当从用户模式切换到快速中断模式(FIQ)时不需要保存这几个寄存器的值,只需要保存R0-R7的值即可。因此发生快速中断只需要保存R0-R7,共8个寄存器。但是,当发生外部中断时需要保存R0~R12共13个寄存器,这里讲的保存寄存器的值是通过将其值入栈实现的,入栈是需要时间的,因此快速中断的响应时间要快一些。
1.2 CPSR 和 SPSR 寄存器
CPSR 寄存器(和保存它的 SPSR 寄存器)中的位分配如下::
31 30 29 28 --- 7 6 - 4 3 2 1 0 N Z C V I F M4 M3 M2 M1 M0
标志的意义:
- N Negative 如果结果是负数则置位
- Z Zero 如果结果是零则置位
- C Carry 如果发生进位则置位
- V Overflow 如果发生溢出则置位
- I IRQ 中断禁用
- F FIQ 快速中断禁用
- M4.. M0 是处理器模式标志:
0 0 0 0 0 User26 模式 0 0 0 0 1 FIQ26 模式 0 0 0 1 0 IRQ26 模式 0 0 0 1 1 SVC26 模式 1 0 0 0 0 User 模式 1 0 0 0 1 FIQ 模式 1 0 0 1 0 IRQ 模式 1 0 0 1 1 SVC 模式 1 0 1 1 1 ABT 模式 1 1 0 1 1 UND 模式
二、寄存器装载和存储
- LDM
- LDR
- STM
- STR
- SWP
它们可能是能获得的最有用的指令。其他指令都操纵寄存器,所以必须把数据从内存装载寄存器并把寄存器中的数据存储到内存中。
2.1 传送单一数据(STR 和 LDR)
ldr命令:把数据从内存加载到寄存器。
ldr{条件} r1, [r0] ; r1 = *r0 ldr{条件} r1, [r0, #4] ; r1 = *(r0+4) ldr{条件} r1, [r0, #4] ! ; r1 = *(r0+4);r0=r0+4; ldr{条件} r1, [r0], #4 ; r1 = *(r0);r0=r0+4;
str命令:把数据从寄存器保存到内存。
str{条件} r1, [r0] ; *r0 = r1 str{条件} r1, [r0, #4] ; *(r0+4) = r1 str{条件} r1, [r0, #4] ! ; *(r0+4) = r1;r0=r0+4; str{条件} r1, [r0], #4 ; *r0 = r1;r0=r0+4;
LDR(伪指令):
伪指令(并不存在的指令,最终被解析成真正的汇编指令)
例1:LDR伪指令可以在立即数前加上=,以表示把一个地址写到某寄存器中。
COUNT EQU 0x56000054 LDR R1,=COUNT
COUNT是我们定义的一个变量,地址为0x56000054。该指令会在内存中开辟一个地址,该地址保存0x56000054。而LDR指令会被转换为 LDR,R1,[PC,偏移量]。
例2:下面的例子是获取代码的绝对位置,这是位置有关码,该地址的值是和程序的链接地址有关的:
LDR R1,=label
label:
...
LDR会去读取标号的地址,然后赋值给R1,由于标号的地址和链接地址有关,因此这是一个位置有关指令。
例3:LDR伪指令把数据从内存中某处读取到寄存器中。
LDR R0, 0x12345678 ;就是把0x12345678这个地址中的值存放到r0中。
该指令会读取0x12345678地址处的值,然后赋值给R0。
LDR伪指令和MOV是比较相似的。只不过MOV指令限制了立即数的长度为8位,也就是不能超过512。而LDR伪指令没有这个限制。如果使用LDR伪指令时,后面跟的立即数没有超过8位,那么在实际汇编的时候该LDR伪指令是被转换为MOV指令的。
2.2 传送多个数据(LDM和STM)
LDM:(load much)多数据加载,将地址上的值加载到寄存器上;
STM:(store much)多数据存储,将寄存器的值存到地址上;
STM 的主要用途是把需要保存的寄存器的值复制到栈上。如我们以前见到过的:
STMFD R13!, {R0-R12, R14}
指令格式是:
xxM{条件}{类型} Rn{!}, <寄存器列表>{^}
‘xx’是 LD 表示装载,或 ST 表示存储。
再加 4 种‘类型’就变成了 8 个指令:
栈 其他
LDMED LDMIB 预先增加装载
LDMFD LDMIA 过后增加装载
LDMEA LDMDB 预先减少装载
LDMFA LDMDA 过后减少装载
STMFA STMIB 预先增加存储
STMEA STMIA 过后增加存储
STMFD STMDB 预先减少存储
STMED STMDA 过后减少存储
LDM例子:
Ldr R1,=0x10000000 #传送数据的起始地址0x10000000
LDMIB R1!,{R0,R4-R6} #从左到右加载,相当于 LDR R0,10000004 LDR R4,10000008... ...
IB:(Increase Before)的含义每次传送前地址加4, 传送前地址加+4;
所以地址加4,R0=0x1000004地址里的内容;
地址加4,R4=0x10000008地址里的内容;
地址加4,R5=0x1000000C地址里的内容;
地址加4,R6=0x10000010 地址里的内容;
由于!, 最后的地址写回到R1中,R1=0x10000010 ;
指令中寄存器列表和内存单元的对应关系为:编号低的寄存器对应内存中的低地址单元。编号搞得寄存器对应内存中的高地址单元。
2.3 单一数据交换(SWP)
指令格式:
SWP{条件}{B} <dest>, <op 1>, [<op 2>]
寄存器和存储器交换指令。使用SWP 可实现信号量操作。
实列代码如下:
SWP R1,R1,[R0] ; 取出r0地址中的数据,放在r1中,并把r1中的数据放在r0中。
SWP R1,R2,[R0] ; 将R0 指向的存储单元内容读取数据到R1 中 ; 并将R2 的内容写入到该内存单元中 使用SWP 指令可以方便地进行信号量的操作:
三、分支指令
3.1 B分支
B跳转指令,语法格式:
B{条件} <地址>
B 是最简单的分支。一旦遇到一个 B 指令,ARM 处理器将立即跳转到给定的地址,从那里继续执行。注意存储在分支指令中的实际的值是相对当前的 R15 的值的一个偏移量;而不是一个绝对地址。它的值由汇编器来计算,它是 24 位有符号数,左移两位后有符号扩展为 32 位,表示的有效偏移为 26 位(+/- 32 M)。
3.2 BL带连接的分支
BL语法格式:
BL{条件} <地址>
带链接程序跳转,也就是要带返回地址。在发生跳转前,将当前PC-4保存到R14中。 也就是返回地址存在R14中,所以可以在子程序返回时只要MOV PC, LR即可。
四、算数和逻辑指令
- ADC
- ADD
- AND
- BIC
- EOR
- MOV
- MVN
- ORR
- RSB
- RSC
- SBC
- SUB
4.1 ADC带进位的加法
ADC{条件}{S} <dest>, <op 1>, <op 2> dest = op_1 + op_2 + carry
ADC 将把两个操作数加起来,并把结果放置到目的寄存器中。它使用一个进位标志位,这样就可以做比 32 位大的加法。
下列例子将两个128位的数进行相加:
128 位结果: 寄存器 0、1、2、和 3;
第一个 128 位数: 寄存器 4、5、6、和 7;
第二个 128 位数: 寄存器 8、9、10、和 11;
ADDS R0, R4, R8 ; 加低端的字
ADCS R1, R5, R9 ; 加下一个字,带进位
ADCS R2, R6, R10 ; 加第三个字,带进位
ADCS R3, R7, R11 ; 加高端的字,带进位
如果如果要做这样的加法,不要忘记设置 S 后缀来更改进位标志。
4.2 ADD加法
ADD{条件}{S} <dest>, <op 1>, <op 2> dest = op_1 + op_2
ADD 将把两个操作数加起来,把结果放置到目的寄存器中。操作数 1 是一个寄存器,操作数 2 可以是一个寄存器,被移位的寄存器,或一个立即值:
ADD R0, R1, R2 ; R0 = R1 + R2
ADD R0, R1, #256 ; R0 = R1 + 256
ADD R0, R2, R3,LSL#1 ; R0 = R2 + (R3 << 1)
加法可以在有符号和无符号数上进行。
4.3 AND : 逻辑与
AND{条件}{S} <dest>, <op 1>, <op 2>
dest = op_1 AND op_2
AND将在两个操作数上进行逻辑与,把结果放置到目的寄存器中;对设置特定的位有用。操作数 1 是一个寄存器,操作数 2 可以是一个寄存器,被移位的寄存器,或一个立即值:
AND R0, R0, #0xFFFFFFE ; 清除R0 中位 0
AND真值表(二者中均为 1 则结果为 1):
Op_1 Op_2 结果
0 0 0
0 0 0
0 0 0
1 1 1
4.4 BIC位清除
BIC{条件}{S} <dest>, <op 1>, <op 2> dest = op_1 AND (!op_2)
BIC 是在一个字中清除位的一种方法,与 OR 位设置是相反的操作。操作数 2 是一个 32 位掩码(mask)。如果如果在掩码中设置了某一位,则清除这一位。未设置的掩码位指示此位保持不变。
BIC R0, R0, #%1011 ; 清除 R0 中的位 0、1、和 3。保持其余的不变。
BIC 真值表:
Op_1 Op_2 结果 0 0 0 0 1 0 1 0 1 1 1 0
译注:逻辑表达式为 Op_1 AND NOT Op_2。
4.5 EOR逻辑异或
EOR{条件}{S} <dest>, <op 1>, <op 2> dest = op_1 EOR op_2
EOR 将在两个操作数上进行逻辑异或,把结果放置到目的寄存器中;对反转特定的位有用。操作数 1 是一个寄存器,操作数 2 可以是一个寄存器,被移位的寄存器,或一个立即值:
EOR R0, R0, #3 ; 反转 R0 中的位 0 和 1
EOR 真值表(二者不同则结果为 1):
Op_1 Op_2 结果
0 0 0
0 1 1
1 0 1
1 1 0
4.6 MOV传送
MOV{条件}{S} <dest>, <op 1> dest = op_1
MOV 从另一个寄存器、被移位的寄存器、或一个立即值装载一个值到目的寄存器。你可以指定相同的寄存器来实现 NOP 指令的效果,你还可以专门移位一个寄存器:
MOV R0, R0 ; R0 = R0... NOP 指令
MOV R0, R0, LSL#3 ; R0 = R0 * 8
如果 R15 是目的寄存器,将修改程序计数器或标志。这用于返回到调用代码,方法是把连接寄存器的内容传送到 R15:
MOV PC, R14 ; 退出到调用者
MOVS PC, R14 ; 退出到调用者并恢复标志位
(不遵从 32-bit 体系)
4.7 MVN传送取反的值
(Move Negative) MVN{条件}{S} <dest>, <op 1> dest = !op_1
MVN 从另一个寄存器、被移位的寄存器、或一个立即值装载一个值到目的寄存器。不同之处是在传送之前位被反转了,所以把一个被取反的值传送到一个寄存器中。这是逻辑非操作而不是算术操作,这个取反的值加 1 才是它的取负的值:
MVN R0, #4 ; R0 = -5
MVN R0, #0 ; R0 = -1
4.8 ORR : 逻辑或
ORR{条件}{S} <dest>, <op 1>, <op 2> dest = op_1 OR op_2
OR 将在两个操作数上进行逻辑或,把结果放置到目的寄存器中;对设置特定的位有用。操作数 1 是一个寄存器,操作数 2 可以是一个寄存器,被移位的寄存器,或一个立即值:
ORR R0, R0, #3 ; 设置 R0 中位 0 和 1
OR 真值表(二者中存在 1 则结果为 1):
Op_1 Op_2 结果 0 0 0 0 1 1 1 0 1 1 1 1
4.9 RSB : 反向减法
RSB{条件}{S} <dest>, <op 1>, <op 2> dest = op_2 - op_1
SUB 用操作数 two 减去操作数 one,把结果放置到目的寄存器中。操作数 1 是一个寄存器,操作数 2 可以是一个寄存器,被移位的寄存器,或一个立即值:
RSB R0, R1, R2 ; R0 = R2 - R1
RSB R0, R1, #256 ; R0 = 256 - R1
RSB R0, R2, R3,LSL#1 ; R0 = (R3 << 1) - R2
反向减法可以在有符号或无符号数上进行。
4.10 RSC : 带借位的反向减法
RSC{条件}{S} <dest>, <op 1>, <op 2> dest = op_2 - op_1 - !carry
同于 SBC,但倒换了两个操作数的前后位置。
4.11 SBC : 带借位的减法
SBC{条件}{S} <dest>, <op 1>, <op 2> dest = op_1 - op_2 - !carry
SBC 做两个操作数的减法,把结果放置到目的寄存器中。它使用进位标志来表示借位,这样就可以做大于 32 位的减法。SUB 和 SBC 生成进位标志的方式不同于常规,如果需要借位则清除进位标志。所以,指令要对进位标志进行一个非操作 - 在指令执行期间自动的反转此位。
4.12 SUB : 减法
SUB{条件}{S} <dest>, <op 1>, <op 2> dest = op_1 - op_2
SUB 用操作数 one 减去操作数 two,把结果放置到目的寄存器中。操作数 1 是一个寄存器,操作数 2 可以是一个寄存器,被移位的寄存器,或一个立即值:
SUB R0, R1, R2 ; R0 = R1 - R2
SUB R0, R1, #256 ; R0 = R1 - 256
SUB R0, R2, R3,LSL#1 ; R0 = R2 - (R3 << 1)
减法可以在有符号和无符号数上进行。
五、程序状态指令
5.1 MRS
MRS指令用于将程序状态寄存器的内容传送到通用寄存器中。
MRS{条件} 通用寄存器,程序状态寄存器(CPSR或SPSR)
例子:
MRS R0, CPSR ; 复制 CPSR 到 R0 中
MRS R0, SPSR ; 复制 SPSR 到 R0 中
注意事项: MRS指令用于将程序状态寄存器的内容传送到通用寄存器中。该指令一般用在以下两种情况:
- 当需要改变程序状态寄存器的内容时,可用MRS将程序状态寄存器的内容读入通用寄存器,修改后再写回程序状态寄存器;
- 当在异常处理或进程切换时,需要保存程序状态寄存器的值,可先用该指令读出程序状态寄存器的值,然后保存。
5.2 MSR
MSR指令用亍将操作数的内容传送到程序状态寄存器的特定域中。其中,操作数可以为通用寄存器或立即数。
MSR{条件} 程序状态寄存器(CPSR或SPSR)_<域>,操作数
例子:
MSR CPSR, R0 ; 复制 R0 到 CPSR 中
MSR SPSR, R0 ; 复制 R0 到 SPSR 中
MSR CPSR_flg, R0 ; 复制 R0 的标志位到 CPSR 中
MSR CPSR_flg, #1<<28 ; 复制(立即值)标志位到 CPSR 中
注意事项:
<域>用于设置程序状态寄存器中需要操作的位,32位的程序状态寄存器可分为4个域:
位[31:24]为条件标志位域,用f表示;
位[23:16]为状态位域,用s表示;
位[15:8]为扩展位域,用x表示;
位[7:0]为控制位域,用c表示;
该指令通常用于恢复或改变程序状态寄存器的内容,在使用时,一般要在MSR指令中指明将要操作的域。
5.3 改变模式示例
使用 _flg
后缀允许你改变标志位而不影响控制位。 要设置 V 标志:
MSR CPSR_flg, #&10000000
要改变模式:
MRS R0, CPSR_all ; 复制 PSR
BIC R0, R0, #&1F ; 清除模式位
ORR R0, R0, #new_mode ; 把模式位设置为新模式
MSR CPSR_all, R0 ; 写回 PSR,变更模式
六、比较指令
- CMN
- CMP
- TEQ
- TST
6.1 CMN:比较取负的值
CMN{条件}{P} <op 1>, <op 2> status = op_1 - (- op_2)
CMN 同于 CMP,但它允许你与负值(操作数 2 的取负的值)进行比较,比如难于用其他方法实现的用于结束列表的 -1。这样与 -1 比较将使用:
CMN R0, #1 ; 把 R0 与 -1 进行比较
6.2 CMP : 比较
CMP{条件}{P} <op 1>, <op 2> status = op_1 - op_2
CMP 允许把一个寄存器的内容如另一个寄存器的内容或立即值进行比较,更改状态标志来允许进行条件执行。它进行一次减法,但不存储结果,而是正确的更改标志。标志表示的是操作数 1 比操作数 2 如何(大小等)。如果操作数 1 大于操作操作数 2,则此后的有 GT 后缀的指令将可以执行。
6.3 TEQ : 测试等价
TEQ{条件}{P} <op 1>, <op 2> Status = op_1 EOR op_2
TEQ 类似于 TST。区别是这里的概念上的计算是 EOR 而不是 AND。这提供了一种查看两个操作数是否相同而又不影响进位标志(不象 CMP 那样)的方法。加上 P 后缀的 TEQ 还可用于改变 R15 中的标志(在 26-bit 模式中)。
6.4 TST : 测试位
TST{条件}{P} <op 1>, <op 2> Status = op_1 AND op_2
TST 类似于 CMP,不产生放置到目的寄存器中的结果。而是在给出的两个操作数上进行操作并把结果反映到状态标志上。使用 TST 来检查是否设置了特定的位。操作数 1 是要测试的数据字而操作数 2 是一个位掩码。经过测试后,如果匹配则设置 Zero 标志,否则清除它。
TST R0, #%1 ; 测试在 R0 中是否设置了位 0。
七、移位指令
- LSL 逻辑左移
- ASL 算术左移
- LSR 逻辑右移
- ASR 算术右移
- ROR 循环右移
- RRX 带扩展的循环右移
移位操作在 ARM 指令集中不作为单独的指令使用,它是指令格式中是一个字段,在汇编语言中表示为指令中的选项。
如果数据处理指令的第二个操作数或者单一数据传送指令中的变址是寄存器,则可以对它进行各种移位操作。如果数据处理指令的第二个操作数是立即值,在指令中用 8 位立即值和 4 位循环移位来表示它,所以对大于 255 的立即值,汇编器尝试通过在指令中设置循环移位数量来表示它,如果不能表示则生成一个错误。在逻辑类指令中,逻辑运算指令由指令中 S 位的设置或清除来确定是否影响进位标志,而比较指令的 S 位总是设置的。在单一数据传送指令中指定移位的数量只能用立即值而不能用寄存器。
7.1 逻辑或算术左移
Rx, LSL #n Rx, ASL #n Rx, LSL Rn Rx, ASL Rn
接受 Rx 的内容并按用‘n’或在寄存器 Rn 中指定的数量向高有效位方向移位。最低有效位用零来填充。除了概念上的第 33 位(就是被移出的最小的那位)之外丢弃移出最左端的高位,如果逻辑类指令中 S 位被设置了,则此位将成为从桶式移位器退出时进位标志的值。
考虑下列:
MOV R1, #12
MOV R0, R1, LSL#2
在退出时,R0 是 48。 这些指令形成的总和是 R0 = #12, LSL#2 等同于 BASIC 的 R0 = 12 << 2
7.2 逻辑右移
Rx, LSR #n
Rx, LSR Rn
它在概念上与左移相对。把所有位向更低有效位方向移动。如果逻辑类指令中 S 位被设置了,则把最后被移出最右端的那位放置到进位标志中。它同于 BASIC 的 register = value >>> shift。
7.3 算术右移
Rx, ASR #n
Rx, ASR Rn
类似于 LSR,但使用要被移位的寄存器(Rx)的第 31 位的值来填充高位,用来保护补码表示中的符号。如果逻辑类指令中 S 位被设置了,则把最后被移出最右端的那位放置到进位标志中。它同于 BASIC 的 register = value >> shift。
7.4 循环右移
Rx, ROR #n Rx, ROR Rn
循环右移类似于逻辑右移,但是把从右侧移出去的位放置到左侧,如果逻辑类指令中 S 位被设置了,则同时放置到进位标志中,这就是位的‘循环’。一个移位量为 32 的操作将导致输出与输入完全一致,因为所有位都被移位了 32 个位置,又回到了开始时的位置!
7.5 带扩展的循环右移
Rx, RRX
这是一个 ROR#0 操作,它向右移动一个位置 - 不同之处是,它使用处理器的进位标志来提供一个要被移位的 33 位的数量。
八、伪指令
ARM 汇编伪指令主要包括ADR、ADRL、ALIGN、EQUx、.extern、.text等等,这里只介绍一部分。下面展示一部分代码:
.extern main
.text
.global _start
_start:
8.1 .extern
.extern定义一个外部符号(可以是变量也可以是函数),上面的代码表示本文件中引用的main是一个外部函数。调用的时候可以遍访所有文件找到main函数并且使用它。
8.2 .text
.text表示下面的语句都属于代码段。
8.3 .global
.global将程序中某个标号定义为全局的。.global _start 让_start符号成为全局可见的标识符,这个符号可以被当前源文件以外的其他文件使用也可以被链接脚本(链接器)使用,_start仅仅是一个标号对应一个地址并不是C中的一个变量。
使用.global之后,这样链接器就知道跳转到程序中的什么地方并开始执行。一般我们在链接文件中会这样使用_start:
/* s3c2440链接脚本 */ OUTPUT_ARCH(arm) ENTRY(_start) SECTIONS { ... }
此外,如果start.S汇编中想引入引入lowleverl_init.S文件中的函数,只需要在lowleverl_init.S中使用.global声明即可;
.global lowlevel_init lowlevel_init: /* memory control configuration */ /* make r0 relative the current location so that it */ /* reads SMRDATA out of FLASH rather than memory ! */ ldr r0, =SMRDATA ldr r1, =CONFIG_SYS_TEXT_BASE sub r0, r0, r1 ldr r1, =BWSCON /* Bus Width Status Controller */ add r2, r0, #13*4 0: ldr r3, [r0], #4 str r3, [r1], #4 cmp r2, r0 bne 0b /* everything is fine now */ mov pc, lr
start.s中直接调用:
bl lowlevel_init
8.4 .DCx初始化数据存储
DCx <值>
没有 DCx 指令。‘x’表示一个可能的范围。它们是:
DCB 预备一个字节(8 位值) DCW 预备一个半字(16 位值) DCD 预备一个字(32 位值) DCS 按给出的字符串的要求预备直到 255 个的字符
例如:
.start_counter
DCB 1
.pointer
DCD 0
.error_block
DCD 17
DCS "Uh-oh! It all went wrong!" + CHR$0
ALIGN
8.5 EQUx : 初始化数据存储
EQUx <值>
没有 EQUx 指令,‘x’表示一个可能的范围。它们是:
EQUB 预备一个字节(8 位值) EQUW 预备一个半字(16 位值) EQUD 预备一个字(32 位值) EQUS 按给出的字符串的要求预备直到 255 个的字符
简单的理解,除了名字不同之外与DCx 完全一样。
九、汇编指令的执行条件
大多数ARM指令都可以条件执行,即根据CPSR寄存器中的条件标志位决定是否执行该指令;如果条件不满足,该指令相当于一条nop指令。
每天ARM指令包含4位的条件码域,这表明可以定义16个执行条件。可以将这些执行条件的助记符附加在汇编指令后,比如MOVEQ、MOVGT。这16个条件码和它们的助记符如下表所示:
条件码 | 助记符 | 含义 | CPSR中条件标志位 |
0000 | EQ | 相等 | Z=1 |
0001 | NE | 不相等 | Z=0 |
0010 | CS/HS | 无符号数大于/等于 | C=1 |
0011 | CC/LO | 无符号数小于 | C=0 |
0100 | MI | 负数 | N=1 |
0101 | PL | 非负数 | N=0 |
0110 | VS | 上溢出 | V=1 |
0111 | VC | 没有上溢出 | V=0 |
1000 | HI | 无符号数大于 | C=1且Z=0 |
1001 | LS | 无符号数小于等于 | C=0或Z=1 |
1010 | GE | 带符号数大于等于 | N=1、V=1或N=0、V=0 |
1011 | LT | 带符号数小于 | N=1、V=0或N=0、V=1 |
1100 | GT | 带符号数大于 | Z=0且N=V |
1101 | LE | 带符号数小于/等于 | Z=1或N!=V |
1110 | AL | 无条件执行 | - |
1111 | NV | 从不执行 | - |
标志的意义:
- N Negative 如果结果是负数则置位
- Z Zero 如果结果是零则置位
- C Carry 如果发生进位则置位
- V Overflow 如果发生溢出则置位
十、注释
在GNU ARM嵌入式汇编源程序中的注释方式有:
- /**/ 块注释,跟C语言的一样;
- // 行注释,跟C语言的一样;
- @ 行注释;
- # 行注释;
另外,汇编指令大小写也是可以混合的。
十一、C与汇编混合编程
11.1 汇编中调用C语言
ldr pc,=C语言函数名
11.2 C语言中调用汇编
在汇编语言中声明为全局标识符:
.global asm
这样在C语言中就可以直接调用:
asm();
11.3 C语言中嵌入汇编
__asm__(
汇编部分语句
:输出部分
:输入部分
:破坏描述部分
);
注:C内嵌汇编以关键字”__asm__”或”asm”开始,下辖四个部分,各部分之间使用":"分开, 第一部分是必须写的,后面三部分是可以省略,但是分号:不能省略;
- 汇编语句部分:汇编语句的集合,可以包含多条汇编语句,每条语句之间需要使用换行符 “\n”隔开或使用分号“ ; ”隔开;
- 输出部分:在汇编中被修改的C变量列表;
- 输入部分: 作为参数输入到汇编中的变量列表;
- 破坏描述部分: 执行汇编指令会破坏的寄存器描述;
示例1,向cp15 c1寄存器写入数值:
void write_p15_c1 (unsigned long value) { __asm__( "mcr p15, 0, %0, c1, c0, 0\n" @%0是一个占位符,被输入部分的寄存器替代 : : "r" (value) @编译器选择一个R*寄存器(通用寄存器) : "memory"); }
示例2:向cp15 c1寄存器读出数值:
unsigned long read_p15_c1 (void) { unsigned long value; __asm__( "mrc p15, 0, %0, c1, c0, 0\n" @%0是一个占位符,被输出部分的寄存器替换 : "=r" (value) @ ’=‘ 表示只写操作数,用于输出部分 : : "memory"); return value; }
对于硬件寄存器操作一般加上volatile关键字,使用volatile来告诉编译器,不要对接下来的这部分代码优化:
__asm__ volatile( ... )
示例3:
#define GPKCON 0x7f008800 #define GPKDAT 0x7f008808 int gboot_main() { __asm__( "ldr r1, =0x11110000\n" "str r1, [%0]\n" "ldr r1, =0xa0\n" "str r1, [%1]\n" : :"r"(GPKCON),"r"(GPKDAT) :"r1" ); return 0; }
参考文章