操作系统的完整代码——内核代码ycker.cpp

#define YCORG -1          //该设置使编译器生成头部为YCEXE结构的可执行文件
#include "ycio.cpp"       //提供前缀为ycio_的函数和C/C++标准函数

ycfsCLASS *YCFS;          //文件管理接口
ycttyCLASS *YCTTY;        //显示和键盘接口
ycmmCLASS *YCMM;          //内存管理接口
char kernel_stack[1024];  //内核堆栈
int isr_reenter;          //中断重入
int process_count;        //进程计数
YTSS ytss;

asm void main()
{
        mov     esp, &kernel_stack + sizeof(kernel_stack)
        lgdt    gdt_descr  //装入全局段描述符
        lidt    idt_descr  //装入中断段描述符
        jmp     os_init
}

//硬件中断服务程序宏定义
#define hard_ISR(hard_proc,irqNo)                               \
        pushad                                                  \
        push    ds                                              \
        push    es                                              \
        push    fs                                              \
        push    gs                                              \
        mov     ax, ss                                          \
        mov     ds, ax                                          \
        mov     es, ax                                          \
        in      al, 0x21                                        \
        or      al, (1 << irqNo)                                \
        out     0x21, al  /* 屏蔽中断 */                        \
        mov     al, 0x20                                        \
        out     0x20, al  /* 设置EOI位 */                      \
        inc     dword isr_reenter                               \
        cmp     dword isr_reenter, 0                            \
        je      hir_1                                           \
        sti                                                     \
        call    hard_proc                                       \
        cli                                                     \
        jmp     hir_2                                           \
hir_1:  mov     esp, &kernel_stack + sizeof(kernel_stack)       \
        sti                                                     \
        call    hard_proc                                       \
        cli                                                     \
        mov     esp, currentProcess                             \
        lea     eax, [esp + offsetof(YProcess, ldt_sel)]        \
        lldt    [eax]                                           \
        mov     ytss.esp0, eax                                  \
hir_2:  in      al, 0x21                                        \
        and     al, ~(1 << irqNo)                               \
        out     0x21, al   /* 去屏蔽中断 */                     \
        dec     dword isr_reenter                               \
        pop     gs                                              \
        pop     fs                                              \
        pop     es                                              \
        pop     ds                                              \
        popad                                                   \
        iret                                                    \

#define P_KRNL  0
#define P_USER  3
#define INDEX_TSS  3

