《操作系统真象还原》学习笔记:第四章保护模式入门

一.为什么要有保护模式

1)实模式下操作系统和用户程序属于同一特权级
2)用户程序所引用的地址都是指向真实物理地址的,也就是说逻辑地址等于物理地址,实实在在的指哪打哪
3)用户程序可以自由修改段基址,可以访问所有内存
4)访问超过64kb的内存区域时要切换段基址,转来转去容易晕乎
5)一次只能运行一个程序,无法充分利用计算机资源
6)共20条地址线,最大可访问的内存为1MB

二.初见保护模式

1.保护模式之寄存器

  • 保护模式大大提高了安全性,其中很大一部分的安全就体现在了内存段的描述方面,偏移地址还是和实模式下一样,但是段寄存器保存的再也不是段基址了,为了更安全添加了约束条件,这些“约束条件”便是对内存段的描述信息,由于信息太多,一个寄存器肯定放不下,所以专门找了数据结构——全局描述符表,其中每一个表项都称为段描述符,其大小为64字节,用来描述各内存段的起始地址、大小、权限等信息,全局描述符表很大所以放在内存中,由GDTR寄存器指向它。

  • 段寄存器里面保存的内容叫“选择子”,selector,选择子其实就是个数,用来索引全局描述符表中的段描述符。把全局描述符表当成数组,选择子就像是数组的下标一样

2.保护模式之寻址运行模式

  • [bis16] 是告诉编译器,下面的代码帮我编译成16位的机器码
  • [bis32]是告诉编译器,下面的代码帮我编译成32位的机器码

3.保护模式之指令扩展

push虽说可以压入8位立即数,但实际上,对CPU来说,出于对齐的考虑,操作数要么是16位,要么是32位,所以8位立即数会扩展成各模式下的默认操作数宽度,即实模式下8位立即数扩展成16位后再入栈,保护模式下扩展成32位后再入栈


在实模式下:

  • 当压入8位立即数时, 由于实模式下默认操作数是16位,CPU将其扩展为16后入栈,esp指针减2
  • 当压入16位立即数时,CPU直接压入2字节,sp-2
  • 当压入32位立即数时,CPU直接压入4字节,sp-4

在保护模式下

  • 当压入8位立即数时, 由于保护模式下默认操作数是32位,CPU将其扩展为32后入栈,esp指针减4
  • 当压入16位立即数时,CPU直接压入2字节,esp-2
  • 当压入32位立即数时,CPU直接压入4字节,esp-4

三.全局描述符表

1.段描述符

实模式下存在的问题:

  • 实模式下的用户程序可以随意破坏存储代码的内存区域,所以要添加个内存段类型属性来阻止这种行为。
  • 实模式下的用户程序和操作系统是同一级别的,所以要添加个特权级属性来阻止这种行为。

其次,是一些访问内存段的必要属性条件:

  • 内存段是一片内存区域,访问内存就要提供段基址,所以要有段基址属性。
  • 为了限制程序访问内存的范围,还要对段大小进行约束,所以要有段界限属性

在这里插入图片描述
段描述符是8字节大小,图中为了方便展示,才“人为的”分成了低32位和高32位。其实他们不能分成两部分,必须是连续的8字节,这样CPU才能读取到正确的段信息

  • 段基址:保护模式下地址总线宽度是32位,段基址需要用32位地址来表示
  • 段界限:20位,段界限表示段边界的扩展最值。内存访问需要用到“段基址:段内偏移地址”,段界限其实是用来限制段内偏移地址的
    实际的段界限边界值=(描述符中段界限+1)*(段界限的颗粒大小:4KB或1)-1

