macchina con codice p - p-code machine

Nella programmazione di computer , una macchina p-code ( macchina a codice portatile ) è una macchina virtuale progettata per eseguire p-code (il linguaggio assembly o il codice macchina di un'ipotetica unità di elaborazione centrale (CPU)). Questo termine viene applicato sia genericamente a tutte queste macchine (come la Java virtual machine (JVM) e il codice precompilato MATLAB ), sia a specifiche implementazioni, la più famosa è la p-Machine del sistema Pascal-P , in particolare la UCSD Pascal implementazione, tra i cui sviluppatori, la p in p-code è stata interpretata per significare pseudo più spesso di portable , quindi pseudo-codice significa istruzioni per una pseudo-macchina.

Sebbene il concetto sia stato implementato per la prima volta intorno al 1966 (come codice O per il linguaggio di programmazione combinato di base ( BCPL ) e codice P per il linguaggio Euler ), il termine codice p è apparso per la prima volta nei primi anni '70. Due dei primi compilatori che generano codice p furono il compilatore Pascal-P nel 1973, di Kesav V. Nori, Urs Ammann, Kathleen Jensen, Hans-Heinrich Nägeli e Christian Jacobi, e il compilatore Pascal-S nel 1975, di Niklaus Wirth .

I programmi che sono stati tradotti in p-code possono essere interpretati da un programma software che emula il comportamento dell'ipotetica CPU o tradotti nel codice macchina della CPU su cui il programma deve essere eseguito e quindi eseguito. Se c'è un interesse commerciale sufficiente, può essere costruita un'implementazione hardware della specifica della CPU (ad esempio, il Pascal MicroEngine o una versione di un processore Java ).

Vantaggi e punti deboli dell'implementazione del codice p

Rispetto alla traduzione diretta in codice macchina nativo , un approccio in due fasi che prevede la traduzione in codice p e l'esecuzione mediante interpretazione o compilazione just-in-time (JIT) offre diversi vantaggi.

  • È molto più semplice scrivere un piccolo interprete p-code per una nuova macchina piuttosto che modificare un compilatore per generare codice nativo per la stessa macchina.
  • La generazione del codice macchina è una delle parti più complicate della scrittura di un compilatore. In confronto, la generazione di p-code è molto più semplice perché nessun comportamento dipendente dalla macchina deve essere considerato nella generazione del bytecode . Questo lo rende utile per far funzionare rapidamente un compilatore.
  • Poiché p-code si basa su una macchina virtuale ideale, un programma p-code è spesso molto più piccolo dello stesso programma tradotto in codice macchina.
  • Quando il p-code viene interpretato, l'interprete può applicare controlli di runtime aggiuntivi che sono difficili da implementare con il codice nativo.

Uno degli svantaggi significativi del p-code è la velocità di esecuzione, che a volte può essere risolta tramite la compilazione JIT. Il codice P è spesso anche più facile da decodificare rispetto al codice nativo.

All'inizio degli anni '80, almeno due sistemi operativi hanno raggiunto l'indipendenza dalla macchina attraverso un uso estensivo del codice p. Il Business Operating System (BOS) era un sistema operativo multipiattaforma progettato per eseguire esclusivamente programmi p-code. L' UCSD p-System , sviluppato presso l'Università della California, San Diego, era un sistema operativo autocompilante e auto-hosting basato su codice p ottimizzato per la generazione dal linguaggio Pascal .

Negli anni '90, la traduzione in p-code è diventata una strategia popolare per le implementazioni di linguaggi come Python , Microsoft P-Code in Visual Basic e Java bytecode in Java .

Il linguaggio Go utilizza un assembly generico e portatile come forma di p-code, implementato da Ken Thompson come estensione del lavoro su Plan 9 dei Bell Labs . A differenza del bytecode CLR ( Common Language Runtime ) o del bytecode JVM, non esiste una specifica stabile e gli strumenti di compilazione Go non emettono un formato bytecode da utilizzare in un secondo momento. L'assemblatore Go usa il linguaggio assembly generico come rappresentazione intermedia e gli eseguibili Go sono binari collegati staticamente specifici della macchina .

UCSD p-Machine

Architettura

Come molte altre macchine p-code, la p-Machine UCSD è una macchina stack , il che significa che la maggior parte delle istruzioni prende i propri operandi da uno stack e rimette i risultati nello stack. Pertanto, l' addistruzione sostituisce i due elementi più in alto dello stack con la loro somma. Alcune istruzioni richiedono un argomento immediato. Come Pascal, il p-code è fortemente tipizzato , supportando in modo nativo i tipi di dati boolean (b), character (c), integer (i), real (r), set (s) e pointer (a) .

Alcune semplici istruzioni:

Insn.   Stack   Stack   Description
        before  after
 
adi     i1 i2   i1+i2   add two integers
adr     r1 r2   r1+r2   add two reals
inn     i1 s1   is1     set membership; b1 = whether i1 is a member of s1
ldi     i1 i1   i1      load integer constant
mov     a1 a2   a2      move
not     b1 b1   -b1     boolean negation

Ambiente

A differenza di altri ambienti basati su stack (come Forth e la macchina virtuale Java ) ma molto simile a una CPU di destinazione reale, il p-System ha un solo stack condiviso da frame di stack di procedure (che forniscono l' indirizzo di ritorno , ecc.) e gli argomenti per istruzioni locali. Tre dei registri della macchina puntano nello stack (che cresce verso l'alto):

  • SP punta alla cima dello stack (il puntatore dello stack ).
  • MP segna l'inizio dello stack frame attivo (il puntatore mark ).
  • EP punta alla posizione dello stack più alta utilizzata nella procedura corrente (il puntatore estremo ).

È presente anche un'area costante e, al di sotto di essa, l' heap che cresce verso lo stack. Il registro NP (il nuovo puntatore ) punta alla parte superiore (l'indirizzo utilizzato più basso) dell'heap. Quando EP diventa maggiore di NP, la memoria della macchina è esaurita.

Il quinto registro, PC, punta all'istruzione corrente nell'area del codice.

Convenzioni di chiamata

I frame stack hanno questo aspetto:

EP ->
      local stack
SP -> ...
      locals
      ...
      parameters
      ...
      return address (previous PC)
      previous EP
      dynamic link (previous MP)
      static link (MP of surrounding procedure)
MP -> function return value

La sequenza di chiamata della procedura funziona come segue: La chiamata viene introdotta con

 mst n

dove nspecifica la differenza nei livelli di annidamento (ricordate che Pascal supporta le procedure annidate). Questa istruzione segnerà lo stack, cioè riserverà le prime cinque celle del precedente stack frame, e inizializzerà il precedente collegamento EP, dinamico e statico. Il chiamante quindi calcola e invia qualsiasi parametro per la procedura, quindi emette

 cup n, p

per chiamare una procedura utente ( nessendo il numero di parametri, pl'indirizzo della procedura). Questo salverà il PC nella cella dell'indirizzo di ritorno e imposterà l'indirizzo della procedura come nuovo PC.

Le procedure per l'utente iniziano con le due istruzioni

 ent 1, i
 ent 2, j

Il primo imposta SP su MP+ i, il secondo imposta EP su SP+ j. Quindi ispecifica essenzialmente lo spazio riservato ai locali (più il numero di parametri più 5) e jfornisce il numero di voci necessarie localmente per lo stack. A questo punto viene controllato l'esaurimento della memoria.

Il ritorno al chiamante avviene tramite

 retC

con Cdando il tipo di ritorno (i, r, c, b, una come sopra, e p per nessun valore di ritorno). Il valore restituito deve essere precedentemente memorizzato nella cella appropriata. Su tutti i tipi tranne p, la restituzione lascerà questo valore nello stack.

Invece di chiamare una procedura utente (cup), la procedura standard qpuò essere chiamata con

 csp q

Queste procedure standard sono procedure Pascal come readln()( csp rln), sin()( csp sin), ecc. Peculiarmente eof()è invece un'istruzione p-Code.

Esempio di macchina

Niklaus Wirth ha specificato una semplice macchina p-code nel libro del 1976 Algorithms + Data Structures = Programs . La macchina aveva 3 registri: un contatore di programma p , un registro di base b e un registro di cima dello stack t . C'erano 8 istruzioni:

  1. acceso 0, a  : costante di carico a
  2. opr 0, a  : esegue l'operazione a (13 operazioni: RETURN, 5 funzioni matematiche e 7 funzioni di confronto)
  3. lod l , a  : variabile di carico l,a
  4. sto l , a  : memorizza la variabile l, a
  5. cal l , una  chiamata di procedura una a livello l
  6. int 0, a  : incrementa il registro t di a
  7. jmp 0, a  : salta ad a
  8. jpc 0, a  : salto condizionato ad a

Questo è il codice della macchina, scritto in Pascal:

const
	amax=2047;      {maximum address}
	levmax=3;       {maximum depth of block nesting}
	cxmax=200;      {size of code array}

type 
	fct=(lit,opr,lod,sto,cal,int,jmp,jpc);
	instruction=packed record 
		f:fct;
		l:0..levmax;
		a:0..amax;
	end;

var
	code: array [0..cxmax] of instruction;

procedure interpret;

  const stacksize = 500;

  var
    p, b, t: integer; {program-, base-, topstack-registers}
    i: instruction; {instruction register}
    s: array [1..stacksize] of integer; {datastore}

  function base(l: integer): integer;
    var b1: integer;
  begin
    b1 := b; {find base l levels down}
    while l > 0 do begin
      b1 := s[b1];
      l := l - 1
    end;
    base := b1
  end {base};

begin
  writeln(' start pl/0');
  t := 0; b := 1; p := 0;
  s[1] := 0; s[2] := 0; s[3] := 0;
  repeat
    i := code[p]; p := p + 1;
    with i do
      case f of
        lit: begin t := t + 1; s[t] := a end;
        opr: 
          case a of {operator}
            0: 
              begin {return}
                t := b - 1; p := s[t + 3]; b := s[t + 2];
              end;
            1: s[t] := -s[t];
            2: begin t := t - 1; s[t] := s[t] + s[t + 1] end;
            3: begin t := t - 1; s[t] := s[t] - s[t + 1] end;
            4: begin t := t - 1; s[t] := s[t] * s[t + 1] end;
            5: begin t := t - 1; s[t] := s[t] div s[t + 1] end;
            6: s[t] := ord(odd(s[t]));
            8: begin t := t - 1; s[t] := ord(s[t] = s[t + 1]) end;
            9: begin t := t - 1; s[t] := ord(s[t] <> s[t + 1]) end;
            10: begin t := t - 1; s[t] := ord(s[t] < s[t + 1]) end;
            11: begin t := t - 1; s[t] := ord(s[t] >= s[t + 1]) end;
            12: begin t := t - 1; s[t] := ord(s[t] > s[t + 1]) end;
            13: begin t := t - 1; s[t] := ord(s[t] <= s[t + 1]) end;
          end;
        lod: begin t := t + 1; s[t] := s[base(l) + a] end;
        sto: begin s[base(l)+a] := s[t]; writeln(s[t]); t := t - 1 end;
        cal: 
          begin {generate new block mark}
            s[t + 1] := base(l); s[t + 2] := b; s[t + 3] := p;
            b := t + 1; p := a
          end;
        int: t := t + a;
        jmp: p := a;
        jpc: begin if s[t] = 0 then p := a; t := t - 1 end
      end {with, case}
  until p = 0;
  writeln(' end pl/0');
end {interpret};

Questa macchina è stata utilizzata per eseguire PL/0 di Wirth , un compilatore di sottoinsiemi Pascal utilizzato per insegnare lo sviluppo del compilatore.

Guarda anche

Riferimenti

Ulteriori letture