相对虚拟地址

在可执行文件中,许多地方都需要被指定一个在内存中的地址。例如在使用全局变量时需要它的地址。PE文件可以被加载到进程地址空间中的任何地方。虽然它有一个首选地址,但你却不能依赖可执行文件一定会被加载到那个地址。因此就需要按一定方式指定地址,使它们并不依赖于可执行文件的加载地址。

为了避免在PE文件中硬编码内存地址,因此就使用了RVA。RVA只是一个相对于PE文件在内存中的加载位置的偏移。例如假定一个EXE文件被加载在0x400000处,而它的代码节在0x401000处。那么这个代码节的RVA就是:

(目标地址)0x401000 - (加载地址)0x400000 = (RVA)0x1000

要把一个RVA转换成实际地址,只需要简单地逆着上述过程进行:将RVA与实际加载地址相加就能得到实际的内存地址。顺便说一下,按照PE格式中的说法,实际的内存地址被称为虚拟地址(Virtual Address,VA)。另外一种考虑VA的方式就是把它当成RVA加上首选加载地址。不要忘了我前面说过加载地址与HMODULE是一回事。

想在内存中探索一些DLL内部的数据结构吗?这里就是方法——用DLL的名称作为参数调用GetModuleHandle函数,它返回的HMODULE就是这个DLL的加载地址,你可以利用你学的关于PE文件结构的知识在这个模块中找到你想找到的一切。

数据目录

在可执行文件中有许多数据结构需要被快速地定位。导入表、导出表、资源以及基址重定位信息等就是一些明显的例子。所有这些广为人知的结构都是以同样的方式被定位的,这些位置被称为数据目录。

struct _IMAGE_DATA_DIRECTORY {

DWORD   VirtualAddress;

DWORD   Size;

} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16

数据目录是一个有16个(WINNT.H中定义为IMAGE_NUMBEROF_DIRECTORY_ENTRIES)元素的结构数组。每个数组元素所指代的内容已经被预先定义好了。WINNT.H文件中的这些IMAGE_DIRECTORY_ENTRY_xxx定义就是数据目录的索引(从0到15)。下表描述了每个IMAGE_DIRECTORY_ENTRY_xxx值所指代的内容。由它们指向的许多数据结构将在本文的第二部分中详细描述。

描述

IMAGE_DIRECTORY_ENTRY_EXPORT

指向导出表(IMAGE_EXPORT_DIRECTORY结构)。

IMAGE_DIRECTORY_ENTRY_IMPORT

指向导入表(IMAGE_IMPORT_DESCRIPTOR结构数组)。

IMAGE_DIRECTORY_ENTRY_RESOURCE

指向资源(IMAGE_RESOURCE_DIRECTORY结构)。

IMAGE_DIRECTORY_ENTRY_EXCEPTION

指向异常处理程序表(IMAGE_RUNTIME_FUNCTION_ENTRY结构数组)。它特定于CPU,用于基于表的异常处理。适用于除x86之外所有类型的CPU。

IMAGE_DIRECTORY_ENTRY_SECURITY

指向WIN_CERTIFICATE结构列表。此结构定义在WinTrust.H文件中。它并不作为映像的一部分被映射进内存。因此VirtualAddress域是文件偏移,而不是RVA。

IMAGE_DIRECTORY_ENTRY_BASERELOC

指向基址重定位信息。

IMAGE_DIRECTORY_ENTRY_DEBUG

指向IMAGE_DEBUG_DIRECTORY结构数组。其中的每个元素描述了映像中的一些调试信息。要获得IMAGE_DEBUG_DIRECTORY结构的数目,用Size域除以IMAGE_DEBUG_DIRECTORY结构的大小。早期的Borland链接器将这个IMAGE_DATA_DIRECTORY项的Size域设置成IMAGE_DEBUG_DIRECTORY结构的数目,而不是数组的大小。

IMAGE_DIRECTORY_ENTRY_ARCHITECTURE

指向与平台相关的数据,这个数据是一个IMAGE_ARCHITECTURE_HEADER结构数组。x86平台和IA-64平台并不使用,但好像已经用于DEC/Compaq Alpha平台。

IMAGE_DIRECTORY_ENTRY_GLOBALPTR

在某些平台上,其VirtualAddress域保存的是全局指针(Global Pointer ,GP)的RVA。x86平台上不使用,但IA-64平台上使用。Size域并未使用。要获取更多关于IA-64 GP方面的信息,可以参考

IMAGE_DIRECTORY_ENTRY_TLS

指向线程局部存储(Thread Local Storage)初始化节。

IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG

指向IMAGE_LOAD_CONFIG_DIRECTORY结构。此结构中的信息特定于Windows NT、Windows 2000和Windows XP(例如GlobalFlag值)。如果你的可执行文件要使用这个结构,需要定义一个名称为__load_config_used,类型为IMAGE_LOAD_CONFIG_DIRECTORY的全局结构体。对于非x86平台,这个名称需要被定义成_load_config_used(单下划线)。如果你想使用IMAGE_LOAD_CONFIG_DIRECTORY结构,必须使用这个技巧才能在你的C++代码中得到正确的名字。链接器看到的符号名一定要是__load_config_used(带两个下划线)。C++编译器要在全局符号前加一个下划线。另外,它还使用类型信息来修饰(decorate)全局符号。因此要使一切正常,你应该像下面这个样子使用:

extern "C"

IMAGE_LOAD_CONFIG_DIRECTORY _load_config_used = {...}

IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT

指向IMAGE_BOUND_IMPORT_DESCRIPTOR结构数组。每个结构对应于这个映像已经绑定的一个DLL。这个结构中的日期/时间戳(TimeDateStamp域)可以让加载器快速确定这个绑定是否是最新的。如果不是,加载器将忽略绑定信息,并正常地解析导入的函数。

IMAGE_DIRECTORY_ENTRY_IAT

指向第一个导入地址表(IAT)的开头。对应于每一个导入的DLL都有一个相应的IAT,并且它们在内存中依次排列。Size域指出了所有IAT的总大小。加载器在解析导入符号期间使用这个地址和大小临时将包含IAT的页面标记为可读/可写。

IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT

指向延迟加载信息,它是CImgDelayDescr结构数组,这个结构被定义在Visual C++的DELAYIMP.H文件中。直到首次调用延迟加载的DLL中的函数时这个DLL才会被加载。特别需要注意的是:Windows并不知道关于延迟加载DLL方面的任何信息。延迟加载特性完全是由链接器与运行时库来实现的。

IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR

这个值在最新的系统头文件(CorHdr.h)中被更名为IMAGE_DIRECTORY_ENTRY_COMHEADER。它指向可执行文件中的.NET信息中的顶层信息,包括元数据。这个信息保存在IMAGE_COR20_HEADER结构中。

导入函数

当你使用其它DLL中的代码或数据时,就需要导入它们。当加载PE文件时,Windows加载器的工作之一就是定位所有导入的函数和数据,以便加载的PE文件可以使用它们。我把对完成这个任务所需的数据结构的详细讨论留给本文的第二部分,在这里仅给出一个整体概念。

当你直接链接其它DLL中的代码和数据时,实际上隐含链接到了相应的DLL上。你并不需要做任何工作来让你的程序使用这些导入的函数。加载器把这些全包了。另一种使用DLL的方式是显式链接(explicit linking)。这意味着你需要明确地加载这些DLL,然后查找这些函数的地址。这种方法几乎总是通过LoadLibrary和GetProcAddress这两个API来完成的。

当你隐含链接函数时,类似于使用LoadLibrary和GetProcAddress这两个API的代码仍然存在,但加载器自动为你做这些事。同时加载器确保这个被加载的PE文件所需的其它DLL也会被加载。例如用Visual C++®创建的程序一般都会链接到KERNEL32.DLL,而KERNEL32.DLL又从NTDLL.DLL中导入了函数。同样,如果你从GDI32.DLL导入函数,而实际上这个DLL依赖于USER32、ADVAPI32、NTDLL以及KERNEL32这些DLL。加载器会确保这些DLL都被加载,以便解析这些导入的函数。(Visual Basic 6.0和Microsoft .NET可执行文件并不直接链接到KERNEL32,而是链接到了其它的DLL上,但原理是一样的。)

当隐含链接(也称为隐式链接)时,对主要的EXE文件及其依赖的所有DLL的解析过程发生在程序启动时。如果这时出现任何问题(例如它引用的一个DLL找不到),相应的进程就会被终止。

Visual C++ 6.0引入了一个延迟加载(delayload)特性,它是隐含链接与显式链接的混合。当你延迟加载DLL时,链接器生成了一些非常类似于它为正常导入的DLL生成的数据那样的数据,但是操作系统却忽略这些数据。当你的程序在执行过程中首次调用这些延迟加载的函数其中之一时,由链接器为此生成的一部分代码就会执行,由它加载相应的DLL(如果尚未加载),然后调用GetProcAddress来确定要调用的函数的地址。这些额外的工作使得接下来对这个函数的调用就好像这个函数是正常导入的一样。

