Fun With Static Libraries

Posted on Aug 23, 2021

Welcome to another episode of Ariadna shows you weird (SFW, though) stuff! It’s sunny here in Pamplona, which is quite nice considering this is the cold and bitter North, and after some days I was feeling sort of in need of disconnecting from programming and FOSS-y things, I’m back with a topic I’ve wanted to write about for quite some time… Static libraries!

Warning I’m adding after reading this post myself: LOOOOONG POST!

OK, why?

Let me start with a disclaimer, a positive one. I don’t have any strong opinion against dynamic libraries. I am aware that dynamic libraries bring way less benefits than people think, especially when compared against static libraries, but I don’t worry too much about the fact that dynamic libraries are the standard way of doing things nowadays. I use Arch, everything is based on them in this distro. On the other hand, I find static libraries to be much more fun to develop, test, and use in general for someone like me.

Ah, and more convenient when developing simple things, which I will explain in a bit… Well, actually towards the end of this… long… long… OK, you get the idea!

What Is A Static Library?

You might recognize static libraries as files that look like this: libfoo.a, compared to dynamic (shared) libraries, which look like: libbar.so.1 If we run file on a static library vs. a dynamic one, this is what we get:

$ file /usr/lib/libm-2.33.a
/usr/lib/libm-2.33.a: current ar archive
$ file /usr/lib/libm-2.33.so
/usr/lib/libm-2.33.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, BuildID[sha1]=2b8fd1f869ecab4e0b55e92f2f151897f6818acf, for GNU/Linux 4.4.0, stripped

If you have some knowledge about building libraries, you know what’s going on here and you may skip this paragraph. Dynamic libraries are built precisely the same way as executables are. In fact, actually, executables in modern Linux are rather a special case of dynamic libraries; if you run file on any executable, you’ll see exactly the same output as the one we get for a dynamic library. On the other hand, to build a static library, what you do is to grab all the object files you compiled and glue them all together into an ar archive, prepending an index to it so that it’s easy for the linker to know what’s inside when you use that library. The ar format is the actual POSIX archiving format, predating both the tar (tape ar, because it was meant for archiving to tape) and cpio archive formats. Yup, you could use ar to archive whatever other stuff you’d like to… it’s in no way limited to building static libraries.

So far, no big deal, right? OK, now let us read some hexdumps! First things first, before getting into reading stuff in binary/hexcode. Let’s not jump over our heads, shall we?

You might be very well aware that The Right Way To Compile Things(tm) when you’ve got a codebase with more than one module is to split compilation into the modules that need to be recompiled, and then relinking everything back together. This is probably the reason why we all use Makefiles, right? If I build, let’s say, sline, this is the result: 2

$ make
Build options:
CPPFLAGS = -DVERSION="0.5.0" -D_POSIX_C_SOURCE=200809L
CFLAGS   = -std=c99 -Wpedantic -Wall -Wextra
LDFLAGS  =
CC       = cc

cc -std=c99 -Wpedantic -Wall -Wextra -DVERSION=\"0.5.0\" -D_POSIX_C_SOURCE=200809L  -c -o history.o history.c
cc -std=c99 -Wpedantic -Wall -Wextra -DVERSION=\"0.5.0\" -D_POSIX_C_SOURCE=200809L  -c -o sline.o sline.c
cc -std=c99 -Wpedantic -Wall -Wextra -DVERSION=\"0.5.0\" -D_POSIX_C_SOURCE=200809L  -c -o strlcpy.o strlcpy.c
ar -rcs libsline.a history.o sline.o strlcpy.o
$ ls -l *.o
-rw-r----- 1 ari ari  2536 ago 22 13:04 history.o
-rw-r----- 1 ari ari 12408 ago 22 13:04 sline.o
-rw-r----- 1 ari ari  1360 ago 22 13:04 strlcpy.o

If I was to modify one thing on, let’s say, history.c, only history.o is rebuilt:

