PE 格式学习总结(三)– PE文件中的输入函数

    关于输入部分,我们将详细介绍关于输入函数的各种结构,通过一个例子来说明输入函数及其相关结构是怎么放在PE文件中的。以及如何在PE文件中找到这些东西。

一 找到输入部分在文件中位置。

1.1 得到PE Header在文件中的位置。
    通过DOS Header结构的成员e_lfanew,可以确定PE Header的在文件中的位置。

1.2 得到文件中节的数目。
    确定PE Header的在文件中的位置之后,就可以确定PE Header中的成员FileHeader和成员OptionalHeader在文件中的位置。根据 FileHeader 中的 成员NumberOfSections 的值,就可以确定文件中节的数目,也就是节表数组中元素的个数。

1.3 得到节表在文件中的位置。
    PE Header在文件中的位置加上PE Header结构的大小就可以得到节表在文件中的开始位置。PE Header结构的大小可以由Signature的大小加上FileHeader的大小再加上FileHeader中的 SizeOfOptionalHeade来确定。其实到目前为止SizeOfOptionalHeade也就是结构Optional Header的大小也是固定的,所以整个PE Header结构的大小也是固定。不过为了安全起见,还是用Signature的大小加上FileHeader的大小再加上FileHeader中的SizeOfOptionalHeade来确定比较保险。

1.4 得到输入部分在文件中的位置。
    第1.2步中我们确定了文件中节的数目,第1.3步中我们确定了节表在文件中的位置。
    现在来确定输入部分在文件中的位置。
    取得PE Header中的Optional Header中的DataDirectory数组中的第二项,
也就是输入部分项。DataDirectory[]数组的每项都是IMAGE_DATA_DIRECTORY结构,该结构定义如下。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
取得DataDirectory数组中的第二项中的成员VirtualAddress的值。这个值就是在内存中资源节的RVA。
如果这个RVA的值为0表示这个PE文件中没有输入部分。
然后根据节的数目,遍历节表数组。也就是从0到(节表数-1)的每一个节表项。
每个节在内存中的RVA的范围是从该节表项的成员VirtualAddress字段的值开始(包括这个值),
到VirtualAddress+Misc.VirtualSize的值结束(不包括这个值)。
我们遍历整个节表,看我们取得的输入部分的RVA,在哪个节表项的RVA范围之内。
如果在范围之内,就找到了输入部分所在节的节表项。
这 个节表项中的 PointerToRawData 中的值,就是输入部分所在节在文件中的位置。这个节表项中的VirtualAddress 中的值,就是输入部分所在节在内存中的RVA。用输入部分的RVA减去输入部分所在节的RVA,就可以得到输入部分在该节内偏移。用这个偏移加上该节的在 文件中的位置,就可以得到输入部分在文件中的位置。即DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]. VirtualAddress – SectionTable[i].VirtualAddress + SectionTable[i].PointerToRawData 。

这样我们就得到了输入部分在文件中开始的位置。

二 PE文件中的输入部分。

    输入部分,如果要调用别的PE文件中的输出函数,需要那些东西呢?首先需要知道所需函数在哪个文件中,比如函数 NtRaiseHardError 就在PE文件 ntdll.dll 中。所以我们需要一个文件名。而如何找到某个函数的入口地址呢,我们还需要知道该函数的函数名,或者改函数的序号,通过这两者的任一种,我们就可以找到该 函数的入口地址(如果不知道为什么,请看

IURL PE 格式学习总结(二)– PE文件中的输出函数)。所以我们还需要函数名或者序号,这两者之一。PE文件的输入部分,有这些内容。我们还可以想到,当一个PE文件被执行的时候,它 会把所用的输入函数所在的每一个文件载入内存,并且,根据函数名或者序号,获得每一个输入函数的入口地址,存放起来,在程序执行的时候使用。还有就是,一 个可执行文件一般都使用好几个PE文件(通常是dll)的输出函数。所以需要有多个dll(就说成dll吧,提供输出函数的PE文件差不多都是dll,下 面就按dll说)的相关信息。

    前面我们已经得到了输入部分在文件中开始的位置,在输入部分的最开始,是一个IMAGE_IMPORT_DESCRIPTOR 结构数组,这个数组的最后一个元素内容全为空,标示着这个数组的结束,这个数组的每个元素,保存着一个dll的相关信息。紧跟着这个 IMAGE_IMPORT_DESCRIPTOR数组的是几个紧挨着的DWORD数组, 数组的每个元素存有函数名字符串的RVA,或者直接保存序号,每个数组的最后一项为空,标示结束。这几个数组之后,紧跟着的是dll名字的字符串和各个输 入函数名结构。

IMAGE_IMPORT_DESCRIPTOR 结构在WINNT.H中定义如下。

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
};
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real datetime stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)

DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;

这个结构长度为20个字节,共有5个字段。
各字段含义如下:

OriginalFirstThunk:(在WINNT.H中Characteristics这个叫法已经不对了)这里实际上保存着一个RVA,这 个RVA指向一个DWORD数组,这个数组可以叫做输入查询表。每个数组元素,或者叫一个表项,保存着一个指向函数名的RVA或者保存着一个函数的序号。
TimeDateStamp:当这个值为0的时候,表明还没有bind。不为0的话,表示已经bind过了。有关bind的内容后面介绍。
ForwarderChain:
Name:一个RVA,这个RVA指向一个ascii以空字符结束的字符串,这个字符串就是本结构对应的dll文件的名字。
FirstThunk:一个RVA,这个RVA指向一个DWORD数组,这个数组可以叫输入地址表。如果bind了的话,这个数组的每个元素,就是一个输入函数的入口地址。

输入查询表,就是OriginalFirstThunk所指向的那个DWORD数组,它的每一个元素是一个DWORD值,当最高位为1时,低31位 中的值,就是一个序号。当最高位为0时,这个元素的值就是一个指向一个输入函数名结构的RVA。这个数组的最后一个元素值为空,表示数组的结束。

