深入理解计算机系统大作业--hello的一生

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业      计算机类                 

学     号      1190201223         

班     级      1903007                 

学       生       涂鑫泊              

指 导 教 师        吴锐               

计算机科学与技术学院

2021年5月

摘  要

本文主要讲述了hello程序在Linux系统中的生命历程,论述了hello程序从hello.c经过预处理、编译、汇编、链接生成可执行文件的全过程以及过程实现的原理并分析了这些过程中产生的文件的相应信息和作用。除此之外本文还结合课本的知识详细阐述计算机系统是如何对hello进行进程管理、内存管理和I/O管理,还介绍了虚拟内存、异常信号等相关内容。本文通过对hello一生周期的探索,让我们对计算机系统有更深的了解。

关键词:Hello程序预处理;编译;汇编;链接;shell;IO管理;进程管理;虚拟内存;异常信号                           

目  录

第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)

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

020From Zero-0 to Zero-0

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

1.2 环境与工具

(1)硬件环境:X64 CPU;2GHz;4GRAM;256Disk
(2)软件环境:Windows10 64位;Vmware 10;Ubuntu 16.04 LTS 64位
(3)使用工具:Codeblocks;Objdump;Gdb;Hexedit

1.3 中间结果

hello.c

源文件

hello.i

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

hello.s

hello.i编译后的汇编文件

hello.o

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

hello

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

hello.elf

hello.o的ELF格式

hello1.elf

hello的ELF格式

hello.txt

hello.o反汇编代码

hello1.txt

hello的反汇编代码

1.4 本章小结

本章主要介绍了hello的P2P,020过程,列出了本次实验的软硬件环境及开发与调试工具和在本论文中生成的中间结果文件。并且大致介绍了hello程序从c程序hello.c到可执行目标文件hello的大致经过的历程。

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。

预处理的作用:

1. 处理宏定义指令预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外,通常把排除在外的语句转换成空行。

2. 处理条件编译指令

3.处理头文件包含指令头文件包含指令如#include "FileName"或者#include 等。

4.处理特殊符号

2.2在Ubuntu下预处理的命令

对hello.c文件进行预处理的命令是:gcc -E -o hello.i hello.c

2.3 Hello的预处理结果解析

下面为hello.i文件内容截图

解析:经过预处理之后,hello.c转化为hello.i文件,打开该文件可以发现,文件的内容增加,扩展到了3000多行但是仍为可以阅读的C语言程序文本文件。对原文件中的宏进行了宏展开,增加的文本其实是三个头文件的源码。例如声明函数、定义结构体、定义变量、定义宏等内容。另外,如果代码中有#define命令还会对相应的符号进行替换。

2.4 本章小结

本章概括了预处理的概念和作用,详细说明了ubuntu下预处理命令,并分析了.i文件。

第3章 编译

3.1 编译的概念与作用

编译的概念:编译器将预处理文本文件 hello.i 翻译成文本文件 hello.s的过程,它包含一个汇编语言程序。

编译的作用:将输入的高级程序设计语言书写的源程序翻译成以汇编语言或机器语言表示的目标程序作为输出

3.2 在Ubuntu下编译的命令

对hello.i文件进行编译的命令是:gcc -S -o hello.s hello.i

hello.s的内容如下图:

3.3 Hello的编译结果解析

3.3.1数据

1.字符串

程序中有两个字符串且这两个字符串都在只读数据段中且这两个字符串作为printf函数的参数

2.局部变量 

main函数声明了一个局部变量i,编译器进行编译的时候将局部变量i会放在堆栈中。分析汇编代码并与源程序比较可知局部变量i被放在了寄存器-4(%rbp)中

3.数组

hello.c中的数组char *argv[]是作为main函数的第二个参数,数组的每个元素都是一个指向字符类型的指针。数组的起始地址存放在栈中-32(%rbp)的位置,被两次调用找参数传给printf

3.3.2处理关系操作符与控制语句

