About Using & Designing DLLs

requirement: medium knowledge of C/C++ or similar languages under MS Windows.

Introduction

Some days ago, I was explaining how a .DLL works, and how to use them in programs written into C or C++.

Supposing to have MYAPP.EXE and MYLIB.DLL, we will face the following main scenarios:

scenario MYAPP.EXE MYLIB.DLL
1 The program uses MYLIB.DLL. The used functions are all exported.
2 The program uses MYLIB.DLL. All functions are exposed into an interface.
This interface is returned by a single exported function.
3 The program uses MYLIB.DLL, offering one or more support functions to the library. All functions are exposed by using an interface.
The library is also using some MYAPP.EXE functions exposed into an interface by the program itself.

Scenario 1

This the most common case used at 90% by Windows itself.

Usually you have to add a proper include file (in our case: mylib.h) into all modules using the library.

#include "MYLIB.H"

Finally you have to choose how to link it: Statically and Dynamically

Linked Statically

You have to add MYLIB.LIB into the project linker settings.
From the program point of view, it is painless.  No further coding effort is necessary.

When starting the program, Windows itself will do everything needed to load MYLIB.DLL and link all needed functions.
Any dependency about is explicitly declared inside MYAPP.EXE file.

These are the most common drawbacks, that may occur:

  • function name mangling.  It can be quite tricky if you have to use different compilers/languages.
  • used .LIB file format may change between different compiler/languages.
  • each imported function shall have a unique name in your project.

Linked Dynamically

You have to do all the dirty work by code.

The first step is to load the DLL by a LoadLibrary() or similar API.

hLIB = LoadLibrary ("MYLIB.DLL");

The second step is call a GetProcAddress() for each MYLIB.DLL function, that MYAPP.EXE will use.
The result will be usually stored into a function pointer.

int (WINAPI *FNGETDATA) (void *pOut, int out_bytes, int val);

...
fnGetData = (FNGETDATA *) GetProcAddress (hLIB, "GetData");

The main effort here is to define & initialize properly the pointers of the used functions.

There are some main advantages in using this very option:

  1. Your program can use different DLLs depending by its own configuration for example.  For instance, you may design each DLL to accomplish different tasks exporting the same functions.
  2. The dependencies of these libraries aren’t declared into MYAPP.EXE, but they are sunk into its code/data segments.  This can make reverse engineering somewhat harder.  You don’t have to set anything into the linker options.
  3. You don’t have to face name mangling even between different compilers/languages…

Scenario 2

In this scenario, the DLL exports a single function to expose an interface pointer.

An interface is a static data structure, containing all needed function pointers.

typedef struct {
    ... <all other needed data here>
    int (WINAPI *MyFn1) (int val);
    int (WINAPI *MyFn2) (const char *pPtr);
    void * (WINAPI *MyFn3) (int val, void *pPtr);
    ... <all other APIs here>
    } IMYLIB;

MYLIBAPI const IMYLIB * WINAPI MYLIBGetInterface (void);

Into MYAPP.EXE, we can easily get the function interface by writing a similar code:

const IMYLIB *iMYLIB;

...
iMYLIB = MYLIBGetInterface ();
if (iMYLIB == NULL)
{
    <error>
}
else
{
    ....
}

This approach has the huge advantage that all static/dynamic link dirty work is limited to 1 simple function only.

It has a quite good impact on reverse engineering side, because without the interface definition, it quite tricky to build it from nothing.

Updating the interface (add/removing a function, a function arguments, …) doesn’t affect the link code or linker settings.

Interface Pointers

On single instance DLL/interface, it is useful to store them into global pointers.

They will be easily accessible by all other program modules.  These modules needs only to include the proper include file, and add something similar:

extern const IMYLIB *iMYLIB;

Since they are usually set once during the program startup, they may be used also by different threads without any synchronization.

Interface Version Number

Now suppose to have a new program (MYAPP2.EXE) that has to use MYLIB.DLL, but it also needs an enhanced version IMYLIB interface.

Since we want to fix and upgrade one project only, we need that MYLIB.DLL new versions shall be backward compatible with all previously released programs (in our case MYAPP1.EXE) without compiling them.

To do this, we shall add the idea of interface version number.

For example, let’s suppose to have the need to upgrade IMYLIB1 to IMYLIB2.
We defined IMYLIB1 into a mylib.h:

// .version
#define IMYLIB_VERSION_1                 1
#define IMYLIB_CURRENTVERSION            IMYLIB_VERSION_1

