uboot源码分析

1 前言

1.1 uboot出现的原因

uboot本身是一个裸机程序,uboot出现的目的就是为内核的运行提供环境,内核是不会去进行相关的硬件配置的,因为硬件的种类很多,所以硬件的初始化和配置分出来变成了bootloader,uboot是bootloader的一种。

1.2 uboot主要做哪些工作

1 设置中断向量表
2 设置CPU时钟
3 配置串口
4 配置定时器
5 关闭看门狗
6 初始化DDR
7 记录DDR的大小和物理地址
8 传递机器码

1.3 uboot启动流程

下面是study210开发板 S5PV210芯片的启动流程

1 开发板内置了一块64K的IROM,存放代码的功能主要是初始化SD卡
  和对SD卡的读取,简称BL0,起始地址0x0000_0000,arm架构的
  cpu刚启动就会到这个地址运行读取代码,这是由硬件决定的。
2 开发板内置了一块96k的SRAM,SRAM不用初始化,上电就可以用,
  但是价格比较贵,起始地址: 0xD002_0000
3 BL0读取SD卡的前8k内容到SRAM中的0xD002_0000中,其中前
  16字节为校验头,具体是怎么校验的是由BL0中的算法决定的,真
  正的代码从0xD002_0010开始,简称BL1
4 BL1只是uboot代码的前8k,是uboot的一部分。因为uboot大小为
  几百K,而且SRAM比较贵,DDR又不能上电直接用,所以先使用
  SRAM运行前8k代码初始化DDR。
5 前8k代码不止用于初始化DDR,在九鼎的uboot源码中,BL1进行了
  设置中断向量表,设置CPU为SVC模式,禁用中断,关闭看门狗,
  初始化时钟,初始化串口,设置上电锁存等。
6 DDR初始化完成后,BL1拷贝SD卡49扇区开始的内容到DDR中,49
  扇区存放着完整的uboot,拷贝完成后,BL1跳转到BL2工作,这里
  跳转的DDR地址是0x33e00000,DDR的物理地址范围0x3000_0000~
  0x4fff_ffff共计512M,这个物理地址是可以选择的,这个范围是在
  BL1中设置的。
7 BL2也会执行前8K代码的工作,但是会判断当前PC指针的运行位置,
  当PC指针在DDR中时,会跳过BL1中已经运行过的函数,BL2会进行
  更加细致的硬件设备初始化,并且记录一些必要的硬件参数,把这些
  参数放到0x3000_0100的位置,内核也会从这个位置读取参数,引导
  内核的运行。
8 BL2在初始化完硬件设备后,会拷贝内核镜像到0x30008000这个地址,
  uboot在最后会跳转执行内核时,会传递三个参数,分别使用r0,r1,r2这
  三个寄存器传递,分别是0,机器码,和参数的存储地址。至此,
  uboot的工作全部结束。

2 源码分析

请看注释,下面的代码已经去掉了没有使用到的条件编译

//BL1部分的前16字节,用来存储校验信息,现在只是内容填充,数据的内容不重要
#if defined(CONFIG_EVT1) && !defined(CONFIG_FUSED)
	.word 0x2000
	.word 0x0
	.word 0x0
	.word 0x0
#endif
/*.globl关键字的作用是把变量声明为类似环境变量一样的东西,在整个Makefile编译
过程中,其他的源文件也可以使用这个变量*/
.globl _start
//设置中断向量表
_start: b	reset  //跳转到reset函数,向下找"reset:"
	ldr	pc, _undefined_instruction
	ldr	pc, _software_interrupt
	ldr	pc, _prefetch_abort
	ldr	pc, _data_abort
	ldr	pc, _not_used
	ldr	pc, _irq
	ldr	pc, _fiq

_undefined_instruction:
	.word undefined_instruction
_software_interrupt:
	.word software_interrupt
_prefetch_abort:
	.word prefetch_abort
_data_abort:
	.word data_abort
_not_used:
	.word not_used
_irq:
	.word irq
_fiq:
	.word fiq
_pad:
	.word 0x12345678 /* now 16*4=64 */
.global _end_vect
_end_vect:

	.balignl 16,0xdeadbeef
	/*
 *************************************************************************
 *
 * Startup Code (reset vector)
 *
 * do important init only if we don't start from memory!
 * setup Memory and board specific bits prior to relocation.
 * relocate armboot to ram
 * setup stack
 *
 *************************************************************************
 */
