嵌入式Linux之常用ARM汇编

在嵌入式开发中,汇编程序常常用于非常关键的地方,比如系统启动时的初始化,中断上下文的保存和恢复,对性能要求非常苛刻的函数等。

在3S3C2440的数据手册中,对各种汇编指令的作用及使用方法都有详细说明,这里只对一些常用的汇编指令进行介绍。

一、ARM寄存器介绍

ARM 处理器有二十七个寄存器,其中一些是在一定条件下使用的,所以一次只能使用十六个:

  • 寄存器 0 到寄存器 7 是通用寄存器并可以用做任何目的。不像 80x86 处理器那样要求特定寄存器被用做栈访问,或者像 6502 那样把数学计算的结果放置到一个累加器中,ARM 处理器在寄存器使用上是高度灵活的。
  • 寄存器 8 到 12 是通用寄存器,但是在切换到 FIQ 模式的时候,使用它们的影子(shadow)寄存器。
  • 寄存器 13 典型的用做 OS 栈指针,但可被用做一个通用寄存器。这是一个操作系统问题,不是一个处理器问题,所以如果你不使用栈,只要你以后恢复它,你可以在你的代码中*的占用(corrupt)它。每个处理器模式都有这个寄存器的影子寄存器。
  • 寄存器 14 专职持有返回点的地址以便于写子例程。当你执行带连接的分支的时候,把返回地址存储到 R14 中。同样在程序第一次运行的时候,把退出地址保存在 R14 中。R14 的所有实例必须被保存到其他寄存器中(不是实际上有效)或一个栈中。这个寄存器在各个处理器模式下都有影子寄存器。一旦已经保存了连接地址,这个寄存器就可以用做通用寄存器了。
  • 寄存器 15 是程序计数器。它除了持有指示程序当前使用的地址的二十六位数之外,还持有处理器的状态。

为更清晰一些... 提供下列图表: 

User 模式  SVC 模式   IRQ 模式   FIQ 模式  APCS

R0 ------- R0 ------- R0 ------- R0        a1
R1 ------- R1 ------- R1 ------- R1        a2
R2 ------- R2 ------- R2 ------- R2        a3
R3 ------- R3 ------- R3 ------- R3        a4
R4 ------- R4 ------- R4 ------- R4        v1
R5 ------- R5 ------- R5 ------- R5        v2
R6 ------- R6 ------- R6 ------- R6        v3
R7 ------- R7 ------- R7 ------- R7        v4
R8 ------- R8 ------- R8         R8_fiq    v5
R9 ------- R9 ------- R9         R9_fiq    v6
R10 ------ R10 ------ R10        R10_fiq   sl
R11 ------ R11 ------ R11        R11_fiq   fp
R12 ------ R12 ------ R12        R12_fiq   ip
R13        R13_svc    R13_irq    R13_fiq   sp
R14        R14_svc    R14_irq    R14_fiq   lr
------------- R15 / PC -------------       pc

最右侧的列是 APCS 代码使用的名字。程序计数器构造如下: :

31  30  29  28  27  26  25------------2  1  0

       N   Z   C   V   I   F    程 序 计 数 器  S1 S0

下面是你想知道的"模式",比如上面提及的"FIQ"模式。

  • 用户模式,运行应用程序的普通模式。限制你的内存访问并且你不能直接读取硬件设备。
  • 超级用户模式(SVC 模式),主要用于 SWI(软件中断)和 OS(操作系统)。这个模式有额外的特权,允许你进一步控制计算机。例如,你必须进入超级用户模式来读取一个插件(podule)。这不能在用户模式下完成。
  • 中断模式(IRQ 模式),用来处理发起中断的外设。这个模式也是有特权的。导致 IRQ 的设备有键盘、 VSync (在发生屏幕刷新的时候)、IOC 定时器、串行口、硬盘、软盘、等等...
  • 快速中断模式(FIQ 模式),用来处理发起快速中断的外设。这个模式是有特权的。导致 FIQ 的设备有处理数据的软盘,串行端口(比如在 82C71x 机器上的 A5000) 和 Econet。

IRQ 和 FIQ 之间的区别是对于 FIQ 你必须尽快处理你事情并离开这个模式。IRQ 可以被 FIQ 所中断但 IRQ 不能中断 FIQ。为了使 FIQ 更快,所以有更多的影子寄存器。FIQ 不能调用 SWI。FIQ 还必须禁用中断。如果一个 FIQ 例程必须重新启用中断,则它太慢了并应该是 IRQ 而不是 FIQ。 

