程序人生-Hello’s P2P

程序人生-Hello’s P2P
专 业 计算机科学与技术
学   号 1180300826
学 生 李天瑞    
指 导 教 师 吴锐

计算机科学与技术学院
2019年12月
摘 要
计算机可以完成成千上万个我们给它的指令,看似很简单,但是隐藏在内部的是一个非常非常复杂的系统,一个简单的命令的顺利执行必须依赖于各部分紧密的配合。
本文通过介绍一个hello.c从一个简单的c源文件一步步执行下去最终变为屏幕上显示的文字的过程,阐述每一步的执行原理,带你看到更深层的执行步骤,会让你对计算机系统的整体运行流程有个初步的认识。
关键词:hello;预处理;编译;链接;内存;进程;管理。

目 录

第1章 概述 - 4 -
1.1 Hello简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 4 -
第2章 预处理 - 5 -
2.1 预处理的概念与作用 - 5 -
2.2在Ubuntu下预处理的命令 - 5 -
2.3 Hello的预处理结果解析 - 5 -
2.4 本章小结 - 5 -
第3章 编译 - 6 -
3.1 编译的概念与作用 - 6 -
3.2 在Ubuntu下编译的命令 - 6 -
3.3 Hello的编译结果解析 - 6 -
3.4 本章小结 - 6 -
第4章 汇编 - 7 -
4.1 汇编的概念与作用 - 7 -
4.2 在Ubuntu下汇编的命令 - 7 -
4.3 可重定位目标elf格式 - 7 -
4.4 Hello.o的结果解析 - 7 -
4.5 本章小结 - 7 -
第5章 链接 - 8 -
5.1 链接的概念与作用 - 8 -
5.2 在Ubuntu下链接的命令 - 8 -
5.3 可执行目标文件hello的格式 - 8 -
5.4 hello的虚拟地址空间 - 8 -
5.5 链接的重定位过程分析 - 8 -
5.6 hello的执行流程 - 8 -
5.7 Hello的动态链接分析 - 8 -
5.8 本章小结 - 9 -
第6章 hello进程管理 - 10 -
6.1 进程的概念与作用 - 10 -
6.2 简述壳Shell-bash的作用与处理流程 - 10 -
6.3 Hello的fork进程创建过程 - 10 -
6.4 Hello的execve过程 - 10 -
6.5 Hello的进程执行 - 10 -
6.6 hello的异常与信号处理 - 10 -
6.7本章小结 - 10 -
第7章 hello的存储管理 - 11 -
7.1 hello的存储器地址空间 - 11 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -
7.5 三级Cache支持下的物理内存访问 - 11 -
7.6 hello进程fork时的内存映射 - 11 -
7.7 hello进程execve时的内存映射 - 11 -
7.8 缺页故障与缺页中断处理 - 11 -
7.9动态存储分配管理 - 11 -
7.10本章小结 - 12 -
第8章 hello的IO管理 - 13 -
8.1 Linux的IO设备管理方法 - 13 -
8.2 简述Unix IO接口及其函数 - 13 -
8.3 printf的实现分析 - 13 -
8.4 getchar的实现分析 - 13 -
8.5本章小结 - 13 -
结论 - 14 -
附件 - 15 -
参考文献 - 16 -

第1章 概述
1.1 Hello简介
P2P: From Program to Process。linux中,hello.c经过cpp的预处理、ccl的编译、as的汇编、ld的链接最终成为可执行目标程序hello,在shell中键入启动命令后,shell为其fork产生子进程的过程。
020: From Zero-0 to Zero-0。shell通过execve加载并执行hello,映射虚拟内存,进入程序入口后,程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:Inter(R) Core(TM) i7-7700HQ CPU;2.80GHz;256G HD DISK以上
软件环境:Windows10 64位;VMware® Workstation 14 Pro;Ubuntu 16.04 LTS 64位
开发工具和调试工具:CodeBlocks17.12 gcc + gedit gdb edb
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c :hello源代码
hello.i :预处理后的文本文件
hello.s :hello.i编译后的汇编文件
hello.o :hello.s汇编后的可重定位目标文件
hello :链接后的可执行文件
hello.elf hello.o的ELF格式
hello1.elf hello的ELF格式
1.4 本章小结
在本章节当中,我简单介绍了Hello程序的P2P以及O2O过程,说明了写计算机系统大作业时的环境和工具,并且列出了完成大作业的过程中生成的Hello的中间文件及其作用。

