ARM体系架构

ARM体系架构

计算机基础:

嵌入式系统分层:

img

应用开发:

之前学的课程,应用层开发,使用操作系统提供的接口(API),做上层的应用程序的开发,基本不用去管内核操作硬件是怎么实现的。

底层开发:

做操作系统本身的开发

Linux层次结构:

img

* Linux子系统

1.进程管理:管理进程的创建、调度、销毁等 Process management

2.内存管理:管理内存的申请、释放、映射等 Memory management

3.文件系统:管理和访问磁盘中的文件 Filesystem support

4.设备管理:硬件设备及驱动的管理 Device control

5.网络协议:通过网络协议栈(TCP、IP...)进行通信 Networking

课程内容:

img

学习方法:

img

计算机基础知识:

进制:

img

高低电平信号是进制在计算机中的本质体现

计算机组成(冯诺依曼模型):

img

输入设备:

将其他信号转换为电信号(二进制)被计算机所识别,比如摄像头,可以将光信号转为电信号

输出设备:

将二进制电信号转换为其他信号,比如显示器

存储器:

存储器是用来存储程序和数据的部件,是实现"存储程序控制"的基础

如内存、硬盘等

CPU内部不能存放大量的指令数据,需要存放在存储器中,CPU做运算时,可以直接从存储器取出

CPU:包含:运算器、控制器:

运算器:

主要做一些数据算术运算或者逻辑运算,比如加减乘除、if...else 、for等

CPU中负责进行算数运算和逻辑运算的部件,其核心是算术逻辑单元ALU

控制器:

控制器是CPU的指挥中心,其控制着整个CPU执行程序的逻辑过程,好比是领导,控制指挥让别人做什么

总线:

* 总线

总线是计算机中各个部件之间传送信息的公共通信干线, 在物理上就是一束导线按照其传递信息的类型可以分为数据总线、地址总线、控制总线

* DMA总线

DMA(Direct Memory Access)即直接存储器访问,使用DMA总线可以不通过CPU直接在存储器之间进行数据传递

img

多级存储结构:

img

注意CPU只能访问内存和高速缓存

* Cache

速度最快、价格最贵、容量最小、断电数据丢失、cpu可直接访问

存储当前正在执行的程序中的活跃部分,以便快速地向CPU提供指令和数据

* 主存储器

速度、价格、容量介于Cache与辅存之间、断电数据丢失、cpu可直接访问

存储当前正在执行的程序和数据

* 辅助存储器

速度最慢、价格最低、容量最大、断电数据不丢失、cpu不可直接访问

存储暂时不运行的程序和数据,需要时再传送到主存

地址空间:

img

CPU通过地址总线向内存传递一个地址,通知内存我要取那个地址的数据,内存就会将该地址中的数据取出,发给CPU

所谓地址空间就是与地址总线关联的内存空间,如上图,假如地址总线就2根,那么地址空间就是2的2次方,如果地址总线的数目为N,那么CPU所能访问的地址空间就是2的N次方,对于32位系统来说就是2的32次方,即4G大小。

* 地址空间

一个处理器能够访问(读写)的存储空间是有限的,我们称这个空间为它的地址空间(寻址空间),一般来说N位地址总线的处理器的地址空间是2的N次方

img

CPU工作原理:

img

CPU运算器运算都是在电路里完成的

1.取指

2.译码

3.执行

一条指令执行完后,指令计数器PC会自动增加,再去取指、译码、执行..........

更为详细的摘图:

img

扩展:

cpu怎样对存储器进行读写:

https://www.jianshu.com/p/5c6cc166e880


ARM体系结构理论基础:

ARM处理器概述:

* ARM公司

> 成立于1990年11月,前身为Acorn计算机公司

> 主要设计ARM系列RISC处理器内核

> 授权ARM内核给生产和销售半导体的合作伙伴,ARM公司并不生产芯片

> 提供基于ARM架构的开发设计技术软件工具、评估板、调试工具、应用软件

总线架构、外围设备单元等

ARM产品系列:

img

* RISC处理器(精简指令集)

只保留常用的的简单指令,硬件结构简单,复杂操作一般通过简单指令的组合实现,一般指令长度固定,且多为单周期指令

RISC处理器在功耗、体积、价格等方面有很大优势,所以在嵌入式移动终端领域应用极为广泛

* CISC处理器(复杂指令集)

不仅包含了常用指令,还包含了很多不常用的特殊指令,硬件结构复杂,指令条数较多,一般指令长度和周期都不固定

CISC处理器在性能上有很大优势,多用于PC及服务器等领域

ARM指令集概述:

* 指令

能够指示处理器执行某种运算的命令称为指令(如加、减、乘 ...)

指令在内存中以机器码(二进制)的方式存在

每一条指令都对应一条汇编

程序是指令的有序集合

* 指令集

处理器能识别的指令的集合称为指令集

不同架构的处理器指令集不同(比如ARM与X86)

指令集是处理器对开发者提供的接口

CPU能够识别且执行的才叫做指令,比如33不能直接被CPU识别,经编译器编译后生成3+3+3,加法可以被CPU识别并执行,所以+是指令,不是

ARM指令集:

img

img

机器码可读性差、不方便维护、不可移植,

汇编语言本质是用一个符号去代替一条指令,一条汇编对应一条指令,但是对于C语言,一条C程序可能对应多条指令

编译原理:

img

为什么说汇编语言的可移植性差?

首先不同处理器的机器码不同,或者说同一条指令在不同的处理器下的内存表现形式:二进制机器码不同,根据上上个图可以看到:同样是加法,X86的机器码为0101,而ARM下为1100,如果把X86的机器码直接拿过去用到ARM上,则无法识别。而汇编就是指令机器码的符号化,与机器码是一一对应的,故也不可移植

为什么C语言不区分处理器架构?

这和C语言本身没有太大的关系,和使用编译器有关,不同的编译器可以将C源文件编译成不同架构处理器下的汇编版本

gcc -S test.i -o test.s

补充:

gcc的编译流程分为四个步骤,分别为:

・ 预处理(Pre-Processing)

・ 编译(Compiling)

・ 汇编(Assembling)

・ 链接(Linking)

下面就具体来查看一下gcc是如何完成四个步骤的。

hello.c源代码

#include<stdio.h>

int main()

{

printf("Hello World!\n");

return 0;

}

(1)预处理阶段

在该阶段,编译器将上述代码中的stdio.h编译进来,并且用户可以使用gcc的选项”-E”进行查看,该选项的作用是让gcc在预处理结束后停止编译过程。还要将以#开头的宏进行展开、删除注释等操作。

《深入理解计算机系统》中是这么说的:

预处理器(cpp)根据以字符#开头的命令(directives),修改原始的C程序。如hello.c中#include <stdio.h>指令告诉预处理器读系统头文件stdio.h的内容,并把它直接插入到程序文本中去。结果就得到另外一个C程序,通常是 以.i作为文件扩展名的。

注意:

Gcc指令的一般格式为:Gcc [选项] 要编译的文件 [选项] [目标文件]

其中,目标文件可缺省,Gcc默认生成可执行的文件名为:编译文件.out

[gan@localhost gcc]# gcc –E hello.c –o hello.i

选项”-o”是指目标文件,”.i”文件为已经过预处理的C原始程序。以下列出了hello.i文件的部分内容:

typedef int (*gconv_trans_fct) (struct gconv_step *,

struct __gconv_step_data *, void *,

__const unsigned char *,

__const unsigned char **,

__const unsigned char *, unsigned char **,

size_t *);

# 2 "hello.c" 2

int main()

{

printf("Hello World!\n");

return 0;

}

由此可见,gcc确实进行了预处理,它把”stdio.h”的内容插入到hello.i文件中。

(2)编译阶段

接下来进行的是编译阶段,在这个阶段中,1.Gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,2.在检查无误后,Gcc把代码翻译 成汇编语言。用户可以使用”-S”选项来进行查看,该选项只进行编译而不进行汇编,生成汇编代码。汇编语言是非常有用的,它为不同高级语言不同编译器提供 了通用的语言。如:C编译器和Fortran编译器产生的输出文件用的都是一样的汇编语言。

[gan@localhost gcc]# gcc –S hello.i –o hello.s

以下列出了hello.s的内容,可见Gcc已经将其转化为汇编了,感兴趣的读者可以分析一下这一行简单的C语言小程序是如何用汇编代码实现的。

.file "hello.c"

.section .rodata

.align 4

.LC0:

.string "Hello World!"

.text

.globl main

.type main, @function

main:

pushl %ebp

movl %esp, %ebp

subl $8, %esp

andl $-16, %esp

movl $0, %eax

addl $15, %eax

addl $15, %eax

shrl $4, %eax

sall $4, %eax

subl %eax, %esp

subl $12, %esp

pushl $.LC0

call puts

addl $16, %esp

movl $0, %eax

leave

ret

.size main, .-main

.ident "GCC: (GNU) 4.0.0 20050519 (Red Hat 4.0.0-8)"

.section .note.GNU-stack,"",@progbits

(3)汇编阶段

汇编阶段是把编译阶段生成的”.s”文件转成目标文件,读者在此可使用选项”-c”就可看到汇编代码已转化为”.o”的二进制目标代码了。如下所示:

[gan@localhost gcc]# gcc –c hello.s –o hello.o

(4)链接阶段

在成功编译之后,就进入了链接阶段。在这里涉及到一个重要的概念:函数库。

读者可以重新查看这个小程序,在这个程序中并没有定义”printf”的函数实现,且在预编译中包含进的”stdio.h”中也只有该函数的声明,而没有 定义函数的实现,那么,是在哪里实现”printf”函数的呢?最后的答案是:系统把这些函数实现都被做到名为libc.so.6的库文件中去了,在没有 特别指定时,gcc会到系统默认的搜索路径”/usr/lib”下进行查找,也就是链接到libc.so.6库函数中去,这样就能实现函 数”printf”了,而这也就是链接的作用。

函数库一般分为静态库和动态库两种。静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件 了。其后缀名一般为”.a”。动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以 节省系统的开销。动态库一般后缀名为”.so”,如前面所述的libc.so.6就是动态库。gcc在编译时默认使用动态库。

完成了链接之后,gcc就可以生成可执行文件,如下所示。

[gan@localhost gcc]# gcc hello.o –o hello

运行该可执行文件,出现正确的结果如下。

[root@localhost Gcc]# ./hello

Hello World!

————————————————

版权声明:本文为CSDN博主「沈二月」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:gcc编译程序的四个阶段(预处理-编译-汇编-链接)_gcc可以完成程序的预处理,汇编-CSDN博客

