HIT-CSAPP大作业程序人生

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学   号 1180300128
班   级 1803001
学 生 xiaoyuan    
指 导 教 师 郑贵滨

计算机科学与技术学院
2019年12月
摘 要
在本来论文中我将运用在计算机系统课程中学到的知识和技能运用出来,全方位展示一个系统程序的前世今生,我们将看到一个c代码文件是如何在计算机中编译,如何转化为汇编文件再变成二进制文件,又是通过哪些步骤和原理转变为一个可执行程序并在系统层面运行,在这中间又发生了哪些有趣的事情,我们将在这篇论文中一探究竟.

关键词:计算机系统 编译 重定位 链接 系统级I/O;

目 录

第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 4 -
第2章 预处理 - 5 -
2.1 预处理的概念与作用 - 5 -
2.2在UBUNTU下预处理的命令 - 5 -
2.3 HELLO的预处理结果解析 - 5 -
2.4 本章小结 - 5 -
第3章 编译 - 6 -
3.1 编译的概念与作用 - 6 -
3.2 在UBUNTU下编译的命令 - 6 -
3.3 HELLO的编译结果解析 - 6 -
3.4 本章小结 - 6 -
第4章 汇编 - 7 -
4.1 汇编的概念与作用 - 7 -
4.2 在UBUNTU下汇编的命令 - 7 -
4.3 可重定位目标ELF格式 - 7 -
4.4 HELLO.O的结果解析 - 7 -
4.5 本章小结 - 7 -
第5章 链接 - 8 -
5.1 链接的概念与作用 - 8 -
5.2 在UBUNTU下链接的命令 - 8 -
5.3 可执行目标文件HELLO的格式 - 8 -
5.4 HELLO的虚拟地址空间 - 8 -
5.5 链接的重定位过程分析 - 8 -
5.6 HELLO的执行流程 - 8 -
5.7 HELLO的动态链接分析 - 8 -
5.8 本章小结 - 9 -
第6章 HELLO进程管理 - 10 -
6.1 进程的概念与作用 - 10 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 10 -
6.3 HELLO的FORK进程创建过程 - 10 -
6.4 HELLO的EXECVE过程 - 10 -
6.5 HELLO的进程执行 - 10 -
6.6 HELLO的异常与信号处理 - 10 -
6.7本章小结 - 10 -
第7章 HELLO的存储管理 - 11 -
7.1 HELLO的存储器地址空间 - 11 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 11 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -
7.5 三级CACHE支持下的物理内存访问 - 11 -
7.6 HELLO进程FORK时的内存映射 - 11 -
7.7 HELLO进程EXECVE时的内存映射 - 11 -
7.8 缺页故障与缺页中断处理 - 11 -
7.9动态存储分配管理 - 11 -
7.10本章小结 - 12 -
第8章 HELLO的IO管理 - 13 -
8.1 LINUX的IO设备管理方法 - 13 -
8.2 简述UNIX IO接口及其函数 - 13 -
8.3 PRINTF的实现分析 - 13 -
8.4 GETCHAR的实现分析 - 13 -
8.5本章小结 - 13 -
结论 - 14 -
附件 - 15 -
参考文献 - 16 -

第1章 概述
1.1 Hello简介
首先介绍一下Hello的P2P过程。Program to process,首先产生program,先编写好一个hello.c文件,然后经过预处理生成hello.i文件,然后汇编器将hello.i文件生成hello.s汇编文件,汇编器将hello.s文件转换为可重定位二进制文件hello.o,最后链接器将该文件与头文件中所需的库函数链接形成可执行elf文件。在命令行中输入./hello,Shell解析命令为外部命令,调用fork函数创建一个子进程,由此将program变成一个progress。调用execve函数执行程序、加载进程,sbrk函数开辟内存空间,mmap将其映射到内存中,这就是P2P的过程。
在运行hello程序的过程中,CPU也发挥着作用。CPU为hello程序分配内存和时间片。CPU通过取指、译码、执行、访存、写回、更新PC等执行hello程序。CPU访问hello程序的数据,首先需要将虚拟地址转换为物理地址,通过TLB和页表加速地址的翻译,cache加快数据的传输。IO管理和Shell的进程处理和信号处理机制可以处理hello运行过程中的信号,最终,当进程结束时,shell调用waitpid函数回收子进程 ,brk函数回收内存。这就是O2O的过程。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件:Inter core i5,X64 CPU,8GRAM
软件:Windows10,VMware 15,Ubuntu 18.04
调试工具:gcc,gdb,edb,readelf,objdump,vscode
1.3 中间结果
中间文件 作用
hello.i 预处理产生的文件

hello.s 将预处理文件变为汇编文件
hello.o 汇编器将汇编文件变为可重定位文件
hello ELF格式的可执行文件
hello l.d hello的反汇编文件,查看链接
hello .d hello的反汇编文件,查看汇编代码
1.4 本章小结

