Lenguaje ensamblador

Compartir Imprimir Citar
Lenguaje de programación de bajo nivel

En programación informática, lenguaje ensamblador (o lenguaje ensamblador, o código de máquina simbólico), a menudo denominado simplemente como ensamblado y comúnmente abreviado como ASM o asm, es cualquier lenguaje de programación de bajo nivel con una fuerte correspondencia entre las instrucciones del lenguaje y la arquitectura' s instrucciones de código de máquina. El lenguaje ensamblador generalmente tiene una declaración por instrucción de máquina (1: 1), pero generalmente también se admiten constantes, comentarios, directivas del ensamblador, etiquetas simbólicas de, por ejemplo, ubicaciones de memoria, registros y macros.

El primer código ensamblador en el que se usa un lenguaje para representar instrucciones de código de máquina se encuentra en el trabajo de Kathleen y Andrew Donald Booth de 1947, Coding for A.R.C.. El código ensamblador se convierte en código de 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 La preparación de programas para una computadora digital electrónica, quienes, sin embargo, usaron el término para referirse a "un programa que ensambla otro programa que consiste en de varias secciones en un solo programa". El proceso de conversión se denomina ensamblaje, como en ensamblaje del código fuente. El paso computacional cuando un ensamblador está procesando un programa se llama tiempo de ensamblaje.

Debido a que el ensamblado depende de las instrucciones del código máquina, cada lenguaje ensamblador es específico para una arquitectura de computadora en particular.

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 en particular. 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, ya que el lenguaje brinda acceso a todas las capacidades reales del procesador, sobre las cuales 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 generalmente son portátiles 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 común que tanto la programación de sistemas como la programación de aplicaciones se realizaran completamente en lenguaje ensamblador. Si bien todavía es irremplazable para algunos propósitos, la mayoría de la programación ahora se lleva a cabo en lenguajes interpretados y compilados 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, confiabilidad y simplicidad del software ha sido el uso progresivo de alta -Lenguajes de nivel para programar. La mayoría de los observadores acreditan ese desarrollo con al menos un factor de cinco en productividad, y con ganancias concomitantes en confiabilidad, simplicidad y comprensibilidad."

Hoy en día, es típico usar pequeñas cantidades de código de lenguaje ensamblador dentro de sistemas más grandes implementados en un lenguaje de nivel superior, por motivos de rendimiento o para interactuar directamente con el hardware de formas no admitidas por el lenguaje de nivel superior. Por ejemplo, poco menos del 2% de la versión 4.9 del código fuente del kernel de Linux está escrito en ensamblador; más del 97% está escrito en C.

Sintaxis del lenguaje ensamblador

El lenguaje ensamblador usa un mnemotécnico para representar, por ejemplo, cada instrucción de máquina de bajo nivel o código de operación, cada directiva, generalmente también cada registro arquitectónico, bandera, etc. Algunos de los mnemotécnicos 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. Por lo tanto, los programadores se liberan de los tediosos cálculos repetitivos y los programas ensambladores son mucho más legibles que el código de máquina. Dependiendo de la arquitectura, estos elementos también pueden combinarse para instrucciones específicas o modos de direccionamiento utilizando compensaciones u otros datos, así como direcciones fijas. Muchos ensambladores ofrecen mecanismos adicionales para facilitar el desarrollo de programas, controlar el proceso de ensamblaje y ayudar en la depuración.

Algunos están orientados a columnas, con campos específicos en columnas específicas; esto era muy común para las máquinas que usaban tarjetas perforadas en la década de 1950 y principios de la de 1960. Algunos ensambladores tienen sintaxis de forma libre, con campos separados por delimitadores, por ejemplo, puntuación, espacios en blanco. Algunos ensambladores son híbridos, con, por ejemplo, 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 la década de 1960.

Sistema IBM/360

Todos los ensambladores de IBM para System/360, por defecto, tienen una etiqueta en la columna 1, campos separados por delimitadores en las columnas 2-71, un indicador de continuación en la columna 72 y un número de secuencia en las columnas 73-80. El delimitador para etiqueta, código de operación, operandos y comentarios son espacios, mientras que los operandos individuales están separados por comas y paréntesis.

Terminología

Conceptos clave

Ensamblador

