HELLO的一生

第1章 概述
1.1 Hello简介
Hello的P2P:从hello.c的源文件开始,通过GCC编译器驱动程序对它进行预处理后生成hello.i文件,再通过编译器生成汇编程序hello.s,汇编程序再通过汇编器生成可重定位目标程序,最后再与标准库函数进行链接生成可执行文件。然后在shell中运行可执行文件,shell通过fork()函数为它开辟新的子进程。Hello.c就从Program变成了Process。

Hello的020:Hello变成进程后,shell再调用execve()函数将程序加载进来:先删除已存在的用户区域,再为Hello映射新的私有区域,映射共享区域。

CPU来控制Hello的运行,为它分配资源。当进程结束后,通过父进程回收,通过内核将数据删除,将Hello运行的所有记录消除。

1.2 环境与工具
软件环境:VMware® Workstation 14 Player,Ubuntu 16.04 LTS 64位。
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk。
开发调试工具:GCC编译器,EDB,as,ld,readelf。

1.3 中间结果
在这里插入图片描述

1.4 本章小结
hello从Progranm到Process,实现了它要执行的操作,就是hello的一生经历。系统为它分配资源,进程结束后,又回收了所有的资源,这就是hello的020。

(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
预处理的概念:在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作,预编译指令指示在程序正式编译之前就由编译器进行的操作,可放在程序中的任何位置。

预处理的作用:预处理器根据以字符#开头的命令,修改原始C程序。比如hello.c中第一行的#include<stdio.h>命令告诉预编译处理器读取系统头文件stdio.h的内容,并把它直接插入到程序文本中。结果就得到了另外一个C程序,通常是以.i作为文件扩展名。

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

2.3 Hello的预处理结果解析
hello.c中的头文件:

在这里插入图片描述

预处理后:
在这里插入图片描述

通过预处理将程序运行所需要的信息从头文件中直接读取插入到文本中,在后边编译时通过这些信息加载程序。

2.4 本章小结
最开始的源程序hello.c通过预处理后将头文件中的信息加载到文本中,完善文本文件,特别是程序中定义的宏需要预处理来对它进行解释,保证了程序在以后的处理中可以正常的进行。
(第2章0.5分)

第3章 编译
3.1 编译的概念与作用

编译的概念:将预处理生成的.i文件翻译成为包含汇编代码的.s文件。
编译的作用:它通过词法分析和语法分析将程序从高级语言转化为了汇编语言,并且对程序进行了初步的优化。

3.2 在Ubuntu下编译的命令
编译命令行:gcc -E hello.c -o hello.s
在这里插入图片描述

3.3 Hello的编译结果解析
对语句的编译:
3.3.1
在这里插入图片描述

常量sleepsecs在.s文件中对它进行了分析,给出了sleepsecs的类型,大小等相关信息,方便后面处理时获得信息。

3.3.2
在这里插入图片描述在这里插入图片描述
对赋值语句进行了编译。这里存在一个隐式的类型转换,sleepsecs是一个int型变量,但它赋值为2.5,所以编译器将2.5转化为2存入。

3.3.3
在这里插入图片描述在这里插入图片描述
对printf()函数中的字符串进行编译。将字符串提取保存。

3.3.4
在这里插入图片描述在这里插入图片描述
对printf()中的格式串进行编译。将printf()中的格式串保存。

3.3.5
在这里插入图片描述在这里插入图片描述
将比较语句翻译。

3.3.6
在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述
对循环控制条件语句进行编译。初始化为0,编译器将小于10转化为和9比较大小,再进行加一操作。

3.3.7
在这里插入图片描述在这里插入图片描述
将if条件分支编译。

3.3.8
在这里插入图片描述在这里插入图片描述
对for()循环的编译。

对函数的编译:
3.3.9
在这里插入图片描述在这里插入图片描述
对printf(“Usage: Hello 学号 姓名!\n”)编译。

3.3.10
在这里插入图片描述在这里插入图片描述
对exit()函数的编译。

3.3.11
在这里插入图片描述在这里插入图片描述
printf(“Hello %s %s\n”,argv[1],argv[2])的编译。

3.3.12
在这里插入图片描述在这里插入图片描述
对sleep()函数编译。

3.3.13
在这里插入图片描述在这里插入图片描述
对getchar函数的编译。

3.4 本章小结
编译器将预处理后的文本进行汇编处理,对于常量,编译器将它储存到一个特定的位置,记录它的一些信息,比如类型;对于一些特定的常量,比如printf()函数中的信息,编译器会把它提取出来保存。程序中的语句,例如赋值语句,编译器通过寄存器,栈等结构进行赋值;分支语句用je,jle,jge等条件跳转语句进行实现。每种语句都有对应的实现方法。程序中的函数,如果不是库函数,则会对函数进行逐句的语法分析和解析,如果是标准的库函数,编译器可以直接用call语句进行调用。
汇编语言相对于高级语言来说,它更加靠近底层机器且直接面对硬件的,所以也为高级语言提供了一种统一的面向机器的解释,它具有一些助记符,所以比直接的机器语言好理解,但相对于高级语言又显得难以掌握。汇编语言具有:机器相关性,高速度和高效率,编写和调试的复杂性等特性。

第4章 汇编
4.1 汇编的概念与作用
汇编的概念:通过汇编器将汇编语言转化为机器语言。

汇编的作用:汇编器将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在hello.o中,hello.o是二进制文件。

4.2 在Ubuntu下汇编的命令
汇编命令行:gcc -c hello.s -o hello.o
在这里插入图片描述

4.3 可重定位目标elf格式

ELF可重定位目标文件的格式:
在这里插入图片描述
在这里插入图片描述

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

汇编器在对hello.s文件转化为机器代码后,对程序做出了最基本的处理,在rel.text对每个调用的标准库函数和全局变量给出了偏移量,以及在信息一类中给出的符号在symbol节中的偏移量,以及符号的类型。
在这里插入图片描述

4.4 Hello.o的结果解析
Hello.s文件中的汇编代码:
在这里插入图片描述

在这里插入图片描述

反汇编生成的汇编代码:
在这里插入图片描述

反汇编后的汇编代码已经进行了初始化的定位,形成EIF可重定位文件,并且每条指令都给出了对应的机器码。

1)对于条件分支:.s文件中通过使用.Ln等助记符来标记跳转位置,而在反汇编文件中去掉了这种记号,直接使用偏移地址来进行跳转。
hello.s文件:
在这里插入图片描述

