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

Linux ARM64 内核解读之内核引导和初始化

        处理器上电以后,首先执行引导程序,引导程序把内核加载到内存,然后执行内核,内核初始化完成以后,启动用户的第一个进程

一、        到哪里读取引导程序

        处理器到哪里读取引导程序的指令?处理器在上电时自动把程序计数器设置为处理器常识设计的某个固定值,对于ARM64处理器,这个固定值是0。处理器的内存管理单元(MMU)负责把虚拟地址转换为物理地址,ARM64处理器刚上电的时候没有开启内存管理单元,物理地址和虚拟地址相同,所以AMR64处理器到物理地址0取第一条指令。

        嵌入式设备通常使用NOR闪存作为只读存储器来存放引导程序。NOR闪存的容量比较小,最小的读写单位是字节,程序可以直接在芯片内执行。从物理地址0开始的一段物理空间地址被分配和NOR闪存。

        综上所述,ARM64处理器到虚拟地址0取指令,就是到物理地址0取指令,也就是到NOR闪存的起始位置取指令

二、        引导程序

        嵌入式设备通常使用U-boot作为引导程序。

        下面简要介绍一下ARM64处理器的U-Boot程序的执行过程,入口是文件“arch/arm/cpu/armv8/start.S”定义的_start,我们从标号_start开始分析。

        2.1         入口_start

        标号_start是U-boot程序的入口,直接跳转到标号reset执行。

        2.2        标号reset 

        从标号reset开始的代码如下:

 55 reset:56     /* Allow the board to save important registers */57     b   save_boot_params58 .globl  save_boot_params_ret59 save_boot_params_ret:60 61 #if CONFIG_POSITION_INDEPENDENT && !defined(CONFIG_SPL_BUILD)62     /* Verify that we're 4K aligned.  */63     adr x0, _start64     ands    x0, x0, #0xfff65     b.eq    1f66 0:67     /*68      * FATAL, can't continue.69      * U-Boot needs to be loaded at a 4K aligned address.70      *71      * We use ADRP and ADD to load some symbol addresses during startup.72      * The ADD uses an absolute (non pc-relative) lo12 relocation73      * thus requiring 4K alignment.74      */75     wfi76     b   0b77 1:78 79     /*80      * Fix .rela.dyn relocations. This allows U-Boot to be loaded to and81      * executed at a different address than it was linked at.82      */83 pie_fixup:84     adr x0, _start      /* x0 <- Runtime value of _start */85     ldr x1, _TEXT_BASE      /* x1 <- Linked value of _start */86     subs    x9, x0, x1      /* x9 <- Run-vs-link offset */87     beq pie_fixup_done88     adrp    x2, __rel_dyn_start     /* x2 <- Runtime &__rel_dyn_start */89     add     x2, x2, #:lo12:__rel_dyn_start90     adrp    x3, __rel_dyn_end       /* x3 <- Runtime &__rel_dyn_end */91     add     x3, x3, #:lo12:__rel_dyn_end92 pie_fix_loop:93     ldp x0, x1, [x2], #16   /* (x0, x1) <- (Link location, fixup) */94     ldr x4, [x2], #8        /* x4 <- addend */95     cmp w1, #1027       /* relative fixup? */96     bne pie_skip_reloc97     /* relative fix: store addend plus offset at dest location */98     add x0, x0, x999     add x4, x4, x9
