HIT2023计算机系统大作业

 

计算机系统

大作业

题     目  程序人生-Hellos P2P  

专       业  未来技术学院            

学     号  2021110772              

班   级  21WL024                

学       生  于子轩             

指 导 教 师  郑贵滨                 

计算机科学与技术学院

2023年5月

摘  要

本文通过对一个简单的C语言程序Hello.c文件从预编译,编译,汇编,链接,运行,结束的实现,较完整的展现了一个进程在Linux系统下的Program to Process过程(P2P)和zero to zero过程(020)。

关键词:计算机系统,Linux,C语言,进程                         

目  录

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

5.3 可执行目标文件hello的格式

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

6.2 简述壳Shell-bash的作用与处理流程

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 hello的存储管理

7.1 hello的存储器地址空间

7.2 Intel逻辑地址到线性地址的变换-段式管理

7.3 Hello的线性地址到物理地址的变换-页式管理

7.4 TLB与四级页表支持下的VA到PA的变换

7.5 三级Cache支持下的物理内存访问

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

7.8 缺页故障与缺页中断处理

7.9动态存储分配管理

7.10本章小结

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献


第1章 概述

1.1 Hello简介

Hello的P2P:程序员先编写出hello的原始C语言代码,当想要运行Hello.c时,需要进行四个步骤的处理:预处理、编译、汇编和链接。最终生成的可执行文件可以在shell中运行,并分配进程空间。此时hello便从代码变成了运行着的进程(from program to process)。

Hello的020:在shell中为hello创建进程并加载其为可执行文件时,将为该进程提供虚拟地址空间等进程上下文,并使得它从无到有地运行起来。在该进程的生命周期中,它可能会遇到各种异常和信号,涉及到不同的存储器访问机制,并通过中断和IO端口与外设进行交互。最终,当hello进程正常退出或者收到信号后被终止时,操作系统会结束该进程并释放它占用的所有资源,然后将控制权返回给shell。这就是hello从从编写到运行结束的完整过程(from zero to zero)。

1.2 环境与工具

硬件环境: CPU:Intel i9-12900H,16GB内存。
系统环境: 虚拟机:Ubuntu 16.04 LTS,VMWare Workstation 16
工具:文本编辑器gedit,反汇编工具edb 1.3,反汇编工具objdump,编译环境gcc等。

1.3 中间结果

1.原始代码hello.c。

2.预处理后的代码hello.i。

3.编译后的汇编语言代码hello.s。

4.可重定位目标文件hello.o。

5.hello.o的objdump结果hello.o.txt。

6.可执行文件hello。

7.hello的objdump结果hello.txt

1.4 本章小结

本章主要介绍了hello.c程序的P2P过程和020过程。


第2章 预处理

2.1 预处理的概念与作用

预处理是C语言编译过程的一个阶段,它在实际的编译之前对源代码进行一些宏替换、头文件包含等预处理操作。主要作用是将源代码中的一些预处理指令和宏展开,生成修改后的代码供编译器进一步处理。可以减少重复冗杂的代码并提供更好的代码组织方式,在编译之前对代码进行优化,从而提高程序的可读性和可维护性,加快了大型程序的编译速度。

2.2在Ubuntu下预处理的命令

gcc -E hello.c -o hello.i

 

2.3 Hello的预处理结果解析

可以看到代码数量相比于.c源文件数量增加许多,这是应为预处理器把所有#include对应的头文件从路径/user/include/中调出写入到预处理过后的文件中,同时将所有宏定义填充。同时可以看到原来的C语言代码被放在了hello.i文件的最后。

2.4 本章小结

本章介绍了linux环境下对C语言程序进行预处理的命令,同时简要介绍了预处理的概念和作用,然后用简单的hello程序实际演示了从hello.c到hello.i的过程并结合具体代码对预处理结果进行了简单的分析。


第3章 编译

3.1 编译的概念与作用

编译的概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s。

编译的作用:将高级语言变成汇编语言,并提示语法错误。并且在编译阶段,编译器还能起到优化的作用,优化处理是编译系统中一项比较艰深的技术。

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s -fno-PIC -no-pie -m64,得到编译结果文件hello.s

3.3 Hello的编译结果解析

3.3.1汇编文件头部声明

汇编文件头部声明:

.file 源文件(指从hello.i汇编得来)

.text代码节

.rodata 制度代码段

.align 代码对齐方式

.global 全局变量

