您当前的位置:首页 > 计算机 > 编程开发 > C语言

到底什么是链接,它起到了什么作用

时间:12-30来源:作者:点击数:

几十年以前,计算机刚刚诞生,人们编写程序时,将所有的代码都写在同一个源文件中,经过长期的积累,程序包含了数百万行的代码,以至于人们无法维护这个程序了。于是人们开始寻找新的方法,迫切地希望将程序源代码分散到多个文件中,一个文件一个模块,以便更好地阅读和维护,这个时候,链接器就粉墨登场了。

一切都是地址

我们知道,数据是保存在内存中的,对于计算机硬件来说,必须知道它的地址才能使用。变量名、函数名等仅仅是地址的一种助记符,目的是在编程时更加方便地使用数据,当源文件被编译成可执行文件后,这些标识符都不存在了,它们被替换成了数据的地址。

假设变量 a、b、c 的地址分别为 0X1000、0X1004、0X1008,加法运算的机器指令为 1010,赋值运算的机器指令为 1110,那么在C语言中实现加法运算的代码为:

c = a + b;

生成可执行文件后的机器码为:

1010  0X1000  0X1004  //将两个数据相加的值保存在一个临时区域
1110  0X1008  //将临时区域中的数据复制到地址为0X1008的内存中

编译器和链接器的一项重要任务就是将助记符替换成地址。

汇编语言的诞生

任何程序的执行,最终都要依靠计算机硬件来完成。现代计算机硬件都是大规模集成电路,它只认识高低两个电平(电压),高电平一般为 5V,用1表示,低电平一般为 0V,用0表示。也就是说,在计算机底层,没有文字、数字、图像、视频等丰富多彩的可视化元素,只有 0 和 1 两个二进制数字,这就是机器语言

计算机刚刚诞生的时候没有编程语言,人们直接使用机器语言(二进制)编程。现在假设有一种跳转指令,它的二进制形式为 0001,如果需要执行地址为 1010 的代码,那么可以这样写:

0001  1010
所谓跳转,就是在执行当前代码块时转而执行其他的代码块。从本质上讲,C语言中的函数就是一个代码块,当发生函数调用时,就会执行其他的代码块,这个过程就是通过跳转指令来完成的。

那么现在问题来了,程序并不是一写好就永远不变化的,它可能会经常被修改。比如我们在地址 1010 之前插入了其他指令,那么原来的代码就得往后移动,上面的跳转指令的跳转地址也得相应地调整。

在这个过程中,程序员需要人工重新计算每个子程序或者跳转的目标地址,这种重新计算各个目标地址的过程叫做重定位(Relocation)每次程序修改时,这些位置都要重新计算,十分繁琐又耗时,并且很容易出错。

如果程序包含了多个源文件,就很可能会有跨文件的跳转,这种人工重定位的方式在程序拥有多个模块时会导致更加严重的问题。

没办法,这种黑暗的程序员生活是没办法容忍的,于是先驱们发明了汇编语言(Assembly),这相比机器语言来说是个很大的进步。

汇编语言使用接近人类的各种符号和标记来帮助记忆,比如用jmp表示跳转指令,用func表示一个子程序(C语言中的函数就是一个子程序)的起始地址,这种符号的方法使得人们从具体的机器指令和二进制地址中解放出来。

将上面的机器指令使用汇编代码来书写:

jmp func

这样,不管在 func 之前增加或者减少了多少条指令导致 func 的地址发生了变化,汇编器在每次汇编程序的时候会重新计算 func 这个符号的地址,然后把所有使用到 func 的地方修正为新的地址,整个过程不需要人工参与。对于一个有成千上百个类似的符号的程序,人们终于摆脱了这种低级的繁琐的计算地址的工作,用一句政治口号来说就是“极大地解放了生产力”。

符号(Symbol)这个概念随着汇编语言的普及被广泛接受,它用来表示一个地址,这个地址可能是一段子程序(后来发展为函数)的起始地址,也可以是一个变量的地址。

C语言的诞生

汇编语言的主要作用是为机器指令提供了助记符,大部分汇编代码和机器指令是一一对应的,这在汇编被发明的初期确实令程序员非常欣喜。

后来随着软件规模的日渐庞大,代码量开始疯长,汇编语言的缺点逐渐暴露出来。汇编虽然提供了多种符号,但它依然非常接近计算机硬件,程序员要考虑很多细节问题和边界问题,并且不利于模块化开发,所以后来人们发明了C语言。

C语言是比汇编更加高级的编程语言,极大地提高了开发效率,以加法为例,C语言只需要一条语句,汇编却需要四五条。

模块化开发

现代软件的规模往往都很大,动辄数百万行代码,程序员需要把它们分散到成百上千个模块中。这些模块之间相互依赖又相互独立,原则上每个模块都可以单独开发、编译、测试,改变一个模块中的代码不需要编译整个程序。

在C语言中,一个模块可以认为是一个源文件(.c 文件)。

在程序被分隔成多个模块后,需要解决的一个重要问题是如何将这些模块组合成一个单一的可执行程序。在C语言中,模块之间的依赖关系主要有两种:一种是模块间的函数调用,另外一种是模块间的变量访问。

函数调用需要知道函数的首地址,变量访问需要知道变量的地址,所以这两种方式可以归结为一种,那就是模块间的符号引用。

模块间依靠符号来“通信”类似于拼图版,定义符号的模块多出一个区域,引用符号的模块刚好少了那一块区域,两者刚好完美组合。如下图所示:

这种通过符号将多个模块拼接为一个独立的程序的过程就叫做链接(Linking)

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
    无相关信息
栏目更新
栏目热门