8086CPU寄存器介绍

转载自    Zachary.XiaoZhen - 梦想的天空 

PS:感觉其实前边通用寄存器里边已经把一些段寄存器的一些东西介绍过了,所以感觉这里的东西没有上一篇内容多。。


段寄存器(CS,IP,SS,SP,DS,ES)

由于段寄存器总是和其他一些像指针寄存器,变址寄存器,控制寄存器一起使用,

所以在这里,我并不会单独介绍段寄存器,而是将段寄存器和一些其他的常用寄存器搭配介绍 。

由于下面的介绍中会涉及到很多关于段和栈的概念,而段和栈的介绍又都必须关系到物理内存,

所以在介绍段寄存器以及其他一些呈协作关系的寄存器之前,还是先来介绍一下这几个基本的概念比较好。

8086 CPU 访问内存(物理地址):

当 CPU 需要访问一个内存单元时,需要给出内存单元的地址,

而每一个内存单元在物理内存空间中都有一个唯一的地址,

即可以通过这个地址定位到内存单元,而这个地址即为物理地址。

CPU 通过地址总线将一个内存单元的物理地址送入存储器,

而后 CPU 便可以通过这个物理地址来访问这个物理地址所指向的内存单元了。

那么这个物理地址在 CPU 中是如何形成的呢?

首先,我们知道 8086 CPU 的地址总线是 20 根,

即每次都可以传输 20 位的地址,从而寻址能力有 220 也就是 1MB 的大小,

但是 8086 CPU 的寄存器只有 16 位,也就是在 8086 CPU 的内部,

一次性处理,传输,暂存的地址都只能是 16 位,

即 8086 CPU 不能完整的保存下一个物理地址(物理地址为 20 位),

如果单单以最简单的方式(即直接用 16 位寄存器来保存物理地址)的话,那么,寻址能力只有 216 ,也就是 64KB,

如果真以如此简单的方式的话,那么地址总线还需要 20 根干嘛呢?而且,难不成我们以后的内存就是 64KB 了吗?

当然不是的,8086 CPU 在这里采取了一定的措施从而使其寻址能力达到 1MB 。

8086 CPU 在内部通过两个 16 位的地址进行合成从而形成一个 20 位的物理地址,由此,8086 CPU 的寻址能力便可以达到 1MB 。

那么 8086 CPU 又是如何将两个 16 位的地址合成为一个20 位的物理地址的呢?

当 CPU 在访问内存时,其会使用一个 16 位的基地址,然后再使用一个 16 位的偏移地址,

通过将基地址和偏移地址传入 8086  CPU 的地址加法器中进行合成即可以构造出 20 位的物理地址。

至于合成的方式如下:

基地址其实是通过一个 16 位的段地址来形成的,将一个段地址左移 4 位即形成了基地址,

而至于偏移地址的话,自然不必多说,为 16 位,通过将基地址和偏移地址相加便形成了 20 位的物理地址 。

下面给出一幅示意图来表示物理地址的合成:

image

段:

至于段的话,其实在物理内存中是没有段这一概念的,事实上,段的概念来自于  CPU ,

因为 CPU 拥有段寄存器,既然在 CPU 中拥有了段寄存器,自然,在 CPU 中就肯定有段的概念了,

其实段也就是在编程时,我们将若干个地址连续的内存单元看做是一个段,

然后通过将一个段地址左移 4 位形成基地址,再通过这个基地址来定位这个段的起始地址,

然后,再通过偏移地址便可以精确定位到段中的内存单元了,由于段的起始地址是一个段地址左移 4 位,

所以很明显,段的起始地址肯定是 16 的倍数,而且由于一个段内部,只能通过偏移地址来定位,

而偏移地址为 16 位,所以一个段的长度也就是 216 也就是 64KB 的大小。

在编程时,可以讲一段内存定义成为一个段,而这里,我们又可以引出数据段,代码段,栈段这三种类型的段 。

何为数据段呢?其实就是我们自个儿定义一段内存(当然段起始地址肯定是 16 的倍数,并且段长度 <= 64KB),

然后我们在这个段里头存放我们所需要使用的数据,这就是数据段;

何为代码段呢?其实也很简单,也是我们自己在编程的时候定义一段内存,然后这段内存用来存放我们的代码(也就是指令),