$ touch history.c
$ make
Build options:
CPPFLAGS = -DVERSION="0.5.0" -D_POSIX_C_SOURCE=200809L
CFLAGS   = -std=c99 -Wpedantic -Wall -Wextra
LDFLAGS  =
CC       = cc

cc -std=c99 -Wpedantic -Wall -Wextra -DVERSION=\"0.5.0\" -D_POSIX_C_SOURCE=200809L  -c -o history.o history.c
ar -rcs libsline.a history.o sline.o strlcpy.o

Now, if we look at an object file using hexdump, this is what we’d see.

$ hexdump -C history.o | head -n 15
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  01 00 3e 00 01 00 00 00  00 00 00 00 00 00 00 00  |..>.............|
00000020  00 00 00 00 00 00 00 00  a8 06 00 00 00 00 00 00  |................|
00000030  00 00 00 00 40 00 00 00  00 00 40 00 0d 00 0c 00  |....@.....@.....|
00000040  55 48 89 e5 89 7d fc 83  7d fc 00 78 0b 8b 05 00  |UH...}..}..x....|
00000050  00 00 00 39 45 fc 7e 07  b8 00 00 00 00 eb 18 8b  |...9E.~.........|
00000060  45 fc 48 98 48 8d 14 c5  00 00 00 00 48 8d 05 00  |E.H.H.......H...|
00000070  00 00 00 48 8b 04 02 5d  c3 55 48 89 e5 8b 05 00  |...H...].UH.....|
00000080  00 00 00 48 98 48 8d 14  c5 00 00 00 00 48 8d 05  |...H.H.......H..|
00000090  00 00 00 00 48 8b 04 02  0f b6 00 84 c0 74 21 8b  |....H........t!.|
000000a0  05 00 00 00 00 83 c0 01  89 05 00 00 00 00 8b 05  |................|
000000b0  00 00 00 00 83 f8 31 7e  08 e8 00 00 00 00 eb 01  |......1~........|
000000c0  90 5d c3 55 48 89 e5 48  83 ec 10 c7 45 fc 01 00  |.].UH..H....E...|
000000d0  00 00 eb 49 48 8b 15 00  00 00 00 8b 45 fc 48 98  |...IH.......E.H.|
000000e0  48 8d 0c c5 00 00 00 00  48 8d 05 00 00 00 00 48  |H.......H......H|

Oh, that’s the ELF signature, which every ELF object, executable, or library has in order to tell the world what it is, right? Now let’s inspect the static library libsline.a. We already know not to expect an ELF file here, but an ar archive… but how does that look like?

