程序人生-Hello’s P2P

目  录

第1章 概述............................................................................................................. - 4 -

1.1 Hello简介...................................................................................................... - 4 -

1.2 环境与工具..................................................................................................... - 4 -

1.3 中间结果......................................................................................................... - 4 -

1.4 本章小结......................................................................................................... - 4 -

第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.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的虚拟地址空间................................................................................ - 17 -

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

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

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

5.8 本章小结....................................................................................................... - 20 -

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

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

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

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

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

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

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

6.7本章小结....................................................................................................... - 26 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结..................................................................................................... - 35 -

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

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

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

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

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

8.5本章小结....................................................................................................... - 40 -

结论......................................................................................................................... - 40 -

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

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

第1章 概述

1.1 Hello简介

P2P: From Program to Process 。linux中,hello.c经过cpp的预处理、ccl的编译、as的汇编、ld的链接最终成为可执行目标程序hello,在shell中键入启动命令后,shell为其fork产生子进程的过程。

020: shell通过execve加载并执行hello,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。

1.2 环境与工具

硬件环境:X64 3.4GHz 8G RAM

软件环境:Windows 10 64位 Vmware workstation pro Ubuntu 16.04 LTS 32位

开发工具:gcc gedit Codeblocks gdb edb

1.3 中间结果

hello.c源文件

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

hello.s编译后的汇编文件

hello.o可重定位目标文件

hello.asm hello.o反汇编代码

hello.obj hello反汇编代码

hello可执行文件

1.4 本章小结

从一个程序被创建到他的执行会经历一个很漫长的过程,其中涉及到各种底层的实现方式,我们要善用linux中为我们提供的各种工具,对一个程序的实现有着深入了解。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

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

C预处理器扩展源代码,插入所有用#include命令指定的文件,并扩展所有用#define声明指定的宏。

  1. 宏定义
  2. 源文件文件包含
  3. 条件编译

2.2在Ubuntu下预处理的命令

gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析

在hello.c中只包含以下三条预处理指令

作用是文件包含。

通过-E指令,可以在hello.i文件中看到已经将以上三个文件的内容引入

2.4 本章小结

       预处理是C语言程序从源代码变成可执行程序的第一步,主要是C语言编译器对各种预处理命令进行处理,包含头文件的包含、宏定义的扩展、条件编译的选择等,便于程序的组织和调试和一些特殊的编程技巧的实现,是一项非常有用的功能。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译阶段是编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序,包含函数main的定义。

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析

       3.3.1: 伪代码字段

                    

                     在这一部分中:

.xxx代表伪指令,.text代表已编译程序的机器代码,.section .rodata这个段中声明的数据类型是只读数据,.align 8代表8字节对齐

      1. C程序在hello.s中的具体体现:
  1. 局部变量i:局部变量i存储在被调用者的栈帧中的寄存器或栈上,这里存在-4(%rbp)中。i的大小占4bytes,所以要将栈指针减4.

  1. 第一个局部变量argc:在x84-64指令集中,局部变量参数存储的寄存器有顺序安排,第一个参数argc为int类型4bytes32位,因此它存在寄存器%edi中。

  1. 第二个局部变量数组argv[]:main函数程序执行时从命令行中读取命令,argc代表着参数个数,argv[0] = filename,argv[n](n>0)代表着参数。在本程序中argv[1] argv[2] argv[3]代表着学号,姓名和秒数。而argv[]的类型为char*,也就是指向字符类型的指针,每一个元素的大小为8bytes。

  1. 字符串:在C程序中

如果argc不为4的话,会打印一个字符串,这个字符串就存储在.string节中

  1. 控制转移操作:C语句有一个if语句。而控制转移操作在汇编代码中是通过跳转指令(jxx)实现的,跳转指令会根据表达式的条件码确定是否执行跳转。

这里将4和argc比较,相等就跳转到.L2节,不相等的话就调用puts函数打印字符串。

  1. 算数操作:在汇编语言中有一系列指令来执行加减乘除、异或、或、与、左移、右移、加1、减1、取负取补等操作。他们的对象有两个操作数,根据不同的指令会对不同的操作数有着不同的要求。

本题中的算数操作主要是i的加1操作。有两种方式,一个是直接用指令INC,或者是用指令ADD将立即数$1和i所在的寄存器进行相加。

