Code snippets, ideas and events from IT related projects

by Robert Gawron

Maszyna wirtualna: jak działa i jak ją napisać

Zakres tematyczny posta

Kod źródłowy pisany jest w języku/formie pozwalającej programiście na szybkie i eleganckie rozwiązanie problemu (języki wysokiego poziomu, jak C, Erlang, czy też mnemoniczny zapis asemblera). Kod taki jest nieprzydatny dla maszyny, gdyż zawiera abstrakcyjne konstrukcje, musi więc zostać przetłumaczony do prostszej/innej postaci - temu służą kompilatory. Efektem jest kod, który maszyna może wykonać (kod maszynowy, kod pośredni).

budowa kompilatora

Wspomnianą maszyną może być komputer, może to być też aplikacja potrafiąca wykonać instrukcje skompilowanego programu. Taka aplikacja nosi nazwę maszyny wirtualnej (VM), a jej implementacji poświęcony jest ten post. Druga część poświęcona zostanie projektowi i implementacji własnego języka programowania (już niedługo - właśnie kończę:)).

Koncepcja, jak ma działać opisywana tu maszynka powstała drogą analizy kompilacji przykładowych kodów źródłowych w C do asemblera, stąd całość jest mało abstrakcyjna, a bardziej przypomina właśnie sprzętowy twór.

VM jest w pełni sprawna, a na końcu notki podane są przykłady programów, które może wykonać. Przed napisaniem posta wykonałem kilka prototypów (C, Erlang, Python), finalny napisałem w Pythonie - znam go w miarę dobrze i szybko się w nim pisze ale mam zamiar kiedyś przepisać kod do Erlanga.

Zasada działania

Program wykonywany przez VM składa się z procedur, każda z nich oznaczona jest etykietą, oznaczony jest również jej koniec. Wykonywanie programu zaczyna się od procedury oznaczonej etykietą main. Z każdej można skoczyć do każdej i wrócić po jej skończeniu. Procedura może mieć argumenty wejściowe (dowolna ilość) i zwracać wyniki (dowolna ilość).

budowa maszynki

Kluczem hasha labels jest nazwa etykiety, a wartością numer linijki. Wyliczane jest to raz, po uruchomieniu programu (CALL, JZ). Zmienna ip (instruction pointer) określa numer aktualnie wykonywanej linijki.

Odpowiednikiem sprzętowego stosu w CPU jest data_stack. Tablica variables przechowuje zmienne, dodatkowe pola, razem z zmienną vsp pozwalają usuwać zmienne, do które wyszły z zasięgu oraz odkładać zawartość zmiennych podczas skoku do innej procedury. Stos functions zawiera ip z których skoczyliśmy do procedury, tak, by po osiągnięciu jej końca można było wrócić do poprzedniej. Służy to też zatrzymaniu działania aplikacji po zakończeniu głownej procedury.

   1  import re
   2  import sys
   3  from instructiondecoder import InstructionDecoder
   4  from alu import ALU
   5  
   6  class VirtualMachine:
   7      def __init__(self):
   8          self.ins_dec = InstructionDecoder()
   9          self.alu = ALU()
  10          self.data_stack = []
  11          self.variables = []
  12          self.instructions = []
  13          self.functions = []
  14          self.labels = {} # label -> instruction index
  15          self.ip = 0 # instruction pointer
  16          self.vsp = 0 # base variable stack pointer
  17          self.commands = {
  18              'push':  lambda args: self._push(args),
  19              'load':  lambda args: self._load(args),
  20              'store': lambda args: self._store(args),
  21              'jz':    lambda args: self._jump_if_zero(args), 
  22              'call':  lambda args: self._call(args),
  23              'ret':   lambda args: self._return(args),
  24              'int':   lambda args: self._interrupt(args)
  25          }
  26  
  27      def run(self, program):
  28          self.instructions = program
  29          self.get_labels()
  30          self.ip = self.labels['main']+1
  31          self.variables.append(0)
  32          self.vsp = 0
  33          self.functions.append(-1)
  34          self._move_to_next_instruction()
  35  
  36      def get_labels(self):
  37          islabel = re.compile(r"[a-z][a-z0-9]*:")
  38          index = 0
  39          for instruction in self.instructions:
  40              if islabel.match(instruction):
  41                  instruction = instruction[:-1] # remove ':'
  42                  self.labels[instruction] = index
  43              index+=1
  44  
  45      def _move_to_next_instruction(self):
  46          instruction = self.instructions[self.ip]
  47          parts = self.ins_dec.get_instruction_parts(instruction)
  48          if parts:
  49              self._instruction_execute(parts[0], parts[1:]) 
  50          if self.ip > -1:
  51              self.ip+=1
  52              self._move_to_next_instruction()
  53  
  54      def _instruction_execute(self, command, args):
  55          # labels can't be executed, they only point places in code
  56          if re.compile(r"[a-z][a-z0-9]*:").match(command):
  57              return
  58          alu_mnem = self.alu.get_mnemonics() 
  59          if re.compile(alu_mnem).match(command):
  60              self.alu.execute(command, self.data_stack)
  61          else:
  62              self.commands[command](args)
  63  
  64      def _call(self, argumments):
  65          (label) = (argumments[0])
  66          self.functions.append(self.ip)
  67          self.ip = self.labels[label]
  68          self.variables.append(self.vsp)
  69          self.vsp = len(self.variables)
  70  
  71      def _return(self, argumments):
  72          self.ip = self.functions.pop()
  73          self.vsp = self.variables[self.vsp-1]
  74  
  75      def _interrupt(self, argumments):
  76          (interrupt_id) = (argumments[0])
  77          if int(interrupt_id)==0:
  78              print '-> %s' % self.data_stack.pop()
  79          elif int(interrupt_id)==1:
  80              v = int(sys.stdin.readline())
  81              self.data_stack.append(v)
  82  
  83      def _push(self, argumments):
  84          (value) = (argumments[0])
  85          self.data_stack.append(value)
  86  
  87      def _store(self, argumments):
  88          (index) = (argumments[0])
  89          index = int(index)
  90          val = self.data_stack.pop()
  91          if index+self.vsp+1 > len(self.variables):
  92               self.variables.append(val)
  93          else:
  94              self.variables[index+self.vsp]=val
  95  
  96      def _load(self, argumments):
  97          (index) = (argumments[0])
  98          index = int(index) #+ self.vsp
  99          self.data_stack.append(self.variables[index+self.vsp])
 100  
 101      def _jump_if_zero(self, argumments):
 102          (label) = (argumments[0])
 103          if int(self.data_stack.pop())==0:
 104              self.ip = self.labels[label]
 105  

