7.6 实现进程挂起与恢复

news2024/12/23 0:48:59

挂起与恢复进程是指暂停或恢复进程的工作状态,以达到一定的控制和管理效果。在 Windows 操作系统中,可以使用系统提供的函数实现进程的挂起和恢复,以达到对进程的控制和调度。需要注意,过度使用进程挂起/恢复操作可能会造成系统性能的降低,导致死锁等问题,因此在使用时应该谨慎而慎重。同时,通过和其他进程之间协同工作,也可以通过更加灵活的方式,实现进程的协调、交互等相应的功能,从而实现更加高效和可靠的进程管理。

要实现挂起进程,首先我们需要实现挂起线程,因为挂起进程的实现原理是通过调用SuspendThread函数循环将进程内的所有线程全部挂起后实现的,而要实现挂起线程则我们需要先确定指定进程内的线程信息,要实现枚举进程内的线程信息则可以通过以下几个步骤实现。

首先通过CreateToolhelp32Snapshot得到当前系统下所有的进程快照,并通过遍历进程的方式寻找是否符合我们所需要枚举的进程名,如果是则调用CreateToolhelp32Snapshot并通过传入TH32CS_SNAPTHREAD代表枚举线程,通过循环的方式遍历进程内的线程,每次通过调用OpenThread打开线程,并调用ZwQueryInformationThread查询该线程的入口信息以及线程所在的模块信息,最后以此输出即可得到当前进程内的所有线程信息。

#include <iostream>
#include <Windows.h>  
#include <TlHelp32.h>
#include <Psapi.h>

using namespace std;

typedef enum _THREADINFOCLASS
{
  ThreadBasicInformation,
  ThreadTimes,
  ThreadPriority,
  ThreadBasePriority,
  ThreadAffinityMask,
  ThreadImpersonationToken,
  ThreadDescriptorTableEntry,
  ThreadEnableAlignmentFaultFixup,
  ThreadEventPair_Reusable,
  ThreadQuerySetWin32StartAddress,
  ThreadZeroTlsCell,
  ThreadPerformanceCount,
  ThreadAmILastThread,
  ThreadIdealProcessor,
  ThreadPriorityBoost,
  ThreadSetTlsArrayAddress,
  ThreadIsIoPending,
  ThreadHideFromDebugger,
  ThreadBreakOnTermination,
  MaxThreadInfoClass
}THREADINFOCLASS;

typedef struct _CLIENT_ID
{
  HANDLE UniqueProcess;
  HANDLE UniqueThread;
}CLIENT_ID;

typedef struct _THREAD_BASIC_INFORMATION
{
  LONG ExitStatus;
  PVOID TebBaseAddress;
  CLIENT_ID ClientId;
  LONG AffinityMask;
  LONG Priority;
  LONG BasePriority;
}THREAD_BASIC_INFORMATION, *PTHREAD_BASIC_INFORMATION;

extern "C" LONG(__stdcall * ZwQueryInformationThread)
(
IN HANDLE ThreadHandle,
IN THREADINFOCLASS ThreadInformationClass,
OUT PVOID ThreadInformation,
IN ULONG ThreadInformationLength,
OUT PULONG ReturnLength OPTIONAL
) = NULL;

