4.3 Windows驱动开发:监控进程与线程对象操作

在内核中,可以使用ObRegisterCallbacks这个内核回调函数来实现监控进程和线程对象操作。通过注册一个OB_CALLBACK_REGISTRATION回调结构体,可以指定所需的回调函数和回调的监控类型。这个回调结构体包含了回调函数和监控的对象类型,还有一个Altitude字段,用于指定回调函数的优先级。优先级越高的回调函数会先被调用,如果某个回调函数返回了一个非NULL值,后续的回调函数就不会被调用。

当有进程或线程对象创建、删除、复制或重命名时,内核会调用注册的回调函数。回调函数可以访问被监控对象的信息,如句柄、进程ID等,并可以采取相应的操作,如打印日志、记录信息等。

首先我们先来解释一下OB_CALLBACK_REGISTRATION结构体,OB_CALLBACK_REGISTRATION结构体是用于向内核注册回调函数的结构体,其中包含了回调函数和监控的对象类型等信息。

OB_CALLBACK_REGISTRATION结构体的定义:

typedef struct _OB_CALLBACK_REGISTRATION {
    PVOID RegistrationContext;
    POB_OPERATION_REGISTRATION OperationRegistration;
} OB_CALLBACK_REGISTRATION, *POB_CALLBACK_REGISTRATION;

其中,RegistrationContext是一个指针,指向一个可以在回调函数中访问的上下文数据结构,可以用来传递一些参数或状态信息。OperationRegistration是一个指向OB_OPERATION_REGISTRATION结构体的指针,该结构体指定了回调函数的相关信息,包括回调函数的地址、监控的对象类型、回调函数的优先级等。

OB_OPERATION_REGISTRATION结构体的定义如下:

typedef struct _OB_OPERATION_REGISTRATION {
    POBJECT_TYPE ObjectType;
    OB_OPERATION Operations;
    POB_PRE_OPERATION_CALLBACK PreOperation;
    POB_POST_OPERATION_CALLBACK PostOperation;
} OB_OPERATION_REGISTRATION, *POB_OPERATION_REGISTRATION;

其中,ObjectType是一个指针,指向要监控的对象类型的OBJECT_TYPE结构体。

Operations是一个枚举类型,表示要监控的操作类型,可以是如下之一:

  • OB_OPERATION_HANDLE_CREATE:创建对象句柄
  • OB_OPERATION_HANDLE_DUPLICATE:复制对象句柄
  • OB_OPERATION_HANDLE_CLOSE:关闭对象句柄
  • OB_OPERATION_HANDLE_WAIT:等待对象句柄
  • OB_OPERATION_HANDLE_SET_INFORMATION:设置对象句柄信息
  • OB_OPERATION_HANDLE_QUERY_INFORMATION:查询对象句柄信息
  • OB_OPERATION_HANDLE_OPERATION:其他操作

PreOperation和PostOperation分别是指向回调函数的指针,用于指定在进行指定操作之前和之后要执行的回调函数。这两个回调函数的参数和返回值等信息可以参考Microsoft官方文档。

我们以创建一个简单的监控进程对象为例,实现一个自己的进程回调函数MyObjectCallBack()当有新进程被加载时,自动路由到我们自己的回调中来;

首先在驱动程序入口处,定义Base结构,并初始化Base.ObjectTypePsProcessType标志着用于监控进程,接着填充Base.Operations并初始化为OB_OPERATION_HANDLE_CREATE表示当有进程句柄操作时触发回调,最后填充Base.PreOperation在回调前触发执行MyObjectCallBack自己的回调,当结构体被填充好以后,直接调用ObRegisterCallbacks()向内核申请回调事件即可,这段代码实现如下所示;

#include <ntddk.h>
#include <ntstrsafe.h>

PVOID Globle_Object_Handle;

