Linux内核构建系统之五

linux内核 专栏收录该内容
25 篇文章 1 订阅

转自:http://www.juliantec.info/julblog/yihect/linux-kernel-build-system-5

Linux内核构建系统之五

yihect | 09 元月, 2011 10:52

对另外构建目标的处理,我们使用两个例子来讲述,那就是配置内核后用来编译内核的命令:"make ARCH=arm CROSS_COMPILE=arm-linux- "和编译外部模块的命令:"make ARCH=arm CROSS_COMPILE=arm-linux- -C KERNELDIR M=dir"。之所以选取这两个 make命令 来作为例子讲述,是因为它所涉及到的关于构建系统的知识比较多,覆盖比较完整。

在开始着手讲例子之前,我们先来关注一下框架中的E部分。其实,E部分本身又可以分成两个小部分,其中之一是为了处理基本内核以及内部模块等的;其中之二是为了处理外部模块的。这两部分就像下面这样按照变量 KBUILD_EXTMOD 的取值分开:

在解压缩后的内核代码目录中直接做 make zImage

值得先说一说的是,第一个例子所涉及的代码均分布在 E1部分中,而第二个例子所涉及的代码都在 E2部分中。好,咱们先讲第一个例子中用来编译内核的命令。在这个make命令中,因为没有明确指出所要make的目标,所以实际上这个命令要处理顶层Makefile中的的缺省目标 _all。

# That's our default target when none is given on the command line
PHONY := _all
_all:
 

而在刚进入 "ifeq ($(skip-makefile),)-endif" 块后,就会有这样的代码:

# If building an external module we do not care about the all: rule
# but instead _all depend on modules
PHONY += all
ifeq ($(KBUILD_EXTMOD),)
_all: all
else
_all: modules
endif

这说明什么?如果我们处理的不是外部模块,那目标 _all 即依赖于 all,否则目标 _all 依赖于 modules。所以前面的 make 命令实际上就等价于:

"make ARCH=arm CROSS_COMPILE=arm-linux- all"

构建系统中关于目标 all 的处理,我们可以从被顶层 Makefile 所包含的架构 Makefile 中找到:

# Default target when executing plain make
ifeq ($(CONFIG_XIP_KERNEL),y)
KBUILD_IMAGE := xipImage
else
KBUILD_IMAGE := zImage
endif
 
all:    $(KBUILD_IMAGE)

同时我们还可以在顶层 Makefile 中找到关于 all 目标的规则(两条):

# The all: target is the default when no target is given on the
# command line.
# This allow a user to issue only 'make' to build a kernel including modules
# Defaults vmlinux but it is usually overridden in the arch makefile
all: vmlinux

以及

ifdef CONFIG_MODULES
# By default, build modules as well
all: modules
....
else # CONFIG_MODULES
....
endif # CONFIG_MODULES

因为对于 s3c2410_defconfig 的默认配置来说,CONFIG_XIP_KERNEL变量没有被定义,而变量 CONFIG_MODULES 被定义为 y 。所以,实际上构建系统对 all 目标的处理,就是先后处理这样三个目标:vmlinux zImage 和 modules。

首先,我们来看看对目标 vmlinux 的处理。在顶层Makefile中,在框架代码的E1部分中可以找到:

# vmlinux image - including updated kernel symbols
vmlinux: $(vmlinux-lds) $(vmlinux-init) $(vmlinux-main) vmlinux.o $(kallsyms.o) FORCE
ifdef CONFIG_HEADERS_CHECK
        $(Q)$(MAKE) -f $(srctree)/Makefile headers_check
endif
ifdef CONFIG_SAMPLES
        $(Q)$(MAKE) $(build)=samples
endif
ifdef CONFIG_BUILD_DOCSRC
        $(Q)$(MAKE) $(build)=Documentation
endif
        $(call vmlinux-modpost)
        echo 'vmlinux-modpost := $(vmlinux-modpost)' > vmlinux-modpost.cmd
        $(call if_changed_rule,vmlinux__)
        $(Q)rm -f .old_version

从上面的代码可见,vmlinux 依赖于 $(vmlinux-lds) $(vmlinux-init) $(vmlinux-main) vmlinux.o $(kallsyms.o), 而从下面的代码中又可以看出 $(vmlinux-lds)、$(vmlinux-init)和$(vmlinux-main)等三个目标又依赖于$(vmlinux-dirs)。