既然是存放的代码,自然就称之为代码段;

何为栈段呢?至于栈段的话,有接触过数据结构的朋友应该是很清楚栈的,而这里我们也就是在内存中分配出一个段,

然后将这个段当做栈来使用,对于栈的介绍,详见下文;

这里呢,顺便还点出几个关于段寄存器的内容,当然下文还会详细介绍的,

首先,对于任何一个段来说,均有段地址,而这些段地址是存放在段寄存器中(段寄存器的作用也在于此),

但是对于不同的段,它们默认的段地址存放在不同的段寄存器中,像

数据段来说,它的段地址存放在  DS (Data  Segment)寄存器中,

代码段的段地址存放在  CS (Code  Segment)寄存器中,

栈段的段地址存放在  SS (Stack  Segment)寄存器中 。

下面给出一幅在段中寻址的示意图:

image 

上面的示意图中,通过将段地址左移四位,然后与偏移地址相加便可以得到 20 位的物理地址了 。

栈:

8086  CPU 中提供了对栈的支持,并且其还提供了相应的指令来以栈的方式访问内存空间 。

什么是栈?

通过上面在段中的介绍,栈其实就是一个段,再说白一点,也就是一块内存,当然,这块内存是一块连续的内存 。

既然栈是一个段的话,那么当然就可以以使用段的方式来使用栈,当然,除了像段一样的使用栈以外,

栈还提供了其特殊的访问方式(如果和段一模一样的话,那还需要栈干吗呢?),

众所周知,栈是先进后出类型的数据结构,在 8086  CPU 中也是如此,

可以通过 ”PUSH“  指令将数据压入栈中,然后再通过 ”POP“  指令将栈顶的元素取出来 。

下面给出一幅示意图来描述栈:

image

即通过 PUSH  10 来将元素 10 放入栈中,因为,先前栈中没有任何数据,所以,10 就会作为栈顶元素存在,

然后再在栈中压入元素 20 ,此时,栈顶中的元素就是 20 了,然后再使用  POP 指令将栈顶元素取出,

此时取出的栈顶元素是 20 ,取出 20 后,栈中便只剩下 10 了,自然 10 就成为了栈顶,

最后再通过 POP 指令将栈顶 10 取出,此时,栈便变成了空栈了 。

好了,在介绍段寄存器之前的基础知识介绍就到这里了,下面开始正式介绍段寄存器以及与它们协作使用的寄存器。

                  

CS 寄存器 和 IP 寄存器:

经过前面对段的介绍,相信各位朋友对段寄存器应该也有一定的了解了,

下面将要介绍的是一组非常非常重要的寄存器,即 CS:IP 。

CS:IP 两个寄存器指示了 CPU 当前将要读取的指令的地址,其中  CS  为代码段寄存器,而   IP  为指令指针寄存器 。

什么叫做指示了 CPU 当前将要读取的指令呢?在 8086  CPU 中,为什么  CPU  会自动的执行指令呢?

这些指令肯定是存放在内存中的,但是  CPU  怎么知道这些指令存放在内存的那个位置呢?

比如,我有下面的两条指令要执行:

 MOV AX,1234H
    MOV BX,AX

而假设这两条指令在内存中存放为:

image

很显然, 1000H:0000H 指向的是  MOV  AX,1234H  的首地址,

如果 CPU 要读取到我的指令的话,很显然,必须要知道地址  1000H:0000H ,

然后  CPU  就可以根据这个首地址,将汇编指令  MOV  AX,1234H  所对应的机器码读入到  CPU  的指令寄存器中,

最后便可以在  CPU  中进行处理了。

但关键是   CPU  如何知道我的  1000H:0000H  这个首地址?

其实这就需要使用到  CS:IP  这个寄存器组了 。

当我们运行一个可执行文件时,很明显,我们需要另外一个程序来将这个可执行文件加载到内存当中,

关于这个加载可执行文件的程序,我们在这里不管他,点一下即可,

一般是通过操作系统的外壳程序(也就是传说中的  Shell  程序),

Shell  将可执行文件加载到内存中以后,就会设置  CPU  中的两个寄存器,

即设置  CS:IP  两个寄存器指向可执行文件的起始地址,此后  CPU  便从这个起始地址开始读取内存中的指令,并且执行,

