当前位置: 首页 > news >正文

解析MCUboot的实现原理和Image结构

目录

概述

1 MCUboot的功能

1.1 代码包结构

1.2 限制

2 MCUboot Image

2.1 Image格式

2.2 Flash Map

2.3 Image 槽

2.4 使用scratch交换

 2.5 Image 尾部数据结构

3 交换区

3.1 单交换区

 3.2 Multiple Image boot 

3.3 Image交换

4  交换状态(swap status)

 5 复位覆盖


概述

本文主要介绍MCUboot的实现原理和Image的结构,主要包括MCUboot的Image的格式,交换区,Resetrecovery等概念,还介绍了使用Image进行代码更新的步骤。

1 MCUboot的功能

1.1 代码包结构

MCUboot包含两个代码包:

1)The bootutil library (boot/bootutil)

2)The boot application (each port has its own at boot/)

bootutil 库执行了引导加载程序的大部分功能。特别缺少的是实际跳转到主映像的最后一步。这最后一步则由引导应用程序来实现。以这种方式分离引导加载程序的功能是为了能够对引导加载程序进行单元测试。库可以进行单元测试,但应用程序不行。因此,只要有可能,功能就会委托给 bootutil 库。 

1.2 限制

当前,引导加载程序仅支持具有以下特征的映像:

1) Built to run from flash.

2)   Built to run from a fixed location (i.e., not position-independent).

2 MCUboot Image

2.1 Image格式

以下定义描述image格式:

#define IMAGE_MAGIC                 0x96f3b83d#define IMAGE_HEADER_SIZE           32struct image_version {uint8_t iv_major;uint8_t iv_minor;uint16_t iv_revision;uint32_t iv_build_num;
};/** Image header.  All fields are in little endian byte order. */
struct image_header {uint32_t ih_magic;uint32_t ih_load_addr;uint16_t ih_hdr_size;           /* Size of image header (bytes). */uint16_t ih_protect_tlv_size;   /* Size of protected TLV area (bytes). */uint32_t ih_img_size;           /* Does not include header. */uint32_t ih_flags;              /* IMAGE_F_[...]. */struct image_version ih_ver;uint32_t _pad1;
};#define IMAGE_TLV_INFO_MAGIC        0x6907
#define IMAGE_TLV_PROT_INFO_MAGIC   0x6908/** Image TLV header.  All fields in little endian. */
struct image_tlv_info {uint16_t it_magic;uint16_t it_tlv_tot;  /* size of TLV area (including tlv_info header) */
};/** Image trailer TLV format. All fields in little endian. */
struct image_tlv {uint8_t  it_type;   /* IMAGE_TLV_[...]. */uint8_t  _pad;uint16_t it_len;    /* Data length (not including TLV header). */
};/** Image header flags.*/
#define IMAGE_F_PIC                      0x00000001 /* Not supported. */
#define IMAGE_F_ENCRYPTED_AES128         0x00000004 /* Encrypted using AES128. */
#define IMAGE_F_ENCRYPTED_AES256         0x00000008 /* Encrypted using AES256. */
#define IMAGE_F_NON_BOOTABLE             0x00000010 /* Split image app. */
#define IMAGE_F_RAM_LOAD                 0x00000020/** Image trailer TLV types.*/
#define IMAGE_TLV_KEYHASH           0x01   /* hash of the public key */
#define IMAGE_TLV_SHA256            0x10   /* SHA256 of image hdr and body */
#define IMAGE_TLV_RSA2048_PSS       0x20   /* RSA2048 of hash output */
#define IMAGE_TLV_ECDSA224          0x21   /* ECDSA of hash output - Not supported anymore */
#define IMAGE_TLV_ECDSA_SIG         0x22   /* ECDSA of hash output */
#define IMAGE_TLV_RSA3072_PSS       0x23   /* RSA3072 of hash output */
#define IMAGE_TLV_ED25519           0x24   /* ED25519 of hash output */
#define IMAGE_TLV_SIG_PURE          0x25   /* If true then any signature found has beencalculated over image directly. */
#define IMAGE_TLV_ENC_RSA2048       0x30   /* Key encrypted with RSA-OAEP-2048 */
#define IMAGE_TLV_ENC_KW            0x31   /* Key encrypted with AES-KW-128 or256 */
#define IMAGE_TLV_ENC_EC256         0x32   /* Key encrypted with ECIES-P256 */
#define IMAGE_TLV_ENC_X25519        0x33   /* Key encrypted with ECIES-X25519 */
#define IMAGE_TLV_DEPENDENCY        0x40   /* Image depends on other image */
#define IMAGE_TLV_SEC_CNT           0x50   /* security counter */

