哈工大 2021春 计算机系统 大作业程序人生

计算机系统

大作业

题 目 程序人生-Hello’s P2P

专 业 计算机

学 号 1190200828

班 级 1936601

学 生 赵英帅

指 导 教 师 刘宏伟

计算机科学与技术学院

2021年6月

摘 要

大作业名为程序人生。人生大家都懂,是从出生到死亡的过程。那么,程序的一生都会经历些什么呢?通过PPT中Hello的自白,我们可以略知一二。本文将较为完整详细地阐述hello程序的生命周期,探讨hello程序从hello.c经过预处理、编译、汇编、链接到可执行文件诞生的全过程。并且我们将分析我们的计算机系统是如何“见证”hello的成长、表演直到谢幕回收hello,即Bash与OS对hello进行的进程管理、存储管理和I/O管理。通过对hello的程序人生,我们对计算机系统将有更深的了解。

**关键词:**计算机系统,Linux,Hello程序,预处理,编译,汇编,链接,进程,存储,虚拟内存,I/O

**
**

目 录

第1章 概述 - 4 -

1.1 Hello简介 - 4 -

1.2 环境与工具 - 4 -

1.3 中间结果 - 5 -

1.4 本章小结 - 5 -

第2章 预处理 - 6 -

2.1 预处理的概念与作用 - 6 -

2.2在Ubuntu下预处理的命令 - 6 -

2.3 Hello的预处理结果解析 - 6 -

2.4 本章小结 - 7 -

第3章 编译 - 8 -

3.1 编译的概念与作用 - 8 -

3.2 在Ubuntu下编译的命令 - 8 -

3.3 Hello的编译结果解析 - 8 -

3.3.1 全局变量sleepsecs - 8 -

3.3.2 输出字符串 - 9 -

3.3.3 main函数参数argc、数组argv - 9 -

3.3.4 局部变量i - 10 -

3.3.5 main函数 - 10 -

3.3.6 关系操作 - 10 -

3.3.7 函数操作 - 10 -

3.3.8 其他操作 - 11 -

3.4 本章小结 - 11 -

第4章 汇编 - 12 -

4.1 汇编的概念与作用 - 12 -

4.2 在Ubuntu下汇编的命令 - 12 -

4.3 可重定位目标elf格式 - 12 -

4.4 Hello.o的结果解析 - 14 -

4.5 本章小结 - 15 -

第5章 链接 - 16 -

5.1 链接的概念与作用 - 16 -

5.2 在Ubuntu下链接的命令 - 16 -

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

5.4 hello的虚拟地址空间 - 18 -

5.5 链接的重定位过程分析 - 19 -

5.6 hello的执行流程 - 21 -

5.7 Hello的动态链接分析 - 21 -

5.8 本章小结 - 22 -

第6章 hello进程管理 - 23 -

6.1 进程的概念与作用 - 23 -

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

6.3 Hello的fork进程创建过程 - 23 -

6.4 Hello的execve过程 - 24 -

6.5 Hello的进程执行 - 24 -

6.6 hello的异常与信号处理 - 25 -

6.7本章小结 - 28 -

第7章 hello的存储管理 - 29 -

7.1 hello的存储器地址空间 - 29 -

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

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

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

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

7.6 hello进程fork时的内存映射 - 32 -

7.7 hello进程execve时的内存映射 - 32 -

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

7.9动态存储分配管理 - 33 -

7.10本章小结 - 36 -

第8章 hello的IO管理 - 37 -

8.1 Linux的IO设备管理方法 - 37 -

8.2 简述Unix IO接口及其函数 - 37 -

8.3 printf的实现分析 - 38 -

8.4 getchar的实现分析 - 40 -

8.5本章小结 - 40 -

结论 - 41 -

附件 - 42 -

参考文献 - 43 -

第1章 概述

1.1 Hello简介

hello是一个基础却最重要的程序,它的一生都在计算机系统的“照顾”下,因此从其运行的过程,我们可以学到计算机系统各个方面的知识。hello的运行过程大致分为两个阶段——P2P和020:

P2P:

From Program to
Process的缩写,即从程序到进程的转换过程。该过程中,hello.c文件首先经历的是来自cpp的预处理,cpp并不尝试着理解C语言语法,而是在编译之前给hello“换身衣服”,即实现文本替换功能(.i)。紧接着它将经历来自ccl的编译,它将hello改头换面,转换成了我们这学期刚认识的“新朋友”,汇编代码(.s),这已经很贴近计算机硬件了。然后as也来凑热闹,将刚刚得到的汇编代码翻译成一定格式的机器码(.o)。最后,经过ld的链接,最终形成了可执行文件(.out)。在在shell中键入启动命令后,shell为其fork产生一个子进程,然后hello便从程序变为了进程。

020:

From Zero to
Zero的缩写,即进程从初始到回收的过程。该过程中,shell为hello进程execve、mmap,进入程序入口后程序开始载入物理内存,然后进入
main函数执行目标代码,TLB、Cache努力地让hello快点登上舞台,CPU为运行的hello分配时间片执行逻辑控制流,I/O管理与信号处理软硬结合,在各种软硬件的支持下,hello开始了它的表演。hello的表演转瞬而逝,完美谢幕,shell父进程回收hello进程,内核删除相关数据结构。

1.2 环境与工具

硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上;

软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS
64位/优麒麟 64位;

开发工具:GDB/OBJDUMP;EDB;GCC,readelf,HexEdit等。

1.3 中间结果

文件名文件作用
hello.ihello.c经过预处理得到的文本文件
hello.shello.i编译生成的汇编文件
hello.ohello.s经过汇编生成的可重定位目标文件
hello.txthello.o反汇编后生成的汇编语言文本文件
hello.out链接后生成的可执行文件
asm.shello.o反汇编后生成的汇编语言文本文件

1.4 本章小结

本章对hello进行了总体的介绍,结合hello的自述,主要简述了hello的P2P和020过程、实验的环境和工具以及实验中生成了的文件及其作用。后文将依据本章作详细展开。

第2章 预处理

2.1 预处理的概念与作用

  1. 预处理的概念

    预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。对于C语言,由预处理器CPP对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位。

  2. 预处理的作用

    预处理中会展开以#起始的行,试图将它们解释为预处理指令,包括#if / #ifdef /
    #ifndef / #else / #elif /
    #endif(条件编译)、#define(宏定义)、#include(头文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。

2.2在Ubuntu下预处理的命令

预处理的命令为:cpp hello.c > hello.i,当然也可以是gcc -E hello.c -o
hello.i。这两个指令都可以得到预处理得到的文件,即hello.i。

由下截图可知,预处理成功,我们得到了hello.i文件。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1iupoXLS-1624591156865)(media/c4ebb97fc1e832d01f294f267dbc0530.png)]

