C/C++动静态库的制作与原理 -- 静态库,动态库,目标文件,ELF文件,动态链接,静态链接

C中使用动态库和静态库 静态库:库在编译时链接到可执行文件中,生成的程序体积大,执行时不依赖外部库,可以独立运行。动态库:库在运行时加载,生成的程序体积小,但执行时依赖外部库,程序运行时需要找到库文件。如果你希望程序可以独立运行,避免外部依赖,使用静态库。如果你希望程序体积小且可以随时更新库文件而不重新编译程序,使用动态库。 阅读详情
AI摘要

本文详细介绍了C/C++中静态库和动态库的制作过程、使用方法以及它们的原理。内容包括静态库和动态库的定义、制作、使用,目标文件和ELF文件的结构,以及静态链接和动态链接的区别和过程。通过实例演示了如何创建和使用静态库与动态库,并解释了ELF文件的形成、加载过程和进程地址空间的管理。

目录

1. 什么是库

2. 静态库

2.1 静态库的制作

2.2 静态库的使用

3. 动态库

3.1 动态库的制作

3.2 动态库的使用

4. 目标文件

5. ELF文件

6. ELF从形成到加载轮廓

6.1 ELF形成可执行

7.2 ELF可执行文件加载

7. 理解链接和加载

7.1 静态链接

7.2 ELF加载与进程地址空间

7.2.1 虚拟地址与逻辑地址

7.2.2 重新理解进程虚拟地址空间

7.3 动态链接与动态库加载

7.3.1 进程如何使用动态库

7.3.2 进程间如何共享库

7.3.3 动态链接

7.3.3.1 概要

7.3.3.2 可执行程序被编译器动了手脚

7.3.3.3 动态库中的相对地址

7.3.3.4 程序怎么和库具体映射起来

7.3.3.5 程序怎么进行库函数调用

7.3.3.6 全局偏移量表GOT(global offset table)

7.3.3.7 库间依赖

7.3.4 总结 


1. 什么是库

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

        本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:
                静态库:.a文件 [Linux],.lib文件 [windows]

                动态库:.so文件 [Linux],.dll文件 [windows]

2. 静态库

        静态库(.a):本质就是对源文件对应的 .o 文件进行一个打包,打包成一个 .a 文件。程序在编译链接的时候把 .a 文件链接到可执行文件中,程序运行的时候将不再需要静态库。

        静态库的命名:规定上以 lib 开头,以 .a 结尾,中间部分就是库的名字。

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

2.1 静态库的制作

        下面做一个实验,演示静态库的制作过程,先给出预备的使用的代码:

//mystdio.h

#pragma once

//定义C标准库缓冲区大小
#define MAX 1024

// 三种缓冲类型标志位
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2

//使用mFILE结构体模拟C标准库中的FILE结构体
typedef struct IO_FILE
{
    int flag;
    int fileno;
    char outbuffer[MAX];
    int size;
}mFILE;

// mfopen == fopen, mfwrite == fwrite, mfflush == fflush, mfclose == fclose
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);
//mystring.h

#pragma once

int my_strlen(const char *s);
//mystdio.c

#include "mystdio.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.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_WRONLY | O_CREAT | 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 == NULL)
    {
        close(fd);
    }

    mf->fileno = fd;
    mf->flag = FLUSH_LINE;
    mf->size = 0;

    return mf;
}

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->outbuffer[stream->size - 1] == '\n')
    {
        mfflush(stream);
    }

    return num;
}
void mfflush(mFILE *stream)
{
    //写到文件内核级缓冲区中
    if (stream->size > 0)
    {
        write(stream->fileno, stream->outbuffer, stream->size);
    }
    fsync(stream->fileno);
    stream->size = 0;
}
void mfclose(mFILE *stream)
{
    if (stream->size > 0)
    {
        mfflush(stream);
    }
    close(stream->fileno);
    free(stream);
}
//mystring.c

#include "mystring.h"
#include <stdio.h>

int my_strlen(const char *s)
{
    const char *start = s;
    while(*s)
    {
        s++;
    }

    return s - start;
}

         步骤1:形成库需要的 .o 文件

        步骤2:使用命令 ar -rc [静态库.a] [*.o] 进行静态库的生成。 ar是gnu归档工具。

2.2 静态库的使用

        任意目录下新建 main.c ,内容如下

// main.c

