Case Study 6.2: NASM vs. GAS Syntax Comparison
The same program in Intel and AT&T syntax, side by side
Overview
GAS (GNU Assembler) uses AT&T syntax by default, which you'll encounter whenever you read GCC's assembly output (gcc -S), system library disassembly, or Linux kernel source. NASM uses Intel syntax. Since both are in widespread use, a practical assembly programmer needs to read both fluently.
This case study presents the same programs in both syntaxes side by side, annotating every difference. By the end, reading either syntax should be straightforward.
The Core Differences
| Feature | NASM (Intel) | GAS (AT&T) |
|---|---|---|
| Operand order | Destination first | Source first |
| Register names | rax, rbx |
%rax, %rbx |
| Immediate values | 42, 0xFF |
$42`, `$0xFF |
| Memory operands | [rax], [rbp-8] |
(%rax), -8(%rbp) |
| Size suffixes | Implicit (from register) | b/w/l/q on instruction |
| Sections | section .text |
.section .text or .text |
| Data bytes | db, dw, dd, dq |
.byte, .short/.word, .long, .quad |
| String data | db "string" |
.ascii "string" or .string "string" (with null) |
| Constants | EQU |
.equ or = |
| Comments | ; comment |
# comment (most) or // comment |
| Global symbol | global _start |
.globl _start |
| SIB addressing | [rbx + rcx*4 + 8] |
8(%rbx, %rcx, 4) |
Side-by-Side: Hello World
NASM (Intel syntax):
; hello_nasm.asm
section .data
msg db "Hello, World!", 10 ; string with newline
msglen equ $ - msg ; length = 14
section .text
global _start
_start:
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
mov rsi, msg ; buffer
mov rdx, msglen ; length
syscall
mov rax, 60 ; sys_exit
xor rdi, rdi ; exit code 0
syscall
GAS (AT&T syntax):
# hello_gas.s
.section .data
msg:
.ascii "Hello, World!\n" # no automatic length
.equ msglen, . - msg # '.' is current position (like NASM's $)
.section .text
.globl _start
_start:
movq $1, %rax # sys_write; 'q' suffix = qword (64-bit)
movq $1, %rdi # stdout
movq $msg, %rsi # buffer address; $ before label = address
movq $msglen, %rdx # length
syscall
movq $60, %rax # sys_exit
xorq %rdi, %rdi # exit code 0
syscall
Key differences in this example:
-
**
msgvs$msg`:** In NASM, `msg` in an instruction is automatically the address. In GAS, you must write `$msgto mean "the address of msg"; without the$, it would mean "the value at address msg" (a memory dereference). -
.asciivsdb: NASM'sdb "string"defines bytes directly; GAS's.asciiis the equivalent but does not add a null terminator. GAS's.string "..."or.asciz "..."adds a null terminator. There's no NASM equivalent for.asciinot including a newline — NASM would usedb "string", 10. -
equvs.equ: NASM useslabel equ value; GAS uses.equ label, valueorlabel = value. -
movqsuffix: GAS appendsb,w,l,qto instructions to specify operand size (byte, word, long/dword, quad/qword). In NASM, the size is usually inferred from the register name. -
Comment style: NASM uses
;; GAS typically uses#(GCC output) or//.
Side-by-Side: A Simple Function
NASM (Intel syntax):
; max_of_two.asm
; long max_of_two(long a, long b)
; Returns: max(a, b)
; Args: rdi=a, rsi=b
section .text
global max_of_two
max_of_two:
push rbp
mov rbp, rsp
cmp rdi, rsi ; compare a and b
jge .a_wins ; if a >= b, return a
mov rax, rsi ; else return b
jmp .done
.a_wins:
mov rax, rdi ; return a
.done:
pop rbp
ret
GAS (AT&T syntax, GCC-generated style):
# max_of_two.s (equivalent, as GCC would generate it with -O0)
.globl max_of_two
.type max_of_two, @function
max_of_two:
pushq %rbp # push rbp (64-bit push)
movq %rsp, %rbp # rbp = rsp (NOTE: source first in AT&T!)
cmpq %rsi, %rdi # CAREFUL: AT&T CMP is "cmp src, dest"
# "cmpq %rsi, %rdi" means: compare rdi with rsi
# i.e., compute rdi - rsi and set flags
# This is confusing! Intel CMP is "cmp rdi, rsi"
jge .L1 # GCC uses .L labels, not .local labels
movq %rsi, %rax # rax = rsi (b wins)
jmp .L2
.L1:
movq %rdi, %rax # rax = rdi (a wins)
.L2:
popq %rbp
ret
.size max_of_two, .-max_of_two
The confusing part: AT&T's cmpq %rsi, %rdi means "compute rdi - rsi" (the destination is the right operand in AT&T, and CMP sets flags based on dest - src). So cmpq %rsi, %rdi with jge means "jump if rdi >= rsi" — which matches the NASM version cmp rdi, rsi with jge. Both say "jump if a >= b". But the AT&T operand order makes cmp %rsi, %rdi look like "compare rsi with rdi" when it actually computes "rdi - rsi". This is the source of more confusion than any other single AT&T syntax quirk.
Side-by-Side: Memory Addressing
NASM:
; Various addressing modes
mov rax, [rbp - 8] ; base + displacement
mov rax, [rdi + rcx*8] ; base + index*scale
mov rax, [rbp + rdi*4 - 16] ; base + index*scale + displacement
GAS:
# Same addressing modes in AT&T syntax
movq -8(%rbp), %rax # displacement(base)
movq (%rdi, %rcx, 8), %rax # (base, index, scale)
movq -16(%rbp, %rdi, 4), %rax # displacement(base, index, scale)
AT&T addressing syntax is displacement(base, index, scale). The order of elements within the parentheses is different from NASM's [base + index*scale + displacement].
Reading GCC Output: A Practical Guide
When you run gcc -S foo.c, you get AT&T syntax. Here's how to read it quickly:
Step 1: Recognize register names. Strip the % prefix. %rax → rax, %rdi → rdi, etc.
Step 2: Recognize immediates. Strip the $` prefix from numeric values. `$42 → 42, $0xFF → 0xFF.
Step 3: Flip operand order. movq %rsi, %rax means mov rax, rsi (destination last in AT&T).
Step 4: Parse memory addressing. displacement(base, index, scale) → [base + index*scale + displacement].
Step 5: Map instruction suffixes. movq = 64-bit move, movl = 32-bit, movw = 16-bit, movb = 8-bit. In NASM, these are inferred from operand sizes.
Step 6: Check for CMP direction. cmpq %rcx, %rax means "compute rax - rcx and set flags" — equivalent to NASM's cmp rax, rcx.
Reading objdump Output
By default, objdump -d uses AT&T syntax on Linux. Add -M intel to get Intel syntax:
objdump -d my_program # AT&T (default)
objdump -d -M intel my_program # Intel (easier to read if you know NASM)
Set the default permanently in your .gdbinit:
set disassembly-flavor intel
And in your shell config for objdump:
alias objdump='objdump -M intel'
GAS Directives Reference
When reading GCC output or GAS source, you'll encounter these directives:
| GAS Directive | NASM Equivalent | Meaning |
|---|---|---|
.byte N |
db N |
Define 1 byte |
.short N / .word N |
dw N |
Define 2 bytes |
.long N / .int N |
dd N |
Define 4 bytes |
.quad N |
dq N |
Define 8 bytes |
.ascii "str" |
db "str" |
Define string bytes (no null terminator) |
.asciz "str" / .string "str" |
db "str", 0 |
Define null-terminated string |
.zero N |
times N db 0 |
N zero bytes |
.fill N, size, val |
times N db val (for size=1) |
Fill N items |
.align N |
align N |
Align to N-byte boundary |
.globl sym |
global sym |
Make symbol global |
.type sym, @function |
(no equivalent) | Mark symbol as function |
.size sym, expr |
(no equivalent) | Set symbol size |
.section .text |
section .text |
Switch section |
.text |
section .text |
Short form |
.data |
section .data |
Short form |
.bss |
section .bss |
Short form |
.equ name, val |
name equ val |
Define constant |
Summary
The syntax differences between NASM and GAS are mechanical, not conceptual. The same machine code, the same instructions, the same registers — just different text representations. With practice, reading either becomes automatic. The key rules:
- AT&T has
%on registers,$on immediates, source before destination - Intel has no sigils, destination before source
- AT&T memory:
disp(base, index, scale)— Intel:[base + index*scale + disp] - AT&T CMP:
cmp src, dest(flags = dest - src) — confusing, memorize it separately objdump -M intelis your friend for quick translation