csapp大作业

 

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业  计算机                 

学     号  120L020618             

班     级  2003004                

学       生  戴明旺                 

指 导 教 师  史先俊                    

计算机科学与技术学院

2022年5月

摘  要

    本文主要介绍了hello如何从无到有,又从有到无的一生。详述了,在它诞生的过程中都有什么硬件、软件参与,在它结束的时候又有什么硬件、软件协助使其不留一丝痕迹。

关键词:P2P;020;预处理;编译;汇编;链接;储存管理;                           

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

目  录

第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的预处理结果解析.............................................................................. - 7-

2.4 本章小结......................................................................................................... - 8 -

第3章 编译............................................................................................................. - 9 -

3.1 编译的概念与作用......................................................................................... - 9 -

3.2 在Ubuntu下编译的命令............................................................................. - 9 -

3.3 Hello的编译结果解析................................................................................ - 10 -

3.4 本章小结....................................................................................................... - 11 -

第4章 汇编........................................................................................................... - 12 -

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

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

4.3 可重定位目标elf格式............................................................................... - 13 -

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

4.5 本章小结....................................................................................................... - 16 -

第5章 链接........................................................................................................... - 17 -

5.1 链接的概念与作用....................................................................................... - 17 -

5.2 在Ubuntu下链接的命令........................................................................... - 17 -

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

5.4 hello的虚拟地址空间................................................................................. - 21-

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

5.6 hello的执行流程........................................................................................ - 22 -

5.7 Hello的动态链接分析................................................................................ - 23 -

5.8 本章小结....................................................................................................... - 24 -

第6章 hello进程管理................................................................................... - 25 -

6.1 进程的概念与作用....................................................................................... - 25 -

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

6.3 Hello的fork进程创建过程..................................................................... - 25 -

6.4 Hello的execve过程................................................................................. - 26 -

6.5 Hello的进程执行........................................................................................ - 26 -

6.6 hello的异常与信号处理............................................................................. - 27-

6.7本章小结....................................................................................................... - 29 -

第7章 hello的存储管理............................................................................... - 30 -

7.1 hello的存储器地址空间............................................................................ - 30 -

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

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

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

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

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

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

7.8 缺页故障与缺页中断处理........................................................................... - 34 -

7.9动态存储分配管理....................................................................................... - 34 -

7.10本章小结..................................................................................................... - 36 -

第8章 hello的IO管理................................................................................. - 37 -

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

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

8.3 printf的实现分析........................................................................................ - 38 -

8.4 getchar的实现分析.................................................................................... - 40 -

8.5本章小结....................................................................................................... - 41 -

结论......................................................................................................................... - 41 -

附件......................................................................................................................... - 42 -

参考文献................................................................................................................. - 43 -

第1章 概述

1.1 Hello简介

P2P: From Program to Process

GCC编译器驱动程序读取源程序文件hello.c,并把它翻译成一个可执行文件hello。这个过程中可分为四个阶段。预处理阶段,预处理器根据以字符#开头的命令,修改C程序,得到hello.i文件;编译阶段,编译器将文本文件hello.i翻译成文本文件hello.s;汇编器将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标文件的格式,并将结果保存到hello.o中;链接阶段,链接器将hello.o所需的外部库与hello.o链接起来,得到可执行目标文件。

命令行中输入“./hello”后,shell将其解析为外部命令,于是调用fork()函数创建子进程。再调用execve()函数,加载hello程序,删除子进程现有的虚拟内存段,创建一组新的段(段与栈初始化为0),并将虚拟地址空间中的页映射到可执行文件的页。新的代码与数据段初始化为可执行文件内容。即p2p

020From Zero-0 to Zero -0

在shell下输入命令“./hello”后,shell为hello创建子进程,并通过execve()函数加载hello到内存。之后,通过段式管理、页式管理、多级cache、I/O设备等联动,CPU为hello程序分配时间片,对每条指令进行取值、译码、执行、访存、写回、更新PC等操作。等程序终止后,由shell回收,释放内存、删除上下文等信息。即020。

1.2 环境与工具

硬件环境:

笔记本电脑(x64 Intel Core i5-9300H CPU;2.40GHz;8G RAM;256GHD Disk)

软件环境:

Windows10 64位;Vmware 16.0 build-14665864;Ubuntu 20.04.2 LTS 64位

开发工具:

Visual Studio 2019 64位;CodeBlocks; 记事本;vi/vim/gpedit+gcc;EDB;GDB/OBJDUMP;