2.3 Hello的预处理结果解析

打开hello.i文件,我们发现,该文件与hello.c相比,hello.i明显多了许多内容。结合预处理的概念及作用,不难得知,多出来的内容为预处理得到的头文件stdio.h、unistd.h、stdlib.h的具体内容。将文件翻到最后可以看到,hello.c中的C语句内容并未发生改变,进一步说明预处理并不对程序的源代码进行解析。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BdH5NJVP-1624591156867)(media/482cccaee3e7609d8fbdfae080c9c735.png)]

2.4 本章小结

本章对hello的P2P阶段的第一步,即来自cpp的预处理过程进行介绍,主要介绍了预处理的概念及作用、在Ubantu中对hello.c的预处理命令以及预处理的结果,了解到预处理是对头文件包含、宏定义等行进行解释,并不对程序的源代码进行解析。

第3章 编译

3.1 编译的概念与作用

  1. 编译的概念

    编译是指编译器ccl将文本文件hello.i
    翻译成文本文件hello.s的过程,翻译得到的hello.s中包含一个汇编语言程序。

  2. 编译的作用

    首先ccl将检查代码的正确性,即进行词法分析、语法分析、语义分析等,检查无误后,ccl将源代码翻译为汇编代码。

3.2 在Ubuntu下编译的命令

编译的命令为:gcc -S hello.c -o hello.s;

由下截图知,编译成功,我们得到了hello.s文件。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c4bVdUfp-1624591156869)(media/e4d5e69d9a419946ea7d1c709a933073.png)]

3.3 Hello的编译结果解析

3.3.1 全局变量sleepsecs

  1. 对于全局变量sleepsecs的定义的汇编代码如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QqLfRhuT-1624591156870)(media/8674fe0877735d563787f14c385fe0b9.png)]

  1. 分析

    首先,globl声明该变量为全局变量,align为其地址对齐方式,type声明该变量为数据类型而非函数类型,.rodata声明只读代码段。因为sleepsecs变量为int型,所以size为4;

    其次,对于后续对sleepsecs的定义中,.long为2表明该变量的值为2,其他声明作用同上;

    最后,读C语言代码可知,sleepsecs =
    2.5,但是在汇编代码中.long却是2,这是因为sleepsecs类型为int而非float,相当进行一个隐式类型转换操作,即int
    sleepsces = (int)2.5,于是编译后其值为2。

3.3.2 输出字符串

  1. 对于两个输出字符串的定义的汇编代码如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OCL0sJN9-1624591156871)(media/51d5588a4cf0314c9138707f10504d24.png)]

  1. 分析:

    根据后续代码结合源文件可知,这两个字符串将作为调用printf函数的参数,具体方法将在后续分析中给出。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IHI5lJQP-1624591156872)(media/300b955e4c5ffa87ad8bc6cfa05daa49.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cd7n70lF-1624591156872)(media/5dad954b83aa76944c1e96384b17ae9d.png)]

3.3.3 main函数参数argc、数组argv

  1. 处理argc、数组argv的汇编代码如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cJkVV28y-1624591156873)(media/c4ea6313165cf5951cfc354abd9e23d5.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kvq7804e-1624591156873)(media/d314f93e4bbb2222f7c80003957eac0c.png)]

  1. 分析:

    第29行和30行进行的是将参数argc与数组argv的地址压栈的过程,主要判断依据是第31行的操作,即将存在地址%rbp

    20处的值与3进行比较,由hello.c的代码可知,第31行实际上正是参数argc与3的比较;而在第41到50行进行的数组操作是,将数组argv中的argv[1]和argv[2]两个值作为调用printf函数输出字符串LC1的两个参数

3.3.4 局部变量i

  1. 处理i的汇编代码如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mIH4p0Hm-1624591156874)(media/acc1abfccbd59e99e89f0d9720ebe97a.png)]

  1. 分析:

    局部变量i被压入栈中,保存在地址%rbp-4中。局部变量作为循环变量,除赋初值为0外,还需要进行的算数操作是每次循环加1,即每次循环中的代码执行完毕,i++,相应的汇编代码出现在L4中:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kTn19uAy-1624591156874)(media/31d8f7d5ac9a7ef1e77d13ff8c3e0c5b.png)]

3.3.5 main函数

  1. 对于main函数的说明的汇编代码如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ro5gbaJK-1624591156874)(media/cee49dfcaf216f3d6ee63856dd5e1b13.png)]

  1. 分析:

    对于main函数,首先.globl声明main为全局变量,而type则声明main为函数类型而非数据类型,因此main为全局函数。main函数为主函数,是程序执行的起点。

3.3.6 关系操作

  1. hello.c中包括的关系操作有argc != 3和i <
    10,相应的源码及其翻译而成的汇编代码如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wjp4yufU-1624591156875)(media/146f9e06e951aa4922d4770e01fc7110.png)]
=>[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7gYYxwzh-1624591156875)(media/66df1470711f076f5d8b6a4b01988cb6.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pllTMzzL-1624591156876)(media/8ea832f2c7867c999d0b06dc939af2c4.png)]=>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zPpK7XDx-1624591156876)(media/faafec942e016315f819a229ef9d6c63.png)]

  1. 分析:

    关系操作argc !=
    3在汇编代码中被翻译为cmpl,而cmpl后将进行跳转je操作,即控制转移。当argc等于3时,不满足条件,则跳过if跳转至L2,执行L2中的操作即将i的初值设置为0,然后跳转L3执行if之后的for循环语句;否则将执行if中的语句。

    关系操作i <
    10在汇编代码中被翻译为cmpl和jle,在L3中判断循环条件,即i是否小于等于9,即i小于10。若满足循环条件,则跳转至L4继续进行循环,L4执行完最后一句时自动进入L3,再次判断循环条件,若满足循环条件则再次跳进L4,直到i
    < 10不成立即循环条件不满足时,循环结束。

