hello的一生——计算机系统大作业

计算机系统大作业——hello的一生
在这里插入图片描述

摘 要
本文主要介绍了hello程序从编写到执行到介绍的全过程,主要包括hello程序的预处理、编译、汇编、链接、进程管理、存储管理、I/O管理等部分的具体操作和结果,并最终得出系统性结论,并且通过这一系列的操作,熟悉了linux系统的运作,熟练的将计算机系统各个组成部分的工作有机地结合统一起来,有利于搭建计算机系统知识体系、融会贯通计算机系统知识
关键词:操作系统,内存,编译,进程管理。
第1章 概述
1.1 Hello简介
在教师处得到hello.c程序。
然后在linux中,hello.c经过cpp的预处理、ccl的编译、as的汇编、ld的链接最终成为可执行目标程序hello,在shell中键入启动命令后,shell为其fork,产生子进程,于是hello便从Program摇身一变成为Process,这便是P2P的过程。
之后shell为其execve,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构,以上全部便是020的过程。
1.2 环境与工具
硬件环境:Intel Core i7-6700HQ x64CPU,16G RAM,256G SSD +1T HDD.
软件环境:Ubuntu18.04.1 LTS、VMware。
开发与调试工具:vim,gcc,as,ld,edb,readelf,HexEdit
1.3 中间结果

文件名称 文件作用
hello.i 预处理之后文本文件
hello.s 编译之后的汇编文件
hello.o 汇编之后的可重定位目标执行
hello 链接之后的可执行目标文件
hello2.c 测试程序代码
hello2 测试程序
helloo.objdump Hello.o的反汇编代码
helloo.elf Hello.o的ELF格式
hello.objdump Hello的反汇编代码
hello.elf Hello的ELF格式

1.4 本章小结
hello.c被编写出来,然后在编译器的作用下被编译成可执行文件,然后在系统的操作下被执行,然后被回收,看似简单的步骤却经历了一番伟大的路程。这不仅仅代表了一个程序,也代表了绝大多数程序的历程。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。
主要功能如下:
1:将源文件中用#include形式声明的文件复制到新的程序中。
2:用实际值替换用#define定义的字符串
3:根据#if后面的条件决定需要编译的代码
2.2在Ubuntu下预处理的命令
命令:cpp hello.c > hello.i
在这里插入图片描述图2.1 使用cpp命令生成hello.i文件
2.3 Hello的预处理结果解析
使用vim打开hello.i之后发现,整个hello.i程序已经拓展为3188行,main函数出现在hello.c中的代码自3099行开始。如下:
在这里插入图片描述
图2.2 hello.i中main函数的位置
在这之前出现的是stdio.h unistd.h stdlib.h的依次展开,以stdio.h的展开为例,cpp到默认的环境变量下寻找stdio.h,打开/usr/include/stdio.h 发现其中依然使用了#define语句,cpp对此递归展开,所以最终.i程序中是没有#define的。而且发现其中使用了大量的#ifdef #ifndef的语句,cpp会对条件值进行判断来决定是否执行包含其中的逻辑。其他类似。
2.4 本章小结
.c文件中包含有头文件也就是有外部文件的,还有一些程序员需要但是对于程序执行没有任何帮助的宏定义以注释,和一些程序员需要的条件编译和完善程序文本文件等操作都需要通过预处理来实现。预处理可以使得程序在后序的操作中不受阻碍,是非常重要的步骤。
第3章 编译
3.1 编译的概念与作用
编译器将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。这个过程称为编译,同时也是编译的作用。
编译器的构建流程主要分为3个步骤:
1:词法分析器,用于将字符串转化成内部的表示结构。
2:语法分析器,将词法分析得到的标记流(token)生成一棵语法树。
3:目标代码的生成,将语法树转化成目标代码。
3.2 在Ubuntu下编译的命令
命令:gcc –S hello.i –o hello.s
在这里插入图片描述
图3.1 使用gcc命令生成64位的hello.s文件
3.3 Hello的编译结果解析

