计算机系统大作业——程序人生

计算机系统

大作业

题     目  程序人生-Hello’s P2P   

专       业 人工智能(未来技术模块)

学     号    7203610518           

班     级     2036013              

学       生         李歆远        

指 导 教 师      刘宏伟              

计算机科学与技术学院

2021年5月

摘  要

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分

本论文通过hello.c程序,对程序从源代码到可执行文件,再到被载入内存执行的过程,使用计算机系统的相关知识概念,进行解释,并在Ubuntu虚拟机Linux系统下进行所有操作,运用Linux系统的工具,较为完整地介绍了hello这一较为简单的c程序的运行过程。本文通过对hello.c执行预处理、编译、汇编、链接等过程的实现来学习各个过程在Linux下实现机制及原因。同时通过探索hello在Shell中的动态链接、进程运行、内存管理、I/O管理等过程的来更好地理解Linux系统下的动态链接、存储层次结构、异常控制流、虚拟内存及I/O等相关内容。

关键词:编译;汇编;链接;进程;虚拟内存;shell;异常控制流;计算机系统

目  录

第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的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

P2P:From Program to Process

用高级语言编写得到.c文件,再经过编译器预处理,将头文件包含进文件中,并将宏替换,得到.i文件,进而对其编译得到.s汇编语言文件。此后通过汇编器将.s文件翻译成机器语言,将指令打包成为可重定位的.o目标文件,再通过链接器与库函数链接得到可执行文件hello,执行此文件hello,操作系统会为其fork产生子进程,再调用execve函数加载进程。至此,P2P结束。

020:From Zero-0 to Zero-0

刚开始程序不在内存空间中,即开始为0。当在shell中使用execve加载执行该程序时,操作系统为此程序分配虚拟内存,将程序加载到虚拟空间所映射的物理内存空间中,然后执行目标文件。代码完成后,父进程回收hello进程,操作系统会释放进程的虚拟空间和相关数据结构,又变为0。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

硬件环境:Intel Core i7-9750H CPU;2.60GHz;8GB RAM

软件环境:Windows10 64位;Ubuntu 16.04 LTS 64位

开发与调试工具: gcc;edb;gdb;objdump;readelf;codeblocks

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

hello.i :hello.c预处理之后文本文件

hello.s :hello.i编译后的汇编文件

hello.o :hello.s汇编之后的可重定位目标文件

hello :链接之后的可执行目标文件

1.4 本章小结

       本章简要介绍了hello的P2P和020的过程,并列出了大作业的软硬件环境,开发和调试工具,展示了操作过程中产生的中间结果文件。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:

预处理,又称预编译,对于C/C++来说,预处理指预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,生成.i文件的过程。在C语言和C/C++中常见的编译程序中,需要进行预处理的程序指令主要有:#define(宏定义),#include(源文件包含),#error(错误的指令)等。通过一个预处理命令使得一个源代码编译程序可以在不同的程序运行语言环境中被各种语言编译器方便的进行编译。

预处理的作用:

  1. 将#include所包含的头文件直接加入到文本文件中,同时对于一些宏定义也在预处理阶段进行宏替换。
  2. 处理所有预编译指令。
  3. 添加行号信息,文件名信息,便于调试
  4. 删除所有注释。
  5. 保留所有的#program编译指令。

总之是得到了一个.i文件,方便编译器进行下一步的编译

2.2在Ubuntu下预处理的命令

命令: gcc -E hello.c -o hello.i

图2.1 预处理命令

2.3 Hello的预处理结果解析

在本实验中给的hello.c文件中,只有三条文件包含类型的预处理指令。可以发现,原本仅仅只有十几行的源码hello.c已经被扩展到了数千行,hello.i文件开始依次是头文件stdio.h unistd.h stdlib.h的展开,描述的是运行库在计算机中的位置。预处理后生成了一个文本文件,为后面的编译等操作做好准备。

图2.2 hello.i 库文件                                   图2.3 hello.i中的源代码

2.4 本章小结

本章主要介绍了预处理的相关概念和作用,了解了其所做的具体处理和目的。同时将hello.c文件进行预处理生成hello.i文本文件,并对生成的hello.i文件进行分析,详细了解了预处理的内涵。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

  编译的概念:

编译是指利用编译器将文本文件hello.i翻译成文本文件hello.s,是将已经预处理好的C语言源代码翻译成汇编语言的过程。是编译程序产生目标程序的动作,把高级语言变成计算机可以识别的2进制语言。编译程序就是把一种语言书写的程序翻译成另一种语言(目标语言)的等家程序

编译的作用:

基本功能是把源程序(高级语言)翻译成目标程序(机器语言表示),把用高级程序设计语言书写的源程序,翻译成等价的计算机汇编或机器语言书写的目标程序。除了基本功能之外,编译程序还有语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用以及人际联系等重要功能。

注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序

3.2 在Ubuntu下编译的命令

命令: gcc -S hello.i -o hello.s

图3.1 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.3.1  数据

(1)字符串:第一个字符串可以发现字符串被编码成UTF-8格式,一个汉字在UTF-8编码中占三个字节,一个\代表一个字节,对应c语言程序中的: "用法: Hello 学号 姓名 秒数!\n"。第二个字符串对应"Hello %s %s\n"

图3.2 字符串的解析

(2)常量:

常量以立即数的形式出现,值保存在.text中,作为指令的一部分

      (3)变量:

初始化的全局变量储存在.data节,它的初始化不需要汇编语句,而是直接完成的。局部变量储存在寄存器或栈中。

程序中定义了局部变量int I,i的初始化

图3.3 i的初始化

循环迭代

图3.4  i的循环迭代

3.3.2  赋值

对变量的赋值

图3.5 变量赋值

3.3.3  类型转换

             argv[3]是字符串格式,使用了atoi函数,将其转换为int型

图3.6 类型转换

3.3.4     算术操作

             循环中对i进行了自加操作

图3.7 算术操作

3.3.5  关系操作

             判断arcg是否等于4

图3.8 关系操作(判断是否为7)

3.3.6  数组操作

main函数参数中有指针数组char argv[] argv[]地址放在了寄存器RSI中,使用时放在栈中,char占八个字节

图3.9 main函数参数存储

图3.10 argv[]数组实现的汇编代码

3.3.7  函数操作

main函数:传入参数argc和argv[],分别用寄存器%rdi和%rsi存储,设置%eax为0并且返回

图3.11 main函数

print函数:共有两处。

第一处 call puts时只传入了字符串参数首地址

图3.12 call puts函数

第二处 for循环中call print时传入了argv[1]和argc[2]的地址。

图3.13 printf函数

exit函数:传入的参数为1,再执行退出命令

图3.14 exit函数

Atoi函数:传入参数argv[3],返回值保存在%eax中

图3.15 atoi函数

Sleep函数:传入参数atoi(argv[3]),%eax为atoi函数的返回值,存入%edi中作为参数传入sleep函数

图3.16 sleep函数

Getchar函数

图3.17 getchar函数

此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析

3.4 本章小结

介绍了编译的概念和作用。对编译命令以及对编译结果中的各种数据类型和操作进行了分析和讲解。通过hello程序展示了c语言转换成汇编代码的过程。介绍了汇编代码如何实现变量、常量、传递参数以及分支和循环。编译程序所做的工作。并对编译的过程进行进一步的分析,加深了对c语言的数据与操作,对c语言翻译成汇编语言的逻辑有进一步的掌握。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

汇编:将汇编代码生成机器级代码的过程,具体指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程,将.s汇编程序翻译车工机器语言并将这些指令打包成可重定目标程序的格式存放在.o中。

作用:生成机器级代码便于机器直接执行该文件。

以下格式自行编排,编辑时删除

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

4.2 在Ubuntu下汇编的命令

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

图4.1 Ubuntu下汇编的命令

4.3 可重定位目标elf格式

       通过命令readelf -a hello.o  > ./elf.txt 获得hello.o 文件的elf格式

图4.2 生成为elf文件的命令

4.3.1  ELF文件头

ELF文件头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。

图4.3 ELF文件头内容

4.3.2节头部表:

节头部表中包含各个节的名称、大小、可以进行的操作(属性)、类型、位置、对齐信息等信息。由于目前是可重定位文件,所以目前虚拟地址信息都是零,在未来链接后将会变为具体的虚拟地址。

