Stos wywołań funkcji

2008-06-13 13:42

Jedną z bardziej przydatnych informacji podczas znajdowania przyczyn błędów wykonania jest śledzenie stosu (stack trace; właściwie to nie ma dobrego polskiego tłumaczenia, jak zwykle zresztą :]). Dzięki niemu możemy mieć informacje o kolejnych wywołaniach funkcji, które doprowadziły błędu wykonania. Zwykle stack trace jest dostępny z poziomu złapanego obiektu wyjątku – np. poprzez właściwość System.Exception.StackTrace w .NET czy metody java.lang.Throwable.get/printStackTrace w Javie.

Standardowa klasa exception w C++ nie posiada podobnej funkcjonalności, bo w tym języku stos nie jest automatycznie śledzony. Przy pewnym wysiłku możemy aczkolwiek zapewnić sobie podobną funkcjonalność – chociażby tak:

  1. typedef std::list<std::string> StackTrace;
  2.  
  3. class Trace
  4. {
  5.     private:
  6.         static StackTrace ms_Trace;
  7.  
  8.     public:
  9.         Trace(const std::string& item) { ms_Trace.push_back (item); }
  10.         ~Trace()    { ms_Trace.pop_back(); }
  11.  
  12.     friend const StackTrace& GetStackTrace()    { return ms_Trace; }
  13. };
  14. StackTrace Trace::ms_Trace;    // inicjalizacja pola statycznego (w .cpp)
  15.  
  16. // makro (dla Visual C++)
  17. #define GUARD Trace __trace(__FUNCTION__ " [" __FILE__ ", line " __LINE__ "]")

Przechowujemy po prostu globalną listę z danymi wszystkich wywoływanych funkcji. Elementy do niej dodawane są przy wejściu w każdą funkcję, która na początku ma makro GUARD:

  1. int Foo() { GUARD; /* ... */ }

zaś zdejmowane wtedy, gdy pomocniczy obiekt typu Trace wyjdzie poza swój lokalny zasięg – czyli po opuszczeniu funkcji. To sprawia, że na liście mamy zawsze informacje o wszystkich wywołaniach funkcji prowadzących do aktualnego miejsca (jakie one są, zależy od kompilatora; użyte wyżej makro __FUNCTION__ nie jest na przykład standardową częścią C++). Tworząc obiekt wyjątku, możemy więc zapisać kopię tej listy, by można ją było odczytać w bloku catch i wyświetlić.
Rozwiązanie nie jest oczywiście bardzo ładne, bo wymaga dodatkowej linijki w każdej funkcji. Zdaje się jednak, że nie ma żadnego sposobu, by tę niedogodność wyeliminować. Ja przynajmniej takiego nie znam :)

Tags: , , ,
Author: Xion, posted under Programming »


11 comments for post “Stos wywołań funkcji”.
  1. io:
    June 13th, 2008 o 14:17

    Skąd Ty bierzesz te wszystkie egzotyczne pomysły? :)

  2. Xion:
    June 13th, 2008 o 20:12

    Jeśli chodzi o samo rozwiązanie stack trace’a, to nie jest specjalnie oryginalne; podobne można znaleźć w kilku miejscach. Jeśli zaś chodzi o to, dlaczego opisałem je właśnie dzisiaj, to nietrudno się domyślić, co było inspiracją… Wskazówka: chodzi o zupełnie niekoderską analogię ;-]

  3. sth:
    June 13th, 2008 o 21:06

    piatek 13 ?

  4. io:
    June 13th, 2008 o 21:22

    Chyba jestem kiepski w zagadkach… wskazówka też mi nie pomaga ;).

  5. revo:
    June 13th, 2008 o 23:20

    Tylko dlaczego friend a nie static? :>

    Zastanawiałem się czy dodać pokazywanie stosu wywołań do swojego projektu i skoro dostałem na tacy to skorzystam ;)

  6. SirMike:
    June 13th, 2008 o 23:41

    Kolejny przyklad na to “jak sprawic, ze programy w C++ pisze sie fatalnie” ;)
    Nie wystarczy Wam polecenie “backtrace” w debuggerze?

  7. revo:
    June 14th, 2008 o 0:44

    Zainspirowany tym co napisał SirMike postanowiłem poszperać trochę i trafiłem na coś innego – http://www.codeguru.com/cpp/w-p/system/threading/article.php/c10317/ . Używa się tego banalnie i już to u siebie zintegrowałem http://revo.pl/stuff/call_stack.png ;)

  8. Xion:
    June 14th, 2008 o 11:45

    revo: Wole GetStackTrace() od Trace::GetStackTrace() ;)

    SirMike: Zazwyczaj wystarczy, chyba że chcemy móc też wykrywać błędy w wersji produkcyjnej (na przykład na podstawie tego, co nam prześlą userzy).

    A co do tego rozwiązania z CodeGuru, to zastanawiam się, jak ono jest jednocześnie “portable” i używające funkcji StackWalk64() z Windows API? :)

  9. revo:
    June 14th, 2008 o 11:49

    Portable w przypadku M$ znaczy pewnie ‘przenośne pomiędzy różnymi wersjami Windows’ ;) No i skoro to jest funkcja z WinAPI, to powinna działać na różnych kompilatorach.

    Jak dla mnie to w zupełności wystarczy – w tej chwili nie piszę na żadną inną platformę.

  10. yarpen:
    June 16th, 2008 o 13:49

    Na kazdym x86 mozna sobie po prostu pojsc po EIP/ESP i odtworzyc stos recznie, jezeli ktos ma alergie na rozwiazania MS. Na innych platformach wystarczy poczytac o ABI, zazwyczaj jest analogicznie proste (chyba, ze ktos kompiluje z FPO, ale to sie rzadko zdarza). Rozwiazanie z makrem jest wziete bodaj z Unreala i troche traci latami 90-tymi :)

  11. TeMPOraL:
    June 17th, 2008 o 22:20

    Napisałem kiedyś posta na ten temat,
    http://temporal.pr0.pl/devblog/2008/01/03/chronienie-funkcji/

    (coś się podziało z WordPressem i padł codebox )

    To rozwiązanie jest bardziej zorientowane na wyjątki – call-stack jako taki nie istnieje do momentu wystąpienia błędu; ale z drugiej strony w Twoim rozwiązaniu call-stack znika w momencie rzutu wyjątkiem :). Takie rozwiązanie kosztuje dodatkowy blok try-catch i wymaga dwóch linijek (linia UNGUARD), ale oszczędza na tworzeniu obiektu lokalnego (użyta jest lokalna dla funkcji tablica statyczna).

    Tak q’woli porównania ;)

Comments are disabled.
 


© 2017 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.