typedef struct {
    unsigned version;
    ... <all other needed data here>
    int (WINAPI *MyFn1) (int val);
    int (WINAPI *MyFn2) (const char *pPtr);
    void * (WINAPI *MyFn3) (int val, void *pPtr);
    ... <all other APIs here>
    } IMYLIB1;

typedef IMYLIB1 IMYLIB;

MYLIBAPI const IMYLIB * WINAPI MYLIBGetInterface (unsigned version);

IMYLIB1.version field is a very important since it will drive the interface version selection into MYLIBGetInterface().

#include "mylib.h"

...

MYLIBAPI const IMYLIB * WINAPI MYLIBGetInterface (unsigned version)
{
static const IMYLIB1 mylib1 = {
    IMYLIB_VERSION_1,
    ...
    };

    switch (version)
    {
        case IMYLIB_VERSION_1:
            return (IMYLIB *) &mylib1;
    }
    return NULL;
}

Into MYAPP1.EXE, we shall add the following code, to get the interface pointer.

const IMYLIB *iMYLIB;

...
iMYLIB = MYLIBGetInterface (IMYLIB_CURRENTVERSION);
if (iMYLIB == NULL)
{
    <error>
}
else
{
    ....
}

Note that it is using IMYLIB_CURRENTVERSION and not the explicit version number.  In this way, it will use the latest interface version available during the compiling time.
All older supported binaries will continue to work by using the older supported interfaces.
In our case, MYAPP1.EXE will use IMYLIB1 interface.

Now MYAPP2.EXE will use IMYLIB2 interface, for example, because it needs some new features.

To define IMYLIB2 properly, you have to:

  • copy IMYLIB1 into a MYLIB.DLL internal include file (ie from mylib.h to mylib_legacy.h).
  • update IMYLIB1 to IMYLIB2 into mylib.h.  In our case, we added one new function IMYLIB2.MyFn3Ex().
  • define a new version number.
// .version
#define IMYLIB_VERSION_2                 2
#define IMYLIB_CURRENTVERSION            IMYLIB_VERSION_2

typedef struct {
    unsigned version;
    ... <all other needed data here>
    int (WINAPI *MyFn1) (int val);
    int (WINAPI *MyFn2) (const char *pPtr);
    void * (WINAPI *MyFn3) (int val, void *pPtr);
    void * (WINAPI *MyFn3Ex) (int val, void *pPtr, int bytes);
    ... <all other APIs here>
    } IMYLIB2;

typedef IMYLIB2 IMYLIB;

MYLIBAPI const IMYLIB * WINAPI MYLIBGetInterface (unsigned version);
  • update MYLIBGetInterface() to select the new interface version.
#include "mylib.h"
#include "mylib_legacy.h"

...

MYLIBAPI const IMYLIB * WINAPI MYLIBGetInterface (unsigned version)
{
static const IMYLIB1 mylib1 = {
    IMYLIB_VERSION_1,
    ...
    };
static const IMYLIB2 mylib2 = {
    IMYLIB_VERSION_2,
    ...
    };

    switch (version)
    {
        case IMYLIB_VERSION_1:
            return (IMYLIB *) &mylib1;
        case IMYLIB_VERSION_2:
            return (IMYLIB *) &mylib2;
    }
    return NULL;
}

MYAPP2.EXE will have an updated copy of mylib.h, and it will have the very same piece of code used into MYAPP1.EXE to get IMYLIB2 interface pointer, because IMYLIB_CURRENTVERSION is also updated.

After having compiled both MYLIB.DLL and MYAPP2.EXE, the new .DLL will be also compatible with the old MYAPP1.EXE.

This kind of magic is done into MYLIB.DLL MYLIBGetInterface() function, which is selecting the proper interface pointer depending on the requested version number.

Scenario 3

Suppose to have a program that is using one or more libraries offering some common services (logs, memory allocations, ….).

In this case, we define IMYAPP interface into MYAPP.EXE usually contained into same IMYLIB include file (just before its declaration).

We update/add a function startup to IMYLIB interface.

BOOL (WINAPI *Startup) (IMYAPP *iMYAPP);

In this method, the library can:

  • verify the interface version.  If it doesn’t match, it shall fail.
  • store the interface pointer to use it later.

In this way, MYAPP.EXE & MYLIB.DLLs will share functions each other.

Print Friendly, PDF & Email