图4.4 节头部表内容

4.4.3 重定位节

这一部分在进行重定位时十分重要。这一部分有完整的计算公式,这一部分主要提供的信息为,重定位的类型包括重定位相对引用和重定位绝对引用、这个重定位区域在代码中的偏移量、重定位到的位置信息,拥有这些信息才可能在链接时完成重定位。

图4.5 重定位节

4.3.5:.symtab符号表:

存放程序中的定义和引用的函数和全局变量的信息。

图4.6 符号表

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

4.4 Hello.o的结果解析

       命令:objdump -d -r hello.o > Disas_hello.s

图4.7 hello.o结果解析

图4.8  反汇编文件内容

对比汇编后的代码发现

  1. 跳转语句操作已经完成了计算,在.o文件中,跳转的位置已经由符号变为了具体的数值。
  2. 代码左侧多了机器码,每一条汇编语句都用相应的机器码表示。
  3. 对函数的调用与重定位条目对应,在可重定位目标文件中call后面不再是函数的具体名称,而是一条重定位 重定位条目指引的信息。而在汇编文件中call后面直接加的是函数名。
  4. 对全局变量的引用与重定位条目相对应。由于全局变量在运行时的内存位置时未知的,所以同样需要生成一条重定位条目,在链接是计算运行时的内存地址,然后分配给每一条引用,保证每一条引用最终都能指向相同的位置
  5. 立即数变为16进制格式。在编译文件中立即数全部是用16进制格式表示的,因为16进制与2进制之间的转化比十进制更加方便,机器是基于二进制运行的,索引都转换成了16进制。

4.5 本章小结

本章说明了从hello.s转化成二进制可重定位目标文件hello.o的过程,以及作用和为后面链接做得重定位的准备。

通过将.s和.o的代码的对比,发现两者的不同在于对不确定地址的引用。.s文件直接用名称引用,而.o文件要用精确的地址进行引用,不确定的地址暂时填为0,并添加到重定位条目中,等待静态和动态链接器的调节。

(第41分)

5章 链接

5.1 链接的概念与作用

       链接的概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,链接执行符号解析、重定位过程。

链接的作用:把可重定位目标文件和命令行参数作为输入,生成可以正常工作的可执行文件。这令分离编译成为可能,节省了工作空间。

    1. 在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

使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件

图5.1 链接命令

    1. 可执行目标文件hello的格式
  1. 命令:readelf -a hello > elf_hello.txt

图5.2 可执行目标文件的格式查看

  1. 可执行文件的ELF头表,可以看出文件是可执行目标文件,并且有36个节。

图5.3 ELF头

  1. 查看目标文件的节头表:

图5.4 目标文件的节头部表

  1. 符号表

图5.5 符号表

  1. 重定位节

图5.6 重定位节

    分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

    1. hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

如下图所示可知hello的虚拟地址空间开始于0x401000

图5.7 虚拟地址空间信息

通过5.3中的节头表对应虚拟地址空间各段信息可以看出各节的的起始位置和大小

图5.8 各节的起始位置和大小

例如.init节起始于0x401000,大小为0x1b,在edb中对应如下:

图5.9 init节的起始位置与大小在edb中的对应

5.5 链接的重定位过程分析

以下格式自行编排,编辑时删除

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。

命令:objdump -d -r hello > hello_disass.s

图5.10 hello的反汇编代码

Hello.o的反汇编与hello的反汇编的区别

1.在hello中链接器加入了hello.c所用的库函数exit、printf、atoi、sleep、getchar,而hello.o的反汇编中代码只是.text段和main函数

2. hello.o的反汇编地址从0开始,hello的反汇编地址从0x400000开始

3. hello中没有hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目

链接过程:(1)链接器将所有类型相同的节合为同一类型的新的聚合节。例如,来自所有输入模块的.data节被全部合并成一个节,这个节成为输出的可执行目标文件的.data节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址了。

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

(3)当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。

结合hello.o的重定位项目,分析hello中对其怎么重定位的。

1.   重定位节和符号定义,连接器将所有相同类型的节合并为同一类型的新的节,然后将运行内存地址赋给这个新的节和输入模块定义的每个节以及输入模块定义的每个符号;

