程序的静态链接(代码片段)

downey-blog downey-blog     2023-03-09     722

关键词:

程序的静态链接

程序的产生

程序是由程序员编写,经过编译链接过程,最终能够在计算机中运行的东西。本质上来说编译链接过程其实就是将由人能看懂的代码段翻译成机器能看懂的代码段,然后指导机器的运行,比起程序在机器中被运行,博主更喜欢程序指导机器运行这样的说法。

编译链接事实上分为4个过程:预编译、编译、汇编、链接,在这里我们笼统地将其分为两个过程:编译和链接,编译包含预编译、编译、汇编。

编译是将程序员写的代码翻译成机器码,即机器能够解析的二进制码,生成二进制目标文件,既然编译过程已经生成了机器能够解析的二进制码,那为何还需要链接过程呢?

在学生时代会经常写一些基于C语言的小程序,那时候习惯于将代码的实现和一些声明同时放在一个文件中,编译过程也很简单。

随着编程水平的提高,自然要尝试写一些更复杂的程序,代码量逐渐增大,这时候就有必要将不同的实现部分放在不同的文件中,第一个是考虑到程序的可读性,再一个就是程序的移植和维护问题。这时候编译器就面临着一个问题:需要编译多个文件以生成一个可执行文件。

在可执行文件中,每个符号(变量和函数)都将对应程序执行时的唯一地址,那么,在分开编译多个文件时,怎么解决不同文件中的符号地址定位问题:

  • 第一种方案可以是这样:将所有程序文件都作为一个整体,在编译前由编译器将所有源文件全部放到一起形成一个文件,将这个文件再进行编译。
  • 第二种方案可以是这样:所有的源文件分离编译,生成二进制目标文件,暂时不给每个文件中的符号确定执行时硬件地址,而是指定一个相应的逻辑偏移地址,最后再统一地再给所有编译时产生的目标文件中的符号分配实际执行时的地址。

如果采用第一种方案,会有什么问题呢?

  • 编译时间上的问题:因为将所有源文件集中到一起,每次改动一点源文件都要全部重新编译,大型程序耗时太多。
  • 在引用第三方代码时,必须以源文件的方式引用,占用大量空间且不利于代码保护。
  • 程序将不支持动态扩展,每次添加功能都需要重新编译。
    ...

毫无疑问,现代编译器采用的是第二种方案,分离式编译的灵活性完美地解决了第一种方案的缺陷所在,而最后为所有目标中符号分配地址的过程就是程序的链接,除此之外,链接将指定程序唯一的入口地址以及一些其它操作。

目标文件的格式

目标文件其实有着十分复杂的格式,内部的结构是以段来作区分,如代码段、数据段、BSS段,为了讲解方便,我们暂且只讨论目标文件中这三个必要的段。

  • 代码段:程序代码

  • 数据段:全局变量,静态变量

  • BSS段:程序中未初始化或者初始化为0的全局变量,特点是只在代码文件中占用一个符号位,节省空间,加载时在内存中展开。

问题的开始

链接的过程既然是将多个目标文件进行组合,那目标文件中的各个段是以什么样的方式进行组合?

众所周知,程序是运行在内存中的,每一个函数每一个变量都会在内存中有相应的地址,在分离式编译中,每个源文件单独编译,生成的二进制目标文件中对符号(函数,变量)地址是怎么确认的?会不会有冲突,如果发生冲突是怎么解决的?

了解编译流程的朋友都知道,在编译时,源文件如果引用了到本文件中没有定义的函数(变量),只要找到了这个函数(变量)的正确声明,编译对它的处理就是记录一个符号,表示这个函数(变量)本文件中无定义,但是编译过程继续,将符号处理的工作抛给链接器,那么链接器又是怎么处理这种符号引用的情况?

链接器的作用

对于链接器而言,主要就是解决上述的问题,链接器提供了三个操作:

  • 空间和地址分配
  • 符号解析
  • 重定位