在这里插入图片描述

  • TYEP字段:4位,用来指定本描述符的类型。段描述符在CPU眼里分为两大类,要么描述的是系统段,要么描述的是数据段,这是由段描述符中S位决定的,用它指示是否是系统段。
    在CPU眼里,凡是硬件运行需要用到的东西都可以称之为系统,凡是软件(操作系统也属于软件)需要的东西都称为数据,我们主要关注非系统段
    -A位表示accessed位,这是由CPU来设置的,每当该段被CPU访问后,CPU就将此位置1。
    -C表示一致性代码
    -R表示可读,R为1表示可读,R为0表示不可读
    -X表示该段是否可执行。X为1表示可执行,0表示不可执行
    -E用来标识段的扩展方向,E为0表示向上扩展,即地址越来越高,通常用于代码段和数据段。E为1表示向下扩展,地址越来越低,通常用于栈段。
    -W指是否可写,W为1表示可写,0表示不可写

  • S字段:S为1表示数据段,S为0表示系统段
  • DPL字段:描述符特权级,这两位能表示4种特权级,分别是0、1、2、3、4级特权。数字越小权力越大
  • P字段:段是否存在于内存中,存在P为1,不存在P位0。P字段是由CPU来检查的。
  • AVL字段:可用的,是针对用户来说的,也就是操作系统可以随意用此位。
  • L字段:用来设置是否64位代码段。L为1表示64位代码段,L为0表示32位代码段
  • D/B字段:用来指示有效地址(段内偏移地址)及操作数的大小。
    对于代码段来说,此位是D位,若D为0,表示指令中的有效地址和操作数都是16位,指令有效地址用IP寄存器。若D为0,表示指令中的有效地址及操作数为32位,指令有效地址用EIP寄存器
    对于栈段来说,此位是B位,用来指定操作数大小。若B为0,使用SP寄存器,也就是栈的起始地址是16位寄存器的最大寻址范围,0xFFFF。若B为1,使用的是ESP寄存器,也就是栈的起始地址是32寄存器的最大寻址范围,0xFFFFFFFF。
  • G字段:粒度,它与段界限一起来决定段的大小。若G为0,表示段界限的单位是1字节,这样的段最大是2的20次方*1字节,即1MB。若G为1,表示段界限的单位是4KB,这样段最大是2的20次方*4KB,即4GB。

2.全局描述符表GDT

  • 段描述符存放在全局描述符表GDT中,全局描述符表位于内存中,需要专门的寄存器指向它,这个寄存器便是GDTR,专门用来存储GDT的内存地址及大小,GDTR是个48位寄存器。对此寄存器的访问用lgdt指令。
    在这在这里插入图片描述
里插入图片描述
  • lgdt指令格式是:lgdt48位内存数据
  • 这48位内存数据划分为两部分,其中前16位是GDT以字节为单位的界限值,其范围是2的16次方等于65536个字节,每个描述符的大小是8字节,故GDT最多可容纳的描述符数量是65536/8=8192个,即GDT中可容纳8192个段或门。后32位是GDT起始地址

3.段选择子

  • 段寄存器CS、DS、ES、FS、GS、SS,在实模式下时,段中存储的是段基址,即内存段的起始地址。而在保护模式下时,由于段基址已经存入段描述符中,所以段寄存器中再存放段基址是没有意义的,在段寄存器中存放的是一个叫做段选择子的东西——selector。

在这里插入图片描述

  • 段选择子:第0~1位用来存储RPL。第2位是TI位,用来指示选择子是在GDT中,还是在LDT中索引描述符。第3~15位是描述符的索引值,用此值在GDT中索引描述符,由于选择子的索引部分是13位,即2的13次方等于8192,故最多可索引8192个段,这和GDT中最多定义8192个描述符吻合。
  • 注意:GDT中的第0个段描述符是不可用的,若选择到了GDT中的第0个描述符,处理器将发出异常

4.局部描述符表LDT

  • 局部描述符表LDT,它是CPU厂商为硬件一级原生支持多任务而创造的表,按照CPU的设想,一个任务对应一个LDT。其实现代操作系统中很少有用LDT。
  • CPU厂商建议每个任务的私有内存段都应该放到自己的段描述符表中,该表就是LDT。

四. 打开A20地址线

