Procesadores de lenguajes de programación

El procesador de lenguaje convierte un programa escrito en un lenguaje de alto nivel en código de máquina.

Introducción

Hoy en día, la mayoría de los programas están escritos en un lenguaje de alto nivel como C, Java o Python. Estos lenguajes están diseñados más para las personas que para las máquinas, al ocultar al programador algunos detalles del hardware de una computadora específica.

En pocas palabras, los lenguajes de alto nivel simplifican el trabajo de decirle a una computadora qué hacer. Sin embargo, dado que las computadoras solo entienden las instrucciones en código de máquina (en forma de 1 y 0), no podemos comunicarnos correctamente con ellas sin algún tipo de traductor.

Esta es la razón por la que existen procesadores de lenguaje.

El procesador de lenguaje es un sistema de traducción especial que se utiliza para convertir un programa escrito en un lenguaje de alto nivel, al que llamamos "código fuente", en código de máquina, al que llamamos "programa objeto" o "código objeto". “.

Para diseñar un procesador de lenguaje, se necesita una descripción muy precisa del léxico y la sintaxis, así como la semántica de un lenguaje de alto nivel.

Hay tres tipos de procesadores de lenguaje:

  • Ensamblador
  • Interprete
  • Compilador

En las próximas secciones repasaremos cada uno de estos tipos de procesadores y discutiremos su propósito, diferencias, etc.

Lenguajes ensambladores y el ensamblador

La mayoría de los lenguajes ensambladores son muy similares al código de máquina (por lo que son específicos de una arquitectura de computadora o un sistema operativo), pero en lugar de usar números binarios para describir una instrucción, usa símbolos mnemotécnicos.

Cada símbolo mnemotécnico representa un código de operación o una instrucción, y normalmente necesitamos varios de ellos en conjunto para hacer algo útil. Estas instrucciones se pueden usar para mover valores entre registros (en la arquitectura Intel86-64 este comando sería “MOV”), para realizar operaciones aritméticas básicas en valores como suma, resta, multiplicación y división (ADD, SUB, MUL, DIV ), así como las operaciones lógicas básicas como desplazar un número hacia la izquierda o hacia la derecha o la negación (SHL, SHR, NEG). También puede usar saltos incondicionales y condicionales, lo cual es útil para implementar un bucle "for", bucle "while" o una instrucción "if" (JMP, JE, JLE ...).

Por ejemplo, si el procesador interpreta el comando binario 10110 como "mover de un registro a otro registro", un lenguaje ensamblador lo reemplazaría con un comando, como MOV.

Cada registro también tiene un identificador binario, como 000. Esto también se puede reemplazar con un nombre más "humano", como EAX, que es uno de los registros generales en x86.

Si, por ejemplo, quisiéramos mover un valor a un registro, el código de máquina se vería así:

1
00001 000 00001010
  • 00001: Es el comando de movimiento
  • 000: Es el identificador del registro
  • 00001010: Es el valor que queremos mover

En un lenguaje ensamblador, esto se puede escribir como algo como:

1
MOV EAX, A
  • MOV es el comando de movimiento
  • EAX es el identificador del registro
  • A es el valor hexadecimal que queremos mover (10 en decimal)

Si quisiéramos escribir una expresión simple EAX = 7 + 4 - 2 en código de máquina, se vería así:

1
2
3
4
5
00001 000 00000111
00001 001 00000100
00010 000 001
00001 001 00000010
00011 000 001
  • 00001 es el comando "mover"
  • 00010 es el comando "adición"
  • 00011 es el comando "resta"
  • 000, 001 son los identificadores de los registros
  • 00000111, 00000100, 00000010 son los valores enteros que estamos usando en estas expresiones

En ensamblador, este grupo de números binarios se escribiría como:

1
2
3
4
5
MOV EAX, 7
MOV R8, 4
ADD EAX, R8
MOV R9, 2
SUB EAX, R9
  • MOV es el comando de movimiento
  • ADD es el comando de adición
  • SUB es el comando de resta
  • EAX, R8, R9 son los identificadores de los registros
  • 7, 4, 2: son los valores enteros que estamos usando en estas expresiones

