程序人生-Hello’s P2P

 

 

 

计算机科学与技术学院

2019年12月

摘  要

摘要:本文通过对hello程序P2P和020的整体介绍,阐述了hello经过多种处理后成为Process,并且hello进程创建直到回收的全部过程。以这些过程的分析为例,我们更好地说明了计算机的底层实现,并且更深地阐明了整个程序的生命周期。

关键词:P2P;020;进程;计算机系统                           

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

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

1.2 环境与工具

硬件环境:Intel core i7 7700hq ,16G RAM

软件环境:Window 10 1903;Vmware 15;Ubuntu 18.04

开发工具:vim, gcc , gedit , Codeblocks , gdb, edb

1.3 中间结果

hello.c :hello源代码

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

hello.s :hello.i编译后的汇编文件

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

hello_objdump :hello的反汇编代码

hello.o_objdump :hello.o的反汇编代码

hello :链接后的可执行文件

1.4 本章小结

本章对hello进行了简单的介绍,分析了其P2P和020的过程,列出了本次任务的环境和工具,并且阐明了任务过程中出现的中间产物及其作用。

(第1章0.5分)

 


第2章 预处理

2.1 预处理的概念与作用

预处理器(cpp)根据以#开头的命令,修改原始的C程序。比如hello.c中第6行的#include<stdio.h>命令高速预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以.i作为文件拓展名。

2.2在Ubuntu下预处理的命令

命令:gcc -E hello.c -o hello.i

 

图2-1 Ubuntu下预处理命令

 

图2-2 预处理结果hello.i文件(部分)

2.3 Hello的预处理结果解析

修改得到的C程序hello.i已经从原来hello.c的534个字节增加到66106个字节,并且增加到3118行。再用vim打开hello.i,发现在main函数在文件的最后部分。

而在main函数之前,预处理器(cpp)读取头文件stdio.h 、stdlib.h 、和unistd.h中的内容,三个系统头文件依次展开。比如stdio.h的展开,打开usr/include/stdio.h发现了其中还含有#开头的宏定义等,预处理器会对此继续递归展开,最终的.i程序中没有#define,并且针对#开头的条件编译语句,cpp根据#if后面的条件决定需要编译的代码。

2.4 本章小结

本阶段完成了对hello.c的预处理工作。使用Ubuntu下的预处理指令可以将其转换为.i文件。完成该阶段转换后,可以进行下一阶段的汇编处理。

(第2章0.5分)


第3章 编译

3.1 编译的概念与作用

编译程序也称为编译器,是指把用高级程序设计语言书写的源程序,翻译成等价的汇编语言格式目标程序的翻译程序。编译程序属于采用生成性实现途径实现的翻译程序。它以高级程序设计语言书写的源程序作为输入,而以汇编语言表示的目标程序作为输出。

编译程序的基本功能是把源程序(高级语言)翻译成目标程序。除了基本功能之外,编译程序还具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用以及人机联系等重要功能。

3.2 在Ubuntu下编译的命令

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

 

图3-1 Ubuntu下编译命令

 

图3-2 编译结果hello.s文件(开头)

3.3 Hello的编译结果解析

3.3.1 全局变量与全局函数

在hello.c中,包含一个全局变量int sleepsecs=2.5;以及一个全局函数int main(int argc,char *argv[]);。经过编译之后,sleepsecs被存放在.rodata节中。而main函数中使用的字符串常量也被存放在数据区。其中,由于sleepsecs被定义为int型,所以为其赋初值2.5后,会进行隐式的类型转换,变为2。

 

图3-3 全局变量和全局函数

3.3.2 主函数的参数

主函数的参数部分给出了int argc,char *argv[]两个参数。在汇编代码中,分别将其存放在栈中rbp寄存器指向地址-20和-32处,如下图所示。其中%edi代表argc,%rsi代表argv[]。

 

图3-4 对传入参数的处理

3.3.3 条件判断语句及分支

