自学内容网 自学内容网

逐梦代码深林:Linux编译之舞,链接之诗——自举、动静态库的浪漫旅程

在这里插入图片描述


问题引入,为什么要进行编译->汇编?

编译过程从高级语言到汇编语言,再到二进制代码,这个逐步转换过程是为了实现代码的高效执行,并降低开发成本:

  1. 编译到汇编再到二进制

    • 高级语言:如C、Java、Python等,为了让程序员方便编写和理解,设计了接近人类自然语言的语法结构。但是,高级语言的代码对于计算机硬件来说是无法直接执行的。
    • 汇编语言:是介于高级语言和机器语言(即二进制)之间的低级语言,与具体的计算机架构紧密相关,直接对应CPU的指令集。编译器先将高级语言翻译成汇编语言,有助于生成有效率、紧凑的二进制代码,减少硬件操作的复杂性。
    • 二进制代码(机器码):是最终的执行代码,由0和1组成,能够直接在硬件上运行。将汇编进一步转换为二进制,得到直接操作内存和处理器的指令,最终提升执行效率。
  2. 减少语言开发成本

    • 从高级语言编译到二进制的分步流程可以简化编译器开发过程。编译器可以分阶段生成特定平台适配的代码,使用现有的汇编器和链接器生成二进制文件,从而减少重写不同平台的工作量。
    • 这种分步式编译还利于优化不同层级代码,适应多种硬件架构,节省了开发和测试成本。
  3. 编译器自举(Compiler Bootstrapping)

    • 定义:编译器自举是指编译器使用自身的编译器来编译其源代码,从而实现自我重构和优化。比如一个C编译器用C语言写成,借助现有的低版本或跨平台编译器来生成二进制。
    • 优点:自举方法在不依赖其他语言编译器的情况下,提供了代码优化、可移植性等好处。例如,GCC(GNU编译器)就是自举的一个经典案例,这种设计降低了对外部工具的依赖,有助于稳定和高效地更新和发布新版本。

因此,从高级语言到汇编再到二进制的逐步编译过程,可以帮助实现跨平台兼容,同时为编译器开发和维护节约成本,而自举技术则推动了编译器的持续演进和优化。


一、详细解释编译器自举

编译器自举(Compiler Bootstrapping)是一种编译器的自我构建方法,让编译器能够用自身的编译器来编译。这个过程之所以重要,是因为它大大提高了编译器开发的灵活性和效率,摆脱了早期二进制编程的繁琐局限。以下是编译器自举从最早的二进制设计到现代编程语言的发展过程,以及自举在编译器设计中的作用:

在这里插入图片描述

1. 从最初的二进制编程到汇编

最早期的计算机程序是用二进制编码直接在硬件上运行的,二进制代码完全由0和1构成。程序员要直接使用二进制编码指令,这不仅容易出错,而且非常不便。为了简化编程,发明了汇编语言——一种更接近人类可理解的低级语言,其中的指令符号化,可以转化成硬件指令。汇编语言不仅直观,还提供了一种“符号化”的方式,使程序开发不再局限于繁琐的二进制输入。

2. 第一代汇编编译器的诞生

尽管有了汇编语言,要让计算机理解并执行汇编代码,还需要一个编译器(也称汇编器)来将汇编代码转换成二进制。最早的汇编器实际上是由直接用二进制编程完成的,这样的汇编器成为第一代汇编编译器。第一代汇编器的诞生意味着以后可以用汇编语言来编写代码,并通过汇编器转换成机器可执行的二进制,这大大简化了代码开发过程。

3. 编译器自举的出现:从汇编到更高级的编译器

由于第一代汇编器是用二进制编程完成的,维护和升级十分困难。于是,为了摆脱对二进制编程的依赖,人们开始用汇编语言写更复杂、更高效的第二代汇编器。这个过程就是编译器的自举:通过编写并运行一个能理解自己语言的编译器,实现自我升级。具体步骤如下:

  • **用第一代汇编编译器(基于二进制)**编写并编译一个更强大的汇编编译器。
  • 使用新生成的汇编编译器,再次编译汇编语言的代码,完成自举。

