哈尔滨工业大学计算机系统大作业

摘  要

本实验主要对hello这一c程序的整个生命周期展开研究。我们从hello.c源程序为起点,从预处理、编译、汇编、链接,到加载、运行,再到终止、回收逐一进行分析综合,并结合对《深入理解计算机系统》一书的内容及计算机系统课上老师的讲授,在Ubuntu系统下对hello程序展开编译、链接、调试、运行等实际操作,顺着hello.c文件在计算机中执行的生命周期,在整个计算机系统中漫游,并把计算机系统的体系整体串联在一起

关键词:Linux;hello程序;生命周期;计算机系统;

目  录

第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简介

P2P:首先hello.c 利用I/O设备通过总线存储进内存中。GCC编译器驱动然后读取源程序文件hello.c,通过预处理器cpp转化为hello.i,再利用编译器ccl读入hello.s(汇编语言),然后用as将其汇编,转化为机器友好的二进制代码并保存在hello.o中。最后,它通过链接器ld与标准C库动态链接,并最终成为hello可执行目标程序。

接下来是在shell中输入字符串“./hello”。shell 程序将字符串读入寄存器并解析。然后shell调用fork函数来创建一个新的子进程。子进程是父进程shell的副本,再通过execve函数调用启动加载器。加载程序删除子进程现有的虚拟内存段,然后使用mmap函数创建新的内存区域,创建一组新的代码、数据、堆栈段。新的堆栈段被初始化为零。通过将虚拟地址空间中的页面映射到可执行文件的页面大小块,新的代码和数据段被赋值为可执行文件的对应内容。最后,加载器跳转到_start的地址运行应用main函数。

O2O:shell使用execve函数运行hello程序,映射虚拟内存,并从程序入口开始载入物理内存,再进入main函数执行目标代码,此时CPU为运行的hello分配时间片执行逻辑控制流,并通过流水线机制运行该程序,在此过程中,计算机通过TLB、4级页表、3级Cache,Pagefile等机制加速hello程序的运行,程序结束后,shell父进程负责回收hello进程,内核删除相关的数据结构。

1.2 环境与工具

硬件环境:CPU:Intel Core i7-9300H 2.40GHz;RAM:16GB

软件环境:Windows10 64位;Ubuntu 16.04.2 LTS

开发与调试工具:objdump,gcc,as,ld,edb,readelf,VScode

1.3 中间结果

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

文件名

文件的作用

hello.i

预处理之后的文件

hello.s

编译之后的文件

hello.o

汇编之后的文件

hello

链接之后的文件

hello_elf

hello.o的elf格式

hellold_elf

hello的elf格式(链接之后)

hello_objdump

hello.o的反汇编代码

hello_objdump0

hello的反汇编代码(链接之后)

1.4 本章小结

本章主要简单介绍了hello的P2P,020过程,列出了本次实验的环境、中间结果。也列出了该篇论文完成所需要生成的一些中间文件,为后续实验提供了基本思路。第一章简单解释了P2P和020的概念,说明了本次大作业的环境和工具,列出了为编写本论文,生成的中间结果文件并解释了其作用。

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:预处理器(cpp)是根据以字符#开头的命令,修改原始的C程序:是指在进行编译的第一遍扫描之前所做的工作。该过程由预处理程序负责完成。当对一个c源文件进行预处理时,系统自动引用预处理程序以解析以字符#开头的预处理命令,比如#include<stdio.h> 等命令来修改原始的C程序,待预处理进行完毕之后自动进入对源程序的编译。

预处理的主要作用如下:

1.删除宏定义“#define”展开并解析所定义的宏。

2.处理所有条件预编译指令,如“#if”,“#ifdef”, “#endif”等。

3.插入include后面的文件到“#include”处。

4.删除所有的注释“//”和“/* */”。

2.2在Ubuntu下预处理的命令

命令内容:>cpp hello.c hello.i

2.3 Hello的预处理结果解析

观察hello.i文件可以发现,文件main函数之前的内容变多,main函数C语言程序文本文件,只是对原文件中的宏进行了宏展开,头文件中的内容被加入此文件中。如果代码中有#define命令还会替换程序中对相应的符号。

