0%

论我是如何被 GCC OpenSSL 链接参数的顺序硬控了一晚上

在 Ubuntu 22.04 上编译以下使用 OpenSSL 库的程序时,发生了 undefined reference to `OPENSSL_init_crypto' 这一链接错误:

1
2
3
4
5
#include <openssl/evp.h>

int main() {
OPENSSL_add_all_algorithms_noconf();
}
1
2
3
4
$ gcc -lssl -lcrypto foo.c
/usr/bin/ld: /tmp/ccHvEUdz.o: in function `main':
foo.c:(.text+0x13): undefined reference to `OPENSSL_init_crypto'
collect2: error: ld returned 1 exit status

与此同时,我在一个干净的 NixOS 系统中尝试了编译同一个文件,是能成功的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
description = "A very basic flake";

inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
};

outputs =
{ self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
in
{
devShells.${system}.default = pkgs.mkShell {
buildInputs = with pkgs; [
openssl
];

nativeBuildInputs = with pkgs; [
gcc11
pkg-config
];
};
};
}
1
2
3
4
NixOS$ pkg-config --cflags --libs openssl
-I/nix/store/xm6d1ig9ff7zkid1gvzsbig45m2pnlaz-openssl-3.3.2-dev/include -L/nix/store/m8gwqmn8k3jm0gbcia358mz4y00lgmbc-openssl-3.3.2/lib -lssl -lcrypto
NixOS$ gcc $(pkg-config --libs openssl) foo.c
NixOS$ ./a.out

为了排查问题,首先确认相关库的版本是否正确。注意在 Ubuntu 22.04 上,OpenSSL 的打包粒度很细,我把相关的包都重装了一遍(实际上还做了一堆别的检查,从略):

1
# apt reinstall openssl libssl3 libssl-dev

接下来确认使用的 linking flags 是否正确:

1
2
$ pkg-config --cflags --libs openssl
-lssl -lcrypto

可见我使用的编译参数与 pkg-config 推荐的一样。接下来我们查看 GCC 实际使用的 .so 文件是什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ gcc --verbose -Wl,--verbose foo.c
(...略...)
/usr/bin/ld: mode elf_x86_64
(...略...)
attempt to open /usr/lib/gcc/x86_64-linux-gnu/11/libssl.so failed
attempt to open /usr/lib/gcc/x86_64-linux-gnu/11/libssl.a failed
attempt to open /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/libssl.so succeeded
/usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/libssl.so
attempt to open /usr/lib/gcc/x86_64-linux-gnu/11/libcrypto.so failed
attempt to open /usr/lib/gcc/x86_64-linux-gnu/11/libcrypto.a failed
attempt to open /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/libcrypto.so succeeded
/usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/libcrypto.so
(...略...)
/usr/bin/ld: /tmp/ccJpwMbq.o: in function `main':
foo.c:(.text+0x13): undefined reference to `OPENSSL_init_crypto'
/usr/bin/ld: link errors found, deleting executable `a.out'
collect2: error: ld returned 1 exit status

可见 ld 实际使用的是 /usr/lib/x86_64-linux-gnu/lib{ssl,crypto}.so 两个文件。我们使用 nm -D 命令查看相关符号是否存在:

1
2
3
4
$ nm -D /usr/lib/x86_64-linux-gnu/libssl.so | rg OPENSSL_init_crypto
U OPENSSL_init_crypto@OPENSSL_3.0.0
$ nm -D /usr/lib/x86_64-linux-gnu/libcrypto.so | rg OPENSSL_init_crypto
00000000001bf600 T OPENSSL_init_crypto@@OPENSSL_3.0.0

根据 nm(1p)

1
2
3
4
"T"
"t" The symbol is in the text (code) section.

"U" The symbol is undefined.

可见 libcrypto.so 中确实定义了 OPENSSL_init_crypto 这一符号。这可奇了,为什么 ld 就是找不到它呢?

经过痛苦而无益的尝试,小编突然发现,只要把链接参数和源文件的顺序调转一下,就能在 Ubuntu 上成功编译了:

1
2
3
4
5
6
$ gcc -lssl -lcrypto foo.c
/usr/bin/ld: /tmp/ccIqp0ls.o: in function `main':
foo.c:(.text+0x13): undefined reference to `OPENSSL_init_crypto'
collect2: error: ld returned 1 exit status
$ gcc foo.c -lssl -lcrypto
$ ./a.out

相对地,在 NixOS 上,参数的顺序并无影响:

1
2
NixOS$ gcc $(pkg-config --libs openssl) foo.c
NixOS$ gcc foo.c $(pkg-config --libs openssl)

其实这一切背后的原因与 --{,no-}as-needed 这一组链接参数有关。具体的细节在 Why does the order in which libraries are linked sometimes cause errors in GCC? - Stack Overflow 这个 Stack Overflow 回答里科普得很清楚,就不多介绍啦。

需要注意的是,这两个参数的影响除了编译成功与否之外,还反映在最终的可执行文件上:如果实际上没有引用库的内容,则 --as-needed 实际上不会将可执行文件链接过去(可以使用 ldd 确认)。

由此可见,虽然 pkg-config 确实能给出(推荐的)编译参数,但怎样把这些编译参数正确地整合到编译命令里(对于没有经验的人来说)还是一件大麻烦。所以,有没有相关的编译命令自动生成 / 验证 / 修复工具(的需求)?

救救孩子……