Microprocessors and Microcontrollers

Chapter 4
Assembly Language Programming

Diploma in Computer Architecture and Networking
Microprocessors and Microcontrollers   •   Diploma

Assembly Language Programming

Introduction

Assembly language is a symbolic representation of a processor’s machine instructions. It provides more control over registers, memory, flags, timing, and input/output than a high-level language, while remaining closely tied to a particular instruction-set architecture. For diploma students, assembly programming is important because it explains how arithmetic expressions, decisions, loops, arrays, procedures, and data conversions are executed inside a microprocessor.

This chapter uses 8086-style assembly syntax for examples. The principles apply broadly to other processors, although instruction names, registers, addressing modes, and assembler directives differ. In 8086 notation, an operand such as AL is an 8-bit register, AX is a 16-bit register, and DS:offset identifies a memory location using the data segment and an offset.

Learning Objectives

After completing this chapter, a learner should be able to:

  • Explain the structure and execution of an assembly-language statement.
  • Select suitable registers and addressing modes.
  • Write and trace arithmetic, logical, branching, and loop programs.
  • Process arrays and strings using indexed addressing.
  • Design procedures using the stack and calling conventions.
  • Convert between binary, hexadecimal, decimal, and ASCII representations.
  • Identify and correct syntax, logical, register, and stack errors.
  • Apply readable and maintainable assembly-language coding conventions.

1. Assembly Program Structure

An assembly statement commonly contains four fields:

label:    mnemonic    operands       ; comment

A label identifies an address. The mnemonic specifies an operation, operands identify data, and a comment documents intent. For example:

MOV     AX, 25       ; Copy the immediate value 25 into AX
ADD     AX, BX       ; AX ← AX + BX

The notation AX ← AX + BX describes the destination after execution; it does not mean that AX and BX are mathematical variables independent of processor state.

A small 8086 program may contain segments such as:

.DATA
value1  DW  25
value2  DW  17
result  DW  ?

.CODE
MOV AX, @DATA
MOV DS, AX
MOV AX, value1
ADD AX, value2
MOV result, AX

DW reserves a 16-bit word. ? indicates uninitialized storage. Loading the data-segment address into DS is required by many 8086 execution environments before accessing data variables.

Registers and Flags

Important 8086 registers include:

RegisterMain purpose
AXAccumulator; arithmetic and input/output operations
BXBase register; general-purpose addressing
CXCount register for loops and shifts
DXData register; multiplication, division, and I/O support
SISource index
DIDestination index
BPStack-frame or base-pointer register
SPPoints to the top of the stack
IPOffset of the next instruction
FLAGSStatus and control bits

The carry flag (CF) indicates an unsigned carry or borrow. The zero flag (ZF) is set when a result is zero. The sign flag (SF) reflects the most significant bit of a result. The overflow flag (OF) indicates signed-range overflow. The parity flag (PF) reflects the parity of the low-order result byte. Conditional jumps interpret these flags.

A central distinction is between unsigned and signed interpretation. The same bit pattern can represent different values. For example, the 8-bit pattern 11111111₂ is 255 unsigned but −1 in two’s-complement signed representation.

2. Addressing Modes

An addressing mode specifies how an instruction obtains an operand.

  • Immediate: MOV AX, 12 uses a constant.
  • Register: MOV AX, BX uses a register.
  • Direct memory: MOV AL, count accesses a named memory location.
  • Register indirect: MOV AL, [BX] uses BX as an address.
  • Indexed: MOV AL, array[SI] accesses an element using an index.
  • Based-indexed: MOV AL, [BX+SI] combines base and index information.

The instruction MOV AX, [BX] copies a word from memory, whereas MOV AX, BX copies the contents of BX. Confusing these forms is a frequent source of errors.

8086 instructions generally cannot copy memory directly to memory with MOV; an intermediate register is required:

MOV AX, source
MOV destination, AX

3. Arithmetic Programming

Addition and Subtraction

MOV AX, 125
MOV BX, 37
ADD AX, BX       ; AX = 162
SUB AX, 12       ; AX = 150

For unsigned addition, CF reveals a carry beyond the operand width. For signed addition, OF reveals that the mathematical result is outside the signed range. A program must test the appropriate flag for the data representation being used.

