计算机系统大作业 程序人生-Hello’s P2P

摘  要

  本文主要围绕在Linux系统下名称为hello的程序,从它的编写、预处理、编译、汇编、链接、运行、创建子程序、加载、访问内存、上下文切换、动态申请内存、信号管理、终止这些步骤,逐步操作并进行深入的剖析,借此来对计算机系统获得更加深入的了解以及总结。

 

关键词:hello,计算机系统,linux  

第1章 概述

1.1 Hello简介

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

 

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

 

020(From Zero-0 to Zero-0): shell为该子进程execve,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。至此即实现了From Zero-0 to Zero-0

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

 

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

Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位

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

1.3 中间结果

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

文件名字

作用

hello.c

编写的c语言程序

hello.i

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

hello.s

hello.i编译后的汇编文件

hello.o

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

hello

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

helloelf

Hello.o的ELF格式文件

helloelf5

Hello的ELF格式文件

helloobj

Hello.o的反汇编代码

helloobj5

Hello的反汇编代码

 

 

1.4 本章小结

本章主要介绍了Hello的P2P和020过程、完成整个大作业过程中使用的软硬件环境,以及开发与调试工具以及生成的中间结果文件的名字和文件的作用

第2章 预处理

2.1 预处理的概念与作用

概念:在C语言的程序中可包括各种以符号#开头的编译指令,这些指令称为预处理命令。预处理命令属于C语言编译器,而不是C语言的组成部分。通过预处理命令可扩展C语言程序设计的环境。

作用:

1、将源文件中以include格式包含的文件复制到编译的源文件中。
2、用实际值替换用“#define”定义的字符串。
3、根据“#if”后面的条件决定需要编译的代码。

2.2在Ubuntu下预处理的命令

预处理命令:gcc -E hello.c -o hello.i

截图如下:

 

                     图2.2.1

2.3 Hello的预处理结果解析

可以看到打开预处理后的hello.i文件我们发现代码从原来的几十行被拓展成了三千多行,源代码中的注释都被删除,而#include命令所包含的头文件都被替代为了相应的代码,扩展了改c语言程序设计的环境

 

                                图2.3.1

2.4 本章小结

本章介绍了预处理的概念和作用、在Ubuntu下预处理的命令以及对预处理的结果进行了解析

 

 

第3章 编译

3.1 编译的概念与作用

概念: 编译是指把用高级语言编写的程序转换成相应处理器的汇编语言程序的过程。   

作用:把用高级语言编写的程序转换成相应处理器的汇编语言程序

3.2 在Ubuntu下编译的命令

编译命令:gcc -S hello.i -o hello.s

截图如下:

 

                         图3.2.1

3.3 Hello的编译结果解析

3.3.1数据类型

3.3.1.1字符串

 

                        图3.3.1

第一个字符串可以发现字符串被编码成UTF-8格式,一个汉字在UTF-8编码中占三个字节,一个\代表一个字节,对应c语言程序中的:用法: "用法: Hello 学号 姓名 秒数!\n"

第二个字符串对应"Hello %s %s\n"

3.3.1.2数组

 

    图3.3.2

movq -32(%rbp),%rax:数组argv存放在-32(%rbp)处

movq (%rax),%rdx:获取argv[1]的地址

movq (%rax),%rax

movq %rax,%rsi: 获取argv[2]的地址

3.3.1.3变量和常量

 

    图3.3.3

这里将局部变量i放在地址-4(%rbp)

许多常量和立即数都在代码中的各处都有体现

argc被放到了堆栈中作为用户传给main函数的参数

 

3.3.2各类操作

3.3.2.1赋值操作

汇编代码中的赋值操作都是使用mov来实现,根据赋值的大小不同有不同的mov操作

操作

大小

movb

1Byte

movw

2Byte

movl

4Byte

movq

8Byte

 

           图3.3.4

 

 

 

此代码作用是将0赋值给%eax,对应c代码中的i=0

3.3.2.2算数操作

 

                   图3.3.5

作用是将-4(%rbp)上的数+1再存入-4(%rbp),对应i++

3.3.2.3控制转移&关系操作

 

           图3.3.6

