CM3 汇编

寄存器 #

R0
R0
R1
R1
R2
R2
R3
R3
R4
R4
R5
R5
R6
R6
R7
R7
R8
R8
R9
R9
R10
R10
R11
R11
R12
R12
R13 (分组)
R13 (分组)
R14
R14
R15
R15
程序计数器 (PC)
程序计数器 (PC)
链接寄存器 (LR)
链接寄存器 (LR)
栈指针 (SP),包含主栈指针 (MSP) 和进程栈指针 (PSP)
栈指针 (SP),包含主栈指针 (MSP) 和进程栈指针 (PSP)
通用寄存器(高寄存器)
通用寄存器(高寄存器)
通用寄存器(低寄存器)
通用寄存器(低寄存器)

Thumb 指令集 #

ARM 架构的指令集有 ARM、Thumb、Thumb-2、ARM64 等等,分别针对不同应用场景:

简单对比:

例如 16 位的 MOV 指令只能承载 8 位立即数:

| 15:11 | 10:8 | 7:0 |     op: 操作码(5位)
|-------|------|-----|     Rd: 目标寄存器(3位)
|  op   |  Rd  | imm |     imm:立即数(8位)

当立即数超过了 8 位,则需要使用多条指令来加载立即数:

MOV R0, #0x34      ; 加载低8位
MOV R1, #0x12      ; 加载高8位

32 位的 MOVW 指令可承载 16 位的数据:

MOVW R0, #0x1234

流水线架构 #

Cortex-M3/M4 采用三级流水线:取指(Fetch)、解码(Decode)、执行(Execute)

取指
取指
执行
执行
解码
解码
取指
取指
执行
执行
解码
解码
取指
取指
执行
执行
解码
解码
取指
取指
执行
执行
解码
解码
指令 N
指令 N
指令 N + 1
指令 N + 1
指令 N + 2
指令 N + 2
指令 N + 3
指令 N + 3

当第 N 条指令执行时,第 N+2 条指令正在取值,而 PC 总是指向正在取指的指令,即指向第三条指令

为了保证 Thumb-2 与 Thumb 的一致性,对于 CM3/4,不管执行 16 位或 32 位指令,PC 总为当前执行的指令地址 + 4 字节

当执行到跳转指令时,需要清洗流水线,处理器必须从跳转目的地重新取指。 因此,尽量地少使用跳转指令可以提高程序的执行效率。

ARM 状态与 Thumb 状态 #

有些处理器同时支持 ARM 和 Thumb 指令集,通过 BXBLX 指令进行切换,当指令 BX 跳转地址的 LSB 位为 1,表示切换到 Thumb 状态,为 0 则切换到 ARM 状态。

无论是 ARM 还是 Thumb,其指令在存储器中都是边界对齐的(2字节或4字节对齐),也就是地址的最低位总是 0。 因此,在执行跳转过程中,PC 寄存器中的最低位被舍弃,不起作用。 在 BX 指令的执行过程中,最低位正好被用作状态判断的标志,不会造成存储器访问不对齐的错误。

Cortex-M 系列处理器不支持 ARM 指令,因此也不存在 ARM 状态,所以反汇编中的函数地址最低位都是 1 。

汇编语法 #

不同供应商的汇编工具,其伪指令、标签和注释的语法会有差异

Keil MDK-ARM(AC5) 语法:

;标签           ;指令    ;操作数
Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]
                IMPORT  __main
                IMPORT  SystemInit
                LDR     R0, =SystemInit
                BLX     R0               
                LDR     R0, =__main
                BX      R0
                ENDP

GNU 汇编语法,指令与寄存器名称可以小写:

/*标签           指令       操作数         */
                .section  .text.Reset_Handler
                .weak     Reset_Handler
                .type     Reset_Handler, %function
Reset_Handler:  
                movs      r1, #0
                b         LoopCopyDataInit

统一汇编语言 #

统一汇编语言(Unified Assembly Language, UAL)是一种语法规范,统一 ARM 和 Thumb 指令集的汇编指令格式

Keil 和 Gnu 是汇编 伪指令 不同,汇编 指令 语法相同

标签 #

标签(lable)用于标记代码或数据在内存中的位置,方便跳转指令等(如 B、BL)通过标签引用这些地址,而不必直接使用具体的内存地址

伪指令 #

伪指令不是实际的机器指令,但在汇编过程中会被转换为一条或多条等效的机器指令

某些指令如 LDR R0, [R1] 是汇编指令,LDR R0, =0x12345678 是伪指令,取决于它的使用方式

Keil 汇编器伪指令 #

DCB : 定义字节数据(Define Constant Byte)

MyByte    DCB  0x12
MyBytes   DCB  0x12, 0x34, 0x56
MyString  DCB  "Hello", 0

