[原创]关于编译,链接及库的一些基础知识
发表于:
2010-5-29 11:59
7257
最近看了本《程序员的自我修养》,其中很多内容着实不错,明确了很多从前模糊的概念,所以贴上关于编译,链接及库的一些笔记,供菜鸟分享。
-------------------------------------------------------------------------------------------------*****************编译和链接*****************
从源代码到可执行代码可以分解为4个步骤,分别是预处理(prepressing)、编译(compilation)、汇编(assembly)和链接(linking)。
预编译 主要处理那些源代码文件中的以“#”开始的预编译指令。比如“#include”、“#define”等,主要处理规则如下:
● 展开所有宏定义,并且将所有的“#define”删除。
● 处理所有条件预编译指令,比如“#if”、“#ifdef”、“#elif”、“#else”、“#endif”。
● 处理“#include”预编译指令,将被包含的文件插入到该指令的位置。这个过程是递归进行的,也就是说被包含的文件可能还包含其它文件。
● 删除所有注释。
● 添加行号和文件名标识,以便产生调试用的行号信息。
●保留所有#pragma编译器指令。
编译 就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后产生相应的汇编代码文件。这个过程是构建整个程序的核心部分,也是最复杂的部分。
现代的编译和链接过程也并非想象中那么复杂。比如我们在程序模块main.c中使用另外一个模块func,c中的函数foo()。我们在main.c模块中每一处调用foo()的时候都必须确切知道foo()的地址,但由于每个模块都是单独编译的,在编译器编译main.c的时候并不知道foo的函数地址,所以它暂时把这些调用foo()的指令的目标地址搁置,等待最后链接 的时候由链接器去将这些指令的目标地址修正。如果没有链接器,我们必须手工把每个调用foo()的指令进行修正,这是相当繁琐的工作。
将地址修正的过程也称“重定位(relocation)”,每个要修正的地方称为一个“重定位入口”。*****************目标文件里有什么*****************
一般C语言的编译后执行语句都编译成机器代码保存在.text段中;已初始化的全局变量和局部静态变量都保存在.data段里;未初始化的全局变量和局部静态变量一般放在.bss段。
程序的指令和数据为何分开存放?
● 当程序被装载后,数据和指令分别被映射到两个虚存区域,这两个虚存区域的权限可以被分别设置成可读写和只读的。
● 对于现代的CPU来说,他们有着极其强大的缓存(cache)体系。指令区和数据区的分离有利于提高程序的局部性。现代CPU的缓存一般都被设计成数据缓存和指令缓存分离,所以程序的指令和数据被分开存放对CPU的缓存命中率提高有好处。
● 最重要的原因是当系统中运行着多个该程序的副本时,它们的指令都是一样的,所以内存中只要保存一份该程序的指令部分。对于其他的只读数据也是一样的,程序中的图标、图片、文本等资源也是属于可共享的。
链接的接口——目标文件中的符号
在链接的过程中,我们将函数和变量统称为符号(symbol);函数名或变量名就是符号名(symbol name)。
每个目标文件都会有一个相应的符号表(symbol table),这个表里记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值(symbol value),对于变量和函数来说,符号值就是它们的地址。将符号表中的所有符号进行分类,可得:
● 定义在本目标文件中的全局符号,可以被其他目标文件引用。比如其中定义的一些函数和全局变量。
● 在本目标文件中引用的全局符号,却并没有定义在本目标文件,这一般称为外部符号(external symbol)。最典型的就是Hello World中的printf。
● 段名,由编译器产生,它的值就是该段的起始地址。
● 局部符号,这类符号只在编译单元内部可见,如函数中的局部变量。这些局部符号对于链接过程没有作用,链接器往往忽略它们。
● 行号信息。
对于我们而言,最值得关注的就是全局符号,即上面的第一和第二类。因为链接过程只关心全局符号的相互粘合。
符号修饰与函数签名
很久以前,编译器编译源代码产生目标文件时,符号名与相应的变量或函数的名字是一样的。比如一个汇编源代码里面包含一个函数foo,那么汇编器将它编译成目标文件以后,foo在目标文件中相对应的符号名也是foo。当C语言出现时,已经存在很多用汇编语言编写的库和目标文件。如果一个C程序要使用这些库的话,则该C程序就不可以使用这些库中定义的函数和变量的名字作为符号名,否则将产生冲突。
为了防止类似的符号名冲突,UNIX下的C语言规定,C语言源代码文件中的所有全局变量和函数经过编译后,相应的符号名前加上下划线“_”。而Fortran语言的源代码经过编译后,所有的符号名前加上下划线,后面也加上下划线。
这种方法减少了多种语言目标文件之间冲突的概率,但同一种语言编写的目标文件还有可能会产生符号冲突,当程序很庞大时,不同的模块由多个部门开发,它们之间的命名规范若不严格,则有可能导致冲突。于是像C++这样的后来者考虑了这个问题,增加了命名空间(namespace)来解决符号冲突。
复杂的C++拥有类、继承、虚机制、重载、命名空间等特性,这使得符号管理更为复杂。
C++允许函数重载,C++还在语言级别支持命名空间。由此我们引入了“函数签名(Function Signature)”,函数签名包含了一个函数的信息,包括函数名、参数类型、函数所在类和命名空间及其他信息。具体的函数签名方法视不同的编译器而定,VISUAL C++编译器是这么做的:
int func(int) >>>>签名后>>>> ?func@@YAHH@Z
float func(float) >>>>签名后>>>> ?func@@YAMM@Z
int C::func(int) >>>>签名后>>>> ?func@C@@AAEHH@Z
int C::C2::func(int) >>>>签名后>>>> ?func@C2@C@@AAEHH@Z
int N::func(int) >>>>签名后>>>> ?func@N@@YAHH@Z
int N::C::func(int) >>>>签名后>>>> ?func@C@N@@AAEHH@Z
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!