dustland

dustball in dustland

程序员的自我修养 chapter 9 DLL

多练练

菜就DLL

DLL刚用起来的时候感觉狠抽象

用MSVC编译生成DLL的同时还会伴随形成两个文件

dll,exp,lib他仨就跟那黄金三镖客似的

使用link命令链接的时候却不用指定dll文件,只需要指定lib文件.

但是运行的时候dll还得和exe在同一个目录下面,否则就报告缺少哪个哪个dll

要是用gcc链接吧,就可以直接链接dll,什么exp什么lib根本用不到

到底怎么才算是一个标准的写法?DLL怎么写,怎么编译,怎么链接.都是问题

DLL

windows上的动态库,特点不用废话了

创建DLL

__declspec(dllexport)

表示该符号(函数或者全局变量)是要从本DLL中导出的.

就DLL屁事多,创建so动态库的时候也没见声明为导出函数

可以不使用__declspec(dllexport),改为使用链接脚本导出符号

使用链接脚本

除了用__declspec声明导入导出函数,还可以使用.def文件控制链接过程

Math.c

1
2
3
4
5
6
7
8
9
double Add(double a,double b){
return a+b;
}
double Sub(double a,double b){
return a-b;
}
double Mul(double a,double b){
return a*b;
}

Math.def

1
2
3
4
5
LIBRARY Math
EXPORTS
Add
Sub
Mul

使用链接脚本编译Math.c

1
cl Math.c /LD /DEF Math.def

此步执行后还是生成了老四样

之后链接就按照"使用DLL"进行

Math.c

一个很简单的DLL文件Math.c,这里使用__declspec(dllexport)声明导出符号

1
2
3
4
5
6
7
8
9
10
__declspec(dllexport) double Add(double a,double b){
return a+b;
}
__declspec(dllexport) double Sub(double a,double b){
return a-b;
}
__declspec(dllexport) double Mul(double a,double b){
return a*b;
}

这里所有的函数都是用__declspec(dllexport)修饰为导出函数,作用是给链接器提供信息,如果不声明为导出函数,编译链接运行也都不会出错,但是运行的时候啥也不会发生

编译Math.c

使用MSVC工具编译生成dll文件

1
cl /LDd Math.c

此后生成了四个文件,obj,lib,exp,dll

1
2
3
4
5
6
7
8
9
10
11
12
13
PS C:\Users\86135\desktop\testDLL> ls


目录: C:\Users\86135\desktop\testDLL


Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2022/8/8 15:47 222 Math.c
-a---- 2022/8/8 15:48 339968 Math.dll
-a---- 2022/8/8 15:48 810 Math.exp
-a---- 2022/8/8 15:48 1918 Math.lib
-a---- 2022/8/8 15:48 779 Math.obj
文件 作用
Math.obj 编译阶段完成的产物,目标文件
Math.lib 导入库,起到胶水作用
Math.exp
Math.dll 动态库本尊

使用DLL

__declspec(dllimport)

表示该符号是从其他DLL中导入的

使用DLL的过程就是引入DLL导出符号的过程,即导入过程

TestMath.c

同一目录下TestMath.c

1
2
3
4
5
6
7
#include <stdio.h>
__declspec(dllimport) double Sub(double,double);
int main(){
double result=Sub(1.0,8.9);
printf("result=%.2f",result);
return 0;
}

编译TestMath.c

首先将TestMath.c编译成目标文件TestMath.obj

1
cl /c TestMath.c

链接导入库

然后将TestMath.obj与刚才的库文件链接

1
link TestMath.obj Math.lib

生成了TestMath.exe

这就很奇怪了,为啥参与链接的是Math.lib一个静态库文件,而不是Math.dll动态库文件?

甚至直接链接dll文件会报错

1
2
3
4
5
PS C:\Users\86135\desktop\testDLL> link TestMath.obj Math.dll
Microsoft (R) Incremental Linker Version 14.31.31106.2
Copyright (C) Microsoft Corporation. All rights reserved.

Math.dll : fatal error LNK1107: 文件无效或损坏: 无法在 0x300 处读取

直接链接exp文件也会出错

image-20220808160800411

运行TestMath.exe

1
2
./TestMath.exe
result=-7.90

运行时链接

前面使用DLL是装载时导入动态库.

现在要在运行时导入动态库

LoadLibray

装载一个dll到进程的地址空间,作用类似于linux上的dlopen

GetProcAddress

查找某dll库中的某个符号的地址,作用类似于linux上的dlsym

FreeLibrary

卸载一个已经加载过的dll库,作用类似于dlclose

运行时链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <windows.h>
#include <stdio.h>
typedef double (*Func)(double,double);
Func getFunction(char *DllName,char *FuncName){

HINSTANCE hinstLib=LoadLibrary(DllName);
if(hinstLib==NULL){
printf("ERROR: loading failed\n");
return NULL;
}
Func function =(Func)GetProcAddress(hinstLib,FuncName);
if(!function){
printf("no such function in this module\n");
FreeLibrary(hinstLib);
return NULL;
}
return function;
}

int main(){
Func Add=getFunction("Math.dll","Add");
if(!Add){
printf("load function failed\n");
return 1;
}
double Sum=Add(5,7.9);
printf("Sum=%.2f\n",Sum);
return 0;
}

编译

1
cl Math.c

运行

1
2
./Math.exe
Sum=12.90

导出表

DataDirectory[0]

导出符号表是PE文件头的DataDirectory结构数组