2.   重定位节中的符号引用,连接器修改代码节和数据节中对每个符号的引用,使之指向正确的运行地址。

5.6 hello的执行流程

以下格式自行编排,编辑时删除

程序的执行流程

<_init>                                    0x0000000000401000

<.plt>                                      0x0000000000401020

<puts@plt>                            0x0000000000401090

<printf@plt>                           0x00000000004010a0

<getchar@plt>                      0x00000000004010b0

<atoi@plt>                             0x00000000004010c0

<exit@plt>                             0x00000000004010d0

<sleep@plt>                            0x00000000004010e0

<_start>                                   0x00000000004010f0

<_dl_relocate_static_pie>        0x0000000000401120

<main>                                    0x0000000000401125

<__libc_csu_init>                   0x00000000004011c0

<__libc_csu_fini>                  0x0000000000401230

<_fini>                                    0x0000000000401238

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。

5.7 Hello的动态链接分析

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。

在hello的elf文件中可以查找到如下信息:

图5.11 GOT起始表位置

GOT起始表位置为0x40400

在edb中打开hello,查看0x40400处,即为执行dl_init前的内容,结果如下:

图5.12 edb中0x40400处

执行dl_init后,内容如下:

图5.13 执行di_init

对于变量而言,我们利用代码段和数据段的相对位置不变的原则计算正确地址。对于库函数而言,需要plt、got合作,plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次再调用plt时,指向的就是正确的内存地址。plt就能跳转到正确的区域。

5.8 本章小结

在本章中主要介绍了链接的概念与作用,并且详细阐述了hello.o是怎么链接成为一个可执行目标文件的过程,详细介绍了hello.o的ELF格式和各个节的含义,并且分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。

(第51分)

6章 hello进程管理

6.1 进程的概念与作用

进程是一个执行中的程序的实例,每一个进程都有自己的地址空间,一般包括内核空间和用户空间,内核区域存储内核代码和内核数据。用户空间主要分为文本区,数据区和堆栈区。文本区域存储处理器执行的代码和一些只读的常量数据;数据区域存储全局变量和静态局部变量;堆栈区域存储区着活动过程调用的指令和本地变量。

作用:进程机制为程序提供了一下两个错觉。

(1) 每个进程都认为自己独占CPU资源,通过上下文切换来实现

(2) 每个进程都认为自己独占内存空间,通过虚拟地址实现

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

Shell是一种交互型的应用级程序,它代表用户运行其他程序。Shell执行一系列的读\求值步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。

具体步骤:

1、对用户输入的命令行进行解析,按照分隔符进行读取。

2、设置传递给execuve的argv参数。并且判断第一个参数,如果是内部命令,那么调用对应的处理函数进行处理,如果不是认为是一个可执行目标文件,会在一个新的子进程中加载并运行这个文件。

3、如果最后一个参数是&,那么表示在后台执行该程序,shell不会等待其完成,否则在前台执行这个程序,shell会等待其完成。

4、完成以上步骤后,shell将会开始下一轮迭代。

6.3 Hello的fork进程创建过程

根据shell的处理流程,输入命令执行hello后,父进程如果判断不是内部指令,即会通过fork函数创建子进程。子进程与父进程近似,并得到一份与父进程用户级虚拟空间相同且独立的副本——包括数据段、代码、共享库、堆和用户栈。父进程打开的文件,子进程也可读写。二者之间最大的不同或许在于PID的不同。Fork函数只会被调用一次,但会返回两次,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。

6.4 Hello的execve过程

当父进程fork之后,子进程调用execve函数在当前进程的上下文中加载并运行一个新程序即hello程序,execve程序加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp,只有当出现错误时,例如找不到filename,execve才会返回到调用程序。

1、加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。

2、新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。

3、最后加载器设置PC指向_start地址,_start最终调用hello中的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存

6.5 Hello的进程执行

在进程调用execve函数之后,进程已经为hello程序分配了新的虚拟的地址空间,并且已经将hello的.text和.data节分配虚拟地址空间的代码区和数据区。最初hello运行在用户模式下,输出字符串,然后hello调用sleep函数之后进程陷入内核模式,内核不会选择什么都不做等待sleep函数调用结束,而是处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。

