冰凌汇编

 找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 145|回复: 0
收起左侧

[原创] 介绍 ARM 汇编语言

[复制链接]
qingjue 发表于 2022-5-25 23:25:50

作者:Carl Burch,亨德里克斯学院,2011 年 10 月

通过介绍 ARM 汇编语言 卡尔·伯奇 获得许可 知识共享署名-相同方式共享 3.0 美国许可. 基于在 www.cburch.com/books/arm/ .

1. 背景

我们实际上关心两种语言,汇编语言和机器语言。

1.1 定义

机器语言将指令编码为 0 和 1 的序列; 这种二进制编码是计算机的处理器被构建来执行的。 但是,使用这种编码编写程序对于人类程序员来说是笨拙的。 因此,当程序员想要指示计算机要执行的精确指令时,他们会使用 汇编语言 ,它允许以文本形式编写指令。 汇编器将包含汇编语言代码的文件翻译成相应的机器语言。

让我们看一个 ARM 设计的简单示例。 这是机器语言指令:

1110 0001 1010 0000 0011 0000 0000 1001

当处理器被告知执行该二进制序列时,它会将值从“寄存器 9”复制到“寄存器 3”。 但作为程序员,您几乎不想阅读长二进制序列并理解它。 相反,程序员更喜欢用汇编语言进行编程,我们将使用以下行来表达这一点。

MOV R3, R9

然后程序员将使用汇编程序将其转换为计算机实际执行的二进制编码。

但不只是一种机器语言:为每条处理器设计了不同的机器语言,旨在提供一组强大的快速指令,同时允许构建相对简单的电路。 处理器通常被设计为与以前的处理器兼容,因此它遵循相同的机器语言设计。 例如,英特尔的处理器系列(包括 80386、奔腾和酷睿 i7)支持类似的机器语言。 但是 ARM 处理器支持完全不同的机器语言。 机器语言编码的设计称为指令集架构ISA)。

并且对于每种机器语言,必须有不同的汇编语言,因为汇编语言必须对应于一组完全不同的机器语言指令。

1.2 ISA品种

在众多 ISA(指令集架构)中,x86 是最广为人知的。它最初由 Intel 于 1974 年设计用于 8 位处理器(Intel 8080),多年来它扩展到 16 位形式(1978,Intel 8086),然后扩展到 32 位形式(1985,Intel 80386),然后是 64 位形式(2003,AMD Opteron)。今天,支持 IA32 的处理器现在由 Intel、AMD 和 VIA 制造,并且可以在大多数个人计算机中找到。

今天另一个著名的 ISA 是 PowerPC。 Apple 的 Macintosh 计算机一直使用这些处理器,直到 2006 年 Apple 将其计算机切换到 x86 系列处理器。但 PowerPC 仍然普遍用于汽车和游戏机(包括 Wii、Playstation 3 和 XBox 360)等应用程序。

但我们将研究的 ISA 来自一家名为 ARM 的公司。 (与其他成功的 ISA 一样,ARM 的 ISA 多年来一直在发展。我们将研究 4T 版本。)支持 ARM ISA 的处理器分布相当广泛,通常用于手机、数字音乐播放器和手持游戏系统等低功耗设备。 iPhone、Kindle 和 Nintendo DS 都是采用 ARM 处理器的设备的突出例子。

检查 ARM 的 ISA 而不是 IA32 有几个原因。

  • 汇编语言编程很少用于功能更强大的计算系统,因为用高级编程语言进行编程要容易得多。但是对于小型设备,汇编语言编程仍然很重要:由于功率和价格的限制,设备资源很少,开发人员可以使用汇编语言尽可能高效地使用这些资源。
  • IA32 架构的多重扩展导致它过于复杂,以至于我们无法真正彻底理解。
  • IA32 可以追溯到 1970 年代,那是一个完全不同的计算时代。 ARM 更能代表更现代的 ISA 设计。

2. ARM 汇编基础

我们现在将转向检查 ARM 的 ISA。

2.1 一个简单的程序:添加数字