1
2
3
4
5
6
7
8
PE头->
NT头->
NT可选头->
DataDirectory[0]->
IMAGE_EXPORT_DIRECTORY->
->AddressOfFunctions
->AddressOfNames
->AddressOfOrdinals
1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;

这个数组的第一项就是导出表信息

1
2
3
4
DataDirectory[0]{
VirtualAddress导出表的相对虚拟地址
Size 导出表的大小
}

IMAGE_EXPORT_DIRECTORY

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;//本模块名字,指向"Math.dll"字符串
DWORD Base;//
DWORD NumberOfFunctions;//导出函数总数
DWORD NumberOfNames;//导出符号总数
DWORD AddressOfFunctions;//导出地址表EAT
DWORD AddressOfNames;//符号名表,保存导出函数的名字,按照字典序排列,方便按图索骥
DWORD AddressOfNameOrdinals;//名字序号对应表
} IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;

导出地址表

导出地址表Export Address Table,EAT,存放的是导出函数在本库中的RVA

用IDA64观察这个位置

1
2
3
4
.rdata:000000018004F5B8 ; Export Address Table for Math.dll
.rdata:000000018004F5B8 ;
.rdata:000000018004F5B8 off_18004F5B8 dd rva Add, rva Mul, rva Sub
.rdata:000000018004F5B8 ; DATA XREF: .rdata:000000018004F5AC↑o

可以发现dd rva Add,rva Mul,rva Sub字样,意思就是Add,Mul,Sub三个函数相对虚拟地址,双击可以直接跳转到该函数的定义

转化成WORD数组得到:

1
2
3
unsigned short off_18004F5B8[6] = {
0x1000, 0x0000, 0x1040, 0x0000, 0x1020, 0x0000
};

由于imagebase=0x180000000,因此RVA=0x1000对应VA=0x180001000,正好就是Add函数的地址

1
.text:0000000180001000 Add             proc near    

然而如果不借助ida64,我们只能知道这里是一个函数的开始,但是不知道这里是哪个函数.

函数名表

函数名表中实际存放的就是字符串的指针数组,只不过这些字符串是函数的名字罢了

1
2
3
.rdata:000000018004F5C4 ; Export Names Table for Math.dll
.rdata:000000018004F5C4 ;
.rdata:000000018004F5C4 off_18004F5C4 dd rva aAdd, rva aMul, rva aSub

ida给出的解释是,0x18004F5C4这个位置是Math.dll的导出函数名表,其中有三个名字,分别是aAdd,aMul,aSub,

貌似导出地址表和函数名表建立了一一对应的关系.

导出地址表的第0项是Add函数的地址,符号名表的第0项恰好又是Add函数名字字符串的指针

导出地址表的第1项是aMul函数的地址,符号名表的第1项又恰好是Mul函数名字字符串的指针

这是因为所有的导出函数都有一个名字.如果有匿名的导出函数就堆不起来了.匿名函数必须要有一个导出地址但是不一定有导出符号,因此导出地址表的表项数是大于等于函数名表的表项数的.

那么如果有匿名函数时,再怎样建立其函数地址和函数名的关联呢?

通过第三张表,符号序号对应表

函数名符号对应表

1
2
3
.rdata:000000018004F5D0 ; Export Ordinals Table for Math.dll
.rdata:000000018004F5D0 ;
.rdata:000000018004F5D0 word_18004F5D0 dw 0, 1, 2 ; DATA XREF: .rdata:000000018004F5B4↑o

由于Math.dll中每个函数都具名,因此此时这个函数名符号对应表很奇怪,或者说很弱智

它的第i项上的值是i

1
2
3
Ordinals[0]=0
Ordinals[1]=1
Ordinals[2]=2

为啥要这样呢?这是一个历史遗留问题

历史遗留问题

早期的计算机内存很小,使用函数名就得存放字符串,这会占用大量内存.

因此当时函数导出不能使用函数名,而是使用函数序号.

这就相当于啥呢,监狱里给犯人编号,不管哪个狱警只要含1000号都是指同一个犯人.

函数序号是怎么编号的呢?一个函数在地址导出表中的下标假设是x,那么它的序号就是IMAGE_EXPORT_DIRECTORY.Base+x

如果一个exe程序需要导入这个函数,

它的导入表中只需要记录IMAGE_EXPORT_DIRECTORY.Base+x,

然后减去IMAGE_EXPORT_DIRECTORY.Base得到对应函数在Math.dll地址导出表EAT中的下标,

拿这个下标一查EAT表就得到了对应函数的相对虚拟地址了

然而为啥要加一个Base再减去他呢?这不多此一举吗?

书上并没有给出解释,我的想法是,假设A库有1000个导出函数,B库有500个刀殂函数,A库的Base=1,那么A库的函数序号会占用1~1000,如果B库和A库统一编号,则B库的Base就是1001.

现在的解决方案

在现在PC机都能达到4G,8G,16G内存的时代,显然没必要这么抠门.

但是以前的机器内存是以K或者M为单位的,以1976年的8086为例,它的内存有1M=1000K.

一个字符占用1B,假设一个库文件中有1K个函数,每个函数名字长10个字符,这就是10kB,已经占用了内存总量的1/100.

对于今天的x86_64一个16G内存的计算机,10K根本不算东西

于是现在使用符号导出,向后兼容保留了序号导入的方式

一个exe文件的导入表中保存的是符号名,如果要调用这个库函数,需要

先查对应库的函数名表,得到对应函数名字符串在函数名表中的下标i0,

