程序的构建和调试之链接

目录

前言

1、简介

 (1)概念

 (2)库文件

 (3)链接的分类

 (4)使用静态链接

 (5)使用动态链接

 (6)C标准库与Linux系统调用

2、动态库的使用

 (1)简介

 (2)Windows上的导出导入

 (3)Linux上的导出导入

 (4)显式加载

 (5)使用静态库


前言

以下是我自己总结的程序构建过程中关于链接的部分,其中包括了静态链接和动态链接,顺便又记录了Windows和Linux中动态库的创建和使用方法。如果谁有建议,欢迎补充。

1、简介
 (1)概念

链接阶段负责将编译器和汇编器生成的一个或多个对象文件,以及所依赖的其它库文件,合并成一个单一的可执行文件或库文件,如Windows中的.exe 文件或Unix/Linux系统中的无后缀的可执行文件。链接过程主要包括符号解析、地址分配和重定位等,具体略。

 (2)库文件

库文件是预先编译好的代码的集合,用于实现和复用特定功能,免除重复编写代码的需要。如果没有库文件,当我们想要调用别人的代码中的某一个函数,就需要引入该函数所在的头文件和源文件。但这样一是会暴露源码,二是不方便控制。因此,出现了库文件。它将一个或多个源文件中的代码编译到一起,使得我们只需要引入头文件和少量的库文件就可以使用他人的代码。库文件将编译后的代码封装,增强了代码的模块化和保密性,同时也便于代码的管理和分发。

库文件分为静态库文件和动态库文件:静态库在程序编译时被整合进最终的可执行文件中,增强了程序的独立性;动态库则在程序运行时被加载,可以被多个程序共享,有助于节省系统资源并便于库的更新。

当我们在使用C语言中的函数时,也是需要引入库文件的,称为C标准库。一般来说,安装GCC编译器的时候就会自动下载C标准库,并且编译器和链接器还自动实现了C标准库的链接,所以我们只需包含对应的头文件即可。

 (3)链接的分类

链接包括静态链接和动态链接两种方式。

    ① 静态链接

静态链接是指在程序编译时将目标文件和静态库文件(Unix/Linux系统中后缀为.a,Windows中是.lib)合并成可执行文件的过程。具体来说,编译器和链接器将从这些静态库中提取出必要的函数和数据,将它们直接嵌入到生成的可执行文件中,从而使得最终的应用程序在没有外部依赖的情况下运行。这种方式的主要特点是:

a)独立性:静态链接生成的可执行文件包含了所有必需的库代码,因此不依赖于系统上的外部库文件。这意味着一旦编译完成,它就是一个独立的可执行文件。

b)性能:由于所有代码都包含在一个二进制文件中,程序启动和执行时不需要额外的库加载或符号解析,这可以略微提高运行效率。

c)文件大小:静态链接的可执行文件通常较大,因为它包括了所有用到的库代码。

d)更新困难:如果库中发现了安全漏洞或需要更新,必须重新编译整个应用程序。这可能导致维护更加困难。

    ② 动态链接

动态链接与静态链接不同,不会将库代码直接嵌入到可执行文件中,而是在程序启动时将动态库(Unix/Linux系统中后缀为.so,Windows中是.dll文件)链接到可执行文件。这种方法的特点包括:

a)节省空间:同一动态库可以被系统上多个不同的程序共享,从而节省内存和磁盘空间。

b)方便更新:库的更新和维护可以独立于应用程序进行。只需要替换系统中的库文件,不需要重新编译依赖它的应用程序。

c)启动较慢:在程序启动时,需要加载库并进行符号解析,这可能导致比静态链接时更慢的启动速度。但是,也可以在真正调用库中的函数时才去加载动态库,即显式加载或懒加载。这种方式可以加快程序启动时间,减少初始内存占用,但增加了程序运行时的复杂性。

d)依赖问题:动态链接的应用程序依赖于操作系统中正确版本的库存在。如果缺少这些库或版本不兼容,程序可能无法运行。

在现代软件开发中,动态链接是更常见的选择,尤其是在操作系统和复杂应用程序中,因为它支持更灵活的应用程序部署和更新策略。

 (4)使用静态链接

