C++类初始化列表在共享库中引发栈损坏问题排查
动态库修改类成员后主程序未重编译导致栈溢出崩溃
环境
- Linux amd64架构
- gcc版本:9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)
操作步骤
- 执行
make clean清理文件 - 执行
make main,同时构建无MORE宏的base-orig版本库 - 运行
./main,程序正常执行 - 执行
make base_more,构建带MORE宏的库版本 - 再次运行
./main,程序崩溃,输出如下:
Base hello Base class constructor has non-null MORE Base goodbye Base class destructor has non-null MORE *** stack smashing detected ***: terminated Aborted (core dumped)
代码
库头文件(base.h)
#ifdef MORE #include <functional> #endif class base { public: base(); ~base(); private: #ifdef MORE std::function<void()> more_; #endif };
库源文件(base.cpp)
#include "base.h" #include <iostream> #ifdef MORE void hi() { std::cout << "Hello from MORE" << std::endl; } #endif base::base() #ifdef MORE : more_(std::bind(&hi)) #endif { std::cout << "Base hello " << std::endl; #ifdef MORE if (nullptr != more_) { std::cout << "Base class constructor has non-null MORE" << std::endl; } #endif } base::~base() { std::cout << "Base goodbye " << std::endl; #ifdef MORE if (nullptr != more_) { std::cout << "Base class destructor has non-null MORE" << std::endl; } #endif }
主程序(main.cpp)
#include "base.h" int main() { base x; }
Makefile
base_orig: g++ -O0 -g -fPIC -shared -olibbase.so base.cpp objdump -C -S -d libbase.so > orig.objdump base_more: g++ -O0 -g -DMORE -fPIC -shared -olibbase.so base.cpp objdump -C -S -d libbase.so > more.objdump main: base_orig g++ -O0 -g -Wextra -Werror main.cpp -o main -L. -Wl,-rpath=. -lbase objdump -C -S -d main > main.objdump clean: rm -f main libbase.so
问题原因与解决方案
核心原因
你误解了ABI兼容性的要求:类的私有成员变化同样会破坏ABI。
- 编译主程序时,未定义
MORE宏,base类是空类(C++中空类默认占1字节),编译器会按照这个大小在栈上为对象x分配空间。 - 编译带
MORE宏的库时,base类新增了std::function<void()> more_成员,这个类型在amd64架构下通常占用32字节左右,类的总大小被大幅扩大。 - 当主程序调用库中的构造/析构函数时,库代码会操作远大于主程序分配的栈空间,写入超出部分会破坏栈帧的关键数据(比如栈保护变量、返回地址),最终触发栈溢出检测(
stack smashing detected)导致崩溃。
C++的ABI要求调用者和库之间,类的大小、内存布局、成员偏移量必须完全一致,任何类定义的修改(哪怕是私有成员)都会打破这个一致性。
解决方案
- 强制重编译依赖代码:只要类的定义发生变化,所有使用该类的代码(包括主程序)都必须重新编译,确保内存布局完全匹配。
- 使用Pimpl模式保持ABI稳定:如果需要在不破坏ABI的前提下扩展类功能,可以将可变的成员放在一个内部结构体中,类对外只保留一个指向该结构体的指针。这样类的大小始终是指针的大小(amd64上8字节),即使内部结构体修改,也不会影响外部的ABI。
内容的提问来源于stack exchange,提问作者Paul Grinberg
相关产品推荐
相关产品推荐