// 枚举进程内的线程
BOOL EnumThread(char *ProcessName)
{
  // 进程快照句柄
  HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  PROCESSENTRY32 process = { sizeof(PROCESSENTRY32) };

  // 遍历进程
  while (Process32Next(hProcessSnap, &process))
  {
    // char* 转 string
    string s_szExeFile = process.szExeFile;
    if (s_szExeFile == ProcessName)
    {
      HANDLE hThreadSnap = INVALID_HANDLE_VALUE;
      THREADENTRY32 te32;

      // 创建线程快照
      hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
      if (hThreadSnap == INVALID_HANDLE_VALUE)
      {
        return FALSE;
      }

      // 为快照分配内存空间
      te32.dwSize = sizeof(THREADENTRY32);

      // 获取第一个线程的信息
      if (!Thread32First(hThreadSnap, &te32))
      {
        return FALSE;
      }

      // 遍历线程
      while (Thread32Next(hThreadSnap, &te32))
      {
        // 判断线程是否属于本进程
        if (te32.th32OwnerProcessID == process.th32ProcessID)
        {
          // 打开线程
          HANDLE hThread = OpenThread(
            THREAD_ALL_ACCESS,        // 访问权限
            FALSE,                    // 由此线程创建的进程不继承线程的句柄
            te32.th32ThreadID         // 线程 ID
            );
          if (hThread == NULL)
          {
            return FALSE;
          }

          // 将区域设置设置为从操作系统获取的ANSI代码页
          setlocale(LC_ALL, ".ACP");

          // 获取 ntdll.dll 的模块句柄
          HINSTANCE hNTDLL = ::GetModuleHandle("ntdll");

          // 从 ntdll.dll 中取出 ZwQueryInformationThread
          (FARPROC&)ZwQueryInformationThread = GetProcAddress(hNTDLL, "ZwQueryInformationThread");

          // 获取线程入口地址
          PVOID startaddr;                          // 用来接收线程入口地址
          ZwQueryInformationThread(
            hThread,                              // 线程句柄
            ThreadQuerySetWin32StartAddress,      // 线程信息类型 ThreadQuerySetWin32StartAddress 线程入口地址
            &startaddr,                           // 指向缓冲区的指针
            sizeof(startaddr),                    // 缓冲区的大小
            NULL
            );

          // 获取线程所在模块
          THREAD_BASIC_INFORMATION tbi;            // _THREAD_BASIC_INFORMATION 结构体对象
          TCHAR modname[MAX_PATH];                 // 用来接收模块全路径
          ZwQueryInformationThread(
            hThread,                             // 线程句柄
            ThreadBasicInformation,              // 线程信息类型,ThreadBasicInformation :线程基本信息
            &tbi,                                // 指向缓冲区的指针
            sizeof(tbi),                         // 缓冲区的大小
            NULL
            );

          // 检查入口地址是否位于某模块中
          GetMappedFileName(
            OpenProcess(                                        // 进程句柄
            PROCESS_ALL_ACCESS,                                 // 访问权限
            FALSE,                                              // 由此线程创建的进程不继承线程的句柄
            (DWORD)tbi.ClientId.UniqueProcess                   // 唯一进程 ID
            ),
            startaddr,                            // 要检查的地址
            modname,                              // 用来接收模块名的指针
            MAX_PATH                              // 缓冲区大小
            );
          std::cout << "线程ID: " << te32.th32ThreadID << " 线程入口: " << startaddr << " 所在模块: " << modname << std::endl;
        }
      }
    }
  }
}

int main(int argc, char* argv[])
{
  EnumThread("lyshark.exe");

  system("pause");
  return 0;
}

读者可自行运行上述代码片段,即可枚举出当前运行进程lyshark.exe中所有的后动线程信息,如下图所示;

当我们能够得到当前进程内的线程信息后,接下来就是实现如何挂起或恢复进程内的特定线程,挂起线程可以使用SuspendThread 其函数声明如下:

DWORD SuspendThread(
  HANDLE hThread
);

其中,hThread 是一个指向线程句柄的指针,指向要挂起的线程的句柄,该函数返回挂起前线程的线程计数器值,表示被挂起线程在挂起前还未执行的指令数目。

可以多次调用 SuspendThread 函数将同一个线程进行多次挂起,每次返回被挂起前线程的线程计数器值,每调用一次则会阻塞该线程,其状态会变为挂起状态。当该线程被 ResumeThread 恢复时,它将继续从上次挂起时的位置开始执行。

ResumeThread 函数声明如下:

DWORD ResumeThread(
  HANDLE hThread
);

其中,hThread 是线程句柄,指向要恢复的线程的句柄。

调用 ResumeThread 函数可以让一个被挂起的线程从上次挂起的位置开始继续执行,函数返回值是被恢复的线程的先前挂起次数。当被恢复的线程的挂起计数器归零时,其状态将自动变为非挂起状态,并开始继续执行。

当有了上述两个函数的支持那么挂起线程将变得很容易实现了,首先后去所有进程快照,接着就是直接打开OpenThread()符合要求的线程,此时只需要调用SuspendThread(hThread)即可挂起一个线程,调用ResumeThread(hThread)则可以恢复一个线程,具体实现代码如下所示;

#include <windows.h>
#include <stdio.h>
#include <TlHelp32.h>

