第一节:基本原理[2]
这里并没不是讨论大学课程中所学的《编译原理》,只是写一些我自己对C++编译器及链接器的工作原理的理解和看法吧,以我的水平,还达不到讲解编译原理(这个很复杂,大学时几乎没学明白)。
要明白的几个概念:
1、编译:编译器对源文件进行编译,就是把源文件中的文本形式存在的源代码翻译成机器语言形式的目标文件的过程,在这个过程中,编译器会进行一系列的语法检查。如果编译通过,就会把对应的CPP转换成OBJ文件。
2、编译单元:根据C++标准,每一个CPP文件就是一个编译单元。每个编译单元之间是相互独立并且互相不可知。
3、目标文件:由编译所生成的文件,以机器码的形式包含了编译单元里所有的代码和数据,还有一些期他信息,如未解决符号表,导出符号表和地址重定向表等。目标文件是以二进制的形式存在的。
根据C++标准,一个编译单元(Translation
Unit)是指一个.cpp文件以及这所include的所有.h文件(编译器通过后缀名来辨识是否编译该文件,因此“.h”的头文件一概不理会,而“.cpp”的源文件一律都要被编译[1]),.h文件里面的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp文件为一个.obj文件,后者拥有PE(Portable
Executable,即Windows可执行文件)文件格式,并且本身包含的就是二进制代码,但是不一定能执行,因为并不能保证其中一定有main函数。当编译器将一个工程里的所有.cpp文件以分离的方式编译完毕后,再由链接器进行链接成为一个.exe或.dll文件。
在一个文件中使用#include <otherFile>包含另外一个文件时,在编译时会把被包含文件的源代码嵌入到当前文件中,而且这种嵌入是递归的,即还会把被包含文件所包含的文件也嵌入到当前文件中。
下面让我们来分析一下编译器的工作过程:
我们跳过语法分析,直接来到目标文件的生成,假设我们有一个A.cpp文件,如下定义:
int n = 1;
void FunA()
{
++n;
}
它编译出来的目标文件A.obj就会有一个区域(或者说是段),包含以上的数据和函数,其中就有n、FunA,以文件偏移量形式给出可能就是下面这种情况:
偏移量 内容 长度
0x0000 n 4
0x0004 FunA ??
注意:这只是说明,与实际目标文件的布局可能不一样,??表示长度未知,目标文件的各个数据可能不是连续的,也不一定是从0x0000开始。
FunA函数的内容可能如下:
0x0004 inc DWORD PTR[0x0000]
0x00?? ret
这时++n已经被翻译成inc DWORD PTR[0x0000],也就是说把本单元0x0000位置的一个DWORD(4字节)加1。
有另外一个B.cpp文件,定义如下:
extern int n;
void FunB()
{
++n;
}
它对应的B.obj的二进制应该是:
偏移量 内容 长度
0x0000 FunB ??
这里为什么没有n的空间呢,因为n被声明为extern,这个extern关键字就是告诉编译器n已经在别的编译单元里定义了,在这个单元里就不要定义了。由于编译单元之间是互不相关的,所以编译器就不知道n究竟在哪里,所以在函数FunB就没有办法生成n的地址,那么函数FunB中就是这样的:
0x0000 inc DWORD PTR[????]
0x00?? ret
那怎么办呢?这个工作就只能由链接器来完成了。
为了能让链接器知道哪些地方的地址没有填好(也就是还????),那么目标文件中就要有一个表来告诉链接器,这个表就是“未解决符号表”,也就是unresolved
symbol table。同样,提供n的目标文件也要提供一个“导出符号表”也就是exprot symbol
table,来告诉链接器自己可以提供哪些地址。
好,到这里我们就已经知道,一个目标文件不仅要提供数据和二进制代码外,还至少要提供两个表:未解决符号表和导出符号表,来告诉链接器自己需要什么和自己能提供些什么。那么这两个表是怎么建立对应关系的呢?这里就有一个新的概念:符号。在C/C++中,每一个变量及函数都会有自己的符号,如变量n的符号就是n,函数的符号会更加复杂,假设FunA的符号就是_FunA(根据编译器不同而不同)。
所以,
A.obj的导出符号表为
符号 地址
n 0x0000
_FunA 0x0004
未解决符号为空(因为他没有引用别的编译单元里的东西)。
B.obj的导出符号表为
符号 地址
_FunB 0x0000
未解决符号表为
符号 地址
n 0x0001
这个表告诉链接器,在本编译单元0x0001位置有一个地址,该地址不明,但符号是n。
在链接的时候,链接在B.obj中发现了未解决符号,就会在所有的编译单元中的导出符号表去查找与这个未解决符号相匹配的符号名,如果找到,就把这个符号的地址填到B.obj的未解决符号的地址处。如果没有找到,就会报链接错误。在此例中,在A.obj中会找到符号n,就会把n的地址填到B.obj的0x0001处。
但是,这里还会有一个问题,如果是这样的话,B.obj的函数FunB的内容就会变成inc DWORD
PTR[0x000](因为n在A.obj中的地址是0x0000),由于每个编译单元的地址都是从0x0000开始,那么最终多个目标文件链接时就会导致地址重复。所以链接器在链接时就会对每个目标文件的地址进行调整。在这个例子中,假如B.obj的0x0000被定位到可执行文件的0x00001000上,而A.obj的0x0000被定位到可执行文件的0x00002000上,那么实现上对链接器来说,A.obj的导出符号地地址都会加上0x00002000,B.obj所有的符号地址也会加上0x00001000。这样就可以保证地址不会重复。
既然n的地址会加上0x00002000,那么FunA中的inc DWORD
PTR[0x0000]就是错误的,所以目标文件还要提供一个表,叫地址重定向表,address redirect table。
总结一下:
目标文件至少要提供三个表:未解决符号表,导出符号表和地址重定向表。
未解决符号表:列出了本单元里有引用但是不在本单元定义的符号及其出现的地址。
导出符号表:提供了本编译单元具有定义,并且可以提供给其他编译单元使用的符号及其在本单元中的地址。
地址重定向表:提供了本编译单元所有对自身地址的引用记录。
链接器的工作顺序:
当链接器进行链接的时候,首先决定各个目标文件在最终可执行文件里的位置。然后访问所有目标文件的地址重定义表,对其中记录的地址进行重定向(加上一个偏移量,即该编译单元在可执行文件上的起始地址)。然后遍历所有目标文件的未解决符号表,并且在所有的导出符号表里查找匹配的符号,并在未解决符号表中所记录的位置上填写实现地址。最后把所有的目标文件的内容写在各自的位置上,再作一些另的工作,就生成一个可执行文件。
说明:实现链接的时候会更加复杂,一般实现的目标文件都会把数据,代码分成好向个区,重定向按区进行,但原理都是一样的。
明白了编译器与链接器的工作原理后,对于一些链接错误就容易解决了。
下面再看一看C/C++中提供的一些特性:
extern:这就是告诉编译器,这个变量或函数在别的编译单元里定义了,也就是要把这个符号放到未解决符号表里面去(外部链接)。
static:如果该关键字位于全局函数或者变量的声明前面,表明该编译单元不导出这个函数或变量,因些这个符号不能在别的编译单元中使用(内部链接)。如果是static局部变量,则该变量的存储方式和全局变量一样,但是仍然不导出符号。
默认链接属性:对于函数和变量,默认链接是外部链接,对于const变量,默认内部链接。
外部链接的利弊:外部链接的符号在整个程序范围内都是可以使用的,这就要求其他编译单元不能导出相同的符号(不然就会报duplicated external
symbols)。
内部链接的利弊:内部链接的符号不能在别的编译单元中使用。但不同的编译单元可以拥有同样的名称的符号。
为什么头文件里一般只可以有声明不能有定义:头文件可以被多个编译单元包含,如果头文件里面有定义的话,那么每个包含这头文件的编译单元都会对同一个符号进行定义,如果该符号为外部链接,则会导致duplicated
external symbols链接错误。
为什么公共使用的内联函数要定义于头文件里:因为编译时编译单元之间互不知道,如果内联被定义于.cpp文件中,编译其他使用该函数的编译单元的时候没有办法找到函数的定义,因些无法对函数进行展开。所以如果内联函数定义于.cpp里,那么就只有这个.cpp文件能使用它。
第二节:标准C和C++将编译过程[1]
1.字符映射(Character
Mapping)
文件中的物理源字符被映射到源字符集中,其中包括三字符运算符的替换、控制字符(行尾的回车换行)的替换。许多非美式键盘不支持基本源字符集中的一些字符,文件中可用三字符来代替这些基本源字符,以??为前导。但如果所用键盘是美式键盘,有些编译器可能不对三字符进行查找和替换,需要增加-trigraphs编译参数。在C++程序中,任何不在基本源字符集中的字符都被它的通用字符名替换。
2.行合并(Line Splicing)
以反斜杠\结束的行和它接下来的行合并。
3.标记化(Tokenization)
每一条注释被一个单独的空字符所替换。C++双字符运算符被识别为标记(为了开发可读性更强的程序,C++为非ASCII码开发者定义了一套双字符运算符集和新的保留字集)。源代码被分析成预处理标记。
4.预处理(Preprocessing)
调用预处理指令并扩展宏。使用#include指令包含的文件,重复步骤1到4。上述四个阶段统称为预处理阶段。
5.字符集映射(Character-set Mapping)
源字符集成员、转义序列被转换成等价的执行字符集成员。例如:'\a'在ASCII环境下会被转换成值为一个字节,值为7。
6.字符串连接(String Concatenation)
相邻的字符串被连接。例如:"""hahaha""huohuohuo"将成为"hahahahuohuohuo"。
7.翻译(Translation)
进行语法和语义分析编译,并翻译成目标代码。
8.处理模板
处理模板实例。
9.连接(Linkage)
第三节:C++中extern “c”[3]
为了解决C++中函数可以重载这个特性,详细原因如下:函数经过编译系统的翻译成汇编,函数名对应着汇编标号。因为C编译函数名与得到的汇编代号基本一样,如:fun()=>_fun,
main=>_main但是C++中函数名与得到的汇编代号有比较大的差别。如:由于函数重载,函数名一样,但汇编代号绝对不能一样。为了区分,编译器会把函数名和参数类型合在一起作为汇编代号,这样就解决了重载问题。具体如何把函数名和参数类型合在一起,要看编译器的帮助说明了。这样一来,如果C++调用C,如fun(),则调用名就不是C的翻译结果_fun,而是带有参数信息的一个名字,因此就不能调用到fun(),为了解决这个问题,加上extern
"C"表示该函数的调用规则是C的规则,则调用时就不使用C++规则的带有参数信息的名字,而是_fun,从而达到调用C函数的目的。
第四节:查看g++编译过程
【参考文件】
1、http://blog.csdn.net/shiwenbin333/archive/2010/01/08/5157797.aspx
2、http://blog.sina.com.cn/s/blog_5f8817250100i3oz.html
3、http://blog.myspace.cn/e/400991786.htm
4、http://xredman.iteye.com/blog/700901
分享到:
相关推荐
总之,C++编译器负责将源代码转化为机器码目标文件,每个编译单元独立处理,而链接器则整合这些目标文件,解决未解析的外部依赖,形成最终的可执行程序。理解和掌握这一过程对于提升C++编程技能和问题排查能力具有...
**连接器ld的功能与原理** 连接器ld的主要任务是在编译器生成的一系列目标文件或静态库之间进行连接,形成一个完整的可执行程序。它的工作流程包括以下几个步骤: 1. **符号解析**:连接器首先处理输入的目标文件...
### Linux上的C-C++编译器与调试器详解 #### 一、GCC:Linux平台上的C/C++编译器 **GCC** (GNU Compiler Collection) 是一个强大的编译器套件,它支持多种编程语言,其中最为人所知的是其支持C、C++和Objective C...
然而,编译器作为连接编程语言与机器语言的桥梁,其工作原理对于许多开发者来说仍然是一个神秘的领域。本文将通过对"UnderCV C++编译器源代码"的分析,帮助读者深入了解C++编译器的工作流程,以及在实际编码中如何...
本文将深入探讨C++编译器的工作原理、重要性以及如何使用。 首先,我们要理解什么是编译器。编译器是一种特殊的程序,它的主要任务是将程序员用高级语言(如C++)书写的源代码转换成目标代码,即机器语言。这个过程...
《自己动手写编译器链接器》是一本深入解析编译器与链接器工作原理的教程,通过PDF文档和源码实例,为读者提供了一条亲手构建这些关键工具的实践之路。编译器和链接器是软件开发过程中的重要组成部分,它们在程序的...
《C语言子集编译器前端的实现与解析》 ...通过这个项目,开发者不仅可以深入理解编译器的工作原理,还可以掌握如何用C++实现编译器的关键组件,这对于提升软件开发能力和理解底层系统运行机制大有裨益。
三合一C/C++编译器,适合初学者:VC:Visual C++ 6.0,简称VC或者VC6.0,是微软的一款C++编译器,将“高级语言”翻译为“机器语言(低级语言)”的程序。;DEV是一个C++ 开发工具。它包括多页面窗口、工程编辑器,在...
Arm编译器6.0是Arm公司为开发人员提供的一款高性能C/C++编译器,它支持最新的Arm架构和语言标准,包括C++14和OpenMP 4.5。以下是一些主要特性: 1. **优化性能**:通过使用先进的代码优化技术,如循环展开、死代码...
在这个过程中,我们经常会遇到两个关键工具:MASM和LINK,它们是微软汇编语言环境中的编译器和连接器。 首先,MASM(Microsoft Macro Assembler)是微软提供的一个汇编程序,用于将汇编语言源代码转换为机器可执行...
标题 "C和C++编译器" 涉及到的是两种重要的编程语言——C语言和C++语言的编译工具。编译器是将高级编程语言(如C或C++)编写的源代码转换为计算机可以理解的机器码的关键软件。在深入探讨之前,我们先了解下编译器的...
我从没见过(不过应该有)任何一本C++教材有讲过何谓编译器(Compiler)及连接器(Linker)(倒是在很老的C教材中见过),现在都通过一个类似VC这样的编程环境隐藏了大量东西,将这些封装起来。在此,对它们的理解是...
G++是GCC(GNU Compiler Collection)的一部分,是一个用于编译C++源代码的开源编译器。GCC最初是由GNU项目创建的,旨在提供一个自由且功能...这个过程虽然较为复杂,但能够让你深入了解操作系统和编译工具的工作原理。
标题中的“c/c++编译器(dc)”指的是一个名为DC的C/C++编程语言的编译器,它专为初学者设计,具有用户友好的界面和简单易用的操作流程。DC编译器是C和C++开发环境中的一种,旨在帮助初学者快速上手编程,同时也适用于...
在学习汇编语言的过程中,理解编译器和链接器的工作原理是非常重要的。编译器负责将源代码翻译成机器码,而链接器则确保所有代码片段正确连接,形成一个完整的程序。通过实际操作MASM和LINK,开发者能更深入地理解...
《编译原理与C编译器实现》 在软件开发领域,编译器扮演着至关重要的角色,它将高级语言转换成机器可执行的指令,是连接编程抽象和硬件细节的桥梁。本主题主要探讨了C编译器的实现过程,通过分析提供的源代码,我们...
-L dirname:将dirname目录添加到连接器的库文件搜索路径。 -l name:在连接时加入名为name的库文件。 GCC的安装非常简单,通常可以通过包管理器如apt-get、yum或dnf进行安装。例如,在基于Debian的系统中,可以...
在编程世界中,编译器是连接高级语言与机器语言的关键桥梁,而语法分析器则是编译器的重要组成部分。本篇文章将深入探讨C++编译原理中的语法分析器,帮助读者理解其工作原理、设计方法以及实现过程。 一、编译原理...
4. 调试:MinGW还可以与开源调试器GDB结合使用,进行程序的调试工作。 5. 创建多文件项目:通过使用Makefile,可以管理多个源文件的编译和链接过程。 总的来说,MinGW是一个轻量级但功能强大的开发环境,对于...
下面我们将深入探讨C++实时编译器的关键特点、工作原理以及其在实际开发中的应用。 一、C++实时编译器的特点 1. 实时性:C++实时编译器的核心特性是实时反馈,即程序员在输入代码后,无需等待整个程序编译完成,...