3.3.0 汇编指令
指令 含义
.file 声明源文件
.text 以下是代码段
.section .rodata 以下是rodata节
.global 声明一个全局变量
.type 用来指定是函数类型或是对象类型
.size 声明大小
.long、.string 生命一个long、string类型
.align 声明对指令或者数据的存放地址进行对齐的方式
3.3.1 数据
hello.s中用到的c数据类型有:整型、字符串、数组。
*整型
在这里插入图片描述
图3.2 hello.s中sleepsecs的声明
程序中涉及的整数有:
1:int sleepsecs:sleepsecs在C程序中被声明为全局变量,且已经被赋值,编译器处理时在.data节声明该变量,.data节存放已经初始化的全局和静态C变量。在图中,可以看到,编译器
首先将sleepsecs在.text代码段中声明为全局变量,
其次在.data段中,设置对齐方式为4、设置类型为对象、设置大小为4字节、设置为long类型其值为2。
2:int i:编译器将局部变量存储在寄存器或者栈空间中,在hello.s中编译器将i存储在栈上空间-4(%rbp)中,可以看出i占据了栈中的4B
3:int argc:作为第一个参数传入。
4:立即数:其他整形数据的出现都是以立即数的形式出现的,直接硬编码在汇编代码中。
*字符串
在这里插入图片描述
图3.3 hello.s中声明在.LC0和.LC1段中的字符串
程序中的字符串分别是:

1:“Usage: Hello 学号 姓名!\n”,第一个printf传入的输出格式化参数,在hello.s中声明如图3.2,可以发现字符串被编码成UTF-8格式,一个汉字在utf-8编码中占三个字节,一个\代表一个字节。
2:“Hello %s %s\n”,第二个printf传入的输出格式化参数,在hello.s中声明。
*数组
在这里插入图片描述
图3.4计算地址取出数组值
程序中涉及数组的是:char argv[] main,函数执行时输入的命令行,argv作为存放char指针的数组同时是第二个参数传入。
argv单个元素char
大小为8B,argv指针指向已经分配好的、一片存放着字符指针的连续空间,起始地址为argv,main函数中访问数组元素argv[1],argv[2]时,按照起始地址argv大小8B计算数据地址取数据,在hello.s中,使用两次(%rax)(两次rax分别为argv[1]和argv[2]的地址)取出其值。
3.3.2 赋值
程序中涉及的赋值操作有:

  1. int sleepsecs=2.5 :因为sleepsecs是全局变量,所以直接在.data节中将sleepsecs声明为值2的long类型数据。
  2. i=0:整型数据的赋值使用mov指令完成,根据数据的大小不同使用不同后缀,分别为:
    指令 b w l q
    大小 8b(1B) 16b(2B) 32b(4B) 64b(8B)
    因为i是4B的int类型,所以使用movl进行赋值,汇编代码如图:

图3.5 hello.s中变量i的赋值
3.3.3 类型转换
程序中涉及隐式类型转换的是:int sleepsecs=2.5,将浮点数类型的2.5转换为int类型。
当在double或float向int进行类型转换的时候,程序改变数值和位模式的原则是:值会向零舍入。如果不能为该浮点数找到一个合适的整数近似值,就会产生一个整数不确定值。
浮点数默认类型为double,所以上述强制转化是double强制转化为int类型。遵从向零舍入的原则,将2.5舍入为2。
3.3.4 算术操作
数据算术操作汇编指令:
指令 效果
leaq S,D D=&S
INC D D+=1
DEC D D-=1
NEG D D=-D
ADD S,D D=D+S
SUB S,D D=D-S
IMULQ S R[%rdx]:R[%rax]=SR%rax
MULQ S R[%rdx]:R[%rax]=S
R%rax
IDIVQ S R[%rdx]=R[%rdx]:R[%rax] mod S(有符号)
R[%rdx]=R[%rdx]:R[%rax] div S
DIVQ S R[%rdx]=R[%rdx]:R[%rax] mod S(无符号)
R[%rdx]=R[%rdx]:R[%rax] div S
程序中涉及的算数操作有:
1:i++,对计数器i自增,使用程序指令addl,后缀l代表操作数是一个4B大小的数据。
2:汇编中使用leaq .LC1(%rip),%rdi,使用了加载有效地址指令leaq计算LC1的段地址%rip+.LC1并传递给%rdi。
3.3.5 关系操作
编译器将i<10编译成:在这里插入图片描述
图3.6 判断i的取值
3.3.6 控制转移
编译器将if,for等控制转移语句都使用了cmp来比较然后使用了条件跳转指令来跳转。编译器将if(argc!=3)编译成:
在这里插入图片描述
图3.7 if语句的汇编代码
将for循环里面的比较和转移编译成:在这里插入图片描述
图3.8 for语句的汇编代码

