Linux x86_64 dump_stack()函数基于FP栈回溯

news2024/11/19 6:23:43

文章目录

  • 前言
  • 一、dump_stack函数使用
  • 二、dump_stack函数源码解析
    • 2.1 show_stack
    • 2.2 show_stack_log_lvl
    • 2.3 show_trace_log_lvl
    • 2.4 dump_trace
    • 2.5 print_context_stack
  • 参考资料

前言

Linux x86_64
centos7
Linux:3.10.0

一、dump_stack函数使用

dump_stack函数用于打印当前任务的信息以及其堆栈跟踪,能够用来回溯打印调用栈信息。

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

void noinline func_c(void)
{
	dump_stack();	
}

void noinline func_b(void)
{
	func_c();	
}

void noinline func_a(void)
{
	func_b();
}

//内核模块初始化函数
static int __init lkm_init(void)
{
	func_a();
	return 0;
}

//内核模块退出函数
static void __exit lkm_exit(void)
{
	printk("Goodbye\n");
}

module_init(lkm_init);
module_exit(lkm_exit);

MODULE_LICENSE("GPL");

这里加了noinline修饰,否则会被优化成 inline 函数。

[1109990.858938] Call Trace:
[1109990.858952]  [<ffffffff8e781340>] dump_stack+0x19/0x1b
[1109990.858960]  [<ffffffffc0a3700e>] func_c+0xe/0x10 [helloworld]
[1109990.858968]  [<ffffffffc0a3701e>] func_b+0xe/0x10 [helloworld]
[1109990.858974]  [<ffffffffc0a3702e>] func_a+0xe/0x10 [helloworld]
[1109990.858981]  [<ffffffffc0153009>] lkm_init+0x9/0x1000 [helloworld]
[1109990.858990]  [<ffffffff8e00210a>] do_one_initcall+0xba/0x240
[1109990.858999]  [<ffffffff8e11e45a>] load_module+0x271a/0x2bb0
[1109990.859007]  [<ffffffff8e3b4290>] ? ddebug_proc_write+0x100/0x100
[1109990.859016]  [<ffffffff8e119fe3>] ? copy_module_from_fd.isra.44+0x53/0x150
[1109990.859024]  [<ffffffff8e11ead6>] SyS_finit_module+0xa6/0xd0
[1109990.859033]  [<ffffffff8e793f92>] system_call_fastpath+0x25/0x2a

Linux dump_stack 函数原理:
栈帧如下如图所示:callee的RBP寄存器的值保存caller的RBP寄存器地址,可以看作每个栈帧用单链表连接。

// linux-3.10/arch/x86/include/asm/stacktrace.h

/* The form of the top of the frame on the stack */
struct stack_frame {
	struct stack_frame *next_frame;
	unsigned long return_address;
};

在这里插入图片描述

帧指针起到了历史上的作用。帧指针是一个寄存器,它始终包含着上一个堆栈指针的值。在 x86_64 架构中,通常使用的寄存器是 RBP。

由于帧指针寄存器的存在,堆栈现在成为了一个“堆栈帧”的链表,我们可以一直沿着链表向前遍历到开头。在任何时刻,我们只需查看当前帧指针寄存器的值,就可以获得先前的 RSP 值。由于先前的 RSP 值恰好是存储先前帧指针的位置,因此这就是一系列指针沿着堆栈向上爬行的过程。

通过遍历堆栈帧链表,我们可以逐个获取每个函数的返回地址、参数和局部变量等信息。这样,我们就可以按顺序打印每个函数的名称,实现堆栈跟踪。

帧指针寄存器的存在使得堆栈帧之间形成了链式结构,使得在堆栈跟踪过程中可以方便地从当前帧指针寄存器获取前一个堆栈帧的位置。通过这种方式,我们可以沿着堆栈链表一直向上遍历,获取所有函数的信息。

帧指针寄存器的使用使得堆栈跟踪变得更加直观和可靠,因为它提供了一种可靠的方式来遍历堆栈帧链表。但是需要注意的是,某些情况下,编译器可能会对帧指针进行优化或省略,因此在特定的编译器优化设置下,帧指针可能不可用或不准确。