Dekoder instrukcji oparty jest o automat skończony. W prezentowanym tu rozwiązaniu można zrezygnować z maszyny stanów (np. na rzecz wyrażeń regularnych), lecz jej wykorzystanie zapewnia dużą elastyczność i łatwy rozwój. Każdą taką maszynę charakteryzuje tabela przejść (którą można zaprezentować jako graf, lecz to jest IMHO trudniejsze w analizie). Modyfikacją standardowego algorytmu jest tutaj dodanie dla każdego przejścia jednej z poniższych czynności:

  • porcja danych (w naszej VM znak ASCII) dokładna jest do buforu (oznaczymy jako ↓)
  • zawartość buforu przenoszona jest do listy przetworzonych wartości (oznaczymy jako ↑)
  • nic

Przy poprawnej tabeli stanów oraz poprawnie przyporządkowanych czynności każdemu z przejść trafimy do stanu błąd (tu nie zaimplementowany, pewnie kod się po prostu ordynarnie posypie) lub do stanu koniec, a w liście przetworzonych wartości będziemy mieli części mnemonika. Przykładowo z load 0 będziemy mieć [load, 0].

Tabela stanów
wejście \ wyjście init number keyword separator end
init [0-9] ↓ [a-z] ↓ spacja ; lub koniec stringa
number [0-9.] ↓ spacja ; lub koniec stringa
keyword [0-9] ↓ [a-z:] ↓ spacja ; lub koniec stringa
separator [0-9] ↓ [a-z] ↓
end
   1  import re
   2  
   3  class InstructionDecoder:
   4      def __init__(self):
   5          ''
   6  
   7      def get_instruction_parts(self, instruction):
   8          p = self._split(instruction, 0, [], '')
   9          if not p or (len(p)==1 and p[0]==''):
  10              return None
  11          return p
  12  
  13      def _split(self, instruction, state, output, buff):
  14          '''its a finite state machine, see documentation'''
  15          (char, instruction) = self._get_char(instruction)
  16  
  17          # init
  18          if state==0:
  19              # init -> number (append)
  20              if re.compile(r"[0-9]").match(char):
  21                  buff+=char
  22                  return self._split(instruction, 1, output, buff)
  23              # init -> keyword (append)
  24              if re.compile(r"[a-z]").match(char):
  25                  buff+=char
  26                  return self._split(instruction, 2, output, buff)
  27              # int -> separator (skip)
  28              if char==' ':
  29                  return self._split(instruction, 0, output, buff)
  30              # init -> end (skip)


  31              if re.compile(r"[;]").match(char) or char=='\0':
  32                  return self._split(instruction, 4, output, buff)
  33  
  34          # number
  35          if state==1:
  36              # number -> number (append)
  37              if re.compile(r"[0-9.]").match(char):
  38                  buff+=char
  39                  return self._split(instruction, 1, output, buff)
  40              # number -> separator (flush)
  41              if char==' ':
  42                  output.append(buff)
  43                  buff=''
  44                  return self._split(instruction, 3, output, buff)
  45              # number -> end (skip)
  46              if re.compile(r"[;]").match(char) or char=='\0':
  47                  return self._split(instruction, 4, output, buff)
  48           
  49          # keyword
  50          if state==2:
  51              # keyword -> keyword (append)
  52              if re.compile(r"[a-z:]").match(char):
  53                  buff+=char
  54                  return self._split(instruction, 2, output, buff)
  55              # keyword -> number (append)
  56              if re.compile(r"[0-9]").match(char):
  57                  buff+=char
  58                  return self._split(instruction, 2, output, buff)
  59              # keyword -> separator (flush)
  60              if char==' ':
  61                  output.append(buff)
  62                  buff=''
  63                  return self._split(instruction, 0, output, buff)
  64              # keyword -> end (skip)
  65              if re.compile(r"[;]").match(char) or char=='\0':
  66                  return self._split(instruction, 4, output, buff)
  67  
  68          # separator
  69          if state==3:
  70               # separator -> number (apped)
  71               if re.compile(r"[0-9]").match(char):
  72                  buff+=char
  73                  return self._split(instruction, 1, output, buff)
  74               # separator -> keyword (append)
  75               if re.compile(r"[a-z]").match(char):
  76                  buff+=char
  77                  return self._split(instruction, 2, output, buff)
  78  
  79          # end
  80          if state==4:
  81              output.append(buff)
  82              return output
  83  
  84      def _get_char(self, instruction):
  85          if not instruction:
  86              return ('\0', '\0')
  87          char = instruction[0]
  88          if len(instruction)>1:
  89              instruction = instruction[1:]

  90          else:
  91              instruction = '\0'
  92          return (char, instruction)
  93  

