| 网站首页 | 业界新闻 | 小组 | 威客 | 人才 | 下载频道 | 博客 | 代码贴 | 在线编程 | 编程论坛
欢迎加入我们,一同切磋技术
用户名:   
 
密 码:  
共有 1711 人关注过本帖
标题:有关导入函数的问题
只看楼主 加入收藏
Lactoferrin
Rank: 1
等 级:新手上路
帖 子:3
专家分:0
注 册:2010-7-2
结帖率:100%
收藏
已结贴  问题点数:20 回复次数:5 
有关导入函数的问题
我用微软的汇编器ml.exe和连接器link.exe,源代码中使用proto和导入库来导入dll导出的函数
然后发现用到那个函数时总是先到一个jmp指令的位置,然后jmp指令再跳转至导入表中的地址
如asm文件中GetModuleHandleW proto stdcall,:dword
includelib kernel32.lib
...
invoke GetModuleHandleW,0
编译后它就成了
push 0
call 4004d6
而4004d6处的东西是jmp dword ptr[40052c],这个40052c就是GetModuleHandleW的地址在导入表中存放的位置
我认为这是因为一开始ml.exe不知道GetModuleHandleW是从另一个dll中导入的函数,就当一般的函数处理的,所以link.exe就自己提供了一个跳转指令
但这样做又占时间又占空间,我想问一下汇编中有没有像vc++中__declspec(dllimport)的东西,让编译器知道函数是导入来的,这样就不需要那个jmp了。
搜索更多相关主题的帖子: 函数 
2010-07-02 10:21
zklhp
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
来 自:china
等 级:贵宾
威 望:254
帖 子:11485
专家分:33241
注 册:2007-7-10
收藏
得分:2 
我认为这是因为一开始ml.exe不知道GetModuleHandleW是从另一个dll中导入的函数,就当一般的函数处理的,所以link.exe就 自己提供了一个跳转指令
但这样做又占时间又占空间,我想问一下汇编中有没有像vc++中__declspec(dllimport)的东西,让编译器知道函数是导入来的,这样就 不需要那个jmp了。

这个理解是错误的~
2010-07-02 13:39
zklhp
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
来 自:china
等 级:贵宾
威 望:254
帖 子:11485
专家分:33241
注 册:2007-7-10
收藏
得分:3 
罗云彬书里有写~



1. 调用导入函数的指令

程序被执行的时候是怎样使用导入函数的呢?先将第3章中那个简单的Hello World程序反汇编一把,看看调用导入函数的指令都是什么样子的,需要反汇编的两句源代码如下:

  invoke  MessageBox,NULL,offset szText,offset szCaption,MB_OK

  invoke  ExitProcess,NULL

当使用W32Dasm反汇编以后,这两句代码变成了以下的指令:

:00401000 6A00     push 00000000

:00401002 6800304000     push 00403000

:00401007 680F304000     push 0040300F

:0040100C 6A00   push 00000000

:0040100E E807000000     Call 0040101A ;MessageBox

:00401013 6A00   push 00000000

:00401015 E806000000     Call 00401020 ;ExitProcess

:0040101A FF2508204000   Jmp dword ptr [00402008]

:00401020 FF2500204000   Jmp dword ptr [00402000]

反汇编后,对MessageBox和ExitProcess函数的调用变成了对0040101A和00401020地址的调用,但是这两个地址显然是位于程序自身模块而不是在DLL模块中的,实际上,这是由编译器在程序所有代码的后面自动加上的Jmp dword ptr [xxxxxxxx]类型的指令,这个指令是一个间接寻址的跳转指令,xxxxxxxx地址中存放的才是真正的导入函数的地址。在这个例子中,00402000地址处存放的就是ExitProcess函数的地址。

那么在没有装载到内存之前,PE文件中的00402000地址处的内容是什么呢?使用在17.1.4节中了解的方法来查看一下。

首先,使用17.1.4节的例子文件PEInfo.exe去查看一下Hello.exe文件,会得到以下的信息:

文件名:C:\Documents and Settings\Administrator\桌面\Hello.exe

----------------------------------------------------------

运行平台:   0x014C

节区数量:   3

文件标记:   0x010F

建议装入地址:   0x00400000

----------------------------------------------------------

节区名称  节区大小  虚拟地址  Raw_尺寸  Raw_偏移  节区属性

