Do spisu tresci tematu 4

4.1.1 Segmentacja

Oznaczenia w tekście

[i386] poświęcone wyłącznie procesorom intel z serii 80x86 i zgodnym, od 386 wzwyż.
[@] informacje które mogą być nieaktualne lub są niepewne.

Spis tresci


Co to jest segmentacja

Segmentacja była dawniej podstawowym mechanizmem podziału pamięci na spójne podobszary - bloki kodu, danych, stosu. Pozwalało to na defragmentację, to jest przesuwanie segmentów, aby w miejsce wielu małych wolnych obszarów pojawił się jeden duży spójny. Adresy bowiem przechowywało się względem początku segmentu, który mógł być już płynny. Jednak takie podejście do problemu fragmentacji pamięci jest dosyć czasochłonne i skomplikowane, szczególnie gdy chciałoby się nie używane fragmenty segmentów zachować na dysk w celu uczynienia miejsca na nowe - to jest już w segmentacji dosyć trudne i czasochłonne.

Łatwiej jest myśleć o pamięci w porcjach (stronach), które każdy proces dostaje w miarę potrzeby, zorganizowanych tak, że ze swego punktu widzenia ma do czynienia ze spójnym fragmentem pamięci, o co dba sprzęt, który strony o byle jakiej kolejności fizycznej układa tak, aby miały określony porządek. Dodatkowo uzyskujemy wygodną metodę podziału pamięci na fragmenty, dla których możemy jakoś badać stopień wykorzystania, i odpowiednio zamrażać je na dysku w celu zwolnienia pamięci dla fragmentów często lub aktualnie potrzebnych. Jednak ponieważ podział ma małą rozdzielczość, to wymagany jest tu trochę inny sposób kompilacji lub pisania kodu/projektowania danych - tak by w efekcie zgrupować obszary używane przez dany fragment programu w spójnym bloku (lokalność odwołań).
Jednak metodologia taka nie jest obecnie szeroko praktykowana - może poza projektowaniem standardowych bibliotek procedur.

W tym momencie segmentacja nie jest już mechanizmem defragmentacji, a jedynie ewentulalnie ochrony i dostępu do pewnych obszarów danych. (które to mechanizmy mogłyby być znacznie uproszczone do postaci nie wymagającej odwoływania się do pojęcia segmentu, co ma jednak miejsce w procesorach serii 386/486/PENTIUM/PPRO/?, zapewne ze względów kompatybilnościowo/hist[e]orycznych.)

Jaka jest zatem relacja między segmentacją a stronicowaniem? Z punktu widzenia adresu: segment wpływa na pierwotny adres wirtualny - dodając do niego liniowy adres początku segmentu, który potem obrabia mechanizm stronicowania.


Dlaczego zbędna i nieprzydatna - krytyka segmentacji

Segmentacja jest:

Dlaczego więc w ogóle wymyślono segmentację projektując procesor 286 - chociaż stronicowanie było już wtedy znane i stosowane przez innych producentów procesorów ? Segmentacja, choć mechanizm trudny w programowaniu i wykorzystaniu, umożliwiał przydzielanie pamięci bardzo małymi elementami, a nie tylko stronami, które mają wielkość 4KB w przypadku [i386], przez co umożliwiło wykorzystanie go w systemie z małą ilością pamięci (np. 1MB). Oczywiście kosztem czasu poświęconego na defragmentację pamięci. Dlatego przy dzisiejszych pojemnościach pamięci segmentacja jest znacznie lepszym rozwiązaniem.

Segmentacja jednak, jeżeli już jest, to może się przydać - jako mechanizm ochrony (szczególnie, gdy nie zadbano o inne, lepsze) - globalnie ustala priorytety dla grupy stron, a nie tylko dla konkretnych stron. Jednak to można sprzętowo rozwiązać lepiej, stosując poziom uprzywilejowania np. dla danej tablicy stron, nie używając jeszcze jednego dodatkowego mechanizmu przeliczania adresu.