实模式下内存访问是采用“段基址:段内偏移地址”的形式,如果段基址和段内偏移地址都为16位的最大值,即0xFFFF:0XFFFF,最大地址是0xFFFF0+0xFFFF=0X10FFEF。由于实模式下的地址线是20位,最大寻址空间1MB,即0x00000~0xFFFFF。超出1MB的部分在物理内存没有与之对应的部分,为了让“段基址:段内偏移地址”策略继续可用,CPU采取的做法是将超过1MB的部分自动回绕到0地址

  • 如果A20Gate被打开,当访问到0x100000~0x10FFEF之间的地址时,CPU将真正访问这块物理内存。
  • 如果A20Gate被禁止,当访问x100000~0x10FFEF之间的地址时,CPU将采用8086/8088的地址回绕

打开A20Gate的方式,将端口0x92第一位置1

in al,0x92
or al,0000_0010B
out 0x92,al

五. 保护模式的开关,CR0寄存器的PE位

在这里插入图片描述
在这里插入图片描述

  • PE为0表示在实模式下运行,PE为1表示在保护模式下运行,我们需要把此位置1:

    mov eax,cr0
    or eax,0x00000001
    mov cr0,eax
    

六.让我们进入保护模式

;mbr.s
;主引导程序 
;------------------------------------------------------------
%include "boot.inc"
SECTION MBR vstart=0x7c00         
   mov ax,0      
   mov ds,ax
   mov es,ax
   mov ss,ax
   mov fs,ax            ;将ax,dx,es,ss,fs初始化为0
   mov sp,0x7c00
   mov ax,0xb800
   mov gs,ax
  
; 清屏 利用0x06号功能,上卷全部行,则可清屏。
; -----------------------------------------------------------
;INT 0x10   功能号:0x06	   功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
   mov     ax, 0x600
   mov     bx, 0x700
   mov     cx, 0           ; 左上角: (0, 0)
   mov     dx, 0x184f	   ; 右下角: (80,25),
			   ; VGA文本模式中,一行只能容纳80个字符,25行。
			   ; 下标从0开始,所以0x18=24,0x4f=79
   int     0x10            ; int 0x10
;输出背景色绿色,前景色红色,并且跳动的字符串”1 MBR“
   mov byte [gs:0x00],'1'
   mov byte [gs:0x01],0xA4
   
   mov byte [gs:0x02],' '
   mov byte [gs:0x03],0xA4
   
   mov byte [gs:0x04],'M'
   mov byte [gs:0x05],0xA4
   
   mov byte [gs:0x06],'B'
   mov byte [gs:0x07],0xA4
   
   mov byte [gs:0x08],'R'
   mov byte [gs:0x09],0xA4
   
   mov eax, LOADER_START_SECTOR    ;loader加载到的扇区
   mov bx , LOADER_BASE_ADDR       ;lodaer写入的地址
   mov cx , 4					   ;待读入的扇区数
   call rd_disk_m_16

   jmp LOADER_BASE_ADDR

;-----------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;-----------------------------------------------
   mov esi,eax		;备份eax
   mov di,cx		;备份cx
;读取硬盘:
;1步:设置要读取的扇区数
   mov dx,0x1f2
   mov al,cl
   out dx,al		;读取的扇区数
   mov eax,esi		;恢复ax
;2步:将LBA地址存入0x1f3~0x1f6
  ;LBA地址7~0位写入0x1f3
  mov dx,0x1f3
  out dx,al
  
  ;LBA地址15~8位写入0x1f4
  mov cl,8
  shr eax,cl
  mov dx,0x1f4
  out dx,al
  
  ;LBA地址23~16位写入0x1f5
  ;mov cl,8
  shr eax,cl
  mov dx,0x1f5
  out dx,al
  
  ;LBA地址24~27位写入端口0x1f64位,高四位设置主盘和LBA模式
  shr eax,cl
  ;and al,0x0f
  or al,0xe0
  mov dx,0x1f6
  out dx,al
  
