Linux x86_64 C语言实现gdb断点机制

news2024/9/20 12:38:01

文章目录

  • 前言
  • 一、trap指令简介
  • 二、调用ptrace
  • 三、创建breakpoints
  • 四、CONT 和 SINGLESTEP
  • 五、完整代码演示
  • 六、增加参数检测
  • 参考资料

前言

本文参考文章:Implementing breakpoints on x86 Linux

一、trap指令简介

将通过在断点地址向目标进程的内存中插入一条新的CPU指令来实现断点。此指令应暂停目标进程的执行,并将控制权交还给操作系统,或者说将目标进程的控制权转移给其他进程,通过是调试进程。

有很多方法可以将控制权返回到操作系统,但希望最大限度地减少对正在进行热修补的代码的干扰。x86提供的int3指令,编码为单字节0xCC:在这里插入图片描述

当CPU执行int3时,它将停止它正在做的事情,并跳到内核函数do_int3函数服务例程,这是操作系统内核中的一段代码。在Linux上,此例程将向当前进程发送信号SIGTRAP。

由于我们将int3放入目标的代码中,因此目标将收到一个SIGTRAP。在正常情况下,这将调用目标的SIGTRAP处理程序,该处理程序通常会杀死进程。相反,我们希望跟踪过程拦截该信号,并将其解释为目标击中断点。我们将通过ptrace系统调用来实现这一点。

关于int3指令可以参考:GDB 源码分析 – 断点源码解析

定义trap指令:

#include <sys/reg.h>

#define REGISTER_IP RIP
#define TRAP_LEN    1
#define TRAP_INST   0xCC
#define TRAP_MASK   0xFFFFFFFFFFFFFF00

常量RIP定义在sys/reg.h文件中,用于标识保存指令指针的机器寄存器。

#ifdef __x86_64__
/* Index into an array of 8 byte longs returned from ptrace for
   location of the users' stored general purpose registers.  */
......
# define RIP	16
......

trap instruction存储为整数TRAP_INST,其字节长度为TRAP_LEN。这些在32位和64位x86上是相同的。陷阱指令是一个单字节,但我们将以一个机器字为增量读取和写入目标的内存,即32或64位。因此,我们将读取4或8个字节的机器代码,用TRAP_MASK清除第一个字节,并替换0xCC。由于x86是一个小端序体系结构,内存中的第一个字节是整数机器字的最低有效字节。

二、调用ptrace

所有各种ptrace请求都是通过一个名为ptrace的系统调用发出的。第一个参数指定请求的类型,第二个参数几乎总是目标的进程ID。

NAME
       ptrace - process trace

SYNOPSIS
       #include <sys/ptrace.h>

       long ptrace(enum __ptrace_request request, pid_t pid,
                   void *addr, void *data);

ptrace是Linux操作系统提供的一个系统调用,用于实现进程间的跟踪和调试功能。通过ptrace系统调用,一个进程(称为追踪器)可以监视和控制另一个进程(称为被追踪进程)的执行。
以下是ptrace系统调用的一些常见用法和功能:

(1)进程跟踪:追踪器可以使用ptrace系统调用启动对一个进程的追踪。追踪器可以监视被追踪进程的系统调用、信号传递、执行状态等,并在需要时对其进行控制。

(2)单步执行:通过使用ptrace系统调用的PTRACE_SINGLESTEP选项,追踪器可以实现单步执行功能,逐条执行被追踪进程的指令并进行调试和分析。

(3)寄存器访问:追踪器可以使用ptrace系统调用的PTRACE_GETREGS和PTRACE_SETREGS选项来读取和修改被追踪进程的寄存器状态,以实现寄存器级别的调试和修改。

(4)内存访问:通过ptrace系统调用的PTRACE_PEEKDATA和PTRACE_POKEDATA选项,追踪器可以读取和写入被追踪进程的内存数据,以进行内存级别的调试和修改。

(5)信号控制:追踪器可以使用ptrace系统调用的PTRACE_GETSIGINFO和PTRACE_SETSIGINFO选项来获取和修改被追踪进程收到的信号信息,以实现对信号的控制和处理。

