《Orange's 一个操作系统的实现》学习笔记--特权级代码段之间的转移(四)

一、演示任务内特权级变换的实例 

下面给出一个演示任务内特权级变换的实例。该实例演示在任务内通过调用门从外层特权级变换到内层特权级;也演示通过段间返回指令从内层特权级变换到外层特权级;还演示通过调用门的无特权级变换的转移。实例使用了任务状态段 TSS,这是因为任务内特权级变换时要使用的内层堆栈指针存放在 TSS 中。

1.实现步骤

该实例的实现步骤为:
(1)实方式下初始化;
(2)切换到保护模式;
(3)设置 TR 和 LDTR。由于在任务内发生特权级变换时要切换堆栈,而内层堆栈的指针存放在当前任务的 TSS 中,所以在进入保护

模式后设置任务状态段寄存器 TR。由于演示任务使用了局部描述符表,所以设置 LDTR;
(4)经调用门进入 32 位过渡代码段;
(5)建立返回 3 级演示代码段的环境;
(6)利用 RET 指令转移到 3 级的演示代码段。为了演示外层程序通过调用门调用内层程序,要使 CPL>0。实例先通过段间返回指令

RET 从特权级 0 变换到特权级 3 的演示代码段。在特权级 3 下,通过调用门调用 1 级的子程序。随着执行段间返回指令 RET,又回到 3级的演示代码段;

(7)在 3 级的演示代码段中,经调用门转移到 0 级的 32 位过渡代码段;(8)直接转 0 级的临时代码段;
(9)准备返回实模式;
(10)切换回实模式;

(11)实模式下的恢复工作。

2.源程序组织和清单

实例四由如下部分组成:

(1)全局描述符表 GDT。GDT 含有演示任务的 TSS 段描述符和 LDT 段描述符,此外还含有临时代码段的描述符、规范数据段描述符和视频缓冲区段描述符。

(2)演示任务的 LDT 段。它含有除临时代码段外的其它代码段的描述符和演示任务各级堆栈段描述符,还含有 3 个调用门。

(3)演示任务的 TSS 段。
(4)演示任务的 0 级、1 级和 3 级堆栈段。
(5)显示子程序段。32 位代码段,特权级 1。

(6)演示代码段。32 位代码段,特权级 3。

(7)过渡代码段。32 位段,特权级 0。
(8)临时代码段。16 位段,特权级 0。
(9)实模式下的数据和代码段。该实例的逻辑功能是显示演示代码段执行时的当前特权级 CPL和TI。源程序清单如下: 


;windows
;16位偏移的段间直接转移指令的宏定义(在16位代码段中使用)
;----------------------------------------------------------------------------
JUMP16 MACRO Selector,Offsetv
	DB 0eah ;操作码
	DW Offsetv ;16位偏移量
	DW Selector ;段值或段选择子
ENDM
;----------------------------------------------------------------------------
;32位偏移的段间直接转移指令的宏定义(在32位代码段中使用)
;----------------------------------------------------------------------------
COMMENT <JUMP32>
JUMP32 MACRO Selector,Offsetv
	DB 0eah ;操作码
	DD Offsetv
	DW Selector ;段值或段选择子
ENDM
<JUMP32>
;-------------------------------------------------
JUMP32 MACRO Selector,Offsetv
	DB 0eah ;操作码
	DW Offsetv
	DW 0
	DW Selector ;段值或段选择子
ENDM
;----------------------------------------------------------------------------
;门描述符结构类型定义
;----------------------------------------------------------------------------
Gate STRUC
OffsetL DW 0 ;32位偏移的低16位
Selector DW 0 ;选择子
DCount DB 0 ;双字计数
GType DB 0 ;类型
OffsetH DW 0 ;32位偏移的高16位
Gate ENDS
;----------------------------------------------------------------------------

;16位偏移的段间调用指令的宏定义(在16位代码段中使用)
;----------------------------------------------------------------------------
CALL16 MACRO Selector,Offsetv
	DB 9ah ;操作码
	DW Offsetv ;16位偏移量
	DW Selector ;段值或段选择子
ENDM
;----------------------------------------------------------------------------
;32位偏移的段间调用指令的宏定义(在32位代码段中使用)
;----------------------------------------------------------------------------
COMMENT <CALL32>
CALL32 MACRO Selector,Offsetv
	DB 9ah ;操作码
	DD Offsetv
	DW Selector ;段值或段选择子
ENDM
<CALL32>
;-------------------------------------------------
CALL32 MACRO Selector,Offsetv
	DB 9ah ;操作码
	DW Offsetv
	DW 0
	DW Selector ;段值或段选择子
ENDM
;----------------------------------------------------------------------------
;存储段描述符结构类型定义
;----------------------------------------------------------------------------
Desc STRUC
	LimitL DW 0 ;段界限(BIT0-15)
	BaseL DW 0 ;段基地址(BIT0-15)
	BaseM DB 0 ;段基地址(BIT16-23)
	Attributes DB 0 ;段属性
	LimitH DB 0 ;段界限(BIT16-19)(含段属性的高4位)
	BaseH DB 0 ;段基地址(BIT24-31)
Desc ENDS
;----------------------------------------------------------------------------


;伪描述符结构类型定义(用于装入全局或中断描述符表寄存器)
;----------------------------------------------------------------------------
PDesc STRUC
	Limit DW 0 ;16位界限
	Base DD 0 ;32位基地址
PDesc ENDS
;----------------------------------------------------------------------------
;存储段描述符类型值说明
;----------------------------------------------------------------------------
D32 EQU 40h ;32位代码段标志
ATDR EQU 90h ;存在的只读数据段类型值
ATDW EQU 92h ;存在的可读写数据段属性值
ATDWA EQU 93h ;存在的已访问可读写数据段类型值
ATCE EQU 98h ;存在的只执行代码段属性值
ATCER EQU 9ah ;存在的可执行可读代码段属性值

;----------------------------------------------------------------------------
;系统段描述符类型值说明
;----------------------------------------------------------------------------
ATLDT EQU 82h ;局部描述符表段类型值

