2022哈工大计算机系统大作业

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业        计算机类       

学     号       120L02xxxx       

班     级         xxxxxxx        

学       生          xxx      

指 导 教 师          郑贵滨       

计算机科学与技术学院

2021年5月

摘  要

本文主要通过观察hello.c程序在Linux系统下的生命周期,探讨hello.c源程序的预处理、编译、汇编、链接、生成可执行文件并运行的主要过程。同时结合课本中所学知识详细说明系统是如何实现对hello程序的进程管理,存储管理和I/O管理。通过对hello.c程序的生命周期的探索,让我们对可执行文件的生成和执行以及其它相关的计算机系统的知识有更深的理解。

关键词:预处理;编译;汇编;链接;加载;进程管理;存储管理;I/O管理;

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

目  录

第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的源程序hello.c经过cpp的预处理后得到修改了的源程序hello.i,再经过cc1的编译得到汇编程序hello.s,然后再经过as的处理,得到可重定位目标程序hello.o,最后由链接器生成可执行目标文件hello。

之后用户通过shell键入./hello命令开始执行程序,shell通过fork函数创建一个子进程,再由子进程执行execve函数加载hello。

以上就是P2P过程。

图1-1 P2P过程

020过程:

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

1.2 环境与工具

硬件环境:

X64 CPU;2GHz;2G RAM;256GHD Disk 以上

软件环境:

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

开发与调试工具:

gcc,vim,edb,readelf,HexEdit

1.3 中间结果

hello.c:源程序

hello.i:预处理修改后的源程序

hello.s:编译后的汇编程序

hello.o:汇编后的可重定位目标程序

hello:链接后的可执行目标程序

1.4 本章小结

       本章简要介绍了hello的P2P过程和020过程,罗列出了编写本论文所需的硬件和环境,列出了为编写本论文生成的中间结果文件的名字,文件的作用等。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

概念:

       预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。C语言的预处理主要有以下三个方面的内容:1. 宏定义;2. 文件包含;3. 条件编译,预处理命令以符号#开头,如#if, #endif, #define, #include等。

作用:

1.实现条件编译,通过预处理可以实现部分代码的在某些条件下的选择性编译。

2.实现宏定义,在预处理阶段用定义的实际数值将宏替换。

3.实现头文件引用,将头文件的内容复制到源程序中以实现引用。

4.实现注释,将c文件中的注释从代码中删除。

5.实现特殊符号的使用。如处理#line、#error、#pragma以及#等。

2.2在Ubuntu下预处理的命令

预处理命令:

       gcc -E hello.c -o hello.i

图2-1 在Ubuntu下预处理

2.3 Hello的预处理结果解析

可以观察到文件夹中生成了一个新的hello.i程序,查看其内容,可以发现hello.i的内容比hello.c多出不少,这是由于预处理对原文件中的宏进行了宏展开,头文件中的内容被包含进该文件中,例如声明函数、定义结构体、定义变量、定义宏等内容。另外,如果代码中有#define命令还会对相应的符号进行替换。

图2-2 hello.i

2.4 本章小结

本章介绍了预处理的概念与作用,展示了预处理的过程,解析了预处理的结果。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

概念:

       编译是指编译器(cc1)将修改了的源程序.i文本文件翻译成汇编程序.s文本文件的过程。

作用:

       为不同高级语言的不同编译器提供了通用的输出语言。除此之外,编译程序还具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用以及人际联系等重要功能。

3.2 在Ubuntu下编译的命令

编译命令:

       gcc -S hello.i -o hello.s

图3-1 在Ubuntu下编译

3.3 Hello的编译结果解析

3.3.1 汇编指令及含义

.file:声明源文件

.text:声明代码段

.rodata:只读数据

.align:声明对指令或数据的存放地址进行对齐的方式

.string:声明字符串

.globl:全局变量

.type:声明一个符号是数据类型还是函数类型

图3-2 hello.s的开头声明

3.3.2 数据

3.3.2.1 常量

       hello.s中有两个字符串常量,它们在只读数据段中,作为函数printf的参数。

图3-3 hello.s的常量

