news 2026/10/31 3:30:43

LSP注入原理:Winsock协议链与WSPStartup机制解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LSP注入原理:Winsock协议链与WSPStartup机制解析

简介:围绕 LSP 注入原理与代码实现展开的实战型资料,面向具备 C++/COM 基础、希望理解 Windows 底层钩子与组件注册机制的安全研究者和开发人员。内容从 LSP 组件创建、注册表注册、运行时注入到请求拦截处理逐层拆解,并给出 IDispatch、IUnknown、ILanguageService 等关键接口及注入代码的工程化示例,可用于学习文件操作监控、调试增强等场景,同时提醒合法合规使用。压缩包共 38 个文件,约 150KB,以 15 个 cpp 源文件和 7 个 h 头文件为核心,辅以 vcxproj、sln、makefile 等项目配置,以及 bat、dll、exe、def 等编译与注册相关文件,目录结构便于对照阅读和二次实践。已有 1582 人学习,适合按模块梳理实现脉络、理解 COM 组件与系统交互流程。

1. LSP 注入:为什么一个 DLL 不改任何进程内存,就能看到全机器的连接

“LSP 注入”这个名字很容易让人联想到写内存、装钩子、打补丁那一套,但它的真实做法要简单得多,几乎不动目标进程:往系统的一张协议目录里登记一个条目,然后等。此后任何进程,包括你没有权限打开的那些,只要调用一次socket(),ws2_32 就会顺着这张目录找到你登记的 DLL,把它加载进这个进程,再调用它导出的唯一入口WSPStartup。没有远程线程,没有驱动,甚至不需要知道对方进程的 PID。

值得花时间理解原因:它是一条“用户态、跨进程、免驱动”的流量观测路径,也是 Winsock 2 可扩展设计中最完整的一次演练。理解了这条链怎么被拼起来、WSPStartup怎么把控制权交回来,你就能看懂为什么早年那些个人防火墙不上驱动也能列出每个进程的连接,也就能明白这条路后来为什么被系统主动关掉。

2. 协议目录与协议链:socket() 到底是怎么找到你的 DLL 的

2.1 一次 socket() 调用其实是一次目录查询

很多人以为socket()是直接进内核的,实际上它在用户态要先过一道“查表”。ws2_32 维护着一张协议目录(protocol catalog),每一项都是一个WSAPROTOCOL_INFOW结构体,记录着这个提供者支持什么地址族、什么套接字类型、什么协议号,以及最关键的ProviderId和 DLL 路径。socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)进来之后,ws2_32 从目录头开始线性扫描,找到第一个三元组匹配的项,加载对应的 DLL,调WSPStartup,把这 30 个函数指针填进一张表。

这张表是进程私有的,加载的 DLL 也来自进程私有空间,但目录本身是系统级共享的,存在注册表的 HKLM 下。这个“目录全局、DLL 私有”的错位,正是 LSP 注入能成立的全部基础:你改一次全局目录,全机器每个进程在下次创建 socket 时都会各自加载一份你的代码。

WSAPROTOCOL_INFOW里字段很多,真正决定行为的是下面这几个:

字段作用LSP 需要关心的点
ProviderId提供者 GUID安装时自己生成并固定,卸载时靠它找回条目
dwCatalogEntryId目录序号由系统分配,安装时不要手填
ProtocolChain链结构ChainLen和ChainEntries决定分层关系
dwServiceFlags1能力位图XP1_IFS_HANDLES、XP1_ASYNC等会影响行为
iAddressFamily/iSocketType/iProtocol匹配三元组必须与 base 提供者完全一致,否则匹配不到
szProtocol显示名只影响netsh之类工具的输出现实,不参与匹配

匹配过程完全靠三元组和顺序,不靠名字。这意味着你可以把szProtocol写成一个看起来很正规的名字(比如伪装成系统自带的传输项),目录扫描照样会选中你——名字在这里只起展示作用。

提示:目录扫描是线性的、从头往后的。这一条决定了一个事实:把条目装进去不等于生效;新装的条目默认排在末尾,永远轮不到它。

2.2 ChainLen 与 ChainEntries:链里到底存了谁

ProtocolChain是整个机制的核心。它是一个长度为MAX_PROTOCOL_CHAIN(值为 7)的数组加一个长度字段:

typedef struct _WSAPROTOCOLCHAIN { int ChainLen; // 链的有效长度 DWORD ChainEntries[MAX_PROTOCOL_CHAIN]; // 目录序号数组 } WSAPROTOCOLCHAIN;

规则可以概括成三条。ChainLen == 1表示这是 base 提供者,自己就是干活的,ChainEntries[0]就是它自己的目录号。ChainLen >= 2表示这是分层提供者,ChainEntries[0]固定为 0 作占位,ChainEntries[1]往下依次是“下一层”的目录号,最后一项是 base 的目录号。

注意一个容易搞错的细节:这个数组里不含自己。你这一层的身份由结构体自己的dwCatalogEntryId表示,链里描述的只是“从我往下走,依次会遇到谁”。所以一个装了两层的目录项,ChainLen是 3,ChainEntries是{0, 中间层目录号, base 目录号},而你自己的目录号在dwCatalogEntryId里,不出现在数组中。

MAX_PROTOCOL_CHAIN是 7,扣掉ChainEntries[0]的占位和末尾那个 base,实际能叠的分层提供者最多 5 层。这个上限不是随便定的:每多一层就多一次函数调用跳转和一次参数搬运,链越长,网络路径上每一次收发付出的固定开销越大,链本身出问题的概率也越高。

2.3 目录存在哪:注册表二进制块与 WSC 系列函数

物理位置在 HKLM 下的 Winsock2 参数区,Catalog_Entries下面按序号排列的一堆二进制值,64 位系统另有Catalog_Entries64。每一项就是一坨WSAPROTOCOL_INFOW的直接内存映射。看到这里容易产生“那我直接改注册表”的想法,这是一个代价很高的做法:这个结构体的布局在不同版本、不同位数下并不一致,里面含有与实际宽度相关的字段,手写偏移一旦算错,整个目录项作废,结果是全机器的网络栈一起异常。

正确入口是一组WSC前缀的函数,它们本质上也是对这张表的受控封装:

// 枚举当前视图下的全部目录项。先传 NULL 拿所需缓冲区大小 INT WSCEnumProtocols(LPINT lpiProtocols, LPWSAPROTOCOL_INFOW lpProtocolBuffer, LPDWORD lpdwBufferLength, LPINT lpErrno); // 安装一个提供者条目(需要管理员权限) INT WSCInstallProvider(const LPGUID lpProviderId, const LPWSTR lpszProviderDllPath, const LPWSAPROTOCOL_INFOW lpProtocolInfoList, DWORD dwNumberOfEntries, LPINT lpErrno); // 重写目录顺序,这一步才让新条目真正被优先选中 INT WSCWriteProviderOrder(LPDWORD lpwdCatalogEntryId, DWORD dwNumberOfEntries); // 卸载,按 ProviderId 匹配 INT WSCDeinstallProvider(LPGUID lpProviderId, LPINT lpErrno);

WSCEnumProtocols第一次调用时把lpdwBufferLength传入一个值、缓冲区传NULL,它返回SOCKET_ERROR并把所需长度写回lpdwBufferLength,错误码是WSAENOBUFS。这是标准的“先问大小再分配”套路,能避免目录在你计算大小时被别的安装程序改动导致缓冲区不够。

还有一个非常关键、几乎所有人第一次都会踩的点:WSCEnumProtocols只枚举当前进程位数视图下的目录。32 位安装程序看到的是 32 位那份,64 位安装程序看到的是 64 位那份。所以“双份安装”不是可选项,是必需的。

3. WSPStartup:唯一入口里的五个参数和一张回调表

3.1 五个参数分别承担什么责任

LSP 的 DLL 可以被导出很多函数,但 ws2_32 只认一个名字:WSPStartup。签名固定,少一个修饰都不行:

BOOL WINAPI WSPStartup( WORD wVersionRequested, // 调用方期望的 SPI 版本,通常 0x0002 LPWSPDATA lpWSPData, // 出参:回填本提供者的版本与能力描述 LPWSAPROTOCOL_INFOW lpProtocolInfo, // 入参:当前这一层的完整目录项快照 WSPUPCALLTABLE UpcallTable, // 入参:ws2_32 交给你的回调表 LPWSPPROC_TABLE lpProcTable); // 出参:你要填满的函数指针表

lpWSPData是自我介绍,主要填wVersion和wHighVersion,szDescription和szProviderName只是给人看的字符串,不需要和注册时一致。lpProtocolInfo是信息量最大的一个,它是当前这一层目录项的副本,里面带着整条协议链,你从这里能知道自己下面还有谁。UpcallTable是 ws2_32 反向提供给你的工具集,下一节细说。lpProcTable是你要交的作业——一张 30 个函数指针的表,ws2_32 拿到之后就把它当作这个 socket 的“实现”。

返回值语义很直白:返回 0 或TRUE表示初始化成功,返回非零错误码表示这一层不可用。返回错误的后果比想象中重:ws2_32 不会默默跳过你继续往下找,创建 socket 的调用会直接失败,错误码WSAEPROVIDERFAILEDINIT。

3.2 UpcallTable:为什么在 LSP 里直接调 socket() 会炸

写转发代码时最自然的想法是:我在WSPConnect里想记一条日志,那就调一下getpeername()呗。这一调就完了。getpeername()是 ws2_32 的导出函数,ws2_32 拿到这个调用之后,会按当前 socket 绑定的提供者表去分派——也就是分派回你自己的WSPGetPeerName,然后你的实现又调getpeername(),无限递归,栈很快被吃干,表现是进程直接崩或者卡死。

UpcallTable就是为这个问题准备的。它是 ws2_32 提供的一组底层辅助函数,绕开 SPI 分派,直接操作句柄表和事件对象:

typedef struct _WSPUPCALLTABLE { LPWPUCLOSEEVENT lpWPUCloseEvent; LPWPUCLOSESOCKETHANDLE lpWPUCloseSocketHandle; LPWPUCREATEEVENT lpWPUCreateEvent; LPWPUCREATESOCKETHANDLE lpWPUCreateSocketHandle; LPWPUFDISSET lpWPUFDIsSet; LPWPUGETPROVIDERPATH lpWPUGetProviderPath; LPWPUMODIFYIFSHANDLE lpWPUModifyIFSHandle; LPWPUQUERYBLOCKINGCALLBACK lpWPUQueryBlockingCallback; LPWPUQUERYSOCKETHANDLECONTEXT lpWPUQuerySocketHandleContext; LPWPUQUEUEAPC lpWPUQueueApc; LPWPURESETEVENT lpWPUResetEvent; LPWPUSETEVENT lpWPUSetEvent; LPWPUCOMPLETEOVERLAPPEDREQUEST lpWPUCompletion; // ... 视版本还有若干项 } WSPUPCALLTABLE;

其中lpWPUCreateSocketHandle值得单独说。它让你能造出一个“属于自己”的 SOCKET 值,并把这个值和一段自定义上下文绑在一起,之后用lpWPUQuerySocketHandleContext反查回来。这是需要维护每连接状态时唯一正规的做法:不自己造句柄,就没法在WSPSocket和后续的WSPRecv之间传递状态。

注意:lpWPUModifyIFSHandle只对 IFS 提供者有意义,普通的分层提供者调用它会失败。看到这个函数就想用,是常见的误判。

3.3 剥一层:怎么拿到下一层的函数表

你的 30 个函数大多只是转发,转发目标就是“下一层”。下一层是谁、它的函数表怎么拿?答案是把lpProtocolInfo剥掉自己这一层,然后加载下层 DLL 调它的WSPStartup。

剥层的代码不长,但每一步都要对得上链的语义:

static WSPPROC_TABLE g_next; // 下层的函数表,转发时用 static WSPUPCALLTABLE g_upcall; static bool StripOneLayer(const WSAPROTOCOL_INFOW* in, WSAPROTOCOL_INFOW* out) { *out = *in; // ChainEntries[0] 是占位 0,不含自己;ChainEntries[1] 就是下一层 if (out->ProtocolChain.ChainLen < 2) return false; // 已经是 base,没有下一层可剥 out->dwCatalogEntryId = out->ProtocolChain.ChainEntries[1]; // 自己降级为下层 // 整条链左移一位,把“下一层”变成“我” for (int i = 1; i < out->ProtocolChain.ChainLen - 1; ++i) out->ProtocolChain.ChainEntries[i] = out->ProtocolChain.ChainEntries[i + 1]; out->ProtocolChain.ChainLen -= 1; // 退化到 base 时,ChainEntries[0] 必须等于自己的目录号 if (out->ProtocolChain.ChainLen == 1) out->ProtocolChain.ChainEntries[0] = out->dwCatalogEntryId; return true; }

拿到out之后,用WSCGetProviderPath按out.ProviderId查出下层 DLL 的完整路径,LoadLibrary加载,GetProcAddress(..., "WSPStartup")取入口,然后以out作为lpProtocolInfo、同一张UpcallTable、一个空的WSPPROC_TABLE调用它。下层如果也是分层提供者,它自己会再剥一层,一路剥到 base 为止。

这里有一个容易忽略的细节:下层的WSPStartup传进去的UpcallTable必须是 ws2_32 给你的那一份,不能自己重造。整个链上所有层共享同一套 upcall,这样lpWPUCreateSocketHandle造出的句柄才在全局可解释。

4. 实现细节:安装、转发与句柄透传

4.1 安装与排序:WSCInstallProvider 之后的半步

安装的全部难点在于“填对目录项”和“排对顺序”。最稳妥的方式是拿一个现成的 base 项当模板,改掉该改的字段,其余原样保留,这样三元组和能力位天然一致:

static const GUID kLayerGuid = { 0x9a3f1c70, 0x2b4e, 0x4c11, { 0x8e, 0x21, 0x7a, 0x55, 0x0d, 0x9c, 0x31, 0x64 } }; bool InstallLayer(const wchar_t* dllPath) { int err = 0; // 1) 先问目录有多大,再分配——目录可能被别的安装程序改过 DWORD bytes = 0; WSCEnumProtocols(nullptr, nullptr, &bytes, &err); // 返回 SOCKET_ERROR + WSAENOBUFS std::vector<BYTE> buf(bytes); auto* list = reinterpret_cast<WSAPROTOCOL_INFOW*>(buf.data()); int count = WSCEnumProtocols(nullptr, list, &bytes, &err); if (count == SOCKET_ERROR) return false; // 2) 找一个 TCP/IPv4 的 base 项当模板 const WSAPROTOCOL_INFOW* base = nullptr; for (int i = 0; i < count; ++i) { if (list[i].iAddressFamily == AF_INET && list[i].iProtocol == IPPROTO_TCP && list[i].ProtocolChain.ChainLen == 1) { base = &list[i]; break; } } if (!base) return false; // 3) 造自己的目录项 WSAPROTOCOL_INFOW layer = *base; layer.ProviderId = kLayerGuid; // 固定 GUID,卸载时靠它找回 layer.dwCatalogEntryId = 0; // 由系统分配,手填必错 layer.ProtocolChain.ChainLen = 2; layer.ProtocolChain.ChainEntries[0] = 0; layer.ProtocolChain.ChainEntries[1] = base->dwCatalogEntryId; wcscpy_s(layer.szProtocol, L"Stream Filter [TCP/IPv4]"); // 4) 写进目录(需要管理员) if (WSCInstallProvider(&kLayerGuid, dllPath, &layer, 1, &err) == SOCKET_ERROR) return false; // 5) 关键半步:把新项排到对应 base 项的前面,否则扫描永远轮不到 // 这里省略枚举新目录号并重排 order 数组的代码,逻辑是: // 先按 ProviderId 找到自己的 dwCatalogEntryId, // 再把它插到 base 项下标之前,最后整体提交。 return true; }

第 5 步是整个安装里最容易被跳过的一步。WSCInstallProvider只是把条目塞进目录尾部,而socket()的匹配是线性从头扫的,尾部意味着“永远被前面的 base 项截胡”。所以安装完成后必须重新枚举、找到自己的新目录号、调整顺序数组、调WSCWriteProviderOrder提交。少了这一步,注册表里能看到你的条目,netsh winsock show catalog也能看到,但流量就是不经过你——这是排查时最典型的一种“看着装上了其实没生效”。

4.2 三十个函数指针的转发骨架

WSPPROC_TABLE大致 30 项,覆盖WSPSocket、WSPBind、WSPConnect、WSPListen、WSPAccept、WSPSend、WSPRecv、WSPCloseSocket、WSPIoctl等等。要让它跑起来,最简单的策略是“全量转发 + 关键点插桩”:把下层表整体复制一份到全局g_next,然后让lpProcTable的每一项都指向自己的同名包装函数,包装函数里只做记录,然后原样透传。

下面是一个带记录的WSPConnect,以及一个最简的初始化骨架:

static WSAPROTOCOL_INFOW g_self; // 自己这一层的目录项副本 static int WSPAPI Hook_Connect(SOCKET s, const struct sockaddr* name, int namelen, LPWSABUF lpCallerData, LPQOS lpSQOS, LPQOS lpGQOS, LPINT lpErrno) { // 只记录,不修改参数。地址长度要先判断,sockaddr_in6 比 sockaddr_in 长 if (name && namelen >= (int)sizeof(sockaddr_in)) { if (name->sa_family == AF_INET) { auto* v4 = reinterpret_cast<const sockaddr_in*>(name); RecordConnect(s, AF_INET, &v4->sin_addr, ntohs(v4->sin_port)); } else if (name->sa_family == AF_INET6 && namelen >= (int)sizeof(sockaddr_in6)) { auto* v6 = reinterpret_cast<const sockaddr_in6*>(name); RecordConnect(s, AF_INET6, &v6->sin6_addr, ntohs(v6->sin6_port)); } } // 透传给下一层。注意这里用的是下一层的函数表,不是 ws2_32 的 connect() return g_next.WSPConnect(s, name, namelen, lpCallerData, lpSQOS, lpGQOS, lpErrno); } BOOL WINAPI WSPStartup(WORD wVersionRequested, LPWSPDATA lpWSPData, LPWSAPROTOCOL_INFOW lpProtocolInfo, WSPUPCALLTABLE UpcallTable, LPWSPPROC_TABLE lpProcTable) { if (wVersionRequested < 0x0002) return WSAEPROTONOSUPPORT; g_self = *lpProtocolInfo; g_upcall = UpcallTable; // 1) 剥一层,拿到下层的目录项 WSAPROTOCOL_INFOW down; if (!StripOneLayer(lpProtocolInfo, &down)) return WSAEPROVIDERFAILEDINIT; // 2) 按 GUID 查下层 DLL 路径并加载 WCHAR path[MAX_PATH] = {0}; int len = MAX_PATH, err = 0; if (WSCGetProviderPath(const_cast<LPGUID>(&down.ProviderId), path, &len, &err) == SOCKET_ERROR) return WSAEPROVIDERFAILEDINIT; HMODULE mod = LoadLibraryW(path); if (!mod) return WSAEPROVIDERFAILEDINIT; auto nextStartup = reinterpret_cast<LPWSPSTARTUP>(GetProcAddress(mod, "WSPStartup")); if (!nextStartup) return WSAEPROVIDERFAILEDINIT; // 3) 调下层初始化,拿它的函数表 WSAPDATA downData = {0}; if (nextStartup(0x0002, &downData, &down, UpcallTable, &g_next) != 0) return WSAEPROVIDERFAILEDINIT; // 4) 回填自己的信息,并把整张表换成我们的包装函数 lpWSPData->wVersion = 0x0002; lpWSPData->wHighVersion = 0x0002; wcscpy_s(lpWSPData->szDescription, L"Stream Filter Layer"); *lpProcTable = g_next; // 先整体复制,再逐项替换 lpProcTable->WSPConnect = Hook_Connect; lpProcTable->WSPRecv = Hook_Recv; // 同理 // ... 其余需要插桩的项逐个替换 return 0; }

这段骨架里,*lpProcTable = g_next再逐项覆盖的做法能保证“没有插桩的函数”依然可用,减少漏填导致的诡异失败。另一个细节在Hook_Connect:ntohs必须在sa_family判断之后用,且namelen要先做长度校验,否则一个 AF_INET6 的地址被当成sockaddr_in读,会读到栈上的垃圾值。

RecordConnect这类记录函数里绝对不能出现任何 ws2_32 调用。要写文件、要发消息、要加锁,都得扔到一个独立线程的队列里去消费。SPI 的每个函数都运行在应用自己的线程上,而且是网络路径的关键位置,任何一次阻塞都会直接变成应用的卡顿。

4.3 句柄、IFS 标志与 32/64 位双份 DLL

句柄处理上有一个明确的选择:透传。Hook_Socket调下层WSPSocket拿到真实句柄,直接返回给应用,后续所有操作都用这个句柄转给下层。这样做最省事,也很安全,因为句柄从始至终只有一个来源,不存在映射表不同步的问题。代价是你没法在句柄上挂自己的状态——需要状态就用lpWPUCreateSocketHandle自己造句柄,然后用lpWPUQuerySocketHandleContext反查,但那意味着你要接管句柄的生命周期,WSPCloseSocket里必须用lpWPUCloseSocketHandle释放,漏一次就是句柄泄漏。

XP1_IFS_HANDLES这个能力位决定了句柄是不是真正的内核文件句柄。设了它,应用可以拿这个 SOCKET 直接喂给ReadFile/WriteFile;没设,就只能走 Winsock API。普通的用户态分层提供者必须把这个位清掉,因为它的句柄不是文件句柄。清掉的连锁反应不小:依赖真文件句柄的机制,比如完成端口、AcceptEx、重叠 I/O,在这条链上会出现各种不兼容。这是 LSP 这条路线的结构性缺陷,后面还会提到。

位数问题没有任何技巧可言:64 位进程只加载 64 位 DLL,32 位进程只加载 32 位 DLL。所以要准备两份二进制,用两个安装程序分别在 32 位和 64 位视图下调用WSCInstallProvider各装一次,两份可以用同一个 GUID(因为两个 catalog 是独立存储),也可以用不同 GUID 便于分别卸载。只装一份的典型表现是“32 位程序能看到流量、64 位程序看不到”,或者反过来。

5. 排查:LSP 装上不生效、一装就断网、卸不干净的常见原因

5.1 装完却没流量经过

最常见的现象是:netsh winsock show catalog里能看到自己那条,szProtocol名字也在,注册表里也有项,但随便写个小程序connect一下,日志一条不打。

原因通常有三个,按出现频率排。第一是没调WSCWriteProviderOrder,条目排在目录末尾,被前面的 base 项先匹配走了。第二是只装了一个位数视图,测试用的程序恰好是另一个位数。第三是 DLL 加载失败但被误判成“没生效”——比如 DLL 依赖某个运行库,而目标进程的工作目录和环境里找不到它,LoadLibrary返回失败,WSPStartup直接返回错误,socket()调用方拿到的是失败而不是“跳过”。

对应的解决方式:第一步先确认顺序,把WSCEnumProtocols的结果按顺序打出来,看自己的dwCatalogEntryId排在第几位;第二步用同位数的一个最小测试程序(不要用系统自带工具,位数不可控)验证;第三步把 DLL 的依赖降到最低,尽量静态链接运行库,避免任何额外的 DLL 依赖,并在WSPStartup里对每一步失败都写日志——注意是写到一个固定路径的文件里,不是OutputDebugString,那个在非调试环境里什么都看不到。

注意:不要用“看注册表有没有项”来判断安装成功。注册表有项只说明写进去了,不说明顺序对,也不说明 DLL 能被加载。

5.2 一装就卡死或断网

现象很吓人:装完立刻,浏览器打不开任何页面,甚至任务管理器里的网络相关进程全部无响应,卸载之后恢复正常。这种情况基本可以锁定到两类原因。

第一类是重入。在自己的某个包装函数里顺手调了一个 ws2_32 的导出函数(getpeername、getsockname、closesocket都算),于是形成“包装函数 → ws2_32 → 包装函数”的循环,栈一路涨到耗尽。这类问题的特征是 CPU 突然打满、进程崩溃或者彻底卡住,而且往往只在特定操作上触发——比如只在connect之后调getpeername的时候。

第二类是阻塞。在 SPI 函数里做了耗时操作:同步解析一个名字、读写一个文件、等一把可能被别的线程持有的锁、发一条同步 IPC 消息。SPI 函数跑在应用自己的线程上,应用的网络调用路径被你这么一挡,表现就是“网络卡住了”。如果这个锁还被另一个正在等网络结果的线程持有,就是死锁,卸载都救不回来,只能重启。

解决办法是硬性纪律:SPI 函数内只做内存拷贝和入队,任何 I/O、锁等待、内存分配以外的系统调用全部挪到独立线程。句柄相关的操作一律用UpcallTable里的lpWPU*版本,不要用 ws2_32 的同名 API。

5.3 卸载之后的残留

现象是卸载程序跑完、提示成功,但网络仍然不正常,或者重装时报“提供者已存在”,或者某些程序启动就报初始化失败。

原因在于卸载的顺序和时机。WSCDeinstallProvider只是删目录项,已经加载到各个进程里的 DLL 还在内存里,正在运行的进程持有的 socket 也还绑在你的链上。如果这时候你直接把 DLL 文件删了,之后任何一次延迟加载都会失败。正确顺序是:先WSCDeinstallProvider删条目,再删文件,中间留出重启;如果情况已经乱了,用系统的网络栈重置命令兜底,再重启。

另一个隐蔽残留是链被改坏。如果你装了两层(自己的层 + 别人的层),卸载时只删了自己那层但没更新上面那层的ChainEntries,上面那层的链里就指着一个不存在的目录号,任何走它的socket()都会失败。所以卸载前必须先枚举,找出所有ChainEntries里引用了自己目录号的项,逐个修正,再删自己。

6. 设计取舍:LSP 这套机制为什么会被系统放弃

LSP 的设计意图是让传输层可插拔:把“谁来实现 socket”这件事从编译期挪到运行期,通过一张目录表加一条链来描述。这个取舍带来的好处很直接:扩展点不在代码里而在数据里,所以不需要知道目标进程是谁、不需要驱动、不需要改一行别人的二进制。装一次,全机器

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 3:30:41

CHARLS数据自动化处理与CKM分期技术解析

1. 项目背景与核心价值CHARLS&#xff08;中国健康与养老追踪调查&#xff09;作为国内最具影响力的老龄化追踪数据库&#xff0c;其数据价值在医学研究中日益凸显。心血管-肾-代谢综合征&#xff08;CKM&#xff09;作为近年来国际医学界提出的新型综合症候群概念&#xff0c;…

作者头像 李华
网站建设 2026/10/31 3:30:27

Eleven v4 语音合成升级解析:音色克隆、IPA 与 API 实战

1. 语音合成赛道的新变量&#xff1a;Eleven v4 到底升级了什么语音合成这个领域&#xff0c;过去两年变化快得让人有点跟不上。从最早只能念出机械感十足的句子&#xff0c;到后来能模仿特定人的音色&#xff0c;再到现在要求模型理解上下文、带情绪、能跨语言无缝切换&#x…

作者头像 李华
网站建设 2026/10/31 3:30:26

佩尔方程最小解:用连分数与纯整数递推避开浮点误差

硬要说的话&#xff0c;佩尔方程最容易让人产生误判的地方&#xff0c;就是它在纸面上看着特别"初中"——x - Dy 1&#xff0c;两个未知数&#xff0c;一个参数&#xff0c;怎么看都像是课后习题第一题。真动笔算的时候才会发现&#xff0c;D 取 61 这一个数&#x…

作者头像 李华
网站建设 2026/10/31 3:29:52

C3k2-YOLO轻量化目标检测网络设计与优化

1. 项目概述&#xff1a;C3k2-YOLO轻量化目标检测网络设计在计算机视觉领域&#xff0c;目标检测算法的实时性和准确性始终是矛盾的焦点。我们团队针对AAAI2026会议设计的C3k2-YOLO网络&#xff0c;通过融合PartialNet Block结构和独创的轻量化模块&#xff0c;在保持YOLO系列实…

作者头像 李华
网站建设 2026/10/31 3:28:51

Python+Django构建在线政务系统开发实践

1. 项目概述"基于Python的在线政务服务中心"是一个典型的Web应用开发项目&#xff0c;旨在通过数字化手段简化传统政务服务流程。这个毕业设计项目采用PythonDjango技术栈构建&#xff0c;包含前端展示、后台管理和数据库三个主要模块&#xff0c;实现了政务事项在线…

作者头像 李华
网站建设 2026/10/31 3:28:51

React+TypeScript组件联合类型解决方案

1. React组件中联合类型的核心挑战在ReactTypeScript开发中&#xff0c;处理组件Props的联合类型是个高频痛点。最常见的情况是&#xff1a;我们需要设计一个通用容器组件&#xff0c;它能根据不同的业务场景渲染不同的子组件&#xff0c;而这些子组件各自拥有不同的Props类型定…

作者头像 李华