当前位置:网站首页>KASLR-内核地址空间布局随机化
KASLR-内核地址空间布局随机化
2022-08-03 05:23:00 【SEVENTHD7】
kaslr全称为kernel address space layout randomization,是linux内核的一个非常重要的安全机制,该机制可以让kernel image映射的执行地址相对于链接地址有个偏移,使内核符号地址变得随机,提升内核的安全性和防攻击能力。
内核源码实现
KASLR的实现原理比较简单,在内核启动阶段,获取一个kernel image的偏移值,偏移值可以通过dtb或者bios传递,或者随机源。
Linux内核支持arm、x86_64、PowerPC等多种不同的架构,不同的架构下,kaslr的实现方式各不相同,但核心思想均在于增加随机偏移。在内核启动阶段,通过获取一个随机值,并对内核加载地址进行相应的随机偏移。该偏移值既可以通过dtb传递,也可以基于随机源生成,在完成内核数据随机映射之后,还需要对符号地址进行重定位,校正内核代码的符号寻址,以此确保内核代码的正常执行。以arm64 5.10内核为例,kaslr在实现时主要通过改变原先固定的内存布局来提升内核安全性,因此在代码实现过程中,kaslr与内存功能存在比较强的耦合关系,Linux内存的虚拟地址空间布局与内核编译配置有关,不同的配置会产生不同的地址空间模型,以4KB pages + 4 levels (48-bit)为例,其虚拟地址空间模型如下:(详细的内存布局信息可参考内核文档:Documentation/arm64/memory.rst)
如图所示,Arm64的虚拟地址空间整体可划分为两个部分:内核地址空间和用户地址空间,通常内核地址空间由内核代码分配使用,所有进程共享内核地址空间,而用户地址空间则为用户态进程独占。内核启动阶段,内核镜像的加载、解压和运行均在内核地址空间完成,kaslr也主要在这里对内核内存布局进行随机调整,arm64的内存布局随机主要分为三部分:内核镜像随机、线性映射区随机以及module随机,在随机过程中需要提供随机种子,随机种子生成如下:
rm64的随机种子通过dtb文件获取,因此会为dtb区域建立映射,从dtb文件解析kaslr-seed的属性配置,同时会获取dtb里面的command line配置,
开启kaslr后,head.S中调用 kaslr_early_init -> get_cmdline
#ifdef CONFIG_RANDOMIZE_BASEtst x23, ~(MIN_KIMG_ALIGN - 1) // already running randomized?b.ne 0fmov x0, x21 // pass FDT address in x0bl kaslr_early_init // parse FDT for KASLR options#endif(arch\arm64\kernel\kaslr.c)>>u64 __init kaslr_early_init(u64 dt_phys)>>get_cmdline(void *fdt){
>>static __initconst const u8 default_cmdline[] =CONFIG_CMDLINE;
}
判断是否存在nokaslr配置参数,当不存在随机种子或者配置了nokaslr参数时,kaslr功能会被关闭,并修改相应的kaslr_status状态变量为KASLR_DISABLED_CMDLINE或者KASLR_DISABLED_NO_SEED。 由dtb文件获取到kaslr-seed配置后,会对种子进行处理,基于不同的处理方式,分别用于镜像、线性区和module区域的随机,如下图:
offset为镜像随机偏移值,内核需要保证偏移地址2M对齐,同时通过(VA_BITS_MIN - 2)限制内核随机范围在vmalloc区域中间的一半,避免使用头部和尾部的1/4区域,以避免跟其他的内存分配特性冲突。memstart_offset_seed为线性区的随机种子,内核使用seed的高16位作为线性区域的随机值。 Arm64在内存初始化时,内核会将物理内存通过线性映射的方式完整映射到虚拟地址空间的线性映射区域,如下图:
随机范围是线性区减去物理内存的大小,同时限制偏移粒度ARM64_MEMSTART_ALIGN(256MB),线性区的使用主要涉及virt_to_phys和phys_to_virt两个地址转换接口,其代码实现如下:
以virt_to_phys为例,通过virt_to_phys的接口实现,可以看出memstart_addr为物理地址的起始值,PAGE_OFFSET为虚拟地址中内核地址空间的起始位置,物理地址与虚拟地址之间存在以下转换关系:物理地址 = 虚拟地址 – PAGE_OFFSET + memstart_addr 因此内核可以通过调整memstart_addr的值来进行线性区的映射关系的随机偏移。 Kernel系统启动初期,__primary_switched函数调用kaslr_early_init函数初始化随机偏移值,并保存在x23寄存器当中,实现如下图:
后续进行kernel image虚拟地址映射,会将内核映射的虚拟地址加上kaslr随机偏移(x23寄存器),如下图:
完成kernel image虚拟地址映射以后,需要调用__relocate_kernel进行重定位,函数实现如下:
__relocate_kernel主要对重定位段进行符号重定位,重定位段包含了内核执行过程中需要用到的变量符号,比如_stext、_etext,这些符号对应的地址在链接时确定的,使能kaslr之后,kernel image经过偏移映射,运行时的虚拟地址与编译确定的原始虚拟地址不同,如果不进行重定位操作,则内核无法正常执行。
kaslr特性使能和调试
使能条件
kaslr功能通过CONFIG_RANDOMIZE_BASE进行控制。
cmdline中不能存在nokaslr参数,否则kaslr不被使能。
随机种子
通过dts指定随机种子,如下:
验证测试
通过内核符号地址信息,可以观察内核符号加载状态,以此判断kaslr特性是否生效,命令如下:
echo 0 > /proc/sys/kernel/kptr_restrict
head /proc/kallsyms
kaslr开源社区参考资料
openEuler Kernel SIG
openEuler kernel 源代码仓库:https://gitee.com/openeuler/kernel 欢迎大家多多 star、fork,多多参与社区开发,多多贡献补丁。关于贡献补丁请参考:如何参与 openEuler 内核开发
以上是基础dt实现的,下面看一个基于BIOS的流程
arm64 linux从4.1x阶段默认配置都打开了CONFIG_EFI_STUB, 默认选择UEFI的启动方式。为什么ARM选择UEFI替换DTB启动方式,可以参考linaro的这篇文章(http://www.linaro.org/blog/when-will-uefi-and-acpi-be-ready-on-arm/)
本篇也基于UEFI启动方式分析下内核KASLR的实现,当前主要通过bios实现 EFI_RNG_PROTOCOL协议来传递硬件熵。
从linux内核的启动流程开始分析,head.S(arch/arm64/kernel/head.S)是vmlinux的入口。
_head:
/*
* DO NOT MODIFY. Image header expected by Linux boot-loaders.
*/
#ifdef CONFIG_EFI ------ EFI启动配置
/*
* This add instruction has no meaningful effect except that
* its opcode forms the magic "MZ" signature required by UEFI.
*/
add x13, x18, #0x16 ----------- 把自己伪装成一个UEFI image,kernel需要符合PE格式
b stext ----------stext的功能是获取处理器类型和机器类型信息,并创建临时的页表,然后开启MMU功能, 是内核启动的分支
#else
b stext // branch to kernel start, magic
.long 0 // reserved
#endif
le64sym _kernel_offset_le // --------- kernel在RAM中加载的偏移,如果等于0,表示加载到RAM的0地址的位置上
le64sym _kernel_size_le // --------- kernel的大小
le64sym _kernel_flags_le // --------- kernel的一些属性
.quad 0 // reserved
.quad 0 // reserved
.quad 0 // reserved
.ascii "ARM\x64" // Magic number
#ifdef CONFIG_EFI
.long pe_header - _head // Offset to the PE header.pe_header:
__EFI_PE_HEADER
__EFI_PE_HEADER定义在efi-head.S文件中(arch/arm64/kernel/efi-head.S)
.macro __EFI_PE_HEADER
.long PE_MAGIC
coff_header:
.short IMAGE_FILE_MACHINE_ARM64 // --------- 表示machine type是AArch64
.short section_count // ------------- 该PE文件有多少个section
.long 0 // --------------- 该文件的创建时间
.long 0 // -------------- 符号表信息
.long 0 // -------------- 符号表中的符号的数目
.short section_table - optional_header // ---------- optional header的长度
.short IMAGE_FILE_DEBUG_STRIPPED | \
IMAGE_FILE_EXECUTABLE_IMAGE | \
IMAGE_FILE_LINE_NUMS_STRIPPED // ----------- Characteristics,具体的含义请查看PE规格书optional_header:
.short PE_OPT_MAGIC_PE32PLUS // PE32+ format
.byte 0x02 // MajorLinkerVersion
.byte 0x14 // MinorLinkerVersion
.long __initdata_begin - efi_header_end // ----------- 正文段的大小
.long __pecoff_data_size // --------- data段的大小
.long 0 // ------- bss段的大小
.long __efistub_entry - _head // 加载到memory后入口函数
.long efi_header_end - _head // ----------- 代码段在image file中的偏移
可以看出,加载到memory后的入口函数是__efistub_entry, 它是在哪里定义的呢?
查看Makefile(arch/arm64/kernel/Makefile)可以发现
OBJCOPYFLAGS := --prefix-symbols=__efistub_
$(obj)/%.stub.o: $(obj)/%.o FORCE
$(call if_changed,objcopy)
编译的对象会有一个预加载的符号__efistub_, 主要作用是为了防止命名冲突,所以真正的入口函数是
entry, 定义在efi-entry.S文件中(arch/arm64/kernel/efi-entry.S)
ENTRY(entry) ------------ entry的入口函数
/*
* Create a stack frame to save FP/LR with extra space
* for image_addr variable passed to efi_entry().
*/
stp x29, x30, [sp, #-32]!
mov x29, sp/*
* Call efi_entry to do the real work.
* x0 and x1 are already set up by firmware. Current runtime
* address of image is calculated and passed via *image_addr.
*
* unsigned long efi_entry(void *handle,
* efi_system_table_t *sys_table,
* unsigned long *image_addr) ;
*/
adr_l x8, _text
add x2, sp, 16
str x8, [x2]
bl efi_entry ----------真正的入口函数是efi_entry
cmn x0, #1
b.eq efi_load_fail
上面代码我们主要关注的是bl efi-entry,现在我们找到了内核中的入口函数的实现
efi_entry(void *handle, efi_system_table_t *sys_table, unsigned long *image_addr) ;
efi_entry定义在/drivers/firmware/efi/libstub/arm-stub.c中,
unsigned long efi_entry(void *handle, efi_system_table_t *sys_table,
unsigned long *image_addr)
{
....
* Get the command line from EFI, using the LOADED_IMAGE
* protocol. We are going to copy the command line into the
* device tree, so this can be allocated anywhere.
*/
cmdline_ptr = efi_convert_cmdline(sys_table, image, &cmdline_size); --- (1)
if (!cmdline_ptr) {
pr_efi_err(sys_table, "getting command line via LOADED_IMAGE_PROTOCOL\n");
goto fail;
}
status = handle_kernel_image(sys_table, image_addr, &image_size,
&reserve_addr,
&reserve_size,
dram_base, image); ------ (2)
if (status != EFI_SUCCESS) {
pr_efi_err(sys_table, "Failed to relocate kernel\n");
goto fail_free_cmdline;
}
if (fdt_addr) { ---- (3)
pr_efi(sys_table, "Using DTB from command line\n");
} else {
/* Look for a device tree configuration table entry. */
fdt_addr = (uintptr_t)get_fdt(sys_table, &fdt_size);
if (fdt_addr)
pr_efi(sys_table, "Using DTB from configuration table\n");
}if (!fdt_addr)
pr_efi(sys_table, "Generating empty DTB\n");....
new_fdt_addr = fdt_addr;
status = allocate_new_fdt_and_exit_boot(sys_table, handle,
&new_fdt_addr, efi_get_max_fdt_addr(dram_base), ----- (4)
initrd_addr, initrd_size, cmdline_ptr,
fdt_addr, fdt_size);...
}
(1) efi_entry 通过efi_convert_cmdline从uefi中拿到cmdline, 然后将cmdline从utf16转成utf8返回。
(2) efi_entry中会调用handle_kernel_image, 重定位内核。
handle_kernel_image(/drivers/firmware/efi/libstub/arm64-stub.c):
efi_status_t handle_kernel_image(efi_system_table_t *sys_table_arg,
unsigned long *image_addr,
unsigned long *image_size,
unsigned long *reserve_addr,
unsigned long *reserve_size,
unsigned long dram_base,
efi_loaded_image_t *image)
{
efi_status_t status;
unsigned long kernel_size, kernel_memsize = 0;
void *old_image_addr = (void *)*image_addr;
unsigned long preferred_offset;
u64 phys_seed = 0; // kaslr-seed, 默认为0
//内核使能CONFIG_RANDOMIZE_BASE
if (IS_ENABLED(CONFIG_RANDOMIZE_BASE)) {
//确保command line没有传递nokaslr参数,如果传递nokaslr则关闭KASLR
if (!nokaslr()) {
// 通过EFI_RNG_PROTOCOL获取BIOS传递过来的随机值
status = efi_get_random_bytes(sys_table_arg,
sizeof(phys_seed),
(u8 *)&phys_seed);
if (status == EFI_NOT_FOUND) {
pr_efi(sys_table_arg, "EFI_RNG_PROTOCOL unavailable, no randomness supplied\n");
} else if (status != EFI_SUCCESS) {
pr_efi_err(sys_table_arg, "efi_get_random_bytes() failed\n");
return status;
}
} else {
pr_efi(sys_table_arg, "KASLR disabled on kernel command line\n");
}
}
// 保证kernel位于VMALLOC区域大小的范围
preferred_offset = round_down(dram_base, MIN_KIMG_ALIGN) + TEXT_OFFSET;
if (preferred_offset < dram_base)
preferred_offset += MIN_KIMG_ALIGN;
kernel_size = _edata - _text;
kernel_memsize = kernel_size + (_end - _edata);
// 如果随机值不为0并且CONFIG_RANDOMIZE_BASE配置打开, 所以BIOS在产生随机值时需要做一个判断
if (IS_ENABLED(CONFIG_RANDOMIZE_BASE) && phys_seed != 0) {
//如果未设置CONFIG_DEBUG_ALIGN_RODATA,则在区间[0,MIN_KIMG_ALIGN]中生成一个不违反此内核的事实对齐约束的位移。
u32 mask = (MIN_KIMG_ALIGN - 1) & ~(EFI_KIMG_ALIGN - 1);
u32 offset = !IS_ENABLED(CONFIG_DEBUG_ALIGN_RODATA) ?
(phys_seed >> 32) & mask : TEXT_OFFSET;
//保证传递的偏移地址2M地址对齐
offset |= TEXT_OFFSET % EFI_KIMG_ALIGN;
// 在一个随机的物理地址加载内核
*reserve_size = kernel_memsize + offset;
status = efi_random_alloc(sys_table_arg, *reserve_size,
MIN_KIMG_ALIGN, reserve_addr,
(u32)phys_seed);
*image_addr = *reserve_addr + offset;
} else {
if (*image_addr == preferred_offset)
return EFI_SUCCESS;
*image_addr = *reserve_addr = preferred_offset;
*reserve_size = round_up(kernel_memsize, EFI_ALLOC_ALIGN);
status = efi_call_early(allocate_pages, EFI_ALLOCATE_ADDRESS,
EFI_LOADER_DATA,
*reserve_size / EFI_PAGE_SIZE,
(efi_physical_addr_t *)reserve_addr);
}
if (status != EFI_SUCCESS) {
*reserve_size = kernel_memsize + TEXT_OFFSET;
status = efi_low_alloc(sys_table_arg, *reserve_size,
MIN_KIMG_ALIGN, reserve_addr);
if (status != EFI_SUCCESS) {
pr_efi_err(sys_table_arg, "Failed to relocate kernel\n");
*reserve_size = 0;
return status;
}
*image_addr = *reserve_addr + TEXT_OFFSET;
}
memcpy((void *)*image_addr, old_image_addr, kernel_size);
return EFI_SUCCESS;
}
handle_kernel_image的主要作用是在一个随机的物理地址中加载内核。
(3) 如果是acpi启动,没有fdt的情况下会生成一个fdt
(4) 在allocate_new_fdt_and_exit_boot -> update_fdt中将之前获取的内容(如cmdline ptr, seed)copy到chosen中
现在我们获取了一个保存关键信息的fdt.
现在我们重新回到head.S的流程
如果使能了KASLR的内核配置(CONFIG_RANDOMIZE_BASE)
__primary_switch:
#ifdef CONFIG_RANDOMIZE_BASE
mov x19, x0 // preserve new SCTLR_EL1 value
mrs x20, sctlr_el1 // preserve old SCTLR_EL1 value
#endifbl __enable_mmu
#ifdef CONFIG_RELOCATABLE
bl __relocate_kernel
#ifdef CONFIG_RANDOMIZE_BASE
ldr x8, =__primary_switched // ----- 跳转到primary_switched
adrp x0, __PHYS_OFFSET
blr x8//-------- 在x23 寄存器中有一个KASLR位移,我们需要通过丢弃当前的内核映射并创建一个新的映射。
pre_disable_mmu_workaround
msr sctlr_el1, x20 // ------------------ 关闭MMU
isb
bl __create_page_tables // ------------------ 创建页表映射tlbi vmalle1 // -------- 删除TBL
dsb nshmsr sctlr_el1, x19 // -------- 打开MMU
isb
ic iallu // 获取刷新指令
dsb nsh
isbbl __relocate_kernel // ------------ relocate kernel
#endif
#endif
ldr x8, =__primary_switched
adrp x0, __PHYS_OFFSET
br x8
ENDPROC(__primary_switch)
现在看下primary_switched中的处理
__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
#ifdef CONFIG_KASAN
bl kasan_early_init
#endif
#ifdef CONFIG_RANDOMIZE_BASE
tst x23, ~(MIN_KIMG_ALIGN - 1) // already running randomized?
b.ne 0f
mov x0, x21 // pass FDT address in x0
bl kaslr_early_init // parse FDT for KASLR options
cbz x0, 0f // KASLR disabled? just proceed
orr x23, x23, x0 // record KASLR offset
ldp x29, x30, [sp], #16 // we must enable KASLR, return
ret // to __primary_switch()
0:
#endif
add sp, sp, #16
mov x29, #0
mov x30, #0
b start_kernel
ENDPROC(__primary_switched)
在配置CONFIG_RANDOMIZE_BASE后,会进入kaslr_early_init的流程
u64 __init kaslr_early_init(u64 dt_phys)
{
void *fdt;
u64 seed, offset, mask, module_range;
const u8 *cmdline, *str;
int size;/*
* Set a reasonable default for module_alloc_base in case
* we end up running with module randomization disabled.
*/
module_alloc_base = (u64)_etext - MODULES_VSIZE;/*
* Try to map the FDT early. If this fails, we simply bail,
* and proceed with KASLR disabled. We will make another
* attempt at mapping the FDT in setup_machine()
*/
early_fixmap_init();
fdt = __fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL);
if (!fdt)
return 0;/*
* Retrieve (and wipe) the seed from the FDT
*/
seed = get_kaslr_seed(fdt); ------------- (1)
if (!seed)
return 0;/*
* Check if 'nokaslr' appears on the command line, and
* return 0 if that is the case.
*/
cmdline = get_cmdline(fdt);
str = strstr(cmdline, "nokaslr"); -------------- (2)
if (str == cmdline || (str > cmdline && *(str - 1) == ' '))
return 0;mask = ((1UL << (VA_BITS - 2)) - 1) & ~(SZ_2M - 1); ----------(3)
offset = BIT(VA_BITS - 3) + (seed & mask);/* use the top 16 bits to randomize the linear region */
memstart_offset_seed = seed >> 48; ----- (4)
if (IS_ENABLED(CONFIG_RANDOMIZE_MODULE_REGION_FULL)) {
/*
* Randomize the module region over a 4 GB window covering the
* kernel. This reduces the risk of modules leaking information
* about the address of the kernel itself, but results in
* branches between modules and the core kernel that are
* resolved via PLTs. (Branches between modules will be
* resolved normally.)
*/
module_range = SZ_4G - (u64)(_end - _stext);
module_alloc_base = max((u64)_end + offset - SZ_4G,
(u64)MODULES_VADDR);
} else {
/*
* Randomize the module region by setting module_alloc_base to
* a PAGE_SIZE multiple in the range [_etext - MODULES_VSIZE,
* _stext) . This guarantees that the resulting region still
* covers [_stext, _etext], and that all relative branches can
* be resolved without veneers.
*/
module_range = MODULES_VSIZE - (u64)(_etext - _stext);
module_alloc_base = (u64)_etext + offset - MODULES_VSIZE;
}/* use the lower 21 bits to randomize the base of the module region */
module_alloc_base += (module_range * (seed & ((1 << 21) - 1))) >> 21;
module_alloc_base &= PAGE_MASK;return offset;
}
(1)kaslr_early_init会根据之前生成的fdt种获取kaslr-seed
(2) 解析cmdline, 确保没有传递nokaslr参数
(3) 保证传递的偏移地址2M地址对齐,并且保证kernel位于VMALLOC区域大小的一半地址空间以下 (VA_BITS - 2)。当VA_BITS=48时,mask=0x0000_3fff_ffe0_0000。
(4) 随机化线性映射区地址
回到上面流程,kaslr_early_init获取的偏移地址offset保存在x23寄存器中。然后重新创建kernel image的映射。
创建映射的函数是__create_page_tables。
函数也定义在head.S文件中,主要是为了映射内核在vmalloc域的随机地址空间. 此处还有一个__relocate_kernel的跳转,有什么用呢?例如链接脚本中常见的几个变量_text、_etext、_end。这几个你应该很熟悉,他们是一个地址并且他们的值是链接的时候确定下来,那么现在使能kaslr的情况下,代码中再访问_text的值就很明显不是运行时的虚拟地址,而是链接时候的值。因此,__relocate_kernel函数可以负责重定位这些变量。保证访问这些变量的值依然是正确的值。
功能验证
因为我这边没有一个实现了EFI_RNG_PROTOCAL的BIOS,所以我对内核代码进行了修改,主要验证下KASLR的整个流程是否ok.
上面已经说过,获取kaslr-seed主要通过efi_get_random_bytes(drivers/firmware/efi/libstub/random.c)
efi_status_t efi_get_random_bytes(efi_system_table_t *sys_table_arg,
unsigned long size, u8 *out)
{
efi_guid_t rng_proto = EFI_RNG_PROTOCOL_GUID;
efi_status_t status;
struct efi_rng_protocol *rng;// *out即使返回的随机值,可以在这里手动赋予一个值,每次启动都重新赋一个
*out = 0x12345678;
return EFI_SUCCESS;
status = efi_call_early(locate_protocol, &rng_proto, NULL,
(void **)&rng);
if (status != EFI_SUCCESS)
return status;return rng->get_rng(rng, NULL, size, out);
}
增加红色代码,修改完成后,多更改几次*out的返回值,查看函数的偏移地址是否每次都不一样即可。
使用如下命令即可:
cat /proc/kallsyms | grep do_fork
总结
如果内核想要使用KASLR的功能,需要保证配置CONFIG_RADOMIZE_BASE和CONFIG_RANDOMIZE_TEXT_OFFSET打开,并且启动参数cmdline中不要添加nokaslr.
kaslr的主要流程可以分为以下几步:
1.通过handle_kernel_image在一个随机的物理地址加载内核
2. 通过kaslr_early_init获取内核映射偏移地址,然后映射内核在vmalloc域的一个随机虚拟地址
3. 映射一些变量以及符号表,偏移地址和image一样
如果需要验证KASLR,可以反复启动内核,查看函数的偏移地址是否每次都不一样即可。
边栏推荐
猜你喜欢
自我监督学习和BERT模型
Execute the mysql script file in the docker mysql container and solve the garbled characters
自监督论文阅读笔记Reading and Writing: Discriminative and Generative Modelingfor Self-Supervised Text Recogn
Sqli-labs-master shooting range 1-23 customs clearance detailed tutorial (basic)
深度学习理论课程第四、五章总结
NFT租赁提案EIP-5006步入最后审核!让海外大型游戏的链改成为可能
进程间通信IPC - 信号量
自监督论文阅读笔记 Self-supervised Learning in Remote Sensing: A Review
Router-view
【Arduino】关于“&”和“|” 运算-----多个参数运算结果异常的问题解决
随机推荐
编程软件配备
EIP-5058 能否防止NFT项目方提桶跑路?
php连接数据库脚本
关于如何向FastAPI的依赖函数添加参数
联邦学习摘录
解决Gradle Download缓慢的百种方法
[XSS, file upload, file inclusion]
spark sql 报错 Can‘t zip RDDs with unequal numbers of partitions
滚动条 scrollbar 和scrollbar-thumb 样式
中国生物反应器行业发展现状及前景规划分析报告报告2022~2028年
当奈飞的NFT忘记了web2的业务安全
优雅的拦截TabLayout的点击事件
动态规划笔记
arm64麒麟安装paddlehub(国产化)注意事项
Qlik Sense 聚合函数及范围详解(Sum、Count、All、ToTaL、{1})
寄存器常见指令
自监督论文阅读笔记 Ship Detection in Sentinel 2 Multi-Spectral Images with Self-Supervised Learning
漫谈Map Reduce 参数优化
中国磷化铟技术行业发展趋势与前景规划建议报告2022~2028年
【Yarn】yarn常用命令 查看日志和Kill任务