(6)进程控制:通过ptrace系统调用的PTRACE_ATTACH和PTRACE_DETACH选项,追踪器可以附加到一个正在运行的进程并开始追踪,或者从被追踪进程中分离出来。

在我们可以调式目标进程之前,我们需要附加到它:

void breakfast_attach(pid_t pid) {
  int status;
  //附加到在pid中指定的进程,使其成为调用进程的跟踪对象(tracee)
  ptrace(PTRACE_ATTACH, pid);
  //使用waitid等待tracee停止
  waitpid(pid, &status, 0);
  ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_TRACEEXIT);
}

ptrace(PTRACE_ATTACH, pid) 来使指定进程号为pid的进程进入被追踪模式,这是一种使进程号为pid的进程被动进入被追踪模式。

PTRACE_ATTACH请求将使用SIGSTOP停止目标进程。我们等待目标进程接收到这个信号。

PTRACE_ATTACH
        Attach  to  the process specified in pid, making it a tracee of the calling process.  The tracee is sent a SIGSTOP, but will not necessarily have stopped by the com‐
        pletion of this call; use waitpid(2) to wait for the tracee to stop.

附加到在PID中指定的进程,使其成为调用进程的跟踪对象(tracee)。调用进程 tracer 给 tracee发送一个SIGSTOP信号,但不一定会在此调用完成时停止;使用waitid等待tracee停止。

请注意,ptrace和waitpid可能会以各种方式失败。在实际应用程序中,需要检查返回值和/或errno。为了简洁起见,在本文中省略了这些检查。

使用另一个ptrace请求来获取目标的指令指针的值:

target_addr_t breakfast_getip(pid_t pid) {
  long v = ptrace(PTRACE_PEEKUSER, pid, sizeof(long)*REGISTER_IP);
  //恢复执行时,将返回并执行用陷阱覆盖的原始指令。减去TRAP_LEN,得到下一条指令的真实地址
  return (target_addr_t) (v - TRAP_LEN);
}
PTRACE_PEEKUSER
       Read a word at offset addr in the tracee's USER area, which holds the registers and other information about the process (see <sys/user.h>).  The word is returned  as
       the result of the ptrace() call.

读取Tracee用户区中偏移量addr处的一个word ,该用户区保存寄存器和有关该过程的其他信息(参见<sys/user.h>)。该word 将作为ptrace()调用的结果返回。

由于目标进程已挂起,因此它不会在任何CPU上运行,并且它的指令指针也不会存储在实际的CPU寄存器中。相反,它被保存到内核内存中的“用户区域”。我们使用PTRACE_PEEKUSER请求以指定的字节偏移量从该区域读取机器字。sys/regs.h中的常量给出了寄存器的出现顺序,因此我们只需乘以sizeof(long)。
x86_64平台下一个寄存器八个字节。

#ifdef __x86_64__
/* Index into an array of 8 byte longs returned from ptrace for
   location of the users' stored general purpose registers.  */

# define R15	0
# define R14	1
# define R13	2
# define R12	3
# define RBP	4
# define RBX	5
# define R11	6
# define R10	7
# define R9	8
# define R8	9
# define RAX	10
# define RCX	11
# define RDX	12
# define RSI	13
# define RDI	14
# define ORIG_RAX 15
# define RIP	16
# define CS	17
# define EFLAGS	18
# define RSP	19
# define SS	20
# define FS_BASE 21
# define GS_BASE 22
# define DS	23
# define ES	24
# define FS	25
# define GS	26

在我们遇到断点后,保存的IP指向陷阱指令之后的指令。当我们恢复执行时,我们将返回并执行用陷阱覆盖的原始指令。所以我们减去TRAP_LEN,得到下一条指令的真实地址。

三、创建breakpoints

关于断点,我们需要记住两件事:我们替换的代码的地址和最初存在于那里的原始代码。

struct breakpoint {
  target_addr_t addr;   //替换的代码的地址
  long orig_code;		//原始代码指令
};

要启用断点,我们保存原始代码并插入陷阱指令:

static void enable(pid_t pid, struct breakpoint *bp) {
  //read bp->addr -->获取原始指令
  long orig = ptrace(PTRACE_PEEKTEXT, pid, bp->addr);
  //write 0xCC into bp->addr -->插入陷阱指令:0xCC
  ptrace(PTRACE_POKETEXT, pid, bp->addr, (orig & TRAP_MASK) | TRAP_INST);
  //保存原始指令
  bp->orig_code = orig;
}

PTRACE_PEEKTEXT请求从目标的代码地址空间读取一个机器字,由于历史原因,该地址空间被命名为“text”。PTRACE_POKETEXT写入该空间。在x86 Linux上,代码空间和数据空间实际上没有区别,因此PTRACE_PEEKDATA和PTRACE_POKEDATA也可以正常工作。

PTRACE_PEEKTEXT, PTRACE_PEEKDATA
       Read  a  word  at the address addr in the tracee's memory, returning the word as the result of the ptrace() call.  Linux does not have separate text and data address
       spaces, so these two requests are currently equivalent. 
PTRACE_POKETEXT, PTRACE_POKEDATA
       Copy the word data to the address addr in the tracee's memory.  As for PTRACE_PEEKTEXT and PTRACE_PEEKDATA, these two requests are currently equivalent.

创建断点非常简单:

struct breakpoint *breakfast_break(pid_t pid, target_addr_t addr) {
  struct breakpoint *bp = malloc(sizeof(*bp));
  bp->addr = addr;
  //启用断点 --> 插入陷阱指令:0xCC
  enable(pid, bp);
  return bp;
}

要禁用断点,我们只需写回保存的word(原始指令):

//写回保存的原始指令
static void disable(pid_t pid, struct breakpoint *bp) {
  ptrace(PTRACE_POKETEXT, pid, bp->addr, bp->orig_code);
}

四、CONT 和 SINGLESTEP

一旦我们连接到目标,它的执行就会停止。以下是如何恢复它:

static int run(pid_t pid, int cmd) {
  int status, last_sig = 0, event;
  while (1) {
    ptrace(cmd, pid, 0, last_sig);
    waitpid(pid, &status, 0);

    if (WIFEXITED(status))
      return 0;

    if (WIFSTOPPED(status)) {
      last_sig = WSTOPSIG(status);
      if (last_sig == SIGTRAP) {
        event = (status >> 16) & 0xffff;
        return (event == PTRACE_EVENT_EXIT) ? 0 : 1;
      }
    }
  }
}

int breakfast_run(pid_t pid, struct breakpoint *bp) {
  if (bp) {
    ptrace(PTRACE_POKEUSER, pid, sizeof(long)*REGISTER_IP, bp->addr);

	//恢复原始指令
    disable(pid, bp);
    //单步执行原始指令
    if (!run(pid, PTRACE_SINGLESTEP))
      return 0;
    //重新启用断点
    enable(pid, bp);
  }
  return run(pid, PTRACE_CONT);
}

我们要求ptrace继续执行——但如果我们从断点恢复,我们必须首先进行一些清理。我们回退指令指针,以便下一条要执行的指令在断点处。然后我们禁用断点,使目标只执行一条指令,单步执行断点处的原始指令。一旦我们通过了断点,我们就可以在下次重新启用它。如果目标退出,run将返回0,理论上这可能发生在我们的单个步骤中。

断点处理过程:

命中断点-->触发int3异常-->调试器观测目标进程-->恢复原始指令(回退指令指针,回退一个字节)-->单步执行原始指令-->重新下断点0xcc

对于gdb调试器:
当断点命中中断到调试器时,调试器会把所在断点处的 int 3指令恢复成原始指令。因此,在用户发出了恢复执行命令后,此时断点处的指令已经是正常的原始指令了,因此要做一些处理,以至于下次还能继续命中该断点。调试器在通知系统真正恢复程序执行前,调试器需要将断点列表中的该断点位置重新启用该断点。但是对于刚才命中的这个断点需要特别对待,试想如果把这个断点处的指令也替换为int 3指令,那么程序一执行便又触发断点了。但是如果不替换,那么这个断点便没有被启动,程序下次执行到这里时就不会触发断点,而用户并不知道这一点。对于这个问题,大多数调试器的做法都是先单步执行一次,单步执行一条指令。也就是说,先设置单步执行标志,然后恢复执行,将断点所在位置的指令执行完。因为设置了单步标志,所以,CPU执行完断点位置的这条指令后会立刻再中断到调试器中,这一次调试器不会通知用户,会做一些内部操作后便立刻恢复程序执行,而且将该断点启动。

