Linux “ 编译 “实战:动态链接库让代码动起来的核心原理

【投稿赢 iPhone 17】「我的第一个开源项目」故事征集:用代码换C位出道! 10w+人浏览 1.4k人参与

专栏:  🎉《C++》                        

    📌《数据结构》

   💡《C语言》

   🚀《Linux》

前面我们已经讲了如何利用文本编辑器——vim来进行快速编辑,今天我们就来讲讲如何让我们写的代码在Linux系统中运行起来。

目录

一、代码到运行需要做什么?

1.1、工具——gcc/g++编译器

1.2、编译过程

1、预处理

2、编译

3、汇编

4、链接

二、为什么?

2.1、什么是条件编译?

2.2、为什么要先编译成汇编代码?

2.3、编译器自举

三、库:静态库+动态库

3.1、库的分类与理解

1. 静态库:

2. 动态库:

3.2、对比测试

3.3、总结一下


一、代码到运行需要做什么?

1.1、工具——gcc/g++编译器

比较项                         gcc                         g++
定义支持多语言(C、C++ 等),主要支持 C 语言处理。专门针对 C++ 语言的编译器
默认处理的语言类型

按文件后缀判断:

 .c  :按照C 语言规则编译

.cpp / .cxx等 :按 C++ 规则编译(但需手动指定 C++ 标准,如-std=c++11

不管文件后缀是.c还是.cpp,均默认按 C++ 规则编译
链接阶段的行为编译 C++ 代码时,不自动链接 C++ 标准库libstdc++,需手动加-lstdc++选项,否则可能报 “未定义引用” 错误编译 C++ 代码时,自动链接 C++ 标准库(libstdc++,无需手动指定额外选项
预编译阶段的宏定义编译 .c 文件时不定义__cplusplus宏;仅编译 .pp 等 C++ 文件时才定义该宏自动定义__cplusplus宏(用于区分 C/C++ 环境的条件编译)
使用场景优先用于编译纯 C 程序优先用于编译 C++ 程序

总而言之,编译C语言就用gcc;编译C++代码就用g++。

话不多说,我们先写一段简单的C语言代码来编译一下。

如图为代码内容,接下来我们运行一下。

指令:将源文件code.c编译形成可执行程序code

// 先编译
gcc code.c -o code

// 运行
./code

1.2、编译过程

分为:预处理,编译,汇编 和 链接。

接下来,我们以code.c文件为例进行一一介绍。

1、预处理

功能:

1)头文件展开;

2)去注释;

3)宏替换;

4)条件编译。

然后生成一个以 .i 结尾的临时文件。

选项:-E 

测试:我们给code.c加上注释,宏和条件编译等内容,再观察 code.i 文件。

#include<stdio.h>
#define N 5  // ------------------- 宏定义
int main() {
    int i=0;
    for(i=0;i<N;i++) {
        printf("hello Linux!\n");// 打印字符串hello Linux!
    }
    // ------------条件编译--------------
    #ifdef A
        printf("定义了A");
    #else
        printf("未定义A");
    return 0;
}

// 对源文件预处理: -E 选项
gcc -E code.c -o code.i

// 查看预处理文件
vim code.i

2、编译

功能:翻译形成汇编语言,完成语法分析,语义检查和代码优化。

并生成一个以 .s 结尾的临时文件。

选项:-S

测试:对生成的 code.i 文件继续编译生成 code.s 文件。

// 编译
gcc -S code.i -o code.s

// 查看code.s文件
vim code.s

3、汇编

功能:将汇编语言翻译成机器能够识别的二进制代码。

并生成以 .o 结尾的可重定位目标文件(二进制文件),其中有很多库方法(如printf),但没有实现。

选项:-c

测试:将code.s 文件汇编成为 code.o 文件。

// 汇编
gcc -c code.s -o code.o

// 查看code.o文件
vim code.o

4、链接

功能:将目标文件与引用的库进行链接,如printf所在的库。

最终形成以 .exe 结尾的可执行文件。

你 .o 文件中引用的库方法(printf)不是没有实现嘛。

那我就将 .o 文件与对应实现这个库方法的库链接。

你不就可以使用printf了嘛。

那什么是库呢?

  先别问,下面会详细讲

测试:链接code.o文件并执行代码。

// 链接:
gcc code.o -o code.exe

// 运行:
./code.exe

二、为什么?

2.1、什么是条件编译?

说个例子你可能就明白了。

就比如你用同一个软件。

当你是普通用户时。

这不能用那不能用的,非常...。

但当你是VIP用户时。

想用什么功能就用什么功能。

那同一个软件,一套源代码。

怎么就给人区别对待呢?

这就是条件编译所导致的。

当你是普通用户时。

人家就把那些实现一些高级功能的代码不给你执行。

条件编译其实就是:

用预处理指令控制代码片段是否参与编译的机制。

人家通过预处理就把那部分代码给咱普通用户干掉了。

#define VERSION 2  // 版本号:1-基础版,2-高级版
void func() 
{
#if VERSION == 1  // 基础版功能
    std::cout << "基础版:仅支持核心功能" << std::endl;
#elif VERSION == 2  // 高级版功能
    std::cout << "高级版:支持核心功能+扩展功能" << std::endl;
#endif
}

