首页
社区
课程
招聘
[原创]关于编译,链接及库的一些基础知识
发表于: 2010-5-29 11:59 7257

[原创]关于编译,链接及库的一些基础知识

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

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 7
支持
分享
最新回复 (3)
雪    币: 366
活跃值: (42)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
2
回头找来看看,这本书没看过。
2010-5-29 23:46
0
雪    币: 232
活跃值: (25)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
3
这本书还是不错的....推荐购买....唯一的缺点就是LINUX方面的东西讲得太多了
2010-5-30 09:44
0
雪    币: 270
能力值: (RANK:10 )
在线值:
发帖
回帖
粉丝
4
潘大叔最近开始忙出书了? 那本内核原理如何?
2010-5-31 21:27
0
游客
登录 | 注册 方可回帖
返回
//