Linux C/C++ 编译集锦 (GCC/build/compile/make)

目录

1. Linux C/C++ 编译集锦

1.1. 常规编译

./configure
make
sudo make install

1.1.1. 修改安装目录

  1. 修改 configure 文件中 prefix 的值:

vi/vim 打开 configure 文件, 然后找到 prefix 值, 修改未 prefix=你的安装目录, 然后保存退出, 再执行 ./configure & make & sudo make install 就可以, 不过该方法比较麻烦, 会容易改动到 configure 文件的其他的参数, 不建议使用。

  1. 执行 configure 文件时指定安装目录:
./configure --prefix=/home/user/zws/build
  1. make install 指定 DESTDIR 参数:
./configure
make 
make install DESTDIR=/home/user/zws/build

1.2. 常用环境变量

# LD_LIBRARY_PATH: lib/library directory, LD_LIBRARY_PATH serves the ld (the dynamic linker at runtime)
export LD_LIBRARY_PATH=xxx:$LD_LIBRARY_PATH

# C_INCLUDE_PATH: C header files
export C_INCLUDE_PATH=xxx:$C_INCLUDE_PATH

# CPLUS_INCLUDE_PATH: C++ header files
export CPLUS_INCLUDE_PATH=xxx:$CPLUS_INCLUDE_PATH

# CPATH: both C and C++ (and any other language)
export CPATH=xxx:$CPATH

注意: include 效果与 PATH 查找的顺序有关, 一般范围小的放 PATH 前面。

1.2.1. 设置 LD_LIBRARY_PATH 不起作用解决

遇到的现象:

/usr/local/lib 加入共享库搜索目录中: sudo vim /etc/profile

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib 加入到最后, 保存该文件即可。

重启 linux, 在终端执行 echo $LD_LIBRARY_PATH, 之后执行结果中没有 /usr/local/lib

解决方法:

部分 Linux 系统设置 LD_LIBRARY_PATH 变量, 并不能生效, 此时需要将变量值写入 /etc/ld.so.conf 文件中, 如下所示:

include /usr/local/lib

然后执行如下命令即可生效。

sudo ldconfig /etc/ld.so.conf

1.2.2. Why LD_LIBRARY_PATH is bad

本人在 /etc/environment 中设置了环境变量:

JAVA_HOME=/opt/java
ORACLE_HOME=/home/user2/
LD_LIBRARY_PATH=/usr/lib:/usr/local/lib

正常情况下执行没有问题, 但使用 sudo XXXXXXX 的时候, 其他变量都好使, 除了 LD_LIBRARY_PATH;

经探索, 找到原因:

据说因为安全原因, Linux 系统做了限制。

LD_LIBRARY_PATH not loading from .profile nor /etc/environment

Ubuntu 不能在 profile, environment, .bashrc 中设置 LD_LIBRARY_PATH;

解决办法:

编辑 /etc/ld.so.conf 文件, 将指定的路径加上, 或者在 /etc/ld.so.conf.d/ 目录中添加一个新的配置文件。

1.2.3. LIBRARY_PATH

  • LIBRARY_PATH: 程序编译时链接 so

1.2.4. 动态库链接和路径问题总结

  • 动态库链接顺序

假如程序 A 依赖动态库 libB, 动态库 libB 又依赖动态库 libC
则需要这样编译: gcc $(CFLAGS) -o $(TARGET) $(OBJS) -lB -lC, 因为 gcc 在链接过程中 -l 的参数顺序有要求, 参数右侧的库先于左侧的库链接。

  • 动态库搜索路径

gcc -L 选项在链接时指定动态库路径, 编译通过, 但是执行时会找不到路径;

gcc -Wl -rpath 选项在运行时指定路径, 运行时按照指定路径寻找动态库;

也就是说处理动态链接库时有 2 个路径: 链接时路径和运行时路径, 2 个路径是分开的; 如: $(CC) -o $@ $^ -L/home/db2 -lfinger_client -Wl,-rpath=/home/db2

动态库的搜索路径搜索的先后顺序是:

  1. 编译目标代码时指定的动态库搜索路径;
  2. 环境变量 LD_LIBRARY_PATH 指定的动态库搜索路径;
  3. 配置文件 /etc/ld.so.conf 中指定的动态库搜索路径;
  4. 默认的动态库搜索路径 /lib ;
  5. 默认的动态库搜索路径 /usr/lib

1.3. 静态链接和动态链接优缺点

静态链接: 不同的程序开发者和部门能够相对独立地开发和测试自己的程序模块, 大大促进了程序开发效率。

优点:

  1. 代码装载速度快, 执行速度略比动态链接库快;
  2. 只需保证在开发者的计算机中有正确的。LIB 文件, 在以二进制形式发布程序时不需考虑在用户的计算机上。LIB 文件是否存在及版本问题。

缺点:

  1. 浪费内存和磁盘空间、模块更新困难;
  2. 会给对程序的更新、部署和发布带来很多麻烦。

例如: 在多进程操作系统情况下, 假如每个程序内部除了都保留着 printf() 函数、scanf() 函数、strlen() 等公用库函数, 还有数量相当可观的其他库函数及它们所需要的辅助数据结构。

假如说程序 program1 和 program2 都公用 lib.o 这个模块, 同时运行 Program1 和 program2 时, lib.o 在磁盘中和内存中都有两个副本。

动态链接基本思想: 把链接过程推迟到运行时再进行。

优点:

  1. 解决了共享目标文件多个副本浪费磁盘和内存空间的问题;
  2. 使开发过程中各个模块 更加独立, 耦合度变小, 便于不同的开发者和开发组织之间独立进行开发和测试。
  3. 在内存中共享一个模块: 节省内存, 还可减少物理页面的换入换出, 也可增加 CPU 缓存的命中率, 因为不同进程间的数据和指令访问都集中在了同一个共享模块上。
  4. 加强程序的兼容性, 一个程序在不同平台运行时可以动态地链接到由操作系统提供的动态链接库。

例如: program1 编译后生成 program1.o 目标文件, 其运行后, 当再运行 program2, 只需加载 program2.o, 不需重新加载 lib.o, 因为内存中已经存在来一份 lib.o 的副本。

缺点:

如果使用载入时动态链接, 程序启动时发现 DLL 不存在, 系统将终止程序并给出错误信息。而使用运行时动态链接, 系统不会终止, 但由于 DLL 中的导出函数不可用, 程序会加载失败; 速度比静态链接慢。当某个模块更新后, 如果新模块与旧的模块不兼容, 那么那些需要该模块才能运行的软件, 则会出现错误。

在 Linux 系统中, ELF 动态链接文件被称为动态共享对象 (DSO, Dynamic Shared Objects), 以 “.so” 为扩展名; 在 Windows 系统中, 动态连接文件被称为动态链接库 (Dynamical Linking Library), 以 “dll” 为扩展名的文件 i, 而静态链接则是以 “.lib” 为扩展名的文件。

据估算, 动态链接与静态链接相比, 性能损失大约在 5% 以下, 但这点性能损失用来换取程序在空间上的节省和程序构建和升级时的灵活性, 是相当值得的。

动态链接比静态链接慢的主要原因: 动态链接下对于全局和静态的数据访问都要进行复杂的 GOT 定位, 然后间接寻址; 对于模块间的调用也要先定位 GOT(全局偏移表), 然后再进行间接跳转。

另一个减慢运行速度的主要原因: 动态链接的链接工作是在运行时完成的。动态链接器会寻找并装载所需要的共享对象, 然后进行符号查找地址重定位工作。

解决方法是:

延迟绑定 (Lazy Binding): 当函数第一次被用到时才进行绑定(符号查找, 重定位等), 不用到则不进行绑定。 这样可大大加快程序的启动速度。

1.4. gcc 寻找头文件的路径(按照 1->2->3 的顺序)

  1. 在 gcc 编译源文件的时候, 通过参数 -I 指定头文件的搜索路径, 如果指定路径有多个路径时, 则按照指定路径的顺序搜索头文件。命令形式如: “gcc -I /path/where/theheadfile/in sourcefile.c”, 这里源文件的路径可以是绝对路径, 也可以是相对路径。eg:

设当前路径为 /root/test, include_test.c 如果要包含头文件 “include/include_test.h”, 有两种方法:

  1. include_test.c 中#include “include/include_test.h” 或者#include “/root/test/include/include_test.h”, 然后 gcc include_test.c 即可
  2. include_test.c 中#include <include_test.h> 或者 #include <include_test.h>, 然后 gcc –I include include_test.c 也可
  1. 通过查找 gcc 的环境变量 C_INCLUDE_PATH/CPLUS_INCLUDE_PATH/OBJC_INCLUDE_PATH 来搜索头文件位置。

  2. 再找内定目录搜索, 分别是

/usr/include

/usr/local/include

/usr/lib/gcc-lib/i386-linux/2.95.2/include

最后一行是 gcc 程序的库文件地址, 各个用户的系统上可能不一样。

gcc 在默认情况下, 都会指定到 /usr/include 文件夹寻找头文件。

gcc 还有一个参数: -nostdinc, 它使编译器不再系统缺省的头文件目录里面找头文件, 一般和 -I 联合使用, 明确限定头文件的位置。在编译驱动模块时, 由于非凡的需求必须强制 GCC 不搜索系统默认路径, 也就是不搜索 /usr/include 要用参数 -nostdinc, 还要自己用 -I 参数来指定内核头文件路径, 这个时候必须在 Makefile 中指定。

  1. #include 使用相对路径的时候, gcc 最终会根据上面这些路径, 来最终构建出头文件的位置。如 #include <sys/types.h> 就是包含文件 /usr/include/sys/types.h

1.5. glibc

1.5.1. 查看版本号

$ ldd --version

1.5.2. glibc vs musl libc

musl 官网有不支持的功能以及和 glibc 行为上的差别: Functional differences from glibc

从性能上看, malloc 系列函数和 memcpy 系列函数可能实现较慢。尤其是 malloc 的性能, 在多线程环境下会显著造成瓶颈, 原因在于 musl 的 malloc 实现在每次 malloc 时都需要对全局变量加锁解锁, 导致严重的竞争现象。

然而 musl 的源代码很简单, 不像 glibc 的代码一样乱如垃圾场, 将性能较慢的函数换掉就能取得较大的性能提升。

我用的 Gentoo Linux 系统就使用了 musl libc(同时还采用了 LLVM clang/lld/libc++/libc++abi/libunwind), 由于开了全局 LTO, 编译 chromium 这类大软件时 lld 的性能很差, 因此对几个关键函数进行了替换, 目前的编译速度要强于同机器、同内核、同样的 clang、同样的优化参数但使用 glibc 的系统。

https://github.com/12101111/musl-malloc

这是我改过的源码, malloc 换成了 rpmalloc, mem 系列函数 x86 的换成了 Meta 开源的一个汇编实现, arm64 的换成了 arm 开源的汇编实现, 在 intel 若干 cpu 和 apple m1 max 上都在使用, 在高频内存分配场景下效果很明显。

如果不想改 musl 源码, 也可以直接链接高性能的 malloc, 比如微软的 https://github.com/microsoft/mimalloc 或者是 GitHub - mjansson/rpmalloc: Public domain cross platform lock free thread caching 16-byte aligned memory allocator implemented in C

mimalloc 应该是目前性能最高的开源 malloc 实现, 使用安全模式的版本也比大部分 malloc 快。rpmalloc 差不多快, 而且代码很短。

其实 musl 的 qsort 实现也不是最快的, 最快的算法是 rust 标准库用的 pdqsort, 但是在 c 中正确的实现这个算法还是比较麻烦的, 因此就没有改, 毕竟 glibc 的 qsort 也不是最快的。

如果用了 musl, 建议一并使用 LLVM libc++, Apple 和 Google 两大企业支持, 性能比 libstdc++强不少。

g++和 gcc 是 GNU 编译器集合中的两个组件, g++是 GNU C++编译器, gcc 是 GNU C 语言编译器。这两个编译器都使用 glibc 作为标准 C 库, glibc 是 GNU 操作系统的标准 C 库, 为支持 C 程序提供了许多函数和服务。

简单来说, glibc 是 C 标准库的一个实现, 它包括头文件、函数库和其他的应用程序。而 g++和 gcc 则是编译器, 它们将源代码编译成可执行文件, 通过调用 glibc 库中的函数来执行程序。

当您使用 g++编译 C++代码时, 它会链接到 glibc 库, 并使用该库中的函数以进行内存分配、字符串处理等操作。同样, 当您使用 gcc 编译 C 代码时, 它也会链接到 glibc 库并使用标准 C 库中的函数。

因此, glibc 库是与 g++和 gcc 紧密相关的一部分, 它为这些编译器提供了基本的 C 和 C++支持, 并为应用程序提供了一组丰富的函数库。

musl libc 和 glibc 都是 C 语言的标准库。它们的功能类似, 都包括了很多常用的函数, 如字符串处理、文件操作、进程控制等等。它们的主要区别在于实现方式和设计哲学。

musl libc 是一个轻量级的 C 标准库, 它的设计目标是尽可能小而快, 同时提供最高的代码质量和安全性。musl libc 的源代码通常较短, 编译速度较快, 没有额外的依赖, 因此非常适用于嵌入式系统和轻量级应用程序。

相比之下, glibc 是一个较重的 C 标准库, 它的设计目标是提供更广泛的功能和更高的兼容性。glibc 的源代码较长, 编译速度较慢, 有许多库文件和依赖项, 因此通常用于桌面系统和服务器软件开发。

总的来说, musl libc 和 glibc 都是优秀的 C 标准库, 但在不同的场景下选择合适的库对于开发者来说非常重要。