该文件扩展到了3061行之多,且原本的C代码被放置在了整个文件的末尾。

2.4 本章小结

本章介绍了linux环境下对C语言程序进行预处理的命令,同时简要介绍了预处理的概念和作用,然后用简单的hello程序实际演示了从hello.c到hello.i的过程并结合具体代码对预处理结果进行了简单的分析。

第3章 编译

3.1 编译的概念与作用

编译的概念:编译是利用编译器从hello.i产生汇编文本文件hello.s的过程。主要包含五个阶段:词法分析、语法分析、语义检查、中间代码生成、目标代码生成。

编译的作用:将文本文件hello.i翻译成文本文件hello.s,并提示出现的语法错误。

3.2 在Ubuntu下编译的命令

命令内容:>gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析

3.3.1 数据

局部变量:作为函数中的局部变量i被储存在栈中,栈地址:%rbp-4

argc:传入的参数,存在栈里,位于%rbp-20。

argv[]:传入的数组,存在栈里,argv的地址位于%rbp-32,利用argv的地址加i*8,就能得到argv[i]。

立即数:储存在.data段中,在运行需要时加入寄存器中,如果无空闲寄存器则放入栈中。

表达式:存在代码段的.rodata中

3.3.2 赋值

为栈上的局部变量i赋初值=0,用movl赋值。

movl     $0, -4(%rbp)

3.3.3 类型转换

atoi函数将字符型argv[3]转换为整型数。

3.3.4 算术操作

编译器将i++编译成

addl $1, -4(%rbp)

3.3.5 关系操作

编译器将i<8与跳转编译成

cmpl $7, -4(%rbp)

jle .L4

将argc!=4编译成

cmpl $4, -20(%rbp)

je .L2

3.3.6 数组/指针/结构操作

argv数组是传入的参数,储存在栈上。

初始地址位于%rbp-32,利用argv的地址加i*8,就能得到argv[i]

3.3.7 控制转移

编译器将if,for等控制转移语句都使用了cmp来比较然后使用了条件跳转指令来跳转。编译器将if(argc!=3)编译成:

cmpl $3, -20(%rbp)

je .L2

将for循环里面的比较和转移编译成:

cmpl $9, -4(%rbp)

jle .L4

3.3.8 函数操作

编译器将printf("用法: Hello 学号 姓名 秒数!\n");编译为:

将printf("Hello %s %s\n",argv[1],argv[2]);编译为:

将sleep(atoi(argv[3]));编译为:

3.4 本章小结

本章介绍了linux环境下对C语言程序进行预处理之后的文件进行编译的命令,同时简要介绍了编译的概念和作用,然后用简单的hello程序实际演示了从hello.i到hello.s的过程并结合具体代码对编译结果进行了简单的分析,通过源程序与汇编语言程序的对比,简要说明了编译器是怎么处理C语言的各个数据类型以及各类操作的,分数据,赋值,算数操作,关系操作,数组,控制转移,函数操作等方面按照类型和操作进行了分析。

第4章 汇编

4.1 汇编的概念与作用

汇编的概念:汇编器(as)将.s汇编程序翻译成机器语言,把这些机器语言指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中。这个过程就叫做汇编。

汇编的作用:将汇编语言翻译成机器语言,因为机器语言是计算机能直接识别和执行的一种语言。

4.2 在Ubuntu下汇编的命令

命令内容:>gcc -c hello.s -o hello.o

4.3 可重定位目标elf格式

命令:readelf -a hello.o > hello.elf

4.3.1 ELF

描述了生成该文件的系统的字大小、字节顺序(大/小)、ELF头的大小、 目标文件的类型、机器类型(如x86-64)、节头部表的偏移、节头部表中条目的大小和数量。

4.3.2 节头部表

节头部表描述了不同节的偏移量和大小,其中目标文件中每个节都有一个固定大小的条目。具体的描述包括节的名称、类型、地址和偏移量等。

4.3.3 重定位节

表述了各个段引用的外部符号等,在链接时,需要通过重定位节对这些位置的地址进行修改。链接器会通过重定位条目的类型判断该使用什么养的方法计算正确的地址值,通过偏移量等信息计算出正确的地址。

