Програма виходить з ладу лише під час побудови випуску - як налагодити?


95

У мене тут проблема типу "кота Шредінгера" - моя програма (насправді тестовий пакет для моєї програми, але програма тим не менше) аварійно завершує роботу, але лише при побудові в режимі випуску і лише при запуску з командного рядка . За допомогою налагодження печерної людини (тобто неприємних повідомлень printf () всюди), я визначив метод тестування, де код збійний, хоча, на жаль, справжнє збій, здається, трапляється в якомусь деструкторі, оскільки останні повідомлення про трасування, які я бачу, знаходяться в інші деструктори, які працюють чисто.

Коли я намагаюся запустити цю програму всередині Visual Studio, вона не виходить з ладу. Те саме стосується запуску з WinDbg.exe. Збій відбувається лише під час запуску з командного рядка. Це відбувається під Windows Vista, до речі, і, на жаль, зараз у мене немає доступу до машини XP для тестування.

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

Редагувати: Проблему справді спричинив позамежний масив, про який я докладніше розповім у цій публікації . Дякуємо усім за допомогу у пошуку цієї проблеми!


Чи можете ви навести зразок цього методу випробування?
akalenuk

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

3
Більшість відповідей - це трохи більше, ніж здогадки. Існує декілька поширених методів аналізу збоїв збірок випусків без приєднання відладчика: stackoverflow.com/a/18513077/214777?stw=2
Себастьян,

Відповіді:


127

У 100% випадків, які я бачив або чув, коли програма C або C ++ працює нормально в налагоджувачі, але не працює, якщо працює за межами, причиною є написання в кінці локального масиву функції. (Налагоджувач кладе більше на стек, тому ви менш імовірно переписуєте щось важливе.)


31
Хтось дасть цьому чоловікові сигару! У моєму випадку я передавав StringBuilder, який не мав достатньо великої ємності для функції P / Invoke. Думаю, це наче хтось пише на вашому обличчі магічним маркером, коли ви спите: під відладчиком вони закінчують писати вам на лобі, тож ви цього не помічаєте, але без відладчика вони в кінцевому підсумку завдають вам удару ножем око ... щось подібне. Дякуємо за цю пораду!
Ніколас Пясецький

1
У моєму випадку це виявилося проблемою вирівнювання ARM-процесора за допомогою Obj-C.
Альмо,

1
Через 11 років, і це все ще звучить правдою ... не забудьте зарезервувати свої вектори.
дав

1
гаразд, то як тоді можна змінити поведінку режиму налагодження, щоб насправді можна було налагодити.
Пол Чайлдс,

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

55

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

