程序人生-Hello’s P2P

文章目录


摘要

本文讲述了hello程序的生命周期是从一个高级C语言开始的,为了在系统上运行hello.c程序,需要经过预处理、编译、汇编和链接最终生成可执行目标程序。并且介绍了如何进行hello的进程管理、存储管理和IO管理,让hello.c从一个程序成为一个进程。在探索hello的一生的过程中,更好地理解了计算机系统是如何使用Editor+Cpp+Compiler+AS+LD + OS + CPU/RAM/IO完成一个程序的创建以及一个进程的结束。


关键词

Hello;P2P;020;预处理;编译;汇编;链接;信号;shell;虚拟内存;地址翻译;页表;IO;

第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:先使用高级语言写出合法的.c文件,例如hello.c,然后经过cpp预处理hello.c文件,预处理就是将#include申明中中头文件加入hello.c中并且对宏进行替换,完成这些生成对应的hello.i文件,然后hello.i通过ccl的编译生成hello.s的汇编语言程序,其中的高级语言被编译器转化为了汇编语言,然后汇编语言程序hello.s通过汇编器as将其翻译汇编为机器语言可重定位目标程序hello.o,hello.o在通过ld与其他的.o文件进行链接并进行重定位等操作得到最终的可执行目标程序(二进制)。要运行可执行目标程序需要在shell中输入./文件名,shell会为其fork一个子进程来执行该文件,这时hello就从一个程序(program)变成了一个进程(process)。
020:一开始的可执行目标程序是不在物理内存中的,要想执行该程序需要在shell通过execve加载并执行该程序后,先为该程序分配一部分虚拟空间,然后需要引用该虚拟空间中的程序,又因为该程序还不在物理内存中,所以触发了缺页,此时操作系统会将该程序加载到物理内存中,然后再次执行该程序。执行该程序后进入main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。在程序结束后shell就会将hello进程回收,并且从内核中删除相关的数据结构。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位
开发与调试工具:gcc,vim,edb,readelf,HexEdit,codeblocks,valgrind

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名称 文件作用
hello.c 高级语言编写的c源码
hello.i hello.c预处理之后的文本文件
hello.s hello.i编译后的汇编文件
hello.o hello.s汇编之后的可重定位目标文件
hello hello.o与其他可重定位目标文件链接之后的可执行目标文件
hello.out hello反汇编之后的可重定位文件

1.4 本章小结

本章大致主要简单介绍了 hello 的 p2p、020 、如何优化源程序、计算机系统层次模型、计算机系统的抽象表示,列出了本次实验信息:环境、中间结果,并且大致简介了hello程序从c程序hello.c到可执行目标文件hello的大致经过的历程。

第2章 预处理

2.1 预处理的概念与作用

预处理概念:预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。
预处理阶段作用:
1.处理宏定义指令预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外,通常把排除在外的语句转换成空行。
2. 处理条件编译指令
条件编译指令如#ifdef,#ifndef,#else,#elif,#endif等。 这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
3.处理头文件包含指令头文件包含指令如#include "FileName"或者#include 等。 该指令将头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
4.处理特殊符号
预编译程序可以识别一些特殊的符号。 例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。

2.2在Ubuntu下预处理的命令

在这里插入图片描述

linux shell下的命令:gcc hello.c -E -o hello.i

2.3 Hello的预处理结果解析

在这里插入图片描述
经过cpp预处理后,可以看到hello.i中不仅包括hello.c主函数中的c代码,还包括源文件中的宏,并对其进行了展开,头文件中的声明函数、结构体、变量、宏等内容也被加入到了hello.i中。

2.4 本章小结

这一章介绍了预处理的概念:修改原始的C程序(hello.c),将引用的头文件中的所有内容展开和合并为一个完整的文本文件,该文本文件仍然是C语言风格的。
还介绍了预处理的作用:处理条件编译指令、宏定义、头文件、一些特殊的符号。
还给出了一个用cpp进行预处理的范例,并查看了cpp预处理后的文件中的内容,将hello.o和hello.i对比能够更好的理解预处理的概念以及作用。

第3章 编译

3.1 编译的概念与作用

编译的概念:编译器将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。其以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。 这个过程称为编译,同时也是编译的作用。
编译程序的基本功能是把源程序(高级语言)翻译成目标程序。除了基本功能之外,编译程序还具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用以及人际联系等重要功能。

3.2 在Ubuntu下编译的命令

linux shell下的命令:gcc -S hello.i -o hello.s
在这里插入图片描述

3.3 Hello的编译结果解析

3.3.0 表头信息

在这里插入图片描述

.file:声明源文件
.text:代码节
.section:
.rodata:只读代码段
.align:数据或者指令的地址对其方式
.string:声明一个字符串(.LC0,.LC1)
.global:声明全局变量(main)
.type:声明一个符号是数据类型还是函数类型

3.3.1 数据