这里是用的第二种方式。

  1. 循环:在汇编语言中,循环还是基于跳转指令(jxx),循环的三种形式for、while、dowhile循环在汇编语言中有不同的结构。实现的方法也各有不同。但是他们的思想都是统一的,就是在满足边界的条件下不断跳转到循环体loop,一旦不满足就跳转到gone。在本C程序中,采用的是for循环。

这里把i和立即数7做比较,只要满足i<=7的,就不断的跳转到.L4,直到i=8跳过jle语句,调用getchar函数。

      1. C程序中函数在汇编语言中的具体实现:

在上面我们分析了这个C程序中的具体语句,但是语句是基于函数的,在汇编语句中还要弄明白几个函数之间的分块以及彼此的调用关系。

首先,调用一个函数是通过call指令实现的。那么如果一个函数在运行时需要外部或内部的参数,我们就需要在调用函数前为这个函数准备好参数,在上面我们总结了寄存器是按照一定的顺序存储函数中的参数的,比如第一个参数就存于%rdi、第二个参数存于%rsi,具体可以参考书上的表格。

在本程序中,拿atoi()函数举例,它的参数是argv[3],存于-8(%rbp)中,所以可以在hello.s中看到在调用它前已经为它准备好了参数。

3.4 本章小结

在这个章节中,我们主要分析了编译器处理一个C程序的基本过程,需要的基础知识是要了解C语言的语句在汇编语言中的表示,以及汇编语言中的程序编码、数据格式、访问信息、算数和逻辑操作、控制、过程等机器级表示方法。在优化等级不同的情况下,产生的汇编代码是不同的,但是他们都是一个C程序的底层实现,在不同的体系结构下,汇编语言的差别也非常大,在这里只分析x86-64下的汇编语言。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

使用汇编器将汇编文件hello.s转换成目标文件hello.o

目标文件hello.o转化为二进制文件

实质上就是将汇编文件.s中的机器指令转化为二进制代码,使机器可以识别并且且执行。

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

    用readelf -a hello.o来查看elf文件中的全部内容。

首先elf可重定位目标文件以ELF头开始,它是以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中的每个节都有一个固定大小的条目。

对于hello.o的头文件,包含了ELF文件类别、数据、入口点地址、程序头起点、以及字节顺序等等内容。

夹在ELF头和节头部表之间的都是节,包含.text(已编译程序的机器代码)、.rodata(只读数据)、.data已初始化的全局和静态C变量。局部C变量在栈中。.bss未初始化的全局和静态C变量,以及所有初始化为0的全局或静态变量。.symtab一个符号表,存放在程序中定义和引用的函数和全局变量的信息。

4.4 Hello.o的结果解析

用objdump工具得到hello.o的反汇编。

首先它告诉我们hello.o文件是一个elf64-x86-64文件。

在这里只包含.text节,也就是已编译的机器代码节。

左边第一列是行号,第二列是偏移量,第三列是十六进制字节数,他们右边的汇编语言的编码,每条汇编语言的编码具有唯一性。

  1. 机器语言与汇编语言的映射关系:机器语言实际上就是对汇编语言的编码,每一条汇编代码都有唯一的机器级编码。
  2. 操作数:hello.o中立即数是以16进制表示的,在hello.s中以十进制表示
  3. 函数调用:在hello.s中函数调用直接call函数的名称,在hello.o中call指令的目标地址是当前指令的下一条指令
  4. 分支转移:在hello.s中将整个函数分为不同的部分,.L2,.L3.L4等,作为跳转指令的目标;在hello.o中则将地址作为跳转指令的目标。

4.5 本章小结

汇编是非常重要的一个部分,它将人类可以看懂的语言转化为机器可以识别的二进制代码,从而完成了一个程序在机器中执行的一个重要步骤。我们也可以通过readelf和objdump等工具来阅读elf格式文件和反汇编文件,从而对一个C程序的底层实现有更为深入的认识。

(第41分)

5章 链接

5.1 链接的概念与作用

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

5.2 在Ubuntu下链接的命令

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

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

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

在ELF头中包含了一些基本信息。

其他和第四节中的基本相同。

5.4 hello的虚拟地址空间

 

在edb中打开symbolviewer查看虚拟各段的信息,与5.3中的信息对比发现和readelf查看结果相同,此外也包含了一些按照地址顺序的一些其他信息。

