✨✨ 欢迎大家来到小伞的大讲堂✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:LInux_st
小伞的主页:xiaosan_blog制作不易!点个赞吧!!谢谢喵!!
1. 什么是库
库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。
本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:
- 静态库.a[Linux]、.lib[windows]
- 动态库.so[Linux]、.dll[windows]
// ubuntu 动静态库
// C
//C++
1.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;
}
当我们借代码给别人时,并且老师想要查看他的源码,这时候,我们拷贝*.c文件,就容易被发现。我们就可以
gcc -c *.c (将.c文件编译成.o文件,.o文件是一堆二进制乱码)
当我们有一千个.c文件时,我们需要tar或者zip打包传输,然而解压出来也是一千个文件,太麻烦了,为了避免这样的麻烦,我们就有ar命令将所有的.o文件打包,可以直接使用gcc/g++链接,无需解压。这就是库
2.静态库的制作
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中,程序运行的时候将不再需要静态库
ar -rc 新建包文件名 目标文件名
![]()
一般命名为:lib+名+(.a)
ar 是 gnu 归档⼯具, rc 表⽰ (replace and create)
$ ar -tv libmystdio.a
rw-rw-r-- 1000/1000 2848 Oct 29 14:35 2024 my_stdio.o
rw-rw-r-- 1000/1000 1272 Oct 29 14:35 2024 my_string.o
• t: 列出静态库中的⽂件
• v:verbose 详细信息
我们的编译默认执行的为动态链接库,只有在该库找不到动态.so的时候才会采用同名静态库,我们也可以采用-static强转设置链接静态库
2.1静态库的生成
2.1.1 当前路径下
通过编译源文件

main.c 为用户调用主程序

此时我们发现其调用接口操作系统不认识,因为这时候的.a文件为库文件,我们需要链接库文件
![]()
唉,为什么链接库了,却显示没有找到,应该是名字的问题,库的文件名应该是去掉lib,去掉.a
![]()
因为gcc查找库不会从当前查找,如果你能编译的过,或许是vim配置更改了某些条件,
![]()
我们需要-L指定当前路径查找 ,我们就能运行成功了
![]()
2.1.1.1总结:

2.1.2 不同路径下
此时你又想到一个方法,我们将库直接打包发

tar czf lib.tgz lib //打包压缩

tar xzf lib.tgz //解压

编译源文件(错误,一般不能)

正确做法 (一般情况下是不能创建的,因为编译时会从当前路径寻找,可能是因为vim原因)

创建可执行程序(链接库)

2.2总结三种情况:
// 场景1:头⽂件和库⽂件安装到系统路径下
$ gcc main.c -lmystdio
// 场景2:头⽂件和库⽂件和我们⾃⼰的源⽂件在同⼀个路径下
$ gcc main.c -L. -lmymath
// 场景3:头⽂件和库⽂件有⾃⼰的独⽴路径
$ gcc main.c -I头⽂件路径 -L库⽂件路径 -lmymath
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''
3.动态库的制作
• 动态库(.so):程序在运⾏的时候才去链接动态库的代码,多个程序共享使⽤库的代码。
• ⼀个与动态库链接的可执⾏⽂件仅仅包含它⽤到的函数⼊⼝地址的⼀个表,⽽不是外部函数所在⽬标⽂件的整个机器码
• 在可执⾏⽂件开始运⾏以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
• 动态库可以在多个程序间共享,所以动态链接使得可执⾏⽂件更⼩,节省了磁盘空间。操作系统采⽤虚拟内存机制允许物理内存中的⼀份动态库被要⽤到该库的所有进程共⽤,节省了内存和磁盘空间。

gcc -fPIC -c *.c

gcc -shared -o libmyc.so *.o

shared:表示生成共享库格式
fPIC:产生位置无关码(position independent code)
库名规则:libxxx.so
当我们编译时,我们发现 undefined

我们链接库

![]()
我们为什么不能执行呢
ldd:查看可执行程序链接库
发现 libmyc.so为not found,我们告诉了gcc我们库的位置,而系统不等于库,系统不知道库的位置,所以不可行

