汇编语言之ARM32汇编

ARM两种编译环境

两种常用的ARM的编译开发环境

  • ARM原生编译环境:ARM官方提供的原生编译环境,相关集成开发软件有ADS,Keil等,常用于ARM单片机开发
  • GNU编译环境: 由GNU的汇编器as,交叉编译器gcc,和链接器ld等组成,一般适用于交叉编译环境需求

以上两种编译环境,使用的指令集都是一致的, 只是语法格式有不同,也就是宏指令,伪指令,伪操作不一样

ARM原生环境搭建

使用 Keil μVision5 这款软件进行ARM32的汇编学习

下载地址:http://www.mcuzone.com/down/Software.asp?ID=10000503

ARM32系列命名

ARM产品 ARM架构
ARM7 ARM v4
ARM9 ARM v5
ARM11 ARM v6
Cortex-A ARM v7-A
Cortex-R ARM v7-R
Cortex-M ARM v7-M

寄存器

在ARM32中一共有37个寄存器,其中包含16个通用寄存器(R0~R15)和1个状态寄存器 ,15个通用影子寄存器,5个状态影子寄存器

影子寄存器

如上图所示,在ARM32中一共有7中不同的处理器模式,分别为:用户模式(User),快速中断模式(FIQ),普通中断模式(IRQ),管理模式(Svc),数据访问中止模式(Abort),未定义指令中止模式(Und),系统模式(Sys)

但是在不同的模式下,同样的一个寄存器名称指向不同的物理寄存器,这些不同的物理寄存器就被称之为影子寄存器

由于这些影子寄存器也属于通用寄存器的范畴, 因此很多人也将ARM32的通用寄存器归纳为31个

语法

  1. 注释(两种方式)

    1
    2
    3
    ; 我是注释

    /*…我是注释..*/
  2. 声明一个代码段

    1
    2
    3
    4
    5
    6
    7
    8
     	AREA test, CODE	 ;声明一个代码段,段的名称为test(名称可自定义),CODE关键字大小写都可,为了区分,一般大写

    ;=========在此编写汇编代码==========

    END ;END表示编译结束标记


    ;段的声明需要以制表符开头,前面留出空
  3. 数据表示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    mov R0,#13 ;将十进制13赋值给R0寄存器

    mov R0,#0x13 ;将十六进制0x13赋值给R0寄存器


    mov R0,#8_12 ;将8进制数12赋值给R0寄存器 N进制则为#N_xxx


    mov R0,#'a' ;将字符a对应的ascii码值传送给R0寄存器
  4. 函数声明和调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
      	AREA test, CODE


    bl print ;函数调用

    print ;函数名称
    mov R3,#5
    bx lr
    END ;END表示编译结束标记
  1. 声明一个数据段

    1
    2
    AREA da, DATA	 ;声明一个数据段 默认可读可写状态
    ;数据定义伪操作
  1. 数据定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # DCB   
    Str DCB "This is a test!" ;分配一片连续的字节存储单元并初始化。取名为Str 类似于8086中的db
    Str = "This is a test!" ;DCB也可用等号代替


    #DCW
    Str DCW 1,2,3 ;定义字型数据 每个字符占用2个字节的空间


    #DCD
    Str DCD 1,2,3 ;定义半字型数据 每个字符占用4个字节的空间

    字符串必须使用DCB进行定义

  2. 分配一块连续的内存空间

    1
    2
    3
    4
    sp1 SPACE 100 ;分配一块连续100个字节的空间

    ;或者使用%代替SPACE简写
    sp1 % 100 ;分配一块连续100个字节的空间

代码编写规范

  1. 所有指令和伪指令不允许顶格
  2. 所有变量和标签必须顶格
  3. 一般我们将伪指令大写,变量和标签小写

