En programación informática , una máquina de código P ( máquina de código portátil [1] ) es una máquina virtual diseñada para ejecutar código P, el lenguaje ensamblador o código de máquina de una unidad central de procesamiento (CPU) hipotética. El término "máquina de código P" se aplica de forma genérica a todas esas máquinas (como la máquina virtual Java (JVM) y el código precompilado de MATLAB ), así como a las implementaciones específicas que utilizan esas máquinas. Uno de los usos más notables de las máquinas de código P es la P-Machine del sistema Pascal-P . Los desarrolladores de la implementación de Pascal de UCSD dentro de este sistema interpretaron la P en código P para significar pseudo más a menudo que portátil; adoptaron una etiqueta única para pseudocódigo que significa instrucciones para una pseudomá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 , [2] el término código P apareció por primera vez a principios de la década de 1970. Los dos primeros compiladores que generaron código P fueron el compilador Pascal-P en 1973, de Kesav V. Nori, Urs Ammann, Kathleen Jensen, Hans-Heinrich Nägeli y Christian Jacobi, [3] y el compilador Pascal-S en 1975, de Niklaus Wirth .
Los programas que se han traducido a código P pueden ser interpretados por un programa de software que emula 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 ejecutados. 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 ).
Si bien un modelo de compilador típico tiene como objetivo traducir un código de programa en código de máquina , la idea de una máquina de código P sigue 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) a través de la máquina de código P.
Esta separación permite separar el desarrollo de un intérprete de código P del compilador de código de máquina subyacente, que tiene que considerar el comportamiento dependiente de la máquina al generar su bytecode . De esta manera, un intérprete de código P también se puede implementar más rápido y la capacidad de interpretar el código en tiempo de ejecución permite verificaciones adicionales en tiempo de ejecución que podrían no estar disponibles de manera similar en el código nativo. Además, como el código P se basa en una máquina virtual ideal, un programa de código P a menudo puede ser más pequeño que el mismo programa traducido a código de máquina. Por el contrario, la interpretación en dos pasos de un programa basado en código P conduce a una velocidad de ejecución más lenta, aunque esto a veces se puede abordar con la compilación justo a tiempo , y su estructura más simple 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 independencia de la máquina mediante el uso extensivo de código P [ cita requerida ] . El Business Operating System (BOS) era un sistema operativo multiplataforma diseñado para ejecutar programas de código P exclusivamente. El UCSD p-System , desarrollado en la Universidad de California en San Diego, era un sistema operativo autocompilado y autohospedado 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 las implementaciones de lenguajes como Python , Microsoft P-Code en Visual Basic y Java bytecode en Java .
El lenguaje Go utiliza un ensamblador genérico y portable 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 bytecode de Common Language Runtime (CLR) o el bytecode de JVM, no existe una especificación estable y las herramientas de compilación de Go no emiten un formato de bytecode para ser utilizado en un momento posterior. El ensamblador de Go utiliza el lenguaje ensamblador genérico como una representación intermedia y los ejecutables de Go son binarios enlazados estáticamente específicos de la máquina . [4]
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 add
instrucción reemplaza los dos elementos superiores de la pila con su suma. Algunas instrucciones toman un argumento inmediato. Al igual que Pascal, el código P está fuertemente tipado y admite tipos de datos booleanos (b), de caracteres (c), enteros (i), reales (r), conjuntos (s) y punteros (a) de forma nativa.
Algunas instrucciones sencillas:
Insn. Stack Descripción de la pila antes después adi i1 i2 i1+i2 suma dos enterosadr r1 r2 r1+r2 suma dos números realesinn i1 s1 b1 conjunto de pertenencia; b1 = si i1 es miembro de s1ldi i1 i1 i1 carga constante enteramovimiento a1 a2 a2 movimientono b1 b1 -b1 negación booleana
De manera similar a una CPU de destino real, el P-System tiene solo una pila compartida por los marcos de pila de procedimientos (que proporcionan la dirección de retorno , etc.) y los argumentos de las instrucciones locales. Tres de los registros de la máquina apuntan a la pila (que crece hacia arriba):
También hay un área constante y, debajo de ella, el montón que crece hacia abajo en dirección a la pila. El registro NP (el nuevo puntero) apunta a la parte superior (la dirección más baja utilizada) 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.
Los marcos de pila se ven así:
EP -> pila localSP -> ... lugareños ... parámetros ... dirección de retorno (PC anterior) EP anterior Enlace dinámico (MP anterior) Enlace estático (MP del procedimiento circundante)MP -> valor de retorno de la función
La secuencia de llamada al 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 pila anterior e inicializará el EP anterior, el enlace dinámico y el estático. Luego, el llamador calcula e introduce los parámetros para el procedimiento y luego emite
taza n, p
para llamar a un procedimiento de usuario ( n
siendo 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 de usuario comienzan con las dos instrucciones
ent 1, yo ent 2, j
El primero establece SP en MP+ i
, el segundo establece EP en SP+ j
. i
Básicamente, especifica el espacio reservado para variables locales (más el número de parámetros más 5) y j
proporciona el número de entradas necesarias localmente para la pila. En este punto, se comprueba el agotamiento de la memoria.
La devolución de la llamada se realiza a través de
retC
con C
el tipo de retorno (i, r, c, b, a como se indica arriba, y p para ningún valor de retorno). El valor de retorno debe almacenarse previamente en la celda correspondiente. En todos los tipos excepto p, el retorno dejará este valor en la pila.
En lugar de llamar a un procedimiento de usuario (cup), q
se puede llamar a un procedimiento estándar con
Csp q
Estos procedimientos estándar son procedimientos Pascal como readln()
( csp rln
), sin()
( csp sin
), etc. Curiosamente, eof()
en su lugar se trata de una instrucción de código p.
Niklaus Wirth especificó una máquina de código p simple en el libro Algoritmos + Estructuras de datos = Programas de 1976. 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:
lit 0, a
: carga constante aopr 0, a
: ejecutar la operación a (13 operaciones: RETORNO, 5 funciones matemáticas y 7 funciones de comparación)lod l, a
: variable de carga l , asto l, a
: almacena la variable l , acal l, a
: llamar al procedimiento a en el nivel lint 0, a
:incrementar el registro t en ajmp 0, a
: saltar a unjpc 0, a
: salto condicional a un [5]Este es el código de la máquina, escrito en Pascal:
const amax = 2047 ; {dirección máxima} levmax = 3 ; {profundidad máxima de anidamiento de bloques} cxmax = 200 ; {tamaño de la matriz de códigos} tipo fct = ( lit , opr , lod , sto , cal , int , jmp , jpc ) ; instrucción = registro empaquetado f : fct ; l : 0 .. levmax ; a : 0 .. amax ; fin ; var código : matriz [ 0 .. cxmax ] de instrucción ; procedimiento interpretar ; constante tamaño de pila = 500 ; var p , b , t : entero ; {registros de programa, base y pila superior} i : instrucción ; {registro de instrucción} s : matriz [ 1 ... tamaño de pila ] de enteros ; {almacén de datos} función base ( l : entero ) : entero ; var b1 : entero ; begin b1 := b ; {encontrar base l niveles hacia abajo} mientras l > 0 hacer begin b1 := s [ b1 ] ; l := l - 1 fin ; base := b1 fin {base} ; empezar writeln ( ' start pl/0' ) ; t := 0 ; b := 1 ; p := 0 ; s [ 1 ] := 0 ; s [ 2 ] := 0 ; s [ 3 ] := 0 ; repetir i := código [ p ] ; p := p + 1 ; con i hacer caso f de lit : empezar t := t + 1 ; s [ t ] := a fin ; opr : caso a de {operador} 0 : empezar {retorno} t := b - 1 ; p := s [ t + 3 ] ; b := s [ t + 2 ] ; fin ; 1 : s [ t ] := - s [ t ] ; 2 : inicio t := t - 1 ; s [ t ] := s [ t ] + s [ t + 1 ] fin ; 3 : inicio t := t - 1 ; s [ t ] := s [ t ] - s [ t + 1 ] fin ; 4 : inicio t := t - 1 ; s [ t ] := s [ t ] * s [ t + 1 ] fin ; 5 : inicio t := t - 1 ; s [ t ] := s [ t ] div s [ t + 1 ] fin ; 6 : s [ t ] := orden ( impar ( s [ t ])) ; 8 : inicio t := t - 1 ; s [ t ] := orden ( s [ t ] = s [ t + 1 ]) fin ; 9 : inicio t := t - 1 ; s [ t ] := orden ( s [ t ] <> s [ t + 1 ]) fin ; 10 : inicio t := t - 1 ; s [ t ] := orden ( s [ t ] < s [ t + 1 ]) fin ; 11 : inicio t := t - 1 ; s [ t ] := orden ( s [ t ] >= s [ t + 1 ]) fin ; 12 : inicio t := t - 1 ; s [ t ] := orden ( s [ t ] > s [ t + 1 ]) fin ; 13 : inicio t := t - 1 ; s [ t ] := orden ( s [ t ] <= s [ t + 1 ]) fin ; fin ; lod : comienzo t := t + 1 ; s [ t ] := s [ base ( l ) + a ] fin ; sto : comienzo s [ base ( l ) + a ] := s [ t ] ; writeln ( s [ t ]) ; t := t - 1 fin ; cal : comienzo {generar nueva marca de bloque} s [ t + 1 ] := base ( l ) ; s [ t + 2 ] := b ; s [ t + 3 ] := p ; b := t + 1 ; p := a fin ; int : t := t + a ; jmp : p := a ; jpc : comienza si s [ t ] = 0 entonces p := a ; t := t - 1 fin fin {con, caso} hasta p = 0 ; writeln ( ' fin pl/0' ) ; fin {interpretar} ;
Esta máquina se utilizó para ejecutar el PL/0 de Wirth , un compilador de subconjuntos de Pascal utilizado para enseñar el desarrollo de compiladores. [6] [ verificación fallida ]
P-code es el nombre de varios lenguajes intermedios patentados de Microsoft . Proporcionaban 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 [7] o pseudocódigo [8] .
El código P de Microsoft se utilizó 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 costa de una ejecución más lenta.