Paleta de cores

Paleta de cores

Exercício de aprendizagem

Introdução

Memória

A memória normalmente é mapeada para um programa pelo Sistema Operacional (SO) em um layout geral:

endereço região de memória
alto pilha
...
heap
segmento de leitura e escrita
segmento de código/somente leitura
baixo reservado

A memória em segmentos é organizada em seções, com permissões diferentes.

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

Seção .data

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

No NASM (The Netwide Assembler, o montador usado por esta trilha), uma variável inicializada tem um nome, uma diretiva que indica o tamanho dos dados e uma lista de valores separados por vírgula. Cada um desses elementos é separado do outro por um espaço, e o rótulo pode, opcionalmente, ser seguido de :.

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

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

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

section .data
    space db 10

Variáveis declaradas na seção .data são mutáveis, ou seja, são de leitura e escrita. Elas também têm duração de armazenamento estática, o que significa que existem durante toda a execução do programa.

Seção .rodata

A seção .rodata é semelhante a section .data. As duas seçõ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 em section .rodata são imutáveis, ou seja, somente leitura.

Note

Constantes definidas com equ são diferentes das definidas em section .rodata.

Uma constante definida com equ não ocupa espaço na memória e é substituída diretamente pelo seu valor pelo montador. Na verdade, ela funciona como um marcador desse valor.

Por outro lado, as constantes definidas em section .rodata são de fato armazenadas na memória e têm um endereço.

Acessando dados

Rótulos e indireção

Dados declarados precisam ter um nome associado a eles. Esse nome é chamado de rótulo.

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

No NASM, tentar acessar os dados diretamente pelo rótulo não devolve a memória alocada, mas sim o endereço dela:

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 acessar o conteúdo de um endereço de memória, é necessário desreferenciá-lo. Isso é chamado de indireção.

No NASM, isso é feito 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á algumas situações em que pode haver ambiguidade quanto ao tamanho da memória desreferenciada. Nesses casos, é preciso usar um prefixo que especifique esse tamanho.

Estes são os prefixos mais importantes e seus tamanhos em um programa típico de x86-64:

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

A mesma carga pode ser escrita com o tamanho declarado explicitamente:

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

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

Escrevendo na memória

Escrever na memória é feito 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

Repare que você pode usar operandos de memória na maioria das instruções sem primeiro carregar o conteúdo em um registrador. No entanto, geralmente não é possível usá-los tanto no operando de origem quanto no de destino, apenas em um 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 um mov possa ser usado para armazenar o endereço de uma variável em um registrador, existe uma instrução com esse propósito específico: lea.

Essa instrução usa um operando em formato de memória, mas não lê a memória. Em vez disso, ela 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 registradores.

Endereçamento relativo

Ao acessar posições de memória, o comportamento padrão do NASM é gerar endereços absolutos, ou seja, endereços de memória fixos.

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

Isso costuma ser chamado de endereçamento relativo a RIP.

No NASM, você pode solicitar acesso relativo a RIP com o operador rel:

mov rax, qword [rel variable]

Também é possível tornar o endereçamento relativo o padrão de um arquivo-fonte com default rel no topo.

Todos os exercícios desta trilha são compilados e ligados como PIE, então rel deve ser usado para gerar endereços relativos.

Visibilidade

Rótulos (funções e dados) definidos em qualquer seção (por exemplo, .text, .data, .rodata) são visíveis dentro do mesmo arquivo-fonte. Se declarados como global, eles também são visíveis para outros arquivos-fonte.

Por outro lado, rótulos definidos em outros arquivos-fonte são visíveis para o arquivo-fonte atual se declarados como extern. Nesse caso, não há indicação do tamanho dos dados no assembly, ele precisa ser conhecido de antemão.

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

Seu amigo José é professor em uma escola local. Ele teve uma ideia para alguns experimentos divertidos para mostrar como cores podem ser combinadas para produzir cores diferentes.

Ele pediu sua ajuda com esses experimentos.

Note

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

Um valor RGB consiste em 3 canais, vermelho, verde e azul, cada um ocupando 8 bits (1 byte). O quarto byte geralmente é reservado para o canal Alfa, mas neste exercício seu valor será vazio (0).

1. Obter o valor RGB de uma cor

Os valores de cada cor já estão armazenados em uma tabela, definida em outro arquivo-fonte. Uma cor é identificada por um endereço único nessa tabela.

Defina uma função get_color_value que retorna o valor de 32 bits de uma cor. Essa 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, José primeiro vai fixar uma cor base e mudar apenas a cor secundária que está sendo combinada com ela.

Defina uma função add_base_color que salva o valor de 32 bits de uma cor na variável base_color, para que esse valor possa ser usado depois. Essa função não tem valor de retorno e recebe como parâmetro o endereço da cor na tabela de cores.

A variável base_color é definida por você e precisa ser acessível a partir de outros arquivos-fonte.

Não haverá mais de 1 cor base ao mesmo tempo. Se uma nova cor base for adicionada, a antiga é descartada.

Por padrã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 cores primárias

José espera fazer muitas combinações usando cores primárias, então quer tê-las separadas para acesso rápido. Como ele está usando 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.

Defina uma constante para cada uma dessas cores. Essas constantes precisam ser acessíveis a partir de outros arquivos-fonte.

4. Combinar cores

As cores devem ser combinadas de acordo com uma combining_function definida em outro arquivo-fonte. Essa função recebe como parâmetros os valores de 32 bits de base_color e de uma cor secundária a ser misturada com ela. Ela retorna o valor de 32 bits da cor combinada.

Defina uma função make_color_combination que combina duas cores e salva o resultado na memória. Essa função não tem valor de retorno e recebe como parâmetros, nesta ordem:

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

Repare que combining_function pode modificar os valores nos registradores que você está usando. Certifique-se de salvar na memória qualquer variável de que você precise antes de chamar a função.

Editar via GitHub O link abre em uma nova janela ou aba
x86-64 Assembly Exercism

Tudo pronto para começar Paleta de cores?

Crie sua conta no Exercism para aprender e dominar x86-64 Assembly com 22 conceitos130 exercícios e mentoria humana de verdade, tudo de graça.