一、内核启动入口点
/*
* Kernel startup entry point.
* ---------------------------
*
* The requirements are:
* MMU = off, D-cache = off, I-cache = on or off,
* x0 = physical address to the FDT blob.
* 这部分注释说明了内核启动入口点的要求和约束条件。
* 要求包括:MMU(内存管理单元)关闭,数据缓存(D-cache)关闭,指令缓存(I-cache)可以开启或关闭,
* 以及需要将物理地址传递给 FDT(平台设备树)blob 的寄存器 x0
*
* Note that the callee-saved registers are used for storing variables
* that are useful before the MMU is enabled. The allocations are described
* in the entry routines.
* 请注意,被调用方保存的寄存器用于存储启用 MMU 之前有用的变量。分配在条目例程中描述。
*/
/* 标识内核启动入口点 */
__HEAD
/*
* DO NOT MODIFY. Image header expected by Linux boot-loaders.
* 请勿修改。Linux 引导加载进程所需的映像标头。
*/
efi_signature_nop // special NOP to identity as PE/COFF executable 特殊 NOP 标识为 PE/COFF 可执行文档
b primary_entry // branch to kernel start, magic 跳转到内核启动代码
/* 这部分代码是内核启动入口点后面的一条 .head 段,包含了用于 Linux 引导加载器期望的镜像头信息。其中包括偏移量、大小、标志等信息 */
.quad 0 // Image load offset from start of RAM, little-endian 镜像加载偏移量
le64sym _kernel_size_le // Effective size of kernel image, little-endian 内核镜像的有效大小
le64sym _kernel_flags_le // Informative flags, little-endian 信息标志
.quad 0 // reserved
.quad 0 // reserved
.quad 0 // reserved
.ascii ARM64_IMAGE_MAGIC // Magic number 幻数
.long .Lpe_header_offset // Offset to the PE header. PE头的偏移量
__EFI_PE_HEADER
/* 这行代码标识接下来的代码是 EFI(扩展固件接口)PE(可执行文件)头部信息 */
.section ".idmap.text","a"
二、primary_entry
/* 这行代码标识接下来的代码是 EFI(扩展固件接口)PE(可执行文件)头部信息 */
.section ".idmap.text","a"
/*
* The following callee saved general purpose registers are used on the
* primary lowlevel boot path:
* 以下被调用者保存的通用寄存器用于主低级引导路径
*
* 寄存器 上下文 目标
* Register Scope Purpose
* x19 primary_entry() .. start_kernel() whether we entered with the MMU on
* x20 primary_entry() .. __primary_switch() CPU boot mode
* x21 primary_entry() .. start_kernel() FDT pointer passed at boot in x0
* x22 create_idmap() .. start_kernel() ID map VA of the DT blob
* x23 primary_entry() .. start_kernel() physical misalignment/KASLR offset
* x24 __primary_switch() linear map KASLR seed
* x25 primary_entry() .. start_kernel() supported VA size
* x28 create_idmap() callee preserved temp register
*/
SYM_CODE_START(primary_entry)
bl record_mmu_state
bl preserve_boot_args
bl create_idmap
/*
* If we entered with the MMU and caches on, clean the ID mapped part
* of the primary boot code to the PoC so we can safely execute it with
* the MMU off.
* 如果我们在 MMU 和缓存打开的情况下输入,请清理主启动代码的 ID 映射部分到 PoC,
* 以便我们可以在关闭 MMU 的情况下安全地执行它。
*/
cbz x19, 0f
adrp x0, __idmap_text_start
adr_l x1, __idmap_text_end
adr_l x2, dcache_clean_poc
blr x2
0: mov x0, x19
bl init_kernel_el // w0=cpu_boot_mode
mov x20, x0
/*
* The following calls CPU setup code, see arch/arm64/mm/proc.S for
* details.
* 下面调用 CPU 设置代码,请参阅 arch/arm64/mm/proc.S 了解详细信息。
* On return, the CPU will be ready for the MMU to be turned on and
* the TCR will have been set.
* 在返回时,CPU将准备好MMU被打开,TCR将被设置。
*/
#if VA_BITS > 48
mrs_s x0, SYS_ID_AA64MMFR2_EL1
tst x0, #0xf << ID_AA64MMFR2_EL1_VARange_SHIFT
mov x0, #VA_BITS
mov x25, #VA_BITS_MIN
csel x25, x25, x0, eq
mov x0, x25
#endif
bl __cpu_setup // initialise processor 初始化处理器
b __primary_switch
SYM_CODE_END(primary_entry)
这段代码是主要的入口点函数 primary_entry
,在启动过程中执行一系列操作后将控制权切换到内核。
以下是对每行代码的解释:
SYM_CODE_START(primary_entry)
这行代码表示这是一个本地符号,标识 primary_entry
函数的开始位置。
bl record_mmu_state
bl preserve_boot_args
bl create_idmap
这三行代码调用了三个不同的函数。首先调用 record_mmu_state
函数来记录 MMU 的状态,然后调用 preserve_boot_args
函数来保留引导参数,最后调用 create_idmap
函数来创建 ID 映射(identity mapping)。
cbz x19, 0f
adrp x0, __idmap_text_start
adr_l x1, __idmap_text_end
adr_l x2, dcache_clean_poc
blr x2
0: mov x0, x19
bl init_kernel_el // w0=cpu_boot_mode
mov x20, x0
这几行代码根据 x19
寄存器的值进行条件分支。如果 x19
为零(等于零),则跳转到标号 0
处。在标号 0
处,会将 __idmap_text_start
和 __idmap_text_end
地址计算给寄存器 x0
和 x1
,并将 dcache_clean_poc
地址计算给寄存器 x2
,然后通过 blr
指令将控制权转移给 dcache_clean_poc
函数进行 ID 映射部分的清理操作。如果 x19
不为零,则继续执行下一行代码,将 x19
的值传递给 init_kernel_el
函数,并将函数的返回值存储在 x20
寄存器中。
#if VA_BITS > 48
mrs_s x0, SYS_ID_AA64MMFR2_EL1
tst x0, #0xf << ID_AA64MMFR2_EL1_VARange_SHIFT
mov x0, #VA_BITS
mov x25, #VA_BITS_MIN
csel x25, x25, x0, eq
mov x0, x25
#endif
bl __cpu_setup // initialise processor
b __primary_switch
这部分代码是一个条件编译块,在条件编译宏 VA_BITS
大于 48 时才会被编译。它首先读取 SYS_ID_AA64MMFR2_EL1
寄存器的值到 x0
中,然后根据位偏移和掩码进行测试,并根据结果设置 x0
的值。接下来,调用 __cpu_setup
函数来初始化处理器。最后,通过无条件分支指令 b
转移到 __primary_switch
位置。
SYM_CODE_END(primary_entry)
这行代码表示 primary_entry
函数的结束位置。
通过这段代码,可以看出 primary_entry
函数在启动过程中调用了几个辅助函数,其中包括记录 MMU
状态、保留引导参数和创建 ID
映射。然后根据条件分支和处理器初始化,将控制权切换到内核的 __primary_switch
位置,最终完成内核的初始化和启动。
三、record_mmu_state
SYM_CODE_START_LOCAL(record_mmu_state)
mrs x19, CurrentEL
cmp x19, #CurrentEL_EL2
mrs x19, sctlr_el1
b.ne 0f
mrs x19, sctlr_el2
0:
CPU_LE( tbnz x19, #SCTLR_ELx_EE_SHIFT, 1f )
CPU_BE( tbz x19, #SCTLR_ELx_EE_SHIFT, 1f )
tst x19, #SCTLR_ELx_C // Z := (C == 0)
and x19, x19, #SCTLR_ELx_M // isolate M bit
csel x19, xzr, x19, eq // clear x19 if Z
ret
/*
* Set the correct endianness early so all memory accesses issued
* before init_kernel_el() occur in the correct byte order. Note that
* this means the MMU must be disabled, or the active ID map will end
* up getting interpreted with the wrong byte order.
* 尽早设置正确的字节序,以便在 init_kernel_el() 之前发出的所有内存访问都以正确的字节顺序发生。
* 请注意,这意味着必须禁用 MMU,否则活动 ID 映射最终将以错误的字节顺序进行解释。
*/
1: eor x19, x19, #SCTLR_ELx_EE
bic x19, x19, #SCTLR_ELx_M
b.ne 2f
pre_disable_mmu_workaround
msr sctlr_el2, x19
b 3f
2: pre_disable_mmu_workaround
msr sctlr_el1, x19
3: isb
mov x19, xzr
ret
SYM_CODE_END(record_mmu_state)
这段代码是一个本地符号 record_mmu_state
,该函数的主要目的是记录 MMU
(内存管理单元)的状态。下面是对每行代码的解释:
SYM_CODE_START_LOCAL(record_mmu_state)
这行代码表示这是一个本地符号,用于定义 record_mmu_state
函数的开始位置。
mrs x19, CurrentEL
cmp x19, #CurrentEL_EL2
这两行代码将当前异常级别的值读取到寄存器 x19
中,并与宏 CurrentEL_EL2
进行比较,以检查是否处于 EL2
异常级别。
mrs x19, sctlr_el1
b.ne 0f
mrs x19, sctlr_el2
0:
这几行代码根据上述比较结果,如果不在 EL2
异常级别,则将 sctlr_el1
寄存器的值读取到寄存器 x19
中;否则,跳转到标号 0
处并将 sctlr_el2
寄存器的值读取到寄存器 x19
中。
CPU_LE( tbnz x19, #SCTLR_ELx_EE_SHIFT, 1f )
CPU_BE( tbz x19, #SCTLR_ELx_EE_SHIFT, 1f )
这两行代码根据 CPU
的大小端模式(little-endian
或big-endian
),分别进行条件分支。根据 SCTLR_ELx_EE
寄存器的值,如果满足条件,则跳转到标号 1
处。
tst x19, #SCTLR_ELx_C // Z := (C == 0)
and x19, x19, #SCTLR_ELx_M // isolate M bit
csel x19, xzr, x19, eq // clear x19 if Z
ret
这几行代码进行位操作和条件选择,并最终返回结果。它们根据 SCTLR_ELx_C
寄存器的值执行相应的操作,并将结果存储在寄存器 x19
中,然后通过 ret
指令返回函数。
1: eor x19, x19, #SCTLR_ELx_EE
bic x19, x19, #SCTLR_ELx_M
b.ne 2f
pre_disable_mmu_workaround
msr sctlr_el2, x19
b 3f
2: pre_disable_mmu_workaround
msr sctlr_el1, x19
3: isb
mov x19, xzr
ret
这部分代码根据条件分支进行不同的操作。根据前面的条件比较结果,它们对寄存器 x19
进行位操作和函数调用,并最终返回结果。具体操作包括将 SCTLR_ELx_EE
寄存器的值与 SCTLR_ELx_M
寄存器的值进行异或和清除操作,然后根据条件选择将结果写入 sctlr_el2
或 sctlr_el1
寄存器。最后通过 isb
指令刷新指令流水线,并将寄存器 x19
设置为零,并通过 ret
指令返回函数。
SYM_CODE_END(record_mmu_state)
这行代码表示 record_mmu_state
函数的结束位置。
通过这段代码,可以看出该函数根据当前的异常级别和 CPU 的大小端模式,获取相应的寄存器值,并进行一系列位操作和条件分支来记录 MMU 状态。具体的操作和处理根据不同的条件和配置进行调整,以满足特定的需求和约束。
四、preserve_boot_args
/*
* Preserve the arguments passed by the bootloader in x0 .. x3
*/
SYM_CODE_START_LOCAL(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]
cbnz x19, 0f // skip cache invalidation if MMU is on
dmb sy // needed before dc ivac with
// MMU off
add x1, x0, #0x20 // 4 x 8 bytes
b dcache_inval_poc // tail call
0: str_l x19, mmu_enabled_at_boot, x0
ret
SYM_CODE_END(preserve_boot_args)
这段代码是一个本地符号 preserve_boot_args
,其主要目的是保留引导加载程序传递给内核的参数。下面是对每行代码的解释:
/*
* Preserve the arguments passed by the bootloader in x0 .. x3
* 在 x0 .. x3 中保留引导加载进程传递的参数
*/
此注释指出了该函数的目标,即保留由引导加载程序传递给内核的参数(保存在 x0
到 x3
寄存器中)。
SYM_CODE_START_LOCAL(preserve_boot_args)
这行代码表示这是一个本地符号,用于定义 preserve_boot_args
函数的开始位置。
mov x21, x0 // x21=FDT
将 x0
的值(引导加载程序传递的第一个参数)复制到寄存器 x21
中,以便进一步处理。
adr_l x0, boot_args // record the contents of
stp x21, x1, [x0] // x0 .. x3 at kernel entry
stp x2, x3, [x0, #16]
这几行代码使用 adr_l
指令将 boot_args
地址加载到寄存器 x0
中,并使用 stp
指令将 x21
、x1
、x2
和 x3
的值按顺序保存到 boot_args
地址所指向的内存中。
cbnz x19, 0f // skip cache invalidation if MMU is on
dmb sy // needed before dc ivac with
// MMU off
根据 x19
的值进行条件分支。如果 x19
不为零,则跳转到标号 0
处。在标号 0
处,执行 dmb sy
指令,该指令用于在 MMU
关闭时,在执行 dc ivac
操作之前保证数据的内存一致性。
add x1, x0, #0x20 // 4 x 8 bytes
b dcache_inval_poc // tail call
计算出新的地址(x0 + 0x20
)并保存到寄存器 x1
中,然后使用尾调用方式跳转到 dcache_inval_poc
函数,该函数用于清除缓存中的数据以确保数据的一致性。
0: str_l x19, mmu_enabled_at_boot, x0
ret
在标号 0
处,将 x19
的值保存到内存中的 mmu_enabled_at_boot
地址处,并通过 ret
指令返回函数。
SYM_CODE_END(preserve_boot_args)
这行代码表示 preserve_boot_args
函数的结束位置。
通过这段代码,可以看出该函数的主要作用是保留由引导加载程序传递给内核的参数,并记录这些参数的值。此外,根据 MMU
的状态,还包括对缓存进行清理和一致性操作,以确保数据的正确性和可靠性。
五、create_idmap
SYM_FUNC_START_LOCAL(create_idmap)
mov x28, lr
/*
* The ID map carries a 1:1 mapping of the physical address range
* covered by the loaded image, which could be anywhere in DRAM. This
* means that the required size of the VA (== PA) space is decided at
* boot time, and could be more than the configured size of the VA
* space for ordinary kernel and user space mappings.
* ID 映射带有加载映像所覆盖的物理地址范围的 1:1 映射,该映射可能位于 DRAM 中的任何位置。
* 这意味着所需的 VA (== PA) 空间大小是在引导时确定的,并且可能大于普通内核和用户空间映射的 VA 空间的配置大小。
*
* There are three cases to consider here:
* 这里有三种情况需要考虑:
* - 39 <= VA_BITS < 48, and the ID map needs up to 48 VA bits to cover
* the placement of the image. In this case, we configure one extra
* level of translation on the fly for the ID map only. (This case
* also covers 42-bit VA/52-bit PA on 64k pages).
* - 39 = VA BITS 48, ID映射需要多达48个VA BITS来覆盖图像的位置。
* 在本例中,我们仅为ID映射动态地配置了一个额外的转换级别。(本例还包括64k页上的42位va52位PA)。
*
* - VA_BITS == 48, and the ID map needs more than 48 VA bits. This can
* only happen when using 64k pages, in which case we need to extend
* the root level table rather than add a level. Note that we can
* treat this case as 'always extended' as long as we take care not
* to program an unsupported T0SZ value into the TCR register.
* - VA BITS == 48, ID映射需要大于48个VA位。这只有在使用64k页时才会发生,
* 在这种情况下,我们需要扩展根级表,而不是添加一个级别。
* 请注意,只要我们注意不将不支持的T0SZ值编程到TCR寄存器中,我们就可以将这种情况视为“始终扩展”。
*
* - Combinations that would require two additional levels of
* translation are not supported, e.g., VA_BITS==36 on 16k pages, or
* VA_BITS==39/4k pages with 5-level paging, where the input address
* requires more than 47 or 48 bits, respectively.
* - 不支持需要两个额外转换级别的组合,
* 例如,16k 页上的 VA_BITS==36,或 5 级分页的 VA_BITS==39/4k 页面,其中输入地址分别需要超过 47 位或 48 位。
*/
#if (VA_BITS < 48)
#define IDMAP_PGD_ORDER (VA_BITS - PGDIR_SHIFT)
#define EXTRA_SHIFT (PGDIR_SHIFT + PAGE_SHIFT - 3)
/*
* If VA_BITS < 48, we have to configure an additional table level.
* First, we have to verify our assumption that the current value of
* VA_BITS was chosen such that all translation levels are fully
* utilised, and that lowering T0SZ will always result in an additional
* translation level to be configured.
* 如果VA_BITS < 48,我们必须配置一个额外的表级别。
* 首先,我们必须验证我们的假设,即选择VA_BITS的当前值,以便充分利用所有翻译级别,
* 并且降低 T0SZ 将始终导致要配置额外的转换级别。
*/
#if VA_BITS != EXTRA_SHIFT
#error "Mismatch between VA_BITS and page size/number of translation levels"
#endif
#else
#define IDMAP_PGD_ORDER (PHYS_MASK_SHIFT - PGDIR_SHIFT)
#define EXTRA_SHIFT
/*
* If VA_BITS == 48, we don't have to configure an additional
* translation level, but the top-level table has more entries.
* 如果VA BITS == 48,我们不需要配置额外的转换级别,但是顶级表有更多的条目。
*/
#endif
adrp x0, init_idmap_pg_dir
adrp x3, _text
adrp x6, _end + MAX_FDT_SIZE + SWAPPER_BLOCK_SIZE
mov x7, SWAPPER_RX_MMUFLAGS
map_memory x0, x1, x3, x6, x7, x3, IDMAP_PGD_ORDER, x10, x11, x12, x13, x14, EXTRA_SHIFT
/* Remap the kernel page tables r/w in the ID map */
/* 在ID映射中重新映射内核页表 */
adrp x1, _text
adrp x2, init_pg_dir
adrp x3, init_pg_end
bic x4, x2, #SWAPPER_BLOCK_SIZE - 1
mov x5, SWAPPER_RW_MMUFLAGS
mov x6, #SWAPPER_BLOCK_SHIFT
bl remap_region
/* Remap the FDT after the kernel image */
/* 在内核映像之后重新映射FDT */
adrp x1, _text
adrp x22, _end + SWAPPER_BLOCK_SIZE
bic x2, x22, #SWAPPER_BLOCK_SIZE - 1
bfi x22, x21, #0, #SWAPPER_BLOCK_SHIFT // remapped FDT address 重映射FDT地址
add x3, x2, #MAX_FDT_SIZE + SWAPPER_BLOCK_SIZE
bic x4, x21, #SWAPPER_BLOCK_SIZE - 1
mov x5, SWAPPER_RW_MMUFLAGS
mov x6, #SWAPPER_BLOCK_SHIFT
bl remap_region
/*
* Since the page tables have been populated with non-cacheable
* accesses (MMU disabled), invalidate those tables again to
* remove any speculatively loaded cache lines.
* 由于页表已填充了不可缓存的访问(已禁用 MMU),因此请再次使这些表失效以删除任何推测加载的缓存行
*/
cbnz x19, 0f // skip cache invalidation if MMU is on 如果MMU打开,则跳过缓存无效
dmb sy
adrp x0, init_idmap_pg_dir
adrp x1, init_idmap_pg_end
bl dcache_inval_poc
0: ret x28
SYM_FUNC_END(create_idmap)
这段代码是用于在Linux内核中创建ID映射表的函数create_idmap
。以下对每行代码进行解释:
SYM_FUNC_START_LOCAL(create_idmap)
mov x28, lr
这两行代码定义了一个本地符号和保存链接寄存器lr
(返回地址)到寄存器x28
。
#if (VA_BITS < 48)
这行代码是一个条件编译指令,判断虚拟地址位数VA_BITS
是否小于48。
#define IDMAP_PGD_ORDER (VA_BITS - PGDIR_SHIFT)
#define EXTRA_SHIFT (PGDIR_SHIFT + PAGE_SHIFT - 3)
这两行代码根据条件编译的结果定义了宏,计算了ID映射表的页目录级别和额外位移。
adrp x0, init_idmap_pg_dir
adrp x3, _text
adrp x6, _end + MAX_FDT_SIZE + SWAPPER_BLOCK_SIZE
mov x7, SWAPPER_RX_MMUFLAGS
map_memory x0, x1, x3, x6, x7, x3, IDMAP_PGD_ORDER, x10, x11, x12, x13, x14, EXTRA_SHIFT
这几行代码使用adrp
和mov
指令加载一些变量的地址或值,并调用了map_memory
函数,将这些地址按照一定的映射关系进行映射。
adrp x1, _text
adrp x2, init_pg_dir
adrp x3, init_pg_end
bic x4, x2, #SWAPPER_BLOCK_SIZE - 1
mov x5, SWAPPER_RW_MMUFLAGS
mov x6, #SWAPPER_BLOCK_SHIFT
bl remap_region
这几行代码同样使用adrp
和mov
指令加载一些变量的地址或值,并调用了remap_region
函数,重新映射内核页表。
adrp x1, _text
adrp x22, _end + SWAPPER_BLOCK_SIZE
bic x2, x22, #SWAPPER_BLOCK_SIZE - 1
bfi x22, x21, #0, #SWAPPER_BLOCK_SHIFT // remapped FDT address
add x3, x2, #MAX_FDT_SIZE + SWAPPER_BLOCK_SIZE
bic x4, x21, #SWAPPER_BLOCK_SIZE - 1
mov x5, SWAPPER_RW_MMUFLAGS
mov x6, #SWAPPER_BLOCK_SHIFT
bl remap_region
这几行代码同样使用adrp
和mov
指令加载一些变量的地址或值,并调用了remap_region
函数,重新映射文件描述符表(FDT)。
cbnz x19, 0f // skip cache invalidation if MMU is on
dmb sy
adrp x0, init_idmap_pg_dir
adrp x1, init_idmap_pg_end
bl dcache_inval_poc
0: ret x28
SYM_FUNC_END(create_idmap)
这几行代码用于在 MMU
开启时执行缓存失效操作,在 MMU
关闭时直接跳过。然后,通过bl
指令调用了dcache_inval_poc
函数,进一步执行缓存失效操作。最后,通过ret
指令返回之前保存的链接寄存器lr
中的值。
综上所述,这段代码的主要功能是创建 ID
映射表,并对一些关键内存区域进行重新映射和缓存失效操作。
六、init_kernel_el
/*
* Starting from EL2 or EL1, configure the CPU to execute at the highest
* reachable EL supported by the kernel in a chosen default state. If dropping
* from EL2 to EL1, configure EL2 before configuring EL1.
* 从EL2或EL1开始,将CPU配置为在所选的默认状态下以内核支持的最高可达EL执行。
* 如果从EL2降为EL1,请先配置EL2,再配置EL1。
*
* Since we cannot always rely on ERET synchronizing writes to sysregs (e.g. if
* SCTLR_ELx.EOS is clear), we place an ISB prior to ERET.
* 由于我们不能总是依赖 ERET 同步写入系统(例如,如果 SCTLR_ELx.EOS 是明确的),我们将 ISB 放在 ERET 之前。
*
* Returns either BOOT_CPU_MODE_EL1 or BOOT_CPU_MODE_EL2 in x0 if
* booted in EL1 or EL2 respectively, with the top 32 bits containing
* potential context flags. These flags are *not* stored in __boot_cpu_mode.
* 如果分别在 EL1 或 EL2 中引导,则在 x0 中返回 BOOT_CPU_MODE_EL1 或 BOOT_CPU_MODE_EL2,
* 前 32 位包含潜在的上下文标志。这些标志不存储在__boot_cpu_mode中。
*
* x0: whether we are being called from the primary boot path with the MMU on
* x0: 在MMU打开的情况下,我们是否从主引导路径被调用
*/
SYM_FUNC_START(init_kernel_el)
mrs x1, CurrentEL
cmp x1, #CurrentEL_EL2
b.eq init_el2
SYM_INNER_LABEL(init_el1, SYM_L_LOCAL)
mov_q x0, INIT_SCTLR_EL1_MMU_OFF
pre_disable_mmu_workaround
msr sctlr_el1, x0
isb
mov_q x0, INIT_PSTATE_EL1
msr spsr_el1, x0
msr elr_el1, lr
mov w0, #BOOT_CPU_MODE_EL1
eret
SYM_INNER_LABEL(init_el2, SYM_L_LOCAL)
msr elr_el2, lr
// clean all HYP code to the PoC if we booted at EL2 with the MMU on
cbz x0, 0f
adrp x0, __hyp_idmap_text_start
adr_l x1, __hyp_text_end
adr_l x2, dcache_clean_poc
blr x2
0:
mov_q x0, HCR_HOST_NVHE_FLAGS
msr hcr_el2, x0
isb
init_el2_state
/* Hypervisor stub */
adr_l x0, __hyp_stub_vectors
msr vbar_el2, x0
isb
mov_q x1, INIT_SCTLR_EL1_MMU_OFF
/*
* Fruity CPUs seem to have HCR_EL2.E2H set to RES1,
* making it impossible to start in nVHE mode. Is that
* compliant with the architecture? Absolutely not!
*/
mrs x0, hcr_el2
and x0, x0, #HCR_E2H
cbz x0, 1f
/* Set a sane SCTLR_EL1, the VHE way */
pre_disable_mmu_workaround
msr_s SYS_SCTLR_EL12, x1
mov x2, #BOOT_CPU_FLAG_E2H
b 2f
1:
pre_disable_mmu_workaround
msr sctlr_el1, x1
mov x2, xzr
2:
mov w0, #BOOT_CPU_MODE_EL2
orr x0, x0, x2
eret
SYM_FUNC_END(init_kernel_el)
这段代码是用于初始化内核执行级别(EL)的函数init_kernel_el
。以下对每行代码进行解释:
SYM_FUNC_START(init_kernel_el)
mrs x1, CurrentEL
cmp x1, #CurrentEL_EL2
b.eq init_el2
这几行代码获取当前的执行级别(EL)到寄存器x1
,并与常量CurrentEL_EL2
比较。如果相等,则跳转到标签init_el2
处理 EL2
的初始化。
SYM_INNER_LABEL(init_el1, SYM_L_LOCAL)
mov_q x0, INIT_SCTLR_EL1_MMU_OFF
pre_disable_mmu_workaround
msr sctlr_el1, x0
isb
mov_q x0, INIT_PSTATE_EL1
msr spsr_el1, x0
msr elr_el1, lr
mov w0, #BOOT_CPU_MODE_EL1
eret
这几行代码定义了一个内部标签init_el1
,将寄存器 x0
设置为一个控制 EL1
配置的值,禁用 MMU
前的工作,使用msr
指令将x0
的值写入sctlr_el1
系统寄存器,使用isb
指令同步指令流水线,将寄存器x0
设置为一个控制程序状态的值,使用msr
指令将x0
的值写入spsr_el1
系统寄存器,使用msr
指令将链接寄存器(返回地址)的值写入elr_el1
系统寄存器,将寄存器w0
设置为常量BOOT_CPU_MODE_EL1
,使用eret
指令从异常状态退出,并将处理器切换到 EL1
级别。
SYM_INNER_LABEL(init_el2, SYM_L_LOCAL)
msr elr_el2, lr
// clean all HYP code to the PoC if we booted at EL2 with the MMU on
cbz x0, 0f
adrp x0, __hyp_idmap_text_start
adr_l x1, __hyp_text_end
adr_l x2, dcache_clean_poc
blr x2
0:
mov_q x0, HCR_HOST_NVHE_FLAGS
msr hcr_el2, x0
isb
init_el2_state
/* Hypervisor stub */
adr_l x0, __hyp_stub_vectors
msr vbar_el2, x0
isb
mov_q x1, INIT_SCTLR_EL1_MMU_OFF
/*
* Fruity CPUs seem to have HCR_EL2.E2H set to RES1,
* making it impossible to start in nVHE mode. Is that
* compliant with the architecture? Absolutely not!
*/
mrs x0, hcr_el2
and x0, x0, #HCR_E2H
cbz x0, 1f
/* Set a sane SCTLR_EL1, the VHE way */
pre_disable_mmu_workaround
msr_s SYS_SCTLR_EL12, x1
mov x2, #BOOT_CPU_FLAG_E2H
b 2f
1:
pre_disable_mmu_workaround
msr sctlr_el1, x1
mov x2, xzr
2:
mov w0, #BOOT_CPU_MODE_EL2
orr x0, x0, x2
eret
SYM_FUNC_END(init_kernel_el)
这段代码定义了一个内部标签init_el2
,使用msr
指令将链接寄存器(返回地址)的值写入elr_el2
系统寄存器。
然后,根据传入的参数判断是否为 EL2
级别,如果是,则执行一些清理操作并配置 EL2
的状态,将寄存器x0
设置为常量HCR_HOST_NVHE_FLAGS
的值,并使用msr
指令将x0
的值写入hcr_el2
系统寄存器,使用isb
指令同步指令流水线。接着,通过调用init_el2_state
函数初始化 EL2
的状态,设置了 Hypervisor stub
的异常向量表,将寄存器x1
设置为一个控制 EL1
配置的值。
接下来,代码检查了处理器的配置,如果发现处理器不支持在 VHE
模式下启动(具体通过HCR_EL2.E2H
寄存器判断),则使用非 VHE
方式设置合理的EL1配置。
最后,使用pre_disable_mmu_workaround
函数禁用 MMU
前的工作,将寄存器w0
设置为常量BOOT_CPU_MODE_EL2
,并使用orr
指令与寄存器x2
进行或操作,将标志位信息添加到返回值中。最后,使用eret
指令从异常状态退出,并将处理器切换到 EL2
级别。
综上所述,这段代码的主要功能是根据当前的执行级别(EL)和参数设置内核执行的EL级别,并进行相应的初始化操作。
七、__cpu_setup
/*
* __cpu_setup
*
* Initialise the processor for turning the MMU on.
* 初始化处理器以启动MMU。
*
* Input:
* x0 - actual number of VA bits (ignored unless VA_BITS > 48)
* Output:
* Return in x0 the value of the SCTLR_EL1 register.
*/
.pushsection ".idmap.text", "a"
SYM_FUNC_START(__cpu_setup)
tlbi vmalle1 // Invalidate local TLB
dsb nsh
mov x1, #3 << 20
msr cpacr_el1, x1 // Enable FP/ASIMD
mov x1, #1 << 12 // Reset mdscr_el1 and disable
msr mdscr_el1, x1 // access to the DCC from EL0
isb // Unmask debug exceptions now,
enable_dbg // since this is per-cpu
reset_pmuserenr_el0 x1 // Disable PMU access from EL0
reset_amuserenr_el0 x1 // Disable AMU access from EL0
/*
* Default values for VMSA control registers. These will be adjusted
* below depending on detected CPU features.
* VMSA控制寄存器的默认值。这些将根据检测到的CPU特性在下面进行调整。
*/
mair .req x17
tcr .req x16
mov_q mair, MAIR_EL1_SET
mov_q tcr, TCR_TxSZ(VA_BITS) | TCR_CACHE_FLAGS | TCR_SMP_FLAGS | \
TCR_TG_FLAGS | TCR_KASLR_FLAGS | TCR_ASID16 | \
TCR_TBI0 | TCR_A1 | TCR_KASAN_SW_FLAGS | TCR_MTE_FLAGS
tcr_clear_errata_bits tcr, x9, x5
#ifdef CONFIG_ARM64_VA_BITS_52
sub x9, xzr, x0
add x9, x9, #64
tcr_set_t1sz tcr, x9
#else
idmap_get_t0sz x9
#endif
tcr_set_t0sz tcr, x9
/*
* Set the IPS bits in TCR_EL1.
* 设置TCR EL1的IPS位。
*/
tcr_compute_pa_size tcr, #TCR_IPS_SHIFT, x5, x6
#ifdef CONFIG_ARM64_HW_AFDBM
/*
* Enable hardware update of the Access Flags bit.
* Hardware dirty bit management is enabled later,
* via capabilities.
* 启用Access Flags位的硬件更新。硬件脏位管理稍后通过功能启用。
*/
mrs x9, ID_AA64MMFR1_EL1
and x9, x9, #0xf
cbz x9, 1f
orr tcr, tcr, #TCR_HA // hardware Access flag update 硬件访问标志更新
1:
#endif /* CONFIG_ARM64_HW_AFDBM */
msr mair_el1, mair
msr tcr_el1, tcr
/*
* Prepare SCTLR
*/
mov_q x0, INIT_SCTLR_EL1_MMU_ON
ret // return to head.S
.unreq mair
.unreq tcr
SYM_FUNC_END(__cpu_setup)
这段代码是一个名为__cpu_setup
的函数,用于初始化处理器以开启MMU功能。以下对每行代码进行解释:
.pushsection ".idmap.text", "a"
SYM_FUNC_START(__cpu_setup)
这两行代码将函数放置在.idmap.text
节中,并开始__cpu_setup
函数。
tlbi vmalle1 // Invalidate local TLB
dsb nsh
这两行代码执行TLBI指令来使本地TLB无效,并使用DSB指令确保操作执行顺序正确。
mov x1, #3 << 20
msr cpacr_el1, x1 // Enable FP/ASIMD
这两行代码将寄存器x1
设置为控制协处理器访问权限的值,并使用msr
指令将其写入cpacr_el1
系统寄存器,以启用浮点和SIMD
指令功能。
mov x1, #1 << 12 // Reset mdscr_el1 and disable
msr mdscr_el1, x1 // access to the DCC from EL0
isb // Unmask debug exceptions now,
enable_dbg // since this is per-cpu
这些代码将寄存器x1
设置为控制调试器及其相关功能的mdscr_el1
寄存器的值,并使用msr
指令将其写入mdscr_el1
系统寄存器以重置相关配置。然后,使用isb
指令同步指令流水线,通过enable_dbg
函数启用调试异常。
reset_pmuserenr_el0 x1 // Disable PMU access from EL0
reset_amuserenr_el0 x1 // Disable AMU access from EL0
这两行代码使用自定义的宏函数禁用来自EL0
级别的性能计数器和监视器的访问。
mair .req x17
tcr .req x16
mov_q mair, MAIR_EL1_SET
mov_q tcr, TCR_TxSZ(VA_BITS) | TCR_CACHE_FLAGS | TCR_SMP_FLAGS | \
TCR_TG_FLAGS | TCR_KASLR_FLAGS | TCR_ASID16 | \
TCR_TBI0 | TCR_A1 | TCR_KASAN_SW_FLAGS | TCR_MTE_FLAGS
这几行代码定义了一些寄存器和常量,其中包括mair
、tcr
以及一系列用于构建tcr_el1
寄存器值的掩码常量。
tcr_clear_errata_bits tcr, x9, x5
这行代码使用一个自定义的宏函数清除TCR_EL1
寄存器中已知的硬件错误的位。
#ifdef CONFIG_ARM64_VA_BITS_52
sub x9, xzr, x0
add x9, x9, #64
tcr_set_t1sz tcr, x9
#else
idmap_get_t0sz x9
#endif
tcr_set_t0sz tcr, x9
这些代码根据配置选项判断是否使用52位的虚拟地址空间。如果是,则将寄存器x9
设置为一个控制TCR_EL1
寄存器中T1SZ
字段的值;否则,调用自定义的函数获取t0sz
的值,并将其写入tcr_el1
寄存器的相应字段(T0SZ)。
tcr_compute_pa_size tcr, #TCR_IPS_SHIFT, x5, x6
#ifdef CONFIG_ARM64_HW_AFDBM
mrs x9, ID_AA64MMFR1_EL1
and x9, x9, #0xf
cbz x9, 1f
orr tcr, tcr, #TCR_HA // hardware Access flag update
1:
#endif /* CONFIG_ARM64_HW_AFDBM */
msr mair_el1, mair
msr tcr_el1, tcr
这些代码使用自定义的宏函数计算物理地址大小,并将计算结果写入tcr_el1
寄存器的相应字段(IPS
)。
如果启用了ARM64
硬件更新访问标志位(AFDBM
)功能,这部分代码会读取ID_AA64MMFR1_EL1
系统寄存器的值,然后根据配置选项进行处理,并将得到的掩码值与tcr
寄存器进行逻辑或操作,以使硬件能够更新Access Flags
位。最后,使用msr
指令将mair
和tcr
的值分别写入mair_el1
和tcr_el1
系统寄存器。
mov_q x0, INIT_SCTLR_EL1_MMU_ON
ret // return to head.S
.unreq mair
.unreq tcr
SYM_FUNC_END(__cpu_setup)
这些代码将寄存器x0
设置为一个常量值INIT_SCTLR_EL1_MMU_ON
,该值将作为返回值存储在x0
寄存器中,并通过ret
指令返回到汇编语言代码的起点。
总结而言,这段代码是用于初始化处理器并开启MMU
功能的函数。它包括**TLB
的使能**、协处理器权限的设置、调试器相关功能的配置、性能计数器和监视器的禁用、VMSA
控制寄存器的设置、虚拟地址空间大小的计算以及最终的SCTLR_EL1
寄存器的初始化。
八、__primary_switch
SYM_FUNC_START_LOCAL(__primary_switch)
adrp x1, reserved_pg_dir
adrp x2, init_idmap_pg_dir
bl __enable_mmu
#ifdef CONFIG_RELOCATABLE
adrp x23, KERNEL_START
and x23, x23, MIN_KIMG_ALIGN - 1
#ifdef CONFIG_RANDOMIZE_BASE
mov x0, x22
adrp x1, init_pg_end
mov sp, x1
mov x29, xzr
bl __pi_kaslr_early_init
and x24, x0, #SZ_2M - 1 // capture memstart offset seed 捕获memstart偏移种子
bic x0, x0, #SZ_2M - 1
orr x23, x23, x0 // record kernel offset 记录内核偏移量
#endif
#endif
bl clear_page_tables
bl create_kernel_mapping
adrp x1, init_pg_dir
load_ttbr1 x1, x1, x2
#ifdef CONFIG_RELOCATABLE
bl __relocate_kernel
#endif
ldr x8, =__primary_switched
adrp x0, KERNEL_START // __pa(KERNEL_START)
br x8
SYM_FUNC_END(__primary_switch)
这段代码是一个名为__primary_switch
的本地函数(SYM_FUNC_START_LOCAL
表示本地函数),以下对每行代码进行解释:
SYM_FUNC_START_LOCAL(__primary_switch)
这行代码开始了__primary_switch
函数。
adrp x1, reserved_pg_dir
adrp x2, init_idmap_pg_dir
bl __enable_mmu
这几行代码使用adrp
指令将reserved_pg_dir
和init_idmap_pg_dir
的地址分别加载到寄存器x1
和x2
中,然后调用__enable_mmu
函数启用MMU功能。
#ifdef CONFIG_RELOCATABLE
adrp x23, KERNEL_START
and x23, x23, MIN_KIMG_ALIGN - 1
#ifdef CONFIG_RANDOMIZE_BASE
mov x0, x22
adrp x1, init_pg_end
mov sp, x1
mov x29, xzr
bl __pi_kaslr_early_init
and x24, x0, #SZ_2M - 1 // capture memstart offset seed
bic x0, x0, #SZ_2M - 1
orr x23, x23, x0 // record kernel offset
#endif
#endif
这部分代码是根据配置选项进行条件编译的。如果启用了可重定位内核(CONFIG_RELOCATABLE
),则会执行相应的操作。首先,使用adrp
指令将KERNEL_START
的地址加载到寄存器x23
中,并使用and
指令和掩码MIN_KIMG_ALIGN - 1
对其进行位与运算。如果启用了随机基址(CONFIG_RANDOMIZE_BASE
),则将x22
的值移动到x0
寄存器,并将init_pg_end
的地址加载到x1
寄存器中。然后,将栈指针sp
设置为x1
寄存器的值,将x29
寄存器(帧指针)清零,并调用__pi_kaslr_early_init
函数执行早期地址空间布局随机化(KASLR)的初始化操作。接下来,使用and
指令和掩码SZ_2M - 1
对x0
进行位与运算,并将结果保存在x24
寄存器中,以记录起始内存偏移种子。再次使用bic
指令将x0
与掩码SZ_2M - 1
进行位清除操作。最后,使用orr
指令将x0
与x23
进行位或运算,将内核偏移量记录在x23
寄存器中。
bl clear_page_tables
bl create_kernel_mapping
这两行代码分别调用clear_page_tables
和create_kernel_mapping
函数。clear_page_tables
函数用于清除页表,而create_kernel_mapping
函数用于创建内核映射。
adrp x1, init_pg_dir
load_ttbr1 x1, x1, x2
这几行代码使用adrp
指令将init_pg_dir
的地址加载到寄存器x1
中,然后调用load_ttbr1
宏函数,将x1
、x1
和x2
作为参数,用于加载TTBR1
(Translation Table Base Register 1
)寄存器。
#ifdef CONFIG_RELOCATABLE
bl __relocate_kernel
#endif
这部分代码也是根据配置选项进行条件编译的。如果启用了可重定位内核(CONFIG_RELOCATABLE),则会调用__relocate_kernel
函数,该函数用于重新定位内核。
ldr x8, =__primary_switched
adrp x0, KERNEL_START // __pa(KERNEL_START)
br x8
这几行代码使用ldr
指令将__primary_switched
的地址加载到寄存器x8
中,然后使用adrp
指令将KERNEL_START
的地址加载到寄存器x0
中(此处的__pa(KERNEL_START)
表示获取KERNEL_START
符号的物理地址)。最后,使用br
指令跳转到x8
寄存器的值所指示的地址。
总结而言,这段代码是一个处理器切换函数__primary_switch
,其中包括启用MMU功能、初始化内核地址空间布局随机化、清除页表、创建内核映射、加载TTBR1寄存器以及可选的重新定位内核等操作。最后,通过跳转到__primary_switched
函数来完成处理器切换。
1、__primary_switched
/*
* The following fragment of code is executed with the MMU enabled.
* 下面的代码片段在启用MMU的情况下执行。
*
* x0 = __pa(KERNEL_START)
*/
SYM_FUNC_START_LOCAL(__primary_switched)
adr_l x4, init_task
init_cpu_task x4, x5, x6
adr_l x8, vectors // load VBAR_EL1 with virtual
msr vbar_el1, x8 // vector table address
isb
stp x29, 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
mov x0, x20
bl set_cpu_boot_mode_flag
// 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
#if VA_BITS > 48
adr_l x8, vabits_actual // Set this early so KASAN early init
str x25, [x8] // ... observes the correct value
dc civac, x8 // Make visible to booting secondaries
#endif
#ifdef CONFIG_RANDOMIZE_BASE
adrp x5, memstart_offset_seed // Save KASLR linear map seed
strh w24, [x5, :lo12:memstart_offset_seed]
#endif
#if defined(CONFIG_KASAN_GENERIC) || defined(CONFIG_KASAN_SW_TAGS)
bl kasan_early_init
#endif
mov x0, x21 // pass FDT address in x0
bl early_fdt_map // Try mapping the FDT early
mov x0, x20 // pass the full boot status
bl init_feature_override // Parse cpu feature overrides
#ifdef CONFIG_UNWIND_PATCH_PAC_INTO_SCS
bl scs_patch_vmlinux
#endif
mov x0, x20
bl finalise_el2 // Prefer VHE if possible
ldp x29, x30, [sp], #16
bl start_kernel
ASM_BUG()
SYM_FUNC_END(__primary_switched)
这段代码是在启用MMU的情况下执行的。以下对每行代码进行解释:
SYM_FUNC_START_LOCAL(__primary_switched)
这行代码开始了名为__primary_switched
的本地函数。
adr_l x4, init_task
init_cpu_task x4, x5, x6
这两行代码使用adr_l
指令将init_task
的地址加载到寄存器x4
中,然后调用init_cpu_task
宏将x4
、x5
和x6
作为参数初始化CPU
任务。
adr_l x8, vectors
msr vbar_el1, x8
isb
这几行代码使用adr_l
指令将vectors
的地址加载到寄存器x8
中,然后使用msr
指令将x8
的值写入VBAR_EL1
寄存器,即异常向量表地址寄存器。最后,使用isb
指令刷新指令流水线。
stp x29, x30, [sp, #-16]!
mov x29, sp
这几行代码将帧指针寄存器x29
和链接寄存器x30
保存在栈上,然后将栈指针寄存器sp
的值赋给x29
,建立新的帧。
str_l x21, __fdt_pointer, x5
这行代码使用str_l
指令将寄存器x21
中的值存储到__fdt_pointer
变量的地址中。
ldr_l x4, kimage_vaddr
sub x4, x4, x0
str_l x4, kimage_voffset, x5
这几行代码使用ldr_l
指令将kimage_vaddr
的值加载到寄存器x4
中,然后使用sub
指令计算内核虚拟和物理地址之间的偏移量,并将结果存储在kimage_voffset
变量中。
mov x0, x20
bl set_cpu_boot_mode_flag
这两行代码将寄存器x20
的值移动到x0
寄存器中,并调用set_cpu_boot_mode_flag
函数设置CPU
引导模式标志。
adr_l x0, __bss_start
mov x1, xzr
adr_l x2, __bss_stop
sub x2, x2, x0
bl __pi_memset
dsb ishst
这几行代码使用adr_l
指令将__bss_start
和__bss_stop
的地址加载到寄存器x0
和x2
中,然后使用sub
指令计算BSS
段的大小,并将结果存储在x2
中。接着,调用__pi_memset
函数将BSS
段清零,并使用dsb
指令确保对BSS
段的修改对Page Table Walkers(PTW)
可见。
#if VA_BITS > 48
adr_l x8, vabits_actual
str x25, [x8]
dc civac, x8
#endif
这部分代码是根据虚拟地址位数进行条件编译的。如果虚拟地址位数超过48
位,则使用adr_l
指令将vabits_actual
的地址加载到寄存器x8
中,然后将寄存器x25
的值存储到x8
所指示的地址中,并使用dc
指令刷新数据缓存。
#ifdef CONFIG_RANDOMIZE_BASE
adrp x5, memstart_offset_seed
strh w24, [x5, :lo12:memstart_offset_seed]
#endif
这部分代码是根据配置选项进行条件编译的。如果启用了随机基址(CONFIG_RANDOMIZE_BASE
),则会使用adrp
指令将memstart_offset_seed
的地址加载到寄存器x5
中,然后使用strh
指令将w24
的低12位存储到x5
所指示的地址中。
#if defined(CONFIG_KASAN_GENERIC) || defined(CONFIG_KASAN_SW_TAGS)
bl kasan_early_init
#endif
这部分代码也是根据配置选项进行条件编译的。如果启用了KASAN
(内核地址检测和调试工具)的通用模式(CONFIG_KASAN_GENERIC
)或软件标记模式(CONFIG_KASAN_SW_TAG
S),则会调用kasan_early_init
函数执行早期的**KASAN
初始化操作**。
mov x0, x21
bl early_fdt_map
这两行代码将寄存器x21
的值移动到x0
寄存器中,并调用early_fdt_map
函数尝试在早期映射FDT
(设备树)。
mov x0, x20
bl init_feature_override
这两行代码将寄存器x20
的值移动到x0
寄存器中,并调用init_feature_override
函数解析CPU
特性的覆盖设置。
#ifdef CONFIG_UNWIND_PATCH_PAC_INTO_SCS
bl scs_patch_vmlinux
#endif
这部分代码也是根据配置选项进行条件编译的。如果启用了将PAC
修补应用到SCS(System Control Space
)的功能(CONFIG_UNWIND_PATCH_PAC_INTO_SCS
),则会调用scs_patch_vmlinux
函数进行相应的操作。
mov x0, x20
bl finalise_el2
这两行代码将寄存器x20
的值移动到x0
寄存器中,并调用finalise_el2
函数,用于在可能的情况下首选VHE(Virtualization Host Extensions)。
ldp x29, x30, [sp], #16
bl start_kernel
这两行代码从栈上恢复帧指针寄存器x29
和链接寄存器x30
的值,然后调用start_kernel
函数启动内核。
ASM_BUG()
SYM_FUNC_END(__primary_switched)
这两行代码使用ASM_BUG()
宏来标记一个汇编错误,并结束__primary_switched
函数的定义。
总结而言,这段代码是在启用MMU的情况下执行的处理器切换函数__primary_switched
,其中包括初始化CPU任务、设置异常向量表地址、保存FDT指针、计算内核虚拟和物理地址偏移、清零BSS段、初始化KASAN、映射FDT、解析CPU特性覆盖设置等操作。最后,恢复寄存器并调用start_kernel
函数启动内核。
九、总结
1、通过 __HEAD
将启动代码链接到文件开始位置。
2、调用 primary_entr
y ,进行初始化相关操作。
3、调用 __primary_switch - > __primary_switched
调用 start_kernel
函数。文章来源:https://www.toymoban.com/news/detail-618297.html
4、start_kernel
函数进入 C
语言环境初始化内核。文章来源地址https://www.toymoban.com/news/detail-618297.html
到了这里,关于基于aarch64分析kernel源码 三:启动代码分析的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!