2024年2月24日发(作者:硕安)
ARM 指令集
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
寄存器和处理器模式(26-bit 体系)
寄存器和处理器模式(32-bit 体系)
程序状态寄存器和操纵它的指令
寄存器装载和存储指令
算术和逻辑指令
移位操作
乘法指令
比较指令
分支指令
条件执行
软件中断指令
APCS (ARM 过程调用标准)
编写安全的 32-bit 代码的基本规则
IEEE 浮点指令
汇编器伪指令
指令快速查找
ARM 指令格式
寄存器和处理器模式
ARM 处理器有二十七个寄存器,其中一些是在一定条件下使用的,所以一次只能使用十六个...
•
寄存器 0 到寄存器 7 是通用寄存器并可以用做任何目的。不象 80x86 处理器那样要求特定寄存器被用做栈访问,或者象 6502 那样把数学计算的结果放置到一个累加器中,ARM 处理器在寄存器使用上是高度灵活的。
寄存器 8 到 12 是通用寄存器,但是在切换到 FIQ 模式的时候,使用它们的影子(shadow)寄存器。
寄存器 13 典型的用做 OS 栈指针,但可被用做一个通用寄存器。这是一个操作系统问题,不是一个处理器问题,所以如果你不使用栈,只要你以后恢复它,你可以在你的代码中自由的占用(corrupt)它。每个处理器模式都有这个寄存器的影子寄存器。
寄存器 14 专职持有返回点的地址以便于写子例程。当你执行带连接的分支的时候,把返回地址存储到 R14 中。同样在程序第一次运行的时候,把退出地址保存在 R14
中。R14 的所有实例必须被保存到其他寄存器中(不是实际上有效)或一个栈中。这•
•
•
1PDF created with pdfFactory trial version
个寄存器在各个处理器模式下都有影子寄存器。一旦已经保存了连接地址,这个寄存器就可以用做通用寄存器了。
•
寄存器 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 代码使用的名字,关于 APCS 的详情参见这里。
程序计数器构造如下:
位 31 30 29 28 27 26 25------------2 1 0
N Z C V I F 程 序 计 数 器 S1 S0
对 R15 的详细解释,请参见 。
下面是你想知道的"模式",比如上面提及的"FIQ"模式。
•
用户模式,运行应用程序的普通模式。限制你的内存访问并且你不能直接读取硬件设备。
超级用户模式(SVC 模式),主要用于 SWI(软件中断)和 OS(操作系统)。这个模式有额外的特权,允许你进一步控制计算机。例如,你必须进入超级用户模式来读取一个插件(podule)。这不能在用户模式下完成。
•
2PDF created with pdfFactory trial version
•
中断模式(IRQ 模式),用来处理发起中断的外设。这个模式也是有特权的。导致 IRQ
的设备有键盘、 VSync (在发生屏幕刷新的时候)、IOC 定时器、串行口、硬盘、软盘、等等...
快速中断模式(FIQ 模式),用来处理发起快速中断的外设。这个模式是有特权的。导致 FIQ 的设备有处理数据的软盘,串行端口(比如在 82C71x 机器上的 A5000)
和 Econet。
•
IRQ 和 FIQ 之间的区别是对于 FIQ 你必须尽快处理你事情并离开这个模式。IRQ 可以被
FIQ 所中断但 IRQ 不能中断 FIQ。为了使 FIQ 更快,所以有更多的影子寄存器。FIQ 不能调用 SWI。FIQ 还必须禁用中断。如果一个 FIQ 例程必须重新启用中断,则它太慢了并应该是 IRQ 而不是 FIQ。 Phew!
关于如果变更处理器的模式的详情请参照 。
32 位操作
这里的许多信息取自 ARM 汇编器手册。我现在没有 32 位处理器,就只能信任文档了... 这个文档中表述的 UMUL 和 UMLA 只能在 32bit 模式下进行是错误的。如果你的处理器(比如:
StrongARM)可以这么做,则它可以在 32bit 或 26bit 下工作...
ARM2 和 ARM3 有一个 32 位数据总线和一个 26 位地址总线。在以后版本的 ARM 上,数据总线和地址总二者都是完全的 32 位宽。这解释了为什么一个“32 位处理器”被称为 26
位。数据宽度和指令/字大小是 32 位,并总是这样,但地址总线只是 24 位。因为 PC 总是字对齐的,一个地址中的低两位总是零,所以在 ARM2/ARM3 处理器上这些位持有处理器模式设置。尽管实际上只使用了 24 位,PC 的有效宽度仍是 26 位。
在老机器上这没有问题。4Mb 内存是基准的。一些人升级到 8Mb、和 16Mb 是理论上的限制。(Some people upgraded to 8Mb, and 16Mb was the theoretical limit.) 但是 RiscPC 使用一个 26 位程序计数器是不可能的,因为 RiscPC 允许安装 258Mb 内存,而 26 位只允许你寻址到 %1100 (或 67108860 字节,或 64Mb)。这附带的解释了对应用任务的 28Mb 大小限制;就是希望系统与老的 RISC OS API 相容。
尽管这个汇编器站点的某些部分覆盖了 32 位模式(比如运行在 SVC32 下的一个简要的例子!),但多数部分是关于 26 位模式操作的,这是为了与 RISC OS 的当前可获得的版本相兼容(就是 RISC OS 2 到 RISC OS 4);我注意到部分例子不适用于 32 位。
RiscPC、Mico、RiscStation、A7000 等都有能力运行完全的 32 位操作系统;实际上
ARMLinux 就是这样的一个操作系统。RISC OS 不是,因为 RISC OS 需要,至少一个时期,保持与现存版本的兼容。这是个古老的两分问题(dichotomy),有一个崭新的完全 32 位版本的 RISC OS 版本是美妙的,但当你发现许多你的现存软件不能继续运行(so much as load)就不那么美妙了!
3PDF created with pdfFactory trial version
RISC OS 不是完全的 26 位。一些处理程序(handler)需要工作在 32 位模式下;限制它的是金钱(就是说,谁为完全转换 RISC OS 付钱;谁为用来重建它们的代码的开发工具付钱(PD
在 RISC OS 上是强壮的))和必要性(就是说,很多人使用 Impression 而 CC 不再与我们同在;Impression 好象不能在更新的 RISC OS 上工作,所以如果人们需要的软件将不能工作,那么他们不会认为有升级的必要)。
为什么这如此重要? 新的 ARM 处理器将不支持 26 位操作。尽管做了一些融合(ARM6、ARM7、StrongARM),但气数就要尽了。你可以增加一个 26/32 位系统的复杂性,或者只用 32 位而得到更简单、更小的处理器。我们要么随波逐流,要么被甩下... 所以我们别无选择。
32 位体系
ARM 体系在 ARM6 系列中进行了重大变更。下面我将描述 26 位 和 32 位操作行为的不同之处。
在 ARM 6 中,程序计数器被扩展到完整的 32 位。结果是:
•
•
•
PSR 从 PC 中分离到自己的寄存器 CPSR(当前的程序状态寄存器)中。
在改变处理器模式的时候,不再与 PC 一起保存 PSR;现在是每个有特权的模式都有一个额外的寄存器 - SPSR (保存的程序状态寄存器) - 用来持有前面模式的
PSR。
增加了使用这些新寄存器的指令。
除了允许 PC 使用完全的 32 位之外,还有进一步的变更,就是给 PSR 增加了额外的有特权的模式。这些模式用于处理未定义指令和异常终止例外:
•
•
未定义指令、异常终止、和超级用户不再共享同一个模式。去掉了在早期 ARM 上存在的对超级用户的那些限制。
在 ARM6 系列(和以后的其他兼容芯片)中通过设置片上某个控制寄存器来确定这些特征的可获得性。可以选择三个处理器配置中的一个:
o
26 位程序和数据空间,这个配置强制 ARM 在 26 位地址空间中进行操作。不可能选择在这个配置中只能获得四个 26 位模式(参照处理器模式描述);任何 32 位模式。在所有当前的 ARM6 和 7 系列上复位(reset)时被设置为这个模式。
o
26 位程序空间和 32 位数据空间。除了禁止地址例外来允许数据传送操作访问完整的 32 位地址空间之外,与 26 位程序和地址空间配置相同。
o
32 位程序和数据空间。这个配置把地址空间扩展成 32 位,并介入了对处理器模型的重大变更。在这个配置中你可以选择任何 26 位和 32 位处理器模式(参见下面的处理器模式)。
在配置成 32 位程序和数据空间的时候,ARM6 和 ARM7 系列支持十个有所重叠的处理器操作模式:
•
用户模式: 正常的程序执行状态;
或 User26 模式: 一个 26 位版本。
4
PDF created with pdfFactory trial version
•
•
•
•
•
FIQ 模式: 设计来支持一个数据传送或通道处理;
或 FIQ26 模式: 一个 26 位版本。
IRQ 模式: 用于通用中断处理;
或 IRQ26 模式: 一个 26 位版本。
SVC 模式: 用于操作系统的保护模式
或 SVC26模式: 一个 26 位模式。
异常终止模式(ABT 模式): 在一个数据或指令预取异常终止(abort)的时候进入的模式。
未定义模式(UND 模式): 在执行了一个未定义的指令的时候进入的模式。
当在一个 26 位处理器模式中的时候,编程模型倒退成早期的 26 位 ARM 处理器。除了下列变动之外,它的行为与 ARM2aS 宏单元(macrocell)相同:
•
•
•
•
只在 ARM 被配置为 26 位程序和数据空间的时候,它才生成地址例外。在其他配置下 OS 仍然可以通过使用外部逻辑模拟地址例外的行为,比如用一个内存管理单元在超出 64Mbyte 范围的时候生成一个异常终止,并把这个异常终止转换成给这个应用程序的一个‘地址例外陷入’。
保持在通用寄存器和程序状态寄存器之间传送数据的新指令可操作。在调用了包含
26 位的 ARM 二进制代码的之后,操作系统可以使用这些新指令返回到一个 32 位模式。
当在一个 32 位程序和数据空间配置下的时候,所有例外(包括未定义指令和软件中断)把处理器返回到一个 32 位模式,所以必须修改操作系统来处理它们。
如果处理器尝试写到在 &0 和 &1F 之间包括二者(就是例外向量)的一个位置,则硬件将禁止写操作并生成一个数据异常终止。这允许操作系统来截获对例外向量的变动并把向量重定向到一些伪装(veneer)代码上。在调用 26 位例外处理程序之前,这些伪装代码应该把处理器置于一个 26 位模式中。
在所有其他方面,当在一个 26 位模式下进行操作的时候,ARM 表现的如同一个 26 位 ARM。CPSR 的相关的位将被组建(incorporated)回到 R15 中,来形成 I 和 F 位在位 27 和 26
的 PC/PSR。指令集表现的如同增加了 MRS 和 MSR 指令的 ARM2aS 宏单元。
在 ARM 6(和以后)的 32 位模式下可获得的寄存器有:
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
5PDF created with pdfFactory trial version
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
简要的说,32 位的与 26 位的不同是:
•
•
•
•
PC 是完全的 32 位宽,并只用做程序计数器。
PSR 包含在它自己的寄存器 CPSR 中。
每个有特权的模式都有一个专有的 SPSR 寄存器,用来保存 CPSR。
这里有两个新的特权模式,每个有特权的模式都有 R13 和 R14 的专有复件。
CPSR 和 SPSR 寄存器
CPSR 寄存器(和保存它的 SPSR 寄存器)中的位分配如下:
31 30 29 28 --- 7 6 - 4 3 2 1 0
6PDF created with pdfFactory trial version
N Z C V I F M4 M3 M2 M1 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 模式
关于 N、Z、C、V 标志和 I、F 中断标志请参见(26 位) PSR。
这在实践中意味着什么?
多数 ARM 代码将正确的工作。唯一不能工作的是通过摆弄 R15 来设置处理器状态的那些操作。不幸的是,好象没有简便的方法修理这个问题。我检查了一个有潜在问题的 9K 程序(一个 MODE 7 teletext frame viewer,用 C 写的),基本上查找:
•
•
用 R15 作为目的寄存器的 MOVS 指令。
以‘^’作为后缀并装载 R15 的 LDMFD 指令。
大约有 64 个指令被归入此类。
好象有没有什么方式来自动进行转换。基本上...
•
•
•
系统如何知道哪个是数据哪个是代码。实际上,一个灵巧的基于规则的程序能够可以做非常准确的猜测,但“非常准确的猜测”就足够了吗?
没有简单的指令替代。一个自动系统可以修补需要的指令并调整(jiggle)周围的代码,但这将导致不希望的副作用,比如一个 ADR 宏指令(directive)不在范围内(in
range)。
需要难以置信的技巧(It is incredibly hacky)。当然,最好重新编译,或修改源代码。
这是很不容易的。这样小的变更,竟有如此严重(far-reaching)的后果。
7PDF created with pdfFactory trial version
程序状态寄存器
•
•
MRS
MSR
直接查看 R15/PSR 在 32-bit 模式下的详情
寄存器 15 (26-bit 模式):
R15 构造如下:
Bit 31 30 29 28 27 26 25------------2 1 0
N Z C V I F 程 序 计 数 器 S1 S0
标志的意义:
N Negative 如果结果是负数则置位
Z Zero 如果结果是零则置位
C Carry 如果发生进位则置位
O Overflow 如果发生溢出则置位
I IRQ 中断禁用
F FIQ 快速中断禁用
S1 和 S0 是处理器模式标志:
S1 S0 模式
0 0 USR - 用户模式
0 1 FIQ - 快速中断模式
1 0 IRQ - 中断模式
1 1 SVC - 超级用户模式
在 R15 作为一个指令的第一个操作数的时候,只有程序计数器部分是可以获得的。所以,下列指令把 PC 复制到一个寄存器中并向这个目标寄存器加上 256:
ADD R0, R15, #256
(对于 BASIC 汇编器 R15 和 PC 的意思是相同的)
8PDF created with pdfFactory trial version
在 R15 作为第二个操作数的时候,所有 32 位都是可以获得的: 程序计数器、标志、和状态。下列代码段将标识当前的处理器模式:
MOV R0, #3 ; 装载一个位掩码(%11)到 R0 中
AND R0, R0, PC; 把 R15 与 R0 做逻辑与并把结果放入 R0,来得到模式状态
CMP R0, #3 ; 把模式与 '3' 相比较(SVC)
BEQ svc ; 如果等于 SVC 模式,分支到 'svc'
CMP R0, #2 ; 把模式与 '2' 相比较 (IRQ)
BEQ irq ; 如果等于 IRQ 模式,分支到 'irq'
CMP R0, #1 ; 把模式与 '1' 相比较(FIQ)
BEQ fiq ; 如果等于 FIQ 模式,分支到 'fiq'
CMP R0, #0 ; 把模式与 '0' 相比较(USR)
BEQ usr ; 如果等于 USR 模式,分支到 'usr'
这个例子不遵从 32-bit 体系。如何在 32-bit 环境中读当前的模式请参照下面的章节。
改变处理器的状态:
要改变处理器模式、或者任何标志,我们需要用想要的标志 EOR(异或)状态标志,新状态 =
旧状态 EOR (1 << 28) 可以成为改变 oVerflow 标志的伪码。但是我们不能做这个简单的
EORS 操作,原因是这将导致随后的两个指令被跳过。不要担心,指令
TEQ 做一个假装的 EOR (结果不存储到任何地方)。把它与
P 后缀组合,则把结果的第 0、1、和 26 至 31
位直接写到 R15 的第 0、1、和 26 至 31 位,这是改变标志的一个简便的方法:
TEQP R15, bit_mask
如果你处在允许你设置一个标志的一个模式中,则你只可以改变这个标志。
这个例子不遵从 32-bit 体系。如何在 32-bit 环境中改变模式请参照下面的章节。
可以被扩充它来改变处理器模式。例如,要进入 SVC 模式你可以:
MOV R6, PC ; 把 PC 的最初状态存储到 R6 中
ORR R7, R6, #3 ; 设置 SVC 模式
TEQP R7, #0 ; 把(在 R7 中的)模式标志写入 PC
9PDF created with pdfFactory trial version
而返回最初的模式是:
TEQP R6, #0 ; 把(在 R6 中的)最初的模式写入 PC
在改变了模式之后,你应该进行一个空操作来允许这个寄存器安定下来。比如
MOV R0, R0
之类的东西就可以。废弃使用 NV 后缀的指令。
32 位 PSR
如同在32 位操作中描述的那样,ARM 3 之后的处理器提供一个 32 bit 地址空间,它们把 PSR 移出 R15 并给予 R15 完整的 32 位位域,在其中存储当前位置的地址。目前,除了一些不太可能遇到的情况之外,RISC OS 工作在 26 位模式。
32 位模式是重要的,因为 26 位(在老的 PSR 中)把每个应用程序的可寻址内存的最大数量限制为 28Mb。这就是不管你安装了多少内存你不能拖动超过 28Mb 的下一个槽(drag the
Next slot beyond 28Mb)的原因。
CPSR 寄存器(和保存它的 SPSR 寄存器)中的位分配如下:
31 30 29 28 --- 7 6 - 4 3 2 1 0
N Z C V I F M4 M3 M2 M1 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 模式
10PDF created with pdfFactory trial version
典型的,处理器将在 User26、FIQ26、IRQ26 和 SVC26 下操作。可以进入一个 32 位模式,但要格外小心。RISC OS 不希望这样,并且如果它发现自己在其中会非常生气!
操纵 32 位 PSR 的指令
你不能在 32 位模式中使用
MOVS PC, R14 或
LDMFD R13!, {registers, PC}^。也不能使用
ORRS PC, R14, #1<<28 来设置 V 标志。现在需要使用
MRS 和
MSR。
复制一个寄存器到 PSR 中
MSR CPSR, R0 ; 复制 R0 到 CPSR 中
MSR SPSR, R0 ; 复制 R0 到 SPSR 中
MSR CPSR_flg, R0 ; 复制 R0 的标志位到 CPSR 中
MSR CPSR_flg, #1<<28 ; 复制(立即值)标志位到 CPSR 中
复制 PSR 到一个寄存器中
MRS R0, CPSR ; 复制 CPSR 到 R0 中
MRS R0, SPSR ; 复制 SPSR 到 R0 中
指令格式
你有两个 PSR -
CPSR 是当前的程序状态寄存器(Current Program Status Register),而
SPSR 是保存的程序状态寄存器(Saved Program Status Register)(前面的处理器模式的
PSR)。每个有特权的模式都有自己的 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。使用
_flg 后缀允许你改变标志位而不影响控制位。
11
PDF created with pdfFactory trial version
在 user(32) 模式中,保护 CPSR 的控制位,你只能改变条件标志。在其他模式中,可获得整个 CPSR。你不应该指定 R15 为一个源寄存器或一个目标寄存器。最后,在 user(32) 模式中,你不能尝试访问 SPSR,因为它不存在!
要设置 V 标志:
MSR CPSR_flg, #&10000000
这将设置 V 标志但不影响控制位。
要改变模式:
MRS R0, CPSR_all ; 复制 PSR
BIC R0, R0, #&1F ; 清除模式位
ORR R0, R0, #new_mode ; 把模式位设置为新模式
MSR CPSR_all, R0 ; 写回 PSR,变更模式
现在我们要做的是进入 SVC32 模式并设置 Z 标志。接着我们返回 SVC26 模式并‘测试’是否设置了 Z。
RISC OS 不希望发现自己处在 32 位模式中,所以我们要禁止所有中断并保持它们这样(keep them that way)。尽管这些代码应该执行的非常快,但我们不应当冒任何风险...
你可能觉得 32 位模式不是非常有用。在当前版本的 RISC OS 下,这是事实。实际上,就我而言,32 位模式提供给你的只是:
访问大于 28Mb 的区域。在 RISC OS 上这不是真的很重要,在这个系统里 web 浏览器适合于 1 M 或 2 M,而重要的艺术程序为那些非常巨大的图象提供它们自己的虚拟内存系统。
本文档的最初版本,和最初的 ARM 汇编器指南包括...
StrongARM 提供了两个指令(UMUL 和 UMLA、IIRC),它们处理 64 位乘法。这只能在 32 位模式下获得。
这是错误的。在 26 位模式下可以使用扩展的乘法;MP3 解码器就使用了它!
尽管 32 位模式的利益好象不是多的那么惊人,新近的处理器(比如 Xscale)不再支持 26
位模式,所以 RISC OS 和它的应用程序要在 32 位环境下工作则必须经过修改。听起来不是很多,但是如果所有补偿/改变 R15 中的 PSR 位的引用都必须被变更为对不在 R15 中的独立的 PSR 的引用,这就突然变成一个非常重大的问题了。还有你不能继续用一个指令来恢复 PSR 并分支回到调用者,现在这需要两个独立的指令。为此代码必须重写。你不能简单的用另一个指令来修补...
12PDF created with pdfFactory trial version
寄存器装载和存储
•
•
•
•
•
LDM
LDR
STM
STR
SWP
它们可能是能获得的最有用的指令。其他指令都操纵寄存器,所以必须把数据从内存装载寄存器并把寄存器中的数据存储到内存中。
传送单一数据
使用单一数据传送指令(STR 和 LDR)来装载和存储单一字节或字的数据从/到内存。寻址是非常灵活的。
首先让我们查看指令格式:
LDR{条件} Rd, <地址>
STR{条件} Rd, <地址>
LDR{条件}B Rd, <地址>
STR{条件}B Rd, <地址>
指令格式
这些指令装载和存储 Rd 的值从/到指定的地址。如果象后面两个指令那样还指定了‘B’,则只装载或存储一个单一的字节;对于装载,寄存器中高端的三个字节被置零(zeroed)。
地址可以是一个简单的值、或一个偏移量、或者是一个被移位的偏移量。可以还可以把合成的有效地址写回到基址寄存器(去除了对加/减操作的需要)。
各种寻址方式的示例:
译注:下文中的 Rbase 是表示基址寄存器,Rindex 表示变址寄存器,index 表示偏移量,偏移量为 12 位的无符号数。用移位选项表示比例因子。标准寻址方式 - 用 AT&T 语法表示为 disp(base, index, scale),用 Intel 语法表示为
[base + index*scale + disp],中的变址(连带比例因子)与偏移量不可兼得。
STR Rd, [Rbase] 存储 Rd 到 Rbase 所包含的有效地址。
13PDF created with pdfFactory trial version
STR Rd, [Rbase, Rindex] 存储 Rd 到 Rbase + Rindex 所合成的有效地址。
STR Rd, [Rbase, #index] 存储 Rd 到 Rbase + index 所合成的有效地址。
index 是一个立即值。
例如,STR Rd, [R1, #16] 将把 Rd 存储到
R1+16。
STR 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,
(不是)。
如果你指定预先变址寻址(这里的基址和变址都在方括号中),用是否存在‘!’来控制写回操作。上面的第4和第5个例子中使用了这个标志。你可以使用它来在内存中自动正向或反向移动。一个字符串打印例程将变成:
.loop
LDRB R0, [R1, #1]!
SWI "OS_WriteC"
CMP R0, #0
BNE loop
而不是:
.loop
LDRB R0, [R1]
SWI "OS_WriteC"
ADD R1, R1, #1
14PDF created with pdfFactory trial version
CMP R0, #0
BNE loop
对于过后变址寻址‘!’是无效的(这里的变址在方括号外面,比如上面的例子6),因为写回是暗含的。
如同你见到的那样,变址可以被移位来实现比例缩放。除此之外,可以从基址上减去偏移量。在这种情况下,你可以使用如下代码:
LDRB R0, [R1, #-1]
尽管你可以存储或装载 PC,但你不可以用装载或存储指令来修改 PSR。要装载一个被存储的‘状态’并正确的恢复它,请使用:
LDR R0, [Rbase]
MOVS R15, R0
假如你在有特权的模式下,MOVS 将导致 PSR 的位被更改。
对 PC 使用
MOVS 不遵从 32-bit 体系,你需要使用 MRS 和 MSR 来处理 PSR。
依照 ARM 汇编手册:
译注:下文所叙述内容针对的是小端字节序配置,对大端字节序配置在手册中另有专门叙述。
•
•
•
如果提供的地址在一个字边界上,则字节装载(LDRB)使用在 0 至 7 位上的数据,如果在一个字地址加上一个字节上,则使用 8 至 15 位,以此类推。选择的字节被放入目标寄存器的低端 8 位中,并把寄存器中其余的位用零填充。
字节存储(STRB)在数据总线上重复源寄存器的的低端 8 位 4 次。由外部的内存系统来激活适当的字节子系统来存储数据。
字装载(LDR)或字存储(STR)将生成一个字对齐的地址。使用一个非字对齐的地址将有不明显和未规定的结果。实际上提示的是你不能使用 LDR 从一个非对齐的地址装载一个字。
传送多个数据
使用多数据传送指令(LDM 和 STM)来装载和存储多个字的数据从/到内存。
LDM/STM 的主要用途是把需要保存的寄存器复制到栈上。如我们以前见到过的
STMFD R13!, {R0-R12, R14}。
指令格式是:
xxM{条件}{类型} Rn{!}, <寄存器列表>{^}
15PDF created with pdfFactory trial version
‘xx’是 LD 表示装载,或 ST 表示存储。
再加 4 种‘类型’就变成了 8 个指令:
栈 其他
LDMED LDMIB 预先增加装载
LDMFD LDMIA 过后增加装载
LDMEA LDMDB 预先减少装载
LDMFA LDMDA 过后减少装载
STMFA STMIB 预先增加存储
STMEA STMIA 过后增加存储
STMFD STMDB 预先减少存储
STMED STMDA 过后减少存储
指令格式
汇编器关照如何映射这些助记符。注意 ED 不同于 IB;只对于预先减少装载是相同的。在存储的时候,ED 是过后减少的。
FD、ED、FA、和 EA 指定是满栈还是空栈,是升序栈还是降序栈。一个满栈的栈指针指向上次写的最后一个数据单元,而空栈的栈指针指向第一个空闲单元。一个降序栈是在内存中反向增长(就是说,从应用程序空间结束处开始反向增长)而升序栈在内存中正向增长。
其他形式简单的描述指令的行为,意思分别是过后增加(Increment After)、预先增加(Increment Before)、过后减少(Decrement After)、预先减少(Decrement
Before)。
RISC OS 使用传统的满降序栈。在使用符合 APCS 规定的编译器的时候,它通常把你的栈指针设置在应用程序空间的结束处并接着使用一个 FD (满降序 - Full
Descending)栈。如果你与一个高级语言(BASIC 或 C)一起工作,你将别无选择。栈指针(传统上是 R13)指向一个满降序栈。你必须继续这个格式,或则建立并管理你自己的栈(如果你是死硬派人士那么你可能喜欢这样做!)。
‘基址’是包含开始地址的寄存器。在传统的 RISC OS 下,它是栈指针 R13,但你可以使用除了 R15 之外的任何可获得的寄存器。
如果你想把复制操作后栈顶的当前的内存地址保存到栈指针中,可以寄存器按从最低到最高的编号次序与到从低端到高端的内存之间传送数据。并且因为用指令中的一个单一的位来表示是否保存一个寄存器,不可能指定某个寄存器两次。它的副作用是不能用下面这样的代码:
STMFD R13!, {R0, R1}
LDMFD R13!, {R1, R0}
来交换两个寄存器的内容。
16PDF created with pdfFactory trial version
提供了一个有用的简写。要包含一个范围的寄存器,可以简单的只写第一个和最后一个,并在其间加一个横杠。例如 R0-R3 等同与 R0, R1, R2, R3,只是更加整齐和理智而已...
在把 R15 存储到内存中的时候,还保存了 PSR 位。在重新装载 R15 的时候,除非你要求否则不恢复 PSR 位。要求的方法是在寄存器列表后跟随一个‘^’。
STMFD R13!, {R0-R12, R14}
...
LDMFD R13!, {R0-R12, PC}
这保存所有的寄存器,做一些事情,接着重新装载所有的寄存器。从 R14 装载 PC,它由一个 BL 或此类指令所设置。不触及 PSR 标志。
STMFD R13!, {R0-R12, R14}
...
LDMFD R13!, {R0-R12, PC}^
这保存所有的寄存器,做一些事情,接着重新装载所有的寄存器。从 R14 装载 PC,它由一个 BL 或此类指令所设置。变更 PSR 标志。
警告: 这些代码不遵从 32 bit 体系。你需要使用 MRS 和 MSR 来处理 PSR,你不能使用‘^’后缀。
注意在这两个例子中,R14 被直接装载到 PC 中。这节省了对 MOV(S) R14 到
R15 中的需要。
警告: 使用 MOVS PC,... 不遵从 32 bit 体系。你需要使用 MRS 和 MSR 来处理 PSR。
SWP : 单一数据交换
(Swap)
SWP{条件}{B}
指令格式
SWP 将:
•
•
从操作数 2 所指向的内存装载一个字并把这个字放置到目的寄存器中。
把寄存器操作数 1 的内容存储到同一个地址中。
如果目的和操作数 1 是同一个寄存器,则把寄存器的内容和给定内存位置的内容进行交换。
如果提供了 B 后缀,则将传送一个字节,否则传送一个字。
17PDF created with pdfFactory trial version
算术和逻辑指令
•
•
•
•
•
•
•
•
•
•
•
•
ADC
ADD
AND
BIC
EOR
MOV
MVN
ORR
RSB
RSC
SBC
SUB
指令格式
ADC : 带进位的加法
(Addition with Carry)
ADC{条件}{S}
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 后缀来更改进位标志。
ADD : 加法
(Addition)
ADD{条件}{S}
18PDF created with pdfFactory trial version
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)
加法可以在有符号和无符号数上进行。
AND : 逻辑与
(logical AND)
AND{条件}{S}
dest = op_1 AND op_2
AND 将在两个操作数上进行逻辑与,把结果放置到目的寄存器中;对屏蔽你要在上面工作的位很有用。 操作数 1 是一个寄存器,操作数 2 可以是一个寄存器,被移位的寄存器,或一个立即值:
AND R0, R0, #3 ; R0 = 保持 R0 的位 0 和 1,丢弃其余的位。
AND 的真值表(二者都是 1 则结果为 1):
Op_1 Op_2 结果
0 0 0
0 1 0
1 0 0
1 1 1
BIC : 位清除
(Bit Clear)
BIC{条件}{S}
dest = op_1 AND (!op_2)
BIC 是在一个字中清除位的一种方法,与 OR 位设置是相反的操作。操作数 2 是一个 32
位位掩码(mask)。如果如果在掩码中设置了某一位,则清除这一位。未设置的掩码位指示此位保持不变。
19PDF created with pdfFactory trial version
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
EOR : 逻辑异或
(logical Exclusive OR)
EOR{条件}{S}
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
MOV : 传送
(Move)
MOV{条件}{S}
dest = op_1
MOV 从另一个寄存器、被移位的寄存器、或一个立即值装载一个值到目的寄存器。你可以指定相同的寄存器来实现 NOP 指令的效果,你还可以专门移位一个寄存器:
MOV R0, R0 ; R0 = R0... NOP 指令
20PDF created with pdfFactory trial version
MOV R0, R0, LSL#3 ; R0 = R0 * 8
如果 R15 是目的寄存器,将修改程序计数器或标志。这用于返回到调用代码,方法是把连接寄存器的内容传送到 R15:
MOV PC, R14 ; 退出到调用者
MOVS PC, R14 ; 退出到调用者并恢复标志位
(不遵从 32-bit 体系)
MVN : 传送取反的值
(Move Negative)
MVN{条件}{S}
dest = !op_1
MVN 从另一个寄存器、被移位的寄存器、或一个立即值装载一个值到目的寄存器。不同之处是在传送之前位被反转了,所以把一个被取反的值传送到一个寄存器中。这是逻辑非操作而不是算术操作,这个取反的值加 1 才是它的取负的值:
MVN R0, #4 ; R0 = -5
MVN R0, #0 ; R0 = -1
ORR : 逻辑或
(logical OR)
ORR{条件}{S}
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
21PDF created with pdfFactory trial version
RSB : 反向减法
(Reverse Subtraction)
RSB{条件}{S}
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
反向减法可以在有符号或无符号数上进行。
RSC : 带借位的反向减法
(Reverse Subtraction with Carry)
RSC{条件}{S}
dest = op_2 - op_1 - !carry
同于
SBC,但倒换了两个操作数的前后位置。
SBC : 带借位的减法
(Subtraction with Carry)
SBC{条件}{S}
dest = op_1 - op_2 - !carry
SBC 做两个操作数的减法,把结果放置到目的寄存器中。它使用进位标志来表示借位,这样就可以做大于 32 位的减法。SUB 和
SBC 生成进位标志的方式不同于常规,如果需要借位则清除进位标志。所以,指令要对进位标志进行一个非操作 - 在指令执行期间自动的反转此位。
22PDF created with pdfFactory trial version
SUB : 减法
(Subtraction)
SUB{条件}{S}
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)
减法可以在有符号和无符号数上进行。
移 位
•
•
•
•
•
•
LSL
ASL
LSR
ASR
ROR
RRX
ARM 处理器组建了可以与数据处理指令(ADC、ADD、AND、BIC、CMN、CMP、EOR、MOV、MVN、ORR、RSB、SBC、SUB、TEQ、TST)一起使用的桶式移位器(barrel shifter)。你还可以使用桶式移位器影响在 LDR/STR 操作中的变址值。
译注:移位操作在 ARM 指令集中不作为单独的指令使用,它是指令格式中是一个字段,在汇编语言中表示为指令中的选项。如果数据处理指令的第二个操作数或者单一数据传送指令中的变址是寄存器,则可以对它进行各种移位操作。如果数据处理指令的第二个操作数是立即值,在指令中用 8 位立即值和 4 位循环移位来表示它,所以对大于 255 的立即值,汇编器尝试通过在指令中设置循环移位数量来表示它,如果不能表示则生成一个错误。在逻辑类指令中,逻辑运算指令由指令中 S 位的设置或清除来确定是否影响进位标志,而比较指令的 S 位总是设置的。在单一数据传送指令中指定移位的数量只能用立即值而不能用寄存器。
下面是给不同的移位类型的六个助记符:
LSL 逻辑左移
23PDF created with pdfFactory trial version
ASL 算术左移
LSR 逻辑右移
ASR 算术右移
ROR 循环右移
RRX 带扩展的循环右移
ASL 和
LSL 是等同的,可以自由互换。
你可以用一个立即值(从 0 到 31)指定移位数量,或用包含在 0 和 31 之间的一个值的寄存器指定移位数量。
逻辑或算术左移
(Logical or Arithmetic Shift Left)
Rx, LSL #n or
Rx, ASL #n or
Rx, LSL Rn or
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
逻辑右移
(Logical Shift Right)
Rx, LSR #n or
Rx, LSR Rn
它在概念上与左移相对。把所有位向更低有效位方向移动。如果逻辑类指令中 S 位被设置了,则把最后被移出最右端的那位放置到进位标志中。它同于 BASIC 的
register =
value >>> shift。
24PDF created with pdfFactory trial version
算术右移
(Arithmetic Shift Right)
Rx, ASR #n or
Rx, ASR Rn
类似于 LSR,但使用要被移位的寄存器(Rx)的第 31 位的值来填充高位,用来保护补码表示中的符号。如果逻辑类指令中 S 位被设置了,则把最后被移出最右端的那位放置到进位标志中。它同于 BASIC 的
register = value >> shift。
循环右移
(Rotate Right)
Rx, ROR #n or
Rx, ROR Rn
循环右移类似于逻辑右移,但是把从右侧移出去的位放置到左侧,如果逻辑类指令中 S 位被设置了,则同时放置到进位标志中,这就是位的‘循环’。一个移位量为 32 的操作将导致输出与输入完全一致,因为所有位都被移位了 32 个位置,又回到了开始时的位置!
带扩展的循环右移
(Rotate Right with extend)
Rx, RRX
这是一个 ROR#0 操作,它向右移动一个位置 - 不同之处是,它使用处理器的进位标志来提供一个要被移位的 33 位的数量。
Thanks to
Ian Jeffray for the correction.
25PDF created with pdfFactory trial version
乘法指令
•
•
MLA
MUL
指令格式
这两个指令与普通算术指令在对操作数的限制上有所不同:
1. 给出的所有操作数、和目的寄存器必须为简单的寄存器。
2. 你不能对操作数 2 使用立即值或被移位的寄存器。
3. 目的寄存器和操作数 1 必须是不同的寄存器。
4. 最后,你不能指定 R15 为目的寄存器。
MLA : 带累加的乘法
(Multiplication with Accumulate)
MLA{条件}{S}
dest = (op_1 * op_2) + op_3
MLA 的行为同于
MUL,但它把操作数 3 的值加到结果上。这在求总和时有用。
MUL : 乘法
(Multiplication)
MUL{条件}{S}
dest = op_1 * op_2
MUL 提供 32 位整数乘法。如果操作数是有符号的,可以假定结果也是有符号的。
26PDF created with pdfFactory trial version
比较指令
•
•
•
•
CMN
CMP
TEQ
TST
指令格式
译注:CMP 和 CMP 是算术指令,TEQ 和 TST 是逻辑指令。把它们归入一类的原因是它们的 S 位总是设置的,就是说,它们总是影响标志位。
CMN : 比较取负的值
(Compare Negative)
CMN{条件}{P}
status = op_1 - (- op_2)
CMN 同于
CMP,但它允许你与小负值(操作数 2 的取负的值)进行比较,比如难于用其他方法实现的用于结束列表的 -1。这样与 -1 比较将使用:
CMN R0, #1 ; 把 R0 与 -1 进行比较
详情参照
CMP 指令。
CMP : 比较
(Compare)
CMP{条件}{P}
status = op_1 - op_2
CMP 允许把一个寄存器的内容如另一个寄存器的内容或立即值进行比较,更改状态标志来允许进行条件执行。它进行一次减法,但不存储结果,而是正确的更改标志。标志表示的是操作数 1 比操作数 2 如何(大小等)。如果操作数 1 大于操作操作数 2,则此后的有 GT 后缀的指令将可以执行。
明显的,你不需要显式的指定
S 后缀来更改状态标志... 如果你指定了它则被忽略。
27PDF created with pdfFactory trial version
TEQ : 测试等价
(Test Equivalence)
TEQ{条件}{P}
Status = op_1 EOR op_2
TEQ 类似于
TST。区别是这里的概念上的计算是 EOR 而不是 AND。这提供了一种查看两个操作数是否相同而又不影响进位标志(不象
CMP
那样)的方法。加上
P 后缀的
TEQ 还可用于改变 R15 中的标志(在 26-bit 模式中)。详情请参照 ,在 32-bit 模式下如何做请参见这里。
TST : 测试位
(Test bits)
TST{条件}{P}
Status = op_1 AND op_2
TST 类似于
CMP,不产生放置到目的寄存器中的结果。而是在给出的两个操作数上进行操作并把结果反映到状态标志上。使用
TST 来检查是否设置了特定的位。操作数 1 是要测试的数据字而操作数 2 是一个位掩码。经过测试后,如果匹配则设置 Zero 标志,否则清除它。象
CMP
那样,你不需要指定
S 后缀。
TST R0, #%1 ; 测试在 R0 中是否设置了位 0。
分支指令
•
•
B
BL
指令格式
B : 分支
(Branch)
B{条件} <地址>
B 是最简单的分支。一旦遇到一个
B 指令,ARM 处理器将立即跳转到给定的地址,从那里继续执行。注意存储在分支指令中的实际的值是相对当前的 R15 的值的一个偏移量;而 28PDF created with pdfFactory trial version
不是一个绝对地址。它的值由汇编器来计算,它是 24 位有符号数,左移两位后有符号扩展为 32 位,表示的有效偏移为 26 位(+/- 32 M)。
在其他处理器上,你可能经常见到这样的指令:
OPT 1
LDA &70
CMP #0
BEQ Zero
STA &72
.Zero RTS
(取自 Acorn Electron User Guide issue 1 page 213)
在 ARM 处理器上,它们将变成下面这些东西:
OPT 1
ADR R1, #&70
LDR R0, [R1]
CMP #0
BEQ Zero
STR R0, [R1, #2]
.Zero
MOV PC, R14
这不是一个很好的例子,但你可以构想如何更好的去条件执行而不是分支。另一方面,如果你有大段的代码或者你的代码使用状态标志,那么你可以使用条件执行来实现各类分支: 这样一个单一的简单条件执行指令可以替代在其他处理器中存在的所有这些分支和跳转指令。
OPT 1
ADR R1, #&70
LDR R0, [R1]
CMP R0, #0
STRNE R0, [R1, #2]
MOV PC, R14
BL : 带连接的分支
(Branch with Link)
BL{条件} <地址>
BL 是另一个分支指令。就在分支之前,在寄存器 14 中装载上 R15 的内容。你可以重新装载 R14 到 R15 中来返回到在这个分支之后的那个指令,它是子例程的一个基本但强力的实现。它的作用在屏幕装载器 2 (例子 4)中得以很好的展现...
.load_new_format
29PDF created with pdfFactory trial version
BL switch_screen_mode
BL get_screen_info
BL load_palette
.new_loop
MOV R1, R5
BL read_byte
CMP R0, #255
BLEQ read_loop
STRB R0, [R2, #1]!
...在这里我们见到在装载器循环之前调用了三个子例程。接着,一旦满足了条件执行就在循环中调用了
read_byte 子例程。
条件执行
条件执行
指令格式
ARM 处理器的一个非常特殊的特征是它的条件执行。我们指的不是基本的如果进位则分支,ARM 使这个逻辑阶段进一步深化为如果进位则
XXX
- 这里的 XXX 是任何东西。
为了举例,下面是 Intel 8086 处理器分支指令的一个列表:
JA Jump if Above
JAE Jump if Above or Equal
JB Jump if Below
JBE Jump if Below or Equal
JC Jump if Carry
JCXZ Jump if CX Zero (CX is a register that can be used for loop counts)
JE Jump if Equal
JG Jump if Greater than
JGE Jump if Greater than or Equal
JL Jump if Less than
JLE Jump if Less Than or Equal
JMP JuMP
JNA Jump if Not Above
JNAE Jump if Not Above or Equal
JNB Jump if Not Below
JNBE Jump if Not Below or Equal
JNC Jump if No Carry
30PDF created with pdfFactory trial version
JNE Jump if Not Equal
JNG Jump if Not Greater than
JNGE Jump if Not Greater than or Equal
JNL Jump if Not Less than
JNLE Jump if Not Less than or Equal
JNO Jump if Not Overflow
JNP Jump if Not Parity
JNS Jump if Not Sign
JNZ Jump if Not Zero
JO Jump if Overflow
JP Jump if Parity
JPE Jump if Parity Even
JPO Jump if Parity Odd
JS Jump if Sign
JZ Jump if Zero
80386 添加了:
JECXZ Jump if ECX Zero
作为对比,ARM 处理器只提供了:
B 分支
BL 带连接的分支
但 ARM 提供了条件执行,你可以不受这个表面上不灵活的方式的限制:
BEQ Branch if EQual
BNE Branch if Not Equal
BVS Branch if oVerflow Set
BVC Branch if oVerflow Clear
BHI Branch if HIgher
BLS Branch if Lower or the Same
BPL Branch if PLus
BMI Branch if MInus
BCS Branch if Carry Set
BCC Branch if Carry Clear
BGE Branch if Greater than or Equal
BGT Branch if Greater Than
BLE Branch if Less than or Equal
BLT Branch if Less Than
BLEQ Branch with Link if EQual
....
BLLT Branch with Link if Less Than
还有两个代码,
•
AL - ALways,缺省条件所以不须指定
•
NV - NeVer,不是非常有用。你无论如何不要使用这个代码...
PDF created with pdfFactory trial version 31
当你发现所有 Bxx 指令实际上是同一个指令的时候,紧要关头就到了。接着你会想,如果你可以在一个分支指令上加上所有这些条件,那么对一个寄存器装载指令能否加上它们? 答案是可以。
下面是可获得的条件代码的列表:
EQ : 等于
如果一次比较之后设置了 Z 标志。
NE : 不等于
如果一次比较之后清除了 Z 标志。
VS : 溢出设置
如果在一次算术操作之后设置了 V 标志,计算的结果不适合放入一个 32bit 目标寄存器中。
VC : 溢出清除
如果清除了 V 标志,与 VS 相反。
HI : 高于(无符号)
如果一次比较之后设置了 C 标志并清除了 Z 标志。
LS : 低于或同于(无符号)
如果一次比较操作之后清除了 C 标志或设置了 Z 标志。
PL : 正号
如果一次算术操作之后清除了 N。出于定义‘正号’的目的,零是正数的原因是它不是负数...
MI : 负号
如果一次算术操作之后设置了 N 标志。
CS : 进位设置
如果一次算术操作或移位操作之后设置了 C 标志,操作的结果不能表示为 32bit。你可以把 C 标志当作结果的第 33 位。
CC : 进位清除
与 CS 相反。
GE : 大于或等于(有符号)
如果一次比较之后...
设置了 N 标志并设置了 V 标志
或者...
清除了 N 标志并清除了 V 标志。
32PDF created with pdfFactory trial version
GT : 大于(有符号)
如果一次比较之后...
设置了 N 标志并设置了 V 标志
或者...
清除了 N 标志并清除了 V 标志
并且...
清除了 Z 标志。
LE : 小于或等于(有符号)
如果一次比较之后...
设置了 N 标志并清除了 V 标志
或者...
清除了 N 标志并设置了 V 标志
并且...
设置了 Z 标志。
LT : 小于(有符号)
如果一次比较之后...
设置了 N 标志并清除了 V 标志。
或者...
清除了 N 标志并设置了 V 标志。
AL : 总是
缺省条件,所以不用明显声明。
NV : 从不
不是特别有用,它表示应当永远不执行这个指令。是穷人的 NOP。
包含 NV 是为了完整性(与 AL 相对),你不应该在你的代码中使用它。
有一个在最后的条件代码
S,它以相反的方式工作。当用于一个指令的时候,导致更改状态标志。这不是自动发生的 - 除非这些指令的目的是设置状态。例如:
ADD R0, R0, R1
ADDS R0, R0, R1
ADDEQS R0, R0, R1
第一个例子是一个基本的加法(把 R1 的值增加到 R0),它不影响状态寄存器。
第二个例子是同一个加法,只不过它导致更改状态寄存器。
最后一个例子是同一个加法,更改状态寄存器。不同在于它是一个有条件的指令。只有前一个操作的结果是 EQ (如果设置了 Z 标志)的时候它才执行。
下面是条件执行的一个工作中的例子。你把寄存器 0 与存储在寄存器 10 中内容相比较。如果不等于 R10,则调用一个软件中断,增加它并分支回来再次做这些。否则清除 R10 并返回到调用它的那部分代码(它的地址存储在 R14)。
33PDF created with pdfFactory trial version
条件执行的一个例子
.loop ; 标记循环开始位置
CMP R0, R10 ; 把 R0 与 R10 相比较
SWINE &40017 ; 不等于: 调用 SWI &40017
ADDNE R0, R0, #1 ; 向 R0 加 1
BNE loop ; 分支到 'loop'
MOV R10, #0 ; 等于 : 设置 R10 为零
LDMFD R13!, {R0-R12,PC} ; 返回到调用者
注解:
•
•
SWI 编号就象我写的这样。在 RISC OS 下,它是给 Econet_DoImmediate 的编号。不要字面的接受它,这只是一个例子!
你可能以前没见过
LDMFD,它从栈中装载多个寄存器。在这个例子中,我们从一个完全正式的栈中装载 R0 至 R12 和 R14。关于寄存器装载和存储的更多信息请参阅 。
我说要装载 R14。那么为什么要把它放入 PC 中? 原因是此时 R14 存储的值包含返回地址。我们也可以采用:
•
LDMFD R13!, {R0-R12,R14}
MOV PC, R14
•
但是直接恢复到 PC 中可以省略这个
MOV 语句。
最后,这些寄存器很有可能被一个 SWI 调用所占用(依赖于在调用期间执行的代码),所以你最好把你的重要的寄存器压入栈中,以后在恢复它们。
SWI 指令
SWI : 软件中断
(Software Interrupt)
SWI{条件} <24 位编号>
指令格式
这是一个简单的设施,但可能是最常用的。多数操作系统设施是用 SWI 提供的。没有 SWI 的 RISC OS 是不可想象的。
Nava Whiteford 解释了 SWI 是如何工作的(最初在 Frobnicate issue 12½)...
34PDF created with pdfFactory trial version
SWI 是什么?
SWI 表示 Software Interrupt。在 RISC OS 中使用 SWI 来访问操作系统例程或第三方生产的模块。许多应用使用模块来给其他应用提供低层外部访问。
SWI 的例子有:
•
•
•
文件器 SWI,它辅助读写磁盘、设置属性等。
打印机驱动器 SWI,用来辅助使用打印并行端口。
FreeNet/Acorn TCP/IP 协议栈 SWI,用 TCP/IP 协议在 Internet 上发送和接收数据。
在以这种方式使用的时候,SWI 允许操作系统拥有一个模块结构,这意味着用来建立完整的操作系统的所需的代码可以被分割成许多小的部分(模块)和一个模块处理程序(handler)。
当 SWI 处理程序得到对特定的例程编号的一个请求的时候,它找到这个例程的位置并执行它,并传递(有关的)任何数据。
它是如何工作的?
首先查看一下如何使用它。一个 SWI 指令(汇编语言)看起来如下:
SWI &02
或
SWI "OS_Write0"
这些指令实际上是相同的,将被汇编成相同的指令。唯一的不同是第二个指令使用一个字符串来表示 SWI 编号 &02。在使用采用了字符串编号的程序的时候,在执行之前首先查找这个字符串。
在这里我们不想处理字符串,因为它不能给出它要进行什么的一个真实表示。它们通常用于增进一个程序的清晰程度,但不是实际执行的指令。
让我们再次看一下第一个指令:
SWI &02
这是什么意思? 字面的意思是进入 SWI 处理程序并传递值 &02。在 RISC OS 中这意味着执行编号是 &02 的例程。
它是如何这么作的? 它如何传递 SWI 编号和进入 SWI 处理程序?
如果你查看内存的开始 32 字节(位于 0-&1C)并反汇编它们(查开实际的 ARM
指令)你将见到如下:
35PDF created with pdfFactory trial version
地址 内容 反汇编
00000000 : 0..å : E5000030 : STR R0,[R0,#-48]
00000004 : .óŸå : E59FF31C : LDR PC,&00000328
00000008 : .óŸå : E59FF31C : LDR PC,&0000032C
0000000C : .óŸå : E59FF31C : LDR PC,&00000330
00000010 : .óŸå : E59FF31C : LDR PC,&00000334
00000014 : .óŸå : E59FF31C : LDR PC,&00000338
00000018 : .óŸå : E59FF31C : LDR PC,&0000033C
0000001C : 2?ã : E3A0A632 : MOV R10,#&3200000
让我们仔细看一下。
除了第一个和最后一个指令之外(它们是特殊情况)你见到的都是把一个新值装载到 PC (程序计数器)的指令,它们告诉计算机到哪里去执行下一个指令。还展示了这个值是从内存中的一个地址接受来的。(你可以在 !Zap 主菜单上使用“Read Memory”选项去自己查看一下。)
这看起来好象与 SWI 没多少关系,下面做进一步的说明。
一个 SWI 所做的一切就是把模式改变成超级用户并设置 PC 来执行在地址 &08
处的下一个指令! 把处理器转换到超级用户模式会切换掉两个寄存器 r13 和
r14 并用 r13_svc 和 r14_svc 替换它们。
在进入超级用户模式的时候,还把 r14_svc 设置为在这个 SWI 指令之后的地址。
这个实际上就象一个连接到地址 &08 的分支指令(BL &08),但带有用于一些数据(SWI 编号)的空间。
象我说过的那样,地址 &08 包含跳转到另一个地址的一个指令,就是实际的 SWI
程序的地址!
此时你可能会想“稍等一会! 还有 SWI 编号呢?”。实际上处理器忽略这个值本身。SWI 处理程序使用传递来的 r14_svc 的值来获取它。
下面是完成它的步骤(在存储寄存器 r0-r12 之后):
1.
2.
3.
4.
5.
6.
7.
它从 r14 中减去 4 来获得 SWI 指令的地址。
把这个指令装载到一个寄存器。
清除这个指令的高端 8 位,去掉了 OpCode 而只剩下的 SWI 编号。
使用这个值来找到要被执行的代码的例程的地址(使用查找表等)。
恢复寄存器 r0-r12。
使处理器离开超级用户模式。
跳转到这个例程的地址。
容易吧! ;)
36PDF created with pdfFactory trial version
下面是一个例子,来自 ARM610 datasheet:
0x08 B Supervisor
EntryTable
DCD ZeroRtn
DCD ReadCRtn
DCD WriteIRtn
...
Zero EQU 0
ReadC EQU 256
WriteI EQU 512
; SWI 包含需要的例程在位 8-23 中和数据(如果有的话)在位 0-7 中。
; 假定 R13_svc 指向了一个合适的栈
STMFD R13, {r0-r2 , R14}
; 保存工作寄存器和返回地址。
LDR R0,[R14,#-4]
; 得到 SWI 指令。
BIC R0,R0, #0xFF000000
; 清除高端的 8 位。
MOV R1, R0, LSR #8
; 得到例程偏移量。
ADR R2, EntryTable
; 得到入口表(EntryTable)的开始地址。
LDR R15,[R2,R1,LSL #2]
; 分支到正确的例程
WriteIRtn
; 写 R0 中的位 0 - 7 中的字符。
.............
LDMFD R13, {r0-r2 , R15}^
; 恢复工作空间,并返回、恢复处理器模式和标志。
这就是 SWI 指令的基本处理步骤。
来源:
The ARM610 datasheet by Advanced Risc Machines
The ARM RISC Chip - A programmers guide by van Someren Atack published
by Addison Wesley
37PDF created with pdfFactory trial version
APCS 简介
(ARM 过程调用标准)
•
•
•
•
•
•
•
•
•
•
•
介绍
寄存器命名
设计关键
一致性
栈
回溯结构
实际参数
函数退出
建立栈回溯结构
APCS 标准
对编码有用的东西
介绍
APCS,ARM 过程调用标准(ARM Procedure Call Standard),提供了紧凑的编写例程的一种机制,定义的例程可以与其他例程交织在一起。最显著的一点是对这些例程来自哪里没有明确的限制。它们可以编译自 C、 Pascal、也可以是用汇编语言写成的。
APCS 定义了:
•
•
•
•
对寄存器使用的限制。
使用栈的惯例。
在函数调用之间传递/返回参数。
可以被‘回溯’的基于栈的结构的格式,用来提供从失败点到程序入口的函数(和给予的参数)的列表。
APCS 不一个单一的给定标准,而是一系列类似但在特定条件下有所区别的标准。例如,APCS-R (用于 RISC OS)规定在函数进入时设置的标志必须在函数退出时复位。在 32 位标准下,并不是总能知道进入标志的(没有 USR_CPSR),所以你不需要恢复它们。如你所预料的那样,在不同版本间没有相容性。希望恢复标志的代码在它们未被恢复的时候可能会表现失常...
如果你开发一个基于 ARM 的系统,不要求你去实现 APCS。但建议你实现它,因为它不难实现,且可以使你获得各种利益。但是,如果要写用来与编译后的 C 连接的汇编代码,则必须使用 APCS。编译器期望特定的条件,在你的加入(add-in)代码中必须得到满足。一个好例子是 APCS 定义 a1 到 a4 可以被破坏,而 v1
38PDF created with pdfFactory trial version
到 v6 必须被保护。现在我确信你正在挠头并自言自语“a 是什么? v 是什么?”。所以首先介绍 APCS-R 寄存器定义...
寄存器命名
APCS 对我们通常称为 R0 到 R14 的寄存器起了不同的名字。使用汇编器预处理器的功能,你可以定义 R0 等名字,但在你修改其他人写的代码的时候,最好还是学习使用 APCS 名字。
寄存器名字
意义
Reg # APCS
R0
R1
R2
R3
R4
R5
R6
R7
R8
R9
R10
R11
R12
R13
R14
R15
译注:ip 是指令指针的简写。
a1
a2
a3
a4
v1
v2
v3
v4
v5
v6
sl
fp
ip
sp
lr
pc
工作寄存器
"
"
"
必须保护
"
"
"
"
"
栈限制
桢指针
栈指针
连接寄存器
程序计数器
这些名字不是由标准的 Acorn 的 objasm(版本 2.00)所定义的,但是 objasm
的后来版本,和其他汇编器(比如 Nick Robert 的 ASM)定义了它们。要定义一个寄存器名字,典型的,你要在程序最开始的地方使用 RN 宏指令(directive):
a1 RN 0
a2 RN 1
a3 RN 2
...等...
39PDF created with pdfFactory trial version
r13 RN 13
sp RN 13
r14 RN 14
lr RN r14
pc RN 15
这个例子展示了一些重要的东西:
1. 寄存器可以定义多个名字 - 你可以定义‘r13’和‘sp’二者。
2. 寄存器可以定义自前面定义的寄存器 - ‘lr’定义自叫做‘r14’的寄存器。
(对于 objasm 是正确的,其他汇编器可能不是这样)
设计关键
•
•
•
•
函数调用应当快、小、和易于(由编译器来)优化。
函数应当可以妥善处理多个栈。
函数应当易于写可重入和可重定位的代码;主要通过把可写的数据与代码分离来实现。
但是最重要的是,它应当简单。这样汇编编程者可以非常容易的使用它的设施,而调试者能够非常容易的跟踪程序。
一致性
程序的遵循 APCS 的部分在调用外部函数时被称为“一致”。在程序执行期间的所有时候都遵循 APCS (典型的,由编译器生成的程序)被称为“严格一致”。协议指出,假如你遵守正确的进入和退出参数,你可以在你自己的函数范围内做你需要的任何事情,而仍然保持一致。这在有些时候是必须的,比如在写 SWI 伪装(veneers)的时候使用了许多给实际的 SWI 调用的寄存器。
栈
栈是链接起来的‘桢’的一个列表,通过一个叫做‘回溯结构’的东西来链接它们。这个结构存储在每个桢的高端。按递减地址次序分配栈的每一块。寄存器
sp 总是指向在最当前桢中最低的使用的地址。这符合传统上的满降序栈。在 APCS-R 中,寄存器
sl 持有一个栈限制,你递减
sp 不能低于它。在当前栈指针和当前栈之间,不应该有任何其他 APCS 函数所依赖的东西,在被调用的时候,函数可以为自己设置一个栈块。
40PDF created with pdfFactory trial version
可以有多个栈区(chunk)。它们可以位于内存中的任何地址,这里没有提供规范。典型的,在可重入方式下执行的时候,这将被用于为相同的代码提供多个栈;一个类比是 FileCore,它通过简单的设置‘状态’信息和并按要求调用相同部分的代码,来向当前可获得的 FileCore 文件系统(ADFS、RAMFS、IDEFS、SCSIFS 等)提供服务。
回溯结构
寄存器
fp (桢指针)应当是零或者是指向栈回溯结构的列表中的最后一个结构,提供了一种追溯程序的方式,来反向跟踪调用的函数。
回溯结构是:
地址高端
保存代码指针 [fp] fp 指向这里
返回 lr 值 [fp, #-4]
返回 sp 值 [fp, #-8]
返回 fp 值 [fp, #-12] 指向下一个结构
[保存的 sl]
[保存的 v6]
[保存的 v5]
[保存的 v4]
[保存的 v3]
[保存的 v2]
[保存的 v1]
[保存的 a4]
[保存的 a3]
[保存的 a2]
[保存的 a1]
[保存的 f7] 三个字
[保存的 f6] 三个字
[保存的 f5] 三个字
[保存的 f4] 三个字
地址低端
这个结构包含 4 至 27 个字,在方括号中的是可选的值。如果它们存在,则必须按给定的次序存在(例如,在内存中保存的 a3 下面可以是保存的 f4,但
a2-f5 则不能存在)。浮点值按‘内部格式’存储并占用三个字(12 字节)。
41PDF created with pdfFactory trial version
fp 寄存器指向当前执行的函数的栈回溯结构。返回 fp 值应当是零,或者是指向由调用了这个当前函数的函数建立的栈回溯结构的一个指针。而这个结构中的返回 fp 值是指向调用了调用了这个当前函数的函数的函数的栈回溯结构的一个指针;并以此类推直到第一个函数。
在函数退出的时候,把返回连接值、返回 sp 值、和返回 fp 值装载到 pc、sp、和 fp 中。
#include
void one(void);
void two(void);
void zero(void);
int main(void)
{
one();
return 0;
}
void one(void)
{
zero();
two();
return;
}
void two(void)
{
printf("won");
return;
}
void zero(void)
{
return;
}
当它在屏幕上输出消息的时候,
APCS 回溯结构将是:
fp ----> two_structure
return link
PDF created with pdfFactory trial version 42
return sp
return fp ----> one_structure
... return link
return sp
return fp ----> main_structure
... return link
return sp
return fp ----> 0
...
所以,我们可以检查 fp 并参看给函数‘two’的结构,它指向给函数‘one’的结构,它指向给‘main’的结构,它指向零来终结。在这种方式下,我们可以反向追溯整个程序并确定我们是如何到达当前的崩溃点的。值得指出‘zero’函数,因为它已经被执行并退出了,此时我们正在做它后面的打印,所以它曾经在回溯结构中,但现在不在了。值得指出的还有对于给定代码不太可能总是生成象上面那样的一个 APCS 结构。原因是不调用任何其他函数的函数不要求完全的 APCS 头部。
为了更细致的理解,下面是代码是 Norcroft C v4.00 为上述代码生成的...
AREA |C$$code|, CODE, READONLY
IMPORT |__main|
|x$codeseg|
B |__main|
DCB &6d,&61,&69,&6e
DCB &00,&00,&00,&00
DCD &ff000008
IMPORT |x$stack_overflow|
EXPORT one
EXPORT main
main
MOV ip, sp
STMFD sp!, {fp,ip,lr,pc}
SUB fp, ip, #4
CMPS sp, sl
BLLT |x$stack_overflow|
BL one
MOV a1, #0
LDMEA fp, {fp,sp,pc}^
DCB &6f,&6e,&65,&00
DCD &ff000004
43PDF created with pdfFactory trial version
EXPORT zero
EXPORT two
one
MOV ip, sp
STMFD sp!, {fp,ip,lr,pc}
SUB fp, ip, #4
CMPS sp, sl
BLLT |x$stack_overflow|
BL zero
LDMEA fp, {fp,sp,lr}
B two
IMPORT |_printf|
two
ADD a1, pc, #L000060-.-8
B |_printf|
L000060
DCB &6d,&61,&69,&6e
DCB &2e,&2e,&2e,&6f
DCB &6e,&65,&2e,&2e
DCB &2e,&74,&77,&6f
DCB &0a,&00,&00,&00
zero
MOVS pc, lr
AREA |C$$data|
|x$dataseg|
END
这个例子不遵从 32 为体系。APCS-32 规定只是简单的说明了标志不需要被保存。所以删除 LDM 的‘^’后缀,并在函数 zero 中删除 MOVS 的‘S’后缀。则代码就与遵从 32-bit 的编译器生成的一样了。
保存代码指针包含这条设置回溯结构的指令(STMFD ...)的地址再加上 12 字节。记住,对于 26-bit 代码,你需要去除其中的 PSR 来得到实际的代码地址。
现在我们查看刚进入函数的时候:
•
•
•
pc 总是包含下一个要被执行的指令的位置。
lr (总是)包含着退出时要装载到 pc 中的值。在 26-bit 位代码中它还包含着 PSR。
sp 指向当前的栈块(chunk)限制,或它的上面。这是用于复制临时数据、寄存器和类似的东西到其中的地方。在 RISC OS 下,你有可选择的至少 256 字节来扩展它。
44
PDF created with pdfFactory trial version
•
•
fp 要么是零,要么指向回溯结构的最当前的部分。
函数实参布置成(下面)描述的那样。
实际参数
APCS 没有定义记录、数组、和类似的格局。这样语言可以自由的定义如何进行这些活动。但是,如果你自己的实现实际上不符合 APCS 的精神,那么将不允许来自你的编译器的代码与来自其他编译器的代码连接在一起。典型的,使用 C 语言的惯例。
•
•
•
前 4 个整数实参(或者更少!)被装载到 a1 - a4。
前 4 个浮点实参(或者更少!)被装载到 f0 - f3。
其他任何实参(如果有的话)存储在内存中,用进入函数时紧接在 sp 的值上面的字来指向。换句话说,其余的参数被压入栈顶。所以要想简单。最好定义接受 4 个或更少的参数的函数。
函数退出
通过把返回连接值传送到程序计数器中来退出函数,并且:
•
•
•
•
如果函数返回一个小于等于一个字大小的值,则把这个值放置到 a1 中。
如果函数返回一个浮点值,则把它放入 f0 中。
sp、fp、sl、v1-v6、和 f4-f7 应当被恢复(如果被改动了)为包含在进入函数时它所持有的值。
我测试了故意的破坏寄存器,而结果是(经常在程序完全不同的部分)出现不希望的和奇异的故障。
ip、lr、a2-a4、f1-f3 和入栈的这些实参可以被破坏。
在 32 位模式下,不需要对 PSR 标志进行跨越函数调用的保护。在 26 位模式下必须这样,并通过传送 lr 到 pc 中(MOVS、或 LDMFD xxx^)来暗中恢复。必须从 lr 重新装载 N、Z、C 和 V,跨越函数保护这些标志不是足够的。
建立栈回溯结构
对于一个简单函数(固定个数的参数,不可重入),你可以用下列指令建立一个栈回溯结构:
function_name_label
MOV ip, sp
STMFD sp!, {fp,ip,lr,pc}
SUB fp, ip, #4
45PDF created with pdfFactory trial version
这个片段(来自上述编译后的程序)是最基本的形式。如果你要破坏其他不可破坏的寄存器,则你应该在这个 STMFD 指令中包含它们。
下一个任务是检查栈空间。如果不需要很多空间(小于 256 字节)则你可以使用:
CMPS sp, sl
BLLT |x$stack_overflow|
这是 C 版本 4.00 处理溢出的方式。在以后的版本中,你要调用
|__rt_stkovf_split_small|。
接着做你自己的事情...
通过下面的指令完成退出:
LDMEA fp, {fp,sp,pc}^
还有,如果你入栈了其他寄存器,则也在这里重新装载它们。选择这个简单的 LDM 退出机制的原因是它比分支到一个特殊的函数退出处理器(handler)更容易和更合理。
用在回溯中的对这个协议的一个扩展是把函数名字嵌入到代码中。紧靠在函数(和 MOV ip, sp)的前面的应该是:
DCD &ff0000xx
这里的‘xx’是函数名字符串的长度(包括填充和终结符)。这个字符串是字对齐、尾部填充的,并且应当被直接放置在 DCD &ff....的前面。
所以一个完整的栈回溯代码应当是:
DCB "my_function_name", 0, 0, 0, 0
DCD &ff000010
my_function_name
MOV ip, sp
STMFD sp!, {fp, ip, lr, pc}
SUB fp, ip, #4
CMPS sp, sl ; 如果你不使用栈
BLLT |x$stack_overflow| ; 则可以省略
...处理...
LDMEA fp, {fp, sp, pc}^
要使它遵从 32-bit 体系,只须简单的省略最后一个指令的‘^’。注意你不能在一个编译的
26-bit 代码中使用这个代码。实际上,你可以去除它,但这不是我愿意打赌的事情。
46PDF created with pdfFactory trial version
如果你不使用栈,并且你不需要保存任何寄存器,并且你不调用任何东西,则没有必要设置 APCS 块(但在调试阶段对跟踪问题仍是有用的)。在这种情况下你可以:
my_simple_function
...处理...
MOVS pc, lr
(再次,对 32 位 APCS 使用 MOV 而不是 MOVS,但是不要冒险与 26 位代码连接)。
APCS 标准
总的来说,有多个版本的 APCS (实际上是 16 个)。我们只关心在 RISC OS 上可能遇到的。
APCS-A
就是 APCS-Arthur;由早期的 Arthur 所定义。它已经被废弃,原因是它有不同的寄存器定义(对于熟练的 RISC OS 程序员它是某种异类)。它用于在 USR 模式下运行的 Arthur 应用程序。不应该使用它。
sl = R13, fp = R10, ip = R11, sp = R12, lr = R14, pc = R15。
•
PRM (p4-411) 中说“用
r12 作为
sp,而不是在体系上更自然的
r13,是历史性的•
•
•
•
•
并先于 Arthur 和 RISC OS 二者。”
栈是分段的并可按需要来扩展。
26-bit 程序计数器。
不在 FP 寄存器中传递浮点实参。
不可重入。标志必须被恢复。
APCS-R
就是 APCS-RISC OS。用于 RISC OS 应用程序在 USR 模式下进行操作;或在 SVC
模式下的模块/处理程序。
•
•
•
•
•
•
sl = R10, fp = R11, ip = R12, sp = R13, lr = R14, pc = R15。
它是唯一的最通用的 APCS 版本。因为所有编译的 C 程序都使用 APCS-R。
显式的栈限制检查。
26-bit 程序计数器。
不在 FP 寄存器中传递浮点实参。
不可重入。标志必须被恢复。
APCS-U
就是 APCS-Unix,Acorn 的 RISCiX 使用它。它用于 RISCiX 应用程序(USR 模式)或内核(SVC 模式)。
47PDF created with pdfFactory trial version
•
•
•
•
•
sl = R10, fp = R11, ip = R12, sp = R13, lr = R14, pc = R15。
隐式的栈限制检查(使用 sl)。
26-bit 程序计数器。
不在 FP 寄存器中传递浮点实参。
不可重入。标志必须被恢复。
APCS-32
它是 APCS-2(-R 和 -U)的一个扩展,允许 32-bit 程序计数器,并且从执行在
USR 模式下的一个函数中退出时,允许标志不被恢复。其他事情同于 APCS-R。
Acorn C 版本 5 支持生成 32-bit 代码;在用于广域调试的 32 位工具中,它是最完整的开发发行。一个简单的测试是要求你的编译器导出汇编源码(而不是制作目标代码)。你不应该找到:
MOVS PC, R14
或者
LDMFD R13!, {Rx-x, PC}^
对编码有用的东西
首先要考虑的是该死的 26/32 位问题。 简单的说,不转弯抹角绝对没有方法为两个版本的
APCS 汇编同一个通用代码。但是幸运的这不是问题。APCS 标准不会突然改变。RISC OS
的 32 位版本也不会立刻变异。所以利用这些,我们可以设计一种支持两种版本的方案。这将远远超出 APCS,对于 RISC OS 的 32 位版本你需要使用 MSR 来处理状态和模式位,而不是使用 TEQP。许多现存的 API 实际上不需要保护标志位。所以在我们的 32 版本中可以通过把
MOVS PC,... 变成
MOV PC,...,和把
LDM {...}^ 变成
LDM {...},并重新建造来解决。objasm 汇编器(v3.00 和以后)有一个
{CONFIG} 变量可以是
26 或
32。可以使用它建造宏...
my_function_name
MOV ip, sp
STMFD sp!, {fp, ip, lr, pc}
SUB fp, ip, #4
...处理...
[ {CONFIG} = 26
LDMEA fp, {fp, sp, pc}^
|
LDMEA fp, {fp, sp, pc}
]
我未测试这个代码。它(或类似的东西)好象是保持与两个版本的 APCS 相兼容的最佳方式,也是对 RISC OS 的不同版本,26 位版本和将来的 32 位版本的最佳方法。
48PDF created with pdfFactory trial version
测试是否处于 32 位? 如果你要求你的代码有适应性,有一个最简单的方法来确定处理器的 PC 状态:
TEQ PC, PC ; 对于 32 位是 EQ;对于 26 位是 NE
使用它你可以确定:
•
•
•
26 位 PC,可能是 APCS-R 或 APCS-32。
32 位 PC,不能 APCS-R。所有 26-bit 代码(TEQP 等)面临着失败!
32 位规则
•
•
不要使用有 P 后缀的测试指令: TEQP、TSTP、CMPP、CMNP。
检查更改 PC、R14 的如 BIC 和 ORR 这样的指令,和把一个寄存器复制到 PC 的指令。例如,ORRS PC, R14, #1<<28 将不能工作。实际上,不要使用带 S 标志设置的写到 PC 的任何指令。
在数据处理操作中不要使用 R15 (PC) 作为移位寄存器。
在 LDR/STR 中,不要使用 PC 作为寄存器偏移量并不要写回到它。
在过后变址 LDR/STR 中,Rm(变址)和 Rn(基址)不能是同一个寄存器。类似的,对于涉及写回的任何指令,Rm 和 Rn 都应该是不同的寄存器。
LDM/STM 在用户模式下不使用 S 位。这意味着,不要使用‘^’后缀(例如
LDMFD
R13!, {PC}^)。
BL 不保存状态寄存器。这必须显式的进行,但是这样的代码将不能在 ARM2 或
ARM3 上运行,因为它们不支持 MRS/MSR 指令。
还要注意,不可能见到被调用者的标志,所以不应该恢复你不知道其状态的标志。你能做的最好的就是在进入的时候保护标志。
•
•
•
•
•
•
任何带 S 位设置的到 R15 的 32-bit 写(MOVS、ORRS、TEQP、^) 将传送当前模式的 SPSR 到 CPSR 中。例如,假定我们在 irq_32 模式下:
MOVS PC, R14
将复制 R14 到 PC,并接着复制 SPSR_IRQ32 到 CPSR。
这在 USR 模式下不是非常有用因为它没有 SPSR!
浮点指令
49PDF created with pdfFactory trial version
2024年2月24日发(作者:硕安)
ARM 指令集
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
寄存器和处理器模式(26-bit 体系)
寄存器和处理器模式(32-bit 体系)
程序状态寄存器和操纵它的指令
寄存器装载和存储指令
算术和逻辑指令
移位操作
乘法指令
比较指令
分支指令
条件执行
软件中断指令
APCS (ARM 过程调用标准)
编写安全的 32-bit 代码的基本规则
IEEE 浮点指令
汇编器伪指令
指令快速查找
ARM 指令格式
寄存器和处理器模式
ARM 处理器有二十七个寄存器,其中一些是在一定条件下使用的,所以一次只能使用十六个...
•
寄存器 0 到寄存器 7 是通用寄存器并可以用做任何目的。不象 80x86 处理器那样要求特定寄存器被用做栈访问,或者象 6502 那样把数学计算的结果放置到一个累加器中,ARM 处理器在寄存器使用上是高度灵活的。
寄存器 8 到 12 是通用寄存器,但是在切换到 FIQ 模式的时候,使用它们的影子(shadow)寄存器。
寄存器 13 典型的用做 OS 栈指针,但可被用做一个通用寄存器。这是一个操作系统问题,不是一个处理器问题,所以如果你不使用栈,只要你以后恢复它,你可以在你的代码中自由的占用(corrupt)它。每个处理器模式都有这个寄存器的影子寄存器。
寄存器 14 专职持有返回点的地址以便于写子例程。当你执行带连接的分支的时候,把返回地址存储到 R14 中。同样在程序第一次运行的时候,把退出地址保存在 R14
中。R14 的所有实例必须被保存到其他寄存器中(不是实际上有效)或一个栈中。这•
•
•
1PDF created with pdfFactory trial version
个寄存器在各个处理器模式下都有影子寄存器。一旦已经保存了连接地址,这个寄存器就可以用做通用寄存器了。
•
寄存器 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 代码使用的名字,关于 APCS 的详情参见这里。
程序计数器构造如下:
位 31 30 29 28 27 26 25------------2 1 0
N Z C V I F 程 序 计 数 器 S1 S0
对 R15 的详细解释,请参见 。
下面是你想知道的"模式",比如上面提及的"FIQ"模式。
•
用户模式,运行应用程序的普通模式。限制你的内存访问并且你不能直接读取硬件设备。
超级用户模式(SVC 模式),主要用于 SWI(软件中断)和 OS(操作系统)。这个模式有额外的特权,允许你进一步控制计算机。例如,你必须进入超级用户模式来读取一个插件(podule)。这不能在用户模式下完成。
•
2PDF created with pdfFactory trial version
•
中断模式(IRQ 模式),用来处理发起中断的外设。这个模式也是有特权的。导致 IRQ
的设备有键盘、 VSync (在发生屏幕刷新的时候)、IOC 定时器、串行口、硬盘、软盘、等等...
快速中断模式(FIQ 模式),用来处理发起快速中断的外设。这个模式是有特权的。导致 FIQ 的设备有处理数据的软盘,串行端口(比如在 82C71x 机器上的 A5000)
和 Econet。
•
IRQ 和 FIQ 之间的区别是对于 FIQ 你必须尽快处理你事情并离开这个模式。IRQ 可以被
FIQ 所中断但 IRQ 不能中断 FIQ。为了使 FIQ 更快,所以有更多的影子寄存器。FIQ 不能调用 SWI。FIQ 还必须禁用中断。如果一个 FIQ 例程必须重新启用中断,则它太慢了并应该是 IRQ 而不是 FIQ。 Phew!
关于如果变更处理器的模式的详情请参照 。
32 位操作
这里的许多信息取自 ARM 汇编器手册。我现在没有 32 位处理器,就只能信任文档了... 这个文档中表述的 UMUL 和 UMLA 只能在 32bit 模式下进行是错误的。如果你的处理器(比如:
StrongARM)可以这么做,则它可以在 32bit 或 26bit 下工作...
ARM2 和 ARM3 有一个 32 位数据总线和一个 26 位地址总线。在以后版本的 ARM 上,数据总线和地址总二者都是完全的 32 位宽。这解释了为什么一个“32 位处理器”被称为 26
位。数据宽度和指令/字大小是 32 位,并总是这样,但地址总线只是 24 位。因为 PC 总是字对齐的,一个地址中的低两位总是零,所以在 ARM2/ARM3 处理器上这些位持有处理器模式设置。尽管实际上只使用了 24 位,PC 的有效宽度仍是 26 位。
在老机器上这没有问题。4Mb 内存是基准的。一些人升级到 8Mb、和 16Mb 是理论上的限制。(Some people upgraded to 8Mb, and 16Mb was the theoretical limit.) 但是 RiscPC 使用一个 26 位程序计数器是不可能的,因为 RiscPC 允许安装 258Mb 内存,而 26 位只允许你寻址到 %1100 (或 67108860 字节,或 64Mb)。这附带的解释了对应用任务的 28Mb 大小限制;就是希望系统与老的 RISC OS API 相容。
尽管这个汇编器站点的某些部分覆盖了 32 位模式(比如运行在 SVC32 下的一个简要的例子!),但多数部分是关于 26 位模式操作的,这是为了与 RISC OS 的当前可获得的版本相兼容(就是 RISC OS 2 到 RISC OS 4);我注意到部分例子不适用于 32 位。
RiscPC、Mico、RiscStation、A7000 等都有能力运行完全的 32 位操作系统;实际上
ARMLinux 就是这样的一个操作系统。RISC OS 不是,因为 RISC OS 需要,至少一个时期,保持与现存版本的兼容。这是个古老的两分问题(dichotomy),有一个崭新的完全 32 位版本的 RISC OS 版本是美妙的,但当你发现许多你的现存软件不能继续运行(so much as load)就不那么美妙了!
3PDF created with pdfFactory trial version
RISC OS 不是完全的 26 位。一些处理程序(handler)需要工作在 32 位模式下;限制它的是金钱(就是说,谁为完全转换 RISC OS 付钱;谁为用来重建它们的代码的开发工具付钱(PD
在 RISC OS 上是强壮的))和必要性(就是说,很多人使用 Impression 而 CC 不再与我们同在;Impression 好象不能在更新的 RISC OS 上工作,所以如果人们需要的软件将不能工作,那么他们不会认为有升级的必要)。
为什么这如此重要? 新的 ARM 处理器将不支持 26 位操作。尽管做了一些融合(ARM6、ARM7、StrongARM),但气数就要尽了。你可以增加一个 26/32 位系统的复杂性,或者只用 32 位而得到更简单、更小的处理器。我们要么随波逐流,要么被甩下... 所以我们别无选择。
32 位体系
ARM 体系在 ARM6 系列中进行了重大变更。下面我将描述 26 位 和 32 位操作行为的不同之处。
在 ARM 6 中,程序计数器被扩展到完整的 32 位。结果是:
•
•
•
PSR 从 PC 中分离到自己的寄存器 CPSR(当前的程序状态寄存器)中。
在改变处理器模式的时候,不再与 PC 一起保存 PSR;现在是每个有特权的模式都有一个额外的寄存器 - SPSR (保存的程序状态寄存器) - 用来持有前面模式的
PSR。
增加了使用这些新寄存器的指令。
除了允许 PC 使用完全的 32 位之外,还有进一步的变更,就是给 PSR 增加了额外的有特权的模式。这些模式用于处理未定义指令和异常终止例外:
•
•
未定义指令、异常终止、和超级用户不再共享同一个模式。去掉了在早期 ARM 上存在的对超级用户的那些限制。
在 ARM6 系列(和以后的其他兼容芯片)中通过设置片上某个控制寄存器来确定这些特征的可获得性。可以选择三个处理器配置中的一个:
o
26 位程序和数据空间,这个配置强制 ARM 在 26 位地址空间中进行操作。不可能选择在这个配置中只能获得四个 26 位模式(参照处理器模式描述);任何 32 位模式。在所有当前的 ARM6 和 7 系列上复位(reset)时被设置为这个模式。
o
26 位程序空间和 32 位数据空间。除了禁止地址例外来允许数据传送操作访问完整的 32 位地址空间之外,与 26 位程序和地址空间配置相同。
o
32 位程序和数据空间。这个配置把地址空间扩展成 32 位,并介入了对处理器模型的重大变更。在这个配置中你可以选择任何 26 位和 32 位处理器模式(参见下面的处理器模式)。
在配置成 32 位程序和数据空间的时候,ARM6 和 ARM7 系列支持十个有所重叠的处理器操作模式:
•
用户模式: 正常的程序执行状态;
或 User26 模式: 一个 26 位版本。
4
PDF created with pdfFactory trial version
•
•
•
•
•
FIQ 模式: 设计来支持一个数据传送或通道处理;
或 FIQ26 模式: 一个 26 位版本。
IRQ 模式: 用于通用中断处理;
或 IRQ26 模式: 一个 26 位版本。
SVC 模式: 用于操作系统的保护模式
或 SVC26模式: 一个 26 位模式。
异常终止模式(ABT 模式): 在一个数据或指令预取异常终止(abort)的时候进入的模式。
未定义模式(UND 模式): 在执行了一个未定义的指令的时候进入的模式。
当在一个 26 位处理器模式中的时候,编程模型倒退成早期的 26 位 ARM 处理器。除了下列变动之外,它的行为与 ARM2aS 宏单元(macrocell)相同:
•
•
•
•
只在 ARM 被配置为 26 位程序和数据空间的时候,它才生成地址例外。在其他配置下 OS 仍然可以通过使用外部逻辑模拟地址例外的行为,比如用一个内存管理单元在超出 64Mbyte 范围的时候生成一个异常终止,并把这个异常终止转换成给这个应用程序的一个‘地址例外陷入’。
保持在通用寄存器和程序状态寄存器之间传送数据的新指令可操作。在调用了包含
26 位的 ARM 二进制代码的之后,操作系统可以使用这些新指令返回到一个 32 位模式。
当在一个 32 位程序和数据空间配置下的时候,所有例外(包括未定义指令和软件中断)把处理器返回到一个 32 位模式,所以必须修改操作系统来处理它们。
如果处理器尝试写到在 &0 和 &1F 之间包括二者(就是例外向量)的一个位置,则硬件将禁止写操作并生成一个数据异常终止。这允许操作系统来截获对例外向量的变动并把向量重定向到一些伪装(veneer)代码上。在调用 26 位例外处理程序之前,这些伪装代码应该把处理器置于一个 26 位模式中。
在所有其他方面,当在一个 26 位模式下进行操作的时候,ARM 表现的如同一个 26 位 ARM。CPSR 的相关的位将被组建(incorporated)回到 R15 中,来形成 I 和 F 位在位 27 和 26
的 PC/PSR。指令集表现的如同增加了 MRS 和 MSR 指令的 ARM2aS 宏单元。
在 ARM 6(和以后)的 32 位模式下可获得的寄存器有:
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
5PDF created with pdfFactory trial version
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
简要的说,32 位的与 26 位的不同是:
•
•
•
•
PC 是完全的 32 位宽,并只用做程序计数器。
PSR 包含在它自己的寄存器 CPSR 中。
每个有特权的模式都有一个专有的 SPSR 寄存器,用来保存 CPSR。
这里有两个新的特权模式,每个有特权的模式都有 R13 和 R14 的专有复件。
CPSR 和 SPSR 寄存器
CPSR 寄存器(和保存它的 SPSR 寄存器)中的位分配如下:
31 30 29 28 --- 7 6 - 4 3 2 1 0
6PDF created with pdfFactory trial version
N Z C V I F M4 M3 M2 M1 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 模式
关于 N、Z、C、V 标志和 I、F 中断标志请参见(26 位) PSR。
这在实践中意味着什么?
多数 ARM 代码将正确的工作。唯一不能工作的是通过摆弄 R15 来设置处理器状态的那些操作。不幸的是,好象没有简便的方法修理这个问题。我检查了一个有潜在问题的 9K 程序(一个 MODE 7 teletext frame viewer,用 C 写的),基本上查找:
•
•
用 R15 作为目的寄存器的 MOVS 指令。
以‘^’作为后缀并装载 R15 的 LDMFD 指令。
大约有 64 个指令被归入此类。
好象有没有什么方式来自动进行转换。基本上...
•
•
•
系统如何知道哪个是数据哪个是代码。实际上,一个灵巧的基于规则的程序能够可以做非常准确的猜测,但“非常准确的猜测”就足够了吗?
没有简单的指令替代。一个自动系统可以修补需要的指令并调整(jiggle)周围的代码,但这将导致不希望的副作用,比如一个 ADR 宏指令(directive)不在范围内(in
range)。
需要难以置信的技巧(It is incredibly hacky)。当然,最好重新编译,或修改源代码。
这是很不容易的。这样小的变更,竟有如此严重(far-reaching)的后果。
7PDF created with pdfFactory trial version
程序状态寄存器
•
•
MRS
MSR
直接查看 R15/PSR 在 32-bit 模式下的详情
寄存器 15 (26-bit 模式):
R15 构造如下:
Bit 31 30 29 28 27 26 25------------2 1 0
N Z C V I F 程 序 计 数 器 S1 S0
标志的意义:
N Negative 如果结果是负数则置位
Z Zero 如果结果是零则置位
C Carry 如果发生进位则置位
O Overflow 如果发生溢出则置位
I IRQ 中断禁用
F FIQ 快速中断禁用
S1 和 S0 是处理器模式标志:
S1 S0 模式
0 0 USR - 用户模式
0 1 FIQ - 快速中断模式
1 0 IRQ - 中断模式
1 1 SVC - 超级用户模式
在 R15 作为一个指令的第一个操作数的时候,只有程序计数器部分是可以获得的。所以,下列指令把 PC 复制到一个寄存器中并向这个目标寄存器加上 256:
ADD R0, R15, #256
(对于 BASIC 汇编器 R15 和 PC 的意思是相同的)
8PDF created with pdfFactory trial version
在 R15 作为第二个操作数的时候,所有 32 位都是可以获得的: 程序计数器、标志、和状态。下列代码段将标识当前的处理器模式:
MOV R0, #3 ; 装载一个位掩码(%11)到 R0 中
AND R0, R0, PC; 把 R15 与 R0 做逻辑与并把结果放入 R0,来得到模式状态
CMP R0, #3 ; 把模式与 '3' 相比较(SVC)
BEQ svc ; 如果等于 SVC 模式,分支到 'svc'
CMP R0, #2 ; 把模式与 '2' 相比较 (IRQ)
BEQ irq ; 如果等于 IRQ 模式,分支到 'irq'
CMP R0, #1 ; 把模式与 '1' 相比较(FIQ)
BEQ fiq ; 如果等于 FIQ 模式,分支到 'fiq'
CMP R0, #0 ; 把模式与 '0' 相比较(USR)
BEQ usr ; 如果等于 USR 模式,分支到 'usr'
这个例子不遵从 32-bit 体系。如何在 32-bit 环境中读当前的模式请参照下面的章节。
改变处理器的状态:
要改变处理器模式、或者任何标志,我们需要用想要的标志 EOR(异或)状态标志,新状态 =
旧状态 EOR (1 << 28) 可以成为改变 oVerflow 标志的伪码。但是我们不能做这个简单的
EORS 操作,原因是这将导致随后的两个指令被跳过。不要担心,指令
TEQ 做一个假装的 EOR (结果不存储到任何地方)。把它与
P 后缀组合,则把结果的第 0、1、和 26 至 31
位直接写到 R15 的第 0、1、和 26 至 31 位,这是改变标志的一个简便的方法:
TEQP R15, bit_mask
如果你处在允许你设置一个标志的一个模式中,则你只可以改变这个标志。
这个例子不遵从 32-bit 体系。如何在 32-bit 环境中改变模式请参照下面的章节。
可以被扩充它来改变处理器模式。例如,要进入 SVC 模式你可以:
MOV R6, PC ; 把 PC 的最初状态存储到 R6 中
ORR R7, R6, #3 ; 设置 SVC 模式
TEQP R7, #0 ; 把(在 R7 中的)模式标志写入 PC
9PDF created with pdfFactory trial version
而返回最初的模式是:
TEQP R6, #0 ; 把(在 R6 中的)最初的模式写入 PC
在改变了模式之后,你应该进行一个空操作来允许这个寄存器安定下来。比如
MOV R0, R0
之类的东西就可以。废弃使用 NV 后缀的指令。
32 位 PSR
如同在32 位操作中描述的那样,ARM 3 之后的处理器提供一个 32 bit 地址空间,它们把 PSR 移出 R15 并给予 R15 完整的 32 位位域,在其中存储当前位置的地址。目前,除了一些不太可能遇到的情况之外,RISC OS 工作在 26 位模式。
32 位模式是重要的,因为 26 位(在老的 PSR 中)把每个应用程序的可寻址内存的最大数量限制为 28Mb。这就是不管你安装了多少内存你不能拖动超过 28Mb 的下一个槽(drag the
Next slot beyond 28Mb)的原因。
CPSR 寄存器(和保存它的 SPSR 寄存器)中的位分配如下:
31 30 29 28 --- 7 6 - 4 3 2 1 0
N Z C V I F M4 M3 M2 M1 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 模式
10PDF created with pdfFactory trial version
典型的,处理器将在 User26、FIQ26、IRQ26 和 SVC26 下操作。可以进入一个 32 位模式,但要格外小心。RISC OS 不希望这样,并且如果它发现自己在其中会非常生气!
操纵 32 位 PSR 的指令
你不能在 32 位模式中使用
MOVS PC, R14 或
LDMFD R13!, {registers, PC}^。也不能使用
ORRS PC, R14, #1<<28 来设置 V 标志。现在需要使用
MRS 和
MSR。
复制一个寄存器到 PSR 中
MSR CPSR, R0 ; 复制 R0 到 CPSR 中
MSR SPSR, R0 ; 复制 R0 到 SPSR 中
MSR CPSR_flg, R0 ; 复制 R0 的标志位到 CPSR 中
MSR CPSR_flg, #1<<28 ; 复制(立即值)标志位到 CPSR 中
复制 PSR 到一个寄存器中
MRS R0, CPSR ; 复制 CPSR 到 R0 中
MRS R0, SPSR ; 复制 SPSR 到 R0 中
指令格式
你有两个 PSR -
CPSR 是当前的程序状态寄存器(Current Program Status Register),而
SPSR 是保存的程序状态寄存器(Saved Program Status Register)(前面的处理器模式的
PSR)。每个有特权的模式都有自己的 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。使用
_flg 后缀允许你改变标志位而不影响控制位。
11
PDF created with pdfFactory trial version
在 user(32) 模式中,保护 CPSR 的控制位,你只能改变条件标志。在其他模式中,可获得整个 CPSR。你不应该指定 R15 为一个源寄存器或一个目标寄存器。最后,在 user(32) 模式中,你不能尝试访问 SPSR,因为它不存在!
要设置 V 标志:
MSR CPSR_flg, #&10000000
这将设置 V 标志但不影响控制位。
要改变模式:
MRS R0, CPSR_all ; 复制 PSR
BIC R0, R0, #&1F ; 清除模式位
ORR R0, R0, #new_mode ; 把模式位设置为新模式
MSR CPSR_all, R0 ; 写回 PSR,变更模式
现在我们要做的是进入 SVC32 模式并设置 Z 标志。接着我们返回 SVC26 模式并‘测试’是否设置了 Z。
RISC OS 不希望发现自己处在 32 位模式中,所以我们要禁止所有中断并保持它们这样(keep them that way)。尽管这些代码应该执行的非常快,但我们不应当冒任何风险...
你可能觉得 32 位模式不是非常有用。在当前版本的 RISC OS 下,这是事实。实际上,就我而言,32 位模式提供给你的只是:
访问大于 28Mb 的区域。在 RISC OS 上这不是真的很重要,在这个系统里 web 浏览器适合于 1 M 或 2 M,而重要的艺术程序为那些非常巨大的图象提供它们自己的虚拟内存系统。
本文档的最初版本,和最初的 ARM 汇编器指南包括...
StrongARM 提供了两个指令(UMUL 和 UMLA、IIRC),它们处理 64 位乘法。这只能在 32 位模式下获得。
这是错误的。在 26 位模式下可以使用扩展的乘法;MP3 解码器就使用了它!
尽管 32 位模式的利益好象不是多的那么惊人,新近的处理器(比如 Xscale)不再支持 26
位模式,所以 RISC OS 和它的应用程序要在 32 位环境下工作则必须经过修改。听起来不是很多,但是如果所有补偿/改变 R15 中的 PSR 位的引用都必须被变更为对不在 R15 中的独立的 PSR 的引用,这就突然变成一个非常重大的问题了。还有你不能继续用一个指令来恢复 PSR 并分支回到调用者,现在这需要两个独立的指令。为此代码必须重写。你不能简单的用另一个指令来修补...
12PDF created with pdfFactory trial version
寄存器装载和存储
•
•
•
•
•
LDM
LDR
STM
STR
SWP
它们可能是能获得的最有用的指令。其他指令都操纵寄存器,所以必须把数据从内存装载寄存器并把寄存器中的数据存储到内存中。
传送单一数据
使用单一数据传送指令(STR 和 LDR)来装载和存储单一字节或字的数据从/到内存。寻址是非常灵活的。
首先让我们查看指令格式:
LDR{条件} Rd, <地址>
STR{条件} Rd, <地址>
LDR{条件}B Rd, <地址>
STR{条件}B Rd, <地址>
指令格式
这些指令装载和存储 Rd 的值从/到指定的地址。如果象后面两个指令那样还指定了‘B’,则只装载或存储一个单一的字节;对于装载,寄存器中高端的三个字节被置零(zeroed)。
地址可以是一个简单的值、或一个偏移量、或者是一个被移位的偏移量。可以还可以把合成的有效地址写回到基址寄存器(去除了对加/减操作的需要)。
各种寻址方式的示例:
译注:下文中的 Rbase 是表示基址寄存器,Rindex 表示变址寄存器,index 表示偏移量,偏移量为 12 位的无符号数。用移位选项表示比例因子。标准寻址方式 - 用 AT&T 语法表示为 disp(base, index, scale),用 Intel 语法表示为
[base + index*scale + disp],中的变址(连带比例因子)与偏移量不可兼得。
STR Rd, [Rbase] 存储 Rd 到 Rbase 所包含的有效地址。
13PDF created with pdfFactory trial version
STR Rd, [Rbase, Rindex] 存储 Rd 到 Rbase + Rindex 所合成的有效地址。
STR Rd, [Rbase, #index] 存储 Rd 到 Rbase + index 所合成的有效地址。
index 是一个立即值。
例如,STR Rd, [R1, #16] 将把 Rd 存储到
R1+16。
STR 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,
(不是)。
如果你指定预先变址寻址(这里的基址和变址都在方括号中),用是否存在‘!’来控制写回操作。上面的第4和第5个例子中使用了这个标志。你可以使用它来在内存中自动正向或反向移动。一个字符串打印例程将变成:
.loop
LDRB R0, [R1, #1]!
SWI "OS_WriteC"
CMP R0, #0
BNE loop
而不是:
.loop
LDRB R0, [R1]
SWI "OS_WriteC"
ADD R1, R1, #1
14PDF created with pdfFactory trial version
CMP R0, #0
BNE loop
对于过后变址寻址‘!’是无效的(这里的变址在方括号外面,比如上面的例子6),因为写回是暗含的。
如同你见到的那样,变址可以被移位来实现比例缩放。除此之外,可以从基址上减去偏移量。在这种情况下,你可以使用如下代码:
LDRB R0, [R1, #-1]
尽管你可以存储或装载 PC,但你不可以用装载或存储指令来修改 PSR。要装载一个被存储的‘状态’并正确的恢复它,请使用:
LDR R0, [Rbase]
MOVS R15, R0
假如你在有特权的模式下,MOVS 将导致 PSR 的位被更改。
对 PC 使用
MOVS 不遵从 32-bit 体系,你需要使用 MRS 和 MSR 来处理 PSR。
依照 ARM 汇编手册:
译注:下文所叙述内容针对的是小端字节序配置,对大端字节序配置在手册中另有专门叙述。
•
•
•
如果提供的地址在一个字边界上,则字节装载(LDRB)使用在 0 至 7 位上的数据,如果在一个字地址加上一个字节上,则使用 8 至 15 位,以此类推。选择的字节被放入目标寄存器的低端 8 位中,并把寄存器中其余的位用零填充。
字节存储(STRB)在数据总线上重复源寄存器的的低端 8 位 4 次。由外部的内存系统来激活适当的字节子系统来存储数据。
字装载(LDR)或字存储(STR)将生成一个字对齐的地址。使用一个非字对齐的地址将有不明显和未规定的结果。实际上提示的是你不能使用 LDR 从一个非对齐的地址装载一个字。
传送多个数据
使用多数据传送指令(LDM 和 STM)来装载和存储多个字的数据从/到内存。
LDM/STM 的主要用途是把需要保存的寄存器复制到栈上。如我们以前见到过的
STMFD R13!, {R0-R12, R14}。
指令格式是:
xxM{条件}{类型} Rn{!}, <寄存器列表>{^}
15PDF created with pdfFactory trial version
‘xx’是 LD 表示装载,或 ST 表示存储。
再加 4 种‘类型’就变成了 8 个指令:
栈 其他
LDMED LDMIB 预先增加装载
LDMFD LDMIA 过后增加装载
LDMEA LDMDB 预先减少装载
LDMFA LDMDA 过后减少装载
STMFA STMIB 预先增加存储
STMEA STMIA 过后增加存储
STMFD STMDB 预先减少存储
STMED STMDA 过后减少存储
指令格式
汇编器关照如何映射这些助记符。注意 ED 不同于 IB;只对于预先减少装载是相同的。在存储的时候,ED 是过后减少的。
FD、ED、FA、和 EA 指定是满栈还是空栈,是升序栈还是降序栈。一个满栈的栈指针指向上次写的最后一个数据单元,而空栈的栈指针指向第一个空闲单元。一个降序栈是在内存中反向增长(就是说,从应用程序空间结束处开始反向增长)而升序栈在内存中正向增长。
其他形式简单的描述指令的行为,意思分别是过后增加(Increment After)、预先增加(Increment Before)、过后减少(Decrement After)、预先减少(Decrement
Before)。
RISC OS 使用传统的满降序栈。在使用符合 APCS 规定的编译器的时候,它通常把你的栈指针设置在应用程序空间的结束处并接着使用一个 FD (满降序 - Full
Descending)栈。如果你与一个高级语言(BASIC 或 C)一起工作,你将别无选择。栈指针(传统上是 R13)指向一个满降序栈。你必须继续这个格式,或则建立并管理你自己的栈(如果你是死硬派人士那么你可能喜欢这样做!)。
‘基址’是包含开始地址的寄存器。在传统的 RISC OS 下,它是栈指针 R13,但你可以使用除了 R15 之外的任何可获得的寄存器。
如果你想把复制操作后栈顶的当前的内存地址保存到栈指针中,可以寄存器按从最低到最高的编号次序与到从低端到高端的内存之间传送数据。并且因为用指令中的一个单一的位来表示是否保存一个寄存器,不可能指定某个寄存器两次。它的副作用是不能用下面这样的代码:
STMFD R13!, {R0, R1}
LDMFD R13!, {R1, R0}
来交换两个寄存器的内容。
16PDF created with pdfFactory trial version
提供了一个有用的简写。要包含一个范围的寄存器,可以简单的只写第一个和最后一个,并在其间加一个横杠。例如 R0-R3 等同与 R0, R1, R2, R3,只是更加整齐和理智而已...
在把 R15 存储到内存中的时候,还保存了 PSR 位。在重新装载 R15 的时候,除非你要求否则不恢复 PSR 位。要求的方法是在寄存器列表后跟随一个‘^’。
STMFD R13!, {R0-R12, R14}
...
LDMFD R13!, {R0-R12, PC}
这保存所有的寄存器,做一些事情,接着重新装载所有的寄存器。从 R14 装载 PC,它由一个 BL 或此类指令所设置。不触及 PSR 标志。
STMFD R13!, {R0-R12, R14}
...
LDMFD R13!, {R0-R12, PC}^
这保存所有的寄存器,做一些事情,接着重新装载所有的寄存器。从 R14 装载 PC,它由一个 BL 或此类指令所设置。变更 PSR 标志。
警告: 这些代码不遵从 32 bit 体系。你需要使用 MRS 和 MSR 来处理 PSR,你不能使用‘^’后缀。
注意在这两个例子中,R14 被直接装载到 PC 中。这节省了对 MOV(S) R14 到
R15 中的需要。
警告: 使用 MOVS PC,... 不遵从 32 bit 体系。你需要使用 MRS 和 MSR 来处理 PSR。
SWP : 单一数据交换
(Swap)
SWP{条件}{B}
指令格式
SWP 将:
•
•
从操作数 2 所指向的内存装载一个字并把这个字放置到目的寄存器中。
把寄存器操作数 1 的内容存储到同一个地址中。
如果目的和操作数 1 是同一个寄存器,则把寄存器的内容和给定内存位置的内容进行交换。
如果提供了 B 后缀,则将传送一个字节,否则传送一个字。
17PDF created with pdfFactory trial version
算术和逻辑指令
•
•
•
•
•
•
•
•
•
•
•
•
ADC
ADD
AND
BIC
EOR
MOV
MVN
ORR
RSB
RSC
SBC
SUB
指令格式
ADC : 带进位的加法
(Addition with Carry)
ADC{条件}{S}
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 后缀来更改进位标志。
ADD : 加法
(Addition)
ADD{条件}{S}
18PDF created with pdfFactory trial version
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)
加法可以在有符号和无符号数上进行。
AND : 逻辑与
(logical AND)
AND{条件}{S}
dest = op_1 AND op_2
AND 将在两个操作数上进行逻辑与,把结果放置到目的寄存器中;对屏蔽你要在上面工作的位很有用。 操作数 1 是一个寄存器,操作数 2 可以是一个寄存器,被移位的寄存器,或一个立即值:
AND R0, R0, #3 ; R0 = 保持 R0 的位 0 和 1,丢弃其余的位。
AND 的真值表(二者都是 1 则结果为 1):
Op_1 Op_2 结果
0 0 0
0 1 0
1 0 0
1 1 1
BIC : 位清除
(Bit Clear)
BIC{条件}{S}
dest = op_1 AND (!op_2)
BIC 是在一个字中清除位的一种方法,与 OR 位设置是相反的操作。操作数 2 是一个 32
位位掩码(mask)。如果如果在掩码中设置了某一位,则清除这一位。未设置的掩码位指示此位保持不变。
19PDF created with pdfFactory trial version
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
EOR : 逻辑异或
(logical Exclusive OR)
EOR{条件}{S}
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
MOV : 传送
(Move)
MOV{条件}{S}
dest = op_1
MOV 从另一个寄存器、被移位的寄存器、或一个立即值装载一个值到目的寄存器。你可以指定相同的寄存器来实现 NOP 指令的效果,你还可以专门移位一个寄存器:
MOV R0, R0 ; R0 = R0... NOP 指令
20PDF created with pdfFactory trial version
MOV R0, R0, LSL#3 ; R0 = R0 * 8
如果 R15 是目的寄存器,将修改程序计数器或标志。这用于返回到调用代码,方法是把连接寄存器的内容传送到 R15:
MOV PC, R14 ; 退出到调用者
MOVS PC, R14 ; 退出到调用者并恢复标志位
(不遵从 32-bit 体系)
MVN : 传送取反的值
(Move Negative)
MVN{条件}{S}
dest = !op_1
MVN 从另一个寄存器、被移位的寄存器、或一个立即值装载一个值到目的寄存器。不同之处是在传送之前位被反转了,所以把一个被取反的值传送到一个寄存器中。这是逻辑非操作而不是算术操作,这个取反的值加 1 才是它的取负的值:
MVN R0, #4 ; R0 = -5
MVN R0, #0 ; R0 = -1
ORR : 逻辑或
(logical OR)
ORR{条件}{S}
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
21PDF created with pdfFactory trial version
RSB : 反向减法
(Reverse Subtraction)
RSB{条件}{S}
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
反向减法可以在有符号或无符号数上进行。
RSC : 带借位的反向减法
(Reverse Subtraction with Carry)
RSC{条件}{S}
dest = op_2 - op_1 - !carry
同于
SBC,但倒换了两个操作数的前后位置。
SBC : 带借位的减法
(Subtraction with Carry)
SBC{条件}{S}
dest = op_1 - op_2 - !carry
SBC 做两个操作数的减法,把结果放置到目的寄存器中。它使用进位标志来表示借位,这样就可以做大于 32 位的减法。SUB 和
SBC 生成进位标志的方式不同于常规,如果需要借位则清除进位标志。所以,指令要对进位标志进行一个非操作 - 在指令执行期间自动的反转此位。
22PDF created with pdfFactory trial version
SUB : 减法
(Subtraction)
SUB{条件}{S}
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)
减法可以在有符号和无符号数上进行。
移 位
•
•
•
•
•
•
LSL
ASL
LSR
ASR
ROR
RRX
ARM 处理器组建了可以与数据处理指令(ADC、ADD、AND、BIC、CMN、CMP、EOR、MOV、MVN、ORR、RSB、SBC、SUB、TEQ、TST)一起使用的桶式移位器(barrel shifter)。你还可以使用桶式移位器影响在 LDR/STR 操作中的变址值。
译注:移位操作在 ARM 指令集中不作为单独的指令使用,它是指令格式中是一个字段,在汇编语言中表示为指令中的选项。如果数据处理指令的第二个操作数或者单一数据传送指令中的变址是寄存器,则可以对它进行各种移位操作。如果数据处理指令的第二个操作数是立即值,在指令中用 8 位立即值和 4 位循环移位来表示它,所以对大于 255 的立即值,汇编器尝试通过在指令中设置循环移位数量来表示它,如果不能表示则生成一个错误。在逻辑类指令中,逻辑运算指令由指令中 S 位的设置或清除来确定是否影响进位标志,而比较指令的 S 位总是设置的。在单一数据传送指令中指定移位的数量只能用立即值而不能用寄存器。
下面是给不同的移位类型的六个助记符:
LSL 逻辑左移
23PDF created with pdfFactory trial version
ASL 算术左移
LSR 逻辑右移
ASR 算术右移
ROR 循环右移
RRX 带扩展的循环右移
ASL 和
LSL 是等同的,可以自由互换。
你可以用一个立即值(从 0 到 31)指定移位数量,或用包含在 0 和 31 之间的一个值的寄存器指定移位数量。
逻辑或算术左移
(Logical or Arithmetic Shift Left)
Rx, LSL #n or
Rx, ASL #n or
Rx, LSL Rn or
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
逻辑右移
(Logical Shift Right)
Rx, LSR #n or
Rx, LSR Rn
它在概念上与左移相对。把所有位向更低有效位方向移动。如果逻辑类指令中 S 位被设置了,则把最后被移出最右端的那位放置到进位标志中。它同于 BASIC 的
register =
value >>> shift。
24PDF created with pdfFactory trial version
算术右移
(Arithmetic Shift Right)
Rx, ASR #n or
Rx, ASR Rn
类似于 LSR,但使用要被移位的寄存器(Rx)的第 31 位的值来填充高位,用来保护补码表示中的符号。如果逻辑类指令中 S 位被设置了,则把最后被移出最右端的那位放置到进位标志中。它同于 BASIC 的
register = value >> shift。
循环右移
(Rotate Right)
Rx, ROR #n or
Rx, ROR Rn
循环右移类似于逻辑右移,但是把从右侧移出去的位放置到左侧,如果逻辑类指令中 S 位被设置了,则同时放置到进位标志中,这就是位的‘循环’。一个移位量为 32 的操作将导致输出与输入完全一致,因为所有位都被移位了 32 个位置,又回到了开始时的位置!
带扩展的循环右移
(Rotate Right with extend)
Rx, RRX
这是一个 ROR#0 操作,它向右移动一个位置 - 不同之处是,它使用处理器的进位标志来提供一个要被移位的 33 位的数量。
Thanks to
Ian Jeffray for the correction.
25PDF created with pdfFactory trial version
乘法指令
•
•
MLA
MUL
指令格式
这两个指令与普通算术指令在对操作数的限制上有所不同:
1. 给出的所有操作数、和目的寄存器必须为简单的寄存器。
2. 你不能对操作数 2 使用立即值或被移位的寄存器。
3. 目的寄存器和操作数 1 必须是不同的寄存器。
4. 最后,你不能指定 R15 为目的寄存器。
MLA : 带累加的乘法
(Multiplication with Accumulate)
MLA{条件}{S}
dest = (op_1 * op_2) + op_3
MLA 的行为同于
MUL,但它把操作数 3 的值加到结果上。这在求总和时有用。
MUL : 乘法
(Multiplication)
MUL{条件}{S}
dest = op_1 * op_2
MUL 提供 32 位整数乘法。如果操作数是有符号的,可以假定结果也是有符号的。
26PDF created with pdfFactory trial version
比较指令
•
•
•
•
CMN
CMP
TEQ
TST
指令格式
译注:CMP 和 CMP 是算术指令,TEQ 和 TST 是逻辑指令。把它们归入一类的原因是它们的 S 位总是设置的,就是说,它们总是影响标志位。
CMN : 比较取负的值
(Compare Negative)
CMN{条件}{P}
status = op_1 - (- op_2)
CMN 同于
CMP,但它允许你与小负值(操作数 2 的取负的值)进行比较,比如难于用其他方法实现的用于结束列表的 -1。这样与 -1 比较将使用:
CMN R0, #1 ; 把 R0 与 -1 进行比较
详情参照
CMP 指令。
CMP : 比较
(Compare)
CMP{条件}{P}
status = op_1 - op_2
CMP 允许把一个寄存器的内容如另一个寄存器的内容或立即值进行比较,更改状态标志来允许进行条件执行。它进行一次减法,但不存储结果,而是正确的更改标志。标志表示的是操作数 1 比操作数 2 如何(大小等)。如果操作数 1 大于操作操作数 2,则此后的有 GT 后缀的指令将可以执行。
明显的,你不需要显式的指定
S 后缀来更改状态标志... 如果你指定了它则被忽略。
27PDF created with pdfFactory trial version
TEQ : 测试等价
(Test Equivalence)
TEQ{条件}{P}
Status = op_1 EOR op_2
TEQ 类似于
TST。区别是这里的概念上的计算是 EOR 而不是 AND。这提供了一种查看两个操作数是否相同而又不影响进位标志(不象
CMP
那样)的方法。加上
P 后缀的
TEQ 还可用于改变 R15 中的标志(在 26-bit 模式中)。详情请参照 ,在 32-bit 模式下如何做请参见这里。
TST : 测试位
(Test bits)
TST{条件}{P}
Status = op_1 AND op_2
TST 类似于
CMP,不产生放置到目的寄存器中的结果。而是在给出的两个操作数上进行操作并把结果反映到状态标志上。使用
TST 来检查是否设置了特定的位。操作数 1 是要测试的数据字而操作数 2 是一个位掩码。经过测试后,如果匹配则设置 Zero 标志,否则清除它。象
CMP
那样,你不需要指定
S 后缀。
TST R0, #%1 ; 测试在 R0 中是否设置了位 0。
分支指令
•
•
B
BL
指令格式
B : 分支
(Branch)
B{条件} <地址>
B 是最简单的分支。一旦遇到一个
B 指令,ARM 处理器将立即跳转到给定的地址,从那里继续执行。注意存储在分支指令中的实际的值是相对当前的 R15 的值的一个偏移量;而 28PDF created with pdfFactory trial version
不是一个绝对地址。它的值由汇编器来计算,它是 24 位有符号数,左移两位后有符号扩展为 32 位,表示的有效偏移为 26 位(+/- 32 M)。
在其他处理器上,你可能经常见到这样的指令:
OPT 1
LDA &70
CMP #0
BEQ Zero
STA &72
.Zero RTS
(取自 Acorn Electron User Guide issue 1 page 213)
在 ARM 处理器上,它们将变成下面这些东西:
OPT 1
ADR R1, #&70
LDR R0, [R1]
CMP #0
BEQ Zero
STR R0, [R1, #2]
.Zero
MOV PC, R14
这不是一个很好的例子,但你可以构想如何更好的去条件执行而不是分支。另一方面,如果你有大段的代码或者你的代码使用状态标志,那么你可以使用条件执行来实现各类分支: 这样一个单一的简单条件执行指令可以替代在其他处理器中存在的所有这些分支和跳转指令。
OPT 1
ADR R1, #&70
LDR R0, [R1]
CMP R0, #0
STRNE R0, [R1, #2]
MOV PC, R14
BL : 带连接的分支
(Branch with Link)
BL{条件} <地址>
BL 是另一个分支指令。就在分支之前,在寄存器 14 中装载上 R15 的内容。你可以重新装载 R14 到 R15 中来返回到在这个分支之后的那个指令,它是子例程的一个基本但强力的实现。它的作用在屏幕装载器 2 (例子 4)中得以很好的展现...
.load_new_format
29PDF created with pdfFactory trial version
BL switch_screen_mode
BL get_screen_info
BL load_palette
.new_loop
MOV R1, R5
BL read_byte
CMP R0, #255
BLEQ read_loop
STRB R0, [R2, #1]!
...在这里我们见到在装载器循环之前调用了三个子例程。接着,一旦满足了条件执行就在循环中调用了
read_byte 子例程。
条件执行
条件执行
指令格式
ARM 处理器的一个非常特殊的特征是它的条件执行。我们指的不是基本的如果进位则分支,ARM 使这个逻辑阶段进一步深化为如果进位则
XXX
- 这里的 XXX 是任何东西。
为了举例,下面是 Intel 8086 处理器分支指令的一个列表:
JA Jump if Above
JAE Jump if Above or Equal
JB Jump if Below
JBE Jump if Below or Equal
JC Jump if Carry
JCXZ Jump if CX Zero (CX is a register that can be used for loop counts)
JE Jump if Equal
JG Jump if Greater than
JGE Jump if Greater than or Equal
JL Jump if Less than
JLE Jump if Less Than or Equal
JMP JuMP
JNA Jump if Not Above
JNAE Jump if Not Above or Equal
JNB Jump if Not Below
JNBE Jump if Not Below or Equal
JNC Jump if No Carry
30PDF created with pdfFactory trial version
JNE Jump if Not Equal
JNG Jump if Not Greater than
JNGE Jump if Not Greater than or Equal
JNL Jump if Not Less than
JNLE Jump if Not Less than or Equal
JNO Jump if Not Overflow
JNP Jump if Not Parity
JNS Jump if Not Sign
JNZ Jump if Not Zero
JO Jump if Overflow
JP Jump if Parity
JPE Jump if Parity Even
JPO Jump if Parity Odd
JS Jump if Sign
JZ Jump if Zero
80386 添加了:
JECXZ Jump if ECX Zero
作为对比,ARM 处理器只提供了:
B 分支
BL 带连接的分支
但 ARM 提供了条件执行,你可以不受这个表面上不灵活的方式的限制:
BEQ Branch if EQual
BNE Branch if Not Equal
BVS Branch if oVerflow Set
BVC Branch if oVerflow Clear
BHI Branch if HIgher
BLS Branch if Lower or the Same
BPL Branch if PLus
BMI Branch if MInus
BCS Branch if Carry Set
BCC Branch if Carry Clear
BGE Branch if Greater than or Equal
BGT Branch if Greater Than
BLE Branch if Less than or Equal
BLT Branch if Less Than
BLEQ Branch with Link if EQual
....
BLLT Branch with Link if Less Than
还有两个代码,
•
AL - ALways,缺省条件所以不须指定
•
NV - NeVer,不是非常有用。你无论如何不要使用这个代码...
PDF created with pdfFactory trial version 31
当你发现所有 Bxx 指令实际上是同一个指令的时候,紧要关头就到了。接着你会想,如果你可以在一个分支指令上加上所有这些条件,那么对一个寄存器装载指令能否加上它们? 答案是可以。
下面是可获得的条件代码的列表:
EQ : 等于
如果一次比较之后设置了 Z 标志。
NE : 不等于
如果一次比较之后清除了 Z 标志。
VS : 溢出设置
如果在一次算术操作之后设置了 V 标志,计算的结果不适合放入一个 32bit 目标寄存器中。
VC : 溢出清除
如果清除了 V 标志,与 VS 相反。
HI : 高于(无符号)
如果一次比较之后设置了 C 标志并清除了 Z 标志。
LS : 低于或同于(无符号)
如果一次比较操作之后清除了 C 标志或设置了 Z 标志。
PL : 正号
如果一次算术操作之后清除了 N。出于定义‘正号’的目的,零是正数的原因是它不是负数...
MI : 负号
如果一次算术操作之后设置了 N 标志。
CS : 进位设置
如果一次算术操作或移位操作之后设置了 C 标志,操作的结果不能表示为 32bit。你可以把 C 标志当作结果的第 33 位。
CC : 进位清除
与 CS 相反。
GE : 大于或等于(有符号)
如果一次比较之后...
设置了 N 标志并设置了 V 标志
或者...
清除了 N 标志并清除了 V 标志。
32PDF created with pdfFactory trial version
GT : 大于(有符号)
如果一次比较之后...
设置了 N 标志并设置了 V 标志
或者...
清除了 N 标志并清除了 V 标志
并且...
清除了 Z 标志。
LE : 小于或等于(有符号)
如果一次比较之后...
设置了 N 标志并清除了 V 标志
或者...
清除了 N 标志并设置了 V 标志
并且...
设置了 Z 标志。
LT : 小于(有符号)
如果一次比较之后...
设置了 N 标志并清除了 V 标志。
或者...
清除了 N 标志并设置了 V 标志。
AL : 总是
缺省条件,所以不用明显声明。
NV : 从不
不是特别有用,它表示应当永远不执行这个指令。是穷人的 NOP。
包含 NV 是为了完整性(与 AL 相对),你不应该在你的代码中使用它。
有一个在最后的条件代码
S,它以相反的方式工作。当用于一个指令的时候,导致更改状态标志。这不是自动发生的 - 除非这些指令的目的是设置状态。例如:
ADD R0, R0, R1
ADDS R0, R0, R1
ADDEQS R0, R0, R1
第一个例子是一个基本的加法(把 R1 的值增加到 R0),它不影响状态寄存器。
第二个例子是同一个加法,只不过它导致更改状态寄存器。
最后一个例子是同一个加法,更改状态寄存器。不同在于它是一个有条件的指令。只有前一个操作的结果是 EQ (如果设置了 Z 标志)的时候它才执行。
下面是条件执行的一个工作中的例子。你把寄存器 0 与存储在寄存器 10 中内容相比较。如果不等于 R10,则调用一个软件中断,增加它并分支回来再次做这些。否则清除 R10 并返回到调用它的那部分代码(它的地址存储在 R14)。
33PDF created with pdfFactory trial version
条件执行的一个例子
.loop ; 标记循环开始位置
CMP R0, R10 ; 把 R0 与 R10 相比较
SWINE &40017 ; 不等于: 调用 SWI &40017
ADDNE R0, R0, #1 ; 向 R0 加 1
BNE loop ; 分支到 'loop'
MOV R10, #0 ; 等于 : 设置 R10 为零
LDMFD R13!, {R0-R12,PC} ; 返回到调用者
注解:
•
•
SWI 编号就象我写的这样。在 RISC OS 下,它是给 Econet_DoImmediate 的编号。不要字面的接受它,这只是一个例子!
你可能以前没见过
LDMFD,它从栈中装载多个寄存器。在这个例子中,我们从一个完全正式的栈中装载 R0 至 R12 和 R14。关于寄存器装载和存储的更多信息请参阅 。
我说要装载 R14。那么为什么要把它放入 PC 中? 原因是此时 R14 存储的值包含返回地址。我们也可以采用:
•
LDMFD R13!, {R0-R12,R14}
MOV PC, R14
•
但是直接恢复到 PC 中可以省略这个
MOV 语句。
最后,这些寄存器很有可能被一个 SWI 调用所占用(依赖于在调用期间执行的代码),所以你最好把你的重要的寄存器压入栈中,以后在恢复它们。
SWI 指令
SWI : 软件中断
(Software Interrupt)
SWI{条件} <24 位编号>
指令格式
这是一个简单的设施,但可能是最常用的。多数操作系统设施是用 SWI 提供的。没有 SWI 的 RISC OS 是不可想象的。
Nava Whiteford 解释了 SWI 是如何工作的(最初在 Frobnicate issue 12½)...
34PDF created with pdfFactory trial version
SWI 是什么?
SWI 表示 Software Interrupt。在 RISC OS 中使用 SWI 来访问操作系统例程或第三方生产的模块。许多应用使用模块来给其他应用提供低层外部访问。
SWI 的例子有:
•
•
•
文件器 SWI,它辅助读写磁盘、设置属性等。
打印机驱动器 SWI,用来辅助使用打印并行端口。
FreeNet/Acorn TCP/IP 协议栈 SWI,用 TCP/IP 协议在 Internet 上发送和接收数据。
在以这种方式使用的时候,SWI 允许操作系统拥有一个模块结构,这意味着用来建立完整的操作系统的所需的代码可以被分割成许多小的部分(模块)和一个模块处理程序(handler)。
当 SWI 处理程序得到对特定的例程编号的一个请求的时候,它找到这个例程的位置并执行它,并传递(有关的)任何数据。
它是如何工作的?
首先查看一下如何使用它。一个 SWI 指令(汇编语言)看起来如下:
SWI &02
或
SWI "OS_Write0"
这些指令实际上是相同的,将被汇编成相同的指令。唯一的不同是第二个指令使用一个字符串来表示 SWI 编号 &02。在使用采用了字符串编号的程序的时候,在执行之前首先查找这个字符串。
在这里我们不想处理字符串,因为它不能给出它要进行什么的一个真实表示。它们通常用于增进一个程序的清晰程度,但不是实际执行的指令。
让我们再次看一下第一个指令:
SWI &02
这是什么意思? 字面的意思是进入 SWI 处理程序并传递值 &02。在 RISC OS 中这意味着执行编号是 &02 的例程。
它是如何这么作的? 它如何传递 SWI 编号和进入 SWI 处理程序?
如果你查看内存的开始 32 字节(位于 0-&1C)并反汇编它们(查开实际的 ARM
指令)你将见到如下:
35PDF created with pdfFactory trial version
地址 内容 反汇编
00000000 : 0..å : E5000030 : STR R0,[R0,#-48]
00000004 : .óŸå : E59FF31C : LDR PC,&00000328
00000008 : .óŸå : E59FF31C : LDR PC,&0000032C
0000000C : .óŸå : E59FF31C : LDR PC,&00000330
00000010 : .óŸå : E59FF31C : LDR PC,&00000334
00000014 : .óŸå : E59FF31C : LDR PC,&00000338
00000018 : .óŸå : E59FF31C : LDR PC,&0000033C
0000001C : 2?ã : E3A0A632 : MOV R10,#&3200000
让我们仔细看一下。
除了第一个和最后一个指令之外(它们是特殊情况)你见到的都是把一个新值装载到 PC (程序计数器)的指令,它们告诉计算机到哪里去执行下一个指令。还展示了这个值是从内存中的一个地址接受来的。(你可以在 !Zap 主菜单上使用“Read Memory”选项去自己查看一下。)
这看起来好象与 SWI 没多少关系,下面做进一步的说明。
一个 SWI 所做的一切就是把模式改变成超级用户并设置 PC 来执行在地址 &08
处的下一个指令! 把处理器转换到超级用户模式会切换掉两个寄存器 r13 和
r14 并用 r13_svc 和 r14_svc 替换它们。
在进入超级用户模式的时候,还把 r14_svc 设置为在这个 SWI 指令之后的地址。
这个实际上就象一个连接到地址 &08 的分支指令(BL &08),但带有用于一些数据(SWI 编号)的空间。
象我说过的那样,地址 &08 包含跳转到另一个地址的一个指令,就是实际的 SWI
程序的地址!
此时你可能会想“稍等一会! 还有 SWI 编号呢?”。实际上处理器忽略这个值本身。SWI 处理程序使用传递来的 r14_svc 的值来获取它。
下面是完成它的步骤(在存储寄存器 r0-r12 之后):
1.
2.
3.
4.
5.
6.
7.
它从 r14 中减去 4 来获得 SWI 指令的地址。
把这个指令装载到一个寄存器。
清除这个指令的高端 8 位,去掉了 OpCode 而只剩下的 SWI 编号。
使用这个值来找到要被执行的代码的例程的地址(使用查找表等)。
恢复寄存器 r0-r12。
使处理器离开超级用户模式。
跳转到这个例程的地址。
容易吧! ;)
36PDF created with pdfFactory trial version
下面是一个例子,来自 ARM610 datasheet:
0x08 B Supervisor
EntryTable
DCD ZeroRtn
DCD ReadCRtn
DCD WriteIRtn
...
Zero EQU 0
ReadC EQU 256
WriteI EQU 512
; SWI 包含需要的例程在位 8-23 中和数据(如果有的话)在位 0-7 中。
; 假定 R13_svc 指向了一个合适的栈
STMFD R13, {r0-r2 , R14}
; 保存工作寄存器和返回地址。
LDR R0,[R14,#-4]
; 得到 SWI 指令。
BIC R0,R0, #0xFF000000
; 清除高端的 8 位。
MOV R1, R0, LSR #8
; 得到例程偏移量。
ADR R2, EntryTable
; 得到入口表(EntryTable)的开始地址。
LDR R15,[R2,R1,LSL #2]
; 分支到正确的例程
WriteIRtn
; 写 R0 中的位 0 - 7 中的字符。
.............
LDMFD R13, {r0-r2 , R15}^
; 恢复工作空间,并返回、恢复处理器模式和标志。
这就是 SWI 指令的基本处理步骤。
来源:
The ARM610 datasheet by Advanced Risc Machines
The ARM RISC Chip - A programmers guide by van Someren Atack published
by Addison Wesley
37PDF created with pdfFactory trial version
APCS 简介
(ARM 过程调用标准)
•
•
•
•
•
•
•
•
•
•
•
介绍
寄存器命名
设计关键
一致性
栈
回溯结构
实际参数
函数退出
建立栈回溯结构
APCS 标准
对编码有用的东西
介绍
APCS,ARM 过程调用标准(ARM Procedure Call Standard),提供了紧凑的编写例程的一种机制,定义的例程可以与其他例程交织在一起。最显著的一点是对这些例程来自哪里没有明确的限制。它们可以编译自 C、 Pascal、也可以是用汇编语言写成的。
APCS 定义了:
•
•
•
•
对寄存器使用的限制。
使用栈的惯例。
在函数调用之间传递/返回参数。
可以被‘回溯’的基于栈的结构的格式,用来提供从失败点到程序入口的函数(和给予的参数)的列表。
APCS 不一个单一的给定标准,而是一系列类似但在特定条件下有所区别的标准。例如,APCS-R (用于 RISC OS)规定在函数进入时设置的标志必须在函数退出时复位。在 32 位标准下,并不是总能知道进入标志的(没有 USR_CPSR),所以你不需要恢复它们。如你所预料的那样,在不同版本间没有相容性。希望恢复标志的代码在它们未被恢复的时候可能会表现失常...
如果你开发一个基于 ARM 的系统,不要求你去实现 APCS。但建议你实现它,因为它不难实现,且可以使你获得各种利益。但是,如果要写用来与编译后的 C 连接的汇编代码,则必须使用 APCS。编译器期望特定的条件,在你的加入(add-in)代码中必须得到满足。一个好例子是 APCS 定义 a1 到 a4 可以被破坏,而 v1
38PDF created with pdfFactory trial version
到 v6 必须被保护。现在我确信你正在挠头并自言自语“a 是什么? v 是什么?”。所以首先介绍 APCS-R 寄存器定义...
寄存器命名
APCS 对我们通常称为 R0 到 R14 的寄存器起了不同的名字。使用汇编器预处理器的功能,你可以定义 R0 等名字,但在你修改其他人写的代码的时候,最好还是学习使用 APCS 名字。
寄存器名字
意义
Reg # APCS
R0
R1
R2
R3
R4
R5
R6
R7
R8
R9
R10
R11
R12
R13
R14
R15
译注:ip 是指令指针的简写。
a1
a2
a3
a4
v1
v2
v3
v4
v5
v6
sl
fp
ip
sp
lr
pc
工作寄存器
"
"
"
必须保护
"
"
"
"
"
栈限制
桢指针
栈指针
连接寄存器
程序计数器
这些名字不是由标准的 Acorn 的 objasm(版本 2.00)所定义的,但是 objasm
的后来版本,和其他汇编器(比如 Nick Robert 的 ASM)定义了它们。要定义一个寄存器名字,典型的,你要在程序最开始的地方使用 RN 宏指令(directive):
a1 RN 0
a2 RN 1
a3 RN 2
...等...
39PDF created with pdfFactory trial version
r13 RN 13
sp RN 13
r14 RN 14
lr RN r14
pc RN 15
这个例子展示了一些重要的东西:
1. 寄存器可以定义多个名字 - 你可以定义‘r13’和‘sp’二者。
2. 寄存器可以定义自前面定义的寄存器 - ‘lr’定义自叫做‘r14’的寄存器。
(对于 objasm 是正确的,其他汇编器可能不是这样)
设计关键
•
•
•
•
函数调用应当快、小、和易于(由编译器来)优化。
函数应当可以妥善处理多个栈。
函数应当易于写可重入和可重定位的代码;主要通过把可写的数据与代码分离来实现。
但是最重要的是,它应当简单。这样汇编编程者可以非常容易的使用它的设施,而调试者能够非常容易的跟踪程序。
一致性
程序的遵循 APCS 的部分在调用外部函数时被称为“一致”。在程序执行期间的所有时候都遵循 APCS (典型的,由编译器生成的程序)被称为“严格一致”。协议指出,假如你遵守正确的进入和退出参数,你可以在你自己的函数范围内做你需要的任何事情,而仍然保持一致。这在有些时候是必须的,比如在写 SWI 伪装(veneers)的时候使用了许多给实际的 SWI 调用的寄存器。
栈
栈是链接起来的‘桢’的一个列表,通过一个叫做‘回溯结构’的东西来链接它们。这个结构存储在每个桢的高端。按递减地址次序分配栈的每一块。寄存器
sp 总是指向在最当前桢中最低的使用的地址。这符合传统上的满降序栈。在 APCS-R 中,寄存器
sl 持有一个栈限制,你递减
sp 不能低于它。在当前栈指针和当前栈之间,不应该有任何其他 APCS 函数所依赖的东西,在被调用的时候,函数可以为自己设置一个栈块。
40PDF created with pdfFactory trial version
可以有多个栈区(chunk)。它们可以位于内存中的任何地址,这里没有提供规范。典型的,在可重入方式下执行的时候,这将被用于为相同的代码提供多个栈;一个类比是 FileCore,它通过简单的设置‘状态’信息和并按要求调用相同部分的代码,来向当前可获得的 FileCore 文件系统(ADFS、RAMFS、IDEFS、SCSIFS 等)提供服务。
回溯结构
寄存器
fp (桢指针)应当是零或者是指向栈回溯结构的列表中的最后一个结构,提供了一种追溯程序的方式,来反向跟踪调用的函数。
回溯结构是:
地址高端
保存代码指针 [fp] fp 指向这里
返回 lr 值 [fp, #-4]
返回 sp 值 [fp, #-8]
返回 fp 值 [fp, #-12] 指向下一个结构
[保存的 sl]
[保存的 v6]
[保存的 v5]
[保存的 v4]
[保存的 v3]
[保存的 v2]
[保存的 v1]
[保存的 a4]
[保存的 a3]
[保存的 a2]
[保存的 a1]
[保存的 f7] 三个字
[保存的 f6] 三个字
[保存的 f5] 三个字
[保存的 f4] 三个字
地址低端
这个结构包含 4 至 27 个字,在方括号中的是可选的值。如果它们存在,则必须按给定的次序存在(例如,在内存中保存的 a3 下面可以是保存的 f4,但
a2-f5 则不能存在)。浮点值按‘内部格式’存储并占用三个字(12 字节)。
41PDF created with pdfFactory trial version
fp 寄存器指向当前执行的函数的栈回溯结构。返回 fp 值应当是零,或者是指向由调用了这个当前函数的函数建立的栈回溯结构的一个指针。而这个结构中的返回 fp 值是指向调用了调用了这个当前函数的函数的函数的栈回溯结构的一个指针;并以此类推直到第一个函数。
在函数退出的时候,把返回连接值、返回 sp 值、和返回 fp 值装载到 pc、sp、和 fp 中。
#include
void one(void);
void two(void);
void zero(void);
int main(void)
{
one();
return 0;
}
void one(void)
{
zero();
two();
return;
}
void two(void)
{
printf("won");
return;
}
void zero(void)
{
return;
}
当它在屏幕上输出消息的时候,
APCS 回溯结构将是:
fp ----> two_structure
return link
PDF created with pdfFactory trial version 42
return sp
return fp ----> one_structure
... return link
return sp
return fp ----> main_structure
... return link
return sp
return fp ----> 0
...
所以,我们可以检查 fp 并参看给函数‘two’的结构,它指向给函数‘one’的结构,它指向给‘main’的结构,它指向零来终结。在这种方式下,我们可以反向追溯整个程序并确定我们是如何到达当前的崩溃点的。值得指出‘zero’函数,因为它已经被执行并退出了,此时我们正在做它后面的打印,所以它曾经在回溯结构中,但现在不在了。值得指出的还有对于给定代码不太可能总是生成象上面那样的一个 APCS 结构。原因是不调用任何其他函数的函数不要求完全的 APCS 头部。
为了更细致的理解,下面是代码是 Norcroft C v4.00 为上述代码生成的...
AREA |C$$code|, CODE, READONLY
IMPORT |__main|
|x$codeseg|
B |__main|
DCB &6d,&61,&69,&6e
DCB &00,&00,&00,&00
DCD &ff000008
IMPORT |x$stack_overflow|
EXPORT one
EXPORT main
main
MOV ip, sp
STMFD sp!, {fp,ip,lr,pc}
SUB fp, ip, #4
CMPS sp, sl
BLLT |x$stack_overflow|
BL one
MOV a1, #0
LDMEA fp, {fp,sp,pc}^
DCB &6f,&6e,&65,&00
DCD &ff000004
43PDF created with pdfFactory trial version
EXPORT zero
EXPORT two
one
MOV ip, sp
STMFD sp!, {fp,ip,lr,pc}
SUB fp, ip, #4
CMPS sp, sl
BLLT |x$stack_overflow|
BL zero
LDMEA fp, {fp,sp,lr}
B two
IMPORT |_printf|
two
ADD a1, pc, #L000060-.-8
B |_printf|
L000060
DCB &6d,&61,&69,&6e
DCB &2e,&2e,&2e,&6f
DCB &6e,&65,&2e,&2e
DCB &2e,&74,&77,&6f
DCB &0a,&00,&00,&00
zero
MOVS pc, lr
AREA |C$$data|
|x$dataseg|
END
这个例子不遵从 32 为体系。APCS-32 规定只是简单的说明了标志不需要被保存。所以删除 LDM 的‘^’后缀,并在函数 zero 中删除 MOVS 的‘S’后缀。则代码就与遵从 32-bit 的编译器生成的一样了。
保存代码指针包含这条设置回溯结构的指令(STMFD ...)的地址再加上 12 字节。记住,对于 26-bit 代码,你需要去除其中的 PSR 来得到实际的代码地址。
现在我们查看刚进入函数的时候:
•
•
•
pc 总是包含下一个要被执行的指令的位置。
lr (总是)包含着退出时要装载到 pc 中的值。在 26-bit 位代码中它还包含着 PSR。
sp 指向当前的栈块(chunk)限制,或它的上面。这是用于复制临时数据、寄存器和类似的东西到其中的地方。在 RISC OS 下,你有可选择的至少 256 字节来扩展它。
44
PDF created with pdfFactory trial version
•
•
fp 要么是零,要么指向回溯结构的最当前的部分。
函数实参布置成(下面)描述的那样。
实际参数
APCS 没有定义记录、数组、和类似的格局。这样语言可以自由的定义如何进行这些活动。但是,如果你自己的实现实际上不符合 APCS 的精神,那么将不允许来自你的编译器的代码与来自其他编译器的代码连接在一起。典型的,使用 C 语言的惯例。
•
•
•
前 4 个整数实参(或者更少!)被装载到 a1 - a4。
前 4 个浮点实参(或者更少!)被装载到 f0 - f3。
其他任何实参(如果有的话)存储在内存中,用进入函数时紧接在 sp 的值上面的字来指向。换句话说,其余的参数被压入栈顶。所以要想简单。最好定义接受 4 个或更少的参数的函数。
函数退出
通过把返回连接值传送到程序计数器中来退出函数,并且:
•
•
•
•
如果函数返回一个小于等于一个字大小的值,则把这个值放置到 a1 中。
如果函数返回一个浮点值,则把它放入 f0 中。
sp、fp、sl、v1-v6、和 f4-f7 应当被恢复(如果被改动了)为包含在进入函数时它所持有的值。
我测试了故意的破坏寄存器,而结果是(经常在程序完全不同的部分)出现不希望的和奇异的故障。
ip、lr、a2-a4、f1-f3 和入栈的这些实参可以被破坏。
在 32 位模式下,不需要对 PSR 标志进行跨越函数调用的保护。在 26 位模式下必须这样,并通过传送 lr 到 pc 中(MOVS、或 LDMFD xxx^)来暗中恢复。必须从 lr 重新装载 N、Z、C 和 V,跨越函数保护这些标志不是足够的。
建立栈回溯结构
对于一个简单函数(固定个数的参数,不可重入),你可以用下列指令建立一个栈回溯结构:
function_name_label
MOV ip, sp
STMFD sp!, {fp,ip,lr,pc}
SUB fp, ip, #4
45PDF created with pdfFactory trial version
这个片段(来自上述编译后的程序)是最基本的形式。如果你要破坏其他不可破坏的寄存器,则你应该在这个 STMFD 指令中包含它们。
下一个任务是检查栈空间。如果不需要很多空间(小于 256 字节)则你可以使用:
CMPS sp, sl
BLLT |x$stack_overflow|
这是 C 版本 4.00 处理溢出的方式。在以后的版本中,你要调用
|__rt_stkovf_split_small|。
接着做你自己的事情...
通过下面的指令完成退出:
LDMEA fp, {fp,sp,pc}^
还有,如果你入栈了其他寄存器,则也在这里重新装载它们。选择这个简单的 LDM 退出机制的原因是它比分支到一个特殊的函数退出处理器(handler)更容易和更合理。
用在回溯中的对这个协议的一个扩展是把函数名字嵌入到代码中。紧靠在函数(和 MOV ip, sp)的前面的应该是:
DCD &ff0000xx
这里的‘xx’是函数名字符串的长度(包括填充和终结符)。这个字符串是字对齐、尾部填充的,并且应当被直接放置在 DCD &ff....的前面。
所以一个完整的栈回溯代码应当是:
DCB "my_function_name", 0, 0, 0, 0
DCD &ff000010
my_function_name
MOV ip, sp
STMFD sp!, {fp, ip, lr, pc}
SUB fp, ip, #4
CMPS sp, sl ; 如果你不使用栈
BLLT |x$stack_overflow| ; 则可以省略
...处理...
LDMEA fp, {fp, sp, pc}^
要使它遵从 32-bit 体系,只须简单的省略最后一个指令的‘^’。注意你不能在一个编译的
26-bit 代码中使用这个代码。实际上,你可以去除它,但这不是我愿意打赌的事情。
46PDF created with pdfFactory trial version
如果你不使用栈,并且你不需要保存任何寄存器,并且你不调用任何东西,则没有必要设置 APCS 块(但在调试阶段对跟踪问题仍是有用的)。在这种情况下你可以:
my_simple_function
...处理...
MOVS pc, lr
(再次,对 32 位 APCS 使用 MOV 而不是 MOVS,但是不要冒险与 26 位代码连接)。
APCS 标准
总的来说,有多个版本的 APCS (实际上是 16 个)。我们只关心在 RISC OS 上可能遇到的。
APCS-A
就是 APCS-Arthur;由早期的 Arthur 所定义。它已经被废弃,原因是它有不同的寄存器定义(对于熟练的 RISC OS 程序员它是某种异类)。它用于在 USR 模式下运行的 Arthur 应用程序。不应该使用它。
sl = R13, fp = R10, ip = R11, sp = R12, lr = R14, pc = R15。
•
PRM (p4-411) 中说“用
r12 作为
sp,而不是在体系上更自然的
r13,是历史性的•
•
•
•
•
并先于 Arthur 和 RISC OS 二者。”
栈是分段的并可按需要来扩展。
26-bit 程序计数器。
不在 FP 寄存器中传递浮点实参。
不可重入。标志必须被恢复。
APCS-R
就是 APCS-RISC OS。用于 RISC OS 应用程序在 USR 模式下进行操作;或在 SVC
模式下的模块/处理程序。
•
•
•
•
•
•
sl = R10, fp = R11, ip = R12, sp = R13, lr = R14, pc = R15。
它是唯一的最通用的 APCS 版本。因为所有编译的 C 程序都使用 APCS-R。
显式的栈限制检查。
26-bit 程序计数器。
不在 FP 寄存器中传递浮点实参。
不可重入。标志必须被恢复。
APCS-U
就是 APCS-Unix,Acorn 的 RISCiX 使用它。它用于 RISCiX 应用程序(USR 模式)或内核(SVC 模式)。
47PDF created with pdfFactory trial version
•
•
•
•
•
sl = R10, fp = R11, ip = R12, sp = R13, lr = R14, pc = R15。
隐式的栈限制检查(使用 sl)。
26-bit 程序计数器。
不在 FP 寄存器中传递浮点实参。
不可重入。标志必须被恢复。
APCS-32
它是 APCS-2(-R 和 -U)的一个扩展,允许 32-bit 程序计数器,并且从执行在
USR 模式下的一个函数中退出时,允许标志不被恢复。其他事情同于 APCS-R。
Acorn C 版本 5 支持生成 32-bit 代码;在用于广域调试的 32 位工具中,它是最完整的开发发行。一个简单的测试是要求你的编译器导出汇编源码(而不是制作目标代码)。你不应该找到:
MOVS PC, R14
或者
LDMFD R13!, {Rx-x, PC}^
对编码有用的东西
首先要考虑的是该死的 26/32 位问题。 简单的说,不转弯抹角绝对没有方法为两个版本的
APCS 汇编同一个通用代码。但是幸运的这不是问题。APCS 标准不会突然改变。RISC OS
的 32 位版本也不会立刻变异。所以利用这些,我们可以设计一种支持两种版本的方案。这将远远超出 APCS,对于 RISC OS 的 32 位版本你需要使用 MSR 来处理状态和模式位,而不是使用 TEQP。许多现存的 API 实际上不需要保护标志位。所以在我们的 32 版本中可以通过把
MOVS PC,... 变成
MOV PC,...,和把
LDM {...}^ 变成
LDM {...},并重新建造来解决。objasm 汇编器(v3.00 和以后)有一个
{CONFIG} 变量可以是
26 或
32。可以使用它建造宏...
my_function_name
MOV ip, sp
STMFD sp!, {fp, ip, lr, pc}
SUB fp, ip, #4
...处理...
[ {CONFIG} = 26
LDMEA fp, {fp, sp, pc}^
|
LDMEA fp, {fp, sp, pc}
]
我未测试这个代码。它(或类似的东西)好象是保持与两个版本的 APCS 相兼容的最佳方式,也是对 RISC OS 的不同版本,26 位版本和将来的 32 位版本的最佳方法。
48PDF created with pdfFactory trial version
测试是否处于 32 位? 如果你要求你的代码有适应性,有一个最简单的方法来确定处理器的 PC 状态:
TEQ PC, PC ; 对于 32 位是 EQ;对于 26 位是 NE
使用它你可以确定:
•
•
•
26 位 PC,可能是 APCS-R 或 APCS-32。
32 位 PC,不能 APCS-R。所有 26-bit 代码(TEQP 等)面临着失败!
32 位规则
•
•
不要使用有 P 后缀的测试指令: TEQP、TSTP、CMPP、CMNP。
检查更改 PC、R14 的如 BIC 和 ORR 这样的指令,和把一个寄存器复制到 PC 的指令。例如,ORRS PC, R14, #1<<28 将不能工作。实际上,不要使用带 S 标志设置的写到 PC 的任何指令。
在数据处理操作中不要使用 R15 (PC) 作为移位寄存器。
在 LDR/STR 中,不要使用 PC 作为寄存器偏移量并不要写回到它。
在过后变址 LDR/STR 中,Rm(变址)和 Rn(基址)不能是同一个寄存器。类似的,对于涉及写回的任何指令,Rm 和 Rn 都应该是不同的寄存器。
LDM/STM 在用户模式下不使用 S 位。这意味着,不要使用‘^’后缀(例如
LDMFD
R13!, {PC}^)。
BL 不保存状态寄存器。这必须显式的进行,但是这样的代码将不能在 ARM2 或
ARM3 上运行,因为它们不支持 MRS/MSR 指令。
还要注意,不可能见到被调用者的标志,所以不应该恢复你不知道其状态的标志。你能做的最好的就是在进入的时候保护标志。
•
•
•
•
•
•
任何带 S 位设置的到 R15 的 32-bit 写(MOVS、ORRS、TEQP、^) 将传送当前模式的 SPSR 到 CPSR 中。例如,假定我们在 irq_32 模式下:
MOVS PC, R14
将复制 R14 到 PC,并接着复制 SPSR_IRQ32 到 CPSR。
这在 USR 模式下不是非常有用因为它没有 SPSR!
浮点指令
49PDF created with pdfFactory trial version