El código 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 para reducir el código repetitivo. Además, programar en ensamblador exige manejar valores sin una semántica evidente. No hay parámetros con nombre y el programador tiene que encargarse a mano de la mayoría de los detalles de la maquinaria subyacente.
NASM, el ensamblador que usa el track, ofrece algunas herramientas que pueden ayudar con ambos problemas. Estas herramientas se ejecutan en tiempo de ensamblado, antes de que ninguna instrucción llegue al procesador. Le indican al ensamblador 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 se pueden ver como una receta para el ensamblador. Se expanden en el lugar donde se usan, como si el código se hubiera escrito ahí 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 simple le da un nombre a un valor o a una expresión corta con %define:
%define WIDTH 8
%define BUFFER_SIZE (WIDTH * 16)
Cada aparición posterior de WIDTH se reemplaza por 8 antes del ensamblado.
El reemplazo es textual, así que el nombre se sustituye en cualquier lugar 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 se puede reasignar o dejar sin definir.
Una constante declarada con equ, en cambio, se evalúa en el punto de definición y nunca se modifica.
Un %define también puede recibir 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)), la 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 busca.
Las macros de una sola línea son útiles para darle significado a distintas partes del código. Si se usan con cuidado, mejoran la legibilidad y reducen la probabilidad de errores de escritura. Por ejemplo, un registro que tiene un propósito específico dentro de una función se puede definir por su propósito:
%define COUNTER rcx
...
.loop:
...
dec COUNTER
jnz .loop
...
Una secuencia completa 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 pueden 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 hace referencia a ellos como %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
Fíjate que los argumentos deben tener 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 sí se ensamblan, pero mult_two eax, cl no, ya que imul eax, cl no está definido.
Una macro que define una etiqueta normal se rompe en el momento en que se usa dos veces, porque entonces la etiqueta también quedaría definida dos veces.
Para usar etiquetas dentro de una macro que puede llamarse más de una vez, es necesario poner el prefijo %% delante de esa etiqueta.
Esto le indica al ensamblador que le 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.
Esto es 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 en su lugar:
%rep 2 ; number of repetitions: 2
add rcx, 32
mov dword [rsi + rcx], 0
%endrep
Fíjate que el número de repeticiones se especifica justo después de %rep y que hay un %endrep para cerrar el bloque repetido.
Es común 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 se puede declarar con equ, para las variables enteras de tiempo de ensamblado usamos %assign.
A diferencia de %define, %assign se evalúa de inmediato a un número y se puede reasignar:
xor eax, eax
%assign i 0
%rep 4
add eax, i ; add 0, then 1, then 2, then 3
%assign i i + 1
%endrep
Después de que se ejecuta el preprocesador, el bucle desaparece y quedan cuatro instrucciones add, con los valores inmediatos 0, 1, 2 y 3 incorporados.
Esto es el desenrollado de bucles que hace el ensamblador: sin registro contador y sin saltos, solo código lineal.
El preprocesador tiene varios condicionales disponibles. Seleccionan entre varias rutas distintas y expanden su cuerpo ahí mismo, 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 literalmente en ese punto, antes del ensamblado, exactamente como si su contenido se hubiera escrito ahí mismo. Esto mantiene las definiciones en un solo lugar y permite que varios archivos fuente las compartan.
Si el archivo de macros se puede ensamblar por sí solo, debe tener la extensión .asm.
Fíjate que las macros no tienen vinculación.
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 común.
Estás a cargo de la biblioteca de macros sobre la que se construye el motor de composición de una revista. El motor en sí, es decir, las funciones que componen cada página, ya está hecho y no lo escribes tú. Lo que tú escribes es la maquinaria de tiempo de ensamblado a partir de la cual se construye.
Este ejercicio no te pide que escribas ninguna función, solo macros. Luego, estas macros se incluyen y se llaman desde funciones que ya vienen predefinidas en otro archivo.
Tienes cuatro tareas.
Cada macro se llama con el nombre y el orden de argumentos exactos que indica su tarea, así que escríbelas tal cual. 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 composición se refiere a ellos solo por su nombre. Dale 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 los reporta en el orden de arriba.
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 traza los filetes de la cabecera estampando una secuencia de un único byte. Captura ese paso en 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 llenar 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.
Fíjate que la cantidad de bytes se puede pasar 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 esta cantidad nunca es negativa.
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 se pueden calcular 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
La cantidad de desplazamientos que se escriben en la dirección de destino es fija 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 encabezado definido 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 simple, 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 se le puede sumar 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 encabezado (PLAIN, RULED o BOXED) con ensamblado condicional (%if / %elif / %else) en lugar de una bifurcación en tiempo de ejecución.
Esta macro no debe 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.