The instruction INC increments an operand by one and DEC decrements it by one. Unlike ADD and SUB, INC and DEC do not modify CF on the 8086. This distinction matters when a carry is being propagated.

Multiplication and Division

For unsigned multiplication, MUL uses an implicit accumulator. MUL BL multiplies AL by BL and places the 16-bit result in AX. MUL BX multiplies AX by BX and places the 32-bit result in DX:AX, where DX contains the high word.

For unsigned division, DIV divides a double-width dividend by its operand. DIV BL divides AX by BL, placing the quotient in AL and remainder in AH. DIV BX divides DX:AX by BX, placing quotient in AX and remainder in DX. Before an 8-bit division, the high half of the dividend must be correctly prepared; for an unsigned byte dividend in AL, MOV AH, 0 is typical.

Signed operations use IMUL and IDIV. Before signed division, CBW sign-extends AL into AX, while CWD sign-extends AX into DX:AX.

Worked Example: Arithmetic Expression

Evaluate result = (a + b) − c, where a = 28, b = 19, and c = 11.

MOV AX, a        ; AX = 28
ADD AX, b        ; AX = 47
SUB AX, c        ; AX = 36
MOV result, AX

The computation is 28 + 19 − 11 = 36. The program is correct because all values fit in a 16-bit unsigned word and no overflow-handling requirement is specified.

4. Logical and Bit-Manipulation Instructions

Logical instructions operate independently on corresponding bits:

InstructionOperationTypical use
ANDBitwise conjunctionClear or test selected bits
ORBitwise inclusive disjunctionSet selected bits
XORBitwise exclusive disjunctionToggle bits or clear a register
NOTComplementInvert every bit
TESTAND for flags onlyTest bits without changing the operand

To clear AX, XOR AX, AX is common. To test whether bit 2 of AL is set:

TEST AL, 00000100b
JNZ  BIT_IS_SET

A mask must be selected carefully. AND AL, 0Fh retains the lower four bits, while OR AL, 80h sets the most significant bit. Shift and rotate instructions support multiplication or division by powers of two, bit extraction, and serial data handling. Logical right shift inserts zeros; arithmetic right shift preserves the sign bit.

5. Comparisons and Branching

CMP destination, source performs a subtraction for flag-setting purposes but does not store the result. A conditional jump then examines the flags.

For unsigned comparisons use JA, JAE, JB, and JBE. For signed comparisons use JG, JGE, JL, and JLE. JE/JZ and JNE/JNZ are useful for equality regardless of signed interpretation.

MOV AX, marks
CMP AX, 50
JAE  PASS
MOV grade, 0
JMP  DONE
PASS:
MOV grade, 1
DONE:

Here JAE is appropriate because marks are treated as unsigned, nonnegative values. Using JL or JG without deciding the data representation can produce incorrect decisions.

A branching algorithm should identify all paths, including equality and boundary values. For a range test such as 10 ≤ x ≤ 20, test the lower and upper boundaries explicitly and document whether endpoints are included.

6. Loops and Counters

A loop contains initialization, a condition, a body, and an update. The 8086 instruction LOOP label decrements CX and jumps when CX is not zero.

MOV CX, 5
MOV AX, 0
L1:
ADD AX, 2
LOOP L1

After five iterations AX equals 10. The body must not modify CX unintentionally. When a loop needs a more complex condition, use CMP and a conditional jump instead of LOOP.

A pre-test loop skips its body when the condition is initially false. A post-test loop executes its body at least once. Assembly programmers must construct these forms explicitly; an accidental placement of the test can create an off-by-one error or an infinite loop.

Tracing a Loop

For a loop that sums 3, 5, and 7:

MOV SI, 0
MOV CX, 3
MOV AX, 0
L2:
ADD AX, numbers[SI]
ADD SI, 2
LOOP L2

For word elements, SI increases by 2, not 1. The trace is:

IterationSI before accessValueAX after addition
1033
2258
34715

7. Arrays and Strings

An array is a consecutive collection of equal-size elements. The effective address of element i is conceptually:

address(elementi) = baseaddress + i × elementsize

Here i is the zero-based element index and element_size is measured in bytes. The expression applies to a contiguous array with fixed-size elements.

