ATF学习 - BL1

今天开始学习ARM Trusted Firmware相关的知识。参考https://blog.csdn.net/puyoupuyou/article/details/85046951

atf的下载地址是https://github.com/ARM-software/arm-trusted-firmware,里面有一些介绍。今天的目的是学习其中BL1部分的代码,使用树莓派3b作为参考板,很多解释都会根据树莓派3b的硬件特性来做说明。下面先放一张rpi3上的内存layout,首先需要说明的是,rpi3上没有安全内存和安全外设,因此atf在设计上只是对整块物理内存做了一个逻辑上的划分来模拟安全内存的存在,其实根本没有安全性,所有内存空间的属性是相同的。atf在rpi3平台上的部署如下。

0x00000000 +-----------------+
           |       ROM       | BL1
0x00020000 +-----------------+
           |       FIP       |
0x00200000 +-----------------+
           |                 |
           |       ...       |
           |                 |
0x01000000 +-----------------+
           |       DTB       | (Loaded by the VideoCore)
           +-----------------+
           |                 |
           |       ...       |
           |                 |
0x02000000 +-----------------+
           |     Kernel      | (Loaded by the VideoCore)
           +-----------------+
           |                 |
           |       ...       |
           |                 |
0x10000000 +-----------------+
           |   Secure SRAM   | BL2, BL31
0x10100000 +-----------------+
           |   Secure DRAM   | BL32 (Secure payload)
0x11000000 +-----------------+
           | Non-secure DRAM | BL33
           +-----------------+
           |                 |
           |       ...       |
           |                 |
0x3F000000 +-----------------+
           |       I/O       |
0x40000000 +-----------------+

BL1代码的入口地址为bl1_entrypoint


#include <arch.h>
#include <el3_common_macros.S>

	.globl	bl1_entrypoint


func bl1_entrypoint
    /*如果reset地址不是programmable的,所有的cpu都会从address=0的地方进行启动
     *rpi3是不programmable的
     *如果只有一个cpu会在cold boot中启动,那么后面就不用区分主从cpu了,这个需要平台保证
     *rpi3是不保证的,因此后面会看到需要区分主从cpu
     */

	el3_entrypoint_common					\
		_init_sctlr=1					\
		_warm_boot_mailbox=!PROGRAMMABLE_RESET_ADDRESS	\
		_secondary_cold_boot=!COLD_BOOT_SINGLE_CPU	\
		_init_memory=1					\
		_init_c_runtime=1				\
		_exception_vectors=bl1_exceptions

	bl	bl1_setup

#if ENABLE_PAUTH
...
#endif /* ENABLE_PAUTH */

	bl	bl1_main

#if ENABLE_PAUTH
...
#endif /* ENABLE_PAUTH */

	b	el3_exit
endfunc bl1_entrypoint

其中调用的el3_entrypoint_common是关键

	.macro el3_entrypoint_common					\
		_init_sctlr, _warm_boot_mailbox, _secondary_cold_boot,	\
		_init_memory, _init_c_runtime, _exception_vectors

	.if \_init_sctlr
		
		mov_imm	x0, (SCTLR_RESET_VAL & ~(SCTLR_EE_BIT | SCTLR_WXN_BIT \
				| SCTLR_SA_BIT | SCTLR_A_BIT | SCTLR_DSSBS_BIT))
		msr	sctlr_el3, x0
		isb
	.endif /* _init_sctlr */

	.if \_warm_boot_mailbox
		bl	plat_get_my_entrypoint /*rpi3没有热启动,因此这里回复的都是0*/
		cbz	x0, do_cold_boot
		br	x0

	do_cold_boot:
	.endif /* _warm_boot_mailbox */


	adr	x0, \_exception_vectors
	msr	vbar_el3, x0 /*vbar=vector base address register,保存异常向量表基地址*/
	isb


    /*不是重启,这里是给具体的平台一个重新设定寄存器参数的机会,行为视平台而定,
     *rpi3在这里设定了19.2 MHz的时钟为architected timer,并且设定了SMP bit
     */
	bl	reset_handler

	el3_arch_init_common /*设定我们关心的EL3级的寄存器,宏定义见下面*/

	.if \_secondary_cold_boot
		bl	plat_is_my_cpu_primary /*根据cpu id判断是否是主cpu*/
		cbnz	w0, do_primary_cold_boot

        /*通过WFE指令挂起执行,恢复执行时会从mail box base address获取执行入口,跳转执行*/
		bl	plat_secondary_cold_boot_setup 

		bl	el3_panic

	do_primary_cold_boot:
	.endif /* _secondary_cold_boot */

	.if \_init_memory
		bl	platform_mem_init /*rpi3目前没有实现这条函数,直接返回*/
	.endif /* _init_memory */

	.if \_init_c_runtime
