调试寄存器(debug registers, DRx)理论及实践

导读: 

标 题:DRx寄存器的使用(待续) (4千字)
发信人:hume  
时 间:2003-06-18 17:33:11
详细信息:




调试寄存器(DRx)理论与实践
By Hume/冷雨飘心
前言+牢骚:
生活的苦痛就象烈火,时时煎熬着伤痕累累疲惫不堪的那颗心。我拼力挣扎,然而却无济于事……
太残酷了….上帝也在苦笑。
                                              题记

很多人问Drx调试寄存器的用法,网上实际上有很多资料,但是很多人还是不肯去翻,于是我来写点东西,算是给对DRx尚有疑惑的解答吧。不要在QQ上问我任何问题,如果有错误,请来信指明:Humewen@21cn.com。

1.一点理论
=====
本文假设你已经知道调试器的使用方法以及bpx,bpm,int 3,异常都分别是什么,本文不是一篇超级扫盲教程。
Intel 80386以上的CPU提供了调试寄存器以用于软件调试。386和486包括6个调试寄存器:Dr0,Dr1,Dr2,Dr3,Dr6和Dr7。这些寄存器全是32位,如下图所示:

   |---------------|----------------|
Dr0|                 用于一般断点的线性地址                    
   |---------------|----------------|
Dr1|                 用于一般断点的线性地址                    
   |---------------|----------------|
Dr2|                 用于一般断点的线性地址                    
   |---------------|----------------|
Dr3|                 用于一般断点的线性地址                    
   |---------------|----------------|
Dr4|                     保留                                
   |---------------|----------------|
Dr5|                     保留                                
   |---------------|----------------|
Dr6|                              |BBB                     BBB B |
   |                              |TSD                      3 2 1 0 |
   |---------------|----------------|
Dr7|RWE LEN   ...    RWE LEN    |  G               GLGLGLGLGL |
   | 3   3    ...        0    0     |  D               E E 3 3 2 21 100 |
   |---------------|----------------|
31                            15                                0


点击图片以查看大图图片名称:	DR0_DR7.jpg查看次数:	2391文件大小:	63.6 KB文件 ID :	39226

Dr0~3用于设置硬件断点,即在调试器中经常使用的bpm断点,由于只有4个断点寄存器,所以最多只能设置4个bpm断点。Dr7是一些控制位,用于控制断点的方式,Dr6用于显示是哪些引起断点的原因,如果是Dr0~3或单步(EFLAGS的TF)或由于GD置位时访问调试寄存器引起1号调试陷阱的话,则相应设置对应的位。下面对Dr6和Dr7的对应位做一些详细介绍:

调试控制寄存器Dr7:
==========
位0 L0和位1 G0:用于控制Dr0是全局断点还是局部断点,如果G0置位则是全局断点,L0置位则是局部断点。
G1L1~G3L3用于控制D1~Dr3,其功能同上。

LEN0:占两个位,开始于位15,用于控制Dr0的断点长度,可能取值:
00  1字节
01  2字节
10  保留
11  4字节
RWE0:从第17位开始,占两个位,控制Dr0的断点是读、写还是执行断点或是I/O端口断点:
00  只执行
01 写入数据断点
10 I/O端口断点(只用于pentium+,需设置CR4的DE位)
11 读或写数据断点
RWE1~3,LEN1~3分别用于控制Dr1~3的断点方式,含义如上。

还有一个GD位:用于保护DRx,如果GD位为1,则对Drx的任何访问都会导致进入1号调试陷阱。即IDT的对应入口,这样可以保证调试器在必要的时候完全控制Drx。

调试状态寄存器Dr6:
=========
该寄存器用于表示进入陷阱1的原因,各个位的含义如下:
B0~B3,如果其中任何一个位置位,则表示是相应的Dr0~3断点引发的调试陷阱。但还需注意的是,有时候不管GiLi如何设置,只要是遇到Drx指定的断点,总会设置Bi,如果看到多个Bi置位,则可以通过GiLi的情况判断究竟是哪个Dr寄存器引发的调试陷阱。
BD置位表示是GD位置位情况下访问调试寄存器引发的陷阱。
BT置位表示是因为TS置位即任务切换时TSS中TS位置1时切到第二个任务时第一条指令时引发的。
BS置位表示是单步中断引发的断点。。。。即EFLAGS的TF置位时引发的调试陷阱。