musl libc 和 glibc 是两个常见的 C 标准库实现, 它们有一些差异。下面列出了其中一些主要的差异:

  1. 大小和速度: musl libc 要小得多, 因为它没有像 glibc 那样提供大量的额外功能。相反, musl libc 专注于尽可能减少代码大小和函数调用开销, 以提高性能。
  2. 兼容性: glibc 是 Linux 系统上最常见的 C 标准库, 并且具有广泛的兼容性, 支持许多架构和操作系统。相比之下, musl libc 对其他平台和操作系统的移植性较差。
  3. 实现方法: musl libc 是使用静态链接编译的, 这使得它更易于构建和管理, 并且不需要动态链接器。相反, glibc 使用动态链接器, 这也使得它更灵活, 因为它可以动态加载所需的库。
  4. POSIX 标准: musl libc 更加严格地遵循 POSIX 标准, 而 glibc 则添加了一些扩展, 以提供更多的功能和兼容性。
  5. 错误处理: musl libc 实现的错误处理更严格和更规范, 而 glibc 则有更多的错误处理选项, 并且支持不同的语言环境。
  6. 版权问题: 由于采用了 BSD 许可证, musl libc 比 glibc 更容易以开源、商业和专有软件的形式使用。

综上所述, 选择使用 musl libc 还是 glibc 取决于您的具体需求。如果您需要一个小巧且速度较快的 C 标准库, 在 Linux 系统上使用, 则可以考虑使用 musl libc。如果您需要更广泛的兼容性和功能, 则可以使用 glibc。

https://musl.cc/#binaries

1.6. gcc

1.6.1. gcc 编译器的 std=c99 选项

gcc 默认使用的是 C89 的标准, 而 C89 的标准不支持在 for 中定义循环变量, 而在 for 循环中需要定义循环变量的话, 需要在 C99 标准中才支持, 因此需要增加 -std=c99-std=gun99 参数才能编译通过。

1.6.2. 编译 gcc

Follow the instructions at https://gcc.gnu.org/wiki/InstallingGCC

Specifically, don’t install ISL manually in some non-standard path, because GCC needs to find its shared libraries at run-time.

The simplest solution is to use the download_prerequisites script to add the GMP, MPFR, MPC and ISL source code to the GCC source tree, which will cause GCC to build them for you automatically, and link to them statically.

If it provides sufficiently recent versions, use your OS package management system to install the support libraries in standard system locations. For Debian-based systems, including Ubuntu, you should install the packages libgmp-dev, libmpfr-dev and libmpc-dev. For RPM-based systems, including Fedora and SUSE, you should install gmp-devel, mpfr-devel and libmpc-devel (or mpc-devel on SUSE) packages. The packages will install the libraries and headers in standard system directories so they can be found automatically when building GCC.

Alternatively, after extracting the GCC source archive, simply run the ./contrib/download_prerequisites script in the GCC source directory. That will download the support libraries and create symlinks, causing them to be built automatically as part of the GCC build process. Set GRAPHITE_LOOP_OPT=no in the script if you want to build GCC without ISL, which is only needed for the optional Graphite loop optimizations.

tar xzf gcc-4.6.2.tar.gz
cd gcc-4.6.2
./contrib/download_prerequisites
cd ..
mkdir objdir
cd objdir
$PWD/../gcc-4.6.2/configure --prefix=$HOME/GCC-4.6.2 --enable-languages=c,c++,fortran,go
make
make install

# option
export PATH=$HOME/GCC-4.6.2/bin:$PATH

1.7. GNU 工具

使用 objdump、readelf、nm 等命令可以查询目标文件的详细内容

gcc -print-search-dirs 可以查看 gcc 在编译、链接过程中的共享库搜索路径。

1.7.1. ldd

这是 Linux 内核中自带的脚本, 可以用来查看可执行文件链接了哪些共享库

1.7.2. ldconfig

ldconfig: 这是个可执行程序, 隶属于 GNU, 作用是在默认搜寻目录 (/lib 和/usr/lib) 以及共享库配置文件 /etc/ld.so.conf 内所列的目录下, 搜索出共享库文件 (lib*.so*), 进而创建出 ld-linux.so 所需要的链接和缓存文件。缓存文件默认为 /etc/ld.so.cache, 此文件保存已排好

1.7.3. strip

strip <可执行文件名>: 去除符号表, 可以给可执行文件瘦身

strip -s xx

1.7.4. objdump

查看 so/bin 中的依赖

objdump - display information from object files

apt-get install binutils

objdump -T db2

db2:     file format elf64-x86-64

DYNAMIC SYMBOL TABLE:
0000000000000000      DF *UND*	0000000000000000  GLIBC_2.2.5 __errno_location
0000000000000000      DF *UND*	0000000000000000  GLIBC_2.2.5 setresuid
0000000000000000      DF *UND*	0000000000000000              SQLGetData
  • objdump -p a.out 命令查看动态库引用的版本
  1. 查看符号表: objdump -t xxx.so 。-T 和 -t 选项在于 -T 只能查看动态符号, 如库导出的函数和引用其他库的函数, 而 -t 可以查看所有的符号, 包括数据段的符号
  2. nm 命令可以查看, linux 以及 windows 下的。o , .obj 文件中的符号列表
objdump -x obj 以某种分类信息的形式把目标文件的数据组织(被分为几大块)输出 <可查到该文件的所有动态库> 
objdump -t obj 输出目标文件的符号表 ()
objdump -h obj 输出目标文件的所有段概括 ()
objdump -j .text/.data -S obj 输出指定段的信息, 大概就是反汇编源代码把
objdump -S obj C 语言与汇编语言同时显示
  1. rodata
    rodata 的意义同样明显, ro 代表 read only, 即只读数据 (const)。关于 rodata 类型的数据, 要注意以下几点:
    常量不一定就放在 rodata 里, 有的立即数直接编码在指令里, 存放在代码段 (.text) 中。
    对于字符串常量, 编译器会自动去掉重复的字符串, 保证一个字符串在一个可执行文件 (EXE/SO) 中只存在一份拷贝。
    rodata 是在多个进程间是共享的, 这可以提高空间利用率。
    在有的嵌入式系统中, rodata 放在 ROM(如 norflash) 里, 运行时直接读取 ROM 内存, 无需要加载到 RAM 内存中。
    在嵌入式 linux 系统中, 通过一种叫作 XIP(就地执行)的技术, 也可以直接读取, 而无需要加载到 RAM 内存中。

由此可见, 把在运行过程中不会改变的数据设为 rodata 类型的, 是有很多好处的: 在多个进程间共享, 可以大大提高空间利用率, 甚至不占用 RAM 空间。同时由于 rodata 在只读的内存页面 (page) 中, 是受保护的, 任何试图对它的修改都会被及时发现, 这可以帮助提高程序的稳定性。

  1. 查看依赖项: objdump -x xxx.so | grep “NEEDED”

  2. .symtab 是符号表、.rodata 是只读数据、还有。comment 和。debug_info 等等。

1.7.5. 查看 so/bin 中字符串

strings -h

1.8. 问题

1.8.1. Target requires the language dialect “CXX17” (with compiler extensions), but CMake does not know the compile flags to use to enable it

需要:

  • CMAKE > 3.8
  • GCC/C++17 > 5.1.0

Still the same "dialect “CXX17” error ?In my case, something else was needed to make it works:

sudo ln -s /usr/local/bin/gcc /usr/local/bin/cc

i’ve setted -DCMAKE_C_COMPILER=/usr/local/bin/gcc and -DCMAKE_CXX_COMPILER=/usr/local/bin/g++ to cmake command options and had worked fine.

1.8.2. 解决 “错误: 只允许在 C99 模式下使用‘for’循环初始化声明” 问题

$ make CFLAGS=-std=c99
# 或者
$ export CFLAGS=-std=c99

1.8.3. Linux error while loading shared libraries: cannot open shared object file: No such file or directory

sudo find / -name the_name_of_the_file.so
echo $LD_LIBRARY_PATH

LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/my_library/
export LD_LIBRARY_PATH
./my_app

sudo ldconfig

sudo ldconfig /opt/intel/oneapi/mkl/2021.2.0/lib/intel64

ln -s /lib/libpthread_rt.so /lib/libpthread_rt.so.1

installed the -dev version of that package.

sudo apt install libgconf2-dev

1.8.4. /usr/bin/ld: cannot find -lzlib

LD_DEBUG=all make
ld --help

ld -lzlib --verbose
==================================================
attempt to open /usr/x86_64-linux-gnu/lib64/libzlib.so failed
attempt to open /usr/x86_64-linux-gnu/lib64/libzlib.a failed
attempt to open /usr/local/lib64/libzlib.so failed
attempt to open /usr/local/lib64/libzlib.a failed
attempt to open /lib64/libzlib.so failed
attempt to open /lib64/libzlib.a failed
attempt to open /usr/lib64/libzlib.so failed
attempt to open /usr/lib64/libzlib.a failed
attempt to open /usr/x86_64-linux-gnu/lib/libzlib.so failed
attempt to open /usr/x86_64-linux-gnu/lib/libzlib.a failed
attempt to open /usr/local/lib/libzlib.so failed
attempt to open /usr/local/lib/libzlib.a failed
attempt to open /lib/libzlib.so failed
attempt to open /lib/libzlib.a failed
attempt to open /usr/lib/libzlib.so failed
attempt to open /usr/lib/libzlib.a failed
/usr/bin/ld.bfd.real: cannot find -lzlib

sudo ln -s /usr/lib/libz.so.1.2.8 /usr/lib/libzlib.so

g++ -L/home/user/myDir -lxyz myprog.cpp -o myprog
g++ main.cpp -o main -L/home/taylor -lswift

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/taylor
export LIBRARY_PATH=/opt/lib/

locate libiconv.so