Un programa ensamblador crea código objeto traduciendo combinaciones de mnemónicos y sintaxis para operaciones y modos de direccionamiento en sus equivalentes numéricos. Esta representación suele incluir 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. El uso de referencias simbólicas es una característica clave de los ensambladores, ahorrando cálculos tediosos y actualizaciones manuales de direcciones después de las modificaciones del programa. La mayoría de los ensambladores también incluyen funciones de macro para realizar la sustitución textual, por ejemplo, para generar secuencias cortas comunes de instrucciones en línea, en lugar de subrutinas llamadas.

Algunos ensambladores también pueden 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, la mayoría de ellos pueden 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 realizar 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 la canalización de la CPU de la manera más eficiente posible.

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 ha habido varias clases de traductores y generadores de código semiautomáticos con propiedades similares a los lenguajes ensamblador y de alto nivel, con Speedcode como quizás uno de los ejemplos más conocidos.

Puede haber varios ensambladores con sintaxis diferente para una arquitectura de conjunto de instrucciones o CPU en particular. Por ejemplo, una instrucción para agregar datos de memoria a un registro en un procesador de la familia x86 podría ser add eax,[ebx], en la sintaxis Intel original, mientras que esto sería escrito addl (%ebx),%eax en la sintaxis de AT&T utilizada por GNU Assembler. A pesar de las diferentes apariencias, las diferentes formas sintácticas generalmente generan el mismo código numérico de máquina. 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 programación de ensamblaje x86).

Número de pases

Hay dos tipos de ensambladores basados en cuántas pasadas a través de la fuente se necesitan (cuántas veces el ensamblador lee la fuente) para producir el archivo de objeto.

En ambos casos, el ensamblador debe poder 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 se refiere a un operando definido posteriormente depende del tipo o la distancia del operando, el ensamblador hará una estimación pesimista cuando encuentre la operación por primera vez y, si es necesario, la rellenará con uno o más "sin operación" instrucciones en un pase posterior o la fe de erratas. En un ensamblador con optimización de mirilla, las direcciones se pueden volver a calcular entre pases para permitir reemplazar el código pesimista con un código adaptado a la distancia exacta del objetivo.

La razón original para el uso de ensambladores de un solo paso fue el tamaño de la memoria y la velocidad de ensamblaje; a menudo, un segundo paso requeriría almacenar la tabla de símbolos en la memoria (para manejar las referencias hacia adelante), rebobinar y volver a leer la fuente del programa en la cinta, o releyendo 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 pasos múltiples es que la ausencia de erratas hace que el proceso de vinculación (o la carga del programa si el ensamblador produce directamente el código ejecutable) sea más rápido.

Ejemplo: en el siguiente fragmento de código, un ensamblador de un solo paso podría determinar la dirección de la referencia anterior BKWD al ensamblar la instrucción S2, pero no podría determinar la dirección de la referencia directa FWD al ensamblar la declaración de rama S1; de hecho, FWD puede no estar definido. Un ensamblador de dos pasos determinaría ambas direcciones en el paso 1, por lo que se conocerían al generar el código en el paso 2.

S1 B FWD...
FWD EQU *
...
BKWD EQU *
...
S2 B BKWD

Montadores de alto nivel

Los ensambladores de alto nivel más sofisticados proporcionan abstracciones de lenguaje como:

Consulte Diseño de lenguaje a continuación para obtener más detalles.

Lenguaje ensamblador

Un programa escrito en lenguaje ensamblador consta de una serie de instrucciones de procesador mnemotécnicas y metainstrucciones (conocidas como operaciones declarativas, directivas, pseudoinstrucciones, pseudooperaciones y pseudooperaciones), comentarios y datos. Las instrucciones del lenguaje ensamblador generalmente consisten en un nemotécnico de código de operación seguido de un operando, que puede ser una lista de datos, argumentos o parámetros. Algunas instrucciones pueden ser "implícitas" lo que significa que los datos sobre los que opera la instrucción están implícitamente definidos por la propia instrucción; tal 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 dice 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 qué registro usar. El identificador del registro AL es 000, por lo que el siguiente código de máquina carga el registro AL con los datos 01100001.

10110000 01100001

Este código informático 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 mnemotécnico 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.

MOVIMIENTO AL, 61h ; Carga AL con 97 decimal (61 hex)