注意I/O端口断点是586+以上CPU才有的功能,受CR4的DE位的控制,DE为1才有效。(DE是CR4的第3位)。

2、一点常识
======
如果你使用调试器的话,一定清楚bpx断点,bpx实际上就通过在代码中插入int 3(0xCC或0xCD03),将引发int 3中断。具体的intel IA-32保护模式异常机制并不是我三言两语能解释清楚的,如必要请参照相关资料。
Bpm和BPIO断点是利用CPU的硬件调试器设置的断点。
有些调试功能只在586+以上CPU才能使用,为增强兼容性用CPUID测试。(测试方法见我后续文章。)
关于调试API,是Windows提供给开发者的调试原API。具体实现涉及windows的内部机制,不是简单的int x就能解释清楚的,有兴趣者可参阅相关资料,或等我有时间胡说一通。


3、一点实践

设置BPM断点很简单,只要相应设置Drx即可,产生的异常是STATUS_SINGLE_STEP,只要用调试API或SEH或VEH处理一下即可。下面是9X下测试BPM断点的一个例子:

A::->
-----------
COMMENT/*

仅工作于9X下,BPM产生的异常类型是SINGLE_STEP
可用来对付调试器,反跟踪,至于效果,试试就知道了
这里采用SEH来设置9x下的Drx寄存器
还可采用GetThreadContext、SetThreadContext设置Drx的值
NT类系统由于安全机制,无法使用该办法
需使用Debug技术

*/
include c:/hd/hhd.h
include c:/hd/drx.h
ASSUME FS:NOTHING
;~~~~~~~~~~~~~~~~~~~
.CODE
_StArT:
        int 3
        SLDT    cx
        JCXZ    isNT
        JMP     @F
     isNT:
        MsgBox  CTEXT("NT Series Not Work! only 9X!")
        JMP     _XXX_
     @@:
        call    instSEH

     XH01       PROC    C  pExcept,pFrame,pContext,pDispatch    Not minimal form
        ASSUME  ESI:PTR EXCEPTION_RECORD,EDI:PTR CONTEXT
        MOV     ESI,pExcept
        MOV     EDI,pContext
        MOV     EAX,1
        TEST    [ESI].ExceptionFlags,7
        JNZ     @@Not_handled
        cmp     [esi].ExceptionCode,STATUS_ILLEGAL_INSTRUCTION
        jz      illegal_instr
        cmp     [esi].ExceptionCode,STATUS_SINGLE_STEP
        JZ      BPM0_ISOK
        jmp     @@Not_handled
      BPM0_ISOK:
        MOV     [EDI].regEip,OFFSET MSGbpmOK
        JMP     SEHexit

        //Set the Dr0 bpm Global BreakPoint
      illegal_instr:
        MOV     [EDI].ContextFlags,CONTEXT_DEBUG_REGISTERS or CONTEXT_FULL
        MOV     [EDI].iDr7,M_INSTR0 or M_GDR0 or M_BYTE0
        MOV     [EDI].iDr0,OFFSET bpm01
        //ByPASS the INVALID INSTRS
        ADD     [EDI].regEip,2           

      SEHexit:
        DEC     EAX
      @@Not_handled:
        ret
     XH01       ENDP

     instSEH:
        LEA     eax,[esp-4]
        XCHG    eax,fs:[0]
        push    eax

        NOP
        DB 0Fh,0Bh              INVALID INSTRS ON ALL PLATFORMS =UD2

     bpm01:                     SIMPLE ANTI DEBUGER
        NOP
        jmp     bpm01

     _XXX_:
        POP     fs:[0]
        pop     EAX   
invoke ExitProcess,0
     MSGbpmOK:
        MsgBox  CTEXT("Hello BPM01 TEST SUC,Prepare TO EXIT")
        JMP     _XXX_
END _StArT

WINNT下设置断点并非如此简单,由于严格的检查机制,原来的SEH的方法或简单的GetThreadContext方法已经失效。那么要我们写驱动吗?大可不必,因为WIndows还提供了调试API,使用调试API可以为目标程序设置断点:

WINNT下设置断点并非如此简单,由于严格的检查机制,原来的SEH的方法或简单的SetThreadContext方法已经失效。那么要我们写驱动吗?大可不必,因为WIndows还提供了调试API,使用调试API可以为目标程序设置断点。使用调试程序设置断点仍然使用GetThreadContext和SetThreadContext两个API。

在看下一个例子之前,还是让我们看看著名的IczeLion的教程中利用调试API检测程序执行了多少指令这一例子,这个例子在9X下运行正常,但在2K/Xp下却显示程序初始化失败。究其原因在于在2K/Xp下程序收到第一个断点异常时被调试程序的主线程内容并未准备好,是无效的!很奇怪但确实如此,这时根本不能通过CONTEXT设置断点或单步等。解决方案之一是Elicz提出来的,就是先在调试程序收到的第一个断点中断中给NtContinue设一bpm断点,截获其第一个参数,这个参数就是指向即将初始化为主线程有效CONTEXT内容的指针,通过在其内设置一个bpm断点,就可以中断在我们想要中断的任何位置。
下面是改造过的计算程序执行指令条数的例子,测试结果显示,一个很简单的程序,
include c:/hd/hhd.h
.DATA?
hInstance dd ?
;;-----------------------------------------
.CODE
_StArT:
        nop
        nop
mov hInstance,$invoke(GetModuleHandle,0)
MsgBox  CTEXT("Hello World")

invoke ExitProcess,0
END _StArT


执行的内核代码+程序代码19196764条,如果只想获得程序执行的程序代码,还是有办法的,自己去想啦。
本文不是讲述调试API的使用,使用见IczeLion的教程:
;
include c:/hd/hhd.h
include c:/hd/drx.h

BREAK_RVA EQU   401000H         断点。。。。
;~~~~~~~~~~~~~~~~~~~
.DATA
dwSScnt dd 0
sinstr  dd 0        
pi      PROCESS_INFORMATION <>
sif     STARTUPINFO 
Dev     DEBUG_EVENT <>
Rs      CONTEXT 
buf     db 256 dup(?)
dwBuf   dd 0
dwBytes dd 0
;;-----------------------------------------
.CODE
WipeContextBPdr0        Proc
        invoke GetThreadContext,pi.hThread,addr Rs
        mov        Rs.iDr0,0
        mov        Rs.iDr7,0
        invoke SetThreadContext,pi.hThread,addr Rs
        ret
WipeContextBPdr0        Endp
;-----------------------------------------

_StArT:
        SLDT    CX
        JCXZ    @F
        MsgBox  CTEXT("9x Not supported")
        JMP     pExit
        @@:
        invoke GetStartupInfo,addr sif
        SUB     EAX,EAX
