2.4.uboot配置和编译过程详解

2.4.1.uboot主Makefile分析1

2.4.1.1、uboot version确定(Makefile的24-29行)

(1)uboot的版本号分3个级别:
VERSION = 1
PATCHLEVEL = 3
SUBLEVEL = 4
EXTRAVERSION =
VERSION:主板本号
PATCHLEVEL:次版本号
SUBLEVEL:再次版本号
EXTRAVERSION:另外附加的版本信息,自己可随便定义
这4个用.分隔开共同构成了最终的版本号。

(2)Makefile中版本号最终生成了一个变量U_BOOT_VERSION,这个变量记录了Makefile中配置的版本号。

(3) include/version_autogenerated.h文件是编译过程中自动生成的一个文件,所以源目录中没有,但是编译过后的uboot中就有了。它里面的内容是一个宏定义,宏定义的值内容就是我们在Makefile中配置的uboot的版本号。
#$(obj)是一个变量,这个变量是在后面定义的,要注意=和:=区别
VERSION_FILE = $(obj)include/version_autogenerated.h


(4)验证方法:自己修改主Makefile中几个Version有关的变量,然后重新编译uboot,然后烧录到SD卡中,从SD卡启动,然后去看启动时uboot打印出来的版本信息,看看变化是不是和自己的分析一致。(这个实验我测试失败了,因为下载的uboot不能用)

2.4.1.2、HOSTARCH和HOSTOS

(1)直接在shell中执行 uname -m得到i686,得到的值其实你当前执行这个命令的电脑的CPU的版本号。
HOSTARCH := $(shell uname -m)	
#以上这种写法类似如下:		
#var=`pwd`
var=$(shell pwd)

all:
	echo $(var)
打印结果:
echo /home/aston    直接执行shell pwd
/home/aston
如果是 var=`pwd` 样定义变量的话,打印结果如下:
echo `pwd`
/home/aston

(2)shell中的|叫做 管道,管道的作用就是把管道前面一个运算式的输出作为后面一个的输入再去做处理,最终的输出才是我们整个式子的输出。
#sed是一个字符串匹配和替换工具
#就是如果前面是i.86就会替换成i386
sed -e s/i.86/i386/

(3)HOSTARCH这个名字:HOST是主机,就是当前在做开发用的这台电脑就叫主机;ARCH是architecture(架构)的缩写,表示CPU的架构。所以HOSTARCH就表示主机的CPU的架构。

(4)HOSTOS
#作用是:uname -s出来的操作系统会转成小写
#tr '[:upper:]' '[:lower:]':作用就是大小转小写的
HOSTOS := $(shell uname -s | tr '[:upper:]' '[:lower:]' | \
	    sed -e 's/\(cygwin\).*/cygwin/')

(5)这两个环境变量是主机的操作系统和主机的CPU架构,得出后保存备用,后面自然会用到。

2.4.2.uboot主Makefile分析2

2.4.2.1、静默编译(50-54行)

#(,$(findstring s,$(MAKEFLAGS))):把$(findstring s,$(MAKEFLAGS)与空进行对比
#当我们make -s,-s就作为MAKEFLAGS传给Makefile,Makefile就会找到,然后就不为空
#ifeq就不成立,就会执行else  XECHO = :的语句
ifeq (,$(findstring s,$(MAKEFLAGS)))
XECHO = echo
else
XECHO = :
endif

(1)平时默认编译时命令行会打印出来很多编译信息。但是有时候我们不希望看到这些编译信息,就后台编译即可。这就叫静默编译。

(2)使用方法就是编译时make -s,-s会作为MAKEFLAGS传给Makefile,在50-54行这段代码作用下XECHO变量就会被变成空(默认等于echo),于是实现了静默编译。

2.4.2.2、2种编译方法(原地编译和单独输出文件夹编译)

(1)编译复杂项目,Makefile提供2种编译管理方法。默认情况下是当前文件夹中的.c文件,编译出来的.o文件会放在同一文件夹下。这种方式叫原地编译。原地编译的好处就是处理起来简单。

(2)原地编译有一些坏处:第一,污染了源文件目录。第二的缺陷就是一套源代码只能按照一种配置和编译方法进行处理,无法同时维护2个或2个以上的配置编译方式。