第2章 预处理
2.1 预处理的概念与作用
预处理的概念:预处理又叫做预编译,是指在对C源代码文件进行词法扫描和语法分析之前所做的工作。
预处理的作用:

  1. 删除#define,展开所有宏定义;
  2. 处理条件预编译 #if, #ifdef, #if, #elif,#endif;
  3. 处理“#include”预编译指令,将包含的“.h”文件插入对应位置。这可是递归进行的,文件内可能包含其他“.h”文件;
  4. 删除所有注释。/**/,//;
  5. 添加行号和文件标识符。用于显示调试信息:错误或警告的位置。
    2.2在Ubuntu下预处理的命令
    预处理命令:gcc -E hello.c -o hello.i
    如下图所示:
    在这里插入图片描述
    图2-1 hello.c预处理产生hello.i
    2.3 Hello的预处理结果解析
    观察hello.i文件,发现文件有几千行,远远大于源代码的行数。使用gedit编辑器打开hello.i文件。发现预处理器(cpp)读取头文件stdio.h 、stdlib.h 、和unistd.h中的内容,并将三个系统头文件依次展开,这样产生的预处理文件就有了能够独立运行的代码;在hello.i文件中也未看到注释,表明在预处理过程中注释被删除掉;并且处理条件预编译 #if, #ifdef。
    在这里插入图片描述
    图2-2 使用gedit编辑器打开hello.i文件
    在这里插入图片描述
    图2-3 hello.c源文件打开
    2.4 本章小结
    本章介绍了hello.c的预处理阶段,预处理过程是计算机对程序进行操作的起始过程,在这个过程中预处理器会对hello.c文件进行初步的解释,对头文件、宏定义和注释进行操作,将程序中涉及到的库中的代码补充到程序中,将注释这个对于执行没有用的部分删除,最后将初步处理完成的文本保存在hello.i中,方便以后的内核器件直接使用。

