最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

《嵌入式C语言自我修养:从芯片、编译器到操作系统》读书笔记

运维笔记admin1浏览0评论

第1章 工欲善其事,必先利其器

1.1 代码编辑工具:Vim

1.1.1 安装Vim

Ubuntu环境下:

#apt-get install vim

1.1.2 Vim常用命令

Vim常见的工作模式如下。
● 普通模式:打开文件时的默认模式,在其他模式下按下ESC键都可返回到该模式。
● 插入模式:按i/o/a键进入该模式,进行文本编辑操作,不同之处在于插入字符的位置在光标之前还是之后。
● 命令行模式:普通模式下输入冒号(:)后会进入该模式,在该模式下输入命令,如输入:set number或:set nu可以显示行号。
● 可视化模式:在普通模式下按v键会进入可视化模式。在该模式下移动光标可以选中一块文本,然后可以进行复制、剪切、删除、粘贴等文本操作。
● 替换模式:在普通模式下通过光标选中一个字符,然后按r键,再输入一个字符,你会发现你输入的字符就替换掉了原来那个被选中的字符。在该模式下进行文本替换很方便,省去了先删除再插入这种常规操作。

光标移动
k:在普通模式下,敲击k键,光标向上移动一个字符。
j:在普通模式下,敲击j键,光标向下移动一个字符。
h:在普通模式下,敲击h键,光标向左移动一个字符。
l:在普通模式下,敲击l键,光标向右移动一个字符。
$:将光标移动到当前行的行尾。
0:将光标移动到当前行的行首。
nG:光标跳转到指定的第n行。
gg/G:光标跳转到文件的开头/末尾

文本的基本操作
i/a:在当前光标的前或后面插入字符。
dd:删除当前光标所在处的一整行。
2dd:删除当前光标所在处的一整行和下一行。
yw:复制一个单词。
yy:复制光标所在处的一整行。
p:粘贴,注意是粘贴到光标所在处的下一行。

文本的查找与替换
/string:在Vim的普通模式下输入/string即可正向往下查找字符串string。
?string:反向查找字符串string。
:set hls:高亮显示光标处的单词,敲击n浏览下一个。
s/old/new:将当前行的第一个字符串old替换为new。
s/old/new/g:将当前行的所有字符串old替换为new。
%s/old/new/g:将文本中所有字符串old替换为new。
%s/^old/new/g:将文本中所有以old开头的字符串替换为new。

文本的保存与退出
u:撤销上一步的操作。
q:若文件没有修改,则直接退出。
q!:若文件已修改,则放弃修改,退出。
wq:若文件已修改,则保存修改,退出。
e!:若文件已修改,则放弃修改,恢复文件打开时的状态。
w !sudo tee %:在Shell的普通用户模式下保存root读写权限的文件,%表示当前的文件名。

1.1.3 Vim配置文件:vimrc

Vim配置文件分为系统级配置文件和用户级配置文件。用户级配置文件只对当前用户有效,一般位于$HOME/.vimrc和~/.vim/vimrc这两个路径下,而系统级配置文件则对所有用户都有效,一般位于/etc/vim/vimrc路径下。
我们可以通过vim --version命令来查看Vim配置文件的路径。

1.1.4 Vim的按键映射

通过修改vimrc文件可以实现按键映射。
除了通过vimrc配置文件来定制功能,Vim还支持通过插件来扩展功能。在Vim的官方网站上有很多xx.vim格式的插件供用户下载使用。如果你想通过插件来扩展Vim的功能,方法很简单:先在你的当前用户下创建一个~/.vim/plugin目录,然后将这些xx.vim格式的插件复制到这个目录,在$HOME/.vimrc配置文件里对这些插件进行配置,就可以直接使用了。

第2章 计算机体系结构与CPU工作原理

2.2 一颗CPU是怎么设计出来的

2.2.1 计算机理论基石:图灵机

图灵机的构造:一条无限长的纸带Tape、一个读写头Head、一套控制规则Table、一个状态寄存器。图灵机内部有一个机器读写头Head,读写头可以一直读取纸带,图灵机根据自己有限的控制规则,根据纸带的输入,不断更新机器的状态,并将输出打印到纸带上。
比较图灵机原型与现代计算机,你会发现有很多相似的地方:
● 无限长的纸带:相当于程序代码。
● 一个读写头Head:相当于程序计数器PC。
● 一套控制规则Table:相当于CPU有限的指令集。
● 一个状态寄存器:相当于程序或计算机的状态输出。

不同架构的CPU,指令集不同,支持运行的机器指令也不同,但是有一条是相同的:每一种CPU只能支持有限个指令,任何复杂的运算最终都可以分解成有限个基本指令来完成:加、减、乘、除、与、或、非、移位等算术运算或逻辑运算。

2.2.2 CPU内部结构及工作原理

CPU内部构造很简单,只包含算术逻辑运算单元(ALU)、控制单元、寄存器等,仅支持有限个指令。CPU支持的有限个基本指令集合,称为指令集。程序代码存储在内部存储器(内存)中,CPU可以从内存中一条一条地取指令、翻译指令并执行它。

ALU由算术单元和逻辑单元组成,算术单元负责数学运算,如加、减、乘等;逻辑单元负责逻辑运算,如与、或、非等。
ALU只是纯粹的运算单元,要想完成一个指令运行的整个流程,还需要控制单元的协助。
控制单元根据程序计数器PC中的地址,会不断地从内存RAM中取指令,放到指令寄存器中并进行译码,将指令中的操作码和操作数分别送到ALU,执行相应的运算。