5.5 链接的重定位过程分析

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

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

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

可以看到重定位后的hello.o也是一个elf64-x86-64文件。不过此时里面多了很多节,在hello.txt中只有.text节,而在hello-r.txt中多了.init节,.plt节,.plt.got节,.plt.sec节,.text节,.finit节。

链接后增加了hello.c中用到的函数,puts,printf,getchar,exit,atoi,sleep。

在函数调用的时候,hello中没有重定位条目,跳转和函数调用的地址全部变成虚拟地址。这些地址是hello.o依靠重定位条目链接后才能确定的。

访问变量时,在链接后变成了虚拟地址,而在hello.o仍然依靠重定位,并且操作数全部是置零的。

链接过程分析:

  1. 符号解析:链接器解析符号引用的方法就是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。
  2. 重定位:一旦链接器完成了符号解析这一步,就把代码中的每个符号引用和正好一个符号定义关联起来,此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。然后就开始重定位步骤,在这个步骤中,将合并输入模块,并为每个符号分配运行时地址。它由两部分构成,重定位节和符号引用,重定位节中的符号引用。
  3. 重定位节和符号引用:在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,来自所有输入模块的.data节被全部合并为一个节,这个节成为输出的可执行目标文件的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。
  4. 重定位节中的目标引用:在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。

5.6 hello的执行流程

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

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

0x400430   init;

0x400460   puts@plt;

0x400470   printf@plt;

0x400480   __libc_start_main@plt;

0x400490   getchar@plt;

0x4004a0   exit@plt;

0x4004b0   sleep@plt;

0x4004d0   _start;

0x4004fe   main;

0x400580   __libc_csu_init;

0x4005f0   __libc_csu_fini;

0x4005f4   _fini;

5.7 Hello的动态链接分析

hello程序的动态链接项目:global_offset表 。

图5-5 do_init前

图5-6 do_init后

如图5-5和图5-6,在do_init前后global_offset表发生变化。

在edb调试之后我们发现原先0x006008c0开始的global_offset表是全0的状态,在执行过_dl_init之后被赋上了相应的偏移量的值。这说明dl_init操作是给程序赋上当前执行的内存地址偏移量 。

5.8 本章小结

链接将一个可重定位目标文件变为一个可执行目标文件,加入了一个文件所需的外部库以及函数。理解链接有助于我们更好的理解一个程序的执行过程。我们也可以用objdump和edu等工具去探究链接前后文件的不同之处。

(第51分)

6章 hello进程管理

6.1 进程的概念与作用

进程的概念:一个执行中的程序的实例,同时也是系统进行资源分配和调度的基本单位。一般情况下,包括文本区域、数据区域和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。

进程的作用:它提供一个假象,好像我们的程序独占地使用内存系统,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

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

作用:shell-bash是一个C语言程序,它代表用户执行进程,它交互性地解释和执行用户输入的命令,能够通过调用系统级的函数或功能执行程序、建立文件、进行并行操作等。同时它也能够协调程序间的运行冲突,保证程序能够以并行形式高效执行。bash还提供了一个图形化界面,提升交互的速度。

处理流程:

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

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

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

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

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

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

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

6.3 Hello的fork进程创建过程

在终端中输入./hello 1190201912 赵岩 4

接下来shell会分析这条命令,由于./hello不是一条内置的命令,于是判断./hello的语义是执行当前目录下的可执行目标文件hello,然后Terminal会调用fork床架一个新的运行的子进程,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,这就意味着,当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程与子进程之间的区别在于它们拥有不同的PID。

6.4 Hello的execve过程

在fork之后,子进程调用execve函数,execve函数在新创建的子进程的上下文中加载并运行hello程序。execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有发生错误时execve才会返回到调用程序。所以,execve调用一次且从不返回。

加载并运行hello需要以下几个步骤:

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

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

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

(4)设置程序计数器。设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。

6.5 Hello的进程执行

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

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

系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

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

处理器通常用某个控制寄存器的一个模式位来提供用户模式和内核模式的功能。设置了模式位时,进程就运行在内核模式中,该进程可以执行指令集中的任何指令,可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中,用户模式中的进程不允许执行特权指令。

在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程的决定叫做调度。如图6.2,上下文切换的流程是:1.保存当前进程的上下文。2.恢复某个先前被抢占的进程被保存的上下文。3.将控制传递给这个新恢复的进程。

