El lenguaje ensamblador escrito a mano está lleno de repetición. La misma secuencia corta de instrucciones aparece una y otra vez, con pocas variaciones, y hay pocas abstracciones que reduzcan el código repetitivo. Además, programar en ensamblador obliga a gestionar valores sin una semántica evidente. No hay parámetros con nombre y el programador debe encargarse a mano de la mayoría de los detalles de la maquinaria subyacente.
NASM, el ensamblador que utiliza el track, ofrece algunas herramientas que pueden ayudar al programador con ambos problemas. Estas herramientas se ejecutan en el tiempo de ensamblado, antes de que ninguna instrucción llegue al procesador. Guían al ensamblador sobre cómo reescribir el texto fuente antes incluso de que se traduzca a código máquina.
La más importante de estas herramientas son las macros, que pueden entenderse como una receta para el ensamblador. Se expanden en el lugar donde se usan, como si el código se hubiera escrito allí a mano.
Las macros pueden recibir varios tokens literales como parámetros, lo que las hace reutilizables en distintos contextos.
Sin embargo, no son funciones: no hay call, ni ret, ni transferencia de control a otro punto del código.
La definición más sencilla da un nombre a un valor o a una expresión corta con %define:
%define WIDTH 8
%define BUFFER_SIZE (WIDTH * 16)
Toda aparición posterior de WIDTH se sustituye por 8 antes del ensamblado.
La sustitución es textual, así que el nombre se reemplaza allá donde aparezca, ya sea en un operando o dentro de otra definición.
Para las constantes numéricas, es preferible usar equ en lugar de %define.
Una macro de una sola línea declarada con %define solo se expande cuando se usa, y puede reasignarse o anularse.
Una constante equ, en cambio, se evalúa en el punto de definición y nunca se modifica.
Un %define también puede aceptar parámetros con nombre, y se comporta como una pequeña función en línea:
%define double(x) ((x) + (x))
mov eax, double(WIDTH) ; assembled as mov eax, ((8) + (8))
Encierra entre paréntesis tanto el cuerpo completo como cada parámetro.
Con %define square(x) x*x, la llamada square(1+1) se expande a 1+1*1+1, que es 3.
Escrito como %define square(x) ((x)*(x)), esa misma llamada se expande a ((1+1)*(1+1)), que es 4.
Los nombres definidos con %define distinguen entre mayúsculas y minúsculas.
La variante %idefine hace que el nombre no distinga entre mayúsculas y minúsculas, si es lo que se quiere.
Las macros de una sola línea son útiles para dar significado a distintas partes del código. Si se usan con cuidado, mejoran la legibilidad y reducen la probabilidad de erratas. Por ejemplo, un registro que tiene un propósito concreto dentro de una función puede definirse por su propósito:
%define COUNTER rcx
...
.loop:
...
dec COUNTER
jnz .loop
...
Toda una secuencia de código, con cualquier número de líneas, se captura con %macro y se cierra con %endmacro.
Un número después del nombre indica cuántos argumentos recibe la macro, que puede ser cero.
Cuando se pasan argumentos, se separan con una coma (,):
%macro example_macro 0
...
%endmacro
Los argumentos de las macros de varias líneas no tienen nombre.
Se nombran %1, %2, etcétera, en el orden en que se pasan:
; Multiplies two values, returning the result in the first argument
%macro mult_two 2 ; %1 = accumulator, %2 = value
imul %1, %2
%endmacro
mult_two eax, ecx ; multiplies eax and ecx and returns the result into eax
mult_two r8, r9 ; multiplies r8 and r9 and returns the result into r8
Ten en cuenta que los argumentos deben ser una sintaxis válida para las operaciones que se realizan en el cuerpo de la macro.
Aquí mult_two eax, ecx y mult_two r8, r9 se ensamblan, pero mult_two eax, cl no lo haría, ya que imul eax, cl no está definido.
Una macro que define una etiqueta normal se rompe en cuanto se usa dos veces, porque entonces la etiqueta también estaría definida dos veces.
Para usar etiquetas dentro de una macro que puede llamarse más de una vez, es necesario anteponer %% a esas etiquetas.
Esto indica al ensamblador que dé a esa etiqueta un nombre distinto y único en cada expansión:
; clobbers rcx
%macro clear_n 2 ; %1 = base address, %2 = count of dwords to clear
xor ecx, ecx
%%loop:
mov dword [%1 + rcx*4], 0
inc ecx
cmp ecx, %2 ; %2 is compared with ecx: it is an immediate or a 32-bit operand
jb %%loop
%endmacro
clear_n rdi, 4 ; gets its own %%loop
clear_n rsi, 8 ; a different %%loop, no clash
Es posible repetir un bloque de código de una línea con times.
Resulta especialmente útil al declarar valores repetidos en memoria:
section .data
negative_block: times 16 db -1
; this assembles to 16 db -1 in sequence, i.e., an array of 16 bytes, all equal to -1
Para repetir código de varias líneas, podemos usar %rep:
%rep 2 ; number of repetitions: 2
add rcx, 32
mov dword [rsi + rcx], 0
%endrep
Ten en cuenta que el número de repeticiones se indica justo después de %rep y que hay un %endrep para cerrar el bloque repetido.
Es habitual usar un contador de tiempo de ensamblado dentro de un bloque %rep, igual que un bucle en tiempo de ejecución usa un contador.
Aunque una constante entera de tiempo de ensamblado puede declararse con equ, para las variables enteras de tiempo de ensamblado usamos %assign.
A diferencia de %define, %assign se evalúa inmediatamente a un número y puede reasignarse:
xor eax, eax
%assign i 0
%rep 4
add eax, i ; add 0, then 1, then 2, then 3
%assign i i + 1
%endrep
Cuando el preprocesador termina, el bucle desaparece y quedan cuatro instrucciones add, con los inmediatos 0, 1, 2 y 3 integrados.
Esto es el desenrollado de bucles que hace el ensamblador: sin registro contador y sin salto, solo código lineal.
El preprocesador dispone de varios condicionales. Seleccionan entre varias rutas distintas y expanden su cuerpo en el mismo sitio, pero solo cuando su condición se cumple.
%if ... %elif ... %else ... %endif seleccionan una rama a partir de una prueba numérica:
%if WIDTH > 4
mov eax, 2
%elif WIDTH > 2
mov eax, 1
%else
mov eax, 0
%endif
A menudo las macros se guardan en un archivo propio y se incorporan a cada fuente que las necesita con %include:
%include "patterns.mac"
El archivo indicado se inserta tal cual en ese punto, antes del ensamblado, exactamente como si su contenido se hubiera escrito allí. Así las definiciones quedan en un solo lugar y varios archivos fuente pueden compartirlas.
Si el archivo de macros puede ensamblarse por sí solo, debería tener la extensión .asm.
Ten en cuenta que las macros no tienen enlazado.
Aunque el archivo de macros se ensamble, sus macros solo son visibles para otro archivo que lo incluya con %include.
En caso contrario, un archivo que solo se use con %include puede tener cualquier extensión.
Usar .asm o .mac es una convención habitual.
Mantienes la biblioteca de macros sobre la que está construido el motor de composición de una revista. El motor en sí, las funciones que maquetan cada página, es fijo y no lo escribes tú. Lo que escribes es la maquinaria de tiempo de ensamblado a partir de la cual está construido.
Este ejercicio no requiere que escribas ninguna función, solo macros. Estas macros se incluyen y se llaman después en funciones ya predefinidas en otro archivo.
Tienes cuatro tareas.
Cada macro se llama con el nombre exacto y el orden de argumentos que indica su tarea, así que escríbelas exactamente así. De lo contrario, el archivo no se ensamblará.
El formato de la revista es un puñado de números fijos, y el resto de la maquetación se refiere a ellos solo por su nombre. Deberías dar a cada uno un nombre legible en tiempo de ensamblado:
COLUMN_COUNT es 4
COLUMN_STRIDE es 18
SPACE es 0x20, el byte en blancoRULE es 0x3D, el byte =
RULE_WIDTH es 4
PLAIN es 0x2E, el byte .
RULED es 0x2D, el byte -
BOXED es 0x23, el byte #
El motor informa de estos en el orden anterior.
out = {4, 18, 32, 61, 4, 46, 45, 35} // the eight named values, in order
fill_run
El motor deja en blanco una galerada y coloca los filetes de la cabecera estampando una secuencia de un solo byte. Captura ese paso como una macro de varias líneas que el motor pueda llamar allí donde haya que rellenar una secuencia.
Primero, define un nombre legible COUNTER para el registro rcx.
Después, define la macro fill_run, que debe rellenar un array con N copias de un byte.
Recibe tres argumentos en este orden:
fill_run rdi, rax, SPACE
fill_run rdx, RULE_WIDTH, RULE
La macro fill_run puede sobrescribir COUNTER libremente, pero ningún otro registro.
Ten en cuenta que el número de bytes puede pasarse en un registro de propósito general de 64 bits, como rax o rsi, o como un valor inmediato, como 10 o 4.
Puedes dar por hecho que este número nunca es negativo.
Esta macro se llama tres veces, en dos funciones distintas.
length = 8
page = {32, 32, 32, 32, 32, 32, 32, 32}
width = 8
page = {61, 61, 61, 61, 32, 32, 32, 32} // '====' then spaces
lay_columns
La retícula es fija para el formato, así que los inicios de columna pueden calcularse en tiempo de ensamblado.
Define la macro lay_columns, que escribe COLUMN_COUNT desplazamientos, cada uno un entero de 64 bits.
Recibe dos argumentos en este orden:
lay_columns rsi, rax
El número de desplazamientos que hay que escribir en la dirección de destino es fijo e igual a COLUMN_COUNT.
Cada desplazamiento es un valor de 64 bits que se calcula con offset[i] = margin + i * COLUMN_STRIDE, donde i es el índice del desplazamiento, que empieza en 0, y margin es el segundo argumento.
Esta macro puede sobrescribir sus dos argumentos de registro, pero ningún otro. Se llama una vez.
margin = 3
out = {3, 21, 39, 57}
set_header
Cada una de las tres secciones fijas tiene un estilo de cabecera fijado por el formato, así que el byte se conoce en tiempo de ensamblado.
Define set_header, que recibe dos argumentos en este orden:
0 es liso, 1 es con filete y 2 es con recuadro.La dirección de destino es la expresión de una dirección de memoria efectiva.
Toma un registro base al que puede sumarse un registro índice escalado por 1, 2, 4 u 8, y un valor inmediato:
set_header rdx+8*rcx+2, 1
Selecciona el byte de la cabecera (PLAIN, RULED o BOXED) con ensamblado condicional (%if / %elif / %else) en lugar de una bifurcación en tiempo de ejecución.
Esta macro no debería sobrescribir ningún registro. Se llama tres veces en la misma función.
pitch = 4
out = {46, 0, 0, 0, 45, 0, 0, 0, 35} // '.', '-', '#'
Regístrate en Exercism para aprender y dominar x86-64 Assembly con 22 conceptos130 ejercicios y mentoría humana real, todo gratis.