invoke CreateProcess,0,CTEXT("target01.exe"),EAX,EAX,EAX,/
                DEBUG_PROCESS or DEBUG_ONLY_THIS_PROCESS,EAX,EAX,addr sif,addr pi
        JEAXZ   crp_fail

            //some basic text macro 
            //why so many ppl use long names,aren't they tired ?
            wmDevent    EQU Dev.dwDebugEventCode
            excCode     EQU Dev.u.Exception.pExceptionRecord.ExceptionCode
            excAddr     EQU Dev.u.Exception.pExceptionRecord.ExceptionAddress
            pDllName    EQU Dev.u.LoadDll.lpImageName
            lpBase      EQU Dev.u.LoadDll.lpBaseOfDll
            //

        .WHILE TRUE
            invoke WaitForDebugEvent,addr Dev,INFINITE
            mov         
            .IF wmDevent==EXIT_PROCESS_DEBUG_EVENT
                MsgBox CTEXT("target Exit...")
                .break
            .ELSEIF wmDevent==LOAD_DLL_DEBUG_EVENT
                //to see how many DLLS were LOADED
                invoke wsprintf,addr buf,CTEXT("DLL BASE =: %08X"),lpBase                                       
                invoke MessageBox,0,addr buf,0,0
                JMP  DBG_con
            .ELSEIF wmDevent==EXCEPTION_DEBUG_EVENT

                .if  excCode==STATUS_BREAKPOINT
                     invoke GetThreadContext,pi.hThread,addr Rs
                     invoke GetProcAddress,$invoke(GetModuleHandle,CTEXT("NTDLL.DLL")),CTEXT("NtContinue")
                     JEAXZ      end_debug
                     mov        Rs.iDr0,EAX             设置NtContinue的断点
                     mov        Rs.iDr7,M_LDR0 or M_INSTR0

                     invoke SetThreadContext,pi.hThread,addr Rs
                     JMP  DBG_con
                .elseif excCode==STATUS_SINGLE_STEP
                     INC        dwSScnt
                     .if        dwSScnt==1

                        //清除NtContinue的bpm断点                         
                         invoke WipeContextBPdr0
                         MOV    EDX,Rs.regEsp
                         ADD    EDX,4           //获取NtContinue的第一个参数pContext

                         //读入pContext值,因为是两个进程要用ReadProcessMemory
                         invoke ReadProcessMemory,pi.hProcess,EDX,addr dwBuf,sizeof DWORD,addr dwBytes
                         //读入真正的CONTEXT值
                         invoke ReadProcessMemory,pi.hProcess,dwBuf,addr Rs,sizeof CONTEXT,addr dwBytes
                         mov    Rs.iDr0,BREAK_RVA
                         mov    Rs.iDr7,M_LDR0 or M_INSTR0
                         invoke WriteProcessMemory,pi.hProcess,dwBuf,addr Rs,sizeof CONTEXT,addr dwBytes
                         JMP    DBG_con
                     .elseif    dwSScnt==2
                         inc    sinstr
                         invoke WipeContextBPdr0        //清除断点
                         invoke wsprintf,addr buf,CTEXT("Break Address: %08X"),excAddr
                         MsgBox addr buf
                     .endif

                     comment $
                     //ICZELION's Example,uncomment this
                     inc        sinstr
                     invoke GetThreadContext,pi.hThread,addr Rs
                     invoke wsprintf,addr buf,CTEXT("XINSTR ADDR %08X"),Rs.regEip
                     MsgBox     addr buf
                     or         Rs.regFlag,100h
                     invoke SetThreadContext,pi.hThread,offset Rs
                     $

                     JMP  DBG_con
                .endif     

            .ENDIF
        
      DBG_no:     //continue Debug events without handling
            invoke ContinueDebugEvent,Dev.dwProcessId,Dev.dwThreadId,DBG_EXCEPTION_NOT_HANDLED
            .continue
      DBG_con:
            invoke ContinueDebugEvent,Dev.dwProcessId,Dev.dwThreadId,DBG_CONTINUE
        .ENDW
        
        invoke wsprintf,addr buf,CTEXT("total : %08d Instructions"),sinstr
        MsgBox  addr buf
    end_debug:
        invoke CloseHandle,pi.hProcess
        invoke CloseHandle,pi.hThread
    pExit:
invoke ExitProcess,0
    crp_fail:
        MsgBox  CTEXT("Create Process Failed!")
        jmp     pExit
END _StArT

只要能使用Drx的断点功能就可以配合SEH、调试API进行一些反跟踪等,具体怎么用,取决于你自己了。

〔完〕
=================================================================================================
一些常量定义(FOR MASM)
; for DR6
M_HIT0                         EQU 1
M_HIT1                         EQU 2
M_HIT2                         EQU 4
M_HIT3                         EQU 8
M_BD                         EQU 2000H         DRX access
M_BS                         EQU 4000H         single step
M_BT                         EQU 8000H         task switch
;-----------------------------------------

; for DR7
M_LDR0                         EQU 01           Li mask for Dr7
M_LDR1                         EQU M_LDR0    SHL 02
M_LDR2                         EQU M_LDR1    SHL 02
M_LDR3                         EQU M_LDR2    SHL 02
M_LDRALL EQU M_LDR0 or M_LDR1 or M_LDR2 or M_LDR3

M_GDR0                         EQU 02           Gi mask for Dr7
M_GDR1                         EQU M_GDR0    SHL 02
M_GDR2                         EQU M_GDR1    SHL 02
M_GDR3                         EQU M_GDR2    SHL 02
M_GDRALL EQU M_GDR0 or M_GDR1 or M_GDR2 or M_GDR3

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

M_LE                         EQU 01      SHL 08   局部断点精确相符
M_GE                         EQU M_LE     SHL 01   全局断点精确相符

;DRX access
M_GD                         EQU M_BD              drx保护位置一即使在ring0也产生int 1