3.3.2.2 局部变量i

       main函数声明了局部变量i,编译器进行编译时会将i入栈。可以知道i被存储在-4(%rbp)中。

图3-4局部变量i

3.3.2.3 参数int argc

argc是main函数的参数之一,main函数运行时也要入栈,它被存放在-20(%rbp)中。

图3-5 main函数的参数int argc

3.3.2.4 参数char *argv[]

       argv[]是main函数的第二个参数,数组的每个元素都是一个指向字符类型的指针。数组的起始地址存放在-32(%rbp)。

图3-6 main函数的参数char *argv[]

3.3.2.5 立即数

       立即数直接体现在汇编代码中,以$开头。

3.3.3 赋值

该程序声明的变量为int i,并且将i的值设为0,通过mov指令实现。mov指令根据操作数的字节大小分为:movb一个字节,movw字,movl双字,movq四字。

图3-7 变量i赋初值

3.3.4 类型转换

       源程序的类型转换语句为sleep(atoi(argv[3])),将字符串类型转换为int类型。

图3-8 调用atoi函数

3.3.5 算术操作

       源程序中出现的算术操作为i++,只需要用add指令即可。

图3-9 i++

3.3.6 关系操作

       源代码中的关系操作有if (argc != 4)以及for(i = 0; i < 8; i++)。在汇编代码中,通过cmp语句来设置条件码,确定跳转位置。

                                        

图3-10 argc != 4

图3-11 i < 8

3.3.7 数组/指针/结构操作

       源程序中的数组访问有argv[1]、argv[2]、argv[3],在汇编代码中访问这三个量是通过数组首地址加偏移量的方式。argv[2]存放在%rdx ,argv[1]存放在%rsi ,argv[3]存放在%rdi。

图3-12 argv[2]存放在%rdx

图3-13 argv[1]存放在%rsi

图3-14 argv[3]存放在%rdi

指针操作是通过在储存指针变量的寄存器名外加括号实现的,源程序中出现的有对argv[1],argv[2]和argv[3]所指向的元素的访问。

3.3.8 控制转移

       源程序中的控制转移有if语句和for语句,在汇编代码中这两者都是通过条件跳转指令来完成的。

if语句中,判断argc是否为4,若为4,则执行if体的语句,若不是,则不执行。

图3-15 if语句

for语句中每次循环前判断i是否小于8,若满足,则执行循环体,否则不执行。

图3-16 for语句

3.3.9 函数操作

源程序涉及的函数有:main,printf,exit,sleep,atoi,getchar。

调用函数之前要完成参数传递,将函数所需的参数储存到相应的寄存器中,顺序是rdi、rsi、rdx、rcx、r8、r9,其余参数压栈。

3.3.9.1 main

       main函数的参数为argc和argv。argc存储在%edi中,argv存储在%rsi中。返回值为int类型,存储在%eax中。

3.3.9.2 printf

       源程序出现了两处printf函数。第一次是以.LC0为参数,把它存储在%rdi中。

图3-17 第一个printf

       第二次是以.LC1,argv[1]和argv[2]为参数。.LC1存储在%rdi,argv[1]存储在%rsi,argv[2]存储在%rdx。

图3-18 第二个printf

3.3.9.3 exit

       exit函数实现从main函数退出。将参数存储在%edi传入的参数是1,表示非正常退出。

图3-19 exit

3.3.9.4 sleep

sleep函数实现程序休眠,传入的参数为atoi(argv[3])(表示休眠秒数)。将atoi存储在%eax的返回值再存储到%edi中作为sleep的参数。

图3-20 sleep

3.3.9.5 atoi

       atoi函数实现将字符串类型数据转变成int类型的数据,传入的参数为argv[3]。

图3-21 atoi

3.3.9.6 getchar

       getchar函数实现读取缓冲区字符。不需要传递参数,直接调用即可。

图3-22 getchar

3.4 本章小结

本章主要介绍了编译的概念与作用,在Ubuntu下编译的命令,以及对hello.s进行了分析,展示了编译的过程,分析了相应语句的汇编代码。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

概念:

       汇编器(as)将汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中。该文件是一个二进制文件,包含程序的指令编码。

