You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修复C++ UBSAN vptr运行时错误(成员调用地址问题)

UBSAN运行时错误:成员调用指向非法类型的问题分析与修复

最近我在项目里启用UBSAN(-fsanitize=undefined)做未定义行为检测时,碰到了一个奇怪的运行时错误,场景是跨动态库调用类成员时触发的。先给大家还原一下问题,再分析原因和修复方案。

问题场景与重现代码

我有一个动态库lib.so,定义了两个类:抽象基类Exception和子类FileException,还有一个调用这个库的可执行文件main。

动态库头文件(lib.h)

#ifndef LIB_H
#define LIB_H
#ifdef API_EXPORTS
#define API __attribute__((visibility("default")))
#else
#define API
#endif
class API Exception {
public:
    virtual ~Exception() = 0;
    void SetReporter();
};
class API FileException : public Exception {
public:
    ~FileException();
};
#endif

动态库实现文件(lib.cpp)

#include "lib.h"
Exception::~Exception() = default;
FileException::~FileException() = default;
void Exception::SetReporter() { }

可执行文件代码(main.cpp)

#include "lib.h"
int main(void) {
    FileException ex;
    ex.SetReporter();
    return 0;
}

GCC构建脚本(build_run_gcc.sh)

#!/bin/bash
gcc --version
# 编译动态库
g++ -fPIC -D API_EXPORTS -o lib.so -shared lib.cpp -fvisibility=hidden -Wall -fsanitize=undefined -lubsan
# 编译可执行文件
g++ -o main main.cpp ./lib.so -fvisibility=hidden -Wall -fsanitize=undefined -lubsan
# 运行测试
./main

运行后触发UBSAN错误:

main.cpp:5:19: runtime error: member call on address 0x7ffcb88a8c60 which does not point to an object of type 'Exception'
0x7ffcb88a8c60: note: object is of type 'FileException'
14 56 00 00 48 cd 41 3d 14 56 00 00 00 fa 14 fd f4 29 3f 51 60 8d 8a b8 fc 7f 00 00 00 00 00 00
^~~~~~~~~~~~~~~~~~~~~~~ vptr for 'FileException'

错误根源分析

这个问题的核心是类可见性与虚函数表(vtable)可见性不匹配,导致跨模块(可执行文件和动态库)的C++类型信息不一致,触发了UBSAN的未定义行为检测:

  • 我们通过API宏给Exception和FileException类标记了__attribute__((visibility("default"))),让类本身可以被外部模块访问;
  • 但编译时用了-fvisibility=hidden作为全局默认可见性,这个选项会把所有未显式标记为default可见的符号都隐藏,包括类的虚函数表(vtable);
  • 在可执行文件main中,编译器看到FileException是Exception的子类,会基于Exception的类型信息来处理对象;但动态库中Exception的vtable是隐藏的,导致可执行文件里的Exception类型信息和动态库中FileException的vptr不匹配;
  • UBSAN检测到这种类型不匹配,就判定为“成员调用指向非法类型”的未定义行为。

修复方案(GCC和Clang通用)

我们需要让虚函数表的可见性和类的可见性保持一致,有两种简单有效的方法:

方法1:显式标记虚函数为默认可见

修改lib.h,把所有虚函数(包括析构函数)也标记为API,这样它们的vtable符号就不会被-fvisibility=hidden隐藏:

#ifndef LIB_H
#define LIB_H
#ifdef API_EXPORTS
#define API __attribute__((visibility("default")))
#else
#define API
#endif
class API Exception {
public:
    API virtual ~Exception() = 0;  // 给虚析构函数加上API标记
    void SetReporter();
};
class API FileException : public Exception {
public:
    API ~FileException();  // 子类的析构函数同样标记
};
#endif

方法2:替换全局可见性选项

如果不想逐个标记虚函数,可以把编译选项中的-fvisibility=hidden换成-fvisibility-inlines-hidden。这个选项只会隐藏内联函数的符号,不会影响类和虚函数表的可见性,既保留了符号隐藏的优化效果,又避免了vtable可见性问题。

修改构建脚本的编译命令:

# 编译动态库
g++ -fPIC -D API_EXPORTS -o lib.so -shared lib.cpp -fvisibility-inlines-hidden -Wall -fsanitize=undefined -lubsan
# 编译可执行文件
g++ -o main main.cpp ./lib.so -fvisibility-inlines-hidden -Wall -fsanitize=undefined -lubsan

验证修复

修改后重新运行构建脚本,UBSAN的运行时错误就会消失,程序可以正常执行。

内容的提问来源于stack exchange,提问作者Hongliang Du

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 09:03:12