Witam.
Zastanawiałem się nad napisaniem własnego języka. Po pierwsze zdobyłbym więcej wiedzy o assemblym, plikach, pamięci, działaniu i wielu więcej podczas tego projektu a po drugie miałbym "narzędzie" (język) w którym można bardzo szybko coś napisać (główny zamysł).
Wiedzy o assemblym zdobędziesz aż nadto, ale chyba nie takiej, jak byś oczekiwał. To, co Cię powinno interesować, to przede wszystkim ISA czyli Instruction Set Architecture. Wykaz instrukcji, odpowiadających im kodów maszynowych, lista rejestrów i ich zastosowanie, tryby adresowania, obsługa przerwań, typy danych itp itd. Do powieszenia nad łóżkiem. Na pierwszy ogień najważniejsze, by dana instrukcja z języka znalazła przełożenie na odpowiednią instrukcję lub zestaw instrukcji asemblera docelowej architektury.
Z tego co wiem, instrukcje assemblyego są zależne od procesora. Na wikipedii, możemy przeczytać, że
Stosowanie kompilatorów ułatwia programowanie (programista nie musi znać języka maszynowego) i pozwala na większą przenośność kodu pomiędzy platformami.
Owszem, każda architektura ma własny assembler, jedne bardziej przyjazny, inne mniej. x86 to był dla mnie jakiś kosmos, a Nios2 jest właściwie naprawdę czytelnym assemblerem. Bardzo spójny, bez dodatkowych nawiasików, dupików, dopowiedzień czy pracujesz na słowie czy może jednak na bajcie - chwalmy RISC. Problem jest taki, że aby zapewnić wieloplatformowość Twój kompilator musi posiadać backendy do wielu architektur, chyba że celujesz w jedną albo jednak wolisz interpreter lub maszynę wirtualną. LLVM ma chyba ze 20 backendów, nie pamiętam. A w każdym backendzie nie brakuje klas po kilka-kilkanaście tysięcy linii kodu.
Dodatkowo gdzieś na 4p znalazłem wątek o tym jakiego kompilatora do C++ używać. Jedna z odpowiedzi:
gcc, bo
[...]
kompiluje na wiele platform,
chwalmy Pana
jest bezpłatny,
LLVM też
ma całkiem niezły optymalizator,
LLVM ma trochę lepszy
dzięki fladze -pedantic możemy pisać przenośny kod, który mamy pewność, że skompiluje każdy kompilator zgodny ze standardem,
bardzo popularny. [...]
Bo powstał w czasach, które pamiętają najstarsi górale. Ale myślęNaj, że będzie stopniowo tracił na rzecz nowszych kompilatorów, którym po prostu nie dotrzyma tempa. Zresztą clang czy kompilator Rusta stoją na LLVM, a nie gcc, a to tylko dwa z kilku albo kilkunastu frontendów (jeden frontend - jeden język wysokiego poziomu) LLVM-a.
A więc pytanie. Jak np. gcc kompiluje ten kod do exe, który można uruchomić wszędzie (chodzi o procesor)?
Jak robi to gcc to tak dokładnie nie wiem, ale pewnie w zbliżony sposób do LLVM - z wykorzystaniem kodu pośredniego. Frontend opiszę w skrócie, bo nigdy w żadnym się nie babrałem.
- Frontend bierze kod napisany w języku wysokiego poziomu, analizuje go pod kątem poprawności składni i semantyki, po czym przekształca do postaci reprezentującej - tak z grubsza rzecz ujmując - model programu. Najprawdopodobniej graf jakichś operacji i wywołań powiązanych ze sobą danymi, na których operują, z uwzględnieniem kolejności itd.
- Na tej reprezentacji frontend jest w stanie przeprowadzić wstępną optymalizację np. usunięcie węzłów które nie mają żadnych efektów (np. przypisanie wartości do zmiennej zostanie zapewne wywalone, jeśli nigdy nie następuje jej odczyt aż do zakończenia życia lub nadpisania).
- Instrukcje języka wysokiego poziomu są rozwijane/zwijane (zależnie od sytuacji) w jedną lub więcej instrukcji reprezentacji pośredniej (IR). To coś w rodzaju uogólnionego assemblera z typami danych, wirtualnymi rejestrami które mogą być zapisywane tylko przy inicjalizacji (ułatwia optymalizacje w kolejnych fazach), agregatami (coś w rodzaju struktur) itd. Ma zarówno instrukcje znajdujące odwzorowanie w praktycznie każdym assemblerze (dodawanie, shifty, ładowanie z pamięci) jak i na nieco wyższym poziomie abstrakcji (np. operacje na wektorach i agregatach).
Następnie, jak już masz reprezentację pośrednią, do gry wchodzi backend LLVM. Backend generalnie zajmuje się skompilowaniem tej uogólnionej reprezentacji pośredniej na assembler i/lub kod maszynowy konkretnej architektury.
- IR po zwalidowaniu składni i semantyki jest przekształcany w DAG (z ang. acykliczny graf skierowany), reprezentujący logiczną strukturę programu. Mówiłem już o tym przy frontendzie.
- następuje wstępna optymalizacja DAG, usuwane są niepotrzebne czy znoszące swoje działanie instrukcje, albo zastępowane są innymi (np. 4-krotne dodanie jakichś stałych pewnie zostanie zastąpione dodaniem jednej) i takie tam.
- Po wstępnej optymalizacji mamy etapy legalizacji oraz ISEL (instruction selection), w których typy danych i instrukcje z IR są zastępowane tymi występującymi w danej architekturze tak, aby nic się nie wysypało. Jeśli np. w IR mamy 64-bitowe operacje na rejestrach a architektura takich nie wspiera, muszą zostać rozbite na odpowiednie instrukcje np. 32-bitowe. W międzyczasie występują dodatkowe przebiegi optymalizacji.
- Scheduler ustala ostateczną kolejność występowania po sobie instrukcji. Generalnie zaczyna od "najpierwszej" instrukcji u "szczytu" grafu i przechodzi po nim tak, aby żadna instrukcja nie wykonała się przed inną, od której wyniku jest zależna.
I w zasadzie jeśli naprawdę bardzo chcesz napisać swój kompilator, to oszczędź sobie mordęgi kompilowania na konkretne platformy i zbuduj swój kompilator jako frontend np. LLVM. Będziesz miał do opanowania jedną reprezentację pośrednią, a nie srylion asemblerów, i bardzo dobrą optymalizację pod spodem out-of-the-box.
Btw zachęcam do lektury:
CodeGen: http://llvm.org/docs/CodeGenerator.html
LLVM IR: http://llvm.org/docs/LangRef.html