DCW : 定义半字数据(2字节)


DCD : 定义字数据(4字节)


DCQ : 定义双字数据(8字节)


SPACE : 分配指定字节的未初始化空间

MyBuffer SPACE 10  ;分配 10 个字节并初始化为零

FILL : 分配指定数量的字节,并用指定值填充

MyBuffer FILL 10, 0xFF  ;填充 10 个字节,值为 0xFF

EQU : 定义符号常量

MAX_VALUE  EQU  100

AREA : 定义代码或数据段,为汇编器提供信息,以便正确组织和分配内存

;AREA   段名,  属性1,  属性2, ...
AREA MyCode,  CODE,  READONLY, ALIGN=2

;段名:段的名称,通常是一个字符串,用于标识该段。
;属性:定义段的特性,常见的属性包括:
;     CODE       表示代码段,通常用于存储可执行指令
;     DATA       表示数据段,通常用于存储变量和常量
;     READONLY   表示该段是只读的
;     READWRITE  表示该段是可读写的
;     ALIGN=n    指定段的对齐方式,ALIGN=n 表示按 2^n 方字节对齐
;     NOINIT     表示该段不需要初始化
;定义了一个名为 MyData 的数据段,具有读写属性
AREA MyData, DATA, READWRITE
Data1 DCD 0x1234
Data2 DCD 0x5678
;定义了一个名为 .text 的代码段,具有只读属性,管道符号 || 是可选的,用于包含特殊字符
AREA |.text|, CODE, READONLY

PROC : 定义一个子程序,与 ENDP 配对使用,ENDP 用于标记子程序的结束

子程序名称 PROC [属性1] [, 属性2]...
    ; 子程序代码
子程序名称 ENDP
NMI_Handler     PROC
                EXPORT  NMI_Handler                [WEAK]
                B       .   ;. 表示当前地址,即 B . 是一个无限循环,相当于 while(1)
                ENDP

EXPORT : 用于声明一个全局符号,使其在其他模块中可见和可调用


IMPORT : 用于声明一个外部符号


WEAK : 用于声明一个弱符号

WEAK    MyFunction 
EXPORT  MyFunction  [WEAK] ;与 EXPORT 配合使用

ALIGN : 对齐到 2^n 字节边界


IFDEF : 用于检查某个符号是否已定义

IFDEF DEBUG
    MOV R0, #1  ; 如果 DEBUG 已定义,编译这行代码
ENDIF

IF ELSE : 条件汇编

IF Var = 1
    MOV R1, #1  ; 如果条件为真,编译这行代码
ELSE
    MOV R1, #2  ; 如果条件为假,编译这行代码
ENDIF
; =	等于
; <>	不等于
; >	大于
; <	小于
; >=	大于等于
; <=	小于等于

GNU 汇编器伪指令 #

更多参考 The GNU Assembler 第 7 章


.align : 用于对齐代码或数据到 2^n 字节边界,如 .align 2 对齐到 4 字节的边界


.extern : 用于声明一个外部符号(如函数或变量)


.include : 用于包含其他汇编文件,如 .include "header.s"


.text : 表示后续代码属于 .text 段,.text 实际上是 .section .text 的简写,类似的还有 .data.bss


.section : 告诉汇编器将本行之后的代码或数据放入指定的段中,直到遇到下一个 .section

.section  .text.Default_Handler,"ax",%progbits
/* 格式:.section 段名 [, 标志] [, 类型] [, 对齐] [, 入口点]                                
   段名:指定段的名称,如 .text、.data、.bss 等                                             
   标志:可选参数,指定段的属性,如 "a"(可分配)、"w"(可写)、"x"(可执行)等             
   类型:可选参数,指定段的类型,如 %progbits(包含数据)、%nobits(不包含数据,如 .bss 段)
   对齐:可选参数,指定段的对齐方式,按 2^n 字节对齐                                        
   入口点:可选参数,指定段的入口点 */

.ascii : 定义字符串

my_string:
    .ascii "ARM GCC Assembly\0"

.string .asciz : 定义字符串,并在末尾添加 \0

my_string:
    .string "ARM GCC Assembly"

.byte : 定义一个或多个字节的数据,类似还有 .word .int .float .double

.data
my_array:
    .byte 1, 2, 3, 4, 5
my_values:
    .word 0x12345678, 0x87654321, 0xABCDEF01

.equ .set : 定义符号常量,类似于 C 语言中的 #define.equ.set 更通用

.equ BUFFER_SIZE, 1024 * 4

.if .ifdef .else .elseif .endif 是条件汇编伪指令

.equ FLAG, 0x1
.if FLAG == 1
    mov r0, r1
.else
    mov r0, r2
.endif

.macro .endm : 定义宏

.macro add_reg reg1, reg2
    add \reg1, \reg1, \reg2