100     str x4, [x0]
101 pie_skip_reloc:
102     cmp x2, x3
103     b.lo    pie_fix_loop
104 pie_fixup_done:
105 #endif
106 
107 #if defined(CONFIG_ARMV8_SPL_EXCEPTION_VECTORS) || !defined(CONFIG_SPL_BUILD)
108 .macro  set_vbar, regname, reg
109     msr \regname, \reg
110 .endm
111     adr x0, vectors
112 #else
113 .macro  set_vbar, regname, reg
114 .endm
115 #endif
116     /*
117      * Could be EL3/EL2/EL1, Initial State:
118      * Little Endian, MMU Disabled, i/dCache Disabled
119      */
120     switch_el x1, 3f, 2f, 1f
121 3:  set_vbar vbar_el3, x0
122     mrs x0, scr_el3
123     orr x0, x0, #0xf            /* SCR_EL3.NS|IRQ|FIQ|EA */
124     msr scr_el3, x0
125     msr cptr_el3, xzr           /* Enable FP/SIMD */
126     b   0f
127 2:  mrs x1, hcr_el2
128     tbnz    x1, #HCR_EL2_E2H_BIT, 1f    /* HCR_EL2.E2H */
129     orr x1, x1, #HCR_EL2_AMO_EL2    /* Route SErrors to EL2 */
130     msr hcr_el2, x1
131     set_vbar vbar_el2, x0
132     mov x0, #0x33ff
133     msr cptr_el2, x0            /* Enable FP/SIMD */
134     b   0f
135 1:  set_vbar vbar_el1, x0
136     mov x0, #3 << 20
137     msr cpacr_el1, x0           /* Enable FP/SIMD */
138 0:
139     msr daifclr, #0x4           /* Unmask SError interrupts */
140 
141 #if CONFIG_COUNTER_FREQUENCY
142     branch_if_not_highest_el x0, 4f
143     ldr x0, =CONFIG_COUNTER_FREQUENCY
144     msr cntfrq_el0, x0          /* Initialize CNTFRQ */
145 #endif
146 
147 4:  isb
148 
149     /*
150      * Enable SMPEN bit for coherency.
151      * This register is not architectural but at the moment
152      * this bit should be set for A53/A57/A72.
153      */
154 #ifdef CONFIG_ARMV8_SET_SMPEN
155     switch_el x1, 3f, 1f, 1f
156 3:
157     mrs     x0, S3_1_c15_c2_1               /* cpuectlr_el1 */
158     orr     x0, x0, #0x40
159     msr     S3_1_c15_c2_1, x0
160     isb
161 1:
162 #endif
163 
164     /* Apply ARM core specific erratas */
165     bl  apply_core_errata
166 
167     /*
168      * Cache/BPB/TLB Invalidate
169      * i-cache is invalidated before enabled in icache_enable()
170      * tlb is invalidated before mmu is enabled in dcache_enable()
171      * d-cache is invalidated before enabled in dcache_enable()
172      */
173 
174     /* Processor specific initialization */
175     bl  lowlevel_init
176 
177 #if defined(CONFIG_ARMV8_SPIN_TABLE) && !defined(CONFIG_SPL_BUILD)
178     branch_if_master x0, master_cpu
179     b   spin_table_secondary_jump
180     /* never return */
181 #elif defined(CONFIG_ARMV8_MULTIENTRY)
182     branch_if_master x0, master_cpu
183 
184     /*
185      * Slave CPUs
186      */
187 slave_cpu:
188     wfe
189     ldr x1, =CPU_RELEASE_ADDR
190     ldr x0, [x1]
191     cbz x0, slave_cpu
192     br  x0          /* branch to the given address */
193 #endif /* CONFIG_ARMV8_MULTIENTRY */
194 master_cpu:
195     msr SPSel, #1       /* make sure we use SP_ELx */
196     bl  _main

        第57行代码,调用各种板卡自定义的函数save_boot_params来保存重要的寄存器。

        第120-140行代码,根据处理器当前的异常级别设置寄存器。

        1)第121-126行代码,如果异常级别是3,那么把向量基准寄存器(VBAR_EL3)设置为异常向量的起始地址;设置安全配置寄存器(SCR_EL3)的NS、IRQ、FIQ和EA这4个位,也就是异常级别0和1处于非安全状态,在任何异常级别执行时都把中断、快速中断、同步外部中止和系统错误转发到异常级别3;把协处理器陷入寄存器(CPTR_EL3)设置为0,允许访问浮点和单指令多数据(SIMD)功能;设置计数器时钟频率寄存器(CNTFRQ_EL0)。

        2)第127-134行代码,如果异常级别是2,那么把向量基准地址寄存器(VBAR_EL2)设置为异常向量表的起始地址;设置协处理器陷入寄存器(CPTR_EL2),允许访问浮点和SIMD功能。

        3)第135-137行代码,如果异常级别是1,那么把向量基准地址寄存器(VBAR_EL1)设置为异常向量表的起始地址;设置协处理器访问控制寄存器(CPACR_EL1),允许访问浮点和SIMD功能。

        第196行代码,主处理器执行函数_main。

        

        下面介绍第二阶段程序加载器

        U-Boot分为SPL和正常的U-Boot程序两个部分,如果想要编译为SPL,需要开启配置宏“CONFIG_SPL_BUILD”。SPL是“Secondary Program Loder”的简称,即第二阶段程序加载器,第二阶段是相对于处理器里面的只读存储器的固化程序来说的,处理器启动时最先执行的只读存储器中的固化程序。

        固化程序通过检测启动方式来加载第二阶段程序加载器。为什么需要第二阶段程序加载器?原因是:一些处理器内部集成的静态随机访问存储器比较小,无法装载一个完整的U-boot镜像,此时需要第二阶段程序加载器,它主要负责初始化内存和存储设备驱动,然后把正常的U-boot镜像从存储设备读到内存中执行

        2.3        函数main

        函数main的代码在文件arch/arm/lib/crt0_64.S中。

 67 ENTRY(_main)68 69 /*70  * Set up initial C runtime environment and call board_init_f(0).71  */72 #if defined(CONFIG_TPL_BUILD) && defined(CONFIG_TPL_NEEDS_SEPARATE_STACK)73     ldr x0, =(CONFIG_TPL_STACK)74 #elif defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)75     ldr x0, =(CONFIG_SPL_STACK)76 #elif defined(CONFIG_INIT_SP_RELATIVE)77 #if CONFIG_POSITION_INDEPENDENT78     adrp    x0, __bss_start     /* x0 <- Runtime &__bss_start */79     add x0, x0, #:lo12:__bss_start80 #else81     adr x0, __bss_start82 #endif83     add x0, x0, #CONFIG_SYS_INIT_SP_BSS_OFFSET84 #else85     ldr x0, =(SYS_INIT_SP_ADDR)86 #endif87     bic sp, x0, #0xf    /* 16-byte alignment for ABI compliance */88     mov x0, sp89     bl  board_init_f_alloc_reserve90     mov sp, x091     /* set up gd here, outside any C code */92     mov x18, x093     bl  board_init_f_init_reserve94 95 #if defined(CONFIG_DEBUG_UART) && CONFIG_IS_ENABLED(SERIAL)96     bl  debug_uart_init97 #endif98 99     mov x0, #0
