Cookies   I display ads to cover the expenses. See the privacy policy for more information. You can keep or reject the ads.

Video thumbnail
- Good morning, everyone.
Before I started, I just wanted to make sure
you're all aware that this is a talk about DLLs, right?
There's people all over the conference
talking about very interesting things right now,
but we're going to talk about DLLs.
(audience laughs)
All right, just I want to give you
the opportunity to leave if you want to.
So actually, the talk is titled
Everything You Ever Wanted to Know about DLLs,
but I need to amend that a little bit.
So it's just actually going to be
a few things you probably didn't want to know about DLLs.
If I was to cover everything there is
to know about them, it would take me
a whole conference worth of sessions.
So this is going to be sort of
an introduction to how DLLs work.
We're gonna touch on a lot of things,
we're not going into a ton of detail in the talk.
So my name is James McNellis, I'm a Senior Engineer
on the Windows debuggers team, so I work on WinDbg.
You may have been at our talk yesterday,
we announced and released some brand new
time-travel or reverse debugging tools for Windows,
which are very exciting, you should go check them out.
Most importantly on this slide,
you can see my Twitter handle,
you should all go and follow me, that would also be great.
So I've got a lot of things that
I'd like to talk about here.
Some of the things toward the end, we may cut,
depending on time, we'll have to see.
So we're gonna start by looking at
how you build a DLL, how to use a DLL in a program,
look at what's inside of a DLL.
We'll look at explicit and implicit linking,
what happens when you load a DLL,
how to diagnose DLL loading failures,
various ways to specify what a DLL exports.
We'll look at data exports, delay loading,
C++ threads and then, DLL Hell
and then, maybe there will be time for questions.
So what we're not going to talk about,
we're not going to talk about dynamic libraries
or shared objects on other platforms.
So, obviously, many other platforms have
shared libraries of various forms.
I've been programming on Windows for a long time now,
so I don't have that familiarity that I'd need
to give a talk on that and also, I only have an hour.
So I have to constrain what I'm going to talk about.
This is CppCon, so we're not going to talk about .NET,
we're going to talk about DLLs
that have native code in them.
We're not going to focus on any other
kinds of DLLs or uses of DLLs, like resource DLLs as well.
And so we'll mention a lot of things
that we won't discuss in depth,
but I've tried to put enough information
on the slides that if you search for things,
you'll be able to find them quite easily using Bing,
or if you prefer, Google.
So what and why?
So a DLL is a dynamic link library,
that's what those letters stand for.
So it's a library that contains code and data,
just like a static library, but it can be
loaded dynamically at run-time.
So you don't actually have to link it
into your program when you actually build it.
You can choose whether or not to load it at run-time.
Additionally, it can be shared
or reused between multiple programs.
So a static library can also be shared
between multiple programs
because you could link into, for example, 20 programs.
But then, if you distribute those programs,
well, you have 20 copies of that code
or data or whatever you've linked from that on disk.
So DLLs, you only need to have one copy of it,
you don't need to have multiple copies.
So most normal DLLs have a .dll file extension,
you don't actually have to have a .dll file extension,
it could be a .txt extension, it will work just as well.
Well, some things will work just as well.
So why use DLLs?
So the biggest advantage that I already touched on
is multiple programs can share code and data
without each programming having it's own copy,
so this can reduce disk space usage
because your EXEs are smaller,
you have this code that's actually shared.
It can also reduce memory usage
because if you have DLLs that are loaded
into many different processes,
all of the read-only pages of those DLLs
can generally be shared between the multiple processes.
You can reduce overall memory usage across the system,
even if you may be increasing slightly
your own virtual address base usage within a process.
You can defer decision of whether to load
functionality until run-time, so perhaps,
you may not always need some functionality,
like, for example, if you write a print driver,
you don't want to load your print driver
into every single process, regardless of whether
it's printing or not, you want to load it
when you actually need to go and print.
Or, perhaps, you want to support other
kinds of open-ended, extensibility-like plugins.
There's also maintainability benefits.
So componentization is enabled by DLLs.
If you only work on one little part of your project,
you can keep rebuilding just your one DLL.
You don't have to rebuild the entire thing, necessarily.
It can improve serviceability because
if, let's say, there's a critical
security bug in some of your code.
If it's in a static library, you actually have to go
and rebuild every single thing that's
linked that bug into the program.
Whereas with a DLL, you can just rebuild the DLL,
redistribute it and every one picks up the fixes.
And then, finally, there's some improved maintainability,
basically for the same reason.
There's also disadvantages.
So it certainly makes software distribution
more complicated, right?
If you build everything into a single EXE,
well, you can just give that to people
and run it and it works happily.
But with DLLs, there's mobile files,
maybe you have to install some of them differently.
So it can be a bit more complex.
There's increased potential for incompatibilities,
I'm sure many people here have had to deal
with DLL Hell or DLL Hell-like issues.
And then, it's a small thing, it's impossible
to optimize code across DLL boundaries,
or, at least, with today's tooling.
So if you stat it with link code in,
you can actually do whole program optimization,
link-time code generation to optimize
across different source files.
But with DLLs, there's actually a hard boundary.
Every call into a DLL is an indirect call.
So enough talking, let's build a little DLL.
So I'm gonna build a DLL named Hello.dll.
Basically all of my slides, I'm going to actually,
we're just going to use the command line.
I'm going to have all the commands you need.
Most of the command output will be shown.
Sometimes I've trimmed things down
just to fit them on the slides,
to focus on individual things.
But you should be able to take all these commands,
all the sample code and just copy and paste it
and do the same thing.
We're going to use the visual C++ tool set,
but you could use an ngw, you could use CLion on Windows,
they'll do the same thing, it's just a different tool set.
So we'll start by looking at this Hello.cpp
that we're going to use to build a DLL.
Now, I know you're here at CppCon,
you're excited for awesome advanced C++.
This is not going to be it.
This is about as advanced as the code
that we're going to be showing in this talk
because we're focusing on the DLL mechanism,
not the C++ code.
So here, this is just a very simple
function called GetGreeting.
It returns a pointer to a string.
The only non-standard thing we've had to do here
is it has the cdecl calling convention
and the reason is, is that when you're talking
across the DLL boundary, you want to make sure that
everyone agrees on the calling convention
since there's more than one.
Otherwise, you'll end up corrupting your stack
or breaking something.
So all of our exports will be explicitly cdecl,
you can pick a calling convention just as long
as you use the same one on both sides.
So we're going to take this, we're going to compile it,
so cl/c compiles it, it does not link it.
And so that'll produce Hello.obj
On most of the future slides,
I'm going to skip the compilation step
because it's always the same,
there's nothing special about that.
And we're just going to show the linking step.
So here is where we're going to produce the DLLs.
We take the Hello.obj file, we pass the DLL option
to the linker which tells it,
"Don't build an XE," which is the default,
"build a DLL."
We say NOENTRY, the only important thing right now
is that basically reduces the amount of stuff
that gets put into the DLL
which makes this example a lot simpler.
And then we use /EXPORT to tell it
GetGreeting is part of the public interface of this DLL
so GetGreeting is one of the things
that you can actually use from outside of the DLL.
And so if we run that, it'll actually,
well it says it created Hello.lib
We'll talk about that in a bit.
It also created Hello.DLL, though it didn't tell us that.
So now that we have our DLL, we can just run it.
No, we can't just run it.
Sorry, that won't work.
So we're going to have to write a program
that actually uses this.
So I've written a program called PrintGreeting,
and we have a main function in it.
What we're going to do is we're going to
call this LoadLibraryFunction
that basically goes, finds the DLL,
loads it into memory and gives us a handle back
called an H module, that we can use to refer
to the DLL that's been loaded in memory.
So this will do that.
We'll then create a function pointer type
that matches the type of this function
that we're exporting,
and we're going to call it GetProcAddress
for the GetGreeting functions,
so we're going to ask the loader to give us
the address of this function inside of that DLL.
Reinterpret cast it to the appropriate type,
we'll then just call puts to print the string out,
and then FreeLibrary because we're done using the DLL.
And then finally we need two headers
because we are using functionality from
for puts and Windows
for the various library loading functions.
So then we can compile and link that,
and if we run the program we see it prints out
Hello, C++ Programmers!
For those of you who have a short attention span,
that was the same string that we had in our DLL,
so good, we've got it working.
This is our example.
All right, so let's take a look
at what's actually inside of that Hello.dll that we built.
So, when I want to look inside a file,
there's a program called Type, very helpful.
So here's what's inside of the file.
It's not particularly useful.
I asked on Twitter, I told people I couldn't figure out
is there a better tool for this?
Someone suggested Notepad, so that works
unfortunately just as well.
We have some other tools we'll be looking at here.
So, a DLL file consists of basically a handful
of things that are just in sequence in the file.
So it starts off, every DLL has a DOS stub program in it.
It's completely useless now, but it's there
for legacy purposes from back
when Windows and DOS inter-operated.
It then has a PE signature which marks
the beginning of the PE.
PE stands for portable executable,
so every DLL and EXE is one of these PE files.
There's a big specification
on the Microsoft documentation website
that has all of the details that I'm about to talk about.
It then has a COFF file header
which has a little information about the file.
It has an optional header that's not actually optional,
that has DLL information in it.
It then has a set of section headers
that tell you how to find information in the DLL,
like what's actually inside of it.
And then it just has all of the code and data after that.
So we'll start by looking at the DOS stub
and the PE signature.
So, I've opened up in a Hex editor,
you can see it's a little stub program or,
well, it's probably not easy to see that.
But basically it's a little DOS program that prints out.
This program cannot be run in DOS mode
so that back when DOS and Windows inter-operated,
if you ran a Windows program in DOS
it wouldn't actually run,
it would just print out this message.
So then we can also see here it's got
the PE signature at the end,
so there's the PE in two null terminators.
How do we find that?
There's basically a well-defined way,
there's add offset 3C in the file,
there's a value, and that tells you
the offset of the PE signature,
so it's at C8 in this file.
All right, that's all just how we get to find
the information we want to look for.
So next step, we're going to look
at the COFF file header, the optional header
and the section headers.
So to do that, we're going to use a tool called DUMPBIN,
which basically just parses DLL or EXE files
and it gives you all sorts of information about them.
So it goes and parses all the data structures inside of them
and presents it in a mostly readable text form.
So here we've run dumpbin/headers
which prints out all of the header information
and at the top of those we'll see
that okay, it's recognized that this is Hello.dll.
It's found the PE signature which means
that it's at least starting to find
that it's a valid file.
It's discovered it's a DLL,
and then it's going to print out
the actual contents of the COFF header.
So we can see here it tells what machine this is made for,
so this is the kind of process
the DLL has to be loaded into.
It tells you that this DLL file has two sections in it,
which we're going to need later when go to look
for the section headers.
It tells you the time and date stamp
that this DLL was built.
This can actually be any value,
so it's not necessarily the time and date stamp.
It is by default, but you can customize that.
It tells you the size of the optional header,
and then it has some characteristics.
This file contains executable code,
so executable does not mean that it's an EXE,
it just means it can be loaded as an executable DLL.
It says it can handle large addresses.
This is a 64-bit binary, it has to be able
to handle large addresses, 32-bit is different,
and that it's a DLL, not an XE.
So then what the loader will do is
it'll look at that size of the optional header
and that will tell it the type
to use to interpret the optional header.
So there's two types of optional headers.
There's PE32 and PE32+,
and as you would guess, PE32 is for 32-bit binaries
and PE32+ is for 64-bit binaries.
So you can see here there's also a magic number
that allows that linker or the loader to make sure
that it's parsing the right kind of data.
We can see here the entry point is null.
We'll talk about entry points later,
but basically we told it "no entry",
so that's what we're expecting to see.
Has the preferred base address for the image,
which becomes important later.
It has some alignment information,
it tells us the size of the image is Hex 3000 bytes.
And it's got some characteristics,
some extra information, and then finally
there's some directories which are additional metadata
about what's inside of the DLL.
So you'll see that in the directories
it actually says RVA and size.
So what is an RVA?
And to talk about that, we have to talk
about how we address within a DLL.
So let's say you've got your program running.
So we've got the heap there, we've got PrintGreeting.exe
We've got a couple thread stacks,
some other data somewhere.
And we want to go and load our Hello.dll
Now our Hello.dll wants to load at this base address.
The loader will say, "All right, is that space free?"
Yes, great, it'll load it there,
and everything works fine, everything's happy.
But what happens if we've started a couple other threads
and they're now occupying that space and memory?
It needs to go and put the DLL somewhere else.
We can't load the DLL there, that memory's already in use.
We'd go and overwrite something, we can't move data around
in a native process because you've got
just random pointers pointing to things.
So maybe the loader will load it up here instead.
We'll talk a little bit more about this later in the talk.
In general, DLLs actually don't get loaded
at the preferred base address anymore.
Basically the security feature of the loader
will try to load them at a random address in each process
so it's not predictable where the data will be.
But the important thing is that a DLL can be loaded
at all sorts of different places in your address space.
So we need a way to address things within a DLL
without relying on the actual address
at which it's loaded.
So what we use are what are called RVAs
or Relative Virtual Addresses,
which are just offsets from the beginning of the DLL.
So if you want to find the address of something
in memory and you have its RVA, you just add it
to the base address at which the DLL was actually loaded.
Or if you have the address of something and you want its RVA
you just subtract the base address from it.
So for example if our GetGreeting function in Hello.dll
has an RVA of 2000, and if Hello gets loaded
at its preferred base address there,
then GetGreeting will be located at this address
because again, you just add the two together.
So that's the optional header.
The optional header then gets followed
by the section headers, so they're just one after the other,
and these tell you where to find the actual data
inside of the DLL.
So here for example is the first section,
it's the text section which contains code.
We can see that because in the flags
it has execute and this needs to be mapped into a page
in memory with execute and read privileges.
We can see here the virtual address,
so this is the RVA where this section will be located
when the DLL is loaded.
And then we can see the virtual size,
which is the number of bytes
that are actually contained here.
There's only eight bytes of code in this DLL.
And then some other information
about how it's laid out in the file.
The second section is an .rdata section
which contains read-only data,
we can see that in the flags here
that it will need to be mapped into a page
that has read-only permissions,
so not writeable, not executable.
And it's got slightly more data in it.
From the optional header we also saw that
this DLL had two directories,
there are many more it could have had.
And the RVAs there happen to be inside of this section,
and we can tell that because the RVA
of the export directory, for example,
is 2040, and this section occupies
all of the virtual addresses from 2000 up until 20D7.
You can see that on the third or fourth line of output.
So from Hello.dll's headers we know
that the DLL will occupy three pages in memory
when loaded into the process.
One page will contain the headers,
one page will contain the .text section,
one page will contain that .rdata section,
and we'll show that in a bit more detail in a bit.
These aren't the only possible sections,
there's other kinds of sections we might find in a DLL,
but these are the only ones this program needs.
The DLL has additional metadata in a pair of directories,
there's the debug directory that we're not going to look at
but it basically has some very basic debug information.
And there's an export directory that we will look at
in a little bit.
And all of that data is contained in the .rdata section
because it's basically just read-only data
that the loader's going to use.
So then after that, basically we just have the sections,
so the section headers told us
where to find the data in the file
and so we can now look at what's actually
inside of the sections.
And to do that we can use dumpbin/rawdata
which will actually just print out the hex of the data
that's inside of each section.
So here we'll start with the text section,
and we can see that, well okay, it contains some bytes.
Since this is executable code,
we can also use the disasm option of dumpbin
which will actually disassemble it,
and so we can see it's two instructions.
All right, well that's not very exciting but there it is.
We then look at the .rdata section
and again we can use the raw data option,
and it's got a bit more data.
Most notably, it has our string there,
so this is where the string is located.
It's a string literal, it's read-only data.
It'll be in the .rdata section.
Additionally, there's two directories
that we talked about previously are also located in here,
and so I can highlight those.
We're not going to talk about the raw bytes there.
But we will look at what's inside of the export directory.
So the export directory defines
the public service of the DLL,
so it defines all of the things
that other DLLs or EXEs can use from this DLL.
And so to get information out of it, there's an option
of dumpbin, you just call dumpbin/exports on the DLL.
And here it'll print out, okay, we have one function
that's exportable so if you have handle to this DLL,
you can call get proc address,
and you can get this one function.
That's the only thing that's available here.
So at this point we can put everything together.
So in the export directory we see, okay,
that GetGreeting function is located at RVA 1000.
Well I remember from previously, RVA 1000
is in the .txt section,
And so we can disassemble the .txt section
and we can see, okay, that's the first instruction in there.
Now, if you're familiar with,
so that's pointing basically to that first instruction,
and if you're familiar
with x64 calling conventions and assembly,
basically if you have a function that returns a pointer
like our GetGreeting function, you put the pointer
into the RAX register and then you return.
And so here we can see that this is going to load
the address of whatever it at RVA 2000
into the RAX register and then return.
Well, what's at RVA 2000?
Well, I remember that was inside of our .rdata section.
And so if we look in there, that's at the very beginning
of the .rdata section,
And it's going to return the address of our string,
or the address of our string literal.
So that's kind of the basics of what's inside of the DLL,
how all the pieces point at each other, work together.
So, in this example that we had previously
of our PrintGreeting,
we saw how to link explicitly to the DLL,
so where you actually call load library
to get the handle of the DLL, you call GetProcAddress
to get the actual address of the function.
But there's a little problem here,
and that is that load library is actually defined
in a DLL itself, and GetProcAddress
is also defined in a DLL.
So both of these are defined in KERNEL32.dll.
So, well we can't call LoadLibrary to get Kernel32
so that we can get the address of LoadLibrary.
That would not work.
So DLLs can actually have implicit dependencies.
So what you can do is you can run dumpbin/dependents,
which will tell you all of the dependencies
that an EXE or a DLL has.
Our PrintGreeting has dependency on KERNEL32.dll,
and we can run dumpbin/imports to find out
what exactly it's importing from that DLL.
And so here it'll say, all right,
it has the following imports.
And we can see FreeLibrary, GetProcAddress, and LoadLibrary,
which are the three functions
that we call in our main function.
And then it turns up because we've statically linked
the C run-time into this XE, so we can call puts,
there's actually a whole bunch more imports.
So, I don't want to look at Kernel2 for this
because again there's so many functions
and it's big and complex,
so what I want to do is I want to convert
this PrintGreeting program that's
used explicit linking to our DLL,
and I want to convert it into this one.
So basically we just want to declare
our GetGreeting function and then call it,
and then use implicit linking to get everything to work.
So what we're going to do is again,
we're going to previously we linked our Hello.dll,
and it printed out Creating library Hello.lib.
That library is an import library for Hello.dll.
And so what we can actually do, we can actually
also run dumpbin exports on that .lib
and it'll tell us okay, well
the GetGreeting function is available.
So what's actually inside of there,
you have to run dumpbin/all to get
more detailed information, and this prints out tons of stuff
so I'm just going to show the important excerpts.
It has the two linkable symbols, so it has
a GetGreeting symbol that it can be linked to
and then it has an imp_GetGreeting.
And then there's an extra member inside of the library
that actually says okay, this GetGreeting function
is a code export, so it's a function, not data
and it's located inside of Hello.dll.
So what happens inside the linker
is this is as if the library had the following.
So it's as if we had a function pointer of the correct type,
and it's got a global variable imp_GetGreeting
that gets initialized to the address
of the GetGreeting function inside the other DLL,
and that happens through magic,
and we'll see how that magic works in an upcoming section.
But for now, just assume that this ends up being
the address of the GetGreeting function.
And then GetGreeting is basically just a stub
that gets statically linked into your program
that calls through that function pointer.
So this way, inside of our main function
if we call GetGreeting, it'll end up calling
through that function pointer
and getting the function from the DLL.
So then if we have our PrintGreeting program,
we can compile it, we can run it,
and we get our message again.
Then if we run dumpbin/dependents
on our PrintImplicit program,
as compared to our PrintGreeting program, we can see,
well now it depends on Hello.dll just like the other one
depended only on Kernel32.
And if we look at the imports, we can see
okay, it imports GetGreeting, and we no longer import
the LoadLibrary, FreeLibrary and GetProcAddress
from KERNEL32 but we still do import all the things
needed for the C standard library.
All right.
So, so far, we've looked at how you can
use /export to tell the linker what you want
your export service to be.
There's a few other ways and there's
a few advanced things that we can do,
so we'll look at that.
So I'm kind of tired of this silly example
of our greeting program so I came up with a new example.
I thought it'd be useful to have a DLL
that has commonly used math constants.
So to that we're going to have these three functions,
now I get one, two and three.
And so from previously we know that we can just pass /export
for each one of those.
Create the DLL and then if we dumpbin the exports
we'll see, okay it exports all three of those,
so that's just the same as we were doing before
except now we have three of them.
And then if we look in the library
it also has all three of them.
You can also rename exports, so for example
here if instead of calling it GetThree,
we wanted to call it GetOnePlusTwo,
we don't even need to rename the function
inside of the code.
We can leave it named GetThree,
I don't know why we wouldn't want to change that
but it's possible.
And then we can export it as GetOnePlusTwo
and so then you see in the exports list
and in the import library, it's just renamed the symbol
but in our code it's stayed the same.
Now let's say we want to add GetThree back, we can do that.
And you can note in the exports list
that we haven't added a new function to the DLL,
we've just added a new name for it in the export list.
So here you can see GetOnePlusTwo and GetThree
are both RVA 1020.
You can also have private exports,
so these are exports that you can call GetProcAddress
to get them.
But they're not in the import library,
so you can't implicitly link to them,
you can only explicitly link to them.
So this is useful, for example, if you want
to deprecate some functionality, you don't want people
calling something anymore,
but you can't remove it because you've got people
depending on the existing APIs.
So this /export can get very unwieldy
if you got 1000 exports from your DLL, right?
You don't want to have to keep updating your make files
every time you add new exports,
and you don't want to have to maintain
a gigantic list of these options.
I expect you'd run up against some limit
on the length of the command line eventually.
So another option is you can use what's called
a module definition file.
So our numbers program is still the same as it was before,
but now we have a new file called Numbers.def.
So this file just says this is a module definition file
for Numbers.dll, the DLL is implicit.
And then it has the list of exports,
so here we export the GetOne function,
we're going to export GetTwo as being private,
and then we'll do the rename trick.
I'm just doing this to show you the syntax
of the different options.
When we link, instead of passing /export we pass /def
to tell it what the module definition file is.
And then if we run dumpbin, everything is just as it was
with the /export options.
So this is basically just another way of specifying
all of the exports for the DLL.
There's another option that lets you do it
inside of your code.
You can annotate a function with _declspec(dllexport),
and then we don't have to tell the linker anything.
Basically we just pass it the object file,
and then if we dumpbin the exports we see
it's got all the exports.
It turns out it's actually not magic.
What happens is the compiler basically just injects
into the object file some strings,
and it tells the linker, "pretend as if you've got these
"on the command line."
So basically _declspec(dllexport) just gets turned
into these linker directives and so
it's exactly the same thing under the hood.
Finally there's a fourth option.
I don't really know why one would use this
but it's a possibility.
You can actually pass those
directives using pragma comment.
So there's this Visual C++ specific pragma
that you can use and so this will inject those directives
into the object file just like
the _decalspec(dllexport) did automatically.
And so if we do that we can see the same exports.
So now kind of the meat of the talk.
What happens when we load Hello.dll?
So, the loader has to do a lot of things
and there's a lot of,
there's a lot of advanced features that DLLs can use,
there's a lot of interesting things they can do.
We're not going to touch on most of those,
we're going to focus on the things that are germane
to our simple example here.
So we're going to look at five steps basically.
So first the loader needs to find Hello.dll,
it needs to map it into memory correctly,
it needs to load any DLLs that our Hello.dll depends on,
any implicit dependencies that it has.
It's going to bind imports from DLLs
on which Hello.dll depends.
So it needs to actually go and initialize those
function pointers like imp_GetGreeting that we had.
And then it needs to call the entry point for Hello.dll
to let it initialize itself.
So before I get into all that,
I just wanted to note, so DLLs are reference counted
so for example, here if we call LoadLibrary twice
on Hello.dll, the first time it will load the DLL
and its refcount will be one.
If you call it again, the refcount will then just be
incremented to the two, it's not going to load the DLL
a second time.
Both Hello1 and Hello2 will actually be the same,
they'll be the same value, same handle.
And then when we call FreeLibrary on the first of those,
it'll just decrement the ref count, it won't actually
unload the DLL.
And then when we call FreeLibrary a second time,
it will then unload the DLL.
All right, so the first thing we have to do
is we have to find the right DLL to load.
If we say LoadLibrary Hello.dll,
how does the loader know where to find Hello.dll?
So, the easy case is if we were to have passed it
an absolute path.
So here, instead of just calling LoadLibrary Hello.dll,
we call Load Library A:\Hello.dll.
So if A:\Hello.dll has already been loaded,
the loader will just return its module handle,
so that's easy, right?
Again, it's going to be reference counted.
Otherwise, the loader's actually
just going to find that file on disk.
If it's there, it'll load it.
If not, it'll fail the load.
Additionally to this, you can actually
load different DLLs that have the same name
from different paths, so here if I have one
on my A drive, one on my B drive,
I guess I have two floppy drives on this laptop,
then it would actually load both of those.
Those would be different module handles
and you'd have two different Hello.dlls in memory.
So then after that, if we haven't passed it
an absolute path,
so here
first we've called
LoadLibrary for the absolute path Hello.dll,
it's gotten loaded.
The second time we call LoadLibrary
on Hello.dll with no path,
the loader's going to say,
"Is there already a DLL with that name loaded?"
In this case, yes, and so it's actually just
going to return, it's going to say, "I assume
"that's the DLL you're looking for,
"and I'm going to return the handle to that."
So here, HelloDll1 and HelloDll2 are the same.
And we saw previously, well you can actually have two DLLs
with the same name that are already loaded.
In that case, the loader is just going to pick
whichever DLL was loaded first,
so in this case our HelloDllX that was loaded with no path
is the same as A, because that was the first of the two
that we loaded.
So then, okay, we say there's no DLL
with the name we're looking for that's already been loaded.
And the loader needs to find the one we're looking for.
So what it's going to ask first is, is it a known DLL?
So there's a small set of operating system DLLs
that are considered well-known to the operating system
and if you call LoadLibrary for them
or if you have a dependency on them,
it's always going to load
the operating system version of it.
It's never going to do any of the other searching,
and it's basically to prevent DLL hijacking.
It's to prevent someone from putting a malicious DLL in
and replacing some OS functionality with their own.
So here for example, if we try to load Kernel32
or Ntdll or Ole32, these are all known DLLs
so they get loaded from the system directory every time.
Whereas if we call LoadLibrary on our Hello.dll,
the OS doesn't know what that is and so the loader
will continue with the search process.
If you want to know what the set of known DLLs are,
you can use, they're all listed in the registry,
you can just look at this registry key.
These are from my Windows 10 laptop.
So after that, all right.
So it's not already loaded into memory,
we don't have an absolute path,
it's not a known DLL.
The loader needs to go and search for the DLL.
So this is the standard search path.
Basically, it's going to start by looking
in the same directory that our application is located,
so the A drive for my little test program here.
It'll look in the system directory, which will be System32
for 32-bit programs, it'll be 62 for-
No, I've got that backwards.
Anyway, it'll be one of those.
It'll then look in the 16-bit system directory
which is just named System, because legacy.
It'll then look in the current directory,
so wherever you ran the program from.
And then it'll go through the directories
in your path environment variable.
And basically it searches these in that order,
and as soon as it finds one, it'll say,
"That's the DLL I must be looking for."
The current directory used to be searched first,
that changed in Windows XP, so that's no longer the case.
This search process, it turns out, is highly customizable,
so there's DLL redirections, side by side components,
the path obviously can be customized,
it's an environment variable.
There's an AddDllDirectory function,
there's a whole bunch of flags
you can pass to LoadLibraryEx.
Windows Store and Windows Universal Applications
are totally different in how they work.
I'm not going to talk about all of those.
I list them here so that if you're curious about them,
all of those you can search for them
and find more information about them.
All right, so the loader has found the DLL it's looking for.
In our case it was easy, it was just right next
to the XE we were running.
But it now needs to map that DLL into memory.
So it's going to open up the DLL
and it's going to look at the header,
and it's going to say, "Excellent,
"Hello.dll occupies 3000, or Hex 3000
"is basically 12,000 bytes in memory,
"so I'm just going to copy the whole file in memory."
But Hello.dll is only 2,048 bytes.
And the reason for this is the DLL actually has different
alignment in the file where it has Hex 200 byte alignment,
and in memory, where it has Hex 1000 byte alignment.
So in memory it has to have 4,096 byte alignment,
because that's the size of a page,
and as we saw previously
in our Hello.dll we have a .txt section,
we have an .rdata section, and they have
to have different protections, right?
The text section needs to have execute privileges,
whereas the .rdata section you don't want
to have execute privileges.
So basically each section has to be aligned
to a page boundary in memory.
In the file, though, it uses 512 byte alignment,
and the reason is that's the FAT sector size.
So back when this was designed, that was the default.
You can actually change that, there's a linker option
to change the alignment, so you could make it smaller
if you wanted, but
the default is probably fine for most purposes.
So sections have to be page-aligned in memory.
This is what I was saying previously,
that the .txt section has different permissions on it
than the .rdata section,
so they have to be on different pages.
Additionally, we don't actually need to store everything
in the file, so for example, let's say I have a version
of our Hello.dll called HelloBuffer, and I've just thrown
an extra one megabyte global buffer inside of that DLL.
Well, the DLL file is still only 2,048 bytes in size.
If we look in the headers, we actually end up seeing
that okay, the DLL does have a larger image size.
When it gets mapped into memory it does have
that extra megabyte that it's going to take up.
But then if we look in the data section, we see
that the size of the raw data is zero,
because again, this is going to be zero initialized
so the default zero initialization that the loader's going
to do of these pages is sufficient.
We don't actually need to put any data in the file for this.
So there's some benefit here that anything
that's zero initialized, we don't need
a whole bunch of zeroes inside of our file.
So, to map the DLL into memory,
basically the loader needs to map the DLL file
and figure out what the image size is,
where all of the sections are located.
It needs to allocate a contiguous page-aligned block
of memory of that size, so all of these pages do have
to be allocated contiguously,
otherwise the relative virtual addresses
don't work correctly.
And it has to copy the contents of each section
into the appropriate area of that block of memory.
Later, it will set the appropriate page protections
on each page in the mapped DLL.
Before it does that, there's a couple more things
it has to do.
So next up, it has to do relocation, potentially.
So let's say we have another little DLL
we're going to write.
And in it we've got two global variables,
and I've exported them just so that we can
get their addresses very easily in the next slides.
So first we've got a global variable named Two,
which is just an integer with the value two,
and then we have a global pointer variable
that just holds the address of that.
And we link it.
So how do these look in the DLL?
Well, we can look at the exports.
Again, this is why I exported them.
And we can see that Two is at RVA 1000
and the pointer is at RVA 1008.
And so if we look at the .rdata section
we can see okay, Two is at RVA 1000
and PointerToTwo is at RVA 1008.
And you can see that the PointerToTwo actually contains
the actual pointer we're expecting to find it at,
so it's assuming that the DLL is located
at its preferred base address.
So this is only going to work if PointerGlobal.dll
gets loaded at that preferred base address.
If it gets loaded at, say
90 million Hex, then this won't work.
That pointer will be pointing somewhere completely wrong.
So what we do is we use,
the DLL file has what are called relocations.
And the relocations are basically a table
of all of the pointers in the DLL
that need to be fixed up if the DLL is loaded
somewhere other than its preferred base address.
So it's actually just a very simple table,
so if you can dump in relocations to get the list of them,
and basically we have one here
and it just says that there is a pointer located
eight bytes from the start of the section beginning
at RVA 1000.
And the reasons you get this kind of weird output
is that the table is very compact,
so they don't actually store all of the data
all together in one place in the DLL.
What the loader will do is it'll just update
each pointer listed in the relocation table
by subtracting the preferred base address
and adding the actual base address.
So for example, here it'll take the original pointer,
it'll subtract the preferred base address,
add the actual base address it was loaded,
we're assuming it's loaded at that address,
and that gives us the pointer value
which the loader will just write
into that location in memory.
The reason that the pointer contains
the address it would have if the DLL is loaded
at the preferred base address,
instead of just having, say, the RVA to start off with
is because a long time ago,
the loader would try to load DLLs
at their preferred base address.
So if that address was available,
it would try to load the DLL there,
and it would only fall back to relocating it
if it absolutely had to.
So basically it was more efficient to do that.
Nowadays, though, there's a feature
called ASLR, Address Space Layout Randomization,
where basically the loader will always try
and load the DLL at some other location
so that attackers can't predict where the C library is,
and use C library functionality to produce exploits.
All right, so now that we've relocated the DLL,
all the pointers are patched up,
we now need to load all of the dependencies
and bind any imports.
So if we look at the imports in our Hello.dll,
we see it doesn't have any,
so all right, we can't use Hello.dll for this part.
So let's add a new function to Hello.dll
that will give us a dependency.
So we already had our GetGreeting function
that returns a narrow string.
I get a wide greeting function
that takes a buffer and it copies that
into a wide character buffer instead.
And I've done this because then we can call
MultiByteToWideChar which we would have to import
from Kernel 32.
So when we link it, we now just add the new export
to the command line, we add kernel32.lib
so it links in the import for that function
that we're now calling.
We look at its exports, we see yes we've got both functions,
and we look at its imports and we see yes
now we're importing MultiByteToWideChar.
So this is for exposition, I just thought
it was easier to show as fake pseudocode.
Basically the loader's going to loop over
all of the DLL dependencies.
It's going to load each dependency,
so it's going to, in this case, we've only got one,
so it's going to go and load Kernel32,
which will probably already be loaded in the process.
If that fails, it'll return failure,
so it'll actually fail to load your DLL as well.
And then for each function that gets imported,
it calls GetProcAddress to go and get the actual procedure.
The loader internally doesn't actually call LoadLibrary
or GetProcAddress, but this is conceptually
exactly what the loader is going to do.
So, or in English, basically the loader's going
to load each DLL that we are dependent on,
and then get all the required exports
that it can fill in those imp variables,
those function pointers.
All right, at this point the DLL is basically loaded,
almost ready for use.
The loader now gives the DLL the opportunity
to initialize itself if it needs to,
and it does this through an entry point
which is conventionally called DllMain.
So DllMain is a function that you implement in your program,
and it's like the C main function but for DLLs.
So it takes three parameters.
The instance is your actual DLL handle,
so it's the same one that will be returned from LoadLibrary
if someone's loading you that way.
The reason indicates why the functions being called,
so there's four different times
when the loader calls this function.
It calls it on process attach,
which is when the DLL gets loaded.
It calls it on process detach,
which is when the DLL gets unloaded,
and then it calls it on thread attach and thread detach,
which are when a thread starts or stops running
within the process.
And then the reserved parameter just gives
a little extra information for some of those.
And then you get the opportunity
to return true on success or false on failure.
If you return false then your DLL fails to load.
Finally, calls to DLL main are sychronized
by a global lock, called the Loader Lock.
So basically, only one thread can be initializing
a DLL at any one time.
Not all DLLs have an entry point,
so for example our Hello program when we linked it,
we said no entry, I don't want an entry point.
And then if we look at the headers there
we saw there's a null pointer for the entry point.
So there's no entry, so basically the loader will see that
and it'll say, "All right, you don't need
"to be initialized, I don't need to do anything special."
So let's build a little DLL with an entry point.
Here I have just a simple DLL main function
that just prints out a message
when it gets called for process attach or process detach
and then returns true.
So when we compile that, we basically instead
of passing no entry, we pass /ENTRY
with the entry point that we want the loader to call.
So we'll write a little test program to demonstrate it.
And so here basically we just print out
I'm about to load the DLL.
I load it, I then say,
"All right, I've loaded the DLL, I'm about to unload it."
I call the FreeLibrary and then I print out,
all right, I've unloaded it.
We link that, and then we run it,
we should see, all right, we're in the main function,
about to load the DLL.
We're now loading the DLL, so we're in DLL main
for the process attach, which it prints out correctly.
We're back in the main function,
the DLL has completed loading.
The DLL main gets called again when we call FreeLibrary,
because it's going to free the DLL and unload it,
and then we're back in main at the end of the program.
So if you write entry points you do have
to be extremely careful, so MSDN has an article
on dynamic-link library best practices.
The short version is do as little as possible
as you have to when your DLL gets loaded.
Be very careful when calling into other DLLs
from your entry point because they may not
have been initialized themselves,
and do not sychronize with other threads
from your entry point because again
the entry point is called but the loader lock held,
so you often end up with deadlocks
if you try synchronizing with other threads.
In general, in C and C++ programs
you won't specify your own entry point.
We'll look at this in a little bit
when we discuss C++ specific things.
But instead, you'll let the C run-time
provide the entry point, and you can define
DLL main which it will call.
All right, so that's what it has to do
in order to load a DLL, but what happens
if something fails, something doesn't work correctly?
How do you diagnose what went wrong, right?
So LoadLibrary is just going to return a null pointer
and a status code.
How do you get more information?
So I know this talk is early in the morning.
If you've fallen asleep, now would be a good time
to wake up, because this is
the most important thing I'm going to share with you today.
I know a lot of people don't know about this,
but it makes debugging this sort of thing so easy.
So what I'm going to do is, I'm going to delete Hello.dll
so that when we run our PrintGreeting program,
oh, well it doesn't print anything out.
And if we look at the error level, it's
that happens to be the code for an access violation
and the reason for that is, well it failed
to load the DLL and we didn't have any error programming
in our test program, so it's just everything's failed.
So how do we find out what went wrong?
What we're going to do is the Windows debuggers package
comes with this tool called Gflags.
So we're going to run Gflags and we're going to say
that when our program runs, show loader snaps.
So then if we open our program in the debugger,
basically the loader is going to print out lots
of information, basically all of the things
that the loader is doing.
So when it starts up, it's going to say,
"Hey, I've just started running this process,"
so it's going to just give you a little information.
So here's what it's going to print when it tries
to load Hello.dll, which is going to fail.
So it says, "All right, I've started,
"I'm trying to load Hello.dll."
I'm going to see if it's a known DLL.
It says, "Nope, it's not a known DLL, that's not it."
So then it starts trying to search the search path,
and it actually tells you the setup paths
that it's going to try and search for this DLL.
You'll notice this matches that search order
that we were looking at earlier.
It's the DLL which our program is,
it's the System32 directory, the system directory,
the Windows directory and so on, until it gets to my path.
Then it's going to try each of those.
It's going to say, "All right, can I load A;\Hello.dll?"
Nope, that failed.
"Can I load System32 Hello.dll?"
Nope, that failed.
And so on until it gets to the end.
This will actually tell you every step
that the loader is going to try and do
to load this DLL for you, so you can kind of see
what went wrong, right?
You can see, oh the DLL wasn't in the right location,
or I've got my paths configured incorrectly.
Whatever, this tells you exactly what the loader did,
and you can compare that against
what you were expecting it to do
and figure out what went wrong.
Similarly, if we were to relink our Hello program
and we were to remove the export
and then call GetProcAddress to try and
get that export, it'll print out a little warning
that says well, when you tried to call GetProcAddress,
it wasn't there, so this is how you can determine
if a load fails because
you were depending on an export
that doesn't exist for some reason,
this will tell you which exports were missing.
Then when you're done, you just need to run Gflags again
with -sls instead of +sls to turn it off again.
Now, you're probably thinking, "Oh, cool,
"this is a great reason to use the Windows debuggers,"
and since I work on the debuggers team,
I'd like to say that, but this actually works
with any debugger.
You have to run the Gflags program separately
but then in Visual Studio the output
will show up there as well.
Basically the loader uses output debug string.
All right, another little feature there is.
So we've already seen _declspec(dllexport),
which you can use inside of a source file
so that the linker will know, "All right,
"I'll just implicitly export this."
So I don't have to pass/export or things like that.
There's also a DLL import, so you use this
inside of programs that use a DLL.
So what does this do?
So, first I'll look at a program
that calls these imports without using _declspec(dllimport).
So here's just a simple program, our main function
calls GetOne, there's no DLL import.
We link this program with the import library,
and if we look at what the main function ends up doing,
it's just a call GetOne, because it thinks, "All right,
"this is just a function I need to call."
And then GetOne just jumps through that function pointer.
So then if we use DLL import,
it does things a little differently.
Basically, this tells the compiler
this function is going to be imported,
so the compiler knows when it's generating code
for the main function, it can just
do the indirect call directly there.
It can avoid that extra thunk through
the GetOne function stub that it had in that binary.
You might think, "All right, this is a great idea!
"We should totally, definitely be using DLL import."
Today, if you're using Whole Program Optimization,
or Link Time Code Generation, the linker
can see right through all of this,
and so it will actually do this optimization for you,
even if you don't have the _declspec(dllimport),
so it's not entirely necessary today obviously,
in unoptimized builds or if you're not using
Link Time Code Generation, this would still be beneficial,
but it's not really necessary if you're actually using
all the optimizations you should be using.
So we've seen exporting functions, you can also export data.
So here, instead of exporting GetOne and GetTwo,
we can export variables named One and Two,
and to do that you use exactly the same
methods that we did for exporting functions,
except you should add ,DATA after it
or in the def file I think there's just no comma,
you just say DATA.
And then, if we dumpbin/exports we see, great
We can see there are RVAs which point as we'd expect
to, well there's a one, and there's a two
inside of our DLL.
And if we want to write a program that uses these,
so UseConstants we can see, we just declare the variables,
we compile it, and oh it doesn't link.
Hmm, that's interesting.
So, when you import data you actually
have to use _declspec(dllimport).
And the reason is that
the variables aren't exported directly,
they have to be exported by address.
The export table only knows about addresses,
it doesn't know what an int is,
so when you export an int, what's in the export table
is the address of that integer,
so if were actually able to bind the name One
to that address, the types wouldn't match.
You would think that that pointer is the integer.
And then we can see we can use it,
One is 1; Two is 2.
You can also get data exports using GetProcAddress,
but note that again, this is what I was saying previously,
the export is an int const*, it's not an int.
Anyway, that all works just the same.
You can also delay load DLLs.
So here if we have our DLL with entry point again,
this is again it just prints out
when it gets called for a process attach in detach.
We're running out of time, so I'm going to
flip through this a little quickly.
Actually I'm just going to skip this section.
And get to C++ and DLLs, so this is CppCon,
I figured I should probably talk a little bit
about C++ and DLLs.
So first off, you can have global variables,
global C++ variables in DLLs.
But you have to use the default entry point,
so basically
the CRT provides this entry point named DllMainCRTStartup.
If you provide your own DLL main,
it will call that automatically,
but the CRT entry point does a bunch of other things
that are necessary in order to do C and C++
inside of your DLL.
So during process attach, it'll
if you've statically linked the CRT,
it'll go and initialize that.
It initializes the security cookie
and other run-time check support.
It runs constructors for global variables,
it initializes atexit support within the DLL.
And then it'll call your DllMain.
Finally during process detach,
it calls atexit registered functions,
it runs destructors for global variables
and it'll shut down the CRT if necessary.
And it also calls your DLL main there for process detach.
I do want to note, so because all of this
is done inside of the entry point,
all of the rules that I mentioned
for implementing your own DLL main
also apply to any global variables
that have constructors inside of your DLL,
so you can't synchronize from with a constructor
of a global variable in a DLL,
because you'll end up deadlocking,
or you'll probably end up deadlocking with the loader lock.
You can also export C++ functions,
so here is a little example,
we can have two add functions,
one that takes ints, one that takes doubles.
We can link it, and we look at the exports
and we get two of them.
I don't know how to de-mangle those in my head,
so one of them is 1, the other's the other.
You can export C++ classes, so here we've got a class
with a couple of member functions.
We can link it, and of course classes aren't things
that exist in the DLL, all you actually have
are all of the member functions
that operate on the class.
So when you export a class it actually exports
all of the member functions for it.
So here we've got the GetValue and Increment
that our class has,
we have the two constructors that our class has,
and then the compiler has gone ahead for us
and actually generated the copy
and moved constructor for us and exported those.
So I have some advice for exporting
C++ functions and classes - don't.
So on Windows, there's no standard C++ ABI,
so you're very dependent on the compiler,
there's no guarantee that if you throw an exception
from one DLL and you catch it somewhere else
that it's guaranteed to work
unless you're using the same run-time library.
Basically when you're exporting things from a DLL,
you really want to make sure you're using
a very stable ABI like C, or Com or WinRT.
The three that are very well-defined on Windows.
So DLLs can have threads and thread-local storage.
I'm not
going to have time for that.
And actually, I think I'm just going to
get to the end so that we can take a few questions,
so if you want to come to the microphones,
I'll be happy to answer any questions, but no heckling.
(audience laughs)
(audience applauds)
- [Audience Member] Hi, I'd just like to know,
is there ways to prevent reading string retrawls
when you peek into your DLL?
- From, pardon?
- So, string retrawls, do you find in your code?
Pretty much dumped into a DLL as is
and is readable when you peek into them?
Can you avoid that?
- So, obfuscation?
- I'm guessing.
- So there are tools to obfuscate code inside of DLLs.
There's a whole bunch of third party tools
that can do that, that will try and hide
what is actually going on under the cover.
It'll go and move code around to make it very difficult
just to disassemble it.
But at the end of the day, if the machine's
going to execute it,
it's got to be there in some form.
- Okay, thanks.
- Yes.
- [Audience Member] Hello, I have a question.
Is there any mechanism that different processes
can share the text segment from a DLL
or that the data segment is shared, like copy and write
like in System 5?
- Yes, so the question, well I guess everyone
heard the question, because we're at the microphones.
So yes, so what I said previously
is that the loader will attempt
with address space layout randomization,
it will try and load each DLL at a different base address
each time the program's run.
That's actually not entirely true,
it will try and use a random base address,
but it will try for any given DLL, like our Hello.dll,
it will try to load it at the same base address
in every process, so that any read-only data
and any text pages can be shared between the processes.
So you end up using
virtual address space in your process,
but actual system memory like that
can all be shared, and that's very critical
for things like Kernel32, which is going
to be loaded into 200 processes,
we don't want 200 copies of that sitting in memory.
Additionally, any writeable pages are marked copy on write,
so basically DLLs will share them
until the first time they try to write to it,
at which point the page gets copied,
and all that happens behind the scenes
in the memory manager, so you don't actually
have to worry about that in your own code.
- So I
It's only possible if they are not reloaded
to a different address, they have to be the same address?
- Right. - I'm right?
- Yeah, so the loader does try its best
to put it at the same address.
- But it's not possible in another case he do.
- Then it'll load it at a different address in that process.
- Okay, thanks.
- Okay.
- [Audience Member] Hello.
- Hi.
- So, if you were writing a library,
would you recommend using DLL export or avoiding it?
- Yeah, so if I was writing a library
would I recommend using DLL export?
My preferred approach is to use the module definition file,
and the reason for that is that you actually have
one place where your entire public contract
of your DLL is defined, right?
So you have one place you know,
it's very easy to make sure that you aren't
accidentally losing exports,
so let's say that you have some things that are
marked DLL.export, and you accidentally stop compiling
that, or maybe you've purposely stopped compiling
that file into your DLL, you've probably just lost
all of those exports from your DLL as well,
so you've made a breaking change to your DLL.
So the def file, makes sure that that won't happen,
because again, if the symbols aren't there,
you'll actually get a link error,
it'll say you've removed this symbol.
So basically if you want to maintain
a stable contract for your DLL, using the def file
is probably the safest approach.
- I was also thinking static linking versus DLL linking.
So if you don't use DLL export,
you're avoiding mismatches.
- Yes, so specifically for the C run-time,
it marks functions as DLL import in its header,
if you're using the DLL run-time.
And so if you have shared static libraries
that use that functionality,
basically static libraries have to be re-compiled
with different flavors of the C run-time
in order to link correctly, which is a big hassle, yeah.
- [Audience Member] Hi James.
As you may know, I'm a bit concerned about portability,
and my question is do you think that standards
should be something
about loading and finding stuff into shared libraries
because when you try to do that on Unix
and then on Windows it gets very messy very quick.
- Yes, I do have an opinion on that,
and the answer is no, I don't think the standards
should standardize that, because it's completely different
on Linux and Windows.
Yes, they both have shared libraries,
they both contain code and data that you can share,
but the way that they work, the way that their
contracts are defined for your libraries,
the way that lifetime is handled,
the way that add exit and globals are handled,
they're all completely different.
And so standardizing that, I think it's just too late.
We can't go and change how Windows does it
because we've got 30 years of software doing it one way.
We can't go and change how Pozix does it,
because again, same thing.
So I don't really see any benefit of standardizing that.
I see opportunity for libraries to help
paper over the differences, C++ libraries,
but I don't think that's a good fit for the standard.
- [Audience Member] Hi James, I have a question
about the one part you skipped over.
We think it quite extensively and I was wondering
was the rationale behind deciding it's not going
to be automatic unloaded when you unload
the DLL loaded something?
- Yes, that I actually don't know the answer
to that question but if you come chat with me afterwards
I can help find it.
- Okay, thanks.
- Yeah.
- You skipped really fast over two parts I'm interested in
but I'll only ask about one of them.
Can you just repeat what happens
if I've got a global variable and I initialize it
and the constructor or whatever it is
is calling something from a DLL.
Is that a deadlock or what happens?
Does it fix it for me?
- It depends, so some things are safe to call,
so for example a lot of the exports from Kernel32
to do initialization of synchronization primitives,
things like that are safe.
Many of the C Runtime APIs are safe to call.
You just have to be extremely careful
because if another program calls something
that holds this lock that you might depend on
and then that in turn tries to load a library
and then basically you can end up
with lock order and versioning very easily.
So you just have to be very careful
with what you end up importing.
If what you end up using is global variables,
so it's things from your own code,
like maybe you know that something will be safe,
but if it's not from your own code
you have to use extra care.
So I know for Windows, basically
there's a small set of functions
from Kernel32 that are guaranteed to be safe,
and then we say try not to call anything else.
- Okay, so even if I'm not doing anything weird
with these LoadLibrary.exes but even
if I just stick to the standard C++ plus DLL import
I can still end up in a deadlock
if I'm not careful. - Yes.
- [Audience Member] Hi, could you please elaborate
on your advice not to export classes from DLLs,
because this is what hundreds and hundreds
of worth of our projects are doing
and it's very, very surprising to learn
that it's not advised.
- Yeah, so this will have to be the last question,
and then I can take more questions afterward.
So basically the C++ ABI on Windows,
basically it's not specified.
So the compiler does what it does,
and each major release of the compiler,
they may make breaking changes,
so I know there's been object layout changes
in some versions of the compiler,
there may have been name-mangling changes.
I don't know if there's any developer
on the compiler here, but I don't want
to say something false.
So basically any major release, they can change that.
And so it's okay to export C++ things from a DLL,
if it's just your one application package
that is going to end up using it, right?
So if you are using DLLs solely for componentization
within your application, it's probably fine
to use C++ across the boundary, as long as
you make sure you're always recompiling everything
with the same toolset, with the same options
to pass things across.
If you're trying to build a reusable component,
so something that a library that someone might use
in their own application,
if you use C++ across that boundary,
you basically need to make sure
that whatever options you use to build your DLL,
they're using also on their side,
so you might have to distribute many versions of your DLLs
for different versions of the toolset.
You might have to deliver different versions
for different compile options, so that standard library
has different layout for different types
under some options, so debug and release
don't have the same layout for all types.
So as long as you control the full set of things
that are exporting the C++ exports
and then importing them on the other side, it's safer,
but if you have reusable components,
you definitely want to use some kind of stable ABI.
All right, well thank you all for coming.
Again, I'm happy to take questions, I'll be here all week.
(audience applauds)