标签:导致 磁盘 这不 寄存器 出现 展开 复杂 怎么 了解
本文的主要说明对象是CPU和内存。为什么学C语言之前必懂呢,因为C语言是非常贴近底层原理的语言,明白了CPU和内存的原理,对学C语言有很大帮助。
其实我个人是比较主张计算机专业本科应该先学计算机组成原理然后再学C语言的,不过好像没有这么干的,而且学C语言之前并不需要学完整个计算机组成原理才能学C,对于想快速入门的来说,理解了这篇文章足够了。
先说说内存,其实就只是一个存数的工具,容量可以相当大,例如现在常见的4GB容量的内存,有40亿个内存单元,或说40亿个字节(字节用B表示),每个内存单元可以放一个8位二进制数。计算机里什么都是二进制,至于为什么是8位,人家就是这么规定的。
(事实上字节B本身就是一个容量单位,1KB=1000或1024B,1MB=1000或1024KB,1GB=1000或1024MB,具体是1000还是1024其实。。。我也是恨死了最初让这个进率为1000的家伙)
内存的基本操作只有读和写两种(对有的CPU还有运算等操作,但咱们只讨论读和写,因为有这两个足够了)。有这么多个内存单元,咱读或写的时候总得告诉人家咱要的是哪个内存单元吧,这时就要通过编号了,对于4GB内存,编号就是从0到40亿-1(注意是从0开始,在计算机或C语言里很多东西都是从0开始的),这个编号就是地址。读的话就是把某个地址里的数读进CPU,写的话就是把CPU里某个数写进某个内存地址处(这个地址之前的数就没有了)。
另外要说明的是,每次读或写,可以读写2个,4个,甚至可能8个内存单元。32位CPU最常见的是一次读4个字节(因为每个字节是8位,32位就是4个字节了),一般来说,要求地址必须是4的倍数,例如读4004开始的4字节,那么这时候会把4004、4005、4006、4007这4个单元的数全部读出来,组成一个32位二进制数。这里有一个问题,怎么组?是从高位到低位分别为4004到4007,还是4007到4004?其实取决于CPU了,两种都有。写也是类似的。
下面说说CPU。CPU有一个重要的概念是寄存器,例如32位MIPS有r0到r31的32个寄存器(实际上不只这些,但其它的寄存器都有他们自己的特殊性,后面讨论),每个寄存器可以放一个32位二进制数。寄存器也是用来存数的(类比内存),但是就这么点儿寄存器,没有内存根本不够。而且CPU要工作必须有内存(后面会说原因)。与内存不同的是,寄存器里的数据可以直接做运算,例如MIPS我可以让CPU做这么件事:把r12和r17里的数相加,加法结果存入r1。结果也可以存入r12或r17。通过这个例子应该能看明白寄存器是用来干什么的了。而内存里的数,不能直接做运算(其实不是所有的CPU都不能,但是那些咱们就不讨论了),只能把某个地址的数读到某个寄存器来,或者把某个寄存器里的数存到内存某个地址处。另外要指出的是,内存取数的操作是非常慢的(相比寄存器运算等操作,不过跟外存磁盘等相比内存真的很快很快),当然现在有一些办法能一定程度上解决这个问题,但常用的数据要尽可能放在寄存器中。
内存里的数读入寄存器的典型操作比如把r14里的数加上12(常数),这个加法结果作为地址,把这个内存地址的数存进r6。看起来好像这个加上12有点奇怪?如果你想把r14里的数直接做地址的话,加上0就可以了,不过实际上这个先加上一个常数再作为地址的操作很常见,所以才会有这个看起来有点奇怪的操作。这里有三个要告诉CPU的东西,地址放在哪个寄存器,地址加的常数,以及把数读进哪个寄存器。写内存的操作也是类似。对于32位MIPS,一般都是直接把4个字节读进来或写出去,但是也可以写1字节或2字节,与寄存器的位数不一样了,就会产生一个问题:多出的位怎么办?不过我也不打算在这里讨论这个问题了。
32位MIPS所有的寄存器都是32位的,读写内存时,能表示的地址只有2^32个,所以如果内存容量超过2^32字节(就是4GB,但是进率是1024),再大也没用,CPU访问不到它们。
前面一直说,我们要告诉CPU做什么操作,那么怎么“告诉”呢?通过指令。前面说的,把r12和r17里的数相加,加法结果存入r1,这就可以写成一条指令(对于32位MIPS)。把r14里的数加上12作为地址,把这个内存地址的数存进r6,这也是一条指令。对于32位MIPS,所有的指令都是用32位二进制数表示的。没错,又是32位二进制数。这32位里,有些表示了指令的类型(做加法还是减法,还是读写内存什么的),有些表示了寄存器编号等。反正只要知道指令能用二进制数表示就可以了。对于x86的CPU,每条指令长度并不固定,有的指令8位,有的很长。
好了,指令写完了,怎么发给CPU?其实指令都是按顺序放在内存里的。这就是CPU离开内存无法正常运行的原因。CPU需要把指令从内存中取到寄存器中才能执行,这个寄存器往往就跟前面说的r0到r31不一样了,是专门做特殊用途的寄存器。另外,还有一个寄存器PC(在MIPS中叫PC)用来指示取指令的地址,也不在r0到r31中。CPU一直在循环做这么几件事:先把PC里的值作为地址,上内存里把指令取出来,再把PC改成下一条指令的地址(对于32位MIPS一般是把PC的值加上4,因为每条指令32位4字节),接下来就去执行这条指令了。然后再重复这个过程。
这里要插一句,C语言写的程序,要先转化成这样一条一条的指令之后,才能运行,当然这个转化的过程不是我们自己做的。C语言里很多操作都能直接对应到这些指令的操作。因此了解这些指令对学习C语言很有帮助。
条件执行指令?先判断一个条件是否成立,如果成立就执行这一段,不成立就执行下一段?能不能做到呢?当然可以,改PC就可以了!当然,MIPS指令里不能直接做某种运算把一个数存进PC,不过MIPS有其它的指令,像无条件跳转,就可以改PC。在ARM里可以随便改PC像改其它寄存器那样。还有时候是条件跳转,先判断一个条件是否成立。一般条件有哪些呢?一般是判断一个数是否大于0,是否等于0,等等,其实在指令层面这些都很容易实现,在这里不展开说了。总之跳转的方法就是改PC。
有一种特殊的跳转指令要重点说,这条指令能在跳转前把旧的PC保存下来(指向跳转指令的下一条指令)。对于MIPS,它会被保存到一个寄存器里(就是r31)。对于x86,则保存到了栈里(后面会说什么是栈)。跳转之后,执行一段代码,完了还可以跳回来,因为保存了旧的PC。如果有好几个地方都想跳到这段代码执行一下然后再回到原来的地方,这种指令就很重要了。例如,有一段代码的功能是,根据r4和r5的值进行某种操作(比如计算r4+r5的值,把r4存入r5地址的内存,或者什么更复杂的操作),把某个值(比如r4+r5的值)写进r2。然后就随时都可以把r4和r5设置好之后就用这种跳转进来执行,当把r2算出来后再跳回去,之后就可以使用r2的值了。这就叫做过程调用,或者在C语言里叫函数调用。当然如果只是把r4和r5相加这么简单的话,是没必要写这么一段代码用过程调用来实现的,但有时候这个操作很复杂,在每个地方都写一遍是不值得的,这时候就可以用过程调用。
不过这又会引出新的问题,比如,执行完那段代码之后,除了r2、r4、r5,其它的寄存器会不会变化?真的有可能变的,尤其是程序复杂的时候。一个比较容易想到的办法是,在跳到那段代码之后,如果要用到其它寄存器,就先把它保存到内存里,等事情干完了,从内存里把数读回来,之后再跳回去,就可以保证其它的寄存器值不被破坏了。后面会详细讨论寄存器如何保存。
但是,32个寄存器,并不一定每一个都存着很重要、不可以丢的数据,往往并没有必要保存它们全部。访问内存可是一个很慢的操作。所以,32位MIPS给了一个约定,约定了在过程调用中,哪些寄存器必须保证不变,哪些可以改变。这个约定的具体内容在这不展开说了。对于前者,如果过程中要用这些寄存器,就必须把它们原来的值保存到内存,用完了要恢复。对于后者,过程中可以改变,不需要保存。这也就意味着,如果用了这种跳转指令,那么回来之后,将无法保证后者的寄存器中的值不被破坏。其中典型的就是r31,只要执行了这个跳转指令,r31立马被破坏(成为旧PC的值)。因此,跳转之前必须保证这些寄存器里没有重要的数据还没保存。
下面来说说这个寄存器保存的问题。要保存肯定是往内存里保存,如果内存不够用了,那别说保存寄存器了连程序都不用跑了,所以咱们假设内存够大。保存到内存的什么位置呢?举个例子吧,程序调用了一段代码(就是用这种特殊跳转指令跳转到了一段代码中),然后这段代码还需要再调用另一段代码,这是过程嵌套调用。想一想,刚跳转到过程中时,r31保存了返回地址,如果要再次调用,那么r31会被破坏,所以再次调用之前要保存r31的值。新的调用完成之后,把r31恢复回来,就可以正常返回了。r31保存到内存的什么位置呢?一个容易想到的方法是固定一个地址,规定在这里保存r31时就是保存到这个地址处,之后恢复的时候也是就从这个地址恢复。这需要保证,保存之后,到恢复之前,不能再次执行到“保存”这个位置,否则之前保存的值就被破坏了。那么这个能不能得到保证呢?保存之后,恢复之前,只要不冒出个跳转语句跳转到保存之前,应该就不会出现这个问题。
但事实上,有一个概念叫递归,就是在一个过程中(返回之前)再次调用这个过程自身。看起来好像很奇怪吧?但实际上真的能这么干的,也不会进死循环,这种情景经常遇到。想想,在递归的时候,显然是有嵌套的过程调用的(因为不断地嵌套调用自身),这就涉及到保存r31,然后调用,再恢复r31。r31的保存和恢复之间有没有跳转语句跳转到保存之前呢?显然有,就是那个调用本身!调用就是一种特殊的跳转(与普通跳转的区别就是跳转之前会自动保存PC),并且这个跳转直接跳转到了过程的开始处,之后还会再执行到保存r31的语句,就会破坏之前保存的r31。
所以,如果出现了递归,每次都保存到同一个位置就不行了。可以想到,把保存地址直接写在指令里是行不通的,因为指令已经写死了不能变,每次执行到这条指令都会保存到同一个地址。所以只能想另一个办法,就是把保存的地址写在一个寄存器里,然后往这个地址保存。现在基本所有CPU都是这么干的,有一个专门的寄存器SP(或其它名字),比如,刚进入这个过程的时候,SP的值是1996,那么下一次要保存寄存器的时候,先把SP减去4变成1992,然后保存到1992地址处(因为每个寄存器32位4字节)。再下一次保存时,再次把SP减去4变成1988,然后把数据保存到1988处,就一直这样,也就是说,SP始终指向上一个保存的数的位置,要保存的时候就把SP减去4然后保存到SP地址处。这样就有效避免了每次都保存到同一个地址导致的问题。恢复的时候,也根据SP的值到相应地址处恢复,另外在返回之前必须把SP的值恢复成1996,否则会导致SP混乱。SP是一个相当重要的值,很容易想象,如果SP的值错了,这里保存的一系列数据就都错了,所以实际的CPU中这个SP寄存器绝对不能乱改,也不会出现什么我需要用SP所以先把SP保存起来这样,会乱掉的。当然,实际编程中SP的使用可以简化,例如总共需要保存4个寄存器,那么一次性把SP减去16,然后把这些数分别保存到SP+12、SP+8、SP+4、SP。在指令层面这完全可以做到的。也许你会奇怪为什么SP是一直减而不是一直加,其实理论上都可以,但是现在很多流行的操作系统中,SP是从整个内存地址最高处往下降的,这也许是历史原因吧,不过这样也不错。
简单地说,就是靠寄存器SP来记录现在保存的数据有多少,下次要保存到哪个位置。这其实就是栈了,栈的本质就是用一个“栈顶指针”来记录了下一次进栈要进到什么位置。这个“栈顶指针”就是SP。不过这个栈跟数据结构中的栈还是有点区别的。如果一个过程中,局部变量(是C语言里的概念,先看看就好)太多,寄存器不够用了,这时候也需要用到栈,把多的变量保存到栈中,能够解决这个问题。只要有过程调用的地方,基本都会用到栈,栈是一个非常重要的概念。这里我好像也没发下一个具体的定义,不过大家能理解SP寄存器的用法就可以了。
标签:导致 磁盘 这不 寄存器 出现 展开 复杂 怎么 了解
原文地址:https://www.cnblogs.com/zzc2422/p/9313846.html