centos 7 配置了CONFIG_FRAME_POINTER选项:

# cat /boot/config-3.10.0-1160.el7.x86_64 | grep CONFIG_FRAME_POINTER
CONFIG_FRAME_POINTER=y

(1)基于Frame Pointer - fp寄存器的栈回溯:
优点:栈回溯比较快,理解简单。相对较简单:基于Frame Pointer寄存器的栈回溯通常比解析unwind节更简单直接。
缺点:gcc添加了优化选项 -O 就会省略掉省略基指针。这样就不能都通过这种形式进行栈回溯了。
-fomit-frame-pointer编译标志进行优化:避免将%rbp用作栈帧指针,把FP当作一个通用寄存器,这样就提供了一个额外的通用寄存器,提高程序运行效率。

(1)func_c RBP寄存器的值存放了父函数func_b的RBP寄存器的地址。其返回地址 = func_cRBP寄存器地址+8。
(2)对函数func_b的RBP寄存器的地址取值获取func_b RBP寄存器的值,func_b RBP寄存器的值存放了父函数func_a的RBP寄存器的地址。其返回地址 = func_bRBP寄存器地址+8。
(3)对函数func_a的RBP寄存器的地址取值获取func_a RBP寄存器的值,func_a RBP寄存器的值存放了父函数lkm_init的RBP寄存器的地址。其返回地址 = func_aRBP寄存器地址+8。
这样一步步回溯就可以获取整个调用栈。

在 x86_64 架构中 rbp 指向当前栈帧的起始位置,这个位置保存着旧的 rbp的值。我们可以看到在旧的 rbp 保存的位置上方保存着返回地址(rbp + 8)。这个返回地址是调用者函数中 call 指令的下一条指令的地址,子函数执行完成后会返回,旧的 rbp 首先出栈并赋值给 rbp 寄存器,同时返回地址也要出栈并赋值给 pc。

上面的过程可以递归的用于多层函数调用上。

我们可以将 dump_stack 函数的栈帧看做 Current frame,当前 pc 的值保存的是 dump_stack 中的某条指令的地址,内核先根据这个地址查询 符号表 获取到 dump_stack 函数的名称与当前指令先相对于 dump_stack 函数起始位置的偏移量,然后通过访问 rbp 寄存器指向的旧 rbp 的值来获取到调用 dump_stack 函数的栈帧指针的值,有了这个值就可以不断的回溯上方的栈帧,一个栈帧就是一个调用层次。

同时返回地址的位置就在旧的 rbp 存储位置的上方,根据这样的特点 dump_stack 也就能回溯不同调用层次中返回地址的值。根据返回地址就可以获取到返回地址的上一条调用语句的地址,对该地址进行寻址,获取到指令的编码,就能够获取到调用函数的入口地址。这里可以使用如下公式:

call 指令调用函数的地址 = call 指令码后面的偏移量 + 返回地址

这之后使用入口地址查询 System-map 获取到函数的名称,同时计算出返回地址相对于函数入口的偏移量就准备好了打印的内容,调用打印函数打印信息,每个栈帧用单链表连接,然后继续重复这一过程直到找不到一个合法的栈帧为止。

二、dump_stack函数源码解析

centos 7 配置了CONFIG_FRAME_POINTER选项:

# cat /boot/config-3.10.0-1160.el7.x86_64 | grep CONFIG_FRAME_POINTER
CONFIG_FRAME_POINTER=y
// linux-3.10/lib/dump_stack.c

/**
 * dump_stack - dump the current task information and its stack trace
 *
 * Architectures can override this implementation by implementing its own.
 */
void dump_stack(void)
{
	dump_stack_print_info(KERN_DEFAULT);
	show_stack(NULL, NULL);
}
EXPORT_SYMBOL(dump_stack);
dump_stack()
	-->show_stack()
		-->show_stack_log_lvl()
			-->show_trace_log_lvl()
				-->dump_trace()
					-->print_context_stack()

2.1 show_stack

// linux-3.10/arch/x86/include/asm/stacktrace.h

#define STACKSLOTS_PER_LINE 4
#define get_bp(bp) asm("movq %%rbp, %0" : "=r" (bp) :)