.endm

.space : 用于在内存中分配指定大小的空间,并用指定的值填充该空间

/* 分配 200 字节的空间,并用 0x00 填充,并使用标签 my_buffer 标记起始地址 */
my_buffer:  .space 200, 0x00

.global : 声明一个全局符号,可以在其他汇编文件或 C/C++ 代码中引用

.global my_variable  /* 声明 my_variable 为全局符号(变量)*/
.global my_function  /* 声明 my_function 为全局符号(函数)*/

.data
my_variable:
    .word 0x12345678  /* 定义一个32位变量 */

.text
my_function:
    mov r0, #42
    bx lr
// 在 C 代码中调用
extern int my_variable;
extern void my_function(void);

.type : 用于指定符号的类型信息,这对于链接器和调试器非常重要

/*  %function :指定符号是一个函数       
    %object   :指定符号是一个数据对象   
    %common   :用于未初始化的公共数据   
    %notype   :无特定类型               */

.globl my_function
.type my_function, %function
my_function:
    bx lr
.globl my_data
.type my_data, %object
my_data:
    .word 0x12345678

.size : 显式指定符号的大小信息(边界),以便链接器可以检查符号引用是否超出范围

.global my_function
.type my_function, %function
my_function:
    /* 函数代码 */
    mov r0, #0
    bx lr
/* 设置 my_function 的大小为当前位置(.)减去 my_function 的起始地址 */
.size my_function, .-my_function   
/* .size 符号名, 表达式
       符号名:指定要设置大小的符号,通常是函数或数据的名称
       表达式:指定符号的大小,通常以字节为单位                      */
.globl my_array
.data
my_array:
    .word 1, 2, 3, 4
.size my_array, 16  // 4个word × 4字节 = 16字节

.weak : 声明一个弱符号

.weak my_function
my_function:
    bx lr

.weak my_variable
.data
my_variable:
    .word 0

.syntax : 汇编代码的语法格式

.syntax unified  /* 统一汇编语言(默认),支持混合使用 ARM 和 Thumb 指令 */
.syntax divided  /* 传统语法,仅支持 ARM 指令集 */

.cpu : 处理器架构,使汇编器针对特定的架构优化

.cpu cortex-m3

.fpu : 处理器支持的浮点单元(FPU)类型

.fpu softvfp  /* 软件浮点运算 */
.fpu vfpv3    /* ARMv7 架构的 VFPv3 */

.thumb : 使用 thumb 指令集,在某些情况下,可以在同一汇编文件中混合使用 .arm.thumb 指令集


.thumb_set : 将一个符号定义为另一个符号的别名 (alias),并且明确指定该别名是 Thumb 指令集的符号

.thumb_set 别名, 目标符号
.global my_function
.type my_function, %function
.thumb_func
my_function:
    movs r0, #0
    bx lr
.size my_function, .-my_function

/* 为 my_function 创建一个 Thumb 别名 */
.thumb_set my_function_alias, my_function

寻址方式 #

