LINUX.ORG.RU

C++, dlopen, Модули, Segmentation fault

 ,


0

3

Пытаюсь написать небольшую программку с возможностью расширения её функционала через модули которые подгружать хочу вручную.

Вроде флаг RTLD_GLOBAL во второй параметр функции dlopen вполне может такое позволить сделать.

Ну так вот. Идея такая.

1. Программка подтягивает библиотечку (libcore.so) которая, в свою очередь добавляет к уже загруженной программе два вызова - core::oninit и core::doinit

2. Дальше программка догружает модуль (module.so) и вызывает функцию (module) которая в нём объявлена - что бы дальше модуль добавлял что-то в систему через обращение к core::oninit

... если будут ещё модули их тоже подгружаем и они тоже в своих функциях «module» вызывают core::oninit, и т.д.

3. Потом в основной программе делается core::doinit что бы по очереди вызвать то, что модули наскладывали в core::oninit

Вроде простая схема, но... упорно вылетает «Segmentation fault» уже ПОСЛЕ ТОГО КАК ВСЁ ПОЛЕЗНОЕ В main УЖЕ ВЫПОЛНЕНО.

Поэтому собственно вопрос - почему?

Ну или, что то же самое, - что я делаю не так? И как такое надо делать на самом деле правильно?

Сори за небольшое полотно из исходников, но их, на самом деле, не много.

Заранее спасибо за ответы.

start.cpp

#include <stdexcept>
#include <memory>
#include <dlfcn.h>

class Lib
{
    public:
        Lib(const std::string &filename)
        {
            auto handle = new void *;
            try
            {
                *handle = dlopen(filename.c_str(), RTLD_LAZY | RTLD_GLOBAL);
                if (*handle == nullptr) throw std::runtime_error(std::string(dlerror()));
                dlerror();
                try
                {
                    this->m_handle = std::shared_ptr<void *>(handle, [] (void **handle)
                    {
                        dlclose(*handle);
                        delete handle;
                    });
                }
                catch (...)
                {
                    dlclose(*handle);
                    throw;
                }
            }
            catch (...)
            {
                delete handle;
                throw;
            }
        }

        void *get(const std::string &itemname)
        {
            auto item = dlsym(*this->m_handle, itemname.c_str());
            auto error = dlerror();
            if (error != nullptr) throw std::runtime_error(std::string(error));
            return item;
        }

    private:
        std::shared_ptr<void *> m_handle;
};

class Module
{
    public:

        Module(const std::string &filename)
        {
            auto state = new State(filename + ".so");
            try
            {
                this->m_state = std::shared_ptr<State>(state, [] (State *state)
                {
                    delete state;
                });
            }
            catch (...)
            {
                delete state;
                throw;
            }
        }

    private:
        struct State
        {
            Lib lib;

            State(const std::string &filename): lib(Lib(filename))
            {
                auto module = (void (*)(void)) this->lib.get("module");
                if (module == nullptr) std::runtime_error("Error get 'module' routine from \"" + filename + '.');
                module();
            }
        };

        std::shared_ptr<State> m_state;
};

#include <iostream>
#include <vector>
#include "core.h"

int main(void)
{
    {
        std::vector<Module> modules;
        modules.push_back(Module("module"));
        core::doinit();
    }
    std::cout << "Ok\n";
    return EXIT_SUCCESS;
}

core.h

#ifndef __CORE__
#define __CORE__

#include <functional>

namespace core
{
    extern void oninit(const std::function<void(void)> &init);
    extern void doinit(void);
}

#endif // __CORE__

core.cpp

#include "core.h"
#include <vector>

static std::vector<std::function<void(void)>> items;

void core::oninit(const std::function<void(void)> &init)
{
    items.push_back(init);
}

void core::doinit(void)
{
    for (auto &init : items) if (init) init();
}

module.cpp

#include "core.h"
#include <iostream>

extern "C" void module(void)
{
    core::oninit([] () {});
    std::cout << "Module loaded.\n";
}

build.sh

#!/usr/bin/env bash

export LD_LIBRARY_PATH=./:${LD_LIBRARY_PATH}

#std=c++11
#std=c++14
std=c++17

gcc --std=$std core.cpp -shared -fPIC -o libcore.so &&
gcc --std=$std module.cpp -shared -fPIC -o module.so &&
gcc --std=$std start.cpp -l stdc++ -ldl -L. -lcore -o start &&

./start

rm *.so
rm start

Падаете в деструкторе static std::vector<std::function<void(void)>> items в libcore.so. Вероятно, потому, что динамическая библиотека оказывается выгружена деструктором Lib::m_handle в процессе завершения работы приложения до разрушения items, а последний пытается обратиться к какой-то детали шаблонного класса std::function, которая была скомпилирована как часть module.so.

Попробуйте ограничить область видимости items или иначе реорганизовать код так, чтобы items уничтожался перед std::vector<Module> modules.

