【操作系统真象还原】第4章:保护模式入门(4.1~4.3节)

目录

4.1 保护模式概述 

4.1.1 为什么要有保护模式

4.1.2 实模式不是 32 位CPU,变成了16 位

4.2 初见保护模式

4.2.1 保护模式之寄存器扩展

4.2.2 保护模式之寻址扩展

4.2.3 保护模式之运行模式反转

4.2.4 保护模式之指令扩展

4.3 全局描述符表

4.3.1 段描述符

4.3.2 全局描述符表 GDT、局部描述符表 LDT 及选择子

4.3.3 打开 A20 地址线

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

4.3.5 让我们进入保护模式


4.1 保护模式概述 

我们能开发出什么样的软件,取决于 CPU 给咱们提供什么样的功能。

🍊“想要啥就有啥” 井不是真正的幸福,而是发自内心地感恩,珍惜目前所拥有的一切。

4.1.1 为什么要有保护模式

解决实模式存在的问题:安全缺陷和使用缺陷。

保护模式下:物理内存地址不能直接被程序访问,程序内部的地址(虚拟地址)需要被转化为物理地址后再去访问,程序对此一无所知。地址转换是由处理器和操作系统共同协作完成的,处理器在硬件上提供地址转换部件,操作系统提供转换过程中所需要的页表。

4.1.2 实模式不是 32 位CPU,变成了16 位

模式是指 CPU 的运行环境、运行方式等。兼容实模式,是指能够正确处理好实模式下的程序,并不是说在实模式下运行时就完全变成了纯 16 位的 CPU。

我们说实模式时,指的是 32 位的 CPU 运行在 16 位模式下的状态。

4.2 初见保护模式

为了区别之前的“远古时代”,将之前的阶段称为实模式;为了突显现阶段的“安全”优势,称现阶段为保护模式。

4.2.1 保护模式之寄存器扩展

