Layout de revista

Layout de revista

Exercício de aprendizagem

Introdução

Macros

O assembly escrito à mão está cheio de repetição. A mesma sequência curta de instruções aparece uma e outra vez, com pouca variação, e há poucas abstrações para reduzir o código repetitivo. Além disso, programar em assembly obriga a gerir valores sem semântica óbvia. Não há parâmetros com nome e a maioria dos detalhes da máquina subjacente tem de ser gerida manualmente pelo programador.

O NASM, o assembler usado pela track, oferece algumas ferramentas que podem ajudar o programador em ambos os problemas. Estas ferramentas atuam em tempo de assemblagem, antes de qualquer instrução chegar ao processador. Orientam o assembler na forma como deve reescrever o texto-fonte antes de este ser sequer traduzido para código máquina.

A mais importante destas ferramentas são as macros, que podem ser encaradas como uma receita para o assembler. São expandidas no local onde são usadas, como se o código tivesse sido escrito ali à mão.

As macros podem receber vários tokens literais como parâmetros, o que as torna reutilizáveis em contextos diferentes. No entanto, não são funções: não há call, nem ret, nem transferência de controlo para outro ponto do código.

Macros de uma linha

A definição mais simples dá um nome a um valor ou a uma expressão curta com %define:

%define WIDTH 8
%define BUFFER_SIZE (WIDTH * 16)

Todas as ocorrências posteriores de WIDTH são substituídas por 8 antes da assemblagem. A substituição é textual, por isso o nome é substituído onde quer que apareça, seja num operando ou dentro de outra definição.

Note

Para constantes numéricas, prefere equ em vez de %define.

Uma macro de uma linha declarada com %define só é expandida quando é usada, podendo ser reatribuída ou removida a sua definição. Uma constante equ, por outro lado, é avaliada no ponto de definição e nunca é modificada.

Um %define também pode receber parâmetros com nome, comportando-se como uma pequena função inline:

%define double(x) ((x) + (x))

    mov eax, double(WIDTH)   ; assembled as mov eax, ((8) + (8))
Caution

Coloca entre parênteses tanto o corpo completo como cada parâmetro. Com %define square(x) x*x, a chamada square(1+1) expande para 1+1*1+1, que é 3. Escrito como %define square(x) ((x)*(x)), a mesma chamada expande para ((1+1)*(1+1)), que é 4.

Os nomes definidos com %define distinguem maiúsculas de minúsculas. A variante %idefine faz com que o nome não distinga maiúsculas de minúsculas, caso se queira.

As macros de uma linha são úteis para dar significado a várias partes do código. Se usadas com cuidado, melhoram a legibilidade e reduzem a probabilidade de erros de escrita. Por exemplo, um registo com uma finalidade específica dentro de uma função pode ser definido por essa finalidade:

%define COUNTER rcx
    ...
.loop:
    ...
    dec COUNTER
    jnz .loop
    ...

Macros de várias linhas

Toda uma sequência de código, com qualquer número de linhas, é capturada com %macro e fechada com %endmacro. Um número após o nome indica quantos argumentos a macro recebe, que pode ser zero. Quando são passados argumentos, estes são separados por uma vírgula (,):

%macro example_macro 0
...
%endmacro

Os argumentos das macros de várias linhas não têm nome. São referidos como %1, %2, e assim por diante, pela ordem em que são passados:

; 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

Repara que os argumentos têm de ser sintaxe válida para as operações realizadas no corpo da macro. Aqui, mult_two eax, ecx e mult_two r8, r9 são ambos assemblados, mas mult_two eax, cl não, uma vez que imul eax, cl não está definido.

Etiquetas dentro de uma macro

Uma macro que define uma etiqueta comum quebra-se no momento em que é usada duas vezes, porque a etiqueta também seria definida duas vezes. Para usar etiquetas dentro de uma macro que possa ser chamada mais do que uma vez, é necessário prefixar essa etiqueta com %%. Isto instrui o assembler a dar a esta etiqueta um nome diferente e único em cada expansão:

; 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

Repetir em tempo de assemblagem

É possível repetir um bloco de código de uma linha com times. Isto é particularmente útil ao declarar valores repetidos na memória:

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 várias linhas, podemos usar antes %rep:

%rep 2          ; number of repetitions: 2
    add rcx, 32
    mov dword [rsi + rcx], 0
%endrep

Repara que o número de repetições é especificado logo após %rep e que há um %endrep para fechar o bloco repetido.

É comum usar um contador de tempo de assemblagem dentro de um bloco %rep, tal como um ciclo em tempo de execução usa um contador. Enquanto uma constante inteira de tempo de assemblagem pode ser declarada com equ, para variáveis inteiras de tempo de assemblagem usamos antes %assign. Ao contrário de %define, %assign é avaliado imediatamente para um número e pode ser reatribuído:

    xor eax, eax
%assign i 0
%rep 4
    add eax, i           ; add 0, then 1, then 2, then 3
%assign i i + 1
%endrep

Depois de o pré-processador correr, o ciclo desaparece e restam quatro instruções add, com os valores imediatos 0, 1, 2 e 3 embutidos. Isto é o desenrolar de ciclos feito pelo assembler: sem registo contador e sem salto, apenas código linear.

Assemblagem condicional

Existem vários condicionais disponíveis para o pré-processador. Selecionam entre vários caminhos diferentes, expandindo o seu corpo no local, mas apenas quando a sua condição é verdadeira.

%if ... %elif ... %else ... %endif selecionam um ramo a partir de um teste numérico:

%if WIDTH > 4
    mov eax, 2