让我们从一个简单的例子开始我们的介绍。想象一下,我们想要将 1 到 10 的数字相加。我们可以在 C 语言中这样做,如下所示。

    int total;
    int i;

    total = 0;
    for (i = 10; i > 0; i--) {
        total += i;
    }

下面将其翻译成 ARM 的 ISA 支持的指令。

        MOV  R0, #0         ; R0 累计总数
        MOV  R1, #10        ; R1 从 10 倒数到 1
again   ADD  R0, R0, R1
        SUBS R1, R1, #1
        BNE  again
halt    B    halt           ; 无限循环停止计算

您会注意到在汇编语言程序中提到了R0R1。这些是对 registers 的引用,它们位于处理器中,用于在计算期间存储数据。 ARM 处理器包括 16 个易于访问的寄存器,编号为 R0R15。每个都存储一个 32 位数字。请注意,尽管寄存器存储数据,但它们与 memory 的概念非常不同:内存通常更大(千字节或通常千兆字节),因此它通常存在于处理器之外。由于内存的大小,访问内存比访问寄存器需要更多的时间——通常大约是 10 倍。因此,汇编语言编程倾向于尽可能关注使用寄存器。

因为汇编语言程序的每一行都直接对应于机器语言,所以这些行的格式受到严格限制。可以看到每一行由两部分组成:第一是操作码,例如MOV,是表示操作类型的缩写;然后是诸如“R0, #0”之类的参数。每个操作码对允许的参数都有严格的要求。例如,MOV 指令必须正好有两个参数:第一个必须标识一个寄存器,第二个必须提供一个寄存器或一个常量(以“#”为前缀)。直接放在指令中的常量称为 immediate ,因为处理器在读取指令时可以立即使用它。

在上述汇编语言程序中,我们首先使用MOV指令将R0初始化为0,R1初始化为10。 ADD 指令计算 R0R1(第二个和第三个参数)之和,并将结果放入 R0(第一个参数);这对应于等效 C 程序的 total += i; 行。随后的 SUBS 指令将 R1 减 1。

要理解下一条指令,我们需要了解除了寄存器R0R15之外,ARM处理器还包含一组四个“标志, ”标记了零标志 (Z)、负标志 (N)、进位标志 (C) 和溢出标志 (V)。每当算术指令的末尾有一个 S 时,就像 SUBS 一样,这些标志将根据计算结果进行更新。在这种情况下,如果将R1减1的结果为0,则Z标志将变为1; N、C 和 V 标志也更新了,但它们与我们对此代码的讨论无关。

以下指令 BNE 将检查 Z 标志。如果未设置 Z 标志(即,前一个减法给出非零结果),则 BNE 安排处理器,以便执行的下一条指令是 ADD指令,标记为再次;这会导致以较小的 R1 值重复循环。如果设置了 Z 标志,处理器将继续执行下一条指令。 (BNE 代表 Branch if Not Equal。这个名字来自于我们想检查两个数字是否相等的想象。使用 ARM 的 ISA 执行此操作的一种方法是首先告诉处理器减去这两个数字;如果差为零,则两个数字必须相等,并且零标志将为 1。它们的结果为零,这将设置零标志。)

最后一条指令B总是分支回到指定的指令。在这个程序中,指令为自己命名,通过使计算机进入一个紧密的无限循环来有效地停止程序。

2.2 另一个例子:冰雹序列

现在,让我们考虑 hailstone sequence 。给定一个整数 n,我们反复想应用以下过程。

iters ← 0
while n ≠ 1:
iters ← iters + 1
if n is odd:
n ← 3 ⋅ n + 1
else:
n ← n / 2

例如,如果我们从 3 开始,那么因为这是奇数,所以我们的下一个数字是 3 ⋅ 3 + 1 = 10。这是偶数,所以我们的下一个数字是 10 / 2 = 5。这是奇数,所以我们的下一个数字是3 ⋅ 5 + 1 = 16。这是偶数,所以我们转到 8,它仍然是偶数,所以我们转到 4,然后是 2 和 1。

在将其翻译成 ARM 的汇编语言时,我们必须面对一个事实,即 ARM 缺少任何与除法相关的指令。 (设计人员认为除法很少需要在它所需的复杂电路上浪费晶体管。)幸运的是,该算法中的除法相对简单:我们只需将 n 除以 2,这可以通过右移来完成。

