assemblyx86masmaddressing-mode

Referencing the contents of a memory location. (x86 addressing modes)


I have a memory location that contains a character that I want to compare with another character (and it's not at the top of the stack so I can't just pop it). How do I reference the contents of a memory location so I can compare it?

Basically how do I do it syntactically.


Solution

  • For a more extended discussion of addressing modes (16/32/64bit), see Agner Fog's "Optimizing Assembly" guide, section 3.3. That guide has much more detail than this answer for relocation for symbols and or 32bit position-independent code, among other things.

    And of course Intel and AMD's manuals have whole sections on the details of the encodings of ModRM (and optional SIB and disp8/disp32 bytes), which makes it clear what's encodeable and why limits exist.

    See also: table of AT&T(GNU) syntax vs. NASM syntax for different addressing modes, including indirect jumps / calls. Also see the collection of links at the bottom of this answer.


    x86 (32 and 64bit) has several addressing modes to choose from. They're all of the form:

    [base_reg + index_reg*scale + displacement]      ; or a subset of this
    [RIP + displacement]     ; or RIP-relative: 64bit only.  No index reg is allowed
    

    (where scale is 1, 2, 4, or 8, and displacement is a signed 32-bit constant). All the other forms (except RIP-relative) are subsets of this that leave out one or more component. This means you don't need a zeroed index_reg to access [rsi] for example.

    In asm source code, it doesn't matter what order you write things: [5 + rax + rsp + 15*4 + MY_ASSEMBLER_MACRO*2] works fine. (All the math on constants happens at assemble time, resulting in a single constant displacement.)

    The registers all have to be the same size as each other. And the same size as the mode you're in unless you use an alternate address-size, requiring an extra prefix byte. Narrow pointers are rarely useful outside of the x32 ABI (ILP32 in long mode) where you might want to ignore the top 32 bits of a register, e.g. instead of using movsxd to sign-extend a 32-bit possibly-negative offset in a register to 64-bit pointer width.

    If you want to use al as an array index, for example, you need to zero- or sign-extend it to pointer width. (Having the upper bits of rax already zeroed before messing around with byte registers is sometimes possible, and is a good way to accomplish this.)


    The limitations reflect what's encodeable in machine-code, as usual for assembly language. The scale factor is a 2-bit shift count. The ModRM (and optional SIB) bytes can encode up to 2 registers but not more, and don't have any modes that subtract registers, only add. Any register can be a base. Any register except ESP/RSP can be an index. See rbp not allowed as SIB base? for the encoding details, like why [rsp] always needs a SIB byte.

    Every possible subset of the general case is encodable, except ones using e/rsp*scale (obviously useless in "normal" code that always keeps a pointer to stack memory in esp).

    Normally, the code-size of the encodings is:

    ModRM is always present, and its bits signal whether a SIB is also present. Similar for disp8/disp32. Code-size exceptions:

    See Table 2-5 in Intel's ref manual, and the surrounding section, for the details on the special cases. (They're the same in 32 and 64bit mode. Adding RIP-relative encoding didn't conflict with any other encoding, even without a REX prefix.)

    For performance, it's typically not worth it to spend an extra instruction just to get smaller x86 machine code. On Intel CPUs with a uop cache, it's smaller than L1 I$, and a more precious resource. Minimizing fused-domain uops is typically more important.


    How they're used

    (This question was tagged MASM, but some of this answer talks about NASM's version of Intel syntax, especially where they differ for x86-64 RIP-relative addressing. AT&T syntax is not covered, but keep in mind that's just another syntax for the same machine code so the limitations are the same.)

    This table doesn't exactly match the hardware encodings of possible addressing modes, since I'm distinguishing between using a label (for e.g. global or static data) vs. using a small constant displacement. So I'm covering hardware addressing modes + linker support for symbols.

    (Note: usually you'd want movzx eax, byte [esi] or movsx when the source is a byte, but mov al, byte_src does assemble and is common in old code, merging into the low byte of EAX/RAX. See Why doesn't GCC use partial registers? and How to isolate byte and word array elements in a 64-bit register)

    If you have an int*, often you'd use the scale factor to scale an index by the array element size if you have an element index instead of a byte offset. (Prefer byte offsets or pointers to avoid indexed addressing modes for code-size reasons, and performance in some cases especially on Intel CPUs where it can hurt micro-fusion). But you can also do other things.
    If you have a pointer char array* in esi:


    Any and all of these addressing modes can be used with LEA to do integer math with a bonus of not affecting flags, regardless of whether it's a valid address. Using LEA on values that aren't addresses / pointers?

    [esi*4 + 10] is usually only useful with LEA (unless the displacement is a symbol, instead of a small constant). In machine code, there is no encoding for scaled-register alone, so [esi*4] has to assemble to [esi*4 + 0], with 4 bytes of zeros for a 32-bit displacement. It's still often worth it to copy+shift in one instruction instead of a shorter mov + shl, because usually uop throughput is more of a bottleneck than code size, especially on CPUs with a decoded-uop cache.


    You can specify segment-overrides like mov al, [fs:esi] (NASM syntax). A segment-override just adds a prefix-byte in front of the usual encoding. Everything else stays the same, with the same syntax.

    You can even use segment overrides with RIP-relative addressing. 32-bit absolute addressing takes one more byte to encode than RIP-relative, so mov eax, [fs:0] can most efficiently be encoded using a relative displacement that produces a known absolute address. i.e. choose rel32 so RIP+rel32 = 0. YASM will do this with mov ecx, [fs: rel 0], but NASM always uses disp32 absolute addressing, ignoring the rel specifier. I haven't tested MASM or gas.


    If the operand-size is ambiguous (e.g. in an instruction with an immediate and a memory operand), use byte / word / dword / qword to specify:

    mov       dword [rsi + 10], 123   ; NASM
    mov   dword ptr [rsi + 10], 123   ; MASM and GNU .intex_syntax noprefix
    
    movl      $123, 10(%rsi)         # GNU(AT&T): operand size from mnemonic suffix
    

    See the yasm docs for NASM-syntax effective addresses, and/or the wikipedia x86 entry's section on addressing modes.

    The wiki page says what's allowed in 16bit mode. Here's another "cheat sheet" for 32bit addressing modes.


    16-bit addressing modes

    16-bit address size can't use a SIB byte, so all the one and two register addressing modes are encoded into the single mod/rm byte. reg1 can be BX or BP, and reg2 can be SI or DI (or you can use any of those 4 registers by themself). Scaling is not available. 16-bit code is obsolete for a lot of reasons, including this one, and not worth learning if you don't have to. (Or not until after you learn 32 or 64-bit.)

    Note that the 16-bit restrictions apply any time you're using 16-bit address-size, including in 32-bit code with an address-size override prefix, so 16-bit LEA-math is highly restrictive. e.g. you can't do lea eax, [dx + cx*2] to do math and truncate + zero-extend. However lea eax, [edx + ecx*2] does set ax = dx + cx*2, because garbage in the upper bits of the source registers has no effect on the low 16.

    See also Differences between general purpose registers in 8086: [bx] works, [cx] doesn't? for a list of the available addressing modes.

    There's also a more detailed guide to addressing modes for 16-bit. You might want to read it to understand some fundamentals about how x86 CPUs use addresses because some of that hasn't changed for 32-bit mode.


    Related topics:

    Many of these are also linked above, but not all.