哈尔滨工业大学计算机系统大作业 程序人生-Hello‘s P2P

题     目  程序人生-Hellos P2P  

专       业       计算机类           

学     号      1190201104          

班   级        1903006           

学       生         叶珏相     

指 导 教 师         史先俊        

计算机科学与技术学院

2021年5月

摘  要

        本文主要阐述了hello程序在Linux系统的生命周期。从hello.c文件编写完成开始,研究它经过预处理、编译、汇编、链接并生成可执行文件的实现过程及原理。同时,结合深入研究计算机系统的书中知识解说我们的计算机系统是如进行进程管理、存储管理以及I/O管理。通过对hello生命周期的探索,体现我们对于计算机系统这门课的深刻理解。

关键词:hello程序、预处理、编译、汇编、链接、进程、存储、虚拟内存、I/O;                            

目  录

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

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

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

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

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 hello的存储管理

7.1 hello的存储器地址空间

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

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

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

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

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

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

7.9动态存储分配管理

7.10本章小结

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献


第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

P2P:先经过高级语言程序的编写得到hello.c文件。接着在Linux中,hello.c经过cpp的预处理得到.i的文本文件、ccl的编译得到.s的汇编语言文件、as的汇编得到.o的可重定位目标文件、ld的链接最终成为可执行目标程序hello。在shell中输入./hello执行hello后,shell会调用fork函数为其产生一个子进程,然后hello便从程序变为了进程。

020: 操作系统为此子进程调用execve,映射虚拟内存,先删除当前虚拟地址的数据结构并创建hello的数据结构。接着进入程序入口后载入物理内存,再进入 main函数执行目标代码,CPU会为执行的hello分配时间片执行逻辑控制流。待程序结束运行,shell父进程会负责回收子进程hello,最后内核删除相关数据结构。

1.2 环境与工具

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

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

开发与调试工具:CodeBlocks,gcc,edb,readelf,HexEdit

1.3 中间结果

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

文件名称

文件作用

hello.i

hello.c经过预处理后的文本文件

hello.s

hello.i编译后的汇编文件

hello.o

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

hello

hello.o链接之后的执行文件

hello.out

hello反汇编之后的可重定位文件

1.4 本章小结

本章大致介绍了hello一生周期中的p2p,020过程,列出了实验所需的软硬件环境、调试工具、中间结果,并且大致叙述了hello程序从刚编写的c程序hello.c一步步变成可执行目标文件hello的大致过程。


第2章 预处理

2.1 预处理的概念与作用

预处理概念:

预处理是程序源代码在被编译之前,由预处理器对程序源代码进行处理。这个过程并不对程序的源代码语法进行解析,但它会把源代码分割或处理成特定的符号为下一步的编译做准备工作。修改原始的C程序并生成一个文本文件。

预处理作用:

  1. 处理宏定义

对于宏定义的预处理工作也叫做宏展开:将宏名替换为字符串,即在对相关命令或语句的含义和功能作具体分析之前就要换。指令预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外,通常把排除在外的语句转换成空行。

2. 处理条件编译指令

条件编译指令如#ifdef,#ifndef,#else,#elif,#endif等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。

  1. 处理文件包含

文件包含处理是指在一个源文件中,通过文件包含命令将另一个源文件的内容全部包含在此文件中。在源文件编译时,连同被包含进来的文件一同编译,生成目标目标文件。头文件包含指令头文件包含指令如#include "FileName"或者#include 等。该指令将头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。

4.处理特殊符号

预编译程序可以识别一些特殊的符号。例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。

2.2在Ubuntu下预处理的命令

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

2.3 Hello的预处理结果解析

hello.c的内容截屏

hello.i的部分内容截屏

c语言文件hello.c在经过gcc的预处理之后,转变为了叫做hello.i的文本文件。打开文件我们可以发现c文件原本的main函数部分的代码内容保持不变仍可阅读,头文件部分不见了,替换成了其他内容。这是对源文件中的宏进行了宏展开,头文件的内容被包含进该文件。包括例如声明函数、定义宏、定义变量、定义结构体等内容。对于代码中的#define宏定义,预处理也会替换相应的符号。

2.4 本章小结

本章内容介绍了预处理的概念及其相关操作,并通过对给出的hello.c文件进行实际处理过程展示了文件经过预处理后的变化及预处理的作用。


第3章 编译

3.1 编译的概念与作用

编译的概念:

编译程序读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码。

编译的作用:

从这次的案例上看来,就是编译器经过一系列操作将原本的文本文件hello.i编译成文本文件hello.s。从内容上看,就是将高级程序设计语言编写的代码转变成等价的汇编代码。除了基本功能之外,编译程序还具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用以及人际联系等重要功能。

3.2 在Ubuntu下编译的命令

gcc hello.i -S -o hello.s

3.3 Hello的编译结果解析

hello.s文件的部分内容截屏

3.3.1数据表示

在原本高级语言编写的代码中,许多的数据内容都以另一种形式在hello.s文本文件中展示

  1. 变量i

在源代码中,main函数声明了一个局部变量i,而编译器对其进行编译时,会将i放在堆栈中。而我们查看汇编代码可知它放在-4(%rbp)的位置。

  1. 立即数

立即数在汇编代码中的表示与在源代码中一直,不过由于汇编规则要求,在前面加一个$符号。

源代码:    汇编代码:

  1. 数组

hello.c中唯一的数组就是作为main函数第二个输入参数的char* argv[],这是一个字符类型指针数组,数组中的每个元素都是一个指向字符类型的指针。观察汇编代码可以得知数组的起始地址存放在-32(%rbp)中,并且被两次调用将参数传给printf

每行的操作是

1.把argv数组的地址存放到%rax中

2.根据一个指针占8个字节,第二行找到了存argv[2]的地址

3.获得argv[2]的内容

4.重新将argv数组的地址存放到%rax中

5.转到存argv[1]的地址

6.获得argv[1]的内容

(4)字符串

程序中有两个字符串,且都位于只读数据段

3.3.2函数表示

由hello.c可知,hello.c声明了一个全局函数int main(int argc,char *argv[]),经过编译之后,main函数中使用的字符串常量也被存放在数据区。

观察汇编代码也可知main是全局函数。

3.3.3赋值操作

在hello.c中的赋值操作有i=0,而在汇编代码中的等价操作主要是通过mov指令实现的。

mov指令会根据后缀的不同移动不同的字节数。

movb:一个字节   movw:两个字节   movl:四个字节   movq:八个字节

3.3.4算数操作

hello.c中的算数操作是i++,在汇编代码中是通过add指令实现的。

同时,汇编指令中的运算操作还有:

指令

操作

leaq S , D

D=&S

INC D

D++

DEC D

D--

NEG D

D=-D

NOT D

D=~D

ADD S , D

D=D+S

SUB S , D

D=D-S

IMUL S , D

D=D*S

XOR S , D

D=D^S

OR S , D

D=D|S

AND S , D

D=D&S

SAL k , D

D=D<

SHL k , D

D=D<

SAR k , D

D=D>>k(算术右移)

SHR k , D

D=D>>k(逻辑右移)

3.3.5类型转换

hello.c中涉及的类型转换是atoi(argv[3]),这里是将字符串类型转换为整数类型。除了这个,int、float、double、short、char之间都可以相互转换。

3.3.6关系操作

在hello.c中有两个进行比较的关系操作。

  1. argc!=4用于if语句的判断条件,如果满足就执行部分语句,它的等价汇编代码是:

  1. i<8是一个判断循环是否继续执行的条件,若不满足就跳出循环,它的等价汇编代码是:

3.3.7控制转移

在汇编语言中,实现控制转移要先设置条件码,然后根据条件码来进行控制转移,在hello.c中,有以下控制转移指令:

(1)判断argc是否等于4,如果等于4就执行if语句,否则不执行,if(argc!=4)在汇编代码下表示为:

(2)for(i=0;i<8;i++)是一个for循环,其中i<8是判断循环是否继续执行的条件,若不满足则跳出循环,它的等价汇编代码是:

3.3.8函数操作

当一个函数P调用另一个函数Q时会有如下操作:

(1)传递控制:进行过程Q的时候,程序计数器必须设置为Q的代码的起始地址,然后在返回时,要把程序计数器设置为P中调用Q后面那条指令的地址。

(2)传递数据:P必须能够向Q提供一个或多个参数,Q必须能够向P中返回 一个值。

(3)分配和释放内存:在开始时,Q可能需要为局部变量分配空间,而在返回 前,又必须释放这些空间。

而hello.c涉及的函数操作有:main函数,printf,exit,sleep ,getchar函数。其中main函数的参数是argc和argv;而两次printf函数的参数是那两个字符串;exit参数是1,sleep函数参数是atoi(argv[3]),函数的返回值存储在%eax寄存器中。