1.8.5. golang 程序运行出错, version `GLIBC_2.32‘ not found

在 Linux 上, 如果不是本机编译的可能出现 GLIBC 版本号对不上的情况。

2. 交叉编译

首先要编译 gcc 然后再编译 glib 这一些。

Shuah_Khan_cross_compile_linux.pdf

2.1. 使用网上编译好的

编译太费时间了, 可以选用网上编译好的, 注意查毒。选用大公司、著名公司的, 一般不会出大的问题。

我们这里选用 linaro 编译的。Linaro, 一间非营利性质的开放源代码软件工程公司, 主要的目标在于开发不同半导体公司系统单芯片 (SoC) 平台的共通软件, 以促进消费者及厂商的福祉。针对于各个成员推出的 ARM 系统单芯片 (SoC), 它开发了 ARM 开发工具、Linux 内核以及 Linux 发行版(包括 Android 及 Ubuntu) 的主要自动建构系统。

由 ARM、飞思卡尔、IBM、Samsung、ST-Ericsson 及德州仪器 (TI) 等半导体厂商联合, 在 2010 年 3 月成立。2010 年 6 月在台北对外宣布这个消息。预计在 2010 年 11 月, 推出第一版以 ARM Cortex-A 为核心的 SoC 进行效能优化的软件工具。

地址

2.1.1. 问题: 此时如果用 /usr/bin/aarch64-linux-gnu-gcc 去编译 c 源码时会发现所有头文件都找不着, 因为 gcc-c+±aarch64-linux-gnu.x86_64 包不负责头文件的安装

下载上面地址中的 gcc-linaro-4.9-2016.02-x86_64_aarch64-linux-gnu.tar.xzsysroot-linaro-eglibc-gcc4.9-2016.02-aarch64-linux-gnu.tar.xz 然后解压在 /opt/linaro/aarch64-linux-gnu 里面就可以顺滑的编译了:

[root@Linux /opt/linaro/aarch64-linux-gnu]# ll
gcc-linaro-4.9-2016.02-x86_64_aarch64-linux-gnu/
sysroot-linaro-eglibc-gcc4.9-2016.02-aarch64-linux-gnu/

产生的原因: 使用官方的安装方法 yum search aarch64 然后 yum install gcc-c++-aarch64-linux-gnu.x86_64

运行 aarch64-linux-gnu-gcc 命令查看它的 sysroot 目录: 

root@mycentos:/root # aarch64-linux-gnu-gcc -v
Using built-in specs.
COLLECT_GCC=aarch64-linux-gnu-gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/aarch64-linux-gnu/4.8.5/lto-wrapper
Target: aarch64-linux-gnu
Configured with: ../gcc-4.8.5-20150702/configure --bindir=/usr/bin --build=x86_64-redhat-linux-gnu --datadir=/usr/share --disable-decimal-float --disable-dependency-tracking --disable-gold --disable-libgomp --disable-libmudflap --disable-libquadmath --disable-libssp --disable-nls --disable-plugin --disable-shared --disable-silent-rules --disable-sjlj-exceptions --disable-threads --enable-checking= --enable-gnu-unique-object --enable-initfini-array --enable-languages=c,c++ --enable-linker-build-id --enable-nls --enable-obsolete --enable-targets=all --exec-prefix=/usr --host=x86_64-redhat-linux-gnu --includedir=/usr/include --infodir=/usr/share/info --libexecdir=/usr/libexec --localstatedir=/var --mandir=/usr/share/man --prefix=/usr --program-prefix=aarch64-linux-gnu- --sbindir=/usr/sbin --sharedstatedir=/var/lib --sysconfdir=/etc --target=aarch64-linux-gnu --with-bugurl=http://bugzilla.redhat.com/bugzilla/ --with-linker-hash-style=gnu --with-newlib --with-sysroot=/usr/aarch64-linux-gnu/sys-root --with-system-libunwind --with-system-zlib --without-headers --with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/isl-install --with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/cloog-install
Thread model: single
gcc version 4.8.5 20150623 (Red Hat 4.8.5-16) (GCC)

找到 sysroot 目录 --with-sysroot=/usr/aarch64-linux-gnu/sys-root
将解压出来的 etc lib sbin usr var 这些目录全部移动到 /usr/aarch64-linux-gnu/sys-root 中, 就可以用 aarch64-linux-gnu-gcc 编译源码了。

2.2. 在 CentOS6.5 上构建高版本 gcc 和 glibc 的交叉编译环境

其它教程: GCC Cross-Compiler

2.2.1. 前言

本文介绍如何在 CentOS6.5(或 RHEL6.5) 上基于源码包构建高版本 gcc 和 glibc, 搭建交叉编译环境; 并举例说明基于这个交叉编译环境, 编译 GNU tool chain 工程(以编译 Python-3 为例)、CMake 工程(以编译 MySQL5.7 为例)、以及其他 C++工程(以编译 boost 库为例)的方法。最后再简要介绍一下, 如何使用交叉编译环境中的 gdb 进行调试。

升级编译环境是一个非常复杂而耗时的工程, 因为 gcc 和 glibc 之间存在相互依赖, 所以只能先构建出高版本的 GNU 工具链和高版本 gcc, 再用高版本 gcc 和 GNU 工具链构建高版本 glibc, 而后再基于新构建出的 glibc 重新构建 gcc, 最后再基于新构建的 glibc 和 gcc 重新构建 GNU 工具链、并进一步构建出 CMake 工具。(注: gcc 和 glibc 的编译时间都非常长, 而且 gcc 要编译两次)

2.2.2. 源码包准备

开始构建工作之前, 我们需要先准备以下源码包:

GCC 和 glibc:

gcc-10.2.1.tar.bz2
glibc-2.24.tar.gz

GNU 工具及其依赖库:

autoconf-2.68.tar.gz
automake-1.16.tar.gz
binutils-2.36.tar.gz
bison-3.4.2.tar.gz
expat-2.3.0.tar.gz
gdb-10.1.tar.gz
libidn-1.35.tar.gz
m4-1.4.18.tar.gz
make-4.3.tar.gz
texinfo-6.7.tar.gz

Linux 内核源码包:

linux-5.11.4.tar.xz

Python-3 及其依赖库 (Python-3 依赖于 libffi 库):

libffi-3.3.tar.gz
Python-3.9.2.tgz

Git 工具及其依赖库 (git 依赖于 zlib 库):

zlib-1.2.11.tar.gz
git-2.28.0.tar.gz

CMake3:

cmake-3.4.3.tar.gz

MySQL5.7 及其依赖库:

ncurses-6.2.tar.gz
openssl-1.1.1-stable.tar.gz
boost_1_59_0.tar.gz
mysql-5.7.32.tar.gz

注:

  • 除 glibc 和 Linux 内核对版本有特定要求以外, 其他源码包都可以采用其他版本(当前最新稳定版即可)。
  • MySQL5.7 依赖于 boost_1_59_0, 我们就以编译这个版本的 boost 库为例; 如需编译 MySQL8.0, 则需要 boost_1_73_0 版本。
  • 编译 glibc 需要 Linux-3.2.0 版本以上的内核源码头文件, 本例中使用了 Linux-5.11.4, 使用其他版本也可以。
  • CentOS6 使用的内核版本是 Linux-2.6, 然而, 并不是所有的 glibc 版本都支持低于 Linux-3.2.0 版本的内核。编译 glibc 的帮助文档中提及, 可以通过指定参数–enable-kernel=2.6.32, 打开对 Linux-2.6 的支持, 但实际上只有 glibc-2.24 才支持这个参数, 其他版本的 glibc 指定这个参数都是无效的。我在这个坑上卡了很久, 下表列出了 glibc 版本和 Linux 内核版本间的兼容性关系:
glibc    Released      Requires Linux kernel version
----------------------------------------------------------
2.31     1 Feb 2020    ?   (same as 2.26?)
2.30     1 Aug 2019    ?   (same as 2.26?)
2.29     1 Feb 2019    ?   (same as 2.26?)
2.28     1 Aug 2018    ?   (same as 2.26?)
2.27     2 Feb 2018    ?   (same as 2.26?)
2.26     2 Aug 2017    3.2 (or higher)
2.25     5 Feb 2017    ?   (same as 2.24?)
2.24     4 Aug 2016    on i[4567]86 and x86_64  ->  2.6.32
2.24     4 Aug 2016    on other platforms       ->  3.2
Note 1:  Some minor architectures require a higher kernel
         version than listed above.
Note 2:  Your Linux distribution may vary from the above,
         as glibc can optionally be configured at compile
         time to require a kernel version newer than the
         oldest supported version.
 
For reference of other Linux distributions:
Linux distribution    glibc version    Requires Linux kernel version
----------------------------------------------------------
Debian 10 Buster      2.28             3.2 ?
Debian 9 Stretch      2.24 on x86      2.6.32
Debian 9 Stretch      2.24 on other    3.2
Ubuntu 20.04          2.31             3.2 ?
Ubuntu 18.04          2.27             3.2 ?
Ubuntu 17.10          2.26             3.2
Ubuntu 17.04          2.24             ? 
Ubuntu 16.10          2.24             ?
Ubuntu 16.04          2.23             ?

2.2.3. 编译环境准备

本例工作在 /opt 目录下, 使用 /opt/mydev 目录作为编译和临时安装目录, 使用 /opt/devtools 目录作为最终安装目录。我们在 /opt/mydev 目录下创建如下 3 个子目录:

  • src 子目录(源码包目录): 我们把源码包都拷贝至这个子目录下。
  • build 子目录(编译目录): 构建过程中, 我们在这个子目录中进行编译。
  • install 子目录(临时安装目录): 如前文所述, 我们需要反复编译 gcc 和 glibc, 因而需要将中间编译结果安装到此目录。

基于上述目录结构, 构建过程大致过程如下:

  1. 先构建 gcc, 并安装在临时安装目录 install 中;
  2. 构建 glibc 所需的依赖包, 并安装在临时安装目录 install 中;
  3. 基于 install 目录中的 gcc 和其他依赖库, 构建 glibc 并安装在 devtools 子目录;
  4. 基于 devtools 子目录中的 glibc 和 install 子目录中的 gcc, 重新构建 gcc, 并安装在 devtools 子目录;
  5. 最后基于 devtools 子目录中的 glibc 和 gcc, 重新构建全部开发库, 并安装至 devtools 子目录。

我们进入 src 子目录, 并解压所有源码包:

cd /opt/mydev/src
tar zxf autoconf-2.68.tar.gz
tar zxf automake-1.16.tar.gz
tar zxf binutils-2.36.tar.gz
tar zxf bison-3.4.2.tar.gz
tar zxf boost_1_59_0.tar.gz
tar zxf boost_1_73_0.tar.gz
tar zxf bzip2-1.0.6.tar.gz
tar zxf cmake-3.4.3.tar.gz
tar zxf expat-2.3.0.tar.gz
tar jxf gcc-10.2.1.tar.bz2
tar zxf gdb-10.1.tar.gz
tar zxf git-2.28.0.tar.gz
tar zxf glibc-2.24.tar.gz
tar zxf libffi-3.3.tar.gz
tar zxf libidn-1.35.tar.gz
tar Jxf linux-5.11.4.tar.xz
tar zxf m4-1.4.18.tar.gz
tar zxf make-4.3.tar.gz
tar zxf mysql-5.7.32.tar.gz
tar zxf ncurses-6.2.tar.gz
tar zxf openssl-1.1.1-stable.tar.gz
tar zxf Python-3.9.2.tgz
tar zxf texinfo-6.7.tar.gz
tar zxf zlib-1.2.11.tar.gz

安装 Linux 内核头文件(编译 glibc 只需要头文件):

cd /opt/mydev/src/linux-5.11.4
make headers_install INSTALL_HDR_PATH=/opt/devtools

2.2.4. 构建过程(编译并行度根据系统性能调整)

  1. 构建编译 gcc 和 glibc 依赖的工具和库(安装至临时安装目录)

构建 Python-3:

mkdir /opt/mydev/build/Python3
cd /opt/mydev/build/Python3
/opt/mydev/src/Python-3.9.2/configure --prefix=/opt/mydev/install
make -j4
make install

构建 binutils:

mkdir /opt/mydev/build/binutils
cd /opt/mydev/build/binutils
/opt/mydev/src/binutils-2.36/configure --prefix=/opt/mydev/install
make -j4
make install

构建 gmake:

mkdir /opt/mydev/build/gmake
cd /opt/mydev/build/gmake
/opt/mydev/src/make-4.3/configure --prefix=/opt/mydev/install
make -j4
make install

构建 bison:

mkdir /opt/mydev/build/bison
cd /opt/mydev/build/bison
/opt/mydev/src/bison-3.4.2/configure --prefix=/opt/mydev/install
make -j4
make install
  1. 构建 gcc(时间很长, 可以根据系统性能增加并行度)
mkdir /opt/mydev/build/gcc
cd /opt/mydev/build/gcc
/opt/mydev/src/gcc-10.2.1/configure --prefix=/opt/mydev/install --disable-multilib
make -j4
make install
  1. 构建 glibc(也非常耗时)
export CC=/opt/mydev/install/bin/gcc
export CXX=/opt/mydev/install/bin/g++
export C_INCLUDE_PATH=/opt/mydev/install/lib/gcc/x86_64-pc-linux-gnu/10.2.1/include
export CPLUS_INCLUDE_PATH=${C_INCLUDE_PATH}
OLDPATH=${PATH}
export PATH=/opt/mydev/install/bin:${PATH}
mkdir /opt/mydev/build/glibc
cd /opt/mydev/build/glibc
/opt/mydev/src/glibc-2.24/configure --prefix=/opt/devtools --with-headers=/opt/devtools/include --without-selinux --enable-kernel=2.6.32 --with-binutils=/opt/mydev/install --disable-werror
make -j4
export CC=
export CXX=
export C_INCLUDE_PATH=
export CPLUS_INCLUDE_PATH=
export PATH=${OLDPATH}
make install

注:

  • 需要把编译器和 GNU 工具路径指定到临时安装目录
  • 需要指定 Linux 内核头文件目录
  • 必须指定 --disable-werror 参数, 因为 glibc-2.24 中的代码风格, 在高版本 gcc 中默认会报错
  • 编译完成后清空环境变量, 后续编译过程会重新指定
  1. 重新构建 gcc(安装至最终安装目录)
GLIBCDIR=/opt/devtools
LDFLAGS="-Wl,-q"
CFLAGS="-L${GLIBCDIR}/lib -Wl,--rpath=${GLIBCDIR}/lib -Wl,--dynamic-linker=${GLIBCDIR}/lib/ld-linux-x86-64.so.2"
export CC=/opt/mydev/install/bin/gcc
export CXX=/opt/mydev/install/bin/g++
export C_INCLUDE_PATH=${GLIBCDIR}/include
export CPLUS_INCLUDE_PATH=${GLIBCDIR}/include
mkdir /opt/mydev/build/gcc-new
cd /opt/mydev/build/gcc-new
/opt/mydev/src/gcc-10.2.1/configure --prefix=/opt/devtools --disable-multilib CFLAGS="${CFLAGS}" CXXFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}"
make distclean
/opt/mydev/src/gcc-10.2.1/configure --prefix=/opt/devtools --disable-multilib CFLAGS="${CFLAGS}" CXXFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}"
make -j4
export CC=
export CXX=
export C_INCLUDE_PATH=
export CPLUS_INCLUDE_PATH=
make install

注:

  • 交叉编译时, 需要使用-Wl 参数对 linker 进行设置, 其中"–rpath"用于指定搜索动态库的目录, "–dynamic-linker"用于指定 ELF 的动态链接器;
  • 需要使用临时安装目录中的 gcc 进行编译(即自举);
  • 必须执行一次 make distclean, 否则后面的编译过程可能会失败。
  1. 设置编译相关环境变量
GLIBCDIR=/opt/devtools
LDFLAGS="-Wl,-q"
CFLAGS="-isystem /opt/devtools/lib/gcc/x86_64-pc-linux-gnu/10.2.1/include -isystem /opt/devtools/lib/gcc/x86_64-pc-linux-gnu/10.2.1/../../../../include/c++/10.2.1/backward -isystem /opt/devtools/lib/gcc/x86_64-pc-linux-gnu/10.2.1/../../../../include/c++/10.2.1/x86_64-pc-linux-gnu -isystem /opt/devtools/lib/gcc/x86_64-pc-linux-gnu/10.2.1/../../../../include/c++/10.2.1 -isystem /opt/devtools/include -L${GLIBCDIR}/lib -L${GLIBCDIR}/lib64 -Wl,--rpath=${GLIBCDIR}/lib -Wl,--dynamic-linker=${GLIBCDIR}/lib/ld-linux-x86-64.so.2"
export CC=/opt/devtools/bin/gcc
export CXX=/opt/devtools/bin/g++
export PATH=/opt/devtools/bin:${PATH}
export C_INCLUDE_PATH=${GLIBCDIR}/include
export CPLUS_INCLUDE_PATH=${GLIBCDIR}/include
OLDPATH=${PATH}
export PATH=/opt/mydev/install/bin:${PATH}

注:

  • 参数-isystem 可以严格指定编译器搜索头文件目录的顺序, 由于 gcc/g++的头文件可能与 glibc 的头文件重名, 因而必须要保证优先搜索 gcc/g++的头文件目录, 否则编译会出错;
  • 后续编译即可使用最终安装目录中的编译工具, 但编译 GNU tool chain(binutils) 时还需要使用临时安装目录中的 binutils, 因而先把环境变量 PATH 保存在 OLDPATH 中, 待编译完 binutils 之后再恢复
  1. 构建 GNU 库和工具(安装至最终安装目录)

构建 expat(GNU 工具依赖于这个库):

mkdir /opt/mydev/build/expat-new
cd /opt/mydev/build/expat-new
/opt/mydev/src/expat-2.3.0/configure --prefix=/opt/devtools CFLAGS="${CFLAGS} -fPIC" CXXFLAGS="${CFLAGS} -fPIC" LDFLAGS="${LDFLAGS}"
make
make install

重新构建 binutils(编译完 binutils 之后, 即可恢复环境变量 PATH):

mkdir /opt/mydev/build/binutils-new
cd /opt/mydev/build/binutils-new
/opt/mydev/src/binutils-2.36/configure --prefix=/opt/devtools CFLAGS="${CFLAGS}" CXXFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}"
make -j4
make install
export PATH=${OLDPATH}

重新构建 gmake:

mkdir /opt/mydev/build/gmake-new
cd /opt/mydev/build/gmake-new
/opt/mydev/src/make-4.3/configure --prefix=/opt/devtools CFLAGS="${CFLAGS}" CXXFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}"
make -j4
make install

构建 m4:

mkdir /opt/mydev/build/m4-new
cd /opt/mydev/build/m4-new
/opt/mydev/src/m4-1.4.18/configure --prefix=/opt/devtools CFLAGS="${CFLAGS}" CXXFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}"
make
make install

构建 autoconf:

mkdir /opt/mydev/build/autoconf-new
cd /opt/mydev/build/autoconf-new
/opt/mydev/src/autoconf-2.68/configure --prefix=/opt/devtools CFLAGS="${CFLAGS}" CXXFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}"
make
make install

构建 automake:

mkdir /opt/mydev/build/automake-new
cd /opt/mydev/build/automake-new
/opt/mydev/src/automake-1.16/configure --prefix=/opt/devtools CFLAGS="${CFLAGS}" CXXFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}"
make
make install

构建 texinfo(gdb 依赖于这个库):

mkdir /opt/mydev/build/texinfo-new
cd /opt/mydev/build/texinfo-new
/opt/mydev/src/texinfo-6.7/configure --prefix=/opt/devtools CFLAGS="${CFLAGS} -fPIC" CXXFLAGS="${CFLAGS} -fPIC" LDFLAGS="${LDFLAGS}"
make -j4
make install

构建 gdb:

mkdir /opt/mydev/build/gdb-new
cd /opt/mydev/build/gdb-new
/opt/mydev/src/gdb-10.1/configure --prefix=/opt/devtools CFLAGS="-static-libstdc++ -static-libgcc ${CFLAGS}" CXXFLAGS="-static-libstdc++ -static-libgcc ${CFLAGS}" LDFLAGS="${LDFLAGS}"
make -j4
make install

重新构建 bison:

mkdir /opt/mydev/build/bison-new
cd /opt/mydev/build/bison-new
/opt/mydev/src/bison-3.4.2/configure --prefix=/opt/devtools CFLAGS="${CFLAGS}" CXXFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}"
make -j4
make install

构建 ncurses(MySQL 依赖于这个库):

mkdir /opt/mydev/build/ncurses-new
cd /opt/mydev/build/ncurses-new
/opt/mydev/src/ncurses-6.2/configure --prefix=/opt/devtools CFLAGS="${CFLAGS} -fPIC" CXXFLAGS="${CFLAGS} -fPIC" LDFLAGS="${LDFLAGS}"
make -j4
make install

构建 libidn:

mkdir /opt/mydev/build/libidn-new
cd /opt/mydev/build/libidn-new
/opt/mydev/src/libidn-1.35/configure --prefix=/opt/devtools CFLAGS="${CFLAGS} -fPIC" CXXFLAGS="${CFLAGS} -fPIC" LDFLAGS="${LDFLAGS}"
make -j4
make install
  1. 重新构建 Python-3

构建依赖库 libffi-3.3:

mkdir /opt/mydev/build/libffi-new
cd /opt/mydev/build/libffi-new
/opt/mydev/src/libffi-3.3/configure --prefix=/opt/devtools CFLAGS="${CFLAGS} -fPIC" CXXFLAGS="${CFLAGS} -fPIC" LDFLAGS="${LDFLAGS}"
make -j4
make install

构建依赖库 openssl(MySQL 同样依赖于这个库):

mkdir /opt/mydev/build/openssl-new
cd /opt/mydev/build/openssl-new
CMAKE_INCLUDE_PATH=/opt/devtools/include:/opt/devtools/lib/gcc/x86_64-pc-linux-gnu/10.2.1/../../../../include/c++/10.2.1:/opt/devtools/lib/gcc/x86_64-pc-linux-gnu/10.2.1/../../../../include/c++/10.2.1/x86_64-pc-linux-gnu:/opt/devtools/lib/gcc/x86_64-pc-linux-gnu/10.2.1/../../../../include/c++/10.2.1/backward:/opt/devtools/lib/gcc/x86_64-pc-linux-gnu/10.2.1/include CMAKE_LIBRARY_PATH=/opt/devtools/lib64:/opt/devtools/lib cmake /opt/mydev/src/openssl-1.1.1-stable -DCMAKE_INSTALL_PREFIX=/opt/devtools -DCMAKE_C_COMPILER=${GLIBCDIR}/bin/gcc -DCMAKE_CXX_COMPILER=${GLIBCDIR}/bin/g++ -DCMAKE_C_FLAGS="-Wall -O2 -fPIC ${CFLAGS}" -DCMAKE_CXX_FLAGS="-Wall -O2 -fPIC ${CFLAGS}"
make -j4
make install

注:

  • 构建 CMake 工程时, 头文件搜索顺序正好与 GNU 工程相反, CMAKE_INCLUDE_PATH 中后出现的路径优先搜索, 因而必须使用上述例子中的顺序。
  • Openssl 库的 cmake 工程不需要 cmake3, 因而可以放在编译 cmake3 之前。

重新构建 Python-3:

mkdir /opt/mydev/build/Python3-new
cd /opt/mydev/build/Python3-new
/opt/mydev/src/Python-3.9.2/configure --prefix=/opt/devtools --with-openssl=/opt/devtools CFLAGS="${CFLAGS}" CXXFLAGS="${CFLAGS}" LDFLAGS="${CFLAGS} ${LDFLAGS} -lrt"
make -j4
make install

注: 构建 Python-3 的过程, 可以作为构建标准 GNU tool chain 工程的例子。

  1. 构建 cmake3
mkdir /opt/mydev/build/cmake-new
cd /opt/mydev/build/cmake-new
CMAKE_INCLUDE_PATH=/opt/devtools/include:/opt/devtools/lib/gcc/x86_64-pc-linux-gnu/10.2.1/../../../../include/c++/10.2.1:/opt/devtools/lib/gcc/x86_64-pc-linux-gnu/10.2.1/../../../../include/c++/10.2.1/x86_64-pc-linux-gnu:/opt/devtools/lib/gcc/x86_64-pc-linux-gnu/10.2.1/../../../../include/c++/10.2.1/backward:/opt/devtools/lib/gcc/x86_64-pc-linux-gnu/10.2.1/include CMAKE_LIBRARY_PATH=/opt/devtools/lib64:/opt/devtools/lib cmake /opt/mydev/src/cmake-3.4.3  -DCMAKE_INSTALL_PREFIX=/opt/devtools -DCMAKE_C_COMPILER=${GLIBCDIR}/bin/gcc -DCMAKE_CXX_COMPILER=${GLIBCDIR}/bin/g++ -DCMAKE_C_FLAGS="-static-libstdc++ -static-libgcc ${CFLAGS}" -DCMAKE_CXX_FLAGS="-static-libstdc++ -static-libgcc ${CFLAGS}"
make -j4
make install
  1. 构建 git(不是必须的)

构建依赖库 zlib:

mkdir /opt/mydev/build/zlib-new
cd /opt/mydev/build/zlib-new
CMAKE_INCLUDE_PATH=/opt/devtools/include:/opt/devtools/lib/gcc/x86_64-pc-linux-gnu/10.2.1/../../../../include/c++/10.2.1:/opt/devtools/lib/gcc/x86_64-pc-linux-gnu/10.2.1/../../../../include/c++/10.2.1/x86_64-pc-linux-gnu:/opt/devtools/lib/gcc/x86_64-pc-linux-gnu/10.2.1/../../../../include/c++/10.2.1/backward:/opt/devtools/lib/gcc/x86_64-pc-linux-gnu/10.2.1/include CMAKE_LIBRARY_PATH=/opt/devtools/lib64:/opt/devtools/lib cmake /opt/mydev/src/zlib-1.2.11 -DCMAKE_INSTALL_PREFIX=/opt/devtools -DCMAKE_C_COMPILER=${GLIBCDIR}/bin/gcc -DCMAKE_CXX_COMPILER=${GLIBCDIR}/bin/g++ -DCMAKE_C_FLAGS="${CFLAGS}" -DCMAKE_CXX_FLAGS="${CFLAGS}"
make -j4
make install

构建 git:

cd /opt/mydev/src/git-2.28.0
./configure --prefix=/opt/devtools CFLAGS="${CFLAGS}" CXXFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS} -ldl"
make -j4
make install

注: git 不支持跨目录编译。

  1. 编译 boost_1_59_0

构建依赖库 bzip2:

cd /opt/mydev/src/bzip2-1.0.6
BZIP2_CFLAGS="-fPIC ${CFLAGS}" BZIP2_LDFLAGS="${LDFLAGS}" make
BZIP2_PREFIX="/opt/devtools" make install

注: bzip2 不支持跨目录编译, 而且 bzip2 既不是 GNU 工程, 也不是 CMake 工程, 而是直接基于 Makefile 编译。对于此类工程的编译, 上述过程可以作为一个例子。

编译 boost 库:

cd /opt/mydev/src/boost_1_59_0
CXX="g++ -static-libstdc++ -static-libgcc ${CFLAGS}" ./bootstrap.sh --prefix=/opt/devtools --with-python=python3
sed -i "s/using gcc/using gcc : 10.2 : g++ : <cxxflags>\"-isystem \/opt\/devtools\/lib\/gcc\/x86_64-pc-linux-gnu\/10.2.1\/include -isystem \/opt\/devtools\/lib\/gcc\/x86_64-pc-linux-gnu\/10.2.1\/..\/..\/..\/..\/include\/c++\/10.2.1\/backward -isystem \/opt\/devtools\/lib\/gcc\/x86_64-pc-linux-gnu\/10.2.1\/..\/..\/..\/..\/include\/c++\/10.2.1\/x86_64-pc-linux-gnu -isystem \/opt\/devtools\/lib\/gcc\/x86_64-pc-linux-gnu\/10.2.1\/..\/..\/..\/..\/include\/c++\/10.2.1 -isystem \/opt\/devtools\/include -L\/opt\/devtools\/lib -L\/opt\/devtools\/lib64 -Wl,--rpath=\/opt\/devtools\/lib -Wl,--dynamic-linker=\/opt\/devtools\/lib\/ld-linux-x86-64.so.2 -fPIC\"/g" project-config.jam
./b2 --segmented-stacks=on --link=static
mkdir /opt/devtools/boost_1_59_0
\cp -frP boost stage /opt/devtools/boost_1_59_0

注:

  • 编译 boost 库需要先执行其自带的配置脚本"bootstrap.sh", 生成编译工具 b2, 再使用 b2 工具进行编译。对于此类工程的编译, 上述过程可以作为一个例子。
  • 执行编译配置脚本时, 可以通过环境变量传递编译命令和参数。
  • 使用 b2 工具时, 需要从配置文件 project-config.jam 读取编译器版本, 以及编译参数。配置脚本"bootstrap.sh"会生成一个默认的配置文件, 上例的第 3 行通过一个 sed 命令将配置文件的内容修改为我们需要的参数配置。
  • 安装 boost 库只需要 boost 和 stage 两个目录。
  1. 编译 MySQL5.7
mkdir /opt/mydev/build/mysql-5.7.32
cd /opt/mydev/build/mysql-5.7.32
CMAKE_INCLUDE_PATH=/opt/devtools/include:/opt/devtools/lib/gcc/x86_64-pc-linux-gnu/10.2.1/../../../../include/c++/10.2.1:/opt/devtools/lib/gcc/x86_64-pc-linux-gnu/10.2.1/../../../../include/c++/10.2.1/x86_64-pc-linux-gnu:/opt/devtools/lib/gcc/x86_64-pc-linux-gnu/10.2.1/../../../../include/c++/10.2.1/backward:/opt/devtools/lib/gcc/x86_64-pc-linux-gnu/10.2.1/include CMAKE_LIBRARY_PATH=/opt/devtools/lib64:/opt/devtools/lib cmake /opt/mydev/src/mysql-5.7.32 -DCMAKE_INSTALL_PREFIX=/opt/mysql5.7.32 -DCMAKE_C_COMPILER=${GLIBCDIR}/bin/gcc -DCMAKE_CXX_COMPILER=${GLIBCDIR}/bin/g++ -DCMAKE_C_FLAGS="${CFLAGS}" -DCMAKE_CXX_FLAGS="${CFLAGS}" -DWITH_SSL=system -DWITH_RAPID=on -DWITH_BOOST="/opt/devtools/boost_1_59_0"
make -j4
make install

注:

  • 参数"-DWITH_SSL=system"表示从指定的 include 路径和动态库路径搜索 openssl, 而不是使用 MySQL 源码包中自带的 openssl;
  • 参数"-DWITH_BOOST"将 boost 目录指定到/opt/devtools 中安装的 boost 版本。
  • MySQL 将被安装至"/opt/mysql-5.7.32"。

2.2.5. 用新编译的 gdb 进行调试

使用非系统自带的 gdb 进行调试时, 需要在启动 gdb 时, 通过 “set auto-load safe-path” 命令将 “/opt/devtools” 指定为安全路径, 否则 gdb 访问 “/opt/devtools” 时会失败。启动命令如下:

/opt/devtools/bin/gdb -q -iex "set auto-load safe-path /opt/devtools" --pid=81665

注: 其中, "–pid"参数用于指定要 debug 的进程 ID。

2.3. configure 配置参数说明

在 Linux 下安装一个应用程序时, 一般先运行脚本 configure, 然后用 make 来编译源程序, 在运行 make install。那 configure 到底是什么呢?

configure 是一个 shell 脚本, 它可以自动设定源程序以符合各种不同平台上 Unix 系统的特性, 并且根据系统叁数及环境产生合适的 Makefile 文件或是 C 的头文件 (header file), 让源程序可以很方便地在这些不同的平台上被编译连接。

configure 一般用来生成 Makefile, 为下一步的编译做准备, 你可以通过在 configure 后加上参数来对安装进行控制

2.3.1. 说明

在 linux 中, 经常需要用到交叉编译, 在 ubuntu 系统中, 交叉编译可以运行在 arm 平台上的 bin 文件。对于大部分代码,
都有 configure 文件, 让开发者进行配置, 配置完毕之后自动生成 makefile, 然后进行编译。本文旨在说明 configure 中
常用的一些参数。

2.3.2. 开发环境

  • 软件环境: ubuntu 操作系统
  • 编译工具链: arm-xilinx-linux-gnueabi
  • 硬件平台: zynq7010

2.3.3. configure 参数说明

  • 查看 configure 配置选项

在 configure 目录下, 运行 --help 命令, 可以查看到 configure 的配置参数一共有哪些。

./configure --help

Defaults for the options are specified in brackets.

Configuration:
  -h, --help              display this help and exit
      --help=short        display options specific to this package
      --help=recursive    display the short help of all the included packages
  -V, --version           display version information and exit
  -q, --quiet, --silent   do not print `checking ...' messages
      --cache-file=FILE   cache test results in FILE [disabled]
  -C, --config-cache      alias for `--cache-file=config.cache'
  -n, --no-create         do not create output files
      --srcdir=DIR        find the sources in DIR [configure dir or `..']

