Anatomia pamięci w systemach (kernel, stack, heap, BSS, data, text ) - pytanie do speców od C/assemblera

1

Witam !

Próbuję zrozumieć anatomię pamięci w systemach ale wszystkie opisy jakie zawierają przykłady dla programistów (i wszystkie są w C). Ja nigdy nie byłem programistą i te przykłady nie są dla mnie do końca jasne. Wszędzie jest opis, że jak aplikacja jest ładowana do pamięci to jest tworzona struktura mapowania stron wirtualnej pamięci i fizycznej pamięci.
Odpuszczam już sobie kwestię fizyczna-->wirtualna (zdaję sobie sprawę, że jest to potrzebne by program miał jednolitą/ciągłą przestrzeń adresową).

Chciałbym się skupić na strukturze - i może jakimś prostym przykładzie jak notepad.exe czy nano :

Kernel
Stack
Heap
BSS
Data
Text.

Wiem, że do sekcji Kernel użytkownik nie powinien się odwołać w żaden sposób (ani czytać ani pisać) - ale nie wiem czy to znaczy, że tam jest załadowane całe jądro OSa ??? Czy tam są też wszystkie biblioteki systemowe etc ???? Czy to się powiela dla każdej aplikacji ładowanej do pamięci ??? Czy tam znajdują się wszystkie polecenie systemowe (np.: dir, ls, whoami, etc.)
Po co ta przestrzeń aplikacji użytkownika ???
Co z tej części jest potrzebne windowsowemu notatnikowy czy nano spod linuksa ?

Tym razem od dołu - Text.
Wiem, że tu ładowany jest kod aplikacji czyli wykonywalne instrukcje dla procesora co ma robić. Ale w przykładach jakie znalazłem były podane polecenia np.: /bin/ls. Przecież to jest część systemu więc nie rozumiem, czemu to się nie zalicza do sekcji Kernel.
Rozumiem, że z mojego przykładu tu znajdzie się notatnik czy nano - ale czy w całej okazałości czy tylko jakaś część ???

Data - segment przewidziany dla "statycznych zainicjowanych danych".
Czytałem, że tutaj znajdują się globalnie zdefiniowane wartości - czyli (jeśli dobrze zrozumiałem) tutaj znalazłyby się zdefiniowane napisy/opcje z programu notatnik np.: "Plik", "Zapisz jako", "Zamień" etc.
Czy tu też mogłyby się znajdować domyślne wartości jak np. szerokość czy wysokość okna edycji ???

BSS - "segment niezainicjalizowanych danych", które na początku nie mają wartości (wypełnione są zerami)
Z tego co zrozumiałem to to tu znajdowałaby się np. definicja zmiennej całkowitej i gdzie nie podajemy wartości (przykłady jakie znalazłem to: static int i; )
Pytanie - co tutaj z takiego notatnika czy nano mogłoby się znaleźć ????

Stack - przestrzeń dla zmiennych lokalnych z punktu widzenia funkcji.
Jest tu podawany również jakiś "return adress" ??? - ale powrót do czego ??? po co jakiś return adress ???
Np. co w takim notatniku może być "return adrese" ???
Co z takiego notatnika znajdzie się w sekcji stak / stos ????

Heap - segment z dynamiczną alokacją pamięci.
Tu znalazłem opisy, że ładowane są współdzielone biblioteki i moduły potrzebne procesowi.
Co z takiego notatnika się tutaj znajdzie ???

No i na koniec - jak użytkownik wpisuje coś do notatnika - to w której części zostają wprowadzone te dane ??? Do stack ???

Z góry wielkie dzięki za podpowiedzi
pozdrawiam
Dominik.

5

Boi, that's gonna be long.

Będę mówić przede wszystkim o Linuksie, bo na tym się znam bardziej.

  1. ls, ani inne takie nie są częścią jądra systemu operacyjnego. Są to zwyczajne programy, takie jak nano, które uruchamiasz jako użytkownik. Co innego cd, które nie jest osobnym programem, ale również nie jest częścią systemu operacyjnego, jest tylko tak zwanym builtinem powłoki. Krótko mówiąc, istnieje sobie program zwany powłoką (zazwyczaj bash), który sobie interpretuje polecenia, i zazwyczaj służy do uruchamiania programów. On również nie jest częścią systemu operacyjnego. Aby uruchomić basha, (na typowym systemie) musisz uruchomić plik /bin/bash (ew. /usr/bin/bash, w zależności od systemu).
    Dlatego też jak najbardziej jest sens mówić o sekcji text programu /bin/ls, gdyż to program taki jak każdy inny.

  2. Kernel ma swoją przestrzeń adresową, i ona jest pod niedostępnymi dla programów przestrzeni użytkownika adresami. To że są widoczne pod jakimś adresem, bynajmniej nie znaczy, że każdy proces przestrzeni użytkownika posiada ich własną kopię, wręcz przeciwnie, żaden z nich nie posiada żadnej kopii.

  3. Nie ma czegoś takiego jak biblioteki systemowe (może na Windowsie są). Zwyczajowo z systemem operacyjnym przychodzi biblioteka C. Wtedy, program sobie korzysta z biblioteki C, ale to korzystanie niczym się nie różni od korzystania z dowolnej innej biblioteki (linkowanej w ten sam sposób). Tekst biblioteki C, jeśli była zlinkowana dynamicznie, jest w pamięci trzymany tylko raz, i jest zmapowany pod jakiś adres, w każdym procesie, który jest z nią zlinkowany, a linker zdecydował się tę bibliotekę załadować.

