文章目录

    • 第1章 概述
    • 第2章 预处理
    • 第3章 编译
    • 第4章 汇编
    • 第5章 链接
    • 第6章 hello进程管理
    • 第7章 hello的存储管理
    • 第8章 hello的IO管理
    • 参考文献

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算学部
学   号
班   级
学 生  
指 导 教 师

计算机科学与技术学院
2021年5月
摘 要
本文简单介绍了Hello World 程序的一生,由源代码作为起点,经历了预处理,编译,汇编,链接,进程管理,存储管理,I/O输入输出等环节,是对计算机系统课程的全面总结,以此来梳理知识框架,从程序员的角度更加完整,清晰,深入的理解计算机的执行过程。
关键词:计算机系统;程序员视角;Hello World;预处理;编译;汇编;链接;进程;存储;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(From Program to Process):

当程序员一行一行的开始书写程序,Hello World 的一生由此开始,当带包书写完成后被保存在Hello.c文件中,之后经过预处理,编译,汇编,链接的到一个可执行目标文件,到此program执行完成,进入process阶段,在shell中输入命令“./hello”,将文件读入内存,shell为hello进程fork子进程,由此便实现了From Program to Process。

O2O (From Zero-0 to Zero -0):
在shell为hello进行fork子进程后,shell进行execve,在当前进程的上下文中加载并运行hello程序,覆盖当前的程序地址空间,为hello代码,数据,bss和栈区域2创建新的区域结构,之后映射共享区域,设置程序计数器,映射虚拟内存,加载物理内存,之后进入主函数执行目标代码,cpu为运行的hello分配时间片执行逻辑控制流,当hello运行结束后,shell父进程负责回收hello进程,内核删除相关数据。

1.2 环境与工具
硬件环境:X64 CPU ; 16GRA; 512G DISK;
软件环境:Visual Studio 2022; GDB/OBJDUMP;EDB;CodeBolcks等
开发工具:Windows10;Vmware Pro;Ubuntu18.04;gedit;gcc;edb;readelf等

1.3 中间结果
hello.c:源代码
hello.i:预处理后的文本文件
hello.s:编译之后的汇编文件
hello.o:汇编之后的可重定位目标执行文件
hello:链接之后的可执行文件
hello.elf:hello.o的ELF格式
Hello1.elf:hello的ELF格式
Hello1.txt:hello.o反汇编代码
Hello2.txt:hello的反汇编代码
1.4 本章小结
本章简述了Hello的From Program To Process 和 From Zero-0 to Zero -0 过程,概述了Hello的一生中在不同阶段所处的不同形态(中间结果),之后的每一章有此章详细展开。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用
预处理:指的是进行编译的第一遍扫描之前所做的工作,由预处理器CPP执行,预处理器根据以字符#开头的命令,修改原始的c程序。比如hello.c的第六行#include 命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入到程序文本中。结果是得到另一个C程序,通常是以,i作为扩展名。
C语言提供了多种预处理功能,如:宏定义,文件包含,条件编译等,作用有以下几种:
(1)把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件,如:把#include 命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入到程序文本中。
(2)使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。
(3)条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。
(4)删除所有注释//和/* */
(5)添加行号和文件名标识,以便编译时编译器产生调试用的行号信息
(6)保留所有的#pragma编译指令

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

没有预处理的源程序:

预处理后的源程序:

可以看到,在hello.c中#开头的三行是预处理指令,包含头文件的操作,需要把这些文件插入到hello.c中并生成了hello.i,在hello.i中,代码的函数显然增加;

2.4 本章小结

