Assembly Nerds

As of now, we have covered different aspects of the CPU in general. Now we shift gears a little bit and go more specific to assembly language. But before jumping in, let me clear things up with a quick context of how a program works in general.

A program takes inputs (either hard-coded or provided by the user) and then the CPU processes those inputs to generate some kind of output. That output doesn’t just float in the air — it has to be stored somewhere. Depending on what we want, it can either be stored in the memory or directly in the registers.

Fig 2.1: Autonomy of Programs

As you know, the CPU is very fast, but it’s also very picky about some things. It always wants to know exactly where the data is coming from, where it should go, and what size the data is. That means whenever we write instructions, we must be clear about whether the value is:

  • A constant number we are directly giving to the CPU,

  • A value sitting inside a register,

  • Something stored in memory at a specific location,

  • Or how much size is required to store the output.

The way of telling the CPU “where the data lives” is what we call addressing (as we discussed before). However, in assembly language, there are specific rules for how we can do that. These rules are known as addressing modes.

So, before we start writing bigger assembly programs, we first need to understand these addressing modes, because every single instruction (mov, add, loop, etc.) depends on them to know where to fetch data from, where to store it, and how much size is required for it.

Addressing Modes

Addressing modes are the rules that restrict us (developers) by defining the correct way to access memory or registers. For example:

  • Can we store more than 20 bits of data in the AX register? (No.)

  • Can we access any random memory location in RAM without restriction? (Also no.)

These rules are what make assembly both strict and predictable.

In 16-bit assembly programming, there are 8 addressing modes, which are the following:

  1. Immediate Mode

  2. Direct Access Mode

  3. Base Register Indirect Mode

  4. Base Register Indirect + Offset Mode

  5. Base Register + Index Mode

  6. Base + Index + Offset Indirect Mode

  7. Index Register Indirect Mode

  8. Index Register Indirect + Offset Mode

We will learn about each of them shortly. Trust me — their meanings are much simpler than how complicated they look at first glance.


The above addressing modes tell us which instruction are allowed, but why not just first know which instruction are not allowed.

Not Allowed Methods

  • 1. Size Mismatch

    We are not allowed to store data in a register that is larger or smaller than the register can hold. For example, AX is a 16-bit register. That means we cannot move just 8-bit data directly into AX. For 8-bit data, we must use either its lower (AL) or higher (AH) part. Similarly, we cannot add a 16-bit value to an 8-bit register.

    Example: Not Allowed

    mov ax, bl 
    mov al, ax
    mov bh, cx
    mov cl, dx
    mov cx, dl

    Example: Allowed

    mov ax, bx
    mov cx, dx
    mov al, bh
    mov cl, dl
  • 2. Memory To Memory

    We are not allowed to directly move data from one memory cell to another. For example, if we have two variables in memory, we cannot tell the CPU: “Take that variable from memory, add it to another variable in memory, and store the result back into a new memory cell.”

    Why? Because (as we discussed in the system buses section) every time the CPU fetches data from memory, it has to go through the address bus, control bus, and data bus. If we tried to do memory-to-memory operations for everything, it would be extremely slow and costly.

    That’s why we have registers. If we need to work with memory data, we first move it into a register, perform our operations there (which is very fast since registers don’t require buses), and then move the result back into memory if needed.

    Example: Not Allowed

    mov var1, var2  ; Very slow and not allowed

    Example: Allowed

    mov ax, var1    ; Load value from memory into register
    mov var2, ax    ; Store it back into memory
    mov ax, bx      ; Fastest way (no buses needed)
  • 3. Offset Subtraction

    First of all, an offset is just a number that represents the distance between memory addresses. You’ll see this more clearly when we work with arrays later, but let me give you a quick idea here.

    Suppose we have 10 consecutive memory locations holding related data. If we want to access the data in the 7th cell, how do we do it? Simple: if the first memory location is 0x00124, then the 7th cell can be accessed as:

    0x00124 + 7

    Easy, right?

    But here’s the catch: while we can add offsets, we are not allowed to subtract them. For example:

    0x00124 - 7

    This is not allowed. We can only add offsets, not subtract. The number we add (like 7 in the example) is what we call the offset.


So all the above not allowed methods are defined by the architecture of the processor, and based on that we can use different addressing modes. The 8 addressing modes that are allowed which previously I was discussing (Sorry for being sidetracked..).

Why did I first explain the “not allowed” things? Because they’re easier to understand — and now that you’ve seen them, it will be much simpler to explain the 8 allowed addressing modes of assembly.

Phew! That was a lot to digest. I know it may feel like your brain just did a full workout, so take a quick sip from your water bottle. Seriously — a tiny pause helps a lot before we dive into learning about Assembly instruction.

1. Immediate Mode

You know we’ve already seen this type of declaration before. Do you remember our Warmup Code from the very beginning? That’s where we moved some values directly into registers. Forgot already? No worries — here’s the snippet again:

mov ax, 6
mov bx, 9

This is exactly what Immediate Mode is about. In this mode, we directly put (or move) data into a register using our instruction. The value we provide is a constant, written right in the code, and it gets copied straight into the register.

For example:

mov cx, 25
mov dx, 100

Here, 25 and 100 are immediate values, and the CPU places them directly inside the registers CX and DX.

Immediate values are fixed at the time of writing the program. You can’t change them during execution because they are hard-coded into the instruction itself.

2. Direct Access Mode