在PE文件中,对应于每一个导入的DLL有一个相应的结构数组。其中的每个结构都给出了导入的DLL的名称和一个指向函数指针数组的指针。这个函数指针数组被称为导入地址表(Import Address Talbe,IAT)。每个导入的函数都在IAT中有一个对应的位置,Windows加载器就在这个位置上写入这个导入函数的地址。这一点非常重要:一旦一个模块的加载过程结束,那么其IAT中就包含了导入函数的地址。

IAT的美妙之处就在于,在PE文件中,只有一个地方保存了导入函数的地址。无论在你的程序中对某个导入的函数调用多少次,所有调用使用的同样都是IAT中对应于这个函数的指针。

现在让我们来看一下如何调用导入函数。它分为两种情况:高效率方式与低效率方式。按最好的情况(高效率方式)来说,对一个导入函数的调用应该像下面这个样子:

CALL DWORD PTR [0x00405030]如果你不熟悉x86汇编语言,我可以告诉你这条指令表示通过函数指针来调用相应的函数。在地址0x00405030处的一个DWORD类型的值就是CALL指令要将控制权转到的地方。在这个例子中,地址0x00405030在IAT中。

调用导入函数的低效率方式类似下面这个样子:

CALL 0x0040100C

...

0x0040100C:

JMP       DWORD PTR [0x00405030]

在这种情况下,CALL指令把控制权转到了一个小占位程序(stub)中。这个占位程序只是一条JMP指令,用以跳转到保存在地址0x405030处的地址中。同样,记住0x405030是IAT中的一个元素。一句话,调用API的这种低效率方式使用了5个字节的附加代码(JMP指令是1字节,地址是4个字节),并且由于使用了额外的JMP指令,因此执行时要花费更长的时间。

你可能奇怪既然有高效率的调用方式,为什么还要使用低效率的调用方式呢?理由是很充足的。由于自身的限制,编译器并不能区分调用导入的函数与调用同一模块中的函数之间的区别。因此编译器为函数调用生成的指令是这样的:

CALL XXXXXXXX而XXXXXXXX处是实际代码的地址,这个地址由链接器在后面填充。注意这最后的CALL指令并不是通过函数指针(调用函数的)。相反,它使用的是实际代码的地址。为了保持一致的方式,链接器需要用一个代码块来替换XXXXXXXX。最简单的做法就是调用一个JMP之类的占位程序,就像上面你所看到的那样。

那JMP指令是从哪里来的呢?令人惊讶的是,它来自于相应函数的导入库。如果你仔细查看导入库,并且查看与导入函数名称相连的代码时,就会看到类似上面的JMP指令。这意味着,默认情况下,如果没有任何干涉,调用导入函数使用的总是低效率的调用方式。

按照逻辑推理,下一个要问的问题一定是如何才能使用高效率的调用方式。答案是你必须给编译器一个提示。__declspec(dllimport)这个函数修饰符告诉编译器这个函数在其它的DLL中,编译器应该生成下面这样的指令:

CALL DWORD PTR [XXXXXXXX]而不是下面这样的指令:CALL XXXXXXXX

另外,编译器生成相应的信息来告诉链接器去解析上面那条指令中的函数指针部分时应该去找的符号是__imp_函数名(就是在相应的函数名称前加上__imp_)。例如,如果你调用MyFunction这个函数,那么相应的符号名应为__imp_MyFunction。看一下导入库,除了看到正常的符号名外,你还会看到一个以__imp_为前缀的同样的符号名。这个__imp_类型的符号被直接解析成了IAT项,而不是JMP占位程序。

这对你日常工作有什么影响呢?如果你编写导出函数并且提供了相应的.H文件,记得使用__declspec(dllimport)函数修饰符。例如:

__declspec(dllimport) void Foo(void);如果你看一下Windows系统头文件,你会发现所有的Windows API都使用了__declspec(dllimport)。要想看到它并不容易,但是如果你搜索定义在WINNT.H中,并且用于像WinBase.h之类的头文件中的DECLSPEC_IMPORT宏,你就会发现__declspec(dllimport)是如何用于系统API声明的。

