加载:loader实现

news2024/9/20 14:52:26

1、利用内联汇编显示字符串

通过反复调用BIOS显示字符的方式来显示一个完整的字符串,该功能将用于loader在初始化过程中显示初始化进度、错误信息。

具体代码

// 16位代码,必须加上放在开头,以便有些io指令生成为32位
__asm__(".code16gcc");

/**
 * BIOS下显示字符串
 */
static void show_msg (const char * msg) {
//定义一个静态函数 show_msg,它接收一个字符指针 msg 作为参数。这意味着该函数只能在当前源文件中使用
    char c;

	// 使用bios写显存,持续往下写
	while ((c = *msg++) != '\0') {
		__asm__ __volatile__(   //这段代码使用了内嵌汇编(inline assembly)来直接调用 BIOS 中断服务例程来显示字符。
				"mov $0xe, %%ah\n\t"   //将寄存器 AH 设置为 0x0E,表示 BIOS 的显示字符功能。
				"mov %[ch], %%al\n\t"  //将字符变量 c 的值加载到寄存器 AL 中。
				"int $0x10"::[ch]"r"(c));   //调用 BIOS 的中断 0x10 来显示字符
}

void loader_entry(void) {
    show_msg("....loading.....\r\n");
    for(;;) {}
}

2、检测内存容量

内存检测方法: INT 0x15, EAX = 0xE820

static void  detect_memory(void) {
	uint32_t contID = 0;
	SMAP_entry_t smap_entry;
	int signature, bytes;

    show_msg("try to detect memory:");

	// 初次:EDX=0x534D4150,EAX=0xE820,ECX=24,INT 0x15, EBX=0(初次)
	// 后续:EAX=0xE820,ECX=24,
	// 结束判断:EBX=0
	boot_info.ram_region_count = 0;
	for (int i = 0; i < BOOT_RAM_REGION_MAX; i++) {
		SMAP_entry_t * entry = &smap_entry;

		__asm__ __volatile__("int  $0x15"
			: "=a"(signature), "=c"(bytes), "=b"(contID)
			: "a"(0xE820), "b"(contID), "c"(24), "d"(0x534D4150), "D"(entry));
		if (signature != 0x534D4150) {
            show_msg("failed.\r\n");
			return;
		}

		// todo: 20字节
		if (bytes > 20 && (entry->ACPI & 0x0001) == 0){
			continue;
		}

        // 保存RAM信息,只取32位,空间有限无需考虑更大容量的情况
        if (entry->Type == 1) {
            boot_info.ram_region_cfg[boot_info.ram_region_count].start = entry->BaseL;
            boot_info.ram_region_cfg[boot_info.ram_region_count].size = entry->LengthL;
            boot_info.ram_region_count++;
        }

		if (contID == 0) {
			break;
		}
	}
    show_msg("ok.\r\n");
}
// 内存检测信息结构
typedef struct SMAP_entry {
    uint32_t BaseL; // base address uint64_t
    uint32_t BaseH;
    uint32_t LengthL; // length uint64_t
    uint32_t LengthH;
    uint32_t Type; // entry Type
    uint32_t ACPI; // extended
}__attribute__((packed)) SMAP_entry_t;

3、切换到保护模式

X86CPU的两种工作模式

1)实模式

x86在上电启动后自动进入实模式,即16位工作模式,这种模式是最早期的8086芯片所使用的工作模式。早期的芯片设计得较简单、工作模式也较简单,所以有诸多限制:

最大只能访问1MB的内存:采用段值:偏移的方式访问,内核寄存器最大为16位宽。如段寄存器CS, DS, ES, FS, GS, SS均为16位宽,AX, BX, CX DX, SI, DI, SP等也均为16位宽,有诸多限制

  1. 所有的操作数最大为16位宽,出栈入栈也以16位为单位
  2. 没有任何保护机制,意味着应用程序可以读写内存中的任意位置
  3. 没有特权级支持,意味着应用程序可以随意执行任何指令,例如停机指令、关中断指令
  4. 没有分页机制和虚拟内存的支

2)保护模式

  1. 寄存器位宽扩展至32位,例如AX扩展至32位的EAX,最大可访问4GB内存
  2. 所有操作数最大为32位宽,出入栈也为32位
  3. 提供4种特权级。操作系统可以运行在最高特权级,可执行任意指令;应用程序可运行于最低特权级,避免其执行某些特权指令,例如停机指令、关中断指令
  4. 支持虚拟内存,可以开启分页机制,以隔离不同的应用程序

