您现在的位置是:网站首页 -> 代码相关 文章内容
C/C++ 彻底了解链接器(一)-itarticl.cc-IT技术类文章记录&分享
发布时间: 9年前【代码相关】 130人已围观【返回】
看看 C 文件中都包含了哪些内容
我们首先要弄清的是声明和定义的区别。定义(definition)是指建立某个名字与该名字的实现之间的关联,这里的“实现”可以是数据,也可以是代码:
变量的定义,使得编译器为这个变量分配一块内存空间,并且还可能为这块内存空间填上特定的值
函数的定义,使得编译器为这个函数产生一段代码
声明是告诉 C 编译器,我们在程序的别处——很可能在别的 C 文件中——以某个名字定义了某些内容(注意:有些时候,定义也被认为是声明,即在定义的同时,也在此处进行了声明)。
对于变量而言,定义可以分为两种:
全局变量:其生命周期存在于整个程序中(即静态范围(static extent)),可以被不同的模块访问
局部变量:生命周期只存在于函数的执行过程中(即局部范围(local extent)),只能在函数内部访问
以下是几个不太直观的特殊情况:
用 static 修饰的局部变量实际上是全局变量,因为虽然它们仅在某个函数中可见,但其生命周期存在于整个程序中
用 static 修饰的全局变量也被认为是全局的,尽管它们只能由它们所在的文件内的函数访问
当我们谈及 “static” 关键字时,值得一提的是,如果某个函数(function)用 static 修饰,则该函数可被调用的范围就变窄了(尤其是在同一个文件中)。
无论定义全局变量还是局部变量,我们可以分辨出一个变量是已初始化的还是未初始化的,分辨方法就是这个变量所占据的内存空间是否预先填上了某个特殊值。
最后要提的一点是:我们可以将数据存于用 malloc 或 new 动态分配的内存中。这部分内存空间没法通过变量名来访问,因此我们使用指针(pointer)来代替——指针也是一种有名字的变量,它用来保存无名动态内存空间的地址。这部分内存空间最终可以通过使用 free 和 delete 来回收,这也是为什么将这部分空间称为“动态区域”(dynamic extent)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | //以下是一个示例程序,也许是一种更简便的记忆方法: // 这是一个未初始化的全局变量的定义 intx_global_uninit; // 这是一个初始化的全局变量的定义 intx_global_init = 1; // 这是一个未初始化的全局变量的定义,尽管该变量只能在当前 C文件中访问 staticinty_global_uninit; // 这是一个初始化的全局变量的定义,尽管该变量只能在当前 C文件中访问 staticinty_global_init = 2; // 这是一个存在于程序别处的某个全局变量的声明 externintz_global; // 这是一个存在于程序别处的某个函数的声明(如果你愿意,你可以在语句前加上 "extern"关键字,但没有这个必要) intfn_a(intx,inty); // 这是一个函数的定义,但由于这个函数前加了 static限定,因此它只能在当前 C文件内使用 staticintfn_b(intx) { returnx +1; } // 这是一个函数的定义,函数参数可以认为是局部变量 intfn_c(intx_local) { // 这是一个未初始化的局部变量的定 inty_local_uninit ; // 这是一个初始化的局部变量的定 inty_local_init = 3 ; // 以下代码通过局部变量、全局变量和函数的名字来使用它 x_global_uninit = fn_a (x_local, x_global_init); y_local_uninit = fn_a (x_local, y_local_init); y_local_uninit += fn_b (z_global); return(x_global_uninit + y_local_uninit); } |
C 编译器都做了些什么
C 编译器的任务是把我们人类通常能够读懂的文本形式的 C 语言文件转化成计算机能明白的内容。我们将编译器输出的文件称为目标文件(object file)。在UNIX平台上,这些目标文件的后缀名通常为.o,在Windows平台上的后缀名为.obj。目标文件本质上包含了以下两项内容:
代码:对应着 C 文件中函数的定义(definitions)
数据:对应着 C 文件中全局变量的定义(definitions)(对于一个已初始化的全局变量,它的初值也存于目标文件中)。
以上两项内容的实例都有相应的名字与之相关联——即定义时,为变量或函数所起的名字。
目标代码(object code)是指将程序员写成的 C 代码——所有的那些if, while, 甚至goto都包括在内——经过适当编码生成对应的机器码序列。所有的这些指令都用于处理某些信息,而这些信息都得有地方存放才行——这就是变量的作用。另外,我们可以在代码中引用另一段代码——说得具体些,就是去调用程序中其它的 C 函数。
无论一段代码在何处使用某个变量或者调用某个函数,编译器都只允许使用已经声明(declaration)过的变量和函数——这样看来,声明其实就是程序员对编译器的承诺:向它确保这个变量或函数已经在程序中的别处定义过了。
链接器的作用则是兑现这一承诺,但反过来考虑,编译器又如何在产生目标文件的过程中兑现这些承诺呢?
大致说来,编译器会留个空白(blank),这个“空白”(我们也称之为“引用”(reference))拥有与之相关联的一个名字,但该名字对应的值还尚未可知。
在熟悉了以上知识后,我们大致可以勾画出上一节示例代码所对应目标文件的样子了:
对象文件原理图
剖析目标文件
目前为止,我们仅仅只从宏观的角度进行讨论,因此,接下来我们很有必要研究一下之前介绍的理论在实际中都是怎么工作的。这里我们需要用到一个很关键的工具,即命令:nm,这是一条UNIX平台上使用的命令,它可以提供目标文件的符号(symbols)信息。在Windows平台上,与其大致等价的是带 /symbols 选项的 dumpbin 命令;当然,你也可以选择安装 Windows 版的 GNU binutils 工具包,其中包含了 nm.exe。
我们来看看运行nm命令后,上文的 C 代码所产生的目标文件是什么结构:
c_parts.o 中的符号如下:
1 2 3 4 5 6 7 8 9 10 | Name Value Class Type Size Line Section fn_a | | U | NOTYPE| | |*UND* z_global | | U | NOTYPE| | |*UND* fn_b |00000000| t | FUNC|00000009| |.text x_global_init |00000000| D | OBJECT|00000004| |.data y_global_uninit |00000000| b | OBJECT|00000004| |.bss x_global_uninit |00000004| C | OBJECT|00000004| |*COM* y_global_init |00000004| d | OBJECT|00000004| |.data fn_c |00000009| T | FUNC|00000055| |.text |
不同平台的输出内容可能会有些许不同(你可以用 man 命令来查看帮助页面,从中获取某个特定版本更多的相关信息),但它们都会提供这两个关键信息:每个符号的类型,以及该符号的大小(如果该符号是有效的)。符号的类型包括以下几种(译者注[1]):
U: 该类型表示未定义的引用(undefined reference),即我们前文所提及的“空白”(blanks)。对于示例中的目标文件,共有两个未定义类型:“fn_a” 和 “z_global”。(有些 nm 的版本还可能包括 section(译注:即宏汇编中的区,后文直接使用section而不另作中文翻译)的名字,section的内容通常为 *UND* 或 UNDEF)
t/T: 该类型指明了代码定义的位置。t 和 T 用于区分该函数是定义在文件内部(t)还是定义在文件外部(T)——例如,用于表明某函数是否声明为 static。同样的,有些系统包括 section ,内容形如.text
d/D: 该类型表明当前变量是一个已初始化的变量,d 指明这是一个局部变量,D 则表示全局变量。如果存在 section ,则内容形如 .data
b/B: 对于非初始化的变量,我们用 b 来表示该变量是静态(static)或是局部的(local),否则,用 B 或 C 来表示。这时 section 的内容可能为.bss 或者 *COM*
我们也很可能会看到一些不属于原始 C 文件的符号,我们可以忽略它们,因为这一般是由编译器“邪恶”的内部机制导致的,这是为了让你的程序链接在一起而额外产生的内容。
发布时间: 9年前【代码相关】130人已围观【返回】【回到顶端】
很赞哦! (1)
相关文章
点击排行

站长推荐

猜你喜欢
站点信息
- 建站时间:2016-04-01
- 文章统计:728条
- 文章评论:82条
- QQ群二维码:扫描二维码,互相交流