3.3.7 函数操作
printf(“Usage: Hello 学号 姓名!\n”);编译结果
在这里插入图片描述
图3.9 printf(“Usage: Hello 学号 姓名!\n”);汇编代码
printf(“Hello %s %s\n”,argv[1],argv[2]);编译结果:
在这里插入图片描述
图3.10 printf(“Hello %s %s\n”,argv[1],argv[2]);汇编代码

sleep(sleepsecs);编译结果:
在这里插入图片描述
图3.11 sleep(sleepsecs);汇编代码
在这里插入图片描述
图3.12 Ubuntu下的hello.c
在这里插入图片描述
图3.13 Ubuntu下的hello.s

3.4 本章小结
在编译阶段,编译器将高级语言编译成汇编语言。汇编语言是直接面向处理器的程序设计语言。处理器是在指令的控制下工作的,处理器可以识别的每一条指令称为机器指令。每一种处理器都有自己可以识别的一整套指令,称为指令集。处理器执行指令时,根据不同的指令采取不同的动作,完成不同的功能,既可以改变自己内部的工作状态,也能控制其它外围电路的工作状态。
汇编语言的另一个特点就是它所操作的对象不是具体的数据,而是寄存器或者存储器,也就是说它是直接和寄存器和存储器打交道,这也是为什么汇编语言的执行速度要比其它语言快,但同时这也使编程更加复杂,因为既然数据是存放在寄存器或存储器中,那么必然就存在着寻址方式,也就是用什么方法找到所需要的数据。例如上面的例子,我们就不能像高级语言一样直接使用数据,而是先要从相应的寄存器中把数据取出。这也就增加了编程的复杂性,因为在高级语言中寻址这部分工作是由编译系统来完成的,而在汇编语言中是由程序员自己来完成的,这无异增加了编程的复杂程度和程序的可读性。
然后,汇编语言指令是机器指令的一种符号表示,而不同类型的CPU 有不同的机器指令系统,也就有不同的汇编语言,所以,汇编语言程序与机器有着密切的关系。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编器将hello.s 翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o 中。
作用:汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。汇编相对于编译过程比较简单,根据汇编指令和机器指令的对照表一一翻译即可。
4.2 在Ubuntu下汇编的命令
命令:gcc -c hello.s -o hello.o
在这里插入图片描述
图4.1 gcc命令生成hello.o
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
节头信息如下:
在这里插入图片描述
图4.2 Ubuntu下readelf列出节头信息
重定位节有.rela.text以及.rela.eh_frame详细信息如下:
在这里插入图片描述
图4-3 Ubuntu下重定位节的详细信息
4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
在这里插入图片描述
图4.4 hello.o的反汇编代码
在这里插入图片描述
图4.5 hello.s的汇编代码
对比两图发现:汇编器在汇编hello.s时:
1:为每条语句加上了具体的地址,全局变量和常量都被安排到了具体的地址里面。
2:操作数在hello.s里面都是十进制,在到hello.o里面的机器级程序时都是十六进制。
3:跳转语句jx&jxx原来对应的符号都变成了相对偏移地址。
4:函数调用时原来的函数名字也被替换成了函数的相对偏移地址。
4.5 本章小结
本章介绍了hello从hello.s到hello.o的汇编过程,通过查看hello.o的elf格式和使用objdump得到反汇编代码与hello.s进行比较的方式,间接了解到从汇编语言映射到机器语言汇编器需要实现的转换。
第5章 链接
5.1 链接的概念与作用
链接本质:合并相同的“节”
作用:目标代码不能直接执行,要想将目标代码变成可执行程序,还需要进行链接操作。才会生成真正可以执行的可执行程序。链接操作最重要的步骤就是将函数库中相应的代码组合到目标文件中。
5.2 在Ubuntu下链接的命令
在这里插入图片描述
图5.1 Ubuntu下生成hello
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
在ELF格式文件中,Section Headers对hello中所有的节信息进行了声明,其中包括大小Size以及在程序中的偏移量Offset,因此根据Section Headers中的信息我们就可以用HexEdit定位各个节所占的区间。其中Address是程序被载入到虚拟地址的起始地址。
在这里插入图片描述
在这里插入图片描述
图5.2 hello ELF格式中的Section Headers Table
5.4 hello的虚拟地址空间
在这里插入图片描述
图5-3 hello的虚拟地址空间在edb中的展示
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
在这里插入图片描述
图5.4 hello的反汇编代码
hello相对于hello.o有如下不同:

1:hello.o中的相对偏移地址到了hello中变成了虚拟内存地址
2:hello中相对hello.o增加了许多的外部链接来的函数。
3:hello相对hello.o多了很多的节类似于.init,.plt等
4:hello.o中跳转以及函数调用的地址在hello中都被更换成了虚拟内存地址。
重定位:链接器在完成符号解析以后,就把代码中的每个符号引用和正好一个符号定义关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。然后就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时的地址。在hello到hello.o中,首先是重定位节和符号定义,链接器将所有输入到hello中相同类型的节合并为同一类型的新的聚合节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每一个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。然后是重定位节中的符号引用,链接器会修改hello中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址。
在这里插入图片描述
图5.5 hello的反汇编代码hello.objdump
在这里插入图片描述
图5.6 hello.o的反汇编代码hello.objdump
通过比较hello.objdump和helloo.objdump了解链接器。

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的重定位,.rodata与.text节之间的相对距离确定,因此链接器直接修改call之后的值为目标地址与下一条指令的地址之差,指向相应的字符串。
5.6 hello的执行流程
(以下格式自行编排,编辑时删除)
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
(以下格式自行编排,编辑时删除)
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
第6章 hello进程管理
6.1 进程的概念与作用
概念:一个执行中程序的实例。
作用:每次用户通过向shell 输入一个可执行目标文件的名字,运行程序时, shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell 是一个交互型的应用级程序,它代表用户运行其他程序。
处理流程:shell 执行一系列的读/求值(read /evaluate ) 步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。
6.3 Hello的fork进程创建过程
Shell通过调用fork 函数创建一个新的运行的子进程。也就是Hello程序,Hello进程几乎但不完全与Shell相同。Hello进程得到与Shell用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。Hello进程还获得与Shell任何打开文件描述符相同的副本,这就意味着当Shell调用fork 时,Hello可以读写Shell中打开的任何文件。Sehll和Hello进程之间最大的区别在于它们有不同的PID。
在这里插入图片描述
图6.1 Hello的fork进程创建进程图
!6.4 Hello的execve过程
execve 函数加载并运行可执行目标文件filename, 且带参数列表argv 和环境变量列表envp 。只有当出现错误时,例如找不到filename, execve 才会返回到调用程序。所以,与fork 一次调用返回两次不同, execve 调用一次并从不返回。
在这里插入图片描述
图6.2 Hello的execve进程创建进程图
6.5 Hello的进程执行
Linux 系统中的每个程序都运行在一个进程上下文中,有自己的虚拟地址空间。当shell 运行一个程序时,父shell 进程生成一个子进程,它是父进程的一个复制。子进程通过execve 系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片(chunk), 新的代码和数据段袚初始化为可执行文件的内容。最后,加载器跳转到_start地址,它最终会调用应用程序的main 函数。
6.6 hello的异常与信号处理
1:异常种类:
中断:SIGSTP:挂起程序
终止:SIGINT: 终止程序
2:命令的运行
*正常运行
在这里插入图片描述
图6.3正常运行hello程序
*ps
在这里插入图片描述
图6.4 ps命令运行截屏
*jobs
在这里插入图片描述
图6.5 jobs命令运行截屏
*pstree
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
图6.6 pstree命令截屏
*fg
在这里插入图片描述
图6.7 fg命令截屏
*kill
在这里插入图片描述
图6.8 kill命令运行截屏
*胡按
在这里插入图片描述
图6.9 胡按运行截屏
6.7本章小结
在本章中,阐明了进程的定义与作用,介绍了Shell的一般处理流程,调用fork创建新进程,调用execve执行hello,hello的进程执行,hello的异常与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址是指由程序产生的与段相关的偏移地址部分。就是hello.o里面的相对偏移地址。