int Start_Stop_Thread(DWORD Pid, DWORD ThreadID, BOOL flag)
{
    THREADENTRY32 te32 = { 0 };
    te32.dwSize = sizeof(THREADENTRY32);

    // 获取全部线程快照
    HANDLE hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    if (INVALID_HANDLE_VALUE != hThreadSnap)
    {
        // 获取快照中第一条信息
        BOOL bRet = Thread32First(hThreadSnap, &te32);
        while (bRet)
        {
            // 只过滤出 pid 里面的线程
            if (Pid == te32.th32OwnerProcessID)
            {
                // 判断是否为ThreadID,暂停指定的TID
                if (ThreadID == te32.th32ThreadID)
                {
                    // 打开线程
                    HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID);

                    if (flag == TRUE)
                    {
                        ResumeThread(hThread);     // 恢复线程
                    }
                    else
                    {
                        SuspendThread(hThread);     // 暂停线程
                    }
                    CloseHandle(hThreadSnap);
                }
            }
            // 获取快照中下一条信息
            bRet = Thread32Next(hThreadSnap, &te32);
        }
        return 0;
    }
    return -1;
}

int main(int argc, char* argv[])
{
    // 暂停或恢复进程ID = 4204 里面的线程ID = 10056
    int ret = Start_Stop_Thread(4204, 10056, TRUE);    // TRUE = 恢复线程 FALSE = 挂起线程
    printf("状态: %d \n", ret);

    system("pause");
    return 0;
}

当有了上述功能的支持以后,那么实现挂起进程将变得很容易,读者只需要在特定一个进程内枚举出所有的活动线程,并通过循环的方式逐个挂起即可实现挂起整个进程的效果,这段完整代码如下所示;

#include <windows.h>
#include <iostream>
#include <tlhelp32.h>
#include <Psapi.h>

#pragma comment(lib,"psapi.lib")

#define NT_SUCCESS(Status) ((NTSTATUS)(Status) >= 0)
#define SystemProcessesAndThreadsInformation    5       // 功能号
typedef DWORD(WINAPI* PQUERYSYSTEM)(UINT, PVOID, DWORD, PDWORD);

// 线程状态的枚举常量
typedef enum _THREAD_STATE
{
  StateInitialized,    // 初始化状态
  StateReady,          // 准备状态
  StateRunning,        // 运行状态
  StateStandby,
  StateTerminated,     // 关闭
  StateWait,           // 等待
  StateTransition,     // 切换
  StateUnknown
}THREAD_STATE;

// 线程处于等待的原因的枚举常量
typedef enum _KWAIT_REASON
{
  Executive,
  FreePage,
  PageIn,
  PoolAllocation,
  DelayExecution,
  Suspended,
  UserRequest,
  WrExecutive,
  WrFreePage,
  WrPageIn,
  WrPoolAllocation,
  WrDelayExecution,
  WrSuspended,
  WrUserRequest,
  WrEventPair,
  WrQueue,
  WrLpcReceive,
  WrLpcReply,
  WrVirtualMemory,
  WrPageOut,
  WrRendezvous,
  Spare2,
  Spare3,
  Spare4,
  Spare5,
  Spare6,
  WrKernel,
  MaximumWaitReason
}KWAIT_REASON;

typedef LONG   NTSTATUS;
typedef LONG   KPRIORITY;

typedef struct _CLIENT_ID
{
  DWORD        UniqueProcess;
  DWORD        UniqueThread;
} CLIENT_ID, * PCLIENT_ID;

typedef struct _VM_COUNTERS
{
  SIZE_T        PeakVirtualSize;
  SIZE_T        VirtualSize;
  ULONG         PageFaultCount;
  SIZE_T        PeakWorkingSetSize;
  SIZE_T        WorkingSetSize;
  SIZE_T        QuotaPeakPagedPoolUsage;
  SIZE_T        QuotaPagedPoolUsage;
  SIZE_T        QuotaPeakNonPagedPoolUsage;
  SIZE_T        QuotaNonPagedPoolUsage;
  SIZE_T        PagefileUsage;
  SIZE_T        PeakPagefileUsage;
} VM_COUNTERS;

// 线程信息结构体
typedef struct _SYSTEM_THREAD_INFORMATION
{
  LARGE_INTEGER   KernelTime;
  LARGE_INTEGER   UserTime;
  LARGE_INTEGER   CreateTime;
  ULONG           WaitTime;
  PVOID           StartAddress;
  CLIENT_ID       ClientId;
  KPRIORITY       Priority;
  KPRIORITY       BasePriority;
  ULONG           ContextSwitchCount;
  LONG            State;// 状态,是THREAD_STATE枚举类型中的一个值
  LONG            WaitReason;//等待原因, KWAIT_REASON中的一个值
} SYSTEM_THREAD_INFORMATION, * PSYSTEM_THREAD_INFORMATION;