输入函数名结构,在WINNT.H中定义如下。

typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

这个结构的长度不定,有两个成员。第一个成员是一个WORD类型,长2个字节,保存着输入函数的序号。第二个成员是一个ascii字符串,这个字符 串是输入函数的名字。为了保证字对齐,可能会在ascii结束符之后再填充一个。比如,1b 01 4e 74 54 65 72 6d 69 6e 61 74 65 50 72 6f 63 65 73 73 00 00 ,如果不填充最后一个00的话,长度为21个字节,不是字对齐。所以要填充一个00。

输入地址表,就是FirstThunk所指向的那个DWORD数组,它的每一个元素是一个DWORD值。如果程序已经bind了的话,(判断依据是 TimeDateStamp,TimeDateStamp为0则没有bind)那么这里的每个元素的值,就是一个输入函数的入口地址。如果没有bind的 话,那么在本pe文件执行时,载入器会载入dll文件,获得每一个输入函数的入口地址,并填入这个输入地址表的每一项中。(这些是我猜的,大家但愿我猜对 吧)这个数组的最后一个元素值为空,表示数组的结束。

bind,从上面的介绍中可以看到,如果没有bind的话,每次pe文件被执行时,载入器都要查询一遍每个函数的入口地址,所以为了优化这一点,就有了bind,把入口点直接存在输入地址表中。

载入器会载入所需要的dll。注意一下没有bind的情况下,载入器对输入部分所要做的事情。总之,在载入之后,所需的dll(根据文件名)已经都被载入到内存。并且输入地址表中的每一个元素都是一个输入函数的入口地址了。

下面我们来看一个例子,通过例子就可以明白是怎么回事了。

我们的例子是Win2k中的exe文件csrss.exe。为了防止大家版本不同,本文附带了这个PE文件。

每个结构的不同成员用 / 分开。每行是一个结构。可以用16进制编辑器打开附带的 routetab.dll 对照着看。
括号中内容为注释。

用开始讲到的寻找输入部分在文件中位置的方法,我们找到了输入部分在文件中的位置为000008DCh。
我们来计算一下第一个IMAGE_IMPORT_DESCRIPTOR中的OriginalFirstThunk,Name,FirstThunk。
输入部分所在节的开始rva(由DataDirectory[2]得到)为1000h。输入部分在节在文件中的位置为600h。
Name 为rva(值从结构中可以看到是0000135e,如果你不明白为什么是0000135e而不是5e130000的话,请看 《JIURL PE 格式学习总结(一)》中关于 big-endian和little-endian的介绍),则Name相对于所在节开始处的偏移为135e-1000。而Name在文件中的位置为 Name在相对于所在节开始的偏移加上所在节开始处在文件中的位置。所以Name在文件中的位置为135eh-1000h+600h=95eh。同样方法 我们可以算出, OriginalFirstThunk:
1318-1000+600=918。FirstThunk:1000-1000+600=600。

000008DC: 18 13 00 00 / ff ff ff ff / ff ff ff ff / 5e 13 00 00 / 00 10 00 00
(结构IMAGE_IMPORT_DESCRIPTOR,每个代表一个dll。可以看到两个IMAGE_IMPORT_DESCRIPTOR,所以本PE文件的输入函数,是由两个dll提供的。第三个全为空,表示结束。)
000008F0: 20 13 00 00 / ff ff ff ff / ff ff ff ff / c2 13 00 00 / 08 10 00 00
(结构IMAGE_IMPORT_DESCRIPTOR)
00000904: 00 00 00 00 / 00 00 00 00 / 00 00 00 00 / 00 00 00 00 / 00 00 00 00  
(全为空,表示结束IMAGE_IMPORT_DESCRIPTOR数组结束)
00000918: 44 13 00 00 (文件中的地址为1344-1000+600=944,指向一个输入函数名结构)
0000091C: 00 00 00 00 (为空,一个输入查询表结束)
00000920: 84 13 00 00 (文件中的地址为1384-1000+600=984,指向一个输入函数名结构)
00000924: 98 13 00 00 (1398-1000+600=998)
00000928: 6a 13 00 00 (136a-1000+600=96a)
0000092C: ae 13 00 00 (13ae-1000+600=9ae)
00000930: cc 13 00 00 (13cc-1000+600=9cc)
00000934: dc 13 00 00 (13dc-1000+600=9dc)
00000938: ee 13 00 00 (13ee-1000+600=9ee)
0000093C: 0e 14 00 00 (140e-1000+600=a0e)
00000940: 00 00 00 00 (为空,一个输入查询表结束)
00000944: 18 00 / 43 73 72 53 65 72 76 65 72 49 6e 69 74 69 61 6c 69 7a 61 74 69 6f 6e 00
(输入函数名结构 IMAGE_IMPORT_BY_NAME hint为18 Name为 "CsrServerInitialization.")
0000095E: 43 53 52 53 52 56 2e 64 6c 6c 00 00
(第一个IMAGE_IMPORT_DESCRIPTOR的Name指向这里"CSRSRV.dll")
0000096A: 00 01 / 4e 74 53 65 74 49 6e 66 6f 72 6d 61 74 69 6f 6e 50 72 6f 63 65 73 73 00
("NtSetInformationProcess.")
00000984: 1c 01 / 4e 74 54 65 72 6d 69 6e 61 74 65 54 68 72 65 61 64 00
00000998: 1b 01 / 4e 74 54 65 72 6d 69 6e 61 74 65 50 72 6f 63 65 73 73 00 00
000009AE: d8 00 / 4e 74 52 61 69 73 65 48 61 72 64 45 72 72 6f 72 00 00
000009C2: 6e 74 64 6c 6c 2e 64 6c 6c 00