作用:

       汇编过程将汇编代码转换为计算机能够理解并执行的二进制机器代码,这个二进制机器代码是程序在本机器上的机器语言的表示。

4.2 在Ubuntu下汇编的命令

汇编命令:

       gcc -c hello.s -o hello.o

图4-1 在Ubuntu下汇编

4.3 可重定位目标elf格式

4.3.1 ELF的格式

对于目标文件,现代x86-64Linux和Unix系统使用可执行可链接格式(ELF)。

图4-2 典型的ELF可重定位目标文件

4.3.2 ELF头

       ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。

图4-3 ELF头

       ELF头以16字节的序列Magic开始。可以看出,hello.o为64位文件;数据采用补码表示,小端存储;文件类型为REL(可重定位的目标文件);程序的入口地址为0x0,因为hello.o还未实现重定位;可重定位文件没有段头表(Start of program headers的值为0);节头表的起始位置为1240;文件中共有14节。

4.3.3 节头部表

       节头部表描述了不同节的位置和大小,其中目标文件中每个节都有一个固定大小的条目。由于是可重定位目标文件,所以每个节的地址都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小。

图4-4 节头部表

可以看出,节头表中一共有14项,其中第一项为空。剩下13项对应于可重定位文件中每一节的相关内容,其中包含每一节的名字,类型,地址,在文件中的偏移量,节的大小,访问权限,对齐方式等等。

4.3.4 .symtab

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

       可以看出,符号表中存储了程序中定义和使用的各种符号,包括函数名,全局变量名等等。其中每一个符号有其对应的值,大小,类型,名字等等内容。Bind字段表明符号是本地的还是全局的。

图4-5 .symtab

4.3.5 .rel.text

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

图4-6 .rel.text

各个字段的含义如下:

Offset:需要被修改的引用节的偏移。

Info:包括symbol和type两个部分,symbol在前面四个字节,type在后面四个字节。symbol标识被修改引用应该指向的符号,type是重定位的类型。

Type:告知链接器应该如何修改新的应用。

Addend:一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整。 Name:重定位到的目标的名称。

4.4 Hello.o的结果解析

hello.o的反汇编如下。

 

图4-7 hello.o的反汇编

       hello.s如下。

 

 

图4-8 hello.s

反汇编代码与hello.s并无太大区别。但反汇编代码中不仅有汇编代码,还有其对应的机器语言代码。机器语言代码是完全面向计算机的二进制数据表示的语言。机器语言代码中包含操作码,数据,寄存器编号等内容,其中机器语言的每一个操作码,寄存器编号等都与汇编语言一一对应。机器语言中的数据采用小端存储的二进制形式表示,而在汇编语言中采用的是顺序十六进制形式表示。通过这些映射关系就可以实现机器语言与汇编语言的一一对应。

反汇编代码和汇编代码对立即数的引用不同。在反汇编中立即数是十六进制的,而汇编代码则是十进制。

对于函数的调用也不同。反汇编代码中子程序的调用是通过对主函数地址的相对偏移进行的,而在汇编代码中则是通过call直接加上函数名的方法进行的。这是因为 hello.c 中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址。在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0,最后在链接时才填入正确的地址。

分支转移也不同。反汇编代码中用的不是hello.s中的符号来代表位置,如.L1等。而是用的相对地址表示。hello.s中的.L1等只是便于编写的助记符,在变成机器语言后自然就不会存在了。

4.5 本章小结

本章介绍了汇编的概念和作用,展示了在Ubuntu下的汇编,并通过对hello.s的汇编结果的分析简要讲述了可重定位文件的格式,重点讲述了可重定位文件的ELF头,节头部表,.symtab和.rel.text节的内容。最后比较了反汇编结果与汇编文件的区别。

(第41分)

第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-1 在Ubuntu下链接

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

5.3.1 ELF头

图5-2 ELF头

       可以看出可执行目标文件的ELF头与可重定位目标文件的ELF头有以下几个不同:

文件类型不同。可执行目标文件的类型是EXEC,可重定位目标文件的类型是REL。

入口点地址不同。因为重定位工作已经完成。

