Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

GNU Assembler (GAS)

The GNU Assembler (as) is the default backend assembler for the GCC toolchain. It translates assembly language source files into object files (ELF on Linux). GAS uses AT&T syntax by default and supports multiple architectures including x86, x86-64, ARM, AArch64, RISC-V, and MIPS.

Introduction

Every C or Rust program compiled with GCC or LLVM ultimately passes through an assembler. On Linux systems, as (often invoked behind the scenes by gcc -S output) is the standard assembler. Understanding GAS is essential for:

  • Reading compiler-generated assembly (gcc -S -O2)
  • Writing inline assembly in C/Rust
  • Bootstrapping toolchains
  • Kernel development and early boot code
  • Reverse engineering and binary analysis

AT&T vs Intel Syntax

The most immediate difference a newcomer notices is syntax style. GAS defaults to AT&T syntax; NASM and MASM use Intel syntax.

FeatureAT&T (GAS default)Intel (NASM/YASM)
Operand ordersrc, dstdst, src
Register prefix%raxrax
Immediate prefix$4242
Memory reference(%rax,%rcx,4)[rax+rcx*4]
Size suffixmovq, movl, movbmov QWORD, DWORD, etc.
Section directive.section .textsection .text

GAS can switch to Intel syntax with .intel_syntax noprefix at the top of the file.

Command Invocation

# Assemble to object file
as -o hello.o hello.s

# Assemble with debug info (DWARF)
as --gdwarf-2 -o hello.o hello.s

# 64-bit mode (x86-64)
as --64 -o hello.o hello.s

# 32-bit mode
as --32 -o hello.o hello.s

# List file (addresses + generated bytes)
as -a=hello.lst -o hello.o hello.s

# Define a preprocessor symbol
as --defsym DEBUG=1 -o hello.o hello.s

# Include path for .include directives
as -I /usr/include -o hello.o hello.s

Typical workflow with GCC:

# Compile C to assembly
gcc -S -O2 -o hello.s hello.c

# Assemble
as -o hello.o hello.s

# Or simply let GCC do both:
gcc -c -o hello.o hello.s

Sections

GAS uses .section (or shorthand directives) to place code and data into specific ELF sections.

Standard ELF Sections

.section .text          # Executable code
.section .rodata        # Read-only data (constants, strings)
.section .data          # Initialized read-write data
.section .bss           # Uninitialized data (zero-filled at load)
.section .note.GNU-stack,"",@progbits  # Stack executability note

Shorthand Directives

.text                   # Switch to .text section
.data                   # Switch to .data section
.bss                    # Switch to .bss section

Custom Sections

.section .my_section,"awx",@progbits
    .align 16
my_data:
    .long 0xDEADBEEF

Section flags: a (allocatable), w (writable), x (executable), M (merge), S (strings).

Data Directives

.byte   0x42                    # 1 byte
.value  0x1234                  # 2 bytes (same as .short)
.long   0x12345678              # 4 bytes (same as .int)
.quad   0x123456789ABCDEF0      # 8 bytes
.float  3.14                    # 4-byte IEEE 754
.double 3.141592653589793       # 8-byte IEEE 754
.string "Hello, World\0"       # Null-terminated string
.ascii  "Hello"                 # No null terminator
.asciz  "Hello"                 # With null terminator

.fill   256, 4, 0              # 256 × 4 bytes, filled with 0
.zero   1024                    # 1024 bytes of zero
.space  512                     # 512 bytes of space

Alignment

.align 4                        # Align to 4-byte boundary (power of 2)
.balign 16                      # Align to exactly 16 bytes
.p2align 5                      # Align to 2^5 = 32 bytes

Symbol and Label Management

.global main                    # Export symbol (ELF global)
.globl  main                    # Synonym for .global
.local  helper_func             # ELF local (not exported)
.weak   optional_hook           # Weak symbol

