Linux系统:库的制作


前言

学习linux的库的制作与原理可以提高代码复用性,减少程序体积、加快编译,理解 Linux 程序的运行机制,方便模块化开发与协作,会做库、懂原理,你就能写出别人能直接用的“工具箱”,还能明白 Linux 在背后到底是怎么帮你把它装进程序里的。


一,库是什么?

  • 库是已经编写好、经过验证并可复用的代码集合。在现实开发中,几乎每个程序都依赖于大量基础的底层库,不可能每位开发者都从零开始实现所有功能。因此,库的存在意义极为重要——它是软件开发高效运作的基石。
  • 从本质上看,库是一种可执行代码的二进制文件,能够被操作系统加载到内存中并直接运行。
    根据链接方式的不同,库主要分为两类:
    • 静态库(Static Library):在编译时将库代码直接打包进可执行文件中。
    • 动态库(Dynamic Library / Shared Library):在运行时由操作系统按需加载,可供多个程序共享使用。

我们先看一下我们最常用的两个C和C++的库

  • libc64:就是 64 位版本的 C/C++ 语言基础工具包,你写 C/C++ 程序时用到的很多函数,比如 printf、scanf、malloc,其实都藏在这个工具包里。

存放位置

  • libc-2.17.so :就是 glibc 2.17 版本的核心动态库文件,也就是 C 标准库的具体实现文件。
[root@hcss-ecs-f59a lib64]# ls /lib64/libc-2.17.so -l
-rwxr-xr-x 1 root root 2156592 Jun  4  2024 /lib64/libc-2.17.so
  • libc.a: 是 C 标准库的静态库文件,它和 libc.so(动态库)是同一个库的两种不同形式。
[root@hcss-ecs-f59a ~]# ls /lib64/libc.a -l
-rw-r--r-- 1 root root 5105516 Jun  4  2024 /lib64/libc.a
  • libstdc++.so.6:是 GNU C++ 标准库(libstdc++)的动态链接库文件,它是 Linux 系统中运行 C++ 程序的核心依赖库之一
[root@hcss-ecs-f59a ~]# ls /lib64/libstdc++.so.6 -l
lrwxrwxrwx 1 root root 19 Jul 26  2024 /lib64/libstdc++.so.6 libstdc++.so.6.0.19
  • libstdc++.a :是 GNU C++ 标准库(libstdc++)的静态版本,与动态库 libstdc++.so 不同,它在编译时会被直接链接到可执行文件中,而不是在运行时动态加载。
[root@hcss-ecs-f59a ~]# ls /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a -l
-rw-r--r-- 1 root root 2932366 Sep 30  2020 /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a

一个简单的库制作

main.c

#include"file.h"
int main()
{
  print("hello,world\n");
  printf("%d\n",Add(100,200));
}

file.c

#include<stdio.h>
void print(const char* ch)
{
  printf("%s",ch);
}
int Add(int a,int b)
{
  return a+b;
}

file.h

#include<stdio.h>
void print(const char* ch);
int Add(int a,int b);
  • main.c :源文件 —> 包含 main() 函数,是程序的入口,调用其他文件中定义的函数。
  • file.c:源文件 —> 实现具体的函数(如 print() 和 Add()),包含函数的具体逻辑。
  • file.h:头文件 —> 声明函数(如 print() 和 Add()),告诉编译器这些函数的存在和格式。

二,静态库

  • 静态库(.a)就是在编译链接阶段,把库里的代码直接拷贝进可执行文件里,运行时就不用再去找那个静态库了。

  • 一个可执行程序可能会用到很多库,这些库有的是静态库,有的是动态库。默认情况下,编译会优先用动态库,只有找不到对应的动态 .so 库时,才会用同名的静态库。我们也可以通过给 gcc 加个 -static 参数,强制让它只用静态库。

2-1 静态库的生成

my_stdio.h

#pragma once
#define SIZE 1024
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2
struct IO_FILE {
    int flag; // 刷新方式
    int fileno; // 文件描述符
    char outbuffer[SIZE];
    int cap;
    int size;
    // TODO
};
typedef struct IO_FILE mFILE;
mFILE * mfopen(const char * filename,const char * mode);
int mfwrite(const void * ptr, int num, mFILE * stream);
void mfflush(mFILE * stream);
void mfclose(mFILE * stream);

my_stdio.c

#include "my_stdio.h"

#include <string.h>

#include <stdlib.h>

#include <sys/stat.h>

#include <sys/types.h>

#include <fcntl.h>

#include <unistd.h>