;3步:向0x1f7端口写入读扇区命令,0x20
  mov dx,0x1f7
  mov al,0x20
  out dx,al

;4步:检查硬盘状态
.not_ready:
	;0X17F同一端口,写时表示写入命令字,读时表示读入硬盘状态
	nop
	;mov dx,0x1f7
	in al,dx
	and al,0x88   ;0x88进行与操作,保留第3位和第7位
	cmp al,0x08   ;0x08表示硬盘准备就绪
	jnz .not_ready
	

;5步:从0x1f0端口读取数据
	mov ax,di
	mov dx,256
	mul dx
	mov cx,ax
;di为读取的扇区数,一个扇区512个字节,每次读入的两个字节共需 di*512/2次,所以是di*256
	mov dx,0x1f0
.go_on_read:
	in ax,dx
	mov [bx],ax
	add bx,2
	loop .go_on_read
	ret

   times 510-($-$$) db 0
   db 0x55,0xaa

;boot.inc
;-------------------loader和kernel--------------------
LOADER_BASE_ADDR    equ 0x900  ;LOADER加载地址
LOADER_START_SECTOR equ 0x2	   ;LOADER开始的扇区

;------------------- gdt描述符属性 ---------------------
DESC_G_4K          equ  1_00000000000000000000000b
DESC_D_32	       equ  1_0000000000000000000000b
DESC_L		       equ  0_000000000000000000000b
DESC_AVL           equ  0_00000000000000000000b
DESC_LIMIT_CODE2   equ  1111_0000000000000000b
DESC_LIMIT_DATA2   equ  DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2  equ  0000_0000000000000000b
DESC_P 			   equ  1_000000000000000b
DESC_DPL_0		   equ  00_0000000000000b
DESC_DPL_1		   equ  01_0000000000000b
DESC_DPL_2         equ  10_0000000000000b
DESC_DPL_3         equ  11_0000000000000b
DESC_S_CODE        equ  1_000000000000b
DESC_S_DATA		   equ  DESC_S_CODE
DESC_S_SYS         equ  0_000000000000b
DESC_TYPE_CODE	   equ  1000_00000000b
;代码段可执行的,非一致性,不可读,已访问位a清0
DESC_TYPE_DATA     equ  0010_00000000b
;数据段不可执行的,向上扩展,可写,已访问位a清0

