计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2022113389
班 级 2203102
学 生 马茂天
指 导 教 师 史先俊
计算机科学与技术学院
2023年4月
本论文跟踪并分析了一个hello.c源文件被创建、预处理、编译、汇编、链接为可执行程序,再到程序被执行而创建为进程、进程在操作系统上运行、最终被杀死的过程,研究了计算机为了编译并运行hello.c在操作系统、处理器、内存、I/O设备等层面进行交互的过程。
关键词:计算机系统;P2P;020;编译系统;异常控制流。
目 录
第1章 概述
1.1 Hello简介
-
-
- P2P (Program to Process,从程序到进程)
-
P2P指Hello.c从源程序到进程的过程。
Hello.c经过预处理器的编译预处理,得到预编译文件Hello.i;Hello.i又经过编译转换为目标文件Hello.o;Hello.o又经过汇编器翻译为机器语言命令汇编文件Hello.s;最后经过链接器链接得到可执行文件Hello;Hello经过运行产生进程。
图1-1,编译系统
020指可执行文件Hello产生的进程从进入内存到从内存被回收的过程。
子进程由父进程fork()产生成为父进程的副本,随着execve函数的执行,可执行目标文件hello被加载并运行,新程序由此开始;随着进程的进行,进程最终由于某种原因终止而变为僵尸进程,最后又父进程或者init进程回收,至此进程最终消失,即“从0到0”。
1.2 环境与工具
1.2.1 硬件环境
x64 CPU;1.60GHz;8G RAM;256GHD Disk。
1.2.2 软件环境
Windows10 64位。
1.2.3 开发工具
VM VirtualBox 6.1;Ubuntu 20.04 LTS 64位;
Visual Studio 2019 64位;CodeBlocks 17.12 64位;vi/vim/gedit+gcc。
1.2.4调试工具
gcc;gdb;edb;hexedit。
1.3 中间结果
hello 的C源文件 | |
hello.i | hello.c经过预处理后的预编译文本文件 |
hello1.i | hello.c经过加上-P选项的预处理屏蔽垃圾内容后的预编译文本文件 |
hello.s | hello.i经过编译得到的汇编语言文本文件 |
hello.o | hello.s经过汇编得到的机器语言二进制文件(可重定位目标文件) |
hello | hello.o经过链接得到的机器语言二进制文件(可执行目标文件) |
hello_objdump.txt | hello经过反汇编得到的机器语言与对应反汇编语言的文本文件 |
gdb.txt | gdb中用于在hello每个函数上加断点的辅助文件 |
breakpoints.txt | 记录gdb中所有断点 |
1.4 本章小结
hello的一生经历了P2P和020两个过程,预处其中包含了预处理、编译、汇编、链接等过程,在进程中含有很多抽象。让我们在接下来的几章中详细了解他们。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:预处理(Pre-treatment)是指在编译程序之前对源代码进行处理的阶段。预处理器是编译器的一部分,负责执行预处理阶段的工作。预处理器(Pre-processor) 根据预处理指令对源代码进行修改、替换和扩展,生成经过预处理的代码,并将其传递给编译器进行后续处理。
作用:预处理将以#开头的行解释为预处理指令,读取相应文件代题#开头行插入到程序文本,得到以.i为文件扩展名的预编译文件。其中ISO C/C++要求支持的包括#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令) 。
预处理阶段通常包括以下几个主要的步骤:
1.宏替换:预处理器根据宏定义将程序中的宏调用替换为相应的宏展开式。
2.文件包含:通过预处理指令 #include,预处理器可以将外部文件的内容插入到源文件中。
3.条件编译:通过使用预处理指令如 #ifdef、#ifndef、#if 等,预处理器可以根据条件选择是否编译特定的代码块。
4.符号常量定义:预处理器允许使用 #define 来定义符号常量,它们在编译过程中会被替换为指定的常量值。
5.注释移除:预处理器会移除掉源代码中的注释部分。
通过预处理器的工作,源代码在编译之前经过修改和扩展,生成经过预处理的代码以供后续的编译阶段使用。预处理提供了一些功能,如符号常量定义、宏展开、文件包含、条件编译等,可以增强代码的可读性、简洁性和灵活性。
2.2在Ubuntu下预处理的命令
对示例.c文件进行预处理:使用gcc -E hello.c -o hello.i
预处理结果表示,预处理后的文件是一个C语言文本文件,但内容量很大,有3062行,这是因为我们在hello.c开始引用了三个头文件stdio.h,unistd.h,和stdlib.h,预处理器将他们替换为系统文件中的内容,这样我们的程序中即使没有写系统文件的内容,仍然可以经由预处理过程得到这些函数,节省了源程序的篇幅。
2.4 本章小结
预处理是编译系统进行的第一步程序,他在编译器编译前进行操作:首先进行条件编译和错误指令;若无问题则将源文件进行修改,替换掉其中的目标头文件和定义的宏。
预处理提供了编译前处理的手段,和在程序中利用外部系统或其他头文件的办法。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译是将高级程序语言(如C、C++、Java等)编写的源代码转换为机器可执行的指令集的过程。编译器是执行编译过程的软件工具,将与预处理文件中的高级语言以文本格式转换为汇编语言,得到一个用汇编语言描述程序的文本文件。
3.2 在Ubuntu下编译的命令
使用命令 gcc -S hello.i -o hello.s 进行编译
图3-1,编译命令
3.3 Hello的编译结果解析
3.3.1 数据:
3.3.1.1 字符常量
图3-2,字符常量对应汇编语言
分别对应两个字符串
图3-3,字符常量在源程序中的位置
3.1.1.2 变量
(1)局部变量i。仅在第一次使用时赋初值,储存在栈区,当函数返回,栈被复原,局部变量在栈中所占空间也会被释放。
图3-4,局部变量对应汇编语言
图3-5,局部变量在源程序中的位置
- 函数参数。即形参,本质是局部变量,仅在所在函数中起作用,函数调用时开始存储在栈中,函数返回时被释放
图3-6,函数参数对应汇编语言
main将argc和argv作为局部变量存储在栈中(argc为main函数命令行参数个数,argv为存放参数字符串指针的数组)
3.3.1.3 表达式
C语言表达式有很多种:变量常量表达式,算数表达式,赋值表达式,逗号表达式,关系表达式,逻辑表达式,复合表达式。
其中变量常量表达式已经在上面讲述了变量和常量的补分,下面介绍五种表达式。
- 算数表达式
即类似a+b或者i++形式的用运算符连接起来的表达式。
Hello.c中i++的自增:
图3-7,自增1算术表达式对应汇编语言
- 赋值表达式
编译器使用MOV指令将一个值赋给另一个地址:
图3-8,赋值表达式对应汇编语言
实现给计数值i赋初值0。
- 关系表达式
编译器使用CMP对两个地址的值进行对比,然后根据比较结果使用jump指令跳转到相应地址。对应判断if中的argc != 4。
图3-9,argc参数个数关系表达式对应汇编语言
3.3.2赋值
对于常量、全局变量和静态变量,编译器在程序开始时就已经赋值;对于局部变量,程序只在初次使用时赋值。(见上文赋值表达式)
3.3.3 算术操作
(见3.3.1.3(1)算术表达式)
3.3.4关系操作
(见3.3.1.4(3)关系表达式)
3.3.5 控制转移
编译器使用jump指令进行跳转转移,一般为判断或循环进行分支操作时,由于不同的逻辑表达式结果导致程序执行不同的代码,编译器使用CMP指令更新条件码寄存器后,使用相应的jump指令跳转到存放对应代码的地址。
图3-10,hello.s中的控制转移
3.3.6 数组操作
argv为字符串指针数组,其中存储着命令行内输入的4个参数的字符串的指针。
我们寻找数组内元素的地址,由于argv被存储在-32(%rbp),而64位编译下指针大小为8个字节,于是每个参数字符串地址相差8个字节。编译器以数组起始-32(%rbp)为基地址,以偏移量为索引,寻找各个字符串指针的地址。即-32(%rbp)+8,-32(%rbp)+16,-32(%rbp)+24。
图3-11,hello.s中的数组操作对应汇编语言
3.3.7 函数操作
3.3.7.1 参数传递
编译器会将函数的参数储存在存储器中,参数个数不大于六个时,按照rdi, rsi, rdx, rcx, r8, r9的顺序存储,大于六个时,前六个用上述寄存器存放,其他在栈中存放。
同时,程序中多次调用函数,
如当参数不足的时候打印的提示信息字符串赋给rdi寄存器,调用puts函数输出:
图3-12,调用printf传递参数对应汇编语言
3.3.7.2 函数调用
函数使用机器指令call调用函数,在hello.o中使用函数名作为助记标志代替没有重定位而无法得知的函数地址。Call函数将返回地址写入栈中,同时为局部变量和函数参数建立栈帧,并跳转到函数首地址。
3.3.6.3 函数返回
程序使用汇编指令ret从调用的函数中返回,还原栈帧并跳转到栈中保存的返回地址。
图3-13,函数返回
3.4 本章小结
编译过程会将高级程序语言翻译成汇编语言,为下一步转变为机器语言做准备。
第4章 汇编
4.1 汇编的概念与作用
汇编,指汇编器(Assembler)把汇编语言翻译成机器语言的过程中,将汇编语言文本文件转变为二进制可重定位目标文件。可重定位文件可以经过重定位和链接与其他可重定位文件合并,创建可执行目标文件。
汇编过程的作用包括:
1.将高级语言转换为低级语言:汇编过程将 C 语言代码转换为汇编语言代码,以便与特定的机器架构对应。汇编语言使用助记符代替机器指令,更接近底层硬件。
2.生成目标文件:编译器将汇编语言代码转换为目标文件。目标文件包含了计算机可以直接执行的机器指令,但它还没有经过最终的链接过程。
3.转换和优化:在汇编过程中,编译器可以对代码进行优化,消除冗余操作、简化表达式、调整指令顺序等,以提高代码的性能和效率。
4.处理模块化和库链接:在汇编过程中,编译器将不同的源码文件编译成独立的目标文件,并在必要时处理模块化和库链接,以生成最终的可执行文件。
5.生成调试信息:汇编过程还可以生成调试信息,这些信息用于调试和跟踪程序的执行。调试信息包括符号表、行号信息和源代码映射等,使得在调试器中可以准确地定位代码的位置和变量的值。
总之,C 语言编译过程中的汇编过程将 C 代码转换为汇编语言代码,并进行优化和转换,以生成目标文件。这个阶段是将高级语言转化为低级语言,为最终的机器码生成和可执行文件生成做准备。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
图4-1,汇编命令
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
图4-2,可重定位布标文件ELF格式
4.3.1 ELF头
使用readelf -h hello.o 查看ELF头如图:
图4-3,hello.o的ELF文件头
其中:
第1行,ELF 头: 指名ELF文件头开始。
第2行,Magic用来指名该文件是一个 ELF 目标文件。第一个字节7f是个固定的数;后面的 3 个字节正是 E, L, F 三个字母的 ASCII 形式。
第3行,表示文件类型,ELF64指这个文件是64位的ELF格式。
第4行,表示文件中的数据是按照什么格式组织(大端或小端)的,不同处理器平台数据组织格式可能就不同,如x86平台为小端存储格式。
第5行,当前 ELF 文件头版本号,这里版本号为1 。
第6行,OS/ABI,指出操作系统类型,ABI是Application Binary Interface 的缩写。
第7行,ABI 版本号,当前为0 。
第8行,表示文件类型。ELF 文件有 3 种类型,一种是如上所示的Relocatable file 可重定位目标文件,一种是可执行文件(Executable),另外一种是共享库(Shared Library) 。
第9行,机器平台类型。
第10行,当前目标文件的版本号。
第11行,程序的虚拟地址入口点,因为这还不是可运行的程序,故而这里为零。
第12行,程序头起点位置,与 11 行同理,这个目标文件没有程序头。
第13行,节头开始处,这里 1240 是十进制。
第14行,是一个与处理器相关联的标志,x86 平台上该处为0。
第15行,ELF 文件头的字节数。
第16行,程序头大小,因为这个不是可执行程序,故此处大小为0。
第17行,程序头数量,同理于第 16 行。
第18行,节头的大小,这里每个节头大小为64个字节。
第19行,一共有多少个节头,这里是14个,与Section Headers中的数量一致。
第20行,节头字符串表索引号。
4.3.2 节头部表
使用 readelf -S(-W) hello.o,查看节头部表如图:
图4-4,hello.o的节头部表
rel.xxx是重定位条目相关的节,这些节不会出现在符号解析和重定位后的可执行目标文件中。
4.3.3 符号表
使用readelf -s hello.o查看符号表信息如图:
图4-5,hello.o的符号表节
符号表存放在程序中定义和引用的函数和全局变量的信息,表示了重定位的所有符号,用于之后的符号解析和重定位。
4.3.4 .rela.XXX
使用 readelf -r hello.o查看重定位信息
图4-6,hello.o的重定位信息
可以看到.rela.XXX中的重定位信息和.symtab中的信息对应,但.rela.XXX的来源不只有一个节(这里为.rela.text和.rela.eh_frame)
另外,使用readelf x.XXX hello.o 可以看到XXX节内的信息,但内容可读性不好,故使用readelf -r hello.o查看重定位信息。这里仅以查看rela.text的信息举例:
图4-7,hello.o的rela.text节信息
4.4 Hello.o的结果解析
objdump -d -r hello.o 进行反汇编如图
图4-8,hello.o反汇编信息
左侧为十六进制机器指令,右边是对应的汇编指令。与hello.o对比我们发现二者指令大致相同,区别如下:
- 伪指令:汇编代码中前后有着指导汇编器和连接器的伪指令(.file .rodata等),而反汇编代码只有开头文件格式说明
- 分支转移;反汇编代码没有助记符(如.L1)进行转移,而是使用机器指令je xx(地址) 的直接地址跳转。只有汇编代码能够用助记符帮助理解指令内容,而机器必须将其翻译为机器指令。
- 函数调用:反汇编代码进行函数调用时与汇编代码一样使用call,但在反汇编代码,地址显示的是下一行指令的地址,在下一行提示了我们在重定位信息中看到过的符号+加数信息,而汇编代码直接使用函数名称作为助记符。这是因为汇编代码没有进行重定位,函数调用时并不知道函数的位置,于是使用00 00 00 00代替,而e8后接的四个字节表示偏移地址,于是该条指令显示为跳转到下一条指令。
- 全局变量:反汇编代码利用间接寻址0x0(%rip)寻找全局变量的地址,而汇编代码直接引用助记符。我们的两个字符串提示符放在静态存储区,属于全局变量,需要进行符号解析和重定位,而反汇编代码不知道其位置所以暂时用偏移量0x0代替,%rip+0x0即是下一条指令的开头。
4.5 本章小结
汇编过程中汇编器将汇编代码翻译为机器指令,生成可重定位目标文件,但该文件仍然不能运行,其中可能有外部声明的变量或函数,需要与其他文件一起生成可执行的文件。可重定位目标文件按照ELF格式组织,方便我们后续与其他可重定位目标文件进行链接,生成可执行目标文件。
第5章 链接
5.1 链接的概念与作用
链接(linking)是链接器将一个或多个可重定位目标文件(包括特殊的共享目标文件)通过符号解析和重定位生成可执行目标文件的过程。链接包括静态链接和加载时共享库的动态链接。
注意:这儿的链接是指从 hello.o 到hello生成过程。
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 /usr/lib/x86_64-linux-gnu/crtn.o /usr/lib/x86_64-linux-gnu/libc.so hello.o
进行链接
图5-1,链接命令
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
图5-2,可执行目标文件ELF格式
5.3.1 ELF头
图5-3,hello的ELF头
与hello.o的ELF头对比可以发现,Type值变成了可执行文件,在hello.o中暂时被设置为0的条目被重新赋值,这也使各个起始点向后移动。
第11行,入口点地址被设置为0x4010f0
第12行,程序头起点,设置为64
第13行,节头开始处,设置为14208
第15行,ELF头文件字节数设置为64
第16行,程序头大小设置为56
第17行,程序头数量被改为12
第18行,节头的大小,这里每个节头大小为64个字
第19行,节头增加了13个
第20行,节头字符串表索引号,同样增加了13个
5.3.2 程序头表
使用readelf -l hello查看程序头表
图5-4,hello的程序头表
可执行文件的连续的片(chunk)被映射到连续的内存段(segment),程序头部表描述了这种映射关系:
(1)上边的程序头描述了段的目标文件中的偏移(Offset),内存地址(VA/PA),目标文件中的段大小(filesiz),内存中的段大小(memsiz),访问权限(Flags)(R:可读/W:可写/E:可执行),对齐要求(Align)。
(2)下边的段节标注了对应索引号的段所映射的节。
索引号为02的程序头LOAD,对应映射的节有:.interp , .note.gnu.property , .note.ABI.tag , .hash , .gnu.hash , .dynsym , .dynstr , .gnu.version_r , .rela.dyn , .rela.plt 。
其他各个段类似可以得到对应信息。
5.3.3 Symtab
使用readelf -s hello 查看符号表
图5-5,hello的符号表
出现XXX_GLIBC_2.2.5,说明在某个库中找到了这个符号的定义。
多出的节.dynsym是动态符号表,因为我们链接了共享库而出现。 .dynsym节保存了动态链接相关的符号。
5.3.4 重定位信息
使用readelf -r hello 查看重定位信息
图5-6,hello的重定位信息
同样多出一个节.rela.dyn,与动态链接有关,连接器在动态链接时在共享库中找到函数定义等待加载/运行时进行动态链接,其他符号在静态链接时完成重定位,不再存在于重定位信息中。
在静态链接中有专门用来重定位的重定位表.rela.text和.rela.plt,动态链接也有这类表,分别为.rela.data和.rela.plt。
5.3.5 节头部表
使用 readelf -S hello 查看节头部表
图5-7,hello的节头部表
相比hello.o的节头信息多了一些新增的与动态链接相关的节。
5.4 hello的虚拟地址空间
5.4.1虚拟内存地址中查看各段:通过edb加载hello,查看Data Dump中的虚拟地址信息,该进程虚拟地址从0x400000-0x401000
图5-8,虚拟地址空间
图5-9,ELF文件中的程序头
对应于5.3节中的inter段,地址由0x400200-0x40021c,记录了ELF解析器的位置位于/lib64/ld-linux-x86-64.so.2。
Load段,地址由0x400000-0x40076c,含ELF头,程序头表和.init .text .rodata节。
图5-10,load段信息
Note段,地址由0x40021c-0x40023c,记录输入的字符串信息
图5-10,note段信息
5.5 链接的重定位过程分析
(以下格式自行编排,编辑时删除)
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
objdump -d -r hello查看反汇编结果发现反汇编结果,它按节(如.init)组织函数(_init),并进行反汇编。
图5-10,note段信息
对比发现,在hello的反汇编代码与hello.o的反汇编代码有所不同,主要有两点:
- 函数调用:hello反汇编代码进行函数调用时直接使用虚拟内存地址,而hello.o反汇编代码由于没有重定位使用0代替。
图5-11,hello反汇编使用虚拟内存地址调用函数
图5-12,hello.o反汇编由于没有重定位使用0代替
- 全局变量:hello.o反汇编代码使用0x0(%rip)寻找全局变量地址,hello反汇编代码因为经过了重定位,(%rip)前是有意义的偏移量
图5-13,hello反汇编使用有意义的偏移寻址全局变量
图5-14,hello.o反汇编由于没有重定位使用0代替偏移进行寻址
- 虚拟地址:hello反汇编代码每行命令左侧有对应的虚拟内存地址,而hello.o为相对位置
图5-15,hello反汇编每行指令和函数有虚拟内存地址
图5-16,hello.o反汇编函数没有地址,指令行使用相对偏移量标记相对位置
5.6 hello的执行流程
加载程序 ld-2.27.so!_dl_start
ld-2.27.so!_dl_init
加载hello hello!_start
libc-2.27.so!__libc_start_main
-libc-2.27.so!__cxa_atexit
-libc-2.27.so!__libc_csu_init
Hello初始化 hello!_init
libc-2.27.so!_setjmp
-libc-2.27.so!_sigsetjmp
–libc-2.27.so!__sigjmp_save
调用main函数(运行) hello!main
调用打印函数 hello!puts@plt
调用退出函数 hello!exit@plt
ld-2.27.so!_dl_runtime_resolve_xsave
-ld-2.27.so!_dl_fixup
–ld-2.27.so!_dl_lookup_symbol_x
退出程序 libc-2.27.so!exit
5.7 Hello的动态链接分析
(以下格式自行编排,编辑时删除)
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
图5-17,dl_init函数地址
图5-18,dl_init函数执行过程
图5-19,dl_init函数执行之前的地址
图5-20,dl_init函数执行之后的地址
dl_init函数执行之后0x600a10处的global_offset表由全0状态被赋上相应偏移量的值
5.8 本章小结
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行,其在软件开发中扮演着重要角色,因为它使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。
第6章 hello进程管理
6.1 进程的概念与作用
进程是指在操作系统中正在运行的一个程序的实例。一个进程具有独立的内存空间、寄存器集合和上下文信息,可以执行特定的任务。进程是操作系统进行资源分配和调度的基本单位。
进程作用包括:
- 资源管理:操作系统通过进程来管理和分配计算机系统中的资源,如内存、CPU、文件和设备等。每个进程在运行时具有独立的资源空间,操作系统通过进程控制块(PCB)来维护进程的资源信息。
- 并发执行:操作系统可以同时运行多个进程,通过分时或并行的方式实现多个任务并发执行。进程切换和调度机制使不同进程可以在一段时间内共享处理器,并且给用户提供执行多个任务的体验。
- 进程间通信:进程之间可以通过进程间通信(IPC)机制进行数据传输和信息交换。常见的IPC方式包括管道、消息队列、共享内存等,这些机制使得进程能够相互协作、共享数据和完成复杂的任务。
4.保护与隔离:每个进程有自己独立的内存空间,使得进程之间的数据和代码可以被隔离和保护。这样可以提高程序的安全性和稳定性,一个进程的错误或崩溃不会影响其他进程的运行。
5.进程的创建和销毁:操作系统提供了创建和销毁进程的机制。通过进程的创建,可以产生新的进程以执行特定任务;通过进程的销毁,释放资源并终止进程的执行。
6.2 简述壳Shell-bash的作用与处理流程
壳(Shell)是操作系统提供的用户界面和命令解释器。它接收用户输入的命令,解析并执行这些命令,并提供相应的输出。其中,bash(Bourne Again Shell)是一个常见的UNIX和Linux操作系统使用的壳。
壳的作用包括:
提供用户界面:壳为用户提供了与操作系统交互的界面。用户可以通过壳输入命令、执行程序并获得相应的输出。
解释和执行命令:壳可以解析用户输入的命令,并根据命令的语法和语义执行相应的操作。它可以执行系统内置的命令、调用外部程序或脚本,并将命令的结果输出给用户。
脚本编程:壳提供了一种脚本编程语言,允许用户编写一系列命令的脚本文件来完成自动化任务。这些脚本可以包含流程控制、变量操作和调用系统命令等功能。
环境配置:壳可以通过设置环境变量、别名和配置文件等,自定义和配置用户的操作环境。用户可以根据自己的需求修改和定制壳的行为和设置。
壳的处理流程一般如下:
1.用户输入命令:用户在终端或命令行中输入命令。
2.壳解析命令:壳接收用户输入的命令,解析命令的语法和语义,确定执行的操作。
3.执行命令:壳根据命令的类型,执行内置命令、调用外部程序或脚本,并传递相应的参数和选项。
4.输出结果:壳获得命令执行的结果,并将结果输出给用户,通常在终端或命令行中显示。
5.等待下一个命令:壳等待用户输入下一个命令,重复上述步骤。
6.3 Hello的fork进程创建过程
新创建的子进程几乎但不完全与父进程相同,子进程会获得与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆共享库以及用户栈。
在父进程创建新的子进程时,子进程还会获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork函数时,子进程可以读写父进程中打开的任何文件。父进程和子进程之间的最大区别在于它们有不同的PID。
Fork创建过程如下:
1.原始进程(父进程)调用fork()系统调用。
2.操作系统内核接收到fork()系统调用请求后,复制原始进程的所有信息,包括代码、数据、堆栈、打开的文件描述符等,并为子进程分配新的进程ID。
3.在子进程中,fork()函数返回值为0,表示这是子进程。在父进程中,fork()函数返回值为新创建的子进程的进程ID。
4.子进程和父进程都从fork()函数返回后,继续执行下面的代码。它们从fork()之后的那一行开始执行。
5.父进程和子进程是独立执行的,它们拥有各自的内存空间和寄存器值。父进程可以通过fork()函数的返回值获得子进程的进程ID,从而对子进程进行控制。
6.Hello程序的运行结果将在父进程和子进程中分别输出。
图6-1,fork过程示例
6.4 Hello的execve过程
execve()是一个系统调用,用于执行一个新的程序。在Hello的fork进程创建过程中,父进程可以通过execve()系统调用来加载并执行一个新的程序。以下是Hello的execve过程:
1.父进程调用execve()系统调用,提供要执行的新程序的路径名和参数列表。
2.操作系统内核接收到execve()系统调用后,会启动加载并执行新程序的过程。
3.内核根据新程序的路径名找到可执行文件,并为新程序分配内存空间。
4.内核将新程序的代码、数据和资源加载到分配的内存空间,并设置相应的执行环境。
5.内核根据参数列表,将参数传递给新程序。
6.新程序从加载的内存空间的入口点开始执行,覆盖了原始进程的代码和数据。
7.Hello程序的运行结果将作为新程序的输出。
6.5 Hello的进程执行
在执行Hello程序时,操作系统将分配一部分内存空间给Hello进程,并为它分配资源。Hello程序将被加载到分配的内存空间中,并按照代码的执行路径执行。执行过程中,程序可以访问和修改它的数据、调用系统提供的函数和服务。运行过程中,Hello进程可能会被操作系统的进程调度器暂停,在合适的时间间隔内与其他进程轮流分享CPU资源。当Hello进程完成任务或被操作系统终止时,它所占用的资源将被释放。
进程调度是操作系统的核心功能之一,它确定操作系统在给定时间点上在可用的处理器中运行哪个进程。调度器的任务是选择一个合适的进程,将其从等待状态转换为运行状态,并分配给可用的处理器来执行。调度器可以基于多种策略进行进程调度,例如先来先服务(FCFS)、最短作业优先(SJF)、时间片轮转(Round Robin)等。
在进程执行时,硬件和操作系统协同工作,形成用户态和核心态之间的转换机制。当进程处于用户态时,它只能访问受限的资源,无法执行特权操作,例如打开和关闭文件、网络通信等,而不会对系统核心造成损坏。当进程需要执行特权操作时,需进行一定的权限检查,并申请核心态权限。此时,操作系统会将进程的执行状态从用户态转移到核心态,并授予进程特权访问系统资源和执行特权操作的权限。当进程完成特权操作后,它将被切换回用户态,继续执行非特权操作。
除了进程间的调度和权限管理外,进程的执行还涉及进程间通信、共享内存、文件I/O等复杂的机制和操作。在这些操作中,操作系统扮演了重要的角色,通过调度和管理进程,使得多个进程可以协同工作、共享资源、完成复杂的任务。
6.6 hello的异常与信号处理
(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
在Hello程序运行过程中,可能会发生多种异常,例如访问非法内存地址、除以0等。这些异常会导致操作系统发送一个或多个信号给进程,以告知进程发生了异常事件。以下是Hello程序中可能发生的异常和相应的信号:
1.SIGSEGV信号:当程序试图访问无效内存时,操作系统会发送SIGSEGV信号给进程。
2.SIGFPE信号:当程序试图进行除以0等非法数学运算时,操作系统会发送SIGFPE信号给进程。
3.SIGILL信号:当程序试图执行非法指令或调用未定义的函数时,操作系统会发送SIGILL信号给进程。
4.SIGABRT信号:当程序因内部错误等原因需要中止时,程序可以发送SIGABRT信号给进程。
5.SIGTERM信号:当用户请求中止进程时,操作系统会发送SIGTERM信号给进程。
这些信号可以被进程捕获并进行处理。进程可以通过注册信号处理函数,在接收到信号时执行特定的操作,而不是直接中止。一些常用的信号处理函数包括signal()、sigaction()等。在Hello程序中,可以通过注册信号处理函数来处理不同的信号。
另外,在程序运行过程中,可能会受到用户的交互请求,例如键盘输入。当用户进行键盘操作时,操作系统会发送相应的信号给进程。以下是常见的键盘操作和相应的信号:
1.Ctrl-C:当用户按下Ctrl-C时,操作系统会发送SIGINT信号给进程,中断其运行。
2.Ctrl-Z:当用户按下Ctrl-Z时,操作系统会发送SIGTSTP信号给进程,并将其状态设置为后台中断状态。
3.回车:当用户按下回车键时,操作系统会发送一个或多个信号给进程,具体取决于程序的实现。
对于以上信号,用户可以通过一些命令来进行处理和控制。常见的命令包括:
1.ps:用于查看进程的运行状态和属性。
2.jobs:用于列出正在运行的后台进程。
3.pstree:用于以树状结构显示进程之间的关系。
4.fg:用于将后台进程转移到前台继续运行。
5.kill:用于发送信号给指定进程,达到中止或修改进程的目的。
下面给出一些截屏来说明异常与信号处理。
- 不停乱按:
并不会影响程序执行
- Ctrl-C
进程终止。由于我们从键盘输入Ctrl-C,bash向进程发送信号SIGINT,进程终止。输入jobs(查看当前进程)看不到我们的hello进程。
- Ctrl-Z
进程被暂停。由于我们从键盘输入Ctrl-Z,bash向进程发送信号SIGTSTP,进程停止。
使用ps t,可以看到终端进程的状况,我们的hello进程状态为T,表示已停止,还能看到进程的PID。
使用pstree可以查看所有进程树状图显示
其中可以找到我们的hello进程
使用fg %1再次激活hello进程,使其到前台进行
使用kill -9 1209737 向进程发送信号SIGKILL杀死进程hello
6.7本章小结
进程是执行中的程序的实例,系统通过使异常控制流发生突变对系统状态变化做出反应,异常控制流发生在计算机系统的各个层次,他们包括异常处理程序、上下文切换、信号和非本地跳转。
第7章 hello的存储管理
7.1 hello的存储器地址空间
(1)逻辑地址:是由程序产生的由段选择符和段内偏移地址组成的地址。
在hello中即为通过偏移相对寻址的例如前面的0x12(%rip)寻找静态提示字符串,又如利用指针进行寻址等。
(2)线性地址(虚拟地址):CPU在保护模式下,“段基址+段内偏移地址”叫做线性地址,注意,保护模式下段基址寄存器中存储的不是真正的段基值(和实模式的含义不一样),而是被称为“段选择子”的东西,通过段选择子在GDT(全局描述表)中找到真正的段基值。另外,如果CPU在保护模式下没有开启分页功能,则线性地址就被当做最终的物理地址来用,若开启了分页功能,则线性地址就叫虚拟地址(在没开启分页功能的情况下线性地址和虚拟地址就是一回事)。但是,如果开启分页功能,虚拟地址(或线性地址)还要通过页部件电路转换成最终的物理地址。
在hello,中即为虚拟内存映像中的虚拟地址。
(3)物理地址:物理地址就是内存单元的绝对地址。
即我们使用CPU外部地址总线上的寻址物理内存的地址信号。
7.2 Intel逻辑地址到线性地址的变换-段式管理
Linux为每个进程维护了一个单独的虚拟地址空间,包括那些熟悉的代码、数据、堆、共享库以及栈段。
我们使用逻辑地址,保存着段选择符和段内偏移地址,我们想要够访存到一个数据,就需要按照段和偏移找到其虚拟地址,然后如果需要再将虚拟地址转为物理地址,如果不需要则直接访问。
虚拟地址(VA) = 段基地址(BA) + 段内偏移量(S)
7.3 Hello的线性地址到物理地址的变换-页式管理
概念上而言,虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每字节都有一个唯一的虚拟地址,作为到数组的索引。磁盘上数组的内容被缓存在主存中。和存储器层次结构中其他缓存一样,磁盘(较低层)上的数据被分割成块,这些块作为磁盘和主存(较高层)之间的传输单元。VM 系统通过将虚拟内存分割为称为虚拟页(Virtual Page ,VP)的大小固定的块来处理这个问题。每个虚拟页的大小为P =2^p字节。类似地,物理内存被分割为物理页(PhysicaI Page,PP) ,大小也为P 字节(物理页也被称为页帧(page frame))。
在任意时刻,虚拟页面的集合都分为三个不相交的子集:
· 未分配的:VM 系统还未分配(或者创建)的页。未分配的块没有任何数据和它们相关联,因此也就不占用任何磁盘空间。
· 缓存的:当前已缓存在物理内存中的已分配页。
· 未缓存的:未缓存在物理内存中的已分配页。
如图7-4展示了一个页表的基本组织结构。页表就是一个页表条目(Page Table Entry,PTE) 的数组。虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个PTE。为了我们的目的,我们将假设每个PTE 是由一个有效位(valid bit)和一个n位地址字段组成的。有效位表明了该虚拟页当前是否被缓存在DRAM 中。如果设置了有效位,那么地址字段就表示DRAM 中相应的物理页的起始位置,这个物理页中缓存了该虚拟页。如果没有设置有效位, 那么一个空地址表示这个虚拟页还未被分配。否则,这个地址就指向该虚拟页在磁盘上的起始位置。
如图7-5展示了MMU如何利用页表来实现虚拟地址空间(VAS)到物理地址空间(PAS)的映射。CPU 中的一个控制寄存器,页表基址寄存器(Page Table Base Reglster,PTBR)指向当前页表。n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(Virtual Page Offset,VPO) 和一个(n-p)位的虚拟页号(Virtual Page Number,VPN)。MMU利用VPN来选择适当的PTE。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB(快速转换缓冲器)是一种缓存结构,用于加速虚拟地址到物理地址的转换过程。在Hello程序中,TLB可以缓存最近的虚拟地址到物理地址的映射关系,以便更快地进行地址变换。
在四级页表的支持下,虚拟地址被分为四个级别的索引,包括页目录表、页中间目录表、页目录和页表。每个级别的索引都会查找相应的页表项,直到找到最终的物理页帧号。
当地址转换过程中,TLB能够缓存最近的映射信息,极大地提高了地址转换的效率。如果TLB中没有找到对应的映射信息,那么就需要显式地访问页表来完成地址转换。
7.5 三级Cache支持下的物理内存访问
在Hello程序中,物理内存访问受到三级Cache的支持。Cache是一种高速缓存,位于CPU和主存之间,并存储最近使用的数据和指令,以提高程序的执行效率。三级Cache一般包括L1、L2和L3三级,具有不同的容量和速度。在物理内存访问过程中,CPU会先访问L1 Cache,如果没有命中,则会继续访问L2和L3 Cache,直到最后访问主存。
因为Cache的速度大大快于主存,所以三级Cache的支持可以使得程序的运行速度大大提高。
7.6 hello进程fork时的内存映射
虚拟内存和内存映射解释了fork函数如何为每个新进程提供私有的虚拟地址空间.
为新进程创建创建当前进程的的mm_struct, vm_area_struct和页表的原样副本,两个进程中的每个页面都标记为只读,两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW),在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存,随后的写操作通过写时复制机制创建新页面。
7.7 hello进程execve时的内存映射
execve()系统调用会将指定的可执行文件加载到当前进程的内存空间,并运行这个可执行文件。在这个过程中,原进程的内存空间会被释放,而指定的可执行文件所需的内存空间会被加载到新的地址空间中。
在Hello程序中,可执行文件被加载到程序的代码段和数据段中。程序的堆和栈也会根据可执行文件的需要进行重新分配和映射。
7.8 缺页故障与缺页中断处理
在Hello程序中,当程序访问未被分配的内存页时,会出现缺页故障。操作系统会检测到这个故障,并发送缺页中断(Page Fault)给进程,以标记访问的内存页不存在。
进程根据缺页中断所提供的信息进行缺页处理。处理方式通常包括:
1.分配新的物理页,将其映射到空闲的虚拟地址。
2.如果需要,将虚拟地址写回磁盘,释放占用的物理内存。
3.重新进行指令或数据的访问操作。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(如图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本章小结
Linux为每个进程维护一个单独但一致的虚拟地址空间,将进程的信息映射到虚拟空间上;虚拟内存地址被翻译为物理地址,又将虚拟内存映射到物理内存上。这样在主存与进程之间构成了一个抽象的层——虚拟内存。虚拟内存是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
在Linux中,IO设备的管理方法是通过将设备抽象为文件的方式进行管理。这种将设备模型化为文件的方法被称为"一切皆文件"的思想。每个设备都被表示为一个文件,应用程序可以通过标准的文件IO操作(如打开、读取、写入和关闭)来对设备进行操作。
设备的模型化使得应用程序可以使用统一的接口来访问不同类型的设备,而无需关心底层设备的具体细节。Linux将设备抽象为字符设备、块设备和网络设备等不同类型的文件,使用标准的文件IO接口对这些设备进行操作。
设备管理采用了Unix IO接口,通过系统调用提供了一组函数来进行设备的读取、写入和控制等操作。
8.2 简述Unix IO接口及其函数
Unix IO接口是一组系统调用函数,用于进行文件和设备的输入输出操作。这些函数提供了对文件描述符的操作,包括打开文件、读取文件内容、写入文件内容和关闭文件等功能。
常用的Unix IO函数包括:
open():打开一个文件,返回一个文件描述符。
read():从文件或设备中读取数据。
write():向文件或设备写入数据。
close():关闭文件。
lseek():设置文件指针的位置。
ioctl():对设备进行控制操作。
这些函数可以使用文件描述符来指定要操作的文件或设备。
8.3 printf的实现分析
8.3.1首先看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为字符指针,指向...中第一个参数,即我们要格式化输出的字符串
8.3.2再看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':
itoa(tmp, *((int*)p_next_arg));
strcpy(p, tmp);
p_next_arg += 4;
p += strlen(tmp);
break;
case 's':
break;
default:
break;
}
}
return (p - buf);
}
vsprintf接受确定输出格式的格式字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出。
8.3.3最后wirte函数将buf中i个元素写到终端。
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
在 write 函数中,将栈中参数放入寄存器,ecx 是要打印的元素个数,ebx 存放buf中的第一个元素地址,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,将要输出的信息的ASCII码从总线传送到显存中,显卡需要把这些图像用帧的形式推送到显示器上,需要调用字符显示驱动子程序,经过三个步骤:
(1)将ASCII码转换为二字节码存储在VRAM中
对要输出在屏幕上的所有字符,需要转换为二字节的特殊编码保存在VRAM中,其中1字节为ASCII码,1字节为字符的属性(颜色、亮度等)。转换后将新的编码储存在VRAM中,等待向屏幕输出。
(2)将VRAM中的二字节码转换为字形码存储在ROM中
字符发生器ROM将VRAM中的输出信息转换为可显示字符字形的点阵数据,并存储起来。
为了将汉字的字形显示输出,汉字信息处理系统还需要配有汉字字模库,也称字形库,它集中了全部汉字的字形信息。需要显示汉字时,根据汉字内码向字模库检索出该汉字的字形信息。
(3)显示控制过程(从VRAM输出到屏幕)
①根据当前被显示字符在屏幕上的位置为地址,到VRAM 中找出被显示字符的ASCII 码;
②再用字符ASCII 码和电子束所处的字符点阵行位置为地址,到ROM中读出该字符的点阵行数据;
③把字符点阵行数据送到移位寄存器,通过逐位移位操作,输出被显示内容的显示点控制信号,送CRT 栅极实现对屏幕像素的显示控制。
8.4 getchar的实现分析
getchar功能:程序调用getchar时,程序等待用户从键盘输入信息。在用户输入有效信息时,输入的字符被放入字符缓冲区,getchar不进行处理;当用户输入回车键时,getchar以字符为单位读取字符缓冲区,但不会读取回车键和文件结束符。
异步异常-键盘中断:getchar的从键盘输入的实现是异常中断后键盘中断的处理程序的结果。
当我们进行键盘输入时,我们从当前进程跳转到键盘中断处理子程序,接受按键扫描码。当键盘上的一个按键按下时,键盘会发送一个中断信号给CPU,与此同时,键盘会在指定端口(0x60) 输出一个数值,这个数值对应按键的扫描码(make code)叫通码,当按键弹起时,键盘又给端口输出一个数值,这个数值叫断码(break code),这样计算机知道我们何时按下何时按下、松开,是否一直按着按键。
中断处理子程序把键盘中断通码和断码数值转换为按键编码(对于字母键、数字键为ASCII码),缓存到键盘缓冲区,然后把控制器交换给原来任务(getchar),若没有遇到回车键,继续等待用户输入,重复上述过程,遇到回车键后getchar按字节读取键盘缓冲区内的内容,处理完毕后getchar返回,getchar进程结束。
8.5本章小结
应用程序现实中需要利用操作系统提供I/O函数来与外部设备传输数据,例如我们的输入输出,需要利用到一系列I/O函数,以及其他辅助函数来帮我们达到输入和输出的目的。
结论
hello的一生包括程序(Program)和进程(Progress)两个阶段:
- Program:hello在编译系统内经过五个阶段,将高级语言的程序逐步翻译为机器能读懂得机器语言:从hello.c被预处理到hello.i,hello.i被编译到hello.s,hello.s被汇编到hello.o,hello.o被链接到hello。
- Progress:可执行目标文件hello在服务于软硬件交互的操作系统上运行,操作系统对其程序抽象为进程,好像系统上只有这个程序在进行,系统利用异常控制流控制进程的运行,利用虚拟内存实现数据到物理内存的映射,提供接口实现与I/O设备以及其他程序通信,使程序在系统上能够自如地走完自己的一生。
附件
hello.c | hello 的C源文件 |
hello.i | hello.c经过预处理后的预编译文本文件 |
hello1.i | hello.c经过加上-P选项的预处理屏蔽垃圾内容后的预编译文本文件 |
hello.s | hello.i经过编译得到的汇编语言文本文件 |
hello.o | hello.s经过汇编得到的机器语言二进制文件(可重定位目标文件) |
hello | hello.o经过链接得到的机器语言二进制文件(可执行目标文件) |
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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分)