#ifdef CONFIG_FRAME_POINTER
static inline unsigned long
stack_frame(struct task_struct *task, struct pt_regs *regs)
{
	unsigned long bp;

	if (regs)
		return regs->bp;

	if (task == current) {
		/* Grab bp right from our regs */
		get_bp(bp);
		return bp;
	}

	/* bp is the last reg pushed by switch_to */
	return *(unsigned long *)task->thread.sp;
}

get_bp(bp)是一个宏定义,使用汇编语句获取当前函数的基址寄存器(rbp)的值,并将其保存在bp变量中。

stack_frame是一个内联函数,用于获取给定任务的栈帧指针。

(1)如果传入的regs参数非空,说明已经提供了寄存器上下文(pt_regs结构),则直接返回其中的基址寄存器(bp)的值。

(2)如果给定的任务结构体指针与当前任务相同(current表示当前任务),则直接使用get_bp宏获取当前函数的基址寄存器的值(rbp),并将其作为栈帧指针返回。

(3)如果以上条件都不满足,则假设bp是由switch_to函数推入的最后一个寄存器,从给定任务的线程结构体中获取栈指针(sp)所指向的地址,并将其解释为unsigned long类型的指针,以获取栈帧指针。

void show_stack(struct task_struct *task, unsigned long *sp)
{
	unsigned long bp = 0;
	unsigned long stack;

	/*
	 * Stack frames below this one aren't interesting.  Don't show them
	 * if we're printing for %current.
	 */
	if (!sp && (!task || task == current)) {
		sp = &stack;
		bp = stack_frame(current, NULL);
	}

	show_stack_log_lvl(task, NULL, sp, bp, "");
}

该函数用于打印给定任务的堆栈跟踪信息。

2.2 show_stack_log_lvl

// linux-3.10/arch/x86/kernel/dumpstack_64.c

void
show_stack_log_lvl(struct task_struct *task, struct pt_regs *regs,
		   unsigned long *sp, unsigned long bp, char *log_lvl)
{
	unsigned long *irq_stack_end;
	unsigned long *irq_stack;
	unsigned long *stack;
	int cpu;
	int i;

	preempt_disable();
	cpu = smp_processor_id();

	irq_stack_end	= (unsigned long *)(per_cpu(irq_stack_ptr, cpu));
	irq_stack	= (unsigned long *)(per_cpu(irq_stack_ptr, cpu) - IRQ_STACK_SIZE);

	/*
	 * Debugging aid: "show_stack(NULL, NULL);" prints the
	 * back trace for this cpu:
	 */
	if (sp == NULL) {
		if (task)
			sp = (unsigned long *)task->thread.sp;
		else
			sp = (unsigned long *)&sp;
	}

	stack = sp;
	for (i = 0; i < kstack_depth_to_print; i++) {
		if (stack >= irq_stack && stack <= irq_stack_end) {
			if (stack == irq_stack_end) {
				stack = (unsigned long *) (irq_stack_end[-1]);
				pr_cont(" <EOI> ");
			}
		} else {
		if (((long) stack & (THREAD_SIZE-1)) == 0)
			break;
		}
		if (i && ((i % STACKSLOTS_PER_LINE) == 0))
			pr_cont("\n");
		pr_cont(" %016lx", *stack++);
		touch_nmi_watchdog();
	}
	preempt_enable();

	pr_cont("\n");
	show_trace_log_lvl(task, regs, sp, bp, log_lvl);
}

show_stack_log_lvl函数用于打印给定任务的堆栈跟踪信息,并在日志级别上进行控制。

函数首先定义了一些局部变量,包括irq_stack_end、irq_stack、stack、cpu和i。

然后,禁用抢占(preempt_disable)并获取当前处理器的 ID(smp_processor_id)。

irq_stack_end表示中断堆栈的结束地址,irq_stack表示中断堆栈的起始地址(通过per_cpu宏和irq_stack_ptr变量计算得到)。

接下来,通过一系列条件判断,确定要打印的堆栈跟踪信息。

如果给定的sp参数为空,表示需要打印当前任务的堆栈跟踪信息。根据是否提供了任务结构体指针(task),确定要使用的栈指针(sp)。如果提供了任务结构体指针,则使用任务的线程结构体中的栈指针;否则,使用当前函数的栈指针。

