AddressSanitizer и загрузка динамических библиотек во время выполнения -> (‹неизвестный модуль›)

Я использую 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 > )), в то время как библиотеки, связанные во время компиляции, работают безупречно. Мой вопрос:

Можно ли исправить это с помощью некоторых параметров компилятора или нет возможности получить дополнительную информацию с помощью AddressSanitizer, когда речь идет о библиотеках, загруженных с помощью dlopen? Очевидно, что llvm-symbolizer можно найти, иначе не было бы номеров строк для другой библиотеки. Запуск программы с

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

не приводит к другому результату. Вместо этого я скомпилировал программу с помощью g++, но вывод остался прежним. Я также передал вывод через asan_symbolize.py, но ничего не изменилось. Я не знаю, где искать дальше. Есть ли фундаментальная ошибка в моем мышлении? Я не специалист по динамической загрузке библиотек.


person kamshi    schedule 19.06.2017    source источник


Ответы (2)


Я действительно срезал углы, когда дело доходит до отслеживания таких проблем в динамически загружаемых библиотеках, но я просто опускаю код выгрузки библиотеки в целях тестирования, поэтому символы по-прежнему будут доступны для sanitizer (и valgrind) после завершения программы. Хотя это может привести к ложному обнаружению утечек, поскольку персонал, выделенный dlopen, не будет освобожден.

И кажется, что нет правильного решения этой проблемы, потому что технически после выгрузки библиотеки ничто не мешает загрузить другую библиотеку по тому же адресу.

person user7860670    schedule 19.06.2017
comment
Спасибо, после того, как я отключил выгрузку библиотеки, я получил правильный вывод. Жалко, что не работает, когда выгружаешь библиотеку как надо. Думаю, я воспользуюсь вашим подходом и просто отключу выгрузку библиотеки при поиске утечек памяти. - person kamshi; 19.06.2017

Это известная ошибка в ASan (см. проблема 89). Он существует уже некоторое время, но, похоже, никто не заинтересован в его исправлении.

person yugr    schedule 19.06.2017