7. 7
1 Wprowadzenie
Moduł zarządzania procesami, opisany w niniejszej pracy, jest częścią eksperymentalnego
rozproszonego systemu operacyjnego projektowanego i realizowanego przez pracowników Akademii
GórniczoHutniczej w Krakowie. System ten składa się z kilku modułów realizowanych jako
samodzielne usługi sieciowe. Usługi te działają wewnątrz istniejącego systemu operacyjnego jak
Linux, Windows czy FreeBSD, przy czym, mogą się one znajdować w jednym systemie operacyjnym
bądź być rozproszone na kilka komputerów. Robocza nazwa tego modułu na potrzeby tej pracy to
„moduł wykonawczy”.
Realizacja tej pracy składa się z kilku etapów:
1. Zdefiniowanie wymagań funkcjonalnych modułu wykonawczego. Poświęcone są temu
rozdziały 1.1 do 1.3.
2. Analiza technologii przydatnych do realizacji modułu wykonawczego. Poświęcony jest temu
rozdział 2.
3. Podział realizacji funkcji systemu na poszczególne komponenty modułu wykonawczego,
opracowanie architektury tych komponentów oraz sposobów komunikacji między nimi.
Poświęcone jest temu rozdział 3. oraz rozdział 4.
4. Opracowanie sposobów implementacji modułów, zbadanie realizowalności tych sposobów
oraz ich wpływu na wydajność procesów użytkownika wykonywanych przez system.
Poświęcony jest temu rozdział 5. zaś programy testujące zostały umieszczone w załącznikach.
5. Implementacja prototypu modułu wykonawczego z perspektywy przykładowego scenariusza
użycia. Poświęcony jest temu rozdział 6.
6. Dyskusja zagadnień, które nie zostały uwzględnione podczas opracowywania architektury
modułu, a które mogą posłużyć do jego dalszego rozwoju. Poświęcony jest temu rozdział 7.
7. Podsumowanie i wnioski z realizacji tej pracy. Poświęcony jest temu rozdział 8.
1.1 O systemie
Podstawowym zadaniem modułu zarządznia procesami jest uruchamianie oraz nadzorowanie
procesów uruchamianych przez użytkowników w systemie.
Rys. 1 Uruchamianie programów z połączonymi potokami.
Rozproszony System Konsola sterująca
Użytkownik
Program1 | program
Zainicjuj program1 i program2,
Przekieruj standardowe wej/wyj,
Uruchom programy
8. 8
Dba on także, żeby użytkownik mógł wprowadzić dane dla procesu oraz odczytać dane zwracane
przez działający proces, innymi słowy, dysponuje standardowym wejściem oraz wyjściem procesów.
Dostęp do systemu oraz sterowanie odbywa się przez sprawdzony mechanizm, znany z sys
temów UNIXlike, czyli tekstową konsolę. Aby obsługa systemu była intuicyjna dla użytkowników,
konsola powinna pozwalać na konstrukcje znane z jej odpowiedników z tradycyjnych systemów czyli
łączenie potoków:
Program1 | program2
czy przekierowanie standardowego wejścia/wyjścia programu do pliku:
program1 > plik.txt
Dodatkowo, lokalne pliki i polecenia mogą być zaznaczane przez specjalny znak np. wykrzyknik
znany z konsolowego klienta FTP, co pozwalałoby na współpracę programów znajdujących się
lokalnie na komputerze użytkownika z tymi znajdującymi się w systemie rozproszonym. Przykładowe
wykonanie dwóch programów w systemie rozproszonym i przekierowanie wyniku ich działania na
standardowe wejście programu trzeciego działającego na lokalnym komputerze użytkownika:
Program1 | Program2 | !Program_lokalny
Inny przykład, w którym treść zdalnego pliku jest wczytywana na standardowe wejście programu, zaś
standardowe wyjście jest przekierowane do pliku w lokalnym komputerze użytkownika:
Program < plik.txt > !plik_lokalny.txt
Choć z punktu widzenia użytkownika, system rozproszony może mieć powłokę użytkownika
bardzo podobną do znanych mu powłok, to wewnętrzna implementacja takiej powłoki ukrywa wiele
szczegółów. Więcej informacji na temat implementacji powłoki oraz zadań przed nią stawianych
zostanie omówione w dalszych rozdziałach.
1.2 Modu wykonawczył
Jest on zrealizowany jako niezależna usługa sieciowa działająca wewnątrz istniejącego systemu
operacyjnego, wobec czego może on korzystać z istniejącego API systemowego. Dzięki temu, że jego
elementy składowe są realizowane jako procesy w systemie operacyjnym, to nie musi się on zajmować
niskopoziomową funkcjonalnością zarządzania procesami. Zamiast planować sprawiedliwy przydział
procesorów dla procesów, zajmuje się on możliwie transparentnym rozproszeniem uruchamianych
procesów na kilka systemów operacyjnych znajdujących się na komputerach umiejscowionych w do
wolnym miejscu na świecie. Realizuje on dodatkową warstwę abstrakcji rozciągając zarządzanie
procesami do wyboru komputera i systemu, w którym procesy mają być uruchamiane.
Rys. 2 Dodatkowa warstwa abstrakcji zapewniona przez moduł zarządzania procesami.
Proces użytkownika
Warstwa zarządzająca
System operacyjny
Proces użytkownika
9. 9
Moduł ten komunikuje się z innymi częściami systemu wykorzystując sieć internetową, przy
czym, kilka modułów może znajdować się w tym samym systemie operacyjnym, w sieci lokalnej LAN
a także w WAN jednocześnie. Jego architektura została opracowana z naciskiem na prostotę
implementacji oraz minimalną ilością czynności potrzebnych do uruchomienia pojedynczych
komponentów. Implementacja została przedstawiona z punktu widzenia systemów Windows oraz
UNIXlike.
1.3 Zalety i wady rozproszonego modu u wykonawczegoł
Dodatkowa warstwa zarządzająca wprowadza dodatkowe opóźnienia w wywołaniach
systemowych, co w przypadku programów, które często je wywołują, spowoduje znaczne
spowolnienie działania. Dodatkowo pojedyncze składniki systemu mogą się znajdować w różnych
miejscach na świecie co powoduje wprowadzenie opóźnień sieciowych, które mogą się wahać od
pojedynczych milisekund do kilkuset milisekund, które dodatkowo spowalniają komunikację między
procesami oraz modułem dyskowym. Jaki jest więc zysk z tej rozproszonej architektury? Po pierwsze
zwiększa się niezawodność. Awaria jednego systemu powoduje przerwanie wykonania procesów w
nim znajdujących się, lecz procesy znajdujące się w innych systemach nadal są wykonywane bez
przeszkód.
Następną zaletą jest transparentne bilansowanie obciążenia na kilka jednostek – programista
może napisać program, który przy wykonaniu korzysta z wielu procesów potomnych np. do obliczeń,
gdzie każdy z nich może być wykonywany na innym komputerze. Pozwala to osiągnąć moc
obliczeniową niedostępną dla pojedynczej jednostki. Przy czym taki rozproszony program nie różni
się wiele od wieloprocesowego programu napisanego dla pojedynczego systemu operacyjnego.
Ułatwia to debugowanie programu, gdyż pozwala korzystać z technik i narzędzi dla standardowej
komunikacji międzyprocesowej.
Warto także wspomnieć, że rozproszona architektura pozwala na współpracę procesów
wykonywanych na różnych systemach operacyjnych. Z punktu widzenia programisty komunikacja z
innymi procesami wygląda tak samo, niezależnie od tego czy wykonują się one w tym samym
systemie operacyjnym, w tych samych rodzajach systemu operacyjnego, lecz znajdujących się w
różnych miejscach na świecie, czy wręcz w różnych typach systemu operacyjnego. Stosując potoki,
znane chociażby z systemów UNIXlike, użytkownik może na przykład napisać w swojej konsoli
sterującej komendę:
program1 | program2 | program3
Gdzie:
• program1 – program java wykonywany w 32bitowym systemie Macintosh
• program2 – binarny program wykonywany w 64bitowym systemie Linux
• program3 – program platformy .NET wykonywany w 64bitowym systemie Windows
Efekt wykonania tego polecenia użytkownik może zobaczyć w konsoli w swoim systemie
operacyjnym, bądź w przeglądarce, gdyż opracowywana konsola sterująca może być także
zaimplementowana przy użyciu języka JavaScript.
10. 10
2. Zagadnienia teorytyczne
Projekt ten jest bardzo wymagający pod względem znajomości rozwiązań technicznych oraz
technik programistycznych, stosowanych we współczesnych systemach operacyjnych. Dodatkowo
wykorzystane zostały techniki wstecznej analizy oprogramowania oraz zaawansowane techniki
programistyczne języka JavaScript, wśród których znajdują się takie, które są obsługiwane przez
przeglądarki internetowe dopiero od kilku lat. Z tego powodu w rozdziale tym opracowano
najważniejsze technologie, użyte bezpośrednio do implementacji modułu zarządzającego lub
pomocne do zrozumienia jego działania.
2.1 Wsteczna in ynieria oprogramowaniaż
W pracy tej kilkukrotnie korzystano z możliwości i narzędzi jakimi dysponuje wsteczna
inżynieria oprogramowania (reverse engineering). Nie jest to popularna dziedzina informatyki, dlatego
w tym rozdziale krótko opiszę czym się zajmuje wsteczna inżynieria oprogramowania oraz
przedstawię kilka narzędzi pomocnych przy analizowaniu programów.
Ze wsteczną inżynierią oprogramowania mamy do czynienia za każdym razem, gdy analizie
poddawany zostaje program. Może mieć to miejsce, gdy programista powraca do rozwoju programu,
którym się nie zajmował już od dłuższego czasu, bądź gdy analizowane jest działanie złośliwego
programu, wysyłającego spam do użytkowników i replikującego się w ich systemach. Wsteczna
inżynieria przydaje się w wielu sytuacjach, w których nie ma dostępu do kodu źródłowego programu,
a istnieje potrzeba zmiany jego zachowania. Jest ona intensywnie wykorzystywana przez twórców
wirusów do znajdywania błędów w systemach operacyjnych, a także przez autorów oprogramowania
antywirusowego.
Niejednokrotnie analiza wsteczna oprogramowania jest zabroniona prawnie, innym razem
programistą może zależeć na utrudnieniu analizy oprogramowania. Sytuacja ta ma miejsce w
przypadku, przywołanego już przykładu, twórców wirusów oraz oprogramowania antywirusowego,
gdzie jedna grupa chce ukryć szczegóły działania ich programów przed drugą. Dlatego, w myśl
anegdoty o ulepszaniu miecza i tarczy, powstało wiele sposobów zapobiegania wstecznej inżynierii
oraz omijania tych zabezpieczeń. Pełne omówienie sposobów analizy oprogramowania oraz ich
zabezpieczania przed analizą oraz omijanie tych zabezpieczeń nie jest istotne w tej pracy, więc
zostanie pominięte.
2.1.1 Analiza behawioralna
Do analizy behawioralnej, czyli analizy zachowania programu, kwalifikują się wszystkie te
narzędzia i sposoby, które monitorują akcje podejmowane przez działające programy [Eil00][Kas00].
W zależności od tego, jakie zachowanie programu jest istotne, takie zostają użyte narzędzia do jego
monitorowania.
Gdy w grę wchodzi krokowe wykonanie programu, analiza pamięci procesu oraz argumentów
wywoływanych funkcji, wówczas użyty zostaje debugger. Debugger jest programem, który jest
głównie wykorzystywany przez programistów języków wysokiego poziomu do odnalezienia błędów
w ich programach. Może być on jednak z powodzeniem stosowany do analizy binarnego programu,
zwłaszcza gdy zawiera wbudowany deasembler jak ollydbg. Ollydbg jest debuggerem systemu
Windows z wbudowanym deasembler oraz graficznym interfejsem użytkownika. Pozwala on na
11. 11
edycję asemblerowego kodu programu w locie i dlatego jest często wykorzystywany przez osoby
łamiące zabezpieczenia autorskie w programach – popularnie określane jako cracki.
#include "stdio.h"
void sayHello(int times, const char * string)
{
int i;
for(i = 0; i < times; ++i)
printf("%sn", string);
}
int main(int argc, char **argv)
{
sayHello(5, "Hello!");
return 0;
}
Przykład 1 Program języka C, wypisujący pięciokrotnie „Hello!”, użyty do dalszych demonstracji.
Rys. 3 Okno programu ollydbg.
Na rys. 3 zaprezentowano zrzut ekranu okna ollydbg 1.10, który został uruchomiony z pro
gramem pokazanym w przykładzie 1. W lewym, górnym fragmencie okna znajduje się zdeasemblo
wany kod programu. Break point został ustawiony na początku funkcji main i jest zaznaczony czerwo
nym kolorem na offsecie 0x004013D7. Program został wykonany krokowo od tego momentu i został
12. 12
zatrzymany tuż przed wywołaniem funkcji sayHello(), czyli na offsecie 0x004013F4. Program został
skompilowany bez symboli debugujących, więc nazwa funkcji sayHello() została bezpowrotnie
utracona i zastąpiona nazwą roboczą hello.004013B0, gdzie 0x004013B0 jest adresem w pamięci
procesu, w którym znajduje się owa funkcja. W prawym, górnym fragmencie okna znajduje się
zawartość rejestrów procesora. W prawym, dolnym rogu znajduje się zawartość stosu programu.
Szczyt stosu ma adres 0x22FF60 i jest zaznaczony czarnym kolorem. Można tam dostrzec argumenty
funkcji sayHello() umieszczone zgodnie z calling conventions języka C, czyli liczbę 5 oraz łańcuch
znakowy, znajdujący się pod adresem 0x00403064. Zawartość tego kawałka pamięci można zobaczyć
w lewej, dolnej części ekranu, gdzie można się przekonać, że ten łańcuch znakowy to „Hello!”.
Rys. 4 Właściwości uruchomionego programu Opera.
Inną metodą monitorowania działania programu jest analizowanie aktywności sieciowej oraz
operacji dyskowych. Rys. 4 zawiera informacje wyświetlane przez program Process Explorer, dostępny
na licencji freeware, w systemie Windows na temat uruchomionej przeglądarki internetowej Opera.
Po lewej znajdują się otwarte połączenia internetowe, zaś po prawej czas spędzony w trybie kernel
oraz user, a także informacje o używanej pamięci i operacjach dyskowych. W chwili pisania tej pracy
adresy 74.125.230.90 i 74.125.230.92 były adresami serwerów www google, zaś 149.156.96.2 adresem
www, na której znajduje się oficjalna strona AGH. Process Explorer potrafi znacznie więcej np. potrafi
wyświetlić listę załadowanych bibliotek dll czy łańcuchy znakowe znajdujące się w pamięci procesu.
Oprócz ogólnych informacji o procesie, można monitorować także konkretne wywołania
systemowe czy funkcje z określonych bibliotek. Na rys. 6 można zobaczyć jak dużo operacji wykonuje
tak prosty program, jak pokazany w przykładzie 1, który wypisuje na konsolę systemową pięciokro
tnie łańcuch znakowy w systemie Windows. Do monitorowania wywołań funkcji i ich argumentów z
określonych bibliotek w systemie Windows można użyć programu WinAPIOverride32 [Wio32],
15. 15
Rys. 8 Okno programu PEview.
Do analizy wywołań systemowych programów w systemach UNIXlike można zastosować
konsolowy program strace. Potrafi on zarówno wyświetlić podsumowanie zawierające listę wywołań
systemowych oraz czas ich wykonywania jak i dokładną listę wywoływanych funkcji systemowych.
Podsumowanie wyświetlane przez strace w systemie kubuntu x86 dla programu z przykładu 1:
user@user:~$ strace -c ./hello
Hello!
Hello!
Hello!
Hello!
Hello!
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
-nan 0.000000 0 1 read
-nan 0.000000 0 5 write
-nan 0.000000 0 2 open
-nan 0.000000 0 2 close
-nan 0.000000 0 1 execve
-nan 0.000000 0 3 3 access
-nan 0.000000 0 1 brk
-nan 0.000000 0 1 munmap
-nan 0.000000 0 3 mprotect
-nan 0.000000 0 7 mmap2
-nan 0.000000 0 3 fstat64
-nan 0.000000 0 1 set_thread_area
------ ----------- ----------- --------- --------- ----------------
100.00 0.000000 30 3 total
Dokładna lista wywołań systemowych, ich argumentów oraz wartości zwracanych:
user@user:~$ strace ./hello
execve("./hello", ["./hello"], [/* 42 vars */]) = 0
brk(0) = 0x9fea000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb78f0000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=78669, ...}) = 0
mmap2(NULL, 78669, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb78dc000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
open("/lib/libc.so.6", O_RDONLY) = 3
16. 16
read(3, "177ELF11100000000030301000@n10004000"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1421892, ...}) = 0
mmap2(NULL, 1427880, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xdcb000
mmap2(0xf22000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x157) =
0xf22000
mmap2(0xf25000, 10664, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) =
0xf25000
close(3) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb78db000
set_thread_area({entry_number:-1 -> 6, base_addr:0xb78db6c0, limit:1048575, seg_32bit:1,
contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
mprotect(0xf22000, 8192, PROT_READ) = 0
mprotect(0x8049000, 4096, PROT_READ) = 0
mprotect(0x3d6000, 4096, PROT_READ) = 0
munmap(0xb78dc000, 78669) = 0
fstat64(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 1), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb78ef000
write(1, "Hello!n", 7Hello!
) = 7
write(1, "Hello!n", 7Hello!
) = 7
write(1, "Hello!n", 7Hello!
) = 7
write(1, "Hello!n", 7Hello!
) = 7
write(1, "Hello!n", 7Hello!
) = 7
exit_group(0) = ?
Podobnie jak i w systemie Windows, także i tym razem lista czynności wykonywanych przez
program, aby wyświetlić pięciokrotnie napis „Hello!”, choć zdecydowanie krótsza, to nadal zawiera
sporo elementów.
Do monitorowania wywołań funkcji można w systemach UNIXlike można użyć programu
etrace opracowanego przez grupę „The ERESI Reverse Engineering Software Interface”. Więcej
informacji o tym, oraz o innych narzędziach stworzonych przez tą grupę można znaleźć na ich stronie
internetowej [ERESI]. Innym narzędziem mogącym posłużyć do tego celu jest ltrace:
user@user:~$ ltrace ./hello
__libc_start_main(0x80483dc, 1, 0xbf94e2b4, 0x8048410, 0x8048400 <unfinished ...>
puts("Hello!"Hello!
)
= 7
puts("Hello!"Hello!
)
= 7
puts("Hello!"Hello!
)
= 7
puts("Hello!"Hello!
)
= 7
puts("Hello!"Hello!
)
= 7
+++ exited (status 0) +++
Do sprawdzenia zależności od dynamicznie ładowanych bibliotek można użyć polecenia ldd:
user@user:~$ ldd hello
linux-gate.so.1 => (0x00ac7000)
libc.so.6 => /lib/libc.so.6 (0x00c2a000)
/lib/ld-linux.so.2 (0x00e02000)
17. 17
Ogólną strukturę plików wykonywalnych ELF można podglądnąć przy pomocy readelf:
user@user:~$ readelf -a hello
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8048300
Start of program headers: 52 (bytes into file)
Start of section headers: 4400 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 8
Size of section headers: 40 (bytes)
Number of section headers: 29
Section header string table index: 26
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 08048134 000134 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048148 000148 000020 00 A 0 0 4
[ 3] .note.gnu.build-i NOTE 08048168 000168 000024 00 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0804818c 00018c 000020 04 A 5 0 4
[ 5] .dynsym DYNSYM 080481ac 0001ac 000050 10 A 6 1 4
(...)
Przy pomocy polecenia lsof można wyświetlić listę otwartych plików:
(...)
bash 6347 user cwd DIR 8,5 4096 12 /home/daroo
bash 6347 user rtd DIR 8,2 4096 2 /
bash 6347 user txt REG 8,2 801808 260616 /bin/bash
bash 6347 user mem REG 8,2 79676 1499 /lib/libnsl-2.12.1.so
bash 6347 user mem REG 8,2 42572 1538 /lib/libnss_files-2.12.1.so
bash 6347 user mem REG 8,2 227864 115 /lib/libncurses.so.5.7
bash 6347 user mem REG 8,2 38504 1554 /lib/libnss_nis-2.12.1.so
bash 6347 user mem REG 8,2 118084 709 /lib/ld-2.12.1.so
bash 6347 user mem REG 8,2 1421892 1200 /lib/libc-2.12.1.so
bash 6347 user mem REG 8,2 9736 1239 /lib/libdl-2.12.1.so
bash 6347 user mem REG 8,2 30496 1500 /lib/libnss_compat-2.12.1.so
bash 6347 user mem REG 8,2 26048 133629 /usr/lib/gconv/gconv-
modules.cache
bash 6347 user mem REG 8,2 5396736 140426 /usr/lib/locale/locale-archive
bash 6347 user 0u CHR 136,1 0t0 4 /dev/pts/1
bash 6347 user 1u CHR 136,1 0t0 4 /dev/pts/1
bash 6347 user 2u CHR 136,1 0t0 4 /dev/pts/1
bash 6347 user 255u CHR 136,1 0t0 4 /dev/pts/1
(...)
Program lsof może także posłużyć do wyświetlenia otwartych połączeń internetowych:
user@user:/usr/include/i386-linux-gnu$ ps ax | grep opera
2843 ? Sl 0:41 /usr/lib/opera/opera
3077 pts/1 S+ 0:00 grep opera
user@user:/usr/include/i386-linux-gnu$ lsof -i -p 2843 | egrep 'IPv4|IPv6'
opera 2843 user 14u IPv4 47387 0t0 TCP user.local:38494->10.0.0.182:2869 (ESTABLISHED)
opera 2843 user 19u IPv4 49501 0t0 TCP user.local:51520->web2.uci.agh.edu.pl:www (ESTABLISHED)
opera 2843 user 20u IPv4 47020 0t0 TCP user.local:51521->web2.uci.agh.edu.pl:www (ESTABLISHED)
opera 2843 user 21u IPv4 47021 0t0 TCP user.local:51522->web2.uci.agh.edu.pl:www (ESTABLISHED)
opera 2843 user 66u IPv4 47023 0t0 TCP user.local:51524->web2.uci.agh.edu.pl:www (ESTABLISHED)
opera 2843 user 71u IPv4 47024 0t0 TCP user.local:51525->web2.uci.agh.edu.pl:www (ESTABLISHED)
opera 2843 user 78u IPv4 33507 0t0 UDP 239.255.255.250:1900
opera 2843 user 79u IPv4 33508 0t0 UDP user.local:41547
opera 2843 user 83u IPv4 47025 0t0 TCP user.local:37819->fra07s07-in-f138.1e100.net:www
(ESTABLISHED)
18. 18
Systemy UNIXlike implementują system pseudo plików procf, w którym można znaleźć
mnóstwo informacji o uruchomionym procesie m. in. otwarte pliki, statystyki pamięci, szczegółowe in
formacje o ruchu sieciowym, polecenie uruchamiania, a także informacje o systemie. Między innymi
program ps korzysta z tych informacji. Przykładowe informacje zawarte w procf dla konsoli KDE w
systemie kubuntu x86:
user@user:~$ ps | grep 3394
3394 pts/3 00:00:00 bash
user@user:~$ cat /proc/3394/net/arp
IP address HW type Flags HW address Mask Device
10.0.0.182 0x1 0x2 70:f1:a1:80:aa:d2 * wlan0
10.0.0.1 0x1 0x2 00:1c:f0:87:a9:e0 * wlan0
user@user:~$ cat /proc/3394/status
Name: bash
State: S (sleeping)
Tgid: 3394
Pid: 3394
PPid: 2218
TracerPid: 0
Uid: 1000 1000 1000 1000
Gid: 1000 1000 1000 1000
FDSize: 256
Groups: 4 20 24 46 112 119 120 1000
VmPeak: 7320 kB
VmSize: 7256 kB
VmLck: 0 kB
VmHWM: 3740 kB
VmRSS: 3740 kB
VmData: 2040 kB
VmStk: 136 kB
VmExe: 764 kB
VmLib: 1884 kB
VmPTE: 32 kB
VmSwap: 0 kB
Threads: 1
(…)
user@user:~$ cat /proc/cpuinfo
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 15
model name : Intel(R) Core(TM)2 Duo CPU T5450 @ 1.66GHz
stepping : 13
cpu MHz : 1000.000
cache size : 2048 KB
(…)
Dla procesu konsoli, o numerze pid równym 3394, wyświetlono tablicę ARP, używaną do tłumaczenia
adresów IP na fizyczne adresy sieciowe oraz ogólne informacje o procesie jak status, ilość wątków, czy
ilość i typ zajętej pamięci. Pokazano także informacje o procesorze w komputerze.
Do dokładnej analizy ruchu sieciowego programu służą sniffery sieciowe, wśród których
bardzo popularny jest WireShark, który jest dostępny na licencji open source. Na rys. 9 zapre
zentowano listę pakietów przechwyconych przez sniffera. Pakiety te zostały wygenerowane podczas
pobierania strony AGH przez przeglądarkę www. WireShark oferuje szeroki zakres filtrowania
przechwyconych pakietów, a także zawiera wbudowaną analizę popularnych protokołów interne
towych.
19. 19
Rys. 9 Okno programu WireShark.
2.1.2 Deasemblacja
Deasemblacja to proces zamiany kodu binarnego z powrotem na kod asemblerowy. Statyczna
deasemblacja, w przeciwieństwie do dynamicznej, zaprezentowanej we wcześniejszym rozdziale,
wymaga od deasemblera analizy całego pliku wykonywalnego. Obecnie wiele pracy jest wykony
wanych jest automatycznie przez nowoczesne, interaktywne deasemblery. Potrafią one wykrywać
argumenty znanych funkcji systemowych, rysować drzewa wywołań i nie tylko [Eil00][Kas00].
Najbardziej funkcjonalnym deasemblerem jest bez wątpienia komercyjny program IDA Pro,
który oprócz deasemblera zawiera także wbudowany debugger. Jest on dostępny zarówno dla
systemów Windows jak i UNIXlike. Potrafi deasemblować 32 oraz 64 bitowe pliki binarne w wielu
formatach, a także bytecod używany przez wirtualne maszyny java oraz .NET. Zrzut ekranu programu
IDA Pro 5.0 jest pokazany na rys. 10. IDA Pro została wykorzystana do deasemblacji funkcji sayHello()
z przykładu 1, a efekt jego pracy jest wyświetlany w postaci grafów wywołań.
W systemach Windows oraz UNIXlike zwykle dostępny jest prosty, konsolowy deasembler
objdump. W niektórych systemach można spotkać także graficzne frontendy dla tych narzędzi, jak na
przykład program dissy.
22. 22
hardware), a programami (ang. software) nań uruchamianymi, dzięki czemu programy muszą jedynie
wiedzieć w jaki sposób się „porozumieć” z systemem. Komunikacja między procesami, a systemem
odbywa się poprzez z góry ustalone API i z góry ustalony sposób, najczęściej inny dla różnych
systemów operacyjnych [Sil05][Tan08].
Rys. 12 Relacja między sprzętem, systemem operacyjnym oraz procesami.
Jak można zauważyć na rys. 12, procesy w systemach operacyjnych można traktować jako dość
niezależne jednostki. Systemy operacyjne starają się je odseparować od siebie na tyle, na ile jest to
możliwe, minimalizując tym samym wpływ błędnego działania jednego procesu na drugi. Procesy nie
mogą być jednak w pełni niezależne, gdyż czasami programistą zależy na czymś zupełnie
przeciwnym – chcą tworzyć programy wieloprocesowe, których wynik działania jest rezultatem
współpracy wielu procesów. Dlatego systemy UNIXlike oraz Windows udostępniają szereg
mechanizmów wzajemnej interakcji między procesami (Interprocess Communication) [Fus09][Ker10].
przykładem wieloprocesowych aplikacji używanych na co dzień są przeglądarki internetowe, w
których pluginy są najczęściej implementowane jako osobne procesy, dzięki czemu błędy w ich
implementacji nie mają wpływu na działanie całej przeglądarki.
System operacyjny dysponuje sprzętem i przydziela każdemu procesowi określony czas
procesora, pamięć RAM. Pośredniczy on także przy operacjach dyskowych oraz połączeniach
internetowych. Dla tych zadań opracowane zostały wydajne algorytmy, które zapobiegają fragmentacji
pamięci czy przywłaszczeniu czasu procesora. Każdy proces ma przydzieloną oddzielną, wirtualną
przestrzeń adresową w pamięci, która jest mapowana na rzeczywistą pamięć operacyjną i która
zapobiega przypadkowej oraz intencjonalnej ingerencji innych procesów w dane procesu. Zapis i od
czyt w pamięci wirtualnej drugiego procesu jest oczywiście możliwy, lecz nie wprost, tylko wymaga
pośredniczenia systemu operacyjnego [Tan08].
Przed systemami operacyjnymi stawianych jest oczywiście szereg innych wymagań, między
innymi zarządzanie zasobami sprzętowymi, w tym przydzielanie czasu procesora dla procesów oraz
zarządzanie pamięcią RAM i nieulotną, czy pośredniczenie w połączeniach internetowych. Kompletne
omówienie zadań i sposobu ich realizacji przez systemy operacyjne jest poza zakresem niniejszej
pracy. Zainteresowanych tymi tematami czytelników odsyłam do bogatej literatury traktujących o tych
zagadnieniach [Tan08][Sil05][Tan05][Tan95].
2.2.1 Procesy i biblioteki
Proces jest jednym z podstawowych pojęć stosowanych w systemach operacyjnych [Sil05].
Tworzenie nowego procesu w systemie operacyjnym jest wieloetapowe i dość złożone [Sil05][Bov00]
sprzęt
System operacyjny
Proces Proces
Proces
23. 23
[Tan08]. Wpływ ma na to elastyczność formatów plików wykonywalnych, a także zależność
programów od bibliotek. Biblioteki te mogą być dołączone do pliku wykonywalnego – biblioteki
kompilowane statycznie, bądź być ładowane do pamięci w chwili, gdy nowy proces od nich zależny
jest uruchamiany, bądź wywołuje funkcje w nich się znajdujące – biblioteki dynamiczne[Fus09]
[Ker10]. Biblioteki dynamicznie ładowane mają zwykle rozszerzenie .dll w systemach Windows oraz
.so w systemach UNIXlike.
Na proces w systemach operacyjnych składa się szereg składników [Sil05][Tan08] w tym:
1. Prywatna przestrzeń adresowa pamięci.
2. Otwarte pliki oraz połączenia sieciowe.
3. Wątki.
4. Katalog roboczy.
5. Dane pomocne do wykonywania procesu – stos, stan rejestrów procesora.
6. Załadowany do pamięci kod programu oraz biblioteki.
Procesy zwykle mają otwarte na początku swojego działania trzy uchwyty plikowe – standardowego
wejścia, standardowego wyjścia oraz błędów.
2.2.2 Plik wykonywalny
Plik wykonywalny programu jest charakterystyczny dla danego systemu operacyjnego, a jego
budowa ma ścisły związek z historią systemu oraz tworzeniem nowego procesu [Pie94][mpe10].
Formaty plików wykonywalnych zostały opracowane w taki sposób, by możliwie wydajnie można je
było uruchamiać, dlatego ich budowa często jest podobna do struktur pamięci już uruchomionego
procesu. Innymi kryteriami, które były rozpatrywane podczas opracowywania formatu plików
wykonywalnych były m. in. elastyczność czy przenośność między platformami sprzętowymi. System
Linux domyślnie korzysta z plików ELF, zaś systemy Windows używają plików PE [Pie94][mpe10]
[elfsp][elf64].
Plik wykonywalny posiada tzw. entry point, w którym zaczyna się wykonywanie procesu.
Dynamicznie ładowane biblioteki mają także entry point, którego kod jest wykonywany w chwili
załadowania biblioteki do pamięci lub usuwania biblioteki z pamięci procesu. Nie należy mylić entry
point z funkcją main() w programach języka C, ponieważ przed rozpoczęciem wykonywania kodu
funkcji main(), wcześniej pracę wykonują funkcje umieszczone w kodzie przez kompilator, które
przygotowują środowisko i m. in. przygotowują argumenty funkcji main() [26][elfsp].
Rys. 13 obrazuje budowę typowego wykonywalnego pliku PE systemu Windows [Pie94]
[mpe10]. Kolejne wycinki oznaczają:
• MSDOS Header i DOS Stub – nagłówek pliku wykonywalnego systemu DOS oraz program
DOS, dzięki któremu program uruchomiony w systemie DOS zwykle wyświetla wiadomość
„This program cannot be run in DOS mode” i kończy działanie.
• Signature – sygnatura pliku PE, wskazuje wersję pliku PE.
• IMAGE_FILE_HEADER – struktura zawierająca informacje takie jak typ procesora, liczba
sekcji czy typ pliku: program, dynamiczna biblioteka, plik OBJ i inne.
24. 24
• IMAGE_OPTIONAL_HEADER – struktura zawierająca m. in. wersję linkera, rozmiar kodu,
adres entry point, wersję systemu operacyjnego, informacje przyspieszające ładowanie pliku
do pamięci i inne.
• IMAGE_SECTION_HEADERS – struktury opisujące sekcje znajdujące się w pliku, ich nazwy,
rozmiary i inne.
• .text – sekcja zwykle zawierająca kod programu.
• .data – sekcja zwykle zawierająca dane programu.
• .rsrc – sekcja zwykle zawierająca osadzone pliki i grafikę.
• .idata – tablica importów, czyli lista funkcji importowanych (używanych) przez program oraz
nazwy bibliotek dll, je zawierających.
• .edata – tablica eksportów, czyli lista funkcji, które mogą być wywołane z tego pliku
wykonywalnego.
Opisane powyżej sekcje i ich nazwy mają jedynie charakter informacyjny, ponieważ mogą się niemal
dowolnie zmieniać [Pie94][mpe10].
Rys. 13 Budowa typowego pliku PE systemu Windows.
Rys. 14 Budowa pliku wykonywalnego ELF.
Rys. 14 obrazuje budowę wykonywalnego pliku ELF [elfsp][elf64]. Kolejne fragmenty oznaczają:
• ELF header – opisuje organizację pliku, typ procesora, entry point, wersję ABI i inne.
• Program header table – zawiera informacje niezbędne do utworzenia procesu.
• Sekcje – zawierają kod programu, dane.
• Section header table – opisuje sekcje w pliku, nazwy, rozmiary i inne.
MSDOS Header
DOS Stub
Signature
IMAGE_FILE_HEADER
IMAGE_OPTIONAL_HEADER
IMAGE_SECTION_HEADERs
.text
.data
.rscr
.idata
.edata
ELF header
Program header table
Sekcje
Section header table
25. 25
Wśród sekcji pliku pojawiają się specjalne sekcje, które opisują np. importowane funkcje.
Podobnie jak w przypadku plików PE, nazwy sekcji mogą się zmieniać (poza sekcjami specjalnymi)
tak, jak ich pozycja w pliku [elfsp][elf64].
Uruchomione programy stają się procesami i są zarządzane przez system operacyjny.
Uruchomienie nowego procesu nie jest jednak takie proste jak brzmi – system operacyjny musi
wykonać szereg zabiegów zanim zostanie powołany do życia nowy, w pełni sprawny proces.
Warto w tym momencie wspomnieć o ABI dla plików wykonywalnych. ABI jest zbiorem zasad,
które formalizuje typy i rozmiar argumentów, sposób nazywania oraz wywoływania funkcji (ang.
calling conventions) z binarnych plików wykonywalnych [sys10][wca64]. Dzięki standardowemu
interfejsowi binarnemu dla bibliotek języka C możliwe jest dynamiczne ładowanie tych bibliotek oraz
wywoływanie funkcji w nich się znajdujących.
W środowisku x86 istnieją trzy szeroko stosowane konwencje wywoływania funkcji [Eil00], są
to cdecl, stdcall oraz fastcall. Pierwsza z nich jest standardową konwencją wykorzystywaną domyślnie w
językach C oraz C++. Argumenty funkcji są przekazywane przez stos, na którym są umieszczane w
odwrotnej kolejności. Uprzątnięcie stosu z argumentów funkcji leży w gestii kodu wywołującego
funkcję. Konwencja stdcall jest bardzo popularna w systemach Windows, gdyż jest wykorzystywana w
bibliotekach systemowych. Podobnie jak w konwencji cdecl argumenty są przekazywane przez stos,
jednak w tym przypadku są one umieszczane w normalnej kolejności, oraz to wywoływana funkcja
jest odpowiedzialna za usunięcie argumentów ze stosu po jej wykonaniu. Konwencja fastcall, jak
nazwa wskazuje, jest zoptymalizowana do szybkiego wywoływania. Pierwsze dwa argumenty funkcji
są przekazywane przez rejestry ECX oraz EDX, kolejne zaś przez stos.
W środowisku x64 obowiązują zupełnie inne konwencje wywoływania funkcji niż w
środowisku x86 . W systemach Linux parametry funkcji są przekazywane w normalnej kolejności przy
czym:
• Argumenty całkowitoliczbowe (także wskaźniki) przez rejestry RDI, RSI, RDX, RCX, R8, R9, a
kolejne przez stos.
• Argumenty zmiennoprzecinkowe przez rejestry zmiennoprzecinkowe XMM0 do XMM7, a
kolejne przez stos.
Z innych, ciekawych informacji na temat funkcji w systemie Linux w środowisku x64, to
wywoływana funkcja posiada zarezerwowany dla niej obszar 128 bajtów powyżej wskaźnika ramki
stosu, przechowywanego w RSP, zwany "red zone", gdzie można przechowywać zmienne tymczasowe
funkcji, zamiast je wrzucać na stos za pomocą pushq. Dodatkowo wskaźnik ramki stosu jest ustawiony
"na sztywno" i nie zmienia swojego położenia po wywołaniu pushq/popq. Pozwala to na operacje na
stosie bez używania rejestru EBP do przechowywania wskaźnika podstawy stosu (początku ramki).
Przypomnę jeszcze tylko, że ramka stosu jest tworzona przy każdorazowym wywołaniu funkcji
[Bry05][sys10].
W przypadku systemów Windows w środowisku x64 sytuacja wygląda podobnie [wca64]. Parametry
funkcji są przekazywane w normalnej kolejności, przy czym:
• Argumenty całkowitoliczbowe (także wskaźniki) przez rejestry RCX, RDX, R8, R9, a kolejne
przez stos.
• Argumenty zmiennoprzecinkowe przez rejestry zmiennoprzecinkowe XMM0 do XMM3, a
26. 26
kolejne przez stos.
2.2.3 Wywo ania systemoweł
Każdy proces ma określoną pulę możliwych instrukcji, które może wykonać. Jeśli chce zrobić
coś ponadto, musi poprosić system operacyjny, aby wykonał dane zadanie w jego imieniu. Ta
komunikacja pomiędzy procesem, a systemem ma miejsce przy pomocy przerwań systemowych, co
ilustruje rys. 15 [Sil05].
Rys. 15 Przerwanie systemowe powoduje wywołanie usług systemowych.
Przerwanie to pojedyncza instrukcja maszynowa:
int 0x80 ;Linux
int 0x2e ;Windows
sysenter
Numer przerwania jest charakterystyczny dla systemu operacyjnego i wynosi 0x80 dla systemu
Linux oraz 0x2e dla systemów Windows z rodziny NT. Dodatkowo, nowoczesne systemy operacyjne
wykorzystują instrukcję sysenter, zamiast przerwania, która jest zoptymalizowana dla wywołań
systemowych [Ker10][Bov00][Sch01].
Systemy UNIXlike mają dobrze udokumentowane przerwania systemowe. Ich listę wraz z
numerami można znaleźć w pliku nagłówkowym unistd.h, lub włączanych przez niego plikach:
#ifndef _ASM_X86_UNISTD_32_H
#define _ASM_X86_UNISTD_32_H
/*
* This file contains the system call numbers.
*/
#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
#define __NR_waitpid 7
#define __NR_creat 8
#define __NR_link 9
(…)
Nazwy wywołań oraz ich argumenty często się pokrywają z funkcjami systemowymi o takich
samych nazwach. De facto funkcje te są bardzo cienkimi nakładkami na te przerwania. Argumenty dla
wywołań systemowych w systemie Linux są przekazywane kolejno przez rejestry EBX, ECX, EDX,
ESX, EDI, zaś numer przerwania jest umieszczany w rejestrze EAX. Rezultat wywołania systemowego
jest dostępny w rejestrze EAX [Bov00]. Przykład programu asemblerowego, który zwraca 2 jako
Proces
User Land Kernel Land
systemPrzerwanie
27. 27
numer wyjścia:
.text
.global _start
_start:
movl $1,%eax
movl $2,%ebx
int $0x80
Asemblacja, konsolidacja oraz wykonanie:
user@user:~$ as exit.s
user@user:~$ ld a.out -o exit
user@user:~$ ./exit
user@user:~$ echo $?
2
W systemach Windows wskaźnik do argumentów przerwania jest umieszczany w rejestrze EDX, zaś
numer przerwania w rejestrze EAX. Rezultat przerwania jest dostępny w rejestrze EAX [Sch01].
Rys. 16 Pośredniczenie bibliotek systemowych podczas wywołań systemowych.
W systemach Windows nie ma tak cienkiej nakładki na wywołania systemowe jak w
przypadku systemu Linux. Użytkownicy nie wywołują przerwań systemowych bezpośrednio, a
zamiast tego korzystają z dynamicznie linkowanych bibliotek, jak zostało to zilustrowane na rys. 16
[Sil05]. Przykładowo, podstawowe funkcje obsługujące pliki i wątki znajdują się w bibliotece
kernel32.dll, podczas gdy funkcje odpowiedzialne za interfejs użytkownika rezydują w user32.dll.
Obie te biblioteki intensywnie korzystają z natywnego interfejsu Windowsa znajdującego się w
ntdll.dll. Biblioteka ntdll.dll realizuje niektóre funkcje w trybie user, inne zaś realizuje wykonując
przerwanie do trybu jądra.
.text
.global _start
_start:
movl $0x101,%eax # zakończ proces
push $13 # kod wyjścia
push $-1
movl %esp,%edx
int $0x2e # przerwanie systemowe
Przykład 2 Program, który kończy działanie zwracając 13 jako kod wyjścia do systemu.
Jest oczywiście możliwe bezpośrednie wywoływanie przerwań systemowych, jest to jednak
bardzo utrudnione, gdyż o ile API Windowsa jest dobrze udokumentowane, o tyle jest już problem z
dokumentacją natywnego API w ntdll.dll, nie wspominając o bezpośrednich przerwaniach. W przy
kładzie 2 zaprezentowano program, który omija natywne API Windowsa. Asemblacja, konsolidacja
Proces
User Land Kernel Land
systemPrzerwanieNtdll.dll
Kernel32.dll
28. 28
oraz wykonanie programu:
C:>as test_calls.s
C:>ld -o test.exe a.out
C:>test.exe
C:>echo %errorlevel%
13
Warto zwrócić uwagę, że program ten został przygotowany dla systemu Windows XP SP3.
Kody wywołań systemowych mogą być inne dla różnych wersji systemu Windows.
2.3 Kontrolowanie wywo a systemowychł ń
Rozdział ten skupia się na kontrolowaniu wywołań systemowych w systemach Windows. Jak
wspomniano w poprzednim rozdziale, Windows korzysta z szeregu bibliotek systemowych. Zatem,
aby kontrolować wywoływanie usług systemowych należy przejąć kontrolę nad wywoływanymi
funkcjami. Technika nadpisywania wywołań funkcji nosi nazwę spoofing lub hooking. Nadpisywanie
to może mieć miejsce w trybie user albo kernel. Hooking w trybie kernel, co w tym wypadku
nazywane jest często syscall proxy, jest zdecydowanie bardziej zaawansowanym problemem [Hog05]
[Blun09][dpw10][wsp09] i zostanie pominięty w tej pracy.
2.3.1 Proxy DLL
Technika proxy dll [Hog05][mll08] korzysta z faktu, że podczas uruchamiania procesu loader
najpierw poszukuje bibliotek do załadowania w katalogu aplikacji, a dopiero potem, gdy nie znajdzie
potrzebnych bibliotek w katalogu programu, wówczas sprawdza inne miejsca. Jest to prawdą dla
większości bibliotek, poza kluczowymi bibliotekami systemowymi, które są poszukiwane w katalogu
systemu Windows przed pozostałymi miejscami. Aby móc zastosować technikę proxy dll dla tych
bibliotek trzeba najpierw zmodyfikować plik wykonywalny programu, zmieniając nazwy tych
bibliotek.
Na potrzeby tego rozdziału załóżmy, że chcemy nałożyć proxy na bibliotekę o nazwie lib.dll.
Pierwszym krokiem, który wykonujemy jest zmiana nazwy biblioteli lib.dll na, powiedzmy, origLib.dll.
Teraz tworzymy bibliotekę – proxy w katalogu programu, o takiej samej nazwie, jaką miała oryginalna
biblioteka, czyli lib.dll. Nasza biblioteka proxy powinna zawierać wszystkie te funkcje, które zawiera
lib.dll. Dodatkowo we wnętrzu wszystkich funkcji z lib.dll wywołujemy odpowiadające im funkcje z
origLib.dll. W tym momencie program działa bez zmian, ponieważ wszystkie funkcje są
przekierowane na ich oryginalne wersje. Jeśli chcemy zmodyfikować, działanie programu musimy
zmienić implementację tych funkcji z lib.dll, na któych nam zależy.
Aby zastosować tą technikę dla biblioteki systemowej, jak ntdll.dll, to musimy wykonać
dodatkowe kroki. Na początku modyfikowany jest plik wykonywalny programu – zmieniana jest
nazwa biblioteki systemowej ntdll.dll na przykład na prepn.dll. Następnie kopiowana jest biblioteka
systemowa ntdll.dll do katalogu programu i zmieniana jest jej nazwa np. na ntdll_orig.dll. Następnie
kopiowana jest specjalnie przygotowana biblioteka prepn.dll, która definiuje aliasy dla funkcji
systemowych tj. dla przykładowej funkcji ZwTerminateProcess definiowany jest alias
ntdll_orig.ZwTerminateProcess. W tym momencie wywołanie funkcji ZwTerminateProcess powoduje
wywołanie funkcji ntdll_orig.ZwTerminateProcess znajdującej się w ntdll_orig.dll i która jest
oryginalną systemową funkcją Windowsa. Nasza, przygotowana wcześniej, biblioteka prepn.dll
oprócz aliasów dla funkcji posiada także własne definicje tych funkcji, które chcemy nadpisać.
30. 30
importów czy lokalnie nadpisać funkcję. Wystarczy tylko ten kod umieścić w załadowanej bibliotece:
BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
if (ul_reason_for_call == DLL_PROCESS_ATTACH)
{
// kod wykonywany w przestrzeni użytkownika podczas ładowania biblioteki
}
return true;
}
__declspec (dllexport) LRESULT myFunction (int code, WPARAM wParam, LPARAM lParam)
{
return CallNextHookEx(g_hhook, code, wParam, lParam);
}
Funkcja DllMain() jest odpowiednikiem funkcji main() z programów języka C. W jej wnętrzu należy
umieścić kod odpowiednio nadpisze wywołania funkcji [Hog05][Blun09].
2.3.4 Wstrzykni cie DLL przy pomocy rejestruę
W systemach Windows od wersji Windowsa 2000 istnieje gałąź rejestru [Hog05][Blun09]:
[HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindows NTCurrentVersionWindows]
"AppInit_DLLs"=""
"DeviceNotSelectedTimeout"="15"
"GDIProcessHandleQuota"=dword:00002710
"Spooler"="yes"
"swapdisk"=""
"TransmissionRetryTimeout"="90"
"USERProcessHandleQuota"=dword:00002710
Zawiera ona klucz AppInit_DLLs. Dodanie biblioteki dll (pełnej ścieżki) do tego klucza spowoduje, że
za każdym razem, gdy uruchomiony zostaje proces, który korzysta z user32.dll, to wczytana zostaje
również wskazana biblioteka do przestrzeni adresowej tego procesu. Wystarczy tylko dodać
odpowiedni kod do funkcji DllMain() w tej bibliotece, która nadpisze wywołania funkcji. Można także
dodać więcej bibliotek do tego klucza oddzielając je znakiem spacji [Blun09].
2.3.5 Wstrzykni cie DLL przy pomocy zdalnego w tkuę ą
Jest to popularna technika, wykorzystana na przykład w programie WinAPIOverride32,
przedstawionym w rozdziale 2 wsteczna inżynieria oprogramowania. Składa się z następujących
etapów wykonywanych przez zewnętrzny proces [Hog05][Blun09]:
1. Pobierany jest uchwyt procesu, do którego ma być wstrzyknięta biblioteka dll, przy pomocy
funkcji OpenProcess().
2. Dynamicznie pobierany jest adres funkcji LoadLibrary() z biblioteki kernel32.dll przy pomocy
funkcji GetProcAddress() oraz GetModuleHandle().
3. Alokowana jest zdalnie pamięć przy pomocy virtualAllocEx().
4. W miejsce zaalokowanej pamięci kopiowana jest pełna nazwa biblioteki przy pomocy
WriteProcessMemory().
5. Tworzony jest zdalny wątek przy użyciu funkcji CreateRemoteThread(), który używa funkcji
LoadLibrary() do wstrzyknięcia biblioteki dll.
31. 31
2.4 Javascript i HTML5
Technologie internetowe stają się coraz bardziej funkcjonalne, co zostało zauważone zarówno
przez użytkowników jak i twórców coraz, to bardziej poważniejszego oprogramowania z interfejsem
użytkownika dla przeglądarek internetowych. Interfejs www staje się także bardziej naturalny dla
użytkowników. W ostatnich latach można było zauważyć wzrost zainteresowania tym medium wśród
twórców oprogramowania, w szczególności wzrosła liczba usług internetowych. Opracowanie to
także nie jest obojętne dla tych technologii i przewiduje interfejs użytkownika między innymi w fo
rmie przystosowanej dla przeglądarek internetowych. Technologie internetowe stały się również
zdecydowanie bardziej złożone, dlatego ten rozdział przybliża kilka z nich, pokazując możliwości
zwłaszcza tych, które zostały użyte dalej w opracowaniu. Opis HTML oraz CSS zostały pominięte w
tym rozdziale, choć zawiera on kilka informacji na temat wybranych elementów szkicu standardu
HTML 5 [39, 40].
2.4.1 JavaScript
Javascript jest skryptowym językiem programowania, którego implementacja jest obecna we
wszystkich popularnych przeglądarkach internetowych. Umożliwia on programowanie funkcyjne,
obiektowe oraz imperatywne. Jego składnia jest implementacją języka ECMAScript, zaś wersją, na
której bazuje większość implementacji w popularnych przeglądarkach internetowych, jest trzecia
edycja ECMA262. Standard ten opisuje takie elementy języka jak typy danych czy struktury sterujące
[ECM99].
Interakcja z elementami dokumentu HTML w przeglądarkach jest standaryzowana przez
World Wide Web Consortium w rekomendacjach wydawanych przez tą grupę. W3C stworzyło model
DOM oraz ustandaryzowało API dla manipulacji elementami dokumentów HTML. Obecnie wszystkie
popularne przeglądarki implementują DOM w wersji 1 oraz dużą część DOM w wersji 2 [DOM].
JavaScript umożliwia manipulację dokumentami HTML, arkuszami stylów CSS, oraz w ogra
niczonym stopniu oknami. Umożliwia wykonywanie kodu w zależności od akcji użytkownika, w tym
umożliwia obsługę myszy i klawiatury. W najbardziej zaawansowanych zastosowaniach może
posłużyć do zbudowania dynamicznego interfejsu użytkownika, podobnego do tradycyjnych
programów z graficznym interfejsem użytkownika, bądź dynamicznej grafiki, nawet trójwymiarowej
[Zak06][Zak07][HTML5][HTM11].
Pomimo rekomendacji wydawanych przez W3C, nadal nie istnieje pełna zgodność między
funkcjami języka JavaScript w różnych przeglądarkach [Zak06][Zak07], dlatego wśród programistów
tego języka popularne jest użycie frameworków, które ukrywają różnice w implementacjach. Jednym z
takich frameworków jest Mootools, choć istnieje oczywiście bardzo dużo innych, popularnych i
dojrzałych frameworków języka JavaScript, rozwijanych przez społeczności programistów, jak jQuery
czy Prototype. Dystrybuowane są one zwykle jako niezależne moduły, przez co użytkownicy mogą
dołączać te moduły fameworka do swoich programów, które w danym momencie potrzebują. Różne
frameworki nie zawsze są one ze sobą zgodne, przez co nie koniecznie można ich używać
jednocześnie.
2.4.2 XML
XML jest uniwersalnym, tekstowym formatem, standaryzowanym przez grupę W3C [XML08]
[Nam09]. Jest on członkiem szerszej rodziny formatów zgodnych z SGML, a częstym jego
32. 32
zastosowaniem jest przechowywanie danych w sposób niezależny od platformy sprzętowej oraz ze
wsparciem wielu systemów kodowania znaków. Powstało wiele formatów danych, bazujących na
dokumentach XML w tym MathML, RSS oraz SVG, które zdobyły dużą popularność w aplikacjach
webowych oraz przeglądarkach www. Dokumenty XML nie muszą być związane tylko z
przechowywaniem informacji, dowodem na to jest szereg języków programowania, które są
dokumentami XML. Jednym z takich języków jest język transformacji XSLT, którego kod jest
pełnoprawnym dokumentem XML. Świadczy to o dużej elastyczności oraz uniwersalności tego
formatu.
Pewną niedogodnością formatu XML jest brak wsparcia dla danych binarnych, które muszą
być odpowiednio przekonwertowane na znaki, zanim można je osadzić w dokumencie. Istnieje wiele
algorytmów, które mogą zostać wykorzystane w tym celu, między innymi Base64 stosowany w
wiadomościach mailowych [bbb06]. Format ten ma jeszcze jedną wadę, a mianowicie jest bardzo
ekspresywny, co skutkuje dużą nadmiarowością formy na treścią. Można sobie z tym radzić
kompresując dokumenty bezstratnymi algorytmami kompresji.
Obsługa dokumentów XML jest obecna we wszystkich popularnych przeglądarkach www. Do
manipulacji tymi dokumentami może zostać użyty język JavaScript. XML zdobył dużą popularność,
dzięki czemu dostępnych jest wiele bibliotek w różnych językach programowania wspierających tą
technologię. Obsługa tego formatu jest nawet częścią standardowej biblioteki niektórych języków
programowania jak Python czy Ruby.
2.4.3 Ajax
Ajax jest techniką programistyczną, na którą się składa szereg technologii zarówno po stronie
przeglądarki jak i serwera [Zak06][Zak07]. Główną jej ideą i zastosowaniem jest asynchroniczne
ładowanie zewnętrznych dokumentów z serwera. Dzięki temu, aplikacje JavaScript mogą być bardziej
interaktywne i w większym stopniu przypominać tradycyjne programy.
Rys. 18 Najczęściej spotykana struktura technologii stojących za Ajax.
Programiści JavaScript opracowali kilka sposobów realizacji asynchronicznych żądań HTTP.
Każdy z tych sposobów ma wady oraz zalety i dlatego w realnych zastosowaniach, najczęściej
korzysta się z kilku technik równocześnie.
Pierwszym sposobem jest technika ukrytej ramki [Zak07]. Wymaga ona, żeby strona www była
oparta na dwóch ramkach – pierwszej, zajmującej cały widoczny ekran, w której znajduje się
wyświetlana strona oraz drugiej, niewidocznej dla użytkownika. Adres niewidocznej ramki jest
zmieniany w kodzie programu JavaScript, co powoduje ładowanie nowych treści do ramki. Treści te,
po załadowaniu, są następnie interpretowane przez kod programu. Aby wykonać żądanie POST
należy utworzyć nowy formularz, wypełnić jego pola oraz wysłać go, używając metody submit().
Oczywistą wadą tej techniki jest wymagana obecność ramek, na których musi się opierać kod strony.
JavaScript
Serwer
Baza
Danych
Przeglądarka
www
XML/XSLT,
HTML,
Tekst,
JSON,
PHP
33. 33
Drugi sposób jest analogiczny do poprzedniego z tą różnicą, że ramka użyta do komunikacji
nie jest na stałe umiejscowiona w kodzie strony, lecz jest tworzona dynamicznie [Zak07]. Program
tworzy ramkę iframe, oraz używa jej do komunikacji, co zostało pokazane w przykładzie 4.
var oFrame = null;
window.onload = function(){
// utwórz ukrytą ramkę i dodaj ją do strony
var oEl = document.createElement("iframe");
oEl.style.display = "none";
oEl.name = "dynFrame";
oEl.id = "dynFrame";
document.body.appendChild(oEl);
oFrame = frames["dynFrame"];
// odczekaj 10 ms
setTimeout(makeRequest, 10);
};
function makeRequest(){
oFrame.location = "hello.html"; // wyślij żądanie
}
Przykład 4 Kod JavaScript, umieszczany na głównej do komunikacji z serwer przy użyciu ukrytej ramki.
Plik hello.html zawiera kod, który ma być wykonany po wczytaniu ramki:
<html>
<head>
<script type="text/javascript">
alert("Hello!");
</script>
</head>
<body>
</body>
</html>
Także i w tym przypadku, aby wysłać żądanie POST należy wczytać formularz do ukrytej
ramki, wypełnić jego pola, a następnie go wysłać. Dostęp do okna klienta w kodzie Javascript serwera
jest możliwy poprzez obiekt parent.
Zupełnie oddzielnym sposobem wykonywania asynchronicznych żądań HTTP jest użycie
obiektu XMLHttpRequest [Zak06][Zak07], obecnego we wszystkich popularnych przeglądarkach www.
Obiekt ten pozwala zdefiniować funkcję, która ma być wywołana w momencie ukończenia żądania,
bądź błędu połączenia. Technika ta jest zdecydowanie najbardziej wygodna z punktu widzenia
programisty JavaScript oraz pozwala na dostęp do nagłówków HTTP oraz kodów zwrotnych serwera.
Niestety także i ona nie jest ona pozbawiona wad. W odróżnieniu od metod opartych na ramkach,
użycie obiektu XMLHttpRequest nie powoduje zmian historii przeglądarki, przez co nie pozwala
symulować nawigacji za pomocą standardowych opcji cofnij/ponów z przeglądarek.
Do demonstracji tej techniki utworzone zostały dwa pliki – hello.txt, zawierający napis „Hello!”
oraz hello.php, którego zawartość jest następująca:
<?php
header("Content-Type: text/plain");
echo $_POST['msg'].' user!';
?>
34. 34
Kod Javascript, który wysyła żądania GET oraz POST:
// utwórz obiekt
function createXHR(){
if (window.XMLHttpRequest) { // IE7+, Firefox, Chrome, Safari, and Opera
var oXHR = new XMLHttpRequest();
} else { // IE5, IE6
var aVersions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0"];
for (var i = 0; i < aVersions.length; i++) {
try {
var oXHR = new ActiveXObject(aVersions[i]);
return oXHR;
}
catch (oError) {
// ignoruj
}
}
}
return oXHR;
}
window.onload = function(){
var oXHR = createXHR();
// wyślij żądanie GET
oXHR.open("get", "hello.txt", true);
oXHR.onreadystatechange = function(){
if (oXHR.readyState == 4) {
alert("Response: " + oXHR.responseText);
}
};
oXHR.send(null);
// wyślij żądanie POST
var oXHRp = createXHR();
var sMsg = "Hello";
oXHRp.open("post", "hello.php", true);
oXHRp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
oXHRp.onreadystatechange = function(){
if (oXHRp.readyState == 4) {
alert("Response: " + oXHRp.responseText);
}
};
oXHRp.send("msg=" + encodeURIComponent(sMsg));
}
Odpowiednik powyższego kodu, napisany przy użyciu frameworku Mootools:
window.onload = function(){
new Request({
url: "hello.txt",
method: "get",
onSuccess: function(responseText, responseXML){
alert("Response: " + responseText);
}
}).send();
var sMsg = "Hello";
new Request({
url: "hello.php",
method: "post",
data: "msg=" + encodeURIComponent(sMsg),
onSuccess: function(responseText, responseXML){
alert("Response: " + responseText);
}
}).send();
}
Istnieją jeszcze dwie techniki asynchronicznych żądań HTTP – jedna oparta na atrybucie src
obrazków oraz druga, bazująca na dynamicznym wczytywaniu skryptów JavaScript (JSONP) [Zak07].
35. 35
Pierwsza, wspomniana technika nie jest wykorzystywana w tym opracowaniu więc zostanie
pominięta. Dynamiczne wczytywanie skryptów bazuje na fakcie, że zewnętrzny dokument JavaScript
zostaje pobrany przez przeglądarkę w momencie dodania elementów <script> go zawierających do
strony. Kod po stronie przeglądarki może wyglądać następująco:
window.onload = function() {
var oScript = document.createElement("script");
oScript.type = "text/javascript";
oScript.src = "dynamicScript.php?functionToCall=dynScriptLoaded";
document.body.appendChild(oScript);
};
function dynScriptLoaded(sMessage){
alert("Dynamic script loaded with message: " + sMessage);
}
Zawiera on funkcję dynScriptLoaded(), która zostanie wykonana po załadowaniu kodu dynamicznego
skryptu. Zaś po stronie serwera, plik dynamicScript.php, jest dynamicznie generowanym kodem
programu Javascript, który może wyglądać następująco:
<?php header("Content-type: text/javascript"); ?>
var sMessage = "Hello!";
<?php echo $_GET["functionToCall"] ?>(sMessage);
Po załadowaniu skryptu dynamicScript.php wyświetlony zostanie alert zawierający przesłaną
wiadomość. Podstawową zaletą techniki dynamicznego skryptu JavaScript jest to, że pozwala ona
obejść politykę bezpieczeństwa wspólnego pochodzenia, która nie pozwala wysyłać żądań HTTP do
dokumentów nie znajdujących się w tej samej domenie, co strona na której jest wykonywany kod.
Wadą tego sposobu jest to, że nie pozwala na wysyłanie żądań POST. Kłopotliwe jest także
obsługiwanie błędów, ze względu na brak możliwości ich detekcji, poza odczekiwaniem przez
określony czas na odpowiedź.
Jakie typy danych są pobierane przez asynchroniczne żądania HTTP, opisane powyżej? Jednym
słowem różne, choć najczęściej są to dokumenty tekstowe, XML, XSLT, HTML bądź kod Javascript
[Zak06][Zak07]. Czasem wykorzystywane są alternatywne formaty np. JSON, który jest
pełnoprawnym kodem Javascript i który dzięki temu, upraszcza parsowanie danych.
Co jest potrzebne po stronie serwera? Dowolny serwer www, który będzie udostępniał żądane
dokumenty. Najczęściej jest to apache z modułem PHP5, do dynamicznego generowania dokumentów
i dostępu do bazy danych. [Zak07]
CORS jest standardem, który umożliwia ominięcie polityki wspólnego pochodzenia w
przeglądarce internetowej. W przeciwieństwie do JSONP, który umożliwia wysyłanie tylko żądań GET
między różnymi domenami, CORS wspiera także żądania POST. Jest to stosunkowo nowy standard,
dostępny przeglądarkach dopiero od kilku lat. Jest on obsługiwany przez firefoxa od wersji 3.5
(wydanej w czerwcu 2009 roku), chrome od bardzo wczesnych wersji tej przeglądarki oraz Internet
Explorer od wersji 8. Planowana jest jego implementacja w przeglądarce opera w wersji 12. Z punktu
widzenia programisty użycie CORS to używanie obiektu XMLHttpRequest w standardowy sposób,
bez żadnych zmian uwzględniających pochodzenie zasobów z innych domen. Obsługa standardu
odbywa się przez przeglądarkę w transparentny dla programisty sposób. Jedynie po stronie serwera
jest wymagany dodatkowy nagłówek http w odpowiedzi, który służy zdeterminowaniu adresów
36. 36
domen, spod których dany zasób jest dostępny.
W przeglądarce Internet Explorer funkcjonalność asynchronicznego i międzydomenowego
żądania http jest zaimplementowana przypomocy obietku XDomainRequest. Tak samo jak w innych
przeglądarkach wymagany jest dodatkowy nagłówek "AccessControlAllowOrigin" w odpowiedzi
serwer przy żądaniu o zasób.
2.4.4 XSLT i XPath
XSLT jest językiem, którego kod źródłowy jest dokumentem XML. Służy on głównie do
transformacji jednych dokumentów XML w inne dokumenty XML bądź inne dokumenty tekstowe.
Powszechne jest jego użycie do przekształcania treści stron internetowych w dokumenty pdf, bądź
transformowania dokumentów XML w strony HTML. Kod programu języka XSLT jest interpretowany
przez procesor XSLT. Procesor ten na podstawie poleceń kodu programu przekształca dokumenty
XML dostarczone do transformacji [Zak07][XSL99]. Obrazowo schemat ten został pokazany na rys. 19.
Procesor XSLT 1.0 jest zaimplementowany we wszystkich popularnych przeglądarkach internetowych
i dlatego chętnie jest on wykorzystywany przez programistów JavaScript do transformacji
dokumentów XML, uzyskiwanych przy użyciu technologii Ajax, opisanej w następnym rozdziale.
Programy XSLT intensywnie korzystają z technologii XPath, do selekcjonowania interesujących
fragmentów dokumentów XML [XPa99]. Dzięki XPath wybieranie węzłów w dokumentach jest dużo
łatwiejsze. Technologia ta przypomina w pewnym sensie wyrażenia regularne, gdyż wyrażenia XPath
mogą być równie zawiłe i niezrozumiałe. XPath jest częścią języka XSLT, lecz dostępne są także
implementacje niezwiązane z XSLT. Przeglądarki internetowe udostępniają interfejs dla programistów
języka JavaScript, który może posłużyć do używania XPath do wyboru węzłów dokumentów XML.
Składnia wyrażenia XPath składa się z dwóch części – węzła lub zbioru węzłów, którego
elementów zapytanie ma dotyczyć oraz samego wyrażenia wyszukującego. Kilka przykładów zapytań
XPath:
//X | //Y – wybierz wszystkie elementy X oraz Y z dokumentu.
X[1] – wybierz pierwsze dziecko X.
X[Z eq '10'] – wybierz dzieci X, które mają dziecko o nazwie Z i wartości '10'.
X[@a ne '10'] - wybierz dzieci X, które mają atrybut o nazwie a, który nie ma wartości '10'.
X[number(@a) < 10] – wybierz dzieci X, które zawierają atrybut a o wartości mniejszej od 10.
X/na:Y – wybierz wszystkie dzieci X o nazwie Y w dokumencie, które należą do przestrzeni nazw
„na”.
/X/Y[last() - 1] – wybierz przedostatnie dziecko X o nazwie Y, X jest głównym elementem
dokumentu (korzeniem).
X[sum(@a*) < 7] – wybierz dzieci X, których atrybuty sumują się do wartości mniejszej niż 7.
//*[count(X) = 2] – wybierz elementy, które mają dwójkę dzieci
//X[position() mod 2 = 0 ] - wybierz elementy X, które mają parzystą pozycje jako dzieci –
wybiera, co drugie dziecko.0
Poniższy kod JavaScript ilustruje użycie XPath do wyboru wartości 23.6 z dokumentu:
var sXmlSample = '<Root><ProcOut id="34"><Data>23.6</Data></ProcOut><ProcOut
id="14"><Data>27.4</Data></ProcOut></Root>';
var sXPath = "//Data[number(.) < 25]"; // wyrażenie XPath
if (window.ActiveXObject) { // IE
var oXmlDom = new ActiveXObject("Microsoft.XmlDom");
oXmlDom.loadXML(sXmlSample); // załaduj z ciągu znakowego
oXmlDom.setProperty("SelectionLanguage", "XPath");
var aNodes = oXmlDom.documentElement.selectNodes(sXPath);
} else { // DOM
var oParser = new DOMParser();
var oXmlDom = oParser.parseFromString(sXmlSample, "text/xml"); // załaduj z ciągu znakowego
var oResult = oXmlDom.evaluate(sXPath, oXmlDom, null,
XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
37. 37
var aNodes = new Array;
if (oResult != null) {
var oElement;
while (oElement = oResult.iterateNext()) {
aNodes.push(oElement);
}
}
}
for(var i = 0; i < aNodes.length; ++i){
alert("Numb = " + aNodes[i].firstChild.nodeValue); // wyświetla „Numb = 23.6”
}
2.4.5 Comet
Bolączką programistów systemów bazujących na Ajax, są programy które oprócz interakcji
typu żądanie – odpowiedź, muszą także obsługiwać dane wysyłane przez serwer bez ich żądania
przez klienta. Jest to o tyle kłopotliwe, że technika Ajax bazuje na żądaniach HTTP, przez co
dziedziczy charakter typu żądanie – odpowiedź tego protokołu. Programiści Javascript opracowali
kilka technik umożliwiających emulowanie wysyłania danych przez serwer, które zostaną krótko
omówione w tym rozdziale. Technika wysyłania danych od serwera ma różne nazwy, m. in. Comet i
HTTP Streaming [Zak07].
Najbardziej oczywistym sposobem emulowania wysyłania danych przez serwer jest
nadmiarowe (pooling) wysyłanie żądań HTTP [Zak07]. Klient wysyła więcej żądań, przy użyciu
obiektów XMLHttpRequest, bądź dynamicznych skryptów Javascript, niż jest to mu potrzebne. Dzięki
temu serwer zawsze dysponuje żądanie HTTP, które może użyć do wysłania danych. Wadą tego
sposobu jest nadmiarowość, która powoduje zwiększone użycie zasobów, co może mieć znaczenie
przy dużym ruchu sieciowym. Rozwiązanie to może być także zbyt wolne przy dużych opóźnieniach
sieciowych.
Innym sposobem jest użycie techniki ukrytej ramki [Zak07]. Do zilustrowania tej techniki,
użyjmy kodu po stronie klienta z przykładu 4, z ukrytą ramką, z rozdziału Ajax, tego opracowania.
Zmienimy adres żądane dokumenty z hello.html, na dynamicznie generowany hello.php, którego
zawartość jest następująca:
<html><head></head><body>
<script type="text/javascript">
alert("Hello!");
</script>
<?php
// wyślij dane
ob_flush();
flush();
sleep(10); // odczekaj 10 sek
?>
<script type="text/javascript">
alert("Hello again!");
</script>
</body></html>
Kod ten spowoduje wyświetlenie okna z napisem „Hello!”, a po 10 sekundach następnego, z
napisem „Hello again!”. Podczas używania tej, oraz innych technik Comet należy pamiętać, że
serwery www mają ograniczony czas połączenia, oraz że połączenie może zostać zerwane. Aby
wykryć takie sytuacje kod JavaScript po stronie serwera powinien od czasu do czasu sygnalizować, że
połączenie jest otwarte np. wysyłając ustaloną wiadomość lub wywołując ustaloną funkcję. Kod po
stronie klienta powinien natomiast sprawdzać, czy otrzymywane są potwierdzenia od serwera. W
przypadku wykrycia braku potwierdzeń połączenie powinno zostać wznowione. Wadą techniki jest
38. 38
to, że przeglądarki w czasie połączenia zachowują się jakby ładowana była strona tj. zmieniają kursor
na zajęty oraz zmieniają powiadomienia na pasku statusu na ładowanie.
Pozostałe techniki Comet są charakterystyczne dla konkretnych przeglądarek i są używane, by
wyeliminować niekorzystne zachowanie interfejsu użytkownika przeglądarek podczas korzystania z
techniki ukrytej ramki [Zak07]. W przeglądarce Internet Explorer używany jest obiekt ActiveX
HTMLFile. Obiekt ten symuluje okno przeglądarki, przez co można dodać do niego ramkę, która
będzie służyć do realizacji techniki Comet. W przeglądarce Opera można wykorzystać
zaimplementowaną w tej przeglądarce technologię ServerSent DOM Events. Sprowadza się to do
dodania elementu HTML, który jest zdarzeniem i którego źródło wskazuje na dokument na serwerze.
Następnie do tego elementu przypisuje się funkcję nasłuchującą, która jest wykonywana w momencie
wysłania zdarzeń przez serwer.
W przeglądarkach bazujących na silniku Gecko, jak Mozilla FireFox, oraz tych bazujących na
silniku WebKit, jak Chrome, można zrealizować to w przyjemny dla programisty sposób. Do
komunikacji używa się obiektu XMLHttpRequest, któremu przypisuje się funkcję wywoływaną
podczas zdarzenia ReadyStateChange. Zdarzenie to jest generowane podczas każdego otrzymania
danych od serwera w tych przęglądarkach.
2.4.6 Websocket
Rozdziały Ajax oraz Comet w tym opracowaniu powinny uświadomić każdego, jak wiele
pracy musieli włożyć programiści Javascript, aby osiągnąć interaktywność programów na poziomie,
który spotykamy współcześnie. Niemniej, opisane techniki są tylko sztuczkami omijającymi sztuczne
ograniczenia języka JavaScript w przeglądarkach. Kreatywność programistów została zauważona
przez twórców samych przeglądarek i dlatego opracowywany standard HTML 5 zawiera obsługę
technologii websocket. Sam standard HTML 5 ciągle jeszcze ewoluuje, lecz duża jego część jest już
zaimplementowana w przeglądarkach, w tym websockety.
Websocket jest tym, co brakowało w JavaScript, a jest dostępne dla tradycyjnych programów od
samego początku internetu. Websocket jest protokołem sieciowym, służącym do emulowania
tradycyjnych gniazd sieciowych na potrzeby serwisów sieciowych i programów JavaScript. Sam
protokół jest standaryzowany przez IETF [Web11], zaś jego obsługa i interfejs programistyczny w
przeglądarkach jest standaryzowane przez W3C [WPI11]. Jego użycie pozwala uniknąć narzutu kodu i
komplikacji, jakie są częścią Ajax oraz Comet.
Z punktu widzenia programisty JavaScript użycie websocket jest intuicyjne:
var socket = new WebSocket('ws://distos.net:7636/app');
socket.onopen = function(){
// kod wykonywany po otwarciu połączenia
socket.send("message", "userProtocol"); // wyślij wiadomość
};
socket.onclose = function(){
// kod wykonywany przy zamknięciu połączenia
};
socket.onmessage = function(oMsg){
var sMsg = oMsg.data; // odczytanie przesłanych danych
};