Pull to refresh

Использование GDB на примере MaNGOS

Reading time 8 min
Views 5.3K
Представьте на мгновение, что вы капитан и механик огромного космического корабля, на котором одновременно находятся множество пассажиров. Но это не идеальный корабль, а полуразвалившаяся посудина. Да многое работает, но многое еще предстоит починить. Возникает вопрос, как? Вот был бы такой инструмент, который позволил разобрать наше судно до мельчайшего болтика, понять как он устроен, что происходит внутри него при нажатии той или иной кнопки, прочувствовать сущность, его душу. Мало того, было бы идеальным еще иметь возможность останавливать время в момент починки неполадки, что бы все разложить по полочкам. Ведь это не так легко, понять, а что же происходит внутри!
Раньше я боялся залазить внутрь такого монстра как mangos, и пользовался отладчиком только при подготовке задания по информатике в институте. Но оказалось, что все намного проще и нет ничего страшного, хоть там и много кода. Я попробую описать, как пользуясь GDB залезть внутрь популярного эмулятора одной MMORPG и посмотреть на его работу изнутри. Главное, не бояться экспериментировать, в пределах разумного. Те, кто знает что такое GDB не найдут тут ничего нового для себя. На эту мысль меня натолкнула игра всеми «любимого» класса как паладин. Собственно, скриншот из игры:



Что же мы видим? В описании спелла 152-172 урона, а на практике 232. Откуда взялся лишний урон? Понять это мне помог GNU дебагер.

Сборка сервера


Для начала, соберем сам сервер с помощь cmake. Собственно, все стандартно:
mkdir build
cd build
cmake ..
make -j4
make install

Я специально не собирал сервер в отладочном режиме, что бы акцентировать внимание на невозможности отладки без установки переменой DEBUG=1
В сборке вылезли первые проблемы. Cmake успешно подготовил мейкфайлы, но при компиляции полезли ошибки, а именно, не собирался ADAPTIVE Communication Environment (ACE) и TBB. Прогнав предварительно Autotools, эти библиотеки собрались, но это путь гентушника. Зачем собирать то, что уже собрано и лежит в репозитории? В итоге имеем добавляем переменные ACE_USE_EXTERNAL=1 TBB_USE_EXTERNAL=1
Отлично, в консоли cmake все схватил:
-- Found ACE library: /usr/lib/libACE.so
-- Found ACE headers: /usr/include
-- Found Intel TBB
-- Using mysql-config: /usr/bin/mysql_config
-- Found MySQL library: /usr/lib/libmysqlclient_r.so
-- Found MySQL headers: /usr/include/mysql
-- Found OpenSSL library: /usr/lib/libssl.so
-- Found OpenSSL headers: /usr/include/openssl
-- Found ZLIB: /usr/include

Я указал компоновщику использовать библиотеки с ОС, а не собирать их. Пока ходил на кухню за чаем, все собралось. Запускаем сервер под отладчиком:
gdb ./mangos-world
GNU gdb (GDB) 7.0.1-debian
Copyright (C) 2009 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/gabriel/projects/cpp/mangos-server/bin/mangos-world...(no debugging symbols found)...done.
(gdb) r

Прошло несколько секунд, спам в консоль о загрузки всего мира и надпись что сервер стартовал:
WORLD: World initialized
SERVER STARTUP TIME: 0 minutes 2 seconds
[New Thread 0x7fffea326700 (LWP 8911)]
[0 ms] SQL: UPDATE realmlist SET color = 0, population = 0, realmbuilds = '5875 6005 '  WHERE id = '1'
[New Thread 0x7fffe9a25700 (LWP 8912)]

engine: Max allowed socket connections 1024
[New Thread 0x7fffe9224700 (LWP 8913)]
Network Thread Starting
[New Thread 0x7fffe8a23700 (LWP 8914)]
Network Thread Starting

Что такое r? Это run, запуск то-есть. Жмем Ctrl+C что бы вернуться к консоли отладчика, а не сервера и прописываю первую точку останова:
(gdb) b Unit::DealDamage
Can't find member of namespace, class, struct, or union named "Unit::DealDamage"
Hint: try 'Unit::DealDamage<TAB> or 'Unit::DealDamage<ESC-?>
(Note leading single quote.)
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (Unit::DealDamage) pending.
(gdb) c
Continuing.