.equ    BUFFER_SIZE, 4096       # Constant definition
.set    MAX_RETRIES, 3          # Same as .equ

main:
    pushq   %rbp
    movq    %rsp, %rbp
    # ...
    ret

Symbol Visibility

.hidden internal_helper         # Not exported, not preemptible
.protected api_function         # Exported but not preemptible
.internal shared_state          # ELF internal visibility

x86-64 Instruction Examples

Function Prologue/Epilogue (System V AMD64 ABI)

.text
.global sum_array
.type   sum_array, @function

sum_array:
    # rdi = pointer to array
    # rsi = count
    pushq   %rbp
    movq    %rsp, %rbp
    xorq    %rax, %rax          # accumulator = 0
    testq   %rsi, %rsi
    jz      .Ldone
.Lloop:
    addq    (%rdi), %rax
    addq    $8, %rdi
    decq    %rsi
    jnz     .Lloop
.Ldone:
    popq    %rbp
    ret
.size   sum_array, .-sum_array

SIMD (SSE2) Example

.text
.global vec_add_f64
.type   vec_add_f64, @function

vec_add_f64:
    # rdi = a[], rsi = b[], rdx = out[], rcx = count
    testq   %rcx, %rcx
    jz      .Lret
.Lvloop:
    movupd  (%rdi), %xmm0
    movupd  (%rsi), %xmm1
    addpd   %xmm1, %xmm0
    movupd  %xmm0, (%rdx)
    addq    $16, %rdi
    addq    $16, %rsi
    addq    $16, %rdx
    subq    $2, %rcx
    jg      .Lvloop
.Lret:
    ret
.size   vec_add_f64, .-vec_add_f64

System Call (Linux x86-64)

.data
msg:    .ascii  "Hello\n"
.equ    msg_len, . - msg

.text
.global _start
_start:
    movq    $1, %rax            # sys_write
    movq    $1, %rdi            # fd = stdout
    leaq    msg(%rip), %rsi     # buffer
    movq    $msg_len, %rdx      # length
    syscall

    movq    $60, %rax           # sys_exit
    xorq    %rdi, %rdi          # status = 0
    syscall

Build and run:

as --64 -o hello.o hello.s
ld -o hello hello.o
./hello
# Output: Hello

ARM / AArch64 Examples

AArch64 Function

.text
.global factorial
.type   factorial, %function

