GCC/G++ 及动态库与静态库简介


一、gcc/g++

1、gcc 编译器简介

  • GCC(GNU Compiler Collection)
    • GNU 编译器套件,它将易于编写、阅读和维护的高级计算机语言翻译为计算机能解读、运行的低级机器语言的程序
    • 其功能由最初仅能编译 C 语言,扩增至可以编译多种编程语言,例如 C++、Go、Objective-C 等
  • GCC/G++ 编译标准:
    • 不同版本的 GCC 编译器,默认使用的标准版本也不尽相同
    • 可以借助 -std 选项指定要使用的编译标准:gcc/g++ -std=编译标准

在这里插入图片描述在这里插入图片描述

  • Windows 平台下 GCC 编译器的安装:
    • GCC 官网提供的 GCC 编译器是无法直接安装到 Windows 平台上的,可以安装 GCC 的移植版本 MinGWCygwin
    • MinGW 侧重于服务 Windows 用户可以使用 GCC 编译环境,直接生成 Windows 平台上的可执行程序,相比Cygwin 体积更小,使用更方便
    • Cygwin 则可以提供一个完整的 Linux 环境,借助它不仅可以在 Windows 平台上使用 GCC 编译器,理论上可以运行 Linux 平台上所有的程序
  • Linux 平台下 GCC 编译器的安装:
# 1、查看 gcc 的版本号
gcc --version

# 输出如下
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.

# 2、查看 gcc 默认搜索库的路径
gcc --print-search-dirs

# 3、使用 apt 包管理器快速安装 gcc g++
sudo apt-get install gcc g++  # 需要注意的是,采用此方式安装的 gcc 编译器,版本通常较低
sudo apt-get install gcc-10 g++-10  # 安装指定版本
sudo apt-get install gcc-arm-linux-gnueabi # 安装交叉编译器,arm-linux-gnueabi-gcc --version

# 4、手动安装 gcc 编译器,官网 https://mirrors.tuna.tsinghua.edu.cn/gnu/gcc/ 下载压缩包
可参考此博客进行安装:http://c.biancheng.net/view/7933.html
  • gcc/g++ 的区别:
    • g++ 指令就约等同于 gcc -xc++ -lstdc++ -shared-libgcc, 即 gcc 在编译 C++ 程序时需要链接 C++ 的标准库
      • gcc 是 GCC 编译器的通用编译指令,可根据程序文件的后缀名自行判断当前程序所用编程语言的类别,用相应语言代码的方式编译此文件
      • 使用 g++ 指令,则无论目标文件的后缀名是什么,该指令都一律按照编译 C++ 代码的方式编译该文件(对于 .c 文件来说,gcc 指令以 C 语言代码对待,而 g++ 指令会以 C++ 代码对待)
    • 通常: 对于 C 语言程序的编译,我们应该使用 gcc 指令,而编译 C++ 程序则推荐使用 g++ 指令

