Tracks
/
x86-64 Assembly
x86-64 Assembly
/
Ejercicios
/
Paleta de colores
Paleta de colores

Paleta de colores

Ejercicio de aprendizaje

Introducción

Memoria

Por lo general, el sistema operativo (SO) mapea la memoria de un programa con una distribución general:

dirección región de memoria
alta pila
...
montículo
segmento de lectura y escritura
segmento de código/solo lectura
baja reservada

La memoria en segmentos se organiza en secciones, con diferentes permisos.

Las funciones que hemos definido hasta ahora estaban todas en la sección .text. Esta sección contiene datos ejecutables de solo lectura. Otras secciones se usan para declarar variables de datos, que pueden ser de solo lectura o de lectura y escritura, pero no son ejecutables.

Sección .data

Los datos inicializados se declaran en la sección .data.

En NASM (The Netwide Assembler, el ensamblador que usa este track), una variable inicializada tiene un nombre, una directiva que indica el tamaño de los datos y una lista de valores separados por comas. Cada uno de estos se separa del siguiente con un espacio, y la etiqueta puede ir opcionalmente seguida de :.

Las directivas principales y sus tamaños de datos correspondientes son:

directiva tamaño
db 1 byte
dw 2 bytes
dd 4 bytes
dq 8 bytes

Por ejemplo, esto declara una variable de un byte llamada space con el valor 10:

section .data
    space db 10

Las variables declaradas en section .data son mutables, es decir, son de lectura y escritura. También tienen una duración de almacenamiento estática, lo que significa que existen durante toda la ejecución del programa.

Sección .rodata

La sección .rodata es similar a section .data. Ambas secciones contienen datos inicializados, que se declaran de la misma manera y tienen la misma duración de almacenamiento.

La principal diferencia entre ellas es que los datos en section .rodata son inmutables, es decir, de solo lectura.

Note

Las constantes definidas con equ son diferentes de las definidas en section .rodata.

Una constante definida con equ no ocupa espacio en memoria y el ensamblador la sustituye directamente por su valor. De hecho, es un marcador de posición para ese valor.

Por otro lado, las constantes definidas en section .rodata sí se almacenan en memoria y tienen una dirección.

Acceder a los datos

Etiquetas e indirección

Los datos declarados deben tener un nombre asociado. Ese nombre se llama etiqueta.

Una etiqueta es un símbolo que codifica la dirección específica de los datos en memoria. Las direcciones en x86-64 son valores de 64 bits.

En NASM, intentar acceder a los datos directamente con su etiqueta no da como resultado la memoria asignada, sino su dirección:

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 acceder al contenido de una dirección de memoria, es necesario desreferenciarla. Esto se llama indirección.

En NASM, esto se hace con []:

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

Sin embargo, hay algunas situaciones en las que puede haber ambigüedad respecto al tamaño de la memoria desreferenciada. En esos casos, se debe usar un prefijo que especifique ese tamaño.

Estos son los prefijos más importantes y sus tamaños en un programa típico de x86-64:

prefijo tamaño
byte 1 byte
word 2 bytes
dword 4 bytes
qword 8 bytes

La misma carga se puede escribir con el tamaño indicado de forma explícita:

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

Es una buena práctica usar siempre un prefijo al desreferenciar memoria.

Escribir en memoria

Escribir en memoria se hace de la misma manera, desreferenciando una dirección:

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

Ten en cuenta que puedes usar operandos de memoria en la mayoría de las instrucciones sin cargar primero el contenido en un registro. Sin embargo, por lo general no es posible usarlos tanto en el operando de origen como en el de destino, solo en uno de los dos:

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]
La instrucción LEA

Aunque se puede usar mov para almacenar la dirección de una variable en un registro, existe una instrucción con este propósito específico: lea.

Esta instrucción usa un operando en forma de memoria, pero no lee la memoria. En su lugar, calcula la expresión de dirección efectiva y escribe el resultado en el operando de destino:

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

Es más idiomático usar lea para calcular y almacenar direcciones de memoria en registros.

Direccionamiento relativo

Al acceder a ubicaciones de memoria, el comportamiento predeterminado en NASM es generar direcciones absolutas, es decir, direcciones de memoria fijas.

