W poprzednich wpisach wychwalałem wartość edukacyjną asemblera i opowiadałem trochę jak wyglądała moja ścieżka nauki. Dzisiaj porozmawiamy sobie o praktycznych zastosowaniach, gdzie faktycznie musimy pisać, albo przynajmniej czytać kod pisany w asemblerze.

Opowiem o 6 głównych przypadkach, kiedy chcemy użyć asemblera. I o jednym kiedy wydaje nam się, że chcemy, ale… no właśnie – źle nam się wydaje.

1. Kod startupowy, czy inaczej boot

Czyli pierwsze instrukcje, które wykonują się po uruchomieniu procesora.

O co chodzi? Standard C mówi nam, że w momencie startu funkcji main system jest już odpowiednio skonfigurowany. Mamy działający stos, pamięć jest zainicjalizowana na początkowe wartości i tak dalej.

Samo się nie zrobi. W mikrokontrolerach albo mamy kod startupowy wygenerowany z IDE – jak w STMach. Albo procedury startupowe są domyślnie zaszyte w plikach kompilatora – jak w PICach.

Dla mnie w praktyce to był najczęstszy use case asemblera w komercyjnych projektach. Co ciekawe – kod startupowy jest pisany w asemblerze również w PCtach. W końcu tam również po uruchomieniu trzeba zaincjować maszynę do pracy, wczytać kod z odpowiedniego sektora pamięci i tak dalej.

W tym temacie polecam świetną serię Gynvaela Coldwinda o pisaniu systemu operacyjnego:

W ten sposób przechodzimy płynnie do punktu nr 2…

2. Systemy operacyjne

W systemach operacyjnych mamy taski i procesy, które działają niezależnie od siebie. Aby to osiągnąć – muszą mieć stos oraz zawartość rejestrów procesora tylko dla siebie. A kod w OSie, który odpowiada za zmianę kontekstu musi zarządzać zawartością wskaźnika stosu i innych rejestrów CPU. Bez asemblera tutaj się nie obejdziemy.

3. Self checki aplikacji

O tym przypadku użycia też już mówiłem nie raz. Ale dla porządku wspomnę po raz kolejny. W systemach safety-critical często normy wymuszają na nas robienie self-checków systemu przy starcie oraz w runtime. W skład tych self-checków wchodzą między innymi testy RAMu, instrukcji i rejestrów CPU. Ale nie tylko.

Testy startupowe najczęściej są umieszczane w kodzie startupowym i mogą być bardziej dokładne. Z kolei testy runtime muszą być wykonywane cyklicznie i nie powinny zakłócać działania aplikacji.

Po więcej odsyłam do serii live na YT, w których pisaliśmy testy startupowe CPU:

4. Debugowanie i optymalizacja

Asembler jest bardzo przydatny w debugowaniu. I to na wielu płaszczyznach. Oczywiście najczęściej się przydaje przy cięższych bugach, na które musimy spędzić wiele godzin, czy nawet dni.

Niektóre błędy ze wskaźnikami najłatwiej zdebugować dzięki asemblerowi. Szczególnie takie, które spowodowały Hard Faulta. Po pierwsze specjalna procedura asemblerowa pozwala zrobić zrzut stanu rejestrów CPU w momencie błędu.

Możemy z niego odczytać adres instrukcji, która wywołała błąd. Wywołać błąd jeszcze raz i przestepować go w disasembly. Najczęściej winny jest jakiśo zapis/odczyt pamięci. Możemy zobaczyć jaka zmienna tam jest zapisana, kto ją modyfikował i mamy rozwiązanie.

Tutaj przydaje się też mocno wiedza o wewnętrznym działaniu procesora. Niektóre operacje na pamięci triggerują błąd parę instrukcji później. Na STMach możemy podejrzeć konkretny rodzaj błędu w Hard Fault Status Register. To też nam daje dalsze wskazówki.