100     bl  board_init_f
101 
102 #if !defined(CONFIG_SPL_BUILD)
103 /*
104  * Set up intermediate environment (new sp and gd) and call
105  * relocate_code(addr_moni). Trick here is that we'll return
106  * 'here' but relocated.
107  */
108     ldr x0, [x18, #GD_START_ADDR_SP]    /* x0 <- gd->start_addr_sp */
109     bic sp, x0, #0xf    /* 16-byte alignment for ABI compliance */
110     ldr x18, [x18, #GD_NEW_GD]      /* x18 <- gd->new_gd */
111 
112     /* Skip relocation in case gd->gd_flags & GD_FLG_SKIP_RELOC */
113     ldr x0, [x18, #GD_FLAGS]        /* x0 <- gd->flags */
114     tbnz    x0, 11, relocation_return   /* GD_FLG_SKIP_RELOC is bit 11 */
115 
116     adr lr, relocation_return
117 #if CONFIG_POSITION_INDEPENDENT
118     /* Add in link-vs-runtime offset */
119     adrp    x0, _start      /* x0 <- Runtime value of _start */
120     add x0, x0, #:lo12:_start
121     ldr x9, _TEXT_BASE      /* x9 <- Linked value of _start */
122     sub x9, x9, x0      /* x9 <- Run-vs-link offset */
123     add lr, lr, x9
124 #if defined(CONFIG_SYS_RELOC_GD_ENV_ADDR)
125     ldr x0, [x18, #GD_ENV_ADDR] /* x0 <- gd->env_addr */
126     add x0, x0, x9
127     str x0, [x18, #GD_ENV_ADDR]
128 #endif
129 #endif
130     /* Add in link-vs-relocation offset */
131     ldr x9, [x18, #GD_RELOC_OFF]    /* x9 <- gd->reloc_off */
132     add lr, lr, x9  /* new return address after relocation */
133     ldr x0, [x18, #GD_RELOCADDR]    /* x0 <- gd->relocaddr */
134     b   relocate_code
135 
136 relocation_return:
137 
138 /*
139  * Set up final (full) environment
140  */
141     bl  c_runtime_cpu_setup     /* still call old routine */
142 #endif /* !CONFIG_SPL_BUILD */
143 #if !defined(CONFIG_SPL_BUILD) || CONFIG_IS_ENABLED(FRAMEWORK)
144 #if defined(CONFIG_SPL_BUILD)
145     bl  spl_relocate_stack_gd           /* may return NULL */
146     /* set up gd here, outside any C code, if new stack is returned */
147     cmp x0, #0
148     csel    x18, x0, x18, ne
149     /*
150      * Perform 'sp = (x0 != NULL) ? x0 : sp' while working
151      * around the constraint that conditional moves can not
152      * have 'sp' as an operand
153      */
154     mov x1, sp
155     cmp x0, #0
156     csel    x0, x0, x1, ne
157     mov sp, x0
158 #endif
159 
160 /*
161  * Clear BSS section
162  */
163     ldr x0, =__bss_start        /* this is auto-relocated! */
164     ldr x1, =__bss_end          /* this is auto-relocated! */
165 clear_loop:
166     str xzr, [x0], #8
167     cmp x0, x1
168     b.lo    clear_loop
169 
170     /* call board_init_r(gd_t *id, ulong dest_addr) */
171     mov x0, x18             /* gd_t */
172     ldr x1, [x18, #GD_RELOCADDR]    /* dest_addr */
173     b   board_init_r            /* PC relative jump */
174 
175     /* NOTREACHED - board_init_r() does not return */
176 #endif
177 
178 ENDPROC(_main)

        第173行代码,调用函数board_init_r(r是rear,标识后期),执行后期初始化。文件common/board_r.c定义了函数board_init_r,依次执行init_sequence_r中的每个函数,最后一个函数是run_main_loop。

        2.4        函数run_main_loop 

        U-Boot程序初始化完成后,准备处理命令,这是通过数组init_sequence_r的最后一个函数run_main_loop实现的。

        函数run_main_loop的执行流程如图所示,把主要工作委托给函数main_loop,函数main_loop的执行过程如下:

        1)调用bootdelay_process以读取环境变量bootdelay和bootcmd,环境变量bootdelay定义延迟时间,即等待用户按键时间的长度;环境变量bootcmd定义要执行的命令。

                U-boot程序到哪里读取环境变量?

                通常我们把NOR闪存分成多个分区,其中一个分区存放U-boot程序,第二个分区存放环

        境变量。U-boot程序里面的NOR的闪存驱动程序对分区信息硬编码,指定每个分区的偏移和

        长度。U-boot程序从环境变量分区读取环境变量 

        2)调用函数autoboot_command。函数autoboot_command先调用函数abortboot,等待用户按键。如果在等待时间内用户没有按键,就调用函数run_command_list,自动执行环境变量bootcmd定义的命令。假设环境变量bootcmd定义的命令是“bootm”,函数run_command_list查找命令表,发现命令“bootm”的处理函数是do_bootm。

        

        函数do_bootm的执行流程如图所示,把主要工作委托给函数do_bootm_states,函数do_bootm_states的执行过程如下。

        1)函数bootm_start负责初始化全局变量“bootm_headers_t images”。

        2)函数bootm_find_os把内核镜像从存储设备读到内存

        3)函数bootm_find_other读取其他信息,对于ARM64架构,通常扁平设备树(FDT)二进制文件,该文件用来传递硬件信息给内核。

        4)函数bootm_load_os把内核加载到正确的位置,如果内核镜像是被压缩过的,需要解压缩

        5)函数bootm_os_get_boot_func根据操作系统类型在数组boot_os中查找引导函数,linux内核的引导函数是do_bootm_linux。

        6)第一次调用do_bootm_linux时,参数flag是BOOTM_STATE_OS_PREP,为执行linux内核做准备工作。函数do_bootm_linux(flag=BOOTM_STATE_OS_PREP)把工作委托给函数boot_prep_linux,主要工作如下。

        1)分配一块内存,把扁平设备树二进制文件复制过去。

        2)修改扁平设备树二进制文件,例如:如果环境变量“bootargs”指定了内核参数,那么把节点“/chosen”的属性“bootargs”设置为内核参数字符串;如果多处理系统使用自旋表启动方法,那么针对每个处理器对应的节点“cpu”,把属性“enable-method”设置为“spi-table”,把属性‘cpu-release-addr“设置为全局变量spi_table_cpu_release_addr的地址。

        7)函数boot_selected_os调用函数do_bootm_linux,这是第二次调用函数do_bootm_linux,参数flag是BOOTM_STATE_OS_GO。函数do_bootm_linux(flag=BOOTM_STATE_OS_GO)调用函数boot_jump_linux,该函数跳转到内核的入口,第一个参数是扁平设备树二进制文件的起始地址,后面3个参数现在没有使用。

        函数boot_jump_linux负责跳转到linux内核,执行流程如图所示:

        

         1)调用函数smp_kick_all_cpus,如果打开了配置宏GONFIG_GICV2或者CONFIG_GIVC3,即使用通用中断控制器版本2或者版本3,那么发送中断请求以唤醒所以从处理器。

        2)调用函数dcache_disable,禁用处理器的缓存和内存管理单元。

        3)如果开启配置宏CONFIG_ARMV8_SWITCH_TO_EL1,表示在异常级别1执行Linux内核,那么先从异常级别3切换到异常级别2,然后切换到异常级别1,最后跳转到内核入口。

        4)如果在异常级别2执行Linux内核,那么先从异常级别3切换到异常级别2,然后跳转到内核入口。

        