(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
预处理的概念:在编译之前进行的处理
预处理的作用:1.文件包含:将源文件中以#include格式包含的文件复制到编译的源文件中;2.宏定义:用实际值代替用#define定义的字符串;3.条件编译:根据#if 后面的条件决定需要编译的代码
2.2在Ubuntu下预处理的命令
Gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析
原来的hello.c
zhi
预处理之后得到的hello.i如下,可以看见hello.i把#include格式baohan的头文件都复制到main函数之前了,还发现头文件的引用是层层嵌套的

2.4 本章小结
这章展示了预处理的概念和作用,对c文件进行了预处理操作得到了.i文件,查看.i文件得知了这一阶段将源文件中以#include格式包含的文件复制到编译的源文件中,这种引用头文件的方法提供了很多便利,对于程序员来说可以直接引用许多已经写好的头文件,减少编程量,规范代码,且便于移植。

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
编译的概念:利用编译程序从源语言编写的源程序产生目标程序的过程,把预处理文件转换成汇编语言文件,把高级语言变成计算机可以识别的2进制语言,编译分为五个阶段:词法分析、语法分析、语义检查和中间代码生成、代码优化、目标代码生成。
编译的作用:将高级语言程序解释称为计算机所需的详细机器语言指令集。
3.2 在Ubuntu下编译的命令
Gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析
Hello.s的文件内容

3.3.1数据
1、全局变量
hello.c定义了一个全局变量int型sleepsecs,且赋初值为2,将sleepsecs存放在.data段,.data段4字对齐,sleepsecs变为long型,数值为2,经过编译之后存放在.rodata段。

2、局部变量
main函数中定义了int型局部变量i,局部变量存储在栈上,从汇编代码中可见,i存储在-4(%rbp)的地址上,初始值为0,每一次循环+1,跳出循环的条件为i>9。

3、字符串
在hello.c的main函数中共有两个printf输出的字符串,查看汇编代码发现存储在.rodata段。
LC0存储的是“Usage: Hello 学号 姓名!”,其中汉字和!一个字符使用三个utf-8编码,LC1存储的是从终端输入的两个参数,argv[0],argv[1],

3.3.2 赋值
hello.c中将全局变量sleepsecs赋初值为2.5,sleepsecs一开始存储在.data段,而赋值完成在.data段,已经将sleepsecs赋值为long型的2,还有对局部变量i的赋值,赋初值为0,由 movl $0, -4(%rbp)完成。

3.3.3 类型转换
hello.c中存在一个隐式类型转换,即int型全局变量sleepsecs的转换,sleepsecs赋初值为2.5,但sleepsecs为int型即向0舍入为2。

3.3.4 函数及其参数
1、main函数
main函数存储在.text段中,程序运行时调用。 main函数的参数为int型的argc和char**的argv, argc为命令行输入的个数,argv[]为输入的字符串,这两个参数都从命令行输入。其中argc存储在%rdi,argv存储在%rsi。然后将argc,argv都存储在栈中,其中,argc存储在-20(%rbp),argv存储在-32(%rbp),在运算时,直接使用栈中存储的数据。

2、printf函数
hello.c中使用了两个printf函数,其中第一个printf函数打印常量字符串,该字符串存储在.LC0中,当判断条件argc!=3满足时,将LC0的值赋给%rdi,其中%rip是下一条指令的地址,然后调用puts函数输出字符串。第二个printf函数打印的是从命令行写入的字符串,分别为argv[0],argv[1],还有.LC1。编译器使用多次movq指令,将argv[0]存储在%rdx,argv[1]存储在%rsi,然后leaq指令将.LC1存储在%rdi,最后调用printf函数输出字符串。

3、getchar函数
getchar函数没有参数,在循环结束之后直接调用。

4、sleep函数
sleep函数的参数是sleepsecs(%rip),movl将参数赋值给%eax,然后再赋值给%edi,最后调用sleep函数。

5、exit函数
将1赋值给%edi,然后call exit函数,1位exit的唯一参数,表示exit非正常运行退出程序。

3.3.5、数组操作
hello.c中使用了argv数组。数组通过地址访存,由首地址为基准进行地址的计算。从汇编代码中可以看到,编译器通过movq取内容指令和addq指令得到argv[0],argv[1],其中argv的首地址存储在-32(%rbp),然后根据指针8字节大小+8,+16得到地址。

3.3.6、关系运算and控制转移
hello.c中使用到了比较运算和控制转移,比较运算分别为argc!=3和i<=9,若argc!=3,跳转到.L2执行,若i<=9,跳转到.L4执行。
编译器通过cmp指令进行比较,然后调用je,jle等条件跳转指令(设置CF,ZE,SF,OF条件码,条件码不同,条件不同)跳转到某一地址执行,执行完之后返回下一条指令,这是控制转移,常常和关系运算一起使用。在这里只使用了cmp指令。其实还有test指令,和cmp指令类似。
常在if else,while,for等选择结构、循环结构中使用。
3.4 本章小结
这一章主要讲述了编译器将预处理文件转换为汇编语言文件时,对于不同的数据类型和各种操作,编译器是如何处理的。以hello.c和hello.s为例,包含了变量、常量、函数、关系运算、数组、控制转移等操作,通过对这些操作汇编代码的解读,了解了编译器的编译功能和汇编语言的基本操作。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编器把汇编语言转换为相对应的机器语言,再把生成的机器语言变成可重定位目标文件,即由.s文件生成.o文件,其中,可重定位目标文件包含二进制代码和数据。
汇编的作用:把编译器产生的汇编语言翻译成二进制机器语言,由.s文件生成.o文件。
4.2 在Ubuntu下汇编的命令
Gcc -c hello.s -o hello.o

4.3 可重定位目标elf格式
ELF头

段头部表

.intt
.text
.rodata
.data

.bss

.symtab
.degbug

.line
.strtab

节头部表

段头部表:将连续的文件映射到运行时的内存段
. init : 定义了_init函数,程序初始化代码会调用它
. text : 已编译程序的机器代码
. rodata : 只读数据,比如printf语句中的格式串和开关语句的跳转表
. data : 已初始化的全局和静态C变量
. bss : 未初始化的全局和静态C变量
. symtab :一个符号表,它存放在程序中定义和引用的函数和全局变量的信息
. debug : 一个调试符号表,其条目时程序中定义的全局变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。
. line : 原始C源程序的行号和.text节中机器指令之间的映射
. strtab : 一个字符串表,其内容包括 .symtab 和 .debug节中的符号表,以及节头部中的节名字。
ELF头:

节头部表:

.text节的重定位信息:

.symtab表:记录了hello.c中调用的函数和全局变量的的名称,类型,地址等信息,value地址信息,在可重定位文件中是起始位置的偏移量。bind表示符号是全局的还是本地的。UND表示为在本文件中定义的符号。

4.4 Hello.o的结果解析
objdump -d -r hello.o

1、机器语言的构成
机器语言是计算机能够直接识别的二进制代码,在hello.o的反汇编文件中,使用16进制表示,由多个字节表示,每个字节由两个十六进制数表示。
2机器语言与汇编语言的映射
机器语言对应的反汇编的每一条语句由多个字节表示,每一组字节都是一条汇编指令,不过由.o文件生成的反汇编文件与hello.s的汇编语言存在差别。
(1)操作数的差别
hello.s中立即数表示使用十进制,而生成的反汇编立即数使用16进制表示。
(2)函数调用
hello.s中函数调用直接call+函数名即可,call printf等,而在反汇编中则不同,链接器为函数调用找到匹配的函数的可执行代码的位置,故在call指令调用函数写的是地址,地址是符号重定位之后的地址,即运行的地址,同时也要记下PC下一条指令的地址。如图中的getchar、sleep函数,会加载可重定位信息以计算重定位后的地址。
(3)分支转移
hello.s中的跳转指令是像jmp .L1一样的格式,.L1是代码段。而反汇编中的跳转指令是直接跳转到某个地址,或是直接的地址,或是函数相对偏移地址。
(4)全局变量
hello.s中调用全局变量,如将全局变量值直接赋给寄存器,全局变量的值即为.LC0。而在反汇编代码中,将可重定位文件重定位之后计算出全局变量的地址,然后将该全局变量地址的值赋值给寄存器。如lea 0x0(%rip) %rdi。
4.5 本章小结
这一章将.s文件转换成.o可重定位文件。分析了ELF格式下可重定位文件的组成和各个节存储的内容,使用readelf看具体查看。用objdump工具对可重定位文件进行了反汇编,并将反汇编代码与汇编代码进行比较,比较两者之间的不同之处,得出机器语言和汇编代码之间的映射关系。
(第4章1分)

第5章 链接
5.1 链接的概念与作用
概念:将一个或多个由编译器或汇编器生成的目标文件外加库链接为一个可执行文件。目标文件是包括机器码和链接器可用信息的程序模块。
作用:把可重定位目标文件和命令行参数作为输入,产生一个完全链接的,可以加载运行的可执行目标文件。
5.2 在Ubuntu下链接的命令
命令:ld -o hello -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 hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o

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

5.3.2 节头部表, 节头部表(Section Headers)对hello的各个节的信息做了说明,包括各段的起始地址(Address),大小(size),类型(Type),偏移(Offset)对齐要求(Align)等信息,如下图:

.text节是保存了程序代码指令的代码节。一段可执行程序,存在Phdr,.text就会存在于text段中。由于.text节保存了程序代码,因此节的类型为SHT_PROGBITS。
.rodata 保存只读数据。类型SHT_PROGBITS。
.plt 过程链接表(Procedure Linkage Table),包含动态链接器调用从共享库导入的函数所必须的相关代码。存在于text段中,类型SHT_PROGBITS。
.bss节保存未初始化全局数据,是data的一部分。程序加载时数据被初始化成0,在程序执行期间可以赋值,未保存实际数据,类型SHT_NOBITS。
.got节保存全局偏移表。它和.plt节一起提供了对导入的共享库函数访问的入口。由动态链接器在运行时进行修改。如果攻击者获得堆或者.bss漏洞的一个指针大小写原语,就可以对该节任意修改。类型SHT_PROGBITS。
.dynsym节保存共享库导入的动态符号信息,该节在text段中,类型SHT_DYNSYM。
.dynstr保存动态符号字符串表,存放一系列字符串,代表了符号的名称,以空字符作为终止符。
.rel节保存重定位信息,类型SHT_REL。
.hash节,也称为.gnu.hash,保存一个查找符号散列表。
.symtab节,保存了ElfN_Sym类型的符号信息,类型SHT_SYMTAB。
strtab节,保存符号字符串表,表中内容被.symtab的ElfN_Sym结构中的st_name条目引用。类型SHT_SYMTAB。
.shstrtab节,保存节头字符串表,以空字符终止的字符串集合,保存了每个节节名,如.text,.data等。有个e_shsrndx的ELF文件头条目会指向.shstrtab节,e_shstrndx中保存了.shstrtab的偏移量。这节的类型是SHT_SYMTAB。
.ctors和.dtors节,前者构造器,后者析构器,指向构造函数和析构函数的函数指针,构造函数是在main函数执行前需要执行的代码,析构是main函数之后需要执行的代码。

5.3.3 链接器在链接可执行文件或动态库的过程中,它会把来自不同可重定位对象文件中的相同名称的 section 合并起来构成同名的 section。接着,它又会把带有相同属性(比方都是只读并可加载的)的 section 都合并成所谓 segments(段)。segments 作为链接器的输出,常被称为输出section。一个单独的 segment 通常会包含几个不同的 sections,比方一个可被加载的、只读的segment 通常就会包括可执行代码section .text、只读的数据section .rodata以及给动态链接器使用的符号section .dymsym等等。section 是被链接器使用的,但是 segments是被加载器所使用的。加载器会将所需要的 segment 加载到内存空间中运行。和用sections header table 来指定一个可重定位文件中到底有哪些 sections一样。在一个可执行文件或者动态库中,也需要有一种信息结构来指出包含有哪些segments。这种信息结构就是 program header table,如下图:

5.3.4 各个段包含的节

5.3.5 .symtab 它存放在程序中定义和引用的函数和全局变量的信息,.symtab符号表不包含局部变量的条目。

5.3.6 .dynsym

5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
5.5 链接的重定位过程分析
1.hello与hello.o的不同:
链接器在链接可执行文件或动态库的过程中,它会把来自不同可重定位对象文件中的相同名称的 section 合并起来构成同名的 section,在这里因为链接的时候指定了/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o,所以将会把这些.o文件的每个节与hello.o的节合并。在合并的过程中,会根据重定位信息对相应的地方进行重定位,比如hello.o中的.text节和.data节,接着,它又会把带有相同属性(比方都是只读并可加载的)的 section 都合并成所谓 segments(段)。segments 作为链接器的输出,常被称为输出section。所以说hello.o只是hello的一部分。通过将两者的反汇编代码进行比对也能看出这一点,hello的反汇编代码中也比hello.o的反汇编代码节要多:.init .plt .text .fini,在这些节中多出了一些main函数运行必要的函数:_start,_init,__libc_csu_init,__libc_csu_fini,__libc_start_main,和在hello中调用的函数:printf、sleep、getchar、exit函数。
2.hello是如何进行重定位的:
①首先计算需要被重定位的位置
refptr = .text + r.offset
②然后链接器计算出运行时需要重定位的位置:
refaddr = ADDR(.text) + r.offset
③然后更新该位置
*refptr = (unsigned) (ADDR(r.symbol) + r.addend-refaddr)
5.6 hello的执行流程
程序名 程序地址

ld-2.23.so!_dl_start 0x00007f11037199b0

ld-2.23.so!_dl_init 0x00007f1103728740
hello!_start 0x400510
libc-2.23.so!__libc_start_main 0x00007fa17c942740
ld-2.23.so!_dl_fixup 0x00007fb4dc2d39f0
libc-2.23.so!__cxa_atexit 0x00007fb4dbf34280
libc-2.23.so!__libc_csu_init 0x400690
libc-2.23.so!_setjmp 0x00007fb4dbf2f250
hello!main 0x400606
hello!puts@plt 0x4004a0
hello!exit@plt 0x4004e0
ld-2.23.so!_dl_fixup 0x00007fb4dc2d39f0
libc-2.23.so!exit 0x00007f2137ae8030
libc-2.23.so!__run_exit_handlers 0x00007f2137ae7f10
5.7 Hello的动态链接分析
程序调用一个由共享库定义的函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。GNU编译系统使用延迟绑定的技术解决这个问题,将过程地址的延迟绑定推迟到第一次调用该过程时。
延迟绑定要用到全局偏移量表(GOT)和过程链接表(PLT)两个数据结构。如果一个目标模块调用定义在共享库中的任何函数,那么它就有自己的GOT和PLT。
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,跳转到动态链接器中。每个条目都负责调用一个具体的函数。PLT[[1]]调用系统启动函数 (__libc_start_main)。从PLT[[2]]开始的条目调用用户代码调用的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[0]和GOT[[1]]包含动态链接器在解析函数地址时会使用的信息。GOT[[2]]是动态链接器在ld-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。
5.8 本章小结
链接(linking) 是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时(compile time), 也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time), 也就是在程序被加载器(loader)加载到内存并执行时;甚至执行于运行时(run time), 也就是由应用程序来执行。