mFILE * mfopen(const char * filename,
    const char * mode) {
    int fd = -1;
    if (strcmp(mode, "r") == 0) {
        fd = open(filename, O_RDONLY);
    } else if (strcmp(mode, "w") == 0) {
        fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
    } else if (strcmp(mode, "a") == 0) {
        fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
    }
    if (fd < 0) return NULL;
    mFILE * mf = (mFILE * ) malloc(sizeof(mFILE));
    if (!mf) {
        close(fd);
        return NULL;
    }
    mf -> fileno = fd;
    mf -> flag = FLUSH_LINE;
    mf -> size = 0;
    mf -> cap = SIZE;
    return mf;
}
void mfflush(mFILE * stream) {
    if (stream -> size > 0) {
        // 写到内核文件的文件缓冲区中!
        write(stream -> fileno, stream -> outbuffer, stream -> size);
        // 刷新到外设
        fsync(stream -> fileno);
        stream -> size = 0;
    }
}
int mfwrite(const void * ptr, int num, mFILE * stream) {
    // 1. 拷贝
    memcpy(stream -> outbuffer + stream -> size, ptr, num);
    stream -> size += num;
    // 2. 检测是否要刷新
    if (stream -> flag == FLUSH_LINE && stream -> size > 0 && stream -
        >
        outbuffer[stream -> size - 1] == '\n') {
        mfflush(stream);
    }
    return num;
}
void mfclose(mFILE * stream) {
    if (stream -> size > 0) {
        mfflush(stream);
    }
    close(stream -> fileno);
}

my_string.h

#pragma once
int my_strlen(const char * s);

my_string.c

#include "my_string.h"

int my_strlen(const char * s) {
    const char * end = s;
    while ( * end != '\0') end++;
    return end - s;
}

Makefile

libmystdio.a:my_stdio.o my_string.o
	@ar -rc $@ $^
	@echo "build $^ to $@ ... done"
%.o:%.c
	@gcc -c $<
	@echo "compling $< to $@ ... done"
.PHONY:clean
clean:
	@rm -rf *.a *.o stdc*
	@echo "clean ... done"
.PHONY:output
output:
	@mkdir -p stdc/include
	@mkdir -p stdc/lib
	@cp -f *.h stdc/include
	@cp -f *.a stdc/libc
	@tar -czf stdc.tgz stdc
	@echo "output stdc ... done"

arGNU 的归档工具,用来把多个文件打包成一个归档文件(比如 .a 静态库)。

  • r(replace):如果归档里已经有同名文件,就替换它。
  • c(create):如果归档不存在,就新建一个。
[gch@hcss-ecs-f59a day8]$ ar -tv libmystdio.a
rw-rw-r-- 1000/1000   2848 Aug 11 17:17 2025 my_stdio.o
rw-rw-r-- 1000/1000   1272 Aug 11 17:17 2025 my_string.o
  • t: 列出静态库中的文件
  • v:verbose 详细信息

2-2 静态库使用

main.c

#include"my_stdio.h"
#include"my_string.h"
#include<stdio.h>
int main()
{
  const char* s="abcdefg";
  printf("%s:%d\n",s,my_strlen(s));
  mFILE *fp = mfopen("./log.txt","a");
  if(fp == NULL)
  {
    return 1;
  }
  mfwrite(s,my_strlen(s),fp);
  mfwrite(s,my_strlen(s),fp);
  mfwrite(s,my_strlen(s),fp);
  mfclose(fp);
  return 0;
}
  • 这个main.c和上面的文件我们都创建好了,我们执行make的结果如下
[gch@hcss-ecs-f59a day8]$ make
compling my_stdio.c to my_stdio.o ... done
compling my_string.c to my_string.o ... done
build my_stdio.o my_string.o to libmystdio.a ... done
[gch@hcss-ecs-f59a day8]$ ll
total 32
-rw-rw-r-- 1 gch gch 4374 Aug 11 17:17 libmystdio.a
-rw-rw-r-- 1 gch gch  334 Aug 11 17:34 main.c
-rw-rw-r-- 1 gch gch  375 Aug 11 14:17 makefile
-rw-rw-r-- 1 gch gch 1533 Aug 11 17:16 my_stdio.c
-rw-rw-r-- 1 gch gch  435 Aug 11 17:16 my_stdio.h
-rw-rw-r-- 1 gch gch 2848 Aug 11 17:17 my_stdio.o
-rw-rw-r-- 1 gch gch  138 Aug 11 17:17 my_string.c
-rw-rw-r-- 1 gch gch   44 Aug 11 17:17 my_string.h
-rw-rw-r-- 1 gch gch 1272 Aug 11 17:17 my_string.o
  • 这里先是生成了my_stdio.omy_string.o,然后将它们打包形成静态库libmystdio.a

