HIT计算机系统大作业

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机
学   号 1190201518
班   级 1903009
学 生 杨东晨    
指 导 教 师 吴锐

计算机科学与技术学院
2021年5月
摘 要
一望无际的草原之上,一个白衣少年迎风而立。少年对面是一个身穿格子衬衫的绝顶中年男人,男人带着黑框眼睛,神情呆滞而又空灵。只见,少年手臂之上字符串环流浮现,左手飞速编写代码,右手对着虚空一指。刹那间风云突变,手臂之上的字符串化作了一条黑色的狂龙朝着中年男人的方向横冲过去。中年男人神色依旧呆滞,只见他缓慢的伸出右手,一串金光凭空出现,金光化作点点星芒,黑色狂龙竟在这星芒之下逐渐消失。还没等少年反应过来,星芒已击中少年的脑袋。在这一瞬间,少年脑中竟浮现出了一串hello world。
少年跪倒在地,已泣不成声。他用难以置信的语气问道:“为什么我用哈夫曼树加密过的字符串竟然打不过一个最基本的hello world?”
中年男人捋了捋头上为数不多的秀发,神情由呆滞转变为了鄙视。他开口道:“年轻人,你对编程一无所知。你只知道最基本的编写hello world,却不知道hello world如何从Program到Process,更不必说它从Zero-0到Zero-0的过程了。基础不牢,地动山摇。我这里有一本CSAPP,今日我看与你有缘,便将它赠与你,回去之后仔细研读,将来必成绝顶”
少年神情严肃的接过男人手中的黑色厚书,朗声问道:“还未请教前辈大名。”
男人淡然一笑:“名字我早就忘了,IT界的朋友们都称呼我为战神。”
少年愕然,在回过神时男人早已消失,只留下那一串hello world在少年脑海之中久久不能散去。
关键词:hello world;P2P;O2O;

目 录

第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的整个过程。
Hello的生命起始是一个.c的文本文件,经过预处理,编译,汇编,链接等一系列操作后,最终变成了一个可执行文件。
在这里插入图片描述

P2P( From Program to Process ):在linux中,hello.c经过cpp的预处理变成hello.i的文本文件,再由他经过ccl的编译生成一个hello.s的文本文件,再经过as的汇编生成hello.o二进制文件,最后由ld生成最终的可执行目标程序hello(二进制)。在shell中键入启动命令后,shell为其fork产生子进程,然后hello就变成了一个进程。
020( From Zero-0 to Zero-0 ):shell为其execve,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位
开发与调试工具:gcc,vim,edb,readelf,HexEdit

1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名称 文件作用
hello.i 预处理之后的文本文件,解析了头文件的内容并将其直接插入到源程序中。
hello.s Hello.i编译后的文本文件,包括词法分析、语法分析、语义分析、中间代码生成以及优化等等一系列的中间操作。
hello.o 汇编器根据指令集将汇编程序 hello.s 翻译成机器指令,并且把这一系列的机器指令按照固定的规则进行打包,得到可重定位目标文件 hello.o 。
hello 链接器(ld)负责把 hello.o 和 printf.o 进行合并,当然这个合并是要遵循一定规则的。正是因为链接器要对 hello.o 和 printf.o 的进行调整,所以 hello.o 才会被称之为可重定位目标文件。最终经过链接阶段可以得到可执行目标文件 hello。
hello.out hello反汇编之后的可重定位文件

1.4 本章小结
本章大致的介绍了hello.c从一个单纯的文本文件,经过了一系列不为人知的处理后(p2p,o2o),变成了一个可执行文件。阐明了大作业所需的环境配置及工具。列出了产生的中间结果文件及其作用。

第2章 预处理
2.1 预处理的概念与作用
概念:程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。
作用:
(1)宏定义
(2)文件包含
(3)条件编译

2.2在Ubuntu下预处理的命令
预处理命令如下:
gcc -E hello.c -o hello.i
截图如下:
在这里插入图片描述
在这里插入图片描述

2.3 Hello的预处理结果解析
在这里插入图片描述

