kr 第三阶段(三)调试器

调试框架

  • 特点:事件驱动,事件响应。 Win32 程序是消息驱动响应的基址,而在调试器则是事件驱动响应,有事件则处理,无事件则去做别的事。
    • 事件:整个调试框架是建立在异常的基础之上的基本单位。
    • 响应:通过事件循环体,持续的获取事件,处理事件。

在这里插入图片描述

创建调试会话

创建进程

创建进程主要是通过 CreateProcess 函数来实现的,该函数的原型如下:

BOOL CreateProcess(LPCTSTR lpApplicationName,                 // name of executable moduleLPTSTR lpCommandLine,                      // command line stringLPSECURITY_ATTRIBUTES lpProcessAttributes, // SDLPSECURITY_ATTRIBUTES lpThreadAttributes,  // SDBOOL bInheritHandles,                      // handle inheritance optionDWORD dwCreationFlags,                     // creation flagsLPVOID lpEnvironment,                      // new environment blockLPCTSTR lpCurrentDirectory,                // current directory nameLPSTARTUPINFO lpStartupInfo,               // startup informationLPPROCESS_INFORMATION lpProcessInformation // process information);

下面是 CreateProcess 函数的一些参数的说明:

  • lpApplicationName:指向可执行文件的路径或者文件名。
  • lpCommandLine:指向一个以 null 结尾的字符串,包含了命令行参数。
  • lpProcessAttributeslpThreadAttributes:用于指定进程和线程的安全性属性,默认为 NULL。
  • bInheritHandles:指示新进程是否继承父进程的句柄。
  • dwCreationFlags:指定进程的创建标志,例如是否创建一个新的控制台窗口、创建方式等。
  • lpEnvironment:指定新进程的环境变量,如果为 NULL,则继承父进程的环境变量。
  • lpCurrentDirectory:指定新进程的当前工作目录,如果为 NULL,则继承父进程的当前目录。
  • lpStartupInfo:指向一个 STARTUPINFO 结构体,用于指定新进程的一些启动参数,例如窗口大小、显示方式等。
  • lpProcessInformation:指向一个 PROCESS_INFORMATION 结构体,用于接收新进程的相关信息,例如进程句柄和线程句柄。
  • CreateProcess 函数成功执行时会返回非零值,否则返回零。

针对 dwCreationFlags 参数,有 DEBUG_ONLY_THIS_PROCESSDEBUG_PROCESS 两个标志参数用于指定创建进程时的调试模式:

  • DEBUG_PROCESS:调试器会受到目标进程及由目标进程创建的所有子进程中发生的调试事件。
  • DEBUG_ONLY_THIS_PROCESS:调试器只收到目标进程的调试事件,对子进程不响应。

调试器通常设置的都是 DEBUG_ONLY_THIS_PROCESS 标志位。

附加

如果想要通过附加调试某一进程需要使用 DebugActiveProcess 函数,该函数定义如下:

BOOL DebugActiveProcess(DWORD dwProcessId   // process to be debugged
);
  • dwProcessId:要调试的目标进程的进程标识符(PID)。
  • 函数调用成功时,返回值为非零值;否则,返回值为零。
  • 该函数附加进程效果与创建进程使用 DEBUG_ONLY_THIS_PROCESS 效果类似。

如果想脱离附加的进程可以使用 DebugActiveProcessStop 函数,该函数定义如下:

BOOL DebugActiveProcessStop(DWORD dwProcessId
);
  • dwProcessId:要调试的目标进程的进程标识符(PID)。
  • 函数调用成功时,返回值为非零值;否则,返回值为零。

然而有时脱离附加的进程会导致被附加的进程退出,这时候就需要在调用 DebugActiveProcessStop 函数之前调用 DebugSetProcessKillOnExit 来防止被调试进程退出,该函数定义如下:

BOOL DebugSetProcessKillOnExit (  BOOL KillOnExit);
  • KillOnExit:指定调试器在退出时是否终止被调试的进程。如果设置为 TRUE,调试器在退出时将终止被调试的进程;如果设置为 FALSE,调试器在退出时不会终止被调试的进程。
  • 函数调用成功时,返回值为非零值;否则,返回值为零。

循环接受调试事件

等待调试事件使用的是 WatiForDebugEvent 函数,该函数定义如下:

BOOL WaitForDebugEvent(LPDEBUG_EVENT lpDebugEvent,  // debug event informationDWORD dwMilliseconds         // time-out value
);
  • lpDebugEvent:一个指向 DEBUG_EVENT 结构的指针,用于接收调试事件的信息。DEBUG_EVENT 结构包含了调试事件的类型和相关的数据,如调试进程、线程、异常等。
  • dwMilliseconds:等待调试事件的超时时间,以毫秒为单位。如果设置为 INFINITE (0xFFFFFFFF),则表示无限等待,直到有调试事件发生。如果设置为零,则表示不等待,立即返回。
  • 函数调用成功时,返回值为非零值;否则,返回值为零。

接收调试事件的 DEBUG_EVENT 结构体定义如下:

typedef struct _DEBUG_EVENT { DWORD dwDebugEventCode; DWORD dwProcessId; DWORD dwThreadId; union { EXCEPTION_DEBUG_INFO Exception; CREATE_THREAD_DEBUG_INFO CreateThread; CREATE_PROCESS_DEBUG_INFO CreateProcessInfo; EXIT_THREAD_DEBUG_INFO ExitThread; EXIT_PROCESS_DEBUG_INFO ExitProcess; LOAD_DLL_DEBUG_INFO LoadDll; UNLOAD_DLL_DEBUG_INFO UnloadDll; OUTPUT_DEBUG_STRING_INFO DebugString; RIP_INFO RipInfo; } u; 
} DEBUG_EVENT, *LPDEBUG_EVENT; 
  • dwDebugEventCode:表示调试事件的类型。可以是以下值之一:
    • EXCEPTION_DEBUG_EVENT:异常事件。
    • CREATE_THREAD_DEBUG_EVENT:线程创建事件。
    • CREATE_PROCESS_DEBUG_EVENT:进程创建事件。
    • EXIT_THREAD_DEBUG_EVENT:线程退出事件。
    • EXIT_PROCESS_DEBUG_EVENT:进程退出事件。
    • LOAD_DLL_DEBUG_EVENT:动态链接库加载事件。
    • UNLOAD_DLL_DEBUG_EVENT:动态链接库卸载事件。
    • OUTPUT_DEBUG_STRING_EVENT:输出调试字符串事件。
    • RIP_EVENT:RIP(调试错误)事件。
  • dwProcessId:调试事件所属的进程标识符(PID)。
  • dwThreadId:调试事件所属的线程标识符(TID)。
  • u:一个联合体,用于存储不同类型的调试事件数据。根据 dwDebugEventCode 的不同值,使用相应的字段来访问具体的调试事件数据。

处理调试事件

调试器具体代码实现。

提交处理结果

在处理调试事件时被调试进程是处于挂起的状态,因此提交处理结果是告诉被调试进程是否继续运行。提交处理结果的函数是 ContinueDebugEvent,该函数定义如下:

BOOL ContinueDebugEvent(DWORD dwProcessId,       // process to continueDWORD dwThreadId,        // thread to continueDWORD dwContinueStatus   // continuation status
);
  • dwProcessId:要继续运行的进程的标识符(PID)。
  • dwThreadId:要继续运行的线程的标识符(TID)。
  • dwContinueStatus:继续运行的状态。它可以是以下值之一:
    • DBG_CONTINUE:继续运行被调试进程。
    • DBG_EXCEPTION_NOT_HANDLED:异常未被处理,系统按照正常的异常处理流程派发异常。
  • 函数返回一个布尔值,表示操作是否成功。如果函数调用成功,返回值为非零值;如果函数调用失败,返回值为零。

环境搭建

所需工具都可以在附件中下载。

由于汇编代码可读性差,后面改用 C++ 实现(感觉w老师自己都蚌埠住了)。

汇编开发环境

由于这里使用的是汇编来编写调试框架,因此需要安装 RadASM(IDE) 和 MASM32(开发环境)。

  • RadASM 直接下载 RadASM-XXX-FullPackage.zip 然后解压到安装目录即可。
  • MASM32 就点下载链接然后一路默认安装即可,注意盘符最好选 C 盘,因为我这里 RadASM 是在 C 盘找 MASM32 的。

因为我们开发的是命令行式的调试器,因此创建项目时工程类型选择 Colsole App ,其他默认设置即可。

反汇编引擎

反汇编引擎这里我使用的是 Udis86 。该项目在 github 下载的源码缺少文件无法编译,因此我是从这里下载的源码进行编译。

由于该项目使用汇编调用过于麻烦,因此我先实现了一个提供汇编的接口项目 MuUdis86Dll

#include "pch.h"
#include "udis86.h"#pragma comment(lib, "libudis86.lib")extern "C" {__declspec(dllexport) DWORD GetAsm(BYTE *pCode,DWORD nCodeLen,DWORD nEIP,CHAR *szAsmBuf,DWORD dwAsmBufLen) {ud u;ud_init(&u);ud_set_mode(&u, 32);ud_set_pc(&u, nEIP);ud_set_input_buffer(&u, pCode, nCodeLen);ud_set_syntax(&u, UD_SYN_INTEL);ud_disassemble(&u);uint32_t nLen = ud_insn_len(&u);strcpy_s(szAsmBuf, dwAsmBufLen, ud_insn_asm(&u));return nLen;}
}