比如我们在写汇编程序时,通常会使用  START  标记,其实这个标记就是用来标记起始地址的,

当将一个汇编程序编译,连接成可执行文件以后,再通过操作系统的  Shell  程序将可执行文件加载到内存中以后,

这个  START  所标记处的地址就是整个可执行文件的起始地址了 。

也就是说,当一个可执行文件加载到内存中以后,CS:IP  两个寄存器便指向了这个可执行文件的起始地址,

然后  CPU  就可以从这个起始地址开始往下读取指令,

当读取完指令后,CS:IP  将会自动的改变,基本上是改变  IP ,从而指向下一条要读取的指令,这样就可以执行这个可执行文件了 。

最后再对  CS:IP  总结一下:

  1. 你想让  CPU  执行哪行指令,你就让  CS:IP  指向保存有指令的那块内存即可。
  2. 任何时候,CS:IP  指向的地址中的内容都是  CPU  当前执行的指令。

下面我们来看一个  Demo,并详细观察其执行的过程:

ASSUME CS:CODES

CODES SEGMENT
	
START:
    
    MOV AX,1234H
    MOV BX,AX
    
    MOV AH,4CH
    INT 21H
CODES ENDS
    END START
语句的执行过程如下:

image

从上面的截图中可以看出,当我使用  Shell (在  DOS  下也就是  Command  命令解释器)将可执行文件加载进内存后,

可以看到,整个程序的起始地址为   0C54H : 0000 H  ,并且,可以看到  CS  的地址为  0C54H ,IP  的地址为  0000H,

这正好吻合我们上面对  CS:IP  的分析,很明显,CPU  将会读取    MOV    AX ,1234H   到 CPU 中并且执行 ,

然后我们继续向下看:

image

可以看到,我们单步执行后,AX 中的值编成了  1234H ,而  IP  寄存器中的值变成了  0003H,

对于  AX  中的值的改变,我们是能够理解的,但是   IP  中的值为什么会从  0000H  变到  0003H  呢?

从最上面的一幅关于指令在内存中的存放可以看出    MOV    AX ,1234H   在内存中需要  3 个内存单元存放,

也就是  CPU  为了执行    MOV    AX ,1234H   这条指令,已经将内存中相对应的 3  个内存单元读入内存中了,

执行完这条指令后,自然,CPU  就要将偏移地址向下移动  3  个单元,从而使得  CS:IP  指向下一条需要执行的指令了 ,

为了更深刻的理解,我们再来继续看执行过程,

image

从最上面的一幅关于指令在内存中的存放可以看出    MOV    BX ,AX  在内存中只占  2  个内存单元,

这也就是为什么  IP  这一次只向下移动了  2  个单元的缘故 。

                  

关于  CS: IP  的遐想:

从上面关于  CS:IP  的介绍中,我们可以大胆的猜想,我们只需要通过手动的改变  CS:IP  所指向的内存地址,

让  CS:IP  指向我们另外的代码,那么我们就可以让  CPU  执行我们自己指定的代码了 。

即可以通过修改  CS:IP  来达到我们想要让  CPU  干什么它就干什么的目的 。

上面的虽然是遐想,但是大家要相信,我们写的是汇编,不是  JAVA  也不是  NET  ,

所以我们还真的可以达到上面的目的,也就是说我们的遐想其实是可以实现的,当然这还是有一定的限制的 ,

关于这个遐想呢,可能会在我后续的博文中有所介绍,不过感兴趣的当然可以自己去尝试了,蛮有味的哦 。

              

SS 寄存器和 SP 寄存器:

根据前面对栈的介绍,相信各位对栈也肯定是有一定了解了的,更何况,估计大家也是职场打滚多年的,

要是栈都没用过的话,那也确实蛮悲剧的 ,所以,我在这里也不会对栈做十分详细的介绍了,

但是,最基本的介绍还是要的,毕竟在底层的话,不像高级语言那么方便,可以直接一个  Stack  就 OK 的,

在底层涉及的是栈在内存中的具体实现 。

不知道,大伙有没有注意笔者在本篇博文的上面介绍关于栈的知识时,我并没有提到如何找到这个栈,

我只提到了一个栈就是先进后出操作,同时可以使用  ”PUSH“ 和  ”POP“ 指令,