Generalnie - mechanizmu sprzętowego warto używać tylko wtedy, gdy jest udogodnieniem bądź usprawnieniem. Ani jedno, ani drugie nie ma jednak miejsca, przynajmniej w odniesieniu do [i386].

Osobiście nie polecałbym stosowania czystego Linuxa do zastosowań wymagających bezpieczeństwa, ochrony czy niezawodności bez jego głębokiego poznania. System ten prawidłowo skonfigurowany umożliwia osiągnięcie bardzo dużego poziomu bezpieczeństwa, jednak nie należy liczyć na to, że samo hasło Linux sprawi że twój system będzie bezpieczny. Trwają już prace nad jego rozszerzeniami np. odpowiednie moduły np. do budowania zapór ogniowych (ang. firewall).

Oczywiście ustalmy jedno - DOS czy Window$95 są w tym względzie znacząco z tyłu, a W$ NT wcale nie jest bardziej bezpieczne. Systemy te, pomijając samą kwestię jakości wykonania przez programistów, nie zostały od początku pomyślane jako systemy z mechanizmami zabezpieczeń, tylko jako systemy dla użytkownika prywatnego. Elementy bezpieczeństwa próbuje się wprowadzić metodą serii poprawek jako odpowiedź na zapotrzebowanie rynku oprogramowania systemowego


Co musi lub [może i jest] użyte,dlaczego i jak

Podkreślę na wstępie - Linux w założeniu ma być najprostszą i przejrzystą implementacją Unixa.
Często można spotkać opinię że sam Unix nie jest systemem bezpiecznym! (w porównaniu z DOS-em może i wypada lepiej, ale o tym ostatnim lepiej nie wspominać) Często taka opinia wynika jednak z niepełnej wiedzy o systemie.

Wszystkie standardowe mechanizmy ochrony Unixa można z powodzeniem realizować na poziomie ochrony stron (bez używania pojęcia segmentu), z jednym wyjątkiem: potrzebne jest jeszcze bezpieczne przechodzenie w tryb nadzorcy i z powrotem - podczas wołania funkcji systemowych. Musi być wogóle realizowalny podział system - użytkownik, aby była mowa o bezpieczeństwie - co jednak nie jest konieczne do samej pracy systemu).

Ma to swoje odbicie w Linuxie: segmentacja została tu właściwie zdegenerowana do niezbędnego minimum wymaganego przez dany sprzęt to jest w przypadku [i386]:

Omówię teraz szerzej te elementy, jednak przed przejściem do szczegółów potrzebny jest wstęp i krótka nota historyczna:

Słów kilka o sprzętowej segmentacji oraz krótka nota historyczna:
Tablice deskryptorów segmentów [@][i386]

IDT (interrupt descriptor table) tablica deskryptorów przerwań, szerzej powinna być opisana przy przełączaniu procesów

GDT (global descriptor table) globalna tablica deskryptorów segmentów - segmenty jądra, także innych procesów
[@] Obecnie każdy proces ma swoje segmenty opisane w pewnych polach GDT - wspólnych dla wszystkich procesów, oddzielnie dla danych, oddzielnie dla kodu. W GDT poza tym znajdują się deskryptory segmentów TSS.

Podobnie jak każdy proces dwa segmenty ma też jądro, ale są to dwa inne segmenty w tablicy GDT, jako że jądro ma inne priorytety (poziom 0 w polach DPL), oraz inny obszar przestrzeni adresowej nie nachodzący na obszar procesów - 3GB w górę. GDT[1]:kod jądra, GDT[2]:dane jądra.

Tablica LDT opisana niżej jest zgodnie z nazwą lokalna, co dodatkowo oznacza, że aby zaistniała, musi być wpisana jako segment do tablicy globalnej (pierwotnego segmentu) GDT.

LDT (local/lokalna, reszta jw.) - nota historyczna: W tych tablicach były kiedyś deskryptory segmentów dla każdego procesu oddzielnie, ale ponieważ każdy proces ma zwykle tylko po dwa segmenty, i to zawsze takie same, to nie ma po co robić wielu identycznych segmentów na tablice LDT, z których każda wyglądała tak samo: LDT[0]:null, LDT[1]:kod, LDT[2]:dane/stos