当hello调用getchar的时候,实际落脚到执行输入流是stdin的系统调用read,hello之前运行在用户模式,在进行read调用之后陷入内核,内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。此时进入内核模式,内核执行上下文切换,切换到其他进程。当完成键盘缓冲区到内存的数据传输时,引发一个中断信号,此时内核从其他进程进行上下文切换回hello进程。

6.6 hello的异常与信号处理

以下格式自行编排,编辑时删除

 hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

 程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

hello程序出现的异常可能有:

中断:在hello程序执行的过程中由于要进行外部设备的输入,可能会出现外部I/O设备引起的异常。

陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。

故障:在执行hello程序的时候,可能会发生缺页故障。

终止:终止时不可恢复的错误,在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。

Hello中可能产生多种信号,比如SIGINT、SIGKILL、SIGSEGV、SIGALRM和SIGCHLD等信号,后面会进行详细介绍。

对各种命令的运行结果

1、作为对比首先给出不进行任何输入的程序执行结果

图6.1 不给任何输入

2、输入ctrl+z

图6.2 输入ctrl+z

可以看到输入ctrl+z后程序运行停止,不再继续产生输出,并且给出了停止的提示信息。此时执行ps和jobs指令,都可以发现对应的进程和作业信息。使用kill指令发送9这个信号,也就是SIGKILL给hello所在的进程,再次查看对应进程和作业已经消失。

3、输入ctrl+c

图6.3 输入ctrl+C

输入ctrl+c后发现程序的执行也已经停止,但是与之前不同,在查看进程和作业信息时,没有对应的信息,所以ctrl+c指令已经将这一进程杀死,而不是暂停。

4、随意输入内容

图6.4 随意输入内容

终端将所有内容都识别为命令,同时也没有这样的命令,所以直接报错。

6.7本章小结

       本章介绍了进程的概念和作用,并对shell的概念和作用进行介绍,总结了fork函数如何创建子进程和execve函数的运行方式。并且以hello进程为例,介绍了进程的创建,运行,终止,和在运行过程中键盘的输入所带来的影响。

(第61分)

7章 hello的存储管理

7.1 hello的存储器地址空间

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。

  1. 逻辑地址:逻辑地址(Logical Address)是指由程序hello产生的与段相关的偏移地址部分。
  2. 线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑地址,或者说是段中的偏移地址,它加上相应段的基地址就生成了一个线性地址。
  3. 虚拟地址:有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。

4.   物理地址:物理地址是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;如果没有启用分页机制,那么hello的线性地址就直接成为物理地址了。

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

一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。

在段式管理系统中,整个进程的地址空间是二维的,即其逻辑地址由段号和段内地址两部分组成。为了完成进程逻辑地址到物理地址的映射,处理器会查找内存中的段表,由段号得到段的首地址,加上段内地址,得到实际的物理地址。

一个段描述符由8个字节组成,分成了GDT和LDT两类,段描述符描述了段的特征。一般系统只定义一个GDT。IA-32中引入了GDTR和LDTR两个寄存器,用来存放当前正在使用的GDT和LDT的首地址。在linux系统中,每个CPU对应一个GDT。一个GDT中有18个段描述和14个未使用的保留项。

段地址的转换过程

1.    IA-32首选确定要访问的段,然后决定使用的段寄存器。

2.    根据段选择符号的TI字段决定是访问GDT和LDT,他们的首地址则通过GTDR和LDTR来获得。

3.    将段选择符的Index字段的值*8,然后加上GDT或LDT的首地址,就能得到当前段描述符的地址。

4.    得到段描述符地址后,可以通过段描述符中BASE获得段的首地址。

5.    将逻辑地址中32位的偏移地址和段首地址相加就可以得到实际要访问的物理地址。

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

将各进程的虚拟空间划分成若干个长度相等的页,页式管理把内存空间按照页的大小划分成片或者页面,然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。虚拟内存系统中MMU负责地址翻译,MMU使用存放在物理内存中的被称为页表的数据结构将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。

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

进行地址转化时,CPU每产生一个虚拟地址,MMU就必须查阅一个PTE,这涉及到内存的读取操作,花费周期较长,但是如果要取出的PTE位于一级高速缓存中,花费周期就会变得很小,所以在MMU中建立一个关于PTE的缓存,这有助于地址转化速率的提高,这个小缓存被称为TLB。