anonymous ()
Ответ на: комментарий от anonymous

Спасибо, я попробовал заменить core.h и core.cpp на вот такие:

core.h

#ifndef __CORE__
#define __CORE__

namespace core
{
    extern void oninit(void (*init)(void));
    extern void doinit(void);
}

#endif // __CORE__

core.cpp

#include "core.h"
#include <vector>

static std::vector<void (*)(void)> items;

void core::oninit(void (*init)(void))
{
    items.push_back(init);
}

void core::doinit(void)
{
    for (auto &init : items) if (init) init();
}

Без std::function работает.

Но Lambda с захватом теперь не работает...

И да, получается что static std::vector<void (*)(void)> items; после main всё ещё содержит в себе адреса относящиеся к модулям которые уже выгружены.

Но вообще в конце самой программы они больше и не нужны, а значит можно за этими items не следить - по сути они же должны освободиться с выгрузкой libcore.so?

Я вообще и про std::function так же думал.

Вот только зачем классу std::function в своём ДЕСТРУКТОРЕ нужно что-то с этими адресами ВСЁ РАВНО ПРОДОЛЖАТЬ что-то делать?

user0xff ()
Ответ на: комментарий от user0xff

В std::function же type erasure практикуется, чтобы заворачивать лямбды. Вот виртуальный деструктор и объявляется на стороне модуля. Он скорее всего и вызывается. (В код библиотеки не смотрел.)

xaizek ★★★★★ ()
Ответ на: комментарий от user0xff

Вот только зачем классу std::function в своём ДЕСТРУКТОРЕ нужно что-то с этими адресами ВСЁ РАВНО ПРОДОЛЖАТЬ что-то делать?

Не с этими. std::function - это шаблон, и части шаблона самого std::function (загляните в /usr/include/c++/6/functional: некая функция _M_manager, которая, зная тип хранящегося в std::function объекта, правильно его уничтожает) оказались скомпилированы в libcore.so.

anonymous ()
Ответ на: комментарий от anonymous

Наверное всё таки std::function надо оставить. Как-то идеологически это правильно будет.

Вот то, что у меня получилось - это можно как-то улучшить?

Как-то не очень нравиться, что нужно дважды core::stop() писать...

Но даже так вроде бы хорошо.

core.cpp

#include "core.h"
#include <vector>

static std::vector<std::function<void(void)>> items;

void core::oninit(const std::function<void(void)> &init)
{
    items.push_back(init);
}

void core::doinit(void)
{
    for (auto &init : items) if (init) init();
}

void core::stop(void)
{
    items.clear();
}

int main(void)
{
    try
    {
        std::vector<Module> modules;
        try
        {
            modules.push_back(Module("module"));
            core::doinit();
        }
        catch (...)
        {
            core::stop();
            throw;
        }
        core::stop();
    }
    catch (const std::exception &e)
    {
        std::cerr << e.what() << '\n';
        return EXIT_FAILURE;
    }
    catch (...)
    {
        std::cerr << "Unknown error.\n";
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}
user0xff ()

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

anonymous ()

Частично согласен с анонимом выше. У тебя вообще есть хоть какой-то вкус? Как можно писать такое говно?

struct lib {
  
  lib(const std::string & filename) {
    handle = dlopen(filename.c_str(), RTLD_LAZY | RTLD_GLOBAL);
    if(auto err = dlerror()) throw std::runtime_error{err};
  }
  
  ~lib() { dlclose(handle); }
  
  template<typename T> auto get(const std::string &itemname) {
//     static_assert(std::is_function_v<T>);
    auto item = (T *)dlsym(handle, itemname.c_str());
    if(auto err = dlerror()) throw std::runtime_error{err};
    return item;
  }

private:
  void * handle;
};

struct module {
  
  module(const std::string & filename): lib(filename + ".so") {
    auto module = lib.get<void(void)>("module");
    if(!module) std::runtime_error("Error get 'module' routine from \"" + filename + '.');//какой в этом смысл?
    module();
  }
  
private:
  struct lib lib;
};

int main() {
  std::vector<module> modules;
  modules.emplace_back("module");
  
  std::cout << "Ok\n";
}

Я переписал твой start, выпилив ненужное. Примерно так это и должно выглядеть.

std::shared_ptr<State>(state, [] (State *state)
                {
                    delete state;
                });

Судя по этому ты просто пишешь рандомную херню. Зачем ты пытаешься переопределить делитер у поинтера анлогичным дефолтному? Просто потому что можешь?

delightfish ()
Ответ на: комментарий от hateyoufeel

Я бы написал «приятной отладки», но кажется автору и так уже весело )

next_time ★★★★★ ()
Ответ на: комментарий от delightfish

У тебя вообще есть хоть какой-то вкус? Как можно писать такое говно?

Твое на самом деле не особо лучше, хотя бы потому что handle не в RAII

yoghurt ★★★★★ ()
Ответ на: комментарий от anonymous

