Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C

Understanding DLLs – Building them and Allowing External Calls to their Functions

4.22/5 (10 votes)
7 May 2009CPOL6 min read 44.8K   1  
The Basics of DLLs

Introduction

In a generic sense of the term, a library is a collection of modules, each of which contains one or more functions or procedures which are packaged together so that they can be reused easily when needed and not used when not needed. There are application DLLs and system DLLs. Many system DLLs are called key system components (kernel32.dll, advapi32.dll, ntdll.dll, hal.dll, user32.dll, gdi32.dll, and so forth). If you have ever programmed in the C language, then you would know that unlike any object-oriented programming language, the basic unit of organization is the function; that C is a function, or procedural (not object) oriented language. The compiler will contain directives (included header files atop the body of the source code) that contain the prototypes of the predefined functions. The name of the header file is based on the type of those semantically-related functions defined in that header file.

These header files link together to form a static, not dynamic, library. The C compiler translates the human-readable source code into machine language. The output is a resource, or object, or module file. But even though the definitions of the functions are not included in the main body of the source code, the C compiler cannot generate the address of a function that it is not seeing. You pass the static library as input to the linker and the linker searches for all of the unresolved externals. This is why it is important to keep your source code files in the same working directory as the directory that contains the library (that contains the header definitions of the predefined functions). DLLs, on the other hand, differ. There is a separate link step for the DLL itself. When the DLL is linked, it will generate an import library. This import library enumerates by name (or sometimes number) all of the functions that are contained in that library.

Assume you have a graphical application and you call CreateWindow to put a window on the screen. When the compiler is generating that module, it doesn't know where CreateWindow is. So we build our application and we pass it our import library for user32.dll. There is no implementation of a function in this import library. The import library communicates back to the call, stating that CreateWindow is the needed function and it lives in user32.dll. Rather than resolve CreateWindow to an absolute (relative) address, the compiler and linker create a slot in an import address table which is the location where later on, the loader will patch with the address of CreateWindow. So when your built application is loaded, Windows sees that you have some functions that depend on user32.dll. So it loads user32.dll in the process address space (where the code and data are mapped to both represent an instance of and run your application). Note that only pieces of the DLL are "virtually loaded": not the entire DLL. This can be a misunderstanding: a loaded executable means that only pieces of it are loaded, not the entire image. Only the needed pieces of it are loaded. As more features are used in application, more pieces of it are loaded, as well as the appropriate DLLs. This is part of the Windows Memory Management mechanism – any memory that can be shared will be shared. Once Windows loads user32.dll into your address space, it knows where the address of CreateWindow is. It then goes to import address table to realize that it must resolve the address for CreateWindow, and it patches it at that point.

Steps to Building a DLL

If you use Visual Studio 2005 or 2008, then start with a new project, a Win32 project. For your application settings, choose DLL and check “empty project”. Name the project Squares. After the wizard closes, right-click “add new item” and add a C++ source file. Name the source file Squares_cpp.cpp, as shown below:

C++
#include <windows.h />

__declspec(dllexport) double WINAPI square(double x)
{
 return x * x;
}

__declspec(dllexport) float __stdcall square(float x)
{
 return x * x;
}

The two functions compute squares: one computes a double (decimal) and the other a floating point. If we want to make these functions available to callers external to the DLL, then we must export them.This means to use the modifier __declspec(dllexport). The WINAPI is a modifier from a header file to denote the standard calling convention.We will add two more source files, one in the C language, and the other in Windows C/C++. But first build the solution. Do not try to use “start without debugging”. We have only defined two functions. Our goal is to make them available so that another application can call them externally. Now because we are using an IDE, we now want to go to the command prompt where the C++ compiler resides, or else set the environmental path:

PATH=%PATH%;.;C:\Program Files\Microsoft Visual Studio 9.0\vc\bin

With our DLL that just copied and pasted from the Squares project folder, we will use a command to examine the exports of this DLL:

c:\Program Files\Microsoft Visual Studio 9.0\VC\bin>vcvars32.bat
Setting environment for using Microsoft Visual Studio 2008 x86 tools.

c:\Program Files\Microsoft Visual Studio 9.0\VC\bin>dumpbin /exports Square.dll
Microsoft (R) COFF/PE Dumper Version 9.00.21022.08
Copyright (C) Microsoft Corporation.  All rights reserved.
Dump of file Square.dll