#if defined(IMAGE_BL31) || (defined(IMAGE_BL2) && BL2_AT_EL3)
...
#endif
		adrp	x0, __BSS_START__
		add	x0, x0, :lo12:__BSS_START__

		adrp	x1, __BSS_END__
		add	x1, x1, :lo12:__BSS_END__
		sub	x1, x1, x0
		bl	zeromem /*清空BSS段*/

#if USE_COHERENT_MEM
		adrp	x0, __COHERENT_RAM_START__
		add	x0, x0, :lo12:__COHERENT_RAM_START__
		adrp	x1, __COHERENT_RAM_END_UNALIGNED__
		add	x1, x1, :lo12: __COHERENT_RAM_END_UNALIGNED__
		sub	x1, x1, x0
		bl	zeromem /*清空COHERENT段*/
#endif

#if defined(IMAGE_BL1) || (defined(IMAGE_BL2) && BL2_IN_XIP_MEM)
		adrp	x0, __DATA_RAM_START__
		add	x0, x0, :lo12:__DATA_RAM_START__
		adrp	x1, __DATA_ROM_START__
		add	x1, x1, :lo12:__DATA_ROM_START__
		adrp	x2, __DATA_RAM_END__
		add	x2, x2, :lo12:__DATA_RAM_END__
		sub	x2, x2, x0
		bl	memcpy16 /*复制DATA段的内容*/
#endif
	.endif /* _init_c_runtime */

	msr	spsel, #0 /*所有EL级别的sp指针都使用sp_el0寄存器*/

	bl	plat_set_my_stack /*获取配置好的栈地址,并赋值给sp寄存器*/

#if STACK_PROTECTOR_ENABLED
...
#endif
	.endm

前面是el3_entrypoint_common函数的代码实现,他首先打开了系统控制寄存器的多项控制位,便于后面的配置:

寄存器值的含义寄存器的含义
sctlr_el3

SCTLR_RESET_VAL & ~(SCTLR_EE_BIT | SCTLR_WXN_BIT \

| SCTLR_SA_BIT | SCTLR_A_BIT | SCTLR_DSSBS_BIT)

  1. 小端
  2. 可写段也可执行
  3. 不检查栈对齐
  4. 不检查内存对齐
系统控制寄存器

然后判断是否是热启动;

然后设置中断向量表;

然后调用el3_arch_init_common再次设定相关寄存器:


	.macro el3_arch_init_common
	mov	x1, #(SCTLR_I_BIT | SCTLR_A_BIT | SCTLR_SA_BIT)
	mrs	x0, sctlr_el3
	orr	x0, x0, x1
	msr	sctlr_el3, x0
	isb

#ifdef IMAGE_BL31
...
#endif /* IMAGE_BL31 */

	mov_imm	x0, ((SCR_RESET_VAL | SCR_EA_BIT | SCR_SIF_BIT) \
			& ~(SCR_TWE_BIT | SCR_TWI_BIT | SCR_SMD_BIT))
#if CTX_INCLUDE_PAUTH_REGS
...
#endif
	msr	scr_el3, x0

	mov_imm	x0, ((MDCR_EL3_RESET_VAL | MDCR_SDD_BIT | \
		      MDCR_SPD32(MDCR_SPD32_DISABLE) | MDCR_SCCD_BIT) \
		    & ~(MDCR_TDOSA_BIT | MDCR_TDA_BIT | MDCR_TPM_BIT))

	msr	mdcr_el3, x0

	msr	daifclr, #DAIF_ABT_BIT

	mov_imm x0, (CPTR_EL3_RESET_VAL & ~(TCPAC_BIT | TTA_BIT | TFP_BIT))

	msr	cptr_el3, x0

	mrs	x0, id_aa64pfr0_el1
	ubfx	x0, x0, #ID_AA64PFR0_DIT_SHIFT, #ID_AA64PFR0_DIT_LENGTH
	cmp	x0, #ID_AA64PFR0_DIT_SUPPORTED
	bne	1f
	mov	x0, #DIT_BIT
	msr	DIT, x0
