Skip to content

BP-FINAL, turning insns into text

Status: stub Applies to: GCC 16.2.0 (tag releases/gcc-16.2.0) Target-dependent: yes Generated sections: none Last verified: 2026-09-02 against releases/gcc-16.2.0

This is a stub. It holds what T09 needed, which is enough of final to read an annotated assembly file, find the machine description pattern that emitted any line of it, and say which row of that pattern was used and why. The machine description as a language, the recognizer that decides which pattern matches in the first place, the operand substitution letters each target defines, and the whole of varasm past section selection are named here and specified elsewhere.

Three documents will eventually split out of this one. BP-MD for the machine description as a language, since it is the largest domain specific language in GCC and genoutput and friends are a compiler of their own. BP-VARASM for everything about getting data rather than code into the object file. BP-DWARF for the debug information final interleaves with the instructions, which this document does not mention again.

1. Purpose and scope

final is the last pass that touches a function. It walks the insn chain in order and writes assembly text to a stream. It is the only place in the compiler that reads the machine description as a set of templates to print rather than a set of patterns to match, and that inversion is the single most useful thing to know about it.

What this document covers. The pass and its position. The insn chain to text loop. The three forms an output template can take and how the choice between them is made. The -dp annotation, its grammar, and the rule that decides whether it prints an alternative number. Operand substitution. Section selection for variables, to the depth of naming the categories and the function that assigns them. Alignment.

What it does not cover. Debug information, which final emits interleaved with the instructions and which is longer than this whole document. The .cfi_ unwind directives, beyond noting that they come from dwarf2cfi and not from any pattern. Delayed branch scheduling and machine reorganisation, both of which run before final and both of which have a target hook of their own. Constant pool emission. Inline assembly, which takes a different path through output_asm_insn and has its own error reporting. Anything about the assembler, the linker or the loader.

Position in the pipeline. pass_final at gcc/final.cc:4340@releases/gcc-16.2.0 is one of the last entries in gcc/passes.def, after pass_dwarf2_frame and after every machine reorganisation the target asked for. It runs rest_of_handle_final at gcc/final.cc:4259@releases/gcc-16.2.0, which calls final_start_function, then final at gcc/final.cc:2009@releases/gcc-16.2.0, then final_end_function.

Inputs and outputs as properties. The pass declares no property change, because nothing it does is visible to the property system. What it requires is not modelled either, and it is substantial: every insn must be recognised, every operand must satisfy its constraints, and there must be no pseudo registers left. All three are established by LRA and none of them is rechecked here. An insn that fails any of them reaches get_insn_template and aborts.

2. Data structures

2.1 The generated insn table

struct insn_data_d at gcc/recog.h:510@releases/gcc-16.2.0, one entry per pattern, in an array insn_data indexed by insn code. The array is generated by genoutput from the machine description at build time, so it is a compiled form of the .md files rather than anything read at run time.

Field What it holds
name the pattern name, which is what -dp prints
output.single the template, when there is one string
output.multi the templates, one per alternative, indexed by which_alternative
output.function the C function, when the pattern's output is code
operand one insn_operand_data per operand, carrying the predicate and the constraint string
n_operands how many operands the pattern has
n_alternatives how many alternatives, and the number the -dp slash rule tests
output_format which of the three output members is the live one

The output_format values are INSN_OUTPUT_FORMAT_NONE, _SINGLE, _MULTI and _FUNCTION, defined immediately above the struct. NONE means the pattern emits nothing and reaching it is an abort.

2.2 which_alternative

extern int which_alternative; at gcc/recog.h:363@releases/gcc-16.2.0. A global, set by the recognizer when it decides which alternative of a pattern the current operands fit, read by get_insn_template to index output.multi, and printed after the slash by output_asm_name. It is meaningful only between the recognizer running on an insn and that insn being printed, and nothing checks that.

2.3 A pattern, as the machine description writes it

DEF_RTL_EXPR(DEFINE_INSN, ...) at gcc/rtl.def:885@releases/gcc-16.2.0. Four parts in order: a name, the RTL template to match, a C condition, and the output. An optional fifth part carries attributes.