接下来,通过循环遍历堆栈,打印堆栈上的地址。在遍历过程中,通过一系列条件判断确定是否处于中断堆栈范围内,并在特定情况下打印(End of Interrupt)标记。如果堆栈地址与线程栈的大小(THREAD_SIZE)对齐,则表示已经遍历到了栈的底部,循环结束。

在每次打印堆栈地址后,调用touch_nmi_watchdog函数,用于触发非屏蔽中断(NMI)看门狗,以确保系统不会因为长时间占用CPU而被认为是死锁。

最后,启用抢占(preempt_enable),打印换行符,然后调用show_trace_log_lvl函数,将任务结构体指针、寄存器上下文、栈指针、栈帧指针和日志级别作为参数传递,继续打印堆栈跟踪信息。

2.3 show_trace_log_lvl

// linux-3.10/arch/x86/kernel/dumpstack.c

void
show_trace_log_lvl(struct task_struct *task, struct pt_regs *regs,
		unsigned long *stack, unsigned long bp, char *log_lvl)
{
	printk("%sCall Trace:\n", log_lvl);
	dump_trace(task, regs, stack, bp, &print_trace_ops, log_lvl);
}
Call Trace:
[1119269.645012]  [<ffffffff8e781340>] dump_stack+0x19/0x1b
[1119269.645021]  [<ffffffffc0a5000e>] func_c+0xe/0x10 [helloworld]
[1119269.645028]  [<ffffffffc0a5001e>] func_b+0xe/0x10 [helloworld]
[1119269.645034]  [<ffffffffc0a5002e>] func_a+0xe/0x10 [helloworld]
[1119269.645041]  [<ffffffffc0153009>] lkm_init+0x9/0x1000 [helloworld]
[1119269.645049]  [<ffffffff8e00210a>] do_one_initcall+0xba/0x240
[1119269.645059]  [<ffffffff8e11e45a>] load_module+0x271a/0x2bb0
[1119269.645066]  [<ffffffff8e3b4290>] ? ddebug_proc_write+0x100/0x100
[1119269.645075]  [<ffffffff8e119fe3>] ? copy_module_from_fd.isra.44+0x53/0x150
[1119269.645083]  [<ffffffff8e11ead6>] SyS_finit_module+0xa6/0xd0
[1119269.645093]  [<ffffffff8e793f92>] system_call_fastpath+0x25/0x2a

2.4 dump_trace

(1)

// linux-3.10/arch/x86/kernel/dumpstack_64.c

/*
 * x86-64 can have up to three kernel stacks:
 * process stack
 * interrupt stack
 * severe exception (double fault, nmi, stack fault, debug, mce) hardware stack
 */

void dump_trace(struct task_struct *task, struct pt_regs *regs,
		unsigned long *stack, unsigned long bp,
		const struct stacktrace_ops *ops, void *data)
{
	const unsigned cpu = get_cpu();
	unsigned long *irq_stack_end =
		(unsigned long *)per_cpu(irq_stack_ptr, cpu);
	unsigned used = 0;
	struct thread_info *tinfo;
	int graph = 0;
	unsigned long dummy;

	if (!task)
		task = current;

	if (!stack) {
		if (regs)
			stack = (unsigned long *)regs->sp;
		else if (task != current)
			stack = (unsigned long *)task->thread.sp;
		else
			stack = &dummy;
	}

	if (!bp)
		bp = stack_frame(task, regs);
	/*
	 * Print function call entries in all stacks, starting at the
	 * current stack address. If the stacks consist of nested
	 * exceptions
	 */
	tinfo = task_thread_info(task);
	for (;;) {
		char *id;
		unsigned long *estack_end;
		estack_end = in_exception_stack(cpu, (unsigned long)stack,
						&used, &id);

		if (estack_end) {
			if (ops->stack(data, id) < 0)
				break;

			bp = ops->walk_stack(tinfo, stack, bp, ops,
					     data, estack_end, &graph);
			ops->stack(data, "<EOE>");
			/*
			 * We link to the next stack via the
			 * second-to-last pointer (index -2 to end) in the
			 * exception stack:
			 */
			stack = (unsigned long *) estack_end[-2];
			continue;
		}
		if (irq_stack_end) {
			unsigned long *irq_stack;
			irq_stack = irq_stack_end -
				(IRQ_STACK_SIZE - 64) / sizeof(*irq_stack);

			if (in_irq_stack(stack, irq_stack, irq_stack_end)) {
				if (ops->stack(data, "IRQ") < 0)
					break;
				bp = ops->walk_stack(tinfo, stack, bp,
					ops, data, irq_stack_end, &graph);
				/*
				 * We link to the next stack (which would be
				 * the process stack normally) the last
				 * pointer (index -1 to end) in the IRQ stack:
				 */
				stack = (unsigned long *) (irq_stack_end[-1]);
				irq_stack_end = NULL;
				ops->stack(data, "EOI");
				continue;
			}
		}
		break;
	}

	/*
	 * This handles the process stack:
	 */
	bp = ops->walk_stack(tinfo, stack, bp, ops, data, NULL, &graph);
	put_cpu();
}
EXPORT_SYMBOL(dump_trace);