b — breakpoint, указание отладчику где сделать остановку, с — continue, продолжить
Я сделал точку останова на ф-ции Unit::DealDamage, судя по названию именно в ней применяются эффекты от заклинаний, но это чисто предположение, и оно оказалось верным. Отладчик кстати поддерживает автодополнение функций, кнопка TAB помогла) Меня смутило предупреждение Make breakpoint pending on future shared library load? (y or [n]), базовые знания английского помогли понять что отладчик предлагает сделать точку останова в зависимости от загрузки общей библиотеки (сложно переводить, но юниксоиду смысл и так ясен). Далее вхожу в игру, создаю паладина, учу экзорцизм и телепортируюсь убивать нежить.



Тут интересное, точка останова не сработала! Это потому что я собрал сервер в релизе. Добавляем -DDEBUG=1
Собирался наш мир чуть дольше, с мусором в консоли про несоответствие типов, но в конце концов собрался. Опять же, запускаем отладчик и ставим точку остановки. Заметьте, теперь он полностью прогрузил сервер (нет фразы no debugging symbols found). И нет ругани:
(gdb) b Unit::DealDamage
Breakpoint 1 at 0xb3d91a: file /home/gabriel/projects/cpp/mangos/src/game/Unit.cpp, line 479.

Ковыряние внутри сервера


Идем дальше.
Жмем экзорцизм и… игра повисла, а в консоли имеем:
Breakpoint 1, Unit::DealDamage (this=0x133c000, pVictim=0x7fffe80f6080, damage=599, cleanDamage=0x7fffeaa4b680, damagetype=SPELL_DIRECT_DAMAGE, damageSchoolMask=SPELL_SCHOOL_MASK_HOLY, 
    spellProto=0x7fffefdb3010, durabilityLoss=true) at /home/gabriel/projects/cpp/mangos/src/game/Unit.cpp:479
479         if(pVictim != this)

Урон уже увеличенный, 599, хотя в описании меньше. Смотрим трассировку:
(gdb) bt
#0  Unit::DealDamage (this=0x133c000, pVictim=0x7fffe80f6080, damage=599, cleanDamage=0x7fffeaa4b680, damagetype=SPELL_DIRECT_DAMAGE, damageSchoolMask=SPELL_SCHOOL_MASK_HOLY, 
    spellProto=0x7fffefdb3010, durabilityLoss=true) at /home/gabriel/projects/cpp/mangos/src/game/Unit.cpp:479
#1  0x0000000000b410bd in Unit::DealSpellDamage (this=0x133c000, damageInfo=0x7fffeaa4b6f0, durabilityLoss=true) at /home/gabriel/projects/cpp/mangos/src/game/Unit.cpp:1331
#2  0x0000000000a21f66 in Spell::DoAllEffectOnTarget (this=0x7fffe7ee3d80, target=0x7fffe7f2bf80) at /home/gabriel/projects/cpp/mangos/src/game/Spell.cpp:1006
#3  0x0000000000a28912 in Spell::handle_immediate (this=0x7fffe7ee3d80) at /home/gabriel/projects/cpp/mangos/src/game/Spell.cpp:2804
#4  0x0000000000a286b2 in Spell::cast (this=0x7fffe7ee3d80, skipCheck=false) at /home/gabriel/projects/cpp/mangos/src/game/Spell.cpp:2769
#5  0x0000000000a29424 in Spell::update (this=0x7fffe7ee3d80, difftime=100) at /home/gabriel/projects/cpp/mangos/src/game/Spell.cpp:2982
#6  0x0000000000a34307 in SpellEvent::Execute (this=0x7fffe7f3bee8, e_time=5167, p_time=100) at /home/gabriel/projects/cpp/mangos/src/game/Spell.cpp:5992
#7  0x0000000000c77b8b in EventProcessor::Update (this=0x133c118, p_time=100) at /home/gabriel/projects/cpp/mangos/src/framework/Utilities/EventProcessor.cpp:34
#8  0x0000000000b3cd04 in Unit::Update (this=0x133c000, update_diff=100, p_time=100) at /home/gabriel/projects/cpp/mangos/src/game/Unit.cpp:300
#9  0x000000000096f66e in Player::Update (this=0x133c000, update_diff=100, p_time=100) at /home/gabriel/projects/cpp/mangos/src/game/Player.cpp:1124
#10 0x0000000000940bbb in WorldObject::UpdateHelper::Update (this=0x7fffeaa4bce0, time_diff=100) at /home/gabriel/projects/cpp/mangos/src/game/Object.h:404
#11 0x0000000000ae873c in Map::Update (this=0x2d14000, t_diff=@0x7fffe9a17e40) at /home/gabriel/projects/cpp/mangos/src/game/Map.cpp:446
#12 0x0000000000c5a8a6 in MapUpdateRequest::call (this=0x7fffe9a17e20) at /home/gabriel/projects/cpp/mangos/src/game/MapUpdater.cpp:61
#13 0x0000000000c76911 in DelayExecutor::svc (this=0x7fffeb8258d0) at /home/gabriel/projects/cpp/mangos/src/shared/DelayExecutor.cpp:57
#14 0x00007ffff7b77847 in ACE_Task_Base::svc_run(void*) () from /usr/lib/libACE-5.7.7.so
#15 0x00007ffff7b78bc1 in ACE_Thread_Adapter::invoke() () from /usr/lib/libACE-5.7.7.so
#16 0x00007ffff5e138ca in start_thread () from /lib/libpthread.so.0
#17 0x00007ffff5b7a86d in clone () from /lib/libc.so.6
#18 0x0000000000000000 in ?? ()

