硬核拆解:Linux动态库从原理到实战

本篇目标:

掌握动态库的基本原理及实际应用示例

一.动态库的原理与实战

1.代码准备

文件一:接口定义 my_lib.h

cpp 复制代码
#ifndef MY_LIB_H
#define MY_LIB_H

// 简单的加法,用来演示函数导出
int add(int a, int b);

// 打印库的信息,证明我们在调用动态库
void print_lib_info();

#endif

文件二:库的实现 my_lib.c

cpp 复制代码
#include <stdio.h>
#include "my_lib.h"

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

void print_lib_info() 
{
    printf("[Dynamic Library] 这是一个由 C 语言生成的动态库 (.so)\n");
    printf("[Dynamic Library] 它的代码段在内存中是共享的。\n");
}

文件三:调用程序 main.c

cpp 复制代码
#include <stdio.h>
#include "my_lib.h"

int main() 
{
    printf("--- 主程序启动 ---\n");

    print_lib_info();
    
    int result = add(10, 20);
    printf("10 + 20 = %d\n", result);

    printf("--- 主程序结束 ---\n");
    return 0;
}

目前这是在同一个目录下,生成的,如图所示:

为了模拟系统中的动态库,我们还要和之前的静态库一样,形成如图所示的目录:

也就是**./shared_code/lib** 和**./shared_code/usr/include**

2.动态库的形成

先复习一下之前的选项:

-c: 把汇编代码翻译成纯粹的机器码,生成目标文件(.o 文件),它只编译,绝对不进行链接(不去找动态库或静态库)。

-o:作用: 让gcc生成文件的指定的名字。

由于所有的库都是由.o文件组成的,因此我们需要先将想要形成库的.c文件转化为.o文件,但是还

需要一个选项,也就是**-fPIC,作用是生成位置无关代码,让库加载到内存任意地址都能正常运行**

操作如图所示:

然后再将其打上共享标签 ,此时要用**-shared** 选项,封装成 .so 文件操作如图所示:

-shared:告诉链接器生成一个用来被别人动态加载的共享对象(了解即可)

注意:当生成多个.o库文件时,可以将它们统一打包成一个共享库,此时我们可以使用

gcc -fPIC -c *.c 和gcc -shared -o libmyc.so *.o,但是绝对不能把 main 函数的.o文件打包进动态

库里!

这里再提醒一下关于动态库的命名: lib + 库的真实名字 + .so

然后我们在将当前目录下的动态库与库的头文件,拷贝到我们自己创建的目录下,如图所示:

注意:建议使用 cp 命令进行复制操作,而非 mv 命令。若误用 mv 命令,当前目录下的文件会被移

动而非复制(之前犯过的错误)。

在上级目录中,我们查看一下shared_code是否成功有拷贝后的文件,如图所示:

此时我们需要删除code目录下的libmyc.so文件以及除main.c之外的其他文件 ,以免影响后续操作,如图所示:

然后我们在将main.c编译成main.o,如图所示:

可是我们却发现找不到这个my_lib.h文件啊?

原因:当你使用 #include "my_header.h" 时,gcc 默认只会在以下几个地方寻找头文件

  1. 当前目录 (也就是**main.c 所在的目录**)

  2. 系统默认的头文件目录 (比如 **/usr/include, /usr/local/include**等)。

但是gcc根本不知道 ../shared_code/usr/include 这个神奇的目录存在,所以肯定会编译中断。

因此我们需要这样操作:

那么此时就会有人说:这也不对啊,我以前使用gcc时,不也用到了库吗,但是我也没有用-I来指

明我要查找的头文件啊?

解释:gcc认识/usr/include与usr/local/include,就会自动在这里面找头文件,但是gcc并不认识

我们自己的my_lib.h。

当我们生成了可执行程序,需要调用这个动态库时,操作逻辑与链接静态库如出一辙。我们需要向编译器(gcc)准确传达三个维度的寻址信息:

  • -I指引头文件的搜索目录。就像是告诉编译器:去这个指定的文件柜里,翻阅函数的接口说明书。

  • -L指引库文件的搜索目录。就像是告诉链接器:别光在系统默认仓库找,去我指定的这个额外仓库里找找高阶零件。

  • -l指定需要链接的具体库名 。它紧跟在 -L 之后,明确点出零件的代号(例如 -lmyc 会自动去寻找 libmyc.so)。