dump_trace函数用于在给定任务的堆栈上进行跟踪,并通过提供的回调函数执行相应的操作。

函数首先定义了一些局部变量,包括cpu、irq_stack_end、used、tinfo和graph,以及一个dummy变量。

然后,根据情况,确定要跟踪的任务和堆栈的起始地址。如果没有给定任务,则默认使用当前任务。如果没有给定堆栈地址,则根据情况选择使用寄存器上下文的栈指针、任务的线程结构体中的栈指针,或者一个临时变量作为栈指针。

接下来,如果没有给定基指针(bp),则通过调用stack_frame函数计算基指针。

在一个无限循环中,函数根据堆栈的类型进行处理。首先,通过调用in_exception_stack函数检查堆栈是否属于异常堆栈(如双重故障、NMI、堆栈故障、调试、MCE等),并获取异常堆栈的结束地址(estack_end)以及用于标识堆栈的字符串(id)。

如果堆栈属于异常堆栈(接上文)

如果堆栈属于异常堆栈,将调用回调函数ops->stack(data, id)打印堆栈标识符,并通过调用ops->walk_stack函数执行堆栈的遍历操作。然后,再次调用ops->stack(data, “”)打印异常堆栈的结束标识符。之后,通过异常堆栈的倒数第二个指针(索引为-2)获取下一个堆栈的起始地址,并继续下一轮循环。

如果堆栈不属于异常堆栈,将检查是否存在中断堆栈(IRQ stack)。如果存在中断堆栈,将通过调用in_irq_stack函数判断当前堆栈是否属于中断堆栈,并获取中断堆栈的起始地址。如果当前堆栈属于中断堆栈,则与异常堆栈类似,调用回调函数打印中断标识符,并通过ops->walk_stack函数执行中断堆栈的遍历操作。然后,通过中断堆栈的最后一个指针(索引为-1)获取下一个堆栈的起始地址,并继续下一轮循环。

如果既不是异常堆栈也不是中断堆栈,表示已经遍历完所有堆栈,退出循环。

最后,通过调用ops->walk_stack函数处理进程堆栈,并完成整个跟踪过程。最后,调用put_cpu()释放当前CPU的引用计数。

该函数使用了一些其他函数和数据结构,例如task_thread_info函数用于获取线程信息,stack_frame函数用于计算基指针,in_exception_stack和in_irq_stack函数用于判断堆栈类型。回调函数ops->stack用于打印堆栈标识符,回调函数ops->walk_stack用于执行堆栈的遍历操作。

(2)

// linux-3.10/arch/x86/include/asm/stacktrace.h

/* Generic stack tracer with callbacks */

struct stacktrace_ops {
	void (*address)(void *data, unsigned long address, int reliable);
	/* On negative return stop dumping */
	int (*stack)(void *data, char *name);
	walk_stack_t	walk_stack;
};
/*
 * x86-64 can have up to three kernel stacks:
 * process stack
 * interrupt stack
 * severe exception (double fault, nmi, stack fault, debug, mce) hardware stack
 */

