How Are Labels Used in Assembly Language Programming?

How are labels used in Assembly language programming

When I wrote my first few Assembly programs, labels felt like the one thing that made the code readable at all — without them, every jump and loop would have to reference raw memory addresses, which change every time you edit the source. Labels are Assembly’s way of giving names to addresses, and once I understood how the assembler resolves them, jumps, loops, and function calls all stopped feeling like guesswork.

Table of Contents

  • What Is a Label?
  • Why Labels Matter
  • How the Assembler Resolves Labels
  • Types of Labels
  • x86/x86-64 Examples
  • ARM Examples
  • Labels and the Symbol Table (Mermaid Diagram)
  • Labels in Data vs. Code Sections
  • Local vs. Global Labels
  • Labels and Linking
  • Debugging with Labels
  • Performance Considerations
  • Comparison: Labels vs. Raw Addresses
  • Best Practices
  • Common Mistakes
  • FAQs
  • Summary and Key Takeaways
  • References

What Is a Label?

A label is a symbolic name given to a specific location — an address — in the program. It can mark the start of a code block (a function, a loop, a jump target) or a data item (a variable, a buffer, a constant). Syntactically, a label is usually just an identifier followed by a colon:

start:
    MOV EAX, 1

loop_top:
    DEC EAX
    JNZ loop_top

Here, start and loop_top are labels. The assembler replaces every reference to them with the actual numeric address once it knows where they resolve to.

Why Labels Matter

Without labels, every jump instruction would need a raw address:

JMP 0x004010A2   ; fragile — breaks if you add or remove even one instruction above

With labels, that address is calculated automatically by the assembler, and stays correct no matter how the surrounding code changes:

JMP process_data   ; robust — always points to wherever 'process_data' ends up

This is the single biggest reason labels exist: maintainability. Assembly code changes constantly during development, and manually recalculating addresses after every edit would be unworkable.

How the Assembler Resolves Labels

During Pass 1 of assembly, the assembler scans through the source, tracking the location counter (the current address). Every time it encounters a label, it records that label’s name and current address in a symbol table. During Pass 2, whenever an instruction references a label, the assembler looks up the resolved address in the symbol table and encodes it into the instruction.

If a label is referenced before it’s defined later in the file (a “forward reference”), the assembler handles this by leaving a placeholder in Pass 1 and filling in the real address once it’s known — which is exactly why multi-pass assembly exists.

Types of Labels

TypePurposeExample
Code labelMarks a jump/call targetmain:, loop_start:
Data labelNames a memory location holding datacount DD 0
Local labelScoped to a function or block (often prefixed .).loop:
Global labelExported for the linker to see across filesGLOBAL main
External labelReferences a label defined in another fileEXTERN printf

x86/x86-64 Examples

SECTION .data
message DB "Counting down...", 0

SECTION .text
GLOBAL _start

_start:
    MOV ECX, 5

count_loop:
    DEC ECX
    CMP ECX, 0
    JNZ count_loop      ; label used as jump target

    MOV EAX, 1
    MOV EBX, 0
    INT 0x80

Here, _start and count_loop are code labels, while message is a data label pointing to a string in the .data section.

ARM Examples

    .global main
    .text

main:
    MOV R0, #5

loop:
    SUBS R0, R0, #1
    BNE  loop            ; branch back to 'loop' while R0 != 0

    BX   LR

main and loop are code labels; BNE loop (Branch if Not Equal) uses the label exactly the way JNZ does on x86.

Labels and the Symbol Table

flowchart TD
    A[Source Code Scan - Pass 1] --> B{Line defines a label?}
    B -->|Yes| C[Record Label Name + Current Address in Symbol Table]
    B -->|No| D[Advance Location Counter by Instruction/Data Size]
    C --> D
    D --> E{More Lines?}
    E -->|Yes| A
    E -->|No| F[Pass 2: Resolve Every Label Reference]
    F --> G[Replace Label Names with Actual Addresses]
    G --> H[Generate Final Machine Code]

Labels in Data vs. Code Sections

  • Data labels point to the start of reserved or initialized memory: buffer: RESB 256 creates a label buffer referring to the first byte of a 256-byte reserved block.
  • Code labels point to the address of the next instruction: loop_start: marks exactly where execution should resume when jumped to.

The assembler treats both the same way internally — a label is just a name bound to an address — but how you use that address (as data or as an instruction pointer target) is entirely up to your code.

Local vs. Global Labels

Many assemblers support local labels, scoped to reduce naming collisions inside large files:

function_a:
.loop:              ; local label, effectively function_a.loop
    DEC ECX
    JNZ .loop

function_b:
.loop:              ; a *different* local label, effectively function_b.loop
    DEC EDX
    JNZ .loop