1.常量
在这里插入图片描述
在代码的头部申明中说明了该程序有两个常量,两个string常量如上图所示,且能够看出这两个常量都是在只读数据段中的,观察hello.c源代码可以看出这两个字符串常量都是printf函数的参数。
2.局部变量
观察汇编代码可以看到在main函数中将局部变量放到了栈中,如图所示
在这里插入图片描述
在这里插入图片描述

都是在栈中通过rbp对存储的局部变量进行操作。
3.立即数
在hello.s中所有的立即数都直接在汇编代码中出现,用$+数表示(该数可能是16进制也可能是10进制,如果是16进制需要在数字前再加上0x)。
4.数组
在hello.c中main函数的第二个参数是一个字符串数组char *argv[],在hello.s汇编代码中可以看出该字符串数组是存储在栈中的,并在之后作为参数传递给printf函数。
在这里插入图片描述

3.3.2 全局函数

在hello.c中全局函数只有一个,那就是主函数main,这个可以在表头声明中看到。函数的参数有两个一个是int argc,一个是char *argv[],两者都存在栈中。

3.3.3 赋值

程序中的赋值主要是寄存器和内存所存的数据通过movb、movw、movl、movq等mov指令来完成的,主要区别就是移动的数的字节大小不同使用不同后缀的mov指令。例如程序中的“=”就是通过将等于号右边数据所存的寄存器或者内存的数据放到等号左边数据所存的寄存器或者内存中去,完成一次赋值操作。

3.3.4 算数操作

hello.c中出现的算数操作是循环时的i++,对于这个操作hello.s使用的汇编指令是addl $1, -4(%rbp),i是存储在栈中即-4(%rbp)中存的值就是i的值。因为i是int类型的,即i是四个字节大小的,故使用的是addl指令。对于其他算数操作的指令如下图
在这里插入图片描述
在这里插入图片描述

3.3.5 关系操作

在hello.c中有两个关系操作,一个是argc!=4,一个是i<8.
首先是argc!=3,hello.s中是使用cmpl $4, -20(%rbp)来进行比较,cmpl的作用是通过两个比较数的大小来设置条件码寄存器OF,DF,IF,TF,SF,ZF,AF,PF,CF等。反映cpu执行的算术和逻辑操作结果的状态标志。
然后是i<8,hello.s是使用cmpl $7, -4(%rbp)来比较i和7的大小来设置条件码。

3.3.6 控制转移

控制转移指令与关系操作的指令是相辅相成的,只有在关系操作设置了条件码之后,才能够通过控制转移指令跳转到相应的位置(但是jmp指令不需要条件码相当于c语言中的goto语句)。
在hello.c中伴随cmpl也有两个控制转移指令,分别是如下图所示
cmpl $4, -20(%rbp)+je .L2是完成argc!=4的判断的,而cmpl $7, -4(%rbp)+jle .L4是完成i<8的判断的。

3.3.7函数操作

在汇编中调用函数一般使用call指令,call指令的作用是将该指令的下一条指令的地址压入栈中然后令rip指向call的那个指令。而传递函数的参数有时是使用寄存器(第一个参数在%rdi中,第二个参数在%rsi中,第三个参数在%rdx中,第四个参数在%rcx中,第五个参数在%r8中,第六个参数在%r9中)其余参数用栈来存,或者直接就是用栈来保存所有的函数参数,而函数的返回值一般都是放在%rax中,当函数返回后如果向使用函数的返回值一般就是使用%rax。并且在调用函数的时候可能需要为这个函数分配它局部变量的空间,所以需要使用%rbp寄存器来存储它的栈帧,并且在返回前要释放这些空间。
在hello.c中的函数有main、printf、exit、atoi、sleep、getchar。
main函数的参数分别是int argc和char *argc[]
printf函数的参数是两个字符串常量
exit的参数是1
atoi的参数是argv[3]
sleep的参数是atoi(argv[3])
所有函数的返回值都在%eax中。

3.3.8 类型转换

hello.c中有一个类型转换atoi(argv[3]),该类型转换是将字符串转化为整型,通过调用头文件中定义的atoi函数完成这部分功能。

3.4 本章小结

本章主要讲述了编译阶段中编译器如何处理各种数据和操作,以及c语言中各种类型和操作所对应的的汇编代码。通过理解了这些编译器编译的机制,我们可以很容易的将汇编语言翻译成c语言。

第4章 汇编

4.1 汇编的概念与作用

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

4.2 在Ubuntu下汇编的命令

linux shell下的命令:gcc -S hello.i -o hello.s
在这里插入图片描述

4.3 可重定位目标elf格式

readelf <option(s)> elf-file(s)
-a –all 等同于同时使用:-h -l -S -s -r -d -V -A -I
-h --file-header 显示ELF文件头
-l --program-headers 显示程序头(执行文件才有)-W 宽显
-S --section-headers 显示节头
-t --section-details 显示节详细信息
-s –syms 显示符号表(symbol table)
-r –relocs 显示重定位信息
-d –dynamic 显示动态节(dynamic section)
-x --hex-dump=<number|name> readelf -x.data xxxx.o
以字节形式显示输出<number|name>指定节的内容
-p --string-dump=<number|name>
以字符串形式显示输出<number|name>指定节的内容
-R --relocated-dump=<number|name>
以重定位后的字节形式显示输出<number|name>指定节内容