;----------------------------------------------------------------------------
;DPL值说明
;----------------------------------------------------------------------------
DPL0 EQU 00h ;DPL=0
DPL1 EQU 20h ;DPL=1
DPL2 EQU 40h ;DPL=2
DPL3 EQU 60h ;DPL=3
;----------------------------------------------------------------------------
;RPL值说明
;----------------------------------------------------------------------------
RPL0 EQU 00h ;RPL=0
RPL1 EQU 01h ;RPL=1
RPL2 EQU 02h ;RPL=2
RPL3 EQU 03h ;RPL=3
;----------------------------------------------------------------------------

;----------------------------------------------------------------------------
;其它常量值说明
;----------------------------------------------------------------------------

TIL EQU 04h ;TI=1(局部描述符表标志)
AT386TSS EQU 89h ;可用386任务状态段类型值
AT386CGate EQU 8ch ;386调用门类型值
;----------------------------------------------------------------------------

.386p

GDTSeg          SEGMENT PARA USE16                ;全局描述符表数据段(16位)
;----------------------------------------------------------------------------
                ;全局描述符表
GDT             LABEL   BYTE
                ;空描述符
DUMMY           Desc    <>
                ;规范段描述符
Normal          Desc    <0ffffh,,,ATDW,,>
                ;视频缓冲区段描述符(DPL=3)
VideoBuf        Desc    <07fffh,8000h,0bh,ATDW+DPL3,,>
;----------------------------------------------------------------------------
EFFGDT          LABEL   BYTE
                ;任务状态段TSS描述符
DemoTSS         Desc    <DemoTSSLen-1,DemoTSSSeg,,AT386TSS,,>
                ;局部描述符表段的描述符
DemoLDTD        Desc    <DemoLDTLen-1,DemoLDTSeg,,ATLDT,,>
                ;临时代码段描述符
TempCode        Desc    <0ffffh,TempCodeSeg,,ATCE,,>  ;注意它的存储段描述符
;----------------------------------------------------------------------------
GDTLen          =       $-GDT                     ;全局描述符表长度
GDNum           =       ($-EFFGDT)/(SIZE Desc)    ;需特殊处理的描述符数
;----------------------------------------------------------------------------
Normal_Sel      =       Normal-GDT                ;规范段描述符选择子
Video_Sel       =       VideoBuf-GDT              ;视频缓冲区段描述符选择子
;----------------------------------------------------------------------------
DemoTSS_Sel     =       DemoTSS-GDT               ;任务状态段描述符选择子
DemoLDT_Sel     =       DemoLDTD-GDT              ;局部描述符表段的选择子
TempCode_Sel    =       TempCode-GDT              ;临时代码段的选择子
;----------------------------------------------------------------------------
GDTSeg          ENDS                              ;全局描述符表段定义结束
;----------------------------------------------------------------------------
DemoLDTSeg      SEGMENT PARA USE16                ;局部描述符表数据段(16位)
;----------------------------------------------------------------------------
DemoLDT         LABEL   BYTE                      ;局部描述符表
                ;0级堆栈段描述符(32位段)
DemoStack0      Desc    <DemoStack0Len-1,DemoStack0Seg,,ATDW+DPL0,D32,>
                ;1级堆栈段描述符(32位段)
DemoStack1      Desc    <DemoStack1Len-1,DemoStack1Seg,,ATDW+DPL1,D32,>
                ;3级堆栈段描述符(16位段)
DemoStack3      Desc    <DemoStack3Len-1,DemoStack3Seg,,ATDW+DPL3,,>
                ;代码段描述符(32位段,DPL=3)
DemoCode        Desc    <DemoCodeLEN-1,DemoCodeSeg,,ATCE+DPL3,D32,>
                ;过渡代码段描述符(32位段)
T32Code         Desc    <T32CodeLen-1,T32CodeSeg,,ATCE,D32,>
                ;显示子程序代码段描述符(32位段,DPL=1)
EchoSubR        Desc    <EchoSubRLen-1,EchoSubRSeg,,ATCER+DPL1,D32,>
;----------------------------------------------------------------------------
DemoLDNum       =       ($-DemoLDT)/(SIZE Desc)
;----------------------------------------------------------------------------
                ;0级堆栈描述符选择子(RPL=0)
DemoStack0_Sel  =       DemoStack0-DemoLDT+TIL+RPL0
                ;1级堆栈描述符选择子(RPL=1)
DemoStack1_Sel  =       DemoStack1-DemoLDT+TIL+RPL1
                ;3级堆栈描述符选择子(RPL=3)
DemoStack3_Sel  =       DemoStack3-DemoLDT+TIL+RPL3
                ;代码段描述符选择子(RPL=3)
DemoCode_Sel    =       DemoCode-DemoLDT+TIL+RPL3
                ;过渡代码段描述符选择子
T32Code_Sel     =       T32Code-DemoLDT+TIL
                ;显示子程序代码段描述符选择子(RPL=1)
Echo_Sel1       =       EchoSubR-DemoLDT+TIL+RPL1
                ;显示子程序代码段描述符选择子(RPL=3)
Echo_Sel3       =       EchoSubR-DemoLDT+TIL+RPL3
;----------------------------------------------------------------------------
                ;指向过渡代码段内T32Begin点的调用门(DPL=0)
ToT32GateA      Gate    <T32Begin,T32Code_Sel,,AT386CGate,>
                ;指向过渡代码段内T32End点的调用门(DPL=3)
ToT32GateB      Gate    <T32End,T32Code_Sel,,AT386CGate+DPL3,>
                ;指向显示子程序代码段的调用门(DPL=3)
ToEchoGate      Gate    <EchoSUB,Echo_Sel3,,AT386CGate+DPL3,>
;----------------------------------------------------------------------------
DemoLDTLen      =       $-DemoLDT
;----------------------------------------------------------------------------
                ;指向过渡代码段内T32Begin点的调用门的选择子
ToT32A_Sel      =       ToT32GateA-DemoLDT+TIL
                ;指向过渡代码段内T32End点的调用门的选择子
ToT32B_Sel      =       ToT32GateB-DemoLDT+TIL
                ;显示子程序调用门的选择子
