Professional Documents
Culture Documents
Programista 95
Programista 95
Programista 95
/* 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
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
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 magazynu Programista.
BIBLIOTEKI I NARZĘDZIA
...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:
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(); }
}
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…
{ 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
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
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:
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);
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
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();
return EXIT_SUCCESS;
}
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
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-
{ 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-
{ WWW.PROGRAMISTAMAG.PL } <15>
BIBLIOTEKI I NARZĘDZIA
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
{ MATERIAŁ INFORMACYJNY }
BIBLIOTEKI I NARZĘDZIA
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]):
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.
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).
}
{ 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))
{ 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
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);
getchar();
from defines import *
int counter = 0;
def mem_bp(pid): for (char *p = buffer ; ; ){
backup_regs = RegsStruct() if (counter == 1024){
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
/* 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ć.
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:
}
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.
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.
return app->exec();
Listing 3. Plik qml/Main.qml
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')
}
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
}
} 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.
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(); }
} }
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.
~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
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;
} };
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
}
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
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
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
}
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.
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.
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:
#
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ń)
{ 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 }
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
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
{ 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
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.
{ 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
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)
{ 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-
Rysunek 13. Schemat sieci CNN wraz z dołączoną do niej siecią MLP
{ 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.
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!
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-
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ą
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).
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
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
{ WWW.PROGRAMISTAMAG.PL } <67>
PLANETA IT
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].
[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/