En algunos lenguajes ensambladores (incluido este), el mismo mnemónico, como MOV, se puede usar para una familia de instrucciones relacionadas para cargar, copiar y mover datos, ya sean valores inmediatos, valores en registros o ubicaciones de memoria. señalado por valores en registros o por direcciones inmediatas (también conocidas como directas). Otros ensambladores pueden usar mnemónicos de código de operación separados como L para "mover memoria a registro", ST para "mover registro a memoria", LR para "mover registro a registro", MVI para "mover el operando inmediato a la memoria", etc.

Si se usa el mismo mnemónico para diferentes instrucciones, eso significa que el mnemónico corresponde a varios códigos de instrucción binarios diferentes, excluyendo datos (por ejemplo, el 61h en este ejemplo), dependiendo de los operandos que siguen el mnemotécnico. 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 de esta instrucción es:

88 E0

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, el el origen es AH y el destino es AL.

En un caso como este, donde el mismo mnemotécnico 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 se puede aplicar la instrucción B0. 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 instrucción 88 puede ser aplicable.

Los lenguajes ensambladores siempre están diseñados para que este tipo de falta de ambigüedad se imponga universalmente mediante 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 termina con la letra H y contiene solo caracteres que son 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 AL, 10110001 (B1) lo mueve a CL y 10110010 (B2) lo hace a DL. A continuación se muestran ejemplos en lenguaje ensamblador para estos.

MOVIMIENTO AL, 1h ; Carga AL con valor inmediato 1MOVIMIENTO CL, 2h ; Carga CL con valor inmediato 2MOVIMIENTO DL, 3h ; Carga DL con valor inmediato 3

La sintaxis de MOV también puede ser más compleja, como muestran los siguientes ejemplos.

MOVIMIENTO EAX, [EBX] ; Mover los 4 bytes en memoria en la dirección contenida en EBX en EAXMOVIMIENTO [ESI+EAX] CL ; Mover el contenido de CL en el byte en la dirección ESI+EAXMOVIMIENTO DS, DX ; Mover el contenido de DX en registro de segmentos DS

En cada caso, el mnemotécnico MOV se traduce directamente a uno de los códigos de operación 88-8C, 8E, A0-A3, B0-BF, C6 o C7 por un ensamblador, y el programador normalmente no tiene que saberlo ni recordarlo. cuales.

Transformar el lenguaje ensamblador en código de máquina es el trabajo de un ensamblador, y un desensamblador puede lograr, al menos parcialmente, lo contrario. A diferencia de los lenguajes de alto nivel, existe una correspondencia uno a uno entre muchas declaraciones de ensamblaje simples e instrucciones en 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 la funcionalidad comúnmente necesaria. Por ejemplo, para una máquina que carece de una "rama si mayor o igual" instrucción, un ensamblador puede proporcionar una pseudoinstrucción que se expande al 'set if less than' de la máquina. y "branch si cero (en el resultado de la instrucción set)". La mayoría de los ensambladores con funciones completas también proporcionan un lenguaje de macros enriquecido (que se analiza a continuación) que utilizan los proveedores y programadores para generar códigos y secuencias de datos más complejos. Dado que la información sobre pseudoinstrucciones y macros definidas en el entorno del ensamblador no está presente en el programa objeto, un desensamblador no puede reconstruir las invocaciones de macros y pseudoinstrucciones, sino que solo puede desensamblar las instrucciones reales de la máquina que el ensamblador generó a partir de esas entidades abstractas del lenguaje ensamblador. Del mismo modo, dado que los comentarios en el archivo fuente del lenguaje ensamblador son ignorados por el ensamblador y no tienen 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. Las computadoras difieren en el número y tipo de operaciones que soportan, en los diferentes tamaños y números de registros, y en las representaciones de los datos almacenados. Si bien la mayoría de las computadoras de propósito general pueden realizar esencialmente la misma funcionalidad, las formas en que lo hacen difieren; los lenguajes ensambladores correspondientes reflejan estas diferencias.

Pueden existir múltiples conjuntos de mnemónicos o sintaxis de lenguaje ensamblador para un único conjunto de instrucciones, normalmente instanciadas en diferentes programas ensambladores. En estos casos, el más popular suele ser el suministrado por el fabricante de la CPU y utilizado en su documentación.

