Wstêp
Swego czau obmy¶li³em prosty asembler, do którego by³ kompilowany inny jêzyk. Ten aembler by³ wykonywany przez maszynê wirtualn±. Teraz piszê kompilator tego asemblera do pliku wykonywalnego, napisany jest w C, pod Linuksem.
Przedstawiê tu pokrótce, jak to dzia³a, gdy¿ temat te wydaje mi siê do¶æ ciekawy, a te¿ ma³o popularny. Kod jest w pocz±tkowej fazie i ma³o jest tam do analizowania, a jednocze¶nie IMHO jest na tyle dobry, ¿e dalsze rzeczy bêdzie w miarê ³atwo dodaæ.
Potrzebne bêd± nam nastêpuj±ce oprogramowanie oraz wiedza z zakresu:
- podstawy asemblera, C, Makefile
- Linux z pakietami libelfg0 oraz libelfg0-dev
- git (by ¶ci±gn±æ kod z repozytorium)
- objdump
Projekt mo¿na ¶ci±gn±æ pomocy git'a klonuj±c repozytorium:
Mo¿liwo¶ci kompilatora
No na razie nie ma tego du¿o:
- obs³uga przerwania, które koñczy dzia³anie programu
- dodawanie
- odk³adanie na stos
- procedury (których póki co nie mo¿na wywo³ywaæ)
Teoria
Pliki wykonywalne w Linuksie oparte s± o pliki ELF, jest to do¶æ stary standard, powszechny w ¶rodowiskach Uniksowych. Ka¿dy taki plik jest plikiem binarnym, tzn. Nie otworzymy go zwyk³ym edytorem tekstu (vim, emacs), lecz musimy skorzystaæ z wyspecjalizowanego programu, który potrafi go odczytaæ I przedstawiæ nam o nim (tzn. o pliku) informacje. Takie narzêdzia to m.in. readelf oraz objdump, ja wykorzysta³em ten drugi. O tym, jak go wykorzystaæ powiem pó¼niej.
Plik ELF sk³ada siê z sekcji, informacje w nich zawarte s± ze sob± powi±zane, najwa¿niejsze z nich to:
- symtab – tablica symboli, s³u¿y m.in. Do tego, by wskazac, w którym miejcu zaczynaj± siê procedury, nazwy tych funkcji zawarte s± w strtab
- strtab – powi±zana z symtab, nazwy procedur
- text – instrukcje do wykonania dla procesora
Toolchain
W skrócie to pisanie takiego kodu u mie dzia³a tak: kompilujê kompilator, odpalam makefile, który tym kompilowanym kmpilatorem kompiluje plik asemblera. Ta kompilacja przebiega pod okiem valgrinda, bym wiedzia³, czy s± wycieki pamiêci. Skompilowany plik wynikowy sprawdzam objdumpem (sekcje z rozkazami dla procesor), linkujê (ld – standardowy linker) sprawdzaj±c czy nie ma b³êdów linkwania, a na koñcu wykonujê (sprawdzaj±c czy program siê nie rozsypie.
By przeprpwadziæ te czynno¶ci trzeba ucruchomiæ makefile, w katalogu /dist/Debug/GNU-Linux-x86 Ma on postaæ:
Kod:
all:
valgrind ./nativecompiler ./hello-*
objdump -D foo.o
ld -s -o foo ./foo.o -lelf -I/lib/ld-linux.so.2
./foo
Plik asemblera ma postaæ:
Kod:
main:
push 5
push 3
add
call foo
int 0
int 2
ret
foo:
ret
bar:
ret
A wynik jest taki:
Kod:
rgawron@foo:/opt/l33tlang/NativeCompiler/dist/Debug/GNU-Linux-x86$ make
valgrind ./nativecompiler ./hello-*
==13616== Memcheck, a memory error detector.
==13616== Copyright (C) 2002-2007, and GNU GPL'd, by Julian Seward et al.
==13616== Using LibVEX rev 1804, a library for dynamic binary translation.
==13616== Copyright (C) 2004-2007, and GNU GPL'd, by OpenWorks LLP.
==13616== Using valgrind-3.3.0-Debian, a dynamic binary instrumentation framework.
==13616== Copyright (C) 2000-2007, and GNU GPL'd, by Julian Seward et al.
==13616== For more details, rerun with: -v
==13616==
call foo
; call is not implmented
==13616==
==13616== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 13 from 1)
==13616== malloc/free: in use at exit: 352 bytes in 1 blocks.
==13616== malloc/free: 14 allocs, 13 frees, 2,164 bytes allocated.
==13616== For counts of detected errors, rerun with: -v
==13616== searching for pointers to 1 not-freed blocks.
==13616== checked 69,732 bytes.
==13616==
==13616== LEAK SUMMARY:
==13616== definitely lost: 0 bytes in 0 blocks.
==13616== possibly lost: 0 bytes in 0 blocks.
==13616== still reachable: 352 bytes in 1 blocks.
==13616== suppressed: 0 bytes in 0 blocks.
==13616== Rerun with --leak-check=full to see details of leaked memory.
objdump -D foo.o
foo.o: file format elf32-i386
Disassembly of section .text:
00000000 <_start>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 68 05 00 00 00 push $0x5
8: 68 03 00 00 00 push $0x3
d: 59 pop %ecx
e: 5b pop %ebx
f: 01 cb add %ecx,%ebx
11: 90 nop
12: b8 01 00 00 00 mov $0x1,%eax
17: cd 80 int $0x80
19: c9 leave
1a: c3 ret
0000001b <foo>:
1b: 55 push %ebp
1c: 89 e5 mov %esp,%ebp
1e: c9 leave
1f: c3 ret
00000020 <bar>:
20: 55 push %ebp
21: 89 e5 mov %esp,%ebp
23: c9 leave
24: c3 ret
ld -s -o foo ./foo.o -lelf -I/lib/ld-linux.so.2
./foo
make: *** [all] Error 8
Pytanie na znajomo¶æ asma+linuka, czy Error, który pojawia siê na koñcu to b³±d czy nie? :)
Kod
Ca³o¶c napisana jest w sposób obiektowy, w C, je¶li siê z tym nie spotkali¶cie, to mo¿e to wygl±daæ trochê dziwnie :) Jakby¶cie chcieli o tym wiêcej poczytaæ, to tutaj jest d³u¿sza notka o programowaniu zorientowanym obiektowo w C.
Obiekt world tworzy ¶rodowisko dla naszego parsera (nom, on te¿ powinien byæ bardziej obiektowo napisany, to swoj± drog±).
Parser, po koleii pobiera linijki z pliku z asemblerem, a ka¿d± z nich parsuje (tzn maj±c string “push 33”, probuje zrozumieæ, ¿e ma od³o¿yæ na stos 33, ¿e ma takie co¶ ma w³a¶cie skompilowaæ). Moje za³o¿enie by³o takie, by kompilacja ka¿dej linijki pliku asemblera wymaga³a wczytania, posiadania informacji tylko tej linijce, by nie trzeba by³o sprawdzaæ, co jest linijkê wcze¶niej, lub pó¼niej.
Obs³uga przerwañ asemblera, generowanie odpowiadaj±cego im kodu znajdje siê wpliku interrupts.c.
Do obs³ugi plików ELF wykorzysta³em gotow± bibliotekê (napisan± w C) o nazwie libelf. AFAIK ma³o jest o niej dokumentacji, lecz do¶c ciekawym linkiem jest ten tutorial.
Biblioteka ta jest ona do¶æ toporna w ob³udze, masê rzeczy trzeba ustawiæ rêcznie, dlatego obudowa³em j± w swój zestaw funkcji (wzoruj± siê na powy¿szym linku), dziêki czemu pisz±æ inne fragmêty kompilatora nie musia³em sie przejmowaæ niskopoziomowymi detalami. Ta czê¶c jest te¿ napisana nieobiektowo.
W ca³ym kodzie czêsto przekazujê funkcjê jako argument inne funkcje, tworze tablice funkcji etc, jest to rozwi±zanie znacznie czytelniejsze, ni¿ siermiê¿ne drabinki if-else. W przysz³o¶ci poprawiê to I zamiast zwyk³ych tablic zastosujê struktury.