void os_init()
{
    printk(1,80*5,0x6f,"Setting Interrupt Server Routine (ISR)... - ycker.cpp");
    set_idt(0x80, int_80_ISR, P_USER);            //设置软中断80h的ISR
    set_idt(0x20, align(16)asm[](){ hard_ISR(clock_proc,0) }, P_KRNL);  //时钟ISR
    set_idt(0x21, align(16)asm[](){ hard_ISR(keyboard_proc,1) },P_KRNL);//键盘ISR
    set_idt(0x22, asm[]() { mov ecx,2; hard_ISR(dump_ISR,2) }, P_KRNL);
    set_idt(0x23, asm[]() { mov ecx,3; hard_ISR(dump_ISR,3) }, P_KRNL);
    set_idt(0x24, asm[]() { mov ecx,4; hard_ISR(dump_ISR,4) }, P_KRNL);
    set_idt(0x25, asm[]() { mov ecx,5; hard_ISR(dump_ISR,5) }, P_KRNL);
    set_idt(0x26, asm[]() { mov ecx,6; hard_ISR(dump_ISR,6) }, P_KRNL);
    set_idt(0x27, asm[]() { mov ecx,7; hard_ISR(dump_ISR,7) }, P_KRNL);
    set_idt(0x28, asm[]() { mov ecx,8;  call dump_ISR; hlt }, P_KRNL);
    set_idt(0x29, asm[]() { mov ecx,9;  call dump_ISR; hlt }, P_KRNL);
    set_idt(0x2a, asm[]() { mov ecx,10; call dump_ISR; hlt }, P_KRNL);
    set_idt(0x2b, asm[]() { mov ecx,11; call dump_ISR; hlt }, P_KRNL);
    set_idt(0x2c, asm[]() { mov ecx,12; call dump_ISR; hlt }, P_KRNL);
    set_idt(0x2d, asm[]() { mov ecx,13; call dump_ISR; hlt }, P_KRNL);
    set_idt(0x2e, asm[]() { mov ecx,14; call dump_ISR; hlt }, P_KRNL);
    set_idt(0x2f, asm[]() { mov ecx,15; call dump_ISR; hlt }, P_KRNL);

    set_idt(0,  asm[]() { push 0;  call exception;  }, P_KRNL); //设置异常中断
    set_idt(1,  asm[]() { push 1;  call exception;  }, P_KRNL);
    set_idt(2,  asm[]() { push 2;  call exception;  }, P_KRNL);
    set_idt(3,  asm[]() { push 3;  call exception;  }, P_USER);
    set_idt(4,  asm[]() { push 4;  call exception;  }, P_USER);
    set_idt(5,  asm[]() { push 5;  call exception;  }, P_KRNL);
    set_idt(6,  asm[]() { push 6;  call exception;  }, P_KRNL);
    set_idt(7,  asm[]() { push 7;  call exception;  }, P_KRNL);
    set_idt(8,  asm[]() { push 8;  call exception;  }, P_KRNL);
    set_idt(9,  asm[]() { push 9;  call exception;  }, P_KRNL);
    set_idt(10, asm[]() { push 10; call exception;  }, P_KRNL);
    set_idt(11, asm[]() { push 11; call exception;  }, P_KRNL);
    set_idt(12, asm[]() { push 12; call exception;  }, P_KRNL);
    set_idt(13, asm[]() { push 13; call exception;  }, P_KRNL);
    set_idt(14, asm[]() { push 14; call exception;  }, P_KRNL);
    set_idt(15, asm[]() { push 15; call exception;  }, P_KRNL);

    set_descriptor((YDescriptor*)&Ygdt[INDEX_TSS],(int)&ytss,sizeof ytss,0x89);

    printk(1,80*6,0x4f,"Loading drivers: ycfs  ycmm  yctty - ycker.cpp");
    ycio_set_driver((byte*)(KERNEL_POS + *(int*)(KERNEL_POS+DATA_POS+0)),"ycfs");
    ycio_get_driver(&YCFS,"ycfs");  //获得文件管理函数接口YCFS

    ycio_set_driver((byte*)(KERNEL_POS + *(int*)(KERNEL_POS+DATA_POS+4)),"ycmm");
    ycio_get_driver(&YCMM,"ycmm");  //获得内存管理函数接口YCMM

    ycio_set_driver(YCFS->get_file_code("yctty.sys"), "yctty");  //安装yctty驱动
    ycio_get_driver(&YCTTY,"yctty");  //获得显示和键盘管理函数接口YCTTY

    auto enable_irq = [](int irqNo)
      {
         short dx = irqNo < 8 ? 0x21 : 0xa1;
         byte al = inb(dx) & ~(1 << (irqNo % 8));
         outb(dx,al);
      };
    for(int ii=0; ii<16; ii++)   enable_irq(ii);   //使能16个硬件中断

    ycio_create_process(YCFS->get_file_code("ycshell.sys"));  //创建shell进程

    printk(1,80*7,0x2f,"Going to shell ...... - ycker.cpp");
    asm
     {
        mov     esp, &procTable   //shell进程结构数据地址
        lea     eax, [esp + offsetof(YProcess, ldt_sel)]
        lldt    [eax]             //装入shell进程的段描述符

        mov     ytss.esp0, eax
        mov     word ytss.ss0, KERNEL_DS  //外层到内层的 ss:esp
        mov     ax, 8 * INDEX_TSS
        ltr     ax

        //以下代码使程序进入 ycshell.cpp 的 main() 函数
        dec     dword isr_reenter  //eax = procTable[0].eax
        pop     gs                 //ebx = procTable[0].ebx
        pop     fs
        pop     es                 //ss =  procTable[0].ss
        pop     ds                 //esp = procTable[0].esp
        popad                      //cs =  procTable[0].cs
        iret                       //eip = procTable[0].eip = main() of ycshell.cpp
     }
}