#include "mystdio.h"
#include "mystring.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
    mFILE *fp = mfopen("log.txt", "w");
    if (fp == NULL)
    {
        return 1;
    }
    int cnt = 10;
    while(cnt--)
    {
        printf("write %d\n", cnt);
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "hello xiaoc, cnt is : %d\n", cnt);
        mfwrite(buffer, strlen(buffer), fp);
        // mfflush(fp);
        sleep(1);
    }

    mfclose(fp);
    return 0;
}

        使用命令将 main.c 文件编译为 main.o 文件,并将 mystdio.h,mystring.h,libmyc.a 文件复制到该目录下。

        使用命令 gcc -o main main.o -L. -lmyc 将main.o 和 静态库 libmyc.a链接起来形成可执行文件 main。

        执行文件 ./main,下列内容表示程序可正常执行,说明自己写的静态库是可以使用的。 

        -L:指定库路径,上述 . 表示指定当前路径。

        -l:指定库名称,库名称是静态库文件去掉开头的 lib 和结尾的 .a 的中间部分。 这个l是小写的“L”。

        -I:指定头文件搜索路径,这里头文件在当前目录中,所以不用指定。这个I是大写的“i”。

        测试目标文件生成后,静态库删除,程序照样可以运行。如果要链接任何非C/C++标准库,都需要指明 -L -l。

        C/C++标准库链接的时候不用指定头文件路径和库路径,是因为在安装gcc/g++的时候,就已经把标准库的头文件和库文件复制到了系统中指定的路径下了,所以在使用的时候不需要指定头文件路径和库文件路径。C/C++标准库编译的时候也不需要指定库名称,因为gcc/g++是C语言和C++的编译器,它们认识C/C++标准库。

        所以在链接第三方库的时候不想指定头文件和库文件的路径时,也可以把第三方库的头文件和库文件拷贝到Linux系统中的指定文件中,但是gcc/g++不认识第三方库,所以就算这样链接的时候也需要指定库的名称。

        这里给出一个makefile脚本,创建一个lib目录,lib目录中用include目录和mylib目录,分别存放静态库的头文件和库文件,并打包成压缩包。

libmyc.a:mystdio.o mystring.o
	ar -rc $@ $^
mystdio.o:mystdio.c
	gcc -c $<
mystring.o:mystring.c
	gcc -c $<

.PHONY:output
output:
	mkdir -p lib/include
	mkdir -p lib/mylib
	cp -f *.h lib/include
	cp -f *.a lib/mylib
	tar czf lib.tgz lib

.PHONY:clean
clean:
	rm -rf *.o libmyc.a lib lib.tgz

3. 动态库

        动态库(.so):本质也是对源文件对应的 .o 文件进行一个打包,打包成一个可执行的 .so 文件。程序运行的时候才去链接动态库,多个程序共享使用库的代码。

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

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

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

3.1 动态库的制作

        步骤1:将 .c 源文件编译成 .o 文件,编译的使用需要带 -fPIC选项。

        步骤2:使用 gcc -shared -o libmyc.so *.o 命令,形成动态库 .so 文件。 

        shared:表示生成共享库格式

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

        库名规则:libxxx.so 

        可以看到该文件是一个动态链接文件

3.2 动态库的使用

        动态库的使用和静态库的使用相同,需要指定头文件路径,库文件路径以及链接的库名称。但是直接运行可执行程序的时候却,报错找不到动态库。

        解决方案1:将头文件和库文件安装到系统路径下。

        解决方案2:在系统存储库文件的路径下,建立与第三方库的软链接。

       解决方案3:给环境变量 LD_LIBRARY_PATH 添加上第三方库文件的路径,程序运行时会在系统路径中查找动态库,也会在该环境变量中的路径中查找动态库。

        将上述环境变量写入相关配置文件中,就可以使其永久可以找到第三方库了。 

        解决方案4:在 /etc/ld.so.conf.d/ 目录中任意创建一个以.conf结尾的配置文件。然后将第三方库的路径复制到该文件中,使用 ldconfig 刷新一下,这样就能找到动态库了。

知识点1:

        gcc/g++默认使用动态库链接。当动态库和静态库都存在的时候,非要静态链接,就在链接的时候加选项 -static。只存在动态库的时候,不能进行静态链接。只存在静态库的时候,链接的时候都只能静态链接。

知识点2:
        在Linux系统下,默认情况安装的大部分库,都优先安装的是动态库。

4. 目标文件

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

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

        编译的过程其实就是将我们程序的源代码翻译成CPU能够直接运行的机器代码。在编译之后会生成拓展名为 .o 的文件,它们被称作目标文件。 要注意的是如果修改了一个源文件,那么只需要单独编译它这一个,而不需要浪费时间重新编译整个工程。目标文件是一个二进制文件,文件格式是ELF,是对二进制代码的一种封装。

        这里给出两份代码hello.c 和code.c,通过编译,编译出hello.o 和 code.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");
}

        使用 file [文件名] 命令查看文件,可以看到目标文件是ELF格式的文件 

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):保存已初始化的全局变量和局部静态变量。

6. ELF从形成到加载轮廓

6.1 ELF形成可执行

        步骤1:将多份C/C++源代码,翻译成目标.o文件。

        步骤2:将多份 .o 文件section进行合并。

        实际合并是在链接时进行的,但是并不是这么简单的合并,也会涉及对库的合并。 

