Linux 5.0在start_kernel之前做了什么事?(以aarch64为例)

news2024/9/23 15:10:10

目录

  • 引言
  • 汇编启动!!!
  • 细节剖析

引言

之前在研究Linux内核源码的时候总是找不到关于这部分源码的相关剖析,要么也是模棱两可的,也有一些比较专业的代码分析,不过比较分散,感觉大家都不太喜欢这部分代码,正好今天周末,这段时间也在学习Arm64汇编,以这部分为研究对象来解析

源码版本:Linux 5.0

架构信息

  • 芯片架构:ARM64
  • 内存架构:UMA
  • CONFIG_ARM64_VA_BITS:39
  • CONFIG_ARM64_PAGE_SHIFT:12
  • CONFIG_PGTABLE_LEVELS:3

之前写的一些相关文章:

Linux内存管理:Bootmem的率先登场

Linux内存管理:Buddy System姗姗来迟

Linux内存管理:Slab闪亮登场

Linux内存管理:内存分配和内存回收原理

Linux CFS调度器:原理和实现

汇编启动!!!

Linux内核代码从哪里执行的?从链接脚本看

# arch/arm64/kernel/vmlinux.lds.S

SECTIONS
{
	. = KIMAGE_VADDR + TEXT_OFFSET;

	.head.text : {
		_text = .;
		HEAD_TEXT
	}
// include/linux/init.h

#define __HEAD		.section	".head.text","ax"
// arch/arm64/kernel/head.S
	__HEAD
_head:
	/*
	 * DO NOT MODIFY. Image header expected by Linux boot-loaders.
	 */
	b	stext				// branch to kernel start, magic

KIMAGE_VADDR vmalloc区域的起始地址,TEXT_OFFSET是内核起始地址距离ram起始地址的偏移(每一版的Linux内核内存架构都有点不同,此处不做纠结)

// arch/arm64/include/asm/memory.h

#define VA_BITS			(39)
#define VA_START		(UL(0xffffffffffffffff) - \
	(UL(1) << VA_BITS) + 1)
#define PAGE_OFFSET		(UL(0xffffffffffffffff) - \
	(UL(1) << (VA_BITS - 1)) + 1)
#define KIMAGE_VADDR		(MODULES_END)
#define BPF_JIT_REGION_START	(VA_START + KASAN_SHADOW_SIZE)
#define BPF_JIT_REGION_SIZE	(SZ_128M)
#define BPF_JIT_REGION_END	(BPF_JIT_REGION_START + BPF_JIT_REGION_SIZE)
#define MODULES_END		(MODULES_VADDR + MODULES_VSIZE)
#define MODULES_VADDR		(BPF_JIT_REGION_END)
#define MODULES_VSIZE		(SZ_128M)
#define VMEMMAP_START		(PAGE_OFFSET - VMEMMAP_SIZE)
#define PCI_IO_END		(VMEMMAP_START - SZ_2M)
#define PCI_IO_START		(PCI_IO_END - PCI_IO_SIZE)
#define FIXADDR_TOP		(PCI_IO_START - SZ_2M)

在这里插入图片描述
这个Linear Mapping区域位置不固定,有时候会在VM_START

上述跳转到stext符号,从这里才正式开始

ENTRY(stext)
	bl	preserve_boot_args
	bl	el2_setup			// Drop to EL1, w0=cpu_boot_mode
	adrp	x23, __PHYS_OFFSET
	and	x23, x23, MIN_KIMG_ALIGN - 1	// KASLR offset, defaults to 0
	bl	set_cpu_boot_mode_flag
	bl	
	/*
	 * The following calls CPU setup code, see arch/arm64/mm/proc.S for
	 * details.
	 * On return, the CPU will be ready for the MMU to be turned on and
	 * the TCR will have been set.
	 */
	bl	__cpu_setup			// initialise processor
	b	__primary_switch
ENDPROC(stext)

首先是preserve_boot_args符号

x20存储的是FDT设备树文件的物理地址,将其传递给x21

/*
 * Preserve the arguments passed by the bootloader in x0 .. x3
 */