.type声明一个符号是数据类型还是函数类型

.string 声明了两个字符串分别为.LC0和.LC1

3.3.2数据分析

无宏变量等,只有一个全局(global)为main函数:

字符串:

字符串实际上存储在某地址当中,当取出字符串时,需要加载字符串所在地址:

局部变量:

将栈指针-4,为局部变量int i开辟一个四字节的空间。

3.3.3赋值

mov指令将寄存器中的数据或者立即数,存入寄存器中,其中根据操作数使用movl,movq分别表示它们操作的数据大小不同:l表示双字,数据占4个字节;q表示四字,数据占8个字节。

3.3.4关系操作与跳转操作

(1) (2)  

cmp比较指令,指令执行的结果是返回条件码,而跳转指令一般衔接在比较指令后,根据比较结果实现跳转,如实例中e表示等于,g表示大于。

3.3.5函数操作

  1. (2)

实例分别展示了调用头文件包含的printf函数和getchar函数

3.4 本章小结

通过以上实例可以看到高级语言让程序员在编程时能够屏蔽底层机器细节的实现,但由于计算机只能执行机器级指令,无法直接识别高级语言,所以编译器扮演了很重要的角色,将高级语言代码转换为汇编代码。通过对hello.i文件编译得到hello.s汇编文件的例子,我们可以感受到编译器对于汇编代码生成、优化等过程所承担的大部分工作。同时,在理解汇编语言在整个程序中所起的作用之后,我们也可以反向地将汇编代码转换为C语言代码,并在计算机系统课程实验中积累更多的熟练经验。


第4章 汇编

4.1 汇编的概念与作用

概念:汇编器(as)将.s汇编程序文件翻译成机器语言,并且打包成可重定位目标程序的格式,将结果保存在.o目标文件中。

作用:将汇编语言翻译成机器语言,这样才能使计算机能直接识别和执行。

4.2 在Ubuntu下汇编的命令

命令:gcc -c hello.s -o hello.o

4.3 可重定位目标elf格式

命令:readelf -h hello.o

命令:readelf -S hello.o

在Section Header Table中,我们可以看到各Section的描述信息,其中.text和.data是我们在汇编程序中声明的Section,而其它Section是汇编器自动添加的

之后我们通过readelf -a hello.o探查ELF文件中能探查的节。其中除了ELF Header和Section Header Table,还有.rela.text和.symtab

.rela.text包含需要重定位的信息,当链接器链接.o文件时,会根据重定位节的信息计算正确的地址,重定位.rela.text中的信息。

4.4 Hello.o的结果解析

命令:objdump -d -r hello.o


与hello.s进行对比可以发现发现,每行代码末尾的指令是基本相同的,但是在每条指令前面都会有一串十六进制的编码;且代码的数量有变化,不完全一样。从文件内容分析,hello.s是由汇编语言组成的,相对于计算机能识别的机器级指令,汇编代码仍是抽象语言;而反汇编得到的代码不仅仅有汇编代码,还有机器语言代码。

4.4.1分支转移

如上图所示,在反汇编文件中,并不存在汇编语言中的代码段地址,而是直接跳转在当前过程的起始地址加上偏移量得到的直接目标代码地址,这与.s文件中通过指令jmp等直接跳转到某一段代码不同。如汇编语言是跳到某一个模块(.L4),机器语言是跳到相对位置<main + x>。

4.4.2函数调用

调用函数时,汇编语言直接用函数名调用,而机器语言仍是用相对位置调用,再在下一行写出函数的名称。

4.5 本章小结

本章详细介绍了从hello.s到hello.o的汇编过程,对可重定位文件的结构及其各个组成部分进行了分析,包括它们的内容和功能。同时,我们还查看了hello.o的elf格式,并利用objdump工具将其反汇编得到代码,与hello.s进行比较,以深入理解汇编语言映射到机器语言所需的转换过程。在这个过程中,生成了重定位条目,为后续链接中的重定位过程奠定了基础。


5链接

5.1 链接的概念与作用

概念:是将各种代码和数据部分收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。

作用:将多个编译后的目标文件、库文件及其他需要的代码和数据片段组合成一个单一的可执行文件或共享库,使其可以在操作系统中加载到内存并被执行。通过链接,程序实现了模块化和分离编译,加快了编译速度,减小了可执行文件的大小,并且提高了代码的可重用性和维护性。

5.2 在Ubuntu下链接的命令