O rozmiarze i obszarze dostępnej pamięci decyduje zatem ilość i miejsce przydzielonych stron.

dokładny opis deskryptora segmentu oraz pozostałe segmenty czyli to co znajduje się w tablicach GDT i ew. w LDT (tłumaczenie z KHG):


Deskryptory segmentów

Oto deskryptor segmentu opisujący każdy segment w systemie. Są deskryptory regularne i systemowe. Dziwny format wynika z zachowania zgodności z 286. Rozmiar: 8 bajtów (64 bity).

63-545554535251-48 47464544-4039-1615-0
Base
31-24
GDRU Limit
19-16
PDPL STYPE Segment Base
23-0
Segment Limit
15-0

Wyjaśnienie:

R zastrzeżone (0)
DPL 0 znaczy jądro, 3 znaczy użytkownik
G 1 to 4K rozdzielczość (Zawsze ustawione w Linux-ie)
D 1 oznacza odwołania 32-bitowe
U definiowalne przez programistę
P 1 - obecny w pamięci fizycznej
S 0 - segment systemowy , 1 - normalny segment kodu lub danych.
TypeWiele różnych możliwości, znaczenie zależy też od wartości S

deskryptory systemowe w Linux-ie :
TSS: P=1, DPL=0, S=0, type=9, limit = 231, miejsce dla 1 tss_struct (patrz TSS).
LDT: P=1, DPL=0, S=0, type=2, limit = 23, miejsce dla 3 deskryptorów segmentów.
Pole base jest ustawiane podczas fork(). Dla każdego procesu Tworzone są TSS i LDT (obecnie LDT już nie, poza szczególnymi programami).

deskryptory regularne/standardowe Linux-a dla jądra: (head.S)
kod: P=1, DPL=0, S=1, G=1, D=1, type=a, base=0xc0000000, limit=0x3ffff
dane: P=1, DPL=0, S=1, G=1, D=1, type=2, base=0xc0000000, limit=0x3ffff

[@]Obecnie poniższe deskryptory są w GDT:

LDT dla task[0] (proces 0?) zawiera: (sched.h)
code: P=1, DPL=3, S=1, G=1, D=1, type=a, base=0xc0000000, limit=0x9f
data: P=1, DPL=3, S=1, G=1, D=1, type=2, base=0xc0000000, limit=0x9f

Domyślne LDT dla pozostałych procesów: (exec())
code: P=1, DPL=3, S=1, G=1, D=1, type=a, base=0, limit= 0xbffff
data: P=1, DPL=3, S=1, G=1, D=1, type=2, base=0, limit= 0xbffff

Z całą segmentacją związane są wydzielone rejestry IDTR, GDTR, LDTR i TR, jednak o nich nie będę pisał

Być może dla procesów, których exec ma ustawioną flagę SetUID, a właściciel jest root-em DPL=0, choć raczej nie - uprawnienia root-a to trochę co innego niż tryb pracy nadzorcy/jądra(ale to już jest działka chłopców od procesów).

Podsumowując - liniowy adres nie jest właściwie modyfikowany przez mechanizm segmentujący.


W praktyce - mapy pamięci (tłumaczone z KHG) [i386]?

Przed rozpoczęciem pracy - pamięć fizyczna

Mapa pamięci fizycznej przed rozpoczęciem jakichkolwiek procesów. Kolumna po lewej podaje adres startowy obszaru, adresy pisane pochyłą czcionką są przybliżone. Środkowa kolumna to nazwa obszaru(ów). Po prawej są podane odpowiednie procedury, zmienne lub objaśnienie roli obszaru.

