欢迎光临
我们一直在努力

深入解析动静态库制作与使用

• 动静态库的制作

• 动静态库的使用

• 动态库的查找

• 可执行程序ELF格式

• 可执行程序的加载过程

• 虚拟地址空间和动态库加载的过程


1. 什么是库

库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能

每个人的代码都从零开始,因此库的存在意义非同寻常。

本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:

• 静态库 .a[Linux]、.lib[windows]

• 动态库 .so[Linux]、.dll[windows]

// ubuntu 动静态库
// C
$ ls -l /lib/x86_64-linux-gnu/libc-2.31.so
-rwxr-xr-x 1 root root 2029592 May 1 02:20 /lib/x86_64-linux-gnu/libc-2.31.so
$ ls -l /lib/x86_64-linux-gnu/libc.a
-rw-r–r– 1 root root 5747594 May 1 02:20 /lib/x86_64-linux-gnu/libc.a

//C++
$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so -l
lrwxrwxrwx 1 root root 40 Oct 24 2022 /usr/lib/gcc/x86_64-linuxgnu/9/libstdc++.so -> ../../../x86_64-linux-gnu/libstdc++.so.6
$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a
/usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a

// Centos 动静态库
// C
$ ls /lib64/libc-2.17.so -l
-rwxr-xr-x 1 root root 2156592 Jun 4 23:05 /lib64/libc-2.17.so
[whb@bite-alicloud ~]$ ls /lib64/libc.a -l
-rw-r–r– 1 root root 5105516 Jun 4 23:05 /lib64/libc.a

// C++
$ ls /lib64/libstdc++.so.6 -l
lrwxrwxrwx 1 root root 19 Sep 18 20:59 /lib64/libstdc++.so.6 ->
libstdc++.so.6.0.19
$ 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-redhatlinux/4.8.2/libstdc++.a

预备工作,准备好历史封装的libc代码,在任意新增"库文件"

// 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;
}


2. 静态库

• 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中,程序运行的时候将不再需要静态库。

• 一个可执行程序可能用到许多的库,这些库运行有的是静态库,有的是动态库,而我们的编译默认为动态链接库,只有在该库下找不到动态.so的时候才会采用同名静态库。我们也可以使用 gcc的 -static 强转设置链接静态库。

2-1 静态库生成

// 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/lib
@tar -czf stdc.tgz stdc
@echo "output stdc … done"

• ar是 gnu 归档工具, rc 表示 (replace and create)

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;
}

// 场景1:头文件和库文件安装到系统路径下
$ gcc main.c – lmystdio

// 场景2:头文件和库文件和我们自己的源文件在同一个路径下
$ gcc main.c -L.- lmymath

// 场景3:头文件和库文件有自己的独立路径
$ gcc main.c -I头文件路径 – L库文件路径 – lmymath

– L库文件路径 – lmymath

• -L: 指定库路径

• -I: 指定头文件搜索路径

• -l: 指定库名

• 测试目标文件生成后,静态库删掉,程序照样可以运行

• 关于 -static 选项,稍后介绍

• 库文件名称和引入库的名称:去掉前缀 lib ,去掉后缀 .so , .a ,

  如:libc.so -> c


3. 动态库

• 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。

• 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。

• 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)

• 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。

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"

• shared: 表示生成共享库格式

• fPIC:产生位置无关码(position independent code)

• 库名规则:libxxx.so

3-2 动态库使用

// 场景1:头文件和库文件安装到系统路径下
$ gcc main.c -lmystdio

// 场景2:头文件和库文件和我们自己的源文件在同一个路径下
$ gcc main.c -L. -lmymath // 从左到右搜索-L指定的目录

// 场景3:头文件和库文件有自己的独立路径
$ gcc main.c -I头文件路径 -L库文件路径 -lmymath

$ ldd libmystdio.so // 查看库或者可执行程序的依赖
linux-vdso.so.1 => (0x00007fffacbbf000)
libc.so.6 => /lib64/libc.so.6 (0x00007f8917335000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8917905000)