用这个下标i0作为符号名序号对应表的下标去查这个Ordinals表,Ordinals[i0]-IMAGE_EXPORT_DIRECTORY.Base得到的又是一个下标i1

然后拿着这个新下标i1去查导出地址表EAT,EAT[i1]上面放着的就是对应函数的相对虚拟地址了

以书上给出的例子推导一遍

image-20220815165550845

现在需要调用Mul函数,

首先去AddressOfNames指向的函数名表查,由于这里的函数名是按照字典序排列的,可以使用二分查找加速.不管怎么,这里查找的结果是AddressOfNames[2]=Mul,也就是说,下标为2对应的是Mul

然后去AddressOfNames指向的符号序号对应表查AddressOfNames[2]=2,减去base(1)得到1,也就是说Mul在导出地址表EAT中的下标是1

然后去查AddressOfFunctions指向的导出地址表,AddressOfFunctions[1]=0x1020即Mul相对于其所在库基址的虚拟地址

向后兼容问题

为了兼容以前的序号导入,需要保证已有的函数序号不改变,

还得保证所有函数名在AddressOfNames这个函数名表中按照字典序排列

如何同时满足这两点要求呢?

现在时光回溯到七八十年代,假设Math.dll是当时开发的

假设原来的库有三个导出函数Add,Mul,Sub,一开始的时候没有符号名表(或者说有也不用),因此一开始没有导出符号,

老头子程序员(当时他还是个年轻人)给这三个函数手工编序号1,2,3(这个编号随意,只不过这样编最方便)考虑到base默认为1,那么分别对应导出地址表中的下标就得是0,1,2

于是AddressOfFunctions[0]=0x1000就是1号函数的地址,即Add函数在其库中的相对虚拟地址

函数的序号先后对函数的地址大小有要求吗?

没有,在编写math.c源代码时,哪一个函数先写哪一个的相对虚拟地址就小,

比如这里Mul在符号表中排第2比Sub的第3靠前,但是Mul的地址0x1020比Sub的地址0x1010大

image-20220815170420215

现在时光来到2202年

如今的程序员在符号名表中给这仨个函数加上了名字,此时已有的序号不能改,因此查符号表中Mul得到的下标一定得是1,同理符号表中查Add一定得到下标0,于是这三个函数的名字安排很简单

1
2
3
AddressOfNames[0]="Add";
AddressOfNames[1]="Mul";
AddressOfNames[2]="Sub";

到此完美解决了历史问题,即函数只有序号没有名字这个问题

下面考虑新增的函数怎么安排

假设要增加一个"Div"函数,这个字符串的字典序在"Add"和"Mul"之间.

因此安排上它之后AddressOfNames这个符号名表应该是

image-20220815171746124

原来查这个表中的Mul得到的下标是1,现在Div把它往后挤了一个位置,再查Mul得到的下标成2了,显然不能再拿着2去查原来的符号序号对照表了,因为原来的符号序号对照表的第2个是Sub的序号

这时候应该咋办呢?

新函数Div插入了AddressOfNames[1],那么原来的AddressOfNames[1]之后的各项顺延一个,AddressOfOrdinals数组中对应项也跟着顺延

1
2
3
4
5
6
for(int i=length(AddressOfNames);i>1;--i){
AddressOfNames[i]=AddressOfNames[i-1];
AddressOfOrdinals[i]=AddressOfOrdinals[i-1];
}
AddressOfNames[1]="Div";
AddressOfOrdinals[1]=4

这样查在AddressOfNames[i]中查Mul得到2,用2查AddressOfOrdinals[2]-base得到的还是原来的AddressOfFunctions中的下标.这就对应上了

还有一个问题没有解决, 新的函数怎么编号,新的函数放到地址导出表的那里?

地址导出表中的位置取决于编号,编号-base就是地址导出表中的下标,因此只需要确定编号

原来的编号已经有1,2,3了,那么4及之后的编号都可是使用,那么新函数Div可以获取任意一个大于等于4的编号.

假如给他的编号是4,那么4-base=4-1=3,那么导出地址表的AddressOfFunctions[3]就得是Div的相对虚拟地址

假如给他的编号是5,那么5-base=5-1=4,那么导出地址表的AddressOfFunctions[4]就得是Div的相对虚拟地址

...以此类推

如果给他的编号是5,那么4没有被使用,并且AddressOfFunctions[3]没有被使用,这合理吗?合理,由于编号4未使用,只要是日后再有新函数加进来,就可以给他分一个编号4,其相对虚拟地址就放在AddressOfFunctions[3].不流失不蒸发零浪费

书上这里就给了Div编号为5的情况

image-20220815172959162

指定导出符号

指定导出符号有多种方法,比如__declspec(dllexport)编译器拓展修饰符或者def链接脚本,或者link命令行上直接指定

__declspec(dllexport)

此种方法编译器会,在只编译不链接形成的,obj文件中的.drectve段中,加入链接指示(实际上就是命令行参数)

链接器处理obj文件时会把.drectve中的信息提取出来放到命令行上当作链接命令

1
PS C:\Users\86135\desktop\math> cl /c Math.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
SECTION HEADER #1
.drectve name
0 physical address
0 virtual address
53 size of raw data
B4 file pointer to raw data (000000B4 to 00000106)
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
100A00 flags
Info
Remove
1 byte align

