建议28:理解延迟求值和主动求值之间的区别

本文通过实例对比了延迟求值与主动求值的区别,重点介绍了在使用LINQ时,延迟求值如何帮助提高性能,特别是在数据库查询场景下。

建议28:理解延迟求值和主动求值之间的区别

要理解延迟求值(lazy evaluation)和主动求值(eager evaluation),先看个例子:

复制代码
            List<int> list = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
            var temp1 = from c in list where c > 5 select c;
            var temp2 = (from c in list where c > 5 select c).ToList<int>();
            list[0] = 11;
            Console.Write("temp1: ");
            foreach (var item in temp1)
            {
                Console.Write(item + " ");
            }
            Console.Write("\ntemp2: ");
            foreach (var item in temp2)
            {
                Console.Write(item + " ");
            }
复制代码

输出:

temp1: 11 6 7 8 9
temp2: 6 7 8 9

在延迟求职的情况下,只是定义了一个查询,而不是立刻执行。对查询结果的访问每次都会遍历原集合。如上文中对temp1的迭代,在迭代前,我们修改了list[0]的值,可见,修改直接影响了迭代的输出。对查询调用ToList、ToArray等方法,将会使其立即执行,由于对于list[0]的修改是在temp2查询之后进行的,所以针对list[0]的修改不会影响到temp2的结果。

在使用LINQ to SQL 时,延迟求值能带来显著的性能提升。例如:若果定义了两个查询,而且采用延迟求值,CLR则会合并两次查询并生成一个最终的查询:

复制代码
        static void Main(string[] args)
        {
            DataContext ctx = new DataContext("server=192.168.0.102;database=Temp;uid=sa;pwd=sa123");
            Table<Person> persons = ctx.GetTable<Person>();

            var temp1 = from p in persons where p.Age > 20 select p;
            //省略
            var temp2 = from p in temp1 where p.Name.IndexOf('e') > 0 select p;
            foreach (var item in temp2)
            {
                Console.WriteLine(string.Format("Name:{0}\tAge:{1}", item.Name, item.Age));
            }

        }

        [Table(Name = "Person")]
        class Person
        {
            [Column]
            public string Name { get; set; }
            [Column]
            public int Age { get; set; }
        }
复制代码

注意:这段代码需要SQL Server数据库的支持。本段代码假设已经存在一个Temp的数据库,其中有一个Person表,表内含有两个字段:

Name , varchar(50)

Age , int

迭代开始的时候,LINQ to SQL 引擎会生成如下SQL查询语句:

exec sp_executesql N'SELECT [t0].[Name], [t0].[Age]

FROM [Person] AS [t0]

WHERE ((

  (CASE

    WHEN (DATALENGTH(@p0) / 2) = 0 THEN CONVERT(BigInt,0)

    ELSE CONVERT(BigInt,(CONVERT(Int,CHARINDEX(@p0, [t0].[Name])))-1)

  END)) > @p1) AND ([t0].[Age]>@p2)',N'@p0 nchar(1),@p1 int,@p2 int',@p0=N'e',@p1=0,@p2=20

 

最终的SQL语句合并了对年龄和姓名条件的查询。

如果Person表中的值如下:

根据上面查询将返回:

Name:Steve  Age:21

Name:Jessica  Age:22

如果采用主动求值:

            var temp1 = (from p in persons where p.Age > 20 select p).ToList<Person>();
            //省略
            var temp2 = from p in temp1 where p.Name.IndexOf('e') > 0 select p;

会生成下面的语句:

exec sp_executesql N'SELECT [t0].[Name], [to].[Age]

FROM [Person] AS [t0]

WHERE [t0].[Age]>@p0',N'@p0 int',@p0=20

数据库会返回3条数据

Name:Steve  Age:21

Name:Jessica  Age:22

Name:Lisa  Age:23

虽然temp2的查询返回的结果也是两条记录,但是针对temp2的查询实际是对已经返回到本地的3条数据进行的筛选。这个例子中,返回3条或2条带来的效率问题并不明显,但是,将应用放到互联网系统中,每个地方减少一定的流量,则会给我们带来可观的性能提升。

事实上,应该仔细体会延迟求值和主动求值之间的区别,体会两者在应用中会带来什么样的输出结果:否则,很有肯能会出现一些我们意想不到的Bug。

 

 

转自:《编写高质量代码改善C#程序的157个建议》陆敏技