通过这种方式,编译器的开发不再依赖二进制编码,而可以完全使用汇编语言。汇编器自举完成后,开发者只需编写汇编代码即可,不再依赖直接的二进制编程,极大地降低了开发难度。

4. 自举的延续:从汇编到高级编程语言

随着计算机发展,汇编语言逐渐不能满足更高层次的编程需求,高级语言如C、C++被设计出来。与汇编编译器类似,最早的C编译器是用汇编语言编写的,这样也允许我们用高级语言开发程序。此时,C编译器的自举过程如下:

  • 用汇编编写最初的C编译器,将其用于生成C语言的第一代二进制代码。
  • 之后,编写一个能用C编写和编译自身的C编译器,并通过现有的C编译器编译出一个更高效的C编译器版本,从而完成自举。

最终,通过多次自举和优化,C编译器可以完全用C语言编写并编译,从而形成了稳定的C/C++开发环境,彻底摆脱了对汇编的依赖。

5. 为什么要进行编译器自举?

编译器自举的主要原因是:

  • 降低开发复杂度:自举过程避免了低级编程的麻烦,使开发者可以专注于更高层次的语言优化和功能。
  • 提高移植性和扩展性:自举让编译器只需用自身语言实现,而不依赖于底层硬件,可以移植到不同平台。
  • 增强稳定性:自举过程多次验证编译器的稳定性和正确性,确保编译器能更高效地自我迭代和更新。

通过自举,从最早的汇编到高级语言(如C/C++),逐步形成了现代编程环境。


二、快速了解动静态库——以及操作

动态库与静态库的概念:

  • 静态库(Static Library):静态库在编译时被直接嵌入到目标程序的可执行文件中,因此当程序运行时不需要额外的库文件。使用静态库会增加可执行文件的体积,因为库的代码被复制到每个使用它的程序中。

  • 动态库(Dynamic Library):动态库(在Linux下也称为共享库)在程序运行时才加载到内存中,多个程序可以共享同一个动态库。这种库不会增加可执行文件的体积,减少了存储和内存的占用。

Linux和Windows下的动态、静态库文件前后缀

  • Linux

    • 动态库文件前后缀为 libXXX.so(即“shared object”),例如 libm.so
    • 静态库文件后缀为 libXXX.a,例如 libm.a
  • Windows

    • 动态库文件后缀为 .dll(即“dynamic-link library”),例如 math.dll
    • 静态库文件后缀为 .lib,例如 math.lib

Linux环境下动态库的操作与查看方法:

  1. ldd命令:用于显示可执行文件或动态库所依赖的动态库。可以使用如下命令查看某个程序的动态库依赖关系:

    ldd <可执行文件或动态库>
    

    在这里插入图片描述

在命令 ldd mytest 的输出中,我们可以看到 mytest 可执行文件所依赖的动态库。每行代表一个动态库的依赖信息,下面逐一解释:

  1. linux-vdso.so.1 => (0x00007ffc5630c000)
  • linux-vdso.so.1:这是 VDSO(Virtual Dynamic Shared Object),即虚拟动态共享对象。VDSO 是 Linux 内核提供的一个特殊共享对象,允许用户空间程序在不进行系统调用的情况下访问一些特定的内核功能,从而减少上下文切换,提高性能。
  • 0x00007ffc5630c000:这是 linux-vdso.so.1 在内存中的加载地址,表示该对象在程序运行时被加载到了内存的这个位置。
  1. libc.so.6 => /lib64/libc.so.6 (0x00007ff4c597a000) 我们主要看这个
  • libc.so.6:这是 GNU C标准库,简称 libc,提供了 C 语言的标准库函数和一些系统调用的接口,例如输入输出、字符串操作、内存管理等。它是几乎所有 Linux 应用程序都依赖的重要基础库。
  • => /lib64/libc.so.6libc.so.6 实际被加载的路径,在这里是 /lib64/libc.so.6,说明 ldd 找到的 libc.so.6 位于系统的 /lib64 目录下。
  • (0x00007ff4c597a000):这是 libc.so.6 在内存中的加载地址,即在运行 mytest 程序时,libc.so.6 被加载到内存的 0x00007ff4c597a000 地址处。
  1. /lib64/ld-linux-x86-64.so.2 (0x00007ff4c5d48000)
  • /lib64/ld-linux-x86-64.so.2:这是 动态链接器(Dynamic Linker/Loader),也叫做动态加载器,负责在程序启动时加载并链接所需的共享库。它会根据程序和库的依赖关系加载所有必要的共享库,并把它们映射到程序的内存空间中。
  • (0x00007ff4c5d48000):这是动态链接器在内存中的加载地址,表示 ld-linux-x86-64.so.2 被加载到内存的 0x00007ff4c5d48000 地址位置。

  1. file命令:用于查看文件类型,能够判断某个文件是否是动态库或静态库。可以通过以下命令来查看文件类型:

    file <文件>
    

