64位内核开发第十二讲,进程监视,ring3跟ring0事件同步.
一丶同步与互斥详解,以及实现一个进程监视软件.
1.用于线程同步的 KEVENT
事件很简单分别分为 事件状态. 以及事件类别.
事件状态:
有信号 Signaled
无信号 Non-signaled
事件类别
自动恢复 Synchronization 自动设置
不自动恢复. Notification 手动设置
事件的创建函数
** IoCreateNotificationEvent() **
** KeClearEvent() ** 设置为无信号状态
** KeResetEvent() ** 设置为无信号状态并返回之前的状态
** KeSetEvent()** 设置事件为有信号.
其实理解有信号跟无信号就可以了. 有信号了.我的等待函数才可以等待.
无信号就会阻塞在哪里.
事件的类别就是等待之后 根据你的设置.是自动设置为无信号. 还是不设置.
如果自动设置为无信号.那么下个线程来就会阻塞.直到我们设置为事件为有信号.才可以.
在内核中一般使用事件是使用匿名内核事件. 而不是使用IoCreateNotificationEvent
代码如下:
KEVENT myKevent;
KeInitializeEvent(&myKevent, NotificationEvent,FALSE);
//设置事件为有信号
KeSetEvent(&myKevent,IO_NO_INCREMENT,FALSE);
//等待事件
KeWaitForSingleObject(&myEvent,Executive,KernerMode,False,NULL);
//因为设置的类别是手动设置类别.所以我们自己要把事件信号设置为无信号
//调用两个都可以
KeResetEvent(&myEvent);
KeClearEvent(&myEvent);
2.进程监视 ring0 与 ring3同步使用Event
如果ring0跟ring3通讯.就要使用我们上面说的
ring0 -> ring3通讯的命名函数了.
IoCreateNotificationEvent
在使用Ring0 -> ring3通讯的时候.我们要了解下这个函数以及其它相关的知识
1.ring0 创建命名事件 - > ring3使用这个事件. 那么就需要建立一个名字了.名字如下;
** L"\\BaseNamedObjects\\你自定义的名字
**
2.再探 IoCreateDevice函数的作用.
IoCreateDevice 函数.众所周知.是建立设备对象.
定义如下:
NTSTATUS IoCreateDevice(
PDRIVER_OBJECT DriverObject,
ULONG DeviceExtensionSize, //设备扩展大小
PUNICODE_STRING DeviceName,
DEVICE_TYPE DeviceType,
ULONG DeviceCharacteristics,
BOOLEAN Exclusive,
PDEVICE_OBJECT *DeviceObject
);
我们先前所说.创建设备对象的时候.第二个参数是设备扩展大小.
我们一直给0.但是现在因为 ring0 - ring3通信. 需要我们自定义数据结构.进行存储ring0的数据. 那么可以使用这个设备扩展了.
如:
我们创建一个结构体. 将这个结构体的大小传入到第二个参数中.
使用的时候在我们创建的设备对象中.有一个成员.是s DeviceExtension.这个就是我们设备扩展为我们申请的那块内存.
我们可以转化为我们自定义结构体大小.
代码很简单.如下:
typedef struct _MYDEVICE_EXTENSION
{
//自定义数据
}MYDEVICE_EXTENSION,*PMYDEVICE_EXTENSION;
IoCreateDevice(DriverObject,sizeof(MYDEVICE_EXTENSION),....);
主要就是第二个参数.
使用:
PMYDEVICE_EXTENSION pMyDevice = (PMYDEVICE_EXTENSION)Device->DeviceExtension; 这个成员就指向我们扩展的内存.
强转为我们的指针即可.
pMyDevice->xxx = xxx;
pMyDevice->xxx = xxx;
最后使用内核创建事件 进行创建即可. IoCreateNotificationEvent
ring3想使用ring0下定义的Event很简单.
如下:
#define EVENT_NAME L"\\Global\\xxx"
HANDLE hEvent = OpenEventW(SYNCHRONIZE,FASE,EVENT_NAME);
while(WaitForSingleObject(hEvent,INFINITE))
{
发送 DeviceIoControl读取内核层的数据即可.(上面说的设备扩展数据)
}
ring3等待ring0的事件就很简单了. 直接打开事件.等待即可.
3.进程监视代码.
进程监视.首先会用到上面所说内容.然后分为下面几个步骤
1.创建设备对象.设备对象中扩展属性我们自定义结构体.传入结构体大小即可.
2.创建全局设备对象变量指针.保存创建的设备对象
3.创建符号链接,ring3 跟 ring 0进行通讯
4.创建控制派遣函数.接受ring3下发的控制吗.
5.使用IoCreateNotificationEvent创建事件对象.用于Ring3跟Ring0的事件同步.
6.注册进程控制回调函数.当进程创建.或者销毁会调用回调
7.回调函数,全局设备对象指针的子成员.指向我们自定义结构体.
转换一下. 赋值参数.并且设置事件对象
8.ring3读取数据的时候.控制函数将回调函数中赋值出来的数据拷贝给
ring3即可.
9.ring3进行打开事件.等待事件.发送DeviceIoControl控制吗.读取数据.显示 数据.
代码如下:
ring0:
#include <ntddk.h>
#include <ntstrsafe.h>
/*
符号连接名
设备对象名
事件等待名
*/
#define IBINARY_LINKNAME L"\\DosDevices\\IBinarySymboliclnk"
#define IBINARY_DEVICENAME L"\\Device\\IBinaryProcLook"
#define IBINARY_EVENTNAME L"\\BaseNamedObjects\\ProcLook"
//定义 ring0 ring3控制码
#define CTRLCODE_BASE 0x8000
#define MYCTRL_CODE(i) \
CTL_CODE(FILE_DEVICE_UNKNOWN,CTRLCODE_BASE +i,METHOD_BUFFERED,FILE_ANY_ACCESS)
#define IOCTL_PROCESS_LOCK_READ MYCTRL_CODE(1)
UNICODE_STRING g_uSymbolicLinkName = { 0 };
//控制派遣函数.以及卸载函数.
void DriverUnLoad(PDRIVER_OBJECT pDriverObject);
NTSTATUS InitDeviceAnSymbolicLink(
PDRIVER_OBJECT pDriverObj,
UNICODE_STRING uDeviceName,
UNICODE_STRING uSymbolicLinkName,
UNICODE_STRING uEventName);
NTSTATUS DisPatchComd(PDEVICE_OBJECT pDeviceObject, PIRP pIrp);
NTSTATUS DisPatchIoControl(PDEVICE_OBJECT pDeviceObject,PIRP pIrp);
VOID pMyCreateRoutine (IN HANDLE pParentId,HANDLE hProcessId,BOOLEAN bisCreate);
//自定义设备扩展.以及全局变量指针.进行保存的.
typedef struct _Device_Exten
{
/*
自定义数据.比如保存
进程PID 父PID
进程事件对象
全局事件对象
*/
HANDLE hProcess; //进程句柄
PKEVENT pkProcessEvent; //全局事件对象,ring3使用
HANDLE hProcessId; //进程的PID
HANDLE hpParProcessId; //父进程ID.当前你也可以有进程名字
BOOLEAN bIsCreateMark; //表示是创建进程还是销毁.创建进程回调可以看到
}DEVICE_EXTEN,* PDEVICE_EXTEN;
PDEVICE_OBJECT g_PDeviceObject;
//定义ring3->读取ring0的数据
typedef struct _PROCESS_LONNK_READDATA
{
HANDLE hProcessId;
HANDLE hpParProcessId;
BOOLEAN bIsCreateMark;
}PROCESS_LONNK_READDATA,*PPROCESS_LONNK_READDATA;
NTSTATUS
DriverEntry(
_In_ PDRIVER_OBJECT pDriverObject,
_In_ PUNICODE_STRING RegistryPath
)
{
NTSTATUS ntStatus;
UNICODE_STRING uDeviceName = { 0 };
UNICODE_STRING uEventName = { 0 };
//setp 1注册卸载函数,以及设置通讯方式
pDriverObject->DriverUnload = DriverUnLoad;
//setp 2 初始化符号链接名.设备名. 以及事件对象名字,并且检验一下
ntStatus = RtlUnicodeStringInit(&uDeviceName, IBINARY_DEVICENAME);
if (!NT_SUCCESS(ntStatus))
{
KdPrint(("初始化设备名称失败"));
return ntStatus;
}
KdPrint(("初始化设备名称成功"));
ntStatus = RtlUnicodeStringInit(&g_uSymbolicLinkName, IBINARY_LINKNAME);
if (!NT_SUCCESS(ntStatus))
{
KdPrint(("初始化符号链接名字失败"));
return ntStatus;
}
KdPrint(("初始化符号链接名字成功"));
ntStatus = RtlUnicodeStringInit(&uEventName, IBINARY_EVENTNAME);
if (!NT_SUCCESS(ntStatus))
{
KdPrint(("初始化全局事件对象失败"));
return ntStatus;
}
KdPrint(("初始化全局事件对象成功"));
//setp 3建立一个函数.函数内部进行初始化设备对象.初始化符号链接.初始化全局事件对象.
ntStatus = InitDeviceAnSymbolicLink(
pDriverObject,
uDeviceName,
g_uSymbolicLinkName,
uEventName);
return ntStatus;
}
//卸载驱动.关闭符号链接
void DriverUnLoad(PDRIVER_OBJECT pDriverObject)
{
NTSTATUS ntStatus;
UNICODE_STRING SymboLicLinkStr = { 0 };
ntStatus = RtlUnicodeStringInit(&SymboLicLinkStr, IBINARY_LINKNAME);
if (NT_SUCCESS(ntStatus))
{
ntStatus = IoDeleteSymbolicLink(&SymboLicLinkStr);
if (!NT_SUCCESS(ntStatus))
{
KdPrint(("删除符号链接失败"));
}
}
IoDeleteDevice(pDriverObject->DeviceObject);
PsSetCreateProcessNotifyRoutine(pMyCreateRoutine, TRUE);
KdPrint(("驱动已卸载"));
}
NTSTATUS InitDeviceAnSymbolicLink(
PDRIVER_OBJECT pDriverObj,
UNICODE_STRING uDeviceName,
UNICODE_STRING uSymbolicLinkName,
UNICODE_STRING uEventName)
{
NTSTATUS ntStatus;
PDEVICE_OBJECT pDeviceObject = NULL;
//使用自定义结构
ULONG i = 0;
ntStatus = IoCreateDevice(
pDriverObj,
sizeof(DEVICE_EXTEN),//使用设备扩展.指定大小.那么设备对象中成员就会指向这块内存
&uDeviceName,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE, //独占设备
&pDeviceObject);
if (!NT_SUCCESS(ntStatus))
{
KdPrint(("创建设备对象失败"));
IoDeleteDevice(pDeviceObject);
return ntStatus;
}
pDriverObj->Flags |= DO_BUFFERED_IO;
//成功之后保存到全局变量中
KdPrint(("创建设备对象成功"));
g_PDeviceObject = pDeviceObject;
//创建事件.ring3->ring0的事件
PDEVICE_EXTEN pDeviceExten = (PDEVICE_EXTEN)pDeviceObject->DeviceExtension;
pDeviceExten->pkProcessEvent = IoCreateNotificationEvent(&uEventName, &pDeviceExten->hProcess);
KeClearEvent(pDeviceExten->pkProcessEvent);
//创建符号链接
ntStatus = IoCreateSymbolicLink(&g_uSymbolicLinkName, &uDeviceName);
if (!NT_SUCCESS(ntStatus))
{
KdPrint(("创建符号链接失败"));
IoDeleteDevice(pDeviceObject);
return ntStatus;
}
KdPrint(("创建符号链接成功"));
/*
因为设备对象扩展我们传入了DEVICE_EXTEN大小.所以在调用IoCreateDevice的时候.返回的
设备对象中.设备对象会根据我们传入的大小创建一块内存.这块内存就保存在DeviceExtension
这个字段中.
下面调用IoCreateNotificationEvent是创建了一个命名事件.我们将事件放到我们结构体中.
这个函数创建的事件必须手工设置事件状态.所以我们首先初始化为无信号状态.
总的来说.IoCreateNotificationEvent创建的时候需要一个HANDLE以及一个PKEVENT.
*/
//注册回调控制函数.当进程来了会通知.
// PsSetCreateProcessNotifyRoutine
ntStatus = PsSetCreateProcessNotifyRoutine(pMyCreateRoutine,FALSE); //FASLE为注册
if (!NT_SUCCESS(ntStatus))
{
KdPrint(("注册系统回调失败"));
IoDeleteDevice(pDeviceObject);
return ntStatus;
}
KdPrint(("注册系统回调成功"));
//初始化派遣函数
for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
{
pDriverObj->MajorFunction[i] = DisPatchComd;
}
pDriverObj->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DisPatchIoControl;
return STATUS_SUCCESS;
}
//当进程来的时候.会通知你.
VOID pMyCreateRoutine(
IN HANDLE pParentId,
HANDLE hProcessId,
BOOLEAN bisCreate)
{
/*
进程来的时候会通知我们.所以我们给设备扩展进行赋值.赋值进程ID以及是否创建
*/
PDEVICE_EXTEN pDeviceExten =(PDEVICE_EXTEN)g_PDeviceObject->DeviceExtension;
pDeviceExten->hProcessId = hProcessId;
pDeviceExten->hpParProcessId = pParentId;
pDeviceExten->bIsCreateMark = bisCreate;
//赋值完毕之后.设置信号状态为有信号. 这样Ring3就会等待到事件了.
KeSetEvent(pDeviceExten->pkProcessEvent,0,FALSE);
//通知ring3可以读取了.那么还要设置为ring0的事件为无信号.用来保持同步
//KeClearEvent(pDeviceExten->pkProcessEvent);
KeResetEvent(pDeviceExten->pkProcessEvent); //跟ClearEvent一样.上面的快.这个会返回上一个设置的信号状态.都用一次
}
NTSTATUS DisPatchComd(PDEVICE_OBJECT pDeviceObject, PIRP pIrp)
{
pIrp->IoStatus.Information = 0;
pIrp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return pIrp->IoStatus.Status;
}
NTSTATUS DisPatchIoControl(PDEVICE_OBJECT pDeviceObject, PIRP pIrp)
{
/*
Ring3 -> ring0通讯的控制派遣函数.自定义控制码.获取Irp堆栈.获取缓冲区.
*/
NTSTATUS ntStatus;
PIO_STACK_LOCATION pIrpStack;
PVOID pUserOutPutBuffer;
PPROCESS_LONNK_READDATA pReadData;
ULONG uIoControl = 0;
ULONG uReadLength;
ULONG uWriteLeng;
PDEVICE_EXTEN pDeviceExten;
/*
开始解析用户操作
*/
KdPrint(("解析用户控制码"));
pIrpStack = IoGetCurrentIrpStackLocation(pIrp);
//从堆栈中获取用户控制数据
pUserOutPutBuffer = pIrp->AssociatedIrp.SystemBuffer; //如果控制码是缓冲区方式.就使用这个.
//定义读取的数据
pReadData = (PPROCESS_LONNK_READDATA)pUserOutPutBuffer;
//获取控制码.长度.进行读取
uIoControl = pIrpStack->Parameters.DeviceIoControl.IoControlCode;
uReadLength = pIrpStack->Parameters.DeviceIoControl.InputBufferLength;
uWriteLeng = pIrpStack->Parameters.DeviceIoControl.OutputBufferLength;
switch (uIoControl)
{
case IOCTL_PROCESS_LOCK_READ:
//拷贝数据即可.
pDeviceExten = (PDEVICE_EXTEN)g_PDeviceObject->DeviceExtension;
pReadData->hProcessId = pDeviceExten->hProcessId;
pReadData->hpParProcessId = pDeviceExten->hpParProcessId;
pReadData->bIsCreateMark = pDeviceExten->bIsCreateMark;
KdPrint(("内核读取 父ID = %d,子Id = %d,是否创建 = %d", (ULONG)pDeviceExten->hpParProcessId, (ULONG)pDeviceExten->hProcessId, (ULONG)pDeviceExten->bIsCreateMark));
break;
default:
KdPrint(("其它控制码"));
ntStatus = STATUS_INVALID_PARAMETER;
uWriteLeng = 0;
break;
}
pIrp->IoStatus.Information = uWriteLeng; //读取的字节数
pIrp->IoStatus.Status = ntStatus;
IoCompleteRequest(pIrp,IO_NO_INCREMENT);
return ntStatus;
}
ring3下打开事件对象即可. 注意我自己写的是打开 全局事件对象\\Global
然后发送控制码.ring0进行赋值即可.
ring3代码.
// ProcWatchClientConsole.cpp : Defines the entry point for the console application.
//
#include "windows.h"
#include "winioctl.h"
#include "stdio.h"
#define EVENT_NAME L"Global\\ProcLook"
#define CTRLCODE_BASE 0x8000
#define MYCTRL_CODE(i) \
CTL_CODE(FILE_DEVICE_UNKNOWN,CTRLCODE_BASE +i,METHOD_BUFFERED,FILE_ANY_ACCESS)
#define IOCTL_PROCESS_LOCK_READ MYCTRL_CODE(1)
#define IOCTL_PROCESS_LOCK_READ MYCTRL_CODE(1)
typedef struct _PROCESS_LONNK_READDATA
{
HANDLE hProcessId;
HANDLE hpParProcessId;
BOOLEAN bIsCreateMark;
}PROCESS_LONNK_READDATA, *PPROCESS_LONNK_READDATA;
int main(int argc, char* argv[])
{
PROCESS_LONNK_READDATA pmdInfoNow = { 0 };
PROCESS_LONNK_READDATA pmdInfoBefore = { 0 };
// 打开驱动设备对象
HANDLE hDriver = ::CreateFile(
"\\\\.\\IBinarySymboliclnk",
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hDriver == INVALID_HANDLE_VALUE)
{
printf("Open device failed:%x\n", GetLastError());
return -1;
}
// 打开内核事件对象
HANDLE hProcessEvent = ::OpenEventW(SYNCHRONIZE, FALSE, EVENT_NAME);
if (NULL == hProcessEvent)
{
OutputDebugString("打开事件对象失败");
system("pause");
return 0;
}
OutputDebugString("打开事件对象成功");
while (TRUE)
{
::WaitForSingleObject(hProcessEvent, INFINITE); //等待事件
DWORD dwRet = 0;
BOOL bRet = FALSE;
bRet = ::DeviceIoControl(
hDriver,
IOCTL_PROCESS_LOCK_READ,
NULL,
0,
&pmdInfoNow,
sizeof(pmdInfoNow),
&dwRet,
NULL);
if (!bRet)
{
OutputDebugString("发送控制码失败");
system("pause");
return 0;
}
OutputDebugString("Ring3发送控制码成功");
if (bRet)
{
if (pmdInfoNow.hpParProcessId != pmdInfoBefore.hpParProcessId || \
pmdInfoNow.hProcessId != pmdInfoBefore.hProcessId || \
pmdInfoNow.bIsCreateMark != pmdInfoBefore.bIsCreateMark)
{
if (pmdInfoNow.bIsCreateMark)
{
printf("进程创建 PID = %d\n", pmdInfoNow.hProcessId);
}
else
{
printf("进程退出,PID = %d\n", pmdInfoNow.hProcessId);
}
pmdInfoBefore = pmdInfoNow;
}
}
else
{
printf("Get Proc Info Failed!\n");
break;
}
}
::CloseHandle(hDriver);
system("pause");
return 0;
}
注意,ring0下设置的进程系统回调是用的 PsSetCreateProcessNotifyRoutine 这个内核函数只能监视
进程ID 父进程ID以及一个创建或者结束标记. 我们可以使用Ex系列.这样的话可以监视到进程的名字.等等.
演示
64位内核开发第十二讲,进程监视,ring3跟ring0事件同步.的更多相关文章
- 64位内核开发第十讲,IRQL中断级别了解
目录 中断级别IROL了解 一丶IRQL 1.了解什么是中断 2.IROL中断级别. 3.遵守IROL编程规范的方式 中断级别IROL了解 一丶IRQL 1.了解什么是中断 中断就是产生的一个电信号. ...
- 64位内核开发第二讲.内核编程注意事项,以及UNICODE_STRING
目录 一丶驱动是如何运行的 1.服务注册驱动 二丶Ring3跟Ring0通讯的几种方式 1.IOCTRL_CODE 控制代码的几种IO 2.非控制 缓冲区的三种方式. 三丶Ring3跟Ring0开发区 ...
- 64位内核开发第六讲,Windbg调试ring3跟Ring0.一起调试
目录 驱动第六讲_Windbg连续调试Ring3.与Ring0 一丶Windbg连调试 驱动第六讲_Windbg连续调试Ring3.与Ring0 一丶Windbg连调试 有时候我们调试一个程序.可以使 ...
- 64位内核开发第四讲,查看SSDT表与showSSDT表
目录 SSDt表与ShadowSSDT表的查看. 一丶SSDT表 1.什么是SSDT表 2.查看步骤 二丶ShadowSSDT表 1.什么是ShadowSSDT表 2.如何查看. 三丶工具介绍 SSD ...
- CG基础教程-陈惟老师十二讲笔记
转自 麽洋TinyOcean:http://www.douban.com/people/Tinyocean/notes?start=50&type=note 因为看了陈惟十二讲视频没有课件,边 ...
- Win7 64位系统,IE11,如何让IE的Tab强制运行64位内核?
有些人在使用TerraExplorer Pro 7版本进行web二次开发的时候,常会遇到下面截图中这样的问题, 这个问题主要是因为安装的TerraExplorer Pro 7 版本是64位的,而模型运 ...
- 【阿里聚安全·安全周刊】阿里双11技术十二讲直播预约|AWS S3配置错误曝光NSA陆军机密文件
关键词:阿里双11技术十二讲直播丨雪人计划丨亚马逊AWS S3配置错误丨2018威胁预测丨MacOS漏洞丨智能风控平台MTEE3丨黑客窃取<权利的游戏>剧本|Android 8.1 本 ...
- win7win8 64位汇编开发环境合集安装与设置
win7win8 64位汇编开发环境合集安装与设置 下载 win7 win8 64位汇编开发环境.rar 下载地址(免积分下载) http://download.csdn.net/detail/li ...
- 64位内核注冊tty设备
在64位系统中,注冊tty设备须要注意的是,Android跑在EL0而且在32位模式下,kernel跑在EL1而且在64位模式下,不但内核须要打开CONFIG_COMPAT选项,非常多android上 ...
随机推荐
- Lucene BooleanQuery中的Occur.MUST与Occur.Should
https://www.cnblogs.com/weipeng/archive/2012/04/18/2455079.html 1. 多个MUST的组合不必多说,就是交集 2. MUST和SH ...
- PDA日常问题
一.连接网络异常 1.摩托摩拉3190连接wifi时报错,提示:scan error adapter unavailable 确认网卡是不是禁用状态,CE是右下角有个蓝色的图,上面有个X,点一下,然后 ...
- ASP.NET Core & 双因素验证2FA 实战经验分享
必读 本文源码核心逻辑使用AspNetCore.Totp,为什么不使用AspNetCore.Totp而是使用源码封装后面将会说明. 为了防止不提供原网址的转载,特在这里加上原文链接: https:// ...
- Window 使用Nginx 部署 Vue 并把nginx设为windows服务开机自动启动
1.编译打包Vue项目 在终端输入 npm run build 进行打包编译.等待... 打包完成生成dist文件夹,这就是打包完成的文件. 我们先放着,进行下一步. 2下载Nginx 下载地址: h ...
- win10 idea启动Tomcat后控制台中文乱码
idea 配置文件新增如下配置 -Dfile.encoding=UTF-8 -Dconsole.encoding=UTF-8
- 浅谈Vue.js2.0某些概念
Vue.js2.0是一套构建用户界面的渐进式框架,目标是实现数据驱动和组件系统. A 渐进式框架 Vue.js是一个提供MVVM数据双向绑定的库,只专注于UI层面,这是它的核心.它本身没有解决SP ...
- 通过标签名获得td和tr
<tr node="123445"> <td> <input type=button name="dak"> </td ...
- C#中hashtable如何嵌套遍历
嵌套hashtable的遍历取值怎么做 hastable中嵌套了hashtable,想用递归的方式把所有hashtable中的key和value取出来 foreach (DictionaryEntry ...
- Scrapy 框架的使用
Scrapy 框架的介绍 Scrapy 是一个基于Twisted的异步处理框架,是纯Python实现的爬虫框架,其架构清晰模块之间的耦合成都低,可扩展性极强,可以灵活完成各种需求.我们只需要定制开发几 ...
- HDU-2204-Eddy's爱好-容斥求n以内有多少个数形如M^K
HDU-2204-Eddy's爱好-容斥求n以内有多少个数形如M^K [Problem Description] 略 [Solution] 对于一个指数\(k\),找到一个最大的\(m\)使得\(m^ ...