máquina de código p - p-code machine

Na programação de computadores , uma máquina de código-p ( máquina de código portátil ) é uma máquina virtual projetada para executar o código-p (a linguagem assembly ou código de máquina de uma unidade de processamento central hipotética (CPU)). Este termo é aplicado genericamente a todas essas máquinas (como a máquina virtual Java (JVM) e o código pré-compilado MATLAB ) e a implementações específicas, sendo a mais famosa a p-Machine do sistema Pascal-P , particularmente o UCSD Pascal implementação, entre cujos desenvolvedores, op em p-código foi interpretado para significar pseudo mais frequentemente do que portátil , portanto, pseudo-código significando instruções para uma pseudo-máquina.

Embora o conceito tenha sido implementado pela primeira vez por volta de 1966 (como código O para a Basic Combined Programming Language ( BCPL ) e código P para a linguagem Euler ), o termo p-code apareceu pela primeira vez no início dos anos 1970. Dois primeiros compiladores gerando p-código foram o compilador Pascal-P em 1973, por Kesav V. Nori, Urs Ammann, Kathleen Jensen, Hans-Heinrich Nägeli e Christian Jacobi, e o compilador Pascal-S em 1975, por Niklaus Wirth .

Os programas que foram traduzidos para código-p podem ser interpretados por um programa de software que emula o comportamento da CPU hipotética ou traduzidos para o código de máquina da CPU na qual o programa deve ser executado e, em seguida, executado. Se houver interesse comercial suficiente, uma implementação de hardware da especificação da CPU pode ser construída (por exemplo, o Pascal MicroEngine ou uma versão de um processador Java ).

Benefícios e fraquezas da implementação de p-code

Em comparação com a tradução direta em código de máquina nativo , uma abordagem de dois estágios envolvendo tradução em código p e execução por interpretação ou compilação just-in-time (JIT) oferece várias vantagens.

  • É muito mais fácil escrever um pequeno interpretador de código p para uma nova máquina do que modificar um compilador para gerar código nativo para a mesma máquina.
  • Gerar código de máquina é uma das partes mais complicadas de escrever um compilador. Por comparação, gerar o código-p é muito mais fácil porque nenhum comportamento dependente da máquina deve ser considerado na geração do bytecode . Isso o torna útil para colocar um compilador em funcionamento e rapidamente.
  • Como o código-p é baseado em uma máquina virtual ideal, um programa de código-p geralmente é muito menor do que o mesmo programa traduzido para o código de máquina.
  • Quando o código-p é interpretado, o interpretador pode aplicar verificações de tempo de execução adicionais que são difíceis de implementar com código nativo.

Uma das desvantagens significativas do código-p é a velocidade de execução, que às vezes pode ser corrigida por meio da compilação JIT. Freqüentemente, o código P também é mais fácil de fazer engenharia reversa do que o código nativo.

No início da década de 1980, pelo menos dois sistemas operacionais alcançaram independência de máquina por meio do uso extensivo de p-code. O Business Operating System (BOS) era um sistema operacional de plataforma cruzada projetado para executar programas de código-p exclusivamente. O UCSD p-System , desenvolvido na The University of California, San Diego, era um sistema operacional de auto-compilação e auto-hospedagem baseado em p-code otimizado para geração pela linguagem Pascal .

Na década de 1990, a tradução em código-p tornou-se uma estratégia popular para implementações de linguagens como Python , Microsoft P-Code em Visual Basic e bytecode Java em Java .

A linguagem Go usa um assembly genérico e portátil como uma forma de código-p, implementado por Ken Thompson como uma extensão do trabalho no Plan 9 da Bell Labs . Ao contrário do bytecode do Common Language Runtime (CLR) ou do bytecode da JVM, não há especificação estável e as ferramentas de construção Go não emitem um formato de bytecode para ser usado posteriormente. O assembler Go usa a linguagem assembly genérica como uma representação intermediária , e os executáveis ​​Go são binários vinculados estaticamente específicos da máquina .

UCSD p-Machine

