计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 人工智能(未来技术)
指 导 教 师 刘宏伟
计算机科学与技术学院
2021年5月
本文通过分析hello.c从诞生到死亡的全过程,包括预处理、编译、汇编、链接、在进程中执行及被销毁被操作系统回收,较全面的回答了计算机如何操控大局“软”“硬”结合完成程序的执行的问题,内容涉及汇编、链接、存储管理、进程管理、IO管理等。
关键词:hello.c;程序;编译;链接;进程;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
程序员写完C语言代码后,利用gcc编译器对C语言程序执行编译命令。
P2P过程:
From Program to Process,hello.c文件先经过预处理器生成hello.i,再经过编译器生成hello.s(汇编程序),然后经过汇编器as生成可重定位目标程序hello.o,最后通过链接器ld链接生成可执行文件hello。在Linux终端执行./hello命令,运行该可执行文件(Process)。
O2O过程:
From Zero-0 to Zero-0,可执行文件hello执行后,shell通过execve函数和fork函数创建子进程并将hello载入,映射虚拟内存,进入程序入口后将程序载入物理内存,开始执行目标代码,CPU执行逻辑控制流。在进程中,TLB、4级页表、3级Cache,Pagefile等等设计会加快程序的运行。程序运行完成后,bash会回收子进程,内核清楚数据痕迹。hello程序重新回归0。
1.2 环境与工具
硬件环境:Intel® Core™ i5-9300H CPU;2.40GHz;8G RAM;120GHD Disk 以上
软件环境:win10家庭中文版、vmware16、Ubuntu20.04LTS
开发与调试工具:VSCODE、objdump、gdb、edb、hexedit、gcc等
1.3 中间结果
hello.c:hello源文件
hello.i:预处理后生成的文件
hello.s:调用ccl编译hello.i形成的hello.s,用于分析编译过程
hello.o:调用汇编器as将hello.s汇编成的hello.o,用于分析汇编过程
hello:链接后生成的可执行文件
1.4 本章小结
本章简单介绍了hello程序,本实验的硬件环境、软件环境、开发工具以及本实验中生成的中间结果文件的名字和作用。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
基本概念:
是指在源代码编译之前对源代码进行的错误处理,一般在源代码被翻译为目标代码的过程中,生成二进制代码之前。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序,并删除注释。C和C++中常见的编译程序需要进行预处理的程序指令主要有#define、#include、#error等。
作用:
预处理的功能包括宏定义,文件包含,条件编译三部分,分别对应宏定义命令、文件包含命令、条件编译命令三部分实现。预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行响应的转换。预处理过程还会删除程序中的注释和多余的空白字符。
上述处理可以使一个源代码编译程序可以在不同的程序运行语言环境中被各种语言编译器更方便地编译。
2.2在Ubuntu下预处理的命令
对hello.c文件进行预处理,指令为:gcc -E -o hello.i hello.c
图 2-2-1 预处理命令
2.3 Hello的预处理结果解析
图 2-3-1 hello.i的文件截图(部分)
通过上述文件截图和Hello.c的对比,看出原本内容很少的hello.c文件被扩充成了很多内容,代码从23行扩充到3060行。编译器分别对#include<stdlib.h>、#include<stdio.h>、#include<unistd.h>进行了相应处理,对于原文件的宏进行了宏展开,引入头文件内容,同时注释也被删除了。
2.4 本章小结
本章介绍了预处理的概念、预处理的运行机制及其作用。我们分析了.c文件预处理的结果(.i)文件
在预处理时。编译器对程序代码进行第一次修改,主要针对头文件、宏定义以及注释进行操作,将结果保存在相应的.i文件中。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:将程序员所撰写的编程语言翻译成汇编语言的过程。
作用:通过语法检验,代码优化等过程,将程序员便于记忆和认知的编程语言转化为机器可识别的语言。)
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
gcc -S -v hello.c -o hello.s
图 3-2-1 编译过程
3.3 Hello的编译结果解析
3.3.1 汇编指令
图 3-3-1汇编指令
指令内容(按照上图顺序):
.file: C语言源文件名
.text:代码段
.section .rodata: 只读数据段
.align8: 声明对指令或者数据的存放地址进行对齐的方式
.string:声明一个字符串类型
.globl :声明一个全局变量
.type: 声明是函数类型还是对象类型
3.3.2 数据
Hello.c中的数据有三种类型:整型、数组、字符串
- 字符串
字符串常量:”用法: Hello 学号 姓名 秒数!\n” “Hello %s %s\n”
图 3-3-2 字符串常量1
图 3-3-3 字符串常量2
如图所示,在使用时需给出其地址,将地址给寄存器后调用相应的函数
- 整型
- 数组
Main函数有两个参数,分别为有符号数argc和字符型数组指针argv,argc是main函数的第一个参数,也是整个程序的第一个参数,先由%edi保存argc(函数的第一个参数),最后argc被存放在栈中栈指针%rbp-20的位置。argv数组是main函数的第二个参数,数组元素为char类型。在hello.s函数中,%rsi寄存器保存argv数组首地址。(如下图所示)
图 3-3-4 两个参数的存放
3.3.3操作
- 赋值:mov语句
图 3-3-5 mov语句实现赋值
- 算术:addl实现加法操作
图 3-3-6 addl语句
算术操作指令还包括inc,dec,neg,sub,imul等等
- 关系操作:cmp命令实现。
①
图 3-3-7 判断argc取值是否不等于4
②
图 3-3-8 for循环结束的判定条件,看i是否小于等于7
- 数组操作
图 3-3-9 数组操作
在访问argv里的数组元素时,利用寄存器%rax来计算地址,访问argv[1]、argv[2]、argv[3],分别存储着学号、姓名及秒数。
- 控制转移
①
图 3-3-10 if判断argc取值的控制转移
②
图 3-3-11 判断for循环结束的控制转移
- 函数操作:
①调用:利用call指令进行函数调用
②传参:大部分是通过寄存器实现,但是通过寄存器最多可以传递6个参数,有多余的参数就会利用栈来传递。
图 3-3-12 函数printf用%rdi传参
图 3-3-13 函数exit用%edi传参
图 3-3-14 函数printf用%rdi、%rsi、%rdx传参
图 3-3-15 函数atoi通过%rdi传参
图 3-3-16 函数sleep通过%edi传参
3.4 本章小结
本章介绍了编译的概念和作用,并针对汇编程序hello.s,分析了编译器对各种数据以及各类操作的处理。在这个过程中,编译器可能会按照自己的理解(一系列算法),对原始代码结构和数据做出调整.
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:
汇编器(as)将.s汇编程序翻译成机器语言指令,然后把这些指令按照可重定位目标程序的格式进行打包,打包完成后保存在后缀为.o 目标文件中,该文件是一个二进制文件,它包含程序的指令编码。
汇编的作用:
将汇编语言转换成最底层的机器语言——真正机器可以读懂的二进制代码。
4.2 在Ubuntu下汇编的命令
汇编的命令:gcc hello.s -c -o hello.o
图 4-1 汇编命令
4.3 可重定位目标elf格式
4.3.1 ELF头
在终端输入命令:readelf -h hello.o
图 4-2 ELF头
图为输入命令后的运行结果,可以看到: 16字节序列为7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00,表明系统的字的大小为8字节,字节顺序为小端序。ELF头中包含了ELF头的大小:64字节;目标文件的类型:REL(可重定位文件);机器类型:Advanced Micro Devices X86-64;节头表的起始位置为1240;文件中共有14节;节头部表中条目的数量:13
4.3.2 节头表
在终端输入命令:readelf -S hello.o
图 4-3 节头表
图为输入命令后的运行结果,可以看到:节头表中一共有14项,其中第一项为空。剩下13项对应于可重定位文件中每一节的相关内容,其中包含每一节的名字,类型,地址(由于还未重定位所以每一节的地址都用0代替),在文件中的偏移量,节的大小,访问权限,对齐方式等等。
4.3.3 符号表
在终端输入命令:readelf -s hello.o
图 4-4 符号表
图为输入命令后的运行结果,符号表中存储了程序中定义和使用的各种符号,包括函数名,全局变量名等等。其中每一个符号有其对应的value,size,type,name等等内容。Bind字段表明符号是本地的还是全局的。
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o
图 4-5 反汇编结果
与hello.s对比,主要差异如下:
1.hello.s中call语句后紧跟着函数名,而在hello.o反汇编中call语句后跟着的是相对地址,显示相应的重定位条目,因为未链接无法确定绝对地址。
2.hello.s中跳转语句后紧跟着的是.L2.L3这样的标签,而在hello.o反汇编中跳转语句后跟着的是相对地址。
3.反汇编代码中对于全局变量的访问0x0(%rip),hello.s文件中为.LC0(%rip),和函数调用一样, rodata 中数据地址是在运行时确定,故访问需要重定位。
4.5 本章小结
本章介绍了汇编的概念和作用,使用Ubuntu下的汇编指令将将hello.s转换为.o可重定位目标文件,并分析了hello.o文件各部分,比较hello.s和hello.o的不同之处,了解了汇编代码和机器代码之间的区别和联系。
(第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-1 链接命令
5.3 可执行目标文件hello的格式
图 5-2
对比上一章hello.o的elf文件节头表,可以发现在hello中每一节都有了实际地址,说明重定位工作已完成。除此之外好多出了很多节,多出的节是为了后续链接器能够实现动态链接。
此外,对ELF头进行分析,可以发现入口地址不为0,说明重定位工作完成。
图 5-3 ELF头
5.4 hello的虚拟地址空间
图 5-4 hello的起始地址
由5.3的ELF头可知入口地址为0x4010f0,在edb中找到对应位置,从节头表中可以看出该地址对应.text段的起始地址
图 5-5 .text段
再根据节头表找到其他各段的信息,下面列出了.rodata段(0x402000)和.data段(0x404048)
图 5-6 .rodata段
图 5-7 .data段
5.5 链接的重定位过程分析
图 5-8 hello的反汇编结果(部分)
指令:objdump -d -r hello
分析hello与hello.o的不同,说明链接的过程。
函数个数:在动态共享库libc.so中,定义了hello需要的printf、sleep、getchar、exit 函数和_start 中调用的 __libc_csu_init,__libc_csu_fini,__libc_start_main。这些函数在链接时被链接器加入其中。
节的改变:增加了.init和.plt节,和一些节定义中的函数。
函数调用:链接器解析重定条目时需要对R_X86_64_PLT32进行重定位。在对外部内存调用函数的地址进行重定位后,链接器针对已经确定的.text与对应的.plt节的相对距离调用地址计算相对偏移距离,修改下条指令的相对偏移地址,将其指向链接器对应的函数,这样hello.o的相对距离偏移调用地址本身就变成了链接器hello.o中的虚拟内存调用地址。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
重定位的方法:链接器通过把每个符合定义与一个内存位置关联起来,然后修改所有对这些符号的引用,从而重定位这些节。
通过call sleep来看一下hello是怎么对其重定位的。
图 5-9 hello.o反汇编中的call sleep语句
图 5-10 hello.o的ELF有关sleep的可重定位条目
这些信息告诉链接器修改开始于偏移量0x74处的32位PLT相对引用,这样在运行时会指向sleep的例程,接着链接器会按照0xR_X86_64_PLT32重定位类型具体的规定计算出相应的数值
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
ld-2.27.so!_dl_start
ld-2.27.so!_dl_init
hello!_start
ld-2.27.so!_dl_start_main
ld-2.27.so!_cxa_atexit
hello!_libc_csu_init
libc-2.27.so!setjump
hello!printf@plt
hello!atoi@plt
hello!sleep@plt
hello!getchar@plt
hello!exit@plt
分析上述函数,其执行过程为:先载入(_dl_start,_dl_init),然后执行初始化过程(_start,_libc_start_main,_init),之后执行主函数(_main,_printf,_exit,_sleep,_getchar),最后退出。
5.7 Hello的动态链接分析
动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
图 5-11 对应的位置
图 5-12 调用dl_init之前
图 5-13 调用dl_init之后
通过这两幅图可以看到,调用dl_init之后这两个位置的8字节全都发生了改变
5.8 本章小结
本章主要介绍了链接的概念和作用,说明了链接生成一个可执行文件的完整过程。同时通过查看hello的虚拟地址空间存放的,对比hello与hello.o的反汇编代码,更好的掌握了链接与重定位的过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:操作系统对一个正在运行的程序的一种抽象。是一个执行中程序的实例.每次用户通过向shell 输入一个可执行目标文件的名字,运行程序时, shell 就会创建一个新的进程。
作用:一个程序在系统上运行时,操作系统会提供一种程序在独占这个系统,包括处理器,主存,I/O设备的假象。处理器看上去在不间断地一条一条执行程序中的指令。进程也被称为计算机科学中最伟大的创新。
6.2 简述壳Shell-bash的作用与处理流程
shell是一个应用程序,他在操作系统中提供了一个用户与系统内核进行交互的界面。shell(即壳)是一个简单的命令解释器,它允许系统接收到一个用户的命令,然后自动调用相应的命令执行应用程序。
处理过程:从终端读入输入的命令。将输入字符串切分获得所有的参数如果是内置命令则立即执行否则调用相应的程序为其分配子进程并运行shell应该接受键盘输入信号,并对这些信号进行相应处理。
6.3 Hello的fork进程创建过程
父进程可以通过fork函数创建一个新的运行的子进程,其函数声明为:
pid_t fork(void),子进程享有与父进程相同但各自独立的上下文,包括代码、堆、数据段、共享库以及用户栈。
在父进程中,fork函数返回子进程的PID,在子进程中,fork函数返回0.
当我们在终端中输入./hello时,shell会先判断发现这个参数并不是Shell内置的命令,于是就把这条命令当作一个可执行程序的名字,它的判断显然是对的。
接下了shell会执行fork函数为hello创建进程。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。其函数声明为:int execve(const char *filename,const char *argv[],const char *envp[]),
Execve函数加载并运行可执行目标文件filename,且带参数列表argc和环境变量列表envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序,所以,与fork函数一次调用返回两次不同,execve调用一次从不返回。
在程序头部表的引导下,加载器将可执行文件的片复制到代码段和数据段,之后,加载器跳转到程序的入口:_start函数的地址。_start函数调用系统启动函数,_libc_start_main(该函数定义在libc.so里),之后初始化环境,调用用户层的main函数,处理main函数返回值,并且在需要的时候返回给内核。
6.5 Hello的进程执行
①上下文信息
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
②进程时间片
时间片是分时操作系统分配给每个正在运行的进程微观上的一段CPU时间。
③逻辑控制流
进程为每个程序提供一种假象,好像它在独占地使用处理器。事实上,CPU为每个进程分配时间片,通过逻辑控制流不停的切换当前进程。每个进程执行它的流的一部分,然后被抢占(暂时挂起),然后轮到其它进程。
图 6-1 逻辑控制流
④进程调度
在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个决策称为调度。调度的过程是由调度器完成的,当内核调度新的进程运行后,它就会抢占当前进程,并进行:
1)保存以前进程的上下文
2)恢复新恢复进程被保存的上下文
- 将控制传递给这个新恢复的进程,来完成上下文切换。
图 6-2 进程调度
6.6 hello的异常与信号处理
信号:
1.中断: 运行 Hello 程序时, 外部 I/O 设备可能出现异常 。
2.陷阱:陷阱是故意的,是执行的。命令结果表明,哈洛在执行sleep函数时出现上述异常。
3.故障:在运行 hello 程序时,可能会发生页面缺陷的错误操作。
- 退出: 退出时不可恢复, hello 运行中可能出现 DRAM 节点损坏的编码错误 。
处理:
图 6-3 乱按键盘不影响输出结果
图 6-4 Ctrl+z让进程暂时挂起
图 7-5 输入ps发现进程未关闭
图 7-6 输入jobs发现进程已经停止
图 7-7 输入fg继续运行
图 7-8 使用kill杀死进程
图 7-9 使用ctrl+c向进程发送sigint信号让进程直接结束
6.7本章小结
本章我们主要了解了hello进程的执行过程,主要讲述了hello程序创建、加载和终止。同时我们也通过这章的学习,我们也了解到了进程的用户模式和内核模式、上下文切换、两个重要函数(fork和execve)、四大异常(中断、陷阱、故障、终止)等等。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
线性地址:地址空间是一个非负整数地址的有序集合,而如果此时地址空间中的整数是连续的,则我们称这个地址空间为线性地址空间。CPU在保护模式下,“段基址+段内偏移地址”为线性地址,线性地址通常用十六进制数字表示,值得范围从0x00000000到0xfffffff)程序代码会产生逻辑地址,通过逻辑地址变换就可以生成一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址。如果没有启用分页机制,那么线性地址直接就是物理地址。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应。计算机系统的主存被组织成一个由 M 个连续的字节大小的单元组成的数组,其每一个字节都被给予一个唯一的地址,这个地址称为物理地址。物理地址也是计算机的硬件中的电路进行操作的地址。
逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。
虚拟地址:CPU 启动保护模式后,程序运行在虚拟地址空间中。与物理地址相似,虚拟内存被组织为一个存放在磁盘上的 N 个连续的字节大小的单元组成的数 组,其每个字节对应的地址成为虚拟地址。虚拟地址包括 VPO(虚拟页面偏移量)、 VPN(虚拟页号)、TLBI(TLB 索引)、TLBT(TLB 标记)。
比如:hello 反汇编代码中的0000000000401125 <main>: 这里的 0x401125 是逻辑地址的偏移量部分,偏移量再加上代码段的段地址就得到 了 main 的虚拟地址(线性地址),虚拟地址是现代系统的一个抽象概念,再经过 MMU 的处理后将得到实际存储在计算机存储设备上的地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
程序代码会产生逻辑地址,一个逻辑地址由两部份组成,段标识符及段内偏移量。段标识符放在段描述表中,在保护方式下,在保护方式下,每个段由如下三个参数进行定义:段基地址(Base Address)、段界限(Limit)和段属性(Attributes)。:
(1):段基地址规定线性地址空间中段的开始地址;
(2):段界限规定段的大小。
(3):段的属性表示段的特性。例如,该段是否可被读出或写入,或者该段是否作为一个程序来执行,以及段的特权级等。
下图表示一个段如何从虚拟地址空间定位到线性地址空间。图中BaseA等代表段基地址, LimitA等代表段界限。另外,段C接在段A之后,也即BaseC=BaseA+LimitA。
图 7-1
7.3 Hello的线性地址到物理地址的变换-页式管理
将程序的逻辑地址空间划分为固定大小的页,而物理内存划分为同样大小的页框。程序加载时,可将任意一页放入内存中任意一个页框,这些页框不必连续,从而实现了离散分配。该方法需要CPU的硬件支持,来实现逻辑地址和物理地址之间的映射。
在页式系统中进程建立时,操作系统为进程中所有的页分配页框。当进程撤销时收回所有分配给它的页框。在程序的运行期间,如果允许进程动态地申请空间,操作系统还要为进程申请的空间分配物理页框。操作系统为了完成这些功能,必须记录系统内存中实际的页框使用情况。操作系统还要在进程切换时,正确地切换两个不同的进程地址空间到物理内存空间的映射。这就要求操作系统要记录每个进程页表的相关信息。下图为页式管理的相关流程。
图 7-2
7.4 TLB与四级页表支持下的VA到PA的变换
VA被分成VPN和VPO两部分,VPN也被分为TLBT和TLBI两部分。其中TLBI是用于确定TLB中的组索引, TLBT用于判断PPN是否已被缓存到TLB中,如果TLB命中,则直接取出PPN,否则取出VPN到页表中查询PPN。在页表中查询PPN时,VPN会被分为四个部分,分别用作一二三四级页表的索引,而前三级页表的查询结果为下一级页表的基地址,第四级页表的查询结果为PPN。将查询到的PPN与VPO组合,得到物理地址PA。
7.5 三级Cache支持下的物理内存访问
图 7-3 典型cache结构
对于一个虚拟地址请求,在三级Cashe下,变为物理地址要经历如下步骤:
①先去TLB寻找,如果命中的话就直接去MMU获取;
②如果未命中,就会结合多级页表得到物理地址,去cache中寻找;
③先到L1,检测是否命中,不命中则紧接着寻找下一级cache L2,接着L3;相当于一级一级往下找,直到找到对应的内容,
在三级Cashe支持下,这样操作可以加快程序运行的速度。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,同时为这个新进程创建虚拟内存。
它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。因此,也就为每个进程保持了私有空间地址的抽象概念
7.7 hello进程execve时的内存映射
下面是加载并运行hello的几个步骤:
1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构
2)映射私有区域:为新程序的代码、数据、bss和栈区域创建新的区域结构。
3)映射共享区域:如果hello程序域共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4)设置程序计数器(PC)
exceve做的最后一件事是设置当前进程的上下文中的程序计数器,是指指向代码区域的入口点。而下一次调度这个进程时,他将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
图 7-4
7.8 缺页故障与缺页中断处理
缺页中断:进程线性地址空间里的页面不必常驻内存,在执行一条指令时,如果发现他要访问的页没有在内存中(即存在位为0),那么停止该指令的执行,并产生一个页不存在的异常,对应的故障处理程序可通过从外存加载该页的方法来排除故障,之后,原先引起的异常的指令就可以继续执行,而不再产生异常。
页面调度算法:将新页面调入内存时,如果内存中所有的物理页都已经分配出去,就按照某种策略来废弃整个页面,将其所占据的物理页释放出来。
缺页中断的处理:处理函数为do_page_fault函数,大致流程中为:
(一)地址为内核空间:
1,当地址为内核地址空间并且在内核中访问时,如果是非连续内存地址,将init_mm中对应的项复制到本进程对应的页表项做修正;
2,地址为内核空间时,检查页表的访问权限;
3,如果1,2没有处理完全,跳到非法访问处理;
(二)地址为用户空间:
4,如果使用了保留位,打印信息,杀死当前进程;
5,如果在中断上下文中火临界区中时,直接跳到非法访问;
6,如果出错在内核空间中,查看异常表,进行相应的处理;
7,查找地址对应的vma,如果找不到,直接跳到非法访问处,如果找到正常,跳到good_area;
8,如果vma->start_address>address,可能是栈太小,对齐进行扩展;
9,good_area处,再次检查权限;
10,权限正确后分配新页框,页表等;
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆.系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址) .对于每个进程,内核维护着一个变量brk, 它指向堆的顶部.
分配器将堆视为一组不同大小的块的集合来维护.每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的.已分配的块显式地保留为供应用程序使用.空闲块可用来分配.空闲块保持空闲,直到它显式地被应用所分配.一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的.
分配器有两种风格,显示分配器(要求应用显式地释放任何已分配的块),隐式分配器(要求分配器检测一个已分配块是否仍然需要,不需要则释放)。
分配策略:
1.空闲链表:
隐式:在每块的头,尾部增加32位存储块大小,以及是否空闲。
显式:在隐式的基础上在头部增加对前后空闲块的指针。
分离:同时维护多个空闲链表。
2.带边界标记的合并:
利用每块头尾的大小和空闲状态信息合并空闲块。
3.无合适空闲块时,申请额外的堆空间。
7.10本章小结
本章分析了虚拟地址、线性地址和虚拟物理线性地址之间的相互转换,简单介绍了段式管理和页式管理。分析了物理内存访问、缺页故障、fork和execve函数的内存映射机制,最后介绍了动态存储的分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
(以下格式自行编排,编辑时删除)
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
hello的一生会经历如下阶段:(1) 预处理:预处理器cpp将.c文件翻译成.i的文件;(2) 编译:gcc编译器将.i文件翻译成.s格式的汇编语言文件;(3) 汇编:as汇编器将.s文件转换成十六进制机器码的.o文件;(4) 链接:ld链接器将一系列.o文件链接起来形成最终的可执行文件hello;(5) 进程创建:shell为hello程序fork一个子进程;(6) 程序运行:shell调用execve函数,映射虚拟内存,载入物理内存,进入main函数;(7) 指令执行:hello和其他进程并发地运行,CPU为其分配时间片;(8) 进程回收:shell回收子进程,系统释放该进程的数据所占的内存空间。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
完成本次大作业后,我从一个简单的Hello程序的开始到结束,深入学习了计算机系统的运作机制。这让我不禁感叹计算机设计的巧妙之处。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello.c C语言源文件
hello.i 经过预处理得到的C语言文件
hello.s 经过编译得到的汇编语言文件
hello.o 经过汇编得到的机器语言文件
hello 经过链接得到的可执行文件
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] CSDN
[2] 百度百科
[3] Computer Systems:A Programmer's Perspective Bryant,R.E..
(参考文献0分,缺失 -1分)