计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 信息安全
学 号 2022111628
班 级 2203201
学 生 方贺
指 导 教 师 史先俊
计算机科学与技术学院
2024年5月
Hello一个简单的c程序文件,它的一生却完整体现了计算机系统的工作机制。本文以hello.c源文件从预处理,编译,汇编,链接等过程,从而展示程序的完整周期。并且展示进程等计算机重要概念,深入理解计算机运行程序时对异常的处理。
关键词:预处理;汇编;链接;进程;信号
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P(From Program to Process):源文件hello.c在经过预处理器cpp后,得到了修改后的源程序hello.i,经过编译器cc1后得到了汇编文件hello.s,经过汇编器as后得到了可重定位目标文件hello.o,最后,经过链接器ld得到了可执行目标文件hello。执行程序 “hello”,shell就会通过fork()创建一个子进程,子进程执行execve()在子进程的上下文中加载并执行hello。
020(From Zero-0 to Zero-0):可执行文件hello执行后,shell通过execve函数进行虚拟内存映射,分配空间,初始化执行环境,调用用户层的main函数,CPU流水线式读取并执行指令,通过TLB、4级页表、3级Cache,Pagefile等方式加速程序运行,程序进程终止后,通过信号处理机制,hello的父进程对hello进行回收,释放内存删除有关进程上下文。进程由0开始,由0结束。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1.2.1 硬件环境
X64CPU;2.30GHz;256GHD Disk
1.2.2 软件环境
Windows10 64位;Vmware 15.5.6;Ubuntu 16.04 LTS 64位
1.2.3 开发工具
Visual Studio 2022 64位;CodeBlocks 64位;vi/vim/gedit+gcc
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名 | 作用 |
hello.c | hello的源文件 |
hello.i | 源文件经过预处理后的文件 |
hello.s | 经过编译生成的汇编文件 |
hello.o | 汇编生成的可重定位目标文件 |
hello | 链接生成的可执行文件 |
hello_elf | hello.o的elf文件 |
hello_asm | 将hello.o反汇编生成的文件 |
hello1.elf | hello的elf文件 |
hello1_asm | hello的反汇编文件 |
1.4 本章小结
这一章简述了Hello的P2P,020的过程。介绍了编写论文过程中使用的软硬件环境,以及开发工具。列出为编写论文生成的中间结果文件的名字和文件的作用。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
(以下格式自行编排,编辑时删除)
预处理的概念:
预处理就是通过预处理器cpp操作hello.c,根据hello.c中所有以 “#”开头的命令,比如一些宏定义、条件编译、include<.h>等,修改原始的C程序,并将引用的所有库展开合并成为一个完整的文本文件。
预处理的作用:
1.宏定义:预处理可以通过宏定义来定义一些常量、函数或代码片段,以提高代码的可读性和重用性。通过宏定义,可以将一些重复使用的代码片段定义为宏,然后在程序中多次使用,减少了代码的重复书写,也便于统一管理和修改。
2.文件包含:预处理还可以通过文件包含指令(如#include)将多个源代码文件合并在一起,以便于模块化开发和组织代码结构。通过文件包含,程序员可以将一些共用的代码单独存放在一个文件中,然后在需要的地方包含进来使用。
3.条件编译:预处理还可以通过条件编译指令(如#ifdef、#ifndef、#if)根据条件选择性地包含或排除某些代码片段。这样可以实现在不同的编译条件下编译不同的代码,使程序更灵活、更具有可配置性。
2.2在Ubuntu下预处理的命令
命令:gcc hello.c -E -o hello.i
2.3 Hello的预处理结果解析
hello.i文件中有3061行比hello.c多出很多,但是main函数中的内容被完整保存了下来,预处理主要是对头文件进行了宏展开,并且插入到了hello.i文件当中去。
2.4 本章小结
这一章介绍了预处理的概念和作用,用hello.c文件实际操作的方法展现了预处理的过程和处理后hello.i文件的内容和解析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念:编译就是将预处理文件(hello.i)生成汇编文件(hello.s)的过程。编译器cc1对hello.i经过一系列的语法分析、语义分析,并根据优化等级(-O)进行优化之后生成了相应的汇编代码文件,汇编代码文件是一个文本文件,是一些低级机器语言指令。
编译的作用:生成的汇编文件每条语句都以一种文本格式描述了一条低级机器指令,汇编语言为不同的高级语言的不同编译器提供了通用的输出语言,汇编语言相对于预处理文件更利于机器理解。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.c -o hello.s
3.3 Hello的编译结果解析
汇编文件内容如上图所示
我针对c语言代码和汇编文件,逐一分析
3.3.1类型
c语言代码中一共定义了三个变量argc,arcv[],i和两个字符串。
1.两个字符串分别被存入了LC0和LC1
- 变量在汇编中一般会存入寄存器%rdi和寄存器%rsi中,这里整型变量argc存入了%edi,因为int是一个字节所以用%edi不需要%rdi,字符数组argv[]被存入了寄存器%rsi中。通过两个mov操作,分别把edi和rsi中的值复制到-20(%rbq)和-32(%rbp)当中,是两个内存。
- L2中通过mov操作赋值给-4(%rbp)为0,jmp跳转到L3中,L3中将-4(%rbp)和9进行比较,通过c语言代码我们知道for循环当中i从0到9,由此我们得知i就是-4(%rbp)。
3.3.2操作
通过类型的分析我们已经看到了很多操作,我们一一分析!
1.mov操作是赋值,后面跟的l,b,q等等都是代表赋值的大小是比特还是字节。
这里就是把0赋值给-4(%rbp)。
2.算术操作:
Add这里就是给i加1,同样在汇编文件中还有很多给%rax加16或者32的行,是给函数结果的寄存器进行算术操作
3比较操作:如3.3.1中比较i和9的大小来判断是否结束for循环
- 函数操作:callq很多都是调用函数。比如printf函数,sleep函数都需要callq来调用
经过上述分析,我已经将c语言代码中每一行都在汇编文件中找到对应代码以及分析。
3.4 本章小结
这一章主要介绍了编译的概念和作用,将hello.c生成hello.s汇编文件的过程以及读懂汇编文件中语句的含义。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:把汇编语言书写的程序.s文件翻译成与之等价的机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件.o文件,.o文件是二进制文件。
汇编的作用:将汇编代码翻译为机器语言指令,即机器能识别的二进制代码,使其在链接过后能被机器识别并运行。
4.2 在Ubuntu下汇编的命令
命令:gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
查看指令:readelf -a hello.o > hello_elf生成hello.o文件的elf格式
- ELF头:ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述,其中目标文件中每个节都有一个固定大小的条目(entry)。
- 节头:记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。
- 重定位节:.rela.text,一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令不需要修改。调用外部函数的指令需要重定位;引用全局变量的指令需要重定位; 调用局部函数的指令不需要重定位;在可执行目标文件中不存在重定位信息。
- 符号表:.symtab,一个符号表,它存放在程序中定义和引用的函数和全局变量的信息,一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o > hello_asm.txt
1.机器语言的构成
在计算机中,机器语言是由一系列的0、1代码构成的,在反汇编文件中可以看到机器代码由16进制数来描述
2.与汇编语言的映射关系
每一个机器指令序列都包含的操作码、操作数等信息,以此一一对应着每一种汇编指令,把汇编转换成为机器语言
3.机器语言中的操作数与汇编语言不一致分析
首先,机器语言中使用的操作数均为16进制表示,原始的汇编语言中的操作数都是10进制表示的。此外,编译阶段没有保留符号的名字,所以函数调用均写成主函数 + 偏移量的形式。
汇编代码hello.s中函数调用的call指令使用的是函数名称,反汇编代码中的call指令使用的是main函数相对偏移地址。由于函数只有在链接后才能确定所运行执行的地址(可以看到call指令对应机器代码的后四个字节均为0),所以为其添加重定位条目。
对机器语言而言,给定文件开始位置就能够将合法字节序列唯一地翻译解释成有效指令;但是机器反汇编代码的操作数却会被映射为特定字节或“小/大端序”来表示的十六进制立即数。
4.5 本章小结
这一章主要介绍了汇编的概念和作用,分析了hello.o的elf头,分析了反汇编和汇编的区别。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接的概念:链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
链接的作用:链接可以使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以将它分解成更小、更好管理的模块,可以独立地修改和编译这些模块,当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
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的格式
命令:readelf -a hello > hello1.elf
- .ELF头:hello的文件头和hello.o文件头不同之处的在于hello是一个可执行目标文件,有27个节
- 节头:对 hello中所有的节信息进行了声明,其中包括大小以及在程序中的偏移量。
- 程序头:
- .重定位节.rela.text:
- 符号表.symtab:
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
、
可以看到,hello的虚拟是从0x401000开始的,根据hello的节头部表,我们得知.interp的偏移量是0x0002e0,所以它的位置应该就在0x4012e0,查看一下
同理我们通过偏移量还能找到其他节的位置。
5.5 链接的重定位过程分析
命令:objdump -d -r hello > hello_asm1.txt
与hello.o的反汇编文件对比可以发现
(1)Hello的反汇编相较于hello.o的反汇编,每行指令都有唯一的虚拟地址,这是因为hello经过链接,已经完成重定位,每条指令的地址关系已经确定。
(2)扩充了很多函数代码,增加了.init段和.plt段,包括程序加载后执行main前的一些准备工作,以及hello需要用到的一些库函数的定义,这是因为动态链接器将共享库中hello.c用到的函数加入可执行文件中。
原本在hello.o中等待重定位而暂时置0的地址操作数,成功进行了重定位,并计算了偏移量,被设置为了虚拟地址空间中的地址。
链接过程描述:链接器在重定位步骤中,合并输入模块并将运行时地址赋给输入模块定义的每个节、符号。当这一步完成时,程序中的每条指令和全局变量才拥有唯一的运行时内存地址。
5.6 hello的执行流程
程序名称 | 程序地址 |
_ init | 0x401000 |
_start | 0x4010f0 |
main | 0x401125 |
puts@plt | 0x401030 |
printf@plt | 0x401040 |
atoi@plt | 0x401060 |
exit@plt | 0x401070 |
sleep@plt | 0x401080 |
getchar@plt | 0x401050 |
5.7 Hello的动态链接分析
动态链接采用了延迟加载的策略,只有在调用函数的时候才会进行符号的映射。动态链接通过使用偏移量表GOT和过程链表PLT的协同工作来实现。
GOT表中存放着函数的目标地址,PLT表则使用GOT中的地址来跳转到目标函数,在程序执行的过程中,dl_init负责修改PLT和GOT。
首先先找到GOT的地址
.got PROGBITS 0000000000403ff0 00002ff0 0000000000000010 0000000000000008 WA 0 0 8 ->起始地址为403ff8
前
后
5.8 本章小结
这一章主要介绍了链接的概念和作用,分析比较了hello和hello.o的ELF文件的异同,仔细分析了动态链接过程中的各种变化。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:进程是指计算机中正在运行的程序的实例。每个进程都拥有自己的地址空间、资源、状态和线程。进程是操作系统进行资源分配和调度的基本单位,它可以独立运行,拥有自己的内存空间和上下文环境。
进程的作用:进程的主要作用在于实现并发执行、资源管理和程序隔离。通过进程,操作系统可以同时运行多个程序,实现多任务并发执行。进程有自己的内存空间和寄存器,因此可以隔离不同程序之间的内存和数据,保证程序之间的安全性和稳定性。进程还负责管理系统资源的分配和调度,包括CPU、内存、文件和设备等资源。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash的作用:
shell是一个交互型的应用程序,代表用户运行其他程序,执行一系列的读、求值步骤,然后终止。
Shell-bash的处理流程:
shell的基本功能是解释并运行用户的指令,重复如下处理过程:
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
(4)如果不是内部命令,调用fork( )创建新进程/子进程
(5)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(6)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid()(或wait…等待作业终止后返回。
7)如果用户要求后台运行(如果命令末尾有&号),则shell返回;
6.3 Hello的fork进程创建过程
父进程沟通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
输入执行hello的命令后,由于输入的命令不是一个内置命令,shello就会调用fork函数创建一个子进程,将程序在该子进程的上下文中运行。
6.4 Hello的execve过程
execve函数加载并运行可执行目标文件hello,且带参数列表argv(也就是学号 姓名 秒数)和环境变量envp。只有出现错误,execve才会返回到调用程序。execve调用一次并从不返回。execve函数在当前进程的上下文中加载并运行一个新的程序。它会覆盖当前进程的地址空间,但并没有创建一个新进程。进的进程仍然有相同的PID,并且继承了调用execve函数时已打开的所有文件描述符。
execve函数加载并运行hello需要一下几个步骤:
删除已存在的用户区域。
映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构,这些区域都是私有,写时复制的。
映射共享区域。共享对象都是动态连接到hello的,然后再映射到用户虚拟地址空间中的共享区域内。
设置程序计数器(PC)。设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
1.上下文信息
上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成(不包含代码段和数据段)。
2.进程的时间片
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
3.用户模式和内核模式
处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任 何命令,并且可以访问系统中的任何内存位置。上下文切换的时候,进程就处于内核模式。
对于hello程序,程序在调用了sleep()后就会进入内核模式,并会进行上下文切换。程序运行到getchar()的时候,内核也会进行上下文切换,让其他进程运行。
除此之外,系统还会为hello程序分配时间片,只要hello时间片被用完,即使没有执行到getchar()或者sleep()函数,系统就会判断当前程序的运行时间已经足够久了,从而进行上下文切换,将处理器让给其他进程,这样可以提升程序运行的效率。
6.6 hello的异常与信号处理
(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
异常分为四类:
中断:异步发生,是来自处理器外部的I/O设备的信号的结果。硬件中断的异常处理程序称为中断处理程序。
陷阱:是有意的异常,最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
故障:由错误情况引起,可能能被故障处理程序修正。如果能修正,就将控制返回到引起故障的指令,从而重新执行它。否则返回到内核中的abort例程,终止引起故障的应用程序。
终止:是不可恢复的致命错误造成的结果,终止处理程序从不将控制返回给应用程序,将控制返回给一个abort例程,终止这个应用程序。
- 按回车:
回车被缓存到stdin,当getchar读出一个回车’\n’作为结束符的字串,其他字串会被当成shell的命令输入。
2.不停乱按:
无关输入被缓存到stdin
3.ctrl c:
终止
4.ctrl z
终止
5.ps:
- Jobs
显示已暂停进程
- Pstree
将所有进程以树状图显示,树状图将会以 pid (如果有指定) 或是以 init 这个基本进程为根 (root),如果有指定使用者 id,则树状图会只显示该使用者所拥有的进程。
- Kill
杀死进程
6.7本章小结
本章介绍了进程的概念和作用,简述了壳Shell-bash的作用与处理流程。对hello的fork和execve过程进行了详细说明,结合进程上下文信息、进程时间片,进程调度的过程,用户态与核心态转换等解说了hello的进程执行。对hello程序运行过程中如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后运行ps jobs pstree fg kill 等命令,分别进行了梳理和说明,对各种异常和信号等进行了介绍。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1.逻辑地址(Logical Address)是指由程序产生的与段相关的偏移地址部分。在这里指的是hello.o中的内容。
2.线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生段中的偏移地址,加上相应段的基地址就生成了一个线性地址。
3.虚拟地址:CPU启动保护模式后,程序hello运行在虚拟地址空间中。注意,并不是所有的“程序”都是运行在虚拟地址中。CPU在启动的时候是运行在实模式的,Bootloader以及内核在初始化页表之前并不使用虚拟地址,而是直接使用物理地址的。
4.物理地址:放在寻址总线上的地址。放在寻址总线上,如果是读,电路根据这个地址每位的值就将相应地址的物理内存中的数据放到数据总线中传输。如果是写,电路根据这个地址每位的值就在相应地址的物理内存中放入数据总线上的内容。物理内存是以字节(8位)为单位编址的。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段描述符是一种数据结构,实际上就是段表项,分两类:
用户的代码段和数据段描述符
系统控制段描述符,又分两种:
特殊系统控制段描述符,包括:局部描述符表(LDT)描述符和任务状态段(TSS)描述符
控制转移类描述符,包括:调用门描述符、任务门描述符、中断门描述符和陷阱门描述符
描述符表实际上就是段表,由段描述符(段表项)组成。有三种类型
全局描述符表GDT:只有一个,用来存放系统内每个任务都可能访问的描述符,例如,内核代码段、内核数据段、用户代码段、用户数据段以及TSS(任务状态段)等都属于GDT中描述的段
局部描述符表LDT:存放某任务(即用户进程)专用的描述符
中断描述符表IDT:包含256个中断门、陷阱门和任务门描述符
被选中的段描述符先被送至描述符cache,每次从描述符cache中取32位段基址,与32位段内偏移量(有效地址)相加得到线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。36位VPN被划分为四个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含L1页表的物理地址。VPN1提供到一个L1 PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2 PTE的偏移量,以此类推。
7.5 三级Cache支持下的物理内存访问
完成从虚拟地址到物理地址的转换后,就可以使用物理地址进行内存访问。
Intel Core i7使用了三级cache来加速物理内存访问,L1级cache作为L2级cache的缓存,L2级cache作为L3级cache的缓存,而L3级cache作为内存(DRAM)的缓存。已知Core i7的三级cache是物理寻址的,块大小为64字节。LI和L2是8路组相联的,而L3是16路组相联的。Corei7实现支持48位虚拟地址空间和52位物理地址空间。因为L1块大小为64字节,所以B=64,b=6.因为L1是8路组相联所以S=8,s=3.所以标记位为43位。根据物理地址的s位组索引索引到L1 cache中的某个组,然后在该组中查找是否有某一行的标记等于物理地址的标记并且该行的有效位为1,若有,则说明命中,从这一行对应物理地址b位块偏移的位置取出一个字节,若不满足上面的条件,则说明不命中,需要继续访问下一级cache,访问的原理与L1相同,若是三级cache都没有要访问的数据,则需要访问内存,从内存中取出数据并放入cache。三级cache不仅仅支持数据指令的访问,也支持页表条目的访问,在MMU进行虚拟地址到物理地址的翻译过程中,三级cache也会起作用。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行hello,需要以下几个步骤:
1.删除已存在的用户区域
2.映射私有区域。创建新的区域结构,这些新的区域都是私有的、写时复制的。代码和初始化数据映射到.text和.data区(目标文件提供),.bss和栈堆映射到匿名文件,栈堆的初始长度0.
3.映射共享区域。共享对象由动态链接映射到本进程共享区域。
4.设置PC,指向代码区域的入口点。Linux根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
在虚拟内存的习惯说法中,DRAM缓存不命中称为为缺页。缺页故障是指当指令引用一个虚拟地址,而与该地址相对应的物理页面不在内存中,因此必须从磁盘中取出时,就会发生故障。
缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中了,指令就可以没有故障地运行完成。
缺页处理程序执行的步骤:
虚拟地址A是合法的吗?即A是否在某个区域结构定义的区域内。缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较。如果指令不合法,就触发段错误,从而终止。
进行的内存访问是否合法?即进程是否有读、写或者执行这个区域内页面的权限。例如对代码段的只读页面进行写操作,一个运行在用户模式中的进程试图从内核虚拟内存中读取字,像这些不合法的访问,缺页处理程序会触发一个保护异常,从而终止。
此时经过上两个步骤,内核知道了这个缺页是对合法的虚拟地址进行合法的操作造成的。然后它选择一个牺牲页面,如果这个页面被修改过,就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU。这次,MMU就能正常翻译A,而不会再产生缺页中断。
7.9动态存储分配管理
动态储存分配管理使用动态内存分配器(如malloc)来进行。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合。每个块就是一个连续的虚拟内存页,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。动态内存分配主要有两种基本方法与策略:
7.9.1 带边界标签的隐式空闲链表分配器管理
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、(可能的)额外填充以及一个字的尾部组成。
隐式空闲链表:空闲块通过头部的大小字段隐含地连接着。分配器遍历堆中所有的块,间接地遍历整个空闲块的集合。
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配和最佳适配。分配器在面对释放一个已分配块时,可以合并相邻的空闲块,其中一种简单的方式,是利用隐式空闲链表的边界标记来进行合并。
7.9.2 显式空间链表管理
显式空闲链表是将堆的空闲块组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。进行内存管理。在显式空闲链表中。可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
7.10本章小结
本章主要介绍了hello 的存储器地址空间、段式管理、页式管理, VA 到PA 的变换、物理内存访问,fork、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理的相关内容
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux中所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得所有的输入和输出都能以一种统一且一致的方式来执行:打开文件、改变当前的文件位置、读写文件、关闭文件。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix IO接口:Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得所有的输入和输出都能以一种统一且一致的方式来执行
函数:
open和close - 打开和关闭文件
read 和 write – 最简单的读写函数;
readn 和 writen – 原子性读写操作;
iseek函数:修改文件偏移量
8.3 printf的实现分析
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;
}
printf函数中调用了vsprint和write两个函数。
vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
调用write系统函数后,程序进入到陷阱,系统调用 int 0x80或syscall等,将buf中的i个字符写到终端,由于i保存的是结果字符串的长度,因此write将格式化后的字符串结果写到终端。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
最后程序返回我们实际输出的字符数量i。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
int getchar(void)
{
static char buf[BUFSIZ];
static char* bb = buf;
static int n = 0;
if(n == 0)
{
n = read(0, buf, BUFSIZ);
bb = buf;
}
return(--n >= 0)?(unsigned char) *bb++ : EOF;
}
getchar函数会从stdin输入流中读入一个字符。调用getchar时,会等待用户输入,输入回车后,输入的字符会存放在缓冲区中。第一次调用getchar时,需要从键盘输入,但如果输入了多个字符,之后的getchar会直接从缓冲区中读取字符。getchar的返回值是读取字符的ASCII码,若出错则返回-1。
getchar的底层实现是通过系统函数read实现的。getchar通过read函数从缓冲区中读入一行,并返回读入的第一个字符,若读入失败则返回EOF。read的具体实现如下:
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了hello中包含的函数所对应的unix I/O,以及printf和getchar
函数的实现。
(第8章1分)
结论
过程:
编程:得到hello.c的c代码。
预处理:预处理器扩展源代码,插入所有用#include命令指定的文件,并扩展所有用#define声明指定的宏。得到hello.i文件。
编译:编译器将文本文件hello.i翻译成汇编语言文件hello.s。
汇编:汇编器将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件hello.o中。
链接:链接器将hello.o与其他可重定位目标文件和动态链接库链接成为可执行文件。
执行:在终端中输入./hello 120L020212 曾正维 1。
创建子进程:由于输入的命令不是内置的shell命令,因此调用fork函数创建一个子进程。
加载程序:shell调用execve函数,加载并运行hello,映射虚拟内存。
动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。 10.信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态
感悟:计算机系统设计是一个复杂但是完备的过程,它确保程序运行的稳定性及准确性,它不仅仅是硬件和软件的结合,更是对计算机科学原理、算法、数据结构、操作系统、编程语言等多个领域深入理解的体现。
创新想法:模块化设计:将系统拆分为若干独立的模块,每个模块负责特定的功能或任务,提高系统的可维护性和可扩展性。
虚拟化技术:通过虚拟化技术,将物理资源如CPU、内存和存储等虚拟化为多个逻辑资源,实现资源的灵活分配和利用。
自动化运维:引入自动化运维工具和技术,如CI/CD(持续集成/持续交付)和DevOps,实现系统的自动化部署、监控和管理。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
(附件0分,缺失 -1分)
文件名 | 作用 |
hello.c | hello的源文件 |
hello.i | 源文件经过预处理后的文件 |
hello.s | 经过编译生成的汇编文件 |
hello.o | 汇编生成的可重定位目标文件 |
hello | 链接生成的可执行文件 |
hello_elf | hello.o的elf文件 |
hello_asm | 将hello.o反汇编生成的文件 |
hello1.elf | hello的elf文件 |
hello1_asm | hello的反汇编文件 |
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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] 兰德尔.E.大卫.R. 深入理解计算机系统[M]. 北京:机械工业出版社,2017:7.
(参考文献0分,缺失 -1分)