作用是比较4和-20(%rbp)上的值是否相等,若相等继续执行下面的语句,若不等则直接跳到L2。对应if(argc!=4)

 

               图3.3.7

作用是比较7和-4(%rbp)上的值是否相等,若相等继续执行下面的语句,若不等则直接跳到L4。对应for(i=0;i<8;i++)

3.3.2.4函数操作

P中调用函数Q包含以下动作:

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

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

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

 

在代码中:

main函数的参数是argc和argv;

printf函数的参数是字符串

exit参数是1

sleep函数参数是atoi(argv[3])

所有函数的返回值都存储在寄存器%eax中。

  

3.4 本章小结

本章介绍了编译的概念和作用、编译的命令以及对编译结果中的各种数据类型和操作进行了分析和讲解

 

第4章 汇编

4.1 汇编的概念与作用

概念:汇编是把汇编语言翻译成目标代码(机器代码)的过程

作用:把汇编语言翻译成目标代码(机器代码)

4.2 在Ubuntu下汇编的命令

汇编命令:gcc -c hello.s -o hello.o

截图如下:

 

                          图4.2.1

4.3 可重定位目标elf格式

通过命令readelf -a hello.o > helloelf获得hello.o文件的elf格式

 

                                                                图4.3.1

 

 

 

ELF格式的可执行目标文件的各类信息按顺序如下所示:

 

ELF头:以一个16字节的序列开始,描述了生成改文件的系统的字的大小和字节顺序

. text :   已编译程序的机器代码

. rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表

. data: 已初始化的全局和静态C变量

. bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量

. symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息

.rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。

.rel.data:被模块引用或定义的所有全局变量的重定位信息

. debug:一个调试符号表,其条目是程序中定义的全局变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。

. line:原始C源程序的行号和.text节中机器指令之间的映射

. strtab:一个字符串表,其内容包括 .symtab 和 .debug节中的符号表,以及节头部中的节名字。

 

 

 

ELF头内容如下:

 

                                                             图4.3.2

 

节头内容如下:

 

                                          图4.3.3

 

本文件无程序头

 

重定位节如下:

 

                                                             图4.3.4

重定位节包含.tex节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改他们的位置。包括偏移量、信息、类型、符号值和符号名称+加数

 

符号表.symtab如下:

 

                                                               图4.3.5

4.4 Hello.o的结果解析

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

反汇编代码如下:

 

                                                             图4.4.1

通过反汇编的代码和hello.s进行比较,可以发现代码内容区别并不大,只是反汇编代码所显示的不仅仅是汇编代码,还有机器代码。

机器语言由处理器的指令集及使用它们编写程序的规则组成,是计算机可以直接识别的二进制数据表示的语言。汇编语言是由助记符表示的指令以及使用它们编写程序的规则组成。汇编语言是一种符号语言,比机器语言容易理解和掌握、也容易调试和维护。不过汇编语言本质上还是机器语言,还是一种面向机器的低级程序设计语言,可以将汇编语言转化为机器语言,因此可以将汇编语言和机器语言建立一一映射的关系。

 

(1)分支转移:反汇编的跳转指令用的不是段名称而是是确定的地址,因为段名称只是在汇编语言中便于编写的助记符,所以在变成机器语言之后将被替换成确定的地址。

(2)函数调用:在反汇编中,call的目标地址是当前下一条指令。而在.s文件中,函数调用之后直接跟着函数名称。这是因为hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数运行时的执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将call指令后的相对地址设置为全0,然后在.rel.text节中为其添加重定位条目,等待静态链接的进一步确定。

4.5 本章小结

本章介绍了汇编的概念及作用、Ubuntu下汇编的命令、可重定位目标elf格式以及比较了hello.s与反汇编语言内容的不同

 

 

第5章 链接

5.1 链接的概念与作用

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

作用:将各种代码和数据片段收集并组合成一个单一文件,使得分离编译成为可能。

5.2 在Ubuntu下链接的命令

命令:

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

 

截图如下:

 

                                                        图5.2.1

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

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

 

用命令readelf -a hello > helloelf5生成文件hello程序的ELF格式文件

 