3)切换至保护模式

实模式(Real Mode)中,地址线的地址范围是 20 位,即可以寻址到 1MB 的内存空间(0x00000 到 0xFFFFF)。然而,在实模式下,CPU 只能寻址 1MB 的内存。

A20 地址线 是一种物理地址线,它在实模式下被禁止,以确保地址不超过 1MB(0xFFFFF)。

在进入保护模式后,CPU 需要开启 A20 地址线以访问 1MB 以上的内存空间。

实模式 中,CPU 使用段寄存器来访问内存,每个段寄存器包含一个段选择子,段选择子指向段描述符表

保护模式 引入了更复杂的内存管理机制,包括段选择子和描述符表。段描述符表包括全局描述符表(GDT)和局部描述符表(LDT)。

在保护模式中,使用 LGDT 指令加载 GDT。此指令设置 GDTR(全局描述符表寄存器),指示 CPU GDT 的基址和大小。

综上具体步骤:

  1. 禁用中断
  2. 打开A20地址线
  3. 加载GDT表
  4. 设置CR0的保护模式使能位
  5. 远跳转、清空流水线

4)主要代码

CPU_instr.h   主要是汇编指令的封装

  1. outb 向指定的 I/O 端口 port 写入一个字节的数据 data。
  2. cli 指令用于清除 CPU 的中断标志,从而禁止中断。
  3. sti 指令用于设置 CPU 的中断标志,从而允许中断
  4. lgdt 指令用于加载全局描述符表(GDT)的基址和限长到 GDTR 寄存器。
#ifndef CPU_INSTR_H
#define CPU_INSTR_H

#include "types.h"

//inb 从指定的 I/O 端口 port 读取一个字节的数据。
static inline uint8_t inb(uint16_t  port) {
	uint8_t rv;
	__asm__ __volatile__("inb %[p], %[v]" : [v]"=a" (rv) : [p]"d"(port));
	return rv;
}

//outb 向指定的 I/O 端口 port 写入一个字节的数据 data。
static inline void outb(uint16_t port, uint8_t data) {
	__asm__ __volatile__("outb %[v], %[p]" : : [p]"d" (port), [v]"a" (data));
}

//cli 指令用于清除 CPU 的中断标志,从而禁止中断。
static inline void cli() {
	__asm__ __volatile__("cli");
}


//sti 指令用于设置 CPU 的中断标志,从而允许中断
static inline void sti() {
	__asm__ __volatile__("sti");
}

//lgdt 指令用于加载全局描述符表(GDT)的基址和限长到 GDTR 寄存器。
static inline void lgdt(uint32_t start, uint32_t size) {
	struct {
		uint16_t limit;
		uint16_t start15_0;    // 视频中这里写成了32位
		uint16_t start31_16;    // 视频中这里写成了32位
	} gdt;

	gdt.start31_16 = start >> 16;
	gdt.start15_0 = start & 0xFFFF;
	gdt.limit = size - 1;
 
	__asm__ __volatile__("lgdt %[g]"::[g]"m"(gdt));        //"lgdt %[g]": 执行 lgdt 指令,将 gdt 的内容加载到 GDTR 寄存器。
}

#endif

loader_16.c

主要实现了进入保护模式的函数enter_protect_mode() 和 GDT表gdt_table[][4]

// GDT表。临时用,后面内容会替换成自己的
uint16_t gdt_table[][4] = {
    {0, 0, 0, 0},
    {0xFFFF, 0x0000, 0x9A00, 0x00CF},
    {0xFFFF, 0x0000, 0x9200, 0x00CF},
};

/**
 * 进入保护模式
 */
static void  enter_protect_mode() {
    cli(); // 关中断

    // 开启A20地址线,使得可访问1M以上空间
    // 使用的是Fast A20 Gate方式,见https://wiki.osdev.org/A20#Fast_A20_Gate
    uint8_t v = inb(0x92);
    outb(0x92, v | 0x2);

    // 加载GDT。由于中断已经关掉,IDT不需要加载
    lgdt((uint32_t)gdt_table, sizeof(gdt_table));
}

void loader_entry(void) {
    show_msg("....loading.....\r\n");
	detect_memory();
    enter_protect_mode();
    for(;;) {}
}

4、使用LBA读取磁盘

由于进入保护模式后,无法使用BIOS中断的方式读取磁盘。另外,由于读取的磁盘数据会放在1MB以上的内存区域,不便于在进入保护模式前使用BIOS的磁盘读取服务来读取

