AddressSanitizer 和运行时加载动态库 -> (<unknown module>)

AddressSanitizer and loading of dynamic libraries at runtime -> (<unknown module>)

我在所有项目中都使用 AddressSanitizer 来检测内存泄漏、堆损坏等。但是,在 运行 时间通过 dlopen 加载动态库时,AddressSanitizer 的输出还有很多待处理想要的。我写了一个简单的测试程序来说明这个问题。代码本身并不有趣,只是两个库,一个在编译时通过 -l 链接,另一个在 运行 时间用 dlopen 加载。为了完整起见,这里是我用于测试的代码:

// ----------------------------------------------------------------------------
// dllHelper.hpp
#pragma once

#include <string>
#include <sstream>
#include <iostream>

#include <errno.h>
#include <dlfcn.h>

// Generic helper definitions for shared library support
#if defined WIN32
#define MY_DLL_EXPORT __declspec(dllexport)
#define MY_DLL_IMPORT __declspec(dllimport)
#define MY_DLL_LOCAL
#define MY_DLL_INTERNAL
#else
#if __GNUC__ >= 4
#define MY_DLL_EXPORT __attribute__ ((visibility ("default")))
#define MY_DLL_IMPORT __attribute__ ((visibility ("default")))
#define MY_DLL_LOCAL  __attribute__ ((visibility ("hidden")))
#define MY_DLL_INTERNAL __attribute__ ((visibility ("internal")))
#else
#define MY_DLL_IMPORT
#define MY_DLL_EXPORT
#define MY_DLL_LOCAL
#define MY_DLL_INTERNAL
#endif
#endif

void* loadLibrary(const std::string& filename) {
    void* module = dlopen(filename.c_str(), RTLD_NOW | RTLD_GLOBAL);

    if(module == nullptr) {
        char* error = dlerror();
        std::stringstream stream;
        stream << "Error trying to load the library. Filename: " << filename << " Error: " << error;
        std::cout << stream.str() << std::endl;
    }

    return module;
}

void unloadLibrary(void* module) {
    dlerror(); //clear all errors
    int result = dlclose(module);
    if(result != 0) {
        char* error = dlerror();
        std::stringstream stream;
        stream << "Error trying to free the library. Error code: " << error;
        std::cout << stream.str() << std::endl;
    }
}

void* loadFunction(void* module, const std::string& functionName) {
    if(!module) {
        std::cerr << "Invalid module" << std::endl;
        return nullptr;
    }

    dlerror(); //clear all errors
    #ifdef __GNUC__
    __extension__
    #endif
    void* result = dlsym(module, functionName.c_str());
    char* error;
    if((error = dlerror()) != nullptr) {
        std::stringstream stream;
        stream << "Error trying to get address of function \"" << functionName << "\" from the library. Error code: " << error;
        std::cout << stream.str() << std::endl;
    }

    return result;
}


// ----------------------------------------------------------------------------
// testLib.hpp
#pragma once

#include "dllHelper.hpp"

#ifdef TESTLIB
#define TESTLIB_EXPORT MY_DLL_EXPORT
#else
#define TESTLIB_EXPORT MY_DLL_IMPORT
#endif

namespace TestLib {

// will be linked at compile time
class TESTLIB_EXPORT LeakerTestLib {
    public:
        void leak();
};

}


// ----------------------------------------------------------------------------
// testLib.cpp
#include "testLib.hpp"

namespace TestLib {

void LeakerTestLib::leak() {
    volatile char* myLeak = new char[10];
    (void)myLeak;
}

}


// ----------------------------------------------------------------------------
// testLibRuntime.hpp
#pragma once

#include "dllHelper.hpp"

#ifdef TESTLIBRUNTIME
#define TESTLIBRUNTIME_EXPORT MY_DLL_EXPORT
#else
#define TESTLIBRUNTIME_EXPORT MY_DLL_IMPORT
#endif

namespace TestLibRuntime {

// will be loaded via dlopen at runtime
class TESTLIBRUNTIME_EXPORT LeakerTestLib {
    public:
        void leak();
};

}

extern "C" {
    TestLibRuntime::LeakerTestLib* TESTLIBRUNTIME_EXPORT createInstance();
    void TESTLIBRUNTIME_EXPORT freeInstance(TestLibRuntime::LeakerTestLib* instance);
    void TESTLIBRUNTIME_EXPORT performLeak(TestLibRuntime::LeakerTestLib* instance);
}

// ----------------------------------------------------------------------------
// testLibRuntime.cpp
#include "testLibRuntime.hpp"

namespace TestLibRuntime {

void LeakerTestLib::leak() {
    volatile char* myLeak = new char[10];
    (void)myLeak;
}

extern "C" {

    LeakerTestLib* createInstance() {
        return new LeakerTestLib();
    }

    void freeInstance(LeakerTestLib* instance) {
        delete instance;
    }

    void performLeak(LeakerTestLib* instance) {
        if(instance) {
            instance->leak();
        }
    }

}

}


// ----------------------------------------------------------------------------
// main.cpp
#include "testLib.hpp"
#include "testLibRuntime.hpp"

#define LEAK_TESTLIB
#define LEAK_TESTLIBRUNTIME