然后就是稍微带了一下  SS 这个寄存器的介绍,

我们虽然在内存中是可以方便的定义一个栈了,但是,我们为什么要定义这么一个栈呢?

自然,是为了操作方便,同时提供给  CPU  使用的,

既然  CPU  要使用的话,自然,CPU  又必须根据一定的方式找到这个栈,

而这就需要使用  SS 和  SP 寄存器了 。

同时,一个栈也就是一块内存区域,通过上面的介绍,我们也知道了如果要在一块内存中精确地定位到内存单元的话(寻址),

我们必须要有基地址(也就是段地址左移  4  位)和偏移地址,自然,要在一个栈中寻址的话,也需要段地址和偏移地址,

而对于一个栈来说,我们使用的最多的是什么呢?

当然是栈顶了,因为只有栈顶可以用来存取数据,所以对于一个栈来说,我们只需要有栈顶的段地址和偏移地址即可,

而对于栈顶的段地址,其是存放在段寄存器  SS  中的,而对于栈顶的偏移地址,其则是存放在  SP  寄存器中的 。

记住,在任何时刻,SS:SP  都是指向栈顶元素 。

其实关于栈的使用还是比较简单的,但是要注意的是  8086  CPU  并不会保证我们对栈的操作会不会越界 。

所以我们在使用栈的时候需要特别注意栈的越界问题 。

当使用  PUSH 指令向栈中压入 1 个字节单元时,SP = SP - 1;即栈顶元素会发生变化;

而当使用  PUSH 指令向栈中压入  2 个字节的字单元时,SP = SP – 2 ;即栈顶元素也要发生变化;

当使用  POP 指令从栈中弹出 1 个字节单元时, SP = SP + 1;即栈顶元素会发生变化;

当使用  POP 指令从栈中弹出 2 个字节单元的字单元时, SP = SP + 2 ;即栈顶元素会发生变化;

下面通过一个  Demo 来介绍栈的使用:

ASSUME CS:CODES

CODES SEGMENT
	
START:
    
    MOV AX,1000H ;首先是定义好栈的段地址 MOV SS,AX    
    MOV AX,10H ;再定义好栈的长度(初始时刻的栈顶偏移地址即栈的长度) MOV SP,AX
    
    MOV AX,0001H
    PUSH AX
    MOV AX,0002H
    PUSH AX
    MOV AX,0003H
    PUSH AX
    MOV AX,0004H
    PUSH AX
    MOV AX,0005H
    PUSH AX
    
    POP AX
    POP AX
    POP AX
    POP AX
    POP AX
    
    
    MOV AH,4CH
    INT 21H
CODES ENDS
    END START
 
然后我们来看栈在内存中的结构图:

image

语句的执行过程如下:

首先我们来看尚未执行上述任何指令时栈中的数据情况:

image

然后我们再来依次执行上述指令:

image

从上副截图中可以看出已经设置好了  SS:SP ,也就是栈已经设置 OK 了,

下面开始往栈中压入数据了,

image

由于我们压入栈中的数据为字数据,即占 2 个内存单元,所以,每次  SP = SP – 2 ;

将 5 个字型数据压入栈中后,我们可以来查看栈中的数据了,

image

因此,在内存中的一个好看点的结构图如下所示:

image

下面开始进行出栈操作了

image

由于我们弹出栈时的数据为字数据,即占 2 个内存单元,所以,每次  SP = SP + 2 ;

将 5 个字型数据全部弹出栈中后,我们可以来查看栈中的数据了,

image

可以看到 SP 变成了初始状态了,也就是说栈中所有的数据已经全部弹出了,虽然我们查看内存时看到的不是 0 ,

但是我们看到的这些数据都是无效的,我们这里不理会 。

                  

DS 寄存器和 ES 寄存器:

DS  寄存器和  ES  寄存器都属于段寄存器,其实它们和  CS  寄存器以及  SS  寄存器用起来区别不大,

既然是段寄存器的话,自然它们存放的就是某个段地址了 。

通过上面对基础知识的介绍呢,我们已经知道,如果  CPU  要访问一个内存单元时,

我们必须要提供一个指向这个内存单元的物理地址给  CPU ,

而我们也知道在  8086  CPU  中,物理地址是由段地址左移 4  位,然后加上偏移地址形成的,