然后分析hello的进程调度,hello在刚开始运行时内核为其保存一个上下文,进程在用户模式下运行,当没有异常或中断信号的产生,hello将一直正常地执行,而当出现异常或系统中断时,内核将启用调度器休眠当前进程,并在内核模式中完成上下文切换,将控制传递给其他进程。

当程序在执行sleep函数时,系统调用显式地请求让调用进程休眠,调度器抢占当前进程,并发生上下文切换,将控制转移到新的进程,此时计时器开始,当计时器达到传入的第四个参数大小(这里是1s)时,产生一个中断信号,中断当前正在进行的进程,进行上下文切换恢复hello的上下文信息,控制会回到hello进程中。当循环结束后,程序调用getchar函数用getchar时,由用户模式进入内核模式,内核中的陷阱处理程序请求来自键盘缓冲区的信号传输,并执行上下文切换把控制转移给其他进程。数据传输结束之后,引发一个中断信号,控制回到hello进程中,执行return,进程终止。

6.6 hello的异常与信号处理

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

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

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

hello在执行过程中可能会出现四种异常:中断、故障、陷阱、终止。

(1)中断是来自I/O设备的信号,异步发生,中断处理程序对其进行处理,返回后继续执行调用前待执行的下一条代码,就像没有发生过中断。

(2)陷阱是有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。

(3)故障是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。

(4)终止是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将控制返回给一个abort例程,该例程会终止这个应用程序。

可能会出现的信号:

hello对各种信号的处理:

  1. 正常情况:程序正常进行,运行完成后被回收。

  1. 不停乱嗯:可以看到乱按可以将输入的内容保存在缓冲区。

  1. 按下Ctrl-C:向进程发送SIGINT信号。信号处理程序终止并回收进程。

  1. 按下Crtl-Z:当按下Ctrl-Z之后,shell进程收到SIGSTP信号,信号处理函数的逻辑是打印屏幕回显、将hello进程挂起,通过ps命令我们可以看出hello进程没有被回收,其进程号时7308,用jobs命令看到job ID是1,状态是Stopped,使用fg 1命令将其调到前台,此时shell程序首先打印hello的命令行命令,然后继续运行打印剩下的信息,之后再按下Ctrl-Z,将进程挂起。

之后,用kill函数向hello进程发送终止信号。

发现hello进程已被杀死。

6.7本章小结

本章主要介绍了进程的概念与作用,阐述了shell的作用和处理流程以及hello的fork进程的创建过程和execve的过程,最后分析了hello的执行过程和过程中出现的异常的处理。

(第61分)

7章 hello的存储管理

7.1 hello的存储器地址空间

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

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

逻辑地址:在有地址变换功能的计算机中,访内指令给出的地址(操作数)叫逻辑地址,也叫相对地址。分为两个部分,一个部分为段基址,另一个部分为段偏移量。

线性地址:逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式。分页机制中线性地址作为输入。

虚拟地址:CPU启动保护模式后,程序运行在虚拟地址空间中。与物理地址相似,虚拟内存被组织为一个存放在磁盘上的N个连续的字节大小的单元组成的数组,其每个字节对应的地址成为虚拟地址。虚拟地址包括VPO(虚拟页面偏移量)、VPN(虚拟页号)、TLBI(TLB索引)、TLBT(TLB标记)。

物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。

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

1.基本原理:

在段式存储管理中,将程序的地址空间划分为若干个段,这样每个进程有一个二维的地址空间。在段式存储管理系统中,为每个段分配一个连续的分区,而进程中的各个段可以不连续地存放在内存的不同分区中。程序加载时,操作系统为所有段分配其所需内存,这些段不必连续,物理内存的管理采用动态分区的管理方法。

在为某个段分配物理内存时,可以采用首先适配法、下次适配法、最佳适配法等方法。在回收某个段所占用的空间时,要注意将收回的空间与其相邻的空间合并。

程序通过分段划分为多个模块,如代码段、数据段、共享段:可以分别编写和编译;可以针对不同类型的段采取不同的保护;可以按段为单位来进行共享,包括通过动态链接进行代码共享。这样做的优点是:可以分别编写和编译源程序的一个文件,并且可以针对不同类型的段采取不同的保护,也可以按段为单位来进行共享。

