计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号
班 级
学 生
指 导 教 师 史先俊
计算机科学与技术学院
2022年5月
摘 要
本论文在本学期学完计算机系统课程之后,对经典书目深入理解计算机系统有了更深的理解,本文在此基础上阐述hello程序编译、运行的过程。
关键词:计算机系统;程序运行过程;
目 录
第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 4 -
第2章 预处理 - 5 -
2.1 预处理的概念与作用 - 5 -
2.2在UBUNTU下预处理的命令 - 5 -
2.3 HELLO的预处理结果解析 - 5 -
2.4 本章小结 - 5 -
第3章 编译 - 6 -
3.1 编译的概念与作用 - 6 -
3.2 在UBUNTU下编译的命令 - 6 -
3.3 HELLO的编译结果解析 - 6 -
3.4 本章小结 - 6 -
第4章 汇编 - 7 -
4.1 汇编的概念与作用 - 7 -
4.2 在UBUNTU下汇编的命令 - 7 -
4.3 可重定位目标ELF格式 - 7 -
4.4 HELLO.O的结果解析 - 7 -
4.5 本章小结 - 7 -
第5章 链接 - 8 -
5.1 链接的概念与作用 - 8 -
5.2 在UBUNTU下链接的命令 - 8 -
5.3 可执行目标文件HELLO的格式 - 8 -
5.4 HELLO的虚拟地址空间 - 8 -
5.5 链接的重定位过程分析 - 8 -
5.6 HELLO的执行流程 - 8 -
5.7 HELLO的动态链接分析 - 8 -
5.8 本章小结 - 9 -
第6章 HELLO进程管理 - 10 -
6.1 进程的概念与作用 - 10 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 10 -
6.3 HELLO的FORK进程创建过程 - 10 -
6.4 HELLO的EXECVE过程 - 10 -
6.5 HELLO的进程执行 - 10 -
6.6 HELLO的异常与信号处理 - 10 -
6.7本章小结 - 10 -
第7章 HELLO的存储管理 - 11 -
7.1 HELLO的存储器地址空间 - 11 -
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 -
7.9动态存储分配管理 - 11 -
7.10本章小结 - 12 -
第8章 HELLO的IO管理 - 13 -
8.1 LINUX的IO设备管理方法 - 13 -
8.2 简述UNIX IO接口及其函数 - 13 -
8.3 PRINTF的实现分析 - 13 -
8.4 GETCHAR的实现分析 - 13 -
8.5本章小结 - 13 -
结论 - 14 -
附件 - 15 -
参考文献 - 16 -
第1章 概述
1.1 Hello简介
P2P:from Program to Process 在Linux系统中,hello.c经过cpp的预处理,ccl的编译,as的汇编,ld的链接最终成为可执行目标程序Hello。在shell键入启动命令后,shell为其fork生成子进程,使得hello程序转变为进程。
020:Zero to zero shell为创建的子进程execve,映射到虚拟内存,进入程序的入口后开始载入物理内存,进入main函数执行目标代码,当进程结束后,shell父进程负责回收hello进程,内核彻底删除相关的数据结构。
1.2 环境与工具
硬件环境:X64 CPU 2GHz 2G RAM 256GHD Disk以上
软件环境:Windows11 64位
开发与测试工具:gcc vim edb readelf
1.3 中间结果
Hello.i hello.c经预处理得到的.i文件
Hello.s hello.i编译后得到的.s文件
Hello.o hello.s汇编后得到的.o文件
Hello hello.o经链接后得到的可执行文件
Hello.out hello经反汇编得到的可重定位文件
1.4 本章小结
本章介绍了hello.c文件从编译到运行的全过程,叙述了各个结算的流程,列举了中间过程的各个文件,并介绍了实验的主要环境,是对本大作业的概括叙述。
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:预处理是预处理器(cpp)根据以#开头的命令,修改原始的C程序。比如hello.c中的第一行#include
预处理的作用:
1、 宏定义指令
对于#define a b类的指令,预编译会将程序中的所有a用b替换,但a作为字符串常量则不被替换。
2、 条件编译指令
#ifdef #ifnef #endif等伪指令的引入让程序员可以通过定义不同的宏来决定编译程序对哪些代码可以进行处理,预编译将根据有关的文件将不必要的代码过滤掉。
3、 头文件包含指令
采用头文件是为了使某些定义可以供多个C源程序使用,因为在需要用到这些定义的C源程序中,只需要加上一条include语句即可。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。包含C源程序中的头文件可以是系统提供的,开发人员可以用系统提供的文件,也可以用自己定义的头文件,这些文件一般与C源程序放在同一目录下,此时在#include中要用“”
4、 特殊符号,预编译程序可以识别一些特殊的符号
在源程序中出现的LINE标识将被解释为当前行号,FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
2.2在Ubuntu下预处理的命令
预处理命令:gcc hello.c -E -o hello.i
可以看到执行预处理命令之后文件夹中增加了hello.i文件
2.3 Hello的预处理结果解析
可以看到原本短短几十行的hello.c文件经预处理后增加到三千多行
2.4 本章小结
本章阐述了预处理的概念和作用,介绍了预处理的指令,并对预处理前后的文件进行了对比分析,了解并验证了预处理执行的功能。
第3章 编译
3.1 编译的概念与作用
编译的概念:编译就是把源代码翻译成计算机可以识别的二进制语言,也就是机器代码。
编译的作用:编译器将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序,以高级程序语言书写的源程序作为输入,而以汇编语言或者机器语言表示的目标程序作为输出。编译就是把代码转为机器可以识别的汇编指令地果醋哼,汇编指令只是CPU相关的,不同语言书写的代码,如果逻辑功能相同,那么编译完生成的汇编代码应该是一样的。
汇编程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析,语法分析,语义检查和中间代码生成,代码优化,目标代码生成。
主要是进行词法分析和语法分析,如果发现语法错误则给出提示信息,编译语言是一种以编译器来实现的编程语言,由编译器先将代码翻译为机器码再加以运行。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
可以看到执行问编译命令后文件夹中增加hello.s文件
3.3 Hello的编译结果解析
一、数据
1、字符串
如图 在程序开头可以看到程序中使用的两个字符串及其内容
2、全局函数
如图 程序最开始对全局函数main声明
3、局部变量
如图 hello程序中用来计数的变量被存储在-4(%rbp)中,相当于局部变量的定义
4、立即数
程序中以$开头的为立即数
二、赋值
这个程序中主要是对计数器复制,和将立即数保存到对应的寄存器中,复制操作在程序中通过movx实现,其中x与被操作数据的类型有关,b为字节,w为双字,l为四字,q为八字。
三、类型转换
如图 在这里进行了类型转换,将字符串转化为整型,Linux下也支持其他类型间的相互转化。
四、数组操作
如图 汇编语言中对数组的操作为:先定义数组的首地址为-32(%rbp),mov指令将地址传递给寄存器%rax后通过增加%rax的值进行对数组元素的访问,即为加十六后只想数组中的第二个元素,在恢复首地址。
五、算术操作
如图 分别进行了减法和加法,除此之外汇编语言还支持其他的算术操作,比如乘、除、算数移位、逻辑移位。
六、关系操作
在c源代码中存在比较操作,在Linux汇编语言中转化为如图cmp指令,满足条件则会改变相应的条件码,跳转指令通过判断相应的条件码决定是否跳转到相应的位置继续执行指令。
七、控制转移
控制转移和关系操作关系密切,一般一起使用,见六中的图
八、函数操作
调用函数(P调用Q)时有以下操作:
1、 传递控制:进行Q时,计数器(pc)必须设置为Q的代码的起始地址,在返回时要把pc设置为P中调用Q的Q的下一条指令的地址
2、 传递数据:P必须能够向Q提供一个或多个参数,Q必须能够向P中返回一个值
3、 分配和释放内存:调用开始时,Q可能要为局部变量分配空间,而在返回时,又必须释放这些空间。
3.4 本章小结
本章讲述了编译阶段编译器如何处理c源代码中各种数据和操作,以及c语言中各种类型和操作所对应的汇编代码。通过了解编译器编译的机制可以让我们读懂汇编代码,将汇编代码转换为C语言。
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编指把汇编语言代码翻译成目标机器指令的过程。对被翻译系统处理的每一个C语言程序都将最终经过这一处理得到相应的目标文件。目标文件中存放的是与源程序等效的机器语言代码。目标文件由段组成,通常至少包含两个段:
1、 代码段:主要存放的是程序的指令,该段一般是可读且可执行的,但一般不可写。
2、 数据段:主要存放程序运行过程中要用到的各种全局变量或者静态的数据,一般数据段都是可读可写可执行。
Unix环境中有三种类型的目标文件:
1、 可重定位文件
2、 共享的目标文件
3、 可执行文件
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
Gcc hello.s -c -o hello.o
执行后可以看到文件夹中增加.o文件
4.3 可重定位目标elf格式
首先执行readelf hello.o -a命令,打印相关信息,如图所示。
1、 ELF Header 如图所示
ELF头以一个十六字节的magic开始,让操作系统和编译器辨别出这个文件是ELF二进制库,然后给出程序的文件类型和机器类型,以及字节头部表的文件偏移,节头部表中条目的大小和数量等信息。
2、 section header 如图所示
可以看到section header有十四个节,节信息不需要复制到虚拟地址空间中为可执行文件创建最终进程映像,但是该信息在二进制文件中仍然存在。
每个节都指定了一个类型PROGBITS表示程序必须解释的信息 SYMTAB表示符号表 REL表示重定位信息 STRTAB用于存储与ELF格式有关的字符串,但是和程序没有直接关联
每个节都是指定大小和在二进制文件中的偏移量的,address字段可用于指定节加载到虚拟地址空间中的位置。
3、 relocation section 如图所示
重定位节:包含.text节中需要进行重定位的信息
偏移量:重定位入口的偏移
信息:包含symbol与type两个信息
类型:告知链接器应该如何修改新的应用
符号值:符号值是每个符号待修改的新偏移量
符号名称:给出需要重定位的符号的名称
4、 symbol table符号表 如图所示
存放程序中定义和引用的函数和全局变量的信息
Name是符号名称,value是符号相对于目标节的起始位置偏移,size是目标的大小,type是数据或者函数,bind字段表明符号是本地还是全局的。
4.4 Hello.o的结果解析
运行objdump -d -r hello.o,得到hello.o的反汇编代码,如图所示。
与第三章的hello.s对比 hello.s如图所示
通过比较反汇编代码和hello.s可以发现,二者在语句语法等方面没有过多区别,但反汇编代码除了展示汇编代码之外还会展示机器代码。机器指令由操作码和操作数构成,汇编语言是人们熟悉的语句直接表述CPU的动作。每条汇编语言操作码都可以用二进制数据表示,可以将汇编语言与二进制机器语言建立一一映射的关系。
4.5 本章小结
本章对汇编的概念与作用和在Ubuntu下汇编的命令,以及可重定位目标elf的格式进行了分析与阐述,对比了反汇编代码和hello.s间的差异。
第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
执行后发现链接成了hello文件如图所示。
可以发现执行后多了hello文件
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
1. ELF Header
如图所示
在编译后与原来的汇编后的表的格式是一样的,仍然以16个字节的magic开始,但是最后几行发生变化,是因为链接后添加了新的内容。
2. section header
节头表包含的节数增加,变成二十七个,每个节中都制定了一个类型,每个节中的信息,比如大小、在虚拟内存中的位置、在二进制文件中的偏移量都清晰可见flags代表如何访问或者处理,例如代码段是可读可执行的,数据段是可读可写的,只读数据段是只读不可写的。
3. 重定位表
4. 符号表
Name:符号名称,value:符号相对于目标节起始位置偏移,size:目标的大小,type:目标的大小,bind:目标是全局的还是本地的
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
5.5 链接的重定位过程分析
(以下格式自行编排,编辑时删除)
1. objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
查看代码发现,call指令调用的语句与机器码都有了具体的地址预期,程序中所有的符号都完成了重定位,增加了的系统级的函数调用,增加了代码的数量。代码如图所示
链接的过程包括符号解析和重定位两部分,如图所示:
2.结合hello.o的重定位项目,分析hello中对其怎么重定位的。
重定位有两个步骤:(1)重定位节和符号定义:链接器将所有相同类型的节合并为同一类型的新的聚合节,例如来自.data节全部合并成一个节,这个节成为可输出的.data节。然后链接器将运行时的内存地址赋给新的聚合节,赋给输入模块定义的每一个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。
(2)重定位节中的符号引用 在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时的地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
子程序名:
ld-2.27.so!_dl_start
ld-2.27.so!_dl_init
hello!_start
libc-2.27.so!__libc_start_main
libc-2.27.so!__cxa_atexit
libc-2.27.so!__libc_csu_init
libc-2.27.so!_setjmp
hello!main
hello!puts@plt
hello!exit@plt
hello!printf@plt
hello!sleep@plt
hello!getchar@plt
ld-2.27.so!_dl_runtime_resolve_xsave
ld-2.27.so!_dl_fixup
ld-2.27.so!_dl_lookup_symbol_x
libc-2.27.so!exit
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
动态链接库中的pic函数,编译器无法预
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:异常是允许操作系统内核提进程概念的基本构造块,进程是计算机科学中最深刻最成功的概念之一。
进程的经典定义就是一个执行中程序的实例,系统中的每个程序都运行在某个进程的上下文中,(上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
每次用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个进程的上下文中运行这个可执行目标文件,应用程序也能够创建新进程,并且这个新进程的上下文中运行它们自己的代码或其他应用程序。进程提供给应用程序的关键抽象是:一个独立的逻辑控制流:它提供一个假象,好像我们的程序独占地使用处理器,以及一个私有的地址空间:好像我们的程序独占地使用内存系统。
(以下格式自行编排,编辑时删除)
6.2 简述壳Shell-bash的作用与处理流程
1、shell-bash的作用
是一个C语言程序,代表用户执行进程,交互性地解释和职系那个用户输入的命令,能够通过调用系统级函数或功能执行程序,建立文件,进行并从操作等,也能够协调程序间的运行冲突,保证程序能够以并行形式高效执行bash还提供了图形化界面,提升交互速度。
2、 shell-bash的处理流程
终端进程读取用户从键盘输入的命令行,再分析命令行字符串,获取命令行参数,构造传递给execve函数的argv向量,再检查第一个命令行参数是否为内置shell命令,如果不是内部命令,那么调用frok函数创建子进程,在子进程中用获取的参数调用execve执行指定程序,如果用户要求后台运行(即命令行末尾有$)那么shell返回。
6.3 Hello的fork进程创建过程
终端程序通过调用fork函数创建一个子进程。子进程得到和父进程完全相同但是独立的一个副本,包括代码段,段,数据段,共享库以及用户栈。子进程和父进程的不同之处在于它们的进程号(PID)是不同的。父进程与子进程是并发执行的独立进程,内核能够以任意方式交替执行他们逻辑控制流的指令,在子进程执行期间,父进程默认等待子进程的完成。
以示例程序hello为例,在输入./hello 2021111565 zhangjie 2之后,shell会对输入的命令进行解析,我们输入的并不是内置的命令,于是shell会调用fork函数船舰一个子进程
6.4 Hello的execve过程
创建子进程之后,子进程调用execve函数在当前子进程上下文加载并运行一个新的程序,即为hello程序,加载并运行它
1、 删除已存在的用户区域,删除当前进程虚拟地址的用户部分中已经存在的区域结构
2、 映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些区域结构都是私有的,写时复制的,虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区,bss区域是请求二进制零的,映射匿名文件,大小包含在hello文件中,栈和堆区域也是请求二进制零的,初始长度为零。
3、 映射共享区域,如果hello程序与共享对象链接,那么这些对象都是动态连接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域
4、 设置程序计数器,execve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点,下一次调用这个进城的时候它将从这个入口点开始执行
加载过程中没有从磁盘到内存的数据复制,直到CPU引用一个被映射的虚拟页时才会进行复制,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
Hello的执行依赖于进程提供的抽象,下面阐述操作系统提供的进程的抽象
1、 逻辑控制流:一系列程序计数器的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中每个进程执行它的流的一部分后被抢占,然后轮到其他进程。
2、 并发流:一个逻辑流的执行时间与另一个流有重叠部分,成为并发流。
3、 时间片:一个进程执行它的控制流的一部分的每一个时间段叫做时间片
4、 私有地址空间:进程为每个流都提供一种假象,好像它在独占地使用系统地址空间,一般而言和这个空间中某个地址相关联的那个内存字节是不能被其他进程读或写的,在这个意义上,这个地址空间是私有的。
5、 用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器藐视了进程当前享有的特权每当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不郧西直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,
并且可以访问系统中的任何内存位置。
6、 上下文信息:上下文就是内核重新启动一个进程所需要的状态,由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈等对象的值构成。
7、 上下文切换:当内核选择一个新的进程运行时,内核调度了这个进程,在内核调度新的进程运行后,它就抢占当前进程,,并使用一种成为上下文切换的机制来控制转移到新的进程:保存以前进程的上下文,回复新进程被保存的上下文,将控制传递给这个新的进程来完成上下文的切换
Hello进程的执行:在进程调用execve之后,进程为hello分配了新的地址空间,并将txt和data节分配到虚拟地址空间的代码区和数据区,最初hello运行在用户模式下,输出hello 2021111565 zhangjie,然后调用sleep函数之后进入内核模式,内核模式选择处理休眠请求主动释放当前进程,并把hello放入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当到时间时发送一个中断信号,此时进入内核状态执行中盾处理,将hello从等待队列挪到运行队列,hello继续执行自己的逻辑控制流。
当hello调用getchar的时候,系统调用read,hello之前运行咋用户模式,调用read之后进入内核模式,内核中陷阱的处理程序请求来自键盘缓冲区的dma传输没并且在完成从键盘缓冲区到内存的数据传输之后中断处理器,此时进入内核模式内核执行上下文切换,切换到其他进程。完成键盘缓冲区到内存的数据传输之后,引发一个中断信号,内核从其他进程切换回hello进程,输出结果如下图下半部分所示:
6.6 hello的异常与信号处理
一、hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
异常可以分为四类:中断,陷阱,故障,终止。四类异常的性质如下图所示:
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
1、 输入CTRLZ:程序终止并退出,调用ps指令查看后台进程发现进程并未终止,通过fg命令继续执行这个进程,程序可以继续运行,如图所示:
2、 输入CTRLC:在输出过程中按下后,程序终止,调用ps查看发i西安没有hello程序了,说明程序已经终止。如图所示
3、 乱按情况乱按只是将屏幕输入缓存到缓冲区,getchar时读出一个\n结尾的字符串作为一次输入,其它字符串被当作shell命令输入,如图所示
6.7本章小结
本章阐述了进程的定义和作用,介绍了shell的一般处理流程合作用,分析了hello的执行以及hello的异常与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:在有地址变换功能的计算机中吗访问内存指令给出的地址叫做逻辑地址,也叫做相对地址,分为两个部分,段基址和段偏移量。
线性地址:逻辑地址经过段机制后转化为线性地址。分页机制中线性地址作为输入。
虚拟地址:CPU启动保护模式后,程序运行在虚拟地址空间中,与物理地址相似,虚拟内存被组织为一个存放在磁盘上的n个联系的字节大小的单元组成的数组,其每个字节对应的地址成为虚拟地址,虚拟地址包括虚拟页面偏移量、虚拟页号、TLB索引、TLB标记。
物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应的地址,在前端总线上传输的内存地址都是物理内存地址。
观察hello的反汇编代码,main函数对应的地址为0x401125,这个地址是逻辑地址的偏移量部分,偏移量加上代码段的段地址就得到main函数的虚拟地址,虚拟地址是现代系统的一个抽象概念,在经过mmu的处理之后得到实际存储在计算机存储设备上的地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
1、基本原理:
在段式存储管理中,程序的地址被划分成若干个段,每个进程有一个二维的地址空间,系统为整个进程分配一个连续的内存空间,在段式存储管理系统中名为每一个段分配一个连续的分区,进程的各个段可以不连续地存放在内存的不同分区中,程序加载时,操作系统为所有段分配所需要的内存,这些段不必连续,物理内存的管理采用动态分区的管理方法,再给某个段分配内存的时候可以采用首先适配法,下次适配法,最佳适配法等方法,在回收某个段占据的空间时,要注意将回收的空间与其相邻的空间合并。段式存储管理需要硬件支持来实现逻辑地址到物理地址的映射。程序通过分段被划分为多个模块,可以分别编写和编译,可以针对不同类型的段采取不同保护措施,可以以段为单位进行共享。
2、段式管理的数据结构:
进程段表:描述组成进程地址空间的各个段,可以是指向系统段表中表项的索引。
系统段表:系统所有占用段
空闲段表:内存中所有空闲段
3、段式管理的地址变换:
在段式管理系统中,整个地址空间是二维的,逻辑地址由段号和段内地址两部分组成,处理器会查找内存中的段表,由短号得到段的首地址,加上段内地址,得到实际的物理地址,这个过程有处理器的硬件直接完成,操作系统在进程切换时将段进程表的首地址装入处理器的特定寄存器中,这个寄存器叫做段表地址寄存器。
7.3 Hello的线性地址到物理地址的变换-页式管理
1、基本原理:
将程序的逻辑地址空间划分为固定大小的页,物理内存划分为同样大小的页框,程序加载时将任意一页放入内存中任意一个页框。需要CPU硬件支持来实现逻辑地址和无力地之间的映射。页式管理系统由页号和页内地址两部分组成。该种管理方式的优点是:没有外碎片,一个程序不必连续存放,便于改变程序占用空间的大小,缺点是:要求程序全部装入内存,没有足够的内存程序就不能执行。
2数据结构:页式系统中进程建立时,操作系统为进程中所有的页分配页框。当进程撤销时收回所有分配给它的页框,操作系统必须记录系统内存中实际的页框使用情况,还要再进程切换时正确地切换两个不同的进程地址空间到物理内存空间的映射,要求操作系统记录每个进程页表的相关信息,采取如下数据结构:
进程页表:完成逻辑页号到物理页号的映射
物理页面表:整个系统有一个物理页面表,描述物理内存空间的分配使用情况,数据结构可以采用位示图和空闲页链表。如果该页面已经被分配则对应比特置1,否则为0
3、页式管理的地址变换:
指令给出的地址分为逻辑页号和页内地址两部分。CPU中的内存管理单元按照逻辑页号通过差进程也标号得到物理页框号,将物理页框号和页内地址相加形成物理地址。这个过程由处理器的硬件直接完成,不需要软件的参与,操作系统只需要在进程切换的时候把进程页表的首地址装入处理器特定的寄存器即可。
7.4 TLB与四级页表支持下的VA到PA的变换
CPU 产生虚拟地址 VA,VA 传送给 MMU,MMU 使用前 36 位 VPN 作为 TLBT(前 32 位)+TLBI(后 4 位)向 TLB 中匹配,如果命中,则得到 PPN (40bit)与 VPO(12bit)组合成 PA(52bit)。 如果 TLB 中没有命中,MMU 向页表中查询,CR3 确定第一级页表的起始地 址,VPN1(9bit)确定在第一级页表中的偏移量,查询出 PTE,如果在物理内存 中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查 询到 PPN,与 VPO 组合成 PA,并且向 TLB 中添加条目。如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。
7.5 三级Cache支持下的物理内存访问
L1Cache的物理访存大致过程如下:
(1) 组选择取出虚拟地址的组索引位,将二进制组索引转化为一个无符号整数,找到相应的组
(2) 行匹配把虚拟地址的标记为拿去和相应的组中所有行的标记位进行比较,当虚拟地址的标记位和高速缓存行的标记位匹配时,而且高速缓存行的有效位是1,则高速缓存命中
(3) 字选择一旦高速缓存命中,我们就知道要找的字节在这个块的某个地方。因此块偏移位提供了第一个字节的偏移。把这个字节的内容取出返回给CPU即可;
7.6 hello进程fork时的内存映射
当fork函数被shell进程调用的时候,内核为新进程创建各种数据结构,并分配唯一的进程号,为了给这个进程创建虚拟内存,它创建了当前进程的mm_struct区域结构和页表的原样副本,将两个进程中的每个页面都标记为只读,并将两个进程中每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
Execve函数在当前进程中加载并运行包含在可执行目标文件中hello中的程序,用hello程序代替了当前程序,加载并运行hello需要以下几个步骤
1、 删除已经存在的用户区域:删除当前进程虚拟地址的用户部分已经存在的区域结构
2、 映射私有区域:为新代码、数据、bss、栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为a.out文件中的.text和.data区。Bss区域请求二进制零,映射到匿名文件,其大小包含在a.out中,栈和堆区域也是请求二进制零的,初始长度为零。
3、 映射共享区域:如果a.out程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态连接到这个程序的,然后再应和道用户虚拟地址空间中的共享区域内。
4、 设置程序计数器:execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障:当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不在内存中,会触发缺页故障,通过查询页表PTE可以知道虚拟页在磁盘的位置,缺页处理程序从指定的位置加载页面到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中,因此指令可以没有故障地运行完成。处理流程如图所示:
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap) 系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk), 要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么 是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
显式分配器(explicit allocator),要求应用显式地释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。
隐式分配器(implicit allocator),另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbage collector) ,而自动释放未使用的已分配的块的过程叫做垃圾收集(garbage collection) o 例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
隐式空闲链表的内存分配:
(1)放置已分配的块当一个应用请求一个k字节的块时,分配器搜索空闲链表。查找一个足够大可以放置所请求的空闲块。分配器搜索方式的常见策略是首次适配、下一次适配和最佳适配。
(2)分割空闲块一旦分配器找到一个匹配的空闲块,就必须做一个另策决定,那就是分配这个块多少空间。分配器通常将空闲块分割为两部分。第一部分变为了已分配块,第二部分变为了空闲块。
(3)获取额外堆内存如果分配器不能为请求块找到空闲块,一个选择是合并那些在物理内存上相邻的空闲块,如果这样还不能生成一个足够大的块,分配器会调用sbrk函数,向内核请求额外的内存。
(4)合并空闲块合并的情况一共分为四种:前空后不空,前不空后空,前后都不空。对于四种情况分别进行空闲块合并,我们只需要通过改变头部的信息就能完成合并空闲块。Knuth提出了一种采用边界标记的技术快速完成空闲块的合并。
显式空闲链表:
隐式空闲链表为我们提供了一种介绍一些基本分配器概念的简单方法。然而,因为块分配与堆块的总数呈线性关系,所以对于通用的分配器,隐式空闲链表是不适合的(尽管对于堆块数量预先就知道是很小的特殊的分配器来说它是可以的)。
一种更好的方法是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。
一种方法是用后进先出(LIFO)的顺序维护链表,将新释放的块放置在链表的开始处。使用LIF的顺序和首次适配的放置策略,分配器会最先检査最近使用过的块。在这种情况下,释放一个块可以在常数时间内完成。如果使用了边界标记,那么合并也可以在常数时间内完成。
另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序的首次适配比LIFO排序的首次适配有更高的内存利用率,接近最佳适配的利用率。
7.10本章小结
本章主要介绍了hello的存储器的地址空间,介绍地址空间的差别和地址的相互转换,介绍了四级页表的虚拟地址空间到物理地址的转换,阐述三级cache的物理内存访问,进程fork时的内存映射,execve时的内存映射,缺页故障与缺页中断处理,动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有I/O设备都被模型化为文件,所有输入输出都被当作相应文件的读和写来完成,这种将设备映射为文件的方式,允许内核引出一个简单的低级的应用接口,使得所有输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
接口的几种操作:
1、 打开文件:程序要求内核打开文件,内核返回一个小的非负整数,用于标识这个文件,程序只要记录这个描述符便能打开文件的所有信息。
2、 Shell在进程的开始为其打开三个文件:标准输入、标准输出、标准错误
3、 关闭文件:内核释放打开文件时创建的数据结构以及占用的内存资源并将描述符恢复到可用的描述符中,无论一个进程因为何种原因终止,内核都会关闭所有打开的文件并释放它们的内存资源。
4、 读写文件:一个读操作就是从文件复制正整数个字节到内存,从当前位置开始然后增加到k+n,
5、 关闭文件:内核释放打开文件时创建的数据结构以及占用的内存资源,并将描述符恢复到可用的描述符池中,无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
函数:
1、intopen(char* filename, int flags, mode_t mode);open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
2、intclose(int fd);关闭一个打开的文件,返回操作结果。
3、ssize_t read(int fd, void *buf, size_t n);read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
4、ssize_t write(int fd, const void *buf,size_t);write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
Printf函数的函数体:程序按照格式fmt结合参数args生成格式化之后的字符串,返回字符串的长度。
Write函数:在printf中调用系统函数write(buf,i),将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址。
Syscall函数体:将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成 ASCII 码,保存到系统的键盘缓冲区之中。getchar 函数落实到底层调用了系统函数 read,通过系统调用 read 读取存储在 键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar 进行封装, 大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
本章介绍了Linux的IO设备管理方法,unixIO接口及其函数,分析了printf函数和getchar函数的实现。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
Hello.c:编写C语言程序,是一个二进制文本文件,每一个字符都用ASCII码表示
Hello.I:hello.c经过预处理阶段变为hello.i
Hello.s:hello.i经过编译变为hello.s
Hello.o:hello.s经过汇编阶段变为hello.o
Hello:hello.o经过与可重定位文件和动态链接库链接成为可执行文件,hello程序正式诞生。
运行:在终端命令行输入2021111565 zhangjie 2
创建子进程:终端中并没有输入内置shell命令,因此shell调用fork函数创建子进程
加载:shell调用execve,execve调用启动加载器,加载映射虚拟内存,进入程序入口后开始载入物理内存,然后进入main函数
上下文切
换:hello调用sleep函数后进程进入内核模式,请求主动释放当前进程,内核进行上下文切换将当前进程的控制权交给其他进程,sleep函数调用完成后,内核执行上下文切换,把控制传递给当前进程。
动态申请内存:hello执行printf函数时,会调用malloc向动态内存分配器申请堆中的内存。
信号管理:程序运行时在终端命令行输入CTRLC,内核会发送SIGINT信号到进程,并终止前台作业,当在终端命令行输入CTRLZ,内核会发送SIGSTP到进程,将前台作业挂起。
终止:子进程执行完成后,内核安排父进程回收子进程,将子进程的退出状态返回给父进程,内核删除为这个子进程创建的所有数据结构。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
计算机系统的设计思想和实现都源于抽象。最底层的信息用二进制表示抽象,实现操作系统管理硬件的抽象,进程是对处理器、主存和IO河北的抽象,虚拟内存是对主存和磁盘设备的抽象,文件是对IO设备的抽象。
计算机系统设计考虑全面,考虑到了一切可能出现的实际情况,设计出一些满足不同情况的策略。在深入理解计算机的硬件设备以及计算机对于各类情况的处理方法后,有利于我们编写出更加适合计算机、更加高效、能将计算机能提供给我们的全部资源应用起来的更优化的程序,对于每一位程序员来说都是非常必要的,尤其是了解简单的程序在机器中究竟是如何被运行的这种方式可以让我们更加直观地理解计算机系统中的一些相对来说抽象的概念,有助于后续专业课的学习和理解。
附件
列出所有的中间产物的文件名,并予以说明起作用.
Hello.i:hello.c预处理后得到的.i文件
Hello.s:hello.i编译后得到的.s文件
Hello.o:hello.s汇编后得到的.o文件
Hello:hello.o经链接后得到的可执行文件
Hello.out:hello经过反汇编得到的可重定位文件
Asm.txt:hello经过反汇编生成的代码保存在这个文本文件中