preserve_boot_args:
	mov	x21, x0				// x21=FDT

	adr_l	x0, boot_args			// record the contents of
	stp	x21, x1, [x0]			// x0 .. x3 at kernel entry
	stp	x2, x3, [x0, #16]

	dmb	sy				// needed before dc ivac with
						// MMU off

	mov	x1, #0x20			// 4 x 8 bytes
	b	__inval_dcache_area		// tail call
ENDPROC(preserve_boot_args)

x21 x1 x2 x3四个寄存器的值存入boot_args数组

// arch/arm64/kernel/setup.c

/*
 * The recorded values of x0 .. x3 upon kernel entry.
 */
u64 __cacheline_aligned boot_args[4];

__inval_dcache_area用于清理32字节的数据缓存,x0boot_args的地址,x132字节,即数组的四个元素(这部分功能代码,在末尾进行解析)

接下来是el2_setup

ENTRY(el2_setup)
	msr	SPsel, #1			// We want to use SP_EL{1,2}
	mrs	x0, CurrentEL
	cmp	x0, #CurrentEL_EL2
	b.eq	1f
	mov_q	x0, (SCTLR_EL1_RES1 | ENDIAN_SET_EL1)
	msr	sctlr_el1, x0
	mov	w0, #BOOT_CPU_MODE_EL1		// This cpu booted in EL1
	isb
	ret

SPsel用于在SP_EL0SP_ELn中选择SP寄存器,此处选择使用当前特权级SP寄存器

CurrentEL用于获取当前运行级别,并与CurrentEL_EL2进行比较,我们假设此处不使用虚拟机,而是使用内核级级别

sctlr_el1为系统控制寄存器,此处用于设置小端模式(如下,默认是小端)

// arch/arm64/include/asm/sysreg.h

#define SCTLR_EL1_RES1	((_BITUL(11)) | (_BITUL(20)) | (_BITUL(22)) | (_BITUL(28)) | \
			 (_BITUL(29)))

#ifdef CONFIG_CPU_BIG_ENDIAN
#define ENDIAN_SET_EL1		(SCTLR_EL1_E0E | SCTLR_ELx_EE)
#define ENDIAN_CLEAR_EL1	0
#else
#define ENDIAN_SET_EL1		0
#define ENDIAN_CLEAR_EL1	(SCTLR_EL1_E0E | SCTLR_ELx_EE)
#endif

BOOT_CPU_MODE_EL1暂时不知道啥用处,只会通过返回存放在w0寄存器,放在这里

// arch/arm64/include/asm/virt.h

#define BOOT_CPU_MODE_EL1	(0xe11)

kaslr假设不开启,此处略过

直接看set_cpu_boot_mode_flag

/*
 * Sets the __boot_cpu_mode flag depending on the CPU boot mode passed
 * in w0. See arch/arm64/include/asm/virt.h for more info.
 */
set_cpu_boot_mode_flag:
	adr_l	x1, __boot_cpu_mode
	cmp	w0, #BOOT_CPU_MODE_EL2
	b.ne	1f
	add	x1, x1, #4
1:	str	w0, [x1]			// This CPU has booted in EL1
	dmb	sy
	dc	ivac, x1			// Invalidate potentially stale cache line
	ret
ENDPROC(set_cpu_boot_mode_flag)

__boot_cpu_mode是一个整数数组

ENTRY(__boot_cpu_mode)
	.long	BOOT_CPU_MODE_EL2
	.long	BOOT_CPU_MODE_EL1
// arch/arm64/include/asm/virt.h

extern u32 __boot_cpu_mode[2];

前面的w0存储的是BOOT_CPU_MODE_EL1,由此处可知:This CPU has booted in EL1,跳转到1标签

此处让__boot_cpu_mode[0]等于BOOT_CPU_MODE_EL1,并且清理此处缓存

然后是__cpu_setup,先是清理tlb缓存

	.pushsection ".idmap.text", "awx"
ENTRY(__cpu_setup)
	tlbi	vmalle1				// Invalidate local TLB
	dsb	nsh

cpacr_el1用于控制对浮点数simd的访问:捕获访问与浮点和SIMD执行相关的寄存器的指令,以便在从EL0EL1执行时捕获到EL1