PTRACE_CONT
       Restart the stopped tracee process.  If data is nonzero, it is interpreted as the number of a signal to be delivered to the tracee; otherwise, no  signal  is  deliv‐
       ered.  Thus, for example, the tracer can control whether a signal sent to the tracee is delivered or not.

PTRACE_CONT是一个用于重新启动被停止的被追踪进程的ptrace系统调用选项。当tracer调用PTRACE_CONT时,被追踪的进程tracee将继续执行。
PTRACE_CONT的行为如下:

如果提供的data参数为非零值,则被解释为要发送给被追踪进程的信号编号。这意味着可以控制是否向被追踪进程发送信号。
如果data参数为零,则不向被追踪进程发送任何信号。
当调用PTRACE_CONT时,被追踪进程将从之前被停止的位置继续执行,并且可能会在之后再次被停止,具体取决于陷阱事件和追踪器的设置。
追踪器可以通过调用ptrace系统调用并使用PTRACE_CONT选项来控制被追踪进程的执行流程,包括在适当的时机发送信号以及决定是否重新启动进程。

PTRACE_CONT是一种ptrace系统调用选项,用于重新启动被停止的被追踪进程,并可选择发送信号给被追踪进程。通过使用PTRACE_CONT,追踪器可以对被追踪进程的执行进行控制和管理。

对于run函数:

static int run(pid_t pid, int cmd) {
  int status, last_sig = 0, event;
  while (1) {
    ptrace(cmd, pid, 0, last_sig);
    waitpid(pid, &status, 0);

    if (WIFEXITED(status))
      return 0;

    if (WIFSTOPPED(status)) {
      last_sig = WSTOPSIG(status);
      if (last_sig == SIGTRAP) {
        event = (status >> 16) & 0xffff;
        return (event == PTRACE_EVENT_EXIT) ? 0 : 1;
      }
    }
  }
}

cmd是PTRACE_CONT或PTRACE_SINGLESTEP。对于PTRACE_SINGLESTEP,OS将设置一个控制位,以使CPU在一条指令完成后引发int3,即单步调试功能。

PTRACE_SYSCALL, PTRACE_SINGLESTEP
       Restart the stopped tracee as for PTRACE_CONT, but arrange for the tracee to be stopped at the next entry to or exit from a system call, or after execution of a sin‐
       gle instruction, respectively.  (The tracee will also, as usual, be stopped upon receipt of a signal.)  From the tracer's perspective, the tracee will appear to have
       been  stopped  by  receipt  of  a  SIGTRAP.   So, for PTRACE_SYSCALL, for example, the idea is to inspect the arguments to the system call at the first stop, then do
       another PTRACE_SYSCALL and inspect the return value of the system call at the second stop.  The data argument is treated as for PTRACE_CONT.

用于单步执行被追踪进程的指令。当使用PTRACE_SINGLESTEP选项调用ptrace时,被追踪进程会在执行一条指令后停止。
以下是关于PTRACE_SINGLESTEP的一些要点:

当被追踪进程收到PTRACE_SINGLESTEP指令后,它会执行一条指令,并在执行完毕后立即停止。这样,追踪器就有机会检查指令的执行结果、寄存器状态或其他相关信息。
追踪器可以利用这个停止点来实现单步调试的功能,例如在每个步骤中检查变量的值、跟踪指令执行路径或进行其他调试操作。

在SIGTRAP的情况下,我们检查状态的位16-31的值PTRACE_EVENT_EXIT,它指示目标即将退出。回想一下,我们通过设置选项PTRACE_O_TRACEEXIT请求了此通知。你可能会认为(至少,我是这么认为的)检查WIFEXITED就足够了。但我遇到了一个问题,向目标发送致命信号会使跟踪过程永远循环。我通过增加一次额外的检查来解决这个问题。

五、完整代码演示

// breakfast.h

#ifndef _BREAKFAST_H
#define _BREAKFAST_H

#include <sys/types.h>  /* for pid_t */