Aunque todavía no es tan legible como un lenguaje de alto nivel, todavía es mucho más legible para los humanos que el comando binario. Los componentes de hardware de la CPU y los registros son mucho más abstractos.

Esto hace que sea más fácil para un programador escribir código fuente, sin necesidad de manipular números para programar. La traducción a código objeto en lenguaje de máquina es simple y directa, realizada por un ensamblador.

Dado que el código fuente ya es bastante similar al código de máquina, no hay necesidad de compilar o interpretar el código - está ensamblado tal cual.

Idiomas interpretados y el intérprete

Cada programa tiene una fase de traducción y una fase de ejecución. En los lenguajes interpretados, estas dos fases están entrelazadas: las instrucciones escritas en un lenguaje de programación de alto nivel se ejecutan directamente sin convertirse previamente en código objeto o código máquina.

Ambas fases son realizadas por un intérprete: un procesador de lenguaje que traduce una sola declaración (línea de código), la ejecuta inmediatamente y luego pasa a la siguiente línea. Si se enfrenta a un error, un intérprete finaliza el proceso de traducción en esa línea y muestra un error. No puede pasar a la siguiente línea y ejecutarla a menos que se elimine el error anterior.

Los intérpretes se han utilizado desde 1952 y su trabajo consistía en facilitar la programación dentro de las limitaciones de las computadoras en ese momento (por ejemplo, había mucho menos espacio de almacenamiento en la primera generación de computadoras que ahora). El primer lenguaje interpretado de alto nivel fue Lisp, implementado por primera vez en 1958 en una computadora IBM704.

Los lenguajes de programación interpretados más comunes hoy en día son Python, Perl y Ruby.

Idiomas compilados y el compilador

A diferencia de los lenguajes de programación interpretados, la fase de traducción y la fase de ejecución en los lenguajes de programación compilados están completamente separadas y la traducción la realiza un compilador.

El compilador es un procesador de lenguaje que lee el código fuente completo escrito en un lenguaje de alto nivel y lo traduce a un código objeto equivalente como un todo. Normalmente, este código objeto se almacena en un archivo. Si hay errores en el código fuente, el compilador los especifica al final de la compilación, junto con las líneas en las que se encontraron los errores. Después de su eliminación, el código fuente se puede volver a compilar.

Los lenguajes de bajo nivel generalmente se compilan porque, al traducirse directamente a código de máquina, le permiten al programador tener mucho más control sobre los componentes de hardware como la memoria o la CPU.

El primer lenguaje de programación compilado de alto nivel fue FORTRAN, creado en 1957 por un equipo dirigido por John Backus en IBM.

Los lenguajes compilados más comunes hoy en día son C++, Rust y Haskell.

Idiomas de bytecode

Los lenguajes de bytecode, también llamados "código portátil" o "p-code" son el tipo de lenguajes de programación que se incluyen en las categorías de ambos lenguajes interpretados y compilados, ya que hacen uso de la compilación y interpretación al traducir y ejecutar el código.

Bytecode es, en pocas palabras, un código de programa que se ha compilado a partir del código fuente en un código de bajo nivel diseñado para un intérprete de software. Después de la compilación (del código fuente al código de bytes), se puede compilar aún más en el código de la máquina, que es reconocido por la CPU, o puede ser ejecutado por una máquina virtual, que luego actúa como intérprete.

El bytecode es universal y se puede transferir en estado compilado a otros dispositivos (con todas las ventajas del código compilado). Luego, la CPU lo convierte en el código de máquina específico para el dispositivo. Dicho esto, puede compilar el código fuente una vez y ejecutarlo en todas partes, dado que el dispositivo tiene otra capa, que se usa para convertir el código de bytes en código de máquina.

La máquina virtual más conocida para la interpretación de códigos de bytes es Java Virtual Machine (JVM), que es tan común que varios lenguajes tienen implementaciones creadas para ejecutarse en ella.

[Crédito: ViralPatel]{.small}

Cuando el programa se ejecuta por primera vez en un lenguaje de bytecode, hay un retraso mientras el código se compila en bytecode, pero la velocidad de ejecución aumenta significativamente en comparación con los lenguajes interpretativos estándar (ya que el código fuente está optimizado para el intérprete).

