当前位置: 首页 > article >正文

第一篇 windows驱动之WinRing0.sys的开发及使用(电脑温度监控软件开发)

简介

  从鲁大师的温度监控谈起,现代的CPU、GPU等芯片一般都具有温度监控的功能,比如我们可能会遇到的电脑散热不好导致系统直接关机黑屏,就是cpu检测到当前温度高于额定温度执行的操作。

  鲁大师的温度监控模块就相当于把这些信息从底层硬件读取,然后通过上层UI显示给用户。其核心就是与硬件的通信驱动。现在win10的驱动都可以通过系统更新直接完成,不再需要安装流氓软件“驱动精灵”、“驱动人生”等。我一般只用到温度检测模块,其他功能于我都是鸡肋,为了一个很小的功能,我需要安装一个庞大的鲁大师程序,甚是不划算。故自己动手实现。本文的主题就是如何实现鲁大师的温度检测模块。

  封闭必将导致落后,开放才能共同进步。WinRing0.sys作为一款windows平台下与设备通信的驱动被广泛使用。却由开源成了闭源,由支持读写成了仅支持读。本文花费数个工作日将08年之前开源的文件进行学习整理。便于后期大家的学习和升级优化。

windows驱动预备知识

1.虚拟设备驱动程序

.vxd程序:虚拟设备驱动程序,主要是为了给dos程序提供一个虚拟硬件

dos程序:早于windows的程序,window 3.x时代为了发展就需要兼容这些程序。类似当前鸿蒙兼容安卓,都是为了钱,不磕碜。

.vxd程序:可以把.vxd程序理解为运行在第0级别的dll。

ring0:最高等级权限,可以为所欲为,直接操作硬件。

ring3:用户等级权限,可以调用user32.dll、NTDLL.dll等库间接控制硬件。

WinRing0.vxd:其目的并非要虚拟硬件,只是需要第0级别的权限。

总结一:.vxd程序最新也得win9的系统才能用到,以上当个科普,了解下就行。对应WingRing0驱动的学习聚焦到.sys文件。

2.设备驱动文件

.sys文件:设备驱动文件,常说的声卡、显卡、键盘驱动等,都需要开发这种文件。

.inf文件:驱动安装的配置文件,主要是写注册表,注册接口。必有参数是ClassGUID。

总结二:.sys文件也当做权限等级为0级的.dll文件就行了。.dll文件的使用不熟悉的可以网上了解下。

3.sys驱动使用

.签名:内核级别的驱动都需要微软公司认证过的秘钥进行签名才能直接安装到windows系统中。签名的秘钥文件后缀是.cer。在这里插入图片描述
注册:注册仅仅是完成在注册表里写一个项。下图中的小工具是专门用于内核驱动安装运行的。链接: 下载地址
在这里插入图片描述
在这里插入图片描述

启动:对应缺微软签名或微软认证过的秘钥进行签名的内核级驱动是无法直接启动起来的。这是由于系统对于涉及内核层级的驱动有签名校验导致,简单说就是没有健康码绿码的不能在小区活动。
在这里插入图片描述

禁用强制签名校验:通过修改bcdedit来取消掉这个限制。有点类似安卓手机安装APP的时候经常需要“允许安装未知来源的程序”。逐条执行以下命令即可(管理员权限):

"Win7系统"bcdedit.exe -set loadoptions DDISABLE_INTEGRITY_CHECKSbcdedit /set TESTSIGNING ONbcdedit /set nointegritychecks yes如果有失败项,一般需要在BIOS里面关闭安全启动启动按DEL键 进入BIOS 然后把 Secure Boot Policy 选项设置为关闭重启电脑
"Win10系统"
bcdedit.exe -set loadoptions DDISABLE_INTEGRITY_CHECKSbcdedit /set TESTSIGNING ONbcdedit /set nointegritychecks yes如果有失败,一般需要在BIOS里面关闭安全启动启动按DEL键 进入BIOS 然后把 Secure Boot Policy 选项设置为关闭重启电脑

代码分析

  通过以上的知识大致了解了windows驱动程序是啥,怎么用。接下来就进入较难的实战部分,写代码调用驱动获取有效信息。.sys驱动是用c语言写的,具体代码如下三部分。

1、驱动入口函数