Dos ejemplos de CPU que tienen dos conjuntos diferentes de mnemónicos son la familia Intel 8080 y la Intel 8086/8088. Debido a que Intel reclamó los derechos de autor sobre sus mnemotécnicos en lenguaje ensamblador (en cada página de su documentación publicada en la década de 1970 y principios de 1980, al menos), algunas empresas que produjeron CPU de forma independiente compatibles con los conjuntos de instrucciones de Intel inventaron sus propios mnemotécnicos. La CPU Zilog Z80, una mejora de Intel 8080A, admite todas las instrucciones 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, cuando Intel utiliza los mnemotécnicos MOV, MVI, LDA, STA, LXI, LDAX, STAX, LHLD y SHLD para varias instrucciones de transferencia de datos, el lenguaje ensamblador Z80 utiliza el mnemotécnico LD para todos ellos. Un caso similar son las CPUs NEC V20 y V30, copias mejoradas de las Intel 8086 y 8088, respectivamente. Al igual que Zilog con el Z80, NEC inventó nuevos mnemotécnicos 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 compañías de CPU posteriores, como AMD y Cyrix, volvieron a publicar los mnemónicos de instrucción x86/IA-32 de Intel exactamente sin permiso ni sanción legal). Es dudoso que en la práctica muchas personas que programaron el V20 y el V30 en realidad escribieron en el lenguaje ensamblador de NEC en lugar del de Intel; dado que dos lenguajes ensambladores para la misma arquitectura de conjunto de instrucciones son isomorfos (algo así como el inglés y el latín pig), no es necesario usar el lenguaje ensamblador publicado por el fabricante con los productos de ese fabricante.

Diseño de lenguaje

Elementos básicos

Existe un alto grado de diversidad en la forma en que los autores de los ensambladores categorizan las declaraciones y en la nomenclatura que utilizan. En particular, algunos describen cualquier cosa que no sea una mnemotecnia de máquina o una mnemotécnica extendida como una pseudooperación (pseudo-op). Un lenguaje ensamblador típico consta de 3 tipos de instrucciones que se utilizan para definir las operaciones del programa:

Mnemónicos de código de operación y mnemónicos extendidos

Las instrucciones (declaraciones) en lenguaje ensamblador son generalmente muy simples, a diferencia de las de los lenguajes de alto nivel. En general, una regla mnemotécnica es un nombre simbólico para una única instrucción de lenguaje de máquina ejecutable (un código de operación), y hay al menos una regla mnemotécnica de código de operación definida para cada instrucción de lenguaje de máquina. Cada instrucción normalmente consta de una operación o opcode más cero o más operandos. La mayoría de las instrucciones se refieren a un solo valor o a un par de valores. Los operandos pueden ser inmediatos (valor codificado en la instrucción misma), registros especificados en la instrucción o implícitos, o 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. Mnemónicos extendidos a menudo se usan para especificar una combinación de un código de operación con un operando específico, por ejemplo, los ensambladores de System/360 usan B como mnemónico extendido para BC con una máscara de 15 y NOP ("NO OPERACIÓN" – no hacer nada en un paso) para BC con una máscara de 0.

Mnemónicos extendidos a menudo se utilizan para apoyar usos especializados de instrucciones, a menudo para propósitos 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 tienen instrucciones que se pueden usar para ese propósito. En las CPU 8086, la instrucción xchg hacha,hacha se usa para nop, con nop siendo un pseudo-opcode para codificar la instrucción xchg ax, ax. Algunos desensambladores reconocen esto y decodificarán el xchg hacha,ax instrucción como nop. De manera similar, los ensambladores de IBM para System/360 y System/370 utilizan los mnemónicos extendidos NOP y NOPR para BC y BCR con cero máscaras. Para la arquitectura SPARC, se conocen como instrucciones sintéticas.

Algunos ensambladores también admiten macroinstrucciones integradas simples que generan dos o más instrucciones de máquina. Por ejemplo, con algunos ensambladores Z80, la instrucción ld hl,bc se reconoce para generar ld l,c seguido por ld h,b. Estos a veces se conocen como pseudo-opcodes.

Los mnemotécnicos son símbolos arbitrarios; en 1985, el IEEE publicó el estándar 694 para un conjunto uniforme de mnemotécnicos para ser utilizado por todos los ensambladores. Desde entonces, el estándar ha sido retirado.

