Chapter 6 Exercises: The NASM Assembler
Section A: Data Declarations and Memory Layout
Exercise 6.1 — Data Layout Prediction
Given the following NASM data section, predict the exact byte values at each offset. Assume the section starts at address 0:
section .data
start:
db 0x01 ; offset 0
dw 0x0203 ; offset 1
dd 0x04050607 ; offset 3
dq 0x08090A0B0C0D0E0F ; offset 7
db 'A', 'B', 'C' ; offset 15
db 10, 0 ; offset 18
Fill in the byte at each address:
| Offset | Hex Value | Notes |
|---|---|---|
| 0 | ||
| 1 | ||
| 2 | ||
| 3 | ||
| 4 | ||
| 5 | ||
| 6 | ||
| 7 | ||
| 8 | ||
| 9 | ||
| 10-14 | (5 bytes) | |
| 15 | ||
| 16 | ||
| 17 | ||
| 18 | ||
| 19 |
Exercise 6.2 — EQU and $ Arithmetic
For the following data section, compute the value of each EQU:
section .data
start_of_section:
header db 0x7F, 'E', 'L', 'F' ; 4 bytes
class db 2 ; 1 byte
padding times 11 db 0 ; 11 bytes
HEADER_SIZE equ $ - start_of_section ; (a)
align 8
count dq 0 ; 8 bytes
flag db 1 ; 1 byte
AFTER_ALIGN equ $ - start_of_section ; (b) -- what is AFTER_ALIGN after align 8?
message db "Test message", 0
MESSAGE_LEN equ $ - message ; (c)
TOTAL_SIZE equ $ - start_of_section ; (d)
Calculate each of (a), (b), (c), (d). For (b), remember that align 8 pads to the next 8-byte boundary.
Exercise 6.3 — BSS vs Data
For each of the following scenarios, decide whether to use .data or .bss:
a) A 64KB input buffer that will be filled by a read() call b) A counter initialized to 0 that will be incremented at runtime c) A lookup table of prime numbers (precomputed, never modified) d) A string constant "Error: permission denied\n" e) An array of 100 integers, all initially 0 f) A configuration flag initialized to 1
For each choice, explain the tradeoff (ELF file size, memory usage, initialization guarantees).
Section B: Labels and Global Symbols
Exercise 6.4 — Local vs Global Labels
The following code has a bug related to label scoping. Find and fix it:
global func_a
global func_b
func_a:
xor eax, eax
test rdi, rdi
jz done ; PROBLEM: this is a global label
mov eax, 1
done: ; first 'done' -- globally visible
ret
func_b:
xor eax, eax
test rsi, rsi
jz done ; PROBLEM: ambiguous -- which 'done'?
mov eax, 2
done: ; second 'done' -- DUPLICATE SYMBOL ERROR!
ret
Rewrite both functions using local labels correctly.
Exercise 6.5 — Extern and Global
Write three assembly files that together form a complete program:
File 1: greeting.asm
- Define a global string greeting_msg and its length greeting_len
- Declare a global function print_greeting that prints the greeting using sys_write
- No other global symbols
File 2: math.asm
- Define a global function multiply_by_two(rdi) that returns 2*rdi in rax
- Define a global function add_numbers(rdi, rsi) that returns rdi+rsi in rax
File 3: main.asm
- Declare extern for all symbols from greeting.asm and math.asm
- Implement _start which:
1. Calls print_greeting
2. Calls multiply_by_two(21) and stores the result
3. Calls add_numbers(result, 0) and exits with the result as exit code
Build: nasm -f elf64 greeting.asm -o greeting.o && nasm -f elf64 math.asm -o math.o && nasm -f elf64 main.asm -o main.o && ld greeting.o math.o main.o -o program
Section C: The Preprocessor
Exercise 6.6 — %define Macros
Write parametric %define macros for the following C expressions:
// (a) Array element access: arr[i] where elements are 8 bytes each
// Expected: ELEM(arr, i) expands to [arr + i*8]
// (b) Structure field access: s->offset where offset is a byte offset
// Expected: FIELD(s, offset) expands to [s + offset]
// (c) Aligned size: round n up to the next multiple of alignment
// Expected: ALIGN_UP(n, align) where align is a power of 2
// Hint: (n + align - 1) & ~(align - 1)
// (d) Minimum of two values (NOTE: this is tricky with %define;
// it requires a macro, not %define, because it needs local labels)
For (d), explain why a %define macro for MIN(a, b) doesn't work safely in assembly, and what the correct approach is.
Exercise 6.7 — Multi-Line Macros
Write the following multi-line macros:
a) %macro zero_register 1 — sets %1 to zero using XOR (e.g., xor eax, eax for zero_register rax)
Hint: for a 64-bit register like rax, the instruction should be xor eax, eax (using the 32-bit form for efficiency)
b) %macro call_syscall 2-7 0,0,0,0,0 — a syscall wrapper:
- %1 = syscall number
- %2-%7 = up to 6 arguments (default 0)
- Sets up RAX, RDI, RSI, RDX, R10, R8, R9 appropriately
- Executes syscall
c) %macro for_loop 3 — a counted loop:
- %1 = loop counter register (e.g., rcx)
- %2 = count (can be immediate or register)
- %3 = macro for the loop body (this is very advanced -- just make a template)
Write a simpler version: %macro loop_n_times 2 where %1 = counter register, %2 = count. The loop body is the code between loop_n_times rcx, 10 and %endloop (you'll need a matching endmacro pattern).
Exercise 6.8 — Conditional Assembly
Write a NASM source file that produces different output based on command-line defines:
; platform.asm
; Assemble with: nasm -DLINUX ... or nasm -DMACOS ...
%ifdef LINUX
%define SYSCALL_WRITE 1
%define SYSCALL_EXIT 60
%elifdef MACOS
%define SYSCALL_WRITE 0x2000004 ; macOS uses BSD syscall numbers with 0x2000000 offset
%define SYSCALL_EXIT 0x2000001
%else
%error "Must define LINUX or MACOS"
%endif
; ... rest of program uses SYSCALL_WRITE and SYSCALL_EXIT
a) What NASM error do you get if you try to assemble without defining LINUX or MACOS?
b) Extend this to support a VERBOSE define that enables extra print statements (define verbose_print as a working macro if VERBOSE is defined, and a no-op if not)
Exercise 6.9 — Include Files
Create a complete include file syscall_linux.inc that provides:
- Constants for all common Linux syscall numbers (at minimum: read, write, open, close, exit, mmap, munmap, brk)
- Constants for common flags (O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_TRUNC; PROT_READ, PROT_WRITE, PROT_EXEC; MAP_PRIVATE, MAP_ANONYMOUS)
- Wrapper macros:
sys_read,sys_write,sys_exit(minimum 3)
Then create a short program that uses %include "syscall_linux.inc" and uses only the macros and constants from that file (no magic numbers in the program body).
Section D: Output Formats
Exercise 6.10 — Flat Binary for a Bootloader
A BIOS-compatible bootloader must:
- Be exactly 512 bytes
- End with the bytes 0x55, 0xAA at positions 510-511
- Start with 16-bit code (NASM's BITS 16 directive)
- Be loadable at address 0x7C00 (use ORG 0x7C00)
Write the skeleton of a minimal BIOS bootloader:
; boot.asm -- minimal BIOS bootloader skeleton
BITS 16 ; generate 16-bit code
ORG 0x7C00 ; assume loaded at 0x7C00
; Entry point: the BIOS jumps here
start:
; Set up segments (they may be garbage at entry)
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00 ; stack below the bootloader
; Print a character using BIOS INT 10h (AH=0Eh, AL=char)
mov ah, 0x0E ; BIOS teletype output
mov al, 'B' ; character to print
int 0x10 ; BIOS video interrupt
; Hang forever
.hang:
hlt
jmp .hang
; Padding and boot signature
times 510 - ($ - $$) db 0 ; pad to byte 510
dw 0xAA55 ; boot signature (note byte order!)
a) Assemble: nasm -f bin boot.asm -o boot.bin
b) Check the size: wc -c boot.bin — should be exactly 512 bytes
c) Examine the last 2 bytes: xxd boot.bin | tail -1 — should show 55 aa
d) Test in QEMU: qemu-system-x86_64 -fda boot.bin — should show 'B' on screen
Section E: Macro Library Development
Exercise 6.11 — A String Library via Macros
Create a macro file string_macros.inc that provides:
; STRLEN dest_reg, src_reg
; Computes length of null-terminated string at src_reg
; Stores result in dest_reg
; Clobbers: rcx (the scan register)
%macro STRLEN 2
; your implementation
%endmacro
; PRINT_CSTR src_reg
; Prints null-terminated string at src_reg to stdout
%macro PRINT_CSTR 1
; your implementation
%endmacro
; PRINT_LIT "literal string"
; Prints a string literal (creates the data inline)
%macro PRINT_LIT 1
; This is the tricky one: must define data AND print it
; Use %%local_data to create a local data label
section .data
%%msg db %1 ; the string data
%%msg_len equ $ - %%msg
section .text
mov rax, 1
mov rdi, 1
mov rsi, %%msg
mov rdx, %%msg_len
syscall
%endmacro
Test with a program that uses these macros.
Exercise 6.12 — NASM vs GAS Syntax
The following program is written in GAS (GNU Assembler) AT&T syntax. Rewrite it in NASM Intel syntax:
# hello_gas.s -- GAS AT&T syntax
.section .data
msg:
.ascii "Hello from GAS\n"
.equ msglen, . - msg
.section .text
.globl _start
_start:
movq $1, %rax # sys_write
movq $1, %rdi # stdout
movq $msg, %rsi # buffer
movq $msglen, %rdx # length
syscall
movq $60, %rax # sys_exit
xorq %rdi, %rdi # exit code 0
syscall
In your rewrite: a) Convert all AT&T syntax to Intel syntax b) Convert GAS directives to NASM directives c) Verify your version produces the same output when assembled and run
Exercise 6.13 — The TIMES Padding Idiom
Write NASM code that: a) Defines a data structure that must start at a 16-byte boundary b) Has a field that must be at a 4-byte offset from the start c) Has a field that must be at a 8-byte offset from the start d) Pads the entire structure to a multiple of 16 bytes
section .data
align 16
my_struct:
.field_a db 0x01 ; 1 byte at offset 0
; (a) pad to 4-byte boundary before field_b
.field_b dd 0x00000002 ; 4 bytes at offset 4
; (b) pad to 8-byte boundary before field_c
.field_c dq 0x0000000000000003 ; 8 bytes at offset 8
; (c) pad the entire structure to 16 bytes
STRUCT_SIZE equ $ - my_struct ; should be 16
Verify: after adding the correct times ... db 0 directives, STRUCT_SIZE should equal 16.
Exercise 6.14 — Macro-Generated Code
Write a macro FILL_ARRAY that generates inline code to fill an array with a constant value:
; FILL_ARRAY dest, value, count
; Generates: mov QWORD [dest], value (for each element)
; If count > 4, use REP STOSQ instead
; Args: %1 = destination address (label), %2 = fill value, %3 = element count (qwords)
%macro FILL_ARRAY 3
%if %3 <= 4
; Unrolled version for small counts
%assign _i 0
%rep %3
mov QWORD [%1 + _i * 8], %2
%assign _i _i + 1
%endrep
%else
; REP STOSQ version for larger arrays
lea rdi, [%1]
mov rax, %2
mov rcx, %3
rep stosq
%endif
%endmacro
Test this macro with:
section .bss
array1 resq 3 ; 3 elements
array2 resq 10 ; 10 elements
section .text
_start:
FILL_ARRAY array1, 0xDEADBEEF, 3 ; should use unrolled version
FILL_ARRAY array2, 0, 10 ; should use REP STOSQ
Verify by examining the disassembly: what actual instructions does each FILL_ARRAY invocation generate?
Exercise 6.15 — Macro for Structured Data
Design a macro system for defining C-like structs in NASM:
; Define structure:
STRUCT task_struct
FIELD pid, 4 ; 4-byte field
FIELD state, 4
FIELD stack, 8
FIELD flags, 8
END_STRUCT
; Expected: creates constants like:
; task_struct.pid equ 0 (offset of pid)
; task_struct.state equ 4 (offset of state)
; task_struct.stack equ 8 (offset of stack)
; task_struct.flags equ 16 (offset of flags)
; task_struct.SIZE equ 24 (total size)
Implement the STRUCT, FIELD, and END_STRUCT macros. Hint: you'll need %assign to track the current offset, and the namespace separator (.) in EQU names.
This is an advanced macro problem — the solution involves managing state across macro calls using %assign.