Memory is usually mapped for a program by the Operating System (OS) in a general layout:
| address | memory region |
|---|---|
| high | stack |
| ... | |
| heap | |
| read-and-write segment | |
| code/read-only segment | |
| low | reserved |
Memory in segments is organized in sections, with different permissions.
The functions we have defined until now were all in section .text. This section holds read-only executable data. Other sections are used to declare data variables, that may be read-only or read-and-write, but are not executable.
The initialized data is declared in the section .data.
In NASM (The Netwide Assembler - the assembler used by this track), an initialized variable has a name, a directive that indicates the data size, and a list of values separated by comma.
Each of these is separated with a space from the other and the label might optionally be followed by a :.
The main directives and their related data sizes are:
| directive | size |
|---|---|
| db | 1 byte |
| dw | 2 bytes |
| dd | 4 bytes |
| dq | 8 bytes |
For instance, this declares a byte variable named space with the value 10:
section .data
space db 10
Variables declared in section .data are mutable, i.e., they are read-and-write.
They also have static storage duration, which means they exist for the entire program runtime.
The section .rodata is similar to section .data.
Both sections contain initialized data, which is declared in the same way and have the same storage duration.
The main difference between them is that data in section .rodata is immutable, i.e., read-only.
Constants defined with equ are different than those defined in section .rodata.
A constant defined with equ does not occupy space in memory and is directly substituted for its value by the assembler.
It is in fact a placeholder for that value.
On the other hand, constants defined in section .rodata are actually stored in memory, having an address.
Declared data must have a name associated with it. This name is called a label.
A label is a symbol that encodes the specific address of data in memory. Addresses in x86-64 are 64-bit values.
In NASM, trying to access data directly with its label does not yield the memory allocated, but its address:
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
...
In order to access the contents of a memory address, it is necessary to dereference it. This is called indirection.
In NASM, this is done with []:
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)
...
However, there are some situations when there might be an ambiguity regarding the size of the memory dereferenced. In those cases, a prefix specifying this size must be used.
These are the most important prefixes and their sizes in a typical x86-64 program:
| prefix | size |
|---|---|
| byte | 1 byte |
| word | 2 bytes |
| dword | 4 bytes |
| qword | 8 bytes |
The same load can be written with the size stated explicitly:
mov rax, qword [example] ; same dereference, size stated explicitly
It is good practice to always use a prefix when dereferencing memory.
Writing to memory is done in the same way, by dereferencing an address:
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
Note that you can use memory operands in most instructions without first loading the contents in a register. However, it is not usually possible to use them in both source and destination operand, only one of the two:
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]
Although a mov can be used to store the address of a variable in a register, there is an instruction with this specific purpose: lea.
This instruction uses a memory-form operand, but it does not read memory. Instead, it computes the effective address expression and writes the result in the destination operand:
lea rax, [example] ; this stores the address of 'example' in rax
It is more idiomatic to use lea to compute and store memory addresses in registers.
When accessing memory locations, the default behavior in NASM is to generate absolute addresses, i.e., fixed memory addresses.
For security reasons, executables are often built as PIE (Position Independent Executable), where memory regions are placed at randomized locations.
In a PIE, the final address of a variable is not known at link time.
So, code instead computes addresses as an offset from the value in a special register called rip, which points to the next instruction to be executed.
This is usually called RIP-relative addressing.
In NASM you can request RIP-relative access with the rel operator:
mov rax, qword [rel variable]
Relative addressing can also be made the default for a source file with default rel at the top.
All exercises in this track are compiled and linked as PIE, so rel should be used to generate relative addresses.
Labels (functions and data) defined in any section (e.g., .text, .data, .rodata) are visible within the same source file.
If declared global, they are visible to other source files as well.
Conversely, labels defined in other source files are visible to the current source file if declared extern.
In this case, there is no indication of data size in assembly, this must be known in advance.
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
...
Your friend José is a teacher in a local school. He had an idea for some fun experiments to show how colors can be combined to produce different ones.
He asked for your help with those experiments.
A color is represented in this exercise by a 32-bit (4-byte) number, encoding its RGB value.
A RGB value consists of 3 channels, Red, Green and Blue, each one occupying 8 bits (1 byte).
The fourth byte is usually reserved for the Alpha channel, but in this exercise its value will be empty (0).
The values for each color are already stored in a table, defined in another source file. A color is identified by an unique address in this table.
Define a function get_color_value that returns the 32-bit value for a color.
This function takes as parameter a valid address for this color in the color table.
get_color_value(black)
// => 0
Hint - 32 bits are equivalent to 4 bytes.
In order to mix different colors, José will first fix a base one and change only the secondary color being combined with it.
Define a function add_base_color that saves the 32-bit value for a color in the variable base_color, so it can be used later.
This function has no return value and takes as parameter the address of the color in the color table.
The variable base_color is defined by you and must be accessible from other source files.
There will be no more than 1 base color at the same time. If a new base color is added, the old one is discarded.
By default, at the start of the program, base_color should be initialized with the 32-bit value for white, which is 0xFFFFFF00.
Hint - NASM accepts numbers defined in hexadecimal using 0x at the start, such as in 0xFFFFFF00.
José expects to make many combinations using primary colors, so he wants to have them separated for fast access.
Since he is using RGB to represent colors, the primary colors are:
RED, with the value 0xFF000000.GREEN, with the value 0x00FF0000.BLUE, with the value 0x0000FF00.Define one constant for each of those colors. Those constants must be accessible from other source files.
The colors should be combined according to a combining_function defined in another source file.
This function takes as parameters the 32-bit values for base_color and for a secondary color to be mixed with it.
It returns the 32-bit value for the combined color.
Define a function make_color_combination that combines two colors and saves the result in memory.
This function has no return value and takes as parameters, in this order:
Notice that combining_function may modify the values in registers you are using.
Make sure to save any variable you need in memory before calling the function.
Sign up to Exercism to learn and master x86-64 Assembly with 22 concepts130 exercises, and real human mentoring, all for free.