Máquina de código P
En programación informática, una máquina de código p (máquina de código portátil) es una máquina virtual diseñada para ejecutar código p (el lenguaje ensamblador o código de máquina de una hipotética unidad central de procesamiento (CPU)). Este término se aplica tanto de forma genérica a todas estas máquinas (como la máquina virtual Java (JVM) y el código precompilado de MATLAB) como a implementaciones específicas, siendo la más famosa la p-Machine del sistema Pascal-P, en particular la UCSD Pascal. implementación, entre cuyos desarrolladores, la p en p-code se interpretó como pseudo más a menudo que portátil, por lo tanto, pseudo-código significa instrucciones para una pseudo-máquina.
Aunque el concepto se implementó por primera vez alrededor de 1966, como código O para el lenguaje de programación combinado básico (BCPL) y código P para el lenguaje Euler, el término código-p apareció por primera vez a principios 1970 Dos de los primeros compiladores que generaron código p fueron el compilador Pascal-P en 1973, por Kesav V. Nori, Urs Ammann, Kathleen Jensen, Hans-Heinrich Nägeli y Christian Jacobi, y el compilador Pascal-S en 1975, por Niklaus Wirth.
Los programas que han sido traducidos a código p pueden ser interpretados por un programa de software que emule el comportamiento de la CPU hipotética, o traducidos al código de máquina de la CPU en la que se ejecutará el programa y luego ejecutado. Si existe suficiente interés comercial, se puede construir una implementación de hardware de la especificación de la CPU (por ejemplo, Pascal MicroEngine o una versión de un procesador Java).
Beneficios y debilidades de implementar código p
En comparación con la traducción directa a código de máquina nativo, un enfoque de dos etapas que implica la traducción a código p y la ejecución mediante interpretación o compilación justo a tiempo (JIT) ofrece varias ventajas.
- Es mucho más fácil escribir un pequeño intérprete de código p para una nueva máquina que modificar un compilador para generar código nativo para la misma máquina.
- Generar código de máquina es una de las partes más complicadas de escribir un compilador. En comparación, generar código p es mucho más fácil porque ningún comportamiento dependiente de la máquina debe ser considerado en la generación del bytecode. Esto hace que sea útil para obtener un compilador y correr rápidamente.
- Puesto que el código p se basa en una máquina virtual ideal, un programa de código p es a menudo mucho más pequeño que el mismo programa traducido al código de máquina.
- Cuando se interpreta el código p, el intérprete puede aplicar cheques adicionales de tiempo de ejecución que son difíciles de implementar con código nativo.
Una de las desventajas significativas del código p es la velocidad de ejecución, que a veces se puede remediar mediante la compilación JIT. El código P a menudo también es más fácil de aplicar ingeniería inversa que el código nativo.
A principios de la década de 1980, al menos dos sistemas operativos lograron la independencia de la máquina mediante el uso extensivo de código p. El Business Operating System (BOS) era un sistema operativo multiplataforma diseñado para ejecutar programas de código p exclusivamente. El p-System de UCSD, desarrollado en la Universidad de California, San Diego, era un sistema operativo de autocompilación y autohospedaje basado en código p optimizado para la generación mediante el lenguaje Pascal.
En la década de 1990, la traducción a código p se convirtió en una estrategia popular para implementaciones de lenguajes como Python, Microsoft P-Code en Visual Basic y Java bytecode en Java.
El lenguaje Go usa un ensamblado genérico y portátil como una forma de código p, implementado por Ken Thompson como una extensión del trabajo en Plan 9 de Bell Labs. A diferencia del código de bytes de Common Language Runtime (CLR) o el código de bytes de JVM, no existe una especificación estable y las herramientas de compilación de Go no emiten un formato de código de bytes para usar más adelante. El ensamblador de Go utiliza el lenguaje ensamblador genérico como una representación intermedia, y los ejecutables de Go son binarios vinculados estáticamente específicos de la máquina.
UCSD p-Máquina
Arquitectura
Al igual que muchas otras máquinas de código p, la UCSD p-Machine es una máquina de pila, lo que significa que la mayoría de las instrucciones toman sus operandos de una pila y colocan los resultados nuevamente en la pila. Por lo tanto, la instrucción add
reemplaza los dos elementos superiores de la pila con su suma. Unas pocas instrucciones toman un argumento inmediato. Al igual que Pascal, el código p está fuertemente tipado y admite tipos de datos booleanos (b), caracteres (c), enteros (i), reales (r), conjuntos (s) y punteros (a) de forma nativa.
Algunas instrucciones sencillas:
Insn. Stack Stack Descripción antes adi i1 i2 i1+i2 añadir dos enteros adr r1 r2 r1+r2 añadir dos reales inn i1 s1 is1 set membership; b1 = whether i1 is a member of s1 Idi i1 i1 carga constante de entero movimiento a1 a2 a2 b1 b1 -b1 negación booleana
Medio ambiente
A diferencia de otros entornos basados en pilas (como Forth y la máquina virtual de Java), pero muy similar a una CPU de destino real, p-System tiene solo una pila compartida por marcos de pilas de procedimientos (que proporcionan la dirección de retorno, etc.) y los argumentos a las instrucciones locales. Tres de los registros de la máquina apuntan a la pila (que crece hacia arriba):
- SP apunta a la parte superior de la pila (el puntero de la pila).
- MP marca el comienzo del marco de la pila activa (el puntero de marca).
- EP apunta a la ubicación de pila más alta utilizada en el procedimiento actual (el puntero extremo).
También está presente un área constante y, debajo de ella, el montón que crece hacia la pila. El registro NP (el nuevo puntero) apunta a la parte superior (dirección utilizada más baja) del montón. Cuando EP es mayor que NP, la memoria de la máquina se agota.
El quinto registro, PC, apunta a la instrucción actual en el área de código.
Convenios de llamadas
Los marcos apilados se ven así:
EP - titulado pila local SP - título... locales ... parámetros ... dirección de retorno (previous PC) EP anterior enlace dinámico (página anterior) enlace estático (MP del procedimiento circundante) MP - Valor de retorno de función
La secuencia de llamada a procedimiento funciona de la siguiente manera: La llamada se introduce con
mst n
donde n
especifica la diferencia en los niveles de anidamiento (recuerde que Pascal admite procedimientos anidados). Esta instrucción marcará la pila, es decir, reservará las primeras cinco celdas del marco de la pila anterior e inicializará el enlace EP, dinámico y estático anterior. Luego, la persona que llama calcula y presiona cualquier parámetro para el procedimiento, y luego emite
taza n, p
para llamar a un procedimiento de usuario (siendo n
el número de parámetros, p
la dirección del procedimiento). Esto guardará la PC en la celda de dirección de retorno y establecerá la dirección del procedimiento como la nueva PC.
Los procedimientos del usuario comienzan con las dos instrucciones
ent 1, i ent 2, j
El primero establece SP en MP + i
, el segundo establece EP en SP + j
. Así que i
esencialmente especifica el espacio reservado para locales (más el número de parámetros más 5), y j
da el número de entradas necesarias localmente para la pila. El agotamiento de la memoria se comprueba en este punto.
Volver a la persona que llama se logra mediante
retC
con C
dando el tipo de retorno (i, r, c, b, a como arriba, y p sin valor de retorno). El valor devuelto debe almacenarse previamente en la celda apropiada. En todos los tipos excepto p, la devolución dejará este valor en la pila.
En lugar de llamar a un procedimiento de usuario (taza), se puede llamar al procedimiento estándar q
con
csp q
Estos procedimientos estándar son procedimientos Pascal como readln()
(csp rln
), sin()
(csp sin
), etc. Curiosamente, eof()
es una instrucción de código p en su lugar.
Ejemplo de máquina
Niklaus Wirth especificó una máquina de código p simple en el libro de 1976 Algoritmos + Estructuras de datos = Programas. La máquina tenía 3 registros: un contador de programa p, un registro base b y un registro superior t. Había 8 instrucciones:
- iluminado 0, a: carga constante a
- opr 0, a: ejecutar operación a (13 operaciones: RETURN, 5 funciones matemáticas y 7 funciones de comparación)
- Lod l, a: variable de carga l,a
- Sto l, a: tienda variable l,a
- Cal l, a: procedimiento de llamada a a nivel l
- int 0, a: aumento t-register por a
- jmp 0, a: salto a a
- jpc 0, a: salto condicional a a
Este es el código de la máquina, escrito en Pascal:
constamax=2047; {Dirección máxima}levmax=3; {máxima profundidad de anidación de bloques}cxmax=200; {size of code array}Tipo f=()iluminado,opr,Lod,Sto,Cal,int,jmp,jpc);instrucción=empaquetado récord f:f;l:0..levmax;a:0..amax;final;Varcódigo: array [0..cxmax] de instrucción;procedimiento interpretación; const stacksize = 500; Var p, b, t: entero; {programa-, base-, topstack-registers} i: instrucción; {Registro de instrucciones} s: array [1..stacksize] de entero; {datastore} función base()l: entero): entero; Var b1: entero; comenzar b1 := b; {find base l levels down} mientras l ■ 0 do comenzar b1 := s[b1]; l := l - 1 final; base := b1 final {base};comenzar Writeln()'comienzo pl/0 '); t := 0; b := 1; p := 0; s[1] := 0; s[2] := 0; s[3] := 0; repetición i := código[p]; p := p + 1; con i do Caso f de iluminado: comenzar t := t + 1; s[t] := a final; opr: Caso a de {operador} 0: comenzar {Retorno} t := b - 1; p := s[t + 3]; b := s[t + 2]; final; 1: s[t] := -s[t]; 2: comenzar t := t - 1; s[t] := s[t] + s[t + 1] final; 3: comenzar t := t - 1; s[t] := s[t] - s[t + 1] final; 4: comenzar t := t - 1; s[t] := s[t] * s[t + 1] final; 5: comenzar t := t - 1; s[t] := s[t] div s[t + 1] final; 6: s[t] := ord()extraño()s[t]); 8: comenzar t := t - 1; s[t] := ord()s[t] = s[t + 1]) final; 9: comenzar t := t - 1; s[t] := ord()s[t] ■ s[t + 1]) final; 10: comenzar t := t - 1; s[t] := ord()s[t] . s[t + 1]) final; 11: comenzar t := t - 1; s[t] := ord()s[t] >= s[t + 1]) final; 12: comenzar t := t - 1; s[t] := ord()s[t] ■ s[t + 1]) final; 13: comenzar t := t - 1; s[t] := ord()s[t] . s[t + 1]) final; final; Lod: comenzar t := t + 1; s[t] := s[base()l) + a] final; Sto: comenzar s[base()l)+a] := s[t]; Writeln()s[t]); t := t - 1 final; Cal: comenzar {generar nueva marca de bloques} s[t + 1] := base()l); s[t + 2] := b; s[t + 3] := p; b := t + 1; p := a final; int: t := t + a; jmp: p := a; jpc: comenzar si s[t] = 0 entonces p := a; t := t - 1 final final {con, case} hasta p = 0; Writeln()Fin pl/0);final {interpret};
Esta máquina se usó para ejecutar el PL/0 de Wirth, un compilador de subconjuntos de Pascal que se usa para enseñar el desarrollo del compilador.
Código P de Microsoft
P-Code es el nombre de varios de los lenguajes intermedios patentados de Microsoft. Proporcionaron un formato binario alternativo al código de máquina. En varias ocasiones, Microsoft ha dicho que p-code es una abreviatura de código empaquetado o pseudocódigo.
El código p de Microsoft se usó en Visual C++ y Visual Basic. Al igual que otras implementaciones de código p, el código p de Microsoft permitió un ejecutable más compacto a expensas de una ejecución más lenta.
Otras implementaciones
Contenido relacionado
Idempotencia
Miniordenador
Iván Sutherland