命令:ld -o hello.o -dynamic-linker /lib64/ld-linux-x86-64.so.2  /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/9/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello

5.3 可执行目标文件hello的格式

5.3.1 ELF Header

命令:readelf -h hello

可以看出这是一个64位ELF文件;数据以2的补码、用小端法表示;是可重定位文件;机器类型X86-64;入口点地址是0x400550;程序头开始地址是64;节头部开始地址是6592字节;ELF头的大小是64字节;节头部的大小是64字节;
共28个节头,节头部的字符串表索引是25。

5.4 hello的虚拟地址空间

    使用edb可以查看hello虚拟地址空间。从ELF开始,我们可以知道起始地址为0x400550;   

5.5 链接的重定位过程分析

Hello.o的反汇编文件:

Hello的反汇编文件:

相对于hello.o而言,hello的反汇编添加了更多的节(.init等)。Hello的反汇编引入了许多函数,所以在main中可以直接调用函数的地址,而hello只能调用函数名。Hello反汇编中的虚拟地址已经确定,但是hello.o中还是0。

重定位过程:

(1)重定位节和符号定义:在这一步中,链接器将所有相同类型的节合并为同一个类型的新的聚合节,然后,链接器将运行时内存地址赋给新的聚合节,赋给输出模块定义的每个节,体积赋给输出模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。

(2)重定位节中的符号引用:在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的地址,要执行这一步,链接器依赖于可冲定位目标模块中称为重定位条目的数据结构

5.6 hello的执行流程

0000000000400550 <_init>

0000000000400560 <.plt>

0000000000400570 <puts@plt>

0000000000400580 <printf@plt>

0000000000400590 <getchar@plt>

00000000004005a0 <atoi@plt>

00000000004005b0 <exit@plt>

00000000004005c0 <sleep@plt>

00000000004005d0 <_start>

00000000004005e0 <_dl_relocate_static_pie>

00000000004005f0 <deregister_tm_clones>

0000000000400620 <register_tm_clones>

0000000000400630 <__do_global_dtors_aux>

0000000000400660 <frame_dummy>

0000000000400690 <main>

0000000000400660 <__libc_csu_init>

0000000000400670 <__libc_csu_fini>:

0000000000400680 <_fini>   

5.7 本章小结

本章主要介绍了链接的概念及作用,并在linux下实际进行了链接操作,并对生成的hello可执行文件的ELF格式、虚拟地址空间、重定位过程、执行流程、动态链接等方面进行了详细分析。


6hello进程管理

6.1 进程的概念与作用

概念:进程是指正在执行的程序在操作系统中的一个实例。每个进程都有自己的上下文,这个上下文是由一组状态信息构成的,包括程序代码及其数据在内存中的映像、堆栈空间、通用寄存器值、程序计数器、环境变量和文件描述符等。这些状态信息共同保证了程序正确运行所必需的状态。

作用:进程可以为应用程序提供两个抽象概念:独立的逻辑控制流和私有地址空间。前者使应用程序看起来像是在独占地使用CPU,而后者则给了应用程序实际上拥有全部计算机资源的假象。这样可以提高CPU执行效率,避免因等待而导致的CPU空闲状态,同时减少了其他计算机软硬件资源的浪费。

6.2 简述壳Shell-bash的作用与处理流程

流程:

1.读取命令行的字符串

2.分割字符串,读取对应的命令

3.判断是否为内置命令,如果是则直接运行,不是则fork一个子进程运行

4.子进程用execve运行对应的命令

5.等待前台进程结束

6.返回读去阶段

6.3 Hello的fork进程创建过程

1.父进程通过fork函数创建一个新的运行的子进程。在子进程中fork返回0;父进程中,返回子进程的PID。

2.新创建的子进程和父进程几乎完全相同,子进程获得了一个与父进程虚拟地址空间相同但独立的副本,并且还会获取到父进程打开文件描述符的相同副本。唯一的区别在于子进程拥有自己不同于父进程的PID

3.当我们在shell中输入./hello来运行这个程序时,此时操作系统就会fork创建一个子进程来运行hello程序。

6.4 Hello的execve过程

execve函数会在当前进程的上下文中加载并运行一个新程序。此函数用于执行可执行目标文件filename,并且在执行时可以携带参数列表argv和环境变量列表envp。但是,与fork不同的是,execve函数只会被调用一次,并且不会返回。

6.5 Hello的进程执行

