PE
来源:互联网 发布:七牛域名和自定义域名 编辑:程序博客网 时间:2024/04/27 08:38
基地址: PE映象到内存的起始地址 = 0x10000;
相对虚拟地址: (RVA)对于基地址的偏移量;
RVA + 基地址 = 线性地址;
文件偏移量: 文件中地址用偏移量表示,第一个字节偏移量0,后面依次递增.;
SoftICE/W32Dasm 显示的地址是内存地址,或称之为虚拟地址(VA);
Hiew/Hex/Workshop显示的地址是文件地址,称之为偏移量(File offset)或物理地址(文件);
虚拟地址 = 逻辑地址 => 线性地址 (内存地址);
////////////////////////////////////////////////////////////////////////////////////
ex:;
(内存 各节按4096字节对齐);
已知执行开始处 RVA = 0x1560;
".code"节 RVA = 0x1000;
0x1560位于".code"节 偏移量0x560处 (0x1560 - 0x1000 = 0x560);
(文件 各节按512字节边界对齐);
".code"节 偏移量 = 0x800;
文件中".code"节开始 = 0x800 + 0x560 = 0xd60;
////////////////////////////////////////////////////////////////////////////////////
DLL 同样是PE格式
导出表: 里面包含函数的名称、序号和入口地址,提供给执行文件调用的。(简单理解是是dll才有使用)
Windows装载器将文件装入内存并将导入表中登记的DLL文件一并装入,
再根据DLL文件中的函数导出信息对被执行文件的IAT表进行修正
PE执行文件 (简单理解是执行文件使用,dll也包含导入表)
导入表: 包含需要用到的dll文件名和函数名,运行时根基信息定位找到dll函数
lib 导入库,有此文件就可以让程序自动载入DLL
包含dll文件名和函数名
lib会将内容装入导入表
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ************************ WINNT.H ************************
#define IMAGE_DOS_SIGNATURE 0x5A4D // MZ (DOS头)
#define IMAGE_OS2_SIGNATURE 0x454E // NE (OS头)
#define IMAGE_OS2_SIGNATURE_LE 0x454C // LE (OS头)
#define IMAGE_NT_SIGNATURE 0x00004550 // PE00 (NT头)
// MS-DOS头(64(0x40)个字节) ---------------------------------
typedef struct _IMAGE_DOS_HEADER { // DOS下的.EXE文件头
USHORT e_magic; // 魔数
USHORT e_cblp; // 文件最后一页的字节数
USHORT e_cp; // 文件的页数
USHORT e_crlc; // 重定位
USHORT e_cparhdr; // 段中头的大小
USHORT e_minalloc; // 需要的最少额外段
USHORT e_maxalloc; // 需要的最多额外段
USHORT e_ss; // 初始的(相对的)SS寄存器值
USHORT e_sp; // 初始的SP寄存器值
USHORT e_csum; // 校验和
USHORT e_ip; // 初始的IP寄存器值
USHORT e_cs; // 初始的(相对的)CS寄存器值
USHORT e_lfarlc; // 重定位表在文件中的地址
USHORT e_ovno; // 交叠数
USHORT e_res[4]; // 保留字
USHORT e_oemid; // OEM识别符(用于e_oeminfo成员)
USHORT e_oeminfo; // OEM信息; e_oemid中指定的
USHORT e_res2[10]; // 保留字
LONG e_lfanew; // PE文件签名(偏移量),4字节
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
// PE文件头结构 -----------------------------------------------
typedef struct _IMAGE_FILE_HEADER {
USHORT Machine; //机器
USHORT NumberOfSections; //节数
ULONG TimeDateStamp; //时间日期戳
ULONG PointerToSymbolTable; //符号表指针
ULONG NumberOfSymbols; //符号数
USHORT SizeOfOptionalHeader; //可选头的大小
USHORT Characteristics; //特性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
#define IMAGE_SIZEOF_FILE_HEADER 20 //PE文件头结构大小
// PE可选头 -------------------------------------------------
typedef struct _IMAGE_OPTIONAL_HEADER {
//标准域
USHORT Magic; //魔数
UCHAR MajorLinkerVersion; //链接器主版本号
UCHAR MinorLinkerVersion; //链接器小版本号
ULONG SizeOfCode; //代码大小
ULONG SizeOfInitializedData; //已初始化数据大小
ULONG SizeOfUninitializedData; //未初始化数据大小
ULONG AddressOfEntryPoint; //入口点地址
ULONG BaseOfCode; //代码基址 已载入的映像文件中代码(“.text”节)的相对偏移量
ULONG BaseOfData; //数据基址 已载入的映像文件中未初始化数据(“.bss”节)的相对偏移量
//NT增加的域
ULONG ImageBase; //映像文件基址, Win32 SDK默认地址0x00400000, 但可改变
ULONG SectionAlignment; //节对齐
ULONG FileAlignment; //文件对齐
USHORT MajorOperatingSystemVersion;//操作系统主版本号
USHORT MinorOperatingSystemVersion;//操作系统小版本号
USHORT MajorImageVersion; //映像文件主版本号
USHORT MinorImageVersion; //映像文件小版本号
USHORT MajorSubsystemVersion; //子系统主版本号
USHORT MinorSubsystemVersion; //子系统小版本号
ULONG Reserved1; //保留项1
ULONG SizeOfImage; //映像文件大小
ULONG SizeOfHeaders; //所有头的大小
ULONG CheckSum; //校验和
USHORT Subsystem; //子系统
USHORT DllCharacteristics; //DLL特性
ULONG SizeOfStackReserve; //保留栈的大小
ULONG SizeOfStackCommit; //指定栈的大小
ULONG SizeOfHeapReserve; //保留堆的大小
ULONG SizeOfHeapCommit; //指定堆的大小
ULONG LoaderFlags; //加载器标志
ULONG NumberOfRvaAndSizes; //RVA的数量和大小
IMAGE_DATA_DIRECTORY DataDirectory [IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //数据目录数组
} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;
// PE的可选头->DataDirectory->16个数据目录
typedef struct _IMAGE_DATA_DIRECTORY {
ULONG VirtualAddress; //偏移地址
ULONG Size; //大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
// 各个目录项 只使用11个 每一个宏都是一个_IMAGE_DATA_DIRECTORY结构
// 输出目录
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0
// 输入目录
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1
// 资源目录
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2
// 异常目录
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3
// 安全目录
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4
// 基址重定位表
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5
// 调试目录
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6
// 描述字符串
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7
// 机器值(MIPS GP)
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8
// TLS(线程本地存储)
#define IMAGE_DIRECTORY_ENTRY_TLS 9
// 载入配置目录
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10
// 节头(40字节/每个) ------------------------------------------------------
// 可以有N个节头, 每个结构都相同
typedef struct _IMAGE_SECTION_HEADER {
UCHAR Name[IMAGE_SIZEOF_SHORT_NAME]; //名字数组
union { //共用体标志
ULONG PhysicalAddress; //物理地址
ULONG VirtualSize; //虚拟大小
} Misc;
ULONG VirtualAddress; //虚拟地址
ULONG SizeOfRawData; //原始数据的大小
ULONG PointerToRawData; //原始数据指针
ULONG PointerToRelocations; //重定位指针
ULONG PointerToLinenumbers; //行数指针
USHORT NumberOfRelocations; //重定位数目
USHORT NumberOfLinenumbers; //行数数目
ULONG Characteristics; //特征
/*
Characteristics
值 定义
0x00000020 代码节
0x00000040 已初始化数据节
0x00000080 未初始化数据节
0x04000000 不可缓存节
0x08000000 不可分页节
0x10000000 共享节
0x20000000 可执行节
0x40000000 可读节
0x80000000 可写节
*/
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
#define IMAGE_SIZEOF_SHORT_NAME 8 //节头大小
// 输出数据节(导出表) .edata --------------------------------------------
typedef struct _IMAGE_EXPORT_DIRECTORY {
ULONG Characteristics; //特征
ULONG TimeDateStamp; //时间日期戳
USHORT MajorVersion; //主版本号
USHORT MinorVersion; //小版本号
ULONG Name; //名字 (可执行模块名字)
ULONG Base; //基址
ULONG NumberOfFunctions; //函数数 (多少函数)
ULONG NumberOfNames; //名字数 (多少函数名字)
PULONG *AddressOfFunctions; //函数的地址
PULONG *AddressOfNames; //名字的地址
PUSHORT *AddressOfNameOrdinals; //名字序数的地址
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ************************ PE.C ************************
// PE文件签名 实际内存镜像地址 = e_lfanew偏移量 + 文件内存镜像基址
#define NTSIGNATURE(a) ((LPVOID)((BYTE *)a + ((PIMAGE_DOS_HEADER)a)->e_lfanew))
// 判断是否NT签名
DWORD WINAPI ImageFileType (LPVOID lpFile)
{
/* DOS文件签名先出现。 */
if (*(USHORT *)lpFile == IMAGE_DOS_SIGNATURE)
{
/* 从DOS头开始确定PE文件头的位置。 */
if (LOWORD (*(DWORD *)NTSIGNATURE (lpFile)) == IMAGE_OS2_SIGNATURE ||
LOWORD (*(DWORD *)NTSIGNATURE (lpFile)) == IMAGE_OS2_SIGNATURE_LE)
{
/* 返回OS头 */
return (DWORD)LOWORD(*(DWORD *)NTSIGNATURE (lpFile));
}
else if (*(DWORD *)NTSIGNATURE (lpFile) == IMAGE_NT_SIGNATURE)
{
/* 返回NT头,只有NT才进行处理 */
return IMAGE_NT_SIGNATURE;
}
else
{
/* 返回DOS头 */
return IMAGE_DOS_SIGNATURE;
}
}
/* 未知的文件类型。 */
else return 0;
}
// PE文件头 实际内存镜像地址 = PE文件签名 实际内存镜像地址 + NT签名size
DWORD SIZE_OF_NT_SIGNATURE // SIZE_OF_NT_SIGNATURE定义为DWORD大小 == NT签名大小 sizeof(IMAGE_NT_SIGNATURE)
#define PEFHDROFFSET(a) ((LPVOID)((BYTE *)a + ((PIMAGE_DOS_HEADER)a)->e_lfanew + SIZE_OF_NT_SIGNATURE))
// 取得PE文件头结构
PIMAGE_FILE_HEADER pfh;
pfh = (PIMAGE_FILE_HEADER)PEFHDROFFSET (lpFile); // lpFile: 可执行文件基址指针
// 取得PE文件头->节的数量 PIMAGE_FILE_HEADER->NumberOfSections
int WINAPI NumOfSections (LPVOID lpFile)
{
/* 文件头中有多少个节 */
return (int)((PIMAGE_FILE_HEADER) PEFHDROFFSET (lpFile))->NumberOfSections);
// PE文件头结构中的其他字段也是这样取出
}
// PE可选头 实际内存镜像地址 = PE文件头 实际内存镜像地址 + PE文件头size
#define OPTHDROFFSET(a) ((LPVOID)((BYTE *)a + ((PIMAGE_DOS_HEADER)a)->e_lfanew + SIZE_OF_NT_SIGNATURE + sizeof (IMAGE_FILE_HEADER)))
// 取得PE可选头->应用程序入口地址 IMAGE_OPTIONAL_HEADER->AddressOfEntryPoint
// 也是输入地址表(Import Address Table,IAT)结束位置 ,因为IAT必定在程序入口地址的前面
LPVOID WINAPI GetModuleEntryPoint (LPVOID lpFile)
{
// 取得PE可选头结构
PIMAGE_OPTIONAL_HEADER poh;
poh = (PIMAGE_OPTIONAL_HEADER)OPTHDROFFSET (lpFile);
if (poh != NULL)
return (LPVOID)poh->AddressOfEntryPoint;
else
return NULL;
// PE可选头结构中的其他字段也是这样取出
}
// 节头就在PE可选头后面 = PE可选头 实际内存镜像地址 + PE可选头size
#define SECHDROFFSET(a) ((LPVOID)((BYTE *)a + ((PIMAGE_DOS_HEADER)a)->e_lfanew + SIZE_OF_NT_SIGNATURE + sizeof (IMAGE_FILE_HEADER) + sizeof (IMAGE_OPTIONAL_HEADER)))
// 取节名相同的节头信息
// lpFile执行文件载入基址, sh空结构, szSection指定的节名
BOOL WINAPI GetSectionHdrByName (LPVOID lpFile, IMAGE_SECTION_HEADER *sh, char *szSection)
{
PIMAGE_SECTION_HEADER psh;
// 取得有多少个节
int nSections = NumOfSections (lpFile);
int i;
// 节头起始地址
if ((psh = (PIMAGE_SECTION_HEADER)SECHDROFFSET (lpFile)) != NULL)
{
/* 按名字循环寻找节 */
for (i=0; i<nSections; i++)
{
if (!strcmp (psh->Name, szSection))
{
/* 将节名相同的节头信息复制到 sh */
CopyMemory ((LPVOID)sh, (LPVOID)psh, sizeof (IMAGE_SECTION_HEADER));
return TRUE;
}
else psh++;
}
}
// 其他节取法相同
return FALSE;
}
// 定位数据目录
LPVOID WINAPI ImageDirectoryOffset (LPVOID lpFile, DWORD dwIMAGE_DIRECTORY)
{
PIMAGE_OPTIONAL_HEADER poh;
PIMAGE_SECTION_HEADER psh;
int nSections = NumOfSections (lpFile);
int i = 0;
LPVOID VAImageDir;
/* 先验证所求数据目录项的数字 */
if (dwIMAGE_DIRECTORY >= poh->NumberOfRvaAndSizes)
return NULL;
/* 可选头偏移量指针 */
poh = (PIMAGE_OPTIONAL_HEADER)OPTHDROFFSET (lpFile);
/* 节头偏移量指针 */
psh = (PIMAGE_SECTION_HEADER)SECHDROFFSET (lpFile);
/* 定位可选头 数据目录(DataDirectory) 相对虚拟地址。 */
VAImageDir = (LPVOID)poh->DataDirectory[dwIMAGE_DIRECTORY].VirtualAddress;
/* 定位节身 循环n个节身 */
while (i++<nSections)
{
/*
lpFile :内存镜像的可执行文件的基址的指针
psh->VirtualAddress :内存中节身的虚拟地址(起始地址)
psh->SizeOfRawData :该节头size
psh->VirtualAddress + psh->SizeOfRawData :节头结束位置
psh->PointToRawData :文件中节身的偏移量
因为数据目录在节头之内,所以:(节头起始起始位置 <= 数据目录 < 节头结束位置)
*/
if (psh->VirtualAddress <= (DWORD)VAImageDir && psh->VirtualAddress + psh->SizeOfRawData > (DWORD)VAImageDir)
break;
psh++;
}
if (i > nSections)
return NULL;
/*
lpFile + psh->PointerToRawData 可执行文件内存基址 + 文件中节身的偏移量 = 节身在内存中实际地址
如果数据目录的虚拟地址不等于节身的偏移量,就要相减 VAImageDir - psh->VirtualAddress ,得出的差再加上上面“节头在内存中实际地址”
*/
/* 内存中节身(数据目录)的实际地址 */
return (LPVOID)(((int)lpFile + (int)VAImageDir - psh->VirtualAddress) + (int)psh->PointerToRawData);
}
// 导出表(.edata)取AddressOfNames值 (还有另外三个函数:GetNumberOfExportedFunctions, GetExportFunctionEntryPoints,GetExportFunctionOrdinals)
int WINAPI GetExportFunctionNames (LPVOID lpFile, HANDLE hHeap, char **pszFunctions)
{
IMAGE_SECTION_HEADER sh;
PIMAGE_EXPORT_DIRECTORY ped;
char *pNames, *pCnt;
int i, nCnt;
/* 取数据目录的指针 */
if ((ped = (PIMAGE_EXPORT_DIRECTORY)ImageDirectoryOffset(lpFile, IMAGE_DIRECTORY_ENTRY_EXPORT)) == NULL)
return 0;
/* 取.edata节头 */
GetSectionHdrByName (lpFile, &sh, ".edata");
/* 确定输出函数名字的偏移量。 */
/*
(int)ped->AddressOfNames - (int)sh.VirtualAddress + (int)sh.PointerToRawData + (int)lpFile
第一句,取得导出表结构里面AddressOfNames的值,但是AddressOfNames本身就是一个虚拟地址,所以要进行第二次计算
AddressOfNames值 - (int)sh.VirtualAddress + (int)sh.PointerToRawData + (int)lpFile
第二句计算方法相同,详细逻辑参考函数ImageDirectoryOffset
*/
pNames = (char *)(*(int *)((int)ped->AddressOfNames - (int)sh.VirtualAddress + (int)sh.PointerToRawData + (int)lpFile) - (int)sh.VirtualAddress + (int)sh.PointerToRawData + (int)lpFile);
/* 计算所有字符串需分配多少内存。 */
pCnt = pNames;
/* NumberOfNames包含多个名字,计算每个名字长度累加 */
for (i=0; i<(int)ped->NumberOfNames; i++)
while (*pCnt++);
nCnt = (int)(pCnt - pNames);
/* 从堆中为函数名字分配内存。 */
*pszFunctions = HeapAlloc (hHeap, HEAP_ZERO_MEMORY, nCnt);
/* 复制所有字符串到缓存区中。 */
CopyMemory ((LPVOID)*pszFunctions, (LPVOID)pNames, nCnt);
return nCnt;
}
/*
由于WINNT.H没有导入表的结构,自定义一个
typedef struct tagImportDirectory
{
DWORD dwRVAFunctionNameList; //函数名字列表的RVA
DWORD dwUseless1; //未用1
DWORD dwUseless2; //未用2
DWORD dwRVAModuleName; //模块名字的RVA
DWORD dwRVAFunctionAddressList; //函数地址列表的RVA
}IMAGE_IMPORT_MODULE_DIRECTORY,
* PIMAGE_IMPORT_MODULE_DIRECTORY;
每个结构代表一个模块(DLL),包含若干个函数
而一个PE中可以包含若干个模块(DLL),即是可以有若干个导入表结构
由数据目录得到的导入表结构地址,代表第一个结构的入口,其他的结构跟随其后
第三个字段:dwRVAFunctionAddressList
载入内存之前与dwRVAFunctionNameList相同,载入内存后才是真正的函数地址列表RVA
*/
// 导入表(.data)取dwRVAModuleName值(模块名)
int WINAPI GetImportModuleNames (LPVOID lpFile, HANDLE hHeap, char **pszModules)
{
PIMAGE_IMPORT_MODULE_DIRECTORY pid;
IMAGE_SECTION_HEADER idsh;
BYTE *pData;
int nCnt = 0, nSize = 0, i;
char *pModule[1024];
char *psz;
/* 取节身内存地址 */
pid = (PIMAGE_IMPORT_MODULE_DIRECTORY)ImageDirectoryOffset(lpFile, IMAGE_DIRECTORY_ENTRY_IMPORT);
pData = (BYTE *)pid;
/* 取节头内存地址 */
if (!GetSectionHdrByName (lpFile, &idsh, ".idata"))
return 0;
/* 提取所有的输入模块,模块名以0结束0 */
while (pid->dwRVAModuleName)
{
/* pData节身 + (模块名偏移地址 - 节身偏移地址) = 模块名实际内存地址 */
pModule[nCnt] = (char *)(pData + (pid->dwRVAModuleName - idsh.VirtualAddress));
/* 为字符串的绝对偏移量分配缓冲区。 */
nSize += strlen (pModule[nCnt]) + 1;
/* 增量到下一个输入目录项。*/
pid++;
nCnt++;
}
/* 复制所有字符串到堆内存的一个块当中。 */
*pszModules = HeapAlloc (hHeap, HEAP_ZERO_MEMORY, nSize);
psz = *pszModules;
for (i=0; i<nCnt; i++)
{
strcpy (psz, pModule[i]);
psz += strlen (psz) + 1;
}
return nCnt;
}
// 导入表(.data)取特定模块的函数名 dwRVAFunctionNameList
int WINAPI GetImportFunctionNamesByModule (LPVOID lpFile, HANDLE hHeap, char *pszModule, char **pszFunctions)
{
PIMAGE_IMPORT_MODULE_DIRECTORY pid;
IMAGE_SECTION_HEADER idsh;
DWORD dwBase;
int nCnt = 0, nSize = 0;
DWORD dwFunction;
char *psz;
/* 取节头内存地址 */
if (!GetSectionHdrByName (lpFile, &idsh, ".idata"))
return 0;
/* 取节身内存地址 */
pid = (PIMAGE_IMPORT_MODULE_DIRECTORY)ImageDirectoryOffset
(lpFile, IMAGE_DIRECTORY_ENTRY_IMPORT);
/*
原句:(DWORD)pid + pid->dwRVAModuleName - idsh.VirtualAddress
拆开后dwBase的值供其他地方使用
*/
dwBase = ((DWORD)pid - idsh.VirtualAddress);
/* 找出特定模块(pszModule)名的pid 函数名列表以0结束 */
while (pid->dwRVAModuleName && strcmp (pszModule, (char *)(pid->dwRVAModuleName+dwBase)))
pid++;
/* 如果找不到模块则退出。 */
if (!pid->dwRVAModuleName)
return 0;
/* 计算函数名的数量以及字符串的长度。 */
dwFunction = pid->dwRVAFunctionNameList;
/*
(dwFunction + dwBase)函数名偏移量的实际地址
(dwFunction + dwBase) + dwBase 因为函数名本来只是偏移量,所以要再计算一次才得出函数名的实际地址
*/
while (dwFunction && *(DWORD *)(dwFunction + dwBase) && *(char *)((*(DWORD *)(dwFunction + dwBase)) + dwBase+2))
{
nSize += strlen ((char *)((*(DWORD *)(dwFunction + dwBase)) + dwBase+2)) + 1;
dwFunction += 4;
nCnt++;
}
/* 为函数名在堆中分配内存。 */
*pszFunctions = HeapAlloc (hHeap, HEAP_ZERO_MEMORY, nSize);
psz = *pszFunctions;
/* 复制函数名到内存指针。 */
dwFunction = pid->dwRVAFunctionNameList;
while (dwFunction && *(DWORD *)(dwFunction + dwBase) && *((char *)((*(DWORD *)(dwFunction + dwBase)) + dwBase+2)))
{
strcpy (psz, (char *)((*(DWORD *)(dwFunction + dwBase)) + dwBase+2));
psz += strlen((char *)((*(DWORD *)(dwFunction + dwBase))+ dwBase+2)) + 1;
dwFunction += 4;
}
return nCnt;
}
- PE
- PE
- PE
- PE
- PE
- PE
- PE
- PE
- PE
- PE
- PE
- PE
- PE文件-PE文件格式
- PE和动态PE
- PE文件格式
- PE文件格式
- PE学习
- PE文件格式
- 查看是否有已经打开的Word对象
- 今天加入,向各位学习!
- 利用boost::asio实现一个简单的服务器框架
- vs2005如何使用用户自定义宏(User Macros)
- 数据库设计原则
- PE
- //有500个人坐成一圈从第一个开始报数为3时出列 再从1开始遇到3出列 依次下去 直到圈里只有一个人时 求它原来在内圈的位置
- 周爱民--等度的流明
- 资深CIO五个步骤教你ERP选型经
- eclipse快捷键大全
- ODS
- 最近的一些安排
- 安全设置
- OpenGL版本与OpenGL扩展机制