【ARM中文手册】第四章 Introduction to Assembly Language

文摘   2024-11-07 17:37   江苏  

4.Introduction to Assembly Language


汇编语言是一种低级编程语言。通常来说,汇编语言指令(助记符)与核心实际执行的二进制操作码之间存在一一对应的关系。

许多编写应用层代码的程序员通常不会使用汇编语言进行编码。然而,汇编代码的知识在某些情况下非常有用,例如在需要高度优化的代码时、编写编译器时,或者在需要使用C语言无法直接实现的底层特性时。汇编语言可能在引导代码、设备驱动程序或操作系统开发的某些部分中需要使用。最后,当调试C程序时,能够阅读反汇编代码并理解汇编指令与C语句之间的映射关系也是非常有帮助的。

想要获得更详细的ARM汇编语言描述的程序员还可以参考ARM编译器工具链汇编器参考手册或ARM架构参考手册。

4.1 Comparison with other assembly languages

ARM处理器是一种精简指令集计算机(RISC)处理器。复杂指令集计算机(CISC)处理器,如x86,拥有丰富的指令集,可以通过单条指令执行复杂操作。这些处理器通常具有大量内部逻辑,用于将机器指令解码为内部操作序列(微代码)。相较之下,RISC架构的指令数量较少,但指令的通用性较强,通常可以用显著更少的晶体管执行,从而使硅片更便宜且更节能。像其他RISC架构一样,ARM核心有大量通用寄存器,许多指令可以在单个周期内执行。它具有简单的寻址模式,所有加载/存储地址都可以从寄存器内容和指令字段中确定。

ARMv7架构具有基本的数据处理指令,允许核心执行算术运算(如ADD)和逻辑位操作(如AND)。这些指令还可以将程序执行从程序的一部分转移到另一部分,以支持循环和条件语句。该架构还具有读取和写入外部内存的指令。

ARM指令集通常被认为是简单、逻辑且高效的。它具有一些处理器中不存在的特性,同时也缺少其他处理器中存在的操作。例如,它无法直接对内存进行数据处理操作。要递增内存位置中的值,必须将该值加载到ARM寄存器中,然后递增该寄存器,再使用第三条指令将更新后的值写回内存。指令集架构(ISA)包括将移位与算术或逻辑操作结合的指令、用于优化程序循环的自增和自减寻址模式、多重加载和存储指令,以便实现高效的堆栈和堆操作,以及几乎所有指令的条件执行能力。

与x86类似(但与68K不同),ARM指令通常具有两个或三个操作数的格式,其中大多数情况下,第一个操作数指定结果的目的地。多重加载和存储指令是这一规则的例外。相对而言,68K将目的地放在最后一个操作数位置。对于ARM指令,通常没有关于可以用作操作数的寄存器的限制。以下两个示例展示了不同汇编语言之间的差异。

4.2 The ARM instruction sets

ARMv7 架构是一种32位处理器架构,它也是一种加载/存储架构,这意味着数据处理指令只对通用寄存器中的值进行操作,只有加载和存储指令才能访问内存。通用寄存器也是32位的。在本书中,当我们提到一个“字”(word)时,指的是32位。因此,一个双字(doubleword)是64位,半字(halfword)是16位宽。

虽然 ARMv7 架构是32位的,但各个处理器的实现不一定在所有模块和互连中都具有32位的宽度。例如,在指令获取或数据访问中,可能会有64位或更宽的路径。

大多数 ARM 处理器支持两种甚至三种不同的指令集,而有些(例如 Cortex-M3 处理器)实际上并不执行原始的 ARM 指令集。ARM 处理器至少可以使用两种指令集:

  • ARM(32位指令):这是原始的 ARM 指令集。

  • Thumb:Thumb 指令集首次在 ARM7TDMI 处理器中添加,包含了仅16位的指令,使得程序更小(在较小的嵌入式系统中,内存占用可能是一个主要问题),但以一定的性能为代价。包括 Cortex-A 系列在内的较新处理器,支持 Thumb-2 技术,它扩展了 Thumb 指令集,提供了16位和32位指令的混合。这结合了两者的优势,性能接近 ARM 指令集,代码大小类似于 Thumb 指令。由于其大小和性能优势,越来越多的代码会编译或汇编以利用 Thumb-2 技术。