3.1解决库路径搜索问题
3.1.1 拷贝库
![]()
系统执行程序回到/lib64找库文件
3.1.2 软链接

3.1.3 添加环境变量


3.1.4加载配置文件
ls /etc/ld.so.conf.d(ld:加载 so:动态库 :conf:配置文件)
![]()
在/etc/ld.so.conf.d创建文件,将库目录写入

刷新系统配置文件
![]()
当我们同时存在动静态库时,可执行程序会使用动态链接
3.2外部库
// 安装
// Centos
$ sudo yum install -y ncurses-devel // ubuntu$ sudo apt install -y libncurses-dev
#include <stdio.h>
#include <string.h>
#include <ncurses.h>
#include <unistd.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()
{
initscr();
start_color();
init_pair(1, COLOR_GREEN, COLOR_BLACK); // 已完成部分:绿⾊前景,⿊⾊背景
init_pair(2, COLOR_RED, COLOR_BLACK); // 剩余部分(虽然⽤红⾊可能不太合适,但为演⽰⽬的):红⾊背景
cbreak();
noecho();
curs_set(FALSE);
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);
box(win, 0, 0); // 加边框
wrefresh(win);
int progress = 0;
int max_progress = PROGRESS_BAR_WIDTH;
while (progress <= max_progress)
{
werase(win); // 清除窗⼝内容
// 计算已完成的进度和剩余的进度
int completed = progress;
int remaining = max_progress - progress;
// 显⽰进度条
int bar_x = BORDER_PADDING + 1; // 进度条在窗⼝中的x坐标
int bar_y = 1; // 进度条在窗⼝中的y坐标(居中)
// 已完成部分
attron(COLOR_PAIR(1));
for (int i = 0; i < completed; i++)
{
mvwprintw(win, bar_y, bar_x + i, "#");
}
attroff(COLOR_PAIR(1));
// 剩余部分(⽤背景⾊填充)
attron(A_BOLD | COLOR_PAIR(2)); // 加粗并设置背景⾊为红⾊(仅⽤于演⽰)
for (int i = completed; i < max_progress; i++)
{
mvwprintw(win, bar_y, bar_x + i, " ");
}
attroff(A_BOLD | COLOR_PAIR(2));
// 显⽰百分⽐
char percent_str[10];
snprintf(percent_str, sizeof(percent_str), "%d%%", (progress * 100) / max_progress);
int percent_x = (WINDOW_WIDTH - strlen(percent_str)) / 2; // 居中显⽰
mvwprintw(win, WINDOW_HEIGHT - 1, percent_x, percent_str);
wrefresh(win); // 刷新窗⼝以显⽰更新
// 增加进度
progress += PROGRESS_INCREMENT;
// 延迟⼀段时间
usleep(DELAY);
}
// 清理并退出ncurses模式 delwin(win);
endwin();
return 0;
}
gcc -o main main.c -lncurses(此时要链接所需要的库)
4.目标文件

// hello.c
#include <stdio.h>
void run();
int main()
{
printf("hello world!\n");
run();
return 0;
}
![]()
可以看到,在编译之后会生成两个扩展名为.○的文件,它们被称作目标文件。要注意的是如果我们修改了一个原文件,那么只需要单独编译它这一个,而不需要浪费时间重新编译整个工程。目标文件是一个二进制的文件,文件的格式是ELF,是对二进制代码的一种封装。
![]()
## file命令⽤于辨识⽂件类型。
5.ELF
要理解编译链链接的细节,我们不得不了解一下ELF文件。其实有以下四种文件其实都是ELF文件:
- 可重定位文件(RelocatableFile)」:即xxx.o文件。包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。
- 可执行文件(ExecutableFile):即可执行程序。
- 共享目标文件(Shared Object File)」:即xxx.so文件。
- 内核转储(coredumps),存放当前进程的执行上下文,用于dump信号触发。
- 一个ELF文件由以下四部分组成:
- ELF头(ELFheader):描述文件的主要特性。其位于文件的开始位置,它的主要目的是定位文件的其他部分。
- 程序头表(Program header tabLe)」:列举了所有有效的段(segments)和他们的属性。表里记着每个段的开始的位置和位移(offset)、长度,毕竟这些段,都是紧密的放在二进制文件中,需要段表的描述信息,才能把他们每个段分割开。
- 节头表(Section header table):包含对节(sections)的描述。
- 节(Section):ELF文件中的基本组成单位,包含了特定类型的数据。ELF文件的各种信息和数据都存储在不同的节中,如代码节存储了可执行代码,数据节存储了全局变量和静态数据等。
最常见的节:
代码节(.text):用于保存机器指令,是程序的主要执行部分。
数据节(.data):保存已初始化的全局变量和局部静态变量。