typedef void *target_addr_t;
struct breakpoint;

void breakfast_attach(pid_t pid);
target_addr_t breakfast_getip(pid_t pid);
struct breakpoint *breakfast_break(pid_t pid, target_addr_t addr);
int breakfast_run(pid_t pid, struct breakpoint *bp);

#endif

// breakfast.c

#include <sys/ptrace.h>
#include <sys/reg.h>
#include <sys/wait.h>
#include <stdlib.h>

#include "breakfast.h"

#define REGISTER_IP RIP
#define TRAP_LEN    1
#define TRAP_INST   0xCC
#define TRAP_MASK   0xFFFFFFFFFFFFFF00


void breakfast_attach(pid_t pid) {
  int status;
  ptrace(PTRACE_ATTACH, pid);
  waitpid(pid, &status, 0);
  ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_TRACEEXIT);
}

target_addr_t breakfast_getip(pid_t pid) {
  long v = ptrace(PTRACE_PEEKUSER, pid, sizeof(long)*REGISTER_IP);
  return (target_addr_t) (v - TRAP_LEN);
}

struct breakpoint {
  target_addr_t addr;
  long orig_code;
};

static void enable(pid_t pid, struct breakpoint *bp) {
  long orig = ptrace(PTRACE_PEEKTEXT, pid, bp->addr);
  ptrace(PTRACE_POKETEXT, pid, bp->addr, (orig & TRAP_MASK) | TRAP_INST);
  bp->orig_code = orig;
}

static void disable(pid_t pid, struct breakpoint *bp) {
  ptrace(PTRACE_POKETEXT, pid, bp->addr, bp->orig_code);
}

struct breakpoint *breakfast_break(pid_t pid, target_addr_t addr) {
  struct breakpoint *bp = malloc(sizeof(*bp));
  bp->addr = addr;
  enable(pid, bp);
  return bp;
}

static int run(pid_t pid, int cmd) {
  int status, last_sig = 0, event;
  while (1) {
    ptrace(cmd, pid, 0, last_sig);
    waitpid(pid, &status, 0);

    if (WIFEXITED(status))
      return 0;

    if (WIFSTOPPED(status)) {
      last_sig = WSTOPSIG(status);
      if (last_sig == SIGTRAP) {
        event = (status >> 16) & 0xffff;
        return (event == PTRACE_EVENT_EXIT) ? 0 : 1;
      }
    }
  }
}

int breakfast_run(pid_t pid, struct breakpoint *bp) {
  if (bp) {
    ptrace(PTRACE_POKEUSER, pid, sizeof(long)*REGISTER_IP, bp->addr);

    disable(pid, bp);
    if (!run(pid, PTRACE_SINGLESTEP))
      return 0;
    enable(pid, bp);
  }
  return run(pid, PTRACE_CONT);
}
// test.c

#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>

#include <sys/ptrace.h>
#include <sys/reg.h>
#include <sys/user.h>

#include "breakfast.h"

int fact(int n) {
  if (n <= 1)
    return 1;
  return n * fact(n-1);
}

void child() {
  //getpid()获取子进程的pid,给其发送 SIGSTOP 信号
  kill(getpid(), SIGSTOP);
  printf("fact(5) = %d\n", fact(5));
}

void parent(pid_t pid) {

  struct breakpoint *fact_break, *last_break = NULL;
  void *fact_ip = fact, *last_ip;

  //该pid是子进程的pid
  breakfast_attach(pid);

  fact_break = breakfast_break(pid, fact_ip);

  while (breakfast_run(pid, last_break)) {
    last_ip = breakfast_getip(pid);
    if (last_ip == fact_ip) {

      int arg = ptrace(PTRACE_PEEKUSER, pid, sizeof(long)*RDI);

      printf("Break at fact(%d)\n", arg);
      last_break = fact_break;

    } else {
      printf("Unknown trap at %p\n", last_ip);
      last_break = NULL;
    }
  }
}


int main() 
{
  pid_t pid = fork();

  if(pid == 0)
    //子进程
    child();
  else if(pid > 0)
    //父进程,pid是子进程pid
    parent(pid);
  else{
    printf("fork error\n");
    return -1;
  }
  return 0;
}