$ hexdump -C libsline.a | head -n 35
00000000  21 3c 61 72 63 68 3e 0a  2f 20 20 20 20 20 20 20  |!<arch>./       |
00000010  20 20 20 20 20 20 20 20  30 20 20 20 20 20 20 20  |        0       |
00000020  20 20 20 20 30 20 20 20  20 20 30 20 20 20 20 20  |    0     0     |
00000030  30 20 20 20 20 20 20 20  32 35 34 20 20 20 20 20  |0       254     |
00000040  20 20 60 0a 00 00 00 10  00 00 01 42 00 00 01 42  |  `........B...B|
00000050  00 00 01 42 00 00 01 42  00 00 01 42 00 00 01 42  |...B...B...B...B|
00000060  00 00 01 42 00 00 01 42  00 00 0b 66 00 00 0b 66  |...B...B...f...f|
00000070  00 00 0b 66 00 00 0b 66  00 00 0b 66 00 00 0b 66  |...f...f...f...f|
00000080  00 00 0b 66 00 00 3c 1a  68 69 73 74 6f 72 79 00  |...f..<.history.|
00000090  68 69 73 74 5f 63 75 72  72 00 68 69 73 74 5f 70  |hist_curr.hist_p|
000000a0  6f 73 00 68 69 73 74 5f  65 6e 74 72 79 5f 73 69  |os.hist_entry_si|
000000b0  7a 65 00 68 69 73 74 6f  72 79 5f 67 65 74 00 68  |ze.history_get.h|
000000c0  69 73 74 6f 72 79 5f 6e  65 78 74 00 68 69 73 74  |istory_next.hist|
000000d0  6f 72 79 5f 72 6f 74 61  74 65 00 68 69 73 74 6f  |ory_rotate.histo|
000000e0  72 79 5f 73 65 74 00 73  6c 69 6e 65 5f 65 72 72  |ry_set.sline_err|
000000f0  00 73 6c 69 6e 65 00 73  6c 69 6e 65 5f 65 6e 64  |.sline.sline_end|
00000100  00 73 6c 69 6e 65 5f 65  72 72 6d 73 67 00 73 6c  |.sline_errmsg.sl|
00000110  69 6e 65 5f 73 65 74 75  70 00 73 6c 69 6e 65 5f  |ine_setup.sline_|
00000120  73 65 74 5f 70 72 6f 6d  70 74 00 73 6c 69 6e 65  |set_prompt.sline|
00000130  5f 76 65 72 73 69 6f 6e  00 73 74 72 6c 63 70 79  |_version.strlcpy|
00000140  00 00 68 69 73 74 6f 72  79 2e 6f 2f 20 20 20 20  |..history.o/    |
00000150  20 20 30 20 20 20 20 20  20 20 20 20 20 20 30 20  |  0           0 |
00000160  20 20 20 20 30 20 20 20  20 20 36 34 34 20 20 20  |    0     644   |
00000170  20 20 32 35 33 36 20 20  20 20 20 20 60 0a 7f 45  |  2536      `..E|
00000180  4c 46 02 01 01 00 00 00  00 00 00 00 00 00 01 00  |LF..............|
00000190  3e 00 01 00 00 00 00 00  00 00 00 00 00 00 00 00  |>...............|
000001a0  00 00 00 00 00 00 a8 06  00 00 00 00 00 00 00 00  |................|
000001b0  00 00 40 00 00 00 00 00  40 00 0d 00 0c 00 55 48  |..@.....@.....UH|
000001c0  89 e5 89 7d fc 83 7d fc  00 78 0b 8b 05 00 00 00  |...}..}..x......|
000001d0  00 39 45 fc 7e 07 b8 00  00 00 00 eb 18 8b 45 fc  |.9E.~.........E.|
000001e0  48 98 48 8d 14 c5 00 00  00 00 48 8d 05 00 00 00  |H.H.......H.....|
000001f0  00 48 8b 04 02 5d c3 55  48 89 e5 8b 05 00 00 00  |.H...].UH.......|
00000200  00 48 98 48 8d 14 c5 00  00 00 00 48 8d 05 00 00  |.H.H.......H....|
00000210  00 00 48 8b 04 02 0f b6  00 84 c0 74 21 8b 05 00  |..H........t!...|
00000220  00 00 00 83 c0 01 89 05  00 00 00 00 8b 05 00 00  |................|

Excuse the long shell paste, but I want you to notice a couple of things there. I don’t fully understand the format, but that isn’t needed to see what’s going on here, with respect to static libraries.

  1. The header is different, of course. This isn’t an ELF file.
  2. You should easily there’s a list of symbols (the exported ones) after the header.
  3. After history.o/ there’s what appears to be some metadata and… the ELF header… and exactly the same content we got from history.o’s hexdump.

So far, everything is expected. We have a bunch of object files thrown together, with an index of public, exported symbols for the linker… Seriously, Ariadna, don’t waste our time, OK? We already knew this.

This was necessary… though… Very necessary…

The Static Binary Size Myth Debunked

There’s this very widespread idea that if you compile against a static library, the result is a huge binary because you’re “copying the whole library into your executable.” That is a misconception that probably comes from this very bad idea, namely comparing the sizes of an executable compiled against the dynamic version of glibc vs. the static version of glibc. I’m using schain here as an example here because it’s a very small program and it doesn’t require any library except the C Standard Library:

schain is 18K built against the glibc dynamic library:

$ make
Build options:
CPPFLAGS = -DVERSION="2.3.0" -D_POSIX_C_SOURCE=200809L
CFLAGS   = -std=c99 -Wpedantic -Wall -Wextra
LDFLAGS  =
CC       = cc

cc -std=c99 -Wpedantic -Wall -Wextra  -DVERSION=\"2.3.0\" -D_POSIX_C_SOURCE=200809L  -c -o date.o date.c
cc -std=c99 -Wpedantic -Wall -Wextra  -DVERSION=\"2.3.0\" -D_POSIX_C_SOURCE=200809L  -c -o schain.o schain.c
cc -std=c99 -Wpedantic -Wall -Wextra  -DVERSION=\"2.3.0\" -D_POSIX_C_SOURCE=200809L  -c -o strlcpy.o strlcpy.c
cc -o schain date.o schain.o strlcpy.o
$ ls -lh schain
-rwxr-x--- 1 ari ari 18K ago 22 13:25 schain

And almost 1M when building it statically against glibc:

$ make
Build options:
CPPFLAGS = -DVERSION="2.3.0" -D_POSIX_C_SOURCE=200809L
CFLAGS   = -std=c99 -Wpedantic -Wall -Wextra
LDFLAGS  = -static
CC       = cc

cc -std=c99 -Wpedantic -Wall -Wextra  -DVERSION=\"2.3.0\" -D_POSIX_C_SOURCE=200809L  -c -o date.o date.c
cc -std=c99 -Wpedantic -Wall -Wextra  -DVERSION=\"2.3.0\" -D_POSIX_C_SOURCE=200809L  -c -o schain.o schain.c
cc -std=c99 -Wpedantic -Wall -Wextra  -DVERSION=\"2.3.0\" -D_POSIX_C_SOURCE=200809L  -c -o strlcpy.o strlcpy.c
cc -o schain date.o schain.o strlcpy.o -static
$ ls -lh schain
-rwxr-x--- 1 ari ari 931K ago 22 13:26 schain

“Oh, static libraries suck! Yeah, this is because you’re copy-pasting the whole of glibc into the executable… and glibc is huge!”

What about this, though?

$ make
Build options:
CPPFLAGS = -DVERSION="2.3.0" -D_POSIX_C_SOURCE=200809L
CFLAGS   = -std=c99 -Wpedantic -Wall -Wextra
LDFLAGS  = -static
CC       = musl-gcc

musl-gcc -std=c99 -Wpedantic -Wall -Wextra  -DVERSION=\"2.3.0\" -D_POSIX_C_SOURCE=200809L  -c -o date.o date.c
musl-gcc -std=c99 -Wpedantic -Wall -Wextra  -DVERSION=\"2.3.0\" -D_POSIX_C_SOURCE=200809L  -c -o schain.o schain.c
musl-gcc -std=c99 -Wpedantic -Wall -Wextra  -DVERSION=\"2.3.0\" -D_POSIX_C_SOURCE=200809L  -c -o strlcpy.o strlcpy.c
musl-gcc -o schain date.o schain.o strlcpy.o -static
$ ls -lh schain
-rwxr-x--- 1 ari ari 71K ago 22 13:27 schain

Yup, statically compiling against MUSL makes the executable just 71K…

Oh, OK… MUSL is known to be much smaller than glibc. But this isn’t just about MUSL being smaller than glibc… If we look at how big the static libraries are, you might wanna rethink that idea that using static libraries is copy-pasting a whole library into your program:

$ ls -lh /usr/lib/libc.a
-rw-r--r-- 1 root root 5,1M may 12 21:18 /usr/lib/libc.a
$ ls -lh /usr/lib/musl/lib/libc.a
-rw-r--r-- 1 root root 2,6M ene 19  2021 /usr/lib/musl/lib/libc.a

