Як можна схопити трасування стека в C?


83

Я знаю, що для цього немає стандартної функції C. Мені було цікаво, які методи цього можна зробити у Windows та * nix? (Windows XP - це моя найважливіша ОС, щоб зробити це зараз.)


Відповіді:


81

glibc забезпечує функцію backtrace ().

http://www.gnu.org/software/libc/manual/html_node/Backtraces.html


6
glibc FTW ... знову. (Це ще одна причина, чому я вважаю glibc абсолютним золотим стандартом, коли мова заходить про програмування на С (це і компілятор, що йде з ним).)
Тревор Бойд Сміт,

4
Але почекайте, є ще! Функція backtrace () забезпечує лише масив покажчиків void *, що представляють функції calltack. "Це не дуже корисно. Аргумент." Не бійся! glibc надає функцію, яка перетворює всі порожні * адреси (адреси функції calltack) у зручні для читання рядкові символи. char ** backtrace_symbols (void *const *buffer, int size)
Тревор Бойд Сміт

1
Застереження: я думаю, працює лише для функцій C. @Trevor: Це просто пошук симс за адресою в таблиці ELF.
Конрад Мейєр,

2
Є також такий, void backtrace_symbols_fd(void *const *buffer, int size, int fd)який може надсилати вихід безпосередньо до stdout / err, наприклад.
wkz

2
backtrace_symbols()смокче. Він вимагає експорту всіх символів і не підтримує символи DWARF (налагодження). libbacktrace - набагато кращий варіант у багатьох (більшості) випадках.
Ерван Легранд

29

Існують backtrace () і backtrace_symbols ():

Зі сторінки користувача:

#include <execinfo.h>
#include <stdio.h>
...
void* callstack[128];
int i, frames = backtrace(callstack, 128);
char** strs = backtrace_symbols(callstack, frames);
for (i = 0; i < frames; ++i) {
    printf("%s\n", strs[i]);
}
free(strs);
...

Одним із способів використовувати це більш зручним способом / ООП є збереження результату backtrace_symbols () у конструкторі класу винятків. Таким чином, кожного разу, коли ви кидаєте такий тип винятку, у вас є трасування стека. Потім просто надайте функцію для його роздрукування. Наприклад:

class MyException : public std::exception {

    char ** strs;
    MyException( const std::string & message ) {
         int i, frames = backtrace(callstack, 128);
         strs = backtrace_symbols(callstack, frames);
    }

    void printStackTrace() {
        for (i = 0; i < frames; ++i) {
            printf("%s\n", strs[i]);
        }
        free(strs);
    }
};

...

try {
   throw MyException("Oops!");
} catch ( MyException e ) {
    e.printStackTrace();
}

Та да!

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


@shuckc лише для перетворення адреси в символьний рядок, що може бути зроблено зовні, використовуючи інші інструменти, якщо це необхідно.
Вудро Барлоу

22

Для Windows перевірте API StackWalk64 () (також на 32-бітовій Windows). Для UNIX ви повинні використовувати рідний спосіб для ОС, щоб зробити це, або повернутися до зворотного трасування glibc (), якщо це можливо.

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

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

Розглядаючи останню проблему, більшість API вимагатимуть від вас явного розподілу пам’яті або можуть робити це внутрішньо. Якщо це зробити в неміцному стані, в якому зараз знаходиться ваша програма, це може ще більше погіршити ситуацію. Наприклад, звіт про аварійне завершення роботи (або coredump) відображатиме не фактичну причину проблеми, а вашу невдалу спробу її усунення).

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


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

Іншим виходом є створення спільної служби, яка працює незалежно
Kobor42,

5

Ви повинні використовувати бібліотеку розмотування .

unw_cursor_t cursor; unw_context_t uc;
unw_word_t ip, sp;
unw_getcontext(&uc);
unw_init_local(&cursor, &uc);
unsigned long a[100];
int ctr = 0;