(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程是一个执行中程序的实例。是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
作用:进程的概念为我们提供这样一种假象,就好像我们的程序是系统中当前运行的唯一程序一样,我们的程序好像是独占地使用处理器和内存,处理器好像是无间断地一条接一条地执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell俗称壳,是指“为使用者提供操作界面”的软件(命令解析器)。它接收用户命令,然后调用相应的应用程序。
1.功能:命令解释。Linux系统中的所有可执行文件都可以作为Shell命令来执行。
2.处理流程:
1)当用户提交了一个命令后,Shell首先判断它是否为内置命令,如果是就通过Shell内部的解释器将其解释为系统功能调用并转交给内核执行。
2)若是外部命令或应用程序就试图在硬盘中查找该命令并将其调入内存,再将其解释为系统功能调用并转交给内核执行。
6.3 Hello的fork进程创建过程
在终端中输入./hello 学号 姓名,shell判断它不是内置命令,于是会加载并运行当前目录下的可执行文件hello.此时shell通过fork创建一个新的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库和用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。子进程与父进程有不同的pid。fork被调用一次,返回两次。在父进程中fork返回子进程的pid,在子进程中fork返回0.父进程与子进程是并发运行的独立进程。
6.4 Hello的execve过程
execve函数在新创建的子进程的上下文中加载并运行hello程序。execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有发生错误时execve才会返回到调用程序。所以,execve调用一次且从不返回。
加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域。如果hello程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器。设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。
6.5 Hello的进程执行
(系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
处理器通常用某个控制寄存器的一个模式位来提供用户模式和内核模式的功能。设置了模式位时,进程就运行在内核模式中,该进程可以执行指令集中的任何指令,可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中,用户模式中的进程不允许执行特权指令。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程的决定叫做调度。
程序在执行sleep函数时,sleep系统调用显式地请求让调用进程休眠,调度器抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。sleep的倒计时结束后,控制会回到hello进程中。程序调用getchar()时,内核可以执行上下文切换,将控制转移到其他进程。getchar()的数据传输结束之后,引发一个中断信号,控制回到hello进程中。
调用sleep:

调用getchar:

6.6 hello的异常与信号处理
信号的种类

  1. 中断是来自 I/O 设备的信号,异步发生,中断处理程序对其进行处理,返
    回后继续执行调用前待执行的下一条代码,就像没有发生过中断。
  2. 陷阱是有意的异常,是执行一条指令的结果,调用后也会返回到下一条指
    令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。
  3. 故障是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成
    功,则将控制返回到引起故障的指令,否则将终止程序。
  4. 终止是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程
    序会将控制返回给一个 abort 例程,该例程会终止这个应用程序。
    编号 名称 默认行为 相应事件
    1 SIGHUP 终止 终端线挂掉
    2 SIGINT 终止 来自键盘的挂掉
    3 SIGQUIT 终止 来自键盘的退出
    4 SIGILL 终止 非法指令
    5 SIGTRAP 终止并转储内存 跟踪陷阱
    6 SIGABRT 终止并转储内存 来自abort函数的终止信号
    7 SIGBUS 终止 总线错误
    8 SIGFPE 终止并转储内存 浮点异常
    9 SIGKILL 终止 杀死程序
    10 SIGUSR1 终止 用户自定义的信号1
    11 SIGPIPE 终止 向一个没有读用户的管道做写操作
    12 SIGUSR2 终止 用户自定义的信号2
    13 SIGSEGV 终止并转储内存 无效的内存引用(段故障)
    14 SIGALRM 终止 来自alarm函数的定时器信号
    15 SIGTERM 终止 软件终止信号
    16 SIGSTKFLT 终止 协处理器上的栈故障
    17 SIGCHLD 忽略 一个子进程停止或终止
    18 SIGCONT 忽略 继续进程如果该进程停止
    19 SIGSTOP 停止直到下一个SIGCONT 不是来自终端的停止信号
    20 SIGTSTP 停止直到下一个SIGCONT 来自终端的停止信号
    21 SIGTTIN 停止直到下一个SIGCONT 后台进程从终端读
    22 SIGTTOU 停止直到下一个SIGCONT 后台进程从终端写
    23 SIGURG 忽略 套接字上的紧急情况
    24 SIGXCPU 终止 CPU时间限制超出
    25 SIGXFSZ 终止 文件大小限制超出
    26 SIGVTALRM 终止 虚拟定时器期满
    27 SIGPROF 终止 剖析定时器期满
    28 SIGWINCH 忽略 窗口大小变化
    29 SIGIO 终止 在某个描述符上可以执行I\O操作
    30 SIGPWR 终止 电源故障
    6.7本章小结
    linux命令行shell是一个非常强大的工具,用它可以更方便的执行Hello和发送各种命令请求。通过信号等方式可以实现异常处理,让Hello在顺序执行者也能处理一些突发状况和实现一些功能。进程调度实现了各个进程计算资源合理分配,互不干扰,提高了系统稳定性和效率

(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
物理地址(physical address)
用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。
逻辑地址(logical address)
逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。如Hello中sleepsecs这个操作数的地址。
线性地址(linear address)或也叫虚拟地址(virtual address)
跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式内存管理方式就是直接将逻辑地址转换成物理地址,也就是CPU不支持分页机制。其地址的基本组成方式是段号+段内偏移地址。
在x86保护模式下,段的信息(段基线性地址、长度、权限等)即段描述符占8个字节,段信息无法直接存放在段寄存器中(段寄存器只有2字节)。Intel的设计是段描述符集中存放在GDT或LDT中,而段寄存器存放的是段描述符在GDT或LDT内的索引值(index)。
首先给定一个完整的逻辑地址[段选择符:段内偏移地址],
1.看段选择描述符中的T1字段是0还是1,可以知道当前要转换的是GDT中的段,还是LDT中的段,再根据指定的相应的寄存器,得到其地址和大小,我们就有了一个数组了。
2.拿出段选择符中的前13位,可以在这个数组中查找到对应的段描述符,这样就有了Base,即基地址就知道了。
3.把基地址Base+Offset,就是要转换的下一个阶段的地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
1.基本原理
将程序的逻辑地址空间划分为固定大小的页(page),而物理内存划分为同样大小的页框(page frame)。程序加载时,可将任意一页放入内存中任意一个页框,这些页框不必连续,从而实现了离散分配。该方法需要CPU的硬件支持,来实现逻辑地址和物理地址之间的映射。在页式存储管理方式中地址结构由两部构成,前一部分是虚拟页号(VPN),后一部分为虚拟页偏移量(VPO):

页式管理方式的优点是:
1)没有外碎片
2)一个程序不必连续存放。
3)便于改变程序占用空间的大小(主要指随着程序运行,动态生成的数据增多,所要求的地址空间相应增长)。
缺点是:要求程序全部装入内存,没有足够的内存,程序就不能执行。
2.页式管理的数据结构
在页式系统中进程建立时,操作系统为进程中所有的页分配页框。当进程撤销时收回所有分配给它的页框。在程序的运行期间,如果允许进程动态地申请空间,操作系统还要为进程申请的空间分配物理页框。操作系统为了完成这些功能,必须记录系统内存中实际的页框使用情况。操作系统还要在进程切换时,正确地切换两个不同的进程地址空间到物理内存空间的映射。这就要求操作系统要记录每个进程页表的相关信息。为了完成上述的功能,—个页式系统中,一般要采用如下的数据结构。
页表:页表将虚拟内存映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。页表是一个页表条目(PTE)的数组。虚拟地址空间的每个页在页表中一个固定偏移量处都有一个PTE。假设每个PTE是由一个有效位和一个n位地址字段组成的。有效位表明了该虚拟页当前是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置,这个物理页中缓存了该虚拟页。如果没有设置有效位,那么一个空地址表示这个虚拟页还未被分配。否则,这个地址就指向该虚拟页在磁盘上的起始位置。