在这里插入图片描述

输出信息中将包含“shared”,说明它是一个动态库。


  1. ll命令:在Linux系统中可以使用 ll 来查看可执行文件所依赖的C标准库。可以先用 ldd 查看依赖关系。

在这里插入图片描述


Linux环境下依赖静态库的方法:

GCC 默认进行动态链接,因为它会使用共享库(如 .so 文件)来生成可执行文件,减少可执行文件的大小并共享内存中的库。要进行静态链接,你需要使用静态库(.a 文件)来构建可执行文件。

如果你没有拷贝静态库,但希望强制 GCC 进行静态链接,可以使用以下方法:

1. 使用 -static 选项进行静态链接
在编译时,你可以通过添加 -static 选项来告诉 GCC 强制进行静态链接,这样即使系统中存在动态库(.so 文件),GCC 会忽略它们,尝试使用静态库(.a 文件)进行链接。

命令如下:

gcc XXX.c -o XXX -static

在这里插入图片描述

这条命令会强制 GCC 使用静态链接创建 myprogram 可执行文件,所有库都会被静态链接到可执行文件中。

2. 创建静态库
有时,如果你没有显式提供静态库,GCC 仍然会默认使用动态库链接。你可以显式指定使用静态版本的标准 C 库(libc.a),具体方法如下:

sudo yum install glibc-static libstdc++-stdtic -y

在这里插入图片描述

glibc-static对应c语言的,libstdc+±stdtic对于c++的。