3.4 本章小结

本章主要讲述了在hello.i到hello.s的编译阶段中,编译器对.i文件进行了何种操作,对于各类数据进行了怎样的处理,最后将高级语言转换成汇编语言。还讲了c语言中各种类型和操作所对应的的汇编代码。通过理解了这些编译器编译的机制,我们可以对于计算机的汇编过程有更深一层的了解,也能更熟练汇编语言和c语言之间的相互翻译。

第4章 汇编

4.1 汇编的概念与作用

概念:

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

作用:

将汇编代码转换为机器指令,使其在链接后能被机器识别并执行。

4.2 在Ubuntu下汇编的命令

gcc hello.s -c -o hello.o

4.3 可重定位目标elf格式

(1)观察ELF Header用命令:readelf -h hello.o,

ELF Header:以16字节的序列Magic开始,Magic描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解##标题释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。

根据头文件的信息,可以知道该文件是可重定位目标文件,有14个节。

(2)观察Section Headers用命令:readelf -S hello.o

Section Headers:节头部表,包含了文件中出现的各个节的语义,包括节 的类型、位置和大小等信息。 由于是可重定位目标文件,所以每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,同时可以观察到,代码是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。

(3)查看符号表.symtab :命令readelf -s hello.o

.symtab: 存放程序中定义和引用的函数和全局变量的信息。name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。

(4)重定位节.rela.text命令readelf -r hello.o

重定位节是一个.text节中位置的列表,其中包含了.text节中所有需要重定位的信息,当链接器把当前文件与其他文件进行链接并重定位时,会修改这些信息。重定位节.rela.text中各项符号的含义:

Offset:需要被修改的引用节的偏移

Info:包括symbol和type两个部分,symbol在前面四个字节,type在后面四个字节,

symbol:标识被修改引用应该指向的符号,

type:重定位的类型

Type:告知链接器应该如何修改新的应用

Attend:一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整Name:重定向到的目标的名称。

4.4 Hello.o的结果解析

objdump -d -r hello.o  

分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

第三章中的汇编代码

经过对反汇编代码和hello.s的比较,可以发现两者并没有什么不同的地方,不过反汇编代码除了汇编代码外,还有些机器代码。机器语言程序是二进制机器指令的集合,是使用二进制数据表示的,可以被电脑直接识别的语言。不同于汇编语言是用人们熟悉的词句描述CPU动作,机器指令是由操作码和操作数组成,最接近CPU运行原理的语言。由于汇编语言的操作码和操作数都是可以用二进制表示的,所以每一条汇编指令都可以映射到一条等价的机器指令,对汇编语言的翻译就可以得到机器语言。

两者的不同之处:

(1)分支跳转不同:在汇编语言组成的.s文件中,我们可以有L2,L3,L4分别代表了一段与前后不同的内容,可以看做是这段分支的名称。而其中的跳转指令,就直接采用操作+名称的方式跳到某个分支。而在反汇编代码中,跳转指令并不是用段名称,而是采用确定的地址。形如 je 2f。所以我们可以得出结论,段名称是我们用在汇编语言中为了便于管理而采用的,类似于别称、标记符。而在汇编成机器语言时,并不是翻译段名称,而是转为确定的地址。

(2)函数调用不同:在.s文件中,函数调用指令后会跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。因为在hello.c中,它只有一个main函数,在main中调用的函数都是共享库中的函数,在hello.c中并不存在,所以需要通过动态链接器才能确定函数的运行时执行地址,然后在汇编成为机器语言的时候,对于这些不确定地址的函数调用,所以在反汇编中,将其call指令后的相对地址偏移设置为全0,这样看起来目标地址就全是下一条指令的地址,然后在.rela.text节中为其添加重定位条目,等待静态链接的进一步确定。

4.5 本章小结

本章对hello.s进行了汇编,并生成hello.o可重定位文件的过程进行了实现并分析其过程。对生成的可重定位文件的各方面的数据都进行了展示,比如ELF头、节头部表、符号表和可重定位节。我们还查看了.o文件的反汇编代码,将它与.s文件中的汇编代码进行比较,分析了两者的不同处,深入理解了汇编语言到机器语言的映射关系。了hello.s和hello.o反汇编代码的不同之处,分析了从汇编语言到机器语言的一一映射关系。


5链接

5.1 链接的概念与作用

概念:

链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。链接是由叫做链接器的程序执行的。链接器使得分离编译成为可能。

作用:

链接器在软件开发过程中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。

5.2 在Ubuntu下链接的命令

命令:

ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/

x86_64-linux-gnu/crtn.o

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

(1)用readelf -h hello查看hello可执行文件的ELF格式。观察比较hello的文件头和hello.o文件头可以发现两个不同之处,一个是Type不同,可以说明hello是可执行文件。还有一个是节的数量不同,hello有27个节。

    (2)用readelf -S hello查看节头部表Section Headers。Section Headers展示了可执行文件hello中全部的节的信息,包括大小、偏移量等等。因此我们可以根据Section Headers中的信息,可以用HexEdit来定位各个节所占的区间。其中Address是程序被载入到虚拟地址的起始地址。

(3)可重定位节rel.text

(4)符号表.symtab

5.4 hello的虚拟地址空间

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

通过查看edb可以发现虚拟地址开始于0x400000

虚拟地址结束于0x400ff0

根据节头目表,我们可以通过edb查看各个节的信息,比如.text节,虚拟地址起始于0x4010f0,大小为0x145

比如.plt节,虚拟地址起始于0x401020,大小为0x70

5.5 链接的重定位过程分析

用命令objdump -d -r hello>hello.out获取hello的反汇编代码。分析hello与hello.o的不同,说明链接的过程。

  1. 观察发现hello的反汇编的代码中,有确定的虚拟地址,这是重定位完成后的结果。而hello.o反汇编代码中代码的虚拟地址均为0,还未完成重定位。

  1. 在hello反汇编的代码中多了很多的节以及很多函数的汇编代码。

其中这些节都有各自的含义,下面用表格列出来

节名称

含义

.init

程序初始化

.plt

动态链接表

.dynamic

存放被ld.so使用的动态链接信息

.data

初始化过的全局变量或者声明过的函数

.comment

一串包含编译器的NULL-terminated字符串

.interp

保存ld.so的路径

.hash

符号的哈希表

.gnu.hash

GNU拓展的符号的哈希表

.dynsym

动态符号表

.gnu.version

符号版本

.gnu.version_r

符号引用版本

.rela.plt

.plt节的重定位条目

.fini

程序终止时需要的执行的指令

.dynstr

动态符号表中的符号名称

.rela.dyn

动态重定位表

.got

存放程序中变量全局偏移量

.data

初始化过的全局变量或者声明过的函数

重定位过程:

我们在链接的生成可执行文件的时候,需要确定装入内存的实际物理地址,并修改程序中与地址有关的代码,这一过程叫做地址重定位。地址空间的程序和数据经过地址重定位处理后,就变成了可由CPU直接执行的绝对地址程序。我们把这一地址集合称为“绝对地址空间”或“存储空间”。重定位有一下几个步骤

(1)重定位节和符号定义链接器将所有类型相同的节都合并到一各节,而这个节就作为可执行目标文件的该类型的节。比如把所有待链接的.text节合并到hello可执行文件的.text节中。然后链接器把运行时的内存地址新的节以及输入模块定义的各个符号。完成后,程序中每条指令和全局变量都有唯一运行时的物理地址。

(2)重定位节中符号引用这一步中,需要链接器修改代码节和数据节中对各个符号的引用,通过可重定位条目的内容,将引用的每个符号都指向正确的地址。

(3)重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目。

重定位过程中的符号及含义

重定位过程中的地址计算算法

5.6 hello的执行流程

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

程序名称

程序地址

hello!_start

0x400500

hello!printf@plt

0x4005db

hello!atoi@plt

0x4005ee

hello!sleep@plt

0x4005f5

hello!getchar@plt

0x400604

ld -2.27.so!_dl_start

0x7fce 8cc38ea0

ld-2.27.so!_dl_init

0x7fce 8cc47630

libc-2.27.so!__libc_start_main

0x7fce 8c867ab0

libc-2.27.so!__cxa_atexit

0x7fce 8c889430

libc-2.27.so!__libc_csu_mit

0x4005c0

libc-2.27.so!_setjump

0x7fce 8c884c10

libc-2.27.so!exit

0x7fce 8c889128

hello!puts@plt

0x40059e

hello!exit@plt

0x4005a8

5.7 Hello的动态链接分析

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

在do_init之前:

在do_init之后:

找到的地址已经由0变成了相应的偏移量。

5.8 本章小结

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


6hello进程管理

6.1 进程的概念与作用