空间和地址的分配

不妨想一想,如果给你一堆目标文件,里面包含代码段,数据段,BSS段,让你将它组合成一个文件,你将怎么做?

最简单的办法就是叠加,一个文件紧跟着上一个文件存放,再使用一个总体的文件头来记录这些信息,这个文件头就像一个目录,可以索引到所有文件。

这种方式当然是可以实现的,链接出来的程序在特定环境下也是可以运行的,但是回头一想,当二进制文件有很多个时,可执行文件中会存在成百上千个零散的段,程序执行的效率显著降低,而在空间上来说,对单片机而言还好,内存通常以字节或者4字节为单位,而在桌面系统中,例如X86电脑上,内存对其单位是页,一般为4096字节,当某个段仅有一个字节时,也会占用一页的空间,这对空间的浪费不言而喻。

当然,不难想到的另一个方法就是将相似的段进行合并,这个看起来更可行,链接生成的可执行文件就只有三个连续的段,代码段、数据段、BSS段,实际应用中这种链接方式一直被沿用至今,事情看起来好像确实简单了很多,将多个文件的段糅合到一起,生成一个新的总段,至少解决了空间上的问题,地址的分配也好说,段与段之间相互叠加就可以了。

符号解析和重定位

多个目标文件合成一个目标文件时,地址分配是比较好解决的,但是当多个目标文件编译成一个可执行文件,就没那么简单了。

我们来看下面的例子:

有两个源文件:test1.c 和 test2.c,程序代码分别为:
test1.c:

void func1(void)

    printf("hello world1
");

test2.c:

void func2(void)

    printf("hello world2
");

我们将这两个源文件编译成目标文件:

gcc -c test1.c
gcc -c test2.c

分别生成了test1.o和test2.o,我们可以通过linux下的nm指令来查看目标文件中的符号表(nm的用法可以查看我另一篇博客):

nm -n test1.o  

输出:

                 U puts
0000000000000000 T func1  

在查看test2.o中符号表:

nm -n test2.o  

输出

                 U puts
0000000000000000 T func2  

(同时也可以使用objdump -h命令)

简单解释一下,上述符号表就是在编译成二进制文件时函数和变量产生的对应的符号

第一列是地址,第二列是当前目标所在段,第三列是对象。

可以看到,在test1.c中,func1放在test段(即代码段),地址为0;

在test2.c中,func2放在test段(即代码段),地址同样为0;

那么问题来了,两个不同文件的目标函数地址都是0,我们都知道,在同一个程序中,函数在运行时需要在内存中确定唯一地址,如果程序同时引用这两个文件,这明显会产生地址冲突。

这就是分离式编译带来的问题所在,每个源文件都彼此独立编译,在编译时编译器根本不知道这个源文件中的函数及变量将被加载到内存的何处。

那就退而求其次,编译器假设这个源文件中的符号地址就是从某个地址(一般是0)开始,结果是每个目标文件编译出来都是在0地址处进行叠加放置,做完这些工作之后,编译器就事了拂身去,告诉链接器:符号相对的地址偏移我给你算好了,怎么去安排这些符号的实际内存我就不管啦!

链接器只好接下这个烂摊子,收集好所有目标文件之后,开始一个个地为这些文件中的符号分配地址,对于这些符号的重新定址就被称为重定位。

事实上编译器的偷懒行为并非这一个,很明显的,在分离式编译中,假如源文件a需要引用源文件b中的函数func,a中没有此函数的定义,通常就只有两个行为:

  • 编译器在编译a时,发现func没有定义,编译中断

  • 编译器在编译a时,发现func没有定义,但是有个函数声明,在这里做个记号,继续编译

正确的处理方式当然是第二个,如果引用的每个函数都必须在本函数内有定义,那么分离编译的意义也就不存在了。

结果就是,编译器遇到未定义的函数或变量,只要有相应声明,就记录下来,编译完成之后,同样告诉链接器:我把这些只有声明没有定义的函数和变量记录下来了,你帮我去找吧,找不找得到我不管啦!

链接器如果找不到,就报出我们非常熟悉的错误:

    undefined reference to `XXX'

没办法,编译器只好又接下这个烂摊子,对于这些符号的处理被称为符号解析或者说符号决议,主要是在其他文件中找到那些声明而在其他文件中定义的符号,并建立联系。

单片机下的地址重定位

对于单片机而言,程序直接操作物理地址,因为没有MMU(内存管理单元)且有其他资源的限制,不存在多进程的概念,重定位时直接将目标文件中的逻辑地址根据链接脚本的设置转换成物理地址,直接下载到flash中运行。

桌面系统下的地址重定位

在桌面系统中,情况就不一样了,由于MMU的存在,应用程序操作虚拟地址而非真实的物理地址,重定位的过程就是将目标文件中的逻辑地址根据链接脚本的设置转换成虚拟地址,当程序被加载进内存时,MMU动态地将虚拟地址映射到相应的物理内存。

这篇文章旨在建立一个链接过程的概念,链接过程的细节且听下回分解。

好了,关于程序的静态链接 的讨论就到此为止啦,如果朋友们对于这个有什么疑问或者发现有文章中有什么错误,欢迎留言

原创博客,转载请注明出处!

祝各位早日实现项目丛中过,bug不沾身.
(完)

链接器如果找不到,就报出我们非常熟悉的错误:

    undefined reference to `XXX'

没办法,编译器只好又接下这个烂摊子,对于这些符号的处理被称为符号解析或者说符号决议。

单片机下的地址重定位

对于单片机而言,程序直接操作物理地址,因为没有MMU(内存管理单元)且有其他资源的限制,不存在多进程的概念,重定位时直接将目标文件中的逻辑地址根据链接脚本的设置转换成物理地址,直接下载到flash中运行。

桌面系统下的地址重定位

在桌面系统中,情况就不一样了,由于MMU的存在,应用程序操作虚拟地址而非真实的物理地址,重定位的过程就是将目标文件中的逻辑地址根据链接脚本的设置转换成虚拟地址,当程序被加载进内存时,MMU动态地将虚拟地址映射到相应的物理内存。

这篇文章旨在建立一个链接过程的概念,链接过程的细节且听下回分解。

好了,关于程序的静态链接 的讨论就到此为止啦,如果朋友们对于这个有什么疑问或者发现有文章中有什么错误,欢迎留言

原创博客,转载请注明出处!

祝各位早日实现项目丛中过,bug不沾身.
(完)

程序的静态链接(代码片段)

程序的静态链接程序的产生程序是由程序员编写,经过编译链接过程,最终能够在计算机中运行的东西。本质上来说编译链接过程其实就是将由人能看懂的代码段翻译成机器能看懂的代码段,然后指导机器的运行,比起程序在机... 查看详情

静态链接库和动态链接库(代码片段)

...。一种是LIB包含函数代码本身,在编译时直接将代码加入程序当中,称为静态链接库staticlinklibrary。共有两种链接方式:动态链接使用动态链接库,允许可执行模块(.dll文件或.exe文件)仅包 查看详情

静态库和共享库(代码片段)

...源代码编译成库提供。库分为共享库和静态库。静态库在程序的链接阶段被复制到了程序中,和程序运行的时候没有关系。动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用。使用动... 查看详情

gcc编译过程与动态链接库和静态链接库(代码片段)

...是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存... 查看详情

第20课链接过程简介(代码片段)

...译后产生目标文件,这些目标文件如何生成最终的可执行程序呢?链接器: 静态链接: 静态链接就是将库文件或者目标文件直接加入到可执行文件当中。Linux下静态库的创建和使用: 静态库示例程序:20-1.c1#include<... 查看详情

彻底搞懂程序链接过程之动态链接(代码片段)

通过静态链接,可以生成一个可执行文件,这个可执行文件既可以是完全链接的也可以是部分链接的,对于部分链接的可执行文件,有些符号引用需要等到可执行文件加载时甚至是运行时才会进行符号解析和重定... 查看详情

程序员自我修养阅读笔记——动态链接(代码片段)

...动态链接  动态链接,顾名思义,就是只有在程序需要调用对应的库中的实现时才将对应的库的映像文件加载到内存。相比而言,静态链接是在编译阶段就将需要的目标文件中的相关实现连接到可执行文件中。动态... 查看详情

动态链接及其部分实现细节(代码片段)

...f0c;但是相对应的其所占空间大小也很大,且还有在对程序的更新、维护方面也有着问题。动态链接则消除了这方面的问题,即使得空间不再浪费,更新一个程序也变得不再麻烦。浪费内存问题的解决假设有两个程序a 查看详情

动态和静态链接库(代码片段)

...和库文件分别放在/usr/include/和/usr/lib/目录。在某些时候程序需要的库不在这些目录下,此时需要在编译时指定所需的头文件和库文件的路径。函数库分为静态链接库(.a、.lib)和动态 查看详情

liteos动态加载(十三)(代码片段)

1.概述1.1基本概念动态加载是一种程序加载技术。静态链接是在链接阶段将程序各模块文件链接成一个完整的可执行文件,运行时作为整体一次性加载进内存。动态加载允许用户将程序各模块编译成独立的文件而不将它们链接起... 查看详情

动态链接库(dll)(代码片段)

...。它们是包含许多函数的独立文件,这些函数可以被应用程序和其他DLL调用以完成某些特定的工作。一个动态链接库只有在另外一个模块调用其所包含的函数时才被启动。 “静态链接”一般是在程序开发过程中发生的,用于... 查看详情

linux入门动静态库(代码片段)

...链接时,把各个.O文件进行链接,最终形成可执行程序。当一堆.o文件要被大量使用时,我们把这些目标文件进行打包,就形成了一个库。动静态库的各自特征静态库:程序在进行汇编链接时,把库的代码... 查看详情

第1章linux系统编程入门:静态链接库的创建和使用(代码片段)

...一些可以直接拿来用的变量、函数或类。库是特殊的一种程序,编写库的程序和编写一般的程序区别不大,只是库不能单独运行。库文件有两种,静态库和动态库(共享库),区别是:静态库在程序的... 查看详情

动静态链接库(代码片段)

文章目录动静态链接库一、什么是库二、静态库1、静态库的优缺点2、静态库的打包3、静态库的使用三、动态库1、动态库的优缺点1、动态库的打包2、动态库的使用四、库的意义动静态链接库一、什么是库将多个目标文件(.o)打... 查看详情

动态链接及其部分实现细节(代码片段)

...f0c;但是相对应的其所占空间大小也很大,且还有在对程序的更新、维护方面也有着问题。动态链接则消除了这方面的问题,即使得空间不再浪费,更新一个程序也变得不再麻烦。浪费内存问题的解决假设有两个程序a... 查看详情

c存储类链接和内存管理——存储类(代码片段)

...(Scope)以及它的链接(Linkage)变量的作用域和链接一起表明程序的哪些部分可以通过变量名来访问该变量不同的存储类提供了变量的作用域、链接以及存储时期的不同组合。作用域作用域分为:代码块作用域(blockscope 查看详情

程序员自我修养阅读笔记——动态链接(代码片段)

...动态链接  动态链接,顾名思义,就是只有在程序需要调用对应的库中的实现时才将对应的库的映像文件加载到内存。相比而言,静态链接是在编译阶段就将需要的目标文件中的相关实现连接到可执行文件中。动态... 查看详情

ios静态库和framework区别(代码片段)

什么是库库是共享程序代码的方式,一般分为静态库和动态库。静态库与动态库的区别静态库:链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。动态库:链接时不复制,程序运行时由系统... 查看详情