Installation directories:
  --prefix=PREFIX         install architecture-independent files in PREFIX
                          [/usr/local]
  --exec-prefix=EPREFIX   install architecture-dependent files in EPREFIX
                          [PREFIX]

By default, `make install' will install all the files in
`/usr/local/bin', `/usr/local/lib' etc.  You can specify
an installation prefix other than `/usr/local' using `--prefix',
for instance `--prefix=$HOME'.

For better control, use the options below.

Fine tuning of the installation directories:
  --bindir=DIR            user executables [EPREFIX/bin]
  --sbindir=DIR           system admin executables [EPREFIX/sbin]
  --libexecdir=DIR        program executables [EPREFIX/libexec]
  --sysconfdir=DIR        read-only single-machine data [PREFIX/etc]
  --sharedstatedir=DIR    modifiable architecture-independent data [PREFIX/com]
  --localstatedir=DIR     modifiable single-machine data [PREFIX/var]
  --runstatedir=DIR       modifiable per-process data [LOCALSTATEDIR/run]
  --libdir=DIR            object code libraries [EPREFIX/lib]
  --includedir=DIR        C header files [PREFIX/include]
  --oldincludedir=DIR     C header files for non-gcc [/usr/include]
  --datarootdir=DIR       read-only arch.-independent data root [PREFIX/share]
  --datadir=DIR           read-only architecture-independent data [DATAROOTDIR]
  --infodir=DIR           info documentation [DATAROOTDIR/info]
  --localedir=DIR         locale-dependent data [DATAROOTDIR/locale]
  --mandir=DIR            man documentation [DATAROOTDIR/man]
  --docdir=DIR            documentation root [DATAROOTDIR/doc/cgminer]
  --htmldir=DIR           html documentation [DOCDIR]
  --dvidir=DIR            dvi documentation [DOCDIR]
  --pdfdir=DIR            pdf documentation [DOCDIR]
  --psdir=DIR             ps documentation [DOCDIR]

Program names:
  --program-prefix=PREFIX            prepend PREFIX to installed program names
  --program-suffix=SUFFIX            append SUFFIX to installed program names
  --program-transform-name=PROGRAM   run sed PROGRAM on installed program names

System types:
  --build=BUILD     configure for building on BUILD [guessed]
  --host=HOST       cross-compile to build programs to run on HOST [BUILD]
  --target=TARGET   configure for building compilers for TARGET [HOST]
  
Some influential environment variables:
  CC          C compiler command
  CFLAGS      C compiler flags
  LDFLAGS     linker flags, e.g. -L<lib dir> if you have libraries in a
              nonstandard directory <lib dir>
  LIBS        libraries to pass to the linker, e.g. -l<library>
  CPPFLAGS    (Objective) C/C++ preprocessor flags, e.g. -I<include dir> if
              you have headers in a nonstandard directory <include dir>
  CPP         C preprocessor
  PKG_CONFIG  path to pkg-config utility
  PKG_CONFIG_PATH
              directories to add to pkg-config's search path
  PKG_CONFIG_LIBDIR
              path overriding pkg-config's built-in search path
  LIBUSB_CFLAGS
              C compiler flags for LIBUSB, overriding pkg-config
  LIBUSB_LIBS linker flags for LIBUSB, overriding pkg-config
  JANSSON_CFLAGS
              C compiler flags for JANSSON, overriding pkg-config
  JANSSON_LIBS
              linker flags for JANSSON, overriding pkg-config
  LIBCURL_CFLAGS
              C compiler flags for LIBCURL, overriding pkg-config
  LIBCURL_LIBS
              linker flags for LIBCURL, overriding pkg-config
  LIBSYSTEMD_CFLAGS
              C compiler flags for LIBSYSTEMD, overriding pkg-config
  LIBSYSTEMD_LIBS
              linker flags for LIBSYSTEMD, overriding pkg-config

Use these variables to override the choices made by `configure' or to help
it to find libraries and programs with nonstandard names/locations.