使用静态链接前需要先制作静态库:

    ① 制作静态库

在Unix-like系统如Linux或macOS中,我们可以使用gcc或clang编译器来创建静态库。例如,如果我们有多个源文件(file1.c, file2.c等),我们可以首先将它们编译为目标文件,然后使用ar命令创建静态库:

1.gcc -c file1.c

2.gcc -c file2.c

3.ar rcs libmystatic.a file1.o file2.o       

4.或简写为:

5.gcc -c *.c        //*表示本目录下的所有

6.ar rcs libmystatic.a *.o

其中,ar 是一个管理静态库的工具,常用于创建和管理归档文件或静态库,rcs是它的三个功能选项,r表示如果库文件中已存在要添加的文件,则替换它。c表示创建一个新的归档库,即使这个库文件之前不存在。s表示添加一个索引到归档库中,这使得链接器可以更快地查找库中的符号。

静态库名和动态库通常习惯以 lib 开头(例如 libmylib.a),这是一种惯例,但不是硬性要求。使用 lib 前缀可以让链接器(如 gcc 或 clang)通过 -l 选项方便地找到静态库。

在Windows中,使用Microsoft Visual Studio可以通过创建一个静态库项目来生成.lib文件。 将属性-常规里的“目标文件扩展名”和“配置类型”都设为.lib即可。

    ② 进行静态链接

Linux中,使用静态库链接程序时,需要在编译命令中指定库的路径(如果库不在标准路径中)和名称。例如,在Linux下使用gcc:

1.gcc -o myprogram main.c -L/path/to/library -lmystatic

如果需要链接多个静态库时,需要考虑静态库之间的依赖关系,将被依赖的放后面,如:

1.gcc -o myprogram myprogram.o -L/path/to/libs -lutils -lmath        //其中 libutils.a 依赖于 libmath.a 中定义的一些数学函数。

如果同时存在静态库和动态库,需要使用 -static 选项来指定静态链接。

Windows中,在Microsoft Visual Studio中使用静态库的方法为在"属性面板"--"配置属性"-- "链接器"--"常规"的“附加库目录”中输入静态库所在目录,然后在 "属性面板"--"配置属性"-- "链接器"--"输入"的“附加依赖库”中输入库名(如libmystatic.lib)即可。

 (5)使用动态链接

使用动态链接前需要先制作动态库:

    ① 制作动态库

在Linux中,我们可以使用以下步骤来创建动态库:

1.gcc -fPIC -c file1.c      

2.gcc -fPIC -c file2.c

3.gcc -shared -o libexample.so file1.o file2.o

4.其实上面两个步骤可以合并为一个命令:

5.gcc -fPIC -shared -o libexample.so file1.c  file2.c

在Windows中,可以使用Microsoft Visual Studio来创建一个动态库,具体过程略。当我们创建一个DLL项目并编译它时,Visual Studio会生成两个关键文件:

DLL文件:包含可执行代码和数据,这些代码和数据在运行时被加载。

导入库(.lib)文件:不包含可执行代码,只包含指向DLL中每个导出函数和变量的入口点的指针。

    ② 进行动态链接

在Linux下,编译时指定动态库:

1.gcc -o myprogram main.c -L/path/to/library -lmydynamic

Windows中,在Microsoft Visual Studio中使用动态库的方法为在"属性面板"--"配置属性"-- "链接器"--"常规"的“附加库目录”中输入动态库所在目录,然后在 "属性面板"--"配置属性"-- "链接器"--"输入"的“附加依赖库”中输入动态库的导入库名即可。

 (6)C标准库与Linux系统调用

C标准库提供跨平台的标准功能,而操作系统相关的功能如进程管理等则通过系统调用实现。Linux系统调用和Windows系统调用分别为各自操作系统提供了文件操作、进程管理、线程管理、网络通信等接口,以支持更具体的操作系统级交互。

C标准库中的常见头文件如下:

1.stdio.h:定义了输入输出函数,如printf和scanf。

2.stdlib.h:包含了一些通用的函数,如内存分配和转换函数。

3.stddef.h:定义了一些通用的宏和类型。