那我们的main.c要怎么使用这个静态库libmystdio.a呢,有以下几种情况

  • 场景1:头文件和库文件安装到系统路径下
[gch@hcss-ecs-f59a day8]$ gcc main.c -lmystdio
  • 场景2:头文件和库文件和我们自己的源文件在同一个路径下
[gch@hcss-ecs-f59a day8]$ $ gcc main.c -L. -lmystdio
  • 场景3:头文件和库文件有自己的独立路径
gcc main.c -I头文件路径 -L库文件路径 -lmymath
  • -lxxx 会去找名字是 libxxx.a(静态库)或 libxxx.so(动态库)的文件,优先找动态库 .so,找不到才会找静态库 .a,如果要强制用静态库,可以加 -static,
  • -I 是 GCC 编译选项中用来指定头文件搜索路径的参数
  • -L是 GCC 中的一个选项,用来 指定库文件的搜索路径。

这里我们的工作更符合场景2,所以我们用场景2来测试一下

[gch@hcss-ecs-f59a day8]$ gcc main.c -L. -lmystdio
[gch@hcss-ecs-f59a day8]$ ./a.out
abcdefg:7
[gch@hcss-ecs-f59a day8]$ cat log.txt
abcdefgabcdefgabcdefg
  • 注意:这里的测试目标文件生成后,静态库删掉,程序照样可以运行,因为静态库是直接将代码导入

三,动态库

动态库(.so 文件)是程序运行时才加载进来的库,多个程序可以一起用同一份库代码。

  • 怎么链接:跟动态库链接的程序里,不会把库的机器码都拷进去,只会记下“用到的函数入口地址在哪”。
  • 什么时候加载:程序一启动,操作系统里的“动态链接器”会从磁盘把需要的动态库读到内存,然后把函数地址补好,这就叫 动态链接。
  • 为什么省资源:因为一份动态库可以被好多程序一起用,内存和磁盘都只要放一份,可执行文件本身也能变得更小。

3-1 动态库的生成

Makefile

libmystdio.so:my_stdio.o my_string.o
	gcc -o $@ $^ -shared
%.o:%.c
	gcc -fPIC -c $<
.PHONY:clean
clean:
	@rm -rf *.so *.o stdc*
	@echo "clean ... done"
.PHONY:output
output:
	@mkdir -p stdc/include
	@mkdir -p stdc/lib
	@cp -f *.h stdc/include
	@cp -f *.so stdc/lib
	@tar -czf stdc.tgz stdc
	@echo "output stdc ... done"
  • -fPIC:核心作用就是:让编译出来的目标文件可以在内存中的任意地址加载运行,这就是所谓的 位置无关代码
    • 编译 动态库 (.so) 时 → 必须加,否则链接会报错
    • 静态库 (.a) 不需要
  • -shared: 表示生成共享库格式
  • 库名规则:libxxx.so

3-2 动态库使用

动态库也有三种使用场景,和上述静态库相同,这里就不再过多阐述了

  • 我们以场景二为例:头文件和库文件和我们自己的源文件在同一个路径下
[gch@hcss-ecs-f59a day8]$ ll
total 24
-rw-rw-r-- 1 gch gch  334 Aug 11 17:34 main.c
-rw-rw-r-- 1 gch gch  320 Aug 11 19:39 makefile
-rw-rw-r-- 1 gch gch 1533 Aug 11 17:16 my_stdio.c
-rw-rw-r-- 1 gch gch  435 Aug 11 17:16 my_stdio.h
-rw-rw-r-- 1 gch gch  138 Aug 11 17:17 my_string.c
-rw-rw-r-- 1 gch gch   44 Aug 11 17:17 my_string.h
[gch@hcss-ecs-f59a day8]$ make
gcc -fPIC -c my_stdio.c
gcc -fPIC -c my_string.c
gcc -o libmystdio.so my_stdio.o my_string.o -shared
[gch@hcss-ecs-f59a day8]$ gcc main.c -L. -lmystdio
[gch@hcss-ecs-f59a day8]$ ./a.out
abcdefg:7
[gch@hcss-ecs-f59a day8]$ cat log.txt
abcdefgabcdefgabcdefg

3-3 库运行搜索路径

我们观察一下a.out所依赖的库