在较早的 ARM 处理器中,系统通常包含为 ARM 状态编译的代码和为 Thumb 状态编译的代码。具有32位指令的 ARM 代码更强大,执行特定任务所需的指令更少,因此可能更适合性能关键的系统部分。它也用于异常处理代码,因为在 ARM7 或 ARM9 系列处理器上,异常无法在 Thumb 状态下处理。

Thumb 代码使用16位指令,与 ARM 代码相比,执行相同任务需要更多的指令。Thumb 代码通常可以在指令中编码较小的常量值,并且具有较短的相对分支。ARM 指令的相对分支范围大约是 ±32MB,而 Thumb-2 扩展的范围是 ±16MB。对于仅使用16位指令的 Thumb,条件分支范围限制为 ±256 字节,而无条件相对分支限制为 ±2048 字节。

然而,由于 Thumb 指令的大小只有 ARM 指令的一半,程序通常比对应的 ARM 代码小三分之一。因此,出于代码密度和减少系统内存需求的原因,使用 Thumb 指令。当处理器直接连接到窄(16位)内存时,Thumb 代码也可能比 ARM 更具性能优势,因为每个周期可以获取一个 Thumb 指令,而每个32位 ARM 指令需要两个时钟周期才能获取。

执行 Thumb 指令时,PC(程序计数器)读取为当前指令的地址加4。唯一能够直接修改 PC 的16位 Thumb 指令是 MOV 和 ADD 的某些编码。写入 PC 的值被强制对齐到半字,通过忽略其最低有效位,将该位视为0。

在 ARMCC(ARM 编译器)中,使用选项 --thumb 或 –arm(默认)可以选择用于编译的指令集。程序可以在运行时在这两种指令集之间分支。

当前使用的指令集由 CPSR 的 T 位指示,核心被称为处于 ARM 状态(T = 0)或 Thumb 状态(T = 1)。代码必须明确编译或汇编为某一状态。使用显式指令在指令集之间切换。调用为不同状态编译的函数被称为互操作(interworking)。

对于 Thumb 汇编代码,通常可以选择16位和32位的指令编码,默认生成16位版本。可以使用 .W(32位)和 .N(16位)宽度说明符来强制使用特定的编码(如果存在这样的编码),例如:

  • BCS.W label ; 强制使用32位指令,即使是短跳转

  • B.N label ; 如果标签超出16位指令范围则出错

4.3 Introduction to the GNU Assembler

GNU 汇编器(GNU Assembler)是 GNU 工具的一部分,用于将汇编语言源代码转换为二进制目标文件。汇编器在 GNU 汇编器手册中有详细的文档,可以在 http://sourceware.org/binutils/docs/as/index.html 找到。如果你在系统上安装了 GNU 工具,文档也可以在 gnutools/doc 子目录中找到。GNU 汇编器文档在 Ubuntu 中的 gcc-doc 包中也可以获取。

下面是一个简要描述,旨在强调 GNU 汇编器与标准 ARM 汇编语言之间的语法差异,并提供足够的信息,以便程序员可以使用这些工具。

GNU 工具组件的名称将包含前缀以指示所选择的目标选项,包括操作系统。一个例子是 arm-none-eabi-gcc,它可能用于使用 ARM EABI 的裸机系统。

4.3.1 Invoking the GNU Assembler

你可以通过运行 arm-none-eabi-as 程序,汇编 ARM 汇编语言源文件的内容,例如:

arm-none-eabi-as -g -o filename.o filename.s

选项 -g 请求汇编器在输出文件中包含调试信息。

当你所有的源文件都汇编成二进制目标文件(扩展名为 .o)后,可以使用 GNU 链接器(Linker)来创建最终的 ELF 格式可执行文件。

这可以通过以下命令完成:

arm-none-eabi-ld -o filename.elf filename.o

对于更复杂的程序(有许多独立的源文件),通常使用类似 make 的工具来控制构建过程。

你可以使用 arm-none-eabi-gdb 或 arm-none-eabi-insight 调试器在主机上运行可执行文件,作为实际目标内核的替代方案。

4.3.2 GNU Assembler syntax