Inny typowy przypadek debugowy to podglądanie kodu generowanego przez kompilator. W ten sposób np. sprawdzimy łatwo, czy nasz kompilator używa instrukcji FPU. Czy działają optymalizacje kompilatora, czy nie ma undefined behavior.

Jeżeli mamy jakąś krytyczną z punktu widzenia optymalizacji funkcję możemy ją zobaczyć na disasembly i sprawdzić, gdzie tracimy czas. Zastanowić się, czy jesteśmy w stanie ją jakoś usprawnić.

Po więcej tego typu analiz odsyłam do kursu C dla Zaawansowanych.

5. Specjalne instrukcje

Czasem nasz procesor posiada instrukcje, których kompilator C normalnie nie wykorzystuje. Na przykład w Cortexach mamy cały zestaw ciekawych operacji bitowych – liczenie zer i jedynek, odwracanie bitów i bajtów w danym słowie 32-bitowym.

Niektóre procesory mają specjalne instrukcje do operacji kryptograficznych albo do wykonywania operacji wektorowych (SIMD – Single Instruction Multiple Data).

Czasem mamy też instrukcje takie jak compare and swap przydatne przy mutexach.

W każdym razie – język C i jego kompilator najczęściej nie zdają sobie z nich sprawy. Możemy ich użyć albo wywołując funkcje asemblerowe. Albo za pomocą funkcji built in kompilatora czy gotowych bibliotek. Nie muszę chyba dodawać, że twórcy tych bibliotek czy kompilatora również musieli znać asemblera, aby je napisać.

Więcej o funkcjach built in mówiłem w tym nagraniu:

A jeszcze więcej – w kursie C dla Zaawansowanych.

Pisanie kompilatorów, czy emulatorów też wymaga dogłębnej znajomości asemblera. Ale to tak niszowy temat, że na pewno nie mogę go uznać za popularne zastosowanie.

6. Security

Tutaj już sprawa wygląda inaczej. W ostatnich latach security w embedded jest gorącym tematem. A znajomość asemblera jest nieoceniona podczas reverse engineeringu czy szukania podatności.

W tym akurat nie czuję się ekspertem, ale na pewno słyszeliście o naszej polskiej aferze kolejowej.

Odsyłam do świetnej prezentacji Dragon Sectora, który tłumaczy w jaki sposób udało im się znaleźć problem. Oczywiście używali do tego reverse engineeringu i narzedzi takich jak Ghidra czy IDA czyli disasemblerów.

No i na koniec bonus…

Kiedy wydaje nam się, że chcemy użyć asemblera

Kompilator jest jakiś głupi. Mógł to przecież zoptymalizować, a tego nie zrobił. Samemu napisze to lepiej.

Chyba każdemu coś takiego kiedyś przeszło przez myśl.

Po pierwsze – jest bardzo duża szansa, że kompilator wcale nie popełnił błędu.

Mogliśmy nie przewidzieć jakiegoś przypadku brzegowego, mogliśmy mieć w kodzie Undefined Behavior. Albo po prostu nie daliśmy kompilatorowi wszystkich danych za pomocą static, const czy atrybutów gcc.

Czasem też nie zdajemy sobie sprawy z różnych ograniczeń sprzętowych np. stałe liczbowe w instrukcjach mogą być ośmio albo 16-bitowe.

A po drugie – czy na pewno napiszesz to lepiej?

Przewidzisz wszystkie przypadki brzegowe, nie popełnisz błędów, będziesz później utrzymywać ten kod w projekcie? Czy cały ten nakład pracy na pewno będzie wart oszczędności pojedynczych instrukcji?

No właśnie – dlatego zwykle to zły pomysł. Lepiej optymalizować tylko krytyczne fragmenty i stosować techniki, które dają solidne zyski. Ale to już zupełnie inny temat.

Na dzisiaj to już wszystko. Na koniec jeszcze przypominam, że pracuję właśnie nad kursem asemblera z praktycznymi przykładami na STM32. I możesz wpłynąć na treść kursu jak i materiałów na YT i blogu. Wystarczy, że wypełnisz ANKIETĘ.