C / MULTI-FILE PROGRAMS AND BUILDS
Static libraries with ar and ranlib
Package object files into a .a archive with ar, keep its symbol index valid with ranlib, and link against it knowing which members get pulled in.
What you will learn
- Create and update an archive with ar rcs, and list its members with ar t
- Explain why only the members that resolve pending symbols get linked in
- Order -L and -l flags so each library follows everything that references it
- Fix a missing or stale archive index with ranlib (equivalently, ar s)
Understanding Static libraries with ar and ranlib
An archive built by ar is not compiled or linked code, it is a container. Each member is a byte-for-byte copy of an object file preceded by a small header holding its name, timestamp, mode and size, and one special member lists every global symbol the objects define together with the offset of the member that defines it. The command ar rcs libstats.a mean.o range.o does both jobs at once: r replaces or inserts the named members, c creates the archive without complaining that it did not already exist, and s writes that symbol index. On GNU binutils, ranlib libstats.a is the same operation as ar s libstats.a; it survives as a separate program because older ar implementations did not maintain the index themselves.
Linking against an archive behaves differently from naming object files. The linker walks its inputs left to right carrying a set of symbols that are referenced but not yet defined; when it reaches libstats.a it consults the index, extracts only the members that define something currently in that set, adds whatever new undefined symbols those members bring with them, and rescans the same archive until nothing more is needed. Then it moves on and never looks at that archive again. That single left-to-right pass is the entire reason gcc -lstats main.c fails while gcc main.c -lstats works: in the first command nothing is undefined yet when the archive is examined, so every member is skipped.
Two practical consequences follow from members being whole object files. Extraction granularity is per file, not per function, so an object defining twenty functions drags all twenty into your executable when you call one of them, which is why C library sources traditionally keep one public function per file. An archive is also a snapshot: ar copied the bytes of mean.o at the moment you ran it, so recompiling mean.c changes nothing until you run ar r again, and the finished executable holds its own copy of the extracted code with no runtime tie to the .a at all.
/* ---- stats.h ---- */
STATS_H
STATS_H
double stats_mean(const double *v, int n);
double stats_range(const double *v, int n);
/* ---- mean.c ---- */
"stats.h"
double stats_mean(const double *v, int n)
{
double sum = 0.0;
for (int i = 0; i < n; i++)
sum += v[i];
return n > 0 ? sum / n : 0.0;
}
/* ---- range.c ---- */
"stats.h"
double stats_range(const double *v, int n)
{
if (n <= 0)
return 0.0;
double lo = v[0], hi = v[0];
for (int i = 1; i < n; i++) {
if (v[i] < lo) lo = v[i];
if (v[i] > hi) hi = v[i];
}
return hi - lo;
}
/* ---- main.c ---- */
<stdio.h>
"stats.h"
int main(void)
{
double v[] = { 3.0, 1.5, 9.0, 4.5 };
int n = (int) (sizeof v / sizeof v[0]);
printf("mean = %.3f\n", stats_mean(v, n));
printf("range = %.3f\n", stats_range(v, n));
return 0;
}
/* Build and run:
* gcc -c mean.c range.c
* ar rcs libstats.a mean.o range.o
* ranlib libstats.a # same thing as: ar s libstats.a
* ar t libstats.a # the only command here that prints anything
* gcc main.c -L. -lstats -o app
* ./app
*/
A static library is a plain archive of object files plus a symbol index, and the linker copies out whole members on demand instead of merging the entire .a into your program.
Worked examples
Unused members are never extracted
A deliberately broken object inside the archive does not break the link, because the linker never needs it.
/* ---- good.c ---- */
<stdio.h>
void used(void) { puts("used() was pulled out of libdemo.a"); }
/* ---- bad.c : never referenced, and deliberately broken ---- */
void missing_helper(void); /* declared, defined nowhere */
void unused(void) { missing_helper(); }
/* ---- app.c ---- */
void used(void);
int main(void) { used(); return 0; }
/* Build and run:
* gcc -c good.c bad.c
* ar rcs libdemo.a good.o bad.o
* gcc app.c -L. -ldemo -o app # links cleanly
* ./app
*
* Now compare naming the objects instead of the archive:
* gcc app.c good.o bad.o -o app
* -> undefined reference to `missing_helper'
*/
Example explained
Line 1void missing_helper(void); is only a declaration, so bad.o really does carry an unresolved reference.
Line 2app.c's single undefined symbol is used, which the index maps to good.o, so only good.o is copied out.
Line 3bad.o never enters the output, so missing_helper is never added to the undefined set and the link succeeds.
Line 4Listing bad.o directly removes the linker's choice: it must include it, and the missing symbol becomes an error.
One member calling another
The symbol index lets the linker rescan the same archive to satisfy symbols introduced by a member it just extracted.
/* ---- temp.h ---- */
TEMP_H
TEMP_H
void print_report(double celsius);
/* ---- report.c ---- */
<stdio.h>
"temp.h"
double to_fahrenheit(double c); /* lives in a different member */
void print_report(double c)
{
printf("%.1fC = %.1fF\n", c, to_fahrenheit(c));
}
/* ---- convert.c ---- */
double to_fahrenheit(double c) { return c * 9.0 / 5.0 + 32.0; }
/* ---- main.c ---- */
"temp.h"
int main(void) { print_report(21.5); return 0; }
/* Build and run (convert.o is stored BEFORE report.o on purpose):
* gcc -c report.c convert.c
* ar rcs libtemp.a convert.o report.o
* gcc main.c -L. -ltemp -o report
* ./report
*/
Example explained
Line 1main.c only asks for print_report, so the linker extracts report.o and gains a new undefined symbol, to_fahrenheit.
Line 2It queries the index of the same archive again, finds to_fahrenheit in convert.o, and extracts that member too.
Line 3This rescan is confined to one archive, so member order inside libtemp.a does not matter while the position of -ltemp on the command line does.
Line 4Strip the index (ar rcS, or ar q which does not update it) and the linker has no way to answer that query; it typically stops with an error telling you to run ranlib.
Replacing a member after an edit
Shows that the archive stores a copy, so recompiling a source has no effect until ar r replaces the member.
/* ---- greet.c, first version ---- */
<stdio.h>
void greet(void) { puts("version 1"); }
/* ---- use.c ---- */
void greet(void);
int main(void) { greet(); return 0; }
/* Build and run:
* gcc -c greet.c
* ar rcs libgreet.a greet.o
* gcc use.c -L. -lgreet -o use && ./use
*
* Now edit greet.c so it prints "version 2", then:
* gcc -c greet.c # greet.o is new, libgreet.a is not
* gcc use.c -L. -lgreet -o use && ./use
* ar r libgreet.a greet.o # replace the stale member
* gcc use.c -L. -lgreet -o use && ./use
*/
Example explained
Line 1The second run still prints version 1 because the executable was built from the copy of greet.o held inside the archive.
Line 2ar r overwrites that member with the freshly compiled object and updates the index in the same step.
Line 3Only the relink after ar r produces a program containing version 2, which is why makefile rules for a .a list ar as an action, not just the compiler.
Important notes
Because a pass over an archive is never revisited, dependencies between archives must be spelled out in order: -lapp before -lcore if libapp.a calls into libcore.a, otherwise repeat one of them on the command line.
Extraction is per object file, so an unused function sharing a .o with a used one still lands in your executable unless you compile with -ffunction-sections and link with -Wl,--gc-sections.
Common mistakes
Writing gcc -L. -lstats main.c. The archive is examined before main.c contributes any references, so nothing is undefined, no member is extracted, and you get 'undefined reference to stats_mean' while staring at a library that obviously contains it.
Recompiling mean.c but forgetting ar r libstats.a mean.o. The archive still holds the previous member, the program keeps its old behaviour, and you debug source code that is not in the binary.
Expecting -lstats to look in the current directory. Without -L. (or just naming ./libstats.a on the command line) the linker searches only its default paths and reports that it cannot find -lstats.
Try it yourself
Change, predict, then run
Split a two-function string helper into upper.c and reverse.c, build libstr.a with ar rcs, and link a main that calls only str_upper. Then run ar t libstr.a to confirm reverse.o is still stored, and move -lstr in front of your .c file to see exactly what the single-pass linker reports.
Open the C workspaceCheck your understanding
libutil.a holds util.o (which defines helper) and broken.o (which calls a function defined nowhere). main.c calls only helper. Why does gcc main.c -L. -lutil link successfully?
- A member is extracted only to satisfy a symbol that is currently undefined, so broken.o is never included and its unresolved call is never examined.
- ranlib leaves members with unresolved references out of the symbol index it builds.
- Unresolved symbols inside a static archive are resolved lazily when the program starts running.
- ar checks every object as it is added and refuses ones with missing definitions.
Show answer
Extraction is driven purely by the linker's set of still-undefined symbols. Nothing in main.c asks for anything defined in broken.o, so those bytes never reach the executable and its missing function never becomes anyone's problem. Option 3 is the tempting one, but a static link produces a self-contained copy of the extracted members with nothing left to resolve at run time; if main.c did call broken.o's function, the failure would appear at link time instead.