1.3 中间结果

hello.c:源程序
hello.i:预处理后的文本文件
hello.s:编译后汇编程序文本文件
hello.o:汇编后的可重定位目标程序(二进制文件)
hello:链接后的可执行目标文件
helloo.txt:hello.o的反汇编文件
hello_out.txt:hello的反汇编文件

 

 

如图所示

1.4 本章小结

       第一章对hello程序进行了简单的介绍,即P2P,如何从程序到进程;020,hello程序如何从无到有,又是如何从有到无,不留一点痕迹。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

概念:

当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。C语言提供多种预处理功能,主要处理#开始的预编译指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。在此阶段,一般预处理器修改hello.c文件,从而生成hello.i文件。

       作用:

合理使用预处理功能编写的程序便于阅读、编译、修改、移植和调试,也有利于模块化程序设计。

预处理中会展开以#起始的行,试图解释为预处理指令(preprocessing directive) ,其中ISO C/C++要求支持的包括#if#ifdef#ifndef#else#elif#endif条件编译)、#define宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令) 

2.2在Ubuntu下预处理的命令

 

2.3 Hello的预处理结果解析

 

Hello.c源程序

​​​​​​​

 

 

Hello.i文件

经过对比,我们很容易发现,c程序从短短的23行扩展到令人惊讶的3900行。而源程序除去预处理命令外,都被放在在最后。显然,前面3000多行代码都因为#include <stdio.h>#include <unistd.h>#include <stdlib.h>这三条预处理命令,让预处理程序从相应的系统库文件中读取、替代从而产生的内容。

2.4 本章小结

第二章,介绍了什么是预处理、预处理的作用。还介绍如何进行预处理生成.i文件。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

概念:

编译是利用编译程序从源语言编写的源程序产生目标程序的过程,就是把高级语言变成计算机可以识别的2进制语言。编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析语法分析、语义检查和中间代码生成代码优化目标代码生成。

作用:

主要是进行词法分析语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。同时编译器将文本文件hello.i翻译成文本文件hello.s。

3.2 在Ubuntu下编译的命令

 

3.3 Hello的编译结果解析

3.31数据

1)常量:

Hello程序中一共有两个常量,且都是字符串常量。分别是

字符串 "用法: Hello 学号 姓名 秒数!\n" 与 字符串 "Hello %s %s\n"。

在hello.s文件中同样有与之相对应的表示。

 

 

 

 

Hello.s文件

显然.LC0与.LC1表示就是这两个字符串常量。为什么用.LCn表示呢?它其实是Local Constant 的缩写,用引用文件中后面出现的字节,也就是表示字符串。而.string中出现的难以看懂的数字其实是汉字UTF-8的编码。至于英文以及相应符号,因为与ASCII码的规则兼容,所以保持不变。

2)变量

图为声明本地变量i,并将其初始化为0。因为它是一个循环变量,经常被使用,因而被编译器放置在寄存器中储存。Ebx表明它使用低32位,也即四个字节的int型变量。

3.32赋值

        movl     $数字,     %ebx       #把立即数的值传送给32位的ebx寄存器。也就是对i进行赋值。

3.33算数操作

 

       其中addl命令是将左侧立即数的值加到ebx寄存器上,图中命令是ebx寄存器中的值加一,也就相当于hello.c中的i++语句。

3.34关系操作与控制转移

 

1

​​​​​​​ 

2

       分别对应于hello源程序中的“ != ”与 “ < ”。cmpl指令其实是令目的操作数与源操作数相减但不更改寄存器的值,而是更改相应的标准寄存器,之后jne或jle根据标志寄存器的值来进行跳转。图1中edi(源程序中的参数变量argc)与4相比较,不相等则跳转到.L6。图2中ebx(源程序中的变量i)与7相比较,小于等于则跳转到.L3。

3.35数组与指针

 

rcx与rdx分别对应着argv[1]、argv[2]的地址,rbp是*argv[]的地址,这表明对编译器而言数组与指针其实是一样的,都是用地址表示。

3.36函数操作

 

  1. 传参
    编译器通过事先将参数传入到esi、edi寄存器中,再在函数中使用。
  2. 调用
    通过call指令进行函数的调用,一般直接跳转到printf函数所在地址。但在.s文件中不显示函数地址。
  3. 返回
    ret指令用栈中的数据,修改IP的内容,实现近转移,也即返回调用前的位置。与调用相同,不显示地址。