7.2 ELF可执行文件加载

        一个ELF会有多种不同的Section,在加载到内存的时候,也会进行Section合并,形成Segment。

        合并原则:根据相同属性,比如:可读,可行,可执行,需要加载时申请空间等将section合并为segment。

        即使是不同的Section,在加载到内存中,可能会以segment形式,加载到一起。

        很显然,这个合并工作也已经在形成ELF的时候,合并方式就已经确定了,具体合并原则被记录在了ELF程序头表中。

        使用命名 readelf -S [ELF文件名] 查看文件的section信息:

        可以看到ls命令可执行程序,有31个section,并且每一个section的信息以类似于数组的方式存储起来。

        使用命令 readelf -l [ELF文件名] 查看文件的segment信息:

        可以看到 ls 程序里面有13个segement,在05下标对应的segment中,将 .data 和 .bss节合并到一个segment中,.data存储的是数据,如全局变量和局部静态变量,.bss中存储的是全局未初始化变量,都是属于数据,所以合并到了一个segment中。上述每一个segment后面都有相关RWX的标志位,表示该segment的权限属性。

        对于 程序头表 和 节头表,其实ELF文件提供2个不同的视图/视角来让我们理解这两个部分:

        链接视图(Linking View)-- 对应节头表(Section Header Table)

                文件结构的粒度更细,将文件按功能模块的差异进行划分,静态链接分析的时候一般关注的是链接视图,能够理解ELF文件中包含的各个部分的信息。

                为了空间布局上的效率,在链接目标文件时,链接器会把很多节合并,规整成可执行的段、可读写的段、只读段等。合并后,空间利用率就高了,否则,对于物理内存页浪费太大(物理内存页分配一般都是整数倍一块一块分配的,比如4K)。

        执行视图(Execution View)-- 对应程序头表(Program Header Table)

                告诉操作系统如何加载可执行文件,完成进程内存的初始化。一个可执行程序的格式中,一定有 program header table。

        section header table 在链接时作用,program header table 在加载时作用。

        上图右边的第一个图,对应的是分段的图,通过RWX属性将每个section分为几个段。第二个图就是每个section的分布图。可以看到上图中.init和.text分到了一个段中,.data自己分为一个段,.rodata和.got分到一个段。 

        从链接视图来看:

                .text节:是保存了程序代码指令的代码节。

                .data节 :保存了初始化的全局变量和局部静态变量等数据。

                .rodata节 :保存了只读的数据,如⼀⾏C语⾔代码中的字符串。由于.rodata节是只读的,所以只能存在于⼀个可执⾏⽂件的只读段中。因此,只能是在text段(不是data段)中找到.rodata 节。

                .BSS节 :为未初始化的全局变量和局部静态变量预留位置。

                .symtab节 : Symbol Table 符号表,就是源码⾥⾯那些函数名、变量名和代码的对应关系。

                .got.plt节 (全局偏移表-过程链接表):.got节保存了全局偏移表。.got节和.plt节⼀起提供了对导⼊的共享库函数的访问⼊⼝,由动态链接器在运⾏时进⾏修改。

        从执行视图来看:

                告诉操作系统哪些模块可以被加载进内存。加载进内存之后哪些分段是可读可写,哪些分段是只读,哪些分段是可执⾏的。

        可以在ELF头中找到文件的基本信息,以及看到ELF头如何定位程序头表和节头表的。使用命令 readelf -h [ELF文件名] 查看ELF头的信息:

        对于ELF header部分,只用知道其作用即可,它的主要目的是定位文件的其他部分。 

知识点1:

        为什么要将section合并成segment?

                section合并的主要原因是为了减少页面碎片,提高内存使用效率。如果不进行合并,假设页面大小为4096字节(内存块基本大小,加载,管理的基本单位),如果 .text 部分为4097字节,.init 部分为512字节,那么将占用3个页面。而合并后,加载是将一个segment同时加载,这两个部分只需要2个页面。

                此外,操作系统在加载程序时,会将具有相同属性的 section 合并成一个大的 segment ,这样就可以实现不同的访问权限,从而优化内存管理和权限访问控制。

7. 理解链接和加载

7.1 静态链接

        无论是自己的.o文件,还是静态库中的.o文件,本质都是把.o文件进行链接的过程,所以静态链接本质就是研究.o文件是如何链接的。

        这里将上述的编译过的hello.o和code.o进行反汇编查看,使用 objdump -d [目标文件名] 对目标文件进行反汇编:

        上图是code.s的内容,这里的e8表示函数调用的机器码,后面的0表示函数地址。 这个函数调用对应着代码中printf的调用。

        上图是hello.s的内容,这里的函数地址也是0。 这里的函数调用对应着代码中printf和run的调用。

        可以从上看出,光是编译的情况下,hello.o中是不认识code.o中的函数的,也不认识C标准库中的函数。因此,编译器只能将函数的跳转地址先暂时设为0。

        这个地址会在链接的时候进行修正。为了让链接器将来在链接时能够正确定位到这些被修正的地址,在代码节(.data)中还存在一个重定位表,在链接的时候就会根据表里记录的地址将其修正。

        使用readelf -s code.o,读取code.o的符号合集,这里可以看到printf函数是未被定义的(UND表示未被定义,puts就是printf的实现)。

        使用readelf -s hello.o,可以看到run和printf都是未被定义的。 

        将这两个.o文件通过链接,链接成可执行文件exe,这是再使用 readelf -s exe 进行查看,就可以看到run函数有了对应的地址。printf是动态链接的,这里不做说明。而这里run前面的16表示在exe程序的第16节中,使用 readelf -S exe 可以看到这个节是 .text节。这表示了它属于代码节。

         使用 objdump -d exe > exe.s,在exe.s中的main中可以看到printf和run的地址被进行了修改。

       静态链接:1.将所有.o和静态库中的.o文件的section合并成segment。2. 将函数的地址进行修正。

7.2 ELF加载与进程地址空间