线性地址:地址空间是一个非负整数地址的有序集合,如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间。就是hello里面的虚拟内存地址。

虚拟地址:CPU 通过生成一个虚拟地址。就是hello里面的虚拟内存地址。

物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。计算机系统的主存被组织成一个由M 个连续的字节大小的单元组成的数组。每字节都有一个唯一的物理地址。就是hello在运行时虚拟内存地址对应的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。
其中:
索引号是“段描述符”的索引,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,每一个段描述符由8个字节组成。
下面是转换的具体步骤:
1:给定一个完整的逻辑地址[段选择符:段内偏移地址]。
2:看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,3:再根据相应寄存器,得到其地址和大小。可以得到一个数组。
4:取出段选择符中前13位,在数组中查找到对应的段描述符,得到Base,也就是基地址。
5:线性地址 = Base + offset。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址被分为以固定长度为单位的组,称为页,例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个tatol_page[220]的大数组,共有2的20个次方个页。这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址——对应的页的地址。另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。这里注意到,这个total_page数组有220个成员,每个成员是一个地址(32位机,一个地址也就是4字节),那么要单单要表示这么一个数组,就要占去4MB的内存空间。为了节省空间,引入了一个二级管理模式的机器来组织分页单元。
对于一个二级管理模式:
1:分页单元中,页目录是唯一的,它的地址放在CPU的cr3寄存器中,是进行地址转换的开始点。
2.每一个活动的进程,因为都有其独立的对应的虚似内存,那么它也对应了一个独立的页目录地址。运行一个进程,需要将它的页目录地址放到cr3寄存器中
3.每一个32位的线性地址被划分为三部份,面目录索引(10位):页表索引(10位):偏移(12位)
并依据以下步骤进行转换:
1.从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器)。
2.根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
3.根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址。
4.将页的起始地址与线性地址中最后12位相加,得到最终我们想要的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
36位VPN 被划分成四个9 位的片,每个片被用作到一个页表的偏移量。CR3 寄存器包含Ll页表的物理地址。VPN 1 提供到一个Ll PET 的偏移量,这个PTE 包含L2 页表的基地址。VPN 2 提供到一个L2 PTE 的偏移量,以此类推。
7.5 三级Cache支持下的物理内存访问
首先我们要先将高速缓存与地址翻译结合起来,首先是CPU发出一个虚拟地址给TLB里面搜索,如果命中的话就直接先发送到L1cache里面,没有命中的话就先在页表里面找到以后再发送过去,到了L1里面以后,寻找物理地址又要检测是否命中,这里就是使用到我们的CPU的高速缓存机制了,通过这种机制再搭配上TLB就可以使得机器在翻译地址的时候的性能得以充分发挥。
7.6 hello进程fork时的内存映射
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用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做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障是一种常见的故障,当指令引用一个虚拟地址,在MMU中查找页表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出的时候就会发生故障。
缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送VA到MMU,这次MMU就能正常翻译VA了。
7.9动态存储分配管理
printf函数会调用malloc,下面简述动态内存管理的基本方法与策略:
基本方法:这里指的基本方法应该是在合并块的时候使用到的方法,有最佳适配和第二次适配还有首次适配方法,首次适配就是指的是第一次遇到的就直接适配分配,第二次顾名思义就是第二次适配上的,最佳适配就是搜索完以后最佳的方案,当然这种的会在搜索速度上大有降低。
策略:这里的策略指的就是显式的链表的方式分配还是隐式的标签引脚的方式分配还是分离适配,带边界标签的隐式空闲链表分配器允许在常数时间内进行对前面块的合并。这种思想是在每个块的结尾处添加一个脚部,其中脚部就是头部的一个副本。如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。显式空间链表就是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继指针,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。为了分配一个块,必须确定请求的大小类,并且对适当的空闲链表做首次适配,查找一个合适的块。如果找到了一个,那么就(可选地)分割它,并将剩余的部分插入到适当的空闲链表中。如果找不到合适的块,那么就搜索下一个更大的大小类的空闲链表。如此重复,直到找到一个合适的块。如果空闲链表中没有合适的块,那么就向操作系统请求额外的堆内存,从这个新的堆内存中分配出一个块,将剩余部分放置在适当的大小类中。要释放一个块,我们执行合并,并将结果放置到相应的空闲链表中。
7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以intel Core7在指定环境下介绍了VA到PA的变换、物理内存访问,还介绍了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行,这就是Unix I/O接口。
8.2 简述Unix IO接口及其函数
Unix I/O接口统一操作:

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

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_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
研究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; 
}

