在 Linux GCC 下,抛出任何 C++ 异常都会终止静态构建中的程序

Throwing any C++ exception terminates program in static build under Linux GCC

我需要在 Linux 下修复一个巨大的商业 C++ 项目。它是使用 GCC(特别是 g++)构建的,在公司中定制编写的构建系统下。它是在 Raspberry Pi 4B.运行 下建造的。

当它是动态构建的(没有 -static 标志)时,一切正常并且程序 运行s 正确并给出正确的结果。

但是当它是静态构建时(使用 -static 标志)然后程序会在任何第一个异常时崩溃。为了检查程序是静态的,我 运行 ldd ./prog 并且它显示 not a dynamic executable。如果对动态构建执行相同的 ldd 命令,那么它会显示一堆动态库。

我决定让整个构建配置完全相同,但在主文件中只是用以下简单代码替换了 main() 函数:

int main() {
    std::cout << "Before exc" << std::endl;
    try {
        throw std::runtime_error("Hello, World!");
    } catch (std::exception const & ex) {
        std::cout << "Thrown '" << ex.what() << "'." << std::endl;
    }
    std::cout << "After exc" << std::endl;
}

如果上面带有 main() 的整个项目是动态构建的,那么它会正确显示以下内容,如预期的那样:

Before exc
Thrown 'Hello, World!'.
After exc

如果我静态构建完全相同的东西,那么输出是这样的:

Before exc
terminate called after throwing an instance of 'std::runtime_error'
terminate called recursively
Aborted

换句话说,消息 Before exc 被显示,然后异常被抛出,但不是捕获它而是调用 terminate 并且程序中止。

如果使用相同的构建系统,我构建的不是整个项目,而是使用上面的代码静态构建单个文件 main.cpp,那么它会输出预期的内容。所以这里的问题是所有项目文件和 linked 库,而不是构建系统(可能)。

可能项目的某些全局变量在初始化时修改了程序,抛出异常导致终止。但是所有全局变量都被正确初始化,因为 main() 运行s 正确地直到抛出异常的点。全局变量仅使静态构建异常不友好。

有谁知道发生了什么以及 C++ 异常调用何时在抛出点终止?是否存在任何程序配置可以设置异常抛出调用std::terminate()?

其他一切正常,只有例外情况。换句话说,静态程序 运行 可以正确运行一段时间并执行正确的操作,但会在第一个异常时崩溃。

使用的构建命令在下面,所有的名字都是故意修改的,也只剩下2-3个目标文件和库文件,在真实的例子中有几十个相同的.a库和几十个.o 对象。首先是将 .cpp 编译为 .o 的编译命令,第二个是将 .o 聚合为 .a 的归档命令,最后是 link 命令 link将所有 .o.a 文件放入最终的单文件程序中:

"ccache" "g++" "-finput-charset=UTF-8" "-fexec-charset=UTF-8" "-std=c++2a" "-fpic" "-c" "-static" "-static-libgcc" "-static-libstdc++" "-Iinc_dir/" "-I." "-g" "-march=armv6zk+fp" "-ffast-math" "-xc++" "-O0" "-DSOME_DEFINE" "-omisc.o" "misc.cpp"

"ar" "rs" "lib1.a" "lib1.o"

"ccache" "g++" "-finput-charset=UTF-8" "-fexec-charset=UTF-8" "-std=c++2a" "-fpic" "-static" "-static-libgcc" "-static-libstdc++" "-oprog" "main.o" "misc.o" "lib1.a" "lib2.a"  "-L./lib_dir/" "-lboost_program_options" "-lboost_system" "-lpthread" "-lc" "-lgcc" "-ldl" "-lstdc++" "-lstdc++fs"

通过二进制搜索删除部分项目,才发现原因在 misc.cpp 中。不知何故,如果我将它包含在构建中,那么异常会崩溃,如果我不包含,那么异常不会崩溃。