RAW DATA #1
00000000: 20 20 20 2F 44 45 46 41 55 4C 54 4C 49 42 3A 22 /DEFAULTLIB:"
00000010: 4C 49 42 43 4D 54 22 20 2F 44 45 46 41 55 4C 54 LIBCMT" /DEFAULT
00000020: 4C 49 42 3A 22 4F 4C 44 4E 41 4D 45 53 22 20 2F LIB:"OLDNAMES" /
00000030: 45 58 50 4F 52 54 3A 41 64 64 20 2F 45 58 50 4F EXPORT:Add /EXPO
00000040: 52 54 3A 53 75 62 20 2F 45 58 50 4F 52 54 3A 4D RT:Sub /EXPORT:M
00000050: 75 6C 20 ul

Linker Directives
-----------------
/DEFAULTLIB:LIBCMT
/DEFAULTLIB:OLDNAMES
/EXPORT:Add
/EXPORT:Sub
/EXPORT:Mul

因此这里使用编译器拓展修饰符的作用就相当于不写拓展修饰,但是使用link命令行

1
2
cl /c Math.c
link /dll Math.obj /EXPORT:Add /EXPORT:Sub /EXPORT:Mul /DEFAULTLIB:LIBCMT /DEFAULTLIB:OLDNAMES

后面这个/DEFAULTLIB:不写也可以,默认自带.LIBCMT全称Library C multithreaded,及VC的多线程C库

link命令行

1
2
cl /c Math.c
link /dll Math.obj /EXPORT:Add /EXPORT:Sub /EXPORT:Mul

/EXPORT:<函数名>指定这个函数为导出函数

.def链接脚本

Math.def

1
2
3
4
5
LIBRARY MATH
EXPORTS
Add
Sub
Mul
1
cl Math.c /LD /DEF Math.def

def链接脚本指定符号序号

1
2
3
4
5
6
LIBRARY Math
EXPORTS
Add@1
Sub@2
Mul@3
Div @4 NONAME

NONAME意思是匿名导出函数,只有序号没有名字

exp文件

创建Math.dll是总会跟着生成一个Math.lib和一个Math.exp.

其中Math.lib是导入库.

Math.exp只是一个中间过程产物,没有作用,即使删了也不影响Math.lib和TestMath.obj的链接.

那这个玩意儿是干啥用的呢?

链接器创建DLL文件时会进行两遍扫描.

第一遍扫描会遍历所有的目标文件,收集所有导出符号以创建DLL导出表,

这第一遍扫描只是为了建立一个导出表,为了方便就直接创建了一个exp文件存放这个导出表.这个exp文件也是一个标准PE/COFF文件

第二次扫描的时候该exp文件就和其他目标文件一样链接进入DLL.由于该exp文件中只有导出表,只读属性,因此exp文件的内容会合并到DLL的.rdata只读数据段

dll文件的导出表结构基本上是从exp照搬过来的

左exp右dll导出表

exp文件的整个.edata区就只有一个导出表还有它指向的三个数组的信息

exp.edata

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.edata:0000000000000000 _edata          segment para public 'DATA' use64
.edata:0000000000000000 assume cs:_edata

//0x0到0x27是IMAGE_EXPORT_DIRECTORY结构体
.edata:0000000000000000 dd 0
.edata:0000000000000004 dd 0FFFFFFFFh
.edata:0000000000000008 dw 0
.edata:000000000000000A dw 0
.edata:000000000000000C dd offset szName ; "Math.dll" ;0x46
.edata:0000000000000010 dd 1
.edata:0000000000000014 dd 3
.edata:0000000000000018 dd 3
.edata:000000000000001C dd offset rgpv ;0x28
.edata:0000000000000020 dd offset rgszName ;0x34
.edata:0000000000000024 dd offset rgwOrd ;0x40


.edata:0000000000000028 rgpv dd offset Add ; DATA XREF: .edata:000000000000001C↑o
.edata:000000000000002C dd offset Mul
.edata:0000000000000030 dd offset Sub


.edata:0000000000000034 rgszName dd offset $N00001 ; DATA XREF: .edata:0000000000000020↑o
.edata:0000000000000034 ; "Add"
.edata:0000000000000038 dd offset $N00002 ; "Mul"
.edata:000000000000003C dd offset $N00003 ; "Sub"

.edata:0000000000000040 rgwOrd dw 0 ; DATA XREF: .edata:0000000000000024↑o
.edata:0000000000000042 dw 1
.edata:0000000000000044 dw 2

.edata:0000000000000046 ; TCHAR szName[40]
.edata:0000000000000046 szName db 'Math.dll',0 ; DATA XREF: .edata:000000000000000C↑o
.edata:000000000000004F $N00001 db 'Add',0 ;4fh ; DATA XREF: .edata:rgszName↑o
.edata:0000000000000053 $N00002 db 'Mul',0 ; ; DATA XREF: .edata:0000000000000038↑o
.edata:0000000000000057 $N00003 db 'Sub',0 ; DATA XREF: .edata:000000000000003C↑o
.edata:0000000000000057 _edata ends
[0,0x58)edata段

edata段的文件偏移为0,到0x57字节结束,一共0x58个字节,其中

[0,0x28) IMAGE_EXPORT_DIRECTORY

0到0x27共40个字节是IMAGE_EXPORT_DIRECTORY结构体,也就是DataDirectory[0].VirtualAddress指向的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions;
DWORD AddressOfNames;
DWORD AddressOfNameOrdinals;
} IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;
[0x28,0x34) 导出地址表

0x28到0x33这12个字节是IMAGE_EXPORT_DIRECTORY.AddressOfFunctions指向的地址导出表EAT

1
2
3
AddressOfFunctions[0]=0x60
AddressOfFunctions[1]=0x68
AddressOfFunctions[2]=0x70