OB_PREOP_CALLBACK_STATUS MyObjectCallBack(PVOID RegistrationContext, POB_PRE_OPERATION_INFORMATION OperationInformation)
{
    DbgPrint("执行了我们的回调函数...");
    return STATUS_SUCCESS;
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
    ObUnRegisterCallbacks(Globle_Object_Handle);
    DbgPrint("回调卸载完成...");
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
    OB_OPERATION_REGISTRATION Base;                          // 回调函数结构体(你所填的结构都在这里)
    OB_CALLBACK_REGISTRATION CallbackReg;

    CallbackReg.RegistrationContext = NULL;                  // 注册上下文(你回调函数返回参数)
    CallbackReg.Version = OB_FLT_REGISTRATION_VERSION;       // 注册回调版本
    CallbackReg.OperationRegistration = &Base;
    CallbackReg.OperationRegistrationCount = 1;               // 操作计数(下钩数量)
    RtlUnicodeStringInit(&CallbackReg.Altitude, L"600000");   // 长度
    
  Base.ObjectType = PsProcessType;                          // 进程操作类型.此处为进程操作
    Base.Operations = OB_OPERATION_HANDLE_CREATE;             // 操作句柄创建
    Base.PreOperation = MyObjectCallBack;                     // 你自己的回调函数
    Base.PostOperation = NULL;

  // 注册回调
    if (ObRegisterCallbacks(&CallbackReg, &Globle_Object_Handle))
  {
        DbgPrint("回调注册成功...");
  }
    
  Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

上方代码运行后,我们可以打开Xuetr扫描一下内核Object钩子,可以看到已经成功挂钩了,输出效果如下图所示;

4.3.1 实现监控进程打开与关闭

接下来我们实现一个简单的需求,通过编写一个自定义MyObjectCallBack回调函数实现保护win32calc.exe进程不被关闭,本功能实现的关键在于如何获取到监控进程的进程名GetProcessImageNameByProcessID函数就是用来实现转换的,通过向此函数内传入一个进程PID则会通过PsGetProcessImageFileName输出该进程的进程名。

而当回调函数内接收到此进程名时,则可以通过strstr(ProcName, "win32calc.exe")对进程名进行判断,如果匹配到结果,则直接通过Operation->Parameters->CreateHandleInformation.DesiredAccess = ~THREAD_TERMINATE去掉TERMINATE_PROCESSTERMINATE_THREAD权限即可,此时的进程将会被保护而无法被关闭,这段代码实现如下所示;

#include <ntddk.h>
#include <wdm.h>
#include <ntstrsafe.h>
#define PROCESS_TERMINATE 1

PVOID Globle_Object_Handle;
NTKERNELAPI UCHAR * PsGetProcessImageFileName(__in PEPROCESS Process);

char* GetProcessImageNameByProcessID(ULONG ulProcessID)
{
    NTSTATUS  Status;
    PEPROCESS  EProcess = NULL;
    Status = PsLookupProcessByProcessId((HANDLE)ulProcessID, &EProcess);
    if (!NT_SUCCESS(Status))
        return FALSE;
    ObDereferenceObject(EProcess);
    return (char*)PsGetProcessImageFileName(EProcess);
}

OB_PREOP_CALLBACK_STATUS MyObjectCallBack(PVOID RegistrationContext, POB_PRE_OPERATION_INFORMATION Operation)
{
    char ProcName[256] = { 0 };
    HANDLE pid = PsGetProcessId((PEPROCESS)Operation->Object);           // 取出当前调用函数的PID
    strcpy(ProcName, GetProcessImageNameByProcessID((ULONG)pid));        // 通过PID取出进程名,然后直接拷贝内存
    //DbgPrint("当前进程的名字是:%s", ProcName);

    if (strstr(ProcName, "win32calc.exe"))
    {
        if (Operation->Operation == OB_OPERATION_HANDLE_CREATE)
        {
            if ((Operation->Parameters->CreateHandleInformation.OriginalDesiredAccess & PROCESS_TERMINATE) == PROCESS_TERMINATE)
            {
                DbgPrint("你想结束进程?");

                // 如果是计算器,则去掉它的结束权限,在Win10上无效
                Operation->Parameters->CreateHandleInformation.DesiredAccess = ~THREAD_TERMINATE;
                return STATUS_UNSUCCESSFUL;
            }
        }
    }
    return STATUS_SUCCESS;
}
VOID UnDriver(PDRIVER_OBJECT driver)
{
    ObUnRegisterCallbacks(Globle_Object_Handle);
    DbgPrint("回调卸载完成...");
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
    NTSTATUS obst = 0;
    OB_CALLBACK_REGISTRATION obReg;
    OB_OPERATION_REGISTRATION opReg;

    memset(&obReg, 0, sizeof(obReg));
    obReg.Version = ObGetFilterVersion();
    obReg.OperationRegistrationCount = 1;
    obReg.RegistrationContext = NULL;
    RtlInitUnicodeString(&obReg.Altitude, L"321125");
    obReg.OperationRegistration = &opReg;

    memset(&opReg, 0, sizeof(opReg));
    opReg.ObjectType = PsProcessType;
    opReg.Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE;
    opReg.PreOperation = (POB_PRE_OPERATION_CALLBACK)&MyObjectCallBack;
    obst = ObRegisterCallbacks(&obReg, &Globle_Object_Handle);

    Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

首先运行计算器,然后启动驱动保护,此时我们在任务管理器中就无法结束计算器进程了。

4.3.2 实现监控进程中的模块加载

系统中的模块加载包括用户层模块DLL和内核模块SYS的加载,在内核环境下我们可以调用PsSetLoadImageNotifyRoutine内核函数来设置一个映像加载通告例程,当有驱动或者DLL被加载时,回调函数就会被调用从而执行我们自己的回调例程。

PsSetLoadImageNotifyRoutine 函数用来设置一个映像加载通告例程。该函数需要传入一个回调函数的指针,该回调函数会在系统中有驱动程序或 DLL 被加载时被调用。

该函数函数的原型为:

NTKERNELAPI
NTSTATUS
PsSetLoadImageNotifyRoutine(
  PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
);

其中,NotifyRoutine 参数是一个指向映像加载通告例程的函数指针。该函数将在系统中有驱动程序或 DLL 被加载时被调用。

当一个映像被加载时,Windows 内核会检查是否已注册了映像加载通告例程。如果已注册,则内核会调用该例程,将被加载的模块的信息作为参数传递给该例程。通常,该例程会记录或处理这些信息。

需要注意的是,映像加载通告例程应该尽可能地简短,不要执行复杂的操作,以避免影响系统性能。同时,该例程应该是线程安全的,以免发生竞态条件或死锁。

如上函数中PLOAD_IMAGE_NOTIFY_ROUTINE用于接收一个自定义函数,该自定义函数需要声明成如下原型;

VOID MyLoadImageNotifyRoutine(
  PUNICODE_STRING FullImageName,
  HANDLE ProcessId,
  PIMAGE_INFO ImageInfo
);
  • 参数FullImageName参数是指被加载的模块的完整路径名;
  • 参数ProcessId参数是指加载该模块的进程ID;
  • 参数ImageInfo参数是指与该模块相关的信息,包括其基地址、大小等。在回调函数中,可以对这些信息进行处理,以实现对模块加载的监控。

有了如上知识体系,实现监控的目的就会变得简单,其监控的实现重点是实现自己的MyLoadImageNotifyRoutine如下代码中简单实现了当有新的DLL被装载到内存是,则通过DbgView输出该模块的具体信息;

#include <ntddk.h>
#include <ntimage.h>

// 用与获取特定基地址的模块入口
PVOID GetDriverEntryByImageBase(PVOID ImageBase)
{
    PIMAGE_DOS_HEADER pDOSHeader;
    PIMAGE_NT_HEADERS64 pNTHeader;
    PVOID pEntryPoint;
    pDOSHeader = (PIMAGE_DOS_HEADER)ImageBase;
    pNTHeader = (PIMAGE_NT_HEADERS64)((ULONG64)ImageBase + pDOSHeader->e_lfanew);
    pEntryPoint = (PVOID)((ULONG64)ImageBase + pNTHeader->OptionalHeader.AddressOfEntryPoint);
    return pEntryPoint;
}

VOID MyLoadImageNotifyRoutine(PUNICODE_STRING FullImageName,HANDLE ProcessId,PIMAGE_INFO ImageInfo)
{
    PVOID pDrvEntry;

  // MmIsAddress 验证地址可用性
    if (FullImageName != NULL && MmIsAddressValid(FullImageName))
    {
        if (ProcessId == 0)
        {
            pDrvEntry = GetDriverEntryByImageBase(ImageInfo->ImageBase);
            DbgPrint("模块名称:%wZ --> 装载基址:%p --> 镜像长度: %d", FullImageName, pDrvEntry,ImageInfo->ImageSize);
        }
    }
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
    PsRemoveLoadImageNotifyRoutine((PLOAD_IMAGE_NOTIFY_ROUTINE)MyLoadImageNotifyRoutine);
    DbgPrint("驱动卸载完成...");
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
    PsSetLoadImageNotifyRoutine((PLOAD_IMAGE_NOTIFY_ROUTINE)MyLoadImageNotifyRoutine);
    DbgPrint("驱动加载完成...");
    Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

输出效果图如下所示:

通过上方代码的学习,相信读者已经学会了如何监视系统内加载驱动与DLL功能,接着我们给上方的代码加上判断,只需在上方代码的基础上进行一定的改进,但需要注意MyLoadImageNotifyRoutine回调函数中的ModuleStyle参数,该参数用于判断加载模块的类型,如果返回值为零则表示加载SYS,如果返回非零则表示加载的是DLL模块。

VOID UnicodeToChar(PUNICODE_STRING dst, char *src)
{
    ANSI_STRING string;
    RtlUnicodeStringToAnsiString(&string, dst, TRUE);
    strcpy(src, string.Buffer);
    RtlFreeAnsiString(&string);
}

VOID MyLoadImageNotifyRoutine(PUNICODE_STRING FullImageName,HANDLE ModuleStyle,PIMAGE_INFO ImageInfo)
{
    PVOID pDrvEntry;
    char szFullImageName[256] = { 0 };

  // MmIsAddress 验证地址可用性
    if (FullImageName != NULL && MmIsAddressValid(FullImageName))
    {
    // ModuleStyle为零表示加载sys非零表示加载DLL
        if (ModuleStyle == 0)
        {
            pDrvEntry = GetDriverEntryByImageBase(ImageInfo->ImageBase);
            UnicodeToChar(FullImageName, szFullImageName);
            if (strstr(_strlwr(szFullImageName), "hook.sys"))
            {
                DbgPrint("准备拦截SYS内核模块:%s", _strlwr(szFullImageName));
            }
        }
    }
}

输出效果图如下所示:

如上方代码中所示,MyLoadImageNotifyRoutine函数只能接收全局模块加载动作,但是此模式无法判断到底是哪个进程加载的hook.sys驱动,因为回调函数本就很底层了,到了一定的深度之后就无法判断到底是谁主动引发的行为,一切回调行为都会变成系统的行为,而某些驱动过滤软件,通常会使用特征扫描等方式来判断驱动是否是我们所需;

当判断特定模块是我们所要拦截的驱动时,则下一步就要进行驱动的屏蔽工作,对于驱动屏蔽来说最直接的办法就是在程序的入口位置写入Mov eax,c0000022h;ret这两条汇编指令从而让模块无法被执行,此时模块虽然被加载了但却无法执行功能,本质上来说已经起到了拒绝加载的效果;

通过ImageInfo->ImageBase 来获取被加载驱动程序hook.sys的映像基址,然后找到NT头的OptionalHeader节点,该节点里面就是被加载驱动入口的地址,通过汇编在驱动头部写入ret返回指令,即可实现屏蔽加载特定驱动文件,这段代码完整实现如下所示;

#include <ntddk.h>
#include <intrin.h>
#include <ntimage.h>

PVOID GetDriverEntryByImageBase(PVOID ImageBase)
{
    PIMAGE_DOS_HEADER pDOSHeader;
    PIMAGE_NT_HEADERS64 pNTHeader;
    PVOID pEntryPoint;
    pDOSHeader = (PIMAGE_DOS_HEADER)ImageBase;
    pNTHeader = (PIMAGE_NT_HEADERS64)((ULONG64)ImageBase + pDOSHeader->e_lfanew);
    pEntryPoint = (PVOID)((ULONG64)ImageBase + pNTHeader->OptionalHeader.AddressOfEntryPoint);
    return pEntryPoint;
}

VOID UnicodeToChar(PUNICODE_STRING dst, char *src)
{
    ANSI_STRING string;
    RtlUnicodeStringToAnsiString(&string, dst, TRUE);
    strcpy(src, string.Buffer);
    RtlFreeAnsiString(&string);
}

// 使用开关写保护需要在C/C++优化中启用内部函数

// 关闭写保护
KIRQL WPOFFx64()
{
    KIRQL irql = KeRaiseIrqlToDpcLevel();
    UINT64 cr0 = __readcr0();
    cr0 &= 0xfffffffffffeffff;
    _disable();
    __writecr0(cr0);
    return irql;
}

// 开启写保护
void WPONx64(KIRQL irql)
{
    UINT64 cr0 = __readcr0();
    cr0 |= 0x10000;
    _enable();
    __writecr0(cr0);
    KeLowerIrql(irql);
}

BOOLEAN DenyLoadDriver(PVOID DriverEntry)
{
    UCHAR fuck[] = "\xB8\x22\x00\x00\xC0\xC3";
    KIRQL kirql;
    /* 在模块开头写入以下汇编指令
    Mov eax,c0000022h
    ret
    */

    if (DriverEntry == NULL)
  {
    return FALSE;
  }

    kirql = WPOFFx64();
    memcpy(DriverEntry, fuck,sizeof(fuck) / sizeof(fuck[0]));
    WPONx64(kirql);
    return TRUE;
}

VOID MyLoadImageNotifyRoutine(PUNICODE_STRING FullImageName, HANDLE ModuleStyle, PIMAGE_INFO ImageInfo)
{
    PVOID pDrvEntry;
    char szFullImageName[256] = { 0 };

  // MmIsAddress 验证地址可用性
    if (FullImageName != NULL && MmIsAddressValid(FullImageName))
    {
    // ModuleStyle为零表示加载sys非零表示加载DLL
        if (ModuleStyle == 0)
        {
            pDrvEntry = GetDriverEntryByImageBase(ImageInfo->ImageBase);
            UnicodeToChar(FullImageName, szFullImageName);
            if (strstr(_strlwr(szFullImageName), "hook.sys"))
            {
                DbgPrint("拦截SYS内核模块:%s", szFullImageName);
                DenyLoadDriver(pDrvEntry);
            }
        }
    }
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
    PsRemoveLoadImageNotifyRoutine((PLOAD_IMAGE_NOTIFY_ROUTINE)MyLoadImageNotifyRoutine);
    DbgPrint("驱动卸载完成...");
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
    PsSetLoadImageNotifyRoutine((PLOAD_IMAGE_NOTIFY_ROUTINE)MyLoadImageNotifyRoutine);
    DbgPrint("驱动加载完成...");
    Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

而对于屏蔽DLL模块加载同样如此,仅仅只是在判断ModuleStyle参数时将非零作为过滤条件即可,要实现该功能只需要在上面的代码上稍微修改一下即可;

char *UnicodeToLongString(PUNICODE_STRING uString)
{
    ANSI_STRING asStr;
    char *Buffer = NULL;;
    RtlUnicodeStringToAnsiString(&asStr, uString, TRUE);
    Buffer = ExAllocatePoolWithTag(NonPagedPool, uString->MaximumLength * sizeof(wchar_t), 0);
    if (Buffer == NULL)
  {
        return NULL;
  }

    RtlCopyMemory(Buffer, asStr.Buffer, asStr.Length);
    return Buffer;
}

VOID MyLoadImageNotifyRoutine(PUNICODE_STRING FullImageName, HANDLE ModuleStyle, PIMAGE_INFO ImageInfo)
{
    PVOID pDrvEntry;
    char *PareString = NULL;

    if (MmIsAddressValid(FullImageName))
    {
    // 非零则监控DLL加载
        if (ModuleStyle != 0)
        {
            PareString = UnicodeToLongString(FullImageName);
            if (PareString != NULL)
            {
                if (strstr(PareString, "hook.dll"))
                {
                    pDrvEntry = GetDriverEntryByImageBase(ImageInfo->ImageBase);
                    if (pDrvEntry != NULL)
                        DenyLoadDriver(pDrvEntry);
                }
            }
        }
    }
}

我们以屏蔽SYS内核模块为例,当驱动文件WinDDK.sys被加载后,尝试加载hook.sys会提示拒绝访问,说明我们的驱动保护生效了;

关键的内核进程操作已经分享,该功能作用使用非常广泛,例如杀软的主动防御系统,游戏的保护系统等都会用到这些东西,以QQ电脑管家为例,默认进程在运行后都会挂钩进程保护钩子用于监控系统内进程与线程操作;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/160510.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

go zero手把手教你入门案例

一、入门案例 1、在黑窗口上安装 go install github.com/zeromicro/go-zero/tools/goctllatest2、使用goland创建一个项目 3、在项目中安装依赖 go get -u github.com/zeromicro/go-zerolatest4、模拟创建一个user的项目 goctl api new user5、安装依赖包 go mod tidy6、补充代…

Java 算法篇-链表的经典算法:有序链表去重、合并多个有序链表

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 链表的说明 2.0 有序链表去重的实现方式 2.1 有序链表去重(保留重复的节点) - 使用递归来实现 2.2 有序链表去重(保留重复的节点) - 使用双指针来实现 2.3 有序…

U盘如何自定义图标?

1、准备一张图片&#xff0c;转换为.ico格式&#xff0c;转换格式的工具推荐一个ToYcon 转换好后放到拷贝到u盘里面。 2、在u盘里面新建一个文本文档&#xff0c;在文档里面写入以下内容&#xff0c;注意&#xff0c;这里的test为图片的名称。 根据自己图片名称做一下修改。 […

三十二、W5100S/W5500+RP2040树莓派Pico<UPnP示例>

文章目录 1 前言2 简介2 .1 什么是UPnP&#xff1f;2.2 UPnP的优点2.3 UPnP数据交互原理2.4 UPnP应用场景 3 WIZnet以太网芯片4 UPnP示例概述以及使用4.1 流程图4.2 准备工作核心4.3 连接方式4.4 主要代码概述4.5 结果演示 5 注意事项6 相关链接 1 前言 随着智能家居、物联网等…

6.7二叉树的最小深度(LC111)

审题要清楚&#xff1a; 最小深度是从根节点到最近叶子节点的最短路径上的节点数量。注意是叶子节点&#xff08;左右孩子都为空的节点才是叶子节点&#xff01;&#xff09;。 算法&#xff1a; 既可以求最小高度&#xff0c;也可以直接求深度。 最小高度&#xff1a; 后序…

4M防错追溯与MES管理系统的融合应用

在现代化制造业中&#xff0c;质量追溯已成为企业核心竞争力的重要组成部分。为了实现精确的质量追溯&#xff0c;制造企业广泛采用了MES管理系统解决方案来进行生产过程中的数据管理。本文将探讨如何通过MES管理系统实现4M防错追溯&#xff0c;并提升企业的生产与管理效率。 一…

软件质量保护与测试(第2版)学习总结第十三章 集成测试

很多人都认为微软是一家软件开发公司&#xff0c;事实上我们是一家软件测试公司。 ---比尔盖茨 集成测试是在单元测试的基础上将多个模块组合在一起进行测试的过程。 13.1.1 区别 单元测试主要关注模块内部&#xff0c;系统测试则是在用户的角度来评价系统&#xff…

第四篇 《随机点名答题系统》——基础设置详解(类抽奖系统、在线答题系统、线上答题系统、在线点名系统、线上点名系统、在线考试系统、线上考试系统)

目录 1.功能需求 2.数据库设计 3.流程设计 4.关键代码 4.1.设置题库 4.1.1数据请求示意图 4.1.2选择题库&#xff08;index.php&#xff09;数据请求代码 4.1.3取消题库&#xff08;index.php&#xff09;数据请求代码 4.1.4业务处理Service&#xff08;xztk.p…

【高并发内存池】第一篇 项目简介及定长内存池

&#x1f57a;作者&#xff1a; 主页 我的专栏C语言从0到1探秘C数据结构从0到1探秘Linux菜鸟刷题集 &#x1f618;欢迎关注&#xff1a;&#x1f44d;点赞&#x1f64c;收藏✍️留言 &#x1f3c7;码字不易&#xff0c;你的&#x1f44d;点赞&#x1f64c;收藏❤️关注对我真的…

Flume学习笔记(4)—— Flume数据流监控

前置知识&#xff1a; Flume学习笔记&#xff08;1&#xff09;—— Flume入门-CSDN博客 Flume学习笔记&#xff08;2&#xff09;—— Flume进阶-CSDN博客 Flume 数据流监控 Ganglia 的安装与部署 Ganglia 由 gmond、gmetad 和 gweb 三部分组成。 gmond&#xff08;Ganglia …

MybatisPlus学习

一.快速入门 1.相关数据库创建 CREATE TABLE USER(id BIGINT(20) NOT NULL COMMENT 主键ID,NAME VARCHAR(30) NULL DEFAULT NULL COMMENT 姓名,age INT(11) NULL DEFAULT NULL COMMENT 年龄,email VARCHAR(50) NULL DEFAULT NULL COMMENT 邮箱,PRIMARY KEY (id));​​INSERT I…

BUG:编写springboot单元测试,自动注入实体类报空指针异常

原因:修饰测试方法的Test注解导入错误 造成错误的原因是 import org.junit.Test;正确的应该是 import org.junit.jupiter.api.Test前者是Junit4,后者是Junit5 junit4的使用似乎要在测试类除了添加SpringbootTest还要添加RunWith(SpringRunner.class) 同时要注意spring-boot-s…

制作翻页电子相册,这个工具你必须了解!

电子相册作为一种很有纪念意义的载体&#xff0c;无论是生日、旅行、结婚、毕业纪念等等&#xff0c;可以应用在很多场合当中&#xff0c;如何制作呢&#xff1f; 而对于不会制作电子相册的人来说&#xff0c;使用套用模板是最直接快速的方式了。所以&#xff0c;推荐大家使用…

Find My充电宝|苹果Find My技术与充电宝结合,智能防丢,全球定位

充电宝是一种个人可随身携带&#xff0c;自身能储备电能&#xff0c;主要为手持式移动设备等消费电子产品&#xff08;例如无线电话、笔记本电脑&#xff09;充电的便携充电器&#xff0c;特别应用在没有外部电源供应的场合。其主要组成部分包括&#xff1a;用作电能存储的电池…

C++软件开发面试场景题

自己在秋招过程中遇到的一些场景题 海量数据N取Top K个元素&#xff0c;复杂度是多少 在处理海量数据中获取前K个元素&#xff08;Top K&#xff09;的问题中&#xff0c;通常会使用一些高效的算法来减少时间和空间复杂度。以下是两种常见的解决方案和它们的复杂度&#xff1…

LeetCode热题100——图论

图论 1. 岛屿的数量2. 腐烂的橘子 1. 岛屿的数量 给你一个由 ‘1’&#xff08;陆地&#xff09;和 ‘0’&#xff08;水&#xff09;组成的的二维网格&#xff0c;请你计算网格中岛屿的数量。岛屿总是被水包围&#xff0c;并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆…

PHP排序sort()、asort() 和 ksort() 的区别及用法

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;CSDN领军人物&#xff0c;全栈领域优质创作者✌&#xff0c;CSDN博客专家&#xff0c;阿里云社区专家博主&#xff0c;2023年6月CSDN上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师…

Centos(Linux)服务器安装Dotnet8 及 常见问题解决

1. 下载dotnet8 sdk 下载 .NET 8.0 SDK (v8.0.100) - Linux x64 Binaries 拿到 dotnet-sdk-8.0.100-linux-x64.tar.gz 文件 2. 把文件上传到 /usr/local/software 目录 mkdir -p /usr/local/software/dotnet8 把文件拷贝过去 mv dotnet-sdk-8.0.100-linux-x64.tar.gz /usr/loc…

多维时序 | MATLAB实现PSO-BiGRU-Attention粒子群优化双向门控循环单元融合注意力机制的多变量时间序列预测

多维时序 | MATLAB实现PSO-BiGRU-Attention粒子群优化双向门控循环单元融合注意力机制的多变量时间序列预测 目录 多维时序 | MATLAB实现PSO-BiGRU-Attention粒子群优化双向门控循环单元融合注意力机制的多变量时间序列预测预测效果基本介绍模型描述程序设计参考资料 预测效果 …

混沌系统在图像加密中的应用(基于哈密顿能量函数的混沌系统构造1.4)

混沌系统在图像加密中的应用&#xff08;基于哈密顿能量函数的混沌系统构造1.4&#xff09; 前言一、逆时间对称性分析二、具有逆时间对称性的单晶格状混沌与拟周期流1.逆时间对称性及哈密顿能量函数2.数值仿真 python代码 前言 续接混沌系统在图像加密中的应用&#xff08;基…