HIT-CSAPP-大作业-Hello的一生

计算机系统大作业

题     目  程序人生-Hello’s P2P 

专       业       计算学部        

学     号     -————        

班     级     —————          

学       生        张——         

指 导 教 师        史先俊          

计算机科学与技术学院

2022年5月

摘  要

    本文我们将追随hello从一个C程序经历预处理、编译、汇编、链接的过程脱胎换骨,从我们熟知的C代码变成一个可执行程序;在它运行的过程中,我们继续追寻它的踪迹,见证shell为他开辟进程,在他结束后进行回收;在程序运行时我们也能看到存储器之间复杂密集的交流过程,见证系统整齐一致的IO管理方式。hello的一生,让我们看到计算机系统的神奇精密。

关键词:编译,链接,进程管理,硬件存储结构,系统级IO                          

目  录

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

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

  1. Hello的P2P(Program to process)

Hello经历的P2P即为从程序到进程的过程:首先编写完的C程序hello.c通过预处理器进行预处理生成hello.i,将带#的头文件引入和宏定义替换为完整的C代码;然后,hello.i经过编译器的编译生成汇编文件hello.s,从C语言转变为方便机器执行的汇编语言,hello.s再经过汇编器生成机器语言二进制文件hello.o,最后通过连接器,将程序和函数库中的预编译好的函数进行链接生成可执行的目标文件hello。最后我们用命令./hello 120L021227 zy 2对其进行执行(后面为我们写入的参数),shell会调用fork生成一个子进程,然后调用execve执行hello程序。

  1. hello的020

在Shell收到命令运行hello时,会首先调用fork函数创建一个子进程,在这个子进程中调用execve来加载hello;接下来程序运行,CPU为hello进程分配执行的时间片、逻辑控制流,通过TLB以及分页机制来让hello所需的数据从磁盘到达寄存器,在调用硬件输入输出,在显示器上输出结果。

在进程终止后,shell对hello进程进行回收,内核从系统上删除hello进程的相关信息,虚拟空间的程序又回到0,这就是020的过程。

1.2 环境与工具

硬件环境:Intel Core i7 9300H X86-64CPU2.40GHz16GRAM

软件环境:VMware 15 Ubuntu18.04

开发调试工具: gccasldedbreadelfCodeBlocks

1.3 中间结果

文件名

文件来源/作用

hello.c

hello的C代码源程序

hello.i

hello.c经过预处理得到

hello.s

hello.i经过编译得到

hello.o

hello.s经过汇编得到

hello.o.elf

hello.o的ELF格式文件,通过readelf输出到文件中

hello.objdump

hello.o的反汇编得到的文本文件

hello

hello.o经过链接器链接后得到的可执行文件

hello.elf

hello的ELF格式文件,通过readelf输出到文件中

a.out

直接执行hello程序产生的默认输出程序

hello.out

使用gcc对输出文件名修改后的输出程序

1.4 本章小结

这一章我们大致展开叙述了hello从代码产生到运行之间P2P的过程,以及在进程创建与回收时的020过程,对环境工具进行简单记录,以及在这一章列出了我们实验过程中产生的中间文件。下一章我们的hello之旅就要正式展开了!

第2章 预处理

2.1 预处理的概念与作用

1.预处理概念

预处理(cpp)根据以#开头的命令,修改原始的C程序;预处理通常包括三个

过程:

  1. 对#include的文件进行包含;
  2. 对代码中#define定义的常量进行替换;
  3. 对代码中的所有注释进行删除。

2.预处理的作用

     预处理会根据#include命令告诉预处理器读取指定的系统头文件,并把头文件直接插入程序文本中;会根据#define命令对程序中的常量进行替换。结果会得到另一个C程序,通常是以.i为扩展名。

2.2在Ubuntu下预处理的命令

gcc -E hello.c -o hello.i

图2.1 Ubuntu下预处理命令

如图,在执行指令后出现了hello.i

2.3 Hello的预处理结果解析

图2.2 hello.i

预处理在程序main函数之前插入了大量的代码,例如对printf的声明等,我们可以看到代码的数量瞬间变成了三千余行,为接下来的编译工作做准备;

2.4 本章小结

本章对预处理的概念和作用进行简单解释,并给出进行预处理的代码以及结果分析。

第3章 编译

3.1 编译的概念与作用

1.编译的概念:

在编译阶段,编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。编译程序通过五个阶段将源程序翻译成目标程序:词法分析、语法分析、语义检查与中间代码生成、代码优化、目标代码生成。

2.编译的作用

       编译能够将高级语言源程序翻译成目标程序,以便进行下一步处理。

3.2 在Ubuntu下编译的命令

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

如图即为编译产生的文件hello.s,可以看到main函数部分被替换为了汇编代码。

3.3 Hello的编译结果解析