3.3.7 函数操作

  1. hello.c中有关函数的操作有main函数的执行,调用printf函数、exit函数、sleep函数、getchar函数,以及main函数的返回return,相关汇编代码如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-55z4GV9z-1624591156876)(media/815ac5bc6d2a9f04fb83ff79ba5b43f2.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IEtIgoVv-1624591156877)(media/9a6bbc66fdd8b6c6d3072c98f32652e8.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zonNGyCn-1624591156877)(media/c60c3da79c8d20ae7b1c57d0c7b9b4d6.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e9grMHMz-1624591156877)(media/3924f79f039565c3f0fbf5fefe605f65.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-phVZ5oxP-1624591156878)(media/2e2c94abf7c31d6aac2c8e27af7da147.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O9XWehfG-1624591156878)(media/79d2b2d2ad860e406f800f5a91711127.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vWQvPkCt-1624591156879)(media/561a77bd763d8e79262f5ba8d70b2d4c.png)]

  1. 分析:

    首先,main函数是主函数,是程序执行的起点。

    main函数中,进行了两次输出函数printf的调用,但是汇编代码中却并不都是call的printf,其中第一次call的是puts,我认为原因在于该次调用输出函数并不需要传递参数,于是调用puts,将字符串输出即可。而第二次调用printf是在循环中,这次调用输出函数需要有参数,即argv[1]和argv[2],传参过程及汇编代码已经在3.3.3中展示,不再重复;

    将1作为参数调用exit函数,表示异常退出;

    将全局变量sleepsecs作为参数调用sleep函数,让进程休眠2秒;

    调用getchar函数,吸收缓存区的所有字符;

    return 0,即main函数的返回值为0。

3.3.8 其他操作

  1. 赋值操作:主要是对全局变量sleepsecs和局部变量i的赋值,翻译为汇编代码的赋值操作为mov,在3.3.1和3.3.4中已经展示,不再重复;

    【注】mov具体包括:movb一个字,movw两个字节,movl四个字节,movq八个字节,遍布整个汇编代码;

  2. 类型转换:在3.3.1中已经展示全局变量sleepsecs赋值时的隐式转换,不再重复;

  3. 算术操作:主要是for循环中i++操作,翻译为汇编代码为add,在3.3.4中已经展示,不再重复;

  4. 数组操作:主要是argv的数组操作,在3.3.3中已经展示,不再重复;

  5. 控制转移:在3.3.6中已经与关系操作一同展示,不再重复。

3.4 本章小结

本章主要介绍了编译的概念及作用,结合PPT中的提示,重点展示了在编译阶段中,编译器如何处理hello.c中的各种数据和操作,以及各类型数据和操作经过编译器翻译后对应的汇编代码。

第4章 汇编

4.1 汇编的概念与作用

  1. 汇编的概念

汇编语言是为特定计算机或计算机系列设计的一种面向机器的语言,由汇编执行指令和汇编伪指令组成。采用汇编语言编写程序虽不如高级程序设计语言简便、直观,但是汇编出的目标程序占用内存较少、运行效率较高,且能直接引用计算机的各种设备资源。它通常用于编写系统的核心部分程序,或编写需要耗费大量运行时间和实时性要求较高的程序段。

  1. 汇编的作用

汇编程序是指把汇编语言书写的程序翻译成与之等价的机器语言程序的翻译程序。汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序。

4.2 在Ubuntu下汇编的命令

预处理的命令为:as hello.s -o hello.o;

由下截图知,编译成功,我们得到了hello.o文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eMnGQpC0-1624591156879)(media/589bcf23f7c6c5bffeffe8d80bd1837e.png)]

4.3 可重定位目标elf格式

  1. readelf -h hello.o 查看ELF头ELF Header,结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kk5qPLL6-1624591156880)(media/c01877ab26f54675771226dfc854e94a.png)]

在ELF
Header中可以看到ELF类型、版本、大小端等信息。根据头文件的信息,可以知道该文件是可重定位目标文件,有14个节。

  1. readelf -S hello.o 查看节头表Section Headers,结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-We4UOmU3-1624591156881)(media/6ffb1579d2b8d1e3dde0d98cc2fd71ad.png)]

在Section
Headers中可以看到,目标文件中的每个节都有一个固定的条目体现在这个表中,指明了各个节的信息,包括名称、类型、起始地址和偏移量等。由于是可重定位目标文件,所以每个节都从0开始,用于重定位。同时从信息中可以观察到,代码段可执行,但是不可写;数据段和只读数据段都不可执行,只读数据段不可写等。

  1. readelf -r hello.o 查看重定位节.rela.text,结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tG10x8ey-1624591156881)(media/6f85b4287c03aec8aa67db9ec82063d0.png)]

重定位节中保存的即是可重定位文件必须包含的说明如何修改其节内容的信息。当汇编器生成一个目标模块时,它并不知道数据和代码最终存放在内存中的什么位置,也不知道这个模块引用的任何外部定义的函数和全局变量的位置。而通过重定位节中保存的修改信息,链接器就能知道在将目标文件链接成可执行文件时如何修改这些未知引用。

  1. readelf -s hello.o 查看符号表.symtab,结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZtYrEtCh-1624591156882)(media/cb189620be29b694e9522e3f2cce7cda.png)]

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

  1. Name:符号名称;

  2. Value:符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址;

  3. Size:目标的大小;

  4. Type:声明是数据类型还是函数类型;

  5. Bind:声明符号是局部的还是全局的。

4.4 Hello.o的结果解析

  1. objdump -d -r hello.o > hello.txt
    对hello.o进行反汇编,并将结果保存至hello.txt文件中;

  2. 分析:

由反汇编生成的hello.txt与hello.s的比较可知,hello.txt中的汇编代码部分与hello.s功能上基本没有区别,形式上略有区别,例如:

  1. .s文件中由对于输出字符串、全局变量的定义,而反汇编代码中没有,这就导致了访问只读字符串、全局变量时方法的改变;

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uk1JgTiI-1624591156883)(media/518a5626e455a3b3548ddd3e78714404.png)] VS
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3MQrWhUY-1624591156883)(media/30b2a6232e9ce271a757a141a21b80c5.png)]

  2. 在.s文件中的操作数为十进制,而在反汇编文件中为十六进制;

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p4UU4tzW-1624591156884)(media/f616c9d335d6d3946e684778e046bcfe.png)] VS
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7fq8Vhvq-1624591156884)(media/36bb69e951ca0fead8f53803c1c1792a.png)]

  3. .s文件中,调用转移函数时,控制转移到的地址是用L2等段名称表示,而反汇编文件中是用确定的地址;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VE8zps4C-1624591156885)(media/1e08eeb5c0bde86e4827c2631565f4b7.png)] VS
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SNV3Z959-1624591156885)(media/1c3322044dc5237dd2b2ed9b2035ae5d.png)]

  1. 调用函数时,.s文件中是调用函数名,而反汇编代码中是调用函数链接修改后的相对偏移地址;

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vErc9Gql-1624591156885)(media/5756bb90b88f5f06cf3b93aff0a50747.png)] VS
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vV4E8YZG-1624591156886)(media/83c0a4b11f4a48a91800c0d361bcd55a.png)]

相对于汇编代码形式上的些许差别,两文件最大的不同是反汇编代码中包括了机器语言代码。

机器语言程序的是二进制机器指令的集合,是纯粹的二进制数据表示的语言,是电脑可以真正识别的语言。机器指令由操作码和操作数构成,汇编语言是人们比较熟悉的词句直接表述CPU动作形成的语言,是最接近CPU运行原理的语言。每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言和二进制机器语言建立一一映射的关系,因此可以将汇编语言转化为机器语言。

