首页 > 代码库 > C#编译和运行原理
C#编译和运行原理
关于编译与内存的关系,以及执行时内存的划分
1、所谓在编译期间分配空间指的是静态分配空间(相对于用new动态申请空间),如全局变量或静态变量(包括一些复杂类型的
常量),它们所需要的空间大小可以明确计算出来,并且不会再改变,因此它们可以直接存放在可执行文件的特定的节里(而且
包含初始化的值),程序运行时也是直接将这个节加载到特定的段中,不必在程序运行期间用额外的代码来产生这些变量。
其实在运行期间再看“变量”这个概念就不再具备编译期间那么多的属性了(诸如名称,类型,作用域,生存期等等),对应的
只是一块内存(只有首址和大小), 所以在运行期间动态申请的空间,是需要额外的代码维护,以确保不同变量不会混用内存。
比如写new表示有一块内存已经被占用了,其它变量就不能再用它了; 写delete表示这块内存自由了,可以被其它变量使用了。
(通常我们都是通过变量来使用内存的,就编码而言变量是给内存块起了个名字,用以区分彼此)
内存申请和释放时机很重要,过早会丢失数据,过迟会耗费内存。特定情况下编译器可以帮我们完成这项复杂的工作(增加额外
的代码维护内存空间,实现申请和释放)。从这个意义上讲,局部自动变量也是由编译器负责分配空间的。进一步讲,内存管理
用到了我们常常挂在嘴边的堆和栈这两种数据结构。
最后对于“编译器分配空间”这种不严谨的说法,你可以理解成编译期间它为你规划好了这些变量的内存使用方案,这个方案写
到可执行文件里面了(该文件中包含若干并非出自你大脑衍生的代码),直到程序运行时才真正拿出来执行。
2、编译其实只是一个扫描过程,进行词法语法检查,代码优化而已。我想你说的“编译时分配内存”是指“编译时赋初值”,它只是形成一个文本,检查无错误,并没有分配内存空间。
当你运行时,系统才把程序导入内存。一个进程(即运行中的程序)在主要包括以下五个分区:
栈区、堆区、全局数据区/静态区、代码区、常量区
- 栈区用来存放局部数据或者是函数的参数,函数的返回值之类的变量(其中还有返回到调用函数下一条指令的地址)
- 堆区用来存放程序中动态申请内存的变量
- 全局变量/静态区用来存放程序中的全局变量或者是静态变量,因为它们的大小是确定的,在编译期间就已经进行静态空间的分配,而且不会改变,这样会提高程序对这些数据的访问速度
- 代码区(code)用来存放编译后的二进制代码
- 常量区用来存放我们声明的常量(const类型)
代码(编译后的二进制代码)放在code区,代码中生成的各种变量、常量按不同类型分别存放在其它四个区。系统依照代码顺序
执行,然后依照代码方案改变或调用数据,这就是一个程序的运行过程。
3、
编译时分配内存
---------------
编译时是不分配内存的。此时只是根据声明时的类型进行占位,到以后程序执行时分配内存才会正确。所以声明是给编译器看的
,聪明的编译器能根据声明帮你识别错误。
运行时分配内存
---------------
这是对的,运行时程序是必须调到“内存”的。因为CPU(其中有多个寄存器)只与内存打交道的。程序在进入实际内存之前要首
先分配物理内存。
编译过程
---------------
当执行这个EXE文件以后,此程序就被加载到内存中,成为进程。此时一开始程序会初始化一些全局对象,然后找到入口函数
,就开始按程序的执行语句开始执行。此时需要的内存只能在程序的堆上进行动态增加/释放了
编译过程
1. 文件信息,包括文件类型,文件大小等等,如:DLL文件的前两个字节是0x4d 0x5a。
2. 代码,就是程序,如 HData data = http://www.mamicode.com/new HData(); HData是自己定义的类,这一句被转换成如下形式,共占37个字节。
00000040 B9 10 7F DA 00 mov ecx,0DA7F10h
00000045 E8 E2 4D D4 FB call FBD44E2C
0000004a 89 45 B4 mov dword ptr [ebp-4Ch],eax
0000004d 8B 4D B4 mov ecx,dword ptr [ebp-4Ch]
00000050 E8 0B F0 AB FB call FBABF060
00000055 8B 55 C4 mov edx,dword ptr [ebp-3Ch]
00000058 8B 45 B4 mov eax,dword ptr [ebp-4Ch]
0000005b 8D 92 84 01 00 00 lea edx,[edx+00000184h]
00000061 E8 3A 5B FE 74 call 74FE5BA0
3. 数据,包括全局变量、静态变量和常量。类成员变量、方法局部变量不编译到文件中。如:static int a = 0; 在文件中占四个字节,int a = 1, 在文件中不占字节,string str = "12345", 虽然是类成员,但其中隐含常量“12345”,在文件中占5个字节 。
运行过程:
1. 双击图标时,系统把exe文件全部调入内存,主要包括所有程序和全局变量,这一部分内存一直被占用到退出程序。
2. 运行程序,还以这一句为例,HData data = http://www.mamicode.com/new HData(); HData类的程序已经在内存中,所有HData类的实例共用一套程序,系统只是为HData的数据(主要是HData中的变量)分配一块内存,并把这块内存的起点,静态变量除外,它在加载exe文件的时候调入内存。data 失效的时候,这一块内存被释放。
局部变量,void aaaa(){ int a = 1; } 这段程序的主体部分大约占5个字节,a变量不占内存,调用aaaa()的时候执行一行汇编码 add bp, 4 就是在栈中分配四个字节给a,aaaa()返回的时候,a变量占用的四个字节被释放。
3. 退出应该程序的时候释放exe文件占用的内存。
以上只是一个大至的原理,实际情况要复杂的多,象分配的内存是可移动的,甚至会被放到内存中。不过咱们做应用程序的了解这些就足够了。再深入是的做系统和做编译器的人管的事。
写完以后才发现是07年的帖子,哈,还是发了吧。
首先是预处理器,如果在项目中有头文件和宏表达式,那么它将负责包含头文件和翻译所有的宏观表达式。
接下来是编译器,它不是直接生成二进制代码,而是生成汇编代码(.s),这基本上是所有现代的非结构化语言的共同基础。
然后,汇编程序把汇编代码翻译成目标代码(.o和.obj文件,机器指令)。
最后链接器,它把所有彼此相关的目标文件和生成的可执行文件或库链接起来。
总而言之,在一般情况下,我们的代码首先翻译成汇编代码,接着翻译成机器指令(二进制代码)。
托管环境的编译过程(C#/Java)
在托管环境中,编译的过程略有不同,我们熟知的托管语言有C#和Java,接下来,我们将以C#和Java为例介绍在托管环境中的编译过程。
当我们在喜爱的IDE中编写代码时,第一个检测我们代码的就是IDE(词法分析),然后,编译成目标文件和链接到动态/静态库或可执行文件进行再次检查(语法分析),最后一次检查是运行时检查。托管环境的共同特点是:编译器不直接编译成机器码,而是中间代码,在.NET中称为MSIL - Microsoft Intermediate Language,Java是字节码(Bytecode)
在那之后,在运行时JIT(Just In Time)编译器将MSIL翻译成机器码,这意味着我们的代码在真正使用的时候才被解析,这允许在CLR(公共语言运行时)预编译和优化我们的代码,实现程序性能的提高,但增加了程序的启动时间,我们也可以使用Ngen(Native Image Generator)预编译我们的程序,从而缩短程序的启动时间,但没有运行时优化的优点。(JeffWong的补充Java是先通过编译器编译成Bytecode,然后在运行时通过解释器将Bytecode解释成机器码;C#是先通过编译器将C#代码编译成IL,然后通过CLR将IL编译成机器代码。所以严格来说Java是一种先编译后解释的语言,而C#是一门纯编译语言,且需要编译两次。)
.Net Framework就是在Win32 core上添加了一个抽象层,它提供的一个好处就是支持多语言、JIT优化、自动内存管理和改进安全性;另外一个完整解决方案是WinRT,但这涉及到另外一个主题了,这里不作详细介绍。
JIT编译的优点和缺点
JIT编译带来了许多好处,最大的一个在我看来是性能的优势,它允许CLR(通用语言运行时扮演Assembler组件)只执行需要的代码,例如:假设我们有一个非常大的WPF应用程序,它不是立即加载整个程序,而是CLR开始执行时,我们代码的不同部分将通过一个高效的方法翻译成本地指令,因为它能够检查系统JIT和生成优化的代码,而不是按照一个预定义的模式。不幸的是,有一个缺点就是启动的过程比较慢,这意味着它不适用于加载时间长的包。
JIT的替代方案使用NGen
如果Visual Studio由JIT创建,那么它的启动我们将需要等待几分钟,相反,如果它是使用Ngen(Native Image Generator)编译,它将创建纯二进制可执行文件,如果只考虑速度的问题,那是绝对是正确的选择。
在非托管环境中,我们需要知道编译的过程分成编译和连接两个阶段,编译阶段将源程序(*.c,*.cpp或*.h)转换成为目标代码(*.o或*.obj文件),至于具体过程就是上面说的C/C++编译过程的前三个阶段;链接阶段是把前面转成成的目标代码(obj文件)与我们程序里面调用的库函数对应的代码链接起来形成对应的可执行文件(exe文件)。
托管环境中,编译过程可以分为:词法分析、语法分析、中间代码生成、代码优化和目标代码生成等等过程;无论是.NET还是Java,它们都会生成中间代码(MSIL或Bytecode),然后把优化后的中间代码翻译成目标代码,最后在程序运行时,JIT将IL翻译成机器码。
无论是托管或非托管语言,它们的编译编译过程是把高级语言翻译成计算机能理解的机器码,由于编译过程涉及的知识面很广(编译的原理和硬件知识),而且本人的能力有限,也只能简单的描述一下这些过程,如果大家希望深入了解编译的原理,我推荐大家看一下《编译原理》。
参考
[1] http://www.developingthefuture.net/compilation-process-and-jit-compiler/
更新:07/31/2013
本文作者:JK_Rush
本文摘抄自博客园
C#编译和运行原理