预处理之后,生成了一个hello.i文件,打开该文件发现,文件共3060行,且是文本文件。hello.i文本文件对原文件中的宏进行了宏展开,并且解析了头文件中包含的内容。另外,如果代码中有#define命令还会对相应的符号进行替换。
2.4 本章小结
大致说明了预处理的相关概念及作用,例如实现将定义的宏进行符号替换、引入头文件的内容、根据指令进行选择性编译等。预处理的引入对于程序的组织,调试和一些编程技巧的实现有着非常显著的作用。

第3章 编译
3.1 编译的概念与作用
概念:编译器将 hello.i 文件翻译成 hello.s 文件,这一过程我们称为编译。
作用:编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
3.2 在Ubuntu下编译的命令
对hello.i进行编译的命令是: gcc -S -o hello.s hello.i
在这里插入图片描述

3.3 Hello的编译结果解析
3.3.1汇编指令介绍
在这里插入图片描述

指令 指令说明
.file 声明源文件
.text 已编译的程序的机器代码。
.rodata 只读数据,比如printf中格式串和开关语句的跳转表。
.align 数据或者指令的地址对其方式
.string 声明一个字符串(.LC0,.LC1)
.global 声明全局变量(main)
.type 声明一个符号是数据类型还是函数类型
3.3.2 数据
1.整型
(1)main的参数argc:
在这里插入图片描述

argc是main函数传入的第一个参数,用来表示写入的有效命令行参数的个数,写法为main(int argc,char *argv[])。argc寄存在%edi中,然后通过movl指令存入到-20(%rbp)中。
(2)局部变量i
在这里插入图片描述

 函数内部的局部变量存储在寄存器或者栈中。int i存储在-4(%rbp)中,初值为0.

2.字符串
在这里插入图片描述

由上图可知,这两个字符串作为printf函数的传入参数。这两个字符串的信息被放在.rodata节中。
3.指针数组 *argv[]
在这里插入图片描述

argv[]是main函数的第二个参数,其中的元素为指向字符类型的指针。数组的起始位置是-32(%rbp),被调用两次传给printf函数。
4.全局变量
在这里插入图片描述

有图可知,全局变量为main函数。
3.3.3运算与赋值操作
(1)赋值
在这里插入图片描述

如上图i的起始值为0,对i的赋值操作通过mov语句来实现。
(2)算术计算
在这里插入图片描述

如上图main中的i++,通过add语句来实现。
3.3.4关系操作
(1)argc!=3;是条件判断语句,用来判断argc的大小。同时这条cmpl的指令还有设置条件码的作用,当根据条件码来判断是否需要跳转到分支中。
在这里插入图片描述

(2)for循环中i<8是判断循环结束条件。将其与7进行比较,为下一步 jle 利用条件码进行跳转做准备。
在这里插入图片描述

3.3.5控制转移指令
在hello.c中有两处。
(1)if(argc!=4):
如前面分析,编译器根据argc与4的大小比较,从而进行跳转。条件码跳转表如下图。
在这里插入图片描述

(2)for(i=0;i<8;i++)
在这里插入图片描述

如前面的分析,将-4(%rbp)中的i与7进行比较,如果不大于7则跳转到.L4
在这里插入图片描述

3.3.6函数调用
调用函数时有以下操作:(以函数P调用函数Q为例)
(1)传递控制:在进行过程 Q 的时候,必须要将程序计数器设置为 Q的起始地址。当函数返回时,要把程序计数器设置为 P 中调用 Q 语句后面的那条指令的地址。
(2)传递数据:P要向Q 传递参数,Q要向P返回一个值。
(3)分配和释放内存:Q可能需要分配空间来存放局部变量,而在返回前,又要释放这些空间。

hello.c涉及的函数操作有:
main函数,printf,exit,sleep,atoi,getchar函数
main函数的参数是argc和argv
exit参数是1,sleep函数参数是atoi(argv[3])
函数的返回值存储在%eax寄存器中。
3.3.7类型转换

hello.c中的类型转换为:atoi(argv[3]),atoi是把字符串转换为int型。

