Paleta de cores

Paleta de cores

Exercício de aprendizagem

Introdução

Memória

A memória é normalmente mapeada para um programa pelo sistema operativo (SO) segundo um esquema geral:

endereço região de memória
alta pilha
...
heap
segmento de leitura e escrita
segmento de código/só de leitura
baixa reservado

A memória, em segmentos, está organizada em secções, com permissões diferentes.

As funções que definimos até agora estavam todas na secção .text. Esta secção contém dados executáveis só de leitura. Outras secções são usadas para declarar variáveis de dados, que podem ser só de leitura ou de leitura e escrita, mas não são executáveis.

Secção .data

Os dados inicializados são declarados na secção .data.

Em NASM (The Netwide Assembler, o assembler usado nesta track), uma variável inicializada tem um nome, uma diretiva que indica o tamanho dos dados e uma lista de valores separados por vírgulas. Cada um destes está separado do seguinte por um espaço e o rótulo pode, opcionalmente, ser seguido de :.

As principais diretivas e os respetivos tamanhos de dados são:

diretiva tamanho
db 1 byte
dw 2 bytes
dd 4 bytes
dq 8 bytes

Por exemplo, isto declara uma variável de um byte chamada space com o valor 10:

section .data
    space db 10

As variáveis declaradas na section .data são mutáveis, ou seja, são de leitura e escrita. Têm também uma duração de armazenamento estática, o que significa que existem durante toda a execução do programa.

Secção .rodata

A secção .rodata é semelhante à section .data. Ambas as secções contêm dados inicializados, que são declarados da mesma forma e têm a mesma duração de armazenamento.

A principal diferença entre elas é que os dados na section .rodata são imutáveis, ou seja, só de leitura.

Note

As constantes definidas com equ são diferentes das definidas na section .rodata.

Uma constante definida com equ não ocupa espaço em memória e é diretamente substituída pelo seu valor pelo assembler. É, na verdade, um marcador para esse valor.

Por outro lado, as constantes definidas na section .rodata são efetivamente armazenadas em memória, tendo um endereço.

Aceder aos dados

Rótulos e indireção

Os dados declarados têm de ter um nome associado. Este nome chama-se rótulo.

Um rótulo é um símbolo que codifica o endereço específico dos dados em memória. Os endereços em x86-64 são valores de 64 bits.

Em NASM, tentar aceder aos dados diretamente com o seu rótulo não dá a memória alocada, mas sim o seu endereço:

section .data
    example dq 27 ; this declares a 8-byte variable initialized with 27

section .text
fn:
    mov rax, example ; this stores the address of the declared variable in rax, not its contents
    ...

Para aceder ao conteúdo de um endereço de memória, é necessário desreferenciá-lo. Isto chama-se indireção.

Em NASM, isto faz-se com []:

section .data
    example dq -27 ; this declares a 8-byte variable initialized with -27

section .text
fn:
    mov rax, [example] ; this dereferences example and access the value stored in memory (-27)
    ...

No entanto, há situações em que pode existir ambiguidade quanto ao tamanho da memória desreferenciada. Nesses casos, tem de se usar um prefixo que especifique esse tamanho.

Estes são os prefixos mais importantes e os respetivos tamanhos num programa x86-64 típico:

prefixo tamanho
byte 1 byte
word 2 bytes
dword 4 bytes
qword 8 bytes

A mesma leitura pode ser escrita com o tamanho indicado explicitamente:

    mov rax, qword [example] ; same dereference, size stated explicitly

É boa prática usar sempre um prefixo ao desreferenciar memória.

Escrever na memória

Escrever na memória faz-se da mesma forma, desreferenciando um endereço:

section .data
    example1 db 10            ; example1 is a 1-byte memory location initialized with value 10
    example2 dq -456          ; example2 is a 8-byte memory location initialized with value -456
    example3 dd 54            ; example3 is a 4-byte memory location initialized with value 54

section .text
fn:
    mov byte [example1], 20   ; example1 now has value 20
    mov qword [example2], rdx ; example2 now has value equal to the contents in rdx
    mov dword [example3], eax ; example3 now has value equal to the contents in eax

Repara que podes usar operandos de memória na maioria das instruções sem primeiro carregar o conteúdo num registo. No entanto, normalmente não é possível usá-los nos dois operandos, de origem e de destino, apenas num dos dois:

section .data
    example4 dw 4
    example5 dq -8
    example6 dd 15

section .text
fn:
    add word [example4], 5     ; example4 is now a 2-byte memory location with the value 4 + 5 = 9
    imul rax, qword [example5] ; rax = rax * (-8)
    ; this is not possible -> sub dword [example6], dword [example6]
A instrução LEA

Embora se possa usar um mov para armazenar o endereço de uma variável num registo, existe uma instrução com este propósito específico: lea.

Esta instrução usa um operando em forma de memória, mas não lê a memória. Em vez disso, calcula a expressão do endereço efetivo e escreve o resultado no operando de destino:

lea rax, [example] ; this stores the address of 'example' in rax