int* p;
....
if (p == 0) { // do stuff }

У режимі налагодження код у if не виконується, але в режимі випуску p містить невизначене значення, яке навряд чи буде 0, тому код виконується часто, що спричиняє збій.

Я б перевірив ваш код на наявність неініціалізованих змінних. Це також може стосуватися вмісту масивів.


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

1
У режимі налагодження змінні зазвичай ініціалізуються до якоїсь константи, визначеної компілятором, яка може використовуватися при налагодженні, щоб вказати, в якому стані знаходиться змінна. Наприклад, популярні покажчики NULL або 0xDeadBeef.
Мартін Йорк,

Часи виконання налагодження зазвичай ініціалізують пам'ять до деякого ненульового значення, зокрема, щоб тести покажчика NULL змусили код діяти так, ніби вказівник ми не NULL. В іншому випадку у вас є код, який працює належним чином у режимі налагодження, який аварійно завершує режим випуску.
Michael Burr

1
Ні, змінні взагалі не ініціалізуються, і все одно UB "використовувати" їх, поки вони не будуть призначені. Однак основний вміст пам'яті часто попередньо заповнюється 0x0000000 або 0xDEADBEEF або іншими впізнаваними шаблонами.
Гонки легкості на орбіті

26

Досі жодна відповідь не намагалася дати серйозний огляд доступних методів налагодження програм випуску:

  1. Збірки Release та Debug поводяться по-різному з багатьох причин. Ось чудовий огляд. Кожна з цих відмінностей може спричинити помилку у збірці Release, яка не існує у збірці налагодження.

  2. Наявність налагоджувача також може змінити поведінку програми , як для випуску, так і для збірки налагодження. Дивіться цю відповідь. Коротше кажучи, принаймні налагоджувач Visual Studio автоматично використовує Debug Heap, коли приєднується до програми. Ви можете вимкнути купу налагодження за допомогою змінної середовища _NO_DEBUG_HEAP. Ви можете вказати це або у властивостях комп'ютера, або в налаштуваннях проекту у Visual Studio. Це може зробити збій відтворюваним із прикріпленим налагоджувачем.

    Детальніше про налагодження пошкодження купи тут.

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

  4. Ви можете вдосконалити код обробки винятків, і якщо це виробнича програма, вам слід:

    a. Встановіть власний обробник завершення за допомогоюstd::set_terminate

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

    У виробничій програмі вам може знадобитися надіслати звіт про помилку додому, в ідеалі, разом із невеликим дамом пам'яті, який дозволяє аналізувати проблему, як описано тут.

    b. Використовуйте структурований механізм обробки винятків Microsoft, який дозволяє вловлювати як апаратні, так і програмні винятки. Див. MSDN . Ви можете захищати частини коду за допомогою SEH і використовувати той самий підхід, що і в а) для налагодження проблеми. SEH надає більше інформації про виняток, який стався, який ви могли б використовувати під час надсилання звіту про помилку з робочої програми.


16

На що слід звернути увагу:

Перевищення масиву - налагоджувач Visual Studio вставляє відступ, який може зупинити збої.

Умови перегонів - чи є у вас задіяні кілька потоків, якщо так, перегонові умови багато хто з’являється лише тоді, коли додаток виконується безпосередньо.

Посилання - це випуск вашої збірки витягування правильних бібліотек.

Що спробувати:

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


1
Привіт, я проголосував за цю відповідь анонімно. Я хотів би зрозуміти, чому?
morechilli

12

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

windbg /I

Детальніше тут .

Що стосується причини, то це, швидше за все, неіціалізована змінна, як пропонують інші відповіді.


2
І не забувайте, що ви можете змусити компілятор генерувати файли PDB навіть для збірки випусків, хоча це не за замовчуванням.
Michael Burr,

Єдина реальна відповідь на питання насправді.
Себастьян,

10

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

char *end = static_cast<char*>(attr->data) + attr->dataSize;

Це помилка огороджувального стовпа (помилка, одинична), яку було виправлено:

char *end = static_cast<char*>(attr->data) + attr->dataSize - 1;

Дивне було те, що я зробив кілька викликів _CrtCheckMemory () навколо різних частин мого коду, і вони завжди повертали 1. Я зміг знайти джерело проблеми, розмістивши "return false;" дзвінки в тестовому випадку, а потім врешті-решт шляхом спроб і помилок визначають, де була помилка.

Дякую всім за ваші коментарі - сьогодні я дізнався багато нового про windbg.exe! :)


8
Сьогодні я налагоджував подібну проблему, і _CrtCheckMemory () завжди повертав 1. Але потім я зрозумів, чому: у режимі звільнення _CrtCheckMemory # визначається як ((int) 1).
Brian Morearty

7

Незважаючи на те, що ви створили свій exe як реліз, ви все одно можете створювати файли PDB (база даних програм), які дозволять стекувати трасування та виконувати обмежену кількість перевірок змінних. У налаштуваннях збірки є можливість створити файли PDB. Увімкніть це та повторно зв’яжіть. Потім спробуйте запустити спочатку з IDE, щоб перевірити, чи не сталася аварія. Якщо так, то чудово - ви готові поглянути на речі. Якщо ні, тоді під час запуску з командного рядка ви можете зробити одне з двох:

  1. Запустіть EXE і перед збоєм виконайте вкладання до процесу (меню Інструменти на Visual Studio).
  2. Після аварії виберіть опцію для запуску налагоджувача.