Una de las mayores ventajas de los lenguajes de código de bytes es su independencia de plataforma, que solía ser típica solo para lenguajes interpretados, mientras que los programas son mucho más rápidos que los lenguajes interpretados normales en lo que respecta a la ejecución.

Otra cosa que vale la pena mencionar aquí es la compilación justo a tiempo (JIT). A diferencia de la compilación por adelantado (AOT), el código se compila mientras se ejecuta. Esto esencialmente mejora la velocidad de compilación y utiliza los beneficios de rendimiento de la compilación con la flexibilidad de interpretación.

Por otra parte, la compilación dinámica no siempre tiene que ser mejor/más rápida que la compilación estática; depende principalmente del tipo de proyecto en el que estés trabajando.

Los lenguajes emblemáticos que se compilan en bytecode son Java y C# y con ellos se encuentran lenguajes como Clojure, Groovy, Kotlin y Scala.

Ventajas y desventajas: compilado frente a interpretado

Actuación

Dado que un compilador traduce un código fuente completo de un lenguaje de programación en un código de máquina ejecutable para la CPU, se necesita una gran cantidad de tiempo para analizar el código fuente, pero una vez que finaliza el análisis y la compilación, la ejecución general es mucho más rápida.

Por otro lado, el intérprete traduce el código fuente línea por línea, ejecutándose cada una a medida que se traduce, lo que conduce a un análisis más rápido del código fuente, pero la ejecución es significativamente más lenta.

Depuración

La depuración es mucho más fácil cuando se trata de lenguajes de programación interpretados porque el código se traduce hasta que se resuelve el error, por lo que sabemos exactamente dónde está y es más fácil de solucionar.

Por el contrario, la depuración en un lenguaje compilado es mucho más tediosa. Si un programa está escrito en un lenguaje compilado, debe compilarse manualmente, lo cual es un paso adicional para ejecutar un programa. Esto puede no parecer un problema, y ​​no lo es con programas pequeños.

Tenga en cuenta que los proyectos masivos pueden tardar decenas de minutos y algunos incluso horas en compilarse.

Además, el compilador genera el mensaje de error después de escanear el código fuente en su totalidad, por lo que el error podría estar en cualquier parte del programa. Incluso si se especifica la línea de un error, después de cambiar el código fuente y corregirlo, debemos volver a compilarlo y solo entonces se puede ejecutar la versión mejorada. Esto puede no parecer un problema, y ​​no lo es con programas pequeños.

Tenga en cuenta que los proyectos masivos pueden tardar decenas de minutos y algunos incluso horas en compilarse. Afortunadamente, se pueden notar muchos errores antes de la compilación con la ayuda de los IDE, pero no todos.

Código fuente frente a código objeto

Para los lenguajes de programación interpretados, el código fuente es necesario para la ejecución. Esto significa que el código fuente de la aplicación está expuesto al usuario, como JavaScript está expuesto en el navegador.

Permitir que los usuarios lean completamente el código fuente puede permitir que los usuarios maliciosos manipulen y encuentren lagunas en la lógica. Esto puede, hasta cierto punto, limitarse usando ofuscación de código, pero todavía es mucho más accesible que el código compilado.

Por otro lado, una vez que el programa escrito en un lenguaje de programación compilado se compila en código objeto, se puede ejecutar un número infinito de veces y ya no se necesita el código fuente.

Por eso, al pasar el programa a un usuario, basta con enviarle el código objeto, y no el código fuente, normalmente en forma de archivo .exe en Windows.

El código interpretado es más susceptible a los ataques de inyección de código y el hecho de que no se verifique el tipo nos presenta un conjunto completamente nuevo de excepciones y errores de programación.

Conclusión

No existe una forma "mejor" de traducir el código fuente, y tanto los lenguajes de programación compilados como los interpretados tienen sus ventajas y desventajas, como se mencionó anteriormente.

En muchos casos, la línea entre "compilado" e "interpretado" no está claramente definida cuando se trata de un lenguaje de programación más moderno, realmente, no hay nada que te impida escribir un compilador para un lenguaje interpretado, por ejemplo. o.