Arquitetura

Como muitas outras máquinas de código p, a máquina p UCSD é uma máquina de pilha , o que significa que a maioria das instruções retiram seus operandos de uma pilha e colocam os resultados de volta na pilha. Assim, a addinstrução substitui os dois elementos superiores da pilha por sua soma. Algumas instruções levam a um argumento imediato. Como Pascal, o código-p é fortemente tipado , suportando nativamente os tipos de dados booleano (b), caractere (c), inteiro (i), real (r), conjunto (s) e ponteiro (a) .

Algumas instruções simples:

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

Ao contrário de outros ambientes baseados em pilha (como Forth e a máquina virtual Java ), mas muito semelhante a uma CPU de destino real, o p-System tem apenas uma pilha compartilhada por quadros de pilha de procedimento (fornecendo endereço de retorno , etc.) e os argumentos para instruções locais. Três dos registros da máquina apontam para a pilha (que cresce para cima):

Também está presente uma área constante e, abaixo dela, a pilha crescendo em direção à pilha. O registrador NP (o novo ponteiro ) aponta para o topo (endereço usado mais baixo) do heap. Quando EP fica maior que NP, a memória da máquina se esgota.

O quinto registrador, PC, aponta para a instrução atual na área de código.

Convenções de chamada

Os frames da pilha são assim:

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

A sequência de chamada do procedimento funciona da seguinte maneira: A chamada é introduzida com

 mst n

onde nespecifica a diferença nos níveis de aninhamento (lembre-se de que Pascal oferece suporte a procedimentos aninhados). Esta instrução marcará a pilha, isto é, reservará as primeiras cinco células do quadro de pilha acima e inicializará o EP anterior, o link dinâmico e estático. O chamador, então, calcula e envia quaisquer parâmetros para o procedimento e, em seguida, emite

 cup n, p

para chamar um procedimento do usuário ( nsendo o número de parâmetros, po endereço do procedimento). Isso salvará o PC na célula do endereço de retorno e definirá o endereço do procedimento como o novo PC.

Os procedimentos do usuário começam com as duas instruções

 ent 1, i
 ent 2, j

O primeiro define SP para MP + i, o segundo define EP para SP + j. Portanto, iessencialmente especifica o espaço reservado para locais (mais o número de parâmetros mais 5) e jfornece o número de entradas necessárias localmente para a pilha. O esgotamento da memória é verificado neste ponto.

O retorno ao chamador é realizado por meio de

 retC

com Cfornecer o tipo de retorno (i, r, c, b, a como acima e p para nenhum valor de retorno). O valor de retorno deve ser armazenado na célula apropriada previamente. Em todos os tipos, exceto p, retornar deixará esse valor na pilha.

Em vez de chamar um procedimento do usuário (xícara), o procedimento padrão qpode ser chamado com

 csp q

Esses procedimentos padrão são procedimentos Pascal como readln()( csp rln), sin()( csp sin), etc. Em eof()vez disso , Peculiarly é uma instrução p-Code.

Máquina de exemplo

Niklaus Wirth especificou uma máquina de código p simples no livro de 1976 Algorithms + Data Structures = Programs . A máquina tinha 3 registradores - um contador de programa p , um registrador de base b e um registrador de topo da pilha t . Havia 8 instruções:

  1. aceso 0, a  : carga constante a
  2. opr 0, a  : executa a operação a (13 operações: RETORNO, 5 funções matemáticas e 7 funções de comparação)
  3. lod l , a  : carga variável l, a
  4. sto l , a  : armazenar a variável l, a
  5. cal l , a  : procedimento de chamada a no nível l
  6. int 0, a  : incrementa o registrador t por um
  7. jmp 0, a  : pule para um
  8. jpc 0, a  : pula condicional a um

Este é o código da máquina, escrito em 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};

Esta máquina foi usada para executar o PL / 0 de Wirth , um subconjunto de compiladores Pascal usado para ensinar o desenvolvimento de compiladores.

Veja também

Referências

Leitura adicional