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.
| Feature | AT&T (GAS default) | Intel (NASM/YASM) |
|---|---|---|
| Operand order | src, dst | dst, src |
| Register prefix | %rax | rax |
| Immediate prefix | $42 | 42 |
| Memory reference | (%rax,%rcx,4) | [rax+rcx*4] |
| Size suffix | movq, movl, movb | mov QWORD, DWORD, etc. |
| Section directive | .section .text | section .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
| Aspect | GAS (as) | NASM |
|---|---|---|
| Syntax default | AT&T | Intel |
| Macro system | .macro / .altmacro | %macro / %define |
| Object format | ELF (native), COFF, Mach-O | ELF, COFF, Mach-O, Win32/Win64 |
| Section naming | .section .text / .text | section .text |
| Conditional assembly | .if, .ifdef | %if, %ifdef |
| Included in | GNU binutils | Standalone |
| Package (Debian/Ubuntu) | binutils | nasm |
| Debug info | DWARF (.debug_* sections) | DWARF (-g) |
| Architecture support | Multi-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
| Directive | Purpose |
|---|---|
.file | Set source file name for debug info |
.loc | Source line number mapping |
.type sym, @function | Mark symbol as function |
.size sym, .-sym | Set symbol size for debuggers |
.align | Align to power-of-2 boundary |
.balign | Align to exact byte boundary |
.org | Set location counter |
.fill | Repeat fill pattern |
.incbin | Include raw binary file |
.ident | Add identification string to .comment |
.pushsection | Save and switch section |
.popsection | Restore 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
| Feature | x86-64 | AArch64 | RISC-V RV64 |
|---|---|---|---|
| Syscall instruction | syscall | svc #0 | ecall |
| Syscall number | %rax | x8 | a7 |
| Return register | %rax | x0 | a0 |
| Stack pointer | %rsp | sp | sp |
| Frame pointer | %rbp | x29 | s0 |
| Call instruction | call | bl | call |
| Return instruction | ret | ret | ret |
| First argument | %rdi | x0 | a0 |
| NOP | nop | nop | nop |
| Fence | mfence | dmb sy | fence rw,rw |
References
- GCC Internals — how GCC generates assembly
- Linker and Linker Scripts — consuming GAS output
- Debugging with GDB — inspecting assembly at runtime
- ELF Binary Format — the object file format GAS produces