GCC 16 writes the output in one of three syntaxes, and a reader has to handle all three because a single target file uses all three.

"add\t%w0, %w1, %2"

A plain string. One alternative, or one template that serves every alternative. Compiles to INSN_OUTPUT_FORMAT_SINGLE.

{@ [ cons: =0 , %1 , 2 ; attrs: type , arch ]
   [ rk      , rk , I ; alu_imm  , *    ] add\t%<w>0, %<w>1, %2
   [ rk      , rk , r ; alu_sreg , *    ] add\t%<w>0, %<w>1, %<w>2
}

The compact alternative table, introduced in GCC 14 and now the usual form in the aarch64 back end. The header names the operands and the attributes, the rows are read by position, and the row index is the number -dp prints after the slash. Compiles to INSN_OUTPUT_FORMAT_MULTI. Four symbols in the body are not literal text:

Symbol Where Meaning
^ in place of a template the same template as the row above
# as the whole template this alternative needs a define_split and emits nothing itself
<< at the start of a template the rest of the row is C, evaluated for its return value
%1 in the header this operand is commutative with the next one
{
  const char *ret = NULL;
  if (aarch64_return_address_signing_enabled ())
    ret = "retaa";
  else
    ret = "ret";
  output_asm_insn (ret, operands);
  return "";
}

A C block. Compiles to INSN_OUTPUT_FORMAT_FUNCTION. The block is a function body, it is handed operands and the insn, and what it returns is printed as a template. Returning the empty string after calling output_asm_insn directly, as above, is the idiom for a pattern that wants to do its own printing.

2.4 Mode iterators

define_mode_iterator at gcc/read-rtl.cc:1482@releases/gcc-16.2.0. A name standing for a list of machine modes, optionally with a C condition per mode. A pattern whose name or body contains <mode> or <ITERATOR:mode> is expanded once per member before anything else sees it, so a pattern written once as *add<mode>3_aarch64 exists at run time as *addsi3_aarch64 and *adddi3_aarch64.

This matters to a reader for one practical reason: the name -dp prints is the expanded name and the name in the file is the unexpanded one, so searching the machine description for the name in the annotation frequently finds nothing.

define_mode_attr is the same idea for a fragment rather than a whole mode name. <w> is a mode attribute on aarch64 that is w for the 32 bit modes and x for the 64 bit ones, which is how one template prints both add w0, w1, w2 and add x0, x1, x2.

2.5 Section categories

enum section_category at gcc/output.h:426@releases/gcc-16.2.0. Eighteen values. Every variable that reaches the assembler is assigned exactly one of them, and the target then maps the category to a section name.

3. Algorithms

3.1 The pass

function final (first: Insn, out: Stream)
    complexity: O(n) in the number of insns, plus whatever a FUNCTION template costs

    insn = first
    while insn != nothing
        final_scan_insn(insn, out)
        insn = insn.next

3.2 Printing one insn

final_scan_insn at gcc/final.cc:2888@releases/gcc-16.2.0, with everything about debug information, dialects and machine specific reorganisation left out.

function final_scan_insn (insn: Insn, out: Stream)
    if insn.code == NOTE
        act on the note if it opens a block or a section, print nothing
        return
    if insn.code == CODE_LABEL
        print the label and return
    if insn.code == BARRIER
        return
    if not is_code(insn)
        return

    recog(insn)                        # sets which_alternative and recog_data.operand
    templ = get_insn_template(insn.icode, insn)
    debug_insn = insn                  # only if flag_print_asm_name
    output_asm_insn(templ, recog_data.operand)

The debug_insn assignment is the whole of the -dp mechanism. output_asm_name reads that global, prints, and clears it, which is what makes exactly one line of a multi line template carry the annotation.

3.3 Choosing a template

get_insn_template at gcc/final.cc:2024@releases/gcc-16.2.0. The entire function.

