En programación informática , el lenguaje ensamblador (alternativamente lenguaje ensamblador [1] o código de máquina simbólico ), [2] [3] [4] a menudo denominado simplemente ensamblaje y comúnmente abreviado como ASM o asm , es cualquier lenguaje de programación de bajo nivel con una correspondencia muy fuerte entre las instrucciones en el lenguaje y las instrucciones del código de máquina de la arquitectura . [5] El lenguaje ensamblador generalmente tiene una declaración por instrucción de máquina (1:1), pero las constantes, los comentarios , las directivas de ensamblador , [6] las etiquetas simbólicas de, por ejemplo, ubicaciones de memoria , registros y macros [7] [1] generalmente también son compatibles.
El primer código ensamblador en el que se utiliza un lenguaje para representar instrucciones de código máquina se encuentra en el trabajo de 1947 de Kathleen y Andrew Donald Booth , Coding for ARC . [8] El código ensamblador se convierte en código máquina ejecutable mediante un programa de utilidad denominado ensamblador . El término "ensamblador" se atribuye generalmente a Wilkes , Wheeler y Gill en su libro de 1951 The Preparation of Programs for an Electronic Digital Computer , [9] quienes, sin embargo, utilizaron el término para significar "un programa que ensambla otro programa que consta de varias secciones en un solo programa". [10] El proceso de conversión se denomina ensamblaje , como ensamblar el código fuente . El paso computacional cuando un ensamblador está procesando un programa se llama tiempo de ensamblaje .
Dado que el ensamblaje depende de las instrucciones del código de máquina, cada lenguaje ensamblador [nb 1] es específico para una arquitectura informática particular . [11] [12] [13]
A veces hay más de un ensamblador para la misma arquitectura, y a veces un ensamblador es específico para un sistema operativo o para sistemas operativos particulares. La mayoría de los lenguajes ensambladores no proporcionan una sintaxis específica para las llamadas al sistema operativo, y la mayoría de los lenguajes ensambladores se pueden usar universalmente con cualquier sistema operativo, [nb 2] ya que el lenguaje proporciona acceso a todas las capacidades reales del procesador , sobre las que descansan en última instancia todos los mecanismos de llamada al sistema . A diferencia de los lenguajes ensambladores, la mayoría de los lenguajes de programación de alto nivel son generalmente portables a través de múltiples arquitecturas pero requieren interpretación o compilación , tareas mucho más complicadas que el ensamblaje.
En las primeras décadas de la informática, era habitual que tanto la programación de sistemas como la programación de aplicaciones se realizaran íntegramente en lenguaje ensamblador. Si bien sigue siendo irremplazable para algunos propósitos, la mayor parte de la programación ahora se realiza en lenguajes compilados e interpretados de alto nivel. En " No Silver Bullet ", Fred Brooks resumió los efectos del abandono de la programación en lenguaje ensamblador: "Seguramente el golpe más poderoso para la productividad, la confiabilidad y la simplicidad del software ha sido el uso progresivo de lenguajes de alto nivel para la programación. La mayoría de los observadores atribuyen a ese desarrollo al menos un factor de cinco en la productividad y las ganancias concomitantes en confiabilidad, simplicidad y comprensibilidad". [14]
En la actualidad, es habitual utilizar pequeñas cantidades de código en lenguaje ensamblador en sistemas más grandes implementados en un lenguaje de alto nivel, por razones de rendimiento o para interactuar directamente con el hardware de formas que no son compatibles con el lenguaje de alto nivel. Por ejemplo, poco menos del 2% del código fuente de la versión 4.9 del núcleo de Linux está escrito en lenguaje ensamblador ; más del 97% está escrito en C. [15]
El lenguaje ensamblador utiliza un mnemónico para representar, por ejemplo, cada instrucción de máquina de bajo nivel o código de operación , cada directiva , típicamente también cada registro arquitectónico , bandera , etc. Algunos de los mnemónicos pueden estar integrados y otros definidos por el usuario. Muchas operaciones requieren uno o más operandos para formar una instrucción completa. La mayoría de los ensambladores permiten constantes con nombre, registros y etiquetas para ubicaciones de programa y memoria, y pueden calcular expresiones para operandos. De este modo, los programadores se liberan de cálculos repetitivos tediosos y los programas ensambladores son mucho más legibles que el código máquina. Dependiendo de la arquitectura, estos elementos también pueden combinarse para instrucciones específicas o modos de direccionamiento utilizando desplazamientos u otros datos, así como direcciones fijas. Muchos ensambladores ofrecen mecanismos adicionales para facilitar el desarrollo del programa, controlar el proceso de ensamblaje y ayudar a la depuración .
Algunos están orientados a columnas, con campos específicos en columnas específicas; esto era muy común en las máquinas que usaban tarjetas perforadas en los años 1950 y principios de los 1960. Algunos ensambladores tienen una sintaxis de formato libre, con campos separados por delimitadores, p. ej., puntuación, espacio en blanco . Algunos ensambladores son híbridos, con, p. ej., etiquetas, en una columna específica y otros campos separados por delimitadores; esto se volvió más común que la sintaxis orientada a columnas en los años 1960.
Un programa ensamblador crea código objeto traduciendo combinaciones de mnemotecnia y sintaxis para operaciones y modos de direccionamiento en sus equivalentes numéricos. Esta representación normalmente incluye un código de operación (" opcode ") así como otros bits de control y datos. El ensamblador también calcula expresiones constantes y resuelve nombres simbólicos para ubicaciones de memoria y otras entidades. [20] El uso de referencias simbólicas es una característica clave de los ensambladores, que ahorra cálculos tediosos y actualizaciones manuales de direcciones después de modificaciones del programa. La mayoría de los ensambladores también incluyen funciones de macro para realizar sustituciones textuales, por ejemplo, para generar secuencias cortas comunes de instrucciones como inline , en lugar de las llamadas subrutinas .
Algunos ensambladores también pueden ser capaces de realizar algunos tipos simples de optimizaciones específicas del conjunto de instrucciones . Un ejemplo concreto de esto pueden ser los ubicuos ensambladores x86 de varios proveedores. Llamados jump-sizing [ 20], la mayoría de ellos son capaces de realizar reemplazos de instrucciones de salto (saltos largos reemplazados por saltos cortos o relativos) en cualquier número de pases, a pedido. Otros pueden incluso hacer una simple reorganización o inserción de instrucciones, como algunos ensambladores para arquitecturas RISC que pueden ayudar a optimizar una programación de instrucciones sensata para explotar el pipeline de la CPU de la manera más eficiente posible. [21]
Los ensambladores han estado disponibles desde la década de 1950, como el primer paso por encima del lenguaje de máquina y antes de los lenguajes de programación de alto nivel como Fortran , Algol , COBOL y Lisp . También han existido varias clases de traductores y generadores de código semiautomáticos con propiedades similares tanto al ensamblador como a los lenguajes de alto nivel, siendo Speedcode quizás uno de los ejemplos más conocidos.
Puede haber varios ensambladores con sintaxis diferente para una CPU o arquitectura de conjunto de instrucciones en particular . Por ejemplo, una instrucción para agregar datos de memoria a un registro en un procesador de la familia x86 podría estar add eax,[ebx]
en la sintaxis original de Intel , mientras que esta estaría escrita addl (%ebx),%eax
en la sintaxis de AT&T utilizada por el ensamblador GNU . A pesar de las diferentes apariencias, las diferentes formas sintácticas generalmente generan el mismo código de máquina numérico . Un solo ensamblador también puede tener diferentes modos para admitir variaciones en las formas sintácticas, así como sus interpretaciones semánticas exactas (como la sintaxis FASM , la sintaxis TASM , el modo ideal, etc., en el caso especial de la programación en ensamblador x86 ).
Hay dos tipos de ensambladores según la cantidad de pasadas a través de la fuente que se necesitan (cuántas veces el ensamblador lee la fuente) para producir el archivo objeto.
En ambos casos, el ensamblador debe ser capaz de determinar el tamaño de cada instrucción en los pases iniciales para calcular las direcciones de los símbolos subsiguientes. Esto significa que si el tamaño de una operación que hace referencia a un operando definido posteriormente depende del tipo o la distancia del operando, el ensamblador realizará una estimación pesimista cuando se encuentre por primera vez con la operación y, si es necesario, la rellenará con una o más instrucciones de " no operación " en un pase posterior o con la errata. En un ensamblador con optimización de mirilla , las direcciones se pueden recalcular entre pases para permitir reemplazar el código pesimista con un código adaptado a la distancia exacta desde el objetivo.
La razón original para el uso de ensambladores de una sola pasada era el tamaño de la memoria y la velocidad de ensamblaje; a menudo, una segunda pasada requeriría almacenar la tabla de símbolos en la memoria (para manejar referencias hacia adelante ), rebobinar y releer el código fuente del programa en cinta , o releer una baraja de cartas o una cinta de papel perforada . Las computadoras posteriores con memorias mucho más grandes (especialmente almacenamiento en disco), tenían el espacio para realizar todo el procesamiento necesario sin tal relectura. La ventaja del ensamblador de múltiples pasadas es que la ausencia de erratas hace que el proceso de enlace (o la carga del programa si el ensamblador produce directamente código ejecutable) sea más rápido. [22]
Ejemplo: en el siguiente fragmento de código, un ensamblador de una sola pasada podría determinar la dirección de la referencia hacia atrás BKWD al ensamblar la instrucción S2 , pero no podría determinar la dirección de la referencia hacia adelante FWD al ensamblar la instrucción de bifurcación S1 ; de hecho, FWD puede no estar definida. Un ensamblador de dos pasadas determinaría ambas direcciones en la pasada 1, por lo que se conocerían al generar el código en la pasada 2.
S1 B tracción delantera ...EQUIPO DE TRACCIÓN DELANTERA * ...Equilibrio BKWD * ...S2 B tracción trasera
Los ensambladores de alto nivel más sofisticados proporcionan abstracciones del lenguaje como:
Consulte el diseño del lenguaje a continuación para obtener más detalles.
Un programa escrito en lenguaje ensamblador consta de una serie de instrucciones mnemotécnicas de procesador y metadeclaraciones (conocidas de diversas formas como operaciones declarativas, directivas, pseudoinstrucciones, pseudooperaciones y pseudoops), comentarios y datos. Las instrucciones en lenguaje ensamblador suelen constar de una mnemotécnica de código de operación seguida de un operando , que puede ser una lista de datos, argumentos o parámetros. [24] Algunas instrucciones pueden estar "implícitas", lo que significa que los datos sobre los que opera la instrucción están definidos implícitamente por la propia instrucción; dicha instrucción no toma un operando. La declaración resultante es traducida por un ensamblador a instrucciones en lenguaje de máquina que pueden cargarse en la memoria y ejecutarse.
Por ejemplo, la siguiente instrucción le indica a un procesador x86 / IA-32 que mueva un valor inmediato de 8 bits a un registro . El código binario para esta instrucción es 10110 seguido de un identificador de 3 bits para el registro que se utilizará. El identificador para el registro AL es 000, por lo que el siguiente código de máquina carga el registro AL con los datos 01100001. [24]
10110000 01100001
Este código de computadora binario se puede hacer más legible para los humanos expresándolo en hexadecimal de la siguiente manera.
B0 61
Aquí, B0
significa "Mover una copia del siguiente valor a AL ", y 61
es una representación hexadecimal del valor 01100001, que es 97 en decimal . El lenguaje ensamblador para la familia 8086 proporciona el mnemónico MOV (una abreviatura de mover ) para instrucciones como esta, por lo que el código de máquina anterior se puede escribir de la siguiente manera en lenguaje ensamblador, completo con un comentario explicativo si es necesario, después del punto y coma. Esto es mucho más fácil de leer y recordar.
MOV AL , 61h ; Cargar AL con 97 decimales (61 hexadecimales)
En algunos lenguajes ensambladores (incluido este), se puede utilizar el mismo mnemónico, como MOV, para una familia de instrucciones relacionadas para cargar, copiar y mover datos, ya sean valores inmediatos, valores en registros o ubicaciones de memoria a las que apuntan los valores en los registros o direcciones inmediatas (también conocidas como directas). Otros ensambladores pueden utilizar mnemónicos de código de operación independientes, como L para "mover la memoria al registro", ST para "mover el registro a la memoria", LR para "mover el registro al registro", MVI para "mover el operando inmediato a la memoria", etc.
Si se utiliza el mismo mnemónico para distintas instrucciones, significa que el mnemónico corresponde a varios códigos de instrucciones binarias diferentes, excluyendo los datos (por ejemplo, 61h
en este ejemplo), dependiendo de los operandos que siguen al mnemónico. Por ejemplo, para las CPU x86/IA-32, la sintaxis del lenguaje ensamblador de Intel MOV AL, AH
representa una instrucción que mueve el contenido del registro AH al registro AL . La forma hexadecimal [nb 3] de esta instrucción es:
88E0
El primer byte, 88h, identifica un movimiento entre un registro del tamaño de un byte y otro registro o memoria, y el segundo byte, E0h, está codificado (con tres campos de bits) para especificar que ambos operandos son registros, la fuente es AH y el destino es AL .
En un caso como este, en el que el mismo mnemónico puede representar más de una instrucción binaria, el ensamblador determina qué instrucción generar examinando los operandos. En el primer ejemplo, el operando 61h
es una constante numérica hexadecimal válida y no es un nombre de registro válido, por lo que solo la B0
instrucción puede ser aplicable. En el segundo ejemplo, el operando AH
es un nombre de registro válido y no una constante numérica válida (hexadecimal, decimal, octal o binaria), por lo que solo la 88
instrucción puede ser aplicable.
Los lenguajes ensambladores siempre están diseñados para que esta clase de falta de ambigüedad sea universalmente impuesta por su sintaxis. Por ejemplo, en el lenguaje ensamblador Intel x86, una constante hexadecimal debe comenzar con un dígito numérico, de modo que el número hexadecimal 'A' (igual al decimal diez) se escribiría como 0Ah
o 0AH
, no AH
, específicamente para que no parezca ser el nombre del registro AH . (La misma regla también evita la ambigüedad con los nombres de los registros BH , CH y DH , así como con cualquier símbolo definido por el usuario que termine con la letra H y que de otro modo contenga solo caracteres que sean dígitos hexadecimales, como la palabra "BEACH").
Volviendo al ejemplo original, mientras que el código de operación x86 10110000 ( B0
) copia un valor de 8 bits en el registro ALB1
, 10110001 ( ) lo mueve al registro CL y 10110010 ( B2
) lo hace en el registro DL . A continuación se ofrecen ejemplos en lenguaje ensamblador para estos casos. [24]
MOV AL , 1h ; Carga AL con valor inmediato 1 MOV CL , 2h ; Carga CL con valor inmediato 2 MOV DL , 3h ; Carga DL con valor inmediato 3
La sintaxis de MOV también puede ser más compleja como lo muestran los siguientes ejemplos. [25]
MOV EAX , [ EBX ] ; Mueve los 4 bytes en memoria en la dirección contenida en EBX a EAX MOV [ ESI + EAX ], CL ; Mueve el contenido de CL al byte en la dirección ESI+EAX MOV DS , DX ; Mueve el contenido de DX al registro de segmento DS
En cada caso, el mnemónico MOV se traduce directamente a uno de los códigos de operación 88-8C, 8E, A0-A3, B0-BF, C6 o C7 mediante un ensamblador, y el programador normalmente no tiene que saber o recordar cuál. [24]
La transformación del lenguaje ensamblador en código de máquina es tarea de un ensamblador, y lo inverso puede lograrse, al menos parcialmente, con un desensamblador . A diferencia de los lenguajes de alto nivel , existe una correspondencia uno a uno entre muchas instrucciones de ensamblador simples e instrucciones de lenguaje de máquina. Sin embargo, en algunos casos, un ensamblador puede proporcionar pseudoinstrucciones (esencialmente macros) que se expanden en varias instrucciones de lenguaje de máquina para proporcionar una funcionalidad comúnmente necesaria. Por ejemplo, para una máquina que carece de una instrucción "ramificar si es mayor o igual a", un ensamblador puede proporcionar una pseudoinstrucciones que se expandan a las instrucciones "establecer si es menor que" y "ramificar si es cero (en el resultado de la instrucción de establecimiento)" de la máquina. La mayoría de los ensambladores con todas las funciones también proporcionan un rico lenguaje de macros (que se analiza a continuación) que utilizan los proveedores y programadores para generar código y secuencias de datos más complejos. Dado que la información sobre las pseudoinstrucciones y las macros definidas en el entorno del ensamblador no está presente en el programa objeto, un desensamblador no puede reconstruir las invocaciones de las macros y las pseudoinstrucciones, sino que solo puede desensamblar las instrucciones de máquina reales que el ensamblador generó a partir de esas entidades abstractas del lenguaje ensamblador. Asimismo, dado que el ensamblador ignora los comentarios del archivo fuente del lenguaje ensamblador y no tienen ningún efecto en el código objeto que genera, un desensamblador siempre es completamente incapaz de recuperar los comentarios fuente.
Cada arquitectura informática tiene su propio lenguaje de máquina. Los ordenadores difieren en la cantidad y el tipo de operaciones que admiten, en los distintos tamaños y cantidades de registros y en las representaciones de los datos almacenados. Si bien la mayoría de los ordenadores de uso general pueden llevar a cabo esencialmente la misma funcionalidad, las formas en que lo hacen difieren; los lenguajes ensambladores correspondientes reflejan estas diferencias.
Pueden existir varios conjuntos de mnemónicos o sintaxis de lenguaje ensamblador para un único conjunto de instrucciones, que normalmente se instancian en distintos programas ensambladores. En estos casos, el más popular suele ser el que proporciona el fabricante de la CPU y que se utiliza en su documentación.
Dos ejemplos de CPU que tienen dos conjuntos diferentes de mnemónicos son la familia Intel 8080 y los Intel 8086/8088. Debido a que Intel reclamó los derechos de autor sobre sus mnemónicos en lenguaje ensamblador (en cada página de su documentación publicada en la década de 1970 y principios de la de 1980, al menos), algunas compañías que produjeron de forma independiente CPU compatibles con los conjuntos de instrucciones de Intel inventaron sus propios mnemónicos. La CPU Zilog Z80 , una mejora del Intel 8080A , admite todas las instrucciones del 8080A y muchas más; Zilog inventó un lenguaje ensamblador completamente nuevo, no solo para las nuevas instrucciones sino también para todas las instrucciones del 8080A. Por ejemplo, donde Intel usa los mnemónicos MOV , MVI , LDA , STA , LXI , LDAX , STAX , LHLD y SHLD para varias instrucciones de transferencia de datos, el lenguaje ensamblador Z80 usa el mnemónico LD para todas ellas. Un caso similar es el de las CPU NEC V20 y V30 , copias mejoradas de las Intel 8086 y 8088, respectivamente. Al igual que Zilog con el Z80, NEC inventó nuevos mnemónicos para todas las instrucciones 8086 y 8088, para evitar acusaciones de infracción de los derechos de autor de Intel. (Es cuestionable si dichos derechos de autor pueden ser válidos, y las empresas de CPU posteriores como AMD [nb 4] y Cyrix republicaron los mnemónicos de instrucciones x86/IA-32 de Intel exactamente sin permiso ni penalización legal). Es dudoso que en la práctica muchas personas que programaron las V20 y V30 realmente escribieron en el lenguaje ensamblador de NEC en lugar del de Intel; dado que dos lenguajes ensambladores cualesquiera para la misma arquitectura de conjunto de instrucciones son isomorfos (algo así como el inglés y el latín pig ), no existe ningún requisito para utilizar el lenguaje ensamblador publicado por el propio fabricante con los productos de ese fabricante.
Existe un alto grado de diversidad en la forma en que los autores de ensambladores categorizan las instrucciones y en la nomenclatura que utilizan. En particular, algunos describen cualquier cosa que no sea una mnemotecnia de máquina o una mnemotecnia extendida como una pseudooperación (pseudo-op). Un lenguaje ensamblador típico consta de 3 tipos de instrucciones que se utilizan para definir operaciones de programa:
Las instrucciones (sentencias) en lenguaje ensamblador son generalmente muy simples, a diferencia de las de los lenguajes de alto nivel . Generalmente, un mnemónico es un nombre simbólico para una única instrucción ejecutable en lenguaje de máquina (un opcode ), y hay al menos un mnemónico de opcode definido para cada instrucción en lenguaje de máquina. Cada instrucción normalmente consta de una operación o un opcode más cero o más operandos . La mayoría de las instrucciones se refieren a un único valor o a un par de valores. Los operandos pueden ser inmediatos (valor codificado en la propia instrucción), registros especificados en la instrucción o implícitos, o las direcciones de datos ubicados en otra parte del almacenamiento. Esto está determinado por la arquitectura del procesador subyacente: el ensamblador simplemente refleja cómo funciona esta arquitectura. Los mnemónicos extendidos se utilizan a menudo para especificar una combinación de un opcode con un operando específico, por ejemplo, los ensambladores System/360 utilizan B
como mnemónico extendido para BC
con una máscara de 15 y NOP
("NO OPeration" – no hacer nada para un paso) para BC
con una máscara de 0.
Los mnemónicos extendidos se utilizan a menudo para admitir usos especializados de instrucciones, a menudo para fines que no son obvios a partir del nombre de la instrucción. Por ejemplo, muchas CPU no tienen una instrucción NOP explícita, pero sí tienen instrucciones que se pueden usar para ese propósito. En las CPU 8086, la instrucción se utiliza para , siendo un pseudocódigo de operación para codificar la instrucción . Algunos desensambladores reconocen esto y decodificarán la instrucción como . De manera similar, los ensambladores IBM para System/360 y System/370 utilizan los mnemónicos extendidos y para y con máscaras cero. Para la arquitectura SPARC, se conocen como instrucciones sintéticas . [26]xchg ax,ax
nop
nop
xchg ax,ax
xchg ax,ax
nop
NOP
NOPR
BC
BCR
Algunos ensambladores también admiten macroinstrucciones integradas simples que generan dos o más instrucciones de máquina. Por ejemplo, con algunos ensambladores Z80, ld hl,bc
se reconoce la instrucción como generate ld l,c
seguida de ld h,b
. [27] A veces, se las conoce como pseudocódigos de operación .
Los mnemónicos son símbolos arbitrarios; en 1985, el IEEE publicó el estándar 694 para un conjunto uniforme de mnemónicos que utilizarían todos los ensambladores. El estándar ha sido retirado desde entonces.
Existen instrucciones que se utilizan para definir elementos de datos que contienen datos y variables. Definen el tipo de datos, la longitud y la alineación de los datos. Estas instrucciones también pueden definir si los datos están disponibles para programas externos (programas ensamblados por separado) o solo para el programa en el que se define la sección de datos. Algunos ensambladores las clasifican como pseudooperaciones.
Las directivas de ensamblaje, también llamadas pseudo-opcodes, pseudo-operaciones o pseudo-ops, son comandos que se dan a un ensamblador "para que realice operaciones distintas a las instrucciones de ensamblaje". [20] Las directivas afectan la forma en que opera el ensamblador y "pueden afectar el código objeto, la tabla de símbolos, el archivo de listado y los valores de los parámetros internos del ensamblador". A veces, el término pseudo-opcode se reserva para directivas que generan código objeto, como las que generan datos. [28]
Los nombres de las pseudooperaciones suelen comenzar con un punto para distinguirlas de las instrucciones de la máquina. Las pseudooperaciones pueden hacer que el ensamblaje del programa dependa de los parámetros ingresados por un programador, de modo que un programa se pueda ensamblar de diferentes maneras, tal vez para diferentes aplicaciones. O bien, una pseudooperación se puede utilizar para manipular la presentación de un programa para que sea más fácil de leer y mantener. Otro uso común de las pseudooperaciones es reservar áreas de almacenamiento para datos de tiempo de ejecución y, opcionalmente, inicializar su contenido con valores conocidos.
Los ensambladores simbólicos permiten a los programadores asociar nombres arbitrarios ( etiquetas o símbolos ) con ubicaciones de memoria y varias constantes. Por lo general, a cada constante y variable se le asigna un nombre para que las instrucciones puedan hacer referencia a esas ubicaciones por nombre, promoviendo así el código autodocumentado . En el código ejecutable, el nombre de cada subrutina está asociado con su punto de entrada, por lo que cualquier llamada a una subrutina puede usar su nombre. Dentro de las subrutinas, los destinos GOTO reciben etiquetas. Algunos ensambladores admiten símbolos locales que a menudo son léxicos distintos de los símbolos normales (por ejemplo, el uso de "10$" como destino GOTO).
Algunos ensambladores, como NASM , proporcionan una gestión flexible de símbolos, lo que permite a los programadores gestionar diferentes espacios de nombres , calcular automáticamente los desplazamientos dentro de las estructuras de datos y asignar etiquetas que hacen referencia a valores literales o al resultado de cálculos simples realizados por el ensamblador. Las etiquetas también se pueden utilizar para inicializar constantes y variables con direcciones reubicables.
Los lenguajes ensambladores, como la mayoría de los demás lenguajes informáticos, permiten añadir comentarios al código fuente del programa que se ignorarán durante el ensamblaje. Los comentarios sensatos son esenciales en los programas en lenguaje ensamblador, ya que el significado y el propósito de una secuencia de instrucciones binarias de máquina pueden ser difíciles de determinar. El lenguaje ensamblador "en bruto" (sin comentarios) generado por compiladores o desensambladores es bastante difícil de leer cuando se deben realizar cambios.
Muchos ensambladores admiten macros predefinidas , y otros admiten macros definidas por el programador (y redefinibles repetidamente) que implican secuencias de líneas de texto en las que se incrustan variables y constantes. La definición de macro es más comúnmente [nb 5] una mezcla de declaraciones de ensamblador, por ejemplo, directivas, instrucciones de máquina simbólicas y plantillas para declaraciones de ensamblador. Esta secuencia de líneas de texto puede incluir códigos de operación o directivas. Una vez que se ha definido una macro, su nombre puede usarse en lugar de un mnemónico. Cuando el ensamblador procesa una declaración de este tipo, reemplaza la declaración con las líneas de texto asociadas con esa macro, luego las procesa como si existieran en el archivo de código fuente (incluyendo, en algunos ensambladores, la expansión de cualquier macro existente en el texto de reemplazo). Las macros en este sentido datan de los autocodificadores de IBM de la década de 1950. [29]
Los ensambladores de macros suelen tener directivas para, por ejemplo, definir macros, definir variables, establecer variables como resultado de una expresión aritmética, lógica o de cadena, iterar y generar código condicionalmente. Algunas de esas directivas pueden estar restringidas para usarse dentro de una definición de macro, por ejemplo, MEXIT en HLASM , mientras que otras pueden estar permitidas dentro del código abierto (fuera de las definiciones de macros), por ejemplo, AIF y COPY en HLASM.
En lenguaje ensamblador, el término "macro" representa un concepto más amplio que en otros contextos, como el preprocesador en el lenguaje de programación C , donde su directiva #define se utiliza normalmente para crear macros breves de una sola línea. Las macroinstrucciones de ensamblador, al igual que las macros en PL/I y algunos otros lenguajes, pueden ser "programas" extensos en sí mismos, ejecutados por interpretación del ensamblador durante el ensamblaje.
Dado que las macros pueden tener nombres "cortos" pero expandirse a varias o incluso muchas líneas de código, se pueden utilizar para hacer que los programas en lenguaje ensamblador parezcan mucho más cortos y requieran menos líneas de código fuente, como sucede con los lenguajes de nivel superior. También se pueden utilizar para agregar niveles superiores de estructura a los programas ensambladores, introducir opcionalmente código de depuración integrado mediante parámetros y otras funciones similares.
Los ensambladores de macros suelen permitir que las macros tomen parámetros . Algunos ensambladores incluyen lenguajes de macros bastante sofisticados, que incorporan elementos de lenguaje de alto nivel como parámetros opcionales, variables simbólicas, condicionales, manipulación de cadenas y operaciones aritméticas, todos utilizables durante la ejecución de una macro dada, y que permiten a las macros guardar el contexto o intercambiar información. Por lo tanto, una macro puede generar numerosas instrucciones en lenguaje ensamblador o definiciones de datos, basadas en los argumentos de la macro. Esto se puede utilizar para generar estructuras de datos de estilo de registro o bucles " desenrollados ", por ejemplo, o puede generar algoritmos completos basados en parámetros complejos. Por ejemplo, una macro "sort" podría aceptar la especificación de una clave de ordenación compleja y generar código diseñado para esa clave específica, sin necesidad de las pruebas de tiempo de ejecución que se requerirían para un procedimiento general que interprete la especificación. Se puede considerar que una organización que utiliza lenguaje ensamblador que se ha extendido en gran medida utilizando un conjunto de macros de este tipo está trabajando en un lenguaje de nivel superior, ya que dichos programadores no están trabajando con los elementos conceptuales de nivel más bajo de una computadora. Para subrayar este punto, se utilizaron macros para implementar una máquina virtual temprana en SNOBOL4 (1967), que se escribió en el lenguaje de implementación SNOBOL (SIL), un lenguaje ensamblador para una máquina virtual. La máquina de destino traduciría esto a su código nativo utilizando un ensamblador de macros . [30] Esto permitió un alto grado de portabilidad para la época.
Las macros se utilizaban para personalizar sistemas de software a gran escala para clientes específicos en la era de los mainframes y también las utilizaba el personal de los clientes para satisfacer las necesidades de sus empleadores creando versiones específicas de los sistemas operativos de los fabricantes. Esto lo hacían, por ejemplo, los programadores de sistemas que trabajaban con el sistema de monitorización conversacional/máquina virtual ( VM/CMS ) de IBM y con los complementos de "procesamiento de transacciones en tiempo real" de IBM, el sistema de control de información del cliente (CICS ) y ACP / TPF , el sistema financiero/de aerolíneas que comenzó en la década de 1970 y que todavía hoy ejecuta muchos sistemas de reserva por ordenador (CRS) y sistemas de tarjetas de crédito de gran tamaño.
También es posible utilizar únicamente las capacidades de procesamiento de macros de un ensamblador para generar código escrito en lenguajes completamente diferentes, por ejemplo, para generar una versión de un programa en COBOL utilizando un programa ensamblador de macros puro que contenga líneas de código COBOL dentro de operadores de tiempo de ensamblaje que instruyan al ensamblador para que genere código arbitrario. IBM OS/360 utiliza macros para realizar la generación del sistema . El usuario especifica opciones codificando una serie de macros de ensamblador. El ensamblaje de estas macros genera un flujo de trabajo para construir el sistema, que incluye el lenguaje de control de trabajos y las instrucciones de control de utilidades .
Esto se debe a que, como se entendió en la década de 1960, el concepto de "procesamiento de macros" es independiente del concepto de "ensamblador", siendo el primero, en términos modernos, más procesamiento de textos que generación de código objeto. El concepto de procesamiento de macros apareció, y aparece, en el lenguaje de programación C, que admite "instrucciones de preprocesador" para establecer variables y realizar pruebas condicionales sobre sus valores. A diferencia de ciertos procesadores de macros anteriores dentro de ensambladores, el preprocesador de C no es Turing-completo porque carece de la capacidad de hacer bucles o de "ir a", lo último que permite que los programas hagan bucles.
A pesar del poder del procesamiento de macros, cayó en desuso en muchos lenguajes de alto nivel (las principales excepciones son C , C++ y PL/I), mientras que siguió siendo un recurso perenne para los ensambladores.
La sustitución de parámetros de macros se realiza estrictamente por nombre: en el momento del procesamiento de la macro, el valor de un parámetro se sustituye textualmente por su nombre. El tipo de error más famoso que se produjo fue el uso de un parámetro que era en sí mismo una expresión y no un simple nombre cuando el autor de la macro esperaba un nombre. En la macro:
foo: macro acarga a*b
La intención era que el llamador proporcionara el nombre de una variable, y la variable o constante "global" b se usaría para multiplicar "a". Si se llama a foo con el parámetro a-c
, load a-c*b
se produce la expansión de la macro de . Para evitar cualquier posible ambigüedad, los usuarios de procesadores de macros pueden poner entre paréntesis los parámetros formales dentro de las definiciones de macros, o los llamadores pueden poner entre paréntesis los parámetros de entrada. [31]
Se han escrito paquetes de macros que proporcionan elementos de programación estructurada para codificar el flujo de ejecución. El primer ejemplo de este enfoque fue el conjunto de macros Concept-14, [32] propuesto originalmente por Harlan Mills (marzo de 1970) e implementado por Marvin Kessler en la División de Sistemas Federales de IBM, que proporcionaba bloques de flujo de control IF/ELSE/ENDIF y similares para programas ensambladores de OS/360. Esta era una forma de reducir o eliminar el uso de operaciones GOTO en código ensamblador, uno de los principales factores que causaban el código espagueti en lenguaje ensamblador. Este enfoque fue ampliamente aceptado a principios de la década de 1980 (los últimos días del uso a gran escala del lenguaje ensamblador). El kit de herramientas de ensamblador de alto nivel de IBM [33] incluye un paquete de macros de este tipo.
Otro diseño fue A-Natural, [34] un ensamblador "orientado a flujo" para procesadores 8080/ Z80 de Whitesmiths Ltd. (desarrolladores del sistema operativo similar a Unix Idris , y lo que se informó que fue el primer compilador de C comercial ). El lenguaje fue clasificado como un ensamblador porque trabajaba con elementos de máquina en bruto como códigos de operación , registros y referencias de memoria; pero incorporaba una sintaxis de expresión para indicar el orden de ejecución. Los paréntesis y otros símbolos especiales, junto con construcciones de programación estructurada orientada a bloques, controlaban la secuencia de las instrucciones generadas. A-natural fue construido como el lenguaje objeto de un compilador de C, en lugar de para codificación manual, pero su sintaxis lógica ganó algunos fanáticos.
Ha habido poca demanda aparente de ensambladores más sofisticados desde el declive del desarrollo del lenguaje ensamblador a gran escala. [35] A pesar de eso, todavía se están desarrollando y aplicando en casos donde las limitaciones de recursos o peculiaridades en la arquitectura del sistema de destino impiden el uso efectivo de lenguajes de nivel superior. [36]
Los ensambladores con un motor de macros potente permiten una programación estructurada a través de macros, como la macro switch proporcionada con el paquete Masm32 (este código es un programa completo):
include \ masm32 \ include \ masm32rt.inc ; utiliza la biblioteca Masm32 .code demomain: REPEAT 20 switch rv ( nrandom , 9 ) ; genera un número entre 0 y 8 mov ecx , 7 case 0 print "case 0" case ecx ; a diferencia de la mayoría de los otros lenguajes de programación, print "case 7" ; el switch Masm32 permite "casos variables" case 1 ... 3 .if eax == 1 print "case 1" .elseif eax == 2 print "case 2" .else print "casos 1 a 3: otros" .endif case 4 , 6 , 8 print "casos 4, 6 u 8" default mov ebx , 19 ; print 20 stars .Repeat print "*" dec ebx .Until Sign ?; repetir hasta que se establezca la bandera de signo endsw print chr$ ( 13 , 10 ) ENDM exit end demomain
Cuando se introdujo la computadora con programa almacenado , los programas se escribían en código de máquina y se cargaban en la computadora desde una cinta de papel perforada o se activaban directamente en la memoria desde los interruptores de la consola. [ cita requerida ] A Kathleen Booth "se le atribuye la invención del lenguaje ensamblador" [37] [38] basándose en el trabajo teórico que comenzó en 1947, mientras trabajaba en el ARC2 en Birkbeck, Universidad de Londres, tras la consulta de Andrew Booth (más tarde su marido) con el matemático John von Neumann y el físico Herman Goldstine en el Instituto de Estudios Avanzados . [38] [39]
A finales de 1948, la Calculadora Automática de Almacenamiento de Retardo Electrónico (EDSAC) tenía un ensamblador (llamado "órdenes iniciales") integrado en su programa de arranque . Utilizaba mnemotécnicos de una letra desarrollados por David Wheeler , a quien la IEEE Computer Society atribuye el mérito de ser el creador del primer "ensamblador". [20] [40] [41] Los informes sobre la EDSAC introdujeron el término "ensamblador" para el proceso de combinar campos en una palabra de instrucción. [42] SOAP ( Programa de Ensamblaje Óptimo Simbólico ) fue un lenguaje ensamblador para la computadora IBM 650 escrito por Stan Poley en 1955. [43]
Los lenguajes ensambladores eliminaron gran parte de la programación de primera generación propensa a errores, tediosa y que consumía mucho tiempo que se necesitaba con las primeras computadoras, liberando a los programadores de tareas tediosas como recordar códigos numéricos y calcular direcciones. Alguna vez fueron ampliamente utilizados para todo tipo de programación. A fines de la década de 1950, su uso había sido reemplazado en gran medida por lenguajes de alto nivel en la búsqueda de una mejor productividad de programación . [44] Hoy en día, el lenguaje ensamblador todavía se usa para la manipulación directa de hardware, el acceso a instrucciones especializadas del procesador o para abordar problemas críticos de rendimiento. [45] Los usos típicos son los controladores de dispositivos , los sistemas integrados de bajo nivel y los sistemas en tiempo real (consulte § Uso actual).
Numerosos programas fueron escritos completamente en lenguaje ensamblador. El Burroughs MCP (1961) fue el primer ordenador para el que no se desarrolló un sistema operativo completamente en lenguaje ensamblador; se escribió en lenguaje orientado a problemas de sistemas ejecutivos (ESPOL), un dialecto de Algol. Muchas aplicaciones comerciales también se escribieron en lenguaje ensamblador, incluida una gran cantidad de software para mainframes de IBM desarrollado por grandes corporaciones. COBOL , FORTRAN y algunos PL/I acabaron desplazando al lenguaje ensamblador, aunque varias grandes organizaciones mantuvieron infraestructuras de aplicaciones en lenguaje ensamblador hasta bien entrada la década de 1990.
El lenguaje ensamblador fue el lenguaje de desarrollo principal para los ordenadores domésticos de 8 bits, como el Apple II , los ordenadores Atari de 8 bits , el ZX Spectrum y el Commodore 64. El BASIC interpretado en estos sistemas no ofrecía la máxima velocidad de ejecución ni el uso completo de las funciones para aprovechar al máximo el hardware disponible. El lenguaje ensamblador fue la opción predeterminada para programar consolas de 8 bits, como el Atari 2600 y el Nintendo Entertainment System .
El software clave para los sistemas IBM PC compatibles, como MS-DOS , Turbo Pascal y la hoja de cálculo Lotus 1-2-3 , se escribió en lenguaje ensamblador. A medida que la velocidad de las computadoras creció exponencialmente, el lenguaje ensamblador se convirtió en una herramienta para acelerar partes de los programas, como la representación de Doom , en lugar de un lenguaje de desarrollo dominante. En la década de 1990, el lenguaje ensamblador se utilizó para maximizar el rendimiento de sistemas como Sega Saturn , [46] y como el lenguaje principal para el hardware de arcade que utilizaba la CPU/GPU integrada TMS34010, como Mortal Kombat y NBA Jam .
Ha habido un debate sobre la utilidad y el rendimiento del lenguaje ensamblador en relación con los lenguajes de alto nivel. [47]
Aunque el lenguaje ensamblador tiene usos específicos donde es importante (ver más abajo), existen otras herramientas para la optimización. [48]
A partir de julio de 2017 [actualizar], el índice TIOBE de popularidad de lenguajes de programación clasifica al lenguaje ensamblador en el puesto 11, por delante de Visual Basic , por ejemplo. [49] El ensamblador se puede utilizar para optimizar la velocidad o para optimizar el tamaño. En el caso de la optimización de la velocidad, se afirma que los compiladores de optimización modernos [50] convierten los lenguajes de alto nivel en código que puede ejecutarse tan rápido como el ensamblador escrito a mano, a pesar de algunos contraejemplos. [51] [52] [53] La complejidad de los procesadores modernos y los subsistemas de memoria hace que la optimización efectiva sea cada vez más difícil tanto para los compiladores como para los programadores de ensamblaje. [54] [55] El aumento del rendimiento del procesador ha significado que la mayoría de las CPU permanecen inactivas la mayor parte del tiempo, [56] con retrasos causados por cuellos de botella predecibles como fallas de caché, operaciones de E/S y paginación , lo que hace que la velocidad de ejecución del código sin procesar no sea un problema para muchos programadores.
Todavía existen situaciones en las que los desarrolladores podrían optar por utilizar el lenguaje ensamblador:
El lenguaje ensamblador todavía se enseña en la mayoría de los programas de informática e ingeniería electrónica . Aunque hoy en día pocos programadores trabajan regularmente con el lenguaje ensamblador como herramienta, los conceptos subyacentes siguen siendo importantes. Temas tan fundamentales como la aritmética binaria , la asignación de memoria , el procesamiento de pila , la codificación del conjunto de caracteres , el procesamiento de interrupciones y el diseño de compiladores serían difíciles de estudiar en detalle sin una comprensión de cómo funciona un ordenador a nivel de hardware. Dado que el comportamiento de un ordenador está definido fundamentalmente por su conjunto de instrucciones, la forma lógica de aprender estos conceptos es estudiar un lenguaje ensamblador. La mayoría de los ordenadores modernos tienen conjuntos de instrucciones similares. Por lo tanto, estudiar un solo lenguaje ensamblador es suficiente para aprender los conceptos básicos, reconocer situaciones en las que el uso del lenguaje ensamblador podría ser apropiado y ver cómo se puede crear código ejecutable eficiente a partir de lenguajes de alto nivel. [23]
puede denominarse código máquina simbólico.
La programación en lenguaje ensamblador tiene los mismos beneficios que la programación en lenguaje máquina, excepto que es más fácil.
lenguaje ensamblador es un lenguaje de programación compilado de bajo nivel. Depende del procesador, ya que básicamente traduce los mnemónicos del ensamblador directamente a los comandos que entiende una CPU en particular, de forma uno a uno. Estos mnemónicos del ensamblador son el conjunto de instrucciones para ese procesador.
El lenguaje ensamblador suele ser específico de una arquitectura informática en particular, por lo que existen varios tipos de lenguajes ensambladores. ARM es un lenguaje ensamblador cada vez más popular.
Utilizado como metaensamblador, permite al usuario diseñar sus propios lenguajes de programación y generar procesadores para dichos lenguajes con un mínimo esfuerzo.
La siguiente restricción o limitación menor está vigente con respecto al uso de 1401 Autocoder al codificar instrucciones macro ...
Las ideas no originales contenidas en el siguiente texto se han derivado de varias fuentes... Sin embargo, se considera que se debe agradecer al profesor John von Neumann y al doctor Herman Goldstein por las muchas discusiones fructíferas...
{{cite web}}
: CS1 maint: URL no apta ( enlace )Siempre hay un debate sobre la aplicabilidad del lenguaje ensamblador en nuestro mundo de programación moderno.
... los cambios de diseño tienden a afectar el rendimiento más que ... no se debe pasar directamente al lenguaje ensamblador hasta que ...