Analysis and introspection options in linkers
Table of Contents
Updated in 2026-01.
Reproduce tarball
LLD offers a convenient feature to bundle all input files into a tarball, making it easier to experiment with different linker options. Use either of these commands:
clang -fuse-ld=lld -Wl,--reproduce=/tmp/rep.tar a.o b.o
LLD_REPRODUCE=/tmp/rep.tar clang -fuse-ld=lld a.o b.o
Then unpack the tarball, navigate to the directory, and invoke LLD with the response file:
cd /tmp; tar xf rep.tar; cd rep
ld.lld @response.txt # append options like -y foo
The response file includes the --chroot option, which GNU ld does not support.
In most cases, you can simply remove this option to examine GNU ld's behavior.
ld.bfd @response.txtSymbol related
--trace-symbol=<sym>
Alias: -y sym
Print files which define or reference the specified non-local symbol. There are three types of definitions: definition in a relocatable object file, definition in a shared object, definition in a lazy object file/archive.
% ld.lld -y foo a1.a a.o a2.so
a1.a(a1.o): lazy definition of foo
a.o: reference to foo
a1.a(a1.o): definition of foo
a2.so: shared definition of fooFile related
--trace
Alias: -t
Print processed files: relocatable object files, shared objects, and extracted lazy object files/archive members. Note that unextracted lazy object files/archive members are not printed.
The GNU ld behavior is a bit strange. You need to specify -t twice to get both the archive name and the member name.
% ld.lld -t a.o b.so c.a
a.o
b.so
c.a(1.o)
% ld.bfd -t a.o b.so c.a
a.o
b.so
c.a
% ld.bfd -t -t a.o b.so c.a
a.o
b.so
(c.a)1.oArchive related
--print-archive-stats=<file>
gold introduced --print-symbol-counts=<file> in 2008-06. The output includes the member count and extracted member count for each archive.
I added --print-archive-stats=<file> to ld.lld to dump the archive information, but in a tab-separated format.
% ld.lld a.o aweak.a a1.a a1.a --print-archive-stats=-
members extracted archive
1 0 aweak.a
3 2 a1.a
3 0 a1.a--why-extract=<file>
Print why each archive member/lazy object file is extracted. I added the option to ld.lld 14. The output is in a tab-separated format.
% ld.lld main.o a_b.a b_c.a c.a -o /dev/null --why-extract=- | tee stdout
reference extracted symbol
main.o a_b.a(a_b.o) a
a_b.a(a_b.o) b_c.a(b_c.o) b()
b_c.a(b_c.o) c.a(c.o) c()
It is easy to track a chain of references to one archive member:
% ruby -ane 'BEGIN{p={}}; p[$F[1]]=[$F[0],$F[2]] if $.>1; END{x="c.a(c.o)"; while y=p[x]; puts "#{y[0]} extracts #{x} to resolve #{y[1]}"; x=y[0] end}' stdout
b_c.a(b_c.o) extracts c.a(c.o) to resolve c()
a_b.a(a_b.o) extracts b_c.a(b_c.o) to resolve b()
main.o extracts a_b.a(a_b.o) to resolve a
ld64 has a similar option named -why_load.
-Map=<file>
Print a link map to the file. To print to stdout, use -M.
The output includes output sections addresses, file offsets, input section to output section mapping, and symbol assignments.
GNU ld prints additional information: Archive member included to satisfy reference by file (symbol), Discarded input sections (similar to --print-gc-sections), Allocating common symbols, Memory Configuration (related to memory regions), and Linker script and memory map (similar to -t).
If the option value is a directory or a name with %, the map filename will be constructed from the output filename.
-o foo.exe -Map=bar [Creates ./bar]
-o ../dir/foo.exe -Map=bar [Creates ./bar]
-o foo.exe -Map=../dir [Creates ../dir/foo.exe.map]
-o ../dir2/foo.exe -Map=../dir [Creates ../dir/foo.exe.map]
-o foo.exe -Map=% [Creates ./foo.exe.map]
-o ../dir/foo.exe -Map=% [Creates ../dir/foo.exe.map]
-o foo.exe -Map=%.bar [Creates ./foo.exe.bar]
-o ../dir/foo.exe -Map=%.bar [Creates ../dir/foo.exe.bar]
-o ../dir2/foo.exe -Map=../dir/% [Creates ../dir/../dir2/foo.exe.map]
-o ../dir2/foo.exe -Map=../dir/%.bar [Creates ../dir/../dir2/foo.exe.bar]
Some folks want a JSON output format with some stability guarantee.
Cross references
--cref
Print a cross reference table.
For each non-local defined or shared symbol, print the defined file on the first line and and referencing files in subsequent lines.
The format is a bit wasteful because there are 50 bytes before the File column.
% ld.lld --cref a1.so a2.o a3.o a.a
Cross Reference Table
Symbol File
foo a1.so
a2.o
a3.o
bar a2.o
a3.o
_start a3.o
baz a3.o
zed a.a(aa.o)
a3.o
If -Map is specified, print along with the link map to the file.
I find this behavior a bit unfortunate, because:
- the information is independent from other pieces of
-Map - both pieces of information has large amount of output. Merging them makes the link action slower if the user just needs one piece of information
--print-dependencies
mold recently added the option in an alternative format to --cref.
# This is an output of the mold linker's --print-dependencies=full option.
#
# Each line consists of 4 fields, <input-section>, <output-section>,
# <symbol-type> and <symbol>, separated by tab characters. It indicates that
# <input-section> depends on <output-section> to use <symbol>. <symbol-type>
# is either "u" or "w" for regular or weak undefined, respectively.
#
# If you want to obtain dependency information per function granularity,
# compile source files with the -ffunction-sections compiler flag.
a3.o:(.text) a1.so u foo
a3.o:(.text) a2.o:(.text) u bar
a3.o:(.text) a.a(aa.o):(.text) u zed-why_live sym
ld64 manpage says: Logs a chain of references to symbol_name. Only applicable with -dead_strip . It can help debug why something that you think should be dead strip removed is not removed.
This can usually be approximated by ld.lld --why-extract=-.
% ld.lld main.o a_b.a b_c.a c.a -o /dev/null --why-extract=- | tee stdout
reference extracted symbol
main.o a_b.a(a_b.o) a
a_b.a(a_b.o) b_c.a(b_c.o) b()
b_c.a(b_c.o) c.a(c.o) c()
% ruby -ane 'BEGIN{p={}}; p[$F[1]]=[$F[0],$F[2]] if $.>1; END{x="c.a(c.o)"; while y=p[x]; puts "#{y[0]} extracts #{x} to resolve #{y[1]}"; x=y[0] end}' stdout
b_c.a(b_c.o) extracts c.a(c.o) to resolve c()
a_b.a(a_b.o) extracts b_c.a(b_c.o) to resolve b()
main.o extracts a_b.a(a_b.o) to resolve a
--why-live=<sym>
LLD's ELF port has implemented this option in March 2025.
Statistics
--stats
% ld.bfd @response.txt --stats
ld.bfd: total time in link: 9.879890
% mold @response.txt --stats
total_input_bytes=1161772844
reloc_alloc=3983368
all_syms=1185762
defined_syms=747420
input_sections=692453
undefined_syms=397776
regular_sections=319069
merged_strings=214750
num_fdes=169858
removed_comdat_mem=140402
comdats=131977
dso_syms=10875
parsed_objs=3099
num_objs=2677
num_cies=2665
output_chunks=36
num_dsos=10
num_unique_cies=2
string_fragments=0
reloc_nonalloc=0
.rodata.cst estimation=5425 actual=4689
.rodata.str estimation=240124 actual=210057
.comment estimation=1628 actual=4--time-trace
% ld.lld @response.txt --time-trace -o clang
% jq -r '.traceEvents[] | select(.name|contains("Total")) | "\(.dur/1000000) \(.name)"' < clang.time-trace
1.099133 Total ExecuteLinker
1.064616 Total Link
0.303137 Total Write output file
0.295151 Total Write sections
0.220898 Total Scan relocations
0.183298 Total Parse input files
0.06039 Total Merge/finalize input sections
0.0377 Total Add local symbols
...
% jq -r '.traceEvents[] | select(.name|contains("Write")) | "\(.dur/1000000) \(.name) \(.args)"' < clang.time-trace
0.130966 Write sections {"detail":".rela.dyn"}
0.001995 Write sections {"detail":".rela.plt"}
0.007248 Write sections {"detail":".dynsym"}
0.000967 Write sections {"detail":".gnu.hash"}
0.01489 Write sections {"detail":".hash"}
0.003147 Write sections {"detail":".dynstr"}
0.034617 Write sections {"detail":".rodata"}
0.030412 Write sections {"detail":".eh_frame"}
0.02487 Write sections {"detail":".text"}
.../summary
lld-link /summary dumps PDB information and the cumulative size of all input OBJ files.
mold --perf
% mold @response.txt --perf
User System Real Name
4.161 0.533 0.254 all
2.077 0.302 0.139 read_input_files
2.100 0.235 0.114 total
1.537 0.129 0.071 before_copy
0.000 0.000 0.000 apply_exclude_libs
0.142 0.051 0.007 do_resolve_symbols
0.142 0.051 0.007 do_resolve_symbols
0.245 0.012 0.013 register_section_pieces
0.002 0.000 0.001 eliminate_comdats
0.002 0.000 0.000 convert_common_symbols
...