void set_idt(byte irqNo,void (*handler)(), byte privilege)
{
    struct YGATE
     {
        unsigned short  offset_low;
        unsigned short  selector;
        byte            dcount;
        byte            attr;
        unsigned short  offset_high;
     };
    YGATE *pGate = (YGATE*) &Yidt [ irqNo ];
    pGate->offset_low = (int)handler & 0xffff;
    pGate->selector = KERNEL_CS;
    pGate->dcount = 0;
    pGate->attr = 0x8e | (privilege << 5);  //DA_IGate
    pGate->offset_high = ((int)handler >> 16) & 0xffff;
}

void clock_proc()
{
    currentProcess->m_ticks--;
    if(isr_reenter)  return;
    if(currentProcess->m_ticks > 0)   return;

    int greatest_ticks = 0;
    while(1)
      {
        YProcess *pProc, *endProc = &procTable[process_count];
        for(pProc=procTable; pProc<endProc; pProc++)
          {
            if(pProc->m_ticks <= greatest_ticks)  continue;
            greatest_ticks = pProc->m_ticks;
            currentProcess = pProc;
          }
        if(greatest_ticks)  break;
        for(pProc=procTable; pProc<endProc; pProc++)
                                pProc->m_ticks = pProc->priority;
      }
}

void keyboard_proc()
{
    YCTTY->keyboard_proc();
}

struct
{
    char name[36];
    void *pObj;
} drTable[256];
int drCnt;

int sys_set_driver(byte *drvSrc,char *drvName)   //安装驱动程序
{
    if(strlen(drvName) >= sizeof(drTable[0].name))  return 2;
    if(sys_get_driver(nullptr,drvName))  return 1;          //该驱动程序已被安装
    if(drCnt >= sizeof drTable/sizeof drTable[0]) return 3; //超出最大数,不能安装
    if(!drvSrc)  return 4;                                     //驱动代码地址为0

    int bb = drvSrc[0]==0x68 ? 1 : drvSrc[0]==0xe8 && drvSrc[5]==0x68 ? 6 : 0;
    if(bb)
      {
        *(void**)&drvSrc[bb] = &drTable[drCnt].pObj; //设置main(pObj)的pObj参数
      }
    else
      {
        if(*(short*)drvSrc != *(short*)"YC")   return 5;
        YCEXE *pYcexe = (YCEXE*)drvSrc;                   //驱动代码文件头结构
        YMEM *pLink = YCMM->malloc(currentProcess->pHead,pYcexe->codelen,true);
        if(!pLink)   return 5;
        drvSrc = to_memPtr(pLink);
        code_copy(drvSrc,pYcexe,&drTable[drCnt].pObj);//驱动代码重定位拷贝到内存
      }

    strcpy(drTable[drCnt].name, drvName);
    drCnt++;
    ((void(*)())drvSrc)();    //运行驱动程序中的main()函数
    return 0;
}

int sys_get_driver(void *pObj,char *drvName) //获取驱动程序函数接口指针
{
    for(int ii=0; ii<drCnt; ii++)
      {
        if(*(int*)drTable[ii].name != *(int*)drvName)  continue;
        if(strcmp(drTable[ii].name,drvName))   continue;
        if(pObj)  *(void**)pObj = drTable[ii].pObj;
        return 1;
      }
    if(pObj)  *(void**)pObj = nullptr;
    return 0;
}

void sys_write(int *pdata)   //写字符串到屏幕
{
    YCTTY->sys_write(pdata);  //调用yctty.cpp函数
}

void sys_clear_screen(int bpos,int len)  //清屏
{
    YCTTY->clear_screen(bpos,len); //调用yctty.cpp函数
}