总的来说,段式存储管理的优点是:没有内碎片,外碎片可以通过内存紧缩来消除;便于实现内存共享。缺点与页式存储管理的缺点相同,进程必须全部装入内存。

2.段式管理的数据结构:

为了实现段式管理,操作系统需要如下的数据结构来实现进程的地址空间到物理内存空间的映射,并跟踪物理内存的使用情况,以便在装入新的段的时候,合理地分配内存空间。

(1)进程段表:描述组成进程地址空间的各段,可以是指向系统段表中表项的索引。每段有段基址,即段内地址。在系统中为每个进程建立一张段映射表,其结构如图。

(2)系统段表:系统所有占用段(已经分配的段)。

(3)空闲段表:内存中所有空闲段,可以结合到系统段表中。

3.段式管理的地址变换

在段式管理系统中,整个进程的地址空间是二维的,即其逻辑地址由段号和段内地址两部分组成。为了完成进程逻辑地址到物理地址的映射,处理器会查找内存中的段表,由段号得到段的首地址,加上段内地址,得到实际的物理地址。这个过程也是由处理器的硬件直接完成的,操作系统只需在进程切换时,将进程段表的首地址装入处理器的特定寄存器当中。这个寄存器一般被称作段表地址寄存器。

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

1.基本原理

将程序的逻辑地址空间划分为固定大小的页,而物理内存划分为同样大小的页框。程序加载时,可将任意一页放入内存中任意一个页框,这些页框不必连续,从而实现了离散分配。该方法需要CPU的硬件支持,来实现逻辑地址和物理地址之间的映射。在页式存储管理方式中地址结构由两部构成,前一部分是VPN(虚拟页号),后一部分是VPO(虚拟页偏移量)。如图所示。

页式管理方式的优点:没有外碎片;一个程序不必连续存放;便于改变程序占用空间的大小(主要指随着程序运行,动态生成的数据增多,所要求的地址空间相应增长)。页式管理方式的缺点:要求程序全部装入内存,没有足够的内存,程序就不能执行。

2.页式管理的数据结构

在页式系统中进程建立时,操作系统为进程中所有的页分配页框。当进程撤销时收回所有分配给它的页框。在程序的运行期间,如果允许进程动态地申请空间,操作系统还要为进程申请的空间分配物理页框。操作系统为了完成这些功能,必须记录系统内存中实际的页框使用情况。操作系统还要在进程切换时,正确地切换两个不同的进程地址空间到物理内存空间的映射。这就要求操作系统要记录每个进程页表的相关信息。为了完成上述的功能,—个页式系统中,一般要采用如下的数据结构。

页表:页表将虚拟内存映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。页表是一个页表条目(PTE)的数组。虚拟地址空间的每个页在页表中一个固定偏移量处都有一个PTE。假设每个PTE是由一个有效位和一个n位地址字段组成的。有效位表明了该虚拟页当前是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置,这个物理页中缓存了该虚拟页。如果没有设置有效位,那么一个空地址表示这个虚拟页还未被分配。否则,这个地址就指向该虚拟页在磁盘上的起始位置。

  1. 页式管理地址变换

MMU利用VPN来选择适当的PTE,将列表条目中PPN和虚拟地址中的VPO串联起来,就得到相应的物理地址。

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

为了消除每次CPU产生一个虚拟地址MMU就查阅一个PTE带来的时间开销,许多系统都在MMU中包括了一个关于PTE的小缓存叫做后备缓冲器TLB,它的速度略快于L1 Cache。

TLB通过虚拟地址VPN部分进行索引,分为索引(TLBI)与标记(TLBT)两个部分。这样,MMU在读取PTE时会直接通过TLB,如果不命中再从内存中将PTE复制到TLB。

同时,为了减少页表太大而造成的空间损失,可以使用层次结构的页表页压缩页表大小。如图所示。

Core i7使用的是四级页表。如图7.9所示,在四级页表层次结构的地址翻译中,虚拟地址被划分为4个VPN和1个VPO。每个第i个VPN都是一个到第i级页表的索引,第j级页表中的每个PTE都指向第j+1级某个页表的基址,第四级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问四个PTE。

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