ELF头内容如下:

 

                                               图5.3.1

 

节头内容如下:

 

                                            图5.3.2

 

重定位节如下:

 

                                                图5.3.3

 

.symtab节内容如下:

 

                                                      图5.3.4

5.4 hello的虚拟地址空间

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

如下图所示可知hello的虚拟地址空间开始于0x400000

 

                                                    图5.4.1

而通过5.3中的节头表对应虚拟地址空间各段信息可以看出各节的的起始位置和大小

 

                                               图5.4.2

例如.init节起始于0x401000,大小为0x1b,在edb中对应如下:

 

                                                 图5.4.3

5.5 链接的重定位过程分析

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

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

 

不同:

1、hello和hello.o的反汇编代码相比多了很多的函数,而hello.o中仅有main函数,hello中却多了很多函数的代码例如start函数,fini函数等等。

2、hello的反汇编代码有确定的虚拟地址,也就是说已经完成了重定位,而hello.o的反汇编代码中虚拟地址均为0,即未完成可重定位的过程

 

                                                图5.5.1

 

                                                   图5.5.2

 

过程:

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

(2)链接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。

(3)当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。

5.6 hello的执行流程

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

程序名

地址

<_init>

0x0000000000401000

<.plt>

0x0000000000401020

<puts@plt>

0x0000000000401090

<printf@plt>

0x00000000004010a0

<getchar@plt>

0x00000000004010b0

<atoi@plt>

0x00000000004010c0

<exit@plt>

0x00000000004010d0

<sleep@plt>

0x00000000004010e0

<_start>

0x00000000004010f0

<_dl_relocate_static_pie>

0x0000000000401120

<main>

0x0000000000401125

<__libc_csu_init>

0x00000000004011c0

<__libc_csu_fini>

0x0000000000401230

<_fini>

0x0000000000401238

 

5.7 Hello的动态链接分析

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

 

在hello的elf文件中可以查找到如下信息:

 

                                                         图5.7.1

 

GOT起始表位置为0x40400

在edb中打开hello,查看0x40400处,即为执行dl_init前的内容,结果如下:

 

                                                  图5.7.2

 

执行dl_init后,内容如下:

 

                                                    图5.7.3

 

对于变量而言,我们利用代码段和数据段的相对位置不变的原则计算正确地址。对于库函数而言,需要plt、got合作,plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次再调用plt时,指向的就是正确的内存地址。plt就能跳转到正确的区域。

 

5.8 本章小结

本章介绍了链接的概念与作用、在Ubuntu下链接的命令、可执行目标文件hello的格式、hello的虚拟地址空间、链接的重定位过程分析、利用edb查看hello的执行流程以及hello的动态链接分析

 

6章 hello进程管理

6.1 进程的概念与作用

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

作用:提供给应用程序关键抽象

·一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器

·一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统

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

作用:Shell是一种应用程序,是以用来连接内核和用户的软件,它提供了一个界面,用户通过这个界面访问操作系统内核的服务。

 

处理流程:

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

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

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

(4)若是则立即解释这个命令,如果不是内部命令,调用fork( )创建新进程/子进程在其上下文中加载并运行这个文件

(6)若最后一个参数是&,程序在后台执行(shell不等它完成),否则shell等待作业完成。

6.3 Hello的fork进程创建过程

在终端中输入./hello,运行的终端程序会对输入的命令行进行解析,因为hello不是一个内置的shell命令所以解析之后终端程序判断./hello的含义为执行当前目录下的可执行目标文件hello,接着终端程序通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是独立的一个副本。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同在于他们的PID是不同的。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。

6.4 Hello的execve过程

当父进程fork之后,子进程调用execve函数在当前进程的上下文中加载并运行一个新程序即hello程序,execve程序加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp,只有当出现错误时,例如找不到filename,execve才会返回到调用程序。

1、加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。

2、新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。

3、最后加载器设置PC指向_start地址,_start最终调用hello中的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存

6.5 Hello的进程执行

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

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

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

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

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

 

                

                                         图6.5.1

6.6 hello的异常与信号处理

会出现中断、陷阱、故障、终止四类异常