之后只需要在汇编项目中导入该项目及接口即可调用 GetAsm 函数实现反汇编。注意这里我没有声明 GetAsm 的调用约定因此默认采用 __cdecl 调用约定,因此在汇编中需要再改接口声明中加 C 来声明调用约定。

includelib MyUdis86Dll.libGetAsm proto C pCode:LPBYTE,nCodeLen:DWORD,nEip:DWORD,szAsmBuf:LPSTR,dwAsmBufLen:DWORD

软件断点

什么是软件断点

软件断点即 CC 断点或 int3 断点,OD中的快捷键F2,使用率也是最多的断点类型。以调试方式创建的进程,必定会有一个系统断点。

软件断点实现思路

  • 断得下来:需要再软件断点位置写入 int3(0xcc)
  • 走的过去:断点处的指令能够正常执行不受影响。
    • 触发 int3 异常后需要还原断点位置的指令。
    • 由于 int3 指令被执行了,因此还需要将 eip 寄存器减 1(int3 指令长度)。
  • 下次还来:下次执行到断点位置时还能断下来。
    • 由于触发 int3 异常后调试器将断点位置的指令恢复,因此在执行完断点位置的指令后需要再次在该位置写入 int3 指令。
    • 可以在触发 int3 异常时 TF 标志寄存器(EFLAGS 标志寄存器第 8 位)置位,这样在执行下一条指令的时候会触发单步异常。
    • 调试器在接收到该异常的时候可以恢复 int3 断点。