3.页式管理地址变换
MMU利用VPN来选择适当的PTE,将列表条目中PPN和虚拟地址中的VPO串联起来,就得到相应的物理地址。

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

图7-5给出了Core i7 MMU如何使用四级页表来将虚拟地址翻译成物理地址。36位的虚拟地址被分割成4个9位的片。CR3寄存器包含L1页表的物理地址。VPN1有一个到L1 PTE的偏移量,找到这个PTE以后又会包含到L2页表的基础地址;VPN2包含一个到L2PTE的偏移量,找到这个PTE以后又会包含到L3页表的基础地址;VPN3包含一个到L3PTE的偏移量,找到这个PTE以后又会包含到L4页表的基础地址;VPN4包含一个到L4PTE的偏移量,找到这个PTE以后就是相应的PPN(物理页号)。
7.5 三级Cache支持下的物理内存访问
Core i7 MMU使用四级页表来将虚拟地址翻译成物理地址,我们得到了物理地址PA。现在分析三级cache支持下的物理内存访问。如下图,以L1 d-cache的介绍为例,L2和L3同理。

L1 Cache是8路64组相联。块大小为64B。因此CO和CI都是6位,CT是40位。根据物理地址(PA),首先使用CI组索引,每组8路,分别匹配标记CT。如果匹配成功且块的有效位是1,则命中,根据块偏移CO返回数据。
如果没有匹配成功或者匹配成功但是标志位是1,则不命中,向下一级缓存中取出被请求的块,然后将新的块存储在组索引指示的组中的一个高速缓存行中。一般而言,如果映射到的组内有空闲块,则直接放置,否则必须驱逐出一个现存的块,一般采用最近最少被使用策略LRU进行替换。
7.6 hello进程fork时的内存映射
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念
7.7 hello进程execve时的内存映射
exceve函数加载和执行程序Hello,需要以下几个步骤:
1.删除已存在的用户区域。
2.映射私有区域。为Hello的代码、数据、bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时复制的。
3.映射共享区域。比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中
7.8 缺页故障与缺页中断处理
在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页(page fault) 。图7-8展示了在缺页之前我们的示例页表的状态。CPU引用了VP3中的一个字,VP3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE3,从有效位推断出VP3未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4。如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4的页表条目,反映出VP4不再缓存在主存中这一事实。