调用fork函数,返回一个父进程和一个子进程。子进程会做一些希望观察到的计算。这里计算一下著名的阶乘函数:

int fact(int n) {
  if (n <= 1)
    return 1;
  return n * fact(n-1);
}

void child() {
  //getpid()获取子进程的pid,给其发送 SIGSTOP 信号
  kill(getpid(), SIGSTOP);
  printf("fact(5) = %d\n", fact(5));
}

父进程将调用PTRACE_ATTACH 并发送子进程SIGSTOP,但子进程可能会在父进程有机会调用ptrace之前完成执行。所以让孩子自己停止自己。在附加到长时间运行的进程时,这不是问题。

实际上,对于fork-跟踪子模式,应该使用PTRACE_TRACEME。
PTRACE_TRACEME – 被调试的进程调用 ptrace(PTRACE_TRACEME, …) 来使自己进入被追踪模式,是进程自己主动进入被追踪模式。gdb调试程序时便是采用此种模式。
我们这里只是做一个小的实验,选择用了PTRACE_ATTACH模式。

父进程用breakfast_break给子进程设置断点:

void parent(pid_t pid) {
  struct breakpoint *fact_break, *last_break = NULL;
  void *fact_ip = fact, *last_ip;
  breakfast_attach(pid);
  fact_break = breakfast_break(pid, fact_ip);
  while (breakfast_run(pid, last_break)) {
    last_ip = breakfast_getip(pid);
    if (last_ip == fact_ip) {
      printf("Break at fact()\n");
      last_break = fact_break;
    } else {
      printf("Unknown trap at %p\n", last_ip);
      last_break = NULL;
    }
  }
}

原则上,我们可以使用breakfast 来跟踪我们拥有的任何正在运行的进程,即使我们没有它的源代码。但我们仍然需要一种方法来找到有趣的断点地址。在这里,这是我们想要的最简单的方法:fork()通过一个进程(父进程)创建一个新进程(子进程),子进程是父进程的副本,因此子进程和父进程共享代码段,所以父子进程 fact function 地 址的一样。

# ./test
Break at fact()
Break at fact()
Break at fact()
Break at fact()
Break at fact()
fact(5) = 120

六、增加参数检测

计数函数调用的功能对于性能评测已经很有用了。但我们通常希望从停止的目标中获得更多信息。让我们看看我们是否能读懂传递给fact函数的参数。这部分将专门针对64位x86,尽管这个想法是通用的。

每个体系结构都定义了一个C调用约定,该约定指定了函数参数的传递方式,通常使用寄存器和堆栈槽的组合。在64位x86上,第一个参数在RDI寄存器中传递。可以通过运行objdump-d测试并查看反汇编的代码来验证这一点:
在这里插入图片描述
由于fact函数的参数类型时int,因此用RDI寄存器的低32位即可,即EDI寄存器。

因此,我们将修改test.c以读取此寄存器:

void parent(pid_t pid) {

  struct breakpoint *fact_break, *last_break = NULL;
  void *fact_ip = fact, *last_ip;

  //该pid是子进程的pid
  breakfast_attach(pid);

  //设置断点
  fact_break = breakfast_break(pid, fact_ip);

  //breakfast_run函数中当执行断点原始指令后会重新启用断点
  //断点的流程:int3 --> 恢复原始指令 --> 单步执行原始指令 -->重新启用断点
  while (breakfast_run(pid, last_break)) {
    
    //子进程此时是stopped状态,指令指针寄存器的值保存到内核内存中的“用户区域”
    //来获取子进程指令指针的值
    last_ip = breakfast_getip(pid);
    if (last_ip == fact_ip) {

      //读取寄存器RDI的值 -- 函数调用时RDI用来传递第一个参数
      int arg = ptrace(PTRACE_PEEKUSER, pid, sizeof(long)*RDI);

      printf("Break at fact(%d)\n", arg);
      last_break = fact_break;

    } else {
      printf("Unknown trap at %p\n", last_ip);
      last_break = NULL;
    }
  }
}
# ./test
Break at fact(5)
Break at fact(4)
Break at fact(3)
Break at fact(2)
Break at fact(1)
fact(5) = 120

