Zephyr - Code checker
July 24, 2024

Poznaj znaczenie i praktyczne zastosowania narzędzia CodeChecker w analizie statycznej kodu w języku C. Artykuł opisuje, jak CodeChecker wspomaga identyfikację i naprawę potencjalnych wad kodu, motywując programistów do przestrzegania najlepszych praktyk i standardów bezpieczeństwa. Przejdziemy za rączkę przez instalację, konfigurację i wykorzystanie tego narzędzia w projektach z użyciem Clang Tidy, Clang Static Analyzer i Cppcheck. Dodatkowo, artykuł oprowadza przez proces integracji CodeCheckera z projektem Zephyr i ilustruje, jak narzędzie to może poprawiać jakość kodu poprzez analizę trendów i działanie na zdalnym serwerze.

Język C zadebiutował w 1972 roku i od tego czasu zdobył ogromną popularność. Trudno oszacować, ile programów powstało w tym języku. Ponad 50 lat to wystarczająco długo, aby poznać jego mocne i słabe strony. W przypadku języka C oznacza to również dostrzeżenie potencjalnych wad i pułapek. Takie przypadki zostały zebrane w formie wytycznych i standardów, które programiści powinni przestrzegać. Aby im w tym pomóc, powstały narzędzia do analizy statycznej. Te narzędzia wykrywają struktury kodu, które są uznawane za złą praktykę lub niebezpieczne z punktu widzenia bezpieczeństwa. Mogą one analizować każdy plik osobno lub uwzględniać szerszy kontekst projektu i jego zależności, aby ocenić zgodność kodu z wytycznymi lub standardami.

Analiza statyczna ma 44% skuteczność w usuwaniu defektów, gdy jest używana samodzielnie! Dlaczego tak jest? Wydaje się, że kluczowym czynnikiem jest tutaj szybkość przekazywania informacji. Podczas kodowania możemy wychwycić każde naruszenie od razu, gdy wpisujemy średnik (przepraszam entuzjastów Pythona :wink:). Jesteśmy wtedy w kontekście naruszenia, więc istnieje duża szansa, że od razu je poprawimy. Jeśli nie, zapamiętamy ten błąd lub przechowamy go w bazie danych dostępnej dla całego zespołu.

Przechowując wyniki analizy w bazie danych, możemy łatwo wizualizować jakość projektu, używając liczby naruszeń jako miary. Większość narzędzi oferuje także analizę trendów, pokazującą historyczną jakość kodu i jej ewolucję. Na podstawie tych danych można podjąć niezbędne działania naprawcze lub zwrócić większą uwagę na obszary wymagające poprawy.

CodeChecker

CodeChecker to infrastruktura wspierająca proces analizy statycznej kodu. Działa na różnych backendach, w tym:

  • Clang Tidy
  • Clang Static Analyzer
  • Cppcheck

System uruchamia serwer, który przechowuje wyniki analizy i może wyświetlać trendy. Zephyr bezproblemowo integruje się z CodeCheckerem. Ten artykuł poprowadzi Cię przez instalację, aktywację i konfigurację CodeCheckera. Dodatkowo, pokaże, jak nawigować po pulpicie CodeCheckera oraz jak przeprowadzać analizę zarówno z Zephyrem, jak i bez niego na starszym kodzie.

Instalacja

CodeChecker to pakiet Pythona, który można łatwo zainstalować za pomocą pip:

pip install codechecker

W tym artykule skupimy się na backendzie clang-tidy w CodeCheckerze. Zrozumienie tego aspektu uprości proces konfiguracji. Jeśli nie masz zainstalowanego clang-tidy, możesz to zrobić w zależności od systemu operacyjnego. Na przykład na Ubuntu, użyj poniższego polecenia:

sudo apt install clang-tidy