内存数据的读写

  1. 从内存中读取数据

    使用中括号表示通过地址取值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    LDR R0,[R1]               ;将内存地址为R1的字数据读入寄存器R0。
    LDR R0,[R1,R2] ;将内存地址为R1+R2的字数据读入寄存器R0。

    LDR R0,[R1,#8] ;将内存地址为R1+8的字数据读入寄存器R0。
    LDR R0,[R1,R2]! ;将内存地址为R1+R2的字数据读入寄存器R0,并将新地 址R1+R2写入R1。



    LDR R0,[R1,#8]! ;将内存地址为R1+8的字数据读入寄存器R0,并将新地址 R1+8写入R1。

    LDR R0,[R1],R2 ;将内存地址为R1的字数据读入寄存器R0,并将新地址 R1+R2写入R1。


    LDR R0,[R1,R2,LSL#2]! ;将内存地址为R1+R2×4的字数据读入寄存器R0,并将新地址R1+R2×4写入R1。


    LDR R0,[R1],R2,LSL#2 ;将内存地址为R1的字数据读入 寄存器R0,并将新地址R1+R2×4写入R1。


    ;从标号即为地址
    LDR R0,label ;将标号对应的内容赋值给R0

复杂格式如LDR R0,[R1],R2,LSL#2 其中 []运算优先
2. 向内存中写入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
STR R0,[R1],#8           ;将R0中的字数据写入以R1为地址的内存中,并 将新地址R1+8写入R1。


STR R0,[R1,#8] ;将R0中的字数据写入以R1+8为地址的内存中。

STRB R0,[R1] ;将寄存器R0中的字节数据写入以R1为地 址的内存中。
STRB R0,[R1,#8] ;将寄存器R0中的字节数据写入以R1+8为地址的存 储器中。
STR R0,[R1],#8 ;将R0中的字数据写入以R1为地址的内存中,并 将新地址R1+8写入R1。


STR R0,[R1,#8] ;将R0中的字数据写入以R1+8为地址的内存中。

STRB R0,[R1] ;将寄存器R0中的字节数据写入以R1为地 址的内存中。
STRB R0,[R1,#8] ;将寄存器R0中的字节数据写入以R1+8为地址的存 储器中。

LDR伪指令

这个指令和内存读取指令长的一模一样,如果我们在使用的时候加个等号,那么它就是另外一个指令

1
2
3
4
5
6
;如果不加等号 是内存读取功能
LDR R0,label ;获取标签所对应的内存数据赋给R0

;一旦加了等号,则变成了传送指令
LDR R0,=label ;将标号对应的实际物理地址值赋值给R0 此时它的作用和mov无异
LDR R0,='a' ;直接将字符数据传送给R0

实际上,加了等号的LDR指令,刚好可以弥补mov指令的不足, mov指令只能传送由八个二进制位右移而得的数据,而LDR则没有这个限制

也就是说如果我们想将一个数值传入寄存器,可以有两种方式:

1
2
3
4
5
6
7
8
9
10
;第一种
mov R1,#0X100

;第二种
ldr R1,=0x100

;mov指令的限制:只能传送由八个二进制位右移而得的数据, 也就相当于是两个十六进制数据,由于可以不断移位那么数据的大小可以伸缩,比如以下数据都可使用mov指令
0x00000058 0x00000580 0x00005800 0x00058000 0x00580000 0x05800000 0x58000000
;我们发现一个规律:mov指令只能传送最大两个十六进制空间的数据,注意是空间,这两个数据随意你移动,一旦不满足这个条件则无法传送,比如
0x00000581

LDR伪指令总结

作用:

  • 弥补mov指令的不足
  • 获取数据所对应的内存地址

ADR指令

那么除了通过LDR伪指令来获取数据所在地址外还有一个指令也可以获取数据地址,那就是adr指令,但这个指令只能获取当前段内数据的地址,段外数据无法获取,ldr则没有这个限制

1
2
3
4
5
	AREA test2,CODE
mov R3,#8
aaaa dcb 1,2,3
adr R0,aaaa ;获取aaaa首地址
END

段的拓展

段属性拓展

  1. 段读写属性
  • READONLY

    该段内存区域数据只能读取,不能写入,也就是如果使用内存读写指令,数据也写入不了

  • READWRITE

    该段内存区域可读可写,不仅可以使用内存读写指令,还可以在调试的时候直接在memory窗口双击修改

示例:

1
2
AREA code, CODE,READWRITE ;将代码段内存区域设置为可读可写状态,如果不写默认为只读
AREA code, DATA,READONLY ;将数据段内存区域设置为只读状态,如果不写默认为可读可写
  1. 段对齐属性 ALIGN

    1
    AREA code, DATA,ALIGN=3  ;对齐数值范围为0~31

    该属性会使得该段的基地址进行相应的偏移,ALIGN=3表示基地址会在上一个段数据的基础之上偏移2^3=8个字节的位置

    1
    2
    3
    4
    5
    6
    7
    8
       	 AREA test1,DATA  ;假设这个段的基地址为0x00000100
    STR1 = "A"

    AREA test2,DATA ;ALIGN默认为2 那么这个段的基地址为0x00000104
    STR2 = "B"

    AREA test3,DATA,ALIGN=8 ;这个段的基地址为0x0000010C
    STR2 = "B"

    我们可以简单理解为,使用ALIGN这个属性可以让我们给上一个段预留除一部分缓冲区域,以ALIGN=2为例,当上一个段中的数据超过4个字节时,当前段基地器会向后再偏移4个字节,避免数据被覆盖,也就是说内存数据位置会进行重新分布,那么我们可以通过这个值来设置内存数据刷新频率,值越低,内存利用率越高,但是内存刷新频率也越高,负荷加重,反之,内存浪费越大,但是内存数据不需要频繁重新分布

    另外:

    除了在段属性中可以设置对齐之外,在指令中也可以插入ALGIN关键字:

    1
    2
    3
    4
    5
    6
    7
    8
    AREA code,CODE	   

    mov R0,R1

    ALIGN 4,3 ;下一条指令4字节对齐,并且偏移3个字节 为了补满4个字节,用0填充剩余1个未偏移位置
    mov R2,R0

    END

代码中使用AGLIN时用空格代替等号,同时单位为字节

多个代码段入口区分

当我们在同一个源文件中定义两个代码段时,程序从哪个段当做执行入口呢?

这个时候我们需要指定程序的入口,使用伪指令entry

1
2
3
4
5
6
7
8
9
10
 AREA test2,CODE 

mov R0,#7

AREA test3,CODE
entry ;程序入口
mov R0,#6
mov R1,#0x00000100
str R0,[R1]
END

栈的操作

  1. 入栈
1
2
3
4
5
push {R0}  ;将R0中的值存入栈内存中  相当于是STR R0,[R13,#-4]

入栈过程:
* 第一步:将栈顶指针往低地址偏移四个字节
* 第二步:将数据存入指针指向的内存空间
  1. 出栈
1
2
3
4
5
pop {R0}  ;将栈顶中的值取出存入R0寄存器 相当于是LDR R0,[R13],#0x0004

出栈过程:
* 第一步:将栈顶指针指向的内容取出存入寄存器
* 第二步:将指针往高地址恢复四个字节

pop和push 本质上使用的是LDR和STR内存读写指令

对栈批量操作

如果想批量操作多个连续栈空间的话,直接使用逗号分隔开,连续标号的寄存器使用横杠分隔

1
2
3
4
push {R0,R4-R12,LR} ;大括号中寄存器从右往左LR R12...R4 R0依次存入


pop {R0,R4-R12,PC} ;从左往右取出

除了使用poppush之外,可以使用STM(store much)和LDM(load much)指令

格式:

1
STM 起始地址/基地址寄存器,{寄存器名称,多个寄存器以逗号或者-分割} ;起始地址寄存器R0-R14任意选择

示例:

1
2
3
4
5
6
7
8
9
STMFD  R13!,{R0,R4-R12,LR}    ;将寄存器列表中的寄存器(R0,R4到 R12,LR)存入栈,。
等价于
push {R0,R4-R12,LR} ;大括号中寄存器从右往左存入



LDMFD R13!,{R0,R4-R12,PC} ;将栈内容恢复到寄存器(R0,R4到R12,LR)。
等价于
pop {R0,R4-R12,PC} ;从左往右取出

poppush,它们内部也是转成STMLDM指令:

批量存取指令扩展

相关后缀含义:

  • IA:(Increase After):数据操作后基地址增4
  • IB:(Increase Before):数据操作前基地址增4
  • DA:(Decrease After):数据操作后基地址减4
  • DB:(Decrease Before):数据操作前基地址减4
  • FD: 满递减堆栈 (相当于STMDB+LDMIA)
  • FA: 满递增堆栈 (相当于STMIB+LDMDA)
  • ED: 空递减堆栈(相当于STMDA+LDMIB)
  • EA: 空递增堆栈 (相当于STMIA+LDMDB)

我们在使用的时候,要么使用结合的形式比如STMDB+LDMIA,要么直接使用封装形式比如STMFD+LDMFD

满栈和空栈

栈的生长方式可以有四种: 满增栈、满减栈、空增栈、空减栈

  • 满栈(full stack):栈指针指向下一个将要取出数据的位置,数据入栈时,栈顶指针先偏移再入栈,数据出栈是,先取数据,后指针偏移。
  • 空栈(empty stack):栈指针指向下一个将要放入数据的位置,数据入栈时,先存数据后指针偏移,数据出栈时,先指针偏移,后数据取出
  • 递增堆栈(ascending stack):堆栈由低地址向高地址生长。
  • 递减堆栈(secending stack):堆栈由高地址向低地址生长。

X86和mips架构都是采用满递减堆栈方式处理栈空间,ARM架构四种方式均支持

内存批量读写示例:

1
2
3
4
5
mov  R1,#4
mov R2,#5
mov R0,#0x00000008
stm R0,{R1,R2}
;以上四行代码表示 从0x00000008这个内存地址开始 将R1和R2中的数据依次存入

如果我想在上面的基础上再往后追加数据

1
2
3
mov  R1,#6
mov R2,#7
stm R0,{R1,R2}

我们发现数据并没有追加,而是被覆盖了,因为R0的值依然还是 0x00000008,这个时候我们需要使用扩展指令,如下:

1
2
3
4
5
6
7
8
mov  R1,#4
mov R2,#5
mov R0,#0x00000008
STMIA R0!,{R1,R2} ;只有在寄存器后加上!才能修改寄存器中的值

mov R1,#6
mov R2,#7
stm R0,{R1,R2}

事实上 STM指令如果不加后缀写法,默认使用的是STMIA指令,LDM指令默认使用LDMIA

多寄存器数据存放顺序

不管使用哪种扩展指令,皆为左低右高的形式,也就是左边的寄存器数据存放在低地址,右边的存放在高地址

1
2
3
4
STMIA  R0!,{R1,R2} ;左边R1的内容放低地址,右边R2的内容放高地址


LDMDB R0!,{R1,R2} ;高地址数据放入R2,低地址数据放入R1

  1. 宏匹配

    • 语法格式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    MACRO
    $label macroname $param
    ;指令序列
    MEND

    ;例如声明一个宏名为print的宏语句
    MACRO
    $label print $param ;带$的表示会被替换的内容
    ;这里写相关汇编代码
    MEND
    • 使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
     MACRO
    $label putR0 $param

    mov R0,$param

    MEND

    ; 使用
    putR0 #10
  • 延伸

第一个$label是干嘛用的呢,由于宏的内部处理方式的替换,为了避免标签名称的冲突,增加一个标识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
;假如我要在宏匹配中定义一个函数fun, 当我调用两次的时候,会出现函数名重复的问题
MACRO
$label putR0 $param
fun
mov R0,$param

MEND



putR0 #10
putR0 #10

;那么如果要解决这个问题的话,我可以利用第一个替换参数如下:
MACRO
$label putR0 $param
$label
mov R0,$param

MEND



fun1 putR0 #10 ;函数名为fun1
fun2 putR0 #10 ;函数名为fun2
  1. 宏定义

    • 全局宏的定义,可跨段访问
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ;全局数字变量
    GBLA number
    number SETA 0Xaa

    ;全局逻辑变量
    GBLL flag
    flag SETL {TRUE}


    ;全局字符串变量
    GBLS str
    str SETS "hello world"
    • 局部宏的定义,只能在当前宏内被访问
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ;局部数字变量
    LCLA number
    number SETA 0Xaa

    ;局部逻辑变量
    LCLL flag
    flag SETL {TRUE}


    ;局部字符串变量
    LCLS str
    str SETS "hello world"
    • 全局常量的定义
    1
    num   EQU      10  ;关键字EQU  数据不允许修改
  1. 宏定义示例

    1
    2
    3
    4
    5
    6
    7
    ;定义一个全局整型数据宏
    GBLA number
    number SETA 0Xaa

    ;使用
    mov R0,#number ;相当于mov R0,#0Xaa
    LDR R0,=number ;相当于LDR R0,=0Xaa

    局部宏数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    MACRO ;声明一个宏
    $label message $a ;取名为message 参数为$a

    ;定义宏内局部变量
    LCLA number
    number SETA $a

    mov R0,#number


    MEND ;宏结束,局部变量不再起作用


    message 10 ;直接调用

    常量数据

    1
    2
    3
    4
    5
    6
    7
    8
    	AREA data ,DATA
    num EQU 10
    AREA code ,CODE

    mov R1,#5
    ldr R1,=num

    END

宏匹配和宏定义需要遵循先定义后使用的原则

如果需要从宏中跳出,可以使用伪指令MEXIT

  1. 宏替换

    使用include或者get伪指令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
       ;使用get伪指令
    AREA code, CODE
    GET pangshu.s ;通知编译器当前源文件包含源文件 softool.s
    GET C:\pp.s ;通知编译器当前源文件包含源文件 C:\cn.s
    END

    ;使用include伪指令
    AREA code, CODE
    include pangshu.s ;通知编译器当前源文件包含源文件 softool.s
    include C:\pp.s ;通知编译器当前源文件包含源文件 C:\cn.s
    END

指令学习

传送指令

  1. 正常传送指令mov

    1
    mov R0,#4 ;将4传送至R0寄存器
  2. 取反传送指令mvn,也叫数据非传送指令

    1
    mvn R0,#4 ;将4取反后传送至R0寄存器  0100取反为1011

转移指令

  1. B指令

直接跳转,仅更改PC寄存器的值

示例:

1
2
3
B 0x00040000 ;直接跳转到物理地址0x00040000读取指令并执行

B 标号 ;直接跳转到标号处
  1. BL指令

跳转并链接,除了更改PC寄存器的值之外,还会将下一条指令所对应的物理地址存放至lr寄存器中

示例:

1
2
3
4
5
6
BL 0x00040000
mov r1,3 ;假设这行指令对应物理地址为0x0040004, 那么BL一旦执行,会将该值存入lr寄存器


或者
BL 标号
  1. BX指令

    跳转并切换状态

    BX指令后面只能跟寄存器,弥补了B指令和BL指令的不足, 同时会根据寄存器中最低比特位值切换ARM/Thumb模式

示例:

1
2
3
4
5
6
BL print


print
mov r1,#1
BX lr ;函数返回 如果R0[0]=1,则进入Thumb状态 反之进入ARM模式

除了通过指令来更改PC寄存器值之外,在ARM32中还可以直接使用传送指令对PC寄存器进行赋值:

1
2
3
mov pc,#0x00000008  ;往pc寄存器中写入一个地址值

mov R0,pc ;获取pc寄存器中的值
  1. BLX指令

    该指令将以下功能集于一身

    • 更改PC寄存器的值
    • 将下一条指令的地址存入lr寄存器
    • 根据寄存器中最低比特位值切换ARM/Thumb模式

算术和逻辑运算指令

  1. 算术运算

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    add R0,#5
    add R0,R1 ;加法


    sub R0,#5
    sub R0,R1 ;减法

    mul R0,R1,R2 ;乘法指令, 这里至少三个寄存器参与

    mla R0,R1,R2,R3 ;先乘后加 R0 = R1 × R2 + R3
  2. 逻辑运算

    1
    2
    3
    4
    5
    6
    7
    8
    and R0.#3
    and R0,R0 ;逻辑与

    orr R0.#3
    orr R0,R0 ;逻辑或

    eor R0.#3
    eor R0,R0 ;逻辑异或

移位指令

1
2
3
MOV   R0, R1, LSL#2    ;将R1中的内容左移两位后传送到R0中。

MOV R0, R1, LSR#2 ;将R1中的内容右移两位后传送到R0中,左端用零来填充。

比较指令

  1. 比较两个值是否相等

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    cmp R0,R1 
    beq sub ;如果两个寄存器中的值相等则跳转到sub函数中,否则继续往下执行

    sub


    cmp R0,#5
    bne sub ;如果两个值不相等,跳转到sub函数,否则继续往下执行

    sub
  2. 大于和小于(带符号)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ;大于
    cmp R0,R1
    bgt sub ;如果R0寄存器中的值大于R1,则跳转至sub

    sub

    ;小于
    cmp R0,R1
    blt sub ;如果R0寄存器中的值大于R1,则跳转至sub

    sub

标志寄存器

试想一下,我们的比较指令cmp,它内部是如何进行数据大小判断的

在高级语言里,直接使用>或者<运算符,来判断两个值的大小,比较结束后返回True或者Flase,可是在汇编语言里面没有这么简便,那它又是如何对两个数据之间大小进行判断的呢?

别忘了, 计算机最擅长做二进制的算术和逻辑运算

1
cmp R0,R1

要想判断两个数据是否相等,或者大于小于,直接做个减法运算不就完事了,也就是R0-R1,如果结果为0,那么两个值相等,如果结果为正数,则R0>R1,结果为负数,则小于

但是问题来了,这个结果值放在哪里呢?放内存中还是寄存器呢? 答案是:寄存器

cpu设计者为了方便区分专门用了一个寄存器来存放数据运算后的结果,这个寄存器叫做状态寄存器,也叫标志寄存器

ARM32中一个寄存器有32二进制位的数据空间,那该怎么存放呢?

1
00000000000000000000000000000000  ;32个二进制位

为了方便程序员开发,设计者给这些二进制位进行了相应的命名:

当两个比较值相等,进行减法运算时,结果为0,那么Z标志位的值为1,也就是

1
01000000000000000000000000000000  ;32个二进制位

如果不相等,结果不为0,那么Z标志位的值变成0

由于每个二进制位只能存0和1两个值,也就是最多只能表示两种状态,那大于和小于的状态表示就得放到另外一个二进制上了,由于二进制运算涉及到有符号和无符号两种情况,因此需要用到两个二进制分别进行处理,有符号的的结果存放在N标志位,无符号的结果存放在C标志位:

cmp指令会同时对两个数据进行有符号和无符号运算

有符号运算,如果结果为正数,N标志位值为0,如果为负数,N标志位值为1

无符号运算,如果结果为正数,C标志位值为1,如果为负数,C标志位值为0

那么我们在使用cmp指令的时候,到底是根据那个标志位的结果进行判断的呢?

如果我们使用bne指令,那么取Z标志位的值进行参考

如果我们使用blt,bgt,那么取N标志位,Z标志位和V标志位三者的值进行参考

总结:

  1. cmp指令的功能相当于减法指令,只是对操作数之间运算比较,结果间接保存在标志寄存器高位中

  2. bne,blt,bgt等这些指令都是通过获取标志寄存器中的值来得知比较结果从而进行相应跳转,不同的指令需要满足不同的条件

  3. 我们可以通过改变状态寄存器中的值来改变代码的走向

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

AREA test,CODE

mov R0,#5
mov R1,#6
cmp R0,R1
;在跳转之前改变状态寄存器的值 使得bgt必然跳转
MSR cpsr_f ,0x20000000
BGT fun
mov R1,#6

fun
mov R0,#4
bx lr


END

知识扩展:

  1. 状态寄存器的读取和写入

    读取指令:MRS{mov to register from special register)

    写入指令: MSR(Move to Special register from Register )

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 读取状态寄存器中的值
    mrs R0,cpsr ;将值读取到R0寄存器


    #修改状态寄存器中的值
    msr cpsr_c,#0x2d ;修改控制位区域
    msr cpsr_x,#0x2d00 ;修改扩展位区域
    msr cpsr_s,#0x2d0000 ;修改状态位区域
    msr cpsr_f,#0x2d000000 ;修改标志位区域

    #修改状态寄存器高位值时,低位必须补足0,虽然加了0但是不会影响其他区域的值
  2. 状态寄存器中各个区域具体描述

    一共分位四个大区域,从低到高分别为:控制位区域, 扩展位区域, 状态位区域, 标志位区域,每个区域各占8个二进制位的空间

    以下是控制位区域细分详解图:

  1. 比较指令标志位条件参考表
指令 含义 需要满足的条件
beq 相等 Z标志位为1
bne 不相等 Z标志位为0
bgt 带符号大于 Z标志位为0,且N和V标志位值相等
blt 带符号小于 N不等于V
bge 带符号大于等于 N等于V
ble 带符号小于等于 Z标志位为1或者N不等于V
bls 无符号小于等于 Z标志位为1且C标志位为0
bhi 无符号大于 Z标志位为0且C标志位为1
bcs 无符号大于等于 C标志位为1
bhs 无符号大于等于 C标志位为1
bcc 无符号小于 C标志位为0
blo 无符号小于 C标志位为0
bmi 负数 N标志位为1
bpl 正数或零 N标志位为0
bvs 溢出 V标志位为1
bvc 未溢出 V标志位为0
bnv 无条件执行 忽略
bal 无条件执行 忽略

条件和循环伪指令

IF、ELSE 和 ENDIF

  • 根据条件的成立与否决定是否执行某个程序段

  • IF、ELSE、ENDIF 伪指令可以嵌套使用

  • 1
    2
    3
    4
    5
    6
    7
    8
    GBLL Test ;声明一个全局逻辑变量Test
    Test SETL {TRUE}

    IF Test = {TRUE}
    程序段1
    ELSE
    程序段2
    ENDIF

WHILE 和 WEND

  • 根据条件的成立与否决定是否重复汇编一个程序段

  • 若 WHILE 后面的逻辑表达式为真,则重复汇编该程序段,直到逻辑表达式为假

  • WHILE 和 WEND 伪指令可以嵌套使用

  • 1
    2
    3
    4
    5
    6
    GBLA Counter  ;声明一个全局数字变量Counter
    Counter SETA 3 ;赋值
    ...
    WHILE Counter < 10
    程序段
    WEND

汇编语言和C语言交互

  • 内嵌汇编
  • 外链汇编

1.引入其他源文件函数

使用import或者extern伪指令

1
2
3
4
5
6
7
8
9
;使用import伪指令
AREA code, CODE
import fun1 ;导入其他源文件中名为fun1的函数
END

;使用extern伪指令
AREA code, CODE
extern fun1
END

两者区别:

  • import:不管当前文件是否使用该引入的函数,该标签都会加入当前文件符号表,即为静态引用
  • extern:只有当前文件使用了该函数,才会将此标签加入符号表,即为动态引用

2.导出当前源文件中函数供其他文件访问

使用export或者global伪指令

1
2
3
4
5
6
7
8
;使用import伪指令
AREA code, CODE
export fun ;导出fun函数供其他源文件使用

fun
mov R0,#4
bx lr
END

3.外链汇编之C语言调汇编函数

第一步,在汇编原文件中将函数暴露出来给供外部调用,使用export或者global伪指令:

1
2
3
4
5
6
7
8
9
10
11
12
AREA code, CODE
export arm_strcpy ;或者使用global

arm_strcpy
loop
ldrb R4,[R0],#1 ;如果使用ldr 那么将偏移值改成4
cmp R4,#0
beq over
strb R4,[R1],#1
b loop
over
END

第二步,在C文件中引用汇编中的函数,C文件中只能使用extern伪指令:

1
2
3
4
5
6
7
extern arm_strcpy(char *src,char*des);

int main2(){
char *a="hello pangshu";
char b[64];
arm_strcpy(a,b);
}

4.外链汇编之汇编调c语言函数

第一步,在C文件中编写好函数

1
2
3
int c_sum(int a,int b){
return a+b;
}

第二步, 在汇编文件中引入函数,使用import或者extern伪指令

1
2
3
4
5
6
7
AREA code, CODE
import c_sum

mov R0,#1 ;第一个参数
mov R1,#2 ;第二个参数

END

第三步, 使用BL指令调用函数

1
2
3
4
5
6
7
AREA code, CODE
import c_sum

mov R0,#1 ;第一个参数
mov R1,#2 ;第二个参数
BL c_sum
END

在ARM中函数参数使用R0~R3这四个寄存器来进行传递,最多传递4个参数,超过4个参数使用栈进行处理,函数返回值通过R0进行传递

由于keil软件的特殊性,我们可以通过以下方式进行互调测试

C文件中代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
extern arm_strcpy(char *src,char*des);

int main2(){

char *a="hello pangshu" ;

char b[64];

arm_strcpy(a,b); //调汇编中函数
return 0;
}

int c_sum(int a,int b){
return a+b;
}

汇编文件中代码:

1
2
3
4
5
6
7
8
9
10
11
 AREA code, CODE

import c_sum
export arm_strcpy

arm_strcpy

mov R0,#1 ;第一个参数
mov R1,#2 ;第二个参数
BL c_sum ;结果存放至R0中
END

5.内嵌汇编

在C语言中嵌入汇编代码,格式如下:

1
2
3
4
5
6
7
8
9
10
11
int main2(){
int a=4;
int b=4;
int c=4;

__asm__{ //使用__asm或者__asm__
mov R5,#0x00000005 //在大括号内部直接写入汇编代码即可
mov R6,#0x00000005
}
return 0;
}

内嵌汇编的注意事项:

  • 不能直接给PC寄存器赋值,如果想改变pc值需要借助转移指令
  • 由于R0R3用于存放函数参数和返回值,R12R15有特殊用途,因此我们能操作的寄存器只有R4~R11, 又因为编译器会优先将寄存器分配给函数中的局部变量,因此我们一般无法在内嵌汇编环境中准确地修改某个寄存器的值,比如我想修改R5寄存器的值,由于函数有个变量占用了R5这个寄存器,那么编译器会自动将你写的这个R5改成R6或者其他,所以,在内嵌汇编时我们需要把寄存器当作变量来看待,把局部变量也当成寄存器看待,就好理解了
1
2
3
4
5
6
7
8
9
10
11
void c_strcopy(char *src,char *des){
char ch
__asm__{
loop:
ldrb ch,[src],#1 //局部变量当成寄存器看待
strb ch,[des],#1
cmp,ch,#0
bne loop
}

}

ARM32中寄存器别名补充

寄存器 别名 用途
r0 a1 第一个函数参数和函数返回值
r1 a2 第二个函数参数
r2 a3 第三个函数参数
r3 a4 第四个函数参数
r4 v1 寄存器变量
r5 v2 寄存器变量
r6 v3 寄存器变量
r7 v4 寄存器变量
r8 v5 寄存器变量
r9 v6 寄存器变量 实际的帧指针
r10 sl 栈接线
r11 fp 参数指针
r12 ip 临时
r13 sp 栈指针
r14 lr 连接寄存器
r15 pc 程序计数器

如何编译16位arm汇编指令

1
2
3
4
AREA test, CODE
code16 ;声明为16位arm指令 如果不写默认则为code32

END

附:指令集汇总

(一) ARM 指令集

1. 指令格式

2. 条件码

3. ARM 存储器访问指令

1) LDR/ STR -加载 /存储指令

2) LDM/ STM -多寄存器加载 /存储指令

3) SWP -寄存器和存储器交换指令

4. ARM 数据处理指令

1) 数据传送指令

a) MOV -数据传送指令

b) MVN -数据非传送指令

2) 算术逻辑运算指令

a) ADD -加法运算指令