ih_hdr_size: 

字段表示头部的长度,因此表示Image本身的偏移量。这个字段提供了向后兼容性,以防更改Image头的格式。


ih_protect_tlv_size:   

字段表示TLV保护区域的长度。如果存在受保护的TLV,则必须存在一个magic等于IMAGE_TLV_PROT_INFO_MAGIC的TLV信息头,并且受保护的TLV(加上信息头本身)必须包含在哈希计算中。否则,哈希值只计算图像标题和图像本身。在此例中,ih_protect_tlv_size字段的值为0。

2.2 Flash Map

设备的闪存根据其闪存映射进行分区。在高层次上,flash映射将数字id映射到flash区域。闪存区是磁盘的一个区域,具有以下属性:

1) 一个区域可以完全擦除而不影响任何其他区域

2) 对一个区域的写入不限制对其他区域的写入。

引导加载程序使用以下flash区域id:

/* Independent from multiple image boot */
#define FLASH_AREA_BOOTLOADER         0
#define FLASH_AREA_IMAGE_SCRATCH      3
/* If the bootloader is working with the first image */
#define FLASH_AREA_IMAGE_PRIMARY      1
#define FLASH_AREA_IMAGE_SECONDARY    2
/* If the bootloader is working with the second image */
#define FLASH_AREA_IMAGE_PRIMARY      5
#define FLASH_AREA_IMAGE_SECONDARY    6

引导加载程序区域包含引导加载程序映像本身。其他方面将在后面的部分中描述。闪存可以包含多个可执行映像,因此主区域和辅助区域的闪存区域id是根据活动映像(引导加载程序当前正在其上工作)的数量映射的。

2.3 Image 槽

闪存的一部分可划分为多个Image区域,每个Image区域包含两个Image插槽:

1)主插槽

2)辅助插槽

正常情况下,引导加载程序只会从主插槽运行Image,因此必须构建映像,使它们能够从flash中的固定位置运行(这方面的例外是direct-xip和ram-load升级模式)。如果引导加载程序需要运行驻留在辅助插槽中的映像,那么它必须在这样做之前将其内容复制到主插槽中,要么交换两个映像,要么覆盖主插槽的内容。

2.4 使用scratch交换

当使用使用scratch交换算法时,除了Image区域的插槽之外,引导加载程序还需要scratch区域来允许可靠的Image交换。scratch区必须具有足够的大小,至少可以存储将要交换的最大扇区。许多设备都有同样大小的小扇区,例如4K,而其他设备则有可变大小的扇区,其中最大的扇区可能是128K或256K,因此scratch必须足够大才能存储这些扇区。

scratch只在交换固件时使用,这意味着只在进行升级时使用。考虑到这一点,使用更大尺寸的划痕的主要原因是闪存磨损将更均匀地分布,因为单个扇区的写入次数是使用两个扇区的两倍,例如。为您的用例评估划痕的理想大小,以下参数是相关的:

  • the ratio of image size / scratch size
  • the number of erase cycles supported by the flash hardware

 2.5 Image 尾部数据结构

为了使引导加载程序能够确定当前状态以及在当前引导操作期间应该采取什么操作,它使用存储在映像闪存区域中的元数据。在交换时,其中一些元数据被临时复制到刮痕区或从scratch区复制出来。