Jeśli konfiguracja Zephyra nie jest ustawiona na Twojej lokalnej stacji roboczej, musisz skonfigurować dwa elementy do tego tutorialu. Pierwszym wymogiem jest instalacja SDK, niezbędna do budowania projektów natywnie bez potrzeby użycia płyty głównej. Odnieś się do oficjalnej dokumentacji Zephyra, aby uzyskać wskazówki dotyczące instalacji SDK: Getting Started Guide — Zephyr Project Documentation [1].

Pierwsze budowanie

Przejdźmy do analizy, aby pokazać, jak proste może to być. Ten artykuł koncentruje się na repozytorium dostępnym pod adresem [2]

Po sklonowaniu repozytorium, zainicjuj i zaktualizuj przestrzeń roboczą, wykonując następujące polecenia:

west init -l app && west update

Jeśli jest to Twoje pierwsze spotkanie z Zephyrem na obecnym komputerze, zaleca się eksport definicji Zephyra dla CMake. Możesz to zrobić, uruchamiając polecenie:

west zephyr-export

Checkpoint! Przed dalszym postępowaniem upewnij się, że poniższe polecenie wykonuje się pomyślnie:

west build -b qemu_cortex_m3 app --pristine && west build -t run

Uruchamianie analizy statycznej z Zephyrem

Aby pomyślnie uruchomić CodeChecker, należy skompilować projekt z włączoną określoną opcją. Wykonaj następujące polecenie:

west build -b qemu_cortex_m3 app --pristine -- -DZEPHYR_SCA_VARIANT=codechecker

Po uruchomieniu tego polecenia mogłeś napotkać problem, gdy terminal zabrakło miejsca, co utrudniało widok początkowej części polecenia. Dzieje się tak, ponieważ CodeChecker z natury wykonuje analizę statyczną za pomocą narzędzi takich jak clang-tidy, clangsa i cppcheck. Analizuje również wszystkie pliki kompilowane w ramach projektu, co w tym przypadku wynosi około 84 plików. Jednak skupimy się na dwóch konkretnych jednostkach translacyjnych: main.cpp i worker.cpp.

Aby skutecznie rozwiązać te problemy, skoncentrujmy się na uzyskaniu wyników z clang-tidy. Istnieją trzy główne metody wprowadzenia tego ustawienia:
  • Uwzględnienie niezbędnych argumentów w poleceniu west build,
  • Scoia'tael
  • Skonfigurowanie ich w pliku CMakeLists.txt,
  • Wykorzystanie pliku konfiguracyjnego specjalnie zaprojektowanego dla CodeCheckera.

Spośród tych opcji zalecamy wykorzystanie pliku konfiguracyjnego dla CodeCheckera. Plik ten ma składnię YAML, co zapewnia ustrukturyzowany format, który zwiększa czytelność i upraszcza konserwację w miarę rozwoju projektu. Więcej szczegółów znajdziesz w poniższej sekcji!

Konfiguracja

Stwórzmy plik konfiguracyjny w folderze app/config/codechecker.yml:

analyzer:
  - --analyzers=clang-tidy
  - --analyzer-config=clang-tidy:take-config-from-directory=true

Wszystkie pliki konfiguracyjne znajdują się w repozytorium.

Informacje potrzebne do tej konfiguracji można znaleźć w przewodniku CodeChecker analyze --help. Te argumenty zostaną włączone do końcowego polecenia CodeChecker analyze. Teraz musimy utworzyć plik .clang-tidy w głównym katalogu repozytorium. Możesz skorzystać z domyślnych ustawień dla clang-tidy i pominąć argument analyzer-config. Celem jest pokazanie, jak powiązać niestandardową konfigurację z CodeCheckerem. Przejdźmy do podstawowego przykładu .clang-tidy:
---
Checks:       'clang-diagnostic-*,clang-analyzer-*,readability-*'