function get_insn_template (code: integer, insn: Insn) -> string
    complexity: O(1), except that FUNCTION runs arbitrary target code

    switch insn_data[code].output_format
        case SINGLE
            return insn_data[code].output.single
        case MULTI
            return insn_data[code].output.multi[which_alternative]
        case FUNCTION
            return insn_data[code].output.function(recog_data.operand, insn)
        default
            abort

3.4 Substituting operands

output_asm_insn at gcc/final.cc:3428@releases/gcc-16.2.0.

function output_asm_insn (templ: string, operands: sequence of Rtx)
    complexity: O(length of templ), plus the cost of each operand printed

    if templ == ""
        return
    emit a tab
    for each character c in templ
        if c == '\n'
            finish the line: verbose names, then the -dp annotation, then the newline
        else if c == '%'
            handle the escape below
        else
            emit c
    if flag_verbose_asm
        output_asm_operand_names(operands)
    if flag_print_asm_name
        output_asm_name()
    emit a newline

The escapes, in the order the function tests them:

Escape Prints
%% one literal %
%{, %}, %\| the character, where the target defines assembler dialects
%= a number unique to this insn across the whole compilation
%l<n> operand n as a label
%a<n> operand n as an address
%c<n> operand n as a constant
%n<n> operand n negated, which is how one pattern prints add and sub
%<letter><n> operand n, handed to TARGET_PRINT_OPERAND with the letter
%<n> operand n, printed the default way for its mode

Only l, a, c and n are implemented in final.cc. Every other letter is a target decision, which is why %w0 means a 32 bit register name on aarch64 and nothing at all anywhere else.

3.5 The annotation

output_asm_name at gcc/final.cc:3219@releases/gcc-16.2.0, called from output_asm_insn when flag_print_asm_name is set. The whole function, since it is short and since its exact output is what a reader parses.

function output_asm_name ()
    if debug_insn == nothing
        return
    print tab, ASM_COMMENT_START, space, debug_insn.uid, tab
    print "[c=", insn_cost(debug_insn), 
    if the target has a length attribute
        print " l=", get_attr_length(debug_insn)
    print "]  "
    num = debug_insn.icode
    print insn_data[num].name
    if insn_data[num].n_alternatives > 1
        print "/", which_alternative
    debug_insn = nothing

The grammar of what comes out, for a reader writing a parser against it:

annotation := COMMENT SP uid TAB "[c=" cost ("l=" length)? "]" SP SP name ("/" alternative)?

COMMENT is ASM_COMMENT_START, which is a target macro. It is # on x86, // on ELF aarch64, ; on Mach-O aarch64, and other things elsewhere. A parser that assumes any one of them is wrong on some target the same compiler supports.

3.6 Where a variable goes

categorize_decl_for_section at gcc/varasm.cc:7368@releases/gcc-16.2.0, reached from assemble_variable at gcc/varasm.cc:2517@releases/gcc-16.2.0, and mapped to a section name by the target's TARGET_ASM_SELECT_SECTION hook, which for ELF is default_elf_select_section at gcc/varasm.cc:7490@releases/gcc-16.2.0.

function categorize_decl_for_section (decl: Decl, reloc: integer) -> symbol
    complexity: O(1)

    if decl is a function
        return SECCAT_TEXT
    if bss_initializer_p(decl)
        return SECCAT_BSS
    if decl is not read only
        return one of SECCAT_DATA, SECCAT_DATA_REL, SECCAT_DATA_REL_LOCAL
             depending on whether the initialiser needs relocations and
             whether those relocations are all to local symbols
    if the initialiser is a string and the target has a mergeable string section
        return SECCAT_RODATA_MERGE_STR
    if the initialiser needs relocations
        return one of SECCAT_DATA_REL_RO, SECCAT_DATA_REL_RO_LOCAL
    return SECCAT_RODATA

bss_initializer_p at gcc/varasm.cc:1107@releases/gcc-16.2.0 is true when there is no initialiser at all or when the initialiser is all zero bytes. That is why int total = 0; and int pending; produce byte for byte identical assembly.

None of this is an optimization and none of it consults the optimization level. It reads the declaration.