概念:进程是正在运行的程序的实例。也是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动过程调用的指令和本地变量。

作用:进程为用户提供了以下假象:

(1) 我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。

(2) 处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

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

作用:

是一种交互型的应用级程序,是Linux的外壳,提供了一个界面,代表用户运行其他程序,用户可以通过这界面访问操作系统内核。

处理流程:

(1)从终端读入输入的命令。

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

(3)如果是内置命令则立即执行

(4)否则调用相应的程序为其分配子进程并运行

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

6.3 Hello的fork进程创建过程

终端程序通过调用fork()函数创建一个子进程,子进程继承了父进程的全部内容。它与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。父进程与子进程并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。

例如我们的hello可执行文件,输入./hello 1190201104 叶珏相 1时,shell解析我们的命令,然后调用fork()创建子进程。子进程得到与父进程用户级虚拟地址空间相同的但是独立的一份副本,拥有不同的PID。

6.4 Hello的execve过程

execve的功能是在当前进程的上下文中加载并运行一个新程序。在我们创建了一个子进程之后,子进程调用exceve函数在当前进程的上下文用加载器加载并运行一个新的程序,这里就是hello程序。

加载器运行的过程中会有如下步骤:

  1. 删除已存在的用户区域,删除当前进程的所有虚拟地址段。
  2. 映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。

(3)映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。

(4)设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

进程提供给应用程序的抽象:
(1) 一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器。
(2) 一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用内存系统。

让我们来深入的看看这些抽象:

逻辑控制流:一系列程序计数器PC的值,这些值唯一的对应于包含在程序的可执行目标文件中的指令,这个PC值的序列叫做逻辑控制流。进程是轮流使用处理器的,每个进程执行它的流的一部分,然后被抢占(暂时挂起),然后轮到其他进程。

并发流:一个逻辑流的执行时间与另一个流重叠,称为并发流,这两个流被称为并发的运行。

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

用户模式和内核模式:处理器通常用某个控制寄存器的一个模式位,来提供用户模式/内核模式这一机制的功能。没有设模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据。设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

上下文切换:操作系统内核使用一种称为上下文切换的异常控制流来实现多任务。当内核选择一个新的进程运行时,称为内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。同时进行这些操作(1)保存当前进程上下文(2)恢复某个先前被抢占的进程被保存的上下文(3)将控制传递给这个新恢复的进程。

如上,我们可以得知在hello进程执行时,在进程调用execve函数之后,进程会为hello分配虚拟地址空间,为.text节和.data节分配代码区和数据区。一开始hello运行在用户模式下,输出hello 1190201104 叶珏相,然后hello就调用了sleep函数,进程陷入内核模式,内核会请求释放当前进程,将hello进程移出运行队列加入等待队列,这时计时器开始计时,内核也进行上下文切换将当前进程的控制权交给其他进程。根据我们选择的时间,时间到后,会发送一个中断信号,此时又进入内核状态执行中断处理,将hello进程重新加入运行队列,hello就继续执行自己的控制逻辑流。

6.6 hello的异常与信号处理

 异常分为四类,以下是他们的种类和属性。

中断:中断是异步发生的,是来自处理器外部的I/O设备的信号导致的异常。

陷阱:陷阱是有意的异常,是执行一条指令的结果。

故障:故障是由错误情况引起,可能可以被故障处理程序修复的异常,比如第一次访问内存时一定会发生缺页,这是后就调用缺页处理程序。

终止:终止是不可恢复的错误导致的,通常是一些硬件错误,比如DRAM或者SRAM位损坏的奇偶错误。

在发生异常后,系统会根据异常的种类发出信号,每个信号都有对应的序号。

正常执行程序:

在执行过程中乱按字母:

在执行过程中按回车。观察可以发现,我们在过程中按得回车数量等同于在正常执行后按的回车数。可以得出结论,乱按只是将屏幕的输入缓存到stdin,当getchar的时候读出一个’\n’作为结束符的字串,其它字串会当成shell的命令输入。

在执行过程中按ctrl+z作为输入。ctrl+z的默认结果是挂起前台的作业,但是hello进程并没有回收,转到了后台。用ps命令可以看到hello进程确实没有被回收。此时他的后台job号是1,所以可以调用fg 1命令将hello进程再调回前台。此时,shell先打印hello的命令行命令,hello则继续运行剩余未完成的内容,继续执行打印操作直到打印了八条信息。最后输入字串,程序结束,同时进程被回收。