代码实现

创建断点实际上就是在断点位置写入 \xCC

    void HandleBpCmd(const std::vector<std::tstring> &args) {if (args.size() != 2) {std::tcout << _T("Invalid Command") << std::endl;return;} PVOID lpBreakPoint = (PVOID) tcstoull(args[1]);if (m_BreakPointList.count(lpBreakPoint)) {return;}m_Process.ReadMemory(lpBreakPoint, &m_BreakPointList[lpBreakPoint], sizeof(m_BreakPointList[lpBreakPoint]));m_Process.WriteMemory(lpBreakPoint, (LPVOID) &INT3, sizeof(INT3));}

如果调试事件 EXCEPTION_DEBUG_EVENT 到来且异常类型为断点异常 EXCEPTION_BREAKPOINT 那么:

  • 将 TF 标志寄存器置位。
  • 修正 eip 寄存器减 1 。
  • 恢复断点处的指令。
  • m_lpNeedRecoverBreakPoint 指向断点位置,以便后续 EXCEPTION_SINGLE_STEP 异常到来时恢复断点。
  • 进入命令行交互。
            case EXCEPTION_BREAKPOINT: {if (m_bIsSysBreakPoint) {std::cout << _T("EXCEPTION_BREAKPOINT") << std::endl;m_bIsSysBreakPoint = FALSE;} else {CONTEXT ctx{};ctx.ContextFlags = CONTEXT_CONTROL;m_Process.GetContext(ctx);ctx.Eip--;if (m_BreakPointList.count((LPVOID) ctx.Eip)) {ctx.EFlags |= 0x100;m_lpNeedRecoverBreakPoint = (LPVOID) ctx.Eip;m_Process.WriteMemory((LPVOID) ctx.Eip, &m_BreakPointList[(LPVOID) ctx.Eip], sizeof(m_BreakPointList[(LPVOID) ctx.Eip]));} m_Process.SetContext(ctx);}return PareseCommandLine();}