4.3.4 符号表

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

4.4 Hello.o的结果解析

命令内容:>objdump -d -r hello.o 

与第三章对比可发现以下不同:

1.操作数进制表示:

可以看出,hello.s文件采用10进制,而反汇编的采用16进制。

2.函数调用

可以看出,hello.s中的函数调用是call 函数名,而反汇编得到的是call 相对地址,因为反汇编的代码已经经过了链接与重定位,知道了相对位置。

3.跳转分支

可以看出,hello.s文件的分支跳转是通过跳转到类似于.L2的形式来表示,而反汇编的代码使用跳转到某个相对位置来表示。

4.5 本章小结

本章介绍了hello hello.s hello.o 的汇编过程,分析了可重定位文件的结构和各个组成部分,以及它们的内容和功能;还查看了hello.o elf 格式,并使用objdump 得到反汇编代码与hello.s 进行比较,了解从汇编语言映射到机器语言汇编器需要实现的转换。在这个过程中,生成了重定位条目,为之后的链接中的重定位过程奠定了基础。

5章 链接

5.1 链接的概念与作用

链接的概念:链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。

链接的作用:链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。链接是由叫做链接器的程序执行的。链接器使得分离编译成为可能。

5.2 在Ubuntu下链接的命令

命令内容:

>ld -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 /usr/lib/gcc/x86_64-linux-gnu/9/crtbegin.o  /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o  hello.o  -lc    -z relro -o hello

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

5.3.1 ELF

ELF头描述文件的总体格式。包括程程序运行时要执行的第一条指令的地址。程序头部大小和数目也确定了下来。其余的部分基本与可重定位目标文件相同。

5.3.2节头

       节头对hello中所有的节信息进行了声明,对于节的分类也更加详细,也确定了每个节在运行时的实际起始地址以及偏移量。其余部分都与可重定位目标文件的节头相同。

5.3.3程序头

程序头一共有8个段:

(1)PHDR:包含程序头表本身;

(2)INTERP:只包含了一个节,在这个节中,包含了动态链接过程中所使用的解释器路径和名称。

(3)两个LOAD段:第一个是代码段,第二个是数据段。在程序运行时需要映射到虚拟空间地址。

(4)DYNAMIC:保存了由动态链接器使用的信息。

(5)NOTE:保存了辅助信息。

(6)GNU_STACK:堆栈段。

(7)GNU_RELRO:在重定位之后哪些内存区域需要设置只读。

5.3.4重定位节

文件的重定位节已经完成重定位,符号表中的符号的信息都被记录在这里。

5.4 hello的虚拟地址空间

使用edb加载hello,可以查看加载到虚拟地址中的 hello程序。 查看 ELF 格式文件中的 Program Headers,它告诉链接器运行时加载的内容并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的各方面的信息。

查看ELF文件的程序头,程序头在为链接器提供运行时的加载内容和提供动态链接的信息,每一个表项提供了各段在虚拟地址空间大小和物理地址空间大小,位置,标志,访问权限和对齐方式。

可以看出,程序头包括8个段,正如5.3中所提到的:

PHDR段;

INTERP段;

两个LOAD段;

DYNAMIC段;

NOTE段;

GNU_STACK段;

GNU_RELRO段。

5.5 链接的重定位过程分析

命令内容:>objdump -d -r hello

不同:

跳转:hello.o为相对偏移地址,hello为虚拟内存地址。

Hello含有外部链接得到的函数。

hello相对hello.o增加了部分节(.init,.plt等)。

重定位:

一旦链接器完成了符号解析这一步,就把代码中的每个符号引用和正好一个符号定义(即其一个输入目标模块中的一个符号表条目)关联起来。此时,链接器就知道其输入目标模块中的代码节和数据节的确切大小。现在就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时地址。重定位由两步组成:

重定位节和符号定义:在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,来自所有输入模块的.data节被全部合并成一个节,这个节成为输出的可执行目标文件的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。

当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。

重定位节中的符号引用:在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目(relocation entry)的数据结构,我们接下来将会描述这种数据结构。

5.6 hello的执行流程

hello!_start 0x401090

__libc_start_main 0x403ff0

main 0x4010c1