CPU的工作频率要比RAM高很多,防止RAM拖后腿,CPU一般都会在内部配置一些寄存器,用来保存CPU在计算过程中的各种临时结果和状态值。ALU在运算过程中,当运算结果为0、为负、数据溢出时,也会有一些Flags标志位输出,这些标志位对控制单元特别有用,如一些条件跳转指令,其实就是根据运算结果的这些标志位进行跳转的。CPU跳转指令的实现其实也很简单:根据ALU的运算结果和输出的Flags标志位,直接修改PC寄存器中的值即可

2.2.3 CPU设计流程

  1. 设计芯片规格
    根据需求,设计出芯片基本的框架、功能,进行模块划分。有些复杂的芯片可能还需要建模,使用MATLAB、CADENCE等工具进行前期模拟和仿真。
  2. HDL代码实现
    使用VHDL或Verilog硬件描述语言把要实现的硬件功能描述出来,接着通过EDA工具不断仿真、修改和验证,直到芯片的逻辑功能完全正确。这种仿真我们一般称为前端仿真,简称前仿。前仿只验证芯片的逻辑功能是否正确,不考虑延时等因素。这个阶段也是芯片设计最重要的阶段,会耗费大量的时间去反复验证芯片逻辑功能的正确性。芯片公司内部一般也会设有数字IC验证工程师岗位,招聘工程师专门从事这个工作。
  3. 逻辑综合
    前端仿真通过后,通过EDA工具就可以将HDL代码转换成具体的逻辑门电路。专业说法是将HDL代码翻译成门级网表:Gate-level netlist,网表文件用来描述电路中元器件之间的连接关系。有数字电路基础的人都知道,任何一个逻辑运算都可以转化为基本的门级电路(与门、或门、非门等)的组合来实现,而网表就是用来描述这些门级电路的连接信息的。在综合过程中,有时候还需要设定一些约束条件,让综合出来的具体电路在芯片面积、时序等参数上满足预期要求。此时的电路考虑了延时等因素,和实际的芯片电路已经很接近了。
    Foundry在集成电路领域一般指专门负责生产、制造芯片的厂家,如台积电、中芯国际等。
    Fabless是Fabrication(制造)和less的组合词,专指那些只专注于集成电路设计,而没有芯片制造工厂的IC设计公司。
    对于一些Fabless的IC设计公司而言,门级电路一般是由晶圆厂,也就是芯片代工厂以工艺库的形式提供的,如中芯国际、台积电、三星半导体等。如果你设计的芯片委托台积电代工制造,工艺制程是14nm,那么当你在设计芯片时,台积电会提供给你14nm级的工艺库,里面包含各种门电路,经过逻辑综合生成的电路参数,如延时参数,和台积电生产芯片实际使用电路的工艺参数是一致的。
  4. 仿真验证
    通过逻辑综合生成的门级电路,已经包含了延时等各种信息,接下来还需要对这些门级电路进行进一步的静态时序分析和验证。为了提高工作效率,除了使用仿真软件,有时候也会借助FPGA平台进行验证。前端仿真发生在逻辑综合之前,专注于验证电路的逻辑功能是否正确;逻辑综合后的仿真,一般称为后端仿真,简称后仿。后端仿真会考虑延时等因素。后端仿真通过后,从HDL代码到生成门级网表电路,整个芯片的前端设计就结束了。
  5. 后端设计
    通过前端设计,我们已经生成了门级网表电路,但门级网表电路和实际的芯片电路之间还有一段距离,我们还需要对其不断完善和优化,将其进一步设计成物理版图,也就是芯片代工厂做掩膜版需要的电路版图,这一阶段称为后端设计。后端设计包括很多步骤,具体如下:
    ● DFT:Design For Test,可测试性设计。芯片内部一般会自带测试电路,如插入扫描链、引出JTAG调试接口。
    ● 布局规划:各个IP电路模块的摆放位置、时钟线综合、信号线的布局等。
    ● 物理版图验证:检查设计规则、连线宽度、间距是否符合工艺要求和电气规则。
    物理版图验证通过后,芯片设计公司就可以将这个物理版图以GDSII文件的格式交给芯片制造代工厂(Foundry)去流片了。到了这一步,整个芯片设计、仿真、验证的流程就结束了,我们称为tap-out。

芯片代工厂根据物理版图提供的这些信息来制造掩膜版,然后使用光刻机,通过掩膜版在晶圆的硅片衬底上开凿出各种掺杂窗口,接着对硅片进行离子注入,掺杂不同的三价元素和五价元素,生成PN结,进而构成二极管、三极管、CMOS管等基本元器件,构建出各种门电路。光刻机根据物理版图的不同层,制作不同的掩膜版,从底层开始,逐层制作,就可以在晶圆硅片衬底上生成多层立体的3D电路结构。

晶圆上的一个个CPU芯片电路在经过切割、封装、引出管脚、测试后,就是我们在市场上常见的各种CPU芯片了。

2.3 计算机体系结构

CPU内部的结构其实很简单,除了ALU、控制单元、寄存器和少量Cache,根本没有多余的空间存放我们编写的代码,我们需要额外的存储器来存放我们编写的程序(指令序列)。

随机访问,CPU可以随机到它的任意地址去读写数据