Коли вас попросять вказати на файли PDB, перегляньте, щоб знайти їх. Якщо PDB було поміщено в ту саму папку виводу, що і ваші EXE або DLL, вони, ймовірно, будуть автоматично підняті.

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

Примітка: Я передбачаю тут середовище Windows / Visual Studio.


3

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

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


3

Для того, щоб отримати дамп аварійного завершення, який ви можете проаналізувати:

  1. Створіть файли pdb для свого коду.
  2. Ви перебазуєте, щоб ваші exe та dll завантажувались за тією ж адресою.
  3. Увімкнути посмертний налагоджувач, такий як доктор Ватсон
  4. Перевірте адресу збоїв при збої, використовуючи такий інструмент, як пошук збоїв .

Вам також слід перевірити інструменти в Інструментах налагодження для Windows . Ви можете відстежувати додаток і бачити всі винятки з перших шансів, які були до винятків з другого шансу.

Сподіваюся, це допоможе ...


3

Чудовим способом налагодження такої помилки є увімкнення оптимізації для вашої збірки налагодження.


2

Одного разу у мене виникла проблема, коли додаток поводився подібно до вашого. Це виявилося неприємним переповненням буфера у sprintf. Природно, це спрацьовувало під час запуску з прикріпленим налагоджувачем. Що я зробив, це встановив фільтр необроблених винятків ( SetUnhandledExceptionFilter ), в якому я просто нескінченно блокував (використовуючи WaitForSingleObject на підробленому дескрипторі зі значенням часу очікування INFINITE).

Отже, ви можете щось на зразок:

довгий __stdcall MyFilter (EXCEPTION_POINTERS *)
{
    HANDLE hEvt = :: CreateEventW (0,1,0,0);
    якщо (hEvt)
    {
        якщо (WAIT_FAILED == :: WaitForSingleObject (hEvt, INFINITE))
        {
            // помилка журналу
        }
    }

}
// десь у вашому wmain / WinMain:
SetUnhandledExceptionFilter (MyFilter);

Потім я підключив налагоджувач після того, як помилка виявилася (програма gui перестала реагувати).

Тоді ви можете взяти дамп і працювати з ним пізніше:

.dump / ma path_to_dump_file

Або відлаштуйте це відразу. Найпростіший спосіб - відстежити, де контекст процесора був збережений механізмом обробки винятків під час виконання:

sd esp Діапазон 1003f

Команда буде шукати адресний простір стека для записів CONTEXT за умови тривалості пошуку. Зазвичай я використовую щось на зразок "l? 10000" . Зауважте, не використовуйте незвично великі числа як запис, який ви переслідуєте, як правило, поруч із кадром фільтру неручних винятків. 1003f - це комбінація прапорів (я вважаю, що вона відповідає CONTEXT_FULL), яка використовується для фіксації стану процесора. Ваш пошук буде виглядати приблизно так:

0: 000> sd esp l1000 1003f
0012c160 0001003f 00000000 00000000 00000000? ...............

Отримавши результати назад, використовуйте адресу в команді cxr:

.cxr 0012c160

Це перенесе вас до цього нового КОНТЕКСТУ, саме під час аварійного завершення роботи (ви отримаєте саме трасування стека на момент аварії вашого додатка). Додатково використовуйте:

.exr -1

щоб точно з’ясувати, який виняток стався.

Сподіваюся, це допоможе.


2

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


1

Що стосується ваших проблем з отриманням діагностичної інформації, чи пробували ви використовувати adplus.vbs як альтернативу WinDbg.exe? Для підключення до запущеного процесу використовуйте

adplus.vbs -crash -p <process_id>

Або щоб запустити програму в тому випадку, якщо збій відбувається швидко:

adplus.vbs -crash -sc your_app.exe

Повну інформацію про adplus.vbs можна знайти за адресою: http://support.microsoft.com/kb/286350


1