;instruction fetch
M_INSTR0                        EQU 00
M_INSTR1                        EQU 00
M_INSTR2                        EQU 00
M_INSTR3                        EQU 00

;memory write
M_WRITE0                        EQU 01 SHL 16
M_WRITE1                        EQU M_WRITE0 SHL 04
M_WRITE2                        EQU M_WRITE1 SHL 04
M_WRITE3                        EQU M_WRITE2 SHL 04

;port watches: 586+ only and CR4 bit DE (=04  bit 3 in termiology) must be 1
;M_DE EQU 04
M_DE EQU 08                
M_PORT0                         EQU 02 SHL 16
M_PORT1                         EQU M_PORT0  SHL 04
M_PORT2                         EQU M_PORT1  SHL 04
M_PORT3                         EQU M_PORT2  SHL 04

;memory access Read or Write!
M_RW0                         EQU 03 SHL 16
M_RW1                         EQU M_RW0    SHL 04
M_RW2                         EQU M_RW1    SHL 04
M_RW3                         EQU M_RW2    SHL 04
M_RWALL EQU M_RW0 or M_RW1 or M_RW2 or M_RW3

;lengths
M_BYTE0                         EQU 00           use byte for instruction by default always
M_BYTE1                         EQU 00            for data BreakPoints,these exist
M_BYTE2                         EQU 00
M_BYTE3                         EQU 00

M_WORD0                         EQU 01 SHL 18
M_WORD1                         EQU M_WORD0  SHL 04
M_WORD2                         EQU M_WORD1  SHL 04
M_WORD3                         EQU M_WORD2  SHL 04

M_DWORD0                        EQU 03 SHL 18
M_DWORD1                        EQU M_DWORD0 SHL 04
M_DWORD2                        EQU M_DWORD1 SHL 04
M_DWORD3                        EQU M_DWORD2 SHL 04
=============================================================================================
网上非
盈利组织或团体个人转载时,请你尊重一下看雪论坛和作者,注明转自


















 ©2000-2005 PEdiy.com All rights reserved. 
By KanXue Studio



src="about:blank" frameborder="0" scrolling="no" style="border-width: 0px; border-style: initial; margin: 5px 0px 7px; overflow: hidden; width: 6px; display: inline; height: 385px;"> src="about:blank" frameborder="0" scrolling="no" style="border-width: 0px; border-style: initial; margin: 0px; overflow: hidden; width: 535px; display: inline; height: 397px;"> src="about:blank" frameborder="0" scrolling="no" style="border-width: 0px; border-style: initial; margin: 7px 0px; overflow: hidden; width: 8px; display: inline; height: 383px;">

id="hylandaClientFrame" src="http://ds.hylanda.com/loading.php?comType=2&hylandaCharSet=IEGB" frameborder="0" name="hylandaClientFrame" scrolling="no" style="overflow: hidden; width: 534px; display: block; height: 358px;">