b) SUB -减法运算指令

c) RSB- 逆向减法指令

d) ADC -带进位加法指令

e) SBC -带进位减法指令

f) RSC -带进位逆向减法指令

g) AND -逻辑“与”

h) ORR -逻辑“或”

i) EOR -逻辑“异或”

j) BIC -位清除指令

3) 比较指令

a) CMP -比较指令

b) CMN -负数比较指令

c) TST -位测试指令

d) TEQ -相等测试指令

4) 乘法指令

a) MUL - 32位乘法指令

b) MLA - 32位乘加指令

c) UMULL - 64位无符号乘法指令

d) UMLAL - 64位无符号乘加指令

e) SMULL - 64位有符号乘法指令

f) SMLAL - 64位有符号乘加指令

5. ARM 分支指令

1) B -分支指令

2) BL -带连接的分支指令

3) BX -带状态切换的分支指令

6. ARM 协处理器指令

1) CDP -协处理器数据操作指令

2) LDC -协处理器数据读取指令

3) STC -协处理器数据写入指令

4) MCR - ARM处理器到协处理器的数据传送指令

5) MRC -协处理器到 ARM处理器的数据传送指令

7. ARM 杂项指令

1) SWI -软中断指令

2) MRS -读状态寄存器指令

3) MSR -写状态寄存器指令

