ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SystemInformer 开发规范完全指南:读懂 Process Hacker 源码必备的七类编码约定

SystemInformer 开发规范完全指南:读懂 Process Hacker 源码必备的七类编码约定

SystemInformer 开发规范完全指南:读懂 Process Hacker 源码必备的七类编码约定

【免费下载链接】systeminformerA free, powerful, multi-purpose tool that helps you monitor system resources, debug software and detect malware. Brought to you by Winsider Seminars & Solutions, Inc. @ https://windows-internals.com项目地址: https://gitcode.com/GitHub_Trending/sy/systeminformer

SystemInformer(前身 Process Hacker,由 Winsider Seminars & Solutions 维护)是一款开源的 Windows 系统监控、调试与反恶意软件工具,其根目录的 HACKING.md 就是这套开发规范的核心文档。本文把其中的命名、类型、同步与内存管理约定整理成一份入职速查手册:读完你能顺畅阅读源码,也能写出符合规范的补丁。

命名篇:一个符号怎么读——风格与前缀体系

风格只有三种,且严格分工:

  • 函数、函数参数、全局变量用CamelCase(如PhCreateThread);
  • 局部变量用lowerCamelCase(如myString);
  • 结构体、枚举、联合体用CAPS_WITH_UNDERSCORES(如PH_STRING、PH_QUEUED_LOCK)。

前缀则是符号的“门牌号”:

  • Ph(函数)或PH_(结构体)=公开 API,外部可以依赖;
  • Php或PHP_=模块私有实现,派生规则很机械——在公开前缀后追加字母p,所以Ph变Php、Pha变Phap;
  • 没有前缀的函数与全局变量必须声明为static,反过来,static符号也不允许带公开前缀;
  • 带私有前缀的名字不必static,可以跨文件服务于本模块;
  • 没有前缀的结构体只允许出现在.c/.cpp源文件内。

这套体系的价值:看符号首字母就能判断它是对外契约、模块内部实现,还是文件级局部——评审代码时立刻知道哪些能碰、哪些不能动。

💡 记住这一点:见到Php/PHP_就当它是“别人家的内接线”,可以读,但不要在你的改动里调用或修改它。

数据篇:类型、布尔与注解

固定宽度类型。除 Win32 API 场景外,全库统一使用 NT 风格的定宽类型:1 字节布尔用BOOLEAN,4 字节布尔用LOGICAL;字节用UCHAR;2 字节SHORT/USHORT;4 字节LONG/ULONG;8 字节LONG64/ULONG64;字符CHAR(1 字节)与WCHAR(2 字节);字符串PSTR/PWSTR。要点是“宽度钉死、跨平台一致”,而不是靠类型名表达语义。

布尔直接判断。写成if (flag)而不是if (flag == TRUE)。原因:TRUE宏值是 1,而部分接口会返回非 1 的真值,相等比较容易误判;直接测试既简洁又安全。配套小技巧:用!!expr把任意表达式“强转”成规范的 0/1 布尔,把非布尔量赋给BOOLEAN时尤其好用。

注解纪律。所有函数都要带_In_、_Inout_、_Out_等 SAL 注解。以 phlib/include/phbasesup.h 中的PhAllocate为例,它同时挂了DECLSPEC_ALLOCATOR、DECLSPEC_NOALIAS、DECLSPEC_RESTRICT——MSVC 的 /analyze 静态分析靠这些注解自动校验内存与别名问题,不是摆设。另有一条禁令:定义中不用volatile;确有必要时先转成指向 volatile 的指针再访问,避免volatile沿变量扩散、过度压制编译器优化。

契约篇:成功与失败的四种说法

全库只有四套“成败话术”:

  1. 返回BOOLEAN,TRUE即成功;
  2. 返回NTSTATUS,用NT_SUCCESS宏判断;
  3. 返回HRESULT,用HR_SUCCESS宏判断——明确禁用SUCCEEDED。原因是某些第三方组件用S_FALSE表达错误,而SUCCEEDED会把它当成功,导致假通过;
  4. 返回结果本身(如指针),用NULL这类特殊值表示失败。

两条附属契约比话术更关键:

  • 失败不改输出参数:除非另有说明,函数失败时_Out_/_Out_opt_参数保持调用前原样;
  • 别假设回调没被调用:传入回调的函数即便最终失败,回调也可能已执行过,所以回调内部必须自带状态防护。

并发篇:线程、集合与同步原语

线程例程与统一入口

线程入口函数必须长成这样(_Function_class_注解配合NTAPI调用约定):