0x110000WOLNE memory_end or high_memory
mem_map mem_init()
inode_table inode_init()
device data device_init()*
0x100000 dalsze pg_tables paging_init()
0x0A0000 ZAREZERWOWANE
0x060000WOLNE
low_memory_start
0x006000 kod i dane jądra
floppy_track_buffer
bad_pg_table
bad_page
używane przez page_fault_handler-y do eleganckiego zabijania procesów gdy brak pamięci.
0x002000 pg0Pierwsza tablica stron jądra.
0x001000 swapper_pg_dirKatalog (tablic?) stron jądra.
0x000000 strona null(zerowa)

*inicjacje urządzeń żądające pamięci to(main.c): profil_buffer, con_init, psaux_init, rd_init, scsi_dev_init.

Obszary nie zaznaczone jako WOLNE są ZAREZERWOWANE. (mem_init). ZAREZERWOWANE strony przynależą do jądra i nie są nigdy zwalniane lub zrzucane(swapowane).


Kolejny proces - jak widzi pamięć

0xc0000000 Niewidzialne jądrozarezerwowane
inicjalny stos
miejsce na rozrost stosu4 strony
0x60000000 biblioteki dzielone
brk nie użyte
pamięć malloc-a
end_data niezainicjalizowane dane
end_code zainicjalizowane dane
0x00000000 tekst

Zarówno segment kodu jak i danych rozciągają się od 0x00 do 3 GB.


Stos procesu jest jak widać po drugiej stronie w segmencie danych - poniżej kodu jądra (od 0x C000 0000 w dół wirtualnie).

Ochrona w ramach całego obszaru segmentu jest realizowana przez mechanizm stronicowania. Sam segment chroni tylko przed odwołaniem poza granicę 3Gb.


Zamrożony stan procesu (TSS - Task State Segment) [i386]

