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:

  1. **msg vs $msg`:** In NASM, `msg` in an instruction is automatically the address. In GAS, you must write `$msg to mean "the address of msg"; without the $, it would mean "the value at address msg" (a memory dereference).

  2. .ascii vs db: NASM's db "string" defines bytes directly; GAS's .ascii is the equivalent but does not add a null terminator. GAS's .string "..." or .asciz "..." adds a null terminator. There's no NASM equivalent for .ascii not including a newline — NASM would use db "string", 10.

  3. equ vs .equ: NASM uses label equ value; GAS uses .equ label, value or label = value.

  4. movq suffix: GAS appends b, w, l, q to instructions to specify operand size (byte, word, long/dword, quad/qword). In NASM, the size is usually inferred from the register name.

  5. 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. %raxrax, %rdirdi, etc.

Step 2: Recognize immediates. Strip the $` prefix from numeric values. `$4242, $0xFF0xFF.

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:

  1. AT&T has % on registers, $ on immediates, source before destination
  2. Intel has no sigils, destination before source
  3. AT&T memory: disp(base, index, scale) — Intel: [base + index*scale + disp]
  4. AT&T CMP: cmp src, dest (flags = dest - src) — confusing, memorize it separately
  5. objdump -M intel is your friend for quick translation