首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【Linux系统】代码星辰里的积木与流萤:动静态库的编程诗篇

【Linux系统】代码星辰里的积木与流萤:动静态库的编程诗篇

作者头像
suye
发布2025-06-01 08:33:41
发布2025-06-01 08:33:41
14700
代码可运行
举报
文章被收录于专栏:17的博客分享17的博客分享
运行总次数:0
代码可运行

前言

本文将以清晰的逻辑脉络,带领读者从基础概念入手,逐步掌握动静态库的制作、生成、发布、使用及安装全流程。通过具体的代码示例与 Makefile 配置解析,结合编译链接原理与操作系统内存管理机制,深入理解静态库的 “空间换时间” 特性与动态库的 “运行时加载” 优势。同时,结合地址空间与程序执行原理,剖析库函数在内存中的定位与调用机制,帮助读者构建从理论到实践的完整知识体系。


一、静态库

静态库是将多个目标文件(.o 文件)打包成一个单一的库文件(.a 文件),然后使用者可以将这个库与他们的程序链接。主要步骤包括:

1.1 静态库的制作

首先定义一组函数(例如 addsubmuldiv),然后将它们编译成目标文件(.c 文件)。

1.mymath.h

代码语言:javascript
代码运行次数:0
运行
复制
#pragma once

#include <stdio.h>

extern int myerrno ;

int add(int x, int y);
int sub(int x, int y);
int mul(int x, int y);
int div(int x, int y);

2.mymath.c

代码语言:javascript
代码运行次数:0
运行
复制
#include "mymath.h"

int myerrno = 0;

int add(int x, int y)
{
    return x + y;
}

int sub(int x, int y)
{
    return x - y;
}

int mul(int x, int y)
{
    return x * y;
}

int div(int x, int y)
{
    if (y == 0)
    {
        myerrno = 1;
        return -1;
    }
    return x / y;
}
1.2 静态库的生成

使用 gccar 编译工具,先编译 .c 文件为 .o 文件,再通过 ar -rc 命令打包生成 .a 静态库文件。

Makefile

代码语言:javascript
代码运行次数:0
运行
复制
lib = libmymath.a

$(lib): mymath.o
    ar -rc $@ $^

mymath.o: mymath.c
    gcc -c $^

.PHONY: clean
clean:
    rm -f *.o *.a lib

解释:

  1. 变量定义
    • lib = libmymath.a:定义了一个名为 lib 的变量,它表示静态库文件的名称。
  2. (lib): mymath.o:构建目标 libmymath.a,依赖于 mymath.o 文件。 ar -rc @ ^:使用 ar 命令创建静态库。@ 代表目标文件 libmymath.a,^ 代表依赖文件 mymath.o。 mymath.o: mymath.c:构建目标 mymath.o,依赖于 mymath.c 源文件。 gcc -c
  3. clean 目标
    • rm -f *.o *.a :删除生成的目标文件。

静态库生成示意图

1.3 静态库的发布

将静态库及其头文件打包发布。发布时的头文件包含了库函数的声明,是供其他程序使用的文档。

在之前的Makefile中加上以下代码就是发布库

代码语言:javascript
代码运行次数:0
运行
复制
.PHONY: output
output:
    mkdir -p lib/include
    mkdir -p lib/mymathlib
    cp *.h lib/include
    cp *.a lib/mymathlib

output 目标

  • 创建目录 lib/includelib/mymathlib,然后将头文件和静态库文件复制到相应的目录中。
1.4 静态库的使用

在其他程序中使用静态库时,必须确保编译器能够找到库的头文件和静态库文件。可以通过添加 -I-L 选项来指定头文件路径和库文件路径,使用 -l 选项指定库的名称。

先创建一个 test 目录,将上面的 lib 目录拷贝到其目录中,再创建一个 main.c 文件进行测试:

代码语言:javascript
代码运行次数:0
运行
复制
#include "mymath.h"

int main()
{
    printf("1+1=%d\n", add(1, 1));
    return 0;
}   

使用 gcc 编译生成可执行程序:

可以发现系统表示没有找到 mymath.h 文件,那么是什么引起的报错呢?

  1. 包含头文件的问题

main.c 中,使用 #include 语句来引用头文件时,编译器的查找顺序会受到影响。具体来说:

  • 使用 <> 时,编译器会在系统指定的目录中查找头文件。
  • 使用 "" 时,编译器首先在当前源文件的同级目录中查找头文件,如果没有找到,才会去系统指定的目录查找。

在代码中,#include "mymath.h" 位于 lib/include 目录下,但由于编译器默认在当前目录查找头文件,因此会报错找不到头文件。

解决方法

  • 方法一:将头文件复制到系统的指定路径下。
  • 方法二:在代码中写明完整路径,例如 #include "/lib/include/mymath.h"
  • 方法三:在编译时使用 -I 选项,指定额外的头文件搜索路径。例:gcc -I ./lib/include main.c .