Directivas de datos

Hay instrucciones que se utilizan para definir elementos de datos para contener 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 los clasifican como pseudooperaciones.

Directivas de ensamblado

Las directivas de ensamblaje, también denominadas pseudocódigos de operación, pseudooperaciones o pseudooperaciones, son comandos que se dan a un ensamblador "que le indican que realice operaciones distintas de las instrucciones de ensamblaje". 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 las directivas que generan código objeto, como las que generan datos.

Los nombres de las pseudooperaciones a menudo comienzan con un punto para distinguirlas de las instrucciones de 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, quizás para diferentes aplicaciones. O bien, se puede usar una pseudo-operación para manipular la presentación de un programa para que sea más fácil de leer y mantener. Otro uso común de pseudo-ops es reservar áreas de almacenamiento para datos en tiempo de ejecución y, opcionalmente, inicializar su contenido a 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 da 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éxicamente distintos de los símbolos normales (por ejemplo, el uso de "10$" como destino GOTO).

Algunos ensambladores, como NASM, brindan una administración de símbolos flexible, lo que permite a los programadores administrar diferentes espacios de nombres, calcular automáticamente las compensaciones dentro de las estructuras de datos y asignar etiquetas que se refieren a valores literales o al resultado de cálculos simples realizados por el ensamblador. Las etiquetas también se pueden usar para inicializar constantes y variables con direcciones reubicables.

Los lenguajes ensambladores, como la mayoría de los demás lenguajes informáticos, permiten agregar comentarios al código fuente del programa que se ignorarán durante el ensamblado. Los comentarios juiciosos 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 "crudo" El lenguaje ensamblador (sin comentarios) generado por compiladores o desensambladores es bastante difícil de leer cuando se deben realizar cambios.

Macros

Muchos ensambladores admiten macros predefinidos, y otros admiten macros definidas por el programador (y repetidamente redefinibles) que involucran secuencias de líneas de texto en las que se incrustan variables y constantes. La definición de macro es más comúnmente 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, se puede usar su nombre en lugar de un mnemotécnico. Cuando el ensamblador procesa tal declaración, 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.

Los ensambladores de macros suelen tener directivas para, por ejemplo, definir macros, definir variables, establecer variables en el resultado de una expresión aritmética, lógica o de cadena, iterar, generar código de forma condicional. Algunas de esas directivas pueden estar restringidas para usar dentro de una definición de macro, por ejemplo, MEXIT en HLASM, mientras que otras pueden estar permitidas dentro de código abierto (fuera de las definiciones de macro), por ejemplo, AIF y COPIAR en HLASM.

En lenguaje ensamblador, el término "macro" representa un concepto más completo que en algunos otros contextos, como el preprocesador en el lenguaje de programación C, donde su directiva #define generalmente se usa para crear macros cortas de una sola línea. Las instrucciones de macros del ensamblador, como las macros en PL/I y algunos otros lenguajes, pueden ser "programas" por sí mismos, ejecutados por interpretación del ensamblador durante el montaje.

Dado que las macros pueden tener 'cortas' nombres pero se expanden a varias o, de hecho, muchas líneas de código, se pueden usar para hacer que los programas en lenguaje ensamblador parezcan mucho más cortos, requiriendo menos líneas de código fuente, como con los lenguajes de nivel superior. También se pueden usar para agregar niveles más altos de estructura a los programas de ensamblaje, opcionalmente introducir código de depuración incrustado a través de parámetros y otras características similares.

Los ensambladores de macros a menudo permiten que las macros tomen parámetros. Algunos ensambladores incluyen lenguajes de macro 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 determinada y que permiten que las macros guarden contexto o intercambien información.. Por lo tanto, una macro puede generar numerosas instrucciones en lenguaje ensamblador o definiciones de datos, según los argumentos de la macro. Esto podría usarse para generar estructuras de datos de estilo de registro o "desenrollado" bucles, por ejemplo, o podría generar algoritmos completos basados en parámetros complejos. Por ejemplo, un "ordenar" macro podría aceptar la especificación de una clave de clasificación compleja y generar código diseñado para esa clave específica, sin necesidad de las pruebas de tiempo de ejecución que serían necesarias para un procedimiento general que interpreta la especificación. Se puede considerar que una organización que utiliza lenguaje ensamblador que se ha ampliado considerablemente 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 usaron macros para implementar una de las primeras máquinas virtuales en SNOBOL4 (1967), que se escribió en el lenguaje de implementación de SNOBOL (SIL), un lenguaje ensamblador para una máquina virtual. La máquina de destino traduciría esto a su código nativo usando un ensamblador de macros. Esto permitió un alto grado de portabilidad para la época.