LBA48模式:将硬盘上所有的扇区看成线性排列,没有磁盘、柱面等概念,因此访问起来更加简单,扇区序号从0开始

在loader_32.c代码如下

/**
* 使用LBA48位模式读取磁盘
	定义一个静态函数 read_disk,用于从磁盘读取数据。函数参数包括:
	sector:要读取的起始扇区。
	sector_count:要读取的扇区数量。
	buf:指向数据缓存的指针,将读取的数据存储到这里。
*/
static void read_disk(int sector, int sector_count, uint8_t * buf) {
    outb(0x1F6, (uint8_t) (0xE0));

	outb(0x1F2, (uint8_t) (sector_count >> 8));
    outb(0x1F3, (uint8_t) (sector >> 24));		// LBA参数的24~31位
    outb(0x1F4, (uint8_t) (0));					// LBA参数的32~39位
    outb(0x1F5, (uint8_t) (0));					// LBA参数的40~47位

    outb(0x1F2, (uint8_t) (sector_count));
	outb(0x1F3, (uint8_t) (sector));			// LBA参数的0~7位
	outb(0x1F4, (uint8_t) (sector >> 8));		// LBA参数的8~15位
	outb(0x1F5, (uint8_t) (sector >> 16));		// LBA参数的16~23位

	outb(0x1F7, (uint8_t) 0x24);

	// 读取数据
	uint16_t *data_buf = (uint16_t*) buf;
	while (sector_count-- > 0) {
		// 每次扇区读之前都要检查,等待数据就绪
		while ((inb(0x1F7) & 0x88) != 0x8) {}

		// 读取并将数据写入到缓存中
		for (int i = 0; i < SECTOR_SIZE / 2; i++) {
			*data_buf++ = inw(0x1F0);
		}
	}
}

具体步骤

  • 设置硬盘控制寄存器

         硬盘控制器寄存器在 I/O 端口 0x1F00x1F7 范围内。这些寄存器用于设置硬盘的读取和写入操作。

  • 配置 LBA48 地址模式:

        使用 0x1F6 端口配置 LBA48 模式。

        向 0x1F20x1F30x1F40x1F5 端口写入扇区数和 LBA 地址的各个部分。

        向 0x1F7 端口发送命令以开始读取操作

  • 读取数据:

        检查数据是否准备好。

        从数据寄存器(0x1F0)读取数据,并将数据存储到缓冲区中。

5、创建内核工程并传递启动信息

创建kernel目录用来保存内核相关代码(init.c 、init.h)

init.c

#include "comm/boot_info.h"

int test (int a, int b) {
    return a + b;
}

/**
 * 内核入口
 */
void kernel_init (boot_info_t * boot_info) {
    int a = 1, b = 2;
    test(a , b);

    for (;;) {}
}

loader_32.c

/**
 * 从磁盘上加载内核
 */
void load_kernel(void) {
    // 读取的扇区数一定要大一些,保不准kernel.elf大小会变得很大

    read_disk(100, 500, (uint8_t *)SYS_KERNEL_LOAD_ADDR);
    ((void (*)(boot_info_t *))SYS_KERNEL_LOAD_ADDR)(&boot_info);
    for (;;) {}
}

load_kernel 函数的目的是从磁盘读取内核映像到内存中,并启动内核。它通过 read_disk 函数读取指定的扇区到内存中,然后调用加载到内存中的内核入口函数,将引导信息传递给内核进行初始化。最后,它进入一个无限循环,确保在内核启动完成后不会返回

向内核传递启动信息

压栈总是先esp指针减4,再写入数据       

出栈总是先取出数据,esp加4

在C函数中,编译器会根据定义的局部变量、计算过程、函数调用按照一定的规范自动规划栈的使用。具体的使用方法如下

  • 保存局部变量和数据
  • 传递参数:从参数列表右侧往左压入栈
  • 保存返回地址
  • 通过ebp+偏移取调用者的传入的参数和自己的局部变量

主要通过内核的汇编代码进行参数传递(有三种方法)

	.text
 	.global _start
	.extern kernel_init
_start:
    # 第一种方法
    # push %ebp
    # mov %esp, %ebp
    # mov 0x8(%ebp), %eax
    # push %eax

    # 第二种方法
    # mov 4(%esp), %eax
    # push %eax

    # 第三种方法
    push 4(%esp)

    # kernel_init(boot_info)
    call kernel_init

	jmp .

6、确定指令、数据在内存中位置

通过分析工程生成的反汇编文件和elf文件,展示了GCC工具链如何安排代码、数据在内存中的位置