# The actual objects are generated when descending, 
# make sure no implicit rule kicks in
$(sort $(vmlinux-init) $(vmlinux-main)) $(vmlinux-lds): $(vmlinux-dirs) ;

在继续讨论之前,我们先看看前面vmlinux对应规则中所碰到过的变量定义。我们一起把它们列出来:

vmlinux-lds  := arch/$(SRCARCH)/kernel/vmlinux.lds
vmlinux-init := $(head-y) $(init-y)
vmlinux-main := $(core-y) $(libs-y) $(drivers-y) $(net-y)

变量 vmlinux-lds 指代的是 arch/arm/kernel/ 目录中的 vmlinux.lds 文件,大家知道lds文件是用来指导连接器做连接的脚本了。关于它的目的我们就这里不做具体描述,我们仅说说内核构建系统是如何处理 .lds 文件的。在内核里面,它是由同目录下的同名 .lds.S 文件所预处理出来的。其对应的规则定义在文件 scripts/Makefile.build 中:

连接器脚本的预处理

观察上面规则中的命令,就是以 cpp_lds_S 为第一个参数,调用了函数 if_changed_dep,该函数(其实就是一个make变量)定义在文件 scripts/Kbuild.include 中:

# Execute command if command has changed or prerequisite(s) are updated.
#       
if_changed = $(if $(strip $(any-prereq) $(arg-check)),                       \
        @set -e;                                                             \
        $(echo-cmd) $(cmd_$(1));                                             \
        echo 'cmd_$@ := $(make-cmd)' > $(dot-target).cmd)
 
# Execute the command and also postprocess generated .d dependencies file.
if_changed_dep = $(if $(strip $(any-prereq) $(arg-check) ),                  \
        @set -e;                                                             \
        $(echo-cmd) $(cmd_$(1));                                             \
        scripts/basic/fixdep $(depfile) $@ '$(make-cmd)' > $(dot-target).tmp;\
        rm -f $(depfile);                                                    \
        mv -f $(dot-target).tmp $(dot-target).cmd)
 
# Usage: $(call if_changed_rule,foo)
# Will check if $(cmd_foo) or any of the prerequisites changed,
# and if so will execute $(rule_foo).
if_changed_rule = $(if $(strip $(any-prereq) $(arg-check) ),                 \
        @set -e;                                                             \
        $(rule_$(1)))

从上面代码可以看出,该函数连同另外两个函数:if_changed 和 if_changed_rule 一起构成一个系列。他们的用法一致,都是 $(call if_XXXX_XX, YYYY) 的形式,调用的结果都是将函数if_XXXX_XX中的$(1)部分用 YYYY 去取代而构成的if判断式,这种调用形式一般放在一个规则的命令部分里面。

其中最简单的就是 if_changed,当发现规则的依赖有被更新了、或者编译该规则对应目标的命令行发生改变了,它就先用 $(echo-cmd) 回显出新的命令$(cmd_$(1)),接着执行命令$(cmd_$(1)),最后再将该命令写到一个叫做 $(dot-target).cmd 的临时文件中去,以方便下一次检查命令行是否有变的时候用。变量 dot-target 定义成 .targetname 的形式,如下:

###
# Name of target with a '.' as filename prefix. foo/bar.o => foo/.bar.o
dot-target = $(dir $@).$(notdir $@)

那如何去检查规则的依赖文件被更新了,以及检查编译该规则对应目标的命令行发生改变了的呢?答案就是下面的两个定义:

# Find any prerequisites that is newer than target or that does not exist.
# PHONY targets skipped in both cases.
any-prereq = $(filter-out $(PHONY),$?) $(filter-out $(PHONY) $(wildcard $^),$^)

ifneq ($(KBUILD_NOCMDDEP),1)
# Check if both arguments has same arguments. Result is empty string if equal.
# User may override this check using make KBUILD_NOCMDDEP=1
arg-check = $(strip $(filter-out $(cmd_$(1)), $(cmd_$@)) \
                    $(filter-out $(cmd_$@),   $(cmd_$(1))) )
endif