接着在main函数中,使用if语句进行了条件判断。cmpl语句进行判断条件的比较。如果条件满足则继续顺序执行,调用puts输出给定字符串(这里puts是对printf的优化),然后使用参数1调用exit结束程序。对应的汇编代码如下。

 

图3-5 if条件语句段对应的汇编代码

3.3.4 循环结构及主函数结尾部分

接下来进入for循环语句部分。该部分使用了一个局部变量i,该变量存放在栈中rbp寄存器指向地址-4处。首先对其置零进行初始化(35行)。接着使用jump to middle模式进入.L3使用cmpl语句先进行条件判断。如果条件满足,那么进入.L4循环体部分调用printf函数和sleep函数。

在调用printf的过程中,进行了数组访问(argv[1]和argv[2])。而argv是指针数组,所以会进行二次寻址。在汇编代码中,38至40行取出argv[2]对应的内容,并放入三号参数寄存器%rdx中。41至44行取出argv[1]对应的内容,并放入二号参数寄存器%rsi中。45行将格式字符串放到一号参数寄存器%edi中,然后调用printf函数进行显示。48-50行读取sleepsecs全局变量并调用sleep函数。最后51行对计数量进行加一,结束循环体部分。

最后调用getchar函数,将返回值设为0,主函数正常返回。

 

图3-6 循环结构及主函数结尾部分

3.4 本章小结

本阶段完成了对hello.i的编译工作。使用Ubuntu下的编译指令可以将其转换为.s汇编语言文件。此外,本章通过与源文件C程序代码进行比较,完成了对汇编代码的解析工作。完成该阶段转换后,可以进行下一阶段的汇编处理。

(第32分)


第4章 汇编

4.1 汇编的概念与作用

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

4.2 在Ubuntu下汇编的命令

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

 

图4-1 hello.s汇编生成hello.o文件

4.3 可重定位目标elf格式

1. ELF头描述了生成该文件的系统的字的大小和字节顺序,并且包含帮助链接器语法分析和解释目标文件的信息。

 

图4-2 ELF头

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

 

图4-3 节头部表

3. 当汇编器生成一个目标模块是,它并不知道数据和代码最终将放在内存中的什么位置,它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行目标文件时如何修改这个引用。代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中。

ELF重定位条目的数据结构如下:

typedef struct{

    long offset;        /*需要被修改的引用的节偏移*/

    long type:32,     /*重定位类型*/

    symbol:32; /*标识被修改引用应该指向的符号*/

    long attend;       /*符号常数,对修改引用的值做偏移调整*/

}Elf64_Rela;

两种最基本的重定位类型:

R_X86_64_PC32 :重定位一个使用32位PC相对地址的引用。

R_X86_64_32 :重定位一个使用32位PC绝对地址的引用。

根据图4-4,可以看出8条重定位信息的详细情况,分别对符号.rodata,函数puts,exit等,加数也在符号名称之后。

 

图4-4 重定位节

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

 

图4-5 符号表.symtab

4.4 Hello.o的结果解析

反汇编命令:objdump -d -r hello.o

对比hello.s文件和反汇编代码,主要有以下的差别

1. 操作数:hello.s中的操作数时十进制,hello.o反汇编代码中的操作数是十六进制。

2. 分支转移:跳转语句之后,hello.s中是.L2和.L3等段名称,而反汇编代码中跳转指令之后是相对偏移的地址。

3. 函数调用:hello.s中,call指令之后直接是函数名称,而反汇编代码中call指令之后是函数的相对偏移地址。因为函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。

4. 全局变量的访问:在hello.s文件中,对于.rodata和sleepsecs等全局变量的访问,是$.LC0和sleepsecs(%rip),而在反汇编代码中是$0x0和0(%rip),是因为它们的地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。

 

图4-6(1) hello.s和反汇编代码的对比

 

图4-6(2) hello.s和反汇编代码的对比

4.5 本章小结

通过汇编操作,汇编语言转化为机器语言,hello.o可重定位目标文件为后面的链接做了准备。通过对比hello.s和反汇编代码的区别,更深刻地理解了汇编语言到机器语言实现地转变,和这过程中为链接做出的准备(设置重定位条目等)。