//后面会有ldr pc,_TEXT_BASE的代码,意思是跳转到TEXT_BASE地址去执行代码
_TEXT_BASE:
	.word	TEXT_BASE

/*
 * Below variable is very important because we use MMU in U-Boot.
 * Without it, we cannot run code correctly before MMU is ON.
 * by scsuh.
 */
_TEXT_PHY_BASE:
	.word	CFG_PHY_UBOOT_BASE

.globl _armboot_start
_armboot_start:
	.word _start

/*
 * These are defined in the board-specific linker script.
 */
.globl _bss_start
_bss_start:
	.word __bss_start

.globl _bss_end
_bss_end:
	.word _end
/*
 * the actual reset code
 */
//跳转到这里执行复位代码
reset:
	/*
	 * set the cpu to SVC32 mode and IRQ & FIQ disable
	 */
	@;mrs	r0,cpsr              //@开头的是注释
	@;bic	r0,r0,#0x1f
	@;orr	r0,r0,#0xd3
	@;msr	cpsr,r0
	//失能中断,设置CPU为SVC模式
	msr	cpsr_c, #0xd3		@ I & F disable, Mode: 0x13 - SVC  
/*
 *************************************************************************
 *
 * CPU_init_critical registers
 *
 * setup important registers
 * setup memory timing
 *
 *************************************************************************
 */
         /*
         * we do sys-critical inits only at reboot,
         * not when booting from ram!
         */