计算机主要用来处理数据。我们编写的程序,除了指令,还有各种各样的数据。指令和数据都需要保存在存储器中,根据保存方式的不同,计算机可分为两种不同的架构:冯·诺依曼架构和哈弗架构。

2.3.1 冯·诺依曼架构

采用冯·诺依曼架构的计算机,其特点是程序中的指令和数据混合存储,存储在同一块存储器上。
冯·诺依曼架构的特点是结构简单,工程上容易实现,所以很多现代处理器都采用这种架构,如X86、ARM7、MIPS等。

2.3.2 哈弗架构

哈弗架构的特点是:指令和数据被分开独立存储,它们分别被存放到程序存储器和数据存储器。每个存储器都独立编址,独立访问,而且指令和数据可以在一个时钟周期内并行访问。使用哈弗架构的处理器运行效率更高,但缺点是CPU实现会更加复杂。8051系列的单片机采用的就是哈弗架构。

2.3.3 混合架构

现在的CPU工作频率越来越高,很容易和内存RAM之间产生带宽问题:CPU的频率可以达到GHz级别,而对应的内存RAM一般工作在几百兆赫兹(目前的DDR4 SDRAM也能工作在GHz级别了)。CPU和RAM之间传输数据,要经过找地址、取数据、配置、等待、输出数据等多个时钟周期,内存带宽瓶颈会拖慢CPU的工作节奏,进而影响计算机系统的整体运行效率。为了减少内存瓶颈带来的影响,CPU引入了Cache机制:指令Cache和数据Cache,用来缓存数据和指令,提升计算机的运行效率。

SoC芯片内部的Cache层采用哈弗架构,集成了指令Cache和数据Cache。当CPU到RAM中读数据时,内存RAM不是一次只传输要读取的指定字节,而是一次缓存一批数据到Cache中,等下次CPU再去取指令和数据时,可以先到这两个Cache中看看要读取的数据是不是已经缓存到这里了,如果没有缓存命中,再到内存中读取。当CPU写数据到内存RAM时,也可以先把数据暂时写到Cache里,然后等待时机将Cache中的数据刷新到内存中。Cache缓存机制大大提高了CPU的访问效率,而SoC芯片外部则采用冯·诺依曼架构,工程实现简单。现代的计算机集合了这两种架构的优点,因此我们很难界定一款芯片到底是冯·诺依曼架构还是哈弗架构,我们就姑且称之为混合架构吧。

2.4 CPU性能提升:Cache机制

2.4.1 Cache的工作原理

Cache在物理实现上其实就是静态随机访问存储器(Static Random Access Memory,SRAM)
Cache的工作原理很简单,就是利用空间局部性和时间局部性原理,通过自有的存储空间,缓存一部分内存中的指令和数据,减少CPU访问内存的次数,从而提高系统的整体性能。

Cache的工作流程以图2-28为例:当CPU读取内存中地址为8的数据时,CPU会将内存中地址为8的一片数据缓存到Cache中。等下一次CPU读取内存地址为12的数据时,会首先到Cache中检查该地址是否在Cache中。如果在,就称为缓存命中(Cache Hit),CPU就直接从Cache中取数据;如果该地址不在Cache中,就称为缓存未命中(Cache Miss),CPU就重新转向内存读取数据,并重新缓存从该地址开始的一片数据到Cache中。

CPU写内存的工作流程和读类似:以图2-29为例,当CPU往地址为16的内存写入数据0时,并没有真正地写入RAM,而是暂时写到了Cache里。此时Cache和内存RAM的数据就不一致了,缓存的每块空间里一般会有一个特殊的标记位,叫“Dirty Bit”,用来记录这种变化。当Cache需要刷新时,如Cache空间已满而CPU又需要缓存新的数据时,在清理缓存之前,会检查这些“Dirty Bit”标记的变化,并把这些变化的数据回写到RAM中,然后才腾出空间去缓存新的内存数据。

以上只是对Cache的工作原理做了简化分析,实际的Cache远比这复杂,如Cache里存储的内存地址,一般要经过地址映射,转换为更易存储和检索的形式。除此之外,现代的CPU为了进一步提高性能,大多采用多级Cache:一级Cache、二级Cache,甚至还有三级Cache。

2.4.2 一级Cache和二级Cache

在CPU内部,Cache和寄存器的电路比内存DRAM复杂了很多,会占用很大的芯片面积,如果大量使用,芯片发热量会急剧上升,所以在CPU内部寄存器一般也就几十个,靠近CPU的一级Cache也就几十千字节。既然无法继续增加一级Cache的容量,一个折中的办法就是在一级Cache和内存之间添加二级Cache,二级Cache的工作频率比一级Cache低,但是电路成本会降低,元器件的运行速度总是和电路成本成正比

现在的CPU一般都是多核结构,一个CPU芯片内部会集成多个Core,每个Core都会有自己独立的L1 Cache,包括D-Cache和I-Cache。在X86架构的CPU中,一般每个Core也会有自己独立的L2 Cache,L3Cache被所有的Core共享。而在ARM架构的CPU中,L2 Cache则被每簇(Cluster)的Core共享。

2.5 CPU性能提升:流水线

2.5.1 流水线工作原理

一条指令的执行一般要经过取指令、翻译指令、执行指令3个基本流程。CPU内部的电路分为不同的单元:取指单元、译码单元、执行单元等,指令的执行也是按照流水线工序一步一步执行的。如图2-34所示,我们假设每一个步骤的执行时间都是一个时钟周期,那么一条指令执行完需要3个时钟周期。

