Computer ScienceCore24 min read

Assembly Language and Bit Manipulation

One instruction per line, and the operations that work on individual bits

This topic appears in:

01

Between machine code and a programming language

Machine code is binary and unreadable. A high-level language is readable but hides the machine entirely. Assembly language sits between them: each instruction corresponds to exactly one machine instruction, but written as a short mnemonic such as LDD or ADD instead of a pattern of bits.

That one-to-one correspondence is the defining property. It means an assembler needs only to look up each mnemonic and substitute the opcode, which is why assembly is translated by a simple assembler rather than a compiler.

It is still used where precise control matters — device drivers, embedded controllers, and small routines where every cycle counts.

InstructionMeaning
LDD <address>load the contents of that address into the ACC
LDM #nload the immediate value n into the ACC
STO <address>store the ACC into that address
ADD <address>add the contents of that address to the ACC
SUB <address>subtract the contents of that address from the ACC
INC <register>add 1 to the register
CMP <address>compare the ACC with the contents of that address
JMP <label>jump unconditionally to the label
JPE <label>jump to the label if the last compare was equal
JPN <label>jump to the label if the last compare was not equal
ENDend the program

Immediate, direct, indirect and indexed

The addressing mode says how to interpret the operand. Immediate means the operand is the value. Direct means it is the address holding the value. Indirect means it is the address of an address. Indexed means add the contents of an index register to it — which is how arrays are walked through. Questions frequently give the same operand under different modes and ask for the resulting value.

02

Tracing a program

The standard exam task is a trace table: work through the instructions in order, recording the accumulator and any changed memory after each one. It is mechanical, and the marks are for accuracy rather than insight.

Two habits prevent most errors. Write one row per instruction executed, not per line of source — a loop body executed four times produces four sets of rows. And carry unchanged values down the table rather than leaving blanks, so the current state is always visible on the last row.

Worked example

Trace this program, giving the final contents of the accumulator.
LDM #5
STO 100
LDM #3
ADD 100
END

  1. LDM #5 — the immediate value 5 is loaded. ACC = 5.The # marks an immediate value, so 5 itself goes into the accumulator rather than the contents of address 5.
  2. STO 100 — the accumulator is stored at address 100. Memory[100] = 5, ACC still 5.STO copies rather than moves, so the accumulator is unchanged.
  3. LDM #3 — the immediate value 3 is loaded. ACC = 3.This overwrites the accumulator; 5 survives only because it was stored first.
  4. ADD 100 — the contents of address 100 are added. ACC = 3 + 5 = 8.ADD uses direct addressing, so it adds what is at address 100, not the number 100.
  5. END — the program stops with ACC = 8.The final state is what the question asks for.

ACC = 8

The same discipline applies to any trace: one row per executed instruction, and every variable carried forward so the current state is always readable off the bottom row.

The # changes everything

LDM #100 loads the number 100. LDD 100 loads whatever is stored at address 100 — which might be any value at all. Missing the # is the single most common trace error, and because the two look almost identical it usually goes unnoticed until the final answer is wrong.

03

Working on individual bits

Sometimes a whole byte is not the unit of interest — a single flag within it is. Bit manipulation provides the operations for reading and changing individual bits, using a mask: a pattern chosen so that the operation affects only the bits you want.

Three logical operations do the work, and each has one job. AND with a mask clears every bit where the mask is 0, so it is used to test or isolate. OR with a mask sets every bit where the mask is 1. XOR with a mask flips every bit where the mask is 1.

test or clear bits: AND with a mask(0 clears, 1 keeps)set bits:OR with a mask(1 sets, 0 keeps)flip bits:XOR with a mask(1 flips, 0 keeps)shifts:logical left by n × 2ⁿzeros shifted inlogical right by n ÷ 2ⁿzeros shifted inarithmetic right÷ 2ⁿsign bit preservedcyclicbits wrap round, nothing losta left shift by one doubles; a right shift by one halves
mask
the chosen bit patterndesigned so only the target bits are affected
logical shift
zeros shifted insuitable for unsigned values
arithmetic shift
sign bit preservedso negative numbers stay negative

Choosing the operation

  1. To check whether bit 3 is set: AND with 00001000 and test for a non-zero result.
  2. To turn bit 3 on: OR with 00001000.
  3. To turn bit 3 off: AND with 11110111 — the complement of the mask.
  4. To toggle bit 3: XOR with 00001000.
  5. A left shift multiplies by two, but bits shifted off the end are lost, which can overflow.
  6. An arithmetic right shift keeps the sign, so −8 shifted right becomes −4 rather than a large positive number.
04

Why anyone still writes it

Almost all software is written in high-level languages, so it is fair to ask why assembly survives at all. Three reasons keep it alive, and questions ask for them.