GCC工具链默认按照 .text  .rodata  .data    .bss  来存储代码和数据

  • text:存储机器指令和代码
  • rodata:存储常量以及字符串本身
  • data:存储初始化的数据,全局的或者static静态的
  • .bss:存储未初始化的数据(即初始化为0),全局的或者静态的
  • stack:存储局部变量和函数调用中返回地址等
  • 相同类型的段会自动进行合并

在kernel下新建kernel.lbs链接文件,具体如下:

SECTIONS
{ 
    . = 0x000100000;   //(.)为 0x000100000 , 接下来的段将从这个地址开始布局

	.text : {
		*(.text)     //*.text 表示将所有输入文件中的 .text 段的内容合并到这个段中。
	} 

	.rodata : {
		*(.rodata)
	}

	.data : {
		*(.data)
	}
	.bss : {
		*(.bss)
	}
}


/*
起始地址: . = 0x000100000; 设置程序的起始地址为 0x000100000。
段定义:
.text: 包含执行代码。
.rodata: 包含只读数据。
.data: 包含已初始化的数据。
.bss: 包含未初始化的数据
*/

最后还要修改CMakeList中的链接文件

7、加载内核映像文件

        windows的可执行文件格式通常为.exe; Liunx更多为elf文件格式

1)具体的加载过程如下:

  1. 初步检查elf header的合法性
  2. 通过elf header->e_phoff 定位到 programe header table,遍历 elf header->e_phnum次,加载各个段
  3.  从文件位置 p_offset 处读取filesz大小的数据,写入到内存中 paddr 的位置处
  4.  如果 p_filesz < p_memsz,则将部分内存清零(bss区初始化) 取elf header->e_entry,跳转到该地址运行。

从上述流程可以看出,在C代码中定义的未初始化的全局变量(分配在BSS区,初始值为0),并没有在ELF中分配相应的空间,需要自己在内存中手动清0.

2)代码实现过程

  1. 首先从磁盘读取内核镜像文件:read_disk()  相关代码前面已经写过
  2. 然后解析ELF文件、将相关数据复制到内存的正确位置、返回 ELF 文件中的入口点地址:reload_elf_file()
  3. 最后跳到内核的入口地址:load_kernel
static uint32_t reload_elf_file (uint8_t * file_buffer) {
    // 读取的只是ELF文件,不像BIN那样可直接运行,需要从中加载出有效数据和代码
    // 简单判断是否是合法的ELF文件
    Elf32_Ehdr * elf_hdr = (Elf32_Ehdr *)file_buffer;
    if ((elf_hdr->e_ident[0] != ELF_MAGIC) || (elf_hdr->e_ident[1] != 'E')
        || (elf_hdr->e_ident[2] != 'L') || (elf_hdr->e_ident[3] != 'F')) {
        return 0;
    }

    // 然后从中加载程序头,将内容拷贝到相应的位置
    for (int i = 0; i < elf_hdr->e_phnum; i++) {
        Elf32_Phdr * phdr = (Elf32_Phdr *)(file_buffer + elf_hdr->e_phoff) + i;
        if (phdr->p_type != PT_LOAD) {
            continue;
        }

		// 全部使用物理地址,此时分页机制还未打开
        uint8_t * src = file_buffer + phdr->p_offset;
        uint8_t * dest = (uint8_t *)phdr->p_paddr;
        for (int j = 0; j < phdr->p_filesz; j++) {
            *dest++ = *src++;
        }

		// memsz和filesz不同时,后续要填0
		dest= (uint8_t *)phdr->p_paddr + phdr->p_filesz;
		for (int j = 0; j < phdr->p_memsz - phdr->p_filesz; j++) {
			*dest++ = 0;
		}
    }

    return elf_hdr->e_entry;
}

/**
 * 从磁盘上加载内核
 */
void load_kernel(void) {
    // 读取的扇区数一定要大一些,保不准kernel.elf大小会变得很大
    // 我就吃过亏,只读了100个扇区,结果运行后发现kernel的一些初始化的变量值为空,程序也会跑飞
    read_disk(100, 500, (uint8_t *)SYS_KERNEL_LOAD_ADDR);

     // 解析ELF文件,并通过调用的方式,进入到内核中去执行,同时传递boot参数
	 // 临时将elf文件先读到SYS_KERNEL_LOAD_ADDR处,再进行解析
    uint32_t kernel_entry = reload_elf_file((uint8_t *)SYS_KERNEL_LOAD_ADDR);
	if (kernel_entry == 0) {
		die(-1);
	}

	// 转换为函数指针,然后跳进内核
    ((void (*)(boot_info_t *))kernel_entry)(&boot_info);
}

