分类

类型:
不限 游戏开发 计算机程序开发 Android开发 网站开发 笔记总结 其他
评分:
不限 10 9 8 7 6 5 4 3 2 1
原创:
不限
年份:
不限 2018 2019

技术文章列表

  • C语言-基于Huffman编码原理的文本编码压缩程序

    C语言实现基于Huffman编码原理的编码压缩程序
    huffman编码原理这里不做介绍,梳理一下编码的代码实现。
    编码压缩部分输入待编码文本,输出压缩文本和编码表(供解码使用)

    1.获取txt文件文本,用char字符串保存。void readfile(){ printf("请输入文本:"); gets();}2.分析文本字符频率,保存字符数据,同时创建字符队列以便生成huffman树void getFrequency(){ int i = 0,j = 0,k = 0; char temp = 0; int tag = 0;//标记是否存在 while (str_text[i] != '\0') { for (temp = 0; temp < charmaxsize; temp++) { tag = 0; for ( k = 0; k < j; k++) { if (str_text[i] == chartype[k].character) { chartype[k].count++; tag = 1; break; } } if (tag) break; if (str_text[i] == temp) { chartype[j].character = temp; chartype[j].count++; j++; break; } } i++; }}3.每次返回构建huffman树时所需要队列中的两个最小值,并返回出队后元素数量,方便新的元素插入队列int getMinArr(Huffchar chartype[], Huffchar minarr[]){ int i = 0; int min;//记录最小节点位置 int max = 2;//记录数组元素个数 Huffchar temp; //直接排序选出最小结点 minarr[0] = chartype[0]; min = 0; while (chartype[i].count>0) { if (minarr[0].count>chartype[i].count) { minarr[0] = chartype[i]; min = i; } i++; } //删除min结点 while (chartype[min+1].count>0) { chartype[min] = chartype[min + 1]; min++; } chartype[min].count = 0; //直接排序选出次小结点 minarr[1] = chartype[0]; min = i = 0; while (chartype[i].count>0) { if (minarr[1].count>chartype[i].count) { minarr[1] = chartype[i]; min = i; } i++; } //删除min结点 while (chartype[min+1].count>0) { chartype[min] = chartype[min + 1]; min++; } chartype[min].count = 0; return min;}4.根据字符队列创建huffman树void buildTree(Huffchar chartype[]){int max = 2;Huffchar minarr[2];//两个最小字符结点Hufftree *rp, *lp, *p;if (chartype[1].count == 0)//考虑单一字符文本,不参与队列排序{ if (lp = (Hufftree *)malloc(sizeof(Hufftree))) { lp->character = chartype[0].character; lp->power = chartype[0].count; lp->self = NULL; lp->lchild = NULL; lp->rchild = NULL; } else { printf("内存分配错误!"); exit(0); } if(p = (Hufftree *)malloc(sizeof(Hufftree))) { p->character = 0; p->power = chartype[0].count; p->self = NULL; p->lchild = lp; p->rchild = NULL; chartype[0].self = p; } else { printf("内存分配错误!"); exit(0); }}while (chartype[1].count > 0)//当数组剩余2个及以上输出最小节点{ max = getMinArr(chartype, minarr); //右子树填充最小值 if (rp = (Hufftree *)malloc(sizeof(Hufftree))) { if (minarr[0].self != NULL)//不为叶子节点 rp = minarr[0].self; else { rp->character = minarr[0].character; rp->power = minarr[0].count; rp->rchild = NULL; rp->lchild = NULL; } } else { printf("内存分配错误!"); exit(0); } //左子树填充次小值 if (lp = (Hufftree *)malloc(sizeof(Hufftree))) { if (minarr[1].self != NULL)//不为叶子节点 lp = minarr[1].self; else { lp->character = minarr[1].character; lp->power = minarr[1].count; lp->rchild = NULL; lp->lchild = NULL; } } else { printf("内存分配错误!"); exit(0); } //更新父节点权值 if (p = (Hufftree *)malloc(sizeof(Hufftree))) { p->character = 0; p->power = minarr[0].count + minarr[1].count; p->self = p; p->lchild = lp; p->rchild = rp; } else { printf("内存分配错误!"); exit(0); } ////更新字符数组,将两个最小节点作为整体放入数组 chartype[max].character = 0;//非叶子字符数据记录NULL; chartype[max].count = minarr[0].count + minarr[1].count; chartype[max].self = p;}Root = chartype[0].self;}5.以上完成了huffman树的构建,然后深度优先遍历得到每个字符的编码void dfs(Hufftree * node,int lev){ int i = 0; static int node_i = 0; if (node->lchild == node->rchild) { char_type[node_i].character = node->character; char_type[node_i].count = node->power; for ( i = 0; i < lev; i++) { char_type[node_i].code[i] = code_temp[i]; } node_i++; } else { if (node->lchild) { code_temp[lev] = '0'; dfs(node->lchild,lev + 1); } if (node->rchild) { code_temp[lev] = '1'; dfs(node->rchild, lev + 1); } } }6.根据每个字符的编码,逐个替换原文得到伪二进制流,并转化为ASC||码形式输出//字符编码后转化为二进制流void converToBit(){ int text_i = 0,char_i = 0,i = 0,j = 0; int num = 0; bool tag = true; char tempc; //转换成伪二进制流。 for (text_i = 0; text_i < strmaxsize; text_i++) { for ( char_i= 0; char_i < charmaxsize; char_i++) { if (char_type[char_i].character == str_text[text_i]) { strcat_s(strbit_temp, char_type[char_i].code); } } } puts(strbit_temp); //转换成二进制流,ASCII码形式输出 text_i = 0; while (tag) { for (i = 0; i < 6; i++, text_i++) { if (strbit_temp[text_i] != '\0') { num += ((int)strbit_temp[text_i] - 48)*(1<<(5 - i)); } else {//结束编码 tag = false; break; } } tempc = (char)num+31;//转化为可显示字符 strcode[j++] = tempc; num = 0; } strcode[j] = '\0';//尾部加结束标志 puts(strcode);}7.输出压缩文件和码表//输出字符对应的词频与编码void puttree(){ for (size_t i = 0; i < charmaxsize; i++) { if (char_type[i].character > 0) { printf("%c---%d---", char_type[i].character, char_type[i].count); for (size_t j = 0; j < 20; j++) { printf("%c", char_type[i].code[j]); } printf("\n"); } }}void outfile(){ int i = 0; FILE* fp; fp = fopen(".\\outfile.txt", "w"); fputs(strcode, fp); fclose(fp);}至此完成编码部分tip编码使用的ASC||字符应该使用可显示字符,避开控制字符。所以使用6位为一组转换为二进制。
    附:结构体定义//字符编码信息typedef struct Huff_char { char character = 0; int count = 0; char code[20];}Huff_char;//字符队列typedef struct Huffchar { char character = 0; int count = 0;//统计词频 struct Hufftree* self = NULL;}Huffchar;//哈夫曼树节点typedef struct Hufftree { char character;//节点字符信息 int power;//权值 struct Hufftree* self, *rchild, *lchild;//自身地址,左右孩子}Hufftree;
    0 留言 2018-11-09 21:03:10 奖励5点积分
  • C语言-关于指针的初始化

    指针笔记//测试内存分配bool memory(){int *p;if ((p = (int *)malloc(sizeof(int)))) printf("p分配内存");if ((p = NULL)) printf("p不分配内存");return true;}//测试未初始化 空指针bool voidpoint(){int *p;int *q = (int *)malloc(sizeof(int));p = q;return true;}
    0 留言 2018-11-09 21:01:41 奖励3点积分
  • C语言-for循环中break与false

    for循环笔记循环结束for ( i = 0; i < 5; i++){ printf("%d ",i);}printf("%d ", i);//0,1,2,3,4,5
    i++为循环内操作,条件为false仍执行。

    break中断for ( i = 0; i < 5; i++){ break;}printf("%d ", i); // 0
    break为终止循环,i++不执行。
    0 留言 2018-11-09 20:58:18 奖励3点积分
  • 运行单一实例

    背景病毒木马在使用各种手段植入到用户计算机后,也会使用浑身解数使自己被用户执行激活。但是,如果病毒木马自己被多次重复运行,系统中存在多分病毒木马的进程,那么,这就有可能增加被暴露的风险。所以,要想解决上述问题,就要确保系统上只运行一个病毒木马的进程实例。
    确保运行一个进程实例的实现方法有很多,可以是通过扫描进程列表来实现,也可以通过枚举程序窗口的方式来实现或者可以通过共享全局变量来实现。接下来,本文将介绍一种被病毒木马广泛使用而且使用简单的方法,即通过创建系统命名互斥对象的方式实现。
    函数介绍1. CreateMutex函数
    创建或打开一个已命名或未命名的互斥对象。
    HANDLE WINAPI CreateMutex( _In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes, _In_ BOOL bInitialOwner, _In_opt_ LPCTSTR lpName);
    参数
    lpMutexAttributes [in, optional]指向SECURITY_ATTRIBUTES结构的指针。如果此参数为NULL,则该句柄不能由子进程继承。bInitialOwner [in]如果此值为TRUE并且调用者创建了互斥锁,则调用线程将获得互斥锁对象的初始所有权。否则,调用线程不会获得互斥锁的所有权。lpName [in, optional]互斥对象的名称。该名称仅限于MAX_PATH字符。名称比较区分大小写。如果lpName为NULL,则会创建不带名称的互斥体对象。如果lpName与现有事件,信号量,等待定时器,作业或文件映射对象的名称匹配,则该函数将失败,并且GetLastError函数返回ERROR_INVALID_HANDLE。这是因为这些对象共享相同的名称空间。该名称可以具有“Global”或“Local”前缀以在全局或会话名称空间中显式创建对象。名称的其余部分可以包含除反斜杠字符(\)以外的任何字符。
    返回值
    如果函数成功,则返回值是新创建的互斥对象的句柄。如果函数失败,返回值为NULL。 要获得扩展的错误信息,请调用GetLastError。如果互斥锁是一个已命名的互斥锁,并且该对象在此函数调用之前就存在,则返回值是现有对象的句柄,GetLastError返回ERROR_ALREADY_EXISTS。

    实现原理通常情况下,系统上的进程是相互独立的,每个进程都拥有自己独立资源和地址空间,进程间互不影响。所以,同一个程序可以重复运行,在系统上的进程互不影响。但是,在一些特殊的情况,需要程序在系统上只存在一份进程实例,这就引出了进程互斥的问题。
    微软提供了CreateMutex函数来实现创建或者打开一个已命名或未命名的互斥对象,程序每次运行的时候,通过判断系统上是否存在相同的命名互斥对象来确定程序是否重复运行。
    CreateMutex函数一共有三个参数,第一个参数表示互斥对象的安全设置,是一个指向SECURITY_ATTRIBUTES结构的指针,在该程序中直接设置为NULL即可。第二个参数表示线程是否获得互斥锁对象的初始所有权,在该程序中,无论该参数为TRUE或者FALSE,均不影响程序的正常执行。第三个参数表示互斥对象的名称,对于通过过互斥对象来判断进程实例是否重复运行的程序来说,该参数一定要设置,而且设置的名称要保证唯一性。
    程序的判断原理是通过CreateMutex函数创建一个命名互斥对象,如果对象创建成功,而且通过调用GetLastError函数获取的返回码的值为ERROR_ALREADY_EXISTS,则表示该命名互斥对象存在,即程序重复运行。否则,认为是首次运行程序。
    编码实现// 判断是否重复运行BOOL IsAlreadyRun(){ HANDLE hMutex = NULL; hMutex = ::CreateMutex(NULL, FALSE, "TEST"); if (hMutex) { if (ERROR_ALREADY_EXISTS == ::GetLastError()) { return TRUE; } } return FALSE;}
    测试直接运行上面的程序,第一次运行的时候,程序提示“NOT Already Run!“,如图2-1所示,意思是系统上没有该实例运行。接着继续双击执行程序,这次程序提示”Already Run!!!!“,如图2-2所示,意思是系统上已经存在该实例正在运行着。所以,程序成功判断出程序是否重复运行。


    小结这个程序实现起来不难,关键是熟悉对CreateMutex函数的调用。在调用CreateMutex函数来创建命名的互斥对象的时候,注意互斥对象的名称不要与现有事件、信号量或者文件映射对象等的名称相同,否则创建互斥对象失败。
    在实现的过程中,特别要注意,程序一定不要调用CloseHandle函数来关闭CreateMutex函数创建出来的互斥对象的句柄,否则会导致互斥对象判断失败。因为CloseHandle函数会关闭互斥对象的句柄,释放资源。这样,系统上便不会存在对应的命名互斥对象了。这样,通过CreateMutex来创建命名互斥对象都不会重复的。
    参考参考自《Windows黑客编程技术详解》一书
    1 留言 2018-11-07 11:53:44 奖励5点积分
  • DLL延迟加载

    背景我们在开发程序的时候,通常会使用第三方库,但是并不是所有的第三方库都会提供静态库文件,而是提供动态库文件。这样,我们的程序就需要相应的DLL文件才能加载启动。
    现在,我们想大家介绍一种DLL延迟加载技术,使用延迟加载方式编译链接可执行文件。这样,可执行程序就可以先加载执行,所依赖的DLL在正式调用时才被加载进来。
    这样做的好处就是,我们可以把必需的DLL文件以资源的形式包含到我们的程序中,并使用DLL延迟加载技术延迟加载必需的DLL。这样,在正式调用必需的DLL之前,程序都是可以正常执行的,我们可以在程序运行的时候,就把在资源中的DLL释放到本地,所以,程序所有功能就可以正常执行了。我们只需把EXE文件发送给用户,而不需要附加DLL文件了,也不需要担心程序会丢失DLL文件。
    实现原理DLL延迟加载原理本程序以加载第三方库——skin++库为例子进行讲解演示。我们在导入库文件,编码完成后,对程序编译链接等到可执行文件。我们使用PEview.exe查看可执行文件的导入表,就可知道可执行文件所必需的DLL文件。

    上面可以知道,导入表中有SkinPPWTL.dll文件,也就是说,程序加载运行的时候,SkinPPWTL.dll文件必需存在,否则程序不能正常加载启动。
    DLL延迟加载技术的原理,其实就是在导入表中去掉SkinPPWTL.dll这一项,等到DLL被正式调用的时候,才会加载DLL文件。这样,程序在正式调用DLL之前,都是可以正常执行的。
    DLL延迟加载实现DLL延迟加载的实现,并不需要任何编码,只需要设置下开发环境的链接选项即可。本程序使用的是VS2013开发环境,还是继续上述skin++皮肤库的例子进行讲解。DLL延迟加载实现的步骤为:
    属性—>链接器—>输入—>延迟加载的DLL—>输入:SkinPPWTL.dll

    这样,DLL延迟加载就完成了。这时,我们编译链接,再用PEview.exe查看可执行程序的导入表信息:

    可见,导入表中没有SkinPPWTL.dll文件了。所以,程序在正式调用该DLL之前,程序都是可以加载执行的。
    接下来,我们就把SkinPPWTL.dll作为资源导入,在加载皮肤之前就释放DLL到本地上,这样程序正常加载执行了。
    编码实现皮肤加载// 加载界面皮肤void LoadSkin(){ // 加载皮肤 // 获取当前目录 char szCurrentPath[MAX_PATH] = { 0 }; GetCurrentPath(szCurrentPath, MAX_PATH); // 构造路径 ::lstrcat(szCurrentPath, "\\Skins\\"); ::lstrcat(szCurrentPath, "dogmax2.ssk"); // 加载皮肤 ::skinppLoadSkin(szCurrentPath);}
    资源释放// 释放资源BOOL FreeMyResource(UINT uiResouceName, char *lpszResourceType, char *lpszSaveFileName){ HRSRC hRsrc = ::FindResource(NULL, MAKEINTRESOURCE(uiResouceName), lpszResourceType); if (NULL == hRsrc) { FreeRes_ShowError("FindResource"); return FALSE; } DWORD dwSize = ::SizeofResource(NULL, hRsrc); if (0 >= dwSize) { FreeRes_ShowError("SizeofResource"); return FALSE; } HGLOBAL hGlobal = ::LoadResource(NULL, hRsrc); if (NULL == hGlobal) { FreeRes_ShowError("LoadResource"); return FALSE; } LPVOID lpVoid = ::LockResource(hGlobal); if (NULL == lpVoid) { FreeRes_ShowError("LockResource"); return FALSE; } FILE *fp = NULL; fopen_s(&fp, lpszSaveFileName, "wb+"); if (NULL == fp) { FreeRes_ShowError("LockResource"); return FALSE; } fwrite(lpVoid, sizeof(char), dwSize, fp); fclose(fp); return TRUE;}
    程序测试我们直接运行EXE程序,程序正常执行,DLL成功释放,皮肤加载成功:

    总结该程序实现并不难,只是多了个设置链接选项。这样,程序就会更加简洁,而且方便易用。这算是程序开发的一个小技巧吧,希望能对大家有所帮助。
    参考参考自《Windows黑客编程技术详解》一书
    1 留言 2018-11-07 11:50:46 奖励6点积分
  • 判断指定进程是否具有管理员权限

    背景某天,一个小伙伴突然问我,用什么程序或者软件可以查看计算机上进程是否具有管理员或者管理员以上的权限。我一下子懵了,印象中好像还真没有遇到过可以判断进程是否有管理员权限的软件。我运行了我电脑上的 Process Explorer 进行查看,好像确实没有检测管理员权限的功能。
    但是,作为一个动手能力较强的程序员,没有的程序可以自己写。于是,自己上网搜索了判断进程是否具有管理员权限的实现原理。原来,就是通过获取进程令牌的令牌特权提升信息,令牌特权提升信息中就专门有一个标志表示进程权限是否提升。如果进程权限已经提升,则说明是具有管理员或者管理员以上的权限,否则,只是普通权限。
    现在,我就把程序实现的过程及其原理进行整理,形成文档,分享给大家。
    函数介绍OpenProcess 函数
    打开一个已存在的进程对象,并返回进程的句柄。
    函数声明
    HANDLE OpenProcess( DWORD dwDesiredAccess, //渴望得到的访问权限(标志) BOOL bInheritHandle, // 是否继承句柄 DWORD dwProcessId // 进程标示符);
    参数

    dwDesiredAccess [in]访问进程对象。 此访问权限将针对进程的安全描述符进行检查。 此参数可以是一个或多个进程访问权限。如果调用者启用了SeDebugPrivilege权限,则无论安全描述符的内容如何,都会授予所请求的访问权限。bInheritHandle [in]如果此值为TRUE,则此进程创建的进程将继承该句柄。 否则,进程不会继承此句柄。dwProcessId [in]要打开的本地进程的标识符。
    返回值

    如果函数成功,则返回值是指定进程的打开句柄。如果函数失败,返回值为NULL。要获取扩展错误信息,请调用GetLastError。

    OpenProcessToken 函数
    打开与进程关联的访问令牌。
    函数声明
    BOOL WINAPI OpenProcessToken( _In_ HANDLE ProcessHandle, _In_ DWORD DesiredAccess, _Out_ PHANDLE TokenHandle);
    参数

    ProcessHandle [in]要打开访问令牌的进程的句柄。 该进程必须具有PROCESS_QUERY_INFORMATION访问权限。DesiredAccess [in]指定一个访问掩码,指定访问令牌的请求类型。 这些请求的访问类型与令牌的自由访问控制列表(DACL)进行比较,以确定哪些访问被授予或拒绝。TokenHandle [出]指向一个句柄的指针,用于标识当函数返回时新打开的访问令牌。
    返回值

    如果函数成功,则返回值不为零。如果函数失败,返回值为零。 要获取扩展错误信息,请调用GetLastError。

    GetTokenInformation 函数
    获取有关访问令牌的指定类型的信息。 调用进程必须具有获取信息的适当访问权限。
    函数声明
    BOOL WINAPI GetTokenInformation( _In_ HANDLE TokenHandle, _In_ TOKEN_INFORMATION_CLASS TokenInformationClass, _Out_opt_ LPVOID TokenInformation, _In_ DWORD TokenInformationLength, _Out_ PDWORD ReturnLength);
    参数

    TokenHandle [in]检索信息的访问令牌的句柄。如果TokenInformationClass指定TokenSource,则该句柄必须具有TOKEN_QUERY_SOURCE访问权限。对于所有其他TokenInformationClass值,句柄必须具有TOKEN_QUERY权限。TokenInformationClass[in]指定来自TOKEN_INFORMATION_CLASS枚举类型的值,以识别函数检索的信息类型。检查TokenIsAppContainer并返回0的任何呼叫者也应该验证呼叫者令牌不是识别级别的模拟令牌。如果当前令牌不是应用程序容器,而是身份级别令牌,则应返回AccessDenied。TokenInformation[out]指向缓冲区的指针,该函数填充所请求的信息。放入此缓冲区的结构取决于由TokenInformationClass参数指定的信息类型。TokenInformationLength[in]指定由TokenInformation参数指向的缓冲区的大小(以字节为单位)。如果TokenInformation为NULL,则此参数必须为 0。ReturnLength [out]指向一个变量的指针,该变量接收TokenInformation参数指向的缓冲区所需的字节数。如果该值大于TokenInformationLength参数中指定的值,则该函数将失败,并且不将数据存储在缓冲区中。如果TokenInformationClass参数的值为TokenDefaultDacl,并且令牌没有默认DACL,则该函数将ReturnLength指向的变量设置为sizeof(TOKEN_DEFAULT_DACL),并将TOKEN_DEFAULT_DACL结构的DefaultDacl成员设置为NULL。
    返回值

    如果函数成功,则返回值不为零。如果函数失败,返回值为零。 要获取扩展错误信息,请调用GetLastError。

    实现原理判断一个进程是否具有管理员权限,可以分成 4个步骤:

    首先,使用 OpenProcess 打开进程,并获取 PROCESS_ALL_ACCESS 权限的进程句柄
    然后,根据进程句柄,可以调用 OpenProcessToken 函数打开进程令牌,并获取具有 TOKEN_QUERY 权限的进程令牌。因为下面我们需要查询进程令牌的权限提升信息,所以要求进程令牌必须具有 TOKEN_QUERY 权限
    接着,我们就可以调用 GetTokenInformation 函数,获取令牌权限提信息 TokenElevation,获取的信息存储在 TOKEN_ELEVATION 结构体缓冲区里。TOKEN_ELEVATION 结构体的成员 TokenIsElevated 就表示进程令牌权限是否提升。TRUE,表示已经提升,则是管理员或者管理员以上的权限;FALSE,表示没有提升,则表示普通权限
    最后,我们关机进程句柄,返回判断结果

    编码实现// 判断指定进程是否具有管理员或者管理员以上的权限BOOL IsProcessRunAsAdmin(DWORD dwProcessId){ HANDLE hProcess = NULL; HANDLE hToken = NULL; TOKEN_ELEVATION tokenEle = {0}; BOOL bElevated = FALSE; DWORD dwRet = 0; BOOL bRet = FALSE; // 打开进程, 获取进程句柄 hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (NULL == hProcess) { ShowError("OpenProcess"); return FALSE; } // 打开进程令牌, 获取进程令牌句柄 bRet = ::OpenProcessToken(hProcess, TOKEN_QUERY, &hToken); if (FALSE == bRet) { ShowError("OpenProcessToken"); return FALSE; } // 获取进程令牌特权提升信息 bRet = ::GetTokenInformation(hToken, TokenElevation, &tokenEle, sizeof(tokenEle), &dwRet); if (FALSE == bRet) { ShowError("GetTokenInformation"); return FALSE; } // 获取进程是否提升的结果 bElevated = tokenEle.TokenIsElevated; // 关闭进程句柄 ::CloseHandle(hToken); return bElevated;}
    程序测试我们以普通权限运行 520.exe 程序,然后使用我们的程序进行判断,结果判断正确:

    然后,我们以管理员权限运行 520.exe 程序,并使用我们的程序进行判断,结果正确:

    总结要注意一点就是,我们运行自己的判断程序的时候,建议使用管理员权限运行。因为我们判断一个程序是否具有管理员权限之前,先调用 OpenProcess 函数打开进程,那么,低权限程序打开高权限的程序,执行这个函数就会因为权限不足而报错。所以,如果一开始我们就以管理员权限运行我们的判断程序,就可以避免这种情况的发生。
    参考参考自《Windows黑客编程技术详解》一书
    1 留言 2018-11-07 11:37:51 奖励3点积分
  • 在DllMain中测试调用LoadLibrary和CreateThread函数可正常使用 精华

    背景某天,在网上无意中看到一篇博文,标题是说在DLL的入口点函数DllMain中不能调用 LoadLibrary 加载DLL,因为会造成死锁。看到这里我楞了一下,因为我之前写过很多DLL程序,在入口点函数DllMain中也加载过其它的DLL,从没有出现过什么问题。然后,我便仔细阅读了这篇博文,大概理解了它的意思。它应该想表达的是在DLL的DllMain函数中谨慎使用 LoadLibrary,以防发生死锁情况。
    虽然都懂了它的意思,我还是决定自己再亲自动手写一下代码看看。现在,我就把实现的过程和测试心得整理成文档,分享给大家。
    实现过程首先,我直接创建一个名为 Dll_LoadLibrary_CreateThread_Test 的DLL项目工程,在 DllMain 的 DLL_PROCESS_ATTACH 时候直接调用 CreateThread 函数创建一个多线程,同时,也调用 LoadLibrary 加载另一个 DLL 文件。
    BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ){ switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { // 创建多线程 ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, 0, NULL); // 加载DLL HMODULE hDll = ::LoadLibrary("testdll.dll"); if (NULL == hDll) { ::MessageBox(NULL, "load testdll.dll error.", "error", MB_OK); } break; } … …(省略) } return TRUE;}
    其中,ThreadProc 多线程函数,执行的操作就是每个 5000 毫秒就弹窗提示一次。
    UINT ThreadProc(LPVOID lpVoid){ while (TRUE) { Sleep(5000); ::MessageBox(NULL, "this si from createthread.", "createthread", MB_OK); } return 0;}
    在 testdll.dll 这个DLL的入口点函数DllMain中,直接弹窗提示。
    BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ){ switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { // 弹窗 ::MessageBox(NULL, "this is from testdll.dll DllMain function.", "testdll.dll", MB_OK); break; } … …(省略) } return TRUE;}
    然后,我们直接加载 Dll_LoadLibrary_CreateThread_Test.dll 文件,成功弹出下图所示的两个提示框,说明 CreateThread 函数成功创建多线程,LoadLibrary 成功加载DLL。


    总结经过上面的例子测试,说明在 DllMain 函数中,是可以使用 CreateThread 以及 LoadLibrary 函数的。只要我们避免相互在 DllMain 中相互调用,出现死锁,如下面这种相互调用的情况:

    DllB 在 DllMain 里调用 LoadLibrary 加载 DllA
    DllA 在 DllMain 里调用 LoadLibrary 加载 DllB

    这样,就会无限循环加载下去,形成死锁。
    所以,之所以说 在DllMain里不能调用LoadLibrary函数,其实,并不是说只要是在 DllMain 中,都不能调用 LoadLibrary 函数,而是说,如果这两个 Dll 如果在 DllMain 中相互调用的情况下,是会出错的,所以,为了避免这种相互调用死锁的情况发生,就不提倡在 DllMain 中调用 LoadLibrary!实际上,只要避免这种相互调用的情况,LoadLibrary 还是可以使用的!
    参考参考自《Windows黑客编程技术详解》一书
    1 留言 2018-11-07 11:31:42 奖励15点积分
  • 编程实现对ini配置文件的读写

    背景在安装一些应用程序的时候,我们经常可以其安装目录下看到有 .ini 格式的配置文件,这种格式配置文件是我们比较常见的。在我们自己电脑的系统上也会有,而且还很多。很多系统自带的程序,都会有一个 .ini 格式的配置文件。
    为此,Windows还特地提供了相应的WIN32 API函数去对 .ini 格式的配置文件进行读写操作。所以,本文就主要介绍下如何使用API函数来实现对 .ini 配置文件的读写。现在,我就把实现过程整理成文档,分享给大家。
    函数介绍WritePrivateProfileString 函数
    将一个字符串复制到INI文件的指定的字段中。
    函数声明
    BOOL WINAPI WritePrivateProfileString( _In_ LPCTSTR lpAppName, _In_ LPCTSTR lpKeyName, _In_ LPCTSTR lpString, _In_ LPCTSTR lpFileName);
    参数

    lpAppName [in]要复制字符串的字段名称。 如果该字段不存在,则创建它。
    lpKeyName [in]
    与字符串关联的键的名称。 如果该键在指定的字段下不存在,则创建它。 如果此参数为NULL,则整个字段(包括该字段中的所有的键)将被删除。
    lpString [in]要写入文件的以NULL结尾的字符串。 如果此参数为NULL,则lpKeyName参数指向的键将被删除。
    lpFileName [in]INI文件的名称。如果文件是使用Unicode字符创建的,则该函数将Unicode字符写入该文件。 否则,函数写入ANSI字符。

    返回值

    如果函数成功将字符串复制到初始化文件,则返回值不为零。如果函数失败,或者刷新最近访问的初始化文件的缓存版本,返回值为零。 要获取扩展错误信息,请调用GetLastError。

    GetPrivateProfileString 函数
    从INI文件中的指定的字段中获取一个字符串。
    函数声明
    DWORD WINAPI GetPrivateProfileString( _In_ LPCTSTR lpAppName, _In_ LPCTSTR lpKeyName, _In_ LPCTSTR lpDefault, _Out_ LPTSTR lpReturnedString, _In_ DWORD nSize, _In_ LPCTSTR lpFileName);
    参数

    lpAppName [in]字段的名称。如果此参数为NULL,则GetPrivateProfileString函数将文件中的所有字段名复制到提供的缓冲区里。lpKeyName [in]要检索的键名。如果此参数为NULL,则由lpAppName参数指定的字段中的所有键名将复制到由lpReturnedString参数指定的缓冲区中。lpDefault [in]默认字符串。如果在INI文件中找不到lpKeyName键,则GetPrivateProfileString将默认字符串复制到lpReturnedString缓冲区。如果此参数为NULL,则默认值为空字符串“”。避免指定一个带有空白字符的默认字符串。该函数在lpReturnedString缓冲区中插入一个空字符以去除任何尾随的空白。lpReturnedString [out]指向接收检索字符串的缓冲区的指针。nSize [in]lpReturnedString参数指向的缓冲区的大小,以字符为单位。lpFileName [in]INI文件的名称。如果此参数不包含文件的完整路径,系统将在Windows目录中搜索该文件。
    返回值
    返回复制到缓冲区的字符数,不包括终止空字符。

    GetPrivateProfileInt 函数
    在INI文件中的指定的字段中获取它的整数值。
    函数声明
    UINT WINAPI GetPrivateProfileInt( _In_ LPCTSTR lpAppName, _In_ LPCTSTR lpKeyName, _In_ INT nDefault, _In_ LPCTSTR lpFileName);
    参数

    lpAppName [in]INI文件中的字段名称。lpKeyName [in]要检索其值的键名。此值为字符串的形式,GetPrivateProfileInt函数将字符串转换为整数并返回整数。nDefault [in]如果在初始化文件中找不到键名,返回的默认值。lpFileName [in]INI文件的名称。 如果此参数不包含文件的完整路径,系统将在Windows目录中搜索该文件。
    返回值

    返回INI文件中指定键名后的字符串转换后的整数。 如果找不到键,则返回值是指定的默认值。

    实现过程根据上面的函数介绍,我们直接调用上述函数对INI配置文件进行操作。本文的例子如下:
    首先,我们调用 WritePrivateProfileString 函数创建一个名为Config.ini的INI文件,并添加INFORMATION和OTHER字段;在INFORMATION字段下,有两个键名分别为name和age的键,其中键名为name中存储的键值是DemonGan,键名为age中存储的键值为18。在OTHER字段中,键名为class的键值为no1。
    char szFileName[] = "C:\\Users\\DemonGan\\Desktop\\ReadWriteIniFile_Test\\Debug\\Config.ini"; // 向INI文件中写入数据 ::WritePrivateProfileString("INFORMATION", "name", "DemonGan", szFileName); ::WritePrivateProfileString("INFORMATION", "age", "18", szFileName); ::WritePrivateProfileString("OTHER", "class", "no1", szFileName);
    那么,生成的INI文件的数据内容如下所示:

    然后,我们调用 GetPrivateProfileString 函数,读取INI文件中的INFORMATION字段下的name键的数据。
    // 从INI文件中读取字符串数据 char szReturnString[MAX_PATH] = {0}; ::GetPrivateProfileString("INFORMATION", "name", NULL, szReturnString, MAX_PATH, szFileName); printf("name=%s\n", szReturnString);
    接着,我们调用 GetPrivateProfileInt 函数,读取INI文件中的INFORMATION字段下的age键的数据。
    // 从INI文件中读取整型数据 int iReturnInt = ::GetPrivateProfileInt("INFORMATION", "age", 0, szFileName); printf("age=%d\n", iReturnInt);
    对于类似age=18的整型数据的读取,我们除了可以使用 GetPrivateProfileInt 函数读取之外,还可以使用 GetPrivateProfileString 函数去读取,只要将获取的字符串转换为整型就可以了。其中,GetPrivateProfileInt 函数也是先调用获取字符串,然后再转换为整型,所以,GetPrivateProfileInt 函数是对 GetPrivateProfileString 函数的封装和拓展。
    程序测试1我们直接运行程序,则成功从INI文件中获取数据并显示出来,同时目录下有 Config.ini 文件生成,打开文件可以查看里面的数据内容。

    总结要注意的是,上面 3 个函数的INI文件路径最好都写上绝对路径,否则系统会自动在Windows目录中搜索,这样,会导致文件数据读取失败。
    1 留言 2018-11-07 11:13:17 奖励5点积分
  • 动态导入DLL动态链接库并调用其导出函数

    背景DLL调用有两种方式,一种是静态调用,另一种是动态调用。对于静态调用主要是对开发环境进行设置,导入DLL所需的头文件和库文件,就可以直接调用DLL中导出的函数了,这种调用方式会使程序较大,占用内存较大,但速度较快,而且使用起来比较方便。
    但是,本文要介绍的是动态调用的方式,动态调用可以根据需要加载响应函数,随时可卸载。不会因为找不到dll,导致程序不能运行。但是,使用上相对于静态调用来说比较麻烦。
    现在,就把DLL动态调用及调用其导出函数的实现过程整理成文档,分享给大家。
    函数介绍LoadLibrary 函数
    将指定的模块加载到调用进程的地址空间中。 指定的模块可能会导致其他模块被加载。函数声明
    HMODULE WINAPI LoadLibrary( _In_ LPCTSTR lpFileName);
    参数

    lpFileName [in]
    模块的名称。这可以是库模块(.dll文件)或可执行模块(.exe文件)。指定的名称是模块的文件名,与库模块本身中存储的名称无关,由模块定义(.def)文件中的LIBRARY关键字指定。
    如果字符串指定完整路径,则该函数仅搜索该模块的该路径。
    如果字符串指定相对路径或没有路径的模块名称,则该函数使用标准搜索策略来查找模块;有关更多信息,请参阅备注。
    如果该功能找不到模块,该功能将失败。指定路径时,请确保使用反斜杠(\),而不是斜杠(/)。有关路径的更多信息,请参阅命名文件或目录。
    如果字符串指定了没有路径的模块名称,并且省略了文件扩展名,则该函数将默认库扩展名.dll附加到模块名称。要防止函数将.dll附加到模块名称,请在模块名称字符串中包含一个尾点字符(.)。

    返回值

    如果函数成功,则返回值是模块的句柄。如果函数失败,返回值为NULL。 要获取扩展错误信息,请调用GetLastError。

    GetProcAddress 函数
    从指定的动态链接库(DLL)检索导出的函数或变量的地址。
    函数声明
    FARPROC WINAPI GetProcAddress( _In_ HMODULE hModule, _In_ LPCSTR lpProcName);
    参数

    hModule [in]包含该函数或变量的DLL模块的句柄。 LoadLibrary,LoadLibraryEx,LoadPackagedLibrary或GetModuleHandle函数返回此句柄。GetProcAddress函数不会从使用LOAD_LIBRARY_AS_DATAFILE标志加载的模块中检索地址。 有关更多信息,请参阅LoadLibraryEx。lpProcName [in]函数或变量名,或函数的序数值。 如果此参数是序数值,则它必须在低位字中,高阶字必须为零。
    返回值

    如果函数成功,则返回值是导出函数或变量的地址。如果函数失败,返回值为NULL。 要获取扩展错误信息,请调用GetLastError。

    实现原理本文调用的 DLL 使用的是 《两种方式实现DLL导出函数之dllexport与def文件导出》文章中实现的 DLL。动态加载的过程如下:
    首先,我们使用 LoadLibrary 函数加载DLL到进程中,并获取 DLL 的加载句柄。
    /* 加载DLL */ HMODULE hDll = ::LoadLibrary("DllExport_Test.dll"); if (NULL == hDll) { ShowError("LoadLibrary"); }
    然后,根据DLL中的导出函数,声明导出函数指针。
    // 声明函数指针typedef BOOL(*typedef_MyExportFunc_def)(char *pszText, char *pszCaption);
    接着,调用 GetProcAddress 函数,根据导出函数的名称获取导出函数的地址。
    // 根据名称获取导出函数地址 typedef_MyExportFunc_def MyFunc1 = (typedef_MyExportFunc_def)::GetProcAddress(hDll, "MyExportFunc_def"); if (NULL == MyFunc1) { ShowError("GetProcAddress"); }
    同时,我们也可以根据导出函数的导出序号来获取导出函数的地址,使用如下所示,要注意第 2 个参数是序数值,则它必须在低位字中,高阶字必须为 0。
    // 根据导出序号获取导出函数地址 DWORD dwExportOrder = 1; typedef_MyExportFunc_def MyFunc2 = (typedef_MyExportFunc_def)::GetProcAddress(hDll, (LPCSTR)dwExportOrder); if (NULL == MyFunc2) { ShowError("GetProcAddress"); }
    然后,我们就可以直接调用导出函数了。
    /* 调用导出函数 */MyFunc1("This Is Export Function By Function Name!", "1");MyFunc1("This Is Export Function By Export Order!", "2");
    最后,当我们程序不在使用到 DLL 的时候,要对它进行卸载释放资源。
    /* 卸载DLL */ ::FreeLibrary(hDll);
    程序测试我们直接运行程序,则成功显示两个弹窗,第一个弹窗如下:

    第二个弹窗如下所示:

    总结对于上面的 GetProcAddress 从 DLL 获取导出函数的地址,它既可以根据导出函数的名称获取也可以根据导出函数的导出序号进行获取,要注意区分两种的使用形式,注意区别。
    1 留言 2018-11-07 11:06:39 奖励5点积分
  • 最简单的图片格式转换支持4种图片类型基于CImage实现

    背景图片文件我们常常在日常生活中都会使用,与我们的生活息息相关。你有没有想过,图片文件不同格式之间有什么差别吗?它们之间是如何进行转换的?好吧,老实说,我也没有想过。如果让你开发一个图片格式转换器,估计你也会觉得很难吧,毕竟不同图片类型的文件格式都不清楚是怎样的。
    如果,我告诉你有这么一种方法,你不需要了解任何的图片格式,而且三四行代码就可以实现图片格式转换了,你会相信吗?事实上,这是可以实现的,本文讲的就是这么一种最简单的方法。使用 CImage 类提供的接口实现,支持.PNG、.JPG、 .GIF、.BMP 等 4 种格式图片的转换。
    那么,我就把程序的实现原理和过程写成文档,分享给大家。
    实现原理先来介绍下 CImage 类,CImage类是ATL和MFC共用的一个类,其头文件为atlimage.h,主要用于图片文件的打开,显示与保存。这里需要注意的是,VC6.0版本不支持 CImage!
    使用 CImage 实现图片类型转换的原理是:

    首先,CImage::Load函数,根据图片文件路径,加载图片到 CImage 对象中
    然后,调用CImage::Save函数,实现将位图存储为图片文件,图片类型根据传入的保存文件名确定。支持.PNG、.JPG、.GIF、.BMP等 4 种格式图片的生成

    就这样,简单的两步操作,就可以将不同格式的图片转换成任意格式的图片文件了,支持.PNG、.JPG、.GIF、.BMP等 4 种格式文件的相互转换。
    编程实现头文件#include <atlimage.h>
    图片类型转换BOOL ConverPicture(){ CImage image; // 加载图片 image.Load("1.jpg"); // 保存图片 // bmp格式 image.Save("ConverPicture.bmp"); // png格式 image.Save("ConverPicture.png"); // jpg格式 image.Save("ConverPicture.jpg"); // gif格式 image.Save("ConverPicture.gif"); return TRUE;}
    程序测试直接运行程序,1.jpg 图片成功转换成.PNG、.JPG、.GIF、.BMP 4 种格式图片文件:

    总结这个程序原理很简单,但是要注意的是,这种方法不适用于VC6.0开发环境,因为VC6.0上不支持 CImage 类的使用。
    参考参考自《Windows黑客编程技术详解》一书
    1 留言 2018-11-07 10:21:23 奖励5点积分
  • 使用AnimateWindow函数实现窗口显示关闭滚动和滑动特效

    背景我们在开发自己的程序的时候,应该都想把自己的程序加上酷炫的界面和酷炫的特效,让人眼前一亮吧。不管你想不想,反正,我一直都想这么做的,只不过是由于水平有限而已。
    在没有了解 AnimateWindow 这个API函数之前,我以为要实现一个窗口淡出淡入的特效是比较复杂的。但,在了解 AnimateWindow 函数之后才发现,原来就是那么一两行代码的问题。
    现在,我把使用 AnimateWindow 函数实现窗口淡出淡入效果的实现思路以及实现过程,写成文档分享给大家。
    函数介绍AnimateWindow 函数
    在显示与隐藏窗口时能产生特殊的效果。有两种类型的动画效果:滚动动画和滑动动画。
    函数声明
    BOOL AnimateWindow( HWND hWnd, DWORD dwTime, DWORD dwFlags);
    参数

    hWnd:指定产生动画的窗口的句柄。
    dwTime:指明动画持续的时间(以微秒计),完成一个动画的标准时间为200微秒。
    dwFags:指定动画类型。这个参数可以是一个或多个下列标志的组合。标志描述:




    VALUE
    MEANING




    AW_SLIDE
    使用滑动类型。缺省则为滚动动画类型。当使用AW_CENTER标志时,这个标志就被忽略


    AW_ACTIVATE
    激活窗口。在使用了AW_HIDE标志后不要使用这个标志


    AW_BLEND
    使用淡出效果。只有当hWnd为顶层窗口的时候才可以使用此标志


    AW_HIDE
    隐藏窗口,缺省则显示窗口


    AW_CENTER
    若使用了AW_HIDE标志,则使窗口向内重叠;若未使用AW_HIDE标志,则使窗口向外扩展


    AW_HOR_POSITIVE
    自左向右显示窗口。该标志可以在滚动动画和滑动动画中使用。当使用AW_CENTER标志时,该标志将被忽略


    AW_VER_POSITIVE
    自顶向下显示窗口。该标志可以在滚动动画和滑动动画中使用。当使用AW_CENTER标志时,该标志将被忽略


    AW_VER_NEGATIVE
    自下向上显示窗口。该标志可以在滚动动画和滑动动画中使用。当使用AW_CENTER标志时,该标志将被忽略


    AW_HOR_NEGATIVE
    自右向左显示窗口。该标志可以在滚动动画和滑动动画中使用。当使用AW_CENTER标志时,该标志将被忽略



    返回值

    如果函数成功,返回值为非零;
    如果函数失败,返回值为零。在下列情况下函数将失败:
    窗口使用了窗口边界;窗口已经可见仍要显示窗口;窗口已经隐藏仍要隐藏窗口。若想获得更多错误信息,请调用GetLastError函数。


    实现原理根据 AnimateWindow 函数的介绍,我们可以知道只要设置 AnimateWindow 函数中的参数,就可以实现窗口的特效效果。
    其中,最后一个参数,则指定了使用哪种特效。
    例如,本文给出的示例:
    滚动效果,自下向上显示窗口:
    ::AnimateWindow(hWnd, 3000, AW_VER_NEGATIVE);
    滑动效果,淡出:
    ::AnimateWindow(hWnd, 3000, AW_HIDE | AW_BLEND);
    编码实现BOOL CALLBACK ProgMainDlg(HWND hWnd, UINT uiMsg, WPARAM wParam, LPARAM lParam){ if (WM_INITDIALOG == uiMsg) { // 滚动 ::AnimateWindow(hWnd, 3000, AW_VER_NEGATIVE); } else if (WM_CLOSE == uiMsg) { // 滑动 ::AnimateWindow(hWnd, 3000, AW_HIDE | AW_BLEND); // 关闭窗口 ::EndDialog(hWnd, NULL); } return FALSE;}
    程序测试直接运行程序,窗口弹出时淡入,关闭窗口时淡出。所以,测试成功。由于特效是动态的,所以从截图上可能看不出什么变化,但是还是放 2 张截图。


    总结AnimateWindow 函数的使用不是很复杂,大家只要对照着函数介绍去正确使用参数就可以实现想要实现的效果了。
    1 留言 2018-11-07 10:15:31 奖励3点积分
  • 判断操作系统及指定进程是32位还是64位

    背景应该很多人开发程序的时候,都会遇到这样的一个功能需求,怎么判断一个进程是32位进程还是64位进程呢?通常,我们的解决方法是,调用IsWow64Process函数进行判断。本文介绍的也是这种方法。
    但是,本文的这种方法考虑的更加全面。因为,大都数网上的方法都只是调用IsWow64Process函数来进行判断,这只考虑了64位操作系统,却没有考虑过程序如果运行在32位操作系统,那么刚才的判断方法是否会出错!
    本文就完善使用IsWow64Process函数判断指定进程是64位还是32位,完美支持32位操作系统和64位操作系统。现在,把程序的实现思路和实现方法写成文档,分享给大家。
    函数介绍OpenProcess 函数
    打开一个已存在的进程对象,并返回进程的句柄。
    函数声明
    HANDLE OpenProcess( DWORD dwDesiredAccess, //渴望得到的访问权限(标志) BOOL bInheritHandle, // 是否继承句柄 DWORD dwProcessId // 进程标示符);
    参数

    dwDesiredAccess [in]访问进程对象。 此访问权限将针对进程的安全描述符进行检查。 此参数可以是一个或多个进程访问权限。如果调用者启用了SeDebugPrivilege权限,则无论安全描述符的内容如何,都会授予所请求的访问权限。bInheritHandle [in]如果此值为TRUE,则此进程创建的进程将继承该句柄。 否则,进程不会继承此句柄。dwProcessId [in]要打开的本地进程的标识符。
    返回值

    如果函数成功,则返回值是指定进程的打开句柄。如果函数失败,返回值为NULL。要获取扩展错误信息,请调用GetLastError。

    GetCurrentProcess 函数
    获取当前程序进程的句柄。
    函数声明
    HANDLE WINAPI GetCurrentProcess(void);
    参数

    无参数
    返回值

    返回当前程序进程的句柄。

    IsWow64Process 函数
    判断指定的进程是否在WOW64下运行。
    函数声明
    BOOL WINAPI IsWow64Process( _In_ HANDLE hProcess, _Out_ PBOOL Wow64Process);
    参数

    hProcess [in]过程的句柄。 句柄必须具有PROCESS_QUERY_INFORMATION或PROCESS_QUERY_LIMITED_INFORMATION权限。 有关更多信息,请参阅流程安全和访问权限。Wow64Process [out]指向如果进程在WOW64下运行时设置为TRUE的值的指针。 如果进程在32位Windows下运行,则该值设置为FALSE。 如果进程是在64位Windows下运行的64位应用程序,则该值也设置为FALSE。
    返回值

    如果函数成功,则返回值为非零值。如果函数失败,返回值为零。 要获取扩展错误信息,请调用GetLastError。

    实现原理先简单介绍下什么是WOW64,WOW64 (Windows-on-Windows 64-bit)是一个Windows操作系统的子系统,它为现有的 32 位应用程序提供了 32 位的模拟,可以使大多数 32 位应用程序在无需修改的情况下运行在 Windows 64 位版本上。也就是说,WOW64就是一个 64 位操作系统模拟 32 位操作系统的一个子系统,而且32位程序都运行在它上面。
    那么,WIN32 API函数 IsWow64Process,就是判断指定程序是否运行在 WOW64 模拟系统中。而且,根据文档可知:




    OS 32bits
    OS 64BITS




    32bits process
    FALSE
    TRUE


    64bits process
    ERROR
    FALSE



    上面意思是说:

    使用IsWow64Process在32位操作系统上获取32位进程的返回结果是FALSE
    使用IsWow64Process在64位操作系统上获取32位进程的返回结果是TRUE
    使用IsWow64Process在32位操作系统上获取64位进程的返回结果是ERROR,因为64位程序不能在32位系统上运行
    使用IsWow64Process在64位操作系统上获取64位进程的返回结果是FALSE

    也就是说,如果我们要写一个判断进程位数的程序,还要能在32位系统和64位系统上运行,那么就必须先判断操作系统的位数。
    判断操作系统位数的原理是:
    利用上述的结果,对于32位的进程,在32位系统上,IsWow64Process返回FALSE,而64位系统返回TRUE。而且,我们程序本身就是32位,只要我们获取程序自身的进程句柄,然后传入IsWow64Process函数中判断是否在仿真环境中,根据返回结果可以知道32位系统还是64位系统。
    那么,判断全系统上,进程的位数的原理是:

    程序自身是32位进程。首先,先判断操作操作系统的位数。若是32位操作系统,那么它上面的所有进程都是32位的
    若是64位进程,那么就打开指定进程获取进程句柄。然后将进程句柄传递给IsWow64Process函数,进行判断。若是返回TRUE,则说明进程运行在WOW64模拟环境中,是一个32位进程;否则,是64位进程

    编码实现判断操作系统的位数BOOL Is64BitsOperateSystem(){ // 判断当前计算机操作系统是32位操作系统还是64位操作系统 BOOL bWow64Process = FALSE; // 获取当前程序进程句柄 HANDLE hProcess = ::GetCurrentProcess(); if (NULL == hProcess) { ShowError("OpenProcess"); return bWow64Process; } // 判断进程是否处于WOW64仿真环境中 ::IsWow64Process(hProcess, &bWow64Process); return bWow64Process;}
    判断指定进程的位数BOOL Is64BitsProcess(DWORD dwProcessId){ // 判断64位系统下, 进程指定是32位还是64位 // 先判断计算机操作系统位数, 32位系统只能执行32位进程 if (FALSE == Is64BitsOperateSystem) { return FALSE; } BOOL bWow64Process = FALSE; // 打开进程, 获取进程句柄 HANDLE hProcess = NULL; hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (NULL == hProcess) { ShowError("OpenProcess"); return bWow64Process; } // 判断进程是否处于WOW64仿真环境中 ::IsWow64Process(hProcess, &bWow64Process); return bWow64Process;}
    程序测试在 main 函数中调用上述封装好的函数进行测试,main 函数为:
    int _tmain(int argc, _TCHAR* argv[]){ // 11360 --> 32位进程 if (Is64BitsProcess(11360)) { printf("32 bits\n"); } else { printf("64 bits\n"); } // 7400 --> 64位进程 if (Is64BitsProcess(7400)) { printf("32 bits\n"); } else { printf("64 bits\n"); } system("pause"); return 0;}
    测试结果
    测试的例子中,进程PID为11360的是32位程序,PDI为7400的是64位程序。
    运行程序,程序判断正确。

    总结很多人使用IsWow64Process来判断进程的位数,往往会忽略考虑32位系统的情况,而造成程序在32位系统上运行出错。所以,这一点一定要考虑全面。
    参考参考自《Windows黑客编程技术详解》一书
    1 留言 2018-11-06 22:48:44 奖励5点积分
  • 获取指定进程的加载基址

    背景之前,自己写过一个进程内存分析的小程序,其中,就有一个功能是获取进程在内存中的加载基址。由于现在Windows系统引入了ASLR (Address Space Layout Randomization)机制,加载程序时候不再使用固定的基址加载。VS默认是开启基址随机化的,我们也可以设置它使用固定加载基址。至于什么是ASLR?或者ASLR有什么作用?本文就不深入探讨了,感兴趣的,可以自己私下了解了解。
    本文就是开发这样的一个小程序,使用两种方法来获取获取指定进程的加载基址。一种是使用进程模块快照方式,然后遍历加载模块并获取基址,另一种是直接调用WIN32 API函数EnumProcessModules,遍历加载模块基址。这两种方法本质上都是一样的。现在,我就把分析过程和实现方式写成文档,分享给大家。
    函数介绍CreateToolhelp32Snapshot 函数
    可以通过获取进程信息为指定的进程、进程使用的堆[HEAP]、模块[MODULE]、线程建立一个快照。
    函数声明
    HANDLE WINAPI CreateToolhelp32Snapshot( DWORD dwFlags, DWORD th32ProcessID );
    参数

    dwFlags指定快照中包含的系统内容,这个参数能够使用下列数值(常量)中的一个或多个:



    VALUE
    MEANING




    TH32CS_INHERIT
    声明快照句柄是可继承的


    TH32CS_SNAPALL
    在快照中包含系统中所有的进程和线程


    TH32CS_SNAPHEAPLIST
    在快照中包含在th32ProcessID中指定的进程的所有的堆


    TH32CS_SNAPMODULE
    在快照中包含在th32ProcessID中指定的进程的所有的模块


    TH32CS_SNAPPROCESS
    在快照中包含系统中所有的进程


    TH32CS_SNAPTHREAD
    在快照中包含系统中所有的线程




    th32ProcessID指定将要快照的进程ID。如果该参数为0表示快照当前进程。该参数只有在设置了TH32CS_SNAPHEAPLIST或者TH32CS_SNAPMODULE后才有效,在其他情况下该参数被忽略,所有的进程都会被快照。
    返回值

    调用成功,返回快照的句柄;调用失败,返回INVALID_HANDLE_VALUE 。

    Module32First 和 Module32Next 函数当我们利用函数CreateToolhelp32Snapshot()获得指定进程的快照后,我们可以利用Module32First函数来获得进程第一个模块的句柄,Module32Next函数来获得进程下一个模块的句柄。
    OpenProcess 函数
    打开一个已存在的进程对象,并返回进程的句柄。
    函数声明
    HANDLE OpenProcess( DWORD dwDesiredAccess, //渴望得到的访问权限(标志) BOOL bInheritHandle, // 是否继承句柄 DWORD dwProcessId// 进程标示符);
    参数

    dwDesiredAccess [in]访问进程对象。 此访问权限将针对进程的安全描述符进行检查。 此参数可以是一个或多个进程访问权限。如果调用者启用了SeDebugPrivilege权限,则无论安全描述符的内容如何,都会授予所请求的访问权限。bInheritHandle [in]如果此值为TRUE,则此进程创建的进程将继承该句柄。 否则,进程不会继承此句柄。dwProcessId [in]要打开的本地进程的标识符。
    返回值

    如果函数成功,则返回值是指定进程的打开句柄。如果函数失败,返回值为NULL。要获取扩展错误信息,请调用GetLastError。

    EnumProcessModules 函数
    在指定的进程中检索每个模块的句柄。要控制64位应用程序是否枚举32位模块,64位模块或两种类型的模块,请使用EnumProcessModulesEx函数。
    函数声明
    BOOL WINAPI EnumProcessModules( _In_ HANDLE hProcess, _Out_ HMODULE *lphModule, _In_ DWORD cb, _Out_ LPDWORD lpcbNeeded);
    参数

    hProcess [in]过程的句柄。lphModule [out]接收模块句柄列表的数组。cb [in]lphModule数组的大小,以字节为单位。lpcbNeeded [out]将所有模块句柄存储在lphModule数组中所需的字节数。
    返回值

    如果函数成功,返回值不为零。如果函数失败,返回值为零。 要获取扩展错误信息,请调用GetLastError。

    实现思路该程序实现进程基址的主要原理是,遍历进程里的所有加载的模块,那么,第一个加载模块的加载基址就是该进程的加载基址。
    那么,对于进程模块加载基址的遍历就有两种两种方法。一种是根据进程模块快照获取,另一种市直接调用EnumProcessModules函数获取。现在,对这两种方法的原理分别进行介绍。
    使用进程模块快照的方式获取模块基址的原理是:

    首先,使用CreateToolhelp32Snapshot 函数获取指定进程的所有模块快照。然后,根据模块快照,使用Module32First 和 Module32Next 函数进行遍历快照,并获取快照信息。其中,就包括有模块的加载基址信息。第一个模块的加载基址便是该进程的加载基址。最后,关闭上面获取的快照的句柄。
    使用EnumProcessModules函数获取模块基址的原理:

    首先,我们需要使用OpenProcess函数打开指定进程并获取进程的句柄
    然后,根据进程句柄调用EnumProcessModules函数获取进程加载的所有模块的加载基址,并保存在数组中。那么,第一个模块的加载基址便是该进程的加载基址
    关闭打开的进程句柄

    编程实现获取指定进程模块快照的方式遍历进程模块PVOID GetProcessImageBase1(DWORD dwProcessId){ PVOID pProcessImageBase = NULL; MODULEENTRY32 me32 = { 0 }; me32.dwSize = sizeof(MODULEENTRY32); // 获取指定进程全部模块的快照 HANDLE hModuleSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessId); if (INVALID_HANDLE_VALUE == hModuleSnap) { ShowError("CreateToolhelp32Snapshot"); return pProcessImageBase; } // 获取快照中第一条信息 BOOL bRet = ::Module32First(hModuleSnap, &me32); if (bRet) { // 获取加载基址 pProcessImageBase = (PVOID)me32.modBaseAddr; } // 关闭句柄 ::CloseHandle(hModuleSnap); return pProcessImageBase;}
    直接使用EnumProcessModules函数获取进程模块基址PVOID GetProcessImageBase2(DWORD dwProcessId){ PVOID pProcessImageBase = NULL; //打开进程, 获取进程句柄 HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (NULL == hProcess) { ShowError("OpenProcess"); return pProcessImageBase; } // 遍历进程模块, HMODULE hModule[100] = {0}; DWORD dwRet = 0; BOOL bRet = ::EnumProcessModules(hProcess, (HMODULE *)(hModule), sizeof(hModule), &dwRet); if (FALSE == bRet) { ::CloseHandle(hProcess); ShowError("EnumProcessModules"); return pProcessImageBase; } // 获取第一个模块加载基址 pProcessImageBase = hModule[0]; // 关闭句柄 ::CloseHandle(hProcess); return pProcessImageBase;}
    程序测试我们在 main 函数中调用上述封装的函数进行测试,main 函数为:
    int _tmain(int argc, _TCHAR* argv[]){ PVOID pProcessImageBase1 = NULL; PVOID pProcessImageBase2 = NULL; pProcessImageBase1 = GetProcessImageBase1(4500); pProcessImageBase2 = GetProcessImageBase2(4500); printf("pProcessImageBase1=0x%p\npProcessImageBase2=0x%p\n", pProcessImageBase1, pProcessImageBase2); system("pause"); return 0;}
    测试结果
    我们运行程序,程序执行成功,并显示两种方法获取的进程加载基址,而且获取结果都相同。

    然后,我们使用 Process Explorer 软件查看PID为 4500 的进程的加载基址,程序基址获取正确,程序测试成功。

    总结这两种方式,本质上都是一样的,只是遍历进程加载模块所使用的方法不相同。那么,进程加载的第一个模块的加载基址,便是进程的加载基址。
    参考参考自《Windows黑客编程技术详解》一书
    1 留言 2018-11-06 22:45:42 奖励5点积分
  • 编程实现遍历进程遍历线程遍历进程加载模块

    背景在我们开发程序的时候,经常需要在程序中实现遍历进程、遍历线程和遍历进程加载模块等获取系统信息的操作。
    本文是基于Win32 API函数实现这三个常用操作,当然,方法肯定不止有一种。比如,我写的《在VS2013中编程使用WMI》这篇文章中,使用的是WMI去遍历进程的。
    现在,我把这个小程序实现的思路以及实现过程,写成文档分享给大家。
    函数介绍CreateToolhelp32Snapshot 函数
    可以通过获取进程信息为指定的进程、进程使用的堆[HEAP]、模块[MODULE]、线程建立一个快照。
    函数声明
    HANDLE WINAPI CreateToolhelp32Snapshot( DWORD dwFlags, DWORD th32ProcessID );
    参数

    dwFlags指定快照中包含的系统内容,这个参数能够使用下列数值(常量)中的一个或多个:



    VALUE
    MEANING




    TH32CS_INHERIT
    声明快照句柄是可继承的


    TH32CS_SNAPALL
    在快照中包含系统中所有的进程和线程


    TH32CS_SNAPHEAPLIST
    在快照中包含在th32ProcessID中指定的进程的所有的堆


    TH32CS_SNAPMODULE
    在快照中包含在th32ProcessID中指定的进程的所有的模块


    TH32CS_SNAPPROCESS
    在快照中包含系统中所有的进程


    TH32CS_SNAPTHREAD
    在快照中包含系统中所有的线程




    th32ProcessID指定将要快照的进程ID。如果该参数为0表示快照当前进程。该参数只有在设置了TH32CS_SNAPHEAPLIST或者TH32CS_SNAPMODULE后才有效,在其他情况下该参数被忽略,所有的进程都会被快照。
    返回值

    调用成功,返回快照的句柄;调用失败,返回INVALID_HANDLE_VALUE 。

    Process32First 和 Process32Next 函数当我们利用函数CreateToolhelp32Snapshot()获得当前运行所有进程的快照后,我们可以利用Process32First函数来获得第一个进程的句柄,Process32Next函数来获得下一个进程的句柄。
    Thread32First 和 Thread32Next 函数当我们利用函数CreateToolhelp32Snapshot()获得当前运行所有线程的快照后,我们可以利用Thread32First函数来获得第一个线程的句柄,Thread32Next函数来获得下一个线程的句柄。
    Module32First 和 Module32Next 函数当我们利用函数CreateToolhelp32Snapshot()获得指定进程的快照后,我们可以利用Module32First函数来获得进程第一个模块的句柄,Module32Next函数来获得进程下一个模块的句柄。
    实现原理大家可以从上面的函数介绍可以看出,重点理解CreateToolhelp32Snapshot这个函数的操作。我们遍历进程、遍历线程以及遍历进程加载模块等3个操作,都是基于CreateToolhelp32Snapshot函数进行下一步实现的。
    对于遍历进程的实现原理是:

    首先,使用CreateToolhelp32Snapshot 函数获取所有进程的快照
    然后,根据进程快照,使用Process32First 和 Process32Next 函数进行遍历快照,并获取快照信息
    最后,关闭上面获取的快照的句柄

    对于遍历线程的实现原理是:

    首先,使用CreateToolhelp32Snapshot 函数获取所有线程的快照
    然后,根据线程快照,使用Thread32First 和 Thread32Next 函数进行遍历快照,并获取快照信息
    最后,关闭上面获取的快照的句柄

    对于遍历进程模块的实现原理是:

    首先,使用CreateToolhelp32Snapshot 函数获取指定进程的所有模块快照。
    然后,根据模块快照,使用Module32First 和 Module32Next 函数进行遍历快照,并获取快照信息
    最后,关闭上面获取的快照的句柄。

    程序实现遍历进程BOOL EnumProcess(){ PROCESSENTRY32 pe32 = { 0 }; pe32.dwSize = sizeof(PROCESSENTRY32); // 获取全部进程快照 HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (INVALID_HANDLE_VALUE == hProcessSnap) { ShowError("CreateToolhelp32Snapshot"); return FALSE; } // 获取快照中第一条信息 BOOL bRet = ::Process32First(hProcessSnap, &pe32); while (bRet) { // 显示 Process ID printf("[%d]\t", pe32.th32ProcessID); // 显示 进程名称 printf("[%s]\n", pe32.szExeFile); // 获取快照中下一条信息 bRet = ::Process32Next(hProcessSnap, &pe32); } // 关闭句柄 ::CloseHandle(hProcessSnap); return TRUE;}
    遍历线程BOOL EnumThread(){ THREADENTRY32 te32 = { 0 }; te32.dwSize = sizeof(THREADENTRY32); // 获取全部线程快照 HANDLE hThreadSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (INVALID_HANDLE_VALUE == hThreadSnap) { ShowError("CreateToolhelp32Snapshot"); return FALSE; } // 获取快照中第一条信息 BOOL bRet = ::Thread32First(hThreadSnap, &te32); while (bRet) { // 显示 Owner Process ID printf("[%d]\t", te32.th32OwnerProcessID); // 显示 Thread ID printf("[%d]\n", te32.th32ThreadID); // 获取快照中下一条信息 bRet = ::Thread32Next(hThreadSnap, &te32); } // 关闭句柄 ::CloseHandle(hThreadSnap); return TRUE;}
    遍历指定进程模块BOOL EnumProcessModule(DWORD dwProcessId){ MODULEENTRY32 me32 = { 0 }; me32.dwSize = sizeof(MODULEENTRY32); // 获取指定进程全部模块的快照 HANDLE hModuleSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessId); if (INVALID_HANDLE_VALUE == hModuleSnap) { ShowError("CreateToolhelp32Snapshot"); return FALSE; } // 获取快照中第一条信息 BOOL bRet = ::Module32First(hModuleSnap, &me32); while (bRet) { // 显示 Process ID printf("[%d]\t", me32.th32ProcessID); // 显示 模块加载基址 printf("[0x%p]\t", me32.modBaseAddr); // 显示 模块名称 printf("[%s]\n", me32.szModule); // 获取快照中下一条信息 bRet = ::Module32Next(hModuleSnap, &me32); } // 关闭句柄 ::CloseHandle(hModuleSnap); return TRUE;}
    程序测试在 main 函数中调用上述封装好的函数,进行测试。main 函数为:
    int _tmain(int argc, _TCHAR* argv[]){ // 遍历进程 if (FALSE == EnumProcess()) { printf("Enum Process Error!\n"); } system("pause"); system("cls"); // 遍历线程 if (FALSE == EnumThread()) { printf("Enum Thread Error!\n"); } system("pause"); system("cls"); // 遍历指定进程模块 if (FALSE == EnumProcessModule(6876)) { printf("Enum Process Module Error!\n"); } system("pause"); return 0;}
    测试结果:
    运行程序,首先成功显示系统上所有进程的信息:

    然后,成功显示系统上所有线程的信息:

    最后,成功显示指定进程“6876”的所有模块信息:

    总结这个小程序看似功能比较多,感觉比较复杂,实际上,它们三者的实现方式和思路都是相同的。你只要会一个,那么这三个应该都会了,这便是举一反三啊。
    其中,这个小程序重点理解CreateToolhelp32Snapshot 函数的参数就好,对于快照的遍历,根据你获取的快照信息不同,调用的快照遍历函数也不同。
    参考参考自《Windows黑客编程技术详解》一书
    1 留言 2018-11-06 22:23:53 奖励5点积分
  • URL分解之InternetCrackUrl函数

    背景近期使用WININET库写的一个数据上传、下载的客户端小程序,上传数据到网站服务器和从网站服务器下载数据到本地。由于,对WININET库部分用的比较少,所以,对这部分知识比较陌生。也遇到一些波折,其中一个就是URL的分解问题。
    起初,使用WININET库连接服务器并发送访问请求的时候,调用这两个API函数:InternetConnect 和 HttpOpenRequest,需要填写网站的信息。我知道URL是什么,但是需要从URL中手动提取信息,比较麻烦。
    后来了解到,原来WININET库专门提供了 InternetCrackUrl 函数来对URL进行分解,提取URL中的信息。现在,总结一下 InternetCrackUrl 的用法。
    函数介绍下面是 MSDN 2001 解释:
    Cracks a URL into its component parts.

    Syntax
    BOOL InternetCrackUrl( LPCTSTR lpszUrl, DWORD dwUrlLength, DWORD dwFlags, LPURL_COMPONENTS lpUrlComponents);
    Parameters

    lpszUrl
    [in] Pointer to a string that contains the canonical URL to crack.
    dwUrlLength
    [in] Unsigned long integer value that contains the length of the lpszUrl string in TCHAR, or zero if lpszUrl is an ASCIIZ string.
    dwFlags
    [in] Unsigned long integer value that contains the flags controlling the operation. This can be one of the following values:ICU_DECODEConverts encoded characters back to their normal form. This can be used only if the user provides buffers in the URL_COMPONENTS structure to copy the components into.ICU_ESCAPEConverts all escape sequences (%xx) to their corresponding characters. This can be used only if the user provides buffers in the URL_COMPONENTS structure to copy the components into.
    lpUrlComponents
    [in, out] Pointer to a URL_COMPONENTS structure that receives the URL components.

    Return Value
    Returns TRUE if the function succeeds, or FALSE otherwise. To get extended error information, call GetLastError.

    其中,URL_COMPONENTS 结构体的内容是:

    typedef struct { DWORD dwStructSize; LPTSTR lpszScheme; DWORD dwSchemeLength; INTERNET_SCHEME nScheme; LPTSTR lpszHostName; DWORD dwHostNameLength; INTERNET_PORT nPort; LPTSTR lpszUserName; DWORD dwUserNameLength; LPTSTR lpszPassword; DWORD dwPasswordLength; LPTSTR lpszUrlPath; DWORD dwUrlPathLength; LPTSTR lpszExtraInfo; DWORD dwExtraInfoLength;} URL_COMPONENTS, *LPURL_COMPONENTS;
    dwStructSize用于表明该结构体大小,一般我们都是传递sizeof(URL_COMPONENTS)。
    lpszSheme指向一段用于保存协议类型的内存空间。dwSchemeLength用于描述传入空间的大小(以TCHARS为单位的大小,下面其他空间大小描述字段都是以TCHARS单位)。
    lpHostName指向一段用于保存域名信息的内存空间。dwHostNameLength用于描述传入空间的大小。nPort用于接收端口号。lpszUserName和lpszPassword分别用于保存URL中携带的用户名和密码。lpszUrlPath指向保存URL的路径——不包含域名的一段内存空间。lpszExtraInfo指向保存URL中参数信息的一段内容空间。

    使用例子1. 导入WININET库#include <WinInet.h>#pragma comment(lib, "Wininet.lib")
    2. 调用 InternetCrackUrl 函数传入要分解的URL链接字符串,调用 InternetCrackUrl 函数分解,并输出分解结果。
    void CrackURL(char *pszURL){ URL_COMPONENTS uc = { 0 }; char szScheme[MAX_PATH] = { 0 }; char szHostName[MAX_PATH] = { 0 }; char szUserName[MAX_PATH] = { 0 }; char szPassword[MAX_PATH] = { 0 }; char szUrlPath[MAX_PATH] = { 0 }; char szExtraInfo[MAX_PATH] = { 0 }; // 初始化缓冲区 ::RtlZeroMemory(&uc, sizeof(uc)); ::RtlZeroMemory(szScheme, MAX_PATH); ::RtlZeroMemory(szHostName, MAX_PATH); ::RtlZeroMemory(szUserName, MAX_PATH); ::RtlZeroMemory(szPassword, MAX_PATH); ::RtlZeroMemory(szUrlPath, MAX_PATH); ::RtlZeroMemory(szExtraInfo, MAX_PATH); // 设置 URL_COMPONENTS 结构体数据 uc.dwStructSize = sizeof(uc); uc.lpszScheme = szScheme; uc.lpszHostName = szHostName; uc.lpszUserName = szUserName; uc.lpszPassword = szPassword; uc.lpszUrlPath = szUrlPath; uc.lpszExtraInfo = szExtraInfo; uc.dwSchemeLength = MAX_PATH; uc.dwHostNameLength = MAX_PATH; uc.dwUserNameLength = MAX_PATH; uc.dwPasswordLength = MAX_PATH; uc.dwUrlPathLength = MAX_PATH; uc.dwExtraInfoLength = MAX_PATH; // 调用函数 if (FALSE == ::InternetCrackUrl(pszURL, 0, 0, &uc)) { printf("Error[%d]\n", ::GetLastError()); } // 显示分解结果 printf("szScheme = %s\n", szScheme); printf("szHostName = %s\n", szHostName); printf("szUserName = %s\n", szUserName); printf("szPassword = %s\n", szPassword); printf("szUrlPath = %s\n", szUrlPath); printf("szExtraInfo = %s\n", szExtraInfo); printf("uc.nPort = %d\n", uc.nPort);}
    我们传入的URL为:http://www.demongan.com/upload.asp?file=520.zip 。主函数部分的代码为:
    int _tmain(int argc, _TCHAR* argv[]){ char szURL[] = "http://www.demongan.com/upload.asp?file=520.zip"; printf("URL: %s\n", szURL); CrackURL(szURL); printf("\n"); system("pause"); return 0;}
    3. 运行结果大家可以看到分解的结果显示,其中:
    协议类型szScheme为http;
    传输端口号uc.nPort为80;
    域名szHostName为 www.demongan.com ;
    用户名szUserName为空;
    用户密码szPassword为空;
    URL路径szUrlPath为/upload.asp;
    URL中的参数信息szExtraInfo为?file=520.zip。

    总结单单这个理解不是很难,主要是之前没有接触过这方面的知识,所以才会感到陌生,希望方便大家的参考吧。
    参考参考自《Windows黑客编程技术详解》一书
    1 留言 2018-11-06 22:06:48 奖励3点积分
显示 285 到 300 ,共 15 条
eject