3.4 本章小结

本章主要介绍了编译器在将hello.i文件转化为hello.s文件的过程。将hello.c中的变量,数据,操作转化成了汇编语言。通过阅读汇编语言可以对应到c语言中的变量,数据,操作等。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

概念:

       汇编器(as)hello.s文件翻译成二进制机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存到目标文件hello.o中。

作用:

生成可重定位目标文件。

4.2 在Ubuntu下汇编的命令

 

4.3 可重定位目标elf格式

ELF格式:

  1. ELF头:描述文件的总体格式。
  2. text节:已编译程序的机器代码。
  3. rodata节:只读数据,比如printf语句中的格式串和开关语句的跳转表
  4. data节:已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中。
  5. bss节:未初始化的全局和静态C变量。
  6. symtab节:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
  7. rel.text节:一个.text节中的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
  8. rel.data节:被模块引用或定义的所有全局变量的重定位信息。
  9. debug节:一个调试符号表,其条目是程序中定义的全局变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。
  10. line节:原始C源程序的行号和.text节中机器指令之间的映射
  11. strtab节:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。
  12. 节头部表:描述目标文件的节

 

Readelf的信息

4.31ELF

 

ELF

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

4.32可重定位相关

1)rel.text节与rel.data

当目标文件进行链接时,链接器将所有相同类型的节合并成同一类型的新的聚合节。在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。执行这一步,需要依赖于可重定位目标模块中称为重定位条目的数据结构。而代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中。

 

.rel.text

当前包含九个条目,我们能看到熟悉的puts、exit、sleep等函数,这些都是需要重定位的函数。有了它们就能确定运行时地址。

2)符号表

每个可重定位目标模块m都有一个符号表,它包含m定义和引用的符号信息。在链接器上下文中,有三种不同的符号:
       ·由模块m定义并能被其他模块引用的全局符号。全局链接器符号对应于非静态的c函数和全局变量。

·由其他模块定义并被模块m引用的全局符号。这些符号称为外部符号,对应于在其他模块中定义的非静态的c函数和全局变量。

·只被模块m定义和引用的局部符号。它们对应于带静态属性的c函数和全局变量。这些符号在m中可见,但不能被其他模块引用。

.symtab中的符号表不包含对应于本地非静态程序变量的任何符号,它们在运行时在栈中被管理。

 

符号表

4.4 Hello.o的结果解析

 

汇编代码

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

与hello.s相比

·反汇编代码前有与之对应的指令。一般而言,指令有操作数、目的寄存器、源寄存器、立即数等几个部分,这些部分与汇编语句的种类、源操作数、目的操作数、立即数相对应。

·反汇编代码以十六进制表示数字,hello.s文件中以十进制表示。两者对比如图。

 

反汇编代码

 

Hello.s文件

·反汇编代码调用函数在call后面会跟着下一条指令的地址(因为还未经过重定位确定最终位置),而hello.s文件中call后面根本没有地址,而是函数的名字。

·反汇编代码中字符串常量以立即数0表示(因为还未经过重定位确定最终位置,常量保存在.rodata中),而hello.s文件中以.LC1表示。

反汇编代码

 

Hello.s文件

4.5 本章小结

本节主要介绍了汇编的概念与作用,可重定位目标elf格式与其各节详细信息,还介绍了反汇编代码与hello.s文件的区别.

(第41分)

5章 链接

5.1 链接的概念与作用

概念:

链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。在早期的计算机系统中,链接是手动执行的。在现代系统中,链接是由叫做链接器的程序自动执行的。

作用:

链接使得分离编译成为可能,使我们无需将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需要加单地重新链接应用,而不必重新编译其他文件。

5.2 在Ubuntu下链接的命令

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

ELF头:

 

与hello.o的ELF头相类似,ELF头描述文件的总体格式,它还包含程序的入口点,也就是当程序运行时要执行的第一条指令的地址。

节头部表:

描述目标文件的节,同时给出各节的类型、地址、偏移量、大小等信息。

程序头:

可执行文件的连续的片(chunk)被映射到连续的内存段,而程序头部表描述了这种映射关系。

Section to Segment mapping:

Dynamic section

 

重定位节:

 

符号表:

 

版本信息:

 

5.4 hello的虚拟地址空间   

用edb加载hello,可以看到hello程序被加载到0x00400000——0x00400ff0段中。

 ​​​​​​​

 