GNU 汇编器可以面向许多不同的处理器架构,而不仅仅是 ARM 架构。这意味着它的语法与其他 ARM 汇编器(例如 ARM 工具链)略有不同。GNU 汇编器使用相同的语法来支持它支持的多种处理器架构。

汇编语言源文件由一系列语句组成,每行一个。

每个语句有三个可选部分,按照以下格式:

label: instruction @ comment
  • 标签(label):用于标识指令的地址。它可以用作分支指令或加载和存储指令的目标。标签可以是字母后跟(可选)一系列字母数字字符,并以冒号结尾。

  • 指令(instruction):可以是 ARM 汇编指令或汇编器指令(伪指令)。这些是告诉汇编器执行某些操作的指令,如设置对齐、创建数据、控制段等。

  • 注释(comment):在 @ 符号后面的所有内容都被视为注释并被忽略(除非它是字符串)。C 风格的注释分隔符 /* 和 */ 也可以使用。

4.3.3 Sections

一个包含代码的可执行程序至少会有一个段,按照惯例,这个段被称为 .text。数据可以包含在 .data 段中。

具有相同名称的指令允许你指定源文件中哪些内容属于哪个段。可执行代码应出现在 .text 段中,读写数据应出现在 .data 段中。只读的常量可以出现在 .rodata 段中。零初始化的数据将出现在 .bss 段中。以符号开头的块(Block Started by Symbol,bss)段定义未初始化静态数据的空间。

4.3.4 Assembler directives

这是 GNU 工具与其他汇编器之间差异的一个关键领域。

所有汇编器指令以句点 “.” 开头。完整的指令列表可以在 GNU 文档中找到。这里,我们给出常用指令的一个子集。

  • .align n:指示汇编器用零值填充二进制文件中的字节,在数据段中,或代码中的 NOP 指令,确保下一个位置在一个字边界上。.align n 在 ARM 核心上提供 2^n 对齐。

  • .ascii "string":将字符串文字按指定的内容插入目标文件中,不附带 NUL(空字符)字符。可以使用逗号作为分隔符指定多个字符串。

  • .asciz "string":与 .ascii 类似,但会在字符串后附加一个 NUL(值为0的字节)字符。

  • .byte expression, .hword expression, .word expression:将字节、半字或字值插入目标文件。可以用逗号分隔多个值。.2byte 和 .4byte 也是可以使用的同义词。

  • .data:将接下来的语句放入最终可执行文件的数据段。

  • .end:标记此源代码文件的结束。汇编器在此之后不会处理文件中的任何内容。

  • .equ symbol, expression:将 symbol 的值设置为 expression= 符号和 .set 具有相同效果。

  • .extern symbol:指示 symbol 在其他源代码文件中定义。

  • .global symbol:告知汇编器 symbol 在其他源文件和链接器中是全局可见的

  • .include "filename"
    将 filename 的内容插入到当前源文件中,通常用于包含包含共享定义的头文件。

.text
将接下来的语句切换到最终输出目标文件的文本段(text section)中。汇编指令必须始终位于文本段中。

作为参考,下表展示了常见的汇编器指令与 GNU 和 ARM 工具的对应关系。并非所有指令都列出,某些情况下它们之间并非100%对应。

4.3.5 Expressions

汇编指令和汇编器指令通常需要一个整数操作数。在汇编器中,这个操作数被表示为一个需要计算的表达式。通常,这将是一个以十进制、十六进制(带有0x前缀)或二进制(带有0b前缀)表示的整数,或者是一个用单引号包围的ASCII字符。

此外,汇编器还可以计算标准的数学和逻辑表达式,以生成一个常量值。这些表达式可以使用标签和其他预定义的值。这些表达式会生成绝对值或相对值。绝对值是与位置无关且恒定的,而相对值是相对于某个由链接器定义的地址指定的,该地址在生成可执行映像时确定,例如分支的目标地址。

4.3.6 GNU tools naming conventions

在GCC中,寄存器命名如下:

  • 通用寄存器:R0 - R15。

  • 堆栈指针寄存器:SP (R13)。

  • 帧指针寄存器:FP (R11)。

  • 链接寄存器:LR (R14)。

  • 程序计数器:PC (R15)。

  • 程序状态寄存器标志:xPSR、xPSR_all、xPSR_f、xPSR_x、xPSR_ctl、xPSR_fs、xPSR_fx、xPSR_f、xPSR_cs、xPSR_cf、xPSR_cx(其中 x = C 表示当前,S 表示保存的)。

