Налагодження пошкодження пам'яті


23

По-перше, я усвідомлюю, що це не ідеальне питання стилю Q&A з абсолютною відповіддю, але я не можу придумати жодних формулювань, щоб зробити це краще. Я не думаю, що для цього немає абсолютного рішення, і це одна з причин, чому я публікую його тут, а не Stack Overflow.

Протягом останнього місяця я переписував досить старий фрагмент коду сервера (mmorpg), щоб бути більш сучасним та легшим для розширення / мод. Я почав з мережевої частини та впровадив сторонні бібліотеки (libevent), щоб обробляти речі для мене. З урахуванням всіх рефакторингу та змін коду я десь ввів корупцію пам'яті, і я намагаюся з'ясувати, де це відбувається.

Я не можу, здається, надійно відтворити його на моєму середовищі розробки / тестування, навіть коли впроваджую примітивні боти для імітації деякого навантаження, я більше не отримую збоїв (я виправив проблему, яка викликала деякі проблеми).

Я намагався поки що:

Valgrinding чорт з цього - Ніяких недійсних записів, поки річ не зламається (що може зайняти 1+ день у виробництві .. або всього лише годину), що нас справді бентежить, напевно, в якийсь момент він отримає доступ до недійсної пам'яті і не перезапише речі шанс? (Чи є спосіб "поширити" діапазон адрес?)

Засоби аналізу коду, а саме покриття та cppcheck. Поки вони вказували на деякі .. гнучкість та крайні випадки в кодексі, нічого серйозного не було.

Записуючи процес, поки він не збоїть з gdb (через unodb), а потім пропрацюю назад. Це / звучить / начебто воно повинно бути виконаним, але я або закінчую збої gdb за допомогою функції автоматичного завершення, або опиняюсь у якійсь внутрішній структурі, де я втрачаюсь, оскільки занадто багато можливих гілок (одна корупція спричиняє іншу і так на). Я думаю, було б добре, якби я міг побачити, що вказівник спочатку належить / де він був виділений, що усуне більшість питань розгалуження. Я не можу запускати valgrind з unodb, і я нормальний запис gdb неприпустимо повільний (якщо це навіть працює в поєднанні з valgrind).

Перегляд коду! Сам (ретельно) і маючи друзів переглядаю мій код, хоча я сумніваюся, що це було досить ретельно. Я думав над тим, щоб, можливо, найняти розробника, щоб зробити якийсь перегляд коду / налагодження зі мною, але я не можу дозволити собі вкласти занадто багато грошей, і я не знаю, де шукати когось, хто був би готовий працювати за мало- без грошей, якщо він не знайде проблему або когось не кваліфікує.

Я також повинен зазначити: я зазвичай отримую послідовний зворотній зв'язок. Є кілька місць, де трапляються збої, в основному пов’язані з тим, що клас сокетів якось пошкоджується. Будь це недійсний вказівник, який вказує на те, що не є сокетом, або сам клас сокетів стає перезаписаним (частково?) Безглуздим. Хоча я підозрюю, що там найбільше виходить з ладу, оскільки це одна з найбільш використовуваних частин, тому це перша пошкоджена пам'ять, яка звикає.

В цілому це питання займало мене майже 2 місяці (і вимикається, більше хобі-проекту), і це насправді засмучує мене до того, коли я стаю бурхливою IRL і думаю про те, щоб просто відмовитись. Я просто не можу думати, що ще я повинен зробити, щоб знайти проблему.

Чи є корисні прийоми, які я пропустив? Як ти з цим справляється? (Це може бути не так часто, оскільки інформації про це не так багато. Або я просто сліпий?)

Редагувати:

Деякі технічні характеристики, якщо це має значення:

Використання c ++ (11) через gcc 4.7 (версія надається debian wheezy)

База даних коду становить близько 150 000 рядків

Редагувати у відповідь на повідомлення david.pfx: (вибачте за повільну відповідь)