This lets you reuse names like .loop or .done throughout a file without conflicts, since each is implicitly scoped to the preceding global label.

Labels and Linking

Labels intended to be called from other files must be explicitly exported:

GLOBAL my_function     ; NASM: make this label visible to the linker
EXTERN printf           ; NASM: this label is defined elsewhere

The linker resolves cross-file references by matching exported (GLOBAL/.global) labels against external (EXTERN/.extern) references, patching in the final addresses once all object files are combined.

Debugging with Labels

Debuggers like GDB display function and jump-target names using label information preserved in the object file’s symbol table (assuming the assembler emits debug symbols, e.g., via -g with GAS). Setting a breakpoint with break my_function only works because the label my_function exists in the symbol table — without labels, you’d be setting breakpoints on raw hex addresses, which is far more error-prone.

Performance Considerations

Labels themselves have zero runtime cost — they’re purely an assembly-time convenience. Once the code is assembled, every label reference becomes a fixed (or relative) address encoded directly into the instruction; there’s no lookup happening at execution time. The performance of a jump depends on the branch itself (and CPU branch prediction), not on whether it was originally written using a label.

Comparison: Labels vs. Raw Addresses

AspectLabelsRaw Addresses
ReadabilityHighVery low
MaintainabilityAutomatically adjusts as code changesMust be manually recalculated
DebuggabilitySymbol names shown in debuggersOnly hex addresses shown
PortabilityWorks across reassembly/relocationBreaks if code is moved or relinked
Runtime costNoneNone

Labels, Relative Addressing, and Position-Independent Code

Modern executables and shared libraries are typically built as position-independent code (PIC), meaning they can be loaded at different memory addresses each run (a key part of ASLR-based security). This changes how label references are actually encoded.

On x86-64, a jump to a label is usually encoded as a relative offset from the current instruction pointer, not an absolute address:

JMP loop_start   ; encoded as a relative displacement, e.g., E9 xx xx xx xx

The assembler calculates the byte distance between the current instruction and loop_start‘s resolved address, and encodes that distance rather than an absolute value. This means the same machine code works correctly no matter where the OS loader places the program in memory — the relative relationship between instructions never changes, even though their absolute addresses do.

Data labels referenced via LEA and RIP-relative addressing on x86-64 work the same way:

LEA RAX, [REL message]   ; RIP-relative reference to a data label

Labels in Multi-File Projects

In any nontrivial Assembly project, you’ll split code across multiple .asm/.s files, and labels are the glue that connects them:

; file: math.asm
GLOBAL add_two
add_two:
    MOV EAX, EDI
    ADD EAX, ESI
    RET
; file: main.asm
EXTERN add_two
GLOBAL _start
_start:
    MOV EDI, 3
    MOV ESI, 4
    CALL add_two      ; linker resolves this to math.o's add_two
    ; ... exit syscall ...

Each file is assembled independently into an object file (nasm -f elf64 math.asm -o math.o), and only at the linking stage does the linker match EXTERN add_two in main.o against GLOBAL add_two in math.o, patching the final call address into the combined executable.

Anonymous and Numeric Labels

Some assemblers (notably GAS) support numeric local labels — labels made of just digits, reusable multiple times in a file, disambiguated by direction (f for forward, b for backward):

1:
    dec %ecx
    jnz 1b     # jump backward to the nearest '1:' label
    jmp 2f     # jump forward to the nearest '2:' label
2:
    nop

These are handy for very short, throwaway jump targets inside macros where a named label might collide across multiple macro expansions.

Labels and Data Structures

Labels aren’t limited to single variables — they’re also how Assembly programmers lay out structured data, effectively hand-rolling what a high-level language would call a struct:

SECTION .data
player:
    .health   DD 100
    .mana     DD 50
    .name     DB "Hero", 0

Here player is the base label, and .health, .mana, .name are local labels scoped beneath it, each resolving to the base address plus whatever offset the assembler calculated based on the preceding fields’ sizes. Accessing player.health (or, in raw form, [player+0]) works exactly like accessing a struct field in C, just without any compiler-generated type checking — the programmer is responsible for tracking every field’s offset and size by hand, which is exactly why clear, consistent labeling of structured data is so important in larger Assembly projects.

Best Practices

  • Use descriptive label names (validate_input: rather than l1:).
  • Prefer local labels (.loop, .done) inside functions to avoid symbol table clutter.
  • Export only the labels that genuinely need to be visible outside the file.
  • Keep a consistent naming convention (snake_case is common in Assembly) across a project.

Label Naming Conventions I’ve Settled On