_Function_class_(USER_THREAD_START_ROUTINE) NTSTATUS NTAPI NameOfRoutine( _In_ PVOID Parameter );

创建一律走四个入口:PhCreateThread(直接返回句柄)、PhCreateThreadEx(NTSTATUS报告结果、句柄走输出参数)、PhCreateThread2(丢弃句柄)、PhQueueUserWorkItem(投递线程池工作项)。入口收敛后,每个线程的生命周期管理才可追溯。

集合怎么选

选型分两个阵营:

  • 非侵入式:PH_ARRAY、PH_LIST、PH_POINTER_LIST(数组)、RTL_AVL_TABLE(AVL 树)、RTL_GENERIC_TABLE(splay 树)、PH_HASHTABLE(哈希表)、PH_CIRCULAR_BUFFER(环形缓冲)——容器自己管理元素存储;
  • 侵入式:LIST_ENTRY(双链表/栈/队列)、SINGLE_LIST_ENTRY(单链表/栈)、PH_AVL_LINKS、PH_HASH_ENTRY——链接字段直接嵌在元素结构体里,省掉每节点一次额外分配。

一句话建议:元素本身需要独立存活、同时挂到列表里(进程项、线程项这类场景)优先侵入式;要下标随机访问就用PH_ARRAY/PH_POINTER_LIST。

同步基石:queued lock

HACKING.md 的硬规定是所有同步都用 queued lock,理由:尺寸小、性能好。phlib/include/queuedlock.h 里的PH_QUEUED_LOCK全部家当就是一个指针宽度的ULONG_PTR Value,所有者标志、共享读计数、等待队列头都编码在它的位段里;可静态初始化为PH_QUEUED_LOCK_INIT { 0 }。发生争用时,等待者挂在 16 字节对齐的_PH_QUEUED_WAIT_BLOCK等待块链表上,按到达顺序唤醒——像银行叫号排队,插队不可能,饿死不存在。

它本质是读写锁,但只拿独占获取/释放这对函数用,效果等同互斥量:

PhAcquireQueuedLockExclusive(&lock); // ...临界区... PhReleaseQueuedLockExclusive(&lock);

事件:PH_EVENT 的惰性内核对象

PH_EVENT把“状态位 + 引用计数 + 内核事件句柄”塞进一个结构体:只有线程真正阻塞时,才在背后创建内核事件对象;平时测状态就是测PH_EVENT_SET这一个标志位,开销远低于一次内核调用。高频 set/test、低频阻塞的场景,这是最划算的形态。

Rundown 保护:对象销毁的“浇灭”

PH_RUNDOWN_PROTECT解决监控工具的典型难题:后台线程还在遍历列表,UI 线程却要销毁它。rundown 的语义是:允许已进入的读者继续跑完、拦住新读者进入、等全部读者退出后销毁方才能继续——先关闸,等店内最后一位顾客出门,再拉闸断电。

条件变量:直接建在 queued lock 上

PH_CONDITION就是_PH_QUEUED_LOCK的 typedef——锁即条件变量,无需额外同步状态。声明一个 queued lock 变量,PhPulseCondition/PhPulseAllCondition负责唤醒,PhWaitForCondition负责等待(二者都是宏,实际落到Phf前缀实现),带超时的版本是PhWaitForConditionEx。

Wake Event:轮询加唤醒的自定义阻塞

需要在循环里同时盯多个条件时,约定姿势是:循环内测试条件 →PhQueueWakeEvent/PhWaitForWakeEvent阻塞 → 条件被别的线程修改时调PhSetWakeEvent唤醒 →如果PhQueueWakeEvent之后发现条件其实已满足、不必阻塞,就改调PhSetWakeEvent撤销等待。PH_WAKE_EVENT同样是 queued lock 的 typedef,全程不产生内核事件对象。

异常篇:SEH 只留给三种时刻

常规错误处理只有返回值一条路。SEH 异常仅用于“无法轻易恢复”的情形(锁获取函数没能阻塞、对象引用计数为负等),且只放行三个特例:

  1. PhAllocate分配失败时抛异常。理由是分配失败罕见得足以让“每次分配都写检查分支”的额外成本不划算;确实需要防御的路径改用PhAllocateSafe——它带_Must_inspect_result_注解,强制调用方检查返回值,两者分工非常清晰;
  2. PhProbeAddress地址越界时抛异常。用异常表达探针失败,能让一个__try块覆盖多次地址范围检查,省掉每次检查一条分支;
  3. 程序员错误路径抛STATUS_NOT_IMPLEMENTED异常——那些本不应被走到的代码;此处用assert(FALSE)也 acceptable。

除此之外别加__try,查返回值就够了。