(3)为了解决以上2种缺陷,uboot支持单独输出文件夹方式的编译(linux kernel也支持,而且uboot的这种技术就是从linux kernel学习来的)。基本思路就是在编译时另外指定一个输出目录,将来所有的编译生成的.o文件或生成的其他文件全部丢到那个输出目录下去。源代码目录不做任何污染,这样输出目录就承载了本次配置编译的所有结果。

(4)具体用法:默认的就是原地编译。如果需要指定具体的输出目录编译则有2种方式来指定输出目录。(具体参考Makefile 56-76行注释内容)
第一种:make O=输出目录
第二种:export BUILD_DIR=输出目录 然后再make
如果两个都指定了(既有BUILD_DIR环境变量存在,又有O=xx),则O=xx具有更高优先级,听他的。

(5)两种编译的实现代码在Makefile的78-123行。

2.4.2.3、编译方法实践

2.4.2.4、相关Makefile分析

2.4.3.uboot主Makefile分析3

2.4.3.1、OBJTREE、SRCTREE、TOPDIR

(1)OBJTREE:编译出的.o文件存放的目录的根目录。在默认编译下,OBJTREE等于当前目录;在O=xx编译下,OBJTREE就等于我们设置的那个输出目录。

(2)SRCTREE: 源码目录,其实就是源代码的根目录,也就是当前目录。

总结:在默认编译下,OBJTREE和SRCTREE相等;在O=xx这种编译下OBJTREE和SRCTREE不相等。Makefile中定义这两个变量,其实就是为了记录编译后的.o文件往哪里放,就是为了实现O=xx的这种编译方式的。

2.4.3.2、MKCONFIG(Makefile的101行)

#源码根目录下(SRCTREE)有个mkconfig的文件
MKCONFIG	:= $(SRCTREE)/mkconfig
export MKCONFIG


    
    
(1)Makefile中定义的一个变量(在这里定义,在后面使用),它的值就是我们源码根目录下面的mkconfig。这个mkconfig是一个脚本,这个脚本就是uboot配置阶段的配置脚本。后面要用至少3节课详细讲这个配置脚本的工作。





2.4.3.3、include $(obj)include/config.mk(133行)

include $(obj)include/config.mk
export	ARCH CPU BOARD VENDOR SOC

(1) include/config.mk不是源码自带的(你在没有编译过的源码目录下是找不到这个文件的),要在配置过程(make x210_sd_config)中才会生成这个文件。因此这个文件的值和我们配置过程有关,是由配置过程根据我们的配置自动生成的。

(2)我们X210在iNand情况下配置生成的config.mk内容为:
ARCH   = arm
CPU    = s5pc11x
BOARD  = x210
VENDOR = samsung
SOC    = s5pc110

(3)我们在下一行(134行)export导出了这5个变量作为环境变量。所以这两行加起来其实就是为当前makefile定义了5个环境变量而已。之所以不直接给出这5个环境变量的值,是因为我们希望这5个值是可以被人很容易的、集中的配置的。

(4)这里的配置值来自于2589行那里的配置项。如果我们要更改这里的某个配置值要到2589行那里调用MKCONFIG脚本传参时的参数。
x210_sd_config :	unconfig
	@$(MKCONFIG) $(@:_config=) arm s5pc11x x210 samsung s5pc110
	@echo "TEXT_BASE = 0xc3e00000" > $(obj)board/samsung/x210/config.mk

2.4.3.4、ARCH CROSS_COMPILE

(1)接下来有2个很重要的环境变量。一个是ARCH,上面导出的,值来自于我们的配置过程,它的值会影响后面的CROSS_COMPILE环境变量的值。ARCH的意义是定义当前编译的目标CPU的架构。

(2)CROSS_COMPILE是定义交叉编译工具链的前缀的。定义这些前缀是为了在后面用(用前缀加上后缀来定义编译过程中用到的各种工具链中的工具)。我们把前缀和后缀分开还有一个原因就是:在不同CPU架构上的交叉编译工具链,只是前缀不一样,后缀都是一样的。因此定义时把前缀和后缀分开,只需要在定义前缀时区分各种架构即可实现可移植性。

