Tutorial 7: Export Table

本文深入讲解了PE文件格式中导出表的工作原理和技术细节,包括如何通过导出表获取模块导出的函数地址,以及按名称和序号两种方式导出函数的区别。

We have learned about one part of the dynamic linking, namely the import table, in the previous tutorial. Now we will learn about the other side of the coin, the export table.

Download the example.

Theory:

When the PE loader runs a program, it loads the associated DLLs into the process address space. It then extracts information about the import functions from the main program. It uses the information to search the DLLs for the addresses of the functions to be patched into the main program. The place in the DLLs where the PE loader looks for the addresses of the functions is the export table.

When a DLL/EXE exports a function to be used by other DLL/EXE, it can do so in two ways: it can export the function by name or by ordinal only. Say if there is a function named "GetSysConfig" in a DLL, it can choose to tell the other DLLs/EXEs that if they want to call the function, they must specify it by its name, ie. GetSysConfig. The other way is to export by ordinal. What's an ordinal? An ordinal is a 16-bit number that uniquely identifies a function in a particular DLL. This number is unique only within the DLL it refers to. For example, in the above example, the DLL can choose to export the function by ordinal, say, 16. Then the other DLLs/EXEs which want to call this function must specify this number in GetProcAddress. This is called export by ordinal only.

Export by ordinal only is strongly discouraged because it can cause a maintenance problem for the DLL. If the DLL is upgraded/updated, the programmer of that DLL cannot alter the ordinals of the functions else other programs that depend on the DLL will break.

Now we can examine the export structure. As with import table, you can find where the export table is from looking at the data directory. In this case, the export table is the first member of the data directory. The export structure is called IMAGE_EXPORT_DIRECTORY. There are 11 members in the structure but only some of them are really used.

Field NameMeaning
nNameThe actual name of the module. This field is necessary because the name of the file can be changed. If it's the case, the PE loader will use this internal name.
nBaseA number that you must bias against the ordinals to get the indexes into the address-of-function array.
NumberOfFunctionsTotal number of functions/symbols that are exported by this module.
NumberOfNamesNumber of functions/symbols that are exported by name. This value is not the number of ALL functions/symbols in the module. For that number, you need to check NumberOfFunctions. This value can be 0. In that case, the module may export by ordinal only. If there is no function/symbol to be exported in the first case, the RVA of the export table in the data directory will be 0.
AddressOfFunctionsAn RVA that points to an array of RVAs of the functions/symbols in the module. In short, RVAs to all functions in the module are kept in an array and this field points to the head of that array.
AddressOfNamesAn RVA that points to an array of RVAs of the names of functions in the module.
AddressOfNameOrdinalsAn RVA that points to a 16-bit array that contains the ordinals associated with the function names in the AddressOfNames array above.

Just reading the above table may not give you the real picture of the export table. The simplified explanation below will clarify the concept.

The export table exists for use by the PE loader. First of all, the module must keep the addresses of all exported functions somewhere so the PE loader can look them up. It keeps them in an array that is pointed to by the field AddressOfFunctions. The number of elements in the array is kept in NumberOfFunctions. Thus if the module exports 40 functions, it must have 40 members in the array pointed to by AddressOfFunctions and NumberOfFunctions must contain a value 40. Now if some functions are exported by names, the module must keep the names in the file. It keeps the RVAs to the names in an array so the PE loader can look them up. That array is pointed to by AddressOfNames and the number of names in NumberOfNames. Think about the job of the PE loader, it knows the names of the functions, it must somehow obtain the addresses of those functions. Up to now, the module has two arrays: the names and the addresses but there is no linkage between them. Thus we need something that relates the names of the functions to their addresses. The PE specification uses indexes into the address array as that essential linkage. Thus if the PE loader finds the name it looks for in the name array, it can obtain the index into the address table for that name too. The indexes are kept in another array (the last one) pointed to by the field AddressOfNameOrdinals. Since this array exists as the linkage between the names and the addresses, it must have exactly the same number of elements as the name array, ie. each name can have one and only one associated address. The reverse is not true: an address may have several names associated with it. Thus we can have "aliases" that refer to the same address. To make the linkage works, both name and index arrays must run in parallel, ie. the first element in the index array must hold the index for the first name and so on.

