一弹指六十刹那,一刹那九百生灭。 --《仁王经》linux
计算机是一种数据处理设备,它由CPU和内存以及外部设备组成。CPU负责数据处理,内存负责存储,外部设备负责数据的输入和输出,它们之间经过总线链接在一块儿。CPU内部主要由控制器、运算器和寄存器组成。控制器负责指令的读取和调度,运算器负责指令的运算执行,寄存器负责数据的存储,它们之间经过CPU内的总线链接在一块儿。每一个外部设备(例如:显示器、硬盘、键盘、鼠标、网卡等等)则是由外设控制器、I/O端口、和输入输出硬件组成。外设控制器负责设备的控制和操做,I/O端口负责数据的临时存储,输入输出硬件则负责具体的输入输出,它们间也经过外部设备内的总线链接在一块儿。git
上面的计算机系统结构图中咱们能够看出硬件系统的这种组件化的设计思路老是贯彻到各个环节。在这套设计思想(冯.诺依曼体系架构)里面,老是有一部分负责控制、一部分负责执行、一部分则负责存储,它之间进行交互以及接口通讯则老是经过总线来完成。这种设计思路同样的能够应用在咱们的软件设计体系里面:组件和组件之间通讯经过事件的方式来进行解耦处理,而一个组件内部一样也须要明确好各个部分的职责(一部分负责调度控制、一部分负责执行实现、一部分负责数据存储)。github
一个完整的CPU系统里面有控制部件、运算部件还有寄存器部件。其中寄存器部件的做用就是进行数据的临时存储。既然有内存做为数据存储的场所,那么为何还要有寄存器呢?答案就是速度和成本。咱们知道CPU的运算速度是很是快的,若是把运算的数据都放到内存里面的话那将大大下降整个系统的性能。解决的办法是在CPU内部开辟一小块临时存储区域,并在进行运算时先将数据从内存复制到这一小块临时存储区域中,运算时就在这一小快临时存储区域内进行。咱们称这一小块临时存储区域为寄存器。由于寄存器和运算器以及控制器是很是紧密的联系在一块儿的,它们的频率一致,因此运算时就不会由于数据的来回传输以及各设备之间的频率差别致使系统性能的总体降低。你可能又会问为何不把整个内存都集成进CPU中去呢?答案其实仍是成本问题! 由于CPU速度很快,相应的寄存器也须要存取很快,两者速度上要匹配,因此这些寄存器的制做难度大,选材精,并且是集成到芯片内部,所价格高。而内存的成本则相对低廉,并且从工艺上来讲,咱们不可能在CPU内部集成大量的存储单元。 运算的问题经过寄存器解决了,可是还存在一个问题:咱们知道程序在运行时是要将全部可执行的二进制指令代码都装载到内存里面去,CPU每执行一条指令前都须要从内存中将指令读取到CPU内并执行。若是按这样每次都从内存读取一条指令来依次执行的话,那仍是存在着CPU和内存之间的处理瓶颈问题,从而形成总体性能的降低。这个问题怎么解决呢?答案就是高速缓存。其实在CPU内部不只有为解决运算问题而设计的寄存器,还集成了一个部分高速缓存存储区域。高度缓存的制形成本要比寄存器低,可是比内存的制形成本高,容量要比寄存器大,可是比内存的容量小不少。虽然没有寄存器和运算器之间的距离那么紧密,可是要比内存到运算器之间的距离要近不少。通常状况下CPU内的高速缓存可能只有几KB或者几十KB那么大。正是经过高速缓存的引入,当程序在运行时,就能够预先将部分在内存中要执行的指令代码以及数据复制到高速缓存中去,而CPU则再也不每次都从内存中读取指令而是直接从高速缓存依次读取指令来执行,从而加快了总体的速度。固然要预读取哪块内存区域的指令和数据到缓存上以及怎么去读取这些工做都交给操做系统去调度完成,这里面的算法和逻辑也很是的复杂,你们能够经过学习操做系统相关的课程去了解,这里就再也不展开了。能够看出高速缓存的做用解决了不一样速度设备之间的数据传递问题。在实际中CPU内部可能不止设有一级高速缓存,有可能会配备两级到三级的高速缓存,越高级的高速缓存速度越快,容量越低,而越低级的高度缓存则速度越慢,可是容量越大。好比iPhoneX上的搭载的arm处理器A11里面除了固有的37个通用寄存器外,L1级缓存的容量是64KB, L2级缓存的容量达到了8M(这么大的二级缓存,都有可能在你的程序代码少时能够一次性将代码读到缓存中去运行), 没有配备三级缓存。算法
咱们知道在软件设计上有一个所谓的空间换时间的概念,就是当两个对象之间进行交互时由于两者处理速度并不一致时,咱们就须要引入缓存来解决读写不一致的问题。好比文件读写或者socket通讯时,由于IO设备的处理速度很慢,因此在进行文件读写以及socket通讯时老是要将读出或者写入的部分数据先保存到一个缓存中,而后再统一的执行读出和写入操做。 能够看出不管是在硬件层面上仍是在软件层面上,当两个组件之间由于速度问题不能进行同步交互时,就能够借助缓存技术来弥补这种不平衡的情况编程
CPU执行的每条指令都由操做码和操做数组成,简单理解就是要对谁(操做数)作什么(操做码)。在CPU内部要运算的数据老是放在寄存器中,而实际的数据则有多是放在内存或者是IO端口中。所以咱们的程序其实大部分时间就是作了以下三件事情:swift
这三件事情都是跟寄存器有关,寄存器就是数据存储的中转站,很是的关键,所以在CPU所提供的指令中,若是操做数有两个时至少要有一个是寄存器。数组
;下面部分是arm64指令示例:
mov x0, #0x100 ;将常数0x100赋值给寄存器x0
mov x1, x0 ;将寄存器x0的值赋值给寄存器x1
ldr x3, [sp, #0x8] ;将栈顶加0x8处的内存值赋值给x3寄存器
add x0, x1, x2 ;x0 = x1 + x2 能够看出运算的指令必须放在寄存器中
sub x0, x1, x2 ;r0 = x1 - x2
str x1, [sp, #0x08] ;将寄存器x1中的值保存到栈顶加0x8处的内存处。
;下面部分是x64指令示例(AT&T汇编):
mov $0x100, %rax ;将常数0x100赋值给寄存器rax
mov %rax, %rbx ;将寄存器rax的值赋值给rbx寄存器
movq 8(%rax), %rbx ;将寄存器rax中的值+8并将所指向内存中的数据赋值给rbx寄存器
复制代码
因此不要将机器语言或者汇编语言当成是很复杂或者难以理解的语言,若是你仔细观察一段汇编语言代码时,你就会发现几乎大部分代码都是作的上面的三件事情。咱们在高级语言里面看到的只是变量,可是在低级语言里面看到的就是内存地址和寄存器,你能够将内存地址和寄存器也理解为定义的变量,带着这样的思路去阅读汇编代码时你就会发现其实汇编语言也不是那么的困难。在高级语言中咱们能够根据自身的须要定义出不少有特殊意义的变量,可是低级语言中由于寄存器就那么几个,它必需要被复用和重复使用,所以汇编语言中就会出现大量的将寄存器的内容保存到内存中的指令代码以及从内存中读取到寄存器中的指令代码。这些代码中有不少都有共性,只要在你实践中多去阅读,而后适应一下就很快可以很高兴的去看汇编代码了,熟能生巧吗。缓存
寄存器是CPU中的数据临时存储单元,不一样的CPU体系结构中的寄存器的数量是不一致的好比: arm64体系下的CPU就提供了37个64位的通用的寄存器,而x64体系下的CPU就提供了16个64位的通用寄存器。在说分类以前要说一下寄存器的长度问题。有时候咱们看汇编代码时会发现代码中出现了x0, w0(arm64); 或者rax, eax, ax, al(x64)。 它们之间有什么关系吗? 寄存器是存储单元,意味着它具有必定的容量,也就是每一个寄存器能保存的最大的数值是多少,也就是寄存器的位数。不一样CPU架构下的寄存器的位数有差异,这个跟CPU的字长有关系。通常状况下64位字长的CPU提供的寄存器的容量是64个bit位,而32位字长的CPU提供的寄存器的容量是32个bit位。好比arm64体系下的CPU提供的37个通用寄存器的容量都是8个字节的,因此每一个寄存器能保存的数值范围就是(0到2^64次方)。bash
对于x64系的CPU来讲,若是寄存器以r开头则代表的是一个64位的寄存器,若是以e开头则代表是一个32位的寄存器,同时系统还提供了16位的寄存器以及8位的寄存器。32位的寄存器是64位寄存器的低32位部分并非独立存在的,16位寄存器则是32位寄存器的低16位部分并非独立存在的,8位寄存器则是16位寄存器的低8位部分并非独立存在的。网络
对于arm64系的CPU来讲, 若是寄存器以x开头则代表的是一个64位的寄存器,若是以w开头则代表是一个32位的寄存器,在系统中没有提供16位和8位的寄存器供访问和使用。其中32位的寄存器是64位寄存器的低32位部分并非独立存在的。
无论寄存器的长度如何,它们有些用来存放将要执行的指令地址,有些用来存储要运算的数据,有些用来存储计算的结果状态,有些用来保存内存的基地址信息,有些用来保存要运算的浮点数。所以CPU中的寄存器能够按照做用进行以下分类:
数据地址寄存器一般用来作数据计算的临时存储、作累加、计数、地址保存等功能。定义这些寄存器的做用主要是用于在CPU指令中保存操做数,在CPU中当作一些常规变量来使用。因此咱们的代码里面看到的以及用到的最多的寄存器就是这些寄存器:
体系结构 | 长度 | 名称 |
---|---|---|
x64 | 64 | RAX,RBX,RCX,RDX,RDI,RSI, R8-R15 |
x64 | 32 | EAX,EBX,ECX,EDX,EDI,ESI, R8D-R15D |
x64 | 16 | AX,BX,CX,DX,DI,SI, R8W-R15W |
x64 | 8 | AL,BL,CL,DL,DIL,SIL, R8L-R15L |
arm64 | 64 | X0-X30, XZR |
arm64 | 32 | W0-W30, WZR |
若是你仔细观察一些汇编代码中的寄存器的使用,其实你会发现一些特色:
早期的16位实模式程序中的内存访问都是基于物理地址的,并且还把整个程序拆分为数据段、代码段、栈段、扩展段四个区域,每一个内存区段内的地址编码都是相对于这个段的偏移来设置的,所以为了定位和区分这些内存区段,CPU分别设置了CS,DS,SS,ES四个寄存器来保存这些段的基地址。后来随着CPU和操做系统的发展,应用程序再也不直接访问物理内存地址了,而是访问由操做系统提供的虚拟内存地址,同时也再也不把整个内存空间划分为数据段和代码段了,而是提供一个从0开始的平坦连续的内存空间了,同时将程序所能访问的内存区域和操做系统内核所能访问的内存区域进行了隔离,咱们称这样的程序为保护模式下运行的程序。所以这时候里面的CS,DS,SS,ES寄存器的做用将再也不用于保存内存区域的基地址了,同时还增长了FS,GS两个寄存器,这6个寄存器的做用变为了保存操做系统进入用户态仍是核心态以及进行用户态和核心态之间进行切换上下文数据的功能了。也就是在保护模式下运行的程序咱们将不须要也没有权利去访问这些段寄存器了。若是你想了解更加具体的内容请搜索:全局描述符表与局部描述符表 相关的知识。在arm体系的CPU中则没有专门提供这些所谓的段寄存器:
体系结构 | 长度 | 名称 |
---|---|---|
x64 | 16 | CS,DS,SS,ES,FS,GS |
这里面须要澄清的是咱们的程序内存区域虽然从物理上再也不划分为代码段、数据段、栈段几个独立的内存空间。可是在平坦内存模式下咱们依然保留了代码段、数据段、栈段的划分,每一个段的基地址都是从0开始,只是各类类型的数据存放到了不一样的内存空间中去了,也就是说程序分段的机制由硬件划分转化为了软件划分了。
栈的概念,在学习数据结构的时候就已经有了解,栈是一块具备后进先出功能的存储区域,在进行操做时咱们老是只能将数据压入栈顶,或者将数据从栈顶弹出来。
从上面能够看出要维护一个栈区域就必需要提供2个寄存器,一个寄存器用来保存栈的基地址也就是栈的底部,而一个寄存器则用来保存栈的偏移也就是栈的顶部。在通常的系统中,咱们都将栈的基地址设置在内存的高位,而将栈顶地址设置在内存的低位。所以每当有进栈操做时则将栈顶地址进行递减,而当有出栈操做时则将栈顶地址递增。栈的这种特性,使得他很是适合于保存函数中定义的局部变量,以及函数内调用函数的状况。(具体栈和函数的关系我会在后续的文章中详细介绍)。在x64体系的CPU中,提供了一个专门的RBP寄存用来保存栈的基地址, 同时提供一个专门的RSP寄存器来保存栈的栈顶地址;而arm64体系的CPU中则没有设置专门的栈基址寄存器而是通常用X29寄存器来保存栈的基地址(至少在iOS的64位系统里面是如此的),可是设置一个SP寄存器来保存栈的栈顶地址。
体系结构 | 长度 | 名称 |
---|---|---|
x64 | 64 | RBP为栈基址寄存器,RSP为栈顶寄存器 |
x64 | 32 | EBP为栈基址寄存器,ESP为栈顶寄存器 |
x64 | 16 | BP为栈基址寄存器,SP为栈顶寄存器 |
arm64 | 64 | X29为栈基址寄存器,SP为栈顶寄存器 |
arm64 | 32 | W29为栈基址寄存器,WSP为栈顶寄存器 |
由于浮点数的存储以及其运算的特殊性,因此CPU中专门提供FPU以及相应的浮点数寄存器来处理浮点数,除了一些浮点数状态和控制寄存器(好比四舍五入的处理方式等)外主要就是一些保存浮点数的寄存器:
体系结构 | 长度 | 名称 |
---|---|---|
x64 | 128 | XMM0 - XMM15 |
arm64 | 64 | D0 - D31 |
arm64 | 32 | S0 - S31 |
如今的CPU除了支持标量运算外,还支持向量运算。向量运算在图形处理相关的领域用得很是的多。为了支持向量计算系统了也提供了众多的向量寄存器,以及SSE和SIMD指令集:
体系结构 | 长度 | 名称 |
---|---|---|
x64 | 128 | XMM0 - XMM15, YMM0-YMM15, STMM0-STMM7 |
arm64 | 128 | V0-V31 |
状态寄存器用来保存指令运行结果的一些信息,好比相加的结果是否溢出、结果是否为0、以及是不是负数等。CPU的某些指令会根据运行的结果来设置状态寄存器的状态位,而某些指令则是根据这些状态寄存器中的值来进行处理。好比一些条件跳转指令或者比较指令等等。咱们在高级语言里面的条件判断最终在转化为机器指令时,机器指令就是根据状态寄存器里面的特殊位置来进行跳转的。**在x64体系的CPU中提供了一个64位的RFLAGS寄存器来做为状态寄存器;arm64体系的CPU则提供了一个32位的CPSR寄存器来做为状态寄存器。**状态寄存器的内容由CPU内部进行置位,咱们的程序中不能将某个数值赋值给状态寄存器。
体系结构 | 长度 | 名称 |
---|---|---|
x64 | 64 | RFLAGS |
arm64 | 32 | CPSR |
咱们知道程序代码是保存在内存中的,那CPU又是如何知道要执行哪一条保存在内存中的指令呢?这就是经过指令寄存器来完成的。由于内存中的指令老是按线性序列保存的,CPU只是按照编制好的程序来执行指令。所以CPU内提供一个指令寄存器来记录CPU下一条将要执行的指令的内存地址,这样每次执行完毕一条指令后,CPU就根据指令寄存器中所记录的地址到内存中去读取指令并执行,同时又将下一条指令的内存地址保存到指令寄存器中,就这样就重复不断的处理来完成整个程序的执行。
可是这里面有两问题:
前面不是说CPU内有高速缓存吗?怎么又说每次都去访问内存呢?并且保存仍是内存的地址呢。 这是没有问题的,指令寄存器中保存的确实是下一条指令在内存中的地址,可是操做系统除了将部份内存区域中的指令保存到高速缓存外还会创建一个内存地址到高速缓存地址之间的映射关系数据结构。所以即便是指令寄存器中保存的是内存地址,可是在指令真实执行时CPU就会根据指令寄存器中的内存地址以及内部创建的内存和高速缓存的映射关系来转化为指令在高速缓存中的地址来读取指令并执行。固然若是发现指令并不在高速缓存中时,CPU就会触发一个中断并告诉操做系统,操做系统再根据特定的策略从内存中再次读取一块新的内存数据到高速缓存中,并覆盖掉原先保存在高速缓存中的内容,而后CPU再次读取高速缓存中的指令后继续执行。
若是说指令寄存器每次都是保存的顺序执行指令的话那么怎么去实现跳转逻辑呢? 答案是跳转指令和函数调用指令的存在。咱们的用户态中的代码不能去人为的改变指令寄存器的值,也就是不能对指令寄存器进行赋值,所以默认状况下指令寄存器老是由CPU内部设置为下一条指令的地址,可是跳转指令和函数调用指令例外,这两条指令的主要做用就是用来改变指令寄存器的内容,正是由于跳转功能才使得咱们的程序能够不仅按顺序去执行而是具备条件执行和循环执行代码的能力。
在x64体系的CPU中提供了一个64位的指令寄存器RIP,而在arm64体系的CPU中则提供了一个64位的PC寄存器。须要再次强调的是指令寄存器保存的是下一条将要执行的指令的内存地址,而不是当前正在执行的指令的内存地址。
体系结构 | 长度 | 名称 |
---|---|---|
x64 | 64 | RIP |
x64 | 32 | EIP |
arm64 | 64 | PC, LR |
这里再看一下arm64体系下的PC和LR寄存器,咱们先看下面一张图:
从上面的图中咱们能够看出PC寄存器和LR寄存器所表示的意义:PC寄存器保存的是下一条将要执行的指令的内存地址,而不是当前正在执行的指令的内存地址。LR寄存器则保存着最后一次函数调用指令的下一条指令的内存地址。那么LR寄存器有什么做用吗?答案就是为了作函数调用栈跟踪,咱们的程序在崩溃时可以将函数调用栈打印出来就是借助了LR寄存器来实现的。具体的实现原理我会在后面的文章里面详细介绍。
上面列出的都是咱们在编程时会用到的寄存器,其实CPU内部还有不少专门用于控制的寄存器以及用于调试的寄存器,这些寄存器通常都提供给操做系统使用或者用于CPU内部调试使用。这里就再也不进行介绍了,感兴趣的同窗能够去下载一本x64或者arm手册进行学习和了解。
这里面须要澄清的是上述中的寄存器名称只是汇编语言里面对寄存器的一个别称或者有意义的命名,咱们知道机器指令是二进制数据,一条机器指令里面不管是操做码仍是操做数都是二进制编码的,二进制数据太过晦涩难以理解,因此才有了汇编语言的诞生,汇编语言是一种机器指令的助记语言,他只不过是以人类更容易理解的天然语言的方式来描述一条机器指令而已。因此虽然上面的寄存器看到的是一个个字母,可是在机器语言里面,则是经过给寄存器编号来表示某个寄存器的。还记得在个人介绍指令集的文章里面,你有看到过里面的虚拟CPU里面的寄存器的定义吗:
//定义寄存器编号
typedef enum : int {
Reg0,
Reg1,
Reg2,
Reg3
} RegNum;
复制代码
上面的枚举你能够看到咱们在代码里面用Reg0, Reg1...来表示虚拟的寄存器编号,可是实际的寄存器编号则分别为0,1... 真实中的CPU的寄存器也是如此编号的,咱们来看下面一段代码,以及其中的机器指令:
mov x0, #0x0 ;0xD2800000
mov x1, #0x0 ;0xD2800001
mov x2, #0x0 ;0xD2800002
复制代码
mov指令的二进制结构以下:
可见上面的二进制机器指令中关于寄存器部分的字段Rd分别从0到2而出现了差别,从而说明了寄存器读写的编码规则。寄存器编码的机制和内存地址编码是一样的原理和机制,CPU访问内存数据时老是要指定内存数据所在的地址,一样CPU访问某个寄存器时同样的要经过寄存器编码来完成,这些东西通通都体如今指令里面。
上面分别介绍了两种不一样CPU上的寄存器,那么咱们如何来查看和设置寄存器的内容呢?在XCODE中能够很方便的在代码执行到断点时查看当前线程中的全部寄存器中内容(请选择最左下角处的all表示显示全部变量)。咱们能够经过下面两张图来查看全部的寄存的信息。
上面两图中的左下角列出了执行到某个断点时全部寄存器的当前值,你能够看到其中的通用寄存器(General Purpose Registers)、浮点寄存器(Floating Point Registers)、异常状态寄存器(Exception State Registers)中的数据。通用寄存器中的每一个寄存器默认都是一个64位长度的存储单元。查看左下角的寄存器值惟一的缺点是你没法看出寄存器中的保存的数据的真实类型,而只能干巴巴的看到16进制的数值。其实你能够将寄存器理解一个个特殊定义的变量,既然能够在lldb中经过expr或者p命令来显示某个变量的更加详细的信息,那么也同样的能够显示某个寄存器当前保存的数据的详细信息。经过看上面图片的右下角你能够看出,**要想打印显示某个寄存器的内容,咱们在使用expr或者po时 只须要在显示的寄存器的前面增长一个$便可。**好比下面的例子中咱们分别显示模拟器下的rdi, rsi以及真机下的x0和x1寄存器中的内容:
//模拟器下
expr -o -- $rdi
expr (char*)$rsi
//真机下
expr -o -- $x0
expr (char*)$x1
expr $r12 = 100; //和变量同样你也能够手动改变寄存器的值
复制代码
当你在某个OC方法内部断点并打印这两个寄存器的值时,大多数状况下你会发现rdi/x0老是指向一个OC的self对象,而rsi/x1则是这个方法的方法名。没有错,这是系统的一个规定:在任何一个OC方法调用前都会将寄存器rdi/x0的值设置为调用方法的对象,而将寄存器rsi/x1设置为方法的签名也就是方法的SEL(具体的缘由我会在后面的文章中详细说明缘由)。很惋惜的是上面的这套读取和设置寄存器的语法在swift中就失效了,当你要在swift中读取和写入寄存器的内容时你应该采用: register read 寄存器
register write 寄存器 值 的方式来读取和写入某个寄存器的值了,好比下面的例子(lldb中):
register read x0 //读取x0寄存器的值,这里再也不须要附加$符号了
register read //读取全部寄存器的值
register write x10 100 //将寄存器的x10的值设置为100
复制代码
arm64体系的CPU中虽然定义X29,X30两个寄存器,可是你在XCODE上是看不到这两个寄存器的,可是你能看到FP和LR寄存器,其实X29就是FP, X30就是LR。
咱们的代码并非只在单线程中执行,而是可能在多个线程中执行。那么这里你就可能会产生一个疑问?既然进程中有多个线程在并行执行,而CPU中的寄存器又只有那么一套,若是不加处理岂不会产生数据错乱的场景?答案是否认的。咱们知道线程是一个进程中的执行单元,每一个线程的调度执行其实都是经过操做系统来完成。也就是说哪一个线程占有CPU执行以及执行多久都是由操做系统控制的。具体的实现是每建立一个线程时都会为这线程建立一个数据结构来保存这个线程的信息,咱们称这个数据结构为线程上下文,每一个线程的上下文中有一部分数据是用来保存当前全部寄存器的副本。每当操做系统暂停一个线程时,就会将CPU中的全部寄存器的当前内容都保存到线程上下文数据结构中。而操做系统要让另一个线程执行时则将要执行的线程的上下文中保存的全部寄存器的内容再写回到CPU中,并将要运行的线程中上次保存暂停的指令也赋值给CPU的指令寄存器,并让新线程再次执行。能够看出操做系统正是经过这种机制保证了即便是多线程运行时也不会致使寄存器的内容发生错乱的问题。由于每当线程切换时操做系统都帮它们将数据处理好了。下面的部分线程上下文结构正是指定了全部寄存器信息的部分:
//这个结构是linux在arm32CPU上的线程上下文结构,代码来自于:http://elixir.free-electrons.com/linux/latest/source/arch/arm/include/asm/thread_info.h
//这里并无保存全部的寄存器,是由于ABI中定义linux在arm上运行时所使用的寄存器并非全体寄存器,因此只须要保存规定的寄存器的内容便可。这里并非全部的CPU所保存的内容都是一致的,保存的内容会根据CPU架构的差别而不一样。
//由于iOS的内核并未开源因此没法获得iOS定义的线程上下文结构。
//线程切换时要保存的CPU寄存器,
struct cpu_context_save {
__u32 r4;
__u32 r5;
__u32 r6;
__u32 r7;
__u32 r8;
__u32 r9;
__u32 sl;
__u32 fp;
__u32 sp;
__u32 pc;
__u32 extra[2]; /* Xscale 'acc' register, etc */
};
//线程上下文结构
struct thread_info {
unsigned long flags; /* low level flags */
int preempt_count; /* 0 => preemptable, <0 => bug */
mm_segment_t addr_limit; /* address limit */
struct task_struct *task; /* main task structure */
__u32 cpu; /* cpu */
__u32 cpu_domain; /* cpu domain */
struct cpu_context_save cpu_context; /* cpu context */
__u32 syscall; /* syscall number */
__u8 used_cp[16]; /* thread used copro */
unsigned long tp_value[2]; /* TLS registers */
#ifdef CONFIG_CRUNCH
struct crunch_state crunchstate;
#endif
union fp_state fpstate __attribute__((aligned(8))); /*浮点寄存器*/
union vfp_state vfpstate; /*向量浮点寄存器*/
#ifdef CONFIG_ARM_THUMBEE
unsigned long thumbee_state; /* ThumbEE Handler Base register */
#endif
};
复制代码
寄存器数据被切换的问题也一样会出如今函数的调用上,举个例子来讲:假设咱们正在调用foo1函数,在foo1中咱们的代码指令会用到x0,x1,x2等寄存器进行数据运算和存储。假设咱们在foo1中的某处调用foo2函数,这时候由于foo2函数内部的代码指令也可能会用到x0,x1,x2等寄存器。那么问题就来了,由于foo2内部的执行会改变x0,x1,x2寄存器的内容,那么当foo2函数返回并再次执行foo1下面的代码时,就有可能x0,x1,x2等寄存器的内容被改动而跟原先的值不一致了,从而致使数据错乱问题的发生。那么这又是如何解决的呢?解决的方法就是由编译器在编译出机器指令时按必定的规则进行编译(这是一种ABI规则,什么是ABI后续我会详细介绍)。 咱们知道在高级语言中定义的变量不管是局部仍是全局变量或者是堆内存分配的变量都是在内存中存储的。编译为机器指令后,对内存数据进行处理时则老是要将内存中的数据转移到寄存器中进行,而后再将处理的结果写回到内存中去,这种场景会发生在每次进行变量访问的情形中。咱们来看以下的高级语言代码:
void foo2()
{
int a = 20;
a = a + 2;
int b = 30;
b = b * 3;
int c = a + b;
}
void foo1()
{
int a = 10;
int b = 20;
int c = 30;
a += 10;
b += 10;
c += 10;
foo2();
c = a + b;
}
复制代码
虽然咱们在foo1和foo2里面都定义了a,b,c三个变量,可是由于这三个变量分别保存在foo1和foo2的不一样栈内存区,他们都是局部变量所以两个函数之间的变量是不会受到影响的。可是若是是机器指令则不同了,由于运算时老是要将内存数据移动到寄存器中去,可是寄存器只有一份。所以解决的方法就是高级语言里面的每一行代码在编译为机器指令时老是先将数据从内存读取到寄存器中,处理完毕后当即写回到内存中去,中间并不将数据进行任何在寄存器上的缓存
从上面的代码对应关系能够看出,每次高级语言的赋值处理老是先读取再计算而后再写回三步,所以当调用foo2函数前,全部寄存器其实都是处于空闲的或者能够被任意修改的状态。而调用完毕函数后要访问变量时又再次从内存读取到寄存器,运算完毕后再当即写回到内存中。正是这种每次访问数据时都从内存读取到寄存器,处理后当即再写会内存的机制就足以保证了即便在函数调用函数时也不会出现数据混乱的问题发生。
上面是对寄存器复用的两种不一样的策略:空间换时间和时间换空间。 在软件设计中当存在有某个共享资源被多个系统竞争或者使用时咱们就能够考虑采用上面的两种不一样方案来解决咱们的问题。
👉【返回目录】