TSS (task swap(lub state) segment) - przełączanie procesów odbywa to się przez (jawny?) skok do danego TSS.
Nie wykonuje się! wtedy zawarty tam kod (bo tak zawsze można interpretować zawartość pamięci, np. te zamrożone dane o procesie właśnie w TSS), tylko makroprocedura procesora zachowuje w bieżącym TSS aktualny stan procesora , a nowy odtwarza z danych, do których skoczył, w szczególności podstawowy dla stronicowania rejestr CR3 czyli adres katalogu stron, oraz adres lokalnej tablicy deskryptorów segmentów LDT, który obecnie z reguły nie jest używany. Czas tej operacji wg. intel military i486 family spec., str. 87, linia 2 wynosi 10 us dla procesora i486DX33 - nie jest to mało w stosunku do czasu najprostszej, 1-taktowej operacji: dla 33 MHz to jest 30ns, (około 300 takich operacji można wykonać zatem nie należy przełączania robić częściej, niż jest to potrzebne. Jeśli się nie mylę, to w Linux-ie częstotliwość przełączania jest rzędu 10ms, to jest 1000x więcej niż 10us, zatem straty powodowane przełączaniem to około 10us+epsilon/10ms , a więc rzędu promili (0,1%), co jest do zaakceptowania. Pomijam tu jednak milczeniem pewną delikatną kwestię wydajnościową zwiazaną z pamięcią cache.

Deskryptory TSS kolejnych procesów znajdują się obecnie w tablicy GDT

Segment kodu i danych poszczególnych procesów [i386][i386]

W tablicy GDT są to odpowiednio deskryptory o dwóch kolejnych indeksach (zapewne 4 i 5, choć to ulega zmianom), wspólne dla wszystkich procesów - bowiem to, co się naprawdę zmienia, to odpowiedni rejestr (CR3) pamiętany w TSS pokazujący adres tablicy stron (a dokładniej katalogu stron, jak dowiemy się przy opisie stronicowania).

Na temat szczegolow patrz pod Sprzetowe deskryptory.


To samo dla jądra [i386][i386]

Jak wyzej - tez dwa deksryptory, z tym ze inne indeksy w tablicy. Nie jest to tez tak ciekawe - jadro jest jedno - zatem nie dziwne, ze ma po jednym segmencie - danych i kodu.

Na temat szczegolow patrz pod Sprzetowe deskryptory.


Jak wędrować między jądrem a procesem [i386]

Skok do konkretnej procedury systemowej


3. Współdzielenie

Odbywa się raczej na poziomie stronicowania, oraz programowo - przez vm_area_struct (zarządzanie blokami SHM i wyzn. spójnych obszarów pamięci).


Uniwersalny zamiennik programowy segmentacji: VM_AREA_STRUCT

Jest to drzewo spójnych obszarów w pamięci wirtualnej, standardowo około 6, jednak łatwo może ich się zrobić dużo, zatem zastosowano wydajną strukturę danych.
Jest to bowiem drzewo avl, służy głównie do wyznaczania wolnych spójnych obszarów podczas tworzenia nowych segmentów(niesprzętowych) pamięci dzielonej. Ogólnie jest zorganizowane jako drzewo avl ze względu na początkowy adres. Wszystkie operacje dostępu/reorganizacji znajdują się w pliku mmap.c.

Także pewne operacje w ramach różnych bloków wykonują się inaczej (nie ma na przykład odczytu z wyprzedzeniem do obszarów współdzielonych), co jest także kontrolowane przez tą strukturę.
Jest to zorganizowane tak, że każdy blok vm_area_struct tego drzewa ma listę operacji, różnie wyglądającą w zależności od typu bloku.

Wszelkie operacje rezerwacji/zwalniania odbywają się kaskadowo właśnie od vm_area_struct poprzez katalogi tablic/tablice (których elementy pokazują na fizyczne strony). Organizacja stronicowania jest opisana tu. Nie ma tu podziału na sprzętowe segmenty.


Podsumowanie - czyli jeszcze raz najistotniejsze wnioski i spostrzeżenia

Jeszcze raz o bezpieczenstwie - ograniczenia wynikające z ochrony na poziomie stron: - tylko zakaz zapisu, nie można zakazać wykonania ani odczytu z danej strony. (Zatem można napisać program, który przeczytałby sam siebie, pomimo, że byłby nie do odczytania - warunek - trzeba w nim zawrzeć własny kod, który to zrealizuje - dlatego piszę NAPISAĆ, zatem raczej nieprzydatne do włamań... (a i tak nie za bardzo))

Rejestr CR3 po wykonaniu przełączenia zadań zostaje wczytany z danego segmentu TSS, w tym rejestrze znajduje się właśnie adres katalogu tablicy stron danego procesu. deskryptory segmentow TSS sa w tablicy GDT (kazdy proces oddzielnie).
W tablicy GDT sa jeszcze 4 istotne deksryptory - dwa dla jadra (kod,dane), dwa wspolne dla wszystkich procesow (kod dane).
W GDT moga sie znalezc jescze nadprogramowo w wyjatkowych sytuacjach wpisy deskrytpory segmentow przeznaczonych na LDT czyli lokalna tablice deksryptorow, w przypadku programow typu WINE (emulator W3.11/W95) czy DOSEMU (wykonawca DOS).

Wlasciwa obrobka pamieci procesow najczęściej zaczyna się dopiero na poziomie stronicowania (poza np. Wine/DOSemu - jw.) - procesy mają bowiem różne katalogi tablic/tablice/strony fizyczne, być może częsciowo wspólne (kod lub SHM). Jednak to jest poza mechanizmem segmentacji (twórcy linuxa, jak już zaznaczyłem, pominęli milczeniem rozbudowane możliwości ochrony/rekalkulacji adresu na poziomie segmentacji, a zatem i ja pomijam je w swym opisie.)


Bibliografia

Pliki źrodłowe: Kernel Hacker's Guide - Linux Memory Management
intel military i486 family spec.

Pytania i odpowiedzi

Co, gdy ten sam adres - kodu i danych?

Takiej sytuacji chyba nie ma. Zgodnie z KHG - wirtualne wartości adresów kodu i danych są rozłączne.
Kod, to domyślnie segment (selektor) z rejestru CS, a dane DS, zatem mogą być inne, choć w praktyce chyba te same.


Autor: Jarosław Ślebocki