由5.3的程序头可知

PHDR

保存程序头表。起始地址0x400000偏移0x40字节处、大小为0x2a0字节

 

INTERP

指定在程序已经从可执行文件映射到内存之后,必须调用的解释器。位于内存起始位置0x400000偏移0x2e0字节处,大小为0x1c个字节,记录了程序所用ELF解析器(动态链接器)的位置位于: /lib64/ld-linux-x86-64.so.2

 

其他:

 LOAD表示一个从二进制文件映射到虚拟地址空间的段。其中保存了常量数据(如字符串),程序的目标代码等等。 DYNAMIC段保存了其他动态链接器(即,INTERP中指定的解释器)使用的信息。NOTE保存了专有信息。它们都能从edbData Dump找到。

5.5 链接的重定位过程分析

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

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

不同:
      
·hello_out.txt比helloo.txt多了.plt节与.init节等。

·多出了一些函数。

·地址从相对地址变成了虚拟地址。

·call指令后添加了正确的地址。

·jmp等跳转指令后也填上了正确地址。

 

 

hello_out.txt

分析:

符号解析过程中,链接器将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。编译器当遇到一个不是在当前模块定义的符号时,会假设该符号是在其他某个模块中定义的,生成一个链接器符号条目,并把它交给链接处理。如果链接器在它的任何输入模块中都找不到这个被引用符号的定义,就输出一条错误信息并终止。

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

5.6 hello的执行流程

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

ld-2.27.so!_dl_start——0x7fce 8cc385c0

ld-2.27.so!_dl_init——0x7fce 8cc55630

hello!_start——0x400500

libc-2.27.so!__libc_start_main——0x7fce 8c855ab0

-libc-2.27.so!__cxa_atexit——0x7fce 8c877430

hello!_init——0x40064c

libc-2.27.so!_setjmp——0x7fce 8c884c10

hello!main——0x4007f3

hello!puts@plt——0x400820

hello!exit@plt——0x4008a1

hello!getchar@plt——不执行

hello!sleep@plt——不执行

libc-2.27.so!exit——0x7fce 8c8a2128

5.7 Hello的动态链接分析

现代系统使用可以加载而无需重定位的代码,也即位置无关代码(Position-Independent Code ,PIC),它可以加载到内存任何位置而无需链接器修改。使用这种方法,无限多个进程可以共享一个共享模块的代码段的单一副本。

数据段与代码段的相对距离总保持不变,利用这个事实,全局变量PIC引用使用了全局偏移量表(Global Offset Table ,GOT)。它处于数据段开始的地方,是一个数组,其中每个条目是8字节地址。

在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它;为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表(Procedure Linkage Table,PLT)和全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用got中地址跳转到目标函数。

从5.3的节头部表可以看到.got.plt节的位置

.got.plt

  

.got.pltdl_init

 

.got.pltdl_init

可以看出.got.plt节在dl_init前后发生了变化。

5.8 本章小结

本节介绍了链接的概念与作用,描述了用readelf看hello文件各节的内容,以及hello动态链接的过程。

(第51分)

6章 hello进程管理

6.1 进程的概念与作用

概念:

进程是正在运行的程序的实例,是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

作用:
       给与一个程序是系统中当前运行的唯一的程序的假象,程序能够独占地使用处理器与内存。处理器就好像是无间断地一条接一条地执行程序的命令,程序的代码与数据就像是系统内存的唯一对象。

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

作用:

shell是一个交互型应用级程序,代表用户运行其他程序(如Windows下的命令行解释器,cmd、powershell,图形界面的资源管理器),是信号处理的代表,负责各进程创建与程序加载运行及前后台控制,作业调用,信号发送与管理等。

处理流程:

shell读取命令,如果是内置命令就直接执行,如果不是,则创建子进程,加载命令程序运行,并根据用户的需求在前台或后台运行。在前台,则将控制权递给程序,等待程序的结束;在后台,则shell仍能继续输入与执行命令。当子程序终止后,将由shell回收子进程。

6.3 Hello的fork进程创建过程

int fork(void),子进程返回0,父进程返回子进程的PID,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本(代码、数据段、堆、共享库以及用户栈),以及与父进程任何打开文件描述符相同的副本。子进程有不同于父进程的PID。

当shell加载hello时,会先判断它是否是内置命令,判断不是,于是shell认为命令是一个可执行文件,于是shell创建一个子进程并调用execve()加载程序。

