计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 120L021410
班 级 2003011
学 生 陈帅行
指 导 教 师 郑贵斌
计算机科学与技术学院
2022年5月
本篇论文为计算机系统的结课论文,研究hello.c在linux系统下的整个生命周期。有以下内容:对hello.c的概述;hello.c的预处理;编译;汇编;链接;一直到hello.c的进程;存储;IO管理。在linux下用gcc对其进行分析。通过分析解读linux下的hello从C代码变成可执行程序的整个过程,将本学期所学重点内容进行整理,巩固所学内容,以便学生将理论知识巩固。
关键词:hitics;hello;程序执行;计算机系统
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
Hello的P2P:From Program To Process
在linux中,键入的hello.c文件,此时hello只是一个program。通过预处理(cpp)、编译(ccl)、汇编(as)、链接(ld)这一系列操作后,生成可执行目标程序hello。在终端hello的目录下,shell中输入“./hello”命令,进程管理为其创建子进程(fork),此时hello已成为process。
Hello的020:From Zero-0 To Zero-0
execve(执行文件)在父进程中fork一个子进程后,在子进程中调用exec函数启动新的程序,映射入内存,操作系统为这个进程分时间片。当该进程的时间片到达时,操作系统设置CPU上下文环境,并跳到程序开始处。当指令将“Hello World\n”字符串中的字节从主存经一系列cache复制到寄存器文件,再从寄存器文件中经I/O管理复制到显示设备,最终显示在屏幕上。程序执行完毕,shell负责回收这个子进程,此时hello不再占用内存
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境
图1.2.1处理器 图1.2.2主板
图1.2.3内存 图1.2.4SPD
软件环境
Windows11,Ubuntu20.04
开发与调试工具:vim,gcc,as,ld,edb,readelf,HexEdit,vscode
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.i:hello.c经预处理之后得到的文本文件
hello.s:hello.i文件经编译后得到的文本文件(汇编语言源代码文件)
hello.o:hello.s文件经汇编后得到的文件(可重定位目标文件)
Hello:hello.o文件经过链接后得到的文件(可执行目标文件)
Helloelf:hello.o的ELF格式
hello2.s:hello.o的反汇编代码
helloelf2:hello的ELF格式
hello3.s:hello的反汇编代码
1.4 本章小结
本章概括论述了 hello的 p2p,020 过程,列出了所需环境:硬件环境、软件环境,开发与调试工具。以及生成的中间结果文件的名字,文件的作用等。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位——(用C/C++的术语来说是)预处理记号(preprocessing token)用来支持语言特性(如C/C++的宏调用)。
1.程序的预处理过程就是将预处理指令(可以简单理解为#开头的正确指令)转换为实际代码中的内容(替换)
2.#include<stdio.h>,这里是预处理指令,包含头文件的操作,将所包含头文件的指令替代
3.如果头文件中包含了其他头文件,也需要将头文件展开包含。【1】
2.2在Ubuntu下预处理的命令
图2.2预处理命令
如图,使用gcc -E选项可以对程序预处理编译
2.3 Hello的预处理结果解析
查看hello.i(vim实在不太方便,这里使用vscode打开查看,hello.i仅展示一部分)
图2.3.1hello的预处理
图2.3.2预处理
图2.3.3预处理
编译预处理后会在当前文件夹下生成.i文件。将所有的#define删除,并且展开所有的宏定义;处理所有条件编译指令,如#if,#ifdef等;处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。该过程递归进行,及被包含的文件可能还包含其他文件。删除所有的注释//和 /**/;添加行号和文件标识。保留所有的#pragma编译器指令,因为编译器需要使用它们;打开此.i文件,会发现经过预处理,文件由原来的23行扩展为3121行。且在.i文件中不存在#include,而是将头文件,,的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件。不难发现主函数是不变的,编译预处理是讲#include的文件一并加入到了源文件中,此时还是C语言文件。
2.4 本章小结
本章主要介绍了预处理的概念与作用、将hello.c进行了预处理,生成hello.i文件,并结合hello.c预处理之后的程序对预处理结果进行了解析,为下一步编译做准备。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
利用编译程序从源语言编写的源程序产生目标程序的过程。编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
编译器的构建流程主要分为 3 个步骤:
1. 词法分析器,用于将字符串转化成内部的表示结构。
2. 语法分析器,将词法分析得到的标记流(token)生成一棵语法树。
3. 目标代码的生成,将语法树转化成目标代码。
作用:把预处理完的文件进行一系列词法分析,语法分析,语义分析及优化后生成相应的汇编代码文件。例如,编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。【2】
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
图3.2在Ubuntu下 的命令
应截图,展示编译过程!
如图,通过gcc的-S编译选项可以将hello.i编译成hello.s文件
3.3 Hello的编译结果解析
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
让我们先看下hello.s是什么样子的(仅有部分截图)
图3.3.1hello的编译
图3.3.2hello的编译
图3.3.3 hello的编译
首先我们的直观感受是代码变短了,仅有81行。
图3.3.4 hello的编译
然后我们从数据结构的角度看
3.3.1字符串
在只读数据段存放这我们的常量字符串(rodata)
图3.3.1.1字符串
需要注意的是这里有标记“.LC1”这里可以看作字符串的名字(地址),使用时像调用函数一样调用“.LC1”即可,比如
图3.3.1.2字符串
3.3.2整型数据
int i;在main函数中作为局部变量直接保存在栈中。
argc: 作为参数传入。
3.3.3数组
传入三个参数与该文件的地址名字组成一个字符串数组argv[],并且这些数据也都保存在栈中,每个元素占8个字节。
这栈中的32个字节就是用来保存四个参数的。
图3.3.3.1数组
3.3.4关系操作
判断argc是否等于4,cmpl一句计算argc-4,并且更新条件码,以便后面判断。
图3.3.4.1 关系操作
判断i是否小于8,同上,这个是小于等于而已
图3.3.4.2关系操作
3.3.5控制转移
- if语句:
图3.3.5.1 if语句
- for语句
图3.3.5.2 for语句
.L2是循环语句初始化过程,然后跳转到.L3-判断条件是否满足,若满足,则回溯到.L4即循环体的内容,执行完后,顺序进入到.L3判断过程,直到条件不满足停止循环。
3.3.6算术操作
算数操作有很多,就不截图了
3.3.7函数调用
- printf先把要打印字符串的地址放到rdi中,即作为参数传给函数,因为只有一个字符串,故函数printf退化为函数puts,然后调用函数puts。
图3.3.7.1printf
- exit,传入退出状态参数1,然后调用函数exit.
图3.3.7.2exit
- Printf,因为默认函数调用前保存参数的寄存器顺序为rdi,rsi,rdx,rcx.故因为本次调用涉及三个参数,rdi中保存字符串,rsi中是argv[1],rdx中是argv[2].最后调用printf。
图3.3.7.3printf
- atoi,把字符串的首地址放入rdi中,然后调用函数,并且函数的返回值会放入寄存器eax中。
图3.3.7.3atoi
- sleep,把atoi的返回值放入rdi中,调用sleep函数。
图3.3.7.4sleep
- Getchar,没有参数,直接调用。
图3.3.7.5getchar
3.3.8函数返回
数组保存在栈中,利用与rbp的相对位置进行寻址。,找到初始位置,后面引用加相对位置即可。
图3.3.8函数返回
3.4 本章小结
本章介绍了编译的概念与作用,通过对hello.s的解析,读懂汇编语句,了解汇编语句下:数据:常量、表达式、类型、,算数操作,逻辑/位操作,关系操作,控制转移以及函数操作的基本知识。
通过编译,我们已将原始.c文件生成为汇编语言程序,距离可执行目标文件又进一步。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
把汇编语言翻译成机器语言的过程称为汇编。汇编器(as)将.s文件中的汇编代码翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在.o目标文件中。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
图4.2在Ubuntu下的命令
应截图,展示汇编过程!
如图,使用as命令将汇编文件汇编成可重定位目标文件
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
为了便于观看,我们将elf文件输出到“a.txt”中,使用vscode观看。
图4.3.1 命令
图4.3.2elf内容
图4.3.3elf内容
图4.3.4内容
项目分析:
偏移量是指需要进行重定向的代码在.text或.data节中的偏移位置。
信息:包括symbol和type两部分,其中symbol占前4个字节,type占后4个字节,symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位的类型。
类型:重定位到的目标的类型。
名称:重定向到的目标的名称。
加数:计算重定位位置的辅助信息。
对于相对寻址的重定位,需要计算其目标机器代码中的地址:
在(偏移量+该节的基地址)处修改,再使下一条指令的地址减去变量地址即可。
其中全零的部分代表需要重定位
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
图4.4.1hello.o反汇编
图4.4.2hello.o反汇编
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
机器语言指令由指令指示符、寄存器指示符和操作数指示符构成。除含具体操作数的指令外,机器语言与汇编语言有一一对应关系,任何汇编指令都有唯一的机器语言编码。
不一致:
A.在指令跳转时,包括实现分支转移、循环和函数调用时需要的地址,在汇编代码中,会直接使用规定的标志符号或函数名称,是字符集表示,而在反汇编代码中则运用了相对于该函数的偏移量,而在二进制代码中,则把该地址置为NULL,以后重定位时再填充。
B.在访问全局变量时,汇编代码中使用的是其原本寄存器中的内容,而反汇编中用的是0,,因为rodata中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全0并添加重定位条目。
4.5 本章小结
本章介绍了通过汇编器(as)将hello.s汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式的过程。最终生成文件为hello.o。并且生成了hello.o的ELF文件格式,介绍了其中的内容。通过hello.o获得反汇编代码,并且与hello.s文件进行了对比。发现反汇编后分支转移,函数调用,全局变量访问的方式有所不同。间接了解到从汇编语言映射到机器语言汇编器需要实现的转换。
第4章1分)
第5章 链接
5.1 链接的概念与作用
将多个可重定位的目标文件合并成可执行文件。链接器将每个符号引用与一个确定的符号定义关联起来。将多个单独的代码节和数据节合并为单个节。将符号从它们的在.o文件的相对位置重新定位到可执行文件的最终绝对内存位置。更新所有对这些符号的引用来反映它们的新位置。
注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
如图,使用命令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.2在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
图5.3.1命令
图5.3.2内容
可以看到原先为全零的地方现在都为非零数了,说明链接器进行了重定位。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
图5.4edb中的elf
在0x400000~0x401000段中,程序被载入,自虚拟地址0x400000开始,到0x400fff结束。
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
与hello.o生成的反汇编文件对比发现,hello1.txt中多了许多节。hello0.txt中只有一个.text节,而且只有一个main函数,函数地址也是默认的0x000000.hello1.txt中有.init,.plt,.text三个节,而且每个节中有许多的函数。库函数的代码都已经链接到了程序中,程序各个节变的更加完整,跳转的地址也具有参考性。
hello比hello.o多出的节头表。
.interp:保存ld.so的路径
.note.ABI-tag
.note.gnu.build-i:编译信息表
.gnu.hash:gnu的扩展符号hash表
.dynsym:动态符号表
.dynstr:动态符号表中的符号名称
.gnu.version:符号版本
.gnu.version_r:符号引用版本
.rela.dyn:动态重定位表
.rela.plt:.plt节的重定位条目
.init:程序初始化
.plt:动态链接表
.fini:程序终止时需要的执行的指令
.eh_frame:程序执行错误时的指令
.dynamic:存放被ld.so使用的动态链接信息
.got:存放程序中变量全局偏移量
.got.plt:存放程序中函数的全局偏移量
.data:初始化过的全局变量或者声明过的函数
图5.5.1hello的elf文件
图5.5.2hello的elf文件
图5.5.3hello的elf文件
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
图5.6.1hello的执行流程
(1) 载入:_dl_start、_dl_init
(2)开始执行:_start、_libc_start_main
(3)执行main:_main、_printf、_exit、_sleep、
_getchar、_dl_runtime_resolve_xsave、_dl_fixup、_dl_lookup_symbol_x
(4)退出:exit
程序名称 地址
hello!_start 0x400582
图5.6.2hello!_start 的地址
hello!puts@plt 0x401030
图5.6.3 hello!puts@plt的地址
hello!exit@plt 0x401070
图5.6.4hello!exit@plt的地址
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
在进行动态链接前,首先进行静态链接,生成部分链接的可执行目标文件hello。此时共享库中的代码和数据没有被合并到hello中。加载hello时,动态链接器对共享目标文件中的相应模块内的代码和数据进行重定位,加载共享库,生成完全链接的可执行目标文件。
动态链接采用了延迟加载的策略,即在调用函数时才进行符号的映射。使用偏移量表GOT+过程链接表PLT实现函数的动态链接。GOT中存放函数目标地址,为每个全局函数创建一个副本函数,并将对函数的调用转换成对副本函数调用。
5.8 本章小结
概括了链接的概念和作用,重点分析了hello程序的虚拟地址空间、重定位和执行过程。简述了动态链接的原理
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
1.进程的概念:
经典定义就是一个执行中程序的实例。广义定义是进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
2.进程的作用:
进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
6.2 简述壳Shell-bash的作用与处理流程
1.Shell-bash的作用:
Linux系统中,Shell是一个交互型应用级程序,为使用者提供操作界面,接收用户命令,然后调用相应的应用程序。
2.处理流程:
①从终端读入输入的命令。
②将输入字符串切分获得所有的参数。
③检查第一个命令行参数是否是一个内置的shell命令,如果是则立即执行。
④如果不是内部命令,调用fork( )创建新进程/子进程执行指定程序。
⑤shell应该接受键盘输入信号,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
在终端中输入命令行./hello 120L021410 陈帅行 1后,首先shell对我们输入的命令进行解析,由于我们输入的命令不是一个内置的shell命令,因此shell会调用fork()创建一个子进程,子进程几乎但不完全与父进程相同。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的但是独立的一份副本,拥有不同的PID。
6.4 Hello的execve过程
当调用fork()函数创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,需要以下步骤:
①删除已存在的用户区域。删除之前进程在用户部分中已存在的结构。
②创建新的代码、数据、堆和栈段。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。如图所示:
图6.4虚拟内存结构
③映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
④设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
进程调度的过程:
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先的被抢占了的进程。这种决策就叫做调度。 在内核调度了一个新的进程运行后,它就抢占当前进程,并使用种称为上下文切换的机制来将控制转移到新的进程。
上下文切换
- 保存当前进程的上下文
- 恢复某个先前被抢占的进程被保存的上下文
3)将控制传递给这个新恢复的进程。
当内核代表用户执行系统调用时,可能会发生上下文切换。如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程。
例如,系统调用sleep函数,就先将本来运行在用户模式中的hello程序,转移到核心态,由内核完成sleep函数的执行,最后再把控制转移给用户即回到用户态。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
可能会出现中断、陷阱和系统调用、故障和终止这几类异常,会产生下列几种信号:
名称 默认行为 相应事件
SIGINT 终止 来自键盘的中断
SIGKILL 终止 杀死程序(该信号不能被捕获不能被忽略)
SIGSEGV 终止 无效的内存引用(段故障)
SIGALRM 终止 来自alarm函数的定时器信号
SIGCHLD 忽略 一个子进程停止或终止
输入回车:刚好8个,并没有影响该进程,说明输入的回车键被忽略。
Ctrl+C:说明终止了该进程,发送了SIGINT信号。
Ctrl+Z:发送一个 SIGTSTP 信号给前台进程组中的进程,从而将其挂起.
输入命令ps查看所有进程及其运行时间,发现hello还没有终止,
输入命令jobs查看当前暂停的进程,发现了hello的进程,输入命令fg使之在前台运行,导致了剩余几行的打印,输入命令kill,结束进程hello。
此时,再用ps查看,已经不存在hello进程了。
6.7本章小结
本章通过对进程的讨论,使shell加载和运行hello程序的过程完整地展现出来,并且通过进一步操作,了解信号对进程的影响。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
1.逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 段标识符:段内偏移量。
2.线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式,分页机制中线性地址作为输入。
3.虚拟地址:就是线性地址。
4.物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,如图所示:
【3】
图7.2.1索引号
索引号就是“段描述符”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组了一个数组,叫“段描述符表”,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符。每一个段描述符由8个字节组成,如图所示:
【4】
图7.2.2索引号
Base字段,它描述了一个段的开始位置的线性地址。Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表”中,用段选择符中的T1字段来判断用全局段描述符表还是局部段描述符表,=0,表示用全局段描述符表,=1,表示用局部段描述符表。如图所示:
【5】
图7.2.3索引号
先给定一个完整的逻辑地址 段标识符:段内偏移量
1.看段选择符的T1=0还是1,知道当前要转换是全局段描述符表中的段还是局部段描述符表中的段。
2.取出段选择符的前13位查找到对应的段描述符,确定了Base基地址。
3.将Base+offset,就是线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址即虚拟地址(VA)到物理地址(PA)之间的转换通过分页机制完成,而分页机制是对虚拟地址内存空间进行分页。
系统将虚拟页作为进行数据传输的单元。Linux下每个虚拟页大小为4KB。物理内存也被分割为物理页, MMU(内存管理单元)负责地址翻译,MMU使用页表将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。
图7.3Hello的线性地址到物理地址的变换-页式管理
n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。
7.4 TLB与四级页表支持下的VA到PA的变换
Core i7采用四级页表的层次结构。CPU产生虚拟地址VA,虚拟地址VA传送给MMU,MMU使用VPN高位作为TLBT和TLBI,向TLB中寻找匹配。如果命中,则得到物理地址PA。如果TLB中没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成物理地址PA,添加到PLT。
图7.4 TLB与四级页表支持下的VA到PA的变换
7.5 三级Cache支持下的物理内存访问
获得物理地址之后,先取出组索引对应位,在L1中寻找对应组。如果存在,则比较标志位,相等后检查有效位是否为1.如果都满足则命中取出值传给CPU,否则按顺序对L2cache、L3cache、内存进行相同操作,直到出现命中。然后再一级一级向上传,如果有空闲块则将目标块放置到空闲块中,否则将缓存中的某个块驱逐,将目标块放到被驱逐块的位置。
7.6 hello进程fork时的内存映射
在shell输入命令行后,内核调用fork创建子进程,为hello程序的运行创建上下文,并分配一个与父进程不同的PID。通过fork创建的子进程拥有父进程相同的区域结构、页表等的一份副本,同时子进程也可以访问任何父进程已经打开的文件。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间。
7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
①删除已存在的用户区域
删除当前进程虚拟地址的用户部分中的已存在的区域结构。
②映射私有区域
为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
③映射共享区域
hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
④设置程序计数器(PC)
execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。如图所示:
图7.7 hello进程execve时的内存映射
7.8 缺页故障与缺页中断处理
如果程序执行过程中发生了缺页故障,则内核调用缺页处理程序。
处理程序执行如下步骤:
1.检查虚拟地址是否合法,如果不合法则触发一个段错误,程序终止。
2.检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,
程序终止。
3.两步检查都无误后,内核选择一个牺牲页面,如果该页面被修改过则将其交换
出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。
图7.8 缺页故障与缺页中断处理
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
动态内存管理的基本方法与策略:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆,系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高地址)。对于每个进程,内核维护着一个变量brk,它指向对的顶部。
图7.9.1动态存储分配管理
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显示地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显示地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显示执行的。要么是内存分配器自身隐式执行的。分配器有两种基本风格。两种风格都要求应用显示地分配块。他们的不同之处在于由哪个实体来负责释放已分配的块。
1.显示分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫
做malloc程序包的显示分配器。
2.隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放
这个块。隐式分配器也叫垃圾收集器。
①隐式空闲链表的堆块格式:
图7.9.2动态存储分配管理
②隐式空闲链表的带边界标记的堆块格式:
使用边界标记的堆块的格式其中头部和脚部分别存放了当前内存块的大小与是否已分配的信息。通过这种结构,隐式动态内存分配器会对堆进行扫描,通过头部和脚部的结构实现查找。
图7.9.3动态存储分配管理
③显示空闲链表:
显示空闲链表是将空闲块组织为某种形式的显示数据结构。如图所示。堆被组织为一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继的指针。
图7.9.4动态存储分配管理
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。维护链表的顺序有:后进先出(LIFO),将新释放的块放置在链表的开始处,使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比LIFO排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。
7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以intel Core7在指定环境下介绍了虚拟地址VA到物理地址PA的转换、物理内存访问,分析了hello进程fork时的内存映射,hello进程execve时的内存映射、缺页故障与缺页中断处理和动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有IO设备都被模型化为文件,所有的输入和输出都能被当做相应文件的读和写来执行。
设备管理:Linux内核有一个简单、低级的接口,成为Unix I/O,是的所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix IO接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux Shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
4.读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件,作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放他们的内存资源。
Unix I/O函数:
①int open(char* filename,int flags,mode_t mode)
进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
②int close(fd)
进程通过调用close函数关闭一个打开的文件,fd是需要关闭的文件的描述符。
③ssize_t read(int fd,void *buf,size_t n)
read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
④ssize_t wirte(int fd,const void *buf,size_t n)
write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
printf代码:
图8.3.1printf的实现
printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。
Vsprintf的代码:
图8.3.2printf的实现
图8.3.3printf的实现
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。getchar函数落实到底层调用了系统函数read,通过系统调用read读取存储在键盘缓冲区中的ASCII码直到读到回车符然后返回整个字串,getchar进行封装,大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
本章介绍了Linux的IO设备管理方法,Unix IO接口及其函数,分析了printf函数和getchar函数的实现。
(第8章1分)
结论
1.输入:将hello.c代码从键盘输入。
2.预处理(cpp):将hello.c进行预处理,将c文件调用的所有外部的库展开合并,
生成hello.i文件。3.编译(ccl):将hello.i文件进行翻译生成汇编语言文件hello.s。
4.汇编(as):将hello.s翻译成一个可重定位目标文件hello.o。
5.链接(ld):将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程
序hello,至此可执行hello程序正式诞生。
6.运行:在shell中输入./hello 120L021410 陈帅行 1
7.创建子进程:由于终端输入的不是一个内置的shell命令,因此shell调用fork ()
函数创建一个子进程。
8.加载程序:shell调用execve函数,启动加载器,映射虚拟内存,进入程序入口
后程序开始载入物理内存,然后进入main函数。
9.执行指令:CPU为进程分配时间片,在一个时间片中,hello享有CPU资源, 顺
序执行自己的控制逻辑流。
10.访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
11.动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。
12.信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进
程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
13终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。
对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法:
抽象对计算机系统是很重要的概念,底层信息用二进制来抽象表示,进程是对处
理器、主存和I/O设备的抽象,虚拟内存是对主存和磁盘设备的抽象,文件是对I/O
设备的抽象,等等。另外,存储器的“过渡”策略也十分精妙,由于CPU的处理
速度比主存快得多,为了减少“供不应求”的现象,在CPU和主存之间增加了一级,二级,三级cache大大提高了CPU访问主存的速度。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello.c 源程序
hello.i 预处理后文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位目标执行文件
hello 链接后的可执行文件
a.txt hello.o的ELF格式
b.txt hello的ELF格式
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
- (15条消息) C语言 预处理作用与宏定义_孙浩的博客的博客-CSDN博客_预处理作用
- (15条消息) 编译的基本概念_董大虾的博客-CSDN博客_编译是什么
- Unix技术网 = 专业的Linux/Unix应用与开发者社区 = IT人的网上家园 (chinaunix.net)
- Unix技术网 = 专业的Linux/Unix应用与开发者社区 = IT人的网上家园 (chinaunix.net)
- Unix技术网 = 专业的Linux/Unix应用与开发者社区 = IT人的网上家园 (chinaunix.net)
(参考文献0分,缺失 -1分)