所以,我们也就只需要提供段地址和偏移地址即 OK 。

8086  CPU  呢,提供了一个  DS  寄存器,并且通常都是通过这个  DS  段寄存器来存放要访问的数据的段地址 。

DS(Data  Segment):很显然,DS 中存放的是数据段的段地址 。

但是这里不得不再点一下,那就是我们对段的支持是在  CPU  上体现的,而不是在内存中实现了段,

所以事实上我们使用的段其实是一个逻辑概念,即是我们自己定义的,

再说白了,我定义一个段,我说它是数据段那它就是数据段,我说它是代码段那么它就是代码段,

它们其实都是一块连续的内存而已,至于为什么要区分为数据段和代码段,

很明显,是用来给我们编程提供方便的,即我们在自己的思想上或者说是编码习惯上规定,

数据放数据段中,代码放代码段中 。而我们在使用数据段的时候,为了方便或者说是代码的编写方便起见,

我们一般把数据段的段地址放在  DS  寄存器中,当然,如果你硬要觉得  DS  不顺眼,那你可以换个  ES  也是一样的,

至于  ES(Extra  Segment)  段寄存器的话,自然,是一个附加段寄存器,如果再说得过分点,

就当它是个扩展吧,当你发现,你几个段寄存器不够用的时候,你可以考虑使用   ES  段寄存器,

在使用方式上,则和其他的段寄存器没什么区别  。

下面看一个介绍使用  DS  寄存器的  Demo:

ASSUME CS:CODES

CODES SEGMENT
   
START:

    MOV AX,1000H
    MOV DS,AX
    MOV AL,1
    MOV BX,0
    
    MOV CX,5 ;设计一个循环,让其循环 5 次 s: MOV [BX],AL ;这里 [BX] 并没有指定段地址哦 INC AL
       INC BX
       LOOP s            
    
    MOV AH,4CH
    INT 21H
CODES ENDS
    END START

上面的代码所做的事情,就是循环将  1,2,3,4,5 写入到地址  1000H:0000H ,1000H:0001H,

1000H:0002H,1000H:0003H,1000H:0004H  中,

语句的执行过程如下:

首先我们来看尚未执行上述任何指令时栈中的数据情况:

image

而当循环执行完成以后,我们再来看内存  1000H:0000H 处的值:

image

在这里,我们可以看到确实达到了我们预期的效果,但是大家注意看代码:

 s: MOV [BX],AL ;这里 [BX] 并没有指定段地址哦 INC AL
       INC BX
       LOOP s 

这里可以看到,我们在  [BX]  中并没有给其指定段地址,而只有一个偏移地址,

但是根据我们一开始的介绍,必须要有段地址和偏移地址才能够定位内存单元,

莫非这里出问题了?

其实不是的,因为我们在最前面定义了段地址   DS  为  1000H,

当我们定义好段地址后,每一次  CPU  执行到  [BX]  时,便会自动或者说是默认的从  DS  中取值,

并且将取得的值作为段地址,因此,当  [BX]  为  0001H  时,CPU  会从   DS  中取得一个  1000H ,

由这两个一合成即可以得到正确的物理地址   1000H:0000H 。

最后还提醒一点,那就是   8086  CPU  不支持直接将一个数据送入段寄存器中,

也就是下面的做法是错误的:

 MOV DS,1000H

           

               

标志寄存器(FLAG):

前面呢,已经介绍了  8086  CPU  14 个寄存器中的 13 个了,下面我们将介绍最后一个寄存器也就是  FLAG  寄存器,

FLAG  寄存器之所以放到最后一个介绍,是因为其和其他的一些寄存器不同,像   AX,BX,CX,DX  这些寄存器来说,

它们都是用来存放数据的,当然  FLAG  中存放的也是数据啦,

呵呵,不过,AX,BX 这些寄存器中的数据是作为一个整体使用的,

最多也就分成一个  AL  和  AH  使用而已,但是在  FLAG  中,数据是按位起作用的,

也就是说,FLAG  中的每一个位都表示不同的状态,

由于一个位也就能表示  0  和  1 ,自然,FLAG  中的每一个位就是用来描述状态的,

而且  FLAG  寄存器中存储的信息通常又被称作程序状态字(PSW) 。