6.4 Hello的execve过程

intexecve(char*filename,char*argv[],char*envp[])

在当前进程中载入并运行程序:loader加载器函数,filename:可执行文件,目标文件或脚本(用#!指明解释器,如#!/bin/bash);argv:参数列表,惯例:argv[0]==filename;envp:环境变量列表。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC指向_start地址,_start最终调用hello中的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。

6.5 Hello的进程执行

上下文信息:

进程的物理实体(代码和数据等)和支持进程运行的环境合称为进程的上下文。由进程的程序块、数据块、运行时的堆和用户栈(两者通称为用户堆栈)等组成的用户空间信息被称为用户级上下文;由进程标识信息、进程现场信息、进程控制信息和系统内核栈等组成的内核空间信息被称为系统级上下文;处理器中各寄存器的内容被称为寄存器上下文(也称硬件上下文),即进程的现场信息。用户级上下文地址空间和系统级上下文地址空间一起构成了一个进程的整个存储器映像。

进程时间片:

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

进程调度过程:

内核为每一个进程维持一个上下文,上下文就是内核重新启动一个被抢占的进程所需的状态。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度,由内核内称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。

用户态与核心态转换:

为使操作系统内核提供一个无懈可击的进程抽象,处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。处理器通常是用某个控制寄存器的一个模式位来提供这种功能的,该寄存器描述了进程当前享有的特权。当设置了模式位时,进程就运行在内核模式中。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式。

6.6 hello的异常与信号处理

异常是指为响应某个事件将控制权转移到操作系统内核中的情况,一共有四种异常:硬件异常、陷阱、故障和终止。

硬件异常:处理器外部I/O设备引起,中断处理程序返回到下
一条指令处。

陷阱:有意的,执行指令的结果,陷阱处理程序将控制返回到下一条指令。

故障:不是有意的,但可能被修复,处理程序要么重新执行引起故障的指令(已修复),要么终止。

终止:非故意,不可恢复的致命错误造成,处理程序中止当前程序。

 

正常运行

 

中途按下ctrl + c

 

中途按下ctrl + z

 

运行Ps

 

运行Jobs

 

运行Pstree

 

运行fg

 

运行kill

异常与信号的处理:

正常运行下,按下ctrl + c会向shell发送SIGINT信号,shell捕获信号后会终止当前进程。之后回收该进程,删除其在内存中的痕迹。

而按下ctrl + z 会向shell发送SIGTSTP信号,shell捕获信号后会停止当前进程。并将其添加至jobs。

此时运行ps会发现当前进程多了一个hello,并且它的状态是停止;运行jobs则可以看到多了一个hello作业;运行pstree则能看到整个进程树,从中我们能找到当前的bash,并从它的分支找到hello。

运行fg将hello从后台转到了前台,并且shell向hello进程发送了SIGCONT信号,使得停止的hello进程继续运行。运行kill -9 %1,则是向hello进程发送了SIGINT信号,终止了hello进程,并被shell回收。

6.7本章小结

本章介绍了进程的概念、什么是shell、以及上下切换的过程与异常与信号的处理。

(第61分)

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:

逻辑地址是用户编程时使用的地址,分为段地址和偏移地址两部分。

线性地址:

CPU在保护模式下,“段基址+段内偏移地址”叫做线性地址。

虚拟地址:

虚拟内存被组织为一个存放在磁盘上的N个连续的字节大小的单元组成的数组,其每个字节对应的地址成为虚拟地址。

物理地址:

物理地址就是内存单元的绝对地址。

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

段寄存器(16位,用于存放段选择符):

CS(代码段),程序代码所在段;SS(栈段),栈区所在段;DS(数据段),全局静态数据区所在段。其他3个段寄存器ES、GS和FS可指向任意数据段。

段式管理:

段式管理就是把虚拟地址空间中的虚拟内存组织成一些长度可变的称为段的内存单元。每个段有三个参数定义:段基地址,指定段在线性地址空间中的开始地址。段偏移量:是虚拟地址空间中段内最大可用偏移地址。段属性:指定段的特性。如该段是否可读、可写或可作为一个程序执行,段的特权级等。在此基础上,处理器有两种寻址模式:实模式与保护模式。

保护模式

保护模式是现代计算机常用的寻址模式。保护模式下,将一个段地址进行分段,使用索引在描述符表中读取及地址。段标识符由16位长的字段组成,称为段选择符。

转化过程:

给定一个逻辑地址。将逻辑地址进行划分得到索引、TLRPL信息。选择是GDT还是LDT中的段,再根据相应的寄存器得到地址。寻找段描述符得到基地址。线性地址=基地址+偏移量。

实模式下,逻辑地址=线性地址=实际的物理地址。段寄存器存放真实段基址,给出32位地址偏移量,则可以访问真实物理内存。

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

页式管理:

和存储器层次结构中其他缓存一样,磁盘(较低层)上的数据被分割成块,这些块作为磁盘和主存(较高层)之间的传输单元。VM系统通过将虚拟内存分割为成虚拟页的大小固定的块来处理这个问题。每个虚拟页的大小为P = 2p字节。类似地,物理内存被分割为物理页,大小也为P字节。

为了判定一个虚拟页是否缓存在DRAM的某个地方,确定虚拟页存放在哪个物理页中等问题,软硬件联合了提供这些功能,包括操作系统、MMU中的地址翻译硬件和一个存放在物理内存中的叫页表的数据结构(页表将虚拟页映射到物理页)。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。操作系统负责维护页表的内容,以及在磁盘与DRAM之间来回传送页。

地址翻译:

地址翻译是一个N元素的虚拟地址空间(VAS)中的元素和一个M元素的物理地址空间(PAS)中元素的映射。CPU中存在一个控制寄存器为页表基址寄存器指向当前页表。n位的虚拟地址包含着p位的虚拟页面偏移和(n-p)位的虚拟页号。MMU利用VPN来选择适当的PTE,将页表条目中的物理页号和虚拟地址中的VPO串联起来就得到相对应的物理地址。

CPU执行过程:

1.处理器生成一个虚拟地址,并把它传送给MMU。

2.MMU生成PTE地址,并从高速缓存/主存请求得到它。

3.告诉缓存/主存向MMU返回PTE。

4.MMU构造物理地址,并把它传送给高速缓存/主存。

5.高速缓存/主存返回所请求的数据字给处理器。

 

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

 

上图给出了Core i7 MMU如何使用四级的页表来将虚拟地址翻译成物理地址。36位VPN被划分成四个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含Ll页表的物理地址。VPN1提供到一个Ll PET的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2PTE的偏移量,以此类推。

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

内存访问过程:

       Cpu产生虚拟地址传给MMU,MMU将PTEA传给L1cache,L1cache不命中,则再将PTEA传给L2cache,L2cache不命中,则再将PTEA传给L3cache,L3cache不命中,则再将PTEA传给内存,找到PTE将其一级一级传上去,将PTE加载到缓存中。MMU将得到的PTE进行翻译,如果PTE的有效位为0,则触发缺页异常处理程序。如果能正常翻译则将生成的物理地址传给三级cache/内存来取数据。

 

7.6 hello进程fork时的内存映射

fork为hello创建虚拟内存,创建当前进程的的mm_struct, vm_area_struct和页表的原样副本,两个进程中的每个页面都标记为只读,两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW),在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存,随后的写操作通过写时复制机制创建新页面。

 

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行新程序hello的步骤:

删除已存在的用户区域,创建新的区域结构,私有的、写时复制,代码和初始化数据映射到.text和.data区(目标文件提供),.bss和栈堆映射到匿名文件,栈堆的初始长度0,共享对象由动态链接映射到本进程共享区域,设置PC,指向代码区域的入口点,Linux根据需要换入代码和数据页面。

 

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

虚拟内存中,DRAM缓存不命中称为缺页。

处理流程:
       1.页面不命中触发缺页异常。

2.缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改,则把它换出到磁盘。

3.缺页处理程序页面调入到新的页面,并更新内存中的PTE。

4.缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放。C语言通过malloc函数来分配一个块,再通过free来释放这个块。

动态内存的分配方式:

带边界标签的隐式空闲链表分配器隐式空闲链表,它的每个空闲块中给出了四个字节的头部header,因为地址一定是8字节对齐,最后三位肯定为空的,所以最后三位中的第0位可以用来存放这个块是否分配(0/1)的信息。还可以加入一个脚部footer,脚部是头部一个副本,便于合并前面块,在合并时可以用来确定前一个块是不是空的,所以最小块大小是8个字节,而将空闲链表分配出去时可以不用加入footer,只需在header的空闲3位中再选1位用来作为前一块是否是空闲的标记(0/1)即可。中间部分有效载荷以及可能的一些额外的填充组成。分配器可以通过遍历堆中的所有块,从而间接地遍历整个空闲块的集合。