Zadaniem ALU jest obsługa operacji arytmetycznych oraz logicznych. Zarówno dane jak i wyniki zapisywane są na stosie.

   1  class ALU:
   2      def __init__(self):
   3          self.commands = { 
   4              # s means stack
   5              'add':   lambda s: self._math_opr(lambda a,b: b+a, s),
   6              'sub':   lambda s: self._math_opr(lambda a,b: b-a, s),
   7              'mul':   lambda s: self._math_opr(lambda a,b: b*a, s),
   8              'div':   lambda s: self._math_opr(lambda a,b: b/a, s),
   9              'and':   lambda s: self._math_opr(lambda a,b: b&a, s),
  10              'or':    lambda s: self._math_opr(lambda a,b: b|a, s),
  11              #'lshr':  lambda s: self._math_opr(lambda a,b: b<<a, s),
  12              #'rshr':  lambda s: self._math_opr(lambda a,b: b>>a, s),
  13          }
  14  
  15      def get_mnemonics(self):
  16          ans = ''
  17          for k in self.commands:
  18              ans += '%s|' % k
  19          return '(' + ans[:-1] + ')'  # [:-1] removes last '|' sign 
  20  
  21      def execute(self, command, stack):
  22          self.commands[command](stack)
  23  
  24      def _math_opr(self, operation, stack):
  25          a = stack.pop()
  26          b = stack.pop()
  27          result = operation(float(a), float(b))
  28          stack.append(result)
  29  

Dokumentacja