Over time I’ve converged on a few habits that make large Assembly projects much easier to navigate:

  • Functions: descriptive snake_case verbs — parse_header:, validate_checksum:, write_output:.
  • Loop labels: scoped locally with a leading dot — .loop:, .done:, .skip: — so the same short name can be reused inside every function without collision.
  • Data labels: nouns that describe content, with a type hint where useful — packet_buffer:, retry_count:, error_msg:.
  • Constants via EQU: uppercase, similar to C macro convention — MAX_RETRIES EQU 5.

This mirrors the naming discipline you’d expect in any high-level codebase, and it matters more in Assembly, not less, precisely because there’s no type system or IDE auto-complete to compensate for a vague name.

How Disassemblers Reconstruct (or Guess) Labels

When you don’t have source code — analyzing a stripped binary, for instance — tools like Ghidra, IDA Pro, and objdump have to reconstruct meaningful labels from scratch. They typically generate synthetic names like sub_401000 (subroutine at address 0x401000) or loc_401020 (a jump target with no clearer name) when the original symbol table has been stripped. Part of a reverse engineer’s job is renaming these synthetic labels to meaningful ones as they figure out what each function actually does — effectively recreating the labeling work the original programmer did, just in reverse. This is a good illustration of just how much labels contribute to readability: a disassembly full of sub_401000, sub_401040, sub_4010A2 is dramatically harder to follow than the same code with labels like parse_header, compute_crc, send_response.

Common Mistakes

  • Reusing the same global label name in two files without marking one as local, causing a linker “duplicate symbol” error.
  • Forgetting the colon (:) after a label definition in assemblers that require it.
  • Confusing a data label’s address with its content — MOV EAX, buffer loads the address, while MOV EAX, [buffer] loads the value stored there.
  • Not exporting a label needed by another object file, leading to “undefined reference” errors at link time.

Labels as a Bridge Between Human Intent and Machine Addresses

If there’s one idea I’d want a beginner to take away from all of this, it’s that labels exist entirely for us, not for the CPU. The processor never sees a label — by the time code reaches execution, every label reference has already been fully replaced by a numeric address or relative offset. That gap between “what the programmer wrote” and “what the CPU actually runs” is a recurring theme throughout Assembly language, and labels are one of the clearest, earliest examples of it: a piece of syntax that exists purely to make the assembler’s job (and the human reader’s job) easier, with zero footprint in the final executing program.

Labels in Macro Expansions

One subtlety worth knowing: when a label is defined inside a macro that gets expanded multiple times, naively reusing the same label name across expansions causes a “duplicate symbol” error, since the assembler sees the same literal label text repeated at different addresses. Assemblers solve this with unique-label facilities — NASM’s %%label syntax inside macros generates a fresh, uniquely numbered label on every expansion:

%macro SAFE_DIV 2
    CMP  %2, 0
    JNE  %%skip_div_error
    CALL handle_div_by_zero
%%skip_div_error:
    DIV  %2
%endmacro

Each call to SAFE_DIV gets its own internally unique %%skip_div_error label behind the scenes, even though the source text looks identical every time it’s invoked. Without this mechanism, using the same macro twice in one file would immediately break assembly with a symbol collision.

FAQs

Do labels take up memory in the final binary? No, not by themselves. A label is just a name; it doesn’t occupy space unless it’s associated with data via a directive like DB/DW/DD.

Can two labels point to the same address? Yes. Multiple labels can be defined consecutively, and they’ll all resolve to the same location counter value.

Are label names case-sensitive? This depends on the assembler — NASM and GAS are case-sensitive by default; check your specific toolchain’s documentation.

What’s the difference between a label and a variable? A label is a name for an address; whether that address behaves like a “variable” depends entirely on whether it’s used with data-definition directives and accessed via memory operands.

Summary and Key Takeaways

  • Labels are symbolic names for addresses, resolved by the assembler via a symbol table.
  • They make Assembly code maintainable, readable, and debuggable, with zero runtime performance cost.
  • Labels can mark code (jump/call targets) or data (variables, buffers, constants).
  • Local labels reduce naming collisions; global labels enable cross-file linking via GLOBAL/EXTERN.
  • Mastering label usage is essential before tackling more advanced control flow and multi-file Assembly projects.

References

  • NASM Documentation — nasm.us/doc
  • GNU Assembler (GAS) Manual — sourceware.org/binutils/docs/as
  • ARM Assembler Reference Guide — developer.arm.com/documentation
  • Intel® 64 and IA-32 Architectures Software Developer’s Manual — intel.com/sdm
Total
1
Shares

Leave a Reply

Previous Post
Explain the concept of opcode and operand in Assembly language

Opcode and Operand in Assembly Language: What They Are and How They Work

Next Post
What is the significance of the stack in Assembly language

What Is the Significance of the Stack in Assembly Language?

Related Posts