3.3.1数据处理的分析

1.常量

在main函数中出现的两个字符串即为代码中的两处常量。

在进行编译后的汇编代码中,字符串被存放在main函数开始前的声明片段.LC1中。

另一个字符串在原本的main函数中是中文串,在.LC0中进行保存。

2.变量

  1. argc和argv

  1. i被存放在栈中使用,地址为%rbp-4;

  1. argc

根据在源程序中与4的比较条件语句,我们可以找到argc被存放的位置。

 

正在上传…重新上传取消正在上传…重新上传取消

可以发现作为传入参数,argc首先被存放在寄存器edi中,接着被存放在栈中

%rbp-20的位置上。

3.3.2赋值的分析

根据源程序,变量i在创建时并没有被赋值;

在后续的for循环进行时才被赋值。

可以在汇编代码中找到对应的语句。

可以看到汇编指令在.L2中用movl进行赋值操作,将0赋值给存储在%rbp-4位置的i。

3.3.3类型转换操作

在for循环体中调用sleep()函数的时候,其中的表达式atoi(atgv[3])使用了显示的类型转换操作,将字符串类型的数据转换成整数类型作为sleep函数的输入。

可以看到,在调用printf之后,调用atoi之前进行了调用函数的准备工作,比如46行对地址的解引用,以及转移数据到指定的寄存器rdi中。接着48行调用call指令进行类型转换。之后产生的数据被从eax移动到edi,作为sleep函数运行的参数传入。

3.3.4算数操作的分析

在源程序中的循环体中,伴随每次循环体进行都有i的自增操作。

可以看到当.L2每一次结构结束时都会对存放在%rbp-4位置的i进行addl操作将其值加1.

3.3.5关系操作的分析

  1. 对i进行的比较操作

每次循环体也会把i和8进行比较判断是否执行循环体中语句。

但是我们找到比对的语句,发现编译器对这个判断语句做了修改;原本小于8的判断被修改为了和7作比较。

接下来的jle语句是在满足小于等于的状况下进行跳转,因此,编译器对小于的判断作了修改优化,将小于8改为小于等于7的判断。

  1. 对argc进行的比较操作

如图源程序对argv进行计算操作判断其与4是否相等,对应cmpl语句。

3.3.6数组操作

在main函数中argv[]被传入;

根据汇编代码,argv的地址被存放在rsi中,通过movq指令存入栈。

3.3.7控制转移操作

控制转移操作出现在源代码的if和for结构中。

  1. If判断语句

在源代码中,对argc的值进行判断决定是否执行print以及exit()函数。

可以看到在汇编代码中的对应语句,将argc与4比较,若相等则进行跳转;

因为if结构体中的语句被顺序写在jump语句后面的语句中,若未满足jump的条件(在这里是je,即如果cmpl与4相等的时候才进行跳转),就会执行if大括号中的语句。

  1. For循环语句

.L4中放置了for循环体中的内容,以及每次循环执行完毕后i++的自增操作也放进了循环体中。

接着在进入.L3之后首先就是判断语句,通过对i和7进行比较操作来确定是否跳转回.L4中。需要注意的是这里原本条件语句是判断i是否小于8,在这里换成了是否小于等于7.可见编译器进行了一定的修改优化。

3.3.8函数操作

  1. 调用exit()函数

将1放到edi然后调用exit。

  1. 调用printf()函数

第一次调用了if判断语句中的printf

直接将.LC0中存放的语句调用puts@PLT输出。

第二次是for循环中的printf函数。

这一次调用涉及到的变量比较多,而且进行的调用是不一样的printf@PLT函数。

由于变量比较多且都是数组类型的引用,这里进行了比较多的准备工作,如6行和9行对地址的解引用操作,需要注意的是在解应用前的addq操作(第五行和第8行),分别是+16和+8,将原本数组的索引乘以数据类型占用的位数之后再进行寻址。

接下来是将.LC1中存放的字串放到rdi上然后执行call调用printf函数。

  1. Sleep()函数和atoi()函数

在源程序中它们在同一条语句中出现,atoi将字符串转为int类型整数作为sleep的参数。

可以看到44到59行的调用过程以及调用前的准备。对于atoi函数我们在3.3.3中已经进行过分析。在调用atoi之后,将存放在eax中的结果移动到edi作为下一个sleep函数的参数,然后发起对sleep的调用。

  1. getchar()函数

如图在55行可见对call的调用过程。

3.4 本章小结

在这一章节中,我们解释了编译的概念及作用并在Ubuntu系统上对hello程序进行了编译,对编译结果进行了分析。

通过分析编译产生的汇编代码,我们发现在C语言中的各种操作都可以对应到汇编代码中,了解了编译对我们的代码作了怎样的处理。

第4章 汇编

4.1 汇编的概念与作用