(第41分)


第5章 链接

5.1 链接的概念与作用

链接程序将分别在不同的目标文件中编译或汇编的代码收集到一个可直接执行的文件中。它还连接目标程序和用于标准库函数的代码,以及连接目标程序和由计算机的操作系统提供的资源(例如,存储分配程序及输入与输出设备)。链接工作大致包含两个步骤,一是符号解析,二是重定位。在符号解析步骤中,链接器将每个符号引用与一个确定的符号定义关联起来。将多个单独的代码节和数据节合并为单个节。将符号从它们的在.o文件的相对位置重新定位到可执行文件的最终绝对内存位置。更新所有对这些符号的引用来反映它们的新位置。

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

 

图5-1 Ubuntu下链接指令

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

     

 

图5-2 hello.out文件的文件头

使用readelf –h hello查看文件头信息。根据文件头的信息,可以知道该文件是可执行目标文件,有31个节,如图5-2。使用readelf –S hello查看节头表。从而得知各节的大小,以及他们可以进行的操作,如图5-3。使用readelf –s hello可以查看符号表的信息,如图5-4。

 

图5-3(1) hello.out的段头表

 

图5-3(2) hello.out的段头表

 

图5-4 hello.out的符号表

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

根据图5-3可以得到各段的基本信息。由于是可执行目标文件,所以每个段的起始地址都不相同,它们的起始地址分别对应着装载到虚拟内存中的虚拟地址。这样可以直接从文件起始处得到各段的起始位置,以及各段所占空间的大小。同时可以观察到,代码段是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间。根据5.3节的信息,可以找到各节的二进制信息。代码段的信息如下所示。代码段开始于0x400500处。 

 

5-5 使用edb查看hello虚拟地址空间段信息

5.5 链接的重定位过程分析

反汇编命令:objdump -d -r hello

hello与hello.o主要有以下的不同:

1.链接增加新的函数:在hello中链接加入了在hello.c中用到的函数,如exit、printf、sleep、getchar等函数。

2.增加的节:hello中增加了.init和.plt节,和一些节中定义的函数。

3.函数调用:hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。

4.地址访问:hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于.rodata和sleepsecs等全局变量的访问,是$0x0和0(%rip),是因为它们的地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。

根据hello和hello.o的不同,分析出链接的过程为:链接就是链接器ld将各个目标文件组装在一起,就是把.o文件中的各个函数段按照一定规则累积在一起,比如规则:解决符号依赖,库依赖关系,并生成可执行文件。

 

图5-6 hello.out的反汇编结果(主函数部分)

5.6 hello的执行流程

hello调用与跳转的各个子程序名或程序地址如下:

0x400488  _init

0x4004a0   .plt

0x4004b0   puts@plt

0x4004c0   printf@plt

0x4004do   geychar@plt

0x4004e0   exit@plt

0x4004f0   sleep@plt

0x400500   _start

0x400530   _dl_relocate_static_pie

0x400532   main

0x4005c0   _libc_csu_init

0x400630   _libc_csu_fini

0x400634   _fini

5.7 Hello的动态链接分析

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

 

图5-7 do_init前

 

图5-8 do_init后

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

5.8 本章小结

本章主要理解了Ubuntu下链接的过程,链接就是是将各种代码和数据片段收集并组合成一个单一文件的过程。通过查看hello的虚拟地址空间,并且对比hello.o和hello的反汇编代码,更好地掌握了链接尤其是重定位的过程,但是我们知道链接并不止于此,hello会在它运行时要求动态链接器加载和链接某个共享库,而无需在编译时将那些库链接到应用中。

(第51分)


第6章 hello进程管理

6.1 进程的概念与作用

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。

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

1.shell的作用

实际上Shell是一个命令解释器,它解释由用户输入的命令并且把它们送到内核。不仅如此,Shell有自己的编程语言用于对命令的编辑,它允许用户编写由shell命令组成的程序。Shell编程语言具有普通编程语言的很多特点,比如它也有循环结构和分支控制结构等,用这种编程语言编写的Shell程序与其他应用程序具有同样的效果