[gch@hcss-ecs-f59a day8]$ ldd a.out
	linux-vdso.so.1 =>  (0x00007ffea3dd0000)
	libmystdio.so => not found
	libc.so.6 => /lib64/libc.so.6 (0x00007f6bf8018000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f6bf85e8000)

为什么这里会出现libmystdio.so => not found?

  • gcc main.c -L. -lmystdio中的 -L. 只是告诉编译器 在链接阶段 去当前目录找 libmystdio.so,所以编译能顺利完成。

  • 但程序运行时,动态链接器并不会参考编译时的 -L 选项,而是只会去系统的默认库目录(如 /lib、/usr/lib、/usr/local/lib)寻找动态库。

  • 如果运行时找不到,就会出现 libmystdio.so => not found 的情况。

解决方法:让运行时也能找到库,可以用以下方式之一:

  • 修改环境变量LD_LIBRARY_PATH

环境变量 LD_LIBRARY_PATH 里保存着一组目录路径。运行程序时,动态链接器会优先在这些路径中查找动态库。也就是说,把目录加入 LD_LIBRARY_PATH,就等于把它变成了运行时的额外默认库搜索路径

我的当前路径为/working/HaoHao/day8,执行指令:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/working/HaoHao/day8
  • /lib64中创建软链接

/lib64 是系统默认的动态库搜索目录之一。如果在该目录下创建一个指向你自己动态库 libmystdio.so 的软链接,就相当于把你的库加入了系统的默认库路径,程序运行时就能自动找到它。

我的当前路径为/working/HaoHao/day8,执行指令:

[gch@hcss-ecs-f59a day8]$ sudo ln -s /working/HaoHao/day8/libmystdio.so /lib64/libmystdio.so
[gch@hcss-ecs-f59a day8]$ ls /lib64/libmystdio.so
/lib64/libmystdio.so
  • 配置/ etc/ld.so.conf.d/

/etc/ld.so.conf.d/ 目录下,可以放用户自定义的动态库路径配置文件。只需用 root 权限新建一个以 .conf 结尾的文件,里面写上你想添加的动态库路径即可。这样系统在运行 ldconfig 后,就会把这些路径加入到动态库搜索范围里。

[gch@hcss-ecs-f59a day8]$ ll /etc/ld.so.conf.d
total 20
-rw-r--r--  1 root root 26 Jun 11  2024 bind-export-x86_64.conf
-r--r--r--. 1 root root 63 Aug  8  2019 kernel-3.10.0-1062.el7.x86_64.conf
-r--r--r--  1 root root 63 Jun  4  2024 kernel-3.10.0-1160.119.1.el7.x86_64.conf
-rw-r--r--  1 root root 17 Oct  2  2020 mariadb-x86_64.conf
-rw-r--r--  1 root root 56 Aug 11 21:41 text.conf
[gch@hcss-ecs-f59a day8]$ ldconfig

四,目标文件

在 Windows 下,编译和链接这两个步骤基本都被 IDE 封装好了,我们平时一键构建,特别方便。但一旦出了问题,尤其是链接错误,很多人就懵了,不知道怎么下手。而在 Linux 下,我们之前学过怎么用 gcc 编译器手动完成这些步骤。
在这里插入图片描述

这里通过预处理,编译,汇编,将.c文件变为了.o文件当然我们可以直接跳过中间的步骤,直接将.c文件变为.o文件,示例如下

hello.c

#include<stdio.h>
void run();
int main()
{
  printf("hello,world!\n");
  run();
  return 0;
}

code.c

#include<stdio.h>
void run()
{
  printf("running...\n");
}
[gch@hcss-ecs-f59a day9]$ gcc -c code.c
[gch@hcss-ecs-f59a day9]$ gcc -c hello.c
[gch@hcss-ecs-f59a day9]$ ll
total 16
-rw-rw-r-- 1 gch gch   59 Aug 12 12:59 code.c
-rw-rw-r-- 1 gch gch 1496 Aug 12 13:00 code.o
-rw-rw-r-- 1 gch gch   94 Aug 12 12:55 hello.c
-rw-rw-r-- 1 gch gch 1560 Aug 12 13:00 hello.o

编译之后,会生成两个 .o 后缀的文件,我们叫它们目标文件。这里要注意,如果你改了其中一个源文件,只需要单独编译改动的那个文件,没必要整个工程都重新编译,省时间又高效。目标文件其实就是二进制文件,它的格式是 ELF,这是一种专门用来装机器码的文件格式。

用file命令辨识一下文件类型

[gch@hcss-ecs-f59a day9]$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

我们可以看到这里的hello.o文件是ELF格式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值