节头数量不同。可执行目标文件的节头数量更多。

5.3.2 节头部表

       与可重定位目标文件不同,在可执行文件中,经过重定位之后,每个节的地址不再是0。同时节头部表中条目的数量也变多了,意味着节的数量变多了,这是为了能够实现动态链接。

图5-3 节头部表

5.3.3 ,symtab

       可以看出,可执行目标文件的符号表包含编号Num、Value、Size、Type、Bind、Vis、Ndx、Name字段。并且在可执行目标文件中多出了.dynym节。这里面存放的是通过动态链接解析出的符号,这里我们解析出的符号是程序引用的头文件中的函数。

图5-4 .symtab

5.3.4 .rel.text

       可以观察到跟可重定位目标文件相比,可执行目标文件的.rel.text节多了.dyn的内容,这是由于已经完成了重定位。

图5-5 .rel.text

5.4 hello的虚拟地址空间

观察程序头可以知道,LOAD可加载的程序段的地址为0x400000。

图5-6 程序头

图5-7 edb查看hello的起始地址

从5.3.1的ELF头中可以看出程序的入口地址为0x4010f0,对应于节头部表中.text节的起始地址。通过edb查看。

图5-8 edb查看hello程序的入口地址

       阅读节头部表,可发现.interp节的偏移量为0x2e0,通过edb查看。

图5-9 edb查看.interp节

       同样可以通过阅读节头部表来找到.text段和.rodata段等。.text段的偏移量为0x10f0,.rodata段的偏移量为0x2000。

图5-10 edb查看.text节

图5-11 edb查看.rodata节

5.5 链接的重定位过程分析

 

 

 

 

 

图5-12 hello的反汇编

可以发现hello.o的反汇编文件和hello的反汇编文件有以下不同:

hello的反汇编代码的语句均有对应的虚拟地址,而hello.o的反汇编代码语句则没有确定的虚拟地址,这是由于前者已经完成了重定位后者却没有;

除main函数的反汇编代码外,hello的反汇编文件中还有许多函数的汇编代码;

图5-13 除main函数外的一些函数

       新增了.init节和.plt节以及这些节定义的函数。

图5-14 .init节

图5-15 .plt节

       由此可以认识到链接的过程是为地址不确定的符号分配一个确定的地址,而在该符号的引用处也将地址改为确定值。

hello的重定位过程:

在汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text节中,已初始化数据的重定位条目放在.rel.data中。重定位条目的结构如下,offset是需要被修改的引用的节偏移。symbol标识被修改引用应该指向的符号。type告知链接器如何修改新的引用。addend是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。

图5-16 重定位条目的结构

有两种最基本的重定位类型是R_X86_64_PC32和R_X86_64_32。前者重定位一个使用32位PC相对地址的引用,后者重定位一个使用32位绝对地址的引用。链接器的重定位算法的伪代码如下。

图5-17 重定位算法

       下图为hello.o的一个反汇编语句,可知它采用32位PC相对地址的引用。

图5-18 32位相对寻址语句

       查询得到main函数地址为0x401125,.rodata的地址为0x402008。代入运算得:*refptr = (unsigned) (ADDR(.rodata) + addend – ADDR(main) - offset) = (unsigned) (0x402008 + (-4) – 0x401125 – 0x1c) = (unsigned) (0xec3)。该结果与hello的反汇编文件中的对应语句比较,发现相符。

图5-19 hello的反汇编文件中的对应语句

同理可以得到其他重定位条目。

5.6 hello的执行流程

图5-20 hello的执行流程

5.7 Hello的动态链接分析

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

共享库:共享库是一个目标模块,在加载或运行时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程被称为动态链接,是由一个叫做动态链接器的程序来执行的。共享库也称为共享目标。

动态链接的基本思想:先把程序按照模块拆分成各个相对独立的部分,在程序运行时将这些相对独立的部分链接在一起形成一个完整的程序。

动态延迟绑定:动态的链接器在正常工作的时候链接器采取了延迟绑定的连接器策略,由于静态的编译器本身无法准确的预测变量和函数的绝对运行时的地址,动态的连接器需要等待编译器在程序开始加载时在对编译器进行延迟解析,这样的延迟绑定策略叫做动态延迟绑定。延迟绑定是通过got和plt实现的。got是数据段的一部分,而plt是代码段的一部分。