下面我给出一幅  FLAG  寄存器中各个位的示意图:

image

从上面这幅图中可以看出,FLAG  的第  0  个位表示的是 CF  ,第 2 个位表示的是  PF  ,与此类推 . . . . 

首先,我们来看一个列表:

image

上面的这个表怎么看呢?我们通过看下面一幅截图就知道了 。

image

从上面的标记中可以看出,从左到右依次代表   OF,DF,SF,ZF,PF,CF  标志位的值,

再通过与上面的表格相对照可以知道:

OF = 0 ;

DF = 0 ;

SF = 0 ;

ZF = 0 ;

PF = 0 ;

CF = 0  ;

至于为什么我们在  Debug  模式下,使用  R  命令时,只会列出这几个标志位,我菜的话是因为相对来说,

列出的这几个标志位更为常用,其他的几个标志位并不经常使用的缘故吧 。

下面我们就按不同的位来分别介绍这些位所描述的状态,以及它们代表的意义:

CF(Carry  FLag) - 进位标志(第 0 位):

CF:    进位标志是用来反映计算时是否产生了由低位向高位的进位,或者产生了从高位到低位的借位 。

if(运算过程中产生了进位或者借位)
{
        CF  =  1;
} else {
        CF  =  0;
}
          

PF(Parity  FLag) - 奇偶标志(第 2 位):

PF:    奇偶标志是用来记录相关指令执行后,其结果的所有的  Bit  位中  1  的个数是否为偶数 。

if(运算结果中 1 的个数为偶数)
{
        PF  =  1;
} else {
        PF  =  0;
}
      

AF(Auxiliary  Carry  FLag) - 辅助进位标志(第 4 位):

AF:    用来辅助进位标志 。

if(字节操作中发生低半个字节向高半个字节借位或者进位  ||  字操作中发生低字节向高字节借位或者进位)
{
       AF = 1;
} else {
       AF = 0;
}

             

ZF(Zero  FLag) – 零标志(第 6 位):

ZF:    记录的是相关的指令执行完毕后,其执行的结果是否为  0 。

if(执行的结果  ==  0)
{
       ZF = 1;
} else {
       ZF = 0;
}

           

SF(Sign  FLag) - 符号标志(第 7 位):

SF:    符号标志,其记录相关指令执行完以后,其结果是否为负数 。

if(运算结果为负数)
{
        SF  =  1;
} else {
        SF  =  0;
}
     

TF(Trap  FLag) - 追踪标志(第 8 位):

TF:    追踪标志,主要是用于调试时使用 。

if(TF  ==  1)
{
       CPU 进入单步方式;
}
     

IF(Interrupt-Enable  FLag) - 中断允许标志(第 9 位):

IF:    中断允许标志,其决定  CPU  是否能够响应外部可屏蔽中断请求(以后会做详细介绍) 。

if(IF  ==  1)
{
        CPU 能够响应外部的可屏蔽中断请求;
} else {
        CPU 不能够响应外部的可屏蔽中断请求;
}
            

DF(Direction  FLag) - 方向标志(第 10 位):

DF:    方向标志,其用于在串处理指令中,用来控制每次操作后  SI  和  DI  是自增还是自减 。

if(DF == 0)
{
        SI++;
        DI++;
} else {
        SI--;
        DI--;
}
               

OF(OverFlow  FLag) - 溢出标志(第 11 位):

OF:    溢出标志,其通常记录了有符号数运算的结果是否发生了溢出 。

if(运算发生溢出)
{
        OF  =  1;
} else {
        OF  =  0;
}

            

                

总结

上面呢,从最简单的开始,循序渐进的介绍了  8086  CPU  中的各个寄存器,

同时也通过一些  Demo  来列举了各个寄存器的使用,

由于写的比较基础,而且量也比较多,所以,造成博文过长了,读者需一定耐心才能看完,

写本篇博文呢,并不是说将来要用汇编去开发个什么东东,

实质上,笔者学习汇编的目的也不在此,只是因为先前在接触到底层的寄存器以及内存时,

笔者总有一丝不爽的感觉,总是感觉不得要领,所以才会开始汇编的学习,

此次推出本系列博文,本意也并不是说要学习汇编做开发,只是为了提升内功而已 。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值