In this mode, we give a direct memory address or a variable name. (Yes, a variable is basically just a direct address — even though we give a memory cell a name, behind the scenes, it’s just an address. The assembler handles that translation for us.)

For example:

mov ax, var1
mov ax, [var1]
mov ax, 0x000124
mov ax, [0x000124]

Here’s an important thing to notice:

  • If the variable or address is written inside [] brackets, it means we want to get the value stored at that address.

  • If it is written without [], it means we want to store the address itself into the register.

This mode is called Direct Access because we are explicitly telling the CPU exactly which memory location to use.

3. Base Register Indirect Mode

Remember the BX register we learned about in the General Purpose Registers section? And in the previous section, we also learned about the meaning of the [] brackets. In this mode, the idea is simple: combine a base register like BX , BPwith the indirect mode [] brackets.

So what does this actually mean? We can use BX together with [] to access memory indirectly. You might be thinking: “Wait… registers don’t have addresses, so how can we use [] with a register?”

You’re right, registers themselves don’t have addresses. But remember the purpose of the BX register: it is often used to store a base pointer, which is essentially the address of some memory location. So when we write [BX], we’re telling the CPU:

“Go to the memory address stored inside BX and get the value there.”

Example:

mov bx, var1            ; Store the address of var1 in BX
mov [bx], ax            ; Store the value of AX into the memory address pointed by BX
mov ax, [bx]            ; Load the value stored at the address in BX into AX

So in short, Base Register Indirect Mode lets us access memory through a pointer stored in a register. I hope that makes sense.

4. Base Register Indirect + Offset Mode

We’ve already talked about offsets and Base Register Indirect Mode. Now, this mode is simply a combination of both. One thing to note is that we often use an index register to store the offset. Remember the index registers SI and DI that we discussed earlier? They come in handy here.

Here’s how it looks:

mov ax, [bx + 2]           ; Load the value at memory address (BX + 2) into AX
mov [bx + 2], ax           ; Store the value of AX into memory address (BX + 2)

We use these kinds of declarations a lot when working with arrays, where the offset helps us access different elements.

For now, just focus on understanding the concept: we are using a base register to point to a starting memory address and then adding an offset (either a number or an index register) to reach the exact location we want.

5. Base Register + Index Mode

The mode is exactly same as before, there we also use both a base register and an index register together to calculate the memory address. The base register usually points to the start of a block of memory, while the index register helps us move around inside that block.

Example:

mov ax, [bx + si]          ; Load value from memory address (BX + SI) into AX
mov [bx + di], ax          ; Store value of AX into memory address (BX + DI)

This is very similar to the previous mode you can see, but here the offset is always coming from an index register, not just a constant number. Just a little distinction other wise the same.

6. Base + Index + Offset Indirect Mode

This is basically the supercharged version of all the previous modes. Here, we combine:

  1. A base register (like BX)

  2. An index register (like SI or DI)

  3. A constant offset

All three together give us the exact memory address we want to access. This mode is very powerful when working with arrays of structures or tables, where the starting point is a base, we move around with an index, and sometimes add a fixed offset to reach a specific element or field.

Example:

mov ax, [bx + si + 4]       ; Load value from memory address (BX + SI + 4) into AX
mov [bx + di + 2], ax       ; Store value of AX into memory address (BX + DI + 2)

Here’s what’s happening step by step:

  1. BX points to the start of a block (base address).

  2. SI or DI moves us to a specific element inside that block (index).

  3. The + 4 or + 2 adds a fixed offset to reach the exact field or location we want.

7. Index Register Indirect Mode

Here, we drop the base register and just use an index register like SI or DI to point directly to a memory location. The value inside the index register acts as the address itself.

Example:

mov ax, [si]               ; Load value from memory address stored in SI into AX
mov [di], ax               ; Store value of AX into memory address stored in DI

So instead of combining with a base register, we rely purely on the index register. It’s simpler but still very powerful when we work with arrays or blocks of data.

8. Index Register Indirect + Offset Mode

Finally, this mode combines an index register with an offset, similar to how we combined a base register with an offset before.

Example:

mov ax, [si + 4]           ; Load value from memory address (SI + 4) into AX
mov [di + 2], ax           ; Store value of AX into memory address (DI + 2)

This is particularly useful when you want to access a specific element in an array while the index register already points to the start of the array.

For now, the key idea is simple:

We can calculate the exact memory location by combining a register (base or index) with an optional offset.

Addressing Modes Summary Table

Mode
Operand Type
Syntax Example
Notes

Immediate Mode

Constant value

mov ax, 6

Value is hard-coded; goes directly into register. Cannot change at runtime.

Direct Access Mode

Memory address / variable

mov ax, [var1]

Access memory directly. [] means fetch value at the address; without [] gives the address itself.

Base Register Indirect Mode

Memory via base register

mov ax, [bx]

Base register (BX/BP) holds address; [] fetches/stores value at that address.

Base Register Indirect + Offset Mode

Base register + numeric offset

mov ax, [bx + 2]

Access memory at base + offset. Useful for arrays.

Base + Index Mode

Base + index register

mov ax, [bx + si]

Base points to start; index moves within block.

Base + Index + Offset Mode

Base + index + constant

mov ax, [bx + si + 4]

Combines base, index, and offset. Perfect for structured arrays or tables.

Index Register Indirect Mode

Index register only

mov ax, [si]

Index register points directly to memory location.

Index Register Indirect + Offset Mode

Index register + numeric offset

mov ax, [si + 4]

Index points to start; offset moves to exact element.

Last updated

Was this helpful?