Co do reszty to nie wiem, czy potrafię wyjaśnić szczegóły, jak sam mówisz, że nie masz doświadczenia programistycznego. Wszystkie przykłady by były znacznie bardziej zrozumiałe, gdybyś znał ich zastosowanie :(

0

@enedil: Dziękuję.

3

Będzie cieżko...

  1. Kernel space to jest kod związany np. z interakcją ze sprzętem. Jak chcesz odczytać dane z dysku twardego to gdzieś musisz mieć kod który faktycznie robi interakcje na niskim poziomie z pinami na płycie głównej. Podobnie wyświetlenie czegoś na ekranie znów wymaga takiej niskopoziomowej interakcji z kartą graficzną itd. Zwykle z poziomu userlandowego kodu interakcje z kernelem są przykryte przez syscalle.
  2. .text to segment kodu twojej aplikacji, czyli to co napisał programista. ls czy jakikolwiek inny program to jest zwykły program i nie ma znaczenia czy przychodzi razem z OSem czy nie.
  3. .data Może, chociaż takie duperele jak parametry okienka częściej lecą do jakichś resources. Ale jeśli masz np. prosty program Hello world to taki string pewnie poleci właśnie do tego segmentu.
  4. .bss to będą jakieś zmienne globalne w kodzie, np. twój notatnik może mieć otwarty tylko jeden plik na raz i ścieżkę aktualnego pliku trzymasz w zmiennej globalnej
  5. Żeby zrozumieć co to jest adres powrotu trzeba najpierw zrozumieć jak działa wykonanie kodu na poziomie asemblera/CPU. CPU trzyma sobie "adres instrukcji" w postaci jednej liczby w rejestrze RIP. Jakiekolwiek skoki modyfikują ten rejestr. Jeśli np. twój program woła jakąś funkcje to wykonujemy taki skok, ale jak funkcja się kończy to chcemy wrócić tam skąd przyszliśmy. Zauważ ze funkcja którą wywołujemy nie wie nic o tym skąd przyszliśmy. Konwencja wywołań zakłada że przed skokiem na stosie odkładamy sobie informacji o tym skąd przychodzimy (czyli stan RIP) i jak funkcja się kończy to zdejmujemy ze stosu tą wartość i skaczemy.
  6. Tutaj jest ładowane wszystko co jest alokowane dynamicznie przez jakieś malloc. Jeśli np. otwierasz notatnikiem plik i ładujesz jego zawartość, to pewnie robisz to właśnie w ten sposób, bo przecież nie wiesz jak duży będzie ten plik zawczasu. W trakcie ładowania sprawdzasz rozmiar pliku, alokujesz pamięć i wczytujesz.

Próbuję zrozumieć anatomię pamięci w systemach

Bez jakiegoś doświadczenia z programowaniem i z asemblerem to możesz sobie odpuścić.

0

@Shalom: Wielkie dzięki.

0

@Shalom: Najlepsze wytłumaczenie jaki wygooglowałem jest tutaj : ale nie było rozbicia na BSS i Data oraz kernel. Zależało mi na wyjaśnieniu bardziej koncepcyjnym (high / helicopter overview) niż odwoływaniu się do samego kodu. Z waszą pomocą już jest lepiej ;-) Jeszcze raz wielkie dzięki.

5

Przede wszystkim te wszystkie sekcje to jest bardzo arbitralna sprawa, jak będą nazwane, technicznie rzecz biorąc jest tak, że każda sekcja jaką możesz sobie wypisać przy pomocy objdump (lub analogicznym ma windzie) ma prawa do odczytu, zapisu i (choć nie zawsze jest to wspierane) uruchamiania. Większość sekcji jest wczytywana z pliku wykonywalnego, ale np. .bss nie jest, po prostu jest podany rozmiar i program ładujący wypełni go zerami — w tej sekcji przeważnie będą zaalokowane zmienne globalne.