For byte arrays, increment the index by 1; for word arrays, increment it by 2. A common pattern for finding the largest byte is:

MOV SI, 0
MOV AL, array[SI]
MOV CX, LENGTHOF array - 1
INC SI
NEXT:
CMP AL, array[SI]
JAE NOT_LARGER
MOV AL, array[SI]
NOT_LARGER:
INC SI
LOOP NEXT
MOV largest, AL

This assumes the array contains at least one element. If the array may be empty, its length must be checked before loading the first element.

String instructions such as MOVSB, CMPSB, SCASB, LODSB, and STOSB use SI, DI, and the direction flag. CLD selects forward movement; STD selects backward movement. A procedure that changes the direction flag should restore it or document the convention.

8. Procedures and the Stack

A procedure divides a program into reusable units. CALL saves a return address on the stack and transfers control; RET removes the return address and resumes execution.

; Input: AX and BX; output: AX = AX + BX
ADD_TWO PROC
    ADD AX, BX
    RET
ADD_TWO ENDP

CALL ADD_TWO

A professional procedure documents inputs, outputs, registers modified, flags affected, and assumptions. Calling conventions may require a procedure to preserve BX, SI, DI, or BP while allowing AX and flags to be changed. The convention must be agreed by caller and callee.

Stack Operations

PUSH decrements SP and stores a word; POP reads the top word and increments SP. The stack is last-in, first-out (LIFO).

PUSH AX
PUSH BX
; temporary work
POP BX
POP AX

The order is essential: the last value pushed is the first value popped. Mismatched push/pop operations can replace the return address with data, causing a crash or uncontrolled execution.

A procedure that uses registers temporarily can preserve them:

PROC1 PROC
    PUSH BX
    PUSH SI
    ; procedure body
    POP SI
    POP BX
    RET
PROC1 ENDP

For local variables and parameters, BP can provide a stable stack-frame reference. A typical frame begins with PUSH BP and MOV BP, SP; however, the exact parameter offsets depend on the calling convention and the number of saved words.

9. Data Conversion

Binary and Hexadecimal

Hexadecimal is compact because one hexadecimal digit represents four binary bits. The mapping is 0–9 followed by A–F. The binary value 1010 0111₂ is A7h.

To convert a nibble to an ASCII hexadecimal character, values 0–9 are adjusted by ASCII '0'; values 10–15 are adjusted by ASCII 'A' − 10.

CMP AL, 9
JBE DIGIT
ADD AL, 'A' - 10
JMP STORE
DIGIT:
ADD AL, '0'
STORE:

ASCII Decimal Input

To convert a sequence of decimal digit characters to an unsigned integer, repeatedly apply:

valuenew = valueold × 10 + digit

The digit is obtained from its character by subtracting ASCII '0'. For input “314”, the stages are 3, 3 × 10 + 1 = 31, and 31 × 10 + 4 = 314. The routine must reject non-digits and check for overflow before multiplying and adding.

To convert an integer to decimal text, repeatedly divide by 10. Each remainder is a digit, but digits are generated from least significant to most significant, so they must be stored or pushed and later emitted in reverse order. Zero is a special case and must produce one digit, “0”.

10. Debugging and Verification

Assembly debugging should proceed systematically:

  1. State the intended input, output, and assumptions.
  2. Identify registers and memory locations used by each variable.
  3. Trace instructions while recording register and flag values.
  4. Check addresses, operand sizes, and loop bounds.
  5. Test normal, boundary, invalid, and overflow cases.
  6. Use a debugger to inspect memory, stack contents, and instruction flow.

Common errors include:

  • Syntax errors: invalid mnemonic, operand size, or label.
  • Register errors: using an incorrect implicit register or destroying a needed value.
  • Flag errors: using a signed jump for unsigned data, or assuming an instruction changes CF when it does not.
  • Addressing errors: treating a byte index as a word index.
  • Logic errors: wrong branch condition, off-by-one loop, or missing initialization.
  • Stack errors: unbalanced pushes and pops, incorrect parameter offsets, or failure to preserve required registers.
  • Data errors: confusing ASCII codes with numeric values; for example, ASCII '5' is not the integer 5.