4.4 ARM tools assembly language

统一汇编语言(UAL)格式现在被ARM工具所采用,能够在ARM和Thumb指令集之间使用相同的标准语法。ARM工具的汇编器语法与GNU汇编器(GNU Assembler)使用的语法并不完全相同,尤其是在预处理和无法直接映射到操作码的伪指令方面。

UAL提供了编写可以在所有ARM内核上运行的汇编代码的能力。在过去,必须明确地为ARM或Thumb状态编写代码。而使用UAL时,同一段代码可以在汇编时针对不同的指令集进行汇编,而无需在编写代码时进行区分。这可以通过使用命令行开关或内联指令来实现。旧的代码仍然可以正确地汇编。值得注意的是,GNU汇编器现在通过.syntax指令支持UAL,尽管其语法可能与ARM工具汇编器不完全相同。

4.4.1 ARM assembler syntax

ARM汇编源文件由一系列语句组成,每行一条语句。
每条语句包含三个可选部分,顺序如下:

label instruction ; comment

标签(label)用于标识该指令的地址,这样可以作为分支指令或加载和存储指令的目标。

指令(instruction)可以是汇编指令,也可以是汇编器指令。这些伪指令用于指示汇编器执行某些操作,主要用于控制段和对齐,或创建数据等。

在行中“;”符号后的所有内容都被视为注释并被忽略(除非它位于字符串中)。还可以使用C风格的注释分隔符“/”和“/”。

4.4.2 Labels

标签必须从一行的第一个字符开始。如果该行没有标签,则需要用空格或制表符(Tab)作为行的起始。如果有标签,汇编器会将该标签与目标文件中对应指令的地址关联起来。标签随后可以用作分支、加载或存储的目标。

在上述示例中,Loop是一个标签,条件分支指令(BNE Loop)将被汇编成一种方式,使分支指令中编码的偏移量指向与标签Loop关联的MUL指令的地址。

4.4.3 Directives

大多数行通常会有一个汇编语言指令,由工具转换为它的二进制等价物,或者一个指示汇编器执行某种操作的指令。它也可以是一个伪指令(由汇编器转换为一个或多个实际指令)。这些指令执行各种任务,可以用于在内存中将代码或数据放置在特定地址、创建对其他程序和数据的引用,等等。

例如,Define Constant(DCD, DCB, DCW)指令可以让你将数据放入一段代码中。这些数据可以用数字(十进制、十六进制、二进制)或ASCII字符来表达。它可以是单个项目或以逗号分隔的列表。DCB 用于字节大小的数据,DCD 可用于字大小的数据,DCW 用于半字大小的数据。

例如,我们可能有:

MESSAGE DCB "Hello World!",0

这将生成对应于字符串中 ASCII 字符的一系列字节,以0表示结束。MESSAGE是一个标签,我们可以用它来获取这段数据的地址。同样,我们可能会有以十六进制表示的数据项,例如:

Masks DCD 0x100, 0x80, 0x40, 0x20, 0x10

EQU指令允许你为地址或数据值赋予名称。例如:

CtrlD EQU 4
TUBE EQU 0x30000000

然后我们可以在其他指令中将这些名称用作需要求值的表达式的一部分。EQU实际上不会在程序中放置任何内容——它仅仅为汇编器的符号表中的值设定一个名称,以供在其他指令中使用。这可以使代码更易于阅读,并且如果我们在代码中改变地址或数值,只需修改原始定义,而不必单独更改所有引用。通常习惯将EQU定义放在一起,通常在程序或函数的开始处,或者在单独的包含文件中。

AREA伪指令用于告诉汇编器如何将代码或数据分组到逻辑部分中,以便以后由链接器进行定位。例如,异常向量可能需要放置在固定地址。汇编器跟踪每个指令或数据位于内存的位置。可以使用AREA指令来修改该位置。

ALIGN指令可以让你将当前位置对齐到指定的边界。通常这是通过用零或NOP指令(在必要时)进行填充完成的,虽然也可以用该指令指定一个填充值。默认行为是将当前位置设置到下一个字(四字节)边界,但可以从该边界指定更大的边界尺寸和偏移量。这可能需要满足某些指令(例如LDRD和STRD双字内存传输)的对齐要求,或者与缓存边界对齐。与GNU汇编器上的.align n指令一样,ALIGN n指令在ARM核心上提供2^n对齐。