(3)CROSS_COMPILE在136-182行来确定。CROSS_COMPILE是被ARCH所确定的,只要配置了ARCH=arm,那么我们就只能在ARM的那个分支去设置CROSS_COMPILE的值。这个设置值只要能保证找到那个交叉编译工具链即可,不一定非得是全路径的,相对路径也可以。(如果已经将工具链导出到环境变量,并且设置了符号链接,这样CROSS_COMPILE = arm-linux-就可以)
#CROSS_COMPILE = arm-linux-
#CROSS_COMPILE = /usr/local/arm/4.4.1-eabi-cortex-a8/usr/bin/arm-linux-
#CROSS_COMPILE = /usr/local/arm/4.2.2-eabi/usr/bin/arm-linux-
CROSS_COMPILE = /usr/local/arm/arm-2009q3/bin/arm-none-linux-gnueabi-

(4)实际运用时,我们可以在Makefile中去更改设置CROSS_COMPILE的值,也可以在编译时用 make CROSS_COMPILE=xxxx来设置,而且编译时传参的方法可以覆盖Makefile里面的设置。

2.4.4.uboot主Makefile分析4

2.4.4.1、$(TOPDIR)/config.mk(主Makefile的185行)

#TOPDIR:主目录
#把主目录下的config.mk(mafile文件)加入到了主Makefile中
include $(TOPDIR)/config.mk
#config.mk中重要的
#CROSS_COMPILE这个变量是之前定义的交叉编译工具链的前缀
#总的意思就前缀加后缀变成了完整的交叉编译工具链
AS	= $(CROSS_COMPILE)as
LD	= $(CROSS_COMPILE)ld
CC	= $(CROSS_COMPILE)gcc
CPP	= $(CC) -E
AR	= $(CROSS_COMPILE)ar
NM	= $(CROSS_COMPILE)nm
LDR	= $(CROSS_COMPILE)ldr
STRIP	= $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump
RANLIB	= $(CROSS_COMPILE)RANLIB

2.4.4.2、编译工具定义(config.mk 94-107行)

2.4.4.3、包含开发板配置项目(config.mk, 112行)

(1)autoconfig.mk文件不是源码提供的,是配置过程自动生成的。
# Load generated board configuration
sinclude $(OBJTREE)/include/autoconf.mk


(2)这个文件的作用就是用来指导整个uboot的编译过程。这个文件的内容其实就是很多CONFIG_开头的宏(可以理解为变量),这些宏/变量会影响我们uboot编译过程的走向(原理就是条件编译)。在uboot代码中有很多地方使用条件编译进行编写,这个条件编译是用来实现可移植性的。(可以说uboot的源代码在很大程度来说是拼凑起来的,同一个代码包含了各种不同开发板的适用代码,用条件编译进行区别。)

(3)这个文件不是凭空产生的,配置过程也是需要原材料来产生这个文件的。原材料在源码目录的inlcude/configs/xxx.h头文件。(X210开发板中为 include/configs/x210_sd.h)。这个h头文件里面全都是宏定义,这些宏定义就是我们对当前开发板的移植。每一个开发板的移植都对应这个目录下的一个头文件,这个头文件里每一个宏定义都很重要,这些配置的宏定义就是我们移植uboot的关键所在。

2.4.5.uboot主Makefile分析5

2.4.5.1、链接脚本(config.mk 142-149行)

(1)如果定义了CONFIG_NAND_U_BOOT宏,则链接脚本叫u-boot-nand.lds,如果未定义这个宏则链接脚本叫u-boot.lds。
通过查uboot/output/include/autoconf.mk可知没有定义CONFIG_NAND_U_BOOT这个宏,同时我们也知道我们的开发板使用的是iNAND

(2)从字面意思分析,即可知:CONFIG_NAND_U_BOOT是在Nand版本情况下才使用的,我们使用的X210都是iNand版本的,因此这个宏没有的。

(3)实际在board\samsung\x210目录下有u-boot.lds,这个就是链接脚本。我们在分析uboot的编译链接过程时就要考虑这个链接脚本。

2.4.5.2、TEXT_BASE(config.mk 156-158行)

