madf: (Default)
[personal profile] madf
Готую Stargazer до релізу, перевіряю його роботу на різних системах. Як я вже писав раніше, Stargazer падав на FreeBSD. І так цікаво падав, що я кілька днів поспіль не міг зрозуміти чому. А сьогодні, нарешті, зрозумів.

Stargazer має модульну структуру: основне ядро і плагіни. Плагіни реалізовані у вигляді класів, що наслідуються від інтерфейсу BASE_PLUGIN. Оскільки ядро саме по собі не знає конкретних класів плагінів - екземпляри класів утворюються всередині динамічної бібліотеки і назовні передається вказівник на створений об'єкт. Щоб не заморочуватись із знищенням об'єктів, в середині бібліотеки створюється об'єкт-CREATOR. У його конструкторі створюється екземпляр об'єкту-плагіну, а у деструкторі - знищується. RAII в повний ріст. У цього об'єкта-CREATOR'а є метод, що повертає назовні вказівник. Для того щоб ядро системи могло цей вказівник отримати бібліотека надає C-функцію BASE_PLUGIN * GetPlugin(), яка отримує вказівник від об'єкта-CREATOR'а і повертає його. Час життя об'єкта-CREATOR'а (а значить - і плагіна) обмежений викликами _init / _fini. Ці дві функції створюються компонувальником автоматично і грають роль конструктора і деструктора для бібліотеки. Саме у ни викликаються конструктор і деструктор об'єкта-CREATOR'а.
Раніше BASE_PLUGIN не мав віртуального деструктора - раз. Раніше бібліотеки не вигружались явно - два.
Перший тривожний дзвіночок пролунав для мене пару років тому, коли я писав утиліту для автоматичного перекидання бази між різними плагінами. Там я явно викликав dlclose() для плагінів і тут виникали падіння. Я грішив на Factory-like спосіб створення об'єктів, і для експерименту спробував замість об'єкта-CREATOR'а використати дві C-функції: BASE_PLUGIN * CreatePlugin() / DestroyPlugin(BASE_PLUGIN *). Зараз вже важко визначити чому - але це допомогло. Минулого літа я залишив таск - перевести всі плагіни на таку схему створення. Це викликало жарку дискусію між мною і Борисом і, як результат, цей таск було залишено на дослідження.
І так мене ця проблема зачепила, що я думав про неї і вдень і вночі! І одного разу просто взяв і дослідив крок за кроком всі дії, що виконуються при при використанні плагінів з об'єктами-CREATOR'ами. І не знайшов нічого підозрілого! Пройшовся gdb по всьому коду - від завантаження плагіна до вивантаження. Дослідив GOT- і PLT-секції бібліотеки - все цивільно, ніяких натяків на можливий SIGSEGV. Про що і отписався в таску.
У лютому, коли закінчив два великих проекти (автомийки і довбаний сайт) знову взявся за Stargazer - пора випускати 2.406. Та і глюки поганяти. Як результат - 3 закритих критичних бага. Випустив альфа- і бета-версію. І от на минулому тижні почав готувати реліз-кандидат. Оскільки це вже серйозна претензія на використання у production - запустив на FreeBSD. І отримав SIGSEGV на вивантаженні плагінів!
В першу чергу, я спробував відтворити ситуацію на Linux, бо FreeBSD стоїть на віртуальній машині на старому серванті. І там крім неї ще дві. І ще багато чого крутиться. Одним словом - працювати там неможливо. І, здається, мені вдалося відтворити падіння на Linux. Вже точно не пам'ятаю що там було, але я привів у порядо код для завантаження плагіна і все запрацювало. От тільки на FreeBSD продовжувало падати. Це було як раз у п'ятницю, під кінець робочого дня. Сестра покликала мене до себе на борщ. До того ж я у них не був із самого переїзду, коли ми тягали на 13-й поверх шкафи і пральну машинку. І в той день я так нічого і не зробів. Зафіксував лише одну дивну річ: перед викликом delete plugin вказівник нормальний, а коли я попадаю у деструктор - this уже зовсім інший!
І ця річ не давала мені спати всі вихідні. У суботу навіть думав приїхати в офіс і подивитись, як таке може бути. Та в решту решт почекав до понеділка.
Сьогодні до обіду займався поточними справами (ТЗ, підрахунки трудозатрат, etc) а потім сів за gdb. Першим розвіяв містичний туман над неправильним this у деструкторі. все виявилось просто: на момент print this цей параметр ще лежав у стеку. Досить було зробити пару разів stepi щоб він став на місце. Це трошки розчарувало - адже тепер у мене не було ідей щодо причин падіння.
Тут покажу скрин - полякати неофітів :)