bt, как вы уже наверное догадались, это backtrace. Поизучав чуть вызовы ф-ций, я понял что необходимо следить за изменением переменной damageInfo в ф-ции Spell::DoAllEffectOnTarget. Ставим на этой ф-ции брейкпоинт, и удаляем старый, а так же лишние брейки для итемов и ГО:
(gdb) b Spell::DoAllEffectOnTarget
Breakpoint 2 at 0xa22e57: file /home/gabriel/projects/cpp/mangos/src/game/Spell.cpp, line 1257.
Breakpoint 3 at 0xa22cef: file /home/gabriel/projects/cpp/mangos/src/game/Spell.cpp, line 1230.
Breakpoint 4 at 0xa21668: file /home/gabriel/projects/cpp/mangos/src/game/Spell.cpp, line 860.
warning: Multiple breakpoints were set.
Use the "delete" command to delete unwanted breakpoints.
(gdb) d 1
(gdb) d 2
(gdb) d 3

Новое зависание игры, мы внутри ф-ции, прогоняем ее по строчно с вхождением внутрь:
Breakpoint 4, Spell::DoAllEffectOnTarget (this=0x7fffe7ecfd80, target=0x7fffe7ef7f80) at /home/gabriel/projects/cpp/mangos/src/game/Spell.cpp:860
860         if (m_spellInfo->Id <= 0 || m_spellInfo->Id > MAX_SPELL_ID ||  m_spellInfo->Id == 32 || m_spellInfo->Id == 80)
(gdb) list
855         m_UniqueItemInfo.push_back(target);
856     }
857
858     void Spell::DoAllEffectOnTarget(TargetInfo *target)
859     {
860         if (m_spellInfo->Id <= 0 || m_spellInfo->Id > MAX_SPELL_ID ||  m_spellInfo->Id == 32 || m_spellInfo->Id == 80)
861             return;
862
863         if (!target || target == (TargetInfo*)0x10 || target->processed)
864             return;
(gdb) n 10
884         unitTarget = unit;
...
985                 caster->CalculateSpellDamage(&damageInfo, m_damage, m_spellInfo, m_attackType);
(gdb) print m_damage
$4 = 535
(gdb) s
Unit::CalculateSpellDamage (this=0x133c000, damageInfo=0x7fffeb24c6f0, damage=535, spellInfo=0x7fffefdb3010, attackType=BASE_ATTACK)
    at /home/gabriel/projects/cpp/mangos/src/game/Unit.cpp:1227
1227        SpellSchoolMask damageSchoolMask = GetSchoolMask(damageInfo->school);
...
1265                damage = SpellDamageBonusDone(pVictim, spellInfo, damage, SPELL_DIRECT_DAMAGE);
(gdb) s
Unit::SpellDamageBonusDone (this=0x133c000, pVictim=0x7fffe80f6080, spellProto=0x7fffefdb3010, pdamage=535, damagetype=SPELL_DIRECT_DAMAGE, stack=1)
    at /home/gabriel/projects/cpp/mangos/src/game/Unit.cpp:5345
5345        if(!spellProto || !pVictim || damagetype==DIRECT_DAMAGE )
...
5425        DoneTotal = SpellBonusWithCoeffs(spellProto, DoneTotal, DoneAdvertisedBenefit, 0, damagetype, true);
(gdb) s
Unit::SpellBonusWithCoeffs (this=0x133c000, spellProto=0x7fffefdb3010, total=0, benefit=0, ap_benefit=0, damagetype=SPELL_DIRECT_DAMAGE, donePart=true)
    at /home/gabriel/projects/cpp/mangos/src/game/Unit.cpp:5302
5302        if (GetTypeId()==TYPEID_UNIT && !((Creature*)this)->IsPet())
(gdb) n
5305        else if (SpellBonusEntry const* bonus = sSpellMgr.GetSpellBonusData(spellProto->Id))
(gdb) n
5307            coeff = damagetype == DOT ? bonus->dot_damage : bonus->direct_damage;
(gdb) n
5310            if (donePart && (bonus->ap_bonus || bonus->ap_dot_bonus))
(gdb) n
5312                float ap_bonus = damagetype == DOT ? bonus->ap_dot_bonus : bonus->ap_bonus;
(gdb) n
5314                total += int32(ap_bonus * (GetTotalAttackPowerValue(IsSpellRequiresRangedAP(spellProto) ? RANGED_ATTACK : BASE_ATTACK) + ap_benefit));
(gdb) print ap_bonus
$5 = 0.150000006
(gdb) n
5321        if (benefit)
(gdb) n
5336        return total;
(gdb) print total
$6 = 68
(gdb) n
5337    };
(gdb) n
Unit::SpellDamageBonusDone (this=0x133c000, pVictim=0x7fffe80f6080, spellProto=0x7fffefdb3010, pdamage=535, damagetype=SPELL_DIRECT_DAMAGE, stack=1)
    at /home/gabriel/projects/cpp/mangos/src/game/Unit.cpp:5427