二、寄存器装载和存储

  • LDM
  • LDR
  • STM
  • STR
  • SWP

它们可能是能获得的最有用的指令。其他指令都操纵寄存器,所以必须把数据从内存装载寄存器并把寄存器中的数据存储到内存中。

2.1 传送单一数据(STR 和 LDR)

首先让我们查看指令格式: 

  LDR{条件}    Rd, <地址>
  STR{条件}    Rd, <地址>
  LDR{条件}B   Rd, <地址>
  STR{条件}B   Rd, <地址>

LDR指令装载指定的地址的数据到Rd ,STR指令存储 Rd 的值到指定的地址。如果像后面两个指令那样还指定了‘B’,则只装载或存储一个单一的字节;对于装载,寄存器中高端的三个字节被置零(zeroed)。

地址可以是一个简单的值、或一个偏移量、或者是一个被移位的偏移量。可以还可以把合成的有效地址写回到基址寄存器(去除了对加/减操作的需要)。

各种寻址方式的示例: 

译注:下文中的 Rbase 是表示基址寄存器,Rindex 表示变址寄存器,index 表示偏移量,偏移量为 12 位的无符号数。用移位选项表示比例因子。标准寻址方式 - 用 AT&T 语法表示为 disp(base, index, scale),用 Intel 语法表示为 [base + index*scale + disp],中的变址(连带比例因子)与偏移量不可兼得。

   STR    Rd, [Rbase]          存储 Rd 到 Rbase 所包含的有效地址。

   STR    Rd, [Rbase, Rindex]  存储 Rd 到 Rbase + Rindex 所合成的有效地址。 

   STR    Rd, [Rbase, #index]  存储 Rd 到 Rbase + index 所合成的有效地址。
                               index 是一个立即值。
                               例如,STR Rd, [R1, #16] 将把 Rd 存储到 R1+16STR    Rd, [Rbase, Rindex]! 存储 Rd 到 Rbase + Rindex 所合成的有效地址,
                               并且把这个新地址写回到 Rbase。

   STR    Rd, [Rbase, #index]! 存储 Rd 到 Rbase + index 所合成的有效地址,
                               并且并且把这个新地址写回到 Rbase。

   STR    Rd, [Rbase], Rindex  存储 Rd 到 Rbase 所包含的有效地址。
                               把 Rbase + Rindex 所合成的有效地址写回 Rbase。

   STR    Rd, [Rbase, Rindex, LSL #2] 
                               存储 Rd 到 Rbase + (Rindex * 4) 所合成的有效地址。

   STR    Rd, place            存储 Rd 到 PC + place 所合成的有效地址。

你当然可以在这些指令上使用条件执行。但要注意条件标志要先于字节标志,所以如果你希望在结果是等于的时候装载一个字节,要用的指令是 LDREQB Rx, <address> (不是 LDRBEQ...)。

LDR(伪指令):

伪指令(并不存在的指令,最终被解析成真正的汇编指令)

例子:

LDR R0,=0x12345678

命令解析:把0x12345678的值赋值给R0。LDR伪指令和MOV是比较相似的。只不过MOV指令限制了立即数的长度为8位,也就是不能超过512。而lLDR伪指令没有这个限制。如果使用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 指令可以方便地进行信号量的操作: 

三、分支指令

2.1 B分支

B跳转指令,语法格式:

B{条件}  <地址>

B 是最简单的分支。一旦遇到一个 B 指令,ARM 处理器将立即跳转到给定的地址,从那里继续执行。注意存储在分支指令中的实际的值是相对当前的 R15 的值的一个偏移量;而不是一个绝对地址。它的值由汇编器来计算,它是 24 位有符号数,左移两位后有符号扩展为 32 位,表示的有效偏移为 26 位(+/- 32 M)。

2.2 BL带连接的分支

BL语法格式:

 BL{条件}  <地址>

带链接程序跳转,也就是要带返回地址。在发生跳转前,将当前PC-4保存到R14中。 也就是返回地址存在R14中,所以可以在子程序返回时只要MOV PC, LR即可。

 

参考文章

[1] ARM汇编手册

嵌入式Linux之常用ARM汇编

上一篇:Linux查看多核CPU利用率


下一篇:第11章:Linux实操篇 定时任务调度