同时可参考:https://blog.csdn.net/bj318318/article/details/108891441?

ARM存储模型;

arm数据类型:

img

word型数据在内存中起始地址必须是4的整数倍

Halfword型数据在内存中的起始地址必须是2的整数倍

C语言数据类型要经过ARM-gcc编译器编译,生成对应的ARM存储类型,比如

char---Byte 8位

unsiged short ---Halfword 16位

unsigned int ---word 32位

那么如果遇到64位数据该怎么办呢?

可以有两种解决方法:

①:使用外围软件比如编译器,将不能直接识别的运算指令,比如double,编译成若干条32位指令

②:通过协处理器进行运算

字节序:

char类型没有字节序

img

ARM一般使用小端对齐,ARM在特殊场合下用大端对齐,比如路由器

验证ubantu下的字节序:

img

linux@linux:~/arm$ ./a.out

*p = 78

可以看出ubuntu是小端字节序模式

也可以使用联合体:

img

ARM指令存储:

* 处理器处于ARM状态时

所有指令在内存的起始地址必须是4的整数倍

PC值由其[31:2]决定,[1:0]位未定义

因为指令计数器PC存放的是要取的指令的起始地址,而指令在内存的起始地址必须是4的整数倍,所以PC的值也是4的整数倍,而且4的二进制100,8:1000 12:1100 可以看到:末两位都是0,所以说PC值由其[31:2]决定,[1:0]位未定义,但假如说PC的值是0x07(0111),那么如果要取指令,会被强制转换为0100,即0x04地址

* 处理器处于Thumb状态时

所有指令在内存的起始地址必须是2的整数倍

PC值由其[31:1]决定,[0]位未定义

注:即指令本身是多少位在内存存储时就应该多少位对齐

ARM工作模式:

* ARM有8个基本的工作模式

User 非特权模式,一般在执行上层的应用程序时ARM处于该模式

FIQ 当一个高优先级中断产生后ARM将进入这种模式

IRQ 当一个低优先级中断产生后ARM将进入这种模式

SVC 当复位或执行软中断指令后ARM将进入这种模式

Abort 当产生存取异常时ARM将进入这种模式 比如指针错误

Undef 当执行未定义的指令时ARM将进入这种模式 比如机器码错误,译码器不识别

System 使用和User模式相同寄存器集的特权模式

Monitor 为了安全而扩展出的用于执行安全监控代码的模式

linux分为用户态和内核态,USER模式是linux处于user态在ARM上跑,执行上层程序时的ARM工作状态

不同模式拥有不同的权限:

USER模式(非特权模式)下权限一般较低,防止用户误操作,引起整个系统的紊乱,用于保护系统

其他模式均为特权模式(权限高)

不同模式执行不同的代码:

USER执行上层应用程序

FIQ/IRQ执行硬件中断请求程序

SVC复位时一般执行初始化程序

不同模式完成不同的功能

* 按照权限

User为非特权模式(权限较低),其余模式均为特权模式(权限较高)

* 按照状态

FIQ、IRQ、SVC、Abort、Undef属于异常模式,即当处理器遇到异常后

会进入对应的模式

ARM寄存器组织:

寄存器概念:

寄存器是处理器内部的存储器,没有地址

注意:

①:寄存器没有地址,如果声明了一个register类型的变量,不能对其取地址

②:处理器内部的寄存器比较少,不支持全局变量,因为全局变量的生存周期很长,会一直占据CPU的寄存器,寄存器本来就少,会影响CPU的执行效率,支持局部变量,局部变量生存时间短,结束后变量会释放出寄存器空间,cpu又可以存入新的数据

* 作用

一般用于暂时存放参与运算的数据和运算结果

img

img

* 分类

包括通用寄存器、专用寄存器、控制寄存器

img

一共是40寄存器

专用寄存器:

* R15(PC,Program Counter)

程序计数器,用于存储当前取址指令的地址

* R14(LR,Link Register)

链接寄存器,一般有以下两种用途:

> 执行跳转指令(BL/BLX)时,LR会自动保存跳转指令下一条指令的地址

程序需要返回时将LR的值复制到PC即可实现

> 产生异常时,对应异常模式下的LR会自动保存被异常打断的指令的下

一条指令的地址,异常处理结束后将LR的值复制到PC可实现程序返回

img

异常模式下类似

* R13(SP,Stack Pointer)

栈指针,用于存储当前模式下的栈顶地址

栈:本质是一段内存,主要存储一些临时数据,如局部变量、函数的参数、函数的返回值

img

CPSR(当前程序状态寄存器):

img

1.* CPSR寄存器分为四个域,[31:24]为条件域用F表示、[23:16]为状

态域用S表示、[15:8]为预留域用X表示、[8:0]为控制域用C表示

2.控制位:

* Bit[4:0]

[10000]User [10001]FIQ [10010]IRQ [10011]SVC

[10111]Abort [11011]Undef [11111]System [10110]Monitor

* Bit[5]

[0]ARM状态 [1]Thumb状态

* Bit[6]

[0]开启FIQ [1]禁止FIQ

* Bit[7]

[0]开启IRQ [1]禁止IRQ

3.条件代码标志:

* Bit[28](有符号数)

> 当运算器中进行加法运算且产生符号位进位时该位自动置1,否则为0

> 当运算器中进行减法运算且产生符号位借位时该位自动置0,否则为1

* Bit[29](无符号数)

> 当运算器中进行加法运算且产生进位时该位自动置1,否则为0

> 当运算器中进行减法运算且产生借位时该位自动置0,否则为1

* Bit[30]

当运算器中产生了0的结果该位自动置1,否则为0

* Bit[31]

当运算器中产生了负数的结果该位自动置1,否则为0

CPSR的N位和C位进位时的区别?

ARM异常处理:

异常:

* 概念

处理器在正常执行程序的过程中可能会遇到一些不正常的事件发生

这时处理器就要将当前的程序暂停下来转而去处理这个异常的事件

异常事件处理完成之后再返回到被异常打断的点继续执行程序

img

注意:异常处理程序也是用户自己写的

* 异常处理机制

不同的处理器对异常的处理的流程大体相似,但是不同的处理器在具体实现的机制上有所不同;比如处理器遇到哪些事件认为是异常事件、遇到异常事件之后处理器有哪些动作、处理器如何跳转到异常处理程序如何处理异常、处理完异常之后又如何返回到被打断的程序继续执行等我们将这些细节的实现称为处理器的异常处理机制

ARM异常源:

* 概念

导致异常产生的事件称为异常源

ARM异常源有哪些?

FIQ 快速中断请求引脚有效

IRQ 外部中断请求引脚有效

Reset 复位电平有效

Software Interrupt 执行swi指令

Data Abort 数据终止

Prefetch Abort 指令预取终止

Undefined Instruction 遇到不能处理的指令

FIQ比IRQ优先级高

* 异常模式

在ARM的基本工作模式中有5个属于异常模式,即ARM遇到异常后会切

换成对应的异常模式

img

注意区分:异常源和异常模式:

什么是异常源? 异常事件就是异常源

什么是异常模式? 当有异常源产生异常后,ARM进入的工作模式成为异常模式

ARM异常响应:

高优先级中断可以打断低优先级中端、同等优先级中断和比自己更低优先级的中断不能打断正在进行的中断:

假如此时正在处理IRQ异常处理程序,此时再来一个IRQ,那么无法打断当前异常处理程序;但是假如此时来一个FIQ中断呢?因为FIQ(快速中断请求)比IRQ(外部中断请求)优先级高,所以FIQ会打断当前IRQ异常处理过程,IRQ先暂停,去处理FIQ中断,处理完了FIQ后,再回到之前被FIQ打断的点继续执行IRQ中断请求,此后,当IRQ中断请求处理完了之后,再回到最开始的用户程序的间断点,去执行接下来的用户程序。

示意图:

img

1.* ARM产生异常后的动作自动完成

1.拷贝CPSR中的内容到对应异常模式下的SPSR_<mode>

2.修改CPSR的值

2.1.修改中断禁止位禁止相应的中断

2.2.修改模式位进入相应的异常模式

2.3.修改状态位进入ARM状态(处理异常必须切换成ARM模式)

3.保存返回地址到对应异常模式下的LR_<mode>

4.设置PC为相应的异常向量(异常向量表对应的地址)

【由于是自动完成的,会跳到一个固定位置:异常向量表,不会直接跳到异常处理程序,因为异常处理程序是用户自己写的,可以放到内存中的任意位置,既然是自动完成,肯定就不会跳到我们想要的地址位置】

img

注意:

ARM处理异常时必须工作于ARM状态

当前指令执行完才会处理异常,比如假如正在执行一条指令,这时突然来了异常,不管这条指令是刚刚执行还是已经将要结束,都要先执行完才会处理异常,所以返回时LR保存的是这条指令的下一条指令的地址;

因为遵循先执行后处理异常的原则,就算这条指令才刚刚开始执行,LR也不会保存当前指令的地址,而是下一条指令。

异常向量表:

* 异常向量表

> 异常向量表的本质是内存中的一段代码(占32个字节)

> 表中为每个异常源分配了四个字节的存储空间

> 遇到异常后处理器自动将PC修改为对应的地址

> 因为异常向量表空间有限一般我们不会再这里

写异常处理程序,而是在对应的位置写一条跳

转指令使其跳转到指定的异常处理程序的入口

注:ARM的异常向量表的基地址默认在0x00地址

但可以通过配置协处理器来修改其地址(这样的话PC会直接跳到基地址+偏移地址的位置)

img

出现什么异常源,PC的值就会被设置为对应异常源的异常向量(地址),而该异常向量里面就是跳转指令,可以跳到异常处理程序的入口,不是直接存放异常处理程序**【因为异常向量表给每一个异常源只分配4个字节,异常处理程序通常比较大,但指令都是4个字节,所以可以存放跳转指令,一般来说,异常处理程序放在内存里的什么位置,跳转指令就可以根据需要写,让它跳到哪里】**

2.处理异常,异常处理程序需要根据需要自己写(要在最后加上模式恢复和LR复制给PC返回)

3.返回

* ARM异常返回的动作(自己编写)

1.将SPSR_<mode>的值复制给CPSR

使处理器恢复之前的状态

产生异常

2.将LR_<mode>的值复制给PC

使程序跳转回被打断的地址继续执行

以IRQ为例:

img

异常优先级:

* 多个异常同时产生时的服务顺序(从下往上依次递增)

Reset