1:
	.endm
寄存器值的含义寄存器的含义
sctlr_el3SCTLR_I_BIT | SCTLR_A_BIT | SCTLR_SA_BIT
  1. 使能I-cache
  2. 使能内存对齐检查
  3. 使能栈对齐检查
系统控制寄存器
scr_el3

(SCR_RESET_VAL | SCR_EA_BIT | SCR_SIF_BIT) \

& ~(SCR_TWE_BIT | SCR_TWI_BIT | SCR_SMD_BIT)

  1. External Abort and SError Interrupt 会陷入EL3

  2. 安全域访问非安全域的内存是不允许的

  3. WFE引起的中断不会陷入EL3

  4. WFI引起的中断不会陷入EL3

  5. SMC指令使能

安全配置寄存器
mdcr_el3  调试和性能监控配置寄存器
daifclr  中断使能寄存器
cptr_el3  控制访问CPACR_EL1,

CPTR_EL2寄存器的寄存器

然后回到el3_entrypoint_common清除BSS段,栈内存,复制DATA段,配置栈地址。

然后设置了spsel寄存器为0,即所有EL级别的sp指针都使用sp_el0寄存器。

最后回到bl1_entrypoint,el3_entrypoint_common调用结束后bl1_entrypoint里调用了bl1_setup,这是一段c函数,定义如下

void bl1_setup(void)
{
	/* Perform early platform-specific setup */
	bl1_early_platform_setup();

#ifdef AARCH64
	/*
	 * Update pointer authentication key before the MMU is enabled. It is
	 * saved in the rodata section, that can be writen before enabling the
	 * MMU. This function must be called after the console is initialized
	 * in the early platform setup.
	 */
	bl_handle_pauth();
#endif /* AARCH64 */

	/* Perform late platform-specific setup */
	bl1_plat_arch_setup();
}

void bl1_early_platform_setup(void)
{
	/* Initialize the console to provide early debug support */
	rpi3_console_init();

	/* Allow BL1 to see the whole Trusted RAM */
	bl1_tzram_layout.total_base = BL_RAM_BASE;
	bl1_tzram_layout.total_size = BL_RAM_SIZE;
}

void bl1_plat_arch_setup(void)
{
	rpi3_setup_page_tables(bl1_tzram_layout.total_base,
			       bl1_tzram_layout.total_size,
			       BL_CODE_BASE, BL1_CODE_END,
			       BL1_RO_DATA_BASE, BL1_RO_DATA_END
#if USE_COHERENT_MEM
			       , BL_COHERENT_RAM_BASE, BL_COHERENT_RAM_END
#endif
			      ); /*设置页表的操作使用了xlat_tables, 见 https://github.com/ARM-software/arm-trusted-firmware/blob/master/docs/components/xlat-tables-lib-v2-design.rst*/

	enable_mmu_el3(0);
}

此步主要任务是设置了页表和bl1可访问的物理地址

bl1下一步就是进入bl1_main函数

void bl1_main(void)
{
	unsigned int image_id;

	/* Announce our arrival */
	NOTICE(FIRMWARE_WELCOME_STR);
	NOTICE("BL1: %s\n", version_string);
	NOTICE("BL1: %s\n", build_message);

	INFO("BL1: RAM %p - %p\n", (void *)BL1_RAM_BASE,
					(void *)BL1_RAM_LIMIT);

	print_errata_status();

#if ENABLE_ASSERTIONS
...
#endif /* ENABLE_ASSERTIONS */

	/* Perform remaining generic architectural setup from EL3 */
	bl1_arch_setup(); /* 设定下一个EL为AArch64 */

#if TRUSTED_BOARD_BOOT
...
#endif /* TRUSTED_BOARD_BOOT */

	/* Perform platform setup in BL1. */
	bl1_platform_setup(); /* 配置rpi3的io接口 */

	/* Get the image id of next image to load and run. */
	image_id = bl1_plat_get_next_image_id(); /* 获取BL2的image id*/

	/*
	 * We currently interpret any image id other than
	 * BL2_IMAGE_ID as the start of firmware update.
	 */
	if (image_id == BL2_IMAGE_ID)
		bl1_load_bl2(); /* 加载BL2,其实是从内存中的装载地址copy到secure ram空间 */
	else
		NOTICE("BL1-FWU: *******FWU Process Started*******\n");

	bl1_prepare_next_image(image_id); /* 准备相关上下文和配置寄存器,此部分需要单独说明 */

	console_flush();
}