----------------------------------------------------------

.text 00000026  00001000  00000200  00000400  60000020

.rdata   00000092  00002000  00000200  00000600  40000040

.data 00000022  00003000  00000200  00000800  C0000040

由于建议装入地址是00400000h,所以00402000h地址实际上处于RVA为2000h的地方,再看看各个节的虚拟地址,可以发现2000h开始的地方位于.rdata节内,而这个节的Raw_偏移项目为600h,也就是说00402000h地址的内容实际上对应PE文件中偏移600h处的数据。

现在随便找一个16进制编辑器来看看文件0600h处的内容是什么:

0600  76 20 00 00 00 00 00 00-5C 20 00 00 00 00 00 00 v ......\ ......

0610  54 20 00 00 00 00 00 00-00 00 00 00 6A 20 00 00 T ..........j ..

0620  08 20 00 00 4C 20 00 00-00 00 00 00 00 00 00 00 . ..L ..........

0630  84 20 00 00 00 20 00 00-00 00 00 00 00 00 00 00 . ... ..........

0640  00 00 00 00 00 00 00 00-00 00 00 00 76 20 00 00 ............v ..

0650  00 00 00 00 5C 20 00 00-00 00 00 00 BB 01 4D 65 ....\ ........Me

0660  73 73 61 67 65 42 6F 78-41 00 55 53 45 52 33 32 ssageBoxA.USER32

0670  2E 64 6C 6C 00 00 75 00-45 78 69 74 50 72 6F 63 .dll..u.ExitProc

0680  65 73 73 00 4B 45 52 4E-45 4C 33 32 2E 64 6C 6C ess.KERNEL32.dll

0690  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................

查看的结果是00002076h,这显然不会是内存中的ExitProcess函数的地址,慢着!将它作为RVA看会怎么样呢?查看节表可以发现RVA地址00002076h也处于.rdata节内,减去节的起始地址00002000h后得到这个RVA相对于节首的偏移是76h,也就是说它对应文件0676h开始的地方,接下来可以惊奇地发现,0676h再过去两个字节的内容正是函数名字符串“ExitProcess”!

这都有点搞糊涂了,Call ExitProcess指令被编译成了Call aaaaaaaa类型的指令,而aaaaaaaa处的指令是Jmp dword ptr [xxxxxxxx],而xxxxxxxx地址的地方只是一个似乎是指向函数名字符串的RVA地址,这一系列的指令显然是无法正确执行的!

但如果告诉你,当PE文件被装载的时候,Windows装载器会根据xxxxxxxx处的RVA得到函数名,再根据函数名在内存中找到函数地址,并且用函数地址将xxxxxxxx处的内容替换成真正的函数地址,那么所有的疑惑就迎刃而解了。

接下来看看如何去获取导入表的位置,以及导入表中的数据是如何组织以便Windows装载器能够顺利地进行上面的转换工作的。

2. 获取导入表的位置

导入表的位置和大小可以从PE文件头中IMAGE_OPTIONAL_HEADER32结构的数据目录字段中获取,对应的项目是DataDirectory字段的第2个IMAGE_DATA_DIRECTORY结构(见表17.4)。

从IMAGE_DATA_DIRECTORY结构的VirtualAddress字段得到的是导入表的RVA值,如果在内存中查找导入表,那么将RVA值加上PE文件装入的基址就是实际的地址;如果在PE文件中查找导入表,那么需要首先使用17.1.4节中例举的_RVAToOffset子程序将RVA首先转换成文件偏移。

 


17.2.2  导入表的结构
1. PE文件中的导入表

现在得到了包含导入表的数据块,导入表由一系列的IMAGE_IMPORT_DESCRIPTOR结构组成,结构的数量取决于程序要使用的DLL文件的数量,每个结构对应一个DLL文件,例如,如果一个PE文件从10个不同的DLL文件中引入了函数,那么就存在10个IMAGE_IMPORT_DESCRIPTOR结构来描述这些DLL文件,在所有这些结构的最后,由一个内容全为0的IMAGE_IMPORT_DESCRIPTOR结构作为结束。

IMAGE_IMPORT_DESCRIPTOR结构的定义如下:

IMAGE_IMPORT_DESCRIPTOR STRUCT

  union

  Characteristics  dd ?

  OriginalFirstThunk dd ?

  ends

  TimeDateStamp dd   ?

  ForwarderChain dd ?

  Name1 dd   ?

  FirstThunk dd ?