Data Abort

FIQ

IRQ

Prefetch Abort

Software Interrupt

Undefined instruction

* FIQ的响应速度比IRQ快

1.**FIQ在异常向量表位于最末**

可直接把异常处理写在异常向量表之后,省去跳转

具体原因:

之前我们说到:IRQ异常在异常向量表中存放的是跳转指令,不能存放IRQ异常处理程序,因为异常向量表给每一个异常源都分 配了4个字节大小的空间,而异常处理程序往往很大,所以不能直接存放到异常向量表中,否则会将FIQ的异常向量表内容覆盖 掉。但是对于FIQ来说,他是位于异常向量表的末尾,后面没有内容,可以直接将FIQ异常处理程序写到异常向量表里面,省去 一 次跳转步骤。

2.**FIQ模式有5个私有寄存器(R8-R12)**

执行中断处理程序前**无需压栈保护,可直接处理中断**

当然如果使用的是R0-R7寄存器,也需要压栈保护

3.**FIQ的优先级高于IRQ**

3.1 两个中断同时发生时先响应FIQ

3.2 FIQ可以打断IRQ,但IRQ不能打断FIQ

另外:

1.进入IRQ异常处理程序后,第一件事是要保护现场,之后才能处理异常,因为处理异常是执行这段程序,既然执行程序就要对数据做一些运算,而参与运算的数据和结果放到哪呢?就是放到寄存器,但是USER模式跳到IRQ模式,他们的寄存器(r8-r12)是共用的,如果直接操作,就会把之前的覆盖掉,所以要先保护现场在进行处理执行异常程序。

2.为什么只有r8到r12是FIQ特有的?如果将所有的寄存器都变为自己特有的,不是更快吗?

这里考虑到不同模式之间要交互数据,比如USER要和FIQ模式交互数据,肯定是放到共有的寄存器比较方便,特定模式下只能使用当前模式下的寄存器,如果FIQ的寄存器都是自己特有的,那么将无法交互数据

ARM微架构:

img

可以根据上面的场景去理解,不同硬件都在干活,而且干不同的活,明确分工,效率就更高

img

指令流水线:

img

* ARM指令流水线

ARM7采用3级流水线

ARM9采用5级流水线

Cortex-A9采用8级流水线(但整体上的步骤还是取指、译码、执行)

注1:**虽然流水线级数越来越多,但都是在三级流水线的基础上进行了细分**

* PC的作用(取指)

不管几级流水线,PC指向的永远是当前正在取指的指令,而当前正在执行

的指令的地址为**PC-8,当前正在译码的指令的地址为PC-4**

注意:

* 指令流水线机制的引入确实能够大大的提升指令执行的速度

但在**实际执行程序的过程中很多情况下流水线时是无法形成的**

比如**芯片刚上电的前两个周期、执行跳转指令后的两个周期等**

所以指令流水线的引入以及优化只能使平均指令周期不断的接

近1而不可能真正的达到1,且流水线级数越多芯片设计的复杂

程度就越高,芯片的功耗就越高

所以并不是说指令流水线的级数越高,就一定好

多核处理器:

img

拓展:

那么三级流水线、5级流水线、8级流水线到底有什么区别?

https://www.cnblogs.com/CorePower/p/CorePower.html


ARM指令集仿真环境搭建

汇编与C:

* 汇编

> 每条汇编都会唯一对应一条机器码,且CPU能直接识别和执行

即汇编中所有的指令都是CPU能够识别和执行的

> 汇编中寄存器的使用、栈的分配与使用、程序的调用、参数的传递等

都需要自己维护

* C语言

> 每条C语句都要被编译器编译成若干条汇编指令才能被CPU识别和执行

即C语句中的指令CPU不一定能直接识别,需要编译器进行“翻译”

> C中寄存器的使用、栈的分配与使用、程序的调用、参数的传递等

都是编译器来分配和维护

学习汇编的目的:

* 底层开发可能会读/写汇编代码

* 理解CPU是怎样执行程序的

* 理解C的本质,用汇编的思想写出高效的C代码

汇编编程注意:

@ 汇编中的符号

@ 1.指令(汇编): 能够编译生成一条32bit机器码,并且能够被cpu识别和执行的命令

@ 2.伪指令: 本身不是指令,编译器可以将其替换成若干条指令

@ 3.伪操作: 不会生成指令,只是在编译器如何编译(本身不参与编译,类似于c语言的条件编译)

@ ARM指令集

@ 1.数据处理指令: 进行数学运算、逻辑运算的指令

@ 2.跳转指令: 完成程序的跳转,本质是修改PC寄存器(例如子程序调用)

@ 3.Load/store指令: 访问(读写)内存

@ 4.状态寄存器传送指令: 访问(读写)CPSR

@ 5.软中断指令: 触发软中断

@ 6.协处理指令: 操作写处理器的指令(浮点数据运算、管理内存等)

作业:

汇编与c的区别;

①:两者所处的层级不同,C语言是高级语言,汇编语言更偏向底层,C语言程序经过编译阶段生成汇编代码,不能直接操控硬件,而汇编直接面向硬件,可以直接对硬件进行操控

②:一条C语言可能对应多条指令,而一条汇编只对应一条指令

③:由于一条汇编对应一条指令(机器码),不同架构处理器的机器码不同,机器码不可移植,故汇编也不可移植,而C语言是不区分架构的,方便移植

④:从代码的执行效率来讲,汇编比C语言的效率要高,速度更快

⑤:汇编语言目标代码体积小,C语言目标代码体积大

⑥:汇编语言不易于维护,而C语言便于维护,汇编中寄存器的使用、栈的分配与使用、程序的调用、参数的传递等

都需要自己维护,C中寄存器的使用、栈的分配与使用、程序的调用、参数的传递等都是编译器来分配和维护

数据处理指令;

1.数据搬移指令:

(数学运算、逻辑运算)

@ MOV R1,#1 @R1=1

@ MOV R2,#2

@ MOV R3,#3

@ MOV R4,R1 @R4=R1

@ MOV R15,#7

@ MVN R0,#0xFF @R0=~0xFF

@测试:一条汇编对应一个机器码,不同汇编机器码不同

@ MOV R0,#0

@ MOV R1,#0

@ MOV R1,#1

@ MVN R0,#0

注意:

MOV R15,#7 PC的值要保持4的整数倍,如果用户自己写的PC值不是4的整数倍,会将最后两位清零(最后两位未定义)

MVN R0,#0xFF @R0=~0xFF

不同汇编机器码不同,一条汇编唯一对应一条机器码

立即数:

MOV R0,#0x12345678

arm-asm.s(41): error: invalid constant (12345678) after fixup ,赋值的0x12345678无效

下面我们换成:0x12试一下:

0x00000000 E3A00012 MOV R0,#0x00000012

可见0x12这个数据占据机器码的一部分,如果换成0x12345678,整个数把整条指令的32位空间全占了,编译报错不允许

如何判断:简单的方法:能被编译器编译通过的数就是立即数

立即数的本质:

@立即数的本质是包含在指令当中的数,属于指令的一部分

那立即数与变量的区别在哪?

变量独占空间,而立即数则是包含在指令里,与指令绑定

比如int a=0x234,a++;cpu要执行,必须先将内存中的数据从内存中取出,再运算;而立即数是包含在指令里的,取指的时候可以直接带上立即数取过来,更快

所以总结一下立即数的优缺点:

优点:取指的时候就可以读取到cpu,不用单独去读取,速度快

缺点:不能是任意的32位的数,有局限性

注意:

@ MOV R0,#0xFFFFFFFF

@ MVN R0,#0

如果写成MOV R0,#0XFFFFFFFF编译器也不报错(虽然它不是立即数),是因为在编译的时候,会自动替换为MVN,R0,#0x00000000这条指令,0是立即数,因此可以执行,执行结果为R0 = 0xFFFFFFF,可以替MOV R0,#0XFFFFFFFF执行(其实这里类似于伪指令)

数据运算指令的格式(严格按照此格式)

@操作码+目标寄存器+第一寄存器+第二操作数

@操作码: 表示执行那种操作

@目标寄存器: 用于存放运算结果

@第一寄存器: 参与运算的第一个数据(只能写寄存器)

@第二操作数: 参与运算的第二个数据(可以是寄存器也可以是操作数【乘法除外,只能是寄存器】)

2.加法指令

ADD R3,#5,#3出错:

arm-asm.s(55): error: bad expression -- `add R3,#5,#3'

@ MOV R1,#2

@ MOV R2,#3

@ ADD R3,R1,R2 @R3=R1+R2(两个寄存器相加结果放到R3)

@ ADD R3,R1,#5 @R3=R1+5(寄存器和一个立即数相加,结果放到R3)

@ ADD R3,#5,#3 @格式错误

@ ADD R3,#5,R1 @格式错误

3.减法指令:

@ MOV R1,#5

@ MOV R2,#3

@ SUB R3,R1,R2 @R3=R1-R2(两个寄存器相减,结果存到R3)

@ SUB R3,R1,#4 @R3=R1-4(一个寄存器减去一个操作数,结果存到R3)

4.逆向减法指令:

@ MOV R1,#5

@ MOV R2,#3

@ RSB R3,R1,R2 @R3=R2-R1

@ RSB R3,R1,#1 @R3=1-R1

5. 乘法指令:

@ MOV R1,#5

@ MOV R2,#3

@ MUL R3,R1,R2 @R3=R1*R2

@ MUL R3,R1,#3 @特别注意:乘法时第二操作数只能是寄存器,这里错误

6.按位与运算:

@ MOV R1,#2

@ MOV R2,#3

@ AND R3,R1,R2 @R3=R1 & R2

7.按位或运算:

@ MOV R1,#2

@ MOV R2,#3

@ ORR R3,R1,R2 @R3=R1 | R2

8. 按位异或运算(相异为1):

@ MOV R1,#1

@ MOV R2,#3

@ EOR R3,R1,R2 @R1 ^ R2

9.左移运算:

@ MOV R1,#0XF0

@ MOV R2,#3

@ LSL R3,R1,R2 @R3=(R1<<R2)

10.右移运算:

@ MOV R1,#0XF0

@ MOV R2,#3

@ LSR R3,R1,R2 @R3=(R1>>R2)

11.位清零:

@ MOV R1,#0XFF

@ MOV R2,#0X0F

@ BIC R3,R1,R2 @,R2的哪些位为1就把R1对应的位清零(注意R1清零后不变)

12.数据运算指令的格式扩展:

@ MOV R1,R2,LSL,#1 @R1=(R2<<1)

13.数据运算对(CPSR寄存器条件位n\z\c\v)的影响:

@ 默认情况下数据运算不会对CPSR的条件位产生影响,在指令后加后缀"S"后会影响条件位

@ MOV R1,#3

@ SUB R3,R1,#5

@ SUBS R3,R1,#5 @验证N位

@ MOV R1,#3

@ SUBS R3,R1,#3 @验证Z位

@ MOV R1,#0XFFFFFFFE

@ ADDS R3,R1,#5 @验证C位

@ MOV R1,#0X7FFFFFFE

@ ADDS R3,R1,#4 @验证V位

14.两个64位的数据做加法;**(低加低,高加高):**

@第一个数的低32位放到R1

@第一个数的高32位放到R2

@第二个数的低32位放到R3

@第二个数的高32位放到R4

@结果的低32位放到R5

@结果的高32位放到R6

@比如:

@第一个数:0x00000001 FFFFFFFF

@第二个数:0x00000002 00000005

MOV R1,#0XFFFFFFFF

MOV R2,#0X00000001

MOV R3,#0X00000005

MOV R4,#0X00000002

@直接使用ADD加法指令(不可取):

@ ADD R5,R1,R3

@ ADD R6,R2,R4 @但是这样不会把R5的进位加上

@带进位的加法(推荐):

ADDS R5,R1,R3 @这里必须用ADDS,影响条件位,否则有进位时ADC不会加上进位标志

ADC R6,R2,R4 @R6=R2+R4+'C'(c为CPSR寄存器的进位/借位标志位)

15.两个64位的数据相减(低减低,高减高):

@第一个数的低32位放到R1

@第一个数的高32位放到R2

@第二个数的低32位放到R3

@第二个数的高32位放到R4

@结果的低32位放到R5

@结果的高32位放到R6

@比如:

@第一个数:0x00000003 00000002

@第二个数:0x00000002 00000005

MOV R1,#0X00000002

MOV R2,#0X00000003

MOV R3,#0X00000005

MOV R4,#0X00000002

@直接使用SUB减法指令(不可取):

@ SUB R5,R1,R3

@ SUB R6,R2,R4

@带借位的减法指令:

SUBS R5,R1,R3

SBC R6,R2,R4 @本质:R6=R2-R4-'!C'(c为CPSR寄存器的进位/借位标志位)

从汇编反观C语言:

1.处理64位数据的运算:

img

作64位数据运算时,编译器会自动帮我们选择合适的汇编指令

2.除法运算:

img

首先从上述结果看:

一条C代码可能会对应多条汇编指令

并且可以发现:对上面a作除法运算时,除法会编译成一大堆包括右移指令,所以能写成a>>=1就不要写成/,因为右移是汇编的基本数据处理指令,C语言如果也写成右移,那么编译的时候会编译成一条汇编代码;但是加入写成了/,就会编译成多条代码,影响程序的执行效率

3.float型运算:

从上面发现,汇编似乎没有float的运算,那么c语言中的float型数据是怎么处理的呢?

img

对于float型变量的运算ARM处理器一般有两种解决方案(ARM指令集是RISC指令集,本身不能直接进行float运算):

①:利用gcc将float型编译成很多汇编的组合来进行运算,如上图所示

②:在ARM处理器周围加上一个协处理器,帮助ARM处理器运算复杂的指令

拓展:

什么是32位的处理器?

单次处理数据的能力为32位

为什么说64位处理器比32位的处理器快?

因为64位处理器单次可以运算64位的数据,而如果换成32位的处理器作64位数据的运算,则只能拆开运算,速度慢,也就是说64位处理器比32位处理器的数据处理能力强

有符号数与无符号数

有符号数和无符号数详解Hani_97的博客-CSDN博客有符号数和无符号数的区别

那么请思考128位的数据又该怎么算?

.编程实现使用32bit的ARM处理器实现两个128位的数据的加法运算。

注:

第一个数的bit[31:0]、bit[63:32]、bit[95:64]、bit[127:96]分别存储在R1、R2、R3、R4寄存器

第二个数的bit[31:0]、bit[63:32]、bit[95:64]、bit[127:96]分别存储在R5、R6、R7、R8寄存器

运算结果的bit[31:0]、bit[63:32]、bit[95:64]、bit[127:96]分别存储在R9、R10、R11、R12寄存

类似于64位处理,只不过拆分的部分较多,下面是调试记录(下面的程序写的是带进位的128位数据运算):

①:

img

img

img

img

可以发现,程序中第一个ADDS一执行,会影响到CPSR的Z、C位,会被置为1,但是后面都是用ADC的情况下,会一直将C位的标志位也加进来(ADC的本质是第一寄存器+第二寄存器+C位),但是我发现过程中C、Z位只是被ADDS影响了一下,后来一直保持不变,所以ADC会一直采集C位数值,从最后的R12的结果也可以看到,当然结果是错误的;原因是ADC不会影响到CPSR的标志位,需要加后缀S

②:下面换成ADCS;

img

img

img

img

可以清楚的看到,采用ADCS后会一直影响标志位,CPSR的标志位一直在动态变化,结果也是正确的


跳转与存储器访问指令:

跳转指令:

本质是修改PC寄存器的值:

@方式一:直接修改PC寄存器的值(不推荐使用,因为每次都要计算要跳转指令的绝对地址)

@MAIN:

@ MOV R1,#1

@ MOV R2,#2

@ MOV R3,#3

@ MOV PC,#0X18

@ MOV R4,#4

@ MOV R5,#5

@FUNC:

@ MOV R6,#6

@ MOV R7,#7

@ MOV R8,#8

@ MOV R9,#9

@ MOV R10,#10

@方式二:不带返回地址的跳转指令:本质:只是修改PC寄存器的值为目的标号下第一条指令的地址

@MAIN:

@ MOV R1,#1

@ MOV R2,#2

@ MOV R3,#3

@ B FUNC

@ MOV R4,#4

@ MOV R5,#5

@FUNC:

@ MOV R6,#6

@ MOV R7,#7

@ MOV R8,#8

@ MOV R9,#9

@ MOV R10,#10

@方式三:带返回地址的跳转指令:本质:修改PC寄存器的值为目的标号下第一条指令的地址,同时将跳转指令的下一条指令的地址给LR

@MAIN:

@ MOV R1,#1

@ MOV R2,#2

@ MOV R3,#3

@ BL FUNC

@ MOV R4,#4

@ MOV R5,#5

@FUNC:

@ MOV R6,#6

@ MOV R7,#7

@ MOV R8,#8

@ MOV R9,#9

@ MOV R10,#10

@ MOV PC,LR

注意:标号即地址

以汇编的角度看C程序跳转(C语言子程序调用):

首先C编译成汇编后,从上往下地址号递增,因为编译时是由上而下编译的

1.跳转

img

运行到func函数,其汇编为0x00000034 EBFFFFF1 BL func(0x00000000),可见跳转到0X00000000地址,跳转的同时LR寄存器保存该跳转指令的下一条指令的地址即0x00000038 E3A00000 MOV R0,#0x00000000(main最后一个括号)

2.跳转后开始执行func内容

img

发现跳转的是func函数的第一个括号那里

3.执行完后要返回

img

原理是LR寄存器的值给PC,实现返回

并且从上面的结果可以发现:定义的全局变量并没有被编译为汇编

问题:

另外从编译的汇编中发现有BX R14,那么BX是什么指令呢?

BX指令是ARM指令系统中的带状态切换跳转指令。X指令跳转到指令中所指定的目标地址,若目标地址的bit[0]为0,则跳转时自动将CPSR中的标志位T复位,即把目标地址的代码解释为ARM代码;若目标地址的bit[0]为1,则跳转时自动将CPSR中的标志位T置位,即把目标地址的代码解释为Thumb代码。

ARM指令的条件码:

img

问题:

汇编语言中无符号数与有符号数区别?

ARM指令的条件执行

@条件指令

@本质就是将两个寄存器里的值做减法(SUBS),没有将运算的结果存入寄存器(比如与ADD相比少了一个目标寄存器

@ MOV R1,#3

@ MOV R2,#4

@ CMP R1,R2

@ BEQ FUNC @IF(EQ){B FUNC} 本质是:IF(Z==1){B FUNC}

@ BNE FUNC @IF(NE){B FUNC} 本质是:IF(Z==0){B FUNC}

@ MOV R3,#5

@ MOV R4,#6

@ MOV R5,#7

@FUNC:

@ MOV R6,#8

@ MOV R7,#9

@ MOV R8,#10

@ARM的大多数指令都可以带条件码后缀

@比如:

@ MOV R1,#1

@ MOV R2,#2

@ CMP R1,R2

@ MOVLT R3,#3

从汇编语言分析C代码:

img

可以看到,if(a==6)这条语句被编译成0x0000005C E3530006 CMP R3,#0x00000006,可以推测出a被放到了R3寄存器

img

可以看到func被编译为0x0000003C 0BFFFFF0 BLEQ func(0x00000004),即如果条件成立,会执行跳转到0x00000004地址这条指令,并且会自动保存LR的值为跳转指令的下一条指令的地址

内存读写指令

@ STR写内存:

@ MOV R1,#0XFF000000

@ MOV R2,#0X40000000

@ STR R1,[R2] @将R1寄存器中的数据存储到R2所指向的内存空间

@ LDR读内存:

@ LDR R3,[R2] @将R2指向的内存中的数据从内存中读取到R3寄存器

@ 读写不同类型的变量:

@ MOV R1,#0XFFFFEEFF

@ MOV R2,#0X40000000

@ STRB R1,[R2] @将R1中的数据以单字节的方式写入到R2指向的内存

@ STRH R1,[R2] @将R1中的数据以半字的方式写入到R2指向的内存

@ STR R1,[R2] @将R1中的数据以字的方式写入到R2指向的内存(默认方式)

@ MOV R1,#0XFFFFEEFF

@ MOV R2,#0X40000000

@ STR R1,[R2]

@ LDRB R3,[R2] @将R2中的数据以单字节的方式读取到R3指向的内存

@ LDRH R3,[R2] @将R2中的数据以半字的方式读取到R3指向的内存

@ LDR R3,[R2] @将R2中的数据以字的方式读取到R3指向的内存(默认方式)

STR或者LDR默认操作的是一个字

结果演示:

1.验证小端对齐

img

可以验证ARM处理器下是小端对齐:低地址存放低字节、高地址存放高字节

2.地址是4的整数倍

img

如果R2的值不是4字节的整数倍,这时候先后往里存数据,系统会强制从4的整数倍地址开始存,如上图所示,同时编译器会有报错信息,所以地址必须写成4的整数倍

3.内存读写

img

先将R1的数写到R2指向的内存空间,再将R2指向内存空间中的值读入到R3寄存器

最后从汇编理解C语言中不同类型的变量的运算:

img

问题:

可是上图中的0x00000004 E59F3030 LDR R3,[PC,#0x0030]是什么意思?

这是基址加变址的前索引寻址方式,意义为将PC指针+0x0030指向的内存中的指令存放到R3中,然后根据上面的描述,将R3指向的内存中的数据(即a)读入到CPU的R2内存中,好像是把指令看作地址了

寻址方式:

寻址方式: 寻址方式就是CPU寻找操作数的方式

@ 立即寻址

@ MOV R1,#1

@ ADD R2,R1,#1

@ 寄存器寻址:

@ ADD R2,R1,R3

@ 寄存器移位寻址:

@ MOV R1,R2,LSL #1

@ 寄存器间接寻址:

@ STR R1,[R2]

@ ....

@ 基址加变址寻址

@ MOV R1,#0XFFFFFFFF

@ MOV R2,#0X40000000

@ MOV R3,#4

@ STR R1,[R2,R3] @将R1中的数据写到R2+R3指向的内存中

@ STR R1,[R2,R3,LSL #1] @将R1中的数据写到R2+(R3<<1)指向的内存中

@ 基址加变址寻址的索引方式:

@ 前索引:

@ MOV R1,#0XFFFFFFFF

@ MOV R2,#0X40000000

@ STR R1,[R2,#8] @将R1中的数据写到R2+8指向的内存中

@ 后索引:

@ MOV R1,#0XFFFFFFFF

@ MOV R2,#0X40000000

@ STR R1,[R2],#8 @将R1中的数据写到R2指向的内存中,然后R2自增8

@ 自动索引:

MOV R1,#0XFFFFFFFF

MOV R2,#0X40000000

STR R1,[R2,#8]! @将R1中的数据写到R2+8指向的内存中,然后R2自增8

思考:C语言中哪里会用到基址加变址的这几种索引的寻址方式

字符数组会经常用到,比如往char a[5]里赋值,并且后索引和自动索引自增会提高效率,写完会自动写下一个,不用我们再去修改地址。

注意:

1.STR,LDR的[ ]里的写地址

2.不同架构的处理器支持的寻址方式可能不同,因为不同架构处理器的内部运算电路可能不同,即寻址方式需要硬件电路的支持

3.立即数的合法性判断与汇编指令编码格式:

我在汇编中写下MOV R1,#0X40000000,编译通过,所以我不是很理解如何判断立即数?

debug下的反汇编:0x00000004 E3A02101 MOV R2,#0x40000000

(先说结论:它是由1右移2位形成的,所以编码中立即数位为101)

那么为什么它是合法的呢,怎么判断一个数是不是立即数呢?

前面我们讲到,立即数是放在指令中的数,取指时捎带回CPU,下面我们重点讨论立即数的合法性判断以及汇编的编码方式:

img

指令解析:

我们可以从图片中看出:一条指令的后 12 位(bit 11~0),是指令中立即数占用的位数。其中这 12 位又分为两部分:前 7~0 位是数值部分;后 11~8 位是前 7~0 位要进行移位操作的移位数。

我们注意到其中 immed_8 有 8 位,也就是我们的立即数部分占 8 位,因此有如下结论:

①:如果一个立即数小于 0xFF(255)那么直接用前 7~0 位表示即可,此时不用移位,11~8 位的 Rotate_imm 等于 0。

②:如果前八位 immed_8 的数值大于 255,那么就看这个数是否能有由immed_8 中的某个数(可以是1位或几位)移位 2*Rotate_imm 位形成的。如果能,那么就是合法立即数;否则非法。

判断方法:

一个 32 位数用 12 位编码表示,符合以下规则才是合法立即数。

立即数 = immed_8 循环右移 (2 * Rotate_imm)

解读:如果存在一个 Rotate_imm 能够让该立即数由 immed_8 循环右移 2*Rotate_imm 位(偶数位)表示,那么这个立即数就是合法的。

举例:

我们来举个例子看看

汇编指令:mov R0, #0x0000f200

经过译码器转化为汇编后的机器指令如下:

机器指令:0xe3a00cf2 ,其中 0xcf2 就是我们的立即数。

下面我们讨论译码器是如何计算出这个 cf2 的:

我们先来解剖一下转换后的 cf2。其中的 c 就是 Rotate_imm 部分,而 f2 则是 Immed_8 部分。

译码器看到 #0x0000f200 也就是二进制的 0000 | 0000 | 0000 | 0000 | 1111 | 0010 | 0000 | 0000 |。然后计算出将其中的 1111 | 0010 部分循环右移 24 位刚好就是那个二进制。(看下面的图会更清楚)

即 0x0000f200=0xf2 循环右移 (2×0x0c)

因此我们可以得出此时的 Rotate_imm 的值就是 12,十六进制就是 c; Immed_8 的值则是 1111 | 0010 即 f2。

到此为止 cf2 的计算已经完毕。

img

例子 0x234

0x234 是不是一个合法的立即数?

我们把 0x234 表示成 0000 | 0000 | 0000 | 0000 | 0000 | 0010 | 0011 | 0100 ,发现将其中的 1000 | 1101 部分循环右移 30 位可以和这个二进制数相同。因此 Rotate_imm 的值是 15,而 Immed_8 的值是 1000 | 1101 即 8D。

例子 0x132

0x132 是不是一个合法的立即数?

我们把 0x132 表示成 0000 | 0000 | 0000 | 0000 | 0000 | 0001 | 0011 | 0010,发现没有任何部分循环右移偶数次可以成为该数本身,因此它不是一个合法的立即数。

判断技巧:

快速判断一个数是不是有效立即数的方法是看最低几位为 0 的个数是不是偶数个(表述的有点问题)。

我来解释一下:比如上面那个 #0x0000f200,其中我们猜测 Immed_8 为 1111 | 0010,这个数右边还有 0000 | 0000 |,是偶数个 0,因此是有效立即数。而我们看 0x132,我们猜测 Immed_8 为 1001 | 1001,右边只有一个 0,因此不是有效立即数。

注意:上面的方法是从网上截取的,验证了一下好像不对,看下面的例子:

0x00000000 E3A01105 MOV R1,#0x40000001

这个是我随便写的,汇编指令为MOV R1,#0X40000001,如果按照上面作者给出的方法来判断的话,他就不是立即数,但是它能编译通过,说明它是立即数,所以判断一个数是不是立即数要依靠编译器哈哈,编译器能编译通过就是立即数,或者不嫌麻烦可以自己按照上面的叙述找出立即数在编码中的immed_8,然后再判断它到底能不能通过右移偶数位获得原来的立即数,但这是比较麻烦的


栈的种类和应用:

stop :

B stop

的作用:死循环,防止程序跑飞

如果去掉它的话会出现:

*** error 65: access violation at 0x000000A0 : no 'execute/read' permission

说明程序跑飞

多寄存器内存访问指令:

(注意使用时要与寄存器间接寻址作区别STR/LDR)

@ MOV R1,#1

@ MOV R2,#2

@ MOV R3,#3

@ MOV R4,#4

@ MOV R11,#0X40000020

@ STM R11,{R1-R4} @将R1-R4中的数据写到以R11为起始地址的内存空间

@ LDM R11,{R6-R9} @将以R11为起始地址的内存中的数据读取到R6-R9寄存器

@ 寄存器不连续时使用逗号分隔

@ STM R11,{R1,R2,R4} @将R1,R2,R4寄存器中的数据写到R11中

@ 无论寄存器列表的顺序如何,写入到内存中永远是低地址存小编号寄存器数据、高地址存大编号数据

@ STM R11,{R2,R4,R1,R3}

@ STM R11!,{R1-R4}

注意使用是采用花括号,与寄存器间接寻址区别

debug调试:

1.STM R11,{R1-R4} //连续写入

img

注意寄存器连续存储时,遵循字节对齐的原则,每次4字节写入读取

2.LDM R11,{R6-R9} //连续读取

img

读取时也按照字节对齐的原则读取

3.STM R11,{R1,R2,R4} //寄存器不连续写入

img

写内存时也会连续存储,注意英文逗号分隔

4.STM R11,{R2,R4,R1,R3} //乱序写入

img

发现存入还是低地址存小编号的寄存器,所以有如下规则:

无论寄存器列表的顺序如何,写入到内存中永远是低地址存小编号寄存器数据、高地址存大编号数据

5.STM R11! ,{R1-R4} //自动索引在多寄存器读写内存同样适用

img

发现写入后R11的值自动增加了4个字节,便于批量写入,提高效率

多寄存器内存访问指令的寻址方式:

img

debug调试:

1.STMIA R11!,{R1-R4} //Increase After

img

2.STMIB R11!,{R1-R4} //Increase Before

img

3.STMDA R11!,{R1-R4} //Decrease After

img

4.STMDB R11!,{R1-R4} //Derease Before

img

栈的种类和应用:

堆:malloc free

栈:

栈的概念:

栈的本质就是一段内存,程序运行时用于保存一些临时数据

如局部变量、函数的参数、返回值、以及程序跳转时需要保护的寄存器等

栈的分类:

img

img

增栈:压栈时栈指针越来越大,出栈时栈指针越来越小

减栈:压栈时栈指针越来越小,出栈时栈指针越来越大

满栈:栈指针指向最后一次压入到栈中的数据,压栈时需要先移动栈指针到相邻位置然后再压栈

空栈:栈指针指向最后一次压入到栈中的数据的相邻位置,压栈时可直接压栈,之后需要将栈指针移动 到相邻位置

* 栈分为空增(EA)、空减(ED)、满增(FA)、满减(FD)四种

ARM处理器一般使用满减栈

知道了栈的概念与分类,那么结合之前的多寄存器内存访问指令的寻址方式,如果使用满减栈,那么改用哪一种后缀的?

img

答案是使用STMDB R11!,{R1-R4}写、LDMIA R11!,{R6-R9}读,下面是debug调试:

img

但是ARM公司为我们提供了人性化的设计:使用满减后缀FD的指令即可实现读写内存

即STMFD R11!,{R1-R4}写、LDMFD R11!,{R6-R9}读,debug调试如下:

img

并且执行的时候,汇编也帮我们编译为:STMDB与LDMIA

img

附上代码:

MOV R1,#1

MOV R2,#2

MOV R3,#3

MOV R4,#4

MOV R11,#0X40000020

STMDB R11!,{R1-R4}

LDMIA R11!,{R6-R9}

STMFD R11!,{R1-R4}

LDMFD R11!,{R6-R9}

栈的应用举例:

@ 栈的应用举例:

@ 1.叶子函数的调用过程:

@ 初始化栈指针

@ MOV SP,#0X40000020

@ MOV R1,#1

@ MOV R2,#3

@ BL FUNC

@ ADD R3,R1,R2

@ B stop

@FUNC:

@ 压栈保护现场

@ STMFD SP!,{R1-R2}

@ MOV R1,#10

@ MOV R2,#20

@ SUB R3,R2,R1

@出栈恢复现场

@ LDMFD SP!,{R1-R2}

@ MOV PC,LR

@ 2.非叶子函数的应用举例:

@ 初始化栈指针

MOV SP,#0X40000020

MOV R1,#1

MOV R2,#3

BL FUNC1

ADD R3,R1,R2

B stop

FUNC1:

STMFD SP!,{R1-R2,LR}

MOV R1,#10

MOV R2,#20

BL FUNC2

SUB R3,R2,R1

LDMFD SP!,{R1-R2,LR}

MOV PC,LR

FUNC2:

STMFD SP!,{R1-R2}

MOV R1,#7

MOV R2,#8

MUL R3,R1,R2

LDMFD SP!,{R1-R2}

MOV PC,LR

stop : @ 死循环,防止程序跑飞

B stop

叶子函数: 位于调用的末端,不再去调用其他任何函数

非叶子函数: 调用了其它函数,不位于最末端

debug调试:

①:如果不压栈保护的话:

img

此时如果主函数与叶子函数所用的寄存器可能一样,这些寄存器在叶子函数存储数据时就会把之前主函数里的要参与运算的寄存器的值覆盖掉.........

②:如果使用压栈保护的话:

img

这样跳到叶子函数的第一步就是要进行压栈保护,将主函数中的寄存器值在栈中备份; 下面虽然执行FUNC叶子函数中的MOV R1,#10、MOV R2,#20将寄存器的值破坏了,但是记得要在LR赋值给PC后返回之前进行出栈恢复现场,将原来主函数的寄存器备份的值还原.......

③:非叶子函数(FUNC1)被主函数调用,同时FUNC1又去调用FUNC2(叶子函数)(不对LR压栈保护):

img

首先主函数调用FUNC1(LR自动保存下一条指令的地址),直接跳到FUNC1先进行压栈保护,将之前R1、R2的值1,3分别进行栈的备份,再去破坏(改变)R1、R2的值,然后又调用了FUNC2(自动保存下一条指令的地址),然后跳到FUNC2,先将FUNC1的寄存器压栈保护, 然后执行FUNC2的任务程序,R1、R2被破坏,然后进行出栈还原(此时还原的是FUNC1的寄存器),接着跳到FUNC1函数,执行完SUB减法指令,再还原主函数的寄存器,将LR赋值给PC计数器,但是注意此时返回的是FUNC1的SUB指令那里,为什么呢?

因为没有对LR寄存器也进行压栈保护,因为当执行BL FUNC2的时候,LR也会自动保存下一条指令的地址,会将之前保存的主函数中的返回点覆盖掉,所以才会无法返回到主函数,因此要进行LR的压栈保护,修改如下:

STMFD SP!,{R1-R2,LR}

MOV R1,#10

MOV R2,#20

BL FUNC2

SUB R3,R2,R1

LDMFD SP!,{R1-R2,LR}

MOV PC,LR

4.对LR寄存器也进行压栈保护:

img

进入到FUNC1非叶子函数中,先对R1、R2、LR进行压栈保护(注意压栈存储时低地址对应小编号、高地址对应大编号寄存器),后续又跳到FUNC2,先进行压栈保护,可以发现FUNC1的寄存器也被压入栈中

5.出栈

img

可以看到出栈后数据呈现绿色,但是并没有变为0,还保留着原始数据

为什么局部变量初始值为随机值,而全局变量的初始值为0?

我们都知道局部变量是保存在栈中的,当程序执行到局部变量时,系统会自动在栈中为该变量申请一段内存,那么申请哪里的内存呢,当然是SP指向的地方,它会往低地址帮我们申请,但是由于栈中申请的地方可能还保留着以前没有被清空的原始数据,所以就会出现如果定义局部变量时未进行初始化,使用printf打印的局部变量的值就是一个随机值

但对于全局变量来说,全局变量保存在BSS段,定义之后,当程序执行到这里,系统会进行BSS段的清空操作,所以此时用printf打印的值为0


专用指令:

状态寄存器传送指令;

首先来看以下几个问题:

1.复位后CPSR的标志位为什么是0X000000D3?

img

* Bit[4:0]

[10000]User [10001]FIQ [10010]IRQ [10011]SVC

[10111]Abort [11011]Undef [11111]System [10110]Monitor

* Bit[5]

[0]ARM状态 [1]Thumb状态

* Bit[6]

[0]开启FIQ [1]禁止FIQ

* Bit[7]

[0]开启IRQ [1]禁止IRQ

对应到CPSR的控制位,SVC模式下的最后5个是模式位:是10011,整个控制位为1101 0011 ,就是0X000000D3

下面我们分析一下:

0X000000D3,也就是1101 0011,对应到每个位:IRQ禁止(1)、FRQ禁止(1)、开机默认是ARM模式(0)、模式位10011(SVC模式)

其中的IRQ和FRQ为什么是禁止的呢?

因为刚复位,系统要进行初始化的操作,此时不允许其他中断打断它,所以会默认屏蔽IRQ位和FRQ位

2.为什么MOV R1,CPSR不行?

arm-asm.s(543): error: immediate expression requires a # prefix -- `mov R1,CPSR'