本文转自 
http://www.pediy.com/bbshtml/bbs6/pediy6751.htm
<think>好的,我现在需要回答用户的问题:“寄存器Registers)是否属于主存(Main Memory)的范畴?” 用户之前已经询问过主存的详细内容,现在希望明确寄存器的归属。 首先,我要回顾一下之前关于主存的讲解,主存主要是DRAM,属于CPU直接访问的易失性存储,而寄存器在计算机体系结构中通常指的是CPU内部的存储单元。根据之前的分类,主存和高速缓存(Cache)都是存储器层次结构的一部分,但寄存器位于更靠近CPU的位置。 接下来需要确认几个关键点:存储层次结构、访问速度、容量、功能作用。主存(Main Memory)通常是DRAM,位于存储层次中的中间层,介于高速缓存和辅存(如磁盘)之间。而寄存器则位于CPU内部,速度最快,容量最小,用于临时存放指令和数据,直接参与运算。 需要区分主存和寄存器的不同点:物理位置、速度、容量、管理方式。主存由操作系统管理,通过地址总线访问;寄存器由CPU直接管理,数量有限,每个寄存器有特定用途,如指令寄存器、数据寄存器等。 可能存在的误区是用户可能将寄存器视为主存的一部分,因为它们都是存储器。但根据计算机组成原理,寄存器属于CPU的一部分,不属于主存。主存属于内存,而寄存器属于处理器内部存储单元。 需要确认资料是否准确,查阅计算机组成教材或权威资料,确认寄存器和主存的分类。例如,在冯·诺依曼结构中,存储器通常指主存,而寄存器属于CPU的组成部分。 总结:寄存器不属于主存,它们属于不同的层次,寄存器更靠近CPU,速度更快,容量更小,由CPU直接管理,而主存是CPU之外的存储器,速度较慢但容量大,由操作系统管理。需要以清晰的结构向用户解释这一点,并引用之前的分类来强化结论。</think>### 寄存器Registers)与主存(Main Memory)的归属关系解析 寄存器Registers)和主存(Main Memory)是计算机存储体系中的两个关键层级,但**寄存器不属于主存的范畴**。以下是详细分析: --- #### 一、存储层次结构的定位 计算机存储体系按速度与容量划分为多级,典型层级如下(从高到低): 1. **寄存器** → 2. **高速缓存(Cache)** → 3. **主存(DRAM)** → 4. **辅存(磁盘/SSD)** - **寄存器**位于最顶层,直接嵌入CPU内部,是速度最快、容量最小的存储单元。 - **主存**位于第三层,通过内存总线与CPU交互,速度显著低于寄存器,但容量更大。 --- #### 二、核心差异对比 | **特性** | **寄存器** | **主存(DRAM)** | |-------------------|-------------------------------------|-----------------------------------| | **物理位置** | CPU内部 | CPU外部,主板上的内存插槽 | | **访问速度** | 0.3-1 ns(直接由CPU时钟驱动) | 10-100 ns(需通过总线传输) | | **容量** | 几十到几百字节(如x86-64有16个通用寄存器) | 通常几GB到几TB | | **管理主体** | CPU硬件直接控制 | 操作系统通过内存管理单元(MMU)控制 | | **功能用途** | 暂存指令操作数和运算结果 | 存储运行中的程序和数据 | --- #### 三、技术细节剖析 1. **寄存器的作用** - **指令执行支持**: - 存放当前执行的指令(如**指令寄存器IR**)。 - 存储运算的操作数和结果(如**累加器AX**)。 - **地址处理**: - 基址寄存器(如**EBX**)和变址寄存器(如**ESI**)用于内存地址计算。 2. **主存的角色** - **程序运行载体**:所有正在执行的代码和数据必须加载到主存。 - **地址映射**:通过逻辑地址到物理地址的转换(由MMU完成),例如: $$ \text{物理地址} = \text{页表基址} + \text{逻辑页号} \times \text{页大小} + \text{页内偏移} $$ 3. **性能差异的本质** - 寄存器访问无需总线传输,直接通过CPU内部电路完成,速度接近CPU时钟频率。 - 主存访问需通过内存控制器和总线协议(如DDR4的64位数据总线),存在信号传输延迟。 --- #### 四、典型操作示例 1. **寄存器操作**(x86汇编指令): ```assembly MOV EAX, 42 ; 将立即数42存入EAX寄存器 ADD EBX, EAX ; 将EAX的值加到EBX ``` - 上述操作在1-2个时钟周期内完成。 2. **主存操作**: ```assembly MOV [0x1000], EAX ; 将EAX的值写入主存地址0x1000 ``` - 需通过总线发出请求,耗时数十到数百时钟周期。 --- #### 五、设计视角的区分 1. **架构设计** - 寄存器是CPU指令集架构(ISA)的核心组成部分,其数量和功能直接影响编程模型。 - 主存容量和带宽是系统级设计的关键参数,需平衡成本与性能。 2. **操作系统管理** - 寄存器操作系统透明,由编译器或汇编程序直接管理。 - 主存需操作系统通过分页、分段、虚拟内存等机制进行动态分配和保护。 --- ### 结论 寄存器与主存在计算机体系结构中是**截然不同的存储层级**: - **寄存器**是CPU内部的高速存储单元,直接参与指令执行,**不属于主存**。 - **主存**是CPU外部的易失性存储器,作为程序运行的临时工作空间,二者共同构成计算机的存储层次,协同完成数据处理任务。 理解这一区别对优化程序性能(如减少内存访问、利用寄存器缓存数据)和掌握计算机组成原理至关重要。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值