8 总结

内联汇编显示字符串:利用显示单个字符的原理
检测内容容量:利用BIOS的软中断服务
切换到保护模式:A20地址线的开启,使得可用内存不局限于1MB,采用GDT表管理内存,使用LGDT寄存器指明GDT表的基这地址和大小

进入保护模式,不方便用BIOS的软中断,因此引入LBA进行磁盘读取,在加载内核文件时,需要对ELF文件解析,将内核相关数据、代码保存到内存的指定位置,然后跳到指定地址去执行内核代码

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

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

相关文章

STM32外设SPI(串行通信),W25Q64(8Mb)

1 非易失存储器:E2PROM,FLASH(断电不丢失) 2 易失存储器&#xff1a;SRAM,DRAM 3 W25Q64 1 从00 00 00 到 7F FF FF 2 block(块)&#xff0c;sector(扇区) &#xff0c;page&#xff08;页区&#xff09; 写数据到FLASH&#xff08;256字节&#xff09; 读数据很快&#…

002.Python爬虫系列_初识协议

我 的 个 人 主 页&#xff1a;&#x1f449;&#x1f449; 失心疯的个人主页 &#x1f448;&#x1f448; 入 门 教 程 推 荐 &#xff1a;&#x1f449;&#x1f449; Python零基础入门教程合集 &#x1f448;&#x1f448; 虚 拟 环 境 搭 建 &#xff1a;&#x1f449;&…

论文学习哇

2024.7.18 1.A gated cross-domain collaborative network for underwater object detection 对图像进行增强 摘要&#xff1a;水下存在低对比度和低光的问题&#xff0c;有的学者通过水下图像增强来提高图片质量&#xff0c;但会移除或者改变水下物体的细节。所以作者探索两…

【Android】使用 ADB 查看 Android 设备的 CPU 使用率

目录 一 查看整体CPU使用率 1 top 二 查看特定应用的CPU使用率 1 获取特定应用的进程 ID (PID) 2 使用 top 命令并过滤该 PID 三 常见的CPU相关命令参数 1 adb shell top 参数 一 查看整体CPU使用率 1 top top命令将显示当前所有进程的 CPU 使用情况&#xff0c;包括每…

Codeforces Round 969 (Div. 2) 题ABC详细题解,包含(C++,Python语言描述)

前言&#xff1a; 首先让我们恭喜tourist创造历史 他是第一&#xff0c;他又是第一&#xff0c;他总是第一&#xff0c;第一个codefores上4000分的&#xff0c;创造一个新的段位:Tourist&#xff0c;他的名字就是一个新的段位&#xff0c;他就是最高的山&#xff0c;最长的河 本…

shell脚本--正则表达式

一、正则表达式的类型 在Linux中,有两种流行的正则表达式引擎: POSIX基础正则表达式(basic regular expression,BRE)引擎 POSIX扩展正则表达式(extended regular expression,ERE)引擎 POSIX BRE引擎通常出现在依赖正则表达式进行文本过滤的编程语言中。它为常见模式提供…

蓝牙对象交换协议(OBEX) - 常见的opcode介绍

零.声明 本专栏文章我们会以连载的方式持续更新&#xff0c;本专栏计划更新内容如下&#xff1a; 第一篇:蓝牙综合介绍 &#xff0c;主要介绍蓝牙的一些概念&#xff0c;产生背景&#xff0c;发展轨迹&#xff0c;市面蓝牙介绍&#xff0c;以及蓝牙开发板介绍。 第二篇:Trans…

六、vue进阶知识点

一、scoped解决样式冲突 默认情况:写在组件中的样式会 全局生效→ 因此很容易造成多个组件之间的样式冲突问题。 1.全局样式:默认组件中的样式会作用到全局 2.局部样式:可以给组件加上 scoped 属性,可以让样式只作用于当前组件scoped原理? 1.当前组件内标签都被添加 data-v-…

【C++从练气到飞升】17---set和map

&#x1f388;个人主页&#xff1a;库库的里昂 ✨收录专栏&#xff1a;C从练气到飞升 &#x1f389;鸟欲高飞先振翅&#xff0c;人求上进先读书&#x1f389; 目录 ⛳️推荐 一、前言 1.1 关联式容器 1.2 键值对 1.3 树型结构的关联式容器 二、set 2.1 set的介绍 2.2 s…