A useful invariant is a statement that remains true at each loop iteration. For an array-sum loop, the invariant may be: “AX contains the sum of all elements already processed.” Invariants make both tracing and debugging more rigorous.

11. Coding Conventions and Professional Practice

Readable assembly is not an optional feature. Recommended conventions include:

  • Use meaningful labels such as SUM_LOOP, INPUT_ERROR, and CONVERT_DONE.
  • Keep one logical operation per line.
  • Align mnemonics and comments consistently.
  • State whether values are signed, unsigned, ASCII, packed, or unpacked.
  • Document procedure interfaces and register preservation.
  • Use symbolic constants instead of unexplained numeric literals.
  • Avoid unnecessary self-modifying code.
  • Keep critical sections short and protect shared data in interrupt-driven systems.
  • Assemble and test after small changes rather than after a large unverified rewrite.

Assembly programs can access hardware directly, so incorrect code may corrupt memory, disable interrupts, expose sensitive data, or leave devices in unsafe states. Production firmware should include bounds checks, controlled privilege, watchdog recovery where appropriate, and review of all memory-mapped I/O operations.

12. Applied Case: Temperature Monitor Conversion

Background

Consider an illustrative embedded temperature monitor. A sensor supplies an unsigned 8-bit Celsius value. The system must determine whether the reading is within the permitted range, convert it to a two-character hexadecimal display, and signal an alarm when it exceeds 50 °C.

Problem

The program must process one sensor byte without confusing its numeric value with display characters. It must also avoid overwriting the original reading before the alarm comparison.

Approach

  1. Read the sensor value into AL.
  2. Copy it to BL so that one copy is retained for comparison.
  3. Compare BL with 50 using an unsigned conditional jump.
  4. Convert the high and low nibbles of AL separately to ASCII hexadecimal.
  5. Send the resulting characters to the display procedure.

A conceptual fragment is:

MOV AL, sensor_value
MOV BL, AL
CMP BL, 50
JA  ALARM

MOV AH, AL
AND AH, 0F0h
MOV CL, 4
SHR AH, CL
CALL NIBBLE_TO_ASCII
; display high character

AND AL, 0Fh
CALL NIBBLE_TO_ASCII
; display low character
JMP DONE

ALARM:
CALL SOUND_ALARM
DONE:

The actual NIBBLE_TO_ASCII procedure must specify its input and output register. If it changes flags or registers used by the caller, that effect must be documented.

Analysis and Outcome

For a sensor value of 37, the alarm condition is false. Hexadecimal conversion produces 25, because 37 = 2 × 16 + 5. For a value of 51, the unsigned comparison transfers control to ALARM. The design separates decision data from display processing and handles the nibble conversion explicitly.

Learning Points

The case demonstrates register allocation, unsigned comparison, bit masking, shifting, procedure contracts, and the difference between a numeric byte and its ASCII representation. It also illustrates why boundary testing is essential: values 49, 50, and 51 exercise both sides of the alarm threshold.

13. Contemporary Relevance and Limitations

Assembly remains important in boot loaders, interrupt handlers, device drivers, real-time control, cryptographic primitives, performance-critical kernels, and reverse engineering. Modern compilers often generate better optimized code for ordinary application logic, so assembly should be used where hardware control, timing, code size, or special instructions justify it.

The principal limitation is portability: code tied to 8086 registers or segmentation cannot be moved directly to ARM, AVR, RISC-V, or x86-64. Other limitations include difficult maintenance, limited abstraction, and increased risk of memory-safety defects. Inline assembly should therefore be small, documented, tested against compiler conventions, and isolated behind a clear interface.

Chapter Summary

Assembly language exposes the operations performed by a microprocessor. Effective programming requires knowledge of registers, flags, addressing modes, operand sizes, and the distinction between signed, unsigned, and character data. Arithmetic instructions must be paired with appropriate overflow or carry tests. Logical instructions manipulate individual bits, while comparisons and conditional jumps implement decisions. Loops require deliberate initialization, termination, and index scaling. Arrays depend on correct element size and address calculation. Procedures rely on explicit input/output contracts and balanced stack operations. Data conversion requires careful handling of ASCII codes, nibbles, quotient–remainder processes, and special cases such as zero. Reliable assembly development combines tracing, invariants, boundary testing, debugger use, and disciplined coding conventions.