TLB的结构如图所示,在原本的虚拟地址中,VPN部分被分为两部分,一部分是TLB组的组索引TLBI,用于定位VPN在TLB中的组号。一部分是标记用于在寻找到组后,寻找对应的行。VPO与原来一致,都是直接复制与找到的PPN合并,构建出新的物理地址。

多级页表的作用是节省内存,如果发现前一级页表中有的PTE是空的,那么也就说明后续所有页表对应的这一部分都是空的,不需要占用内存,因此多级页表可以极大地节约内存。

这里用一个36位的VPN为例,VPN被分为4个9位的片,每个片作用于一个页表的偏移量。VPN1中保存的是PTE在L1中的偏移量,在L1中定位到的PTE中保存的是其对应的L2部分的首地址,相应的VPN2就保存的在L2中的偏移量,以此类推,最终在L4中找到对应的PPN。在每一级寻找VPN时都可以采取TLB加速策略。

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

三级Cache的核心思想就是每次访问数据的时候都将一个数据块存放到更高一层的存储器中,根据计算的局部性,程序在后面的运行之中有很大的概率再次访问这些数据,高速缓存器就能够提高读取数据的速度。

每级Cache的物理访存大致过程如下:

(1) 组选择取出虚拟地址的组索引位,将二进制组索引转化为一个无符号整数,找到相应的组

(2) 行匹配把虚拟地址的标记为拿去和相应的组中所有行的标记位进行比较,当虚拟地址的标记位和高速缓存行的标记位匹配时,而且高速缓存行的有效位是1,则高速缓存命中。

(3) 字选择一旦高速缓存命中,我们就知道我们要找的字节在这个块的某个地方。因此块偏移位提供了第一个字节的偏移。把这个字节的内容取出返回给CPU即可

(4)不命中如果高速缓存不命中,那么需要从存储层次结构中的下一层取出被请求的块,然后将新的块存储在组索引位所指示的组中的一个高速缓存行中。一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块, 产生冲突,则采用最近最少使用策略LFU进行替换。

7.6 hello进程fork时的内存映射

内核将fork后两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时。写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

mm_struct(内存描述符):描述了一个进程的整个虚拟内存空间。

vm_area_struct(区域结构描述符):描述了进程的虚拟内存空间的一个区间。在用fork创建虚拟内存的时候,要经历以下步骤:

1.创建当前进程的mm_struct, vm_area_struct和页表的原样副本。

2.两个进程的每个页面都标记为只读页面。

3.    两个进程的每个vm_area_struct都标记为私有,这样就只能在写入时复制。

7.7 hello进程execve时的内存映射

      execve函数在当前进程中加载并运行包含在可执行文件hello中的程序

加载并运行hello的几个步骤:

1.   删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。

2.   创建新的区域结构,映射私有区域和共享区域,代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

3.   设置程序计数器PC,指向代码区域的入口点。而下一次调度这个进程时,他将从这个入口点开始执行。

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

缺页是发生在虚拟地址转化为物理地址时的故障,当CPU发出一个虚拟地址后,根据前文的翻译流程进行转化,如果在内存中的页表中没有找到对应的物理地址,此时便会出发缺页故障。

缺页处理程序从磁盘中加载合适的页面,然后将控制返回给引起故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中了,指令就可以没有故障地运行完成了。

整体的处理流程:

  1. 处理器生成一个虚拟地址,并将它传送给MMU
  2. MMU生成PTE地址,并从高速缓存/主存请求得到它
  3. 高速缓存/主存向MMU返回PTE
  4. PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
  5. 缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
  6. 缺页处理程序页面调入新的页面,并更新内存中的PTE

缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。

7.9动态存储分配管理

Printf会调用malloc,请简述动态内存管理的基本方法与策略。

7.9.1动态内存分配地基本原理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。

分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

分配器有两种基本风格,两种风格都要求应用显式地分配块,它们的不同之处在于由哪个实体来负责释放已分配的块。

显式分配器:要求应用显式地释放任何已分配的块。例如,c标准库提供一种叫做malloc程序包的显式分配器。c程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。c++中的new和delete操作符与c中的malloc和free相当。

