.a archives
Unix-like systems represent static libraries as .a
archives. A .a
archive is a header and a collection of .o files (relocatable object files) and their metadata. One may add others files to .a
but that is almost assuredly a bad thing. As a special case, ar r a.a b.a
merge members of b.a
into a.a
.
The original linker designers noticed that for many programs not every member was needed, so they invented the interesting and confusing archive member extraction rule. See Symbol processing#Archive processing for details.
Disk seeks and parsing the symbol table for each member was slow, so precursors added an index (symbol table) to the archive. The index is a list of (name, member) pairs for defined non-local symbols. The index is represented as a special member of the archive. Its name varies on different systems (no name, /SYM64
, __.SYMDEF
, __.SYMDEF_64
, etc).
GNU ld does not accept an archive without an index:
1 | % ld.bfd a.o a.a |
Thin archives
In 2008-03, Cary Coutant (then at Google) posted PATCH: Add support for "thin" archives. Here is the key insight:
At other times, I've received requests from customers who are using archive libraries as intermediate collections of .o files during a build of a large project. In this usage model, the archives aren't intended for distribution outside that project; they're just used during the build. In these situations, the .o files will remain where they are in the build directories, and the copying of the files into the archive libraries is a waste of time and space -- useful only because the archive library serves as a useful collection mechanism to simplify the later link command.
A thin archive does not copy the member contents. It just stores the filename (which is typically relative). While a regular archive (let's call it a trick archive) is self-contained, a thin archive needs to reference files. This is perfectly fine for throw-away archives created as intermediate build artifacts. If one distributes static libraries, a thich archive should be the choice as a thin archive needs the referenced files, which is cumbersome.
A thin archive has the nice property that it is transparent. When the referenced files exist, a thin archive is indistinguishable from a thick archive from the view of some tools (ld, make). Some, however, may consider this a disadvantage as they need to run some command (e.g. file
) to know whether an archive is trick or thin, when distributing a static library. I think the transparent design philosophy might be the reason a thin archive reuses the .a
suffix name.
In the thin archive patch for GNU ar, the modifier T
is picked to create a thin archive. I got to know the portability issue when emaste from FreeBSD created [META] Make llvm-ar a drop-in replacement for BSD ar and noted that in FreeBSD /usr/bin/ar
, T
means Use only the first fifteen characters of the archive member name or command line file name argument when naming archive members.
So I researched a bit. X/Open System Interface (XSI) says
Allow filename truncation of extracted files whose archive names are longer than the file system can support. By default, extracting a file with a name that is too long shall be an error; a diagnostic message shall be written and the file shall not be extracted.
The text dated at least as early as 2004. Many implementations including elfutils ar (2007-02), FreeBSD ar, and macOS ar have adopted this interpretation. This modifier T
is, however, quite obscure.
Nevertheless, I think for binutils ar, adding --thin
and educating projects to use the long option will be best for portability. binutils 2.38 will include my submitted patch. In the NEWS I call T
deprecated without diagnostics. It is unclear when a diagnostic can be issued. The Linux kernel has used the thin archive -T
semantics since 2016-09: kbuild: allow architectures to use thin archives instead of ld -r.
I have a pending patch [llvm-ar] Add --thin for creating a thin archive. Hopefully it will land before the 14.0.0 release.
Note: linking thin archives may be slower because thick archives has better disk locality. The linker being asynchronous or parallel on reading input files may alleviate the problem. Thick archives may be slower because an object file costs 2x of buffer cache.
--start-lib
In 2011-03, Cary Coutant posted [gold patch] incremental 5/18: support --start-lib/--end-lib.
If a.a
contains b.o c.o
, ld ... --start-lib b.o c.o --end-lib
works like ld ... a.a
.
The apparent pros of --start-lib
over thin archives are:
- The build system can skip building archives.
- We don't waste archiver's time on building the thin archive index.
- No thick and thin archive confusion.
Cons:
- The linker command line built by the compiler driver is longer.
It is worth elaborating on the archive index. Due to the archive has no index; run ranlib to add one
GNU ld diagnostic, every archive needs an index. To build the index, for a member of an ELF relocatable object file, ar needs to parse its symbol table and find the non-local definitions. For a member of a GCC LTO object file or an LLVM bitcode file, ar needs a plugin to parse the symbol table. A build system mistake is to mix LTO files and an ELF relocatable object file in one archive. The archive still has an index, but the definition list is incomplete, and there may be weird "undefined reference" errors.
--start-lib
and --end-lib
allow us to stop wasting time/space for the index and worrying about the LTO case.
I created a feature request for GNU ld in 2019: ld: Support --start-lib --end-lib.
String interning
In a typical linker implementation, the linker interns symbol names in an archive index and a .o
symbol table. For every definition in an extracted archive member, we intern it twice, once for the archive index, once for the .o
symbol table after extraction. If we ignore the index, we just need to intern the symbol once.
ld64.lld
I have a pending patch adding --start-lib
and --end-lib
: https://reviews.llvm.org/D116913. The feature request suggests that we can replace the pair of options with one single -objlib
: -objlib a.o -objlib b.o
. I prefer --start-lib
and --end-lib
mainly because they convey more information.
--start-lib a.o --end-lib --start-lib b.o --end-lib
means thata.o
andb.o
belong to different virtual libraries.--start-lib a.o b.o --end-lib
means thata.o
andb.o
belong to the same virtual library.
In ld.lld, this information has usage in --warn-backrefs
, as described by Dependency related linker options in detail. I do find an immediate use case for ld64.lld but just think this flexibility will turn out to be useful (e.g. if we assign a virtual library a name).
Thin archives without index
--start-lib
and --end-lib
have poor support in build systems. CMake doesn't support them. On the other hand, thin archives without an index are quite good alternatives. We still pay the filename (and other metadata) size costs in the archive member, but otherwise ar does lightweight work.
Once llvm-ar supports --thin
, I plan to use -DCMAKE_CXX_ARCHIVE_CREATE='path/to/llvm-ar crS --thin <TARGET> <OBJECTS>' -DCMAKE_CXX_ARCHIVE_FINISH=:
for my llvm-project builds.
-DCMAKE_CXX_ARCHIVE_FINISH=:
is to (a) work around a llvm-ranlib bug that converts a thin archive without an index into a thick archive and (b) avoid ranlib costs. Many ar implementations ensure the index is up-to-date automatically, no need for a separate ranlib command.
I have a pending ld.lld patch (D117284) to drop the archive has no index; run ranlib to add one
diagnostic. Normally ld.lld tends to be strict. Dropping a diagnostic makes us loose and I tend to think of ecosystem influence: users may get addicted to the behavior and build archives not working with GNU ld and gold. Since the repair is a straightforward ranlib command, I think this addiction is not bad.