Por razones de seguridad, los ejecutables a menudo se compilan como PIE (ejecutable independiente de la posición), donde las regiones de memoria se colocan en ubicaciones aleatorizadas. En un PIE, la dirección final de una variable no se conoce en el momento del enlace. Por eso, el código calcula las direcciones como un desplazamiento a partir del valor de un registro especial llamado rip, que apunta a la siguiente instrucción por ejecutar.

A esto se le suele llamar direccionamiento relativo a RIP.

En NASM puedes solicitar acceso relativo a RIP con el operador rel:

mov rax, qword [rel variable]

El direccionamiento relativo también se puede establecer como predeterminado para un archivo fuente con default rel al principio.

Todos los ejercicios de este track se compilan y enlazan como PIE, así que se debe usar rel para generar direcciones relativas.

Visibilidad

Las etiquetas (funciones y datos) definidas en cualquier sección (p. ej., .text, .data, .rodata) son visibles dentro del mismo archivo fuente. Si se declaran global, también son visibles para otros archivos fuente.

A la inversa, las etiquetas definidas en otros archivos fuente son visibles para el archivo fuente actual si se declaran extern. En este caso, no hay ninguna indicación del tamaño de los datos en el ensamblado; esto se debe conocer de antemano.

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

Instrucciones

Tu amigo José es profesor en una escuela de la zona. Tuvo una idea para hacer unos experimentos divertidos que muestran cómo se pueden combinar los colores para producir otros distintos.

Te pidió ayuda con esos experimentos.

Note

En este ejercicio, un color se representa con un número de 32 bits (4 bytes) que codifica su valor RGB.

Un valor RGB consta de 3 canales, Red, Green y Blue, y cada uno ocupa 8 bits (1 byte). El cuarto byte normalmente se reserva para el canal Alpha, pero en este ejercicio su valor estará vacío (0).

1. Obtén el valor RGB de un color

Los valores de cada color ya están guardados en una tabla, definida en otro archivo fuente. Cada color se identifica por una dirección única en esa tabla.

Define una función get_color_value que devuelva el valor de 32 bits de un color. Esta función recibe como parámetro una dirección válida para ese color en la tabla de colores.

get_color_value(black)
// => 0

Pista: 32 bits equivalen a 4 bytes.

2. Agrega el color base

Para mezclar distintos colores, José primero fija un color base y solo cambia el color secundario que se combina con él.

Define una función add_base_color que guarde el valor de 32 bits de un color en la variable base_color, para poder usarlo después. Esta función no devuelve nada y recibe como parámetro la dirección del color en la tabla de colores.

La variable base_color la defines tú y debe ser accesible desde otros archivos fuente.

No habrá más de 1 color base a la vez. Si se agrega un nuevo color base, se descarta el anterior.

De forma predeterminada, al inicio del programa, base_color debe inicializarse con el valor de 32 bits de white, que es 0xFFFFFF00.

Pista: NASM acepta números definidos en hexadecimal usando 0x al principio, como en 0xFFFFFF00.

3. Define constantes para los colores primarios

José espera hacer muchas combinaciones con colores primarios, así que quiere tenerlos separados para acceder a ellos rápido. Como usa RGB para representar colores, los colores primarios son:

  • RED, con el valor 0xFF000000.
  • GREEN, con el valor 0x00FF0000.
  • BLUE, con el valor 0x0000FF00.

Define una constante para cada uno de esos colores. Esas constantes deben ser accesibles desde otros archivos fuente.

4. Combina colores

Los colores deben combinarse según una combining_function definida en otro archivo fuente. Esta función recibe como parámetros los valores de 32 bits de base_color y de un color secundario que se mezclará con él. Devuelve el valor de 32 bits del color combinado.

Define una función make_color_combination que combine dos colores y guarde el resultado en memoria. Esta función no devuelve nada y recibe como parámetros, en este orden:

  • La dirección donde se debe guardar el valor de 32 bits del color combinado.
  • La dirección de un color secundario en la tabla de colores, para combinarlo con el color primario.
Caution

Fíjate que combining_function puede modificar los valores de los registros que estás usando. Asegúrate de guardar en memoria cualquier variable que necesites antes de llamar a la función.

Editar en GitHub El enlace se abre en una ventana o una pestaña nuevas
x86-64 Assembly Exercism

¿Todo listo para empezar Paleta de colores?

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