参数中明显采用了可变参数的定义,可以看到*fmt是一个char 类型的指针,指向字符串的起始位置。而且arg获得第二个不定长参数,即输出的时候格式化串对应的值。
再来看vsprintf代码:
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程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。
在printf中调用系统函数write(buf,i)将长度为i的buf输出。write函数如下:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
在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 

可以看出代码里面的call是访问字库模板并且获取每一个点的RGB信息最后放入到eax也就是输出返回的应该是显示vram的值,然后系统显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。getchar进行封装,大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf函数和getchar函数。
(第8章1分)
结论
1:编写,得到hello.c
2:预处理,将hello.c调用的所有外部的库展开合并到一个hello.i文件中
3:编译,将hello.i编译成为汇编文件hello.s
4:汇编,将hello.s会变成为可重定位目标文件hello.o
5:链接,将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello
6:运行:在shell中输入./hello 1170300825 lidaxin
7:创建子进程:shell进程调用fork为其创建子进程
8:运行程序:shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数。
9:执行指令:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流
10:访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
11:动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。
信号:如果运行途中键入ctr-c ctr-z则调用shell的信号处理函数分别停止、挂起。
12:结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。
(结论0分,缺少 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
(附件0分,缺失 -1分)

文件名称 文件作用
hello.i 预处理之后文本文件
hello.s 编译之后的汇编文件
hello.o 汇编之后的可重定位目标执行
hello 链接之后的可执行目标文件
hello2.c 测试程序代码
hello2 测试程序
helloo.objdump Hello.o的反汇编代码
helloo.elf Hello.o的ELF格式
hello.objdump Hello的反汇编代码
hello.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] ELF 构造:https://www.cs.stevens.edu/~jschauma/631/elf.html

[8] 16进制计算器:http://www.99cankao.com/digital-computation/hex-calculator.php

[9] Linux下进程的睡眠唤醒:https://blog.csdn.net/shengin/article/details/21530337

[10]进程的睡眠、挂起和阻塞:https://www.zhihu.com/question/42962803

[11]虚拟地址、逻辑地址、线性地址、物理地址:https://blog.csdn.net/rabbit_in_android/article/details/49976101

[12]printf函数实现的深入剖析:https://blog.csdn.net/zhengqijun_/article/details/72454714

[13] 内存地址转换与分段 https://blog.csdn.net/drshenlei/article/details/4261909
(参考文献0分,确实 -1分)

猜你喜欢

转载自blog.csdn.net/qq_44094873/article/details/85484169