隐式分配器:另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集,例如Lisp,ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。

7.9.2隐式空闲链表原理

首先介绍隐式空闲链表

一个块是由一个字的头部、有效载荷、可能的一些额外的填充,以及在块的结尾处的一个字的脚部组成的。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是0。因此,我们只需要内存大小的29个高位,释放剩余的3位来编码其他信息。在这种情况中,我们用其中的最低位(已分配位)来指明这个块是已分配的还是空闲的。

头部后面就是应用调用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。需要填充有很多原因。比如,填充可能是分配器策略的一部分,用来对付外部碎片。或者也需要用它来满足对齐要求。

我们称这种结构称为隐式空闲链表,是因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。注意:此时我们需要某种特殊标记的结束块,可以是一个设置了已分配位而大小为零的终止头部。

为了加快空闲块之间的合并速度,为每一个块加装一个脚部。在每个块的结尾添加一个脚部,那么分配器就可以通过检查它的脚部,判断前一个块的起始位置和状态,这个脚部总是在距离当前块开始位置一个字的距离。当需要进行空闲块的合并时,总共分为四种情况,对于每种情况分别进行不同的更新,即可完成在常数时间内的合并操作。

7.9.3显示空闲链表原理

隐式链表因为块分配与堆块的数量呈线性关系,所以对于通用的分配器,隐式链表是不合适的,将空闲块组织为某种形式的显示数据结构更好。

因为根据定义,程序不需要一个空闲块的主体,所以实现空闲链表数据结构的指针可以存放在这些空闲块的主体里面。

显式空闲链表结构将堆组织成一个双向空闲链表,在每个空闲块的主体中,都包含一个pred(前驱)和succ(后继)指针。

使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于空闲链表中块的排序策略。

一种方法是用后进先出(LIFO)的顺序维护链表,将新释放的块放置在链表的开始处。另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。

7.10本章小结

本章主要介绍了 hello 的存储器地址空间、 intel 的段式管理、 hello 的页式管理,在指定环境下介绍了 VA 到 PA 的变换、物理内存访问,还介绍 hello 进程 fork 时的内存映射、 execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

(第7 2分)

8章 hello的IO管理

8.1 Linux的IO设备管理方法

       一个Linux文件就是一个m字节的序列:B0,B1,B2……Bm