在 any-prereq 变量定义中,$(filter-out $(PHONY),$?) 指代的是那些比目标还新的依赖文件,而 $(filter-out $(PHONY) $(wildcard $^),$^) 指的是那些当前还不存在的依赖文件。另外注意 arg-check 变量定义中比较新老命令的方式。假设我们现在有下面这样一条规则调用了函数 if_changed:

使用 if_changed 的例子

那么上面比较的是变量 cmd_link_target 所指代的新命令和变量 cmd_target 所指代的老命令。而这个老命令就是被 if_changed 写入文件 .target.cmd 的。可以想见,内核构建系统必定在某个地方将这些包含老命令的 .*.cmd 读入进来。没错,读入代码可以找到在顶层Makefile中:

# read all saved command lines
 
targets := $(wildcard $(sort $(targets)))
cmd_files := $(wildcard .*.cmd $(foreach f,$(targets),$(dir $(f)).$(notdir $(f)).cmd))
 
ifneq ($(cmd_files),)
  $(cmd_files): ;       # Do not try to update included dependency files
  include $(cmd_files)
endif

注意了,上面只包含了处理那些列在变量 targets 中的目标的老命令。所以如果你想让构建系统也帮你比较新老命令,并若发现其中有区别就帮你处理的话,你需要将你的目标也列入 targets 变量中。另外,因为构建系统中目录 scripts 下的很多Makfile和顶层Makefile是独立运行的,所以在目录 scripts 下面,像在 Makefile.build、Makefile.headersinst、Makefile.modpost以及Makefile.fwinst等文件中,你也可以找到类似的读入代码。

if_changed_dep 函数和 if_changed 差不多,所不同的是它用 fixdep 工具程序处理了依赖文件 *.d ,并将依赖信息也一并写入到文件 .targetname.cmd 文件中去。可以说依赖处理是整个内核构建系统中最难理解的部分,我们后面会花一点专门的篇幅来讨论它。if_changed_rule 其实也和 if_changed 差不多,只不过它直接调用了命令 rule_$(1),而不是 cmd_$(1) 所指代的命令。if_changed_XXX 系列在内核构建系统中用的比较多,还请注意掌握。

回过头去看看变量 vmlinux-init 和 vmlinux-main 的定义。其中变量 head-y 通常被定义在架构相关的Makefile中,它通常包含那些需要放在编译出来的内核映像文件前面的对象文件。比方在arch/arm/Makefile中定义的 head-y:

head-y := arch/arm/kernel/head$(MMUEXT).o arch/arm/kernel/init_task.o

除了 head-y 之外,其他的变量最开始的时候都是作为目录定义在顶层Makefile中:

ifeq ($(KBUILD_EXTMOD),)
...
 
# Objects we will link into vmlinux / subdirs we need to visit
init-y          := init/
drivers-y       := drivers/ sound/ firmware/
net-y           := net/
libs-y          := lib/
core-y          := usr/
endif # KBUILD_EXTMOD
 
...
core-y          += kernel/ mm/ fs/ ipc/ security/ crypto/ block/

注意,在架构相关的Makefile中,通常都会往这些变量中追加架构相关的代码目录。这些目录中包含架构相关的需要编译进内核的代码,而顶层Makefile中的这些都是架构无关的代码。举例说来,比方在 arch/arm/Makefile 中有这样的代码:

# If we have a machine-specific directory, then include it in the build.
core-y                          += arch/arm/kernel/ arch/arm/mm/ arch/arm/common/
core-y                          += $(machdirs) $(platdirs)
core-$(CONFIG_FPE_NWFPE)        += arch/arm/nwfpe/
core-$(CONFIG_FPE_FASTFPE)      += $(FASTFPE_OBJ)
core-$(CONFIG_VFP)              += arch/arm/vfp/
 
drivers-$(CONFIG_OPROFILE)      += arch/arm/oprofile/
 
libs-y                          := arch/arm/lib/ $(libs-y)

接下来,内核构建系统又会在顶层Makefile中重新设置这些指代不同目录的变量,如下:

vmlinux-dirs    := $(patsubst %/,%,$(filter %/, $(init-y) $(init-m) \
                     $(core-y) $(core-m) $(drivers-y) $(drivers-m) \
                     $(net-y) $(net-m) $(libs-y) $(libs-m)))
 
