1. 从零理解可被 svchost 调用的服务
1.1 为什么会有“寄生”在 svchost 里的服务
如果你在任务管理器里翻过进程列表,一定见过svchost.exe这个家伙——它往往同时存在几十个实例,每个实例后面跟着一串-k参数。很多人第一次看到会以为它是病毒,其实它是 Windows 系统里一个非常核心的“服务宿主容器”。所谓“可被 svchost 调用的服务”,本质上就是把我们自己写的服务 DLL 挂载到svchost.exe这个宿主进程里运行,而不是让服务以独立 exe 的形式单独起一个进程。
这么做的动机很实际。独立 exe 服务每个都要占一份进程开销,几十个服务就是几十个进程,内存、句柄、启动时间都是成本。而 svchost 的设计思路是“多个服务共享一个宿主进程”,把一组逻辑相关的服务 DLL 加载进同一个 svchost 实例,由它统一管理生命周期。系统自带的很多服务就是这么干的,比如事件日志、主题、部分网络相关组件。我们自己写服务时,如果希望更贴近系统原生风格、减少进程数量、或者需要和某些系统机制配合,就会选择做成“可被 svchost 调用的服务”。
这篇文章要解决的核心问题很明确:怎么把一个普通的服务 DLL,改造成能被 svchost 正确加载、启动、停止、并且能被 SCM(服务控制管理器)识别的形态。适合谁看?适合已经写过基础 Windows 服务、懂一点 C/C++ 或至少能看懂代码、对注册表和服务控制机制有初步概念的开发者。如果你连CreateService都没调用过,建议先补一下基础服务开发,再来看这篇,不然会有点吃力。
1.2 svchost 与普通服务进程的本质区别
普通服务 exe 的模型是这样的:SCM 启动你的 exe,你的main里调用StartServiceCtrlDispatcher,把服务入口函数注册进去,然后 SCM 通过控制管道和你通信。整个进程就是你自己的代码,你想怎么初始化都行。
svchost 模型完全不同。svchost.exe 本身是一个通用宿主,它启动后读取命令行里的-k参数,比如-k netsvcs,然后去注册表HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost下面找名为netsvcs的那个多字符串值,里面列出了一堆服务名。接着它再逐个去HKLM\SYSTEM\CurrentControlSet\Services\<服务名>\Parameters里读ServiceDll这个值,把对应的 DLL 加载进来,并调用 DLL 导出的ServiceMain。
所以关键差异在于:你的代码不再拥有进程,你只是宿主里的一个 DLL。进程的创建、StartServiceCtrlDispatcher的调用、控制分发器的注册,全都由 svchost 替你做了。你只需要导出一个符合签名的ServiceMain,并在里面完成服务初始化、注册控制处理函数、进入服务循环。这个模型带来的约束很多,比如你不能随便改进程级的东西、不能假设自己是唯一服务、DLL 加载时机和卸载时机都要小心处理。
理解这一点之后,后面所有的配置和代码写法就都顺理成章了——我们做的每一件事,都是在“配合宿主”而不是“主导进程”。
2. 核心机制拆解:svchost 到底怎么加载你的 DLL
2.1 注册表里的三处关键配置
要让 svchost 认识你的服务,注册表必须写对三块内容,缺一不可。我见过太多人卡在这里,代码没问题,就是起不来,最后发现是注册表少了一项。
第一块是服务本身的键:HKLM\SYSTEM\CurrentControlSet\Services\<你的服务名>。这里面必须有Type、Start、ImagePath、ObjectName等标准值。其中Type要设成0x20,也就是SERVICE_WIN32_SHARE_PROCESS,这个值告诉 SCM“我这个服务是共享进程模式的”。如果你写成0x10(SERVICE_WIN32_OWN_PROCESS),SCM 就会尝试把ImagePath当独立 exe 启动,那就完全走错路了。
第二块是Parameters子键下的ServiceDll,类型是REG_EXPAND_SZ,值指向你的 DLL 完整路径。注意这里推荐用REG_EXPAND_SZ而不是REG_SZ,因为路径里可能包含%SystemRoot%这类环境变量,svchost 加载时会做展开。用REG_SZ的话环境变量不会被解析,路径就错了。
第三块是分组配置:HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost下面要有一个多字符串值,名字就是你的组名(比如netsvcs或你自己新建的组),内容里包含你的服务名。svchost 启动时靠这个列表决定加载哪些 DLL。如果你不想污染系统自带的组,可以新建一个自己的组名,然后让ImagePath指向%SystemRoot%\System32\svchost.exe -k 你的组名。
这三块的关系可以这样理解:ImagePath决定用哪个宿主和哪个组,组列表决定这个宿主加载哪些服务,ServiceDll决定每个服务具体加载哪个 DLL。三者对上了,svchost 才能正确找到并加载你的代码。
2.2 ServiceMain 的签名与调用约定
svchost 加载你的 DLL 后,会通过GetProcAddress找名为ServiceMain的导出函数。这个函数的签名是固定的:
VOID WINAPI ServiceMain(DWORD dwArgc, LPWSTR *lpszArgv);必须是WINAPI调用约定,也就是__stdcall,参数是两个。第一个是参数个数,第二个是参数数组,通常第一个参数就是服务名。很多人在这里犯错,要么忘了WINAPI,要么参数类型写错,结果 svchost 调用时栈不平衡,直接崩溃或者静默失败。
还有一点容易被忽略:ServiceMain必须被导出。如果你用.def文件,要写EXPORTS ServiceMain;如果用__declspec(dllexport),要确保名字没有被 C++ 名称修饰破坏。C++ 默认会做 name mangling,导出的名字会变成?ServiceMain@@YGXKPEAPEA_W@Z这种,svchost 按字面找ServiceMain就找不到了。解决办法是用extern "C"包起来,或者用.def文件强制指定导出名。
extern "C" __declspec(dllexport) VOID WINAPI ServiceMain(DWORD dwArgc, LPWSTR *lpszArgv) { // 服务初始化逻辑 }这个细节我在第一次做的时候踩过,DLL 编译出来用 Dependency Walker 看导出表,名字是一长串修饰过的,当时还纳闷为什么 svchost 死活不加载。后来改成extern "C"就正常了。
2.3 控制处理器的注册时机
在ServiceMain里,第一件必须做的事是调用RegisterServiceCtrlHandlerEx注册控制处理函数,拿到SERVICE_STATUS_HANDLE。这个句柄是后续向 SCM 汇报状态的唯一通道,没有它你什么都做不了。
SERVICE_STATUS_HANDLE hStatus = RegisterServiceCtrlHandlerExW( lpszArgv[0], ControlHandlerEx, NULL); if (hStatus == NULL) { return; // 注册失败,直接退出 }注册完之后要立刻设置服务状态为SERVICE_START_PENDING,告诉 SCM“我正在启动,别急”。然后做初始化工作,完成后设成SERVICE_RUNNING。这个状态机是有严格顺序的,SCM 会根据你上报的状态判断服务是否正常。如果你跳过START_PENDING直接报RUNNING,大多数情况下也能跑,但某些系统版本上会认为状态跳变非法。
控制处理函数本身要处理至少SERVICE_CONTROL_STOP和SERVICE_CONTROL_SHUTDOWN两个控制码。收到停止请求时,你要设置一个事件或者标志位,让服务主循环退出,然后上报SERVICE_STOPPED。这里有个坑:停止处理必须尽快返回,不能在里面做耗时操作,否则 SCM 会认为服务无响应,可能强制杀进程。正确做法是发信号让主线程去清理,控制处理函数本身只做最小动作。
3. 动手实现:从 DLL 工程到可加载服务
3.1 工程配置与导出设置
先建一个 DLL 工程,不管是 Visual Studio 还是手写 Makefile 都行。关键配置有几项:字符集用 Unicode,因为RegisterServiceCtrlHandlerExW是宽字符版本;运行库建议用静态链接(/MT),避免目标机器缺运行时;导出方式推荐用.def文件,最直观也最不容易出错。
.def文件内容就两行:
LIBRARY MyServiceDll EXPORTS ServiceMain这样编译出来的 DLL,导出表里干干净净只有一个ServiceMain,svchost 一找就能找到。如果你用__declspec(dllexport)加extern "C",效果一样,但要注意extern "C"只影响 C++ 的名称修饰,不影响调用约定,WINAPI还是要写。
工程里还要注意一点:不要链接main或WinMain,DLL 的入口是DllMain。DllMain里不要做复杂初始化,尤其是不要调用会加载其他 DLL 或者等待同步对象的操作,因为DllMain在加载器锁里执行,容易死锁。把真正的初始化放到ServiceMain里做。
3.2 ServiceMain 的完整实现骨架
下面是一个可以直接参考的骨架,我把它拆成几个阶段来讲。
#include <windows.h> SERVICE_STATUS_HANDLE g_hStatus = NULL; SERVICE_STATUS g_Status = {0}; HANDLE g_hStopEvent = NULL; VOID WINAPI ServiceMain(DWORD dwArgc, LPWSTR *lpszArgv); DWORD WINAPI ControlHandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext);全局变量用来保存状态句柄和停止事件。g_hStopEvent是一个手动重置事件,服务主循环等它,控制处理函数收到停止请求时设置它。
ServiceMain的实现分四步:
VOID WINAPI ServiceMain(DWORD dwArgc, LPWSTR *lpszArgv) { // 第一步:注册控制处理函数 g_hStatus = RegisterServiceCtrlHandlerExW( lpszArgv[0], ControlHandlerEx, NULL); if (!g_hStatus) return; // 第二步:上报 START_PENDING g_Status.dwServiceType = SERVICE_WIN32_SHARE_PROCESS; g_Status.dwCurrentState = SERVICE_START_PENDING; g_Status.dwControlsAccepted = 0; g_Status.dwWin32ExitCode = NO_ERROR; g_Status.dwCheckPoint = 1; g_Status.dwWaitHint = 5000; SetServiceStatus(g_hStatus, &g_Status); // 第三步:创建停止事件,做初始化 g_hStopEvent = CreateEventW(NULL, TRUE, FALSE, NULL); if (!g_hStopEvent) { g_Status.dwCurrentState = SERVICE_STOPPED; g_Status.dwWin32ExitCode = GetLastError(); SetServiceStatus(g_hStatus, &g_Status); return; } // 这里做你的实际初始化,比如打开设备、读配置 // 第四步:上报 RUNNING,进入主循环 g_Status.dwCurrentState = SERVICE_RUNNING; g_Status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN; g_Status.dwCheckPoint = 0; g_Status.dwWaitHint = 0; SetServiceStatus(g_hStatus, &g_Status); // 主循环:等停止事件 WaitForSingleObject(g_hStopEvent, INFINITE); // 清理 CloseHandle(g_hStopEvent); g_hStopEvent = NULL; g_Status.dwCurrentState = SERVICE_STOPPED; SetServiceStatus(g_hStatus, &g_Status); }注意dwServiceType这里写的是SERVICE_WIN32_SHARE_PROCESS,和注册表里的Type值对应。dwControlsAccepted在START_PENDING阶段要设成 0,因为此时还没准备好接受控制;到RUNNING阶段再打开STOP和SHUTDOWN。
3.3 控制处理函数的正确写法
控制处理函数收到停止或关机请求时,只做一件事:设置停止事件。
DWORD WINAPI ControlHandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext) { switch (dwControl) { case SERVICE_CONTROL_STOP: case SERVICE_CONTROL_SHUTDOWN: g_Status.dwCurrentState = SERVICE_STOP_PENDING; g_Status.dwCheckPoint = 1; g_Status.dwWaitHint = 5000; SetServiceStatus(g_hStatus, &g_Status); if (g_hStopEvent) SetEvent(g_hStopEvent); return NO_ERROR; case SERVICE_CONTROL_INTERROGATE: SetServiceStatus(g_hStatus, &g_Status); return NO_ERROR; default: return ERROR_CALL_NOT_IMPLEMENTED; } }这里先上报STOP_PENDING再设置事件,顺序很重要。如果先设事件,主循环可能立刻退出并上报STOPPED,而控制处理函数还没返回,SCM 那边状态就乱了。先报PENDING给 SCM 一个缓冲,再触发退出,主循环清理完再报STOPPED,整个流程就顺了。
SERVICE_CONTROL_INTERROGATE是 SCM 查询服务状态用的,必须处理,直接回当前状态即可。其他控制码如果不支持,返回ERROR_CALL_NOT_IMPLEMENTED,不要返回NO_ERROR,否则 SCM 会以为你处理了。
3.4 注册表配置的脚本化落地
手动改注册表容易出错,建议写成一个.reg文件或者用命令行工具导入。下面是一个模板,把占位符替换成你自己的值即可。
Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MySampleSvc] "Type"=dword:00000020 "Start"=dword:00000003 "ErrorControl"=dword:00000001 "ImagePath"=hex(2):25,00,53,00,79,00,73,00,74,00,65,00,6d,00,52,00,6f,00,6f,00,\ 74,00,25,00,5c,00,53,00,79,00,73,00,74,00,65,00,6d,00,33,00,32,00,5c,00,73,\ 00,76,00,63,00,68,00,6f,00,73,00,74,00,2e,00,65,00,78,00,65,00,20,00,2d,00,\ 6b,00,20,00,4d,00,79,00,47,00,72,00,6f,00,75,00,70,00,00,00 "ObjectName"="LocalSystem" [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MySampleSvc\Parameters] "ServiceDll"=hex(2):43,00,3a,00,5c,00,50,00,61,00,74,00,68,00,5c,00,54,00,6f,\ 00,5c,00,4d,00,79,00,53,00,65,00,72,00,76,00,69,00,63,00,65,00,2e,00,64,00,\ 6c,00,6c,00,00,00 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost] "MyGroup"=hex(7):4d,00,79,00,53,00,61,00,6d,00,70,00,6c,00,65,00,53,00,76,00,\ 63,00,00,00,00,00上面ImagePath和ServiceDll用的是十六进制形式,因为它们是REG_EXPAND_SZ类型,.reg文件里宽字符要这样写。如果你觉得麻烦,可以用 PowerShell 脚本设置,可读性好得多:
$svcName = "MySampleSvc" $dllPath = "C:\Path\To\MyService.dll" $groupName = "MyGroup" New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Services\$svcName" -Force Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\$svcName" -Name "Type" -Value 0x20 -Type DWord Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\$svcName" -Name "Start" -Value 3 -Type DWord Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\$svcName" -Name "ImagePath" -Value "%SystemRoot%\System32\svchost.exe -k $groupName" -Type ExpandString Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\$svcName" -Name "ObjectName" -Value "LocalSystem" -Type String New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Services\$svcName\Parameters" -Force Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\$svcName\Parameters" -Name "ServiceDll" -Value $dllPath -Type ExpandString $svchostKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost" $existing = (Get-ItemProperty -Path $svchostKey -Name $groupName -ErrorAction SilentlyContinue).$groupName if (-not $existing) { New-ItemProperty -Path $svchostKey -Name $groupName -Value @($svcName) -Type MultiString } elseif ($existing -notcontains $svcName) { Set-ItemProperty -Path $svchostKey -Name $groupName -Value ($existing + $svcName) -Type MultiString }PowerShell 版本的好处是ExpandString类型直接写%SystemRoot%就行,不用手动转十六进制。而且追加组列表的逻辑也处理了,不会覆盖已有的服务。
4. 调试与排错:那些让人抓狂的坑
4.1 服务起不来时的排查顺序
服务注册完,用sc start MySampleSvc启动,如果报错,按下面的顺序查,基本能覆盖九成问题。
第一,看sc query MySampleSvc的状态和退出码。如果状态是STOPPED且退出码非零,说明ServiceMain里某一步失败了。常见的是RegisterServiceCtrlHandlerExW返回 NULL,原因通常是服务名不对或者 SCM 没连上。
第二,查事件查看器里的系统日志。svchost 加载 DLL 失败时,系统日志里会有明确记录,比如“无法加载服务 DLL”或者“指定的模块找不到”。这个信息比sc的报错详细得多。
第三,用Dependency Walker或者dumpbin /exports检查 DLL 的导出表,确认ServiceMain存在且名字正确。这一步能排除掉大部分“代码没问题但就是加载不了”的情况。
第四,确认 DLL 的位数和系统匹配。64 位系统上,System32下的 svchost 是 64 位的,你的 DLL 也必须是 64 位。如果你编译了 32 位 DLL 放到System32,加载会直接失败。32 位服务 DLL 应该放SysWOW64,但对应的 svchost 也是 32 位的,这个要分清楚。
4.2 常见错误对照表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
sc start报 1053 | 服务未在 30 秒内响应 | 检查ServiceMain是否卡在初始化,是否及时上报了RUNNING |
| 报 1067 | 进程意外终止 | 看事件日志,通常是 DLL 加载失败或ServiceMain崩溃 |
服务状态一直START_PENDING | 没上报RUNNING | 检查SetServiceStatus调用是否执行到 |
| 服务启动后立刻停止 | ServiceMain提前返回 | 确认主循环的WaitForSingleObject在等事件 |
| 停止服务时卡住 | 控制处理函数阻塞 | 确认停止处理只设事件,不做耗时操作 |
| DLL 加载失败 | 导出名不对或位数不匹配 | 用dumpbin /exports检查,确认位数 |
这张表是我自己踩坑总结的,基本上每次出问题都能对上其中一条。特别是 1053 和 1067 这两个,新手最容易遇到。
4.3 调试 DLL 服务的实用技巧
调试服务 DLL 比调试普通 exe 麻烦,因为进程是 svchost 起的,你没法直接 F5。我常用的办法有两种。
第一种是“附加到进程”。先把服务启动起来,然后在 Visual Studio 里选“调试 -> 附加到进程”,找到承载你服务的那个 svchost 实例。怎么找?用tasklist /svc /fi "imagename eq svchost.exe"列出所有 svchost 和它们承载的服务,找到包含你服务名的那个 PID,附加进去。附加之前最好在ServiceMain开头加个DebugBreak()或者Sleep(10000),给你留出附加的时间窗口。
第二种是“独立宿主调试”。写一个小的 exe,手动LoadLibrary你的 DLL,然后直接调用ServiceMain。这样你就在自己的进程里调试,断点随便下。缺点是RegisterServiceCtrlHandlerExW会失败,因为你的进程不是 SCM 启动的服务进程。解决办法是在调试版本里加一个开关,检测到注册失败就跳过状态上报,只跑业务逻辑。这样能调大部分代码,只有状态机相关的部分需要回到真实环境验证。
提示:调试版本和发布版本一定要分开编译,调试开关不要带到发布版里,否则服务在真实环境下行为会不一致。
4.4 卸载与清理的注意事项
测试完要卸载服务,别直接删注册表键就完事。正确顺序是:先sc stop,确认服务已停止,再sc delete,最后删 DLL 文件。如果服务还在运行就删注册表,SCM 那边状态会不一致,可能导致后续注册同名服务失败。
还有一个坑:svchost 加载你的 DLL 后,DLL 会被锁住,你没法直接覆盖或者删除。必须先停服务,等 svchost 释放 DLL,才能替换文件。如果停了服务还是删不掉,可能是别的 svchost 实例也加载了同一个 DLL,检查一下组配置是不是把服务加到了多个组里。
sc delete只是标记删除,实际注册表键要等重启或者 SCM 刷新后才真正消失。如果你急着重新注册同名服务,可能会遇到“服务已存在”的报错。这时候可以手动删注册表键,然后重启 SCM 或者重启机器。
5. 进阶话题与安全边界
5.1 服务权限与运行账户的选择
注册表里的ObjectName决定服务以哪个账户运行。默认是LocalSystem,权限最高,能访问大部分系统资源。如果你的服务不需要那么高权限,可以改成NT AUTHORITY\NetworkService或者NT AUTHORITY\LocalService,降低安全风险。
但要注意,svchost 宿主进程的账户是由组配置决定的,不是单个服务决定的。同一个组里的所有服务共享同一个 svchost 实例,也就共享同一个账户。如果你把高权限服务和低权限服务放在同一个组里,低权限服务实际上也获得了高权限。所以分组的时候要按权限级别分,不要图省事全塞一个组。
ServiceDll指向的 DLL 文件权限也要注意。如果 DLL 放在用户可写的目录里,攻击者可能替换 DLL 来提权。正确做法是放在System32或者Program Files下,并且设置严格的 ACL,只允许管理员和 SYSTEM 写入。
5.2 多服务共享宿主的隔离问题
同一个 svchost 实例里的多个服务 DLL 共享进程空间。这意味着一个服务崩溃,整个 svchost 进程都会挂掉,同组的其他服务也跟着遭殃。这是共享宿主模型的固有风险,没法完全避免,但可以通过分组来降低影响范围。
我的建议是:把稳定性要求高、互相不相关的服务分到不同组,每个组一个 svchost 实例。这样即使某个服务出问题,也不会波及其他服务。系统自带的组划分其实也是这个思路,netsvcs里放了一堆网络相关服务,LocalService和NetworkService又各自分组,都是有考量的。
另外,DLL 之间的全局符号冲突也要注意。如果两个服务 DLL 都导出了同名的全局变量或函数,加载到同一个进程里可能互相覆盖。解决办法是尽量用static或者匿名命名空间限制符号可见性,只导出必要的ServiceMain。
5.3 服务 DLL 的加载时机与 DllMain 禁忌
svchost 加载 DLL 时,会先执行DllMain的DLL_PROCESS_ATTACH分支。这个时机非常早,加载器锁还持有,很多操作都不能做。具体来说,不要在里面调用LoadLibrary、CreateThread、WaitForSingleObject、CoInitialize等,这些都可能死锁或者失败。
正确的做法是DllMain里只做最基础的初始化,比如保存模块句柄,其他全部放到ServiceMain里。ServiceMain是在加载器锁释放之后调用的,那时候环境才安全。
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { if (fdwReason == DLL_PROCESS_ATTACH) { DisableThreadLibraryCalls(hinstDLL); // 只做最基础的记录,不做复杂操作 } return TRUE; }DisableThreadLibraryCalls可以禁用DLL_THREAD_ATTACH和DLL_THREAD_DETACH通知,减少不必要的开销,也避免在DllMain里处理线程事件带来的风险。
5.4 从测试到上线的检查清单
在把服务部署到生产环境之前,我一般会过一遍这个清单:
- 注册表三处配置是否都正确,特别是
Type是否为0x20 - DLL 导出表是否只有
ServiceMain,名字是否精确匹配 - DLL 位数是否和系统匹配,路径是否在安全目录
ServiceMain是否及时上报START_PENDING和RUNNING- 控制处理函数是否处理了
STOP、SHUTDOWN、INTERROGATE - 停止流程是否能在 30 秒内完成,避免 SCM 超时
- 事件日志里是否有加载失败或权限相关的警告
- 卸载流程是否验证过,能否干净地停止和删除
这个清单看起来简单,但每一条我都见过有人栽在上面。尤其是Type值和导出名这两个,出问题的时候报错信息很不直观,排查起来费时间。
6. 我个人在实际操作中的几点体会
做可被 svchost 调用的服务,最深的体会就是“约束比自由多”。你不能像写独立 exe 那样随心所欲,每一步都要考虑宿主的感受。但正是这些约束,让服务更贴近系统原生形态,资源占用更小,启动更快。
我建议第一次做的时候,先用一个最简单的 DLL,ServiceMain里只做状态上报和等待停止事件,不写任何业务逻辑。把注册表配好,确认能正常启动、停止、查询状态。这个最小闭环跑通了,再往里加业务代码。很多人一上来就写一大堆逻辑,结果服务起不来,分不清是配置问题还是代码问题,排查成本翻倍。
另外,注册表操作一定要脚本化,不要手动点。手动改注册表容易漏项、写错类型,而且没法版本管理。用 PowerShell 或者.reg文件,改了什么一目了然,回滚也方便。
最后,测试环境一定要和真实环境一致。位数、系统版本、权限配置,任何一项不同都可能导致行为差异。我在 32 位环境测好的 DLL,拿到 64 位系统上直接加载失败,就是因为位数不匹配。这种问题在测试环境里根本复现不了,只有到了真实环境才暴露。
这个内容后续还可以往两个方向扩展:一是服务 DLL 之间的进程内通信,比如共享内存或者命名管道;二是服务的动态加载和卸载,不重启 svchost 的情况下替换 DLL。这两个话题都挺有意思,等有机会再单独写。