三、        内核初始化

        3.1        汇编语言部分

        ARM64架构的内核入口是标号_head,入口文件arch/arm64/kernel/head.S。内核各版本的细节可能不一样,框架大概是一样的。

        函数__primary_switch 

        函数__primary_switch的主要执行流程如下:

        1)调用函数__enable_mmu以开启内存管理单元

        2)调用函数__primary_switched。

        

        函数__enable_mmu的主要执行流程如下。

        1)把转换表基准寄存器0(TTBR0_EL1)设置为恒等映射的页全局的起始物理地址。

        2)把转换表基准寄存器1(TTBR1_EL1)设置为内核的页全局目录的起始物理地址。

        3)设置系统控制寄存器(SCTLR_EL1),开启内存管理单元,以后执行程序时内存管理单元将会把虚拟地址转换成物理地址。

        函数__primary_switched的执行流程如下。

        1)把当前异常级别的栈指针寄存器设置0号线程内核堆栈的顶部(init_thread_union+THREAD_SIZE)。

        2)把异常级别0的栈指针寄存器(SP_EL0)设置为0号线程的结构体thread_info的地址(init_task.thread_info)。

        3)把向量基准地址寄存器(VBAR_EL1)设置为异常向量的起始地址(vectors)。

        4)计算内核镜像的起始虚拟地址(kimage_vaddr)和物理地址的差值,保存在全局变量kimage_voffset中。

        5)用0初始化内核的未初始化数据段。

        6)调用C语言函数start_kernel

        

        3.2        C语言部分

        内核初始化的C语言入口是函数start_kernel,函数start_kernel首先初始化基础设施,即初始化内核的各个子系统,然后调用rest_init。函数rest_init的执行流程如下。

        1)创建1号线程,即init线程和,线程函数是kernel_init。

        2)创建2号线程,即kthreadd线程,负责创建内核线程。

        3)0号线程最终变成空闲线程。

        init线程继续初始化,执行的主要的操作如下。

        1)smp_prepare_cpus():在启动从处理器以前执行准备工作。

        2)do_pre_smp_initcalls():执行在初始化SMP系统以前的早期初始化,即使用宏early_initcall注册的初始化函数

        3)smp_init():初始化SMP系统,启动所有处理器。

        4)do_initcalls():执行级别0~7的初始化

        5)打开控制台的字符设备文件"/dev/console",文件描述符0\1\2分别是标准输入、标准输出和标准错误,都是控制台的字符设备文件。

        6)prepare_namespace():挂载根文件系统,后面装载init程序时需从存储设备上的文件系统读文件。

        7)free_initmen():释放初始化代码和数据占用的内存。

        8)装载init程序(U-Boot程序可以传递内核参数“init=”以指定init程序),从内核线程转换成用户空间的init进程。

        级别0~7的初始化,是指使用以下宏注册的初始化函数

        3.3        SMP系统的引导 

  1. 自旋表(Spin Table)

    • 原理‌:从核复位后持续轮询内存中的特定地址(自旋表),主核完成初始化后向该地址写入从核的启动指令地址,触发从核跳转执行系统代码。
    • 硬件依赖‌:仅需基础内存管理单元(MMU),无需专用硬件支持。
  2. 电源状态协调接口(PSCI)

    • 原理‌:主核通过固件(如ARM Trusted Firmware)调用标准接口(如 PSCI_CPU_ON),由固件控制从核的电源状态和启动入口地址。
    • 硬件依赖‌:需芯片支持PSCI协议且预装兼容固件(如ATF)。
  3. ACPI停车协议(ACPI Parking Protocol)

    • 原理‌:主核解析ACPI表获取从核状态寄存器地址,写入启动指令唤醒从核,遵循x86平台的电源管理标准。
    • 硬件依赖‌:依赖ACPI兼容的硬件和操作系统驱动(如Linux ACPI子系统)。