Przeważnie mamy sekcje .data i .rodata na, odpowiednio, zmienne globalne, które mają przypisaną wartość przy deklaracji i stałe. .data będzie do odczytu i zapisu, a .rodata tylko do oczytu. Nie da się łatwo stwierdzić gdzie będą przechowywane konkretne dane, bo to już zalezy od implementacji programu, często, poszczególne dane mogą się znajdować w kilku miejscach na raz. Jeśli chodzi o rzeczy wyświetlane w okienkach, one raczej będą znajdować się w pamięci obsługującej tryb okienkowy (WinApi, Xlib oraz korzystające z nich GTK, QT, itp.). Te biblioteki są raczej ładowane dynamiczne i mają osobny komplet takich samych sekcji. Gdzie będą trzymać takie rzeczy? Prawdopodobnie na stercie (heap), bo biblioteka nie wie jakie okienka będzie produkować, w jakiej ilości, więc raczej będzie to wszystko alokowane dynamicznie. Oczywiście wcale nie musi, często też będzie tak, że główny kod programu będzie mieć swoją własną kopię tych danych i tu prędzej wyobrażam sobie, żeby była na stosie lub w sekcji .data. Też przeważnie te same dane (choć już w formie rastrowej), oczywiście są też zapisane w jakimś buforze ramki, obszarze pamięci, z którego karta graficzna sczytuje bezpośrednio to co ma wyświetlać. Nad takim burorem, zwykle trzyma pieczę system operacyjny i albo pozwala go zmapować, albo daje pośredni dostęp.

Sekcja .text (albo .code na windzie) przeważnie jest od odczytu i wykonywania i tam przeważnie główny kod wykonywalny. Piszę, glowny, bo (przynajmniej na linuksie) jest przeważnie jeszcze .init i .plt. Ta pierwsza zawiera bezwzględny początek programu, który wywołuję inicjalizację biblioteki standardowej, itp. .plt zawiera kod wykonywany jako pośrednik w wywoływaniu funkcji z bibliotek linkowanych dynamicznie.

Mogą też być sekcje pliku wykonywalnego, które nie są ładowane do pamięci przez program ładujący, np. .comment. Taka sekcja może zawierać dodatkowe informacje, symbole do debugowania, itp. Natomiast, jeśli chodzi o kernel, heap i stack, to węszę tu pomieszanie pojęć, bo pozostałe są sekcjami pliku wykonywalnego (np. ELF, PE), podzielone ze względu na funkcje i uprawnienia (raczej unikałbym określenia segment, bo to jest pojęcie na poziomie sprzętu i może się różnić od przyjemnych logicznych pojęć, jakimi się tutaj posługujemy). Natomiast te dwa są obszarami pamięci (przynajmniej na Linksach, nie jestem pewny co do Windy, ale chyba też żadne z nich nie figurują jako takie), tworzone tu i ówdzie. Stos, wydaje mi się, jest alokowany przez program ładujący. Sterta (heap), wydaje mi się jest ładowana przez bibliotekę standardową. Oczywiście to może zależeć od systemu operacyjnego, ja raczej piszę o Linuksach, bo na Windowsie może być inaczej, na Macu i *BSD powinno być podobnie jak na Linuksie.

I teraz wreszcie kernel, czyli jądro systemu operacyjnego. Jądro, a nie system operacyjny. Jądro zawiera tylko funkcje wymagające uprzywilejowanych instrukcji, a zatem będzie tam się znajdować obsługa sprzętu, zarządzanie pamięcią i procesami, komunikcaja międzyprocesowa, itp. Zaś program inicjujący (init lub systemd na Linuksie), powłoka i podstawowe komendy zaimplementowane poza powłoką (jak np. ls), takimi programami nie są. O ile się orientuję, kod jądra znajduje się w ogóle w odrębnej przestrzeni pamięci, to jest związane z pamięcią wirtualną. Jądro, dla każdego procesu mapuje na pamięć wirtualną tylko te obszary pamięci, które są potrzebne. Są specjalne instrukcje procesora (int, syscall), które załatwiają przełączenie mapowania pamięci i podniesienia praw i one są używane do komunikacji z jądrem (tu, znów nie jestem pewny, jak to jest na windzie, wydaje mi się, że podobnie). Zwykle jednak, biblioteka standardowa zawiera funkcje pośredniczące w takich działaniach, żeby łatwo było je wołać jak zwykłe funkcje.

0

@elwis: Wielkie dzięki.

0

W zasadzie są tylko trzy segmenty: data, stos i kod.

BSS? nawet nie wiem do czego mogłoby to służyć.
Dane niezainicjowane i tak są w segmencie danych... być może komuś chodziło o próbę alternatywnej alokacji tych danych, ale to raczej nie udało się. :)

Heap - to jest zwyczajny ram, dowolnego typu, który jest przydzielany dopiero na żądanie.