反汇编文件:
在这里插入图片描述

2)对于函数调用:在.s文件中,call指令后面接的是函数名,并没有给出地址,而在反汇编文件中,给出了函数的偏移地址。因为在汇编的过程中,对于程序中的函数,汇编器在EIF文件的rel.text段中给出了定位,所以反汇编中函数就有了相对于首地址的偏移地址。
hello.s文件:
在这里插入图片描述

反汇编文件:
在这里插入图片描述

3)对于全局变量调用:.s文件中通过全局变量的名字加上%rip的值来调用,而在反汇编代码中通过0x0加上%rip的值来调用。因为在.s文件中并没有给出全局变量地址的信息,所以只能通过变量名来标记;在反汇编代码中,已经对全局变量作出了定位处理,当后续操作进行链接后就可以给出绝对地址了。
hello.s文件:
在这里插入图片描述

反汇编文件:
在这里插入图片描述

4.5 本章小结
汇编器对hello.s文件进行汇编,生成了可重定位文件。对文件的全局变量,函数,程序语句都进行了分析,给出了初始的相对位置信息,相当于对整个文件做出了一个初始的整理,为后面的链接等操作做准备。
(第4章1分)

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

链接的作用:将程序中用到的各种模块,例如标准函数模块,与原程序进行合并,使得原程序可以执行。
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.ohello.o/usr/lib/x86_64-linux-gnu/libc.so/usr/lib/x86_64-linux-gnu/crtn.o

在这里插入图片描述

5.3 可执行目标文件hello的格式
用readelf命令生成hello的ELF文件中关于每个节的基本信息:
其中包括了每个节的大小,相对于ELF头的偏移等信息。
在这里插入图片描述
在这里插入图片描述

5.4 hello的虚拟地址空间
在edb的Data Dump一栏中可以看到程序的虚拟地址。
在这里插入图片描述

在hello的ELF文件的“程序头”一节中,也可以找到各节对应的虚拟地址:
在这里插入图片描述

分析:
在hello的ELF文件的“程序头”一节中,一共有八段,并且给出了每一段的偏移量,虚拟地址,物理地址,访问权限,对齐等信息。

1)PHDR:用来保存程序头表。