特性自旋表(Spin Table)PSCIACPI停车协议
控制主体主核直接操作内存固件代理电源管理操作系统通过ACPI驱动控制
硬件依赖低(仅需MMU)高(需PSCI兼容固件)高(需ACPI兼容硬件)
功耗效率❌ 轮询导致高功耗✅ 休眠管理优化能效️ 中等(依赖OS电源策略)
启动延迟⚠️ 较高(等待轮询)✅ 低(事件触发唤醒)⚠️ 中等(需解析ACPI表)
适用场景低成本嵌入式设备(如IoT)移动设备/通用ARM平台ARM服务器或x86/ARM混合架构
系统兼容性✅ 所有ARM架构✅ ARMv8+⚠️ 需操作系统支持ACPI

  

四、        init进程

        嵌入式设备常用init程序对比表

特性BusyBox initSystem V initsystemd
复杂性低(仅Shell脚本)12中(分级启动脚本)812高(二进制+配置文件)512
启动速度快(轻量级)12慢(串行启动)812中(并行启动)512
依赖环境需ash Shell支持12需ash/bash Shell12需glibc库12
配置文件/etc/inittab(简化版)12/etc/inittab+/etc/rc.d6.target单元文件58
进程管理基础服务控制12脚本手动管理6内置守护进程监控58
典型应用场景资源受限的嵌入式设备12传统嵌入式Linux12复杂嵌入式系统(如树莓派)5
  1. BusyBox init‌:作为嵌入式领域的“瑞士军刀”,集成基础功能且体积极小,适合内存小于32MB的设备。
  2. System V init‌:通过运行级别(0-6)管理服务状态,但启动效率较低。
  3. systemd‌:支持并行启动和服务依赖管理,但占用资源较多(约34MB)。