第3章 编译
3.1 编译的概念与作用
编译的概念:编译就是将用c语言写成的源代码文件,在不改变其实现的功能的前提下翻译成汇编语言文件的过程。
编译的作用:
1.扫描(词法分析):将源代码程序输入扫描器,将源代码的字符序列分割成一系列记号;
2.语法分析:基于词法分析得到的一系列记号,生成语法树;
3.语义分析:由语义分析器完成,指示判断是否合法,并不判断对错;
4.源代码优化(中间语言生成):中间代码(语言)使得编译器分为前端和后端,前端产生与机器(或环境)无关的中间代码,编译器的后端将中间代码转换为目标机器代码;
5.代码生成:目标代码优化。
编译器后端主要包括:1)代码生成器:依赖于目标机器,依赖目标机器的不同字长,寄存器,数据类型等;2)目标代码优化器:选择合适的寻址方式,左移右移代替乘除,删除多余指令。
3.2 在Ubuntu下编译的命令
编译命令:gcc -S hello.i -o hello.s
在这里插入图片描述
图3-1 hello.i编译生成hello.s文件
3.3 Hello的编译结果解析
3.3.1数据的处理
观察hello.c源文件代码中出现的数据有:
①全局变量:int sleepsecs = 2.5;
②常量:2.5,字符串等
③局部变量:int i;
④外部参数:main函数的传参:int argc,char *argv[];
以下进行逐个分析
1) 全局变量的处理
在这里插入图片描述
图3-2 全局变量的处理
分析:源代码中的sleepsecs整型变量是全局变量。全局变量的特点是在C程序的任意函数中都能够直接读写,在汇编代码中,全局变量会被存放在函数体外的data段。
2) 常量的处理
在这里插入图片描述
图3-3 常量的处理
分析:在汇编程序中,常量一般存放在专门的区域,需要的时候直接调用。为了链接的方便,一般会采取全局偏移量表(GOT)的形式来调用全局变量。源代码中的常量主要是两个用于在printf中输出的字符串,这两个字符串直接存放在汇编程序中的只读数据域。
3) 局部变量与外部参数的处理
在这里插入图片描述
图3-4 局部变量与外部参数的处理
分析:源代码中的用于计数的整型变量i、main函数的参数argc和argv,这类数据一般是在程序运行的栈中保存,寄存器中进行传递。同时在栈与寄存器中都可以对其进行修改。
3.3.2赋值语句的处理
局部变量i是如何进行赋值的。可以注意到hello.s的36行中有一个对寄存器的寻址操作,这个操作将0放入到了这个地址中。由此可以确定这个就是对变量i的初始化。
.L2:
movl $0, -4(%rbp)
jmp .L3
3.3.3算术操作
源代码中算数操作:for(i = 0;i < 10;i++)
因为局部变量i存放于栈中:movl $0, -4(%rbp),所以直接使用指令addl进行累加:addl $1, -4(%rbp),把相应的栈中的数值每次加1。
3.3.4关系操作
源代码中关系操作有两个地方:

  1. argc!=3;
  2. i<10
    汇编代码中相应代码
  3. cmpl $3, -20(%rbp)
    je .L2
  4. cmpl $9, -4(%rbp)
    jle .L4
    分析:cmpl是一个比较函数,这个函数中将比较的结果保存在条件码中。当满足相应条件时,就会执行条件跳转指令jxx,关于控制转移将在第6节进行阐释。
    3.3.5数组/指针/结构操作
    源文件中指针操作为:
    在这里插入图片描述
    图3-5 源文件指针操作
    hello.c中在输出的时候调用了argv数组中的元素。所以对于一个数组的保存,在汇编中我们只保存了起始地址,对应的也就是argv[0]的地址,对于数组的中其他元素,我们利用了数组在申请的过程中肯定是一段连续的地址这样的性质,直接用起始地址加上偏移量。
    汇编代码中相应代码
    在这里插入图片描述
    图3-6 汇编文件指针操作
    3.3.6控制转移
    源代码中控制转移操作有两个地方:
  5. argc!=3;
    分析:当argc不等于3时进行跳转。cmpl语句比较 -20(%rbp)和-3,设置条件码,判断ZF零标志,如果最近的操作得出的结果为0,则跳到.L2中,否则顺序执行下一条语句。
    cmpl $3, -20(%rbp)
    je .L2
  6. i<10;
    分析:for循环的控制时比较cmpl $9, -4(%rbp) ,当i大于9时跳出循环,否则进入.L4循环体内部执行。
    cmpl $9, -4(%rbp)
    jle .L4
    3.3.7函数操作
    1.main函数:
    参数传递:传入参数argc和argv,分别用寄存器%rdi和%rsi存储;
    函数调用:被系统启动函数调用;
    函数返回:设置%eax为0并且返回,对应return 0 。
    2.printf函数:
    参数传递:call puts时只传入了字符串参数首地址;for循环中call printf时传入了 argv[1]和argc[2]的地址;
    函数调用:for循环中被调用
    在这里插入图片描述
    图3-7 汇编文件printf函数调用
    3.exit函数:
    参数传递:传入的参数为1,再执行退出命令;
    函数调用:if判断条件满足后被调用
    在这里插入图片描述
    图3-8 汇编文件exit函数调用
    4.sleep函数:
    参数传递:传入参数sleepsecs,传递控制call sleep;
    函数调用:for循环下被调用
    在这里插入图片描述
    图3-9 汇编文件sleep函数调用
    5.getchar
    传递控制:call getchar
    函数调用:在main中被调用
    在这里插入图片描述
    图3-10 汇编文件getchar函数调用
    3.4 本章小结
    本章我们主要介绍了编译器是如何将文本编译成汇编代码的。可以发现,编译器并不是死板的按照我们原来文本的顺序,逐条语句进行翻译下来的。编译器在编译的过程中,会对我们的代码做一些隐式的优化,而且会将原来代码中用到的跳转,循环等操作操作用控制转移等方法进行解析。最后生成我们需要的hello.s文件。

