Statyczna analiza kodu, czyli jak komputer może szukać błędów

03.02.2010 - Krzysztof Skrzętnicki
TrudnośćTrudność

A teraz na serio

Bardzo wiele rzeczy działa dobrze na małą skalę. Problemy zaczynają się, gdy przechodzimy od problemów zabawkowych do prawdziwych zastosowań. Z zadowoleniem mogę jednak stwierdzić, że scan-build doskonale spisuje się nawet przy projektach o znacznej wielkości.

Osobiście pracuję od kilku nad rozwojem internetowej gry tekstowej Killer MUD. Kod gry jest bardzo złożony i rozległy - ponad 260 tysięcy linii kodu ANSI C. Wielokrotnie w trakcie prac pojawiały się błędy których wytropienie i naprawienie było bardzo czasochłonne. Czasami było to nawet kilka dni pracy. O ile prościej byłoby, gdyby błędy te można było znaleźć z góry. Znalezienie błędu zanim on wystąpi w grze jest jednak praktycznie nierealne - jeżeli miałby to robić człowiek. Co innego komputer.

Uruchomiłem $ scan-build make aby zbudować serwer gry. Po kilku minutach działania zakończył on pracę z komunikatem:

scan-build: 622 bugs found.
scan-build: Run 'scan-view /tmp/scan-build-2010-02-03-1' to examine bug reports.

Całkiem niezły wynik - na pewno jest co analizować. Statystyki pokazane na początku raportu mówią nam nieco o najczęściej napotykanych błędach:

Widzimy dwie kategorie błędów: Dead store i Logic errors. Pokazaliśmy na naszym poprzednim przykładzie, że błędów Dead store nie można ignorować. Mimo tego druga kategoria błędów zwykle jest o wiele poważniejsza.

Łatamy prawdziwe błędy

Po otworzeniu pierwszego lepszego przypadku ujrzałem taki widok. W poniższym kodzie poprawnie zauważone zostało użycie niezainicjowanej zmiennej clone.

Gdyby makro act istotnie wykorzystywało tą zmienną, z dużym prawdopodobieństwem błąd taki spowodowałby błąd segmentacji, czyli w najlepszym wypadku restart serwera. Istotnie, jeżeli błędna linijka zostanie minimalnie zmieniona, by wyświetlać nazwę obiektu:

1
act( "{RNIE MOŻNA KLONOWAĆ ARTEFAKTÓW!!!{x. (Artefakt = $p) [@3276]", ch, clone, NULL, TO_CHAR );

to w trakcie wykonania odpowiedniej funkcji wystąpi błąd, który unieruchomi serwer.

Kontynuujemy polowanie

Załatawszy poprzedni błąd możemy przejść do kolejnego. Zanim to nastąpi, zastanówmy się najpierw, co wiemy o błędach z pierwszego ekranu raportu? Po pierwsze znamy kategorię - to może nam sugerować powagę błędu. Wiemy w jakim pliku, a co za tym idzie w jakim elemencie programu występuje błąd. Bez znajomości projektu nie mówi nam to jednak wiele. Mamy też podaną statystykę o nazwie "Path Length".

"Path Length" oznacza po angielsku "długość ścieżki". Co oznacza ta wielkość? Oznacza ona liczbę kroków, które muszą zostać wykonane, aby wystąpił błąd. Jako wykonanie kroku rozumiane jest wykonanie jakiejś instrukcji sterującej, np. if, for czy while. W trywialnych przypadkach jest to 1. Przykładowo w poniższej funkcji nie inicjujemy zmiennej victim, ale przekazujemy ją jako parametr do innej funkcji (za pośrednictwem makra act):

Im krótsza długość ścieżki, tym łatwiej jest zrozumieć o co chodzi w raporcie błędu. Z drugiej strony dłuższa ścieżka wskazuje skomplikowany kod, w którym wychwycenie błędów bez pomocy programu może być trudne, albo wręcz niemożliwe. Rekordowa ścieżka znaleziona w kodzie Killer MUDa wynosiła 63 kroki i odnosiła się do dokładnie 500 linii kodu. To bardzo dużo i analiza tych wszystkich warunków jest trudna. Najlepszym rozwiązaniem jest nie tworzyć tak skomplikowanego kodu i dzielić złożone funkcje na mniejsze fragmenty, o jasno określonych zadaniach.

Fałszywy alarm

Po przejrzeniu kilku raportów staje się jednak jasne, że nie wszystkie ostrzeżenia otrzymane od narzędzia faktycznie prowadzą nas do błędów. Zdarza się, że zgłoszony błąd to fałszywy alarm. (Należy jednak zdawać sobie sprawę z tego, że napisanie narzędzia idealnego, które znajduje każdy błąd, jest niemożliwe.) Przykładowo, scan-build zupełnie nie analizuje tego, jakie wartości zwracają funkcje numeryczne. Widoczne jest to w poniższym, trywialnym programie:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int constant( int x ){ return x; }
 
int main(){
  int x;
  if ( constant( 1 ) == 1 )
    x = 5;
  printf("x = %d\n", x);
  return 0;
}

Program ten jest poprawny i zawsze wyświetli "x = 5". scan-build zgłasza jednak błąd:

warning: Pass-by-value argument in function call is undefined.
      printf("x = %d\n", x);
      ^                  ~

Argumentuje to tym, że w przypadku, gdy gałąź kodu dla if nie wykona się, zmienna x nie będzie miała przypisanej żadnej wartości. Łatwo jednak widać, że gałąź ta wykona się zawsze.

4.666665
Twoja ocena: Brak Ocena: 4.7 (3 ocen)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com