6.ELF从形成到加载轮廓
6.1 ELF形成可执行
- step-1:将多份c/C++源代码,翻译成为目标.o文件
- step-2:将多份·o文件section进行合并

6.2 ELF可执行文件加载
- 一个ELF会有多种不同的Section,在加载到内存的时候,也会进行Section合并,形成segment
- 合并原则:相同属性,比如:可读,可写,可执行,需要加载时申请空间等.
- 这样,即便是不同的Section,在加载到内存中,可能会以segment的形式,加载到一起
- 很显然,这个合并工作也已经在形成ELF的时候,合并方式已经确定了,具体合并原则被记录在了ELF的 程序头表(Program header table)中
查看可执行程序的section

查看section合并的segment

- Section合并的主要原因是为了减少页面碎片,提高内存使用效率。如果不进行合并:假设页面大小为4096字节(内存块基本大小,加载,管理的基本单位),如果.text部分为4097字节,.init部分为512字节,那么它们将占用3个页面,而合并后,它们只需2个页面。
- 此外,操作系统在加载程序时,会将具有相同属性的section合并成一个大的segment,这样就可以实现不同的访问权限,从而优化内存管理和权限访问控制。
6.3 程序表头和节表头
链接视图(Linkingview)」-对应节头表 Sectionheader table
- 件结构的粒度更细,将文件按功能模块的差异进行划分,静态链接分析的时候一般关注的是链接视图,能够理解ELF文件中包含的各个部分的信息。
- 为了空间布局上的效率,将来在链接目标文件时,链接器会把很多节(section)合并,规整成可执行的段(segment)、可读写的段、只读段等。合并了后,空间利用率就高了,否则,很小的很小的一段,未来物理内存页浪费太大(物理内存页分配一般都是整数倍一块给你,比如4k),所以,链接器趁着链接就把小块们都合并了。

从链接视图来看:
- 命令readelf-Shello.o 可以帮助查看ELF文件的节头表。
- .text节:是保存了程序代码指令的代码节。
- .data节:保存了初始化的全局变量和局部静态变量等数据。
- rodata节:保存了只读的数据,如一行C语言代码中的字符串。由于.rodata节是只读的,所以只能存在于一个可执行文件的只读段中。因此,只能是在text段(不是data段)中找到.rodata节。
- BSS节:为未初始化的全局变量和局部静态变量预留位置
.Symtab节:SymbolTable符号表,就是源码里面那些函数名、变量名和代码的对应关系。- got.plt节(全局偏移表-过程链接表):·got节保存了全局偏移表。·got节和.plt节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。
- 使用readelf 命令查看.so文件可以看到该节。

7.理解连接与加载
7.1静态链接
无论是自己的.o还是静态库的.o,本质是把.o文件进行连接的过程
1.两个.0的代码段合并到了一起,并进行了统一的编址
2.链接的时候,会修改.o中没有确定的函数地址,在合并完成之后,进行相关call地址,完成代码调用
• objdump -d 命令:将代码段(.text)进⾏反汇编查看
• hello.o 中的 main 函数不认识 printf和run 函数
• code.o 不认识 printf 函数
链接其实就是将编译之后的所有目标文件连同用到的一些静态库运行时库组合,拼装成一个独立
的可执行文件。其中就包括我们之前提到的地址修正,当所有模块组合在一起之后,链接器会根据我们的.o文件或者静态库中的重定位表找到那些需要被重定位的函数全局变量,从而修正它们的地址。这其实就是静态链接的过程。