x86体系结构具有用于设置断点的专用寄存器,但受到各种限制。我们忽略了这个特性,而选择了更灵活的软件断点。硬件断点可以做一些这种技术做不到的事情,比如中断从特定内存地址读取的操作。

参考资料

Implementing breakpoints on x86 Linux
https://blog.csdn.net/dog250/article/details/106267041

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

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

相关文章

【数据库必备插件】Navicat Premium 15安装教程

软件下载 软件&#xff1a;Navicat Premium版本&#xff1a;15语言&#xff1a;简体中文大小&#xff1a;68.85M安装环境&#xff1a;Win11/Win10/Win8/Win7硬件要求&#xff1a;CPU2.0GHz 内存4G(或更高&#xff09;下载通道①百度网盘丨64位下载链接&#xff1a;https://pan…

【MATLAB第70期】基于MATLAB的LightGbm(LGBM)梯度增强决策树多输入单输出分类预测模型(全网首发,敬请期待)

【MATLAB第70期】基于MATLAB的LightGbm(LGBM)梯度增强决策树多输入单输出分类预测模型&#xff08;全网首发&#xff0c;敬请期待&#xff09; (LGBM)是一种基于梯度增强决策树(GBDT)算法。 基于MATLAB的LightGbm即将研究测试上线。 下一个研究对象&#xff1a; ABCBOOST模型 一…

【大数据之Kafka】六、Kafka Broker工作流程

1 Zookeeper存储的Kafka消息 &#xff08;1&#xff09;启动zookeeper可客户端 [lyxhadoop102 zookeeper-3.5.7]$ bin/zkCli.sh&#xff08;2&#xff09;通过ls命令查看Kafka相关信息 [zk: localhost:2181(CONNECTED) 0] ls /kafka2 Kafka Broker总体工作流程 Zookeeper集…

在springmvc框架中加入tomcat插件失败(找不到插件)

思考问题 在仓库里找不到tomcat插件&#xff0c;与springmvc无关&#xff0c;应该是tomcat版本错最终结论&#xff1a;tomcat插件没用过&#xff0c;所以在idea中找不到&#xff0c;需要从maven仓库中下载解法&#xff1a; 1、复制后面括号里的仓库地址&#xff0c;在浏览器打开…

AMBA_AXI Protocol_基本读写事务

基本读写事务 1. 握手的过程 2. 信道信令要求 3. 通道之间的关系1. 握手的过程 当地址、数据或控制信息可用时&#xff0c;源端&#xff08;source&#xff09;产生VALID信号。终端&#xff08;destination&#xff09;生成READY信号&#xff0c;表示它可以接受该信息。传输只…

【闭源期刊】Elsevier旗下,1区(TOP),3个月录用

闭源期刊 1区&#xff08;TOP&#xff09; 出版社&#xff1a;Elsevier 影响因子&#xff1a;IF&#xff08;2022&#xff09;8.5-9.0 期刊分区&#xff1a;JCR1区&#xff0c;中科院2区&#xff08;无预警记录&#xff09; 检索情况&#xff1a;SCIE&EI 双检&#xff…

Nacos使用(上):Nacos安装

Nacos使用(上)&#xff1a;Nacos安装 Nacos简介 ​ Nacos /nɑ:kəʊs/ 是阿里巴巴的开源项目&#xff0c;是 Dynamic Naming and Configuration Service的首字母简称&#xff0c;一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 ​ Nacos作用类似于Spring…

【人工智能】—_监督学习、分类问题、决策树、信息增益

文章目录 Decision Trees 决策树建立决策树分类模型的流程如何建立决策树? 决策树学习表达能力决策树学习信息论在决策树学习中的应用特征选择准则一&#xff1a;信息增益举例 结论不足 回到餐厅的例子从12个例子中学到的决策树&#xff1a; Decision Trees 决策树 什么是决策…

基础知识回顾:借助 SSL/TLS 和 NGINX 进行 Web 流量加密

原文作者&#xff1a; Robert Haynes 原文链接&#xff1a; 基础知识回顾&#xff1a;借助 SSL/TLS 和 NGINX 进行 Web 流量加密 NGINX 唯一中文官方社区 &#xff0c;尽在 nginx.org.cn 网络攻击者肆无忌惮、作恶多端&#xff0c;几乎每天都有网络入侵、数据窃取或勒索软件攻击…