static inline int valid_stack_ptr(struct thread_info *tinfo,
			void *p, unsigned int size, void *end)
{
	void *t = tinfo;
	if (end) {
		if (p < end && p >= (end-THREAD_SIZE))
			return 1;
		else
			return 0;
	}
	return p > t && p < t + THREAD_SIZE - size;
}

unsigned long
print_context_stack(struct thread_info *tinfo,
		unsigned long *stack, unsigned long bp,
		const struct stacktrace_ops *ops, void *data,
		unsigned long *end, int *graph)
{
	struct stack_frame *frame = (struct stack_frame *)bp;

	while (valid_stack_ptr(tinfo, stack, sizeof(*stack), end)) {
		unsigned long addr;

		addr = *stack;
		if (__kernel_text_address(addr)) {
			if ((unsigned long) stack == bp + sizeof(long)) {
				ops->address(data, addr, 1);
				frame = frame->next_frame;
				bp = (unsigned long) frame;
			} else {
				ops->address(data, addr, 0);
			}
			print_ftrace_graph_addr(addr, data, ops, tinfo, graph);
		}
		stack++;
	}
	return bp;
}
EXPORT_SYMBOL_GPL(print_context_stack);

static int print_trace_stack(void *data, char *name)
{
	printk("%s <%s> ", (char *)data, name);
	return 0;
}

void printk_address(unsigned long address, int reliable)
{
	pr_cont(" [<%p>] %s%pB\n",
		(void *)address, reliable ? "" : "? ", (void *)address);
}

/*
 * Print one address/symbol entries per line.
 */
static void print_trace_address(void *data, unsigned long addr, int reliable)
{
	touch_nmi_watchdog();
	printk(data);
	printk_address(addr, reliable);
}

static const struct stacktrace_ops print_trace_ops = {
	.stack			= print_trace_stack,
	.address		= print_trace_address,
	.walk_stack		= print_context_stack,
};

2.5 print_context_stack

/* The form of the top of the frame on the stack */
struct stack_frame {
	struct stack_frame *next_frame;
	unsigned long return_address;
};
/*
 * x86-64 can have up to three kernel stacks:
 * process stack
 * interrupt stack
 * severe exception (double fault, nmi, stack fault, debug, mce) hardware stack
 */

static inline int valid_stack_ptr(struct thread_info *tinfo,
			void *p, unsigned int size, void *end)
{
	void *t = tinfo;
	if (end) {
		if (p < end && p >= (end-THREAD_SIZE))
			return 1;
		else
			return 0;
	}
	return p > t && p < t + THREAD_SIZE - size;
}

unsigned long
print_context_stack(struct thread_info *tinfo,
		unsigned long *stack, unsigned long bp,
		const struct stacktrace_ops *ops, void *data,
		unsigned long *end, int *graph)
{
	struct stack_frame *frame = (struct stack_frame *)bp;

	while (valid_stack_ptr(tinfo, stack, sizeof(*stack), end)) {
		unsigned long addr;

		addr = *stack;
		if (__kernel_text_address(addr)) {
			if ((unsigned long) stack == bp + sizeof(long)) {
				ops->address(data, addr, 1);
				frame = frame->next_frame;
				bp = (unsigned long) frame;
			} else {
				ops->address(data, addr, 0);
			}
			print_ftrace_graph_addr(addr, data, ops, tinfo, graph);
		}
		stack++;
	}
	return bp;
}
EXPORT_SYMBOL_GPL(print_context_stack);

print_context_stack函数用于在给定线程的堆栈上打印函数调用的地址,并通过提供的回调函数执行相应的操作。

函数首先定义了局部变量frame,它是一个指向struct stack_frame类型的指针,用于表示帧结构。

然后,使用一个循环遍历堆栈中的每个地址。在每次循环迭代中,函数检查堆栈指针是否有效,并获取当前堆栈指针处的地址。

如果地址属于内核文本空间(通过__kernel_text_address函数判断),则进行以下操作:

  1. 如果当前堆栈指针等于基指针加上一个long大小,表示该地址是当前函数调用的返回地址。在这种情况下,将调用回调函数ops->address(data, addr, 1)打印地址,并更新帧结构和基指针,使其指向上一帧的基指针。
  2. 如果当前堆栈指针不等于基指针加上一个long大小,表示该地址是普通的函数调用地址。在这种情况下,将调用回调函数ops->address(data, addr, 0)打印地址。
  3. 最后,调用print_ftrace_graph_addr函数打印与地址相关的ftrace图形信息。

