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.