vmlinux-alldirs := $(sort $(vmlinux-dirs) $(patsubst %/,%,$(filter %/, \
                     $(init-n) $(init-) \
                     $(core-n) $(core-) $(drivers-n) $(drivers-) \
                     $(net-n)  $(net-)  $(libs-n)    $(libs-))))
 
init-y          := $(patsubst %/, %/built-in.o, $(init-y))
core-y          := $(patsubst %/, %/built-in.o, $(core-y))
drivers-y       := $(patsubst %/, %/built-in.o, $(drivers-y))
net-y           := $(patsubst %/, %/built-in.o, $(net-y))
libs-y1         := $(patsubst %/, %/lib.a, $(libs-y))
libs-y2         := $(patsubst %/, %/built-in.o, $(libs-y))
libs-y          := $(libs-y1) $(libs-y2)

设置的结果,就是原先指代目录的这些变量都变成了指代文件的变量。这些文件的名称,大部分是built-in.o。你可以看到几乎任何一个目录中都应该会有这样的文件。另外,还有一部分名为 lib.a 的文件。你看到后面会知道,内核构建系统会一一产生这些对象文件和库代码,连同前面的变量head-y指代的对象文件,还有其他的对象文件,构建系统会将他们连接起来,构成基本的Linux内核映像 vmlinux。

注意看,在上面的代码中,在设置这些目录之前,构建系统会将所有可能的代码目录保存在变量 vmlinux-dirs 。之所以会有 -y 和 -m 的区别,那是因为将这些目录赋值给这些目录变量的时候,常会使用配置选项 CONFIG_XXXXX 之类的。比方前面在 arch/arm/Makefile 中给 core-$(CONFIG_VFP) 赋值的形式。

由于我们在前面已经看到 $(vmlinux-lds)、$(vmlinux-init)、$(vmlinux-main)等目标依赖于 $(vmlinux-dirs)。所以对目标$(vmlinux-dirs)处理的规则就成为关键。我们从顶层 Makefile 中把它找出来:

 
# Handle descending into subdirectories listed in $(vmlinux-dirs)
# Preset locale variables to speed up the build process. Limit locale
# tweaks to this spot to avoid wrong language settings when running
# make menuconfig etc.
# Error messages still appears in the original language
 
PHONY += $(vmlinux-dirs)
$(vmlinux-dirs): prepare scripts
        $(Q)$(MAKE) $(build)=$@

在看这条规则的命令之前,我们先看看它有两个依赖:prepare 和 scripts。下面是顶层Makefile中prepare 相关的代码:

# Things we need to do before we recursively start building the kernel
# or the modules are listed in "prepare".
# A multi level approach is used. prepareN is processed before prepareN-1.
# archprepare is used in arch Makefiles and when processed asm symlink,
# version.h and scripts_basic is processed / created.
 
# Listed in dependency order
PHONY += prepare archprepare prepare0 prepare1 prepare2 prepare3
 
# prepare3 is used to check if we are building in a separate output directory,
# and if so do:
# 1) Check that make has not been executed in the kernel src $(srctree)
# 2) Create the include2 directory, used for the second asm symlink
prepare3: include/config/kernel.release
ifneq ($(KBUILD_SRC),)
        @$(kecho) '  Using $(srctree) as source for kernel'
        $(Q)if [ -f $(srctree)/.config -o -d $(srctree)/include/config ]; then \
                echo "  $(srctree) is not clean, please run 'make mrproper'";\
                echo "  in the '$(srctree)' directory.";\
                /bin/false; \
        fi;
        $(Q)if [ ! -d include2 ]; then                                  \
            mkdir -p include2;                                          \
            ln -fsn $(srctree)/include/asm-$(SRCARCH) include2/asm;     \
        fi
endif
 
# prepare2 creates a makefile if using a separate output directory
prepare2: prepare3 outputmakefile
 
prepare1: prepare2 include/linux/version.h include/linux/utsrelease.h \
                   include/asm include/config/auto.conf
        $(cmd_crmodverdir)
 
archprepare: prepare1 scripts_basic
 