这个元数据位于Inage区域的末尾,称为Image trailer。其具体结构如下:

 0                   1                   2                   30 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+~                                                               ~~    Swap status (BOOT_MAX_IMG_SECTORS * min-write-size * 3)    ~~                                                               ~+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|                 Encryption key 0 (16 octets) [*]              ||                                                               |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|                    0xff padding as needed                     ||  (BOOT_MAX_ALIGN minus 16 octets from Encryption key 0) [*]   |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|                 Encryption key 1 (16 octets) [*]              ||                                                               |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|                    0xff padding as needed                     ||  (BOOT_MAX_ALIGN minus 16 octets from Encryption key 1) [*]   |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|                      Swap size (4 octets)                     |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|                    0xff padding as needed                     ||        (BOOT_MAX_ALIGN minus 4 octets from Swap size)         |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|   Swap info   |  0xff padding (BOOT_MAX_ALIGN minus 1 octet)  |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|   Copy done   |  0xff padding (BOOT_MAX_ALIGN minus 1 octet)  |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|   Image OK    |  0xff padding (BOOT_MAX_ALIGN minus 1 octet)  |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|                    0xff padding as needed                     ||         (BOOT_MAX_ALIGN minus 16 octets from MAGIC)           |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|                       MAGIC (16 octets)                       ||                                                               |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

 1)Swap info

交换信息:单个字节,编码以下信息:
交换类型:0 ~ 3位存储。指示正在进行的交换操作的类型。当MCUboot恢复被中断的交换时,它使用这个字段来确定要执行的操作类型。该字段包含下表中下列值之一。


图像号:存储在4-7位。它在单映像引导时总是0值。在多映像引导的情况下,它指示中断发生时交换了哪个映像。在所有图像交换操作期间使用相同的划痕区域。因此使用该字段

NameValue
BOOT_SWAP_TYPE_TEST2
BOOT_SWAP_TYPE_PERM3
BOOT_SWAP_TYPE_REVERT4

 2)拷贝完成:单字节表示该槽中的映像是否完成(0x01=done;0 xff =没有完成)。

3)image OK:一个单字节,表示这个槽中的映像是否已被用户确认为好的(0x01=confirmed;0 xff =没有证实)。

4)MAGIC:标识Image trailer布局的16字节字段。它可以根据映像支持的最大写对齐(BOOT_MAX_ALIGN)假设不同的值,由以下结构定义:

union boot_img_magic_t
{struct {uint16_t align;uint8_t magic[14];};uint8_t val[16];
};

如果BOOT_MAX_ALIGN是8字节,那么MAGIC包含以下16字节:

const union boot_img_magic_t boot_img_magic = {.val = {0x77, 0xc2, 0x95, 0xf3,0x60, 0xd2, 0xef, 0x7f,0x35, 0x52, 0x50, 0x0f,0x2c, 0xb6, 0x79, 0x80}
};

如果BOOT_MAX_ALIGN被定义为任何不同于8的值,那么支持的最大写对齐值将在MAGIC字段中编码,后跟一个固定的14字节模式:

const union boot_img_magic_t boot_img_magic = {.align = BOOT_MAX_ALIGN,.magic = {0x2d, 0xe1,0x5d, 0x29, 0x41, 0x0b,0x8d, 0x77, 0x67, 0x9c,0x11, 0x0f, 0x1f, 0x8a}
};

3 交换区

3.1 单交换区

