前者稍微解除了一下PE文件格式,这次简要说一下PE的装载,PE文件中,所有段的起始地址都是页的整数倍,段的长度如果不是

页的整数倍,那就会映射时向上补齐到页的整数倍,PE文件中,连接器在生产可执行文件时,往往将所有的段尽可能的合并,所以一般

只有代码段,数据段,只读数据段和BSS等为数不多的几个段。

PE的术语中,有个相对虚拟地址的概念,其实当当与文件中的偏移量。它是相对于PE文件的装载基地址的一个偏移地址。如果一个pe文件

被装载到虚拟地址0x00400000,那么虚拟偏移地址为0x1000的地址就是0x00401000,每个pe文件在装载时都会有

一个装载目标地址,即所谓的基地址。装载一个PE可执行文件的过程如下:

先读取文件的第一个页,此页包含了DOS头,PE文件头和段表

检查进程地址空间中,目标地址是否可用

使用段表中提供的信息,将PE文件中所有的段一一映射到地址空间中相应的位置

如果装载地址不是目标地址,则进行Rebasing

装载所有PE文件所需要的DLL文件

对PE文件中的所有导入符号进行解析

根据PE头中指定的参数,简历初始化栈和堆

建立主线程并且启动进程

PE文件中,与装载有关的信息都包含在PE扩展头和段表,具体结构如下,只分析32位

typedef struct _IMAGE_OPTIONAL_HEADER32{

WORD Magic;

BYTE MajorLinkerVersion,MinorLinkerVersion;

DWORD SizeOfCode;

DWORD SizeOfInitializedData; ///初始化了的数据段长度

DWORD SizeofUninitializedData; ///未初始化的数据段长度

DWORD AddressOfEntryPoint; PE装载器准备运行的PE文件的第一个指令的RVA

DWORD BaseOfCode; 代码段起始RVA

DWORD BaseOfData; 数据段起始RVA

DWORD ImageBase; PE文件的优先装载地址

DWORD SectionAlignment; 内存中段对齐的粒度,一般为4096

DWORD FileAlignment; 文件中段对齐的粒度,一般为512字节

WORD MajorOperatingSystemVersion;

WORD MinorOperatingSystemVersion;

WORD MajorImageVersion;

WORD MinorImageVersion;

WORD MajorSubsystemVersion; 程序运行所需要的子系统版本

WORD MinorSubsystemVersion;

DWORD Win32VersionValue;

DWORD SizeofImage; 内存中整个PE映像体的尺寸

DWORD SizeofHeaders; 所有头+节表的大小,等于文件尺寸减去文件中所有节的尺寸

DWORD CheckSum;

WORD subsystem; NT用来识别PE文件属于哪个子系统,GUI和CUI

DWORD sizeofStackReserve;

DWORD sizeofStackCommit;

DWORD sizeofHeapReserve;

DWORD sizeofHeapCommit;

DWORD LoaderFlags;

IMAGE_DATA_DIRECTORY DataDirectory[16];

}IMAGE_OPTIONAL_HEADER32,*PIMAGE_OPTIONAL_HEADER32

typedef struct _IMAGE_DATA_DIRECTORY{

DWORD VirtualAddress;

DWORD Size;

}IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;

关于动态链接

要解决空间浪费和更新困难的简单办法就是把程序的模块相互分割开来,形成独立的文件,而不再将他们静态的连接在一起,

简单来说,就是不对那些组成程序的目标文件进行连接,等到程序要运行时才进行连接,也就是说,把连接过程推迟到运行时

在进行,这就是动态连接

动态连接的思想是把程序按照模块拆分成各个相对独立的部分,在程序运行时才将他们连接在一起形成一个完整的程序,而不是

像静态链接那样把所有的程序模块都连接在一个单独的可执行文件。换句话说,动态链接把连接过程从本来的程序装载前推迟到了

装载的时候

在静态链接时,整个程序最终只有一个可执行文件,它是一个不可以分割的整体,但是在动态连接下,一个程序被分成了若干个文件

有程序的主要部分,即可执行文件和程序所依赖的共享对象,很多时候称为模块。

/* Program1.c */

#include "lib.h"

int main()

{

show(1);

return 0;

}

/* Program2.c */

#include "lib.h"

int main()

{

show(2);

return 0;

}

/* lib.c */

#include <stdio.h>

void show(int i)

{

printf("Printing from lib.so %d\n",i);

}

/**  lib.h **/

#ifndef LIB_H

#define LIB_H