现代嵌入式系统(如Yocto项目)通常允许在编译时选择这三种实现。

            

         

http://www.dtcms.com/a/329124.html

相关文章:

  • 算法详细讲解 - 离散化/区间合并
  • AI编程:python测试MQ消息服务联接和消息接收
  • SimD小目标样本分配方法
  • 什么是HTTP的无状态(举例详解)
  • JavaScript 中 let、var、const 的区别详解
  • 如何用外部电脑访问本地网页?
  • Leetcode题解:215,数组中的第k个最大元素,如何使用快速算法解决!
  • 6 ABP 框架中的事件总线与分布式事件
  • 豆包 + 蘑兔 AI:圆你创作歌曲梦​
  • JavaWeb-Servlet基础
  • 4.0 vue3简介
  • 【深入浅出STM32(1)】 GPIO 深度解析:引脚特性、工作模式、速度选型及上下拉电阻详解
  • 【Docker项目实战】使用Docker部署todo任务管理器
  • [AI React Web]`意图识别`引擎 | `上下文选择算法` | `url内容抓取` | 截图捕获
  • Android 双屏异显技术全解析:从原理到实战的多屏交互方案
  • 开发手记:一个支持自动翻译的H5客服系统
  • TeamViewer 以数字化之力,赋能零售企业效率与客户体验双提升
  • 在线 A2C实践
  • 玩转Docker | 使用Docker部署MediaWiki文档管理平台
  • 大文件上传解决方案
  • React useMemo 深度指南:原理、误区、实战与 2025 最佳实践
  • 【SpringBoot系列-01】Spring Boot 启动原理深度解析
  • C->C++核心过渡语法精讲与实战
  • 深度学习——03 神经网络(2)-损失函数
  • Spring Boot 使用 @NotBlank + @Validated 优雅校验参数
  • react+antd+vite自动引入组件、图标等
  • 适配安卓15(对应的sdk是35)
  • 单片机启动流程详细介绍
  • 开源WAF新标杆:雷池SafeLine用语义分析重构网站安全边界
  • vscode远程服务器出现一直卡在正在打开远程和连接超时解决办法