本程序出现了if(argc!=4)的!=关系操作符

编译器转换成汇编语言后就成了:

我们可以看到argc与4进行比较时,可以看到je指令,cmpl与je是放在一起的,如果两数相等je条件成立,跳转.L2也就是后面的循环否则就跳过je继续向下执行。可以看到关系操作符与控制语句是借助jx指令实现的,对于其它的关系操作符有:

3.3.3赋值操作

程序中的赋值操作主要有:i=0这条赋值操作在汇编代码主要使用mov指令来实现,而根据数据的类型又有好几种不一样的后缀

movb:一个字节

movw:两个字节

movl:四个字节

movq:八个字节

3.3.4处理四则运算与复合语句

(1)加: x=x+y汇编语言是addq y,x

(2)减: x=x-y 汇编语言是subq y,x

(3)乘: x=x*y 汇编语言是imulq y,x

(4)除: z=x/y 汇编语言是

movq x, z

cqto

idivq y

复合语句就是上面的组合,或者也有复合的汇编语句:z=x+Ay+B(A,B都是立即数)的汇编语言是leaq B(x,y,A), z

本程序中出现了addq:

3.3.5处理数组、指针与结构体

(1)数组:取数组头指针加上第i位偏移量来处理。
(2)指针与数组类似,如果rax表示指针所存的寄存器,访问x指向的值就是(%rax)
(3)结构体:通过结构体内部的偏移量来访问。
本程序中出现了数组,截图如下:

3.3.6函数操作

