Programista 95

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 70

FPGA POD LUPĄ  | UX W PODPISIE ELEKTRONICZNYM – CO JEST WAŻNE Z PERSPEKTYWY UŻYTKOWNIKA?

Index: 285358 www • programistamag • pl

Magazyn programistów i liderów zespołów IT

1/ 2021 (95)   Cena 25,90 zł (w tym VAT 8%) 


luty/marzec 2021
01010000
01110010
01101111
01100111
01110010
01100001
01101101
01101001
01110011
01110100
01100001
#
/* Wygoda a bezpieczeństwo */
Programując, nierzadko możemy spotkać  się  z interfejsami pro- główek: int strncmp(const char *s1, const char *s2, size_t n).
gramistycznymi zaprojektowanymi tak, aby były wygodne. Przykła- Funkcja ta przyjmuje wskaźniki na dwa ciągi znaków oraz liczbę bajtów
dem takiego interfejsu jest ustawianie uprawnień w systemach Linux. do porównania. Aby zatem sprawdzić, czy dany ciąg zapisany w zmien-
Uprawnienia danego pliku wyrażamy poprzez liczbę, zazwyczaj zapi- nej text zaczyna się od "hello", musimy wykonać kod strncmp(text,
saną w systemie ósemkowym, gdzie każdy bit odpowiada konkretnym "hello", 5). Można tu jednak zauważyć pewną niedogodność: pomi-
uprawnieniom dla właściciela, grupy lub innych użytkowników. I tak na mo że długość porównywanego prefiksu jest znana, to i tak musimy
przykład liczba 0740 (gdzie zero wiodące oznacza zapis ósemkowy) liczbę tę przekazać w trzecim argumencie. Niestety, w bibliotece stan-
odpowiada uprawnieniom rwxr-----. W zapisie tym r, w, x oznacza dardowej języka C nie ma funkcji służącej do sprawdzenia, czy dany
kolejno uprawnienia do odczytu (read), modyfikacji (write) i wykony- ciąg zaczyna się od innego, a zamiast tego są właśnie funkcje z rodziny
wania (execute), a kolejne trójki znaków za różne grupy użytkowników: strcmp, które mogą zostać wykorzystane w różnych sytuacjach.
właściciela, grupy, do której należy plik, a także pozostałych użytkowni- To może powodować problemy, które umożliwiają obejście jakie-
ków. Co ciekawe, niektóre narzędzia, jak na przykład program chmod, goś mechanizmu bezpieczeństwa, gdy pomylimy się w liczbie porów-
interpretują przekazaną wartość liczbową uprawnień jako liczbę ósem- nywanych bajtów. Na przykład, wykonując strncmp(path, "/home/
kową, nawet gdy liczba ta nie zaczyna się od zera. Poprzez takie udo- user/", 10), sprawdzimy, czy dana ścieżka zaczyna się od "/home/
godnienie, czyli fakt, że nie musimy pisać początkowego zera dla licz- user", bez ostatniego znaku „/”, oddzielającego katalogi. W przypad-
by w zapisie ósemkowym, łatwo o błędy. ku gdy zmienna path zawierałaby ścieżkę "/home/userX/plik", po-
W języku Python uprawnienia do pliku możemy zmienić za pomo- wyższe porównanie zwróci wynik 0, oznaczający, że zaczyna się ona
cą funkcji os.chmod, która również przyjmuje wartość uprawnień jako od "/home/user", gdy intencją było "/home/user/". Wiele tego typu
liczbę. Niestety, jeśli podczas wywoływania tej funkcji zapomnimy błędów udało mi się znaleźć podczas pracy z otwartym oprogramowa-
o początkowym zerze i napiszemy na przykład: os.chmod("folder/ niem, co wraz z wynikami opisałem w [2].
plik", 740), to zamiast ustawić uprawnienia danego pliku na 0740 Jak zaradzić przedstawionym wyżej problemom? W przypadku
(rwxr-----), ustawimy je na 01344 (-wxr--r-T), gdyż liczba 740 za- uprawnień można wykorzystać stałe zamiast liczb. W Pythonie liczbową
pisana ósemkowo to 01344. Taki kod spowoduje zatem, że plik będzie wartość uprawnień 0740 można zapisać jako stat.S_IRWXU|stat.S_
mógł być zapisywany oraz wykonywany przez jego właściciela; czytany IRGRP. Niektóre języki programowania mają obiektowy interfejs do
przez grupę oraz odczytywany przez innych użytkowników. Dodatkowo zmiany uprawnień. Na przykład w języku Java obiekty typu java.nio.
plik będzie miał ustawiony tak zwany „sticky bit” (T), o którym można file.File mają metody setReadable, setWritable oraz setEx-
przeczytać więcej w [1]. To wszystko oznacza, że zamiast jedynie wła- ecutable, nie pozwalają one jednak na ustawianie aż tak granularnych
ściciela oraz użytkowników należących do danej grupy, dostęp do pliku uprawnień, jak wyrażanie uprawnień wartością liczbową. Dodatkowo
otrzymają  również wszyscy inni użytkownicy. W przypadku gdy dany niektóre narzędzia do analizy kodu, jak na przykład staticcheck dla
plik przechowuje poufne informacje, taka sytuacja może być poważ- języka Go, znajdują wskazane problemy [3]. W przypadku drugiego
nym problemem bezpieczeństwa. Oczywiście, błąd ten można zauwa- problemu – z funkcjami porównującymi tekst – rozwiązanie jest pro-
żyć np. podczas nieudanej próby otworzenia pliku (jako jego właści- ste: warto utworzyć funkcję pomocniczą lub makro preprocesora do
ciel), ze względu na brak uprawnień do odczytu. Jednakże wielokrotnie sprawdzania, czy ciąg zaczyna się  od danego prefiksu, i używać tej
znalazłem takie lub podobne błędy w kodzie, który analizowałem pod funkcji lub makra zamiast samemu przekazywać liczbę bajtów do po-
kątem bezpieczeństwa. równania.
Innym ciekawym „interfejsem” może być porównywanie łańcuchów [1] https://pl.wikipedia.org/wiki/Sticky_bit, https://en.wikipedia.org/wiki/Sticky_bit
tekstowych w języku C, gdzie służą do tego funkcje z rodziny strcmp. W [2] https://github.com/disconnect3d/cstrnfinder
przypadku gdy chcemy sprawdzić, czy dany łańcuch zaczyna się od in- [3] https://staticcheck.io/docs/checks#SA9002
nego, możemy wykorzystać funkcję strncmp, która ma następujący na-
Dominik 'Disconnect3d' Czarnota

/* REKLAMA */
SPIS TREŚCI

01010000
BIBLIOTEKI I NARZĘDZIA
6 # Dear ImGui: pragmatyczne podejście do programowania interfejsów użytkownika 01110010
01101111
> Rafał Kocisz

18 # ptrace: implementacja debuggera pod Linuksem


> Przemysław Samsel

PROGRAMOWANIE URZĄDZEŃ MOBILNYCH


26 # Aplikacje natywne na Ubuntu Touch
> Marcin Ławicki
01100111
PROGRAMOWANIE SYSTEMOWE
38 # Pszczoła – najlepszy przyjaciel programisty
01110010
01100001
> Tomasz Duszyński

LABORATORIUM EVORAIN

01101101
46 # Sieci neuronowe w pigułce
> Piotr Woldan

LABORATORIUM TEINA
58 # UX w podpisie elektronicznym
> Katarzyna Małecka 01101001
PLANETA IT
64 # FPGA pod lupą
> Rafał Kozik
01110011
01110100
01100001
ZAMÓW PRENUMERATĘ MAGAZYNU PROGRAMISTA

Przez formularz na stronie:.............................http://programistamag.pl/typy-prenumeraty/


Na podstawie faktury Pro-forma:.........................redakcja@programistamag.pl

Prenumerata realizowana jest także przez RUCH S.A.


Zamówienia można składać bezpośrednio na stronie:.......www.prenumerata.ruch.com.pl
Pytania prosimy kierować na adres e-mail:...............prenumerata@ruch.com.pl
Kontakt telefoniczny:...................................801 800 803 lub 22 717 59 59*

*godz. 7 : 00 – 18 : 00 (koszt połączenia wg taryfy operatora)

Magazyn Programista wydawany jest przez Dom Wydawniczy Anna Adamczyk Nota prawna
Wydawca/Redaktor naczelny: Anna Adamczyk (annaadamczyk@programistamag.pl). Redaktor prowadzący: Redakcja zastrzega sobie prawo do skrótów i opracowań tekstów oraz do zmiany planów
Mariusz „maryush” Witkowski (mariuszwitkowski@programistamag.pl). Korekta: Tomasz Łopuszański. Kierownik wydawniczych, tj. zmian w zapowiadanych tematach artykułów i terminach publikacji, a także
produkcji/DTP: Krzysztof Kopciowski. Dział reklamy: reklama@programistamag.pl, tel. +48 663 220 102, nakładzie i objętości czasopisma.
tel. +48 604 312 716. Prenumerata: prenumerata@programistamag.pl. Współpraca: Michał Bartyzel, Mariusz O ile nie zaznaczono inaczej, wszelkie prawa do materiałów i znaków towarowych/firmowych
Sieraczkiewicz, Dawid Kaliszewski, Marek Sawerwain, Łukasz Mazur, Łukasz Łopuszański, Jacek Matulewski, zamieszczanych na łamach magazynu Programista są zastrzeżone. Kopiowanie i rozpowszechnianie
Sławomir Sobótka, Dawid Borycki, Gynvael Coldwind, Bartosz Chrabski, Rafał Kocisz, Michał Sajdak, Michał ich bez zezwolenia jest Zabronione.
Bentkowski, Paweł „KrzaQ” Zakrzewski, Radek Smilgin, Jarosław Jedynak, Damian Bogel (https://kele.codes/), Redakcja magazynu Programista nie ponosi odpowiedzialności za szkody bezpośrednie
Michał Zbyl. Adres wydawcy: Dereniowa 4/47, 02-776 Warszawa. Druk: http://www.edit.net.pl/, Nakład: 4500 egz. i pośrednie, jak również za inne straty i wydatki poniesione w związku z wykorzystaniem informacji
Projekt okładki: Przemysław Banasiewicz prezentowanych na łamach magazy­nu Programista.
BIBLIOTEKI I NARZĘDZIA

Dear ImGui: pragmatyczne podejście do programowania


interfejsów użytkownika
Kiedyś ktoś nieco przekornie stwierdził, że lenistwo jest motorem wszelkiego postępu. Oczy-
wiście jest w tym stwierdzeniu sporo przesady, ale też można w nim znaleźć ziarno prawdy.
Otóż istnieje pewna kategoria lenistwa, którą ja nazywam „lenistwem pragmatycznym”. Pod-
stawową zasadą pragmatycznego lenia jest słynna reguła DRY (skrót od angielskich słów
„don’t repeat yourself”, czyli: „nie powtarzaj się”). I tutaj należy uczciwie przyznać: ten rodzaj
lenistwa często prowadzi do zwiększenia efektywności pracy i do powstawania ciekawych
rozwiązań. Dziś chciałbym zaprezentować czytelnikowi Dear ImGui: bibliotekę służącą do
tworzenia interfejsów użytkownika, która powstała w duchu tak właśnie postrzeganego leni-
stwa. Jeśli chcesz się przekonać, co z tego wynikło, to zapraszam do lektury!

DAWNO, DAWNO TEMU… 1. Budowanie drzewa kontrolek (komponentów interfejsu użyt-


kownika: przyciski, pola edycyjne, pola wyboru itd.); proces
Mniej więcej w latach 2008–2013 miałem okazję pracować nad sze- tworzenia takiego drzewa może przebiegać na różne sposoby:
regiem projektów związanych z tworzeniem interfejsów użytkownika czasami programista buduje je ręcznie, innym razem tworzo-
dla gier oraz dla narzędzi wspomagających ich tworzenie. I pomimo ne jest ono na podstawie jakiejś zewnętrznej definicji – często
tego, że z czasem miałem coraz więcej doświadczenia związane- wygenerowanej przez zewnętrzne narzędzie służące do projek-
go z realizacją tego typu projektów, to ciągle byłem niezadowolony towania interfejsu.
z efektów mojej pracy. Przy każdym kolejnym przedsięwzięciu, które 2. Aktualizacja drzewa kontrolek: krok ten wykonywany jest
tworzyłem, część związana z interfejsem użytkownika – nawet pomi- w głównej pętli programu i z punktu widzenia programisty
mo tego, że moja wiedza i umiejętności z czasem rosły – koniec koń- aplikacji jest on zazwyczaj kompletnie nieprzezroczysty – w tle
ców z pięknie uporządkowanej, czystej grządki zamieniała się w pole dzieje się jakaś „magia”, dzięki której kontrolki odpowiednio
pełne chwastów. Byłem coraz bardziej sfrustrowany – praca w takim reagują na zdarzenia (np. naciśnięcia klawisza lub kliknięcia
środowisku nie sprawiała mi przyjemności. Tak się złożyło, że w oko- myszką na ekranie).
licach 2013 roku zmieniłem pracę i skupiłem się na innych wyzwa- 3. Obsługa funkcji zwrotnych: funkcje te wywoływane są w od-
niach. Pozostał jednak pewien niedosyt. Olśnienie przyszło kilka lat powiedzi na pojawiające się zdarzenia. Na przykład kliknięcie
później, kiedy dość przypadkowo natrafiłem na niepozorną biblio- myszką na fragmencie okna, gdzie znajduje się przycisk, powo-
tekę: Dear ImGui. Nie jest ona bynajmniej panaceum na wszystkie duje wywołanie funkcji zwrotnej pokroju OnButtonClicked()
bolączki związane z budowaniem GUI, jednakże leżąca u jej podstaw z odpowiednimi parametrami (np. ID kontrolki), dzięki którym
koncepcja programowania interfejsów użytkownika w trybie natych- programista może zorientować się, który przycisk został akty-
miastowym (ang. immediate mode GUI) jest genialna w swojej pro- wowany, i podjąć odpowiednią akcję. Gdy mamy do czynienia
stocie, a jednocześnie rozwiązuje szereg istotnych problemów w tej z dynamicznym interfejsami, to wewnątrz funkcji zwrotnych
dziedzinie. Dla mnie zrozumienie tej koncepcji stanowiło moment często umieszcza się kod modyfikujący drzewo kontrolek (two-
pewnego myślowego przełomu, olśnienia. Zrozumiałem wtedy, że rzenie/usuwanie/aktywowanie/dezaktywowanie kontrolek itp.).
powodem opisanego wyżej stanu rzeczy nie był brak wiedzy czy sta-
ranności. Problem tkwił w tym, że koncepcja leżąca u podstaw ar- Już czytając powyższy opis, trudno nie oprzeć się wrażeniu, że tak
chitektury bibliotek służących do tworzenia graficznych interfejsów skonstruowany system może być trudny i mało intuicyjny w obsłu-
użytkownika, z których korzystałem, generowała zbyt duży poziom dze ze względu na decentralizację logiki obsługującej drzewo kontro-
złożoności. W efekcie stosowanie rozwiązań opartych na tej koncep- lek. Śledzenie i debugowanie tej logiki (szczególnie w świetle braku
cji kończyło się powstawaniem rozwiązań nazbyt skomplikowanych, transparencji na poziomie aktualizacji drzewa kontrolek, co w prak-
pełnych nadmiarowości oraz redundancji. Zacznijmy więc od po- tyce oznacza, że jesteśmy w stanie przechwytywać tylko wyrwane
czątku i omówmy najpierw pokrótce... z kontekstu funkcje zwrotne) bywa koszmarem (kto tego sam nie
doświadczył, ten nie zrozumie...). Dramat zaczyna się w momencie,

...KLASYCZNE PODEJŚCIE DO TWORZENIA gdy projekt rozwijany w ten sposób przekroczy pewną masę krytycz-
ną. Wtedy poziomu złożoności nie da się już okiełznać (warto w tym
INTERFEJSÓW UŻYTKOWNIKA przypadku pamiętać, że w większych projektach kod taki jest zazwy-
Praca z typową biblioteką służącą do budowania interfejsów użyt- czaj rozwijany przez więcej niż jedną osobę). Kończy się to zazwyczaj
kownika składa się z trzech kroków: katastrofą, wyrywaniem włosów z głowy oraz powtarzaniem pytania:

<6> {  1 / 2021 < 95 >  }


BIBLIOTEKI I NARZĘDZIA

CZY NIE DAŁOBY SIĘ ZROBIĆ TEGO Listing 2. Przykładowy interfejs biblioteki GUI działającej w trybie natych-
miastowym
PROŚCIEJ?
namespace Gui
Omówione wyżej problemy związane z budowaniem graficznych in- {
void Label(int x, int y,
terfejsów użytkownika już od bardzo dawna spędzały sen z powiek const char* text);
wielu programistom. Problem ten był widoczny szczególnie wyraźnie bool Button(int x, int y,
int width, int height,
w środowisku twórców gier, a jeszcze bardziej – w części tego środo- const char* text);
wiska, które zajmowało się budowaniem narzędzi wspomagających bool RadioButton(bool active,
int x, int y,
tworzenie gier. Branża gamedev jest mocno specyficzna i bardzo
int width, int height,
wymagająca. Wymagania zmieniają się tutaj czasami z godziny na const char* text);
godzinę. Jeśli stosowana technologia/silnik/architektura/framework bool CheckBox(bool active,
int x, int y,
(niepotrzebne skreślić) jest niedostatecznie elastyczna, to kończy się int width, int height,
na zszarganych nerwach, nocach zarwanych w biurze oraz innych const char* text);
bool TextInput(int x, int y,
nieprzyjemnych sytuacjach. Z drugiej strony mówi się, że potrzeba std::string& string);
jest matką wynalazków. Może to właśnie dlatego w środowisku ga- };
medev zrodziła się koncepcja tzw. Budowania Graficznych Interfej-
sów Użytkownika w Trybie Natychmiastowym (ang. Immediate Mode Pomysł generalnie polega na tym, że poszczególne elementy interfej-
GUI). Otóż ktoś wpadł na dość szalony pomysł: dlaczego nie spróbo- su graficznego (zwane potocznie widgetami) reprezentowane są przez
wać programowania GUI w sposób bezstanowy – na podobnej zasa- funkcje (nie przez obiekty!), które jako parametry przyjmują właści-
dzie, jak renderuje się grafikę przy pomocy niskopoziomowych API wości tychże elementów, takie jak pozycja, rozmiar czy tekst. Przy-
pokroju OpenGL, DirectX czy Vulkan. Rysowanie grafiki w takim kładowy kod realizujący interfejs użytkownika w oparciu o takie API
trybie, zwanym potocznie Trybem Natychmiastowym (ang. Immedia- mógłby wyglądać tak jak w Listingu 3.
te Mode), polega na sekwencyjnym wykonywaniu operacji rysowania
Listing 3. Funkcja budująca fragment interfejsu użytkownika za pomocą API
wywoływanych ramka po ramce. W Listingu 1 pokazany jest przy- przedstawionego w Listingu 2
kład wykorzystania takiego trybu przy użyciu biblioteki OpenGL.
void DoSomeGui()
Listing 1. Fragment kodu renderujący trójkąt w trybie natychmiastowym za {
Gui::Label(10, 10,
pomocą biblioteki OpenGL
"Hello, please enter some "
"text and press OK.");
glBegin(GL_POLYGON);
glColor3f(1, 0, 0); std::string text;
glVertex3f(-0.6, -0.75, 0.5); Gui::TextInput(10, 30, text);
glColor3f(0, 1, 0);
glVertex3f(0.6, -0.75, 0); if(Gui::Button(64, 50, 60, 20, "OK"))
glColor3f(0, 0, 1); {
glVertex3f(0, 0.75, 0); // … obsłuż przyciśnięcie klawisza
glEnd(); }
}

W efekcie działania tego programu uzyskamy obraz przedstawiony


na Rysunku 1. No dobrze… – myślisz sobie zapewne – pytanie tylko, jak można
zaimplementować funkcje do obsługi poszczególnych kontrolek,
np. Gui::Button(). Okazuje się, że można to zrobić bardzo prosto
(Listing 4).

Listing 4. Poglądowa implementacja funkcji Gui::Button()

bool Gui::Button(int x, int y,


int width, int height,
const char* text);
{
Gfx::DrawRect(x, y, width, height);
Gfx::DrawText(x, y, text);

return Input::Mouse::LeftButtonPressed() &&


Input::Mouse::PosX() >= x &&
Input::Mouse::PosY() >= y &&
Input::Mouse::PosX() < (x + width) &&
Input::Mouse::PosY() < (y + height);
}
Rysunek 1. Trójkąt narysowany przez program z Listingu 1

Spróbujmy sobie wyobrazić, jak mógłby wyglądać interfejs bibliote- Oczywiście, jest to bardzo uproszczona, poglądowa implementacja,
ki przeznaczonej do tworzenia graficznych interfejsów użytkownika, zakładająca istnienie dodatkowych API służących do rysowania oraz
działającej w trybie natychmiastowym. W Listingu 2 możesz zapo- odczytywania stanu urządzeń wejścia (a konkretnie: myszki). Mam
znać się z przykładową definicją takiego interfejsu. jednak nadzieję, że pokazuje ona dobitnie istotę pomysłu…

<8> {  1 / 2021 < 95 >  }


/ Dear ImGui: pragmatyczne podejście do programowania interfejsów użytkownika /

DEAR IMGUI: TRYUMF PRAGMATYZMU ZANIM ZACZNIEMY PRACĘ


Ktoś kiedyś powiedział, że świetny pomysł bez konkretnej realiza- Na początek kilka słów na temat integracji biblioteki ImGui. Pierw-
cji wart jest funta kłaków. Czy w każdym przypadku jest to prawdą, szy, najważniejszy fakt: ImGui zaimplementowana jest w języku C++.
długo można by debatować. Faktem pozostaje to, że koncepcja Im- Implementacja ta podzielona jest na dwie główne składowe:
mediate Mode GUI nie zdobyłaby zapewne tak dużej popularności » Zestaw implementacji komponentów interfejsu użytkownika,
bez dobrych bibliotek pozwalających stosować ją w praktyce. Rzecz zrealizowany oczywiście w duchu koncepcji Immediate Mode
w tym, że większość twórców bibliotek wspomagających tworzenie GUI; kod znajdujący się w tej części biblioteki z założenia jest
interfejsów użytkownika (szczególnie w grach) staje przed szeregiem niezależny od platformy.
bardzo trudnych problemów, które należy rozwiązać, aby ich dzieło » Zestaw implementacji tzw. backendów, tj. kodu pośredniczące-
mogło zyskać popularność i zachęcić szersze grono odbiorców (czy- go, który umożliwia integrację Dear ImGui z szeregiem plat-
taj: programistów) do jej używania: form programowo-sprzętowych, jak i popularnych potoków
» Przenośność: biblioteka działająca tylko na jednej wybranej graficznych.
platformie sprzętowo-programowej ma małe szanse na zyskanie
większego zainteresowania. W momencie pisania tego artykułu oficjalnie wspierane platformy to:
» Elastyczność: w świecie gamedev roi się od przeróżnych silni- » Allegro5
ków i frameworków służących do tworzenia gier – im bardziej » GLFW
dana biblioteka będzie elastyczna w zakresie integracji z różny- » GLUT
mi silnikami, tym większe szanse, że zyska popularność. » Marmalade
» Prostota: zasada KISS (Keep It Simple, Stupid) rządzi – jeśli » OSX
stworzysz wspaniałą bibliotekę, ale jej użycie wymagać będzie » Win32
na wejściu lektury opasłej księgi, to mało kto z niej skorzysta. » SDL2
Obsługiwane przez Dear ImGui potoki graficzne to:
Wierz mi, zbudowanie biblioteki, która spełnia powyższe warunki, » DirectX od wersji 9 do wersji 12
jest szalenie trudne. Autorowi Dear ImGui (a jest nim Omar Cor- » OpenGL – wersje 2 i 3
nut) udało się sprostać temu wyzwaniu. ImGui jest połączeniem » Vulkan
świetnej idei (programowania interfejsów użytkownika w trybie » Metal
natychmiastowym) podanej w postaci przenośnej, elastycznej i pro-
stej biblioteki. ImGui to prawdziwy tryumf pragmatyzmu, co widać Co więcej, Interfejs programistyczny (API) backendu ImGui jest bar-
po rewelacyjnym feedbacku, który spływa od rzeszy zachwyconych dzo prosty i klarowny, dzięki czemu stosunkowo łatwo można dodać
użytkowników. Przyjrzyjmy się zatem, co oferuje nam ta obiecująca obsługę alternatywnych platform czy też potoków graficznych. Przy-
biblioteka. Sprawdźmy, co na temat pisze sam jej autor. Poniżej cytat kładami mogą być tutaj następujące biblioteki:
z pliku README.md biblioteki umieszczonego w jej repozytorium » SFML
pod adresem https://github.com/ocornut/imgui. » Sokol
» Bgfx
Dear ImGui jest minimalistyczną biblioteką służącą do budowa-
nia graficznego interfejsu użytkownika w języku C++. Dostarcza
Osobiście miałem okazję integrować Dear ImGui z moim autorskim
zoptymalizowaną listę wierzchołków, które można renderować
w dowolnym momencie w aplikacji obsługującej potok 3D. Jest silnikiem (wspierającym platformy desktopowe i mobilne) i mogę
szybka, przenośna, niezależna od renderera i samowystarczalna zaświadczyć, iż proces ten był zadziwiająco prosty i bezproblemowy.
(ze względu na brak zewnętrznych zależności). Zakładam, że stosunkowo łatwo da się połączyć ImGui z każdym sil-
nikiem i potokiem graficznym (jak twierdzi sam jej autor: każda plat-
Dear ImGui zostało zaprojektowane tak, aby zapewnić elastycz- form pozwalająca wyświetlać oteksturowane trójkąty będzie w stanie
ność i umożliwić programistom budowanie narzędzi służących
do tworzenia treści oraz do wizualizacji/debugowania (w prze- obsłużyć tę bibliotekę).
ciwieństwie do UI dla przeciętnego użytkownika końcowego). W W celu przygotowania przykładów na potrzeby niniejszego arty-
tym celu preferuje prostotę i produktywność. Z tego też względu kułu postanowiłem skorzystać z biblioteki SFML. Biblioteka ta do-
brakuje jej pewnych funkcji, które zwykle można znaleźć w bar- starcza zarówno warstwę obsługi sprzętu (ang. hardware abstraction
dziej zaawansowanych bibliotekach.
layer), jak i warstwę renderingu (w oparciu o bibliotekę OpenGL).
W celu połączenia bibliotek SFML oraz Dear ImGui użyłem biblio-
Dear ImGui szczególnie nadaje się do integracji w silnikach gier,
aplikacjach 3D czasu rzeczywistego, aplikacjach pełnoekrano- teki ImGui-SFML (https://github.com/eliasdaler/imgui-sfml). Aby uzy-
wych, aplikacjach wbudowanych lub wszelkich aplikacjach na skać zamierzony efekt, wykonałem następujące kroki:
platformach konsolowych, gdzie funkcje systemu operacyjnego są » połączyłem mój programu z biblioteką SFML (pobrałem ją ze
niestandardowe. strony https://www.sfml-dev.org/download/sfml/2.5.1/),
» dodałem do projektu źródła biblioteki ImGui (część niezależ-
ną od platformy oraz backend obsługujący potok graficzny
OpenGL 3),

{  WWW.PROGRAMISTAMAG.PL  } <9>
BIBLIOTEKI I NARZĘDZIA

» dodałem do projektu źródła biblioteki ImGui-SFML, Czas zaprezentować naszego głównego bohatera w akcji. W tym
» dodałem do projektu źródła biblioteki loadera biblioteki OpenGL 3 celu spójrzmy w Listing 6.
wygenerowane za pomocą narzędzia Glad (https://glad.dav1d.de/).
Listing 6. SFML + ImGui: pierwsze podejście

W efekcie byłem w stanie uruchomić program zbudowany w oparciu #include <SFML/Graphics.hpp>

o SFML z podłączoną biblioteką Dear ImGui, o czym w szczegółach #include "imgui.h"


#include "imgui-SFML.h"
opowiem w kolejnym punkcie.
int main()
{
HELLO, DEAR IMGUI! sf::RenderWindow window(
sf::VideoMode(800, 600),
"ImHello");
W pierwszej kolejności napiszemy prosty program w oparciu o API
ImGui::SFML::Init(window);
SFMLa wyświetlający logo tej biblioteki w okienku z czarnym tłem:
char windowTitle[255] = "ImGui + SFML";
takie SFMLowe „Hello, World!”. Kod programu przedstawiony jest
window.setVerticalSyncEnabled(true);
w Listingu 5, zaś efekt działania programu znajduje się na Rysunku 2. window.setTitle(windowTitle);
window.resetGLStates();
Listing 5. Kod źródłowy prostej aplikacji SFML
sf::Texture texture;
#include <SFML/Graphics.hpp> if(!texture.loadFromFile("SFML_Logo.png"))
return EXIT_FAILURE;
int main()
{ sf::Sprite sprite(texture);
sf::RenderWindow window( sprite.setPosition(
sf::VideoMode(800, 600), (800-sprite.getTexture()->getSize().x)/2.f,
"ImHello"); (600-sprite.getTexture()->getSize().y)/2.f);

window.setVerticalSyncEnabled(true); sf::Clock clock;


window.setTitle("ImHello");
while(window.isOpen())
sf::Texture texture; {
if(!texture.loadFromFile("SFML_Logo.png")) sf::Event event;
return EXIT_FAILURE;
while(window.pollEvent(event))
sf::Sprite sprite(texture); {
sprite.setPosition( ImGui::SFML::ProcessEvent(event);
(800-sprite.getTexture()->getSize().x)/2.f,
if(event.type == sf::Event::Closed)
(600-sprite.getTexture()->getSize().y)/2.f);
window.close();
while(window.isOpen()) }
{
ImGui::SFML::Update(
sf::Event event;
window,
while(window.pollEvent(event)) clock.restart());
{
ImGui::Begin("ImGui Window");
if(event.type == sf::Event::Closed)
window.close(); if(ImGui::InputText("Window title", windowTitle, 255))
} window.setTitle(windowTitle);
window.clear(); ImGui::End();
window.draw(sprite);
window.display(); window.clear();
} window.draw(sprite);

return EXIT_SUCCESS; ImGui::SFML::Render(window);


}
window.display();
}

ImGui::SFML::Shutdown();

return EXIT_SUCCESS;
}

Jak można zauważyć, w tej wersji programu pojawiło się kilka zmian.
Poniżej krótkie ich podsumowanie:
» Na początku programu dodaliśmy nagłówki biblioteki Dear Im-
Gui oraz ImGui-SFML.
» Na początku programu umieściliśmy wywołanie funkcji od-
powiedzialnej za inicjalizację biblioteki ImGui w aplikacji
SFML, przekazując do niej obiekt reprezentujący okno aplikacji
(ImGui::SFML::Init(window)).
» Na końcu programu umieściliśmy kod zwalniający zasoby bi-
blioteki ImGui (ImGui::SFML::Shutdown()).
» Aby biblioteka ImGui działała poprawnie w aplikacji zbudo-
Rysunek 2. Prosta aplikacja SFML wanej w oparciu o SFML, konieczne jest zresetowanie stanu

<10> {  1 / 2021 < 95 >  }


/ Dear ImGui: pragmatyczne podejście do programowania interfejsów użytkownika /

biblioteki OpenGL (która w SFML odpowiada za rendero- Kliknij teraz w pole edycyjne i zacznij pisać. Widzisz, co się dzie-
wanie); w tym celu wywołujemy odpowiednią metodę klasy je? Przy każdej zmianie zawartości pola edycyjnego zmienia się rów-
sf::RenderWindow: window.resetGLStates(). nież wartość nazwy głównego okna aplikacji (Rysunek 5).
» W pętli głównej aplikacji, w sekcji obsługującej zdarzenia, wy-
wołujemy odpowiednią funkcję przekazującą zdarzenia do bi-
blioteki ImGui (ImGui::SFML::ProcessEvent(event)).
» W pętli głównej wywołujemy funkcję ImGui::SFML::Update().
» W pętli głównej aplikacji wywołujemy szereg funkcji z biblioteki
ImGui w celu zbudowania pożądanego interfejsu użytkownika; o
ile wcześniej wymienione wywołania funkcji były specyficzne dla
konkrentego backendu (w tym przypadku: SFML), o tyle kod, któ-
ry umieścimy pomiędzy parą wywołań funkcji ImGui::Begin()
oraz ImGui::End(), będzie niezależny od platformy.
» W naszym przykładzie użyliśmy funkcji ImGui::InputText(),
która pozwala stworzyć tekstowe pole edycyjne.

Efekt uruchomienia programu przedstawionego w Listingu 6 poka- Rysunek 5. SFML+ImGui: pole edycji tekstu w akcji
zany jest na Rysunku 3.
Prześledźmy po kolei, co się tutaj dzieje. W kodzie pokazanym w Li-
stingu 2 zdefiniowaliśmy tablicę znaków służącą do przechowywania
nazwy okienka:

char windowTitle[255] = "ImGui + SFML";

Tablica ta, wraz z wartością określającą jej długość, przekazana zo-


stała do funkcji ImGui::InputText(). Jak można się domyślić, we-
wnątrz tej funkcji znajduje się kod, który odpowiednio modyfikuje
zawartość wspomnianej tablicy w zależności od interakcji ze strony
użytkownika. Jeśli w trakcie wywołania funkcji zawartość tablicy
uległa zmianie, to zwraca ona wartość true. Dzięki temu wywołanie
funkcji możemy umieścić w instrukcji warunkowej i zareagować od-
powiednio na zmianę danych. Spróbujmy zmodyfikować teraz dzia-
łanie naszej aplikacji w taki sposób, aby zmiana nazwy okna wykony-
wana była tylko na wyraźne żądanie użytkownika (Listing 7).
Rysunek 3. SFML+ImGui: pierwsze okienko (w postaci zwiniętej)
Listing 7. SFML+ImGui: drugie podejście

Jak widać na Rysunku 3, w lewym górnym rogu ekranu pojawia się #include <SFML/Graphics.hpp>

tajemnicza belka opisana jako „ImGui Window”. Po najechaniu kur- #include "imgui.h"
#include "imgui-SFML.h"
sorem na ikonkę strzałki na tej belce pojawi się podświetlenie, nato-
int main()
miast po kliknięciu na tę ikonkę możemy podziwiać swoje pierwsze {
okienko Dear ImGui (Rysunek 4). sf::RenderWindow window(
sf::VideoMode(800, 600),
"ImHello");

ImGui::SFML::Init(window);

char windowTitle[255] = "ImGui + SFML";

window.setVerticalSyncEnabled(true);
window.setTitle(windowTitle);
window.resetGLStates();

sf::Texture texture;
if(!texture.loadFromFile("SFML_Logo.png"))
return EXIT_FAILURE;

sf::Sprite sprite(texture);
sprite.setPosition(
(800-sprite.getTexture()->getSize().x)/2.f,
(600-sprite.getTexture()->getSize().y)/2.f);

sf::Clock clock;

while(window.isOpen())
{
Rysunek 4. SFML+ImGui: pierwsze okienko (w postaci rozwiniętej) sf::Event event;

{  WWW.PROGRAMISTAMAG.PL  } <11>
BIBLIOTEKI I NARZĘDZIA

while(window.pollEvent(event)) uwzględnieniem informacji zwrotnych od sporej liczby użytkowni-


{ ków. Przykład, który pokazałem w poprzednim punkcie, to zaledwie
ImGui::SFML::ProcessEvent(event);
malutki strzępek możliwości tej biblioteki. Możesz łatwo przekonać
if(event.type == sf::Event::Closed)
window.close(); się, co oferuje ImGui. W tym celu wystarczy uruchomić kod przed-
} stawiony w Listingu 8.
ImGui::SFML::Update(
window, Listing 8. Dear ImGui Demo
clock.restart());
#include <SFML/Graphics.hpp>
ImGui::Begin("ImGui Window");
#include "imgui.h"
ImGui::InputText("Window title", windowTitle, 255); #include "imgui-SFML.h"
if(ImGui::Button("Update window title")) int main()
window.setTitle(windowTitle); {
sf::RenderWindow window(
ImGui::End();
sf::VideoMode(800, 600),
window.clear(); "ImHello");
window.draw(sprite);
ImGui::SFML::Init(window);
ImGui::SFML::Render(window);
char windowTitle[255] = "ImGui + SFML";
window.display();
window.setVerticalSyncEnabled(true);
}
window.setTitle(windowTitle);
ImGui::SFML::Shutdown(); window.resetGLStates();

return EXIT_SUCCESS; sf::Clock clock;


}
while(window.isOpen())
{
sf::Event event;
Efekt działania tego programu pokazany jest na Rysunku 6. Analizu-
while(window.pollEvent(event))
jąc kod umieszczony w Listingu 7, można zauważyć, że wywołanie {
funkcji ImGui::InputText() nie jest już otoczone instrukcją warun- ImGui::SFML::ProcessEvent(event);

kową. Pojawił się za to nowy element w postaci wywołania funkcji if(event.type == sf::Event::Closed)
window.close();
ImGui::Button(). Funkcja ta, jak się zapewne domyślasz, dodaje }
do naszego okienka przycisk o zadanej nazwie (przekazanej jako ar- ImGui::SFML::Update(
gument do funkcji). Wywołanie ImGui::Button() zwraca wartość window,
clock.restart());
true w sytuacji, kiedy użytkownik kliknie na dany przycisk. Wyniko-
ImGui::ShowDemoWindow();
wy program działa tak jak to sobie założyliśmy: nazwa okienka zmie-
ni się na zawartość wpisaną w polu edycyjnym, ale tylko wtedy, gdy window.clear();

użytkownik wciśnie przycisk "Update window title". ImGui::SFML::Render(window);

Mam nadzieję, że ten krótki, poglądowy przykład pozwolił ci po- window.display();


}
czuć klimat biblioteki ImGui i że uczucie to było pozytywne. Zapew-
niam, że dalej będzie tylko lepiej! ImGui::SFML::Shutdown();

return EXIT_SUCCESS;
}

Kluczowym elementem w powyższym listingu jest wywołanie funkcji


ImGui::ShowDemoWindow(). Funkcja ta wyświetla bardzo rozbudo-
wane demo prezentujące możliwości biblioteki. Uruchomiony pro-
gram z Listingu 8 pokazany jest na Rysunku 7.

Rysunek 6. SFML+ImGui: pole edycji tekstu i przycisk

MORZE MOŻLIWOŚCI
Warto w tym miejscu podkreślić, że Dear ImGui to bardzo dojrza-
ła i rozbudowana biblioteka, rozwijana przez dobrych kilka lat z Rysunek 7. Dear ImGui Demo: główne okienko

<12> {  1 / 2021 < 95 >  }


/ Dear ImGui: pragmatyczne podejście do programowania interfejsów użytkownika /

Pokazana tutaj lista daje dobry pogląd na możliwości tej biblioteki: » tabele (ich obsługę wprowadzono w wersji 1.80 biblioteki, która
» bogaty zestaw kontrolek, między innymi: została wypuszczona w trakcie prac nad przygotowaniem ni-
ǿ elementy podstawowe (różnego rodzaju przyciski, pola niejszego artykułu),
edycji itd.), » bogaty zestaw opcji konfiguracyjnych (stylowanie kontrolek,
ǿ listy (zarówno płaskie, jak i zagnieżdżone), wygląd okienka, interakcja z użytkownikiem).
ǿ pola tekstowe,
ǿ obrazki, Gorąco polecam uruchomienie i przeklikanie demonstracji bibliote-
ǿ rozwijane nagłówki, ki (program przedstawiony w Listingu 8). Dla osób, które nie mają
ǿ pola combo, takiej możliwości, na Rysunku 8 pokazana jest mała próbka tego, co
ǿ pola edycji tekstu, oferuje Dear ImGui. Świetną sprawą jest też możliwość przejrzenia
ǿ wykresy, kodu funkcji ImGui::ShowDemoWindow(). Jej implementację można
ǿ kontrolki wyboru koloru. znaleźć w pliku imgui_demo.cpp, który jest dołączony do biblioteki.
» kontrola układu (ang. layout) oraz przewijania okienek,
» okienka dialogowe,
ZASTOSOWANIA
» menu,
» filtry, Na początek kilka słów na temat moich własnych zastosowań bibliote-
» kolumny, ki Dear ImGui. Dla jasności, na samym początku przyznam się otwar-

Rysunek 8. Przykład możliwości biblioteki Dear ImGui (źródło: https://github.com/ocornut/imgui)

{  WWW.PROGRAMISTAMAG.PL  } <13>
BIBLIOTEKI I NARZĘDZIA

cie: nie uważam się za wybitnie zaawansowanego użytkownika tej bi- » Tracy (https://github.com/wolfpld/tracy) – zaawansowany profiler
blioteki. Korzystam z niej najczęściej przy tworzeniu gier w oparciu o działający w czasie rzeczywistym z rozdzielczością rzędu nano-
stworzony przeze mnie silnik (przy czym proces integracji był bardzo sekund i z opcją telemetrii, przeznaczony dla gier i aplikacji in-
prosty). Najczęściej używam tej biblioteki do debugowania, do edy- teraktywnych (Rysunki 9 i 10).
cji właściwości gry w czasie wykonania oraz do testowania skryptów » Mosaic (https://github.com/d3cod3/Mosaic) – platforma do kre-
pisanych w języku Lua. Niby proste rzeczy, ale dla mnie bardzo przy- atywnego kodowania w trybie wizualnym (Rysunki 11, 12 i 13).
datne. I wcale nie takie łatwe do samodzielnej implementacji… Warto » NVIDIA Omniverse (https://developer.nvidia.com/nvidia-omniver-
jednak wspomnieć, że ludzie tworzą przy pomocy ImGui niesamowi- se-platform) – potężna, wieloprocesorowa platforma do symula-
cie rozbudowane, potężne narzędzia. Oto kilka przykładów: cji i współpracy w czasie rzeczywistym, przeznaczona dla po-

Rysunek 9. Tracy (ekran 1) (źródło: https://github.com/wolfpld/tracy)

Rysunek 10. Tracy (ekran 2) (źródło: https://github.com/wolfpld/tracy)

<14> {  1 / 2021 < 95 >  }


/ Dear ImGui: pragmatyczne podejście do programowania interfejsów użytkownika /

Rysunek 11. Mosaic (ekran 1) (źródło: https://mosaic.d3cod3.org/)

Rysunek 12. Mosaic (ekran 2) (źródło: https://mosaic.d3cod3.org/)

Rysunek 13. Mosaic (ekran 3) (źródło: https://mosaic.d3cod3.org/)

{  WWW.PROGRAMISTAMAG.PL  } <15>
BIBLIOTEKI I NARZĘDZIA

Rysunek 14. NVIDIA Omniverse (ekran 1) (źródło: https://developer.nvidia.com/nvidia-omniverse-platform)

Rysunek 15. NVIDIA Omniverse (ekran 2) (źródło: https://developer.nvidia.com/nvidia-omniverse-platform)

toków produkcyjnych 3D, oparta na uniwersalnym opisie sceny daje ona olbrzymi potencjał, szereg możliwości konfiguracji, a także
firmy Pixar i technologii NVIDIA RTX (Rysunki 14 i 15). opcje modyfikacji i rozszerzania funkcjonalności. Liberalna licencja
» Niezliczona ilość edytorów zintegrowanych z przeróżnymi sil- biblioteki (MIT) pozwala wykorzystać ją zarówno w otwartych, jak
nikami służącymi do tworzenia gier, narzędzi, programów do i w zamkniętych projektach. Liczna i otwarta społeczność użytkow-
symulacji oraz innych aplikacji – wiele z nich można obejrzeć ników Dear ImGui stanowi gwarancję, że łatwo znajdziesz infror-
w galerii biblioteki ImGui dostępnej pod adresem https://github. macje na jej temat, przykładowe fragmenty kodu itp. Jeśli wcześniej
com/ocornut/imgui/labels/gallery. nie miałeś/aś styczności z tą biblioteką, to gorąco polecam bliższe się
z nią zapoznanie – na pewno nie pożałujesz!

PODSUMOWANIE
Biblioteka Dear ImGui to rewelacyjne i sprawdzone w boju narzę-
RAFAŁ KOCISZ
dzie, bardzo wysoko oceniane przez twórców, obok którego nie da rafal.kocisz@gmail.com
się przejść obojętnie, gdy ktoś ma do czynienia z programowaniem Od prawie dwudziestu lat pracuje w branży zwią-
graficznych interfejsów użytkownika. Niski próg wejścia oraz łatwość zanej z produkcją oprogramowania. Aktualnie
zatrudniony w roli Kierownika Portfela Projektów
używania sprawiają, że bibliotekę tę mogą z powodzeniem wykorzy-
w firmie intive.
stywać zarówno hobbyści, jak i profesjonaliści. Dla tych ostatnich

<16> {  1 / 2021 < 95 >  }


PO CO CI OWOCOWE
CZWARTKI, JAK MOŻESZ
ZMIENIAĆ ŚWIAT?
Co sprawia, że jesteś zainteresowany ofertą pracy?
Czy wystarczą elastyczne godziny pracy, prywatna
opieka medyczna w pakiecie premium lub słynne
owocowe czwartki, by dana propozycja wydawała
się warta uwagi? A może decydują perspektywy za-
wodowe i wizja realnego udziału w tworzeniu roz-
wiązań, które rewolucjonizują świat?

O benefitach pracowniczych mówi się dużo, zwłaszcza w kontekście


branży IT. Przekazywane opowieści o wygórowanych oczekiwaniach
programistów przerodziły się w wiele rekrutacyjnych legend, które GlobalLogic realizuje w tym momencie kilka dużych
projektów, do których trwają rekrutacje.
obecnie krążą po Internecie. Poszukiwanie pracy ostatecznie jed-
nak sprowadza się do podejmowania trudnych decyzji, które wpłyną Możesz włączyć się w tworzenie nowej generacji
produktu medycznego, jakim jest pompa insulinowa,
na nasze życie. Dlatego też warto spojrzeć poza listę benefitów i zgłę-
i odmienić życie milionów osób chorych na cukrzycę
bić informacje ukryte pod tajemniczą pozycją „udział w atrakcyjnych typu 1. Możesz projektować platformę oprogramowania
projektach”. dla kokpitu samochodowego w oparciu o wirtualizację,
architekturę AUTOSAR i system Android Automotive.
Możesz również działać w zespole odpowiedzialnym
za projekt wsparcia produktu w obszarze IoT, który
Co chcesz robić, programisto? spełnia rolę bramy dla innych urządzeń podłączonych
przez różne kanały komunikacyjne.
Inżynierem oprogramowania nie zostaje się z przypadku. Oczywiście
pewna grupa osób decyduje się na tę ścieżkę kariery, skuszona wy- Chcesz pracować z pasją? Owocowe czwartki są dzisiaj
wszędzie, a wyjątkowe propozycje zawodowe tylko
sokimi zarobkami, ale dla większości to coś więcej niż tylko metoda
w GlobalLogic.
zarabiania pieniędzy. Decydując się na ten zawód, nikt nie zakłada, że
będzie się nudzić, powtarzając te same czynności każdego dnia. Nikt Sprawdź oferty pracy na:
www.globallogic.com/pl/careers/
nie chce, by jego praca przypominała zadania maszyny funkcjonu-
jącej przy taśmie w fabryce. W tej specjalizacji chodzi o tworzenie,
uruchamianie wyobraźni i poszukiwanie nowych rozwiązań. W erze
cyfrowej, w której cały świat tak szybko się zmienia, ta pomysłowość udział w powstawaniu rozwiązania, które zmieni sposób korzystania
nie może być ograniczana. Dlatego warto, szukając pracy, sprawdzić, z urządzeń IoT lub pozwoli na dalszy rozwój usług przesyłania stru-
czy nasza przyszła praca pozwoli stworzyć nam coś wartościowego. mieniowego. Móc podpisać się pod takim projektem, to jak zostawić
Rewolucja może mieć wiele twarzy. Papier przyjmuje wszystko, swoje nazwisko w creditsach Cyberpunka 2077. I to bez crunchu.
więc często pod nią podpinane są nawet tak „przełomowe” dokona- W GlobalLogic regularnie szukamy nowych programistów do pra-
nia, jak stworzenie sklepu internetowego. W zespołach GlobalLo- cy. Nasze ogłoszenia mogą wyglądać niepozornie, ale zapowiadane
gic słowo „rewolucyjne” nigdy nie jest jednak nadużywane. Jeśli już w nich „innowacyjne i zróżnicowane projekty” oraz „szkolenia i cer-
pada, to tylko w uzasadnionym kontekście. Na inne użycie nie po- tyfikacje” nie są tylko pustymi hasłami. Jeśli tylko chcesz, poznasz
zwalają nasi eksperci, którzy zdołali poznać realia branży i wiedzą, architekturę SoC ARM-Cortex A53/A53 oraz system czasu rzeczy-
jak może i jak powinna wyglądać praca nad „ciekawymi projektami”. wistego (RTOS) na R7. Będziesz obcować z normami ISO26262 oraz
procesami ASpice-lv3. Nowe technologie typu Go, Rust, NodeJS,
Docker będą zaraz obok Ciebie. A przy tym w cenie będzie Twoja
Praca w IT – zmieniaj świat od dziś
kreatywność – nasze zespoły inżynierskie nie realizują tylko konkret-
Stworzyć nową generację produktu medycznego, który zmieni ży- nych poleceń, ale wykazują sporą dowolność w kwestii proponowa-
cie milionów ludzi na całym świecie. Opracować platformę, która nia własnych rozwiązań. Masz więc szansę naprawdę zmieniać świat.
za moment będzie powszechna w branży motoryzacyjnej. Wziąć Zainteresowany/a?

{  MATERIAŁ INFORMACYJNY  }
BIBLIOTEKI I NARZĘDZIA

ptrace: implementacja debuggera pod Linuksem


Instrumentacja dynamiczna procesów jest zbiorem technik i narzędzi służących do analizy
zachowania programów wykonujących się w określonym środowisku. Narzędziem, któremu
przyjrzymy się bliżej w tym artykule, jest debugger. Wiedza na temat podstaw jego działania
pozwoli czytelnikowi na lepsze zrozumienie pewnych zależności, które z pewnością umożli-
wią w pełni wykorzystanie jego potencjału. Wykorzystując m.in. narzędzie ptrace, przyjrzymy
się budowie debuggera działającego w środowisku linuksowym pod architekturą x86_64.

WSTĘP
kich zdarzeń mogą być wszelkie wyjątki systemowe (ang. exceptions) [2],
Instrumentacja dynamiczna procesów jest bardzo ważnym elemen- automatycznie generowane, gdy procesor napotka na np. niedozwo-
tem rozwiązania zagadki pod tytułem „jak dany program działa?”. loną instrukcję (np. dzielenie przez zero), bądź gdy proces spróbuje
Znaleźć odpowiedź na to pytanie pomagają w tym przypadku m.in. uzyskać dostęp do pamięci znajdującej się poza przydzieloną przez
debuggery. Znane chyba większości programistów tzw. debuggery system przestrzenią adresową. Innym przykładem są zdarzenia de-
whiteboksowe, najczęściej uruchamiane poprzez ikonkę „robaczka” bugowania (ang. debugging events) [3] występujące w czasie wykony-
w dowolnym IDE, pozwalają na prześledzenie dopiero co napisane- wania, np. utworzenie wątku czy dołączenie biblioteki współdzielo-
go kodu, zatrzymując się we wskazanych momentach i informując nej. Szczególnym rodzajem zdarzeń są z kolei breakpointy. Mogą być
programistę o zależnościach między zmiennymi w kolejnych etapach one implementowane na różne sposoby, ale każdy z nich ma swoje
wykonania programu. Są jednak takie sytuacje, w których nie mamy miejsce. Dobrze jest znać ich mechanizmy działania, aby wiedzieć,
dostępu do kodu źródłowego aplikacji – na przykład podczas analizy w jakich określonych sytuacjach jakiego rodzaju breakpointu użyć.
złośliwego oprogramowania. W takim przypadku użyteczny będzie Tym zagadnieniem zajmiemy się w dalszej części artykułu. Na razie
debugger blackboksowy – czyli mający dostęp jedynie do wygene- przyjrzymy się temu, jak wygląda debugger w najprostszym ujęciu.
rowanego kodu assemblera wykonującego się w pamięci kompute- Do jego implementacji w systemach linuksowych może posłużyć na
ra, który zazwyczaj pozbawiony jest symboli debugowania. Główny przykład ptrace – narzędzie, które umożliwia m.in przechwytywanie
cel korzystania z debuggera jest wspólny zarówno dla white-, jak działania dowolnego procesu (ang. attach), czyli przejmowanie kon-
i blackboksowego – zrozumieć, jak w rzeczywisvtości program jest troli nad jego wykonaniem. Nie zawsze jest to możliwe z uwagi na
interpretowany przez procesor w danym środowisku. [1] Co robi? Do wiele mechanizmów bezpieczeństwa, które zostały zaimplemento-
jakich zasobów próbuje uzyskać dostęp? Z jakimi procesami próbu- wane w Linuksie, jak np. moduł bezpieczeństwa YAMA [14] zaim-
je się skomunikować? Z jakich funkcji bibliotecznych korzysta? Czy plementowany w kernelu. Przejęcie kontroli nad danym procesem
próbuje nawiązać połączenie z siecią? Jakie skoki warunkowe podej- pozwala debuggerowi uzyskać dostęp do jego przestrzeni adresowej
muje procesor w trakcie wykonania i jak wygląda pokrycie kodu? w pamięci komputera, a co się z tym wiąże – podmieniać zawartości
Są to przykłady pytań, na które debugger potrafi pomóc w ustaleniu rejestrów czy np. odczytywać wybrane obszary pamięci. Jest to zatem
odpowiedzi. bardzo ważne pojęcie związane z debuggerami. Ptrace przechwytuje
W zasadzie obydwa wymienione rodzaje debuggerów mają po- wykonanie procesu na poziomie pojedynczego wątku, zatem w wie-
dobną budowę i zasadę działania, jednak sposób korzystania z nich lowątkowej aplikacji jesteśmy w stanie przechwycić i osobno poddać
jest nieco inny. W niniejszym artykule skupimy się na mechani- analizie oddzielnie każdy z wątków [12], odwołując się poprzez jego
zmach działania debuggerów blackboksowych. Na przykładzie pro- identyfikator (ang. Thread ID). W Listingu 1 przedstawiono „szkie-
stych programów w C przedstawiony zostanie proces analizy pliku let” debuggera.
wykonywalnego, od znalezienia interesującego adresu na breakpoint
Listing 1. „Szkielet” debuggera – główna pętla oczekująca na wystąpienie
po jego obsługę przez debugger. zdarzeń
Wszystkie przykłady w tym artykule zostały napisane i testowane
from defines import *
w języku Python 3.8.5, pod systemem Ubuntu focal 20.04.1 LTS dzia-
def debugger(pid):
łającym pod Virtualbox 6.1.16. Wykorzystano również kompilator ptrace(PTRACE_ATTACH, pid, 0, None)
gcc w wersji 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04). status = waitpid(pid, 0)
while True:
if WIFSTOPPED(status[1]):

BUDOWA DEBUGGERA do_some_action()

Debugger, w najprostszym ujęciu, jest po prostu procesem oczekują- Na potrzeby tego i kolejnych przykładów został utworzony plik za-
cym na wystąpienie zdarzeń debugowania, które następnie są prze- wierający najważniejsze definicje, z którego będą korzystały skrypty
chwytywane i odpowiednio obsługiwane. Jednym z przykładów ta- debuggera. Został on przedstawiony w Listingu 2.

<18> {  1 / 2021 < 95 >  }


/ ptrace: implementacja debuggera pod Linuksem /

Listing 2. Najważniejsze definicje – defines.py działanie. Zakładając, że mamy odpowiednio zdefiniowaną funkcję
do_some_action() (na początek w celach testowych można po pro-
from os import execv, fork, WIFSTOPPED, waitpid,\
WSTOPSIG, wait stu wypisać w niej jakąkolwiek wartość na ekran), możemy spróbo-
from sys import argv
from ctypes import c_ulonglong, byref, cast, CDLL,\
wać uruchomić nasz skrypt. Jako program do debugowania przyj-
c_uint64, c_void_p, Structure, c_int, c_size_t mijmy kod w C przedstawiony na Listingu 3 (nazwijmy go loop.c).
from mmap import PAGESIZE
Program ten należy skompilować w poniższy sposób, uzyskując plik
# Requesty ptrace - man 2 ptrace
wykonywalny o nazwie loop:
# https://code.woboq.org/gcc/include/sys/ptrace.h.html
PTRACE_TRACEME = 0 $> gcc -o loop loop.c
PTRACE_PEEKTEXT = 1
PTRACE_PEEKDATA = 2
PTRACE_POKETEXT = 4 Listing 2. Krótki program w C
PTRACE_POKEDATA = 5
#include <stdio.h>
PTRACE_CONT = 7
#include <stdlib.h>
PTRACE_SINGLESTEP = 9
#include <unistd.h>
PTRACE_GETREGS = 12
void print(int it)
PTRACE_SETREGS = 13
{
PTRACE_ATTACH = 16 printf("Petla nr %d.\n", it);
PTRACE_DETACH = 17 sleep(1);
}
# Ptrace
ptrace = CDLL("libc.so.6").ptrace int main()
ptrace.argtypes =\ {
[c_uint64, c_uint64, c_uint64, c_void_p] for(int i=0; /* nieskoczona */; i++)
ptrace.restype = c_uint64 print(i);
}
# Requesty memprotect - man 2 memprotect
PROT_NONE = 0x0
Po skompilowaniu powyższego programu w gcc i uruchomieniu nad-
# Mapowanie nazw sygnalow na ich kody - man 7 signal
signals = { chodzi pora na użycie naszego debuggera. Część poniższej komendy
2: 'SIGINT',
5: 'SIGTRAP',
w odwróconych cudzysłowach (ang. backticks) jest interpretowana
11: 'SIGSEGV', przez powłokę jako dodatkowe polecenie i wykonana przed urucho-
15: 'SIGTERM',
18: 'SIGCONT',
mieniem naszego skryptu, pozwalając na bezpośrednie przekazanie
19: 'SIGSTOP', PID procesu poprzez parametr (Listing 3).
}

# Rejestry CPU Listing 3. Błąd spowodowany zabezpieczeniami przed śledzeniem procesów


class RegsStruct(Structure):
_fields_ = [ ppos@ppospcv:~/Desktop/kody$ python3 listing1.py `pidof loop`
("r15", c_ulonglong), Traceback (most recent call last):
("r14", c_ulonglong), File "listing1.py", line 13, in <module>
("r13", c_ulonglong), debugger(int(argv[1]))
("r12", c_ulonglong), File "listing1.py", line 8, in debugger
("rbp", c_ulonglong), status = waitpid(pid, 0)
("rbx", c_ulonglong), ChildProcessError: [Errno 10] No child processes
("r11", c_ulonglong),
("r10", c_ulonglong),
("r9", c_ulonglong), Błąd ten powoduje nic innego jak pewien moduł bezpieczeństwa pod
("r8", c_ulonglong),
("rax", c_ulonglong), Linuksem zwany YAMA [14]. Skutkuje on przede wszystkim tym, że
("rcx", c_ulonglong),
żaden proces w systemie na standardowych prawach użytkownika
("rdx", c_ulonglong),
("rsi", c_ulonglong), nie może przechwycić wykonania innego procesu. Wyjątkiem jest,
("rdi", c_ulonglong),
("orig_rax", c_ulonglong),
gdy przechwytujemy wykonanie procesu-potomka (utworzonego np.
("rip", c_ulonglong), za pomocą funkcji fork()), wtedy możemy w śledzonym potomku
("cs", c_ulonglong),
("eflags", c_ulonglong), użyć funkcji ptrace(), podając jako argument PTRACE_TRACEME, aby
("rsp", c_ulonglong), zezwolić rodzicowi na przechwytywanie. Zostanie to przedstawione
("ss", c_ulonglong),
("fs_base", c_ulonglong), w dalszej części tej sekcji, tymczasem w aktualnej sytuacji rozwiąza-
("gs_base", c_ulonglong), nia są dwa: albo uruchomić debugger z uprawnieniami roota, albo
("ds", c_ulonglong),
("es", c_ulonglong), ręcznie wyłączyć mechanizm YAMA. Należy zwrócić uwagę, że jest
("fs", c_ulonglong), to mimo wszystko ważne zabezpieczenie systemu, w związku z tym
("gs", c_ulonglong),
] eksperymenty z jego wyłączeniem należy przeprowadzić w odizolo-
wanym środowisku (np. maszynie wirtualnej):
W Listingu 1 przedstawiono wywołanie funkcji waitpid() oczekują-
$> echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
cej na zmianę stanu procesu o wskazanym PID (ang. Process Identi-
fier), która następnie zwraca krotkę przechowującą PID oraz powód
zatrzymania procesu. Powód ten jest następnie sprawdzany w pętli Większość współczesnych systemów operacyjnych wyposażona jest
debuggera przy użyciu funkcji WIFSTOPPED(). Gdy proces został za- w szereg zabezpieczeń przed wieloma technikami debugowania.
trzymany, możemy wykonać na nim dodatkowe akcje i wznowić jego Wymagają one zazwyczaj wysokich uprawnień systemowych, z tego

{  WWW.PROGRAMISTAMAG.PL  } <19>
BIBLIOTEKI I NARZĘDZIA

względu ciężko byłoby je wykorzystać atakującym. Nie oznacza to, że w języku polskim odnosić będziemy się do wspomnianych pojęć
techniki te nie mogą zostać wykorzystane w złym celu. Same w sobie w języku angielskim.
są jednak neutralne – mogą zostać użyte zarówno przez osoby łamią-
ce zabezpieczenia legalnych programów, gier etc., jak również przez
SOFTWARE BREAKPOINTS
osoby analizujące złośliwe oprogramowanie, aby móc się przed nim
odpowiednio zabezpieczyć. Najprostszym, a zarazem najbardziej elastycznym sposobem na za-
trzymanie programu w określonym miejscu jest umieszczenie bre-

DWA TRYBY DZIAŁANIA akpointu programowego (ang. software breakpoint). Technika ta po-
lega na podmianie pierwszego bajtu wykonywanej instrukcji na bajt
Wyjaśniliśmy już, jak przechwycić wykonanie dowolnego procesu o wartości 0xCC. Wartość ta odpowiada instrukcji INT3. Gdy proce-
w systemie. Jest to przydatne, gdy nie mamy możliwości samodziel- sor ją napotka, natychmiast wywołuje przerwanie (nazwane również
nego uruchomienia śledzonego procesu od początku – lub interesu- INT3) i zwraca kontrolę debuggerowi. Różne przykłady, które mogą
je nas śledzenie wykonania tylko pewnej jego części. Istnieje także nieco lepiej zobrazować to pojęcie, zostały szerzej omówione w ar-
możliwość uruchomienia procesu poprzez debugger, aby uzyskać tykule [1].W celu lepszego zrozumienia tego mechanizmu, a także
nad nim pełną kontrolę już od początku wykonania. Przykład zapre- ogólnie tego, jak funkcjonują poszczególne komponenty kompute-
zentowano w Listingu 4. ra na najniższym poziomie, zachęcamy do lektury [4], a także [5].
Bardziej szczegółowo procedura założenia breakpointu wygląda
Listing 4. Przechwytywanie procesu od momentu rozpoczęcia jego wykonania
następująco:
from defines import * 1. Przy użyciu PTRACE_PEEKTEXT odczytujemy oryginalną zawar-
# Funkcja sledzonego potomka tość komórki pamięci, w której chcemy ustawić breakpoint.
# wykona program wskazany przez
# parametr progname 2. Podmieniamy pierwszy bajt instrukcji pod wskazanym adre-
def tracee(progname): sem na 0xCC poprzez odpowiednie wykorzystanie maski i ope-
ptrace(PTRACE_TRACEME, 0, 0, 0)
execv(progname, (str(0))) ratorów bitowych. Uważny czytelnik zauważy, że wspomniany
# Funkcja rodzica, patrz fork() nizej „pierwszy” bajt instrukcji znajduje się w zasadzie z lewej stro-
def debugger(pid): ny. Jest to związane z konwencją zapisu danych w pamięci.
status = wait()
while True: W przypadku Intela jest to tzw. Little Endian [15]. Dla nas ozna-
if WIFSTOPPED(status[1]): cza to tyle, że bajty przechowywane w określonej komórce pa-
do_some_action()
mięci mają odwróconą kolejność.
pid = fork()
3. Przy użyciu PTRACE_POKETEXT podmieniamy zawartość komór-
# fork() zwraca PID potomka w ciele rodzica
ki, następnie przy użyciu PTRACE_PEEKTEXT sprawdzamy jej
if pid:
debugger(pid) nową zawartość.
# oraz 0 w ciele potomka
else:
4. Wznawiamy uruchomienie, używając PTRACE_CONT, oczekując
tracee(argv[1]) na zatrzymanie śledzonego procesu na ustawionym breakpoin-
cie (używając WIFSTOPPED() [6]).
Gdyby uruchomić ten skrypt w następujący sposób (podając jako pa- 5. Tutaj możemy dodać obsługę breakpointu – odczytanie stosu,
rametr nazwę dowolnego pliku wykonywalnego): zawartości rejestrów procesora, wstrzyknięcie kodu – sky is the
limit. W przykładzie odczytujemy także zawartość rejestrów
$> python3 listing3.py loop
przy użyciu PTRACE_GETREGS.
6. Po wykonaniu interesujących nas operacji przywracamy ory-
rozpoczęlibyśmy śledzenie procesu od samego początku jego wyko- ginalną zawartość komórki pamięci pod adresem breakpointu,
nywania. Należy zwrócić uwagę, że w przypadku śledzenia potomka cofamy rejestr RIP (ang. Instruction Pointer) (wskazujący na
procesu konieczna jest jego zgoda poprzez wywołanie funkcji ptrace aktualnie wykonywaną instrukcję) o 1 – spowoduje to powrót
z argumentem PTRACE_TRACEME. W przeciwnym przypadku ani zdję- wykonywania programu do oryginalnej instrukcji– ustawiamy
cie zabezpieczeń YAMA, ani uruchomienie debuggera z prawami ro- rejestry (PTRACE_SETREGS) i wznawiamy wykonywanie procesu.
ota nie sprawi, że proces dobrowolnie podda się śledzeniu.
Poza przechwytywaniem określonych zdarzeń główną funk- W praktyce założenie breakpointu programowego mogłoby wyglą-
cją debuggera jest ustawianie breakpointów. W kolejnych sekcjach dać jak w Listingu 5.
przedstawione zostaną ich główne rodzaje, charakterystyki oraz me-
Listing 5. Software breakpoint
tody ich implementacji. Tematyka ta została szerzej opisana w [1].
Jedną z głównych różnic dla czytelnika będzie nazewnictwo break- from defines import *

pointów – we wspomnianym artykule zostały one nazwane „wyjątka- def soft_bp(pid, instr_addr):
regs = RegsStruct()
mi” albo „przerwaniami”. Niestety nie ma jednoznacznego tłumacze- # 1
nia tego pojęcia na język polski, a wszystko komplikuje dodatkowo org_instr = ptrace(PTRACE_PEEKTEXT, pid,
c_ulonglong(instr_addr), 0)
fakt, że skoro „breakpoint” można przetłumaczyć jako „wyjątek”, to print("Oryginalna zawartość komórki z"
jak przetłumaczyć „exception”? Z uwagi na te drobne niejasności "adresu 0x%x: 0x%x" % (instr_addr, org_instr))

<20> {  1 / 2021 < 95 >  }


/ ptrace: implementacja debuggera pod Linuksem /

# 2 akcji, gdy wykryje próby debugowania. Różne techniki wykrywania


zm_instr = (org_instr & 0xFFFFFFFFFFFFFF00) | 0xCC
# 3
debuggera oraz breakpointów zostały szerzej opisane w kilku roz-
ptrace(PTRACE_POKETEXT, pid, instr_addr, zm_instr) działach [2] oraz [5], a także [9].
nowa_instr = ptrace(PTRACE_PEEKTEXT, pid, instr_addr, 0)
print("Nowa zawartość komórki z"
"adresu 0x%x: 0x%x" % (instr_ addr, nowa_instr))
HARDWARE BREAKPOINTS
# 4
ptrace(PTRACE_CONT, pid, 0, 0)
status = wait()
Breakpointy sprzętowe (ang. hardware breakpoint) wykorzystują do
if WIFSTOPPED(status[1]): implementacji specjalny zestaw rejestrów debugowania, który obec-
# 5
do_some_action() ny jest w procesorach nowej generacji. O budowie tych rejestrów
oraz ich parametryzacji w celu ustawienia konkretnego rodzaju wy-
ptrace(PTRACE_GETREGS, pid, 0, byref(regs))
print("Zatrzymano proces na adresie(RIP): 0x%x" jątku przeczytać można w artykule [1]. Dobrym źródłem informacji
% (regs.rip))
o tej tematyce są także podręczniki Intela [13]. Rejestry debugowania
# 6 oznaczone są kolejno DR0..DR7 (ang. Debug Register). Z uwagi na ich
ptrace(PTRACE_POKETEXT, pid, instr_addr, org_instr)
regs.rip -= 1 strukturę jesteśmy w stanie ustawić jedynie 4 breakpointy jednocze-
ptrace(PTRACE_SETREGS, pid, 0, byref(regs))
śnie (zachowując ich adresy w rejestrach DR0–DR3). Do włączania/
input() wyłączania poszczególnych breakpointów, a także wyboru ich rodza-
ju służy rejestr DR7, a dokładniej jego dolne 32 bity (z uwagi na kom-
Mając tak zdefiniowaną funkcję, możemy umieścić ją w pętli debug- patybilność wsteczną). Bity 16–31 służą do określenia długości oraz
gera w sposób pokazany w Listingu 6. typu breakpointu. Możliwe długości to 1 bajt, 2 bajty, 4 oraz 8 bajtów.
Typy breakpointów pozwalają rozróżnić poszczególne zdarzenia wy-
Listing 6. Pętla główna debuggera – przechwytywanie breakpointu
konywania programu:
from defines import * » breakpoint na wykonanie,
def debugger(pid): » breakpoint na zapis,
breakpoint_addr = 0x40102c
status = waitpid(pid, 0) » breakpoint na odczyt lub zapis, ale nie wykonanie.
while True:
if WIFSTOPPED(status[1]): Bity 8–15, jak również górne 32 bity rejestru są wyłączone z użytku,
soft_bp(pid, breakpoint_addr)
ptrace(PTRACE_SINGLESTEP, pid, 0, 0) zaś bity 0–7 służą do określenia zasięgu breakpointu. W większości
status = waitpid(pid, 0) przypadków nie ma rozróżnienia pomiędzy zasięgiem globalnym
(bit G) oraz lokalnym (bit L). W momencie napotkania breakpointu
W przykładzie użyty został konkretny adres dla breakpointu, którego sprzętowego procesor przerywa wykonanie i rzuca przerwanie INT1,
znalezienie zostanie omówione w kolejnych sekcjach. Po jego usta- przekazując kontrolę debuggerowi.
wieniu i wyjściu z funkcji soft_bp() pozwalamy śledzonemu pro- Sposób zakładania breakpointu sprzętowego jest analogiczny do
cesowi na wykonanie pojedynczej instrukcji (PTRACE_SINGLESTEP), breakpointu programowego. Przechwytujemy wykonanie procesu,
następnie ponownie uruchamiamy całą procedurę założenia bre- ustawiamy odpowiednio bity w rejestrze DR7 oraz zapisujemy adres
akpointu. Ciekawy efekt można uzyskać, uruchamiając powyższy breakpointu do odpowiedniego rejestru DR0–DR3. Wznawiamy dzia-
skrypt wraz z wcześniejszym programem loop.c: łanie procesu, czekamy, aż mikroprocesor napotka na wskazany adres
i przekaże kontrolę debuggerowi. Na końcu cofamy licznik instrukcji
$> python3 debugger.py `pidof loop`
RIP, wznawiamy wykonanie procesu. Główna różnica polega na tym,
że nie manipulujemy pamięcią procesu i nie musimy zapamiętywać
Dzięki temu możemy zatrzymywać proces co wykonanie pojedynczej oryginalnej zawartości komórki pamięci w debuggerze. Dzięki temu
pętli. Kolejnym krokiem mogłaby być zmiana parametru przekazy- mamy mniejszą ingerencję w wykonywanie danego procesu, a wy-
wanego do funkcji print(). Dla zainteresowanych, zostało to szerzej krycie takiego breakpointu jest znacznie trudniejsze. Możemy także
opisane w [1]. Zachęcamy także do lektury [7], gdzie przedstawione przechwycić na poziomie jądra funkcje pozwalające sprawdzić za-
zostały omówione zagadnienia, jak również pokrewne z dziedziny wartość rejestrów debugowania. Wówczas możemy zwrócić fałszywą
inżynierii wstecznej. wartość rejestru w sytuacji, gdy śledzony proces chciałby sprawdzić,
Charakterystyka breakpointów programowych pozwala nam umie- czy ustawiono breakpointy sprzętowe. I tutaj tak naprawdę jest pies
ścić ich nieskończenie wiele wewnątrz śledzonego procesu, a sposób pogrzebany. Z uwagi, że ten rodzaj wyjątku jest znacznie trudniejszy
ich umieszczania nie jest skomplikowany. Z drugiej jednak strony w do wykrycia i rozszerza możliwości śledzenia procesu, jego użycie
ogólnym przypadku breakpoint ten dotyczy pojedynczego bajtu – czy nie jest możliwe z przestrzeni użytkownika. Zostało to zablokowane
też pojedynczej komórki pamięci. W związku z tym breakpointy pro- w systemach linuksowych właśnie ze względów bezpieczeństwa, aby
gramowe nie są zbyt elastyczne w sensie pokrycia pamięci i ciężko „zwykły” użytkownik nie uzyskał zbyt dużych możliwości śledzenia
byłoby z ich użyciem śledzić jej większe obszary. Kolejną wadą jest to, innych procesów. Zatem ten rodzaj breakpointu nie będzie możliwy
że taki breakpoint modyfikuje obraz procesu w pamięci – przez co do zrealizowania za pomocą narzędzia ptrace czy jakiegokolwiek in-
jest łatwo wykrywalny przez śledzony proces. Na przykład analizo- nego narzędzia dostępnego z przestrzeni użytkownika. Możliwe jest
wane złośliwe oprogramowanie może ukryć część swoich złośliwych jedynie użycie specjalnych funkcji kernela, które wykraczają poza

{  WWW.PROGRAMISTAMAG.PL  } <21>
BIBLIOTEKI I NARZĘDZIA

ramy tego artykułu. Temat ten pozostawimy zainteresowanemu czy- regs = RegsStruct()
# Opkod instrukcji syscall
telnikowi do samodzielnego prześledzenia. syscall_opcode = 0x050f

# Pobierz aktualny stan RIP


MEMORY BREAKPOINTS ptrace(PTRACE_GETREGS, pid, 0, byref(regs))
print("\nPotomek zostal zatrzymany na"
"adresie RIP = 0x%x" % (regs.rip))
Ostatni zaprezentowany w tym artykule rodzaj breakpointów będzie
# Zachowaj aktualna instrukcje oraz stan rejestrow
w sposób bezpośredni związany z pamięcią. Nie jest to de facto bre- ptrace(PTRACE_GETREGS, pid, 0, byref(backup_regs))
ptrace(PTRACE_GETREGS, pid, 0, byref(regs))
akpoint w pełnym tego słowa znaczeniu, a raczej odpowiednie wy-
org_instr = ptrace(PTRACE_PEEKDATA, pid,
korzystanie mechanizmów ochrony pamięci udostępnionych przez c_ulonglong(regs.rip), 0)
systemy operacyjne. Pamięć komputera jest poddana przez system # Zmien instrukcje na syscall,
operacyjny procesowi stronicowania, czyli podziału na obszary pa- # przygotuj rejestry do mprotect
# zwroc uwage, ze przesuwamy adres breakpointu
mięci o określonej długości. W systemach o architekturze programo- # o jedna strone (+ PAGESIZE) - w celu
# lepszej wizualizacji przykladu
wej x86-64 rozmiar strony wynosi zazwyczaj 4 kB. Każda strona ma regs.rax = 10 # mprotect - system call nr
określone uprawnienia, które mogą być nadawane i interpretowane regs.rdi = get_page_start_addr(
int(argv[1], 16)) + PAGESIZE # adres startu
przez system operacyjny. Przykładami takich uprawnień mogą być: print("BP ustawiony na:",
» zezwolenie na odczyt, hex(get_page_start_addr(
int(argv[1], 16)) + PAGESIZE))
» zezwolenie na zapis, regs.rsi = PAGESIZE # dlugosc
» zezwolenie na wykonanie regs.rdx = PROT_NONE # Guard Page

# Podmiana wartosci rejestrow


ptrace(PTRACE_SETREGS, pid, 0, byref(regs))
danych znajdujących się w określonym obszarze pamięci. Jednym # Zmiana wykonywanej instrukcji na syscall
z ciekawych uprawnień jest tzw. Guard Page, które zgłasza jednora- ptrace(PTRACE_POKEDATA, pid,
c_ulonglong(regs.rip), syscall_opcode)
zowy wyjątek (ang. exception) przy jakiejkolwiek próbie dostępu do ptrace(PTRACE_SINGLESTEP, pid, 0, 0)
tej strony pamięci (a następnie przywraca jej wartości i prawa dostę- ptrace(PTRACE_GETREGS, pid, 0, byref(regs))
pu do oryginalnej postaci). I właśnie na tym mechanizmie opierają print("Memprotect zwrocil: ", regs.rax)
się breakpointy pamięciowe (ang. memory breakpoint). Za pomocą # Przywroc oryginalne instrukcje
# oraz stan rejestrow
specjalnego narzędzia mprotect [8], którego zastosowania łatwo jest
ptrace(PTRACE_SETREGS, pid, 0, byref(backup_regs))
się domyślić, ustawiamy wspomniane zabezpieczenie przed jakim- ptrace(PTRACE_POKEDATA, pid,
c_ulonglong(backup_regs.rip), org_instr)
kolwiek dostępem do określonej strony pamięci. Należy pamiętać, że # Kontynuuj wykonanie
mprotect przyjmuje jako pierwszy parametr adres początku strony. ptrace(PTRACE_CONT, pid, 0, 0)
Możemy obliczyć go na podstawie wybranego adresu dla breakpoin- # Oblicz adres poczatkowy strony
def get_page_start_addr(addr):
tu, a także wielkości strony w danym systemie. return addr & -PAGESIZE
Jak widać, ten rodzaj breakpointów jest dużo mniej precyzyjny
# Funkcja debugger - patrz Listing 6
od programowych, jednakże umożliwia śledzenie bardzo dużych ob- if __name__ == "__main__":
szarów pamięci. Może to w pewnych sytuacjach ułatwić znalezienie debugger(int(argv[2]))

interesującego nas obszaru. Występuje tutaj jednak pewne ogranicze-


nie systemów operacyjnych, które utrudnia implementację tego typu Przykładowy program, dla którego można wypróbować działanie po-
breakpointów. Otóż wszystkie procesy mają dostęp tylko i wyłącznie wyższego skryptu – przykład ze strony mprotect [8] został przedsta-
do swojej przestrzeni adresowej i nie ma tutaj wyjątku dla przechwy- wiony w Listingu 6.
conego procesu. W związku z tym nie jesteśmy w stanie wykonać
Listing 8. Przykładowy program w C demonstrujący działanie mprotect
funkcji mprotect z poziomu debuggera, tak aby miało to wpływ
na uprawnienia stron należących do śledzonego procesu. Aby takie #include <unistd.h>
#include <signal.h>
zmiany odniosły rzeczywisty skutek, funkcja ta musi zostać wykona- #include <stdio.h>
na z jego kontekstu. Jest kilka sposobów, jak tego dokonać – zostały #include <malloc.h>
#include <stdlib.h>
one opisane w [10]. Najprostszym może być wstrzyknięcie określo- #include <errno.h>
#include <sys/mman.h>
nych wartości do rejestrów tak, aby zasymulować wywołanie mpro-
tect z odpowiednimi parametrami, wywołanie syscalla, a następnie static char *buffer;

cofnięcie licznika instrukcji o 1. Jak widać, metoda ta jest bardzo po- int main(int argc, char *argv[])
{
dobna do podmiany pierwszego bajtu instrukcji na 0xCC i w związku int pagesize;
z tym nie będzie to już szerzej opisywane. Całość została zademon- pagesize = sysconf(_SC_PAGE_SIZE);

strowana w Listingu 7. buffer = memalign(pagesize, 4 * pagesize);


printf("Start of region: %p\n", buffer);
Listing 7. Zakładanie breakpointu na obszarze pamięci printf("Process ID: %d\n", getpid());

getchar();
from defines import *
int counter = 0;
def mem_bp(pid): for (char *p = buffer ; ; ){
backup_regs = RegsStruct() if (counter == 1024){

<22> {  1 / 2021 < 95 >  }


/ ptrace: implementacja debuggera pod Linuksem /

counter = 0; z różnych przyczyn wynik mprotect może być różny od 0, zazwy-


sleep(1);
printf("a");
czaj wystarczy wtedy uruchomić skrypt kilka razy „aż do skutku”).
printf(" - Current region: %p\n", p); Następnie w konsoli z programem memory przeskakujemy funkcję
}
*(p++) = ‚a'; getchar(), naciskając klawisz Enter, po czym program powinien
counter++; zacząć iterować po zaalokowanym buforze, zapisując do niego kolej-
}
ne znaki. Po chwili program zatrzyma się.
}
Listing 11. Zatrzymanie programu memory.c

W celu przedstawienia przykładu należy najpierw uruchomić skom- ppos@ppospcv:~/Desktop/kody$ gcc -o memory memory.c && ./memory
Start of region: 0x5567fb086000
pilowany kod memory.c. Process ID: 3943
a - Current region: 0x5567fb086400
Listing 9. Uruchomienie programu memory.c a - Current region: 0x5567fb086800
a - Current region: 0x5567fb086c00
ppos@ppospcv:~/Desktop/kody$ gcc -o memory memory.c && ./memory a - Current region: 0x5567fb087000
Start of region: 0x5567fb086000
Process ID: 3943
Natomiast w oknie skryptu pojawi się informacja o przechwyconym
Następnie, posiadając adres w pamięci, gdzie został zaalokowany bu- sygnale SIGSEGV (Segmentation Fault). Program memory.c zakoń-
for, uruchamiamy skrypt debuggera, podając ten adres jako parametr. czy się awaryjnie po zakończeniu skryptu – natrafienie na chronioną
stronę w pamięci skutecznie zatrzymuje wykonywanie programu.
Listing 10. Uruchomienie skryptu ustawiającego breakpoint pamięciowy
Listing 12. Skrypt debuggera – analizowany program otrzymał sygnał
ppos@ppospcv:~/Desktop/kody$ python3 mem_bp.py \
> 0x5567fb086000 3943
SIGSEGV

Potomek zostal zatrzymany na adresie RIP = 0x7ff46a12b142 ppos@ppospcv:~/Desktop/kody$ python3 mem_bp.py \


BP ustawiony na: 0x5567fb087000 > 0x5567fb086000 3943
Memprotect result: 0
Potomek zostal zatrzymany na adresie RIP = 0x7ff46a12b142
BP ustawiony na: 0x5567fb087000
Memprotect result: 0
Należy zwrócić uwagę, że mprotect zwrócił 0, czyli ustawienie po- Potomek otrzymal sygnal: SIGSEGV
żądanych uprawnień na danej stronie pamięci się powiodło (czasem

/* REKLAMA */

{  WWW.PROGRAMISTAMAG.PL  } <23>
BIBLIOTEKI I NARZĘDZIA

JAK ZNALEŹĆ ADRES BREAKPOINTU gającymi na przepełnieniu bufora (ang. buffer overflow). Cały proces
znalezienia interesującego nas adresu został bardziej szczegółowo
Trudność znalezienia adresu breakpointu zależy od rodzaju anali- opisany w [1], a także w [11].
zowanego programu. W najprostszym przypadku, mając program w
assemblerze zbudowany przy użyciu np. narzędzia NASM [16], mo-
PODSUMOWANIE
żemy po prostu wrzucić go do narzędzia objdump [1], np.:
W tym artykule przedstawiono podstawowe zagadnienia związane
$> objdump -dM Intel scieżka/do/programu
z instrumentacją dynamiczną razem z funkcjonowaniem debugge-
rów. Przy użyciu Pythona zaimplementowaliśmy podstawowe me-
aby zobaczyć, jak mapowany jest program na określony obszar pa- chanizmy debugowania na architekturze x86_64 pod Linuksem. Ko-
mięci w momencie jego uruchomienia. Adresy komórek pamięci wi- rzystając z narzędzia ptrace, omówione zostały rodzaje breakpointów,
doczne dla kolejnych instrukcji programu są bezwzględne dla całego w tym programowe, sprzętowe oraz pamięciowe. Krótkiej charakte-
systemu, możemy zatem umieścić je w debuggerze w dokładnie ta- rystyce poddano także wyszukiwanie miejsca w pamięci analizowa-
kiej formie jak zostały przedstawione. Podobnie sprawa odnosi się do nego programu na umieszczenie potencjalnego breakpointu. Są to
programów w językach wysokiego poziomu (np. C) skompilowanych fundamentalne zagadnienia dotyczące funkcjonowania debuggerów,
statycznie (w gcc flaga -static). Wszystkie widoczne adresy są adre- których dogłębne zrozumienie z pewnością pozwoli czytelnikowi
sami bezwzględnymi i mogą zostać użyte jak w przykładzie z assem- wykorzystać w pełni możliwości narzędzia do debugowania, a także
blerem. Gdy jednak program zostanie skompilowany dynamicznie łatwiejsze ich rozwijanie i dostosowanie do swoich potrzeb. A przede
(opcja domyślna), a wszystkie adresy wszystkich funkcji bibliotecz- wszystkim pomoże podjąć decyzję, jakich technik debugowania użyć
nych i systemowych odnajdywane są w czasie wykonania programu, w konkretnych sytuacjach.
adresy przedstawiane przez narzędzie objdump są jedynie adresami Czytelników zainteresowanym tą tematyką zachęcamy do zgłę-
względnymi. Aby przetłumaczyć je na adresy bezwzględne, należy bienia tajników zarówno wspomnianego rozdziału [7], w którym
dodać offset bazowy przydzielany programowi w momencie uru- omawiane są zaawansowane metody śledzenia procesów (np. Intel
chamiania. Jednym z przykładów jego odnalezienia jest przyjrzenie PT), jak również lektury pozostałych rozdziałów z tej książki. Bardzo
się zawartości pliku /proc/<PID>/maps, gdzie <PID> odnosi się do ciekawy wstęp w niskopoziomowy świat debuggerów stanowią także
śledzonego procesu. Dodatkowym zabezpieczeniem, które ma tutaj wspomniane pozycje [4] oraz [5]. Przede wszystkim jednak zachęca-
znaczenie, jest ASLR [17]. Jest to najogólniej mówiąc randomizacja my do samodzielnego eksperymentowania czy próby implementacji
określonych bajtów w adresach należących do wykonywanego pro- breakpointów sprzętowych w Pythonie.
cesu, mająca zabezpieczyć go przed ewentualnymi atakami np. pole-

Bibliografia
[1] https://github.com/PrzemyslawSamsel/LinuxPYDebug
[2] J. Seitz, „2. Debuggers and debugger design”, Gray Hat Python, 2009
[3] https://docs.microsoft.com/en-us/windows/win32/debug/debugging-events
[4] I. Zhirkov, Low-Level Programming, 2017
[5] R. O’Neill, „Linux Process Tracing” – Learning Linux Binary Analysis, 2016
[6] https://linux.die.net/man/2/waitpid
[7] R. Święcki, „10. Śledzenie ścieżki wykonania procesu w systemie Linux” – Praktyczna Inżynieria Wsteczna – Metody, Techniki, Narzędzia, 2018
[8] https://man7.org/linux/man-pages/man2/mprotect.2.html
[9] NSA, „LIMITING PTRACE ON PRODUCTION LINUX SYSTEMS,” 2019
[10] https://perception-point.io/changing-memory-protection-in-an-arbitrary-process/
[11] https://ancat.github.io/python/2019/01/01/python-ptrace.html
[12] https://man7.org/linux/man-pages/man2/ptrace.2.html
[13] Intel, „17.2 Debug Registers” – Intel® 64 and IA-32 Architectures Software Developer’s Manual, 2020
[14] https://www.kernel.org/doc/html/v4.15/admin-guide/LSM/Yama.html
[15] https://www.youtube.com/watch?v=hYkkrEcpV3E
[16] http://libra.cs.virginia.edu/~aaron/08-nasm/nasmexamples.html
[17] T. Kwiecień, „3. Mechanizmy ochrony aplikacji” – Praktyczna Inżynieria Wsteczna – Metody, Techniki, Narzędzia, 2018

PRZEMYSŁAW SAMSEL
samselprzemyslaw@gmail.com
Student II stopnia Zachodniopomorskiego Uniwersytetu Technologicznego w Szczecinie. Aktywny członek koła naukowego
.NET (akademie Pythona, cybersecurity). Hobbystycznie grywa w CTFy, rozwiązuje napotkane zagadki oraz czyta wszystko, co
wpadnie w ręce w tematyce kryminalistyki czy kognitywistyki. Czasami także chodzi spać.

<24> {  1 / 2021 < 95 >  }


PROGRAMOWANIE URZĄDZEŃ MOBILNYCH

Aplikacje natywne na Ubuntu Touch


Aplikacje tworzone dla systemu Ubuntu Touch można podzielić na dwa rodzaje – webowe
i natywne. Pierwsze to strony internetowe renderowane przy pomocy webview Oxide urucha-
mianego wewnątrz kontenera aplikacji internetowych. Drugie to aplikacje zdolne do pełnego
wykorzystania zasobów i możliwości urządzenia, tworzone przy pomocy Qt QML w połącze-
niu z innymi językami programowania, np. C++, JavaScript czy Python. W poniższym tekście
skupimy się na aplikacjach natywnych budowanych za pomocą QML i C++.

QML
Canonical, najwygodniejszym sposobem na stworzenie aplikacji było
W związku z tym, że QML znajduje się w każdej aplikacji, bez wzglę- skorzystanie z Ubuntu SDK. Jednakże po przejęciu projektu przez
du na to, jaki język zostanie użyty do utworzenia backendu, warto UBports nie jest ono dalej wspierane i choć teoretycznie w dalszym
zadać sobie pytanie, czym on tak naprawdę jest. ciągu można go używać, nie jest to zalecane i nie ma gwarancji, że
QML to deklaratywny język programowania służący do budo- wszystko będzie działać poprawnie.
wania interfejsu użytkownika. Może być łączony z JavaScriptem, Rozwiązaniem alternatywnym jest użycie clickable – build sys-
C++, Pythonem, a nawet Rustem. Jego składnia przypomina JSONa, temu stworzonego dla Ubuntu Touch. Clickable obsługiwane jest
a komponenty łatwo można budować poprzez łączenie i rozbudowy- z wiersza poleceń i umożliwia tworzenie pakietów click, które można
wanie już istniejących. W parze z QML stosuje się Qt Quick – bi- publikować w oficjalnym sklepie z aplikacjami – OpenStore. Użyt-
bliotekę standardową typów, w której skład wchodzą widoki, modele, kownicy, którzy preferują narzędzia z graficznym interfejsem, mogą
animacje, efekty cząsteczkowe, shadery, elementy wizualne i elemen- użyć Atoma – edytora, dla którego powstała wtyczka integrująca
ty interaktywne. Kod QML zapisujemy w plikach .qml. o nazwie atom-clickable-plugin.
Podstawowym wymogiem do rozpoczęcia przygody z tworze-
Listing 1. Przycisk stworzony w QML z trzech komponentów – prostokąta
(wizualny), tekstu (wizualny) oraz obszaru myszy (interaktywny) niem aplikacji dla Ubuntu Touch jest posiadanie komputera z Li-
nuxem. Najprościej będzie użyć jednej z dystrybucji wymienianych
import QtQuick 2.4
w instrukcjach instalacyjnych clickable – Ubuntu lub Arch.
Rectangle {
width: 100
Zalecaną metodą na Ubuntu jest instalacja Dockera, adb, gita,
height: 50 Pythona (v3) oraz pip3:
color: "red"
anchors.centerIn: parent
sudo apt install docker.io adb git python3 python3-pip
Text { python3-setuptools
text: "Hello"
color: "white"
font.pointSize: 24
Następnie za pomocą instalatora pakietów Pythona należy zainstalo-
anchors.centerIn: parent wać clickable:
}

MouseArea { pip3 install --user --upgrade clickable-ut


anchors.fill: parent
onClicked: console.log("hello button")
}
} dodać zainstalowany skrypt do ścieżki:

echo 'export PATH="$PATH:~/.local/bin"' >> ~/.bashrc

i ostatecznie uruchomić auto-konfigurację clickable:

Rysunek 1. Przycisk utworzony za pomocą kodu z Listingu 1 clickable setup

Aby wymagane typy były dostępne w tworzonym widoku, należy za- Następnym krokiem jest utworzenie nowego projektu za pomocą
importować je za pomocą deklaracji import. polecenia:

clickable create
KONFIGURACJA ŚRODOWISKA
Zanim zaczniemy budować aplikację, musimy przygotować środo- Do wyboru dostajemy jeden z kilku szablonów aplikacji i zgodnie
wisko pracy. W czasie kiedy rozwojem Ubuntu Touch zajmowało się z wcześniejszą deklaracją wybieramy opcję numer 2.

<26> {  1 / 2021 < 95 >  }


PROGRAMOWANIE URZĄDZEŃ MOBILNYCH

Rysunek 2. Szablony clickable

PROJEKT BAZOWY
Po wybraniu odpowiedniego szablonu i określeniu atrybutów apli-
kacji, takich jak jej nazwa, informacje o twórcy czy rodzaj licencji,
w katalogu, w którym wywołano polecenie, pojawi się szkielet pro-
jektu. Zawarte w nim pliki i katalogi mają określone role:
» katalog assets – umieszczamy w nim pliki graficzne używane
Rysunek 3. Menu clickable
przez naszą aplikację,
» katalog plugins – umieszczamy w nim pluginy C++, które mogą
dostarczać nowe typy dla QMLa, Po zbudowaniu projektu na ekranie pojawi się aplikacja z wygenero-
» katalog po – tutaj znajdują się translacje dla aplikacji wspierają- wanego szablonu – Rysunek 4.
cych wiele języków,
» katalog qml – w nim umieszczamy pliki z kodem QML,
» plik main.cpp – punkt wejściowy naszej aplikacji,
» plik manifest.json.in – opis aplikacji dla potrzeb OpenStore,
» plik *.apparmor – zawiera uprawnienia wymagane przez
aplikację,
» plik clickable.json – podstawowe informacje konfiguracyjne
clickable,
» plik CMakeLists.txt – konfiguracja generatora CMake.

Oczywiście nic nie stoi na przeszkodzie, aby rozbudować lub prze-


budować strukturę projektu. Aby tego dokonać, należy wprowadzić
odpowiednie zmiany w pliku CMakeLists.txt znajdującym się w ka-
talogu głównym, a także, jeśli wystąpi taka konieczność, stworzyć lub
edytować pliki CMakeLists.txt w innych miejscach.
W pliku main.cpp dzieje się kilka istotnych rzeczy. Najpierw
stworzona zostaje instancja klasy QGuiApplication, która odpowie-
dzialna będzie, między innymi, za główną pętlę zdarzeń oraz inicjali-
zację i finalizację aplikacji. Następnie utworzony zostaje obiekt klasy
QQuickView, którego rolą jest automatyczne załadowanie i wyświe-
tlenie widoku ze wskazanego pliku qml. Jest on również odpowie-
dzialny za obsługę żądań zmiany rozmiaru okna.

Listing 2. Inicjalizacja aplikacji i widoku

auto *app = new QGuiApplication(argc, (char**)argv);


app->setApplicationName("appname.yourname");
Rysunek 4. Wygenerowana aplikacja po uruchomieniu
qDebug() << "Starting app from main.cpp";

QQuickView *view = new QQuickView();


view->setSource(QUrl("qrc:/Main.qml"));
Przeanalizujmy zawartość pliku qml/Main.qml, w którym zawarto
view->setResizeMode(QQuickView::SizeRootObjectToView); deklarację interfejsu przedstawionego na Rysunku 4.
view->show();

return app->exec();
Listing 3. Plik qml/Main.qml

import QtQuick 2.7


import Ubuntu.Components 1.3
Aby uruchomić aplikację bez konieczności podłączania prawdziwego //import QtQuick.Controls 2.2
urządzenia, możemy skorzystać z komendy clickable desktop lub, import QtQuick.Layouts 1.3
import Qt.labs.settings 1.0
jeśli korzysta się z Atoma, z menu przedstawionego na Rysunku 3.

<28> {  1 / 2021 < 95 >  }


/ Aplikacje natywne na Ubuntu Touch /

import Example 1.0

MainView {
id: root
objectName: 'mainView'
applicationName: 'appname.yourname'
automaticOrientation: true

width: units.gu(45)
height: units.gu(75)

Page {
anchors.fill: parent
header: PageHeader {
id: header
title: i18n.tr('empty')
}

ColumnLayout { Rysunek 5. Przykłady konwersji jednostki gu dla różnych typów urządzeń


spacing: units.gu(2)
anchors {
margins: units.gu(2) Jednostka gu jest jednostką podstawową i powinna być stosowana do
top: header.bottom
left: parent.left wszystkich wymiarów, chyba że konieczne jest wyrażenie wielkości
right: parent.right mniejszych niż 1 gu – wtedy zalecane jest użycie dp.
bottom: parent.bottom
} Komponentem będącym jedynym potomkiem MainView jest
Item { Page. Jest on używany do tworzenia pojedynczego widoku i dostar-
Layout.fillHeight: true cza nagłówek. Poniżej deklaracji nagłówka znajduje się następny za-
}
gnieżdżony element – ColumnLayout. Jest to typ definiujący układ
Label {
id: label swoich widoków-potomków jako kolumnę bądź stos. Wewnątrz ko-
Layout.alignment: Qt.AlignHCenter lumny znajdują się jeszcze 4 elementy:
text: i18n.tr('Press the button below'
+ ' and check the logs!') » Label – widok wyświetlający tekst,
} » Button – przycisk z tekstem,
Button { » Dwa elementy podstawowego typu Item służące do wypełnienia
Layout.alignment: Qt.AlignHCenter
text: i18n.tr('Press here!') strony pod i nad przyciskiem oraz tekstem tak, aby znajdowały
onClicked: Example.speak() się one w centrum i blisko siebie.
}

Item {

}
Layout.fillHeight: true PROJEKT: LISTA NOTATEK
} Na podstawie projektu bazowego zbudujemy prostą aplikację umoż-
} liwiającą tworzenie listy notatek składających się z tytułu i opisu. Aby
}
lista nie ginęła po wyłączeniu aplikacji, dane przechowywanie będą
w bazie danych. Część związana z zapisywaniem danych oraz udo-
Pierwsza część pliku to sekcja importu – widzimy tutaj import typów stępnianiem ich widokom QML wykonana zostanie w C++.
Qt Quick, komponentów specyficznych dla Ubuntu, typów layoutów, Aplikacja składa się z dwóch widoków – głównego, zawierającego
a także typu Settings umożliwiającego zapisywanie ustawień apli- listę notatek, oraz dodatkowego, umożliwiającego tworzenie nowych
kacji oraz typu Example, który jest przykładowym pluginem z kata- bądź edycję istniejących. W celu ułatwienia nawigacji między stro-
logu plugins. Poniżej znajduje się deklaracja typu MainView – jest to nami zastosowano komponent PageStack, który zarządza stosem
widok główny stanowiący korzeń drzewa widoków i musi znajdować widoków i automatycznie dodaje w nagłówku przycisk powrotu do
się w każdej aplikacji dla Ubuntu Touch. Wewnątrz obiektu Main- poprzedniej strony. Aby zapewnić spójny styl, dodany został kompo-
View znajdują się deklaracje jego atrybutów i kolejne zagnieżdżone nent AppPage, na podstawie którego powstaną wszystkie strony apli-
elementy wyświetlanej strony. Zanim przejdziemy do pozostałych kacji. Jest to w rzeczywistości pochodzący z Ubuntu.Components typ
komponentów, zwróćmy uwagę na to, jak zdefiniowano wysokość Page z zagnieżdżonym prostokątem pełniącym rolę tła oraz definicją
i szerokość MainView – zamiast ustawienia określonej wartości licz- nagłówka. Dodatkowo AppPage deklaruje kilka własności będących
bowej użyto jednostki gu. Podobnie jak przy tworzeniu aplikacji na aliasami dla atrybutów zagnieżdżonych komponentów. Jest to ko-
inne systemy mobilne – np. Android, nie powinniśmy definiować nieczne, ponieważ nie są one bezpośrednio dostępne z zewnątrz.
wymiarów elementów interfejsu za pomocą wartości wyrażonych
Listing 4. Plik qml/components/AppPage.qml
w pikselach. Wynika to z tego, że różne urządzenia mają różną liczbę
pikseli na określoną powierzchnię ekranu. Aby zapewnić jednakowy import QtQuick 2.4
import Ubuntu.Components 1.3
wygląd interfejsu na różnych ekranach, wprowadzono dwie jednost-
Page {
ki – gu (grid unit) oraz dp (density independent pixel). Przykładowe property alias pageTitle: headerId.title
wartości, jakie przyjmuje jednostka gu w zależności od wyświetlacza, property alias pageHeader: headerId
property alias background: background
jakim dysponuje urządzenie, przedstawiono na Rysunku 5.
anchors.fill: parent

{  WWW.PROGRAMISTAMAG.PL  } <29>
PROGRAMOWANIE URZĄDZEŃ MOBILNYCH

Rectangle { Listing 7. Plik qml/qml.qrc.


id: background
color: "#424242" <RCC>
anchors.fill: parent <qresource prefix="/">
} <file>Main.qml</file>
<file>HomePage.qml</file>
header: PageHeader { <file>components/AppPage.qml</file>
id: headerId </qresource>
title: i18n.tr("Default AppPage Title") </RCC>
StyleHints {
foregroundColor: "white"
backgroundColor: "#212121"
dividerColor: UbuntuColors.slate

}
} MODEL DANYCH
}
Lista powinna zawierać dane. Aby tak się stało, należy ustawić dla
niej odpowiedni model. Klasę modelu możemy stworzyć przy po-
Strona główna składa się z komponentu AppPage, wewnątrz którego mocy C++, a następnie zarejestrować ją jako typ dostępny dla QML.
zagnieżdżony został obiekt ListView. Lewy, prawy i dolny punkt li- Wykorzystamy do tego celu klasę abstrakcyjną QAbstractListMod-
sty zakotwiczony został na odpowiednich krawędziach strony, nato- el. Wymaga ona implementacji (przynajmniej) metod przedstawio-
miast górny bezpośrednio pod nagłówkiem – chcemy uniknąć sytu- nych w Listingu 8.
acji, w której część listy byłaby przez niego przysłonięta.
Listing 8. Wymagane metody klasy QAbstractListModel
Listing 5. Plik qml/HomePage.qml
int rowCount(const QModelIndex &parent = QModelIndex()) const;
int columnCount(const QModelIndex& parent = QModelIndex()) const;
import QtQuick 2.4
QVariant data(const QModelIndex &index, int role) const;
import Ubuntu.Components 1.3

AppPage {
id: homePage Zapewniają one dostęp do podstawowych informacji, takich jak licz-
pageTitle: i18n.tr("Notes") ba elementów w liście, liczba kolumn w elemencie listy oraz wartość
ListView { dla wskazanego indeksu i roli. O ile rola indeksu jest oczywista, to
id: mainList
anchors.top: pageHeader.bottom cel użycia „roli” wymaga dodatkowego wyjaśnienia. Rolami posłu-
anchors.bottom: parent.bottom gujemy się jako dodatkowym selektorem, a ich konkretne znaczenie
anchors.left: parent.left
anchors.right: parent.right może być specyficzne dla danego widoku i modelu. Qt, za pomocą
} typu enumeracyjnego, definiuje kilka podstawowych ról, takich jak
}
na przykład:
» Qt::DisplayRole – służy do pobierania z modelu kluczowych
Chcemy, żeby MainView nie wyświetlał bezpośrednio żadnych wi- danych do wyświetlenia,
doków, jak to miało miejsce w przypadku aplikacji wygenerowanej, » Qt::DecorationRole – służy do pobierania „ozdobników”
a jedynie miał obiekt PageStack i automatycznie umieszczał na nim wiersza, takich jak ikony czy obrazki,
stronę główną. Wstawienie pierwszej strony na stos odbywa się po » Qt::FontRole – służy do definiowania rodzaju fontu, który po-
odebraniu sygnału o pomyślnym utworzeniu obiektu MainView. winien być użyty do renderowania przy zastosowaniu domyśl-
nego delegata.
Listing 6. PageStack w MainView i automatyczne ładowanie pliku HomePa-
ge.qml
Istnieje również możliwość określenia własnych ról bazujących na
import QtQuick 2.7
import Ubuntu.Components 1.3 Qt::UserRole.
import QtQuick.Controls 2.2 Do projektu dodajemy plik src/model/NotesListModel.hpp zawie-
import QtQuick.Layouts 1.3
import Qt.labs.settings 1.0 rający deklarację naszego modelu.
MainView { Listing 9. Model danych listy
id: root
objectName: 'mainView' class NotesListModel : public QAbstractListModel
applicationName: 'Notes' {
automaticOrientation: true Q_OBJECT
width: units.gu(45) public:
height: units.gu(75) NotesListModel(QObject * parent = 0);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
PageStack { int columnCount(const QModelIndex& parent = QModelIndex()) const
id: mainStack override;
anchors.fill: parent QVariant data(const QModelIndex &index, int role) const override;
}
private:
Component.onCompleted: mainStack.push( std::vector<QString> m_data;
Qt.resolvedUrl("HomePage.qml")) };
}

Ostatnią czynnością przed uruchomieniem aplikacji jest dodanie Poza wspomnianymi metodami w klasie umieszczono jeszcze zmien-
nowo powstałych plików qml do listy w pliku qml/qml.qrc. ną m_data do przechowywania danych modelu oraz makro Q_OBJECT.

<30> {  1 / 2021 < 95 >  }


/ Aplikacje natywne na Ubuntu Touch /

Jest ono potrzebne między innymi po to, aby umożliwić działanie me- anchors.top: pageHeaderBottom
anchors.bottom: parent.bottom
chanizmu slotów i sygnałów czy udostępnić informacje o typie w cza- anchors.left: parent.left
sie działania programu. anchors.right: parent.right

model: dataModel
Listing 10. Implementacja klasy NotesListModel
delegate: ListItem {
NotesListModel::NotesListModel(QObject* parent) id: listItem
: QAbstractListModel(parent) color: "#616161"
{ divider.colorTo: "#757575"
m_data = { "List element 1", "List element 2", "List element 3" }; ListItemLayout {
} title.text: model.display
int NotesListModel::rowCount(const QModelIndex &parent) const title.color: "#ffffff"
{ }
return m_data.size(); }
} }

int NotesListModel::columnCount(const QModelIndex& parent) const


{
return 1;
Delegat pozwala nam zdefiniować kolor tła, kolor separatora oraz
} layout elementu listy. Tutaj deklarujemy kolor tytułu elemen-
QVariant NotesListModel::data(const QModelIndex &index, int role) const tu listy jako biały, a tekst jako dane, jakie model zwraca dla roli
{
if (!index.isValid()) { Qt::DisplayRole. Ostatnią rzeczą, którą należy zrobić, żeby pro-
return QVariant();
gram poprawnie się skompilował, jest dodanie za pomocą dyrektywy
}
add_subdirectory() katalogu src/ w pliku CMakeLists.txt znajdują-
const int row = index.row();
switch(role) { cym się w katalogu głównym projektu.
case Qt::DisplayRole:
return m_data[row];

default:
return QVariant();
BAZA DANYCH I REPOZYTORIUM
}
Następnym etapem jest pozbycie się statycznych danych i zasilenie
}
modelu prawdziwymi informacjami z bazy danych. Dla potrzeb re-
alizacji tego zadania utworzymy klasy Database i NotesListRepos-
Dodatkowo zarówno w katalogu src/, jak i src/model/ należy dodać itory. Klasa Database odpowiedzialna będzie za enkapsulację
pliki CMakeLists.txt. obiektu bazy danych oraz inicjalizację tabel i tworzenie odpowiednio
skonfigurowanego obiektu zapytania SQL. Klasa NotesListRepos-
Listing 11. Plik src/CMakeLists.txt
itory zapewni wygodny interfejs umożliwiający wykonywanie
add_subdirectory(model) wszystkich potrzebnych zapytań, jednocześnie ukrywając ich szcze-
Listing 12. Plik src/model/CMakeLists.txt góły przed użytkownikiem.

target_sources(${PROJECT_NAME} PRIVATE Listing 16. Klasa Database


src/NotesListModel.cpp
) class Database
{
public:
Ten prosty model pozwoli nam sprawdzić działanie ListView. Jed- enum class DbColumn
{
nak żeby móc użyć go w kodzie QML, musimy go najpierw zareje- Id,
Title,
strować. W tym celu w pliku main.cpp, zaraz po utworzeniu QGuiAp- Description
plication, należy wywołać funkcję qmlRegisterType. };

static QString ColumnName(DbColumn column);


Listing 13. Rejestracja klasy NotesListModel w systemie typów QML static const Database& GetInstance();

qmlRegisterType<NotesListModel>("notes.model", 1, 0, "NotesListModel"); std::unique_ptr<QSqlQuery> createQuery() const;

~Database();
Następnie należy zaimportować NotesListModel w pliku qml/Ho- private:
Database(const Database&) = delete;
mePage.qml, ustawić go jako model listy i zadeklarować delegata od- Database& operator=(const Database&) = delete;
powiedzialnego za sposób wyświetlania elementów listy. Database();

bool exists();
Listing 14. Import NotesListModel w QML
void createNotesTable();
import notes.model 1.0 QSqlDatabase m_database;
};
Listing 15. Dodanie modelu i delegata do listy (nowy kod wytłuszczono)

NotesListModel {
id: dataModel Do stworzenia klasy wykorzystana została prosta implementacja
} wzorca Singleton. Interfejs składa się z 3 metod:
ListView { » ColumnName – zwraca nazwę kolumny dla wskazanego
id: mainList
identyfikatora,

{  WWW.PROGRAMISTAMAG.PL  } <31>
PROGRAMOWANIE URZĄDZEŃ MOBILNYCH

» GetInstance – tworzy i zwraca instancję klasy, {


return true;
» createQuery – tworzy obiekt klasy QSqlQuery skonfigurowany }
do łączenia się z konkretną bazą. return false;
}
Listing 17. Implementacja klasy Database void Database::createNotesTable()
{
static const char* DBNAME = "notes.db"; QSqlQuery query(m_database);
static const char* CREATE_NOTES_TABLE_STATEMENT = if(!query.exec(CREATE_NOTES_TABLE_STATEMENT))
"CREATE TABLE notes (" {
"id INTEGER PRIMARY KEY AUTOINCREMENT," qDebug() << query.lastError().text();
"title VARCHAR(255) NOT NULL," }
"description VARCHAR(8096) )"; else
{
const Database& Database::GetInstance() qDebug() << "Successfully created table notes";
{ }
static Database instance;
return instance; }
}

QString Database::ColumnName(DbColumn column) { Jest to w zasadzie dość standardowy kawałek kodu odpowiedzialnego
switch (column) {
case DbColumn::Id: za utworzenie bazy i odpowiedniej tablicy. Szczególną uwagę nale-
return "id"; ży zwrócić na część konstruktora, w której pobierana jest ścieżka do
case DbColumn::Title: przestrzeni, która dostępna jest do zapisu. Gdybyśmy pominęli ten
return "title";
krok, nie udałoby się stworzyć pliku bazy danych na urządzeniu.
case DbColumn::Description:
return "description";
Jak wcześniej wspomniano, nie chcemy tworzyć zapytań SQL ani
udostępniać niskopoziomowego obiektu klasy Database w QML. Do
default:
return ""; pobierania i zapisywania danych, zarówno w modelu, jak i w kodzie
}
QML, wykorzystamy klasę NotesListRepository.
}
Listing 18. Klasa NotesListRepository
Database::~Database()
{ class NotesListRepository : public QObject
m_database.close();
{
}
Q_OBJECT
Database::Database() public:
: m_database(QSqlDatabase::addDatabase("QSQLITE")) static NotesListRepository& GetInstance();
{
Q_INVOKABLE void remove(int id);
QDir dbDir;
Q_INVOKABLE void add(QString title, QString description);
dbDir.mkpath(
Q_INVOKABLE void update(int id, QString title, QString description);
QStandardPaths::writableLocation(
QStandardPaths::DataLocation) + "/Database"); std::vector<NoteItem> getAll() const;
dbDir.setPath( int columnsCount() const;
QStandardPaths::writableLocation( private:
QStandardPaths::DataLocation) + "/Database"); NotesListRepository(const NotesListRepository&) = delete;
QString dbName(dbDir.absolutePath() + DBNAME); NotesListRepository& operator=(const NotesListRepository&) = delete;
if(!exists(dbName)) NotesListRepository() = default;
{
qDebug() << "Database does not exist."; std::unique_ptr<QSqlQuery> prepareQuery(QString statement) const;
qDebug() << "Attempting to create..."; void debugQueryLog(const QSqlQuery* query) const;
m_database.setDatabaseName(dbName); void execute(std::unique_ptr<QSqlQuery> query);
m_database.open();
signals:
if(!m_database.isOpen()) void refreshData();
{
qDebug() << "ERROR: could not open database!"; };
qDebug() << m_database.lastError().text();
}
else
{ Do przekazywania danych notatki klasa NotesListRepository ko-
createNotesTable(); rzysta z prostej struktury przedstawionej w Listingu 19.
}
} Listing 19. Struktura NoteItem
else
{
struct NoteItem
qDebug() << "Database already exists.";
{
m_database.setDatabaseName(dbName);
int id;
m_database.open();
QString title;
}
QString description;
} };

std::unique_ptr<QSqlQuery> Database::createQuery() const


{ Podobnie jak miało to miejsce z klasą NotesListModel, musimy za-
return std::make_unique<QSqlQuery>(m_database);
} rejestrować typ NotesListRepository w QML. Aby było to możli-
we, klasa musi dziedziczyć z QObject i zawierać makro Q_OBJECT.
bool Database::exists()
{ Ponadto metody, których chcemy używać z poziomu QML, muszą
if(QFile::exists(DBNAME))
zostać opatrzone makrem Q_INVOKABLE.

<32> {  1 / 2021 < 95 >  }


/ Aplikacje natywne na Ubuntu Touch /

Listing 20. Implementacja NotesListRepository

NotesListRepository& NotesListRepository::GetInstance() ustawienie własności kontekstu w obiekcie klasy QQmlEngine. Nasze


{
static NotesListRepository instance;
repozytorium będzie widoczne w QML pod nazwą, którą ustawimy
return instance; w kontekście – notesListRepoObj.
}

std::vector<NoteItem> NotesListRepository::getAll() const Listing 21. Rejestracja typu NotesListRepository w QML


{
auto query = prepareQuery(
"SELECT id, title, description FROM notes"); qmlRegisterUncreatableType<NotesModel>("notes.repo", 1, 0,
if(!query->exec()) "NotesRepository", "Uncreatable type");
{
debugQueryLog(query.get()); QQmlEngine *engine = view->engine();
return std::vector<NoteItem>(); auto* repo = &NotesListRepository::GetInstance();
} engine->rootContext()->setContextProperty("notesListRepoObj", repo);
std::vector<NoteItem> items;
const int size = query->size();
if(size > 0)
{
Posiadając bazę danych i repozytorium, możemy zmodyfikować mo-
items.reserve(size); del, aby zaczął z nich korzystać. Ostatnią wersję danych pobraną z re-
}

while(query->next())
pozytorium przechowywać będziemy w prywatnej zmiennej m_data.
{ Utworzymy również slot, który będziemy mogli połączyć z sygnałem
NoteItem item;
item.id = query->value(Database::ColumnName( o zmianie danych pochodzącym z repozytorium.
Database::DbColumn::Id)).toInt();
item.title = query->value(
Database::ColumnName( Listing 22. Nowy slot i zmienna w klasie NotesListModel
Database::DbColumn::Title)).toString();
item.description = query->value( std::vector<NoteItem> m_data;
Database::ColumnName(
Database::DbColumn::Description)) public slots:
.toString();
items.emplace_back(std::move(item)); void dataChanged();
}
return items; Listing 23. Połączenie sygnału i slotu
}

void NotesListRepository::remove(int id) NotesListModel::NotesListModel(QObject* parent)


{ : QAbstractListModel(parent)
auto query = prepareQuery( {
"DELETE FROM notes WHERE id=?");
query->addBindValue(QString::number(id));
auto& repository = NotesListRepository::GetInstance();
execute(std::move(query)); QObject::connect(&repository,
} &NotesListRepository::refreshData,
this,
void NotesListRepository::update(int id, QString title, QString &NotesListModel::dataChanged);
description)
{ m_data = repository.getAll();
}
auto query = prepareQuery(
"UPDATE notes SET title=?, description=? "
"WHERE id=?"); Aby umożliwić pobranie identyfikatora i opisu notatki, musimy dodać
query->addBindValue(title);
query->addBindValue(description); samodzielnie zdefiniowane role i metodę zwracającą mapę ich nazw.
query->addBindValue(id);
execute(std::move(query));
} Listing 24. Nowe role i metoda roleNames
void NotesListRepository::add(QString title, QString description)
{ enum ModelRoles {
auto query = prepareQuery( IdRole = Qt::UserRole + 1,
"INSERT INTO notes (title, description) " TitleRole,
"VALUES (:title, :description)"); DescriptionRole
query->addBindValue(title); };
query->addBindValue(description);
execute(std::move(query)); QHash<int, QByteArray> roleNames() const;
}

std::unique_ptr<QSqlQuery> NotesListRepository::prepareQuery(QString
statement) const
{ Aktualizacji wymagają również metody rowCount() oraz data(),
auto query = Database::GetInstance().createQuery();
query->prepare(statement); tak aby zwracały faktyczne dane i ich rozmiar. Należy także zaimple-
return query;
} mentować pełniącą rolę slotu metodę dataChanged().
void NotesListRepository::execute(std::unique_ptr<QSqlQuery> query)
{ Listing 25. Implementacja roleNames() i integracja z repozytorium
if(!query->exec())
{ QHash<int, QByteArray> NotesListModel::roleNames() const
debugQueryLog(query.get()); {
} QHash<int, QByteArray> roles;
else
{ roles[IdRole] = "id";
emit refreshData(); roles[TitleRole] = "title";
} roles[DescriptionRole] = "description";
} return roles;
}
void NotesListRepository::debugQueryLog(const QSqlQuery* query) const
{ int NotesListModel::rowCount(const QModelIndex &parent) const
qDebug() << "[SQL query log]: " << query->lastError().text(); {
} return m_data.size();
}

Rejestrujemy typ NotesListRepository w QML jako uncreatable type, QVariant NotesListModel::data(const QModelIndex &index, int role) const
{
a następnie przekazujemy wskaźnik do jedynej jego instancji poprzez

{  WWW.PROGRAMISTAMAG.PL  } <33>
PROGRAMOWANIE URZĄDZEŃ MOBILNYCH

if(!index.isValid()) anchors.fill: parent


{ maximumLength: 8096
return QVariant(); text: description
} hintAnchors.margins: units.gu(1.2)
const int row = index.row(); hintFont.pointSize: units.gu(2)
switch(role) hintText: "Enter description"
{ }
case TitleRole: }
return m_data[row].title;
Component.onCompleted: {
case IdRole: if (id > 0) {
return m_data[row].id; root.pageTitle = "Edit entry"
} else {
case DescriptionRole:
root.pageTitle = "Create entry"
return m_data[row].description;
}
default: }
return QVariant();
} Component.onDestruction: {
if (titleInput.text.length > 0) {
} if (id > 0) {
notesListRepoObj.update(id,
void NotesListModel::dataChanged()
titleInput.text,
{
descInput.text)
auto& repository = NotesListRepository::GetInstance();
beginResetModel(); } else {
m_data = repository.getAll(); notesListRepoObj.add(titleInput.text,
endResetModel(); descriptionInput.text)
} }
}
}
}
Logika jest gotowa. Teraz należy dodać nową stronę, przy pomo-
cy której będzie można stworzyć nowy wpis lub edytować istnieją-
cy. Odpowiedzialny za to kod QML umieszczamy w nowym pliku Strona EditCreateEntryPage składa się z kilku części. Na szczycie
qml/EditCreateEntryPage.qml. Oczywiście należy jeszcze pamiętać komponentu AppPage znajdują się deklaracje własności (id, title,
o umieszczeniu stosownego wpisu w pliku qml/qml.qrc. description), których w przypadku edycji użyjemy do przekazania
informacji o wpisie. W części środkowej mieszczą się deklaracje pól
Listing 26. Deklaracja interfejsu użytkownika do tworzenia i edycji notatek
edycyjnych umieszczonych nad różnymi tłami. Na końcu znajdują
import QtQuick 2.4 się dwie akcje. Pierwsza zarejestrowana jest na sygnał o pomyślnym
import Ubuntu.Components 1.3
import "components" utworzeniu komponentu AppPage i dotyczy ustawienia odpowied-
import notes.repo 1.0 niego tytułu strony. Drugą zarejestrowano na sygnał o rozpoczęciu
AppPage { niszczenia komponentu AppPage, który zostanie wysłany podczas
id: root
anchors.fill: parent nawigacji do poprzedniej strony. Reakcją na ten sygnał jest zlecenie
pageTitle: i18n.tr("Create New Entry") obiektowi repozytorium utworzenia lub uaktualnienia wpisu. Na po-
property int id trzeby tej strony utworzony został nowy komponent, który wzbogaca
property string title
property string description standardowy TextInput o możliwość wyświetlenia wskazówki, pod-
Rectangle {
czas gdy pole edycji jest nieaktywne i nie ma żadnej zawartości.
id: titleBg
color: "#505050" Listing 27. TextInputWIthHint
anchors.top: pageHeaderBottom
anchors.left: parent.left import QtQuick 2.4
anchors.right: parent.right import Ubuntu.Components 1.3
height: units.gu(6)
Item {
TextInputWithHint { property alias text: input.text
id: titleInput property alias color: input.color
textAnchors.margins: units.gu(1.2) property alias maximumLength: input.maximumLength
font.pointSize: units.gu(2) property alias wrapMode: input.wrapMode
anchors.fill: parent property alias textAnchors: input.anchors
maximumLength: 255 property alias font: input.font
text: title property alias length: input.length
hintAnchors.margins: units.gu(1.2) property alias hintFont: hint.font
hintFont.pointSize: units.gu(2) property alias hintAnchors: hint.anchors
hintText: "Enter title" property alias hintText: hint.text
} property alias hintColor: hint.color
}
TextInput {
Rectangle { id: input
anchors.top: titleBg.bottom anchors.fill: parent
anchors.bottom: parent.bottom focus: true
anchors.left: parent.left color: "white"
anchors.right: parent.right maximumLength: 255
color: "#707070" wrapMode: TextInput.Wrap
}
TextInputWithHint {
id: descInput Label {
textAnchors.margins: units.gu(1.2) id: hint
font.pointSize: units.gu(2) anchors.fill: parent

<34> {  1 / 2021 < 95 >  }


/ Aplikacje natywne na Ubuntu Touch /

anchors.centerIn: parent
focus: true
}
font.italic: true
color: "#aaaaaa" MouseArea {
visible: { id: clickableArea
input.length == 0 && anchors.fill: parent
input.cursorVisible == false onClicked: parent.clicked()
} }
} }
}
Listing 29. Konfiguracja CircularButton w ramach HomePage.qml
TextInputWithHint składa się ze standardowego TextInput oraz CircularButton {
Label, który służy do wyświetlania wskazówki. Wprowadzone zosta- id: addButton
radius: units.gu(3.1)
ły również liczne aliasy, tak aby umożliwić w miarę elastyczną konfi- anchors.bottom: parent.bottom
gurację komponentu w miejscu jego użycia. Aby dostać się na nową anchors.right: parent.right
anchors.margins: units.gu(2.5)
stronę, musimy jeszcze utworzyć odpowiedni przycisk na stronie color: "#ffab00"
srcWidth: units.gu(2.5)
głównej. Chcąc utrzymać aplikację w stylu material design, dodamy
srcHeight: units.gu(2.5)
dedykowany komponent o nazwie CircularButton i jako przycisk srcFillMode: Image.PreserveAspectFit
buttonSrc: Qt.resolvedUrl("assets/plus.svg")
pływający umieścimy go jako ostatni element wewnątrz AppPage onClicked: {
w pliku HomePage.qml. mainStack.push(
Qt.resolvedUrl("EditCreateEntryPage.qml"))
Listing 28. Okrągły przycisk pływający }
}
import QtQuick 2.4
import Ubuntu.Components 1.3
Na koniec umożliwimy edycję oraz usuwanie wpisów z poziomu li-
Rectangle {
width: radius*2 sty. Rozpoczęcie edycji będzie możliwe poprzez dotknięcie elemen-
height: radius*2 tu listy. Dane wpisu zostaną przekazane do strony EditCreateEn-
property alias buttonSrc: image.source tryPage za pomocą zadeklarowanych w niej własności id, title,
property alias srcWidth: image.width
property alias srcHeight: image.height description. W celu umożliwienia usuwania wpisów, w ramach
property alias srcFillMode: image.fillMode delegata dodamy przycisk, a jego widoczność przełączać będziemy za
signal clicked() pomocą długiego przyciśnięcia elementu listy.
Image {
id: image
/* REKLAMA */

{  WWW.PROGRAMISTAMAG.PL  } <35>
PROGRAMOWANIE URZĄDZEŃ MOBILNYCH

Listing 30. Własność showDeleter należąca do mainList w qml/HomePage.


qml, która steruje widocznością przycisku do kasowania wpisów

property bool showDeleter: false

Listing 31. Przycisk do kasowania wpisów oraz reakcja na dotknięcie i długie


przyciśnięcie elementu listy

Button {
text: i18n.tr('Delete')
color: UbuntuColors.red
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: units.gu(1)
visible: mainList.showDeleter

onClicked: {
notesListRepoObj.remove(model.id)
}
}

onClicked: mainStack.push(
Qt.resolvedUrl("EditCreateEntryPage.qml"),
{ id: model.id, title: model.title,
description: model.description })

onPressAndHold: {
mainList.showDeleter = !mainList.showDeleter
}

Jesteśmy gotowi do zbudowania ostatecznej wersji naszego projek-


tu. W tym celu z Atoma lub wiersza poleceń wykonujemy komendę
clickable (jeśli mam podłączone do komputera urządzenie z Ubun- Rysunek 6. Przykładowe zrzuty ekranu aplikacji
tu Touch) lub clickable desktop, jeśli chcemy uruchomić aplikację
lokalnie. Wygląd aplikacji przedstawiono na Rysunku 6. wano prostą aplikację. Czytelnik mógł przekonać się, jak łatwo można
budować interfejs użytkownika, zastępując tradycyjny styl imperatywny

DEBUGOWANIE stylem deklaratywnym. Pokazano również, jak łączyć kod C++ z kodem
QML, a dodatkowo omówiono kwestię debugowania.
Podczas pracy nad aplikacją często zachodzi konieczność zdiagnozo- Zastanawiając się nad stylem tworzenia aplikacji dla Ubuntu To-
wania problemów i naprawienia błędów. Podobnie jak w innych śro- uch, można dojść do wniosku, że było ono w tej kwestii prekursorem.
dowiskach, problemy na Ubuntu Touch można rozwiązywać poprzez QML pojawił się w 2009 roku, a Ubuntu Touch stało się szerzej do-
zbieranie i analizę logów z urządzenia oraz pracę z debuggerem. stępne w roku 2014, podczas gdy Flutter – devkit Google wspierający
W przypadku uruchomienia aplikacji na desktopie logi pojawiają deklaratywny styl programowania interfejsu użytkownika – pojawił
się po prostu w terminalu, w którym wywołano komendę clickable się w roku 2017. W przypadku Apple było to jeszcze później – dający
desktop. Natomiast aby wyświetlić logi z urządzenia, należy skorzy- podobne możliwości SwiftUI ogłoszony został w roku 2019.
stać z polecenia clickable logs. W obydwu przypadkach będziemy Doświadczenie budowania aplikacji dla Ubuntu Touch jest cieka-
mogli zobaczyć wszystkie wiadomości wygenerowane przy użyciu we, a dzięki temu, że wykorzystuje się Qt i QML, zdobyta wiedza (po-
qDebug() oraz console.log(). mijając komponenty typowe dla Ubuntu) może być zastosowana do
W celu debugowania naszej aplikacji możemy skorzystać z na- tworzenia aplikacji na inne, w tym także mobilne, systemy operacyjne.
rzędzia gdb. Aby zrobić to na desktopie, należy wywołać polecenie
clickable desktop --gdb. Po uruchomieniu gdb postępujemy tak
jak w przypadku każdego innego programu. Istnieje również możli- W sieci
wość analizy problemów związanych z pamięcią takich jak, na przy-
» https://docs.ubports.com
kład, wycieki. Możemy to zrobić przy użyciu narzędzia valgrind uru- » https://api-docs.ubports.com/sdk/apps/qml/index.html
chamianego poleceniem clickable desktop ‑‑valgrind. » https://clickable-ut.dev/en/latest/
» https://doc.qt.io/qt-5/
Debugowanie bezpośrednio na urządzeniu wymaga wcześniej- » https://qmlbook.github.io/
szego uruchomienia na nim serwera gdb za pomocą polecenia » https://gitlab.com/ubports/apps
» https://phone.docs.ubuntu.com/en/apps/
clickable gdbserver. Następnie w osobnym terminalu urucha- » https://cmake.org/documentation/
miamy gdb, które powinno połączyć się do uruchomionego serwera.

MARCIN ŁAWICKI
PODSUMOWANIE
Autor zajmuje się programowaniem od kilkunastu lat. W tym czasie miał
okazję pracować nad różnego rodzaju oprogramowaniem, począwszy od
W artykule przedstawiono, w jaki sposób tworzy się aplikacje dla Ubun-
gier, przez aplikacje dla sektora bankowego i telekomunikacji, do aplika-
tu Touch, jak skonfigurować środowisko programistyczne, a następnie cji mobilnych, desktopowych czy IoT. Aktualnie pracuje jako programista
omówiono automatycznie wygenerowany przykład i zaimplemento- C++ w Huuuge Games, gdzie pomaga tworzyć technologie własne firmy.

<36> {  1 / 2021 < 95 >  }


PROGRAMOWANIE SYSTEMOWE

Pszczoła – najlepszy przyjaciel programisty


BPF to technologia, która pozwala na uruchamianie niewielkich programów w odpowiedzi na
zdarzenia, na przykład szeroko pojęte operacje wejścia/wyjścia (I/O), nieustannie zachodzące
podczas pracy systemu operacyjnego. To coś przypominającego JavaScript, znany osobom zaj-
mującym się programowaniem aplikacji webowych, aczkolwiek to daleko idące uproszczenie.

B PF nie jest nowym pomysłem. Pierwotnie była to nieskompliko-


wana maszyna wirtualna działająca wewnątrz systemów UNIX,
której jedynym przeznaczeniem było filtrowanie ruchu sieciowego.
cji logicznych, arytmetycznych oraz skoków warunkowych. Wszyst-
kie charakteryzują się stałą długością, każda zapisana na 64 bitach.
Format instrukcji przedstawiono na Rysunku 1.
Teraz – narzędzie wszechstronne, znajdujące zastosowanie w obsza-
rach związanych z bezpieczeństwem, sieciami, technologią kontene- 16 bitów 8 bitów 8 bitów 32 bity
rów, profilowaniem, monitorowaniem i wielu innych. opcode jt jf k

Rysunek 1. Format instrukcji BPF


KONTEKST HISTORYCZNY
Interpreter dba, żeby kolejność bajtów w poszczególnych polach od-
Filtr pakietów
powiadała natywnej kolejności bajtów procesora (ang. host endian-
Jest rok 1992. Steven McCanne i Van Jacobson publikują wyniki ness). Pole opcode przechowuje kod instrukcji. jt, jf są wykorzy-
swojej kilkuletniej pracy dotyczącej szybkiego filtrowania pakietów stywane przez instrukcje skoku warunkowego. Zawierają offset do
w dokumencie zatytułowanym „The BSD Packet Filter: A New Ar- następnej instrukcji dla warunków prawdziwego i fałszywego. War-
chitecture for User-level Packet Capture”1. Ich rozwiązanie znacznie tość 0 oznacza brak skoku. k jest ogólnego przeznaczenia i interpre-
przewyższa już istniejące. Z przeprowadzonej przez nich analizy wy- towane w kontekście wartości pola opcode. Na Rysunkach 2 i 3 po-
nika, że w zależności od rodzaju ruchu sieciowego konkurencyjne kazano możliwe tryby adresowania oraz zbiór dostępnych instrukcji.
rozwiązania (np. CMU/Stanford czy NIT firmy SUN) są średnio od
półtora do nawet stu pięćdziesięciu [sic!] razy wolniejsze. Tajemni-
ca sukcesu? Konkurencyjne rozwiązania opierają swoje działanie na
maszynie stosowej. Jednak nowoczesne procesory typu RISC operują
na pliku rejestrów. Interpreter języka filtrów oparty na maszynie re-
jestrowej wydaje się być naturalnym wyborem. Co więcej, zastosowa-
nie innego typu interpretera pozwala na użycie równoważnych funk-
cjonalnie, ale mniej złożonych obliczeniowo algorytmów do analizy
pakietów2. Ważną rolę odgrywa również opracowanie innego modelu
buforowania danych. Podczas gdy konkurencyjne rozwiązania tracą
cenne cykle procesora na wykonanie kopii pakietu, BPF przystępuje
do pracy, jak tylko kontroler DMA umieści dane w pamięci kompu-
tera. Werdykt pozytywny oznacza umieszczenie żądanej ilości da-
nych w niewielkim buforze, do którego dostęp ma aplikacja analizu-
jąca wycinek ruchu sieciowego.
Rysunek 2. Lista dostępnych instrukcji wraz z trybami adresowania
(źródło: http://www.tcpdump.org/papers/bpf-usenix93.pdf)
Zasada działania
Sercem mechanizmu filtrowania jest interpreter języka BPF. Dyspo-
nuje on trzema rejestrami: akumulatorem, rejestrem ogólnego prze-
znaczenia oraz ukrytym licznikiem programu. Każdy z rejestrów
zajmuje dokładnie 32 bity. Na dane tymczasowe przewidziany jest
fragment pamięci złożony z szesnastu 32-bitowych słów. Do tego do-
chodzi stosunkowo mała jak na standardy procesorów liczba instruk-

1.  Filtr pakietów, w pewnym uproszczeniu, jest zdefiniowany jako funkcja zwracająca prawdę lub
fałsz. Rezultat prawdziwy powoduje, że system operacyjny udostępnia pakiet aplikacji, która zainsta-
lowała filtr. W przeciwnym wypadku pakiet jest ignorowany.
Rysunek 3. Tryby adresowania (źródło: http://www.tcpdump.org/papers/bpf-usenix93.pdf)
2. Więcej informacji na temat filtrów opartych na drzewie wyrażeń (ang. binary expression tree)
i grafie kontroli przepływu (ang. flow control graph) można znaleźć w cytowanej publikacji.

<38> {  1 / 2021 < 95 >  }


PROGRAMOWANIE SYSTEMOWE

Autorzy doskonale zdają sobie sprawę, że gwarantem sukcesu jest


BPF a Linux
nie tylko świetny koncept, ale też wsparcie społeczności odpowied-
nim narzędziem. Naturalnym wyborem jest program tcpdump, który Technologia BPF zyskuje swój odpowiednik nazwany Linux Socket
zresztą sami stworzyli. Wbudowany w niego kompilator tłumaczy Filtering w 1997 roku. Na salonach gości kernel w wersji 2.1.x. Za-
wyrażenie filtrujące na listę instrukcji programu BPF, które ostatecz- chowana jest kompatybilność z pierwowzorem, ale wprowadzane są
nie trafiają do kernela. ulepszenia. Kernel pozwala aplikacji z przestrzeni użytkownika do-
Przykładowy filtr akceptujący ramki ethernetowe, których adre- łączyć filtr w postaci listy instrukcji (zobacz Listing 1) do wybranego
satem jest komputer o adresie MAC 00:1e:ec:d1:f2:32, przedsta- socketa, używając funkcji setsockopt(). Tym samym tworzy się lo-
wiono w Listingu 1. kalny firewall przepuszczający do aplikacji tylko istotny z jej punktu
widzenia ruch sieciowy4.
Listing 1. Filtr w postaci listy instrukcji
W myśl zasady, że jeśli coś działa dobrze, to lepiej tego nie ruszać,
# tcpdump -i enp20s0 -dd ether src 00:1e:ec:d1:f2:32 w ciągu następnych kilku lat nie pojawiają się żadne przełomowe
{ 0x20, 0, 0, 0x00000008 },
{ 0x15, 0, 3, 0xecd1f232 }, zmiany. Trzeba poczekać aż do roku 2011, kiedy do kernela zostaje
{ 0x28, 0, 0, 0x00000006 }, dodany kompilator JIT (ang. just-in-time), który tłumaczy kod BPF
{ 0x15, 0, 1, 0x0000001e },
{ 0x6, 0, 0, 0x00040000 }, bezpośrednio na kod assemblera platformy x86. Skompilowany filtr
{ 0x6, 0, 0, 0x00000000 },
trafia do specjalnie wydzielonego fragmentu pamięci, skąd zosta-
nie wykonany po odebraniu pakietu5. Jakie korzyści przynosi zatem
Do analizy zdecydowanie bardziej nadaje się kod pokazany w Listin- kompilacja JIT? Pierwsze testy wykazały oszczędność 50 ns na wy-
gu 2, gdzie mało mówiące liczby zastępują mnemoniki. wołaniu prostego filtra6. To dużo, biorąc pod uwagę, że na przykład
czas upływający pomiędzy dwoma najmniejszymi ramkami etherne-
Listing 2. Filtr w formie czytelnej dla użytkownika
towymi na „wysyconym” łączu 10G to zaledwie 67.2 ns.
# tcpdump -i enp20s0 -d ether src 00:1e:ec:d1:f2:32 Kolejnym nieoczekiwanym użytkownikiem języka staje się podsys-
(000) ld [8] tem seccomp (skrót od secure computing), który w wersji 3.4 kernela zo-
(001) jeq #0xecd1f232 jt 2 jf 5
(002) ldh [6] staje wzbogacony o możliwość filtrowania wywołań systemowych, przy
(003) jeq #0x1e jt 4 jf 5 użyciu BPF. Idea seccompa jest prosta. Polega na ograniczeniu dostępu
(004) ret #262144
(005) ret #0 procesowi do niektórych wywołań systemowych. Linux ma ich setki. Je-
śli proces zostanie skutecznie zaatakowany, a atakujący uruchomi wywo-
Podczas analizy kodu programu pomocna jest grafika przedstawiają- łanie systemowe (o którym wiadomo, że ma niezałataną podatność), to
ca format ramki ethernetowej (Rysunek 4). takie zdarzenie może być brzemienne w skutkach. Istnieje bowiem szan-
sa, że system znajdzie się pod kontrolą atakującego. Pozwalając aplikacji
46-1500 na dostęp tylko do niektórych wywołań systemowych, znacznie ograni-
6 bajtów 6 bajtów 2 bajty 4 bajty
bajtów cza się wektor ataku. Z tego powodu seccomp jest kluczowym kompo-
Destination Source MAC Type / Length Payload Frame check
nentem, jeśli chodzi o budowanie sandboxów dla aplikacji.
MAC sequence

Rysunek 4. Ramka sieci Ethernet


Ewolucja
Pierwotne założenia o małej ilości nieskomplikowanych instrukcji
Krok (000) to załadowanie 4 bajtów spod offsetu 8 do akumulatora. języka przestają pasować do realiów nowoczesnych 64-bitowych ma-
Z Rysunku 4 wynika, że są to cztery ostatnie bajty adresu źródłowe- szyn. BPF przyciąga uwagę coraz to większej liczby programistów i do
go. Następnie w (001) zawartość akumulatora porównywana jest kernela 3.15 zostaje wprowadzona kolejna znacząca zmiana. Od teraz
z ostatnimi czterema bajtami spodziewanego adresu MAC. Jeśli re- język ma dwa warianty. Klasyczny BPF (classic BPF, cBPF) i rozszerzo-
zultat okaże się prawdziwy, interpreter rozpocznie wykonywanie in- ny BPF (extended BPF, eBPF). Kompatybilność dwóch jest zachowana.
strukcji znajdującej się w linii (002). W przeciwnym wypadku nastąpi cBPF jest przez kernel automatycznie tłumaczony do wersji rozszerzo-
skok do instrukcji (005), tym samym odrzucając ramkę. Instrukcja nej. Zmianie ulega format instrukcji. Nowy format pozwala na większe
(002) powoduje umieszczenie w akumulatorze pozostałych bajtów, upakowanie instrukcji, dzięki czemu udaje się zaoszczędzić miejsce na
tj. dwóch najstarszych. Jeżeli porównanie w kroku (003) okaże się nowe. Format instrukcji eBPF zaprezentowano na Rysunku 5.
prawdziwe, filtr zakończy swoje działania w kroku (004). Zwracana
wartość oznacza maksymalny rozmiar bufora po stronie aplikacji3. 8 bitów 4 bity 4 bity 16 bitów 32 bity
Offsety używane przez instrukcje skoków warunkowych różnią op dst_reg src_reg off imm32
się między Listingami 1 oraz 2. Nie jest to błąd. W rzeczywistości
Rysunek 5. Format instrukcji eBPF
offsety są wyliczane względem następnej instrukcji, a nie początku
programu, jak ma to miejsce w Listingu 2. 4.  Binarne instrukcje filtra zapisane w tablicy są doczepiane przy pomocy opcji SO_ATTACH_FIL-
TER. Podobnie zwalniane, używając SO_DETACH_FILTER. Po więcej informacji odsyłam na siódmą
stronę podręcznika funkcji socket().
5.  Mowa tu o fragmencie pamięci ciągłej wirtualnie z ustawionymi prawami do wykonania kodu.
3.  Gdyby ktoś miał ochotę zajrzeć do źródeł programu tcpdump, to magiczna liczba kryje się pod Zainteresowanych odsyłam do funkcji kernela module_alloc().
makrem MAXIMUM_SNAPLEN. 6.  https://lwn.net/Articles/437986/

<40> {  1 / 2021 < 95 >  }


/ Pszczoła – najlepszy przyjaciel programisty /

Pole op przechowuje kod instrukcji. dst_reg oraz src_reg prze-


Pierwszy program
chowują numery, odpowiednio, docelowego i źródłowego rejestru.
off, używany przez część instrukcji, przechowuje offset względny BPF to technologia rozwijana przez programistów Linuksa dla „pro-
w bajtach. Z kolei przeznaczeniem imm32 jest przechowywanie war- gramistów” Linuksa. Wybór języka, w którym będą pisane filtry,
tości stałej. Dwa ostatnie pola są liczbami ze znakiem. pada na język C8. Oczywiście, w dalszym ciągu można pisać filtr,
W eBPF wszystkie rejestry są 64-bitowe. Rośnie również ich używając assemblera (a właściwie makrodefinicji odpowiadających
liczba. Dostępnych jest jedenaście, z których dziesięć jest ogólnego poszczególnym instrukcjom), jednak jest to najczęściej niepraktycz-
przeznaczenia, a jeden, tylko do odczytu, służy do przechowywania ne, nieefektywne oraz czasochłonne. Zdarza się jednak, że nawet
adresu ramki stosu. Biorąc pod uwagę, że współczesne procesory najlepszy kompilator nie zastąpi doświadczonego inżyniera w zagad-
64-bitowe również mają kilka rejestrów ogólnego przeznaczenia, nieniach związanych z optymalizacją kodu. Nie powinno to zaskaki-
udaje się utworzyć ABI dla BPF. Od tego momentu każdemu reje- wać zwłaszcza programistów zajmujących się na co dzień systemami
strowi maszyny wirtualnej BPF odpowiada rejestr procesora. ABI wbudowanymi.
dobrane jest tak, żeby pasowało do innych ABI dla architektur 64-bi- Przygodę z BPF rozpoczniemy od analizy programu z Listingu 3.
towych wspieranych przez kernel. Po kompilacji JIT instrukcje pro- Zadaniem programu jest wpisanie do pliku /sys/kernel/debug/tracing/
gramu BPF wykonywane są niemal natywnie. Oprócz tego możliwe trace tekstu "hello bpf\n". Ma się tak stać za każdym razem, gdy
jest wywoływanie funkcji pomocniczych zdefiniowanych w kernelu kernel rozpocznie wykonywanie wywołania systemowego openat().
czy wykonywanie innych programów BPF. W Tabeli 1 zawarto krótki
Listing 3. Pierwszy program
opis przeznaczenia poszczególnych rejestrów.
1 #include <linux/bpf.h>
2 #include <bpf/bpf_helpers.h>
Rejestr Przeznaczenie 3
R0 Przechowuje wartość zwracaną przez funkcję pomoc- 4 char fmt[] = "hello bpf\n";
niczą (BPF helper) oraz rezultat programu BPF 5
6 SEC("tracepoint/syscalls/sys_enter_openat")
7 int bpf1(void *ctx)
R1-R5 Służą do przekazywania argumentów do funkcji po- 8 {
mocniczych. Rejestr R1 przechowuje kontekst funkcji 9 bpf_trace_printk(fmt, sizeof(fmt));
10 return 0;
głównej programu BPF
11 }
12
R6-R9 Ta grupa rejestrów zachowuje wartość pomiędzy 13 char _license[] SEC("license") = "GPL";
kolejnymi wywołaniami funkcji

R10 Rejestr przechowujący wskaźnik ramki stosu, tylko do


odczytu Dyrektywa z linii 1 powoduje wczytanie pliku nagłówkowego zawie-
rającego typy oraz definicje powszechnie używane przez programy
Tabela 1. ABI eBPF BPF. W kolejnej wczytywany jest kolejny, zawierający listę funkcji po-
mocniczych. W nomenklaturze BPF takie funkcje noszą nazwę BPF
eBPF to nie tylko zmiany samego języka, ale także dodanie map, czyli helpers, a bpf_trace_printk() z linii 9 to przykład jednej z wielu.
struktur danych służących do przechowywania danych, znaczne roz- W linii 4 zdefiniowany jest tekst do wyświetlenia. Można zadać py-
budowanie istniejącego weryfikatora języka oraz wprowadzenie de- tanie, dlaczego znalazł się akurat w tym miejscu, a nie w ciele funk-
dykowanego wywołania systemowego bpf(). cji bpf1(). Okazuje się, że zdefiniowanie go bezpośrednio w funkcji
Zanim program BPF zostanie uznany za bezpieczny i dopuszczo- spowoduje, że kompilator umieści go w sekcji .rodata.str1.1 pliku
ny do przyszłego wykonania, jest skrupulatnie sprawdzany przez ker- ELF. Ta nie jest aktualnie wspierana przez bibliotekę pomocniczą.
nel pod kątem potencjalnych zagrożeń. Na przykład nie są akcepto- Zmienna globalna trafi natomiast do obsługiwanej sekcji .data.
wane programy, które się nigdy nie kończą. Ani też takie, które mają Makro SEC() skrywa atrybut kompilatora, który informuje go,
zbyt dużą liczbę instrukcji. Obowiązujący limit to 4096 instrukcji na w jakiej sekcji pliku ELF powinna znaleźć się funkcja (lub zmienna).
program. Jeszcze inne to te, które zawierają „martwy” kod, czyli in- Sekcje kodu w teorii można nazwać zupełnie dowolnie. W praktyce
strukcje, do których interpreter nie może dotrzeć. Weryfikator dba pewne ograniczenia narzuca biblioteka libbpf. To niewielka zapłata za
także o to, żeby program otrzymał poprawny kontekst oraz dostęp wygodne API, które wyręcza programistę w żmudnym procesie, po-
tylko do niektórych funkcji kernela7. cząwszy od parsowania plików ELF, na zarządzaniu programami BPF
kończąc. Umieszczenie funkcji bpf1() w sekcji tracepoint/sys-

BPF calls/sys_enter_openat to informacja dla libbpf o miejscu w ker-


nelu, do którego przypiąć filtr. W linii 7 rozpoczyna się ciało funkcji
Historyczne spojrzenie na technologię BPF było potrzebne, dlatego głównej programu, która jako jedyny argument przyjmuje wskaźnik
że dostarczyło kontekst. Pozwoliło na prześledzenie łańcucha przy- na typ void. Więcej informacji na temat kontekstu programów BPF
czynowo-skutkowego, co powinno ułatwić zrozumienie samej tech- znajduje się w dalszej części artykułu. Każdy program musi być zgodny
nologii. Pora na uzupełnienie wiedzy o kilka kolejnych elementów z licencją. GPL to często bezpieczny wybór. Nic nie stoi na przeszko-
układanki. dzie, żeby wybrać inny wariant licencji, na przykład Dual BSD/GPL.

7.  Więcej na temat procesu weryfikacji można przeczytać w dokumencie „Documentation/networ- 8.  Filtry można również pisać w wysokopoziomowych językach, jak Python (BPF Compiler Collec-
king/filter.rst” dostępnym w repozytorium z kodem Linuksa. tion, BCC) czy Go (gobpf ).

{  WWW.PROGRAMISTAMAG.PL  } <41>
PROGRAMOWANIE SYSTEMOWE

Do kompilacji potrzebne są nagłówki kernela, biblioteka libbpf, ki. struct bpf_object przechowuje informacje związane z samym
clang w wersji co najmniej 3.40 oraz llvm od od 3.7.1 wzwyż, przy plikiem ELF, na przykład o liczbie programów BPF w pliku. Z kolei
czym llvm musi być zbudowany z obsługą BPF. Można to sprawdzić, struct bpf_link reprezentuje miejsce instalacji filtra.
wydając polecenie z Listingu 5. Wywołanie z linii 10 spowoduje wczytanie ELFa z dysku, zwe-
ryfikowanie jego poprawności i przetworzenie go w reprezentację
Listing 4. Sprawdzenie wsparcia dla BPF
użyteczną dla biblioteki. Dalej następuje sprawdzenie, czy czasem nie
$ llc --version | grep bpf wystąpił błąd. Przed rozpoczęciem działania program musi zostać
bpf - BPF (host endian)
bpfeb - BPF (big endian) wgrany do kernela. Zajmuje się tym funkcja z linii 14 i robi to na
bpfel - BPF (little endian) podstawie nazwy sekcji "tracepoint/syscalls/sys_enter_ope-
nat", do której trafiła funkcja główna programu. Miejsce instalacji
Program należy skompilować, używając polecenia z Listingu 5. to tracepoint9, związany z wywołaniem systemowym openat().
W linijce 16 otwierany jest plik tylko po to, żeby zapewnić co naj-
Listing 5. Kompilacja pierwszego programu
mniej jedno wykonanie wspomnianej funkcji.
$ clang -O2 -g -target bpf -c bpf1.c -o bpf1.o Program ładujący należy skompilować przy użyciu polecenia:

Linsting 8. Polecenie odpowiedzialne za kompilację programu


Jeśli proces kompilacji przebiegł pomyślnie, w katalogu znajdzie się
plik bpf1.o. $ gcc -o loader loader.c -lbpf

Przed uruchomieniem otwieramy plik, do którego powędrują ko-


munikaty z filtra, a następnie uruchamiamy program ładujący. Pliki
Ładowanie programu
trace oraz trace_pipe różnią się sposobem, w jaki realizowany jest do-
Procesy komunikują się z systemem operacyjnym przy pomocy wy- stęp. W przypadku tego drugiego dostęp jest blokujący. Jeśli wszystko
wołań systemowych. Jedne odpowiadają za zapis do pliku, inne za przebiegło pomyślnie, powinniśmy uzyskać rezultat jak w Listingu 9.
wysłanie danych po sockecie, a jeszcze inne za operacje na progra-
Listing 9. Próba generalna
mach BPF. Prototyp funkcji bpf() przedstawiono w Listingu 6.
cat /sys/kernel/debug/tracing/trace_pipe &
Listing 6. Wywołanie systemowe bfp() (kernel/bpf/syscall.c) [1] 7955
# ./loader bpf1.o
int bpf(int cmd, union bpf_attr *attr, unsigned int size); loader-7965 [001] d..3 24539.968025:
bpf_trace_printk: hello bpf

#
Parameter attr odpowiada za przekazywanie danych pomiędzy ker-
nelem a przestrzenią użytkownika. Jego format zależy od parametru
Typy programów
cmd, który może przyjąć kilkadziesiąt wartości. W praktyce jednak,
zamiast bezpośrednio wywoływać bpf(), używa się wygodnych Programy BPF są wykonywane w następstwie zdarzeń zachodzących
wrapperów dostarczanych przez libbpf. w systemie operacyjnym. Z kolei z każdym zdarzeniem związany jest
Skorzystamy z jej dobrodziejstw do załadowania wcześniejszego pewien kontekst. Na przykład, w skład kontekstu wywołania systemo-
programu. W Listingu 7 znajduje się kod programu ładującego. Co wego lseek() wejdą między innymi deskryptor otwartego pliku, offset
prawda uproszczony, bez należytej obsługi błędów, jednak na tym czy informacja mówiąca, względem czego (początku, aktualnego offse-
etapie wystarczający. tu czy może końca pliku) należy go interpretować. Dlatego przy pisaniu
programu BPF trzeba z góry określić jego typ. Typ to również informacja
Listing 7. Przykład programu ładującego kod BPF
dla weryfikatora na temat funkcji pomocniczych, do których program
1 #include <fcntl.h> uzyska dostęp. Lista dostępnych typów zawiera kilkanaście pozycji, jed-
2 #include <bpf/libbpf.h>
3 /* Usage: ./loader bpfX.o */ nak nic nie stoi na przeszkodzie, żeby została w przyszłości rozszerzo-
4 int main(int argc, char *argv[]) {
5 struct bpf_program *prog; na o kolejne. Kompletną listę można znaleźć w pliku /usr/include/linux/
6 struct bpf_object *obj; bpf.h. Kilka przykładowych pozycji wraz z opisem znajduje się w Tabeli 2.
7 struct bpf_link *link;
8 const char *path;
9 path = argv[1]; Typ programu Opis
10 obj = bpf_object__open(path);
11 if (libbpf_get_error(obj) return 1; BPF_PROG_TYPE_XDP Programy XDP pozwalają uzyskać dostęp do nieprzetwo-
rzonego pakietu (tj. przed tym, jak kernel utworzy struktu-
12 if (bpf_object__load(obj)) return 2; rę struct sk_buff) zanim trafi do stosu sieciowego
13 prog = bpf_program__next(NULL, obj);
14 link = bpf_program__attach(prog); BPF_PROG_TYPE_PERF_EVENT Programy PERF_EVENT pozwalają na instalację filtra w
15 if (libbpf_get_error(link)) return 3; miejscu, gdzie występują tzw. perf events. Program perf
16 open(path, O_RDONLY); /* trigger syscall */ to wewnętrzny profiler Linuksa
17 return 0; BPF_PROG_LIRC_MODE2 Występują także bardziej egzotyczne typy programów,
18 } jak ten, który pozwala zdekodować transmisję IR
(podczerwień)

W liniach 5, 6 i 7 definiowane są trzy różne typy zmiennych. I tak,


Tabela 2. Przykłady typów wraz z opisem
struct bpf_program reprezentuje program BPF. Struktura zawiera
informacje o jego typie, ilości instrukcji, sekcji pliku ELF, w której
9.  Tracepoint to miejsce w Linuksie, w którym można zainstalować funkcję. Funkcja jest wywoływa-
się znajduje, oraz wszelkie inne istotne z punktu widzenia bibliote- na za każdym razem, gdy procesor wykonuje kod tracepointa.

<42> {  1 / 2021 < 95 >  }


/ Pszczoła – najlepszy przyjaciel programisty /

Przykładowy rezultat wykonania programu jest pokazany w Li-


Drugi program
stingu 12.
Zadanie polega na napisaniu programu, który wyświetli nazwę wła-
Listing 12. Rezultat wykonania
śnie otwieranego pliku. Na tym etapie wiadomo już, które z wywo-
łań systemowych za to odpowiada. Brakujący element układanki to # cat /sys/kernel/debug/tracing/trace_pipe &
[1] 8012
informacje o przekazywanym kontekście, a dokładnie o jego struk- # ./loader bpf2.o
turze. Skąd ją zabrać? To zależy. Czasami informacje o kontekście loader-8015 [000] d..3 24655.930737:
bpf_trace_printk: bpf2.o
kernel udostępnia w katalogu /sys, innym razem należy jej szukać
#
w źródłach Linuksa, bądź też dokumentacji. Czy wszystkie programy
danego typu otrzymają od kernela identyczny kontekst? Oczywiście, Kontekst jest ulotny, a informacje w nim zamieszczone mogą być warte
że nie. Przekazanie tego samego kontekstu do programu związanego zapisania. Do tego i innych celów służą mapy.
z wywołaniem systemowym reboot() i poll() byłoby pozbawio-
ne sensu, ponieważ obydwa są wykonywane w kompletnie innych
Mapy
okolicznościach. 
Informacje o kontekście związanym z openat() najłatwiej uzy- Mapy w świecie BPF zajmują specjalne miejsce. Służą do przechowy-
skać, odczytując plik /sys/kernel/debug/tracing/events/syscalls/sys_en- wania danych w postaci par klucz oraz wartość. Dostęp do wartości
ter_openat/format. Jego zawartość znajduje się w Listingu 10. uzyskuje się przy pomocy klucza. Mogą być współdzielone z inny-
mi programami BPF i, co najważniejsze, z aplikacjami działającymi
Listing 10. Kontekst openat()
w przestrzeni użytkownika.
# cat /sys/kernel/debug/tracing/events/syscalls/sys_enter_openat/format
name: sys_enter_openat
Niskopoziomowo zarządzanie mapami odbywa się przy użyciu
ID: 625 bpf(). Znacznie wygodniejsze jest jednak wykorzystanie funkcji bi-
format:
field:unsigned short common_type; offset:0; size:2; signed:0; bliotecznych. Z każdą utworzoną mapą związany jest deskryptor.
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0; Aplikacja posługuje się nim w celu uzyskania dostępu do mapy.
field:int common_pid; offset:4; size:4; signed:1;
field:int __syscall_nr; offset:8; size:4; signed:1; Zamknięcie aplikacji powoduje automatyczne zwolnienie miejsca
field:int dfd; offset:16; size:8; signed:0;
field:const char __attribute__((user)) * filename;
w pamięci, które zajmowała mapa.
offset:24; size:8; signed:0; Dostępnych jest wiele typów map, każdy przeznaczony do innego
field:int flags; offset:32; size:8; signed:0;
field:umode_t mode; offset:40; size:8; signed:0; zastosowania. Kilka przykładów znajduje się w Tabeli 3. Natomiast
print fmt: "dfd: 0x%08lx, filename: 0x%08lx, flags: 0x%08lx, mode: kompletna lista dostępna jest w pliku /usr/include/linux/bpf.h.
0x%08lx", ((unsigned long)(REC->dfd)), ((unsigned long)(REC->filename)),
((unsigned long)(REC->flags)), ((unsigned long)(REC->mode))
Typ mapy Opis
Wyświetlony format bez najmniejszych problemów można przekuć BPF_MAP_TYPE_HASH Mapy hashujące to jeden z pierwszych ty-
pów map ogólnego przeznaczenia, które
w strukturę. Należy jednak koniecznie zwrócić uwagę na rozmiary znalazły się w BPF. Kernel gwarantuje, że
poszczególnych pól (size) i ich wyrównanie względem początku dostęp do nich jest atomowy. Ich prze-
znaczenie i zastosowania nie odbiegają
struktury (offset). od tego spotykanego wszędzie indziej
Przykładowy program może wyglądać jak ten z Listingu 11. BPF_MAP_TYPE_ARRAY Mapa typu tablicowego jest zoptymalizo-
wana pod kątem szybkiego wyszukania
Listing 11. Drugi program elementu. W przeciwieństwie do po-
przedniego typu dostęp nie jest atomowy,
1 #include <linux/bpf.h> ale można go zapewnić przy pomocy
2 #include <bpf/bpf_helpers.h> __sync_fetch_and_add(). Częste
3 zastosowanie to zliczanie wystąpień
4 #define __aligned(x) __attribute__((aligned(x)))
5 BPF_MAP_TYPE_LPM_TRIE Mapy tego typu znajdują zastosowanie na
6 struct sys_enter_open_ctx { przykład w routerach, gdzie szybko trzeba
7 /* common types aren’t accessible */ podjąć decyzję o przekierowaniu pakietu,
8 char nothing; bazując na najlepszym dopasowaniu
9 int __syscall_nr __aligned(8); adresu IP
10 int dfd __aligned(8);
11 const char *filename __aligned(8); Tabela 3. Mapy wraz z krótkim opisem
12 int flags __aligned(8);
13 long long mode __aligned(8);
14 };
15 Mapę BPF definiuje się poprzez określenie jej typu, maksymalnej
16 char fmt[] = "%s\n";
17 liczby przechowywanych elementów, rozmiaru pojedynczego ele-
18 SEC("tracepoint/syscalls/sys_enter_openat") mentu, rozmiaru klucza oraz opcjonalnie flag. Do tego celu używa się
19 int bpf2(struct sys_enter_open_ctx *ctx)
20 { makr __uint(), __type() oraz __array(). Pewnym zaskoczeniem
21 bpf_trace_printk(fmt, sizeof(fmt), ctx->filename); może być fakt, że struktura definiująca mapę w rzeczywistości składa
22 return 0;
23 } się ze wskaźników. Jest to częściowo podyktowane potrzebą zacho-
24
25 char _license[] SEC("license") = "GPL";
wania kompatybilności wstecznej, częściowo chęcią zmniejszenia pli-
ków ELF, a częściowo chęcią uchwycenia informacji o typie wartości
przechowywanej w mapie (BTF – BPF Type Format).

{  WWW.PROGRAMISTAMAG.PL  } <43>
PROGRAMOWANIE SYSTEMOWE

Flagi mogą zmienić charakter pracy mapy, np. sprawiając, że apli- Listing 15. Odświeżony program ładujący kod BPF
kacja będzie mogła z niej tylko czytać. Elementy określające rozmiary
1 #include <bpf/bpf.h>
wyrażone są w bajtach. Przykład mapy przedstawiono w Listingu 13. 2 #include <bpf/libbpf.h>
3 #include <fcntl.h>
Mapa reprezentuje jednoelementową tablicę typu int. 4 #include <unistd.h>
5 /* Usage: ./loader bpfX.o */
Listing 13. Struktura opisująca mapę 6 int main(int argc, char *argv[]) {
7 struct bpf_program *prog;
1 struct { 8 struct bpf_object *obj;
2 __uint(type, BPF_MAP_TYPE_ARRAY); 9 struct bpf_link *link;
3 __type(key, int); 10 int key = 0, val, fd;
4 __type(value, int); 11 const char *path;
5 __uint(max_entries, 1); 12 path = argv[1];
6 } bpf3_map SEC(".maps"); 13 obj = bpf_object__open(path);
14 if (libbpf_get_error(obj)) return 1;
15 if (bpf_object__load(obj)) return 2;
Dostęp do map odbywa się za pośrednictwem funkcji bibliotecznych, 16 prog = bpf_program__next(NULL, obj);
17 link = bpf_program__attach(prog);
na przykład bpf_map_lookup_elem() czy bpf_map_update_elem().
18 if (libbpf_get_error(link)) return 3;
Kompletną listę można znaleźć w pliku /usr/include/bpf/bpf.h. 19 open(path, O_RDONLY);
20 fd = bpf_object__find_map_fd_by_name(obj, "bpf3_map");
21 if (fd < 0) return 4;
22 if (bpf_map_lookup_elem(fd, &key, &val)) return 5;
Trzeci program 23 printf("%d\n", val);
24 return 0;
W tym akapicie rozszerzymy pierwszy program o obsługę mapy. 25 }

Zadanie programu będzie tym razem polegać na zliczaniu ilości wy-


wołań openat(). W Listingu 14 pokazana jest odświeżona wersja Zmiany związane z odczytaniem wartości z mapy rozpoczynają się
programu. w linii 20. Tam na podstawie nazwy wyszukiwany jest deskryptor
mapy. W linii 22 deskryptor jest wykorzystywany do odczytania war-
Listing 14. Trzeci program
tości znajdującej się pod indeksem 0.
1 #include <linux/bpf.h> Po uruchomieniu programu ładującego powinniśmy zobaczyć na
2 #include <bpf/bpf_helpers.h>
3 przykład rezultat z Listingu 16.
4 struct {
5 __uint(type, BPF_MAP_TYPE_ARRAY); Listing 16. Rezultat wykonania
6 __type(key, int);
7 __type(value, int); # ./loader bpf3.o
8 __uint(max_entries, 1); # 1
9 } bpf3_map SEC(".maps");
10
11 SEC("tracepoint/syscalls/sys_enter_openat")
12
13
int bpf3(void *ctx)
{ ZAKOŃCZENIE
14 int key = 0, *value;
15 BPF to technologia, która na dobre zagościła w ekosystemie Linuk-
16 value = bpf_map_lookup_elem(&bpf3_map, &key);
17 if (value) sa. Udało się wokół niej skupić dużą społeczność użytkowników
18 *value += 1; oraz programistów. Trafiła pod strzechy firm IT. Na jej bazie wyrosły
19
20 return 0; – i wciąż rosną – nowe projekty. Katran – skalowalny load balancer
21 }
warstwy L4, Falco – monitor niepożądanych zachowań aplikacji czy
22
23 char _license[] SEC("license") = "GPL"; ply – dynamiczny tracer stworzony z myślą o systemach wbudowa-
nych to tylko kilka przykładów. Co rusz pojawiają się nowe zastoso-
Zmiany nie są wielkie – polegają jedynie na dodaniu obsługi mapy. wania. Ewolucja trwa i nic nie zapowiada, żeby w najbliższym czasie
W linii 16 na podstawie klucza wyszukiwana jest odpowiadająca mu coś lub ktoś miał przerwać jej bieg.
wartość i zwracany wskaźnik do elementu. Jeśli element istnieje, a ist-
nieje, bo to tablica, inkrementowany jest licznik.
Wymagany jest również lifting aplikacji ładującej program BPF.
Bibliografia
Nowsza wersja znajduje się w Listingu 15. 1. S. McCanne, V. Jacobson. „The BSD Packet Filter: A New Architecture for User-level
Packet Capture”
2. Mogul, J.C., and R.F. Rashid, and M.J. Accetta. „The packet filter: An efficient me-
chanism for user-level network code”
3. T. Høiland-Jørgensen, J. D. Brouer, d. Borkmann, J.Fastabend, T. Herbert, D. Ahern,
TOMASZ DUSZYŃSKI and D. Miller. „The eXpress Data Path: Fast Programmable Packet Processing in the
Operating System Kernel”
Programista systemów wbudowanych. Zawodowo 4. A JIT for packet filters (https://lwn.net/Articles/437981/)
zajmuje się tworzeniem sterowników kart siecio- 5. BPF: the universal in-kernel virtual machine (https://lwn.net/Articles/599755/)
wych dla systemu Linux. 6. A thorough introduction to eBPF (https://lwn.net/Articles/740157/)
7. Bounded loops in BPF for the 5.3 kernel (https://lwn.net/Articles/794934/)
8. eBPF – Introduction, Tutorials & Community Resources (https://ebpf.io)
9. Linux kernel sources (https://elixir.bootlin.com/linux/latest/source)

<44> {  1 / 2021 < 95 >  }


LABORATORIUM EVORAIN

Sieci neuronowe w pigułce


Sztuczna inteligencja jest obszerną dziedziną, w której sieci neuronowe odgrywają niezwykle
istotną rolę. Wiedza na temat ich podstaw pozwoli inaczej spojrzeć na rozwiązanie dotych-
czas nierozwiązywalnych problemów. W tym artykule przedstawione zostaną również wybra-
ne struktury głębokiego uczenia (ang. deep learning) – dziedziny, która zrewolucjonizowała
obszar sieci neuronowych. Deep learning otwiera się na wykorzystanie nowych możliwości
inteligentnego przetwarzania danych w sposób równoległy.

M nogość i stopień skomplikowania problemów, które stawiamy


do wykonania ludziom, coraz częściej skłania do kreowania
nowoczesnych algorytmów wspomagających pracę człowieka po-
inteligencji. Wyobraźmy sobie tylko, jak skomplikowane są gusta
użytkownika. Istnieje wiele produktów, które są opisane cechami. Po-
wiązania między cechami produktów, a także historią działań użyt-
przez automatyzację. Często możemy napotkać na problemy, które kownika lub podobnych użytkowników mogą być bardzo złożone,
są tak bardzo skomplikowane, że ciężko jest opisać reguły postępo- przez co warto jest zaangażować sztuczne sieci neuronowe do roz-
wania, które prowadzą do rozwiązania stawianych zadań. Przykładem wiązania tego typu problemów.
może być klasyfikacja chorób, przewidywanie pogody, rozpoznawanie Sceptykom, którzy uważają, że przydatność sieci neuronowych
pisma pisanego odręcznie czy też mowy, identyfikacja emocji ludzi. jest niewielka, chciałbym przypomnieć, że głębokie sieci neuronowe
Niejednokrotnie w swojej pracy nad AI (ang. artificial intelligence) na- wprowadziły istotną rewolucję w procesie wytwarzania oprogramo-
potkałem zadania, dla których ekspertom z wieloletnim stażem cięż- wania opartego o AI. Nierzadko byłem świadkiem, że wykorzystanie
ko jest znaleźć rozwiązanie. Przykładem może być detekcja komórek technik głębokiego uczenia daje bardzo dobre rezultaty. Jeśli jesteś
nowotworowych raka piersi na zdjęciach z biopsji. Na zdjęciach ilu- już zaznajomiony/a z podstawami sieci neuronowych, to zachęcam
strujących tysiące komórek nie każda z nich, która ma nieprawidło- do przyjrzenia się dalszej części artykułu, w której wspominam o kil-
wości w procesie ich podziału, jest nowotworowa. To bardzo złożony ku ważnych algorytmach z tej właśnie dziedziny.
problem, a w dodatku jeszcze nie dość zbadany przez medycynę. Nie- Aby dobrze zrozumieć sieci neuronowe, warto zacząć naukę od
dawno miałem okazję przyjrzeć się zdjęciom RTG płuc osób z wyni- podstaw. Nie zrażaj się, gdy napotkasz na trudności podczas procesu
kiem pozytywnym/negatywnym na COVID-19 i z pomocą głębokich nauki sztucznej inteligencji. Ja miałem ich sporo. Nie zwracałem też
sieci neuronowych (ang. deep neural networks) udało mi się stworzyć uwagi na wiele osób, które często negatywnie odnosiły się do nauki
system, który rozpoznaje, czy pacjent jest chory, oraz wskazuje na po- od podstaw, gdyż uważali, że uczenie się polegające na zrozumieniu
tencjalne ogniska chorobowe. Przykład działania tego systemu przed- każdej operacji matematycznej zachodzącej w sieciach neuronowych
stawiono na Rysunku 1. Po lewej widać surowy obraz, w środku mapę, jest bezcelowe, skoro dzisiaj mamy tak rozbudowane biblioteki do
na której przedstawiono interpretację działania sieci neuronowej pod uczenia maszynowego – wystarczy podać, jakie algorytmy chcemy,
względem wykrywania ognisk chorobowych, natomiast po prawej za- znać ich ogólne przeznaczenie, wrzucić dane i na pewno coś z tego
prezentowano zdjęcie będące połączeniem dwóch poprzednich. wyjdzie. Po wielu latach pracy muszę z całą pewnością powiedzieć,
Inny przykład mogą stanowić systemy rekomendacyjne, któ- że tak nie jest – znajomość każdej operacji matematycznej pozwoli ci
re obecnie są wyposażone w zaawansowane mechanizmy sztucznej dobrze projektować systemy oparte o AI.

Rysunek 1. Wynik działania systemu opartego o technikę głębokiego uczenia do rozpoznawania chorych na COVID-19 oraz wskazywania ognisk chorobowych w przypadku wyniku pozytyw-
nego. Jaśniejsze obszary wskazują na ogniska chorobowe

<46> {  1 / 2021 < 95 >  }


LABORATORIUM EVORAIN

BIOLOGICZNY NEURON Wzór na pobudzenie neuronu – agregację sygnału wygląda


następująco:
Sieci neuronowe, jak wskazuje sama nazwa, składają się z neuronów.
Algorytmy sieci neuronowych biorą początek od obserwacji biolo-
gicznych neuronów, które stanowią niezwykle ważne budulce orga-
nizmów żyjących oraz pełnią różnorakie funkcje. Przede wszystkim
dzięki nim człowiek potrafi m.in widzieć, mówić, liczyć, śpiewać Zagregowany sygnał y trafia do funkcji aktywacji i w ten sposób
i zapamiętywać duże ilości informacji. Na Rysunku 2 przedstawiono otrzymujemy sygnał wyjściowy z = f(y). Funkcja ta odgrywa niezwy-
uproszczoną budowę biologicznego neuronu. kle ważną rolę, gdyż transformuje zagregowany sygnał do formy po-
zwalającej osiągnąć ściśle określone ramy sygnału i w optymalniejszy
sposób dążyć do optimum globalnego. Przykładem takich funkcji
może być funkcja sigmoidalna, czy też bardzo szeroko stosowana
w różnych wariantach funkcja ReLU. Nieliniowe funkcje aktywacji
zapewniają dokładniejszą aproksymację dla wielu zadań natury nieli-
niowej, jak np. rozpoznawanie znaków.

Rysunek 2. Biologiczny neuron

Impulsy docierają do jądra komórkowego poprzez wypustki zwane


dendrytami. Można powiedzieć, że dendryty stanowią wejścia do neu-
ronu. Synapsy odgrywają niezwykle istotną rolę w procesie przesyłu in-
formacji. Sygnał przechodzący przez nie jest odpowiednio „wartościo-
wany” poprzez związki chemiczne zwane neuromediatorami. Wpływają
one na zwiększenie, bądź zmniejszenie, potencjału wejścia napływają- Rysunek 4. Przykładowe funkcje aktywacji. Artykuł odnoszący się do tematyki funkcji
aktywacji wraz z rysunkiem można znaleźć na: https://medium.com/@shrutijadon10104776/
cego do neuronu. Dzięki nim neuron „wie”, jak ważny jest dany impuls. survey-on-activation-functions-for-deep-learning-9689331ba092
W jądrze komórkowym impulsy są poddawane stosownemu przetwo-
rzeniu, a wynik tych działań wyprowadzany jest przez akson. Informa- Warto pamiętać, że funkcja aktywacji musi być funkcją ciągłą. To zna-
cja z aksonu wędruje do innych neuronów poprzez ich dendryty. czy, że w dowolnym miejscu tej funkcji możemy znaleźć pochodną.
O tym, jak ważne są pochodne w sieciach neuronowych, opowiem

SZTUCZNY NEURON w dalszej części artykułu. Na ten moment zaznaczę, że im bardziej


złożona obliczeniowo funkcja (np. zawiera w sobie liczbę Eulera pod-
Neurony stosowane w sztucznych sieciach neuronowych są zdecydo- niesioną do potęgi x), tym dłużej będzie w sieci zachodzić propagacja
wanie prostsze. To uproszczenie pozwala na stosowanie dużych sieci sygnału. Wykorzystanie danej funkcji aktywacji zależy od problemu,
neuronowych, które składają się z wielu neuronów. Zaawansowane na który chcemy znaleźć rozwiązanie za pomocą mechanizmów AI.
sieci neuronowe potrafią obciążyć sprzęt obliczeniowy w znacznym
stopniu. Na Rysunku 3 przedstawiono schemat sztucznego neuronu.
W jaki sposób sieci neuronowe są inteligentne?
Przedstawiony wcześniej schemat neuronu zawiera wagi, które są od-
powiednikami synaps neuronu biologicznego. To one zawierają wie-
dzę, która jest ustalana w procesie trenowania sieci. W jaki sposób
zatem ustalić te wagi? Recepta jest bardzo prosta. Najpierw jednak
opiszę krótko specyfikę danych. Zakładając, że chcemy sztuczny neu-
ron nauczyć w sposób nadzorowany (z nauczycielem) – znamy oczeki-
wane wyniki dla poszczególnych wejść (oznaczyłem je w kolumnie p).
W Tabelach 1 i 2 przedstawiono zestaw wzorców treningowych (uczą-
Rysunek 3. Schemat sztucznego neuronu
cych), jak również testowych. Pierwszy zbiór używany jest, jak sama
nazwa wskazuje, w procesie trenowania sieci. Natomiast testowy
Wejścia stanowiące sygnały napływające do neuronu oznaczone są zbiór używany jest do weryfikacji skuteczności działania sieci i nie
symbolami x1, x2,..., xn. Docierając do neuronu, każde z wejść połączo- uczestniczy on w procesie trenowania. Dla przykładu zilustrowania
ne jest z odpowiednią wagą, które stanowią odpowiedniki synaps. Po- działania sieci neuronowych posłużę się sztucznie zdefiniowanymi
budzenie neuronu (agregacja sygnału) oznaczone jako y obliczane jest danymi. Będzie to abstrakcyjny przykład, który pozwoli jednak zro-
na podstawie sumowania iloczynów wejść i odpowiadających im wag. zumieć ideę stosowania sieci. W Tabelach 1 i 2 każdy wiersz ozna-

<48> {  1 / 2021 < 95 >  }


/ Sieci neuronowe w pigułce /

cza pomiary dla pacjenta. Zmienna x1 opisuje temperaturę ciała, Pochodna funkcji aktywacji jest niezwykle ważna we wzorze ko-
natomiast zmienna x2 opisuje puls. Oczywiście istnieje wiele innych rekcji wag. Mówi ona nam o szybkości zmiany funkcji względem jej ar-
parametrów opisujących zdrowie pacjenta – ja chciałem stworzyć gumentów. Każda funkcja aktywacji ma swoją pochodną. Współczyn-
sztuczny przykład bazujący jedynie na dwóch atrybutach, gdyż łatwo nik uczenia jest jednym z hiperparametrów sieci i steruje wielkością
możemy je zilustrować w przestrzeni 2D. Zmienna p opisuje etykietę korekt dla wag. Jeśli jest większy, wtedy korekty są większe – i odwrot-
– czyli informację o tym, czy dany pomiar świadczy o tym, że osoba nie. Należy zadbać o dobre dopasowanie współczynnika uczenia.
jest chora (1), czy zdrowa (0). W tym miejscu warto wspomnieć o drobnej optymalizacji, która
przekłada się na zmniejszenie liczby obliczeń. Obliczona pochodna,
x1 x2 p
temp. ciała puls chory (1), zdrowy(0) oraz błąd na jego wyjściu, nie zmieniają się podczas procesu korekcji
34.5 45 1 wag. W związku z tym ten iloczyn można przenieść do osobnej zmien-
36.6 65 0 nej, której wartość wyliczana jest dla neuronu tylko raz. Następnie
41 120 1
człon e * f ’(z) każdej z wag możemy zastąpić wspomnianą zmienną.
36.7 70 0

Tabela 1. Wzorce treningowe


Dopasowanie współczynnika uczenia
Aby sprawdzić, jak nasz model się uczy i jak duże popełnia błędy, sto-
x1 x2 p
temp. ciała puls chory (1), zdrowy(0) sujemy tak zwaną funkcję kosztu. Jedną z miar błędu sieci jest funk-
33.2 57 1 cja błędu średniokwadratowego. Załóżmy, że mamy wybraną funkcję
37 80 0 kosztu i chcemy dopasować współczynnik uczenia. Możemy wyko-
36.4 85 0 rzystać metodę przeszukiwania siatki lub przeszukiwania losowego,
40.5 89 1 aby wybrać optymalną wartość, o których wspomniałem w poprzed-
nim artykule (Programista 7/2020 (94)). Najlepszym jednak wybo-
Tabela 2. Wzorce testowe rem jest dobór współczynnika uczenia w sposób automatyczny wedle
zmieniającej się funkcji kosztu, bądź rozważenie skorzystania z opty-
Ideą stosowania ciągu testowego jest zbadanie zdolności prawidłowej malizatorów adaptacyjnych1. Warto jednak wiedzieć, dlaczego jest
oceny rozpoznawania nieznanych przez sieć przypadków. Istnieje rów- to aż tak ważne. Na Rysunku 5 przedstawiłem wpływ zbyt niskiego
nież ciąg walidacyjny, który służy do odpowiedniego dopasowania hi- i zbyt wysokiego współczynnika uczenia na wartości funkcji kosztu.
perparametrów sieci, o których więcej w dalszej części artykułu. Hiper-
parametry te wpływają na budowę sieci oraz proces trenowania. Dzięki
walidacyjnym próbkom możemy optymalnie dobrać ich wartości.
Trenowanie należy wykonywać przez ustaloną liczbę epok. W ramach
epoki podajemy wielokrotnie (przez zadaną liczbę iteracji) przykłady
treningowe. Liczba ta może być ustalona doświadczalnie, tak samo jak
ilość iteracji w epoce. Ważne jest mierzenie błędu ogólnego sieci, jak
również wybranych metryk skuteczności. Należy jednak pamiętać, że
uczenie ze zbyt dużą ilością epok czy też iteracji nakłada duże ryzyko
na powstanie efektu uczenia się na pamięć. Sieć w takim przypadku źle
zareaguje na przykłady testowe, błędnie je rozpoznając.
Proces trenowania sztucznego neuronu można opisać w następu-
jący sposób:
Dla liczby epok od 1 do n
1. Przez ustaloną liczbę iteracji m wykonujemy następujące kroki:
1 a. Pobieramy losowo przykład z tablicy wzorców treningowych. Rysunek 5. Wpływ zbyt niskiego i zbyt wysokiego współczynnika uczenia na wartość funkcji kosztu
1 b. Podajemy do neuronu x1 oraz x2 i na tej podstawie wyzna-
czamy agregację sygnału y. Proces minimalizacji funkcji kosztu może być bardzo długotrwały
1 c. Następnie argument y podajemy do funkcji aktywacji, a wy- dla dużych sieci. Występuje wtedy wiele minimów lokalnych, w któ-
nik oznaczony jako z stanowi wyjście neuronu. re bardzo łatwo „wpaść”, jeżeli np. współczynnik uczenia będzie nie-
1 d. Obliczamy błąd stanowiący różnicę pomiędzy wartością dostosowany – w tym przypadku za niski. Równocześnie nigdy nie
z a wartością p (oczekiwaną) wedle wzoru: e = p - z. znajdziemy się w pobliżu minimum globalnego, jeżeli współczynnik
1 e. Następnie korygujemy wagi według wzoru: uczenia będzie zbyt wysoki. Dążymy zawsze do minimum globalne-
wi = wi + lr * xi * e * f’(z) go, a co za tym idzie – do uzyskania najmniejszego błędu dla stoso-
xi – wejście skojarzone z daną wagą wanej sieci. Dynamicznie zmieniający się współczynnik uczenia mo-
f’(z) – pochodna funkcji aktywacji żemy utrzymać w bardzo prosty sposób, ustalając chociażby pewne
lr – współczynnik uczenia (ang. learning rate)
1.  https://towardsdatascience.com/a-visual-explanation-of-gradient-descent-methods-momentum-adagrad-
2. mierzymy błąd oraz metryki na koniec epoki rmsprop-adam-f898b102325c

{  WWW.PROGRAMISTAMAG.PL  } <49>
LABORATORIUM EVORAIN

Rysunek 6. Umiejscowienie punktów klas 1 oraz 2 w przestrzeni 2D. Wskazane są również linie decyzyjne, które po nauczeniu powinny wskazać podział klas

wartości po przejściu zadanej ilości epok podczas trenowania. Dużo sieci neuronowej do klasyfikowania zdrowych i chorych pacjentów.
lepszym sposobem jest jednak skorzystanie z własności pochodnej, Przykład danych został zamieszczony w Tabelach 1 i 2. Punkty po-
która wskazuje szybkość zmiany funkcji. kolorowane na czerwono oznaczają przynależność do klasy „Zdro-
wy’’, natomiast niebieskie do klasy „Chory”. Obiekty umiejscowione
w przestrzeni 2D przedstawiają się tak jak na Rysunku 6. Zadanie to
Bias
jest również nazywane problemem XOR, które wiąże się z bramką lo-
Często sztuczne neurony mają dodatkową wagę zwaną biasem. Jest giczną o tej samej nazwie. Aby rozdzielić te dwie przedstawione klasy,
ona skojarzona z impulsem stałym, równym najczęściej 1. Rolą biasu potrzebne są dwie proste, które również zostały zaprezentowane na
jest przesunięcie progu dla funkcji aktywacji. Ponieważ jest to waga – Rysunku 6, jednakże ich położenie jest losowe. Dążymy do tego, aby
może ona zmieniać swoją wartość w procesie uczenia. Bias zapewnia sieć neuronowa nauczyła się prawidłowo rozgraniczać dwie klasy, co
dodatkową elastyczność w szukaniu rozwiązania danego problemu. odzwierciedli się poprzez prawidłowe położenie linii decyzyjnych.
Wzór na korektę biasu przedstawia się następująco: Warto zaznaczyć, iż pojedynczy neuron nie byłby w stanie poradzić
sobie z problemem XOR, gdyż nie da się rozgraniczyć przedstawionych
w0 = w0 + lr * 1 * e * f’(z) dwóch klas przy pomocy jednej linii prostej. Jak zapewne się domy-
ślasz, rozwiązanie tego zadania znajdzie sieć neuronowa, zawierająca
Oczywiście można pominąć jedynkę we wzorze. Dodałem ją jedynie kilka neuronów współpracujących ze sobą. Jej struktura przedstawiona
ze względu na oznaczenie sygnału wejściowego do neuronu. jest na Rysunku 7, natomiast na Rysunku 8 zilustrowano rozwiązanie.

SIECI NEURONOWE
Sztuczne neurony współpracując wspólnie w formacjach zwanych
sieciami neuronowymi, pozwalają na realizację złożonych pro-
blemów natury nieliniowej, z którymi nie poradzą sobie pojedyn-
cze neurony. Chcąc zilustrować problem, posłużę się przykładem
związanym z prostą klasyfikacją obiektów na podstawie sztucznie Rysunek 7. Struktura, której nauczenie pozwoli rozwiązać problem XOR. Wartości Y1, Y2, Y3
oznaczają neurony. Kolorem zielonym oznaczono neurony warstwy ukrytej, natomiast czerwonym
wykreowanych danych o pacjentach. Celem będzie wytrenowanie neuron warstwy wyjściowej. Jako z oznaczono wyjście z sieci, natomiast x1, x2 to jej wejścia

Rysunek 8. Ilustracja rozwiązania dla przedstawionego problemu XOR

<50> {  1 / 2021 < 95 >  }


/ Sieci neuronowe w pigułce /

Wspomniany przykład jest bardzo mocno uproszczony. Można Stosowano podejście, w którym układano wiele neuronów w jednej
wyobrazić sobie mnogość parametrów, które mogą wpływać na na- warstwie, które pracowały oddzielnie – nie przynosiło to jednak
sze zdrowie. Wystarczy wspomnieć o wynikach morfologii, badaniu oczekiwanych rezultatów. Stosowano nawet podejście wielowarstwo-
ciśnienia tętniczego czy tendencjach chorobowych w rodzinie, żeby we, lecz pojawiał się pewien problem. Otóż korzystając z podejścia
zrozumieć, jak wiele czynników może wpływać na zdrowie pacjen- do nauki z nauczycielem, bardzo łatwo jest oszacować wartości ocze-
tów. To nie stoi jednak na przeszkodzie, aby zaprzęgnąć sieci neuro- kiwane na wyjściu sieci, a co z tym idzie – obliczyć błąd dla danych
nowe do analizy dużo bardziej złożonych danych! neuronów wyjściowych. Wystarczy tylko obliczyć różnicę pomiędzy
Ogólnie rzecz biorąc, strukturę sieci neuronowej można przed- wartością oczekiwaną a wyjściem z neuronu. Dawniej zagadkę sta-
stawić w sposób widoczny na Rysunku 9. Sieci tego typu nazywane nowiło obliczenie błędu w neuronach, które były „ukryte” wewnątrz
są często sieciami wielowarstwowymi. W literaturze funkcjonuje po- sieci – za warstwą wyjściową, patrząc od końca struktury. Co zatem
jęcie MLP (ang. multilayer perceptron), co odzwierciedla rozmiesz- zrobić z błędem w warstwach poprzedzających warstwę wyjściową,
czenie wielu neuronów w sposób wielowarstwowy. W naszych mó- tak aby sieć mogła się uczyć, a neurony współpracować ze sobą?
zgach istnieje wiele warstw, które mają rozmaite połączenia między Odpowiedź jest zaskakująco prosta. Należy rozpocząć „wędrówkę”
neuronami. Sztuczne sieci neuronowe typu MLP zostały znacznie po sieci od wyjścia do wejścia – przesuwając się do wcześniejszych
uproszczone, gdyż mamy tutaj do czynienia z warstwami, w których neuronów – korzystając z połączeń. Znamy błędy w warstwie wyj-
każdy z neuronów warstwy następnej jest połączony ze wszystkimi ściowej, gdyż obliczamy je na podstawie wspomnianej wcześniej róż-
neuronami z warstwy poprzedniej, tworząc w efekcie warstwę w peł- nicy. Następnie cofając się do neuronu względem danego połączenia,
ni połączoną (ang. fully connected layer). W takim przypadku wektor mnożymy odpowiadającą wagę tego połączenia przez błąd obliczony
wejściowy z poprzedniej warstwy podawany jest na wejście każdego w neuronie po drugiej stronie. Jeżeli wcześniejszy neuron połączony
z neuronów warstwy następnej. z więcej niż jednym neuronem w warstwie następnej – należy ilo-
czyny obliczonych błędów oraz wag zsumować. Ten krótko opisany
proces nazywany jest algorytmem wstecznej propagacji błędu. Po-
staram się go podsumować poprzez poniższy, krokowy zapis:

1 Krok
Dla każdego neuronu należy ustalić jego wyjście, będące wyni-
kiem funkcji aktywacji (dla której argumentem jest pobudzenie neu-
ronu) w procesie propagacji sygnału w przód (od wejścia do wyjścia
struktury). Należy obliczyć wyjścia dla neuronów następnej warstwy
po obliczeniu wyjść wszystkich neuronów z warstwy poprzedniej.

2 Krok
Rysunek 9. Ogólny schemat sieci neuronowej typu MLP Dla ostatniej – wyjściowej warstwy sieci należy obliczyć błąd, dla
każdego z neuronów w tej warstwie stanowiący różnicę między war-
Na Rysunku 9 można wyróżnić trzy typy warstw dla sieci MLP. War- tością oczekiwaną na wyjściu a tą otrzymaną w danej iteracji.
stwa wejściowa stanowi replikację wektora wejściowego. Jej zada-
niem jest dostarczyć sygnał wejściowy do pierwszej warstwy ukrytej. 3 Krok
Z racji bardzo prostej funkcji sprowadzającej się jedynie do przepi- Dokonać wstecznej propagacji błędu: dla każdego neuronu po-
sania wejść sieci często spotyka się w publikacjach schematy z pomi- przedzającego neuron z obliczonym wcześniej błędem na jego wyj-
niętą warstwą wejściową. Nie jest to jednak dobre rozwiązanie. Pod ściu ustalić jego błąd, który odpowiada sumie iloczynów wag połą-
względem architektonicznym, jak również pod względem zastosowa- czeń i obliczonego wcześniej błędu następnika. Kalkulacje należy
nia obliczeń równoległych, zalecam uwzględnić w swoich projektach wykonywać aż do pierwszej warstwy ukrytej.
warstwę wejściową. Propagując sygnał od warstwy wejściowej w głąb,
można zauważyć warstwy ukryte – więcej o nich w dalszej części ar- 4 Krok
tykułu. W warstwie wyjściowej sieci MLP znajdują się neurony wyj- Wykonać modyfikację wag dla każdego neuronu, a następnie
ściowe, które wyprowadzają sygnał na zewnątrz. przejść do następnej iteracji z nowym wektorem wejściowym.

Warstwy ukryte i algorytm wstecznej propagacji błędu Vanishing gradient


Wszystkie warstwy pomiędzy warstwą wejściową a wyjściową na- Propagując błąd wstecz w sieciach neuronowych, możemy napotkać
zywane są warstwami ukrytymi, a neurony znajdujące się w nich – na problem zanikającego gradientu (ang. vanishing gradient). To zja-
ukrytymi (ang. hidden). Dlaczego mają one taką nazwę? Przez wiele wisko daje o sobie znać w sieciach neuronowych, które są zbudowane
lat zarzucono badania nad sztuczną inteligencją, gdyż pojedyncze z wielu warstw. Transportowany błąd od wyjścia do wejścia jest mno-
neurony nie wykazywały zdolności do nauczenia się problemów bar- żony przez wagi. Jeżeli iloczyny te są mniejsze od 1, to za każdym
dziej skomplikowanych, jak chociażby przedstawiony wcześniej XOR. razem powoduje to zmniejszenie błędu w neuronach poprzedzają-

{  WWW.PROGRAMISTAMAG.PL  } <51>
LABORATORIUM EVORAIN

cych. Jak już wiemy, wielkość błędu wpływa na stopień korekcji wag składający się z 10 tysięcy próbek i pozwala zaadresować problem
neuronów. Jeśli błąd będzie zbyt niski, to może to mieć negatywny rozpoznawania cyfr pisanych odręcznie. Ciekawe, prawda? Trudno
skutek w postaci widocznego spadku efektywności uczenia lub nawet jest określić, jak wiele reguł trzeba by było napisać, aby w sposób ma-
utknięcia w lokalnym minimum! tematyczny opisać sposoby pisania danej cyfry. Dodatkowo niektóre
cyfry są podobne do siebie, np. 1 i 7, różni ludzie mają inny charakter
pisma itd. Pokażę ci, jak stworzyć rozwiązanie tego problemu przy
Overfitting/Underfitting
pomocy biblioteki TensorFlow 2.x oraz języka Python i zbudowaniu
Stosując jakąkolwiek strukturę sieci neuronowej, zawsze należy pa- sieci MLP, o której już wspominaliśmy.
miętać o dokładnym opracowaniu jej hiperparametrów, odnoszą- Baza MNIST przedstawia cyfry pisane odręcznie w formie obrazów
cych się do jej budowy. Każde działanie, które wykonasz w procesie jednokanałowych, o rozmiarach 28x28 pikseli. Na Rysunku 10 przed-
tworzenia modelu, spowoduje wykreowanie większej, lub mniejszej, stawiono przykłady próbek treningowych. Przyjrzyj się im dokładnie
liczby „reguł” w sieci neuronowej, które zapisane za pomocą liczb i zwróć uwagę na label, który jest umieszczony nad każdym obraz-
prowadzą do wyprowadzenia określonych rezultatów. Jeśli struktura kiem. Nasza sieć będzie klasyfikatorem, który w warstwie wyjściowej
będzie zbyt duża, spowoduje to występowanie zjawiska typu over- będzie mieć 10 neuronów, każdy z nich sygnalizujący zaklasyfikowa-
fitting. Ma ono miejsce, gdy sieć ze względu na np. za dużą liczbę nie jednej z 10 cyfr – jeżeli np. na wyjściu 0 pojawi się impuls bliski
warstw – lub za dużą liczbę neuronów – wypracowuje nadmiarowe 1, a na pozostałych wyjściach impuls będzie relatywnie niski, próbka
reguły, które prowadzą do błędnych rezultatów w działaniu. Często zostanie zaklasyfikowana jako cyfra 0. Oczywiście może się zdarzyć
można wykryć to negatywne zjawisko, gdy podczas śledzenia proce- sytuacja, że sieć wyznaczy wysoką (bliską jedynce) wartość wyjścia
su uczenia błąd określony na danych treningowych zaczyna gwałtow- dla wyjścia 1 (cyfra 1) oraz wyjścia 7 (cyfra 7). Jest to bardzo natural-
nie spadać, podczas gdy błąd określony na danych testowych/walida- ne, gdyż cyfry te są do siebie podobne, a sieć odwzorowuje te reguły.
cyjnych nie maleje lub, co gorsza, gwałtownie wzrasta. W takim przypadku możemy przy interpretacji wyjścia przyjąć jedno
Kolejnym problemem, na który powinno się mieć baczenie pod- z dwóch podejść. Możemy zdecydować, że wygrał neuron o wyższym
czas budowania modelu sieci neuronowej, jest underfitting. To zja- wyjściu, lub też oznaczyć daną próbkę jako błędnie rozpoznaną, jeże-
wisko odwrotne od przedstawionego wcześniej overfittingu. Pojawia li różnica pomiędzy wartościami wyjść będzie bliska pewnemu zada-
się wtedy, gdy sieć jest zbyt prosta w swojej budowie, co w konse- nemu wcześniej progowi.
kwencji skutkuje słabą skutecznością jej działania. To kodowanie pozwala na oznaczenie wartości oczekiwanej dla
neuronu na odpowiedniej pozycji. Wystąpienie jedynki na zerowym
miejscu oznacza, iż sieć po zadaniu przykładu z cyfrą zero powinna
Przykład w Tensorflow 2.x – rozpoznawanie cyfr
zareagować jedynką na wyjściu zero.
z bazy MNIST W TensorFlow 2.x mamy możliwość skorzystania z wbudowanej
Jednym z najczęściej opisywanych w Internecie przykładów wyko- powłoki Keras, która pozwala na wczytanie danych z wielu źródeł.
rzystania sieci neuronowych jest ten opierający się na przetwarzaniu Tak jest również w przypadku bazy MNIST. Jedyne, co musimy zro-
bazy MNIST (http://yann.lecun.com/exdb/mnist/). Baza zawiera 60 bić, to załadować odpowiednie biblioteki, a następnie wywołać wła-
tysięcy przykładów treningowych, a także wydzielony zbiór testowy ściwą metodę ładującą dane (Listing 1).

Rysunek 10. Przykładowe próbki treningowe z bazy MNIST. Zwróć uwagę na label (etykietę) dla każdego z przykładów

<52> {  1 / 2021 < 95 >  }


/ Sieci neuronowe w pigułce /

Listing 1. Załadowanie niezbędnych do działania bibliotek, a następnie Listing 2. Model sieci MLP na potrzeby analizy danych z bazy MNIST
pobranie danych z bazy MNIST i ustandaryzowanie przy użyciu skalowania
standardowego from tensorflow.keras import layers

digit_classifier = tf.keras.models.Sequential()
import tensorflow as tf digit_classifier.add(layers.Flatten(input_shape=(28, 28)))
import tensorflow.keras.datasets.mnist as mnist_data digit_classifier.add(layers.Dense(128, activation='relu'))
import numpy as np digit_classifier.add(layers.Dense(64, activation='relu'))
from sklearn.preprocessing import StandardScaler digit_classifier.add(layers.Dense(128, activation='relu'))
digit_classifier.add(layers.Dense(10, activation='softmax'))
(x_train, y_train), (x_test, y_test) = mnist_data.load_data()

x_scaler = StandardScaler()
Następnie tworzony jest opis metryk wydajności oraz definiowany
x_train = x_train.reshape((x_train.shape[0], -1))
x_test = x_test.reshape((x_test.shape[0], -1)) optymalizator uczenia. W tym przypadku zastosowany został algo-
x_train = x_scaler.fit_transform(x_train)
rytm stochastyczny dla wstecznej propagacji błędów z członem pędu
x_test = x_scaler.transform(x_test) (momentum), jak również dodatkowo wsparty członem Nesterova.
Metoda fit uruchamia proces trenowania. W zaprezentowanym
StandardScaler pozwala na ustandaryzowanie danych. Pamiętaj, przykładzie podano dane testowe jako walidacyjne. Zrobiłem to dla
aby dla danych trenigowych stosować metodę dopasowująco-trans- uproszczenia – w rzeczywistości nie powinno się tego praktykować,
formującą fit_transform, natomiast dla danych testowych jedynie ponieważ skutkuje to nadmiernym dopasowaniem modelu do ciągu
transformującą transform. walidacyjnego, który jest jednocześnie testowym, a co za tym idzie –
Warto aby dostosować format analizowanych w tym przykładzie niewystarczającą skutecznością modelu!
danych wejściowych do formy NHWC (N – ilość próbek, H – wy-
Listing 3. Ustawienia metryk, optymalizatora oraz kompilacja i uruchomienie
sokość, W – szerokość, C – liczba kanałów). Istnieją również inne procedury trenowania modelu
wariacje formatów danych. W tym celu wystarczą tylko dwie linijki
metrics_vector = ['accuracy']
kodu: opt = tf.keras.optimizers.SGD(learning_rate=0.02, momentum=0.8,
nesterov=True)
x_train = x_train.reshape((x_train.shape[0], 28, 28, 1)) digit_classifier.compile(optimizer=opt,
loss='categorical_crossentropy',
x_test = x_test.reshape((x_test.shape[0], 28, 28, 1))
metrics=metrics_vector)

history = digit_classifier.fit(x_train, y_train, epochs=20,


batch_size=64,
Dla sieci mającej 10 wyjść finalne każda z etykiet może być przetrans- validation_data=(x_test, y_test))
formowana za pomocą kodowania One Hot Encoding do odpowied-
niej formy tak jak poniżej: W poprzednim artykule (Programista 7/2020 (94)) wspomniałem
o metrykach skuteczności. Można tworzyć własne, jednak metryki
» label 0 (cyfra 0): 1 0 0 0 0 0 0 0 0 0 Recall Precision oraz F1 Score świetnie uzupełniają metrykę accura-
» label 5 (cyfra 5): 0 0 0 0 1 0 0 0 0 0 cy, która stosowana jako jedyna może być niewiarygodna.
Po zakończeniu procesu trenowania można wywołać metodę eval-
Aby to wykonać, wystarczy napisać poniższy kod: uate, aby sprawdzić, jak model poradzi sobie z danymi testowymi:

n_outputs = 10 digit_classifier.evaluate(x_test, y_test, verbose=2)


y_train = tf.one_hot(y_train, n_outputs)
y_test = tf.one_hot(y_test, n_outputs)

Udało ci się nauczyć swój model sieci MLP do rozpoznawania cyfr


Teraz przyszła najwyższa pora na stworzenie struktury AI, a także pisanych odręcznie! Świetnie! Ja otrzymałem wynik dla wybranego
podanie do niej odpowiednich danych. W tym celu możemy stwo- modelu i metryki accuracy na poziomie 0.97. Możesz mieć na swo-
rzyć model sekwencyjny w Tensorflow, który pozwoli zbudować im komputerze nieco inny wynik, co jest konsekwencją inicjalizacji
wspomnianą wcześniej warstwę sieci typu MLP. Zobacz, jak szybko wag, która jest losowa i przybierze inne wartości na moim, a inne
możesz utworzyć swój pierwszy model! W Listingu 2 przedstawiono na twoim komputerze. Powracając do wyniku – 97% skuteczności!
kod potrzebny do utworzenia sieci MLP, która ma 3 warstwy ukryte Jednak tkwi tutaj pułapka. Proszę spojrzeć na Rysunek 11. Przedsta-
(128 neuronów, 64 neurony, 128 neuronów) oraz warstwę wyjśiową wiono na nim błąd dla funkcji kosztu, który obliczany jest po każdej
skupiającą 10 neuronów. Po każdej warstwie ukrytej występuje funk- epoce dla zbioru treningowego i walidacyjnego. Dla uproszczenia na-
cja aktywacji ReLU, natomiast na wyjściu mamy funkcję Softmax. szego przykładu walidacyjny zbiór to zbiór testowy.
Ta ostatnia świetnie nadaje się do problemów natury klasyfikacyj- Widzisz, jak na Rysunku 11 błąd treningowy spada cały czas, a po
nej; pozwala skupić sumę wszystkich wartości wyjść do jedynki. Jeśli pewnym czasie błąd na ciągu walidacyjnym zaczyna wzrastać? To
zatem na wyjściu 5 będziemy mieli wartość 0.86, to suma wartości sygnalizuje problem overfittingu. Sieć zaczęła wypracowywać nad-
pozostałych wyjść nie przekroczy 0.14. Z dużym prawdopodobień- miarowe reguły, stąd przestała poprawnie generalizować problem
stwem (86%) wykryto więc cyfrę 5! Na wejściu do modelu zauważysz i w rzeczywistości będzie radziła sobie w środowisku produkcyjnym
również warstwę Flatten. Jest to warstwa spłaszczająca obrazek do niewystarczająco dobrze. Czy wiesz, jak rozwiązać ten problem?
wektoru jednowymiarowego, który możemy podać do warstwy typu Spróbuj zmniejszyć współczynnik uczenia, liczbę epok i/lub zmniej-
w pełni połączonego – Dense. szyć strukturę trenowanej sieci. To niezwykle ważne pod względem

{  WWW.PROGRAMISTAMAG.PL  } <53>
LABORATORIUM EVORAIN

Rysunek 11. Przykład zaobserwowanego zjawiska typu overfitting dla utworzonej sieci MLP

optymalizacyjnym, jak również ze względu na skuteczność twoich nie miałaś/eś styczności z filtracją obrazu, postaram się ją przybliżyć
modeli AI. Nie zawsze bowiem więcej znaczy lepiej. za pomocą krótkiego przykładu. Na Rysunku 12 uwidoczniony jest
obraz wejściowy, a także wynikowe obrazy powstałe w wyniku nało-

DEEP LEARNING – WYBRANE STRUKTURY żenia odpowiednich filtrów.

Bardzo dziękuję, jeśli wciąż czytasz ten artykuł. Wszystkie przedsta-


wione do tej pory zagadnienia są bardzo przydatne w procesie po-
znawania głębokich sieci neuronowych (ang. deep neural networks).
Sieci te, począwszy od 2012 roku, zyskały ogromną popularność ze
względu na struktury, które pozwalają na lepsze dostosowanie do
stawianych problemów i osiąganie niespotykanych dotąd rezultatów.
Rozwój sprzętu, głównie kart graficznych, pozwolił na tworzenie głę-
bokich – złożonych z wielu warstw (nawet kilkuset) – sieci neurono-
wych, których uczenie może być wykonane w stosunkowo krótkim
czasie. W tej sekcji chciałbym przedstawić kilka przykładów struk- Rysunek 12. Proces filtracji przedstawiony na przykładzie filtru Sobela – poziomego oraz
tur deep learningowych. Jeżeli nie miałeś/miałaś z nimi do czynienia pionowego

wcześniej, ich poznanie sprawi, że twoje umiejętności z zakresu ucze-


nia maszynowego znacznie wzrosną. Przyjrzyj się dwóm wynikowym obrazom, które zostały przedstawio-
ne na Rysunku 10. Filtracja pozwoliła uwydatnić pionowe oraz po-
ziome krawędzie na obrazie w zależności od filtru. Filtr jest macierzą,
Splotowe sieci neuronowe
która ma na stałe zdefiniowane wartości. Omiatając obraz danym fil-
Sieci CNN (Convolutional Neural Network) przetwarzają dane wej- trem, możemy wykryć odpowiednie cechy na obrazie.
ściowe w sposób warstwowy, gdzie każda z warstw realizuje odrębne Sieci CNN też zawierają „filtry” zwane kernelami. Gdzie zatem
zadanie, polegające na ekstrakcji cech. Często ze względu na swoją tkwi tak duży potencjał tych sieci? Otóż w tym, że konwolucyjne sie-
budowę są stosowane w analizie obrazów. Nie brakuje również zasto- ci neuronowe same wykształcają wartości filtrów w procesie uczenia.
sowań, w których sam miałem okazję się przekonać, iż sieci te nadają Daje to ogromne możliwości, gdyż w zależności od problemu sieć
się również do przetwarzania np. strumieni danych w celu wykrywa- może uczyć się wykrywać pewnie krawędzie, kształty bądź części
nia zachodzących anomalii. Podczas opisu tego typu sieci skupię się obiektów na zdjęciach. Analiza za pomocą sieci CNN pozwala na roz-
jednak na obrazach, gdyż łatwiej jest zilustrować ich działanie, posłu- poznawanie coraz bardziej rozbudowanych wzorców w ramach kolej-
gując się właśnie tym zastosowaniem. Sieci CNN u podstaw są po- nych warstw. Ponadto w odróżnieniu od sieci MLP wagi mają rozmiar
wiązane z operacją filtrowania, która daje możliwość wykrycia cech kernela, a nie całego wektora wejściowego. Na Rysunku 13 przedsta-
obrazu niezależnie od położenia obiektu na obrazie. Jeżeli wcześniej wiono schemat sieci CNN wraz z dołączoną do niej siecią MLP.

<54> {  1 / 2021 < 95 >  }


/ Sieci neuronowe w pigułce /

Ograniczone Maszyny Boltzmanna


Głębokie architektury oprócz podejścia direct encoding dają moż-
liwość budowania modeli o podłożu probabilistycznym. Maszyny
Boltzmana są sieciami symetrycznie połączonych neuronów – co
stanowi w pełni połączony nieskierowany graf, który może zostać po-
dzielony na dwie warstwy zaprezentowane na Rysunku 14 i 15.

Rysunek 13. Schemat sieci CNN wraz z dołączoną do niej siecią MLP

Sieci konwolucyjne mogą składać się z wielu różnych typów warstw,


na przykład normalizacyjnych, aktywacji itd. Nieodzowną jednak
częścią dla sieci CNN są warstwy konwolucyjne (splotowe). Defi-
niując je, należy pamiętać o określeniu rozmiarów kerneli, jak rów-
nież o ich ilości w danej warstwie. Należy pamiętać, że zastosowa-
nie n kerneli poskutkuje wytworzeniem n obrazów zwanych feature
map’ami. Jeśli na wejście sieci podałaś/eś obrazek 3-kanałowy, to po Rysunek 14. Schemat maszyny Boltzmanna
przejściu przez warstwę z n kernelami ten obrazek obierze określo-
ne wymiary, lecz będzie miał n kanałów. Pamiętaj jednak o zjawisku
overfittingu – czyli co za dużo, to niezdrowo. Z warstwami sploto-
wymi często skojarzone są warstwy typu pooling. Pozwalają one wy-
brać maksymalne (MaxPooling), minimalne (MinPooling), średnie
(AvgPooling) wartości w ramach nałożonej na obraz maski o zada-
nych wymiarach. Przesuwać filtr możemy co jedną jednostkę w po-
ziomie/pionie – mówimy wtedy, że stride w danym kierunku wynosi
jeden. Stride większy od 1 powoduje downsampling, czyli zmniej-
szenie obrazu. Warto się nad tym zastanowić, ponieważ z jednej Rysunek 15. Schemat ograniczonej maszyny Boltzmanna
strony downsampling upraszcza obliczenia oraz proces uczenia,
a z drugiej może być przeszkodą, gdyż wpływa on na utratę pewnej Warto zaznaczyć, że maszyny Boltzmana nawiązują do tzw. sieci
ilości informacji. Hopfielda, wprowadzają jednak modyfikację w sposób probabili-
Skoro sieci CNN świetnie współpracują przy analizie obrazów, to styczny i opierają się na losowych polach Markova. Jednostki Vi –
czemu nie spróbować zastosować ich do problemu rozpoznawania często zwane widocznymi (ang. visible units) – tworzą dane wejścio-
znaków z bazy MNIST? W tym celu wystarczy jedynie inaczej zdefi- we dla sieci.
niować model. Zauważ, że warstwa splotowa może przyjąć wieloka- Ukryte jednostki Hi (ang. Hidden units) określają zależność mię-
nałowy obraz – stąd warstwa spłaszczająca do jednego wymiaru na dzy wejściami poprzez wzajemną interakcję – każda nieskierowana
wejściu jest zbędna. W naszym przykładzie jest ona jednak potrzebna krawędź tworzy zależność. Jeżeli model maszyny Boltzmana widocz-
przed warstwą w pełni połączoną. Przykład zdefiniowanego mode- ny na Rysunku 14 podda się przekształceniu, polegającym na usu-
lu sieci CNN na potrzeby klasyfikowania cyfr z bazy MNIST został nięciu połączeń pomiędzy neuronami tej samej grupy, to uzyska się
przedstawiony w Listingu 4. Ograniczoną Maszynę Boltzmana (ang. Restricted Boltzman Machi-
ne), której model przedstawiono na Rysunku 15.
Listing 4. Przykład sieci CNN do realizacji problemu klasyfikacji cyfr pisa-
nych odręcznie z bazy MNIST Trenowanie ograniczonej maszyny Boltzmana może odbywać się
w sposób nienadzorowany, z wykorzystaniem metody Contrastive
digit_classifier = tf.keras.models.Sequential()
digit_classifier.add(layers.Conv2D(16, (3, 3), strides = (1, 1), Divergence (CD-1). Działanie tej metody uczenia opiera się na reali-
padding='same', activation='relu', zowaniu funkcji energii (analogicznie do sieci Hopfielda), która skła-
input_shape=(28, 28, 1)))
digit_classifier.add(layers.Conv2D(32, (3, 3), strides = (1, 1), da się z fazy pozytywnej oraz negatywnej. Pierwsza z nich stanowi
padding='same', activation='relu')) swoistą propagację sygnału w przód, natomiast w fazie negatywnej
digit_classifier.add(layers.AveragePooling2D((2, 2)))
digit_classifier.add(layers.Conv2D(32, (3, 3), sieć stara się odwzorować wartości uzyskane w jednostkach ukrytych
padding='same', activation='relu'))
na jednostki widoczne. Standardowa wersja RBM ma binarnie war-
digit_classifier.add(layers.Flatten())
digit_classifier.add(layers.Dense(10, activation='softmax')) tościowane jednostki widoczne i ukryte. Jedną z wariancji RBMów
jest cRBM (Continuous Restricted Boltzmann Machine), który po-
Dla ciągu testowego (dla uproszczenia ciąg walidacyjny w tym przy- zwala na uzyskiwanie wyjść z sieci w formie ciągłej.
kładzie to również testowy) skuteczność na moim komputerze wy- Sieci typu RBM, jak również inne sieci deep learningowe, są ze-
niosła: 0.9885! To prawie 2% lepiej. Trzeba pamiętać, że zdjęcia z bazy stawiane ze sobą, tworząc tak zwane stosy (ang. stacks), co pozwala
MNIST są bardzo proste, a prawdziwą potęgę sieci CNN zobaczysz na zwiększenie skuteczności działania sieci. W takich przypadkach
na dużo większych i bardziej skomplikowanych zbiorach danych. uczenie odbywa się dla każdych RBMów z osobna – najpierw uczony

{  WWW.PROGRAMISTAMAG.PL  } <55>
LABORATORIUM EVORAIN

jest pierwszy, a następnie uzyskana wiedza z pierwszego RBMa jest np. 10 dni obserwacji, z których każda z obserwacji jest traktowana
podawana do drugiego, po czym uczony jest drugi itd. jako osobna obserwacja w czasie, sieć podejmuje analizę, podczas
Sieci RBM, o których mowa w tej sekcji, dobrze nadają się do której wykrywa „reguły” zachodzące pomiędzy krokami czasowy-
uczenia nienadzorowanego w celu wykrywania cech (ang. featu- mi, jak również w odniesieniu do kroku czasowego znajdującego się
re extraction), gdzie wektory cech stanowią wyjścia z jednostek o wiele wstecz.
ukrytych. Sieci te znajdują szerokie zastosowanie w realizacji wszelkich
zadań polegających na analizie szeregów czasowych. W procesach
translacji językowej stosuje się sieci LSTM, gdyż niezwykle ważna jest
LSTM (Long short-term memory)
analiza kontekstowa, aby przetłumaczyć zdanie na inny język. Klu-
Jeśli nie słyszałeś/aś do tej pory o sieciach LSTM, gorąco zachęcam czowa jest również pamięć, która pozwala na przechowywanie zależ-
do ich poznania. W przeciwieństwie do przedstawionych do tej pory ności pomiędzy następnymi wartościami a wartościami oddalonymi
sieci, struktury te są wręcz stworzone do zapamiętywania różnych wstecz.
ciągów danych. Świetnie nadają się na przyklad do predykcji zda- Do analizy szeregów czasowych często używa się również sie-
rzeń, gdzie analizie podlegają wszelkiego rodzaju dane historyczne. ci CNN. W takim przypadku kernele mogą wykryć pewne wzorce
Na Rysunku 16 uwidoczniona jest struktura sieci LSTM. Model ten w szeregach czasowych. Konwolucyjne sieci natomiast pozbawione
w swojej budowie jest znacznie bardziej skomplikowany w porówna- są elementów pamięciowych w stosunku do LSTM. Obecnie sieci
niu do dawniej stosowanych sieci RNN. Struktura sieci LSTM opiera LSTM mają warianty struktur, które stanowią połączenie zalet sieci
się na wykorzystaniu bramek przetwarzających sygnał w konkret- CNN oraz LSTM.
nych jednostkach. Jednostki te można odnieść w dużym przybliże-
niu do neuronów w sieciach MLP. Każda jednostka, która pracuje
PODSUMOWANIE
w ramach sieci LSTM, ma wspomniane bramki. Bramki te są używa-
ne wewnątrz sieci i służą do sterowania wejściem i wyjściem. Każda Sztuczna inteligencja jest niezwykle ciekawą dziedziną nauki. Sieci
z jednostek LSTM ma wbudowaną komórkę pamięci, która jest stero- neuronowe są rozwijane bardzo prężnie, przez co pojawiają się co-
wana za pomocą oddzielnej bramki, która pozwala na manipulowa- raz bardziej nowoczesne struktury, które pozwalają rozwiązywać
nie informacją. wiele problemów. Nawet podstawowa wiedza, którą posiądziesz,
jest bardzo ważna, gdyż pozwala stopniowo poznawać coraz bar-
dziej złożone zagadnienia związane z sieciami neuronowymi. Wie-
dza o zjawiskach takich jak overfitting, underfitting czy vanishing
gradient pozwala zapobiegać często popełnianym błędom na etapie
budowania sieci.
W artykule poruszono zagadnienia związane z głębokim ucze-
niem. W literaturze można znaleźć wiele struktur i technik sto-
sowanych w ramach tej dziedziny. Zachęcam do szczegółowego
zapoznania się z przedstawionymi strukturami. Istnieją różne wa-
rianty wspomnianych typów sieci głębokich, aczkolwiek techniki
uczenia głębokiego zawierają wiele innych struktur i metod, jak np.
Rysunek 16. Schemat sieci LSTM. Schemat ten wraz z dokładnym opisem działania sieci autoenkodery czy modele generatywne, które pozwalają zaadreso-
LSTM znajdziesz na https://colah.github.io/posts/2015-08-Understanding-LSTMs/ wać nowe rozwiązania problemów z użyciem algorytmów uczenia
maszynowego.
Artykuł ten dedykuję mojej żonie Marzenie i córeczce Hani.
Sieci typu LSTM mają duże zapotrzebowanie na moc obliczeniową.
Wystarczy tylko spojrzeć na ilość funkcji nieliniowych, na których
opierają się bramki. Ponadto najistotniejszym jest fakt, iż sieci te
mogą uczyć się związków, które zachodzą pomiędzy krokami czaso- W sieci
wymi, którymi może być jedna cecha w czasie (ang. univariate analy-
1. https://www.tensorflow.org/addons/overview
sis), bądź więcej (ang. multivariate analysis). To znaczy, że analizując

PIOTR WOLDAN
piotr.woldan@evorain.com
Jest współzałożycielem firmy Evorain sp. z o.o. Pasjonat algorytmów, uczenia maszynowego, a w szczególności metod głębokie-
go uczenia, jak również programowania równoległego na architekturach heterogenicznych. Od 2014 roku posiada doświadcze-
nie w pracy badawczej, powiązanej z zastosowaniami biznesowymi.

<56> {  1 / 2021 < 95 >  }


LABORATORIUM TEINA

UX w podpisie elektronicznym
– co jest ważne z perspektywy użytkownika?
Podpis pieczętuje umowę czy ustalenie. Kojarzy się z rytuałem, ceremonią. Równocześnie
jest zobowiązaniem, którego niedopełnienie może wiązać się z konsekwencjami. Moment
podpisu jest ważny, gdyż niesie znaczenie. Dlatego też zaprojektowanie elektronicznej wersji
jest tak dużym wyzwaniem. Czy w ogóle jest możliwe odwzorowanie tradycyjnego podpisu
w wersji cyfrowej? A może zupełnie nie tędy droga i e-podpis powinien być czymś innym, no-
wym? Dołóżmy do tego kwestie bezpieczeństwa i mamy interesujący temat!

W yobraźmy sobie sytuację, że zmieniamy pracę ze zwykłej, któ-


ra nie budzi ekscytacji, na nową, lepszą, z ciekawszymi zada-
niami oraz o wiele lepszą pensją. Rekrutacja i negocjacje warunków
podpisuje pierwszy, a kto ostatni. W związku z tym projektowanie
ścieżki użytkownika w systemie, który oferuje podpis elektroniczny,
jest swoistym wyzwaniem.
trwały tygodniami, ale obie strony są usatysfakcjonowane z efektu. Na co warto zwrócić uwagę:
Na stole czeka dokument, wystarczy go podpisać i nowa rzeczywi- » W ścieżce użytkownika trzeba uwzględnić, czy dany user jest
stość stanie się faktem. Z takimi zarobkami za rok możliwe będzie autorem dokumentu, współpracownikiem, czy też osobą ze-
spłacenie kredytu za dom. Albo, po tak trudnym 2020, będzie można wnętrzną, która, zależnie od potrzeb, posiada albo nie posiada
pojechać na trzy tygodnie wakacji w odległym, ciepłym miejscu. Do uprawnienia do edycji dokumentu. Oznacza to przygotowa-
tego w nowej pracy obowiązki będą o wiele ciekawsze, będzie też dużo nie kilku wersji widoków oraz dobrego wdrożenia schematu
wyzwań, w końcu kariera potoczy się lepiej, wręcz wystrzeli. Wystar- uprawnień dla poszczególnych użytkowników. Projektant UX
czy podpisać. W tym celu wyciągamy pióro, prezent od rodziców na powinien pamiętać o tym, by odzwierciedlić w systemie po-
maturę. Ozdobne, ciężkie. Tym piórem podpisywane były zawsze tylko wyższe elementy architektury tak, by każdy uczestnik procesu
ważne papiery. I teraz oto wystarczy otworzyć pióro i podpisać. Chwila podpisywania dokumentu wyraźnie rozumiał, co jest możliwe
zadumy i po sekundzie zamaszystym ruchem podpis jest złożony. Za do wykonania (czy pełna edycja treści dokumentu, czy ograni-
moment wyschnie… i nowa praca jest już faktem! czona edycja dokumentu, zupełny brak możliwości ingerencji
Podpisanie dokumentu kojarzy się z rytuałem. Oczywiście są w dokument czy załączniki, możliwość negocjacji dokumentu,
wyjątki, na przykład masowe parafowanie i podpisywanie stosu do- zapraszania kolejnych użytkowników, czy może tylko podgląd
kumentów przez szefa działu w korporacji, gdzie czynność jest tylko dokumentu z opcją podpisania, bez możliwości np. negocjacji).
formalnością, do tego powodującą ból nadgarstka. W momencie jed- » Użytkownik ma pełne prawo, by się zgubić w procesie. Zada-
nak, gdy mowa o jednostkowym podpisywaniu dokumentu, którego niem projektanta UX jest przygotowanie systemu tak, by na
treść jest ważna dla obu stron, moment podpisu to po prostu mo- każdym kroku procesowania dokumentu każda strona umowy
ment ważny. Jak ten rytuał przenieść do Internetu? wiedziała, na jakim etapie ten proces aktualnie się znajduje.
Oznacza to np. wdrożenie systemu automatycznych powiado-

UX ŚCIEŻKI UŻYTKOWNIKA – PODPIS mień, różnych dla autora dokumentu, użytkownika wewnętrz-
nego danej firmy oraz zewnętrznego kontrahenta, a także wdro-
TRADYCYJNY A PODPIS ELEKTRONICZNY żenie prostego progress baru – tak aby na pierwszy rzut oka było
Podpis tradycyjny kojarzy się ze wspomnianym wcześniej rytuałem. wiadomo, czy dokument jest procesowany sprawnie, czy też
Niestety, kojarzy się także z zadrukowaną ryzą papieru. W przypadku może „utknął” u któregoś z uczestników procesu.
podpisu tradycyjnego ścieżka oczywiście może być najprostsza: dwie » Nawet jeśli instrukcje kojarzą się negatywnie projektantom UX
osoby spotykają się, zapisują kartkę papieru i podpisują. Zwykle jed- („system powinien być oczywisty w użyciu”), to jednak warto
nak proces jest dłuższy i bardziej skomplikowany. Zwłaszcza w cza- wziąć pod uwagę, że podpisywanie dokumentu ma też konse-
sach pracy zdalnej i zamkniętych biur ścieżka podpisu dokumentów kwencje prawne. Dlatego dobrą praktyką jest uwzględnianie
w sposób tradycyjny wiąże się przynajmniej z dwukrotnym zaanga- krótkich tutoriali czy dodanie objaśnień tam, gdzie użytkow-
żowaniem poczty czy kuriera. Jeśli osób podpisujących jest więcej niż nik może popełnić błąd niewiedzy – i pokierować użytkownika
dwie, proces się komplikuje. A jeśli ktoś się pomyli… Cóż, wszystko w dobrą stronę. Chodzi np. o sytuacje, gdy autor wysyła doku-
trzeba zacząć od nowa, a błędnie wypełnione fizyczne dokumenty menty do nowych kontrahentów (system powinien sygnalizo-
zniszczyć. wać ostrożność przy wpisywaniu adresu e-mail, by dokument
Inaczej to wygląda w przypadku podpisu elektronicznego. Nadal nie trafił przypadkiem w niepowołane ręce), albo gdy kontra-
obowiązuje proces. Dotyczy on ustalania osób podpisujących, nada- hent podpisuje dokument w pierwszym odruchu (system powi-
wania dostępu elektronicznego do dokumentu oraz ustalania, kto nien przypominać o wydaje się oczywistym fakcie: sprawdze-

<58> {  1 / 2021 < 95 >  }


LABORATORIUM TEINA

niu swoich danych osobowych, wpisanych dat, kwot – zawsze


UX W WIZUALIZACJI PODPISU
przed podpisem, niezależnie od tego, ile razy dokument czytało
ELEKTRONICZNEGO – TRZY PODEJŚCIA
się wcześniej). W samym systemie warto dodać krótkie tuto-
riale ogólnie objaśniające specyfikę dokumentów, e-podpisu, Podpis elektroniczny to wyzwanie dla projektantów UX. Użytkownik
przy okazji wyjaśniając, jak korzystać z funkcji dostępnych oczekuje jednocześnie:
w oprogramowaniu. » by składanie podpisu było wygodne i szybkie,
» Komunikacja systemowa także powinna znaleźć się na celow- » by było bezpiecznie,
niku projektanta UX. Oczywiście warto współpracować w tym » ale także by były widoczne elementy znane z rytuału podpisu
zakresie z działem marketingu, jednak to projektant ścieżki „mokrego”, tego składanego długopisem.
użytkownika powinien zasugerować, gdzie są szczególne punk-
ty w procesie, które powinny być triggerem do wysyłki automa- Podejść do e-podpisu jest kilka, w tym tekście zaprezentowane zosta-
tycznej komunikacji. Chodzi o sytuacje, gdy np. dokonywana jest ną trzy najpopularniejsze – wraz z subiektywną oceną plusów i mi-
zmiana w dokumencie – których użytkowników należy poinfor- nusów każdego z nich.
mować i jak? Bądź o moment, gdy kontrahent podpisze umowę,
ale nadal nie można jej zamknąć i archiwizować, bo np. ekspert
Podpis elektroniczny udający podpis tradycyjny
wewnętrzny z firmy autora dokumentu nadal nie wykonał jakiejś
akcji – tu także decyzją projektanta UX powinno być, którzy użyt- Ten typ podpisu jest często spotykany. Subiektywnym zdaniem au-
kownicy i jak są informowani o sytuacji (e-maile, notyfikacje, czy torki – niestety jest tak często spotykany. Udawanie podpisu trady-
komunikaty tylko w ramach systemu, po zalogowaniu). cyjnego niesie bowiem zły przekaz użytkownikowi. Nawet jeśli este-
tyka jest miła dla oka i dokument podpisany w ten sposób wygląda
Dodatkowo w przypadku podpisów elektronicznych na uwagę zasłu- „jak zwykle”, czyli tak, jakby to był podpisany długopisem czy piórem
guje fakt, że niezależnie od wybranego systemu do zarządzania doku- papierowy egzemplarz (por. Rysunek 1), jednak wizualizacja podpisu
mentami i e-podpisu możliwe jest popełnienie błędu i szybkie popra- nijak się ma do realnego, ręcznego podpisu danej osoby. Oznacza to,
wienie go. Konsekwencją jest np. odrzucenie już złożonych podpisów że użytkownik może mieć wrażenie „fałszu” w tym podpisie, a nawet
i wysyłanie powiadomień do zainteresowanych osób, że muszą pod- nie traktować go zupełnie poważnie.
pisać jeszcze raz, gdyż została wprowadzona zmiana. Nie powoduje
to jednak problemu dla użytkownika, bo takie wiadomości są wysy-
łane systemowo. Tu warto jednak pamiętać o zaprojektowaniu dobre-
go user experience, by użytkownik rozumiał, co się wydarzyło.
Do decyzji projektanta UX są zatem, na przykład, takie kwestie:
» Czy komunikat błędu jest wyświetlany tylko w systemie, czy
pewne typy błędów użytkowników powodują automatyczną wy-
syłkę powiadomień do innych użytkowników.
» Jeśli któryś użytkownik zauważy w dokumencie błąd, jakie ma
możliwości reakcji, zwłaszcza w sytuacji, gdy ma mocno ograni-
czone uprawnienia do edycji? Czy są jakieś miejsca, gdzie może
zgłosić swoje uwagi czy komentarze? Gdy taki użytkownik doda
Rysunek 1. Podpis elektroniczny udający podpis tradycyjny
swoją notatkę, jak jest wysyłana informacja do autora dokumen-
tu (tak, by nie przeoczył tej wiadomości)? Plusy Minusy
» Wygląda „znajomo”, co ma swoje » Budzi nieufność co do ważności
Na sam koniec projektant UX musi rozważyć, w którym momencie zalety w momencie oswajania takiego podpisu.
należy powiedzieć „stop” komunikatom, by użytkownicy nie zostali za- użytkowników z ideą podpisu » Budzi niepokój co do bezpie­
cyfrowego. czeństwa takiego podpisu (jako
sypani informacjami. Jak wiadomo, w pewnym momencie każdy z nas
użytkownik mogę obawiać się,
zaczyna być odporny na pewne komunikaty. Jak więc zaprojektować że łatwo jest taki podpis podrobić.
system oraz komunikację wokół procesu, by nie przytłoczyć użytkow- Przypadkowo użyty font sam
w sobie wygląda jak podrobiony,
ników, a jednocześnie dostarczyć wszystkie wymienione informacje? jest bowiem niezgodny z realnym,
To jest prawdziwe wyzwanie. Pomocą może być podpowiedź: warto odręcznym podpisem).
przygotować pełen schemat ścieżki użytkownika wraz z naniesioną
mapą komunikacji. Mając w ten sposób przygotowany „rzut z góry” na Możliwości: podpis tego typu ma sens w momencie, gdy jest to albo
cały flow, o wiele łatwiej jest podjąć decyzję, które informacje wymaga- zdigitalizowany odręczny podpis z certyfikacją autentyczności, albo
ją e-maila, które notyfikacji, a które tylko odpowiedniego oznaczenia w gdy jest to po prostu zdigitalizowana wersja odręcznego podpisu wy-
systemie, które jest widoczne dopiero po zalogowaniu. konanego np. na tablecie. Jeśli jednak jest to, jak w przypadku niektó-
Proces podpisywania dokumentu elektronicznie to jednak nie rych narzędzi do e-podpisu, po prostu użycie jednego z kilku uda-
wszystko. Cała magia (oraz problem) rytuału kryje się w samym jących ręczne pismo fontów do zaprezentowania imienia i nazwiska
e-podpisie. osoby podpisującej, autorka jest zdecydowanie na „nie”.

<60> {  1 / 2021 < 95 >  }


/ – co jest ważne z perspektywy użytkownika? /

Podpis elektroniczny w stylu „tech”


Kolejny, często spotykany typ podpisu to podpis w stylu „tech”. Jego
głównym założeniem jest jak najdalsze odejście od przykładu 1, czyli
podpisu udającego odręczny. W związku z tym, w tym typie wizualiza-
cji podpisu elektronicznego głównym elementem są dane, zaprezento-
wane w formie suchej, sugerującej technologiczny, systemowy stempel.
Przykład 2 jest jedną z wersji takiego podpisu: nacisk jest położo-
ny na zaprezentowanie kluczowych informacji, w tym e-maila, który
jest podstawą weryfikacji danego użytkownika, dokładnego czasu
podpisu, danych dokumentu, także ze stemplami czasowymi.
W przypadku takiego typu podpisu danych można pokazywać o wiele
więcej, np. urządzenie, miejscowość, więcej informacji o użytkow-
nikach itd. Wszystko, co zwiększa zaufanie, że można osoby pod-
pisujące zidentyfikować poprawnie w przypadku potrzeby. Wizu-
alizacja w/w informacji ma sugerować systemowy „wyciąg”, a więc
brak tu elementów sugerujących jakikolwiek odręczny podpis czy
dodatkowych elementów graficznych. Czasami, jako dodatek, poja-
wia się ikonka odznaki czy stempelek, są to jednak drobne dodatki
estetyczne.

Rysunek 3. Podpis elektroniczny w stylu certyfikatu

Plusy Minusy
» Budzi zaufanie dzięki większej » W tej formie zajmuje sporo
zawartości danych systemowych, przestrzeni.
łatwiejsza jest weryfikacja
poprawności podpisów.
» Stosunkowo łatwo można
dodawać nowe elementy.
» Forma certyfikatu (i elementy
graficzne) polepszają odbiór tej
Rysunek 2. Podpis elektroniczny w stylu „tech” wersji e-podpisu, kojarzą się
z osiągnięciem czegoś, jest ele­
ment ceremonii.
Plusy Minusy
» Skojarzenie z wyciągiem z danych » Wizualnie ten typ podpisu jest
systemowych powoduje większe bardzo suchy i nie jest w stanie
zaufanie do danego e-podpisu. wypełnić luki po tradycyjnym ry­ Możliwości: nieograniczone, jeśli chodzi o rozwój czy dodawanie
» Ten typ podpisu można sto­ tuale podpisywania; zatem ważne elementów.
sunkowo prosto rozbudowywać dokumenty podpisane w ten
o dodatkowe elementy i infor­ sposób tracą część swojej magii.
macje, które mogą zwiększać
zaufanie do cyfrowego podpisy­
CO DALEJ Z PODPISEM
wania dokumentów. ELEKTRONICZNYM?
W ramach pozyskania wiedzy prosto z rynku o wypowiedź został po-
Możliwości: podpis z pewnością wygląda „bezpieczniej”, brakuje w nim proszony ekspert. Jeśli więc chodzi o wizję, o przyszłość w temacie
jednak pewnego ceremoniału. rozwoju podpisu elektronicznego, oddajmy głos Antoniemu Wędzi-
kowskiemu, współzałożycielowi Pergaminu, startupu, który specjali-
zuje się w automatyzacji pracy z umowami.
Podpis elektroniczny w stylu certyfikatu
Trzeci typ, w opinii autorki rekomendowany. Jest to próba stworzenia Podpis elektroniczny powoli przekształca się w twór taki, ja-
kim jest bramka płatnicza, w której mamy wielu dostawców do
wersji podpisu elektronicznego, który nawiązuje do najlepszych ele-
wyboru. Firma zdobędzie użytkowników, jeśli będzie posiadać
mentów podpisu poszukiwanych przez użytkowników, przy jedno- tzw. „portfel podpisów”, od najprostszych, z małą barierą wejścia,
czesnym odcięciu się od „udawania” podpisu odręcznego. Przybiera idealnych w przypadku nieskomplikowanych dokumentów, przez
różne formy, jest szansa, że jedna z nich stanie się powszechną „do- podpisy uwierzytelnione np. poprzez bank, aż do podpisów kwa-
brą praktyką”, co pozytywnie wpłynie też na oswojenie się użytkow- lifikowanych. Przejście z podpisu tradycyjnego na elektroniczny
to dopiero początek możliwości, jakie oferują dostępne na rynku
ników z e-podpisem.

{  WWW.PROGRAMISTAMAG.PL  } <61>
LABORATORIUM TEINA

rozwiązania. Od współczesnych systemów oczekuje się, że ob- Podstawy prawne: Rozporządzenie Parlamentu Europejskiego i Rady
służą proces od A do Z. Pozwolą nie tylko e-podpisać dokument, (UE) NR 910/2014 z dnia 23 lipca 2014 r. w sprawie identyfikacji elek-
ale również stworzyć go od podstaw, przeprowadzić negocjacje, tronicznej i usług zaufania w odniesieniu do transakcji elektronicz-
a po wszystkim zapiszą finalną wersję w bezpiecznym repozyto-
rium ze wszystkimi metadanymi. Dokument elektroniczny może nych na rynku wewnętrznym oraz uchylające dyrektywę 1999/93/WE
również automatycznie przekazywać lub pobierać informacje (L 257/73) oraz Ustawa z dnia 23 kwietnia 1964 r. – Kodeks cywilny
z innych systemów (np. CRM), a także ułatwia analitykę zawar- (Dz.U. 1964 nr 16 poz. 93 z późn. zm.).
tych w nim danych. Dlatego szukając e-podpisu do swojej firmy,
warto zwrócić uwagę na systemy, które umożliwiają zarządzanie
całym cyklem życia dokumentu. – Antoni Wędzikowski. Bezpieczeństwo podpisu
Ryzyka związane z podpisywaniem dokumentów elektronicznie są

PODSUMOWANIE bardzo podobne do tych, z którymi trzeba się mierzyć w przypadku


podpisu tradycyjnego. Główną kwestią jest weryfikacja tożsamości
Rok 2020 pokazał, że możliwa jest szybka cyfryzacja procesów, jeśli podpisujących:
tylko sytuacja tego wymaga. Ten progres już z nami zostanie, jest też » Czy to są osoby, które mają umocowanie, by podpisać dokumenty?
szansa, że tempo, jakiego nabraliśmy, nie spowolni nawet wtedy, gdy » Czy podpisujący są rzeczywiście tymi, za których się podają?
możliwy będzie powrót do trybów pracy sprzed epidemii. Stare proce-
sy bowiem będą już wymieszane z nowymi. Oznacza to, że tradycyjne Tak jak można manualnie sfałszować czyjś podpis, posługując się
drukowanie i podpisywanie długopisem, obieg dokumentów „od biur- czyjąś skradzioną tożsamością (bądź nieprawdziwą tożsamością), tak
ka do biurka”, a następnie kartony pełne papierów piętrzące się gdzieś samo można podszyć się pod kogoś, kradnąc tożsamość w Interne-
w archiwach będą i tak musiały być dołączone do elektronicznego obie- cie. To jednak nie jest jedyny kłopot. O ile poproszenie o dowód oso-
gu. Pracownicy czy kontrahenci dostępni tylko online będą wymagali bisty w przypadku kontaktu osobistego może wiązać się tylko z po-
pełnego dostępu do dokumentów. Mix offline i online będzie niezbędny. czuciem ewentualnego zażenowania taką prośbą, o tyle w przypadku
Nieuchronnym wydaje się więc postępowanie już rozpoczętej podpisu elektronicznego sposobów weryfikacji jest dosyć mało. Zwy-
zmiany, z widoczną na końcu drogi stuprocentową cyfryzacją. Za- kle sprawdzenie polega na potwierdzeniu adresu e-mail czy numeru
pach zadrukowanej kartki papieru, ceremoniał podpisu specjalnym telefonu, a usługa weryfikacji np. poprzez konto bankowe jest stosun-
piórem czy ręczne przeszukiwanie dziesiątek kartek z umowami, by kowo rzadka. Obserwując rynek, widoczny jest jednak nowy trend
znaleźć tę jedną… to będzie tylko wspomnienie. i jest duża szansa, że w 2021 roku wiele się zmieni na lepsze.
Druga kwestia to bezpieczeństwo treści dokumentu. Jeśli istnieje

GARŚĆ INFORMACJI DLA jakakolwiek możliwość modyfikacji zapisów w umowie po złożeniu


podpisów, budzi to wątpliwość. Zwyczajowo dodawane są aneksy czy
ZAINTERESOWANYCH załączniki, treść zaś powinna być niezmienna po podpisie. Ponieważ
podpisanie dokumentu tworzy zobowiązanie, warunki nie mogą
Typy podpisów
niepostrzeżenie się zmienić. W przypadku dokumentów papiero-
Zawarcie umowy może być ustne bądź w formie dokumentu trady- wych zmiana w treści wymaga zachodu i kojarzy się z fałszerstwem.
cyjnego, spisanego na papierze, wydrukowanego czy mającego postać W przypadku dokumentów elektronicznych – tu zaufanie do moż-
całkowicie elektroniczną. Jeżeli przepisy bądź ustalenia z drugą stro- liwości danych systemów wymaga jeszcze pracy. Czerwone światło
ną (czy stronami) umowy nie mówią inaczej, taka umowa jest ważna powinno się zapalać, gdy dostawca usługi obiegu dokumentów bez-
i złamanie jej wiąże się z konsekwencjami. trosko stwierdza, że może nanieść dowolną zmianę w dokumentach
W przypadku formy ustnej nie ma mowy o podpisie. Ten zaczyna już elektronicznie podpisanych.
być istotny w pozostałych opcjach, czyli w dokumentach papierowych Trzeba pamiętać, że tak jak ktoś może przechwycić koresponden-
bądź elektronicznych. O ile podpisu długopisem czy piórem przed- cję w Internecie, tak samo niepowołana osoba może otrzymać prze-
stawiać nie trzeba, warto rozróżnić typy podpisów elektronicznych: syłkę (np. listownie czy kurierem), bądź nawet omyłkowo do rąk wła-
» Prosty podpis elektroniczny (gdzie dane osób podpisujących muszą snych w biurze. Takie sytuacje są jednakowo prawdopodobne, czy to
być jednoznacznie powiązane z takimi danymi, które pozwalają zi- dokument tradycyjny, czy elektroniczny. W przypadku dokumentów
dentyfikować osobę; jest to zwykle adres e-mail, kombinacja adresu elektronicznych warto sprawdzać, komu wysyła się czy przekazuje
e-mail i numeru telefonu, rzadziej jedynie numer telefonu). dokumenty (literówka w adresie e-mail może dużo kosztować).
» Zaawansowany podpis elektroniczny (spełnia wymogi podsta-
wowe, ale dodatkowo zawiera takie elementy, jak możliwość KATARZYNA MAŁECKA
unikalnego powiązania danych z osobą, dodatkiem jest możli- katarzyna@teina.co
wość weryfikacji zmian wprowadzanych przez daną osobę, do-
Pracuje w IT od 2008 roku. Posiada doświadczenie
łączane są dane związane z np. urządzeniem, bardzo dokładnym w zarządzaniu projektami i produktami cyfrowymi
trackingiem czasu akcji itd.). oraz z zakresu UX: tworzenia user flows, prototy-
powania oraz badań z użytkownikami końcowymi.
» Elektroniczny podpis kwalifikowany (taki podpis jest składany
W ramach zawodowego hobby zajmuje się tematy-
przy użyciu kwalifikowanego urządzenia do składania e-podpi- ką dostępności produktów cyfrowych. Prywatnie
su i jest oparty na kwalifikowanym certyfikacie). pisze i ilustruje bajki dla dzieci.

<62> {  1 / 2021 < 95 >  }


PLANETA IT

FPGA pod lupą


Układy FPGA zyskują coraz większą popularność. W poniższym artykule zapoznamy się z ich
wewnętrzną budową, a także zastanowimy, do jakich zastosowań mogą być przydatne. Po-
nadto przyjrzymy się przykładowemu urządzeniu, w których układy te zostały wykorzystane.

CO TO JEST FPGA? zwalają na szybką komunikację szeregową. Znajdziemy także sprzę-


towe kontrolery zewnętrznej pamięci DDR, wsparcie dla PCIe czy
Układy FPGA, od angielskiego field-programmable gate array, co moż- Ethernetu. Dokładne informacje można znaleźć w karcie katalogowej
na przetłumaczyć jako bezpośrednio programowalna macierz bramek, wybranego układu.
to rodzaj rekonfigurowalnego układu scalonego. Poprzez zmianę
konfiguracji możemy modyfikować połączenia bloków wewnątrz.
LOGIKA
W efekcie możemy tworzyć „własny układ scalony” (z własną logiką
uzyskaną poprzez zmianę ustawień – fizycznie układ jest ciągle taki Budowa wewnętrzna bloków CLB (a także sama ich nazwa) jest róż-
sam). Dzięki temu, wgrywając odpowiednie ustawienia, możemy na w zależności od producenta. Jednak sama idea ich działania jest
uzyskać sprzęt wyspecjalizowany do realizacji konkretnego zadania. mniej więcej taka sama. Zwykle składają się one z 8 do 16 modułów,
W uproszczeniu układ FPGA możemy wyobrazić sobie jako sza- które przedstawiono na Rysunku 2. Moduły te składają się z LUT
chownicę zaprezentowaną na Rysunku 1. W środku mamy dużą licz- (ang. look up table – tablica prawdy) oraz przerzutnika typu D.
bę podstawowych elementów. Najbardziej typowe, występujące prak-
tycznie we wszystkich rozwiązaniach to:
» programowalne bloki logiczne (CLB – Configurable Logic
Block) [1],
» bloki pamięci RAM,
Rysunek 2. Podstawowe elementy budowy CLB
» bloki DSP.

Na pewno uwagę czytelnika przykuło, że do tej pory nie pojawiły się


bramki, choć występują one w samej nazwie FPGA. Otóż to właśnie
LUT możemy traktować jako uniwersalną „bramkę”. Dokładniej mó-
wiąc, pozwala ona zrealizować dowolną funkcję logiczną jej wejść.
Budowę trójwejściowej tablicy zaprezentowano na Rysunku 3. Za-
wiera ona 23, czyli osiem komórek pamięci konfiguracyjnej: po jed-
nej dla każdej możliwej konfiguracji sygnałów wejściowych. Wybór
odpowiedniej jest dokonywany za pomocą kaskadowo połączonych
multiplekserów. W układach FPGA spotyka się tablice mające od
trzech do nawet ośmiu wejść.

Rysunek 1. Uproszczony schemat układu FPGA

Bloki tego samego typu są identyczne. Ich działanie jest jednak pro-
gramowalne za pomocą ustawienia odpowiednich bitów w pamięci
konfiguracyjnej. Pomiędzy nimi znajdują się zasoby połączeniowe;
poprzez ich odpowiednią konfigurację można łączyć wejścia/wyjścia Rysunek 3. Budowa LUT

różnych bloków.
Ponadto, zwykle na brzegach układu, znajdują się dodatkowe pe- Sygnał wyjściowy może, ale nie musi być zatrzaśnięty w przerzutniku
ryferia pozwalające na komunikację ze światem zewnętrznym. Naj- typu D. LUT i przerzutniki należące do jednego bloku CLB zwykle
prostsze z nich to wejścia/wyjścia (GPIO – General-Purpose Input/ mają także dodatkowe połączenia, dzięki czemu mogą współpraco-
Output). Znajdziemy też jedną albo kilka pętli PLL, pozwalających wać ze sobą. Często także pojedyńcze CLB może zostać skonfiguro-
na skonfigurowanie zegarów. Często spotykane jest także wsparcie wane jako mała pamięć RAM (przeważnie do kilkunastu bitów). Jest
dla różny protokołów komunikacji – na przykład transceivery po- ona określana jako distributed (rozproszona).

<64> {  1 / 2021 < 95 >  }


PLANETA IT

Zauważono jednak, że często potrzebne są dużo większe bloki Tutaj warto zasygnalizować jeszcze jeden ważną kwestię. Otóż
pamięci. Ich realizacja poprzez łączenie wielu bloków CLB jest bar- projekt zaimplementowany w układzie FPGA powinien być tak zwa-
dzo nieefektywna. Dlatego zaczęto dodawać bloki pamięci RAM ną logiką sekwencyjną. Popatrzmy na Rysunek 5. Chmurką zaznaczo-
(block RAM). W zależności od modelu i producenta pojedynczy blok na jest tak zwana logika kombinacyjna. Charakteryzuje się ona tym,
ma pojemność około 10–40 tysięcy bitów. Ich liczba wynosi zwykle że nie ma w sobie pamięci: wyjście jest określone przez stan wejść.
od kilku do kilku tysięcy sztuk, więc całkowity rozmiar dostępnej Jednak zanim na wyjściu pojawi się stabilny wynik, musi upłynąć
pamięci może sięgać nawet kilkudziesięciu megabitów. Jej wejścia pewien czas, zależny między innymi od liczby potrzebnych obliczeń.
i wyjścia (takie jak adres i dane) są wystawione na zewnątrz i poprzez Im bardziej złożona logika, tym dłuższy czas jest potrzebny. Możemy
zasoby połączeniowe mogę być dołączone do logiki sterującej utwo- jednak dodać pomiędzy nie przerzutniki, które zapisują („zatrzasku-
rzonej w blokach CLB. Często pamięć może być zainicjalizowana ją”) nam stany pośrednie na kolejnych (zwykle narastających) zbo-
przy konfiguracji układu i wówczas może pełnić rolę pamięci ROM. czach zegara. W uproszczeniu maksymalna częstotliwość pracy na-
Dostrzeżono również, że bardzo często potrzebne jest wykonanie szego układu musi być tak dobrana, aby sygnał zdążył przejść przez
operacji mnożenia, którą również można zbudować z pojedynczych najdłuższą ścieżkę pomiędzy dwoma przerzutnikami. Zależy ona
bloków CLB, kosztem jednak użycia znacznych zasobów. Producen- od złożoności logiki oraz odległości pomiędzy blokami w układzie
ci zaczęli więc dodawać różnego rodzaju „mnożarki”. Można spotkać FPGA. Zwykle maksymalna możliwa do uzyskania częstotliwość,
tu zarówno proste bloki realizujące tylko samo mnożenie dwóch w zależności od wersji układu, waha się od kilkuset do niekiedy tysią-
liczb, jak i bardziej złożone pozwalające na wspomaganie typowych ca MHz. Widzimy, że jest to kilkakrotnie mniej niż procesory. Jednak
operacji występujących w cyfrowym przetwarzaniu sygnałów. Stąd tutaj możemy zyskać przewagę w wydajności, ponieważ ze swojej na-
pochodzi popularna nazwa bloki DSP (Digital Signal Processing). tury bloki w układzie FPGA pracują równolegle.
Na przykład na Rysunku 4 zaprezentowano schemat bloku z ukła-
du Cyclone V firmy Intel (kiedyś Altera) [2]. Poza samymi blokami
mnożenia mamy tu dodatkowe moduły pozwalające na wykonanie
dodawania. Częstym zastosowaniem układów FPGA jest realizacja
filtrów cyfrowych. Wymagają one przemnażania próbek przez kolej-
ne współczynniki. Moduł pozwala skonfigurować ich listę, która uła-
twi implementację algorytmów. Rysunek 5. Logika kombinacyjna i sekwencyjna

Rysunek 4. Budowa bloku DSP w układzie Cyclone V firmy Intel [2]

<66> {  1 / 2021 < 95 >  }


/ FPGA pod lupą /

ZASTOSOWANIA
Układy FPGA znajdują zastosowanie głównie w sytuacjach, gdy
mamy do czynienia z ciągłym strumieniem danych, na który nale-
ży wykonać ciąg operacji. Dlatego jednym z głównych obszarów za-
stosowań są algorytmy cyfrowego przetwarzania sygnałów. Głównie
spotyka się je przy sygnałach radiowych oraz video. Spotykamy tu
ciągły strumień danych, który musi zostać przetworzony w czasie
rzeczywistym. Dodatkowo zadania te da się zwykle podzielić na wie-
le prostych operacji. Jak widać na Rysunku 6, powoduje to, że dane
„przepływają” (pipelining) przez kolejne bloki sprzętowe. Dzięki Rysunek 7. Kolejne kroki budowania projektu

temu nawet przy stosunkowo niewielkiej częstotliwości pracy otrzy-


mujemy dużą sumaryczną przepustowość: kolejne wyniki są dostęp- Na Rysunku 7 widoczne są kolejne kroki wykonywane podczas bu-
ne na wyjściu na każdym takcie zegara. dowy projektu. Synteza polega na mapowaniu opisanej logiki na ele-
menty dostępne w strukturze danego układu. Tataj podejmowana jest
także decyzja, które funkcjonalności będą zrealizowane w blokach
CLB, RAM czy DSP. Producenci w dokumentacji zwykle podają,
Rysunek 6. Pipelining – dane przepływające przez kolejne operacje jakie konstrukcje językowe należy wykorzystać, aby użyć wybranej
struktury.
Z podobną sytuacją spotykamy się także przy przetwarzaniu ruchu Kolejny krok to przyporządkowanie każdemu elementowi kon-
sieciowego. Karta sieciowa wyposażona w układ FPGA umożliwia kretnego fragmentu układu FPGA (ang. placement) oraz odpo-
akcelerację niektórych funkcji (najprostszą formą jest tu obliczanie wiednia konfiguracja połączeń pomiędzy nimi (ang. routing). Jest
sum kontrolnych). Możliwa jest też wstępna inspekcja i klasyfikacja to najdłuższa część procesu. Dla bardzo złożonych projektów może
pakietów, przed przekazaniem ich do systemu operacyjnego. ona zająć nawet do kilkunastu godzin. Na końcu generowany jest
Układy FPGA używane są także do przyspieszenia obliczeń. Do- plik binarny, który można wgrać do pamięci układu FPGA. Po za-
stępne są karty ze złączem PCIe, które umożliwia szybką wymianę kończonym procesie dostajemy także różne raporty, na przykład
danych pomiędzy komputerem PC a akceleratorem. Dzięki możliwo- o liczbie zajętych podzespołów. Jednym z ważniejszych jest analiza
ści rekonfiguracji tego typu podzespoły są stosunkowo uniwersalnym czasowa (ang. timing analysis). Dostajemy informację, z jakimi mak-
narzędziem. Między innymi są dostępne rozwiązania chmurowe. symalnymi częstotliwościami zegarów może pracować nasz projekt.
Przykładem jest Amazon F1, czyli serwer wyposażony dodatkowo W przypadku gdy są one niższe niż wymagane dla poprawnej pracy
w układy FPGA firmy Xilinx (wkrótce AMD). Dostępne są obrazy projektu, środowisko przedstawi nam listę sygnałów, które nie speł-
maszyn wirtualnych ze skonfigurowanymi środowiskami develo- niają narzuconych ograniczeń czasowych. W takim przypadku musi-
perskimi oraz przykładowe projekty. Wśród zastosowań znajdziemy my wrócić do kodu naszego projektu i dokonać w nim odpowiednich
analizę genomów, przyśpieszanie zapytań do baz danych czy prze- zmian. Warto także sprawdzić, czy estymowane zużycie mocy mieści
twarzanie w czasie rzeczywistym sygnałów video. się w zakresie, który jest w stanie dostarczyć nasz zasilacz oraz roz-
proszyć zamontowany układ chłodzenia.
PROGRAMOWANIE
ETAPY PROJEKTU
Układów FPGA nie programuje się bezpośrednio, tworząc konfigu-
rację poszczególnych komponentów (choć środowiska dostarczone W przypadku większych projektów mogą one zostać podzielone na
przez producentów umożliwiają wprowadzanie modyfikacji na tym moduły. Nazywa się je blokam IP (ang. intellectual property – wła-
poziomie). Byłoby to zadanie skomplikowane i bardzo nieefektywne. sność intelektualna). Pozwala to na podzieł zadania pomiędzy róż-
Dodatkowym problemem jest nieudostępnianie przez producentów ne zespoły, a także ułatwia powtórne wykorzystanie poszczególnych
wystarczająco dokładnej dokumentacji pozwalającej na samodzielne części. Jak pokazano na Rysunku 8, każdy z nich powinien zostać
tworzenie narzędzi do implementacji. Dla niektórych rodzin są do- sprawdzony w symulacji. Debugowanie układu FPGA w sprzęcie jest
stępne otwarte narzędzia, lecz powstały one na zasadzie inżynierii stosunkowo skomplikowane. Nie jest możliwe bezpośrednie „pod-
wstecznej. glądanie” każdego z sygnałów, a jedynie dołożenie specjalnego mo-
Podstawowym sposobem modelowania jest korzystanie z języ- dułu pozwalającego na rejestrację wybranych przebiegów. Wymaga
ków opisu sprzętu. Dwoma najpopularniejszymi są obecnie VHDL on jednak powtórnej syntezy, co może spowodować inne rozłożenie
oraz SystemVerilog. Dostępne są także bardziej wysokopoziomowe elementów w strukturze układu. W przypadku szczególnie „złośli-
narzędzia, zwykle jednak wynikiem ich pracy jest właśnie kod na- wych” błędów może to wpłynąć na zmianę działania projektu.
pisany w którymś ze wspomnianych powyżej języków. Przykładem Testy tworzy się często w języku SystemVerilog – tym samym,
może być tu narzędzie HDL coder będące częścią pakietu MATLAB. którego używa się także do opisu sprzętu. Jego składnię można po-
Dostępne są też darmowe rozwiązanie, takie jak Chisel pozwalający dzielić na część syntezowalną – która da się odwzorować w układzie
na tworzenie projektów w języku Scala [3]. FPGA, oraz część niesyntezowalną, która może być uruchomiona je-

{  WWW.PROGRAMISTAMAG.PL  } <67>
PLANETA IT

figurowanie okien czasowych, w których kolejne pakiety powinny


dotrzeć do celu.
Nie są to jednak jedyne różnice pomiędzy nimi. Protokół CPRI
przesyła dane w dziedzinie czasu. W przybliżeniu oznacza to, że ko-
lejne próbki można bezpośrednio przekazać na przetwornik analogo-
wo-cyfrowy. Otrzymany sygnał analogowy trzeba jeszcze przesunąć
na odpowiednią częstotliwość nośną i wysłać do anteny. Natomiast
protokół eCPRI przesyła dane w dziedzinie częstotliwości. Oznacza
Rysunek 8. Projekt układu FPGA
to, że tym razem potrzebne jest jeszcze wykonanie dyskretnej trans-
formaty Fouriera.
dynie w symulacji. Standard opisujący metodologię tworzenia testów Załóżmy teraz, że potrzebujemy połączyć radia korzystające z pro-
nosi nazwę UVM ( Universal Verification Methodology – Uniwersal- tokołu CPRI ze stacją bazową używającą eCPRI. W tym celu potrze-
na Metoda Weryfikacji). bujemy przejściówki. Nosi ona nazwę Fronthaul Gateway [5]. Mamy
Kolejnym etapem jest integracja modułów w gotowy projekt. Ca- tu więc do czynienia zarówno z szybkimi sieciami, jak i cyfrowym
łość także powinna zostać sprawdzona w symulacji. Natomiast po przetwarzaniem sygnałów. Jak widzimy na Rysunku 9, po stronie eC-
zbudowaniu projektu następuje faza testów w sprzęcie. PRI mamy cztery interfejsy 100 Gb/s, natomiast protokół CPRI może
dotrzeć poprzez osiemnaście interfejsów o prędkości 9,8 Gb/s. Zada-

STUDIUM PRZYPADKU: FRONTHAUL niem układu FPGA jest rozpakowanie odebranych pakietów siecio-
wych, obliczenie dyskretnej transformaty Fouriera oraz zapakowanie
GATEWAY uzyskanego wyniku i wysłanie go drugim interfejsem. Całość wyko-
W sieciach telekomunikacyjnych 4G i 5G stosuje się różne protokoły nywana jest w czasie rzeczywistym.
komunikacyjne pozwalające łączyć głowice radiowe ze stacjami bazo-
wymi. Do przesyłania sygnałów radiowych wymagana jest stosunkowo
PODSUMOWANIE
duża przepustowość oraz przewidywane opóźnienia. Standard CPRI
(Common Public Radio Interface) w warstwie fizycznej bazuje na pro- Układy FPGA są bardzo szerokim zagadnieniem. Mam nadzieję, że
tokole Ethernet, jednak istotnie różni się na wyższych warstwach. Aby udało mi się zaciekawić tą tematyką oraz pokazać, że te interesują-
zapewnić deterministyczny czas dojścia danych, nie stosuje się tu pa- ce podzespoły mają także zastosowania praktyczne. Warto zapoznać
kietów, ale ciągłe przesyłanie danych w przydzielonych slotach [3]. się z materiałami edukacyjnymi na stronach internetowych produ-
Nowszym protokołem jest eCPRI (ang. enhanced CPRI) [5], który centów (Intel, Xilinx), przykładem może być kurs [7] (wymagane lo-
w całości bazuje na protokole Ethernet. Dane są przesyłane wewnątrz gowanie) albo webinaria [8]. Ciekawe informacje można też znaleźć
zwykłej ramki. Ich dojście na czas gwarantowane jest poprzez kon- w [9], [10], [11] oraz [12].

Rysunek 9. Fronthaul Gateway – FPGA w akcji [6]

[1] Kania D., „Układy logiki programowalnej. Podstawy syntezy i sposoby odwzorowa-
nia technologicznego”, Wydawnictwo Naukowe PWN, Warszawa 2012
[2] https://www.intel.com/content/www/us/en/programmable/documentation/
sam1403481100977.html
[3] https://www.chisel-lang.org/
RAFAŁ KOZIK [4] http://www.cpri.info/
[5] https://www.o-ran.org/
rafkozik@gmail.com [6] https://www.nokia.com/networks/products/airframe-fronthaul-gateway/
Absolwent Automatyki i Robotyki na Akademii [7] https://www.intel.la/content/www/xl/es/programmable/support/training/course/
odswbecome.html
Górniczo-Hutniczej. Pracował między innymi
[8] https://www.xilinx.com/about/webinar.html
z systemem operacyjnym FreeBSD oraz frame- [9] https://zipcpu.com/
workiem DPDK. Obecnie zajmuje się układami [10] https://github.com/enjoy-digital/litex
FPGA w krakowskim oddziale Nokii. [11] https://verificationacademy.com/topics/fpga-verification
[12] https://www.veripool.org/

<68> {  1 / 2021 < 95 >  }

You might also like