hello!puts@plt 0x401030

hello!exit@plt 0x401070

hello!printf@plt 0x401040

hello!atoi@plt 0x401060

hello!sleep@plt 0x401080

hello!getchar@plt 0x401050

5.7 Hello的动态链接分析

动态链接:

共享库(shared library)是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链楼(dynamic linking),是由一个叫做动态链接器(dytamic linkeg的程序来执行的。共享库也称为共享目标(shared object),在Linux系统中通常用,s0后缀来表示,微软的操作系统大量地使用了共享库,它们称为DLL(动态链接库)。

共享库是以两种不同的方式来“共享”的。首先,在任何给定的文件系统中,对于一个库只有一个.so文件。所有引用该库的可执行目标文件共享这个.so文件中的代码和数据,而不是像静态库的内容那样被复制和嵌入到引用它们的可执行的文件中。其次,在内存中,一个共享库的.text节的一个副本可以被不同的正在运行的进程共享。

比如说:GOT:GOT是一个数组,其中元素是8字节的地址。和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时需要用到的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。根据hello的ELF文件可知GOT起始表位置为0x404000。

Dl_init执行前0x404000的16个字节均为0:

Dl_init执行后0x404000的16个字节有所改变:

5.8 本章小结

第五章中主要介绍了链接的概念与作用,并且详细说明了hello.o是怎么与其他.o(.so)文件链接成为一个可执行目标文件的过程,展示了hello.o的ELF文件形式和各个节的含义,分析了hello的虚拟地址空间、重定位过程与动态链接过程。

6章 hello进程管理

6.1 进程的概念与作用

进程的概念:进程是一个正在运行的程序的实例。系统中的每个程序都在某个进程的上下文中运行。上下文由程序正确运行所必需的状态组成。这种状态包括存储在内存中的代码和程序数据、堆栈、通用寄存器的内容、程序计数器、环境变量和文件描述符的集合。

进程的功能:进程为应用程序提供了两种抽象,一种是独立的逻辑控制流,一种是私有地址空间。提高CPU执行效率,减少因程序等待造成的CPU空闲和其他计算机软硬件资源的浪费。

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

shell是一个应用程序,他在操作系统中提供了一个用户与系统内核进行交互的界面。他的处理过程一般是这样的:首先从终端读入输入的命令,并将输入字符串分割获取参数,如果是内置命令则立即执行,如果不是内置命令则调用对应的程序并运行。Shell还可以接受键盘输入的信号比如ctrl c,并对这些信号进行处理。

6.3 Hello的fork进程创建过程

根据shell的处理流程,可以推断,输入命令执行 hello后,父进程如果判断不是内部指令,即会通过 fork函数创建子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到一份与父进程用户级虚拟空间相同的(但是独立的)副本,包括数据段、代码、共享库、堆和用户栈。父进程打开的文件,子进程也可读写。二者之间最大的不同或许在于 PID的不同。 fork函数只会被调用一次,但会返回两次,在父进程中,fork返回子进程的PID,在子进程中fork返回0。

6.4 Hello的execve过程

当fork 之后,子进程调用execve 函数在当前进程的上下文中加载并运行一个新程序即hello 程序,execve 调用驻留在内存中的被称为启动加载器的操作系统代码来执行hello 程序,加载器删除子进程现有的用户虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC 指向_start 地址,_start 最终调用hello中的main 函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。

6.5 Hello的进程执行

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

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

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

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

在hello运行过程中,若hello进程不被抢占,则正常执行;若被抢占,则进入内核模式,进行上下文切换,转入用户模式,调度其他进程。

这里有个特殊的情况,当hello执行到sleep()的时候,为了不浪费处理器资源,hello进程会被抢占,直到sleep()返回,触发一个中断,使得hello进程重新被调度。

6.6 hello的异常与信号处理

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

Ctrl+Z

运行时按Ctrl-Z之后,将会发送一个SIGTSTP信号给shell。然后shell将转发给当前执行的前台进程组,使hello进程停止。

       再输入ps查看当前进程,截图;输入jobs命令,截图;可见该进程已经停止。再输入pstree,可见这是一个树状图,显示了进程之间的关系。再输入fg(一个继续运行的命令),可以看出,刚才停止的程序继续运行,此时正在等我们输入一个字符。再运行一次这个程序,使用ctrl-z停止后再用kill杀死程序,并截图。

乱按:

程序运行情况正常,shell将乱输入的第一个字符当做getchar的输入,其余都当做新的shell命令,在hello进程结束被回收之后,将会在命令行中尝试解释这些命令。

6.7本章小结

本章简述了进程管理以及shell的一些信息,包括进程的概念与作用,shell的作用和处理流程,shell如何调用fork和execve运行我们的hello进程,以及hello是如何被执行的,当hello进程在执行时遇到特殊情况(比如回车,Ctrl-Z,Ctrl-C等)会如何处理。又介绍了一些常见异常和其信号处理方法。我们可以看出,一个进程的运行与信号密切相关,受到信号的调控。

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:程序代码经过编译后出现在汇编程序中地址,逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。

线性地址与虚拟地址:逻辑地址经过段机制后转化为线性地址(虚拟地址),是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。线性地址通常用十六进制数字表示,程序代码会产生逻辑地址,通过逻辑地址变换就可以生成一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址。如果没有启用分页机制,那么线性地址直接就是物理地址。

物理地址:CPU地址总线传来的地址,由硬件电路控制。物理地址中很大一部分是留给内存条中的内存的,但也常被映射到其他存储器上。在没有使用虚拟存储器的机器上,虚拟地址被直接送到内存总线上,使具有相同地址的物理存储器被读写;而在使用了虚拟存储器的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到存储器管理单元MMU,把虚拟地址映射为物理地址。

在hello程序中,他就表示了这个程序运行时的一条确切的指令在内存地址上的具体哪一块进行执行。

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

段式管理是从逻辑地址到线性地址的变换:

一个逻辑地址由两部分组成,段标识符、段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号,后面3位包含一些硬件细节,索引号,是“段描述符”,段描述符具体地址描述了一个段。这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,由8个字节组成。

给定一个逻辑地址[段选择符:段内偏移地址],转换过程如下:

1、首先根据段选择符判断当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。这样就得到了一个数组。

2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。

3、Base + offset就是要转换的线性地址。

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

页式管理是从线性地址到物理地址的变换:

Linux有一个的虚拟内存系统,其虚拟内存组织形式如下图。Linux将虚拟内存组织成一些区域(称为段)的集合,段之外的虚拟内存不存在因此不需要记录。内核为hello进程维护一个单独的任务结构即图中的task_struct,其中条目 mm指向一个mm_struct,它描述了虚拟内存的当前状态,pgd指向第一级页表的基地址(结合一个进程一串页表),mmap指向一个vm_area_struct的链表,每个vm_area_struct都维护者一个区域。

物理内存被划分为一小块一小块的帧。分配内存时,帧是分配时的最小单位,最少也要给一帧。在虚拟内存中,与帧对应的概念就是页。线性地址的表示方式是:前部分是虚拟页号后部分是虚拟页偏移。

CPU通过将逻辑地址转换为虚拟地址来访问主存,这个虚拟地址在访问主存前必须先转换成适当的物理地址。CPU通过内存管理单元(MMU)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址。然后CPU会通过这个物理地址来访问物理内存。

页表就是一个页表条目(PTE)数组,虚拟地址空间中的每个页在页表中的一个固定偏移量处都有一个PTE。PTE是由一个有效位和一个n个字段组成的。有效位表明了该虚拟页当前是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置。

MMU利用虚拟页号(VPN)来在虚拟页表中选择合适的PTE,当找到合适的PTE之后,PTE中的物理页号(PPN)和虚拟页偏移量(VPO)就会组合形成物理地址。其中VPO与PPO相同,因为虚拟页大小和物理页大小相同,所需要的偏移量位数也就相同。此时,物理地址就通过物理页号先找到对应的物理页,然后再根据物理页偏移找到具体的字节:

1.如果有效位是0,PPN为NULL 则代表没有在虚拟内存空间中分配该内存;

2.如果是有效位0,PPN不为NULL,则代表在虚拟内存空间中分配了但是没有被缓存到物理内存中;

3.如果有效位是1则代表该内存已经缓存在了物理内存中,可以得到其物理页号PPN,与虚拟页偏移量共同构成物理地址PA。

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

TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相联度。用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。下图展示了当TLB命中时所包括的步骤,所有的地址翻译步骤都是在芯片上的MMU中执行的,因此非常快。

第1步:CPU产生一个虚拟地址。

第2步和第3步:MMU从TLB中取出相应的PTE。

第4步:MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存。

第5步:高速缓存/主存将所请求的数据字返回给CPU。当TLB不命中时,MMU必须从Ll缓存中取出相应的PTE。新取出的PTE存放在TLB中,可能会覆盖一个已经存在的条目。

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

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

CPU发送一条虚拟地址,随后 MMU按照上述操作获得了物理地址 PA。根据cache大小组数的要求,将 PA分为 CT,CS,CO。根据CS寻找到正确的组,比较每一个 cacheline是否标记位有效以及 CT是否相等。如果命中就直接返回想要的数据,如果不命中,就依次去 L2,L3,主存判断是否命中,当命中时,将数据传给 CPU同时更新各级 cache的 cacheline(如果 cache已满则要采用换入换出策略)。

7.6 hello进程fork时的内存映射

虚拟内存和内存映射解释fork函数如何为每个新进程提供私有的虚拟地址空间,在shell运行hello进程时,shell为hello进程创建虚拟内存创建当前进程的的mm_struct, vm_area_struct和页表的原样副本,两个进程中的每个页面都标记为只读两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW)在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存随后的写操作通过写时复制机制创建新页面。