7.2.1 虚拟地址与逻辑地址

        一个ELF可执行程序,在没有被加载到内存的时候,本身就有地址,当代计算机工作的时候,都采用“平坦模式”进行工作。所以也要求ELF对自己的代码和数据进行统一编制。下面是对exe.s中的部分内容。

        平坦模式:就是可执行程序中的所以segment都是从0开始依次递增的统一编制。

        最左侧的就是ELF的虚拟地址,其实严格意义上应该叫做逻辑地址(起始地址+偏移量),但是平坦模式下,起始地址为0,所以起始虚拟地址在我们的程序还没有加载到内存的时候,就已经把可执行程序进行统一编制了。 

        进程的mm_struct,vm_area_struct在进程刚刚创建的时候,初始化的数据就是从ELF各个segment来的,每个segment有自己的起始地址和自己的长度,用来初始化内核结构中vm_area_struct的[start, end] 等范围数据,另外在将虚拟地址和物理地址的映射填入页表当中。

        所以虚拟地址机制不仅仅操作系统要支持,编译器也要支持。

7.2.2 重新理解进程虚拟地址空间

        ELF被编译好之后,会把自己未来程序的入口地址记录在ELF header的Entry point address字段当中。

        所以一个可执行程序在Linux系统中的加载过程如下:

        步骤1: 在 bash 进程中,通过命令行的形式输入可执行程序的文件名,识别到是一个可执行程序之后,在 bash 进程中打开了可执行程序的文件,并且创建可执行程序的进程。

        步骤2:在这个进程中通过 dentry 结构体找到文件的 inode,找到 inode 之后,加载文件的属性,并且通过 inode 和 data blocks 的映射关系,找到数据和代码,将其放入物理内存中。

        步骤3:然后初始化 mm_struct 和 vm_area_struct 结构体的[start, end],并且填充页表。

        步骤4:将程序的入口地址加载到 cpu当中,此时 cpu 进行调度然后运行该进程。cpu中有个EIP字段,里面存放的是当前指令的下一条指令的地址,这样就可以通过入口地址依次运行整个程序了。

知识点1:

        在计算机中,磁盘上的逻辑地址就是进程中的虚拟地址,cpu使用的也是虚拟地址。

7.3 动态链接与动态库加载

7.3.1 进程如何使用动态库

        下面对上图进行说明:

                如果一个可执行程序的进程要调用动态库,首先需要先将动态库的代码和数据加载到物理内存当中,其次通过页表将动态库的物理地址映射到进程的虚拟地址空间的共享区上,然后在通过代码区的代码调用动态库中函数的地址,进行地址跳转调用动态库中的库函数。

7.3.2 进程间如何共享库

        如果两个进程同时使用一个动态库,就是将一份动态库的物理地址通过两个页表分别映射到两个进程的虚拟地址空间的共享区中。

        所以在多个进程使用同一个动态库的时候,动态库只需要加载一份在内存中,减少了内存的浪费。 

7.3.3 动态链接

7.3.3.1 概要

        动态链接远比静态链接要常用得多。使用 ldd [可执行程序文件名] 就可以查看该可执行程序链接的动态库。

        编译器默认不使用静态链接,是因为静态链接最大的问题在于生成的文件体积大,并且相当耗费内存资源。 

        动态链接的优势就是,我们可以将需要共享的代码单独提取出来,保存成一个独立的动态库,等到程序加载的时候再将它们加载到内存,这样不但可以节省磁盘和内存的空间,而且可以被不同的进程所共享。

        首先,动态链接实际上是将链接的整个过程推迟到了程序加载的时候。比如运行一个程序,操作系统会首先将程序的数据和代码连同使用到的一系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,操作系统会根据当前地址空间的使用情况为它们动态分配一段内存。当动态库被加载到内存之后,一旦它的内存地址被确定,就可以去修正动态库中库函数在进程虚拟地址空间中的函数跳转地址了。

