看了很多计算机的书,从来没有自己写过东西,其实,写写东西还是不错的。下面谈谈对计算机语言的认识,以c\c++为主,略谈其它。
1.计算机是怎么执行程序的
最简单的计算机应该由一个CPU和RAM组成,但是考虑到RAM在掉电的情况下无法保存数据,还需要一个在掉电情况下能够保存数据的存储器(为了方便就叫永久存储器,用英文就叫Permanent Storage,以下就叫PS,顺便说一下这是我临时取的名字),比如硬盘,ROM,光盘等。现在的计算机都是基于存储程序的原理,意思就是把程序放在PS里面,然后在计算机开机后,自动从PS中的一个地址开始读取程序,一般来说,这段程序是一段启动代码,负责把剩下的主要程序读入RAM,然后跳到RAM开始执行(因为RAM读写比较快),这样计算机就跑起来了。所以程序的一切都在RAM里面。
程序执行过程中会进行些什么操作呢?大概包括以下这些:1.读入RAM中某几个地址的数据到寄存器2.对寄存器中的数据执行逻辑和算术操作3.把数据放到RAM中的某个位置。仅此而已。计算机有一个专门的计数器IP,用来记录下一条指令的地址。每执行一条指令,IP就增加一点指向下一条指令,然后,CPU从IP中记录的地址读取下一条指令执行。可见,CPU只是不断往下执行而已,不过有一种特殊指令可以修改IP的值,使得程序能够改变执行路径,在X86中就有jmp,call,int等。
2.汇编语言
计算机CPU只能识别二进制指令,但是用二进制编写程序太难懂,易出错,编写效率低下。所以有了汇编语言,汇编语言基本就是把二进制指令用不同的单词代替,让汇编器去把它们转换成二进制指令。概念上基本和二进制一样。很多C编译器就是先把C语言先编译成汇编语言程序,再调用汇编器转换成二进制机器指令的。如果要看到编译成的汇编代码可以加上相应的编译器参数。
3.C语言
由于汇编语言和二进制机器语言是一一对应的,所以给不同的cpu写程序,要使用不同的指令集。完全没有可移植性。而且,由于要不断的根据不同机器学习不同语言,显然让人头疼。高级语言应运而生,最开始的高级语言就是FORTRAN,编译器是用汇编语言写成的。然后出现了C语言,并用它完成了UNIX操作系统。
C语言提供了什么呢?它提供了一套标准的操作符号。但实现的功能无非还是跳转,读写内存和逻辑算术运算。其实还有一点,就是提供了统一的内存空间申请和释放方法,以及给地址空间中的数据赋予了类型的概念。并把子程序抽象成了函数的概念,提供了标准的参数传递规则。
4.C语言中的类型
C语言中的变量一般都有个类型,类型分为基本类型,比如int,char,float,long,wchar_t等等,这些都是基本类型。
还有复合类型。
比如:
typedef struct _NODE{
int size;
node_t * next;
}node_t;
node_t就是一个复合类型。它由基本类型组成。
为什么要类型呢?下面看看类型对于编译器意味着什么。
比如一个
int a;
就会分配一个空间(一般是4bytes),它的地址由编译器和运行时的状况决定。那么这个4bytes就是由a的类型int决定的。
再比如:
int a=1;float b;b=a*3;
这里先分配一个sizeof(int)的空间给a,再一个sizeof(float)大小的空间给b,这个已经讲了。a*3呢,这是两个整数相乘,现在编译器就知道编译成X86的整数乘法指令,如果a是float型的,就需要使用浮点乘法指令(需要专门硬件:浮点协处理器),编译完后的指令是完全不同的。a*3的值保存在寄存器中,然后编译器发现b是个浮点型的,由于float和int是二进制不兼容的,所以必须进行整数转换成浮点型的转换操作。这些操作编译器都自动完成了,我们看不到而已。
从这里可以知道,当类型和操作符联系在一起就会决定产生什么指令。所以类型决定了如下东西:
(1)具有该类型的变量的大小
(2)具有该类型的变量的操作
还看一点:
node_t *p=(node_t*) malloc(sizeof(node_t));
p->next=NULL;
这里malloc从堆(heap)里面分配了大小为sizeof(node_t)大小的内存,并返回内存首地址,把这个地址放到p中(X86 32bit中指针是32bit,所以指针很特殊,无论是指向什么类型,指针的大小是不变的,这也是为什么会有void *类型存在的原因)。p->next=NULL做了什么事情?其实在C中如下代码和这句是等价的(假设struct中的数据时连续存储的,不考虑地址对齐问题):
(node_t *)((char *)p+sizeof(int))=NULL;
就是说用把p的值加4让p指向next的首地址,然后根据next的类型给其赋值为NULL(也就是0)。所以复合结构中类型也规定了复合结构的变量的空间的分布,这种分布也是根据基本类型的大小得到的。
上面的代码中,p之所以要转换成char*是因为指针类型的加法操作,比如p+N(p为A类型)实际上是把p的值增加sizeof(A)*N.,如果不转换,这里就是把p增加了sizeof(int)*sizeof(node_t),因为我们只想增加sizeof(int)所以要保证sizeof(A)==1,为此先把p变成char *类型,因为sizeof(char)==1.
4.C内存分配
其实任何计算机语言都要为变量申请内存,那么变量是怎么获得内存的呢?一份程序编译成二进制了之后,载入到内存是这样的:
一部分内存用来放代码,称为代码段。
一部分内存用来放数据,称为数据段。数据段比较复杂。
比如我们:
int a;
int b=1;
int main(){return 0;}
a和b就会被放到数据段,也就是说变量a,b的地址在数据段中。数据段又分为两类,初始化数据和未初始化数据段。a没有赋值是没有初始化了,b初始化为1.这两类数据分别放到不同数据段。b的地址中在文件中就已经有值1,只要载入文件到内存就获得了数据,但是a呢,一般来说,在main运行以前,编译器插入的CRT 启动代码会吧未初始化数据段全部清零的。这两种数据段都是放静态数据的。
除此之外还有:
栈(stack)。栈也是一个数据段,即一段内存。x86中,cpu有一个sp寄存器(stack pointer),指向栈的末地址。
看下面代码:
void fun(int a ){
int b;
return;
}
这里a,b都是变量,它们其实只有当调用函数fun的时候才分配空间,这个空间就在栈中。基本方法就是把sp减去几个值(sizeof(a)+sizeof(b)),然后认为a的地址就是sp+sizeof(b),b的地址就是sp。在函数结束后会插入代码把sp加上8回归原址。(这里只是笼统说一下,要说清楚,实在太费口舌)。
当然还有malloc函数是在(heap)中分配的。heap其实是一块内存,使用链表连接起来。由一堆函数来管理,malloc,free就是。当然malloc是C运行时的标准函数,其实除了malloc也可以使用操作系统提供的其它函数,来获取更大的灵活性和性能,不过操作系统函数不能操作标准堆,而是另外的空间。
5函数
在使用汇编语言编程序的时候,没有函数这个东西。但是为了把程序简化,会手工编写子程序。同时会遇到一个问题,即参数传递问题。
一个子程序有个入口地址,我们可以直接跳到这个地址,开始子程序的执行。但是我们希望子程序根据不同的参数返回不同的答案。那么参数怎么传给子程序呢?一般有这么几种方法:
1.把参数放到寄存器中,然后由子程序从寄存器中读取参数。当然使用哪个寄存器必须约定好。
2.把参数放到栈中。按约定从栈中读取。这个约定就是函数的类型。前面已经稍微提到过一点栈。
3.放到一个静态数据中。其实和放到寄存器意思差不多,只不过慢一些。
X86中标准C语言使用方法2。所以C函数支持递归操作,因为每次进入都是重新从栈中分配内存,不会覆盖原有数据,是可重入的。不过在一些嵌入式的小型CPU编译器也有使用寄存器的,那么函数将是不可重入的。