3.4 本章小结
本节涉及到大量的汇编指令,理解汇编指令对于程序的分析和过程的理解有着不可轻视的作用。通常,程序会将常量放入.radata节中,已初始化的全局变量放入.data节中,通过赋值,算术运算,控制转移指令,函数调用等操作,生成hello.s文件为后续生成可执行文件做准备。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
概念:汇编器根据指令集将汇编程序 hello.s 翻译成机器指令,并且把这一系列的机器指令按照固定的规则进行打包,得到可重定位目标文件 hello.o 。
作用:翻译生成机器语言,因为机器语言是计算机能直接识别和执行的一种语言.
4.2 在Ubuntu下汇编的命令
在Ubuntu下汇编的命令:gcc -c hello.s -o hello.o
在这里插入图片描述

4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
elf格式:
在这里插入图片描述

使用readelf获得hello.o的elf格式
在这里插入图片描述

4.3.1 elf头
ELF Header包含了ELF头的大小,文件类型,机器类型等信息。根据下图所示信息,该文件是REL,有14个节,节头大小64字节,字符串索引节头为13。
在这里插入图片描述

4.3.2节头部表
记录了各个节的位置和大小,其中目标文件中每个节都有一个固定大小的条目。具体的描述包括节的名称、类型、地址和偏移量等。由于是可重定位目标文件,所以每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,同时可以观察到,代码是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。如下图所示:
在这里插入图片描述

4.3.3重定位条目
ELF重定位条目的数据结构如下图
Typedef struct{
long offset; /需要被修改的引用的节偏移/
long type:32, /重定位类型/
symbol:32; /标识被修改引用应该指向的符号/
long attend; /符号常数,对修改引用的值做偏移调整/
}ELF64_Rela
有两种基本的重定位类型,分别为绝对地址引用,相对地址引用。
在这里插入图片描述

4.3.4符号表
存放程序中定义和引用的函数和全局变量的信息。name是符号名称,value是相对位置偏移。size是目标的大小,type是类型。Bind字段表明符号是本地的还是全局的。
在这里插入图片描述

4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
hello.o的反汇编代码如下图
在这里插入图片描述
在这里插入图片描述

hello.s如下图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

对比以上两个文件,发现不同之处大致如下:
(1)操作数
hello.o的反汇编代码中的操作数是十六进制的。
在这里插入图片描述

而hello.s文件中的操作数是十进制的。
在这里插入图片描述

(2)分支转移
hello.s中的分支转移是通过段名称进行的。
在这里插入图片描述

hello.o反汇编代码中的分支转移则是通过函数的相对偏移地址进行。
在这里插入图片描述

(3)函数调用
hello.s中调用函数用call后加函数的名称。
在这里插入图片描述

hello.o中调用函数call后面是函数的相对偏移地址。因为函数在链接后才会有确定的地址,所以在.rela.text节中为其添加重定位条目。
在这里插入图片描述

(4)指令
hello.s中的指令都有后缀,如movl,subq,leaq等。
在这里插入图片描述

hello.o的反汇编代码中的指令则大多都没有后缀。
在这里插入图片描述

4.5 本章小结
本章主要分析了汇编生成的可重定位文件的ELF格式,并且对hello.o的结果进行分析,对比hello.o的反汇编代码与hello.s文件代码,分析汇编语言到机器语言的一一映射关系。

第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合成可执行文件的过程,这个文件可被加载到内存执行。
作用:合并各个.obj文件的节合并符号表,进行符号解析;进行符号地址的重定位;生成可执行文件
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.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
先生成hello的ELF格式文件,如下图
在这里插入图片描述

分析如下:
5.3.1 ELF头
在这里插入图片描述

包含了文件的总体信息,包括程序入口点,头的大小,文件类型等。
5.3.2 节头
在这里插入图片描述
在这里插入图片描述

节头对hello中的所有节的信息进行了声明,包括大小,偏移量,起始地址等。根据节头所给信息可以定位各个节所在的区间。
3.3.3 程序头
在这里插入图片描述