ARM 有一种不同寻常的移位方法:我们已经看到,每条基本算术指令,最后的参数都可以是一个常量(如 SUBS R1、R1、#1)或寄存器(如 ADD、R0、R0、R1)。但是当最后一个参数是一个寄存器时,我们可以选择添加一个移位距离:例如,指令“ADD R0, R0, R1,LSL #1”。表示在将 R1 添加到 R0 之前添加左移版本的 R1(而 R1 本身保持不变)。 ARM 指令集支持四种类型的移位:

LSL 逻辑左移
LSR 逻辑右移
ASR 算术右移
ROR 右旋

移位距离可以是 1 到 32 之间的立即数,也可以基于寄存器值:“MOV R0, R1, ASR R2”相当于“R0 = R1 >> R2”。

在将我们的伪代码翻译成汇编语言时,我们会发现移位操作对于将 n 乘以 3(计算为 n + (n « 1))和除以 n (计算为 n » 1)都很有用。 我们还需要处理测试 n 是否为奇数。 我们可以通过测试 n 的 1 位是否被设置来做到这一点,我们可以使用 ANDS 指令对 1 执行按位与来完成。ANDS 指令 根据结果是否为 0 设置 Z 标志。如果结果为 0,则表示 n 的 1 位为 0,因此 n 为偶数。

        MOV  R0, #5         ; R0 是当前编号
        MOV  R1, #0         ; R1 是迭代次数的计数
again   ADD  R1, R1, #1     ; 增加迭代次数
        ANDS R0, R0, #1     ;测试R0是否为奇数
        BEQ  even
        ADD  R0, R0, R0, LSL #1 ; 如果奇数,设置 R0 = R0 + (R0 << 1) + 1
        ADD  R0, R0, #1     ; 并重复(保证 R0 > 1)
        B    again
even    MOV  R0, R0, ASR #1 ; 如果是偶数,则设置 R0 = R0 >> 1
        SUBS R7, R0, #1     ; 如果 R0 != 1 则重复
        BNE  again
halt    B    halt           ;无限循环停止计算

2.3 另一个例子:添加数字

让我们看另一个例子。 在这里,假设我们要添加一个正数的数字; 例如,给定数字 1,024,我们想要计算 1 + 0 + 2 + 4,即 7。用 C 语言表达这一点的明显方法如下。

    total = 0;
    while (i > 0) {
        total += i % 10;
        i /= 10;
    }

但是,很难将其转换为 ARM 的 ISA,因为 ARM 没有任何除法指令。 但是,我们可以使用一个巧妙的技巧来使用乘法来执行这种除法:如果我们将一个数字乘以 232 / 10,则乘积的高 32 位告诉我们将原始数字除以 10 的结果。这种见解导致 以下替代方法对数字中的数字求和。

    base = 0x1999999A;
    total = 0;
    while (i > 0) {
        iDiv10 = (i * base) >> 32;
        total += i - iDiv10 * 10;
        i = iDiv10;
    }

在将其翻译成汇编代码时,我们必须面对两个问题。 更明显的是确定使用哪个指令来执行乘法。 在这里,我们要使用 UMULL 指令(Unsigned MULtiply Long),它将两个寄存器解释为无符号的 32 位数字,并将寄存器值的 64 位乘积放入两个不同的 寄存器。 下面的例子说明了。

UMULL R4, R5, R0, R2  ; 计算 R0 * R2,将低 32 位放在 R4 中,将高 32 位放在 R5 中

我们必须面对的不太明显的问题是将 0x1999999A 放入寄存器中。 一开始你可能会想使用MOV,但是这条指令有一个主要的限制:任何立即数都必须循环偶数位才能达到八位值。 对于 0 到 255 之间的数字,这不是问题; 对于 1,024 也不是问题,因为 0x400 可以通过将 1 向左旋转 12 位来实现。 但是对于 0x1999999A 没有办法做到这一点。 我们将使用的解决方案是分别加载每个字节,使用 ORR 指令将它们连接起来,该指令计算两个值的按位或。

        MOV R0, #1024          ; R0 为输入,按 10 倍递减
        MOV R1, #0             ; R1 是数字的总和
        MOV R2, #0x19000000    ; R2 一直是 0x1999999A
        ORR R2, R2, #0x00990000
        ORR R2, R2, #0x00009900
        ORR R2, R2, #0x0000009A
        MOV R3, #10            ; R3 始终为 10
loop    UMULL R4, R5, R0, R2   ; R5 为 R0 / 10
        UMULL R4, R6, R5, R3   ; R4 现在是 10 * (R0 / 10)
        SUB R4, R0, R4         ; R5 现在是 R0 的一位数
        ADD R1, R1, R4         ; 将其添加到 R1
        MOVS R0, R5
        BNE loop
halt    B halt

顺便说一句,您有时可能希望将一个小的负数(如 -10)放入寄存器中。你不能使用 MOV 来完成这个,因为它的二进制补码表示是 0xFFFFFFF6,不能旋转成 8 位数字。如果碰巧知道某个寄存器包含数字 0,那么您可以使用 SUB。但如果不是,那么 MVN (MoVe Not) 指令很有用:它将其参数的按位 NOT 放入目标寄存器。所以要让-10进入R0,我们可以使用“MVN R0, #0x9” .

2.4 到目前为止的说明摘要

ARM 包括 16 条“基本”算术指令,编号从 0 到 15。下面列出了所有 16 条指令,其功能由相关的 C 运算符总结。 (每行开头的数字用于将指令翻译成机器语言。程序员没有理由记住这种对应关系:毕竟,这就是我们有汇编程序的原因。)

Figure 1: ARM's basic arithmetic instructions
介绍 ARM 汇编语言 - qingjue_冰凌汇编

除了TSTTEQCMPCMN,所有指令都可以有一个 S 后缀到操作码,表示操作应该设置标志。对于TSTTEQCMPCMN,S是隐含的:指令不会更改任何通用寄存器,因此执行指令的唯一要点是设置标志。

我们还看到了上述基本算术指令中没有的其他三个操作码:UMULL 是“非基本”算术指令,BBNE 不是算术指令。

2.5 条件代码

每条 ARM 指令都可以包含一个条件代码,指定仅当某些标志组合成立时才应进行操作。您可以通过将条件代码包含在操作码中来指定条件代码。它通常出现在操作码的末尾,但它在基本算术指令上的可选 S 之前。条件代码的名称是基于假设标志是基于 CMPSUBS 指令设置的。

Figure 2: ARM's condition codes
介绍 ARM 汇编语言 - qingjue_冰凌汇编

到目前为止,我们看到的这个条件代码的唯一实例是 BNE 指令:在这种情况下,我们有一个用于分支的 B 指令,但是分支 仅当 Z 标志为 0 时才会发生。

但是 ARM 的 ISA 也允许我们将条件代码应用于其他操作码。 例如,ADDEQ 表示如果 Z 标志为 1,则执行加法。在非分支指令上使用条件代码的一个常见场景是使用 Euclid 的 GCD 算法计算两个数的最大公约数 .

    a = 40;
    b = 25;
    while (a != b) {
        if (a > b) a -= b;
        else       b -= a;
    }

传统的汇编语言翻译只会在分支指令上使用条件代码。

        MOV R0, #40      ; R0 is a
        MOV R1, #25      ; R1 is b
again   CMP R0, R1
        BEQ halt
        BLT isLess
        SUB R0, R0, R1
        B again
isLess  SUB R1, R1, R0
        B again
halt    B halt

但是,以下是更短且更有效的翻译。

        MOV R0, #40      ; R0 is a
        MOV R1, #25      ; R1 is b
again   CMP R0, R1
        SUBGT R0, R0, R1
        SUBLT R1, R1, R0
        BNE again
halt    B halt

由于两个原因,这更有效。更明显的是,每次迭代执行的指令数量更少(四个对五个)。但另一个原因来自现代处理器在执行当前指令时“预取”下一条指令的事实。但是,由于无法确定下一条指令的位置,因此分支会中断此过程。第二次翻译涉及更少的分支指令,因此预取指令的问题更少。

3. 内存

我们已经了解了如何构建执行基本数值计算的汇编程序。我们现在将转向检查汇编程序如何访问内存。

3.1基本内存指令

ARM 支持通过两条指令访问内存,LDRSTRLDR 指令从内存中加载数据,而STR 将数据存储到内存中。每个都有两个参数。第一个参数是数据寄存器:对于一个LDR指令,加载的数据放在这个寄存器中;对于 STR 指令,在该寄存器中找到的数据存储到内存中。第二个参数表示包含正在访问的内存地址的寄存器;它将使用括号中的寄存器名称写入。 (在 Section 3.2 中,我们将看到如何编写第二个参数还有其他选择。)

有关这些指令如何工作的示例,假设我们需要一个汇编程序片段,它将整数添加到数组中。我们假设 R0 保存数组的第一个整数的地址,而 R1 保存数组中整数的个数。

addInts MOV R4, #0
addLoop LDR R2, [R0]
        ADD R4, R4, R2
        ADD R0, R0, #4
        SUBS R1, R1, #1
        BNE addLoop

在这个片段中,我们使用 R4 来保存到目前为止的整数之和。在 LDR 指令中,我们在 R0 中查找内存地址,并将在该地址找到的数据加载到 R2 .然后我们将此值添加到R4。然后,我们移动R0,使其包含数组中下一个整数的内存地址;我们将R0增加四,因为每个整数消耗四个字节的内存。最后,我们递减 R1,这是要从数组中读取的整数个数,如果还有整数,我们就重复这个过程。

LDRSTR 都加载和存储 32 位值。还有使用 8 位值的说明,LDRBSTRB;这些主要用于处理字符串。下面是 C 的 strcpy 函数的实现;我们假设 R0 保存目标数组的第一个字符的地址,而 R1 保存源字符串的第一个字符的地址。我们希望继续复制,直到我们复制终止 NUL 字符(ASCII 0)。

strcpy  LDRB R2, [R1]
        STRB R2, [R0]
        ADD R0, R0, #1
        ADD R1, R1, #1
        TST R2, R2      ; 如果 R2 非零则重复
        BNE strcpy

3.2 寻址模式

上一节 示例中,我们通过将寄存器名称括在括号中来提供地址。但是 ARM 也允许使用其他几种方式来指示内存地址。每一种这样的技术都被称为寻址模式;简单地命名保存内存地址的寄存器的技术就是一种这样的寻址模式,称为寄存器寻址,但还有其他的。

其中之一是 scaled register offset ,我们在括号中包含一个寄存器、另一个寄存器和一个移位值。为了计算要访问的内存地址,处理器获取第一个寄存器,并将根据移位值移位的第二个寄存器添加到它。 (括号中提到的寄存器都不会改变值。)当访问知道数组索引的数组时,这种寻址模式很有用。我们可以修改之前的例程,将整数添加到数组中,以利用这种寻址模式。

addInts MOV R4, #0
addLoop SUBS R1, R1, #1
        LDR R2, [R0, R1, LSL #2]
        ADD R4, R4, R2
        BNE addLoop

对于循环的每次迭代,我们首先减少循环索引R1。然后我们使用缩放的寄存器偏移量检索数组条目处的元素:我们使用 R0 作为我们的基础,并添加向左移动的 R1两个地方。我们将 R1 左移两位,以便 R1 乘以 4;毕竟,数组中的每个整数都是四个字节长。将加载的值添加到 R4 中,累加总数后,如果 R1 尚未达到 0,我们重复循环。

除了使用不同的寻址模式之外,这个版本的代码在三个方面与我们的原始实现略有不同。首先,它以相反的顺序加载数组中的数字——也就是说,它首先加载数组中的最后一个数字。其次,R0 在片段的过程中保持不变。最后,它会更快一些,因为它在每次循环迭代中减少了一条指令。

立即后索引寻址是另一种寻址模式。为了在汇编语言中表示这种模式,我们在括号后面加上逗号和正或负立即数。在执行指令时,处理器仍然会访问在寄存器中找到的内存地址,但在访问内存后,地址寄存器会根据立即数增加或减少。

我们的 strcpy 实现是一个有用的示例,其中立即后索引寻址很有用:在我们存储到 R0 之后,我们想要 R0 为下一次迭代增加 1;同样,在我们从 R1 加载之后,我们希望 R1 增加 1。我们可以使用立即后索引寻址来避免两个 添加我们早期版本的说明。

strcpy  LDRB R2, [R1], #1
        STRB R2, [R0], #1
        TST R2, R2      ; 如果 R2 非零则重复
        BNE strcpy

ARM 处理器总共支持十种寻址模式。

[Rn, #±imm] 立即偏移
访问的地址比在 Rn 中找到的地址多/少。 Rn 不变。
[Rn] 寄存器
访问的地址是在 Rn 中找到的值。 这只是 [Rn, #0] 的简写。
[Rn, ±Rm, shift] 缩放的寄存器偏移
访问的地址是 Rn 中的值与 Rm 中的值按指定移位的和/差。 Rn 和 Rm 不改变值。
[Rn, ±Rm] 寄存器偏移
访问的地址是 Rn 中的值与 Rm 中的值的和/差。 Rn 和 Rm 不改变值。 这只是 [Rn, ±Rm, LSL #0] 的简写。
[Rn, #±imm]! 立即预索引
访问的地址与立即偏移模式一样,但 Rn 的值会更新为访问的地址。
[Rn, ±Rm, shift]! 缩放寄存器预索引
访问地址与缩放寄存器偏移模式一样,但 Rn 的值会更新为访问的地址。
[Rn, ±Rm]! 注册预索引
地址访问与寄存器偏移模式相同,但 Rn 的值更新为访问的地址。
[Rn], #±imm 立即后索引  
访问的地址是在 Rn 中找到的值,然后 Rn 的值通过 imm 增加/减少。
[Rn], ±Rm, shift 缩放寄存器后索引
访问的地址是在 Rn 中找到的值,然后 Rn 的值根据移位增加/减少 Rm 移位。
[Rn], ±Rm 注册后索引
访问的地址是在 Rn 中找到的值,然后将 Rn 的值增加/减少 Rm。 这只是 [Rn]、±Rm、LSL #0 的简写。

对于那些涉及移位的寻址模式,移位技术与算术指令(LSL、LSR、ASR、ROR、RRX)一样。 但移位距离不能根据寄存器:距离必须是立即数。

3.3 初始化内存

我们经常希望保留内存来保存程序中的数据。 为了做到这一点,我们使用 directives :汇编器做一些事情的指令,而不是简单地将汇编语言指令翻译成相应的机器代码。 一个有用的指令是DCD,它将一个或多个32位数值插入机器代码输出。 (DCD 神秘地代表定义常量双字。)

primes  DCD   2, 3, 5, 7, 11, 13, 17, 19

在此示例中,我们创建了标签“primes”,它将对应于 2 放入内存的地址。 在接下来的四个字节中放置整数 3,然后是 5,依此类推。

在我们的程序中,我们希望将数组的地址加载到寄存器中; 为此,我们将 primes 添加到程序计数器 PC(与 R15 同义)。 下面的片段将第五个素数 (11) 加载到 R1 中。

ADD R0, PC, #primes  ; 将 primes[0] 的地址加载到 R0
        LDR R1, [R0, #16]    ; 将素数 [4] 加载到 R1

另一个值得一提的指令是DCB,用于将字节加载到内存中。 因此,我们可以编写以下内容。

primes  DCB   2, 3, 5, 7, 11, 13, 17, 19

但是,我们只为每个数字使用一个字节,因此我们只能包含介于 -128 和 127 之间的数字。我们还可以在列表中包含一个字符串; 字符串的每个字符将占用一个字节的内存。

greet   DCB   "hello world\n", 0

请注意我们如何在字符串之后包含 0。 没有这个,字符串不会被 NUL 字符终止。

这里还有一个值得注意的指令是百分号%。 当您希望保留一块内存但您不关心内存的初始值时,这很有用。

array   % 120  ; 预留 120 字节内存,可容纳 30 个整数

3.4 多寄存器内存指令

ARM ISA 还包括允许在同一指令中加载或存储多个值的指令。 LDMIA 指令就是这样一条指令:它允许从另一个寄存器中指定的地址开始加载到多个寄存器中。 在下面的使用示例中,我们将代码用于添加数组的整数,并使用 LDMIA 对其进行修改,以便在循环的每次迭代中处理四个整数。 这种策略允许程序使用更少的指令运行,但代价是更高的复杂性。

; R0 保存数组中第一个整数的地址
; R1 保存数组的长度; 只有当长度是 4 的倍数时,片段才有效
addInts MOV R4, #0
addLoop LDMIA R0!, { R5-R8 }
        ADD R5, R5, R6
        ADD R7, R7, R8
        ADD R4, R4, R5
        ADD R4, R4, R7
        SUBS R1, R1, #4
        BNE addLoop

在执行上面的 LDMIA 指令时,ARM 处理器会在 R0 寄存器中查找地址。它将从该地址开始的四个字节加载到R5,接下来的四个字节加载到R6,下一个加载到R7四个字节,然后进入R8接下来的四个字节。同时,R0 前进了 16 个字节,因此在下一次迭代中,LDMIA 指令会将接下来的四个字加载到寄存器中。

大括号内可以是任何寄存器列表,使用破折号表示寄存器范围,并使用逗号分隔范围。因此,指令 LDMIA R0!, { R1-R4, R8 , R11-R12 } 将从内存中加载七个单词。寄存器列出的顺序并不重要;即使我们写 LDMIA R0!, { R11-R12, R8 , R1-R4 }, R1 将接收从内存中加载的第一个单词。

在我们的例子中,R0 后面的感叹号可以省略;如果省略,则地址寄存器不会被指令更改。也就是说,R0 将继续指向数组中的第一个整数。在上面的示例中,我们希望 R0 发生变化,使其指向下一次迭代的下一个四个整数块,因此我们包含了感叹号。

另一条指令是STMIA,它将几个寄存器存储到内存中。在下面的示例中,我们将数组中的每个数字移动到下一个位置;因此,数组 <2,3,5,7> 变为 <0,2,3,5>。

; R0 保存数组中第一个整数的地址
; R1 保存数组的长度; 只有当长度是 4 的倍数时,片段才有效
shift   MOV R4, #0
shLoop  LDMIA R0, { R5-R8 }
        STMIA R0!, { R4-R7 }
        MOV R4, R8
        SUBS R1, R1, #4
        BNE shLoop

注意 LDMIA 指令是如何省略感叹号的,因此 R0 不会被修改。 这是为了让 STMIA 存储到刚刚加载到寄存器中的相同地址范围内。 STMIA 指令带有感叹号,因为必须修改R0 以准备循环的下一次迭代。

ARM 处理器包括多重加载和多重存储指令的四种变体; LDMSTM 缩写必须始终表示这四种变体之一。

LDMIA, STMIA 之后增加
我们从命名地址开始加载,并不断增加地址。
LDMIB, STMIB 之前的增量
我们开始从比命名地址多四个的地址开始加载,并不断增加地址。
LDMDA, STMDA 减量后
我们从命名地址开始加载并进入递减地址。
LDMDB, STMDB 减量前
我们从比指定地址少四个开始加载到递减地址。

在所有四种模式中,编号最高的寄存器始终对应于内存中的最高地址。 因此,指令 LDMDA R0, { R1-R4 } 会将 R4 放入由 R0 命名的地址,将 R3 放入 R0 - 4,依此类推。

正如我们将在研究子程序时看到的那样,当我们想将一块未使用的内存用作堆栈时,不同的变体特别有用。

冰凌汇编免责声明
以上内容均来自网友转发或原创,如存在侵权请发送到站方邮件9003554@qq.com处理。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|小黑屋|站点统计|Archiver|小黑屋|RSS|冰凌汇编 ( 滇ICP备2022002049号 滇公网安备 53032102000029号)|网站地图

GMT+8, 2022-10-2 16:44 , Processed in 0.142379 second(s), 7 queries , Redis On.

冰凌汇编 - 建立于2021年12月20日

Powered by Discuz! © 2001-2022 Comsenz Inc.

快速回复 返回顶部 返回列表