传统意义上面的CPU是指运算执行单元,不包含外部设备的接口,也就是一个CPU core,外部接口非常简单,仅仅是一个高速总线和一些异常(中断)接口,内部集成了cache/MMU等硬件模块。
SOC是一个core并集成了若干外部设备接口。通常包含内存控制器,串口,硬盘控制器,localbus控制器等等。这些集 合在一起就是一个SOC。
广义上的CPU与SOC是完全等同的。
我们平时用的大部份PowerPC其实都是一个SOC,但也有不是的,比如7457,这个芯片就只是一个CPU core,外部通过桥片实现和其它控制器,与intel的CPU类似,intel比较老的CPU也仅仅是一个core,内存控制器等都是在南北桥上面实现的。
本文的主要工作就是集中在狭义的CPU,也就是core上面。
8位/16位/32位机是什么意思?
每一个core运行的基础就是指令集,指令的长度就是core的位数。
8位core最多有256条指令,16位最多的64K条指令,32位则是4G
但实际上,各种core能用的指令的总个数远少于上面的数字。
我们以powerPC的add指令为例说明
Bits | Value | 说明 |
0-5 | 31 | Bit0-5,22-30共同组成指令 |
6-10 | RT | 目的寄存器 |
11-15 | RA | 寄存器A的ID |
16-20 | RB | 寄存器B的ID |
21 | OE | Overflow exception,运算溢出时是否触发异常(中断)。 |
22-30 | 266 | Bit0-5,22-30共同组成指令 |
31 | Rc | 用于比较的控制位,在这个例子中无视 |
对于一个加法,A + B = C,有三个被 操作的数,在一个指令里面需要说明要操作哪些数。也就是对于这种有三个操作数的指令,操作数本身就占用了15bit,那么用于区分指令的bit远没有达到32bit.
实际上面,不带操作对向的指应是非常少的,只有少量的特殊指令是无操作数的。
因为硬件设计这么做最简单,事实上面是有的CPU支持可变长超长指令的,但PowerPC是不支持的
但这从core设计的角度上来讲,就需要考虑更多的情况,例如预取,cache等问题
而定长的指令是最简单的。
数据和指令的差别。
数据和指令在PowerPC上面是分为两种cache的,data cache和instruction cache.
为什么这么设计?如果两者混合不是利用率更高么?
比如我们一般把数据缓冲关掉,运行效率也就低几倍,但指令缓冲一关,速度慢的就是几十倍。所以分开设计更容易保证代码cache的命中率。
代码被CPU正常获取是每一个指令周期有效的必要条件,数据cache的重要性不如代码cache。
CPU的数据的长度是多变的,PowerPC的load(将外部数据装载入寄存器)和store(将寄存器装入外部存储器)的指令有8bit/16bit/32bit以及64bit(浮点指令)
因为指令是32位的,所以对于指令而言,是不可能用一条指令做到从某个地址把某个数据
int a, b, c func() { a=b+c; } |
a,b,c是三个全局变量,对于一个程序而言,这三个变量是存放在三个不同的地址上面的。
c语言a=b+c只有一句,但对于汇编而言是不可能一句话搞定的。为什么?
因为一条指令只有32bit,对于32位的操作系统而言,一个地址的编码就是32bit的,怎么可能通过一个指令完成所有的操作呢?
所以实际工作的代码是这样的
ADDR_a, ADDR_b, ADDR_c分别是a/b/c三个变量在内存中的地址,这是一个常量。因为链接后一定会给a/b/c三个变量分配一个固定的地址。
Reg0 = ADDR_a //现在reg0里面就放了ADDR_a这个地址 reg1 = load(reg0) //将reg0指向的地址的数据装到reg1中,reg1也就是a的值 Reg0 = ADDR_b //现在reg0里面就放了ADDR_b这个地址 Reg2 = load(reg0) //将reg0指向的地址的数据装到reg2中,reg2也就是b的值 Reg2 = reg1 + reg2 //计算reg1 + reg2的内容 reg 0 = ADDR_c store(reg2, reg0) //将reg2的内容存到reg0的地址当中。 |
仅仅是一个全局变量的加法,就涉及到这么多的指令,这上面是有几句话是用一条代码搞不定的。那就是regx = ADDR_x,因为ADDR_x是32bit的
实际的代码是这么写的
(LO)regx = LO(ADDR_a),低16位装到regx的低16bit中
(HI)regx = HI(ADDR_a),高16位装到regx的高16bit中
仅仅是c代码中最简单的一个加法,就需要好几条指令才能完成。
这就是代码优化存在的意思,但实际上面,由于代码是可以并行处理的。运行的速度会比这快。并不代表做这么一个加法就需要十几个指令周期,而且编译器本身也会做些优化。否则C语言写的代码的运行效率太低了。
刚才我们看到了ADDR_x这些地址,ADDR_x指向的地址是怎么分配的呢?实际运行的那些指令又是如何存放的呢?
代码段的概念和分配
首先我们需要了解CPU的发展历史
最初的CPU的ram是非常小的。但是小内存也是可以做很多事的。很多单片机只有512B的内存,但依然可以做很多事,比较外面挂个LCD做做显示,AD信号采集。
对于大多数程序,运行过程中少量的RAM就够用了。但是代码不行,比如我们写了1万句a=b+c
这个代码只需要12B的ram(每个变量是32位,4Bytes)和几十上百K的代码。
代码的大小远远大于数据。而且代码有一个特性,通常代码是不需要修改的。
所以老的CPU和单片机,代码都是放在ROM上面的,只有通过仿真器或ISP等烧写工具才能更新代码。而数据则不同,数据是要随时改变的,必须放到RAM当中。这怎么办?
所有最早的CPU开发者规定了几个段,必要的有代码段和数据段。通常这两者是不会混杂在一起的,分在不同的地址空间当中。像单片机上面,代码段和数据段就离的很远,前者在ROM空间当中,后者在RAM空间当中。
core从代码段获取代码,从数据段获取数据。不管怎么讲,对core而言都是地址空间中的不同的地址。所以代码段就在RAM当中也是可行的。如何部署完全是根据不同的CPU的特性和要求来的。比如我们的kboot,代码段就是放在flash当中的。而数据段是放到SRAM当中。而vxworks的代码段和数据段都放在内存当中。
PowrPC的RAM很大,放的下代码,所以vxworks代码段和地址段都在内存当中有着更好的运行效率。
至于其它的什么.bss段,还有.debug段,就无所谓了,都是因为某些原因创建的。实际上面对于CPU运行,有了代码段和数据段就可以工作了。