So static glibc is actually 5,1M and static MUSL libc is 2,6M… None of the static schain builds get to those numbers, respectively. So, we’re not copy-pasting the whole library in any case! What are we doing, then? OK, to answer that I must take a short detour… It’ll be short, trust me!

A Small But Necessary (Musical?) Intermission!

You surely have experienced in the past, when learning C, that you can’t declare a public symbol more than once across your whole codebase, right? If I copied strlcpy.c into strlcpy_dup.c, without any further modification, and I expected schain to be built including both files, this would happen:

$ make
Build options:
CPPFLAGS = -DVERSION="2.3.0" -D_POSIX_C_SOURCE=200809L
CFLAGS   = -std=c99 -Wpedantic -Wall -Wextra
LDFLAGS  =
CC       = cc

cc -std=c99 -Wpedantic -Wall -Wextra  -DVERSION=\"2.3.0\" -D_POSIX_C_SOURCE=200809L  -c -o date.o date.c
cc -std=c99 -Wpedantic -Wall -Wextra  -DVERSION=\"2.3.0\" -D_POSIX_C_SOURCE=200809L  -c -o schain.o schain.c
cc -std=c99 -Wpedantic -Wall -Wextra  -DVERSION=\"2.3.0\" -D_POSIX_C_SOURCE=200809L  -c -o strlcpy.o strlcpy.c
cc -std=c99 -Wpedantic -Wall -Wextra  -DVERSION=\"2.3.0\" -D_POSIX_C_SOURCE=200809L  -c -o strlcpy_dup.o strlcpy_dup.c
cc -o schain date.o schain.o strlcpy.o strlcpy_dup.o
/usr/bin/ld: strlcpy_dup.o: in function `strlcpy':
strlcpy_dup.c:(.text+0x0): multiple definition of `strlcpy'; strlcpy.o:strlcpy.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
make: *** [Makefile:28: schain] Error 1

That linker error means strlcpy() was found twice, so it doesn’t know which one to use. When compiling, you just cannot have a symbol name defined as non-static more than once across your object files.

Now Back To Our Show…

Let’s build cras. cras uses sline, so when built, the linker is going to include sline into cras, right? We already know that we’re somehow not including all of sline, as we saw that not the whole of glibc or MUSL libc were being included into schain earlier… but what are the criteria, then?

I want you to look at these directory listings though, before building cras.

For cras:

$ ls -l
total 64
-rw-r----- 1 ari ari  564 may  8 01:21 config.def.h
-rw-r----- 1 ari ari  564 may  8 01:07 config.h
-rw-r----- 1 ari ari  386 ago 18 16:31 config.mk
-rw-r----- 1 ari ari 3266 ago 11 22:21 cras.1
-rw-r----- 1 ari ari 8266 ago 19 09:44 cras.c
-rw-r----- 1 ari ari 1669 jun 20 14:09 date.c
-rw-r----- 1 ari ari  178 jun 19 19:19 date.h
-rw-r----- 1 ari ari 1205 ago 19 09:44 LICENSE
-rw-r----- 1 ari ari  952 ago  3 22:18 Makefile
-rw-r----- 1 ari ari 1600 ago  3 22:18 README.md
-rw-r----- 1 ari ari 1510 abr 10 02:18 strlcpy.c
-rw-r----- 1 ari ari  115 abr 10 02:18 strlcpy.h
-rw-r----- 1 ari ari 3911 ago 19 09:44 tasklst.c
-rw-r----- 1 ari ari  823 ago  8 21:56 tasklst.h

For sline:

$ ls -l
total 56
-rw-r----- 1 ari ari  323 ago 11 22:05 config.mk
-rw-r----- 1 ari ari  785 ago 22 13:06 history.c
-rw-r----- 1 ari ari  311 jul 22 00:32 history.h
-rw-r----- 1 ari ari 1078 jul 25 09:36 LICENSE
-rw-r----- 1 ari ari 1658 ago 11 16:01 Makefile
drwxr-x--- 2 ari ari 4096 ago 11 16:05 man
-rw-r----- 1 ari ari 1939 jul 23 02:14 README.md
-rw-r----- 1 ari ari 8837 ago 11 16:01 sline.c
-rw-r----- 1 ari ari  439 ago 11 16:01 sline.h
-rw-r----- 1 ari ari  818 ago 11 16:01 sline_test.c
-rw-r----- 1 ari ari 1510 jul 21 18:52 strlcpy.c
-rw-r----- 1 ari ari  115 jul 21 18:52 strlcpy.h

Do you see it? Hey, there’s a reason why I used strlcpy.c as my example during the Intermission! Both codebases include strlcpy.c. In sline, strlcpy() is… something that might challenge your view of things…

In sline, strlcpy() isn’t declared as static because it’s used by other modules. So, within the sline codebase, strlcpy() is public; it’s not declared static. And if you remember what were the contents of the static library’s index:

$ hexdump -C libsline.a | head -n 21
00000000  21 3c 61 72 63 68 3e 0a  2f 20 20 20 20 20 20 20  |!<arch>./       |
00000010  20 20 20 20 20 20 20 20  30 20 20 20 20 20 20 20  |        0       |
00000020  20 20 20 20 30 20 20 20  20 20 30 20 20 20 20 20  |    0     0     |
00000030  30 20 20 20 20 20 20 20  32 35 34 20 20 20 20 20  |0       254     |
00000040  20 20 60 0a 00 00 00 10  00 00 01 42 00 00 01 42  |  `........B...B|
00000050  00 00 01 42 00 00 01 42  00 00 01 42 00 00 01 42  |...B...B...B...B|
00000060  00 00 01 42 00 00 01 42  00 00 0b 66 00 00 0b 66  |...B...B...f...f|
00000070  00 00 0b 66 00 00 0b 66  00 00 0b 66 00 00 0b 66  |...f...f...f...f|
00000080  00 00 0b 66 00 00 3c 1a  68 69 73 74 6f 72 79 00  |...f..<.history.|
00000090  68 69 73 74 5f 63 75 72  72 00 68 69 73 74 5f 70  |hist_curr.hist_p|
000000a0  6f 73 00 68 69 73 74 5f  65 6e 74 72 79 5f 73 69  |os.hist_entry_si|
000000b0  7a 65 00 68 69 73 74 6f  72 79 5f 67 65 74 00 68  |ze.history_get.h|
000000c0  69 73 74 6f 72 79 5f 6e  65 78 74 00 68 69 73 74  |istory_next.hist|
000000d0  6f 72 79 5f 72 6f 74 61  74 65 00 68 69 73 74 6f  |ory_rotate.histo|
000000e0  72 79 5f 73 65 74 00 73  6c 69 6e 65 5f 65 72 72  |ry_set.sline_err|
000000f0  00 73 6c 69 6e 65 00 73  6c 69 6e 65 5f 65 6e 64  |.sline.sline_end|
00000100  00 73 6c 69 6e 65 5f 65  72 72 6d 73 67 00 73 6c  |.sline_errmsg.sl|
00000110  69 6e 65 5f 73 65 74 75  70 00 73 6c 69 6e 65 5f  |ine_setup.sline_|
00000120  73 65 74 5f 70 72 6f 6d  70 74 00 73 6c 69 6e 65  |set_prompt.sline|
00000130  5f 76 65 72 73 69 6f 6e  00 73 74 72 6c 63 70 79  |_version.strlcpy|
00000140  00 00 68 69 73 74 6f 72  79 2e 6f 2f 20 20 20 20  |..history.o/    |

There you have it, right at the end of the index: strlcpy!

So… why does building cras even work? cras includes strlcpy.c, and we’re “copy-pasting” sline, which also includes that very same module… Shouldn’t it fail? What’s going on here?

The Linker Is Your Friend

Using static libraries is the same as using dynamic libraries, but static libraries get all the bad press, for reasons I will explain later in this already waaay too long post (Please, don’t kill me!) You’re using precisely the same linker in both cases: ld for those of us still chained to the GCC toolchain… lld for those in LLVM-land.

When linking everything together, the linker is able to distinguish between object files and libraries. Actually, this goes even a bit further, because within object files order matters, but I won’t get into that today. The linker will prioritize symbol resolution using object files first, and then resort to the libraries you told it to look at. And it will only import the code of that symbol if needed.

Yes, this means you may shadow a library symbol by declaring it locally in your codebase. That’s how custom malloc() wrappers work! This is intended design. If you want to see this in action, I’ve written a simple demo you will find here.

So, this means that only the parts that are actually needed are copied over to your binary! That explains why schain or cras don’t becoma a massive 5M+ executable… Ring any bells?

Dynamic libraries do precisely the same. OK, the crucial difference is when linking occurs… and consequentially, in the static case it means that you insert the code into the binary… and in the dynamic case, it means that you map the code from RAM into the virtual memory space of your process… but, again, only the required symbols are mapped, not the whole library!

If you read the symbols table for the dynamically compiled version of schain, you’ll see that, for example, that sprintf() will be included, but putchar() won’t because schain doesn’t ever use it!

$ readelf -s schain | grep 'sprintf'
    67: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND sprintf@GLIBC_2.2.5
$ readelf -s schain | grep 'putchar'

Isn’t it a constant in the human experience that apparent deep divides are just an illusion?

An another illusion there is with respect to all of this is… how deployment actually works in a real setting. OK, dynamic libraries make users' lives easier in a usual Linux distro: update the library and you probably don’t need to rebuild the packages that depend on it if no ABI/API changes have occured… This does help to tackle bugs and security issues with a much smaller deployment cost in most cases. In the case of a static library, the rebuild of dependent packages is always required… That means… OK, users will see a bigger update… but… that’s all.

Why The Bad Press, Though?

This is a question I’ve been trying to answer for quite some time in my head… Why static libraries are sort of vilified? OK, not vilified… I’ve never really encountered anyone explicitly talking bad things of them, except the updating issue, maybe? But they are sort of second-class citizens in the grand scheme of things, I feel.

On the other hand, I have found cat-v-ish rabbit holes of irrational ranting, conspiracy theories, Plan 9 fanboyism against dynamic libraries… Those rants are considered harmful 😉

I think binary distributions have some responsibility in dynamic libraries being favored as some sort of “obvious default…” Let’s face it, dynamic libraries made things easier for them right from the beginning. So, static libraries went hidden under the rug in the process. Also, the myth of binary size may have had some influence in this? I’m just speculating there.

I find this a bit sad. As I was developing sline, I thought not supporting a dynamic library to be a very, very good way to help development. If you really need one, it’s easy to build one… but deploy a dynamic library usually involves quite dirty hacks if you’re not willing to install it into a path the linker is aware of… and during development that can be inconvenient. A static library is much easier to manipulate outside standard paths… and your code will behave exactly the same! No worries on that part!

So, I don’t know. It’s a bit of a shame. Again, I don’t have any strong opinons on what’s better… But it’s been a lot of fun to get into the depths of how static libraries really work… and I ended up learning more about dynamic libraries as well!

I hope this post served to give some more bit of information, though… and maybe, why not?, help people to understand a component of our systems a bit better! Because that’s the deal, right? Sharing is caring!


  1. However, you’re probably going to find them under filenames like this: libbar.so.0.1.0, which is a mess of a problem in and of itself… and I’m not getting into it at all↩︎

  2. A quick primer on the flags ar takes to create an archive usable as a static library: -r is inserting the provided files and replace them into the archive; -c creates the archive if it doesn’t exist; and more importantly, -s is in charge of prepending an index to the beginning of the archive. (cfr. ar(1)↩︎