ToEcho_Sel      =       ToEchoGate-DemoLDT+TIL
;----------------------------------------------------------------------------
DemoLDTSeg      ENDS                              ;局部描述符表段定义结束
;----------------------------------------------------------------------------
DemoTSSSeg      SEGMENT PARA USE16                ;任务状态段TSS
;----------------------------------------------------------------------------
                DD      0                         ;Back
                DW      DemoStack0Len,0            ;0级堆栈指针
                DW      DemoStack0_Sel,0            ;初始化
                DW      DemoStack1Len,0             ;1级堆栈指针
                DW      DemoStack1_Sel,0            ;初始化
                DD      0                         ;2级堆栈指针
                DD      0                         ;未初始化
                DD      0                         ;CR3
                DD      0                         ;EIP
                DD      0                         ;EFLAGS
                DD      0                         ;EAX
                DD      0                         ;ECX
                DD      0                         ;EDX
                DD      0                         ;EBX
                DD      0                         ;ESP
                DD      0                         ;EBP
                DD      0                         ;ESI
                DD      0                         ;EDI
                DW      ?,0                         ;ES
                DW      ?,0                         ;CS
                DW      ?,0                         ;SS
                DW      ?,0                         ;DS
                DW      ?,0                         ;FS
                DW      ?,0                         ;GS
                DW      DemoLDT_Sel,0               ;LDT
                DW      0                         ;调试陷阱标志
                DW      $+2                       ;指向I/O许可位图
                DB      0ffh                    ;I/O许可位图结束标志
;----------------------------------------------------------------------------
DemoTSSLen      =       $-DemoTSSSeg
;----------------------------------------------------------------------------
DemoTSSSeg      ENDS                              ;任务状态段TSS结束
;----------------------------------------------------------------------------
DemoStack0Seg   SEGMENT DWORD STACK USE32         ;0级堆栈段(32位段)
DemoStack0Len   =       512
                DB      DemoStack0Len DUP(0)
DemoStack0Seg   ENDS                              ;0级堆栈段结束
;----------------------------------------------------------------------------
DemoStack1Seg   SEGMENT DWORD STACK USE32         ;1级堆栈段(32位段)
DemoStack1Len   =       512
                DB      DemoStack1Len DUP(0)
DemoStack1Seg   ENDS                              ;1级堆栈段结束
;----------------------------------------------------------------------------
DemoStack3Seg   SEGMENT DWORD STACK USE16         ;3级堆栈段(16位段)
DemoStack3Len   =       512
                DB      DemoStack3Len DUP(?)
DemoStack3Seg   ENDS                              ;3级堆栈段结束
;----------------------------------------------------------------------------
EchoSubRSeg     SEGMENT PARA USE32                ;显示子程序代码段(32位,1级)
                ASSUME  CS:EchoSubRSeg
;----------------------------------------------------------------------------
Message         DB      'CPL=  TI=',0                  ;显示信息(该代码段可读)
;----------------------------------------------------------------------------
EchoSub         PROC    FAR
                cld
                push    ebp
                mov     ebp,esp
                mov     ax,Echo_Sel1                ;该代码段是可读段
                mov     ds,ax                     ;采用RPL=1的选择子
                mov     ax,Video_Sel
                mov     es,ax
                mov     edi,320                   ;信息显示位置
                mov     esi,OFFSET Message
                mov     ah,4eh                    ;置显示属性(红底黄字)
EchoSub1:        lodsb
                or      al,al
                jz      EchoSub2
                stosw
                jmp     EchoSub1
EchoSub2:         mov     al,[ebp+8]               ;从堆栈中取调用程序的选择子
                and     al,3                      ;调用程序的CPL在CS的RPL字段
                add     al,'0'
                mov     ah,4eh                    ;置显示属性(红底黄字)
		   sub     edi,10
                stosw
		   add     edi,8
		   mov     al,[ebp+8]               ;从堆栈中取调用程序的选择子
                and     al,1                     ;调用程序TI字段
                add     al,'0'
                mov     ah,4eh  
		   stosw
                pop     ebp
                retf
EchoSub         ENDP
;----------------------------------------------------------------------------
EchoSubRLen     =       $-EchoSubRSeg
;----------------------------------------------------------------------------
EchoSubRSeg     ENDS                              ;显示子程序代码段结束
;----------------------------------------------------------------------------
DemoCodeSeg     SEGMENT PARA USE32                ;32位代码段(3级)
                ASSUME  CS:DemoCodeSeg
;----------------------------------------------------------------------------
DemoBegin       PROC    FAR
                CALL32  ToEcho_Sel,0              ;显示当前特权级(变换到1级)
                CALL32  ToT32B_Sel,0              ;转到过渡代码段(变换到0级)
DemoBegin       ENDP
DemoCodeLen     =       $-DemoCodeSeg
;----------------------------------------------------------------------------
DemoCodeSeg     ENDS                              ;32位代码段结束
;----------------------------------------------------------------------------
T32CodeSeg      SEGMENT PARA USE32                ;32位过渡代码段(0级)
                ASSUME  CS:T32CodeSeg
;----------------------------------------------------------------------------
T32Begin        PROC    FAR
                mov     ax,DemoStack0_Sel         ;建立0级堆栈
                mov     ss,ax
                mov     esp,DemoStack0Len
                push    DWORD PTR DemoStack3_Sel  ;压入3级堆栈指针
                push    DemoStack3Len
                push    DWORD PTR DemoCode_SEL    ;压入入口点
                push    OFFSET DemoBegin
                retf                              ;利用RET实现转3级的演示代码
T32Begin        ENDP
;----------------------------------------------------------------------------
T32End          PROC    FAR
                JUMP32  TempCode_Sel,<OFFSET ToReal>
T32End          ENDP
T32CodeLen      =       $-T32CodeSeg
;----------------------------------------------------------------------------
T32CodeSeg      ENDS
;----------------------------------------------------------------------------
TempCodeSeg     SEGMENT PARA USE16                ;16位临时代码段(0级)
                ASSUME  CS:TempCodeSeg
;----------------------------------------------------------------------------
Virtual         PROC    FAR
                mov     ax,DemoTSS_Sel            ;装载TR
                ltr     ax
                mov     ax,DemoLDT_Sel            ;装载LDTR
                lldt    ax
                JUMP16  ToT32A_Sel,0              ;通过调用门转过渡段
ToReal:          mov     ax,Normal_Sel             ;准备切换回实模式
                mov     ds,ax
                mov     es,ax
                mov     fs,ax
                mov     gs,ax
                mov     ss,ax
                mov     eax,cr0
                and     al,11111110b
                mov     cr0,eax
                JUMP16  <SEG Real>,<OFFSET Real>