IMAGE_IMPORT_DESCRIPTOR ENDS

结构中的Name1字段(使用Name1作为字段名同样是因为Name一词和MASM的关键字冲突)是一个RVA,它指向此结构所对应的DLL文件的名称,这个文件名是一个以NULL结尾的字符串。

OriginalFirstThunk字段和FirstThunk字段的含义现在可以看成是相同的(使用“现在”一词的含义马上会见分晓),它们都指向一个包含一系列IMAGE_THUNK_DATA结构的数组,数组中的每个IMAGE_THUNK_DATA结构定义了一个导入函数的信息,数组的最后以一个内容为0的IMAGE_THUNK_DATA结构作为结束。

一个IMAGE_THUNK_DATA结构实际上就是一个双字,之所以把它定义成结构,是因为它在不同的时刻有不同的含义,结构的定义如下:

IMAGE_THUNK_DATA STRUCT

  union u1

  ForwarderString dd ?

  Function dd     ?

  Ordinal dd   ?

  AddressOfData dd     ?

  ends

IMAGE_THUNK_DATA ENDS

一个IMAGE_THUNK_DATA结构如何用来指定一个导入函数呢?当双字(就是指结构!)的最高位为1时,表示函数是以序号的方式导入的,这时双字的低位就是函数的序号。读者可以用预定义值IMAGE_ORDINAL_FLAG32(或80000000h)来对最高位进行测试,当双字的最高位为0时,表示函数以字符串类型的函数名方式导入,这时双字的值是一个RVA,指向一个用来定义导入函数名称的IMAGE_IMPORT_BY_NAME结构,此结构的定义如下:

IMAGE_IMPORT_BY_NAME STRUCT

  Hint dw ?

  Name1 db   ?

IMAGE_IMPORT_BY_NAME ENDS

结构中的Hint字段也表示函数的序号,不过这个字段是可选的,有些编译器总是将它设置为0,Name1字段定义了导入函数的名称字符串,这是一个以0为结尾的字符串。

整个过程听起来有些复杂,其实再看一下图17.5就很清楚了,图中示意了可执行文件导入了Kernel32.dll中的ExitProcess,ReadFile,WriteFile和lstrcmp函数的情况,其中,前面3个函数按照名称方式导入,最后的lstrcmp函数按照序号导入,这4个函数的序号分别是02f6h,0111h,002bh和0010h。



图17.5  函数的导入方法举例

现在来分析一下图17.5中的示例,导入表中IMAGE_IMPORT_DESCRIPTOR结构的Name1字段指向字符串“Kernel32.dll”,表明当前要从Kernel32.dll文件中导入函数,OriginalFirstThunk和FirstThunk字段指向两个同样的IMAGE_THUNK_DATA数组,由于要导入的是4个函数,所以数组中包含4个有效项目并以最后一个内容为0的项目作为结束。

第4个函数lstrcmp函数是以序号导入的,与其对应的IMAGE_THUNK_DATA结构的最高位等于1,和函数的序号0010h组合起来的数值就是80000010h,其余的3个函数采用的是以函数名导入的方式,所以IMAGE_THUNK_DATA结构的数值是一个RVA,分别指向3个IMAGE_IMPORT_BY_NAME结构,每个IMAGE_IMPORT_BY_NAME结构的第一个字段是函数的序号,后面就是函数的字符串名称了,一切就是这么简单!

2. 内存中的导入表

为什么需要两个一模一样的IMAGE_THUNK_DATA数组呢?答案是当PE文件被装入内存的时候,其中一个数组的值将被改作他用,还记得前面分析Hello World程序时提到的吗?Windows装载器会将指令Jmp dword ptr [xxxxxxxx]指定的xxxxxxxx处的RVA替换成真正的函数地址,其实xxxxxxxx地址正是由FirstThunk字段指向的那个数组中的一员。

实际上,当PE文件被装入内存后,内存中的映像就被Windows装载器修正成了图17.6所示的样子,其中由FirstThunk字段指向的那个数组中的每个双字都被替换成了真正的函数入口地址,之所以在PE文件中使用两份IMAGE_THUNK_DATA数组的拷贝并修改其中的一份,是为了最后还可以留下一份拷贝用来反过来查询地址所对应的导入函数名。