查看ELF头:readelf -h hello.o,如下图所示。
ELF头:以16B的序列 Magic开始,Magic描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解 ## 标题释目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大 小和数量等信息。
从图中可以看到我们的hello.o是小端序,且类型是REL(可重定位文件),架构是x86-64。
在这里插入图片描述

查看节头命令:readelf -S hello.o
节头部表:包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。由于是可重定位目标文件,所以每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,同时可以观察到,代码是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。如下图所示。
在这里插入图片描述

查看符号表.symtab命令:readelf -s hello.o
.symtab: 存放程序中定义和引用的函数和全局变量的信息。name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。如下图所示
在这里插入图片描述
查看重定位节信息:readelf -r hello.o
重定位节:一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
重定位节.rela.text中各项符号的信息:

Offset:需要被修改的引用节的偏移Info:包括symbol和type两个部分,symbol在前面四个字节,type在后面四个字节,
symbol:标识被修改引用应该指向的符号,
type:重定位的类型
Type:告知链接器应该如何修改新的应用
Attend:一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整Name:重定向到的目标的名称。
hello.o的重定位节如下图所示:
在这里插入图片描述

4.4 Hello.o的结果解析

objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
hello.o的反汇编和hello.s如下图所示
在这里插入图片描述
在这里插入图片描述

通过对照hello.o的反汇编和hello.s,可以看出两者的差别不是很大,主要的差别就是hello.o的反汇编代码中立即数都是以16进制数表示的,相反在hello.s中立即数都是10进制表示的(这里可以看出机器语言中的操作数和汇编语言中的不一致:机器语言中的操作数都是16进制的,而汇编语言中的操作数可以是不同的数制,在将汇编翻译为机器语言时需要将这些操作数都转化为16进制,还有一些不一致是出现在),而且在hello.o的反汇编中不仅仅有汇编语言还有汇编语言对应的机器语言和一些重定位信息。机器语言程序的是二进制机器指令的集合,是纯粹的二进制数据表示的语言,是电脑可以真正识别的语言。这些机器语言(二进制编码)和所有的汇编语言有着一一对应的关系,所以可以根据汇编语言得到对应的机器语言也可以根据机器语言得到对应的汇编语言。

分支转移:在汇编语言中分支转移的汇编指令是jmp、jl、jg、jle、jge、ja、jb、jae、jbe等这些跳转指令,这些跳转指令的操作数一般是段名称或者跳转位置与目前位置的相对偏移量(又称为相对寻址),在hello.o的反汇编中可以看到其中对汇编代码进行了分段:L1,L2,L3,然后就可以使用如jmp .L3这样的汇编指令跳转到.L3段的起始位置运行。

函数调用:函数调用的汇编指令是call,后面跟的要么是一个函数名,要么是一个相对偏移量,在hello.s中call后跟着的是函数名,而在hello.o的反汇编中call后跟的就是相对偏移量,但是在hello.o的反汇编机器语言中显示的都是全0,因为在hello.c中调用的都是共享库中的函数,要最终使用链接器链接重定位(在rela.text节中的重定位条目)才能够计算出这些函数调用操作数的重定位值。

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

4.5 本章小结

本章对hello.s进行了汇编,生成了hello.o可重定位目标文件,并且分析了可重定位文件的ELF头、节头部表、符号表和可重定位节,比较了hello.s和hello.o反汇编代码的不同之处,分析了从汇编语言到机器语言的一一映射关系。

第5章 链接

5.1 链接的概念与作用

链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。链接是由叫做链接器的程序执行的。链接器使得分离编译成为可能。

5.2 在Ubuntu下链接的命令

链接hello.o:
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.o之后生成的hello反汇编代码如下:
在这里插入图片描述

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

首先看ELF头的信息:通过ELF头能够看出hello和hello.o的不同点在于hello的类型是EXEC(可执行文件),而hello.o的类型是REL(可重定位文件),且hello的入口点地址、程序头起点、程序头部的大小和数量、节头部的数量都 与hello.o不同。
在这里插入图片描述

然后看节头的信息:Section Headers 对 hello中所有的节信息进行了声明,其 中包括大小 Size 以及在程序中的偏移量 Offset,因此根据 Section Headers 中的信息我们就可以用 HexEdit 定位各个节所占的区间(起始位置,大小)。其中 Address 是程序被载入到虚拟地址的起始地址。分析节头的信息可以看出hello的节头和hello.o的节头也是有一些不一样的,一些主要的不一样就是hello的节头没有.bss段和.rela.text段,hello.o中的节头就有,但是hello的节头有很多hello.o没有的段。
在这里插入图片描述
最后是符号表.dynsym:从符号表中可以看到hello中所有的符号条目,在下图中可以看到所有符号(包括全局符号、外部符号、局部符号)的偏移量、类型、所存的段(.data,.text等)。
在这里插入图片描述