Virtual         ENDP
;----------------------------------------------------------------------------
TempCodeLen     =       $-TempCodeSeg
TempCodeSeg     ENDS

;============================================================================
RDataSeg        SEGMENT PARA USE16                ;实方式数据段
VGDTR           PDesc   <GDTLen-1,>               ;GDT伪描述符
SPVar           DW      ?                         ;用于保存实方式下的SP
SSVar           DW      ?                         ;用于保存实方式下的SS
RDataSeg        ENDS
;----------------------------------------------------------------------------
RCodeSeg        SEGMENT PARA USE16
                ASSUME  CS:RCodeSeg,DS:RDataSeg
;----------------------------------------------------------------------------
Start           PROC
                mov     ax,RDataSeg
                mov     ds,ax
                cld
                CALL    InitGDT                   ;初始化全局描述符表GDT
                mov     ax,DemoLDTSeg
                mov     fs,ax
                mov     si,OFFSET DemoLDT
                mov     cx,DemoLDNum
                CALL    InitLDT                   ;初始化局部描述符表LDT
                mov     SSVar,ss
                mov     SPVar,sp
                lgdt    FWORD PTR VGDTR           ;装载GDTR并切换到保护方式
                cli
                mov     eax,cr0
                or      al,1
                mov     cr0,eax
                JUMP16  <TempCode_Sel>,<OFFSET Virtual>
Real:            mov     ax,RDataSeg
                mov     ds,ax
                lss     sp,DWORD PTR SPVar        ;又回到实方式
                sti
                mov     ax,4c00h
                int     21h
Start           ENDP
;----------------------------------------------------------------------------
;初始化GDT表
InitGDT         PROC
                push    ds
                mov     ax,GDTSeg
                mov     ds,ax
                mov     cx,GDNum
                mov     si,OFFSET EFFGDT
InitG:           mov     ax,[si].BaseL
                movzx   eax,ax
                shl     eax,4
                shld    edx,eax,16
                mov     WORD PTR [si].BaseL,ax
                mov     BYTE PTR [si].BaseM,dl
                mov     BYTE PTR [si].BaseH,dh
                add     si,SIZE Desc
                loop    InitG
                pop     ds
                mov     bx,16
                mov     ax,GDTSeg
                mul     bx
                mov     WORD PTR VGDTR.Base,ax
                mov     WORD PTR VGDTR.Base+2,dx
                ret
InitGDT         ENDP
;----------------------------------------------------------------------------
;初始化LDT表
;入口参数:FS:SI=第一个要初始化的描述符,CX=要初始化的描述符数
;----------------------------------------------------------------------------
InitLDT         PROC
ILDT:            mov     ax,WORD PTR FS:[si].BaseL
                movzx   eax,ax
                shl     eax,4
                shld    edx,eax,16
                mov     WORD PTR fs:[si].BaseL,ax
                mov     BYTE PTR fs:[si].BaseM,dl
                mov     BYTE PTR fs:[si].BaseH,dh
                add     si,SIZE Desc
                loop    ILDT
                ret
InitLDT         ENDP
;----------------------------------------------------------------------------
RCodeSeg        ENDS
                END     Start  
程序中部分片段的背景和实现方法在前面的实例中做过介绍,下面主要就如何实现任务内特权级变换做些说明:                                                            

(1)通过段间返回指令实现特权级变换

实例在两处使用段间返回指令实现任务内的特权级变换。一处是在 0 级的过渡代码段中用段间 RET 指令从特权级 0 变换到特权级 3的演示代码段。该处 RET 指令并不对应 CALL 指令。实例从实模式切换到保护模式后 CPL=0。为了演示如何通过调用门调用内层程序,要设法使 CPL>0。为此,实例先建立一个已发生的从外层到内层变换的环境,即按上图所示在当前堆栈(0 级堆栈)中放入外层堆栈的指

针和外层演示程序的入口指针,形成一个如下图所示的 0 级堆栈,无需传递参数。然后,执行段间返回指令 RET,从堆栈中弹出 3 级演示代码段的选择子,RPL=3,而当时 CPL=0,所以导致向外层变换特权级,从 0 级的过渡代码段变换到 3 级的演示代码段,同时切换到3 级堆栈。

另一处是从 1 级的显示子程序 EchoSub 返回到 3 级的演示程序段。该处的 RET 指令与演示程序中使用的通过调用门的段间调用指令 CALL 相对应,执行段间返回指令 RET 时的 1 级堆栈也如上图所示,其中的返回地址和外层栈指针由 CALL 指令压入。

(2)通过调用门实现特权级变换

实例在两处使用了段间调用指令,通过调用门实现特权级的变换。一处是 3 级演示代码通过调用门 ToEchoGate 调用 1 级的显示子程序。调用门 ToEchoGate 自身的 DPL=3,只有这样,3 级的演示代码才能够使用该调用门。由于调用门内的选择子 Echo_Sel3 所指示的显示子程序代码段描述符 DPL=1,而当时 CPL=3,所以引起从外层特权级向内层特权级的变换,使 CPL=1。同时形成如上图所示的 1级堆栈。虽然调用门内的选择子 Echo_Sel3 的 RPL=3,大于目标代码段的 DPL,但没有关系,因为在通过调用门转移时,门内指示目标代码段的选择子 RPL 总被作 0 对待。

另一处是 3 级演示代码还通过调用门 ToT32GateB 调用了 0 级的过渡代码。该处使用的调用门描述符 DPL 也等于 3。由于调用门内的选择子 T32Code_Sel 所指示的过渡代码段描述符的 DPL=0,而当时 CPL=3,所以引起从 3 特权级向 0 特权级的变换,使 CPL=0。同时形成如上图所示的 0 级堆栈。但该处的调用实际上是 “有去无回”的,调用的目的是转移到 0 级的过渡代码,准备返回实模式。由于从 3级的演示代码到 0 级的过渡代码要发生特权级变换,所以不能使用转移指令 JMP,必须使用调用指令 CALL。

(3)通过调用门实现无特权级变换
在临时代码段中,使用调用门 ToT32GateA 转移到过渡代码段。尽管调用门内的选择子 T32Code_Sel 所指示的过渡代码段描述符的

DPL=0,但当时 CPL=0,所以不发生特权级变换。正是这个原因,才可以使用段间转移指令 JMP。

(4)子程序EchoSub的实现