常见的产生的信号和处理方式大致如下:

ID

名称

默认行为

相应事件

2

SIGINT

终止

来自键盘的中断

9

SIGKILL

终止

杀死程序(该信号不能被捕获不能被忽略)

11

SIGSEGV

终止

无效的内存引用(段故障)

14

SIGALRM

终止

来自alarm函数的定时器信号

17

SIGCHLD

忽略

一个子进程停止或终止

 

                                                             图6.6.1

如图是在程序正常执行中按下ctrl+z,程序被停止

 

                                                          图6.6.2

 

                                                                  图6.6.3

 

                                     图6.6.4

用ps命令可以看到hello进程并没有被回收,而是在后台继续运行

 

                                            图6.6.5

将hello调到前台继续执行,由于上述已经答应出了8行字符串了,所以在我敲入回车后,程序自动结束并退出

 

                                                         图6.6.6

重新执行hello,在执行过程中进行ctrl+c

 

                                          图6.6.7

再次执行ps命令发现没有了hello进程,说明在ctrl+c过后进程被回收了

 

                                                               图6.6.8

而在进程执行过程中乱按并不会影响进程的执行

6.7本章小结

本章介绍了进程的概念及作用、壳Shell-bash的作用于处理流程、Hello的fork进程创建过程、execve过程、Hello的进程执行以及hello的异常与信号处理

 

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:逻辑地址是用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,偏移量指明了从段开始的地方到实际地址之间的距离。

 

线性地址:也叫虚拟地址,逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式

 

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

 

物理地址:是对应于主存的真实地址,是能够用来直接在主存上进行寻址的地址。

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

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

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

       进程段表:描述组成进程地址空间的各段,可以是指向系统段表中表项的索引。每段有段基址,即段内地址。

        在系统中为每个进程建立一张段映射表,如图:

 

                                     图7.2.1

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

      

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

 

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

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

 

                                 图7.3.1

n位的线性地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,例如VPN 0选择PTE 0。根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。这里的VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。

 

                                                              图7.3.2

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

当一个进程执行一条访存指令时,它发出的内存地址是虚拟地址,由内存管理单元(MMU)将虚拟地址转化为物理地址,并访问主存,取出所要读取的数据。

当TLB命中时翻译步骤如下:

1、CPU产生一个虚拟地址

2&3、MMU从TLB中取出相应的PTE

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

5、高速缓存/主存将所请求的数据字返回给CPU

 

                                                               图7.4.1

 

多级页表翻译如下:

                                                                      图7.4.2

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

三级Cache的核心思想就是每次访问数据的时候都将一个数据块存放到更高一层的存储器中,根据计算的局部性,程序在后面的运行之中有很大的概率再次访问这些数据,高速缓存器就能够提高读取数据的速度。

每级Cache的物理访存大致过程如下:

(1) 组选择取出虚拟地址的组索引位,将二进制组索引转化为一个无符号整数,找到相应的组

(2) 行匹配把虚拟地址的标记为拿去和相应的组中所有行的标记位进行比较,当虚拟地址的标记位和高速缓存行的标记位匹配时,而且高速缓存行的有效位是1,则高速缓存命中。

(3) 字选择一旦高速缓存命中,我们就知道我们要找的字节在这个块的某个地方。因此块偏移位提供了第一个字节的偏移。把这个字节的内容取出返回给CPU即可

(4)不命中如果高速缓存不命中,那么需要从存储层次结构中的下一层取出被请求的块,然后将新的块存储在组索引位所指示的组中的一个高速缓存行中。一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块, 产生冲突,则采用最近最少使用策略LFU进行替换。

7.6 hello进程fork时的内存映射

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

当fork在新进程中返回时,新进场现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。因此,也就为每个进程都保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

Execve在加载并运行可执行目标文件hello的步骤如下:

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

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

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

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

 

                                                                   图7.7.1

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

缺页故障:当指令引用一个相应的虚拟地址,而该地址相应的物理地址不再内存中,会触发缺页故障。