while (unw_step(&cursor) > 0) {
  unw_get_reg(&cursor, UNW_REG_IP, &ip);
  unw_get_reg(&cursor, UNW_REG_SP, &sp);
  if (ctr >= 10) break;
  a[ctr++] = ip;
}

Ваш підхід також буде добре працювати, якщо ви не подзвоните зі спільної бібліотеки.

Ви можете використовувати addr2lineкоманду на Linux, щоб отримати вихідну функцію / номер рядка відповідного ПК.


"функція джерела / номер рядка"? Що робити, якщо зв’язок оптимізовано для зменшення розміру коду? Однак я скажу, що це виглядає як корисний проект. Шкода, що немає можливості отримати реєстри. Я точно вивчу це. Чи знаєте ви, що він абсолютно незалежний від процесора? Просто працює над тим, що має компілятор C?
Моуг каже, відновити Моніку

1
Гаразд, цей коментар коштував того, хоча б через згадку про корисну команду addr2line!
Ogre Psalm33

addr2line не вдається перемістити код в системах з ASLR (тобто більшість того, що люди використовували протягом останнього десятиліття).
Ерван Легранд

4

Не існує незалежного від платформи способу це зробити.

Найближче, що ви можете зробити, це запустити код без оптимізації. Таким чином ви можете приєднати до процесу (за допомогою візуального налагоджувача c ++ або GDB) і отримати корисну трасування стека.


Це не допомагає мені, коли на полі вбудованого комп’ютера трапляється збій. :(
Кевін

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

якщо ви використовуєте gcc-glibc на вибраній вами платформі windows / linux / mac ..., тоді backtrace () і backtrace_symbols () працюватимуть на всіх трьох платформах. Враховуючи це твердження, я б використав слова "не існує [портативного] способу зробити це".
Тревор Бойд Сміт

4

Для Windows - CaptureStackBackTrace()це також варіант, який вимагає менше коду підготовки на кінці користувача, ніж StackWalk64()це потрібно. (Крім того, для подібного сценарію, який я мав, в CaptureStackBackTrace()підсумку працював краще (надійніше), ніж StackWalk64().)


2

Solaris має команду pstack , яку також скопіювали в Linux.


1
Корисно, але насправді не C (це зовнішня утиліта).
ефемієнт

1
також, з опису (розділ: обмеження) ": pstack наразі працює лише на Linux, лише на машині x86, на якій запущено 32-бітові бінарні файли ELF (64-біт не підтримується)"
Ciro Costa

0

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


1
Не могли б ви пояснити "ходіння стеком назад" більш докладно?
Spidey

@Spidey На вбудованих системах іноді це все, що у вас є - я думаю, це було проголосовано проти, тому що платформа WinXP. Але без libc, який підтримує стек, ви в основному повинні "ходити" по стеку. Ви починаєте з поточного базового вказівника (на x86 це вміст реєстру RBP). Це призведе до того, що ви вкажете на стек 1. збереженим попереднім RBP (саме так підтримуватиметься ход стека), та 2. зворотною адресою виклику / гілки (збережений регістр RIP виклику), який повідомляє вам, якою була ця функція. Потім, якщо у вас є доступ до таблиці символів, ви можете знайти адресу функції.
Тед Міддлтон,

0

Останні кілька років я використовую libbacktrace Ian Lance Taylor. Це набагато чистіше, ніж функції в бібліотеці GNU C, які вимагають експорту всіх символів. Це забезпечує більше корисності для генерації зворотних слідів, ніж libunwind. І останнє, але не менш важливе: його не перемагає ASLR, як підходи, що вимагають зовнішніх інструментів, таких як addr2line.

Спочатку Libbacktrace був частиною розповсюдження GCC, але тепер він доступний автору як окрема бібліотека за ліцензією BSD:

https://github.com/ianlancetaylor/libbacktrace

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

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