如果调试事件 EXCEPTION_DEBUG_EVENT 到来且异常类型为单步异常 EXCEPTION_SINGLE_STEPm_lpNeedRecoverBreakPoint 不为 NULL 则需要将断点回复。

            case EXCEPTION_SINGLE_STEP: {if (m_lpNeedRecoverBreakPoint != NULL) {m_Process.WriteMemory(m_lpNeedRecoverBreakPoint, (LPVOID) &INT3, sizeof(INT3));m_lpNeedRecoverBreakPoint = NULL;}}

效果展示

效果如下:

skydbg> u 0x772d78f1
772D78F1 mov dword [ebp-0x4], 0xfffffffe
772D78F8 mov ecx, [ebp-0x10]
772D78FB mov [fs:0x0], ecx
772D7902 pop ecx
772D7903 pop edi
772D7904 pop esi
772D7905 pop ebx
772D7906 leave
skydbg> bp 772D7902
skydbg> g
772D7902 pop ecx
skydbg> u
772D7902 pop ecx
772D7903 pop edi
772D7904 pop esi
772D7905 pop ebx
772D7906 leave
772D7907 ret
772D7908 int3
772D7909 int3

单步

单步命令实现思路

单步指令分为单步步入(t 命令)和单步步过(p 命令),这两条命令只有在 call 指令处会有所不同。

  • 单步步入:逢 call 则入
    • 设置 TF 标志位即可
  • 单步步过:逢 call 则过
    • 如果当前指令不是 call 则与单步步入相同
    • 否则在 call 的下一条指令设置临时断点

单步命令代码实现

在这里插入图片描述
为了能够让单步步入,单步步过,用户断点三者能够和谐相处,OnException 实现如上图所示。

注意:单步异常到来时当前指令还没有执行,而断点异常到来时当前指令(int3)已经被执行。

    DWORD OnException(DEBUG_EVENT& de) {switch (de.u.Exception.ExceptionRecord.ExceptionCode) {case EXCEPTION_BREAKPOINT: {if (m_bIsSysBreakPoint) {std::cout << _T("EXCEPTION_BREAKPOINT") << std::endl;m_bIsSysBreakPoint = FALSE;} else {CONTEXT ctx{};ctx.ContextFlags = CONTEXT_CONTROL;m_Process.GetContext(ctx);ctx.Eip--;if (m_BreakPointList.count((LPVOID) ctx.Eip)) {ctx.EFlags |= 0x100;m_lpNeedRecoverBreakPoint = (LPVOID) ctx.Eip;m_Process.WriteMemory((LPVOID) ctx.Eip, &m_BreakPointList[(LPVOID) ctx.Eip], sizeof(m_BreakPointList[(LPVOID) ctx.Eip]));} if ((LPVOID) ctx.Eip == m_lpSignelStepBreakPoint.first) {if (!m_BreakPointList.count((LPVOID) ctx.Eip)) {m_Process.WriteMemory((LPVOID) ctx.Eip, &m_lpSignelStepBreakPoint.second, sizeof(m_lpSignelStepBreakPoint.second));}m_lpSignelStepBreakPoint = {};m_nSingleStepCountP--;} m_Process.SetContext(ctx);}return PareseCommandLine();}case EXCEPTION_SINGLE_STEP: {if (m_lpNeedRecoverBreakPoint != NULL) {m_Process.WriteMemory(m_lpNeedRecoverBreakPoint, (LPVOID) &INT3, sizeof(INT3));m_lpNeedRecoverBreakPoint = NULL;}if (m_nSingleStepCountT || m_nSingleStepCountP) {if (m_nSingleStepCountT) { m_nSingleStepCountT--; assert(m_nSingleStepCountP == 0);}if (m_nSingleStepCountP) {m_nSingleStepCountP--;assert(m_nSingleStepCountT == 0);}CONTEXT ctx{};ctx.ContextFlags = CONTEXT_CONTROL;m_Process.GetContext(ctx);if (m_BreakPointList.count((LPVOID)ctx.Eip)) {ctx.EFlags |= 0x100;m_lpNeedRecoverBreakPoint = (LPVOID) ctx.Eip;m_Process.WriteMemory((LPVOID) ctx.Eip, &m_BreakPointList[(LPVOID) ctx.Eip], sizeof(m_BreakPointList[(LPVOID) ctx.Eip]));m_Process.SetContext(ctx);}return PareseCommandLine();}}}}