unsigned int sys_create_process(byte *codeptr,int *pdata)  //创建进程
{
    YProcess *pProc = procTable;   //进程表头部
    YProcess *endProc = &procTable[process_count];  //进程表尾部

    //查找空闲进程表
    for(; pProc<endProc; pProc++)  if(!pProc->eip)  break;
    if(pProc >= endProc)
      {
        if(process_count >= sizeof(procTable)/sizeof(procTable[0]))   return 0;
        pProc = &procTable[process_count++];
      }

    static unsigned int proc_id = 0;

    //申请内存 ,设置进程内存链表
    YCEXE *pYcexe = (YCEXE*)codeptr;
    int stackSize = pYcexe->stacksize;
    pProc->pHead = YCMM->malloc(nullptr,stackSize + pYcexe->codelen + 32 +
                                                pdata[1] + 1 + 32*sizeof(char*));
    if(!pProc->pHead)   return 0;

    pProc->pHead->pLast = pProc->pHead->pNext = pProc->pHead;

    //设置删除进程代码 - yc_delete_process()
    codeptr = to_memPtr(pProc->pHead) + stackSize;
    codeptr[ 0] = 0xe8;      *(int*)&codeptr[1] = 32 - 5;  //call 32
    codeptr[ 5] = 0xb8 + 0;  *(int*)&codeptr[6] = 4;       //mov  eax,4
    codeptr[10] = 0xb8 + 1;  *(int*)&codeptr[11] = 0;      //mov  ecx,0
    codeptr[15] = 0xcd;      codeptr[16] = 0x80;           //int  80h
    codeptr[17] = 0xc3;                                    //ret

    //设置命令行参数
    pProc->CommandLine = (char*)codeptr + pYcexe->codelen + 32;
    memcpy(pProc->CommandLine,(char*)pdata[0],pdata[1]);
    pProc->CommandLine[pdata[1]] = 0;
    char **argv = (char**)&pProc->CommandLine[pdata[1] + 1];
    int argc = getArgA(pProc->CommandLine,argv,32);
    static char *envp[] = { "PATH=C:/", "include=c:/include", nullptr };

    //重定位拷贝代码到内存。code_copy()位于ycio.cpp
    code_copy(&codeptr[32],pYcexe,(void*)argc,argv,envp);

    pProc->pid = proc_id++;
    if(proc_id == 0)   proc_id++;

    memcpy(&pProc->ldts[0], &Ygdt[KERNEL_CS / 8], sizeof(YDescriptor));
    pProc->ldts[0].attr1 = 0x98 | (P_USER << 5);  //局部描述符属性=c,特权级=3

    memcpy(&pProc->ldts[1], &Ygdt[KERNEL_DS / 8], sizeof(YDescriptor));
    pProc->ldts[1].attr1 = 0x92 | (P_USER << 5); //局部描述符属性=r/w,特权级=3

    pProc->cs = (8 * 0) | 4 | P_USER;            //设置进程代码选择子和特权级
    pProc->ds =
    pProc->es =
    pProc->fs =
    pProc->gs =
    pProc->ss = (8 * 1) | 4 | P_USER;           //设置进程数据堆栈选择子和特权级
    pProc->esp = (unsigned int)to_memPtr(pProc->pHead) + stackSize; //进程堆栈
    pProc->eip = (unsigned int)codeptr;                         //进程执行代码
    pProc->eflags = 0x0202;

    int dPos = INDEX_TSS + 1 + pProc - procTable;
    pProc->ldt_sel = 8 * dPos;
    pProc->m_ticks = pProc->priority = 5;
    set_descriptor((YDescriptor*)&Ygdt[dPos],(int)pProc->ldts,
                                                      sizeof pProc->ldts,0x82);
    return pProc->pid;
}

void sys_delete_process(int procID)   //删除进程
{
    if(!procID)   procID = currentProcess->pid;            //进程号
    YProcess *endProc = &procTable[process_count];         //进程表尾
    for(YProcess *pProc=procTable; pProc<endProc; pProc++) //遍历进程表查找
      {
        if(pProc->pid != procID)   continue;
        pProc->eip = pProc->m_ticks = pProc->priority = 0;
        clock_proc();
        yc_run_link(pProc->pHead, [](YMEM *pLink)
                                   {
                                     YCMM->free(pLink);  //释放进程所占内存
                                   });
        YCTTY->wr_prompt();      //显示提示符
        break;
      }
}

void set_descriptor(YDescriptor *pDesc, int baddr, int limit, short attrV)
{
    pDesc->limit_low = limit & 0x0ffff;
    pDesc->base_low = baddr & 0x0ffff;
    pDesc->base_mid = (baddr >> 16) & 0xff;
    pDesc->attr1 = attrV & 0xff;
    pDesc->limit_high_attr2 = ((limit >> 16) & 0x0f) | (attrV >> 8) & 0xf0;
    pDesc->base_high = (baddr >> 24) & 0xff;
}

int sys_get_current_process()
{
    return currentProcess->pid;
}

void *sys_malloc(unsigned int _Size)
{
    YMEM *pLink = YCMM->malloc(currentProcess->pHead,_Size);
    if(!pLink)  return nullptr;
    yc_add_link(pLink,currentProcess->pHead);
    return to_memPtr(pLink);
}