对于新的交换,MCUboot必须检查一个字段集合,以确定执行哪个交换操作。
Image trailers记录是围绕flash硬件施加的限制构建的。因此,它们没有一个非常直观的设计,并且很难通过查看图像预告片来了解设备的状态。最好通过一组表将所有可能的跟踪状态映射到上面描述的交换类型。这些表格转载如下。

    State I (swap using offset only)| primary slot | secondary slot |-----------------+--------------+----------------|magic | Any          | Good           |image-ok | Any          | Unset          |copy-done | Any          | Set            |-----------------+--------------+----------------'result: BOOT_SWAP_TYPE_REVERT                   |-------------------------------------------------'State II| primary slot | secondary slot |-----------------+--------------+----------------|magic | Any          | Good           |image-ok | Any          | Unset          |copy-done | Any          | Any            |-----------------+--------------+----------------'result: BOOT_SWAP_TYPE_TEST                     |-------------------------------------------------'State III| primary slot | secondary slot |-----------------+--------------+----------------|magic | Any          | Good           |image-ok | Any          | 0x01           |copy-done | Any          | Any            |-----------------+--------------+----------------'result: BOOT_SWAP_TYPE_PERM                     |-------------------------------------------------'State IV| primary slot | secondary slot |-----------------+--------------+----------------|magic | Good         | Any            |image-ok | 0xff         | Any            |copy-done | 0x01         | Any            |-----------------+--------------+----------------'result: BOOT_SWAP_TYPE_REVERT                   |-------------------------------------------------'

上述三种状态中的任何一种都会导致MCUboot尝试交换Image。否则,MCUboot不会尝试交换映像,从而导致其他三种交换类型之一,如状态IV所示。

    State V| primary slot | secondary slot |-----------------+--------------+----------------|magic | Any          | Any            |image-ok | Any          | Any            |copy-done | Any          | Any            |-----------------+--------------+----------------'result: BOOT_SWAP_TYPE_NONE,                    |BOOT_SWAP_TYPE_FAIL, or                 |BOOT_SWAP_TYPE_PANIC                    |-------------------------------------------------'

 3.2 Multiple Image boot 

当闪存包含多个可执行映像时,引导加载程序的操作稍微复杂一些,但与前面描述的使用一个映像的过程类似。每个图像都可以独立更新,因此闪存进一步分区,为每个图像安排两个插槽。

+--------------------+
| MCUboot            |
+--------------------+~~~~~            <- memory might be not contiguous
+--------------------+
| Image 0            |
| primary   slot     |
+--------------------+
| Image 0            |
| secondary slot     |
+--------------------+~~~~~            <- memory might be not contiguous
+--------------------+
| Image N            |
| primary   slot     |
+--------------------+
| Image N            |
| secondary slot     |
+--------------------+
| Scratch            |
+--------------------+

3.3 Image交换

 引导程序交换两个映像槽的内容有两个原因:
1) 用户发出了“设置挂起”操作;次要插槽中的映像应该运行一次(状态I)或重复运行一次(状态II),这取决于是否指定了永久交换。
2) 测试映像在未确认的情况下重启;引导加载程序应该恢复到当前在辅助插槽中的原始映像(状态III)。

 如果映像显示应该运行辅助插槽中的映像,则引导加载程序需要将其复制到主插槽。当前在主插槽中的映像也需要保留在flash中,以便以后使用。此外,如果引导加载程序在交换操作中重置,则两个映像都需要是可恢复的。按照以下步骤交换两个镜像:

Step-1: 确定两个插槽是否足够兼容以交换它们的映像。为了兼容,两者都必须只有能够容纳划痕区域的扇区,如果其中一个扇区比另一个扇区大,它必须能够完全容纳来自另一个插槽的某些四舍五入扇区。在接下来的步骤中,我们将使用术语“区域”来表示复制/擦除的数据总量,因为这可以是任何数量的扇区,这取决于有多少扇区能够适合某些交换操作。

Step-2: 按降序迭代区域索引列表(即从最大索引开始);仅复制预定为图像一部分的区域;当前元素= "index"。

a.擦除划痕区域。
b.将secondary_slot[index]拷贝到划痕区。
如果这是槽中的最后一个区域,则刮擦区域具有初始化以存储初始状态的临时状态区域,因为必须擦除主槽的最后一个区域。在这种情况下,只复制计算为图像的数据。
否则,如果这是交换的第一个区域,但不是槽中的最后一个区域,则初始化主槽中的状态区域并复制完整的区域内容。否则,复制整个区域的内容。

c.写入更新后的swap状态(i)。
d.擦除secondary_slot[index]