2、gcc/g++ 的基本用法

  • gcc/g++ [options] [filenames] ,参数选项可参考:option summary 及下面第 3 项中 gcc 命令行参数的说明(参数顺序一般没影响)

    • -o file:指定生成的输出文件名为 file,如果不指定,会自动生成一个默认名 a.out
    • -E:只进行预处理(头文件展开,宏定义展开,条件编译选择,将代码中的注释删除掉等等)
      • gcc -E hello.c -o hello.i,将预处理结果写到文件中,gcc -E -C hello.c -o hello.i,添加 -C 参数,可阻止 GCC 删除源文件和头文件中的注释
      • 头文件展开:将包含的头文件内容展开, 并删除 #include;通过头文件包含可以实现模块化编程
      • 宏展开: 展开所有的宏定义, 并删除#define;使用宏可以定义一个常量, 提高程序的可读性
      • 条件编译(#if #else #endif:根据宏定义条件,选择要参与编译的分支代码,其余的分支丢弃;通过条件编译可以让代码 兼容不同的处理器架构和平台, 以最大限度地复用公用代码
      • 编译控制:保留#pragma 命令,编译时打印自定义文本信息,#pragma message("string")
      • 删除注释并添加行号和文件名标识
    • -S(大写):只进行预处理、编译,生成汇编文件(会检查语法)
      • gcc -S hello.i -o hello.s,将预处理得到的程序代码,经过一系列的词法分析、语法分析、语义分析以及优化,加工为当前机器支持的 汇编代码
      • gcc -S hello.i -o hello.s -fverbose-asm,借助 -fverbose-asm 选项,GCC 编译器会自行为汇编代码添加必要的注释
      • 如果操作对象为 .i 文件,则 GCC 编译器只需编译此文件;如果操作对象为 .c 或者 .cpp 源代码文件,则 GCC 编译器会对其进行 预处理和编译这 2 步操作
    • -c(小写):只进行预处理、编译和汇编,不进行链接,会自动生成一个默认名为 ×××.o的目标文件(二进制文件),不可执行,属于可重定位的目标文件,它们还要经过链接器重定位、 链接(需要用到符号表和重定位表)之后, 才能组装成一个可执行的目标文件
      • gcc -S hello.s -o hello.o,将汇编代码转换成可以执行的 机器指令(二进制 0 1)
      • 如果指定文件为源程序文件(例如 hello.c),则 gcc -c 指令会对 hello.c 文件执行 预处理、编译以及汇编这 3 步操作
      • 如果指定文件为预处理后的文件(例如 hello.i),则 gcc -c 指令对 hello.i 文件执行 编译和汇编这 2 步操作
      • 如果指定文件为编译后的文件(例如 hello.s),则 gcc -c 指令只对 hello.s 文件执行 汇编这 1 步操作
  • 分步编译流程如下图所示:
    在这里插入图片描述
    在这里插入图片描述在这里插入图片描述

3、gcc 命令行参数说明

(I)、预处理选项

使用预处理选项,可以配合源代码中 #ifdef MACROS 命令实现条件编译

DEFINE_MACROS := -D ARCH_X86_32_LINUX
DEFINE_MACROS += -D MULTDETECT_FPN
DEFINE_MACROS += -D _LINUX_  # 相当于 C 语言中的 #define _LINUX_ 

gcc test.c -o test -DTRUE # 相等于在代码第一行定义 #define TRUE 1,以字符串“1”定义 TRUE 宏。
gcc test.c -o test -Dmacro=string # 相等于在代码第一行定义 #define macro string,以字符串“string”定义 macro 宏

在这里插入图片描述

(II)、库操作选项
  • GCC 默认会链接 libc.a 或者 libc.so,但是对于其他的库(例如非标准库、第三方库等),就需要手动添加
  • 在 Linux 下开发软件时,完全不使用第三方函数库的情况是比较少见的,通常来讲都需要借助一个或多个函数库的支持才能够完成相应的功能。因此,gcc 在编译时必须有自己的办法来查找所需要的头文件和库文件。常用的方法有:
    • -Wl,rpath=:添加运行时库路径,在编译时会记录到 target 文件中,所以编译之后的 target 文件在执行时会按这里给出的路径去找库文件,通常用于指定依赖库的路径(可以不显示的指出依赖库)
    • -Wl,--strip-all:去除所有符号信息
    • -L dir:指除标准库搜索路径外的函数库搜索路径,告诉链接器从哪找库(.so文件),只在链接时会用到
    • -lxxx:指定链接时需要的动态库 xxx,编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上 lib,后面加上.a/.so来确定库的名称,当静态库和动态库同名时,gcc 命令将优先使用动态库
    • -I dir :指定除标准头文件搜索路径外的头文件搜索路径
    • -shared :指定生成动态链接库
    • -static :指定生成静态链接库
  • 链接选项一般使用 LDFLAGS 来指定:
    • LDFLAGS := -L"./../common/" -lopencv_world -losp -lmpi -lVoiceEngine -lsecurec -lpthread
    • LDFLAGS += -L"./../common/3rdparty/" -llibjpeg-turbo -llibpng -llibtiff -llibwebp -lzlib
(III)、优化、警告及代码生成选项
  • 优化选项:

    • -O0:编译器采用的默认优化选项,编译耗时最短
    • -O1:编译器尝试减少代码大小和执行时间
    • -O2:进行更多优化操作,会打开除了循环展开和 inline 函数外的所有优化选项
    • -O3:最高优化等级,比 -O2 更进一步优化,包括 inline 函数,armcc 编译器使用 –vectorize 选项来使能向量化编译,一般选择更高的优化等级如 -O2 或者 -O3 就能使能 –vectorize 选项
    • -Os:编译器在 -O2 的基础上,专门用于优化二进制文件的体积
    • -Og:在保持快速编译和良好调试体验的同时,为代码应用合理的优化级别
  • 警告提示功能选项:

    • -w:不生成任何警告信息
    • -Wall :使 gcc 产生尽可能多的警告信息
    • -Werror :要求 gcc 将所有的警告当成错误进行处理,在所有产生警告的地方停止编译,迫使程序员对自己的代码进行修改
    • -v :输出 gcc 工作的详细过程
    • -Q :显示编译过程的统计数据和每一个函数名
  • 代码生成选项:

    • -fPIC(pic,position independent code):令 GCC 编译器生成动态链接库时,表示各目标文件中函数、类等功能模块的地址使用相对地址,而非绝对地址;这样,无论将来链接库被加载到内存的什么位置,都可以正常使用
    • -std=:手动指定编程语言所遵循的标准,eg:-std=c11; -std=c++11
(IV)、(交叉)编译选项
  • CFLAGS 和 CXXFLAGS 两个变量通常用来指定编译选项。前者仅仅用于指定 C 程序的编译选项,后者仅仅用于指定 C++ 程序的编译选项,其实也可以在两个变量中指定一些预处理选项(即一些本来应该放在 CPPFLAGS 中的选项),和 CPPFLAGS 并没有明确的界限
  • 通常情况下使用 gcc 编译的目标代码都与使用的机器是一致的,但 gcc 也支持交叉编译的功能,能够编译其他不同 CPU 的目标代码。使用 gcc 开发嵌入式系统,我们几乎都是以通用的 PC 机(X86)平台来做宿主机,通过 gcc 的交叉编译功能对其他嵌入式 CPU 的开发任务
DEFINE_MACROS := -D ARCH_ARMV8_ANDROID
DEFINE_MACROS += -D __ARM_NEON
DEFINE_MACROS += -D ADD_OSP  # 编译时设置宏

# 编译选项:命令行参数
CFLAGS := 
-mcpu=cortex-a73.cortex-a53  # 选择处理器类型,指定指令集架构和 CPU 类型
-mfloat-abi=softfp # 见下面解释
-mfpu=neon-vfpv4 # 选择 NEON 和 VFP 类型,neon-vfpv4 指定为 NEON + VFP 结构
-mno-unaligned-access # 进行对齐的 memory 访问
-fno-aggressive-loop-optimizations  # 不允许程序中的无限循环行为
-ffast-math # 令 GCC 采用非 ANSI 或 IEEE 标准来加快浮点运算
-O3 # 最高优化等级,优化代码及执行时间,向量化编译等
-Wall  # 使 GCC 产生尽可能多的警告信息
-fPIC  # 令 GCC 编译器生成动态链接库时,表示各目标文件中函数、类等功能模块的地址使用相对地址,而非绝对地址
-fpermissive  # Downgrade some diagnostics about nonconformant code from errors to warnings. 
			  # 可以兼容一些老的语法,但是一些语法错误也会被忽略(一般不推荐使用)
$(DEFINE_MACROS) # 可在编译选项中指定一些预处理选项

# Hi3559A 具有浮点运算单元和 neon,文件系统中的库是采用软浮点和 NEON 编译而成
# 因此所有板端代码编译时需要在 Makefile 里面添加选项 -mcpu=cortex-a53、-mfloat-abi=softfp 和 -mfpu=neon-vfpv4
# -mfloat-abi=softfp 参数解释,ABI即“application binary interface”,即编译器将 c 代码编译成汇编代码时使用的一种规则,使用规范如下:
# 在编译带有浮点参数的函数时,有三种可能的编译选项:
-mfloat-abi=soft  # 使用 GCC 的整数算术运算来模拟浮点运算,不使用 VFP 或者 NEON 指令
-mfloat-abi=softfp  # 使用 FPU 硬件来做浮点运算,函数的参数传递到整数寄存器(r0-r3)中,然后再传递到 FPU 中(可以使用 VFP 和 NEON 指令)
-mfloat-abi=hard  # 表明要使用 FPU 硬件来做浮点运算,函数的参数直接传递到 FPU 的寄存器(s0、d0)中(可以使用 VFP 和 NEON 指令)
# 并且改变 ABI 调用规则来产生更有效率的代码,如用 vfp 寄存器来进行浮点数据的参数传递,从而减少 NEON 寄存器和 ARM 寄存器的拷贝
(V)、调试选项
  • -g 和 -ggdb:尽可能的生成 gdb 可以使用的调试信息

    • gcc 在产生调试符号时,同样采用了分级的思路,开发人员可以通过在 -g 选项后附加数字 1、2、3 指定在代码中加入调试信息的多少。
    • 默认的级别是 2(-g2),此时产生的调试信息包括:扩展的符号表、行号、局部或外部变量信息
    • 级别3(-g3)包含级别2中的所有调试信息以及源代码中定义的宏
    • 级别1(-g1)不包含局部变量和与行号有关的调试信息,因此只能够用于回溯跟踪和堆栈转储
      • 回溯追踪:指的是监视程序在运行过程中函数调用历史
      • 堆栈转储:则是一种以原始的十六进制格式保存程序执行环境的方法
  • -p 和 -pg:会将剖析(Profiling)信息加入到最终生成的二进制代码中,剖析信息对于找出程序的性能瓶颈很有帮助,是协助Linux程序员开发出高性能程序的有力工具。

  • -save-temps:保存编译过程中生成的一些列中间文件,如 xxx.i xxx.s 等,供用户查询调试

  • 注意: 使用任何一个调试选项都会使最终生成的二进制文件的大小急剧增加,同时增加程序在执行时的开销,因此,调试选项通常仅在软件的开发和调试阶段使用


4、glibc 和 libstdc++ 简介

构建程序时,只要链接的 libstdc++,glibc(libc.so,libpthread,动态连接器等系列库)版本正确,就不会出问题;在低版本的 OS 上,安装高版本的 gcc,glibc,只要可以顺利编译通过,则意味这高版本库是支持低版本 OS

(I)、glibc 简介
  • glibcgnu 发布的 libc 库,是系统中最底层的 API,几乎其它任何运行时库都依赖于 glibc,对应的动态库的名字 libc.so.6
  • glibc 除了封装 linux 操作系统所提供的系统服务外,它本身也提供了许多其它必要功能服务的实现,例如:动态加载模块libdl、实时扩展接口 librt
  • 查看 glibc 版本和位置
# 1、通过 getconf 获取
getconf GNU_LIBC_VERSION  # glibc 2.31
getconf GNU_LIBPTHREAD_VERSION  # NPTL 2.31,pthread 被包含在 glibc 中,它的版本就是 glibc 的版本


# 2、通过 ldd 获取,ldd 是隶属于 glibc,它的版本就是 glibc 的版本
ldd --version  # ldd (Ubuntu GLIBC 2.31-0ubuntu9.9) 2.31

# 3、查找 gcc 依赖的 glibc 库位置
gcc -print-file-name=libc.so.6  # /usr/lib/x86_64-linux-gnu/libc.so.6

# 4、查看 glibc API 支持的版本
strings /usr/lib/x86_64-linux-gnu/libc.so.6 | grep GLIBC
# 输出如下
LIBC_2.2.5
GLIBC_2.2.6~2.31
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31.

# 5、确定程序需要的 glibc 的版本
readelf -s 可执行程序 | grep -oP "GLIBC_[\d\.]*" | sort | uniq
(II)、libstdc++ 简介
# 1、libstdc++ 是 gcc 的标准 C++ 库(libc++ 是 clang 的标准 C++ 库)

# 2、查看 libstdc++ 的版本
gcc --version  # libstdc++ 是被包含在 gcc 中的,对应为 gcc 的版本

# 3、查找 libstdc++.so 库的位置
/sbin/ldconfig -p | grep stdc++ # 输出 /lib/x86_64-linux-gnu/libstdc++.so.6

# 4、查看系统 libstdc++ API 支持的版本
strings /lib/x86_64-linux-gnu/libstdc++.so.6 | grep LIBCXX 
# 输出如下
GLIBCXX_3.4~3.4.28
GLIBCXX_DEBUG_MESSAGE_LENGTH

# 5、确定程序需要的 libstdc++ 的版本
readelf -s 可执行程序 | grep -oP "GLIBCXX_[\d\.]*" | sort | uniq

二、库文件

1、库的基本概念

  • 库可以简单看成一组常用函数目标文件(.o)的集合,经过 压缩打包 之后形成的一个文件,提供相应函数的接口,便于程序员使用
  • 库是已经写好的、成熟的、可复用的代码,可分为静态库和动态库,在不同系统下的对应关系如下:
    • linux: .a (Archive libraries) 和 .so(Shared object)
    • Windows: .lib.dll(Dynamic-link library)
  • 所谓静态、动态是指链接生成可执行程序阶段如何处理库
    在这里插入图片描述

2、静态库

(I)、静态库的概念
  • 静态库可以简单看成是一组目标文件(.o/.obj文件)经过压缩打包后形成的一个文件
  • 在链接阶段,会将汇编生成的 静态库 再与 引用到的库 一起通过静态链接的方式生成可执行文件
    • 可执行文件会从静态库中拷贝它需要的内容,添加为可执行程序的一部分进行使用
    • 可执行文件在运行时不再依赖静态库,加载速度快,但会造成空间浪费、更新需要重新编译静态库和可执行文件等问题
  • Linux 下静态库命名规范:lib[your_library_name].a,前缀为 lib ,中间是静态库名,扩展名为 .a
(II)、静态库的创建
  • Linux 下使用 ar 工具、Windows 下 vs 使用 lib.exe,将目标文件压缩到一起,并且对其进行编号和索引,以便于查找和检索
  • 注意: 并非任何一个源文件都可以被加工成静态链接库,其至少需要满足以下 2 个条件:
    • 源文件中只提供可以重复使用的代码,例如函数、设计好的类等,不能包含 main 主函数
    • 源文件在实现具备模块功能的同时,还要提供访问它的接口,也就是包含各个功能模块声明部分的头文件
# 1、将代码文件编译成目标文件.o(StaticMath.o)
g++ -c StaticMath.cpp

# 2、通过 ar 工具将目标文件打包成 .a 静态库文件
ar rcs libstaticmath.a StaticMath.o

# Note:大一点的项目会编写 makefile 文件(CMake等等工程管理工具)来生成静态库
  • 一般创建静态库的步骤如下图所示:
    在这里插入图片描述
(III)、静态库的使用
  • 库文件和相应头文件共享给用户,用户就可以使用该库里的函数了
# Linux 下使用静态库,只需要在编译的时候,指定:
#    1、静态库的搜索路径(-L 选项)
#    2、指定静态库名(-l 选项)不需要 lib 前缀和 .a 后缀 (编译器查找静态连接库时有隐含的命名规则,
#      即在给出的名字前面加上 lib,后面加上.a 来确定库的名称)
# 在 TestStaticLibrary.cpp 中包含相应的头文件,然后直接调用静态库中的函数即可

g++ TestStaticLibrary.cpp -L ../StaticLibrary -lstaticmath -o test.out  # 静态链接
./test.out  # 直接执行不会报错 

3、动态库

(I)、动态库的概念
  • 动态库也可以简单看成是一组目标文件(.o/.obj文件)经过压缩打包后形成的一个文件
  • 在链接阶段,会将汇编生成的 动态库 再与 引用到的库 一起通过动态链接的方式生成可执行文件
    • 可执行文件会仅拷贝一些重定位和函数引用表信息(不拷贝二进制代码),它会在程序运行时完成真正的链接过程
    • 不同的应用程序如果调用相同的库,操作系统会使用虚拟内存,使得一份库文件驻留在内存中被多个程序使用,节约了内存
    • 可执行文件在运行时依赖动态库,加载速度稍慢,但更新不需要重新编译可执行文件(只需重新编译动态库即可),直接替换相应的动态库即可(只要函数接口不变)
  • Linux 下动态库命名规范:lib[your_library_name].so.x.y.z,前缀为 lib ,中间是动态库名,扩展名为 .so,主版本号为 x,次版本号为 y,发布版本号为 z
    • 主版本号: 是重大升级,不同主版本之间是不兼容的
    • 次版本号: 是增量升级,只增加了一些新的接口符号,在主版本号相同的前提下,高次版本向低次版本兼容
    • 发布版本号: 是一些错误的修正、性能的改进等,在主版本号和次版本号相同的情况下,不同发布版本之间完全兼容
(II)、动态库的创建
  • 与静态库不同的是,不需要打包工具(ar、lib.exe),直接使用 gcc/g++编译器 即可创建动态库
# 1、将代码文件编译成目标文件.o(DynamicMath.o)此时要加编译器选项-fpic
#    创建与地址无关的编译程序(pic,position independent code),是为了能够在多个应用程序间共享
g++ -fPIC -c DynamicMath.cpp

# 2、生成动态库,此时要加链接器选项-shared 指定生成动态链接库
g++ -shared DynamicMath.o -o libdynmath.so

# 3、合并为一个命令
g++ -fPIC -shared DynamicMath.cpp -o libdynmath.so
(III)、动态库的使用
  • 库文件和相应头文件共享给用户,用户就可以使用该库里的函数了
# Linux 下使用动态库,只需要在编译的时候,指定:
#    1、动态库的搜索路径(-L 选项)
#    2、指定动态库名(-l 选项)不需要 lib 前缀和 .so 后缀 (编译器查找动态连接库时有隐含的命名规则,
#      即在给出的名字前面加上 lib,后面加上 .so 来确定库的名称)
# 在 TestStaticLibrary.cpp 中包含相应的头文件,然后直接调用动态库中的函数即可

g++ TestDynamicLibrary.cpp -L../DynamicLibrary -ldynmath -o test.out  # 动态链接
./test.out  # 直接执行会报错,需指定动态库的搜索路径(临时设置 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:xxx)

# 动态链接时、执行时搜索路径顺序
1、编译目标代码时用 "-L""-Wl,-rpath" 指定的动态库搜索路径
2、环境变量 LD_LIBRARY_PATH 指定的动态库搜索路径
3、配置文件 /etc/ld.so.conf 中指定的动态库搜索路径
- /etc/ld.so.conf 第一行有个引用命令:include ld.so.conf.d/*.conf
- 因此最优雅的方式是在 ld.so.conf.d 目录下创建一个自己的程序依赖的配置文件,配置文件内容为程序依赖的动态库路径,一个路径一行
- 最后 ldconfig 更新配置文件
4、默认动态库搜索路径 /lib /usr/lib /lib64 /usr/lib64/ /usr/lib/x86_64-linux-gnu /usr/lib/x86_64-linux-gnu64 等
# 查看动态库搜索路径:ld --verbose | grep SEARCH

# 如何让系统能够找到新添加的库?
1、将库文件移动到 /lib 或者 /usr/lib 下,那么 ld 默认能够找到
2、修改 .bashrc 中的环境变量(export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:xxx),然后更新生效(source .bashrc)
3、编辑 /etc/ld.so.conf 文件:
- 加入库文件所在目录的路径(eg:/usr/local/lib,很多开源的共享库都会安装到该目录下)
- 运行 ldconfig,重建 /etc/ld.so.cache 文件(linux下的共享库机制采用了类似于高速缓存的机制,将库信息保存在/etc/ld.so.cache里) 

# 多个库文件链接顺序问题
1、在链接命令中给出所依赖的库时,依赖其它库的库一定要放到被依赖库的前面,避免 undefined reference 的错误,完成编译链接
2、库的链接顺序:g++ ...  obj($?) -l(上层逻辑lib) -l(中间封装lib) -l(基础lib) -l(系统lib)  -o $@
Note:查找需要连接的符号名是 根据-L指定的路径顺序查找;从左往右找(如果符号有依赖,第一个-l库文件最
先调用,如果它依赖于其它库,依赖的库应该放在它的后面,在后面继续寻找它的依赖,就像主obj依赖-l库一样;
如果符号没有依赖,则第一个库最先调用,它后面的库里如果有同名符号函数则不会生效)
(IV)、动态库链接相关和常用的 binutils 命令
  • ld 命令
    • gcc 的链接程序,该程序将目标文件(.o)的集合组合成可执行程序,在链接过程中,它必须把符号(变量名、函数名等一些列标识符)用对应的数据的内存地址(变量地址、函数地址等)替代,以完成程序中多个模块的外部引用
    • 目标文件已经是二进制文件,与可执行文件的组织形式类似,只是有些函数和全局变量的地址还未找到,因此还无法执行。链接的作用就是找到这些目标地址,将所有的目标文件组织成一个可以执行的二进制文件
// 常用动态库
lm(libm.so):数学库(math)
lc(libc.so):标准 C 库(C lib)
lrt(librt.so):实时库(real time)
ldl(libdl.so):显式加载动态库的动态函数库
lpthread(libpthread.so):多线程库

// 查看动态库搜索路径:ld --verbose | grep SEARCH
SEARCH_DIR("=/lib");
SEARCH_DIR("=/lib64");  
SEARCH_DIR("=/usr/lib"); 
SEARCH_DIR("=/usr/lib64"); 
SEARCH_DIR("=/usr/local/lib"); 
SEARCH_DIR("=/usr/local/lib64"); 
SEARCH_DIR("=/usr/x86_64-linux-gnu/lib");
SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64");
SEARCH_DIR("=/usr/lib/x86_64-linux-gnu");  // 此路径下包含 librt.so、libpthread.so、libdl.so、libc.so、libm.so
SEARCH_DIR("=/usr/lib/x86_64-linux-gnu64"); 
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); 
SEARCH_DIR("=/lib/x86_64-linux-gnu"); 

// 动态库的搜索顺序:
// 1、程序运行时所在的当前目录
// 2、搜索 LD_LIBRARY_PATH 环境变量指定的路径
// 3、搜索 /etc/ld.so.conf 中指定的路径
// 4、搜索默认的系统路径如 /lib 和 /usr/lib
  • ldconfig 命令
    • 用来更新 /etc/ld.so.conf 文件(可以遍历所有默认的共享库目录,更新所有 SO-NAME 的软链接的,使其指向最新的版本库)
    • 手动建立SO-NAME,ldconfig -n shared_library_directory
  • ldd 命令:查看一个可执行程序所依赖的动态库
# eg:ldd a.out
linux-vdso.so.1 =>  (0x00007ffe2aaad000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1d651c3000)
/lib64/ld-linux-x86-64.so.2 (0x00007f1d6558d000)
  • nm 命令:列出目标文件中的符号(可查看库中到底有哪些函数)
# eg:nm libmain.a
main.o:
                 U exp
0000000000000000 T main    # 库中定义的函数,用 T 表示
                 U printf  # 库中被调用,但并没有在库中定义(表明需要其他库支持),用 U 表示
                 U puts
  • readelf 命令:显示关于 ELF 格式文件内容的信息
// 获取库文件所依赖的动态库
readelf -d libCPUAlgFramework.so
Dynamic section at offset 0x6ced8 contains 33 entries:
  标记        类型                         名称/0x00000001 (NEEDED)                     共享库:[libmi_ipu.so]
 0x00000001 (NEEDED)                     共享库:[libcam_fs_wrapper.so]
 0x00000001 (NEEDED)                     共享库:[libcam_os_wrapper.so]
 0x00000001 (NEEDED)                     共享库:[libmi_sys.so]
 0x00000001 (NEEDED)                     共享库:[libmi_common.so]
 0x00000001 (NEEDED)                     共享库:[libstdc++.so.6]
 0x00000001 (NEEDED)                     共享库:[libm.so.6]
 0x00000001 (NEEDED)                     共享库:[libgcc_s.so.1]
 0x00000001 (NEEDED)                     共享库:[libc.so.6]
 0x0000000c (INIT)                       0xd9b0
 0x0000000d (FINI)                       0x67ad8
 0x00000019 (INIT_ARRAY)                 0x7cec8



// 获取库文件的头部信息:包括系统架构,数据大小端,文件类型,入口地址等
readelf -h libCPUAlgFramework.so 

// 输出如下:
ELF 头:
  Magic:   7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00 
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  Version:                           1 (current)
  OS/ABI:                            UNIX - GNU
  ABI 版本:                          0
  类型:                              DYN (共享目标文件)
  系统架构:                          AArch64
  版本:                              0x1
  入口点地址:               0x16c40
  程序头起点:          64 (bytes into file)
  Start of section headers:          977352 (bytes into file)
  标志:             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         7
  Size of section headers:           64 (bytes)
  Number of section headers:         37
  Section header string table index: 36

// 确定程序需要的 libstdc++ 的版本
readelf -s 可执行程序 | grep -oP "GLIBCXX_[\d\.]*" | sort | uniq
  • addr2line 命令:将程序地址翻译成文件名和行号,常用于 debug

4、静态库和动态库的优缺点

  • 静态库:空间浪费(重复打包依赖库)和更新困难,但执行速度快(牺牲了空间效率,换取了时间效率)
  • 动态库:(只打包依赖库一份依赖库)执行速度慢和依赖性强,但升级简单、节省内存(牺牲了时间效率换取了空间效率)
    在这里插入图片描述

5、显示调用 C/C++ 动态链接库

  • 动态链接库的调用方式有 2 种,分别是:

    • 隐式调用(静态调用):将动态链接库和其它源程序文件(或者目标文件)一起参与链接
    • 显式调用(动态调用):手动调用动态链接库中包含的资源,同时用完后要手动将资源释放
  • 显式调用动态链接库的过程,类似于使用 malloc() 和 free()(C++ 中使用 new 和 delete)管理动态内存空间,需要时就申请,不需要时就将占用的资源释放。由此可见,显式调用动态链接库对内存的使用更加合理。

// 无需引入和动态链接库相关的头文件。但要引入另一个头文件,即 <dlfcn.h> 头文件
// 因为要显式调用动态链接库,需要使用该头文件提供的一些函数
#include <dlfcn.h>

// 打开库文件,将库文件装载到内存中,为后续使用做准备
void *dlopen (const char *filename, int flag);
/********************************************
filename: 用于表明目标库文件的存储位置和库名
flag:
    RTLD_NOW:将库文件中所有的资源都载入内存
    RTLD_LAZY:暂时不将库文件中的资源载入内存,使用时才载入
*******************************************/

// 获得指定函数在内存中的位置
void *dlsym(void *handle, char *symbol);
/********************************************
hanle:表示指向已打开库文件的指针
symbol:用于指定目标函数的函数名
********************************************/

// 关闭已打开的动态链接库,当函数返回 0 时,表示函数操作成功
// 注意,调用 dlclose() 函数并不一定会将目标库彻底释放,它只会是目标库的引用计数减 1,
// 当引用计数减为 0 时,库文件所占用的资源才会被彻底释放。
int dlclose (void *handle);


// 获得最近一次 dlopen()、dlsym() 或者 dlclose() 函数操作失败的错误信息
// 该函数不需要传递任何参数, 若返回 NULL,则表明最近一次操作执行成功。
const char *dlerror(void);
  • 下面通过一个 C 语言项目实例,演示显式调用动态链接库的具体实现过程, demo 项目结构如下:
 demo
   ├─ headers
   │     └─ test.h
   └─ sources
          ├─ add.c
          ├─ sub.c
          ├─ div.c
          └─ main.c
  • 项目中各个文件包含的代码如下:
// test.h
#ifndef __TEST_H_
#define __TEST_H_

int add(int a,int b);
int sub(int a,int b);
int div(int a,int b);

#endif

// add.c
#include "test.h"
int add(int a,int b)
{
    return a + b;
}

// sub.c
#include "test.h"
int sub(int a,int b)
{
    return a - b;
}

// div.c
#include "test.h"
int div(int a,int b)
{
    return a / b;
}

// 将 add.c、sub.c 和 div.c 这 3 个源文件打包生成一个动态链接库
gcc -fpic -shared add.c sub.c div.c -o libmymath.so
  • 主程序文件 main.c 的代码
#include <stdio.h>
#include <dlfcn.h>
void fdlerror()
{
    if(dlerror() != NULL){
        printf("%s",dlerror());
    }
}

int main()
{
    int m,n;
    // 打开库文件
    void* handler = dlopen("libmymath.so", RTLD_LAZY);
    fdlerror();
   
    // 获取库文件中的 add() 函数
    int(*add)(int, int)=dlsym(handler, "add");
    fdlerror();
  
    // 获取库文件中的 sub() 函数
    int(*sub)(int, int)=dlsym(handler, "sub");
    fdlerror();

    // 获取库文件中的 div() 函数
    int(*div)(int, int)=dlsym(handler, "div");
    fdlerror();

    // 使用库文件中的函数实现相关功能
    printf("Input two numbers: ");
    scanf("%d %d", &m, &n);
    printf("%d+%d=%d\n", m, n, add(m, n));
    printf("%d-%d=%d\n", m, n, sub(m, n));
    printf("%d÷%d=%d\n", m, n, div(m, n));
    
    // 关闭库文件
    dlclose(handler)return 0;
}

// 生成可执行文件:这里需要添加 -ldl 选项(需要 libdl.so 动态库的支持, 包含了 dlopen、dlsym、dlclose、dlerror 4 个函数)
gcc main.c -ldl -o main.exe

三、C/C++ 预编译与混合编译

  • 预编译指令:
    • 所有以 # 开头的行,都代表 预编译指令,预编译指令行结尾是没有分号的
    • #include "文件名"#include <文件名>的区别:前者预处理时首先在当前文件所在的文件目录(文件名中若包含路径,则在指定路径查找头文件)中寻找,若找不到才到系统指定的文件夹中查找;后者直接在系统指定的文件夹中寻找(通常用于包含标准头文件)
#define    // 定义一个预处理宏
#undef     // 取消宏的定义

// 对部分源程序行只在满足一定条件时才编译(即对这部分源程序行指定编译条件)
#if        // 编译预处理中的条件命令,相当于 C 语法中的 if 语句
#else      // 与#if, #ifdef, #ifndef对应, 若这些条件不满足,则执行#else之后的语句,相当于C语法中的else
#endif

/*************作用:可以区隔一些与特定头文件、程序库和其他文件版本有关的代码 *************/
#ifdef     // 判断某个宏是否被定义,若已定义,执行随后的语句
#elif      // 若 #if, #ifdef, #ifndef 或前面的 #elif 条件不满足,则执行 #elif 之后的语句,相当于 C 语法中的 else-if
#endif

#ifndef    // 与 #ifdef 相反,判断某个宏是否未被定义
#else
#endif     // #if, #ifdef, #ifndef 这些条件命令的结束标志

// 防止头文件被重复包含
#ifndef _SOMEFILE_H 
#define _SOMEFILE_H
// 需要声明的变量、函数 
// 宏定义 
// 结构体
#endif


// 无参宏定义:
#define 标识符 替换列表 
#define N (3+2)
int r=N*N;  // 替换后为 int r=(3+2)*(3+2); 不加括号可能会出现逻辑上的错误

// 带参宏定义:可以不考虑数据的类型(既是优点也是缺点:优点是可用于多种数据类型,缺点是类型不安全)
#define 标识符(参数1,参数2,...,参数n) 替换列表
#define MUL(a,b) ((a)*(b))

// Note:
// 宏定义仅是做简单的文本替换,故替换列表中如有表达式,必须把该表达式用括号括起来,否则可能会出现逻辑上的“错误”
// 在宏定义时,除单一值参数外,替换列表中的每个参数均加括号,整个替换列表也加括号
#define MUL(a,b) (a*b)  // 错误示例
int c = MUL(3, 5+1);    // 会替换成 c=(3*5+1)=16; 与预期功能不符

// 简单函数宏定义: 在预处理阶段进行展开,没有函数调用的开销,加快程序运行速度
// C++ 中可以用内联函数 inline void func() 来实现,但内联函数会占用空间,可看作是以空间换时间
#ifndef MIN
#define MIN(a, b)   ((a) > (b) ? (b) : (a))
#endif

#ifndef MAX
#define MAX(a, b)   ((a) > (b) ? (a) : (b))
#endif

#ifndef ABS
#define ABS(x) ((x) >= 0 ? (x) : (-(x)))
#endif
  • C/C++ 混合编译:
    • 一个项目中 .cpp 调用的头文件中含有需要 gcc 编译的部分,那么需要使用 extern “C”{} 让这段代码按照 C 语言的方式进行编译、链接
    • 使用 extern “C”{}的主要原因是:
      • C++ 对函数进行了重载,在编译生成的汇编码中会对函数的名字进行一些处理,加入比如函数的参数类型、个数、顺序等,而在 C 中,只是简单的函数名字而已,不会加入其它的信息
      • 若在 C++ 中调用一个使用 C 语言编写的函数,C++ 会根据C++名称修饰方式来查找并链接这个函数,那么就会发生 链接错误
// 1、C 和 C++ 中对同一个函数经过编译后生成的函数名是不相同的
C 函数: void MyFunc(){}, 被编译成函数: MyFunc
C++ 函数: void MyFunc(){}, 被编译成函数: _Z6Myfuncv  
// C++ 中调用 MyFunc 函数,在链接阶段会去找 _Z6Myfuncv,
// 结果是没有找到的,因为这个 MyFunc 函数是 C 语言编写的,编译生成的符号是 MyFunc
  • 代码实现示例:
// 1、在 .c 相应的 .h 头文件中包含如下代码
#ifndef _SOMEFILE_H   // 防止头文件被重复包含
#define _SOMEFILE_H

#ifdef __cplusplus  // __cplusplus 是 g++ 编译器中的自定义宏,用于说明正在使用 g++ 编译
extern "C" {
#endif

// 一段声明代码,使用 gcc 来编译、链接

#ifdef __cplusplus
}
#endif  // __cplusplus 
#endif  // _SOMEFILE_H 

// 2、直接在 cpp 使用 extern "C" 包含 C 头文件
extern "C" {
// 一段声明代码,使用 gcc 来编译、链接
}

四、参考资料

1、http://c.biancheng.net/gcc/
2、C++静态库与动态库
3、静态库和动态库区别
4、gcc编译过程、gcc命令参数、静态库和动态库搜索路径
5、gcc的使用简介与命令行参数说明
6、#ifdef __cplusplus extern C{}与C和C++间的关系
7、#ifdef __cplusplus 到底是什么意思?
8、交叉编译和交叉工具链
9、多个gcc/glibc版本的共存及指定gcc版本的编译
10、https://www.gnu.org/software/libc/

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