typedef struct _UNICODE_STRING
{
  USHORT Length;
  USHORT MaximumLength;
  PWSTR  Buffer;
} UNICODE_STRING, * PUNICODE_STRING;

// 进程信息结构体
typedef struct _SYSTEM_PROCESS_INFORMATION
{
  ULONG            NextEntryDelta;    // 指向下一个结构体的指针
  ULONG            ThreadCount;       // 本进程的总线程数
  ULONG            Reserved1[6];      // 保留
  LARGE_INTEGER    CreateTime;        // 进程的创建时间
  LARGE_INTEGER    UserTime;          // 在用户层的使用时间
  LARGE_INTEGER    KernelTime;        // 在内核层的使用时间
  UNICODE_STRING   ProcessName;       // 进程名
  KPRIORITY        BasePriority;
  ULONG            ProcessId;         // 进程ID
  ULONG            InheritedFromProcessId;
  ULONG            HandleCount;       // 进程的句柄总数
  ULONG            Reserved2[2];      // 保留
  VM_COUNTERS      VmCounters;
  IO_COUNTERS      IoCounters;
  SYSTEM_THREAD_INFORMATION Threads[5];    // 子线程信息数组
}SYSTEM_PROCESS_INFORMATION, * PSYSTEM_PROCESS_INFORMATION;

// 获取线程是被是否被挂起 1=表示线程被挂起  0=表示线程正常 -1=未知状态
int IsThreadSuspend(DWORD dwProcessID, DWORD dwThreadID)
{
  int nRet = 0;
  NTSTATUS Status = 0;

  PQUERYSYSTEM NtQuerySystemInformation = NULL;
  PSYSTEM_PROCESS_INFORMATION pInfo = { 0 };

  // 获取函数地址
  NtQuerySystemInformation = (PQUERYSYSTEM) GetProcAddress(LoadLibrary("ntdll.dll"), "NtQuerySystemInformation");

  DWORD   dwSize = 0;

  // 获取信息所需的缓冲区大小
  Status = NtQuerySystemInformation(SystemProcessesAndThreadsInformation,// 要获取的信息的类型
    NULL, // 用于接收信息的缓冲区
    0,  // 缓冲区大小
    &dwSize
  );

  // 申请缓冲区
  char* pBuff = new char[dwSize];
  pInfo = (PSYSTEM_PROCESS_INFORMATION)pBuff;
  if (pInfo == NULL)
    return -1;

  // 再次调用函数, 获取信息
  Status = NtQuerySystemInformation(SystemProcessesAndThreadsInformation, // 要获取的信息的类型
    pInfo, // 用于接收信息的缓冲区
    dwSize,  // 缓冲区大小
    &dwSize
  );
  if (!NT_SUCCESS(Status))
  {
    /*如果函数执行失败*/
    delete[] pInfo;
    return -1;
  }

  // 遍历结构体,找到对应的进程
  while (1)
  {
    // 判断是否还有下一个进程
    if (pInfo->NextEntryDelta == 0)
      break;

    // 判断是否找到了ID
    if (pInfo->ProcessId == dwProcessID)
    {

      // 找到该进程下的对应的线程,也就是遍历所有线程
      for (DWORD i = 0; i < pInfo->ThreadCount; i++)
      {
        if (pInfo->Threads[i].ClientId.UniqueThread == dwThreadID)
        {
          // 找到线程 
          // 如果线程被挂起
          if (pInfo->Threads[i].State == StateWait&& pInfo->Threads[i].WaitReason == Suspended)
          {
            nRet = 1;
            break;
          }
        }
      }
      break;
    }
    // 迭代到下一个节点
    pInfo = (PSYSTEM_PROCESS_INFORMATION)(((PUCHAR)pInfo) + pInfo->NextEntryDelta);
  }

  delete[] pBuff;
  return nRet;
}

// 设置进程状态 挂起/非挂起
int SuspendProcess(DWORD dwProcessID, BOOL fSuspend)
{
  HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, dwProcessID);

  if (hSnapshot != INVALID_HANDLE_VALUE)
  {
    THREADENTRY32 te = { sizeof(te) };
    BOOL fOk = Thread32First(hSnapshot, &te);
    for (; fOk; fOk = Thread32Next(hSnapshot, &te))
    {
      if (te.th32OwnerProcessID == dwProcessID)
      {
        HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME,FALSE, te.th32ThreadID);
        if (hThread != NULL)
        {
          if (fSuspend)
          {
            if (IsThreadSuspend(dwProcessID, te.th32ThreadID) == 0)
            {
              SuspendThread(hThread);
            }
            else
            {
              return 0;
            }
          }
          else
          {
            if (IsThreadSuspend(dwProcessID, te.th32ThreadID) == 1)
            {
              ResumeThread(hThread);
            }
            else
            {
              return 0;
            }
          }
        }
        CloseHandle(hThread);
      }
    }

  }
  CloseHandle(hSnapshot);
  return 1;
}

