计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 英才学院计算机
学 号 120L020524
班 级 2036014
学 生 朱星默
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年5月
本论文将CSAPP课程所学内容通过hello小程序的一生,对所学进行全面的梳理与回顾。论文对hello.c文件在Linux系统下的生命周期进行了研究,依次研究编译、链接、加载、运行、终止、回收的过程。结合《深入理解计算机系统》书中的内容与课上老师的讲授,我们主要在Ubuntu下进行相关操作,合理运用了Ubuntu下的操作工具,完成对hello程序的整个生命周期的分析,加深了我们对计算机系统的了解。
关键词:hello;Ubuntu;计算机系统;计算机体系结构;程序生命周期;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
2.2在Ubuntu下预处理的命令............................................................................. - 5 -
5.3 可执行目标文件hello的格式........................................................................ - 8 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 10 -
6.3 Hello的fork进程创建过程......................................................................... - 10 -
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 -
8.2 简述Unix IO接口及其函数.......................................................................... - 13 -
第1章 概述
1.1 Hello简介
P2P是指hello.c文件从可执行程序(Program)变为运行时进程(Process)的过程。在Linux系统下,hello.c 文件依次经过cpp(C Pre-Processor,C预处理器)预处理、ccl(C Compiler,C编译器) 编译、as (Assembler,汇编器)汇编、ld (Linker,链接器)链接最终成为可执行目标程序hello(在Linux下该文件无固定后缀)。打开shell,输入命令./hello后,shell 通过fork产生子进程,hello 便从可执行程序(Program)变成为进程(Process)
020是指hello.c文件“From 0 to 0”,初始时内存中并无hello文件的相关内容,这便是“From 0”。通过在Shell下调用execve函数,系统会将hello文件载入内存,执行相关代码,当程序运行结束后, hello进程被回收,并由内核删除hello相关数据,这即为“to 0”
1.2 环境与工具
1.2.1 硬件环境
处理器:AMD Ryzen 7 4700U with Radeon Graphics 2.00 GHz
RAM :8.00 GB
系统类型:64 位操作系统, 基于 x64 的处理器
1.2.2 软件环境
VMware16 、 Ubuntu 20.04 、 Windows10 64位
1.3 中间结果
文件的作用 | 文件名 |
预处理后的文件 | hello.i |
编译之后的汇编文件 | hello.s |
汇编之后的可重定位目标文件 | hello.o |
hello的ELF 格式 | hello.elf |
由hello可执行文件生成的.elf文件 | hello2.elf |
1.4 本章小结
本章介绍了P2P、020的意义和过程,作业中的硬件环境、软件环境和开发工具,简述了从.c文件到可执行文件中间经历的过程
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:
预处理程序是根据源代码中的预处理指令修改你的源代码。预处理指令是一种命令语句(如#define),它指示预处理程序如何修改源代码。在对程序进行通常的编译处理之前,编译程序会自动运行预处理程序,对程序进行编译预处理,这部分工作对程序员来说是不可见的。
预处理的作用:
1.程序的预处理过程就是将预处理指令(可以简单理解为#开头的正确指令)转换为实际代码中的内容(替换)
2.#include<stdio.h>,这里是预处理指令,包含头文件的操作,将所包含头文件的指令替代
3.如果头文件中包含了其他头文件,也需要将头文件展开包含
4.用实际值替换用#define 定义的字符串
5.如果源代码中包含条件预处理指令(如#if),那么预处理程序将先判断条件,再相应地修改源代码
2.2在Ubuntu下预处理的命令
在Ubuntu系统下,进行预处理的命令为: cpp hello.c > hello.i
2.3 Hello的预处理结果解析
在Linux下打开hello.i文件,可以发现hello.i程序已经拓展为3060行。其中, hello.c中的main函数相关代码在hello.i程序中对应着3047行到3060行。
在这之前出现的是头文件 stdio.h unistd.h stdlib.h 的依次展开。以stdio.h为例,是这样展开的:CPP先删除指令#include <stdio.h>,然后到Ubuntu系统的环境变量中寻找 stdio.h,并打开路径/usr/include/stdio.h下的stdio.h文件。若stdio.h文件中使用了#define语句,则按照上述流程继续对 stdio 中的define 宏定义递归地展开,直到所有#define语句都被解释替换掉为止。CPP还会进行删除程序中的注释和多余的空白字符等操作,并对一些值进行替换。预编译程序可识别一些特殊的符号,预编译程序对在源程序中出现的这些串将用合适的值进行替换。
2.4 本章小结
本章主要介绍了预处理(包括头文件的展开、宏替换、去掉注释、条件编译)的概念及作用、并结合Ubuntu系统下hello.c文件实际预处理之后得到的hello.i程序对预处理结果进行了解析
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念:
1、利用编译程序从源语言编写的源程序产生目标程序的过程。
2、用编译程序产生目标程序的动作。
编译语言是一种以编译器来实现的编程语言。它不像直译语言一样,由解释器将代码一句一句运行,而是以编译器,先将代码编译为机器码,再加以运行。理论上,任何编程语言都可以是编译式,或直译式的。它们之间的区别,仅与程序的应用有关。通过编译过程,编译器将文本文件 hello.i 翻译成汇编语言文件 hello.s
编译的作用:
编译就是把高级语言变成计算机可以识别的2进制语言,计算机只认识1和0,编译程序把人们熟悉的语言换成2进制的。 编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
3.2 在Ubuntu下编译的命令
在Ubuntu系统下命令为:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1文件结构
内容 | 含义 |
.file | 源文件 |
.text | 代码段 |
.global | 全局变量 |
.data | 存放已经初始化的全局和静态C 变量 |
.section .rodata | 存放只读变量 |
.align | 对齐方式 |
.type | 表示是函数类型/对象类型 |
.size | 表示大小 |
.long .string | 表示是long类型/string类型 |
3.3.2数据
3.3.3算术操作
汇编语言中,算数操作的指令包括:
指令 | 效果 |
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]=s*r[%rax](有符号) |
mulq s | r[%rdx]:r[%rax]=s*r[%rax](无符号) |
idivq s | r[%rdx]=r[%rdx]:r[%rax] mod s(有符号) r[%rax]=r[%rdx]:r[%rax] div s |
divq s | r[%rdx]=r[%rdx]:r[%rax] mod s(无符号) r[%rax]=r[%rdx]:r[%rax] div s |
3.3.3.1赋值操作
对局部变量int i的赋值在汇编代码中通过mov指令完成。具体使用哪条mov指令由数据的大小决定
3.3.3.4关系操作和控制转移
3.4 本章小结
本章介绍了编译的概念与作用。同时,本章以hello.s文件为例,介绍了编译器如何处理各个数据类型以及各类操作。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念
汇编是指汇编器(assembler)将汇编语言(hello.s)翻译成机器语言(hello.o)的过程,同时这个机器语言文件也是可重定位目标文件。
作用
汇编就是将汇编语言翻译为机器语言,转化为代码文件,汇编器将.s 汇编程序翻译成机器语言指令,并将相关指令以可重定位目标程序格式保存在.o文件中。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。首先,在shell中输入readelf -a hello.o > hello.elf 指令获得 hello.o 文件的 ELF 格式:
4.3.1 ELF头
ELF 头以 16字节序列 Magic 开始,描述生成该文件的系统的字的大小和字节顺序,还包含帮助链接器语法分析和解释目标文件大小、目标文件类型、机器类型、节头部表的文件偏移、节头部表中条目的大小和数量等信息。
4.3.2节头
包含了.o文件中出现的各个节类型、位置、所占空间大小等信息。
4.3.3重定位节
通过重定位节在链接时对位置的地址进行修改。链接器会通过重定位条目的类型判断该使用什么方法计算正确的地址值。
在这里,重定位信息分别是对:.rodata中的模式串,puts,exit,printf,slepsecs,sleep,getchar进行重定位声明。
.rela.text节包含如下信息:偏移量、信息(包括两部分,其中symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位的类型)、重定位到的目标的类型、计算重定位位置的辅助信息即加数。
4.3.4符号表
存放定位、重定位程序中定义和引用的函数和全局变量的信息
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
4.4.1数的表示
hello.s中的操作数时十进制,hello.o反汇编代码中的操作数是十六进制。
4.4.2分支转移
在hello.s中,跳转指令的目标地址直接记为段名称,如.L2,.L3等。而在反汇编中跳转指令之后是相对偏移的地址跳转的目标为具体的地址,也即间接地址。在机器代码中体现为目标指令地址与当前指令下一条指令的地址之差。
4.4.3函数调用
hello.s中,call之后直接是函数名称,指令直接使用函数名称,而反汇编代码中call 的目标地址是当前指令的下一条指令,指令使用main函数的相对偏移地址。因为因为 hello.c 中调用的函数都是共享库中的函数,最终函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目,等待静态链接进一步确定。
4.5 本章小结
本章介绍了汇编的概念与作用,将hello.s文件翻译为hello.o文件,并生成hello.o的ELF格式文件hello.elf,研究了ELF格式文件的具体结构。比较hello.o的反汇编代码(保存在hello.asm中)与hello.s中代码,并研究了汇编语言与机器语言的异同。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:
链接是指通过链接器(Linker),将各种不同文件的代码和数据收集并整理成一个单一文件,生成完全链接的可执行的目标文件的过程。
作用:
可以将程序编写为一个较小的源文件的集合,节省空间,提供了一种模块化的方式,令分离编译成为可能,减少整体文件的复杂度与大小,方便对某一模块进行针对性修改,增加容错性。
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
输入命令 readelf -a hello > hello1.elf 生成 hello 程序的 ELF 格式文件
5.3.1ELF头
Hello1.elf中的ELF头与hello.elf中的ELF头包含的信息种类基本相同,而类型发生改变,程序头大小和节头数量增加,并且获得了入口地址。
5.3.2节头
包含各个节的大小、偏移量等信息。
5.3.3程序头
一个描述了系统准备程序执行所需的段或其他信息的结构数组。
5.3.4 Dynamic section
5.3.5 Symbol table
所有重定位需要引用的符号都在其中声明
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
地址0x400000~0x401ff0,根据表头对照,找到第一个地址
Main函数也由.init中启动语段开启,对应关系正确
5.5 链接的重定位过程分析
执行objdump -d -r hello 得到反汇编,分析hello与hello.o的不同,说明链接的过程。结合hello.o的重定位项目,分析hello中对其怎么重定位的。
通过对比hello与hello.o发现不同之处:
1.增加了函数:
在hello中链接加入了puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等函数的执行语句代码,如下图:
2.增加的节:
增加了.init和.plt节
3.函数调用:
对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此添加了重定位条目。
4. 反汇编后call调用的值变为一个明确的虚拟地址空间
链接的过程:
链接就是链接器将各个目标文件(各种.o文件)组装在一起。重定位一般分作两步:重定位节和符号定义;重定位节的符号引用,依靠重定位条目选择合适方式计算
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
子函数名和地址(后6位)
通过edb的调试,记录call命令进入的函数,参照symbolviewer。
5.7 Hello的动态链接分析
(以下格式自行编排,编辑时删除)
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
指向的就是正确的内存地址。如果地址已经确定,就直接执行函数;如果没有确定好,就会回到.plt的开头位置去调用链接器ld来把地址确定好之后,再执行具体的函数。
5.8 本章小结
本章中介绍了链接的概念与作用、得到了链接后的hello可执行文件的ELF格式文本,分析了与hello.o反汇编的不同。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:
狭义定义:进程就是一段程序的执行过程。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
进程的作用:
1.每次运行程序时,shell创建一新进程,在这个进程的上下文切换中运行这个可执行目标文件。应用程序也能够创建新进程,并且在新进程的上下文中运行它们自己的代码或其他应用程序。
2.进程提供给应用程序的关键抽象:一个独立的逻辑控制流;一个私有的地址空间。
6.2 简述壳Shell-bash的作用与处理流程
Shell:
Shell 是一个用 C 语言编写的程序,它是用户使用 Linux 的桥梁,是一种交互型的应用级程序,它可以代表用户执行程序。
功能:
执行一系列的读/求值步骤然后终止。求值步骤读取来自用户的一个命令行,求值步骤解析该命令行,并代表用户执行程序。在解析命令后,如果是内置命令,则解析指令并执行,否则就根据相关文件路径执行可执行目标文件。Shell为我们提供了一个界面,用户通过这个界面访问操作系统内核的服务。
处理流程:
1)从终端读入输入的命令。命令行是一串 ASCII 字符由空格分隔。字符串的第一个单词是一个可执行程序,或者是 shell 的内置命令。命令行的其余部分是命令的参数。
2)将输入字符串切分获得所有的参数
3)如果第一个单词是内置命令,shell 会立即在当前进程中执行。否则,shell 会新建一个子进程,然后再子进程中执行程序。新建的子进程又叫做作业。通常,作业可以由 Unix 管道连接的多个子进程组成。如果命令行以 &符号结尾,那么作业将在后台运行,这意味着在打印提示符并等待下一个命令之前,shell 不会等待作业终止。否则,作业在前台运行,这意味着 shell 在作业终止前不会执行下一条命令行。
4)shell 应该接受键盘输入信号,并对这些信号进行相应处理。
6.3 Hello的fork进程创建过程
当shell判断输入命令是一个可执行程序后(一般为 ./ 开头的命令),执行中的进程调用fork()系统函数。父进程通过调用 fork()函数创建一个新的运行的子进程。fork()函数的返回值分以下情况:若在子进程中,返回0;若在父进程中,则返回子进程的 PID。子进程与父进程近似,并得到一份与父进程用户级虚拟空间相同且独立的副本——包括数据段、代码、共享库、堆和用户栈,二者之间最大的不同或许在于PID的不同。
6.4 Hello的execve过程
execve函数加载并运行Hello文件。函数作用就是在当前进程的上下文中加载并运行一个新的程序。只有当出现错误时,execve才会返回到调用程序。加载了Hello之后,execve函数调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数:int main(intargc , char **argv , char *envp);
进程步骤大致如下:
1.删除已存在的用户区域(自父进程独立)。
2.映射私有区:为Hello的代码、数据、.bss和栈区域创建新的区域结构。
3.映射共享区:如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
4.设置PC:设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
6.5.1逻辑控制流
程序计数器 PC 的值的序列叫做逻辑控制流。逻辑控制流是程序创造的第一个抽象,由于进程是轮流使用处理器的,同一个处理器每个进程执行它的流的一部分后被抢占,然后这个进程被挂起轮到其他进程。
6.5.2 并发流
一个逻辑流在时间线上与另一个正在执行的逻辑流发生重合,则成为了并发流。
6.5.3用户模式和内核模式
处理器使用一个寄存器提供两种模式的区分。
用户模式:进程不允许执行特殊指令,不可以直接引用地址空间中内核区的代码和数据;
内核模式:进程可以执行指令集中的任何命令,可以访问系统中的任何内存位置。
6.5.4 上下文切换
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态。
sleep进程的执行:
6.5.5调度的过程
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这是由内核中调度器的代码处理的,叫做调度。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
6.5.6用户态与核心态转换
核心态拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,让处理器能安全运行,不至于损坏操作系统,保证了系统的安全性。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.6.1正常运行状态:
6.7本章小结
本章详细介绍了进程管理的相关事项。了解了hello进程的执行过程。让我们进一步了解了Linux系统下进程的开始和终结过程。涉及到进程,shell的概念和作用,hello这个可执行文件开始执行后产生的进程情况,当接受到不同的异常信号时,异常处理程序将对异常信号做出相应,执行相应的代码,每种信号都有不同的处理机制,对不同的异常信号,hello不同的处理结果。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
- 逻辑地址
逻辑地址是指由程序产生的与段相关的偏移地址部分,逻辑地址由选择符和偏移量两部分组成。hello.asm中的相对偏移地址是逻辑地址。
- 线性地址
逻辑地址经过段机制转化后为线性地址,其为处理器可寻址空间的地址,用于描述程序分页信息的地址。在hello中线性地址标志着程序应在内存上哪些具体数据块上运行。
- 虚拟地址
CPU启动保护模式后,程序运行在虚拟地址空间中。但并不是所有的“程序”都是运行在虚拟地址中。
- 物理地址
真实的物理内存对应地址,CPU通过地址总线的寻址找到。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。索引号,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。DT在内存中的地址和大小存放在CPU的gdtr控制
寄存器中,而LDT则在ldtr寄存器中。
将段基址与段内偏移量的相应数值相加,就可以得到对应的线性地址。这种操作叫做段式管理。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
动态页式管理提供了内存和外存统一管理的虚存实现方式,使用户可以利用的存储空间大大增加。这既提高了主存的利用率,又有利于组织多道程序执行。但要求有相应的硬件支持。这增加了机器成本。虽然消除了碎片,但每个作业或进程的最后一页内总有一部分空间得不到利用果页面。
7.4 TLB与四级页表支持下的VA到PA的变换
VA:virtual address称为虚拟地址
PA:physical address称为物理地址
MMU:memory management unit内存管理单元
PTE:页表条目
每次CPU产生一个虚拟地址,MMU就必须查阅一个PTE,以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。
多级页表:
进入main函数时,CPU发出的内核信号将被MMU截获,将这个信号从虚拟地址映射到物理地址之中。将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。解析VA,利用前m位vpn1寻找一级页表位置,接着一次重复k次,在第k级页表获得了页表条目,将PPN与VPO组合获得PA。
7.5 三级Cache支持下的物理内存访问
CPU发送一条虚拟地址,随后MMU按照上述操作获得了物理地址PA。根据cache大小组数的要求,将PA分为CT(标记位)CS(组号),CO(偏移量)。根据CS寻找到正确的组,比较每一个cacheline是否是一个有效的标记位;如果命中,则直接返回所需要的值;如果不命中,则去L2 、L3、主存中继续寻找。匹配成功后,数据传给CPU同时更新各级cache的cacheline,向上一级逐渐返回,直至返回至L1。
7.6 hello进程fork时的内存映射
fork函数调用执行时,内核为新进程创建各种数据结构、创建虚拟内存,并分配给它新的PID,创建当前子进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存地址和最开始创建时的虚拟内存地址相同。完成了一次内存的“映射”,为每个进程保持了私有空间地址的抽象概念。。
7.7 hello进程execve时的内存映射
execve函数的结构:int execve(char *filename, char *argv[], char *envp[]),其函数作用就是在当前进程的上下文中加载并运行一个新的程序。只有当出现错误时,execve才会返回到调用程序。三个参数的意义如下:
1)filename:可执行文件目标文件或脚本(用#!指明解释器)
2)argv:参数列表,惯例
3)envp:环境变量列表
进程步骤大致如下:
1.删除已存在的用户区域(自父进程独立)。
2.映射私有区:为Hello的代码、数据、.bss和栈区域创建新的区域结构。
3.映射共享区:如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
4.设置PC:设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
7.8.1 缺页故障的定义
7.9动态存储分配管理
动态内存分配器用来进行动态储存分配管理,它维护着一个进程的虚拟内存区域即堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用,空闲块可以用来分配。空闲块在被应用所分配前保持空闲,已分配的块在它被释放前保持已分配的状态。动态内存分配主要有两种基本方法与策略:带边界标签的隐式空闲链表分配器管理,显示空间链表管理。
下面介绍一些相关概念:
- 隐式链表
堆中的空闲块通过头部中的大小字段隐含地连接,分配器通过遍历堆中所有的块,从而间接遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。当一个应用请求一个k字节的块时,分配器搜索空闲链表查找一个可以放置所请求块的空闲块。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以便利用隐式空闲链表的边界标记来进行合并空闲块。
- 显式链表
在每个空闲块中,都包含前驱(pred)与后继(succ)指针。显式空闲链表是将空闲块组织为某种形式的显式数据结构。在显式空闲链表中可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址。
- 带边界标记的合并
采取使用边界标记的堆块的格式,在堆块的末尾为其添加一个脚部,分配器就可以通过检查前面一个块的脚部,判断前面一个块的起始位置和状态。这是一种通过实现快速合并,实现性能消耗减小优化的方式。
- 分离存储
维护每个链表的块具有相同的大小的多个空闲链表,将所有可能的块大小分成一些等价类进行分离存储。
7.10本章小结
本章主要介绍了hello 的存储器地址空间、intel 的段式管理、hello 的页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理相关内容。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,所有 IO 设备都被当作文件。我们可以像对文件一样操作:打开关闭操作open和close;读写操作read和write;改变当前文件位置lseek等
8.2 简述Unix IO接口及其函数
8.2.1 Unix IO接口:
1.打开文件
一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。对于Shell创建的每个进程,其都有三个打开的文件:标准输入,标准输出,标准错误。
2.改变当前的文件位置
对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
3.读写文件
一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
4.关闭文件
内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。8.2.2 Unix IO函数:
1. open()函数
功能描述:用于打开或创建文件。
函数原型:int open(const char *pathname,int flags,int perms)
参数:pathname:被打开的文件名;flags:文件打开方式,
返回值:成功:返回文件描述符;失败:返回-1
2. close()函数
功能描述:用于关闭一个被打开的的文件
所需头文件:#include <unistd.h>
函数原型:int close(int fd)
参数:fd文件描述符
函数返回值:0成功,-1出错
3. read()函数
功能描述:从文件读取数据。
所需头文件:#include <unistd.h>
函数原型:ssize_t read(int fd, void *buf, size_t count);
参数:fd:文件描述词;buf:指缓冲区;count:调用一次read读多少字符。
返回值:返回所读取的字节数;0(读到EOF);-1(出错)。
4. write()函数
功能描述:向文件写入数据。
所需头文件:#include <unistd.h>
函数原型:ssize_t write(int fd, void *buf, size_t count);
返回值:写入文件的字节数(成功);-1(出错)
5. lseek()函数
功能描述:用于在指定的文件描述符中将将文件指针定位到相应位置。
所需头文件:#include <unistd.h>,#include <sys/types.h>
函数原型:off_t lseek(int fd, off_t offset,int whence);
参数:fd;文件描述符。offset:偏移量,每一个读写操作所需要移动的距离
返回值:成功:返回当前位移;失败:返回-1
8.3 printf的实现分析
printf函数:接受一个格式化的命令,并把指定的匹配的参数格式化输出。
可以看到其中调用了一个vsprintf函数:接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
getchar源码:
8.5本章小结
本章主要介绍了linux的IO设备管理方法和及其接口和函数,printf函数和getchar函数的实现。
(第8章1分)
结论
hello.c程序的一生:
1. 经过预处理,从hello.c变成了hello.i
2. 经过编译,从hello.i变成了hello.s
3. 经过汇编,从hello.s变成了可重定位文件hello.o
4. 经过链接,从hello.o变成了初级完全体hello的可执行文件
5. 通过shell,按照格式输入./hello 120L020524朱星默 1 让程序开始执行
6.首先分析输入命令,并作出相应操作:bash进程调用fork函数生成子进程;并由execve函数加载运行当前进程的上下文中加载并运行新程序hello
7. hello的变化过程中,会有各种地址,但最终我们真正期待的是PA物理地址
8. hello再运行时会调用一些函数,比如printf函数,这些函数与linux I/O的设备模拟化密切相关
9. hello最终被shell父进程回收,内核会收回为其创建的所有信息,hello至此也结束了他的一生
CSAPP介绍了计算机系统的基本概念,包括最底层的内存中的数据表示、流水线指令的构成、虚拟存储器、编译系统、动态加载库,以及用户应用等。书中提供了大量实际操作,可以帮助读者更好地理解程序执行的方式,改进程序的执行效率。hello.c程序在Linux系统中的一生,带着我们遨游了一次Linux系统下,程序普遍运行的规律。我们HITers要成为优秀的程序员,而不是码农。我们基于实践在学习计算机,然而却也要基于理论,总之,这一门课程让我收获颇多。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
文件的作用 | 文件名 |
预处理后的文件 | hello.i |
编译之后的汇编文件 | hello.s |
汇编之后的可重定位目标文件 | hello.o |
hello的ELF 格式 | hello.elf |
由hello可执行文件生成的.elf文件 | hello2.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] 汇编指令大全 CSDN 汇编语言指令大全_Andrewniu的博客-CSDN博客_汇编指令大全
[8] 链接器 百度百科
[9] 进程 百度百科
[10] 逻辑地址、线性地址和物理地址的关系 CSDN
逻辑地址、线性地址和物理地址的关系_5G砖家的博客-CSDN博客_线性地址和物理地址