hint - handle завернуть в юникптр и будет по красоте.

yoghurt ★★★★★ ()

расширения её функционала через модули которые подгружать

Это называется плагины (plugins), модули всё же совсем другое понятие.

hbee ★★★ ()
Ответ на: комментарий от yoghurt

Это чушь, а не дальше. Конкретно нужно показывать в чём проблема. Аргументация уровня «смотри лучше» - это не аргументация, а глупость.

delightfish ()
Ответ на: комментарий от delightfish

Написано уже дважды, но раз надо разжевать - разжевыаваю.

delete x в деструкторе своего класса вовсе не гарантирует отсутствие утечек. Если аффтар словит (или сам решит кинуть) исключение из конструктора этого же класса, но после того, как x (в данном случае - хендл) «заалоцирован» (dlopen), этот x благополучно утечет. А вот определение его в классе не сырым указателем (хендлом, интом, итп), а юникптром или аналогичным гарантирует освобождение в вышеприведенной ситуации.

yoghurt ★★★★★ ()
Ответ на: комментарий от yoghurt

Так у царя исключение только если dlopen завершился ошибкой. В этом случае там нечего освобождать.

Конкретно в данном случае проблемы нет и уник не нужен. А так да, следить за ресурсами в конструкторах нужно.

Ivan_qrt ★★★★★ ()
Ответ на: комментарий от yoghurt

Хоть уже и ответили, но отвечу тоже:

delete x в деструкторе своего класса вовсе не гарантирует отсутствие утечек.

Чини методичку.

Если аффтар словит (или сам решит кинуть) исключение из конструктора этого же класса, но после того, как x (в данном случае - хендл) «заалоцирован» (dlopen), этот x благополучно утечет.

Нет. Кидается там исключение только по dlerr, а когда dlerr говорит об ошибке - dlopen() возвращает nullptr.

этот x благополучно утечет.

Нет.

А вот определение его в классе не сырым указателем (хендлом, интом, итп), а юникптром или аналогичным гарантирует освобождение в вышеприведенной ситуации.

Ты попытался применить методичку туда, куда ненужно.

Я думал о том, о чём ты пишешь. Даже хотел написать ссылку на ман с инвариантом err() -> nullptr, но не нашел. Но практика показывает истинность этого инварианта.

Если тебе прям хочется «надёжности» - добавь в if поверку. Добавление сюда sp слишком избыточно и уродски.

По поводу «гарантирует» - нихрена оно не гарантирует. Оно гарнатирует только в примитивных кейсах. Если ты работаешь с лолевел-дристнёй это тебе поможет мало. И нужно постоянно решать подобные задачки что-бы не обосраться.

Но в целом твои рассуждения имеют смысл, что лучше перебдеть и я бдел мало. Но не пихать бездумно всё с sp.

delightfish ()
Ответ на: комментарий от Ivan_qrt

Конкретно в этом случае конечно все отработает корректно, но представь что потом кто-то (может и не Царь, а например стажёр) решит туда навалить ещё проверок. Свято место для подводного камня тут же заполнится.

В общем, солому надо стелить всегда

yoghurt ★★★★★ ()
Ответ на: комментарий от delightfish

Нет. Кидается там исключение только по dlerr, а когда dlerr говорит об ошибке - dlopen() возвращает nullptr.

Это сегодня. А завтра (через неделю, месяц, год) вставят туда ещё какой-нибудь throw не подумав, и наступят на удачно расположенную граблю

yoghurt ★★★★★ ()
Ответ на: комментарий от yoghurt

Это сегодня.

Все ходы записаны:

Да, но leak flaw всё равно есть, смотри лучше

вставят туда ещё какой-нибудь throw не подумав, и наступят на удачно расположенную граблю

Не подумав моют полы. Но в любом случае я уже сказал, что частично твои рассуждения имеют смысл - нужно побдеть. Но они ложны.

Ложная уверенность всегда хуже. Лучше знать, что может что-то произойти и быть осторожнее, чем думать «а проблемы нет, раз добавил sp» и обосраться.

Подобные советы работают для хайлевел С++-кода, где уже у всего есть exception safety. Подобные советы на уровне лоулевел дристни - работают слабо и даже вредны.

delightfish ()
Ответ на: комментарий от delightfish

Но они ложны.

Не ложны, а во много ложны. Либо во много обманчивы и не всегда применимы.

delightfish ()
Ответ на: комментарий от yoghurt

но представь что потом кто-то (может и не Царь, а например стажёр) решит туда навалить ещё проверок. Свято место для подводного камня тут же заполнится.

Ну в принципе да. Согласен, разумно заранее оборачивать.

Но это всё-таки опция, а не обязательное требование. Тем более странно требовать это от примера нормальной организации классов на форуме.

Ivan_qrt ★★★★★ ()

Segmentation fault


дебагером нынче не модно пользоваться ?
пости сразу простыню кода, пусть пацаны с лора разбираются ?

anonymous ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.