Las macros se usaban para personalizar sistemas de software a gran escala para clientes específicos en la era del mainframe y también las usaba el personal del cliente para satisfacer las necesidades de sus empleadores. necesidades haciendo versiones específicas de los sistemas operativos del fabricante. Esto lo hicieron, por ejemplo, los programadores de sistemas que trabajan con el sistema de monitorización conversacional/máquina virtual (VM/CMS) de IBM y con el sistema de procesamiento de transacciones en tiempo real de IBM. complementos, el Sistema de control de información del cliente CICS y ACP/TPF, el sistema financiero/de líneas aéreas que comenzó en la década de 1970 y todavía ejecuta muchos sistemas de reserva por computadora (CRS) y sistemas de tarjetas de crédito de gran tamaño en la actualidad.

También es posible usar únicamente las habilidades 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 usando un programa ensamblador de macros puro que contiene líneas de código COBOL en su interior. Operadores de tiempo de ensamblaje que instruyen al ensamblador para generar código arbitrario. IBM OS/360 utiliza macros para realizar la generación del sistema. El usuario especifica las opciones codificando una serie de macros de ensamblador. El ensamblaje de estas macros genera un flujo de trabajo para construir el sistema, incluido el lenguaje de control de trabajo y las declaraciones de control de utilidad.

Esto se debe a que, como se descubrió en la década de 1960, el concepto de "procesamiento de macros" es independiente del concepto de "ensamblado", siendo el primero en términos modernos más procesamiento de textos, procesamiento de texto, 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" establecer variables y hacer pruebas condicionales sobre sus valores. A diferencia de ciertos macroprocesadores anteriores dentro de los ensambladores, el preprocesador C no es Turing-completo porque carece de la capacidad de realizar un bucle o 'ir a', lo que permite que los programas realicen un bucle.

A pesar de la potencia del procesamiento de macros, cayó en desuso en muchos lenguajes de alto nivel (las principales excepciones son C, C++ y PL/I) y permaneció perenne para los ensambladores.

La sustitución de parámetros de macro es estrictamente por nombre: en el momento del procesamiento de la macro, el valor de un parámetro se sustituye textualmente por su nombre. La clase más famosa de errores resultantes fue el uso de un parámetro que en sí mismo era una expresión y no un simple nombre cuando el escritor de macros esperaba un nombre. En la macro:

foo: macro a
cargar a*b

la intención era que la persona que llama proporcionara el nombre de una variable y el "global" la variable o constante b se usaría para multiplicar "a". Si se llama a foo con el parámetro a-c, se produce la expansión de la macro de load a-c*b. Para evitar cualquier posible ambigüedad, los usuarios de macroprocesadores pueden poner entre paréntesis los parámetros formales dentro de las definiciones de macro, o los llamantes pueden poner entre paréntesis los parámetros de entrada.

Soporte para programación estructurada

Se han escrito paquetes de macros que proporcionan elementos de programación estructurados para codificar el flujo de ejecución. El primer ejemplo de este enfoque fue el conjunto de macros Concept-14, propuesto originalmente por Harlan Mills (marzo de 1970) e implementado por Marvin Kessler en la División de Sistemas Federales de IBM, que proporcionó IF/ELSE/ENDIF y control similar. bloques de flujo para programas ensambladores de OS/360. Esta fue una forma de reducir o eliminar el uso de operaciones GOTO en el código ensamblador, uno de los principales factores que causan el código espagueti en el lenguaje ensamblador. Este enfoque fue ampliamente aceptado a principios de la década de 1980 (los últimos días del uso del lenguaje ensamblador a gran escala). El High Level Assembler Toolkit de IBM incluye un paquete de macros de este tipo.