在执行过程中按ctrl+c作为输入。ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业。用ps查看前台进程组,可以发现其中并没有hello进程。

6.7本章小结

本章阐述了进程的概念及作用,介绍了shell的一般处理流程及作用,以及fork和execve函数是如何创建新进程、调用进程的。还分析了hello执行过程中会遇到的异常信号及处理方法。


7hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:CPU所生成的地址。逻辑地址是内部和编程使用的、并不唯一。每一个逻辑地址都是有一个段和偏移量组成,偏移量指明了从段开始的地方到时间地址之间的距离逻辑地址用来指定一个操作数或者是一条指令的地址。

线性地址:也叫虚拟地址,和逻辑地址类似,也是一个不真实的地址。逻辑地址对应的是硬件平台段式管理转换前地址,那么线性地址对应的是硬件也是内存的转换前地址。

虚拟地址:也就是线性地址。

物理地址:加载到内存地址寄存器中的地址,内存单元的真正地址。在前端总线上传输的内存地址都是物理内存地址,编号从0开始一直到可用物理内存的最高端。这些数字被北桥映射到实际的内存条上。

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

一个逻辑地址实际是由48位组成的,前16位为段选择符,后16位为段内偏移量。其中,段选择符又称为段标识符。后面3位包含一些硬件细节。索引号就是段描述符的索引,段描述符具体地址描述了一个段。而很多个段描述符,就组了一个段描述符表。可以通过段标识符的前13位,在段描述符表中找到一个对应的段描述符。

段内偏移量,顾名思义就是对应于数据在段内,相对于段起始地址的地址偏移量。

在下图中,TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)。

RPL=00,为第0级,位于最高级的内核态,RPL=11,为第3级,位于最低级的用户态,第0级高于第3级。

结合下图得知,我们可以根据段选择符的T1是0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。再根据段选择符中前13位,可以查找到对应的段描述符,基地址就知道了。然后把段基地和段内便宜量合在一起,就得到了线性地址。

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

将一个虚拟地址转换为物理地址的任务叫做地址翻译。

由本书知识可知,虚拟系统通过将虚拟内存分割为称为虚拟页的大小固定的块来进行信息传输。虚拟页的集合可分为三种

  1. 未分配的:未分配的页不和任何数据相关联,所以也不占用任何磁盘空间
  2. 缓存的:当前已经缓存在物理内存中的已分配项
  3. 未缓存的:未缓存在物理内存中的已分配项

虚拟页的大小往往很大,通常是4KB~2MB,这是为了减少不命中时的处罚。

页表:系统为了确定一个虚拟页存放在哪个物理页,创建了页表这一数据结构。页表将虚拟页映射到物理页,每次地址翻译软件硬件将一个虚拟地址转换为物理地址时,都会读取页表。操作系统负责维护页表的内容,以及在磁盘与DRAM之间来回传送页。页表就是一个页表条目的数字,虚拟地址空间中的每一个页在页表中的固定偏移量处有一个PTE,而每个PTE是由一个有效位和一个n位地址字段组成的。有效位表示当前虚拟页是否缓存在DRAM中。若没有设置有效位,地址字段为空地址时代表这个虚拟页还没有被分配。地址字段不为空令这个地址指向该虚拟页在磁盘上的起始地址。

页表的基本组织结构如下

我们让虚拟地址的前面几位作为虚拟页号VPN,作为到页表中的索引,然后根据对应的物理页号,以及虚拟页偏移量,合在一起作为物理地址。

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

如果只有一个单独的页表进行地址翻译的话,页表本身就会占用部分内存。而为了压缩页表,使用层次结构的页表变成了一种可行的方法。先用一个二级页表的示例进行讲解。假设内存的前2K个页面分配给了代码和数据,后面的6K个页面也未分配。再往后的1023个页面也未分配,这之后的一个页面分配给了用户栈。在一级页表中,每个PTE负责映射虚拟地址空间一个4MB的片,而每个片都是由1024个连续的页面组成。而二级页表的每个PTE都负责映射一个4KB的虚拟内存页面,就像只有一级页表一样来查看。这种方式从两方面减少了内存要求(1)如果一级页表中点的一个PTE是空的,那么就没有相应的二级页表。(2)只有一级页表需要一直在主存中,虚拟内存系统可以在需要时创建、页面调入或调出二级页表,只有最经常使用的二级页表才缓存在主存中。