操作如图所示:

最后来到了最终时刻,执行这个可执行程序,如图所示:

我们发现可执行程序main无法运行,原因是加载时找不到所需的库文件。虽然我们已通过-l参数

gcc在../shared_code/lib目录下查找,但问题依然存在。于是我们使用ldd命令来验证库文件是否确

实未被找到,结果如图所示:

解释:虽然gcc能够识别libmyc.so,但系统本身并不认识该库文件。生成可执行程序后,程序运

行将由系统接管。系统会在预设的库文件路径中查找依赖库,而我们的../shared_code/olib目录并

不在这些系统默认搜索路径中,因此会导致库文件找不到的问题。

解决办法

1.系统找不到,那我们就在运行前,临时强行塞给它一个搜索路径,我们让系统在

LD_LIBRARY_PATH 环境变量中查找库,操作如图所示:

但是这个是临时指定搜索路径,关闭终端后就失效了。

2.干脆在编译的时候,就把运行期的寻找路径死死烙印在可执行文件的骨子里

操作:

cpp 复制代码
gcc -o main main.o -I ../shared_code/usr/include -L ../shared_code/lib -lmyc -Wl,-rpath=../shared_code/lib

如图所示:

也是成功了的。

3.系统只认 /usr/lib 或者 /lib 这种目录,那我们直接把自己的库扔进这里面就行了,但是这样

会污染系统目录(不太建议这样做)。

4.修改系统的动态加载器配置文件,把你的目录合法注册进去。