主要介绍了预处理的概念和作用,通过hello.c与预处理后的hello.i对比,分析与处理结果,了解预处理的功能。
(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译阶段:编译器ccl将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编汇编语言程序,该程序包含main函数的定义,如下图:

汇编语言为不同高级语言的不同编译器提供了通用的输出语言。
编译的过程大概可以分为三个阶段,这也正是当前主流的编译器架构,即:编译前端(frontEnd)、中间代码优化(optimizer)、编译后端(backEnd)。
编译前端将源代码转化成中间代码。其详细过程包括:预处理、词法分析、语法分析、生成中间代码;
中间代码优化则是对编译器生成的中间代码进行一些优化,最终提供给编译后端;
编译后端根据不同的 cpu 架构,将中间代码汇编,产生汇编代码,最后解析汇编指令,生成目标代码,也就是机器码。
至此,编译器的工作结束。但是,机器码可以被 cpu 识别,却不能直接执行的。要生成可执行文件,还需要进行链接操作。

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.3.1指令解析
.file 声明源文件
.test 代码段
.section\rodata 只读数据段
.type 声明函数类型,对象类型
.size 声明大小
.long 声明long数据类型
.string 声明string类型
.ailgn 声明数据和指令的对其方式
.global 定义一个全局符号

3.3.2

汇编代前四行说明了源文件的名称是Hello.c,是文本文件类型,位于只读数据段,数据和指令按照8字节对齐

3.3.3

在hello.c函数中,有两段字符串需要输出,他们是:
(1)“用法: Hello 学号 姓名 秒数!\n”,这段字符串对应表9,是string.类型
(2)“Hello %s %s\n”,这段字符串对应表10,也是也string.类型

3.3.4

对输出的字符串编译,位于代码段,定义了全局变量main,类型是函数。

3.3.5

进入main函数后,首先开辟栈指针,main函数接受两个参数,第一个是argc,通过寄存器%edi传递到main,第二个是*argv,通过寄存器%rsi传递到main;进入main函数后说先保存两个参数到栈中相应位置,cmpl是比较argc是否等于4,相同则跳转到L2,对应于下图中的源代码

3.3.6

上面讲到argc等于4则跳转到L2,如果不等于则执行上图的指令,LC0是之前定义的字符串:“用法: Hello 学号 姓名 秒数!\n”,在此处把他保存到%rid做为参数传到puts函数中输出,输出后把1传给%edi作为exit函数的status,对应于下图中的源代码:

3.3.7

这是L2汇编代码,当argc==4时执行,收先初始化i=0,之后直接跳转到L3
对应下图中的源代码:

3.3.8:

上图为L3,首先比较i和7的值,如果i<=7则跳转到L4;否则继续执行,调用getchar()函数,最后设置main函数返回值为0,退出main函数;对应圆的码如下:

3.3.9

此部分是L4,对应源代码中的循环体,首先把*argv[]传递给%rax,之后+8,+16分别的到argv[1]和argv[2],把L1中的字符串:“Hello %s %s\n” 与argv[1]和argv[2]作为三个参数传递到puts函数输出;再让%rax+24获得argv[3]的值,把他传递给atoi,atoi的返回值传给sleep,最后i+1对应下面的源程序:

3.4 本章小结

汇编器ccl将文本hello.i翻译成文本文件hello.s,它包含一个汇编语言程序,我们通过对比源代码和汇编指令,深入理解了汇编器是如何采取优化,高效的将C语言中的各个类型的数据,代码转换为汇编指令,包括:操作数指令。数据传送指令,压出和弹出栈数据,加载有效地址,一元和二元操作,条件码,访问条件码,跳转指令,条件控制和条件分支,循环。
(第3章2分)

第4章 汇编

4.1 汇编的概念与作用
汇编阶段:汇编器as将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并把结果保存在hello.o中,是一个二进制文件,它包含的17个字节是main函数指令编码,在文本编辑器中打开hello.o文件为一堆乱码。
4.2 在Ubuntu下汇编的命令

表 12 linux下汇编命令

表 13 二进制汇编文件

4.3 可重定位目标elf格式

在linux下 输入readelf -a hello.o 读出二进制可重定位文件,可重定位目标文件中包含二进制代码和数据,包含重定位信息,可被链接,其形式可以再编译时和其它可重定位目标文件合并起来,创建一个可执行文件,可重定位目标文件的格式如下:

4.3.1
ELF头:包含16字节标识信息,文件类型,机器类型,节头表的偏移,节头表的表项大小以及表项个数:

(1)Magic:文件开头的几个字节通常用来确定文件类型或格式,加载或读取文件时,可用魔术确认文件类型是否正确;
(2)类别:指明了是64位版本
(3)数据:指明了数据格式是二进制补码,采用小端法;
(4)版本:1
(5)OS/ABI:指出操作系统
(6)类型:指出文件类型,可重定位不标文件
(7)系统架构:指出了X86-64
(8)入口地址:程序入口的虚拟地址。
(9)Start of section headers:指出节头表在ELF中的位置;
(10)Size of this head:指出了ELF头的大小
(11)Program headers:程序头表,在可重定位目标文件中没有程序投标,因此相关项都为0;
(12)Size of section headers:指出节头表中每行(节)大小
(13)Number of section headers:指出节头表中的行(节)数
(14)Section header string table index 13 :指出在节头表中第十二个表项式字符串表,索引就是13.

4.3.2

此部分是节头表中的信息,可见表项数为14个(0-13),附和ELF头中的数量,节头表中描述了每个节的名称,在文件中的偏移,大小,访问属性,对齐方式等。按照ELF头中节头表中每个节大小是64bytes,节头表中有14个表项目(节),我们可以算出整个节头表大小为64*14bytes;
一些常用行(节):
(1).text:已编译程序的机器代码;
(2).rodata:只读数据;
(3).data:已初始化的全局和静态变量。局部变量运行时被保存在栈中,不出现在.data节,也不出现在.bss节;
(4).bss:未初始化的全局和静态C变量;
(5).symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
(6)rel.text:.text节的重定位信息,用于修改代码段的指令的地址信息。
(7)rel.data: .data节的重定位信息,用于对模块使用或定义的全局变量进行重定位信息;
(8).debug:调试用符号表(gcc -c);
(9)strtab:包含symtab和debug节中符号及节名;
(10).rela.eh_frame:指明重定位类型;

Key to flags:指出每个节的属性。
详细查看rel.text节:

其中包含了.text节中的重定位信息,包含 puts exit .rodata printf等重定位符号;其中类型指出了重定位类型,基本的类型有两种:
(1)R X86_ 64 PC32: 重定位一个使用32位PC相对地址的引用。一个PC相对地址就是距程序计数器(PC)的当前运行时值的偏移量。当CPU执行一条使用PC相对寻址的指令时,它就将在指令中编码的32位值加上PC的当前运行时值,得到有效地址(如call指令的目标),PC值通常是下一条指令在内存中的地址。.
(2)R X86_ 64 _32:重定位一个使用32位绝对地址的引用。通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。

4.4 Hello.o的结果解析
在linux下使用objdump -d -r hello.o > hello.objdump得到hello.o的反汇编文件,将其与hello.s文件进行对比。

表 14 反汇编文件
与hello.s中对比可以发现:
(1):在条件分支语句中,hello.s使用符号L1,L2作为跳转目标,而在反汇编文件中,使用了相对偏移地址进行替换,可知段名称知识在汇编语言中便于比编写的之巨幅,在会变成机器语言中显然不存在,跳转目标的计算方法为上文中所述的R X86_ 64 PC32;


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

(3).数据访问变化: 全局变量访问,在hello.objdump中对.rodata中printf的格式串的访问需要通过链接时重定位的绝对引用确定地址,在hello.s中相应位置仍为占位符表示。hello.objdump中对.data中已初始化的全局变量sleepsecs为0x0+%rip的方式访问hello.s中访问方式为sleepsecs+%rip。
(4)hello.s是文本文件,可以直接打开阅读,而hello.o是二进制文件,无法直接查看。

4.5 本章小结

本章介绍了汇编的作用和概念,使用linux下的命令的到了hello的可重定位目标文件,并对可重定位目标文件的格式进行分析,通过查看hello.o的elf格式使用objdump得到反汇编代码与hello.s进行比较的方式,对汇编过程即作用,重定位的方式以及典型的ELF可重定位目标文件有了深入的理解。
(第4章1分)

第5章 链接

5.1 链接的概念与作用
链接阶段:在我们的程序中调用了一些其它函数库里的函数,例如hello.c中调用了C标准库提供的printf函数,它存在于另一个可重定位目标文件printf.o中,连接器把这类型的文件合并到我们的hello.o程序中,得到一个可执行的目便文件。到此,program的过程完全执行完成,接下来hello.c进入到process阶段。
作用:提供了一种模块化的方式,可以将程序编写为一个较小的源文件的集合,且实现了分开编译更改源文件,从而减少整体文件的复杂度与大小,增加容错性,也方便对某一模块进行针对性修改。
5.2 在Ubuntu下链接的命令

表 15 链接命令
5.3 可执行目标文件hello的格式
通过readelf -a hello> hello_out.elf获得hello的ELF格式,可执行目标文件装入存储器后可被执行,他的格式与可重定位目标文件有相似之处,也有不同点,它的典型格式如下:

表 16 ELF可执行目标文件结构

Hello_out.elf:

表 17 ELF头信息

在ELF头中,与可重定位目标文件有几个不同之处:
(1)在程序头起点,给出了程序第一条指令的地址,而在可重定位文件中,次字段为0;
(2)其次是多了一个程序头表,并指出了程序头表在文件中的起始位置,大小,程序头个数,也称段头表,是一个结构体数组,描述节和段的对应关系;
(3)多了init.节,用于定义init函数,该文件用来进行可执行目标文件开始执行时的初始化工作。
(4)少了两个.rel节,因为可执行目标文件不需要重定位。

表 18 节头表信息
节头表中的信息和可重定位目标文件中的基本一致,:对hello中所有的节信息进行了声明,包括大小Size以及在程序中的偏移量Offset,大小、全体大小、旗标、链接、信息、对齐等信息,根据Section Headers中的信息可以定位各个节所占的区间。其中地址一般是程序被载入到虚拟地址的起始地址

表 19 标志信息

表 20 程序头表

程序头表描述可执行文件中的节与虚拟内存空间中的存储映射关系,一个表表项说明虚拟地址空间中一个连续的段或一个特殊的节:
(1)type:指出段的种类。LOAD表示可装载段,在程序执行前从二进制文件映射到内存;DYMNAMIC包含了用于动态链接器的信息;INTERP表示当前段指定了可用于动态链接的程序解释器。通常是ld-linux.so;
(2)PHDR保存程序头表
(3)offset:段在文件中的偏移量
(4)VirtAddr:给出了短的数据映射到虚拟地址空间中的位置,只支持物理寻址;
(5)Flags:保存了标志信息,定义了该段的访问权限;
(6)Align:对齐

表 21 DYNAMIC

表 22 符号表

5.4 hello的虚拟地址空间

表 23 虚拟地址空间

使用EDB加载hello,并在Data Dump 窗口观察hello程序被加载到0x00401000-0x00495000段中
对比5.3中的两个LOAD段可知:
(1)第一个可装入段:第0x00000000-0x0x00001000字节(包括ELF头,程序头表,.inin .text .rodata节)映射到虚拟地址0x00400000开始长度为0x1000字节的区域,按照0x1000=4KB对齐,具有R只读权限,是只读代码段。
(2)第二个可装入段:第0x0001f20开始长度为0x00e字节的.data节,映射到虚拟地址0x00401f20开始长度为0x00e0字节的存储区域,同样是按照4KB对齐,具有RW可读可写权限,是可读写数据段。
(3)DYNAMIC:保存了动态链接器使用的信息,映射信息同上;权限为RW,对齐方式为0X08;
(4)GNU_RELOR:表示这段重定位结束之后那些内存区域是需要设置只读,权限为R,对齐方式为0x01;
(5)
表 24 虚拟内存
5.5 链接的重定位过程分析
Linux下使用指令objdump -d -r hello > hello_out.objdump 得到hello的反汇编文件,把他和hello文件进行对比。

表 25 重定位
首先,地址改为了虚拟内存地址,如0x00401c70,并且多了许多行,即许多节和子程序。
分析:在使用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。
重定位:链接器在完成符号解析以后,就把代码中的每个符号引用和正好一个符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。然后就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时的地址。重定位由两部组成。在hello到hello.o中,首先是重定位节和符号定义,链接器将所有输入到hello中相同类型的节合并为同一类型的新的聚合节。例如,来自所有的输入模块的.data节被全部合并成一个节,这个节成为hello的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每一个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。然后是重定位节中的符号引用,链接器会修改hello中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址。

5.6 hello的执行流程

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。通过edb的调试,一步一步地记录下call命令进入的函数。
函数 地址
hello!_libc_start_main 00401c98
hello!_deregister_tm_clones 00401d31
hello!_deregister_frame_info 00401d45
hello!puts 00401db5
hello!exit 00401dbf
hello!printf 00401df2
hello!atoi 00401e05
hello!sleep 00401e0c
hello!getchar 00401e1b
hello!_dl_relocate_static_pie 004021f7
hello!_dl_aux_init 00402233
hello!_libc_init 00402272
hello!_tunables_init 0040227e
hello!_tunable_get_val 0040232e
hello!_libc_fatal 00402418
hello!_get_common_indice.constprop.0 00402455
hello!_assert_fail 00402526
hello!_libc_setuo_tls 0040252b
hello!_setjmp 004025ec
hello!exit 00402642

5.7 Hello的动态链接分析
在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定(lazybinding),将过程地址的绑定推迟到第一次调用该过程时。

延迟绑定是通过GOT(全局偏移量表)和PLT(过程链接表)实现的。GOT是数据段的一部分,而PLT是代码段的一部分。
过程链接表(PLT)。PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。PLT[1]调用系统启动函数(__libc_start_main),它初始化执行环境。
全局偏移量表(GOT)。GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在ld-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
进入edb查看:

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

5.8 本章小结

本章介绍了hello链接的概念和作用,分析了可执行目标文件的格式,以及可执行文件到虚拟内存空间的映射,分析了静态链接是的重定位和符号解析流程,使用edb执行hello,最后分析hello动态链接过程,对在系统上运行程序的流程有了更深的理解。
(第5章1分)

第6章 hello进程管理

6.1 进程的概念与作用
进程:进程是操作系统对CPU执行程序的运行过程的一种抽象。一个执行中的程序的实例,指程序的一次运行过程,进程是具有独立功能的一个程序关于某个数据集合的一次运行活动,是一个动态的概念。
作用:(1)提供一个独立的逻辑控制流,它提供一种假象,好像我们运行的每个程序独占的适用处理器。
(2)提供一个私有地址空间,他提供一种假象,好像我们的程序独占的使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
shell是Linux上的一个命令解释器。Shell是Linux的外壳,它包在Linux内核的外面,为用户和内核之间提供了一个接口。当用户下达指令给操作系统的时候,实际上是将指令告诉shell,经过shell解释,处理后让内核做出相应的动作。系统的回应和输出的信息也由shell处理,然后显示在用户的屏幕上。
功能:shell应用程序提供了一个界面,用户通过访问这个界面访问操作系统内核的服务。
处理流程:
(1)从终端读入输入的命令。
(2)将输入字符串切分获得所有的参数
(3)如果是内置命令则立即执行
(4)否则调用相应的程序执行
(5)shell 应该接受键盘输入信号,并对这些信号进行相应处理

6.3 Hello的fork进程创建过程
父进程通过pid_t fork(void) 函数创建一个子进程,新创建的进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的但是独立的一份副本,包括代码段和数据段,堆,共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件,父进程与子进程最大的不同在于他没有不同的PID。
在hello的创建过程中 ,shell通过fork创建了hello子进程;

6.4 Hello的execve过程
execve函数加载并运行可执行文件hello,并且通过参数列表和环境变量传如命令参数,在当前进程的上下文中加载并运行一个新程序即hello程序,execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行hello程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC指向_start地址,_start最终调用hello中的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。

表 26 用户栈结构

6.5 Hello的进程执行
上文中讲到,hello有一个独立的逻辑控制流,一个私有地址空间。
逻辑控制流:一系列程序计数器PC的值,这些值唯一的对应于包含在程序的可执行目标文件中的指令,即hello可执行文件中的指令,这就是hello的逻辑控制流。
私有地址空间:进程为hello提供了它自己的私有地址空间,不能被其它进程读或写,在私有地址空间中,底部保留给hello程序,包括了hello的代码,数据,对=堆,栈段,代码段总是从0x00400000开始,顶部是内核虚拟内存;用户模式和内核模式:处理器通过某个控制寄存器中的一个模式位来提供限制一个应用可以执行的指令以及它可以访问的地址空间范围的功能。该寄存器描述了当前进程享有的特权。当设置了模式位时,进程就运行在内核模式中。没有设置模式位时,进程就运行在用户模式中。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存;用户模式的进程不允许和执行特权指令、也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。

表 27 进程地址空间
用户模式和内核模式:处理器通过某个控制寄存器中的一个模式位来提供限制一个应用可以执行的指令以及它可以访问的地址空间范围的功能。该寄存器描述了当前进程享有的特权。当设置了模式位时,进程就运行在内核模式中。没有设置模式位时,进程就运行在用户模式中。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存;用户模式的进程不允许和执行特权指令、也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。
上下文切换:较高形式的异常控制流来实现都任务;内核为每个进程维持一个上下文,上下文就是内核重新启动的一个被强占的进程所需的状态。由包括通用目的寄存器、浮点寄存器、程序计数器、用户站、状态寄存器、内核栈和各种内核数据结构。
进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。

表 28 上下文切换

6.6 hello的异常与信号处理
hello执行过程中出现的异常种类可能会有:中断、陷阱、故障、终止。
中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。硬件中断的异常处理程序被称为中断处理程序。
陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则处理程序返回到内核中的abort例程,abort例程会终止引起故障的应用程序。
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者SRAM位被损坏时发生的奇偶错误。终止处理程序从不将控制返回给应用程序。处理程序将控制返回给一个abort例程,该例程会终止这个应用程序。

表 29 正常运行hello

表 30 按下ctrl+z

表 31 运行 ps

表 32 运行jobs

表 33 fg

表 34 pstree

输入命令 kill -9 7488 杀死bash即全部子进程

表 35 重新运行hello

表 36 不停乱按后按下ctrl+c

当按下ctrl + c之后,shell父进程收到SIGINT信号,信号处理函数的逻辑是结束hello,并回收hello进程。
在程序运行中途乱按的结果,可以发现,乱按只是将屏幕的输入缓存到stdin,当getchar的时候读出一个’\n’结尾的字串(作为一次输入),其他字串会当做shell命令行输入。

6.7本章小结
本章介绍了进程的概念和作用,简述壳Shell-bash的作用与处理流程;调用fork创建子进程并加载hello函数,分析了hello的创建过程和加载过程,分析了hello的异常以及信号处理,对进程的创建,加载,信号的处理,回收有了更深的理解。
(第6章1分)

第7章 hello的存储管理

7.1 hello的存储器地址空间
在我们的系统中,hello进程是和其它进程共享内存和CPU资源的,为了更加有效地管理内存并且少出错,现代操作系统提供了一种对主存抽象的概念,叫做虚拟内存,虚拟内存为每个进程提高了一个大的,一致的私有地址空间。
逻辑地址:相对地址,hello中的地址空间,从0开始编号,有两种形式:以为逻辑地址和二维逻辑地址(段内地址);
线性地址:逻辑地址到物理地址变换的中间层,逻辑地址经过段机制后转换为线性地址,
虚拟地址:CPU 启动保护模式后,程序运行在虚拟地址空间中。与物理地址相 似,虚拟内存被组织为一个存放在磁盘上的 N 个连续的字节大小的单元组成的数 组,其每个字节对应的地址成为虚拟地址。虚拟地址包括 VPO(虚拟页面偏移量)、VPN(虚拟页号)、TLBI(TLB 索引)、TLBT(TLB 标记)。
计算机会呈现出要比实际拥有的内存大得多的内存量。因此他允许程式员编制并运行比实际系统拥有的内存大得多的程式。这使得许多大型项目也能够在具有有限内存资源的系统上实现
物理地址:物理地址是内存单元的绝对地址,与地址总线具有对应关系,又称绝对地址,即程CPU执行指令所使用的地址空间。计算机系统的准村被组织成一个由M个连续字节大小的单元组成的数组,每个字节都有一个唯一的物理地址;
7.2 Intel逻辑地址到线性地址的变换-段式管理
基本思想:
(1)把进程的所有分段都存放在辅存中,进程运行时先把当前需要的一段或几段装入主存,在执行过程中访问到不在主存的段时再把它们动态装入。
(2)段式虚拟存储管理中段的调进调出是由OS自动实现的,对用户透明。
(3)段覆盖技术不同,它是用户控制的主存扩充技术,OS不感知。
基本原理:
(1)虚地址以程序的逻辑结构划分为段,这是段页式的段式特征。
(2)实地址划分层位置固定、大小相同的页框(块),这是段页式的页式特征。
(3)将每一段的线性地址空间划分成与页框大小相同的页面,段页式的特征
(4)逻辑地址由段号s、段内页号p和页内偏移d组成
(5)对用户,虚拟地址由段号s和段内位移d’组成
(6)系统内部将d’分解为p和d,d’ = p * 块长 + d
(7)请求段页式虚拟存储管理的数据结构比较复杂,包含作业表、段表和页表三部分。
(8)作业表:进入系统的作业和作业段表的起始地址
(9)段表:是否在内存、段页表起始地址
(10)页表:是否在内存、对应内存块号
一个逻辑地址由两部分组成,段标识符和段内偏移量。段标识符由16位字段组成,前13位为索引号。

表 37 逻辑地址
索引号是段描述符的索引,很多个描述符,组成了一个数组,叫做段描述表,可以通过段描述标识符的前13位,再这个表中找到一个具体的段描述符,这个描述符就描述了一个段,每个段描述符由八个字节组成。
段描述符中的base字段,描述了段开始的线性地址,一些全局的段描述符,放在全局段描述符表中,一些局部的则对应放在局部段描述符表中。由T1字段决定使用哪个。
以下是具体的转化步骤:
(1)给定一个完整的逻辑地址;
(2)看段选择符T1,知道要转换的是GDT中的段还是LDT中的段,通过寄存器得到地址和大小;
(3)取段选择符中的13位,再数组中查找对应的段描述符,得到BASE,就是基地址;
(4)线性地址等于基地址加偏移;
7.3 Hello的线性地址到物理地址的变换-页式管理
核心思想: 每个进程都拥有一个独立的虚拟地址空间把内存看作独立的简单线性数组,操作系统为系统中每个进程都维护一个单独的页表,也就是一个独立的虚拟地址空间。
概念上而言,虚拟内存被组织为一个由存放在磁盘上的N个连续字节大小的单元组成的数组. 磁盘上数组的内容被缓存在物理内存中 (DRAM cache)  这些内存块被称为页 (每个页面的大小为P = 2p字节)

表 38 页式管理
VM系统将hello的虚拟内存分割为虚拟页的大小固定的块,物理内存也被分割为物理页,虚拟页就可以作为缓存的工具;虚拟页地址映射到物理页地址由页表进行映射,页表常住内存空间,虚拟地址空间中的每个页(VP)在页表固定位置有一个PTE,PTE由一个有效位和一个n位地址字段组成,有效位表示虚页是否被缓存,有效位为0,地址表示VP在磁盘位置或地址为空表示虚拟页还未分配,1则表示已缓存。

表 39 页表结构
Page hit页命中: 虚拟内存中 的 一个字存在于物理内存中,
即(DRAM)缓存命中。

表 40 页表命中
Page fault缺页: 虚拟内存中的字不在物理内存中 (DRAM 缓
存不命中)。缺页导致页面出错 (缺页异常),会进行缺页处理,如果内存中仍有空间,则直接到磁盘中缓存,如果物理内存已满,缺页异常处理程序选择一个牺牲页,进行替换。

表 41 页表不命中

表 42 缺页处理

7.4 TLB与四级页表支持下的VA到PA的变换
页表PTE(页表条目)的数组,它将虚拟页映射到物理页,每个 PTE 都有一个有效位和一个 n 位地址字段,有效位表明该虚拟页是否被缓存在 DRAM 中,地址字段表明 DRAM 中相应物理页的起始位置,它分为两个部分:VPO(虚拟页面偏移)和 VPN(虚拟页号),如图

表 43 VP到PA
页面命中时:
(1) 处理器生成一个虚拟地址,并将其传送给MMU
(2) MMU 生成PTE地址发出请求,高速缓存/主存向MMU返回PTE
(3)MMU 将物理地址传送给高速缓存/主存
(4)高速缓存/主存返回所请求的数据字给处理器
缺页时:
(1)处理器将虚拟地址发送给 MMU
(2) MMU 使用内存中的页表生成PTE地址
(3)有效位为零, 因此 MMU 触发缺页异常
(4) 缺页处理程序确定物理内存中牺牲页 (若页面被修改,则换出到磁盘)
(5)缺页处理程序调入新的页面,并更新内存中的PTE
(6) 缺页处理程序返回到原来进程,再次执行导致缺页的指令

利用TLB加速地址翻译:MMU中一个小的具有较高相联度的缓存,实现虚拟页面向物理页面的映射,对于页面数很少的页表可以完全包含在TLB中。
TLB可以理解为页表的一个小的,虚拟寻址的高速缓存(类似cache是内存的一个缓存),用于组选择和行匹配的索引和标记字段是从虚拟地址的页号中提取出来的,如下图所示,如果TLB有2^t组,TLB索引(TLBI)由VPN的t个低位组成的,TLB标记(TLBG)是由VPN中剩余的位组成的。

表 44 TLB 机构

表 45 使用TLB

表 46 结合高速缓存和虚拟内存
为了避免页表在内存空间中的占用,例如:4KB (212) 页面, 48位地址空间, 8字节 PTE,将需要一个大小为 512 GB 的页表! (248 * 2-12 * 23 = 239 bytes),因此引入了多级页表的机制,k级页表示意图如下。如果虚拟内存不存在,则不分配页表来记录这段内存。
1级页表指向2级页表,2级页表存储的是3级页表基地址,只有最后一级页表的内容是PTE和物理页号。

表 47 多级页表地址翻译过程

7.5 三级Cache支持下的物理内存访问
MMU 发送物理地址 PA 给 L1 缓存,L1 缓存从物理地址中抽取出缓存偏移CO、缓存组索引 CI 以及缓存标记 CT。高速缓存根据 CI 找到缓存中的一组,并通过 CT 判断是否已经缓存地址对应的数据,若缓存命中,则根据偏移量直接从缓存中读取数据并返回;若缓存不命中,则继续从 L2、L3 缓存中查询,若仍未命中, 则从主存中读取数据。
7.6 hello进程fork时的内存映射
在 Shell 输入命令行后,内核调用fork创建子进程,为 hello 程序的运行创建上下文,并分配一个与父进程不同的PID。通过 fork 创建的子进程拥有父进程相同的区域结构、页表等的一份副本,同时子进程也可以访问任何父进程已经打开的文件。当 fork 在新进程中返回时,新进程现在的虚拟内存刚好和调用 fork 时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间。
7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
(1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已 存在的区域结构。
(2)映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域 结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文 件,其大小包 含在hello中,栈和堆地址也是请求二进制零的,初始长度为 零。
(3)映射共享区域, hello程序与共享对象libc.so链接,libc.so是动态 链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器(PC),execve做的最后一件事情就是设置当前进 程上下文的程序计数器,使之指向代码区域的入口点

表 48 虚拟地址空间
7.8 缺页故障与缺页中断处理
在7.3页式管理中已经详细讲述。
7.9动态存储分配管理
在程序运行时程序员使用动态内存分配器 (比如malloc) 获得虚拟内存. 数据结构的大小只有运行时才知道。
动态内存分配器维护着一个进程的虚拟内域,称为堆。
分配器将堆视为一组不同大小的 块(blocks)的集合来维护,每个块要么是已分配的,要么是空闲的。
分配器的类型:
 (1)显式分配器: 要求应用显式地释放任何已分配的块。例如,C语言中的 malloc 和 free
 (2)隐式分配器: 应用检测到已分配块不再被程序所使用,就释放
这个块,比如Java,ML和Lisp等高级语言中的垃圾收集 (garbage
collection)。

表 49 动态内存分配

7.10本章小结
本章重点介绍了计算机系统中存储管理部分的虚拟内存系统,本章首先描述了虚拟内存的概念以及它的工作方式,特别是:存储器地址空间、段式管理、页式管理,VA 到 PA 的变换、物理内存访问, hello 进程 fork 时和 execve 时的内存映射、缺页故障与缺页中断处理、等。接着描述了应用程序如何使用和管理虚拟内存地址,如动态存储分配管理,结合书本第六章存储器体系结构,对整个计算机系统的内存管理有了更深入的认识。
(第7章 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口

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

表 50 课本p631

表 51 课本p632

Linux文件都有一个类型来表明它在系统中的角色:
普通文件包含任意数据;
目录是包含一组链接的文件;
套字节是用来与另一个进程进行跨网络通信的文件;

8.2 简述Unix IO接口及其函数

8.2.1打开文件

表 52 打开文件
Open函数将filename转换为一个文件描述符,并返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件:
(1)O_RAONLY:只读
(2)O_WRONLY:只写
(3)O_RDWR:可读可写

8.2.2关闭文件

表 53 关闭文件

8.2.3读和写文件

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

在某些情况下,read和write传送的字节比应用程序要求的要少,这些不足值不表示有错误,出现这样情况的原因有:
(1)读时遇到EOF
(2)从终端读入文本行
(3)读和写网络套字节

8.3 printf的实现分析
1.int printf(const char fmt, …)
2.{
3.int i;
4.char buf[256];
5.
6. va_list arg = (va_list)((char
)(&fmt) + 4);
7. i = vsprintf(buf, fmt, arg);
8. write(buf, i);
9.
10. return i;
11. }
Vsprintf函数将所有的参数内容格式化之后存入 buf,返回格式化数组的长度,vsprintf 函数如下:
1.int vsprintf(char *buf, const char fmt, va_list args)
2.{
3. char
p;
4. char tmp[256];
5. va_list p_next_arg = args;
6. for (p=buf;*fmt;fmt++) {
7. if (*fmt != ‘%’) {
8. *p++ = *fmt;
9. continue;
10. }
11. fmt++;
12. switch (*fmt) {
13. case ‘x’:
14. itoa(tmp, ((int)p_next_arg));
15. strcpy(p, tmp);
16. p_next_arg += 4;
17. p += strlen(tmp);
18. break;
19. case ‘s’:
20. break;
21. default:
22. break;
23. }
24. }
25. return (p – buf);
26.}

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
最后,hello 程序的输出:hello 就显示在了屏幕上。
https://www.cnblogs.com/pianist/p/3315801.html
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5NwOkeU3-1653193306518)(在这里插入图片描述)]](https://img-blog.csdnimg.cn/2790eb6fc3404800b18b5741339006ac.png)

8.4 getchar的实现分析

表 55 getchar
hello 程序调用 getchar 后,它就等着键盘输入。当我们输入时,会发生异常,内核中的键盘中断处理子程序来进行处理,接受按键扫描码转成 ASCII 码,保存到系统的键盘缓冲区。
当键入回车之后,getchar 开始从 stdin 流中每次读入一个字符。
getchar 函数的返回值是用户输入的字符的 ASCII 码,若文件结尾则返回 -1(EOF),且将输入的字符回显到屏幕。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了linux I/O的一般概念,UNIX I/O接口以及执行方式,介绍了文件的打开关闭读写函数,参考博客了解了printf和getchar的实现分析。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
通过前八章的介绍和总结,就好似读完一本书,看完一部电影,而其中的主角无疑是hello,hello在前往cup的道路上经历了千难万险:

从大一时初懵懵懂懂照着书一个字符一个字符把敲入电脑得到自己的第一个程序hello.c,鼠标点击运行按钮,无意识中将hello进行预处理、编译、汇编、链接;在壳(Bash)里,伟大的OS(进程管理)为hello fork(Process),为hello execve,为hello mmap,为hello分配时间片,让hello得以在Hardware(CPU/RAM/IO)上驰骋(取指译码执行/流水线等);OS(存储管理)与MMU将 hello从 VA翻译到PA;TLB、4级页表、3级Cache,Pagefile等等各显神通为hello的运行加速;IO管理与信号处理使尽了浑身解数,软硬结合,才使的hello能在键盘、主板、显卡、屏幕间游刃有余, 最终才成为我们在窗口中看到的那一句“Hello World!”,当任意按下按键关闭后,Bash!在hello完美谢幕后为我收尸,内核删除与它相关的所有数据结构,但它在这个世界的所有痕迹并未被完全清除,他留下了一系列里程碑供后来者学习参考;

Hello的每一个阶段的都是21世界人类最伟大的智慧结晶。
(结论0分,缺失 -1分,根据内容酌情加分)

附件
hello.c:源代码
hello.i:预处理后的文本文件
hello.s:编译后的文本文件
hello.o:汇编后的二进制可重定位目标文件
hello:连接后的可执行目标文件
hello.out:hello.o的ELF
hello_out.elf:hello的ELF
hello.objdump:hello.o的反汇编
hello_out.objdump:hello的反汇编

表 56 中间结果
(附件0分,缺失 -1分)

参考文献

为完成本次大作业你翻阅的书籍与网站等
[1] (54条消息) 预处理的功能_bluedogcolan888的博客-CSDN博客_预处理作用
[2] (54条消息) ARM汇编基础_Stone啦的博客-CSDN博客
[3] linux应用程序——ELF查看工具 – 简书 (jianshu.com)
[4] (54条消息) 计算机操作系统-3-存储管理_SpriCoder的博客-CSDN博客_存储管理
[5] https://www.cnblogs.com/pianist/p/3315801.html

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