这里0x60,0x68,0x70指向的是.edata段之后的UNDEF部分

1
2
3
4
5
UNDEF:0000000000000060 ; Segment type: Externs
UNDEF:0000000000000060 ; UNDEF
UNDEF:0000000000000060 extrn Add:near ; DATA XREF: .edata:rgpv↑o
UNDEF:0000000000000068 extrn Mul:near ; DATA XREF: .edata:000000000000002C↑o
UNDEF:0000000000000070 extrn Sub:near ; DATA XREF: .edata:0000000000000030↑o

每一项8个字节,是64位机器上一个指针的长度,看来应该是存放一个函数地址的地方.

显然这里是一个桩代码,因为exp文件不含这三个函数的定义,因此需要等到进入dll后才能确定三个函数的位置

[0x34,0x40) 函数名表

0x34到0x3F这12个字节是IMAGE_EXPORT_DIRECTORY.AddressOfNames指向的函数名表

1
2
3
IMAGE_EXPORT_DIRECTORY.AddressOfNames[0]=0x4F
IMAGE_EXPORT_DIRECTORY.AddressOfNames[0]=0x53
IMAGE_EXPORT_DIRECTORY.AddressOfNames[0]=0x57

而0x4F开始正好是三个函数名

1
2
3
.edata:000000000000004F $N00001         db 'Add',0              ; DATA XREF: .edata:rgszName↑o
.edata:0000000000000053 $N00002 db 'Mul',0 ; DATA XREF: .edata:0000000000000038↑o
.edata:0000000000000057 $N00003 db 'Sub',0 ; DATA XREF: .edata:000000000000003C↑o
[0x40,0x46) 符号序号对应表

0x40到0x45这6个字节是IMAGE_EXPORT_DIRECTORY.AddressOfOrdinals指向的符号序号对照表

这个表的表项是字类型的序号整数值,因此三个表项占用三个字,六个字节

1
2
3
IMAGE_EXPORT_DIRECTORY.AddressOfOrdinals[0]=0
IMAGE_EXPORT_DIRECTORY.AddressOfOrdinals[1]=1
IMAGE_EXPORT_DIRECTORY.AddressOfOrdinals[1]=1
[0x46,0x58) 字符串表

最后一段,用来存放字符串表,包括dll名,三个函数名

dll.rdata.导出表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.rdata:0000000180017DE0 ; Export directory for Math.dll
.rdata:0000000180017DE0 ;
.rdata:0000000180017DE0 dd 0 ; Characteristics
.rdata:0000000180017DE4 dd 0FFFFFFFFh ; TimeDateStamp
.rdata:0000000180017DE8 dw 0 ; MajorVersion
.rdata:0000000180017DEA dw 0 ; MinorVersion
.rdata:0000000180017DEC dd rva aMathDll ; Name
.rdata:0000000180017DF0 dd 1 ; Base
.rdata:0000000180017DF4 dd 3 ; NumberOfFunctions
.rdata:0000000180017DF8 dd 3 ; NumberOfNames
.rdata:0000000180017DFC dd rva off_180017E08 ; AddressOfFunctions
.rdata:0000000180017E00 dd rva off_180017E14 ; AddressOfNames
.rdata:0000000180017E04 dd rva word_180017E20 ; AddressOfNameOrdinals
.rdata:0000000180017E08 ;
.rdata:0000000180017E08 ; Export Address Table for Math.dll
.rdata:0000000180017E08 ;
.rdata:0000000180017E08 off_180017E08 dd rva Add, rva Mul, rva Sub
.rdata:0000000180017E08 ; DATA XREF: .rdata:0000000180017DFC↑o
.rdata:0000000180017E14 ;
.rdata:0000000180017E14 ; Export Names Table for Math.dll
.rdata:0000000180017E14 ;
.rdata:0000000180017E14 off_180017E14 dd rva aAdd, rva aMul, rva aSub
.rdata:0000000180017E14 ; DATA XREF: .rdata:0000000180017E00↑o
.rdata:0000000180017E14 ; "Add" ...
.rdata:0000000180017E20 ;
.rdata:0000000180017E20 ; Export Ordinals Table for Math.dll
.rdata:0000000180017E20 ;
.rdata:0000000180017E20 word_180017E20 dw 0, 1, 2 ; DATA XREF: .rdata:0000000180017E04↑o
.rdata:0000000180017E26 aMathDll db 'Math.dll',0 ; DATA XREF: .rdata:0000000180017DEC↑o
.rdata:0000000180017E2F aAdd db 'Add',0 ; DATA XREF: .rdata:off_180017E14↑o
.rdata:0000000180017E33 aMul db 'Mul',0 ; DATA XREF: .rdata:off_180017E14↑o
.rdata:0000000180017E37 aSub db 'Sub',0 ; DATA XREF: .rdata:off_180017E14↑o
[0x180017DE0,0x180017E38)

共58个字节,正好和exp.edata段对应,甚至每个字节意义都对应

成分 exp.edata dll.idata.导出表
IMAGE_EXPORT_DIRECTORY结构体 [0,0x28) [0x180017DE0,0x180017E08)
导出地址表 [0x28,0x34) [0x180017E08,0x180017E14)
函数名表 [0x34,0x40) [0x180017E14,0x180017E20)
符号序号对应表 [0x40,0x46) [0x180017E20,0x180017E26)
字符串表 [0x46,0x58) [0x180017E26,0x180017E38)

相当于把exp.edata段照搬到dll的0x180017DE0处,然后修改了到处地址表指向的函数地址.显然这个事应该是运行时动态链接器干的.