没引入流水线时,CPU执行指令的3个时钟周期里,取指单元只在第一个时钟周期里工作,其余两个时钟周期都处于空闲状态,其他两个执行单元也是如此。

引入流水线后,除了刚开始的第一个时钟周期大家可以偷懒,其余的时间都不能闲着:从第二个时钟周期开始,当译码单元在翻译指令1时,取指单元也不能闲着,要接着去取指令2。从第三个时钟周期开始,当执行单元执行指令1时,译码单元也不能闲着,要接着去翻译指令2,而取指单元要去取指令3。从第四个时钟周期开始,每个电路单元都会进入满负荷工作状态,像富士康工厂里的流水线一样,源源不断地执行一条条指令。

2.5.2 超流水线技术

每一道工序都称为流水线中的一级,流水线越深,每一道工序的执行时间就会变得越小,处理器的时钟周期就可以更短,CPU的工作频率就可以更高,进而可以提升CPU的性能,提高工作效率。我们把5级以上的流水线称为超流水线结构。

流水线中耗时最长的那道工序单元的执行时间(即时间延迟)决定了CPU流水线的性能。CPU流水线中的每一级电路单元一般都是由组合逻辑电路和寄存器组成的,组合逻辑电路用来执行本道工序的逻辑运算,寄存器用来保存运算输出结果,并作为下一道工序的输入。

流水线通过减少每一道工序的耗费时间来提升整条流水线的效率。在CPU内部也是如此,CPU内部的数字电路是靠时钟驱动来工作的,既然每条指令的执行时钟周期数不变,即执行每条指令都需要3个时钟周期,但是我们可以通过缩短一个时钟周期的时间来提升效率,即减少每条指令所耗费的时间。一个时钟周期的时间变短,CPU主频也就相应提升,影响时钟周期时间长短的一个关键的制约因素就是CPU内部每一个工序执行单元的耗费时间。


流水线的本质是拿空间换时间,流水线越深,电路会越复杂,就需要更多的组合逻辑电路和寄存器,芯片面积也就越大,功耗也就随之上升了。

流水线越深,就越能提升性能吗?也不一定。流水线是靠指令的并行来提升性能的,第一条指令还没有执行完,下面的第二条指令就开始取指、译码了。执行的程序指令如果是顺序结构的,没有中断或跳转,流水线确实可以提高执行效率。但是当程序指令中存在跳转、分支结构时,下面预取的指令可能就要全部丢掉了,需要到跳转的地方重新取指令执行。

流水线越深,一旦预取指令失败,浪费和损失就会越严重,因为流水线中预取的几十条指令可能都要丢弃掉,此时流水线就发生了停顿,无法按照预期继续执行,这种情况我们一般称为流水线冒险(hazard)。

2.5.3 流水线冒险

引起流水线冒险的原因有很多种,根据类型不同,我们一般分为3种。
● 结构冒险:所需的硬件正在为前面的指令工作。
● 数据冒险:当前指令需要前面指令的运算数据才能执行。
● 控制冒险:需根据之前指令的执行结果决定下一步的行为。

结构冒险可以通过将冲突的内存单元或是寄存器进行替换进行解决,比如两条紧跟着的指令都要用R1寄存器,且没有依赖关系,则可以通过编译器或者硬件自动完成这种类似替换动作;

数据冒险和控制冒险,则会通过插入空指令,等待前一条指令运行完毕之后再去运行或者读取后面的指令

2.5.4 分支预测

条件跳转引起的控制冒险虽然也可以通过在流水中插入空泡来避免,但是当流水线很深时,需要插入更多的空泡。

根据工作方式的不同,分支预测可分为静态预测和动态预测。静态预测在程序编译时通过编译器进行分支预测,这种预测方式对于循环程序最有效,它可以根据你的循环边界反复取指令。而对于跳转分支,静态预测就比较简单粗暴了,一般都是默认不跳转,按照顺序执行。我们在编写有跳转分支的程序时,要记得把大概率执行的代码分支放在前面,这样可以明显提高代码的执行效率。

动态预测则指在程序运行时进行预测。在CPU内部,除了Cache,就数分支预测器的电路版图最大。

2.5.5 乱序执行

我们编写的代码指令序列按照顺序依次存储在RAM中。当程序执行时,PC指针会自动到RAM中去取,然后CPU按照顺序一条一条地依次执行,这种执行方式称为顺序执行(in order)。当这些指令前后有数据依赖关系时,就会产生数据冒险,我们可以通过在指令序列之间添加空指令,让流水线暂时停顿来避免流水线中预期的指令被冲刷掉。除此之外,我们还可以通过乱序执行(out of order)来避免流水线冲突。

造成流水线冲突的根源在于指令之间存在相关性:前后指令之间要么产生数据冒险,要么产生结构冒险。我们可以通过重排指令的执行顺序,而不是被动地填充空指令来去掉这种依赖。

支持乱序执行的CPU处理器,其内部一般都会有专门的乱序执行逻辑电路,该控制电路会对当前指令的执行序列进行分析,看能否提前执行。如整型计算、浮点型计算会使用不同的计算单元,同时执行这些指令并不会发生冲突。CPU分析这些不相关的指令,并结合各电路单元的空闲状态综合判断,将能提前执行的指令进行重排,发送到相应的电路单元执行。

2.5.6 SIMD和NEON