图5-21 .got.plt

.got.plt起始表的位置为0x404000。

可以观察到got表调用dl_init前,0x404008后的16个字节均为0。调用dl_init后,.got.plt的条目已经发生变化。

图5-22 调用dl_init前

图5-23 调用dl_init后

5.8 本章小结

本章主要介绍了链接的概念和作用,详细介绍了hello.o是如何链接生成一个可执行文件的。同时展示了可执行文件中不同节的内容。最后分析了程序是如何实现的动态链接的。

(第51分)

第6章 hello进程管理

6.1 进程的概念与作用

概念:

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

作用:

       向用户提供了一种假象。 我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

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

Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。

其基本功能是解释并运行用户的指令,重复如下处理过程:

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

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

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

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

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

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

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

6.3 Hello的fork进程创建过程

当在shell上输入./hello命令时,命令行会首先判断该命令是否为内置命令,如果是内置命令则立即对其进行解释。否则将其看成一个可执行目标文件,再调用fork创建一个新进程并在其中执行。

当shell运行一个程序时,父进程通过fork函数生成这个程序的进程。新创建的子进程几乎但不完全与父进程相同,包括代码、数据段、堆、共享库以及用户栈。父进程和新创建的子进程之间最大的区别在于他们有不同的PID。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。

6.4 Hello的execve过程

execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。加载并运行需要以下几个步骤:

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

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

3.映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。

4.设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

6.5 Hello的进程执行

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

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

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

上下文切换:当一个进程正在执行时,内核调度了另一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。在进行上下文切换时,需要保存以前进程的上下文,恢复新恢复进程被保存的上下文,将控制传递给这个新恢复的进程来完成上下文切换。

hello的进程执行过程如下:

hello初始运行在用户模式,在hello进程调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程以加载新的进程进行执行。同时将hello进程从运行队列中移入到等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程并执行,当定时器到时时发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。

当hello调用getchar的时候,实际落脚到执行输入流是stdin的系统调用read,hello之前运行在用户模式,在进行read调用之后陷入内核,内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。此时进入内核模式,内核执行上下文切换,切换到其他进程。当完成键盘缓冲区到内存的数据传输时,引发一个中断信号,此时内核从其他进程进行上下文切换回hello进程。

6.6 hello的异常与信号处理

异常的类别如下。

图6-1 异常的类别

当出现异常时,操作系统会根据异常表进行一个间接过程调用,找到异常对应的异常处理程序。当异常处理程序完成处理后,根据引起异常的事件的类型,会发生以下3种情况中的一种:

1.处理程序将控制返回给当前指令,即当事件发生时正在执行的指令。

2.处理程序将控制返回给当前指令的下一条指令,即如果没有异常发生将会执行的下一条指令。

3.处理程序终止被中断的程序。

程序正常执行的结果如下。

图6-2 正常输出

若不停乱按包括回车,虽然乱按不会影响程序的运行,但是会在程序运行结束后对shell发送许多无效指令,不过要注意因为hello程序最后有一个getchar,因此第一个乱按的指令被getchar给读走了,不会成为发送给shell的无效指令。因为我们可以判断,我们乱按的内容被放入缓冲区,等待程序执行结束被shell当作命令读走。

而若是只输入回车,输入回车后程序的行为和乱按几乎是一致的,只是回车没有被识别为无效指令而是被无视。

图6-3 一开始乱按且回车,最后只输入回车

若按下Ctrl-Z,会发送一个SIGTSTP信号到前台进程组中的每个进程,前台作业会被挂起。

图6-4 输入Ctrl-Z

       此时输入ps,可以发现hello进程并未被回收。

图6-5 输入ps

       输入jobs,可以看出此时hello程序的状态为Stopped。

图6-6 输入jobs

       输入pstree,通过pstree我们可以看到所有进程之间的父子关系,结果如下。

 

 

图6-7 输入pstree

       输入fg会使第一个后台作业变为前台,而第一个后台作业是hello,所以输入fg 后hello程序又在前台开始运行。