4.5 本章小结

本章对hello.s进行了汇编,生成了hello.o可重定位目标文件,并且分析了可重定位文件的ELF头、节头表、可重定位节和符号表。本章还对hello.s进行了反汇编,比较了hello.s和hello.o反汇编代码的略微不同之处,介绍了机器语言的概念,并分析了从汇编语言到机器语言的一一映射关系。

第5章 链接

5.1 链接的概念与作用

  1. 链接的概念

链接是指在电子计算机程序的各模块之间传递参数和控制命令,并把它们组成一个可执行的整体的过程。

  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 -o hello.out

由下截图知,连接成功,生成了hello.out文件。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9tOGDDMG-1624591156886)(media/6a8d3e78968621881e4310f9b9eab67a.png)]

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

  1. 查看hello.out的ELF头:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dEqamn3G-1624591156887)(media/5d98f37c68562a799e5b5b55bf373100.png)]

从ELF头中可以知道,hello.out为可执行文件,并且包含27个节;

  1. 查看节头表:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OWuDfAOJ-1624591156887)(media/e8fabd4d45368783206bc03866068ff8.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lyne0WGJ-1624591156888)(media/e878b786a4012fa34f342d3651b1bda5.png)]

节头部表Section Headers对hello.out中所有的节信息进行了声明,包括大小
、类型、地址、偏移量等,其中地址是指程序被载入到虚拟地址的起始地址。根据 Section
Headers 中的信息我们就可以用HexEdit定位各个节所占的区间(起始位置,大小)。

  1. 查看链接后的重定位节:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E1PtDV52-1624591156889)(media/9bf636aea04351d67588c81871039bca.png)]

  1. 查看符号表:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eZyv8w9q-1624591156889)(media/0b341ccf978f7c0c9614b2ba880edb4b.png)]

5.4 hello的虚拟地址空间

通过edb查看 hello.out的虚拟地址空间各段的信息:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LOG5eXLp-1624591156890)(media/f17131a001e7a21248df7ff9498545b1.png)]

结合5.3中的节头表中各节的起始地址信息,可以通过edb的Memory
Reigions找到各个节段的信息,例如.rodata节,起始地址为0x402000,大小为0x3A,在EDB中查看如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P8lV5MGV-1624591156890)(media/bf8ec4bcb45f865573f4186108f7c2da.png)]

其他段的信息查看方式与.rodata节相同。

5.5 链接的重定位过程分析

  1. 用命令 objdump -d -r hello.out > asm.txt
    获得hello的反汇编代码并存入asm.txt文件中,由下截图知反汇编成功,生成了asm.txt文件。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xfSjAnrv-1624591156890)(media/73258818341aa1a325d9226dc5a1fa83.png)]

  1. 分析hello.out与hello.o的不同:

  2. hello.out反汇编的代码中多了很多的节以及其函数的汇编代码,如.init节、.plt节等,如下截图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mTSTqU6d-1624591156891)(media/b5e78b99952a70dd7d8815ee27834c8a.png)]

  1. hello.out反汇编的代码有确定的虚拟地址,也就是说已经完成了重定位,而hello.o反汇编代码中代码的虚拟地址均为0,未完成可重定位的过程。例如下面两个截图,第一行为hello.out反汇编的代码而第二行为hello.o反汇编代码中与第一行对应的的代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h5ivYucZ-1624591156892)(media/1104758b052b99ab76f628d6356ebf7f.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GvDfdu92-1624591156892)(media/8115a13f0e32b77f4db5c98248fde80c.png)]

  1. 链接与重定位过程:

  2. 符号解析:

    目标文件(.o)定义和引用了符号,每个符号对应着一个函数、一个全局变量、一个静态变量等。符号解析的作用就是给每个符号引用分配一个精确的符号定义。链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。对于那些引用定义在相同模块中的局部符号的引用,符号解析是非常简单明了的。

    但是,对全局符号的引用解析要棘手得多。当编译器遇到一个不是在当前模块中定义的符号(变量或函数名)时,会假设该符号是在其他某个模块中定义的,生成一个链接器符号表条目,并把它交给链接器处理。如果链接器在它的任何输入模块中都找不到这个被引用符号的定义,就输出一条(通常很难阅读的)错误信息并终止。

  3. 重定位:

  4. 重定位条目:

    当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置。它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在rel.text中。已初始化数据的重定位条目放在,rel.data中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vk1XWYZ6-1624591156893)(media/061db4fd07eeca2127e5dc012014578e.png)]

  1. 重定位节和符号定义:

    链接器将所有相同类型的节合并为同一类型的新的聚合节;

  2. 重定位节中的符号引用:

    链接器依赖于可重定位目标模块中的重定位条目,修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。

  3. 举例分析hello.out中的重定位过程:

puts():

  1. 重定位算法:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x2OuOrVv-1624591156893)(media/de36c033475d7e7404986483b92d758d.jpeg)]

  1. 由puts的重定位条目,可得:

    r.offset = 0x21;

    r.type = R_X86_64_PLT32;

    r.symble = puts;

    r.attend = -4;

    确定ADDR(s) = 0x401105;

  2. 由重定位算法,公式为:

    refaddr = ADDR(s) + r.offset = 0x401105 + 0x21 = 0x401126

    *refptr=(unsigned)(ADDR(r.symbol)+r.addend-refaddr)=(0x401080+(-4)-0x401126)
    = -0xaa = (unsigned)(0xff ff ff 56)

经验证,计算正确!相关截图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FLHKqUei-1624591156893)(media/ebfd747ea49acd0aa745b766b3f40b33.png)]

5.6 hello的执行流程

ld-linux-x86-64.so!_dl_start

ld-linux-x86-64.so!_dl_init

hello!_start

hello!__libc_csu_init

hello!_init

libc.so!_setjmp

hello!main

hello!puts@plt

ld-linux-x86-64.so!_dl_runtime_resolve_xsave

ld-linux-x86-64.so!_dl_fixup

ld-linux-x86-64.so!_dl_lookup_symbol_x

hello!exit@plt

libc.so!exit

hello!_fini

5.7 Hello的动态链接分析

  1. 动态链接简介

    动态链接,在可执行文件装载时或运行时,由操作系统的装载程序加载库。动态链接库中的函数在程序执行的时候才会确定地址,所以编译器无法确定其地址。为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。延迟绑定通过两个数据结构之间简洁但又有些复杂的交互来实现,即过程链接表(PLT)和全局偏移量表(GOT)。

PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。

GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。

  1. dl_init调用前后.got.plt的变化:

    由节头表可得.got.plt的地址为0x404000,大小为0x40:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jlONDXQb-1624591156894)(media/a9314c8778a39a81ca686a95b615b819.png)]

【注】蓝色选中区域即为.got.plt:

调用dl_init前:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z1IH7jDG-1624591156894)(media/60b0fdd7bc35239b26d29494e3a6e2b8.png)]

调用dl_init后:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AqO1xalA-1624591156895)(media/310f78a6d769b2415a777bdd8ffd10de.png)]

对比信息,可见动态链接器解析函数的地址加入了信息中。

5.8 本章小结

在本章中主要介绍了链接的概念与作用,并且详细阐述了hello.o链接成为一个可执行目标文件的过程,介绍了hello.o的ELF格式和某些节的含义,分析了hello.out的虚拟地址空间、重定位过程、执行流程、动态链接过程。经历了预处理、编译、汇编、链接,可执行的hello终于诞生了!虽然它在我这里叫做hello.out,但这对它的功能并没有什么影响。在以下的实验中,hello.out简称为hello。

第6章 hello进程管理

6.1 进程的概念与作用

  1. 进程的概念

进程是计算机科学中最深刻、最成功的概念之一,其经典定义为一个执行中程序的实例。

  1. 进程的作用

我们将关注它提供给应用程序两个关键抽象:一是一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器;二是一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。

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

shell本身是一个用C语言编写的程序,它是用户使用Linux的桥梁。Shell既是一种命令语言,又是一种程序设计语言。作为命令语言,它交互式地解释和执行用户输入的命令;作为程序设计语言,它定义了各种变量和参数,并提供了许多在高级语言中才具有的控制结构,包括循环和分支;

shell的处理流程:

  1. 从终端读入输入的命令;

  2. 将输入字符串切分获得所有的参数;

  3. 如果是内置命令则立即执行,否则调用相应的程序执行;

  4. shell 应该接受键盘输入信号,并对这些信号进行相应处理

6.3 Hello的fork进程创建过程

当输入的命令不是内置shell命令时,父进程调用fork()函数创建一个新的子进程,子进程得到与父进程完全相同(但是独立)的一个副本,包括代码段、段、数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和子进程最大的不同时他们的PID是不同的。

有关hello的fork进程创建过程,例如当我在终端运行hello,输入运行指令./hello x
y时,首先shell判断我输入的指令是否是shell内置命令,很显然不是,因此会调用fork为hello创建一个新的进程。

6.4 Hello的execve过程

成功创建hello子进程之后,将调用execve函数在当前子进程的上下文加载并运行一个新的程序即hello程序。

当执行成功时,execve()并不会返回,调用execve的程序的代码段,data段,bss段和stack都将被加载的新程序的内容重写.这种状况其实就是金蝉脱壳,能够使用该函数来作程序的自更新;当调用失败时,返回-1,errno也被当即更新。

6.5 Hello的进程执行

  1. 进程执行的相关概念

  2. 两个抽象:

一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器;一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用CPU内存。

  1. 逻辑并发流:

即使在系统中通常有许多其他程序在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称逻辑流。

  1. 并发与时间片:

一个逻辑流的执行在时间上与另一个流重叠,称为并发流;

多个流并发地执行的一般现象被称为并发;

一个进程执行它的控制流的一部分的每一时间段叫做时间片。

  1. 私有地址空间:

进程为每个程序都提供一种假象,好像它独占地使用系统地址空间。一般而言,和地址空间中某个地址相关联的那个内存字节是不能被其他进程读或者写的,在这个意义上,这个地址空间是私有的。

  1. 用户模式和内核模式

处理器提供了一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。处理器通常是用某个控制寄存器中的一个模式位(mode
bit)来提供这种功能的,该寄存器描述了进程当前享有的特权。

当设置了模式位时,进程就运行在内核模式中,一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置;没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。

运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。

  1. 上下文切换:

内核为每个进程维持一个上下文(context)。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。

在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换1)保存当前进程的上下文,2)恢复某个先前被抢占的进程被保存的上下文,3)将控制传递给这个新恢复的进程。

  1. hello进程的执行

进程调用execve函数之后,进程已经为hello程序分配了自己的私有地址空间。当自定义参数有两个,分别是我的姓名和学号时,一开始,hello运行在用户模式下,输出Hello
1190200828
赵英帅,然后hello调用sleep函数之后进程转变为内核模式,内核不会等待sleep函数调用结束,而是休眠并释放hello进程,并将hello进程从当前运行队列中移出加入待运行队列,并转换回用户模式。随后定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程。2秒后,休眠结束,定时器发送一个中断信号,进入内核模式处理中断,并将hello进程从待运行队列重新移入到运行队列,然后hello进程就又可以“登场表演”了。当然,再次输出Hello
1190200828
赵英帅后将重复以上过程,直到运行10次。而当自定义参数不为2时,则要简单的多,因为不会调用sleep也就没有模式转换,也就没有额外的上下文切换,hello表演转瞬即逝,直接退场。

6.6 hello的异常与信号处理

  1. 异常的分类

异常可以分为四类:中断、陷阱、故障和终止,下图为对这些类别异常的属性做了小结。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yNDuU44i-1624591156895)(media/77b8b62534babb014b4c612bad70d1be.jpeg)]

  1. 中断

    中断是异步发生的,是来自处理器外部的I/O设备信号的结果。硬件中断不是任何一条专门的指令造成的,从这个意义上来说它是异步的。处理器从系统总线读取异常信号,然后调用适当的中断处理程序。当处理程序返回时,它将控制返回下一条指令,程序继续执行,就像没有发生中断一样。

  2. 陷阱:

    陷阱是有意的异常,是执行一条指令的结果。像中断处理程序一样,陷阱处理程序将控制返回下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,称为系统调用。

    用户程序经常需要向内核请求服务,为了允许对这些内核服务的受控的访问,处理器提供了一条特殊的“syscall
    n”指令,当用户程序想要请求服务n时,可以执行这条指令。执行该指令会导致一个异常处理程序的陷阱,这个处理程序解析参数,并调用适当的内核程序。

  3. 故障:

    故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的abort例程,abort例程会终止引起故障的应用程序。

  4. 终止:

    终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM
    或者SRAM位被损坏时发生的奇偶错误。终止处理程序从不将控制返回给应用程序。

  5. hello会出现的异常及信号

  6. 在hello程序执行的过程中可能会出现外部I/O设备引起的异常,造成中断;

  7. hello执行sleep函数的时候会出现陷阱;

  8. 执行hello程序的时候,可能会发生缺页故障;

  9. hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误,引起终止。

  10. 常见信号:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TrJZf8LA-1624591156895)(media/8ac0289d4bdb73eb883375525cbec3d5.jpeg)]

  1. 程序运行过程的键盘输入

  2. 随便输入:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9MEyCiGS-1624591156896)(media/28921418fd2ddd2cd1e04c8fd004f519.png)]