1.汇编的概念

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

2.汇编的作用

经过汇编过程可以从汇编指令进一步得到CPU可以执行的机器指令。

4.2 在Ubuntu下汇编的命令

gcc -m64 -no-pie -fno-PIC -c -o hello.o hello.s

如图即为指令产生的对应文件。由于是二进制文件我们在这里是不能直接进行查看的。

之后我们采用反汇编的形式再来对它进行察看。

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

  1. ELF头

在使用readelf语句之后,我们首先看到用来描述ELF文件各个信息的ELF头。

(ELF Header)

2.节头

在ELF头之后的是节头(Section Header)。

在节头中有各个节的节类型、地址以及它们占用的空间大小等信息。

3.重定位节

这里是重定位节。在重定位节中包含了.text节中需要进行重定位的信息,提供给链接器,方便链接器将目标文件与其它文件链接起来。

在偏移量(Offset)栏中标记了需要重定位的位置在.txt中的偏移量;

在信息(Info)一栏中,最左侧的4位是省略前四位的高四字节,用以保存所调用的函数的符号表索引;

右侧的8位为信息的低四字节,定义了重定位的方式type。可以看到不同类型采用不同的重定位方式。

在最右侧的函数符号名称后还有加数(Addend),这是由于它们的重定位需要用其对被修改引用的值作偏移调整。

4.符号表(Symbol table)

这里就没有进行汉化。符号表.symtab中存放着程序中定义,引用的函数以及全局变量的信息,在符号表中对重定位需要引用的符号都进行了声明。

4.4 Hello.o的结果解析

objdump -d -r hello.o  分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

1.反汇编过程

在这里我们直接输入语句objdump -d -r hello.o得到的反汇编文本直接在终端中进行显示。我们改变指令将内容输出到文本文件中。

输入objdump -d -r hello.o>hello.objdump,我们得到了文本文件格式的反汇编代码。

2.jump指令地址的替换

我们将返汇编后的代码与编译后的代码进行对照比较。

我们首先注意到,这次的文件中已经没有了.L2,.LFB6这样的段名称标记,而对应的je标记的跳转位置从原先的段名称变成了main的地址加上偏移量,如我们图中标记出的.L2的位置,跳转语言从je .L2 变成了je  2f<main+0x2f>;在跳转到该位置之后我们可以看到一条相同的movl的指令,只是数值的显示方式略有不同。

类似的,所有的jump指令都被进行了对应的替换。

3.call指令地址的替换

接着我们把两个文件中call命令的指令对应起来。我们发现在右侧call指令被具体华为64位的callq,后面跟上了具体的地址<main+偏移量>,这个是为了指示下一条指令的位置的——例如第一句callq后面的是25,对应下一条指令mov的相对地址。

后面跟的才是调用的函数信息。正好可以和我们在重定位节中看到的信息联合起来,后面还标注了调用函数的类型,符号名称和加数等具体信息。

通过分析,我们发现在进行汇编之后所有的地址操作都被进行了详细的定位替换,在这里main的地址还未确定,要到链接后才会进一步确定,当前为全0.

4.5 本章小结

在这一章中,我们分析了汇编操作的概念与作用,并在ubuntu系统上进行汇编将hello.s文件汇编为hello.o文件。

由于产生的二进制文件无法直接访问,我们通过查看hello.o的ELF格式,以及用objdump进行反汇编等操作来对反汇编之后的代码进行分析,我们发现各种操作中的地址被具体化,做了详细定位。

5章 链接

5.1 链接的概念与作用

  1. 链接的概念

在链接阶段,链接器(ld)负责将预编译好的函数文件和汇编后的程序进行链接并进行合并处理。

  1. 链接的作用

在经过链接之后可以得到一个hello文件,它是一个可执行目标文件(简称可执行文件),可以被加载到内存中,由系统执行。

5.2 在Ubuntu下链接的命令

ld的链接命令:

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

如图即为输入的命令,以及进行链接后得到的结果程序hello。

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

在命令行输入readelf -a hello > hello.elf,将ELF格式导入文本文件中察看。

1.ELF

在打开hello.o的ELF时我们看到的入口点地址是0x0,在这一次我们得到的地址为0x4010f0,具有了真正的程序地址。

同时,程序类型由REL(可重定位文件)变成了EXEC(可执行文件)。

2.节头

我们可以看到这一次节头的容量比上一次大了不少,左侧的序号个数比.o程序要多了十来个,可见有更多的节被链接进了程序中。

  1. 程序头

在这里以文件状态给出了程序运行环境中的各种组件的种类和信息如文件大小,存储空间大小和文件状态等。

  1. 其他信息

在程序头之后还有Dynamic Section,重定位节等部分。可以看到main函数以及我们在主函数中调用的函数都有进行加载。

5.4 hello的虚拟地址空间

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