图17.6  导入表被装入内存后的样子

3. 导入地址表(IAT)

IMAGE_IMPORT_DESCRIPTOR结构中FirstThunk字段指向的数组最后会被替换成导入函数的真正入口地址,暂且把这个数组称为导入地址数组。在PE文件中,所有DLL对应的导入地址数组在位置上是被排列在一起的,全部这些数组的组合也被称为导入地址表(Import Address Table,或者简称为IAT),导入表中第一个IMAGE_IMPORT_DESCRIPTOR结构的FirstThunk字段指向的就是IAT的起始地址。

还有一个方法可以更方便地找到IAT的地址,那就是通过数据目录表。数据目录表中的第13项(索引值为12/ IMAGE_DIRECTORY_ENTRY_IAT)直接用来定义IAT数据块的位置和大小。

17.2.3  查看PE文件导入表举例

有一个查看PE文件所有导入函数信息的例子,例子的源代码在本书所附光盘的Chapter17\Import目录中,为了节省篇幅,例子的界面处理代码使用PEInfo例子中的Main.asm和Main.rc文件,只修改了其中的_ProcessPeFile.asm文件,另外,还使用了17.1.4节中例举的_RvaToFileOffset.asm文件。

 

 

ProcessPeFile.asm文件的内容如下:

  .const

 

szMsg     db    ~文件名: %s~,0dh,0ah

  db   ~--------------------------------------~,0dh,0ah

    db    ~导入表所处的节:%s~,0dh,0ah,0

szMsgImport db   0dh,0ah

  db   ~--------------------------------------~,0dh,0ah

    db    ~导入库: %s~,0dh,0ah

  db   ~--------------------------------------~,0dh,0ah

  db   ~OriginalFirstThunk %08X~,0dh,0ah

  db   ~TimeDateStamp   %08X~,0dh,0ah

  db   ~ForwarderChain %08X~,0dh,0ah

  db   ~FirstThunk %08X~,0dh,0ah

  db   ~--------------------------------------~,0dh,0ah

    db    ~导入序号  导入函数名称~,0dh,0ah

  db   ~------------------------------------~,0dh,0ah,0

szMsgName db   ~%8u  %s~,0dh,0ah,0

szMsgOrdinal     db    ~%8u  (按序号导入)~,0dh,0ah,0

szErrNoImport db    ~这个文件不使用任何导入函数~,0

 

  .code

include _RvaToFileOffset.asm

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

_ProcessPeFile  proc   _lpFile,_lpPeHead,_dwSize

  local @szBuffer[1024]:byte,@szSectionName[16]:byte

 

  pushad

  mov edi,_lpPeHead

  assume  edi:ptr IMAGE_NT_HEADERS

;********************************************************************

  mov eax,[edi].OptionalHeader.DataDirectory[8].VirtualAddress

  .if ! eax

  invoke  MessageBox,hWinMain,addr szErrNoImport,NULL,MB_OK

  jmp _Ret

  .endif

  invoke  _RVAToOffset,_lpFile,eax

  add eax,_lpFile

  mov edi,eax

  assume  edi:ptr IMAGE_IMPORT_DESCRIPTOR

;********************************************************************

; 显示 PE 文件名

;********************************************************************

  invoke  _GetRVASection,_lpFile,[edi].OriginalFirstThunk

  invoke wsprintf,addr @szBuffer,addr szMsg,addr szFileName,eax

  invoke  SetWindowText,hWinEdit,addr @szBuffer

;********************************************************************

; 循环处理 IMAGE_IMPORT_DESCRIPTOR 直到遇到全零的则结束

;********************************************************************

.while  [edi].OriginalFirstThunk || [edi].TimeDateStamp || \

  [edi].ForwarderChain || [edi].Name1 || [edi].FirstThunk

  invoke  _RVAToOffset,_lpFile,[edi].Name1

  add eax,_lpFile

  invoke wsprintf,addr @szBuffer,addr szMsgImport,eax,\     [edi].OriginalFirstThunk,[edi].TimeDateStamp,\

  [edi].ForwarderChain,[edi].FirstThunk

  invoke  _AppendInfo,addr @szBuffer

;********************************************************************

; 获取 IMAGE_THUNK_DATA 列表地址 ---> ebx