2)INTERP:指定程序从可行性文件映射到内存之后,必须调用的解释器,它是通过链接其他库来满足未解析的引用,用于在虚拟地址空间中插入程序运行所需的动态库。

3)LOAD:表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了常量数据(如字符串)、程序的目标代码等。

4)DYNAMIC:保存了由动态连接器(即INTERP段中指定的解释器)使用的信息。

5)NOTE:保存辅助信息

6)GNU_STACK:权限标志,标志栈是否是可执行的。

7)GNU_RELRO:指定在重定位结束之后那些内存区域是需要设置只读。
5.5 链接的重定位过程分析
可执行文件objdump指令得到的代码(部分):
在这里插入图片描述

hello.o文件objdump指令得到的代码(部分):
在这里插入图片描述

分析:可以明显看出,可执行文件对每个函数给出了重定位处理,给出了它们的绝对物理地址;而在hello.o中,只是给出了每个函数对于程序首地址的偏移地址。

通过上面两个文件,可以了解到链接实现的过程:将可重定位文件中的.text节中函数以及全局变量的相对地址转变为了绝对地址,全局变量的寻址方式0x0%rsp也将0x0改为了确定的地址。

对hello.中的重定位节中的定位:在hello.o中,给出了函数和全局变量相对于EIF头的偏移量,所以在链接后,给定了程序首地址,然后根据偏移量,计算出函数和全局变量的绝对地址。

5.6 hello的执行流程
在这里插入图片描述

5.7 Hello的动态链接分析
分析:对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,在链接时,对所有绝对地址的引用不做重定位,把这一步推迟到装载的时候进行;一旦模块装载地址确定,即目标地址确定,则系统对程序中所有的绝对地址的引用进行重定位。

5.8 本章小结
链接在程序编译的过程中有着十分重要的作用,它为程序处理好了需要的绝大多数资源,将所需要的函数,变量等信息都整理好,给出了绝对地址,使得程序完整,可以执行。
而执行链接的链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。

(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:进程是操作系统一个正在运行的程序的一种抽象。程序在系统上运行时,操作系统会提供一种假象,就好像系统上只有这个程序在运行。程序看上去是独占地使用处理器,主存和I/O设备。处理器看上去就像在不间断地一条接一条的执行程序中的指令。

进程的作用:通过进程的概念,系统可以实现多线程,并发运行等操作。
6.2 简述壳Shell-bash的作用与处理流程
壳Shell_bash的作用:接受终端输入的命令行,并对命令行分析,然后代表用户执行命令行请求的操作。

处理流程:分析命令行中的参数,分辨是内置命令还是可执行文件,是内置命令则执行相应操作,如果是可执行文件,则fork()产生子进程,execve()加载程序,在子进程中完成请求的操作,然后结束进程,并回收。
6.3 Hello的fork进程创建过程
fork()创建子进程全过程:
在fork()后,子进程得到和父进程完全一样但独立的虚拟地址空间。并且可以打开所有父进程可以打开的文件。

6.4 Hello的execve过程
execve函数加载并运行可执行目标文件,且带参数列表argv和环境变量列表envp。它在加载文件后,调用文件的启动代码,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。

6.5 Hello的进程执行
当创建子进程后,子进程就有了它自己的上下文,包括存放在内存的程序的代码和数据,它的栈,通用目的寄存器中的内容等。进程在处理器中运行,处理器中有多个进程,但是它通过逻辑控制流提供一个假象,好像每个进程独占处理器,关键在于进程是轮流使用处理器,在不同的时间片上使用。此外处理器还通过某个控制寄存器中的一个模式位提供内核模式,当设置模式位时,就是内核模式,可以享有特权,没有设置模式位时,就是用户模式,没有特权。通过改变模式位来切换两种模式。
6.6 hello的异常与信号处理
hello执行过程中出现的异常:中断——来自处理器外部的I/O设备,比如Ctrl+C。

1)正常运行:
在这里插入图片描述

在我输入lc后,程序结束,进程被回收。

2)通过输入Ctrl+Z暂停进程:
在这里插入图片描述

通过ps和jobs指令可以查看暂停的进程PID,以及进程的JID。
在这里插入图片描述

通过fg指令,将JID为3的进程调到前台继续运行,然后打印剩下的6个信息。

3)通过输入Ctrl+Z来暂停进程,并用kill来终止进程:通过输入Ctrl+Z来暂停进程,并用kill来终止进程:在Crtl+ Z暂停进程后,再用kill指令杀死进程,但进程并没有被回收。
在这里插入图片描述