DESC_CODE_HIGH4    equ (0x00<<24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE+ 0x00

DESC_DATA_HIGH4    equ (0x00<<24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA+ 0x00

DESC_VIDEO_HIGH4   equ (0x00<<24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA+ 0x0b

;----------------------选择子属性-------------------
RPL0   equ 00b
RPL1   equ 01b
RPL2   equ 10b
RPL3   equ 11b
TI_GDT equ 000b
TI_LDT equ 100b

;loader.s

%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
jmp  near loader_start 		    	; 此处的物理地址是:
   
;构建gdt及其内部的描述符
   GDT_BASE:   		  dd    0x00000000 
	       	   		  dd    0x00000000

   CODE_DESC:  		  dd    0x0000FFFF 
	           		  dd    DESC_CODE_HIGH4

   DATA_STACK_DESC:  dd    0x0000FFFF
		    		 dd    DESC_DATA_HIGH4

   VIDEO_DESC: 		 dd    0x80000007	       ;limit=(0xbffff-0xb8000)/4k=0x7
	       			 dd    DESC_VIDEO_HIGH4  ; 此时dpl已改为0

   GDT_SIZE   equ   $ - GDT_BASE  ;GDT的大小
   GDT_LIMIT   equ   GDT_SIZE -	1  ;GDT的界限
   
   times 60 dq 0					 ; 此处预留60个描述符的slot
   SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0         ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
   SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0	 ; 同上
   SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0	 ; 同上 

   ;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址

   gdt_ptr  dw  GDT_LIMIT 
	    dd  GDT_BASE

loader_start:
   mov byte [gs:0xA0],'L'
   mov byte [gs:0xA1],0xA4
   
   mov byte [gs:0xA2],'O'
   mov byte [gs:0xA3],0xA4
   
   mov byte [gs:0xA4],'A'
   mov byte [gs:0xA5],0xA4
   
   mov byte [gs:0xA6],'D'
   mov byte [gs:0xA7],0xA4
   
   mov byte [gs:0xA8],'E'
   mov byte [gs:0xA9],0xA4
   
   mov byte [gs:0xAA],'R'
   mov byte [gs:0xAB],0xA4

;----------------------------------------   准备进入保护模式   ------------------------------------------
									;1 打开A20
									;2 加载gdt
									;3 将cr0的pe位置1


   ;-----------------  打开A20  ----------------
   in al,0x92
   or al,0000_0010B
   out 0x92,al

   ;-----------------  加载GDT  ----------------
   lgdt [gdt_ptr]


   ;-----------------  cr0第0位置1  ----------------
   mov eax, cr0
   or eax, 0x00000001
   mov cr0, eax
   

   jmp  dword SELECTOR_CODE:p_mode_start	     ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
					     ; 这将导致之前做的预测失效,从而起到了刷新的作用。

[bits 32]
p_mode_start:
   mov ax, SELECTOR_DATA
   mov ds, ax
   mov es, ax
   mov ss, ax
   mov esp,LOADER_STACK_TOP
   mov ax, SELECTOR_VIDEO
   mov gs, ax

   mov byte [gs:0x140], 'P'

   jmp $

1 .使用远跳转指令清空流水线,更新段描述符缓冲器

  • 段描述符缓冲器未更新,它的值还是实模式下的值,进入保护模式后需要填入正确的信息。
  • 段描述符缓冲寄存器在CPU的实模式和保护模式中都同时使用,在不重新引用一个段时,段描述符缓冲器中的内容是不会更新的,无论是实模式还是保护模式下,CPU都以段描述符缓冲寄存器中的内容为主。实模式进入保护模式时,由于段描述符缓冲寄存器中的内容仅仅是实模式下的20位的段基址,很多属性位都是错误的值,这对保护模式来说必然是错误,所以需要马上更新段描述符缓冲寄存器,也就是要想办法往相应段寄存器中加载选择子。

七.保护模式之内存段的保护

1.向段寄存器加载选择子时的保护

  • 当引用一个内存段时,实际上就是往段寄存器中加载一个选择子,为了避免出现非法引用内存段的情况,在这时候,处理器会在以下几方面做检查。
  1. 首先根据选择子的值验证段描述符是否超越界限。
    描述符表基地址+选择子中的索引值*8+7 <= 描述符表基地址+描述符表界限值
    检查过程如下:处理器先检查TI的值,如果TI是0,则从全局描述符表寄存器GDTR中拿到GDT基地址和GDT界限值。如果TI是1,则从局部描述符表寄存器LDTR中拿到LDT基地址和LDT界限值。有了描述符表基地址和描述符表界限值后,把选择子的高13位代入上面的表达式,若不成立,处理器则抛出异常。
    在这里插入图片描述

在选择子检查后,就要检查段的类型了(段描述符中type字段)

  • 只有具备可执行属性的段(代码段)才能加载到 CS 段寄存器中。
  • 只具备执行属性的段(代码段)不允许加载到除 CS 外的段寄存器中。
  • 只有具备可写属性的段(数据段)才能加载到 SS 栈段寄存器中。
  • 至少具备可读属性的段才能加载到 DS、ES、FS、GS 段寄存器中。
    在这里插入图片描述
  • 在检查完type后,还会检查段是否存在(段描述符中的P位)
    CPU 通过段描述符中的 P 位来确认内存段是否存在,如果P 位为 1,则表示存在
    这时候就可以将选择子载入段寄存器了,同时段描述符缓冲寄存器也会更新为选择子对应的段描述符的内容,随后处理器将段描述符中的 A 位置为1,表示已经访问过了。如果 P 位为 0,则表示该内存段不存在,不存在的原因可能是由于内存不足,操作系统将该段移出内存转储到硬盘上了。这时候处理器会抛出异常,自动转去执行相应的异常处理程序,异常处理程序将段从硬盘加载到内存后并将 P 位置为 1,随后返回。CPU 继续执行刚才的操作,判断 P 位。

2. 代码段和数据段的保护

对于代码段和数据段来说,CPU每访问一个地址,都要确认该地址不能超过其所在内存段的范围。

  • 实际段界限的值为:(描述符中段界限+1)*(段界限的粒度大小:4k或者1)-1
  • 对于G位为1的4K粒度大小的段公式为:(描述符中段界限+1)4k-1 = 描述符中段界限4k-4k-1 = 描述符中段界限 *0x1000+0xFFF
  • 实际的段界限大小,是段内最后一个可访问的有效地址。由于有了段界限的限制,我们给CPU提交的每一个内存地址,无论是指令地址,还是数据地址,CPU都要帮我们检查地址的有效性。首先地址指向的数据是有宽度的,CPU要保证该数据一定要落在段内,不能“骑”在段边界上。
  • 对于数据段和代码段的访问,都要用“段基址:段内偏移地址”的形式
  • 对于代码段要满足的条件:EIP中的偏移地址+指令长度-1 <= 实际段界限大小。如果不满足条件,指令未完整的落在本段内,CPU则会抛出异常。
  • 对于数据段要满足的条件:偏移地址+数据长度-1 <= 实际段界限大小。如果不满足条件,数据未完整的落在本段内,CPU则会抛出异常。

例如:
假设数据段描述符的段界限是 0x12345,段基址为 0x00000000。
如果 G 位为 0,那么实际段界限便是 0x12345。如果 G 位为 1,那么实际段界限便是 0x12345* 0x1000+0xFFF=0x12345FFF。如果访问的数据地址是 0x12345FFF,还要看访问的数据宽度。若数据大小是 1 字节,如 mov ax,byte [0x12345fff],这种内存操作一点问题都没有,数据完全在实际段界限之内。若该数据大小是 2 字节,如 mov ax,word [0x12345fff],这种内存操作超过了实际的段界限,数据所在地址分别是 0x12345FFF 和 0x12346000 这两个字节,CPU 会抛异常。


3. 栈段的保护

  • 虽然段描述符type中的e位用来表示段的扩展方向,但它和别的描述符属性一样,仅仅是用来描述段的性质,即使e=1向下扩展,依然可以引用不断向上递增的内存地址,即使e=0向上扩展,也依然可以引用不断向下递增的内存地址。栈顶指针esp的值逐渐降低,这是push指令的作用,与描述符是否向下扩展无关,也就是说,是数据段就可以用作栈。

  • CPU对数据的检查,其中一项就是看地址是否超越界限。段的扩展方向决定了CPU对数据的检查方式:

    1. 对于向上扩展的段,实际的段界限是段内可以访问的最后一个字节(上一节数据段的方式来检查,偏移地址+数据段长度-1 <= 实际段界限大小)

    在这里插入图片描述

    1. 对于向下扩展的段,即栈段。段界限本质上就是段的范围大小,范围没有负数只说。栈的段界限是以栈段的基址为准的,并不是栈底,因此栈的段界限肯定是位于栈顶之下。为了避免碰撞,将段界限地址+1视为栈可以访问的下限。段界限+1,才是栈指针可达的下边界。

  • 32位保护模式下栈的栈顶指针是esp,栈的操作数大小是由B位决定的,假设B=1,表示32位操作数,所以栈指针最大可访问的地址是0xFFFFFFFF
  • 每次向栈段中压入数据时就是CPU检查栈段的时机,它必须满足:
    实际段界限+1 <= esp-操作数大小 <= 0xFFFFFFFF
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值