一共有8个段
(1)PHDR包含程序头表本身
(2)INTERP:只包含了一个section,在这个节中,包含了动态链接过程中所使用的解释器路径和名称。
(3)两个LOAD:第一个是代码段,第二个是数据段。在程序运行时需要映射到虚拟空间地址。
(4)DYNAMIC:保存了由动态链接器使用的信息。
(5)NOTE: 保存了辅助信息。
(6)GNU_STACK:堆栈段。
(7)GNU_RELRO:在重定位之后哪些内存区域需要设置只读。
3.3.4 重定位节
在这里插入图片描述

3.3.5 段节
在这里插入图片描述

5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
用edb加载hello程序后,发现在Data Dump里的hello的虚拟地址空间。
在这里插入图片描述

其余各段依据前面的程序头依次映射。
在这里插入图片描述

5.5 链接的重定位过程分析
执行命令objdump -d -r hello ,得到hello的反汇编代码如下图。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

5.5.1 hello与hello.o的反汇编代码比较
将其与hello.o的反汇编代码比较,可得到以下几点不同:
(1)地址访问
hello.o反汇编代码中没有虚拟地址,而hello的反汇编代码中有明确的虚拟地址。这意味着hello已完成了重定位。
(2)链接增加新的函数
hello的反汇编代码相比于hello.o的反汇编代码增加了hello.c中用到的函数,如printf, sleep, exit等。
(3)新加入的节
hello的反汇编代码中还增加了许多hello.o代码中没有的节,他们各自有各自的作用。如.init, .plt节等。
(4)函数调用
hello中没有hello.o中的重定位条目,函数调用的地址都是确定的虚拟地址。而hello.o中的地址要在链接后才能确定,所以在.rela.text节中添加了重定位条目。
5.5.2 hello的重定位过程分析
链接过程就是把各个目标文件的各节组合在一起,并且将所有重定位条目的地址替换为运行时的相对或绝对地址的引用。具体过程如下:
(1)连接器将所有类型相同的节合并,这个节就是可执行文件的节。然后连接器把运行时的地址分配给新的聚合节。
(2)在符合引用中,连接器修改代码节和数据节中对每个符号的引用,是他们指向新的地址。此时,连接器依赖于可重定位目标模块中的可重定位条目。
(3)当连接器遇到一个未知的目标引用时,他会生成一个放在.rel.txt中的重定位条目。
(4)重定位过程地址计算
在这里插入图片描述

5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
_dl_start_user

_dl_init

_start

_libc_start_main

_cxa_atexit

_libc_csu_init

_init

_setjmp

_sigsetjmp

_sigjmp_save

main

(main后)

puts@plt

exit@plt

_dl_runtime_resolve_xsave

_dl_fixup

_dl_lookup_symbol_x

exit

5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。
5.7.1 GOT表
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
在这里插入图片描述

5.7.2 PLT
PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
5.7.3 结果分析
由上图可知,got表头起始位置为:0x404000。
调用之前0x404000后的16个字节均为0:
在这里插入图片描述

调用完之后,0x404000后的两个8字节分别为:0x40 3e 50, 0x7f 2e 62 e1 c1 90。
其中GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,改变后的GOT表如下: ​
在这里插入图片描述

在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码,GOT存放的是PLT中函数调用指令的下一条指令地址。
在函数调用时,首先跳转到PLT执行.plt中操作,第一次访问跳转时GOT地址为下一条指令,将函数序号入栈,然后跳转到PLT[0],之后将重定位表地址入栈,访问动态链接器,在动态链接器中使用在栈里保存的函数序号和重定位表计算函数运行时的地址,重写GOT,返回调用函数.之后如果还有对该函数的访问,就不用执行第二次跳转,直接参看GOT信息.
5.8 本章小结
本章主要介绍了链接的概念与作用,可执行目标文件hello的格式,hello的虚拟地址空间,链接的重定位过程分析,hello的执行流程。主要运用EDB工具来检查hello的反汇编代码。通过hello与hello.o的反汇编代码对比分析,更加有效的说明了链接的作用。至此,可执行目标文件hello就此诞生。