类似于GOT和PLT的机制

导入表

使用dumpbin观察Math.dll的导入表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PS C:\Users\86135\desktop\Math> dumpbin /IMPORTS Math.dll
Microsoft (R) COFF/PE Dumper Version 14.31.31106.2
Copyright (C) Microsoft Corporation. All rights reserved.


Dump of file Math.dll

File Type: DLL

Section contains the following imports:

KERNEL32.dll
18000F000 Import Address Table
180017E68 Import Name Table
0 time date stamp
0 Index of first forwarder reference

470 QueryPerformanceCounter
233 GetCurrentProcessId
237 GetCurrentThreadId
...
DA CreateFileW
94 CloseHandle

Math.dll自己偷着导入了kernel32.dll等库中的函数,然而我们并没有显式调用这些函数,也没有在链接的时候指定要链接kernel32.lib导入库.

这是因为kernel32包含的C基本运行库是自动链接的

装载器会尽量少导入函数但是保证满足所有依赖.

DataDirectory[1]

1
2
3
4
5
6
7
8
PE头->
NT头->
NT可选头->
DataDirectory[1]->
IMAGE_IMPORT_DESCRIPTOR[]->
FirstThunk->IMAGE_IMPORT_BY_NAME


PE/COFF文件的NT可选头的DataDirectory[1]就是导入表的数据目录

DataDirectory[1].VirtualAddress指向的是一个IMAGE_IMPORT_DESCRIPTOR[]结构体数组,该结构体数组以一个全空的结构体元素结尾

DataDirectory[1].Size表明该结构体数组的总大小

因此可以得到这样的公式 \[ \frac{DataDirectory[1].Size}{sizeof(IMAGE\_IMPORT\_DESCRIPTOR)}-1=有意义的导入描述符个数 \]

IMAGE_IMPORT_DESCRIPTOR

每一个需要导入的都会对应一个IMAGE_IMPORT_DESCRIPTOR导入描述符

DataDirectory[1].VirtualAddress[0]就是kernel32.dll的导入描述符结构

1
2
3
4
5
6
7
8
9
10
11
   typedef struct _IMAGE_IMPORT_DESCRIPTOR {
__C89_NAMELESS union {
DWORD Characteristics;
DWORD OriginalFirstThunk;//导入名称表INT,import name table
} DUMMYUNIONNAME;
DWORD TimeDateStamp;

DWORD ForwarderChain;
DWORD Name; //导入库名指针
DWORD FirstThunk;//导入地址表IAT,import address table
} IMAGE_IMPORT_DESCRIPTOR;

一开始时,OriginalFirstThunk和FirstThunk都是各自指向一个IMAGE_THUNK_DATA结构体数组

1
2
3
4
5
6
7
8
9
   typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString;
DWORD Function;
DWORD Ordinal;//序号
DWORD AddressOfData;//指向IMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 *PIMAGE_THUNK_DATA32;

后来INT还是指向这个结构体数组不变,但是IAT需要填上函数的实际地址

IMAGE_IMPORT_BY_NAME

1
2
3
4
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;//该符号最有可能的序号
CHAR Name[1];//该符号的符号名
} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;

动态链接器拿到符号名之后还得先去查对应库导出的函数名表,用得到的下标再去查符号序号对应表,得到的值才是序号,减去base再查导出地址表得到真正的函数地址

如果Hint就是目标符号的序号,则直接减去base就可以查出导出地址了

因此动态链接器会首先尝试使用Hint作为目标符号的序号,减去base去查导出地址表.但是这种方法不一定命中,因为导入时认为的函数序号和库中的序号可能不同.因此需要验证是否命中.

书上没有给出验证方法,我的猜测是,用Hint查符号名称对应表得到的下标,拿去直接访问符号名表,看看对应符号名是否和IMAGE_IMPORT_BY_NAME.name相同,相同则命中

如果没有命中再用符号名从头查,符号名是最靠谱的

间接调用指令

TestMath主函数中调用了Math库的Sub函数

1
double result=Sub(1.0,8.9);

其反汇编指令

1
2
140001014:	ff 15 3e 52 01 00    	call   QWORD PTR [rip+0x1523e]        # 0x140016258
14000101a: f2 0f 11 44 24 20 movsd QWORD PTR [rsp+0x20],xmm0

这有一个间接调用

1
call   QWORD PTR [rip+0x1523e]

把内存中rip+0x1523e这个地址上的四字拿出来再当作一个64位内存地址,调用该地址

如果写成call rip+0x1523e就相当于直接调用rip+0x1523e这个地址

0x140016258这个地方是啥呢?IAT

1
2
3
.idata:0000000140016258 ; Imports from Math.dll
.idata:0000000140016258 ;
.idata:0000000140016258 extrn Sub:qword ; CODE XREF: main+14↑p

idata段,给Math.dll建立的的导入地址表IAT,其中只有一项,Sub的桩代码,占用四字64个字节

显然日后这个位置需要填入正确的函数地址,这样看IAT表就相当于GOT表

__declspec(dllimport)

TestMath.exe针对Math.dll的导入地址表IAT中只有一项,是因为在TestMath.c中只声明导入了一个函数

1
__declspec(dllimport) double Sub(double,double);

这个声明告诉编译器Sub符号是从外部导入的,关于它的调用指令啃腚是间接调用.

在引入__desclpec关键字之前,编译器是不知道一个函数是本地的还是导入的,它统一都生成直接调用指令.