2.shell的处理流程

shell首先检查命令是否是内部命令,若不是再检查是否是一个应用程序(这里的应用程序可以是Linux本身的实用程序,如ls和rm,也可以是购买的商业程序,如xv,或者是自由软件,如emacs)。然后shell在搜索路径里寻找这些应用程序(搜索路径就是一个能找到可执行程序的目录列表)。如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息。如果能够成功找到命令,该内部命令或应用程序将被分解为系统调用并传给Linux内核。

6.3 Hello的fork进程创建过程

一个进程,包括代码、数据和分配给进程的资源。fork函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。一个进程调用fork函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

6.4 Hello的execve过程

execve函数在加载并运行可执行目标文件Hello,且带列表argv和环境变量列表envp。只有当出现错误时,例如找不到Hello,execve才会返回到调用程序。在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下的原型:

int main(int argc , char **argv , char *envp);

结合虚拟内存和内存映射过程,可以更详细地说明exceve函数实际上是如何加载和执行程序Hello,需要以下几个步骤:

1.删除已存在的用户区域。

2.映射私有区域。为Hello的代码、数据、bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时复制的。

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

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

6.5 Hello的进程执行

新进程的创建,首先在内存中为新进程创建一个task_struct结构,然后将父进程的task_struct内容复制其中,再修改部分数据。分配新的内核堆栈、新的PID、再将task_struct 这个node添加到链表中。然后将可执行文件装入内核的linux_binprm结构体。进程调用execve时,该进程执行的程序完全被替换,新的程序从main函数开始执行。调用execve并不创建新进程,只是替换了当前进程的代码区、数据区、堆和栈。在进程调用了exit之后,该进程并非马上就消失掉,而是留下了一个成为僵尸进程的数据结构,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。

为了控制进程的执行,内核必须有能力挂起正在CPU上执行的进程,并恢复以前挂起的某个进程的执行,这叫做进程切换。进程上下文切换由以下4个步骤组成:

(1)决定是否作上下文切换以及是否允许作上下文切换。包括对进程调度原因的检查分析,以及当前执行进程的资格和CPU执行方式的检查等。在操作系统中,上下文切换程序并不是每时每刻都在检查和分析是否可作上下文切换,它们设置有适当的时机。

(2)保存当前执行进程的上下文。这里所说的当前执行进程,实际上是指调用上下文切换程序之前的执行进程。如果上下文切换不是被那个当前执行进程所调用,且不属于该进程,则所保存的上下文应是先前执行进程的上下文,或称为“老”进程上下文。显然,上下文切换程序不能破坏“老”进程的上下文结构。

(3)使用进程调度算法,选择一处于就绪状态的进程。

(4)恢复或装配所选进程的上下文,将CPU控制权交到所选进程手中。

6.6 hello的异常与信号处理

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

hello执行过程中会出现的异常:

中断:信号SIGTSTP,默认行为是 停止直到下一个SIGCONT

终止:信号SIGINT,默认行为是 终止

下面演示程序运行时各命令情况:

  1. hello运行时什么都不按。程序执行完后,进程被回收。再按回车键,结束。

 

图6-1 正常运行hello程序

  1. 运行过程中按Ctrl+C。父进程收到SIGINT信号,终止hello进程,并且回收hello进程。

 

图6-2 运行时按Ctrl+C

  1. 运行时乱按。如图6-3,发现乱按的输入并不会影响进程的执行,当按到回车键时,getchar会读入回车符,并且后面的字符串会当作shell的命令行输入。

 

图6-3 hello运行时乱按

  1. 按下Ctrl+Z后运行ps命令。按下Ctrl+Z后,父进程收到SIGTSTP信号,将hello进程挂起,ps命令列出当前系统中的进程(包括僵死进程)。

 

图6-4 按下Ctrl+Z后运行ps命令

  1. 按下Ctrl+Z后运行jobs命令。jobs命令列出 当前shell环境中已启动的任务状态。

 