4. Invariants

I1. Every insn reaching final_scan_insn that is code has a valid insn code, meaning INSN_CODE(insn) >= 0. Established by: LRA, which recognises every insn it leaves behind. Checked by: a gcc_assert inside recog_memoized under --enable-checking, and by gcc_unreachable in get_insn_template when output_format is NONE. May be broken by: nothing after LRA.

I2. No insn reaching final refers to a pseudo register. Established by: lra_spill, which rewrites every remaining pseudo to a hard register or a stack slot. Checked by: nothing directly. A pseudo that survives produces either a crash inside a target print hook or, worse, plausible looking assembly naming a register that does not exist.

I3. Exactly one line of assembly per RTL insn carries a -dp annotation, and it is the first. Established by: output_asm_name clearing debug_insn after printing. Checked by: nothing. Relied on by: any tool that counts insns by counting annotations, which is correct, and by any tool that counts instructions that way, which is not.

I4. The alternative number is printed if and only if the pattern has more than one alternative. Established by: the n_alternatives > 1 test in output_asm_name. Checked by: nothing in GCC. Checked by: tests/test_mdesc.py in this repository, over every annotated instruction in the T09 corpus.

I5. which_alternative is valid only between the recognizer running on an insn and that insn being printed. Established by: recog. Checked by: nothing. A FUNCTION template that reads it after calling something that recognises another insn reads a stale value.

5. Observable behaviour

Everything in this section is visible with -S -dp and no other tools.

What How to see it Corpus entry
The pattern that emitted a line -dp, last field of the trailing comment t09-final
The alternative that was used -dp, after the slash, when present t09-final
The insn uid, joinable to the RTL dump -dp, first field, against -fdump-rtl-final t09-final
That one insn can emit several lines only the first line is annotated t09-final
That some insns emit nothing a uid in the dump with no line in the file t09-final
Which section a variable lands in the directives around its label t09-sections
That int x = 0; and int x; are identical both produce .zero in .bss t09-sections
That an alignment attribute is one directive .align 6 for aligned (64) on aarch64 t09-sections
That the comment character is a target decision // against ; for the same architecture t09-final, t09-local

Two numbers worth recording because they surprise people. For the four line function in corpora/programs/l1.c at -O2 on aarch64, the file is 46 lines and 12 of them are instructions. The same function compiled for aarch64 Darwin is 56 lines with the same 12 instructions.

6. Edge cases and error paths

An empty template. output_asm_insn returns immediately on an empty string and prints nothing, not even the leading tab. This is the mechanism by which a FUNCTION template does its own printing and then declines to have anything printed for it.

A # template. The alternative was chosen and the pattern has nothing to print, because a define_split is expected to have replaced the insn before final ran. Reaching final with a # alternative live emits a bare # into the assembly and the assembler then fails, which is a confusing way to discover a missing split.

A multi line template. A template containing \n produces several assembly lines from one insn. The annotation goes on the first, because output_asm_name clears debug_insn, and the verbose operand names go on each line.

An unrecognised insn. INSN_CODE is negative and insn_data[-1] is out of bounds. Under --enable-checking this asserts in recog_memoized. Without checking it reads whatever is before the table.

Inline assembly. asm statements have no pattern, so -dp prints a code number in place of a name. this_is_asm_operands is set while they are printed and it changes the error path: a bad operand number produces a user visible error against the asm statement rather than an internal compiler error.

-fverbose-asm with -dp. Both write a comment on the same line, verbose first and the annotation last. A parser has to anchor to the end of the line. Compiler Explorer sets -fverbose-asm and offers no way to turn it off, so this is the common case rather than an exotic one.

A template shorter than nine characters. output_asm_insn emits an extra tab before the comments, purely to line the comments up. The threshold is a literal 9 in the source.

7. Interactions

Runs after. LRA, which establishes I1 and I2. dwarf2cfi, which inserts the NOTE_INSN_CFI notes that become .cfi_ directives. Every target machine reorganisation pass, since TARGET_MACHINE_DEPENDENT_REORG is the last chance to change the insn chain.