在bl1_prepare_next_image中会准备下一个image的启动,一般bl1的下一个image就是bl2

/*******************************************************************************
 * This function prepares the context for Secure/Normal world images.
 * Normal world images are transitioned to EL2(if supported) else EL1.
 ******************************************************************************/
void bl1_prepare_next_image(unsigned int image_id)
{
...

	/* Prepare the SPSR for the next BL image. */
	if (security_state == SECURE) {
		next_bl_ep->spsr = SPSR_64(MODE_EL1, MODE_SP_ELX,
				   DISABLE_ALL_EXCEPTIONS);
	} else {
...
	}

	/* Allow platform to make change */
	bl1_plat_set_ep_info(image_id, next_bl_ep); /*rpi3没有做出什么改变,直接跳过*/

	/* Prepare the context for the next BL image. */
	cm_init_my_context(next_bl_ep); /*调用cm_setup_context设置各种寄存器的值*/
	cm_prepare_el3_exit(security_state); /*调用了cm_el1_sysregs_context_restore*/

	/* Indicate that image is in execution state. */
	image_desc->state = IMAGE_STATE_EXECUTED;

	print_entry_point_info(next_bl_ep);
}

/*******************************************************************************
 * The following function initializes the cpu_context 'ctx' for
 * first use, and sets the initial entrypoint state as specified by the
 * entry_point_info structure.
 *
 * The security state to initialize is determined by the SECURE attribute
 * of the entry_point_info.
 *
 * The EE and ST attributes are used to configure the endianness and secure
 * timer availability for the new execution context.
 *
 * To prepare the register state for entry call cm_prepare_el3_exit() and
 * el3_exit(). For Secure-EL1 cm_prepare_el3_exit() is equivalent to
 * cm_e1_sysreg_context_restore().
 ******************************************************************************/