所有的 IO 设备(如网路、磁盘、终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都被当做相应文件的读和写来执行。

设备的模型化:文件

所有的IO设备都被模型化为文件,所有的输入和输出都被当做对相应文件的读和写来执行。

设备管理:unix io接口

这种将设备模型化为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O

8.2 简述Unix IO接口及其函数

Unix IO接口:

  1. 打开文件:内核返回一个非负整数的文件描述符,通过对此文件描述符对文件进行所有操作。
  2. Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(文件描述符0)、标准输出(描述符为1),标准出错(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,他们可用来代替显式的描述符值。
  3. 改变当前的文件位置,文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。
  4. 读写文件,读操作:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k
  5. 关闭文件:当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中

Unix IO函数:

  1. open()函数

功能描述:用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。

函数原型:int open(const char *pathname,int flags,int perms)

参数:pathname:被打开的文件名(可包括路径名如"dev/ttyS0")flags:文件打开方式。

返回值:成功:返回文件描述符;失败:返回-1

  1. close()函数

功能描述:用于关闭一个被打开的的文件

所需头文件: #include <unistd.h>

函数原型:int close(int fd)

参数:fd文件描述符

函数返回值:0成功,-1出错

  1. read()函数

功能描述: 从文件读取数据。

所需头文件: #include <unistd.h>

函数原型:ssize_t read(int fd, void *buf, size_t count);

参数:fd:将要读取数据的文件描述词。buf:指缓冲区,即读取的数据会被放到这个缓冲区中去。count: 表示调用一次read操作,应该读多少数量的字符。

返回值:返回所读取的字节数;0(读到EOF);-1(出错)。

  1. write()函数

功能描述: 向文件写入数据。

所需头文件: #include <unistd.h>

函数原型:ssize_t write(int fd, void *buf, size_t count);

返回值:写入文件的字节数(成功);-1(出错)

  1. lseek()函数

功能描述: 用于在指定的文件描述符中将将文件指针定位到相应位置。

所需头文件:#include <unistd.h>,#include <sys/types.h>

函数原型:off_t lseek(int fd, off_t offset,int whence);

参数:fd;文件描述符。offset:偏移量,每一个读写操作所需要移动的距离,单位是字节,可正可负(向前移,向后移)

返回值:成功:返回当前位移;失败:返回-1

8.3 printf的实现分析

以下格式自行编排,编辑时删除

  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;

}

printf函数主要调用了vsprintf和write函数。

  1. 首先介绍vsprintf(buf, fmt, arg)是什么函数。

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。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

  1. 下面分析write的作用

write:

mov eax, _NR_write

mov ebx, [esp + 4]

mov ecx, [esp + 8]

int INT_VECTOR_SYS_CALL

可以看到在write的汇编代码中进行了系统调用sys_call,下面分析syss_call函数的行为。

  1. sys_call

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

sys_call的功能为显示格式化了的字符串

ecx中是要打印出的元素个数

     ebx中的是要打印的buf字符数组中的第一个元素

   这个函数的功能就是不断的打印出字符,直到遇到:'\0'

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成 ASCII 码,保存到系统的键盘缓冲区之中.

getchar 函数落实到底层调用了系统函数 read,通过系统调用 read 读取存储在 键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar 进行封装, 大体逻辑是读取字符串的第一个字符然后返回。

8.5本章小结

本章介绍了Linux的IO设备管理方法,Unix IO接口及其函数,最后分析了两个常用函数:printf getchar的实现分析。

(第81分)

结论

hello所经历的过程

1.编写程序。

2.源文件的预处理:将hello.c中头文件的内容都插入到文本中,将所有的宏都解释成具体的值。

3.编译:编译器将hello.i转化成汇编语言的ascii码文件hello.s。

4.汇编:汇编器将hello.s转化成二进制的机器代码,生成hello.o(可重定位目标文件)。

5.链接:对hello.o中引用的外部函数,全局变量等进行符号解析,重定位并且生成hello的可执行目标文件。

6.运行:在命令行中输入hello 1182910227 liufengzhe

7.创建子进程:通过fork创建。

8.执行:通过execxe调用加载器,建立虚拟内存映射,设置当前进程的上下文中的程序计数器,使之指向程序入口处。

9.访存:CPU上的MMU根据页表将CPU生成的虚拟地址翻译成物理地址,并用物理地址在主存的高速缓存中找相应的数据。

10.动态内存申请:printf调用malloc进行动态内存分配,在堆中申请所需的内存。

11.接收信号:中途接收ctrl+z挂起,ctrl+c终止。

12.程序返回后,内核向父进程发送SIGCHLD信号,终止的hello被父进程回收,内核将其上下文删除。

感悟:

通过半学期的学习,我们对计算机各个基本概念都进行了深入的学习。从最底层的硬件设计,再到指令的运作,再到进程和存储的逻辑,每天都在使用的计算机却有如此复杂的体系结构,一个简单的hello程序却也需要如此复杂的一套运作流程,让人不禁感叹现代计算机工业的精密。通过此次大作业,让我对计算机系统的设计与实现做了一个系统且细致的总结,我深刻意识到了计算机系统的博大精深和我知识储备的缺乏,今后一定要更加努力学习计算机系统。

日后在写程序时,也要时刻牢记计算机的底层设计,提升代码的效率。在整体系统的构建上,也会受到计算机系统的启发,书中很多概念都会使日后的编码更得心应手。另外,尽管经过一学期的学习,恐怕还有诸多问题理解不够深入,在日后的学习中,也会不断学习实践,多阅读此书,做到常读常新。

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

附件

列出所有的中间产物的文件名,并予以说明起作用。

hello.c                       编写的c语言程序

hello.i                        hello.c预处理之后文本文件

hello.s                        hello.i编译后的汇编文件

hello.o                       hello.s汇编之后的可重定位目标文件

hello                          链接之后的可执行目标文件

elf.txt                         Hello.o的ELF格式文件

elf_hello.txt               Hello的ELF格式文件

hello_o_disass.s        Hello.o的反汇编代码

hello_disass.s            Hello的反汇编代码

(附件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分)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值