一、什么是汇编?为什么学汇编?
在之前写控制代码的时候就在想:底层是怎么控制的?后来经过学习知道之前所编写的代码都是应用层代码,顾名思义就是在系统写好的底层之上调用系统函数。原以为底层是指写系统写好的底层函数,类似于写linux系统函数read、open、close之类的函数。但是再往下挖:read、open这类函数的下一层是什么?经过系统性查资料,总结了下面的流程:
从上面的图可以知晓:linux系统所提供的IO函数属于应用层,并未涉及到最底层。open、read等函数通过file_operation
结构体去调用驱动程序中的自定义的接口函数。所说的写驱动就是写自定义的device_open()、device_close()、device_write()、device_read()、device_ioctl()
函数这些名字随便起。
1.1 那么这些自己编写的驱动函数是如何写入硬件设备中?
众所周知机器所能识别的只有0和1二进制码,如果想要写入硬件设备,就要转换成二进制码写给机械。那么就又引入一个问题:
我们所写的C语言代码,怎么转换成二进制码?转换成机械识别的指令?
提一下C语言编译流程:.c---->.i----->.s------>.o-------->二进制
1.2 由此引入汇编指令:
汇编语言是直接与机器打交道的,机器只能读懂二进制码,在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。汇编语言是面向机器的,处于整个计算机语言层次结构的底层,故被视为一种低级语言。从上面的图可知,预处理部分是将程序中所引用的文件打开,汇编部分是将C语言转换成可被机器识别的代码,针对CPU中的寄存器进行操作。
汇编是一种语言,和C语言一样,有自己的编写方式,不过指令个人感觉有点难懂。
知道汇编是什么了,但还是没有解决问题:使用汇编写入硬件设备,但是写进硬件设备的哪里去了?至此引入最重要的一块:寄存器。
二、寄存器
2.1 为什么要用寄存器?
一块成熟的板子笼统分两部分:CPU和内存,使用流程是从CPU从内存中搬出来数据使用(大致如此)。
CPU 本身只负责运算,不负责储存数据。数据一般都储存在内存之中,CPU 要用的时候就去内存读写数据。但是,CPU 的运算速度远高于内存的读写速度,为了避免被拖慢,CPU 都自带一级缓存和二级缓存。(CPU 缓存可以看作是读写速度较快的内存。)
CPU缓存还不够快,并且数据在缓存的地址不固定,导致CPU每次读写都会拖慢速度。因此,CPU还自带寄存器:用来储存最常用的数据。也就是说,那些最频繁读写的数据(比如循环变量),都会放在寄存器里面,CPU 优先读写寄存器,再由寄存器跟内存交换数据。
寄存器不依靠地址区分数据,而依靠名称。每一个寄存器都有自己的名称,我们告诉 CPU 去具体的哪一个寄存器拿数据,这样的速度是最快的。有人比喻寄存器是 CPU 的零级缓存。
总结一下就是寄存器是CPU用来存储频繁读写的数据(变量之类的)
比如硬件有个固定地址0x300000,那么想要往这个地址里面写东西的时候,就需要将地址先暂存在寄存中使用。
下面说一下寄存器种类
2.2 寄存器种类
参考文档:ARM寄存器组织(常见专用寄存器、控制寄存器CPSR)_arm cpsr-CSDN博客
寄存器分为三种:通用寄存器、专用寄存器(有特定的用途和功能,只能存放指定内容的寄存器。常见的有sp、lr、pc寄存器)、控制寄存器CPSR。
2.2.1 通用寄存器
没有指定用途,可以存放任意内容的寄存器,既可以存放地址,也可以存放参与运算的数据或者运算产生的结果。在上图中:除去FIQ模式下,r0-r12是通用寄存器。
2.2.2 栈指针寄存器 r13 SP
栈指针,用于存储当前模式下的栈顶地址。假设现在CPU要将运算结果保存到栈上,这个时候SP寄存器就会告诉CPU栈顶的位置在哪,保存完毕以后,SP指向的地址会更新。
2.2.3 链接寄存器 r14 LR
链接寄存器,也是保存指令地址,一般是发生跳转的时候,事先保存跳转指令下一条指令的地址,一般有两种用途:
1、调用函数发生跳转
假设main
函数在执行程序的时候,需要调用函数func()
,这个时候会跳转到func()
函数的定义。由于执行完func()函数以后还要继续运行main
函数,在跳转之前LR寄存器会保存func()
函数下一条指令的地址,也就是printf函数的地址。在执行完func
函数以后,只需要让PC = LR
就可以回到func()
函数下一条指令的地址。
2、异常发生(产生中断)
产生异常时,异常模式下的LR会自动保存被异常打断的指令的下一条指令的地址。(也可以理解成是一种函数跳转)。比如CPU正在忙手里的任务,突然收到了网卡发来的信号,CPU就会进入FIQ或者IRQ模式,此时CPU就会停下手里的任务,转而先去执行异常处理程序。异常处理结束后将LR的值复制到PC可实现程序返回。
没有哪个寄存器可以检测异常产生,因为能检测的就不是异常产生了。产生异常的时候LR
默认自动保存下一条指令的地址,处理完异常情况再返回。
异常情况时要在PC
特定位置执行相应错误处理,由于偏移量(地址)是固定死的。所以当发生异常情况时,会自动跳转到该异常情况的地址执行处理异常,但PC寄存器
是一条指令4字节并且是按照下面的顺序存放的(系统要求给的,只能这么写)。无法在此处处理异常,因为处理异常肯定不止一条指令,所以这里指的都是b跳转
到相应标签位置处理异常
异常的8种情况,这个需要在汇编启动代码中写出来,否则无法正常运行程序:
偏移量(地址) | 异常情况 | 注释 |
0x1C | FIQ | 快中断 |
0x18 | IRQ | 慢中断 |
0x14 | (Reserved) | 保留异常,现在没有用 |
0x10 | Data Abort | 数据访问异常 访问合法空间的数据发现数据不存在,访问了非法的空间(越界) |
0x0c | Prefetch Abort | 指令预取异常, 对程序指令预取时产生的异常 |
0x08 | Software Interrupt(SWI) | 软中断 即User模式级别使用代码发出的中断 系统调用就是使用软中断从用户模式切换到特权模式的 |
0x04 | Undefined Instruction | 未定义异常 |
0x00 | Reset | 重启异常, 模式切换到Supervisor模式 |
-
通常说的异常主要来自cpu内部,abort,reset等;而中断主要来自外设。
-
从高到低的顺序依次为:Reset,Data Abort, FIQ, IRQ,Prefetch Abort,SWI(软中断),undefined Instruction
-
高优先级的异常会终止底先级的异常
-
上面的异常向量表在代码中的体现
2.2.4 程序指针寄存器 r15 PC
程序计数器,用于存储当前取址指令的地址。我们写的程序在经过预处理、编译、汇编以后,得到的二进制机器码就是指令。这些指令是被保存在内存中的,CPU接下来要执行哪一条指令都是由PC控制的。
我们写程序时的逻辑是顺序执行,那么CPU在执行指令的时候也是如此,在ARM状态下,每一条指令都占4个字节,所以每执行完一条指令,PC的值会自动自增4个字节(地址自增4字节),为下一次取指令做准备
2.2.5 控制寄存器 CPSR
CPSR很重要,该寄存器里面的每个位都代表了各自含义,下图有一些说明
举个例子:比如在进行加法运算的时候,CPSR的C位(进位)会变化
adc:要计算十进制 3389+443 ,但是只会计算和记录两位数,于是先计算了 89+43=132 ,但是你只能记住两位数,所以记下了 32,然后把进位出的1扔进了CF 暂存;然后再计算 33+4=37 ,顺便加上 CF 的进位得到 38,跟刚才的 32 组合起来得到结果3832
相对值得注意的是工作模式,由于板子在运行程序的时候肯定会遇到各种情况,那么其应对各种情况也是有不同的工作模式。ARM总共有7个基本工作模式:
那么体现在CPSR中MODE位,不同的编码值对应不同的模式
2.3 异常处理流程
上面说完寄存器的种类后,那么他工作的流程顺序是如何的?
在没有异常的时候,也是按照C语言执行顺序一样,按顺序执行,遇到特殊标记跳转。
在有异常的时候,那么就需要对一些寄存器进行配置,流程如下图:
三、总结
简单了解完寄存器之后,那么可以解决第一个问题:为什么使用汇编?
汇编语言的大部分语句直接对应着机器指令,执行速度快,效率高,代码体积小
在系统程序的核心部分,以及与系统硬件频繁打交道的部分,可以使用汇编语言。比如操作系统的核心程序段。
缺点:
不同的处理器有不同的汇编语言语法和编译器,编译的程序无法在不同的处理器上执行,缺乏可移植性
难懂且工作量大
C语言与汇编是可以交叉使用的,C语言的好处是易懂,但是要相对寄存器直接操作是很复杂的,不如汇编直接了当,毕竟汇编指令是直接操作寄存器的,可以简单了解一些汇编指令。
自己总结的一个图,简单一点,可能具体情况也会更复杂。
写的文章可能还会存在缺陷,我自己是这么理解的,参考了其他博主一些文章,文章内容觉得写得好就引用了一些,勿怪勿怪。关于汇编和寄存器,弄懂就好