e.将primary_slot[index]拷贝到secondary_slot[index]。
如果这不是槽中的最后一个区域,则擦除辅助槽中的预告片,以始终使用主槽中的预告片。
f.写入更新后的swap状态(ii)。
g.擦除primary_slot[index]。
h.将刮痕区复制到primary_slot[index]中,根据之前在步骤b中复制的数量。如果这是槽中的最后一个区域,则从头读取状态(它临时存储在那里),然后在主槽中重新写入状态

Step-3:  将交换过程的完成持久化到主插槽映像 trailer。

注意点: 

步骤2-f中的附加警告是必要的,以便用户可以在稍后的时间写入辅助插槽映像预告片。在不写入映像预告片的情况下,用户可以在二级插槽中测试映像(即切换到状态I)。

步骤3的细节取决于是否正在测试映像、永久使用映像、恢复映像或在请求交换时发生了辅助插槽的验证失败:

* test:o Write primary_slot.copy_done = 1(swap caused the following values to be written:primary_slot.magic = BOOT_MAGICsecondary_slot.magic = UNSETprimary_slot.image_ok = Unset)* permanent:o Write primary_slot.copy_done = 1(swap caused the following values to be written:primary_slot.magic = BOOT_MAGICsecondary_slot.magic = UNSETprimary_slot.image_ok = 0x01)* revert:o Write primary_slot.copy_done = 1o Write primary_slot.image_ok = 1(swap caused the following values to be written:primary_slot.magic = BOOT_MAGIC)* failure to validate the secondary slot:o Write primary_slot.image_ok = 1

4  交换状态(swap status)

