Bonus: Make Your Own Library — Static vs Dynamic Linking¶
Tools: GCC, ar, ldd, nm, objdump, size, /usr/bin/time
Goal¶
Turn the string functions you wrote earlier into a real library, ship it in both a static and a shared flavour (libmystringstatic.a and libmystringdyn.so), link the same program against each, and measure what the difference actually costs.
At the end you will have built by hand the two things -lc has been silently giving you since your first printf().
Background¶
A static library (.a) is an archive: a bag of .o files with an index, closer to a .tar than to a program.
At link time the linker copies the code you use out of it and into your executable.
A shared library (.so) stays a separate file.
It may be mapped at a different address in every process that loads it, so its code must be position-independent (-fPIC) and its calls must go through a level of indirection that the dynamic loader fills in at run time.
Both cost something. Which one costs more depends entirely on what you measure.
Your Task¶
-
Copy your solution from the string-functions exercise into this directory:
main.cand theMakefileare already here and need no changes.main.ccalls your functions in a tight loop and times them;./main_x 0does no work at all, which measures start-up only. -
Build
libmystringstatic.aby hand: compilemystring.cto an object file, thenar rcsit into an archive. Inspect the archive withar tandar x. -
Build
libmystringdyn.soby hand: compile with-fPIC, then link with-shared. -
Link
main.cthree times: against the.a, against the.so, and fully statically (-static, libc included).makedoes all of this; do it manually first, then read theMakefileto compare. The two libraries have different names, so-lmystringstaticand-lmystringdyneach pick exactly one file. -
Run the dynamically linked build directly, without setting anything. It will fail. Understand the error before you fix it, then fix it in all three ways:
LD_LIBRARY_PATH,-Wl,-rpath,'$ORIGIN', and installing the library system-wide. -
Inspect what the linker produced:
Look at the
sizeoutput, thelddoutput, whethermy_strlenis an undefined symbol, and how the call tomy_strlenis encoded in each binary. -
Measure, twice:
Build & Run¶
make # build all three executables
make run-dynamic # runs main_dynamic with LD_LIBRARY_PATH set
make clean
ITERS and RUNS can be overridden: make bench ITERS=50000000.
Check Your Work¶
make benchinterleaves the three binaries and repeats five times on purpose: the effect being measured is smaller than the run-to-run noise on a normal desktop. Do not draw a conclusion from a single pair of runs. A claim is only safe if the ranges for two binaries do not overlap.- Expect the per-call difference between the static and the dynamic build to be a small number of nanoseconds — on the order of a couple of clock cycles, not a factor of two. If you measure a large ratio, something else is going on; find it before believing it.
- Expect the start-up difference to be in the hundreds of microseconds per process, i.e. many orders of magnitude larger per process than the per-call difference. Divide one by the other: how many calls must a program make before the per-call overhead even matches what it paid to load the library? Whether that number is large or small is the actual lesson here.
- From
make inspect, the two disassembled call sites should differ in exactly one visible way. Be able to name it and say who fills in the missing address. - The
sizecolumn should show one binary vastly larger than the other two. Be sure you can say what the extra bytes are. - Bring your numbers and your reading of them to the teaching assistant. Static linking wins both timing measurements — yet nearly everything on your system is dynamically linked. Be ready to explain why that is not a contradiction.