百元蓝牙耳机品牌哪个牌子好?入围四大排名蓝牙耳机推荐

蓝牙耳机有两个极端&#xff0c;好用的蓝牙耳机音质效果堪比专业音响&#xff0c;而不好用的则不仅佩戴不舒服还容易伤耳&#xff0c;但是想要找到一款性价比高的百元蓝牙耳机不是一件容易的事。百元蓝牙耳机品牌哪个牌子好&#xff1f;身为一名蓝牙耳机发烧友&#xff0c;就给…

红石电路(我的世界)

红石电路&#xff08;Redstone circuits&#xff09;为玩家建造的&#xff0c;可以用于控制或激活其他机械的结构。 电路本身既可以被设计为用于响应玩家的手动激活&#xff0c;也可以让其自动工作——或是反复输出信号&#xff0c;或是响应非玩家引发的变化&#xff0c;例如生…

基于机器学习的商品评论情感分析

从淘宝爬取评论 使用Selenium模拟真实登录行为&#xff0c;并爬取数据。 数据清理 如果文本中有“666“&#xff0c;”好好好“等无用词语&#xff0c;去掉评论中的标点符号。 分词 使用jieba精确模式进行分词&#xff0c;构造词典 将词汇向量化 创建词语字典&#xff0c;并…

视频:Python深度学习量化交易策略、股价预测:LSTM、GRU深度门控循环神经网络|附代码数据...

全文链接&#xff1a;https://tecdat.cn/?p37539 分析师&#xff1a;Shuo Zhang 本文以上证综指近 22 年的日交易数据为样本&#xff0c;构建深度门控循环神经网络模型&#xff0c;从股价预测和制定交易策略两方面入手&#xff0c;量化循环神经网络在股票预测以及交易策略中的…

又一个免费代码生成工具

很多开发者对代码生成的印象可能只是单表 CURD&#xff0c;然而橙单却选择运营免费的代码生成工具&#xff0c;一定是有干货的。 功能 赠人玫瑰&#xff0c;手有余香。做良心开源&#xff0c;让更多开发者受益&#xff0c;这是我们的选择。 支持工作流、在线表单、用户和数据权…

用Python实现时间序列模型实战——Day 6: ARIMA 模型的理论基础

一、学习内容 1. ARIMA 模型的定义与公式推导 ARIMA 模型&#xff1a; ARIMA 模型全称为自回归积分滑动平均模型 (AutoRegressive Integrated Moving Average)&#xff0c;用于分析和预测单变量时间序列数据。ARIMA 模型结合了自回归 (AR) 模型、差分 (I) 和移动平均 (MA) 模…

黑神话悟空带火了哪些三维建模技术

自《黑神话&#xff1a;悟空》面世以来&#xff0c;不少小伙伴们被其中的3D场景所惊艳&#xff01; 在人工智能时代&#xff0c;NeRF&#xff08;Neural Radiance Fields&#xff0c;神经辐射场&#xff09;与3DGS&#xff08;3D Gaussian Splatting&#xff0c;3D高斯溅射&am…

Elastic Stack--ELFK架构

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 学习B站博主教程笔记&#xff1a; 最新版适合自学的ElasticStack全套视频&#xff08;Elk零基础入门到精通教程&#xff09;Linux运维必备—Elastic…

肿瘤免疫新视野:揭秘CXCL13+ T细胞对免疫检查点阻断的响应

肿瘤免疫新视野&#xff1a;揭秘CXCL13 T细胞对免疫检查点阻断的响应 在免疫治疗领域&#xff0c;探索肿瘤反应性T细胞对免疫检查点阻断&#xff08;ICB&#xff09;的响应机制一直是研究热点。一篇2022年发表在《Nature Cancer》上的研究——“Single-cell meta-analyses rev…

C++set与map容器

目录 一、关联式容器和序列式容器 二、树形结构的关联式容器 三、set容器 1.set容器的定义 2.set的构造 3.set的迭代器 4.set的容量 5.set的修改操作&#xff08;set容器不支持修改数据&#xff09; 6.set的一些其他常用接口 &#xff08;1&#xff09;find函数 &…

Google Earth Engine:对NDVI进行惠特克平滑算法进行长时序分析

目录 简介 函数 ee.Array.identity(size) Arguments: Returns: Array transpose(axis1, axis2) Arguments: Returns: Array matrixMultiply(image2) Arguments: Returns: Image matrixSolve(image2) Arguments: Returns: Image arrayFlatten(coordinateLabels, …