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.
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.
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.
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.
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 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]
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.
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.
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
...
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.
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).
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.
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.
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.
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:
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.
Inscreve-te no Exercism para aprenderes e dominares x86-64 Assembly com 22 conceitos130 exercícios, e mentoria humana real, tudo grátis.