mdscr_el1(Monitor Debug System Control Register)debug功能不做概述

	mov	x0, #3 << 20
	msr	cpacr_el1, x0			// Enable FP/ASIMD
	mov	x0, #1 << 12			// Reset mdscr_el1 and disable
	msr	mdscr_el1, x0			// access to the DCC from EL0
	isb					// Unmask debug exceptions now,
	enable_dbg				// since this is per-cpu
	reset_pmuserenr_el0 x0			// Disable PMU access from EL0

mair_el1用于控制存储器属性的编码:分为八段,用于描述不同的内存属性,后续会在页表中使用AttrIndx[2:0]进行索引

ARMv8最多可以定义八种不同的内存属性,而Linux内核只定义了六种

	ldr	x5, =MAIR(0x00, MT_DEVICE_nGnRnE) | \
		     MAIR(0x04, MT_DEVICE_nGnRE) | \
		     MAIR(0x0c, MT_DEVICE_GRE) | \
		     MAIR(0x44, MT_NORMAL_NC) | \
		     MAIR(0xff, MT_NORMAL) | \
		     MAIR(0xbb, MT_NORMAL_WT)
	msr	mair_el1, x5

TCR寄存器主要包括了与地址转换相关的控制信息以及与高速缓存相关的配置信息

	/*
	 * Set/prepare TCR and TTBR. We use 512GB (39-bit) address range for
	 * both user and kernel.
	 */
	ldr	x10, =TCR_TxSZ(VA_BITS) | TCR_CACHE_FLAGS | TCR_SMP_FLAGS | \
			TCR_TG_FLAGS | TCR_KASLR_FLAGS | TCR_ASID16 | \
			TCR_TBI0 | TCR_A1 | TCR_KASAN_FLAGS
	ldr_l		x9, idmap_t0sz
	tcr_set_t0sz	x10, x9

	/*
	 * Set the IPS bits in TCR_EL1.
	 */
	tcr_compute_pa_size x10, #TCR_IPS_SHIFT, x5, x6
	msr	tcr_el1, x10
	ret					// return to head.S
ENDPROC(__cpu_setup)

这部分是初始化内存部分

最重要的是__primary_switch

__primary_switch:
	adrp	x1, init_pg_dir
	bl	__enable_mmu
	ldr	x8, =__primary_switched
	adrp	x0, __PHYS_OFFSET
	br	x8
ENDPROC(__primary_switch)

__enable_mmu这个看名词即知,用于开启mmu

__create_page_tables 用于创建两个页表:init_pg_diridmap_pg_dir

在执行之前先关闭mmu

	msr	sctlr_el1, x20			// disable the MMU
	isb
	bl	__create_page_tables		// recreate kernel mapping

清除init_pg_dir页表缓存

__create_page_tables:
	mov	x28, lr

	/*
	 * Invalidate the init page tables to avoid potential dirty cache lines
	 * being evicted. Other page tables are allocated in rodata as part of
	 * the kernel image, and thus are clean to the PoC per the boot
	 * protocol.
	 */
	adrp	x0, init_pg_dir // adrp获取的是物理地址
	adrp	x1, init_pg_end
	sub	x1, x1, x0
	bl	__inval_dcache_area // 清除缓存

init_pg_dir页表内存重置为xzr

	adrp	x0, init_pg_dir
	adrp	x1, init_pg_end
	sub	x1, x1, x0 // x1为init_pg_dir占用的字节数
1:	stp	xzr, xzr, [x0], #16
	stp	xzr, xzr, [x0], #16
	stp	xzr, xzr, [x0], #16
	stp	xzr, xzr, [x0], #16
	subs	x1, x1, #64 // 一次清理64B
	b.ne	1b

vabits_user用于保存虚拟地址位数

	/*
	 * Create the identity mapping. 恒等映射
	 */
	adrp	x0, idmap_pg_dir // idmap_pg_dir的物理地址
	adrp	x3, __idmap_text_start		// __pa(__idmap_text_start)

	mov	x5, #VA_BITS
1:
	adr_l	x6, vabits_user
	str	x5, [x6]
	dmb	sy
	dc	ivac, x6		// Invalidate potentially stale cache line

idmap_ptrs_per_pgd用于获得PGD(idmap_pg_dir)PGD表项数

