Linux基础开发工具(五):理解链接与库——从原理到实战
目录
- 前言
- 一、C语言程序的翻译全过程
- 1.1 翻译的四个阶段详解
- 二、为什么 C/C++ 必须经过汇编?
- 2.1 早期编程:开关与打孔纸带
- 2.2 CPU 指令集与汇编语言的诞生
- 2.3 历史的逆向路径
- 三、核心概念:编译器与语言的“自举”
- 3.1 第一个编译器是怎么来的?
- 3.2 GCC 是用什么语言写的?
- 三、理解链接与库:程序员的“共享经济”
- 3.1 常规编译习惯与 GCC 的本质
- 3.1.1 从源码到可执行程序的流水线
- 3.1.2 链接的必要性
- 3.2 为什么要有库?——程序员之间的“浪漫协作”
- 3.3 库在哪里?——动态库与静态库的区别
- 3.3.1 查看系统库文件
- 3.3.2 感性理解:动态链接 vs 静态链接
- 3.3.2.1 动态链接(共享经济模式)
- 3.3.2.2 静态链接(私有财产模式)
- 3.4 实战演示:默认动态链接与强制静态链接
- 3.4.1 准备测试代码
- 3.4.2 默认情况:动态链接
- 3.4.3 强制静态链接
- 3.4.4 C++ 的情况
- 3.5 避坑指南:静态链接报错怎么办?
- 结语
前言
大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~
一、C语言程序的翻译全过程
我们在前面的文章提到了,程序的翻译过程主要分为四个步骤:预处理、编译、汇编、链接。这个过程就像是把一段“不干净”的 C 语言代码,一步步打磨成机器能直接运行的二进制文件。
1.1 翻译的四个阶段详解
简单回顾一下,不然感觉直接讲生态有些奇怪
- 预处理:这一步主要是把“不干净”的 C 语言变成“干净”的 C 语言。比如处理宏定义、头文件包含等,让代码变得纯粹。
- 编译:把我们对应的 C 语言翻译成汇编语言。这是从高级语言向低级语言跨越的关键一步。
- 汇编:把汇编语言编译成我们对应的二进制文件。这个文件叫做可重定位目标二进制文件,也简称为目标文件(后缀通常是
.o)。 - 链接:最后将目标文件与库文件链接,生成最终的可执行程序。
所以,整个流程就是:C语言->预处理->干净的C语言->编译->汇编语言->汇编->可重定位目标二进制文件(.o)->链接->最终的二进制可执行文件。
二、为什么 C/C++ 必须经过汇编?
大家平时在用 VsCode 或者 Visual Studio 写代码的时候可能会发现, C 或者 C++ 在翻译的时候,必须是先翻译成汇编,然后再变成二进制,它能不能直接从 C 语言跳到二进制?
要回答这个问题,我们需要聊聊编程语言和计算机发展的历史。这不仅仅是个历史故事,它还对“自举”这个核心概念的理解。
2.1 早期编程:开关与打孔纸带
大家可能听说过,在早期计算机刚出来的时候,科学家控制计算机的方式非常原始——靠拨动一大堆物理开关。通过打开或闭合开关,形成一段二进制信息传给计算机。
无论计算机发展到今天多么先进,有一个点永远没变:CPU 只认识二进制指令。
后来觉得开关太累了,就出现了打孔纸带。就像电影《三体》里叶文洁用的那种设备,通过纸带上有孔和无孔来表示 0 和 1。这本质上还是二进制,只是存储介质变了。
2.2 CPU 指令集与汇编语言的诞生
既然 CPU 只认二进制,那程序员就得记那一堆0101的代码。CPU 生产时在内部通过光刻机固化了硬件级的指令集,比如加法、减法、入栈、出栈等操作,都有对应的二进制编码。
但这太难记了,也太容易写错。于是,人们想了个办法:用英文助记符来代替这些二进制码。比如用add代表加法指令,用sub代表减法。这就是汇编语言的由来。
- 本质:汇编语言是对 CPU 指令集的人类可读封装。
- 工具:汇编语言需要通过汇编器(Assembler)翻译回二进制,才能被 CPU 执行。
虽然汇编比二进制好写多了,但它依然很繁琐,而且跟硬件绑定太死。为了解放生产力,更高级的语言(如 C 语言)应运而生。
2.3 历史的逆向路径
既然 C 语言是后来才有的,而汇编器和二进制转换技术早就成熟了,那么最顺理成章的设计思路就是:
利用现有的成熟工具(汇编器),来解决新问题(C 语言转二进制)。
如果直接让 C 语言编译器去生成二进制,难度极大且没必要。不如先把 C 语言翻译成汇编,再交给成熟的汇编器去处理。这就是为什么我们今天看到的编译流程是“逆历史”的:先转汇编,再由汇编器生成二进制。
三、核心概念:编译器与语言的“自举”
这里我们要引出一个非常重要的知识点,也是面试中经常会被问到的问题:既然C的编译器是用C写的,那么到底是先有语言,还是先有编译器?
答案其实藏在**“自举”**(Bootstrapping)这个过程中。
3.1 第一个编译器是怎么来的?
这就好比“鸡生蛋,蛋生鸡”的问题。我们来推演一下 C 语言和 GCC 编译器的诞生过程:
- 第一步(无中生有):最开始没有 C 语言编译器。科学家们只能用汇编语言,甚至直接用二进制,硬生生写出了第一代 C 语言编译器。
- 最早是用二进制写了第一版的汇编编译器,有了汇编编译器后,再用汇编写了第一版的 C 编译器。
- 第二步(自我复制):有了第一代编译器(它是用汇编写的,但能看懂 C 语言),我们就可以用 C 语言去写一个功能更强大的第二代编译器。
- 第三步(编译升级):用第一代编译器,把第二代编译器的 C 语言源代码编译成可执行程序。
- 第四步(迭代发展):从此以后,C 语言和 C 语言写的编译器就形成了“双螺旋式”的发展,就像左脚踩右脚。C 语言出了新特性,就用旧版本的编译器去编译支持新特性的新版本编译器。
这个过程,我们就叫做编译器的自举。
3.2 GCC 是用什么语言写的?
基于上面的原理,我们可以得出一个结论:GCC 本身就是用 C 语言写的。
如果你不信,我们可以现场验证一下。在 Linux 终端中,我们可以通过以下命令来看看 GCC 到底依赖什么库,从而推断它的“血统”。
[yunze@iZuf6bvbodyqq8qxjtf7l6Z lesson10]$whichgcc /usr/bin/gcc[yunze@iZuf6bvbodyqq8qxjtf7l6Z lesson10]$ ldd /usr/bin/gcc linux-vdso.so.1=>(0x00007ffe80fb9000)libm.so.6=>/lib64/libm.so.6(0x00007f8c9c896000)libc.so.6=>/lib64/libc.so.6(0x00007f8c9c4c8000)/lib64/ld-linux-x86-64.so.2(0x00007f8c9cb98000)通过ldd命令查看/usr/bin/gcc的动态链接库,我们可以看到它主要依赖libc(C 标准库)和libm(数学库)。这有力地证明了 GCC 是一个标准的 C 语言程序。
所以,最终的结论是:先有语言的设计,才有编译器的实现;而编译器的完善,又反过来推动了语言的发展。这就是编译器自举的奥秘。
三、理解链接与库:程序员的“共享经济”
接下来进入下一个核心话题——链接。该篇文章我们要初步建立起对链接和库的理解。虽然关于库的制作原理后面会开专题详细讲(现有的铺垫解释不清楚),但目前必须先从理论上把“为什么要用库”以及“库到底是什么”交代清楚。
3.1 常规编译习惯与 GCC 的本质
3.1.1 从源码到可执行程序的流水线
在常规的 Linux 开发中,比较好的编译习惯是:永远把你的所有源文件(.c)统一经过编译器编译变成目标文件(.o),最后再把所有的.o经过链接,形成对应的可执行程序。
在这个过程中,虽然我们口头上常说“编译器”,但实际上现代编译器(如gcc)已经发展成了一个庞大的工具集。它内部包含了预处理器、编译器、汇编器甚至链接器的能力。所以当我们说“编译”时,其实是指调用这个工具集完成了一系列工作。
3.1.2 链接的必要性
当我们把程序翻译成.o文件之后,我们的工作还没结束。这些.o文件必须和一块叫作**“库”**的东西进行链接,才能最终形成可执行程序。这就引出了我们今天要讨论的核心主角——库。
3.2 为什么要有库?——程序员之间的“浪漫协作”
在平时写代码中,比如说你写了个 Hello World,代码里就几行,是感觉不到库的存在的
其实,当你写下printf("Version1: 免费\n");或者std::cout << "hello yunze"时,你并没有自己实现打印功能的底层逻辑。库的本质,就是由顶尖程序员实现并共享的基础功能集合。
我们可以把库理解为一种“程序员之间的浪漫协作方式”,或者说是技术界的“巨人的肩膀”。如果没有库,每个程序员都要重复造轮子去写底层的打印、文件操作、数学计算等功能,那将是巨大的人力浪费。有了库,全球开发者可以直接调用这些现成的、高质量的代码,极大地提高了开发效率。
3.3 库在哪里?——动态库与静态库的区别
既然库这么重要,那它们藏在哪里呢?Windows下软件安装时所附带的后缀为lld的文件就是库
在 Linux 系统中,我们可以通过命令来一探究竟。
3.3.1 查看系统库文件
我们可以使用ls /lib64/libc.* -l命令来查看 C 语言标准库的文件:
[whb@bite-alicloud lesson10]$ls/lib64/libc.*-l-rw-r--r--1root root5105516Jun42024/lib64/libc.a -rw-r--r--1root root253Jun42024/lib64/libc.so lrwxrwxrwx1root root12Oct162024/lib64/libc.so.6 ->libc-2.17.so这里我们可以看到两种不同后缀的文件,它们代表了库的两种存在形式:
| 操作系统 | 动态库 (Dynamic Library) | 静态库 (Static Library) |
|---|---|---|
| Windows | .dll | .lib |
| Linux | .so(Shared Object) | .a(Archive) |
- 动态库 (
.so):就像是一个公共的“小蚂蚁电竞馆”,程序运行时去那里找功能。 - 静态库 (
.a):就像是把功能直接打包塞进了你的程序里。
3.3.2 感性理解:动态链接 vs 静态链接
为了让大家更直观地理解这两者的区别,我们用生活中的例子打个比方:
3.3.2.1 动态链接(共享经济模式)
想象一下,小王(我们的程序)要去云泽一中(内存)上学。他的课程表里有“上网”这一项。
- 做法:小王不需要自己买电脑,他只需要知道“东门左100米有个网吧”这个地址。当他需要上网时,他就跑去网吧(动态库)使用那里的电脑。
- 优点:节省资源!如果有一百个学生都要上网,只需要这一个网吧就够了,大家共享。
- 缺点:一旦网吧倒闭(库文件缺失),所有依赖它的学生都无法上网了。
在技术上,动态链接通常是在程序运行时,由加载器查找并运行库程序。
3.3.2.2 静态链接(私有财产模式)
还是小王要去上学,这次他决定不依赖外面的网吧。
- 做法:小王的爸爸(老板/编译器)直接给他买了一台电脑,装进书包里带去学校。无论走到哪,他都用自己的电脑。
- 优点:程序不再依赖任何其他的库,独立性强。
- 缺点:让可执行程序体积变大!如果一百个学生每人都背一台电脑,那就会非常沉,这在计算机里意味着占据更多的内存空间,造成资源浪费。
3.4 实战演示:默认动态链接与强制静态链接
光说不练假把式,我们通过实际的代码和命令来看看这两种链接方式的区别。
3.4.1 准备测试代码
我们编写一个简单的soft.c文件,通过条件编译来模拟版本切换:
#include<stdio.h>voidVersion1(){printf("Version1: 免费\n");}voidVersion2(){printf("Version2: 收费\n");}// #define FREE 1intmain(){#ifdefFREEVersion1();#elseVersion1();Version2();#endifreturn0;}3.4.2 默认情况:动态链接
在 Linux 下,直接使用gcc编译,默认采用的是动态链接。
[whb@bite-alicloud lesson10]$ gcc soft.c-osoft[whb@bite-alicloud lesson10]$ ll -rwxrwxr-x1whb whb8424Nov1920:58 soft可以看到生成的soft文件只有8424 字节。我们可以用file命令确认它是动态链接的:
[whb@bite-alicloud lesson10]$filesoft soft: ELF64-bit LSB executable, x86-64, version1(SYSV), dynamically linked(uses shared libs)...使用ldd命令可以查看它依赖了哪些动态库:
[whb@bite-alicloud lesson10]$ ldd soft linux-vdso.so.1=>(0x00007fffefb923000)libc.so.6=>/lib64/libc.so.6(0x00007f8205584000)/lib64/ld-linux-x86-64.so.2(0x00007f8205952000)3.4.3 强制静态链接
如果我们想让它变成静态链接,需要在编译时加上-static参数:
[whb@bite-alicloud lesson10]$ gcc soft.c-osoft-s-static[whb@bite-alicloud lesson10]$ ll... -rwxrwxr-x1whb whb861352Nov1921:01 soft-s注意看文件大小!从原来的 8KB 变成了861KB,体积膨胀了上百倍。这就是因为我们将库的代码全部拷贝了一份塞进了程序里。
再次验证类型:
[whb@bite-alicloud lesson10]$filesoft-s soft-s: ELF64-bit LSB executable, x86-64, version1(GNU/Linux), statically linked...3.4.4 C++ 的情况
对于 C++ 程序(例如soft.cpp),情况也是类似的。C++ 的标准库(STL 等)对应的动态库通常是libstdc++.so。
#include<iostream>intmain(){std::cout<<"hello yunze"<<std::endl;return0;}编译后使用ldd查看,你会发现除了libc.so.6,还多了一个libstdc++.so.6的依赖。
3.5 避坑指南:静态链接报错怎么办?
在实际操作中,你可能会遇到一个坑。当你尝试在 Linux 系统下进行强制静态链接(-static)时,可能会报出如下错误:
/usr/bin/ld: cannotfind-lccollect2: error: ld returned1exitstatus原因分析:
这是因为 Linux 系统默认只安装了动态库(为了节省空间),而没有安装静态库。当你要求编译器去链接静态库(-lc指的是 libc 的静态版)时,系统找不到对应的.a文件,自然就报错了。
解决方案:
你需要手动安装对应的静态库包。以 CentOS/Yum 为例,执行以下命令即可:
yuminstallglibc-static libstdc++-static-y安装完成后,再次执行gcc soft.c -o soft-s -static就能顺利通过了。