针对单步步入命令只需要 TF 置位。

    void HandleTCmd(const std::vector<std::tstring> &args) {if (args.size() > 2) {std::tcout << _T("Invalid Command") << std::endl;return;}if (!args.empty()) {m_nSingleStepCountT = args.size() == 2 ? tcstoull(args[1]) : 1;}CONTEXT ctx{};ctx.ContextFlags = CONTEXT_CONTROL;m_Process.GetContext(ctx);ctx.EFlags |= 0x100;m_Process.SetContext(ctx);}

单步步过命令需要判断指令是否是 call 指令,如果不是 call 指令处理方式和单步步过相同,否则需要再 call 指令的下一条指令设置临时断点。

    void HandlePCmd(const std::vector<std::tstring>& args) {if (args.size() > 2) {std::tcout << _T("Invalid Command") << std::endl;return;}if (!args.empty()) {m_nSingleStepCountP = args.size() == 2 ? tcstoull(args[1]) : 1;}LPVOID lpEip = m_Process.GetPC();std::string Code, Asm;m_Process.ReadMemory(lpEip, Code, 16);DWORD dwCodeLen = m_Asm.GetOneAsm(Code, lpEip, Asm);if (Asm.starts_with("call")) {m_lpSignelStepBreakPoint.first = (LPVOID) ((SIZE_T) lpEip + dwCodeLen);m_Process.ReadMemory(m_lpSignelStepBreakPoint.first, &m_lpSignelStepBreakPoint.second, sizeof(m_lpSignelStepBreakPoint.second));m_Process.WriteMemory(m_lpSignelStepBreakPoint.first, (LPVOID) &INT3, sizeof(INT3));} else {CONTEXT ctx{};ctx.ContextFlags = CONTEXT_CONTROL;m_Process.GetContext(ctx);ctx.EFlags |= 0x100;m_Process.SetContext(ctx);}}