000009CC: 0d 00 / 44 62 67 42 72 65 61 6b 50 6f 69 6e 74 00
000009DC: 4a 01 / 52 74 6c 41 6c 6c 6f 63 61 74 65 48 65 61 70 00
000009EE: 85 02 / 52 74 6c 55 6e 69 63 6f 64 65 53 74 72 69
6e 67 54 6f 41 6e 73 69 53 74 72 69 6e 67 00 00
00000A0E: 30 02 / 52 74 6c 4e 6f 72 6d 61 6c 69 7a 65 50 72 6f 63 65 73 73 50 61 72 61 6d 73 00
00000A2A: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00000A3A: …

00000600: 38 1f f8 5f
00000604: 00 00 00 00 (为空,一个输入地址表结束)
00000608: 6d f0 f8 77
0000060C: d8 c3 f8 77
00000610: a5 b7 f8 77
00000614: 38 a4 f9 77
00000618: df f9 f9 77
0000061C: 6b 97 fc 77
00000620: ec e5 f8 77
00000624: 18 2c f9 77
00000628: 00 00 00 00 (为空,一个输入地址表结束)

本例比较可惜的是,在两个输入查询表中,都是函数名结构的RVA,没有直接的序号(是序号还是RVA的判别方法为,看最高位是否为1,为1,其余部分表示序号。为0,整个字段表示RVA)。

三 遍历PE文件中的输入

    用while循环,遍历IMAGE_IMPORT_DESCRIPTOR数组的每个元素(每个可以找到一个dll的信息和该 dll提供的输入函数)。当某元素的值都为空时,表示遍历到了数组的最后。而对于IMAGE_IMPORT_DESCRIPTOR数组的一个元素,再用 while循环,遍历IMAGE_IMPORT_DESCRIPTOR中,两个RVA所指的两个DWORD数组,输入查询表和输入地址表。判断结束的条件 也是看,是否数组元素的值已经为空了。也就是while(..){..while(..){}..}这样就可获得每一个有关输入的内容。

    实现遍历输入的源程序,可以参考 PEDUMP – Matt Pietrek 1995 。《Windows95系统程式设计大奥秘》附书源码中有。

本文所使用的PE文件csrss.exe

Read: 732

PE 格式学习总结(二)– PE文件中的输出函数

    一般来说输出函数都是在dll中。我们将详细介绍关于输出函数的各种结构,通过一个例子来说明输出函数及其相关结构是怎么放在PE文件中的。以及如何在PE文件中找到这些东西。

一 找到输出函数在文件中位置。

1.1 得到PE Header在文件中的位置。
    通过DOS Header结构的成员e_lfanew,可以确定PE Header的在文件中的位置。

1.2 得到文件中节的数目。
    确定PE Header的在文件中的位置之后,就可以确定PE Header中的成员FileHeader和成员OptionalHeader在文件中的位置。根据 FileHeader 中的 成员NumberOfSections 的值,就可以确定文件中节的数目,也就是节表数组中元素的个数。

1.3 得到节表在文件中的位置。
    PE Header在文件中的位置加上PE Header结构的大小就可以得到节表在文件中的开始位置。PE Header结构的大小可以由Signature的大小加上FileHeader的大小再加上FileHeader中的 SizeOfOptionalHeade来确定。其实到目前为止SizeOfOptionalHeade也就是结构Optional Header的大小也是固定的,所以整个PE Header结构的大小也是固定。不过为了安全起见,还是用Signature的大小加上FileHeader的大小再加上FileHeader中的SizeOfOptionalHeade来确定比较保险。

1.4 得到输出函数在文件中的位置。
    第1.2步中我们确定了文件中节的数目,第1.3步中我们确定了节表在文件中的位置。
    现在来确定输出函数在文件中的位置。
    取得PE Header中的Optional Header中的DataDirectory数组中的第一项,
也就是输出函数项。DataDirectory[]数组的每项都是IMAGE_DATA_DIRECTORY结构,该结构定义如下。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
取得DataDirectory数组中的第一项中的成员VirtualAddress的值。这个值就是在内存中资源节的RVA。
如果这个RVA的值为0表示这个PE文件中没有输出函数。
然后根据节的数目,遍历节表数组。也就是从0到(节表数-1)的每一个节表项。
每个节在内存中的RVA的范围是从该节表项的成员VirtualAddress字段的值开始(包括这个值),
到VirtualAddress+Misc.VirtualSize的值结束(不包括这个值)。
我们遍历整个节表,看我们取得的输出函数的RVA,在哪个节表项的RVA范围之内。
如果在范围之内,就找到了输出函数所在节的节表项。
这 个节表项中的 PointerToRawData 中的值,就是输出函数所在节在文件中的位置。这个节表项中的VirtualAddress 中的值,就是输出函数所在节在内存中的RVA。用输出函数的RVA减去输出函数所在节的RVA,就可以得到输出函数在该节内偏移。用这个偏移加上该节的在 文件中的位置,就可以得到输出函数在文件中的位置。即DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]. VirtualAddress – SectionTable[i].VirtualAddress + SectionTable[i].PointerToRawData 。

这样我们就得到了输出函数在文件中开始的位置。

二 PE文件中的输出函数。

    输出函数是用来给其他程序使用的。其他程序如果知道了某个输出函数的入口地址(就是实现这个函数功能的代码开始的地方),就可以转到那里去执行。一个PE 文件中,如果有有输出函数,一般都不是一个。所以有一个数组来保存每个输出函数的入口地址。在PE文件中,提供两种方法,来找到某个输出函数的入口地址。 第一种方法是通过入口地址数组序号,就是说知道是入口地址数组中的第几个元素,这样就可以得到里面的入口地址。第二种方法是通过函数名,通过比较函数名, 然后得到对应该函数名的入口地址数组的序号,从而得到该函数名的对应函数的入口地址。为了能够通过函数名得到序号,就需要一些相关的结构。具体内容后面 讲。总得来说PE文件的输出函数部分中就是这些东西。

    前面我们已经得到了输出函数部分在文件中开始的位置,在输出函数部分的最开始,是一个IMAGE_EXPORT_DIRECTORY 结构,这个结构提供很多重要的信息。这个结构的后面紧跟着的是 输出函数入口地址数组 。输出函数入口地址数组之后紧跟着的是输出函数名的指针数组。输出函数名的指针数组之后紧跟着的是输出函数名对应的序号的数组。输出函数名对应的序号的数 组之后紧跟着dll的名字和输出函数的名字。注意,他们之间是紧挨着的。并且顺序为IMAGE_EXPORT_DIRECTORY,输出函数入口地址的数 组,输出函数名的指针的数组,输出函数名对应的序号的数组。最后是dll的名字的字符串和那些输出函数名的字符串。

    先看IMAGE_EXPORT_DIRECTORY 结构,在WINNT.H中定义如下。

typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

这个结构长度为40个字节,共有11个字段。
各字段含义如下:

Characteristics:一个保留字段,目前为止值为0。
TimeDateStamp:产生的时间。
MajorVersion:
MinorVersion:
Name:一个RVA,指向一个dll的名称的ascii字符串。
Base:输出函数的起始序号。一般为1。
NumberOfFunctions:输出函数入口地址的数组 中的元素个数。
NumberOfNames:输出函数名的指针的数组 中的元素个数,也是输出函数名对应的序号的数组 中的元素个数。
AddressOfFunctions:一个RVA,指向输出函数入口地址的数组。
AddressOfNames:一个RVA,指向输出函数名的指针的数组。
AddressOfNameOrdinals:一个RVA,指向输出函数名对应的序号的数组。

输出函数入口地址的数组,这个数组是一个DWORD数组,每个元素都是一个RVA,指向一个输出函数的入口地址,每个元素长4个字节。

输出函数名的指针的数组,这个数组是一个DWORD数组,每个元素都是一个RVA,指向一个输出函数名的ascii字符串,每个元素长4个字节。

输出函数名对应的序号的数组,这个数组是一个WORD数组,每个元素都是某个输出函数名函数对应的索引,这个索引是输出函数入口地址的数组的索引(已经用序号减去起始序号了),这个每个元素长2个字节。

dll名字符串和输出函数名字符串,都是ascii字符串,以空结束。一个紧挨着一个。dll名字符串的地址存在IMAGE_EXPORT_DIRECTORY 的 Name 中。输出函数名字符串 的地址存在 输出函数名的指针的数组 中。

还有要注意的是:

   输出函数入口地址的数组包含着输出函数的入口点地址,一个序号减去起始序号(起始序号就是 IMAGE_EXPORT_DIRECTORY 中的 Base),用来索引这个数组。比如,起始序号为1,要找序号为1的函数的入口地址,那么该函数的入口地址为 输出函数入口地址数组[0](0是由1-1算出来的)序号为3的函数的入口地址为   输出函数入口地址数组[2](2是由3-1算出来的)。

当载入器要修正一个函数的调用,而这个函数是用序号输入的,载入器只要用序号减去起始序号,得到输出函数入口地址的数组的索引,就可以了。
当 载入器要修正一个函数的调用,而这个函数是用函数名输入的,载入器比较输出函数名的指针的数组每个元素所指的函数名,比如在第3个元素中比较,发现相同。 载入器就会从 输出函数名对应的序号的数组 的第三个元素的值 得到该函数的序号。用这个序号就可以在象前面那样用序号得到入口地址。输出函数名的指针的数组 和 输出函数名对应的序号的数组 有相同的元素个数(IMAGE_EXPORT_DIRECTORY 中的 NumberOfNames)。并且是有所关联的,函数名指针数组的第i个元素的序号,在序号数组的第i个元素中。输出函数名的指针的数组和输出函数名对 应的序号的数组,分开成两个数组,而不是合并成一个结构体的数组(这个结构体第一个成员是指针,第二个成员是序号),是因为,那样的话数组的一个元素长6 个字节,不利于对齐。

下面我们来通过一个例子,来看上面所介绍的内容。

我们的例子是Win2k中的dll文件routetab.dll。为了防止大家版本不同,本文附带了这个PE文件。

用开始讲到的寻找输出部分在文件中位置的方法,我们找到了输出部分在文件中的位置为00001460h。

由于第一个结构IMAGE_EXPORT_DIRECTORY比较长,一行方不下,所以放了三行,结构的不同成员用 / 分开。
其他每行是一个结构。可以用16进制编辑器打开附带的 routetab.dll 对照着看。

我们来算一下 Name,AddressOfFunctions,AddressOfNames,AddressOfNameOrdinals 在文件中的位置。
输出部分的开始rva(由DataDirectory[1]得到)为1e60h。输出部分在文件中的位置为1460h。
Name 为rva(值从结构中可以看到是00001eec,如果你不明白为什么是00001eec而不是ec1e0000的话,请看 《JIURL PE 格式学习总结(一)》中关于 big-endian和little-endian的介绍),则Name相对于输出部分开始处的偏移为1eec-1e60。而Name在文件中的位置为 Name在相对于输出部分开始的偏移加上输出部分开始处在文件中的位置。所以Name在文件中的位置为1EEC-1E60+1460=14ECh。同样方 法我们可以算出, AddressOfFunctions:
1e88-1e60+1460=1488。AddressOfNames:1eb0- 1e60+1460=14b0 。AddressOfNameOrdinals:1ed8-1e60+1460=14d8。 从结构中还可以看到有0000000a(十进制10)个输出函数。

00001460: {00 00 00 00 / dc 5b ec 37 / 00 00 / 00 00 / ec 1e 00 00 /
00001470: 01 00 00 00 / 0a 00 00 00 / 0a 00 00 00 / 88 1e 00 00 /
00001480: b0 1e 00 00 / d8 1e 00 00 }
(我们用大括号括起来了,IMAGE_EXPORT_DIRECTORY结构,长度为40个字节)

00001488: 41 1a 00 00 (函数入口点的RVA,长4个字节)
0000148C: 64 1a 00 00
00001490: 02 18 00 00
00001494: 02 18 00 00
00001498: 71 16 00 00
0000149C: 07 16 00 00
000014A0: 26 18 00 00
000014A4: 84 1a 00 00
000014A8: 06 17 00 00
000014AC: 5b 19 00 00

