第1章 概述
1.1 Hello简介
hello程序的生命周期是从一个高级C语言程序的源文件开始的,即程序员通过编辑器(Editor)将源代码一字一键地输入到一个新创建的文本文件中,并保存文件名为hello.c。hello程序一开始什么也没有,因此可以说是从0开始。将hello.c(Program)依次执行预处理、编译、汇编、链接,最终生成一个可执行目标文件hello。
在shell(命令行解释器)里输入命令行 ./hello,shell在当前目录下搜索名为hello的可执行目标文件,通过操作系统(OS)提供的一系列操作进程的系统调用函数将hello加载到内存并执行。首先调用fork函数创建一个子进程(Process),然后在子进程内调用execve函数,它通过内存映射(mmap)将可执行目标文件hello映射到虚拟内存(Virtual Memory),最后通过一系列的页面换入换出机制才真正的将可执行目标文件hello加载到物理内存中并执行。至此hello实现了从程序转变为进程(P2P)的过程。
hello程序执行过程可以简述为:在操作系统(OS)和一系列硬件,如CPU,存储器,I/O设备等的配合下,在一个流水线化的处理器上不断地执行①从内存取指②译码③算术逻辑单元(ALU)执行操作的一个循环过程。
最后在hello进程终止时,shell通过系统调用函数wait/waitpid将它回收,从系统上删除掉hello进程的所有痕迹。系统中不在保留有关hello进程的一切信息,因而也可以说是从0结束。至此hello实现了从零开始从零结束(O2O)的过程。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位
开发与调试工具:gcc,vim,edb,readelf,HexEdit1
1.3 中间结果
hello.i:预处理后的文本文件
hello.s:编译后的汇编语言文件
hello.o:汇编后的可重定位目标文件
hello.oelf:hello.o的ELF文件
hello.o.obj:hello.o的反汇编文件
hello:链接后的可执行目标文件
hello.elf:hello的ELF文件
hello.obj:hello的反汇编文件
1.4 本章小结
本章主要简单的介绍了P2P和020的基本概念,开发环境以及简要介绍一下本次实验的中间结果
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理:在编译之前对源文件进行的处理。
作用:预处理器(cpp)根据以字符#开头的命令,即预处理命令,来修改原始的C程序。
预处理命令主要包括以下三种:
1.宏定义,例如:#define size 10,修改方式是将源文件中所有的size(不包括字符串常量中的size)进行宏替换(宏展开)修改为10。
2.文件包含,例如:#include <stdio.h>,修改方式是读取系统头文件stdio.h的内容,并把它直接插入到该命令在源文件中的位置。
3.条件编译,例如:#ifdef firstfit ,#endif,修改方式是根据是否定义相应的宏来选择对位于#ifdef和#endif之间的内容进行保留或删除。
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
预处理得到的文件hello.i部分代码与hello.c相比较的截图如下
可以看到在hello.i文件中,对hello.c中的头文件进行解析,主要是将#include <stdio.h>、#include <unistd.h> 、#include <stdlib.h>所解析出的代码加在了hello中
2.4 本章小结
在处理hello.c到hello.i的过渡过程中,可以看到预处理主要处理了一些宏定义、条件编译并将其整合在*.i文件中
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译是将经预处理的C语言程序翻译成汇编指令的过程。编译器通过解析、
翻译将后缀为.i的文件转化成包含汇编代码的s文件。
编译器会对C语言的代码进行语法检查,对语法有错误的地方给出error,对存在潜在运行时错误的代码给出warning。此外,编译器还会依据命令选项对C语言代码进行适当的优化,从而提高程序的运行性能。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
编译命令:gcc -o hello.s -s hello.i
3.3 Hello的编译结果解析
3.3.1 hello.s内容
如下图所示
3.3.2 常量
程序中含有字符串常量,所对应的代码如下
可以看出字符串是存储在只读数据段(.rodata)的
3.3.3 变量
局部变量
如图所示
可以看到将hello.c中的局部变量i储存在堆栈段,开辟了4字节的储存空间
全局变量
如图所示
此处代码说明main函数是一个全局变量
3.3.4 数组
此段代码表示了对argv[1]和argv[2]的数据获取
3.3.5 控制转移
程序流程如图所示
分析.LFB6知,首先是惯用指令endbr64,以及保存被调用者保存寄存器%rbx。
这里主要是分配了一些基本的内存,如%rbx保存%rsp的值作为引用局部变量的中介,以及对输入的合法性做判断:若argc!=4则输出提示信息并设置exit状态(%rdi)并结束运行。是通过cmpl $4, -20(%rbp)设置条件码,由jle指令利用条件码决定跳转的目标。
如果argc=4,程序跳转到.L2。这里,为局部变量i赋初值0,而后进入.L3运行。
.L3是一个循环结构,这里,当i(即-4(%rbp))<= 7时,执行循环体.L4。.L4包含一些系统调用,包括标准输出函数以及sleep函数。否则,程序等待一个键盘输入(调用getchar()函数)后,结束运行。这里的条件跳转同.LFB6是相似的
3.3.6 函数
调用函数时有以下操作:(假设函数P调用函数Q)
(1)传递控制:进行调用 Q 的时候,PC设置为 Q 的代码的起始地址,将P中调用Q后面的那条语句地址入栈,返回时,出栈,将PC设置为P中调用Q后面的那条语句地址。
(2)传递数据:P能够向Q提供一个或多个参数,Q 能够向P中返回一个值。
(3)分配和释放内存:在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。
hello.C涉及的函数操作有:
main、printf、exit、sleep、getchar函数
如图所示
3.4 本章小结
本章主要探究了编译阶段hello.i到hello.s的转变,更深入理解了编译器对数据的一些操作
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编是由汇编器(s)将汇编代码翻译成目标代码的过程。汇编时,汇编器将每一条汇编指令(ASCLL字节流)都翻译成对应的机器码(二进制流),得到可重定位目标文件。汇编时,汇编代码中的立即数会被直接编译到二进制码当中,PC相对寻址中的地址差值也会被编译到二进制码当中,其余的一些符号例如全局变量、外部符号、局部符号的地址值此时还未确定,它们会被记录到重定位条目当中。在汇编后的目标代码内没有直接写出这些符号的地址,经链接后才能得到它们真正的地址。
4.2 在Ubuntu下汇编的命令
如图所示
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
4.3.1 典型的ELF可重定位目标文件格式
4.3.2 ELF头
以16B的序列Magic开始,Magic描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型、字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息
4.3.3 节头部表
包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。由于是可重定位目标文件,所以每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,同时可以观察到,代码是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写
4.3.4 符号表
存放程序中定义和引用的函数和全局变量的信息。name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编 如图所示
机器语言就是一系列的字节序列构成,是对一系列指令的编码。而汇编语言是机器语言的文本表示,对于汇编代码中的每一条指令,都有唯一的条与之对应 的机器代码,而对于给定的机器代码,反汇编器也能够确定出唯一的一条与之对应的汇编代码。因而汇编语言和机器语言之间存在一一对应的关系(双射)。
机器语言的反汇编代码与汇编代码之间的不一致的地方有三种:
1、从表中可以看出在汇编代码中所有以 . 开头的伪指令在反汇编代码中都被去除了。这是因为这些伪代码是用来知道汇编器工作的,而在反汇编代码是从.o文件得到的,汇编器已经完成了工作,自然在反汇编代码中不会包含这些内容。
2、汇编代码中的跳转指令是跳转到语句标号的位置,而在反汇编代码中已经替换成相对于函数起始的偏移。这是因为在汇编器生成重定位文件时,发现函数内部有语句标号,自动地将这些跳转指令的值根据PC相对寻址的规则计算了出来。
3、反汇编代码中所有的call指令跳转位置都是0,涉及到.rodata段的字符串参数地址值也是0,还有涉及到.data段的sleepsecs参数的值也是0。这是因为这些符号定义还没有和一个内存位置关联起来,所有的这些符号的引用都对应着一个重定位条目,在链接时根据重定位条目指向一个具体的内存位置。
4.5 本章小结
本章主要介绍了汇编过程,汇编概念及作用,并根据文件hello.s与生成的hello.o进行反汇编然后比较以分析了可重定位目标文件格式.elf
(第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.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
如图所示
5.3 可执行目标文件hello的格式
5.3.1 ELF头
5.3.2 节头部表
与之前的的hello.o文件的节头部表比较发现各段地址的数值已被正确填入
5.4 hello的虚拟地址空间
打开EDB运行hello,观察Data Dump窗口,虚拟地址空间由0x400000到0x400fff。其中的节与每个节头表的声明一一对应
5.5 链接的重定位过程分析
5.5.1生成的hello的反汇编代码(部分)
5.5.2与hello.o的反汇编代码相比较
(1)hello比hello.o多了许多文件节,如.init节和.plt节;
(2)hello文件中的地址是虚拟地址,而hello.o节中的是相对偏移地址;
(3)hello中增加了许多外部链接的共享库函数,如puts@plt,printf@plt等;
(4)hello中跳转和函数调用的地址是虚拟地址。
5.5.3 重定位过程分析
链接的重定位过程说明要合并相同的节,确定新节中所有定义符号在虚拟地址空间中的地址,对引用符号进行重定位(确定地址),修改.text节和.data节中对每个符号的引用(地址),而这些都需要用到在.rel_data和.rel_text节中保存的重定位信息
5.6 hello的执行流程
5.7 Hello的动态链接分析
5.7.1 通过readelf -S hello获取GOT地址
5.7.2 获取GOT表相关信息
init前:
init后:
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行地址,需要添加重定位记录,等待动态链接器的处理。
链接器采用延迟绑定的策略避免运行时修改调用的代码段。使用数据结构:PLT+GOT实现函数的动态链接,其中GOT存放目标函数的地址,而PLT使用该地址跳转到目标位置。GOT与PLT条目之间的交叉映射使得动态链接(不对代码进行重定位)成为可能、
5.8 本章小结
本章主要探究了在链接过程中可执行目标文件的ELF文件的相关信息以及重定位过程
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程:一个执行中程序的实例。
作用:为每个应用程序提供两个关键的抽象:
1、独立的逻辑控制流
2、私有的地址空间
6.2 简述壳Shell-bash的作用与处理流程
shell是一个交互型的应用级程序(命令行解释器),代表用户运行其他的程序。
基本功能是解释并执行用户输入的指令。
处理流程如下:
(1)终端进程读取用户从键盘输入的命令行。
(2)解析以空格分隔的命令行参数,并构造传递给execve的argv向量。
(3)判断是否是空行,若是则进行下一轮循环。
(4)检查第一个命令行参数是否是一个内置的shell命令,若是则直接在当前进程的上下文中执行该内置命令。
(5)若不是则调用fork()创建子进程,在子进程的上下文中,利用第(2)步构造的argv向量调用execve()执行该程序。
(6)如果用户要求在后台运行该程序,那么回到第(1)步,等待下一个命令行,否则,shell使用waitpid函数等待作业终止。当作业终止时,shell就开始下一轮迭代。
6.3 Hello的fork进程创建过程
shell父进程通过调用fork函数创建hello子进程。
新创建的子进程与父进程最大的区别在于它们有不同的PID。除此之外几乎与父进程完全相同。子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。
6.4 Hello的execve过程
当创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,加载并运行需要以下几个步骤:
(1)删除已存在的用户区域:删除当前进程虚拟地址的用户部分中已存在的区域结构。
(2)映射私有区域:为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
(3)映射共享区域:如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
(4)设置程序计数器(PC):exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
6.5 Hello的进程执行
内核为每个进程维持一个上下文(context),上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表,包含有关当前进程信息的进程表,以及包含进程已打开的文件的信息的文件表。
时间片:时间片由操作系统内核的调度程序分配给每个进程。首先,内核会给每个进程分配相等的初始时间片,然后每个进程轮番地执行相应的时间,当所有进程都处于时间片耗尽的状态时,内核会重新为每个进程计算并分配时间片,如此往复。
当进程执行的某些时刻,例如一个进程的时间片耗尽,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这个决策就叫做调度,是由内核中称为调度器的代码处理的。内核使用一种称为上下文切换的机制来将控制转移到新的进程。
上下文切换一共有三个步骤:1)保存当前进程的上下文,2)恢复某个先前被抢占的进程保存的上下文,3)将控制传递给这个新恢复的进程。
6.6 hello的异常与信号处理
6.6.1异常分类
1.中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常。
2.陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。
3.故障:在执行hello程序的时候,可能会发生缺页故障。
4.终止:终止时不可恢复的错误,在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。
6.6.2 运行测试
正常执行
Ctrl Z发送SIGTSTP信号挂起进程,ps检查:
Ctrl C发送SIGINT信号终止进程,ps检查:
kill杀死进程:
随意输入不影响进程运行
jobs列出当前暂停进程
pstree列出各个进程的树状图
6.7本章小结
本章主要描述了进程和shell,并且根据对hello程序进行一系列调试,让我对进程和shell有了更深入的理解
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址(logical address):访问指令给出的地址,是指从应用程序角度看到的内存单元的地址。逻辑地址是 selector:offset 这种形式,selector 是CS寄存器的值,offset是%eip寄存器的值。
线性地址:如果用 selector 去 GDT( 全局描述符表 ) 里拿到 segment base address(段基址) 然后加上 offset(段内偏移),这就得到了线性地址。
虚拟地址:和线性地址其实是一个意思。
物理地址:虚拟地址经MMU地址翻译后所得的最终结果地址,如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,也称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,如图:
索引号中存放的是段描述符的索引(可以理解为段号)可以分为两种,全局描述符(GDT)和局部描述符(LDT),对应着第2位,而0,1两位是表示CPU的权限级别(0-4级)。在IA-32中一共有6个段选择器
CS保存了代码段描述符的索引;
DS保存了数据段描述符的索引;
SS保存堆栈段描述符索引;
ES、FS、GS则作为一般用途,可以指向任意的数据段,实现自定义寻址。
在IA-32中,逻辑地址是16位的段选择符+32位偏移地址,段寄存器不在保存段基址,而是保存段描述符的索引。逻辑地址变换成虚拟地址的方式如图所示,具体步骤如下:
1. IA-32首先确定要访问的段,然后决定使用的段寄存器。
2. 根据段选择符号的TI字段决定是访问GDT还是LDT,他们的首地址则通过GTDR和LDTR来获得。
3. 将段选择符的Index字段(索引号)的值*8,然后加上GDT或LDT的首地址,就能得到当前段描述符的地址。(乘以8是因为段描述符为8字节)
4. 得到段描述符的地址后,可以通过段描述符中BASE获得段首地址。
5. 将逻辑地址中32位的偏移地址和段首地址相加就可以得到实际要访问的物理地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
从虚拟地址到物理地址
CPU中有一个控制寄存器,页表基址寄存器(Page Table Base Register,PTBR)指向当前的页表。N位的虚拟地址包括两个部分:一个p位的虚拟页面偏移(Virtual Page Offset,VPO)和一个(n-p)的虚拟页号(Virtual Page Number,VPN)。MMU利用VPN来选择适当的PTE(页表条目),将页表条目中的物理页号(Physical Page Number,PPN)和虚拟地址中的VPO串联起来,就得到了相应的物理地址。其中最需要注意的一点是,因为物理页面和虚拟页面都是P字节的,所以PPO和VPO是相同的。所谓页式管理就是,从内存的角度去看,内存是被划分成固定大小的页,因而不要求进程的代码段和数据段在内存中连续存放,从而有效地解决了碎片问题,更加合理地使用了稀有的内存资源。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB:翻译后备缓冲器,用来缓存页表条目,从而减小到L1高速缓存中去查找PTE的额外开销。
四级页表:因为一般的进程中虚拟内存的大部分区域都是为分配的,因而通过使用多级页表的方式来减少页表带来的内存占用。若低一级的页表条目均为空,那么就不用存储高一级的页表条目,因为用不到所以也就不用存,从而节省内存资源。
7.5 三级Cache支持下的物理内存访问
由于有64组,所以组索引CI需要6 bit,块大小为64B故组内偏移CO需要6bit。因为PA共52 bit所以剩余部分CT共40 bit。
物理内存访问时,MMU发送PA给L1缓存,高速缓存根据CI找到组、CT找到地址,并根据标记位判断该地址是否已缓存数据。若命中,则根据偏移量CO找到值取出数据后返回。若不命中,则再一次查找缓存L2、L3。如果仍不命中,则要去主存中读取数据。
7.6 hello进程fork时的内存映射
mm_struct(内存描述符):描述了一个进程的整个虚拟内存空间。
vm_area_struct(区域结构描述符):描述了进程的虚拟内存空间的一个区间。
当用fork创建一个进程时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、vm_area_struct和页表的原样副本。然后将这两个两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数加载并运行hello的步骤如下:
(1)删除当前的用户区域
(2)映射私有区域:为新程序的代码、数据、bss和栈区域创建新的区域结构。
(3)映射共享区域:hello程序与标准C库链接,这些对象动态链接到这个程序,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器。execve做得最后一件事情是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
当指令引用一个虚拟地址,在MMU中查找页表 时发现与该地址相对应的物理地址不在内存中,此时即为缺页故障。
当出发缺页故障时,处理程序执行如下的步骤:
(1)判断虚拟地址是否合法
把该虚拟地址与每个区域结构中的vm_start和vm_end作比较。如果不合法,则发送段错误信号,终止这个进程。
(2)判断试图访问的内存是否合法
如果不合法则出发一个保护进程终止进程。
(3)判断地址和访问内存都合法后
选择牺牲一个页面,如果这个页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送VA到MMU,这次MMU就能正常翻译VA了。
7.9动态存储分配管理
程序员通过动态内存分配器在运行时获取额外的虚拟内存。动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap),应用程序通过动态内存分配器来管理堆。堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对每个进程,内核维护着一个变量brk,它指向堆的顶部。
分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
7.10本章小结
本章主要介绍了hello如何存储在内存中,包括hello的虚拟地址空间,物理地址空间,地址之间的映射关系以及相互转化等内容,并以core it为例,介少了缺页故障处理等问题
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
8.1.1 设备的模型化
文件:所有的I/O设备都被模型化为文件。
8.1.2 设备管理
(1)UNIX IO接口:这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。
(2)对文件的操作包括:打开/关闭;读/写;改变当前文件位置k等。
8.2 简述Unix IO接口及其函数
8.2.1接口
(1)打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息,应用程序只需要记住这个描述符。linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。
(2)关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源.
(3)读写文件:一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号” 。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k 。
(4)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。
8.2.2 函数
(1)打开和关闭文件
int open(char* filename,int flags,mode_t mode)
若成功则为新文件描述符,否则返回-1;
int close(int fd)
若成功则返回0,否则为-1。
(2)读和写文件
ssize_t read(int fd,void *buf,size_t n)
从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。若成功则返回读的字节数,若EOF则为0,出错为-1;
ssize_t wirte(int fd,const void *buf,size_t n)
8.3 printf的实现分析
8.3.1 调用外部函数:vsprintf/write
这里,vsprintf的作用就是格式化,它接受确定输出格式的格式字符串fmt,并用格式字符串对个数变化的参数进行格式化,产生格式化输出,生成显示信息。格式化后的参数被存到buf,再由write将buf中的数据写到终端。
8.3.2 实现分析
(1)从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall;
(2)字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息);
(3)显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(1)运行到getchar函数时,程序将控制权交给os。当你键入时,内容进入缓存并在屏幕上回显。按enter,通知 os输入完成,这时再将控制权在交还给程序。
(2)异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
(3)函数getchar调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回
8.5本章小结
本章主要介绍了文件操作,包括Linux提供的基于unix I/O模型的系统级函数以及它们允许应用程序执行的包括打开/关闭/读/写等在内的各项操作;另外,还介绍了printf函数与getchar函数,它们的实现涉及复杂的系统调用和中断处理
(第8章1分)
结论
可执行程序hello最初是一个C语言源程序hello.c,使用ASCII码保存的高级语言源码;
文件hello.c经过预处理后,得到hello.i,经过预处理,处理了宏/调用库和条件编译等;
文件hello.i经历编译后,得到hello.s,保存汇编代码;
文件hello.s经历汇编后,得到hello.o(可重定位目标文件);
文件hello.o经历链接后,符号被处理,相关地址被重定位,得到可执行目标文件hello;
inux > ./hello,则shell会为hello程序fork一个子进程,调用execve加载hello并运行,此时的hello是一个子进程;
运行过程中涉及复杂的内存管理,包括虚拟内存与物理内存的映射(保存在页表),TLB和数据/指令Cache与主存的精密配合,以及缺页等异常处理;通过IO设备以及UNIX IO管理,实现用户与hello的交互;
面对可能产生的异常,内核有强大的异常与信号处理机制;最终,内核将回收进程,hello的一生也就此结束。
感悟:计算机真好玩
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello.c 源码
hello.i 对预处理指令做初步处理,如把库展开
hello.s 汇编代码文件
hello.o 可重定位目标程序
hello 可执行目标程序
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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.
(参考文献0分,缺失 -1分)