4)Crtl+C来终止并回收进程
在这里插入图片描述

Ctrl+C终止了进程并且回收了。

5)pstree指令:
在这里插入图片描述

6)在执行过程中乱按键盘:
在这里插入图片描述

说明程序在运行过程中对于I/O设备的信号具有阻塞作用(除了一些特定的信号,如Ctrl+C等)。

6.7本章小结
hello的可执行文件通过shell来接受,分析,然后通过fork()为hello开辟子程序,然后execve()函数将程序加载进去,这时候hello就从程序变成了进程,fork()函数给于了hello进程和父进程一样的资源,所以hello进程具有了上下文。
处理器对hello进程提供逻辑控制流的抽象,控制hello进程的运行时间片,并且通过模式位来定义内核模式和用户模式,让hello进程可以进行上下文切换。
hello进程在运行过程中需要处理异常,对异常作出反应。
hello进程终止后,将被父进程回收,然后删除它的上下文,这就是整个hello进程开始到结束。

(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
1) 逻辑地址:逻辑地址(LogicalAddress)是指由程序产生的与段相关的偏移地址部分。就是hello.o里面的相对偏移地址。

2)线性地址:地址空间(address space) 是一个非负整数地址的有序集合,如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间(linear address space) 。它等价于虚拟地址。

3)虚拟地址:CPU 通过生成一个虚拟地址(Virtual Address, VA) 。就是hello里面的虚拟内存地址。

4)物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。计算机系统的主存被组织成一个由M 个连续的字节大小的单元组成的数组。每字节都有一个唯一的物理地址。就是hello在运行时虚拟内存地址对应的物理地址。

7.2 Intel逻辑地址到线性地址的变换-段式管理
首先确定逻辑地址的组成:一个逻辑地址由两部份组成,段标识符, 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。索引号,或者直接理解成数组下标。
在这里插入图片描述
具体的变换方式:
首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],
1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。

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

3、把Base + offset,就是要转换的线性地址了。

7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址到物理地址的变换是通过分页机制完成的。
首先先分析线性地址的组成:设虚拟地址长度为n,它由虚拟页号(VPN)和虚拟页偏移(VPO)组成,根据虚拟页面大小,可以确定VPO所占的字节长度;而虚拟页号(VPN)又由TLB索引(TLBT)和TLB标记(TLBI)组成,可以根据TLB的大小,确定它们所占的字节。
在这里插入图片描述

通过虚拟地址得到虚拟页号(VPN),然后将虚拟页号(VPN)传入TLB中得到对应的物理页号,然后结合虚拟偏移(VPO),确定唯一的物理地址;若是在TBL中找不到,则需要从高速缓存或者磁盘中取出对应的虚拟页表,放入TLB再进行操做。
1)TLB命中
在这里插入图片描述

2)缺页
在这里插入图片描述
7.4 TLB与四级页表支持下的VA到PA的变换
若TLB命中,则从TLB中可以直接找到各级页表,然后得到PPN,与PPO结合即可得到物理地址。若TLB不命中,则需要从高速缓存中到PPN。
在这里插入图片描述

7.5 三级Cache支持下的物理内存访问
先对物理地址分析:由CT,CI,CO三部分组成。CT为缓存标记位;CI为缓存组索引;CO为缓存偏移。
在上一步中我们已经获得了物理地址VA,使用CI(后六位再后六位)进行组索引,每组8路,对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO(后六位)取出数据返回。如果没有匹配成功或者匹配成功但是标志位是1,则不命中(miss),向下一级缓存中查询数据(L2 Cache->L3 Cache->主存)。查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略LFU进行替换。
在这里插入图片描述
7.6 hello进程fork时的内存映射
当fork 函数被shell调用时,内核为子程序复制了一份和父进程完全一样但独立的用户级虚拟地址空间,包括代码和数据段,堆,共享库,以及用户栈。并且创建了当前进程的mm_struct,区域结构和页表的原样副本,内核将两个进程中的每个页面都标记为只读,并且将两个进程的区域结构都标记为私有的写时复制。当有进程想要修改私有区域的某个页面时,就会触发写时复制。

7.7 hello进程execve时的内存映射
execve 函数在shell中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:

1)删除已存在的用户区域。删除shell虚拟地址的用户部分中的已存在的区域结构。映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。图7.7 概括了私有区域的不同映射。
2)映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
3)设置程序计数器(PC) 。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

