4

I don't understand one thing in the export data directory of PE files.

The documentation says that there is a set count of exports (let's name it ExportCount, first row of following table) and another count of names/ordinals (name it NameCount, second row in following table). I read it like that the count of export names is the same as the ordinal indices count. At least that's what their documentation says:

Microsoft Documentation

I tried parsing a Win8.1.1 x64 Shell32.dll, and I get different results compared to Dependency Walker. I have 933 as ExportCount and 354 as NameCount. So there should be 933 exports in total, with only 354 having an ordinal and/or name. Don't ask me how you would import the remaining 579 exports, as that's what I don't understand.

If I open Shell32 in Dependency Walker, it first lists NameCount exports with a name and ordinal, but then it shows the remaining amount of ExportCount - NameCount exports which surprisingly do have ordinals (starting at the blue line here):

Dependency Walker

To me, this doesn't make any sense according to the documentation. I tried to read ExportCount ordinals instead of only NameCount ones in sequence, but only rubbish comes out.

So my questions are:

  • Is the documentation wrong / incomplete?
  • Am I understanding something wrong in the documentation?
  • How to get the remaining ordinals like Dependency Walker does it?

3 Answers 3

3

If i understand http://win32assembly.programminghorizon.com/pe-tut7.html correctly, the ordinal table lists only the number of exports that actually have a name. So, your program might have the following exports:

n  name            address
0  funca           12345678
1  -- no name --   9abcdef0
2  funcb           76543210
3  -- no name --   fedcba98

This would result in

address table entries   = 4
number of name pointers = 2
export address table    = [ 12345678, 9abcdef0, 76543210, fedcba98 ]
name pointer table      = [ funca, funcb ]
ordinal table           = [ 0, 2 ]

There's a 1:1 match between name pointer entries and ordinal table entries, which is why the number of name pointers equals both array sizes. But both tables list only the functions exported by name; exports that are by ordinal only don't appear in either of them. They still appear in the export address table, however.

(If you want a better/more precise answer, you'll have to wait for Jason Geffner, he's an astonishing source of information for this kind of thing).

3
  • I thought similar about this, but that would mean I have no chance to read the ordinals of those unnamed exports, e.g. the -- no name -- exports with 1 and 3 as ordinals in your example (because they're probably not guaranteed to be in increasing order).
    – Ray
    Mar 4, 2015 at 13:58
  • Ah well, I bet you're right. I just didn't get one thing (see my added assumption at the top), that the ordinal is simply the index of the export address table.
    – Ray
    Mar 4, 2015 at 14:11
  • Great answer, as always, Guntram. @DebugErr, as I'm sure you've noticed by now, the wording in Microsoft's PE specification document can be rather misleading; I try to rely on the names of the structure fields instead (for example, AddressOfNameOrdinals instead of "Ordinal Table RVA"). And as Guntram pointed out, Iczelion's tutorials do a fantastic job at explaining many of these PE structures. Mar 4, 2015 at 16:49
3

The documentation is correct. Items can be exported by either name or ordinal.

For export by name, the ordinal table is used to locate the address in the export table. The index of the name within the Name table is the index of the ordinal within the Ordinal table. The value of the ordinal in the Ordinal table is the index into the Address table.

For export by ordinal, the value of the ordinal is used to index directly into the Address table (so it is entirely possible to have no names at all, yet many exports). There is no entry in the ordinal table. For obvious reasons, export by ordinal is not recommended unless there is an agreement that such exports can never move.

As a peculiar edge case, the ExportCount can appear to be higher than the number of exports, until you subtract the Ordinal base. For example, if I create a DLL with two exports, and whose OrdinalBase is -1, my ExportCount will appear to be 4.

0

This answer was originally added by me as an edit to my question, I have now separated it from there:

Thanks to Guntrams accepted answer, I found out where my brain was faulty. This is how it really is:

  • The ordinal of each exported address is simply the index in the export address array (plus the ordinal base specified in the export directory table however, which may make it differ from simple indices. And that is often the case since ordinals usually start at 1 according to the documentation).
  • Some exports have names, and these names are mapped to the export address array with an ordinal, which in turn is the index to the export address array.
  • Since some exports have no names, the ordinal is not explicitly specified in the ordinal array, because it would make no sense to map "no name" to an export.

My C# code does it like this, for reference - the output is the same like the one in Dependency Walker:

private void ReadExportTables(
    PEFile peFile,
    BinaryReader reader,
    DataDirectoryHeader header)
{
    // Read export address table which contains RVAs to exported code or forwarder names.
    ExportEntry[] exportEntries = new ExportEntry[EntryCount];
    reader.BaseStream.Seek(peFile.GetFileOffset(CodeAddressTableRva), SeekOrigin.Begin);
    for (int i = 0; i < exportEntries.Length; i++)
    {
        exportEntries[i].Ordinal = (ushort)(i + OrdinalStartNumber);
        exportEntries[i].CodeOrForwarderRva = reader.ReadUInt32();
    }

    // Read ordinal table containing indices (with base) to named entries in export entry table.
    reader.BaseStream.Seek(peFile.GetFileOffset(OrdinalTableRva), SeekOrigin.Begin);
    uint[] ordinals = new uint[NameEntryCount];
    for (int i = 0; i < ordinals.Length; i++)
    {
        // Get name for ordinal, which has the same index as the ordinal array element.
        ordinals[i] = reader.ReadUInt16();
    }

    // Read the export name pointer table which contains pointers to names of exports.
    reader.BaseStream.Seek(peFile.GetFileOffset(NameAddressTableRva), SeekOrigin.Begin);
    for (int i = 0; i < ordinals.Length; i++)
    {
        exportEntries[ordinals[i]].Hint = i;
        exportEntries[ordinals[i]].NameRva = reader.ReadUInt32();
    }

    // Read the names of the exports or forwarders.
    for (int i = 0; i < exportEntries.Length; i++)
    {
        if (exportEntries[i].NameRva > 0)
        {
            reader.BaseStream.Seek(peFile.GetFileOffset(
                exportEntries[i].NameRva),
                SeekOrigin.Begin);
            exportEntries[i].Name = reader.Read0AsciiString();
        }
        // Check if forwarder export (RVA points within export directory to forwarder name).
        if (exportEntries[i].CodeOrForwarderRva >= header.Rva
            && exportEntries[i].CodeOrForwarderRva < header.Rva + header.Size)
        {
            reader.BaseStream.Seek(
                peFile.GetFileOffset(exportEntries[i].CodeOrForwarderRva),
                SeekOrigin.Begin);
            exportEntries[i].ForwarderName = reader.Read0AsciiString();
        }
    }
}

In case you wondered: There can still be completely empty exports, with a code address of 0, no name and thus not forwarded. Just sort these out when displaying your exports.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.