// 以场景2为例
$ ll
total 24
-rwxrwxr-x 1 whb whb 8592 Oct 29 14:50 libmystdio.so
-rw-rw-r– 1 whb whb 359 Oct 19 16:07 main.c
-rw-rw-r– 1 whb whb 447 Oct 29 14:50 my_stdio.h
-rw-rw-r– 1 whb whb 447 Oct 29 14:50 my_string.h
$ gcc main.c -L. -lmystdio
$ ll
total 36
-rwxrwxr-x 1 whb whb 8600 Oct 29 14:51 a.out
-rwxrwxr-x 1 whb whb 8592 Oct 29 14:50 libmystdio.so
-rw-rw-r– 1 whb whb 359 Oct 19 16:07 main.c
-rw-rw-r– 1 whb whb 447 Oct 29 14:50 my_stdio.h
-rw-rw-r– 1 whb whb 447 Oct 29 14:50 my_string.h
[whb@bite-alicloud other]$ ./a.out

3-3 库运行搜索路径

3-3-1 问题

$ ldd a.out
linux-vdso.so.1 => (0x00007fff4d396000)
libmystdio.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00007fa2aef30000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa2af2fe000)

3-3-2 解决方案

• 拷贝 .so 文件到系统共享库路径下, 一般指 /usr/lib、/usr/local/lib、/lib64 或者开篇指明的库路径等

• 向系统共享库路径下建立同名软连接

• 更改环境变量: LD_LIBRARY_PATH

• ldconfig方案:配置 /etc/ld.so.conf.d/ ,ldconfig更新

[root@localhost linux]# cat /etc/ld.so.conf.d/bit.conf
/root/tools/linux
[root@localhost linux]# ldconfig // 要生效,这里要执行ldconfig,重新加载库搜索路径


4. 目标文件

编译和链接这两个步骤,在Windows下被我们的IDE封装的很完美,我们一般都是一键构建非常方便,但一旦遇到错误的时候呢,尤其是链接相关的错误,很多人就束手无策了。在Linux下,我们之前也学习过如何通过gcc编译器来完成这一系列操作。

接下来我们深入探讨一下编译和链接的整个过程,来更好的理解动静态库的使用原理。

先来回顾下什么是编译呢?编译的过程其实就是将我们程序的源代码翻译成CPU能够直接运

行的机器代码。

比如:在一个源文件 hello.c 里便简单输出"hello world!",并且调用一个run函数,而这个函数被定义在另一个原文件 code.c 中。这里我们就可以调用 gcc -c 来分别编译这两个原文件。

// 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");
}

// 编译两个源文件
$ gcc -c hello.c
$ gcc -c code.c
$ ls
code.c code.o hello.c hello.o

可以看到,在编译之后会生成两个扩展名为 .o 的文件,它们被称作目标文件。要注意的是如果我们修改了一个原文件,那么只需要单独编译它这一个,而不需要浪费时间重新编译整个工程。目标文件是一个二进制的文件,文件的格式是 ELF ,是对二进制代码的一种封装。

$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

## file命令用于辨识文件类型。


5. ELF文件

要理解编译链链接的细节,我们不得不了解一下ELF文件。其实有以下四种文件其实都是ELF文件:

• 可重定位文件(Relocatable File) :即 xxx.o 文件。包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。

• 可执行文件(Executable File) :即可执行程序。

• 共享目标文件(Shared Object File) :即 xxx.so文件。

• 内核转储(core dumps) ,存放当前进程的执行上下文,用于dump信号触发。

一个ELF文件由以下四部分组成:

• ELF头(ELF header) :描述文件的主要特性。其位于文件的开始位置,它的主要目的是定位文件的其他部分 (包含具体的合并原则)

• 程序头表(Program header table) :列举了所有有效的段(segments)和他们的属性。表里记着每个段的开始的位置和位移(offset)、长度,毕竟这些段,都是紧密的放在二进制文件中,需要段表的描述信息,才能把他们每个段分割开。

• 节头表(Section header table) :包含对节(sections)的描述。

• 节(Section ):ELF文件中的基本组成单位,包含了特定类型的数据。ELF文件的各种信息和数据都存储在不同的节中,如代码节存储了可执行代码,数据节存储了全局变量和静态数据等。

最常见的节

• 代码节(.text):用于保存机器指令,是程序的主要执行部分。

• 数据节(.data):保存已初始化的全局变量和局部静态变量。

赞(0)
未经允许不得转载:171主机测评 » 深入解析动静态库制作与使用
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址