调用函数时有以下操作:(假设函数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.3.7类型转换

hello.c中涉及的类型转换是:atoi(argv[3]),将字符串类型转换为整数类型其他的类型转换还有int、float、double、short、char之间的转换

3.4 本章小结

本章主要讲述了编译的概念和作用,重点分析了编译阶段中编译器如何处理各种数据和操作,以及c语言中各种类型数据和操作翻译成汇编语言时所对应的的汇编代码。

第4章 汇编

4.1 汇编的概念与作用

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

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

4.2 在Ubuntu下汇编的命令

在Ubuntu下汇编的命令为:gcc -c -o hello.o hello.s

4.3 可重定位目标elf格式

在linux下生成hello.o文件elf格式的命令:readelf -a hello.o > hello.elf

分析hello.o的ELF格式:

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

  1. 节头:记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、对齐等信息。 由于是可重定位目标文件,所以每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,同时可以观察到,代码是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。

  1. 符号表:

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

  1. 重定位节:

.rela.text,保存的是.text节中需要被修正的信息;任何调用外部函数或者引用全局变量的指令都需要被修正;调用外部函数的指令需要重定位;引用全局变量的指令需要重定位; 调用局部函数的指令不需要重定位;在可执行目标文件中不存在重定位信息。本程序需要被重定位的是printf、puts、exit、sleepsecs、getchar、sleep和.rodata中的.L0和.L1。

.rela.eh_frame节是.eh_frame节重定位信息。

4.4 Hello.o的结果解析

Hello.o的结果解析指令如下:objdump -d -r hello.o

第3章的 hello.s如下

对照发现汇编语言的指令并没有不同,只是反汇编代码所显示的除了汇编代码以外,还有机器代码,机器语言程序是二进制机器指令的集合,是纯粹的二进制数据表示的语言,是电脑可以真正识别的语言。机器指令由操作码和操作数构成,汇编语言是人们比较熟悉的词句直接表述CPU动作形成的语言,是最接近CPU运行原理的语言。每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言(操作码和操作数)和二进制机器语言建立一一映射的关系,因此可以将汇编语言转化为机器语言,通过对机器代码的分析可以看出以下三点不同的地方。

(1)分支转移:在汇编代码中,分支跳转是直接以.L0等助记符表示,但在反汇编代码中,分支转移表示为主函数+段内偏移量。反汇编代码跳转指令的操作数使用的不是段名称,因为段名称知识在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。

(2)函数调用:汇编代码中函数调用时直接个函数名称,而在反汇编的文件中call之后加main+偏移量(定位到call的下一条指令),即用具体的地址表示。在.rela.text节中为其添加重定位条目等待链接。

(3)访问全局变量:汇编代码中使用.LC0(%rip),反汇编代码中为0x0(%rip),因为访问时需要重定位,所以初始化为0并添加重定位条目。

4.5 本章小结

本章概括了汇编的概念和作用,并且通过对hello.s进行了汇编,生成了hello.o可重定位目标文件,分析了ELF文件的内容,另外比较了重定位前汇编程序和重定位后反汇编的差别,了解从汇编语言翻译成机器语言的转换处理和机器语言和汇编语言的映射关系。

5章 链接

5.1 链接的概念与作用

链接的概念:

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

链接的作用:

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

5.2 在Ubuntu下链接的命令

在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的格式

命令为:readelf -a hello > hello1.elf

ELF头:上次的节头数量为13个,这次变为27个

节头:节头对hello中所有的节信息进行了声明,其中包括大小Size以及在程序中的偏移量 Offset,因此根据节头中的信息我们就可以用HexEdi定位各个节所占的区间(起始位置,大小)。其中Address是程序被载入到虚拟地址的起始地址。

符号表:

重定位节.rela.text:

5.4 hello的虚拟地址空间

通过查看edb,看出hello的虚拟地址空间开始于0x400000,结束于0x400ff0

根据节头部表,可以通过edb找到各个节的信息,比如.txt节,虚拟地址开始于0x400550,大小为0x132.

5.5 链接的重定位过程分析

链接的重定位命令: objdump -d -r hello > hello1.txt

与hello.o生成的反汇编文件对比发现,hello1.txt中多了许多节。hello0.txt中只有一个.text节,而且只有一个main函数,函数地址也是默认的0x000000。hello1.txt中有.init,.plt,.text三个节,而且每个节中有许多的函数。库函数的代码都已经链接到了程序中,程序各个节变的更加完整,跳转的地址也具有参考性。

hello比hello.o多出的节头表。

.interp:保存ld.so的路径

.note.ABI-tag

.note.gnu.build-i:编译信息表

.gnu.hash:gnu的扩展符号hash表

.dynsym:动态符号表

.dynstr:动态符号表中的符号名称

.gnu.version:符号版本

.gnu.version_r:符号引用版本

.rela.dyn:动态重定位表

.rela.plt:.plt节的重定位条目

.init:程序初始化

.plt:动态链接表

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

.eh_frame:程序执行错误时的指令

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

.got:存放程序中变量全局偏移量

.got.plt:存放程序中函数的全局偏移量

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

hello1.txt部分截图如下:

hello重定位的过程:重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。在重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目。代码的重定位条目放在.rel.txt中。

5.6 hello的执行流程

载入:_dl_start、_dl_init

开始执行:_start、_libc_start_main

执行main:_main、_printf、_exit、_sleep、_getchar、_dl_runtime_resolve_xsave、_dl_fixup、_dl_lookup_symbol_x

退出:exit

程序名称

程序地址

ld-2.27.so!_dl_start 

0x7fb85a93aea0

ld-2.27.so!_dl_init

0x7f9612138630

hello!_start

0x400582

lib-2.27.so!__libc_start_main

0x7f9611d58ab0

hello!puts@plt

0x4004f0

hello!exit@plt

0x400530

5.7 Hello的动态链接分析

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。

在进行动态链接前,首先进行静态链接,生成部分链接的可执行目标文件hello。此时共享库中的代码和数据没有被合并到hello中。加载hello时,动态链接器对共享目标文件中的相应模块内的代码和数据进行重定位,加载共享库,生成完全链接的可执行目标文件。

动态链接采用了延迟加载的策略,即在调用函数时才进行符号的映射。使用偏移量表GOT+过程链接表PLT实现函数的动态链接。GOT中存放函数目标地址,为每个全局函数创建一个副本函数,并将对函数的调用转换成对副本函数调用。

调用init之前的.got.plt如下图:

调用init之前的.got.plt如下图:

由此可看出进行动态链接后, .got.plt的条目发生了变化

5.8 本章小结

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

6章 hello进程管理

6.1 进程的概念与作用

进程的概念:

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

进程的作用: 进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。进程让我们感觉我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。并且处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

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

壳的作用:

Shell是一个交互型应用级程序,它提供了一个界面,用户可以通过此界面访问操作系统内核

处理流程:

(1)终端进程读取用户由键盘输入的命令行。

(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量

(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令

(4)如果不是内部命令,调用fork( )创建新进程/子进程

(5)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。

(6)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid等待作业终止后返回。

(7)如果用户要求后台运行(如果命令末尾有&号),则shell返回;

6.3 Hello的fork进程创建过程

在终端中输入命令行./hello1190201223 涂鑫泊1后,shell会处理该命令,如果判断出不是内置命令,则终端程序会通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,但是子进程和父进程拥有不同的PID。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。

6.4 Hello的execve过程

execve的功能是在当前进程的上下文中加载并运行一个新程序。在执行fork得到子进程后随即使用解析后的命令行参数调用execve,execve调用启动加载器来执行hello程序。加载器执行的操作是,删除子进程现有的虚拟内存段,并创建新的代码、数据、堆和栈段。代码和数据段被初始化为hello的代码和数据。堆和栈被置空。然后加载器将PC指向hello程序的起始位置,即从下条指令开始执行hello程序。

6.5 Hello的进程执行

要想理解Hello的进程执行,首先要理解操作系统所提供的的进程抽象:

首先是1.逻辑控制流:一系列程序计数器 PC的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。

2.并发流:一个逻辑流的执行时间与另一个流重叠,成为并发流,这两个流成为并发的运行。多个流并发的执行的一般现象成为并发。

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

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

5.用户模式和内核模式::处理器通常使用一个寄存器提供两种模式的区分,该寄 存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的 代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

6.上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。

7.上下文切换:当内核选择一个新的进程运行时,则内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程:

(1) 保存以前进程的上下文

(2)恢复新恢复进程被保存的上下文,

(3)将控制传递给这 个新恢复的进程,来完成上下文切换。

之后再看hello进程执行,在进程调用execve函数之后,进程已经为hello程序分配了新的虚拟的地址空间,并且已经将hello的.txt和.data节分配虚拟地址空间的代码区和数据区。最初hello运行在用户模式下,输出hello 1190201223 涂鑫泊,然后hello调用sleep函数之后进程陷入内核模式,内核不会选择什么都不做等待sleep函数调用结束,而是处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。当hello调用getchar的时候,实际落脚到执行输入流是stdin的系统调用read,hello之前运行在用户模式,在进行read调用之后陷入内核,内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。此时进入内核模式,内核执行上下文切换,切换到其他进程。当完成键盘缓冲区到内存的数据传输时,引发一个中断信号,此时内核从其他进程进行上下文切换回hello进程。程序执行过程如下:

6.6 hello的异常与信号处理

执行过程可能出现的异常一共有四种:中断、陷阱、故障、终止。

中断:来自I/O设备的信号,异步发生,总是返回到下一条指令。

陷阱:有意的异常,同步发生,总是返回到下一条指令。

故障:潜在可恢复的错误,同步发生,可能返回到当前指令或终止。

终止:不可恢复的错误,同步发生,不会返回。

1.正常运行时:

 

2.ctrl+c终止

3. ctrl+z暂停,输入ps可以发现hello并未关闭

4. 运行过程中乱按,无关输入被缓存到stdin,并随着printf指令被输出到结果。

6.7本章小结

本章概括了进程的概念和作用、shell-bash的处理过程与作用,介绍了fork和execve进程以及hello进程的执行过程以及过程中的信号异常处理过程。

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:格式为“段地址:偏移地址”,是CPU生成的地址,在内部和编程使用,并不唯一。

线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址。

虚拟地址:保护模式下访问存储器所用的逻辑地址。

物理地址:加载到内存地址寄存器中的地址,内存单元的真正地址。CPU通过地址总线的寻址,找到真实的物理内存对应地址。在前端总线上传输的内存地址都是物理内存地址。

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

一个逻辑地址由两部份组成,段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,表示具体的是代码段寄存器还是栈段寄存器或是数据段寄存器。

索引号就是“段描述符(segment descriptor)”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组成了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这句话很关键,说明段标识符的具体作用,每一个段描述符由8个字节组成。

Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。那究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT,GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。

将逻辑地址变换线性地址时,首先看段选择符的T1=0还是1,以此来得知当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。然后拿出段选择符中前13位,可以在这个数组中查找到对应的段描述符,这样就得知了基地址。最后把Base + offset,就得到要转换的线性地址了。

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

线性地址到物理地址之间的转换是通过分页机制完成的。而分页机制是对虚拟地址内存空间进行分页。使用虚拟寻址,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址被送到内存之前首先要转换为适当的物理地址。将一个虚拟地址转换为物理地址叫做地址翻译,需要CPU硬件和操作系统之间的紧密合作。虚拟地址作为到磁盘上存放字节的数组的索引,磁盘上的数组内容被缓存在主存中。同时,磁盘上的数据被分割成块,这些块作为磁盘和主存之间的传送单元。虚拟内存分割被成为虚拟页。物理内存被分割为物理页,物理页和虚拟页的大小是相同的。在任意时刻虚拟页都被分为三个不相交的子集,即未分配的:VM系统还未分配的页;缓存的:当前已经缓存在物理内存的已分配页;未缓存的:当前未缓存在物理内存的已分配页。每次将虚拟地址转换为物理地址,都会查询页表来判断一个虚拟页是否缓存在DRAM的某个地方,如果不在DRAM的某个地方,通过查询页表条目就可以知道虚拟页在磁盘的位置。页表将虚拟页映射到物理页。页表就是一个页表条目的数组,每一个页表条目是由一个有效位和一个n为地址字段组成。有效位表明虚拟页是否缓存在DRAM中,n位地址字段是物理页的起始地址或者虚拟页在次胖的起始地址。形如下图:

而虚拟地址到物理地址的转换是系统将虚拟页作为进行数据传输的单元。Linux下每个虚拟页大小为4KB。物理内存也被分割为物理页, MMU(内存管理单元)负责地址翻译,MMU使用页表将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。形如下图:

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

Core i7采用四级页表的层次结构。CPU产生VA,VA传送给MMU,MMU使用VPN高位作为TLBT和TLBI向TLB中寻找匹配。如果命中,则得到PA。如果TLB中没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成PA,添加到PLT。

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

首先使用上一问中获得的PA,取组索引对应位,并向L1cache中寻找对应组。如果存在,则比较标志位,并检查对应行的有效位是否为1。如果标志位为1,则命中。否则按顺序对L2cache、L3cache、内存进行上述相同操作,直到出现命中为止。然后向上一级cache返回直到返回到L1cache。如果有空闲块则将目标块放置到空闲块中,否则将缓存中的某个块驱逐,并将目标块放到被驱逐块的位置。

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 缺页故障与缺页中断处理

缺页故障:当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。如果程序执行过程中遇到了缺页故障,内核会调用缺页处理程序。处理程序会进行如下步骤:首先检查虚拟地址是否合法,如果不合法则触发一个段错误,程序终止。然后检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,程序终止。在两步检查都无误后,内核选择一个牺牲页面,如果该页面被修改过则将其交换出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。具体过程如下图:

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。

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

分配器分为两种基本风格:显式分配器、隐式分配器。

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

隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。

堆中的块主要组织为两种形式:

1.隐式空闲链表(带边界标记)

在块的首尾的四个字节分别添加header和footer,负责维护当前块的信息(大小和是否分配)。由于每个块是对齐的,所以每个块的地址低位总是0,可以用该位标注当前块是否已经分配。可以利用header和footer中存放的块大小寻找当前块两侧的邻接块,方便进行空闲块的合并操作。

2.显式空闲链表

在未分配的块中添加两个指针,分别指向前一个空闲块和后一个空闲块。采用该策略,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。

7.9.1带边界标记的隐式空闲链表

1. 堆中内存块的组织结构

如图,一个块是由一个字的头部、有效载荷,以及可能的填充组成。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。块的头最后一位指明这个块是已分配的还是空闲的。头部后面是应用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。空闲块通过头部块的大小字段隐含的连接着,所以我们称这种结构为隐式空闲链表。在内存块中增加4B的Header和4B的Footer,其中Header用于寻找下一个blcok,Footer用于寻找上一个block。Footer的设计是专门为了合并空闲块方便的。因为Header和Footer大小已知,所以我们利用Header和Footer中存放的块大小就可以寻找上下block。

2. 隐式链表

所谓隐式空闲链表,对比于显式空闲链表,代表并不直接对空闲块进行链接,而是将对内存空间中的所有块组织成一个大链表,其中Header和Footer中的block大小间接起到了前驱、后继指针的作用。

3.空闲块合并

因为有了Footer,所以我们可以方便的对前面的空闲块进行合并。合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。对于四种情况分别进行空闲块合并,我们只需要通过改变Header和Footer中的值就可以完成这一操作。

7.9.2显示空间链表

显示空闲链表是将空闲块组织为某种形式的显示数据结构。堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针,如下图:

使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。维护链表的顺序有:后进先出(LIFO),将新释放的块放置在链表的开始处,使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比LIFO排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。

7.10本章小结

本章主要介绍了hello的存储器的地址空间以及计算机中的虚拟内存管理,虚拟地址、物理地址、线性地址、逻辑地址的区别以及它们之间的变换模式,以及段式、页式的管理模式。同时介绍了hello的四级页表的虚拟地址空间到物理地址的转换。阐述了三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配的方法和原理。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

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

设备管理:Linux内核有一个简单、低级的接口,成为Unix I/O,是的所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

Unix I/O接口统一操作:

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

(2) Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。

(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分量)。

于是我们的打印字符串就显示在了屏幕上。

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 1190201223 涂鑫泊

(7) 创建子进程:shell进程调用fork为其创建子进程,供之后hello程序的运行。

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

(9) 执行指令:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流

(10) 访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。

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

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

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

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

计算机系统的设计与实现是一个严密而精致的过程,它在不断更新也不断有新的技术涌现。就hello的诞生到结束,可能在操作者手中只经历了一段很短的时间,但是实际上在计算机中经历了千辛万苦,在硬件、操作系统、软件的相互协作配合下,终于完美地完成了它的使命。这让我认识到:一个完美的系统需要各个方面协作配合完成各项指令,实现各个功能。中间的每一个环节都很重要,不得有失。同时,计算机系统相比于现实事务又很抽象,从最底层的信息的表示用二进制表示抽象开始,到实现操作系统管理硬件的抽象:进程是对处理器、主存和I/O设备的抽象。虚拟内存是对主存和磁盘设备的抽象。文件是对I/O设备的抽象等等,我觉得要学好计算机系统就要对抽象有很好的理解,这样也有助于培养我们的抽象思维和创新理念,才能更加推动计算机的发展。

附件

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

hello.c

源文件

hello.i

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

hello.s

hello.i编译后的汇编文件

hello.o

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

hello

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

hello.elf

hello.o的ELF格式

hello1.elf

hello的ELF格式

hello.txt

hello.o反汇编代码

hello1.txt

hello的反汇编代码

参考文献

为完成本次大作业你翻阅的书籍与网站等

[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分)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值