写这篇文章的灵感来于对Windows存储设备栈的分析,截获了SCSI命令。
分析思路
Windows存储设备栈布局
我们先看一张图(画的比例不好请见谅)

R3与ntdll.dll
在3环下,一个程序发起文件读写请求,被传递给ntdll.dll,ntdll.dll对请求进行包装,成为了IRP(I/O Request Package),IRP通过KiFastCallEntry进入内核,这些都很熟悉了。我们着重分析一下内核层。
R0下的各个模块
IO Manger
IO管理器接收来自用户态(ntdll.dll)的IRP,进行合法性校验和分发。也就是通过它的DriverDispatch向下传递IRP包,同时构建标准化的IRP结构,包含操作类型(如文件读写)、缓冲区指针等元数据。
MiniFilter
过滤驱动层位于文件系统驱动(FSD)之上,主动拦截所有IRP。这也是最常见的过滤驱动地点。可检测文件读写、进程创建等敏感操作。也可数据加密/解密,在IRP传递过程中透明处理数据(如BitLocker)。
ntfs.sys
文件系统驱动解析IRP中的文件路径,定位磁盘物理位置(簇映射)。管理元数据:NTFS主文件表($MFT)、权限、日志等。实现缓存机制:通过内存管理器(Mm)减少磁盘访问。
这之后,才是我们需要关注的设备
Storage Driver
存储类驱动(Storage Driver)通过SCSI端口接口控制特定类型设备(如CD-ROM统一驱动、磁盘统一驱动),将IRP转换为SRB+CDB(命令描述块),成为为SCSI设备请求。
SRB/CDB是什么?
SRB
SRB:SCSI请求块(协议传输载体),可以跨层通信,是在存储类驱动 → 端口驱动 → 微端口驱动间传递命令/数据的标准化容器。
举个简化示例:SRB的结构
typedef struct _SCSI_REQUEST_BLOCK {
USHORT Length; // SRB结构体长度
UCHAR Function; // 操作类型(如SRB_FUNCTION_EXECUTE_SCSI)
UCHAR SrbStatus; // 状态码(如SCSI_STATUS_SUCCESS)
PVOID DataBuffer; // 数据缓冲区指针(读写目标地址)
ULONG DataTransferLength; // 传输数据长度
ULONG TimeOutValue; // 超时时间(毫秒)
PVOID OriginalRequest; // 关联的原始IRP指针
PVOID NextSrb; // 队列中下一个SRB(支持链表)
// --- SCSI专属字段 ---
UCHAR PathId; // SCSI路径ID(多通道HBA)
UCHAR TargetId; // 目标设备ID
UCHAR Lun; // 逻辑单元号(LUN)
UCHAR CdbLength; // CDB长度(6/10/12/16字节)
UCHAR SenseInfoBuffer[18];// 感知数据缓冲区(错误详情)
UCHAR Cdb[16]; // 内嵌CDB命令块
} SCSI_REQUEST_BLOCK, *PSCSI_REQUEST_BLOCK;
CDB
命令描述块,内嵌在SRB中,封装SCSI标准操作指令(如读/写/查询),为变长指令。
举个简化示例:CDB的结构
typedef struct _CDB {
UCHAR OperationCode; // 操作码(决定命令类型和CDB长度)
UCHAR Flags; // 控制标志(如强制单元访问FUA)
ULONG LBA; // 逻辑区块地址(Little-Endian)
UCHAR TransferLength; // 传输区块数
UCHAR Control; // 附加控制位
} CDB, *PCDB;
典型CDB命令示例
READ(10) 0x28 28 00 AA AA AA AA 00 00 BB BB 读取LBA=0xAAAAAA, 长度0xBBBB区块
WRITE(12) 0xAA AA 00 00 00 LL LL LL LL 00 00 NN NN 写入LBA=0xLLLLLLLL, 长度0xNNNN区块
INQUIRY 0x12 12 00 00 00 LL 00 查询设备信息(返回LL字节)
现代CDB支持扩展操作码(如READ(16)的0x88),应对大容量设备。
SRB+CDB 协作流程

