计算机系统
大作业
题 目 程序人生-Hello’s
P2P
专 业 数据科学与大数据技术
学 号 1180400417
班 级 1804004
学 生 朱润文
指 导 教 师 史先俊
计算机科学与技术学院
2020年3月
摘 要
本论文通过记录Hello World 程序的整个生命周期,完整探索了程序在计算机系统下的每部实现过程,从预处理到编译汇编再到链接以及程序运行的的内存管理,进程控制及IO口控制,生动展示了计算机系统的基础知识,程序的生成运行过程。
关键词:Hello World;预处理;编译;汇编;链接;计算机系统…;
目 录
第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,即从源文件到可执行目标文件然后被分配进程执行的过程。生成可执行目标文件中间经历了预处理,编译,汇编,链接这四个具体的过程。
预处理:通过预处理器(ccp)完成程序的宏替换,条件编译以及文件包含等功能,最后输出的文件为hello.i;
编译:编译器(ccl)将hello.i翻译成汇编语言程序,得到hello.s汇编文本文件;
汇编:汇编器(as)将hello.s翻译成机器语言指令(二进制文件)。把这些指令打包成可重定位目标文件hello.o;
链接:链接器(ld)将hello中调用的库函数如printf的可重定位目标文件printf.o与hello.o合并起来生成可执行目标文件hello;
最后,OS在Bash中加载运行hello,给hello分配进程,至此,Hello完成了整个Program to Process的过程。
所谓Hello的020指的是Bash通过exceve在子进程中加载Hello,这一过程首先需要删除原虚拟内存的数据以及代码,给Hello构建新的代码段,数据段,bss等区域结构,cpu给Hello分配时间片,I/O控制负责程序在屏幕的输出,最后,程序去执行结束,Hello的父进程回收内存,删除Hello运行痕迹,Hello完成从0到0的整个过程。
1.2 环境与工具
x64 CPU;2.70GHz;8.00G RAM;256GHD Disk 以上;
Windows10 64位;Vmware 11以上;Ubuntu 19.04 LTS
64位;
Visual Studio 2019 64位;CodeBlocks 64位;vi/vim/gedit+gcc
1.3 中间结果
Hello.i
hello.c经过预处理得到的文本文件
Hello.s
hello.i经过编译得到的汇编语言文本文件
Hello.o
hello.s经过汇编得到的可重定位的目标文件
Hello
hello.o经过链接得到的可执行目标文件
Hello.txt
hello.s的反汇编文本文件
1.4 本章小结
本章我们概括性的阐述了Hello的P2P以及020的过程,从整体上把握了一个程序从源文件到执行,从分配进程到结束进程的大致过程,体现了计算机系统内部的调度过程,加深了对计算机系统的了解。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理指的是在编译之前通过预处理器对程序进行的操作,主要根据#符后的内容对源文件进行修改,插入等功能,最后生成扩展名为.i的文件
预处理主要有三个方面的作用。
(1) 实现宏定义,即将#define 后面定义的字符用实际数值进行替换
(2) 实现文件包含,如预处理器翻译#include
(3) 实现条件编译,有些代码语句只希望在满足某些条件下才进行编译,通过预处理就可实现选择性编译
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
使用gedit命令查看hello.i文件以及hello.c源文件。
对比发现在hello.i文件中插入了大量不属于源程序的代码语句,这些代码语句就是预处理所进行的文件包含操作,预处理将#include后的文件插入源文件中。
由于源文件中没有宏定义,所以在.i中没有体现,我们自己尝试编写代码。
查看.i文件,发现宏定义的ONE在源文件的语句中已经被替换为1.
2.4 本章小结
本章通过linux下的gcc实现了对hello.c的预处理工作,通过分析hello.i文件,看到了预处理实现的文件包含以及宏定义的替换等功能;了解了预处理的作用和原理,加深了对源文件最终转化成为可执行目标文件的中间过程的理解。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译指的是编译器将预处理过后的.i文件编译成为汇编语言文本文件的过程。其输出文件扩展名为.s.
编译的作用是将所用的用高级语言编写的程序转化为统一的汇编语言程序。每种不同高级语言的编译的的编译方式是不同的,编译过程中,编译器会检查语法以及代码的正确性。
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1数据
(1)字符串常量,查看hello.s文件可以看到,源文件的两个字符串用法: “Hello 学号 姓名 秒数!\n”以及“Hello %s %s\n”分别被存在.LC0以及.LC1处,均在.rodate段
(2)变量
整型变量i: 可知未初始化的i被放在地址为-4(%rab)栈区上,movl $0, -4(%rab)将其初始化为0;
传入main函数的agrc参数:,通过分析可以看出,传入main函数的第一个参数首先被放在edi寄存器中,通过语句将其存储在栈上空间-20(%rbp)中。
数组:由该段代码可知,agrv[]被存在栈上空间-32(%rbp)上,通过指针偏移的方式寻找数组元素。
3.3.2赋值
整个程序的赋值语句只有一处,“movl $0, -4(%rab)”将i赋初值为0;
3.3.3类型转换
通过调用atoi()这个库函数实现了将存在-8(%rbp)处的argv[3]由字符串转化为整型数据
3.3.4算数操作
对i实现了加1操作;
,实现了数组的访问,其中8是每个数组元素所占字节数
3.3.5关系操作
,将argc的值与4进行比较,以此改变标志位
,将i的值与7作比较,继而实现了循环控制
3.3.7控制转移
,这两句代码实现了for(i=0;i<8;i++)的循环控制代码
这两句实现了if(argc!=4)的条件分支代码
3.3.8函数操作
整个hello的main函数中共有五次函数调用,分别调用了puts(),printf(),exit(),getchar(),sleep()
函数调用通过call语句实现,参数传递首要通过寄存器,如果寄存器不够则会通过栈的方式传递参数。
函数返回时,需要进行弹栈操作,找到函数返回地址进行返回
3.4 本章小结
本章我们深入了解了编译过程及其输出文件的具体内容,了解了数据存放,数据运算,函数调用以及分支控制的相关实现,加深了我对汇编语言的了解。
(以下格式自行编排,编辑时删除)
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编指汇编器将hello.c翻译成二进制的可重定位目标文件hello.o的过程。其作用在于生成计算机可读的二进制代码,并为下一步的链接做准备。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
ELF Header:
各节基本信息:
重定位项目分析:
由图中可以看出,重定位信息共有七条,分别对应.L0(存放输出的字符串),puts(),exit(),.L1(存放printf()的格式字符),printf(),atoi(),sleep()以及getchar()
其中offset表示需要进行重定位的代码在.text或.date中的偏移量
Info的高8位表示符号值r.symbol,低八位表示重定位的类型,如.L0的重定位类型为一个使用32位PC相对地址的引用。
Attend为计算偏移量的辅助信息。
4.4 Hello.o的结果解析
对比汇编文件和编译文件的汇编代码,发现有如下区别
关系操作跳转如条件判断,循环分支都与重定位条目对应
编译文件跳转至.L2,而在汇编文件中跳转到的是一条重定位条目指引的信息
立即数变为16进制表示
对函数的调用也与重定位条目对应
4.5 本章小结
本章学习了从hello.s到hello.o的汇编过程,学习了如何通过objdump查看elf信息,掌握了重复定位的过程,比较了.s文件的反汇编代码与hello.s的区别。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接的主要内容就是将各个模块之间相互引用的部分正确的衔接起来。它的工作就是把一些指令对其他符号地址的引用加以修正。链接过程主要包括了地址和空间分配、符号决议和重定向
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
5.4 hello的虚拟地址空间
分析5.3中的节头部表可知,hello的所有节均保存在0x400270至0x404058内存空间中。
打开edb,查看对应内存地址
0x
0x400270处的.interp节存着动态链接的路径名
.rodate节
.text节
5.5 链接的重定位过程分析
分析.o文件的反汇编代码与hello文件的反汇编代码,可以看出,.o文件的反汇编代码的高亮部分需要重定位
而在hello文件的反汇编代码中,库函数已经完成了重定位
5.6 hello的执行流程
3._libc_start-main()
main()
5.7 Hello的动态链接分析
5.8 本章小结
在本章中主要学习了hello经过链接最终生成可执行文件的过程,了解了可执行文件的elf的格式,分析了hello的虚拟内存空间。收获匪浅
(以下格式自行编排,编辑时删除)
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是并发程序的动态运行,是多道程序系统中程序的动态运行过程。
进程是一个活动的实体,除了指令代码,进程通常还包括进程堆段、栈段(包含临时数据,如方法参数、返回地址和局部变量)和数据段(包含常量或全局变量等)。
进程是程序在数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。
进程使得每个程序独立运作,使得程序看起来独占cpu.
6.2 简述壳Shell-bash的作用与处理流程
作用:为操作者提供操作界面,接受并解析外部命令,接受到用户的命令后,调运相应的应用程序
处理流程:(1)读取命令:从界面上读取用户键入的命令()
(2)解析命令:对获取的命令进行解析,得到命令名和参数
(3)寻找命令文件:寻找可执行目标文件
(4)执行命令:通过fork系统调用创建一个进程来完成执行命令的任务。
6.3 Hello的fork进程创建过程
Shells首先判断hello不是一个内置命令,之后shell会调用系统函数fork为hello创建一个新的子进程,新创建的子进程与父进程几乎相同但有所区别。最大的区别在于二者有不同的PID.
子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得父进程打开任何文件描述符相同的副本,这意味着当父进程调用fork函数时,子进程可以读写父进程中任何打开的文件。
6.4 Hello的execve过程
fork子进程之后, execve( )执行指定程序。
在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下的原型:
int main(int argc , char **argv , char *envp);
6.5 Hello的进程执行
操作系统使用上下文切换的异常控制流来实现多任务进程运行。所谓上下文就是内核重新启动一个进程所需的状态,包括寄存器信息,栈和各种内核数据结构。在程序执行的某些时刻,内核可以结束当前进程,并重启之前的进程,这就被称为调度。
下面分析hello的进程调度
在调用sleep函数之前,如果hello不被抢占,则hello顺序执行。Hello初始运行在用户模式,sleep后进入内核模式,内核处理休眠请求主动释放进程。计时器开始计时,将当前的进程控制权交给其他进程,当停止时间结束后,hello从等待队列中加入到运行队列,成为就绪状态,hello进程就可以继续执行自己的控制流了。
6.6 hello的异常与信号处理
在程序执行过程中,如果乱按键盘,会在shell上显示相应的字符,如果输入的字符中含有回车键,则程序结束后无需等待getchar(),getchar函数直接读取了随机乱输入的字符并自动调用。
在程序执行过程中,如果Ctrl-Z,hello会被挂起但并未结束进程,通过ps可以看到,shell仍然保持hello的进程
若输入fg,则hello继续运行
在程序执行过程中,如果Ctrl-C,则程序中止,进程被回收
6.7本章小结
在本章中,学习了解了进程的定义与作用,介绍了Shell的运作流程,即调用fork创建子进程,调用execve执行hello.hello执行过程中,了解了几类异常以及异常信号及处理方法。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:出现在hell.o的地址即为逻辑地址。逻辑地址由段标识符加上偏移组成。
线性地址:逻辑地址经过段机制后转化为线性地址。Hello中的虚拟内存地址即为线性地址
虚拟地址:实际上指的就是线性地址,cpu通过生成一个虚拟地址来访问主存
物理地址:计算机系统贮存被组织成一个由M个连续字节大小的单元组成的数组,每字节都有一个唯一的地址,即物理地址。对于hello程序为当hello运行时通过MMU将虚拟内存地址映射到内存中的地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
如上所述,一个完整的逻辑地址由段标识符加上偏移组成,段标示符的具体结构如下
其中前13位为索引号,通过索引号可以从段描述符表中获取Base字段,该字段描述了一个段开始位置的线性地址,TI的1值决定了索引是全局段描述符表的索引还是局部段描述符表的索引。
这样,通过逻辑地址,就得到了线性地址的基址,加上偏移也就完成了变换
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元数组。VM系统通过将虚拟内存分割为称为虚拟页(VP)的大小固定的块来处理这个问题,物理内存被分割为物理页(PP),大小同VP。虚拟页的大小通常在4KB-2MB,为了管理这也虚拟页,有了页表的概念。页表就是一个页表条目的数组。每个页在页表的一个固定偏移量处都有一个PTE,PTE由一个有效位和n为地址字段组成。地址字段表示物理页的起始地址。页表存放在内存中,通过MMU,可以实现从虚拟地址翻译到物理地址的过程。虚拟地址由虚拟页号以及虚拟页偏移组成。通过虚拟页号找到相应的物理页号后,与虚拟页偏移合并即可得到对应的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,MMU就必须查阅一个PTE,以便将虚拟地址翻译为物理地址。在最糟糕的情况下,会要求从内存多取一次数据,代价是几十到几百个周期。许多系统试图消除这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓冲器(TLB)
VA到PA的变换过程:
1.CPU产生一个虚拟地址;
2.MMU从TLB中取出相应的PTE;
3.MMU将其翻译成一个物理地址并将其发送到高速缓存/主存;
4.高速缓存将主存所请求的数据返回给cpu;
当TLB不命中时,MMU必须从L1缓存中取出相应的PTE,以此类推.
7.5 三级Cache支持下的物理内存访问
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进进程的mm_struct、段结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个段结构都标记为私有的写时复制
7.7 hello进程execve时的内存映射
Execve需要执行的几个步骤:
1.删除已存在的用户区域:
2.删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域:
为新程序的代码、数据、bss 和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长度为零。
3.映射共享区域:
hello 程序与共享对象 libc.so 链接,libc.so 是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC):
execve 做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
当发生缺页异常时,控制权转交给内核执行相应的缺页处理程序。
该中断处理程序首先判断虚拟地址是否合法,如果不合法,访问了不存在的段或者试图修改只读段,则程序进程终止,否则会从外存中加载页面并跟新页表,再将程序返回。
7.9动态存储分配管理
基本方法:使用malloc获得虚拟内存后,该虚拟内存区域被称为堆。分配器将堆视为不同大小的blocks来维护,每个块要么是已分配的,要么是未分配的。
策略:
1.隐式空闲列表;通过头部中的大小字段隐含地连接空闲块
3.分离的空闲链表:每个大小类的空闲链表包含大小相等的块
4.按照尺寸排序的块:可以使用平衡树(例如红黑树),在每个空闲块中有指针,尺寸作为键。
7.10本章小结
本节阐述了hello在运行过程中的储存空间,引出了逻辑地址,线性地址,虚拟地址以及物理地址的概念,讲解了逻辑地址到线性地址再到物理地址的转换,了解了基于3级cache的内存访问以及动态内存分配的有关内容。并以此为基础,讲解了fork和exceve是的内存映射。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
所有的I/O设备都被模型化为文件,输入输出被当作相应的读和写来操作
设备管理:unix io接口,该接口能实现以下功能:打开和关闭文件open()和close();读出或写入文件read()和write();改变当前的文件位置lseek()
8.2 简述Unix
IO接口及其函数
Unix I/O 函数:
open()函数
功能描述:用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。
函数原型:int open(const char *pathname,int flags,int perms)
参数:pathname:被打开的文件名(可包括路径名如"dev/ttyS0")flags:文件打开方式,
返回值:成功:返回文件描述符;失败:返回-1
close()函数
功能描述:用于关闭一个被打开的的文件
所需头文件: #include
函数原型:int close(int fd)
参数:fd文件描述符
函数返回值:0成功,-1出错
read()函数
功能描述:
从文件读取数据。
所需头文件: #include
函数原型:ssize_t read(int fd, void *buf, size_t count);
参数:fd:将要读取数据的文件描述词。buf:指缓冲区,即读取的数据会被放到这个缓冲区中去。count: 表示调用一次read操作,应该读多少数量的字符。
返回值:返回值-1 表示一个错误,0 表示 EOF,否则返回值表示的是实际传送的字节数量。
write()函数
功能描述: write 函数从内存位置 buf 复制至多 n 个字节到描述符 fd 的当前文 件位置
所需头文件: #include
函数原型:ssize_t write(int fd, void *buf, size_t count);
返回值:写入文件的字节数(成功)或-1(出错)
lseek()函数
功能描述:
用于在指定的文件描述符中将将文件指针定位到相应位置。
所需头文件:#include
函数原型:off_t lseek(int fd, off_t offset,int whence);
参数:fd;文件描述符。offset:偏移量,每一个读写操作所需要移动的距离,单位是字节,可正可负
返回值:成功:返回当前位移;失败:返回-1
8.3 printf的实现分析
分析printf的函数体,其中arg被赋值为输入字符串的首地址,通过vsprintf获得了字符串长度i,最后由write系统函数输出
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar 函数内部调用了系统函数 read,通过系统调用 read 读取存储在键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar 进行封装,大体逻辑是读取字符串的第一个字符然后返回。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了liunxI/O口的管理方法,介绍了相关的I/O接口及其函数,最后,深入底层分析了getchar()和printf()的实现。
(第8章1分)
结论
hello的020的坎坷历程:
通过键盘I/O被以hello.c的形式得以保存;
经过预处理,编译,汇编,链接的过程最终艰辛成为了可执行目标文件hello
在shell上键入运行命令,系统为hello fork新的子进程,exceve加载运行main函数
在hello的运行过程中,不断的有内存访问,中断异常处理,cpu给hello分配时间片等等,终于使得hello通过I/O在屏幕上得以输出
Hello结束运行,shell回收进程,就这样,hello又消失系统进程中,不带走一片云彩
太难了,都说hello的一生是坎坷的是艰难的,可谁又知道这中间过程有多么繁复深奥呢,一个简简单单的hello,浓缩着太多现代计算器系统思维,无时无刻不展现着人类智慧的伟大,我知道,我今天看到的hello的一生终究也是被抽象包装过的,每一个电信号,每一个门电路,又有谁真正探寻过hello的奥秘呢,学海无涯,人类几千年的积淀一生又能品味多少呢。但终究要抱着可知的态度,在追寻真理的路上修行!
(结论0分,缺失 -1分,根据内容酌情加分)
附件
Hello.i hello.c经过预处理得到的文本文件
Hello.s hello.i经过编译得到的汇编语言文本文件
Hello.o hello.s经过汇编得到的可重定位的目标文件
Hello hello.o经过链接得到的可执行目标文件
Hello.txt hello.s的反汇编文本文件
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 兰德尔E,
布莱恩特.
深入理解计算机系统[M].
北京:机械工业出版社,
2016…
[2]计算机操作系统进程的概念https://www.kyjxy.com/zhuanyeke/jisuanji/69554.html
[3] MMU映射过程分析https://blog.csdn.net/qq_26093511/article/details/51228377
[4] [转]printf函数实现的深入刨析https://www.cnblogs.com/pianist/p/3315801.html
(参考文献0分,缺失 -1分)