np.:
double *p = new [1000000]; // 8 MB

takie coś pójdzie faktycznie do systemu, który przydzieli ten ram.

Stos:
gdy wchodzisz w kawałek kodu (funkcję), wtedy są tam zwykle tzw. zmienne robocze, tymczasowe.

Takie funkcje są wywoływane za pomocą skoków w kodzie... ze śladem = adres powrotny z tego skoku do punktu wywołania.

np.:

double f;
f = arctan(x,y); // tu jest skok pod adres _arctan (jakiś numer w kodzie, co wygląda np. tak: 5674764);

// w tym arctan jest powrót - return, i lądujemy tu:

if( f > M_PI ) f -= M_PI; // ciąg dalszy działania...

........

w ramach funkcji, coś w stylu:

int a[1000000], tab[6668];

oznacza zmienne robocze - na stosie, co jest w tym przypadku raczej błędne,
bo stos ma raczej niewielkie rozmiary, więc zbyt duże zmienne mogą go rozwalić.

int *a = new [1000000]; // tak należy to robić, czyli z tej sterty
sama zmienna: a - jako adres do danych, leży na stosie, ale nie dane w postaci: 1000000 ints = 4MB dla int32.

Jeszcze segment kodu:
to jest też segment 'danych', tyle że z instrukcjami dla procesora.

Nie ma zasadniczej różnicy pomiędzy kodem a stertą, danymi, można zrobić np. tak:

byte *code = new [1000];

teraz wpisać kod pod ten adres, znaczy w code[],
no i wywołać to - uruchomić:

copy(code, mycode);

code(); // jump w code... hihi!

no, ale to już wyższa szkoła jazdy - dla hakerów i wirusomanów.

0

Uwagi odnośnie mitologii pamięciowej, którą tu niektórzy usiłują wciskać.

żadne programy oparte na plikach, bazy danych, nie potrzebują w ogóle alokować specjalnych buforów do trzymania danych,
bo takie coś załatwia z marszu sam system:

File Cache + FileMapping.

Np. aby przetwarzać dane o rozmiarze 10, 100, czy 1000MB, np. obrazki, filmy, lub cokolwiek,
wcale tego nie musimy ładować do aplikacji, bo wystarczy tak zrobić:

hfile = openfile(...);
CreateFileMapping;
MapViewOfFile(...      
itd.

ostatecznie w ogóle nic tu czytam - nie ładujemy,
otrzymujemy jedynie adres do danych, np.: byte *ptr,
który traktujemy jak zwyczajną tablicę,

x = ptr[5000];
memcpy(s, ptr+456, 1024);

itd.

nie potrzeba tu w ogóle żadnego ładowania, ani alokowania gigantycznych tablic,
bo system sam to robi - dla nas, od tego jest!

1

@kwalifika ano genialny pomysł, szkoda że będzie kilka rzędów wielkości wolniejsze niż wrzucenie konkrentych danych, o których wiemy że będą często potrzebne, do pamięci. System nic sam nie zrobi bo zwykle taki cache jest podyktowany uwarunkowaniami biznesowymi o których system nie ma pojęcia. System to może ci najwyżej coś wrzucić w jakieś LRU, jeśli w ogóle.
Chyba że to jakiś twój własny system operacyjny, który idzie w tandemie z twoją magiczną klawiaturą i potrafi wykminić sobie wymagania biznesowe xD

0

Tradycyjnie żartujesz sobie.

Ja nawet znam implementację tego sposobu dostępu do baz danych, który jest chyba obecnie dopiero... wdrażany,
bo jest rewelacyjny i bezkonkurencyjny - chyba z 1000 razy szybszy od tradycyjnych metod w warunkach multidostępu!

Ponadto: ja od dawna tak czytam wszelkie dane - pliki, np. obrazki, teksty, itp., np.:

MapFile map(image.jpg/bmp/txt/gif...);

byte *ptr = map.GetPtr();

dekode/display(ptr);

i tyle z tym roboty: potem przetwarzam tę tablicę, zamiast się męczyć z czytaniem plików,
za pomocą ReadFile, czy innym, co spowalnia z 10-100 razy przetwarzanie...
no i dubluje dane, marnuje ram, bo sys i tak już to załadował do ram - optymalnie!

2

Tak dla sprawdzenia - jakby ktoś chciał zobaczyć co rzeczywiście robi system (Windows) - polecam spojrzeć pod ten adres:
https://docs.microsoft.com/en-us/sysinternals/downloads/vmmap

Jest tam podany odnośnik do małego narzędziowego programu.
Odpalamy ten program, wskazujemy proces, który chcemy obserwować i możemy sobie popatrzeć jak to wygląda w praktyce.

1 użytkowników online, w tym zalogowanych: 0, gości: 1