这里我实现单步支持 trace 功能,即一次可以执行多步并记录执行过的命令,因此对于单步步入和步过我都记录了这条命令的剩余执行次数。在用户命令交互函数 PareseCommandLine 中,如果单步命令还有剩余执行次数则直接调用对应命令的处理函数自动执行。

    DWORD PareseCommandLine() {ShowOneAsm(m_Process.GetPC());if (m_nSingleStepCountT) {assert(m_nSingleStepCountP == 0);HandleTCmd({});return DBG_CONTINUE;}if (m_nSingleStepCountP) {assert(m_nSingleStepCountT == 0);HandlePCmd({});return DBG_CONTINUE;}...

效果展示

首先 trace 功能能够不受断点影响,且断点不受 trace 功能影响。

skydbg> u 0x772d78f1
772D78F1 mov dword [ebp-0x4], 0xfffffffe
772D78F8 mov ecx, [ebp-0x10]
772D78FB mov [fs:0x0], ecx
772D7902 pop ecx
772D7903 pop edi
772D7904 pop esi
772D7905 pop ebx
772D7906 leave
skydbg> bp 772D7902
skydbg> bp 772D7903
skydbg> bp 772D7904
skydbg> g
772D7902 pop ecx
skydbg> t 5
772D7903 pop edi
772D7904 pop esi
772D7905 pop ebx
772D7906 leave
772D7907 ret
skydbg> u 772D7902 6
772D7902 int3
772D7903 int3
772D7904 int3
772D7905 pop ebx
772D7906 leave
772D7907 ret

步过功能遇到 call 指令不会步入且临时断点自动去除。

skydbg> p 5
772D7905 pop ebx
772D7906 leave
772D7907 ret
772D1D3E call 0x7728b6c4
772D1D43 cmp byte [0x7734d1ae], 0x0
skydbg> u 772D1D3E 3
772D1D3E call 0x7728b6c4
772D1D43 cmp byte [0x7734d1ae], 0x0
772D1D4A jnz 0x772d1de5

利用单步异常实现反调试

主要利用了调试器先于 SEH 接管单步异常来实现反调试的特性(实际上这里的单步异常换成其他异常也是可以的)。
在这里插入图片描述

硬件断点

调试寄存器

IA-32 处理器定义了 8 个调试寄存器,分别称为 DR0~DR7 。这个 8 个寄存器结构如下图所示:
在这里插入图片描述

  • DR0~DR3:调试地址寄存器,用于保存 4 个硬件断点的地址。
  • DR4~DR5:保留未使用。
  • DR6:调试状态寄存器,用于在调试事件发生时向调试器报告详细信息。
    • B0~B3:如果其中任何一个置位,则表示是相应的 DR0~DR3 断点引发的调试陷阱。
    • BD:检测到访问调试寄存器,这一位与 DR7GD 位相联系,当 GD 位被置为 1,而且 CPU 发现了要修改调试寄存器(DR0~DR7)的指令时,CPU 会停止继续执行这条指令,把 BD 位设为 1,然后把执行权交给调试异常(#DB)处理程序。
    • BS:单步,这一位与标志寄存器的 TF 位相联系,如果该位为 1,则表示异常是由单步执行(single step)模式触发的。与导致调试异常的其他情况相比,单步情况的优先级最高,因此当此标志为 1 时,也可能有其他标志也为 1。
    • BT:任务切换,这一位与任务状态段(TSS)的 T 标志(调试陷阱标志,debug trap flag)相联系。当 CPU 在进行任务切换时,如果发现下一个任务的 TSS 的 T 标志为 1,则会设置 BT 位,并中断到调试中断处理程序。
    • DR6 寄存器的值建议在每次异常提交之前清除。
  • DR7:调试控制寄存器,用于进一步定义断点的中断条件。
    • R/W0~R/W3:读写域,分别与 DR0~DR3 这 4 个调试地址寄存器相对应,用来指定被监控地址的访问类型。
      • 00:执行断点
      • 01:写断点
      • 10:386 和 486 不支持此组合。对于以后的 CPU,可以通过把 CR4 寄存器的 DE(调试扩展)位设为 1 启用该组合,其含义为“当相应地址进行输入输出(即 I/O 读写)时中断”
      • 11:读写断点,但是从该地址读取指令除外。
    • LEN0~LEN3:长度域, 分别与 DR0~DR3 这 4 个调试地址寄存器相对应,用来指定被监控区域的长度。
      • 00:1 字节长
      • 01:2 字节长
      • 10:8 字节长(奔腾 4 或至强 CPU)或未定义(其他处理器)
      • 11:4 字节长
      • 注意:如果对应 R/Wn 为 0(即执行指令中断),那么这里的设置应该为 0 。
    • L0~L3:局部断点启用, 分别与 DR0~DR3 这 4 个调试地址寄存器相对应,用来启用或禁止对应断点的局部匹配。
      • 如果该位设为 1,当 CPU 在当前任务(线程,寄存器是线程独占的)中检测到满足所定义的断点条件时便中断,并且自动清除此位。
      • 如果该位设为 0,便禁止此断点。
    • G0~G3:全部断点启用,分别与 DR0~DR3 这 4 个调试地址寄存器相对应,用来全局启用或禁止对应的断点(实测没有用,要想真正设置区局断点需要遍历进程中的所有线程然后都设置 Ln,x64dbg 和 OllyDbg 都是这么实现的)。
      • 如果该位设为 1,当 CPU 在任何任务中检测到满足所定义的断点条件时便中断,不会自动清除此位。
      • 如果该位设为 0,便禁止此断点。
    • LEGE:启用局部或者全局(精确)断点。从 486 开始的 IA-32 处理器都忽略这两位的设置。此前这两位是用来启用或禁止数据断点匹配的。对于早期的处理器,当设置有数据断点时,需要启用本设置,这时CPU会降低执行速度,以监视和保证当有指令要访问符合断点条件的数据时产生调试异常。
    • GD:启用或禁止对调试寄存器的保护。当设为 1 时,如果 CPU 检测到将修改调试寄存器(DR0~DR7)的指令,CPU 会在执行这条指令前产生一个调试异常。

内存断点

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于ssm的互联网废品回收/基于web的废品资源利用系统

摘 要 本毕业设计的内容是设计并且实现一个基于SSM框架的互联网废品回收。它是在Windows下&#xff0c;以MYSQL为数据库开发平台&#xff0c;Tomcat网络信息服务作为应用服务器。互联网废品回收的功能已基本实现&#xff0c;主要包括用户、回收员、物品分类、回收物品、用户下单…

【技能树笔记】网络篇——练习题解析(四)

目录 前言 一、传输层的作用 1.1 传输层的作用 1.2 传输层的PDU 二、端口号的分类、作用 2.1 传输层的PDU 三、TCP协议的特点及应用 3.1 传输层的PDU 3.2 TCP协议连接的建立 四、UDP协议的特点及应用 4.1 UDP协议的特点 总结 前言 本篇文章给出了CSDN网络技能树中…

操作系统内存管理相关

1. 虚拟内存 1.1 什么是虚拟内存 虚拟内存是计算机系统内存管理的一种技术&#xff0c;我们可以手动设置自己电脑的虚拟内存。不要单纯认为虚拟内存只是“使用硬盘空间来扩展内存“的技术。虚拟内存的重要意义是它定义了一个连续的虚拟地址空间&#xff0c;并且 把内存扩展到硬…

2023年中国体育赛事行业现状及趋势分析:体育与科技逐步融合,推动产业高质量发展[图]

体育赛事运营是指组织体育赛事或获取赛事版权&#xff0c;并进行赛事推广营销、运营管理等一系列商业运作的运营活动。体育赛事运营相关业务主要包括赛事运营与营销、赛事版权运营两个部分。 体育赛事运营行业分类 资料来源&#xff1a;共研产业咨询&#xff08;共研网&#x…

Java 实现遍历一个文件夹,文件夹有100万数据,获取到修改时间在2天之内的数据

目录 1 需求2 实现1&#xff08;第一种方法&#xff09;2 实现2 &#xff08;推荐使用这个&#xff0c;快&#xff09;3 实现3&#xff08;推荐&#xff09; 1 需求 现在有一个文件夹&#xff0c;里面会一直存数据&#xff0c;动态的存数据&#xff0c;之后可能会达到100万&am…

JVM-满老师

JVM 前言程序计数器&#xff0c;栈&#xff0c;虚拟机栈&#xff1a;本地方法栈&#xff1a;堆&#xff0c;方法区&#xff1a;堆内存溢出方法区运行时常量池 前言 JVM 可以理解的代码就叫做字节码&#xff08;即扩展名为 .class 的文件&#xff09;&#xff0c;它不面向任何特…

SLAM从入门到精通(python开发)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 在ROS下面&#xff0c;开发的方法很多&#xff0c;可以是c&#xff0c;可以是python。大部分接口操作类的应用&#xff0c;其实都可以用python来开…

Day 04 python学习笔记

Python数据容器 元组 元组的声明 变量名称&#xff08;元素1&#xff0c;元素2&#xff0c;元素3&#xff0c;元素4…….&#xff09; &#xff08;元素类型可以不同&#xff09; eg: tuple_01 ("hello", 1, 2,-20,[11,22,33]) print(type(tuple_01))结果&#x…

<C++> 异常

C语言传统的处理错误的方式 传统的错误处理机制&#xff1a; 终止程序&#xff0c;如assert&#xff0c;缺陷&#xff1a;用户难以接受。如发生内存错误&#xff0c;除0错误时就会终止程序。返回错误码&#xff0c;缺陷&#xff1a;需要程序员自己去查找对应的错误。如系统的…

c++中的动态内存管理

目录 1.内存分布 2.c语言动态内存管理 3.c动态内存管理 4.operator new 与operator delete 函数 5.定位new 6.malloc/free 与 new/delete 的区别 1.内存分布 首先我们需要了解一下数据在内存中的分布&#xff0c;请看以下代码&#xff1a; int globalVar 1; static in…

2023年全球接口IP市场发展趋势分析:市占率第二IP品类,受大数据及计算需求推动高速增长[图]

接口IP是基于标准接口协议&#xff0c;实现芯片与内外部设备进行通信、传输数据的电路模块&#xff0c;分为有线接口IP与无线接口IP&#xff0c;主要用于数字信号处理和嵌入式系统中的接口设计。 接口IP分类 资料来源&#xff1a;共研产业咨询&#xff08;共研网&#xff09; …

计算机视觉——飞桨深度学习实战-深度学习网络模型

深度学习网络模型的整体架构主要数据集、模型组网以及学习优化过程三部分&#xff0c;本章主要围绕着深度学习网络模型的算法架构、常见模型展开了详细介绍&#xff0c;从经典的深度学习网络模型以CNN、RNN为代表&#xff0c;到为了解决显存不足、实时性不够等问题的轻量化网络…

【LeetCode热题100】--226.翻转二叉树

226.翻转二叉树 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode right) {* …

TouchGFX之后端通信

在大多数应用中&#xff0c;UI需以某种方式连接到系统的其余部分&#xff0c;并发送和接收数据。 它可能会与硬件外设&#xff08;传感器数据、模数转换和串行通信等&#xff09;或其他软件模块进行交互通讯。 Model类​ 所有TouchGFX应用都有Model类&#xff0c;Model类除了存…

苹果ios系统ipa文件企业签名是什么?优势是什么?什么场合需要应用到?

企业签名是苹果开发者计划中的一种签名类型&#xff0c;允许企业开发者签署和分发企业内部使用的应用程序&#xff0c;而无需通过App Store进行公开发布。通过企业签名&#xff0c;企业可以在内部部署自己的应用程序&#xff0c;以满足特定的业务需求。 企业签名能够做到以下…

搭建自己的搜索引擎之五

一、前言 接上文 搭建自己的搜索引擎之四&#xff0c;下面继续介绍茴香豆茴字的另外两种写法。 二、Jest Jest是ES的Java Http Rest客户端&#xff0c;它主要是为了弥补以前ES自有API缺少HttpRest接口客户端的不足&#xff0c;但因为现在ES官方已经提供了RestClient ,该项目已…

数据结构和算法

数据结构&#xff1a; 线性结构&#xff1a; 顺序存储方式&#xff0c;顺序表 常见的顺序存储结构有&#xff1a;数组、队列、链表、栈 链式存储方式&#xff0c;链表 非线性结构&#xff1a; 常见的非线性结构有&#xff1a;二维数组、多维数组、广义表、树结构、图结构 实…

Nginx 通过A域名代理B域名,保持A域名访问状态

在某些业务场景中需要一种代理方式&#xff0c;就是隐藏某个域名使用另一个域名去代理被需要隐藏的域名&#xff0c;在别人抓包或者别人查看访问地址的时候&#xff0c;看的域名都不是真实域名地址。所以需要用到这种代理方式。 需要被代理的B域名和地址&#xff1a; https:/…

基于Redis实现消息队列的实践

为什么要基于Redis实现消费队列&#xff1f; 消息队列是一种典型的发布/订阅模式&#xff0c;是专门为异步化应用和分布式系统设计的&#xff0c;具有高性能、稳定性及可伸缩性的特点&#xff0c;是开发分布式系统和应用系统必备的技术之一。目前&#xff0c;针对不同的业务场…

【计算机】CPU,芯片以及操作系统概述

1.CPU 什么是CPU? CPU&#xff08;Central Processing Unit&#xff09;是计算机系统的运算和控制核心&#xff0c;是信息处理、程序运行的最终执行单元&#xff0c;相当于系统的“大脑”。 CPU的工作流程&#xff1f; CPU 的工作流程分为以下 5 个阶段&#xff1a;取指令…