图6-5 按下Ctrl+Z后运行jobs命令

  1. 按下Ctrl+Z后运行pstree命令。pstree命令是以树状图显示进程间的关系。

 

图6-6 按下Ctrl+Z后运行pstree命令部分截图

  1. fg命令将进程调到前台。

 

图6-7 fg命令

  1. kill发送信号给一个进程或多个进程。通过kill -9 24169杀死pid为24169的进程。

 

图6-8 kill命令

6.7本章小结

本章了解了hello进程的执行过程,主要是hello的创建、加载和终止,通过键盘输入,对hello执行过程中产生信号和信号的处理过程有了更多的认识,从而对异常的掌握加深了。

(第61分)


第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:包含在机器语言中用来指定一个操作数或一条指令的地址。每一个逻辑地址都由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。就是hello.o里相对偏移地址。

线性地址:逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。是hello中的虚拟内存地址。

虚拟地址:一个带虚拟内存的系统中,CPU从一个有N=2^n个地址空间中生成虚拟地址。虚拟地址其实就是线性地址。

物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。地址翻译会将hello的一个虚拟地址转化为物理地址。

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

一个逻辑地址由两部分组成,段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。

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

CPU的页式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址。从管理和效率的角度出发,线性地址被分为以固定长度为单位的组,称为页(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个tatol_page[2^20]的大数组,共有2的20个次方个页。这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址一一对应的页的地址。另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。

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

Core i7 MMU 使用四级的页表将虚拟地址翻译成物理地址。36位VPN 被划分成四个9 位VPN,分别用于一个页表的偏移量。具体结构如图7-1所示。

 

 图7-1 二级管理模式图

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

 

图7-2 三级Cache支持下的物理内存访问

首先CPU发出一个虚拟地址,在TLB里面寻找。如果命中,那么将PTE发送给L1Cache,否则先在页表中更新PTE。然后再进行L1根据PTE寻找物理地址,检测是否命中的工作。这样就能完成Cache和TLB的配合工作。具体流程如图7-2所示。

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行新程序hello.out的步骤:删除已存在的用户区域,创建新的区域结构,代码和初始化数据映射到.text和.data区(目标文件提供),.bss和栈映射到匿名文件,设置PC,指向代码区域的入口点。Linux根据需要换入代码和数据页面。

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

DRAM 缓存不命中称为缺页,即虚拟内存中的字不在物理内存中。缺页导致页面出错,产生缺页异常。缺页异常处理程序选择一个牺牲页,然后将目标页加载到物理内存中。最后让导致缺页的指令重新启动,页面命中。

7.9动态存储分配管理

在程序运行时程序员使用动态内存分配器(如malloc)获得虚拟内存。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的。分配器的类型包括显式分配器和隐式分配器。前者要求应用显式地释放任何已分配的块,后者在检测到已分配块不再被程序所使用时,就释放这个块。

动态内存管理的策略包括首次适配、下一次适配和最佳适配。首次适配会从头开始搜索空闲链表,选择第一个合适的空闲块。搜索时间与总块数(包括已分配和空闲块)成线性关系。会在靠近链表起始处留下小空闲块的“碎片”。下一次适配和首次适配相似,只是从链表中上一次查询结束的地方开始。比首次适应更快,避免重复扫描那些无用块。最佳适配会查询链表,选择一个最好的空闲块,满足适配,且剩余最少空闲空间。它可以保证碎片最小,提高内存利用率。

7.10本章小结

本章通过hello的内存管理,复习了与内存管理相关的重要的概念和方法。加深了对动态内存分配的认识和了解。

(第7 2分)


第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

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

B0 , B1 , … , Bk , … , Bm-1

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

8.2 简述Unix IO接口及其函数

Unix IO接口:

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

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

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

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

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

Unix IO函数:

1. open()函数

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

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

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

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

2. close()函数

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

所需头文件: #include <unistd.h>

函数原型:int close(int fd)

参数:fd文件描述符

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

3. read()函数

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

所需头文件: #include <unistd.h>

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

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

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

4. write()函数

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

所需头文件: #include <unistd.h>

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

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

5. lseek()函数

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

所需头文件:#include <unistd.h>,#include <sys/types.h>

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

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

返回值:成功:返回当前位移;失败:返回-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;

}

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

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

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