通过7.3和7.4两节,hello的物理地址已经得知,现在需要访问该物理地址。在现代计算机中,存储器被组织成层次结构,因为这样可以最大程度地平衡访存时间和存储器成本。所以在CPU在访存时并不是直接访问内存,而是访问内存之前的三级cache。已知Core i7的三级cache是物理寻址的,块大小为64字节。LI和L2是8路组相联的,而L3是16路组相联的。Corei7实现支持48位虚拟地址空间和52位物理地址空间。

得到了52位物理地址,接下来CPU把地址发送给L1,因为L1块大小为64字节,所以B=64,b=6。又L1是8路组相联的,所以S=8,s=3。标记位t有52-6-3=43位,即是得到的52位物理地址的前43位。首先,根据物理地址的s位组索引索引到L1 cache中的某个组,然后在该组中查找是否有某一行的标记等于物理地址的标记并且该行的有效位为1,若有,则说明命中,从这一行对应物理地址b位块偏移的位置取出一个字节,若不满足上面的条件,则说明不命中,需要继续访问下一级cache,访问的原理与L1相同,若是三级cache都没有要访问的数据,则需要访问内存,从内存中取出数据并放入cache。

7.6 hello进程fork时的内存映射

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

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

缺页故障是一种常见的故障,当指令引用一个虚拟地址,在MMU中查找页表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出的时候就会发生故障。其处理流程遵循图所示的故障处理流程。

缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送VA到MMU,这次MMU就能正常翻译VA了。

7.9动态存储分配管理

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

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

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

在程序运行时程序员使用动态内存分配器(比如malloc)获得虚拟内存。动态内存分配器维护者一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

分配器的类型有两种:显式分配器和隐式分配器。

显式分配器:要求应用显式地释放任何已分配的块。例如,C语言中的malloc函数申请了一块空间之后需要free函数释放这个块

隐式分配器:应用检测到已分配块不再被程序所使用,就释放这个块。比如Java,ML和Lisp等高级语言中的垃圾收集。

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

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

7.10本章小结

本章主要介绍了hello的存储地址空间、intel的段式管理,以及TLB与四级页表支持下的VA到PA的变换过程和三级Cache支持下的物理内存访问。还阐述hello进程fork和execve时的内存映射、缺页故障的处理流程和动态存储分配器的管理。

(第7 2分)

8章 hello的IO管理

8.1 Linux的IO设备管理方法

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

设备的模型化:文件

设备管理:unix io接口

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