缺页处理程序从磁盘上用VP3的副本取代VP4,在缺页处理程序重新启动导致缺页的指令之后,该指令将从内存中正常地读取字,而不会再产生异常。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆.分配器将堆视为一组不同大小的块的集合,来维护,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

1.显式分配器:要求应用显式地释放任何已分配的块。例如C程序通过调用malloc函数来分配一个块,通过调用free函数来释放一个块。其中malloc采用的总体策略是:先系统调用sbrk一次,会得到一段较大的并且是连续的空间。进程把系统内核分配给自己的这段空间留着慢慢用。之后调用malloc时就从这段空间中分配,free回收时就再还回来(而不是还给系统内核)。只有当这段空间全部被分配掉时还不够用时,才再次系统调用sbrk。当然,这一次调用sbrk后内核分配给进程的空间和刚才的那块空间一般不会是相邻的。

2.隐式分配器:也叫做垃圾收集器,例如,诸如Lisp、ML、以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。

隐式空闲链表:

这样的一种结构,主要是由三部分组成:头部、有效载荷、填充(可选);

头部:是由块大小+标志位(a已分配/f空闲);有效载荷:实际的数据

简单的放置策略:

1> 首次适配:从头搜索,遇到第一个合适的块就停止;

2> 下次适配:从头搜索,遇到下一个合适的块停止;