5.4 hello的虚拟地址空间

根据hello中记录的.rodata相对虚拟地址起始地址的偏移量能够算出其虚拟地址为0x402000大小为0x3b个字节,进入EDB中查看相应位置的数据可以得到如下图
可以看出存储的刚好是程序中的两个字符串常量。
在这里插入图片描述
还可以从节头信息中看出hello的.text的起始位置是0x4010f0大小为0x145个字节,从EDG中查看0x4010f0出所存的数据,可以看出刚好是hello的代码段的二进制机器语言代码。
在这里插入图片描述在这里插入图片描述

5.5 链接的重定位过程分析

(以下格式自行编排,编辑时删除)
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
上图为hello反汇编后的main函数汇编代码,从这段汇编代码中可以看到
hello.o中的je,call,jmp后面跟的操作数是全0,而hello中是已经计算出来的相应段或函数的地址。根据这个不同可以分析出hello.o链接成为hello的过程中需要对重定位条目进行重定位,对相应的条目进行计算得到地址。并且再hello的反汇编代码中除了main函数以外还有很多其他的函数,例如puts,printf,getchar,atoi,exit,sleep,_start等函数的汇编代码,从这个不同可以分析出链接会将共享库中函数的汇编代码加入hello.o中。

hello重定位的过程:
(1)重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。

(2)重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。

(3)重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目。代码的重定位条目放在.rela.txt

重定位算法:

foreach section s
{
	foreach relocation entry r
	{
		refptr = s + r.offset;/*ptr to reference to be relocated*/
		
		/*Relocate a PC-relative reference*/
		if(r.type == R_X86_64_PC32){//PC相对寻址的引用
			refaddr = ADDR(s) + r.offset;/*ref's run-time address*/
			*refptr = (unsigned) (ADDR(r.symbol) + r.addend - refaddr);
		}
		
		/*Relocate an absolute reference*/
		if( r.type == R_X86_64_32)//使用32位绝对地址
			*refptr = (unsigned)(ADDR(r.symbol) + r.addend); 
	}
}

例如exit()相对地址引用定义exit的重定位条目为r,则
通过图5.5.6的公式可知:
ADDR(s)=0x4010f0refaddr=ADDR(s) +r.offset= (unsigned)(0xffffff7c),查看反汇编中的机器语言可以看出计算正确。
在这里插入图片描述

5.6 hello的执行流程

函数名 函数地址
ld-2.31.so!_dl_start 0x7fce8cc38ea0
ld-2.31.so!_dl_init 0x7fce8cc47630
hello!_start 0x4010f0
libc-2.31.so!_libc_start_main 0x7fce8c867ab0
-libc-2.31.so!_cxa_atexit 0x7fce8c889430
-libc-2.31.so!_libc_csu_init 0x401260
libc-2.31.so!_setjmp 0x7fce8c884c10
libc-2.31.so!exit 0x7fce8c889128

5.7 Hello的动态链接分析

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。

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

延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:

PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。

GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
查看hello的ELF可以看到GOT起始表位置为0x404000
在这里插入图片描述
进入EDB中查看0x404000中的数据如下图
在这里插入图片描述

可以看出GOT表位置在调用dl_init之前0x404008后的16个字节均为0.
调用_start之后再次查看GOT表中的内容可以看到如下图:
在这里插入图片描述

从中可以看出在调用_start之后0x404008后的16个字节分别为0x7f10afe06190和0x7f10afdefbb0,其中GOT[0](0x403e50)和GOT1包含了动态链接器在解析函数地址时会使用的信息。GOT[2](0x7f10afdefbb4)是动态链接器在ld-linux,so模式中的入口点。其余每个条目都对应一个被调用的函数。下图是0x7f10afdefbb4作为共享库模块的入口点。
在这里插入图片描述

5.8 本章小结

在本章中主要介绍了链接的概念与作用,并且详细阐述了hello.o是怎么链接成为一个可执行目标文件的过程,详细介绍了hello.o的ELF格式和各个节的含义,并且分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。

第6章 hello进程管理

6.1 进程的概念与作用

进程是一个执行中的程序的实例,系统中的每个程序都运行在某个进程上下文中。上下文是由程序正确运行所需的状态组成的。存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

每次用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或者其他应用程序。

作用:进程提供给应用程序的关键抽象:
(1) 一个独立的逻辑控制流,我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。

(2) 一个私有的地址空间,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

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

shell:shell是一个用c语言编写的程序,它是用户使用Linux的桥梁,shell既是一种命令语言,又是一种程序设计语言,shell是一种应用程序。

功能:shell应用程序提供了一个界面,用户通过访问这个界面访问操作系统内 核的服务。

处理流程:
1)从终端读入输入的命令。
2)将输入字符串切分获得所有的参数
3)如果是内置命令则立即执行
4)否则调用相应的程序执行
5)shell 应该接受键盘输入信号,并对这些信号进行相应处理