prepare0: archprepare FORCE
        $(Q)$(MAKE) $(build)=.
        $(Q)$(MAKE) $(build)=. missing-syscalls
 
# All the preparing..
prepare: prepare0

构建系统处理 prepare 及相关目标的目的是为了后面真正进入各子目录编译内核或模块做准备。

在这些目标的处理中,最重要的莫过于对 prepare0 目标的处理了。注意目标 prepare0 对应规则的命令,它们会调用 scripts/Makefile.build,并且在其中包含顶层目录中的 Kbuild 文件,其功能分别是由 arch/arm/kerel/asm-offset.c 文件生成 include/asm-arm/asm-offset.h 文件以及使用 scripts/checksyscalls.sh 来检查是否还有未实现的系统调用(检查时,以i386所实现的系统调用为比较依据)。

至于prepare相关处理的其他部分,限于本文的篇幅过于庞大,这里就先略去不讲了。有兴趣的朋友可以参与我们的mail list进行讨论。

至于 $(vmlinux-dires) 所依赖的另外一个目标 scripts。它就是为了在真正进入各子目录编译内核或者模块之前,在目录 scripts 中准备好若干工具。其规则如下:

# Additional helpers built in scripts/
# Carefully list dependencies so we do not try to build scripts twice
# in parallel
PHONY += scripts
scripts: scripts_basic include/config/auto.conf
        $(Q)$(MAKE) $(build)=$(@)

回到我们对 $(vmlinux-dirs) 目标进行处理的命令上来。 命令 "$(Q)$(MAKE) $(build)=$@" 其实就是调用 scripts/Makefile.Build 文件,并依次在其中包含变量 $(vmlinux-dirs) 所对应各目录的 Kbuild/Makefile,最终在各目录中编译出不同的对象文件来(一系列的.o文件和.a文件)。这到底是如何实现的?我们先看命令 "$(Q)$(MAKE) $(build)=$@",简化出来就是:

make -f scripts/Makefile.build obj=$@

由于上面这个命令中并没有指定要处理什么具体的目标,所以此make命令实际上是在处理 scripts/Makefile.build 中的默认目标:__build,我们列出具体的规则:

__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \
         $(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \
         $(subdir-ym) $(always)
        echo 'KBUILD_MODULES := $(KBUILD_MODULES)' >> modules_builtin.cmd
        echo 'KBUILD_BUILTIN := $(KBUILD_BUILTIN)' >> modules_builtin.cmd
        @:

这条规则没有什么命令,但是却有很多依赖。我们说正是这些依赖指明了内核构建系统进入到各个子目录(由vmlinux-dirs中列出)后所要编译处理的各种目标。我们通过分析这些依赖来搞清楚这些目标有哪些。在此之前,我们观察到两个变量 KBUILD_BUILTIN 和 KBUILD_MODULES,它们是在顶层Makefile中就初始化好了并导出来的,其代码如下:

# Decide whether to build built-in, modular, or both.
# Normally, just do built-in.
 
KBUILD_MODULES :=
KBUILD_BUILTIN := 1
 
#       If we have only "make modules", don't compile built-in objects.
#       When we're building modules with modversions, we need to consider
#       the built-in objects during the descend as well, in order to
#       make sure the checksums are up to date before we record them.
 
ifeq ($(MAKECMDGOALS),modules)
  KBUILD_BUILTIN := $(if $(CONFIG_MODVERSIONS),1)
endif
 
#       If we have "make <whatever /> modules", compile modules
#       in addition to whatever we do anyway.
#       Just "make" or "make all" shall build modules as well
 
ifneq ($(filter all _all modules,$(MAKECMDGOALS)),)
  KBUILD_MODULES := 1
endif
 
ifeq ($(MAKECMDGOALS),)
  KBUILD_MODULES := 1
endif
 
export KBUILD_MODULES KBUILD_BUILTIN

这两个变量用于记录在用内核构建系统进行make过程中是否处理基本内核(basic kernel,namely vmlinux),以及是否处理包含在内核代码树中的内部模块(为了编译内部模块,你需要在配置的时候选择m,而不是y)。如果要处理基本内核,那变量KBUILD_BUILTIN被设置为1;如果要编译内部模块,那变量KBUILD_MODULES被设置为1。对于我们所举的例子:"make ARCH=arm CROSS_COMPILE=arm-linux-",由于既要处理基本内核又要处理内部模块(all既依赖于vmlinux,又依赖于modules),所以此两变量均取1的值。

好,回到我们前面处理 __build 的那条规则上面来,我们可以开始分析构建系统进入各子目录后,到底要处理哪些目标了,总共有这么几项:

1) 由于有变量 lib-target 的定义:

ifneq ($(strip $(lib-y) $(lib-m) $(lib-n) $(lib-)),)
lib-target := $(obj)/lib.a
endif

所以构建系统有可能要编译出各子目录下的 lib.a。注意其要不要编译,是看在各子目录下的Makefile中有无"lib-*"之类变量的定义。

对于 lib-target 的处理规则,定义在 scripts/Makefile.build中,列出如下:

 
#
# Rule to compile a set of .o files into one .o file
#
ifdef builtin-target
quiet_cmd_link_o_target = LD      $@
# If the list of objects to link is empty, just create an empty built-in.o
cmd_link_o_target = $(if $(strip $(obj-y)),\
                      $(LD) $(ld_flags) -r -o $@ $(filter $(obj-y), $^) \
                      $(cmd_secanalysis),\
                      rm -f $@; $(AR) rcs $@)
 
$(builtin-target): $(obj-y) FORCE
        $(call if_changed,link_o_target)
 
targets += $(builtin-target)
endif # builtin-target

上面的规则表明构建系统会使用归档工具将变量 lib-y 中列出的所有对象文件归档成lib.a文件。而在整个 Linux 内核代码树中,只有两类(个)目录下的 Kbuild/Makefile 包含有对 lib-y 的定义,一个是内河代码树下的lib/目录,另外一个arch/$(ARCH)/lib/目录。我们等会会举目录arch/arm/lib/下的Makefile来说明,这里先看看构建系统在进行归档之前,会对变量 lib-y做何种处理。在 scripts/Makefile.lib 中,有这样的代码:

# Libraries are always collected in one lib file.
# Filter out objects already built-in
 
lib-y := $(filter-out $(obj-y), $(sort $(lib-y) $(lib-m)))
....
lib-y           := $(addprefix $(obj)/,$(lib-y))

上面代码将列在 lib-y/lib-m 中,但同时又定义在 obj-y 中的文件过滤掉;并在 lib-y 所包含文件名前面加上目录名。好,我们拿 arch/arm/lib/Makefile 中的 lib-* 定义进行举例:

 
lib-y           := backtrace.o changebit.o csumipv6.o csumpartial.o   \
                   csumpartialcopy.o csumpartialcopyuser.o clearbit.o \
                   delay.o findbit.o memchr.o memcpy.o                \
                   memmove.o memset.o memzero.o setbit.o              \
                   strncpy_from_user.o strnlen_user.o                 \
                   strchr.o strrchr.o                                 \
                   testchangebit.o testclearbit.o testsetbit.o        \
                   ashldi3.o ashrdi3.o lshrdi3.o muldi3.o             \
                   ucmpdi2.o lib1funcs.o div64.o sha1.o               \
                   io-readsb.o io-writesb.o io-readsl.o io-writesl.o
 
mmu-y   := clear_user.o copy_page.o getuser.o putuser.o
 
... #  others assignment of mmu-y variable
 
# using lib_ here won't override already available weak symbols
obj-$(CONFIG_UACCESS_WITH_MEMCPY) += uaccess_with_memcpy.o
 
lib-$(CONFIG_MMU) += $(mmu-y)
 
ifeq ($(CONFIG_CPU_32v3),y)
  lib-y += io-readsw-armv3.o io-writesw-armv3.o
else
  lib-y += io-readsw-armv4.o io-writesw-armv4.o
endif
 
lib-$(CONFIG_ARCH_RPC)          += ecard.o io-acorn.o floppydma.o
lib-$(CONFIG_ARCH_L7200)        += io-acorn.o
lib-$(CONFIG_ARCH_SHARK)        += io-shark.o
 

上面代码中,对 lib-y 进行很多 *.o 的赋值。构建系统会编译对应的同名C程序文件或同名汇编程序文件编译成对象文件,并将这些对象文件链接成 lib.a。


  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值