Rozwiązując początkowy problem, zawężając zakres analizy do jednego analizatora i określonych reguł, musimy teraz zająć się drugim problemem, jakim jest analiza plików zewnętrznych. Aby to osiągnąć, powinniśmy poinstruować CodeCheckera, aby ignorował pliki, które nie leżą w naszym obszarze zainteresowań. Zgodnie z oczekiwaniami, CodeChecker można skonfigurować do ignorowania plików na podstawie określonych wzorców w pliku ignorowania. Utwórz więc plik o nazwie app/config/skip.codechecker i wprowadź następującą zawartość:

-*/zephyr/kernel/*
+*/app/src/*
-*

Jak interpretować ten plik? Każda ścieżka jednostki translacyjnej jest porównywana z każdym regexem w kolejności od góry do dołu. Pierwsza pasująca linia określa, czy plik powinien być sprawdzany (oznaczone przez + na początku) czy ignorowany (oznaczone przez -).

Następnie oba te pliki muszą zostać dodane do CodeCheckera do domyślnego włączenia. Można to osiągnąć, ustawiając zmienną CMake CODECHECKER_ANALYZE_OPTS. Dodaj następujące linie na początku pliku app/CMakeLists.txt:
cmake_minimum_required(VERSION 3.20.0)
set(CODECHECKER_ANALYZE_OPTS
    "--config;${CMAKE_SOURCE_DIR}/config/codechecker.yml;"
    "--skip;${CMAKE_SOURCE_DIR}/config/skip.codechecker"
    CACHE STRING "Code checker options")
find_package(Zephyr-sdk REQUIRED)
# ...
Ważne jest, aby umieścić ten kod przed poleceniem find_package, aby wartość była znana w tym momencie. Następnie ponownie uruchom analizę i przejrzyj wyniki, które powinny wyglądać podobnie do podanego poniżej przykładu:
west build -b qemu_cortex_m3 app --pristine -- -DZEPHYR_SCA_VARIANT=codechecker
...
[120/121] Running utility command for codechecker
Found no defects in worker.cpp
[STYLE] /repos/code-checker-example/app/src/main.cpp:5:12: implicit conversion 'int' -> bool [readability-implicit-bool-conversion]
    while (1) blink_led();
           ^
[STYLE] /repos/code-checker-example/app/src/main.cpp:5:14: statement should be inside braces [readability-braces-around-statements]
    while (1) blink_led();
             ^
Found 2 defect(s) in main.cpp
...
----==== Checker Statistics ====----
-------------------------------------------------------------------
Checker name                         | Severity | Number of reports
-------------------------------------------------------------------
readability-implicit-bool-conversion | STYLE    |                 1
readability-braces-around-statements | STYLE    |                 1
-------------------------------------------------------------------
----=================----

Raporty

Choć możesz preferować wyświetlanie wyników w innym formacie, aby lepiej je odczytać, na przykład w HTML. Aby to osiągnąć, musisz ustawić zmienną CODECHECKER_EXPORT. Można to zrobić w pliku app/CMakeLists.txt, zaraz poniżej linii set(CODECHECKER_ANALYZE_OPTS):
set(CODECHECKER_EXPORT "html" CACHE STRING "Format of a generated report")
Po ponownym uruchomieniu procesu za pomocą west build, będziesz mógł uzyskać raport w formacie HTML. Aby go wyświetlić, wykonaj polecenie rekomendowane przez CodeChecker w ostatnich liniach wyników. Na przykład, w moim przypadku polecenie to:
firefox /repos/code-checker-example/build/sca/codechecker/codechecker.html/index.html

To wyświetli indeks zawierający wszystkie wykryte naruszenia, jak pokazano poniżej:

Figure1. Plik indeksu ze wszystkimi naruszeniami dokonanymi w ostatnim uruchomieniu.
Dla bardziej szczegółowego przeglądu, możesz uzyskać dostęp do pliku main.cpp w kolumnie File. To pozwoli ci przejrzeć jego zawartość i zlokalizować miejsca naruszeń. Po kliknięciu, zobaczysz widok podobny do tego przedstawionego poniżej:
Figure 2.: Naruszenia w pliku

Integracja z IDE

Możemy wyświetlić raport w formacie tekstowym po każdym buildzie, w tym również opcję dostępu do niego w formacie HTML. Pytanie jednak pozostaje: czy potencjalne naruszenia zostaną naprawione natychmiast po zakończeniu pracy? Odpowiedź może się różnić. Dlatego mocno popieram zasadę fail-fast w przypadku analizy statycznej kodu. Uważam, że otrzymywanie tych wyników w ciągu kilku sekund po zakończeniu linii kodu jest kluczowe. Takie podejście maksymalizuje szanse na natychmiastowe naprawienie naruszeń, co sprawia, że workflow jest bardziej efektywny i komfortowy.

Aby to osiągnąć, możesz aktywować wtyczkę CodeChecker w VS Code (codechecker.vscode-codechecker). Po odpowiedniej konfiguracji będziesz w stanie identyfikować błędy bezpośrednio w swoim IDE, analogicznie do tych zaznaczonych w raporcie HTML. Poniżej znajduje się konfiguracja, której osobiście używam:
{
    "codechecker.executor.runOnSave": true,
    "codechecker.executor.arguments": "--config ${workspaceFolder}/app/config/codechecker.yml --skip ${workspaceFolder}/app/config/skip.codechecker",
    "codechecker.backend.compilationDatabasePath": "${workspaceFolder}/build/compile_commands.json"
}

Po zastosowaniu tej konfiguracji w pliku .vscode/settings.json, CodeChecker będzie uruchamiany automatycznie za każdym razem, gdy zapiszesz plik. Jeśli masz również włączoną funkcję automatycznego zapisywania, analiza będzie odbywać się w tle podczas pisania, co usprawnia workflow.

Dla argumentów executora, możesz przekazać te same pliki, które są używane w CMake dla CodeCheckera. To zapewnia perfekcyjną synchronizację konfiguracji. Na koniec, pamiętaj, aby określić lokalizację bazy danych kompilacji, którą w naszym przypadku jest plik compile_commands.json.

Ciesz się płynnym procesem sprawdzania kodu!

Figure 3. CodeChecker plugin in VsCode

Praktyczne Zastosowania

Pierwsza część artykułu skupiała się głównie na konfiguracji i wykorzystaniu CodeCheckera. W tej sekcji omówimy tematy mające praktyczne znaczenie w rzeczywistych projektach. Tematy te obejmują:

  • Współpracę z zdalnym serwerem CodeChecker w środowisku Continuous Integration (CI),
  • Integrację CodeCheckera z dowolnym narzędziem do sprawdzania przed-commit,
  • Implementację funkcji automatycznego naprawiania,
  • Uruchamianie CodeCheckera na projektach spoza ekosystemu Zephyr.

CodeChecker i CI

CodeChecker ma możliwość przesyłania swoich wyników na zdalny serwer. Ta sekcja skupia się na uruchomieniu lokalnego serwera w celu przesłania najnowszych wyników analizy. Dodatkowo, przedstawię rozszerzone funkcjonalności dostępne na serwerze w porównaniu do lokalnie generowanych raportów.

Na początek, uruchom serwer, wykonując następujące polecenie w osobnym terminalu:

CodeChecker server

Następnie sprawdź łączność, otwierając w przeglądarce adres:

http://localhost:8001/Default/

Jeśli wszystko działa poprawnie, ponownie uruchom analizę z dodatkową flagą, aby przekierować wyniki na serwer. Użyj poniższego polecenia:

west build -b qemu_cortex_m3 app --pristine -- -DZEPHYR_SCA_VARIANT=codechecker -DCODECHECKER_STORE_OPTS="--name;example;--url;localhost:8001/Default"

Po odświeżeniu przeglądarki zauważysz, że produkt "Default" zarejestrował nowy przebieg od ostatniej analizy. Przejdź do produktu "Default" > zakładka "Statistics", aby zobaczyć wykres ilustrujący naruszenia w czasie. Ilość dostarczonych informacji jest oczywiście ograniczona, mając tylko jedną iterację.

Figure 4. Przykładowy wykres naruszeń w czasie

Z perspektywy serwera masz dostęp do tych samych statystyk co lokalnie. Jednak z pulpitami masz możliwość:

  • Śledzenia zmian liczby naruszeń w czasie,
  • Agregacji statystyk z wielu produktów w jednym miejscu,
  • Klasyfikacji naruszeń jako "potwierdzone błędy", "fałszywe pozytywy" lub "celowe",
  • Prowadzenia dyskusji na temat naruszeń, używając swojego osobistego konta.

Warto spróbować, zwłaszcza jeśli masz CodeCheckera skonfigurowanego w swoim projekcie. Uruchomienie i wdrożenie serwera jest stosunkowo proste.

Blokowanie commitów w przypadku nierozwiązanych naruszeń

W poprzedniej sekcji nauczyłeś się o różnych stanach, w jakich może znajdować się naruszenie. Te stany obejmują:

  • Potwierdzony błąd
  • Fałszywy pozytyw
  • Celowy
  • Nierozwiązany

Moim zdaniem, najgroźniejszym stanem jest nierozwiązany. Idealnie, takich problemów w ogóle nie powinno być. Dlaczego? Ponieważ co to oznacza? Czy ktoś jest świadomy istnienia takiego naruszenia? A może nie jest ono istotne? A może jest, i mamy do czynienia z poważnymi problemami. Wolę mieć jasność w tej kwestii. Dlatego cieszę się, że każde naruszenie może znaleźć się w jednym z tych stanów.

Jednak przejdźmy do sedna. Przypuszczalnie, chcesz sprawdzić, czy to w pre-commit hook czy na ciągłej integracji (CI), że nie ma nierozwiązanych naruszeń. Aby to osiągnąć, musisz przetworzyć wygenerowany raport używając polecenia podobnego do poniższego:

CodeChecker parse build/sca/codechecker/codechecker.plist --review-status unreviewed

To polecenie powinno zwrócić kod powrotu 2, co oznacza, że przynajmniej jeden raport został wygenerowany przez analizator. Kod powrotu inny niż 0 powinien być również zrozumiały dla każdego narzędzia pre-commit lub CI, co pozwala na użycie takiego polecenia do blokowania commitu lub builda CI. Jednak czy naprawdę chcemy przepchnąć ten kod z naruszeniami na główną gałąź? Naturalnie, takie sytuacje się zdarzą. Co wtedy? Cóż, zalecam oznaczenie tego naruszenia jako potwierdzonego w kodzie, abyś mógł się nim zająć, gdy nadejdzie właściwy czas. Można to zrobić, dodając komentarz podobny do poniższego:

// codechecker_confirmed [all] this code intentionally contains violations
while (1) blink_led();

Dzięki temu polecenie parsowania zakończy się sukcesem, a nadal będziesz mógł zobaczyć to naruszenie zarówno w swoim zintegrowanym środowisku programistycznym (IDE), jak i na zdalnym pulpicie. Czy to nie stwarza sytuacji korzystnej dla wszystkich?

Automatyczne Naprawianie

Style checkery często oferują opcję automatycznej korekty naruszeń. Tak jest również w naszym przypadku. Wymagane poprawki obejmują dodanie klamr po pętli while i zamianę 1 na true. Brzmi prosto, prawda? Oczywiście. Wykorzystanie CodeCheckera może załatwić tę sprawę bez wysiłku. Wystarczy wykonać poniższe polecenie, a wszystkie poprawki zostaną naniesione, zostawiając CodeCheckera zadowolonego:

CodeChecker fixit build/sca/codechecker/codechecker.plist --apply

CodeChecker w Projekcie Bez Zephyra

Wow, wszystko brzmi imponująco do tej pory, ale co, jeśli nie pracujesz nad projektem Zephyra? Nie martw się, nie jesteś zagubiony! Bardzo przydatną funkcją jest możliwość szybkiej analizy w CodeCheckerze. Jak to działa? Analizuje on trwający proces budowy lub bazę danych kompilacji, aby przeprowadzić analizę na podstawie tych informacji. Zanurzmy się w to!

Rozważmy prosty projekt (katalog non-zephyr-app w repozytorium), który składa się z jednego pliku źródłowego i Makefile służącego jako przepis budowy. Zawartość main.cppjest następująca:
#include <chrono>
#include <iostream>
#include <thread>
namespace {
    const auto BLINK_DURATION = std::chrono::milliseconds(1000);
    void blink_led() {
        std::cout << "LED blinks" << std::endl;
        std::this_thread::sleep_for(BLINK_DURATION);
    }
}
int main(void)
{
    while (1) blink_led();
    return 0;
}

Oto prosty Makefile:

blink: main.cpp
	g++ -Wall main.cpp -o blink
Aby bezproblemowo włączyć ten projekt do CodeCheckera, wykorzystamy polecenie CodeChecker check. Wykonaj poniższe polecenie:
CodeChecker check --config ../app/config/codechecker.yml --build "make"

Po uruchomieniu tego polecenia powinieneś zobaczyć wynik podobny do poniższego:

> CodeChecker check --config ../app/config/codechecker.yml --build "make"
...
[INFO 2024-07-09 09:34] - ----=================----
[INFO 2024-07-09 09:34] - Analysis length: 0.8173582553863525 sec.
[STYLE] /repos/code-checker-example/non-zephyr-app/main.cpp:17:12: implicit conversion 'int' -> bool [readability-implicit-bool-conversion]
    while (1) blink_led();
           ^
[STYLE] /repos/code-checker-example/non-zephyr-app/main.cpp:17:14: statement should be inside braces [readability-braces-around-statements]
    while (1) blink_led();
             ^
Found 2 defect(s) in main.cpp
...
Ten wynik ściśle przypomina raport wygenerowany dla aplikacji Zephyra, prawda? Ponowne uruchomienie tego samego polecenia nie przyniesie żadnego wyniku, ponieważ nic się nie zmieniło i w związku z tym make nie rekompiluje projektu.
Polecenie CodeChecker check można również wykorzystać w inny sposób. Na przykład w skomplikowanym scenariuszu projektu, gdzie chcesz analizować tylko zmodyfikowane pliki, to polecenie okazuje się nieocenione.

Podsumowanie

Narzędzia do analizy statycznej są kluczowe dla identyfikacji potencjalnych wad i pułapek w kodzie, zwłaszcza w językach takich jak C, które istnieją od ponad 50 lat. Te narzędzia pomagają programistom przestrzegać wytycznych i standardów, analizując struktury kodu pod kątem złych praktyk lub ryzyk związanych z bezpieczeństwem. Przechowywanie wyników analizy w bazie danych pozwala na wizualizację jakości projektu na podstawie liczby naruszeń i analizy trendów, umożliwiając podjęcie niezbędnych działań naprawczych. Dodatkowo, CodeChecker wspiera proces analizy statycznej kodu, działając na różnych backendach takich jak Clang Tidy, Clang Static Analyzer i Cppcheck, oferując serwer do przechowywania wyników analizy i wizualizacji trendów. Instalacja, konfiguracja i uruchamianie CodeCheckera to kluczowe kroki zapewniające efektywną analizę statyczną kodu i poprawę jego jakości.

Ref:
[1] Getting Started Guide -> https://docs.zephyrproject.org/latest/develop/getting_started/index.html
[2] Github Code checker example -> https://github.com/goodbyte-software/code-checker-example
[3] Code checker -> https://codechecker.readthedocs.io/en/latest/
[4] CodeChecker support, Zephyr -> https://docs.zephyrproject.org/latest/develop/sca/codechecker.html