CPU的控制单元到内存中取数据,将操作数送到算术逻辑单元中,取数据的方法有两种:第一种是先取第一个操作数,然后访问内存读取第二个操作数,最后才能进行求和计算。这种数据操作类型一般称为单指令单数据(Single Instruction Single Data,SISD);第二种方法是几个执行部件同时访问内存,一次性读取所有的操作数,这种数据操作类型称为单指令多数据(Single Instruction Multiple Data,SIMD)。毫无疑问,SIMD通过单指令多数据运算,帮助CPU实现了数据并行访问,SIMD型的CPU执行效率更高。

NEON是适用于Cortex-A和Cortex-R52系列处理器的一种128位的SIMD扩展指令集

2.5.7 单发射和多发射

每个时钟周期只能从存储器取一条指令,每个时钟周期也只能执行一条指令,这种处理器一般叫作单发射处理器。

多发射处理器在一个时钟周期内可以执行多条指令。处理器内部一般有多个执行单元,如算术逻辑单元(ALU)、乘法器、浮点运算单元(FPU)等。

根据实现方式的不同,多发射处理器又可分为静态发射和动态发射。静态发射指在编译阶段将可以并行执行的指令打包,合并到一个64位的长指令中。在打包过程中,若找不到可以并行的指令配对,则用空指令NOP补充。这种实现方式称为超长指令集架构(Very Long Instruction Word,VLIW)。

采用SuperScalar结构的处理器又叫超标量处理器,这种处理器在多发射的实现过程中会增加额外的取指单元、译码单元、逻辑控制单元等硬件电路。在指令运行时,将串行的指令序列转换为并行的指令序列,分发到不同的执行单元去执行,通过指令的动态并行化来提升CPU的性能。

VLIW和SuperScalar分别从编译器和硬件上实现了指令的并行化,各有各的优势和局限性:VLIW虽然实现简单,但由于兼容性问题,不支持目前主流的X86、ARM处理器;而采用SuperScalar结构的处理器,完全依赖流水线硬件去动态识别可并行执行的指令,并分发到对应的执行单元执行,不仅大大增加了硬件电路的复杂性,而且也存在极限。学者和工业界一致认为,同时执行8条指令将是SuperScalar结构的极限。

现在新架构的处理器没有指令集兼容的历史包袱,一般会采用显式并行指令计算(Explicitly Parallel Instruction Computing,EPIC)的指令集结构。EPIC结合了VLIW和SuperScalar的优点,允许处理器根据编译器的调度并行执行指令而不增加硬件的复杂性。EPIC的实现原理也很简单,就是在指令中使用3个比特位来表示相邻的两条指令有没有相关性、当前指令要不要等上一条指令运行结束后才能执行。程序在运行时,流水线根据指令中的这些信息可以很轻松地实现指令的并行化和分发工作。

2.6 多核CPU

多核CPU需要考虑CPU之间的同步以及资源竞争问题

2.6.4 超线程技术

超线程技术通过增加一定的控制逻辑电路,使用特殊指令可以将一个物理处理器当两个逻辑处理器使用,每个逻辑处理器都可以分配一个线程运行,从而最大限度地提升CPU的资源利用率。

在CPU内部很多资源其实也是可以共享的,如ALU、FPU、Cache、总线等,也有很多资源是每个线程独有的,如寄存器状态、堆栈等。我们通过增加一些控制逻辑电路,保存各个线程的状态,共享ALU、Cache等共享资源,就可以在一个物理Core上实现两个逻辑Core,操作系统可以给每个逻辑Core都分配1个线程运行。


在同一个物理Core上的两个线程并不是同时运行的,因为每个线程都需要使用物理Core上的共享资源(如ALU、Cache等)

超线程技术其实就是“欺骗”操作系统,让操作系统认为它有更多的Core,给它分配更多的任务执行,通过减少CPU的空闲时间来提高CPU的利用率。

2.7 后摩尔时代:异构计算的崛起

什么TPU,APU,XPU等等

2.8 总线与地址

CPU与内存、各种外部设备等IP之间都是通过总线相连的。
CPU通过地址访问内存,以及控制外部设备的运行。
在一个计算机系统中,CPU内部的寄存器是没有地址的,可直接通过寄存器名访问。而内存和外部设备控制器中的寄存器都需要有一个地址,然后CPU才能通过地址去读写这些外部设备控制器的寄存器,控制外部设备的运行,或者根据地址去读写指定的内存单元。

2.8.1 地址的本质

计算机的内存其实就是将一系列存储单元和译码器组装在一起。
地址的本质其实就是由CPU管脚发出的一组地址控制信号。因为这些信号是由CPU管脚直接发出的,因此也被称为物理地址。地址信号线的位数决定了寻址空间的大小,如两根A1A0地址信号线,有4字节的寻址空间;CSA1A0三根地址信号线有8字节的寻址空间(38译码器)。在一个32位的计算机系统中,32位的地址线有4GB大小的寻址空间。

2.8.2 总线的概念

总线其实就是各种数字信号的集合,包括地址信号、数据信号、控制信号等。有的总线还可以为挂到总线上的设备提供电源。一个计算机系统中可能会有各种不同的总线,不同的总线读写时序、工作频率不一
样,不同的总线之间通过桥(bridge)来连接。
例如,APB总线,AHB总线等等

2.8.3 总线编址方式

计算机一般采用两种编址方式:统一编址和独立编址。