Ntdll.dll із налагодженим налагоджувачем

Мало відома різниця між запуском програми з IDE або WinDbg на відміну від її запуску з командного рядка / робочого столу полягає в тому, що при запуску з підключеним налагоджувачем (тобто IDE або WinDbg) ntdll.dll використовує іншу реалізацію купи, яка виконує незначну перевірку щодо виділення / звільнення пам'яті.

Ви можете прочитати деяку відповідну інформацію в несподіваній точці зупинки користувача в ntdll.dll . Одним із інструментів, який може допомогти вам визначити проблему, є PageHeap.exe .

Аналіз аварії

Ви не писали, що таке "крах", який ви відчуваєте. Як тільки програма аварійно завершує роботу та пропонує вам надіслати інформацію про помилку в корпорацію Майкрософт, ви зможете натиснути на технічну інформацію та перевірити принаймні код винятку, а з певними зусиллями ви навіть зможете провести аналіз після забою (див. Heisenbug : Програма WinApi виходить з ладу на деяких комп’ютерах) для отримання інструкцій)


1

У системі Vista SP1 насправді вбудований дуже приємний генератор дампа аварійного завершення роботи. На жаль, це не ввімкнено за замовчуванням!

Див. Цю статтю: http://msdn.microsoft.com/en-us/library/bb787181(VS.85).aspx

Перевага цього підходу полягає в тому, що на уражену систему не потрібно встановлювати додаткове програмне забезпечення. Хапай і рвай, малята!


1

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

Наприклад :

char a[8];
memset(&a[0], 0, 16);

: /*use array a doing some thing */

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

Але у звіті це може / може бути аварією.

Для мене копатись там, де пам’ять втрачена, занадто важко.

Використовуйте деякі інструменти, такі як Visual Leak Detector (windows) або valgrind (linux), які є більш розумним вибором.


1

Я бачив багато правильних відповідей. Однак немає жодного, що мені допомогло. У моєму випадку сталося неправильне використання інструкцій SSE з незрівнянтованою пам'яттю . Погляньте на свою математичну бібліотеку (якщо ви її використовуєте) і спробуйте відключити підтримку SIMD, перекомпілювати та відтворити збій.

Приклад:

Проект включає mathfu і використовує класи з STL vector: std :: vector <mathfu :: vec2> . Таке використання, ймовірно, спричинить збій під час побудови елемента mathfu :: vec2, оскільки розподілювач за замовчуванням STL не гарантує необхідне вирівнювання 16 байт. У цьому випадку, щоб довести ідею, можна визначити #define MATHFU_COMPILE_WITHOUT_SIMD_SUPPORT 1перед кожним включенням mathfu , перекомпілювати у конфігурації випуску та перевірити ще раз.

У Debug і RelWithDebInfo конфігурація працювала добре для мого проекту, але не в релізі один. Причиною такої поведінки є, мабуть, те, що налагоджувач обробляє запити на виділення / вивільнення та робить деяку бухгалтерію пам'яті для перевірки та перевірки доступу до пам'яті.

Я пережив ситуацію в середовищах Visual Studio 2015 та 2017.


0

Щось подібне трапилося зі мною одного разу з GCC. Це виявилося занадто агресивною оптимізацією, яка була включена лише при створенні остаточного випуску, а не в процесі розробки.

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

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


0

Я знайшов цю статтю корисною для вашого сценарію. Параметри компілятора ISTR були трохи застарілими. Огляньте параметри проекту Visual Studio, щоб побачити, як генерувати файли PDB для збірки випуску тощо.


0

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


0

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

Одним із методів принаймні звуження проблеми є використання MessageBox () для відображення швидких операторів із зазначенням того, до якої частини програми потрапив ваш код ("Запуск Foo ()", "Запуск Foo2 ()"); почніть розміщувати їх у верхній частині функцій в області вашого коду, яку ви підозрюєте (що ви робили в той час, коли він розбився?). Коли ви можете визначити, яка функція, змініть вікна повідомлень на блоки коду або навіть окремі рядки в цій функції, поки не звузите її до декількох рядків. Потім ви можете почати роздруковувати значення змінних, щоб побачити, в якому стані вони перебувають у момент збою.