%elif WIDTH > 2
    mov eax, 1
%else
    mov eax, 0
%endif

Dividir em ficheiros

As macros são frequentemente guardadas num ficheiro próprio e incluídas em cada fonte que precisa delas com %include:

%include "patterns.mac"

O ficheiro indicado é inserido tal e qual naquele ponto, antes da assemblagem, exatamente como se o seu conteúdo tivesse sido escrito ali. Isto mantém as definições num único local e permite que vários ficheiros-fonte as partilhem.

Se o ficheiro de macros puder ser assemblado de forma autónoma, deve ter a extensão .asm. Repara que as macros não têm ligação. Mesmo que o ficheiro de macros seja assemblado, as suas macros só são visíveis para outro ficheiro que o inclua com %include.

Caso contrário, um ficheiro usado apenas com %include pode ter qualquer extensão. A utilização de .asm ou .mac é uma convenção comum.

Instruções

Fazes a manutenção da biblioteca de macros sobre a qual assenta o motor de composição de uma revista. O próprio motor, as funções que compõem cada página, é fixo e não és tu que o escreves. O que escreves é a maquinaria de assemblagem a partir da qual ele é construído.

Este exercício não te pede para escreveres nenhuma função, apenas macros. Estas macros são depois incluídas e chamadas em funções já predefinidas noutro ficheiro.

Tens quatro tarefas.

Caution

Cada macro é chamada pelo nome exato e pela ordem de argumentos que a sua tarefa indica, por isso escreve-as exatamente assim. Caso contrário, o ficheiro não será montado.

1. Nomeia as constantes de formato

O formato da revista é um conjunto de números fixos, e o resto da composição refere-se a eles apenas pelo nome. Deves dar a cada um deles um nome legível em tempo de assemblagem:

  • COLUMN_COUNT é 4
  • COLUMN_STRIDE é 18
  • SPACE é 0x20, o byte em branco
  • RULE é 0x3D, o byte =
  • RULE_WIDTH é 4
  • PLAIN é 0x2E, o byte .
  • RULED é 0x2D, o byte -
  • BOXED é 0x23, o byte #

O motor devolve estes valores pela ordem acima indicada.

out = {4, 18, 32, 61, 4, 46, 45, 35} // the eight named values, in order

2. Escreve a macro fill_run

O motor esvazia uma galé e traça as linhas do cabeçalho carimbando uma sequência de um único byte. Captura esse passo numa macro de várias linhas que o motor possa chamar sempre que for preciso preencher uma sequência.

Primeiro, define um nome legível COUNTER para o registo rcx. Depois, define a macro fill_run, que deve preencher um array com N cópias de um byte. Recebe três argumentos, por esta ordem:

  1. o endereço de destino, passado num registo de uso geral de 64 bits.
  2. o número de bytes (N) a escrever, passado num registo de uso geral de 64 bits ou como imediato.
  3. o byte a escrever, passado como imediato.
fill_run rdi, rax, SPACE
fill_run rdx, RULE_WIDTH, RULE

A macro fill_run pode sobrescrever o COUNTER à vontade, mas mais nenhum registo.

Tem em atenção que o número de bytes pode ser passado num registo de uso geral de 64 bits, como rax ou rsi, ou como imediato, como 10 ou 4. Podes assumir que este número nunca é negativo.

Esta macro é chamada três vezes, em duas funções diferentes.

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. Escreve a macro lay_columns

A grelha é fixa para o formato, por isso o início das colunas pode ser calculado em tempo de assemblagem.

Define a macro lay_columns, que escreve COLUMN_COUNT deslocamentos, cada um deles um inteiro de 64 bits. Recebe dois argumentos, por esta ordem:

  1. o endereço de destino, passado num registo de uso geral de 64 bits
  2. a margem esquerda, passada num registo de uso geral de 64 bits
lay_columns rsi, rax

O número de deslocamentos a escrever no endereço de destino é fixo e igual a COLUMN_COUNT.

Cada deslocamento é um valor de 64 bits calculado com offset[i] = margin + i * COLUMN_STRIDE, em que i é o índice do deslocamento, a começar em 0, e margin é o segundo argumento.

Esta macro pode sobrescrever os seus dois argumentos de registo, mas mais nenhum. É chamada uma vez.

margin = 3
out    = {3, 21, 39, 57}

4. Escreve a macro set_header

Cada uma das três secções permanentes tem um estilo de cabeçalho determinado pelo formato, por isso o byte é conhecido em tempo de assemblagem.

Define set_header, que recebe dois argumentos, por esta ordem:

  1. o endereço de destino.
  2. um identificador de estilo literal, passado como imediato, em que 0 é simples, 1 é com linha e 2 é com caixa.

O endereço de destino é a expressão de um endereço de memória efetivo. Aceita um registo de base que pode ser somado com um registo de índice escalado por 1, 2, 4 ou 8, e um imediato:

set_header rdx+8*rcx+2, 1

Seleciona o byte do cabeçalho (PLAIN, RULED ou BOXED) com assemblagem condicional (%if / %elif / %else), em vez de uma ramificação em tempo de execução.

Esta macro não deve sobrescrever nenhum registo. É chamada três vezes na mesma função.

pitch = 4
out   = {46, 0, 0, 0, 45, 0, 0, 0, 35} // '.', '-', '#'
Editar via GitHub A ligação abre numa nova janela ou separador
x86-64 Assembly Exercism

Estás pronto para começar Layout de revista?

Inscreve-te no Exercism para aprenderes e dominares x86-64 Assembly com 22 conceitos130 exercícios, e mentoria humana real, tudo grátis.