在每次循环迭代后,将堆栈指针指向下一个地址。

最后,函数返回更新后的基指针。

参考资料

Linux 3.10.0

https://blogs.oracle.com/linux/post/unwinding-stack-frame-pointers-and-orc
https://blog.csdn.net/Longyu_wlz/article/details/103327538

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

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

相关文章

【Python基础】装饰器(3848字)

文章目录 [toc]闭包什么是装饰器装饰器示例不使用装饰器语法使用装饰器语法 装饰器传参带参数的装饰器类装饰器魔术方法\__call__()类装饰器示例带参数类装饰器property装饰器分页操作商品价格操作 个人主页&#xff1a;丷从心 系列专栏&#xff1a;Python基础 学习指南&…

Redis不同数据类型value存储

一、Strings redis中String的底层没有用c的char来实现&#xff0c;而是使用SDS数据结构( char buf[])。 缺点:浪费空间 优势: 1.c字符串不记录自身的长度&#xff0c;所以获取一个字符串长度的复杂度是O(N),但是SDS记录分配的长度alloc,已使用长度len&#xff0c;获取长度的…

资深测试必备技能!TestNG自动化测试框架实战详解

1、TestNG导言 在软件测试工作中&#xff0c;自动测试框架是不可或缺的&#xff0c;之前有Junit和Nunit框架&#xff0c;后有TestNG。TestNG不但吸取了Junit和Nunit框架的思想&#xff0c;而且创造了更强大的功能&#xff0c;它不但是单元测试框架&#xff0c;同时也是集成自动…

Qt Tab键切换焦点顺序:setTabOrder()

使用这个方法setTabOrder()&#xff0c;设置使得焦点的顺序从前到后依次是&#xff1a; ui->lineEdit》 ui->lineEdit_2》ui->lineEdit_3 》ui->lineEdit_4 焦点先在ui->lineEdit上&#xff0c;当按下Tab键时&#xff0c;焦点跑到ui->lineEdit_2上。。。按…

VS远程调试

因为是做工厂应用的客制化项目&#xff0c;在客户现场出现异常&#xff0c;本地又很难复现&#xff0c;而且重启软件可能又自动恢复了&#xff0c;此时可以用VisualStudio自带的远程调试功能进行调试&#xff0c;不需要重启软件&#xff0c;能较好的定位问题。客户电脑上不需要…

VBA信息获取与处理第四节:获取唯一非重复值返回数组的代码

《VBA信息获取与处理》教程(版权10178984)是我推出第六套教程&#xff0c;目前已经是第一版修订了。这套教程定位于最高级&#xff0c;是学完初级&#xff0c;中级后的教程。这部教程给大家讲解的内容有&#xff1a;跨应用程序信息获得、随机信息的利用、电子邮件的发送、VBA互…

k8s部署skywalking(helm)

官方文档 官方文档说明&#xff1a;Backend setup | Apache SkyWalking官方helm源码&#xff1a;apache/skywalking-helm官方下载&#xff08;包括agent、apm&#xff09;:Downloads | Apache SkyWalking 部署 根据官方helm提示&#xff0c;选择你自己部署的方式&#xff0c…

企业网站从传统服务器迁移到弹性云有什么优势呢?

现代企业对于网站和应用程序的可用性和性能要求越来越高&#xff0c;传统基础设施可能无法满足这些需求。弹性云作为一种新兴的云计算服务模式&#xff0c;对于企业网站的运行和管理带来了许多优势。下面是企业网站从传统服务器迁移到弹性云的五大优势&#xff1a; 灵活弹性&a…

【Qt 学习笔记】Qt常用控件 | 多元素控件 | Tree Widget的说明及介绍

博客主页&#xff1a;Duck Bro 博客主页系列专栏&#xff1a;Qt 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ Qt常用控件 | 多元素控件 | Tree Widget的说明及介绍 文章编号&#x…