int main(int argc, char *argv[])
{
  // 挂起进程
  SuspendProcess(20308, TRUE);

  // 恢复进程
  SuspendProcess(20308, FALSE);

  return 0;
}

读者可自行编译并运行上述代码,通过调用SuspendProcess函数并以此传入需要挂起的进程PID以及一个状态,当该状态为TRUE时则代表挂起进程,而当状态值为FALSE时则代表为恢复一个进程,当一个进程被挂起后其会出现卡死的现象,当恢复后一切都会变得正常。

本文作者: 王瑞
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于or-tools的护士排班问题建模求解

基于or-tools的护士排班问题建模求解 护士排班问题&#xff08;Nurse Rostering Problem&#xff0c;NRP&#xff09;ortools官网例题1&#xff1a;A nurse scheduling problem代码解析完整代码 ortools官网例题2&#xff1a;Scheduling with shift requests代码解析完整代码 …

比特币的蒙提霍尔问题

把钱放在嘴边 我们在比特币上建立了蒙提霍尔问题模拟。 如果您知道概率谜题的正确答案&#xff0c;不仅炫耀您的数学技能&#xff0c;还会获得金钱奖励。 它完全无需信任地在链上运行。 蒙提霍尔问题 蒙提霍尔问题&#xff08;三门问题&#xff09;是一个以蒙提霍尔命名的概率…

Redis桌面管理工具Redis Desktop Manager mac中文版功能特色

Redis Desktop Manager for Mac是一款实用的Redis可视化工具。RDM支持SSL / TLS加密&#xff0c;SSH隧道&#xff0c;基于SSH隧道的TLS&#xff0c;为您提供了一个易于使用的GUI&#xff0c;可以访问您的Redis数据库并执行一些基本操作&#xff1a;将键视为树&#xff0c;CRUD键…

计算机网络工程师多选题系列——操作系统

得多选者得天下啊同志们&#xff01; 摘录按照章节顺序&#xff0c;但事实上各章节习题有交叉。 1 操作系统 1.1 操作系统概论 操作系统的主要功能&#xff1a;进程管理、存储管理、文件管理、设备管理和用户接口。 操作系统的主要功能——设备管理&#xff1a;为用户程序提…

metinfo_5.0.4 EXP Python脚本编写

文章目录 metinfo_5.0.4EXP编写SQL注入漏洞 metinfo_5.0.4EXP编写 SQL注入漏洞 漏洞点&#xff1a;/about/show.php?langcn&id22 http://10.9.75.142/metInfo_5.0.4/about/show.php?langcn&id22验证漏洞(数字型注入) 状态码区分正确与错误 做比较的时候不能采用…

实现顺序表——实践报告

W...Y的主页 &#x1f60a; 代码仓库分享 &#x1f495; 目录 一、实验目的&#xff1a; 二、实验内容&#xff1a; 三、实验要求&#xff1a; 四.实验步骤&#xff08;给出每个函数的算法描述&#xff09;&#xff1a; 五.实验结果&#xff1a; 六.源代码 实验名称 &am…

【深度学习实验】前馈神经网络(五):自定义线性模型:前向传播、反向传播算法(封装参数)

目录 一、实验介绍 二、实验环境 1. 配置虚拟环境 2. 库版本介绍 三、实验内容 0. 导入必要的工具包 1. 线性模型Linear类 a. 构造函数__init__ b. __call__(self, x)方法 c. 前向传播forward d. 反向传播backward 2. 模型训练 3. 代码整合 一、实验介绍 实现线性…

【计算机网络】IP协议(下)

文章目录 1. 特殊的IP地址2. IP地址的数量限制3. 私有IP地址和公网IP地址私有IP为什么不能出现在公网上&#xff1f;解决方案——NAT技术的使用 4. 路由5. IP分片问题为什么要进行切片&#xff1f;如何做的分片和组装&#xff1f;16位标识3位标志13位片偏移例子 细节问题如何区…

一文带你玩转logo:含义、获取、使用以及2000多知名logo大图资源