如果是本模块的函数,则编译器给他写上正确的地址.如果本模块中没有

这个直接调用的操作数是一个桩地址,其上只有一条指令,跳转指令,跳转到真正的函数地址.这个真正的函数地址是链接器填上的

1
2
3
CALL 0x0040100C 
...
0x0040100C: JMP DWORD PTR [0x0040D11C]

0x4D11C还是在IAT表中,DWORD PTR [0x0040D11C]才是真正的函数地址

导入函数的定位过程

在这里插入图片描述

装载前,OriginalFirstThunk和FirstThunk指向不同的地方,但是其中的数据相同

装载结束后,导入函数定位完成,OriginalFirstThunk指向的INT表不变,但是FirstThunk指向的IAT变成了函数的实际地址

这里写图片描述

装载时重定位

Rebasing,重定基地址

DLL中的代码段不是地址无关的,都是以DLL的基地址为基准,计算位置.

一般情况下,EXE程序是第一个装载进入虚拟内存的,没人和他争抢虚拟地址空间,exe程序就可以准确的装载进入ImageBase指定的基地址.在32位windows上这个地址通常是0x400000

而DLL就没有这么幸运了,DLL的默认基地址是0x10000000,假设第一个DLL需要装载时其基地址没有被占用.当第二个DLL需要装载时,其ImageBase恰好和第一个DLL相同,但是这个坑已经有人占了,那么第二个DLL应该放到哪里呢?

满足地址16K对齐要求的前提下找一个能放开此DLL的地方塞进去.

但是问题又来了.

对于64位Math.dll,其基地址是0x180000000,对于需要rip相对寻址的符号比如函数尚且好说.

但是对于需要绝对寻址的符号比如全局变量,假设其原地址为0x180001000,现在由于其他库早于Math.dll装载进入了0x180000000这个位置,那么Math.dll就得另寻他处,比如0x180010000,那么这个全局变量就得放到0x180011000.

所有引用到它的指令都需要被重定位,将该全局变量的位置从0x180001000修改为0x180011000 \[ 新符号位置-旧符号位置=新ImageBase-旧ImageBase \] 问题又来了

如果一个DLL装载进入A进程地址空间的0x180000000,装载进入B进程地址空间的0x180011000,那么该DLL中的一个需要绝对寻址的符号,在A进程地址空间中所有关于它的指令的操作数都得是0x180001000,在B进程地址空间中是0x180011000.

那么两个进程就得各自持有一份DLL的拷贝,这与linux上位置无关的so动态库不同.so只需要在物理内存中存在一份,映射进入多个进程的地址空间.DLL得有几个进程就得在物理内存中有几个拷贝

系统DLL

系统DLL比如kernel32.dll,user32.dll,gdi32.dll等等

开发人员在设计操作系统的时候就已经给他们刻意安排了一个基地址,不大容易和其他dll冲突

windows XP 32位 系统dll imagebase
user32 77D10000h
kernel32 77E40000h
shell32 773A0000h
gdi32 77C40000h

导入函数绑定

系统DLL每次加载进入进程地址空间的位置基本是不变的,一个函数今天加载到0x1800001000,明天又加载到0x1800001000,后天,大后天,一辈子都是这个位置.那么每次程序运行时都进行符号解析重定位多是一件废事儿啊

可以在运行之间就"确定"库函数的地址,即地址绑定.如果真的在装载时发生重定位,那时候再重新计算符号的真正地址也不迟.

绑定前的导入表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
C:\Users\86135\Desktop\math>dumpbin TestMath.exe /IMPORTS
Microsoft (R) COFF/PE Dumper Version 14.31.31106.2
Copyright (C) Microsoft Corporation. All rights reserved.


Dump of file TestMath.exe

File Type: EXECUTABLE IMAGE

Section contains the following imports:

MATH.dll
414110 Import Address Table
41A640 Import Name Table
0 time date stamp
0 Index of first forwarder reference

2 Sub

KERNEL32.dll
414000 Import Address Table
41A530 Import Name Table
0 time date stamp
0 Index of first forwarder reference

367 HeapFree
639 WriteConsoleW
46D QueryPerformanceCounter
...

地址绑定后的导入表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
C:\Users\86135\Desktop\math>dumpbin TestMath.exe /IMPORTS
Microsoft (R) COFF/PE Dumper Version 14.31.31106.2
Copyright (C) Microsoft Corporation. All rights reserved.


Dump of file TestMath.exe

File Type: EXECUTABLE IMAGE

Section contains the following imports:

MATH.dll
414110 Import Address Table
41A640 Import Name Table
FFFFFFFF time date stamp
FFFFFFFF Index of first forwarder reference

10001020 2 Sub

KERNEL32.dll
414000 Import Address Table
41A530 Import Name Table
FFFFFFFF time date stamp
FFFFFFFF Index of first forwarder reference

6B815FE0 367 HeapFree
6B81F1F0 63A WriteConsoleW
6B819970 46D QueryPerformanceCounter
...

可以发现每个导入函数都有了一个绝对地址,然而此时还是在文件中,没有加载进入内存,就已经预料到导入符号的地址了

然而这个关闭ASLR之后用od调试运行TestMath.exe可以发现实际上kernel32.dll和Math.dll并没有装载进入这里的位置

工具

MSVC

调整MSVC环境变量

现在的x86_64机器上MSVC默认将源代码编译成64位程序,如果需要编译成32位程序,则需要调整环境

1
Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build

这个文件夹下有几个写好的修改环境的bat文件(微软不建议自己手敲代码修改环境,因为需要修改的变量比较多)

