Lenguaje de programación de bajo nivel
Un lenguaje de programación de bajo nivel es un lenguaje de programación que proporciona poca o ninguna abstracción de la arquitectura del conjunto de instrucciones de una computadora: comandos o funciones en el mapa del lenguaje que son estructuralmente similares al procesador& #39;s instrucciones. Generalmente, esto se refiere al código de máquina o al lenguaje ensamblador. Debido a la baja abstracción (de ahí la palabra) entre el lenguaje y el lenguaje de máquina, los lenguajes de bajo nivel a veces se describen como "cercanos al hardware". Los programas escritos en lenguajes de bajo nivel tienden a ser relativamente no portátiles, debido a que están optimizados para cierto tipo de arquitectura de sistema.
Los lenguajes de bajo nivel pueden convertirse en código de máquina sin un compilador o intérprete (los lenguajes de programación de segunda generación usan un procesador más simple llamado ensamblador) y el código resultante se ejecuta directamente en el procesador. Se puede hacer que un programa escrito en un lenguaje de bajo nivel se ejecute muy rápidamente, con una pequeña huella de memoria. Un programa equivalente en un lenguaje de alto nivel puede ser menos eficiente y usar más memoria. Los lenguajes de bajo nivel son simples, pero se consideran difíciles de usar debido a numerosos detalles técnicos que el programador debe recordar. En comparación, un lenguaje de programación de alto nivel aísla la semántica de ejecución de una arquitectura informática de la especificación del programa, lo que simplifica el desarrollo.
Código de máquina
El código máquina es el único lenguaje que una computadora puede procesar directamente sin una transformación previa. Actualmente, los programadores casi nunca escriben programas directamente en código de máquina, porque requiere atención a numerosos detalles que un lenguaje de alto nivel maneja automáticamente. Además, requiere memorizar o buscar códigos numéricos para cada instrucción, y es extremadamente difícil de modificar.
El verdadero código de máquina es un flujo de datos sin procesar, generalmente binarios. Un programador codificando en "código de máquina" normalmente codifica instrucciones y datos en una forma más legible, como decimal, octal o hexadecimal, que se traduce a formato interno mediante un programa llamado cargador o se cambia a la memoria de la computadora desde un panel frontal.
Aunque pocos programas están escritos en lenguajes de máquina, los programadores a menudo se vuelven expertos en leerlos trabajando con volcados del núcleo o depurando desde el panel frontal.
Ejemplo de una función en representación hexadecimal de código máquina x86 de 32 bits para calcular el nésimo número de Fibonacci:
8B542408 83FA0077 06B80000 0000C383 FA027706 B8010000 00C353BB 01000 B9010000 008D0419 83FA0376 078BD989 C14AEBF1 5BC3
Lenguaje ensamblador
Los lenguajes de segunda generación proporcionan un nivel de abstracción además del código de máquina. En los primeros días de la codificación en computadoras como TX-0 y PDP-1, lo primero que hicieron los piratas informáticos del MIT fue escribir ensambladores. El lenguaje ensamblador tiene poca semántica o especificación formal, siendo solo un mapeo de símbolos legibles por humanos, incluidas direcciones simbólicas, códigos de operación, direcciones, constantes numéricas, cadenas, etc. Por lo general, una instrucción de máquina se representa como una línea de código ensamblador. Los ensambladores producen archivos de objetos que pueden vincularse con otros archivos de objetos o cargarse solos.
La mayoría de los ensambladores proporcionan macros para generar secuencias comunes de instrucciones.
Ejemplo: la misma calculadora de números de Fibonacci que la anterior, pero en lenguaje ensamblador x86-64 usando la sintaxis de AT&T:
_fib: movl 1 dólar, %eax xorl %ebx, %ebx.fib_loop: cmpl 1 dólar, %edi jbe .fib_done movl %eax, %ecx addl %ebx, %eax movl %ecx, %ebx subl 1 dólar, %edi jmp .fib_loop.fib_done: RetEn este ejemplo de código, las funciones de hardware del procesador x86-64 (sus registros) se nombran y manipulan directamente. La función carga su entrada desde %edi de acuerdo con System V ABI y realiza su cálculo manipulando valores en EAX, EBX y ECX se registra hasta que finaliza y regresa. Tenga en cuenta que en este lenguaje ensamblador, no existe el concepto de devolver un valor. Habiendo sido almacenado el resultado en el registro EAX, el comando RET simplemente mueve el procesamiento del código a la ubicación del código almacenado en la pila (generalmente la instrucción inmediatamente después de la que llamó a este función) y depende del autor del código de llamada saber que esta función almacena su resultado en EAX y recuperarlo desde allí. El lenguaje ensamblador x86-64 no impone ningún estándar para devolver valores de una función (y, de hecho, no tiene el concepto de función); Depende del código de llamada examinar el estado después de que el procedimiento regrese si necesita extraer un valor.
Compare esto con la misma función en C:
no firmado int fib()no firmado int n) {} si ()!n) retorno 0; más si ()n . 2) retorno 1; más {} no firmado int a, c; para ()a = c = 1; ; --n) {} c += a; si ()n . 2) retorno c; a = c - a; } }}Este código es muy similar en estructura al ejemplo del lenguaje ensamblador, pero hay diferencias significativas en términos de abstracción:
- La entrada (parametro n) es una abstracción que no especifica ninguna ubicación de almacenamiento en el hardware. En la práctica, el compilador de C sigue una de las muchas convenciones posibles de convocatoria para determinar un lugar de almacenamiento para la entrada.
- La versión del lenguaje de montaje carga el parámetro de entrada de la pila en un registro y en cada iteración de los decrementos del bucle el valor en el registro, nunca alterando el valor en la ubicación de memoria en la pila. El compilador C puede cargar el parámetro en un registro y hacer lo mismo o puede actualizar el valor donde sea que se almacena. Lo que elija es una decisión de implementación completamente oculta del autor de código (y una sin efectos secundarios, gracias a los estándares de lenguaje C).
- Las variables locales a, b y c son abstracciones que no especifican ninguna ubicación de almacenamiento específica en el hardware. El compilador C decide cómo almacenarlas para la arquitectura de destino.
- La función de retorno especifica el valor de retorno, pero no dicta cómo es devuelto. El compilador C para cualquier arquitectura específica implementa a estándar mecanismo para devolver el valor. Los compiladores de la arquitectura x86 suelen (pero no siempre) utilizar el registro EAX para devolver un valor, como en el ejemplo del lenguaje de montaje (el autor del ejemplo del lenguaje de montaje tiene elegido para copiar la convención C, pero el lenguaje de montaje no requiere esto).
Estas abstracciones hacen que el código C sea compilable sin modificaciones en cualquier arquitectura para la que se haya escrito un compilador C. El código del lenguaje ensamblador x86 es específico de la arquitectura x86.
Programación de bajo nivel en lenguajes de alto nivel
A fines de la década de 1960, los lenguajes de alto nivel como PL/S, BLISS, BCPL, ALGOL extendido (para sistemas grandes de Burroughs) y C incluían cierto grado de acceso a funciones de programación de bajo nivel. Un método para esto es el ensamblado en línea, en el que el código ensamblador está incrustado en un lenguaje de alto nivel que admite esta función. Algunos de estos lenguajes también permiten directivas de optimización del compilador dependientes de la arquitectura para ajustar la forma en que un compilador usa la arquitectura del procesador de destino.
Contenido relacionado
Dillo
Dnix
Informática