Фонове вікно - vim з джерельними кодами системи на локальній машині. Над ним 4 шелл на віртуалку із FreeBSD: один для компіляції, у лівому gdb, у правому результат objdump -S у vi, у верхньому nm.
А тепер те, до чого я дійшов:
0x2839f443 - виклик із середини деструктора плагіна через PLT.
0x28372658 - загадкова адреса, куди передається керування і де виникає SIGSEGV.
А, ще забув сказати - перший плагін вивантажується нормально. Наступний - падає. Від самого плагіну це не залежить: якщо їх поміняти місцями - все одно буде падати другий.
Ставлю break на PLUGIN_RUNNER::Unload()
(gdb) break plugin_runner.cpp:132
Breakpoint 1 at 0x80ca13b: file plugin_runner.cpp, line 132.

Запускаю:
(gdb) r ./
Starting program: /tmp/stg-2.4-2009.03.20-17.47.29/projects/stargazer/stargazer ./
      settings.cpp > 03:00:00 > SETTINGS::SETTINGS(const std::string &)
     stg_timer.cpp > 17:24:24 > STG_TIMER started. Time: 1237821864
      settings.cpp > 03:00:00 > SETTINGS::~SETTINGS()
          main.cpp > 17:24:24 > Module: ./modules/mod_auth_ia.so
          main.cpp > 17:24:24 > Module: ./modules/mod_auth_ao.so
          main.cpp > 17:24:24 > Module: ./modules/mod_conf_sg.so
          main.cpp > 17:24:24 > Module: ./modules/mod_cap_bpf.so
          main.cpp > 17:24:24 > Module: ./modules/mod_cap_nf.so
          main.cpp > 17:24:24 > Module: ./modules/mod_ping.so
    inetaccess.cpp > 17:24:24 > sizeof(CONN_SYN_6) = 96 96
    inetaccess.cpp > 17:24:24 > sizeof(CONN_SYN_8) = 96 96
    inetaccess.cpp > 17:24:24 > sizeof(CONN_SYN_ACK_6) = 192 192
    inetaccess.cpp > 17:24:24 > sizeof(CONN_SYN_ACK_8) = 200 200
    inetaccess.cpp > 17:24:24 > sizeof(CONN_ACK_6) = 64 64
    inetaccess.cpp > 17:24:24 > sizeof(ALIVE_SYN_6) = 368 368
    inetaccess.cpp > 17:24:24 > sizeof(ALIVE_SYN_8) = 384 384
    inetaccess.cpp > 17:24:24 > sizeof(ALIVE_ACK_6) = 64 64
    inetaccess.cpp > 17:24:24 > sizeof(DISCONN_SYN_6) = 96 96
    inetaccess.cpp > 17:24:24 > sizeof(DISCONN_SYN_ACK_6) = 24 24
    inetaccess.cpp > 17:24:24 > sizeof(DISCONN_SYN_ACK_8) = 32 32
    inetaccess.cpp > 17:24:24 > sizeof(DISCONN_ACK_6) = 64 64
    inetaccess.cpp > 17:24:24 > sizeof(FIN_6) = 24 24
    inetaccess.cpp > 17:24:24 > sizeof(FIN_8) = 32 32
    inetaccess.cpp > 17:24:24 > sizeof(ERR) = 256 256
    inetaccess.cpp > 17:24:24 > sizeof(INFO_6) = 256 256
    inetaccess.cpp > 17:24:24 > sizeof(INFO_7) = 272 272
    inetaccess.cpp > 17:24:24 > sizeof(INFO_8) = 1068 1072
 plugin_runner.cpp > 17:24:24 > Plugin InetAccess authorizator v.1.2 parsesettings
 plugin_runner.cpp > 17:24:25 > Plugin Always Online authorizator v.1.0 parsesettings
 plugin_runner.cpp > 17:24:25 > Plugin Stg configurator v.0.07 parsesettings
 plugin_runner.cpp > 17:24:25 > Plugin bpf_cap v.1.0 parsesettings
 plugin_runner.cpp > 17:24:26 > Plugin CAP_NF v. 0.3 parsesettings
 plugin_runner.cpp > 17:24:26 > Plugin Pinger v.1.01 parsesettings
          main.cpp > 17:24:26 > Unloading module './modules/mod_auth_ia.so'