Чи ведете ви ретельний облік аварій, щоб шукати шаблони?

Так, у мене все ще лежать звалища останніх аварій

Чи справді кілька місць подібні? Яким чином?

Добре, що в останній версії (вони, здається, змінюються щоразу, коли я додаю / видалюю код або змінюю пов'язані структури), він завжди потрапляє у метод таймера елементів. В основному предмет має певний час, після якого він закінчується, і він надсилає оновлену інформацію клієнту. Недійсний покажчик сокета був би в (все ще діє, наскільки я можу сказати) клас гравців, в основному пов'язаний з цим. Я також відчуваю безліч збоїв у фазі очищення, після нормального відключення, коли це руйнує всі статичні класи, які не були явно знищені ( __run_exit_handlersу backtrace). В основному це std::mapодин клас, здогадуючись, що це лише перше, що з'являється.

Як виглядають корумповані дані? Нулі? Ascii? Шаблони?

Я ще не знайшов жодної структури, мені здається дещо випадковою. Це важко сказати, оскільки я не знаю, звідки почалася корупція.

Це пов’язано з купою?

Це повністю пов'язано з купою (я включив захист стека gcc, і це нічого не застало).

Чи корупція трапляється після free()?

Вам доведеться трохи детальніше розібратися над цим. Ви маєте на увазі наявність покажчиків уже вільних об'єктів, що лежать навколо? Я встановлюю кожне посилання на null, коли об’єкт знищується, тому, якщо я десь щось не пропустив, ні. Це повинно з'явитися у вальгринд, хоча цього не було.

Чи є щось відмінне в мережевому трафіку (розмір буфера, цикл відновлення)?

Мережевий трафік складається з необроблених даних. Отже, масиви char, (u) intX_t або упаковані (для видалення padding) структури для складніших речей, кожен пакет має заголовок, що складається з ідентифікатора та самого розміру пакета, який перевіряється на очікуваний розмір. Вони становлять близько 10-60 байт, найбільший (внутрішній пакет "завантаження", запускається один раз при запуску), розміром якого є декілька Мб.

Багато і багато тверджень про виробництво. Збій рано і передбачувано до того, як шкода пошириться.

У мене колись стався збій, пов’язаний з std::mapкорупцією, кожен суб'єкт господарювання має карту його "перегляду", кожна організація, яка може його бачити, і навпаки - в цьому. Я додав 200-байтний буфер спереду і після, заповнив його 0x33 і перевірив перед кожним доступом. Корупція просто магічно зникла, я, мабуть, пересунув щось, що зробило її корумпованою чимось іншим.

Стратегічний журнал, щоб ви точно знали, що відбувалося напередодні. Додайте до журналу, коли ви наближаєтесь до відповіді.

Це працює .. в розширеному сенсі.

Ви можете у відчаї зберегти стан та автозавантажити? Я можу придумати кілька програм виробничого програмного забезпечення, які це роблять.

Я дещо так роблю. Програмне забезпечення складається з основного "кеш-процесу" та деяких інших робочих, які отримують доступ до кешу, щоб отримати та зберегти речі. Тож за аварію я не втрачаю особливого прогресу, він все ще відключає всіх користувачів і так далі, це точно не рішення.

Паралельність: нарізка на нитку, умови гонки тощо

Існує тема mysql для запитів "асинхронізації", але це все не зачіпається, і лише ділиться інформацією класу бази даних за допомогою функцій із усім блокуванням.

Переривання

Існує таймер переривання, щоб запобігти його запису, який просто перериває, якщо він не завершив цикл протягом 30 секунд, але цей код повинен бути безпечним:

if (!tics) {
    abort();
} else
    tics = 0;

тики, volatile int tics = 0;які збільшуються щоразу, коли цикл завершується. Старий код теж.

події / зворотні виклики / винятки: непредсказуемо пошкоджує стан або стек

Використовується безліч зворотних дзвінків (асинхронний введення / виведення мережі, таймери), але вони нічого поганого не повинні робити.

Незвичайні дані: незвичні вхідні дані / терміни / стан

У мене було кілька крайніх випадків, пов’язаних із цим. Відключення сокета, поки пакети ще обробляються, призводило до доступу до nullptr і подібного, але це було легко помітити досі, оскільки кожна посилання очищається відразу після того, як повідомити самому класу, що це зроблено. (Сам руйнування обробляється циклом, що видаляє всі знищені об'єкти кожного циклу)

Залежність від асинхронного зовнішнього процесу.

Хочете допрацювати? Це дещо так, згаданий вище процес кешування. Єдине, що я міг собі уявити вгорі голови, - це не закінчити досить швидко та використовувати дані сміття, але це не так, оскільки для цього також використовується мережа. Та ж модель пакету.


7
На жаль, це привіт звичайний у нетривіальних додатках C ++. Якщо ви використовуєте керування джерелами, тестування різних наборів змін для звуження того, яка зміна коду спричинила проблему, може допомогти, але, можливо, це неможливо в цьому випадку.
Теластин

Так, це справді не можливо в моєму випадку. Я в основному перейшов від роботи до повністю і зовсім зламаного протягом 2 місяців, а потім до стадії налагодження, де у мене є дещо робочий код. Стара система насправді не дозволила мені реалізувати свій новий гнучкий мережевий код, не порушуючи все.
Робін

2
У цей момент вам, можливо, доведеться спробувати ізолювати кожну деталь. Візьміть кожен клас / підмножину рішення, зробіть макет навколо нього, щоб він міг функціонувати, і випробовуйте живий пекло з нього, поки не знайдете розділ, який не вдається.
квітня

почніть з коментування частин кодів, поки у вас більше не виникне збій.
cpp81

1
Окрім Valgrind, Coverity та cppcheck, вам слід додати Asan та UBsan до режиму тестування. Якщо ваш код - corss-platofrm, додайте також Microsoft Enterprise Analysis ( /analyze) та Apple Malloc та Scribble. Ви також повинні використовувати якомога більше компіляторів, використовуючи якомога більше стандартів, оскільки попередження компілятора є діагностикою, і вони з часом стають кращими. Срібної кулі немає, і один розмір підходить не всім. Чим більше інструментів та компіляторів ви використовуєте, тим повніше охоплення, оскільки кожен інструмент має свої сильні та слабкі сторони.

Відповіді:


21

Це складна проблема, але я підозрюю, що в аваріях, які ви вже бачили, є набагато більше підказок.

  • Чи ведете ви ретельний облік аварій, щоб шукати шаблони?
  • Чи справді кілька місць подібні? Яким чином?
  • Як виглядають корумповані дані? Нулі? Ascii? Шаблони?
  • Чи задіяні якісь багаторізки? Чи може це бути умовою гонки?
  • Це пов’язано з купою? Чи відбувається корупція після безкоштовного ()?
  • Це пов'язано зі стеком? Чи зіпсується стек?
  • Чи можлива звисаюча посилання? Значення даних, яке загадково змінилося?
  • Чи є щось відмінне в мережевому трафіку (розмір буфера, цикл відновлення)?

Речі, які ми використовували в подібних ситуаціях.

  • Багато і багато тверджень про виробництво. Збій рано і передбачувано до того, як шкода пошириться.
  • Багато і багато охоронців. Додаткові елементи даних перед і після локальних змінних, об'єктів і малок () встановлюються на значення, а потім перевіряються часто.
  • Стратегічний журнал, щоб ви точно знали, що відбувалося напередодні. Додайте до журналу, коли ви наближаєтесь до відповіді.

Ви можете у відчаї зберегти стан та автозавантажити? Я можу придумати кілька програм виробничого програмного забезпечення, які це роблять.

Не соромтеся додавати деталі, якщо ми взагалі можемо допомогти.


Чи можу я просто додати, що такі серйозно невизначені помилки, як це, є не всіма поширеними, і не так багато речей, які можуть (як правило) їх викликати. Вони включають:

  • Паралельність: нарізка на нитку, умови гонки тощо
  • Перебої / події / зворотні виклики / винятки: непредсказуемо пошкоджує стан або стек
  • Незвичайні дані: незвичайні вхідні дані / терміни / стан
  • Залежність від асинхронного зовнішнього процесу.

Це частини коду, на яких слід зосередити увагу.


+1 Усі хороші пропозиції, особливо твердження, охорону та ведення журналів.
andy256

Я відредагував додаткову інформацію у своєму запитанні як відповідь на вашу відповідь. Це насправді змусило мене думати про збої при вимиканні, на які я ще не дивився широко, тому я зараз нагадаю.
Робін

5

Використовуйте налагоджувальну версію malloc / free. Загорніть їх і, якщо потрібно, напишіть своє. Дуже весело!

Версія, яку я використовую, додає байти охоронців до та після кожного розподілу та підтримує "виділений" список, на який безкоштовні перевірки звільняють шматки проти. Це вловлює більшість буферних перевиконань та декілька помилок або безвідмовної помилки.

Одне з найбільш підступних джерел корупції продовжує використовувати шматок після його звільнення. Free повинен заповнити звільнену пам'ять відомим шаблоном (традиційно 0xDEADBEEF) Це допомагає, якщо виділені структури включають елемент "магічне число", і вільно включати перевірки на відповідне магічне число перед використанням структури.


1
Valgrind, однак, повинен спіймати подвійне безкоштовно / використання даних free'd, чи не варто?
Робін

Написання подібних перевантажень для нового / видалення допомогло мені знайти численні проблеми з корупцією пам’яті. Особливо байти охоронців, які перевіряються при видаленні і викликають програму, викликану точкою розриву, яка автоматично перекидає мене на відладчик.
Емілі Л.

3

Якщо перефразовувати те, що ви сказали у своєму запитанні, то дати остаточну відповідь неможливо. Найкраще, що ми можемо зробити, - це запропонувати речі, які потрібно шукати, а також інструменти та методи.

Деякі пропозиції будуть здаватися наївними, інші можуть виглядати більш застосовними, але, сподіваємось, це викликає думку, яку ви можете продовжити. Треба сказати, що відповідь від david.pfx має надійні поради та пропозиції.

З симптомів

  • для мене це звучить як перевищення буфера.

  • пов'язана проблема - використання недійсних даних сокета в якості підпису або ключа тощо.

  • чи можливо, ви використовуєте глобальну змінну десь, або маєте глобальну та локальну з тим самим іменем, або якимось чином дані одного гравця заважають іншим?

Як і у багатьох помилок, ви, мабуть, десь робите недійсне припущення. Або, можливо, більше одного. Кілька взаємодіючих помилок важко виявити.

  • Чи кожна змінна має опис? І чи можете ви визначити твердження про дійсність?
    Якщо їх не додати, перегляньте код, щоб переконатися, що кожна змінна використовується правильно. Додайте це твердження там, де це має сенс.

  • Пропозиція додати твердження про лоти є хорошою: перше місце, яке потрібно поставити, - це на кожній точці введення функції. Обґрунтуйте аргументи та будь-яку відповідну глобальну державу.

  • Я використовую безліч журналів для налагодження довготривалих / асинхронних / кодів у режимі реального часу.
    Знову вставляйте запис журналу на кожен виклик функції.
    Якщо файлів журналу стає занадто великим, функції журналу можуть обгортати / перемикати файли / тощо
    . Найбільш корисно, якщо повідомлення журналу відступають з глибиною виклику функції.
    Файл журналу може показувати, як поширюється помилка. Корисно, коли один фрагмент коду робить щось не зовсім правильне, що діє як бомба із затримкою дії.

У багатьох людей є власний домашній код реєстрації. У мене десь є стара система журналів макросів C і, можливо, версія C ++ ...


3

Все, що було сказано в інших відповідях, дуже актуально. Одне важливе, що частково згадує ddyer, - це те, що обгортання malloc / free має переваги. Він згадує декілька, але я хотів би додати до цього дуже важливий інструмент налагодження: ви можете ввідати кожен malloc / free у зовнішній файл разом з кількома рядками callstack (або повним стовпчиком callstack, якщо вам все одно). Якщо ви будете обережні, ви можете легко зробити це досить швидко і використовувати його у виробництві, якщо воно дійшло до нього.

З того, що ви описуєте, моя особиста здогадка полягає в тому, що ви, можливо, зберігаєте посилання на вказівник десь для звільнення пам’яті, і в кінцевому підсумку ви можете звільнити вказівник, який більше вам не належить, або записати на нього. Якщо ви можете зробити висновок про діапазон розмірів для моніторингу за вищенаведеною технікою, ви зможете значно звузити лісозаготівлю. В іншому випадку, як тільки ви знайдете, яка пам’ять була пошкоджена, ви можете легко зрозуміти шаблони malloc / free, що призвели до нього з журналів.

Важлива примітка полягає в тому, що, як ви вже згадували, зміна компонування пам'яті може приховати проблему. Таким чином, дуже важливо, щоб ваш журнал не мав розподілу (якщо ви можете!) Або якомога менше. Це допоможе відтворюваності, якщо це стосується пам'яті. Це також допоможе, якщо це буде якомога швидше, якщо питання пов'язане з багатопотоковою резьбою.

Також важливо відстежувати асигнування з сторонніх бібліотек, щоб ви могли їх також правильно записувати. Ніколи не знаєш, звідки це може прийти.

В якості останньої альтернативи ви також можете створити спеціальний алокатор, де ви виділите щонайменше 2 сторінки для кожного розподілу та скасуйте їх, коли ви звільнені (вирівняйте алокацію до межі сторінки, виділіть сторінку раніше та позначте її як недоступну або вирівняйте виділити в кінці сторінки та виділити сторінку після, а позначити як недоступну). Не забудьте хоча б деякий час використовувати ці адреси віртуальної пам'яті для нових виділень. Це означає, що вам потрібно буде самостійно керувати віртуальною пам'яттю (зарезервувати її та використовувати її як завгодно). Зауважте, що це погіршить вашу ефективність і може призвести до використання значної кількості віртуальної пам’яті залежно від того, скільки виділень ви їх годуєте. Пом'якшити це допоможе, якщо ви можете працювати в 64-бітовій та / або зменшити діапазон виділень, які цього потребують (виходячи з розміру). Валгрінд може дуже добре вже зробити це, але це може бути занадто повільним, щоб ви могли вирішити цю проблему. Якщо це зробити лише для декількох розмірів або об'єктів (якщо ви знаєте, який, ви можете використовувати спеціальний розподільник лише для цих об'єктів), це забезпечить мінімальний вплив на продуктивність.


0

Спробуйте встановити точку спостереження за адресою пам'яті, за якою вона завершується. GDB перерветься за інструкцією, яка викликала недійсну пам'ять. Потім із зворотним слідом ви побачите ваш код, який спричиняє корупцію. Це може бути не джерелом корупції, але повторення точки спостереження за кожною корупцією може призвести до джерела проблеми.

До речі, оскільки питання позначено тегом C ++, подумайте про використання спільних покажчиків, які піклуються про право власності, підтримуючи кількість відліку та безпечно видаляйте пам’ять після того, як покажчик вийде за межі області. Але використовуйте їх обережно, оскільки вони можуть спричинити тупик при рідкісному використанні кругової залежності.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.