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 add
instruçã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):
- SP aponta para o topo da pilha (o ponteiro da pilha ).
- MP marca o início do quadro de pilha ativo (o ponteiro de marca ).
- EP aponta para o local de pilha mais alto usado no procedimento atual (o ponteiro extremo ).
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 n
especifica 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 ( n
sendo o número de parâmetros, p
o 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, i
essencialmente especifica o espaço reservado para locais (mais o número de parâmetros mais 5) e j
fornece 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 C
fornecer 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 q
pode 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:
- aceso 0, a : carga constante a
- opr 0, a : executa a operação a (13 operações: RETORNO, 5 funções matemáticas e 7 funções de comparação)
- lod l , a : carga variável l, a
- sto l , a : armazenar a variável l, a
- cal l , a : procedimento de chamada a no nível l
- int 0, a : incrementa o registrador t por um
- jmp 0, a : pule para um
- 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
- Apple Integer Sweet 16
- Bytecode
- Joel McCormack , designer da versão da NCR Corporation da máquina de código-p
- LLVM IR
- Microsoft P-Code
- Sistema de tempo de execução
- Segmentação de token
Referências
Leitura adicional
- Pemberton, Steven ; Daniels, Martin. Implementação Pascal: O Compilador e Interpretador P4 . John Wiley . ISBN 0-13-653031-1.
- Pemberton, Steven , ed. (13/04/2011). "Implementação Pascal: um livro e fontes" .(NB. Possui fontes Pascal do compilador e interpretador P4 , instruções de uso.)
- Pemberton, Steven , ed. (13/04/2011). "pcode do compilador Pascal conforme compilado por ele mesmo" .(NB. Possui o código-p do compilador P4 , gerado por ele mesmo.)
- "A página do Jefferson Computer Museum no UCSD p-System" .
- "Implementação de código aberto" ., incluindo empacotamento e binários pré-compilados; uma bifurcação amigável do Klebsch. “Implementação de Klebsch” .
- Terry, Pat (2005). Compilando com C # e Java . p. 624. ISBN 0-321-26360-X.
- Wirth, Niklaus (1975). Algoritmos + Estruturas de Dados = Programas . ISBN 0-13-022418-9.
- Wirth, Niklaus (1996). Construção do compilador . ISBN 0-201-40353-6.
- Liffick, Blaise W., ed. (1979). O Livro Byte de Pascal . ISBN 0-07-037823-1.
- Barron, David William , ed. (1981). Pascal: a linguagem e sua implementação . ISBN 0-471-27835-1.(NB. Veja especialmente os artigos Pascal-P Implementation Notes e Pascal-S: A Subset and its Implementation .)