统一编址,顾名思义,就是内存RAM和外部设备共享CPU的寻址空间,ARM、MIPS架构的CPU都采用这种编址方式。
在统一编址模式下,内存RAM、外部设备控制器的寄存器、集成在外部设备控制器内部的RAM共享CPU的可寻址空间。在统一编址模式下,CPU可以像操作内存一样去读写外部设备的寄存器和内部RAM。

在独立编址模式下,内存RAM和外部设备的寄存器独立编址,分别占用不同的地址空间。如X86架构的CPU,外部设备的寄存器有独立的64KB空间,需要专门的IN/OUT指令才能访问,这片独立编址的64KB大小的空间也被称为I/O地址空间。

2.9 指令集与微架构

CPU支持的有限个指令的集合,我们称之为指令集。

2.9.1 什么是指令集

指令集作为CPU和编译器的设计规范和参考标准,主要用来定义指令的格式、操作数的类型、寄存器的分配、地址的格式等,指令集主要由以下内容组成。
● 指令的分发、预取、解码、执行、写回。
● 操作数的类型、存储、存取、旁路转移。
● Load/Store架构。
● 寄存器。
● 地址的格式、大端模式、小端模式。
● 字节对齐、边界对齐等。
指令集最终的实现就是微架构,就是CPU内部的各种译码和执行电路。

2.9.2 什么是微架构

指令集在CPU处理器内部的具体硬件电路的实现,我们就称为微架构。
一套相同的指令集,可以由不同形式的电路实现,可以有不同的微架构。

2.9.3 指令助记符

为了方便编程,我们给这些二进制指令定义了各种助记符,这种助记符其实就是汇编指令。一段汇编程序经过汇编器的翻译,才能变成CPU真正能识别、译码和运行的二进制指令。

第3章 ARM体系结构与汇编语言

3.1 ARM体系结构

计算机的指令集一般可分为4种:复杂指令集(CISC)、精简指令集(RISC)、显式并行指令集(EPIC)和超长指令字指令集(VLIW)。我们在嵌入式学习和工作中需要经常打交道的是RISC指令集。RISC指令集相对于CISC指令集,主要有以下特点。
● Load/Store架构,CPU不能直接处理内存中的数据,要先将内存中的数据Load(加载)到寄存器中才能操作,然后将处理结果Store(存储)到内存中。
● 固定的指令长度、单周期指令。
● 倾向于使用更多的寄存器来存储数据,而不是使用内存中的堆栈,效率更高。

ARM指令集虽然属于RISC,但是和原汁原味的RISC相比,还是有一些差异的,具体如下。
● ARM有桶型移位寄存器,单周期内可以完成数据的各种移位操作。
● 并不是所有的ARM指令都是单周期的。
● ARM有16位的Thumb指令集,是32位ARM指令集的压缩形式,提高了代码密度。
● 条件执行:通过指令组合,减少了分支指令数目,提高了代码密度。
● 增加了DSP、SIMD/NEON等指令。

ARM处理器中的寄存器可分为通用寄存器和专用寄存器两种。
寄存器R0~R12属于通用寄存器,除了FIQ工作模式,在其他工作模式下这些寄存器都是共用、共享的:R0~R3通常用来传递函数参数,
R4~R11用来保存程序运算的中间结果或函数的局部变量等,
R12常用来作为函数调用过程中的临时寄存器。
ARM处理器有多种工作模式,除了这些在各个模式下通用的寄存器,还有一些寄存器在各自的工作模式下是独立存在的,如R13、R14、R15、CPSP、SPSR寄存器,在每个工作模式下都有自己单独的寄存器。R13寄存器又称为堆栈指针寄存器(Stack Pointer,SP),用来维护和管理函数调用过程中的栈帧变化,R13总是指向当前正在运行的函数的栈帧,一般不能再用作其他用途。
R14寄存器又称为链接寄存器(Link Register,LR),在函数调用过程中主要用来保存上一级函数调用者的返回地址。
寄存器R15又称为程序计数器(Program Counter,PC),CPU从内存取指令执行,就是默认从PC保存的地址中取的,每取一次指令,PC寄存器的地址值自动增加。在ARM三级流水线中,PC指针的值等于当前正在运行的指令地址+8,后续的32位处理器虽然流水线的级数不断增加,但为了简化编程,PC指针的值继续延续了这种计算方式。

在每种工作模式下,都有一个单独的程序状态保存寄存器(Saved Processor State Register,SPSR)。当ARM处理器切换工作模式或发生异常时,SPSR用来保存当前工作模式下的处理器现场,即将CPSR寄存
器的值保存到当前工作模式下的SPSR寄存器。当ARM处理器从异常返回时,就可以从SPSR寄存器中恢复原先的处理器状态,切换到原来的工作模式继续运行。
在ARM所有的工作模式中,有一种工作模式比较特殊,即FIQ模式。为了快速响应中断,减少中断现场保护带来的时间开销,在FIQ工作模式下,ARM处理器有自己独享的R8~R12寄存器。

3.2 ARM汇编指令

一个完整的ARM指令通常由操作码+操作数组成,指令的编码格式如下。

这是一个完整的ARM指令需要遵循的格式规则,指令格式的具体说明如下。
● 使用<>标起来的是必选项,使用{}标起来的是可选项。
● 是二进制机器指令的操作码助记符,如MOV、ADD这些汇编指令都是操作码的指令助记符。
● cond:执行条件,ARM为减少分支跳转指令个数,允许类似BEQ、BNE等形式的组合指令。
● S:是否影响CPSR寄存器中的标志位,如SUBS指令会影响CPSR寄存器中的N、Z、C、V标志位,而SUB指令不会。
● Rd:目标寄存器。
● Rn:第一个操作数的寄存器。
● operand2:第二个可选操作数,灵活使用第二个操作数可以提高代码效率。