寄存器中低 16 位的部分是为了兼容实模式,可以单独使用。高 16 位没办法单独使用,只能在用 32 位寄存器时才有机会用到它们。(之前咱们所讲的实模式是和 CPU 8086 运行模式一样的

8086:16位运算,通用寄存器和段寄存器都是16位,地址总线20位,物理内存地址=段寄存器的值二进制左移4位+段内偏移地址;运行模式被后来成为实模式。

80286:开始有了保护模式,16位段寄存器中存的是段“选择子”——一个数,用这个数来索引在内存中的全局描述符表中的段描述符,(段描述符其大小为 64 位,用来描述各个内存段的起始地址、大小、权限等信息)。段描述符缓冲寄存器(Descriptor Cache Registers):CPU 每次将千辛万苦获取到的内存段信息,整理成“完整的,通顺,不鳖脚”的形式后,存入段描述符缓冲寄存器,以后每次访问相同的段时,就直接读取该段寄存器对应的段描述符缓冲寄存器(段描述符缓冲寄存器是多少位的???);虽然段描述符缓冲寄存器是保护模式下的产物,但它也可以用在实模式下,不用每次计算将段基址左移 4 位后的结果(CPU 只是兼容实模式,不管 CPU 用什么资源,只要能把实模式下的程序处理好就行啦);缓存的失效时间:原则上,只要往段寄存器中赋值, CPU 就会更新段描述符缓冲寄存器。

80286:依然是 16 位的 CPU,其通用寄存器和段寄存器还是 16 位宽,但其与 8086 不同的是其地址线由 20 位变为了 24 位。字段名称为 base 的部分就是该描述符所描述的内存段的起始地址,但用于寻址的通用寄存器还是 16 位,因此要访问全部地址空间还是要不断变换段基址。

1985 年推出了首款 32 位处理器 80386:地址总线和通用寄存器宽度都是 32 位,只有段寄存器仍然是 16 位。其 base 部分是个 32 位的段基址,位于该结构的第 32~63 位,这样任意一个段都能访问到全部的 4GB 内存空间。甚至段基址可以是0,光用段内偏移就可以指向 4GB 空间任意角落,这就开启了 “平坦模式” 的时代,大大方便了开发人员的工作。

CPU 的三种模式:

实模式:8086 CPU的运行模式

虚拟8086模式:实模式程序在保护模式下的运行模式

保护模式:有众多规定和约束,不用的运行模式 

4.2.2 保护模式之寻址扩展

实模式下:对于内存寻址来说,其中的基址寻址、变址寻址、基址变址寻址,这三种形式中的基址寄存器只能是 bx、bp,变址寄存器只能 si 、di,也就是说,只能用这 4 个寄存器。其中 bx 默认的段寄存器是 ds,它经常用于访问数据段,bp 默认的段寄存器是 ss,它经常用于访问栈。(对于寻址中的偏移量,只能是 1 个字以内的立即数,即不能超过 16 位)

在保护模式下:这一切都不同了,同样是内存寻址中,基址寄存器不再只是 bx、bp,而是所有 32 位的通用寄存器,变址寄存器也是一样,不再只是 si 、di,而是除 esp 之外的所有 32 位通用寄存器,偏移量由实模式的 16 位变成了 32 位。并且,还可以对变址寄存器乘以一个比例因子,注意比例因子只能是1、2、4、8。

  

4.2.3 保护模式之运行模式反转

译码器的工作:根据编码确定指令、寻址方式、寄存器等。

16 位环境下可以用 32 位环境的资源,而 32 位下也可以用 16 位的资源(编译后的二进制文件中可以有两种不同的机器码)。
bits 指 令的范围是从当前 bits 标签直到下 一个 bits 标签的范围。
[bits 16]是告诉编译器,下面的代码帮我编译成 16 位的机器码;(指令+操作数)
[bits 32]是告诉编译器,下面的代码帮我编译成 32 位的机器码。
添加指令前缀是编译器的工作:
反转操作数大小前缀 0x66,寻址方式反转前缀 0x67: 模式之 间可以互相使用对方环境下的资源,比如, 16 位实模式下可以用 32 位保护模式下的寄存器。如果要用另一 模式下的操作数大小,需要在指令前添加指令前缀 0x66 ,将当前模式临时改变成另一模式。这就是反转的意义,不管是当前模式是什么,总是转变成相反的运行模式(实模式和保护模式)。

bits 伪指令用于指定运行模式,操作数大小反转前缀 0x66 和寻址方式反转前缀 0x67,用于临时将当前运行模式下的操作数大小和寻址方式转变成另外一种模式下的操作数大小及寻址方式。

4.2.4 保护模式之指令扩展

本身是 32 位 CPU,它天生就具备处理 32 位数据的能力。在 16 位的实模式下,该 CPU 照样可以处理 32 位的数据。

复习一下:

内存中存储的是指令和数据。

指令是由操作码和操作数组成的(汇编语言——>机器码),操作数本质是被操作的数据,只不过在指令中以不同的形式出现,如立即数、寄存器、内存。

CPU 没有直接操作 8 位数据的功能电路,16 位实模式下默认的操作数大小是 16 位,32 位也同样默认是 32 位。因此,汇编语言中的 byte 是给编译器看的,具体会转换成各模式下默认的操作数大小;而在 16 位实模式中操作 32 位数据,或在 32 位实模式中操作 16 位数据,就会用到前面提到的反转操作数大小前缀。

对于段寄存器的入栈,即 cs ds es fs gs ss,无论在哪种模式下,都是按当前模式的默认操作数大小压入的。例如,在 16 位模式下,CPU 直接压入 2 字节,栈指针 sp 减 2,在 32 位模式下,CPU 直接压入 4 字节,栈指针 esp 减 4。

对于通用寄存器和内存,无论是在实模式或保护模式:

  • 如果压入的是 16 位数据,栈指针减 2。
  • 如果压入的是 32 位数据,栈指针减 4。

4.3 全局描述符表

全局描述符表(Global Descriptor Table, GDT)是保护模式下内存段(代码段、数据段)的登记表,这是不同于实模式的显著特征之一。

(这里提到的各种描述符大小都是8字节,64位)

4.3.1 段描述符

这里格式乱是为了兼容,80286 是第一款具有保护模式 CPU,当时就已经采用段描述符来描述内存段信息了。只是当时它是 16 位的保护模式,地址总线是 24 位,最多访问 16MB 内存,产品定位模糊。

32 位 CPU 在保护模式下,地址总线宽度是 32 位,所以段基址需要用 32 位地址来表示;

段界限表示段边界的扩展最值,即最大扩展到多少或最小扩展到多少,用 20 个二进制位来表示。只不过此段界限只是个单位量,它的单位要么是字节B,要么是 4KB,这是由描述符中的 G 位来指定的;实际的段界限边界值=(描述符中段界限+1) * (段界限的粒度大小:4KB 或者 1) -1。🤔️

S字段:S为0时表示系统段,S为1时表示数据段。凡是硬件运行需要用到的东西都可称之为系统,凡是软件(操作系统也属于软件,在CPU 眼中,它与用户程序无区别)需要的东西都称为数据。

type 字段:共 4 位,用来指定本描述符的类型。 只有 S 字段的值确定后,type 字段的值才有具体意义。

DPL字段:Descriptor Privilege Level,即描述符特权级,分别是0、1、2、3 级特权,数字越小,特权级越大。操作系统应该处于最高的 0 特权级,用户程序通常处于 3 特权级,权限最小。

P字段:Present,即段是否存在内存中

AVL 字段:AVaiLable,可用的,不过这“可用的”是对用户来说的,也就是操作系统可以随意用此位。

L字段:用来设置是否是 64 位代码段,L 为 1 表示 64 位代码段,否则表示 32 位代码段。这目前属于保留位,在我们 32 位 CPU 下编程,将其置为 0 便可。🤔️

D/B 字段:用来指示有效地址(段内偏移地址)及操作数的大小。

 G 字段:Granularity,粒度,用来指定段界限的单位大小。若 G 为 0,表示段界限的单位是 1 字节,这样段最大是 2 的 20 次方*字节,即 1 MB;若 G 为 1,表示段界限的单位是 4KB,这样段最大是 2 的 20 次方*4KB,即 4GB。

4.3.2 全局描述符表 GDT、局部描述符表 LDT 及选择子

一个段描述符只用来定义(描述)一个内存段。

这些描述符放在全局描述符表中,就是 GDT (Global Descriptor Table ),这个表相当于是描述符的数组。数组中的每个元素都是 8 字节的描述符,可以用选择子中提供的下标在 GDT 中索引描述符。

全局体现在多个程序都可以在里面定义自己的段描述符,是公用的。全局描述符表位于内存中,需要用专门的寄存器指向它后,CPU 才知道它在哪里。这个专门的寄存器便是 GDTR,GDT Register,专门用来存储 GDT 的内存地址及大小。GDTR 是个 48 位的寄存器:

有专门的指令来为 gdtr 初始化,即 lgdt 指令,格式是:lgdt 48位内存数据。

由于段寄存器是 16 位,所以选择子也是 16 位,在其低 2 位即第 0~1 位,用来存储 RPL,即请求特权级,可以表示 0、1、2、3 四种特权级。在选择子的第 2 位是 TI 位,即 Table Indicator,用来指示选择子是在 GDT 中,还是在 LDT 中索引描述符。选择子的高 13 位,即第 3~15 位是描述符的索引值,用此值在 GDT 中索引描述符。前面说过 GDT 相当于一个描述符数组,所以此选择子中的索引值就是 GDT 中的下标。

由于选择子的索引值部分是 13 位,即 2 的 13 次方是 8192,故最多可以索引 8192 个段,这和 GDT 中最多定义 8192 个描述符是吻合的。

到了保护模式下后,由于已经是 32 位地址线和 32 位寄存器,任意一寄存器都能够提供 32 位地址,故不需要再将段基址乘以 16 后再与段内偏移地址相加,直接用选择子对应的“段描述符中的段基址” 加上 “段内偏移地址” 就是要访问的内存地址。( GDT 中第 0 个段描述符不可用,因为如果使用的选择子忘记初始化,选择子的值便会是 0,这便会访问到第 0 个段描述符。)

局部描述符表,叫 LDT,Local Descriptor Table,它是 CPU 厂商为在硬件一级原生支持多任务而创造的表,按照 CPU 的设想,一个任务对应一个 LDT。

LDT 也位于内存中,其地址需要先被加载到某个寄存器后,CPU 才能使用 LDT,该寄存器是LDTR,即 LDT Register。同样也有专门的指令用于加 LDT,即 lldt。每切换任务时,都要用 lldt 指令重新加载任务的私有内存段。

LDT 虽然是个表,但其也是一片内存区域,所以也需要用一个描述符在 GDT 中先注册。段描述符是需要用选择子去访问的,该指令格式:lldt 16 位寄存器/16 位内存。这里无论是寄存器、还是内存,其内容一定是个选择子,该选择子用来在 GDT 中索引 LDT 的段描述符。LDT 被加载到 ldtr 寄存器后,之后再访问某个段时,选择子中的 TI 位若为 1,就会用该选择子中的高 13 位在 ldtr 寄存器所指向的 LDT 中去索引相应段描述符。(与 GDT 不同的是 LDT 中的第 0 个段描述符是可用的) 

4.3.3 打开 A20 地址线

实模式下的地址线是 20 位,为了兼容,地址回绕:

对于80286(24根地址线)后续的CPU,通过 A20GATE 来控制 A20 地址线。在保护模式下,我们要突破第 20 条地址线 (A20) 去访问更大的内存空间,打开 A20Gate—将端口 0x92 的第 1 位置 1。

in al, 0x92

or al, 0000_0010B

out 0x92, al

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

控制寄存器系列 CRx,控制寄存器是 CPU 的窗口,既可以用来展示 CPU 的内部状态,也可用于控制 CPU 的运行机制。

CR0 寄存器的第 0 位,即 PE 位,Protection Enable,此位用于启用保护模式,是保护模式的开关,PE 为 0 表示在实模式下运行,PE 为 1 表示在保护模式下运行。

mov eax, cr0

or eax, 0x00000001

mov cr0, eax

4.3.5 让我们进入保护模式

保护模式是在 loader.bin 中进入的,除了源程序 loader.S 要更新外,还更新了相关的 2 个文件:第一个是 mbr.S,由于 loader.bin 超过了 512 字节,所以我们要把 mbr.S 中加载 loader.bin 的读入扇区数增大;另一个要更新的文件是 include/boot.inc,里面是一些配置信息,loader.S 中用到的配置都是定义在 boot.inc 中的符号。

;主引导程序 MBR
;------------------------------------------------------------
%include "boot.inc" 
;这个%include 是 nasm 编译器中的预处理指令,意思是让编译器在编译之前把 boot.inc 文件包含进来
;"boot.inc"文件里面是关于加载器的配置信息
 
SECTION MBR vstart=0x7c00
    mov ax,cs
    mov ds,ax   ;将段寄存器cs中的值通过中转到ds段寄存器中
    mov es,ax
    mov ss,ax
    mov fs,ax   ;初始化一系列段寄存器
    mov sp,0x7c00   ;栈是向低地址发展的
    mov ax,0xb800   ;显存映射内存地址
    mov gs,ax
 
    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
    
    ;输出字符串: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	   ;A表示绿色背景闪烁,4表示前景色为红色

    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 ;起始扇区lba地址
    mov bx,LOADER_BASE_ADDR     ;写入的内存地址
    mov cx,4                    ;待读入的扇区数
    call rd_disk_m_16           ;调用函数读取程序
 
    jmp LOADER_BASE_ADDR        ;将loader程序都读入内存后,mbr交出接力棒
;————————————————————————————————————————————————————————————
;读取硬盘n个扇区
rd_disk_m_16:
    mov esi,eax     ;备份,因为 al 在 out 指令中会被用到,这会影响到 eax 的低 8 位
    mov di,cx       ;cx 的值会在读取数据时用到,所以在此提前备份
;读写硬盘
    mov dx,0x1f2    ;0x1f2端口负责扇区数
    mov al,cl
    out dx,al   ;将要读的扇区数写入0x1f2端口
 
    mov eax,esi ;恢复ax
;将lab地址存入0x1f3~0x1f6端口,lba 24位
    mov dx,0x1f3
    out dx,al
 
    mov cl,8
    shr eax,cl
    mov dx,0x1f4
    out dx,al
 
    shr eax,cl
    mov dx,0x1f5
    out dx,al
 
    shr eax,cl
    and al,0x0f
    or al,0xe0
    mov dx,0x1f6
    out dx,al
 
;向0x1f7端口写入读命令0x20    
    mov dx,0x1f7
    mov al,0x20
    out dx,al
 
;检测硬盘状态
    .not_ready:
        nop
        in al,dx
        and al,0x88
        cmp al,0x08
        jnz .not_ready
 
    ;从0x1f0端口读数据
    mov ax,di
    mov dx,256
    mul dx
    mov cx,ax
 
    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 在内存中的位置,mbr在内存中的0x7c00处,占512个字节
LOADER_START_SECTOR equ 0x2     ;定义 loader 在硬盘上的逻辑扇区地址,即 LBA 地址,第2块扇区,mbr在第0扇区

;---------------- gdt 中的段描述符属性 -----------------
DESC_G_4K equ 1_00000000000000000000000b	;nasm编译器支持这种分隔符的写法,在编译阶段会忽略此分隔符
DESC_D_32 equ 1_0000000000000000000000b		;若是代码段(type字段来决定),代表D位,值为1代表有效地址和操作数都是32位的
DESC_L equ 0_000000000000000000000b 			;用来设置是否是 64 位代码段,0表示是32位代码段
DESC_AVL equ 0_00000000000000000000b 		;对用户来说该段是否可用,0表示不可,但操作系统总是可用
DESC_LIMIT_CODE2 equ 1111_0000000000000000b 	;段界限第19~16位
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2 equ 0000_0000000000000000b 	

DESC_P equ 1_000000000000000b				;第15位,为1表示段在内存中

DESC_DPL_0 equ 00_0000000000000b				;描述符特权级,分别是0、1、2、3 级特权
DESC_DPL_1 equ 01_0000000000000b
DESC_DPL_2 equ 10_0000000000000b
DESC_DPL_3 equ 11_0000000000000b

DESC_S_CODE equ 1_000000000000b				;第12位,S字段,为1代表数据段而非系统段
DESC_S_DATA equ DESC_S_CODE
DESC_S_sys equ 0_000000000000b

DESC_TYPE_CODE equ 1000_00000000b			;第11-8位type字段,x=1, c=0, r=0, a=0表示可执行代码段、非一致、不可读、已访问位为0
DESC_TYPE_DATA equ 0010_00000000b			;x=0, e=0, w=1, a=0 表示为数据段, 是不可执行的,向上扩展的,可写,己访问位为0

;段描述符的高4个字节值
;下面表示代码段
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 + 0x00


;---------------- 选择子属性 -----------------
;请求特权级
RPL0 equ 00b
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
;指示选择子是在 GDT 中,还是在 LDT 中索引描述符
TI_GDT equ 000b
TI_LDT equ 100b
;加载器代码 loader,进入保护模式
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR 			;在内存中的位置0x900
LOADER_STACK_TOP equ LOADER_BASE_ADDR		;栈是向低地址发展的
jmp loader_start

;构建 gdt 及其内部的描述符,在gdt中定义段描述符,GDT 中的第 0 个描述符不可用
GDT_BASE:
dd 0x00000000 	;低4字节 	
dd 0x00000000 	;高4字节

;定义代码段描述符、数据段和栈段描述符、显存段描述符
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 个描述符的空位, dq定义四字-八字节

;构建选择子
SELECTOR_CODE equ (0x0001 << 3) + 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

loadermsg db '2 loader in real.'		;这是用一个byte来定义一个字符串???

loader_start:
mov sp, LOADER_BASE_ADDR
mov bp, loadermsg	;ES:BP =字符串地址
mov cx, 17			;cx=字符串长度
mov ax, 0x1301		;AH = 13, AL = 01h
mov bx, 0x001f		;页号为0 (BH = 0)蓝底粉红字(BL = 1fh)
mov dx, 0x1800
int 0x10				;10h号中断

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

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

lgdt [gdt_ptr]		;gdtr寄存器初始化

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

jmp dword SELECTOR_CODE:p_mode_start		;刷新流水线
[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:240], 'P'        ;这里打印的位置不对??????

jmp $

用到的命令:

nasm -I include/ -o mbr.bin mbr.S

dd if=mbr.bin of=hd60.img bs=512 count=1 conv=notrunc

nasm -I include/ -o loader.bin loader.S

dd if=loader.bin of=hd60.img bs=512 count=4 seek=2 conv=notrunc
bin/bochs -f bochsrc.disk

梳理:

1、开机启动,运行BIOS,自检,将磁盘中第0扇区的1字节mbr加载到内存0x7c00处,之后跳转到该位置开始执行mbr代码。

2、mbr的作用就是从磁盘中将内核加载器loader加载到内存0x900处,并跳转到该位置执行loader代码。

3、loader的作用就是进入保护模式,并且从磁盘中将内核加载到内存中。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值