int main(int argc, char** argv) {
    #ifdef LEAK_TESTLIBRUNTIME
    void* testLibRuntimeModule = loadLibrary("libtestLibRuntime.so");

    if(!testLibRuntimeModule) {
        return -1;
    }

    TestLibRuntime::LeakerTestLib* testLibRuntime = nullptr;

    auto createInstance = (TestLibRuntime::LeakerTestLib * (*)())loadFunction(testLibRuntimeModule, "createInstance");
    if(!createInstance) {
        return -1;
    }
    auto freeInstance = (void(*)(TestLibRuntime::LeakerTestLib*))loadFunction(testLibRuntimeModule, "freeInstance");
    if(!freeInstance) {
        return -1;
    }
    auto performLeak = (void(*)(TestLibRuntime::LeakerTestLib*))loadFunction(testLibRuntimeModule, "performLeak");
    if(!performLeak) {
        return -1;
    }

    testLibRuntime = createInstance();
    performLeak(testLibRuntime);
    freeInstance(testLibRuntime);
    #endif

    #ifdef LEAK_TESTLIB
    TestLib::LeakerTestLib testLib;
    testLib.leak();
    #endif

    #ifdef LEAK_TESTLIBRUNTIME
    unloadLibrary(testLibRuntimeModule);
    #endif

    return 0;
}

我使用以下命令编译了上面的代码:

clang++ -std=c++11 -O0 -g -ggdb -Wl,-undefined -Wl,dynamic_lookup -fsanitize=address -fsanitize-recover=address -fno-omit-frame-pointer -fsanitize-address-use-after-scope -DTESTLIB -shared -fPIC -o libtestLib.so testLib.cpp -ldl -shared-libasan
clang++ -std=c++11 -O0 -g -ggdb -Wl,-undefined -Wl,dynamic_lookup -fsanitize=address -fsanitize-recover=address -fno-omit-frame-pointer -fsanitize-address-use-after-scope -DTESTLIBRUNTIME -shared -fPIC -o libtestLibRuntime.so testLibRuntime.cpp -ldl -shared-libasan
clang++ -std=c++11 -O0 -g -ggdb -Wl,-undefined -Wl,dynamic_lookup -fsanitize=address -fsanitize-recover=address -fno-omit-frame-pointer -fsanitize-address-use-after-scope -o leak main.cpp -ldl -L./ -ltestLib -shared-libasan

当我 运行 程序时,我得到以下输出(我必须事先导出 LD_LIBRARY_PATH 才能找到 libasan):

$ export LD_LIBRARY_PATH=/usr/lib/clang/4.0.0/lib/linux/:./
$ ./leak

=================================================================
==4210==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 10 byte(s) in 1 object(s) allocated from:
    #0 0x7fb665a210f0 in operator new[](unsigned long) (/usr/lib/clang/4.0.0/lib/linux/libclang_rt.asan-x86_64.so+0x10e0f0)
    #1 0x7fb66550d58a in TestLib::LeakerTestLib::leak() /home/jae/projects/clang_memcheck/testLib.cpp:6:29
    #2 0x402978 in main /home/jae/projects/clang_memcheck/main.cpp:37:13
    #3 0x7fb6648d4439 in __libc_start_main (/usr/lib/libc.so.6+0x20439)

Direct leak of 10 byte(s) in 1 object(s) allocated from:
    #0 0x7fb665a210f0 in operator new[](unsigned long) (/usr/lib/clang/4.0.0/lib/linux/libclang_rt.asan-x86_64.so+0x10e0f0)
    #1 0x7fb6617fd6da  (<unknown module>)
    #2 0x7fb6617fd75f  (<unknown module>)
    #3 0x402954 in main /home/jae/projects/clang_memcheck/main.cpp:31:5
    #4 0x7fb6648d4439 in __libc_start_main (/usr/lib/libc.so.6+0x20439)

SUMMARY: AddressSanitizer: 20 byte(s) leaked in 2 allocation(s).

虽然检测到泄漏,但 AddressSanitizer 似乎无法解析通过 dlopen 加载的库的模块名称、函数名称和行号(改为打印 ( < unknown module > )),而库在编译时链接时间完美地工作。我的问题是:

是否可以使用一些编译器开关来解决这个问题,或者当涉及到使用 dlopen 加载的库时,是否无法使用 AddressSanitizer 获取更多信息?显然可以找到 llvm-symbolizer,否则不会有其他库的行号。 运行 程序

ASAN_OPTIONS=symbolize=1 ASAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer ./leak

不会产生不同的输出。我改用 g++ 编译程序,但输出保持不变。我还通过 asan_symbolize.py 管道输出,但没有任何改变。我不知道接下来要看哪里。我的想法有根本性的错误吗?我不是动态加载库方面的专家。

在跟踪动态加载库中的此类问题时,我一直在偷工减料,但出于测试目的,我只是省略了库卸载代码,因此当程序终止时,符号仍可用于消毒程序(和 valgrind)。虽然这样做可能会导致一些错误的泄漏检测,因为 dlopen 分配的人员不会被释放。

这个问题似乎没有合适的解决方案,因为从技术上讲,卸载库后没有什么可以阻止在同一地址加载另一个库。

这是 ASan 中的一个已知错误(请参阅 Issue 89)。它已经存在了一段时间,但似乎没有人愿意修复它。