3.2.1 存储访问指令

ARM指令集属于RISC指令集,RISC处理器采用典型的加载/存储体系结构,CPU无法对内存里的数据直接操作,只能通过Load/Store指令来实现:
当我们需要对内存中的数据进行操作时,要首先将这个数据从内存加载到寄存器,然后在寄存器中对数据进行处理,最后将结果重新存储到内存中。
ARM处理器一般程序和数据都存储在同一存储器上,内存空间和I/O空间统一编址,ARM处理器对程序指令、数据、I/O空间中外设寄存器的访问都要通过Load/Store指令来完成。ARM处理器中经常使用的Load/Store指令的使用方法如下。

LDM/STM指令常用来加载或存储一组寄存器到一片连续的内存,通过和堆栈格式符组合使用,LDM/STM指令还可以用来模拟堆栈操作。LDM/STM指令常和表3-2的堆栈格式组合使用。

在一个堆栈内存结构中,如果堆栈指针SP总是指向栈顶元素,那么这个栈就是满栈;如果堆栈指针SP指向的是栈顶元素的下一个空闲的存储单元,那么这个栈就是空栈。

ARM处理器使用的一般都是满递减堆栈,在将一组寄存器入栈,或者从栈中弹出一组寄存器时,我们可以使用下面的指令。

ARM还专门提供了PUSH和POP指令来执行栈元素的入栈和出栈操作。PUSH和POP指令的使用方法如下。

3.2.2 数据传送指令

LDR/STR指令用来在寄存器和内存之间输送数据。如果我们想要在寄存器之间传送数据,则可以使用MOV指令。MOV指令的格式如下。

其中,{cond}为条件指令可选项,{S}用来表示是否影响CPSR寄存器的值,如MOVS指令就会影响寄存器CPSR的值,而MOV则不会。
MVN指令用来将操作数operand2按位取反后传送到目标寄存器Rd。操作数operand2可以是一个立即数,也可以是一个寄存器。
MOV和MVN指令的一般使用方法如下。

3.2.3 算数逻辑运算指令

算术运算指令包括基本的加、减、乘、除,逻辑运算指令包括与、或、非、异或、清除等。指令格式如下。

3.2.4 操作数:operand2详解

很多ARM指令会使用第二个参数operand2:可以是一个常数,也可以是寄存器+偏移的形式。操作数operand2在汇编程序中经常出现的两种格式如下。

第一种格式比较简单,操作数是一个立即数,第二种格式可以直接使用寄存器的值作为操作数。
在第二种格式中,通过{,shift}可选项,我们还可以通过多种移位或循环移位的方式,构建更加灵活的操作数。可选项{,shift}可以选择的移位方式如下。

3.2.5 比较指令

比较指令的运算结果会影响CPSR寄存器的N、Z、C、V标志位,比较指令的格式如下。


比较指令的运行结果Z=1时,表示运算结果为零,两个数相等;N=1表示运算结果为负,N=0表示运算结果为非负,即运算结果为正或者为零。

3.2.6 条件执行指令

几乎所有的ARM指令都可以根据CPSR寄存器中的标志位,通过指令组合实现条件执行。如无条件跳转指令B,我们可以在后面加上条件码组成BEQ、BNE组合指令。BEQ指令表示两个数比较,结果相等时跳转;BNE指令则表示结果不相等时跳转。CPSR寄存器中的标志位根据需要可以任意搭配成不同的条件码,和ARM指令一起组合使用。ARM指令的条件码如表3-3所示。

我们可以将无条件跳转指令B和条件码NE组合在一起使用,构成一个循环程序结构。

3.2.7 跳转指令

ARM指令集提供了B、BL、BX、BLX等跳转指令,跳转指令的格式如下。

B跳转指令的跳转范围大小为[0,32MB],可以往前跳,也可以往后跳。

BL跳转指令表示带链接的跳转。在跳转之前,BL指令会先将当前指令的下一条指令地址(即返回地址)保存到LR寄存器中,然后跳转到label处执行。

BX表示带状态切换的跳转。Rm寄存器中保存的是跳转地址,要跳转的目标地址处可能是ARM指令,也可能是Thumb指令。处理器根据Rm[0]位决定是切换到ARM状态还是切换到Thumb状态。
● 0:表示目标地址处是ARM指令,在跳转之前要先切换至ARM状态。
● 1:表示目标地址处是Thumb指令,在跳转之前要先切换至Thumb状态。

BLX指令是BL指令和BX指令的综合,表示带链接和状态切换的跳转,使用方法和上面相同。

3.3 ARM寻址方式

ARM指令的操作数,可以是寄存器,