Key Terms

Assembly language: Symbolic language representing processor instructions.

Mnemonic: Human-readable name of an instruction.

Operand: Register, constant, or memory item operated on by an instruction.

Register: Fast storage location inside the processor.

Flag: Status bit affected by instruction execution.

Carry flag: Flag indicating an unsigned carry or borrow.

Overflow flag: Flag indicating signed-range overflow.

Addressing mode: Method used to locate an operand.

Immediate operand: Constant encoded in an instruction.

Effective address: Computed address of a memory operand.

Opcode: Machine-level operation code.

Accumulator: Register conventionally used for arithmetic results.

Procedure: Reusable named block of instructions.

Stack: LIFO memory structure used for returns and temporary data.

Calling convention: Agreement about parameters, results, and preserved registers.

ASCII: Character encoding in which digits and letters have numeric codes.

Nibble: Four-bit group.

Sign extension: Replication of a signed value’s sign bit into higher bits.

Loop invariant: Condition that remains true during loop execution.

Memory-mapped I/O: Hardware access through designated memory addresses.

Review Questions

Short Answer Questions

  1. What are the main fields of an assembly-language statement?
  2. Distinguish CF from OF.
  3. Why must a word-array index normally increase by two bytes?
  4. What is the purpose of CMP?
  5. Why must pushes and pops be balanced in a procedure?

Long Answer Questions

  1. Explain 8086 addressing modes and illustrate at least four with instructions.
  2. Describe unsigned and signed multiplication and division, including implicit registers.
  3. Explain how arrays, indexed addressing, and loop counters work together.
  4. Discuss procedure interfaces, register preservation, and stack frames.
  5. Explain decimal ASCII input conversion and integer-to-decimal output conversion.

Analytical / Application Questions

  1. Design an assembly algorithm to find the smallest unsigned byte in an array. State the empty-array assumption and identify the loop invariant.
  2. A program uses JL to compare two sensor readings that are never negative. Analyse the error and specify the appropriate unsigned branch strategy.
  3. Trace a procedure that pushes AX and BX but pops AX and BX in the same order. Explain the resulting register values and correct the code.

Multiple Choice Questions with Answers

Question 1

Which flag is primarily used to detect carry out of an unsigned addition?

A. Zero flag

B. Carry flag

C. Sign flag

D. Overflow flag

Correct Answer: B. Carry flag

Question 2

Which instruction compares two operands without storing the subtraction result?

A. TEST

B. CMP

C. XOR

D. LOOP

Correct Answer: B. CMP

Question 3

For a word array, what is the usual byte increment for moving from one element to the next on the 8086?

A. 1

B. 2

C. 4

D. 8

Correct Answer: B. 2

Question 4

What is the result of XOR AX, AX?

A. AX becomes all ones

B. AX is shifted left

C. AX becomes zero

D. AX is copied into BX

Correct Answer: C. AX becomes zero

Question 5

Which register is automatically decremented by the 8086 LOOP instruction?

A. AX

B. BX

C. CX

D. DX

Correct Answer: C. CX

Question 6

What is the stack order operation represented by PUSH followed later by POP?

A. FIFO

B. LIFO

C. Random access

D. Circular buffering

Correct Answer: B. LIFO

Question 7

What is the decimal value of the hexadecimal byte 25h?

A. 25

B. 32

C. 37

D. 42

Correct Answer: C. 37

Question 8

Which instruction sign-extends the byte in AL into AX on the 8086?

A. CWD

B. CBW

C. CDQ

D. MOVSX

Correct Answer: B. CBW

Question 9

Which conditional jump is appropriate for an unsigned 'greater than or equal' comparison after CMP?

A. JGE

B. JAE

C. JG

D. JL

Correct Answer: B. JAE

Question 10

When converting decimal text to an integer, which recurrence processes digits from left to right?

A. new = old + digit ÷ 10

B. new = old × 2 + digit

C. new = old × 10 + digit

D. new = old − digit × 10

Correct Answer: C. new = old × 10 + digit

Microprocessors and Microcontrollers
Chapter 4 of 14