AddressOfNamesAddressOfNameOrdinals
|
|
RVA of Name 1
RVA of Name 2
RVA of Name 3
RVA of Name 4
...
RVA of Name N
<-->
<-->
<-->
<-->
...
<-->
Index of Name 1
Index of Name 2
Index of Name 3
Index of Name 4
...
Index of Name N

An example or two is in order. If we have the name of an export function and we need to get its address in the module, we can do like this:

  1. Go to the PE header
  2. Read the virtual address of the export table in the data directory
  3. Go to the export table and obtain the number of names (NumberOfNames)
  4. Walk the arrays pointed to by AddressOfNames and AddressOfNameOrdinals in parallel, searching for the matching name. If the name is found in the AddressOfNames array, you must extract the value in the associated element in the AddressOfNameOrdinals array. For example, if you find the RVA of the matching name in 77th element of the AddressOfNames array, you must extract the value stored in the 77th element of the AddressOfNameOrdinals array. If you walk the array until NumberOfNames elements are examined, you know that the name is not in this module.
  5. Use the value from the AddressOfNameOrdinals array as the index into the AddressOfFunctions array. Say, if the value is 5, you must extract the value in the 5th element of the AddressOfFunctions array. That value is the RVA of the function.

Now we can turn our attention to the nBase member of the IMAGE_EXPORT_DIRECTORY structure. You already know that the AddressOfFunctions array contains the addresses of all export symbols in a module. And the PE loader uses the indexes into this array to find the addresses of the functions. Let's imagine the scenario where we use the indexes into this array as the ordinals. Since the programmers can specify the starting ordinal number in .def file, like 200, it means that there must be at least 200 elements in the AddressOfFunctions array. Furthermore the first 200 elements are not used but they must exist so that the PE loader can use the indexes to find the correct addresses. This is not good at all. The nBase member exists to solve this problem. If the programmer specifies the starting ordinal of 200, the value in nBase would be 200. When the PE loader reads the value in nBase, it knows that the first 200 elements do not exist and that it should subtract the ordinal by the value in nBase to obtain the true index into the AddressOfFunctions array. With the use of nBase, there is no need to provide 200 empty elements.

Note that nBase doesn't affect the values in the AddressOfNameOrdinals array. Despite the name "AddressOfNameOrdinals", this array contains the true indexes into the AddressOfFunctions array, not the ordinals.

With the discussion of nBase out of the way, we can continue to the next example.
Suppose that we have an ordinal of a function and we need to obtain the address of that function, we can do it like this:

  1. Go to the PE header
  2. Obtain the RVA of the export table from the data directory
  3. Go to the export table and obtain the value of nBase.
  4. Subtract the ordinal by the value in nBase and you have the index into the AddressOfFunctions array.
  5. Compare the index with the value in NumberOfFunctions. If the index is larger or equal to the value in NumberOfFunctions, the ordinal is invalid.
  6. Use the index to obtain the RVA of the function in the AddressOfFunctions array.

Note that obtaining the address of a function from an ordinal is much easier and faster than using the name of the function. There is no need to walk the AddressOfNames and AddressOfNameOrdinals arrays. The performance gain, however, must be balanced against the difficulty in the maintaining the module.

In conclusion, if you want to obtain the address of a function from its name, you need to walk both AddressOfNames and AddressOfNameOrdinals arrays to obtain the index into the AddressOfFunctions array. If you have the ordinal of the function, you can go directly to the AddressOfFunctions array after the ordinal is biased by nBase.

If a function is exported by name, you can use either its name or its ordinal in GetProcAddress. But what if the function is exported by ordinal only? We come to that now.
"A function is exported by ordinal only" means the function doesn't have entries in both AddressOfNames and AddressOfNameOrdinals arrays. Remember the two fields, NumberOfFunctions and NumberOfNames. The existence of these two fields is the evidence that some functions may not have names. The number of functions must be at least equal to the number of names. The functions that don't have names are exported by their ordinals only. For example, if there are 70 functions but only 40 entries in the AddressOfNames array, it means there are 30 functions in the module that are exported by their ordinals only. Now how can we find out which functions are exported by ordinals only? It's not easy. You must find that out by exclusion, ie. the entries in the AddressOfFunctions array that are not referenced by the AddressOfNameOrdinals array contain the RVAs of the functions that are exported by ordinals only.