或者是立即数(#xx),

也可以将寄存器或者某个数当做地址([xx])

,也可以在某个地址基础上进行移位或者加减操作,

也可以进行多寄存器操作

3.4 ARM伪指令

常见的ARM伪指令主要有4个:ADR、ADRL、LDR、NOP,它们的使用示例如下。

为了与ARM指令集中的加载指令LDR区别开来,LDR伪指令中的操作数前一般会有一个等于号=,用来表示该指令是个伪指令。LDR伪指令的主要用途是将一个32位的内存地址保存到寄存器中。

ADR伪指令的功能与LDR伪指令类似,将基于PC相对偏移的地址值读取到寄存器中。ADR为小范围的地址读取伪指令,底层使用相对寻址来实现,因此可以做到代码与位置无关。

ADR伪指令和LDR伪指令的相似之处在于:两者都是为了加载一个地址到指定的寄存器中。两者的不同之处在于:LDR伪指令通常被翻译为ARM指令集中的LDR或MOV指令,而ADR伪指令则通常会被ADD或SUB指令代替。

3.5 ARM汇编程序设计

3.5.1 ARM汇编程序格式

ARM汇编程序是以段(section)为单位进行组织的。我们可以使用AREA伪操作来标识一个段的起始、段名和段的读写属性。

ARM汇编程序通过ENTRY这个伪操作来标识汇编程序的运行入口,使用伪操作END来标识汇编程序的结束。
在汇编程序中使用分号;来注释代码。

3.5.2 符号与标号

汇编语言中,符号由字母、数字和下划线组成,符号的开头不能使用数字。当用符号来标识一个地址时,这个符号通常又被称为标号。标号的开头可以是数字,整个标号可以是一个纯数字。

直接通过数字[0,99]而不是使用字符来进行地址引用,我们称这种数字为局部标号。局部标号的作
用域为当前段,在汇编程序中,我们可以使用下面的格式来引用局部标号。

● %:引用符号,对一个局部标号产生引用。
● F:指示编译器只向前搜索。
● B:指示编译器只向后搜索。
● A:指示编译器搜索宏的所有宏命令层。
● T:指示编译器搜索宏的当前层。
● N:局部标号的名字。
● routename:局部标号作用范围名称,使用ROUT定义。

3.5.3 伪操作

在一个汇编程序中经常使用的伪操作如下


3.6 C语言和汇编语言混合编程

3.6.1 ATPCS规则

ATPCS的全称是ARM-Thumb Procedure Call Standard,其核心内容就是定义了ARM子程序调用的基本规则及堆栈的使用约定等。规则的主要内容如下。
● 子程序间要通过寄存器R0~R3(可记作a0~a3)传递参数,当参数个数大于4时,剩余的参数使用堆栈来传递。
● 子程序通过R0~R1返回结果。
● 子程序中使用R4~R11(可记作v1~v8)来保存局部变量。
● R12作为调用过程中的临时寄存器,一般用来保存函数的栈帧基址,记作FP。
● R13作为堆栈指针寄存器,一般记作SP。
● R14作为链接寄存器,用来保存函数调用者的返回地址,记作LR。
● R15作为程序计数器,总是指向当前正在运行的指令,记作PC。

3.6.2 在c程序中内嵌汇编代码

GNU ARM编译器提供了一个__asm__关键字

3.6.3 汇编程序中调用C程序

需要先IMPORT C代码中的函数,然后将其需要的参数放入R0-R3寄存器之中,再使用BL跳转指令即可
在函数调用过程中,当要传递的参数大于4个时,除了前4个参数使用寄存器R0~R3传递,剩余的参数要使用堆栈进行传递,这时候就需要编译器通过栈指针来进行管理和维护

3.7 GNU ARM汇编语言

第4章 程序的编译、链接、安装和运行

readelf -h xxx.elf

可以查看可执行文件xxx.elf的头信息

readelf -S xxx.elf

可以查看其section信息
readelf在win中,可以替换成arm-linux-gnueabi-readelf

● 预处理器:将源文件main.c经过预处理变为main.i。
● 编译器:将预处理后的main.i编译为汇编文件main.s。
● 汇编器:将汇编文件main.s编译为目标文件main.o。
● 链接器:将各个目标文件main.o、sub.o链接成可执行文件a.out

arm-linux-gnueabi-gcc -E main.c > main.i

可以输出预编译之后的文件内容到main.i中

4.13 常用的binutils工具集



objdump


将一个ELF文件转换为BIN文件

BIN文件转换为十六进制的HEX文件

第5章 内存堆栈管理

linux提供了用于检查内存泄露和内存越界的工具

第6章 GNU C编译器扩展语法精讲

“ANSI C”表示的就是C语言标准。

6.2 指定初始化




使用了typeof关键字来自动获取宏的两个参数类型。比较难理解的是(void)(&x==&y);这句话,看起来很多余,仔细分析一下,你会发现这条语句很有意思。它的作用有两个:一是用来给用户提示一个警告,对于不同类型的指针比较,编译器会发出一个警告,提示两种数据的类型不同。二是两个数进行比较运算,运算的结果却没有用到,有些编译器可能会给出一个warning,加一个(void)后,就可以消除这个警告。

6.4 typeof与container_of宏


在上面的代码中,因为变量i的类型为int,所以typeof(i)就等于int,typeof(i) j=20就相当于int j=20,typeof(int*) a;相当于int*a,f()函数的返回值类型是int,所以typeof(f()) k;就相当于int k;。


从语法角度来看,container_of宏的实现由一个语句表达式构成。语句表达式的值即最后一个表达式的值。
主要是两句,第一句是获取member的类型,并将ptr的值,赋给对应类型的__mptr指针;第二句就是用这个指针减去member在结构体type中的偏移,得到结构体首地址。

6.6 属性声明


目前__attribute__支持十几种属性声明。
● section.
● aligned.
● packed.
● format.
● weak.
● alias.
● noinline.
● always_inline.
● ……

发布评论

评论列表(0)

  1. 暂无评论