void show(int i);

#endif

下面是进程运行时的虚拟地址空间分布

$cat /proc/12985/maps

08048000-08049000 r-xp 00000000 08:01 1343422 ./Program1

08049000-0804a000 rwxp 00000000 08:01 1343432 ./Pragram1

b7e83000-b7e84000 rwxp b7e83000 00:00 0

b7e84000-b7fc8000 r-xp 00000000 08:01 1488993 /lib/tls/i686/cmov/libc-2.6.1.so

b7fc80000-b7fc9000 r-xp 00143000 08:01 1488993 /lib/tls/i686/cmov/libc-2.6.1.so

b7fc9000-b7fce000 r-xp 00144000 08:01 1488993 /lib/tls/i686/cmov/libc-2.6.1.so

b7fcb000-b7fce000 rwxp b7fcb000 00:00 0

b7fd8000-b7fd9000 rwxp b7fd8000 00:00 0

b7fd9000-b7fda000 r-xp 00000000 08:01 1343290 ./lib.so

b7fda000-b7fdb000 rwxp 00000000 08:01 1343290 ./lib.so

b7fdb000-b7fdd000 rwxp b7fdb000 00:00 0

b7fdd000-b7ff7000 r-xp 00000000 08:01 1455332 /lib/ld-2.6.1.so

b7ff7000-b7ff9000 rwxp 00019000 08:01 1455332 /lib/ld-2.6.1.so

bf965000-bf97b000 we-p bf965000 00:00 0 [stack]

ffffe000-fffff000 r-xp 00000000 00:00 0 [vdso]

可以看到,整个进程虚拟地址空间中,多出了几个文件的映射。lib.so与program1一样,都是被操作系统用同样的方法映射到进程的虚拟地址空间。

关于ld-2.6.so,实际删格式linux下的动态连接器,动态连接器与普通共享对象一样被映射到了进程的地址空间,在系统开始运行program1之前,

首先会把控制权交给动态连接器,由他完成所有的动态链接工作以后再把控制权交给program1,然后开始执行。

共享对象的最终链接装在地址在编译时是不确定的,而是在装载时,装载器根据当前地址空间的空闲情况,动态分配一块大小的虚拟地址空间给

相应的共享对象。

其实程序模块的指令和数据中可能会包含一些绝对地址的引用,在连接产生输出文件的时候,就要假设模块被装载 的目标地址。但是共享对象在

编译时不能假设自己在进程虚拟地址空间中的位置。与此不同的是,可执行文件基本可以确定在进程虚拟空间中的起始位置,因为可执行文件往往

是第一个被加载的文件,它可以选择一个固定空闲的地址。在连接时的重定位称为连接时重定位,而此时,装载时也需要对模块地址进行重定位

,我们称为装载时重定位。而windows下又称为基址重置。

装载时重定位虽然解决了动态模块中绝对地址引用,但是使得指令部分无法再多个进程之间共享,此时我们希望程序模块中共享的指令部分在

装载时不需要根据装载地址的改变而改变,所以需要把指令中那些需要被修改的部分分离出来,和数据部分放在一起,这样指令部分就可以保持

不变,而数据部分可以在每个进程中拥有一个副本,这种方法被称为地址无关代码PIC。

其实产生地址无关代码并不麻烦,先将模块中各种类型的地址引用方式是否跨模块分为两类:模块内部引用和模块外部引用:按照不同的引用

方式又可以分为指令引用和数据访问。此时分为四种情况

模块内部的函数调用、跳转等

模块内部的数据访问,比如模块中定义的全局变量、静态变量

模块外部的函数调用、跳转等。

模块外部的数据访问,比如其它模块中定义的全局变量。

static int a;

extern int b;

extern void ext();

void bar()

{

a = 1;                       ///模块内部数据访问

b = 2; ///模块外部数据访问

}

void foo()

{

bar(); ///模块内部函数访问

ext(); ///模块外部函数访问

}

当编译器在编译此文件时,实际上不能确定变量b和函数ext() 是模块外部的还是模块内部的,因为它们有可能定义在同一共享对象的其它目标文件

中。由于没法确定,编译器只能把它们都当做外部函数和变量来处理。msvc编译器提供了__declspec(dllimport)扩展来标识一个符号是模块内部

还是模块外部的。

第一种情况中,对于被调用函数和调用者都处于同一个模块,他们之间的相对位置是固定的,因此模块内部的跳转、函数调用都可以是相对地址