6.3 Hello的fork进程创建过程

父进程(在这里指的是shell)通过调用fork函数创建一个新的运行的子进程。
新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们不同的PID。

fork函数只被调用一次但是会返回两次,一次在调用进程(父进程)中,一次是在新创建的子进程中。
在shell中输入./hello 1190300611 杨宇辰 1 ,之后shell会识别到该命令并不是内置命令,因此shell会调用fork函数创建一个新的子进程,然后在该子进程中运行该可执行二进制目标程序。
在这里插入图片描述

6.4 Hello的execve过程

execve函数在当前进程的上下文中加载并运行一个新程序。
execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量envp。具体数据结构如下图所示:
在这里插入图片描述

argv变量指向一个以null结尾的指针数据,其中每个指针都指向一个参数字符串,按照惯例,argv[0]是可执行目标文件的名字。环境变量的列表是由一个类似的数据结构表示的envp也指向一个以null结尾的指针数组,其中每个指针都指向一个环境变量字符串,每个串都是形如“name=value”的名字-值对。
只有当出现错误时,例如找不到filename,execve才会返回到调用程序,所以execve一般是调用一次并不返回的。
在execve加载了filename之后,它调用启动代码,启动代码设置栈,并将控制传递给新程序的主函数。

6.5 Hello的进程执行

时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户模式和内核模式::处理器通常使用一个寄存器提供两种模式的区分,该寄 存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的 代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任 何命令,并且可以访问系统中的任何内存位置。

上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。

上下文切换:当内核选择一个新的进程运行时,则内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程:
1) 保存以前进程的上下文
2)恢复新恢复进程被保存的上下文,
3)将控制传递给这 个新恢复的进程 ,来完成上下文切换。

在我们的hello程序被shell调用execve函数加载并运行之后,进程就已经为hello程序分配好了新得虚拟地址空间,并且将代码段.text和数据段.data都设置成了hello fork之前的代码段和数据段。一开始运行hello的时候进入主函数先输出hello 1190300611 杨宇辰,然后系统调用sleep函数,显式地请求让调用进程休眠,此时内核不会因为要等待而什么都不做,而会处理休眠请求主动释放当前进程,并将hello进程从运行队列中移到等待队列,然后计时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时的时候发送一个中断信号,然后内核就会执行中断处理,将hello进程从等待队列中重新移回运行队列,然后重复循环,直到main函数中对应的for循环结束。用户态和内核态的转换如下图所示:
在这里插入图片描述

当循环结束之后就是调用getchar函数,该函数在执行输入流是stdin的系统调用read,hello之前运行在用户模式,在进行read调用之后陷入内核,内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。此时进入内核模式,内核执行上下文切换,切换到其他进程。当完成键盘缓冲区到内存的数据传输时,引发一个中断信号,此时内核从其他进程进行上下文切换回hello进程。
在这里插入图片描述

6.6 hello的异常与信号处理