据我了解,misc.cpp 可以使异常崩溃的唯一原因仅是由于 main.cpp 中的某些全局变量对程序进行了此类设置,使异常可崩溃。提醒一下,我将 throw.cpp 包含在抛出玩具异常的项目中,如上面的代码所示。从 throw.cpp 开始,misc.cpp 根本没有被使用。此外 main.cpp 不包含在构建中。基本上我只留下了项目中的 misc.cpp 文件,只有它导致了错误。项目构建配置的所有其余部分只是不会导致崩溃的外部库。

现在我做了一个最小的可复制示例。现在我的项目由 throw.cppmisc.cpp 加上一堆外部 linked 库组成。这个 throw.cpp 只包含 main() 和我在问题开头提到的抛出玩具代码,它没有别的,除了 iostreamstdexcept 之外不包含任何东西。 misc.cpp没有main(),根本没有使用misc.cpp,只是编译和linked。所以现在,如果我从 linking 中删除 misc.cpp,那么异常将在 throw.cpp 的 main() 中正确抛出,没有错误。如果我 link misc.cpp (我根本不使用,只是 link)然后在 throw.cpp 的 main() 中抛出异常会使程序崩溃,仅在静态 link.

经过 6 个小时的问题搜索,我设法找出原因不在项目文件/库/构建系统中。

看来问题出在将任何 C++ 代码与异常和 C 代码与 sscanf()(甚至可能是任何 C 标准库函数)的使用相结合。

这可能是由于 Raspberry Pi 4B 在默认安装中的 GCC/G++ 版本太旧,g++ -v 产生:

Target: arm-linux-gnueabihf
...
Thread model: posix
gcc version 8.3.0 (Raspbian 8.3.0-6+rpi1)

所以版本是 8.3.0,而 GCC 已经有 11.2 版了。

我创建了最小的崩溃示例:

文件z.cpp:

#include <stdexcept>
#include <iostream>

int z2_f();

int main() {
    std::cout << "Before exc" << std::endl;
    try {
        throw std::runtime_error("Hello, World!");
    } catch (std::exception const & ex) {
        std::cout << "Thrown '" << ex.what() << "'." << std::endl;
    } catch (...) {
        std::cout << "Thrown 'Unknown'." << std::endl;
    }
    std::cout << "After exc" << std::endl;
    std::cout << z2_f() << std::endl;
}

文件z2.cpp:

#include <cstdio>

int z2_f() {
    char s[] = "123";
    int x = 0;
    sscanf(s, "%d", &x);
    return x;
}

编译命令:

"g++" "-std=c++2a" "-c" "-static" "-O0" "-oz.o" "z.cpp"

"g++" "-std=c++2a" "-c" "-static" "-O0" "-oz2.o" "z2.cpp"

"g++" "-std=c++2a" "-static" "-oprog.exe" "z.o" "z2.o"

编译后 运行 prog.exe 它在控制台中显示:

Before exc
terminate called after throwing an instance of 'std::runtime_error'
terminate called recursively
Aborted

如果只编译并运行第一个文件 z.cpp(删除了对 z2_f() 的使用),那么一切都会按预期运行而不会崩溃:

Before exc
Thrown 'Hello, World!'.
After exc

我根据页面的 this tutorial (WayBack mirror 安装 GCC/G++ 版本 10.1.0 设法解决了这个问题。基本上可以通过 git clone https://bitbucket.org/sol_prog/raspberry-pi-gcc-binary.git --depth=1.

获得 GCC 二进制作者发布的版本

上面这个版本的玩具例子运行没有崩溃,原来的大项目也运行没有崩溃。

提醒一下,在我使用 8.3.0 版本的 GCC 之前,它因异常而崩溃。

我的 uname -a 显示:

Linux raspberrypi 5.4.51-v7l+ #1333 SMP Mon Aug 10 16:51:40 BST 2020 armv7l GNU/Linux

lsb_release -d 显示:

Raspbian GNU/Linux 10 (buster)