我们首先在edb中加载可运行程序hello。

根据edb提供的程序起始位置我们可以找到对应的ELF头所处的空间;

同时根据ELF文件的提示,我们可以找到位于0x004010f0的程序入口点地址。这里就是代码段起始的地方。

5.5 链接的重定位过程分析

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

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

我们用objdump对hello进行反汇编(这次叫hello2,跟上次区别一下)得到可执行程序的反汇编代码。接下来结合与hello.o的不同对新的代码进行分析

  1. 新的函数指令

与上一次的反汇编文件中只有main函数的语句块以及对其它函数的调用信息不同,这一次我们得到的代码中将各种调用的函数的汇编指令块都写入了代码中,说明hello通过重定位C函数库中的函数链接到了代码中。

我们可以看到新增的节section .plt.sec,在这里,我们可以找到前面熟悉的各种函数,puts,prnitf,getchar等;当然这里给的依然不是具体的函数实现过程,而是指向了已经通过预编译的函数文件的地址。

  1. 新的节以及节自带的函数

在hello中增加了新的节如section .init和section .plt、section .text、section .fini等节,以及它们自带的函数也被加入了程序。

  1. 语句中地址的定位

首先我们对第一行<main>的地址进行观察,main函数已经被从全0的地址改成了具体地址401125,说明在转换为可运行程序的过程中,hello的main函数被定位到了虚拟内存中。其他语句的地址也被替换成了具体的地址。

相应的,在hello.o中的jump指令的相对地址被替换成了具体的地址,我们把jump语句同原来的语句对应起来能够观察的清晰一点。

由于call后面不需要保存函数调用的一堆信息,因此call的后面也无需专门对下一条语句的位置进行指示,call后面的内容被替换为具体的地址,指向程序中的节section .plt.sec。

因此根据我们的分析和寻找资料可以大致还原重定位的过程:

首先进行关联符号的定义,链接器将代码中的每个符号引用和一个符号定义相关联,并获得需要输入的目标模块的代码节,数据节的确定大小;

随后,链接器进行重定位,把每个符号定义与一个内存位置关联起来,进而修改所有对这些符号的引用,使得它们指向这个内存位置;

通过符号解析以及重定位的过程,链接器将汇编后的可重定位程序链接成一个可运行程序。

5.6 hello的执行流程

使用edb执行hello,从加载hello到_start,到call main,以及程序终止的主要过程如下:

函数名                                           地址

ld-2.27.so!_dl_start                          0x7f96ed2e1ea0

ld-2.27.so!_dl_init                           0x7f96ed2f0630

hello!_start                                       0x403ff0

libc-2.27.so!__libc_start_main        0x7fbdf0cccab0

hello!puts@plt                                 0x404018

hello!exit@plt                                  0x404038

hello!printf@plt                               0x404020

hello!sleep@plt                               0x404040

hello!getchar@plt                            0x404028

libc-2.27.so!exit                               6b7b9e120

5.7 Hello的动态链接分析

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

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

首先我们用readelf读取hello程序的ELF格式,寻找到.got函数的位置,在函数运行的过程中地址中会指向下一个运行的函数。

       可以看到,.got位于位置0x403ff0,在程序运行前后我们需要到这个位置去寻找它发生的变化。接着我们进入edb,拖入hello程序开始追踪

这个时候我们从Data Dump跳转到0x403ff0的位置,此时程序未开始运行。

然后我们开始运行程序进行追踪;

可以看到在.got中存放的内容已经变化。

对于动态共享链接库中的PIC函数,编译器无法预测函数实际运行时的地址,因此需要添加重定位记录,等待动态链接器进行处理。链接器采用延迟绑定的策略。

动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,

GOT中存放了函数的目标地址,PLT使用GOT中地址跳转到目标函数。

5.8 本章小结

在这一章中,我们分析了链接的概念与作用并在Ubuntu上实践了链接的过程。经过对链接后的可执行程序hello进行elf以及objdump的反汇编分析,我们发现这一步程序被真正和系统函数结合在了一起,hello现在可以算是整装待发了,地址信息被进一步具体化,系统提前准备好的函数也被链接到了程序中。

6章 hello进程管理

6.1 进程的概念与作用

1.进程的概念

进程是一个执行中程序的实例。系统中每个程序都运行在某个进程的上下文中。

2.进程的作用

进程是对处理器、主存和磁盘I/O设备的抽象表示,通过进程,虚拟内存和文件等抽象概念,操作系统实现了防止硬件被失控的应用程序滥用,实现了向应用程序提供简单一致的机制来控制复杂又大不相同的低级硬件设备。

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

1.Shell-bash的作用

shell是系统的用户界面,是命令解释器,也是一种应用程序。

根据交互形式的不同,有图形界面的shell和命令行形式的shell。它接受用户的命令,并将命令送到内核执行。