子程序 EchoSub 的功能是显示调用程序执行时的特权级。调用程序的执行特权级在代码段寄存器 CS 内选择子的 RPL 字段,在调用EchoSub 时,CS 寄存器的内容被压入堆栈。子程序从堆栈取得调用程序的代码段选择子,再从中分离出 RPL 就可得调用程序的执行特权级。

(5)装载任务状态段寄存器TR

在任务内发生特权级变换时堆栈也随着自动切换,外层堆栈指针保存在内层堆栈中,而内层堆栈指针存放在当前任务的 TSS 中。所以,在从外层向内层变换时,要访问 TSS(从内层向外层转移时不需要访问 TSS,而只需内层栈中保存的栈指针)。实例在进入保护模式下的临时代码段后,通过如下两条指令装载任务状态段寄存器 TR,使其指向已预置好的任务的 TSS:

mov  ax,DemoTSS_Sel

ltr    ax
LTR 指令是专门用于装载任务状态段寄存器 TR 的指令。该指令的操作数是对应 TSS 段描述符的选择子。LTR 指令从 GDT 中取出

相应的 TSS 段描述符,把 TSS 段描述符的基地址和界限等信息装入 TR 的高速缓冲寄存器中。 


我要说的:
如果从开始慢慢学到这里的话,看这个代码应该没什么大问题的,说一下调用栈里面的选择子显示CPL,TI值的代码

EchoSub2:       mov     al,[ebp+8]               ;从堆栈中取调用程序的选择子
                and     al,3                      ;调用程序的CPL在CS的RPL字段
                add     al,'0'
                mov     ah,4eh                    ;置显示属性(红底黄字)
		sub     edi,10
                stosw
		add     edi,8
		mov     al,[ebp+8]               ;从堆栈中取调用程序的选择子
                and     al,1                     ;调用程序TI字段
                add     al,'0'
                mov     ah,4eh  
		stosw
                pop     ebp
                retf

为什么是ebp+8?调用这段代码的是

CALL32  ToEcho_Sel,0              ;显示当前特权级(变换到1级)

该代码所在段是3级,转到1级代码段,发生特权级变换,会切换栈,将当前的段选择子(被扩展为32位)和偏移(32位)压入3级栈,进入到1级代码段,由外层栈切换到内层栈,然后进行参数复制,复制开始压入的段选择子和偏移,然后push ebp,1级栈里面存的就是这些了示意图如下:




目前还有一个问题未弄明白:

TempCode        Desc    <0ffffh,TempCodeSeg,,ATCE,,>  ;注意它的存储段描述符

我在测试的时候发现它的选择子的段界限不能是TempCodeLen-1(或TempCodeLen),前者直接让DOS崩溃了,TempCodeLen+4到是可以正常输出,但是就是没有办法正常返回,段界限为0ffffh就可以正常执行并返回,难道是存在访问越界,被处理器终止或者访问出错吗,这点现在我还没有很好的想明白。

运行结果:



下面是书上的代码(Linux平台)在特权级切换的同时输出字符(这里我做了一点小改动)


这里的value是特权级为3的选择子的输出,本来打算直接在ring3代码段中输出,后来发现DOS会崩溃,突然才发现数据段CPL=0,而ring3的CPL=3,不能访问数据段,所以就干脆放到了CPL=0的代码段(LABEL_SEG_CODE32)中输出了。


%macro Descriptor 3

	dw	%2 & 0FFFFh				; 段界限 1				(2 字节)

	dw	%1 & 0FFFFh				; 段基址 1				(2 字节)

	db	(%1 >> 16) & 0FFh			; 段基址 2				(1 字节)

	dw	((%2 >> 8) & 0F00h) | (%3 & 0F0FFh)	; 属性 1 + 段界限 2 + 属性 2		(2 字节)

	db	(%1 >> 24) & 0FFh			; 段基址 3				(1 字节)

%endmacro ; 共 8 字节



%macro Gate 4

	dw	(%2 & 0FFFFh)				; 偏移 1				(2 字节)

	dw	%1					; 选择子				(2 字节)

	dw	(%3 & 1Fh) | ((%4 << 8) & 0FF00h)	; 属性					(2 字节)

	dw	((%2 >> 16) & 0FFFFh)			; 偏移 2				(2 字节)

%endmacro ; 共 8 字节



DA_32		EQU	4000h	; 32 位段

DA_DPL3		EQU	  60h	; DPL = 3

; 存储段描述符类型值说明

;----------------------------------------------------------------------------

DA_DR		EQU	90h	; 存在的只读数据段类型值

DA_DRW		EQU	92h	; 存在的可读写数据段属性值

DA_DRWA		EQU	93h	; 存在的已访问可读写数据段类型值

DA_C		EQU	98h	; 存在的只执行代码段属性值



;----------------------------------------------------------------------------

; 系统段描述符类型值说明

;----------------------------------------------------------------------------

DA_LDT		EQU	  82h	; 局部描述符表段类型值

DA_TaskGate	EQU	  85h	; 任务门类型值

DA_386TSS	EQU	  89h	; 可用 386 任务状态段类型值

DA_386CGate	EQU	  8Ch	; 386 调用门类型值

; 选择子类型值说明

; 其中:

;       SA_  : Selector Attribute



SA_RPL0		EQU	0	; ┓

SA_RPL1		EQU	1	; ┣ RPL

SA_RPL2		EQU	2	; ┃

SA_RPL3		EQU	3	; ┛



SA_TIG		EQU	0	; ┓TI

SA_TIL		EQU	4	; ┛





org	0100h

	jmp	LABEL_BEGIN



[SECTION .gdt]

; GDT

;                            段基址,           段界限     , 属性

LABEL_GDT:             Descriptor 0,                 0, 0		   ;空描述符

LABEL_DESC_NORMAL:     Descriptor 0,            0ffffh, DA_DRW		   ;Normal描述符

LABEL_DESC_CODE32:     Descriptor 0,    SegCode32Len-1, DA_C+DA_32	   ;非一致,32

LABEL_DESC_CODE16:     Descriptor 0,            0ffffh, DA_C		   ;非一致,16

LABEL_DESC_CODE_DEST:  Descriptor 0,  SegCodeDestLen-1, DA_C+DA_32	   ;非一致,32

LABEL_DESC_CODE_RING3: Descriptor 0, SegCodeRing3Len-1, DA_C+DA_32+DA_DPL3