cpu_init_crit:
	bl	disable_l2cache //做一些关于缓存的配置,我不懂,老师说和主体无关不用管
	bl	set_l2cache_auxctrl_cycle
	bl	enable_l2cache
	/*
    * Invalidate L1 I/D
     */
     mov	r0, #0                  @ set up for MCR
     mcr	p15, 0, r0, c8, c7, 0   @ invalidate TLBs
     mcr	p15, 0, r0, c7, c5, 0   @ invalidate icache

    /*
     * disable MMU stuff and caches
     */
     mrc	p15, 0, r0, c1, c0, 0
     bic	r0, r0, #0x00002000     @ clear bits 13 (--V-)
     bic	r0, r0, #0x00000007     @ clear bits 2:0 (-CAM)
     orr	r0, r0, #0x00000002     @ set bit 1 (--A-) Align
     orr	r0, r0, #0x00000800     @ set bit 12 (Z---) BTB
     mcr 	p15, 0, r0, c1, c0, 0


    /* Read booting information */
    //0xE000_0004这个寄存器会被自动设置,被设置值和选择的启动介质有关 
     ldr	r0, =PRO_ID_BASE
     ldr	r1, [r0,#OMR_OFFSET]
     bic	r2, r1, #0xffffffc1   //r2中根据启动介质的不同,被设置成不同的值
     /* SD/MMC BOOT */
	cmp     r2, #0xc              //当r2中的值为0xc的时候,执行下一句代码
	/*"moveq"指令用于将一个立即数加载到寄存器中,其中"eq"表示条件码,表示只有
	在上一条指令影	响标志位并且结果为零时才执行该指令。*/
	moveq   r3, #BOOT_MMCSD	      //BOOT_MMCSD=0X3,r3=0x3
	/*
	 * Go setup Memory and board specific bits prior to relocation.
	 */
	//根据芯片手册推荐,设置栈指针到合适的SRAM中的区段,为C代码运行做准备
	ldr	sp, =0xd0036000 /* end of sram dedicated to u-boot */
	sub	sp, sp, #12	/* set stack */
	mov	fp, #0  //这个fp指针不知道是干什么的
	//跳转到C函数执行代码
	bl	lowlevel_init	/* go setup pll,mux,memory */
lowlevel_init:
    //压栈,因为lr寄存器只能保存一个地址,且SVC模式下只有一个lr寄存器,函数跳转必须压栈
	push	{lr}   

	/* check reset status 检查热启动还冷启动,和启动主体关系不大 */
	ldr	r0, =(ELFIN_CLOCK_POWER_BASE+RST_STAT_OFFSET)
	ldr	r1, [r0]
	bic	r1, r1, #0xfff6ffff
	cmp	r1, #0x10000
	beq	wakeup_reset_pre
	cmp	r1, #0x80000
	beq	wakeup_reset_from_didle

	/* IO Retention release 和主要流程关系不大,后面我就不再赘述,有注释就是重要*/
	ldr	r0, =(ELFIN_CLOCK_POWER_BASE + OTHERS_OFFSET)	/* 0xE0100000 + 0xE000 */
	ldr	r1, [r0]
	ldr	r2, =IO_RET_REL
	orr	r1, r1, r2
	str	r1, [r0]

	/* Disable Watchdog */
	ldr	r0, =ELFIN_WATCHDOG_BASE	/* 0xE2700000 */
	mov	r1, #0
	str	r1, [r0]

	/* SRAM(2MB) init for SMDKC110 */
	/* GPJ1 SROM_ADDR_16to21 */
	ldr	r0, =ELFIN_GPIO_BASE
	
	ldr	r1, [r0, #GPJ1CON_OFFSET]
	bic	r1, r1, #0xFFFFFF
	ldr	r2, =0x444444
	orr	r1, r1, r2
	str	r1, [r0, #GPJ1CON_OFFSET]

	ldr	r1, [r0, #GPJ1PUD_OFFSET]
	ldr	r2, =0x3ff
	bic	r1, r1, r2
	str	r1, [r0, #GPJ1PUD_OFFSET]

	/* GPJ4 SROM_ADDR_16to21 */
	ldr	r1, [r0, #GPJ4CON_OFFSET]
	bic	r1, r1, #(0xf<<16)
	ldr	r2, =(0x4<<16)
	orr	r1, r1, r2
	str	r1, [r0, #GPJ4CON_OFFSET]

	ldr	r1, [r0, #GPJ4PUD_OFFSET]
	ldr	r2, =(0x3<<8)
	bic	r1, r1, r2
	str	r1, [r0, #GPJ4PUD_OFFSET]


	/* CS0 - 16bit sram, enable nBE, Byte base address */
	ldr	r0, =ELFIN_SROM_BASE	/* 0xE8000000 */
	mov	r1, #0x1
	str	r1, [r0]

	/* PS_HOLD pin(GPH0_0) set to high 上电所存,按下POWER键,板子持续供电*/
	ldr	r0, =(ELFIN_CLOCK_POWER_BASE + PS_HOLD_CONTROL_OFFSET)
	ldr	r1, [r0]
	orr	r1, r1, #0x300	
	orr	r1, r1, #0x1	
	str	r1, [r0]

	/* init system clock 初始化时钟*/
	bl	system_clock_init
	//初始化DDR
	bl	mem_ctrl_asm_init
//汇编语言的执行顺序是流水型,bl执行完函数会跳转回来,然后执行1:
1:
	/* for UART */
	bl	uart_asm_init
	
	bl	tzpc_init
	
	bl	onenandcon_init
	//出栈
	pop	{pc}
/* get ready to call C functions */
//再次设置栈指针,因为DDR已经初始化,且SRAM的栈太小,所以再次设置栈指针
ldr	sp, _TEXT_PHY_BASE	/* setup temp stack pointer */
sub	sp, sp, #12
mov	fp, #0			/* no previous frame, so fp=0 */
/* when we already run in ram, we don't need to relocate U-Boot.
* and actually, memory controller must be configured before U-Boot
 * is running in ram.
 */
 //判断pc寄存器现在运行的地址,如果在SRAM中,进行BL2拷贝到DDR中,否则不进行
ldr	r0, =0xff000fff
bic	r1, pc, r0		/* r0 <- current base addr of code */
ldr	r2, _TEXT_BASE		/* r1 <- original base addr in ram */
bic	r2, r2, r0		/* r0 <- current base addr of code */
//请看英文注释
cmp     r1, r2                  /* compare r0, r1                  */
beq     after_copy		/* r0 == r1 then skip flash copy   */

//注意一下哈,这里我把after_copy函数拷贝过来了,并不是紧挨着的
after_copy:
//使能mmu,设置虚拟地址转换表,这个内容比较难懂,我看不懂虚拟地址是怎么转换的
enable_mmu:
	/* enable domain access */
	ldr	r5, =0x0000ffff
	mcr	p15, 0, r5, c3, c0, 0		@load domain access register

	/* Set the TTB register */
	ldr	r0, _mmu_table_base
	ldr	r1, =CFG_PHY_UBOOT_BASE
	ldr	r2, =0xfff00000
	bic	r0, r0, r2
	orr	r1, r0, r1
	mcr	p15, 0, r1, c2, c0, 0

	/* Enable the MMU */
mmu_on:
	mrc	p15, 0, r0, c1, c0, 0
	orr	r0, r0, #1
	mcr	p15, 0, r0, c1, c0, 0
	nop
	nop
	nop
	nop
//after_copy 结尾处
//再次设置栈指针,这次设置的目的是把栈放置到合适的位置
ldr	sp, =(CFG_UBOOT_BASE + CFG_UBOOT_SIZE - 0x1000)
sub	sp, r0, #12		/* leave 3 words for abort-stack    */
//bss段变量的值设置为0
clear_bss:
	ldr	r0, _bss_start		/* find start of bss segment        */
	ldr	r1, _bss_end		/* stop here                        */
	mov 	r2, #0x00000000		/* clear                            */

clbss_l:
	str	r2, [r0]		/* clear loop...                    */
	add	r0, r0, #4
	cmp	r0, r1
	ble	clbss_l
	/*跳转执行_start_armboot,本人认为啊,上面的代码知道具体干什么就行了,代码
	只是对功能的实现,知道功能,再去实现代码不是一件难事,所以学习要学习思路*/
	ldr	pc, _start_armboot
_start_armboot:
	.word start_armboot

上面是uboot的BL1部分,主要进行了:

1 异常中断向量表的创建
2 关闭看门狗
3 上电锁存
4 DDR初始化
5 系统时钟初始化
6 串口初始化
7 设置栈指针
8 使能MMU,创建内存映射表(老版本uboot并没有使用虚拟地址)

接下来进行BL2源码的解析,注意一下哈,我贴出来的代码都是去掉条件编译的

void start_armboot (void)
{
	init_fnc_t **init_fnc_ptr;
	char *s;
	int mmc_exist = 0;
	ulong gd_base;
	//设置全局变量存储的地址,事实上,你想怎么设置就怎么设置,毕竟DDR随便操作,但是不要出现数据覆盖的情况
	gd_base = CFG_UBOOT_BASE + CFG_UBOOT_SIZE - CFG_MALLOC_LEN - CFG_STACK_SIZE - sizeof(gd_t);
	//转成gd_t结构体类型,因为/**/这种注释不能嵌套,所以我使用条件编译的形式进行注释的添加
	#if 0
	typedef	struct	global_data {
	bd_t		*bd;
	unsigned long	flags;
	unsigned long	baudrate;
	unsigned long	have_console;	/* serial_init() was called */
	unsigned long	reloc_off;	/* Relocation Offset */
	unsigned long	env_addr;	/* Address  of Environment struct */
	unsigned long	env_valid;	/* Checksum of Environment valid? */
	unsigned long	fb_base;	/* base address of frame buffer */
	void		**jt;		/* jump table */
	} gd_t;
	#endif
	gd = (gd_t*)gd_base; 
	/* compiler optimization barrier needed for GCC >= 3.4 */
	//内存优化方面的知识,我不了解
	__asm__ __volatile__("": : :"memory");
	
	memset ((void*)gd, 0, sizeof (gd_t));
	//从这句代码来看,结构体在内存中的存储形式是大端模式
	gd->bd = (bd_t*)((char*)gd - sizeof(bd_t));
	memset (gd->bd, 0, sizeof (bd_t));
	//_bss_start和_armboot_start是makefile链接脚本指定的
	monitor_flash_len = _bss_start - _armboot_start;
	//和BL1的硬件初始化不同,这里是存储硬件的相关信息,比如:内存的地址分布、数量和大小
	for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
		if ((*init_fnc_ptr)() != 0) {
			hang ();
		}
	}
	//uboot中实现了一个malloc函数,用来申请内存,堆区的大小由宏配置文件的宏定义决定
	//如果没记错的话,这里的堆区应该是900多K。实际上malloc的底层实现原理只是有一个
	//内存管理表,这个表是个数据结构,我好像记得里面存储的是一个个的结构体,记录了每一块
	//内存的起始地址,类型,大小等,free函数就是删除这个结构体
	mem_malloc_init (CFG_UBOOT_BASE + CFG_UBOOT_SIZE - CFG_MALLOC_LEN - CFG_STACK_SIZE);
	//这里进行更细致的SD卡初始化,IROM中只是进行了简单的初始化
	puts ("SD/MMC:  ");
	mmc_exist = mmc_initialize(gd->bd);
	if (mmc_exist != 0)
	{
		puts ("0 MB\n");
	}
	/* initialize environment */
	env_relocate ();  //我没了解,但是老师讲了
	/* IP Address */
	gd->bd->bi_ip_addr = getenv_IPaddr ("ipaddr");	//获取IP地址
	/* MAC Address */
	{
		int i;
		ulong reg;
		char *s, *e;
		char tmp[64];

		i = getenv_r ("ethaddr", tmp, sizeof (tmp));
		s = (i > 0) ? tmp : NULL;

		for (reg = 0; reg < 6; ++reg) {
			gd->bd->bi_enetaddr[reg] = s ? simple_strtoul (s, &e, 16) : 0;
			if (s)
				s = (*e) ? e + 1 : e;
		}	
	}
		//这个函数里面存储的应该是函数名,然后去执行函数
	    devices_init ();	/* get the devices list going. */
	    jumptable_init ();   //不懂
	    /* enable exceptions */
		enable_interrupts ();
		cs8900_get_enetaddr (gd->bd->bi_enetaddr);  //应该和网卡驱动有关
		/* Initialize from environment 从环境变量配置那里读取信息,并给一些变量赋值*/
		if ((s = getenv ("loadaddr")) != NULL) {
			load_addr = simple_strtoul (s, NULL, 16);
		}
		if ((s = getenv ("bootfile")) != NULL) {
			copy_filename (BootFile, s, sizeof (BootFile));
		}
		/*这后面的知识,我偷懒了,没学,哈哈哈,大概的作用就是为后面向内核传递
		硬件设备的参数做准备*/
		board_late_init ();
		puts ("Net:   ");
		eth_initialize(gd->bd);
		puts("IDE:   ");
		ide_init();
		extern int x210_preboot_init(void);
		x210_preboot_init();
		/* check menukey to update from sd */
		extern void update_all(void);
		if(check_menu_update_from_sd()==0)//update mode
		{
			//按下LEFT按键好像可以使用SD卡向iNand烧录uboot kernel rootfs,使用SD卡快速烧录
			puts ("[LEFT DOWN] update mode\n");
			run_command("fdisk -c 0",0);
			update_all();
		}
		else
			puts ("[LEFT UP] boot mode\n");
	
		/* main_loop() can return to retry autoboot, if so just run it again. */
		for (;;) {
			//进入uboot的类型shell的命令终端窗口,进入后,后自动调用一些设置的命令,比如:bootm 0x30008000;跳转到指定地址执行程序。具体kernel和rootfs的挂载地址我忘记了,这里可能不准确。bootm对应的函数是 do_bootm_linux,请看下一个函数。
			main_loop ();
		}
	
		/* NOTREACHED - no way out of command loop except booting */
}
//bootm调用的函数
void do_bootm_linux (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[],
		     bootm_headers_t *images)
{
	ulong	initrd_start, initrd_end;
	ulong	ep = 0;
	bd_t	*bd = gd->bd;
	char	*s;
	//机器码,正常来说,每个板子的机器码是唯一的,可以通过查看好像叫type-machines的文件获取
	int	machid = bd->bi_arch_number;
	//定义个执行内核的函数,并进行传参
	void	(*theKernel)(int zero, int arch, uint params);
	int	ret;

#ifdef CONFIG_CMDLINE_TAG
	//获取在BL2开头部分存储的硬件信息,这里是获取自己设置的环境变量
	char *commandline = getenv ("bootargs");
#endif

	/* find kernel entry point */
	if (images->legacy_hdr_valid) {
		//ep:entry point获取内核的程序入口,就行C语言中的main入口
		ep = image_get_ep (&images->legacy_hdr_os_copy);
#if defined(CONFIG_FIT)
	} else if (images->fit_uname_os) {
		ret = fit_image_get_entry (images->fit_hdr_os,
					images->fit_noffset_os, &ep);
		if (ret) {
			puts ("Can't get entry point property!\n");
			goto error;
		}
#endif
	} else {
		puts ("Could not find kernel entry point!\n");
		goto error;
	}
	//跳转到内核的程序入口地址,这里并不是真正的入口地址,而是zImage头部解压代码的入口
	theKernel = (void (*)(int, int, uint))ep;

	s = getenv ("machid");
	if (s) {
		machid = simple_strtoul (s, NULL, 16);
		printf ("Using machid 0x%x from environment\n", machid);
	}
	//不懂
	ret = boot_get_ramdisk (argc, argv, images, IH_ARCH_ARM,
			&initrd_start, &initrd_end);
	if (ret)
		goto error;
	//不懂,有的没注释是因为有英文注释,或者自己不懂
	show_boot_progress (15);

	debug ("## Transferring control to Linux (at address %08lx) ...\n",
	       (ulong) theKernel);
	//内核中取参数时,setup_start_tag会作为开始标志。传参按照tag结构体的形式,内核也会使用这种方式取
	setup_start_tag (bd);
	setup_memory_tags (bd);  //这里重要,会传递几块DDR,起始地址和大小。这些信息在BL2中设置
	setup_commandline_tag (bd, commandline);
	if (initrd_start && initrd_end)
		setup_initrd_tag (bd, initrd_start, initrd_end);
	setup_mtdpartition_tag();
	setup_end_tag (bd);
	printf ("\nStarting kernel ...\n\n");  //uboot最后打印的信息
	cleanup_before_linux ();

	theKernel (0, machid, bd->bi_boot_params); //开始跳转执行kernel
	/* does not return */
	return;
error:
	do_reset (cmdtp, flag, argc, argv);
	return;
}

至此,uboot源码全部分析完毕,上面内容是自己的理解,难免有错误,读者注意辨别。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
所有资料来源网上,与朋友分享 u-boot-1.1.6之cpu/arm920t/start.s分析 2 u-boot中.lds连接脚本文件的分析 12 分享一篇我总结的uboot学习笔记(转) 15 U-BOOT内存布局及启动过程浅析 22 u-boot中的命令实现 25 U-BOOT环境变量实现 28 1.相关文件 28 2.数据结构 28 3.ENV 的初始化 30 3.1env_init 30 3.2 env_relocate 30 3.3*env_relocate_spec 31 4. ENV 的保存 31 U-Boot环境变量 32 u-boot代码链接的问题 35 ldr和adr在使用标号表达式作为操作数的区别 40 start_armboot浅析 42 1.全局数据结构的初始化 42 2.调用通用初始化函数 43 3.初始化具体设备 44 4.初始化环境变量 44 5.进入主循环 44 u-boot编译过程 44 mkconfig文件的分析 47 从NAND闪存中启动U-BOOT的设计 50 引言 50 NAND闪存工作原理 51 从NAND闪存启动U-BOOT的设计思路 51 具体设计 51 支持NAND闪存的启动程序设计 51 支持U-BOOT命令设计 52 结语 53 参考文献 53 U-boot给kernel传参数和kernel读取参数—struct tag (以及补充) 53 1 、u-boot 给kernel 传RAM 参数 54 2 、Kernel 读取U-boot 传递的相关参数 56 3 、关于U-boot 中的bd 和gd 59 U-BOOT源码分析及移植 60 一、u-boot工程的总体结构: 61 1、源代码组织 61 2.makefile简要分析 61 3、u-boot的通用目录是怎么做到与平台无关的? 63 4、smkd2410其余重要的文件 : 63 二、u-boot的流程、主要的数据结构、内存分配 64 1、u-boot的启动流程: 64 2、u-boot主要的数据结构 66 3、u-boot重定位后的内存分布: 68 三、u-boot的重要细节 。 68 关于U-boot中命令相关的编程 : 73 四、U-boot在ST2410的移植,基于NOR FLASH和NAND FLASH启动。 76 1、从smdk2410到ST2410: 76 2、移植过程: 76 3、移植要考虑的问题: 77 4、SST39VF1601: 77 5、我实现的flash.c主要部分: 78 6、增加从Nand 启动的代码 : 82 7、添加网络命令。 87
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值