Bash是目前最常用的shell,一般与我们所说的shell等价,我们输入echo $SHELL

可以查看当前运行的shell:

用户的默认shell一般都是bash或者与bash兼容。

  1. 处理流程
  • 读取从键盘输入的命令
  • 判断命令格式是否正确,将命令行的参数改造为系统调用execve()内部处理所要求的形式
  • 终端进程调用fork()来创建子进程,自身则用系统调用wait()来等待子进程完成
  • 当子进程运行时,它调用execve()根据命令的名字指定的文件到目录中查找可行性文件,调入内存并执行这个命令
  • 如果命令行末尾有后台命令符号&,终端进程不执行等待系统调用,而是立即发提示符,让用户输入下一条命令;

如果命令末尾没有&,终端进程要一直等待子进程完成处理。完成处理后子进程向父进程报告,唤醒终端进程,在进行清理后发送提示符让用户输入下一条命令。

6.3 Hello的fork进程创建过程

我们在hello所处的目录下输入./hello 120L021227 zy 10(学号,姓名,秒数)并回车运行,shell就会对我们输入的命令行进行解析,并通过fork函数为hello程序创建一个新的子进程。

新创建的子进程得到与父进程用户级虚拟地址空间相同的,但是独立的一个副本。

子进程与父进程直接拥有不同的PID。

6.4 Hello的execve过程

在调用fork()之后,被创建的新子进程会调用execve函数,在当前子进程的上下文中对hello程序进行加载,随后运行。

与fork一次调用返回两次不同,execve调用一次并从不返回,execve函数调用驻留在内存中被称为启动加载器的操作系统代码来执行程序,加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序。

6.5 Hello的进程执行

  1. 上下文信息

操作系统保持跟踪进程运行所需的所有状态信息,这种状态就是上下文,包括PC和寄存器文件的当前值,以及主存的内容。

当操作系统将控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文,恢复新进程的上下文,然后将控制权传递给新进程,这样新进程就会从它上次停止的地方开始;

  1. 进程切换

在我们运行shell进程和hello进程的时候,它们被视为并发的进程;最初的时候只有shell进程在运行,即等待命令行上的输入;当我们输入./hello 120L021227 zy 2

的时候,shell会运行fork开辟一个子进程,操作系统保存原来的shell进程的上下文,然后将控制权交给子进程通过系统调用execve创建的新进程hello;

如图,进程A为原来的shell进程,进程B为我们创建的hello进程;在控制流处于hello内的时候执行hello中的语句;当调用系统函数sleep的时候,根据我们前面的输入应该是sleep(2);进入内核态,控制权移交回系统执行系统函数sleep,此时间片停止;

在两秒后发送中断信号,转回用户模式,继续执行指令;

6.6 hello的异常与信号处理

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

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

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

  1. 不停乱按

可以发现乱按并没有阻止程序的正常运行,依然按照时间间隔对我们输出hello;

Shell将我们在程序运行期间输入的第一个回车之后的每一条语句都按照新的shell命令,在hello进程被回收之后对这些命令进行逐条执行。

根据推测,第一个回车之前的字符被当作了getchar的输入。

  1. Ctrl+C

在我们按下Ctrl+C后,shell程序会收到一个SIGINT信号,shell会强行停止hello进程,然后进行回收;

  1. Ctrl+Z(+ps,+jobs)

在我们按下Ctrl+Z后,shell程序会受到一个SIGSTP信号,在终端可以看到停止进程的提示,这个时候hello进程被挂起;

接着我们输入ps来查看当前进程,可以在后台发现被挂起的hello进程。

同样,我们也可以输入jobs,在作业组中同样可以发现hello的踪迹;

前面编号1的是我前面尝试乱按的时候按到as产生的进程!所以这里hello被排在第二了。运气真是奇妙。

  1. Ctrl+Z,+pstree,+kill

在我们使用Ctrl+Z挂起hello之后,我们可以输入pstree来查看进程树,看看能不能再次找到它:

在这里展示了所有进程的树状图。

我们找到hello在树状图中的位置,位于bash分支下。

输入kill %2(job编号),我们可以终止hello程序,再调用ps进行查看,我们可以看到hello进程已经被我们清理掉了。

  1. fg

在输入Ctrl+Z之后,输入fg命令可以让hello进程回到前台执行,shell首先打印hello的命令行命令,然后将控制权移交给hello继续运行。

6.7本章小结

在这一章,我们分析了进程的概念和作用,简单描述了shell的概念以及处理流程,接着对shell通过fork函数创建一个新进程,hello的execve过程进行了分析;

在hello的进程执行中我们观察了shell和hello两个进程之间上下文切换以及控制权交接的过程,在hello的异常分析与处理一节中我们又看到了在程序过程中不同的操作给程序进行带来的影响,如停止,挂起,前后台切换等。