Runs before. Nothing. final is the end of the per function pipeline.

Reads. insn_data, generated from the machine description. recog_data, filled in by the recognizer. which_alternative, debug_insn, insn_counter, flag_print_asm_name and flag_verbose_asm, all globals.

Calls into the target. ASM_COMMENT_START for the comment character. TARGET_PRINT_OPERAND and TARGET_PRINT_OPERAND_ADDRESS for every operand escape final.cc does not implement itself. TARGET_ASM_SELECT_SECTION and TARGET_ASM_NAMED_SECTION for where things go. ASM_OUTPUT_OPCODE and ASSEMBLER_DIALECT where the target has more than one syntax.

Depends on being right about. get_attr_length, which is a per target function generated from the length attribute in the machine description. It is a prediction, and branch shortening uses it, so a target that gets it wrong produces branches that do not reach.

8. Conformance

An implementation of this component is correct when, for every entry in the corpus, it produces byte for byte the assembly GCC produced.

The invariants restated as assertions:

I1  for each insn in chain where is_code(insn): insn.icode >= 0
I3  count(lines with annotation) == count(code insns that emitted anything)
I4  for each annotated line: (line.alternative != nothing) == (pattern.n_alternatives > 1)

The checks that exist in this repository:

  • tests/test_asm.py reads all three T09 recordings, sorts every line into exactly one kind, and asserts the annotation is parsed into all four of its fields.
  • tests/test_mdesc.py::test_the_number_of_alternatives_is_what_decides_whether_dp_prints_a_slash is I4 over 36 annotated instructions.
  • tests/test_mdesc.py::test_the_extract_in_the_repository_matches_the_tree_it_says_it_came_from reads the pinned GCC tree and checks the committed extract against a fresh extraction.
  • tests/test_mdesc.py::test_a_citation_in_the_extract_points_at_the_line_it_says_it_does checks every line number in the extract against the file it names.

Upstream tests worth knowing about: gcc/testsuite/gcc.dg/pr*.c cases that turn on -dp are checking the annotation as a side effect rather than on purpose, and there is no upstream test of the annotation format itself. It is a debugging aid with no compatibility promise, and it has changed shape before.

9. Port notes

Forced. Something has to choose between the templates of a multi alternative pattern, and that something has to be the same thing that checked the constraints, because the constraint check is what determines the answer. Whether it is a global like which_alternative or a return value is arbitrary.

Forced. The instruction to text mapping has to be data rather than code for most instructions, or the back end is unmaintainable. Every production compiler does some version of this.

Arbitrary. Three output formats. A design with only the function form would be uniformly less pleasant to write and exactly as capable. GCC has three because the string form came first, the table form was added when targets grew alternatives, and the function form was added when neither was enough.

Arbitrary. Printing the assembly as text and running a separate assembler. LLVM's integrated assembler goes straight to an object file, and the only thing GCC loses by not doing that is time. What GCC gains is that every stage of the back end is inspectable with cat.

Arbitrary. The -dp format, in every detail. It is a debugging aid.

Target dependent, and this is the table.

What Varies how
ASM_COMMENT_START # on x86, // on ELF aarch64, ; on Mach-O aarch64, ! on sparc
Operand escape letters entirely. %w0 is aarch64, %q0 is x86, and neither means anything to the other
Section names and syntax .section .rodata,"a",@progbits on ELF against .section __TEXT,__const on Mach-O
Local label spelling .L4 on ELF, L4 on Mach-O
Whether l= appears at all only when the target defines a length attribute, which nearly all do
The alignment directive .align takes a power of two on aarch64 and a byte count on x86 ELF, with the same spelling
Assembler dialects ASSEMBLER_DIALECT exists so one x86 back end can emit both AT&T and Intel syntax. Most targets have one syntax and never touch it

The last row of that table is the one to be careful with. .align 2 means four bytes on aarch64 and two bytes on x86 ELF, out of the same compiler, and nothing in the directive says which reading applies.