void *sys_realloc(void *_Memory,unsigned int _NewSize)
{
    if(!_Memory)  return sys_malloc(_NewSize);
    YMEM *oldLink = to_memLink(_Memory);
    YMEM *pLink = YCMM->realloc(oldLink,_NewSize);
    if(pLink != oldLink)
      {
        if(!pLink)   return nullptr;
        yc_add_link(pLink,currentProcess->pHead);
        sys_free(_Memory);
      }
    return to_memPtr(pLink);
}

void sys_free(void *_Memory)
{
    if(!_Memory)  return;
    YMEM *pLink = to_memLink(_Memory);
    YCMM->free(pLink);
    yc_delete_link(pLink,currentProcess->pHead);
}

void sys_cursor(int curpos)
{
    YCTTY->sys_cursor(curpos);
}

void *sys_call[] = { sys_write,                 //0
                     sys_set_driver,            //1
                     sys_get_driver,            //2
                     sys_create_process,        //3
                     sys_delete_process,        //4
                     sys_get_current_process,   //5
                     sys_malloc,                //6
                     sys_realloc,               //7
                     sys_free,                  //8
                     sys_cursor,                //9
                     sys_clear_screen,          //10
                     };

align(16)
asm void int_80_ISR()
{
        pushad
        push    ds
        push    es
        push    fs
        push    gs

        mov     bx,  ss
        mov     ds,  bx
        mov     es,  bx
        mov     esi, esp                        //esi = 进程结构地址

        inc     dword isr_reenter
        cmp     dword isr_reenter, 0
        jne     sy_1
        mov     esp, &kernel_stack + sizeof(kernel_stack)  //切换到内核栈

  sy_1: sti
        push    edx
        push    ecx
        call    dword [&sys_call + eax * 4]     //call 系统调用子程序(ecx,edx)
        add     esp, 4 * 2
        mov     [esi + offsetof(YProcess, eax)], eax   //返回值
        cli

        cmp     dword isr_reenter, 0
        jne     sy_2
        mov     esp, currentProcess
        lea     eax, [esp + offsetof(YProcess, ldt_sel)]
        lldt    [eax]
        mov     ytss.esp0, eax

  sy_2: dec     dword isr_reenter
        pop     gs
        pop     fs
        pop     es
        pop     ds
        popad
        iret
}

void dump_ISR()
{
    int irq;
    asm { mov irq, ecx; }
    printk(1,80*2,0x74,"dump_ISR: %d  %s",irq,irq>=8 ? "hlt..." : "");
}

void exception(int irqNo, int eip, int cs, int eflags)
{
    char *msg2[] = {"#DE Divide Error",
                    "#DB RESERVED",
                    "—  NMI Interrupt",
                    "#BP Breakpoint",
                    "#OF Overflow",
                    "#BR BOUND Range Exceeded",
                    "#UD Invalid Opcode (Undefined Opcode)",
                    "#NM Device Not Available (No Math Coprocessor)",
                    "#DF Double Fault",
                    "    Coprocessor Segment Overrun (reserved)",
                    "#TS Invalid TSS",
                    "#NP Segment Not Present",
                    "#SS Stack-Segment Fault",
                    "#GP General Protection",
                    "#PF Page Fault",
                    "—  (Intel reserved. Do not use.)",
                    "#MF x87 FPU Floating-Point Error (Math Fault)",
                    "#AC Alignment Check",
                    "#MC Machine Check",
                    "#XF SIMD Floating-Point Exception" };

    printk(1,80*3,0x74,`
            interrupt Error: %d - %s
            eFlags: %p
            CS:EIP  %x:%p`,  irqNo, msg2[irqNo], eflags, cs, eip);
    while(1);
}

YProcess procTable[1024];
YProcess *currentProcess = procTable;
align(8)
int64 Ygdt[1024] = {0, 0x00cf9a000000ffff,  //大小=4G 基址=0 rd/exec代码段描述符
                       0x00cf92000000ffff}; //大小=4G 基址=0 rd/wr数据段描述符
int64 Yidt[129];
align(4) GDT_PTR idt_descr = { sizeof Yidt, (char*)Yidt };
align(4) GDT_PTR gdt_descr = { sizeof Ygdt-1, (char*)Ygdt };

                      C/C++代码文件: ycker.cpp