通过这一章,我们对shell-bash与进程控制有了更详细的理解。

7章 hello的存储管理

7.1 hello的存储器地址空间

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

  1. 逻辑地址:指由程序产生的与段相关的偏移地址部分,是CPU生成的地址,格式为段地址:偏移地址。
  2. 线性地址:线性地址是逻辑地址到物理地址变换之间的中间层,在分段部件中,逻辑地址是段中的偏移地址,之后逻辑地址加上基地址就是线性地址。
  3. 虚拟地址:根据课本虚拟地址与线性地址是等价的。
  4. 物理地址:加载到内存地址寄存器中的地址,在前端总线上传输的内存地址都是物理内存地址,这些数字被北桥(Nortbridge chip)映射到实际的内存条上。物理地址是明确的、最终用在总线上的编号。

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

逻辑地址的表达方式是<选择符,偏移>,选择符的内容构成如图:

最左侧的13位是索引部分,通过这个索引,可以定位到段描述符(segment descriptor),段描述符记录了段位置,段的大小以及访问控制的状态等信息,段描述符一般由8个字节组成,由于8B较大,Intel为了保持向后兼容性仍然将段寄存器规定为16bit。实际上每个段寄存器其实有64bit长的不可见部分,但对于程序员来说段寄存器只有16bit;在使用过程中,我们不能把64位的段描述符直接放进16位的段寄存器,因此我们只用13位来记录段描述符的索引,把段描述符的本体放进数组中进行保存。

在内存中的数组就叫做GDT(Global Descriptor Table,全局描述表),Intel提供了一个寄存器GDTR来存放GDT的入口地址。在将GDT设定在内存中某位置之后,我们可以通过LGDT指令将GDT的入口地址装入此寄存器,CPU此后就可以根据GDTR中的内容作为GDT的入口来访问GDT了。

除GDT还有LDT(Local Descriptor Table,本地描述表),但与GDT的是,LDT可以在系统中可以存放多个,每个进程可以拥有自己的LDT。LDT的内存地址在LDTR寄存器中。

逻辑地址中的TI位,就是用来表示此索引指向的段描述符是存在于全局描述表还是本地描述表,当TI=0时表示使用GDT,当TI=1时表示使用LDT。

RPL位占2bit,是保护信息位,当找到段描述符之后加上偏移量就得到了线性地址。

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

CPU通过逻辑地址转换为虚拟地址来访问主存,在进行主存访问之前需要将虚拟地址再转换成适当的物理地址;CPU芯片上提供MMU(内存管理单元)来利用存放在主存中的查询表来动态翻译虚拟地址,再通过得到的物理地址访问物理内存。

页表结构:再物理内存中存放了一个叫做页表的数据结构,页表将虚拟页映射到物理页,每次地址翻译硬件将一个虚拟地址转换为物理地址时,就会读取页表;页表是一个由页表条目(PTE)构成的数组,虚拟地址空间的每个页再页表中的一个固定偏移量处都有一个PTE。

PTE由一个有效位和一个n字段组成,有效位表面该虚拟页当前是否被缓存在了DRAM中。若有效位被设置为有效,地址字段就表示DRAM中相应物理页的起始位置;

MMU利用虚拟页号(VPN)来再虚拟页表中选择合适的PTE,当找到合适PTE之后将PTE中的PPN(物理页号)和VPO(虚拟页偏移量)组合成一个物理地址,VPO与PPO相同,虚拟页的大小和物理页的大小是相同的,所以所需偏移的位数也相同;此时使用物理地址,通过物理页号找到对应物理页,再根据物理页偏移即可找到具体的字节。

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

TLB中缓存了页表条目PTE,当需要进行地址翻译的时候首先MMU先查询TLB,若TLB含有所需的PTE,即内存命中;

不命中则MMU再次从高速缓存中查找PTE。

用于查询TLB所需的组选择和行匹配的索引、标记字段等都是从虚拟地址的虚拟页号中提取出来的,当TLB有T=2^t个组的时候,组索引就是虚拟页号VPN的低t位,其余位为TLB标记。当发生TLB不命中时,MMU从L1缓存中取出相应的PTE;当取出一个新的PTE存放在TLB中时,可能会覆盖一个以及存在的条目。

从VA到PA的变换:

首先CPU计算出一个虚拟地址,将其传到MMU,MMU使用虚拟地址的VPN对TLB进行查询,若命中则直接将对应PTE中的物理页号与虚拟页偏移量组合起来形成物理地址,若发生TLB不命中则查询四级结构;然后构造出物理地址。

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

Core i7 MMU使用四级页表来将虚拟地址翻译成物理地址,之后我们得到物理地址PA,由于L2 、L3和L1的处理结果类似,所以我们只具体分析一级cache。

