Skip to main content
Since version 0.9.16, TinyCC integrates its own assembler supporting GNU gas-like syntax. The assembler handles both standalone assembly files and inline assembly within C code.
TCC’s assembler enables you to write low-level code without requiring an external assembler, maintaining TCC’s self-reliant philosophy.

Overview

TinyCC Assembler is used to handle:
  • .S files: C preprocessed assembler source
  • .s files: Raw assembler source (no preprocessing)
  • Inline assembly: Using the asm or __asm__ keyword in C code
You can deactivate assembler support for a smaller TCC executable. The C compiler does not rely on the assembler.

Assembly syntax

The TCC assembler supports most of the GNU as (gas) syntax with a few differences.

Tokens and comments

# This is a comment
// C++ style comments also work
/* C style comments work too */

mov %eax, %ebx  # Move ebx to eax
Identifiers follow C rules:
  • Cannot use . or $ in identifiers (unlike gas)
  • Must start with a letter or underscore
  • Can contain letters, numbers, and underscores
my_label:        # Valid
_start:          # Valid
loop.1:          # Invalid (contains .)
temp$var:        # Invalid (contains $)

Number formats

mov $42, %eax       # Decimal
mov $0x2A, %eax     # Hexadecimal
mov $052, %eax      # Octal
Only 32-bit integer numbers are supported in expressions.

Expressions

Operators

mov $+10, %eax      # Positive (unary +)
mov $-10, %eax      # Negative (unary -)
mov $~0xFF, %eax    # Bitwise NOT
Highest precedence:
mov $(4 * 5), %eax     # Multiplication
mov $(20 / 4), %eax    # Division
mov $(17 % 5), %eax    # Modulo
Medium precedence:
mov $(0xFF & 0x0F), %eax    # Bitwise AND
mov $(0xF0 | 0x0F), %eax    # Bitwise OR
mov $(0xFF ^ 0x0F), %eax    # Bitwise XOR
Lowest precedence:
mov $(label + 4), %eax      # Addition
mov $(label2 - label1), %eax # Subtraction

Label arithmetic

All operators accept absolute values except + and -. These can add offsets to labels or compute differences between labels in the same section.
.text
start:
    nop
end:
    mov $(end - start), %eax  # Size of code between labels
    mov $(buffer + 16), %edx  # Address offset

Labels

Named labels

.globl main
main:
    push %ebp
    mov %esp, %ebp
    # Function code
    leave
    ret

local_label:
    # Local code
    ret
All labels are considered local by default, except undefined ones.

Numeric labels

Numeric labels can be defined multiple times and referenced with b (backward) or f (forward):
1:
    cmp %eax, %ebx
    jne 1f          # Jump forward to next "1:" label
    inc %eax
    jmp 1b          # Jump backward to previous "1:" label
1:
    ret
Numeric labels are useful for local jumps within a function without polluting the namespace.

Directives

All directives are preceded by a . character.

Data definition

.byte 0x10, 0x20, 0x30      # Define bytes
.word 0x1234, 0x5678        # Define words (2 bytes)
.short 100, 200             # Define shorts (2 bytes)

Alignment and spacing

Align to a power-of-two boundary:
.align 16           # Align to 16-byte boundary
.align 8, 0x90      # Align to 8 bytes, fill with 0x90 (NOP)
.balign 4           # Same as .align 4
Align using power-of-two exponent:
.p2align 3          # Align to 2^3 = 8 bytes
.p2align 4, 0x90    # Align to 16 bytes, fill with 0x90
Reserve space:
.skip 10            # Skip 10 bytes (filled with 0)
.skip 16, 0xFF      # Skip 16 bytes filled with 0xFF
.space 100          # Reserve 100 bytes
Repeat a pattern:
.fill 10, 4, 0x12345678    # Repeat 0x12345678 (4 bytes) 10 times
.fill 5, 1, 0xAA           # Repeat 0xAA (1 byte) 5 times

Section control

.text               # Switch to code section
.data               # Switch to initialized data section
.bss                # Switch to uninitialized data section

Symbol visibility

.globl symbol_name  # Make symbol visible externally
.global main        # Same as .globl
.weak weak_symbol   # Declare weak symbol
.hidden int_func    # Hide symbol (not exported)

Advanced directives

.org 0x1000         # Set address to 0x1000
    # Code at absolute address 0x1000
The .org directive can only move forward and must stay within reasonable bounds (0 to 0x100000).
.rept 4
    nop             # Repeat 4 NOPs
.endr

.rept 3
    push %eax
    call function
    pop %eax
.endr
.set BUFFER_SIZE, 1024
.set MAX_ITEMS, 100

mov $BUFFER_SIZE, %eax

X86 assembly

All x86 opcodes are supported with ATT syntax (source then destination operand order).

Syntax differences

mov %eax, %ebx      # Move eax to ebx (source, dest)
add $10, %eax       # Add 10 to eax
mov (%ebx), %eax    # Load from address in ebx

Size suffixes

movb $0x42, %al     # Move byte
movw $0x1234, %ax   # Move word
movl $0x12345678, %eax  # Move long (32-bit)

# TCC can infer size from operands:
mov $10, %eax       # Inferred as movl
mov %al, %bl        # Inferred as movb

Addressing modes

mov %eax, %ebx

Inline assembly in C

TCC supports GCC-style inline assembly with full constraint syntax.

Basic inline assembly

int main() {
    __asm__("nop");                    // Single instruction
    __asm__("mov $1, %eax\n\t"        // Multiple instructions
            "xor %ebx, %ebx");
    return 0;
}

Extended inline assembly

static inline int atomic_add(int *ptr, int value) {
    int result;
    __asm__ __volatile__(
        "lock; xaddl %0, %1"
        : "=r" (result), "+m" (*ptr)
        : "0" (value)
        : "memory"
    );
    return result;
}

Constraint examples

int result;
__asm__("movl %%eax, %0" : "=r" (result));
// "=r" means write-only register
// "+r" means read-write register
TCC generates the assembly code directly without intermediate files and supports GCC 3.x named operands for better readability.

Limitations

Currently not supported:
  • SSE instructions (MMX opcodes are supported)
  • Intel syntax (only ATT syntax)
  • 64-bit operand sizes in 32-bit mode
  • Some advanced gas directives

Platform support

The assembler is available on:
  • i386: Full support (Linux and Windows)
  • x86_64: Full support (Linux and Windows)
  • ARM: Supported
  • ARM64: Supported
  • RISC-V: Supported
For cross-platform code, use C with inline assembly only when necessary for performance-critical or hardware-specific operations.

Build docs developers (and LLMs) love