委托和方法的异步调用

概述 异步调用&#xff0c;通常情况下用于执行一些耗时的操作&#xff0c;同时能够保证当前主线程在执行异步调用之后依然能够继续向下执行。异步执行时&#xff0c;系统往往会创建一个新的线程&#xff0c;但需注意&#xff0c;当每一个异步执行都需要创建线程时&…

【校招VIP】java语言考点之synchronized和volatile

考点介绍&#xff1a; synchronized和volatile两个关键字也是校招常考点之一。volatile可以禁止进行指令重排。synchronized可作用于一段代码或方法&#xff0c;既可以保证可见性&#xff0c;又能够保证原子性...... 『java语言考点之synchronized和volatile』相关题目及解析…

C语言:指针数组

一、指针数组介绍 指针数组本质是数组&#xff0c;是一个存放指针的数组 代码如下&#xff1a; arr1和arr2就是指针数组 int main() {int a 1; int *pa &a;int b 2; int *pb &b;int c 3; int *pc &c;int d 4; int *pd &d;int e 5; int *pe &e;in…

【pyqt5界面化工具开发-12】QtDesigner图形化界面设计

目录 0x00 前言 一、启动程序 二、基础的使用 三、保存布局文件 四、加载UI文件 0x00 前言 关于QtDesigner工具的配置等步骤&#xff08;网上链接也比较多&#xff09; 下列链接非本人的&#xff08;如果使用pip 在命令行安装过pyqt5以及tools&#xff0c;那么就可以跳过…

Ansible学习笔记5

copy模块&#xff1a;&#xff08;重点&#xff09; copy模块用于对文件的远程拷贝&#xff08;如把本地的文件拷贝到远程主机上。&#xff09; 在master的主机上准备一个文件&#xff0c;拷贝文件到group1的所有主机上。 这个用的频率非常高&#xff0c;非常有用的一个模块…

28 - restful -request和response

response: 需要定制返回字段的格式 request: 需要校验前端传来的参数 代码示例: 1. 创建模型类 from datetime import datetime from ext import dbclass User(db.Model):id db.Column(db.Integer, primary_keyTrue, autoincrementTrue)username db.Colu…

Error:java: OutOfMemoryError: insufficient memory

现象&#xff1a;idea编译代码&#xff08;Build&#xff09;报错&#xff1a;Error:java: OutOfMemoryError: insufficient memory 亲测有效 在进行代码编译的时候出现以上的提示。从中可以看是内存方面的问题。只需要调节IDEA在编译过程中&#xff0c;内存大小的设置&#…

面向对象的设计原则

设计模式 Python 设计模式&#xff1a;对软件设计中普遍存在&#xff08;反复出现&#xff09;的各种问题&#xff0c;所提出的解决方案。每一个设计模式系统地命名、解释和评价了面向对象系统中一个重要的和重复出现的设计 面向对象 三大特性&#xff1a;封装、继承、多态 …

代码随想录笔记--哈希表篇

目录 1--有效的字母异位词 2--两个数组的交集 3--两数之和 4--四数相加II 5--三数之和 6--四数之和 1--有效的字母异位词 利用哈希表存储每个字母的出现次数&#xff0c;比较两个字符串各个字母出现次数是否相等即可&#xff1b; #include <iostream> #include <…

常用的css样式

1&#xff1a;flex布局 .flex-between {display: flex;justify-content: space-between; }.flex-evenly {display: flex;justify-content: space-evenly; }.flex-end {display: flex;justify-content: flex-end; }.flex {display: flex; }.flex-center {display: flex;justify…

实验室的服务器和本地pycharm怎么做图传

参考 远程调试 qt.qpa.xcb: could not connect to display, echo DISPLAY为空[已解决]_功夫小象的博客-CSDN博客 先安装x11 MobaXterm x11-forwarding_C--G的博客-CSDN博客 我是在容器中搞得 1&#xff0c;安装qt5 pip install PyQt5 -i https://pypi.douban.com/simple …