7.8 缺页故障与缺页中断处理
DRAM缓存不命中成为缺页。CPU 引用了VP 3 中的一个字, VP 3 并未缓存在DRAM 中。地址翻译硬件从内存中读取PTE 3, 从有效位推断出VP 3 未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4 。如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4 的页表条目,反映出VP 4 不再缓存在主存中这一事实。接下来,内核从磁盘复制VP3到内存的PP3,更新PTE3,随后返回。
在这里插入图片描述
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap) 。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址) 。对于每个进程,内核维护着一个变量brk, 它指向堆的顶部。分配器将堆视为一组不同大小的块(block) 的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
基本方法:这里指的基本方法应该是在合并块的时候使用到的方法,有最佳适配和第二次适配还有首次适配方法,首次适配就是指的是第一次遇到的就直接适配分配,第二次顾名思义就是第二次适配上的,最佳适配就是搜索完以后最佳的方案,当然这种的会在搜索速度上大有降低。
策略:这里的策略指的就是显式的链表的方式分配还是隐式的标签引脚的方式分配还是分离适配,带边界标签的隐式空闲链表分配器允许在常数时间内进行对前面块的合并。这种思想是在每个块的结尾处添加一个脚部,其中脚部就是头部的一个副本。如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。显式空间链表就是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继指针,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。为了分配一个块,必须确定请求的大小类,并且对适当的空闲链表做首次适配,查找一个合适的块。如果找到了一个,那么就(可选地)分割它,并将剩余的部分插入到适当的空闲链表中。如果找不到合适的块,那么就搜索下一个更大的大小类的空闲链表。如此重复,直到找到一个合适的块。如果空闲链表中没有合适的块,那么就向操作系统请求额外的堆内存,从这个新的堆内存中分配出一个块,将剩余部分放置在适当的大小类中。要释放一个块,我们执行合并,并将结果放置到相应的空闲链表中。

7.10本章小结
hello刚开始给出的逻辑地址,在汇编的时候由汇编器整理得到,然后hello变为进程的时候,就要把逻辑地址变为虚拟地址,当需要操作的时候,就将虚拟地址翻译成物理地址。
虚拟地址的储存在磁盘中,为了优化,在内存中建立虚拟地址缓冲DRAM,并利用页表结构来储存虚拟地址,每个页表由页表项组成,将虚拟页从磁盘中取出,然后放入DRAM的物理页中;为了简化页表,还使用了多级页表结构。将虚拟地址翻译为物理地址后,物理地址在cache和主存中访问数据。
hello中需要用到的动态存储空间堆,堆中的空间有的是以分配,有的是未分配,为了更好的运行程序,利用资源,堆中的空闲块采用隐式和显式链表来储存,而显式空闲链表又可以采用分离链表来储存。
这就是hello所涉及的储存管理。
(第7章 2分)

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

8.2 简述Unix IO接口及其函数
Unix I/O设备接口执行方式:
1)打开文件。一个应用程序通过要求打开相应的文件,来宣告它想要访问一个I/O设备,内存返回一个叫做描述符的小的非负整数,它在后续对此文件的所有操作中标识这个文件。应用程序只要记住这个描述符。

2)Linux shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。头文件<unistd.h>中定义的某些常量,可以代替显式的描述符值。

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(int 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的实现分析

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

这是printf()函数的函数体。
在参数中用了“…”来表示参数长度,个数不确定。
然后va_list arg = (va_list)((char*)(&fmt) + 4); 代码行表示将参数右边第一个参数压栈(参数是从右到左入栈)。

int vsprintf(buf,fmt,arg)返回的是要打印的字符串的长度。vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

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.     
7.    for (p=buf;*fmt;fmt++) {   
8.    if (*fmt != '%') {   
9.    *p++ = *fmt;   
10.    continue;   
11.    }   
12.     
13.    fmt++;   
14.     
15.    switch (*fmt) {   
16.    case 'x':   
17.    itoa(tmp, *((int*)p_next_arg));   
18.    strcpy(p, tmp);   
19.    p_next_arg += 4;   
20.    p += strlen(tmp);   
21.    break;   
22.    case 's':   
23.    break;   
24.    default:   
25.    break;   
26.    }   
27.    }   
28.     
29.    return (p - buf);   
30.   } 