4.string.h:提供了字符串处理函数,如strcpy和strcat。

5.math.h:包含了数学函数,如sin和cos。

6.time.h:定义了时间函数,如time和clock。

7.ctype.h:包含了字符分类函数,如isalpha和isdigit。

8.stdbool.h:定义了布尔类型和true/false宏。

9.errno.h:定义了错误码常量,用于处理函数调用返回的错误信息。

10.assert.h:包含了断言宏,用于在程序中添加断言检查。

Linux系统调用的常见头文件如下:

1.unistd.h:定义了系统调用的函数原型和常量。

2.sys/types.h:包含了一些基本类型的定义。

3.sys/stat.h:定义了文件状态的结构和相关常量。

4.sys/socket.h:定义了套接字操作相关的函数和数据结构。

5.fcntl.h:提供了文件控制操作的函数和常量。

6.netinet/in.h:定义了Internet地址族的数据结构。

7.signal.h:定义了信号处理函数和相关的宏。

8.pthread.h:提供了线程相关的函数和数据类型,用于多线程编程。

9.arpa/inet.h:定义了Internet地址转换函数,如htonl、ntohl等。

2、动态库的使用
 (1)简介

上面讲解了动态库的创建和链接方式,这里讲解如何使用动态库,通常分为以下三步:

    ① 编写动态库代码

在Windows和Linux上编写动态库代码时都需要使用特定关键字来对动态库中的函数或变量进行导出声明,表示该函数或变量是需要导出的,即这些函数或变量可以被外部程序使用。Windows上使用__declspec(dllexport)关键字,Linux上使用__attribute__((visibility("default")))和 __attribute__ ((visibility("hidden")))。

    ② 编译生成动态库

见上面的“使用动态链接”。

    ③ 使用动态库代码

在 DLL 的使用方式上,Windows和Linux都主要有两种方法:

a)动态链接

在程序启动时将动态库链接到可执行文件。这种方式下,Windows需要在项目文件中使用__declspec(dllimport)关键字来对动态库中的函数或变量进行导入声明,Linux 上通常不需要特殊的导入声明。

b)显式加载

不进行动态链接,而是在需要时使用特定的函数来显式加载。这种情况下,不需要进行导入声明,因为是在运行时动态解析函数的地址。并且,这种方式不需要引入动态库的头文件,只引入动态库本身即可。这种方式提供了更大的灵活性,允许程序根据需要加载不同的库版本或仅在需要时加载库。

Windows使用 Windows API 的 LoadLibrary 和 GetProcAddress 函数(或类似的其他库提供的功能,如 Qt 的 QLibrary)来手动加载 DLL 并获取函数地址,Linux可以使用 dlopen 和 dlsym 函数。

 (2)Windows上的导出导入

Windows上使用__declspec(dllexport)关键字进行导出声明,实例如下:

1.在dll的某个头文件中:

2.extern "C" __declspec(dllexport) int start(QString url, QString user, QString pwd, QString & res);

Windows上使用__declspec(dllimport)关键字进行导入声明,这让链接器知道它们在运行时需要从 DLL 加载。实例如下:

1.在调用dll的项目文件的某个头文件中:

2.__declspec(dllimport) int start(QString url, QString user, QString pwd, QString & res);

实际工作中的导出导入声明会稍微复杂一点。如现有一个项目文件project,我们想引入A.dll动态库,A.dll中的导出导入声明通常如下:

A.dll的某个头文件,如test.h中:
#ifndef BUILD_STATIC
# if defined(YONYOUPLMCAD_LIB)
#  define YONYOUPLMCAD_EXPORT Q_DECL_EXPORT
# else
#  define YONYOUPLMCAD_EXPORT Q_DECL_IMPORT
# endif
#else
# define YONYOUPLMCAD_EXPORT
#endif
 
#ifdef __cplusplus
extern "C" {
#endif

extern "C"  YONYOUPLMCAD_EXPORT int start(QString url, QString user, QString pwd, QString & res);

#ifdef __cplusplus
}
#endif

其中各宏的作用如下:

1. BUILD_STATIC 宏指定是否构建静态库。如果定义了BUILD_STATIC 宏,表示要构建静态库,那么YONYOUPLMCAD_EXPORT 宏会被定义为空,被YONYOUPLMCAD_EXPORT修饰的项不会受到任何额外的修饰。如果未定义该宏,表示动态库构建中的导出声明或导入声明。

2. YONYOUPLMCAD_LIB宏用于控制本项目文件是导出还是导入。

3. Q_DECL_EXPORT和Q_DECL_IMPORT是QT框架中的预定义宏,分别表示__declspec(dllexport)和__declspec(dllimport)。

当引入A.dll时,会将它的头文件一起引入到项目中,而不引入源文件,这样我们只需包含它的头文件,然后直接调用它的函数即可。在构建项目时,由于项目中没有定义YONYOUPLMCAD_LIB宏,所以YONYOUPLMCAD_EXPORT会被定义为__declspec(dllimport),相当于进行了导入声明,不需要我们再进行手动声明。使用这种方式,可以让dll使用变得更加简洁和高效。

 (3)Linux上的导出导入

在 Linux 上,使用 GNU 编译器(如 GCC)时,默认情况下,动态链接库(共享对象.so)中的所有符号(例如函数、变量)都是可见的,也就是说它们都是自动导出的,默认使用了__attribute__ ((visibility ("default")))进行导出声明。然而,如果我们希望某些符号不被导出,可以使用 __attribute__ ((visibility ("hidden")))进行隐藏导出声明。实例如下:

1.static int internalFunction() __attribute__ ((visibility ("hidden")));

或者在编译整个库时使用 -fvisibility=hidden 参数,并只对需要导出的符号使用 __attribute__ ((visibility ("default"))),这样可以更安全地隐藏其他所有不应被外部访问的符号。

在跨平台的项目中,可能需要根据操作系统使用不同的导出导入声明。通常,项目会定义一些宏来处理这些差异:

#ifdef _WIN32
    #define EXPORT __declspec(dllexport)
    #define IMPORT __declspec(dllimport)
#else
    #define EXPORT __attribute__ ((visibility ("default")))
    #define IMPORT
#endif

#ifdef BUILD_LIB
    #define LIB_API EXPORT
#else
    #define LIB_API IMPORT
#endif

extern "C" LIB_API int start(const QString& url, const QString& user, const QString& pwd, QString& res);

在 Linux 系统上,当使用动态库时,链接器(如 GNU ld)会自动处理符号的导入。这意味着在代码中我们不需要进行导入声明。

 (4)显式加载

Windows使用 Windows API 的 LoadLibrary 和 GetProcAddress 函数来显式加载的实例如下:

typedef void (*FunctionType)();  // 定义函数指针类型

int main() {
    // 加载 DLL
    HMODULE hModule = LoadLibrary(TEXT("A.dll"));
    if (hModule == NULL) {
        std::cerr << "Failed to load DLL!" << std::endl;
        return 1;
    }

    // 获取函数地址
    FunctionType someFunction = (FunctionType) GetProcAddress(hModule, "someFunction");
    if (someFunction == NULL) {
        std::cerr << "Failed to locate the function!" << std::endl;
        FreeLibrary(hModule);
        return 1;
    }

    // 调用函数
    someFunction();

    // 卸载 DLL
    FreeLibrary(hModule);
    return 0;
}

Linux可以使用 dlopen 和 dlsym 函数来显式加载的实例如下:

int main() {
    void* handle = dlopen("./libexample.so", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "%s\n", dlerror());
        return 1;
    }

    // 获取函数指针
    int (*add)(int, int) = dlsym(handle, "add");
    const char *dlsym_error = dlerror();
    if (dlsym_error) {
        fprintf(stderr, "%s\n", dlsym_error);
        dlclose(handle);
        return 1;
    }

    // 使用函数
    printf("Add(2, 3) = %d\n", add(2, 3));

    // 关闭库
    dlclose(handle);
    return 0;
}
 (5)使用静态库

因为静态库中的符号在编译时直接被链接到最终的可执行文件中,所以使用静态库不需要像动态库那样进行导出和导入的声明,只要正确的编译即可。使用的时候包含对应的头文件、库目录、库名即可。

  • 24
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值