总结

  • 使用 -static 强制进行静态链接。
  • 如果没有静态库,首先需要安装相应的静态库(例如 libc.a
  • 静态链接会将所有库包含进可执行文件,导致文件体积增大,但可以使得可执行文件独立于外部库。

三、❤️❤️生动小故事解释动静态库与动静态链接——理论❤️❤️

1. 概念详解

在实际开发中,通常会将代码分散在多个源文件中,每个源文件可能依赖其他源文件的函数或数据。但每个源文件是独立编译的,这意味着每个 .c 文件会生成一个对应的目标文件 .o。为了让这些文件之间的依赖关系得以实现,需要通过链接将生成的目标文件整合为一个可执行程序。这个过程可以分为静态链接动态链接

静态链接
静态链接是在编译阶段将所有依赖的库直接嵌入到可执行文件中。这样生成的可执行文件包含了程序运行所需的所有库代码,运行时不需要依赖外部库文件。静态链接的主要缺点包括:

  • 空间浪费:每个可执行文件都独立包含所有依赖的库代码,因此多个程序如果都依赖某个库(例如 printf 函数),那么每个可执行文件中都会有 printf.o 的副本,导致内存中存在多个相同库代码的副本。
  • 更新困难:当库中的代码更新时,所有依赖该库的可执行文件都需要重新编译和链接。

尽管有这些缺点,静态链接的优点在于,生成的可执行文件独立性高,运行速度快,因为程序启动时无需加载外部库。

动态链接
动态链接在运行时将库文件加载到程序中,从而生成完整的可执行程序。与静态链接不同,动态链接在编译时不将库代码嵌入可执行文件,而是在程序运行时通过系统的动态链接器加载所需的库文件。这种方式的优势在于:

  • 节省空间:多个程序可以共享同一个库文件,在内存中只需加载一次,节省系统资源。
  • 便于更新:库更新时,无需重新编译依赖该库的可执行文件。

动态链接已广泛应用。例如,在 Linux 下查看一个 hello 可执行程序的依赖库,可以看到 hello 依赖于 libc.so.6(C 标准库的动态版本):

$ ldd hello
linux-vdso.so.1 =>  (0x00007fffeb1ab000)
libc.so.6 => /lib64/libc.so.6 (0x00007ff776af5000)
/lib64/ld-linux-x86-64.so.2 (0x00007ff776ec3000)

在这里,ldd 命令用于打印程序的共享库依赖列表。


2.🥰🥰 故事引入🥰🥰

我是张三,一个即将进入高中的学生,面临着一个封闭式管理的学校。学校有严格的规定,不允许带电子产品进校园,尤其是手机和电脑。但我有一个秘密爱好——玩游戏。对于我来说,游戏不仅仅是娱乐,它能帮助我放松心情,甚至让我在学习上更有效率。

有一天,一位学长发现了我的困境,他知道我对游戏的热爱,于是悄悄告诉我一个重要的秘密:“如果你想在学校放松一下,学校东门出去直走100米,左转150米,就有一家网吧,叫‘蚂蚁网吧’。”我心里一惊,记下了这条信息,心想,周末一定能派上用场。

经过一周紧张的学习,终于迎来了周日。老师布置了不少作业,我决定先把语文和数学作业写完,吃完午饭后去网吧玩一会儿游戏放松一下,然后再完成化学和物理作业。于是,我按照计划一步步进行。

中午吃完饭后,我迫不及待地依照学长告诉的路线,找到那家网吧。进门后,我打开了一台电脑,开始了我的上网计划。恰巧,我还在网吧遇到了我的舍友和好朋友,他们也在打游戏。我们一起享受了短暂的游戏时光。

玩完游戏后,我回到宿舍,继续按计划写作业。这个过程中,我就像一个可执行程序,而学长就好像是编译器和链接器。学长告诉我网吧的位置,这就像编译器在编译过程中加载了程序所需的外部信息。当我在执行程序时,遇到需要“上网”的步骤,而我自己并没有实现这个功能时,我就像调用了一个库函数,进入网吧,完成了“上网”这个功能。

网吧是一个典型的共享库,在这里,我不仅可以自己上网,其他人也可以使用这个资源。完成上网后,我就回到宿舍,继续执行剩下的任务,这就是动态库和动态链接的过程。

然而,好景不长,网吧被人举报了。突然有一天,网吧的老板关了门,帽子叔叔把它封了。我再也不能去网吧玩游戏了。于是,我把这个问题告诉了父亲,父亲决定解决这个问题,他去找到了网吧的老板,买下了一台我常用的电脑,把它搬到了宿舍里。这样,我就不再需要依赖网吧了,随时可以在宿舍里玩游戏了。

这就像是把“动态库”转换成了“静态库”。从此,我不再需要依赖外部资源,直接在宿舍里就能执行游戏程序,所有的游戏功能都已经在我的计算机里实现。这就是静态链接的过程:库函数被嵌入到程序内部,程序不再需要外部的支持。

在这里插入图片描述


优缺点

总结优缺点:

  • 静态链接的优点是:程序运行时不依赖外部库,所有功能都已经嵌入到程序中,因此程序独立性强,不会受外部环境的影响。

    • 缺点:会浪费大量空间。每个程序都包含了所有需要的库函数,这不仅占用了磁盘空间,还浪费了内存资源。而且,一旦库更新,所有的程序都需要重新编译。
  • 动态链接的优点是:库函数是共享的,不同的程序可以使用同一个库文件,这样节省了磁盘空间、内存以及更新的难度。

    • 缺点:如果动态库丢失或损坏,所有依赖该库的程序都无法运行,程序的可移植性受到影响。

综上,我们就初步完成了对于动静态库的学习,如果对您有帮助的话,求一个一键三连,谢谢大佬!

在这里插入图片描述


原文地址:https://blog.csdn.net/Jdxxwu/article/details/143643512

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!