Example:

This example is similar to the one in the previous tutorial. However, it displays the values of some members of IMAGE_EXPORT_DIRECTORY structure and also lists the RVAs, ordinals, and names of the exported functions. Note that this example doesn't list the functions that are exported by ordinals only.

.386
.model flat,stdcall
option casemap:none
include /masm32/include/windows.inc
include /masm32/include/kernel32.inc
include /masm32/include/comdlg32.inc
include /masm32/include/user32.inc
includelib /masm32/lib/user32.lib
includelib /masm32/lib/kernel32.lib
includelib /masm32/lib/comdlg32.lib

IDD_MAINDLG equ 101
IDC_EDIT equ 1000
IDM_OPEN equ 40001
IDM_EXIT equ 40003

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
ShowExportFunctions proto :DWORD
ShowTheFunctions proto :DWORD,:DWORD
AppendText proto :DWORD,:DWORD


SEH struct
   PrevLink dd ?
   CurrentHandler dd ?
   SafeOffset dd ?
   PrevEsp dd ?
   PrevEbp dd ?
SEH ends

.data
AppName db "PE tutorial no.7",0
ofn OPENFILENAME <>
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
             db "All Files",0,"*.*",0,0
FileOpenError db "Cannot open the file for reading",0
FileOpenMappingError db "Cannot open the file for memory mapping",0
FileMappingError db "Cannot map the file into memory",0
NotValidPE db "This file is not a valid PE",0
NoExportTable db "No export information in this file",0
CRLF db 0Dh,0Ah,0
ExportTable db 0Dh,0Ah,"======[ IMAGE_EXPORT_DIRECTORY ]======",0Dh,0Ah
            db "Name of the module: %s",0Dh,0Ah
            db "nBase: %lu",0Dh,0Ah
            db "NumberOfFunctions: %lu",0Dh,0Ah
            db "NumberOfNames: %lu",0Dh,0Ah
            db "AddressOfFunctions: %lX",0Dh,0Ah
            db "AddressOfNames: %lX",0Dh,0Ah
            db "AddressOfNameOrdinals: %lX",0Dh,0Ah,0
Header db "RVA Ord. Name",0Dh,0Ah
       db "----------------------------------------------",0
template db "%lX %u %s",0

.data?
buffer db 512 dup(?)
hFile dd ?
hMapping dd ?
pMapping dd ?
ValidPE dd ?

.code
start:
invoke GetModuleHandle,NULL
invoke DialogBoxParam, eax, IDD_MAINDLG,NULL,addr DlgProc, 0
invoke ExitProcess, 0

DlgProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
.if uMsg==WM_INITDIALOG
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETLIMITTEXT,0,0
.elseif uMsg==WM_CLOSE
   invoke EndDialog,hDlg,0
.elseif uMsg==WM_COMMAND
   .if lParam==0
     mov eax,wParam
     .if ax==IDM_OPEN
       invoke ShowExportFunctions,hDlg
     .else ; IDM_EXIT
       invoke SendMessage,hDlg,WM_CLOSE,0,0
     .endif
   .endif
.else
   mov eax,FALSE
   ret
.endif
mov eax,TRUE
ret
DlgProc endp

SEHHandler proc C pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD
mov edx,pFrame
assume edx:ptr SEH
mov eax,pContext
assume eax:ptr CONTEXT
push [edx].SafeOffset
pop [eax].regEip
push [edx].PrevEsp
pop [eax].regEsp
push [edx].PrevEbp
pop [eax].regEbp
mov ValidPE, FALSE
mov eax,ExceptionContinueExecution
ret
SEHHandler endp

ShowExportFunctions proc uses edi hDlg:DWORD
LOCAL seh:SEH
mov ofn.lStructSize,SIZEOF ofn
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
   invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
   .if eax!=INVALID_HANDLE_VALUE
     mov hFile, eax
     invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
     .if eax!=NULL
       mov hMapping, eax
       invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
       .if eax!=NULL
         mov pMapping,eax
         assume fs:nothing
         push fs:[0]
         pop seh.PrevLink
         mov seh.CurrentHandler,offset SEHHandler
         mov seh.SafeOffset,offset FinalExit
         lea eax,seh
         mov fs:[0], eax
         mov seh.PrevEsp,esp
         mov seh.PrevEbp,ebp
         mov edi, pMapping
         assume edi:ptr IMAGE_DOS_HEADER
         .if [edi].e_magic==IMAGE_DOS_SIGNATURE
           add edi, [edi].e_lfanew
           assume edi:ptr IMAGE_NT_HEADERS
           .if [edi].Signature==IMAGE_NT_SIGNATURE
             mov ValidPE, TRUE
           .else
             mov ValidPE, FALSE
           .endif
         .else
           mov ValidPE,FALSE
         .endif