缺页中断处理:通过查询页表PTE可以知道虚拟页在磁盘的位置。缺页处理程序从指定的位置加载页面到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中,因此指令可以没有故障的运行完成。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不用,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk,它指向堆的顶部

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

   分配器分为两种基本风格:

    显式分配器:要求应用显式地释放任何已分配的块。

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

7.10本章小结

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

 

8章 hello的IO管理

8.1 Linux的IO设备管理方法

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

 B0, B1,…,Bk…Bm-1

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

·打开文件

·改变当前文件位置

·读写文件

·关闭文件

 

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

Unix I/O 接口:

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

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

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

4、读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的文件,当k>=m时,触发 一个称为EOF的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“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(int fd),进程通过调用close函数关闭一个打开的文件。若成功返回值为0,若出错为-1。

(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 的当前文件位置。返回值-1表示一个错误,若成功返回值表示的是写的字节数。

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;
    }

1、对于va_list arg = (va_list)((char*)(&fmt) + 4):

va_list arg是一个字符指针。 (char*)(&fmt) + 4) 表示的是...中的第一个参数

2、vsprintf

其函数如下:

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返回的是一个长度即为要打印出来的字符串的长度,vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

3、write

 write:
     mov eax, _NR_write
     mov ebx, [esp + 4]
     mov ecx, [esp + 8]
     int INT_VECTOR_SYS_CALL

write(buf,i)将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址

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

4、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的功能为显示格式化了的字符串

ecx中是要打印出的元素个数
  ebx中的是要打印的buf字符数组中的第一个元素
   这个函数的功能就是不断的打印出字符,直到遇到:'\0'
   

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

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

8.4 getchar的实现分析

异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成 ASCII 码,保存到系统的键盘缓冲区之中,等待后续getchar调用读取.也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键.。

 

getchar函数调用了系统函数read,read函数会产生一个陷阱,通过系统调用read读取存储在键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar对这个字符串进行处理,读取字符串的第一个字符然后返回,将其余输入丢弃。

8.5本章小结

本章介绍了Linux的I/O设备管理方法、Unix I/O接口及其函数、printf的实现分析以及getchar的实现分析

 

结论

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

1、编写:编写c语言程序,hello.c诞生,

2、预处理:hello.c经过预处理变为hello.i。

3、编译:hello.i经过编译变为hello.s。

4、汇编:hello.s经过汇编变为hello.o。

5、链接:hello.o与可重定位目标文件和动态链接库链接成为可执行文件hello。从此诞生了可执行hello程序。

6、运行:在终端输入./hello 1190202012 李昕羽 2。

7、创建子进程:shell调用fork()函数创建一个子进程。

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

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

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

11、动态申请内存:当hello程序执行printf函数时,会调用malloc向动态内存分配器申请堆中的内存。

12、信号管理:当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,将前台作业停止挂起。当输入Ctrl+c时,内核会发送SIGINT信号给进程并终止前台作业。。

13、终止:当子进程执行完毕,父进程回收子进程,并且内核删除为这个进程创建的所有数据。

至此hello程序终于走完了它的充满传奇色彩且丰富的一生

 

通过此次大作业,让我对计算机系统的设计与实现做了一个系统且细致的总结,我深刻意识到了计算机系统的博大精深和我知识储备的缺乏,今后一定要更加努力学习计算机系统。

 

 

附件

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

文件名字

作用

hello.c

编写的c语言程序

hello.i

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

hello.s

hello.i编译后的汇编文件

hello.o

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

hello

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

helloelf

Hello.o的ELF格式文件

helloelf5

Hello的ELF格式文件

helloobj

Hello.o的反汇编代码

helloobj5

Hello的反汇编代码

 

 

参考文献

[1]  Randal E.Bryant,David R. O’Hallaron. Computer Systems A Programmer’s Perspective Third Edition

[2]  Pianistx. [转]printf函数实现的深入剖析.

https://www.cnblogs.com/pianist/p/3315801.html

[3]  风过无痕521. 机器语言与汇编语言的关系

https://www.cnblogs.com/wangwangfei/p/4381021.html

[4]  spfLinux. Ubuntu系统预处理、编译、汇编、链接指令

https://blog.csdn.net/spflinux/article/details/54427494

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值