When I first tried reading x86 Assembly and ARM Assembly side by side, I got genuinely confused — the same operation looked completely different depending on which syntax convention I was looking at. It wasn’t until I broke instruction syntax down into its actual grammatical components — label, mnemonic, operands, and comment — that everything started clicking into a consistent pattern, regardless of which architecture or assembler I was reading.
Table of Contents
- The General Structure of an Assembly Instruction
- Breaking Down Each Component
- Intel Syntax vs. AT&T Syntax
- x86/x86-64 Syntax Examples
- ARM Syntax Examples
- Instruction Syntax Grammar (Mermaid Diagram)
- Operand Order and Direction
- Instruction Prefixes and Suffixes
- Whitespace, Case Sensitivity, and Line Structure
- Common Instruction Formats Table
- Performance Considerations
- Comparison: Syntax Styles Across Assemblers
- Best Practices
- Common Mistakes
- FAQs
- Summary and Key Takeaways
- References
The General Structure of an Assembly Instruction
Nearly every Assembly instruction, regardless of architecture or assembler, follows this general template:
[label:] mnemonic [operand1[, operand2[, operand3]]] [; comment]
Every part except the mnemonic is optional. A bare NOP is a complete, valid instruction on its own; a fully decorated line might have a label, a three-operand instruction, and a trailing comment all on the same line.
Breaking Down Each Component
| Component | Role | Optional? |
|---|---|---|
| Label | Names the address of this instruction | Yes |
| Mnemonic | The operation to perform | No — required |
| Operand(s) | Data or references the operation acts on | Depends on the instruction |
| Comment | Human-readable explanation, ignored by assembler | Yes |
loop_start: ADD EAX, EBX, 4 ; label, mnemonic, operands, comment
(Note: x86 ADD only takes two operands; this illustrative line is closer to how a three-operand ARM instruction would look. Real x86 syntax is shown in the next section.)
Intel Syntax vs. AT&T Syntax
x86 Assembly has two competing syntax conventions, and confusing them is one of the most common early mistakes.
| Feature | Intel Syntax (NASM, MASM) | AT&T Syntax (GAS, default on Linux) |
|---|---|---|
| Operand order | dest, src | src, dest |
| Register prefix | None (EAX) | % prefix (%eax) |
| Immediate prefix | None (5) | $ prefix ($5) |
| Memory operand | [EBX+4] | 4(%ebx) |
| Instruction size suffix | Implicit from operand size | Explicit suffix: b, w, l, q |
Intel syntax example:
MOV EAX, [EBX+4]
Equivalent AT&T syntax:
movl 4(%ebx), %eax
Both lines do exactly the same thing: load the 32-bit value at address EBX + 4 into EAX. The direction of data flow is just written in opposite order.
x86/x86-64 Syntax Examples
; NASM (Intel syntax)
start:
MOV EAX, 10 ; two operands: dest, src
ADD EAX, EBX ; register-to-register
MOV [result], EAX ; register-to-memory (direct addressing)
CMP EAX, 0 ; comparison, sets flags
JE end_label ; single operand: jump target
NOP ; zero operands
end_label:
RET
# GAS (AT&T syntax)
start:
movl $10, %eax # source first, destination second
addl %ebx, %eax
movl %eax, result
cmpl $0, %eax
je end_label
nop
end_label:
ret
ARM Syntax Examples
ARM syntax (used consistently by both the GNU and ARM’s own toolchains, unlike x86’s split) generally follows a uniform format:
[label:] mnemonic{cond}{S} Rd, Rn, Operand2 [; comment]
Where {cond} is an optional condition code (like EQ, NE, GT), and {S} optionally indicates the instruction should update the status flags.
loop:
ADDS R0, R1, R2 @ R0 = R1 + R2, updates flags (S suffix)
CMP R0, #10
BLE loop @ Branch if Less than or Equal
MOVEQ R3, #1 @ Conditional MOV: only if EQ flag is set
Here, ADDS combines the mnemonic ADD with the S suffix (update flags), and MOVEQ combines MOV with the EQ condition code — a distinctly ARM feature where nearly every instruction can be conditionally executed.
Instruction Syntax Grammar
flowchart TD
A[Start of Line] --> B{Optional Label?}
B -->|Yes| C[label:]
B -->|No| D[Mnemonic]
C --> D
D --> E{Operands Required?}
E -->|Zero| F[No Operand Field]
E -->|One| G[Single Operand]
E -->|Two or Three| H[Comma-Separated Operand List]
F --> I{Trailing Comment?}
G --> I
H --> I
I -->|Yes| J[; or # or @ Comment Text]
I -->|No| K[End of Line]
J --> K
Operand Order and Direction
This is the single most common source of confusion when switching between syntaxes:
| Syntax | Order | Example | Meaning |
|---|---|---|---|
| Intel | destination, source | MOV EAX, EBX | EBX’s value goes into EAX |
| AT&T | source, destination | movl %ebx, %eax | Same operation, reversed order |
| ARM | destination, source(s) | ADD R0, R1, R2 | R0 = R1 + R2 |
Instruction Prefixes and Suffixes
- x86 prefixes:
LOCK(atomic bus lock),REP/REPE/REPNE(string instruction repetition), segment override prefixes (CS:,DS:). - x86-64 REX prefix: enables access to 64-bit registers and the extended register set (
R8–R15). - AT&T size suffixes:
b(byte),w(word),l(long/32-bit),q(quad/64-bit) — appended to the mnemonic since AT&T syntax doesn’t always infer size from operands. - ARM condition codes:
EQ,NE,GT,LT,GE,LE, etc., appended directly to the mnemonic to make execution conditional. - ARM
Ssuffix: appended to update the condition flags as a side effect of the instruction.
Whitespace, Case Sensitivity, and Line Structure
- Whitespace (spaces/tabs) between fields is generally flexible — most assemblers don’t care about exact alignment, though consistent indentation greatly improves readability.
- Case sensitivity varies: NASM and GAS are case-sensitive for labels by default but often case-insensitive for mnemonics; always check your specific assembler’s documentation.
- Each instruction typically occupies one line, though line-continuation and multi-instruction-per-line syntax (separated by
;in some assemblers, though this conflicts with NASM’s comment character) exist in select toolchains.
Common Instruction Formats Table
| Format | Example (x86 Intel) | Example (ARM) |
|---|---|---|
| Zero operand | NOP | NOP |
| One operand | INC EAX | PUSH {R0} |
| Two operand | MOV EAX, EBX | — (rare, ARM favors three-operand) |
| Three operand | — (rare on classic x86) | ADD R0, R1, R2 |
| Memory operand | MOV EAX, [EBX+4] | LDR R0, [R1, #4] |
| Immediate operand | MOV EAX, 5 | MOV R0, #5 |
Performance Considerations
Syntax choice itself (Intel vs. AT&T, for instance) has no effect on performance — both compile down to identical machine code for the same logical instruction, since syntax is purely a textual representation chosen by the assembler front-end. What does matter for performance is the actual instruction and operand types chosen (register vs. memory operand, immediate vs. computed value), which is a semantic decision independent of which syntax convention you’re reading or writing in.
Comparison: Syntax Styles Across Assemblers
| Assembler | Default Syntax | Operand Order | Register Notation |
|---|---|---|---|
| NASM | Intel | dest, src | EAX |
| MASM | Intel | dest, src | EAX |
| GAS (default) | AT&T | src, dest | %eax |
GAS (.intel_syntax noprefix) | Intel | dest, src | eax |
ARM as/armasm | ARM (unified) | dest, src(s) | R0 |
A Complete Annotated Program
Seeing the full instruction grammar applied across an entire small program makes the pattern much easier to internalize. Here’s a complete NASM program that prints “Hello, World!” on Linux x86-64, with every line following the [label:] mnemonic [operands] [; comment] template:
SECTION .data
msg DB "Hello, World!", 10 ; string literal + newline
msg_len EQU $ - msg ; EQU: computed constant, not an instruction
SECTION .text
GLOBAL _start ; directive, not an instruction — no operand comma needed
_start: ; label with no instruction on the same line
MOV RAX, 1 ; syscall number for sys_write
MOV RDI, 1 ; file descriptor: stdout
MOV RSI, msg ; pointer to the string
MOV RDX, msg_len ; length of the string
SYSCALL ; zero-operand instruction
MOV RAX, 60 ; syscall number for sys_exit
XOR RDI, RDI ; exit code 0 (XOR is a common idiom for "set to zero")
SYSCALL
Every non-blank, non-directive line here fits the grammar exactly: an optional label, a required mnemonic, zero to three operands, and an optional trailing comment.
Pseudo-Instructions That Look Like Real Instructions
Some “instructions” you’ll encounter aren’t real opcodes at all — they’re assembler shorthand that expands into one or more actual instructions, similar in spirit to macros but built into the assembler itself.
| Pseudo-Instruction | Architecture | Expands To |
|---|---|---|
LDR Rd, =value | ARM | Loads a 32-bit constant via a literal pool when it can’t fit in a normal immediate field |
PUSH/POP (with multiple registers) | ARM | {R0-R3} expands into multiple internal store/load operations |
MOVZX/MOVSX | x86 | Zero/sign-extending move, a genuine opcode but often explained as “MOV plus extension” |
CALL/RET | x86 | Genuine opcodes, but conceptually “PUSH address + JMP” and “POP address + JMP” respectively |
Recognizing these helps when reading architecture reference manuals, since they’ll sometimes explicitly note “this is a pseudo-instruction” rather than a true single-opcode operation — useful context when you’re trying to predict exactly how many bytes or cycles something will cost.
How Assemblers Report Syntax Errors
Understanding the grammar also makes assembler error messages far easier to interpret. A typical NASM error like:
error: parser: instruction expected
almost always means the assembler expected a mnemonic where it found something else — often a misplaced comma, a missing colon after a label, or a stray operand left over from editing. Recognizing which grammatical slot (label, mnemonic, operand, comment) the parser was in when it failed makes debugging syntax errors dramatically faster than guessing.
Syntax Consistency Across a Growing Codebase
As an Assembly project grows past a single file, syntax consistency stops being a stylistic preference and starts being a practical necessity. A shared style guide — even an informal one — covering indentation width, capitalization of mnemonics and registers, comment placement, and label naming pays off enormously once more than one file (or more than one contributor) is involved. I’ve found it useful to settle these choices explicitly at the very start of a project rather than letting each file drift into its own conventions, since reformatting Assembly retroactively is far more error-prone than reformatting a high-level language, precisely because whitespace and alignment carry no semantic meaning the assembler will catch if you get it wrong — an accidentally deleted operand or misplaced comma can silently change behavior rather than throwing an obvious error.
Best Practices
- Pick one syntax convention per project and stay consistent — mixing Intel and AT&T syntax in the same codebase invites confusion and bugs.
- When reading unfamiliar Assembly, first identify the syntax style (look for
%register prefixes as a quick AT&T indicator) before interpreting operand order. - Use consistent indentation for mnemonics and operands to keep large files readable.
- Comment non-obvious operand choices, especially when register reuse makes intent unclear.
Converting Between Syntaxes in Practice
Because Intel and AT&T syntax coexist in real-world tooling, it’s worth knowing how to move between them without introducing bugs. GDB, by default on Linux, disassembles in AT&T syntax, but this can be switched:
(gdb) set disassembly-flavor intel
(gdb) disassemble
Similarly, objdump supports a -M intel flag:
objdump -d -M intel program.o
I switch to Intel-flavor output constantly when debugging, simply because I find mov eax, [ebx+4] easier to reason about at a glance than movl 4(%ebx), %eax — but knowing both is essential, since a lot of existing Linux tooling, inline assembly in C (asm volatile(...)), and GCC-generated .s files default to AT&T syntax.
Inline Assembly Syntax: A Related but Distinct Grammar
It’s worth flagging that when Assembly is embedded inside a C/C++ file via GCC’s extended inline assembly, there’s an additional layer of syntax on top of the underlying AT&T grammar — output/input operand constraints and clobber lists:
int result;
__asm__ (
"addl %%ebx, %%eax"
: "=a" (result) // output operand
: "a" (10), "b" (20) // input operands
: // clobbered registers
);
This isn’t a different Assembly instruction syntax so much as a C-level wrapper describing how C variables map onto the registers referenced inside the Assembly string — but it’s a common source of confusion for people learning standalone Assembly syntax first and inline Assembly second, since the underlying addl %ebx, %eax line is still ordinary AT&T syntax once you strip away the surrounding C constraint syntax.
Common Mistakes
- Assuming operand order is universal — swapping source and destination is one of the most common bugs when porting code between Intel and AT&T syntax.
- Forgetting AT&T’s
%register prefix and$immediate prefix, causing assembly errors. - Missing size suffixes in AT&T syntax (
movlvs.mov) when the operand size can’t be inferred. - Misreading ARM condition codes and
Ssuffixes as separate instructions rather than modifiers on the base mnemonic.
The Grammar Is Smaller Than It Looks
Looking back at everything covered here, I think the most reassuring realization for anyone new to Assembly is that the actual grammar — label, mnemonic, operands, comment — is genuinely small. What makes Assembly feel complicated at first isn’t the sentence structure; it’s the sheer number of mnemonics, operand combinations, and architecture-specific quirks layered on top of that simple four-part template. Once the underlying grammar stops requiring conscious thought, reading unfamiliar Assembly — even in a syntax or architecture you haven’t used before — becomes a matter of looking up unfamiliar mnemonics rather than re-learning how to parse a line at all.
FAQs
Which syntax should I learn first, Intel or AT&T? Intel syntax (via NASM) is generally considered more approachable for beginners due to its more intuitive dest, src order and cleaner memory operand notation; AT&T is worth learning afterward since it’s the Linux/GCC toolchain default.
Does ARM have an equivalent Intel/AT&T split? Not really — ARM assembly syntax is largely unified across GNU and ARM’s own toolchains, though minor directive differences exist.
Can I mix syntaxes in one file? Generally no, though GAS supports a .intel_syntax noprefix directive to switch modes within a single file if truly necessary.
Why do ARM instructions often have three operands while x86 mostly has two? This reflects ARM’s RISC “load-store” design philosophy, where the destination register can differ from either source operand without needing an extra MOV, unlike x86’s traditional two-operand, destructive-operation style.
Summary and Key Takeaways
- Every Assembly instruction generally follows:
[label:] mnemonic [operands] [; comment]. - x86 has two competing syntaxes — Intel (
dest, src) and AT&T (src, dest) — that produce identical machine code but read very differently. - ARM syntax is largely unified, favors a three-operand format, and supports rich condition codes and flag-update suffixes directly on the mnemonic.
- Syntax choice is a textual convention with zero performance impact; what matters for performance is the underlying instruction and operand type chosen.
- Recognizing these structural patterns makes it far easier to read, write, and port Assembly code across different tools and architectures.
References
- Intel® 64 and IA-32 Architectures Software Developer’s Manual — intel.com/sdm
- NASM Documentation — nasm.us/doc
- GNU Assembler (GAS) Manual — sourceware.org/binutils/docs/as
- ARM Architecture Reference Manual — developer.arm.com/documentation