L1 Cache为8路,64组,块大小为64B,因此CO和CI都是6位,CT占40位;由物理地址PA,首先使用CI进行组索引,每组8路,分别匹配标记CT;若匹配成功且有效位为1(标记为有效),则内存命中,根据块偏移CO返回需要的数据。

若匹配未成功,或匹配后标志位为1则不命中,向下一级缓存取出需要的块并将新块加载到缓存中。

一般而言,映射到的组内由空闲块时可以直接进行放置,否则则需要覆盖一个现存的块,一般采取最近最少被使用策略LRU来完成替换。

7.6 hello进程fork时的内存映射

当前进程调用fork函数时,内核为新进程创建数据结构,并分配一个唯一的PID,为新的进程创建当前进程的mm_struct、区域结构和页表的副本,内核将两个进程中的每个页面标记为只读,再将两个进程中的每个区域结构都标记为私有的写时复制。

当fork函数再新进程中返回时,hello进程正好拥有和调用fork前存在的虚拟内存是相同的,当这两个进程中的任意一个在后来进行写操作时,写时复制机制就会创建新页面,也就为每一个进程保持了私有的地址空间。

7.7 hello进程execve时的内存映射

execve函数经过以下步骤加载和执行程序hello:

  1. 删除已存在的用户区域
  2. 映射私有区域。为Hello的代码、数据、bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时复制的。
  3. 映射共享区域。比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
  4. 设置程序计数器PC。

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

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

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

3) 高速缓存/主存向MMU返回PTE。

4) PTE中的有效位为0,那么此时MMU就触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。缺页处理程序首先会对虚拟地址的合法性进行判断,即缺页处理程序搜素区域结构的链表,把该虚拟地址和每个区域结构中的vm_start和vm_end进行比较,若该虚拟地址没有在某个区域结构定义的区域内,那么将会触发一个段错误,进程终止。或者缺页是由对一个只有读权限的区域进行的写操作引起的,那么将会触发一个保护异常,从而终止这个进程。最后,若该虚拟地址时合法的,则继续下一步。如图7.14

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

6) 缺页处理程序调入新的页面,并更新内存中的PTE。

7) 缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,在MMU执行了图7.13中的步骤之后,主存就会将所请求字返回给处理器。

7.9动态存储分配管理

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

1.显式分配器:要求应用显式地释放任何已分配的块。例如C程序通过调用malloc函数来分配一个块,通过调用free函数来释放一个块。其中malloc采用的总体策略是:先系统调用sbrk一次,会得到一段较大的并且是连续的空间。进程把系统内核分配给自己的这段空间留着慢慢用。之后调用malloc时就从这段空间中分配,free回收时就再还回来(而不是还给系统内核)。只有当这段空间全部被分配掉时还不够用时,才再次系统调用sbrk。当然,这一次调用sbrk后内核分配给进程的空间和刚才的那块空间一般不会是相邻的。

2.隐式分配器:也叫做垃圾收集器,例如,诸如Lisp、ML、以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。

隐式空闲链表:

这样的一种结构,主要是由三部分组成:头部、有效载荷、填充(可选);

头部:是由块大小+标志位(a已分配/f空闲);有效载荷:实际的数据

简单的放置策略:

1> 首次适配:从头搜索,遇到第一个合适的块就停止;

2> 下次适配:从头搜索,遇到下一个合适的块停止;

3> 最佳适配:全部搜索,选择合适的块停止。

分割空闲块:适配到合适的空闲块,分配器将空闲块分割成两个部分,一个是分配块,一个是新的空闲块。

增加堆的空间:通过调用sbrk函数,申请额外的存储器空间,插入到空闲链表中 。

合并:

(1)合并时间:立即合并和推迟合并。

立即合并:在每次一个块被释放时,就合并所有的相邻块

推迟合并:直到某个分配请求失败时,扫描整个堆,合并所有的空闲块。

(2)合并:(4种情况)

a.当前块前后的块都为已分配块:不需要合并

b.当前块后面的块为空闲块:用当前块和后面块的大小的和来更新当前块的头部和后面块的脚部。

c.当前块前面的块为空闲块:用当前块和前面块的大小的和来更新前面块 的头部和当前块的脚部。

d.当前块的前后块都为空闲块:用三个块大小的和来更新前面块的头部和 后面块的脚部。

其中,查询前面块的块大小时可以通过脚部来查,查询后面块的块大小时 可以通过头部来查。

7.10本章小结

在本章我们首先进行hello存储器地址空间的分析,随后分析Intel逻辑地址到线性地址的变换过程,线性地址到物理地址的变换过程,在TLB与四级页表支持下从VA到PA的变换,三级Cache下的物理内存访问,从地址解析的角度分析了程序运行时发生的事情;