Він вже спробував посипати printfs, тому скриньки з повідомленнями не перемогли; не принесуть нічого нового на вечірку.
Грег Вітфілд,

0

Спробуйте використати _CrtCheckMemory (), щоб побачити, в якому стані знаходиться виділена пам'ять. Якщо все піде добре, _CrtCheckMemory повертає TRUE , інакше FALSE .


0

Ви можете запустити програмне забезпечення з увімкненим глобальним прапором (див. Інструменти налагодження для Windows). Дуже часто це допоможе вирішити проблему.


0

Зробіть, щоб ваша програма генерувала міні-дамп, коли виникає виняток, а потім відкрийте його в налагоджувачі (наприклад, у WinDbg). Основні функції, на які слід звернути увагу: MiniDumpWriteDump, SetUnhandledExceptionFilter


0

Ось такий у мене був випадок, який хтось може визнати повчальним. Він вийшов з ладу лише у випуску в Qt Creator, а не під час налагодження. Я використовував файли .ini (оскільки я віддаю перевагу програмам, які можна скопіювати на інші диски, порівняно з тими, які втрачають свої налаштування, якщо реєстр пошкоджено). Це стосується будь-яких програм, які зберігають свої налаштування у дереві каталогів програм. Якщо збірки налагодження та випуску знаходяться в різних каталогах, ви також можете встановити інший параметр між ними. Я віддав перевагу в одному, а не в іншому. Це виявилось джерелом моєї аварії. Добре, що я знайшов.

Я ненавиджу це говорити, але діагностував збій лише в MS Visual Studio Community Edition; після встановлення VS, дозволивши програмі аварійно завершити роботу в Qt Creator та вибравши її відкрити в налагоджувачі Visual Studio . Хоча у моїй програмі Qt не було інформації про символи, виявляється, що бібліотеки Qt мали дещо. Це привело мене до кривдної лінії; оскільки я міг бачити, який метод називається. (Тим не менш, я думаю, що Qt - це зручний, потужний і крос-платформний фреймворк LGPL.)


-3

У мене сталася ця помилка, і vs розбився навіть при спробі очистити! мій проект. Тож я видалив файли obj вручну з каталогу Release, і після цього він склався просто чудово.


-6

Я згоден з Рольфом. Оскільки відтворюваність настільки важлива, у вас не повинно бути режиму без налагодження. Усі ваші збірки мають бути налагодженими. Наявність двох цілей для налагодження більш ніж подвоює навантаження для налагодження. Просто надішліть версію "режим налагодження", якщо вона непридатна для використання. У такому випадку зробіть його придатним для використання.


Це може працювати для 10% програм, але, звичайно, не для всіх. Ви хочете грати в ігри, випущені під час створення DEBUG? Видавати свій секретний код безпеки в товарному знаку в зручному для розбирання режимі, можливо, навіть разом з PDB? Я думаю, що не.
steffenj

Стеффендж: Я хочу, щоб розробники ігор знаходили помилки. В ідеалі, до того, як вони відправляться, але якщо це пізніше, я хочу, щоб вони змогли отримати достатньо інформації для відтворення та відстеження її. якщо це секретний код, торгова марка не застосовується. PDB? Банк даних білка? python налагоджувач?
wnoise

ІМХО, це погана ідея. Виконувані файли більші, вони не оптимізовані та працюють набагато повільніше. Ці випадки справді досить рідкісні; навіть не дивлячись на особливо божевільне, коли вони трапляються. Ви не повинні поставляти стабільно гірший товар, турбуючись про надзвичайно рідкісні найгірші налагодження. (Моє не було одним із багатьох проти.) Я програмував для NASA; і ми сказали, що як мінімум, кожен рядок коду повинен бути протестований один раз. Можливо також допомогти модульне тестування.
CodeLurker
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.