同上,如果采用k级页表就会如下所示。这里令k等于4。

TLB是一个关于PTE的小的缓存,成为翻译后备缓冲器。每一行都保持着一个由单个PTE组成的块。

所以,实际操作时,先由CPU产生虚拟地址VA,VA先传送给MMU,MMU用前面的几位VPN作为TLB标记(TLBT)和TLB索引(TLBI)向TLB中匹配。如果命中了就直接得到了PPN和VPO组合起来得到PA。如果没命中,MMU就向页表中查询,确定一级页表的起始地址以及在一级页表中的偏移量。然后根据一级页表查询到二级页表的起始地址,如此操作一直到四级页表中查询到PPN,与VPO组合成PA,并向TLB添加条目。如果查询PTE时发现未缓存到物理内存,就会引发缺页故障,调用缺页处理程序。

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

L1的Cache有64组,所以组索引位有6位,每组有8个高速缓存行,而每块的大小为64B,所以块偏移也有6位。因此标志位有52-6-6=40位。

  1. 在得到物理地址后,对8路的块分别匹配CT进行标志位匹配。若匹配成功且块的标志位valid为1,则判定为hit。
  2. 然后根据块偏移得知我们要的字节在块中的具体位置,然后就可以直接取用该字节内容返回给CPU。
  3. 如果高速缓存没命中,则要从存储层次结构的下一层去找被要求的块。L1没命中就到L2中找,L2中没命中就到L3找,L3也没没命中就去主存里找。然后将新的块存储在组索引位所指示的组中的一个高速缓存中。
  4. 映射到的组内有空闲块的,就直接放置,若组内无空闲块,则产生冲突,与最近最少使用的块进行替换。

7.6 hello进程fork时的内存映射

在shell输入命令行后,内核调用fork创建子进程,为hello程序的运行创建上下文,并分配一个与父进程不同的PID。通过fork创建的子进程拥有父进程相同的区域结构、页表等的一份副本,同时子进程也可以访问任何父进程已经打开的文件。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

7.7 hello进程execve时的内存映射

execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。 加载并运行 hello 需要以下几个步骤:

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

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

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

(4)设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。

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

当指令引用一个虚拟地址,而与之相应的物理地址不在内存中时,就会引发缺页故障。然后调用内核中的缺页异常处理程序,选择一个牺牲页进行替换,若牺牲页内容已经被修改了,则会把它复制给磁盘。然后,内核从磁盘复制相应物理地址的内容到牺牲页。做完这一切后,重新启动导致缺页的程序。

7.9动态存储分配管理

动态内存分配器维护着动态内存分配器维护者一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。分配器有两种基本风格。两种风格都是要求显示的释放分配块。它们的不同之处哪个实体来负责释放已分配的块。

显式分配器:要求应用显式的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器。

隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。

隐式空闲链表:

在隐式空闲链表中,每个块都是由一个字的头部,一个有效载荷还有一部分可能的填充部分组成。头部的信息包含了这个块的大小以及是否已分配。因为块的大小一定是8的倍数,所以用来表示块大小的后三位一定是0,所以可以用来表示其它信息,比如最后一位是1就表示已分配,0就表示是空闲的。头部后面是应用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。如下图所示,空闲块通过头部块的大小字段隐含的连接着,所以我们称这种结构组成的链表就隐式空闲链表。

当应用请求k字节的块时,分配器会搜索空闲链表,依次查找一个足够大的空闲块。查找方式有三种,首次适配,下一次适配和最佳适配。

首次适配:选择第一个合适的空闲块。

下一次适配:从上一次查询结束的地方开始搜索空闲链表。

最佳适配:检查每一个空闲块,选择适合所需求大小的最小空闲块。

分割空闲块一旦分配器找到一个匹配的空闲块,就必须做一个另策决定,那就是分配这个空闲块中多少空间。可以用整个空闲块,但是会造成内部碎片。分配器常用的选择是将空闲块分割为两部分。第一部分变为了已分配块,第二部分变为了空闲块。

获取额外堆内存如果分配器不能为请求块找到空闲块,一个选择是合并那些在物理内存上相邻的空闲块,如果这样还不能生成一个足够大的块,分配器会调用sbrk函数,向内核请求额外的内存。合并空闲块合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。对于四种情况分别进行空闲块合并,我们只需要通过改变头部的信息就能完成合并空闲块。Knuth提出一种技术叫做边界标记,给每个块增加一个脚部,这样分配器可以通过检查脚部来判断前面一个块的起始位置和状态。

