Bachelors Program in Electrical Engineering
Microprocessors & Microcontrollers
•
Bachelor
Assembly-Language Programming
Introduction
Assembly language exposes the processor’s registers, instructions, memory accesses, status flags, and calling conventions directly. This visibility makes it valuable for startup code, interrupt handling, device drivers, deterministic control, optimized signal processing, and debugging compiler-generated programs. It also imposes responsibility: the programmer must preserve registers correctly, maintain stack alignment, prevent buffer violations, and respect the target instruction set.
This chapter uses Arm Cortex-M–style examples in GNU assembler unified syntax. Most examples are compatible with ARMv6-M processors such as Cortex-M0 and Cortex-M0+, but an assembler must still be given the correct architecture and processor options. ARMv6-M supports both CBZ/CBNZ and IT; however, their encoding and use are restricted. For example, CBZ and CBNZ operate on low registers and branch only forward over a limited range, while conditional instructions in an IT block must satisfy architectural placement rules. Instructions such as MOVW, MOVT, UDIV, SDIV, and many Thumb-2 operations are not available on ARMv6-M. Always verify the architecture reference and assembler diagnostics rather than assuming that code for a larger Cortex-M processor will execute on Cortex-M0/M0+.
Learning Objectives
After completing this chapter, the reader should be able to:
translate structured algorithms into assembly-language control flow;
distinguish instructions, labels, directives, constants, and macros;
implement loops, arrays, lookup tables, and arithmetic operations;
construct subroutines that comply with an application binary interface;
use full-descending stacks for saved registers, parameters, and local storage;
pass parameters through registers and the stack;
interface assembly routines with C;
analyze overflow, alignment, reentrancy, and instruction-set limitations;
debug register, memory, stack, and control-flow faults systematically.
1. From Source Text to Executable Code
An assembly source file contains more than processor instructions. An assembler translates mnemonics into machine code, evaluates expressions, records symbols, and emits object-file sections and relocation information. A linker then combines object files, resolves external symbols, places sections according to a linker script, and creates an executable image.
.global exports a symbol to the linker, .type records its function type, and .size records its extent. .align requirements are assembler-dependent; source portability therefore requires checking the toolchain manual. Data can be placed with directives such as .byte, .hword, .word, .ascii, and .asciz. Reserve uninitialized storage using an appropriate zero-fill directive or a symbol in a .bss section.
Location and relocation
The assembler may not know a symbol’s final address. It emits a relocation so that the linker can complete the encoding. Pseudo-instructions such as ldr r0, =symbol may generate a PC-relative load from a literal pool rather than a single instruction that constructs the complete address. Literal pools must be within the range supported by the selected encoding.
2. Registers, Flags, and Data Representation
In the common 32-bit Cortex-M programming model, r0–r12 are general-purpose registers, sp is the stack pointer, lr is the link register, and pc is the program counter. Under the Arm Procedure Call Standard, r0–r3 normally carry the first four arguments and return values. Registers r4–r11 are callee-saved, although exact availability and instruction access vary by architecture. r12 is a caller-saved scratch register.
Arithmetic instructions may update the APSR condition flags:
N: the result’s most significant bit is 1;
Z: the result is zero;
C: unsigned carry from addition or no borrow from subtraction;
V: signed two’s-complement overflow.
Unsigned and signed comparisons interpret the same bits differently. For unsigned ordering, branches use the carry and zero flags; for signed ordering, they use relationships involving N, V, and Z. Confusing BHI with BGT, for example, can make a loop fail only for values with bit 31 set.
For two unsigned w-bit operands a and b, addition can require w+1 bits:
a + b ≤ 2(2w - 1) = 2w+1 - 2
For an unsigned sum of N samples, each in the range 0 ≤ xi ≤ 2b-1, the maximum is:
Smax = N(2b - 1)
The required unsigned width is:
W = ⌈log2(Smax+1)⌉
These equations assume exact nonnegative integer samples and no intermediate saturation.
3. Structured Control Flow
Assembly has labels and branches rather than language-level if, while, and for statements. Reliable code nevertheless retains the same structures: sequence, selection, and iteration.
Selection
The pseudocode if (x < limit) x++; for unsigned values can be written:
cmp r0, r1 @ x compared with limit
bhs done @ unsigned x >= limit
adds r0, r0, #1
done:
On ARMv6-M, CBZ and CBNZ are available for testing a low register against zero. They use a short forward branch, so a backward loop branch still requires another branch form. The IT instruction is also part of ARMv6-M, but conditionalized instructions must obey IT-block restrictions. Explicit branches are often clearer and easier to maintain.
Counted loops
A safe loop establishes its invariant, bound, and termination condition explicitly:
The loop invariant is that r2 equals the sum of bytes already visited, r0 points to the next byte, and r1 is the number remaining. The routine is mathematically safe from 32-bit unsigned overflow only when 255N ≤ 232-1, giving N ≤ 16,843,009. A real interface should specify a smaller accepted bound or use a wider accumulation method.
Sentinel loops
Sentinel loops terminate when a distinguished value is encountered. They require a guarantee that the sentinel exists within accessible memory; otherwise, the routine can read beyond an array. Length-bounded interfaces are usually safer for embedded software.
4. Addressing Arrays and Lookup Tables
An array address follows:
Ai = A0 + i × s
Here A0 is the base byte address, i is the zero-based index, and s is the element size in bytes. For a word table, s=4.
@ r0 = base, r1 = word index; returns table[index]
lookup_word:
lsls r1, r1, #2 @ byte offset = index × 4
ldr r0, [r0, r1]
bx lr
This routine assumes the caller has verified the index and that the resulting address satisfies the processor and system’s access requirements. A checked interface can reject invalid indices:
@ r0 = base, r1 = index, r2 = length
@ returns value in r0 and status in r1: 0 success, 1 error
lookup_checked:
cmp r1, r2
bhs bad_index
ldrb r0, [r0, r1]
movs r1, #0
bx lr
bad_index:
movs r0, #0
movs r1, #1
bx lr
Lookup tables trade memory for execution time and deterministic behavior. They are useful for waveform generation, calibration, nonlinear sensor conversion, CRC algorithms, and state machines. Their risks include unchecked indexing, stale calibration data, endianness assumptions, and interpolation error.
5. Subroutines and the Application Binary Interface
A subroutine call must satisfy both instruction semantics and the platform’s application binary interface (ABI). On Cortex-M, BL target writes a return address into lr; BX lr returns from a leaf routine. A non-leaf function that calls another function must preserve its own return address, normally by saving lr on the stack.
Under the commonly used Arm AAPCS:
Resource
Typical role
r0–r3
Arguments, result, caller-saved temporaries
r4–r11
Callee-saved registers
r12
Caller-saved scratch register
lr
Link register
sp
Stack pointer
If a function modifies a callee-saved register, it must restore the original value before returning. Caller-saved registers can be destroyed by a called function, so a caller must preserve any live values it still needs.
@ uint32_t weighted_sum(uint32_t a, uint32_t b)
@ returns 3a + b
weighted_sum:
push {r4, lr}
mov r4, r0
adds r0, r0, r4 @ 2a
adds r0, r0, r4 @ 3a
adds r0, r0, r1 @ 3a + b
pop {r4, pc}
The lr save is unnecessary for this leaf implementation, but the example demonstrates a valid preserved-register frame. Production code would normally omit unnecessary stack traffic.
6. Full-Descending Stack Operation
Cortex-M software normally uses a full-descending stack. “Descending” means allocation proceeds toward lower addresses. “Full” means sp identifies the lowest occupied address: after a push, it points to the newly occupied stack word. A pop reads from the address identified by sp and then raises sp.
Full-descending stack with high and low addresses
Figure: Full-descending stack with high and low addresses
Figure: In a full-descending stack, SP identifies the lowest occupied address and growth is toward lower addresses.
The stack must remain within its allocated region. Excess recursion, large local buffers, nested interrupts, or corrupted return addresses can cause stack overflow. Embedded systems often use linker-defined limits, guard regions, fill-pattern high-water measurements, or an MPU where available.
7. Stack-Passed Parameters and Local Variables
Passing more than four arguments
Under AAPCS, the first four 32-bit arguments normally use r0–r3; additional arguments are placed on the stack. At a public interface, the stack must satisfy the ABI’s alignment rule, commonly 8-byte alignment. The following caller allocates eight bytes so that one 4-byte stack argument does not break alignment:
At entry to sum5, the fifth argument is at [sp,#0]. Once the callee pushes two registers, sp decreases by eight bytes, so the argument’s address becomes [sp,#8]:
@ uint32_t sum5(uint32_t a, uint32_t b, uint32_t c,
@ uint32_t d, uint32_t e)
sum5:
push {r4, lr} @ 8-byte frame
ldr r4, [sp, #8] @ e at original entry SP
adds r0, r0, r1
adds r0, r0, r2
adds r0, r0, r3
adds r0, r0, r4
pop {r4, pc}
The offset must be derived from the actual frame size, not memorized. If more registers or local bytes are added, the offset changes.
Explicit local-stack allocation
A function can reserve automatic local storage by subtracting from sp. Every exit path must release exactly the same amount before restoring saved registers.
@ Illustrative non-leaf function with 16 bytes of local storage
process_block:
push {r4, r5, r6, lr} @ 16 bytes; preserves alignment
sub sp, sp, #16 @ local byte buffer at [sp,#0]...[sp,#15]
mov r4, sp @ r4 = address of local buffer
movs r0, #0
str r0, [sp, #0] @ local word 0
str r0, [sp, #4] @ local word 1
mov r0, r4 @ first argument: buffer address
movs r1, #16 @ second argument: length
bl fill_buffer
ldr r0, [sp, #0] @ use a local result
add sp, sp, #16 @ deallocate locals
pop {r4, r5, r6, pc}
The total frame is 32 bytes: 16 bytes of saved registers plus 16 bytes of locals. Stack-resident buffers must not escape after return because subsequent calls may overwrite them. Large arrays are often better placed statically or supplied by the caller, particularly when stack capacity is small.
A frame pointer can provide stable offsets when sp changes within a function, but it consumes a register. Many embedded functions instead use a fixed frame size and constant sp-relative offsets.
8. Macros and Reusable Source Patterns
A macro is expanded by the assembler; a subroutine executes at run time. Macros avoid call overhead and permit parameterized instruction sequences, but every expansion consumes code space.
Local labels or assembler-provided unique suffixes should be used so that multiple expansions do not define the same global label. Macro parameters must be documented with register-clobber behavior. Complex algorithmic macros are difficult to debug because source lines may expand into many instructions.
Use a macro when
Use a subroutine when
The sequence is very short
Code size matters
Compile-time variation is needed
Behavior is reused without variation
Call overhead is unacceptable
A stable ABI boundary is desirable
Inline context manipulation is required
Debugging and maintenance dominate
9. Mixed-Language Interfaces
Assembly and C can interoperate reliably only when both follow the same ABI. The assembly author must know symbol naming, parameter locations, return-value rules, register preservation, stack alignment, data layout, and instruction-state requirements.
This implementation returns the low 32 bits of the mathematical dot product; it does not detect overflow. Since each product can be as large as 65,5352 = 4,294,836,225, even two terms can overflow 32 bits. The interface must therefore impose an input bound, use a wider accumulator, or define modular arithmetic explicitly.
For C structures, do not guess member offsets: use shared generated constants, assembler-compatible headers, or verified static assertions. C objects modified asynchronously by hardware or an interrupt may require volatile access at the C level, but volatile alone does not provide atomicity or memory ordering.
10. Debugging Assembly Programs
Assembly debugging should proceed from observable machine state rather than from source appearance.
Confirm the image: inspect the map file and disassembly to verify symbol addresses and encodings.
Check entry conditions: record argument registers, sp, lr, and relevant memory.
Single-step critical instructions: observe changed registers and APSR flags.
Use watchpoints: detect unexpected writes to arrays, stack frames, or peripherals.
Validate stack symmetry: every allocation, push, pop, and deallocation must balance along every path.
Inspect control flow: confirm branch conditions use signed or unsigned semantics as intended.
Test boundaries: include zero length, one element, maximum index, maximum arithmetic values, and invalid pointers where the interface defines behavior.
Common faults include loading an address instead of the data at that address, using a byte load for a word object, failing to preserve r4–r11, returning through a corrupted lr, violating stack alignment, and reading memory-mapped registers with ordinary assumptions about repeatability.
A useful stack invariant is:
SPexit = SPentry
This applies to a normally returning function after its own frame has been removed. It does not mean that sp remains unchanged during execution.
11. Applied Case: ADC Block Averaging and Calibration
Background
Consider an illustrative battery-monitoring device with a 12-bit ADC. A block contains 1,024 unsigned samples. Firmware must compute the integer mean and map it through a 256-entry calibration table.
Problem analysis
Each sample lies in 0 ≤ xi ≤ 4095. The maximum block sum is:
Smax = 1024 × 4095 = 4,193,280
Since 221=2,097,152 and 222=4,194,304, the maximum sum requires 22 unsigned bits. It therefore fits safely in a 32-bit register. Because 1,024 is 210, truncating unsigned division by 1,024 is a right shift by ten bits:
mean = ⌊S / 1024⌋ = S >> 10
This special shift replacement is valid because the divisor is a positive power of two and the operands are unsigned. Signed rounding or division by a non-power-of-two requires different treatment.
To select one of 256 calibration entries from the 12-bit mean, the high eight ADC bits can form an index:
index = mean >> 4
Because 0 ≤ mean ≤ 4095, the index lies from 0 to 255. This direct mapping is valid only if the table was constructed for those uniform 16-code bins. Higher accuracy may require interpolation using the lower four bits.
Outcome
The accumulator cannot overflow for the specified block, the loop executes a fixed number of iterations, and the index is proven to remain within the table. Timing still depends on memory-system behavior and interrupts. If the ADC buffer is concurrently filled by DMA, ownership or double buffering is required so that data are not modified while being summed.
Learning points
derive accumulator width from the input range and count;
prove table bounds before loading data;
document truncation rather than silently assuming rounding;
treat concurrent data acquisition as a synchronization problem;
test all-zero, all-maximum, alternating, and ramp input blocks.
12. Reliability, Security, and Professional Practice
Assembly can bypass safeguards normally introduced by compilers. In safety- or security-related systems, review should cover memory bounds, worst-case stack usage, execution timing, integer overflow, fault handling, and compliance with organizational coding standards. Peripheral writes can have physical consequences; tests should begin with simulation, emulation, or controlled hardware where hazardous actuation is inhibited.
AI-assisted code generation and optimization tools may propose plausible but architecture-incompatible instructions or violate an ABI invisibly. Generated assembly must be checked against the selected architecture, disassembled, boundary-tested, and reviewed like manually written code. Tools do not remove engineering accountability.
Contemporary compilers often outperform hand assembly for general code. Assembly is best reserved for demonstrated needs: startup sequences, context switching, specialized instructions, exact timing, ABI stubs, or measured optimization bottlenecks. Maintain a reference implementation in C where practical and compare outputs automatically.
Chapter Summary
Assembly-language programming connects algorithms directly to processor state. Assemblers translate instructions and directives into relocatable objects, while linkers resolve symbols and place code and data. Structured branches and loops remain essential even when language-level constructs disappear. Correct subroutines obey the ABI, preserve callee-saved registers, maintain stack alignment, and balance every stack operation. A full-descending stack grows toward lower addresses, with sp identifying the lowest occupied address. Arguments beyond register capacity and automatic local objects require carefully derived stack offsets. Lookup tables, macros, and mixed-language routines can improve performance and reuse, but they introduce bounds, code-size, and interface risks. Reliable assembly development therefore depends on numerical range analysis, explicit contracts, disassembly inspection, stack checking, boundary testing, and disciplined debugging.
Key Terms
Assembler: A tool that translates assembly source into machine-code object files.
Directive: A command to the assembler rather than an instruction executed by the processor.
Label: A symbolic name associated with an address.
Relocation: Information allowing the linker to complete an address-dependent encoding.
Literal pool: Nearby memory containing constants loaded through PC-relative addressing.
ABI: Rules governing calls, registers, stack use, data layout, and binary interoperability.
Caller-saved register: A register that a called routine may overwrite.
Callee-saved register: A register a routine must restore if it modifies it.
Link register: A register holding a subroutine return address.
Full-descending stack: A stack that grows toward lower addresses, with SP identifying the lowest occupied address.
Stack frame: Storage associated with one active routine invocation.
Frame pointer: A register used as a stable reference to a stack frame.
Loop invariant: A condition that remains true before and after each loop iteration.
Lookup table: Stored values indexed to replace or accelerate a computation.
Macro: A parameterized assembly-time source expansion.
Condition flags: Processor bits recording properties such as zero, carry, sign, and overflow.
Reentrant routine: A routine safe for overlapping invocations because it avoids unprotected shared mutable state.
Watchpoint: A debugger facility that halts execution when selected memory is accessed.
Stack alignment: The requirement that SP satisfy an address multiple at specified interfaces.
Review Questions
Short Answer Questions
Distinguish an assembly instruction from an assembler directive.
What is the difference between caller-saved and callee-saved registers?
Why can a literal-load pseudo-instruction depend on a literal pool?
What does SP identify in a full-descending stack?
Why must signed and unsigned comparisons use different branch conditions?
Long Answer Questions
Explain the complete translation process from an assembly source file to an executable embedded image.
Describe how to design, document, and verify an ABI-compliant non-leaf subroutine.
Compare macros and subroutines in terms of execution time, code size, maintainability, and debugging.
Explain how stack-passed arguments and local variables are addressed after a function prologue.
Develop a systematic procedure for diagnosing a control-flow fault caused by stack corruption.
Analytical / Application Questions
Design an ARMv6-M routine that finds the maximum value in a nonempty unsigned 16-bit array. State its ABI, loop invariant, input assumptions, and boundary tests.
A routine receives six 32-bit arguments and allocates 24 bytes of local storage after pushing four registers. Derive the offsets of the fifth and sixth arguments relative to the final SP, assuming they were at entry offsets 0 and 4.
A data-acquisition system sums 2,000 unsigned 14-bit samples. Determine the maximum sum and minimum accumulator width, then propose tests that would expose overflow or loop-bound errors.
Multiple Choice Questions with Answers
Question 1
Which component normally resolves external symbols and places sections at final addresses?
A. Assembler preprocessor
B. Linker
C. Debugger
D. Instruction decoder
Correct Answer: B. Linker
Question 2
On an ARMv6-M processor, which statement is correct?
A. Neither CBZ nor IT is supported
B. CBZ is supported, but IT is not supported
C. CBZ and IT are supported, subject to architectural restrictions
D. UDIV is guaranteed to be supported with CBZ and IT
Correct Answer: C. CBZ and IT are supported, subject to architectural restrictions
Question 3
In a full-descending stack, SP identifies:
A. The highest unused address
B. The lowest occupied address
C. The function's first instruction
D. The caller's first argument in all cases
Correct Answer: B. The lowest occupied address
Question 4
A function pushes 8 bytes immediately after entry. A fifth argument was at [sp,#0] on entry. Where is it afterward?
A. [sp,#0]
B. [sp,#4]
C. [sp,#8]
D. [sp,#12]
Correct Answer: C. [sp,#8]
Question 5
What is the maximum sum of 1,024 unsigned 12-bit samples?
A. 4,193,280
B. 4,194,304
C. 2,097,152
D. 4,095,000
Correct Answer: A. 4,193,280
Question 6
How many unsigned bits are required to represent 4,193,280?
A. 20
B. 21
C. 22
D. 32
Correct Answer: C. 22
Question 7
Under the commonly used Arm AAPCS, which registers normally carry the first four 32-bit arguments?
A. r0–r3
B. r4–r7
C. r8–r11
D. sp, lr, pc, xPSR
Correct Answer: A. r0–r3
Question 8
Which practice is required when a subroutine modifies a callee-saved register?
A. Clear the register before returning
B. Return its new value in that register
C. Restore its original value before returning
D. Copy it to a caller-saved register after returning
Correct Answer: C. Restore its original value before returning
Question 9
For a 32-bit word lookup table, what byte offset corresponds to index i?
A. i
B. 2i
C. 4i
D. 8i
Correct Answer: C. 4i
Question 10
Which debugging check most directly detects an unbalanced local-stack allocation in a normally returning function?
A. Verify that the zero flag is clear
B. Verify that SPexit=SPentry
C. Verify that r0 contains zero
D. Verify that every loop uses CBZ
Correct Answer: B. Verify that SPexit=SPentry
Microprocessors & Microcontrollers
Chapter 5
of 14