factorial:
    # x0 = n
    cmp     x0, #1
    b.le    .Lbase
    stp     x29, x30, [sp, #-16]!
    mov     x29, sp
    sub     x0, x0, #1
    bl      factorial
    ldp     x29, x30, [sp], #16
    # x0 already holds result of factorial(n-1)
    mul     x0, x0, x0          # simplified; real code saves original n
    ret
.Lbase:
    mov     x0, #1
    ret
.size   factorial, .-factorial

AArch64 Linux Syscall

.data
msg:    .ascii  "Hello from AArch64\n"
.equ    msg_len, . - msg

.text
.global _start
_start:
    mov     x8, #64             # sys_write
    mov     x0, #1              # stdout
    adr     x1, msg
    mov     x2, #msg_len
    svc     #0

    mov     x8, #93             # sys_exit
    mov     x0, #0
    svc     #0

GAS vs NASM

AspectGAS (as)NASM
Syntax defaultAT&TIntel
Macro system.macro / .altmacro%macro / %define
Object formatELF (native), COFF, Mach-OELF, COFF, Mach-O, Win32/Win64
Section naming.section .text / .textsection .text
Conditional assembly.if, .ifdef%if, %ifdef
Included inGNU binutilsStandalone
Package (Debian/Ubuntu)binutilsnasm
Debug infoDWARF (.debug_* sections)DWARF (-g)
Architecture supportMulti-arch (x86, ARM, RISC-V, …)x86 only

NASM Equivalent of the Hello Example

section .data
    msg     db  "Hello", 10
    msg_len equ $ - msg

section .text
    global _start

_start:
    mov     rax, 1          ; sys_write
    mov     rdi, 1          ; stdout
    lea     rsi, [rel msg]
    mov     rdx, msg_len
    syscall

    mov     rax, 60         ; sys_exit
    xor     rdi, rdi
    syscall
nasm -f elf64 -o hello_nasm.o hello_nasm.asm
ld -o hello_nasm hello_nasm.o

Assembler Macros

GAS has its own macro system, distinct from the C preprocessor.

.macro push_regs regs:vararg
    .ifnb \regs
    pushq   \regs
    .endif
.endm

.macro save_callee_saved
    pushq   %rbx
    pushq   %r12
    pushq   %r13
    pushq   %r14
    pushq   %r15
.endm

.macro restore_callee_saved
    popq    %r15
    popq    %r14
    popq    %r13
    popq    %r12
    popq    %rbx
.endm

Local Labels

Labels prefixed with .L are local and do not appear in the symbol table:

.Lretry:
    # ...
    jnz     .Lretry

Numeric labels (1:, 2:) with forward/backward references (1f, 1b):

1:
    cmp     %rax, $0
    je      2f
    dec     %rax
    jmp     1b
2:
    ret

Linker Script Interaction

GAS symbols are consumed by the linker. Special section attributes work with linker scripts:

.section .init.text,"ax",@progbits
.global early_init
early_init:
    # Runs during kernel early boot
    ret

.section .init.data,"aw",@progbits
init_msg:
    .asciz "Initializing...\n"

Using the C Preprocessor with GAS

GCC can pipe through the C preprocessor before assembling:

# .S files (uppercase) are preprocessed; .s files (lowercase) are not
gcc -c -o entry.o entry.S
/* entry.S — preprocessed assembly */
#include <asm/unistd.h>
#include <sys/syscall.h>

.text
.global _start
_start:
    movq    $__NR_write, %rax
    movq    $STDOUT_FILENO, %rdi
    leaq    msg(%rip), %rsi
    movq    $6, %rdx
    syscall

Common Debugging Tips

# Disassemble an object file
objdump -d hello.o

# Show all sections
objdump -h hello.o

# Show symbol table
nm hello.o

# Show relocations
objdump -r hello.o

# Read ELF headers
readelf -a hello.o

Pseudo-Operations Reference

DirectivePurpose
.fileSet source file name for debug info
.locSource line number mapping
.type sym, @functionMark symbol as function
.size sym, .-symSet symbol size for debuggers
.alignAlign to power-of-2 boundary
.balignAlign to exact byte boundary
.orgSet location counter
.fillRepeat fill pattern
.incbinInclude raw binary file
.identAdd identification string to .comment
.pushsectionSave and switch section
.popsectionRestore previously saved section

RISC-V Examples

RISC-V Function (RV64GC)

.text
.global fib
.type   fib, @function

fib:
    # a0 = n
    li      t0, 2
    blt     a0, t0, .Lbase
    addi    sp, sp, -16
    sd      ra, 8(sp)
    sd      s0, 0(sp)
    mv      s0, a0          # save n
    addi    a0, a0, -1      # fib(n-1)
    call    fib
    mv      t1, a0          # t1 = fib(n-1)
    addi    a0, s0, -2      # fib(n-2)
    call    fib
    add     a0, a0, t1      # fib(n-1) + fib(n-2)
    ld      ra, 8(sp)
    ld      s0, 0(sp)
    addi    sp, sp, 16
    ret
.Lbase:
    ret                     # fib(0) = 0, fib(1) = 1
.size   fib, .-fib

RISC-V Linux Syscall (RV64)

.data
msg:    .ascii  "Hello from RISC-V\n"
.equ    msg_len, . - msg

.text
.global _start
_start:
    li      a7, 64          # sys_write
    li      a0, 1           # stdout
    la      a1, msg
    li      a2, msg_len
    ecall

    li      a7, 93          # sys_exit
    li      a0, 0           # status
    ecall
# Cross-compile for RISC-V
riscv64-linux-gnu-as -o hello.o hello.s
riscv64-linux-gnu-ld -o hello hello.o
qemu-riscv64 ./hello
# Output: Hello from RISC-V

RISC-V Vector Extension (RVV 1.0)

.text
.global vec_add_f64
.type   vec_add_f64, @function

vec_add_f64:
    # a0 = a[], a1 = b[], a2 = out[], a3 = count
    vsetvli t0, a3, e64, m1  # Set vector: 64-bit elements
    beqz    t0, .Ldone
.Lloop:
    vle64.v v0, (a0)         # Load a[]
    vle64.v v1, (a1)         # Load b[]
    vfadd.vv v0, v0, v1      # v0 = a + b
    vse64.v v0, (a2)         # Store out[]
    slli    t1, t0, 3        # t0 * 8 bytes
    add     a0, a0, t1
    add     a1, a1, t1
    add     a2, a2, t1
    sub     a3, a3, t0
    vsetvli t0, a3, e64, m1
    bnez    t0, .Lloop
.Ldone:
    ret
.size   vec_add_f64, .-vec_add_f64

Inline Assembly in C

GCC inline assembly allows embedding assembly within C code:

Basic Inline Assembly

/* Simple inline assembly */
static inline uint64_t rdtsc(void) {
    uint32_t lo, hi;
    __asm__ __volatile__ (
        "rdtsc"
        : "=a" (lo), "=d" (hi)  /* outputs */
        :                       /* inputs */
        :                       /* clobbers */
    );
    return ((uint64_t)hi << 32) | lo;
}

/* Memory barrier */
static inline void barrier(void) {
    __asm__ __volatile__ ("" : : : "memory");
}

/* System call wrapper */
static inline long syscall3(long nr, long a1, long a2, long a3) {
    long ret;
    __asm__ __volatile__ (
        "syscall"
        : "=a" (ret)
        : "a" (nr), "D" (a1), "S" (a2), "d" (a3)
        : "rcx", "r11", "memory"
    );
    return ret;
}

Extended Inline Assembly

/* Atomic compare-and-swap */
static inline int cas(int *ptr, int old, int new) {
    int result;
    __asm__ __volatile__ (
        "lock cmpxchgl %2, %1"
        : "=a" (result), "+m" (*ptr)
        : "r" (new), "0" (old)
        : "memory"
    );
    return result;
}

/* CPUID instruction */
static inline void cpuid(uint32_t leaf, uint32_t *eax, uint32_t *ebx,
                         uint32_t *ecx, uint32_t *edx) {
    __asm__ __volatile__ (
        "cpuid"
        : "=a" (*eax), "=b" (*ebx), "=c" (*ecx), "=d" (*edx)
        : "a" (leaf)
    );
}

AArch64 Inline Assembly

/* Read system register */
static inline uint64_t read_mpidr(void) {
    uint64_t val;
    __asm__ __volatile__ (
        "mrs %0, mpidr_el1"
        : "=r" (val)
    );
    return val;
}

/* Data cache clean/invalidate by VA */
static inline void dc_cvac(const void *addr) {
    __asm__ __volatile__ (
        "dc cvac, %0"
        : : "r" (addr) : "memory"
    );
}

/* Yield instruction (hint to CPU) */
static inline void cpu_relax(void) {
    __asm__ __volatile__ ("yield" : : : "memory");
}

RISC-V Inline Assembly

/* Read cycle counter */
static inline uint64_t rdcycle(void) {
    uint64_t val;
    __asm__ __volatile__ (
        "rdcycle %0"
        : "=r" (val)
    );
    return val;
}

/* Memory fence */
static inline void smp_mb(void) {
    __asm__ __volatile__ ("fence rw, rw" : : : "memory");
}

/* Read mhartid */
static inline uint64_t get_hartid(void) {
    uint64_t val;
    __asm__ __volatile__ (
        "csrr %0, mhartid"
        : "=r" (val)
    );
    return val;
}

Debugging Assembly

Using GDB with Assembly

# Compile with debug info
as --gdwarf-4 -o hello.o hello.s
ld -o hello hello.o

# Debug with GDB
gdb ./hello
(gdb) break _start
(gdb) run
(gdb) layout asm          # Show assembly window
(gdb) layout regs         # Show registers + assembly
(gdb) stepi               # Step one instruction
(gdb) info registers      # Show all registers
(gdb) print/x $rax        # Print register in hex
(gdb) x/10i $pc           # Show 10 instructions at PC
(gdb) x/16xb $rsp         # Examine stack (16 bytes)
(gdb) disassemble         # Disassemble current function

Debugging Inline Assembly

# See what GCC generates from inline asm
gcc -S -O2 -o output.s inline_test.c

# Check register allocation
gcc -S -O2 -fverbose-asm -o output.s inline_test.c

# View preprocessed assembly with C source interleaved
gcc -g -O2 -c -o test.o inline_test.c
objdump -dS test.o

Verifying Assembly Output

# Disassemble object file
objdump -d hello.o

# Show relocations (linker needs)
objdump -r hello.o

# Show symbol table
nm hello.o
# 0000000000000000 T _start
# 0000000000000020 D msg

# Show all ELF sections
readelf -S hello.o

# Verify instruction encoding
objdump -d hello.o | grep "movabs"
# Shows both disassembly and raw bytes

Optimization Directives

Alignment for Performance

# Align hot loops to 16-byte boundaries
.p2align 4
.Lhot_loop:
    # ... fast path ...
    jnz .Lhot_loop

# Align function entry points
.p2align 4
.global fast_function
fast_function:
    # ... function body ...

Branch Prediction Hints

# GAS does not have direct branch prediction hints,
# but GCC's __builtin_expect() generates .subsection placement
# Cold code goes in a separate subsection (moved away from hot path)

.section .text.unlikely
.Lcold_path:
    # Error handling, slow path
    # Placed far from hot code for better I-cache usage
    jmp .Lcommon_exit

.section .text
.Lhot_path:
    # Fast path
    testq %rax, %rax
    jz .Lcold_path

Instruction Scheduling

# GAS can schedule instructions with .sched_order
# Usually handled by compiler, but useful in hand-written asm

# On modern x86, out-of-order execution handles most scheduling
# Focus on:
# 1. Minimizing data dependencies
# 2. Avoiding store-to-load forwarding stalls
# 3. Aligning hot loops

Advanced GAS Features

Conditional Assembly

.macro save_all_regs
#ifdef __x86_64__
    pushq %rbx
    pushq %r12
    pushq %r13
    pushq %r14
    pushq %r15
#elif defined(__aarch64__)
    stp x19, x20, [sp, #-16]!
    stp x21, x22, [sp, #-16]!
    stp x23, x24, [sp, #-16]!
    stp x25, x26, [sp, #-16]!
    stp x27, x28, [sp, #-16]!
#endif
.endm

Including Binary Data

# Include a binary file (e.g., font, firmware, image)
.section .rodata
.global font_data
font_data:
    .incbin "font.bin"
.equ font_size, . - font_data

ELF Section Attributes

# Custom section with specific attributes
.section .initcall.init,"aw",@progbits
.align 8
.quad my_init_function

# Discarded section (for linker script)
.section .discard,"",@progbits

Cross-Architecture Assembly Quick Reference

Featurex86-64AArch64RISC-V RV64
Syscall instructionsyscallsvc #0ecall
Syscall number%raxx8a7
Return register%raxx0a0
Stack pointer%rspspsp
Frame pointer%rbpx29s0
Call instructioncallblcall
Return instructionretretret
First argument%rdix0a0
NOPnopnopnop
Fencemfencedmb syfence rw,rw

References