图6-8 输入fg

       输入kill,在这里选择用9号信号杀死进程。可以看到kill成功的杀死了一个进程,fg无法将其唤醒。

图6-9 输入kill

       若输入Ctrl-C,内核会发送一个SIGINT信号到前台进程组中的每个进程,终止前台作业。

图6-10 输入Ctrl-C

6.7本章小结

本章介绍了计算机中最重要的概念之一——进程。可以说现代计算机离不开进程的概念,只有充分了解进程的创建与异常流控制,才可能成为一个优秀的程序员。这章我们讲述了一个进程是怎么在计算机中被创建的,一个程序是怎么通过子进程被执行的,这是P2P中的最后一步process。这一章我们还介绍了异常与信号,并实际对hello用各种信号进行测试,来了解常用信号的用途。

(第61分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:逻辑地址指的是在汇编代码中通过偏移量+段基址得到的地址,与物理地址不同。在hello反汇编代码中我们能够看到的就是逻辑地址。

线性地址:线性地址就是虚拟地址,具体见下。

虚拟地址:虚拟地址是逻辑地址计算后的结果,同样不能直接用来访存,需要通过MMU翻译得到物理地址来访存。在hello反汇编代码计算后就能得到虚拟地址。

物理地址:计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每个字节都有一个唯一的物理地址。第一个字节的地址为0,写下来的字节地址为1,再下一个为2,以此类推。虚拟地址通过MMU翻译后得到物理地址。在hello中通过翻译得到的物理地址来得到我们需要的数据。

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

一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。

索引号就是段描述符的索引。段描述符具体描述了一个段地址,这样,很多段描述符就组成段描述符表。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符。

Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。

一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。

首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],

看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。

拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。把Base + offset,就是要转换的线性地址了。

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

计算机利用页表,通过MMU来完成从虚拟地址到物理地址的转换。

线性地址即虚拟地址,用VA来表示。VA被分为虚拟页号(VPN)与虚拟页偏移量(VPO),CPU取出虚拟页号,通过页表基址寄存器来定位页表条目,在有效位为1时,从页表条目中取出信息物理页号(PPN),通过将物理页号与虚拟页偏移量(VPO)结合,得到由物理地址(PPN)和物理页偏移量(PPO)组合的物理地址。

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

每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。

TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单一PTE组成的块。TLB通常有高的相联度。

多级页表:将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。VPN被分为k个部分,第一级VPN结合基址寄存器得到一个页表条目,其中存放下一级页表的基址,再结合VPN2,得到第三级页表基址,继续寻找,以此类推,直到最后确定对应的物理页号,与VPO结合,得到由PPN与PPO结合成的物理地址,用于物理地址寻址。

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

在从TLB或者页表中得到物理地址后,根据物理地址从cache中寻找。到了L1里面以后,寻找物理地址要检测是否命中,不命中则紧接着寻找下一级cache L2,接着L3,如果L3也不命中,则需要从内存中将对应的块取出放入cache中,其中可能会发生块的替换等其它操作。这里就是使用到CPU的高速缓存机制了,一级一级往下找,直到找到对应的内容。

7.6 hello进程fork时的内存映射

当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

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

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

3.映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。

4.设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

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

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

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

7.9动态存储分配管理

动态内存分配器维护者一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。分配器有两种基本风格。两种风格都是要求显示的释放分配块。

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

(2) 隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。

隐式空闲链表:一个块是由一个字的头部、有效载荷,以及可能的填充组成。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。块的头最后一位指明这个块是已分配的还是空闲的。

具体的隐式空闲链表形式如下:

放置已分配的块当一个应用请求一个k字节的块时,分配器搜索空闲链表。查找一个足够大可以放置所请求的空闲块。分配器搜索方式的常见策略是首次适配、下一次适配和最佳适配。

当分配器找到一个匹配的空闲块时,通常将空闲块分割为两部分。第一部分变为了已分配块,第二部分变为了空闲块。

当分配器找不到合适的空闲块一个选择是合并那些在物理内存上相邻的空闲块,如果这样还不能生成一个足够大的块,分配器会调用sbrk函数,向内核请求额外的内存。

