30天自制操作系统笔记

2016.8月做的笔记,偶然翻到以防后面丢失。最后的操作系统代码 上传GitHubhttps://github.com/Dandelionshine/30OS

  1. console:控制台 通过键盘输入命令的一种方式。基本上只用文字进行计算机操作。
    • 开发操作系统:制作一张“含有操作系统的,能够自动启动的磁盘”
    • 映像文件:软盘的备份数据。做出备份数据,将备份数据写入磁盘
      世界上第一个操作系统:对着CPU的命令代码表,将0,1排列,将数据写入磁盘。之后利用它开发后面的操作系统
      汇编:
      计算机读写软盘按照一个个扇区进行读取,一张软盘空间为1440KB(512*2880),共2880个扇区,每个扇区512个字节.软盘第一个扇区称为启动区(boot sector)
      计算机从最初一个扇区开始读软盘,然后去检查这个扇区最后2个字节是否为55AA,若为,则计算机认为这个扇区的开头是启动程序,并开始执行这个程序
      IPL(启动程序加载器 initial program loader):大部分将加载操作系统本身的程序放在启动区里
      启动(boot)
      机器语言中寄存器的编号顺序:AX,CX,DX,BX
      软盘的磁面不能摸
      与光盘不同,软盘磁盘是两面记录数据
      EBX->4G内存,CPU能处理的最大内存量
      Makefile可以使用简单的变量
  2. 系统装载时自动装载启动区
  3. 进入32位模式并导入C语言 2M内存,段寄存器,读写磁盘
    1. 制作真正的IPL(启动程序装载器),
    • 调用磁盘BIOS后,返回值存在进位标志里,若无错,则CF=0,AH=0,若有错,则CF=1,AH=错误号码
    1. 试错
    • 软盘不可靠,有时不能读数据,尝试再读,但一直重试磁盘会真的坏掉,决定重试5次
    1. 读到18扇区
      • 循环读取2-18扇区,没有采用直接读取17个扇区,方便以后循环读取其他磁道以及反面的磁道
    2. 读入10个柱面
    3. 着手开发操作系统
      • 将文件1.sys保存到磁盘映像1.img里,即:
        1. 使用make install指令,将磁盘映像文件写入磁盘
        2. 在Windows里打开那个磁盘,把1.sys保存到磁盘上
        3. 使用工具将磁盘备份为磁盘映像
          • 一般向一个空软盘保存文件时
            1. 文件名会写在0x002600以后的地方
            2. 文件的内容会写在0x004200以后的地方,即内存里0x8000+0x4200=0xc200, 故将操作系统本身的内容写在1.sys文件中,再把它保存到磁盘映像里,在从启动区执行1.sys就行了
    4. 从启动区执行操作系统
      • 启动区内容最后跳到0xc200(操作系统的内容)
    5. 确认操作系统的执行情况->颜色模式
    6. 32位模式前期准备->设置画面模式并保存在内存里,从BIOS得到键盘状态
      • 32位模式指的是CPU的模式,16位模式使用32位寄存器(EAX)比较麻烦。
      • 模式不同,机器语言的命令代码不同。16位模式的机器语言在32位模式下不能运行,反之亦然
      • CPU的自我保护功能(识别出可疑的机器语言并进行屏蔽以免破坏系统)在16位下不能用,但32位能用
      • 32位模式不能调用BIOS(16位机器语言写的),故调用BIOS放在开头做
    7. 开始导入C语言-》切换到32位模式
      • C语言的不便之处,生成的目标文件是一种特殊的机器语言文件,必须与其他文 件链接后才能成为真正可以执行的机器语言文件
      • 编译器内部:C语言->机器语言
        1. 使用ccl.exe(C编译器,gcc以gas汇编语言为基础,输出gas用的源程序),1.c->1.gas
        2. gas2nask.exe,1.gas->1.nas
        3. nask.exe,1.nas->1.obj,目标文件必须与其他文件进行链接
        4. obj2bim.exe,1.obj->1.bim,将必要的目标文件全部链接,生成二进制映像文件binary image
        5. bim2hrb.exe,1.bim->1.hrb,针对不同操作系统进行必要的加工
    8. 实现HLT
  4. C语言与画面显示的练习
    1. 用C语言实现内存写入
      与C语言联合使用,能自由使用的寄存器只有EAX,ECX,EDX3个,其他寄存器只能使用其值,而不能改变其值
      CPU(英特尔系列)家谱:
      8086->80186->286(16位)->386(32位)->486->Pentium->PentiumPro->PentiumII->PentiumIII->Pentium4->…
    2. 条纹实现
    3. 挑战指针
    4. 色号设定
    5. 绘制矩形
  5. 结构体、文字显示与GDT/IDT初始化
    • 接收启动信息
    • 显示字符->1个字符是16个字节
    • 显示变量值->自制操作系统中不能随便使用printf函数(按指定格式输出),但可以使用sprintf(将输出内容作为字符串写在内存中),只对内存进行操作,应用于所有操作系统
    • 显示鼠标指针->16*16字节
    • GDT与IDT的初始化
      • 采用分段,32位模式下,段寄存器仍为16位,由于设计上的原因,段寄存器的低3位不能使用,段号0-8181
      • CPU用8个字节表示段的信息
      • GDT(global descriptor table):全局段号记录表,记录段号与段的对应关系,存放在内存中;
        • 内存的起始地址和有效设定个数放在CPU内的GDTR的特殊寄存器中,GDTR低16位表示段上限,高32位表示GDT的开始地址
        • IDT(interrupt descriptor table):中断记录表,记录了0-255的中断号码与调用函数的对应关系。
        • 中断:当CPU遇到外部状况变化,或者是内部偶然发生某些错误时,会临时切换过去处理这些突发事件。
          • 由于中断,CPU不用一直查询键盘、鼠标、网卡等设备的状态
  6. 分割编译与中断处理
    1. 分割源文件
    2. 整理Makefile,Makefile一般规则,普通生成规则优于一般规则,文件可使用单独的规则进行编译
    3. 整理头文件
      • 双引号:头文件与源文件在同一个文件夹下;尖括号:头文件位于编译器所提供的文件夹里
    4. CPU处于系统模式还是应用模式,取决于执行中的应用程序是位于访问权为0x9a的段还是0xfa。
    5. 初始化PIC(programmable interrupt controller):可编程中断控制器:将8个中断信号集合成一个中断信号的装置。
      • PIC监视输入管脚的8个中断信号,只要有1个,则将唯一的输出信号变成ON,并通知给CPU;
      • 主PIC负责0-7号中断,从PIC负责8-15号中断。从PIC仅能通过连接主PIC的第2号IRQ通知CPU。
      • 以INT0x20-0x2f接收中断信号IRQ0-15;
      • PIC寄存器8位,IMR(interrupt mask register):中断屏蔽寄存器,若某位为1,PIC就忽视该信号。
        ICW(initial control word):初始化控制数据,4个,1-4,ICW2决定IRQ以哪一号中断通知CPU。
    6. 中断处理程序的制作
      • 鼠标:IRQ12,键盘:IRQ1
  7. FIFO与鼠标控制
    1. 获取按键键码
    2. 加快中断处理 键盘缓冲区存储一个字节 右Ctrl两个字节
    3. 制作FIFO缓冲区 32个字节
    4. 改善FIFO缓冲区 循环数组,不需数据移送
    5. 整理FIFO缓冲区 鼠标移动3个字节数据
    6. 鼠标:先有效鼠标控制电路,在有效鼠标
    7. 从鼠标接受数据
  8. 鼠标控制与32位模式切换
    • 鼠标3个字节数据,第一个字节:点击,移动;第二个字节:左右移动;第三个字节:上下移动
  9. 内存管理
    • 只有在内存检查时才将缓存设为OFF
    • 内存管理:内存分配,内存释放;
    • 内存管理:表或者列表,表采用将内存分为4KB的大小,用表记录每块是否空闲;本系统采用了列表方式,列表记录可用内存的地址及大小。
    • MEMMAN,当可用空间很零散时,暂时先割舍掉小块内存,当MEMMAN有空闲时,再对使用中的内存进行检查,将割舍掉的捡回来
  10. 叠加处理
    1. 内存以0x1000=4KB进行分配,释放;与运算只能用于二进制数向下舍入处理,在以2^n以外的数进行向下,上舍入处理时必须使用除法指令
    2. 叠加处理:鼠标,窗口叠加,将图层按照高度升序排列,并将其地址写入sheets;从下往上描画图层;
    3. 提高叠加处理速度:图层移动,对整个画面进行刷新->只描画移动前后的部分
  11. 制作窗口
    • 鼠标显示:鼠标移到画面之外隐藏
    • 显示窗口
    • 高速计数器-闪烁
    • 消除闪烁->仅对refresh对象及以上的图层进行刷新
    • 消除闪烁->刷新窗口时避免鼠标所在的地方对VRAM进行写入处理
  12. 定时器:每隔一段时间发送一个中断信号给CPU
    • 管理定时器->设定PIT(可编程的间隔型定时器),PIT连接着IRQ的0号
    • 中断产生的频率=单位时间时钟周期数(即主频)/设定的数值,设值为11932(0x2e9c)-》中断频率为100HZ,即1s发生100次中断
    • 超时功能-》计量处理所花费的时间:处理前时间存放,处理结束后时间存放,计算时间差
  13. 定时器2
    • 简化字符串显示
    • 所有定时器使用一个缓冲区,每个定时器写入的数据不同
    • 重新调整FIFO缓冲区-》键盘、鼠标、定时器仅使用一个缓冲区
      • 写入FIFO的数值 中断类型
      • 0-1 光标闪烁用定时器
      • 3 3秒定时器
    • 加快中断处理
      • 取代移位处理:1.改变下一次的读取地址 2.用next链表实现
    • 使用“哨兵”简化程序-》在最后加上一个永远也到不了的定时器
  14. 高分辨率及键盘输入
    • VBE:使用高分辨率显卡;不使用VBE:AH=0;AL=画面模式号码; 使用VBE:AX=0x4f02;BX=画面模式号码+0x4000;
      VBE的画面模式号码:
      0x101–6404808bit彩色
      0x103–8006008bit彩色
      0x105–10247688bit彩色
      0x107–128010248bit彩色**
    • 键盘输入–根据键盘缓冲区得到的键码在图层上显示相应的字符
    • 窗口移动–将窗口图层移到鼠标附近
  15. 多任务:多个应用程序同时运行,操作系统尽可能快地切换任务,切换任务就是执行JMP指令;
    1. 任务切换:A任务->B任务,设置任务的TSS,在GDT中设定,初始化寄存器;
      任务切换:CPU把寄存器的值全部写入内存;然后CPU将下一个程序有关的寄存器值从内存中读取出来
      任务切换所需的时间=内存写入和读取操作的时间
      任务状态段(task status segment)TSS,使用32位版。26个int成员,104字节;
      EIP:CPU记录下一条执行的指令在内存中的地址;
      JMP若跳转到TSS,则进行任务切换;CPU执行带有段地址的指令时,去GDT里确认是否执行任务切换;
      TR寄存器:CPU记住当前正在运行哪一个任务,TR=GDT的编号*8;
    2. .任务切换:返回到A任务
    3. 简单的多任务-》计数,刷新次数多,计数慢,背景图层的地址放在内存中共享
    4. 提高运行速度-》每隔0.01秒刷新一次;背景图层的地址放在该任务的栈中最后,作为传递的变量;
    5. 多任务切换函数设置定时器,2个任务自动进行切换,在定时器中断里若控制任务切换的定时器发生中断,则进行任务切换;
  16. 多任务2
    1. 任务管理自动化,任务管理的结构体
    2. 让任务休眠–将任务从tasks中删除,则任务处于休眠;但FIFO有数据过来,将任务唤醒-运行一次task_run,进入等待状态
      在fifo中加入要唤醒的任务
    3. 增加窗口数量-》任务A,B0-3,任务切换间隔固定为0.02s
    4. 设定任务优先级-》时间片轮转调度算法:为每个任务在0.01s-0.1s的范围内设定不同的任务切换间隔,数值越大,计数速度越快
    5. 设定任务优先级-》高优先级的任务同时运行-混乱-》
      • 把任务分到Level0、Level1、Level2这三层中的一个,当Level0有活动的任务(非休眠状态)时,只在Level0的任务间切换。
      • 当Level0没有任务或均处于休眠状态时,在Level1的任务间切换。Level2同理。
  17. 命令行窗口
    1. 闲置任务–所有LEVEL都没有任务,在最下层LEVEL中加一个闲置任务(只执行HLT)
    2. 创建命令行窗口–光标
    3. 切换输入窗口–按下“TAB”键,将输入窗口切换到命令行窗口
      • 改变窗口标题栏颜色—key_to为0,键盘输入发送到任务A,为1则发送到命令行任务
    4. 实现字符输入—key_to为0,键盘输入发送到任务A,为1则发送到命令行任务的缓冲区
    5. 符号的输入—实现!,%;key_shift变量,当左shift按下时置1,右shift按下时置2,两个都不按时置0,两个都按下时置3;
    6. 大写字母与小写字母–同时判断shift和CapsLock的状态
      • 从BIOS获取到了键盘状态,保存在binfo->leds,第4位:ScrollLock状态,第5位:NumLock状态,第6位:CapsLock的状态;
    7. 对各种锁定键的支持—向键盘控制器发送数据(数据放在新的缓冲区中)
  18. dir命令
    1. 控制光标闪烁–只有可以接受键盘输入的窗口有光标闪烁,
    2. 控制光标闪烁–控制命令行窗口中光标闪烁,按下TAB,在命令行任务的FIFO中放入2,光标开始闪烁;放入3,光标停止闪烁;
    3. 对回车键的支持–cursor_y记录光标的纵坐标;空格消除光标,cursor_y+=16;
    4. 对窗口滚动的支持–将所有的像素向上移动一行,并将最下面一行涂黑
    5. mem命令:显示内存使用情况,用cmdline依次记录键盘输入的内容
    6. cls命令:清屏;将图层所有像素涂黑
    7. dir命令:显示磁盘内文件名,文件的日期和大小;
      • 用BIOS读取磁盘;在ipl10.nas已读取10个柱面的内容存放在内存中;
      • 文件信息(32字节):
        struct FILEINFO {
        unsigned char name[8], ext[3], type;/文件名(8),扩展名(3),文件的属性信息(1)/
        char reserve[10];/保留/
        unsigned short time, date, clustno;/时间,日期,簇号(文件的内容从磁盘上的哪个扇区开始存放)/
        unsigned int size;/文件大小/
        };
        文件名的第一个字节为0xe5,表示这个文件已经被删除;文件名第一个字节为0x00,代表这一段不包含任何文件信息;
        文件的属性信息:0x01—只读文件;0x02–隐藏文件;0x04–系统文件;0x08–非文件信息(比如磁盘名称);0x10–目录;
        多种属性值相加;一般为0x20或0x00
  19. 应用程序
    1. type命令:type 文件名:显示文件的内容;
      磁盘映像中的地址=clustno * 512+ 0x003e00;
    2. type命令改良–处理换行和制表符的字符编码
      0x09–制表符:显示空格直到x被4整除;当前位置到下一个制表位之间没有填充空格;
      0x0a–换行符:换行
      0x0d–回车符:忽略;让文字显示的位置回到行首
      实现的type可以正确显示文件开头的512字节的内容。若大于512字节,中间可能突然显示其他文件的内容。
    3. 对FAT的支持
      大于512字节的文件可能并不是存入连续的扇区(磁盘碎片)。Windows中的磁盘整理工具,用来将扇区内容重新排列以减少磁盘碎片的程序。
      文件分配表(FAT):磁盘中记录文件的下一段存放在哪个扇区;FAT位于从0柱面、0磁头、2扇区-10扇区;在磁盘映像中0x00200-0x0013ff;
      FAT中以3字节为一组,扇区号对应的数字即是clustno,里面存储的数字即是该文件下一段存放的扇区。一般来说,FF8-FFF,代表文件数据到此结束。
      FAT中一个地方损坏,之后的部分就会全部混乱。故磁盘中存放了2份FAT(第2份是备份FAT,内容与第1份完全相同)
    4. 应用程序
      为每个应用程序创建一个内存段,并用file_loadfile将文件内容读到内存中,然后goto到该段中的程序(farjmp)。
      GDT:1-2:dsctbl.c;3-1002:mtask.c;1003:hlt.hrb;
  20. API(系统调用:由应用程序对操作系统功能的调用)
    2. 显示单个字符的API:应用程序调用cons_putchar;
    3. 显示单个字符的API2:应用程序对API要执行far-CALL;far-RET;
    4. 结束应用程序:应用程序结束后返回操作系统(RETF),操作系统通过far-CALL调用应用程序;
    5. 不随操作系统版本而改变的API:–通过中断来调用API
    • IDT(中断号码与函数的对应)仅32种中断,IDT中0x30-0xff均空闲,将asm_cons_putchar注册在0x40;
    • 故INT 0X40就能调用asm_cons_putchar;但中断无法通过RETF返回,需用IRETD.
    1. 为应用程序自由命名
    2. 当心寄存器 加上PUSHAD;POPAD;
    3. 用API显示字符串–1.显示一串字符遇到字符0结束;2.指定好字符串的长度再显示
      • 借鉴BIOS的调用方式,在寄存器中存入功能号(EDX),只用一个INT选择调用不同的函数;
      • 功能号1–显示单个字符(AL=字符编码)
      • 功能号2–显示字符串0( EBX=字符串地址)
      • 功能号3–显示字符串1(EBX=字符串地址,ECX=字符串长度)
  21. 保护操作系统
    1. 字符串显示API—应用程序的起始地址通过内存传递
    2. 用C语言编写应用程序
      • 凡是通过bim2hrb生成的hrb文件,其第4-7字节一定为Hari;
    3. 保护操作系统
    4. 保护操作系统2–为应用程序提供专用的内存空间
      • 创建应用程序专用的数据段,并在应用程序运行期间,将DS,SS指向该段地址
      • 操作系统用代码段–28
      • 操作系统用数据段–18
      • 应用程序用代码段–10038
      • 应用程序用数据段–10048
      • 38-10028为TSS使用的段
      • CPU可以自动进行复杂段切换;
    5. 对异常的支持–强制结束程序–在中断号0x0d中注册
      • 当应用程序试图破坏操作系统,或者试图违背操作系统的设置时,自动产生0x0d中断(一般性异常)
    6. 保护操作系统3–应用程序用汇编直接将操作系统的段地址存入DS
    7. 保护操作系统4–应用程序无法使用操作系统的段地址
      1. 在段定义时,访问权限+0x60,将段设置为应用程序用。当CS中为应用程序用段地址时,则DS无法存入操作系统用的段地址
        但必须在TSS中注册操作系统用的段地址和ESP;
      2. 不可以从操作系统CALL/JMP应用程序,但可以从应用程序CALL操作系统
  22. 用C语言编写应用程序
    1. 保护操作系统5–随意更改定时器·
      当以应用程序模式运行时,执行IN指令和OUT指令都会产生一般保护异常。通过修改CPU设置,可允许应用程序使用IN指令和OUT指令。
      当以应用程序模式运行时,执行CLI/STI/HLT这些指令都会产生异常。调用任务休眠API来实现省电。
      CPU规定除了设置好的地址之外,禁止应用程序CALL其他的地址。
    2. 帮助发现bug-知道引发异常的指令的地址
      CPU的异常处理功能,保护操作系统,帮助应用程序发现bug
      栈异常中断号0x0c;
      产生异常时寄存器的值:
      esp[0-7]为asm_inthandler中PUSHAD的结果:
      esp[0]:EDI;esp[1]:ESI;esp[2]:EBP;esp[3]:ESP;esp[4]:EBX;esp[5]:EDX;esp[6]:ECX;esp[7]:EAX;
      esp[8-9]为asm_inthandler中PUSH的结果:esp[8]:DS;esp[9]:ES;
      esp[10-15]为一场产生时CPU自动PUSH的结果:
      esp[10]:错误编号(基本上是0);esp[11]:EIP;esp[12]:CS;esp[13]:EFLAGS;esp[14]:ESP(应用程序用ESP);esp[15]:SS(应用程序用SS);
    3. 强制结束应用程序–强制结束键-shift+F1
      当应用程序运行时(task_cons->tss.ss0不为0),按下强制结束键结束程序;
    4. 用C语言显示字符串—由bim2hrb生成的hrb文件运行;
      数据段的大小根据.hrb文件中指定的值进行分配
      将.hrb文件中的数据部分先复制到应用程序的数据段后再启动程序;
    5. 应用程序显示窗口
      API:EDX=5;EBX=窗口缓冲区;ESI=窗口在x轴上的大小;EDI=窗口在y轴上的大小;EAX=透明色;ECX=窗口名称;
      返回值EAX=用于操作窗口的句柄(用于刷新窗口等操作);
    6. 在窗口中描绘字符和方块
      显示字符的API:EDX=6;EBX=窗口句柄;ESI=显示位置的x坐标;EDI=显示位置的y坐标;EAX=色号;ECX=字符串长度;EBP=字符串;
      描绘方块的API:EDX=7;EBX=窗口句柄;EAX=x0;ECX=y0;ESI=x1;EDI=y1;EBP=色号;
  23. 图形处理相关
    1. 编写malloc-API
      • 本系统采用为应用程序事先多分配一点数据段内存空间,malloc时从多余的空间里拿出一部分。
    2. 画点–API
    3. 刷新窗口
      • 在所有的窗口绘图命令中设置一个不自动刷新的选项(窗口句柄(图层地址,一定为偶数)设为奇数(在原来的数值上加1)),再编写一个仅用来刷新的API;
    4. 画直线
      • API–指定直线两端的坐标,自动计算出dx,dy,len并画出直线;
      • 目前不支持浮点数,将坐标预先扩大1000倍,速度:除以1024,
    5. 关闭窗口–将窗口图层高度设为-1,并标为未使用;
    6. 键盘输入API–当按下回车键时再结束运行应用程序;等待键盘输入过程中-休眠;
    7. 键盘输入–实现上下左右移动
    8. 强制结束并关闭窗口–按下shift+F1强制结束应用程序
      • 当应用程序结束时,查询所有的图层,若图层的task为将要结束的应用程序任务,则关闭该图层。
  24. 窗口操作
    1. 窗口切换–按下F11(0x57),将最下面的窗口放在最上面
    2. 窗口切换2–用鼠标点击来切换窗口
      • 从上到下判断鼠标的位置落在哪个图层的范围内,并确保该位置不是透明色区域。
    3. 移动窗口
      • 当鼠标左键点击窗口的位置位于窗口的标题栏区域,则移动;放开鼠标左键时,退出移动;
    4. 用鼠标关闭窗口
      • 若点击了x按钮,结束程序;
    5. 将输入切换到应用程序窗口
      • 按下tab键时将键盘输入切换到当前输入窗口下面一层的窗口中,若当前窗口为最下层,则切换到最上层窗口。
      • 若应用程序窗口处于输入模式被关闭,让系统自动切换到最上层的窗口;
    6. 用鼠标切换输入窗口–按下左键
    7. 定时器API–应用程序使用定时器;每1秒定时器发送128(不是键盘编码),计时
    8. 取消定时器–在应用程序结束的同时取消定时器 ,定时器标记区分是否需要在应用程序结束时自动取消
  25. 增加命令行窗口
    1. 蜂鸣器发声–PIT控制
      PIT时钟与CPU无关,频率恒定为1.19318MHz;
      2.为窗口移动提速
    2. 提高窗口移动速度–改进refreshamap;
      接收到鼠标移动数据后不立即进行绘图操作,等FIFO为空时再进行绘图操作;
    3. 启动时只打开一个命令行窗口–随意启动新命令行窗口
      按下shift+F2打开一个新的命令行窗口;
    4. 关闭命令行窗口
      在命令行中输入exit命令关闭当前窗口–释放窗口占用的内存,释放窗口的图层和任务结构
      exit命令:取消光标定时器,FAT用的内存释放,调用close_console(从命令行任务想task_a发送一个数据,请task_a帮忙关闭命令行任务)
    5. 关闭命令行窗口–鼠标关闭命令行窗口
      当鼠标点击窗口上的X按钮,向命令行窗口任务发送4;命令行任务接受到4后开始执行exit命令;
      在应用程序运行的时候,点击命令行窗口的X按钮不会关闭命令行窗口;
    6. start命令–打开一个新的命令行窗口并运行指定的应用程序
    7. ncst命令–不打开新命令行窗口的start命令;
      用ncst命令启动的应用程序会忽略字符串显示的API的调用;
      不显示命令行窗口,无法输入其他命令,在命令执行完毕时立即结束命令行窗口任务;
      29.
      2.文件压缩–将压缩好的文件解压缩
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值