2.3.4. 参数说明

  • build 参数
--build=BUILD     configure for building on BUILD [guessed]

build: 执行代码编译的主机, 正常的话就是你的主机系统。这个参数一般由 config.guess 来猜就可以。当然自己指定也可以。可以默认不写, 默认为当前正在使用的 ubuntu 主机, 如 i386-linux

--build=i386-linux
或者 xilinx-arm 的编译器主机

--build=i686-pc-linux-gnu
或者不写
  • host 参数
--host=HOST       cross-compile to build programs to run on HOST [BUILD]

指定软件运行的系统平台。如果没有指定, 将会运行`config.guess’来检测。–host 指定的是交叉编译工具链的前缀。如本位中采用的是 arm-xilinx-linux-gnueabi 工具链, 则参数配置为:

--host=arm-xilinx-linux-gnueabi
  • target 参数
--target=TARGET   configure for building compilers for TARGET [HOST]

target: 这个选项只有在建立交叉编译环境的时候用到, 正常编译和交叉编译都不会用到。他用 build 主机上的编译器, 编译一个新的编译器 (binutils, gcc,gdb 等), 这个新的编译器将来编译出来的其他程序将运行在 target 指定的系统上。

如果不编译新的编译器, 这个参数可以不填, 或者与 host 的参数一致

--target=arm-xilinx-linux-gnueabi
或者不写 --target 的参数
  • CC 编译器参数
CC          C compiler command

指定 GCC 交叉编译器命令, 如果配置了, 则使用 CC 配置的编译器, 如果不配置则默认为 host 对应的 GCC 工具
如配置了 --host=arm-xilinx-linux-gnueabi, 则默认 CC 的编译器为 arm-xilinx-linux-gnueabi-gcc
这个参数如无特殊指定, 可以忽略不写。

  • prefix 安装参数

该参数指定编译后, 文件安装的目录。

--program-prefix=PREFIX            prepend PREFIX to installed program names

不指定 prefix, 则可执行文件默认放在 /usr/local/bin, 库文件默认放在 /usr/local/lib, 配置文件默认放在 /usr/local/etc。其他的资源文件放在 /usr/local/share。你要卸载这个程序, 要么在原来 make 目录下用 make uninstall(前提是 make 文件指定过 make uninstall), 要么去上述文件中一个一个手动删除。如需指定 make install 的目录, 如 /home/tmp/test

--prefix=/home/tmp/test

2.3.5. 编译参数示例

如编译 zynq 平台下的程序, 则配置如下即可

./configure  --host=arm-xilinx-linux-gnueabi --build=i686-pc-linux-gnu --target=arm-xilinx-linux-gnueabi CC=arm-xilinx-linux-gnueabi-gcc
或者
./configure --host=arm-xilinx-linux-gnueabi  --build=i386-linux
或者
./configure  --host=arm-xilinx-linux-gnueabi

2.4. glibc 交叉编译

我们在开发过程中, 有时候可能需要根据我们的业务场景对 glibc 进行定制化修改, 因此有必要了解 glibc 的编译方法。通常编译 glibc 需要以下几个步骤:

  1. 由于我们一般是在 x86 环境的编译服务器下编译运行在 arm 环境下的目标固件, 因此我们通常需要利用交叉编译工具链。所以, 我们首先需要指定我们的交叉编译工具链的路径。
export PATH=$PATH:/home/xxxx(交叉编译工具链路径)
  1. 利用 configure 编译生成 makefile 文件
../glibc-2.28/configure --host=arm-soft-linux-guneabi --disable-profile --enable-obsolete-rpc --disable-werror --enable-add-ons CC=arm-soft-linux-gnueabi-gcc CFLAGS="-g -O2 -fPIC -U_FORTIFY_SOURCE -funwind-tables" LDFLAGS="-Wl,--dynamic-linker=/lib/ld-linux.so.3" libc_cv_forced_unwind=yes libc_cv_c_cleanup=yes libc_cv_ssp=yes --enable-kernel=3.0.0 --prefix

注意, 我们执行 configure 命令时和 glibc 的源码不能在同一个路径下, 否则会报错。我们可以在源码路径外单独建立文件夹用于 glibc 的编译。

上面关键参数说明如下:

  • –host: 指定生成库目标运行机器, 即 lib 库最终运行的平台环境

  • –build: 当前编译服务器环境。可以让编译脚本自动监测, 不直接指定。(本例即没有指定)

  • CC: 指定交叉编译 gcc 工具名称

  • CFLAGS: 指定编译时参数, 如

    • g 表示编译带符号信息, 这样方便利用 gdb 调试(前提是没有 strip);
    • O2: 对编译生成程序进行优化
    • fPIC: 生成位置无关代码
    • funwind-tables: 固定栈格式形式。若不指定该参数, 则 backstrace 函数不能对生成的库获取栈回溯信息。
  • LDFLAGS: 指定链接时参数, 如

    • -Wl,–dynamic-linker=/lib/ld-linux.so.3: 指定 glibc 的动态链接器位置。如不指定则默认动态链接器为当前编译路径, 这样运行时将找不到动态链接器位置, 导致系统启动失败。glibc 的动态链接器位置必须放在/lib 下。-Wl 表示将后面的参数在链接时传递给动态链接器。
  • –enable-kernel: 表示生成的 glibc 支持的最低内核版本

  • –prefix: 指定生成的 lib 库运行时保存的路径。最终一直 glibc 交付件时存放位置必须要和 prefix 指定路径保持一致。

注意: 由于 glibc 生成的动态链接器 ld-linux.so.3 必须放在/lib 下, 因此编译 glibc 时必须指定为根目录。另外指定为根目录时, prefix 后面直接空白就行, 这样就是运行时保存在根目录下。若 prefix 指定为"/“, 则最终 glibc 生成的 libc-2.28.so 中指定动态库路径为”//XXX", 即指定的动态链接器路径中前面会多一个"/"。

通过 readelf -l libc-2.28.so 可查看生成的 libc 中指定要求的动态链接器 ld-linux.so.3 位置。

  1. 利用 make 命令执行 makefile 脚本编译
make -j32

相关参数说明如下:

  • -j32: 表示最多可以开启 32 个线程进行多线程编译, 这样可以提高编译速度。
  1. 利用 make install 命令安装生成最终交付件
make install DESTDIR=/home/glibc/glibc-build/output

通过上述命令可以在 /home/glibc/glibc-build/output 路径下生成安装 glibc 的交付件, 包括 bin、etc、include、liblibexec、sbin、share 和 var 目录。我们需要将 include 下的头文件、lib 下的动态库等拷贝到最终的系统中根目录下, 其他的可以不需要。

注意: make install 安装 lib 库时, 若后面不指定 DESTDIR 参数(即安装路径), 则默认安装路径和 prefix 指定的库运行路径保持一致。我们要注意区分库的安装路径和运行路径。DESTDIR 指定的是生成的库交付件保存在编译服务器上的哪个位置, 而 prefix 指定的是生成的库最终在目标系统上运行时存放的路径。当我们不指定 DESTDIR 时, 则 DESTDIR 将和 prefix 保持一致。

我们在拷贝交付件到目标系统时, 注意以下几点:

  • 不能对 glibc 中 main 函数的引导文件 crt1.o、crti.o、crtn.o 进行 strip, 否则 main 函数的入参顺序会弄反。
  • 有时候不要替换原系统中的 libc_nonshared.a 文件, 否则会编译报错。具体原因暂时还不太清楚, 有时候即使替换了也不会报错。

2.4.1. go 交叉编译

只依赖 go 文件的交叉编译是非常简单的, 一条命令即可解决, 但如果汇集了 CGO, 并且需要引入第三方库, 那才是真的麻烦, 以下就针对这种。

一般需要在 CGO_LDFLAGS 中指定, 注意, 不只要通过 -L 指定库的搜索目录, 还需要指定库, 即 -l, 如下面这句:

export CGO_LDFLAGS="-g -O2 -L/u01/obclient/lib -lobclnt"

-lobclnt 指定了需要 link 的库, 指要 link libobclnt

其它的 -g -O2 是默认值。-g 表示可执行程序包含调试信息, 是为了 gdb 用, -O2 意思是开启编译优化, 等级为 2

Tip: 上面的 -L/u01/obclient/lib -lobclnt 也可以放在 CC 的命令行中, 甚至直接把库文件复制到 libc 目录(如 /opt/linaro/aarch64-linux-gnu/gcc-linaro-4.9-2016.02-x86_64_aarch64-linux-gnu/aarch64-linux-gnu/libc/lib) 下。

2.5. gcc

2.5.1. GCC/G++ 编译器中指定库文件 (LIB)、头文件 (INCLUDE)

库文件在连接 (静态库和共享库) 和运行 (仅限共享库的程序, 静态库会和可执行编译到一起) 时被使用, 其搜索路径是在系统中进行设置的。一般 Linux 系统把 /lib/usr/lib 两个目录作为默认的库搜索路径, 所以使用这两个目录中的库时不需要进行设置搜索路径即可直接使用。对于处于默认库搜索路径之外的库, 需要将库的位置添加到库的搜索路径之中。

设置库文件的搜索路径总的来说有以下几种:

  • LIBRARY_PATHLD_LIBRARY_PATH 等环境变量: 指定连接、运行时库文件路径;
  • /etc/ld.so.conf 文件: 添加链接时库文件的搜索路径, 运行时还需要使用 ldconfig 命令将路径刷新到 ld.so.cache 中;
  • g++/gcc 参数 -L-l-I: 指定链接时库文件的路径、名字和头文件, 运行时还需要使用环境变量或者在文件 /etc/ld.so.conf 中指定(或者放到默认 /lib 路径), 然后 ldconfig;

接下来我们分连接、运行不同阶段来分别解读一下。见: 动态链接、静态链接。note

环境变量:

/etc/profile 中添加如下环境变量。

编译时用到的环境变量:

#gcc 找到头文件的路径
C_INCLUDE_PATH=/usr/include/libxml2:/MyLib
export C_INCLUDE_PATH

#g++ 找到头文件的路径
CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/usr/include/libxml2:/MyLib
export CPLUS_INCLUDE_PATH
 
#gcc 和 g++ 在编译的链接 (link) 阶段查找库文件的目录列表
LIBRARY_PATH=$LIBRARY_PATH:/MyLib
export LIBRARY_PATH

运行时用到的环境变量:

# 程序运行时查找 ku 文件的路径
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/MyLib
export LD_LIBRARY_PATH

示例:

一个头文件 my_so_test.h 和两个源文件 test1.cpptest2.cpp, 将他们制作成一个名为 libmytest.so 的动态链接库文件:

  1. 先通过 $ g++ -o libmytest.so -shared -fPIC test1.cpp test2.cpp 创建动态链接库;

  2. 编写一个 main.cpp 调用动态连接库:

#include "my_so.h"
int main()
{
  test1();
  test2();
  
  return 0;
}

编译、连接:

解决方案: vim /etc/profile, 添加如下, 然后 source /etc/profile:

LIBRARY_PATH=$LD_LIBRARY_PATH:/home/roo/kevinliu/test6/so
export LIBRARY_PATH

然后再编译、连接: $ g++ -o run main.cpp -lmytest 没问题了。

运行

vim /etc/profile, 添加如下, 然后 source /etc/profile

LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/roo/kevinliu/test6/so
export LD_LIBRARY_PATH

然后再运行 ./run 没问题了。

/etc/ld.so.conf 文件:

将库文件的路径都加入到 /etc/ld.so.conf 中是明智的选择, 添加方法也很简单, 一行一个, 例如:

/usr/X11R6/lib
/usr/local/lib
/opt/lib

添加完成后只是链接时不会报错, 执行前还需要使用 ldconfig 命令刷新到 ld.so.cache 中。

示例: 还是上面的例子

  1. 连接:

将动态库拷贝到系统默认的库路径中, 即可编译链接通过。或者在 /etc/ld.so.conf 中加上一行 /home/roo/kevinliu/test6/so

  1. 运行:

解决方案: 执行 ldconfig, 之后即可运行成功。

3.gcc/g++ 参数:

通上面的例子看一下 -L-l-I(指定头文件的)参数。

  1. 编译、连接:
$ g++ -o run main.cpp -L./ -lmytest

说明: 通过 -L 参数指定库文件的地址; -l 指定动态库的名字;

  1. 运行:

  2. 解决:

通过 LD_LIBRARY_PATH 环境变量, 或者在文件 /etc/ld.so.conf 中指定库文件路径(或者将库文件放到默认 /lib 路径), 然后 ldconfig 刷新 ld.so.cache

默认库文件路径、头文件路径

1、查看默认库文件路径:

gcc --print-search-dir
g++ --print-search-dir

2、查看默认头文件 (include) 路径:

`gcc -print-prog-name=cc1plus` -v
`g++ -print-prog-name=cc1plus` -v

2.5.2. gcc -I -L -l 区别

我们用 gcc 编译程序时, 可能会用到"-I"(大写 i), “-L”(大写 l), “-l”(小写 l) 等参数, 下面做个记录:

例: gcc -o hello hello.c -I /home/hello/include -L /home/hello/lib -lworld

上面这句表示在编译 hello.c 时:

  • -I /home/hello/include 表示将 /home/hello/include 目录作为第一个寻找头文件的目录, 寻找的顺序是: /home/hello/include–>/usr/include–>/usr/local/include
  • -L /home/hello/lib 表示将 /home/hello/lib 目录作为第一个寻找库文件的目录, 寻找的顺序是: /home/hello/lib–>/lib–>/usr/lib–>/usr/local/lib
  • -lworld 表示在上面的 lib 的路径中寻找 libworld.so 动态库文件(如果 gcc 编译选项中加入了"-static"表示寻找 libworld.a 静态库文件)

2.5.3. GCC 命令行详解 -L -l

我们用 gcc 编译程序时, 常常会用到"-I"(大写 i), “-L”(大写 l), “-l”(小写 l) 等参数, 下面做个记录:

GCC 命令行详解 -L 指定库的路径 -l 指定需连接的库名

Linux 下动态链接库默认后缀名是".so", 静态链接库默认后缀名是".a"。
例:

gcc -o hello hello.c   -I/home/hello/include   -L/home/hello/lib    -lworld

上面这句表示在编译 hello.c 时:

  • -I /home/hello/include, 表示将/home/hello/include 目录作为第一个寻找头文件的目录, 寻找的顺序是: /home/hello/include–>/usr/include–>/usr/local/include

    • 也就是指定优先查找的目录, 找不到的话查找默认目录
  • -L /home/hello/lib, 表示将/home/hello/lib 目录作为第一个寻找库文件的目录, 寻找的顺序是: /home/hello/lib–>/lib–>/usr/lib–>/usr/local/lib

    • 同上, 也是指定优先查找的目录
  • -l word , 表示寻找动态链接库文件 libword.so(也就是文件名去掉前缀和后缀所代表的库文件)

    • 如果 加上编译选项 -static, 表示寻找静态链接库文件, 也就是 libword.a

对于第三方提供的动态链接库 (.so), 一般将其拷贝到一个 lib 目录下 (/usr/local/lib), 或者使用 -L 来指定其所在目录, 然后使用 -l 来指定其名称

2.5.4. gcc 的 -g,-o,-c,-D,-w,-W,-Wall,-O3 等参数的意义

-g

  • -g 可执行程序包含调试信息
  • -g 为了调试用的

加个 -g 是为了 gdb 用, 不然 gdb 用不到

-o

  • -o 指定输出文件名
  • -o output_filename, 确定输出文件的名称为 output_filename, 同时这个名称不能和源文件同名。如果不给出这个选项, gcc 就给出预设的可执行文件 a.out

一般语法:

gcc filename.c -o filename

上面的意思是如果你不打 -o filename(直接 gcc filename.c )

那么默认就是输出 a.out. 这个 -o 就是用来控制输出文件的。 ------用 ./a.out 执行文件

-c

-c 只编译不链接

产生 .o 文件, 就是 obj 文件, 不产生执行文件

-D

其意义是添加宏定义, 这个很有用。

当你想要通过宏控制你的程序, 不必傻乎乎的在程序里定义, 然后需要哪个版本, 去修改宏。

只需要在执行 gcc 的时候, 指定-D, 后面跟宏的名称即可。

示例:

gcc test.c -o test -D OPEN_PRINTF_DEBUG

或者 gcc test.c -o test -DOPEN_PRINTF_DEBUG

两者都是可以的。

-w
-w 的意思是关闭编译时的警告, 也就是编译后不显示任何 warning, 因为有时在编译之后编译器会显示一些例如数据转换之类的警告, 这些警告是我们平时可以忽略的。

-W 和-Wall

-W 选项类似-Wall, 会显示警告, 但是只显示编译器认为会出现错误的警告。

-Wall 选项意思是编译后显示所有警告

-O3
-O 是大写字母 O, 不是数字 0 哦。

意思是开启编译优化, 等级为三。

-shared

如果想创建一个动态链接库, 可以使用 gcc 的-shared 选项。输入文件可以是源文件、汇编文件或者目标文件。

-fPIC

-fPIC 选项作用于编译阶段, 告诉编译器产生与位置无关代码 (Position-Independent Code)

这样一来, 产生的代码中就没有绝对地址了, 全部使用相对地址, 所以代码可以被加载器加载到内存的任意位置, 都可以正确的执行。这正是共享库所要求的, 共享库被加载时, 在内存的位置不是固定的。

-l (小写的 l) 参数和-L (大写的 l) 参数

gcc 编译程序时, 可能会用到"-I"(大写 i), “-L”(大写 l), “-l”(小写 l) 等参数, 下面做个记录。

例 1:
例 2:

-l(小写的 l) 参数就是用来指定程序要链接的库, -l 参数紧接着就是库名, 那么库名跟真正的库文件名有什么关系呢?
就拿数学库来说, 他的库名是 m, 他的库文件名是 libm.so, 很容易看出, 把库文件名的头 lib 和尾。so 去掉就是库名了。

好了现在我们知道怎么得到库名了, 比如我们自已要用到一个第三方提供的库名字叫 libtest.so, 那么我们只要把 libtest.so 拷贝到/usr/lib 里, 编译时加上-ltest 参数, 我们就能用上 libtest.so 库了(当然要用 libtest.so 库里的函数, 我们还需要与 libtest.so 配套的头文件)。放在/lib 和/usr/lib 和/usr/local/lib 里的库直接用-l 参数就能链接了。

但如果库文件没放在这三个目录里, 而是放在其他目录里, 这时我们只用-l 参数的话, 链接还是会出错, 出错信息大概是: “/usr/bin/ld: cannot find -lxxx”, 也就是链接程序 ld 在那 3 个目录里找不到 libxxx.so, 这时另外一个参数-L 就派上用场了, 比如常用的 X11 的库, 它放在/usr/X11R6/lib 目录下, 我们编译时就要用-L /usr/X11R6/lib -lX11 参数, -L 参数跟着的是库文件所在的目录名。再比如我们把 libtest.so 放在/aaa/bbb/ccc 目录下, 那链接参数就是-L/aaa/bbb/ccc -ltest

Linux-(C/C++) 动态链接库生成以及使用 (libxxx.so)

编译结果: 生成 libmax.so
运行结果: 生成可执行程序 main(成功了)(这里编译使用的是相对路径)
接下来, 使用绝对路径编译 main(在我自己编译):

使用 C++编译使用 C 语言提供的链接库, 编译链接出错(下面只是简单将 main.c 改为 main.cpp)
怎么办呢? ?

libmax 这个库仅适合 C 使用, C++并不适合, 如果想编译一个可以供 C++使用。那么头文件 (max.h) 就需要改变,

需要额外增加一句: extern “C”

max.h(修改如下):

这样就解决了

但是这样有一个问题, 难道每次编译都要改来改去, 有没有同时适合 C/C 链接库的方法呢?
答案是有的, 只需要改动头文件即可, 使用条件编译
C++有一个宏: __cpluscplus 当用 g++ 编译的时候, 就可以识别这个宏

在 gcc 中使用 -g-O2 选项

混合使用 -g(调试符号)和 -O2(最安全的优化)

我们在生产环境中一起使用, 这使得如果客户只看到一次崩溃, 调试变得更容易。它给了你一个很好的主意, 问题在哪里(不是如果它是内存损坏)。

理论上, 添加-g 应该不会影响性能, 尽管可执行文件变大了。在嵌入式环境中, 这是一个很大的折衷。

2.6. cgo sqlite3 arm 交叉编译

环境

  • x86_64 机器, 装的是 centos7, 64 位的。
  • arm 机器, aarch64-Linux-5.0.2-aml-s905, little_endian, 装的 armbian, 64 位的。

2.6.1. 尝试用 go 直接交叉编译

  • 用 CGO_ENABLED=1 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags “-w -s” -o arm-mygofile mygofile.go 交叉编译失败。
  • 用 CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags “-w -s” -o arm-mygofile mygofile.go 交叉编译成功, 但执行时发现 sqlite3 库没有加载。

golang 在没有 CGO 时, 直接可以交叉编译。有 CGO 就不行了。

2.6.2. 尝试用 arm 的 gcc

  • 在 centos7 中安装, yum install gcc-arm-linux-gnu
  • 用 CGO_ENABLED=1 GOOS=linux GOARCH=arm GOARM=7 CC=arm-linux-gnu-gcc go build -ldflags “-w -s” -o arm-mygofile mygofile.go 交叉编译失败。说 stdlib.h 找不到。

从 arm-linux-gnu-gcc -v 中看到 sysroot 目录 /usr/arm-linux-gnu/sys-root/ 为空。

从 yum search 中, 也没找到对应的包。看来只能手工去下载编译用的 head 文件和库文件。

2.6.3. 尝试交叉编译 arm 执行文件 (32bit)

  • 之前已经安装 yum install gcc-arm-linux-gnu
  • 去 https://releases.linaro.org/components/toolchain/binaries/ 找 latest-7
  • 下载 arm-linux-gnueabi/sysroot-glibc-linaro-2.25-2019.02-arm-linux-gnueabi.tar.xz
  • 解压 tar Jxvf sysroot-glibc-linaro-2.25-2019.02-arm-linux-gnueabi.tar.xz

build 时, 指定 sysroot 的位置。

  • 用 CGO_ENABLED=1 GOOS=linux GOARCH=arm GOARM=7 CC=arm-linux-gnu-gcc CGO_CFLAGS=“-g -O2 --sysroot=/…/sysroot-glibc-linaro-2.25-2019.02-arm-linux-gnueabi/” CGO_LDFLAGS=“-g -O2 --sysroot=/…/sysroot-glibc-linaro-2.25-2019.02-arm-linux-gnueabi/” go build -ldflags “-w -s” -o arm-mygofile mygofile.go 编译成功。

但 copy 到 armbian 后, 无法执行(动态库 ld-linux.so 找不到)。似乎是 32 位的动态库没找到。

尝试加上 -static 参数。使用静态链接。

  • 用 CGO_ENABLED=1 GOOS=linux GOARCH=arm GOARM=7 CC=arm-linux-gnu-gcc CGO_CFLAGS=“-g -O2 --sysroot=/…/sysroot-glibc-linaro-2.25-2019.02-arm-linux-gnueabi/” CGO_LDFLAGS=“-g -O2 --sysroot=/…/sysroot-glibc-linaro-2.25-2019.02-arm-linux-gnueabi/ -static” go build -ldflags “-w -s” -o arm-mygofile mygofile.go 编译成功(有错误警告, 说 sqlite3 使用了 glibc 动态库)。

copy 到 armbian 后, 执行正常。执行文件是 32 位静态链接的。

—似乎成功了—

用 armeb-linux-gnueabihf 中的 sysroot-glibc-linaro…-armlinux-gnueabihf.tar.xz 文件 应该也可以。听说 64 位的 arm, 默认就是带 hf 的。
我没有尝试。
s905, 好像是 armv8 的。估计用 armv8l-linux-gnueabihf 也行。我也没尝试。

2.6.4. 交叉编译 aarch64 执行文件 (64bit)

  • 在 centos7 中安装, yum install gcc-aarch64-linux-gnu
  • 去 https://releases.linaro.org/components/toolchain/binaries/ 找 latest-7
  • 下载 aarch64-linux-gnu/sysroot-glibc-linaro-2.25-2019.02-aarch64-linux-gnu.tar.xz
  • 解压 tar Jxvf sysroot-glibc-linaro-2.25-2019.02-aarch64-linux-gnu.tar.xz

build 时, 指定 sysroot 的位置。

用 CGO_ENABLED=1 GOOS=linux GOARCH=arm64 CC=“aarch64-linux-gnu-gcc” CGO_CFLAGS=“-g -O2 --sysroot=/…/sysroot-glibc-linaro-2.25-2019.02-aarch64-linux-gnu/” CGO_LDFLAGS=“-g -O2 --sysroot=/…/sysroot-glibc-linaro-2.25-2019.02-aarch64-linux-gnu/” go build -ldflags “-w -s” -o arm-mygofile mygofile.go 编译成功。

2.7. 基于 Docker 容器的 xgo 打包工具

它的实现也很简单: 将多平台所需要的 Go 工具链, C/C++交叉编译器和头文件/库都组装到 Docker 容器中(因此, 在镜像拉取时, 会下载大量的依赖资源), 再借助 xgo 打包工具实现跨平台编译。

  • Docker 安装(省略)
  • 拉取镜像

docker pull karalabe/xgo-latest

  • 打包工具安装

go get github.com/karalabe/xgo

轻量级的命令包装器, 它的作用就是简化复杂的 Docker 命令。

  • 跨平台编译

指定要编译的导入路径即可, 其余工作由 xgo 完成。在本例中, 代码位置位于 $GOPATH/src/workspace/example/cgoDemo2/

xgo $GOPATH/src/workspace/example/cgoDemo2/

编译之后, 本目录下会存在以下各平台可执行文件

$ ls -al
total 44960
drwxr-xr-x  23 slp  staff      736 Nov 17 11:43 .
drwxr-xr-x  39 slp  staff     1248 Nov 16 17:59 ..
-rwxr-xr-x   1 slp  staff  1761872 Nov 17 11:42 cgoDemo2-android-16-386
drwxr-xr-x   5 slp  staff      160 Nov 17 11:42 cgoDemo2-android-16-aar
-rwxr-xr-x   1 slp  staff  1778464 Nov 17 11:42 cgoDemo2-android-16-arm
-rwxr-xr-x   1 slp  staff   902436 Nov 17 11:43 cgoDemo2-darwin-10.6-386
-rwxr-xr-x   1 slp  staff  1053816 Nov 17 11:43 cgoDemo2-darwin-10.6-amd64
-rwxr-xr-x   1 slp  staff  1065232 Nov 17 11:43 cgoDemo2-ios-5.0-arm64
-rwxr-xr-x   1 slp  staff   978016 Nov 17 11:43 cgoDemo2-ios-5.0-armv7
drwxrwxrwx   3 slp  staff       96 Nov 17 11:43 cgoDemo2-ios-5.0-framework
-rwxr-xr-x   1 slp  staff  1084208 Nov 17 11:42 cgoDemo2-linux-386
-rwxr-xr-x   1 slp  staff  1226072 Nov 17 11:42 cgoDemo2-linux-amd64
-rwxr-xr-x   1 slp  staff  1093728 Nov 17 11:42 cgoDemo2-linux-arm-5
-rwxr-xr-x   1 slp  staff  1074348 Nov 17 11:43 cgoDemo2-linux-arm-6
-rwxr-xr-x   1 slp  staff  1073800 Nov 17 11:43 cgoDemo2-linux-arm-7
-rwxr-xr-x   1 slp  staff  1196520 Nov 17 11:43 cgoDemo2-linux-arm64
-rwxr-xr-x   1 slp  staff  1152088 Nov 17 11:43 cgoDemo2-linux-mips
-rwxr-xr-x   1 slp  staff  1274272 Nov 17 11:43 cgoDemo2-linux-mips64
-rwxr-xr-x   1 slp  staff  1271464 Nov 17 11:43 cgoDemo2-linux-mips64le
-rwxr-xr-x   1 slp  staff  1148892 Nov 17 11:43 cgoDemo2-linux-mipsle
-rwxr-xr-x   1 slp  staff  1712214 Nov 17 11:43 cgoDemo2-windows-4.0-386.exe
-rwxr-xr-x   1 slp  staff  2115121 Nov 17 11:43 cgoDemo2-windows-4.0-amd64.exe
-rw-r--r--   1 slp  staff      157 Nov 16 16:51 main.go

默认情况下, xgo 会尝试编译 Go 运行时所支持的所有平台。如果我们只想构建特定的几个目标系统, 可以使用逗号分隔的 --targets 选项控制, 例如 --targets=windows/amd64,linux/amd64 代表编译目标仅包括 amd64 架构的 windows 和 linux 平台。

$ xgo --targets=windows/amd64,linux/amd64 $GOPATH/src/workspace/example/cgoDemo2/

目前支持的平台列表如下

  • 操作系统: android, darwin, ios, linux, windows
  • 架构: 386, amd64, arm-5, arm-6, arm-7, arm64, mips, mipsle, mips64, mips64le

xgo 提供了比较灵活的编译方案, 通过$ xgo -h 查看选项信息, 更多详情可点击文末的 xgo 链接。

3. 编译查找路径

3.1. 动态链接库查找路径全面解析, 与 undefined symbol 说再见

编译、运行大型项目的过程中, "undefined symbol"这一报错可以说是最常见、最棘手的问题了。出现这类问题时, 经常需要调整各种环境变量及配置文件, 包括但不限于 LD_LIBRARY_PATH/LDFLAGS/LIBRARY_PATH/ldconfig 等等。而大型项目的编译动辄需要几个小时, 编译的过程仿佛成了一段无尽的等待, 代码的世界就像一座迷宫, 而我们在其中徘徊, 希望某一次调整环境变量之后能够编译通过。有时调整之后编译就通过了, 但是运行时又找不到动态库文件了, 真是让人感到无尽的沮丧和绝望。

本文以常用的链接器即 GNU linker(一般对应于 Linux 上的 ld 命令)为例, 详细分析动态链接库查找路径的细节, 并针对编译和运行两种场景给出 SEARCH_DIR 和 /etc/ld.so.cache 两大终极解决方案。

3.1.1. 编译时的动态链接库

关于动态链接库用于编译过程的原理, 这里不再赘述, 网上已经有很多非常好的解释了。本文要解决的问题, 就是当我们调用 ld -lxxx 的时候, 如何让链接器找到 libxxx.so(动态链接库)或者 libxxx.a(静态链接库)文件。具体来说, 我们以 cuda 的动态链接库 cudart 为例, 其链接命令为 ld -lcudart。这里假设 cuda 已经安装好了, 所以 libcudart.so 存在, 但是链接器目前找不到它, 我们的目标是让链接器找到 libcudart.so。

该命令的完整形式为: ld -L/path1 -lcudart。通过加入更详细的输出选项(即 ld -L/path1 -lcudart --verbose), 我们可以看到链接器查找动态链接库的全过程。这个命令的输出非常多, 我们主要关注两部分内容: 包含 SEARCH_DIR 的预定义路径 (ld -L/path1 -lcudart --verbose | grep SEARCH_DIR) 和链接器尝试的过程 (ld -L/path1 -lcudart --verbose | grep attempt):

$ ld -L/path1 -lcudart --verbose | grep SEARCH_DIR | tr -s ' ;' \\012
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu")
SEARCH_DIR("=/lib/x86_64-linux-gnu")
SEARCH_DIR("=/usr/lib/x86_64-linux-gnu")
SEARCH_DIR("=/usr/lib/x86_64-linux-gnu64")
SEARCH_DIR("=/usr/local/lib64")
SEARCH_DIR("=/lib64")
SEARCH_DIR("=/usr/lib64")
SEARCH_DIR("=/usr/local/lib")
SEARCH_DIR("=/lib")
SEARCH_DIR("=/usr/lib")
SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64")
SEARCH_DIR("=/usr/x86_64-linux-gnu/lib")

$ ld -L/path1 -lcudart --verbose | grep attempt
attempt to open /path1/libcudart.so failed
attempt to open /path1/libcudart.a failed
attempt to open /usr/local/lib/x86_64-linux-gnu/libcudart.so failed
attempt to open /usr/local/lib/x86_64-linux-gnu/libcudart.a failed
attempt to open /lib/x86_64-linux-gnu/libcudart.so failed
attempt to open /lib/x86_64-linux-gnu/libcudart.a failed
attempt to open /usr/lib/x86_64-linux-gnu/libcudart.so failed
attempt to open /usr/lib/x86_64-linux-gnu/libcudart.a failed
attempt to open /usr/lib/x86_64-linux-gnu64/libcudart.so failed
attempt to open /usr/lib/x86_64-linux-gnu64/libcudart.a failed
attempt to open /usr/local/lib64/libcudart.so failed
attempt to open /usr/local/lib64/libcudart.a failed
attempt to open /lib64/libcudart.so failed
attempt to open /lib64/libcudart.a failed
attempt to open /usr/lib64/libcudart.so failed
attempt to open /usr/lib64/libcudart.a failed
attempt to open /usr/local/lib/libcudart.so failed
attempt to open /usr/local/lib/libcudart.a failed
attempt to open /lib/libcudart.so failed
attempt to open /lib/libcudart.a failed
attempt to open /usr/lib/libcudart.so failed
attempt to open /usr/lib/libcudart.a failed
attempt to open /usr/x86_64-linux-gnu/lib64/libcudart.so failed
attempt to open /usr/x86_64-linux-gnu/lib64/libcudart.a failed
attempt to open /usr/x86_64-linux-gnu/lib/libcudart.so failed
attempt to open /usr/x86_64-linux-gnu/lib/libcudart.a failed

以上输出内容说明了两个问题:

  • 使用 -L/path1 声明的路径会被优先查找。
  • ld 自带一些预定义的查找路径。

可惜的是, ld 的预定义查找路径是嵌入在可执行文件里的, 并不能通过配置文件更改。

然后解释一些常见的环境变量对 ld 的影响:

  • LD_LIBRARY_PATH: 只影响依赖的动态链接库查找路径, 不影响由-l 参数指定的动态链接库查找。输入命令 export LD_LIBRARY_PATH=/what:/else:/nothing ; ld -L/path1 -lcudart --verbose | grep attempt 就可以看到, LD_LIBRARY_PATH 是不会被查找的。
  • LIBRARY_PATH: 对 ld 没有直接影响, 但一般的 C/C++编译器会把这个环境变量转化成 ld 的参数。在 a.c 里面写一个 hello-world 程序, 输入 export LIBRARY_PATH=/path/exist ; gcc a.c -o a.out -v, 就可以看到这部分影响了。gcc 还会主动查找路径是否存在, 并忽略不存在的路径, 而 clang 则直接传给 ld。为了避免覆盖其它地方设置的路径, 通常采用 export LIBRARY_PATH="/path/exist/:$LIBRARY_PATH"的形式追加一个路径。
  • LDFLAGS: 对 ld 及编译器都没有影响, 必须在 Makefile 或者 CMakeLists.txt 里手动读取并传递给编译器(例如 gcc ${LDFLAGS}), 编译器再传递给链接器。它的多个路径必须分开输入, 比如 export LDFLAGS=“-L/path1 -L/path2”, 而不是像常见的 PATH 那样用冒号分开。为了避免覆盖其它地方设置的路径, 通常采用 export LDFLAGS="-L/path1 -L/path2 $LDFLAGS"的形式追加一个路径。

总结: LDFLAGS 由项目开发者传递给编译器; LIBRARY_PATH 由编译器传递给链接器; LD_LIBRARY_PATH 不影响链接器查找动态链接库; 链接器内置一些查找路径, 但是不可配置。

以上细节, 都可以在 Linux 官方网站上找到, 这里是关于 ld 的最权威信息来源。

由于以上环境变量都不是直接作用于链接器, 如果项目配置不当, 即使设置了环境变量, 也还是可能出现 undefined symbol 的问题。如果多次尝试依然失败, 可以考虑直接从链接器预定义的查找路径入手: 由于这些预定义的路径是嵌入在链接器可执行程序中、不可更改的部分, 所以为了考虑兼容性, 链接器会把一些常见的库文件目录都包含在里面, 于是有可能出现某些预定义路径在当前机器中并不存在的问题。如果确定要找的 libxxx.so 就是在/path/路径下面, 但是怎么都无法配置成功, 可以在链接器的预定义路径中找到一个不存在的路径, 把/path/软链接到那个路径上。这样, 无论项目如何配置、编译器什么版本, 最终调用链接器的时候一定会搜索这个路径。

当然, 以上是不得已而为之的杀手锏, 一般情况下还是尽量不用, 而且要记得在编译成功之后删除这个软链接, 否则会影响其他人使用。

3.1.2. 运行时的动态链接库

动态链接库, 顾名思义, 就是在编译时只是动态链接, 只记录符号的位置, 到运行时再去找对应的代码。如果运行时无法找到动态链接库, 程序依然会报错。

运行时的动态链接库查找由 Linux 库函数 dlopen 负责。以查找 libcudart.so 为例, 根据文档, 主要涉及以下内容:

  • 查找环境变量 LD_LIBRARY_PATH 对应的目录;
  • 查找 /etc/ld.so.cache 里面的条目;
  • 查找 /lib/usr/lib;

因此, 一般来说, 如果运行时遇到 undefined symbol 问题, 只需要修改环境变量 export LD_LIBRARY_PATH=/path/:$LD_LIBRARY_PATH 就行了。

如果无法影响环境变量(或者不想每次都定义环境变量), 则可以修改/etc/ld.so.conf 及/etc/ld.so.conf.d/里面的配置文件内的路径, 并且调用 ldconfig 命令(需要 sudo 权限)更新 /etc/ld.so.cache。/etc/ld.so.cache 内的条目可以通过 ldconfig -p 查看。

3.1.3. 总结

一般来说, 可以用 LIBRARY_PATH 来控制编译时链接器查找的路径、用 LD_LIBRARY_PATH 来控制运行时查找动态链接库的路径。如果实在遇到问题无法解决, 可以(在编译时)把路径链接到链接器默认的 SEARCH_DIR、(在运行时)更新到/etc/ld.so.cache 内, 这样能确保编译/运行, 但是之后建议取消这些更改, 避免影响其他用户。

本文假设动态链接库已经下载安装好了。与 cuda 相关的动态链接库, 目前有非常方便的使用方式: NVIDIA 在 conda 上面维护了非常完备的 cuda 全套版本, 只需要 conda install cuda -c “nvidia/label/cuda-11.8.0”, 就可以下载全套的 cuda 开发套件, 其中 C O N D A P R E F I X / l i b / 里面有全部的动态链接库 ( 例如 l i b c u d a r t . s o ) 、 {CONDA_PREFIX}/lib/里面有全部的动态链接库(例如 libcudart.so)、 CONDAPREFIX/lib/里面有全部的动态链接库(例如libcudart.so){CONDA_PREFIX}/bin/里面有全部的可执行文件(例如 nvcc)。如果安装的是 11.8.0 版本的 cuda, 则存在 libcudart.so/libcudart.so.11/libcudart.so.11.8/libcudart.so.11.8.0 这几个文件, 并且其实都是指向同一个文件的符号链接。

希望以上内容能帮助大家彻底告别 undefined symbol 问题!

3.2. ld 命令及其搜索路径顺序

3.2.1. ld 命令

参考:
Linux 命令 (65)—— ld 命令
使用 ld 命令链接目标文件生成可执行文件

3.2.2. 简介

ld(Link eDitor) 命令是二进制工具集 GNU Binutils 的一员, 是 GNU 链接器, 用于将目标文件与库链接为可执行文件或库文件。

ld 命令支持众多链接选项, 但是大部分选项很少被使用, 常用参数:

  • -o 指定输出文件名
  • -e 指定程序的入口符号

3.2.3. 使用示例

  • 链接目标文件生成可执行文件。如给定 C++ 目标文件 test.o 与 main.o, 生成可执行文件 test.out。注意这个过程中还需要链接很多系统库文件, 因此写法非常复杂。这也是 ld 命令很少使用的原因。
ld /usr/lib64/crt1.o /usr/lib64/crti.o /usr/lib64/crtn.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtbegin.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtend.o -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5 -L/usr/lib64 -L/usr/lib -lstdc++ -lm -lgcc_s -lc -lgcc  main.o test.o -o test.out
  • 因为生成一个 C++可执行文件, 需要依赖很多系统库和相关的目标文件, 比如 C 语言库 libc.a, 所以使用 ld 进行链接时, 需要注意添加较长的命令选项, 不然会报链接错误。使用 g++ -v 命令可以查看生成可执行文所需的相关依赖。
g++ -v main.o test.o
...
usr/libexec/gcc/x86_64-redhat-linux/4.8.5/collect2 --build-id --no-add-needed --eh-frame-hdr --hash-style=gnu -m elf_x86_64 -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib64/crt1.o /usr/lib64/crti.o /usr/lib64/crtn.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtbegin.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtend.o -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5 -L/usr/lib64 -L/usr/lib -lstdc++ -lm -lgcc_s -lc -lgcc  main.o test.o

3.2.4. 命令搜索路径顺序

  • 搜索路径顺序

参考: Ubuntu 之 ld 搜索路径顺序

注意: 下面说的搜索顺序应该是对的, 但是其他内容可能存在一些错误。比如 LIBRARY_PATH 和 LD_LIBRARY_PATH 就分别是静态库和动态库吗? 另外库的默认搜索路径不止/lib /usr/lib /usr/local/lib 这些。

静态库链接时搜索路径顺序

  1. ld 会去找 GCC 命令中的参数-L
  2. 再找 gcc 的环境变量 LIBRARY_PATH
  3. 再找内定目录 /lib /usr/lib /usr/local/lib 这是当初 compile gcc 时写在程序内的

动态链接时、执行时搜索路径顺序:

  1. 编译目标代码时指定的动态库搜索路径
  2. 环境变量 LD_LIBRARY_PATH 指定的动态库搜索路径
  3. 配置文件/etc/ld.so.conf 中指定的动态库搜索路径
  4. 默认的动态库搜索路径/lib
  5. 默认的动态库搜索路径/usr/lib

有关环境变量

LIBRARY_PATH 环境变量: 指定程序静态链接库文件搜索路径
LD_LIBRARY_PATH 环境变量: 指定程序动态链接库文件搜索路径

  • 如何查看 ld(链接器)的搜索顺序及编译时需要打开的库

参考: 如何查看 ld(连接器)的搜索顺序及编译时需要打开的库

查看 ld 默认搜索路径

# 1. 把 ld 命令脚本的所有内容输入到 ld_path 文件中
ld --verbose | tee ~/ld_path.txt

# 2. 仅仅过滤其中 SEARCH 有关的部分显示
ld --verbose | grep SEARCH

其中, ld --verbose | grep SEARCH 显示结果如下, 可以看到不仅包括 lib /usr/lib uar/local/lib 等目录, 还有其他目录。

  • 查看编译时需要打开的库

假设有一个文件 dummy.cpp, 内容如下:

#include <iostream>

int main()
{

}

使用 g++ dummy.cpp -Wl,--verbose | grep succeeded, 或者系统语言是中文的话, 输入 g++ dummy.cpp -Wl,--verbose | grep 成功, 查看编译过程中打开的库。结果如下:

4. 动态库

4.1. 动态链接和动态加载的区别

动态加载和动态链接都是在程序运行时发生, 并将所需代码拷贝到内存, 这点很重要!

关键区别是: 动态链接的流程是 OS 直接把共享库的代码拷贝到内存, 而动态加载由人工指定(代码中的 dlopen() 接口)。

动态链接需要 OS 的特殊支持, 通过动态链接方式拷贝到内存的库代码可以在各个进程之间共享。而对动态加载而言, 可以在各自进程中打开共享库代码。

5. go 编译

选自 《Go 语言高级编程》

5.1. CGO 编译和链接参数

编译和链接参数是每一个 C/C++程序员需要经常面对的问题。构建每一个 C/C++应用均需要经过编译和链接两个步骤, CGO 也是如此。 本节我们将简要讨论 CGO 中经常用到的编译和链接参数的用法。

5.1.1. 编译参数: CFLAGS/CPPFLAGS/CXXFLAGS

编译参数主要是头文件的检索路径, 预定义的宏等参数。理论上来说 C 和 C++是完全独立的两个编程语言, 它们可以有着自己独立的编译参数。 但是因为 C++语言对 C 语言做了深度兼容, 甚至可以将 C++理解为 C 语言的超集, 因此 C 和 C++语言之间又会共享很多编译参数。 因此 CGO 提供了 CFLAGS/CPPFLAGS/CXXFLAGS 三种参数, 其中 CFLAGS 对应 C 语言编译参数(以。c 后缀名)、 CPPFLAGS 对应 C/C++ 代码编译参数 (.c,.cc,.cpp,.cxx)、CXXFLAGS 对应纯 C++编译参数 (.cc,.cpp,*.cxx)。

5.1.2. 链接参数: LDFLAGS

链接参数主要包含要链接库的检索目录和要链接库的名字。因为历史遗留问题, 链接库不支持相对路径, 我们必须为链接库指定绝对路径。 cgo 中的 ${SRCDIR} 为当前目录的绝对路径。经过编译后的 C 和 C++目标文件格式是一样的, 因此 LDFLAGS 对应 C/C++共同的链接参数。

5.1.3. pkg-config

为不同 C/C++库提供编译和链接参数是一项非常繁琐的工作, 因此 cgo 提供了对应 pkg-config 工具的支持。 我们可以通过#cgo pkg-config xxx 命令来生成 xxx 库需要的编译和链接参数, 其底层通过调用 pkg-config xxx --cflags 生成编译参数, 通过 pkg-config xxx --libs 命令生成链接参数。 需要注意的是 pkg-config 工具生成的编译和链接参数是 C/C++公用的, 无法做更细的区分。

pkg-config 工具虽然方便, 但是有很多非标准的 C/C++库并没有实现对其支持。 这时候我们可以手工为 pkg-config 工具创建对应库的编译和链接参数实现支持。

比如有一个名为 xxx 的 C/C++库, 我们可以手工创建/usr/local/lib/pkgconfig/xxx.bc 文件:

Name: xxx
Cflags:-I/usr/local/include
Libs:-L/usr/local/lib –lxxx2

其中 Name 是库的名字, Cflags 和 Libs 行分别对应 xxx 使用库需要的编译和链接参数。如果 bc 文件在其它目录, 可以通过 PKG_CONFIG_PATH 环境变量指定 pkg-config 工具的检索目录。

而对应 cgo 来说, 我们甚至可以通过 PKG_CONFIG 环境变量可指定自定义的 pkg-config 程序。 如果是自己实现 CGO 专用的 pkg-config 程序, 只要处理–cflags 和–libs 两个参数即可。

下面的程序是 macos 系统下生成 Python3 的编译和链接参数:

// py3-config.go
func main() {
    for _, s := range os.Args {
        if s == "--cflags" {
            out, _ := exec.Command("python3-config", "--cflags").CombinedOutput()
            out = bytes.Replace(out, []byte("-arch"), []byte{}, -1)
            out = bytes.Replace(out, []byte("i386"), []byte{}, -1)
            out = bytes.Replace(out, []byte("x86_64"), []byte{}, -1)
            fmt.Print(string(out))
            return
        }
        if s == "--libs" {
            out, _ := exec.Command("python3-config", "--ldflags").CombinedOutput()
            fmt.Print(string(out))
            return
        }
    }
}

然后通过以下命令构建并使用自定义的 pkg-config 工具:

$ go build -o py3-config py3-config.go
$ PKG_CONFIG=./py3-config go build -buildmode=c-shared -o gopkg.so main.go

5.1.4. go get 链

在使用 go get 获取 Go 语言包的同时会获取包依赖的包。比如 A 包依赖 B 包, B 包依赖 C 包, C 包依赖 D 包: pkgA -> pkgB -> pkgC -> pkgD -> …。再 go get 获取 A 包之后会依次线获取 BCD 包。 如果在获取 B 包之后构建失败, 那么将导致链条的断裂, 从而导致 A 包的构建失败。

链条断裂的原因有很多, 其中常见的原因有:

  • 不支持某些系统, 编译失败
  • 依赖 cgo, 用户没有安装 gcc
  • 依赖 cgo, 但是依赖的库没有安装
  • 依赖 pkg-config, windows 上没有安装
  • 依赖 pkg-config, 没有找到对应的 bc 文件
  • 依赖 自定义的 pkg-config, 需要额外的配置
  • 依赖 swig, 用户没有安装 swig, 或版本不对

仔细分析可以发现, 失败的原因中和 CGO 相关的问题占了绝大多数。这并不是偶然现象, 自动化构建 C/C++代码一直是一个世界难题, 到目前位置也没有出现一个大家认可的统一的 C/C++管理工具。

因为用了 cgo, 比如 gcc 等构建工具是必须安装的, 同时尽量要做到对主流系统的支持。 如果依赖的 C/C++包比较小并且有源代码的前提下, 可以优先选择从代码构建。

5.1.5. 多个非 main 包中导出 C 函数

官方文档说明导出的 Go 函数要放 main 包, 但是真实情况是其它包的 Go 导出函数也是有效的。 因为导出后的 Go 函数就可以当作 C 函数使用, 所以必须有效。但是不同包导出的 Go 函数将在同一个全局的名字空间, 因此需要小心避免重名的问题。 如果是从不同的包导出 Go 函数到 C 语言空间, 那么 cgo 自动生成的_cgo_export.h 文件将无法包含全部到处的函数声明, 我们必须通过手写头文件的方式什么导出的全部函数。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云满笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值