7.7 hello进程execve时的内存映射

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

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

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

在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页(page fault)。下图展示了在缺页之前我们的示例页表的状态。CPU引用了VP 3中的一个字,VP 3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE3,从有效位推断出VP3未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4。如果 VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4的页表条目,反映出VP4不再缓存在主存中这一事实。

接下来,内核从磁盘复制VP 3到内存中的PP 3,更新PTE3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但是现在,VP 3已经缓存在主存中了,那么页命中也能由地址翻译硬件正常处理了。下图展示了在缺页之后我们的示例页表的状态。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)。堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。分配器将堆视为一组不同大小的块( block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

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

显式分配器,要求应用显式地释放任何已分配的块;而隐式分配器要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。因此隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。

7.10本章小结

本章简要地介绍了hello进程的内存地址空间管理,并对intel的段式管理和页式管理做了介绍,对TLB与四级页表支持下的VA到PA的变换和三级Cache支持下的物理内存访问进行了介绍,进而结合hello进程对fork与execve从虚拟内存视角进行了分析。最后介绍了缺页故障与缺页中断处理和动态存储分配管理。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

一个 Linux 文件是一个 m 字节的序列,所有的 I/O 设备(如网络、磁盘和终端)都被建模为文件。并且所有的输入和输出都被认为是对相应文件的读取和写入并执行。这种将设备映射到文件的方法允许 Linux 内核在称为 Unix I/O 上运行一个简单的低级应用程序接口,它允许输入和输出以一致和一致的方式运行。

应用程序通过要求内核打开相应的文件来声明它要访问 I/O 设备。内核返回一个小的非负整数称为描述符,文件的关联数据由内核保存,应用程序只需要保存这个描述符。

Linux shell 创建的每个进程都包含三个文件:

标准输入、标准输出和标准错误以供操作时使用

对于每个打开的文件内核维护文件位置 k,它从 0 开始,它是从文件开头的字节偏移量。应用程序可以通过执行搜索显式更改该值。

对于读操作,从文件复制n个字节到内存,文件位置k增加到k+n,当k大于等于文件大小时,触发EOF条件,即结束文件被读取。

最后,在文件访问结束后。该文件将被内核关闭。内核释放文件打开时创建的数据结构,并将描述符恢复到现有的描述符池中。

8.2 简述Unix IO接口及其函数

(1)打开文件:一个应用程序同步异常(陷阱)请求内核打开某文件,表示其要访问一个 I/O 设备,内核返回一个小的非负整数(描述符),然后对此文件的所有操作中标识这个文件,内核把有关这个文件的所有信息进行记录。

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

(3)更改当前的文件位置:对于已打开的某个文件,内核保存着一个文件位置,初始化为0,是从文件开头起始的字节偏移量是这个文件的位置,应用程序能够通过执行seek函数更改当前文件位置。

(4)读写文件:对于读操作,从文件复制n个字节到内存,文件位置k增加到k+n,当k大于等于文件大小时,触发EOF条件,即结束文件被读取。同理写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置k开始,写完成后更新 k。

(5)关闭文件:内核释放文件打开时创建的数据结构,并将描述符恢复到现有的描述符池中。

Unix I/O 函数:

(1)int open(char* filename,int flags,mode_t mode) ,通过调用open函数打开现有文件或创建新文件的过程。open函数将文件名转换为文件描述符并返回描述符编号。返回的描述符始终是进程标志中未打开的最小描述符参数。指定进程打算如何访问文件。 mode 参数指定新的文件访问权限位。

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

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

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

8.3 printf的实现分析

printf函数的函数体如下:

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

{

int i;

char buf[256];

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

   i = vsprintf(buf, fmt, arg);

   write(buf, i);

   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将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址。

int INT_VECTOR_SYS_CALLA表示通过调用系统syscall。

然后是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

sys_call函数通过总线将字符串中的字节从寄存器复制到显卡的显存。显存存储ASCII字符码,字符显示驱动子程序通过ASCII码在字体库中查找点阵信息,将点阵信息存储在vram中。

显示芯片根据刷新频率逐行读取vram。并通过信号线将每个点(RGB分量)发送到液晶显示器。所以我们的输入字符串出现在屏幕上。从vsprintf生成显示数据,写系统函数,int 0x80拦截系统调用,或者sys_call字符显示驱动子程序:从ASCII到字体库显示vram。 (采集每个点的RGB颜色数据)

显示芯片根据刷新频率逐行读取vram。并通过vsprintf的信号线将每个点(RGB分量)发送到液晶显示器,生成显示数据。编写write函数,然后到陷阱-系统调用int 0x80 或 sys_call 等。

字符显示驱动子程序:从ASCII到字体库显示vram(存储每个点的RGB颜色数据),显示芯片相应地逐行读取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系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

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

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

8.5本章小结

第八章讲述了IO设备的管理方法,IO接口及其函数,最后分析了printf和getchar函数的实现方法。

结论

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

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

编写:通过编辑器将代码输入hello.c。

预处理:将hello.c调用的所有外部库和宏替换预处理、扩展和合并到一个hello.i文件中。

编译:将hello.i编译成汇编文件hello.s。

汇编:把hello.s汇编成可重定位的目标文件hello.o。

链接:将hello.o与可重定位的目标文件和动态链接库链接成可执行的目标程序hello。

运行:在shell命令行中输入./hello 1190202008 赵加毅 2。

创建子进程:shell进程调用fork为其创建子进程。

运行程序:shell调用execve,execve调用loader,添加映射的虚拟内存,程序进入程序入口后开始加载物理内存,然后进入main函数。

执行指令:CPU为其分配时间片,在一个时间片内,hello可以使用CPU,依次执行自己的控制逻辑流。

上下文切换:hello调用sleep函数之后进程进入内核模式,内核进行上下文切换将当前进程的控制权交给其他进程,当sleep函数调用完成时,内核执行上下文切换将控制返还给hello进程。

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

动态内存申请:printf调用malloc动态内存分配器在堆中申请内存。

信号:如果在运行时输入ctr-c ctr-z,会分别调用shell的信号处理函数终止和停止。

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

计算机系统的设计思想和实现是基于抽象实现的:抽象体现在:用二进制01表示的最低层信息,操作系统管理硬件,进程是处理器、主存和I/O设备的抽象。虚拟内存是主内存与磁盘设备联系的抽象等。

计算机系统的设计是统筹兼顾的:计算机系统的设计考虑了所有可能的实际情况,并相应地设计了一系列的处理方法来适应不同的情况。

计算机系统设计精巧:为了解决快设备小存储与大存储设备慢存储的不平衡,设计了Cache和TLB等缓存设备作为下层存储设备的缓存,很大程度上提高了CPU运行的速度。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值