寄存器寻址MOV R0, R1R0R1
立即数寻址MOV R0, #4R04
寄存器间接寻址LDR R0, [R1]R0*R1
基址加偏移寻址LDR R0, [R1, #4]R0*(R1 + 4)
基址加索引寻址LDR R0, [R1, R2]R0*(R1 + R2)
前变址寻址LDR R0, [R1, #4]!R1R1 + 4R0*R1
后变址寻址LDR R0, [R1], #4R0*R1R1R1 + 4
PC 相对寻址LDR R0, [PC, #4]R0*(PC + 4)
字面量池寻址LDR R0, =0x12345678R00x12345678
多寄存器寻址LDMIA SP!, {R0, R1}R0*SPSPSP + 4R1*SPSPSP + 4
堆栈寻址PUSH {R0, R1}SPSP - 4*SPR0SPSP - 4*SPR1
POP {R0, R1}R0*SPSPSP + 4R1*SPSPSP + 4

! 表示在数据传输完成后,将更新后的内存地址写回基址寄存器,这个特性称为 “写回”

地址对齐 #

0x0000
0x0000
0x1
0x1
0x78
0x78
0x56
0x56
0x34
0x34
0x12
0x12
0x0001
0x0001
0x0002
0x0002
0x0003
0x0003
0x0004
0x0004
0x0005
0x0005
0x0006
0x0006
0x0007
0x0007
0x0008
0x0008
0x0009
0x0009
0x000A
0x000A
0x000B
0x000B
0x000C
0x000C
0x000D
0x000D
0x000E
0x000E
0x000F
0x000F
按 2^2 = 4 字节对齐
按 2^2 = 4 字节对齐
二进制地址最后两位是 0
二进制地址最后两位是 0
十六进制地址最后一位是 0、4、8、C
十六进制地址最后一位是 0、4、8、C
.section .rodata
.section .rodata
my_byte:
my_byte:
.byte 0x1
.byte 0x1
.align 2
.align 2
my_word:
my_word:
.word 0x12345678
.word 0x12345678

在汇编中 .align n2^n 字节对齐,在链接脚本中 ALIGN(n)n 字节对齐。 大多数情况下,4 字节传输的地址要 4 字节对齐,2 字节传输的地址要 2 字节对齐

应用程序状态寄存器 ASPR #

标志位置置位条件(等于1)
N31结果为负、算术或逻辑运算结果的最高有效位为 1、被比较数 < 另一个数
Z30结果为零、比较相等
C29无符号加法溢出、被减数 >= 减数,被比较数 >= 另一个数、移位时最后一个被移出的位为 1
V28有符号运算结果溢出
Q27饱和运算

指令后缀 #

例如 beq equal 表示当 Z == 1 时,执行跳转:

cmp r0, r1      /* 比较 r0 和 r1 的值              */
beq equal       /* 如果 r0 == r1,跳转到标签 equal */
后缀含义标志状态英文
EQ等于Z == 1Equal
NE不等于Z == 0Not Equal
HI无符号 >C == 1 Z == 0Higher
CS/HS无符号 >= ,进位设置C == 1Carry set / High or Same
CC/LO无符号 < ,进位清除C == 0Carry Clear / Lower
LS无符号 <=C == 0 Z == 1Lower or Same
MI负数N == 1Minus
PL正数或零N == 0Plus
VS有符号溢出V == 1Overflow Set
VC有符号不溢出V == 0Overflow Clear
GE有符号 >=N == VGreater or Equal
LT有符号 <N != VLess Than
GT有符号 >Z == 0 N == VGreater Than
LE有符号 <=Z == 1 N != VLess or Equal
AL无条件,始终执行Always
后缀含义
S更新 APSR
.N .W指定使用 16 位指令(narrow)或 32 位指令(wide)
.32 .F32指定 32 位单精度运算
.64 .F64指定 64 位双精度运算

处理器内传送数据的指令 #

传送数据(Move)MOV R4, R0R0R4
MOVS R4, R0R0R4,更新 APSR
传送至低 16 位MOVW R6, #0x1234R6[0:15]0x1234
传送至高 16 位(Move Top)MOVT R6, #0x8765R6[16:31]0x8765
取反传送(Move Not)MVN R3, R7R3取反R7
特殊寄存器 → 通用寄存器MRS R7, PRIMASKR7PRIMASK
通用寄存器 → 特殊寄存器MSR CONTROL, R2CONTROLR2

加载标签地址 #

下面两条都是伪指令,即汇编器会根据实际情况,用一条或多条等效的指令来替代它

文字池,是汇编器在 .text 段内部开辟的一块用于存储常量数据的特殊区域,通常位于 .text 末尾,也可使用 .pool 伪指令手动指定。

my_function:
    LDR R0, =0x12345678    ; 需要一个文字池项
    LDR R1, =my_global_var ; 需要另一个文字池项
    ...                    ; 很多代码
    BX LR                  ; 函数返回

    .pool                  ; 手动提示汇编器在此处生成文字池
                           ; 汇编器会将两个常量放在这里

汇编后可能变成:

my_function:
    LDR R0, [PC, #offset1]  ; PC + offset1 指向 0x12345678
    LDR R1, [PC, #offset2]  ; PC + offset2 指向 my_global_var的地址
    ...
    BX LR

.word 0x12345678          ; 文字池项 #1
.word my_global_var       ; 文字池项 #2

存储器访问指令 #

加载(读存储器)存储(写存储器)
8 位无符号(Byte)LDRBSTRB
8 位有符号 (Signed Byte)LDRSBSTRB
16 位无符号 (Half Word)LDRHSTRH
16 位有符号 (Signed Half Word)LDRSHSTRH
32 位(Word)LDRSTR
多个 32 位 (Multiple)LDMSTM
双字 64 位 (Double)LDRDSTRD
栈操作 32 位POPPUSH
LDR R0, [R1, #0x8]    /*  R0 ← *(R1 + 8)                     */
LDR R0, [R1, #0x8]!   /*  R0 ← *(R1 + 8) , R1 += 8           */
STR R0, [R1, #0x8]    /*  *(R1 + 8) ← R0                     */

多加载和多存储 LDM / STM #

地址模式后缀
存储后地址递增(Increment After)IA(默认)
存储前地址递增(Increment Before)IB
存储后地址递减(Decrement After)DA
存储前地址递减(Decrement Before)DB

STMDB SP!, {R0-R3, LR} 等同于 PUSH {R0-R3, LR}

SP      SP-4       SP-8       SP-12      SP-16      SP-20       SP=SP-20
  →│xx│       │  │       │  │       │  │       │  │       │  │   
   │  │      →│LR│       │LR│       │LR│       │LR│       │LR│       
   │  │       │  │      →│R3│       │R3│       │R3│       │R3│   
   │  │       │  │       │  │      →│R2│       │R2│       │R2│        
   │  │       │  │       │  │       │  │      →│R1│       │R1│        
   │  │       │  │       │  │       │  │       │  │      →│R0│        
   │  │       │  │       │  │       │  │       │  │       │  │   

LDMIA SP!, {R0-R3, PC} 等同于 POP {R0-R3, PC}

SP      SP+4       SP+8       SP+12      SP+16      SP+20       SP=SP+20
   │  │       │  │       │  │       │  │       │  │      →│xx│ 
   │LR│       │LR│       │LR│       │LR│      →│LR│       │  │     
   │R3│       │R3│       │R3│      →│R3│       │  │       │  │ 
   │R2│       │R2│      →│R2│       │  │       │  │       │  │      
   │R1│      →│R1│       │  │       │  │       │  │       │  │      
  →│R0│       │  │       │  │       │  │       │  │       │  │      
   │  │       │  │       │  │       │  │       │  │       │  │ 

注意:SP! 表示 所有数据 传输完成后再写回 SP

注意:无论寄存器列表如何书写,编号 高的寄存器 总是 先存储,编号 低的寄存器 总是 先加载

PUSH 实际上是 STMDB 的别名,POPLDMIA 的别名

数组在栈中的存储顺序:

│ a[3] │ 高地址
│ a[2] │
│ a[1] │
│ a[0] │ 低地址

函数调用与返回 #

.global _start

_start:
    MOV R0, #3        /* 参数 1                               */
    MOV R1, #5        /* 参数 2                               */
    LDR R4, =0x1234   /* 测试 R4-R6 是否会被破坏              */
    LDR R5, =0x5678   /* 测试 R4-R6 是否会被破坏              */
    LDR R6, =0x9ABC   /* 测试 R4-R6 是否会被破坏              */
    BL add            /* 调用 add                             */ 
    LDR R3, =0x8888   /* lr = 此处地址:返回后从这里继续执行  */

add: 
    PUSH {R4-R6,LR}   /* 保存 R4-R6 和返回地址 LR             */
    MOV R4, R0
    MOV R5, R1
    ADD R6, R4, R5
    MOV R0, R6        /* R0 保存返回值                        */
    POP {R4-R6,PC}    /* 恢复 R4-R6,将 LR 的值弹入 PC 寄存器 */

R0 通常作为返回值的寄存器,约定俗成?

排他访问 #

排他访问(Exclusive)后缀,字节(B),半字(H)
加载字LDREX Rd, [Rn]Rd*Rn , 标记 *Rn 为独占访问
存储字STREX Rt, Rd, [Rn]*RnRdRt返回值 (0:成功,1:失败)
清除独占状态CLREX

LDREX 必须与 STREX (Exclusive) 指令配合使用,用于实现原子读-修改-写操作

try_atomic_update:
    LDREXB R1, [R0]       /* 从 R0 指向的地址加载数据到 R1,并标记 R0 指向的地址为独占访问 */
    ...                   /* 在这里对 R1 的值进行修改                                      */
                          /* 在此期间 R0 指向的地址处的数据被修改,则下面的指令将执行失败  */
    STREXB R2, R1, [R0]   /* 将修改后 R1 的值存回 R0 地址,返回状态存入 R2                 */
    CMP R2, #0            /* 检查存储是否成功(R2=0表示成功)                              */
    BNE try_atomic_update /* 如果不成功则重试                                              */

只有在执行 STREX 之前,独占访问的地址没有被其它程序(如中断或 DMA) 写入过,STREX 才能返回成功

算术运算 #

加法ADD Rd, Rn, RmRd = Rn + Rm
ADD Rd, Rn, #immedRd = Rn + #immed
带进位的加法ADC Rd, Rn, RmRd = Rn + Rm + 进位
ADC Rd, #immedRd = Rn + #immed + 进位
减法SUB Rd, Rn, RmRd = Rn - Rm
SUB Rd, #immedRd = Rd - #immed
SUB Rd, Rn, #immedRd = Rn - #immed
带借位的减法SBC Rd, Rn, #immedRd = Rn - #immed - 借位
SBC Rd, Rn, RmRd = Rn - Rm - 借位
减反转RSB Rd, Rn, #immedRd = #immed - Rn
RSB Rd, Rn, RmRd = Rm - Rn
乘法MUL Rd, Rn, RmRd = Rn * Rm
无符号除法UDIV Rd, Rn, RmRd = Rn / Rm
有符号除法SDIV Rd, Rn, RmRd = Rn / Rm

ADC 指令通常与 ADDS 指令配合使用,先用 ADDS 设置进位标志,再用 ADC 处理高位:

MOV  R0, #0xFFFFFFFF    ; R0 = -1 (unsigned 0xFFFFFFFF)
MOV  R1, #1
ADDS R2, R0, R1         ; R2 = 0, 设置进位 C = 1
MOV  R3, #0
ADC  R4, R3, R3         ; R4 = 0 + 0 + 1 = 1

64 位加法示例 R1:R0 + R3:R2 = R5:R4

ADDS R4, R0, R2    ; 低 32 位相加,更新 APSR
ADC  R5, R1, R3    ; 高 32 位带进位相加

逻辑运算 #

按位与AND Rd, RnRd = Rd & Rn
AND Rd, Rn, #immedRd = Rn & #immed
AND Rd, Rn, RmRd = Rn & Rm
按位或ORR Rd, Rn, RmRd = Rn | Rm
按位异或(Exclusive OR)EOR Rd, Rn, RmRd = Rn ^ Rm
按位或非(OR Not)ORN Rd, Rn, RmRd = Rn | (~Rm)
位清除(Bit Clear)BIC Rd, Rn, RmRd = Rn & (~Rm)

BIC 指令用于清除特定位:

BIC R0, R1, #0xFF    /* 清除 R1 的低 8 位,结果存入 R0 */
                     /* 等效于:R0 = R1 & (~0xFF)    */

移位和循环移位指令 #

寄存器
寄存器
C
C
寄存器
寄存器
C
C
寄存器
寄存器
C
C
寄存器
寄存器
C
C
寄存器
寄存器
C
C
ASR
ASR
LSR
LSR
LSL
LSL
ROR
ROR
RRX
RRX
0
0
0
0
算术右移(Arithmetic Shift Right)ASR Rd, Rn, #immedRd = Rn >> #immed
ASR Rd, RnRd = Rd >> Rn
ASR Rd, Rn, RmRd = Rn >> Rm
逻辑右移(Logical Shift Right)LSR Rd, Rn, #immedRd = Rn >> #immed
逻辑左移(Logical Shift Left)LSL Rd, Rn, #immedRd = Rn << #immed
循环右移(Rotate Right)ROR Rd, Rn, #immed从右侧移出的位会循环填充到左侧空出的位置
循环右移并展开(Rotate Right with Extend)RRX Rd, RnC 标志位参与右移 1 位

算术右移左侧用符号位填充,逻辑右移左侧用 0 填充:

MOV R0, #0xF0000000          /* R0 = 11110000 00000000 00000000 00000000              */
ASR R1, R0, #4               /* R1 = 11111111 00000000 00000000 00000000 (0xFE000000) */

ROR 指令示例:

MOV R0, #0x80000001          /* R0 = 10000000 00000000 00000000 00000001              */
ROR R1, R0, #1               /* R1 = 11000000 00000000 00000000 00000000              */

RRX 指令示例:

MOV R0, #0x80000001          /* R0 = 10000000 00000000 00000000 00000001              */
MSR APSR, #0x20000000        /* 设置 C 标志为 1                                       */
RRX R1, R0                   /* R1 = 11000000 00000000 00000000 00000000 (0xC0000000) */
MOV R0, #0x80000001          /* R0 = 10000000 00000000 00000000 00000001              */
MSR APSR, #0                 /* 清除 C 标志                                           */
RRX R1, R0                   /* R1 = 01000000 00000000 00000000 00000000 (0x40000000) */

实现 64 位数逻辑右移 4 位:

.global _start
_start:
	MOV  R0, #0x80000001   /* 低 32 位                 */
	MOV  R1, #0x80000001   /* 高 32 位                 */
	MOV  R2, #4            /* 循环 4 次计数            */

shift_loop:
	LSRS R1, R1, #1        /* R1 = R1 >> 1 并更新 APSR */
	RRX  R0, R0            /* R0 = {R0, C} >> 1        */
	SUBS R2, R2, #1        /* R2 = R2 - 1 并更新 APSR  */
	BNE  shift_loop        /* 如果 R2 != 0 则跳转      */

.end

/*                               移位前
|          R1 = 0x8000001          |           R0 = 0x80000001        | 
10000000 00000000 00000000 00000001 10000000 00000000 00000000 00000001

                                 移位后
|          R1 = 0x0800000          |           R0 = 0x18000000        | 
00001000 00000000 00000000 00000000 00011000 00000000 00000000 00000000   */

数据转换运算 #

有符号展开字节为字(Signed eXtend Byte)SXTB Rd, RnRn = 有符号展开 Rn[7:0],高位补符号位
有符号展开半字为字(Signed eXtend Half)SXTH Rd, RnRn = 有符号展开 Rn[15:0]
无符号展开字节为字(Unsigned eXtend Byte)UXTB Rd, RnRn = 无符号展开 Rn[7:0],高位补 0
无符号展开半字为字(Unsigned eXtend Half)UXTH Rd, RnRn = 无符号展开 Rn[15:0]
LDR R0, =0x55AA8765
SXTB R1, R0          /* R1 = 0x00000065 */
SXTH R1, R0          /* R1 = 0xFFFF8765 */
UXTB R1, R0          /* R1 = 0x00000065 */
UXTH R1, R0          /* R1 = 0x00008765 */
循环移位有符号展开字节SXTB Rd, Rn, ROR #n(n = 8/16/24)
MOV  R0, #0x8000     /* R0 = 0x00008000 */
SXTB R1, R0, ROR #8  /* R1 = 0xFFFFFF80 */
REV
REV
REV16
REV16
REVSH
REVSH
有符号展开
有符号展开
反转字中的字节(Reverse)REV Rd, Rn
反转半字中的字节REV16 Rd, Rn
反转低半字中的字节,并将结果有符号展开REVSH Rd, Rn
LDR   R0, =0x12345678
REV   R1, R0  /* R1 = 0x78563412 */
REV16 R2, R0  /* R2 = 0x34127856 */

LDR   R0, =0x33448899
REVSH R3, R0  /* R3 = 0xFFFF9988 */

位域处理指令 #

清除寄存器中的位域(Bit Field Clear)BFC Rd, #lsb, #width从 lsb 位开始清除 width 位
将位域插入寄存器(Bit Field Insert)BFI Rd, Rn, #lsb, #widthRn 第 0 位开始的共 width 位,插入 Rd 第 lsb 位
前导零计数(Count Leading Zeros)CLZ Rd, Rn计算 Rn 最高位开始的连续 0 的个数并存入 Rd
反转寄存器中位的顺序(Reverse Bits)RBIT Rd, Rn如 1100 反转为 0011
提取位域并有符号展开(Signed Bit Field Extract)SBFX Rd, Rn, #lsb, #width提取 Rn 第 lsb 开始的 width 位并有符号展开后存入 Rd
提取位域并无符号展开(Unsigned Bit Field Extract)UBFX Rd, Rn, #lsb, #width提取 Rn 第 lsb 开始的 width 位并存入 Rd
LDR R0, =0x1234FFFF
BFC R0, #4, #8       /* R0 = 0x1234F00F */
LDR R0, =0x12345678
LDR R1, =0xFFFFFFFF
BFI R1, R0, #8, #16  /* R1 = 0xFF5678FF */
LDR R0, =0x00FFFFFF
CLZ R1, R0           /* R1 = 8          */
LDR  R0, =0xC0C00000
RBIT R1, R0          /* R1 = 0x00000303 */
LDR  R0, =0x00000880
SBFX R1, R0, #4, #8  /* R1 = 0xFFFFFF88 */
LDR  R0, =0x00000880
UBFX R1, R0, #4, #8  /* R1 = 0x00000088 */

比较和测试 #

比较(Compare)CMP Rn, Rm计算 Rn-Rm
CMP Rn, #immed计算 Rn-#immed
负比较(Compare Negative)CMN Rn, #immed计算 Rn+#immed
测试某些位是否被置 1(Test)TST Rn, #immed计算 Rn&#immed,按位与
测试两个数是否按位相等(Test Equivalence)TEQ Rn, #immed计算 Rn^#immed,按位异或

以上指令均不保存计算结果,且总是会更新 APSR,因此这些指令没有 S 后缀

TSTTEQ 会更新 NZ,若使用了桶形移位还会更新 C

程序流程控制 #

跳转(Branch)B lable跳转到 lable 处,跳转范围 ±2KB,超出范围使用 B.W 的 32 位指令
间接跳转(Branch and Exchange)BX Rm跳转到 Rm 中存放的地址处,并且基于 Rm 第 0 位设置 ARM 或 Thumb 状态
函数调用(Branch with Link)BL lable跳转到 lable 处,并将返回地址(下一条指令的地址)保存在 LR 寄存器中
函数调用BLX Rm跳转到 Rm 中的地址处,返回地址保存在 LR ,更新 EPSR 的 T 为 Rm 的 0 位

Cortex-M 处理器只支持 Thumb 状态,BXBLX 指令中 Rm 第 0 位必须为 1

比较和跳转(Compare and Branch if Zero)CBZ Rn, lable若 Rn = 0 则跳转到 lable 处
比较和跳转(Compare and Branch if None-Zero)CBNZ Rn, lable若 Rn != 0 则跳转到 lable 处

CBZCBNZ 不会更新 APSR 寄存器,且只能向前(更高的地址)跳转

IT(IF-THEN)指令

IT _ _ _    条件码
IT _ _ _    条件码
T(then)、E(else)
T(then)、E(else)
GE、NE、EQ 等等
GE、NE、EQ 等等

IT 指令为后续的 1-4 条指令创建了一个条件执行块:

MOV R0, #5
CMP R0, #5           /* 比较 R0 和 5                         */
ITTEE EQ             /* 如果相等则执行前两条,否则执行后两条 */
MOVEQ R1, #1         /* R0 == 5 时执行                       */
MOVEQ R2, #2         /* R0 == 5 时执行                       */
MOVNE R3, #3         /* R0 != 5 时执行                       */
MOVNE R4, #4         /* R0 != 5 时执行                       */

在 CM3/4 中,代码中无需 IT 指令,CMP 会更新 APSR,后续指令靠条件后缀就行了,也许是为了兼容其他不支持条件后缀的处理器??? 书中的描述也很是模糊???先插个眼???

TBB 表格跳转字节(Table Branch Byte)

TBB [PC, 3]
TBB [PC, 3]
tab0 = 0x4
tab0 = 0x4
tab1 = 0x6
tab1 = 0x6
tab2 = 0x8
tab2 = 0x8
tab3 = 0xA
tab3 = 0xA
指令
指令
指令
指令
指令
指令
指令
指令
...
...
PC
PC
当前执行
当前执行
jumptable + 3
jumptable + 3
case0
case0
case1
case1
case2
case2
case3
case3
jumptable
jumptable
新 PC
新 PC
0x1000
0x1000
0x1004
0x1004
0x1005
0x1005
0x1006
0x1006
0x1007
0x1007
0x100C
0x100C
0x1010
0x1010
0x1014
0x1014
0x1018
0x1018
TBB [Rn, Rm]
TBB [Rn,...
Cortex-M3/4 中,读 PC 寄存器的返回值 = 当前指令地址 + 4
Cortex-M3/4 中,读 PC 寄存器的返回值 = 当前指令地址 + 4
tab3 = (case3 - jumptable)/2 = (0x1018 - 0x1004)/2 = 0xA
tab3 = (case3 - jumptable)/2 = (0x1018 - 0x1004)/2 = 0xA
= jumptable + tab3*2 = 0x1004 + 0xA*2 = 0x1018
= jumptable + tab3*2 = 0x1004 + 0xA*2 = 0x1018
Cortex-M3/4 支持 16 位指令
Cortex-M3/4 支持 16 位指令
指令按 2 字节边界对齐
指令按 2 字节边界对齐
指令地址的最后一位永远是 0
指令地址的最后一位永远是 0
所以除以 2 增加偏移寻址的范围
所以除以 2 增加偏移寻址的范围
TBB [PC, 3]
jumptable:
  .byte (case0 - jumptable)/2
  .byte (case1 - jumptable)/2
  .byte (case2 - jumptable)/2
  .byte (case3 - jumptable)/2

case0:
  /* case0 的代码 */
  B endswitch
case1:
  /* case1 的代码 */
  B endswitch
case2:
  /* case2 的代码 */
  B endswitch
case3:
  /* case3 的代码 */
endswitch:
  /* 后续代码     */

TBH 表格跳转字(Table Branch Halfword)

饱和运算 #

饱和运算是指当运算结果超出目标数据类型所能表示的范围时, 结果会被限制在该数据类型所能表示的最大值或最小值, 而不是像常规运算那样发生溢出

有符号饱和(Signed Saturate)SSAT Rd, #imm, Rn{, shift}shift 为算术右移 ASR 或逻辑左移 LSL
SSAT R1, #16, R0R0 的值饱和到 16 位有符号范围并存入 R1
SSAT R1, #8, R0, ASR #2R0 >> 2 的值饱和到 8 位有符号范围并存入 R1
无符号饱和(Unsigned Saturate)USAT Rd, #imm, Rn{, shift}
LDR  R0, =0x00020000
SSAT R1, #16, R0      /* R1 = 0x00007FFF */
USAT R2, #16, R0      /* R2 = 0x0000FFFF */

NOP 指令 #

用于指令对齐或延时,用 C 可写作 _NOP();

桶形移位器 #

一些数据处理指令可以在数据处理前选择移位操作,如 LDR Rd, [Rn, Rm, LSL #n]

.global _start
_start:
        ldr  r0, =my_array           /* my_array[0][0]                           */
        mov  r1, #2                  /* my_array[2][0]                           */
        ldrb r2, [r0, r1, lsl #3]    /* r2 = r0+(r1<<3) = my_array[2*8] = 0x20   */

.section .rodata
my_array:                            /* my_array[3][7]                           */
        .byte 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07
        .byte 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17
        .byte 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27
        .byte 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37

参考链接 #