Lista operacji (nie licząc związanych z ALU)
Nazwa Opis Kod Operacja
push odłożenie elementu na stos push S ↓S
load odłożenie elementu z pamięci głównej na stos load S1 MEMMORY(S1)→S2
↓S2
store zapis w podanym miejscu pamięci wartości pobranej ze stosu store S1 ↑S2
S2→MEMMORY(S1)
jz sprawdzenie czy wartość pobrana ze stosu jest równa 0, jeżeli tak skok do etykiety podanej jako argument jz S1 ↑S2
IF S2==0 →LABEL(S1)
call Przechodzi do wykonywania instrukcji pod etykietą podaną jako argument. Od JZ różni się tym, że z po skończeniu wykonywania kodu pod w/w labelką można wrócić. call S1 →LABEL(S1)
ret Znak końca procedury, wykonywanie kodu powraca do następnej instrukcji w poprzedniej procedurze. ret
nazwa-etykiety: identyfikator miejsca przez który można się przenieść poprzez instrukcje skoku (JZ) lub etykieta procedury, do której można się przenieść przez CALL. S:
int Przerwanie - zatrzymuje działanie programu i przechodzi do wykonania procedury o identyfikatorze podanym jako argument. Lista identyfikatorów tych przerwań podana jest w osobnej rozpisce. int S
Lista instrukcji ALU
add dodawanie ↑S1
↑S2
↓(S1+S2)
sub odejmowanie ↑S1
↑S2
↓(S1-S2)
mul mnożenie ↑S1
↑S2
↓(S1*S2)
div dzielenie ↑S1
↑S2
↓(S1/S2)
and iloczyn logiczny ↑S1
↑S2
↓(S1 & S2)
or suma logiczna ↑S1
↑S2
↓(S1 | S2)
lshr [Nie zaimplementowane] lewy shift
rshr [Nie zaimplementowane] prawy shift
Lista przerwań
Numer Opis
0 Ściąga element ze stosu i wyświetla na STDOUT
1 Znaki z STDIN, aż do napotkania znaku nowej linii odkłada jako element na stos
2 [Nie zaimplementowane] Odkłada na stos losową liczbę z przedziału <0,1>

Instalacja oraz uruchomienie

Do uruchomienia konieczny jest interpreter Pythona. W linii poleceń uruchamiamy skrypt main.py, jako parametr podając ścieżkę do kodu, który ma być wykonany.

Przykładowe kody źródłowe

Program wypisujący objętość sześcianu, jako argument pobiera długość boku:
main:
    int 1
    call square
    int 0
ret

square:
    store 0
    load 0
    load 0
    mul
    load 0
    mul
ret
rgawron@vk1004:/tmp/vm$ ./main.py cube.asm 
3
-> 27.0

Program wypisujący n (n=17) liczb Fibonacciego. Liczby te określa ciąg:

F(0) = 1
F(1) = 1
F(n) = F(n-1) + F(n-2)

Dla przejrzystości kodu nie wypisywany jest zerowy element. Jak trzeba przerobić kod by zamiast 17 wyświetlana była ilość liczb podawana przez usera (odczytywana z klawiatury)?

; foo(x(n-1), x(n-2), n)
fib:
    ; x(n-1)
    store 0
    ; x(n-2)
    store 1
    ; n
    store 2

    ; print x(n-1)
    load 0
    int 0
    ; n = n - 1
    load 2
    push 1
    sub
    store 2
    ; if n equival 0 return
    load 2
    jz a00
    ; assign new n value    
    load 2
    ; assign x(n-2) as x(n-1)
    load 0
    ; count x(n-1)
    load 0
    load 1
    add
    ; count nex element
    call fib
    ; never been here 
a00:
    ; empty
ret

main:
    push 17
    push 0
    push 1
    call fib; foo(1,1,17)
ret
rgawron@vk1004:/tmp/vm$ ./main.py ./fib.asm 
-> 1
-> 1.0
-> 2.0
-> 3.0
-> 5.0
-> 8.0
-> 13.0
-> 21.0
-> 34.0
-> 55.0
-> 89.0
-> 144.0
-> 233.0
-> 377.0
-> 610.0
-> 987.0
-> 1597.0

Poniżej zaprezentowany jest program, w którym procedura foo może przyjmować dowolną ilość argumentów.

foo:
a00:
    store 0
    load 0
    jz a01

    int 0

    load 0
    push 1
    sub

    push 0
    jz a00
a01:
    ; empty
ret

main:
    push 11
    push 31
    push 2
    call foo

    push 100
    push 1000
    push 1
    push 3
    call foo

    push 0
    call foo
ret
rgawron@foo:~/fire/vm$ ./main.py free.asm
-> 31
-> 11
-> 1
-> 1000
-> 100

Bardziej skomplikowane przykłady będą w następnym poście poświęconym nowemu językowi programowania, który będzie kompilowany do tego asemblera. Inny kod prostej maszyny wirtualnej można znaleźć na blogu Indefinite Studies.

Materiały do pobrania

Link do źródeł maszyny wirtualnej oraz w/w przykładowych programów.

Pingbacks

No pingbacks yet

Comments

avatar
czarodziej , 1.05.2009 8:35, reply

jej, powazna sprawa, szkoda ze malo wiem o tym i nie znam pythona, ale sam post napewno warto ze sie pojawil w internecie. Moze powinienes dac go na jakis bardziej poczytny serwis? Mysle ze wielu moze on rozjasnic sprawe. Ej, i pakujesz w rar… ;)

