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.
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:
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.pyreads 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_slashis I4 over 36 annotated instructions.tests/test_mdesc.py::test_the_extract_in_the_repository_matches_the_tree_it_says_it_came_fromreads 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_doeschecks 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.