第6章 hello进程管理
6.1 进程的概念与作用
概念:在操作系统的角度来看进程是操作系统分配资源的最小单位。
简单来说进程就是处于执行期的程序(目标码存放在某种存储介质上)。但进
程并不局限于一段可执行程序代码(代码段)。通常进程还要包含其他资源,像打
开的文件(即在 Linux 中对应的文件描述符),挂起的信号,内核内部数据,处理
器的状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程,当
然包括用来存放全局变量的数据段等
作用:进程为用户提供以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象.进程也被称为计算机科学中最伟大的创新。
6.2 简述壳Shell-bash的作用与处理流程
1.shell 的定义
shell 是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接
收用户输入的命令并把它送入内核去执行。
2.shell 的功能
Shell 是一个命令行解释器,它为用户提供了一个向 Linux 内核发送请求以便
运行程序的界面系统级程序,用户可以用 Shell 来启动、挂起、停止甚至时编写
一些程序。Shell 还是一个功能相当强大的编程语言,易编写,易调试,灵活性较
强。Shell 是解释执行的脚本语言,在 Shell 中可以直接调用 Linux 系统命令
3.shell 的处理流程
1)从终端读入输入的命令。
2)将输入字符串切分获得所有的参数
3)如果是内置命令则立即执行
4)否则调用相应的程序执行
5)shell 应该接受键盘输入信号,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
在终端中输入命令行./hello 1190201518 杨东晨 1后,shell会处理该命令,如果判断出不是内置命令,则会调用fork函数创建一个新的子进程。
linux使用fork来生成一个子进程,fork() 函数的返回值。如果创建失败,将得到 -1;如果创建成功,在父进程中将得到子进程的 PID,在子进程中将得到 0。
如果在 fork() 函数之后用一个 if 语句对 fork() 函数的返回值进行判断,子进程和父进程将进入不同的分支。
注:fork之后,子进程和父进程,一开始得到的资源是一样的,但是之后相互独立。子进程修改后,父进程是得不到。
进程图如下:
在这里插入图片描述

6.4 Hello的execve过程
Execve函数加载并运行可执行文件,将hello加载到当前的进程中,主要步骤如下:
(1)加载器删除子进程现有的虚拟地址的用户部分中的已存在的区域结构;
(2)映射私有区域,为新程序的代码、数据、bss、栈区域创建新的区域结构。虚拟地址空间的代码和数据区域被映射为hello文件中的.txt和.data区域。其中栈和堆初始长度为0,栈和堆的结构如图6.1;
(3)映射共享区域,将动态链接的部分映射到用户虚拟地址空间中的共享区域;
(4)设置程序计数器,exceve做的最后一件事就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。下一次调用此进程时,它将从这个入口点开始执行。
在这里插入图片描述

               图6.1

6.5 Hello的进程执行
由于计算机不止需要处理hello,但又只有有限的CPU,而且不同进程间还会同时使用内存资源。故而hello并不是独享计算资源。而是通过不断保存读取上下文信息,在自己的时间片内处理自己的计算。
(1)逻辑控制流:即使在系统中有许多进程在运行,但进程会给我们提供一种它在独占地使用处理器的假象。如果我们用调试器单步执行程序,我们可以看到一系列的PC的值,这些值唯一的对应于包含在程序的可执行目标文件中的指令,或者时运行时动态链接到程序的共享对象中的指令。这个PC值的序列就是逻辑控制流。
(2)时间片:一个进程和其它进程轮流运行的概念称为多任务,也叫做时间分片。一个进程执行它的控制流的一部分的每一时间段叫做时间片。
(3)用户模式与内核模式:处理器通常是用某个控制寄存器中的一个模式位来限制一个应用可以执行的指令以及它可以访问的地址空间范围。当设置了模式位时,进程就运行在内核模式中。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中。用户模式不允许执行特权指令,也不允许用户模式下的进程直接引用地址空间中内核区内的代码和数据。
(4)上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构。
其具体过程如下:
(1)此时hello正在正常执行
(2)这时hello发生休眠或者时间片用完,这时内核将会决定把计算资源交给另外一个进程——进程B
(3)此时磁盘开始为B复制信息至内存,而操作系统进入内核模式,交接hello和B的上下文。
(4)当磁盘工作完成时,将会发送信号至内核,内核此时恢复了B上一次执行的状态,在B自己看起来就像自己连续执行一样。
(5)随后B将会进行与hello相同的操作
在这里插入图片描述