3> 最佳适配:全部搜索,选择合适的块停止。

分割空闲块:

适配到合适的空闲块,分配器将空闲块分割成两个部分,一个是分配块,一个是新的空闲块。

增加堆的空间:
通过调用sbrk函数,申请额外的存储器空间,插入到空闲链表中 。
合并
(1)合并时间:立即合并和推迟合并。
立即合并:在每次一个块被释放时,就合并所有的相邻块
推迟合并:直到某个分配请求失败时,扫描整个堆,合并所有的空闲块。
(2)合并:(4种情况)
a.当前块前后的块都为已分配块:不需要合并
b.当前块后面的块为空闲块:用当前块和后面块的大小的和来更新当前块的头部和后面块的脚部。
c.当前块前面的块为空闲块:用当前块和前面块的大小的和来更新前面块 的头部和当前块的脚部。
d.当前块的前后块都为空闲块:用三个块大小的和来更新前面块的头部和 后面块的脚部。
其中,查询前面块的块大小时可以通过脚部来查,查询后面块的块大小时 可以通过头部来查。
7.10本章小结
虚拟内存使用硬盘的一部分来模拟主存,主存就是虚拟内存的一个cache,CPU寻址的时候首先会访问虚拟内存(使用页表来翻译成物理地址),若虚拟内存中的这个内容没有被缓存,那么就会触发一个缺页故障(miss),主存就会将这个内容从硬盘中拿出来,放到主存上。然后CPU重新访存,这就相当于CPU一直在CPU在主存中“取东西”(数据和指令),所以相当于虚拟内存也成了主存。为了更快的速度,CPU需要在主存上取东西的时候,也不是直接访问主存,而是先查看cache(查询页表先查询TLB,再查询cache)中有没有它想要的东西。由于局部性原理,这样使得CPU访存更快了。要执行一个新程序时,首先给新程序创建一个新进程(fork),创建新进程即就是复制一份父进程的各种的数据结构来表示它,这些数据结构中有可以表示虚拟内存的mm_struct、区域结构和页表等。加载可执行程序(execve),就是把在硬盘上的可执行文件的各个段映射到新进程的虚拟内存的各个段中,当CPU要执行的时候发现它还没有被缓存到主存上(缺页故障),此时就会将硬盘上的可执行文件复制到主存中,复制时是按页的大小来复制,需要哪一页就复制哪一页。动态分配就是将虚拟内存中的堆映射到一个匿名文件,当程序第一次要使用这个内存时,内核就会在主存中找到一个合适的牺牲页面,将其复制到硬盘中,将这块区域作为程序申请到的内存。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
文件的类型:
普通文件(regular file):包含任意数据的文件。
目录(directory):包含一组链接的文件,每个链接都将一个文件名映射到一个文件(他还有另一个名字叫做“文件夹”)。
套接字(socket):用来与另一个进程进行跨网络通信的文件
命名通道
符号链接
字符和块设备
设备管理:unix io接口
打开和关闭文件
读取和写入文件
改变当前文件的位置
8.2 简述Unix IO接口及其函数
8.2.1打开和关闭文件:
int open(char *filename, int flags, mode_t mode);
open函数将filename转换为一个文件描述符,并返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
int close(int fd);
进程通过调用close关闭一个打开的文件。
8.2.2读和写文件
ssize_t read(int fd, void *buf, size_t n);
read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。
ssize_t write(int fd, const void *buf, size_t n);
write函数从内存位置buf复制之多n个字节到描述符fd的当前文件位置。
DIO *opendir(const char *name);
函数opendir以路径名为参数,返回指向目录流的指针。流是对条目有序列表的抽象,在这里是指目录项的列表。
struct dirent *readdir(DIR *dirp);
每次对readdir的调用返回的都是指向流dirp中下一个目录项的指针,或者,如果没有更过目录项则返回NULL。
int closedir(DIR *dirp);
函数closedir关闭流并释放其所有的资源。
8.2.3I/O重定向
int dup2(int oldfd, int newfd);
dup2函数复制描述符表表项oldfd到描述符表项newfd,覆盖描述符表表项newfd以前的内容。如果newfd已经打开了,dup2会在复制oldfd之前关闭newfd。
8.3 printf的实现分析
查看printf代码:
int printf(const char fmt, …)
{
int i;
char buf[256];
va_list arg = (va_list)((char
)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
首先arg获得第二个不定长参数,即输出的时候格式化串对应的值。
查看vsprintf代码:
int vsprintf(char *buf, const char fmt, va_list args)
{
char
p;
char tmp[256];
va_list p_next_arg = args;
for (p = buf; *fmt; fmt++)
{
if (*fmt != ‘%’) //忽略无关字符
{

        *p++ = *fmt;
        continue;
    }
    fmt++;
    switch (*fmt)
    {
        case 'x':     //只处理%x一种情况
            itoa(tmp, *((int*)p_next_arg)); //将输入参数值转化为字符串保存在tmp
            strcpy(p, tmp);  //将tmp字符串复制到p处
            p_next_arg += 4; //下一个参数值地址
            p += strlen(tmp); //放下一个参数值的地址
            break;
        case 's':
            break;
        default:
            break;
    }
}
return (p - buf);   //返回最后生成的字符串的长度

}
则知道vsprintf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。
在printf中调用系统函数write(buf,i)将长度为i的buf输出。write函数如下
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall,查看syscall的实现:
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
syscall将字符串中的字节“Hello 1180300128 肖远”从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。
显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
于是我们的打印字符串“Hello 1180300128 肖远”就显示在了屏幕上.
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。
getchar函数落实到底层调用了系统函数read,通过系统调用read读取存储在键盘缓冲区中的ASCII码直到读到回车符然后返回整个字串,getchar进行封装,大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf函数和getchar函数。(第8章1分)
结论
1.用计算机系统的语言,逐条总结hello所经历的过程。

预处理阶段:预处理(cpp)根据字符#开头的命令,修改原始的C程序。比如我们的hello.c里的第一行的#include<stdio.h>命令告诉预处理读取系统文件stdio.h文件的内容,并把它插入程序文本的中,结果就得到另一个C程序,通常以.i作为文件的扩展名。
编译:编译器(ccl)Complier Collection:作用将文本文件的hello.i翻译成hello.s 它包括以一个汇编语言程序。汇编语言程序中的每条语句都以一种标准的文本格式确切地描述一条低级机器语言指令。

汇编: 汇编器(as)Assembler将hello.s翻译成机器语言指令,也就是转换为二进制指令,计算机可以真正运行的指令,它是通过这样处理,它 把这些指令打包成可重定位目标文件, 并将所有的结果保存在目标文件中hello.o。目标英文(object) 就是一个二进制文件,它的里面装的全是二进制,而不是字符。
链接: 把可重定位目标文件和命令行参数作为输入,产生一个完全链接的,可以加载运行的可执行目标文件。
运行:在shell中输入./hello 1170300825 lidaxin
创建子进程:shell进程调用fork为其创建子进程
运行程序:shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数。
执行指令:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流
访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。
信号:如果运行途中键入ctr-c ctr-z则调用shell的信号处理函数分别停止、挂起。
结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。
2. 感想,深刻感受到了计算机系统的复杂与精妙.

(结论0分,缺失 -1分,根据内容酌情加分)

附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.i(hello.c预处理之后的程序文本)
hello.s(hello.i编译成汇编语言之后的程序文本)
hello.o(hello.s生成的二进制文件)
hello(可执行的hello二进制文件)
asm.txt (反汇编文件)

(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
[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.
(参考文献0分,缺失 -1分)

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值