LABEL_DESC_DATA:       Descriptor 0,	     DataLen-1, DA_DRW             ;Data

LABEL_DESC_STACK:      Descriptor 0,        TopOfStack, DA_DRWA+DA_32	   ;Stack,32

LABEL_DESC_STACK3:     Descriptor 0,       TopOfStack3, DA_DRWA+DA_32+DA_DPL3

LABEL_DESC_LDT:        Descriptor 0,          LDTLen-1, DA_LDT		   ;LDT

LABEL_DESC_TSS:        Descriptor 0,          TSSLen-1, DA_386TSS	   ;TSS

LABEL_DESC_VIDEO:      Descriptor 0B8000h,      0ffffh, DA_DRW+DA_DPL3



; 门                                            目标选择子,       偏移, DCount, 属性

LABEL_CALL_GATE_TEST:	Gate		  SelectorCodeDest,          0,      0, DA_386CGate + DA_DPL3

; GDT 结束



GdtLen		equ	$ - LABEL_GDT	; GDT长度

GdtPtr		dw	GdtLen - 1	; GDT界限

		dd	0		; GDT基地址



; GDT 选择子

SelectorNormal		equ	LABEL_DESC_NORMAL	- LABEL_GDT

SelectorCode32		equ	LABEL_DESC_CODE32	- LABEL_GDT

SelectorCode16		equ	LABEL_DESC_CODE16	- LABEL_GDT

SelectorCodeDest	equ	LABEL_DESC_CODE_DEST	- LABEL_GDT

SelectorCodeRing3	equ	LABEL_DESC_CODE_RING3	- LABEL_GDT + SA_RPL3

SelectorData		equ	LABEL_DESC_DATA		- LABEL_GDT

SelectorStack		equ	LABEL_DESC_STACK	- LABEL_GDT

SelectorStack3		equ	LABEL_DESC_STACK3	- LABEL_GDT + SA_RPL3

SelectorLDT		equ	LABEL_DESC_LDT		- LABEL_GDT

SelectorTSS		equ	LABEL_DESC_TSS		- LABEL_GDT

SelectorVideo		equ	LABEL_DESC_VIDEO	- LABEL_GDT



SelectorCallGateTest	equ	LABEL_CALL_GATE_TEST	- LABEL_GDT + SA_RPL3

; END of [SECTION .gdt]



[SECTION .data1]	 ; 数据段

ALIGN	32

[BITS	32]

LABEL_DATA:

SPValueInRealMode	dw	0

; 字符串

PMMessage:		db	"In Protect Mode now. ^-^", 0	; 进入保护模式后显示此字符串

OffsetPMMessage		equ	PMMessage - $$

StrTest:		db	"value=", 0

OffsetStrTest		equ	StrTest - $$

DataLen			equ	$ - LABEL_DATA

; END of [SECTION .data1]





; 全局堆栈段

[SECTION .gs]

ALIGN	32

[BITS	32]

LABEL_STACK:

	times 512 db 0

TopOfStack	equ	$ - LABEL_STACK - 1

; END of [SECTION .gs]





; 堆栈段ring3

[SECTION .s3]

ALIGN	32

[BITS	32]

LABEL_STACK3:

	times 512 db 0

TopOfStack3	equ	$ - LABEL_STACK3 - 1

; END of [SECTION .s3]





; TSS ---------------------------------------------------------------------------------------------

[SECTION .tss]

ALIGN	32

[BITS	32]

LABEL_TSS:

		DD	0			; Back

		DD	TopOfStack		; 0 级堆栈

		DD	SelectorStack		; 

		DD	0			; 1 级堆栈

		DD	0			; 

		DD	0			; 2 级堆栈

		DD	0			; 

		DD	0			; CR3

		DD	0			; EIP

		DD	0			; EFLAGS

		DD	0			; EAX

		DD	0			; ECX

		DD	0			; EDX

		DD	0			; EBX

		DD	0			; ESP

		DD	0			; EBP

		DD	0			; ESI

		DD	0			; EDI

		DD	0			; ES

		DD	0			; CS

		DD	0			; SS

		DD	0			; DS

		DD	0			; FS

		DD	0			; GS

		DD	0			; LDT

		DW	0			; 调试陷阱标志

		DW	$ - LABEL_TSS + 2	; I/O位图基址

		DB	0ffh			; I/O位图结束标志

TSSLen		equ	$ - LABEL_TSS

; TSS ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^





[SECTION .s16]

[BITS	16]