PGDIR_SHIFTPGD的偏移位数

	/*
	 * If VA_BITS == 48, we don't have to configure an additional
	 * translation level, but the top-level table has more entries.
	 */
	mov	x4, #1 << (PHYS_MASK_SHIFT - PGDIR_SHIFT)
	str_l	x4, idmap_ptrs_per_pgd, x5

创建并更新idmap_pg_dir页表

	ldr_l	x4, idmap_ptrs_per_pgd
	mov	x5, x3				// __pa(__idmap_text_start)
	adr_l	x6, __idmap_text_end		// __pa(__idmap_text_end)

	// 创建各个页表
	map_memory x0, x1, x3, x6, x7, x3, x4, x10, x11, x12, x13, x14

这部分空间是什么?

# arch/arm64/kernel/vmlinux.lds.S

#define IDMAP_TEXT					\
	. = ALIGN(SZ_4K);				\
	__idmap_text_start = .;				\
	*(.idmap.text)					\
	__idmap_text_end = .;

其中.idmap.text如下

pushsection    .idmap.text, "awx"
	//
.popsection

使用map_memory创建页表并映射,map_memory宏定义如下

/*
 * Map memory for specified virtual address range. Each level of page table needed supports
 * multiple entries. If a level requires n entries the next page table level is assumed to be
 * formed from n pages.
 *
 *	tbl:	location of page table
 *	rtbl:	address to be used for first level page table entry (typically tbl + PAGE_SIZE)
 *	vstart:	start address to map
 *	vend:	end address to map - we map [vstart, vend]
 *	flags:	flags to use to map last level entries
 *	phys:	physical address corresponding to vstart - physical memory is contiguous
 *	pgds:	the number of pgd entries
 *
 * Temporaries:	istart, iend, tmp, count, sv - these need to be different registers
 * Preserves:	vstart, vend, flags
 * Corrupts:	tbl, rtbl, istart, iend, tmp, count, sv
 */
	.macro map_memory, tbl, rtbl, vstart, vend, flags, phys, pgds, istart, iend, tmp, count, sv
	add \rtbl, \tbl, #PAGE_SIZE
	mov \sv, \rtbl
	mov \count, #0
	compute_indices \vstart, \vend, #PGDIR_SHIFT, \pgds, \istart, \iend, \count
	populate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmp
	mov \tbl, \sv
	mov \sv, \rtbl
#if SWAPPER_PGTABLE_LEVELS > 2
	compute_indices \vstart, \vend, #SWAPPER_TABLE_SHIFT, #PTRS_PER_PMD, \istart, \iend, \count
	populate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmp
	mov \tbl, \sv
#endif

	compute_indices \vstart, \vend, #SWAPPER_BLOCK_SHIFT, #PTRS_PER_PTE, \istart, \iend, \count
	bic \count, \phys, #SWAPPER_BLOCK_SIZE - 1
	populate_entries \tbl, \count, \istart, \iend, \flags, #SWAPPER_BLOCK_SIZE, \tmp
	.endm

创建并更新init_pg_dir表项

	/*
	 * Map the kernel image (starting with PHYS_OFFSET).
	 */
	adrp	x0, init_pg_dir
	mov_q	x5, KIMAGE_VADDR + TEXT_OFFSET	// compile time __va(_text)
	add	x5, x5, x23			// add KASLR displacement
	mov	x4, PTRS_PER_PGD
	adrp	x6, _end			// runtime __pa(_end)
	adrp	x3, _text			// runtime __pa(_text)
	sub	x6, x6, x3			// _end - _text
	add	x6, x6, x5			// runtime __va(_end)

	map_memory x0, x1, x5, x6, x7, x3, x4, x10, x11, x12, x13, x14

由上面可知,前面已经建立了恒等映射和内核映射,下面开启MMU,并执行到__primary_switched

	msr	sctlr_el1, x19			// re-enable the MMU
	isb
	ic	iallu				// flush instructions fetched
	dsb	nsh				// via old mapping
	isb

	ldr	x8, =__primary_switched
	adrp	x0, __PHYS_OFFSET
	br	x8