c语言的编译过程,程序编译过程相关推荐

  1. 使用gcc编译和链接C语言程序,用GCC编译链接程序--编译链接器GCC常用功能(菜鸟级)...

    转载自:http://daimajishu.iteye.com/blog/1089740 对gcc认识的一篇文章,就转载了,截取了自己感兴趣部分. 一,GCC编译器简介 虽然我们称Gcc是C语言的编译 ...

  2. linux gcc 静态编译,GCC 程序编译的静态链接和动态链接

    (给Linux爱好者加星标,提升Linux技能)转自:Mr_Bluyee 在链接阶段中,所有对应于源文件的 .o 文件.'-l' 选项指定的库文件.无法识别的文件名(包括指定的.o目标文件和.a库文件 ...

  3. 终端编译opengl程序编译运行_ubuntu编译opengl和demo之二(glfw版本)

    [TOC] 1.安装GL和glut等 sudo apt-get install mesa-common-dev libgl1-mesa-dev libglu1-mesa-dev sudo apt-ge ...

  4. c语言如何把c程序编译成可执行的exe文件

    1.编写一段简单的代码,如下图所示,编辑一段简单的输出语句. 2.依次点击菜单栏内的编译→组建→执行,确保这段成率可以正确执行. 3.从运行结果来看程序正确,没有问题,然后找到exe可执行程序看其是否 ...

  5. 编译原理——程序编译的基本流程

    目录标题 词法分析 语法分析 从语法树到中间代码再到目标代码 链接与载入 总结 源程序是给人看的,本质上就是文本文件,可以用Linux中的vi或Windows中的记事本之类的文本编辑程序打开.编写,但 ...

  6. 终端编译opengl程序编译运行_ubuntu – 通过SSH编写opengl代码,通过机器显示运行程序...

    你没有详细说明你的环境,所以让我们一个接一个.首先,如果您在以前的计算机上运行Ubuntu(或任何GNU / Linux),您最有可能使用X Windows在OpenGL应用程序中显示.鉴于这种假设, ...

  7. C语言程序编译和执行过程

    1.预处理 1).宏定义展开2).头文件展开3).删除注释4).条件编译格式:gcc -E a.c -o a.i 2.编译1).检查语法2).转化成汇编语言格式:gcc -S a.i -o a.s 3 ...

  8. Linux 程序编译过程

    前言 计算机程序设计语言通常分为机器语言,汇编语言和高级语言三类.而高级语言需要被翻译成机器语言才可以被执行,而翻译的方式也被分为两种,一种是编译型,另一种为解释型,根据这两种的不同,我们将其分为编译 ...

  9. 手工编译Android程序

    手工编译Android程序   赖锋 我的china-unix博客 http://laiboy.cublog.cn 我的CSDN博客 http://blog.csdn.net/laiboy 程序描述 ...

最新文章

  1. django官方文档1.11编翻:1-1-1概述
  2. 【Android 应用开发】Android 网络编程 API笔记 - java.net 包 权限 地址 套接字 相关类 简介
  3. 上下文保存 中断_Linux性能优化(CPU篇)(5)——CPU的上下文切换有几种类型?什么是进程上下文切换?...
  4. Linux 用户 和 用户组 管理 (添加、删除、修改)及说明
  5. 华为荣耀电脑第三方linux,【第三方Linux版】荣耀MagicBook Pro 16.1英寸全面屏如何?某东入手评测...
  6. c#如何实现从xml中加载树目录,并且显示完整的Text
  7. 爱着你,恨着你——BCGControlBar的Menu字体
  8. mysql 关闭autocommit_mysql禁用autocommit,以及遇到的问题
  9. 我才是嗨到最晚的男人
  10. y480 linux无线网卡驱动,联想y480无线网卡驱动下载
  11. pdf转word工具大全
  12. php怎么获取图片信息,PHP获取图片信息exif
  13. Winfrom控件的使用
  14. redis desktop manager安装以及使用教程
  15. 原生JS --360度全景展示
  16. python刷步数程序设计_乐心健康间接修改微信步数-Docker持久运行python脚本
  17. python100天-如何系统地学习 Python,100天从新手到大师
  18. 华为android9王者荣耀卡,华为mate9玩王者荣耀怎么样 mate9玩王者荣耀卡吗
  19. G2Plot 图例(legend)带瞄准图标解决
  20. 代理ip的使用场景。

热门文章

  1. 十个 SCP 传输命令例子
  2. “远程服务器返回错误: (404) 未找到”的正确解决方法
  3. mysql多实例(多个配置文件方式)
  4. UEFI、BIOS、Secure Boot的关系和知识介绍
  5. python 中关于py2exe打包
  6. ganglia安装与配置
  7. c# 自定义文件关联程序
  8. 下列那个语句在python中是非法的_下列哪个语句在Python中是非法的()A.x=y=z=1B.x=(y=z+1)C.x,y=y,xD...
  9. java无法获取套接字_无法从套接字获取更多数据
  10. hud抬头显示器哪个好_还在看汽车仪盘表?带你了解一下HUD抬头显示器,开车很方便...