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

前言

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

相关推荐

  1. Linux x86_64 backtrace 回溯

    2024-05-12 12:38:05       28 阅读
  2. <span style='color:red;'>Linux</span> <span style='color:red;'>ftp</span>

    Linux ftp

    2024-05-12 12:38:05      50 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-05-12 12:38:05       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-05-12 12:38:05       100 阅读
  3. 在Django里面运行非项目文件

    2024-05-12 12:38:05       82 阅读
  4. Python语言-面向对象

    2024-05-12 12:38:05       91 阅读

热门阅读

  1. 法人单位和产业活动单位有什么区别和联系

    2024-05-12 12:38:05       37 阅读
  2. 比亚迪算法岗面试,问的贼细!

    2024-05-12 12:38:05       31 阅读
  3. 数据库监控监听

    2024-05-12 12:38:05       37 阅读
  4. C# 实现加减乘除 (备忘)

    2024-05-12 12:38:05       35 阅读
  5. 计算机视觉教学实训解决方案

    2024-05-12 12:38:05       37 阅读
  6. 1080:余数相同问题

    2024-05-12 12:38:05       30 阅读
  7. [C/C++] -- 适配器模式

    2024-05-12 12:38:05       36 阅读
  8. 整体意义的构成与构建

    2024-05-12 12:38:05       42 阅读
  9. 【负载均衡式在线OJ项目day5】OJ服务模块概要

    2024-05-12 12:38:05       35 阅读
  10. 复习用到知识(asp.net)

    2024-05-12 12:38:05       37 阅读
  11. sass详解与使用

    2024-05-12 12:38:05       34 阅读