ENDPROC(__primary_switch)
__primary_switched:
	adrp	x4, init_thread_union
	add	sp, x4, #THREAD_SIZE
	adr_l	x5, init_task
	msr	sp_el0, x5			// Save thread_info

	// 设置异常向量表
	adr_l	x8, vectors			// load VBAR_EL1 with virtual
	msr	vbar_el1, x8			// vector table address
	isb

	stp	xzr, x30, [sp, #-16]!
	mov	x29, sp

	str_l	x21, __fdt_pointer, x5		// Save FDT pointer

	ldr_l	x4, kimage_vaddr		// Save the offset between
	sub	x4, x4, x0			// the kernel virtual and
	str_l	x4, kimage_voffset, x5		// physical mappings

	// Clear BSS
	adr_l	x0, __bss_start
	mov	x1, xzr
	adr_l	x2, __bss_stop
	sub	x2, x2, x0
	bl	__pi_memset
	dsb	ishst				// Make zero page visible to PTW

	add	sp, sp, #16
	mov	x29, #0
	mov	x30, #0
	b	start_kernel
ENDPROC(__primary_switched)

init_thread_union存放了init栈的起始地址,如下__start_init_task = init_thread_union = init_stack,并将其add sp, x4, #THREAD_SIZE赋值为sp寄存器,并将init_task进程描述符存储到sp_el0寄存器

// include/asm-generic/vmlinux.lds.h

#define INIT_TASK_DATA(align)						\
	. = ALIGN(align);						\
	__start_init_task = .;						\
	init_thread_union = .;						\
	init_stack = .;							\
	KEEP(*(.data..init_task))					\
	KEEP(*(.data..init_thread_info))				\
	. = __start_init_task + THREAD_SIZE;				\
	__end_init_task = .;
// init/init_task.c

/*
 * Set up the first task table, touch at your own risk!. Base=0,
 * limit=0x1fffff (=2MB)
 */
struct task_struct init_task
#ifdef CONFIG_ARCH_TASK_STRUCT_ON_STACK
	__init_task_data
#endif
= {
	// 
};
EXPORT_SYMBOL(init_task);

在这里插入图片描述
设置异常向量表(vectors在后续会进行剖析)

	adr_l	x8, vectors			// load VBAR_EL1 with virtual
	msr	vbar_el1, x8			// vector table address
	isb

FDT物理地址(刚开始的时候将其地址存入x21)存入__fdt_pointer

	str_l	x21, __fdt_pointer, x5		// Save FDT pointer

保存kimage_vaddr,这个地址是kernel的虚拟地址,x0是内核被加载的物理地址

	ldr_l	x4, kimage_vaddr		// Save the offset between
	sub	x4, x4, x0			// the kernel virtual and
	str_l	x4, kimage_voffset, x5		// physical mappings

最后是清理bss段位执行内核函数做准备

跳转到start_kernel执行

细节剖析

map_memory宏定义

由上面idmap_pg_dir页表创建可知,

寄存器地址
x0idmap_pg_dir
x3__idmap_text_start
x6__idmap_text_end
x7SWAPPER_MM_MMUFLAGS
x4idmap_ptrs_per_pgd
	map_memory x0, x1, x3, x6, x7, x3, x4, x10, x11, x12, x13, x14

可知,PGD页表占用一个页:PAGE_SIZEtbl用于存储下一级页表的基址

	.macro map_memory, tbl, rtbl, vstart, vend, flags, phys, pgds, istart, iend, tmp, count, sv
	add \rtbl, \tbl, #PAGE_SIZE
	mov \sv, \rtbl
	mov \count, #0

compute_indices宏的功能:用于计算虚拟地址计算各级页表的索引值

	compute_indices \vstart, \vend, #PGDIR_SHIFT, \pgds, \istart, \iend, \count

populate_entries宏的功能:填充索引值index对应的页表项

此处用于设置PGD PUD PMD页表项,此处不会设置PTE,使用段映射,一般是2MB

// arch/arm64/include/asm/pgtable-hwdef.h

/* Initial memory map size */
#if ARM64_SWAPPER_USES_SECTION_MAPS
#define SWAPPER_BLOCK_SHIFT	SECTION_SHIFT
#define SWAPPER_BLOCK_SIZE	SECTION_SIZE
#define SWAPPER_TABLE_SHIFT	PUD_SHIFT
#else
#define SWAPPER_BLOCK_SHIFT	PAGE_SHIFT
#define SWAPPER_BLOCK_SIZE	PAGE_SIZE
#define SWAPPER_TABLE_SHIFT	PMD_SHIFT
#endif
	populate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmp

是怎么切换页表的?每一次切换一级页表

	mov \tbl, \sv
	mov \sv, \rtbl

来看看具体的宏定义

	.macro compute_indices, vstart, vend, shift, ptrs, istart, iend, count
	lsr	\iend, \vend, \shift // 计算结束PGD表项
	mov	\istart, \ptrs
	sub	\istart, \istart, #1 // 获得页表最大索引
	and	\iend, \iend, \istart	// iend = (vend >> shift) & (ptrs - 1) 将iend限制在最大范围内
	mov	\istart, \ptrs
	mul	\istart, \istart, \count
	add	\iend, \iend, \istart	// iend += (count - 1) * ptrs
					// our entries span multiple tables

	lsr	\istart, \vstart, \shift
	mov	\count, \ptrs
	sub	\count, \count, #1
	and	\istart, \istart, \count

	sub	\count, \iend, \istart
	.endm

populate_entries下次补上

__inval_dcache_area宏定义

dc指令用于控制数据缓存

  • civacPoC,清理并使指定的虚拟地址对应的高速缓存失效
  • ivacPoC,使指定的虚拟地址中对于的高速缓存失效
ENTRY(__inval_dcache_area)
	/* FALLTHROUGH */

/*
 *	__dma_inv_area(start, size)
 *	- start   - virtual start address of region x0
 *	- size    - size in question x1
 */
__dma_inv_area:
	add	x1, x1, x0 // x1=x0(start)+x1(size) 结束地址end
	dcache_line_size x2, x3 // x2为缓存行大小
	sub	x3, x2, #1
	tst	x1, x3				// end cache line aligned? 缓存行是否对齐
	bic	x1, x1, x3 // 不对齐则清除为0,这会提前地址,而不会跳过规定的起始范围
	b.eq	1f
	dc	civac, x1			// clean & invalidate D / U line
1:	tst	x0, x3				// start cache line aligned?
	bic	x0, x0, x3
	b.eq	2f
	dc	civac, x0			// clean & invalidate D / U line
	b	3f
2:	dc	ivac, x0			// invalidate D / U line
3:	add	x0, x0, x2
	cmp	x0, x1
	b.lo	2b
	dsb	sy
	ret
ENDPIPROC(__inval_dcache_area)

vectors异常向量表

ARM64中的中断向量表占用2048B,分为四组,每组四个表项,每表项占用128B,四组分别是:

  • EL1t:在EL1下,与当前栈指针SP_ELx不同(一般是SP_EL0
  • EL1t:在EL1下,与当前栈指针SP_ELx相同(即SP_EL1
  • 从低异常级EL0进入当前异常级EL1Lower EL, AArch64
  • 从低异常级EL0进入当前异常级EL1Lower EL, AArch32
/*
 * Exception vectors.
 */
	.pushsection ".entry.text", "ax"

	.align	11
ENTRY(vectors)
	kernel_ventry	1, sync_invalid			// Synchronous EL1t
	kernel_ventry	1, irq_invalid			// IRQ EL1t
	kernel_ventry	1, fiq_invalid			// FIQ EL1t
	kernel_ventry	1, error_invalid		// Error EL1t

	kernel_ventry	1, sync				// Synchronous EL1h
	kernel_ventry	1, irq				// IRQ EL1h
	kernel_ventry	1, fiq_invalid			// FIQ EL1h
	kernel_ventry	1, error			// Error EL1h

	kernel_ventry	0, sync				// Synchronous 64-bit EL0
	kernel_ventry	0, irq				// IRQ 64-bit EL0
	kernel_ventry	0, fiq_invalid			// FIQ 64-bit EL0
	kernel_ventry	0, error			// Error 64-bit EL0

	kernel_ventry	0, sync_invalid, 32		// Synchronous 32-bit EL0
	kernel_ventry	0, irq_invalid, 32		// IRQ 32-bit EL0
	kernel_ventry	0, fiq_invalid, 32		// FIQ 32-bit EL0
	kernel_ventry	0, error_invalid, 32		// Error 32-bit EL0
END(vectors)

每四个异常分别对应于

#define BAD_SYNC	0
#define BAD_IRQ		1
#define BAD_FIQ		2
#define BAD_ERROR	3

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

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

相关文章

[Excel VBA]如何使用VBA按行拆分Excel工作表

如何使用VBA按行拆分Excel工作表 在Excel中&#xff0c;按行拆分工作表并生成多个新工作表是一项实用的技能&#xff0c;尤其在处理大量数据时。以下是一个VBA代码示例&#xff0c;能帮助你轻松实现这一功能。 1. 代码说明 本代码会根据源工作表中每个姓名创建一个新工作表&a…

ArcGIS核密度分析(栅格处理范围与掩膜分析)

多时候我们在进行栅格分析的时候&#xff0c;处理的结果不能完全覆盖我们需要的范围。 比如&#xff0c;我们对点数据进行密度分析、栅格插值等。比如下图 为什么会如此呢&#xff1f; 那是因为在做这个密度分析或者栅格插值的时候&#xff0c;默认是以点的四至范围来生成的&am…

抖音生活服务入局攻略曝光!普通人也能抓住风口!

当前&#xff0c;抖音生活服务的热度持续飙升&#xff0c;让不少人都有了入局的打算&#xff0c;与之相关的各类话题如抖音生活服务的入局途径有哪些等也因此成为了人们热议的对象。而从这些话题的讨论情况来看&#xff0c;绝大多数讨论者只知道抖音生活服务火爆&#xff0c;却…

​智慧铜矿厂综合管控平台,智慧矿山数字孪生

随着矿山行业的不断发展&#xff0c;传统的管理方式已经无法满足现代铜矿高效、安全、环保和精细化管理的需求&#xff0c;因此&#xff0c;构建一个综合管控平台变得尤为必要。HT 铜矿综合管控平台应运而生&#xff0c;通过信息化和智能化手段&#xff0c;整合采矿、选矿、冶炼…

Redis简单介绍与安装应用

在当今的互联网时代&#xff0c;数据的快速存取和处理变得至关重要。Redis&#xff0c;作为一种高性能的键值存储系统&#xff0c;已经成为许多开发者和企业的首选。本文将简要介绍Redis的基本概念、工作原理以及其在实际应用中的一些典型用例。 一、简介 1&#xff09;概念 …

Vue3:mitt实现组件通信

目录 一.性质 1.轻量级 2.单例 3.异步 4.事件绑定与解绑 二.作用 1.组件间通信 2.解耦 3.状态管理 4.事件的集中处理 三.使用 1.安装mitt 2.引入mitt&#xff1b;调用mitt&#xff1b;暴露mitt 3.组件1 4.组件2 四.代码 1.组件1 2.组件2 五.效果 一.性质 1…

多模态论文串讲-学习笔记(上)

入门参考&#xff1a;跟着chatgpt一起学|多模态入门-CSDN博客 学习参考&#xff1a;多模态论文串讲上【论文精读46】_哔哩哔哩_bilibili&#xff0c;强烈推荐这个博主啊&#xff0c;感觉比沐神讲的还要清楚&#xff0c;非常喜欢。 本文介绍只使用transformer encoder的方法&a…

医院为什么要安装医疗设备防漏费系统?

一、医院防漏费管理的重要性 随着人们健康意识的加强&#xff0c;医生对诊断的依据都造就了检查和化验在新形式下的重要性。人们对体检重要性的认识等各方面因素。导致了现在医院检查和化验位置一度提升。成为了医院工作的重中之中。而在中国国情的大环境下&#xff0c;熟人检…

20240923 每日AI必读资讯

GPT-4o能玩《黑神话》&#xff01;精英怪胜率超人类&#xff0c;无强化学习纯大模型方案 - 阿里巴巴的研究人员们提出了一个新型VARP&#xff08;视觉动作角色扮演&#xff09;智能体框架。 - 能直接将游戏截图作为输入&#xff0c;通过视觉语言模型推理&#xff0c;最终生成…

超强AI绘画工具StableDiffusion,SD整合包V4.9 来了 版本win加mac安装包以及搭载PS安装说明

众所周知&#xff0c;StableDiffusion 是非常强大的AI绘图工具&#xff0c;今天为大家带来的是 Stable Diffusion SD整合包v4.9 版本安装说明 。 这里带来的安装版本是9月最新整合包sd-webui-aki-v4.9 版本 。WIN加MAC stable diffusion整合包可以扫描下方&#xff0c;免费获取…

Spring实战——入门讲解

​ 博客主页: 南来_北往 系列专栏&#xff1a;Spring Boot实战 Spring介绍 Spring实战的入门讲解主要涵盖了Spring框架的基本概念、核心功能以及应用场景。以下是关于Spring实战入门的具体介绍&#xff1a; Spring框架概述&#xff1a;Spring是一个轻量级的Java开发框架…

Leetcode 65. 有效数字

1.题目基本信息 1.1.题目描述 给定一个字符串 s &#xff0c;返回 s 是否是一个 有效数字。 例如&#xff0c;下面的都是有效数字&#xff1a;”2″, “0089”, “-0.1”, “3.14”, “4.”, “-.9”, “2e10”, “-90E3”, “3e7”, “6e-1”, “53.5e93”, “-123.456e789…

WebLogic命令执行漏洞CVE-2019-2725

1.环境搭建 cd vulhub-master/weblogic/weak_password docker-compose up -d 2.漏洞验证 http://47.121.211.205:7001/_async/AsyncResponseService 说明存在漏洞 3.在当前页面抓包 修改请求包 写入shell wget http://47.121.211.205/1.txt -O servers/AdminServer/tmp/_W…

大数据新视界 --大数据大厂之 Vue.js 与大数据可视化:打造惊艳的数据界面

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

Unity 设计模式 之 创造型模式-【工厂方法模式】【抽象工厂模式】

Unity 设计模式 之 创造型模式-【工厂方法模式】【抽象工厂模式】 目录 Unity 设计模式 之 创造型模式-【工厂方法模式】【抽象工厂模式】 一、简单介绍 二、工厂方法模式 (Factory Method Pattern) 1、什么时候使用工厂方法模式 2、使用工厂模式的好处 3、使用工厂方法模…

SPI驱动学习六(SPI_Master驱动程序)

目录 前言一、SPI_Master驱动程序框架1. SPI传输概述1.1 数据组织方式1.2 SPI控制器数据结构 2. SPI传输函数的两种方法2.1 老方法2.2 新方法 二、如何编写SPI_Master驱动程序1. 编写设备树2. 编写驱动程序 三、SPI_Master驱动程序简单示例demo1. 使用老方法编写的SPI Master驱…

WEB领域是不是黄了还是没黄

进入2024年后&#xff0c;WEB领域大批老表失业&#xff0c;一片哀嚎&#xff0c;个个饿的鬼叫狼嚎&#xff0c;为啥呢&#xff0c;下面是我个人的见解和看法。 中国程序员在应用层的集中 市场需求&#xff1a;中国的互联网行业在过去几年中经历了爆炸性增长&#xff0c;尤其是…

平板电容笔哪个牌子好?精选电容笔品牌排行榜前五名推荐!

在当今时代&#xff0c;平板电容笔已经成为平板电脑的重要配件&#xff0c;为人们的学习、工作和创作带来了极大的便利。然而&#xff0c;市场上平板电容笔的品牌众多&#xff0c;质量和性能也参差不齐&#xff0c;这让消费者在选择时常常感到困惑。平板电容笔究竟哪个牌子更好…

Revit 2018 提示 您使用的 Revit 授權無效。

昨天晚上想学下BIM&#xff0c;安装了这个软件&#xff0c;忘了给他断网了&#xff0c;今天早上起来一直提示这个信息&#xff0c;通过查看进程的位置找到了一个acwebbrowser 路径如下&#xff1a;C:\Program Files\Common Files\Autodesk Shared\CLM\V5\MSVC14\cliccore 防火…

如何使用 Rust 框架进行 RESTful API 的开发?

一、RESTful API 的开发 使用 Rust 框架进行 RESTful API 开发&#xff0c;你可以选择多种流行的 Rust Web 框架&#xff0c;如 Actix-web、Rocket、Warp 和 Tide 等。以下是使用这些框架进行 RESTful API 开发的基本步骤和概念&#xff1a; 选择框架&#xff1a;根据项…