.o文件中的符号会在链接过程中进行地址重定位
8.ELF加载与进程地址空间
8.1虚拟地址与逻辑地址
一个ELF程序,在没有被加载到内存的时候,本来就有地址,当代计算机工作的时候,都采用"平坦模式"进行工作。所以也要求ELF对自己的代码和数据进行统一编址,下面是objdump -S 反汇编
之后的代码

最左侧的就是ELF的虚拟地址,其实,严格意义上应该叫做逻辑地址(起始地址+偏移量),但是我们
认为起始地址是0.也就是说,其实虚拟地址在我们的程序还没有加载到内存的时候,就已经把可执
行程序进行统一编址了.
程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪里来的?从ELF各个
segment来,每个segment有自己的起始地址和自己的长度,用来初始化内核结构中的[start,end]
等范围数据,另外在用详细地址,填充页表。
所以:虚拟地址机制,不光光OS要支持,编译器也要支持.
8.2重新理解进程虚拟地址空间
ELF在被编译好之后,会把自已未来程序的入口地址记录在ELFheader的Entry字段中:


解释一下大体流程:一开始一个ELF可执行程序在磁盘中时(还未加载到内存中时)就已经有虚拟地址了;然后程序要加载到内存中,于CPU中的EIP(程序计数器:当前正在执行的指令的下一条指令的地址)读取程序入口地址,先根据 CR3(控制寄存器,主要用于内存管理,特别是与分页机制)寄存器中保存的当前进程的页目录表地址,找到页目录表,然后通过页目录表和页表来建立虚拟地址与物理地址的映射关系,这里是用MMU(用于管理虚拟存储器和物理存储器的控制部件)将虚拟地址转换为物理地址。同时MMU将物理地址传给物理内存;当CPU 根据程序执行的需要向物理内存请求数据,物理内存响应请求将数据传给 CPU,这样就形成了闭环!!!

8.2.1单进程如何看到动态库
8.2.2多进程共享动态库

8.3 动态链接
8.3.1 动态库优于静态库的原因
对于库来讲,动态库是要常用的多,静态库会导致生成的文件太大,相当浪费内存资源。随着软件复杂度的提升,我们的操作系统也越来越臃肿,不同的软件就有可能都包含了相同的功能和代码,显然会浪费大量的硬盘空间。
这个时候,动态链接的优势就体现出来了,我们可以将需要共享的代码单独提取出来,保存成一个独立的动态链接库,等到程序运行的时候再将它们加载到内存,这样不但可以节省空间,因为同一个模块在内存中只需要保留一份副本,可以被不同的进程所共享。
8.3.2 动态库如何工作的
动态链接实际上将链接的整个过程推迟到了程序加载的时候。比如我们去运行一个程序,操作系统会首先将程序的数据代码连同它用到的一系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,操作系统会根据当前地址空间的使用情况为它们动态分配一段内存。当动态库被加载到内存以后,一旦它的内存地址被确定,我们就可以去修正动态库中的那些函数跳转地址了。
8.3.3程序与库的映射
- 动态库也是一个文件,要访问也是要被先加载,要加载也是要被打开的
- 让我们的进程找到动态库的本质:也是文件操作,不过我们访问库函数,通过虚拟地址进行跳转访问的,所以需要把动态库映射到进程的地址空间中

8.3.4 如何进行库的调用
- 库已经被我们映射到了当前进程的地址空间中
- 库的虚拟起始地址我们也已经知道了
- 库中每一个方法的偏移量地址我们也知道
- 所有:访问库中任意方法,只需要知道库的起始虚拟地址+方法偏移量即可定位库中的方法
- 而且:整个调用过程,是从代码区跳转到共享区,调用完毕在返回到代码区,整个过程完全在进程地址空间中进行的






502

被折叠的 条评论
为什么被折叠?