NTSTATUS DriverEntry(	IN PDRIVER_OBJECT  DriverObject,	IN PUNICODE_STRING RegistryPath	)
{NTSTATUS		status;UNICODE_STRING  ntDeviceName;UNICODE_STRING  win32DeviceName;PDEVICE_OBJECT  deviceObject = NULL;RtlInitUnicodeString(&ntDeviceName, NT_DEVICE_NAME);status = IoCreateDevice(DriverObject,					// Our Driver Object0,								// We don't use a device extension&ntDeviceName,					// Device name OLS_TYPE,						// Device type  自定义的值FILE_DEVICE_SECURE_OPEN,		// Device characteristicsFALSE,							// Not an exclusive device 非排他性设备&deviceObject );				// Returned ptr to Device Objectif(!NT_SUCCESS(status)){refCount = (ULONG)-1;return status;}else{refCount = 0;}// Initialize the driver object with this driver's entry points.// 构造DriverObject这个结构体变量DriverObject->MajorFunction[IRP_MJ_CREATE] = OlsDispatch;// 1.注册函数,就是把函数指针告诉操作系统。当操作系统发送 IRP_MJ_CREATE 请求时,会调用这个函数DriverObject->MajorFunction[IRP_MJ_CLOSE] = OlsDispatch;// 2.DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = OlsDispatch;// 3.DriverObject->DriverUnload = Unload;//驱动卸载函数// Initialize a Unicode String containing the Win32 name for our device.RtlInitUnicodeString(&win32DeviceName, DOS_DEVICE_NAME);// Create a symbolic link between our device name  and the Win32 namestatus = IoCreateSymbolicLink(&win32DeviceName, &ntDeviceName);if (!NT_SUCCESS(status)){// Delete everything that this routine has allocated.IoDeleteDevice( deviceObject );}return status;
}

2、调度函数

  实际读取CPU信息的实现代码在此处,主要涉及两个参数,其一是:DEVICE_OBJECT ,其二是:I/O Request Packet (IRP) 。OlsDispatch()这个函数根据传进来的请求信息调用具体的执行函数。

/*++Routine Description:This routine is the dispatch handler for the driver.  It is responsiblefor processing the IRPs.Arguments:pDO - Pointer to device object.pIrp - Pointer to the current IRP.Return Value:STATUS_SUCCESS if the IRP was processed successfully, otherwise an errororindicating the reason for failure.--*/
NTSTATUS OlsDispatch( 	IN	PDEVICE_OBJECT pDO,	IN	PIRP pIrp	)
{PIO_STACK_LOCATION pIrpStack;NTSTATUS status;int index;//  Initialize the irp info field.//	  This is used to return the number of bytes transfered.pIrp->IoStatus.Information = 0;pIrpStack = IoGetCurrentIrpStackLocation(pIrp);//  Set default return statusstatus = STATUS_NOT_IMPLEMENTED;// Dispatch based on major fcn code.switch(pIrpStack->MajorFunction){case IRP_MJ_CREATE:if(refCount != (ULONG)-1){refCount++;}status = STATUS_SUCCESS;break;case IRP_MJ_CLOSE:if(refCount != (ULONG)-1){refCount--;}status = STATUS_SUCCESS;break;case IRP_MJ_DEVICE_CONTROL://  Dispatch on IOCTLswitch(pIrpStack->Parameters.DeviceIoControl.IoControlCode){case IOCTL_OLS_GET_DRIVER_VERSION:*(PULONG)pIrp->AssociatedIrp.SystemBuffer = OLS_DRIVER_VERSION;pIrp->IoStatus.Information = 4;status = STATUS_SUCCESS;break;case IOCTL_OLS_GET_REFCOUNT:*(PULONG)pIrp->AssociatedIrp.SystemBuffer = refCount;pIrp->IoStatus.Information = sizeof(refCount);status = STATUS_SUCCESS;break;case IOCTL_OLS_READ_MSR:status = ReadMsr(pIrp->AssociatedIrp.SystemBuffer,pIrpStack->Parameters.DeviceIoControl.InputBufferLength,pIrp->AssociatedIrp.SystemBuffer,pIrpStack->Parameters.DeviceIoControl.OutputBufferLength,(ULONG*)&pIrp->IoStatus.Information);break;case IOCTL_OLS_WRITE_MSR:status = WriteMsr(pIrp->AssociatedIrp.SystemBuffer,pIrpStack->Parameters.DeviceIoControl.InputBufferLength,pIrp->AssociatedIrp.SystemBuffer,pIrpStack->Parameters.DeviceIoControl.OutputBufferLength,(ULONG*)&pIrp->IoStatus.Information);break;case IOCTL_OLS_READ_PMC:status = ReadPmc(pIrp->AssociatedIrp.SystemBuffer,pIrpStack->Parameters.DeviceIoControl.InputBufferLength,pIrp->AssociatedIrp.SystemBuffer,pIrpStack->Parameters.DeviceIoControl.OutputBufferLength,(ULONG*)&pIrp->IoStatus.Information);break;case IOCTL_OLS_HALT:__halt();status = STATUS_SUCCESS;break;case IOCTL_OLS_READ_IO_PORT:case IOCTL_OLS_READ_IO_PORT_BYTE:case IOCTL_OLS_READ_IO_PORT_WORD:case IOCTL_OLS_READ_IO_PORT_DWORD:status = ReadIoPort(pIrpStack->Parameters.DeviceIoControl.IoControlCode,pIrp->AssociatedIrp.SystemBuffer,pIrpStack->Parameters.DeviceIoControl.InputBufferLength,pIrp->AssociatedIrp.SystemBuffer,pIrpStack->Parameters.DeviceIoControl.OutputBufferLength,(ULONG*)&pIrp->IoStatus.Information);break;case IOCTL_OLS_WRITE_IO_PORT:case IOCTL_OLS_WRITE_IO_PORT_BYTE:case IOCTL_OLS_WRITE_IO_PORT_WORD:case IOCTL_OLS_WRITE_IO_PORT_DWORD:status = WriteIoPort(pIrpStack->Parameters.DeviceIoControl.IoControlCode,pIrp->AssociatedIrp.SystemBuffer,pIrpStack->Parameters.DeviceIoControl.InputBufferLength,pIrp->AssociatedIrp.SystemBuffer,pIrpStack->Parameters.DeviceIoControl.OutputBufferLength,(ULONG*)&pIrp->IoStatus.Information);break;case IOCTL_OLS_READ_PCI_CONFIG:status = ReadPciConfig(pIrp->AssociatedIrp.SystemBuffer,pIrpStack->Parameters.DeviceIoControl.InputBufferLength,pIrp->AssociatedIrp.SystemBuffer,pIrpStack->Parameters.DeviceIoControl.OutputBufferLength,(ULONG*)&pIrp->IoStatus.Information);break;case IOCTL_OLS_WRITE_PCI_CONFIG:status = WritePciConfig(pIrp->AssociatedIrp.SystemBuffer,pIrpStack->Parameters.DeviceIoControl.InputBufferLength,pIrp->AssociatedIrp.SystemBuffer,pIrpStack->Parameters.DeviceIoControl.OutputBufferLength,(ULONG*)&pIrp->IoStatus.Information);break;case IOCTL_OLS_READ_MEMORY:status = ReadMemory(pIrp->AssociatedIrp.SystemBuffer,pIrpStack->Parameters.DeviceIoControl.InputBufferLength,pIrp->AssociatedIrp.SystemBuffer,pIrpStack->Parameters.DeviceIoControl.OutputBufferLength,(ULONG*)&pIrp->IoStatus.Information);break;case IOCTL_OLS_WRITE_MEMORY:status = WriteMemory(pIrp->AssociatedIrp.SystemBuffer,pIrpStack->Parameters.DeviceIoControl.InputBufferLength,pIrp->AssociatedIrp.SystemBuffer,pIrpStack->Parameters.DeviceIoControl.OutputBufferLength,(ULONG*)&pIrp->IoStatus.Information);break;}break;}// We're done with I/O request.  Record the status of the I/O action.pIrp->IoStatus.Status = status;// Don't boost priority when returning since this took little time.IoCompleteRequest(pIrp, IO_NO_INCREMENT);return status;
}

  下面的代码是具体每个case的实现部分,直接在注释里面做介绍:

//-----------------------------------------------------------------------------
//
// CPU
//
//-----------------------------------------------------------------------------NTSTATUS
ReadMsr(	void	*lpInBuffer, ULONG	nInBufferSize, void	*lpOutBuffer, ULONG	nOutBufferSize, ULONG	*lpBytesReturned)
{__try{//MSR是CPU的一组64位寄存器,可以分别通过RDMSR和WRMSR 两条指令进行读和写的操作。//__readmsr就是生成指令并执行。此函数仅在内核模式下可用,且对应程序仅可用作内部函数,//这也就是为啥需要Ring0权限的原因。ULONGLONG data = __readmsr(*(ULONG*)lpInBuffer);memcpy((PULONG)lpOutBuffer, &data, 8);*lpBytesReturned = 8;return STATUS_SUCCESS;}__except(EXCEPTION_EXECUTE_HANDLER){*lpBytesReturned = 0;return STATUS_UNSUCCESSFUL;}
}NTSTATUS
WriteMsr(	void	*lpInBuffer, ULONG	nInBufferSize, void	*lpOutBuffer, ULONG	nOutBufferSize, ULONG	*lpBytesReturned)
{__try{//与上面的__readmsr类似OLS_WRITE_MSR_INPUT* param;param = (OLS_WRITE_MSR_INPUT*)lpInBuffer;__writemsr(param->Register, param->Value.QuadPart);*lpBytesReturned = 0;return STATUS_SUCCESS;}__except(EXCEPTION_EXECUTE_HANDLER){*lpBytesReturned = 0;return STATUS_UNSUCCESSFUL;}
}NTSTATUS
ReadPmc(	void	*lpInBuffer, ULONG	nInBufferSize, void	*lpOutBuffer, ULONG	nOutBufferSize, ULONG	*lpBytesReturned)
{__try{   //pmc 性能计数器ULONGLONG data = __readpmc(*(ULONG*)lpInBuffer);memcpy((PULONG)lpOutBuffer, &data, 8);*lpBytesReturned = 8;return STATUS_SUCCESS;}__except(EXCEPTION_EXECUTE_HANDLER){*lpBytesReturned = 0;return STATUS_UNSUCCESSFUL;}
}
//-----------------------------------------------------------------------------
//
// IO Port IO口
//
//-----------------------------------------------------------------------------NTSTATUS
ReadIoPort( ULONG	ioControlCode,void	*lpInBuffer, ULONG	nInBufferSize, void	*lpOutBuffer, ULONG	nOutBufferSize, ULONG	*lpBytesReturned)
{ULONG nPort = *(ULONG*)lpInBuffer;switch(ioControlCode){case IOCTL_OLS_READ_IO_PORT_BYTE://从指定的端口地址读取单个字节*(PUCHAR)lpOutBuffer = READ_PORT_UCHAR((PUCHAR)(ULONG_PTR)nPort);break;case IOCTL_OLS_READ_IO_PORT_WORD://从指定的端口地址读取单个字*(PUSHORT)lpOutBuffer = READ_PORT_USHORT((PUSHORT)(ULONG_PTR)nPort);break;case IOCTL_OLS_READ_IO_PORT_DWORD://从指定的端口地址读取双字节*(PULONG)lpOutBuffer = READ_PORT_ULONG((PULONG)(ULONG_PTR)nPort);break;default:*lpBytesReturned = 0;return STATUS_INVALID_PARAMETER;break;}*lpBytesReturned = nInBufferSize;return STATUS_SUCCESS;
}NTSTATUS
WriteIoPort(ULONG	ioControlCode,void	*lpInBuffer, ULONG	nInBufferSize, void	*lpOutBuffer, ULONG	nOutBufferSize, ULONG	*lpBytesReturned)
{ULONG nPort;OLS_WRITE_IO_PORT_INPUT* param;param = (OLS_WRITE_IO_PORT_INPUT*)lpInBuffer;nPort = param->PortNumber;switch(ioControlCode){case IOCTL_OLS_WRITE_IO_PORT_BYTE:WRITE_PORT_UCHAR((PUCHAR)(ULONG_PTR)nPort, param->CharData);break;case IOCTL_OLS_WRITE_IO_PORT_WORD:WRITE_PORT_USHORT((PUSHORT)(ULONG_PTR)nPort, param->ShortData);break;case IOCTL_OLS_WRITE_IO_PORT_DWORD:WRITE_PORT_ULONG((PULONG)(ULONG_PTR)nPort, param->LongData);break;default:return STATUS_INVALID_PARAMETER;break;}return STATUS_SUCCESS;
}
//-----------------------------------------------------------------------------
//
// PCI
//
//-----------------------------------------------------------------------------NTSTATUS
ReadPciConfig(	void	*lpInBuffer, ULONG	nInBufferSize, void	*lpOutBuffer, ULONG	nOutBufferSize, ULONG	*lpBytesReturned)
{OLS_READ_PCI_CONFIG_INPUT *param;NTSTATUS status;if(nInBufferSize != sizeof(OLS_READ_PCI_CONFIG_INPUT)){return STATUS_INVALID_PARAMETER;}param = (OLS_READ_PCI_CONFIG_INPUT *)lpInBuffer;status = pciConfigRead(param->PciAddress, param->PciOffset,lpOutBuffer, nOutBufferSize);//最终调用的是HalGetBusDataByOffset,这是硬件抽象层函数,获取总线数据if(status == STATUS_SUCCESS){*lpBytesReturned = nOutBufferSize;}else{*lpBytesReturned = 0;}return status;
}NTSTATUS
WritePciConfig(	void *lpInBuffer, ULONG nInBufferSize, void *lpOutBuffer, ULONG nOutBufferSize, ULONG *lpBytesReturned){OLS_WRITE_PCI_CONFIG_INPUT *param;ULONG writeSize;NTSTATUS status;if(nInBufferSize < offsetof(OLS_WRITE_PCI_CONFIG_INPUT, Data)){return STATUS_INVALID_PARAMETER;}param = (OLS_WRITE_PCI_CONFIG_INPUT *)lpInBuffer;writeSize = nInBufferSize - offsetof(OLS_WRITE_PCI_CONFIG_INPUT, Data);*lpBytesReturned = 0;return pciConfigWrite(param->PciAddress, param->PciOffset,&param->Data, writeSize);}//-----------------------------------------------------------------------------
//
// Support Function
//
//-----------------------------------------------------------------------------NTSTATUS pciConfigRead(ULONG pciAddress, ULONG offset, void *data, int length)
{PCI_SLOT_NUMBER slot;int error;ULONG busNumber;busNumber = PciGetBus(pciAddress);slot.u.AsULONG = 0;slot.u.bits.DeviceNumber = PciGetDev(pciAddress);slot.u.bits.FunctionNumber = PciGetFunc(pciAddress);error =	HalGetBusDataByOffset(PCIConfiguration, busNumber, slot.u.AsULONG,data, offset, length);if(error == 0){return OLS_ERROR_PCI_BUS_NOT_EXIST;}else if(length != 2 && error == 2){return OLS_ERROR_PCI_NO_DEVICE;}else if(length != error){return OLS_ERROR_PCI_READ_CONFIG;}return STATUS_SUCCESS;
}NTSTATUS pciConfigWrite(ULONG pciAddress, ULONG offset, void *data, int length)
{PCI_SLOT_NUMBER slot;int error;ULONG busNumber;busNumber = PciGetBus(pciAddress);slot.u.AsULONG = 0;slot.u.bits.DeviceNumber = PciGetDev(pciAddress);slot.u.bits.FunctionNumber = PciGetFunc(pciAddress);error = HalSetBusDataByOffset(PCIConfiguration, busNumber, slot.u.AsULONG,data, offset, length);if(error != length){return OLS_ERROR_PCI_WRITE_CONFIG;}return STATUS_SUCCESS;
}
//-----------------------------------------------------------------------------
//
// Physical Memory
//
//-----------------------------------------------------------------------------NTSTATUS
ReadMemory(	void	*lpInBuffer, ULONG	nInBufferSize, void	*lpOutBuffer, ULONG	nOutBufferSize, ULONG	*lpBytesReturned)
{OLS_READ_MEMORY_INPUT *param;ULONG	size;PHYSICAL_ADDRESS address;PVOID	maped;BOOLEAN	error;if(nInBufferSize != sizeof(OLS_READ_MEMORY_INPUT)){return STATUS_INVALID_PARAMETER;}param = (OLS_READ_MEMORY_INPUT *)lpInBuffer;size = param->UnitSize * param->Count;if(nOutBufferSize < size){return STATUS_INVALID_PARAMETER;}address.QuadPart = param->Address.QuadPart;#ifndef _PHYSICAL_MEMORY_SUPPORTif(0x000C0000 > address.QuadPart || (address.QuadPart + size - 1) > 0x000FFFFF){return STATUS_INVALID_PARAMETER;}#endifmaped = MmMapIoSpace(address, size, FALSE);//把物理地址 转为虚拟地址,比如我们要读取物理地址为100的一个字节数据,//其通过系统被映射到虚拟内存的地址是200,那我们就通过读虚拟内存的200位置来实现error = FALSE;switch(param->UnitSize){case 1:READ_REGISTER_BUFFER_UCHAR(maped, lpOutBuffer, param->Count);//从虚拟地址里间接读物理地址的信息break;case 2:READ_REGISTER_BUFFER_USHORT(maped, lpOutBuffer, param->Count);break;case 4:READ_REGISTER_BUFFER_ULONG(maped, lpOutBuffer, param->Count);break;default:error = TRUE;break;}MmUnmapIoSpace(maped, size);if(error){return STATUS_INVALID_PARAMETER;}*lpBytesReturned = nOutBufferSize;return STATUS_SUCCESS;
}NTSTATUS
WriteMemory(void	*lpInBuffer, ULONG	nInBufferSize, void	*lpOutBuffer, ULONG	nOutBufferSize, ULONG	*lpBytesReturned)
{
#ifdef _PHYSICAL_MEMORY_SUPPORTOLS_WRITE_MEMORY_INPUT *param;ULONG size;PHYSICAL_ADDRESS address;PVOID	maped;BOOLEAN	error;if(nInBufferSize < offsetof(OLS_WRITE_MEMORY_INPUT, Data)){return STATUS_INVALID_PARAMETER;}param = (OLS_WRITE_MEMORY_INPUT *)lpInBuffer;size = param->UnitSize * param->Count;if (nInBufferSize < size + offsetof(OLS_WRITE_MEMORY_INPUT, Data)){return STATUS_INVALID_PARAMETER;}address.QuadPart = param->Address.QuadPart;maped = MmMapIoSpace(address, size, FALSE);error = FALSE;switch(param->UnitSize){case 1:WRITE_REGISTER_BUFFER_UCHAR(maped, (UCHAR*)&param->Data, param->Count);break;case 2:WRITE_REGISTER_BUFFER_USHORT(maped,(USHORT*)&param->Data, param->Count);break;case 4:WRITE_REGISTER_BUFFER_ULONG(maped,(ULONG*)&param->Data, param->Count);break;default:error = TRUE;break;}MmUnmapIoSpace(maped, size);if(error){return STATUS_INVALID_PARAMETER;}*lpBytesReturned = 0;return STATUS_SUCCESS;#else*lpBytesReturned = 0;return STATUS_INVALID_PARAMETER;#endif
}

3、驱动卸载函数

  实际的卸载工作被IRP_MN_REMOVE_DEVICE对应的派发函数处理了,不需要自己实现。这里的DriverUnload主要处理在DriverEntry中申请的内存,也就是“new”出的“win32NameString”。

/*++Routine Description:This routine is called by the I/O system to unload the driver.Any resources previously allocated must be freed.Arguments:DriverObject - a pointer to the object that represents our driver.Return Value:None
--*/
VOID Unload(	PDRIVER_OBJECT DriverObject	)
{PDEVICE_OBJECT deviceObject = DriverObject->DeviceObject;UNICODE_STRING win32NameString;PAGED_CODE();// Create counted string version of our Win32 device name.RtlInitUnicodeString(&win32NameString, DOS_DEVICE_NAME);// Delete the link from our device name to a name in the Win32 namespace.IoDeleteSymbolicLink(&win32NameString);if(deviceObject != NULL){IoDeleteDevice(deviceObject);}
}

革命尚未成功,以上只是.sys驱动的代码实现,主要目的是获取Ring0权限,为了给开发者使用,需要做一个DLL提供对外的接口。请参阅第二篇。。。

http://www.lryc.cn/news/2412791.html

相关文章:

  • 全新酷盒9.0多功能工具箱软件的最新iapp源码
  • Windows桌面软件美化界面:分享著名的VC++ DirectUI/duilib/SOUI/REDM,IMGUI和C#开源界面库
  • ROS常用命令及脚本
  • 新浪微博模拟登录
  • 十大局域网监控软件优选(2024最新排行榜)
  • 工作以来接触到的技术流
  • easeljs中的movieClip控件示例
  • Win11没有TPM2.0解决方法
  • jeesite配置指南(官方文档有坑,我把坑填了!)
  • 如何配置强缓存和协商缓存
  • shader理解
  • 全网最全的权限系统设计方案(图解)
  • 什么是API?(看完你就理解了)
  • stpcpy和stpncpy用法 strcpy和strncpy用法
  • strcat,strncat函数详解
  • Tomcat之点击startup.bat出现闪退的解决办法
  • opencv Scalar()的使用 心得
  • 【目标检测】39、一文看懂计算机视觉中的数据增强
  • NoSQL数据库Redis使用命令简介
  • textarea控件
  • 如何打开DMG文件
  • VMware和Ubuntu安装教程(2024年最新超详细!每个步骤都有)
  • 一文完全弄懂EndPoint组件
  • Linux之wc命令详解
  • Glide介绍及基本使用方法
  • trunc和date_trunc的区别
  • linux安装openoffice
  • PWA 入门教程
  • 容器化应用的救命稻草:K3s 备份和恢复中文指南
  • wampserver 64位是一款windows系统下的Apache+PHP+Mysql集成环境整合包