对于隐式空闲链表,有三种放置策略:首次适配,从头开始搜索空闲链表,选择第一个合适的空闲块;下次适配,从上一次查询结束的地方开始,选择第一个合适的空闲块;最佳适配,检查每个空闲块,选择适合所需请求大小的最小空闲块;当分配器释放一个已分配块时,可以选择立即合并或者推迟合并,一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。对于四种情况分别进行空闲块合并,只需要通过改变Header和Footer中的值就可以。

显示空闲链表:

将空闲块组织成链表形式的数据结构。堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。维护链表时,可以采用先进后出的方式,使得新释放的块放在量表的开始处,使用后进先出的顺序和首次适配的放置策略,分配器会检查最近使用过的块。另一种是按照地址顺序来维护链表,其中链表每个块的地址都小于它后继的地址。在这种情况下每释放一个快需要线性时间搜索来定位合适的前驱。

分离链表:

简单分离链表的每个等价类中的块大小是一样的,所以可以根据地址判断块大小,所以分配可以在常数时间内完成。因为块大小固定,所以不需要合并,只需要寻找不同的等价类就可以了,那么header和footer也不需要了,只需要一个能够指向后面块的指针就可以了。它分为简单分离链表、分离适配和伙伴系统几个方式。

7.10本章小结

本节主要介绍了虚拟内存的相关知识,即页式管理,段式管理,虚拟地址翻译,堆空间管理等。

(第7 2分)

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:

文件是C语言和Linux管理的思想,所有的IO设备都被抽象为文件,所有的输入输出都作为对文件的操作。

设备管理

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

8.2 简述Unix IO接口及其函数

Unix I/O接口:

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

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

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

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

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

Unix I/O函数:

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

2. int close(int fd), 关闭一个已经打开的文件,fd是需要关闭的文件的描述符,成功:返回0;失败:返回-1,并设置errno。

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

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

8.3 printf的实现分析

 

Printf函数

其中(char*)(&fmt) + 4) 表示的是...中的第一个参数。

 

Vsprintf函数

此函数的作用是按照格式fmt 结合参数args 生成格式化之后的字符串,并返回字串的长度。

而write函数的作用显然是将字符串输出到屏幕,要弄清怎么做到的,我不得不看它的底层实现。

 

Write函数

由我们学到的汇编知识,它先是传了几个参数,然后调用系统调用sys_call。要进一步弄清,仍要再看sys_call的底层。

 

Sys_call函数

可以看出代码里面的call是访问字库模板并且获取每一个点的RGB信息最后放入到eax也就是输出返回的应该是显示vram的值,然后系统显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

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

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

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

​​​​​​​ 

8.5本章小结

本节介绍了linux的IO管理,以及其各种函数。还分析了printf函数与getchar函数的实现。

(第81分)

结论

  1. 预处理:编译器预处理器根据以字符#开头的命令,修改C程序,得到hello.i文件。
  2. 编译:编译器将文本文件hello.i翻译成文本文件hello.s。
  3. 汇编:汇编器将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标文件的格式,并将结果保存到hello.o中。
  4. 链接:链接器将hello.o所需的外部库与hello.o链接起来,得到可执行目标文件。
  5. 加载运行:命令行中输入“./hello”后,shell将其解析为外部命令。于是shell调用fork()函数创建子进程。再调用execve()函数,加载hello程序。

删除子进程现有的虚拟内存段,创建一组新的段(段与栈初始化为0),并将虚拟地址空间中的页映射到可执行文件的页。

  1. 执行:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流。
  2. 访存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。printf会调用malloc向动态内存分配器申请堆中的内存。
  3. 信号:hello运行过程中发生了一个某种类型的事件,就会收到某种信号,并进行相应的处理。
  4. 回收:shell将子进程hello回收,内核将其痕迹删除干净。

不得不说,最为简单的hello程序,其实真的没有我们想象的那么简单,任何一种习以为常的规则,其底层都是集中了前人的智慧。

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

附件

hello.c:源程序
hello.i:预处理后的文本文件
hello.s:编译后汇编程序文本文件
hello.o:汇编后的可重定位目标程序(二进制文件)
hello:链接后的可执行目标文件
helloo.txt:hello.o的反汇编文件
hello_out.txt: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分)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值