5427        float tmpDamage = (int32(pdamage) + DoneTotal * int32(stack)) * DoneTotalMod;
(gdb) n
5429        if(Player* modOwner = GetSpellModOwner())
(gdb) print tmpDamage
$7 = 603
(gdb) c

Что бы вас не утомлять, я упустил часть вывода консоли как я построчно следил за изменением урона. Опишу лишь вкратце команды:
n — next, выполнить следующую сточку кода без вхождения внутрь
s — step, то же самое, только зайти внутрь ф-ции
l — list, распечатать кусок кода
p — print, вывести переменную.
В итоге я узнал, что базовый урон был 535, как и положено в описании спелла, а дополнительный урон рассчитывается в 5314 строчке файла Unit.cpp и составляет 15% от АП. Почему так? Механика игры. В сумме получилось 603, что и вылетело потом в игре.
Скриншот с АП:



Вот и все, я надеюсь что вкратце и понятно описал основы работы с отладчиком под никсами на живом проекте.

Ссылки


Отладка с помощью GBD — детальное описание GDB, Ричард Столлман, Роланд Пеш, Стан Шебс и другие.
Mangos project — отличная площадка для изучения c++

UPD:
от mejedi:
«Меня смутило предупреждение Make breakpoint pending on future shared library load? (y or [n]), базовые знания английского помогли понять что отладчик предлагает сделать точку останова в зависимости от загрузки общей библиотеки»

Это сообщение означает, что gdb не нашел функцию, в которой хотели поставить брейкпоинт, и спрашивает: может отложить установку брейкпоинта? Если согласиться, то при загрузке каждой новой динамической библиотеки (shared library) gdb будет пытаться найти эту функцию там, и в случае успеха установит брейкпоинт.
Tags:
Hubs:
+27
Comments 8
Comments Comments 8

Articles