LABEL_BEGIN:

	mov	ax, cs

	mov	ds, ax

	mov	es, ax

	mov	ss, ax

	mov	sp, 0100h



	mov	[LABEL_GO_BACK_TO_REAL+3], ax

	mov	[SPValueInRealMode], sp



	; 初始化 16 位代码段描述符

	mov	ax, cs

	movzx	eax, ax

	shl	eax, 4

	add	eax, LABEL_SEG_CODE16

	mov	word [LABEL_DESC_CODE16 + 2], ax

	shr	eax, 16

	mov	byte [LABEL_DESC_CODE16 + 4], al

	mov	byte [LABEL_DESC_CODE16 + 7], ah



	; 初始化 32 位代码段描述符

	xor	eax, eax

	mov	ax, cs

	shl	eax, 4

	add	eax, LABEL_SEG_CODE32

	mov	word [LABEL_DESC_CODE32 + 2], ax

	shr	eax, 16

	mov	byte [LABEL_DESC_CODE32 + 4], al

	mov	byte [LABEL_DESC_CODE32 + 7], ah



	; 初始化测试调用门的代码段描述符

	xor	eax, eax

	mov	ax, cs

	shl	eax, 4

	add	eax, LABEL_SEG_CODE_DEST

	mov	word [LABEL_DESC_CODE_DEST + 2], ax

	shr	eax, 16

	mov	byte [LABEL_DESC_CODE_DEST + 4], al

	mov	byte [LABEL_DESC_CODE_DEST + 7], ah



	; 初始化数据段描述符

	xor	eax, eax

	mov	ax, ds

	shl	eax, 4

	add	eax, LABEL_DATA

	mov	word [LABEL_DESC_DATA + 2], ax

	shr	eax, 16

	mov	byte [LABEL_DESC_DATA + 4], al

	mov	byte [LABEL_DESC_DATA + 7], ah



	; 初始化堆栈段描述符

	xor	eax, eax

	mov	ax, ds

	shl	eax, 4

	add	eax, LABEL_STACK

	mov	word [LABEL_DESC_STACK + 2], ax

	shr	eax, 16

	mov	byte [LABEL_DESC_STACK + 4], al

	mov	byte [LABEL_DESC_STACK + 7], ah



	; 初始化堆栈段描述符(ring3)

	xor	eax, eax

	mov	ax, ds

	shl	eax, 4

	add	eax, LABEL_STACK3

	mov	word [LABEL_DESC_STACK3 + 2], ax

	shr	eax, 16

	mov	byte [LABEL_DESC_STACK3 + 4], al

	mov	byte [LABEL_DESC_STACK3 + 7], ah



	; 初始化 LDT 在 GDT 中的描述符

	xor	eax, eax

	mov	ax, ds

	shl	eax, 4

	add	eax, LABEL_LDT

	mov	word [LABEL_DESC_LDT + 2], ax

	shr	eax, 16

	mov	byte [LABEL_DESC_LDT + 4], al

	mov	byte [LABEL_DESC_LDT + 7], ah



	; 初始化 LDT 中的描述符

	xor	eax, eax

	mov	ax, ds

	shl	eax, 4

	add	eax, LABEL_CODE_A

	mov	word [LABEL_LDT_DESC_CODEA + 2], ax

	shr	eax, 16

	mov	byte [LABEL_LDT_DESC_CODEA + 4], al

	mov	byte [LABEL_LDT_DESC_CODEA + 7], ah



	; 初始化Ring3描述符

	xor	eax, eax

	mov	ax, ds

	shl	eax, 4

	add	eax, LABEL_CODE_RING3

	mov	word [LABEL_DESC_CODE_RING3 + 2], ax

	shr	eax, 16

	mov	byte [LABEL_DESC_CODE_RING3 + 4], al

	mov	byte [LABEL_DESC_CODE_RING3 + 7], ah



	; 初始化 TSS 描述符

	xor	eax, eax

	mov	ax, ds

	shl	eax, 4

	add	eax, LABEL_TSS

	mov	word [LABEL_DESC_TSS + 2], ax

	shr	eax, 16

	mov	byte [LABEL_DESC_TSS + 4], al

	mov	byte [LABEL_DESC_TSS + 7], ah



	; 为加载 GDTR 作准备

	xor	eax, eax

	mov	ax, ds

	shl	eax, 4

	add	eax, LABEL_GDT		; eax <- gdt 基地址

	mov	dword [GdtPtr + 2], eax	; [GdtPtr + 2] <- gdt 基地址



	; 加载 GDTR

	lgdt	[GdtPtr]



	; 关中断

	cli



	; 打开地址线A20

	in	al, 92h

	or	al, 00000010b

	out	92h, al



	; 准备切换到保护模式

	mov	eax, cr0

	or	eax, 1

	mov	cr0, eax



	; 真正进入保护模式

	jmp	dword SelectorCode32:0	; 执行这一句会把 SelectorCode32 装入 cs, 并跳转到 Code32Selector:0  处



;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;



LABEL_REAL_ENTRY:		; 从保护模式跳回到实模式就到了这里

	mov	ax, cs

	mov	ds, ax

	mov	es, ax

	mov	ss, ax



	mov	sp, [SPValueInRealMode]



	in	al, 92h		; ┓

	and	al, 11111101b	; ┣ 关闭 A20 地址线

	out	92h, al		; ┛



	sti			; 开中断



	mov	ax, 4c00h	; ┓

	int	21h		; ┛回到 DOS

; END of [SECTION .s16]





[SECTION .s32]; 32 位代码段. 由实模式跳入.

[BITS	32]



LABEL_SEG_CODE32:

	mov	ax, SelectorData

	mov	ds, ax			; 数据段选择子

	mov	ax, SelectorVideo

	mov	gs, ax			; 视频段选择子



	mov	ax, SelectorStack

	mov	ss, ax			; 堆栈段选择子



	mov	esp, TopOfStack





	; 下面显示一个字符串

	mov	ah, 0Ch			; 0000: 黑底    1100: 红字

	xor	esi, esi

	xor	edi, edi

	mov	esi, OffsetPMMessage	; 源数据偏移

	mov	edi, (80 * 10 + 0) * 2	; 目的数据偏移。屏幕第 10 行, 第 0 列。

	cld

.1:

	lodsb

	test	al, al

	jz	.2

	mov	[gs:edi], ax

	add	edi, 2

	jmp	.1

.2:	; 显示完毕



	call	DispReturn


;显示LABEL_CODE_RING3的CPL
        mov     ax,SelectorData
	mov 	ds,ax
	mov 	esi,OffsetStrTest

	mov 	ax,SelectorVideo
	mov 	es,ax
	mov     ah,4eh                    ;置显示属性(红底黄字)
EchoSub1: 
	lodsb
        or      al,al
        jz      EchoSub2
        stosw
        jmp     EchoSub1
EchoSub2:
	mov ax,SelectorCodeRing3             
        and     al,3                      ;调用程序的CPL在CS的RPL字段
        add     al,'0'
        mov     ah,4eh                    ;置显示属性(红底黄字)
        mov	[gs:edi], ax

	; Load TSS

	mov	ax, SelectorTSS

	ltr	ax	; 在任务内发生特权级变换时要切换堆栈,而内层堆栈的指针存放在当前任务的TSS中,所以要设置任务状态段寄存器 TR。

	push	SelectorStack3

	push	TopOfStack3

	push	SelectorCodeRing3

	push	0

	retf		; Ring0 -> Ring3,历史性转移!将打印数字 '3'。



; ------------------------------------------------------------------------

DispReturn:

	push	eax

	push	ebx

	mov	eax, edi

	mov	bl, 160

	div	bl

	and	eax, 0FFh

	inc	eax

	mov	bl, 160

	mul	bl

	mov	edi, eax

	pop	ebx

	pop	eax



	ret

; DispReturn 结束---------------------------------------------------------



SegCode32Len	equ	$ - LABEL_SEG_CODE32

; END of [SECTION .s32]





[SECTION .sdest]; 调用门目标段

[BITS	32]