6.6 hello的异常与信号处理
6.6.1异常的分类
在这里插入图片描述

6.6.2 产生的信号
在发生异常时会发出信号,比如缺页故障会导致OS发生SIGSEGV信号给用户进程,而用户进程以段错误退出。常见信号种类如下表所示。
在这里插入图片描述

6.6.3 处理方法
1)中断处理:中断是异步发生的,是来自处理器外部的 I/O 设备的信号的结果。硬件中断不是由任何一条专门的指令造成的,从这个意义上来说它是异步的。硬件中断的异常处理程序常常称为中断处理程序。
在这里插入图片描述

2)陷阱处理:陷阱是有意的异常,是执行一条指令后的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
在这里插入图片描述

3)故障处理:故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误,它就将控制返回给引起故障的指令,从而重新执行它,否则,处理程序返回到内核中的 abort 例程,abort 例程会终止引起故障的应用程序。一个典型的故障示例是缺页异常。
在这里插入图片描述

4)终止处理:终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如 DRAM 或者 SRAM 位被损坏时发生的奇偶错误。终止处理程序不会将控制返回给应用程序。
在这里插入图片描述

6.6.4 执行异常
(1)正常执行。下图是hello程序正常运行的结果。输入ps命令后发现后台没有正在执行的hello进程。此时进程正常结束并被回收了。
在这里插入图片描述

(3)乱按。发现在执行过程中乱按的结果会出现在输出中,被当成了新的shell命令,在hello结束后,用ps查看hello进程,发现其并没有正在执行。此时进程正常结束并被回收了。此外,hello与之前正常执行的情况一致。
在这里插入图片描述

(4)按CTRL+Z。运行中hello进程按下CTRL+Z后,会发送一个SIGTSTP信号给shell。然后shell将其发送给当前的进程组,挂起hello进程。
在这里插入图片描述

输入jobs命令:
在这里插入图片描述

输入pstree命令,查看进程关系的树状图:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

使用fg命令,将hello重新运行一次,使用CTRL+Z使其挂起,使用fg指令完成剩下的执行。
在这里插入图片描述

使用kill命令,运行进程,使用kill命令杀死它。
在这里插入图片描述

(5)CTRL+C命令。在hello进程运行是按下CTRL+C,将会使内核发送一个SIGINT信号到前台进程组的每个进程。这会终止前台作业,并杀手hello进程。

在这里插入图片描述

6.7本章小结
在本章中,概括了hello进程的概念,作用,进程的执行以及异常信号处理。加深了对异常控制流的了解。介绍了一些重要的进程,如fork与execve进程。分析了进程的创建,程序的运行,上下文的切换,并发程序的机理,描述shell工作简单原理。

第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:逻辑地址(LogicalAddress)是指由程序产生的与段相关的偏移地址部分。就是hello.o里面的相对偏移地址。
线性地址:逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式。分页机制中线性地址作为输入。
虚拟地址:CPU产生的需要访问主存的地址(Virtual Address,VA).就是hello中的虚拟内存地址。
物理地址:计算机系统的主存被组织成一个由M 个连续的字节大小的单元组成的数组。每字节都有一个唯一的物理地址。hello在运行时虚拟内存地址对应的物理地址。用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。

7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由段选择符和偏移量组成,线性地址为段地址与逻辑地址中的偏移量的和。其中,段地址存放在段描述符中。而段描述符存放在描述符表中,也就是GDT(全局描述符表)或LDT(局部描述符表)中。段标识符是由一个16位长的字段组成,称为段选择符,其中前13为是索引号,后三位描述了一些硬件细节。
7.3 Hello的线性地址到物理地址的变换-页式管理
CPU的页式内存管理单元,负责把一个线性地址,转换为物理地址。Linux将虚拟内存组织成一些段的集合,段之外的虚拟内存不存在因此不需要记录。内核为hello进程维护一个段的任务结构即图中的task_struct,其中条目mm指向一个mm_struct,它描述了虚拟内存的当前状态,pgd指向第一级页表的基地址(结合一个进程一串页表),mmap指向一个vm_area_struct的链表,一个链表条目对应一个段,所以链表相连指出了hello进程虚拟内存中的所有段。
在这里插入图片描述