交换状态区域允许引导加载程序在映像交换操作中间重新启动时进行恢复。交换状态区域由一系列单字节记录组成。这些记录是独立写入的,因此必须根据flash硬件施加的最小写入大小进行填充。交换状态区域的结构如下所示。在该图中,为了简单起见,假设最小写大小为1,该图显示了每个扇区的3种状态,适用于使用scratch交换和使用move交换,但是在使用offset交换模式中只有3种状态。

     0                   1                   2                   30 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|sec127,state 0 |sec127,state 1 |sec127,state 2 |sec126,state 0 |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|sec126,state 1 |sec126,state 2 |sec125,state 0 |sec125,state 1 |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|sec125,state 2 |                                               |+-+-+-+-+-+-+-+-+                                               +~                                                               ~~               [Records for indices 124 through 1              ~~                                                               ~~               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+~               |sec000,state 0 |sec000,state 1 |sec000,state 2 |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

 以上可能一点帮助都没有;这里有一段英文描述。
每个映像槽被划分成一系列的闪存扇区。如果我们枚举单个槽中的扇区,从0开始,我们将得到一个扇区索引列表。由于有两个映像槽,每个扇区索引将对应一对扇区。例如,扇区索引0分别对应主槽位的第一个扇区和从槽位的第一个扇区。最后,反转索引列表,使列表以索引BOOT_MAX_IMG_SECTORS - 1开始,以0结束。

在交换操作期间,每个扇区索引通过四个不同的状态转换:

0. primary slot: image 0,   secondary slot: image 1,   scratch: N/A
1. primary slot: image 0,   secondary slot: N/A,       scratch: image 1 (1->s, erase 1)
2. primary slot: N/A,       secondary slot: image 0,   scratch: image 1 (0->1, erase 0)
3. primary slot: image 1,   secondary slot: image 0,   scratch: N/A     (s->0)

 每次扇区索引转换到新状态时,引导加载程序都会向交换状态区域写入一条记录。逻辑上,引导加载程序只需要每个扇区索引一个记录来跟踪当前的交换状态。然而,由于flash硬件的限制,当索引状态改变时,不能重写记录。为了解决这个问题,引导加载程序为每个扇区索引使用三条记录,而不是一条。

每个部门-状态对表示为一组三条记录。记录值映射到上述四种状态如下所示:

            | rec0 | rec1 | rec2--------+------+------+------state 0 | 0xff | 0xff | 0xffstate 1 | 0x01 | 0xff | 0xffstate 2 | 0x01 | 0x02 | 0xffstate 3 | 0x01 | 0x02 | 0x03

 5 复位覆盖

如果引导加载程序在交换操作中重置,则两个Image可能在flash中不连续。Bootutil通过使用Image起始数据来确定Image部分如何在flash中分布来从这种情况中恢复。

第一步是确定相关交换状态区域的位置。由于该区域嵌入到映像槽中,因此在交换操作期间,它在闪存中的位置会发生变化。下面的一组表将图像预告片内容映射到交换状态位置。在这些表中,“source”字段表示交换状态区域的位置。在多IMage启动的情况下,总是成对地检查图像的主区和单划痕区。如果在划痕区域找到交换状态,那么它可能不属于当前IMage。交换状态的swap_info字段存储相应的信息.

              | primary slot | scratch      |----------+--------------+--------------|magic | Good         | Any          |copy-done | 0x01         | N/A          |----------+--------------+--------------'source: none                            |----------------------------------------'| primary slot | scratch      |----------+--------------+--------------|magic | Good         | Any          |copy-done | 0xff         | N/A          |----------+--------------+--------------'source: primary slot                    |----------------------------------------'| primary slot | scratch      |----------+--------------+--------------|magic | Any          | Good         |copy-done | Any          | N/A          |----------+--------------+--------------'source: scratch                         |----------------------------------------'| primary slot | scratch      |----------+--------------+--------------|magic | Unset        | Any          |copy-done | 0xff         | N/A          |----------+--------------+--------------|source: primary slot                    |----------------------------------------+------------------------------+This represents one of two cases:                                      |o No swaps ever (no status to read, so no harm in checking).           |o Mid-revert; status in the primary slot.                              |For this reason we assume the primary slot as source, to trigger a     |check of the status area and find out if there was swapping under way. |-----------------------------------------------------------------------'

如果交换状态区域表明映像不是连续的,那么MCUboot通过读取活动映像尾片中的交换信息字段并从0-3位提取交换类型来确定被中断的交换操作的类型,然后恢复操作。换句话说,它应用前一节中定义的过程,将映像1移动到主插槽中,将映像0移动到辅助插槽中。如果引导状态表明在刮擦区域中存在映像部分,则从区域交换过程中的步骤e或步骤h开始将该部分复制到正确的位置。 

相关文章:

  • ReentrantLock实现公平锁和非公平锁
  • 关于离散化算法的看法与感悟
  • 用状态变量根据超稳定性理论设计模型参考自适应系统
  • 2025年深圳杯D题第二版本python代码 论文分享
  • 一些好玩的东西
  • 学习方法讨论——正论科举精神的内核
  • 十大机器学习算法:理论与实战
  • 「Mac畅玩AIGC与多模态18」开发篇14 - 多字段输出与结构控制工作流示例
  • Android逆向学习(八)Xposed快速上手(上)
  • RTX-3090 Qwen3-8B Dify RAG环境搭建
  • Vue 3 中 ref 的使用例子
  • 大连理工大学选修课——图形学:第一章 图形学概述
  • 相向双指针-16. 最接近的三数之和
  • 新一代智能座舱娱乐系统软件架构设计文档
  • 【计网】互联网的组成
  • Linux watch 命令使用详解
  • Easy云盘总结篇-文件上传01
  • 高等数学-第七版-下册 选做记录 习题10-2
  • LangChain4J-XiaozhiAI 项目分析报告
  • FiLo++的框架图介绍
  • 爱彼迎:一季度总收入约23亿美元,将拓展住宿以外的新领域
  • “特朗普效应”下澳大利亚执政工党赢得大选,年轻选民担忧房价
  • 美国多地爆发集会抗议特朗普政府多项政策
  • 李在明涉嫌违反《公职选举法》案将于15日进行首次重审公审
  • 印巴局势紧张或爆发军事冲突,印度空军能“一雪前耻”吗?
  • 讲武谈兵|朝鲜“崔贤”号驱逐舰下水,朝版“宙斯盾”战力如何?