C运行机制

  在尝试用opencl编写matlab上的代码之初,因要选择在GPUCPU上进行,不由联想到C代码在计算机上是怎么运行的呢。于是从网上整理了一些资料。 

  得意作为一种编程语言,本身是谈不上工作原理的,实际上C语言所有的语法,正是C语言编译器的工作原理或者工作机制的具体实现。要细致的讨论起来是不可能,但是作为C语言程序员,必须了解这个大致的流程。

  使用C语言开发软件的一般步骤为:
  1、使用编辑器编写源代码(source code)
  2、使用编译器将源代码(source code)编译成目标代码(object language)
  3、运行目标代码(object language),即编写好的程序。

  可以看到,编译器是编程语言的关键,一种编程语言,本质上就是一种编译器,编程语言的效率高低,取决于编译器转换成的目标代码的效率高低。

  得意可以这样来理解一门编程语言:
  1、首先定义好该语言的语法和规范,例如,如何输入输出数据,如何操作文件等;
  2、按照语法和规范开发编译器,这是关键步骤;
  3、开发辅助功能,例如,调试器,编辑器等;
  4、向世界公开该语言。

  得意下面具体介绍C语言运行过程的几个步骤:

一、程序运行之前,代码从一个文本文件变成可执行程序经过了四个步骤,两次编译,汇编和链接

1. 预编译preprocessor,对程序做预处理工作。比如对宏定义的变量替换。

2. 编译。Compiler,C程序翻译成汇编语言程序。此过程有时会对程序进行优化。

(在计算机底层硬件中,所有的数据都是以1和0两个高低电平来表示,计算机只能识别这两个电平。编程语言类似于人类语言,我们很容易就能理解它的意思,编写代码的效率非常高。但是,计算机只认识0和1,如何才能将“人类语言”转换成“0&1语言”呢?这就是编译器的工作了。编译器能够将源代码(人类语言)翻译成目标代码(0&1语言),便于人类和计算机的沟通。)

3. 汇编。Assembler,将步骤2得到的汇编语言再汇编得到可重定位目标程序.o文件。

4. 链接。Link,通常.o有很多个,并不能直接运行。还要包含一些共享目标文件。比如printf函数,它位于静态库中,需要linker进行链接。

注:

  头文件中牵涉到的所有函数分别编译成单独模块并最后打包成了一个文件(放在系统固定的文件夹中),这个文件就是所谓的静态库

  windows中后缀名是.libunix是.a,当我们link的时候,只需要在指定库中找到printf对应的那部分二进制代码添加到程序中就行了。从理论上讲hello.c中有几个printf,就会在可执行文件中嵌入几次printf的二进制模块,而且当系统内有多个hello同时运行时每个hello都会维护一段属于自己的printf,这样做显然是一种浪费。

  使用共享库(shared library)可以解决这个问题,共享库也是一个目标模块(后缀名.so),它在程序运行之前会被加载到存储器中某一个特定的区域(linux中,是从地址0×40000000开始的一段区域),并和用到它的程序链接起来,这个过程被称为动态链接,因此共享库在windows中又被称为动态链接库(DLL)。比如hello在链接时其实并没有把printf模块加到可执行程序中,而只是告诉我们的hello一声,待会要用到printf的时候去共享库里找xx就行了。链接是程序再被真正执行前一个极其重要的步骤

二、程序在存储器中

  可执行程序在被CPU执行之前存在于内存中。有动态随机存储器(DRAM)和静态随机存储器(SRAM),cpu内部的高速缓存用的是SRAM。

  区别进程和程序。进程是程序执行的饿一个实例。instance。,是程序执行的一个过程。因此,程序时留在磁盘中宏的一些磁信号,进程是系统各种资源(CPU、存储器、IO设备、、)共同作用的结果。现代操作系统采取了一种叫虚拟存储器(virtual memory)的机制来有效地管理存储器,即把系统的存储设备全部隐藏在背后,无论实际的物理存储器(dram)有多大都提供给我们一个固定虚拟的线性空间(32位操作系统就有4G空间),系统在幕后对实际的地址进行映射(可能在dram中,也可能在磁盘上),而我们就感觉自己在使用一台存储器很大的计算机,尽管当实际的dram很小时我们还是感觉很慢,于此同时硬盘灯在不停闪烁。

   Linux将虚拟存储器高端的1/4留给内核,剩下3/4全留给用户进程。虚拟存储器上中的程序主要由以下几个重要组成部分:

1.进程控制块(process control block,简称PCB)

PCB中保存了进程运行时的存储器图像和寄存器信息,它帮助操作系统在内存中找到我们的程序。

2.栈(stack)和堆(heap)

程序中的自动变量都位于栈上,而堆则用来让程序员自己手动分配(malloc)和释放(free)的内存空间,如果程序员忘了释放,则有垃圾收集器gc代劳。除此以外,栈还是程序转移中一个很重要的概念,程序的返回地址通常也保存在栈上。

3.文本段(text segment)和数据段(data segment)

  所谓的文本段和数据段对应的就是程序的代码部分和全局变量,把程序的代码和数据分开处理是有好处的,比如我们在windows打开好几个word,这些进程只是数据段不同罢了(它们都拥有相同的代码),因此内存中永远只要有一份word的代码就行了。

4.共享库的映射区域

  操作系统通过将共享的对象映射到虚拟存储器的“共享区域”来使得代码能够共享,一方面提高存储器的利用率,一方面可以使得进程能够共享一些数据。

  如果某一时刻系统中有20个程序正在运行,而这些程序都需要在屏幕上打印东西,系统就没有必要为每个程序都维护一段printf的代码,只要分别从各自的.bss中取出字符串然后用同一个printf完成输出就行了。同样的道理,当有多个hello在系统中运行时,它们也完全可以共享同一个文本段。这也就是为什么会把进程定义为程序的一个实例的原因。不妨回想一下面向对象中对象的概念,我们在写class的时候定义成员字段不就是在分配数据?而定义方法字段不就是在操作这些数据?在对象被实例化以前,这些定义只不过是一些“白纸黑字”,而只有经过实例化,实例们才在存储器中有了自己的映像。而多个实例之间可以共享“方法”(文本)但是独有“成员”(数据)的特点,也和进程如出一辙。

  现在我们可以描述hello在存储器中图像了。hello的代码位于文本段中,字符串“hello,world”在只读段中,printf位于共享库的映射区域,程序在执行时用到了用户栈,用户栈从0xbfffffff开始,向下生长。以上的图景只发生在一瞬间,我们难以追踪,要想看清hello的本来模样,还是得在目标文件上做文章。

三、目标文件的格式。

1、可重定位目标文件.o

2、可执行文件a.out

  可重定位目标文件(hello.o)离最终的可执行目标文件(a.out)只有一步之遥,这关键的一步就是前面说的链接。

  链接通常有两步,第一步是解析符号,符号解析主要用来解决多个模块之间全局字段的协调问题,比如我们在两个.c的文件中都定义了全局变量x,或者引用 了不曾定义过的函数foo(),链接器都会报错(link error)。第二步就是重定位,重定位将每个目标模块的节最终合并成一个大的节(section),并且根据rel.text来修改调用外部函数(printf)或者引用任何全局变量(“hello,world”)的指令。hello.o和a.out最大的区别在于,a.out的节头目表为每个节都分配了真实地址,而hello.o中的节头目表只在重定位时为    链接器提供了一个快速定位节的方式。(以hello world为例)

  编译器在_main之前会先去找一个_start符号。事实上程序在运行的初期还需要做一些初始化和清理的工作,这些代码位于crt1.o模块中,即c运行时(runtime)库,它包含了程序的入口函数_start,由它负责调用__libc_start_main初始化libc,并且调用main函数进入真正的程序主体,这部分代码必须在链接时加进来(对我们来说是透明的),否则程序根本运行不到的main。

3、printf

   printf的机器码位于/lib/libc.so.6的共享库中,它将在程序运行时被加载到存储器的共享库映射区域。printf中又用到了系统调用write来输出格式串,所谓系统调用可以看成是操作提供给程序员的一个编程接口,我们可以调用它来获取操作系统提供的一些服务,完成一些和输入输出有关的操作。

  原地址为:http://blog.csdn.net/witsmakemen/article/details/17653231

猜你喜欢

转载自blog.csdn.net/qq_34018578/article/details/78840002