(1)Makefile中在配置X210开发板时,在board/samsung/x210目录下生成了一个文件config.mk,其中的内容就是:TEXT_BASE = 0xc3e00000相当于定义了一个变量。






(2)TEXT_BASE是将来我们整个uboot链接时指定的链接地址。因为uboot中启用了虚拟地址映射,因此这个C3E00000地址就等于0x23E00000(也可能是33E00000具体地址要取决于uboot中做的虚拟地址映射关系)。

(3)回顾裸机中讲的链接地址的问题,再想想dnw方式先下载x210_usb.bin然后再下载uboot.bin时为什么第二个地址是23E00000.
ifneq ($(TEXT_BASE),)
LDFLAGS += -Ttext $(TEXT_BASE)
endif
这里的-Ttext与裸机的的-Ttext是一样的

2.4.5.3、自动推导规则(config.mk 239-256行)

(1)我们在讲Makefile时提到过自动推导规则,具体理解可以参考《跟我一起学Makefile》

ifndef REMOTE_BUILD

%.s:	%.S
	$(CPP) $(AFLAGS) -o $@ $<
%.o:	%.S
	$(CC) $(AFLAGS) -c -o $@ $<
%.o:	%.c
	$(CC) $(CFLAGS) -c -o $@ $<

else

$(obj)%.s:	%.S
	$(CPP) $(AFLAGS) -o $@ $<
$(obj)%.o:	%.S
	$(CC) $(AFLAGS) -c -o $@ $<
$(obj)%.o:	%.c
	$(CC) $(CFLAGS) -c -o $@ $<
endif

2.4.6.uboot主Makefile分析6

(1)291行出现了整个主Makefile中第一个目标all(也就是默认目标,我们直接在uboot根目录下make其实就等于make all,就等于make这个目标)

(2)目标中有一些比较重要的。譬如:u-boot是最终编译链接生成的elf格式的可执行文件,

(3)unconfig字面意思来理解就是未配置。这个符号用来做为我们各个开发板配置目标的依赖。目标是当我们已经配置过一个开发板后再次去配置时还可以配置。

(4)我们配置开发板时使用:make x210_sd_config,因此分析x210_sd_config肯定是主Makefile中的一个目标。
#MKCONFIG:主目录下的主mkconfig脚本
#$(@:_config=) arm s5pc11x x210 samsung s5pc110:传了6个参数 
x210_sd_config :	unconfig
	@$(MKCONFIG) $(@:_config=) arm s5pc11x x210 samsung s5pc110
	@echo "TEXT_BASE = 0xc3e00000" > $(obj)board/samsung/x210/config.mk

2.4.7.uboot配置过程详解1

#MKCONFIG:主目录下的主mkconfig脚本
#$(@:_config=) arm s5pc11x x210 samsung s5pc110:传了6个参数 
#$(@:_config=):$(@($@)=x210_sd_config,
#:_config=): _config用空来替换
#$(@:_config=):整句意思就是把x210_sd_config中的_config用空来替换,最后得到x210_sd
#所以$1第一个参数是x210_sd
x210_sd_config :	unconfig
	@$(MKCONFIG) $(@:_config=) arm s5pc11x x210 samsung s5pc110
	@echo "TEXT_BASE = 0xc3e00000" > $(obj)board/samsung/x210/config.mk

(1)mkconfig脚本的6个参数
$(@:_config=) arm s5pc11x x210 samsung s5pc110

x210_sd_config里的_config部分用空替换,得到:x210_sd,这就是第一个参数,所以:

$1: x210_sd
$2: arm
$3: s5pc11x
$4: x210
$5: samsumg
$6: s5pc110
所以,$# = 6

(2)第23行:其实就是看BOARD_NAME变量是否有值,如果有值就维持不变;如果无值就给他赋值为$1,实际分析结果:BOARD_NAME=x210_sd

(3)第25行:如果$#小于4,则exit 1(mkconfig脚本返回1)

(4)第26行:如果$#大于6,则也返回1
.
所以:mkconfig脚本传参只能是4、5、6,如果大于6或者小于4都不行。
#定义了两个变量,一个为no,一个为空
APPEND=no	
BOARD_NAME=""