操作:首先获取库的绝对路径 (假设是 /home/kong/shared_code/lib;其次,在

/etc/ld.so.conf.d/ 目录,用sudo touch新建一个文件 (比如 myc.conf),用sudo vim打开

这个文件把绝对路径写进去;最后,刷新系统缓存,sudo ldconfig。

如图所示:

此时在执行,就成功了。

3.补充点

1.GCC/G++ 默认采用动态链接方式,如需使用静态库,需添加 -static 参数,且系统中必须存在对应的静态库文件。

我们先创建一个.c文件,代码如图所示:

cpp 复制代码
#include<stdio.h>

int main()
{
    printf("hello world\n");
    return 0;
}

操作如图所示:

通过ldd后的libc.so,也就是c语言的动态库,我们可以看到code默认是动态链接的,此时code的大

小为8360字节,也验证了我们的猜想,如果是静态链接呢?首先我们需要先安装静态库,在

centos下的安装操作:

cpp 复制代码
sudo yum install glibc-static

操作如图所示:

通过code的大小与ldd的信息,可以确定这是个静态链接。

2.Linux 系统中,大多数库在安装时默认优先安装动态库版本。

直接操作,如图所示:

cpp 复制代码
# 统计动态库 (.so) 的数量
find /usr/lib /usr/lib64 -name "*.so*" 2>/dev/null | wc -l
cpp 复制代码
# 统计静态库 (.a) 的数量
find /usr/lib /usr/lib64 -name "*.a" 2>/dev/null | wc -l

注:目前这个操作不需要认识,直接复制粘贴

结果如图所示:

可以看到动态库的数量是大几千,而静态库的数量少得可怜!这就直接从系统生态的层面上证明了:Linux 官方和各大软件厂商,默认发放和安装的都是动态库。

4.总结
  1. 动态库制作:必须使用 -fPIC 生成位置无关代码,再用 -shared 打包成 .so 文件。
  2. 编译链接阶段:需要使用 -I(头文件路径)、-L(库文件路径)、-l(库名)三个参数准确指引 gcc。
  3. 运行时加载gcc 能找到库 ≠ 系统运行时能找到库 。系统动态加载器(ld.so)默认只在标准路径搜索,因此需要通过以下方式解决:
    • LD_LIBRARY_PATH:临时设置(测试推荐)
    • -Wl,-rpath:编译时嵌入路径(项目推荐)
    • ldconfig:永久注册到系统(公共库推荐)
  4. 动态链接 vs 静态链接:Linux 系统默认优先使用动态库,程序体积更小,内存共享效率更高,这也是现代软件的主流方式。

二.外部库

我们现在没接触过太多的库,唯⼀接触过的就是C、C++标准库,这⾥我们可以推荐⼀个好玩的图形 库:ncurses,安装操作:

cpp 复制代码
// Centos下
sudo yum install -y ncurses-devel

// ubuntu下
sudo apt install -y libncurses-dev

系统中其实有很多库,它们通常由⼀组互相关联的⽤来完成某项常⻅⼯作的函数构成。⽐如⽤来处理 屏幕显⽰情况的函数(ncurses库)(看看就行)

代码如图所示:

cpp 复制代码
#include <ncurses.h>
#include <unistd.h>
#include <string.h>

#define PROGRESS_BAR_WIDTH 30
#define BORDER_PADDING 2
#define WINDOW_WIDTH (PROGRESS_BAR_WIDTH + 2 * BORDER_PADDING + 2) // 加边框的宽度
#define WINDOW_HEIGHT 5
#define PROGRESS_INCREMENT 3
#define DELAY 300000  // 微秒(300毫秒)

int main() {
    // 1. 初始化 ncurses 环境
    initscr();
    start_color();
    cbreak();
    noecho();
    curs_set(FALSE); // 隐藏光标,让界面更干净

    // 2. 初始化颜色对
    init_pair(1, COLOR_GREEN, COLOR_BLACK); // 已完成部分
    init_pair(2, COLOR_RED, COLOR_BLACK);   // 剩余部分

    // 3. 计算屏幕居中坐标并创建窗口
    int max_y, max_x;
    getmaxyx(stdscr, max_y, max_x);
    int start_y = (max_y - WINDOW_HEIGHT) / 2;
    int start_x = (max_x - WINDOW_WIDTH) / 2;
    WINDOW *win = newwin(WINDOW_HEIGHT, WINDOW_WIDTH, start_y, start_x);

    int progress = 0;
    int max_progress = PROGRESS_BAR_WIDTH;

    // 4. 核心渲染循环
    while (progress <= max_progress) {
        werase(win);    // 清除上一帧内容
        box(win, 0, 0); // 必须在擦除后立刻重新绘制边框!

        // 防溢出保护,确保进度不超过最大值
        int completed = (progress > max_progress) ? max_progress : progress;
        
        int bar_x = BORDER_PADDING + 1; 
        int bar_y = 1; // 进度条放在第 2 行 (y=1)

        // 渲染已完成部分 (注意必须用 wattron)
        wattron(win, COLOR_PAIR(1));
        for (int i = 0; i < completed; i++) {
            mvwprintw(win, bar_y, bar_x + i, "#");
        }
        wattroff(win, COLOR_PAIR(1));

        // 渲染剩余部分
        wattron(win, A_BOLD | COLOR_PAIR(2));
        for (int i = completed; i < max_progress; i++) {
            mvwprintw(win, bar_y, bar_x + i, " ");
        }
        wattroff(win, A_BOLD | COLOR_PAIR(2));

        // 渲染百分比文本
        char percent_str[10];
        int percent_val = (completed * 100) / max_progress;
        snprintf(percent_str, sizeof(percent_str), "%d%%", percent_val);
        int percent_x = (WINDOW_WIDTH - strlen(percent_str)) / 2;
        
        // 打印在进度条下方,避开底部边框 (y=3)
        mvwprintw(win, 3, percent_x, percent_str);

        wrefresh(win); // 刷新窗口,将缓冲区推送到屏幕

        progress += PROGRESS_INCREMENT;
        usleep(DELAY);
    }

    // 跑满 100% 后停顿 1 秒,让你能看清结果再退出
    sleep(1);

    // 5. 优雅清理资源
    delwin(win);
    endwin();
    return 0;
}

我们此时用到了第三方动态库 ncurses,虽然 库中有这个**ncurses**库,但是gcc不认识它,就要用-l

和这个库指定连接**,也就是让**让 GCC 去系统的公共大仓库里,精准提取这一个特定的库文件。

cpp 复制代码
gcc main.c -o main -lncurses

结果如图所示:

相关推荐
A小辣椒2 天前
TShark:Wireshark CLI 功能
linux
A小辣椒2 天前
TShark:基础知识
linux
AlfredZhao2 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao3 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334663 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪3 天前
linux 拷贝文件或目录到指定的位置
linux
大树884 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠4 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质4 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush44 天前
嵌入式linux学习记录十四、术语
linux·嵌入式