~kaction/log

e884047a7ed681d7861d075f67ee4f22913bbad1 — Dmitry Bogatov 1 year, 6 months ago d5f25bc wip/dynamic-loading
draft-2
1 files changed, 68 insertions(+), 0 deletions(-)

M src/2022-12-06.1.gmi
M src/2022-12-06.1.gmi => src/2022-12-06.1.gmi +68 -0
@@ 78,3 78,71 @@ map .rodata and .data sections correspondingly. Not that hard.

### Data relocations

Things get more complicated if we have pointers inside data, like in
following:

```c
const char *strings[] = {"foo", "bar", NULL};
```

We can find array relative to current intruction, sure, but what is
inside this array? Right, addresses that we don't know at compile
time. So dynamic loader have to populate this array with addresses of
strings in .rodata at run-time. This is called data relocation. Can we
avoid it?

Yes, we can. We can store offsets instead of pointers:

```
const char *strtab = "\0foo\0bar\0";
const long strings[] = [1, 5, -1];
```

And whenever we need string, we use "strtab + strings[i]" instead. A
bit more verbose and requires some build automation to generate array
of offsets, but now everything is back into .rodata and no relocations
are necessary.

### Procedure linkage table

Now suppose we are trying to dynamically load shared object that have
some third-party library, like libgdbm or libexpat, compiled in. And
let's even assume that this third-party does not need data
relocations. But we can be almost sure that it will call some
functions from standard C library, like read(2) or malloc(3), and does
it directly, since it is the most natural and efficient way for static
linking.

In ideal world all libraries would be minimalistic and only work with
data provided by input buffers, but in our worlds that is not the
case. Actually, I am not sure if it is even possible to design
equivalent of "libcurl" in minimalistic way.
=> https://nullprogram.com/blog/2018/06/10

Problem is that we don't know addresses of functions in standard
library either, so loaded shared object includes array of addresses
for standard library functions together with directives about which
functions and in which order should be populated by dynamic loader.
And answer "which" is answered by function name. And to make it
possible, main executable must maintain mapping from every function
name in standard library to its address. And we might include every
function in other libraries linked into main executable too, since
price is already paid. This is quite involved process, and now length
of function name actually affects size of the executable.

The only alternative to this process is to compile standard library
into shared object itself, but that would mean that executable will
have multiple copies of standard library functions in memory at same
time. Plus, now we need somehow to ensure that main application and
shared object have compatible implementations of malloc(3) compiled
in. Sounds like even bigger mess.

## Conclusion

When I started writing this post, I wanted to make a point that with a
bit of coding discipline we can drastically reduce complexity of
dynamic loading. While doing research I realized that it is not true.

While interface of dlopen(3) could probably made more minimalistic,
dynamically loading code that uses functions from standard library is
fundamentally hard.