计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术学院
学 号 120L022431
班 级 2003008
学 生 张欣洋
指 导 教 师 吴锐
计算机科学与技术学院
2021年5月
本次大作业详细、系统地分析了hello程序的整个生命周期。从最初的手动编写hello.c源程序,到利用C预处理器(C Preprocessor)输出hello.i文件,再到利用C编译器(Compiler)翻译生成汇编文件hello.s,然后运行汇编器(Assembler)将其翻译成一个可重定位目标文件hello.o,最后链接器ld将hello.o和系统目标文件组合起来,生成可执行程序hello。在执行阶段中, shell接受指令后创建进程,加载hello进入内存,由CPU控制程序逻辑流、系统中断、上下文切换、异常处理,最后进程终止并被父进程回收。这一整个流程之后,hello程序的生命周期宣告结束。
关键词:预编译;汇编;链接;进程;存储;IO管理
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
2.2在Ubuntu下预处理的命令............................................................................. - 5 -
5.3 可执行目标文件hello的格式........................................................................ - 8 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 10 -
6.3 Hello的fork进程创建过程......................................................................... - 10 -
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 -
8.2 简述Unix IO接口及其函数.......................................................................... - 13 -
第1章 概述
1.1 Hello简介
1.文本编辑器手动编写hello的代码,保存在.c文件。得到源程序文件hello.c。
2.运行C预处理器进行预处理。得到预编译文件hello.i。
3.运行C编译器进行翻译。得到汇编语言文件hello.s。
4.运行汇编器。得到可重定位目标文件hello.o。
5.运行链接器组合hello.o与系统目标文件。得到可执行目标文件hello。
6.shell接受指令./shell,通过fork函数创建新进程。随后,调用execve映射虚拟内存,通过mmap为hello程序开创了一片空间。
7.CPU从虚拟内存中的.text,.data节取代码和数据,调度器为进程规划时间片,有异常时触发异常处理子程序。
8.程序运行结束时,父进程回收hello进程和它创建的子进程,内核删除相关数据结构。
1.2 环境与工具
1.硬件环境:AMD Ryzen 5900HX CPU 32GB RAM
2. Windows10 64位;VMWare Workstation Pro;Ubuntu 20.04 LTS 64位
3. 工具:codeblocks;gdb;Visual Studio;readelf;HexEdit;ld
1.3 中间结果
hello.c 源程序
hello.i 预处理文件
hello.s 汇编文件
hello.o 可重定位目标文件
hello 可执行文件
hello.elf hello.o的ELF格式
hello1.txt hello.o的反汇编
hello2.txt hello的反汇编代码
hello1.elf hello的ELF格式
1.4 本章小结
简要介绍了hello的生命周期。介绍了实验环境。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
2.1.1.概念:预处理器(C Preprocessor)根据预处理指令,修改原始的C程序。
2.1.2.作用:根据源代码中的以#开头的指令,预处理器将从系统的头文件包中将头文件中的代码插入到目标文件中,并且宏与常量将被相应的代码和值替换,最终生成.i文件。
2.2在Ubuntu下预处理的命令
gcc -E -o hello.i hello.c
效果截图:
2.3 Hello的预处理结果解析
文件的内容大大增加了,同时仍然是一个C源程序文件。新增的内容包括了头文件中定义的内容、宏指令等,常量亦进行了替换。
2.4 本章小结
本章介绍了预处理的内容、结果等相关概念,并且实机执行了一次预处理,观察其处理结果等。预处理过程是对源程序的补充与替换。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
3.1.1.概念:编译器(Compiler)将预处理后的.i文件翻译成汇编语言程序。
3.1.2.作用:对源代码进行语义分析、词法分析,将源程序翻译为汇编语言程序.s文件。如果发现语法错误,给出提示信息。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
Gcc -S hello.i -o hello.s
效果截图:
应截图,展示编译过程!
3.3 Hello的编译结果解析
3.3.1. .s文件的文件结构
节名称及其作用:
.file 声明源文件
.text 代码节
.section.rodata 只读数据段
.globl 声明全局变量
.type 声明一个符号是函数类型还是数据类型
.size 声明大小
.string 声明一个字符串
.align 声明对指令或者数据的存放地址进行对齐的方式
3.3.2. 数据
(1)字符串
(2)局部变量
局部变量被储存在堆栈中。
(3)main函数的参数argc
参数储存在堆栈中。
(4)main函数的参数数组argv[]
参数储存在堆栈中。该参数的每个元素是一个指向字符类型的指针。
(5)立即数
立即数直接储存在汇编代码中。
3.3.3. 赋值
汇编代码中,赋值操作使用mov系列指令。
根据传送的操作数大小,可分为mov+
b:字节
w:字
l:双字
q:四字
3.3.4. 算术
hello.c中的算术操作是i++
3.3.4. 关系操作
(1).条件判断语句
对应的指令是判断argc是否等于4.
(2).循环中的条件跳转语句
它对应的是循环中的条件i<8
3.3.5. 控制转移操作
根据3.3.4中的关系操作,进行条件跳转。
如果argc≠4,则顺序执行if操作,如果argc=4,则跳转执行后续操作。
根据3.3.4中的关系操作,进行循环。先赋初值0,自增1,和7比较。如果小于等于7,则继续进行循环。否则跳出循环。
3.3.6. 函数操作
Hello中的函数操作有:
1.main:参数是int argc,char *argv[];
2.printf:参数是argv[1],argv[2];
3.exit:参数是1;
4.sleep:参数是atoi(argv[3]);
5.getchar:无参数;
3.3.6. 类型转换操作
hello.c中atoi(argv[3])将字符串类型转换为整型。
3.4 本章小结
本章主要介绍了c语言程序编译处理的基本过程。该过程中,源代码被转换为汇编代码。编译器将源码中的数据,赋值,类型转换等操作等价地翻译为汇编语句。如果理解了编译器翻译c程序的机制,我们就可以对汇编语言进行人工读写,提高了逆向工程的能力。
第4章 汇编
4.1 汇编的概念与作用
1.概念:汇编器(Assembler)将汇编语言翻译成机器语言,打包生成可重定位目标文件hello.o。
2.作用:将汇编程序翻译成机器语言,方便链接器链接执行。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
效果截图:
4.3 可重定位目标elf格式
4.3.1典型的可重定位目标文件的elf格式为:
生成elf格式文件的指令:readelf -a hello.o > hello.elf
效果:生成了hello.elf文件。
4.3.2分析elf文件:
①ELF header:ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
②Section Headers:记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。
③Relocation Section:保存的是.text节中的重定位信息。调用外部函数的指令需要重定位;引用全局变量的指令需要重定位; 调用局部函数的指令不需要重定位。在可执行目标文件中不存在重定位信息。本程序需要被重定位的是puts、exit、printf、atoi、sleep、getchar与.rodata中的L0、L1。
④符号表:.symtab,一个符号表,它存放在程序中定义和引用的函数和全局变量的信息,一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。本符号表中,所有使用头文件中定义的函数均被标记为UND,等待链接器进行链接。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。指令后加 > hello.dump,将结果保存到本地文件。
与汇编源码相比,已经翻译成了16进制的机器指令,可供处理器接受处理。
与hello.s的差异:
- 跳转操作
.s文件中,跳转指令参数为段名称。而.o文件中,跳转指令的参数为<函数+段内偏移量>。这是因为段名称是编写汇编指令的助记符,汇编为.o文件后就不再存在。
汇编后:
- 函数调用
.s 文件中,调用指令的参数为函数名。而.o 文件中为<函数+偏移量>。这是由重定位条目指示的信息。
汇编后:
- 进制转换
.s文件中的立即数都从十进制转换为16进制。
->
4.5 本章小结
本章对汇编器处理hello.s并生成hello.o的过程进行了分析。查看了可重定位文件的ELF头、节头、符号表和可重定位节。查看了objdump后的hello.o的反汇编代码。比较了与hello.s的不同之处。分析了汇编语言与机器语言的对应关系。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
1.概念:链接是将各种代码和数据片段收集并合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。
2.作用:链接器在软件开发过程中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。
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的格式
5.3.1 生成elf格式文件的指令:readelf -a hello > hello_exec.elf
效果:生成了hello_exec.elf文件。
5.3.2 分析hello_exec.elf。可以发现相比hello.o变长了很多。
①ELF头。
相比之下,Type变成了Executable file。同时节的数量变为27.
②Section Headers。对全部的节及其地址进行了声明
③重定位节
④符号表。
5.4 hello的虚拟地址空间
①程序起始点为0x400000。
②程序.init节的地址为0x401000。
观察data dump
③程序.text节的起始地址为0x4010f0。
观察data dump
5.5 链接的重定位过程分析
重定位过程:
①重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
②重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。
③重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目,代码的重定位条目放在.rel.txt中
指令:objdump -d -r hello > hello_exec.dump
部分hello_exec.dump内容如下:
与hello.dump相比,hello_exec.dump的长度大大增长,多出了很多节。hello.dump只有一个.text节,只描述了一个main函数,地址也是默认的0x000000。Hello_exec.dump中有.init,.plt,.text三个节,每个节中有很多函数。这说明,库函数的代码均被集成进了程序,程序的各个节得到完善。
程序中的跳转地址被替换成了正确的地址。
观察hello.o的反汇编代码hello.dump
能够注意到,在这些位置并没有正确的地址。这是因为还没有进行链接。
观察hello的反汇编代码hello_exec.dump
链接之后,被替换为正确的地址。
5.6 hello的执行流程
①程序入口401000 <_init>
4010f0 <_start>:
403ff0 <__libc_start_main>
②执行函数401125 <main>
4010a0 <printf>
4010d0 <exit>
4010e0 <sleep>
4010b0 <getchar>
③程序退出 4010d0 <exit>
5.7 Hello的动态链接分析
GOT起始位置为404000。
链接前:
链接后:
发生变化。
5.8 本章小结
本章介绍了链接的概念与作用,了解了可执行文件的ELF格式,使用edb分析了hello可执行程序的虚拟地址空间,重定位过程,执行过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
1.概念:
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
2.作用:
进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。我们应当感谢局部性的存在,进程所对应的处理功能部件会把它提供出来给所有的应用程序。它有两个关键抽象:一个独立的程序逻辑控制流:它可以提供一个独立的假象,好像我们的应用程序在一个独占的空间使用内存处理器。一个应用程序私有的地址处理器空间,它可以提供一个独立的假象,好像我们的应用程序独占的一个使用内存的系统。
6.2 简述壳Shell-bash的作用与处理流程
shell俗称壳,是一种指"为使用者提供操作界面"的嵌入式软件(也被称为命令解析器)。软件提供了一种允许用户与其他操作系统之间进行通讯的一种方式。这种简单的通讯方式可以以交互方式(从键盘输入,并且用户可以立即地得到命令响应),或者以交互方式shellscript(非交互)的方式允许用户执行。shell(即壳)它是一个简单的命令解释器,它允许系统接收到一个用户的命令,然后自动调用相应的命令执行应用程序。
Shell 的处理流程:shell读取用户从终端使用外部设备输入(通常是键盘输入)的指令。解析所读取的指令,如果这个指令是一个内部指令则立即执行,否则,加载调用一个应用程序为申请的程序创建新的子进程,在子进程的上下文中运行。同时shell还允许接收从键盘读入的外部信号,(如:kill)并根据不同信号的功能进行对应的处理。
①从终端读入输入的命令。
②将输入字符串切分获得所有的参数。
③检查第一个命令行参数是否是一个内置的shell命令,如果是则立即执行。
④如果不是内部命令,调用fork( )创建新进程/子进程执行指定程序。
⑤shell应该接受键盘输入信号,并对这些信号进行相应处理。
6.3 Hello的fork进程创建过程
在终端输入对应的指令(./hello 120L022431 张欣洋 1)后,shell开始进行以下操作:首先语义分析,判断hello不是内置指令,所以调用当前目录下的可执行文件hello。随后Shell会自动的调用fork()函数为父进程创建一个新的子进程。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的但是独立的一份副本,拥有不同的PID。
6.4 Hello的execve过程
当调用fork()函数创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序。execve在调用成功的情况下不会返回,只有当出现错误时,例如找不到需要执行的程序时,execve才会返回到调用程序。
execve需要以下步骤:
- 删除已存在的用户区域,删除之前进程在用户部分中已存在的结构。
②映射私有区:为Hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的。
③映射共享区:比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
④设置PC:exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
6.5.1 逻辑控制流和时间片:
进程的运行本质上是CPU不断从程序计数器 PC 指示的地址处取出指令并执行,值的序列叫做逻辑控制流。操作系统会对进程的运行进行调度,执行进程A->上下文切换->执行进程B->上下文切换->执行进程A->… 如此循环往复。 在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。在一个程序被调运行开始到被另一个进程打断,中间的时间就是运行的时间片。
6.5.2 用户模式和内核模式:
用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据。
内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
6.5.3 上下文:
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
6.5.4 调度的过程:
在对进程进行调度的过程,操作系统主要做了两件事:加载保存的寄存器,切换虚拟地址空间。
6.5.5 用户态与核心态转换:
为了能让处理器安全运行,需要限制应用程序可执行指令所能访问的地址范围。因此划分了用户态与核心态。
核心态可以说是拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
6.5.6 hello进程的执行:在进程调用execve函数之后,进程已经为hello程序分配了新的虚拟的地址空间,最初hello运行在用户模式下,输出hello 1190201016 石衍,然后调用sleep函数进程进入内核模式,运行信号处理程序,之后再返回用户模式。运行过程中,cpu不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用cpu,实现进程的调度。
6.6 hello的异常与信号处理
6.6.1. 异常与信号异常种类
- 中断 原因:来自I/O设备的信号,异步处理,并且总是返回到下一条指令。
- 陷阱 原因:有意的异常,同步处理,并且总是返回到下一条指令。
- 故障 原因:潜在的可恢复错误,同步处理,并且有可能返回到当前指令或者终止。
- 终止 原因:不可恢复错误,同步处理,并且不会返回。
6.6.2. 测试结果
①正常运行
- ctrl-z
按下ctrl-z结果是挂起前台作业,但是hello进程并未得到回收。输入ps命令可以看到hello进程仍然在后台运行
此进程job号为1,输入fg 1将其恢复前台。这时hello打印剩下的输出并被回收。输入ps命令发现hello已不在运行。
- ctrl-c
按下ctrl-c会发送SIGINT信号到前台进程组。Hello进程被中断。
输入ps后,发现进程已被回收。
- 乱按
其结果是乱码被输入stdin并被打印。
6.7本章小结
本章介绍了进程的概念和作用,shell-bash的原理,fork、execve函数执行hello的过程,hello的异常和信号处理。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1.逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。
7.1.2.线性地址:逻辑地址向物理地址转化过程中的中间状态。逻辑地址经过段机制后转化为线性地址,通过段偏移地址加上相应段的基地址计算生成。
7.2.3.虚拟地址:hello运行在虚拟地址中,表现为线性地址
7.2.4.物理地址:处理器通过硬件总线的寻址操作,找到真实的物理内存的对应地址。总线上传输的地址都是物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
Intel平台下,逻辑地址表示为<段选择符:偏移量>的形式。段标识符是一个16位长的字段(段选择符)。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。
给定一个完整的逻辑地址<段选择符+段内偏移地址>,通过判断段选择符的T1=0还是1,得知当前要转换是GDT中的段,还是LDT中的段,再根据段选择符在全局描述符表中检索到段基址。将段基址与偏移量相加,计算得到线性地址。该过程称为段式管理。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
控制寄存器CR3的高20位作为页目录表所在物理页的页码。首先把线性地址的最高10位作为页目录表的索引,对应表项所包含的页码指定页表;然后,再把线性地址的中间10位作为所指定的页目录表中的页表项的索引,对应表项所包含的页码指定物理地址空间中的一页;最后,把所指定的物理页的页码作为高20位,把线性地址的低12位不加改变地作为32位物理地址的低12位。
为了避免在每次存储器访问时都要访问内存中的页表,以便提高访问内存的速度,80386处理器的硬件把最近使用的线性—物理地址转换函数存储在处理器内部的页转换高速缓存中。在访问存储器页表之前总是先查阅高速缓存,仅当必须的转换不在高速缓存中时,才访问存储器中的两级页表。页转换高速缓存也称为页转换查找缓存,记为TLB。
7.4 TLB与四级页表支持下的VA到PA的变换
CPU产生虚拟地址VA,VA传送给MMU,MMU使用前36位VPN向TLB中匹配,如果命中,则得到PPN与VPO组合成物理地址PA。如果没有命中,MMU向页表中查询,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成物理地址PA,并向TLB中添加条目。如果查询PTE的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。
7.5 三级Cache支持下的物理内存访问
CPU发送一条虚拟地址,经过转换,获得了物理地址PA。随后将PA分为CT(标记位)、CI(组索引)、CO(块偏移)。根据CI寻找到正确的组,依次与每一行的数据比较,有效位有效且标记位一致则命中。如果命中,直接返回想要的数据。否则就依次去L2、L3、主存检索并判断是否命中。
7.6 hello进程fork时的内存映射
当fork函数被调用时,内核为hello创建各种数据结构,并分配给它一个唯一的PID。同时给hello创建虚拟内存,fork创建了当前进程的mm_struct、区域结构和页表的原样副本。当fork在hello中返回时,hello现在的虚拟内存刚好和调用shell的虚拟内存相同。当这两个进程中的任何一个进行写操作时,写时复制机制会创建新页面。因此也就为每个进程保持了私有地址空间的概念
7.7 hello进程execve时的内存映射
①执行execve调用。
②execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello替代了当前bash中的程序。
③删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
- 映射私有区域
- 映射共享区域
- 设置程序计数器(PC)到代码区域的入口点。
7.8 缺页故障与缺页中断处理
处理缺页是由硬件和操作系统内核协作完成的。当MMU翻译某个地址时,触发了缺页异常。此时,控制便会转移到内核的缺页处理程序。处理程序会判断:1、虚拟地址是否合法。缺页处理程序会搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较。如果指令不合法,便会触发段错误。2、试图进行的内存访问是否合法。判断进程是否有足够的权限。当内核知道缺页是正常情况时,会选择牺牲页,换入新的页面并进行页表更新,完成缺页处理。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合。各个块是已分配的或者是空闲的。分配器有两种基本风格,两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
显式分配器:
要求应用显式地释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。
隐式分配器:
要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
7.10本章小结
本章概述了虚拟地址、线性地址、物理地址的概念,从linux的内存系统出发,介绍了VA、PA之间的转换,通过分析,了解了如何组织空闲区域、申请空间、空间的分割、合并、回收等过程。介绍了动态分配机制,比如函数malloc。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有IO设备被模型化为文件,输入输出操作可当作对应文件的读写。
设备管理:Linux内核有一个简单、低级的接口,称为Unix I/O,所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
8.2.1 Unix IO接口:
①.打开文件。一个应用程序通过请求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
②.Linux Shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。
③.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
④.读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
⑤.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件,作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放他们的内存资源。
8.2.2 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 的函数体:
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;
}
申请变量i与数组buf。
其后,注意到语句va_list arg = (va_list)((char*)(&fmt) + 4)。该语句中计算并赋值第一个参数的地址。
然后程序调用了函数vsprintf。查看vsprintf函数体:
int vsprintf(char *buf, const char *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);
}
vsprintf的作用是接受格式化字符串fmt并产生格式化输出。之后调用write将长度为i的buf输出。write函数的汇编代码如下:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。
sys_call函数如下:
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
syscall 将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII 码,字符找到对应点阵信息传送到vram中,显示芯片按照一定刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点的颜色值,于是我们的打印字符串就显示在了屏幕上。
8.4 getchar的实现分析
异步异常-键盘中断的处理:在用户敲击或者按键盘上面的按钮的时候,键盘接口获得一个键盘扫描码,这样会在此时同时产生一个中断的请求,这会调用键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区的内部。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回这个字符串。Getchar的大概思想是读取字符串的第一个字符之后再进行返回操作。
8.5本章小结
本章介绍了Linux的IO接口与管理机制,IO函数,printf与getchar函数的实现方法。
(第8章1分)
结论
Hello程序的一生代表了一个程序在计算机系统中经历的全过程,可以概括为:代码编写,预处理,编译,汇编,链接,运行,信号的接收与处理,内存管理,IO管理。
本次大作业,更加系统性的对之前的知识进行了串联和整理。有利于对于计算机系统的整体感知。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello可执行程序
hello.c源代码
hello.dump可重定位目标文件的反汇编结果
hello.elf可重定位目标文件的ELF阅读结果
hello.i预处理文件
hello.o可重定位目标文件
hello.s编译文件
hello_exec.dump可执行程序的反汇编结果
hello_exec.elf可执行程序的ELF阅读结果
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] (美)布赖恩特(Bryant,R.E.). 深入理解计算机系统[M]. 北京:机械工业出版社,2016.7
(参考文献0分,缺失 -1分)