É mais idiomático usar lea para calcular e armazenar endereços de memória em registos.

Endereçamento relativo

Ao aceder a posições de memória, o comportamento predefinido do NASM é gerar endereços absolutos, ou seja, endereços de memória fixos.

Por razões de segurança, os executáveis são frequentemente compilados como PIE (Position Independent Executable), em que as regiões de memória são colocadas em posições aleatórias. Num PIE, o endereço final de uma variável não é conhecido no momento da ligação. Assim, o código calcula os endereços como um deslocamento a partir do valor de um registo especial chamado rip, que aponta para a próxima instrução a executar.

A isto chama-se normalmente endereçamento relativo ao RIP.

Em NASM podes pedir acesso relativo ao RIP com o operador rel:

mov rax, qword [rel variable]

O endereçamento relativo também pode ser tornado o predefinido para um ficheiro de código fonte com default rel no topo.

Todos os exercícios desta track são compilados e ligados como PIE, por isso deve usar-se rel para gerar endereços relativos.

Visibilidade

Os rótulos (funções e dados) definidos em qualquer secção (por exemplo, .text, .data, .rodata) são visíveis dentro do mesmo ficheiro de código fonte. Se forem declarados como global, também são visíveis para outros ficheiros de código fonte.

Por outro lado, os rótulos definidos noutros ficheiros de código fonte são visíveis para o ficheiro de código fonte atual se forem declarados como extern. Neste caso, não há indicação do tamanho dos dados em assembly; este tem de ser conhecido antecipadamente.

default rel

section .data

global number1 ; 'number1' is a variable visible to other source files
number1 db 200

extern number2 ; 'number2' is a variable visible to the current source file, but defined in another

section .text

extern sum ; sum is a function visible to the current source file, but defined in another

fn:
    mov dil, byte [number1]
    mov sil, byte [number2]
    call sum
    ...

Instruções

O teu amigo José é professor numa escola da zona. Teve uma ideia para umas experiências divertidas que mostram como as cores se podem combinar para produzir outras diferentes.

Pediu-te ajuda para essas experiências.

Note

Neste exercício, uma cor é representada por um número de 32 bits (4 bytes), que codifica o seu valor RGB.

Um valor RGB é composto por 3 canais, vermelho, verde e azul, cada um a ocupar 8 bits (1 byte). O quarto byte está normalmente reservado para o canal alfa, mas neste exercício o seu valor estará vazio (0).

1. Obter o valor RGB de uma cor

Os valores de cada cor já estão guardados numa tabela, definida noutro ficheiro de código-fonte. Cada cor é identificada por um endereço único nesta tabela.

Define uma função get_color_value que devolve o valor de 32 bits de uma cor. Esta função recebe como parâmetro um endereço válido dessa cor na tabela de cores.

get_color_value(black)
// => 0

Dica - 32 bits equivalem a 4 bytes.

2. Adicionar cor base

Para misturar cores diferentes, o José vai primeiro fixar uma cor base e mudar apenas a cor secundária que é combinada com ela.

Define uma função add_base_color que guarda o valor de 32 bits de uma cor na variável base_color, para que possa ser usada mais tarde. Esta função não devolve nada e recebe como parâmetro o endereço da cor na tabela de cores.

A variável base_color és tu que a defines e tem de estar acessível a partir de outros ficheiros de código-fonte.

Nunca haverá mais do que 1 cor base ao mesmo tempo. Se for adicionada uma nova cor base, a antiga é descartada.

Por predefinição, no início do programa, base_color deve ser inicializada com o valor de 32 bits de branco, que é 0xFFFFFF00.

Dica - O NASM aceita números definidos em hexadecimal usando 0x no início, como em 0xFFFFFF00.

3. Definir constantes para as cores primárias

O José espera fazer muitas combinações com as cores primárias, por isso quer tê-las separadas para um acesso rápido. Como está a usar RGB para representar cores, as cores primárias são:

  • RED, com o valor 0xFF000000.
  • GREEN, com o valor 0x00FF0000.
  • BLUE, com o valor 0x0000FF00.

Define uma constante para cada uma dessas cores. Essas constantes têm de estar acessíveis a partir de outros ficheiros de código-fonte.

4. Combinar cores

As cores devem ser combinadas de acordo com uma combining_function definida noutro ficheiro de código-fonte. Esta função recebe como parâmetros os valores de 32 bits de base_color e de uma cor secundária a misturar com ela. Devolve o valor de 32 bits da cor combinada.

Define uma função make_color_combination que combina duas cores e guarda o resultado na memória. Esta função não devolve nada e recebe como parâmetros, por esta ordem:

  • O endereço onde deve ser guardado o valor de 32 bits da cor combinada.
  • O endereço de uma cor secundária na tabela de cores, a combinar com a cor primária.
Caution

Repara que a combining_function pode modificar os valores nos registos que estás a usar. Certifica-te de que guardas na memória qualquer variável de que precises antes de chamares a função.

Editar via GitHub A ligação abre numa nova janela ou separador
x86-64 Assembly Exercism

Estás pronto para começar Paleta de cores?

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