Un diseño curioso fue A-natural, un "orientado a la corriente" ensamblador para 8080/Z80, procesadores de Whitesmiths Ltd. (desarrolladores del sistema operativo Idris similar a Unix, y lo que se informó fue el primer compilador comercial de C). El lenguaje se clasificó como ensamblador porque trabajaba con elementos de máquina sin procesar, como códigos de operación, registros y referencias de memoria; pero incorporó 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 se creó como el lenguaje objeto de un compilador de C, en lugar de para la codificación manual, pero su sintaxis lógica ganó algunos seguidores.

Ha habido poca demanda aparente de ensambladores más sofisticados desde el declive del desarrollo del lenguaje ensamblador a gran escala. A pesar de eso, todavía se están desarrollando y aplicando en los casos en que las limitaciones de recursos o las peculiaridades en la arquitectura del sistema de destino impiden el uso efectivo de lenguajes de nivel superior.

Los ensambladores con un potente motor de macros permiten la programación estructurada a través de macros, como la macro de cambio proporcionada con el paquete Masm32 (este código es un programa completo):

incluir masm32incluirmasm32rt.inc; utilizar la biblioteca Masm32.codedemomain: REPEAT 20interruptor rv()náutica, 9); generar un número entre 0 y 8mov ecx, 7Caso 0impresión "caso 0"Caso ecx; en contraste con la mayoría de los demás idiomas de programación,impresión "caso 7"; el interruptor Masm32 permite "casos variables"Caso 1 .. 3.if eax==1impresión "caso 1".elseif eax==2impresión "caso 2".elseimpresión "casos 1 a 3: otros".endifCaso 4, 6, 8impresión "casos 4, 6 o 8"por defectomov ebx, 19 ; imprimir 20 estrellas. Repitoimpresión "*dec ebx. Hasta luego ¿Firmar? ; bucle hasta que se establezca la bandera de señalTerminawimpresión chr$()13, 10) ENDM Salidafinal demomain

Uso de lenguaje ensamblador

Perspectiva histórica

Los lenguajes ensambladores no estaban disponibles en el momento en que se introdujo la computadora con programa almacenado. A Kathleen Booth "se le atribuye la invención del lenguaje ensamblador" basándose en el trabajo teórico, comenzó en 1947, mientras trabajaba en el ARC2 en Birkbeck, Universidad de Londres, luego de la consulta de Andrew Booth (más tarde su esposo) con el matemático John von Neumann y el físico Herman Goldstine en el Instituto de Estudios Avanzados.

A fines de 1948, la Calculadora automática de almacenamiento de retardo electrónico (EDSAC) tenía un ensamblador (denominado "pedidos iniciales") integrado en su programa de arranque. Utilizaba mnemotécnicos de una letra desarrollados por David Wheeler, a quien la IEEE Computer Society acredita como el creador del primer 'ensamblador'. Los informes sobre el EDSAC introdujeron el término "asamblea" para el proceso de combinar campos en una palabra de instrucción. SOAP (Symbolic Optimal Assembly Program) fue un lenguaje ensamblador para la computadora IBM 650 escrito por Stan Poley en 1955.

Los lenguajes ensambladores eliminan gran parte de la programación de primera generación propensa a errores, tediosa y que consume mucho tiempo necesaria con las primeras computadoras, liberando a los programadores del tedio, como recordar códigos numéricos y calcular direcciones. Alguna vez fueron ampliamente utilizados para todo tipo de programación. Sin embargo, 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. Hoy en día, el lenguaje ensamblador todavía se usa para la manipulación directa del hardware, el acceso a instrucciones especializadas del procesador o para abordar problemas críticos de rendimiento. Los usos típicos son controladores de dispositivos, sistemas integrados de bajo nivel y sistemas en tiempo real (ver § Uso actual).

Históricamente, numerosos programas se han escrito completamente en lenguaje ensamblador. El Burroughs MCP (1961) fue la primera computadora para la cual no se desarrolló un sistema operativo completamente en lenguaje ensamblador; fue escrito 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 de mainframe de IBM escrito por grandes corporaciones. COBOL, FORTRAN y algunos PL/I finalmente desplazaron gran parte de este trabajo, aunque varias organizaciones grandes mantuvieron infraestructuras de aplicaciones en lenguaje ensamblador hasta bien entrada la década de 1990.

