In object files, certain code patterns incorporate data directlywithin the code or transit between instruction sets. This practice canpose challenges for disassemblers as data might be mistakenlyinterpreted as code, leading to nonsensical output. In addition, codefrom instruction set A might be disassembled as instruction set B. Toaddress the issues, some architectures define mapping symbols todescribe state transition. Let's explore this concept using an AArch32code example:
1 | .text |
Jump Tables (.LJTI0_0): Jump tables, like .LJTI0_0, store a list ofabsolute addresses used for efficient branching. They can reside ineither data or text sections, each with its trade-offs.
Jump tables (.LJTI0_0
): Jump tables canreside in either data or text sections, each with its trade-offs. Herewe see a jump table in the text section, allowing a single instructionto take its address. Other architectures generally prefer to place jumptables in data sections. While avoiding data in code, RISC architecturestypically require two instructions to materialize the address, sincetext/data distance can be pretty large.
Constant pool (.LCPI0_0
): Thevldr
instruction loads a 16-byte floating-point literal tothe SIMD&FP register.
ISA transition: This code blends A32 and T32instructions (the latter used in thumb_callee
).
In these cases, a dumb disassembler might treat data as code and trydisassembling them as instructions. Assemblers create mapping symbols toassist disassemblers. For this example, the assembled object file lookslike the following:
1 | $a: |
Now, let's delve into how mapping symbols are managed within thetoolchain.
llvm-objdump sorts symbols, including mapping symbols, relative tothe current section, presenting interleaved labels and instructions.Mapping symbols act as signals for the disassembler to switchstates.
1 | % fllvm-objdump -d --triple=armv7 --show-all-symbols a.o |
I changed llvm-objdump18 to not display mapping symbols as labels unless--show-all-symbols
is specified.
Both llvm-nm and GNU nm typically conceal mapping symbols alongsideSTT_FILE
and STT_SECTION
symbols. However, youcan reveal these special symbols using the --special-syms
option.
1 | % cat a.s |
GNU nm behaves similarly, but with a slight quirk. If the default BFDtarget isn't AArch32, mapping symbols are displayed even without--special-syms
.
1 | % arm-linux-gnueabi-nm a.o |
Mapping symbols, being non-unique and lacking descriptive names, areintentionally omitted by symbolizers like addr2line and llvm-symbolizer.Their primary role lies in guiding the disassembly process rather thanproviding human-readable context.
While mapping symbols are useful, they can significantly inflate thesymbol table, particularly in 64-bit architectures(sizeof(Elf64_Sym) == 24
) with larger programs. This issuebecomes more pronounced when using-ffunction-sections -fdata-sections
, which generatesnumerous small sections.
1 | % cat a.c |
Except the trivial cases (e.g. empty section), in both GNU assemblerand LLVM integrated assembler:
$d
.$x
.ABI requires a mapping symbol at offset 0.The behaviors ensure that each function or data symbol has acorresponding mapping symbol, while extra mapping symbols might occur inrare cases. Thereore, the number of mapping symbols in the output symboltable usually exceeds 50%.
Most text sections have 2 or 3 symbols:
STT_FUNC
symbol.STT_SECTION
symbol due to a referenced from.eh_frame
. This symbol is absent if-fno-asynchronous-unwind-tables
.$x
mapping symbol.During the linking process, the linker combines input sections andeliminates STT_SECTION
symbols.
I have proposed an alternaive scheme to address the size concern.
$x
at offset 0. Addan ending $x
if the final data isn't instructions.$d
at offset 0.Add an ending $d
only if the final data isn't datacommands.This approach eliminates most mapping symbols while ensuring correctdisassembly. Here is an illustrated assembler example:
1 | .section .text.f0,"ax" |
The ending mapping symbol is to ensure the subsequent section in thelinker output starts with the desired state. The data in code case isextremely rare for AArch64 as jump tables are usually placed in.rodata
.
Experiments with a Clang build using this alternative scheme haveshown impressive results, eliminating over 50% of symbol tableentries.
1 | .o size | build | |
1 | % bloaty a64-2/bin/clang -- a64-0/bin/clang |
However, omitting a mapping symbol at offset 0 for sections withinstructions is currently non-conformant. An ABI update has been requestedto address this.
Some interoperability issues might arise, but a significant portionof users don't care.
particularly when linking text sections with trailing data assembledusing the traditional behavior, or when a linker script combinesnon-text and text sections. These scenarios could potentially confusedisassemblers.
There are some interop issues that a significant portion of usersdon't care.
In a text section with trailing data assembled using the traditionalbehavior, the last mapping symbol will be $d
. During thelinking process, if the subsequent section lacks an initial$x
due to the new behavior, the result could confusedisassemblers.
In addition, a linker script combining non-text sections and textsections might confuse disassemblers.
1 | SECTIONS { |
In conclusion, the proposed alternative scheme solves the symboltable bloat problem, but it requires careful consideration of complianceand interoperability. An opt-in assembler option might be useful. Withthis optimization in place, the remaining symbols would primarilyoriginate from range extension thunks, prebuilt libraries, or highlyspecialized assembly implementations.
When lld creates an AArch64range extension thunk, it defines a $x
symbol tosignify the A64 state. This symbol is only relevant when the precedingsection ends with the data state, a scenario that's only possible withthe traditional assembler behavior.
Given the infrequency of range extension thunks, the $x
symbol overhead is generally tolerable.
In contrast to LLVM's integrated assembler, which restricts statetransitions to instructions and data commands, GNU assembler introducesadditional state transitions for alignments. These alignments can beeither implicit (arising from alignment requirements) or explicit(specified through directives). This behavior has led to someinteresting edge cases and bug fixes over time. (See related code beside[PATCH][GAS][AARCH64]Fix"align directive causes MAP_DATA symbol to be lost"https://sourceware.org/bugzilla/show_bug.cgi?id=20364)
1 | .section .foo1,"a" |
In the example, .foo1
only contains data directives andthere is no $d
. However, .foo2
includes analignment directive, triggering the creation of a $d
symbol. Interestingly, .foo3
starts with data but ends withan instruction, necessitating both a $d
and an$a
mapping symbol.
It's worth noting that DWARF sections, typically generated by thecompiler, don't include explicit alignment directives. They behavesimilarly to the .foo1
example and lack an associated$d
mapping symbol.
RISC-V mapping symbols are similar to AArch64, but with a notableextension:
1 | $x<ISA> | Start of a sequence of instructions with <ISA> extension. |
The alternative scheme for optimizing symbol table size can beadapted to accommodate RISC-V's $x<ISA>
symbols. Theapproach remains the same: add an ending $x<ISA>
onlyif the final data in a text section doesn't belong to the desiredISA.
The alternative scheme can be adapted to work with$x<ISA>
: Add an ending $x<ISA>
ifthe final data isn't of the desired ISA.
This adaptation works seamlessly as long as all relocatable filesprovided to the linker share the same baseline ISA. However, inscenarios where the relocatable files are more heterogeneous, a crucialquestion arises: which state should be restored at section end? Wouldthe subsequent section in the linker output be compiled with differentISA extensions?
LC_DATA_IN_CODE
load commandIn contrast to ELF's symbol pair approach, Mach-O employs theLC_DATA_IN_CODE
load command to store non-instructionranges within code sections. This method is remarkably compact, witheach entry requiring only 8 bytes. ELF, on the other hand, needs twosymbols ($d
and $x
) per data region, consuming48 bytes (in ELFCLASS64) in the symbol table.
1 | struct data_in_code_entry { |
In llvm-project, the possible kind
values are defined inllvm/include/llvm/BinaryFormat/MachO.h
. I recentlyrefactored the generic MCAssembler
to place this Mach-Ospecific thing, alongside others, to MachObjectWriter
.
1 | enum DataRegionType { |
Given ELF's symbol table bloat due to the st_size
member(myprevious analysis), how can it attain Mach-O's level of efficiency?Instead of introducing a new format, we can leverage the standard ELFfeature: SHF_COMPRESSED
.
Both .symtab
and .strtab
lack theSHF_ALLOC
flag, making them eligible for compressionwithout requiring any changes to the ELF specification.
The implementation within LLVM shouldn't be overly complex, and I'mmore than willing to contribute if there's interest from thecommunity.