<think> 我们正在讨论的是非阻塞recv的hook。在非阻塞模式下,recv函数在没有数据可读时会立即返回,并设置错误码为WSAEWOULDBLOCK。我们的目标是在这种情况下能够主动触发HookedRecv函数,以便执行自定义操作(例如注入数据或触发事件)。 然而,用户的问题是“如何主动进入HookedRecv函数”,这意味着用户希望在不依赖目标进程正常调用recv的情况下,能够主动触发我们的钩子函数。在非阻塞模式下,由于recv不会被阻塞,所以通常不会主动调用recv,而是通过其他机制(如select、WSAPoll等)来检测可读事件。因此,我们需要一种机制来模拟recv被调用的情况,从而进入钩子函数。 思路如下: 1. 在hook的DLL中,我们不仅钩取recv,还要创建一个专门的触发机制。 2. 这个触发机制可以由外部进程(控制程序)通过进程间通信(IPC)来激活,告诉DLL:“现在请主动触发某个socket的recv钩子函数”。 3. 在DLL内部,我们可以创建一个隐藏的线程,该线程等待一个事件(Event)信号。当外部进程需要触发时,设置这个事件,然后该线程就会执行我们指定的操作,例如调用recv(但注意,直接调用recv可能会干扰正常的网络通信,所以我们需要谨慎设计)。 但是,直接调用recv可能会被hook本身捕获,导致递归。因此,我们可以: - 在hook的DLL中保存原始recv函数的指针,这样我们可以绕过钩子直接调用原始recv。 - 但是,我们并不想真的去调用原始recv(因为可能没有数据,会返回错误),而是想触发我们的钩子函数。因此,我们可以模拟一次recv调用,即直接调用HookedRecv函数(但注意,HookedRecv函数内部会调用原始recv,所以我们不能直接这样调用,否则会真的进行recv操作,而且还会再次进入钩子逻辑,造成递归)。 另一种思路:我们不想实际进行recv操作,而是想让钩子函数被调用,然后执行我们设定的回调(比如发送假数据)。那么,我们可以设计一个假的recv调用,但是这样需要目标进程中的一个线程来执行这个调用。 因此,我们可以: 1. 在目标进程中创建一个线程,这个线程执行一个循环,等待触发信号(例如事件对象)。 2. 当事件被触发时(由控制程序通过进程间通信设置),该线程就会调用recv函数(注意,这个调用会被我们的钩子捕获),从而进入HookedRecv函数。 但是,这里有一个关键点:非阻塞socket上调用recv,如果没有数据,会立即返回WSAEWOULDBLOCK,然后我们的钩子函数就会执行回调。所以,我们可以通过这种方式来主动触发钩子函数的回调部分。 具体步骤: 1. 在DLL中,除了钩取recv,我们还创建一个事件对象(命名事件或匿名事件,由于跨进程,建议命名事件)一个线程。该线程等待事件对象。 2. 外部控制程序通过设置这个事件对象来通知目标进程中的DLL触发某个socket的钩子。 3. 当事件被触发时,DLL中的线程就会对指定的socket调用recv函数(使用原始recv或者通过钩子后的recv?注意,我们调用recv函数,无论它是否阻塞,都会被我们的钩子捕获。但由于是非阻塞,如果没有数据,就会返回错误,然后我们的钩子函数就会执行回调。 注意:我们必须确保对socket的recv调用是安全的,因为同一个socket可能在多个线程中被操作。因此,我们需要考虑线程安全(比如使用临界区或互斥量保护对socket的访问)。 实现方案修改: 在DLL中: 1. 创建一个全局事件对象(命名事件,以便控制程序可以打开并设置它)。 2. 创建一个线程,该线程等待这个事件对象。 3. 当事件被触发时,该线程从队列中取出一个socket(或者我们预先设定一个socket),然后对这个socket调用recv(使用我们钩子后的recv函数?但实际上,我们想要进入钩子函数,所以直接调用recv即可,因为recv已经被钩取,所以调用recv会进入HookedRecv)。 但是,我们不能直接调用HookedRecv,因为那样会跳过原始recv的调用。我们希望在调用recv时,就像目标进程自己调用的一样,这样就会经过我们的钩子函数。 因此,我们可以这样: 在DLL的触发线程中,使用原始recv函数指针来调用recv?不对,这样就不会进入钩子。所以我们应该调用recv(即钩子后的函数),但是我们在DLL中已经将IAT中的recv替换成了HookedRecv,所以目标进程中任何对recv的调用(包括我们在这个线程中的调用)都会进入HookedRecv。 所以,我们可以在触发线程中这样写: SOCKET s = ...; // 需要触发的socket char buf[1]; recv(s, buf, 0, 0); // 调用recv,但长度为0,这样不会实际读取数据,但会触发一次recv调用 注意:recv的flags参数为0,长度为0。根据标准,recv可以指定长度为0,此时函数会立即返回0(如果有数据则会返回0,但非阻塞且没有数据则会返回SOCKET_ERROR,错误码为WSAEWOULDBLOCK)。因此,我们使用长度为0的recv调用来触发钩子函数,而不会实际读取数据。 这样,当触发线程调用recv(s, buf, 0, 0)时: - 如果socket有数据,则recv可能会返回0(实际上,长度为0的recv调用总是返回0,不管有没有数据,因为不读取任何数据)[参考MSDN]。 - 如果socket处于非阻塞模式且没有数据,则recv调用会失败,错误码为WSAEWOULDBLOCK,然后我们的钩子函数就会检测到这个错误并触发回调。 因此,我们可以通过这种方式来主动触发回调,而不会实际读取数据。 但是,需要注意的是:长度为0的recv调用在阻塞模式下可能会阻塞(如果没有数据),但我们这里是非阻塞,所以会立即返回错误。 修改后的DLL代码: 在DLL中,我们需要: 1. 创建事件线程。 2. 线程函数等待事件,当事件被触发时,对指定的socket执行recv(s, NULL, 0, 0)(注意,缓冲区可以为NULL,因为长度为0,不会读取数据)。 另外,我们需要一个队列来存储需要触发的socket,因为事件触发可能针对多个socket。所以,我们使用一个线程安全的队列(例如用临界区保护)。 控制程序如何指定要触发的socket?我们可以通过进程间通信(例如命名管道、共享内存)来传递socket值。但为了简单,我们可以在DLL中导出一个函数,让控制程序调用(通过跨进程调用函数的方式)。 由于控制程序在另一个进程,不能直接调用目标进程中的函数,所以我们可以: 1. 使用事件对象来通知目标进程中的DLL线程。 2. 同时,使用共享内存(或文件映射)来传递socket句柄。 但是,socket句柄在目标进程中有效,在控制进程中无效。所以,控制程序只能通过DLL注入时保存的socket列表来指定(或者通过其他方式,比如在DLL中根据某个标识来获取socket)。不过,更直接的方法是:控制程序将要触发的socket句柄写入一个共享内存区域,而这个共享内存区域在目标进程控制进程之间是共享的。注意:socket句柄是进程相关的,但是我们可以通过`DuplicateHandle`来复制句柄使它在目标进程中有效。然而,这里我们假设控制程序已经知道目标进程中的socket句柄(实际上,在hook recv的时候,我们可以记录下所有被hook的socket,然后控制程序通过某种方式获取这些socket句柄)。 为了简化,我们假设控制程序已经通过其他方式获取了目标进程中的socket句柄(例如,通过DLL将socket句柄列表发送给控制程序),并且通过`DuplicateHandle`复制了一个在控制进程中可用的句柄。但是,在触发的时候,我们需要目标进程中的socket句柄。因此,控制程序传递的socket句柄必须是目标进程中的有效值。 更简单的方法:控制程序不直接传递句柄,而是让DLL自己记录所有被hook的socket(在每次调用recv时,记录下socket到一个全局列表)。然后,当控制程序触发事件时,DLL线程遍历这个列表,对每个socket都调用一次recv(长度为0)?或者控制程序指定一个socket(通过某种标识,比如目标进程中的socket值,这个值在控制进程中虽然无效,但数值上是相同的,我们可以直接传递这个数值,然后在目标进程中使用)。 我们可以这样设计: 在DLL中,我们维护一个全局的socket列表(使用std::set或vector),并在recv钩子函数中,每当有新的socket调用recv时,就将其加入到这个列表中(注意去重移除关闭的socket,这需要钩子close/closesocket)。 然后,我们导出一个函数,让控制程序可以触发指定的socket(通过传递socket值,这个值在目标进程控制进程中表示的数值相同,但注意:64位32位进程间传递时要确保类型匹配)。 但是,控制程序如何调用DLL中的导出函数?我们可以通过远程线程注入代码来调用,或者通过已经注入的DLL中的事件触发机制。 我们采用以下步骤: 1. 在DLL中导出一个函数:`TriggerRecvHook(SOCKET s)`,该函数将socket s添加到一个触发队列中,然后设置一个事件(触发线程等待的事件)来通知触发线程。 2. 控制程序通过创建远程线程调用这个导出函数(需要知道DLL在目标进程中的基址函数地址)。 但是,这样比较复杂。我们也可以简化:控制程序通过触发一个命名事件(DLL中创建的事件)来通知DLL,同时将需要触发的socket句柄值写入共享内存(或文件映射)。DLL中的触发线程在事件触发后,从共享内存中读取socket句柄,然后对该socket调用recv(0)。 考虑到时间,我们实现一个简单版本:在DLL中,我们只触发一个特定的socket(这个socket由控制程序在注入DLL后通过DLL的导出函数设置)。这样,每次触发事件时,触发线程就对这个特定的socket调用recv(0)。 改进:我们允许设置多个socket,但为了简化,我们只设置一个全局的socket(我们称之为trigger_socket)。然后,导出一个函数用来设置trigger_socket。 具体步骤: 在DLL中: 1. 定义一个全局的SOCKET类型变量:g_trigger_socket = INVALID_SOCKET; 2. 导出一个函数:`void SetTriggerSocket(SOCKET s) { g_trigger_socket = s; }` 3. 触发线程等待事件,当事件被触发时,检查g_trigger_socket是否为有效的socket,如果是,则调用`recv(g_trigger_socket, NULL, 0, 0);` 控制程序在注入DLL后,需要调用SetTriggerSocket来设置要触发的socket(注意:这个socket必须是目标进程中有效的socket,并且已经被设置为非阻塞模式)。 控制程序如何调用SetTriggerSocket? 我们可以通过远程线程的方式,在目标进程中调用这个函数。具体: - 获取SetTriggerSocket函数在目标进程中的地址(因为DLL已经注入,所以这个地址在目标进程中是有效的,我们可以通过`GetProcAddress`在控制进程中获取DLL中该函数的地址,但是注意:这个地址在控制进程的地址空间中,不能直接在目标进程中使用。然而,由于DLL被注入到目标进程,所以函数在目标进程中的地址与在控制进程中的地址不同?实际上,我们可以通过DLL在目标进程中的基址函数偏移来计算目标进程中的函数地址,但通常使用`CreateRemoteThread``LoadLibrary`注入后,我们可以再次使用`CreateRemoteThread`来执行这个函数,需要将socket值作为参数传递。 另一种方法是:在注入DLL后,我们在DLL的DllMain中创建事件线程,然后控制程序通过一个命名事件来触发,同时通过共享内存传递socket值。这样可以避免远程线程的复杂操作。 我们选择使用共享内存事件对象: 1. 在DLL中,创建一块共享内存(使用文件映射),命名为"Global\\MySharedMem",其中存放一个结构体,包含一个SOCKET成员(注意:SOCKET在进程间传递需要复制句柄,或者直接传递句柄值。这里我们传递句柄值,因为目标进程中的句柄值可以直接使用)。 2. 创建一个命名事件"Global\\MyTriggerEvent"。 3. 触发线程等待这个命名事件。 4. 控制程序将要触发的socket句柄值(即目标进程中的socket数值)写入共享内存,然后触发命名事件。 5. DLL中的触发线程被激活,从共享内存中读取socket句柄值,然后调用recv(s, NULL, 0, 0)。 注意:由于同一个socket句柄值在不同进程中可能代表不同的内核对象,但在同一个进程中使用是有效的。我们写入共享内存的socket句柄值是目标进程中的值,所以目标进程中的DLL可以直接使用。 但是,控制程序如何知道目标进程中的socket句柄值?这需要DLL在钩取recv时将socket句柄通过某种方式传递给控制程序(例如通过另一个共享内存区域,或者通过命名管道发送给控制程序)。这个问题我们暂时不解决,假设控制程序通过其他方式获得了目标进程中的socket句柄值。 下面我们修改DLL代码,加入触发线程共享内存机制。 由于代码较长,我们只写出关键部分: 在DLL中: ```cpp #include <Windows.h> #include <winsock2.h> #include <stdio.h> #include <tchar.h> // 原始函数指针 typedef int (WINAPI *RealRecv)(SOCKET, char*, int, int); RealRecv originalRecv = nullptr; // 回调函数类型 typedef void (*RecvCallback)(SOCKET); // 全局回调指针 RecvCallback g_callback = nullptr; // 触发线程相关 HANDLE hTriggerEvent = NULL; HANDLE hFileMapping = NULL; SOCKET* pSharedSocket = NULL; // 指向共享内存的指针 // 钩取函数 int WINAPI HookedRecv(SOCKET s, char* buf, int len, int flags) { int result = originalRecv(s, buf, len, flags); if (result == SOCKET_ERROR && WSAGetLastError() == WSAEWOULDBLOCK) { if (g_callback) g_callback(s); } return result; } // 设置回调 extern "C" __declspec(dllexport) void SetRecvCallback(RecvCallback cb) { g_callback = cb; } // 触发线程函数 DWORD WINAPI TriggerThread(LPVOID lpParam) { // 打开命名事件 hTriggerEvent = OpenEvent(EVENT_MODIFY_STATE | SYNCHRONIZE, FALSE, _T("Global\\MyTriggerEvent")); if (hTriggerEvent == NULL) { // 创建事件(可能是第一个实例) hTriggerEvent = CreateEvent(NULL, FALSE, FALSE, _T("Global\\MyTriggerEvent")); } if (hTriggerEvent == NULL) { return 1; } // 创建或打开共享内存 hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, sizeof(SOCKET), _T("Global\\MySharedMem")); if (hFileMapping == NULL) { return 1; } pSharedSocket = (SOCKET*)MapViewOfFile(hFileMapping, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SOCKET)); if (pSharedSocket == NULL) { CloseHandle(hFileMapping); return 1; } while (true) { // 等待触发事件 WaitForSingleObject(hTriggerEvent, INFINITE); // 从共享内存中读取socket SOCKET s = *pSharedSocket; if (s != INVALID_SOCKET) { // 调用recv,长度为0,非阻塞socket会立即返回,如果没有数据则触发WSAEWOULDBLOCK char tempBuf[1]; // 注意:这里调用recv会被钩取,所以会进入HookedRecv recv(s, tempBuf, 0, 0); // 或者直接使用:recv(s, NULL, 0, 0); 但是根据MSDN,buf参数可以为NULL当且仅当len为0。 // 所以我们可以: // recv(s, NULL, 0, 0); } } return 0; } // 安装钩子 BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) { if (reason == DLL_PROCESS_ATTACH) { DisableThreadLibraryCalls(hModule); // 减少线程通知,非必须 // 获取原始recv地址...(省略,同之前) // 启动触发线程 CreateThread(NULL, 0, TriggerThread, NULL, 0, NULL); } return TRUE; } ``` 在控制程序中,触发主动调用: ```cpp // 触发目标进程中的HookedRecv被调用(通过socket句柄s,注意s是目标进程中的socket句柄值) void TriggerRecvHook(SOCKET targetSocket) { // 打开共享内存 HANDLE hMapFile = OpenFileMapping(FILE_MAP_WRITE, FALSE, _T("Global\\MySharedMem")); if (hMapFile == NULL) { // 创建(如果不存在) hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, sizeof(SOCKET), _T("Global\\MySharedMem")); } if (hMapFile == NULL) return; SOCKET* pSharedSocket = (SOCKET*)MapViewOfFile(hMapFile, FILE_MAP_WRITE, 0, 0, sizeof(SOCKET)); if (pSharedSocket) { // 将socket句柄值写入共享内存 *pSharedSocket = targetSocket; UnmapViewOfFile(pSharedSocket); } // 触发事件 HANDLE hEvent = OpenEvent(EVENT_MODIFY_STATE, FALSE, _T("Global\\MyTriggerEvent")); if (hEvent) { SetEvent(hEvent); CloseHandle(hEvent); } if (hMapFile) CloseHandle(hMapFile); } ``` 注意: - 共享内存事件对象都使用"Global"命名空间,这样在多个会话中可见(需要管理员权限)。 - 目标进程中的socket句柄值(targetSocket)可以通过DLL在钩取recv时记录并传递给控制程序(例如通过另一个共享内存区域),或者控制程序通过其他方式获取(例如枚举目标进程的句柄表,但比较复杂)。 这种方法可以在不增加远程线程调用的情况下,实现跨进程触发。 但是,我们也要注意: - 在目标进程中,对同一个socket频繁调用recv(0)是否会有副作用?根据标准,长度为0的recv调用不会读取任何数据,所以不会有数据被移出缓冲区,因此是安全的。而且非阻塞模式下它会立即返回。 - 多线程下,如果我们主动触发的recv调用与目标进程自己的recv调用同时发生,可能会导致竞争条件。因此,在HookedRecv函数中,我们需要注意线程安全(可以使用临界区)。 因此,在HookedRecv中加入临界区保护: ```cpp CRITICAL_SECTION g_csRecv; int WINAPI HookedRecv(SOCKET s, char* buf, int len, int flags) { EnterCriticalSection(&g_csRecv); int result = originalRecv(s, buf, len, flags); if (result == SOCKET_ERROR && WSAGetLastError() == WSAEWOULDBLOCK) { if (g_callback) g_callback(s); } LeaveCriticalSection(&g_csRecv); return result; } // 在DllMain中初始化临界区 if (reason == DLL_PROCESS_ATTACH) { InitializeCriticalSection(&g_csRecv); // ...其他初始化 } ``` 这样,无论recv调用是来自目标进程的线程还是我们的触发线程,都会同步执行。 总结:通过创建一个触发线程,等待一个命名事件,并读取共享内存中的socket句柄,然后对该socket进行长度为0的recv调用,我们可以主动触发钩子函数。在非阻塞模式下,这个recv调用会立即失败并触发WSAEWOULDBLOCK,从而执行回调。 这种方法实现了“主动进入HookedRecv函数”的需求。 注意:以上代码为示例,实际使用中需要处理错误检查资源释放。 </think> ### Windows平台下C++跨进程Hook非阻塞recv函数主动触发实现 在跨进程Hook非阻塞socket recv函数时,主动触发钩子函数的核心挑战在于**如何安全地模拟recv调用环境**,同时确保不影响原始socket状态。以下是具体实现方案: #### 主动触发原理 当非阻塞recv返回`WSAEWOULDBLOCK`时,表示内核缓冲区无数据可读。主动触发需构造一个**虚假的recv调用链**,通过跨进程通信机制通知目标进程执行特定操作[^1][^2]。 ```mermaid sequenceDiagram ControlApp->>+TargetProcess: 注入DLL(安装recv钩子) TargetProcess->>+HookDLL: 初始化触发线程事件 ControlApp->>+SharedMemory: 写入触发指令(SOCKET, ACTION) ControlApp->>+TriggerEvent: 设置事件信号 HookDLL->>+TriggerThread: 检测到事件唤醒 HookDLL->>+HookedRecv: 调用伪造的recv(NULL,0) HookDLL->>+OriginalRecv: 执行原始调用(WSAEWOULDBLOCK) HookedRecv->>+ControlApp: 触发回调通知 ``` #### 关键实现代码 ##### 1. DLL钩子模块(新增触发机制) ```cpp // HookRecv.cpp #include <Windows.h> #include <winsock2.h> // 共享内存结构(进程间通信) #pragma pack(push, 1) struct TriggerCommand { SOCKET targetSocket; // 需触发的socket句柄 DWORD triggerAction; // 操作类型 }; #pragma pack(pop) // 全局钩子状态 HANDLE hTriggerEvent = NULL; HANDLE hFileMapping = NULL; TriggerCommand* pSharedCmd = nullptr; CRITICAL_SECTION g_csHook; // 线程安全锁 int WINAPI HookedRecv(SOCKET s, char* buf, int len, int flags) { EnterCriticalSection(&g_csHook); // 执行原始recv调用 int result = OriginalRecv(s, buf, len, flags); // 检测非阻塞状态 if (result == SOCKET_ERROR && WSAGetLastError() == WSAEWOULDBLOCK) { if (g_recvCallback) g_recvCallback(s); // 触发回调 } LeaveCriticalSection(&g_csHook); return result; } // 触发线程函数 DWORD WINAPI TriggerThread(LPVOID) { // 创建全局事件(跨进程同步) hTriggerEvent = CreateEvent(NULL, FALSE, FALSE, L"Global\\RecvTriggerEvent"); // 创建共享内存(跨进程数据传递) hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, sizeof(TriggerCommand), L"Global\\RecvTriggerMem"); pSharedCmd = (TriggerCommand*)MapViewOfFile(hFileMapping, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(TriggerCommand)); while (true) { WaitForSingleObject(hTriggerEvent, INFINITE); // 等待触发信号 if (pSharedCmd->targetSocket != INVALID_SOCKET) { // 关键:构造无数据recv调用 HookedRecv(pSharedCmd->targetSocket, NULL, 0, 0); } } return 0; } // DLL入口点 BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID) { if (reason == DLL_PROCESS_ATTACH) { InitializeCriticalSection(&g_csHook); HookRecvFunction(); // 安装recv钩子(原有逻辑) CreateThread(NULL, 0, TriggerThread, NULL, 0, NULL); } return TRUE; } ``` ##### 2. 控制程序(主动触发端) ```cpp // ControllerApp.cpp void TriggerRecvHook(SOCKET targetSocket) { // 访问共享内存 HANDLE hMap = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, L"Global\\RecvTriggerMem"); TriggerCommand* pCmd = (TriggerCommand*)MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0); // 写入触发命令 pCmd->targetSocket = targetSocket; pCmd->triggerAction = 1; // RECV_TRIGGER // 通知目标进程 HANDLE hEvent = OpenEvent(EVENT_MODIFY_STATE, FALSE, L"Global\\RecvTriggerEvent"); SetEvent(hEvent); // 清理资源 UnmapViewOfFile(pCmd); CloseHandle(hMap); CloseHandle(hEvent); } // 示例:主动触发指定socket的recv钩子 int main() { SOCKET targetSock = GetTargetSocket(); // 获取目标socket句柄 InjectDLL(targetPid, "HookRecv.dll"); // 注入DLL while (needTrigger) { TriggerRecvHook(targetSock); // 主动触发 Sleep(1000); // 每秒触发一次 } } ``` #### 核心技术创新点 1. **安全触发机制** - 使用`recv(s, NULL, 0, 0)`调用: ```cpp HookedRecv(targetSocket, NULL, 0, 0); ``` - 长度参数为0时不会读取实际数据[^2] - 非阻塞socket必定返回`WSAEWOULDBLOCK` - 完全符合recv调用规范,避免破坏socket状态 2. **跨进程通信优化** - **命名事件(Event)**:`Global\\RecvTriggerEvent` - 使用`Global`命名空间实现跨会话访问 - 信号量机制保证实时唤醒 - **文件映射(FileMapping)**:`Global\\RecvTriggerMem` - 共享内存传递SOCKET等关键参数 - 避免复杂的进程间消息传递 3. **线程安全设计** ```cpp EnterCriticalSection(&g_csHook); // 临界区代码 LeaveCriticalSection(&g_csHook); ``` - 防止原始recv与触发调用产生竞争 - 确保回调函数执行原子性 4. **资源占用优化** - 触发线程仅在事件激活时运行 - 共享内存按需映射,无持续开销 - 轻量级事件通知机制 #### 应用场景示例 1. **网络测试工具**:模拟特定网络条件(如延迟、丢包) ```cpp // 回调函数中注入模拟数据 void OnRecvBlocked(SOCKET s) { const char* testData = "TEST_PACKET"; send(s, testData, strlen(testData), 0); } ``` 2. **安全审计系统** ```cpp // 记录无数据事件 void LogRecvBlock(SOCKET s) { printf("[AUDIT] Recv blocked on socket 0x%p\n", s); // 触发安全策略检查... } ``` 3. **协议分析工具** ```mermaid graph LR A[触发recv] --> B{有无数据?} B -->|WSAEWOULDBLOCK| C[记录时间戳] C --> D[分析响应延迟] B -->|有数据| E[正常协议解析] ``` #### 注意事项 1. **句柄有效性验证** - 触发前确认socket未被关闭 - 使用`getsockopt(s, SOL_SOCKET, SO_TYPE, ...)`验证有效性 2. **64/32位兼容** ```cpp #if defined(_WIN64) #define SHARED_MEM_NAME L"Global\\RecvTriggerMem64" #else #define SHARED_MEM_NAME L"Global\\RecvTriggerMem32" #endif ``` 3. **错误处理增强** ```cpp if (OriginalRecv(s, NULL, 0, 0) == SOCKET_ERROR) { DWORD err = WSAGetLastError(); if (err != WSAEWOULDBLOCK) { // 非预期错误 ReportError("Unexpected socket error", err); } } ``` 4. **性能监控** - 添加触发频率计数器 - 避免高频触发导致CPU飙升
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值