条件编译允许同一套源代码根据不同条件(如平台、编译模式、版本等)生成不同的目标代码,实现了一套源代码适配多种情景。

大家就不难能够猜到一些条件编译的其他应用场景了吧。

•  跨平台适配(Windows/Linux):

如编译代码时自动选择合适的系统。

•  防止头文件被重复包含:

// 文件名:myheader.h
#ifndef MYHEADER_H  // 若MYHEADER_H未定义
#define MYHEADER_H  // 定义该宏,标记已包含

int add(int a, int b) {
    return a + b;
}

#endif  // 结束条件编译块

当第一次#include"myheader.h"时,MYHEADER_H未定义,编译头文件内容并定义宏;

后续再次#include"myheader.h"时,MYHEADER_H已定义,跳过头文件内容,避免重复定义错误。

2.2、为什么要先编译成汇编代码?

我们知道汇编代码是早于C/C++的。

所以在C/C++出现之前。

都是先将汇编代码翻译成机器能够识别的二进制码。

而且CPU只能够识别二进制码。

所以C/C++你也要翻译成二进制码吧。

摆在你面前的有两条路:

A,把C/C++直接翻译成二进制码。

B,先翻译成汇编代码(编译),再由汇编转二进制码(汇编)。

此时很重要的一点就是。

人家汇编代码翻译成二进制码已经发展的很成熟了。

那我们为什么不选择站在巨人的肩膀上。

所以你看到了今天的编译 -> 汇编的过程。

如果选择将C/C++直接转成二进制码。

 踩不完的坑等你呢!

...才选。

2.3、编译器自举

在没有编译器之前。

人们要手写二进制码。

但效率非常低,而且容易出错。

别怕,有大佬呀!

所以编译器就横空出世了。

那什么是编译器的自举呢?

最开始只有汇编代码呀。

你不可能说是用C写个编译器编译汇编吧。

所以大佬就用汇编代码写了一个极简的汇编器。

用汇编编译汇编(将汇编 -> 二进制码)。

此时,汇编器就完成了初步自举。

当c语言出现后。

先用汇编写了一个极简编译器。

用来将c语言编译成汇编。

而汇编转成二进制机器码根本就不需要操心。

这不就有了一个简单的C编译器(c00)。

而我们又可以编写更完整的 C 编译器源代码。

用这个C编译器就可以编译这段源代码。

获得了一个更加完备的C编译器(c01)了。

然后用c01编译自身获得c11。

至此C编译就完成了完全自举。

三、库:静态库+动态库

链接过程中我们讲到:

链接就是将目标文件与其引用的库进行合并形成可执行程序。

从而达到我们想要的效果。

所以简单理解一下就是:

库是一套方法或数据集(包含基本功能接口)。

所以有了库我们就不用自己造轮子了。

想用什么接口到对应的库里面找就行了。

 总算的解放了。

这就大大提升了我们二次开发的效率。

3.1、库的分类与理解

1. 静态库:

静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运 行时也就不再需要库文件了。Linux中静态库文件以(.a)结尾,Windows(.lib)。

也就是,当我们编译链接时。

直接将整个库文件拷贝一份放到我们的目标文件中。

 但无脑!

然后在代码执行到对应库方法时。

传参给库文件中对应的接口。

而一个程序不免要多次调用相同或不同的库方法。

你把相关的库都给我拷贝过来。

重复了不说。

这段代码也太浪费内存了吧。

2. 动态库:

动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时通过引用的库方法的地址到对应的库中去找对应的库方法,这样可以节省系统的开销。Linux(.so),Windows(.dll)。

注意:调用对应的库方法需要包含头文件。

动态库的实质就是:

你代码中用到的库方法。

在链接时我先记录 “需要调用的函数名 / 符号” 以及对应的动态库信息(如库文件名)

当程序运行时操作系统的 “动态链接器”将动态库加载到内存

计算函数在当前进程地址空间中的实际地址

然后将程序中的函数的调用处 “绑定” 到这个地址

同时,你在引用对应的库时。

我让重复的库在内存中只出现一份

而且动态库还可以共享

即多个文件可以同时使用同一个库。

3.2、对比测试

我们的编译器默认都是动态链接

所以一般我们的云服务器,C/C++的静态库并没有安装。

可以采用如下方法安装:

# 指令(Centos)
yum install glibc-static libstdc++-static -y

1、Linux的库

指令:

ls /usr/lib64

2、动态链接

// 创建文件
touch code.c

// 编译形成可执行程序
gcc code.c -o code.exe
 
// 查看code.exe 详情
file code.exe

即编译器默认动态链接。

3、静态链接

gcc code.c -o code.exe -static

3.3、总结一下

1、动态库形成的可执行程序体积一定很小。

2、可执行程序对静态库的依赖度小;动态库依赖度大,不可缺失。

3、静态链接的目标文件,当程序运行,加载到内存时,就会有大量重复的代码。

4、动态链接,比较节省内存和磁盘资源。

评论 71
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值