Czy zastanawialiście się kiedyś…
Na ostatnie pytanie odpowiemy od razu, a resztę wątpliwości wyjaśni poniższy scenariusz do dzisiejszych zajęć.
Booting to sekwencja operacji, które doprowadzają do pełnego uruchomienia komputera i załadowania systemu operacyjnego do pamięci operacyjnej. Samo pojęcie jest skrótem od angielskiego słowa bootstrapping. Nawiązuje ono do wyrażenia: pull oneself over a fence by one's bootstraps, które przenośnie oznacza samodoskonalenie się bez pomocy z zewnątrz. Ukryte znaczenie tego słowa opisuje więc świetnie ideę uruchamiania komputera: najpierw wykonywane są proste, niskopoziomowe programy, które ładują coraz to bardziej skomplikowany i abstrakcyjny kod – komputer doskonali sam siebie.
Tuż po włączeniu zasilania płyta główna uruchamia wbudowane oprogramowanie (firmware) oraz inicjuje działanie jednego z procesorów (BSP – bootstrap procesor). Wybrany procesor będzie wykonywał instrukcje potrzebne do pełnego uruchomienia systemu, a sposób jego funkcjonowania jest kluczowy dla całego procesu. Z tego właśnie powodu wsteczna kompatybilność procesorów produkowanych przez firmę Intel uczyniła bootstrapping dość skomplikowaną operacją.
Procesory Intela (oraz innych firm produkujących procesory o tej samej architekturze) zachowują wsteczną kompatybilność z modelem 8086 z 1978 roku, o 16-bitowej architekturze i 1 MiB przestrzeni adresów fizycznych. To oznacza, że współczesne procesory są w stanie obsługiwać oprogramowanie sprzed czterech dekad! Aby zrealizować ten cel, procesor, zaczynając swoją pracę, emuluje dawne urządzenie i nakłada na siebie sztuczne ograniczenia (tak – wszystko po to, aby móc uruchomić np. 86-DOS-a). Ten specyficzny tryb funkcjonowania procesora nazywamy trybem rzeczywistym (ang. real mode).
Na początku tryb rzeczywisty był jedynym trybem procesora i systemy operacyjne, zaprojektowane pod kątem ówczesnego sprzętu o względnie słabych parametrach jak na obecne wymagania, korzystały z tego trybu. Współczesne systemy działają w trybie chronionym (ang. protected mode), który pozwala na pełne wykorzystanie mechanizmów i zasobów aktualnie oferowanych przez sprzęt. Określenie „tryb chroniony” nawiązuje do jednej z najważniejszych cech, która odróżnia ten tryb od trybu rzeczywistego: ochrona pamięci (w trybie chronionym proces może odwoływać się tylko do pamięci, która została mu przydzielona).
Poza brakiem ochrony pamięci, wadami trybu rzeczywistego są:
Dla programisty szczególnie bolesny w trybie rzeczywistym może być sposób adresowania pamięci. Wszystko dlatego, że inżynierowie Intela zaprojektowali architekturę procesora 8086 z myślą o 1 MiB przestrzeni adresów (220 adresów) i jednocześnie możliwości automatycznego przenoszenia na tę architekturę kodu 8-bitowych procesorów 8080/8085 (o 16-bitowych adresach), a jednocześnie uznali 20-bitowe rejestry za niepraktyczne. W efekcie 16-bitowy rejestr Intela 8086 mógł zaadresować tylko 64 KiB pamięci. Rozwiązaniem, które zaimplementowano z myślą o możliwości adresowania całego 1 MiB, była segmentacja pamięci.
Wspierając segmentację pamięci, procesor dysponuje dodatkowymi rejestrami – rejestrami segmentowymi:
rejestr | przeznaczenie |
---|---|
cs |
code segment
wskazuje na aktualnie wykonywany segment kodu
jest używany razem z rejestrem ip , aby wskazać adres kolejnej instrukcji do wykonania
|
ds |
data segment wskazuje na aktualny segment z danymi, zwykle zawierający zmienne globalne i statyczne aktualnie wykonywanego programu |
es |
extra segment wskazuje na pomocniczy segment danych, służy głównie do przenoszenia danych pomiędzy różnymi segmentami, ułatwia implementację, gdy program potrzebuje więcej niż 64 KiB pamięci na dane |
ss |
stack segment
wskazuje na segment zawierający stos aktualnie wykonywanego programu
wierzchołek stosu znajduje się pod adresem ss:sp
|
fs | general purpose segment dodatkowy segment danych wprowadzony od procesora 80386 |
gs | general purpose segment dodatkowy segment danych wprowadzony od procesora 80386 |
ds
) oraz rejestru z przesunięciem (ang. offset), (np. bx
). Adresy są zapisywane w postaci segment:offset, np. 12ab:34cd, a ich wartość jest obliczana zgodnie ze wzorem
address = segment * 16 + offset.
W naszym przykładzie 12ab:34cd oznacza więc adres
0x12ab0 + 0x34cd = 0x15f7d.
Zwróćmy uwagę, że do danego segmentu można maksymalnie przypisać 64 KiB adresów oraz że jeden adres fizyczny może być reprezentowany przez różne adresy postaci segment:offset. Na przykład, adres fizyczny 0x210 da się wyrazić jako 0020:0010, 0000:0210, 001b:0060 itd.
Pisząc kod, nie trzeba wskazywać segmentów jawnie – procesor domyśla się (czasem niesłusznie), do którego segmentu chcemy się odwołać:
mov [si], ax ; procesor zapisze wartość z rejestru ax pod adresem ds:si mov es:[si], ax ; procesor zapisze wartość z rejestru ax pod adresem es:si mov [bp + 0], ax ; procesor zapisze wartość z rejestru ax pod adresem ss:bp
Wystarczy więc, że na początku programu ustawimy odpowiednie wartości segmentów, a później będziemy w świadomy sposób korzystać z instrukcji asemblera.
Skoro wiemy już, jak zaadresować 1 MiB – czy to oznacza, że boot loader w trybie rzeczywistym może skorzystać z takiej ilości RAM-u? Niestety. We wczesnych systemach (i w fazie bootowania) jedynie 640 KiB adresów odnosi się do pamięci operacyjnej. Pozostałe adresy są mapowane na urządzenia lub inne rodzaje pamięci, m.in. pamięć nieulotną, która (inaczej niż RAM) zachowuje zawartość bez potrzeby ciągłego zasilania i w której znajduje się firmware (tak naprawdę kod firmware, ze względu na czas dostępu, jest po włączeniu zasilania kopiowany do RAM-u).
Pierwszym programem, który procesor rozpocznie wykonywać po uruchomieniu w trybie rzeczywistym, jest wbudowany na płycie głównej firmware, czyli BIOS (ang. Basic Input/Output System). Narzuca się pytanie: w jaki sposób procesor dowiaduje się, że powinien zacząć pracę właśnie od instrukcji BIOS-u? Uruchomienie właściwego kodu jest możliwe dzięki następującym rozwiązaniom (patrz też Minimal Intel Architecture Boot Loader):
cs
+ip
;cs
oraz ip
są ustalane po włączenia zasilania procesora lub po jego wyzerowaniu następująco: rejestr ip
zawiera wartość 0xfff0, a rejestr cs
w części widocznej dla programisty (selektor) – wartość 0xf000, w części niewidocznej dla programisty (deskryptor segmentu) – adres bazowy segmentu 0xffff0000 i limit 0xffff;Wykonując kod BIOS-u, procesor inicjuje działanie sprzętu i przeprowadza testy POST (ang. Power-on Self Test), których celem jest weryfikacja, czy sprzęt funkcjonuje prawidłowo. BIOS sprawdza m.in. stan:
Jeśli testy POST zakończą się pomyślnie, kolejnymi zadaniami BIOS-u są zainicjowanie tablicy przerwań (po adresem 0), aby możliwe było wywoływanie funkcji BIOS-u, zainicjowanie podstawowych urządzeń wejścia-wyjścia (klawiatura, ekran, dyski), a następnie rozpoczęcie ładowania systemu operacyjnego. Ze względu na swoje ograniczenia BIOS nie może jednak bezpośrednio załadować systemu – to zadanie zostaje przekazane programowi nazywanemu boot loaderem.
Aby znaleźć kod boot loadera, BIOS przegląda zamontowane nośniki danych (np. twardy dysk, CD-ROM), sprawdzając, czy na którymś z nich pierwszy sektor danych (tradycyjnie o rozmiarze 512 bajtów) kończy się magiczną liczbą 0xaa55. Na podstawie tej ustalonej wartości BIOS identyfikuje specjalny sektor MBR (Master Boot Record). Jego klasyczną strukturę przedstawia rysunek poniżej.
Zauważmy, że poza kodem boot loadera oraz magiczną liczbą częścią sektora jest także tablica partycji.
BIOS kończy swoje poszukiwania po znalezieniu pierwszego z boot loaderów i ładuje go do pamięci pod ustalony adres: 0x7c00. Po czym zostaje tam przekazane sterowanie, a procesor zaczyna wykonywać instrukcje boot loadera.
Od boot loadera oczekujemy wykonania następujących zadań:
Inny sposób obejścia niewielkiego rozmiaru MBR wymaga, aby boot loader najpierw przeanalizował tablicę partycji. W tej strukturze znajduje się m.in. informacja, które partycje są bootowalne (czyli zawierają w pierwszym sektorze własny boot loader, najczęściej dedykowany dla konkretnego systemu, zainstalowanego na tej partycji). Boot loader wyświetla wówczas menu, umożliwiając wybór, który system ma zostać uruchomiony. Następnie kopiuje swój kod do innego miejsca w pamięci (tradycyjnie nowym adresem jest 0x0000:0x0600), a pod swój oryginalny adres (0x0000:0x7c00) ładuje boot loader danej partycji, któremu pozostawia do wykonania resztę zadań.
Niewystarczająca przestrzeń przydzielona w ramach MBR jest tylko jedynym z wielu ograniczeń, z którymi boot loader musi się zmierzyć. Zastanówmy się, w jaki sposób boot loader może załadować do pamięci jądro, wiedząc, że:
ds
) wartość selektora, który wskazuje na deskryptor segmentu rozpoczynającego się pod adresem fizycznym 0 i mającego rozmiar 4 GiB, jak to jest przewidziane w trybie chronionym;Po załadowaniu jądra do pamięci i zaopatrzeniu go w odpowiednie informacje cel boot loadera zostaje osiągnięty. Od tego momentu proces bootowania jest nadzorowany przez jądro danego systemu operacyjnego.
Przykładowo dla systemu Linux i platformy i386 przestrzeń adresowa po załadowaniu jądra do pamięci wygląda zazwyczaj tak, jak prezentuje to rysunek poniżej (adres X
jest uzależniony od konkretnego boot loadera).
Zauważmy, że obraz jądra Linuxa składa się z dwóch części. Pierwsza z nich (od której zaczyna się wywołanie kodu) załadowana do pamięci poniżej 640 KiB funkcjonuje w trybie rzeczywistym, druga zaś znajduje się powyżej 1 MiB i działa w trybie chronionym.
O tym, jak wyglądają dalsze etapy uruchamiania systemu w przypadku Linuxa można przeczytać m.in. na świetnym blogu Gustavo Duarte.
Zdobytą wiedzę warto uzupełnić o informacje na temat popularnych boot loaderów:
oraz na temat UEFI (Unified Extensible Firmware Interface), który jest względnie nowym standardem interfejsu między systemem operacyjnym a firmware'em. UEFI przejmuje część zadań BIOS-u, w szczególności nie powiela ograniczeń związanych ze strukturą MBR, wykonując proces bootowania w inny sposób. O różnicach między BIOS-em a UEFI można przeczytać m.in. na następujących stronach:Spróbujmy zastąpić oryginalny kod boot loadera spreparowanym przez nas. Zaczniemy od napisania bardzo prostego programu, który zamiast załadować system operacyjny zapętli się w miejscu. Nie tylko działanie naszego boot loadera będzie nieskomplikowane, ale również sposób, w jaki go stworzymy. Głównym celem tej części ćwiczeń jest bowiem zapoznanie się z przydatnymi narzędziami i oswojenie z trybem rzeczywistym.
Program data duplicator
wywoływany poleceniem dd
służy do kopiowania i konwersji danych. Schemat użycia prezentuje się następująco:
dd if=<źródło danych> of=<docelowe miejsce danych> [opcje]
Domyślne wartości if
oraz of
to odpowiednio: stdin
i stdout
. W ich miejsce można podać zarówno „zwyczajny” plik systemowy, jak i plik urządzenia (reprezentujący sterownik urządzenia, np. sterownik dysku: /dev/hda
). Poniższa lista wymienia typowe komendy z wykorzystaniem dd
, prezentując przy okazji popularne opcje programu.
~/hdadisk.img
:
dd if =/dev/sda2 of=~/hdadisk.img
/home/abc.txt
z wyłączeniem pierwszego kilobajta do /home/xyz.txt
:
dd bs=1 skip=1024 if=/home/abc.txt of=/home/xyz.txt
dd seek=2 if=/dev/random/ of=/dev/sda2
dd
można znaleźć m.in. na tych stronach:
The Linux Juggernaut, Linux manual.
Kolejnego narzędzia, octal dump
, będziemy używać do wyświetlania danych w formacie szesnastkowym:
od -A x -t x1 -v <ścieżka do pliku> > <ścieżka do pliku wynikowego>
Plik, do którego za pomocą instrukcji >
przekierowaliśmy rezultat działania programu, przyjmie następującą postać:
[offset od początku pliku w zapisie szesnastkowym] [16 kolejnych bajtów pliku wejściowego] [>odpowiadający im zapis w ASCII<]
Jak zwykle, więcej informacji można znaleźć w manualu. Warto również zobaczyć przykłady użycia.
Alternatywnie do przeglądania plików binarnych poza MINIX-em można użyć programu hexdump
:
$ hexdump -C <ścieżka do pliku> > <ścieżka do pliku wynikowego>
Aby zainstalować program hexedit
na MINIX-ie, używamy polecenia:
# pkgin install hexedit
Za pomocą tego narzędzia będziemy mogli edytować pliki binarne w bezpośredni sposób – np. zmieniając poszczególne bajty kodu maszynowego w zapisie szesnastkowym. Program oferuje dość rozbudowany interfejs, opisany m.in. na stronie autora. Na nasz użytek potrzebujemy jedynie wiedzieć, że:
Ctrl-w
,Ctrl-c
.$ hexedit <ścieżka do dowolnego pliku>
Wyposażeni w opisany powyżej warsztat jesteśmy gotowi, aby przystąpić do wykonania pierwszych zadań.
Sprawdź, jak wygląda zapis szesnastkowy kodu boot loadera MINIX-a. Do tego celu użyj gotowego polecenia, zastępując znaki zapytania poprawnymi wartościami:
# dd bs=? count=? if=/dev/c0d0? od -Ax -tx1 -v
Następnie, zmieniając parametry polecenia, przyjrzyj się kilku kolejnym sektorom za MBR, czy zostały wykorzystane przez boot loader? Czy pierwsza partycja (/dev/c0d0p0
) rzeczywiście zaczyna się dopiero w odległości 62 sektorów od MBR?
Wyjaśnienie: w przypadku naszego obrazu MINIX-a /dev/c0d0
oznacza główny dysk twardy.
Wskazówka: sprawdź, czym różni się przekierowywanie wyników działania programu za pomocą >
(redirect) i za pomocą |
(pipe).
Wykonując pierwsze zadanie, zwróć uwagę na dwa ostatnie bajty przeglądanego sektora (pamiętając, że kolejność bajtów w tej architekturze jest zgodna ze standardem little-endian) – czy są równe magicznej wartości, która powinna znajdować się na końcu każdego boot loadera?
Napisz kod nowego boot loadera, którego jedynym zadaniem będzie wykonywanie skoku ciągle w to samo miejsce. Zacznij od utworzenia pliku binarnego, custom_bl
, wypełnionego zerami o odpowiedniej wielkości (jak poprzednio należy zastąpić znaki zapytania, aby uzyskać poprawne polecenie):
# dd count=Następnie:? if=/dev/? of=custom_bl
custom_bl
w edytorze hexedit,eb fe
– kod maszynowy instrukcji zapętlenia,eb fe
.
Podpowiedź:
fe
(pamiętając o tym, jak zapisywane są liczby ujemne).# dd bs=? count=1 if=custom_bl of=/dev/? # reboot
Oczywiście, bezpośrednie edytowanie kodu maszynowego nie jest ani wygodne, ani efektywne. Kolejne programy napiszemy więc w asemblerze, kompilując je następująco:
$ nasm -f bin file.asm -o fileJak wyglądałby więc kod w asemblerze odpowiadający obecnej wersji
custom_bl
?
Uzupełnij linię 4 programu tak, aby po skompilowaniu, plik binarny miał wielkość 512 bajtów, a bajty między instrukcją skoku a magiczną wartością były zerami.
1: loop: 2: jmp loop 3: 4: times? db 0; 5: dw 0xaa55
Podpowiedź: "$" w asemblerze oznacza adres, gdzie zostanie zapisana aktualna instrukcja, a symbol "$$" to adres początku bieżącej sekcji (dokumentacja NASM-a).
Skompiluj program i wyświetl zawartość binarnego pliku, używając do tego poznanych narzędzi (od
albo hexedit
). Wygląda znajomo, prawda?
Działanie napisanego przez nas boot loadera jest na razie mało efektowne. Czy możemy zrobić cokolwiek więcej? Na etapie wykonywania kodu boot loadera nie ma przecież ani systemu plików, ani działających bibliotek, ani nawet jądra systemu operacyjnego… Czy to oznacza, że aby wypisać cokolwiek na ekranie albo odczytać z dysku, musimy sami obsłużyć odpowiednie urządzenia?
Na szczęście nie musimy sami obsługiwać podstawowych urządzeń wejścia-wyjścia.
Zajmuje się tym BIOS.
Funkcje BIOS-u wywołuje się za pomocą przerwań programowych.
Po zwięzłe i klarowne wyjaśnienie odsyłamy do rozdziału 17.2 kultowej książki The Art of Assembly Language.
Aby wywołać funkcję BIOS-u, należy wywołać przerwanie programowe za pomocą asemblerowej instrukcji int
, podając numer przerwania (indeks w wektorze przerwań). Dodatkowe argumenty przekazuje się w rejestrach.
Przykładowe użycie prezentuje poniższy kod, którego zadaniem jest wypisanie na ekranie znaku 'H':
mov ah, 0xe ; argument - doprecyzowanie funkcji przerwania (wypisz znak i przesuń kursor) mov al, 'H' ; argument - znak do wypisania int 0x10 ; wywołanie przerwania nr 16 - obsługa ekranu
W celu sprawdzenia, jakie inne funkcjonalności udostępnia opisany mechanizm, warto przyjrzeć się liście przerwań BIOS-u.
Zmodyfikuj tak obecną wersję naszego boot loadera custom_bl.s
, aby program przed zapętleniem się wyświetlał na ekranie napis „Hello real world!\n”. Wyjątkowo tym razem oczekujemy, że wypisywanie kolejnych znaków zostanie zaimplementowane w stylu copy-paste.
Oczywiście, byłoby znacznie wygodniej, gdybyśmy mogli umieścić powtarzające się instrukcje w funkcji. Zdefiniujmy więc funkcję print_char
w następujący sposób:
; w rejestrze al funkcja spodziewa się otrzymać argument - znak do wypisania print_char: mov ah, 0xe int 0x10 ret
Z naszej funkcji korzystamy w tradycyjny sposób, czyli za pomocą instrukcji call
:
mov al, 'H' call print_char
Przypomnijmy sobie, że przy okazji wywoływania funkcji, a także przerwań, procesor korzysta ze stosu, m.in. po to, żeby zapisać w pamięci adres powrotu. Z tego powodu powinniśmy ustawić w boot loaderze swój własny stos. Inicjacja stosu wymaga:
ss
i sp
.start: xor ax, ax mov ss, ax ; po tej instrukcji procesor blokuje przerwania na czas wykonania kolejnej instrukcji mov sp, 0x8000 ; rejestr sp musi być załadowany natychmiast po załadowaniu rejestru ss
Ostatnie usprawnienie, które omówimy, dotyczy deklarowania statycznych wartości. Przypomnijmy, że w asemblerze możemy przypisać wartość fragmentom pamięci za pomocą psuedoinstrukcji: db
(declare byte), dw
(declare word) i innych, o których przeczytamy w dokumentacji NASM-a. Chcąc skorzystać z tej funkcjonalności, powinniśmy upewnić się, że tryb rzeczywisty pozwoli nam swobodnie odwoływać się do adresów deklarowanych danych.
Rozważmy następujący fragment kodu:
WELCOME_MSG: db 'Hello!', 0xd, 0xa, 0x0 ; napis kończy się znakiem nowej linii (0xd, 0xa) i nullem (0x0) BUFFER times 64 db 0 ; inicjacja 64-bajtowego bufora start: mov ax, BUFFER ; zapisujemy w rejestrze ax adres bufora
Jaka wartość znajdzie się w rejestrze ax
?
Skompiluj powyższy fragment kodu i otwórz plik wyjściowy programem hexedit
. Znajdź instrukcję wpisywania wartości do rejestru ax
(opkod 0xb8). Sąsiedni bajt to wartość (adres bufora), którą kompilator przypisze stałej BUFFER
i która znajdzie się w rejestrze.
W rejestrze ax
zostanie umieszczona wartość 0x9, ponieważ pierwsze 9 bajtów kodu maszynowego zajmie definicja napisu WELCOME_MSG
, a domyślnym adresem załadowania programu do pamięci jest adres 0x0. Wiemy już jednak z poprzedniego punktu, że pod adresem 0x0 mieści się wektor przerwań, a kod boot loadera zwyczajowo zaczyna się od adresu 0x7c00. Musimy więc przekazać kompilatorowi informację na temat adresu, pod którym znajdzie się wygenerowany kod maszynowy, aby poprawnie policzył bezwzględne adresy, do których odwołujemy się w kodzie:
org 0x7c00 ; informacja o początkowym adresie programu ('Binary File Program Origin') WELCOME_MSG: db 'Hello!', 0xd, 0xa, 0x0 ; napis kończy się znakiem nowej linii (0xd, 0xa) i nullem (0x0) BUFFER times 64 db 0 ; inicjacja 64-bajtowego bufora start: mov ax, BUFFER ; tym razem w rejestrze ax znajdzie się wartość 0x7c09
Dodajmy teraz następującą instrukcję:
mov al, byte [WELCOME_MSG]
Oczekujemy, że w rezultacie rejestr al
będzie zawierał pierwszy znak zapisany pod adresem WELCOME_MSG
('H'). Może się jednak zdarzyć, że wykonując tę instrukcję, odwołamy się do zupełnie innego adresu niż spodziewane 0x0. Jest to wynikiem segmentacji pamięci, o której była mowa w sekcji 1.1. W trybie rzeczywistym procesor interpretuje nasz kod jako:
mov al, byte ds:[WELCOME_MSG] ; ds to początek segmentu danych (data segment)
Chcielibyśmy uchronić się przed odwołaniami do niepożądanych adresów, dlatego na początku programu powinniśmy zainicjować wszystkie rejestry segmentowe (jak uczyniliśmy w przypadku rejestru ss
, inicjując stos).
Nasz kod będzie odwoływał się jedynie do początkowych 64 KiB pamięci, więc wystarczy wyzerować wszystkie rejestry segmentowe.
W szczególności, wykonując daleki skok pod adres 0:start
upewniamy się, że wartość rejestru cs
zostanie również wyzerowana:
org 0x7c00 ; informacja o początkowym adresie programu ('Binary File Program Origin') jmp 0:start ; wyzerowanie rejestru cs start: mov ax, cs ; wyzerowanie pozostałych rejestrów segmentowych mov ds, ax mov es, ax mov ss, ax mov sp, 0x8000 ; inicjacja stosu
To wszystko, co potrzebujesz wiedzieć na tym etapie – możesz teraz śmiało przystąpić do napisania ulepszonej wersji Twojego boot loadera.
Napisz pomocniczą funkcję print
, która przyjmuje adres do bufora w rejestrze ax
i wypisuje tekst z bufora aż do napotkania znaku końca napisu (0x0).
Następnie zmodyfikuj program z zadania E4, aby używał funkcji print
do wyświetlenia „Hello real world!\n” i przetestuj działanie nowego boot loadera.
Tuż po uruchomieniu MINIX-a, aby rozpocząć pracę z systemem, należy zalogować się na nasze konto użytkownika. Gdy podamy poprawny login oraz hasło, przygotowywane jest środowisko zgodne z wcześniej wyspecyfikowaną konfiguracją. Aby zrozumieć sens tego procesu i móc wpłynąć na jego przebieg, powinniśmy odpowiedzieć przynajmniej na dwa pytania.
MINIX, podobnie jak inne systemy z rodziny UNIX, pozwala na jednoczesne korzystanie z systemu wielu użytkownikom. Można dla nich tworzyć własne, strzeżone hasłem konta, do których zostaną przypisane pliki konfiguracyjne oraz prywatne dane. Różnym nadużyciom zapobiega ograniczenie możliwości interakcji z systemem nałożone na przeciętnych użytkowników. Standardowym wyjątkiem jest użytkownik root – superuser o nieograniczonym dostępie do wszelkich interfejsów MINIX-a.
Swoją tożsamość użytkownika systemu możesz poznać, wpisując polecenie:
$ id uid=1001(alice) gid=1001(friends) groups=1000(geeks),1001(friends)i odczytując wartość przypisaną do
uid
.
Użytkownicy mogą zostać włączeni do rozmaitych grup – aby było łatwiej przydzielać im odpowiednie uprawnienia. Jeden użytkownik może należeć do wielu grup, ale w danym momencie tylko jedna z nich jest jego główną grupą (primary group). W przykładzie powyżej alice należy do grup geeks oraz friends. Druga z wymienionych grup jest główną grupą alice. Aby tymczasowo zmienić główną grupę na inną, należy użyć polecenia newgrp
, które ponownie uruchomi shell, przypisując nas do właściwej grupy:
$ newgrp geeks; $ id uid=1001(alice) gid=1000(geeks ) groups=1000(geeks),1001(friends)
O tym, jak tworzyć grupy, dodawać oraz usuwać użytkowników w systemie MINIX, można dowiedzieć się z wiki MINIX-a. Modyfikowanie danych o użytkowniku odbywa się za pomocą polecenia usermod
. Na przykład, aby dopisać użytkownika alice do grupy students, wpisujemy:
usermod -G students alicelub
usermod -g students alicejeśli grupa students ma być domyślnie nową grupą główną alice.
Aby poznać listę użytkowników, wystarczy wyświetlić zawartość pliku /etc/passwd
. Każda linia zawiera informacje na temat jednego użytkownika i ma następującą postać:
alice:*:1000:100:Alice Smith:/home/alice:/bin/shDane w ramach wpisu są oddzielone dwukropkiem i opisują kolejno:
Listę grup można znaleźć w pliku /etc/group
:
$ tail -n 1 /etc/group friends:*:1001:root,aliceDla każdej grupy lista zawiera jej:
Na swoim obrazie systemu MINIX utwórz nową grupę „friends”. Dodaj dwóch nowych użytkowników o loginach „alice” i „bob”. Dla każdego z nich utwórz katalog domowy i ustaw grupę „friends” jako główną. Obejrzyj pliki /etc/passwd
i /etc/group
, żeby zweryfikować poprawność rozwiązania. Następnie zaloguj się na konto alice, używając polecenia login
.
Jak już wspomniano, wprowadzenie abstrakcji użytkownika ma na celu nie tylko dopasowanie środowiska pracy do preferencji poszczególnych osób. Istotna jest także możliwość wprowadzenia ograniczeń w dostępie do zasobów, np. zapewnienie, że dokumenty użytkownika A są widoczne dla użytkownika B, ale nie mogą zostać przez niego zmodyfikowane. Podstawowy mechanizm UNIX-a odpowiadający na tę potrzebę działa w oparciu o zasady wymienione poniżej.
Do pozyskiwania konkretnych informacji o danym pliku przydaje się program stat
. Po uruchomieniu MINIX-a i zalogowaniu się jako root możesz wypróbować następujące polecenia tego programu na dowolnym pliku, np. /root/.exrc
:
sprawdzenie, który użytkownik jest właścicielem
# stat -f "%Su" /root/.exrc
sprawdzenie, która grupa jest właścicielem
# stat -f "%Sg" /root/.exrc
wypisanie uprawnień
# stat -f "%Sp -> owner=%SHp group=%SMp other=%SLp" /root/.exrc
Alternatywnie, aby zobaczyć wszystkie z wymienionych powyżej informacji jednocześnie, można użyć polecenia ls
z opcją -l
. Wyświetloną informację w obu przypadkach interpretujemy w ten sam sposób – został on klarownie wyjaśniony na stronie Linuxa. Zachęcamy do przeczytania całego artykułu ze szczególnym zwróceniem uwagi na istnienie sticky bit oraz przydatne polecenia: chmod
i chown
, które należałoby uzupełnić o chgrp
.
Pliki konfiguracyjne użytkownika znajdują się w jego katalogu domowym, czyli domyślnie pod ścieżką /home/username
. W systemie MINIX początkowo zawartością katalogu jest kopia plików z /usr/ast
:
/usr/ast/.exrc
/usr/ast/.profile
/usr/ast
jest uhonorowaniem twórcy MINIX-a Andrew S. Tananbauma.
Pierwszy z wymienionych plików zawiera konfigurację edytora vi
oraz narzędzi bazujących na vi
: ex
, view
. Aby zrozumieć, co oznaczają domyślnie wpisane opcje, należy posłużyć się np. fragmentem książki Linux in a Nutshell.
Natomiast plik .profile
zawiera polecenia powłoki wykonywane automatycznie za każdym razem, kiedy użytkownik loguje się na swoje konto. Można zdefiniować w nim między innymi:
np. PATH
– zmienną, która zawiera listę ścieżek do miejsc, gdzie znajdują się pliki wykonywalne programów globalnie dostępnych dla użytkownika
PATH=$PATH:~/my_programms # do aktualnej listy dodajemy ścieżkę do ~/my_programms
w ten sposób można np. zapewnić, że pewne opcje są ustawione domyślnie dla wybranych poleceń:
alias ls="ls -l" # wpisując "ls" będziemy w rzeczywistości wywoływać polecenie "ls -l"
Korzystając z konfiguracji przygotowanej w zadaniu E6, zmodyfikuj pliki ~/.profile
roota i alice w taki sposób, aby:
/tmp/secret
(jeśli nie istnieje), należący do grupy friends;