Auxiliary Vectors są mechanizmem przekazywania do programu informacji, które zwykle są dostępne tylko podczas programowania modułów jądra (lub są po prostu trudno dostępne). Informacje te zostają wstrzyknięte do pamięci procesu przez jądro, a konkretnie przez loader plików binarnych formatu ELF. Poniższa notka krótko opisuje w jaki sposób można dostać się do tych informacji i jak je zinterpretować.
Wektory pomocnicze (posiłkowe? Właściwie to nie mam pomysłu jak przetłumaczyć tą nazwę) są częścią „tablic formatu ELF”, do których należą tablica argumentów oraz tablica zmiennych środowiskowych. Dla lepszego zobrazowania sytuacji powiem, że przez tablicę argumentów rozumiem tą tablicę, którą w poniższym wycinku kodu oznaczyłem jako argv, natomiast tablica zmiennych środowiskowych to envp:
- int main(int argc, char *argv[], char *envp[]) {
- // …
- }
Dla mniej zorientowanych spieszę z wyjaśnieniem, że tak, to jest prawidłowa definicja funkcji main ;). Tablica zmiennych środowiskowych (envp) zawiera po prostu listę nazw zmiennych i ich wartości w takiej postaci:
- envp[0] = „NAZWA1=WARTOŚĆ1”
- envp[1] = „NAZWA2=WARTOŚĆ2”
- …
- envp[n] = „NAZWAn=WARTOŚĆn”
Ostatni element tej tablicy zawiera wskaźnik NULL, na który można ustawić warunek zakończenia iteracji pętli chodzącej po kolejnych elementach. W myśl powiedzenia: „umarł król, niech żyje król”, koniec tablicy zmiennych środowiskowych oznacza początek tablicy wektorów pomocniczych (posiłkowych? :)). Tablica ta jednak ma już nieco inny format, ponieważ jest szeregiem struktur Elf32_auxv_t (lub Elf64_auxv_t na x86_64), gdzie każdy element to inny wektor. Definicja tego typu znajduje się w pliku /usr/include/elf.h, który z kolei dostępny jest po zainstalowaniu paczki libc6-dev (na Ubuntu lub Linux Mint).
Można w prosty sposób sprawdzić, czy te wektory istnieją naprawdę, pytając loader o ich treść. Wykorzystamy do tego prosty program /bin/true, którym jedynym sensem istnienia jest zwracanie prawdy (prawdziwy wzór do naśladowania). Przed jego uruchomieniem należy stworzyć zmienną środowiskową LD_SHOW_AUXV na 1, a otrzymamy w konsoli listę wektorów wraz z ich wartościami:
- antek@tranquility:~/dev/cpp/envaddr$ LD_SHOW_AUXV=1 /bin/true
- AT_SYSINFO_EHDR: 0x7fff3f5ff000
- AT_HWCAP: bfebfbff
- AT_PAGESZ: 4096
- AT_CLKTCK: 100
- AT_PHDR: 0x400040
- AT_PHENT: 56
- AT_PHNUM: 9
- AT_BASE: 0x7f949af39000
- AT_FLAGS: 0x0
- AT_ENTRY: 0x400f40
- AT_UID: 1000
- AT_EUID: 1000
- AT_GID: 1000
- AT_EGID: 1000
- AT_SECURE: 0
- AT_RANDOM: 0x7fff3f4b7d59
- AT_EXECFN: /bin/true
- AT_PLATFORM: x86_64
Tak więc widać już, że wektory są jak najbardziej realne i tylko czekają na wykorzystanie ;).
Do czego może jednak służyć tablica tych wektorów? Właściwie do różnych rzeczy, albo żadnych :) - gdyby ich wykorzystanie było niezbędne dla użytkownika (a przez użytkownika mam na myśli programistę, który pisze program w środowisku systemowym), wtedy istniałyby odpowiednie bindingi lub funkcje w standardowej bibliotece. Zamiast tego, wartości z wektorów służą pomocą głównie standardowej bibliotece właśnie, która stosując się do tych informacji w odpowiedni sposób inicjuje środowisko naszego programu.
Jak odczytać wektory z poziomu programu? Teoria znajduje się kilka paragrafów wcześniej, natomiast praktyka może wyglądać na wykorzystaniu narzędzia gdb w celu odnalezienia wektorów, jak również można napisać własny krótki program wyświetlający wektory na ekranie (język C):
- #include <elf.h>
- #include <stdio.h>
- int main(int argc, char *argv[], char *envp[]) {
- Elf64_auxv_t *v;
- while(* envp++);
- printf("------- program auxv's below --------\n");
- for(v = (Elf64_auxv_t *) envp; v->a_type; v++) {
- printf("%02ld = 0x%08lX\n", v->a_type, v->a_un.a_val);
- }
- return 0;
- }
Wyżej przytoczony program należy skompilować np. w ten sposób, po czym uruchomić:
- antek@hydra ~/dev/c $ gcc -ansi -pedantic auxv.c -o auxv && LD_SHOW_AUXV=1 ./auxv
- AT_SYSINFO_EHDR: 0x7fffea993000
- AT_HWCAP: bfebfbff
- AT_PAGESZ: 4096
- AT_CLKTCK: 100
- AT_PHDR: 0x400040
- AT_PHENT: 56
- AT_PHNUM: 9
- AT_BASE: 0x7f6d0d3e6000
- AT_FLAGS: 0x0
- AT_ENTRY: 0x400480
- AT_UID: 1000
- AT_EUID: 1000
- AT_GID: 1000
- AT_EGID: 1000
- AT_SECURE: 0
- AT_RANDOM: 0x7fffea9626f9
- AT_EXECFN: ./auxv
- AT_PLATFORM: x86_64
- ------- program auxv's below --------
- 33 = 0x7FFFEA993000
- 16 = 0xBFEBFBFF
- 06 = 0x00001000
- 17 = 0x00000064
- 03 = 0x00400040
- 04 = 0x00000038
- 05 = 0x00000009
- 07 = 0x7F6D0D3E6000
- 08 = 0x00000000
- 09 = 0x00400480
- 11 = 0x000003E8
- 12 = 0x000003E8
- 13 = 0x000003E8
- 14 = 0x000003E8
- 23 = 0x00000000
- 25 = 0x7FFFEA9626F9
- 31 = 0x7FFFEA962FF1
- 15 = 0x7FFFEA962709
Niektóre wartości na pierwszy rzut oka mogą się nie zgadzać, ale tylko dlatego, że wartości przedstawione są w innym systemie liczbowym, a nazwy wektorów numerycznie zamiast symboli ;).
W kodzie źródłowym widać dwie pętle; najpierw pętla while(* envp++); (ze średnikiem na końcu) ustawia wskaźnik envp na ostatniego NULL'a, oznaczającego koniec tablicy zmiennych środowiskowych. Jednocześnie, zgodnie z charakterystyką operatora ++ wskaźnik ten ustawiany jest na pierwszy element tablicy wektorów. Druga pętla, for, iteruje po każdym elemencie wektorze, aż do napotkania wektora AT_NULL (czyli wektora zerowego, o symbolu zerowym, etc). Każda iteracja to wywołanie funkcji printf(), która w skrócie wyświetla numer porządkowy wektora (v->a_type) oraz wartość, jaką trzyma wektor (v->a_un.a_val).
Tak więc, jak widać na powyższym przykładzie, każdy wektor ma swój symbol i wartość. Lista symboli znajduje się we wcześniej przytoczonym pliku /usr/include/elf.h, natomiast wartość wektora zależy od jego symbolu – niektóre pola „wartość” używają do zapisywania wskaźników do konkretnych danych, niektóre po prostu liczb. Poniżej znajduje się krótki opis tych bardziej pomocnych wektorów jak i typy wartości, które można z nich wyciągnąć.
AT_EXECFN (31)
W polu wartości wektor ten oczekuje wskaźnika do ciągu znaków, który jest nazwą pliku aktualnie wykonywanego programu. Nazwa ta zawiera też przy okazji ścieżkę relatywną do katalogu w którym znajduje się użytkownik uruchamiający program.
AT_SYSINFO (32), AT_SYSINFO_EHDR (33)
Wskaźniki do sekcji pamięci zawierającej dynamicznie generowany kod wywoływania syscalli. W dużym skrócie, znajduje się tutaj ten sam adres, który zarezerwowany jest dla sekcji [vdso] w pliku maps w procfs, natomiast bardziej obszerny opis wychodzi nieco poza sztywne i z góry narzucone ramy tematyczne tego posta. Pewnego dnia postaram się opisać tą sekcję i dlaczego jest ona taka ciekawa, lecz dzień ten jeszcze nie nastąpił ;).
AT_PAGESZ (6)
Wartość tego wektora oznacza rozmiar strony pamięci na danym systemie. Informacje te można pobrać też w nieco wygodniejszy sposób za pomocą funkcji getpagesize(), lub wywołania sysconf(_SC_PAGESIZE) na systemach, gdzie getpagesize() nie istnieje - np. HP-UX.
AT_BASE (7)
Wektor zawiera wskaźnik do pamięci, pod którą został załadowany linker /lib/ld.so. Linker ten wymagany jest w przypadku, gdy plik programu (w formacie ELF) posiada wpisy wymagające ładowanie współdzielonych obiektów (lub, w win-mowie: bibliotek dynamicznego łączenia) – zostają one przez niego zlokalizowane i zmapowanie do odpowiednich regionów pamięci. Więcej informacji na temat regionów pamięci współdzielonych obiektów znajdziesz w rozdziale „System plików /proc” w tej notce, natomiast na większą ilość informacji o linkerze ld.so trzeba będzie nieco poczekać ;).
AT_ENTRY (9)
Jest to wskaźnik do pierwszej instrukcji, która jest wykonywana w programie. Nie jest to pierwsza instrukcja funkcji main(), ale raczej pierwsza instrukcja kodu startowego, czyli inaczej offset symbolu _start.
AT_UID (11), AT_EUID (12), AT_GID (13), AT_EGID (14),
Jak łatwo rozpoznać po – znajomych zresztą – nazwach, wektory te zawierają informacje o użytkowniku, który uruchomił dany program. Odpowiednio: Real UID, Effective UID, Real GID, Effective GID.
AT_CLKTCK (17)
Zapewne skrót od Clock Tick ;), czyli wartość CLOCKS_PER_SEC.
AT_PLATFORM (15)
Jest to string identyfikujący platformę na której działa program. String ten używany jest przez loader w celu załadowania odpowiednich bibliotek optymalizacyjnych dla danej platformy i jest bardziej w tym celu przystosowany niż dane w /proc/cpuinfo. Dane są zgodne (na dzień dzisiejszy) z tabelą poniżej. Jeśli nie ma w niej Twojej architektury, to znaczy, że Linux po prostu nie używa żadnych optymalizacji podczas ładowania programu wykonywalnego, nie świadczy to więc o wsparciu dla danej architektury lub braku tego wsparcia.
alpha: String stały i jego definicję można odnaleźć w okolicach ciągu ELF_PLATFORM w pliku arch/alpha/include/elf.h.
Dostępne ciągi znaków: „ev4”, „ev5”, „ev56”, „ev6”, „ev67”
arm: String generowany jest dynamicznie przy użyciu dwóch części: nazwy procesora i kolejności bajtów (endianness). Nazwy procesorów można wyłuskać z plików arch/arm/mm/proc-*.S w okolicy etykiety cpu_elf_name, natomiast specyfikator kolejności bajtów do „l” lub „b” oznaczający odpowiednio: little-endian oraz big-endian w okolicy „elf_platform” w arch/arm/kernel/setup.c. Jeśli zainteresowany jesteś lepszym rozeznaniem, który procesor ARM jest którym, odsyłam do Wikipedii. :)
- „v3l”, „v3b”: dla arm6 (armv3), arm7 (armv3),
- „v4l”, „v4b”: dla arm720 (armv4), arm740 (armv4), arm7tdmi (armv4t), arm9tdmi (armv4t), arm920 (armv4t), arm922 (armv4t), arm925 (armv4t), arm940 (armv4t), fa526 (armv4), sa1100 (armv4), sa110 (armv4),
- „v5l”, „v5b”: dla arm926 (armv5tej), arm946 (armv5te), arm1020 (armv5t), arm1020e (armv5te), arm1022 (armv5te), arm1026 (armv5tej), feroceon (armv5te), mohawk (armv5te), xscale (armv5te), xscale3 (armv5te),
- „v6l”, „v6b”: dla armv6,
- „v7l”, „v7b”: dla armv7 (te ARM'y, a konkretnie v7l, można znaleźć np. w telefonach HTC Desire).
mips: „octeon”, lub NULL, jeśli procesor nie jest zgodny.
String jest budowany w arch/mips/kernel/cpu-probe.c w zmiennej __elf_platform.
parisc: „PARISC”
String jest stały, dostępny w arch/parisc/include/asm/elf.h, niedaleko symbolu ELF_PLATFORM.
powerpc: String budowany dynamicznie. Tablica procesorów i ich cech charakterystycznych znajduje się w arch/powerpc/kernel/cpu-table.c. Treść wektora AT_PLATFORM ustawiana jest na to, co znajduje się w polu „platform” danego procesora, czyli:
- „power3” dla Power3 (630, 630+),
- „rs64” dla RS64-II (Northstar), RS64-III (Pulsar), RS64-III (Icestar), RS64-IV (S-star),
- „power4” jest domyślny dla 64bit, jak też i Power4 (gp), Power4+ (gq),
- „ppc970” dla PPC970, PPC970FX, PC970MP,
- „power5” dla Power5 (gr),
- „power5+” dla Power5++, Power5 GS, Power6 w trybie Power5+,
- „power6x” dla Power6 (tryb surowy),
- „power6” dla Power6 (tryb kompatybilny),
- „power7” dla Power7 (tryb surowy i tryb kompatybilny),
- „ppc-cell-be” dla Cell Broadband Engine,
- „pa6t” dla PA Semi PA6T,
- „ppc601” dla PPC 601,
- „ppc603” domyślny 32bit, oraz dla PPC 603, 603e, 603ev, 8240, 8245, 8260, e300c1..c4,
- „ppc604” dla PPC 604, 604e, 604ev, 604r,
- „ppc750” dla PPC 740, 745, 750, 750CX, 750CXe, 750CL, 750FX, 750GX, 755,
- „ppc7400” dla PPC 7400, 7410,
- „ppc7450” dla PPC 7447, 7447A, 7450, 7455, 7457, 7448,
- „ppc823” dla PPC 8xx,
- „ppc403” dla PPC 403GC, 403GCX,
- „ppc405” dla PPC 405GP, 405GPr, 405EP, 405EX, 405EXr, 405EZ, 405LP, STBx25xx, STB03xxx, STB04xxx, NP405L, NP405H, NP4GS3, Virtex-II Pro, Virtex-4 FX,
- „ppc440” dla PPC 440GR, 440GX, 440SP, 440SPe, 440EP, 440EPX, 440GRX, 460EX, 460GT, 460SX,
- „ppc440gp” dla PPC 440GP,
- „ppc470” dla PPC 476,
- „powerpc” dla PPC E500.
s390: Plik arch/s390/kernel/setup.c zawiera procedurę setup_hwcaps(), która precyzuje wartość wektora na jeden z ciągów:
„g5”, „z900”, „z990”, „z9-109”, „z10”.
Wartości te wybierane są na podstawie wyjścia instrukcji STIDP. Po więcej szczegółów odsyłam do pliku setup.c.
x86: „i686”, „x86_64”, w zależności od tego, czy architektura jest 32 czy 64-bitowa.
String jest statyczny, definiowany w arch/x86/include/asm/elf.h, otoczony dyrektywami preprocesora rozróżniającymi 32/64 bity.
AT_HWCAP (16)
Jest to wskazówka dotycząca rozszerzeń i możliwości, jakie wspiera procesor. Zależna jest od platformy i hardware'u. Na architekturze x86 jest to wartość, jaką zwraca instrukcja CPUID, na innych architekturach występują inne instrukcje zwracające te dane. Być może lepiej przeczytać /proc/cpuinfo aby uzyskać te informacje?
AT_RANDOM (25)
Wartość pamięci wskazuje na obszar, który zawiera 16 losowych bajtów. Bajty te są oznaczone (przez kernel developerów) jako bezpieczne kryptograficznie, tzn. mogą być stosowane tam, gdzie należy zapewnić niski stopień przewidywalności i względnie dużą prędkość generowania nowych bajtów. Rozwiązanie polega na zbieraniu danych z załadowanych sterowników urządzeń do puli entropii, która pełni rolę bufora danych. Przy żądaniu losowych bajtów, bufor ten zostaje poddany pod działanie funkcji skrótu (np. SHA), aby ukryć stan entropii w puli źródłowej i zapewnić jej wysoki poziom w zwracanych danych. Dobrym przykładem wykorzystania wektora jest możliwość inicjowania lokalnego generatora liczb losowych (np. rand()) losowymi danymi znajdującymi się w tym wektorze, zamiast oklepanej funkcji time(NULL).
Inne wektory również mogą zawierać ciekawe informacje. Polecam samemu przyjrzeć się funkcji która zajmuje się ich inicjacją. Jest to dobry trening nauki czytania kodu, nauki czytania kodu jądra i nauki kojarzenia faktów z kodu źródłowego :). Interesująca funkcja nazywa się create_elf_tables, znajduje się w pliku fs/binfmt_elf.c, a makro inicjujące kolejne wektory nazywa się NEW_AUX_ENT. Metodą Searcha in Files'a na pewno uda ci się zlokalizować interesujący kod ;).