显示空闲链表:显示空闲链表是将空闲块组织为某种形式的显示数据结构。堆被组织为一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继的指针。

使用双向链表,可以使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间,有效的减少了首次适配的时间。

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

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

7.10本章小结

本章主要介绍了hello的存储器的地址空间,分析了计算机中四种地址空间的概念、区别以及如何相互转换。还介绍了hello的四级页表的虚拟地址空间到物理地址的转换,阐述了三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等。


8hello的IO管理

8.1 Linux的IO设备管理方法

一个Linux文件就是一个m字节的序列:

B0,B1,B2……Bm

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

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

Unix I/O接口:

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

(2)Shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。头文件定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可用来代替显式的描述符。

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

(4)读写文件:一个读操作就是从文件复制 n>0个字节到内存,从当前文件位置k开始,然后将k增加到 k+n,给定一个大小为 m字节的而文件,当 k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

(5)关闭文件:当应用完成对文件的访问后,会通知内核关闭这个文件。内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

Unix I/O函数:

(1)int open(char* filename,int flags,mode_t mode) ,进程通过调用 open 函 数来打开一个存在的文件或是创建一个新文件的。 open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在 进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访 问这个文件,mode 参数指定了新文件的访问权限位。

(2)int close(fd),fd 是需要关闭的文件的描述符,close 返回操作结果。

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

4) ssize_t wirte(int fd,const void *buf,size_t n),write 函数从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置。

8.3 printf的实现分析

printf函数的函数体:

static int printf(const char *fmt, ...)

{  

va_list args;  

int i;  

 va_start(args, fmt);  

write(1,printbuf,i=vsprintf(printbuf, fmt, args));  

va_end(args);  

return i;  

}

printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。

write函数:

write:

mov eax, _NR_write

mov ebx, [esp + 4]

mov ecx, [esp + 8]

int INT_VECTOR_SYS_CALL

在printf中调用系统函数write(buf,i)将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。

syscall函数体:

sys_call:  

call save

push dword [p_proc_ready]  

sti   

push ecx   

 push ebx  

 call [sys_call_table + eax * 4]  

 add esp, 4 * 3  

mov [esi + EAXREG - P_STACKBASE], eax   

 cli  

ret

syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。于是我们的打印字符串就显示在了屏幕上。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar代码:

int getchar(void)  

{  

static char buf[BUFSIZ];  

static char *bb = buf;  

static int n = 0;  

if(n == 0)  

{  

n = read(0, buf, BUFSIZ);  

bb = buf;  

}  

return(--n >= 0)?(unsigned char) *bb++ : EOF;  

}

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

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

8.5本章小结

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

结论

用计算机系统的语言,逐条总结hello所经历的过程。

(1) 用编程软件编写hello.c文本文件。

(2) 预处理,将hello.c调用的所有外部的库展开合并到一个hello.i文件中。

(3) 编译,用编译器将hello.i编译成为汇编文件hello.s。

(4) 汇编,用汇编器将hello.s汇编成为可重定位目标文件hello.o。

(5) 链接,将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello。

(6) 运行:在shell中输入./hello 1190201104 叶珏相 1。

(7) 创建子进程:shell进程调用fork创建子进程。

(8) 加载:shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数。

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

(10) 动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。

(11) 信号:如果运行途中键入ctrl+c ctrl+z则调用shell的信号处理函数分别停止、挂起。

(12) 结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。

对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

计算机系统的设计思想和实现都是基于抽象实现的。从最底层的信息的表示用二进制表示抽象开始,到实现操作系统管理硬件的抽象:进程是对处理器、主存和I/O设备的抽象。虚拟内存是对主存和磁盘设备的抽象。文件是对I/O设备的抽象。

计算机系统的设计精巧:为了解决快的设备存储小、存储大的设备慢的不平衡,设计了高速缓存来作为更底层的存储设备的缓存,大大提高了CPU访问主存的速度!!!

计算机系统的设计考虑全面:计算机系统设计考虑一切可能的实际情况,设计出一系列的满足不同情况的策略。比如写回和直写,写分配和非写分配,直接映射高速缓存和组相连高速缓存等等。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。


附件

hello.c:源代码

hello.i:预处理后的文本文件

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

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

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

Hello.out:反汇编后的可重定位文件


参考文献

[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.

[7] 深入理解计算机系统原书第3版

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值