可以发现,随便输入只是将屏幕的输入缓存,当打印结束执行getchar时,会读入一个回车结尾的字串作为一次输入,而其他字符串包括回车会在程序结束后当做
shell 命令行输入呈现。

  1. Ctrl-z后分别运行ps jobs pstree fg kill等命令:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kGiKagRf-1624591156896)(media/8aa0b4af68f166e9b730b875cc4af2a4.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PyT7f711-1624591156897)(media/49e1da59dd72c1debc455ff16f14d4d7.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mQfQcc0m-1624591156898)(media/1ca30a536f4fd78dcc66ae0a60ba50c5.png)]

Ctrl-z后运行ps可以看出,hello并未被回收,而是被挂在了后台,jobs显示hello进程已停止,由于pstree太长,此处知识截了一部分的图。运行fg命令,发现hello进程又一次来到了前台继续运行。再次Ctrl-z,然后运行命令kill
-9
3827,即向hello进程发送SIGKILL信号杀死程序,再向用fg将hello调回前台运行时发现任务已被杀死。

  1. Ctrl-C:

    输入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业,如下截图所示,用ps查看前台进程组发现没有hello进程信息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i1s8fWHN-1624591156898)(media/a508dadeff093ee25e7fc043568e8e8c.png)]

6.7本章小结

本章阐述进程的概念与作用,同时介绍了
shell对命令的一般处理流程和shell的作用,着重分析了调用 fork 创建新进程,调用
execve函数加载并执行hello,hello进程的执行,以及hello的异常与信号处理。

第7章 hello的存储管理

7.1 hello的存储器地址空间

  1. 逻辑地址:

    指由程式产生的和段相关的偏移地址部分。例如,在进行C语言指针编程中,能读取指针变量本身值(&操作),实际上这个值就是逻辑地址,他是相对于你当前进程数据段的地址,与绝对物理地址不相关。只有在Intel实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,Cpu不进行自动地址转换);逻辑也就是在Intel保护模式下程式执行代码段限长内的偏移地址(假定代码段、数据段如果完全相同)。

  2. 线性地址:

    是逻辑地址到物理地址变换之间的中间层。程式代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。

  3. 虚拟地址:

    指由程序产生的由段选择符和段内偏移地址组成的地址,实际上就是线性地址;

  4. 物理地址:

    用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应。可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址

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

一个逻辑地址由两部份组成,段标识符: 段内偏移量

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aqytV7a0-1624591156899)(media/d513e506ac08ee67a57dc2f605be34a0.png)]

段式管理的特点:

  1. 段式管理以段为单位分配内存,每段分配一个连续的内存区;

  2. 由于各段长度不等,所以这些存储区的大小不一;

  3. 同一进程包含的各段之间不要求连续;

  4. 段式管理的内存分配与释放在作业或进程的执行过程中动态进行。

逻辑地址转换为线性地址的一般步骤:

首先,给定一个完整的逻辑地址[段选择符:段内偏移地址];

  1. 看段选择符的T1 =
    0还是1,判断当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,求其地址和大小,得到一个数组;

  2. 拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,得到基地址base;

  3. base + 偏移量就是要转换的线性地址。

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

  1. 虚拟寻址形式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NpU0jjdO-1624591156900)(media/8c0c8ea87cd778cf9acfd241d2bb02ff.jpeg)]

  1. 虚拟页和物理页:

    虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每字节都有一个唯一的虚拟地址,作为到数组的索引。磁盘上数组的内容被缓存在主存中。和存储器层次结构中其他缓存一样,磁盘(较低层)上的数据被分割成块,这些块作为磁盘和主存(较高层)之间的传输单元。

    VM系统通过将虚拟内存分割为称为虚拟页(Virtual
    Page,VP)的大小固定的块来处理这个问题。每个虚拟页的大小为P=2p字节。类似地,物理内存被分割为物理页(Physical
    Page,PP),大小也为P字节(物理页也被称为页帧(page
    frame))。在任意时刻,虚拟页面的集合都分为三个不相交的子集:

  2. 未分配的:VM系统还未分配(或者创建)的页。未分配的块没有任何数据和它们相关联,因此也就不占用任何磁盘空间;

  3. 缓存的:当前已缓存在物理内存中的已分配页;

  4. 未缓存的:未缓存在物理内存中的已分配页。

  5. SRAM和DRAM:

    SRAM缓存表示位于CPU和主存之间的2高速缓存L1、L2和L3;DARRAM缓存表示虚拟内存系统的缓存,它在主存中缓存虚拟页。

    在存储层次结构中,DRAM缓存的位置对它的组织结构有很大的影响。DRAM比SRAM要慢大约10倍,而磁盘要比DRAM
    慢大约100
    000多倍。因此DRAM缓存中的不命中比起SRAM缓存中的不命中要昂贵得多,这是因为DRAM缓存不命中要由磁盘来服务,而SRAM缓存不命中通常是由基于DRAM的主存来服务的。因为大的不命中处罚和访问第一个字节的开销,虚拟页往往很大,通常是4KB~2MB。由于大的不命中处罚,DRAM缓存是全相联的,即任何虚拟页都可以放置在任何的物理页中。不命中时的替换策略也很重要,因为替换错了虚拟页的处罚也非常之高。

  6. 页表:

    页表是一个存放在物理内存中的数据结构,页表将虚拟页映射到物理页,配合硬件确定虚拟页在磁盘中的位置。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。操作系统负责维护页表的内容,以及在磁盘与DRAM之间来回传送页。

  7. 地址翻译:

    形式上来说,地址翻译是一个N元素的虚拟地址空间中的元素和一个M元素的物理地址空间中元素之间的映射。

    虚拟地址(VA)由VPN和VPO组成,其中VPO是虚拟页面偏移量,取值区间在0到2p-1,因此VPO为虚拟地址的低p位;VPN围殴虚拟页号,包含了TLB索引(TLBI)和TLB标记(TLBT);物理地址(PA)由PPN和PPO组成,其中PPN为物理页号,PPO为物理页面偏移量。物理地址可以被分为CO、CI、CT,即缓冲块内的字节偏移量、缓存索引和缓存标记。

    由虚拟地址VPN中的TLBI和TLBT结合TLB表可以知道该次访存命中还是缺页,若命中则由TLB表可得PPN,而PPO与VPO相等,而PPN与PPO合在一起就构成了物理地址。若缺页,则引发缺页故障,从磁盘中将待访问页拿出来,用优良的替换算法进行页替换。

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

  1. Core i7地址翻译的概况图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TH7mKjLq-1624591156900)(media/651c663aecf6434a9c078e6622793a9b.jpeg)]

  1. 由上图不难得知,该虚拟地址共48位,其中前36位为VPN,后12位为VPO,TLB为16组级联,共64个条目,所以TLBI应为4位,TLBT位36-4等于32位。由于是四级页表,VPN可以分为VPN1~4,对应一到四级页表。

  2. 地址翻译:

    基本与7.3.5中单页表中的地址翻译方式相同,此处不再重复。

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

  1. 分析:

    由7.4中的概况图,因为有64组,所以组索引应有6位;每行64B,所以块偏移为6位;所以标记位为52-6-6
    = 40位。

  2. 物理访存过程:

  3. 通过组索引选择相应的组;

  4. 检查高速缓存是否命中,即该组中是否某一行的标记位与物理地址的标记位相同且该行的有效位为1;

  5. 若命中,则通过块偏移找到返回的缓存字节;若未命中,则需要从存储层次结构中的下一层取出被请求的块,然后将新的块存储在组索引位所指示的组中的一个高速缓存行中。一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置;否则组内都是有效块,
    产生冲突,最好采用最近最少使用策略LFU进行替换。