图说得很清楚了,我就不赘述了。
Storage Port Driver
存储端口驱动为所有Windows存储类驱动(如磁盘、磁带、DVD驱动)定义统一接口,提供硬件抽象,隔离Storage Driver与HBA(主机总线适配器)的硬件特性差异,同步管理同一HBA上所有设备的访问。
协议转换:
对SCSI设备:将SRB(SCSI请求块)直接传递至Storport微端口驱动(硬件专属)
对传统IDE/ATAPI/IEEE 1394总线:将SRB转换为适配器所需格式(如重组CDB/协议包)。
挂钩的设置
这是Windows存储设备栈的底部,也是我们拦截的地方。

我们想在Storage Port Driver上挂钩,设置Filter Driver。
(这里画的有点问题,Capture Driver应该在Storage Driver和Storage Port Driver之间,Capture还打错了。
看一下windbg的输出:

storahci.sys便是Storage Port Driver,也是我们挂钩的位置。
那么如何挂钩呢
答案在DriverObject->MajorFunction中

IRP_MJ_INTERNAL_DEVICE_CONTROL
Storage Driver和Storage Port Driver之间通过DriverDispatchXxx联系,所以只要修改MajorFunction的地址我们就可以截获SRB请求。
经过调试得知,二者通过IRP_MJ_INTERNAL_DEVICE_CONTROL联系,所以只需修改MajorFunction 的IRP_MJ_INTERNAL_DEVICE_CONTROL即可。
WinDbg调试
首先打个断点看一下调用上下文,这是Call到MajorFunction的IRP_MJ_INTERNAL_DEVICE_CONTROL的上下文:


NTSTATUS __fastcall DispatchXxx(
PDEVICE_OBJECT DeviceObject,
PIRP Irp
)
{
RCX:DeviceObject
RDX:Irp
}
根据fastcall调用协议,rdx是指向IRP的指针,所以我们解析IRP即可。
根据Windows文档,SRB块的指针位于CurrentStackLoaction中,而CurrentStackLoaction在DeviceObject中,具体见下(也可看后面的代码):
2:kd> dt _IRP @RDX
nt!_IRP
+0x000 Type :0n6
+0x002 Size :0x1f0
+0x004 AllocationProcessorNumber :0
+0x006 Reserved :0
+0x008 MdlAddress :0xffffa68d`f25f9340 _MDL
+0x010 Flags :0
+0x018 AssociatedIrp :<anonymous-tag>
+0x020 ThreadListEntry :_LIST_ENTRY [ 0xffffa68d`f2b28e00 -0xffffa68d`f2b28e00 ]
+0x030 IoStatus :_IO_STATUS_BLOCK
+0x040 RequestorMode :0 ''
+0x041 PendingReturned :0 ''
+0x042 StackCount :3 ''
+0x043 CurrentLocation :3 ''
+0x044 Cancel :0 ''
+0x045 CancelIrql :0 ''
+0x046 ApcEnvironment :0 ''
+0x047 AllocationFlags :0x44 'D'
+0x048 UserIosb :(null)
+0x050 UserEvent :(null)
+0x058 Overlay :<anonymous-tag>
+0x068 CancelRoutine :(null)
+0x070 UserBuffer :(null)
+0x078 Tail :<anonymous-tag>
2:kd> dx -id 0,0,ffffa68df247f0c0 -r1 (*((ntkrnlmp!_IRP *)0xffffa68df2b28de0)).Tail
(*((ntkrnlmp!_IRP *)0xffffa68df2b28de0)).Tail [Type:<anonymous-tag>]
[+0x000] Overlay [Type:<anonymous-tag>]
[+0x000] Apc [Type:_KAPC]
[+0x000] CompletionKey :0x0 [Type:void *]
2:kd> dx -r1 (*((ntkrnlmp!_IRP *)0xffffa68df2b28de0)).Tail.Overlay
(*((ntkrnlmp!_IRP *)0xffffa68df2b28de0)).Tail.Overlay [Type:<anonymous-tag>]
[+0x000] DeviceQueueEntry [Type:_KDEVICE_QUEUE_ENTRY]
[+0x000] DriverContext [Type:void * [4]]
[+0x020] Thread :0x0 [Type:_ETHREAD *]
[+0x028] AuxiliaryBuffer :0x0 [Type:char *]
[+0x030] ListEntry [Type:_LIST_ENTRY]
[+0x040] CurrentStackLocation : 0xffffa68df2b28f40 :IRP_MJ_INTERNAL_DEVICE_CONTROL / 0x0 for Device for "\Driver\storahci" [Type:_IO_STACK_LOCATION *]
[+0x040] PacketType :0xf2b28f40 [Type:unsigned long]
[+0x048] OriginalFileObject :0x0 [Type:_FILE_OBJECT *]
[+0x050] IrpExtension :0xffffa68df4ec6aa0 [Type:void *]
2:kd> dx -r1 ((ntkrnlmp!_IO_STACK_LOCATION *)0xffffa68df2b28f40)
((ntkrnlmp!_IO_STACK_LOCATION *)0xffffa68df2b28f40) : 0xffffa68df2b28f40 :IRP_MJ_INTERNAL_DEVICE_CONTROL / 0x0 for Device for "\Driver\storahci" [Type:_IO_STACK_LOCATION *]
[<Raw View>] [Type:_IO_STACK_LOCATION]
Device : 0xffffa68debde6050 :Device for "\Driver\storahci" [Type:_DEVICE_OBJECT *]
File :0x0 [Type:_FILE_OBJECT *]
CompletionRoutine : 0xfffff8047af260f0 :0xfffff8047af260f0 [Type:long (__cdecl*)(_DEVICE_OBJECT *,_IRP *,void *)]
2:kd> dx -r1 -nv (*((ntkrnlmp!_IO_STACK_LOCATION *)0xffffa68df2b28f40))
(*((ntkrnlmp!_IO_STACK_LOCATION *)0xffffa68df2b28f40)) : IRP_MJ_INTERNAL_DEVICE_CONTROL / 0x0 for Device for "\Driver\storahci" [Type:_IO_STACK_LOCATION]
[+0x000] MajorFunction :0xf [Type:unsigned char]
[+0x001] MinorFunction :0x0 [Type:unsigned char]
[+0x002] Flags :0x10 [Type:unsigned char]
[+0x003] Control :0xe0 [Type:unsigned char]
[+0x008] Parameters [Type:<anonymous-tag>]
[+0x028] DeviceObject : 0xffffa68debde6050 :Device for "\Driver\storahci" [Type:_DEVICE_OBJECT *]
[+0x030] FileObject :0x0 [Type:_FILE_OBJECT *]
[+0x038] CompletionRoutine : 0xfffff8047af260f0 :0xfffff8047af260f0 [Type:long (__cdecl*)(_DEVICE_OBJECT *,_IRP *,void *)]
[+0x040] Context :0xffffa68df5406b90 [Type:void *]
2:kd> dx -r1 (*((ntkrnlmp!_IO_STACK_LOCATION *)0xffffa68df2b28f40)).Parameters
(*((ntkrnlmp!_IO_STACK_LOCATION *)0xffffa68df2b28f40)).Parameters [Type:<anonymous-tag>]
[+0x000] Create [Type:<anonymous-tag>]
[+0x000] CreatePipe [Type:<anonymous-tag>]
[+0x000] CreateMailslot [Type:<anonymous-tag>]
[+0x000] Read [Type:<anonymous-tag>]
[+0x000] Write [Type:<anonymous-tag>]
[+0x000] QueryDirectory [Type:<anonymous-tag>]
[+0x000] NotifyDirectory [Type:<anonymous-tag>]
[+0x000] NotifyDirectoryEx [Type:<anonymous-tag>]
[+0x000] QueryFile [Type:<anonymous-tag>]
[+0x000] SetFile [Type:<anonymous-tag>]
[+0x000] QueryEa [Type:<anonymous-tag>]
[+0x000] SetEa [Type:<anonymous-tag>]
[+0x000] QueryVolume [Type:<anonymous-tag>]
[+0x000] SetVolume [Type:<anonymous-tag>]
[+0x000] FileSystemControl [Type:<anonymous-tag>]
[+0x000] LockControl [Type:<anonymous-tag>]
[+0x000] DeviceIoControl [Type:<anonymous-tag>]
[+0x000] QuerySecurity [Type:<anonymous-tag>]
[+0x000] SetSecurity [Type:<anonymous-tag>]
[+0x000] MountVolume [Type:<anonymous-tag>]
[+0x000] VerifyVolume [Type:<anonymous-tag>]
[+0x000] Scsi [Type:<anonymous-tag>]
[+0x000] QueryQuota [Type:<anonymous-tag>]
[+0x000] SetQuota [Type:<anonymous-tag>]
[+0x000] QueryDeviceRelations [Type:<anonymous-tag>]
[+0x000] QueryInterface [Type:<anonymous-tag>]
[+0x000] DeviceCapabilities [Type:<anonymous-tag>]
[+0x000] FilterResourceRequirements [Type:<anonymous-tag>]
[+0x000] ReadWriteConfig [Type:<anonymous-tag>]
[+0x000] SetLock [Type:<anonymous-tag>]
[+0x000] QueryId [Type:<anonymous-tag>]
[+0x000] QueryDeviceText [Type:<anonymous-tag>]
[+0x000] UsageNotification [Type:<anonymous-tag>]
[+0x000] WaitWake [Type:<anonymous-tag>]
[+0x000] PowerSequence [Type:<anonymous-tag>]
[+0x000] Power [Type:<anonymous-tag>]
[+0x000] StartDevice [Type:<anonymous-tag>]
[+0x000] WMI [Type:<anonymous-tag>]
[+0x000] Others [Type:<anonymous-tag>]
2:kd> dx -r1 (*((ntkrnlmp!_IO_STACK_LOCATION *)0xffffa68df2b28f40)).Parameters.Others
(*((ntkrnlmp!_IO_STACK_LOCATION *)0xffffa68df2b28f40)).Parameters.Others [Type:<anonymous-tag>]
[+0x000] Argument1 :0xffffa68df2c556b0 [Type:void *]
[+0x008] Argument2 :0x0 [Type:void *]
[+0x010] Argument3 :0x0 [Type:void *]
[+0x018] Argument4 :0x0 [Type:void *]
这里的Argument1便是指向SRB的指针,srb的结构定义在windbg 中可以看到,CDB的位置在偏移的0x048处:
2:kd> dt storport!_SCSI_REQUEST_BLOCK
+0x000 Length :Uint2B
+0x002 Function :UChar
+0x003 SrbStatus :UChar
+0x004 ScsiStatus :UChar
+0x005 PathId :UChar
+0x006 TargetId :UChar
+0x007 Lun :UChar
+0x008 QueueTag :UChar
+0x009 QueueAction :UChar
+0x00a CdbLength :UChar
+0x00b SenseInfoBufferLength :UChar
+0x00c SrbFlags :Uint4B
+0x010 DataTransferLength :Uint4B
+0x014 TimeOutValue :Uint4B
+0x018 DataBuffer :Ptr64 Void
+0x020 SenseInfoBuffer :Ptr64 Void
+0x028 NextSrb :Ptr64 _SCSI_REQUEST_BLOCK
+0x030 OriginalRequest :Ptr64 Void
+0x038 SrbExtension :Ptr64 Void
+0x040 InternalStatus :Uint4B
+0x040 QueueSortKey :Uint4B
+0x040 LinkTimeoutValue :Uint4B
+0x044 Reserved :Uint4B
+0x048 Cdb : [16] UChar
于是乎,可以这样解析得到CDB地址:
CurrentStackLocation=DeviceObject->Tail.OverLay.CurrentStackLocation
SRB=CurrentStackLocation->Parameters.Others.Argument1
CDB=SRB+0x048
现在,在Windbg里看一下CDB:

解析一下这条CDB命令:
28 00 03 55 67 ba 00 00 40 00 00 00 00 00 00 00 90 99 fc e6 8e e1
28 00 03 55 67 ba 00 00 40 00 <---有效部分
Byte 0:0x28 表示 READ(10) 命令。用于从指定逻辑块地址 (LBA) 开始读取数据块。
Byte 1:这是一个位掩码字段,包含控制和保护标志。值 0x00 表示所有标志未设置。
Bytes 2-5 (Logical Block Address ): LBA 是一个 32 位无符号整数,表示读取起始地址(块号)。
计算:0x035567BA(大端序)。从逻辑块地址 55928762 开始读取数据。
Byte 6 (Group Number): 组号字段,通常保留,必须为 0。无特殊功能。
Bytes 7-8 :传输长度是一个 16 位无符号整数,表示要读取的数据块数量。
计算:0x0040(大端序)。含义:读取 64 个连续数据块。每个块大小由设备定义(通常为 512 字节或 4K 字节),需结合设备参数确定总字节数。
Byte 9 (Control): 控制字段,通常用于链接、队列或供应商特定选项。值 0x00 表示:
成功!我们解析了CDB!
编写驱动
最重要的DriverDispatch函数
首先,这个DriverDispatch应该有两个功能,一是分析SRB,二是调用源函数。
微软文档解释道这个级别的驱动程序不应调用系统例程(特别是文件系统例程,Io开头的),这个在我看来可能是死锁问题。
例如,我在DriverDispatch调用了NtWriteFile,而NtWriteFile又会调用我的DriverDispatch,造成死锁。
但是DbgPrint应该没什么事。
直接看代码解释吧,在DriveEntry中完成了对原函数地址有效性的检验,所以这里不用检验地址。
在这个示例中,不需对传入的参数进行检验,因为这是被修改的原函数内部完成的, 我们自己校验可能还会画蛇添足。
无论处理结果如何,都要将SRB传递。
NTSTATUS DispatchSCSI(
PDEVICE_OBJECT pDeviceObject,
PIRP pIrp
){
PIO_STACK_LOCATION pStackLocation = NULL;
PVOID pSrb = NULL;
NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
// CDB is in the CurrentStackLocation
// We need to transform Irp first
pStackLocation = pIrp->Tail.Overlay.CurrentStackLocation;
if (pStackLocation != NULL) {
//CDB is here
pSrb = pStackLocation->Parameters.Others.Argument1;
if (pSrb != NULL) {
ReadCdb(pSrb);
}
}
// the function is defined as __fastcall convention
status = DispatchRoutine(pDeviceObject, pIrp, gFuctionAddress);
return status;
}
}
DispatchRoutine是用汇编写的
DispatchRoutine proc
mov rax, r8
mov r8, 0H
jmp rax
ret
DispatchRoutine endp
辅助函数
首先看修改MajorFunction的函数,在多核系统上还是挂一个锁比较好:
BOOLEAN WriteDriverMajorFunction(
_In_ PDRIVER_OBJECT TargetDriver,
_In_ PDRIVER_OBJECT SourceDriver,
_Out_ PVOID* pOriginalFunctionAddr
)
{
DbgBreakPoint();
if (TargetDriver == NULL ||
SourceDriver == NULL)
{
DbgPrint("Invalid parameters\n");
return FALSE;
}
// Check whether the function is valid
if (TargetDriver->MajorFunction == NULL ||
SourceDriver->MajorFunction == NULL)
{
DbgPrint("Driver has no MajorFunction table\n");
return FALSE;
}
// Read the pointer
PDRIVER_DISPATCH pTargetFn = TargetDriver->MajorFunction[IRP_MJ_INTERNAL_DEVICE_CONTROL];
PDRIVER_DISPATCH pSourceFn = SourceDriver->MajorFunction[IRP_MJ_INTERNAL_DEVICE_CONTROL];
if (pTargetFn == NULL || pSourceFn == NULL)
{
DbgPrint("IRP_MJ_INTERNAL_DEVICE_CONTROL handler missing\n"
"Target: %p, Source: %p\n",
pTargetFn, pSourceFn);
return FALSE;
}
// Acquire a lock to exchage pointer
PDRIVER_DISPATCH pOriginal = (PDRIVER_DISPATCH)InterlockedExchangePointer(
(PVOID volatile*)&TargetDriver->MajorFunction[IRP_MJ_INTERNAL_DEVICE_CONTROL],
pSourceFn
);
*pOriginalFunctionAddr = (PVOID)pOriginal;
return TRUE;
}
那么,如何遍历设备栈获得底层设备呢?
我们可以利用DeviceObject的AttachTo指针遍历:
BOOLEAN GetLowestStackDevice(
_In_ PDEVICE_OBJECT CurrentDevice,
_Out_ PDEVICE_OBJECT* LowestStackDevice,
_Out_ PDRIVER_OBJECT* LowestStackDriver
)
{
//DbgBreakPoint();
BOOLEAN bstatus = FALSE;
if (KeGetCurrentIrql() != PASSIVE_LEVEL) {
DbgPrint("Must be called at PASSIVE_LEVEL\n");
return bstatus;
}
if (CurrentDevice == NULL) {
DbgPrint("Empty CurrentDevice!,check it\n");
return bstatus;
}
PDEVICE_OBJECT currentDevice = CurrentDevice;
do {
if (!currentDevice->DeviceObjectExtension ||
!currentDevice->DeviceObjectExtension->AttachedTo)
{
break;
}
currentDevice = currentDevice->DeviceObjectExtension->AttachedTo;
} while (1);
if (!currentDevice->DriverObject) {
DbgPrint("Lowest device has no DriverObject\n");
return FALSE;
}
*LowestStackDevice = currentDevice;
*LowestStackDriver = currentDevice->DriverObject;
ObReferenceObject(currentDevice);
ObReferenceObject(currentDevice->DriverObject);
bstatus = TRUE;
return bstatus;
}
遍历的起点是disk.sys,因为这个起点在绝大多数Windows上适用:
通过ObReferenceObjectByName打开disk.sys
BOOLEAN OpenDeviceObjectByDriverName(
_In_ PUNICODE_STRING DriverName,
_Out_ PDEVICE_OBJECT* DeviceObject
)
{
UNICODE_STRING driverName;
PDEVICE_OBJECT pDeviceObject = NULL;
PDRIVER_OBJECT pDriverObject = NULL;
NTSTATUS status = 0;
BOOLEAN bstatus = FALSE;
driverName = *DriverName;
status = ObReferenceObjectByName(
&driverName,
OBJ_CASE_INSENSITIVE,
NULL,
FILE_ALL_ACCESS,
*IoDriverObjectType,
KernelMode,
NULL,
(PVOID*)&pDriverObject
);
if (!NT_SUCCESS(status)) {
DbgPrint("Failed in ObReferenceObjectByName,status: 0x%X\n", status);
return bstatus;
}
pDeviceObject = pDriverObject->DeviceObject;
if (pDeviceObject != NULL)
{
ObReferenceObject(pDeviceObject);
*DeviceObject = pDeviceObject;
bstatus = TRUE;
}
else {
DbgPrint("Driver has no associated device object\n");
}
ObDereferenceObject(pDriverObject);
return bstatus;
}
使用ObReferenceObjectByName应注意
extern POBJECT_TYPE* IoDriverObjectType;
NTKERNELAPI
NTSTATUS
ObReferenceObjectByName(
IN PUNICODE_STRING ObjectName,
IN ULONG Attributes,
IN PACCESS_STATE PassedAccessState OPTIONAL,
IN ACCESS_MASK DesiredAccess OPTIONAL,
IN POBJECT_TYPE ObjectType,
IN KPROCESSOR_MODE AccessMode,
IN OUT PVOID ParseContext OPTIONAL,
OUT PVOID* Object
);
把以上补充在头文件中,然后是解析CDB中的LBA
BOOLEAN ReadCdb(
_In_ PVOID SrbAddress
)
{
PUCHARcdb = (PUCHAR)SrbAddress + CDB_OFFSET_IN_SRB;
UCHARopcode = cdb[0];
ULONGLONGlba =0;
// As a demonstration, this only solve READ_12 Command
// You can add others
if (opcode != 0x28) { // READ_12
return FALSE;
}
lba = (cdb[2] << 24) | (cdb[3] << 16) | (cdb[4] << 8) | cdb[5];
DbgPrint("LBA: 0x%08X\n", (ULONG_PTR)lba);
return TRUE;
}
作为示例,这里只解析了READ_12命令,你也可以添加对其他命令的处理,如对Inquiry的分析。
效果

写到这里,突然想到一些ARK不是支持通过SCSI命令解析磁盘信息吗,这个理论上可以拦截,但也没试过。
总结
为什么只解析CDB而非源文件
从CDB逆推文件极为困难,首先是一个磁盘可以有多个卷,文件系统各不一样,即使只有一个文件系统,以NTFS为例,MFT到LCN再到LBA可以完成,但是LBA逆推到MFT就涉及所有文件的映射,这个映射的大小堪比页表,甚至远大于页表,而且需要实时更新,所以这种方法对内存和CPU的开销都很大。
所以呢,这个项目的示例意义大于实际意义,是一个实验性质的项目。
关于附的代码(点击“阅读原文”查看/下载)
这份代码的健壮性是很差的,其只考虑SATA硬盘等典型情况,而对NVMe硬盘和PnP设备完全未考虑,正因如此,这份代码只作为示例实现,不可直接应用于生产环境。
同时,对系统底层的修改会带来极大的不稳定性,应该谨慎使用。至于PG,我进行了三次5min的测试,均未蓝屏。这份代码仅作为研究使用。
看雪ID:TurkeybraNC
https://bbs.kanxue.com/user-home-982720.htm
# 往期推荐
球分享
球点赞
球在看
点击阅读原文查看更多