LABEL_SEG_CODE_DEST:

	mov	ax, SelectorVideo

	mov	gs, ax			; 视频段选择子(目的)



	mov	edi, (80 * 12 + 0) * 2	; 屏幕第 12 行, 第 0 列。

	mov	ah, 0Ch			; 0000: 黑底    1100: 红字

	mov	al, 'C'

	mov	[gs:edi], ax

	; Load LDT

	mov	ax, SelectorLDT

	lldt	ax

	jmp	SelectorLDTCodeA:0	; 跳入局部任务,将打印字母 'L'。



SegCodeDestLen	equ	$ - LABEL_SEG_CODE_DEST

; END of [SECTION .sdest]





; 16 位代码段. 由 32 位代码段跳入, 跳出后到实模式

[SECTION .s16code]

ALIGN	32

[BITS	16]

LABEL_SEG_CODE16:

	; 跳回实模式:

	mov	ax, SelectorNormal

	mov	ds, ax

	mov	es, ax

	mov	fs, ax

	mov	gs, ax

	mov	ss, ax



	mov	eax, cr0

	and	al, 11111110b

	mov	cr0, eax



LABEL_GO_BACK_TO_REAL:

	jmp	0:LABEL_REAL_ENTRY	; 段地址会在程序开始处被设置成正确的值



Code16Len	equ	$ - LABEL_SEG_CODE16



; END of [SECTION .s16code]





; LDT

[SECTION .ldt]

ALIGN	32

LABEL_LDT:

;                                         段基址       段界限     ,   属性

LABEL_LDT_DESC_CODEA:	Descriptor	       0,     CodeALen - 1,   DA_C + DA_32	; Code, 32 位



LDTLen		equ	$ - LABEL_LDT



; LDT 选择子

SelectorLDTCodeA	equ	LABEL_LDT_DESC_CODEA	- LABEL_LDT + SA_TIL

; END of [SECTION .ldt]





; CodeA (LDT, 32 位代码段)

[SECTION .la]

ALIGN	32

[BITS	32]

LABEL_CODE_A:

	mov	ax, SelectorVideo

	mov	gs, ax			; 视频段选择子(目的)



	mov	edi, (80 * 13 + 0) * 2	; 屏幕第 13 行, 第 0 列。

	mov	ah, 0Ch			; 0000: 黑底    1100: 红字

	mov	al, 'L'

	mov	[gs:edi], ax



	; 准备经由16位代码段跳回实模式

	jmp	SelectorCode16:0

CodeALen	equ	$ - LABEL_CODE_A

; END of [SECTION .la]





; CodeRing3

[SECTION .ring3]

ALIGN	32

[BITS	32]

LABEL_CODE_RING3:

	mov	ax, SelectorVideo

	mov	gs, ax			; 视频段选择子(目的)



	mov	edi, (80 * 14 + 0) * 2	; 屏幕第 14 行, 第 0 列。

	mov	ah, 0Ch			; 0000: 黑底    1100: 红字

	mov	al, '3'

	mov	[gs:edi], ax

	call	SelectorCallGateTest:0	; 测试调用门(有特权级变换),将打印字母 'C'。                 

SegCodeRing3Len	equ	$ - LABEL_CODE_RING3

; END of [SECTION .ring3]



在代码段LABEL_DESC_CODE32中有这么一段代码。

	push	SelectorStack3

	push	TopOfStack3

	push	SelectorCodeRing3

	push	0

	retf		; Ring0 -> Ring3,历史性转移!将打印数字 '3'。

后面三句语句都理解吧,就是把选择子,偏移压栈,利用retf指令返回,但是为什么要前两个语句了,这里需要回顾一下:

《Orange's 一个操作系统的实现》学习笔记--特权级代码段之间的转移(二)里面的内容,看下面图示:


有ring0到ring3转移过程中,会把调用者的ss,esp恢复,并且esp调整,废除开始前入栈的参数,但是我们这里没有从ring3进入ring0,返回从ring0直接进入ring3,所以这里我们需要自己手动的把目的栈的选择子和偏移压入栈,这样才能顺利执行,这里没有函数参数,所以返回后不用调整esp.



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对x86架构的处理器,业界一向是褒贬不一。但是毫无疑问的是,x86架构的处理器是迄今为止在市场上最成功的处理器。它既催生了Intel、微软这样的业界巨头,也改变了普通人们的生活。到今天,虽然有arm的异军突起,但是大部分程序员所编写的程序依然在运行在x86架构上。 虽然很多上层的程序员绕过了对CPU架构本身的理解,而直接使用高语言进行编程,但是对CPU本身的熟悉,其实依然是所有想被称为优秀的程序员所难以绕过的一道坎。对CPU的不熟悉,实际上限制了程序员的思维方式、对程序的理解和实际解决问题的能力。 对于普通的芯片,阅读几页的说明书就可以大致理解如何让它工作。CPU是比较复杂的一种,对于比较简单的CPU架构,阅读几十页的文档也能大致熟悉。然而 x86架构的CPU的说明可不是这么简单,其手册估计有一共有、五千页之巨。不要说理解透彻,就是从头到尾翻一遍也不是一件简单的事情。并非Intel 有意将它做得复杂,这里有历史的原因。因为这个架构的应用实在太广了,全世界有无数的软件都在它的基础之上工作。为此它自身的升也就变得举步维艰。每次升都不得不要兼容之前的特性。这也就导致了新旧指令层层堆积,种种特性互相兼顾,最终变成如今的一团乱麻了。 对于入的选手,读完那些手册可不是一件容易的事情。但是于渊的这本《orange's:一个操作系统实现》却是一条难得的终南捷径。因为要理解如何让一个芯片正常工作,最简单的办法就是从头开始去写程序让它运行起来,然后操作它做自己想做的事情。如果是平时的编程,这些下层的工作都已经有操作系统帮你做了,对理解x86架构的帮助就大为有限。如果去读那几千页的文档,不但读起来很痛苦,中间又没有多少可以实际操作的工作来帮助你温故而知新,这其中的枯燥乏味,绝对不是一般人可以忍受的了。而且更重要的手册中虽然包含了x86所有的特性,然而其中有些特性是现代操作系统根本就没有用到的。努力的去理解的话,又是吃力不讨好了。如果每个读者都可以随着这本书的介绍,去逐步的实现一个操作系统,不但这中间其乐无穷,而且实现到最后,对x86架构的理解也就不在话下。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值