November 30th, 2023

Why does the Windows Portable Executable (PE) format have separate tables for import names and import addresses?, part 2

In the Windows Portable Executable (PE) format, the image import descriptor table describes the functions imported from a specific target DLL.

struct IMAGE_IMPORT_DESCRIPTOR {
    DWORD   OriginalFirstThunk;
    DWORD   TimeDateStamp;
    DWORD   ForwarderChain;
    DWORD   Name;
    DWORD   FirstThunk;
};

The OriginalFirstThunk points to an array of pointer-sized IMAGE_THUNK_DATA structures which describe the functions being imported. The FirstThunk points to an array of pointers, whose initial values are a copy of the values pointed to by OriginalFirstThunk.

Last time, we looked at why the two identical tables can’t be merged: Because the second table can be modified by binding, and then the two copies both contain distinct information that is needed at run time.

Okay, so you can’t merge the two tables, but why not combine them into a single mega-table? Why split it into two mini-tables?

The tables are kept separate because one is read-only and the other is read-write. Splitting them up allows the read-only part to be combined with other read-only data, and the read-write part to be combined with other read-write data. This allows for a reduction in the number of read-write pages. This benefit could end up being small for a single DLL, but if the DLL is used by many processes, the multiplicative effect can be significant.

Topics
Other

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

2 comments

Discussion is closed. Login to edit/delete existing comments.

  • Joshua Hudson

    As far as I can tell, ForwarderChain is unnecessary and it’s always possible to emit a binary for which it is zero. The documentation suggests it’s an initial index into OriginalFirstThunk, but on testing it I found no reason that OriginalFirstThunk can’t point to the right place to begin with.

  • skSdnW

    I believe old Borland compilers produce a empty/zero OriginalFirstThunk.