#定义了一个while循环,然后while循环中是case语句
#[ $# -gt 0 ]:$#=6,6大于0成立,然后就case中的“$1”
#$1等于x210_sd,然后--、-a、-n没有一个能匹配得上
#*匹配成立,然后跳出循环
#总结:这语句相当于啥事都没干
while [ $# -gt 0 ] ; do
	case "$1" in
	--) shift ; break ;;
	-a) shift ; APPEND=yes ;;
	-n) shift ; BOARD_NAME="${1%%_config}" ; shift ;;
	*)  break ;;
	esac
done

#以上定义BOARD_NAME="",因此[ "${BOARD_NAME}" ]不成立
#所以BOARD_NAME="$1",即BOARD_NAME=x210_sd
[ "${BOARD_NAME}" ] || BOARD_NAME="$1"

#[ $# -lt 4 ] && exit 1:如果$#小于4,则exit 1(mkconfig脚本返回1),程序执行错误
#[ $# -gt 6 ] && exit 1:如果$#大于6,则exit 1(mkconfig脚本返回1),程序执行错误
#总结:mkconfig脚本传参只能是4、5、6,如果大于6或者小于4都不行
[ $# -lt 4 ] && exit 1
[ $# -gt 6 ] && exit 1

(5)从第33行到第118行,都是在创建符号链接。为什么要创建符号链接?这些符号链接文件的存在就是整个配置过程的核心,这些符号链接文件(文件夹)的主要作用是给头文件包含等过程提供指向性连接。根本目的是让uboot具有可移植性。
uboot可移植性的实现原理:在uboot中有很多彼此平行的代码,各自属于各自不同的架构/CPU/开发板,我们在具体到一个开发板的编译时用符号连接的方式提供一个具体的名字的文件夹供编译时使用。这样就可以在配置的过程中通过不同的配置使用不同的文件,就可以正确的包含正确的文件。

(6)创建的符号链接:
第一个:在include目录下创建asm文件,指向asm-arm。(46-48行)



第二 个:在inlcude/asm-arm下创建一个arch文件,指向include/asm-arm/arch-s5pc110,这个会被后面代码删除,无任何意义


第三个:在include目录下创建regs.h文件,指向include/s5pc110.h
删除第二个。



第四个:在inlcude/asm-arm下创建一个arch文件,指向include/asm-arm/arch-s5pc11x
相当于上面第三个做了两件事情,删除了第二个和创建了第三个

第五个:在include/asm-arm下创建一个proc文件,指向include/asm-arm/proc-armv


总结:一共创建了4个符号链接。这4个符号链接将来在写代码过程中,头文件包含时非常有用。譬如一个头文件包含可能是:#include <asm/xx.h>

2.4.8.uboot配置过程详解2

(1)创建include/config.mk文件(mkconfig文件123-129行)
echo "ARCH   = $2" >  config.mk
echo "CPU    = $3" >> config.mk
echo "BOARD  = $4" >> config.mk

[ "$5" ] && [ "$5" != "NULL" ] && echo "VENDOR = $5" >> config.mk

[ "$6" ] && [ "$6" != "NULL" ] && echo "SOC    = $6" >> config.mk

(2)创建include/config.mk文件是为了让主Makefile在第133行去包含的(详解见2.4.3.3节)。

(3)思考:uboot的配置和编译过程的配合。编译的时候需要ARCH=arm、CPU=xx等这些变量来指导编译,配置的时候就是为编译阶段提供这些变量。那为什么不在Makefile中直接定义这些变量去使用,而要在mkconfig脚本中创建config.mk文件然后又在Makefile中include这些文件呢?

(4)理解这些脚本时,时刻要注意自己当前所处的路径。

(5)创建(默认情况)/追加(make -a时追加)include/config.h文件(mkconfig文件的134-141行)。


(6)这个文件里面的内容就一行#include <configs/x210_sd.h>,这个头文件是我们移植x210开发板时,对开发板的宏定义配置文件。这个文件是我们移植x210时最主要的文件。

(7)x210_sd.h文件会被用来生成一个autoconfig.mk文件,这个文件会被主Makefile引入,指导整个编译过程。这里面的这些宏定义会影响我们对uboot中大部分.c文件中一些条件编译的选择。从而实现最终的可移植性。

注意:uboot的整个配置过程,很多文件之间是有关联的(有时候这个文件是在那个文件中创建出来的;有时候这个文件被那个文件包含进去;有时候这个文件是由那个文件的内容生成的决定的)
注意:uboot中配置和编译过程,所有的文件或者全局变量都是字符串形式的(不是指的C语言字符串的概念,指的是都是字符组成的序列)。这意味着我们整个uboot的配置过程都是字符串匹配的,所以一定要细节,注意大小写,要注意不要输错字符,因为一旦错一个最后会出现一些莫名其妙的错误,很难排查,这个是uboot移植过程中新手来说最难的地方。

2.4.9.uboot的链接脚本

路径: \uboot-jiuding\board\samsung\x210\u-boot.lds

(1)uboot的链接脚本和我们之前裸机中的链接脚本并没有本质区别,只是复杂度高一些,文件多一些,使用到的技巧多一些。

(2)ENTRY(_start)用来指定整个程序的入口地址。所谓入口地址就是整个程序的开头地址,可以认为就是整个程序的第一句指令。有点像C语言中的main。

(3)之前在裸机中告诉大家,指定程序的链接地址有2种方法:一种是在Makefile中ld的flags用-Ttext 0x20000000来指定;第二种是在链接脚本的SECTIONS开头用.=0x20000000来指定。两种都可以实现相同效果。其实,这两种技巧是可以共同配合使用的,也就是说既在链接脚本中指定也在ld flags中用-Ttext来指定。两个都指定以后以-Ttext指定的为准。

(4)uboot的最终链接起始地址就是在Makefile中用-Ttext 来指定的,具体参见2.4.5.2节,注意TEXT_BASE变量。最终来源是Makefile中配置对应的命令中,在make xxx_config时得到的。

(5)在代码段中注意文件排列的顺序。指定必须放在前面部分的那些文件就是那些必须安排在前16KB内的文件,这些文件中的函数在前16KB会被调用。在后面第二部分(16KB之后)中调用的程序,前后顺序就无所谓了。

(6)链接脚本中除了.text  .data .rodata .bss段等编译工具自带的段之外,编译工具还允许我们自定义段。譬如uboot总的.u_boot_cmd段就是自定义段。自定义段很重要。
/****些代码仅为.lds的链接脚本,非c语言代码
 ****仅仅是为了观看方便********************/
 
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
/*OUTPUT_FORMAT("elf32-arm", "elf32-arm", "elf32-arm")*/
OUTPUT_ARCH(arm)  //输出构架:arm
ENTRY(_start)  //整个个程序的入口地址,可以说是第一句指令
SECTIONS
{
	. = 0x00000000;  //指定当前地址

	. = ALIGN(4);   //4字节对齐
	.text      :	//代码段:必须注意文件排列顺序
	{
	  cpu/s5pc11x/start.o	(.text)
	  cpu/s5pc11x/s5pc110/cpu_init.o	(.text)
	  board/samsung/x210/lowlevel_init.o	(.text)
          cpu/s5pc11x/onenand_cp.o      (.text)                 
          cpu/s5pc11x/nand_cp.o (.text)                     
          cpu/s5pc11x/movi.o (.text) 
          common/secure_boot.o (.text) 
	  common/ace_sha1.o (.text)
	  cpu/s5pc11x/pmic.o (.text)
	  *(.text)  //*是万能匹配,这里可以自由排列顺序的代码段
	}

	. = ALIGN(4);
	.rodata : { *(.rodata) } //只读数据段

	. = ALIGN(4);
	.data : { *(.data) }  //普通数据段

	. = ALIGN(4);
	.got : { *(.got) } //自定义的一个段

	__u_boot_cmd_start = .;
	.u_boot_cmd : { *(.u_boot_cmd) } //自定义数据段
	__u_boot_cmd_end = .;

	. = ALIGN(4);
	.mmudata : { *(.mmudata) } //自定义数据段

	. = ALIGN(4);
	__bss_start = .;
	.bss : { *(.bss) } //清零段
	_end = .;
}





 






























  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值