FinalExit:
         push seh.PrevLink
         pop fs:[0]
         .if ValidPE==TRUE
           invoke ShowTheFunctions, hDlg, edi
         .else
           invoke MessageBox,0, addr NotValidPE, addr AppName, MB_OK+MB_ICONERROR
         .endif
         invoke UnmapViewOfFile, pMapping
       .else
         invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
       .endif
       invoke CloseHandle,hMapping
     .else
       invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
     .endif
     invoke CloseHandle, hFile
   .else
     invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR
   .endif
.endif
ret
ShowExportFunctions endp

AppendText proc hDlg:DWORD,pText:DWORD
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,pText
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,addr CRLF
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETSEL,-1,0
ret
AppendText endp

RVAToFileMap PROC uses edi esi edx ecx pFileMap:DWORD,RVA:DWORD
mov esi,pFileMap
assume esi:ptr IMAGE_DOS_HEADER
add esi,[esi].e_lfanew
assume esi:ptr IMAGE_NT_HEADERS
mov edi,RVA ; edi == RVA
mov edx,esi
add edx,sizeof IMAGE_NT_HEADERS
mov cx,[esi].FileHeader.NumberOfSections
movzx ecx,cx
assume edx:ptr IMAGE_SECTION_HEADER
.while ecx>0
   .if edi>=[edx].VirtualAddress
     mov eax,[edx].VirtualAddress
     add eax,[edx].SizeOfRawData
     .if edi <eax
       mov eax,[edx].VirtualAddress
       sub edi,eax
       mov eax,[edx].PointerToRawData
       add eax,edi
       add eax,pFileMap
       ret
     .endif
   .endif
   add edx,sizeof IMAGE_SECTION_HEADER
   dec ecx
.endw
assume edx:nothing
assume esi:nothing
mov eax,edi
ret
RVAToFileMap endp

ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD
LOCAL temp[512]:BYTE
LOCAL NumberOfNames:DWORD
LOCAL Base:DWORD

mov edi,pNTHdr
assume edi:ptr IMAGE_NT_HEADERS
mov edi, [edi].OptionalHeader.DataDirectory.VirtualAddress
.if edi==0
  invoke MessageBox,0, addr NoExportTable,addr AppName,MB_OK+MB_ICONERROR
  ret
.endif
invoke SetDlgItemText,hDlg,IDC_EDIT,0
invoke AppendText,hDlg,addr buffer
invoke RVAToFileMap,pMapping,edi
mov edi,eax
assume edi:ptr IMAGE_EXPORT_DIRECTORY
mov eax,[edi].NumberOfFunctions
invoke RVAToFileMap, pMapping,[edi].nName
invoke wsprintf, addr temp,addr ExportTable, eax, [edi].nBase, [edi].NumberOfFunctions, [edi].NumberOfNames, [edi].AddressOfFunctions, [edi].AddressOfNames, [edi].AddressOfNameOrdinals
invoke AppendText,hDlg,addr temp
invoke AppendText,hDlg,addr Header
push [edi].NumberOfNames
pop NumberOfNames
push [edi].nBase
pop Base
invoke RVAToFileMap,pMapping,[edi].AddressOfNames
mov esi,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfNameOrdinals
mov ebx,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfFunctions
mov edi,eax
.while NumberOfNames>0
   invoke RVAToFileMap,pMapping,dword ptr [esi]
   mov dx,[ebx]
   movzx edx,dx
   mov ecx,edx
   shl edx,2
   add edx,edi
   add ecx,Base
   invoke wsprintf, addr temp,addr template,dword ptr [edx],ecx,eax
   invoke AppendText,hDlg,addr temp
   dec NumberOfNames
   add esi,4
   add ebx,2