接着是hello进程fork时的内存映射,execve时的内存映射。以及缺页故障和缺页中断处理,动态存储分配管理。经过本章的详细分析,我们对程序运行时具体的内存调用过程以及各种处理方法有了更加深入的理解。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

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

设备的模型化:文件

文件类型:

  1. 普通文件(regular file):包含任意数据的文件。
  2. 目录(directory):包含一组链接的文件,每个链接都将一个文件名映射到一个文件(他还有另一个名字叫做“文件夹”)。
  3. 套接字(socket):用来与另一个进程进行跨网络通信的文件
  4. 命名通道
  5. 符号链接
  6. 字符和块设备

设备管理:unix io接口

  1. 打开和关闭文件
  2. 读取和写入文件
  3. 改变当前文件的位置

8.2 简述Unix IO接口及其函数

1.打开和关闭文件

int open(char *filename, int flags, mode_t mode);

open函数将filename转换为一个文件描述符,并返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。

int close(int fd);

进程通过调用close关闭一个打开的文件。

2.读写文件

ssize_t read(int fd, void *buf, size_t n);

read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。

ssize_t write(int fd, const void *buf, size_t n);

write函数从内存位置buf复制之多n个字节到描述符fd的当前文件位置。

DIO *opendir(const char *name);

函数opendir以路径名为参数,返回指向目录流的指针。流是对条目有序列表的抽象,在这里是指目录项的列表。

struct dirent *readdir(DIR *dirp);

每次对readdir的调用返回的都是指向流dirp中下一个目录项的指针,或者,如果没有更过目录项则返回NULL。

int closedir(DIR *dirp);

函数closedir关闭流并释放其所有的资源。

3.I/O重定向

int dup2(int oldfd, int newfd);

dup2函数复制描述符表表项oldfd到描述符表项newfd,覆盖描述符表表项newfd以前的内容。如果newfd已经打开了,dup2会在复制oldfd之前关闭newfd。

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;

}

首先arg获得第二个不定长参数,即输出的时候格式化串对应的值。

查看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':     //只处理%x一种情况

                itoa(tmp, *((int*)p_next_arg)); //将输入参数值转化为字符串保存在tmp

                strcpy(p, tmp);  //将tmp字符串复制到p处

                p_next_arg += 4; //下一个参数值地址

                p += strlen(tmp); //放下一个参数值的地址

                break;

            case 's':

                break;

            default:

                break;

        }

    }

    return (p - buf);   //返回最后生成的字符串的长度

}

则知道vsprintf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。

在printf中调用系统函数write(buf,i)将长度为i的buf输出。write函数如下

write:

mov eax, _NR_write

mov ebx, [esp + 4]

mov ecx, [esp + 8]

int INT_VECTOR_SYS_CALL

在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall,查看syscall的实现:

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

syscall将字符串中的字节“Hello 120L021227 zy”从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。

字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。

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

于是我们的打印字符串“Hello 120L021227 zy”就显示在了屏幕上.

8.4 getchar的实现分析

异步异常-键盘中断的处理:用户从键盘输入,触发了一个中断信号,当前进程被抢占,进而开始执行键盘中断处理子程序。键盘中断处理子程将输入的字符序通过按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar底层实现使用了read系统函数,通过系统调用来触发一个中断信号,执行一次上下文切换,当前进程被挂起CPU去执其他进程,read读取按键ascii码,直到接受到回车键才返回,当read函数返回时,当前进程重新被调度,getchar获得输入的第一个字符。

8.5本章小结

在本章我们分析了Linux的IO设备管理方法,并对UnixIO接口及其函数进行了简单介绍;最后通过printf和getchar两个基础函数的实现,让我们看到了linux能够高效运行的优势所在:进行简单高效的IO管理。

结论

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

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

hello的经历如下:

  1. 编写,通过editor将代码键入hello.c
  2. 预处理,将hello.c调用的所有外部的库展开合并到一个hello.i文件中
  3. 编译,将hello.i编译成为汇编文件hello.s
  4. 汇编,将hello.s会变成为可重定位目标文件hello.o
  5. 链接,将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello
  6. 运行:在shell中输入./hello 1170300825 lidaxin
  7. 创建子进程:shell进程调用fork为其创建子进程
  8. 运行程序:shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数。
  9. 执行指令:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流
  10. 访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
  11. 动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。
  12. 信号:如果运行途中键入ctr-c ctr-z则调用shell的信号处理函数分别停止、挂起。
  13. 结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。

我的感想:一个小小的hello蕴含了多少智慧。通过这一个程序,我们实实在在的与计算机系统认识了。我们看到数据在计算机的内部运作,配合的神奇方式,看到了计算机复杂精密设计的冰山一角。这一次大作业需要的精力很多,但收获也很大!

附件

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

参考文献

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值