La mayoría de las primeras microcomputadoras se basaban en lenguaje ensamblador codificado a mano, incluidos la mayoría de los sistemas operativos y aplicaciones grandes. Esto se debió a que estos sistemas tenían severas restricciones de recursos, impusieron arquitecturas de visualización y memoria idiosincrásicas, y proporcionaron servicios de sistema limitados y con errores. Quizás más importante fue la falta de compiladores de lenguaje de alto nivel de primera clase adecuados para el uso de microcomputadoras. Un factor psicológico también puede haber influido: la primera generación de programadores de microcomputadoras retuvo a un aficionado, "alambres y alicates" actitud.

En un contexto más comercial, las razones más importantes para usar el lenguaje ensamblador fueron la mínima expansión (tamaño), la mínima sobrecarga, la mayor velocidad y la confiabilidad.

Ejemplos típicos de grandes programas en lenguaje ensamblador de esta época son los sistemas operativos IBM PC DOS, el compilador Turbo Pascal y las primeras aplicaciones como el programa de hoja de cálculo Lotus 1-2-3. El lenguaje ensamblador se utilizó para obtener el mejor rendimiento de Sega Saturn, una consola que era notoriamente difícil de desarrollar y programar juegos. El juego de arcade de 1993 NBA Jam es otro ejemplo.

El lenguaje ensamblador ha sido durante mucho tiempo el principal lenguaje de desarrollo para muchas computadoras domésticas populares de las décadas de 1980 y 1990 (como MSX, Sinclair ZX Spectrum, Commodore 64, Commodore Amiga y Atari ST). Esto se debió en gran parte a que los dialectos BASIC interpretados en estos sistemas ofrecían una velocidad de ejecución insuficiente, así como instalaciones insuficientes para aprovechar al máximo el hardware disponible en estos sistemas. Algunos sistemas incluso tienen un entorno de desarrollo integrado (IDE) con funciones de macro y depuración muy avanzadas. Algunos compiladores disponibles para Radio Shack TRS-80 y sus sucesores tenían la capacidad de combinar la fuente de ensamblaje en línea con declaraciones de programa de alto nivel. Tras la compilación, un ensamblador incorporado produjo un código de máquina en línea.

Uso actual

Ha habido debate sobre la utilidad y el rendimiento del lenguaje ensamblador en relación con los lenguajes de alto nivel.

Aunque el lenguaje ensamblador tiene usos de nicho específicos donde es importante (ver más abajo), existen otras herramientas para la optimización.

En julio de 2017, 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. El ensamblador se puede utilizar para optimizar la velocidad o el tamaño. En el caso de la optimización de la velocidad, se afirma que los compiladores de optimización modernos convierten los lenguajes de alto nivel en un código que puede ejecutarse tan rápido como un ensamblaje escrito a mano, a pesar de los contraejemplos que se pueden encontrar. La complejidad de los procesadores modernos y los subsistemas de memoria hace que la optimización efectiva sea cada vez más difícil para los compiladores, así como para los programadores de ensamblaje. Además, el aumento del rendimiento del procesador ha significado que la mayoría de las CPU permanezcan inactivas la mayor parte del tiempo, con demoras causadas por cuellos de botella predecibles, como errores de caché, operaciones de E/S y paginación. Esto ha hecho que la velocidad de ejecución del código sin procesar no sea un problema para muchos programadores.

Existen algunas situaciones en las que los desarrolladores pueden 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 fundamentales como la aritmética binaria, la asignación de memoria, el procesamiento de pilas, la codificación de conjuntos de caracteres, el procesamiento de interrupciones y el diseño del compilador serían difíciles de estudiar en detalle sin comprender cómo funciona una computadora a nivel de hardware. Dado que el comportamiento de una computadora se define fundamentalmente por su conjunto de instrucciones, la forma lógica de aprender dichos conceptos es estudiar un lenguaje ensamblador. La mayoría de las computadoras modernas tienen conjuntos de instrucciones similares. Por lo tanto, estudiar un solo lenguaje ensamblador es suficiente para aprender: I) los conceptos básicos; II) reconocer situaciones donde el uso del lenguaje ensamblador puede ser apropiado; y III) para ver cómo se puede crear un código ejecutable eficiente a partir de lenguajes de alto nivel.

Aplicaciones típicas