000014B0: f9 1e 00 00 (函数名的指针,长4个字节,指向 1ef9-1e60+1460=14f9)
000014B4: 02 1f 00 00
000014B8: 0e 1f 00 00
000014BC: 21 1f 00 00
000014C0: 30 1f 00 00
000014C4: 42 1f 00 00
000014C8: 4d 1f 00 00
000014CC: 5b 1f 00 00
000014D0: 6c 1f 00 00
000014D4: 81 1f 00 00

000014D8: 00 00 (索引,说明的1个函数名的函数,入口地址在 地址数组[0])
(并不是每个PE文件序号数组的第0个元素值就是0,第1个元素值就是1,ntdll.dll中就不是)
000014DA: 01 00
000014DC: 02 00
000014DE: 03 00
000014E0: 04 00
000014E2: 05 00
000014E4: 06 00
000014E6: 07 00
000014E8: 08 00
000014EA: 09 00

000014EC: 52 4f 55 54 45 54 41 42 2e 64 6c 6c 00                       ROUTETAB.dll.
000014F9: 41 64 64 52 6f 75 74 65 00                                   AddRoute.
00001502: 44 65 6c 65 74 65 52 6f 75 74 65 00                          DeleteRoute.
0000150E: 46 72 65 65 49 50 41 64 64 72 65 73 73 54 61 62 6c 65 00     FreeIPAddressTable.

00001521: 46 72 65 65 52 6f 75 74 65 54 61 62 6c 65 00                 FreeRouteTable.
00001530: 47 65 74 49 50 41 64 64 72 65 73 73 54 61 62 6c 65 00        GetIPAddressTable.
00001542: 47 65 74 49 66 45 6e 74 72 79 00                             GetIfEntry.
0000154D: 47 65 74 52 6f 75 74 65 54 61 62 6c 65 00                    GetRouteTable.
0000155B: 52 65 66 72 65 73 68 41 64 64 72 65 73 73 65 73 00           RefreshAddresses.
0000156C: 52 65 6c 6f 61 64 49 50 41 64 64 72 65 73 73 54 61 62 6c 65 00                                                                      ReloadIPAddressTable.
00001581: 53 65 74 41 64 64 72 43 68 61 6e 67 65 4e 6f 74 69 66 79 45 76 65 6e 74 00
                                                                    SetAddrChangeNotifyEvent.
0000159A: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
000015AA: …
000015F0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

还是比较清楚的,就不再讲了。

三 遍历PE文件中的输出

    根据前面的方法得到输出部分的开始地址,最开始是一个IMAGE_EXPORT_DIRECTORY,根据这个结构中的内容,可以得到,和输出相关的三个数组的开始地址,和元素个数。用for循环可以很简单的遍历。

    实现遍历输出的源程序,可以参考 PEDUMP – Matt Pietrek 1995 。《Windows95系统程式设计大奥秘》附书源码中有。

本文所使用的PE文件routetab.dll

Read: 672

PE 格式学习总结(一)– PE文件概述

零 前言

    PE格式,是Windows的可执行文件的格式。Windows中的 exe文件,dll文件,都是PE格式。PE 就是Portable Executable 的缩写。Portable 是指对于不同的Windows版本和不同的CPU类型上PE文件的格式是一样的,当然CPU不一样了,CPU指令的二进制编码是不一样的。只是文件中各种 东西的布局是一样的。

图 1.1

    图1.1是 JIURL PEDUMP 打开 Win2K 中的 explorer.exe 的截图。JIURL PEDUMP 是我写的一个小工具,从文件开始的 Dos Header 一直到 Section Table,打开PE文件之后,点击相应结构,就会高亮显示文件中相应的部分。不过没有Sections。对了解 PE 格式有所帮助,可以很好的配合后面的介绍。可以到我的主页 http://jiurl.yeah.net 上下载。

一   PE文件格式概述

PE文件结构的总体层次分布如下所示

————–
|DOS MZ Header |
|————–|
|DOS Stub      |
|————–|
|PE Header     |
|————–|
|Section Table |
|————–|
|Section 1     |
|————–|
|Section 2     |
|————–|
|Section …   |
|————–|
|Section n     |
————–

 

1.1 DOS Header

    PE文件最开始是一个简单的 DOS MZ header,它是一个 IMAGE_DOS_HEADER 结构。有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随 MZ Header 之后的 DOS Stub。

1.2   DOS Stub   

    DOS Stub 是一个有效的 DOS 程序。当程序在DOS下运时,输出象 "This program cannot be run in DOS mode" 这样的提示。在 图1.1中就可以看到字符串 "This program cannot be run in DOS mode"。这是编译器生成的默认stub程序。你也可以通过链接选项 /STUB:filename 指定任何有效的MS-DOS可执行文件来替换它。

1.3 PE Header

    紧接着 DOS Stub 的是 PE Header。它是一个 IMAGE_NT_HEADERS 结构。其中包含了很多PE文件被载入内存时需要用到的重要域。执行体在支持PE文件结构的操作系统中执行时,PE装载器将从 DOS MZ header 中找到 PE header 的起始偏移量。因而 ?DOS stub 直接定位到真正的文件头 PE header。

1.4 Section Table

    PE Header 接下来的数组结构 Section Table (节表)。如果PE文件里有5个节,那么此 Section Table 结构数组内就有5个成员,每个成员包含对应节的属性、文件偏移量、虚拟偏移量等。图1中的节表有4个成员。

1.5 Sections

    PE文件的真正内容划分成块,称之为sections(节)。Sections 是以其起始位址来排列,而不是以其字母次序来排列。通过节表提供的信息,我们可以找到这些节。图1.1所示的 explorer.exe 中有4个节。程序的代码,资源等等就放在这些节中。

二 PE文件格式中的结构及其作用