出于对系统的保护,不会轻易让有些指令操作CPSR寄存器,除了状态寄存器传送指令

状态寄存器传送指令:读写(访问CPSR)

@ 读CPSR:

MRS R1,CPSR @CPSR读到R1

@ 写CPSR:

MSR CPSR,#0X10 @将0X00000010写到CPSR

@ User模式下不能更改CPSR寄存器,非特权模式

MSR CPSR,#0XD3 @将0xD3写到CPSR寄存器失败

debug调试:

3.先读取CPSR的内容到R1中,再将0X10写到CPSR寄存器

img

进入User模式(模式位10000)

2.再将0XD3写入到CPSR寄存器:

img

发现模式仍然为User模式,且CPSR的值并没有变化,这是因为User模式为非特权模式,不允许更改,除了USER其它模式都是特权模式

C语言中没有和CPSR读写指令对应的语句,为什么?

不同处理器的模式和状态不同

那么CPSR适用于哪些场合呢?

①:操作系统内部用的比较多,系统刚上电时,处于SVC模式,当初始化完成后会切换为User模式,需要用到CPSR,指令为MSR CPSR,#0X10

②:系统调用之后会用到CPSR寄存器,因为要从其他模式返回到user用户模式,指令为MSR CPSR,#0X10

软中断指令:

@ 软中断指令:触发软中断

@ 异常向量表:

B MAIN

B .

B SWI_HANDLER

B .

B .

B .

B .

B .

@ 应用程序:

MAIN:

MOV SP,#0X40000020

MSR CPSR,#0X10

MOV R1,#4

MOV R2,#5

SWI #1

ADD R3,R1,R2

B STOP

SWI_HANDLER:

STMFD SP!,{R1-R2,LR}

MOV R1,#11

MOV R2,#55

MUL R3,R1,R2

LDMFD SP!,{R1-R2,PC}^

STOP:

B STOP

debug调试:

1.MAIN函数触发软中断,但是跳到MAIN函数

img

为什么要写MSR CPSR ,#0X10?

因为系统刚上电复位,系统会在SVC(Supervisor)模式,为了要运行用户程序,需要切换到user模式,0x10对应user模式

程序单步调试后发现:(蓝色为刚发生过变化的)

SWI #1触发软中断:

系统会自动完成几件事:

1.复制CPSR的值到SVC模式下的SPSR

2.修改CPSR的值

①:屏蔽相应的中断位(IRQ)

②:修改工作模式进入SVC模式(10011)

③:修改状态为ARM状态(T=0)

即CPSR的值修改为0X93

3.SWI的下一条指令的地址会自动被保存在SVC模式下的LR

4.设置PC为SWI异常向量(0x08)

同时注意,复位状态下的CPSR的值为0XD3,会将IRQ与FRQ全部屏蔽,但是软中断触发只会屏蔽IRQ位

这一步虽然验证了一些异常处理部分事项,但是这样写是有问题的,因为用户程序会将异常向量表占据

2.将MAIN函数搬家不占用异常向量表,同时加上异常处理程序

img

B .表示跳转到当前位置,我们在0X0地址写上B MAIN(系统默认复位后PC为0,即下图的reset对应的地址)

img

在0X08地址写上SWI对应的跳转指令(不能直接写异常处理程序)

执行步骤如下:

①:先执行B MAIN跳到MAIN函数

②:再对SP进行初始化(注意这是SVC模式下的SP,稍后讲解)

③:修改为User模式

④:执行完两行用户程序,再执行SWI #1,触发软中断

⑤:系统会自动完成几件事,大致与上面debug1相同,但是此时SP是蓝色的,说明刚刚发生过变化,因为发生了模式的切换SVC-USER-SVC

触发软中断后,PC会被自动设置为0x08,而恰好我们就在该地址写下跳转程序SWI_HANDLER,会自动跳到异常处理程序:

进入异常处理程序:

img

如果异常处理程序中使用的寄存器与MAIN函数相同,处理前应该进行压栈保护,返回前应该出栈恢复

处理完异常处理程序之后,用户需要自己写上SPSR还原给CPSR、LR复制到PC的程序,这里为了写的方便,可以采用另外的写法:

STMFD SP!,{R1-R2,LR}

LDMFD SP!,{R1-R2,PC}^(注意^表示:SPSR切换到CPSR)

将LR与R1、R2一起进行压栈保护(因为当时系统自动保存SWI指令的下一条指令地址到SVC模式下的LR,压栈保护的就是SVC模式下的LR)

而且压栈时地址分配是低地址对应小编号的寄存器、高地址对应大编号的寄存器,出栈时也是如此

出栈时出给R1、R2、PC,可以看下面的图:

img

可以看到之前的LR的0X34确实是出给了PC,指向了ADD R3,R1,R2