.endw
ret
ShowTheFunctions endp
end start

Analysis:

mov edi,pNTHdr
assume edi:ptr IMAGE_NT_HEADERS
mov edi, [edi].OptionalHeader.DataDirectory.VirtualAddress
.if edi==0
  invoke MessageBox,0, addr NoExportTable,addr AppName,MB_OK+MB_ICONERROR
  ret
.endif

After the program verifies that the file is a valid PE, it goes to the data directory and obtains the virtual address of the export table. If the virtual address is zero, the file doesn't have any exported symbol.

mov eax,[edi].NumberOfFunctions
invoke RVAToFileMap, pMapping,[edi].nName
invoke wsprintf, addr temp,addr ExportTable, eax, [edi].nBase, [edi].NumberOfFunctions, [edi].NumberOfNames, [edi].AddressOfFunctions, [edi].AddressOfNames, [edi].AddressOfNameOrdinals
invoke AppendText,hDlg,addr temp

We display the important information in the IMAGE_EXPORT_DIRECTORY structure in the edit control.

push [edi].NumberOfNames
pop NumberOfNames
push [edi].nBase
pop Base

Since we want to enumerate all function names, we need to know how many names there are in the export table. nBase is used when we want to convert the indexes into the AddressOfFunctions array into ordinals.

invoke RVAToFileMap,pMapping,[edi].AddressOfNames
mov esi,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfNameOrdinals
mov ebx,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfFunctions
mov edi,eax

The addresses of the three arrays are stored in esi, ebx, and edi, ready to be accessed.

.while NumberOfNames>0

Continue until all names are processed.

   invoke RVAToFileMap,pMapping,dword ptr [esi]

Since esi points to an array of RVAs of the exported names, dereference it will give the RVA of the current name. We convert it to the virtual address, to be used in wsprintf later.

   mov dx,[ebx]
   movzx edx,dx
   mov ecx,edx
   add ecx,Base

ebx points to the array of ordinals. Its array elements are word-size. Thus we need to convert the value into a dword first. edx and ecx contain the index into the AddressOfFunctions array. We will use edx as the pointer into the AddressOfFunctions array. We add the value of nBase to ecx to obtain the ordinal number of the function.

   shl edx,2
   add edx,edi

We multiply the index by 4 (each element in the AddressOfFunctions array is 4 bytes in size) and then add the address of the AddressOfFunctions array to it. Thus edx points to the RVA of the function.

   invoke wsprintf, addr temp,addr template,dword ptr [edx],ecx,eax
   invoke AppendText,hDlg,addr temp

We display the RVA, ordinal, and the name of the function in the edit control.

   dec NumberOfNames
   add esi,4
   add ebx,2
.endw

Update the counter and the addresses of the current elements in AddressOfNames and AddressOfNameOrdinals arrays. Continue until all names are processed.


[Iczelion's Win32 Assembly Homepage]

 

内容概要:本文系统阐述了智能物流路径规划的技术体系与实践应用,涵盖其发展背景、核心问题建模、关键算法、多目标与动态环境处理、系统架构及典型应用场景。文章以车辆路径问题(VRP)及其变体为核心数学模型,介绍了从Dijkstra、A*等单智能体算法到多车VRP的元启发式求解方法(如遗传算法、蚁群算法、大规模邻域搜索),并深入探讨了多目标优化(成本、时间、碳排放)与动态环境(实时订单、交通变化)下的自适应规划策略。结合城市配送、干线运输、场内物流等案例,展示了路径规划在提升效率、降低成本方面的实际价值,并分析了当前面临的复杂性、不确定性等挑战,展望了AI融合、数字孪生、车路协同等未来趋势。; 适合人群:具备一定物流、运筹学或计算机基础,从事智能交通、物流调度、算法研发等相关工作的技术人员与管理人员,工作年限1-5年为宜。; 使用场景及目标:①理解智能物流路径规划的整体技术架构与核心算法原理;②掌握VRP建模方法与多目标、动态环境下路径优化的实现策略;③为物流系统设计、算法选型与系统优化提供理论依据与实践参考; 阅读建议:建议结合文中案例与数学模型,重点理解算法选择与实际业务场景的匹配逻辑,关注动态规划与多目标优化的工程实现难点,可配合仿真工具或开源求解器进行实践验证。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值