(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

hello执行过程中出现的异常:
中断:异步发生的,在hello程序执行的过程中可能会出现外部I/O设备引起的异常。
陷阱:陷阱是有意的异常,是执行一条指令的结果,在hello的执行过程中在执行sleep函数时会出现这个异常。
故障:故障由错误情况引起,它可能能够被故障处理程序修正。在执行hello程序的时候可能会发生缺页故障。
终止:终止是不可修复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者SRAM位被损坏时发生的求错误。
下面是信号种类:

编号信号名称缺省动作说明
1SIGHUP终止终止控制终端或进程
2SIGINT终止键盘产生的中断(Ctrl-C)
3SIGQUITdump键盘产生的退出
4SIGILLdump非法指令
5SIGTRAPdumpdebug中断
6SIGABRT/SIGIOTdump异常中止
7SIGBUS/SIGEMTdump总线异常/EMT指令
8SIGFPEdump浮点运算溢出
9SIGKILL终止强制进程终止
10SIGUSR1终止用户信号,进程可自定义用途
11SIGSEGVdump非法内存地址引用
12SIGUSR2终止用户信号,进程可自定义用途
13SIGPIPE终止向某个没有读取的管道中写入数据
14SIGALRM终止时钟中断(闹钟)
15SIGTERM终止进程终止
16SIGSTKFLT终止协处理器栈错误
17SIGCHLD忽略子进程退出或中断
18SIGCONT继续如进程停止状态则开始运行
19SIGSTOP停止停止进程运行
20SIGSTP停止键盘产生的停止
21SIGTTIN停止后台进程请求输入
22SIGTTOU停止后台进程请求输出
23SIGURG忽略socket发生紧急情况
24SIGXCPUdumpCPU时间限制被打破
25SIGXFSZdump文件大小限制被打破
26SIGVTALRM终止虚拟定时时钟
27SIGPROF终止profile timer clock
28SIGWINCH忽略窗口尺寸调整
29SIGIO/SIGPOLL终止I/O可用
30SIGPWR终止电源异常
31SIGSYS/SYSUNUSEDdump系统调用异常

键盘上各种操作导致的异常:
随意输入一些字符:
在这里插入图片描述

按下ctrl-z的结果:
在这里插入图片描述

由于输入ctrl-z的结果时挂起前台作业,所以hello进程只是停止被没有被回收,此时hello进程运行在后台下。使用ps命令可以看到如下图:
在这里插入图片描述

此时hello进程在后台的PID时39236,其后台job号时1,调用fg 1将其调到前台,此时shell继续执行hello进程打印Hello 1190300611 杨宇辰,输入任意字符后进程结束并被回收。
在这里插入图片描述
在这里插入图片描述

输入ctrl-c的结果:
在这里插入图片描述

由于输入ctrl-c是向当前前台进程组发送一个SIGINT信号,该信号的默认信号处理程序是终止前台作业。故使用ps查看前台进程组发现没有hello进程
在这里插入图片描述

当在程序运行过程(在输出Hello 1190300611 杨宇辰时)中不停乱按键盘:
在这里插入图片描述

可以看到键盘的输入只是将屏幕的输入缓存到stdin中,当getchar的时候读出一个’\n’结尾的字符串,其它字符串会被当作shell的命令行输入。

6.7本章小结

在本章中,阐述进程的定义与作用,同时介绍了 Shell 的一般处理流程和作用,并且着重分析了调用 fork 创建新进程,调用 execve函数 执行 hello,hello的进程执行,以及hello 的异常与信号处理。

第7章 hello的存储管理

7.1 hello的存储器地址空间

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:是指由程序产生的和段相关的偏移地址部分,程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。

线性地址:是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。

虚拟地址:也就是线性地址。

物理地址:是指出目前CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。

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

一个逻辑地址由两部分组成:段标识符:段内偏移量。段标识符由一个16位长的字段组成,成为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,如图:
在这里插入图片描述

索引号,又可以理解为数组下标,段描述符具体地址描述了一个段。很多个段描述符组成了一个数组,叫“段描述符表”,可以通过段标识符的前13位(即索引号)直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,由8个字节组成。如下图。
在这里插入图片描述

在这里的Base字段描述了一个短的开始位置的线性地址。Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。那究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT。 GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。如下图
在这里插入图片描述

首先,给定一个完整的逻辑地址[段选择符:段内偏移地址]
1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。

2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。

3、把Base + offset,就是要转换的线性地址了。 还是挺简单的,对于软件来讲,原则上就需要把硬件转换所需的信息准备好,就可以让硬件来完成这个转换了。

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

概念上而言,虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每个字节都有一个唯一的虚拟地址,作为到数组的索引。磁盘上数组的内容被缓存在主存中。和存储器层次结构中其他缓存一样,磁盘上的数据被分割为块,这些块作为磁盘和主存之间的传输单元。VM系统通过将虚拟内存分隔为成为虚拟页的大小的固定的块来处理这个问题。每个虚拟页的大小为P=2^p字节。类似地,物理内存被分割为物理页,大小也为P字节(物理页被称为页帧)。

在任意时刻,虚拟页面的集合都被分为三个不相交的子集:未分配的、缓存的、未缓存的。未分配的就是在虚拟内存中和物理内存中都没有被分配,不占用任何磁盘空间,未缓存的是在虚拟内存中已经分配但是未缓存在主存中,已缓存的是即在虚拟页中被分配,也在物理页中被分配。

因为系统需要判定一个虚拟页是否缓存在DRAM中的某个地方。如果命中,那么需要确定这个虚拟页存放在哪个物理页中,如果不命中,系统必须判断这个虚拟页存放在磁盘的哪个位置,在物理内存中选择一个牺牲页,并将虚拟页从磁盘复制到DRAM中,替换这个牺牲页。这种功能就需要操作系统软件、MMU中的地址翻译硬件和一个存放在物理内存中叫做页表的数据结构,页表将虚拟页映射到物理页。每次地址翻译讲一个线性地址转换为物理地址时,都会读取页表。

如下图所示是页表的基本组织结构。页表就是一个页表条目的数组,每个页在页表中一个固定偏移量出都有一个PTE。每个PTE由一个有效位和一个n位地址字段组成。
在这里插入图片描述

从虚拟地址到物理地址的转换就依赖于页表,从虚拟地址到物理地址的转换过程:
n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,例如VPN 0选择PTE 0。根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。这里的VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。
在这里插入图片描述

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

Intel Core i7实现支持48位虚拟地址空间和52位物理地址空间,采用的是司机也表层测结构,每个进程都有它自己私有的页表层次结构。下图为Core i7从VA到PA变换的概况
在这里插入图片描述

其中VA是48位的,低12位为虚拟页号,高36位被分成四个9位的四级页表索引。在TLB与四级页表支持下的VA到PA变换过程:首先通过VPN得到TLBT和TLBI,若TLB命中,则直接取出TLB中的PPN加上VA中的VPO构成物理地址,用此物理地址对cache进行查找。若TLB不命中,则需要根据VPN中所含的四个9位的四级页表索引,首先先找到该进程常驻内存的一级页表,根据VPN1作为索引得到对应的条目,根据条目指向的下一级页表以及下一级页表索引VPN2得到指向第三级页表的条目,再根据VPN3和三级页表得到四级页表然后就能够在第四级页表中根据VPN4(页表索引)得到PPN,然后用得到的PPN和VPO组成物理地址进行操作。图示如下:
在这里插入图片描述

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

三级Cache中的原理都是一样的,所以在这里只对一级Cache的物理内存访问进行说明。
根据7.4中从VA到PA的变换得到对应的物理地址后,就可以根据物理地址得到CT(标记),CI(组索引),CO(段偏移),然后根据得到的CI到L1 d-cache中去找到相应的组,看该组下的各路是否有标记和CT相同的路,若有则取出该路下段偏移位CO的数据返回给CPU,若没有则cache不命中,需要向下一级存储器层次结构中取出被请求的块,然后将新的块存储在组索引位所指示的组中的一个高速缓存行中。图示如下:
在这里插入图片描述

7.6 hello进程fork时的内存映射

当 fork 函数被 shell 进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只 读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行包含在可执行目标文件a.out中的程序,用a.out程序有效的替代了当前程序。加载并运行a.out需要以下几个步骤:
·删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
·映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
·映射共享区域, hello程序与共享对象 libc.so链接,libc.so是动态链 接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
·设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
下图图示解释了加载器是如何映射用户地址空间的区域的
在这里插入图片描述

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

缺页故障:当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。通过查询页表PTE可以知道虚拟页在磁盘的位置。缺页处理程序从指定的位置加载页面 到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中,因此指令可以没有故障的运行完成。

7.9动态存储分配管理

动态内存分配器维护着一个进程的的虚拟内存区域,成为堆。对于每个进程,内核都维护一个变量brk,它指向堆的顶部。
分配器将堆视为一组不同大小的块的集合来维护。每个块要么是已分配的要么是空闲的。已分配的块显式地保留为供应用程序使用,空闲块可用来分配,在这里介绍两种空闲链表:显式空闲链表和隐式空闲链表。下图分别是隐式空闲链表和显式空闲链表
隐式空闲链表
隐式空闲链表

(带边界标记的合并时会在每个块增加一个脚部,存储着跟头部一样的信息,便于对上一个块信息的读取)
在这里插入图片描述

(对于显式空闲链表中只放置空闲的链表,并且可以维护多个空闲链表,将大小相近的块放到同一个空闲链表中,这样可以减少分配时间)
分配器也有两种风格:
·显式分配器:要求应用显式地释放任何已分配的块,如c标准库中提供的一种叫做malloc程序包的显式分配器。
·隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么释放这个块。

下面主要介绍隐式空闲链表的初始化、释放、合并、分配。
·初始化:mm_init函数,从内存系统中得到四个字,并将它们初始化(为了8字节对齐),然后创建一个序言块和一个结尾块并创建一个空的空闲链表,将堆扩展4kb个字节,并且创建初始的空闲块。
·释放:根据需要释放块的指针得到该块的块大小以及头部和尾部的地址,然后将头部和尾部的标记位修改为0,然后再合并空闲块。

·合并:合并分为四种情况:前面的块和后面的块都是已分配的;前面的块是已分配的,后面的块是空闲的;前面的块是空闲的,后面的块是已分配的;前面的块和后面的块都是空闲的。若两个邻接的块都是已分配的,那么不需要任何操作,直接返回该块的载荷首地址即可;若前面的块已分配后面的块未分配,那么需要将该块的头部和后面的块的尾部中块大小的信息修改为这两个块块大小之和,并返回该块的载荷首地址;若前面的块未分配后面的块已分配,那么需要将该块的尾部和前面的块的头部中块大小修改为两块块大小之和,并返回前面那个块的载荷首地址;若前后两个块都是未分配的,那么就将前面的块的头部和后面的块的尾部修改为三个块块大小之和,然后返回前面那个块的载荷首地址。

·分配:先从内存中请求大小为size字节的块,然后调整请求块的大小,使其有为头部和尾部留有空间并且满足8字节对齐的要求。一旦分配器完成了以上的操作,就会搜索空闲链表寻找一个合适的空闲块(这个寻找分为两种:首次适配和最佳适配)。如果有合适的那么分配器就放置这个请求快,并分割出多余的部分(这个多余的部分必须大于等于最小对齐块的大小,否则就变成内部碎片)。如果分配器不能够发现一个匹配的块,那么就用一个新的空闲块来扩展堆,把请求块放置在这个新的空闲块里,可选地分割这个块,然后返回一个指针,指向这个新分配的块。

7.10本章小结

本章主要介绍了hello的存储器的地址空间,介绍了四种地址空间的差别和地址的相互转换。同时介绍了hello的四级页表的虚拟地址空间到物理地址的转换。阐述了三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

一个Linux文件就是一个m字节的序列:
B0,B1,B2……Bm

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

设备的模型化:文件
设备管理:unix io接口

8.2 简述Unix IO接口及其函数

Unix I/O接口:
(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
(2)Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。(3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
(4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
(5)关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

Unix I/O函数:
(1)int open(char* filename,int flags,mode_t mode),进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
(2)int close(fd),fd是需要关闭的文件的描述符,close返回操作结果。
(3)ssize_t read(int fd,void* buf,size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
4)ssize_t wirte(int fd,const void *buf,size_tn),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。

8.3 printf的实现分析

printf函数的c源码:

static int printf(const char *fmt, ...) 
{  
	    va_list args;  
	    int i;  
	    va_start(args, fmt);  
	    write(1,printbuf,i=vsprintf(printbuf, fmt, args));  
	    va_end(args);  
	    return i;  
}

write函数的汇编代码:

write: 
mov eax, _NR_write 
mov ebx, [esp + 4] 
mov ecx, [esp + 8] 
int INT_VECTOR_SYS_CALL

在printf中调用系统函数write(buf,i)将长度为i的buf输出,在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将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。
显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
于是打印字符串就显示在了屏幕上。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar的c源码为:

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;  
} 

异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成 ASCII 码,保存到系统的键盘缓冲区之中。
getchar 函数落实到底层调用了系统函数 read,通过系统调用 read 读取存储在 键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar 进行封装, 大体逻辑是读取字符串的第一个字符然后返回。

8.5本章小结

本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数的实现。

结论

hello.c:由计算机提供的编辑器编辑出高级C语言代码hello.c,从此一个程序就诞生了。
hello.i:由hello.c经过预处理器预处理得到,将以字符#开头的命令,修改原始的C程序。
hello.s:由hello.i经过编译器编译得到,编译器将文本文件hello.i翻译为汇编程序hello.s,hello.s中是汇编语言。
hello.o:由hello.s经过汇编器汇编得到,汇编器将hello.s中的汇编代码翻译成对应的机器语言指令,并将这些指令打包成一种可重定位目标程序的格式。

hello:由hello.o经过链接器链接得到,将可重定位目标程序中的可重定位代码以及数据重定位并且可以将多个可重定位程序合并,得到一个可执行目标文件。

在shell中运行:输入:”./hello 1190300611 杨宇辰 1”.

进程管理:由于输入的命令行命令并不是内置命令,故shell调用fork函数创建一个子进程,调用execve函数调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存在该子进程中加载并执行该命令。并且负责在该进程结束后回收该子进程。

上下文:操作系统保持跟踪进程运行所需的所有状态信息,这个状态就是上下文,包括PC、寄存器文件的当前值、主存的内容。当操作系统决定要把控制权从当前进程转移到某个新进程时就会进行上下文切换,即保存当前进程的上下文、回复新进程的上下文。hello调用sleep函数之后进程陷入内核模式,处理休眠请求主动释放当前进程,内核进行上下文切换将当前进程的控制权交给其他进程,当sleep函数调用完成时,内核执行上下文切换将控制传递给当前进程。

内存系统:先根据虚拟地址得到VPN,然后根据VPN中的TLBT和TLBI访问TLB,若命中,则取出对应PPN,若不命中,则去存在主存中的页表中查看对应的条目是否是已分配的,若已分配则取出对应的PPN,否则触发缺页异常。得到PPN之后根据物理地址得到CT、CI、CO访问Cache,若不命中则去下一级存储器结构中取值。
动态申请内存:当hello程序执行printf函数是, 会调用 malloc 向动态内存分配器申请堆中的内存。

计算机系统的设计思想和实现都是基于抽象实现的。从最底层的信息的表示用二进制表示抽象开始,到实现操作系统管理硬件的抽象:进程是对处理器、主存和I/O设备的抽象。虚拟内存是对主存和磁盘设备的抽象。文件是对I/O设备的抽象。

计算机系统的设计精巧:为了解决快的设备存储小、存储大的设备慢的不平衡,设计了高速缓存来作为更底层的存储设备的缓存,大大提高了CPU访问主存的速度!!!

计算机系统的设计考虑全面:计算机系统设计考虑一切可能的实际情况,设计出一系列的满足不同情况的策略。比如写回和直写,写分配和非写分配,直接映射高速缓存和组相连高速缓存等等。

附件

hello.c:由编辑器编辑的高级C语言程序,是一个hello程序生命周期的开始。
hello.i:hello.c经过预处理后得到的修改了的源程序。
hello.s:hello.i经过编译后的汇编程序。
hello.o:hello.s经过汇编之后的可重定位目标程序。
hello:hello.o经过链接之后的可执行目标程序。

参考文献

[1] https://www.cnblogs.com/whwywzhj/p/6790914.html
[2] Randal E. Bryant, and David R. O’Hallaron. Computer Systems: A Programmer’s Perspective(3rd Edition),2016.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sg62198458

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值