{

    char* p;

    char tmp[256];

    va_list p_next_arg = args;

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

    if (*fmt != '%') {

    *p++ = *fmt;

    continue;

    }

    fmt++;

    switch (*fmt) {

    case 'x':

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

    strcpy(p, tmp);

    p_next_arg += 4;

    p += strlen(tmp);

    break;

    case 's':

    break;

    default:

    break;

    }

    }

   return (p - buf);

}

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

对write进心追踪:

write:

mov eax, _NR_write

mov ebx, [esp + 4]

mov ecx, [esp + 8]

int INT_VECTOR_SYS_CALL

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

sys_call的实现:

sys_call:

call save

push dword [p_proc_ready]

sti

push ecx

push ebx

call [sys_call_table + eax * 4]

add esp, 4 * 3

mov [esi + EAXREG - P_STACKBASE], eax

cli

ret

于是可以直到printf函数执行过程如下:

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

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

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

8.4 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由宏实现:#define getchar() getc(stdin)。getchar有一个int型的返回值。当程序调用getchar时.程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才等待用户按键。

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

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

8.5本章小结

本章通过介绍hello中包含的函数所对应的unix I/O,大致了解了I/O接口及其工作方式,同时也了解了硬件设备的使用和管理的技术方法。

(第81分)

结论

hello所经历的过程:

1.hello被I/O设备编写,以文件的方式储存在主存中。

2.hello.c被预处理hello.i文件

3.hello.i被编译为hello.s汇编文件

4.hello.s被汇编成可重定位目标文件hello.o

5.链接器将hello.o和外部文件链接成可执行文件hello

6.在shell输入命令后,通过exceve加载并运行hello

7.在一个时间片中,hello有自己的CPU资源,顺序执行逻辑控制流

8.hello的VA通过TLB和页表翻译为PA

9.三级cache 支持下的hello物理地址访问

10.hello在运行过程中会有异常和信号等

11.printf会调用malloc通过动态内存分配器申请堆中的内存

12.shell父进程回收hello子进程,内核删除为hello创建的所有数据结构

深切感悟:计算机系统的设计与实现,处处体现着抽象的含义。比如,程序的本质是01二进制码,也就是机器语言。而汇编语言实现了对机器语言的抽象,高级语言实现了对汇编语言的抽象。再比如,各种物理内存的实现方式各不相同,有磁盘、软盘等。使用虚拟内存的概念实现了对各种物理内存的抽象,而具体实现则交给I/O设备进行处理,使得上层在使用的时候非常方便。概念上的抽象使得对概念的使用变得简单,这就是我对计算机系统设计实现的一个感悟。

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


附件

hello.c :hello源代码

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

hello.s :hello.i编译后的汇编文件

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

hello_objdump :hello的反汇编代码

hello.o_objdump :hello.o的反汇编代码

hello :链接后的可执行文件

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


参考文献

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

[1]逻辑地址到线性地址的转换

https://blog.csdn.net/xuwq2015/article/details/48572421

[2]  LINUX 逻辑地址、线性地址、物理地址和虚拟地址转换

https://www.cnblogs.com/zengkefu/p/5452792.html

[3]  深入了解计算机系统(第三版)2016 Bryant,R.E. 机械工业出版社

[4]  printf 函数实现的深入剖析

https://blog.csdn.net/zhengqijun_/article/details/72454714

[5]  百度百科 getchar计算机语言函数

https://baike.baidu.com/item/getchar/919709?fr=aladdin

[5]  CSDN博客 gcc详解

https://blog.csdn.net/qq_35144795/article/details/77580913

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

猜你喜欢

转载自www.cnblogs.com/7K7K163/p/12085267.html