Rutas
/
x86-64 Assembly
x86-64 Assembly
/
Ejercicios
/
Maquetación de revista
Maquetación de revista

Maquetación de revista

Ejercicio de aprendizaje

Introducción

Macros

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.

Macros de una sola línea

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.

Note

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))
Caution

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
    ...

Macros de varias líneas

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.

Etiquetas dentro de una macro

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

Repetición en tiempo de ensamblado

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.

Ensamblado condicional

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

Dividir en archivos

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.

Instrucciones

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.

Caution

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á.

1. Nombra las constantes de formato

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 blanco
  • RULE 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

2. Escribe la macro 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:

  1. la dirección de destino, que se pasa en un registro de propósito general de 64 bits.
  2. el número de bytes (N) que hay que escribir, que se pasa en un registro de propósito general de 64 bits o como valor inmediato.
  3. el byte que hay que escribir, que se pasa como valor inmediato.
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

3. Escribe la macro 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:

  1. la dirección de destino, que se pasa en un registro de propósito general de 64 bits
  2. el margen izquierdo, que se pasa en un registro de propósito general de 64 bits
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}

4. Escribe la macro 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:

  1. la dirección de destino.
  2. un identificador de estilo literal que se pasa como valor inmediato, donde 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} // '.', '-', '#'
Editar en GitHub El enlace se abre en una ventana o pestaña nueva
x86-64 Assembly Exercism

¿Listo para empezar Maquetación de revista?

Regístrate en Exercism para aprender y dominar x86-64 Assembly con 22 conceptos130 ejercicios y mentoría humana real, todo gratis.