第4章 汇编
4.1 汇编的概念与作用
汇编的概念:把汇编语言翻译成机器语言的过程称为汇编。
汇编的作用:将我们之前再hello.s中保存的汇编代码翻译成可以供机器执行的二进制代码,这样机器就可以根据这些二进制代码,开始执行程序。
4.2 在Ubuntu下汇编的命令
汇编指令:gcc -c hello.s -o hello.o
在这里插入图片描述
图4-1 hello.s汇编生成hello.o文件
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
ELF格式中都存储的文件
在这里插入图片描述
图4-2 典型的ELF可重定位目标文件
接下来逐个分析

  1. ELF头描述了生成该文件的系统的字的大小和字节顺序,并且包含帮助链接器语法分析和解释目标文件的信息。
    在这里插入图片描述
    图4-3 ELF头
  2. 节头部表描述了不同节的位置和大小,其中目标文件中每个节都有一个固定大小的条目。具体的描述包括节的名称、类型、地址和偏移量等。
    在这里插入图片描述
    图4-4 节头部表
  3. 可重定位节:rela.text中保存了代码的重定位信息,也就是.text节中的信息的重定位信息。可以看到这里面有.rodata,puts等很多函数的重定位信息。
    在这里插入图片描述
    图4-5 重定位节
  4. .symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
    在这里插入图片描述
    图4-6 符号表.symtab
    4.4 Hello.o的结果解析
    反汇编命令:objdump -d -r hello.o
    hello.s文件:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    图4-7 hello.s文件代码
    反汇编代码:
    在这里插入图片描述
    在这里插入图片描述
    图4-8 反汇编代码
    对比hello.s文件和反汇编代码,主要有以下的差别:
  5. 操作数:hello.s中的操作数时十进制,hello.o反汇编代码中的操作数是十六进制。
  6. 分支转移:跳转语句之后,hello.s中是.L2和.L3等段名称,而反汇编代码中跳转指令之后是相对偏移的地址。
  7. 函数调用:hello.s中,call指令之后直接是函数名称,而反汇编代码中call指令之后是函数的相对偏移地址。因为函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
  8. 全局变量的访问:在hello.s文件中,对于.rodata和sleepsecs等全局变量的访问,是$.LC0和sleepsecs(%rip),而在反汇编代码中是$0x0和0(%rip),是因为它们的地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。
    4.5 本章小结
    本章阐述了hello.s汇编语言转换成hello.o机器语言的过程,重定位查看hello.o的内容,分析了其作用,然后又对比了hello.s与hello.o之间映射与差别,更深刻地理解了汇编语言到机器语言实现地转变。

第5章 链接
5.1 链接的概念与作用
链接的概念:将各种代码和数据片段收集并组合成一个单一文件的过程。
链接的作用:地址和空间的分配,符号决议和重定位。
1.符号决议:也可以说地址绑定,分动态链接和静态链接;
2.重定位:假设此时又两个文件:A,B。A需要B中的某个函数mov的地址,未链接前将地址置为0,当A与B链接后修改目标地址,完成重定位。
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 使用ld链接生成可执行目标文件hello
5.3 可执行目标文件hello的格式
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
图5-2 ELF各节信息

根据图5-2我们可以看到hello文件中的节的数目比hello.o中增多。
ELF Header描述了整个ELF表的信息,包括Magic,Class,Data等。
Section Header描述了各节的信息,下面列出每一节中各个信息条目的含义:

  1. Name Size中存储了每一个节的名称和这个节在重定位文件中所占的大小;
  2. Address保存了各个节在重定位文件中的具体位置也就是地址。
  3. Offset保存的是这个节在程序里面的地址的偏移量,也就是相对地址。
    5.4 hello的虚拟地址空间
    使用edb加载hello,在Data Dump中查看虚拟地址的空间。
    在这里插入图片描述
    图5-3 使用edb查看hello虚拟地址空间段信息
    在这里插入图片描述
    图5-4 查看Program Header
    Program Header中信息分别为每个节对应的偏移地址,虚拟地址位置,物理地址位置,文件大小,存储大小,标志和对齐方式。
    5.5 链接的重定位过程分析
    在这里插入图片描述
    在这里插入图片描述
    图5-5 hello.o与hello对比
    由图5-5可知hello.o与hello不同:
    1.hello.o中一开始就是.text,而在hello中一开始是.init,而后才是.text;
    2.hello.o中函数为虚拟地址,而在hello中确是具体地址;
    3.hello.o中跳转以及函数调用的地址在hello中都被更换成了虚拟内存地址。
    链接过程:
    1.函数个数:在使用ld命令链接的时候,指定了动态链接器为64的/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o中主要定义了程序入口_start、初始化函数_init,_start程序调用hello.c中的main函数,libc.so是动态链接共享库,其中定义了hello.c中用到的printf、sleep、getchar、exit函数和_start中调用的__libc_csu_init,__libc_csu_fini,__libc_start_main。链接器将上述函数加入。
    2.函数调用:链接器解析重定条目时发现对外部函数调用的类型为R_X86_64_PLT32的重定位,此时动态链接库中的函数已经加入到了PLT中,.text与.plt节相对距离已经确定,链接器计算相对距离,将对动态链接库中函数的调用值改为PLT中相应函数与下条指令的相对地址,指向对应函数。对于此类重定位链接器为其构造.plt与.got.plt。
    3…rodata引用:链接器解析重定条目时发现两个类型为R_X86_64_PC32的对.rodata的重定位(printf中的两个字符串),.rodata与.text节之间的相对距离确定,因此链接器直接修改call之后的值为目标地址与下一条指令的地址之差,指向相应的字符串。
    重定位过程:
    1.链接器在完成符号解析以后,就把代码中的每个符号引用和正好一个符号定义关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。
    2.将合并输入模块,并为每个符号分配运行时的地址。在hello到hello.o中,首先是重定位节和符号定义,链接器将所有输入到hello中相同类型的节合并为同一类型的新的聚合节。
    3.链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每一个符号。
    4.符号引用,链接器会修改hello中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址
    5.6 hello的执行流程
    使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
    0x400430 init;
    0x400460 puts@plt;
    0x400470 printf@plt;
    0x400480 __libc_start_main@plt;
    0x400490 getchar@plt;
    0x4004a0 exit@plt;
    0x4004b0 sleep@plt;
    0x4004d0 _start;
    0x4004fe main;
    0x400580 __libc_csu_init;
    0x4005f0 __libc_csu_fini;
    0x4005f4 _fini;
    5.7 Hello的动态链接分析
    在这里插入图片描述
    图5-6 do_init前文件
    在这里插入图片描述
    图5-7 do_init后文件
    对比以上两图可知,在do_init前后global_offset表发生变化:0x006008c0开始的global_offset表是全0的状态,在执行过_dl_init之后被赋上了相应的偏移量的值。由此可知,dl_init操作是给程序赋上当前执行的内存地址偏移量 。
    5.8 本章小结
    在本章中主要介绍了链接的概念与作用、hello的ELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。熟练掌握edb调试的操作。