这里我们选择方法三来解决问题:

可以看到,即使头文件正确找到,链接静态库时也可能出现错误,接下来我们继续解决问题。

  1. 链接静态库的问题

GCC 默认只会在标准路径下查找库文件,因此会找不到自定义的静态库。

解决方法

  • 方法一:将静态库文件(如 libmymath.a)复制到系统默认的库文件路径。(库的安装
  • 方法二:在编译时指定库的路径和库名。使用 -L 来指定库的搜索路径,使用 -l 来指定链接的库。例如: gcc main.c -I /path/to/include -L /path/to/lib -lmymath 这里 -L /path/to/lib 指定了静态库的路径,-lmymath 指定了库的名称。(mymath 的原名叫做 libmymath.a ,不过库名称要去掉前缀 lib 和后缀 .a )

要点

  • 如果系统中只提供静态链接,gcc 则只能对该库进行静态链接。
  • 如果系统中需要链接多个库,则 gcc 可以链接多个库。
1.5 静态库的安装

静态库的安装本质上是将库文件和头文件放到系统的指定目录下。可以通过创建软链接的方式来实现安装,如将库文件链接到 /usr/lib 或者 /lib 目录下,这样系统和其他程序就能识别到这些库。

通过在指定目录下创建软链接的方式,如下图所示:

代码语言:javascript
代码运行次数:0
运行
复制
sudo ln -s /home/xny/linux/lesson23/test/lib/include /usr/include/myinc
代码语言:javascript
代码运行次数:0
运行
复制
sudo ln -s /home/xny/linux/lesson23/test/lib/mymathlib/libmymath.a /lib64/libmymath.a

此时包含头文件前面应该加上软链接的名字,如:#include <myinc/mymath.h> 这种形式。

二、动态库

2.1 动态库的制作与生成
代码语言:javascript
代码运行次数:0
运行
复制
// myprintf.h
#pragma once

#include <stdio.h>

void Print();
代码语言:javascript
代码运行次数:0
运行
复制
// myprintf.c
#include "myprintf.h"

void Print()
{
    printf("Hello Linux\n");
}
代码语言:javascript
代码运行次数:0
运行
复制
// mylog.h
#pragma once

#include <stdio.h>

void Log(const char* info);
代码语言:javascript
代码运行次数:0
运行
复制
// mylog.c
#include "mylog.h"

void Log(const char* info)
{
    printf("log: %s\n", info);
}

编译选项:使用 -fPIC 选项编译源文件,生成位置无关的代码,这对于动态库至关重要。然后,使用 -shared 选项将目标文件 .o 链接成动态库 .so 文件。例如:

代码语言:javascript
代码运行次数:0
运行
复制
gcc -fPIC -c myprintf.c
gcc -fPIC -c mylog.c
gcc -shared -o libmymethod.so *.o
2.2 动态库的发布
  • 生成静态库和动态库:通过 Makefile 来自动化生成静态库和动态库。例如,静态库通过 ar -rc 命令打包,动态库通过 gcc -shared 生成。
  • 发布库文件:使用 cp 命令将头文件、静态库和动态库复制到适当的目录(如 mylib/includemylib/lib)中,以便其他程序可以访问这些文件。
代码语言:javascript
代码运行次数:0
运行
复制
dy-lib = libmymethod.so
static-lib = libmymath.a

.PHONY: all
all: $(dy-lib) $(static-lib)

$(static-lib): mymath.o
    ar -rc $@ $^

mymath.o: mymath.c
    gcc -c $^

$(dy-lib): mylog.o myprint.o
    gcc -shared -o $@ $^

mylog.o: mylog.c
    gcc -fPIC -c $^

myprint.o: myprint.c
    gcc -fPIC -c $^

.PHONY: clean
clean:
    rm -rf *.o *.a *.so mylib

.PHONY: output
output:
    mkdir -p mylib/include
    mkdir -p mylib/lib
    cp *.h mylib/include
    cp *.a mylib/lib
    cp *.so mylib/lib
2.3 动态库的使用

使用动态库:可以将动态库中的函数像使用静态库一样在程序中引用。需要包含相应的头文件,并在编译时指定动态库的路径和库名。例如:

代码语言:javascript
代码运行次数:0
运行
复制
#include "myprintf.h"
#include "mylog.h"

编译时链接动态库:通过 -L 选项指定动态库的位置,使用 -l 选项指定库名。例如:

代码语言:javascript
代码运行次数:0
运行
复制
gcc -L/path/to/lib -lmymethod main.c -o main

运行时问题:虽然在编译时指定了动态库的路径,程序运行时仍然可能找不到库。这是因为动态库需要通过加载器来加载。解决方法有以下几种:

将库文件拷贝到系统默认路径(如 /lib64/usr/lib64)。

在系统默认路径下建立软链接

  • 这种及其容易出现问题,例如:

因为 我们仅仅是告诉了编译器所需的动态库在哪里,而可执行程序运行靠的是加载器,上面的 not found 表示加载器不知道动态库在哪里。 解决方案:在系统默认的库路径(/lib64/usr/lib64/usr/lib)下建立软链接

设置 LD_LIBRARY_PATH 环境变量,指定动态库路径。

代码语言:javascript
代码运行次数:0
运行
复制
export LD_LIBRARY_PATH=yourpath:$LD_LIBRARY_PATH

/etc/ld.so.conf.d/ 中添加路径,并运行 ldconfig 更新系统库缓存

2.4 动态库的加载与共享

加载与共享:动态库在进程运行时被加载到内存。操作系统管理着所有动态库的加载情况,确保同一库在多个进程间共享。例如,当 A.exeB.exe 都使用同一个动态库时,该库只会被加载一次,多个进程通过映射共享这份内存。

动态库的共享与写时拷贝:由于动态库被多个进程共享,全局变量(如 errno)可能会产生问题。操作系统通过 写时拷贝(Copy-on-Write)机制来解决这个问题:当某个进程修改共享的全局变量时,会为该进程创建该变量的副本,避免其他进程受到影响。

三、再谈地址空间

3.1 逻辑地址的引入
  • 逻辑地址(虚拟地址):当一个 C 源文件被编译成可执行文件(如 .exe 文件)时,会为每个代码段、数据段等指定地址。这些地址被称为逻辑地址。逻辑地址是程序在虚拟内存中的地址,而不是真正的物理地址。
  • 平坦模式:在现代操作系统中,通常采用平坦地址空间,也就是整个程序的地址空间都是连续的,程序可以按顺序加载到内存中。
3.2 CPU 如何知道指令位置
  • 当一个可执行程序加载到内存时,程序中的每条指令都会被赋予一个虚拟地址。
  • PC 指针(程序计数器):CPU 有一个寄存器叫做程序计数器(PC),它存储着下一条要执行指令的地址。可执行程序的入口地址会保存在程序的文件头中,并在程序加载到内存后,CPU 从这个地址开始执行程序。
  • 缺页中断:程序的页面没有完全加载时,操作系统会触发缺页中断,加载缺失的部分到内存。这样,程序的每个部分都会根据虚拟地址和物理地址的映射关系加载到内存。
3.3 库函数如何被找到并执行
  • 动态库的加载:当程序调用动态库中的函数(例如 printf)时,程序内部有一个逻辑地址(例如 0x11223344)对应于该函数。操作系统并不会把动态库加载到固定的内存地址,而是通过相对地址来加载和执行函数。
  • 相对编址:动态库中的函数地址不再是固定的绝对地址,而是相对于库起始地址的偏移量。通过这种方式,动态库可以加载到虚拟内存的任意位置,而不影响地址的正确性。
  • fPIC 选项:在编译动态库时,通过 -fPIC 选项告诉编译器采用位置无关代码(PIC)。这样,生成的库会使用相对地址而不是绝对地址,以便操作系统可以在加载时将库加载到任何位置。
  • 页表与虚拟地址:操作系统通过页表将虚拟地址映射到物理地址,并且在运行时根据虚拟地址查找物理地址,从而执行库函数。
3.4 静态库与动态库的区别
  • 静态库:静态库在编译时直接被拷贝到可执行文件中,函数的地址使用绝对地址,因此不需要加载到内存中。静态库在程序启动时已经包含在可执行文件中。
  • 动态库:动态库在程序运行时被加载到内存中,它使用相对地址,并通过操作系统的加载机制(如 ld.so)在运行时根据需求加载。

结语

动静态库技术的背后,折射出软件开发中 “分治” 与 “抽象” 的核心思想。通过将复杂功能封装为库,开发者得以在更高抽象层构建系统,这不仅降低了开发复杂度,更奠定了现代软件开发生态的基础。希望读者能以本文为起点,进一步探索 Linux 系统编程的更多奥秘,在代码的世界中不断积累与成长。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-05-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
    • 一、静态库
      • 1.1 静态库的制作
      • 1.2 静态库的生成
      • 1.3 静态库的发布
      • 1.4 静态库的使用
      • 1.5 静态库的安装
    • 二、动态库
      • 2.1 动态库的制作与生成
      • 2.2 动态库的发布
      • 2.3 动态库的使用
      • 2.4 动态库的加载与共享
    • 三、再谈地址空间
      • 3.1 逻辑地址的引入
      • 3.2 CPU 如何知道指令位置
      • 3.3 库函数如何被找到并执行
      • 3.4 静态库与动态库的区别
  • 结语
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档