avatar
czarodziej , 1.05.2009 8:36, reply

a nie, zle odczytalem! przepraszam, tar bylo.

avatar
czarodziej , 1.05.2009 8:58, reply

ojj, ja sie dziwie czemu nie moge nic swojego napisac, a tu po prostu musza byc spacje a nie tab… co do pytania o 17 nr ciagu fibb, to push 17 trzeba zamienic z int 1. :)

avatar
czarodziej , 1.05.2009 9:25, reply

.

  main:                                                                       
push 1
int 1
call silnia
int 0
ret
silnia:
store 0
load 0
jz koniec
load 0
mul
load 0
push 1
sub
call silnia
koniec:
ret

moja tworczosc w twoim asemblerze :)

avatar
Robert , 1.05.2009 10:48, reply

Przede wszystkim dzięki za uwagi. Całość wystawię gdy obie części będą gotowe, zrobię też projektowi stronę na googlecode czy czymś podobnym. Na razie jest IMHO za mało treści, kodu, funkcjonalności by dzielić się tym z większym gronem.

Cieszę się, że ktoś napisał kod w tym asmie i działa, pomysł z tabami IMHO jest dobry, dodam niedługo.

Formatowanie kodu poprawiłem (na ile umiałem). Markdown jest beznadziejne :|

Wracając do GG, czym jest schema? Ja nie wiem, a google mówi że to wiele różnych rzeczy :)

avatar
czarodziej , 1.05.2009 11:37, reply

schema to miala byc moja proba odmiany slowa scheme — dialekt lispa, jak na szybko rzucilem okiem pomyslalem ze jak tworzenie wlasnego jezyka to scheme — bo sa nawet gotowe tutoriale na sieci naj napisac wlasna implementacje, ale widze ze robisz cos o wiele ciekawszego :)

avatar
Robert , 1.05.2009 11:46, reply

OK, zobaczę. O czymś Lispowatym myślałem, nawet semantykę takiego języka napisałem ale brzydkie to było. Teraz myślę o czymś o składni/semantyce na wzór C, Pythona (bez obiektowości) i Erlanga.

avatar
czarodziej , 1.05.2009 22:45, reply

napisalem vm’a do twojego asma w C, jutro jeszcze pare poprawek zrobie i wysle ci kod :)

avatar
czarodziej , 2.05.2009 8:31, reply

http://www.dominik.unixstorm.org/vm.tar.gz

wiec tak, istrukcja call wg twojego opisu zdejmuje ze stosu wartosc sprawdza czy 0 i skacze, nie wiem czy tak mialo byc ale ja sprawdzania i zdejmowania ze stosu przy callu nie zrobilem.

dodalem jeszcze instrukcje mod — dzielenie modulo i jmp skok bez sprawdzanai czegokolwiek bezpowrotny

avatar
Robert , 2.05.2009 10:10,

Zaskoczyłeś mnie, nie myślałem, że komuś się będzie chciało pisać implementację tego. Na jakiej to jest licencji? Mogło by być domyślne zamiast kodu w pythonie no i mógłbyś napisać tu posta gośćinnego o tym:) Z ciekawości zrobię jakieś porównanie jak szybko oba działają:)

Co do call’a, racja, pozostałość po starej koncepcji, zaraz poprawię.

Leave your reply

Let me know what you think

Required. 30 chars of fewer.

Required.

captcha image

avatar
Robert , 2.05.2009 10:31, reply

BTW co myślicie o języku w stylu czegoś poniższego?

#  example program written in xxx
# http://rgawron.megiteam.pl/
def fib (n x0 x1) {
ass(tmp add(x0 x1))
ass(x0 x1)
ass(x1 tmp)
# display new element on the screen
say(tmp)
# n--
ass(n sub(n 1))
# if n>0 count next element
if(gtz(n) fib(n x0 x1))
}

def main () {
ask(n)
fibb(n 0 1)
}

Takie trochę C, tylko bez przecinków i średników =) If’y bez ciała tylko z wywołaniem funkcji. Bez pętli! Funkcje mogły by być zagnieżdżone (jak w Pythonie). Inspirowałem się tą książka:

http://www.math.chalmers.se/~rjmh/Papers/whyfp.pdf

avatar
czarodziej , 2.05.2009 11:15, reply

hmm, troche byloby roboty przy prasowaniu tego kodu, z tym wlasnie podoba mi sie lisp ze cala skladnia sprowadza sie do prostego:

(funkcja parametry)

(if (funkcja) (jesli-tak) (jesli-nie)) itd.

a moj kod jest bez licencji bo i po co? :)