7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了当前进程的mn_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

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

7.7 hello进程execve时的内存映射

execve 函数在当前进程中加载并运行包含在可执行目标文件hello.out中的程序,用hello
.out程序有效地替代了当前程序。加载并运行hello.out需要以下几个步骤:

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

  2. 映射私有区域,即为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello.out文件中的.text
    和.data 区。bss 区域是请求二进制零的,映射到匿名
    文件,其大小包含在hello.out中。栈和堆地址也是请求二进制零的,初始长度为零;

  3. 映射共享区域,即hello程序与共享对象标准C库 libc.so 链接,libc.so
    是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内;

  4. 设置程序计数器(PC),即设置当前进程上下文的程序计数器,使之指向代码区域的入口点。

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

  1. 某指令引用的某字并未缓存在DRAM 中;

  2. 地址翻译硬件从内存中读取该字,并且触发一个缺页异常;

  3. 缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,;

  4. 如果牺牲页已经被修改过,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改牺牲页的页表条目,反映出该页不再缓存在主存中这一事实;

  5. 内核从磁盘复制待缓存的页到内存中牺牲页的物理地址,更新PTE,随后返回;

  6. 重新启动导致缺页的命令,该命令会把导致缺页的虚拟地址重发送到地址翻译硬件;

  7. 当指令再次执行时,相应的物理页面已经驻留在内存中,页命中,因此指令可以没有故障的运行完成。

7.9动态存储分配管理

  1. 动态内存分配器的基本原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hlo5E8uJ-1624591156901)(media/c377ec21fdb980d57f2570f4d4bbbce1.jpeg)]动态内存分配器维护着一个进程的虚报内存区域,移为地(heap)。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk(读做“break”),它指向堆的顶部。

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

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

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

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

  1. 带边界标签的隐式空闲链表分配器

隐式空闲链表区别块的边界、已分配块和空闲块的方法如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xdInLrb9-1624591156901)(media/4e2c991c761f5a3ce6242e10d627b44f.jpeg)]

这种情况下,一个块是由一个字的头部、有效载荷,以及可能的填充组成。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。块的头最后一位指明这个块是已分配的还是空闲的。

头部后面是应用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。块的格式如图所示,空闲块通过头部块的大小字段隐含的连接着,所以我们称这种结构就隐式空闲链表。

  1. 放置已分配的块

当一个应用请求一个k字节的块时,分配器搜索空闲链表。查找一个足够大可以放置所请求的空闲块。分配器搜索方式的常见策略是首次适配、下一次适配和最佳适配。

  1. 分割空闲块

一旦分配器找到一个匹配的空闲块,就必须做一个另策决定,那就是分配这个块多少空间。分配器通常将空闲块分割为两部分。第一部分变为了已分配块,第二部分变为了空闲块。如图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ROCqwgTR-1624591156902)(media/d76de44948cd2b3038d346220b868188.jpeg)]

  1. 获取额外堆内存

如果分配器不能为请求块找到空闲块,一个选择是合并那些在物理内存上相邻的空闲块,如果这样还不能生成一个足够大的块,分配器会调用sbrk函数,向内核请求额外的内存。

  1. 合并空闲块

合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。对于四种情况分别进行空闲块合并,我们只需要通过改变头部的信息就能完成合并空闲块。Knuth提出了一种采用边界标记的技术快速完成空闲块的合并。如图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EU5StDV2-1624591156902)(media/886f30c2dc04b93916698856027e231c.jpeg)]

  1. 显式空间链表的基本原理

隐式空闲链表为我们提供了一种介绍一些基本分配器概念的简单方法。然而,因为块分配与堆块的总数呈线性关系,所以对于通用的分配器,隐式空闲链表是不适合的(尽管对于堆块数量预先就知道是很小的特殊的分配器来说它是可以的)。

一种更好的方法是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针,如图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XUzPDus0-1624591156903)(media/0d5972340b7bf7493cba8ee7ccc0b35c.jpeg)]

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

一种方法是用后进先出(LIFO)的顺序维护链表,将新释放的块放置在链表的开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块。在这种情况下,释放一个块可以在常数时间内完成。如果使用了边界标记,那么合并也可以在常数时间内完成。

另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序的首次适配比LIFO排序的首次适配有更高的内存利用率,接近最佳适配的利用率。

  1. 动态分区分配策略

  2. 最先匹配:分配n个字节,使用第一个可用空间比n大的空闲区。

原理:空闲分区列表按照地址顺序排序;分配过程中,搜索第一个适合的分区;释放分区时,检查是否可与临近的空闲分区合并;

优点:简单;在高地址空间有大块的空闲分区;

缺点:外部碎片;分配大块使较慢。

  1. 最佳匹配:分配n个字节时,查找并使用不小于n的最小空闲分区。

原理:空闲分区列表按照大小排序;分配时,查找一个合适的分区;释放时,查找并且合并临近的空闲分区;

优点:避免大的空闲分区被拆分;可减小外部碎片的大小;

缺点:外部碎片;释放分区较慢;容易产生很多无用的小碎片

  1. 最差匹配;分配n个字节时,查找并使用不小于n的最大空闲分区。

原理:空闲分区列表按照从大到小排序;分配时,选最大的分区;释放时,检查是否可与临近的空闲分区合并,进行可能的合并,并调整空闲分区列表顺序;

优点:中等大小的分配较多时,分配效果最好; 避免出现太多的小碎片;

缺点:释放分区较慢;外部碎片;容易破坏大的空闲分区,因此后续难以分到大的分区

7.10本章小结