执行其中一共bat就可以修改环境

或者直接运行相应环境的shell工具

image-20220817095545711

cl /c 只编译不链接

cl /LD 创建动态库文件

1
2
3
4
5
6
7
8
9
10
11
12
13
PS C:\Users\86135\desktop\testDLL> cl /LDd Math.c
用于 x64 的 Microsoft (R) C/C++ 优化编译器 19.31.31106.2
版权所有(C) Microsoft Corporation。保留所有权利。

Math.c
Microsoft (R) Incremental Linker Version 14.31.31106.2
Copyright (C) Microsoft Corporation. All rights reserved.

/out:Math.dll
/dll
/implib:Math.lib
Math.obj
正在创建库 Math.lib 和对象 Math.exp
1
cl /LDd Math.c  #创建带有调试信息的Math.dll动态库

dumpbin

dumpbin /EXPORTS观察导出符号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
PS C:\Users\86135\desktop\testDLL> dumpbin /EXPORTS Math.dll
Microsoft (R) COFF/PE Dumper Version 14.31.31106.2
Copyright (C) Microsoft Corporation. All rights reserved.


Dump of file Math.dll

File Type: DLL

Section contains the following exports for Math.dll

00000000 characteristics
FFFFFFFF time date stamp
0.00 version
1 ordinal base
3 number of functions
3 number of names

ordinal hint RVA name

1 0 00001000 Add
2 1 00001040 Mul
3 2 00001020 Sub

Summary

3000 .data
3000 .pdata
13000 .rdata
1000 .reloc
3C000 .text
1000 _RDATA

dumpbin /IMPORTS 观察导入符号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
PS C:\Users\86135\desktop\Math> dumpbin /IMPORTS Math.dll
Microsoft (R) COFF/PE Dumper Version 14.31.31106.2
Copyright (C) Microsoft Corporation. All rights reserved.


Dump of file Math.dll

File Type: DLL

Section contains the following imports:

KERNEL32.dll
18000F000 Import Address Table
180017E68 Import Name Table
0 time date stamp
0 Index of first forwarder reference

470 QueryPerformanceCounter
233 GetCurrentProcessId
237 GetCurrentThreadId
...
DA CreateFileW
94 CloseHandle

Summary

2000 .data
1000 .pdata
A000 .rdata
1000 .reloc
E000 .text
1000 _RDATA

dumpbin /RELOCATIONS 观察重定位信息

editbin

editbin /REBASE:BASE=<新基址> 修改基地址

editbin /BIND 绑定导入函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
PS C:\Users\86135\desktop\Math> dumpbin /IMPORTS TestMath.exe
Microsoft (R) COFF/PE Dumper Version 14.31.31106.2
Copyright (C) Microsoft Corporation. All rights reserved.


Dump of file TestMath.exe

File Type: EXECUTABLE IMAGE

Section contains the following imports:

Math.dll
140016258 Import Address Table
140020060 Import Name Table
0 time date stamp
0 Index of first forwarder reference

2 Sub

KERNEL32.dll
140016000 Import Address Table
14001FE08 Import Name Table
0 time date stamp
0 Index of first forwarder reference

370 HeapFree
64A WriteConsoleW
470 QueryPerformanceCounter
233 GetCurrentProcessId
237 GetCurrentThreadId
30A GetSystemTimeAsFileTime
38A InitializeSListHead
4F5 RtlCaptureContext
4FD RtlLookupFunctionEntry
504 RtlVirtualUnwind
3A0 IsDebuggerPresent
5E6 UnhandledExceptionFilter
5A4 SetUnhandledExceptionFilter
2F1 GetStartupInfoW
3A8 IsProcessorFeaturePresent
295 GetModuleHandleW
DA CreateFileW
503 RtlUnwindEx
27D GetLastError
564 SetLastError
149 EnterCriticalSection
3E0 LeaveCriticalSection
123 DeleteCriticalSection
386 InitializeCriticalSectionAndSpinCount
5D6 TlsAlloc
5D8 TlsGetValue
5D9 TlsSetValue
5D7 TlsFree
1C5 FreeLibrary
2CD GetProcAddress
3E6 LoadLibraryExW
145 EncodePointer
487 RaiseException
4FF RtlPcToFileHeader
2F3 GetStdHandle
64B WriteFile
291 GetModuleFileNameW
232 GetCurrentProcess
178 ExitProcess
5C4 TerminateProcess
294 GetModuleHandleExW
1F0 GetCommandLineA
1F1 GetCommandLineW
36C HeapAlloc
1B4 FlsAlloc
1B6 FlsGetValue
1B7 FlsSetValue
1B5 FlsFree
AA CompareStringW
3D4 LCMapStringW
26A GetFileType
18F FindClose
195 FindFirstFileExW
1A6 FindNextFileW
3AE IsValidCodePage
1CC GetACP
2B6 GetOEMCP
1DB GetCPInfo
412 MultiByteToWideChar
637 WideCharToMultiByte
253 GetEnvironmentStringsW
1C4 FreeEnvironmentStringsW
546 SetEnvironmentVariableW
57F SetStdHandle
2F8 GetStringTypeW
2D4 GetProcessHeap
1B9 FlushFileBuffers
21A GetConsoleOutputCP
216 GetConsoleMode
268 GetFileSizeEx
555 SetFilePointerEx
375 HeapSize
373 HeapReAlloc
94 CloseHandle

Summary

2000 .data
2000 .pdata
B000 .rdata
1000 .reloc
15000 .text
1000 _RDATA