Breakpoint 1, PLUGIN_RUNNER::Unload (this=0x80f0388) at plugin_runner.cpp:132
132	    if (dlclose(libHandler))

Так, хто тут у нас де?
(gdb) list *0x2839f443
0x2839f443 is in ~AUTH_AO (ao.h:82).
77	//-----------------------------------------------------------------------------
78	class AUTH_AO :public BASE_AUTH
79	{
80	public:
81	    AUTH_AO();
82	    virtual ~AUTH_AO(){};
83	
84	    void                SetUsers(USERS * u);
85	    void                SetTariffs(TARIFFS *){};
86	    void                SetAdmins(ADMINS *){};
(gdb) list *0x28372658
0x28372658 is in ~BASE_AUTH (base_auth.h:41).
36	
37	//-----------------------------------------------------------------------------
38	class BASE_AUTH : public BASE_PLUGIN
39	{
40	public:
41	    virtual ~BASE_AUTH() {};
42	    virtual int SendMessage(const STG_MSG & msg, uint32_t ip) const = 0;
43	};
44	//-----------------------------------------------------------------------------
45	#endif

Перший - пустий деструктор плагіна (звісно, не зовсім пустий - там викликаються деструктори членів класа). Другий - пустий дестркутор базового класа. Хм...
Перший пішов:
(gdb) cont
Continuing.
          main.cpp > 17:24:26 > Unloading module './modules/mod_auth_ao.so'

Breakpoint 1, PLUGIN_RUNNER::Unload (this=0x80f0408) at plugin_runner.cpp:132
132	    if (dlclose(libHandler))

Так, а тепер хто де?
(gdb) list *0x28372658
No source file for address 0x28372658.

Отак-от. Шматок коду деструктора базового класу пішов у нас із попереднім плагіном.
А тепер зупинимось на хвильку і задумаємось: "Чому, хай йому грець?!"
Для того щоб отримати відповідь на це запитання мені вистачило 30 хвилин. Простий логічний аналіз.
1. Другий плагін перед смертю викоикає 0x28372658
2. Перший плагін перед смертю теж викликає 0x28372658
3. 0x28372658 - деструктор базового класу
4. Перший плагін забирає його із собою в могилу
5.
-bash-2.05b# nm mod_auth_ao.so  | grep BASE_PLUGIN
00015140 W _ZN11BASE_PLUGINC2Ev
0001ce54 W _ZN11BASE_PLUGIND0Ev
0001ce94 W _ZN11BASE_PLUGIND1Ev
00014e30 W _ZN11BASE_PLUGIND2Ev
0002246c V _ZTI11BASE_PLUGIN
00020c38 V _ZTS11BASE_PLUGIN
00022420 V _ZTV11BASE_PLUGIN

6. У другого плагіна є свій власний екземпляр цього коду
7. Він не завантажується із другим плагіном
8. Щось йому заважає
9. Код із першого плагіну? Він що, у глобальній області видимості?!
plugin_runner.cpp:91
libHandler = dlopen(pluginFileName.c_str(), RTLD_NOW | RTLD_GLOBAL);

Ага. Прибираємо RTLD_GLOBAL, компілюємо, запускаємо...
Круто. Все працює!

PS: а хто знає, чому деструктори ідуть у декількох екземплярах?
0001ce54 W _ZN11BASE_PLUGIND0Ev
0001ce94 W _ZN11BASE_PLUGIND1Ev
00014e30 W _ZN11BASE_PLUGIND2Ev

- аж три штуки!
При чому objdump -S показує ідентичний код...

Profile

madf: (Default)
madf

April 2018

S M T W T F S
1234567
891011121314
15161718192021
22232425262728
2930     

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Jun. 7th, 2026 10:30 am
Powered by Dreamwidth Studios