计算机系统
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 120L022013
班 级 2003007
学 生 王炜栋
指 导 教 师 吴锐
计算机科学与技术学院
2021年5月
摘 要
本文阐述了hello.c程序,从源代码到可执行目标文件,再到执行进程,最终被终止并回收的过程。具体描述了它的处理全过程,包括P2P(program to progress)和020.全面阐释了计算机的底层实现
关键词:预处理;编译;汇编语言;机器语言;可重定位目标文件ELF格式;链接;重定位;动态链接;shell;进程;存储;虚拟内存;I/O;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
1.2 环境与工具
1.3 中间结果
1.4 本章小结
第2章 预处理
2.1 预处理的概念与作用
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
2.4 本章小结
第3章 编译
3.1 编译的概念与作用
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.4 本章小结
第4章 汇编
4.1 汇编的概念与作用
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
4.4 Hello.o的结果解析
4.5 本章小结
第5章 链接
5.1 链接的概念与作用
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
5.4 hello的虚拟地址空间
5.5 链接的重定位过程分析
5.6 hello的执行流程
5.7 Hello的动态链接分析
5.8 本章小结
第6章 hello进程管理
6.1 进程的概念与作用
6.2 简述壳Shell-bash的作用与处理流程
6.3 Hello的fork进程创建过程
6.4 Hello的execve过程
6.5 Hello的进程执行
6.6 hello的异常与信号处理
6.7本章小结
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.2 Intel逻辑地址到线性地址的变换-段式管理
7.3 Hello的线性地址到物理地址的变换-页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
7.5 三级Cache支持下的物理内存访问
7.6 hello进程fork时的内存映射
7.7 hello进程execve时的内存映射
7.8 缺页故障与缺页中断处理
7.9动态存储分配管理
7.10本章小结
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
8.2 简述Unix IO接口及其函数
8.3 printf的实现分析
8.4 getchar的实现分析
8.5本章小结
结论
附件
参考文献
Hello.i 预处理生成的文本文件
Hello.s .i编译后得到的汇编语言文件
Hello.o .s汇编后得到的可重定位目标文件
Hello.elf hello.o的elf文件
Hello.asm .o经过反汇编生成的汇编语言文件
Hello .o经过链接生成的可执行目标文件
Linkhello.elf Hello的elf文件
Obj_hello.s .o经过链接反汇编的汇编语言文件
Hello.c 源代码
本章对hello进行了一个简介,并对其运行流程进行了一个总体的概括。然后介绍了所有操作的环境与工具,以及描述了中间产生的文件信息。
2.1.1:预处理的概念
C语言对源程序处理的四个步骤:预处理、编译、汇编、链接。预处理是在程序源代码被编译之前,由预处理器(Preprocessor)对程序源代码进行的处理。这个过程并不对程序的源代码语法进行解析,但它会把源代码分割或处理成为特定的符号为下一步的编译做准备工作。例如#include、#define、#if,分别可以在正式编译前进行引入外部库文件、进行宏定义、如果后接语句为真则添加#if与#endif之间的代码进行编译。
2.1.2:预处理的作用
1:宏定义(如#define) 2:文件包含(#include) 3:条件编译。(#if、#else、#endif)4.:删除注释
预处理命令:gcc -E -o hello.i hello.c
如图2.1所示
图2.1
进行预处理后,我们发现在文件夹中生成了“hello.i”文件,如图2.2所示
图2.2
我们发现经过预处理后,原本几行的代码扩充到了3060行,他的作用是处理了所有的预编译操作,即宏定义、文件包含、条件编译、删除注释;
接下来的3060行代码又将完成什么样的功能呢?
本章借用Ubuntu的Terminal对Hello.c进行预编译的操作解释了预编译的概念和作用。通过解析hello.c与hello.i的差异直观比对出预编译的处理结果。
3.1.1编译的概念:
编译,就是把代码转化为汇编指令的过程,汇编指令只是CPU相关的,也就是说C代码和python代码,代码逻辑如果相同,编译完的结果其实是一样的。即将高级语言程序转为机器能读懂的语言,在本文中编译是将hello.i转为hello.s
3.1.2编译的作用:
命令:gcc -S hello.i -o hello.s
编译结果如下图3.1所示
图3.1
3.3.1:文件结构
图3.2
表格 hello.s文件结构
内容 |
含义 |
.file |
源文件 |
.text |
代码段 |
.global |
全局变量 |
.data |
存放已经初始化的全局和静态C 变量 |
.section .rodata |
存放只读变量 |
.align |
对齐方式 |
.type |
表示是函数类型/对象类型 |
.size |
表示大小 |
.long .string |
表示是long类型/string类型 |
3.3.2:数据类型
1)String类型
图3.3
1.立即数,通常用$n,n表示为定义立即数数值
图3.4
2.int型
如图3.4所示,hello中定义的int型数据占据了4字节大小的地址空间,且初始赋值为立即数$0
通过观察下图3.5代码,发现hello.c中使用的计数功能临时变量i被储存在-4(%rbp)中,相当于在这里定义了一个局部变量。
图3.5
图3.6
3.3.3:类型转换
如下图3.7所示,调用atoi函数将字符型转为int整型变量
图3.7
3.3.4:赋值操作
如图3.4,将立即数0赋值给地址为%rbp-4的变量i,其占用4个字节
3.3.5:数组(栈)操作
如图3.8,movq的作用是开辟一个32字节的空间用来存放数组,首地址-32(%rbp)存放在%rax中,每8个字节是一个数值,即-24(%rbp)就是数组argv[1];
图3.8
3.3.6:算术操作
如图3.9,addl可以用作向地址中所存数值加立即数,本图中为向-4(%rbp)中所存数值加1
图3.9
类似的,也可以通过addl或subq进行加减运算、地址偏移、开辟空间等操作
3.3.7:关系操作(比较跳转)
如图3.10,我们在程序中常常会遇到if、elif、else等条件函数,其汇编代码的呈现形式就是判别跳转,cmpl的作用是将后面两个参数进行比较、je表示相等跳转(jump equal)、还有类似的如js、ja、jne、jbe等
图3.10
3.3.8:函数操作
如下图3.11,汇编语言中已经编译成功的函数可以通过“call 函数名”的方式调用
图3.11
hello.c涉及的函数操作有:main、printf、exit、sleep 、getchar、
其中:
图3.12
main函数的参数是argc和argv[];两次printf函数的参数是那两个字符串;exit参数是1,sleep函数参数是atoi(argv[3])。函数的返回值存储在%eax寄存器中。
本章主要介绍了编译阶段编译的基本概念和作用,通过在Ubuntu上对hello.i编译为hello.s的操作展示汇编代码,并以hello.s为例理解了一些汇编指令及代码,通过理解编译机制,我们也更容易理解高级语言程序的编译原理。
第4章 汇编
汇编的作用:将汇编语言程序撰写为机器语言程序的翻译程序,使其连接后能使机器识别
命令:gcc hello.s -c -o hello.o
作用:将hello.s转为hello.o文件
图4.1
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
readelf -a hello.o >hello.elf,通过重定位将hello.o转为.elf文件
图4.2
打开.elf文件,由于截图限制展示部分.elf内容
图4.3
以一个16位的Magic序列开始,描述生成该文件的字大小和字节顺序,剩下部分包括帮助链接器语法分析和解释目标文件的信息,还有节头部表中条目的大小和数量。
图4.4
记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。
图4.5
一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
偏移量:重定位入口的偏移,记录了需要重定位的项的位置;
信息:包含symbol与type两个信息,分别存在高8位与低16o位中,前者用于记录该符号在符号表中的索引,提供了需要插入到重定位位置的数据;后者用于记录符号的重定义类型,提供了重定位后的地址的计算方法;
类型:告知链接器应该如何修改新的应用;
符号值:符号值是每个符号待修改的新偏移量;
符号名称:给出需要重定位的符号的名称;
图4.6
存放程序中定义的全局变量和函数的信息。name记录目标名称,value记录符号地址,size记录目标大小,type记录目标类型,是函数还是数据,bind表示全局还是本地。
图4.7
执行objdump -d -r hello.o >hello.asm后得到hello.o的反汇编
图4.8
Disassembly of section .text:
0000000000000000
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 83 ec 20 sub $0x20,%rsp
c: 89 7d ec mov %edi,-0x14(%rbp)
f: 48 89 75 e0 mov %rsi,-0x20(%rbp)
13: 83 7d ec 04 cmpl $0x4,-0x14(%rbp)
17: 74 16 je 2f
19: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 20
1c: R_X86_64_PC32 .rodata-0x4
20: e8 00 00 00 00 callq 25
21: R_X86_64_PLT32 puts-0x4
25: bf 01 00 00 00 mov $0x1,%edi
2a: e8 00 00 00 00 callq 2f
2b: R_X86_64_PLT32 exit-0x4
2f: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
36: eb 48 jmp 80
38: 48 8b 45 e0 mov -0x20(%rbp),%rax
3c: 48 83 c0 10 add $0x10,%rax
40: 48 8b 10 mov (%rax),%rdx
43: 48 8b 45 e0 mov -0x20(%rbp),%rax
47: 48 83 c0 08 add $0x8,%rax
4b: 48 8b 00 mov (%rax),%rax
4e: 48 89 c6 mov %rax,%rsi
51: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 58
54: R_X86_64_PC32 .rodata+0x22
58: b8 00 00 00 00 mov $0x0,%eax
5d: e8 00 00 00 00 callq 62
5e: R_X86_64_PLT32 printf-0x4
62: 48 8b 45 e0 mov -0x20(%rbp),%rax
66: 48 83 c0 18 add $0x18,%rax
6a: 48 8b 00 mov (%rax),%rax
6d: 48 89 c7 mov %rax,%rdi
70: e8 00 00 00 00 callq 75
71: R_X86_64_PLT32 atoi-0x4
75: 89 c7 mov %eax,%edi
77: e8 00 00 00 00 callq 7c
78: R_X86_64_PLT32 sleep-0x4
7c: 83 45 fc 01 addl $0x1,-0x4(%rbp)
80: 83 7d fc 07 cmpl $0x7,-0x4(%rbp)
84: 7e b2 jle 38
86: e8 00 00 00 00 callq 8b
87: R_X86_64_PLT32 getchar-0x4
8b: b8 00 00 00 00 mov $0x0,%eax
90: c9 leaveq
91: c3 retq
以下说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等:
通过比较反汇编的代码和hello.s,可以发现二者在语句、语法、语义上没有过多区别,但反汇编代码除了显示汇编代码以外,还会显示机器代码。此外还有以下差异
本章主要介绍了汇编的概念及作用、Ubuntu下终端的指令,通过对hello.o重定位到的.elf格式与之前的hello.s进行比对得出的差异进行分析后探究了汇编语言到机器语言的一一映射关系
链接的作用:将程序从.o文件转化为可执行文件。使分离编译成为可能。
执行指令:
如图5.1所示,执行指令后生成了hello文件
图5.1
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
执行指令:
readelf -a hello > linkhello.elf
执行后如图5.2所示生成了linkhello.elf文件
图5.2
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
ELF Header在编译后与原来的汇编后的表的格式是一模一样的,仍然以一个16字节大小的Magic头开始,给出了操作系统和编译器辨别此文件为ELF二进制库,但是会发现type从REL变为EXEC,节点数量变为27个
图5.3
Linkhello.elf中的节头包含了文件中出现的各个节的语义,包括节的类型、位置、偏移量和大小等信息。与hello.elf相比,其在链接之后的内容更加丰富详细
图5.4
重定位表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明
图5.5
符号表存放程序中定义的函数和局部变量的信息
1.Name:符号名称2.Value:符号相对于目标节起始位置偏移3.Size:目标的大小4.Type:类型,全局变量或函数5.Bind:表明是本地的还是全局的
图5.6
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
使用edb打开hello可执行文件,可以在edb的Data Dump窗口看到hello的虚拟地址空间分配的情况,具体内容截图如下:
图5.7
可通过Symbols Viewer查看具体节点位置
图5.8
例如.sleep@plt在0x401080,在datadump中找到对应位置,类似的,也可以通过查表寻找到其他节点。
图5.9
执行命令,发现obj_hello.s已经出现在指定位置上
图5.10
打开obj_hello.s,观察其与hello.o的差异
图5.11
从图中不难看出,在代码数量上obj_hello.s远超过hello.o,这是因为obj_hello.s增加了一些系统级函数调用,同时也根据连接完成了符号的重定位。
链接(linking)是将各种代码块组合成一个可被载入内存并执行的文件的过程。链接过程可以在编译时(compile time)进行,此时源代码已经被翻译成了机器指令;也可以在载入时(load time)进行,此时程序已经被加载入了内存并且被加载器(loader)执行;也可以在运行时(run time)进行,此时链接过程是由应用程序进行的。在早期的操作系统中,链接过程需要手动完成,而在现在的系统中,链接过程是由称为链接器(linker)的程序完成的。链接器在软件的发展中起到了很重要的作用,因为它允许独立编译(separate compilation)。现在我们可以将一个大程序分解为较小的且更便于管理的小模块。我们对这些模块进行单独的修改和编译,而不是将整个程序编写为一个大的源文件。当我们改变这些小模块后,只需要将改变的部分简单的进行重新编译链接,而不需要重新编译其他文件。为了生成可执行文件,静态链接器必须执行如下的动作:
符号解析(symbol resolution):在目标文件中我们定义并引用了符号(symbol)。符号解析的目的是将每个符号引用和具体的符号定义结合在一起。
重定位(relocation):编译器和汇编器会从地址0开始生成数据section和代码section。链接器通过将具体的内存地址和每个定义的符号结合在一起来重定位这些section。接下来链接器会修改所有指向这些符号的引用,使得这些引用指向分配的内存地址。
那么结合一下hello.o与obj_hello.s的差异看看链接是如何完成的;
1)链接增加新的函数:
在hello中链接加入了在hello.c中用到的库函数,如exit、printf、sleep、getchar等函数。
2)增加的节:
hello中增加了.init和.plt节,和一些节中定义的函数。
3)函数调用:
hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
4)地址访问:
hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于某些地址的定位是不明确的,其地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。
我们得出链接实际上是通过链接器将各个.o文件串联在一起,组装成一个新文件的过程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
图5.12
如图5.12使用edb执行hello后,我们通过查看Symbols可以找到其调用与跳转的各个子程序名或程序地址。如图5.13.
图5.13
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表plt+全局偏移量表got实现函数的动态链接,got中存放函数目标地址,plt使用got中地址跳转到目标函数。
首先打开linkhello.elf文件找到got的存放地址为0x404000如下图5.14
图5.14
在调用前404008后的16个字节都是0,存放PLT中函数调用下一条指令
图5.15
调用后变为7f9ba6a2e190、7f9ba6a17ae0两个地址
图5.16
本章介绍了链接的概念和作用,对链接后生成的可执行文件hello的elf格式文件进行了分析,分析了hello的重定位过程、执行过程、动态链接分析的各种处理操作。
(第5章1分)
6.1.1进程的概念
相较于程序,进程是动态的概念,比如gcc -o pro生成的这个pro文件,当他运行起来以后,系统中就会多一个进程。进程是一个正在运行的程序的实例,系统中的每一个程序都运行在某个进程的上下文中。
6.1.2进程的作用
给应用程序提供两个关键抽象:
6.2.1:Shell-bash的作用
Shell-bash的基本作用是一个交互型应用级程序,代表用户运行其他程序,接收用户输入的命令并把它送入内核去执行。它交互性地解释和执行用户输入的命令,能够通过调用系统级的函数或功能执行程序、建立文件、进行并行操作等。包括历史、别名、快捷键、输入输出、执行顺序、管道符。
6.2.2:Shell-bash的处理流程
1.终端进程读取用户由键盘输入的命令行。
2.分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
3.检查第一个命令行参数是否是一个内置的shell命令
4.如果不是内部命令,调用fork()创建新进程/子进程
5.在子进程中,用步骤2获取的参数,调用execve()执行指定程序。
6.如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid等待作业终止后返回。
7.如果用户要求后台运行(如果命令末尾有&号),则shell返回;
fork进程的创建过程如下:首先,带参执行当前目录下的可执行文件hello,父进程会通过fork函数创建一个新的运行的子进程hello。子进程获取了与父进程的上下文,包括栈、通用寄存器、程序计数器,环境变量和打开的文件相同的一份副本。子进程与父进程的最大区别是有着跟父进程不一样的PID,子进程可以读取父进程打开的任何文件。当子进程运行结束时,父进程如果仍然存在,则执行对子进程的回收,否则就由init进程回收子进程。
调用函数fork创建新的子进程之后,子进程会调用execve函数,在当前进程的上下文中加载并运行一个新程序hello。execve 函数从不返回,它将删除该进程的代码和地址空间内的内容并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行该程序。将私有的区域映射进来,例如打开的文件,代码、数据段,然后将公共的区域映射进来。后面加载器跳转到程序的入口点,即设置PC指向_start 地址。_start函数最终调用hello中的 main 函数,这样,便完成了在子进程中的加载。
一系列程序计数器 PC 的值的序列叫做逻辑控制流。由于进程是轮流使用处理器的,同一个处理器每个进程执行它的流的一部分后被抢占,然后轮到其他进程。处理器使用一个寄存器提供两种模式的区分。用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据;内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。为了能让处理器安全运行,不至于损坏操作系统,必然需要先知应用程序可执行指令所能访问的地址空间范围。因此,就存在了用户态与核心态的划分,核心态可以说是“创世模式”,拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
以执行sleep函数为例,sleep函数请求调用休眠进程,sleep将内核抢占,进入倒计时,当倒计时结束后,hello程序重新抢占内核,继续执行。
图6.1
当hello调用getchar的时候,实际落脚到执行输入流是stdin的系统调用read,hello之前运行在用户模式,在进行read调用之后陷入内核,内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。此时进入内核模式,内核执行上下文切换,切换到其他进程。当完成键盘缓冲区到内存的数据传输时,引发一个中断信号,此时内核从其他进程进行上下文切换回hello进程。而我们看到的输出结果如下:
图6.2
6.6.1 hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
异常可以分为四类:中断(interrupt)、陷阱(trap)、故障(fault)和终止(abort),下表对这些类别的属性做了小结 :
类别 |
原因 |
异步/同步 |
返回行为 |
中断 |
来自I/O设备的信号 |
异步 |
总是返回到下一条指令 |
陷阱 |
有意的异常 |
同步 |
总是返回到下一条指令 |
故障 |
潜在可恢复的错误 |
同步 |
可能返回到当前指令 |
终止 |
不可恢复的错误 |
同步 |
不会返回 |
中断:中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。硬件中断不是由任 何一条专门的指令造成的,从这个意义上来说它是异步的。硬件中断的异常处理程序常常 称为中断处理程序(interrupt handler) 。图6.3概述了一个中断的处理
图6.3
陷阱:陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序 将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一 样的接口,叫做系统调用。图6.4概述了一个系统调用的处理
图6.4
故障:故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控 制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故 障的指令,从而重新执行它。否则,处理程序返回到内核中的abort例程,abort例程会 终止引起故障的应用程序。图6.5概述了一个故障的处理
图6.5
终止:终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者 SRAM位被损坏时发生的奇偶错误。终止处理程序从不将控制返回给应用程序。如图6.6所示,处理程序将控制返回给一个abort例程,该例程会终止这个应用程序。
图6.6
6.6.2:以hello为例的异常与信号处理
我们可以发现不停乱按只是将乱按的字符作为输入存在stdin中,当 getchar 的时候读出一个’\n’结尾的字串(作为一次输入)
图6.7
我们可以发现在程序进行中按回车会打印出空行,且会存在缓冲区,在程序正常结束时执行之前输入的Enter操作,如图6.8
图6.8
在程序运行中按Ctrl-C,可以发现程序终止,shell收到sigint信号,回收hello进程
图6.9
输入CTRL+Z:在输出过程中按下CTRL+Z,程序中止并退出,此时调用ps指令查看后台进程,发现程序并未终止,而是中止存放在后台,如图6.10
图6.10
此时可以输入pstree查看进程树,如图6.11
图6.11
还可以输入fg回到前台继续执行如图6.12
图6.12
通过本章的学习,我们了解了进程的概念与作用、体会了壳Shell-bash的作用与处理流程、利用Hello的fork进程学习创建过程、研究了Hello的execve过程、明白了Hello的进程执行以及hello的异常与信号处理
(第6章1分)
逻辑地址
是指由程式产生的和段相关的偏移地址部分。逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。例如,你在进行C语言指针编程中,能读取指针变量本身值(&操作),实际上这个值就是逻辑地址,他是相对于你当前进程数据段的地址,不和绝对物理地址相干。只有在Intel实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,Cpu不进行自动地址转换);逻辑也就是在Intel保护模式下程式执行代码段限长内的偏移地址(假定代码段、数据段如果完全相同)。以hello为例,其为hello反汇编代码中的相对偏移地址。
线性地址
也叫虚拟地址(virtual address)。是逻辑地址到物理地址变换之间的中间层。程式代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。Intel 80386的线性地址空间容量为4G(2的32次方即32根地址总线寻址)。
物理地址
用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。
是指目前CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
图7.1
前提如下: 虚拟地址空间 48 位,物理地址空间 52 位,页表大小 4KB,4 级页表。TLB 4 路 16 组相联。CR3 指向第一级页表的起始位置(上下文一部分)。 解析前提条件:由一个页表大小 4KB,一个 PTE 条目8B,共 512 个条目,使 用 9 位二进制索引,一共 4 个页表共使用 36 位二进制索引,所以 VPN 共 36 位, 因为 VA 48 位,所以 VPO 12 位;因为 TLB 共 16 组,所以 TLBI 需 4 位,因为 VPN 36 位,所以 TLBT 32 位。
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.2
图7.3
缺页中断处理:确认出物理内存中的牺牲页,如果已被修改,则放入虚拟内存,调出新的页面,更新内存中的页表条目,返回到原先的进程中,重新执行引起缺页故障的命令,重新在内存管理单元中查找页表,发现造成缺页故障的物理地址已在物理内存中,命中。
图7.4
图7.5
显式分配器(explicit allocator):
隐式分配器(implicit allocator):
虚拟内存是对主存的一个抽象。支持虚拟内存的处理器通过使用一种叫做虚拟寻址的间接形式来引用主存。处理器产生一个虚拟地址,在被发送到主存之前,这个地址被翻译成一个物理地址。从虚拟地址空间到物理地址空间的地址翻译要求硬件和软件紧密合作。专门的硬件通过使用页表来翻译虚拟地址,而页表的内容是由操作系统提供的。
虚拟内存提供三个重要的功能。第一,它在主存中自动缓存最近使用的存放磁盘上的虚拟地址空间的内容。虚拟内存缓存中的块叫做页。对磁盘上页的引用会触发缺页,缺页将控制转移到操作系统中的一个缺页处理程序。缺页处理程序将页面从磁盘复制到主存缓存,如果必要,将写回被驱逐的页。第二,虚拟内存简化了内存管理,进而又简化了链接、在进程间共享数据、进程的内存分配以及程序加载。最后,虚拟内存通过在每条页表条目中加入保护位,从而了简化了内存保护。
地址翻译的过程必须和系统中所有的硬件缓存的操作集成在一起。大多数页表条目位于L1高速缓存中,但是–个称为TLB的页表条目的片上高速缓存,通常会消除访问在Ll上的页表条目的开销。
现代系统通过将虚拟内存片和磁盘上的文件片关联起来,来初始化虚拟内存片,这个过程称为内存映射。内存映射为共享数据、创建新的进程以及加载程序提供了一种高效的机制。应用可以使用mmap函数来手工地创建和删除虚拟地址空间的区域。然而,大多数程序依赖于动态内存分配器,例如 malloc,它管理虚拟地址空间区域内一个称为堆的区域。动态内存分配器是一个感觉像系统级程序的应用级程序,它直接操作内存,而无需类型系统的很多帮助。分配器有两种类型。显式分配器要求应用显式地释放它们的内存块。隐式分配器(垃圾收集器)自动释放任何未使用的和不可达的块。
(第7章 2分)
一个Linux文件就是一个m个字节的序列:
B0,B1,…,Bk ,…,Bm - 1
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行:
设备的模型化:文件
设备管理:unix io接口
Unix IO接口:
(1)打开文件:一个应用程序要求内核打开相应文件,来访问I/O设备。会返回一个描述符,内核记录该文件所有信息,程序只需要记住该描述符。
(2)linux shell 创建的每个进程开始时都有三个打开的文件:标准输入 、标准输出和标准错误。头文件定义了常量来代替显式的描述符值。
(3)改变文件位置:内核对于每个文件保持一个初始为0的文件位置,该位置标示从头部开始的文件的偏移量。程序可以通过函数修改此文件位置。
(4)读写文件:读操作是从当前文件位置开始,复制相应数量的字节到内存,写操作则是从内存读入相应数量的字节到当前文件位置,然后更新文件位置。
(5)关闭文件:一个应用程序完成对文件访问后,要求内核关闭相应文件,内核会删除期间创建的数据结构,并恢复描述符。
Unix IO函数:
(1)打开函数:int open(char* filename,int flags,mode_t mode)将filename文件转为操作符,mode为访问权限
(2)关闭函数:int close(fd)关闭fd文件,返回操作结果
(3)读函数:ssize_t read(int fd,void *buf,size_t n)从fd文件复制n个字节到buf处
(4)写函数:ssize_t wirte(int fd,const void *buf,size_t n)从buf处复制n个字节给fd文件
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;
}
int vsprintf(char *buf, const *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);
}
程序调用getchar后,等待用户按键,将输入的字符储存在电脑的缓冲区,等待键入回车。键入回车后,getchar从stdio流中每次读取一个字符,如果输入不止一个字符,则保存在缓冲区中,等待后需getchar调用,直到缓冲区清空后,等待用户按键。
异步异常-键盘中断的处理:按键后,不仅产生该按键的码,还产生一个中断信号,使正在运行的程序中断后运行一个子程序,读取按键的码并转为ASCII码保存到缓冲区内。都结束后回到下一条指令。
本章介绍了 Linux 的 I/O 设备的基本概念和管理方法,以及Unix I/O 接口及其函数。最后分析了printf 函数和 getchar 函数的工作过程。
(第8章1分)
用计算机系统的语言,逐条总结hello所经历的过程。
编写hello.c的源程序
预处理:从hello.c生成hello.i文件,将文件中调用的库展开并入文件中。
编译:从hello.i生成hello.s文件,将高级语言转为汇编语言。
汇编:从hello.s生成hello.o文件,机器语言写的可以重定位的文件。
链接:将hello.o经重定位和动态链接生成可执行文件hello
运行:在shell中输入命令,终端为其fork新建进程,并通过execve把代码和数据加载入虚拟内存空间,程序开始执行;
加载:shell 调用 execve()函数,execve()函数 调用启动加载器,映射到虚拟内存,程序开始加载到物理内存,然后进入 main 函数。
执行命令:hello顺序执行自己的逻辑控制流
动态内存分配:通过malloc函数申请动态内存
信号:Ctrl+c,发送SIGINT信号给进程并终止前台作业。Ctrl+z时,发送SIGTSTP信号给进程,并将前台作业停止挂起。
终止:子进程结束后,父进程回收子进程,并回收内存空间。
感悟:一个简简单单的hello.c的程序,背后却要经历那么复杂和漫长的一生。
原来一个短短几行的hello程序背后却是那么的不简单,不平凡。计算机系统的设计者是多么巧妙构思,帮助我们简化了程序运行的过程,让计算机成为简单易用的工具。我明白了时代的进步离不开这些敢打敢拼艰苦奋斗的科学家们,无数次的更新迭代是他们绞尽脑汁的结果。我们不仅要学习他们的智慧结晶,更要学习他们不畏艰难的意志品质,才能深刻理解计算机科学,让计算机更好的、更便捷的为人类所使用。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
(结论0分,缺失 -1分,根据内容酌情加分)
列出所有的中间产物的文件名,并予以说明起作用。
Hello.c 源代码
Hello.i 预处理生成的文本文件
Hello.s .i编译后得到的汇编语言文件
Hello.o .s汇编后得到的可重定位目标文件
Hello.elf hello.o的elf文件
Hello.asm .o经过反汇编生成的汇编语言文件
Hello .o经过链接生成的可执行目标文件
Linkhello.elf Hello的elf文件
Obj_hello.s .o经过链接反汇编的汇编语言文件
(附件0分,缺失 -1分)
为完成本次大作业你翻阅的书籍与网站等
[2]https://blog.csdn.net/will130/article/details/50917166?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165235876916780366591062%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=165235876916780366591062&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-2-50917166-null-null.142^v9^control,157^v4^control&utm_term=%E9%80%BB%E8%BE%91%E5%9C%B0%E5%9D%80%E3%80%81%E7%BA%BF%E6%80%A7%E5%9C%B0%E5%9D%80%E3%80%81%E8%99%9A%E6%8B%9F%E5%9C%B0%E5%9D%80%E3%80%81%E7%89%A9%E7%90%86%E5%9C%B0%E5%9D%80&spm=1018.2226.3001.4187
[3]https://blog.csdn.net/weixin_41019383/article/details/98207386?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165235934416781667899564%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=165235934416781667899564&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-5-98207386-null-null.142^v9^control,157^v4^control&utm_term=+%E7%BA%BF%E6%80%A7%E5%9C%B0%E5%9D%80%E5%88%B0%E7%89%A9%E7%90%86%E5%9C%B0%E5%9D%80%E7%9A%84%E5%8F%98%E6%8D%A2-%E9%A1%B5%E5%BC%8F%E7%AE%A1%E7%90%86&spm=1018.2226.3001.4187
[4] [转]printf 函数实现的深入剖析 - Pianistx - 博客园
(参考文献0分,缺失 -1分)