void cm_setup_context(cpu_context_t *ctx, const entry_point_info_t *ep)
{
	unsigned int security_state;
	uint32_t scr_el3, pmcr_el0;
	el3_state_t *state;
	gp_regs_t *gp_regs;
	unsigned long sctlr_elx, actlr_elx;

	assert(ctx != NULL);

	security_state = GET_SECURITY_STATE(ep->h.attr);

	/* Clear any residual register values from the context */
	zeromem(ctx, sizeof(*ctx));

	/*
	 * SCR_EL3 was initialised during reset sequence in macro
	 * el3_arch_init_common. This code modifies the SCR_EL3 fields that
	 * affect the next EL.
	 *
	 * The following fields are initially set to zero and then updated to
	 * the required value depending on the state of the SPSR_EL3 and the
	 * Security state and entrypoint attributes of the next EL.
	 */
	scr_el3 = (uint32_t)read_scr();
	scr_el3 &= ~(SCR_NS_BIT | SCR_RW_BIT | SCR_FIQ_BIT | SCR_IRQ_BIT |
			SCR_ST_BIT | SCR_HCE_BIT);
	/*
	 * SCR_NS: Set the security state of the next EL.
	 */
	if (security_state != SECURE)
		scr_el3 |= SCR_NS_BIT;
	/*
	 * SCR_EL3.RW: Set the execution state, AArch32 or AArch64, for next
	 *  Exception level as specified by SPSR.
	 */
	if (GET_RW(ep->spsr) == MODE_RW_64)
		scr_el3 |= SCR_RW_BIT;
	/*
	 * SCR_EL3.ST: Traps Secure EL1 accesses to the Counter-timer Physical
	 *  Secure timer registers to EL3, from AArch64 state only, if specified
	 *  by the entrypoint attributes.
	 */
	if (EP_GET_ST(ep->h.attr) != 0U)
		scr_el3 |= SCR_ST_BIT;

#if !HANDLE_EA_EL3_FIRST
	/*
	 * SCR_EL3.EA: Do not route External Abort and SError Interrupt External
	 *  to EL3 when executing at a lower EL. When executing at EL3, External
	 *  Aborts are taken to EL3.
	 */
	scr_el3 &= ~SCR_EA_BIT;
#endif

#if FAULT_INJECTION_SUPPORT
	/* Enable fault injection from lower ELs */
	scr_el3 |= SCR_FIEN_BIT;
#endif

#if !CTX_INCLUDE_PAUTH_REGS
	/*
	 * If the pointer authentication registers aren't saved during world
	 * switches the value of the registers can be leaked from the Secure to
	 * the Non-secure world. To prevent this, rather than enabling pointer
	 * authentication everywhere, we only enable it in the Non-secure world.
	 *
	 * If the Secure world wants to use pointer authentication,
	 * CTX_INCLUDE_PAUTH_REGS must be set to 1.
	 */
	if (security_state == NON_SECURE)
		scr_el3 |= SCR_API_BIT | SCR_APK_BIT;
#endif /* !CTX_INCLUDE_PAUTH_REGS */

#ifdef IMAGE_BL31
	/*
	 * SCR_EL3.IRQ, SCR_EL3.FIQ: Enable the physical FIQ and IRQ routing as
	 *  indicated by the interrupt routing model for BL31.
	 */
	scr_el3 |= get_scr_el3_from_routing_model(security_state);
#endif

	/*
	 * SCR_EL3.HCE: Enable HVC instructions if next execution state is
	 * AArch64 and next EL is EL2, or if next execution state is AArch32 and
	 * next mode is Hyp.
	 */
	if (((GET_RW(ep->spsr) == MODE_RW_64) && (GET_EL(ep->spsr) == MODE_EL2))
	    || ((GET_RW(ep->spsr) != MODE_RW_64)
		&& (GET_M32(ep->spsr) == MODE32_hyp))) {
		scr_el3 |= SCR_HCE_BIT;
	}

	/*
	 * Initialise SCTLR_EL1 to the reset value corresponding to the target
	 * execution state setting all fields rather than relying of the hw.
	 * Some fields have architecturally UNKNOWN reset values and these are
	 * set to zero.
	 *
	 * SCTLR.EE: Endianness is taken from the entrypoint attributes.
	 *
	 * SCTLR.M, SCTLR.C and SCTLR.I: These fields must be zero (as
	 *  required by PSCI specification)
	 */
	sctlr_elx = (EP_GET_EE(ep->h.attr) != 0U) ? SCTLR_EE_BIT : 0U;
	if (GET_RW(ep->spsr) == MODE_RW_64)
		sctlr_elx |= SCTLR_EL1_RES1;
	else {
		/*
		 * If the target execution state is AArch32 then the following
		 * fields need to be set.
		 *
		 * SCTRL_EL1.nTWE: Set to one so that EL0 execution of WFE
		 *  instructions are not trapped to EL1.
		 *
		 * SCTLR_EL1.nTWI: Set to one so that EL0 execution of WFI
		 *  instructions are not trapped to EL1.
		 *
		 * SCTLR_EL1.CP15BEN: Set to one to enable EL0 execution of the
		 *  CP15DMB, CP15DSB, and CP15ISB instructions.
		 */
		sctlr_elx |= SCTLR_AARCH32_EL1_RES1 | SCTLR_CP15BEN_BIT
					| SCTLR_NTWI_BIT | SCTLR_NTWE_BIT;
	}

#if ERRATA_A75_764081
	/*
	 * If workaround of errata 764081 for Cortex-A75 is used then set
	 * SCTLR_EL1.IESB to enable Implicit Error Synchronization Barrier.
	 */
	sctlr_elx |= SCTLR_IESB_BIT;
#endif

	/*
	 * Store the initialised SCTLR_EL1 value in the cpu_context - SCTLR_EL2
	 * and other EL2 registers are set up by cm_prepare_ns_entry() as they
	 * are not part of the stored cpu_context.
	 */
	write_ctx_reg(get_sysregs_ctx(ctx), CTX_SCTLR_EL1, sctlr_elx);

	/*
	 * Base the context ACTLR_EL1 on the current value, as it is
	 * implementation defined. The context restore process will write
	 * the value from the context to the actual register and can cause
	 * problems for processor cores that don't expect certain bits to
	 * be zero.
	 */
	actlr_elx = read_actlr_el1();
	write_ctx_reg((get_sysregs_ctx(ctx)), (CTX_ACTLR_EL1), (actlr_elx));

	if (security_state == SECURE) {
		/*
		 * Initialise PMCR_EL0 for secure context only, setting all
		 * fields rather than relying on hw. Some fields are
		 * architecturally UNKNOWN on reset.
		 *
		 * PMCR_EL0.LC: Set to one so that cycle counter overflow, that
		 *  is recorded in PMOVSCLR_EL0[31], occurs on the increment
		 *  that changes PMCCNTR_EL0[63] from 1 to 0.
		 *
		 * PMCR_EL0.DP: Set to one so that the cycle counter,
		 *  PMCCNTR_EL0 does not count when event counting is prohibited.
		 *
		 * PMCR_EL0.X: Set to zero to disable export of events.
		 *
		 * PMCR_EL0.D: Set to zero so that, when enabled, PMCCNTR_EL0
		 *  counts on every clock cycle.
		 */
		pmcr_el0 = ((PMCR_EL0_RESET_VAL | PMCR_EL0_LC_BIT
				| PMCR_EL0_DP_BIT)
				& ~(PMCR_EL0_X_BIT | PMCR_EL0_D_BIT));
		write_ctx_reg(get_sysregs_ctx(ctx), CTX_PMCR_EL0, pmcr_el0);
	}

	/* Populate EL3 state so that we've the right context before doing ERET */
	state = get_el3state_ctx(ctx);
	write_ctx_reg(state, CTX_SCR_EL3, scr_el3);
	write_ctx_reg(state, CTX_ELR_EL3, ep->pc); /*设定了下一个image的入口函数*/
	write_ctx_reg(state, CTX_SPSR_EL3, ep->spsr);

	/*
	 * Store the X0-X7 value from the entrypoint into the context
	 * Use memcpy as we are in control of the layout of the structures
	 */
	gp_regs = get_gpregs_ctx(ctx);
	memcpy(gp_regs, (void *)&ep->args, sizeof(aapcs64_params_t));
}