生命周期篇:从 Allocate 到 Dereference

基座是PhAllocate/PhFree;复杂对象一律交给引用计数系统:PhReferenceObject加一、PhDereferenceObject减一,归零即销毁;批量版PhReferenceObjects/PhDereferenceObjects对数组整体操作;PhDereferenceObjectDeferDelete把删除推迟执行(定义见 phlib/include/ref.h)。引用计数就是借阅台账:每次借出登记一笔,计数归零才真正还书。

不想手工记账的话,半自动机制兜底:PhAutoDereferenceObject(宏名PH_AUTO)把对象塞进当前线程的 auto-dereference 池,设计对标 Apple 的NSAutoreleasePool——对象在未来某个不确定的时刻自动解引用,但保证当前函数执行期间不被动。便捷宏PH_AUTO_T(PH_STRING, ...)即基于它。

两条铁律,违反是崩溃重灾区:

  • 指针一旦存入全局可见位置或传给另一个线程,必须先引用;其余本地短暂引用多数情况不必;
  • 传给函数的对象,整个调用期间必须至少持有一个引用。经典错误是窗口过程里留着可能已被销毁的引用并隐式使用它——窗口过程执行中仍可能泵消息(比如弹出对话框),所以窗口过程要第一时间引用对象。

动词语义精确分工,这张表最容易写出“看着对、语义错”的代码:

动词含义收尾配对
Allocate分配内存,不初始化Free
Create分配并初始化;引用计数体系下指经引用系统创建Free/Destroy/PhDereferenceObject
Free释放Allocate或Create产物—
Destroy同Free,用于Create产物—
Initialize用调用方提供的存储初始化Delete
Delete与Initialize配对,只清状态、不释放内存—

官方例子一一对应:PhAllocateFromFreeList/PhFreeToFreeList、PhCreateFileDialog/PhFreeFileDialog、PhInitializeWorkQueue/PhDeleteWorkQueue、PhCreateString/PhDereferenceObject。

计数的三种单位:Length恒为字节(PhCreateString要求字节长度)、Count为元素/字符(PhSubstring的长度)、Index亦为元素(PhSubstring的起点)。细节:向输出写 null 结尾字符串时,返回计数必须包含 null 终止符本身。

字符串对象PH_STRING:头部是一个 union——既可当PH_STRINGREF看,也可直接取Length(永远字节)与Buffer;数据区WCHAR Data[1]与头部连续分配,尾部另有 null 兼容传统 C 接口。操作面:PhCreateString(内联,内部按字节数转调PhCreateStringEx)、PhReferenceEmptyString取共享空串、PhConcatStrings/PhConcatStrings2拼接、PhFormatString格式化(格式串带_Printf_format_string_注解,分析器会替你核对参数)。

📌 记住这一点——不可变性边界:字符串只在“PhCreateString/PhCreateStringEx刚创建之后、指针尚未外传之前”允许本地修改;指针一旦交给别的函数或存入全局变量,立即冻结为只读。

PPH_STRING myString = PhCreateString(L"hello"); PPH_STRING title = PH_AUTO_T( PH_STRING, PhConcatStrings2(L"Process: ", myString->Buffer) );

例子里的title随函数结束自动进池解引用,省掉手写PhDereferenceObject的样板。

收尾:贡献者“下笔前自查清单”

提交 PR 前逐项过一遍:

  • 命名三种风格没有串门;公开符号带Ph/PH_前缀,文件私有函数是static且无前缀
  • 类型用定宽家族(ULONG、PWSTR等);布尔直接判断、不与TRUE比较
  • 参数全部带 SAL 注解;HRESULT 用HR_SUCCESS判断,全库搜不到SUCCEEDED
  • 失败路径保证不碰输出参数;回调体做了状态防护
  • 线程走PhCreateThread/PhQueueUserWorkItem,入口函数是统一签名
  • 同步用 queued lock(含条件变量、wake event);定义里没有volatile
  • 指针跨线程或入全局前先引用;窗口过程里第一时间引用对象
  • 动词配对正确:Create对Free/Dereference、Initialize对Delete;Length字节、Count/Index元素

清单用熟之后,“SystemInformer 式写法”就成肌肉记忆——这正是这套约定的目的:让几十万行代码的评审与协作,快如“一眼定归属”。

【免费下载链接】systeminformerA free, powerful, multi-purpose tool that helps you monitor system resources, debug software and detect malware. Brought to you by Winsider Seminars & Solutions, Inc. @ https://windows-internals.com项目地址: https://gitcode.com/GitHub_Trending/sy/systeminformer

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进