当hello进程运行时,如果未发生抢占,则会正常执行。但是如果发生抢占,那么hello进程将进入内核模式以进行上下文切换,然后再转入用户模式,并调度其他进程。在hello进程执行到sleep()函数时,它可能会被抢占,直到sleep()返回后,hello进程再次被调度并继续执行。

6.6 hello的异常与信号处理

  1. 乱按普通字符或者回车时,字符可以正常打印在屏幕上但是没有任何反应。
  2. Ctrl+C:hello程序直接终止,shell返回到接受命令行的状态。
  3. Ctrl+Z:与按下Ctrl+C时情况类似,但是会使得hello进程接收到信号SIGTSTP,然后hello进程暂停,shell发现它的前台作业暂停了,就重新进入接受用户命令行的状态。

6.7本章小结

通过对hello进程运行的分析和实验,我们能够深入了解到其背后的复杂机制。尽管hello进程看起来只是独占CPU和内存的简单程序,实际上它是在操作系统的进程调度机制下与其他进程并发地运行的。同时,在hello进程的运行过程中,会出现各种异常和收到各种信号,这些异步事件实现了操作系统中的控制流转移和消息传递,可以说是一个非常重要的机制。


7hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:机器语言中用来指定一个指令或操作数的地址,由段和偏移量组成

线性地址:是逻辑地址到物理地址的中间层,由段地址加偏移地址组成如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址。如果没有启用分页机制,那么线性地址直接就是物理地址。

虚拟地址:与线性地址相似。

物理地址:在计算机科学中,物理地址,也叫实地址、二进制地址,它是在地址总线上,以电子形式存在的,使得数据总线可以访问主存的某个特定存储单元的内存地址。在和虚拟内存的计算机中,物理地址这个术语多用于区分虚拟地址。尤其是在使用内存管理单元(MMU)转换内存地址的计算机中,虚拟和物理地址分别指在经MMU转换之前和之后的地址。在计算机网络中,物理地址有时又是MAC地址的同义词。这个地址实际上是用于数据链路层,而不是如它名字所指的物理层上的。

7.2 Intel逻辑地址到线性地址的变换-段式管理

段式管理是从逻辑地址到线性地址的变换:一个逻辑地址由段标识符和段内偏移量组成。其中,段标识符由16位二进制数构成,称为段选择子。其中前13位是索引号,用于在段描述符表中查找一个特定的段描述符,后面3位包含一些硬件细节。段描述符是一个8字节的数据结构,具体地址描述了一个段。通过使用段选择子的索引号进行访问,可以直接在段描述符表中查找和获取对应的段描述符,从而描述一个段。因此,大量不同的段描述符组成一个表格,被称为“段描述符表”。给定一个逻辑地址[段选择符:段内偏移地址],转换过程如下:首先根据段选择符判断当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。这样就得到了一个数组。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。Base + offset就是要转换的线性地址。

7.3 Hello的线性地址到物理地址的变换-页式管理

虚拟地址(也就是线性地址)由虚拟页号VPN与虚拟页偏移量VPO组成;物理地址由物理页号PPN和物理页偏移量PPO组成。

虚拟内存被分割为成为虚拟页的大小固定的块来解决虚拟内存的存储问题。页式管理将虚拟地址与内存地址建立一一对应的页表。

PTBR指向当前页表。MMU利用VPN 来选择适当的PTE。例如VPN 0选择PTE 0。将页表条目中物理页号PPN与虚拟地址的VPO串联起来,就得到相应的物理地址。其中由于虚拟地址与物理地址的偏移量大小相同,所以PPO和VPO是相同的。

7.4 TLB与四级页表支持下的VA到PA的变换

以Core i7为例,Core i7采用四级页表层次结构,以Core i7的地址翻译为例

36的VPN被划分成四个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含L1页表的物理地址。VPN1提供到一个L1 PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2 PTE的偏移量,以此类推。最后得到PPN,而PPO与VPO 仍相同。

7.5 三级Cache支持下的物理内存访问

在将虚拟地址翻译为物理地址后,我们会将物理地址划分为缓存偏移量(CO)、缓存组索引(CI)和缓存标记(CT)。首先,我们使用缓存组索引(CI)来确定该地址是否与Cache中的某个组相对应;接着,我们使用缓存标记(CT)来检查Cache中是否存在此内容。如果Cache命中,则可以访问对应的物理地址;否则我们需要索引、访问下一个级别的缓存,以此类推。

7.6 hello进程fork时的内存映射