File Type: DLL

  Section contains the following exports for Square.dll

    00000000 characteristics
    49FCB08F time date stamp Sat May 02 16:43:59 2009
        0.00 version
           1 ordinal base
           2 number of functions
           2 number of names

    ordinal hint RVA      name

          1    0 00011005 ?square@@YGMM@Z = @ILT+0(?square@@YGMM@Z)
          2    1 0001100A ?square@@YGNN@Z = @ILT+5(?square@@YGNN@Z)
    (the C++ compiler performs name decoration, or name mangling)
    Summary

        1000 .data
        1000 .idata
        2000 .rdata
        1000 .reloc
        1000 .rsrc
        4000 .text
       10000 .textbss

The above should look like it makes no sense. We used the dumpbin.exe tool and dumped the exports of the Squares.dll. But notice the name mangling performed by the C++ compiler. So we will use the “undname.exe” tool. But how do we copy those undiscernable characters to the command line? Right-click the corner of your DOS prompt, select mark, and drag it along the decorated name of the first function contained in this DLL until its name is whitened. Then press enter, and then right-click your mouse and click paste to transfer that data to command line containing the undname.exe command:

c:\Program Files\Microsoft Visual Studio 9.0\VC\bin>undname ?square@@YGMM@Z
Microsoft (R) C++ Name Undecorator
Copyright (C) Microsoft Corporation. All rights reserved.

Undecoration of :- "?square@@YGMM@Z"
is :- "float __stdcall square(float)"


c:\Program Files\Microsoft Visual Studio 9.0\VC\bin>undname ?square@@YGNN@Z
Microsoft (R) C++ Name Undecorator
Copyright (C) Microsoft Corporation. All rights reserved.

Undecoration of :- "?square@@YGNN@Z"
is :- "double __stdcall square(double)"

Those are the two functions contained in the DLL. Because we used a C++ compiler, the names were “decorated” to define return type information. The function we write in the C language will not be decorated. The important point about this very basic aspect of DLLs is the calling convention. We are using the standard calling convention, which is why we will also add a simple module definition file. Here is the added C file. Just right-click again and add new item, call it Square_c.c, and then copy and paste it to the UI.

C++
#include <windows.h>

int WINAPI square(int n)
{
 return n * n;
}

Now let’s add a Module Definition File. Right-click the solution, and choose “Add module definition file”.

C++
LIBRARY	"Squares"

EXPORTS

	square

The name of the file is Squares.def by default. Now we are going to add one last file called main.cpp. The purpose of this file should be self-explanatory. If it is not clear, just bear with this file and remember its format:

C++
#include <windows.h>

HINSTANCE ghInstance;

BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID)
{
 switch ( dwReason )
 {
  case DLL_PROCESS_ATTACH:

       ghInstance = hInstance;
       break;

  case DLL_PROCESS_DETACH:

       break;

  case DLL_THREAD_ATTACH:

       break;

  case DLL_THREAD_DETACH:

       break;
 }

 return TRUE;
}

Now build the entire solution, but do not try to run it as an executable. This is a dynamically-linked library that contains functions that another application (which we will build) will use by calling externally. After the solution is built, keep it on the IDE, and click, “add new project”. Make this an empty, Win32 console application project, and call it SqTest. Again, right-click the solution container and choose “Add new item”. Choose a *.cpp source file, and paste this file on the empty IDE:

C++
#include <windows.h>
#include <iostream>

__declspec(dllimport) double WINAPI square(double);
__declspec(dllimport) float WINAPI square(float);
extern "C" { __declspec(dllimport) int WINAPI square(int); };
 

int main()
{
 int n = 1;
 float x = 2.0f;
 double y = 3.0l;

 std::cout << "square of 1 is " << square(n) << std::endl;
 std::cout << "square of 2.0f is " << square(x) << std::endl;
 std::cout << "square of 3.0l is " << square(y) << std::endl;
 
 return 0;
}

Build this solution, and then choose “run without debugging”:

Capture.JPG

As you can see, the exported functions contained in the DLL were not implemented inside the DLL. We built the SqTest application and passed it an import library for Square.dll. The call for the square functions causes the compiler the linker to create a slot in an import address table. The loader program then patches that import address table slot with the address of the externally called function.

References

  • Will Palo, MSDN

History

  • 2nd May, 2009: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)