;********************************************************************

  .if [edi].OriginalFirstThunk

  mov eax,[edi].OriginalFirstThunk

  .else

  mov eax,[edi].FirstThunk

  .endif

  invoke  _RVAToOffset,_lpFile,eax

  add eax,_lpFile

  mov ebx,eax

;********************************************************************

; 循环处理所有的 IMAGE_THUNK_DATA

;********************************************************************

  .while  dword ptr [ebx]

;********************************************************************

; 按序号导入

;********************************************************************

  .if dword ptr [ebx] & IMAGE_ORDINAL_FLAG32

  mov eax,dword ptr [ebx]

  and eax,0FFFFh

  invoke  wsprintf,addr @szBuffer,addr szMsgOrdinal,eax

  .else

;********************************************************************

; 按函数名导入

;********************************************************************

  invoke  _RVAToOffset,_lpFile,dword ptr [ebx]

  add eax,_lpFile

  assume  eax:ptr IMAGE_IMPORT_BY_NAME

  movzx ecx,[eax].Hint

  invoke  wsprintf,addr @szBuffer,\

  addr szMsgName,ecx,addr [eax].Name1

  assume  eax:nothing

  .endif

  invoke  _AppendInfo,addr @szBuffer

  add ebx,4

  .endw

  add edi,sizeof IMAGE_IMPORT_DESCRIPTOR

.endw

;********************************************************************

_Ret:

  assume  edi:nothing

  popad

  ret

 

_ProcessPeFile  endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

如果读者已经理解了17.2.1和17.2.2节中的内容,那么这段源程序是很容易看懂的,程序首先查找数据目录表并得到导入表的地址,然后循环处理导入表中的每个IMAGE_IMPORT_DESCRIPTOR结构。

对于每个IMAGE_IMPORT_DESCRIPTOR结构,程序首先显示Name1字段指向的DLL文件名;然后继续构造一个循环来处理OriginalFirstThunk指向的IMAGE_THUNK_DATA数组,程序使用预定义值IMAGE_ORDINAL_FLAG32来测试IMAGE_THUNK_DATA结构的最高位,当最高位为1时则显示导入函数的序号,当最高位为0时则显示导入函数的Hint值和函数名称。
2010-07-02 13:41
Lactoferrin
Rank: 1
等 级:新手上路
帖 子:3
专家分:0
注 册:2010-7-2
收藏
得分:0 
这书我看过,还是不会,但是你看一下c语言,如果用void*__stdcall GetModuleHandleW(void*);来声明导入函数就会产生jmp指令,如果用__declspec(dllimport)void*__stdcall GetModuleHandleW(void*);就不会,而是直接call dword ptr[IAT中的地址],在masm中有没有类似__declspec(dllimport)功能的东西?还有,masm中怎么使用其它pe文件中的导出变量,比如ntoskrnl.exe中的PsInitialSystemProcess?不会都要用MmGetSystemRoutineAddress或自己去分析映像吧,我想直接用导入地址表。

说白了,我就是不想要那个跳转指令,请问如何处理。c语言能办到,汇编为什么办不到?

[ 本帖最后由 Lactoferrin 于 2010-7-2 15:05 编辑 ]
2010-07-02 14:56
东海一鱼
Rank: 13Rank: 13Rank: 13Rank: 13
等 级:贵宾
威 望:48
帖 子:757
专家分:4760
注 册:2009-8-10
收藏
得分:15 
很简单
简单例子:
externdef _imp__MessageBoxA@16:PTR pr4
MessageBox equ <_imp__MessageBoxA@16>

举世而誉之而不加劝,举世而非之而不加沮,定乎内外之分,辩乎荣辱之境,斯已矣。彼其于世未数数然也。
2010-07-02 17:21
Lactoferrin
Rank: 1
等 级:新手上路
帖 子:3
专家分:0
注 册:2010-7-2
收藏
得分:0 
谢谢,很好。
这里是不是要评分?如果是再请教一下如何给分,我不是很熟悉论坛的功能。

找到给分方法了

[ 本帖最后由 Lactoferrin 于 2010-7-2 21:09 编辑 ]
2010-07-02 20:08
快速回复:有关导入函数的问题
数据加载中...
 
   



关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.017716 second(s), 9 queries.
Copyright©2004-2025, BCCN.NET, All Rights Reserved