END用于表示汇编语言源程序的结束。未使用END指令将导致错误返回。INCLUDE告诉汇编器将另一个文件的内容包含到当前文件中。包含文件可用于在相关文件之间共享定义。

4.5 Interworking

当核心执行ARM指令时,称其处于ARM状态。当它处于Thumb状态时,表示它正在执行Thumb指令。处于特定状态的核心只能执行该指令集中的指令。我们必须确保核心不会接收到错误指令集的指令。

每个指令集都包括用于改变处理器状态的指令。如果代码符合ARM和Thumb过程调用标准,则可以混合使用ARM和Thumb代码。编译器生成的代码始终会符合此标准,但汇编语言程序员必须注意遵循指定的规则。

处理器状态的选择由CPSR中的T位控制。当T位为1时,处理器处于Thumb状态;当T位为0时,处理器处于ARM状态。然而,当T位被修改时,还需要刷新指令流水线(以避免指令在一种状态下解码而在另一种状态下执行的问题)。特殊指令用于实现这一点,包括BX(带交换的分支)和BLX(带链接的分支和交换)。加载PC的LDR指令和使用POP/LDM修改PC的指令也具有这种行为。除了使用这些指令改变处理器状态外,汇编程序员还必须使用适当的指令来告诉汇编器生成适合相应状态的代码。

BX或BLX指令会分支到指定寄存器中包含的地址或操作码中指定的偏移量。分支目标地址的位[0]的值决定了接下来的执行是在ARM状态还是Thumb状态。由于ARM指令(对齐到字边界)和Thumb指令(对齐到半字边界)都不使用位[0]来形成指令地址,因此该位可以安全地用于提供额外的信息,以指示BX或BLX指令应将状态更改为ARM(地址位[0] = 0)或Thumb(地址位[0] = 1)。如果调用者的指令集与标签的指令集不同,并且调用是无条件的,BL标签在链接时将被适当地转换为BLX标签。

这些指令的典型用法是,当一个函数通过BL或BLX指令调用另一个函数时,可以通过BX LR指令从该函数返回。或者,对于一个非叶函数(在入口处将链接寄存器压入堆栈,在退出时将存储的链接寄存器从堆栈中弹出到程序计数器中),可以使用内存加载来返回,而不是使用BX LR指令。修改PC的内存加载指令也可能根据加载地址的位[0]的值来改变处理器状态。

4.6 Identifying assembly code

当面对一段汇编语言源代码时,确定使用哪种指令集以及它面向哪种汇编器是非常有用的。旧的ARM汇编语言代码可能会包含三(甚至四)个操作数的指令(例如,ADD R0, R1, R2),或者包含非跳转指令的条件执行(例如,ADDNE R0, R0, #1)。文件扩展名通常为.s.S

针对较新的UAL(统一汇编语言)的代码会包含.syntax unified指令,但在其他方面看起来与传统的ARM汇编语言相似。立即数操作数前的符号“#”可以省略。条件指令序列必须紧跟在IT指令之后。这样的代码可以汇编成固定大小的32位(ARM)指令,或者根据.thumb.arm指令的存在与否,汇编成混合大小(16位和32位)的Thumb指令。

您偶尔会遇到使用16位Thumb汇编语言编写的代码。这类代码可能包含指令如.code 16.thumb.thumb_func,但不会指定.syntax unified。大多数指令使用两个操作数,但ADDSUB有时可能会使用三个。只有分支指令可以进行条件执行。

所有GCC内联汇编代码类型(如.c.h.cpp.cxx.c++)都可以根据GCC配置和命令行开关(-marm-mthumb)来构建为Thumb或ARM代码。

4.7 Compatibility with ARMv8-A

随着ARMv8-A架构的引入,为了提供向后兼容性,ARMv7 ISA(指令集架构)进行了多项更改。以下指令已被添加到ARMv7 ISA中。


Arm精选
ARMv8/ARMv9架构、SOC架构、Trustzone/TEE安全、终端安全、SOC安全、ARM安全、ATF、OPTEE等
 最新文章