Big picture on shared libraries when they go wrong, how?

Richard (Rikki) Andrew Cattermole richard at cattermole.co.nz
Wed May 8 06:56:29 UTC 2024


On 08/05/2024 5:13 PM, Gregor Mückl wrote:
> On Wednesday, 8 May 2024 at 03:08:15 UTC, Walter Bright wrote:
>> Thanks for writing this.
>>
>> Are you writing solely about DLLs on Windows? They don't have much in 
>> common with shared libraries on OSX and Posix.
> 
> That is confusing me as well. DLLs share concepts with shared libraries 
> on other platforms, but they have subtle differences. The ones that come 
> to my mind:
> 
> - Shared libraries export everything by default. DLLs export nothing by 
> default. This relates to the non-standard declspec(dllexport) 
> declaration supported by MSVC to mark exported symbols.

It is a convention on POSIX systems to export everything by default 
(negative annotation).

On Windows you have the 64k exported symbol limit so from a practical 
stand point you have to go positive instead.

About a year ago deadalnix told me that he thought that this was 
changing for some linux distros (unconfirmed) and it makes sense why the 
desire might be there.

Anytime you export a symbol you are pinning it into existence. You are 
preventing both compiler and linker from performing optimizations. It 
also makes your binaries larger and increases your load times.

Positive annotation might be a bit annoying and require you to 
understand how symbols are represented but using it regardless of 
platform is a much better default, this is something both me and Walter 
agree with although I am unsure what information he used to come to that 
conclusion so I cannot speak for him on that.

> - Unix system linkers take shared libraries as input files directly. 
> Windows linkers require import libraries. These import libraries contain 
> thunks that jump to the real code in the DLL. Those thunks can be 
> avoided if the compiler knows a symbol comes from a DLL. This is why 
> declspec(dllimport) exists in MSVC (as a performance optimization).

That is mostly correct, but your conclusion is wrong.

It's only a performance optimization for functions. For anything else 
you're stuck with going into ``DllImport`` mode explicitly. Such as an 
array that's in ROM like our ``.init`` symbol or ``TypeInfo`` instances; 
so being explicit about symbol modes is quite important to D, without 
the explicitness D simply won't load.

As for externs into a DLL, on Windows its pretty common for the exports 
to be missing from the DLL itself, hence you need the extra file for 
static linking. The tradeoffs that Microsoft picked here must have an 
interesting origin. I don't think it will be purely because Windows 95a 
was distributed on 30 floppy disks (or there abouts I'd have to count).

> - DllMain() is a Windows only construct. If it is present, it is invoked 
> for a lot of different events (PROCESS_ATTACH, THREAD_ATTACH...). Some 
> Unix/Posix OSes support callbacks for loading/unloading libraries at 
> most. The mechanisms are not equivalent.

I covered this in ``TLS Hooking`` heading. But basically inside of 
PE-COFF there is a TLS section that allows providing as many of these 
functions are you like. The name might be special (as to indicate the 
purpose is meant for user-code not library code such as druntime), but 
the purpose is not special.

I have no idea why POSIX hasn't added this as a feature to pthread. As 
far as I'm aware there is no legitimate reason why it shouldn't exist. 
It seems like an "ewww Microsoft did it so we won't copy their good 
idea" kind of thing.

> - And then there are all the funny ways in which static initialization 
> in C++ can break in combination with Unix shared libraries. There are 
> some fun, really opaque pitfalls like static constructors getting 
> executed multiple times (and at times when you probably woudldn't 
> expect). I don't think the same is true on Windows.

See ``TLS Hooking``, but one thing I did find is as part of glibc it'll 
hook the thread death and run all the thread destructors.

Did I mention that those destructors can be run multiple times? Yeah 
it's a mess.

> These differences result in a number of things that are different in one 
> model and not the other. On Unix, it's legal to have name collisions 
> between symbols exported from different libraries. Typically, the first 
> encountered symbol wins. This allows mechanisms like LD_PRELOAD to work 
> and and use a program with a replacement malloc() implementation, for 
> example. There is no Windows equivalent for this. You'd have to provide 
> a shim DLL in the search path that provides all symbols.

I've done a quick look, it seems its allowed to have duplicate symbols 
on Windows as well, which makes sense otherwise things like plugins 
wouldn't exactly work right (and could lead to failures for stuff like 
REPL's).

https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/nc-dbghelp-psymbol_registered_callback64

That function callback/struct is used as part of the Windows image 
introspection library for both loaded and not yet loaded binaries so it 
must be possible to get into that situation.

As for stuff like ``LD_PRELOAD`` I don't think there is anything to 
prevent it from existing, its just Microsoft decided not to support it. 
In some ways this is a security concern in it existing so I can 
understand that they didn't want to implement it.

Unfortunately the Windows loader is pretty badly documented, the only 
place I know of that documents it is the Windows Internal books and I'm 
a couple of versions behind (I don't remember 5 mentioning duplicate 
symbols).

After more reading there is a something akin to ``LD_PRELOAD`` which 
shock and horror is not recommended and is disabled with secure boot 
enabled.

https://devblogs.microsoft.com/oldnewthing/20071213-00/?p=24183


More information about the Digitalmars-d mailing list