7.3.3.2 可执行程序被编译器动了手脚

         在C/C++程序中,当程序开始执行时,首先并不会直接跳转到main函数。实际上,程序的入口点是_start,这是一个由C运行时库(通常是glibc)或链接器(如ld)提供的特殊函数。上图中就是Linux中的一个链接器。

        在_start函数中,会执行一系列初始化操作,包括如下:

                1. 设置堆栈:为程序创建一个初始的堆栈环境。

                2. 初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段中复制到相应的内存位置,并清零未初始化的数据段。

                3. 动态链接:_start函数会调用动态链接器的代码来解析和加载程序所依赖的动态库(shared libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调用和变量范围能够正确的映射到动态库中的实际地址。

        动态链接器(如ld-linux.so):负责在程序运行时加载动态库。当程序启动时,动态链接器会解析程序中的动态库依赖,并加载这些库到内存中。

        环境变量和配置文件:Linux系统通过环境变量(如LD_LIBRARY_PATH)和配置文件(如/etc/ld.so.conf及其子配置文件)来指定动态库的搜索路径。这些路径会被动态链接器在加载动态库时搜索。

        缓存文件:为了提高动态库的加载效率,Linux系统会维护一个名为/etc/ld.so.cache的缓存文件。该文件包含了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会首先搜索这个缓存文件。

                4. 调⽤ __libc_start_main :⼀旦动态链接完成, _start 函数会调⽤__libc_start_main (这是glibc提供的⼀个函数)。 __libc_start_main 函数负责执⾏⼀些额外的初始化⼯作,⽐如设置信号处理函数、初始化线程库(如果使⽤了线程)等。

                5. 调⽤ main 函数:最后, __libc_start_main 函数会调⽤程序的 main 函数,此时程序的执⾏控制权才正式交给⽤⼾编写的代码。

                6.处理 main 函数的返回值:当 main 函数返回时, __libc_start_main 会负责处理这个返回值,并最终调⽤ _exit 函数来终⽌程序。

7.3.3.3 动态库中的相对地址

        动态库为了随时进行加载,为了支持并映射到任意进程的任意位置,对动态库中的方法的统一编址,在进程虚拟地址空间中采用相对编址的方案进行编址的(动态库的编址和可执行程序一样,都要遵守平坦模式)。

        在动态库中的编址还是起始地址(0)+ 偏移量,由于起始地址为0,所以偏移量就是动态库的编址。映射到进程的虚拟地址空间时,在进程的虚拟地址空间中,动态库的库函数地址就是动态库在进程虚拟地址空间中的起始地址,加上动态库的库函数的偏移量,这被称为相对编址。

7.3.3.4 程序怎么和库具体映射起来

        动态库也是一个文件,要被访问也是要先被加载,要被加载就是要被打开的,让进程找到动态库的本质:就是文件操作,不过我们访问库函数,通过虚拟地址进行跳转访问的,所以需要把动态库映射到进程的地址空间中。

        首先进程打开动态库,通过动态库 struct_file 结构体找到 dentry 然后找到动态库的 inode,将动态库的属性,数据和代码加载到内存中,然后为动态库开辟一个 vm_area_struct 并初始化,最后再将动态库的物理地址通过页表映射到进程的虚拟地址空间中。 

7.3.3.5 程序怎么进行库函数调用

        库已经被映射到了当前进程的虚拟地址空间中,库的在进程虚拟地址空间中的起始地址也知道,库中每一个方法的偏移量也知道,所以访问库中的任意方法,只需要知道 库的起始虚拟地址 + 库方法偏移量 就可定位库中的方法。

        而且,整个调用过程是从代码区跳转到共享区,调用完毕再返回到代码区,整个过程完全在进程虚拟地址空间中进行的。

7.3.3.6 全局偏移量表GOT(global offset table)

        程序运行之前先把所有动态库加载并映射,所有库的起始虚拟地址都提前知道,然后对我们加载到内存中的程序的库函数调用进行地址修改,在内存中二次完成地址设置(加载地址重定位)。但是修改代码区调用库函数时的函数地址不是修改代码区吗?代码区不是只读的吗?怎么进行修改?

        给出的解决方法是,动态链接采用的做法是在 .data 中专门预留一片区域用来存放函数的跳转地址,它也被叫做全局偏移量表GOT,表中每一项都是动态库模块要引用的一个全局变量或函数的地址(偏移量)。因为 .data 区域是可读写的,所以可以支持动态进行修改。

        1. 有了GOT表,代码便可以被所以进程共享。但在不同进程的虚拟地址空间中,各个库的起始虚拟地址不同。反映到GOT表上,就是每个进程的每个动态库都有独立的GOT表,所以进程间不能共享GOT表。 

        2. 在单个.so下,由于 GOT 表与 .text 的相对位置是固定的,所以完全可以利用相对寻址来找到GOT表。

        3. 在调用函数的时候会首先查GOT表,然后根据表中的地址来进行跳转,这些地址在动态库加载的时候会被修改为真正的地址。

        4.这种方式实现的动态链接被叫做PIC地址无关代码。换句话说,我们的动态库不需要做任何修改,被加载到任意内存地址都能够正常运行,并且能够被所有进程共享,这就是为什么之前给编译器指定 -fPIC 参数的原因,PIC = 相对编址 + GOT。

7.3.3.7 库间依赖

        不仅仅可执行程序调用库,库也会调用其他库。库之间是有依赖的,库中也有.got。

         由于动态链接在程序加载的时候需要对大量函数进行重定位,这一步显然是非常耗时的。为了进一步降低开销,操作系统还做了一些其他的优化,比如延迟绑定,或者也叫做PLT(过程链接表)。与其在程序一开始就对所有函数进行地址重定位,不如将这个过程推迟到函数第一次被调用的时候,因为绝大多数动态库中的函数可能在程序运行期间一次都不会被使用到。

        动态链接实际上将链接的整个过程,比如符号查询,地址的重定位从编译时推迟到程序运行时,虽然牺牲了一定的性能和程序加载时间,但动态链接能够更有效的利用磁盘空间和内存资源,以极大方便了代码的更新和维护,更关键的是,它实现了二进制级别的代码复用。

知识点1:
        解析依赖关系的时候,就是加载并完善相互之间的GOT表的过程。

7.3.4 总结 

        1. 静态链接提⾼了程序的模块化⽔平。对于⼀个⼤的项⽬,不同的⼈可以独⽴地测试和开发⾃⼰的模块。通过静态链接,将自己的demo代码和静态库链接起来⽣成可执⾏⽂件,进行自己模块的开发测试。

        2. 静态链接会将编译产⽣的所有⽬标⽂件,和⽤到的各种库合并成⼀个独⽴的可执⾏⽂件,
其中我们会去修正模块间函数的跳转地址,也被叫做编译重定位(也叫做静态重定位)。

        3.动态链接是将链接的整个过程推迟到了程序加载的时候。运⾏⼀个程序时,操作系统会⾸先将程序的数据代码连同它⽤到的⼀系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,但是⽆论加载到什么地⽅,都要映射到进程对应的地址空间,然后通过.GOT⽅式进⾏调⽤(运⾏重定位,也叫做动态地址重定位)。

库制作与原理 库是写好的现有的,成熟的,可以复用的代码。细节:动静态库g++/gcc默认使用动态库,非静态链接只能-static,一旦-static,就必须得存在对应的静态库,只存在静态库,可执行程序对于该库,只能静态链接了。头文件包含了方法的声明,库包含了方法的实现,所以,对库的安装,其实就是将对应的头文件,库文件拷贝到系统的指定文件下,将来编译时只要指定库名称,gcc自动找头文件和库文件。在Linux系统下,默认安装大部分库,默认优先安装动态库,库:应用程序=1:n,vs不仅可以形成可执行程序,也能形成动静态库。 阅读详情

相关推荐

C/C++ 静态库(lib)的概念和使用方法

lib文件的作用及使用

m0_62599305的博客 2万+

【c/c++】静态库

1、静态库介绍 静态库是指在我们的应用中,有一些公共代码是需要反复使用,就把这些代码编译为“库”文件;在链接步骤中,连接器将从库文件取得所需的代码,复制到生成的可执行文件中的这种库。 程序编译一般需经预处理、编译、汇编和链接几个步骤。静态库特点是可执行文件中包含了库代码的一份完整拷贝;缺点就是被多次使用就会有多份冗余拷贝。 静态库和动态库是两种共享程序代码的方式,它们的区别是:静态库在程序的链接阶段被复制到了程序中,和程序运行的时候没有关系;动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动

StudyWinter的博客 6689

手机耳机麦克风(ECM)电路设计实战:从差分走线到射频干扰滤波,一个电阻引发的灵敏度问题

本文深入探讨了手机耳机麦克风(ECM)电路设计中的关键问题,从差分走线到射频干扰滤波,特别关注了一个电阻引发的灵敏度问题。通过实际案例分析,详细解析了ECM麦克风工作原理、偏置电阻选型、差分信号传输设计以及射频干扰抑制策略,为工程师提供了实用的设计指南和优化建议。

weixin_42563415的博客 244

C++(GCC)生成和使用静态库

静态库是一种可重用的代码模块集合,具有提高编译速度、代码可移植性、简化部署、减少运行时开销以及提高可靠性等优点。

mahuifa的博客 3365

ZYNQ平台上基于SGMII接口的IEEE1588/PTP协议高效实现方案

本文深入探讨了在Xilinx ZYNQ平台上,利用SGMII接口实现高精度IEEE1588/PTP协议的方案。通过将硬件时间戳单元集成在PL端的AXI以太网子系统IP核中,该方案绕过了软件协议栈和外部PHY的延迟,实现了亚微秒级的时间同步精度。文章详细解析了硬件设计、Vivado IP配置、Linux驱动移植及PTP协议栈部署等关键步骤,为工业控制、通信等领域的高精度网络同步提供了高效、灵活的解决路径。

q5r6s7的博客 606

基于Matlab与Bladed联合仿真的交互软件设计

基于Matlab与Bladed的交互软件设计

weixin_45014222的博客 816

【C语言学习笔记】:静态库

打开工程“属性面板”→”配置属性”→“C/C++”→” 常规”,在“附加包含目录”属性值中,键入StaticMath.h 头文件所在目录的路径或浏览至该目录。✪ -l:指定链接时需要的动态库,编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上lib,后面加上.a或.so来确定库的名称。还有2中方法设置都可行。① “属性面板”→”配置属性”→“链接器”→”常规”,附加依赖库目录中输入,静态库所在目录;打开工程“属性面板”→”配置属性”→ “链接器”→ ”命令行”,输入静态库的完整路径即可。

Jiangziyadizi的博客 1649

从设计到实战:手把手教你用SolidWorks完成一个简易离心泵叶轮的水力设计全流程

本文详细介绍了使用SolidWorks进行简易离心泵叶轮水力设计的全流程,涵盖参数计算、水力设计和三维建模等关键步骤。通过实际案例演示,帮助机械设计工程师和学生掌握从理论到实践的转化技巧,特别适合低比转速离心泵叶轮的设计需求。

weixin_30621711的博客 236

【Linux文件系统】动静态库:从原理到制作

本文介绍了动静态库的基本概念、制作方法及使用方式。静态库(.a/.lib)通过直接拷贝代码实现到程序中,而动态库(.so/.dll)在运行时加载到内存供程序共享。制作静态库需使用ar打包目标文件,动态库则通过gcc -shared生成位置无关码。使用时需通过-I、-L、-l选项指定头文件路径、库路径及库名。动态库还涉及运行时加载问题,可通过环境变量或系统路径解决。动态库的共享特性节省了内存空间,其实现与进程地址空间密切相关。

2401_87944878的博客 1万+

envi反演水质参数_遥感水质反演之反演模型

水质反演的框架图此为反演模型的章节第一部分,统计学基础:概率论、矩阵和数理统计的知识统计学知识框架# 1 基本统计概念知识在于应用,而非记忆!我们都是在样本的层面去操作数据的,以达到对总体性质的估计和推断,拿到一组数据首先进行基本的统计描述,常用的有:样本均值: 样本方差(调整方差)不是除以n,而是n-1, 标准差为样本方差开方即可皮尔逊积矩相关系数(简称相关系数): 相关系数范围在[-1,...

weixin_35275922的博客 3229

一维光子晶体态密度计算与应用解析

态密度(DOS)是凝聚态物理和光子学中的核心概念,描述了量子态在能量空间的分布规律。基于麦克斯韦方程组的周期性介电结构分析表明,一维光子晶体通过布拉格散射形成光子带隙,其态密度曲线呈现典型的范霍夫奇点和禁带特征。这种特性使光子晶体在光学工程中具有重要价值,特别是在滤波器设计和传感器开发领域。通过转移矩阵法等数值计算手段,可以精确预测带隙位置和宽度,其中折射率对比度与周期数是关键设计参数。实际应用中,1550nm通信波段滤波器常采用Si/SiO2交替结构,而引入缺陷层则能产生高Q值缺陷模。计算优化时需注意数值

weixin_36487018的博客 193

【Linux】动静态库链接原理

详细讲解:【Linux】动静态库链接原理!

tan_run的博客 1917

YOLOv8-Pose NCNN Android部署优化实践

本文探讨了在Android设备上高效部署YOLOv8-Pose姿态检测模型的优化实践。通过修改Pose头部导出逻辑,分离后处理到C++端实现,并替换SiLU激活函数为ReLU,显著提升了计算效率。具体步骤包括模型结构优化、ONNX导出、NCNN转换与算子融合,最终在Android端实现了30FPS的稳定推理性能。项目提供了完整的优化思路和实现方案,对移动端AI应用开发具有参考价值。

FJN110的博客 325

【Linux:文件】库的制作与原理:动静态库

1、四步骤:预处理(.c→.i)、编译(.i→.s)、汇编(.s→.o)、链接(.o+库→可执行);2、三类文件:.o(ELF-REL,符号表/重定位表)、.a(.o归档,静态链接)、.so(ELF-DYN,PIC,动态链接);3、两个核心:链接的2个任务(符号解析、地址重定位);GOT/PLT(延迟绑定,优化性能);4、一个选型原则:按移植性、内存、更新频率、运行场景,选择静态库 / 动态库;

艾莉丝努力练剑的博客,感谢大家一直以来的对艾莉丝的支持和鼓励,uu们如果对C/C++方向感兴趣的话,可以通过艾莉丝主页的微信名片加艾莉丝的好友,和努力练剑的艾莉丝一起加油! 1524

手把手教你学Simulink——基于自动驾驶辅助系统的场景实例:车道保持辅助系统(LKA)的仿真与实现

本文通过Simulink实例讲解车道保持辅助系统(LKA)的实现方法。首先介绍LKA从预警到主动控制的技术演进,阐述其系统架构、核心组件和工作原理。重点演示基于自行车模型的车辆横向动力学建模过程,以及使用线性二次调节器(LQR)设计横向控制器的详细步骤。教程包含四个仿真场景测试,分析关键指标如控制精度、舒适性和响应性。最后探讨LKA技术的发展方向,强调人机协同的安全理念。该实例为自动驾驶辅助系统开发提供了完整的Simulink实践方案。

MHD0815的博客 182

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

【Linux】ELF与动静态库的“暗黑兵法”:程序是如何跑起来的?

静态库在编译时嵌入程序,动态库则运行时加载,实现多程序共享。文章解析了两种库的生成与使用,并提供了库路径问题的解决方案。通过ELF文件结构分析,揭示了程序加载的底层机制,最后对比了静态链接和动态链接的工作原理,重点讲解了动态链接中GOT/PLT的实现机制。

2401_83431652的博客 1880

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

【Linux:文件】Linux 动静态库与可执行文件深度解析

库是写好的、现有的、成熟的、可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。本质上,库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。本文系统梳理 Linux 动静态库的制作与使用、ELF 文件结构、程序加载过程等核心知识点,确保所有概念表述严谨准确,每个结论都有充分的依据支撑。静态库(.a)是指程序在编译链接的时候把库的代码链接到可执行文件中,程序运行的时候将不再需要静态库。动态库(.so。

艾莉丝努力练剑的博客,感谢大家一直以来的对艾莉丝的支持和鼓励,uu们如果对C/C++方向感兴趣的话,可以通过艾莉丝主页的微信名片加艾莉丝的好友,和努力练剑的艾莉丝一起加油! 1410

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

Linux系统学习【全面剖析动静态库制作与使用、ELF与库加载的相关内容】

本文围绕 Linux 系统中的 Ext 系列文件系统展开深入学习,系统介绍 Ext2、Ext3 和 Ext4 的发展历程、设计思想以及核心技术特点。文章将从文件系统整体架构入手,详细解析超级块、块组、inode、数据块、目录项等关键数据结构,帮助读者理解 Linux 如何组织和管理磁盘中的文件。同时结合实际应用场景,介绍日志机制、文件读写流程、文件系统挂载、容量查看、检测与修复等常用操作。通过对 Ext 系列文件系统的原理分析与实践讲解,使读者能够掌握 Linux 文件存储管理的底层逻辑。

承于所学,探技术之渊;以代码求政,寻成长之道。 1万+

【Linux学习笔记】深入理解ELF和动静态库加载原理

本文深入解析了ELF文件的结构和加载原理。ELF文件可分为四种类型:可重定位文件、可执行文件、共享目标文件和内核转储文件。ELF由四部分组成:ELF头、程序头表、节头表和节。重点介绍了ELF从编译链接到加载执行的完整过程,包括.section合并为Segment的原则,以及如何通过程序头表确定加载方式。文章还提供了查看ELF文件结构的实用命令(readelf -S/-l),帮助开发者理解可执行程序的内存布局。通过对ELF文件加载机制的剖析,读者可以更深入地掌握Linux程序运行的底层原理。

大白的编程日记. 1332

Linux的库制作与原理

程序运行时,有了库的起始虚拟地址,我们要对加载到内存中的程序的库函数调用处进行地址修改,在内存中二次完成地址设置 (这个叫做。程序运行时,加载动态库,有了库的起始虚拟地址+方法偏移量(编译时获取),定位库中的方法。等等,修改的是代码区?不是说代码区在进程中是只读的吗?的,但是并不是这么简单的合并,也会涉及对库合并,此处不做过多追究。,表中每一项都是本运行模块要引用的一个全局变量或函数的地址。所以:虚拟地址机制,不光光OS要支持,链接器也要支持。:核心的虚拟地址分配者,写入ELF文件供OS加载。

博客 | 复习知识点 + 练习 + 最终的领悟 | 全力以赴 2164

Linux《库的制作与原理》

本文介绍了Linux系统中库的制作与加载过程,包括静态库和动态库的生成、使用及区别。主要内容包括: 库的概念与分类:库是二进制可复用代码,分为静态库(.a)和动态库(.so)。 静态库制作:使用ar工具将.o文件打包,通过gcc的-l和-L选项链接使用。 动态库制作:使用gcc的-fPIC和-shared选项生成,需要设置环境变量或配置文件让系统能找到库。 ELF文件格式:详细解析了可执行文件、目标文件和库的ELF格式结构。 程序加载过程:从磁盘加载到内存,建立虚拟地址空间映射的完整流程。 动态链接原理:通

mljy.博客 2254

【Linux系统】动静态库的制作 与 原理

一、动静态库的介绍 和 使用(编译器链接时默认动态链接) 二、ELF文件格式 及 查看方法(对比各类型ELF文件 ELF头的区别) 三、理解链接与加载过程

无限进步!!! 1563

Linux系统入门:动静态库的理解与加载

本文系统介绍了编程中的库概念及其实现原理。主要内容包括:1.库的分类:静态库(.a/.lib)和动态库(.so/.dll),前者在编译时链接到可执行文件,后者在运行时加载共享;2.库的生成与使用:详细说明了静态库和动态库的生成命令及使用方法;3.ELF文件格式:深入解析了可执行文件的结构,包括节(section)合并为段(segment)的机制;4.链接加载原理:分别阐述了静态链接和动态链接的工作机制,重点分析了动态库的GOT(全局偏移量表)和PLT(过程链接表)实现原理。文章通过技术细节揭示了库在程序运行

小苹果今天也拜托你了 1177

【Linux:动静态库】Linux 动静态库与可执行文件

静态库(.a文件)是指程序在编译链接时将库的代码完整复制到可执行文件中,程序运行时不再需要外部库文件。静态库在链接阶段被直接嵌入到最终的可执行文件中,形成独立的二进制程序。动态库(.so文件)则采用不同的机制:程序运行时才去链接动态库代码。与动态库链接的可执行文件仅包含所用函数的入口地址表,而非整个目标文件的机器码。在程序执行前,操作系统将动态库从磁盘加载到内存,这个过程称为动态链接。特性静态链接动态链接文件大小较大(包含全部库代码)较小(仅包含引用信息)内存占用每个进程独立副本。

艾莉丝努力练剑的博客,感谢大家一直以来的对艾莉丝的支持和鼓励,uu们如果对C/C++方向感兴趣的话,可以通过艾莉丝主页的微信名片加艾莉丝的好友,和努力练剑的艾莉丝一起加油! 652

Linux 下 ELF 格式与程序加载机制:静态链接与动态库原理

ELF文件与动静态库加载看着一篇就够了,超详细,无论就业还是考研都能用到!速来看看!!

Zach_yuan的博客 1286

Linux动态库与静态库解析

在Linux系统开发中,库文件扮演着至关重要的角色。它们提供了程序运行所需的各种功能,使得开发者能够高效地复用代码,减少重复劳动。库文件通常分为动态库(也称为共享库)和静态库两种类型,它们在程序链接和运行阶段扮演着不同的角色。

无敌岩雀的博客 1786
上一篇: Ext系列文件系统 -- 磁盘结构,磁盘分区,inode,ext文件系统,软硬链接
下一篇: 进程间通信(Inter-Process Communication,IPC) -- 匿名管道,命名管道,System V,共享内存,消息队列,信号量
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值