B0,B1,`````Bk,````Bm-1

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

8.2 简述Unix IO接口及其函数

Unix IO接口:

打开文件,内核返回一个非负整数的文件描述符,通过对此文件描述符对文件进行所有操作。

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

改变当前的文件位置,文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。

读写文件,读操作:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k

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

Unix IO函数:

1. open()函数

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

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

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

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

2. close()函数

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

所需头文件: #include

函数原型:int close(int fd)

参数:fd文件描述符

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

3. read()函数

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

所需头文件: #include

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

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

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

4. write()函数

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

所需头文件: #include

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

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

5. lseek()函数

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

所需头文件:#include ,#include

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

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

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

8.3 printf的实现分析

首先来看看printf函数的函数体。 调用printf函数的时候,先是最右边的参数入栈。fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素。fmt也是个变量,它的位置,是在栈上分配的,它也有地址。

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

   {

   int I;

   va_list arg = (va_list)((char*)(&fmt) + 4);

   i = vsprintf(buf, fmt, arg);

   write(buf, i);

   return i;

   }

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

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

int vsprintf(char *buf, const char *fmt, va_list args)

   {

    char* p;

    char tmp[256];

    va_list p_next_arg = args;

    for (p=buf;*fmt;fmt++) {

    if (*fmt != '%') {

    *p++ = *fmt;

    continue;

    }

    fmt++;

    switch (*fmt) {

    case 'x':

    itoa(tmp, *((int*)p_next_arg));

    strcpy(p, tmp);

    p_next_arg += 4;

    p += strlen(tmp);

    break;

    case 's':

    break;

    default:

    break;

    }

    }

    return (p - buf);

   }

从上面vsprintf函数可以看出,这个函数的作用是将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。

让我们追踪下write吧:

write:

mov eax, _NR_write

mov ebx, [esp + 4]

mov ecx, [esp + 8]

int INT_VECTOR_SYS_CALL

一个int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。

sys_call的实现:

sys_call:

call save

push dword [p_proc_ready

sti

push ecx

push ebx

call [sys_call_table + eax * 4]

add esp, 4 * 3

mov [esi + EAXREG - P_STACKBASE], eax

cli

ret

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

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

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

8.4 getchar的实现分析

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

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

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

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

再看getchar的代码如图8.5。可以看到,getchar调用了read函数,read函数也通过sys_call调用内核中的系统函数,将读取存储在键盘缓冲区中的ASCII码,直到读到回车符,然后返回整个字符串,getchar函数只从中读取第一个字符,其他的字符被缓存在输入缓冲区。

8.5本章小结

本章介绍了Linux中I/O设备的管理方法,UnixI/O接口和函数,并且分析了printf和getchar函数是如何通过UnixI/O函数实现其功能的。

(第81分)

结论

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

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

(1)编写,通过codeblocks写一个C程序hello.c

(2)预处理,经过预处理器cpp的预处理,处理以#开头的行,得到hello.i

(3)编译,编译器ccl将得到的hello.i编译成汇编文件hello.s

(4)汇编,汇编器as又将hello.s翻译成机器语言指令得到可重定位目标文件hello.o

(5)链接,链接器ld将hello.o与动态链接库链接生成可执行目标文件hello,至此,hello成为了一个可以运行的程序。

(6)运行,在shell中输入./hello 1190201912 赵岩 1

(7)创建子进程,shell进程调用fork为其创建子进程

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

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

(10)访问内存,当CPU访问hello时,请求一个虚拟地址,MMU把虚拟地址转换成物理地址并通过三级cache访存。

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

(12)信号,hello运行过程中可能遇到各种信号,shell为其提供了各种信号处理程序。

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

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

附件

hello.c源文件

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

hello.s编译后的汇编文件

hello.o可重定位目标文件

hello.asm hello.o反汇编代码

hello.obj hello反汇编代码

hello可执行文件

(附件0分,缺失 -1分)

参考文献

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

[1]  兰德尔 E.布莱恩特. 深入理解计算机系统. 龚奕利 译

[2]  printf 函数实现的深入剖析 https://www.cnblogs.com/pianist/p/3315801.html

[3]  库函数getchar()详解https://blog.youkuaiyun.com/hulifangjiayou/article/details/40480467

(参考文献0分,缺失 -1分)

【A股温度计】www.agwdj.com 镜像版程序V1.0说明 •通过数据可视化技术,将复杂的A股市场数据转化为直观的图形界面,帮助投资者快速把握市场脉搏。 【核心功能】 •全景视角:突破信息碎片化局限,快速定位涨跌分布,一眼锁定今日热点板块 •板块排序:基于申万行业分类标准,对31个一级行业和131个二级行业实时动态排序 •硬件适配:智能适配不同分辨率屏幕,4K以上屏幕显示信息更多(视觉更佳) •智能缩放:A股全图让大A市场5000+个股同屏显示(支持鼠标滚轮及触摸设备5级缩放) 【三秒原则】 •三秒看懂:通过精心设计的视觉图形,让用户在三秒内看清市场整体状况 •三秒定位:智能算法让大成交额个股和热点板块自动靠前,快速定位机会 •三秒操作:极简的界面,让用户减少操作 【使用场景】 •盘前准备:快速了解隔夜市场变化,制定当日策略 •盘中监控:实时跟踪市场动向,及时把握当日机会 •盘后复盘:全面分析当日市场表现,总结经验教训 【适合人群】 •个人用户:快速了解市场整体趋势变化,辅助决策 •专业人员:获取每天市场的数据云图支持研究工作 •金融机构:作为投研系统的可视化补充组件 •财经媒体:制作专业市场分析图表和报道 【市场切换】 •默认加载"A股全图",可切换单独显示的类型如下: •上证A股/深证A股/北证A股/创业板/科创板/ST板块/可转债/ETF 【程序优势】 •运行环境:纯PHP运行(无需安装任何数据库) •数据更新:实时同步→A股温度计→www.agwdj.com •显示优化:自动适配8K/4K/2K/1080P等不同分辨率的屏幕 •设备兼容:对市面上主流的设备及浏览器做了适配(检测到手机/平板/电视等默认Chrome/Firefox/Edge内核过低的情况会自动提示) 【其他说明】 •A股温度计程序演示网址:https://www.agwdj.com
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值