第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:当hello程序在计算机中开始执行时,操作系统给了它一种假象,仿佛它是当前系统中唯一正在运行的程序一样,它独自占有一块完整的内存空间,处理器仿佛一直在执行hello这一个程序的指令。这种状态就成为进程。
进程的作用:进程就是一个执行中的程序的实例,系统中每一个程序都运行在某个进程的上下文中,系统始终维护着这个上下文,使进程与上下文之间的互动天衣无缝。在操作系统的辛苦维持下,才给予了程序独自占用所有计算资源的假象。
进程提供给应用程序的关键抽象如下:一个独立的逻辑控制流。一个私有的地址空间。
6.2 简述壳Shell-bash的作用与处理流程
shell是一个交互型的应用级程序,它代表用户运行其他程序。
shell首先打印一个命令行提示符,等待用户输入命令行,然后对命令行进行求值。shell的基本流程是读取命令行,解析命令行,然后代表用户运行程序。
shell调用parseline函数,通过这个函数解析以空格分隔的命令行参数,并构造最终会传递给execve的argv向量。
若第一个参数是内置的shell命令名,马上就会解释这个命令。如果不是,shell就会假定这是一个可执行程序,然后在一个新的子进程的上下文中加载并运行这个文件。若最后一个参数是&,那么这个程序将会在后台执行,即shell不会等待其完成。若没有,则这是一个将要在前台执行的程序,shell会显式地等待这个程序执行完成。
当作业终止时,shell就会开始下一轮迭代。
6.3 Hello的fork进程创建过程
Hello的执行是通过在终端中输入./Hello来完成的。接下来shell会执行fork函数。fork函数的作用是创建一个与当前进程平行运行的子进程。系统会将父进程的上下文,包括代码,数据段,堆,共享库以及用户栈,甚至于父进程打开的文件的描述符,都创建一份副本。然后利用这个副本执行子进程。从这个角度上来说,子进程的程序内容与父进程是完全相同的
6.4 Hello的execve过程
execve函数的作用是在当前进程的上下文中加载并运行一个新的程序。与fork函数不同的是,fork函数创建了一个新的进程来运行另一个程序,而execve直接在当前的进程中删除当前进程中现有的虚拟内存段,并穿件一组新的代码、数据、堆和用户栈的段。将栈和堆初始化为0,代码段与数据段初始化为可执行文件中的内容,最后将PC指向_start的地址。在CPU开始引用被映射的虚拟页的时候,内核才会将需要用到的数据从磁盘中放入内存中。
在这里插入图片描述
图6-1 堆栈信息
6.5 Hello的进程执行
1.进程上下文信息:就是内核重新启动一个被抢占的程序所需的状态,它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈、和各种内核数据结构。
2.进程时间片:是指一个进程和执行它的控制流的一部分的每一时间段。
3.用户模式和内核模式:处理器为了安全起见,不至于损坏操作系统,必须限制一个应用程序可执行指令能访问的地址空间范围。就发明了两种模式用户模式和内核模式,其中内核模式有最高的访问权限,甚至可以停止处理器、改变模式位,或者发起一个I/O操作,处理器使用一个寄存器当作模式位,描述当前进程的特权。进程只有当中断、故障或者陷入系统调用时,才会得到内核访问权限,其他情况下都始终在用户权限中,就能够保证系统的绝对安全
4.用户态与核心态转换:Hello进程在内存中执行的过程中,并不是一直占用着cpu的资源。因为当内核代表用户执行系统调用时,可能会发生上下文切换,比如说Hello中的sleep语句执行时,或者当Hello进程以及运行足够久了的时候。每到这时,内核中的调度器就会执行上下文切换,将当前的上下文信息保存到内核中,恢复某个先前被抢占的进程的上下文,然后将控制传递给这个新恢复的进程。
6.6 hello的异常与信号处理
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
hello执行过程中会出现的异常:
中断:信号SIGTSTP,默认行为是 停止直到下一个SIGCONT;
终止:信号SIGINT,默认行为是 终止。
下面演示程序运行时各命令情况:

  1. hello运行时什么都不按。程序执行完后,进程被回收。再按回车键,结束。
    在这里插入图片描述
    图6-2 正常运行hello程序

  2. 运行过程中按Ctrl+C。父进程收到SIGINT信号,终止hello进程,并且回收hello进程。
    在这里插入图片描述
    图6-3 运行时按Ctrl+C

  3. 运行时乱按。如下图,发现乱按的输入并不会影响进程的执行,当按到回车键时,getchar会读入回车符,并且后面的字符串会当作shell的命令行输入。
    在这里插入图片描述
    图6-4 hello运行时乱按

  4. 按下Ctrl+Z后运行ps命令。按下Ctrl+Z后,父进程收到SIGTSTP信号,将hello进程挂起,ps命令列出当前系统中的进程(包括僵死进程)。
    在这里插入图片描述
    图6-5 按下Ctrl+Z后运行ps命令

  5. 按下Ctrl+Z后运行jobs命令。jobs命令列出 当前shell环境中已启动的任务状态。
    在这里插入图片描述
    图6-6 按下Ctrl+Z后运行jobs命令

  6. 按下Ctrl+Z后运行pstree命令。pstree命令是以树状图显示进程间的关系。
    在这里插入图片描述
    图6-7 按下Ctrl+Z后运行pstree命令部分截图

  7. fg命令将进程调到前台。
    在这里插入图片描述
    图6-8 fg命令

  8. kill发送信号给一个进程或多个进程。通过kill -9 4514杀死pid为4514的进程。
    在这里插入图片描述
    图6-9 kill命令
    6.7本章小结
    本章了解了hello进程的执行过程,主要是hello的创建、加载和终止,通过键盘输入,对hello执行过程中产生信号和信号的处理过程有了更多的认识。

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:包含在机器语言中用来指定一个操作数或一条指令的地址。每一个逻辑地址都由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。
线性地址:Linux中逻辑地址等于线性地址。因为Linux所有的段(用户代码段、用户数据段、内核代码段、内核数据段)的线性地址都是从 0x00000000 开始,长度4G,这样 线性地址=逻辑地址+0x00000000,也就是说逻辑地址等于线性地址了。
虚拟地址:虚拟地址将贮存看成是一个存储在磁盘上的地址空间的高速缓存,再主存中只保存活动区域,并根据需要再磁盘和主存之间来回传送数据,通过这种方式,它高效的使用了主存。同时,它为每个进程提供了一致的地址空间,从而简化了内存管理。最后,它保护了每个进程的地址空间不被其他进程破坏。
物理地址:则是对应于主存的真实地址,是能够用来直接在主存上进行寻址的地址。几乎无法直接用物理地址直接访问。
结合hello来说,hello.o中应该为逻辑地址,hello.s应当为线性地址或者虚拟地址,hello中的地址应为物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在多段模式下,每个程序都有自己的局部段描述符表,而每个段都有独立的地址空间在80386 的段机制中,逻辑地址由两部分组成,即段部分(选择符)及偏移部分。段是形成逻辑地址到线性地址转换的基础。如果我们把段看成一个对象的话,那么对它的描述如下:
(1)段的基地址(Base Address):在线性地址空间中段的起始地址。
(2)段的界限(Limit):表示在逻辑地址中,段内可以使用的最大偏移量。
(3)段的属性(Attribute):表示段的特性。例如,该段是否可被读出或写入,或者该段是否作为一个程序来执行,以及段的特权级等。
在这里插入图片描述
图7-1 段描述符
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每字节都有一个唯一的虚拟地址作为到数组的索引。磁盘上数组的内容被缓存在主存中。和存储器层次结构中其他缓存一样,磁盘(较低层)上的数据被分割成块,这些块作为自盘和主存(较高层)之间的传输单元。VM系统通过将虚拟内存分割为成为虚拟页的大小固定的块来处理这个问题,对这些虚拟页的管理与调度就是页式管理。同任何缓存一样,虚拟内存系统必须有某种方法来判定一个虚拟页是否缓存在DRAM中的某个地方。如果是,系统还必须确定这个虚拟页存放在哪个物理页中。如果不命中,系统必须判断这个虚拟页存放在磁盘的那个位置,在物理内存中选择一个牺牲页,并将虚拟页从磁盘复制到DRAM中,替换这个牺牲页。
在这里插入图片描述
图7-2 虚拟内存的管理
7.4 TLB与四级页表支持下的VA到PA的变换
页表的地址映射规则:
在这里插入图片描述
图7-3 页表的地址映射规则
在这个过程中,处理器硬件将会执行以下步骤:
1.处理器生成一个虚拟地址,并把它传送给MMU;
2.MMU生成PTE地址,并从告诉缓存/主存请求得到它;
3.高速缓存/主存向MMU返回PTE;
4.MMU构造物理地址,并把它传送给高速缓存/主存;
5.高速缓存/主存发挥所请求的数据字给处理器。
7.5 三级Cache支持下的物理内存访问
不同存储技术的访问时间差异很大,速度较快的计数每字节的成本要比速度较慢的计数高,而且容量较小。计算的另一个特点就是局部性,即计算机程序倾向于访问最近访问过的某一块程序。存储器的这些基本属性相互补充使得计算机可以通过采用构建存储器层次结构来提升运行效率。
三级Cache的核心思想就是每次访问数据的时候都将一个数据块存放到更高一层的存储器中,根据计算的局部性,程序在后面的运行之中有很大的概率再次访问这些数据,高速缓存器就能够提高读取数据的速度。
7.6 hello进程fork时的内存映射
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
主要分为以下几个步骤:
1.删除已存在的用户区域:删除当前进程虚拟地址的用户部分中已存在的区域结构;
2.映射私有区域:为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的,写时复制的。代码和数据区域被映射为hello文件中的.test和.data区。bss区域是请求二进制0的,映射到匿名文件,其大小包含在a.out当中。栈和堆区域也是请求二进制零的,初始长度为零;
3.映射共享区域;
4.设置程序计数器:execve做的最后一件事情就是设置当前进程上下文中的程序计数器,指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页:当CPU想要读取虚拟内存中的某个数据,而这一片数据恰好存放在主存当中时,就称为页命中。相对的,如果DRAM缓存不命中,则称之为缺页。
缺页故障与缺页中断的处理:缺页异常会调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,然后用磁盘中将要读取的页来替代牺牲页。处理程序解决了这个故障,将控制流转移会原先触发缺页故障的指令,当CPU再次执行这条指令时,对应的页已经缓存到主存当中了。
在这里插入图片描述
图7-4 缺页故障与缺页中断的处理
7.9动态存储分配管理
动态内存分配器原理:动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为 一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。例如C程序中的malloc和free。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么 就释放这个块, 自动释放未使用的已经分配的块的过程叫做垃圾收集。 例如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的内存。
带边界标签的隐式空闲链表分配器原理:
通过头部中的大小字段隐含的连接。分配器可以通过遍历堆中的所有块,从而间接地遍历整个空闲块地集合。
在这里插入图片描述
图7-5 带边界标签的隐式空闲链表分配器原理
显式空间链表的基本原理:
将空闲块组织成链表形式的数据结构。因为根据定义,程序需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面,例如,堆可以组织成一个双向空闲链表,在每 个空闲块中,都包含一个 pred(前驱)和 succ(后继)指针,如下图:
在这里插入图片描述
图7-6 显式空间链表的基本原理
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时 间减少到了空闲块数量的线性时间。
维护链表的顺序有:

  1. 后进先出(LIFO),将新释放的块放置在链表的开始处, 使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在 这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。
  2. 按照地址顺序来维护链表,其中链表中的每个块 的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索 来定位合适的前驱。平衡点在于,按照地址排序首次适配比LIFO排序的首次适配 有着更高的内存利用率,接近最佳适配的利用率。
    7.10本章小结
    本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,在指定环境下介绍了VA到PA的变换、物理内存访问,还介绍了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m个字节的序列,所有的I/O设备(例如网络,磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux内核引出一个简单的、低级的应用皆可,称为UnixI/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行:
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
1.打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可用来代替显式的描述符值。
3.改变当前的文件位置:对于每个打开的文件内核保持着一个文件位置k,初始为0.这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k
4.读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会出发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处没有明确的“EOF符号”。(类似的,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。)
5.关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
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函数的实现代码:
    在这里插入图片描述
    图8-1 printf函数的实现代码
    接下来可以发现,调用了两个外部函数,一个是vsprintf,还有一个是write。
    vsprintf函数,其函数体如下:
    在这里插入图片描述
    图8-2 vsprintf函数的函数体
    write是一个系统函数,其作用就是从内存buf位置复制最多i个字节到一个文件位置。
    综上所述,可以知道printf函数执行过程如下:
    1.从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.;
    2.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息);
    3.显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
    8.4 getchar的实现分析
    getchar 由宏实现:#define getchar() getc(stdin)。
    异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
    getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
    getchar函数的功能是从键盘上输入一个字符。
    其一般形式为: getchar();
    通常把输入的字符赋予一个字符变量,构成赋值语句。
    进入getchar之后,进程会进入阻塞状态,等待外界的输入。系统开始检测键盘的输入。此时如果按下一个键,就会产生一个异步中断,这个中断会使系统回到当前的getchar进程,然后根据按下的按键,转化成对应的ascii码,保存到系统的键盘缓冲区。接下来,getchar调用了read函数。read函数会产生一个陷阱,通过系统调用,将键盘缓冲区中存储的刚刚按下的按键信息读到回车符,然后返回整个字符串。
    最后,getchar会对这个字符串进行处理,只取其中第一个字符,将其余输入简单的丢弃,然后将字符作为返回值。
    8.5本章小结
    本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf函数和getchar函数。