7.4 TLB与四级页表支持下的VA到PA的变换
下图给出了Core i7 MMU 如何使用四级的页表来将虚拟地址翻译成物理地址。36位VPN 被划分成四个9 位的片,每个片被用作到一个页表的偏移量。CR3 寄存器包含Ll页表的物理地址。VPN 1 提供到一个Ll PET 的偏移量,这个PTE 包含L2 页表的基地址。VPN 2 提供到一个L2 PTE 的偏移量,以此类推。

在这里插入图片描述

7.5 三级Cache支持下的物理内存访问
在这里插入图片描述

Core i7的内存系统

在这里插入图片描述

Core i7的地址翻译情况
7.6 hello进程fork时的内存映射
fork函数被进程调用时,内核为新进程创建各种数据结构,并分配给他唯一的PID。为了给这个进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。新进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新的进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行目标文件a.out中的程序,用a.out程序有效地替代了当前程序。加载并运行a.out程序需要以下几个步骤:
1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构;
2)映射私有区域,为新程序的代码、数据、bss、栈区域创建新的区域结构,所有这些区域都是私有的、写时复制的,其中bss段请求二进制0,大小在可执行文件中,栈和堆初始长度为0;
3)映射共享区域,将动态链接的部分映射到共享区域中;
4)设置程序计数器,最后一件事就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
1.缺页故障:DRAM缓存不命中称作缺页。当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。
在这里插入图片描述

2.缺页中断处理:缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,如果该页面被修改过则将其复制回磁盘,接下来内核从磁盘复制新的页,并换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。对于每个进程,内核维护一个brk变量,它指向堆的顶部。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格,都要求应用显式地分配块,它们的不同之处在于由哪个实体来负责释放已分配的块。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。而自动释放未使用的已分配的块的过程叫做垃圾收集。
基本方法:
1隐式空闲链表是空闲块通过头部中的大小字段隐含着连接的,分配器可以通过遍历堆中的所有块简介遍历整个空闲块集合,优点是简单,缺点是比较浪费时间。.
2.显示空闲链表。在空闲块中使用指针连接空闲块。
3.分离的空闲链表.维护多个空闲链表,每个链表的块有大致相等的大小
放置策略:
1.首次适配。从头开始搜索空闲链表,选择第一个合适的空闲块,优点是趋向于将大的空闲块保留在链表的后面,缺点是会在链表的起始处留下许多小空闲块的碎片。
2.下次适配。下次适配和首次适配的方式很相似,只不过是从上一次查询结束的地方开始,而不是从链表的起始处开始搜索。
3.最佳适配。检查每个空闲块,选择适合所需请求大小的空闲块。
7.10本章小结
本章讨论了地址空间和地址的管理方式,以及TLB和三级缓存、多级页表协同工作的内存管理机制,以及对缺页故障的处理,同时也进行了动态内存分配方法的分析。并顺带介绍了程序与内存交互的机制。同时以intel Core7在指定环境下介绍了VA到PA的变换、物理内存访问,还介绍了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
一个Linux文件就是一个m字节的序列:
B0,B1,B2,…,BK,…B(m-1)
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得所有输入和输出都能以一种统一且一致的方式来执行:
打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。
Linux Shell创建的每个进程开始时都有三个打开的文件:标准输入、标准输出、标准错误。
改变当前文件的位置。对于每个打开的文件,内核保持着一个文件位置k、初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显示地设置文件的当前位置为k。
读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。
8.2 简述Unix IO接口及其函数
Unix IO接口:
a.打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。
b.Linux Shell创建的每个进程开始时都有三个打开的文件:标准输入、标准输出、标准错误。
c.改变当前文件的位置。对于每个打开的文件,内核保持着一个文件位置k、初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显示地设置文件的当前位置为k。
d.读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
e.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。
Unix IO函数:
A.int open(char* filename,int flags,mode_t mode) ,进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
B.int close(fd),fd是需要关闭的文件的描述符.成功返回0,出错则返回-1。
C.ssize_t read(int fd,void *buf,size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
D.ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.3.1 vprintf的定义如下图
在这里插入图片描述

8.3.2 在标准库中的实现:
1.typedef char* va_list;
2.#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1) & ~(sizeof(int)-1)) //保证4字节对齐
3.#define va_start(ap, v) (ap = (va_list)&v + _INTSIZEOF(v)) //获取第一个变参在栈中的地址
4.#define va_arg(ap, t) (*(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t))) //获取ap所指向的数据,并把ap偏移至下一个变参的地址
5.#define va_end(ap) (ap = (va_list)0) //使ap指向空,避免野指针
在这里插入图片描述

printf(format, arg1, arg2, arg3);参数在栈中的存放
8.3.3 对_INTSIZEOF(n)分析
栈指针总是4字节对齐的,因此使用_INTSIZEOF(n),使变量的大小是4的倍数(实际变量在栈中占据的空间)。
#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1) & ~(sizeof(int)-1))
对这个宏定义有一个形象的比喻:
  比方说有一个箱子可以装4个瓶子,
  如果我有8个瓶子 ,那么我需要2个箱子;
  如果我有10个瓶子呢,我不能说我需要10除4,需要2.5个箱子吧,实际上我需要3个箱子;
  那怎么求我实际需要的箱子数呢?
   用一个容易理解的公式来求上述问题:
  设我的瓶子数为B,我需要的箱子数为C,一个箱子最多可以装A个瓶子。
公式:C =(B+A-1)/ A (舍去余数)
  因此,((sizeof(n)+sizeof(int)-1) & ~(sizeof(int)-1))相当于C * A。
8.3.4对va_arg(ap, t)分析
#define va_arg(ap, t) ((t )((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
这个宏定义实现了两个功能:
A. ap += _INTSIZEOF(t) —— 求下一个参数的指针
B.(
(t
)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t))) —— 取当前参数值,这个宏定义将返回的就是当前参数值
8.3.5 printf的源代码
在这里插入图片描述