调用,或者基于寄存器的相对调用,所以对于这种指令是不需要重定位的。

<bar>:

8048344: 55 push %ebp

8048345: 89  e5 mov %esp,%ebp

8048347: 5d pop %ebp

8048348: c3 ret

8048349: <foo>:

......

8048357: e8  e8  ff  ff  ff call 8048344 <bar>

804835c: b8  00  00  00 00 mov $0x0,%eax

......

foo中对bar的调用那条指令实际上就是一条相对地址调用指令,此条指令中后4个字节是目的地址相对于当前指令的下一条指令的偏移,即

0xffffffe8,0xffffffe8是-24的补码形式,即bar的地址为0x804835c-24 = 0x8048344 只要bar和foo的相对位置不变,这条指令是地址无关的,

这种相对地址的方式对于jmp指令也是有效的。

很明显,指令中不能直接包含数据的绝对地址,唯一的办法就是使用相对地址,一个模块前面一般是若干个页的代码,后面紧跟若干个页的数据

这些页之间的相对位置是固定的,如此,任何一条指令与它需要访问的模块内部数据之间的相对位置是固定的,只需要相对于当前指令加上固定

偏移量就可以访问模块内部的数据了。

00000044c <bar>:

44c: 55 push %ebp

44d: 89 e5 mov %esp,%ebp

44f: e8  40  00  00 00   call 494 <__i686.get_pc_thunk.cx>

454: 81  c1  8c  11  00  00 add  $0x118c,%ecx

45a: c7  81   28  00  00  00  01 movl $0x1,0x28(%ecx)

461: 00  00  00

464: 8b  81  fb  ff  ff  ff mov 0xfffffff8(%ecx),%eax

46a: c7  00  02  00 00 00 movl $0x2,(%eax)

470: 5d pop %ebp

471: c3 ret

00000494 <__i686.get_pc_thunk.cx>

494: 8b  0c  24 mov (%esp),%ecx

497: c3 ret

当处理器执行call指令以后,下一条指令的地址就会被压到栈顶,而esp寄存器始终指向栈顶,当"__i686.get_pc_thunk.cx"执行"mov (%esp),%ecx"

时,返回地址就被赋值到ecx寄存器了。

接着执行一条add和一条mov,就可以看到遍历a地址是add指令地址(保存在ecx寄存器)加上另个偏移量0x118c和0x28,即如果模块被装载到

0x10000000这个地址,那么变量a的实际地址是0x100000000 + 0x454 +0x118c + 0x28 = 0x10001608 如图

|--------------------------------------------------------------------------------0x00000000

|

|

|

|

|---------------------------------------------------------------------------------0x10000000

|--------- |  44f: e8  40  00  00 00    call 494 <__i686.get_pc_thunk.cx>

| | 454: 81  c1  8c  11  00  00 add  $0x118c,%ecx

| |  45a: c7  81   28  00  00  00  01 movl $0x1,0x28(%ecx)

| |  461: 00  00  00

| | .text

0x118c  +  0x28   |

| |

| |--------------------------------------------------------------------------------------

| |

|--------- |static int a;

|

|

| .data

|

|----------------------------------------------------------------------------------------

而模块间的数据访问,需要等到装载时才决定,例如变量b,被定义在其它模块中,并且改地址在装载时才能确定,使得代码地址无关,基本

思想就是把跟地址相关的部分放到数据段中,很明显,这些其它模块的全局变量的地址是跟模块装载地址有关的,此时在数据段建立一个

指向这些变量的指针数组,也称为全局偏移表GOT,当代码需要引用该全局变量时,可以通过GOT中相对应的项间接引用。当指令中需要访问

变量b时,程序先找到GOT,此时根据GOT中变量所对应的项找到变量的木匾地址,每个变量对应一个4字节的地址,连接器在装载模块的时候

会查找每个变量所在的地址,填充GOT中各个项,以确保每个指针指向地址正确。由于GOT本身是放在数据段的,所以它可以在模块装载时被修改

,并且每个进程都可以有独立的副本。

模块在编译时可以确定模块内部变量相对与当前指令的偏移,那么我们也可以在编译时确定GOT相对于当前指令的偏移即确定GOT的位置,然后根据

变量地址在GOT中的偏移就可以得到变量地址。

但是定义在模块内部的全局变量该如何处理呢?比如一个共享对象定义了一个全局变量global,在模块module.c中是这么引用的

extern int global;

int foo()

{

global = 1;

}