最后是write(buf,i)函数,将buf中的第i个元素写入终端。write()函数将buf中的数据取出来后,交给操作系统,然后操作系统将数据交给硬件。通过对write()函数的追踪,可以得到write()函数的大致操作过程:
1.write:
2. mov eax, _NR_write
3. mov ebx, [esp + 4]
4. mov ecx, [esp + 8]
5. int INT_VECTOR_SYS_CALL
给寄存器传递参数,然后INT_VECTOR_SYS_CALL执行最后的硬件操作。

我们继续找到INT_VECTOR_SYS_CALL的实现:
1.init_idt_desc(INT_VECTOR_SYS_CALL, DA_386IGate, sys_call, PRIVILEGE_USER);
通过系统调用函数sys_call函数来完成显示。

最后再来看sys_call函数的主体:

1.sys_call:   
2.     call save   
3.      
4.     push dword [p_proc_ready]   
5.      
6.     sti   
7.      
8.     push ecx   
9.     push ebx   
10.     call [sys_call_table + eax * 4]   
11.     add esp, 4 * 3   
12.      
13.     mov [esi + EAXREG - P_STACKBASE], eax   
14.      
15.     cli   
16.      
17.     ret   

它只有一个功能:显示格式化了的字符串。

接下来就是硬件的操作:
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:进程会接受来自键盘的中断异常,触发异常处理子程序,并且异常处理子程序会被调为当前进程处理。键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
hello进程需要I/O设备的信号来正确执行,而Unix通过将设备映射为文件来管理I/O设备。
printf()和getchar()函数通过调用vprintf()函数处理传入的参数,调用write()函数将数据提交给操作系统,然后操作系统通过管理硬件最后实现终端显示。

(第8章1分)

结论
hello最开始是一段代码,描述了它想要进行的操作,但它却不能实现操作,因为它缺少各种条件,它只有代码。当系统想要执行它的时候,hello就开始了它的一生:
首先先对hello.c源文件进行预处理,将hello需要的头文件中的信息插入到文本中,生成了hello.i文件;为了更好的翻译为机器代码,hello.i文件被汇编为hello.s汇编文件,对于程序中的函数,变量,语句都进行了语法分析;再将汇编文件编译成为hello.o机器代码文件,是一个二进制文件,并且对于程序中的函数,变量做出了最基础的处理,为它们划分了不同的段,储存了不同的信息,包括偏移量,访问权限等等信息;最后将程序调用的子函数进行链接,并且将逻辑地址转化为虚拟地址,形成了可执行文件。这个时候hello就有了可以被执行的条件,但是它还不能执行,因为它没有系统分配的资源,包括它要运行的空间等等。
当在shell中输入命令行执行hello时,shell首先分析参数,然后调用fork()函数为它开辟新的子进程,这个时候hello就有了资源:它拥有了和父进程完全一样但独立的用户虚拟空间。execxe()函数将hello程序加载到进程。子进程执行hello所请求的操作,并且通过调用异常处理子程序处理执行过程的异常。对于hello的储存管理,处理器采用逻辑控制流的抽象来保证hello的执行,并且采用虚拟内存的抽象,好像hello独占整个内存。系统采用页表来管理hello的虚拟地址,hello的虚拟地址要翻译为物理地址才能找到数据,物理地址首先到cache中找,然后再到主存中找,找到后返回给进程。
hello进程和I/O设备有交互,所以调用了printf()函数。系统对I/O设备采用映射文件的管理方式。
最后hello进程执行完操作后,会由父进程来回收,并且将它所占用的资源删除,这时候hello就结束了。它又变成了一段代码。下一次在用它时,开启的进程是不是和上一个进程是一个呢?还是上一个进程已经永远的结束了呢?
(结论0分,缺失 -1分,根据内容酌情加分)

附件
列出所有的中间产物的文件名,并予以说明起作用。
在这里插入图片描述
(附件0分,缺失 -1分)

参考文献
[1]我理解的逻辑地址、线性地址、物理地址和虚拟地址
https://blog.csdn.net/haiross/article/details/50995750

[2]ELF格式文件符号表全解析及readelf命令使用方法
https://blog.csdn.net/edonlii/article/details/8779075

[3]Pianistx
https://www.cnblogs.com/pianist/p/3315801.html

[4]《深入理解计算机系统》(原书第三版)

[5]动态链接的整个过程
https://blog.csdn.net/a1342772/article/details/77688148

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值