Jak samemu uruchomić program bez uruchomionego systemu operacyjnego? Czy można tworzyć programy asemblacyjne, które komputer może ładować i uruchamiać podczas uruchamiania, np. Uruchomić komputer z dysku flash i uruchamiać program na CPU?
assembly
x86
operating-system
bootloader
osdev
użytkownik2320609
źródło
źródło
Odpowiedzi:
Umieszczasz swój kod binarny w miejscu, w którym procesor szuka po ponownym uruchomieniu (np. Adres 0 na ARM).
Ogólna odpowiedź na pytanie: można to zrobić. Często nazywane jest „programowaniem bez systemu metalowego”. Aby czytać z dysku flash, chcesz wiedzieć, co to jest USB i chcesz mieć sterownik do pracy z tym USB. Program na tym dysku musiałby również mieć określony format, na określonym systemie plików ... Jest to coś, co zwykle robią programy ładujące, ale twój program może zawierać własny program ładujący, więc jest samodzielny, jeśli oprogramowanie układowe będzie tylko załaduj mały blok kodu.
Wiele tablic ARM pozwala ci robić niektóre z tych rzeczy. Niektóre mają moduły ładujące, które pomagają w podstawowej konfiguracji.
Tutaj możesz znaleźć świetny samouczek na temat wykonywania podstawowego systemu operacyjnego na Raspberry Pi.
Edytuj: ten artykuł, a cała wiki.osdev.org odpowie na większość twoich pytań http://wiki.osdev.org/Introduction
Ponadto, jeśli nie chcesz eksperymentować bezpośrednio ze sprzętem, możesz uruchomić go jako maszynę wirtualną przy użyciu hiperwizorów, takich jak qemu. Zobacz, jak uruchomić „hello world” bezpośrednio na zwirtualizowanym sprzęcie ARM tutaj .
źródło
Przykłady możliwe do uruchomienia
Stwórzmy i uruchommy kilka maleńkich programów typu hello world, które działają bez systemu operacyjnego na:
Wypróbujemy je również na emulatorze QEMU w jak największym stopniu, ponieważ jest to bezpieczniejsze i wygodniejsze w rozwoju. Testy QEMU przeprowadzono na hoście Ubuntu 18.04 z wstępnie spakowanym pakietem QEMU 2.11.1.
Kod wszystkich przykładów x86 poniżej i więcej jest obecny w tym repozytorium GitHub .
Jak uruchomić przykłady na prawdziwym sprzęcie x86
Pamiętaj, że uruchamianie przykładów na prawdziwym sprzęcie może być niebezpieczne, np. Możesz przypadkowo wyczyścić dysk lub uszkodzić sprzęt: rób to tylko na starych komputerach, które nie zawierają krytycznych danych! Lub jeszcze lepiej, użyj tanich półprzezroczystych devboardów, takich jak Raspberry Pi, patrz przykład ARM poniżej.
W przypadku typowego laptopa x86 musisz zrobić coś takiego:
Wypal obraz na pendrivie (zniszczy twoje dane!):
podłącz USB do komputera
włącz to
powiedz, żeby uruchomił się z USB.
Oznacza to, że oprogramowanie układowe wybiera USB przed dyskiem twardym.
Jeśli nie jest to domyślne zachowanie twojego komputera, po włączeniu naciskaj Enter, F12, ESC lub inne takie dziwne klawisze, aż pojawi się menu rozruchu, w którym możesz wybrać rozruch z USB.
Często w tych menu można skonfigurować kolejność wyszukiwania.
Na przykład na moim T430 widzę następujące.
Po włączeniu muszę nacisnąć klawisz Enter, aby wejść do menu rozruchu:
Następnie tutaj muszę nacisnąć F12, aby wybrać USB jako urządzenie rozruchowe:
Stamtąd mogę wybrać USB jako urządzenie rozruchowe w następujący sposób:
Alternatywnie, aby zmienić kolejność rozruchu i wybrać USB, który ma wyższy priorytet, więc nie muszę go wybierać ręcznie za każdym razem, uderzyłbym F1 na ekranie „Menu przerwania uruchamiania”, a następnie nawiguję do:
Sektor rozruchowy
Na x86 najprostszym i najniższym poziomem, co możesz zrobić, jest utworzenie głównego sektora rozruchowego (MBR) , który jest rodzajem sektora rozruchowego , a następnie zainstalowanie go na dysku.
Tutaj tworzymy jedną za pomocą jednego
printf
połączenia:Wynik:
Pamiętaj, że nawet bez robienia kilku znaków jest już wydrukowanych na ekranie. Są one drukowane przez oprogramowanie układowe i służą do identyfikacji systemu.
A na T430 dostajemy pusty ekran z migającym kursorem:
main.img
zawiera następujące elementy:\364
in octal ==0xf4
in hex: kodowaniehlt
instrukcji, która każe CPU przestać działać.Dlatego nasz program nic nie zrobi: wystarczy uruchomić i zatrzymać.
Używamy ósemki, ponieważ
\x
liczby szesnastkowe nie są określone przez POSIX.Możemy łatwo uzyskać to kodowanie za pomocą:
które wyjścia:
ale jest to również oczywiście udokumentowane w podręczniku Intela.
%509s
wyprodukuj 509 miejsc. Konieczne jest wypełnienie pliku do bajtu 510.\125\252
ósemkowo ==,0x55
po której następuje0xaa
.Są to 2 wymagane bajty magiczne, które muszą być bajtami 511 i 512.
BIOS przeszukuje wszystkie nasze dyski w poszukiwaniu dysków rozruchowych i bierze pod uwagę tylko te, które mają te dwa bajty.
Jeśli nie jest obecny, sprzęt nie będzie traktował tego jako dysku rozruchowego.
Jeśli nie jesteś
printf
mistrzem, możesz potwierdzić zawartość zamain.img
pomocą:który pokazuje oczekiwane:
gdzie
20
jest spacja w ASCII.Oprogramowanie układowe BIOS odczytuje te 512 bajtów z dysku, umieszcza je w pamięci i ustawia komputer na pierwszy bajt, aby rozpocząć ich wykonywanie.
Witaj, sektorze rozruchowym świata
Teraz, gdy stworzyliśmy minimalny program, przejdźmy do cześć świata.
Oczywiste pytanie brzmi: jak zrobić IO? Kilka opcji:
poprosić oprogramowanie wewnętrzne, np. BIOS lub UEFI, aby zrobiło to za nas
VGA: specjalny region pamięci, który zostanie wydrukowany na ekranie, jeśli zostanie zapisany. Może być używany w trybie chronionym.
napisz sterownik i porozmawiaj bezpośrednio ze sprzętem wyświetlającym. Jest to „właściwy” sposób na zrobienie tego: mocniejszy, ale bardziej złożony.
port szeregowy . Jest to bardzo prosty znormalizowany protokół, który wysyła i odbiera znaki z terminala hosta.
Na komputerach wygląda to tak:
Źródło .
Niestety nie jest narażony na większość współczesnych laptopów, ale jest powszechną drogą do tworzenia płyt deweloperskich, patrz przykłady ARM poniżej.
To naprawdę szkoda, ponieważ takie interfejsy są naprawdę przydatne na przykład do debugowania jądra Linuksa .
użyj funkcji debugowania układów. ARM na przykład nazywa ich semihosting . Na prawdziwym sprzęcie wymaga dodatkowej obsługi sprzętu i oprogramowania, ale na emulatorach może być bezpłatną wygodną opcją. Przykład .
Tutaj zrobimy przykład systemu BIOS, ponieważ jest on prostszy na x86. Pamiętaj jednak, że nie jest to najbardziej niezawodna metoda.
sieć elektryczna
GitHub w górę .
link.ld
Złóż i połącz z:
Wynik:
A na T430:
Testowane na: Lenovo Thinkpad T430, UEFI BIOS 1.16. Dysk wygenerowany na hoście Ubuntu 18.04.
Oprócz standardowych instrukcji montażu dla użytkownika, mamy:
.code16
: informuje GAS, aby wyprowadził 16-bitowy kodcli
: wyłącz przerwania programowe. Mogą one spowodować, że procesor zacznie ponownie działać pohlt
int $0x10
: wykonuje połączenie BIOS. To właśnie drukuje znaki jeden po drugim.Ważne flagi linków to:
--oformat binary
: wypisuje nieprzetworzony kod binarnego zestawu, nie zawijaj go w pliku ELF, jak ma to miejsce w przypadku zwykłych plików wykonywalnych dla użytkownika.Aby lepiej zrozumieć część skryptu linkera, zapoznaj się z krokiem relokacji linkowania: Co robią linkery?
Chłodniejsze programy x86 bez systemu metalowego
Oto kilka bardziej skomplikowanych ustawień bez systemu metalowego:
Użyj C zamiast montażu
Podsumowanie: użyj GRUB multiboot, który rozwiąże wiele irytujących problemów, o których nigdy nie pomyślałeś. Sekcja poniżej.
Główną trudnością na x86 jest to, że BIOS ładuje tylko 512 bajtów z dysku do pamięci, i możesz wysadzić te 512 bajtów podczas używania C!
Aby rozwiązać ten problem, możemy użyć dwustopniowego programu ładującego . Powoduje to kolejne wywołania systemu BIOS, które ładują więcej bajtów z dysku do pamięci. Oto minimalny przykład montażu etapu 2 od zera przy użyciu wywołań int 0x13 BIOS :
Alternatywnie:
-kernel
opcji, która ładuje cały plik ELF do pamięci. Oto przykład ARM, który utworzyłem tą metodą .kernel7.img
, podobnie jak-kernel
robi to QEMU .Tylko do celów edukacyjnych, oto przykładowy minimalny przykład C w jednym etapie :
main.c
wejście S.
linker.ld
biegać
Biblioteka standardowa C.
Sprawa staje się jeszcze przyjemniejsza, jeśli chcesz również korzystać ze standardowej biblioteki C, ponieważ nie mamy jądra Linux, które implementuje większość standardowych funkcji biblioteki C za pośrednictwem POSIX .
Kilka możliwości, bez przechodzenia na w pełni funkcjonalny system operacyjny, taki jak Linux, to:
Napisz swoje własne. To tylko garść nagłówków i plików C, prawda? Dobrze??
Newlib
Szczegółowy przykład na stronie : /electronics/223929/c-standard-libraries-on-bare-metal/223931
Newlib realizuje wszystkie rzeczy nudne non-OS specyficzne dla ciebie, na przykład
memcmp
,memcpy
itdNastępnie udostępnia kilka kodów pośredniczących umożliwiających wdrożenie potrzebnych połączeń systemowych.
Na przykład możemy zaimplementować
exit()
na ARM poprzez semihosting z:jak pokazano w tym przykładzie .
Na przykład, można przekierować
printf
do układów UART lub ARM, lub realizowaćexit()
z semihosting .wbudowane systemy operacyjne, takie jak FreeRTOS i Zephyr .
Takie systemy operacyjne zazwyczaj umożliwiają wyłączenie planowania wyprzedzającego, co daje pełną kontrolę nad czasem działania programu.
Można je postrzegać jako rodzaj wstępnie wdrożonego Newlib.
GNU GRUB Multiboot
Sektory rozruchowe są proste, ale nie są zbyt wygodne:
Z tych powodów GNU GRUB stworzył wygodniejszy format pliku o nazwie multiboot.
Minimalny przykład działania: https://github.com/cirosantilli/x86-bare-metal-examples/tree/d217b180be4220a0b4a453f31275d38e697a99e0/multiboot/hello-world
Używam go również w moim repozytorium przykładów GitHub, aby móc z łatwością uruchamiać wszystkie przykłady na prawdziwym sprzęcie bez milionowego spalania USB.
Wynik QEMU:
T430:
Jeśli przygotujesz system operacyjny jako plik z wieloma uruchomieniami, GRUB będzie mógł go znaleźć w zwykłym systemie plików.
To właśnie robi większość dystrybucji, umieszczając obrazy systemu operacyjnego
/boot
.Pliki Multiboot są w zasadzie plikiem ELF ze specjalnym nagłówkiem. Są one określone przez GRUB pod adresem : https://www.gnu.org/software/grub/manual/multiboot/multiboot.html
Możesz zmienić plik z wieloma uruchomieniami na dysk rozruchowy za pomocą
grub-mkrescue
.Oprogramowanie układowe
W rzeczywistości sektor rozruchowy nie jest pierwszym oprogramowaniem działającym na procesorze systemu.
Najpierw uruchamia się tak zwane oprogramowanie układowe , które jest oprogramowaniem:
Dobrze znane oprogramowanie wewnętrzne obejmuje:
Oprogramowanie wewnętrzne wykonuje następujące czynności:
zapętlaj każdy dysk twardy, USB, sieć itp., aż znajdziesz coś rozruchowego.
Kiedy uruchamiamy QEMU,
-hda
mówi, żemain.img
jest to dysk twardy podłączony do sprzętu ihda
jest pierwszym, który zostanie wypróbowany i jest używany.załaduj pierwsze 512 bajtów na adres pamięci RAM
0x7c00
, umieść tam RIP procesora i pozwól mu działaćpokaż na ekranie takie menu, jak menu uruchamiania lub wywołania drukowania systemu BIOS
Oprogramowanie układowe oferuje funkcje podobne do systemu operacyjnego, od których zależy większość systemów operacyjnych. Np. Podzestaw Python został przeniesiony do działania w systemie BIOS / UEFI: https://www.youtube.com/watch?v=bYQ_lq5dcvM
Można argumentować, że oprogramowanie układowe jest nierozróżnialne od systemów operacyjnych i że oprogramowanie układowe jest jedynym „prawdziwym” programowaniem bez systemu operacyjnego.
Jak to ujął to deweloper CoreOS :
Stan początkowy po BIOS
Podobnie jak wiele rzeczy w sprzęcie, standaryzacja jest słaba, a jedną z rzeczy, na których nie należy polegać, jest stan początkowy rejestrów, gdy kod zaczyna działać po BIOS.
Więc zrób sobie przysługę i użyj kodu inicjalizacji, takiego jak: https://stackoverflow.com/a/32509555/895245
Rejestruje się
%ds
i%es
ma ważne skutki uboczne, więc powinieneś je wyzerować, nawet jeśli nie używasz ich wyraźnie.Zauważ, że niektóre emulatory są ładniejsze niż prawdziwy sprzęt i zapewniają dobry stan początkowy. Potem, gdy idziesz na prawdziwym sprzęcie, wszystko się psuje.
El Torito
Format, który można wypalić na płytach CD: https://en.wikipedia.org/wiki/El_Torito_%28CD-ROM_standard%29
Możliwe jest również wytworzenie obrazu hybrydowego działającego na ISO lub USB. Można to zrobić za pomocą
grub-mkrescue
( przykład ), a także przymake isoimage
użyciu jądra Linuxisohybrid
.RAMIĘ
W ARM ogólne pomysły są takie same.
Nie ma powszechnie dostępnego, częściowo znormalizowanego, wstępnie zainstalowanego oprogramowania układowego, takiego jak BIOS, do użycia dla IO, więc dwa najprostsze typy IO, które możemy wykonać to:
Przesłałem:
kilka prostych QEMU C + Newlib i surowe przykłady montażu tutaj na GitHub .
Na przykład prompt.c pobiera dane wejściowe z terminala hosta i zwraca dane wyjściowe za pośrednictwem symulowanego UART:
Zobacz także: Jak tworzyć programy ARM typu bare metal i uruchamiać je na QEMU?
w pełni zautomatyzowana konfiguracja migacza Raspberry Pi pod adresem : https://github.com/cirosantilli/raspberry-pi-bare-metal-blinker
Zobacz także: Jak uruchomić program C bez systemu operacyjnego na Raspberry Pi?
Aby „zobaczyć” diody LED na QEMU, musisz skompilować QEMU ze źródła z flagą debugowania: /raspberrypi/56373/is-it-possible-to-get-the-state-of- the-leds-and-gpios-in-a-qemu-emulation-like-t
Następnie powinieneś wypróbować cześć świata UART. Możesz zacząć od przykładu migacza i zastąpić jądro tym: https://github.com/dwelch67/raspberrypi/tree/bce377230c2cdd8ff1e40919fdedbc2533ef5a00/uart01
Najpierw poproś UART o pracę z Raspbian, jak wyjaśniłem na: /raspberrypi/38/prepare-for-ssh-woutout-a-screen/54394#54394 Będzie to wyglądało mniej więcej tak:
Upewnij się, że używasz odpowiednich pinów, w przeciwnym razie możesz spalić konwerter UART na USB. Zrobiłem to już dwa razy, powodując zwarcie do masy i 5 V ...
Na koniec połącz się z komputerem szeregowym za pomocą:
W przypadku Raspberry Pi używamy karty Micro SD zamiast pamięci USB do przechowywania naszego pliku wykonywalnego, do którego zwykle potrzebujesz adaptera do połączenia z komputerem:
Nie zapomnij odblokować adaptera SD, jak pokazano na stronie : /ubuntu/213889/microsd-card-is-set-to-read-only-state-how-can-i-write-data -on-it / 814585 # 814585
https://github.com/dwelch67/raspberrypi wygląda jak najpopularniejszy dostępny obecnie samouczek Raspberry Pi od zera.
Niektóre różnice w stosunku do x86 obejmują:
IO odbywa się poprzez bezpośrednie pisanie na magiczne adresy, nie ma instrukcji
in
iout
instrukcji.Nazywa się to We / Wy mapowanym na pamięć .
w przypadku niektórych prawdziwych urządzeń, takich jak Raspberry Pi, możesz samodzielnie dodać oprogramowanie układowe (BIOS) do obrazu dysku.
To dobrze, ponieważ sprawia, że aktualizacja tego oprogramowania jest bardziej przejrzysta.
Zasoby
źródło
System operacyjny jako inspiracja
System operacyjny jest także programem , więc możemy również stworzyć własny program, tworząc od podstaw lub zmieniając (ograniczając lub dodając) funkcje jednego z małych systemów operacyjnych , a następnie uruchamiając go podczas procesu rozruchu (przy użyciu obrazu ISO ) .
Na przykład tę stronę można wykorzystać jako punkt wyjścia:
Jak napisać prosty system operacyjny
Tutaj cały system operacyjny mieści się całkowicie w 512-bajtowym sektorze rozruchowym ( MBR )!
Taki lub podobny prosty system operacyjny może być wykorzystany do stworzenia prostej struktury, która pozwoli nam:
Istnieje jednak wiele możliwości. Na przykład, aby zobaczyć większy system operacyjny w języku asemblera x86 , możemy zapoznać się z systemem operacyjnym MykeOS , x86, który jest narzędziem do nauki pokazującym proste 16-bitowe działanie systemów operacyjnych w trybie rzeczywistym, z dobrze skomentowanym kodem i obszerną dokumentacją .
Boot Loader jako inspiracja
Inne popularne typy programów, które działają bez systemu operacyjnego, to także programy ładujące rozruch . Możemy stworzyć program inspirowany taką koncepcją, na przykład korzystając z tej strony:
Jak opracować własny moduł ładujący
Powyższy artykuł przedstawia także podstawową architekturę takich programów :
Jak widzimy, ta architektura jest bardzo elastyczna i pozwala nam na implementację dowolnego programu , niekoniecznie programu ładującego.
W szczególności pokazuje, jak stosować technikę „kodu mieszanego”, dzięki której można łączyć konstrukcje wysokiego poziomu (z C lub C ++ ) z poleceniami niskiego poziomu (z asemblera ). Jest to bardzo przydatna metoda, ale musimy pamiętać, że:
Artykuł pokazuje także, jak zobaczyć stworzony program w akcji oraz jak przeprowadzić jego testowanie i debugowanie.
Aplikacje UEFI jako inspiracja
W powyższych przykładach wykorzystano fakt ładowania MBR sektora na nośnik danych. Możemy jednak zagłębić się w otchłań , grając na przykład w aplikacjach UEFI :
Jeśli chcielibyśmy zacząć tworzyć takie programy , możemy na przykład zacząć od tych stron internetowych:
Programowanie dla EFI: tworzenie programu „Hello, World” / Programowanie UEFI - pierwsze kroki
Inspiracją jest zgłębianie zagadnień bezpieczeństwa
Powszechnie wiadomo, że istnieje cała grupa złośliwego oprogramowania przed uruchomieniem systemu operacyjnego działa (które są programami) .
Ogromna grupa z nich działa w sektorze MBR lub aplikacjach UEFI, podobnie jak wszystkie powyższe rozwiązania, ale są też takie, które wykorzystują inny punkt wejścia, taki jak Volume Boot Record (VBR) lub BIOS :
a może jeszcze jeden.
Ataki przed uruchomieniem systemu
Różne sposoby uruchamiania
Myślę również, że w tym kontekście warto również wspomnieć, że istnieją różne formy uruchamiania systemu operacyjnego (lub przeznaczonego do tego programu wykonywalnego) . Istnieje wiele, ale chciałbym zwrócić uwagę na ładowanie kodu z sieci za pomocą opcji rozruchu sieciowego ( PXE ), która pozwala nam uruchamiać program na komputerze niezależnie od systemu operacyjnego, a nawet niezależnie od dowolnego nośnika pamięci, który jest bezpośrednio podłączony do komputera:
Co to jest ładowanie sieciowe (PXE) i jak z niego korzystać?
źródło