源代码分析
    语句set_idt(0x80, DA_IGate, int_80_ISR, P_USER)将int_80_ISR()函数设置为int 80h中断服务程序,服务程序提供sys_set_driver,sys_create_process,sys_malloc等9个内核功能。80h中断是应用程序(也叫用户进程)调用系统内核的唯一接口。

    语句set_idt(0x20, DA_IGate, align(16)asm { hard_ISR(clock_proc,0) }, P_KRNL)设置时钟中断服务程序:
        align(16)asm { hard_ISR(clock_proc,0) }
    它是一个匿名的汇编函数,起始地址16字节对齐。hard_ISR是宏命令:
        #define hard_ISR(hard_proc,irqNo)
    其中汇编语句 mov esp,currentProcess 使堆栈指向当前进程指针:currentProcess,查看其结构定义可知,执行下列语句:
        pop gs
        pop fs
        pop es
        pop ds
        popad
        iret
    后,CPU的寄存器之值改变为:
        eax = currentProcess->eax
        ebx = currentProcess->ebx
        ……
        eip = currentProcess->eip
        cs = currentProcess->cs
        eflags = currentProcess->eflags
        esp = currentProcess->esp
        ss = currentProcess->ss
    可见中断程序退出后便进入了currentProcess->eip指向的代码。
    如果在执行时钟中断程序时按顺序让currentProcess指向不同的进程,就可以实现进程之间的切换,也就是同时执行多个应用程序。

语句set_idt(0x21,DA_IGate, align(16)asm { hard_ISR(keyboard_proc,1) }, P_KRNL)
设置键盘中断服务程序。其中函数:void keyboard_proc() { YCTTY->keyboard_proc(); }
调用了yctty.cpp中的接口函数。

    int ycfs_pos = (int)(KERNEL_POS + DATA_POS + 0)获得在ychead.cpp设置的ycfs.cpp的代码位置
    yc_set_driver((byte*)(KERNEL_POS + ycfs_pos),“ycfs”)设置驱动程序:ycfs.cpp。查看ycio.cpp可知,yc_set_driver调用了内核接口1号功能:
        mov eax, 1
        int 80h
    从数组sys_call可知,内核接口1号功能调用sys_set_driver()函数。

    语句yc_get_driver(&YCFS,“ycfs”)获得ycfs.cpp的接口指针YCFS,通过YCFS可调用ycfs.cpp中的函数。
    语句yc_get_driver(&YCMM,“ycmm”)获得ycmm.cpp接口YCMM

    语句YCFS->get_code(“yctty.sys”), “yctty”)用YCFS接口从ycfs.cpp中获得驱动程序代码:yctty.sys
    语句yc_set_driver(YCFS->get_code(“yctty.sys”), “yctty”)设置驱动程序yctty.cpp

    语句yc_create_process(YCFS->get_code(“ycshell.exe”))创建进程执行shell程序,它是0号进程,其进程数据为procTable[0]
    汇编语句mov esp,&procTable将ycshell.exe进程数据地址放入esp,
    语句lea eax,[esp + offsetof(YProcess,ldt_sel)]将进程的局部段描述符procTable[0].ldt_sel的地址放入eax,
    语句lldt [eax]装入进程的局部段描述符。
执行下列代码
        pop gs
        pop fs
        pop es
        pop ds
        popad
        iret
    后,CPU的指令寄存器eip = procTable[0].eip, 而procTable[0].eip 已被设置为 ycshell.sys代码的首地址,故程序便进入ycshell.cpp的main()函数。

    每次时钟中断都要执行clock_proc()函数,该函数检查当前正在等待运行的process_count个进程,按一定的规则用语句currentProcess = pProc把其中一个进程设置为当前进程,以便退出中断时切换到该进程。

    每次按键中断都要执行keyboard_proc()函数,该函数通过语句YCTTY->keyboard_proc()调用yctty.cpp的ycttyCLASS::keyboard_proc()函数。

    sys_set_driver(byte *drvSrc,char *drvName)函数安装驱动程序,应用程序通过软件中断 int 80h 调用它。
    sys_get_driver(void *pObj,char *drvName)函数获得驱动程序接口地址,应用程序通过软件中断 int 80h 调用它。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值