大家好&#xff01;logo是我们非常熟悉的一种事物&#xff0c;但是我发现很多场合的logo使用并不规范、高效&#xff0c;所以今天六分成长来带着大家了解一下关于logo的方方面面。 一、什么是logo&#xff1f; logo不是某一些英文单词的缩写&#xff0c;是一个完整的单词&…

uniapp如何判断是哪个(微信/APP)平台

其实大家在开发uniapp项目的时候长长会遇到这样一个问题&#xff0c;就是针对某些小程序&#xff0c;没发去适配相关的功能&#xff0c;所以要针对不同的平台&#xff0c;进行不同的处理。 #ifdef &#xff1a; if defined 仅在某个平台编译 #ifndef &#xff1a; …

机器学习实验一:使用 Logistic 回归来预测患有疝病的马的存活问题

代码&#xff1a; import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report import matplotlib.pyplot as plt def train(): # …

机器学习---神经元模型

1. 生物学的启示 神经元在结构上由细胞体、树突、轴突和突触四部分组成。 细胞体是神经元的主体&#xff0c;由细胞核、细胞质和细胞膜3部分组成。细胞体的外部是细胞膜&#xff0c;将 膜内外细胞液分开。由于细胞膜对细胞液中的不同离子具有不同的通透性&#xff0c;这使得膜…

XXE 漏洞及案例实战

文章目录 XXE 漏洞1. 基础概念1.1 XML基础概念1.2 XML与HTML的主要差异1.3 xml示例 2. 演示案例2.1 pikachu靶场XML2.1.1 文件读取2.1.2 内网探针或者攻击内网应用&#xff08;触发漏洞地址&#xff09;2.1.4 RCE2.1.5 引入外部实体DTD2.1.6 无回显读取文件 3. XXE 绕过3.1 dat…

【操作系统】线程、多线程

为什么要引入线程&#xff1f; 传统的进程只能串行的执行一系列程序&#xff0c;线程增加并发度。同一个进程分为多个线程。 线程是调度的基本单元&#xff0c;程序执行流的最小单位&#xff0c;基本的CPU执行单元。 进程是资源分配的基本单位。 线程的实现方式 用户级线程 代…

Unity入门教程(上)

七、运行游戏 再次保存我们的项目文件&#xff08;返回步骤四&#xff09;。保存完成后&#xff0c;让我们把游戏运行起来。 1&#xff0c;确认游戏视图标签页右上方的Maximize on Play图标处于按下状态&#xff0c;然后点击画面上方的播放按钮&#xff08;位于工具栏中间的播…

C++类模板学习

之前已经学习了函数模板&#xff0c;在这里&#xff0c; C函数模板Demo - win32 版_c编写的opc da demo_bcbobo21cn的博客-CSDN博客 下面学习类模板&#xff1b; VC6&#xff1b; 做一个星星类&#xff0c;Star&#xff1b; Star.h&#xff1b; #if !defined(AFX_STAR_H_…

(十二)VBA常用基础知识:worksheet的各种操作之sheet移动

当前sheet确认 把sheet1移动到sheet3前边 Sub Hello()10Worksheets("Sheet1").Move Before:Worksheets("Sheet3") End Sub3. 把sheet2移动到sheet1后边 Sub Hello()11Worksheets("Sheet2").Move after:Worksheets("Sheet1") End Sub…

MissionPlanner编译过程

环境 windows 10 mission planner 1.3.80 visual studio 2022 git 2.22.0 下载源码 (已配置git和ssh) 从github上克隆源码 git clone gitgithub.com:ArduPilot/MissionPlanner.git进入根目录 cd MissionPlanner在根目录下的ExtLibs文件下是链接的其它github源码&#xff0…

pymysql简介以及安装

视频版教程 Python操作Mysql数据库之pymysql模块技术 前面基础课程介绍了使用文件来保存数据&#xff0c;这种方式虽然简单、易用&#xff0c;但只适用于保存一些格式简单、数据量不太大的数据。对于数据量巨大且具有复杂关系的数据&#xff0c;当然还是推荐使用数据库进行保存…

79、SpringBoot 整合 R2DBC --- R2DBC 就是 JDBC 的 反应式版本, R2DBC 是 JDBC 的升级版。

★ 何谓R2DBC R2DBC 就是 JDBC 的 反应式版本&#xff0c; R2DBC 是 JDBC 的升级版。 R2DBC 是 Reactive Relational Database Connectivity (关系型数据库的响应式连接) 的缩写 反应式的就是类似于消息发布者和订阅者&#xff0c;有消息就进行推送。R2DBC中DAO接口中方法的…