当当前进程调用 fork 函数时,内核会为新的进程创建各种数据结构,并分配给它一个唯一的 PID。为了在新进程中创建虚拟内存,它会以原样复制当前进程的 mm_struct、区域结构和页表。此时,两个进程中的每个页面都被标记为只读权限,并且两个进程中的每个区域结构都被标记为私有写时复制。当 fork 在新进程中返回时,新进程的虚拟内存空间与它调用 fork 时存在的虚拟内存空间完全相同。当这两个进程中有任何一个进程执行写操作时,写时复制机制就会创建新的页面,在每个进程中维护其私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

在shell进程调用execve函数时,它会加载并运行一个新的程序hello。该过程中,已经存在的用户区域将被删除,并为新程序创建一个新的区域结构,其为私有的写时复制结构。程序的代码和初始化数据将映射到.text和.data段,通过目标文件提供相应信息;而.bss和栈堆则会映射到匿名文件中,初始长度设为0。共享对象则由动态链接所映射的共享区域提供支持。最后,设置PC指针,使其指向代码区域的入口点。Linux会根据需要将代码和数据页面换入物理内存中。

7.8 缺页故障与缺页中断处理

缺页故障概念:当指令引用一个虚拟地址,而与该地址相对于的物理页面不在内存中,因此必须从磁盘中取出时,就会发送缺页故障。

缺页中断处理:在试图翻译某个虚拟地址A时,触发了一个缺页异常。这个异常转移到内核的缺页异常处理程序。

缺页异常处理程序要进行判断:

如果虚拟地址是一个不存在的页面,则视为段错误;

如果虚拟地址不合法,比如违反了只读的约定,则触发保护异常机制;

如若虚拟地址可访问且合法,那么视为正常缺页。对于正常缺页,程序会选择一个牺牲页,牺牲掉它,然后将虚拟地址不知道内存中,并更新PTE。这样,再访问虚拟地址对应的物理地址,就不会缺页了。

7.9本章小结

在本章中,我们简要介绍了hello进程的内存地址空间管理,并对Intel的段式管理和页式管理进行了讨论。我们还探究了如何使用TLB和四级页表支持完成从虚拟地址到物理地址的转换,以及如何通过三级缓存实现高效的物理内存访问。接着,我们结合hello进程从虚拟内存的角度分析了fork和execve操作的实现过程。最后,我们介绍了缺页异常的产生原因以及中断处理过程的细节。


结论

首先,我们在计算机的文本编辑器上用C语言编写hello.c的源文件;

然后,hello.c在预处理之后,将头文件的内容插入到程序文本中,得到hello.i;

编译器对hello.i进行编译,从而得到汇编文件hello.s;

计算机无法直接执行汇编代码,需要使用汇编器将其转换为与汇编语言一一对应的机器语言指令。通过汇编的过程,我们可以得到一个可重定位目标文件hello.o,该文件是一个二进制文件。这个文件包含了计算机能够理解和执行的指令,使计算机能够运行hello程序。

此时hello.o是一个包含机器语言指令的二进制文件,但其中的指令仍需要被分配虚拟地址方便计算机执行。这一过程通常由链接器完成,链接器会将hello.o中调用其他函数的指令进行重定位,并将所需的系统函数(如printf.o等)与hello.o进行链接,得到可执行目标文件hello。在此之后,hello程序才能真正地在计算机上运行。

得到的hello文件就是可以运行的文件了。接下来在Linux中可以输入./hello运行这个程序。首先在shell-Bash中输入运行hello的命令行./hello,操作系统就为hello创建一个子进程,hello就在这个子进程当中运行;

程序的运行会伴随着对存储、地址的操作;首先,在hello中的地址为虚拟地址,要讲虚拟地址翻译映射到物理地址,才能对该地址进行操作;

最后由shell父进程回收终止的hello进程。

总而言之,我们会发现一个小小的hello world的背后隐藏的是一大堆复杂的机制支撑着它,这便是“计算机系统”的真正含义。很难想象那些活跃于大家日常生活中、大家耳熟能详的程序有多么的复杂,但是通过计算机系统这门课的知识可以让我们对这些程序的内涵有了一些基本的理解,这也许就是这门课真正的意义。


附件

   


参考文献

[1]  林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.

[2]  辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.

[3]  赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).

[4]  谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.

[5]  KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.

[6]  CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.

[7] https://blog.csdn.net/qq_53314152/article/details/124620978

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值