本章主要介绍了hello.out的存储器的地址空间,介绍了四种地址空间的概念和地址的相互转换。同时介绍了hello.out的四级页表的虚拟地址空间到物理地址的转换、三级cashe的物理内存访问、进程fork、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等相关知识。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

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

8.2 简述Unix IO接口及其函数

Unix I/O接口的统一输入输出方式:

  1. 打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个I/O
    设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符;

  2. Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)
    、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件<unistd.h>
    定义了常量STDIN_FILENO、STOOUT_FILENO和STDERR _FILENO,
    它们可用来代替显式的描述符值;

  3. 改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,
    初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k;

  4. 读写文件:一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k
    开始,然后将k增加到k+n 。给定一个大小为m
    字节的文件,当k~m时执行读操作会触发一个称为end-of-file(EOF)
    的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号”
    。类似地,写操作就是从内存复制n>0
    个字节到一个文件,从当前文件位置k开始,然后更新k;

  5. 关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

Unix I/O接口函数:

  1. int open(char *filename, int flags, mode_t mode);

    函数功能:

    open函数将filename
    转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符;

    函数参数:

    filename:要打开的文件名;

    flags:指明进程将如何访问这个文件;

    mode:指定新文件的访问权限位;

  2. int close(int fd);

    函数功能:

    关闭一个打开的文件,返回操作结果;

函数参数:

fd:要关闭的文件的描述符;

  1. ssize_t read(int fd, void *buf, size_t n);

    函数功能:

    read 函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置
    buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量;

函数参数:

fd:要读的文件的描述符;

buf:将要读入到的内存位置;

n:复制字节上限;

  1. ssize_t wirte(int fd, const void *buf, size_t n);

    函数功能:

    从内存位置buf复制至多n个字节到描述符为fd的当前文件位置;

函数参数:

fd:要读的文件的描述符;

buf:将要读入到的内存位置;

n:复制字节上限;

8.3 printf的实现分析

  1. printf的函数体:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-baHeOfAb-1624591156903)(media/44d8c86b550e633eacb1e6b7885b4053.png)]

  1. printf函数的内容:

首先将第一个参数入栈,然后调用vsprint函数,该函数作用为格式化输出字符串,返回值为要打印的字符串长度i。再调用write函数,将i位字符串写入终端,将栈中参数放入寄存器。int
INT_VECTOR_SYS_CALLA代表通过系统调用syscall,该函数功能是不断打印出字符。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。于是字符串被打印到了屏幕上。

【补】vsprint函数:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xi9NBFWA-1624591156904)(media/9650c78a154fcc2334b50ff5eb9846eb.png)]

sys_call的实现:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J5uL5lMn-1624591156904)(media/9d1b1859ed232e957a9a0b97babcfa49.png)]

8.4 getchar的实现分析

  1. 异步异常-键盘中断的处理:

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

  2. getchar函数返回原理:

    当程序调用getchar()时,程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中,直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar()才开始调用read系统函数,从输入流中每次读入一个字符。getchar()函数的返回值是用户输入的第一个字符的ASCII码,如出错返回EOF。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,待后续getchar()调用读取。也就是说,后续的getchar()调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。

  3. getchar函数作用:

    从标准输入流只读取一个字符(包括空格、回车、tab),读到回车符(’\n’)时退出,键盘输入的字符都存到缓冲区内,一旦键入回车,getchar就进入缓冲区读取字符,一次只返回第一个字符作为getchar函数的值;如果有循环或足够多的getchar语句,就会依次读出缓冲区内的所有字符直到’\n’。

8.5本章小结

本章主要介绍了Linux的IO设备管理方法、Unix
IO接口及其函数,并且分析了printf函数和getchar函数的实现。

结论

首先程序员编写hello.c文件;下面分析hello.c在计算机中的历程:

  1. hello.c文件首先经历的是来自cpp的预处理,生成hello.i;

  2. hello.i将经历来自ccl的编译,生成hello.s;

  3. hello.s将经历as的翻译,生成一定格式的机器码文件hello.o;

  4. 经过ld的链接,形成了可执行文件hello.out;

  5. 在在shell中键入运行命令./hello.out 1190200828 赵英帅;

  6. 因为输入的不是shell的内置命令,所以为其fork产生一个子进程,然后hello便从程序变为了进程;

  7. 调用execve函数加载运行hello.out,映射虚拟内存,开始载入物理内存,进入main函数;

  8. 当hello程序执行printf函数时,会调用malloc向动态内存分配器申请堆中的内存;

  9. 打印完成后,hello调用sleep函数之后进程陷入内核模式,处理休眠请求主动释放当前进程,内核进行上下文切换将当前进程的控制权交给其他进程,2秒后休眠结束,内核执行上下文切换将控制传递给hello进程;

  10. 若无输入信号干扰,8、9将重复执行10次;若输入Ctrl
    Z,内核会发送SIGTSTP信号给hello进程,使之停止前台作业并挂起,后续执行fg将使之再次成为前台程序,继续执行;若输入Ctrl
    C,内核会发送SIGINT信号给进程并终止hello进程;输入为其他字符时,第一次回车后再输入的字符将在hello进程运行结束后被当作命令行输入进行解释;

  11. 当hello进程执行完成时,内核安排父进程回收hello进程,并将hello进程的退出状态传递给父进程。

hello程序短暂的一生就此结束啦!

完结撒花!

附件

文件名文件作用
hello.ihello.c经过预处理得到的文本文件
hello.shello.i编译生成的汇编文件
hello.ohello.s经过汇编生成的可重定位目标文件
hello.txthello.o反汇编后生成的汇编语言文本文件
hello.out链接后生成的可执行文件
asm.shello.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.
内核模式,处理休眠请求主动释放当前进程,内核进行上下文切换将当前进程的控制权交给其他进程,2秒后休眠结束,内核执行上下文切换将控制传递给hello进程;

  1. 若无输入信号干扰,8、9将重复执行10次;若输入Ctrl
    Z,内核会发送SIGTSTP信号给hello进程,使之停止前台作业并挂起,后续执行fg将使之再次成为前台程序,继续执行;若输入Ctrl
    C,内核会发送SIGINT信号给进程并终止hello进程;输入为其他字符时,第一次回车后再输入的字符将在hello进程运行结束后被当作命令行输入进行解释;

  2. 当hello进程执行完成时,内核安排父进程回收hello进程,并将hello进程的退出状态传递给父进程。

hello程序短暂的一生就此结束啦!

完结撒花!

附件

文件名文件作用
hello.ihello.c经过预处理得到的文本文件
hello.shello.i编译生成的汇编文件
hello.ohello.s经过汇编生成的可重定位目标文件
hello.txthello.o反汇编后生成的汇编语言文本文件
hello.out链接后生成的可执行文件
asm.shello.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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值