debug_printf.c
在这里插入图片描述

8.4 getchar的实现分析
getchar 的源代码为:

  1. int getchar(void)
  2. {
  3. static char buf[BUFSIZ];
  4. static char *bb = buf;
  5. static int n = 0;
  6. if(n == 0)
  7. {
  8. n = read(0, buf, BUFSIZ);
  9. bb = buf;
  10. }
  11. return(–n >= 0)?(unsigned char) *bb++ : EOF;
  12. }

异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。getchar函数落实到底层调用了系统函数read,通过系统调用read读取存储在键盘缓冲区中的ASCII码直到读到回车符然后返回整个字串,getchar进行封装,大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数的实现。
结论
hello历程:
1.编写。以高级语言的形式编写.c代码
2.预处理。将hello.c所需要的外部的库展开成文本文件。并写入到一个hello.i文件中。
3.编译。将hello.i编译成为机器可以识别的汇编文件hello.s
4.汇编。将hello.s会变成为可重定位目标文件hello.o
5.链接。链接器(ld)负责把 hello.o 和 printf.o 进行合并。最终经过链接阶段可以得到可执行目标文件 hello。
6.运行:在shell中输入./hello 1190201518 杨东晨 1
7.创建子进程:shell进程调用fork为其创建子进程
8.加载:shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数。
9.上下文切换:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流
10.访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
11.动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。
12.信号:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
13.终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有 数据结构。
感悟与创新理念:
Hello程序虽然是每个程序员的开始,但要想真正了解hello程序P2P和O2O过程还是要很多努力的。作为一个合格的程序员,并不是只要会编写程序就行,更加要理解一个程序是如何产生,运行,到结束,以及为他提供支持的操作系统和底层硬件之间的调配。
理解这些对程序质量的提高有着不可忽略的作用。比如用缓存区命中的知识来提高数组的访问效率。

附件
hello.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.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值