这部分内容请参考下面的几篇文章,使用工具 JIURL PEDUMP 有助于快速了解。
大家不要因此,而失望不看,本文重点在后三篇,本篇只是为了有个交代,和介绍些相关内容。
注意,在WINNT.H中,有所有PE相关结构的定义。我们用到的结构定义都来自那里。

Microsoft Portable Executable and Common Object File Format Specification
MSDN

《Windows95系统程式设计大奥秘》
第8章 PE 与COFF OBJ 档案格式
Matt Pietrek 着 侯杰译

Iczelion的PE教程

PE学习笔记(一) rivershan
PE学习笔记(二) rivershan

Inside Windows
An In-Depth Look into the Win32 Portable Executable File Format

Matt Pietrek
已经被人翻译了。

Inside Windows
An In-Depth Look into the Win32 Portable Executable File Format
Matt Pietrek

三 几个要注意的问题

3.1 文件中大量的空白

    在 PE Header结构 中的 OptionalHeader 结构中的成员 FileAlignment 的值是文件中节的对齐粒度,单位是字节,这个值应该是2的n次方,范围从512到64k。如果这里的值是512,那么PE文件中的节的长度都是512字节 的整数倍,内容不够的部分用0填充。比如一个PE文件的 FileAlignment 为200h(十进制512),它的第一个节在400h处,长度为100h,那么从文件400h到500h中为这一节的内容,而文件对齐粒度是200h,所 以为了使这一节长度为FileAlignment的整数倍,500h到600h会被用零填充。而下一个节的开始地址为600h。用16进制编辑器打开PE 文件,就可以看到这种情况,PE文件头的内容结束到第一个节开始之间的地方,每一个节中内容结束到下一节开始的地方都会有大量的空白。VC6编译链接时默 认的FileAlignment为1000h(4k),可以使用链接选项 /ALIGN:number 来改变这个值。比如把4k改成512时,可以明显减小生成文件的大小。

3.2 big-endian和little-endian

    PE Header中的 FileHeader 的成员 Machine 中的值,根据WINNT.H中的定义,对于 Intel CPU 应该为 0x014c。但是你用16进制编辑器打开PE文件,看到这个WORD显示的却是 4c 01 。你看到的并没有错,你看到的 4c 01 就是 0x014c,只不过由于 intel cpu 是ittle-endian,所以显示出来是这样的。对于 big-endian 和 little-endian,请看下面的例子。

比如一个整形int变量。长为四个字节。
这个变量的地址比如为n。
则这个变量的4个字节地址分别为n,n+1,n+2,n+3。

当 这个整形变量 的值为 0x12345678 时,

对于 big-endian 来说
地址n+0的那个字节中的值为 0x12
地址n+1的那个字节中的值为 0x34
地址n+2的那个字节中的值为 0x56
地址n+3的那个字节中的值为 0x78

按如下方式就会显示为
n n+1 n+2 n+3
12 34 56 78

对于 ittle-endian 来说
地址n+0的那个字节中的值为 0x78
地址n+1的那个字节中的值为 0x56
地址n+2的那个字节中的值为 0x34
地址n+3的那个字节中的值为 0x12

按如下方式就会显示为
n n+1 n+2 n+3
78 56 34 12

Intel使用的是 ittle-endian 。

一个整形 int 变量 i,的地址是&i,那么这个i的四个字节是&i,&i+1,&i+2,&i+3。
可以用这样一个程序看到。

#include <stdio.h>
#include <conio.h>

void main()
{
int i;
char* p;
p=(char*)&i;

printf("i: ");
scanf("%x",&i);
printf("n");

printf("&i+0: %xn",*p);
printf("&i+1: %xn",*(p+1));
printf("&i+2: %xn",*(p+2));
printf("&i+3: %xn",*(p+3));

printf("n");
printf("&i-4: %xn",*(p-4));
printf("&i-3: %xn",*(p-3));
printf("&i-2: %xn",*(p-2));
printf("&i-1: %xn",*(p-1));

printf("n");
printf("&i+4: %xn",*(p+4));
printf("&i+5: %xn",*(p+5));
printf("&i+6: %xn",*(p+6));
printf("&i+7: %xn",*(p+7));

getch();

}

当我们输入 12345678 的时候可以看到,输出

i: 12345678

&i+0: 78
&i+1: 56
&i+2: 34
&i+3: 12

&i-4: 7c
&i-3: ffffffff
&i-2: 12
&i-1: 0

&i+4: ffffffc0
&i+5: ffffffff
&i+6: 12
&i+7: 0
正是&i,&i+1,&i+2,&i+3这四个字节中储存了i的值。

对于int,WORD,DWORD等等都要注意 big-endian 和 little-endian 。

3.3 RVA (Relative Virtual Address) 相对虚拟地址

    RVA是一个简单的相对于PE载入点的内存偏移。比如,PE载入点为0X400000,那么代码节中的地址0X401000的RVA为(target address) 0x401000 – (load address)0x400000 = (RVA)0x1000.换句话说 RVA是0x1000,载入点为0X400000,那么该RVA的在内存中的实际地址就是0X401000。注意一下RVA是指内存中,不是指文件中。是 指相对于载入点的偏移而不是一个内存地址,只有RVA加上载入点的地址,才是一个实际的内存地址。

3.4 三种不同的地址

    PE的各种结构中,涉及到很多地址,偏移。有些是指在文件中的偏移,有的是指在内存中的偏移。一定要搞清楚,这个地址或者是偏移,是指在文件中,还是指在 内存中。第一种,文件中的地址。比如用16进制编辑器打开PE文件,看到的地址(偏移)就是文件中的地址,我们使用某个结构的文件地址,就可以在文件中找 到该结构。第二种,文件被整个映射到内存时,比如某些PE分析软件, 把整个PE文件映射到内存中,这时是内存中的地址,如果知道某一个结构在文件中的地址的话,那么这个PE文件被映射到内存之后该结构的在内存中的地址,可 以用文件中的地址加上映射内存的地址,就可以得到在该结构内存中的地址。第三种,执行PE时,PE文件会被载入器载入内存,这时经常需要的是RVA。比如 知道一个结构的RVA,那么载入点加上RVA就可以得到内存中该结构的实际地址。比如,某个程序,我们用16进制编辑器打开它,看到PE Header开始在16进制编辑器显示为000000C8的地方。于是我们在16进制编辑器显示为000000FC的地方找到了 OptionalHeader的ImageBase,值为400000h,那么当这个程序被执行时,如果内存中400000h处没有使用,该程序就会被载 入到那里。而我用CreateFileMapping将这个PE文件映射到内存中时,可以得到块内存的地址为5505024。对于映射入内存的这个PE文 件,我们就可以在内存中000000FCh+05505024h=5505120处找到这个PE的OptionalHeader的ImageBase。