void cm_el1_sysregs_context_restore(uint32_t security_state)
{
	cpu_context_t *ctx;

	ctx = cm_get_context(security_state);
	assert(ctx != NULL);

	el1_sysregs_context_restore(get_sysregs_ctx(ctx)); /*把前面初始化好的寄存器值设下去*/

}

前面可以看到,在进入下一个image前主要设定了以下几个寄存器,注意下一个image也是处于安全域的,所以security_state == SECURE:

寄存器值的含义寄存器的含义
spsr_el3((MODE_RW_64 << MODE_RW_SHIFT) |  \
    (0x01 << MODE_EL_SHIFT) |    \
    (0x01 << MODE_SP_SHIFT) |    \
    (0xf << SPSR_DAIF_SHIFT))
  1. 异常来自AArch64
  2. 异常来自EL1
  3. 使用各自el的sp
  4. 禁用FIQ, IRQ, ABT.DBG中断

el3级别的程序状态存储寄存器,

在异常发生时存储PE的状态,

退出异常处理时恢复PSTATE

scr_el3| SCR_RW_BIT  &

~(SCR_NS_BIT | SCR_FIQ_BIT |

SCR_IRQ_BIT |

SCR_ST_BIT | )

  1. 设定下一个EL为AArch64
  2. EL0和EL1处于安全域,可以访问安全内存
  3. 不处理FIQ
  4. 不处理IRQ
  5. EL1使用AArch64模式时,如果访问某些安全计时器会触发异常陷入EL3
 
sctrl_el1SCTLR_EL1_RES1 | SCTLR_EE_BIT下一个image运行的是小端模式el1级别的系统控制器
pmcr_el0  性能控制寄存器
elr_el3 退出el3以后进入el1的跳转地址,类似lr寄存器exception link register

在准备好BL2 image了以后,bl1_entrypoint最后调用exit_el3来退出当前EL。在bl1_prepare_next_image中已经设置好了SPSR_EL3寄存器和ELR_EL3,因此当exit_el3最终调用eret指令时,会进入EL1等级(记录在SPSR_EL3)跳转的地址记录在ELR_EL3。

最后通过“b el3_exit”语句跳转到bl2执行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值