Unable to locate the .NET SDK

问题描述&#xff1a; vs2019 加载项目时&#xff0c;提示如下&#xff1a; Unable to locate the .NET SDK as specified by global.json, please check that the specified version is installed. 项目中没有globan找al.json 文件 先使用&#xff1a; dotnet --list-sdks 命…

使用 docker-compose 搭建个人博客 Halo

说明 我这里使用的是 Halo 作为博客的工具&#xff0c;毕竟是开源了&#xff0c;也是使用 Java 写的嘛&#xff0c;另外一点就是使用 docker 来安装&#xff08;自动挡&#xff0c;不用自己考虑太多的环境因素&#xff09;&#xff0c;这样子搭建起来更快一点&#xff0c;我们…

SpringCloudAlibaba:5.1Sentinel的基本使用

概述 简介 Sentinel是阿里开源的项目&#xff0c;提供了流量控制、熔断降级、系统负载保护等多个维度来保障服务之间的稳定性。 官网 https://sentinelguard.io/zh-cn/ Sentinel的历史 2012 年&#xff0c;Sentinel 诞生&#xff0c;主要功能为入口流量控制。 2013-2017 年…

[ue5]编译报错:使用未定义的 struct“FPointDamageEvent“

编译报错&#xff0c;错误很多&#xff0c;但很明显核心问题是第一个&#xff1a;使用未定义的 struct“FPointDamageEvent“&#xff1a; 程序没有找到FPointDamageEvent的定义。 解决办法&#xff1a; 处理这类未定义都可以先F12&#xff0c;找到它的库位置&#xff0c;之后…

数据结构——快速排序

基本思想&#xff1a; 快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法&#xff0c;其基本思想为&#xff1a;任取待排序元素序列中的某元素作为基准值&#xff0c;按照该排序码将待排序集合分割成两子序列&#xff0c;左子序列中所有元素均小于基准值&#xff0c;…

BMJ英国医学杂志文献去哪里下载

《柳叶刀》The Lancet、《新英格兰医学期刊》NEJM、《美国医学会杂志》JAMA、《英国医学期刊》BMJ是世界四大医学顶尖期刊&#xff0c;今天有位医学同学求助一篇BMJ英国医学杂志文献&#xff0c;下面就用这篇文献演示一下在家获取BMJ文献的方法及过程。 文献名&#xff1a;Sur…

Multitouch for Mac:手势自定义,提升工作效率

Multitouch for Mac作为一款触控板手势增强软件&#xff0c;其核心功能在于手势的自定义和与Mac系统的深度整合。通过Multitouch&#xff0c;用户可以轻松设置各种手势&#xff0c;如三指轻点、四指左右滑动等&#xff0c;来执行常见的任务&#xff0c;如打开应用、切换窗口、滚…

视频号创作分成计划实战指南:保姆级教程,带你玩转新副业

视频号的月活跃用户数量已在去年突破了8亿大关&#xff0c;并且不断增长。 在这篇文章中&#xff0c;我将与大家分享视频号的运营玩法和作品创作方向。 一、玩法流程 开通条件 要开通创作者分成计划功能&#xff0c;需要满足以下条件&#xff1a; 粉丝数量达到100及以上。 …

Python专题:十一、集合

集合的初始化 . 字符串中的重复元素只能保留一个 数学中的集合操作 - 求两个集合的差集 & 求两个集合的交集 | 合并两个集合元素

word-快速入门

1、熟悉word界面 2、word排版习惯 3、排版文本基本格式 1、word界面 选项卡 功能组 点击功能组右下角小三角可以开启完整功能组&#xff0c;获得启动器 软件右上角有功能显示折叠按钮 2、排版好习惯 &#xff08;1&#xff09;随时保存 &#xff08;2&#xff09;规范文件命…

矩阵相关运算1

矩阵运算是线性代数中的一个核心部分&#xff0c;它包含了许多不同类型的操作&#xff0c;可以应用于各种科学和工程问题中。 矩阵加法和减法 矩阵加法和减法需要两个矩阵具有相同的维度。操作是逐元素进行的&#xff1a; CAB or CA−B其中 A,B 和 C 是矩阵&#xff0c;且 C…