3.5 几个重要结构的说明

PE Header 的 FileHeader 的 NumberOfSections:这是一个很重要的字段,用来确定文件中节的数目。

PE Header 的 OptionalHeader 的 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]:一个IMAGE_DATA_DIRECTORY 结构数组。到目前为止这个数组的长度是固定的,有16个元素,这16个元素分别代表
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
每个元素是一个IMAGE_DATA_DIRECTORY结构,IMAGE_DATA_DIRECTORY定义如下。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
第一个字段是一个RVA,第二个字段是一个大小。

Section Table 节表紧跟在OptionalHeader之后,是一个IMAGE_SECTION_HEADER结构的数组。该数组中成员的个数由 File Header (IMAGE_FILE_HEADER) 结构中 NumberOfSections 域的域值来定。节表中的成员是IMAGE_SECTION_HEADER 结构,IMAGE_SECTION_HEADER 结构的长度固定,长40个字节。整个Section Table 的长度不固定,等于 NumberOfSections*sizeof(IMAGE_SECTION_HEADER)。IMAGE_SECTION_HEADER 结构中,

VirtualAddress:本节的RVA(相对虚拟地址)。
PointerToRawData:这是本节基于文件的偏移量。

3.6 DOS MZ Header 中的 MZ

    MZ是MZ格式的主要作者 Mark Zbikowski 的名字的缩写。

Read: 920

Delphi数据库编程技巧

  我原来在万千的新闻组(news://news.webking.com.cn/)Delphi版上闲逛那段时间,发现经常有人提出一些数据库方面的技巧性问题,问题其实不难,但是要一个简单的解决方案,可能也破费思量,特别在此简单谈谈:

1. 如何动态设置ODBC源

很多时候程序员需要自动生成ODBC数据源,而不是指导客户"打开控制面板… … ",到底如何去做呢?相信很多人会选择编程修改注册表来实现这项功能,因为ODBC的详细信息全部存放在注册表的下述键值内:

"HKEY_LOCAL_MACHINESOFTWAREODBC"

修改一下ODBC的配置,通过前后的注册表比较,你可以发现一定的规律。在这里我只是想说,哥们,别这么犯傻了(如果我让你写一个通用的ODBC源处理程序,你得累死),用这个Windows ODBC API函数吧,

function SQLConfigDataSource(hwndParent: Integer; fRequest: Integer;

lpszDriverString: String; lpszAttributes: String): Integer;

stdcall;external ”ODBCCP32.DLL”;

了解数据库编程的朋友都知道,数据库的访问方式不论DAO、ADO、ODBC或是BDE或是其它第三方的数据库连接控件,归根结底,都是一些个函数集,只要你愿意,你可以编写出自己的数据库访问方式用以替代。深入研究这些底层函数,很多时候会为你提供相当地便利。

SQLConfigDataSource这个函数MSDN有详细的说明,我不想整段翻译下来让你扁我,我只是结合流行的SQL Server谈谈如何有技巧的调用该函数。其它的数据库大同小异。

SQLConfigDataSource(0, ODBC_ADD_SYS_DSN,”SQL Server”,
”DSN=Record_ODBC”+ chr(0) +
”Server=(local)”+ chr(0) +
”Database=master”+ chr(0) +
”Description=DragonPC SQLServer ODBC Source”+ chr(0));

这是我的Delphi程序中调用该函数的一个实例,第一个参数是父窗口句柄,设置为0则该函数不显示任何对话框。第二个参数是操作类型,你需要定义如下的操作类型常量:

Const
ODBC_ADD_DSN = 1; // Add a new user data source.
ODBC_CONFIG_DSN = 2; // Configure (modify) an existing user data source.
ODBC_REMOVE_DSN = 3; // Remove an existing user data source.
ODBC_ADD_SYS_DSN = 4; // Add a new system data source.
ODBC_CONFIG_SYS_DSN = 5; // Modify an existing system data source.
ODBC_REMOVE_SYS_DSN = 6; // Remove an existing system data source.

从名字我们知道,要添加ODBC源,我们需要调用的是ODBC_ADD_SYS_DSN 或是ODBC_ADD_DSN参数。第三个参数也没有什么好说的,我们添加的是SQL Server数据库的ODBC源,所以填入”SQL Server”参数,如果需要建立Excel文件的ODBC数据源,我们可以填入”Excel Files (*.xls)”,这些字符串参数相信各位同志在添加ODBC源时,已经多次见过。

关键的是第三个参数的设置,不同的数据库类型所支持的关键字是不一样的,这里仅仅就SQL Server所支持的关键字作一个简单说明:

DSN:你的ODBC数据源名称。

Server:你的数据库服务器名称,使用(local)指的是本地计算机安装的数据库。注:最新的SQL Server 2000支持一台计算机运行多个SQL Server服务,这个时候你需要指定SqlSever的InstanceName。

Address:指定SQL Server服务器的网络IP地址。

Database:指定默认数据库名称。

Language:指定默认语言。

Description:备注信息。

详细的参数和信息可以查阅微软网站的以下网址。

http://msdn.microsoft.com/library/psdk/dasdk/odch3kit.htm

http://msdn.microsoft.com/library/psdk/sql/od_odbc_c_99yd.htm

  2. 如何动态设置BDE别名

这个问题其实是考察程序员对BDE的 TSession组件的熟悉程度,一个数据库程序的建立,即使你没有显式的添加TSession组件,系统中依然存在一个名字为Session的 TSession对象,你可以在任何位置调用该对象的方法和属性。TSession类的很多方法可以帮助我们的应用程序获取系统BDE环境,下面介绍一个 代码片断用以添加一个BDE别名:

var
BDEList : TStringList ;
...
begin
...
BDEList := TStringList.Create () ;
try
Session.GetAliasNames(BDElist) ; // 获取系统所有BDE别名列表
if BDEList.IndexOf(”DragonPC”)= -1 then begin // 如果没有我们的BDE别名"DragonPC"
BDEList.Clear ;
BDEList.Add(”SERVER NAME=” + ”SQLServerName”)); // 数据库服务器名称
BDEList.Add(”DATABASE NAME=master”) ; // 默认数据库
BDEList.Add(”USER NAME=sa”); // 用户名
Session.AddAlias(”DragonPC”, ”MSSQL”, BDEList) ; // 添加一个MSSQL类型的BDE别名
ShowMessage(”系统即将建立BDE别名!”) ;
Session.SaveConfigFile() ; // 存储BDE配置
end;
finally
BDEList.Free ;
end;