The first is direct hardware access. A device driver has to write specific values to specific hardware registers at specific addresses, and a high-level language deliberately hides exactly that. The second is predictable timing: in an embedded controller running a motor or an airbag, the number of clock cycles a routine takes may genuinely matter, and only assembly makes it exactly knowable. The third is size — a microcontroller with two kilobytes of memory cannot afford the overhead a compiler adds.

The costs are equally real. Assembly is specific to one processor family, so nothing ports. It is verbose, since one high-level statement may take a dozen instructions. And it is far harder to read, which makes maintenance expensive and errors more likely.

AssemblyHigh-level language
Hardware controlcompleteabstracted away
Timingexactly predictabledepends on the compiler
Portabilitynone — tied to one processorrecompile and run
Development speedslowfast
Readabilitypoorgood
Program sizevery compactlarger

The modern compromise

Most embedded projects are now written almost entirely in C, with assembly used only for the few routines that genuinely need it — an interrupt handler, or a timing-critical inner loop. That keeps the bulk of the code portable and readable while retaining exact control where it matters. A question asking whether a whole system should be written in assembly usually wants this answer rather than a straight yes or no.

Practice questions

5 questions · 16 marks · full working on every one

Try each one on paper first, then open the working. The marks are shown where they are actually awarded, because that is where they are actually lost.

Short questions

3 · 6 marks

Two marks each, in the style of the short-question section of the paper. Answer in two or three lines.

SQ1[2 marks]
Explain what is meant by assembly language and why it needs only an assembler rather than a compiler.
Model answer

Assembly language uses short mnemonics in place of binary machine code, with one instruction corresponding to one machine instruction. Because the relationship is one-to-one, translation is a matter of substituting each mnemonic for its opcode — a much simpler task than compiling a high-level language, where one statement may become many machine instructions.

Examiner tip. The one-to-one correspondence is the mark. It is what distinguishes an assembler from a compiler.

SQ2[2 marks]
Explain the difference between LDM #20 and LDD 20.
Model answer

LDM #20 uses immediate addressing: the value 20 itself is loaded into the accumulator. LDD 20 uses direct addressing: the contents of memory address 20 are loaded, which could be any value.

Examiner tip. Name both addressing modes explicitly — the marks are for the modes, not just the outcomes.

SQ3[2 marks]
State the effect of a logical left shift of 3 places on an unsigned binary value, and one circumstance in which the result would be wrong.
Model answer

It multiplies the value by 2³ = 8. The result would be wrong if any bits shifted off the left-hand end were 1s, since those bits are lost — the value would then have overflowed the available word length.

Examiner tip. The overflow condition is the second mark, and is what distinguishes a full answer from a partial one.

Solved numericals

1 · 4 marks

Full working, one step per line, with the marks shown where they are awarded.

N1[4 marks]
The accumulator holds 10110110. State the result of (a) AND with 00001111, (b) OR with 01000000, (c) XOR with 11111111, (d) a logical left shift of 1 place.
Full working
  1. (a) 10110110 AND 00001111 = 00000110The mask keeps the lower four bits and clears the upper four.[1]
  2. (b) 10110110 OR 01000000 = 11110110OR sets bit 6; every other bit is unchanged because the mask has 0 there.[1]
  3. (c) 10110110 XOR 11111111 = 01001001XOR with all ones inverts every bit — a one's complement.[1]
  4. (d) 10110110 shifted left 1 = 01101100A zero enters at the right and the leftmost bit is lost, so the value does not simply double here.[1]

(a) 00000110 (b) 11110110 (c) 01001001 (d) 01101100

Exam questions

1 · 6 marks

Multi-part questions with a full mark scheme.

Q1[6 marks]
A status byte uses bit 2 as an error flag (bit 0 is the least significant).
(a) Give the mask and operation to test whether the flag is set.
(b) Give the mask and operation to set the flag.
(c) Give the mask and operation to clear the flag.
(d) Explain why an arithmetic right shift is used rather than a logical one when dividing a signed number.
Mark scheme
  1. (a) AND with 00000100AND isolates the bit of interest and clears all others.[1]
  2. If the result is non-zero the flag is set; if zero it is clear.The test on the result is part of the answer.[1]
  3. (b) OR with 00000100OR forces a 1 into that position without disturbing the others.[1]
  4. (c) AND with 11111011The complement of the mask — a 0 in the target position clears it, 1s elsewhere preserve.[1]
  5. (d) A logical shift brings in a 0 at the left, which would overwrite the sign bit and turn a negative number positive.The sign bit is the crux.[1]
  6. An arithmetic shift replicates the sign bit instead, so the sign is preserved and the division gives the correct signed result.Both halves — what goes wrong and what is done instead — are needed.[1]

(a) AND 00000100, non-zero means set; (b) OR 00000100; (c) AND 11111011; (d) preserves the sign bit