此时编译器编译module.c时,无法根据上下文判断global是定义在同一个模块的其它目标文件还是定义在另外一个共享对象之中,即无法判断

是否跨模块调用,也就是无法判断是通过GOT方式引用还是在本地可执行文件.bss中。此时我们把所有的使用这个变量的指令都指向位于可执行文件

中的那个副本。elf共享库在编译时,默认把定义在模块内部的全局变量当做定义在其他模块的全局变量,通过GOT来实现变量的访问。当共享模块

被装载时,如果某个全局变量在可执行文件中拥有副本,那么动态链接器就会把GOT中相应地址指向该副本,这样该变量在运行时实际上最终只有

一个实例。如果变量在共享模块中被初始化,俺么动态连接器还需要将该初始化值复制到程序主模块中的变量副本。如果该全局变量在程序主模块中

没有副本,那么GOT的相应地址就指向模块内部的该变量副本。

如果lib.so中定义了一个全局变量G,而进程A和进程B都是用了lib.so,那么当进程A改变G时,进程B会受到影响吗?

不会,当lib.so被两个进程加载时,它的数据段部分在每个进程中都有独立的副本,此时,共享对象中的全局变量实际上和定义在程序内部的全局变量

没什么区别,任何一个进程访问的只是那个副本,而不会影响到其它进程,但是如果是同一个进程的线程A和线程B,此时是会影响到的。此时windows

上有个专门的术语线程私有存储(Thread Local Storage).

此时我们该对比一下静态链接和动态连接的区别了,动态连接比静态链接慢的主要原因是动态链接下对于全局和静态的数据访问都要进行负载的GOT

定位,并进行间接寻址;对于模块间的调用也要先进性GOT,然后间接跳转。如此一来,必然使程序的运行速度收到影响。同时,动态链接的连接工作

在运行时完成,即程序执行时,还要进行一次连接工作,装载所需要的共享对象,然后进行符号查找地址重定位等工作。针对第二种情况,基于共享对象

中的很多函数不会被用到,如果一开始就把所有函数都连接实际上也是一种浪费。所以此时采用一种延迟绑定的做法,基本思想就是,函数第一次

用到时才进行绑定(符号查找、定位)

elf使用PLT(Procedure Linkage Table)方法实现。假设liba.so需要调用libc.so中的bar函数,那么当liba.so第一次调用bar函数时,需要动态连接器

中的某个函数来完成地址绑定工作,我们假设lookup()来查询bar地址,此时lookup需要知道地址绑定发生在哪个模块,哪个函数。

lookup(module,function),当调用外部模块的函数时,通常用GOT中相应项进行间接跳转,PLT为了实现延迟绑定,又增加了一层间接跳转。

此时,每个外部函数在PLT中都有一个相应的项,比如bar的项地址为bar@plt。实现如下

bar@plt:

jmp *(bar@GOT)

push n

push moduleID

jump _dl_runtime_resolve

很明显,第一条指令的效果就是跳转到第二条指令,而第二条指令将n压入堆栈,这个数字是bar这个符号引用在重定位表的.rel.plt的下标,接着

又是将moduleid压入堆栈,然后跳转到_dl_module_resolve

这实际就是lookup(module,function)的调用。

其实PLT真正实现起来要复杂一些,elf将GOT拆分为.got和.got.plt,其中.got用来保存全局变量引用地址,而.got.plt用来保存函数的地址,对于外部函数

的引用部分被分离出来放入.got.plt中,另外.got.plt的前三项如下:

第一项保存的是.dynamic段的地址,此段描述了本模块动态连接相关的信息

第二项保存本模块的ID,第三项保存的是_dl_runtime_resolve的地址。