合并空闲块合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。为了提高合并效率,Knuth提出了一种采用边界标记的技术快速完成空闲块的合并。其所用到的结构如下图所示:

显式空闲链表:显式空闲链表是将空闲块组织为某种形式的显示数据结构。在每个空闲块中,都包含一个前驱和后继的指针。

使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。

维护链表可以采用后进先出(LIFO)的顺序或者按照地址增大的顺序来维护。

7.10本章小结

本章主要介绍了有关内存管理的知识。详细阐述了hello程序是如何存储,如何经过地址翻译得到最终的物理地址。介绍了hello的四级页表的虚拟地址空间到物理地址的转换。阐述了三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

(第7 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

一个linux文件就是一个m个字节的序列。所有的I/O设备(如网络、磁盘和终端)都被模型话为文件,而所有的输入和输出都被当做相应的文件的读和写来执行。这种将设备优雅地映射为文件的方式,运行linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

8.2.1 Unix I/O接口:

根据8.1中描述的Unix I/O接口的概念,我们可以确定I/O接口需要有如下结构功能:

打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备。

Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。

改变当前文件的位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。

读写文件。一个读操作就是从文件复制n > 0个字符到内存,从当前文件位置k开始,然后k += n。对给定一个大小为m字节的文件,当k>=m时执行读操作会出发一个称为EOF的条件。

关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。

8.2.2 Unix I/O函数:

打开文件函数:int open(char *filename, int flags, mode_t mode)。flag参数为写提供一些额外的指示,mode指定了访问权限。

关闭文件函数:int close(int fd)。fd是打开文件时的返回值。

读文件函数:ssize_t read(int fd, void *buf, size_t n)。

写文件函数:ssize_t write(int fd, const void *buf, size_t n)。

8.3 printf的实现分析

[转]printf 函数实现的深入剖析 - Pianistx - 博客园

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

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

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

首先来看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;

}

其中传递参数中的...表示不确定个数。

函数中的va_list实际上就是typedef后的char *。而va_list arg = (va_list)((char *)(&fmt) + 4)这句操作实际上就是得到了...中的第一个量。

之后我们调用vsprintf函数。vsprintf函数将我们需要输出的字符串格式化并把内容存放在buf中。并返回要输出的字符个数i。然后调用系统函数write来在屏幕上打印buf中的前i个字符,也就是我们要输出的格式串。

调用write系统函数后,程序进入到陷阱,系统调用 int 0x80或syscall等,通过字符驱动子程序打印我们的线性。

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

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

最后程序返回我们实际输出的字符数量i。

8.4 getchar的实现分析

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

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

先来看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;

}

可以看到getchar的底层实现是通过系统函数read实现的。getchar通过read函数从缓冲区中读入一行,并返回读入的第一个字符,若读入失败则返回EOF。read的具体实现如下:

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

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

8.5本章小结

本章简单的介绍了Unux的I/O操作是如何进行的,以及hello中设计到的两个I/O函数printf和getchar是怎么实现的。在我看来Unix I/O是一个非常有趣且成功的抽象,因为它把一切输入输出都归结为对文件的操作,而且这一抽象是十分成功的,因为I/O的过程本质上就是一个对信息交换的过程,因此把所有与程序进行信息交换的主体,比如网络设备当作文件是完全没问题的。

结论

1.输入:将hello.c代码从键盘输入。

2.预处理(cpp):将hello.c进行预处理,将c文件调用的所有外部的库展开合并,生成hello.i文件。

3.编译(ccl):将hello.i文件进行翻译生成汇编语言文件hello.s。

4.汇编(as):将hello.s翻译成一个可重定位目标文件hello.o。

5.链接(ld):将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello,至此可执行hello程序正式诞生。

6.运行:在shell中输入命令。

7.创建子进程:由于终端输入的不是一个内置的shell命令,因此shell调用fork函数创建一个子进程。

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

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

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

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

12.信号管理:当程序在运行的时候我们输入Ctrl+C,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+Z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。

13终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。

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

附件

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

(附件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
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值