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.hqwc.cn/news/124388.html

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

相关文章

SpringBoot中使用拦截器

拦截器属于MVC中的内容 SpringBoot项目,引入web依赖即可 需要访问的控制器 拦截器第一步实现HandlerInterceptor接口 第二步实现WebMvcConfigurer接口,并重写addInterCeptors()方法,将自定义的拦截器注册 也就是说这里add进去拦截的请求,才会进入到prehandle方法,这里放行的请…

调用gethostbyname实现域名解析(附源码)

VC常用功能开发汇总&#xff08;专栏文章列表&#xff0c;欢迎订阅&#xff0c;持续更新...&#xff09;https://blog.csdn.net/chenlycly/article/details/124272585C软件异常排查从入门到精通系列教程&#xff08;专栏文章列表&#xff0c;欢迎订阅&#xff0c;持续更新...&a…

一文拿捏分布式协调Redis客服端-Redisson

Redisson 1.介绍 Redisson - 是一个高级的分布式协调Redis客服端 , 专注于分布式系统开发&#xff0c;让用户可以在分布式系统中很方便的去使用Redis。 2.相关使用 1.加锁 //底层是lua脚本保证了加锁的原子性 // 一直等待获取锁&#xff0c;直到获取到锁为止! 默认锁的存活…

【多模态融合】TransFusion学习笔记(1)

工作上主要还是以纯lidar的算法开发,部署以及系统架构设计为主。对于多模态融合(这里主要是只指Lidar和Camer的融合)这方面研究甚少。最近借助和朋友们讨论论文的契机接触了一下这方面的知识&#xff0c;起步是晚了一点&#xff0c;但好歹是开了个头。下面就借助TransFusion论文…

一键智能视频语音转文本——基于PaddlePaddle语音识别与Python轻松提取视频语音并生成文案

前言 如今进行入自媒体行业的人越来越多&#xff0c;短视频也逐渐成为了主流&#xff0c;但好多时候是想如何把视频里面的语音转成文字&#xff0c;比如&#xff0c;录制会议视频后&#xff0c;做会议纪要&#xff1b;比如&#xff0c;网课教程视频&#xff0c;想要做笔记&…

第 4 章 串(图书关键字索引表实现)

1. 背景说明 需要从书目文件中获取其关键字及对应的书号索引 bookInfo.txt 005 Computer Data Structures 010 Introduction to Data Structures 023 Fundamentals of Data Structures 034 The Design and Analysis of Computer Algorithms 050 Introduction to Numerical Anal…

Audacity 使用教程:轻松录制、编辑音频

Audacity 使用教程&#xff1a;轻松录制、编辑音频 1. 简介 Audacity 是一款免费、开源且功能强大的音频录制和编辑软件。它适用于 Windows、Mac 和 Linux 等多种操作系统&#xff0c;适合音乐制作、广播后期制作以及普通用户进行音频处理。本教程将带领大家熟悉 Audacity 的…

【OSCAR开源产业大会分论坛】开源大模型走向何方?

再过俩月&#xff0c;ChatGPT 即将迎来推出一周年纪念日。作为开历史先河的 AI 大模型&#xff0c;ChatGPT 像一针猛戳进千行百业中枢神经的兴奋剂&#xff0c;在全球掀起空前绝后的 AI 军备竞赛热潮。 近一年来&#xff0c;我们看到 GPT-3.5 完成向多模态的 GPT-4 进化&#x…

AWS-Lambda之导入自定义包-pip包

参考文档&#xff1a; https://repost.aws/zh-Hans/knowledge-center/lambda-import-module-error-python https://blog.csdn.net/fxtxz2/article/details/112035627 简单来说,以 " alibabacloud_dyvmsapi20170525 " 包为例 ## 创建临时目录 mkdir /tmp cd ./tmp …

网络基础(了解网络知识的前提)

前言 在正式学习网络之前&#xff0c;我们需要了解的一些关于计算机网络的基本知识&#xff0c;本文主要阐述这些基本知识&#xff0c;带着大家一步一步迈进互联网网络的世界&#xff1b; 一、局域网与广域网的概念 在正式了解这些概念的前提是我们要搞懂网络出现的意义&#x…

Leetcode202. 快乐数

力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 编写一个算法来判断一个数 n 是不是快乐数。 「快乐数」 定义为&#xff1a; 对于一个正整数&#xff0c;每一次将该数替换为它每个位置上的数字的平方和。然后重复这个过程直到这个数变为 1&#xff0…