茫无头绪的瞎侃(一)相关推荐

  1. 转《胡侃学习(理论)计算机》的心得

    今天推荐的是篇老帖,南京大学sir先生的<胡侃>以及后来的两篇补充帖子.算算是十几年前的帖子了,我知道帖子出自南京大学的BBS,百度了一下,却没有翻到原文.不过百度到了一大堆不负责任的转帖 ...

  2. 【转】胡侃学习(理论)计算机

      原帖大概是出自南京大学的BBS,南京大学sir先生的<胡侃>,算算应该是十几年前的老帖子了,后来又被某位作者追加了一些他自己的读后感进去,就是现在这样.原帖可能网上找不到了,转载的倒是 ...

  3. 奶猫侃GPS(屏幕、CMMB部分)

    两篇帖子<瞎侃--GPS市场之我见>和<奶猫再侃GPS>转眼间已经过去了一年多,现在已经来到了09年的夏天. GPS市场上的格局又发生了变化. 早在08年下半年,根据对市场的预 ...

  4. 胡侃学习(理论)计算机【被大佬推荐,转载以膜拜】

    <胡侃学习(理论)计算机> 作者: Sir (阿涩) 我也来冒充一回高手,谈谈学习计算机的一点个人体会.由于我是做理论的,所以先着重谈谈理论. 记得当年大一,刚上本科的时候,每周六课时数学 ...

  5. 郭天祥:我的大学六年

    在哈尔滨工程大学六年,我在学校电子创新实验室呆了四年,这四年里创新实验室给我提供了良好的学习环境和完善的实验设备:在与众多电子爱好者的交流中,使我学到了更多的专业知识:在学校老师们的教导下,让我学会了 ...

  6. 机器人彩铅画_彩铅画嗔

    彩铅画<嗔>未完成图. 这个题目也用过.喜欢画女孩娇憨薄嗔的表情. 薄嗔不是盛怒,是爱多于恼的小情绪.要人知道自己在介意.在生气.要人安慰要人哄.不需要头头是道的道理.不需要来龙去脉的原因 ...

  7. curl 访问不到html_嵌入式工程师入门前后端系列1:访问一个网页

    做为嵌入式行业的从业者,最近经常听到PAAS,SAAS等和"云"相关的概念,被整的一头雾水.很多时候咱们的物联网硬件设备都会有一个云平台,用于设备管理或者UI应用展示等功能,这通常 ...

  8. 谷歌看下!罗永浩谈谷歌砍掉平板线 :主要是因为软件太烂

    最近老罗又恢复了以往的风格,在微博上活跃的像个高仿号.刚刚认真考虑了收购苹果事宜的他,最近就谷歌砍掉平板线发表了意见. 跌跌撞撞9年时间,谷歌终于宣布放弃自己的平板电脑业务,同时谷歌还宣布硬件团队将专 ...

  9. 一个网工的十年奋斗史 - 移民篇

    我在茶余饭后总能听到:某同事出国以后的生活多好,什么时候买了个别墅大house,什么时候晒了一下蓝天白云没有雾霾,让人羡慕不已. 可是我们也同样忽略了移民背后的努力和艰辛.殊不知对于移民的人来说,需要 ...

最新文章

  1. 锁定计算机好在下游戏吗,巧用win7锁定计算机 防止孩子沉迷游戏
  2. Weblogic常见配置
  3. PMP知识点(一、全局概览)
  4. RabbitMQ Header模式
  5. 第2章 Python 数字图像处理(DIP) --数字图像基础4 -- 像素间的一些基本关系 - 邻域 - 距离测试
  6. 每日一笑 | 爱的魔力转圈圈~
  7. 3d点击_3D打印服务加工在医疗器械行业的应用
  8. 电子商务对物流的影响
  9. c语言编写conio库函数,c语言库函数头文件注释
  10. extend 和 append 的区别
  11. crazy-tentacles -- 一个非常有意思的东西
  12. php 滑块验证,实现一个滑块验证功能
  13. 田刚:庞加莱猜想与几何
  14. Warning: To load an ES module, set “type“: “module“ in the package.json or use the .mjs extensi
  15. docker 安装完成后测试hello-world出现问题(Unable to find image 'hello-world:latest' locally)
  16. vue中wath的源码实现
  17. Java基本方法命名
  18. matlab中图像批量改名字,MATLAB中批量修改文件的名字
  19. ubuntu16奥比中光相机标定
  20. 潘建伟:量子技术实现“绝对安全”通信

热门文章

  1. 2015年最新Android基础入门教程目录(完结版)
  2. 洛谷刷题 (Python)P1425 小鱼的游泳时间
  3. Meta-StyleSpeech : Multi-Speaker Adaptive Text-to-Speech Generation
  4. instagram营销全攻略,看这一篇就够了(附10个分析工具)
  5. js数组的findIndex和indexOf对比
  6. 学计算机的笔记本电脑配置,如何查看笔记本电脑的配置 查看笔记本配置的方法【详细步骤】...
  7. [源码和文档分享]基于VC++的WIN32 API界面编程实现的百战天虫小游戏
  8. Everything 打开文件失败
  9. 计算机病毒对电脑的影响,电脑病毒有什么危害呢 电脑病毒对个人PC的危害
  10. docker常规操作——启动、停止、重启容器实例