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:

  1. Constants for all common Linux syscall numbers (at minimum: read, write, open, close, exit, mmap, munmap, brk)
  2. Constants for common flags (O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_TRUNC; PROT_READ, PROT_WRITE, PROT_EXEC; MAP_PRIVATE, MAP_ANONYMOUS)
  3. 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.