这么简单,用户就可以随时建立、删除和修改BDE别名(有兴趣的朋友可以查看TSession组件的源代码,看看调用了哪些BDE函数)。另外像 DeleteAlias,ModifyAlias,GetDatabaseNames,GetDriverNames, GetStoredProcNames,GetTableNames,GetPassword等等TSession类的方法,使用起来非常简单,通过 Delphi的随机帮助,读者可以试着自己调用一下看看。通过对Session的灵活应用,再配合我下面将要提到的扑捉SQL异常的技巧,你完全可以写一 个媲美SQL Explorer的通用数据库查询工具。

  3. 如何扑捉运行SQL语句时的错误

老是有朋友在开发一些开放的数据库接口(比如Delphi的SQL Explorer工具)时发愁,既然是开放的,当然需要允许用户使用SQL语句访问数据库,这些还好办,一旦用户的运行SQL语句出现错误,程序员如何扑捉该异常呢?很简单,看看下面的函数:

Const
ExecSQLMode = 0 ;
OpenSQLMode = 1 ;
ResultRight = ”SQL query result is right” ;
...
function RunSql(RunQuery: TQuery; Sqls: TStringList; var ErrorMsg: string;
Mode: integer) : integer ;
begin
ErrorMsg := ResultRight ;
Result := 0 ;
try
RunQuery.DatabaseName := ”RecordDB” ;
RunQuery.SQL.Clear() ;
RunQuery.SQL.AddStrings(Sqls) ;
if Mode = ExecSQLMode then
RunQuery.ExecSQL()
else
RunQuery.Open() ;
except
on e:exception do
ErrorMsg := e.Message + #13 + #10 +”— 错误是俺发现的 —” ;
end;
end;

朋友看明白了吧,我的函数很简单,将SQL语句代码段作为参数传递给TQuery组件,通过设置查询方式(ExecSQLMode、 OpenSQLMode)来处理有结果集返回的数据查询语句(select)或是没有结果集返回的数据操作语言(update、delete、 insert、create等)。而异常的扑捉呢,我们扑捉所有异常的老祖宗Exception,因为Delphi的所有异常都是从Exception继 承下来的,这样一个简单的SQL语句运行和异常处理函数就完成了,一旦返回的ErrorMsg的值不是ResultRight,ErrorMsg就会返回 异常的信息,程序员就可以加以判断以处理不同的情况。

Read: 1089

[转]visual c++对大型数据文件的读取

  笔者前不久曾遇到一个问题,解决之后的经验愿与大家分享。问题是这样的,有一批数据文件,数据格式如下:

日期,开盘,最高,最低,收盘,成交量,成交金额

1996年5月13日,636.96,636.96,636.96,636.96,0,0,

1996年5月14日,641.61,641.61,641.61,641.61,0,0,

1996年5月15日,637.83,637.83,637.83,637.83,0,0,

………….

要求将数据填写到四张表中,以便作相应的分析。笔者开始用CFile和CStdioFile类的方法读取件。Cfile类提供了基于二进制流的文件操 作,功能类似于C语言中的fread()和fwrite()函数。CStdioFile提供了基于字符串流的文件操作,功能类似于C语言中fgets() 和fputs()函数。但是笔者发现,使用这两个类进行文件操作时,对于一次文件读写的数据量的大小必须限制在65535字节以内。究其原因是在VC中访 问大于65535字节的缓冲区需要Huge型指针,而在CFile和CStdioFile类中,使用的是Far型的指针。由于Far型指针不具有跨段寻址 的能力,因此限制了一次文件读写的长度小于65535字节。如果传递给CFile和CStdioFile两个类的成员函数的数据缓冲区的大小大于 65535字节的时候,VC就会产生ASSERT错误。

针对文件格式特点,笔者改用CArchive类进行读取如下:

CFile SourceFile;//数据文件

CString SourceData;//定义一临时变量保存一条记录

SourceFile.Open(…….);

CArchive ar(&SourceFile,CArchive::load);

while(NULL!=ar.ReadString(SourceData))//循环读取文件,直到文件结束

{

if(SourceData=="日期,开盘,最高,最低,收盘,成交量,成交金额"||SourceData=="")

continue;//跳过文件头部的提示信息

//分析并填充//

}

在进行分析时,笔者采取了逐步分析并修改的办法,过程如下:

int nYear;

CString Year= SourceData.Left(SourceData.Find("年"));//截取年前面的字符串

nYear=atoi(Year);//类型转换

SourceData=SourceData.Righ(SourceData.GetLength()-SourceData.Find("年")-2);//将年以及前面的字符删除。

重复上面分析过程,直到记录末尾。

通过上述方法,笔者成功地将文件读取并分析填充。

Read: 658