8. ARM 伪指令

1) ADR -小范围的地址读取伪指令

2) ADRL -中等范围的地址读取伪指令

3) LDR -大范围的地址读取伪指令

4) NOP -空操作伪指令

(二) Thumb 指令集

1. Thumb 指令集和 ARM指令集的区别

2. Thumb 存储器访问指令

1) LDR/ STR -加载 /存储指令

2) PUSH/ POP -寄存器入栈 /出栈指令

3) LDMIA/ STMIA -多寄存器加载 /存储指令

3. Thumb 数据处理指令

1) 数据传送指令

a) MOV -数据传送指令

b) MVN -数据非传送指令

c) NEG -数据取负指令

2) 算术逻辑运算指令

a) ADD -加法运算指令

b) SUB -减法运算指令

c) ADC -带进位加法指令

d) SBC -带进位减法指令

e) MUL -乘法运算指令

f) AND -逻辑“与”

g) ORR -逻辑“或”

h) EOR -逻辑“异或”

i) BIC -位清除指令

j) ASR -算术右移指令

k) LSL -逻辑左移指令

l) LSR -逻辑右移指令

m) ROR -循环右移指令

3) 比较指令

a) CMP -比较指令

b) CMN -负数比较指令

c) TST -位测试指令

4. Thumb 分支指令

1) B -分支指令

2) BL -带连接的分支指令

3) BX -带状态切换的分支指令

5. Thumb 杂项指令

1) SWI -软中断指令

6. Thumb 伪指令

1) ADR -小范围的地址读取伪指令

2) LDR -大范围的地址读取伪指令

3) NOP -空操作伪指令

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

0%