另外注意:为什么SP初始化要写到CPSR的前面?写到它后面可以吗?

写到CPSR写操作指令后不行,因为要进行压栈出栈的是SVC模式下的SP栈指针,而且特定模式下只能使用当前模式下的寄存器,R13(SP)是每个模式下特有的,因此初始化的应该是SVC模式下的SP,如果将SP初始化指令写到CPSR写操作的后面,那么就是在用户模式进行的SP初始化,与下面的异常处理程序的SVC模式的SP不是同一个SP

下面来解释SWI软中断用于哪些场合?

一般系统调用时会频繁使用:

比如在用户层调用API接口write函数对磁盘进行写操作,那么通过系统调用就会进入linux系统内部,而且系统调用时会自带SWI软中断触发,从USER模式进入SVC模式,然后才能进行磁盘的读写,

img

协处理器指令:

几种常用的协处理器:

FPU :浮点运算协处理器

CP15 协处理器:

①:配置异常向量表的地址

②:MMU

@ 协处理器指令:操作控制协处理器的指令

@ 1.协处理器数据运算指令:

@ CDP

@ 2.协处理器存储器访问指令

@ STC 将协处理器中的数据存储到存储器中

@ LDC 将存储器中的数据读取到协处理器中

@ 3.协处理器寄存器传送指令

@ MRC 将协处理器中的寄存器中的数据传送到ARM处理器的寄存器

@ MCR 将ARM处理器中的寄存器中的数据传送到协处理器中的寄存器

伪指令:

@ 2.伪指令:本身不是指令,但是编译器能够将其替换成若干条指令

@ 空指令:

@ NOP

@ MOV R0,R0

@ LDR指令:LDR R1,[R2] //便于类比放到这里

@ LDR伪指令:

@ LDR R1,=0X12345678 @R1=0X12345678 @ 可以将任意一个32位数据放到寄存器中

@ LDR R1,=stop @将stop的地址写入到R1寄存器

@ LDR R1,stop @将stop地址中的内容写入到R1寄存器

stop : @ 死循环,防止程序跑飞

B stop

debug调试:

1.NOP空指令

img

通过对比可以看出,NOP与MOV R0,R0被编译器编译所生成的机器码是一样的,都是E1A00000,说明他俩其实被编译成了一条指令(注意这里换成R1就不对了,只能是R0) 【NOP==MOV R0,R0】

作用是让CPU什么也不干,空操作,消耗CPU一个机器周期的时间

但为什么NOP既然是伪指令,反汇编上还能显示NOP?不应该被编译成MOV R0,R0吗

img

  1. 0: e1a0f00f mov pc, pc

  2. 4: e1a0e00e mov lr, lr

  3. 8: e1a0d00d mov sp, sp

  4. c: e1a0c00c mov ip, ip

  5. 10: e1a0b00b mov fp, fp

  6. 14: e1a0a00a mov sl, sl

  7. 18: e1a09009 mov r9, r9

  8. 1c: e1a08008 mov r8, r8

  9. 20: e1a07007 mov r7, r7

  10. 24: e1a06006 mov r6, r6

  11. 28: e1a05005 mov r5, r5

  12. 2c: e1a04004 mov r4, r4

  13. 30: e1a03003 mov r3, r3

  14. 34: e1a02002 mov r2, r2

  15. 38: e1a01001 mov r1, r1

  16. 3c: e1a00000 nop ; (mov r0, r0)

  17. 40: e1a00000 nop ; (mov r0, r0)

也就是说无论写的是NOP还是MOV R0,R0,汇编时都会被替换成NOP空操作【其实这里NOP不是伪指令,而是MOV R0,R0的标识符】

拓展;

nop (No Operation) 指令作用:

1,通过 nop 指令的填充(nop指令长度从一个字节到九个字节,用于对齐),使指令对齐,从而减少取指令时的内存访问次数。一般用来内存地址偶数对齐,比如有一条指令占 3 字节,这时使用 nop 指令,CPU 就可以从第四个字节处读取指令。

2,清除由上一个算术逻辑指令设置的 flag 位。

3,破解:对于原程序中验证部分使用 nop 来填充,使验证失效。

2.LDR伪指令1(LDR R1,=0X12345678)

img

LDR R1,=0X12345678,作用是将任意一个32位的数放到寄存器,是一个伪指令,相比于MOV来说,可以写的数范围更宽,因为用MOV时需要判断是不是立即数,不是立即数的话编译器会报错,但是LDR可以随便写任意一个32位数据,所以如果想写32位的数又无法判断它是不是立即数,那就直接使用LDR R1,=0X12345678即可

另外,从反汇编可以看出,所写的伪指令:LDR R1,=0X12345678,被编译为0x00000000 E51F1000 LDR R1,[PC]

img

那么这句话什么意思呢?

其实它是将PC指向的内容写到R1寄存器,那PC指向哪里呢?其实当执行0x00000000 E51F1000 LDR R1,[PC]的时候,根据ARM三级流水线:取指时PC的值比译码的指令地址大4、比执行大8,所以PC指向0x08地址,即将0x08地址中的12345678写入到R1寄存器,从上面的调试图也可以看到,R1确实被写入了0x12345678

其实在内存中从低地址到高地址存放完汇编机器码后,又分配了4个字节的内存用来存放数据0X12345678

3.LDR伪指令2(LDR R1,=stop);

img

LDR R1,=stop是将stop的地址写入到R1寄存器,从现象可以验证:R1确实被写入了0x00000004地址,而且stop标号的第一条指令所在的地址恰好就是0x00000004,下面我们深入分析:

LDR R1,=stop是一条伪指令,被编译器编译成了0x00000000 E51F1000 LDR R1,[PC],这条指令的意思和上面的类似,也是将PC指向的内容写入到R1寄存器,根据ARM三级流水线:PC指向的地址应为0X00000008,而该地址中的内容为0X00000004,即将0X00000004写入到R1寄存器

4.LDR伪指令3(LDR R1,stop)

img

意义为将stop地址的内容写入到R1寄存器,同样根据上面的判断方法(三级流水线),可知是将0X00000004地址中的内容0XEAFFFFFE写入到R1寄存器【其实伪指令被编译成了文字池的形式】

网上摘下的:

LDR与LDR伪指令详解:

ARM32位指令的构成

ARM是RISC结构,数据从内存到CPU之间的移动只能通过LDR/STR指令来完成。 32bit = 指令码 + 数据。所以32bit的一条指令不可能表示再带一个32bit的数据,实际只有其中的12bit来表示立即数,其中4bit表示移位的位数(循环右移,且数值x2),8bit用来表示要移位的一个基数。这就产生了非法立即数和合法立即数的问题,经过移位操作,不为零的部分不能用8bit表示的数就是非法立即数。ldr伪指令就是用来解决非法立即数问题的。

ldr指令和ldr伪指令的使用区别:

ldr r0, =0xFFF0 @伪指令

ldr r0, 0xFFFF @指令

直观的区别就是ldr伪指令使用时,后面的数据前会有"=",实际使用时,大部分都使用伪指令,这样就不用考虑合法和非法立即数的问题。在编译的时候,编译器会将ldr伪指令进行替换,用文字池的方式来解决非法立即数的问题。文字池就是划分出一段地址空间用来存放常量或者地址,需要时用基址+变址的方式去取数据,这样就不用受到合法立即数的限制,可以表示32bit的数据。例如:

汇编源代码:

_start:

ldr r0, =0x11111111

经过反汇编:

00000000 <_start>:

0: e59f009c ldr r0, [pc, #156] ; a4 <delay_loop+0x10>

·

·

·

98: e1520003 cmp r2, r3

9c: 1afffffc bne 94 <delay_loop>

a0: e1a0f00e mov pc, lr

a4: 11111111 tstne r1, r1, lsl r1

分析:

通过反汇编可以看到,ldr伪指令被一条寄存器基址变址指令给替代了。其中以pc为基址,偏移156个字节(16进制是0x9c)。这条指令的作用是将内存地址"pc + 156"开头的4个字节读取到r0中,此时pc的值等于当前执行指令的地址+8(因为流水线的原因),因此pc + 156 = 0xa4,而0xa4地址处存的值刚好是0x11111111。这样就完成了将0x11111111加载到r0。

补充:

RAM处理器存在流水线,目前已经有十几级流水线,但是ARM为了兼容,无论Soc有多少级流水线,PC的值都是等于当前指令地址 + 8。PC = 当前指令地址 + 8, 记住就行。

原文链接:LDR指令和LDR伪指令详解-CSDN博客

调试问题:

问题:

1.PC是在取指后就自增4还是整个指令执行完才自增?

可以复习指令流水线,是在取指后自增4,这个过程叫做指令预取

ARM的R15(PC)总是指向取指的地方(取指的地方为高地址)。

当ARM处在ARM指令的时候,每条指令得长度为4,PC = 当前执行+8,当然如果处在THUMB指令中,每条指令长度为2,PC = 当前执行+4.

ARM正在执行第1条指令的同时对第2条指令进行译码,并将第3条指令从存储器中取出,所以,ARM7流水线只有在取第4条指令时,第1条指令才算完成执行。无论处理器处于何种状态,程序计数器R15(PC)总是指向“正在取指”的指令,即下图中的第三条指令。而不是指向“正在执行”的指令或者正在“译码”的指令。人们一般会习惯性的将正在执行的指令作为参考点,即当前第1条指令。所以,PC总是指向第3条指令,或者说PC总是指向当前正在执行的指令地址再加2条指令的地址。

原文链接:Arm的三级流水线-CSDN博客

2.那为什么R15的值是0X00000004?

img

这个还有待商榷,这里暂且先认为编译器中的R15指示的是当前要执行的指令的地址

贴吧大佬回复:keil软件PC指向的是下一条将要执行的指令,即上一步译码完成的指令

作业:

伪指令与指令的区别?

指令:是一种语句,它在程序汇编翻译时变得可执行,汇编器将其翻译成机器语言字节,并且在运行时由CPU加载和执行每一条指令语句表示CPU具有的一个基本能力,比如数据传送,两数相加或相减,移位等,而这种能力是在目标程序运行时完成的,是依赖于CPU、存储器、IO等接口设备来实现的。

伪指令:顾名思义,它不是真正的指令,也就是不是最终的指令,是用于指示汇编程序如何汇编源程序,所以这种语句又叫命令语句,例如伪指令告诉汇编程序,该源程序如何分段,有哪些逻辑段在程序段中,哪些是当前段等等,伪指令语句的这些命令功能是由汇编程序在汇编源程序时,通过执行另外一段程序来完成的,而不是在运行目标程序时实现的。

通俗地讲:

就好像召开新闻发布会,主持人用中文讲话,由一个翻译,现场翻译成英文,讲给外国记者听。

主持人说的中文,大部分都是要翻译成英文的;少数几句,是说给翻译听的,告诉他如何翻译。

我们写的指令,由“编译软件”翻译成机器码的,称为指令语句;有一些是写给“编译软件”看的,不翻译成机器码,这样的就是“伪指令”。

翻译人员—>编译软件

大部分中文化—>指令 是给外国记者听的

少部分中文话—>伪指令 是给翻听的

总结:指令是控制程序运行时的机器代码运作的,是CPU执行的依据,编程、编译、执行都是有效的。

伪指令不直接控制运行时刻的机器,但是控制翻译程序如何生成机器指令代码,也就是只为编译服务,编译完成后,伪 指令的作用也就消失了。

原文链接:指令和伪指令的区别及作用-CSDN博客


伪操作与混合编程:

伪操作:

@ 伪操作:不会生成代码,只是在编译阶段告诉编译器怎么编译

@ .GNU伪操作一般都以‘.’开头

@ .global symbol @将symbol声明为全局

@ .local symbol @将system声明为局部

@ .equ DATA, 0XFF @类似于C中的宏定义,注意英文逗号

@ MOV R1,#DATA

@ .macro FUNC @函数的封装

@ MOV R1,#1

@ MOV R2,#2

@ .endm

@ FUNC

@ .if 0 @条件编译

@ MOV R1,#1

@ MOV R2,#2

@ .endif

@ .rept 10

@ MOV R1,#1

@ MOV R2,#2

@ .endR

@ .weak symbol

@ .weak FUNC @弱化函数,即使没有定义FUNC也告诉编译器不要报错

@ B FUNC @那么编译器会处理成NOP空操作

@ .word @在当前地址申请一个字的空间并将其初始化

@ MOV R1,#1

@ .word 0XFFFFFFFE

@ MOV R2,#2

@ .byte @在当前地址申请一个字节的空间并将其初始化

@ MOV R1,#1

@ .byte 0XFF

@ .align 4 @参数代表后面的指令的机器码要以2^n的整数倍对齐

@ MOV R2,#2

@ .space @在当前地址申请任意多个字节的空间并将其初始化

@ MOV R1,#1

@ .space 12 ,0x12

@ MOV R2,#2

@ .arm @后面的指令都是arm指令

@ .thumb @后面的指令都是thumb指令

debug调试:

1.没有声明为全局,会找不到stop:

img

2.

.equ DATA ,0XFF

MOV R1,#DATA

类似于C语言中的宏定义

img

3..macro函数封装

img

4..weak弱化

img

5..word @在当前地址申请一个字的空间并将其初始化

img

6..byte @在当前地址申请一个字节的空间并将其初始化

Error: arm-asm.s: Error: unaligned opcodes detected in executable segment

原因:0X04地址内存被.byte占据,所以后面的指令会从0x5开始存放,0x5不是4的整数倍,所以编译器会报错,字节未对齐,下面加上.align n 用于字节对齐,指示后面的指令要以2的n次方的整数倍字节对齐

img

另外:伪指令与指令最终都会生成机器码,是由ARM公司规定的,而伪操作是用来告诉编译器怎么编译的,所以不同编译器的伪操作也不一样

C和汇编的混合编程:

通用指令:数据处理指令、跳转指令、load/store ,基本每一个CPU都支持这三种指令,并且C语言中都有对应的语句

专用指令:状态寄存器传送指令、SWI软中断指令、协处理器指令,ARM下有这几种指令,其他CPU不一定支持,另外C语言是通用的,肯定也不会有对应的指令。

任何一个芯片刚上电的第一段代码都是用汇编写的:比如用状态寄存器传送指令修改工作模式为USER模式、初始化栈等

有些功能C语言实现不了,会需要用到汇编

C和汇编的混合编程:

@ C和汇编混合编程的原则:在哪种语言环境下符合哪种语言的语法规则

@ 1.在汇编语言中将C语言中的函数视为标号来处理

@ 2.在C语言中将汇编语言中的标号视为函数来处理

arm-asm.s:

@ 1.汇编语言调用(跳转)C语言

MOV R1,#1

MOV R2,#2

BL func_c

MOV R3,#3

@ 2.C语言调用(跳转)汇编语言

.global FUNC_ASM

FUNC_ASM:

MOV R1,#2

MOV R2,#4

@ 3.C内联汇编

@ 在C语言中写汇编,且格式为:

@ asm

@ (

@ "MOV R3,#5\n"

@ "MOV R4,#6\n"

@ );

main.c:

void func_c(void)

{

int a=0,b=1;

a++;

asm

(

"MOV R3,#5\n"

"MOV R4,#6\n"

);

FUNC_ASM();

b++;

}

现在对子程序调用有了更清楚的认识:其实子程序调用的跳转本质就是编译为汇编之后的B +地址,执行此指令之后就会跳到对应的函数的第一条指令,所以为什么说函数名就是函数的入口地址,就是这个原因

FUNC_ASM() : 0x00000030 EBFFFFF6 BL 0x00000010

ATPCS协议:

函数的参数传递:为什么不都使用栈传参?为什么不使用所有的寄存器用来传参

寄存器位于CPU的内部,读写访问比较快,如果都是用栈传参,速度会很慢

但如果所有的寄存器都用来传参,寄存器本身数量就比较少,会影响其他指令的执行

所以ATPCS协议兼顾速度与可执行性,规定如果参数不多于4个使用R0-R3寄存器,多余4个的部分用栈传参

传参的本质:调用函数准备一个数给被调用函数去用

返回值的本质:被调用函数返回一个数给调用函数

@ ATPCS协议

@ 协议的主要内容:

@ 1.栈的种类

@ 使用满减栈

@ 2.寄存器的使用

@ R15: 程序计数器,只能存储程序的指针,不能做其他用途

@ R14: 链接计数器,只能存储返回地址,不能做其他用途

@ R13: 栈指针,只能用于存储栈指针,不能作其他用途,且栈指针只能用R13,不能使用其他的寄存器

@ R0-R3: 当传递的参数不多于4个时用R0-R3传参,多余4个的部分使用栈传参,R0寄存器用于传递函数的返回值

@ 其他寄存器用于存储局部变量

main.c:

int a=1,b=1,c=1,d=1,e=1,f=1,g;

int func(int a,int b,int c,int d,int e,int f);

int main()

{

g = func(a,b,c,d,e,f);

return 0;

}

int func(int a,int b,int c,int d,int e,int f)

{

return a+b+c+d+e+f;

}

debug调试:

img

下面我们对反汇编代码进行解析:

跳转前的准备工作:

①:0x00000030 E59FC038 LDR R12,[PC,#0x0038]

②:0x00000034 E59C0000 LDR R0,[R12]

③:0x00000038 E59C1004 LDR R1,[R12,#0x0004]

④:0x0000003C E59C2008 LDR R2,[R12,#0x0008]

⑤:0x00000040 E59C300C LDR R3,[R12,#0x000C]

⑥:0x00000044 E59CE010 LDR R14,[R12,#0x0010]

⑦:0x00000048 E58DE000 STR R14,[R13]

⑧:0x0000004C E59CC014 LDR R12,[R12,#0x0014]

⑨:0x00000050 E58DC004 STR R12,[R13,#0x0004]

⑩:0x00000054 EBFFFFE9 BL func(0x00000000)

1.0x00000058 E59F3014 LDR R3,[PC,#0x0014]

2.0x0000005C E5830000 STR R0,[R3]

①:将PC加0X0038地址中的内容写入R12,而当前PC的值为0X00000030+8=0X00000038,所以PC+OX0038=0X0070,而观察反汇编中0X0070,内容为0X00008138,所以①的含义是将0X00008138写入到R2中

②:将R2所指向的内容写入到R0寄存器,即将地址为0X00008138中的内容写入到R0寄存器,而观察右边的memory显示,内容为01,其实就是变量a

③:在R12地址的基础上偏移4个字节,将变量b写入到R1寄存器

④:在R12地址的基础上偏移8个字节,将变量c写入到R1寄存器

⑤:在R12地址的基础上偏移12个字节,将变量d写入到R1寄存器

⑥:在R12地址的基础上偏移16个字节,将变量e写入到R14寄存器

⑦:将R14中的内容e写入到R13指向的内存中,即栈指针指向的栈中

⑧:在R12地址的基础上偏移20个字节,将变量f写入到R12寄存器

⑨:将R12中的内容f写入到R13栈指针基础上偏移4个字节指向的内存中

⑩:跳转到func入口地址,且R14自动保存下一条指令地址

反汇编的压栈是用STR进行的,采用寄存器间接寻址或基址加变址寻址索引的方式

其实上面的操作是跳转前的参数准备,将各个参数放到合适的位置

跳转到func后:

①:0x00000000 E0801001 ADD R1,R0,R1

②:0x00000004 E0811002 ADD R1,R1,R2

③:0x00000008 E0811003 ADD R1,R1,R3

④:0x0000000C E59D2000 LDR R2,[R13]

⑤:0x00000010 E0810002 ADD R0,R1,R2

⑥:0x00000014 E59D2004 LDR R2,[R13,#0x0004]

⑦:0x00000018 E0800002 ADD R0,R0,R2

⑧:0x0000001C E12FFF1E BX R14

①:R0与R1内容相加,即a+b,结果存到R1

②:在R1基础上加上R2,结果存放到R1里,即此时R1=a+b+c

③:同样此时R1=a+b+c+d

④:将R13栈指针指向的内容e先读取到R2寄存器

⑤:R0=R1+R2,此时R0=a+b+c+d+e

⑥: R13栈指针基础上偏移4字节,再将偏移后的地址指向的内容f先读取到R2寄存器

⑦:R0=R0+R2,此时R0=a+b+c+d+e+f

⑧:跳转到R14,实现返回

跳转到mian函数后:

1.0x00000058 E59F3014 LDR R3,[PC,#0x0014]

2.0x0000005C E5830000 STR R0,[R3]

①:PC基础上偏移20个字节后的地址中的内容读取到R3寄存器,即将0X00008150读取到R3,其实0X00008150就是变量g的地址

②:再将R0写入到0X00008150指向的内存,R0即返回值,实现了返回值赋给g

问题:

可是前面说ATPCS协议规定R14只能存放返回地址,为什么反汇编里却可以用来存放参数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值