结论
让我们回顾一下C文件是怎样一步一步的变成可以输出我们想看到的结果的程序:
1、首先通过各种各样的文本编辑器,将使用高级语言编写的程序存储到了hello.c文件中;
2、预处理器将hello.c文件经过初步的修改变成了hello.i文件;
3、编译器将hello.i文件处理成为了汇编代码并保存在了hello.s文件中;
4、汇编器将hello.s文件处理成了可重定位的目标程序,也就是hello.o文件;
5、最后,链接器将我们的hello.o与外部文件进行链接,得到可执行程序;
6、在shell中输入运行hello文件的命令,内核会分配好运行程序所需要的堆、用户栈、虚拟内存等一系列信息;
7、在从外部对hello程序进行操控时,只需要在键盘上给一个相应的信号,就会按照相应指令来执行;
8、当hello执行完所有工作之后,最后被shell回收掉了。
通过这次大作业,我对计算机系统的各项内容又有了更加深刻的认识,对书中的知识有了更加全面的认识。在实验的过程中,也感受到了计算机系统的复杂性以及严密性。

附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c 源C程序代码
hello.i 预处理后的文本文件
hello.s 编译后的文本文件
hello.o 汇编之后的可重定位目标执行文件
hello 链接之后的可执行的目标文件
hello.elf hello.o的ELF格式
hello1.elf hello的ELF格式

参考文献
为完成本次大作业你翻阅的书籍与网站等
[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.
[7] fork之后父子进程的内存关系
https://blog.csdn.net/shreck66/article/details/47039937
[8] shell进程
https://blog.csdn.net/guiliguiwang/article/details/80605456
[9] Memory Translation and Segmentation.内存地址转换与分段
https://www.cnblogs.com/tcicy/p/10185353.html
[10] Linux内核中的printf实现
https://blog.csdn.net/u012158332/article/details/78675427
[11] Bryant,R.E. 深入理解计算机系统教材

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值