计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机类
学 号 120L030321
班 级 2003004
学 生 曾文浩
指 导 教 师 史先俊
计算机科学与技术学院
2022年5月
摘要
本文通过一个简单的程序hello.c的“一生”,在Linux系统下,对该程序从到运行到最后终止过程的底层实现进行了分析,介绍了与之相关的计算机系统知识。通过gcc、objdump、edb等工具对该程序代码预处理、编译、汇编、链接以及反汇编的过程进行分析与比较,并且通过Shell-Bash及其他Linux内置程序对进程运行过程进行了分析。研究该内容对深入理解计算机系统具有积极作用。
关键词:Linux;hello;编译;反汇编;进程管理;存储管理;IO管理
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 5 -
3.2 在Ubuntu下编译的命令............................................................................. - 6 -
4.2 在Ubuntu下汇编的命令............................................................................. - 7 -
5.2 在Ubuntu下链接的命令............................................................................. - 8 -
5.3 可执行目标文件hello的格式.................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 10 -
6.3 Hello的fork进程创建过程..................................................................... - 10 -
6.6 hello的异常与信号处理............................................................................ - 10 -
7.1 hello的存储器地址空间............................................................................ - 11 -
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.1 Linux的IO设备管理方法.......................................................................... - 13 -
8.2 简述Unix IO接口及其函数....................................................................... - 13 -
第1章 概述
1.1 Hello简介
源程序文本hello.c,首先由预处理器(cpp)根据以字符#开头的命令进行修改生成新的源程序文本hello.i;然后编译器(ccl)将文本文件hello.o翻译成汇编程序文本文件hello.s;然后汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序二进制文本文件hello.o;然后,链接器(ld)将hello.o与其他函数的目标文件合并,生成hello可执行目标程序。最后,在Linux系统的内置命令行解释器Shell加载运行hello,通过fork创建一个新的进程,hello就完成了P2P的一生。
Shell使用execve函数加载运行hello,为其分配虚拟内存空间,构建虚拟内存映射,MMU组织各级页表与cache为hello提供需要的所有信息,CPU为hello提供时间片并控制逻辑流。最后,由Shell回收hello的进程,内核清除与hello相关的所有内容,hello就完成了020的一生。
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;2GHz;2G RAM;256GHD Disk
1.2.2 软件环境
Windows11 64位以上;Vmware 16.2.3;Ubuntu 20.04 LTS 64位/优麒麟 64位
1.2.3 开发工具
Visual Studio 2022 64位;CodeBlocks 20.03 64位;vim;gedit;vim;gcc;as;ld;edb;readelf;objdump
1.3 中间结果
hello.i : hello.c经过预处理得到的文本文件
hello.s : hello.i经过编译器得到的汇编程序文本
hello.o : hello.s经过汇编器得到的可重定位的目标程序
objdump_o.txt : hello.o反汇编生成的文本文件
hello.out : hello.o链接后生成的可执行目标程序
objdump_out.txt : hello.out反汇编生成的文本文件
1.4 本章小结
本章介绍了Hello的P2P以及020的过程;并对使用到的硬件环境、软件环境以及开发工具等做了说明;最后,对产生的中间文件进行了说明。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
1.概念:程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。由预处理器(preprocessor)对程序源代码文本进行处理,这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位,即预处理记号(preprocessing token),可以用来支持语言特性(如宏调用)。
2.作用:预处理器(cpp)根据以字符#开头的命令,修改原始的c程序。比如,hello.c中第六行的#include <stdio.h>、第七行的#include <unistd.h>以及第八行的#include <stdlib.h>命令告诉预处理器读取头文件stdio.h,unisted以及stdlib.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常以.i作为文件扩展名。
2.2在Ubuntu下预处理的命令
1.预处理的命令: cpp hello.c > hello.i
2.预处理的过程:
如图2.2
图2.2
2.3 Hello的预处理结果解析
预处理过程只是将头文件stdio.h,unisted以及stdlib.h中的内容插入了源程序hello.c,得到了修改了的源程序hello.i,此时仍然是文本文件,可以用文本编辑器打开。如下图所示,hello.i之中就有库文件中fprint()、printf()等的函数声明,如图2.3.1。
此外,hello.i文本文件末尾是没有注释以及以字符#开头命令的剩余代码,如图2.3.2所示。
图2.3.2
可以看到,经过预处理后得到的hello.i源程序相较于hello.c大小大了很多,如图2.3.3所示。
图2.3.3
2.4 本章小结
本章主要介绍了预处理的概念以及预处理的作用,并给出了在Ubuntu下C语言中预处理的命令以及预处理的过程,最后,对hello.c的预处理结果进行了解析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
1.概念: 将一个预处理后的源程序文本文件(以.i作为扩展文件名)翻译成一个执行完全相同操作的汇编程序文本文件(以.s作为扩展文件名)的过程。
2.作用: 编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含了一个汇编语言程序。其中,hello.s中的每条语句都以一种文本格式描述了一条低级机器语言指令。汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。例如,C编译器和Fortran编译器产生的输出文件用的都是一样的汇编语言。
注意:这章的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序。
3.2 在Ubuntu下编译的命令
1.编译的命令: gcc -S hello.i -o hello.s
2.编译的过程:如图3.2所示
图3.2
3.3 Hello的编译结果解析
3.3.1数据
3.3.1.1常量
.LC0:与.LC1:处有两个常量String,如图3.3.1.1.
图3.3.1.1
3.3.1.2变量
循环中的变量i,存放在-4(%rbp)的位置,如图3.3.1.2.
图3.3.1.2
3.3.2赋值
通过movl,给循环中的变量i赋初始值0,如图3.3.2.1
图3.3.2.1
通过movq,movl等,给寄存器赋值传入参数值,如图3.3.2.2
图3.3.2.2
3.3.3算术操作
在取地址传参数的过程中,用addq对地址进行操作,如图3.3.3
图3.3.3
3.3.4关系操作
通过cmpl以及je,执行argv4==4操作,如图3.3.4.1
图3.3.4.1
通过cmpl以及jle,执行循环变量i<=7操作,如图3.3.4.2
图3.3.4.2
3.3.5数组/指针/结构操作
通过subq $32,%rsp,移动栈指针,存放argc以及char *argv[]数组,如图3.3.5.1
图3.3.5.1
通过栈指针的变换对数组argv[1],argv[2],argv[3]进行取值,并传参,如图3.3.5.2
图3.3.5.2
3.3.6控制转移
该控制转移是一个for循环,其中.L2包含了循环变量i的初始化;.L3包含了循环体以及对循环变量的+1操作;.L4包含了对循环条件的判断,如图3.3.6
图3.3.6
3.37函数操作
将.LC0(%rip)处的String作为参数传进函数puts(),并且调用函数puts(),如图3.3.7.1
图3.3.7.1
将常量1作为参数传进函数exit(),并且调用函数exit(),如图3.3.7.2
图3.3.7.2
对数组地址进行一系列的操作,将argv[1],argv[1]以及.LC1(%rip)处的String传进函数printf(),并且调用printf(),如图3.3.7.3
图3.3.7.3
对数组地址进行操作,取-8(%rbp)处的argv[3]作为参数传进函数atoi(),并且调用函数atoi(),然后函数返回值存在寄存器%eax。然后将%eax处的返回值作为参数传进函数sleep(),并且调用函数sleep(),如图3.3.7.4
图3.3.7.4
最后,调用函数getchar(),如图3.3.7.5
图3.3.7.5
3.4 本章小结
本章主要介绍了编译的概念以及编译的作用,并给出了在Ubuntu下C语言中编译的命令以及编译的过程,最后,对的编译的结果hello.s进行了详细的解析,包括数据、赋值、算数操作、关系操作、数组/指针/结构操作、控制转移、函数操作等。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
1.概念: 汇编是把汇编语言翻译成机器语言的过程。
2.作用: 汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件。如果我们在文本编辑器中打开hello.o文件,将看到一堆乱码。
注意:这章的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
1.汇编的命令: gcc -c hello.s -o hello.o
2.汇编的过程:如图4.2
图4.2
4.3 可重定位目标elf格式
4.3.1 ELF头
ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(可重定位的)、机器类型(AMD X86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。如图4.3.1所示。
图4.3.1
4.3.2 节
夹在ELF头和节头部表之间的都是节。该ELF重定位目标文件包含下面几个节:
.text: 已编译程序的机器代码。
.rela.txt: 一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
.data: 已初始化的全局和静态C变量。
.bss: 未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。
.rodata: 只读数据,比如printf语句中的格式串和开关语句的跳转表。
.symtab: 一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
.strtab: 一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。
以及.shstrtab,.comment,.note.GNU-stack,.note.gnu.propert,.eh_frame,.rela.eh_frame等
,如图4.3.2
图4.3.2
4.3.3 重定位节
重定位节是一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
图4.3.3.1展示了ELF重定位条目的格式。offset是需要被修改的引用的节偏移。symbol标识被修改引用应该指向的符号。type告知链接器如何修改新的引用。addend是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。
图4.3.3.1
图4.3.3.2
4.3.4 符号表
符号表存放在程序中定义和引用的函数和全局变量的信息。
图4.3.4.1展示了ELF符号表条目的格式。name是字符串表中的字节偏移,指向符号的以null结尾的字符串名字。value是符号的地址。size是目标的大小(以字节为单位)。type通常要么是函数要么是数据。section表示每个符号被分配到目标文件的节。
图4.3.4.1
图4.3.4.2
4.4 Hello.o的结果解析
4.4.1 反汇编代码
通过objdump -d -r hello.o > objdump.txt生成了hello.o的反汇编文本文件,如图4.4.1.1所示。
图4.4.4.1
文本内容如图4.4.1.2,4.4.1.3所示。
图4.4.1.2
图4.4.1.3
4.4.2机器语言以及与汇编语言的映射关系
汇编代码的左侧这些数字代表的是机器语言的二进制代码。机器语言是一种指令集的体系,是最底层的计算机语言,用机器语言编写的程序都是由8位二进制数构成的。每个8位的二进制数都是有特定含义的指令或数据,是计算机唯一可以直接识别和执行的语言,它具有计算机可以直接执行、简洁、运算速度快等优点,但它的直观性差,非常容易出错,程序的检查和调试都比较困难,此外对机器的依赖型也很强。
4.4.3对照分析
对于操作数
hello.s的汇编代码中,操作数是用10进制进行表示的,如图4.4.3.1
图4.4.3.1
而在反汇编生成的代码中,操作数是用16进制进行表示的,如图4.4.3.2
图4.4.3.2
对于分支转移函数
hello.s的汇编代码中,跳转是直接跳转至代码段,如.L2,.L3,如图4.4.3.3
图4.3.3.3
而反汇编生成的代码中,跳转时给的是地址或者说是地址偏移量,如图4.4.3.4中80<main+0x80>。
图4.3.3.4
此外,机器代码为这些函数和数据留下了一些地址以便链接后重定向。例如图4.4.3.5中8<main+0x58>。
图4.4.3.5
4.5 本章小结
本章主要介绍了汇编的概念以及汇编的作用,并给出了在Ubuntu下C语言中汇编的命令以及汇编的过程。然后,对的可重定位目标文件的ELF格式进行了分析,包括ELF头、节、重定位节以及字符表等。最后,对hello.o进行了反汇编,并分析反汇编的结果中机器代码与汇编代码的对应,然后与hello.s的汇编代码进行了对照分析,包括操作数,分支转移函数以及重定位信息等。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
1.概念: 链接是将多个可重定位目标文件组合成为一个可执行目标文件的过程。
2.作用: 链接使用链接器(ld)将该目标文件与其他目标文件、库文件、启动文件等链接起来生成可执行文件,可以被加载到内存,由系统执行。附加的目标文件包括静态连接库和动态连接库。
注意:这章的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令
1.命令: gcc hello.o -o hello.out
2.过程:如图5.2
图5.2
5.3 可执行目标文件hello的格式
5.3.1 ELF头
ELF头的入口点地址0x4010f0,大小为64字节。
ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(可执行文件)、机器类型(AMD X86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。如图5.3.1所示。
图5.3.1
5.3.2节
夹在ELF头和节头部表之间的都是节。该ELF可执行文件相较于hello.o没有了.rela.text以及.rela.data节,具体包含下面几个节:
.text: 已编译程序的机器代码。
.data: 已初始化的全局和静态C变量。
.bss: 未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。
.rodata: 只读数据,比如printf语句中的格式串和开关语句的跳转表。
.symtab: 一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
.strtab: 一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。
.text: 该节中包含程序的指令代码;
.init: 该节包含进程初始化时要执行的程序指令。当程序开始运行时,系统会在进程进入主函数之前先执行这一个节中的指令代码;、。
.fini: 该节中包含进程终止时要执行的指令代码。当程序退出时,系统会执行这个节中的指令代码。
.dynamic: 该节中包含动态链接信息,并且可能有SHF_ALLOC和SHF_WRITE等属性。
.dynstr : 该节中包含用于动态链接的字符串,一般是那些与符号表相关的名字。
.dynsym : 该节中包含动态链接符号表。
.got : 该节中包含全局偏移表(global offset table)。
.plt : 该节中包含函数链接表(function link table)。
.hash : 该节中包含一张哈希表。
.interp : 该节中包含ELF文件解析器的路径名。如果该节被包含在某个可装载的段中,那么该节的属性中应设置SHF_ALLOC标志位,否则不设置此标志位。
.shstrtab: 该节是节名字表,含有所有其它节的名字。
.comment: 该节中包含版本控制信息。
.line : 该节中包含调试信息,包括哪些调试符号的行号,为程序指令码与源文件的行号建立联系。
.note : 该节中包含注释。
如图5.3.2.1、5.3.2.2、5.3.2.3所示。
图5.3.2.1
图5.3.2.2
图5.3.2.3
5.3.2 程序头
程序头部表描述了可执行文件的连续的片被映射到的连续的内存段。如图5.3.2.1、5.3.2.2所示,其中,offset表示目标文件中的偏移;virtAddr表示虚拟内存地址;physAddr表示物理内存地址;Filesize表示目标文件中的段大小;MemSiz表示内存中的段大小;Flags表示运行时的访问权限;align表示对齐的要求。
图5.3.2.1
图5.3.2.2
5.3.3字符表
与hello.o文件相比,字符表得到了扩展,具有66个入口,如下图所示
图5.3.3.1
图5.3.3.2
图5.3.3.3
图5.3.3.4
5.4 hello的虚拟地址空间
5.4.1.init节
根据5.3中虚拟地址位置信息得到,.init节起始位置:0x401000;大小:0xe0;占用内存地址:0x401000-0x4010e0,如图5.4.1
图5.4.1
5.4.2.text节
根据5.3中虚拟地址位置信息得到,.text节起始位置:0x4010f0;大小:0x200;占用内存地址:0x4010f0-0x4012f0,如图5.4.2
图5.4.2
5.4.3.rodata节
根据5.3中虚拟地址位置信息得到,.rodata节起始位置:0x402000;大小:0x130;占用内存地址:0x402000-0x402130,如图5.4.3
图5.4.3
5.5 链接的重定位过程分析
通过objdump -d -r hello.out查看反汇编的代码,对汇编代码分析注意到以下几点:
5.5.1额外的节
该汇编代码相较于he1llo.o多了.init , .plt , .plt.sec , .fini节。其中,.init节是程序初始化需要执行的代码,.fini节是程序正常终止时需要执行的代码,.plt和.plt.sec节分别是动态链接中的过程链接表和全局偏移量表。如下图所示。
图5.5.1.1
图5.5.1.2
图5.5.1.3
图5.5.1.4
5.5.2地址的不同
相较于hello.o的反汇编代码,该反汇编生成的指令使用的是物理地址(如图5.5.2)进行标识,而 hello.o生成的使用的是虚拟地址。
图5.5.2
总而言之,重定位的实现依靠.rodata这个段,这个段保留重定位所需的信息,叫做重定位条目。链接器解析重定条目时发现两个类型为R_X86_64_PC32的对.rodata的重定位,如printf中的两个字符串,在hello.o的反汇编代码中对printf参数字符串的引用使用虚拟地址,而在hello.out中则使用确定地址。.rodata与.text节之间的相对距离确定,因此链接器将call之后的地址修改为目标地址与下一条指令的地址之差即可。
5.6 hello的执行流程
1.程序名:ld-2.29.so!_dl_start 程序地址:0x7f7955504030
2.程序名:ld-2.29.so!_dl_init 程序地址:0x7f79555129e0
3.程序名:libc-2.29.so!__libc_start_main 程序地址:0x7f795550d350
4.程序名:hello!printf@plt 程序地址:0x00000000004011cb
5.程序名:hello!atoi@plt 程序地址:0x00000000004011de
6.程序名:hello!sleep@plt 程序地址:0x00000000004011e5
7.程序名:hello!getchar@plt 程序地址:0x00000000004011f4
8.程序名:libc-2.29.so!exit 程序地址:0x7f7ae3297800
5.7 Hello的动态链接分析
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向 PLT中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。如图5.7.1.
图5.7.1
在dl_init调用之后,GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址,GOT[2]指向动态链接器ld-linux.so运行时地址。如图5.7.2.
图5.7.2
在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问跳转时GOT地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。之后如果对同样的函数调用,第一次访问跳转直接跳转到目标函数。因为在PLT中使用的jmp,所以执行完目标函数之后的返回地址为最近call指令下一条指令地址,即在main 中的调用完成地址。
5.8 本章小结
本章首先介绍了链接的概念与作用;然后给出了Ubuntu下链接的命令以及过程;然后对hello.out的ELF格式,hello虚拟地址空间,链接的重定位过程,hello的执行流程以及hello的动态链接等做了分析说明。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
1.概念:进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需状态状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
2.进程提供给应用程序两个关键的抽象。一是,一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器;二是,一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
1.作用:Shell是一个宏处理器,允许交互式或非交互式命令执行。它完全基于图形用户界面,以便用户可以与底层操作系统进行交互,其基本功能是解释并运行用户的指令。
2.处理流程
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv参数向量。
(3)检查第一个命令行参数是否是一个内置的shell命令。
(3)如果不是内部命令,调用fork( )创建新进程/子进程。
(4)在子进程中,用步骤(2)获取的参数,调用execve( )执行指定程序。
(5)如果用户没要求后台运行(命令末尾没有&号),shell使用waitpid(或wait)等待作业终止后返回。
(6)如果用户要求后台运行(如果命令末尾有&号),则shell返回;
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于他们有不同的PID。
6.4 Hello的execve过程
execve函数在当前进程上下文加载并运行一个新程序。execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到hello,execve才会返回到调用程序。所以,与fork一次调用返回两次不同,execve调用一次并从不返回。
execve会调用驻留在内存中启动加载器来执行hello程序。加载器会删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。然后新的栈和堆段会被初始化为零,通过将虚拟地址空间中的页映射到可执行文件页大小的片上,将新代码和数据段初始化为可执行文件中的内容。最后加载器设置PC指向_start 地址,再由_start最终调用hello中的main函数。除了一些头部信息,在这个加载过程中没有任何从磁盘到内存的复制操作。一直到CPU引用了一个被映射的虚拟页时才会进行复制。这时候操作系统会利用它的页面调度机制自动将页面从磁盘传送到内存。
6.5 Hello的进程执行
hello在刚开始运行时内核为其保存一个上下文,进程在用户状态下运行。如果没有异常或中断信号的产生,hello将继续正常地执行。如果有异常或系统中断,那么内核控制转移到内核模式并完成上下文切换,将控制传递给其他进程。当 hello运行到sleep时,hello显式地请求休眠,并发生上下文切换,控制转移给另一个进程,此时计时器开始计时,当计时器到达argv[3]时,它会产生一个中断信号,中断当前正在进行的进程,进行上下文切换,恢复hello在休眠前的上下文信息,控制权回到hello继续执行。当循环结束后,hello调用getchar函数,之前hello运行在用户模式下,在调用getchar时进入内核模式,内核中的陷阱处理程序请求来自I/O传输,并执行上下文切换,并把控制转移给其他进程。当完成键盘缓冲区到内存的数据传输后,引发一个中断信号,此时内核从其他进程切换回hello进程,然后hello执行return,进程终止。
6.6 hello的异常与信号处理
Hello执行过程中可能出现的异常种类有中断、故障(缺页故障,调用缺页异常处理函数即可)等,可能出现的信号有SIGINT、SIGSTP等
6.6.1正常运行
程序正常运行的结果,如图6.6.1所示。
图6.6.1
6.6.2不停乱按键盘
由图6.6.2可知,乱按键盘不会影响程序的正常运行。
图6.6.2
6.6.3键盘输入Ctrl-Z
当键盘输入Ctrl-Z时,进程会被挂起,如图6.6.3.1所示。
图6.6.3.1
当进程被挂起时,输入ps,会显示当前进程,如图6.6.3.2所示。
图6.6.3.2
当进程被挂起时,输入jobs,可以看到作业1已停止,如图6.6.3.3所示。
图6.6.3.3
当进程被挂起时,输入pstree,可以看到进程树,包括所有进程,如图6.6.3.4所示。
图6.6.3.4
当进程被挂起时,再输入ps 1,作业1会重新运行,如图6.6.3.5所示。
图6.6.3.5
重新运行进程,并用Ctrl-Z将进程挂起,使用ps查看进程PID后,用kill杀死作业1,当输入fg 1时,会显示任务已杀死,如图6.6.3.6所示。
图6.6.3.6
6.6.4键盘输入Ctrl-C
当在进程运行时,键入Ctrl-C时,进程会直接终止,并且退出。如图6.6.4。
图6.6.4
6.7本章小结
本章介绍了进程的概念和作用以及Shell-bash的作用与处理流程。此外,还介绍了进程的控制操作,包括是Hello的fork进程创建过程和Hello的execve过程,并详细解析了Hello的进程执行。最后,对hello的异常和信号进行了实践分析。其中,异常包括中断、陷阱、故障和终止四种,信号包括Ctrl-Z,ps,jobs,pstree,kill,pg以及Ctrl-C等。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
1.逻辑地址的概念:指由程式产生的和段相关的偏移地址部分,分为两个部分,一部分为段基址,另一部分为段偏移量。
2.线性地址的概念:非负整数并连续的地址的有序集合,称为线性地址空间。
3.虚拟地址的概念:由逻辑地址翻译得到的线性地址就叫虚拟地址。
4.物理地址的概念:计算机系统的主存被组织成一个由 M 个连续的字节大小的单元组 成的数组,其每个字节都被给予一个唯一的地址,这个地址称为物理地址。物理地址用于内存芯片级的单元寻址,与处理器和 CPU 连接的地址总线相对应。
hello.o的反汇编代码中使用的是逻辑地址。由于链接阶段进行了重定位操作,指令以及大部分引用都已经被映射到虚拟内存空间,因此hello的反汇编代码中是使用的是虚拟地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由段标识符和段内偏移量两部分组成。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后3位包含一些硬件 细节。索引号是“段描述符”的索引,多个段描述符,就组了一个数组,叫做“段描述符表”。这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,每一个段描述符由8字节组成。其中Base字段描述了一个段的开始位置的线性地址。全局的段描述符,存放放在“全局段描述符表(GDT)”中,而局部的段描述符,存放在“局部段描述符表(LDT)”中。由段选择符中的T1字段表示选择使用的段描述附表,等于0时,表示用GDT,等于1时,表示用LDT。GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT在ldtr寄存器中。
具体的变换步骤如下:
1. 首先,给定一个完整的逻辑地址[段选择符:段内偏移地址]。
2. 然后,根据段选择符的T1字段,得知需要转换的段描述符表,再根据相应寄存器,得到其地址和大小,得到一个数组。
3. 然后,取出段选择符中的前13位,在数组中查找到对应的段描述符,得到Base字段,也就是基地址。
4. 最后,线性地址 = Base + offset
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址到物理地址(PA)之间的转换通过分页机制完成。而分页机制是对虚拟地址内存空间进行分页。线性地址被分为以固定长度为单位的组,称为页(page)。为了节省空间,引入了一个二级管理模式的机器来组织分页单元。
1.分页单元中,页目录是唯一的,它的地址放在CPU的cr3 寄存器中,是进行地址转换的开始点;
2.每一个活动的进程,因为都有其独立的对应的虚似内存(页目录也是唯一的),那么它也对应了一个独立的页目录地址。运行一个进程,需要将它的页目录地址放到cr3寄存器中;
3.每一个32位的线性地址被划分为三部份,目录索引(10位),页表索引(10位),偏移(12位)。
具体转换步骤如下:
1.从cr3中取出进程的页目录地址
2.根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。页的地址被放到页表中;
3.根据线性地址的中间十位,在页表中找到页的起始地址;
4.将页的起始地址与线性地址中最后12位相加,得到最终我们想要的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
虚拟地址被划分成4个VPN和1个VPO。每个VPNi都是一个到第i级页表的索引,其中1<=i<=4.第j级页表中的每个PTE,1<=j<=3,都指向第j+1级的某个页表的基址。第四级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问4个PTE。将得到的PPN和虚拟地址中的VPO串联起来,就得到相应的物理地址。
7.5 三级Cache支持下的物理内存访问
具体步骤如下:
1.CPU给出VA
2.MMU用VPN到TLB中找寻PTE,若命中,得到PA;若不命中,利用VPN(多级页表机制)到内存中找到对应的物理页面,得到PA。
3.PA分成PPN和PPO两部分。利用其中的PPO,将其分成CI和CO,CI作为cache组索引,CO作为块偏移,PPN作为tag。
先访问一级缓存,不命中时访问二级缓存,再不命中访问三级缓存,再不命中访问主存,如果主存缺页则访问硬盘
7.6 hello进程fork时的内存映射
fork一个hello进程时会为这个新进程提供私有的虚拟地址空间。
1.为新进程hello创建虚拟内存:创建当前进程的mm_struct、vm_area_struct链表和页表的原样副本;把两个进程中的每个页面都标记为只读;再把两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制。
2.当从hello进程返回时,hello进程拥有与调用fork的父进程相同的虚拟内存。
3.随后的写操作会通过写时复制机制创建新页面。
其中,mm_struct(内存描述符)描述了一个进程的整个虚拟内存空间;vm_area_struct(区域结构描述符)描述了进程的虚拟内存空间的一个区间。
7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello的具体步骤如下:
1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区 域结构;
2.映射私有区域,为新程序的代码、数据、.bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text,.data区,和.bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零;
3.映射共享区域,hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内;
4.设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页(page fault)
- 段错误:首先,先判断这个缺页的虚拟地址是否合法,那么遍历所有的合法区域结构,如果这个虚拟地址对所有的区域结构都无法匹配,那么就返回一个段错误(segment fault)
- 非法访问:接着查看这个地址的权限,判断一下进程是否有读写改这个地址的权限。
- 如果不是上面两种情况那就是正常缺页,那就选择一个页面牺牲然后换入 新的页面并更新到页表。
7.9动态存储分配管理
动态内存分配器维护者一个进程的虚拟内存区域,成为堆。分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。分配器有两种基本风格。两种风格都是要求显示的释放分配块。
1.显式分配器:要求应用显示的释放任何已分配的块。例如,C标准库提供一个叫做malloc程序包的显示分配器。
2.隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。
分配器有两种主要实现形式,分别为隐式空闲链表和显式空闲链表。
7.10本章小结
本章首先介绍了储存器的地址空间,说明了虚拟地址、物理地址、线性地址、逻辑地址的概念以及由逻辑地址到线性地址的变换和由线性地址到物理地址的变换;然后介绍了TLB与四级页表支持下的VA到PA的变换和三级Cache支持下的物理内存访问;然后还介绍了进程fork和execve时的内存映射的内容以及描述了系统应对缺页异常与缺页中断处理的方法;最后,描述了malloc的动态存储分配管理机制。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
1.设备的模型化:文件
2.文件的类型:
①普通文件(regular file):包含任意数据的文件。
②目录(directory):包含一组链接的文件,每个链接都将一个文件名映射到一个文件
③套接字(socket):用来与另一个进程进行跨网络通信的文件
④命名通道
⑤符号链接
⑥字符和块设备
3.设备管理:unix io接口
①打开和关闭文件
②读取和写入文件
③改变当前文件的位置
8.2 简述Unix IO接口及其函数
8.2.1UNIX I/O接口
Unix IO将设备映射为文件,允许linux内核引出一个简单、低级的应用接口,使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2.2Unix IO函数
Unix I/O 接口提供了以下函数供应用程序调用:
1.打开文件:
int open(char *filename, int flags, mode_t mode)
2.关闭文件:
int close(int fd)
3.读文件:
ssize_t read(int fd, void *buf, size_t n
4.写文件:
ssize_t write(int fd, const void *buf, size_t n)
8.3 printf的实现分析
8.3.1printf函数体
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;
}
在形参列表里token:…这个是可变形参的一种写法。当传递参数的个数不确定时,就可以用这种方式来表示。很显然,我们需要一种方法,来让函数体可以知道具体调用时参数的个数。
8.3.2分析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返回要打印出来的字符串的长度。所以说,vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
8.3.3分析write
跟踪write得到:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
write函数的作用便是将字符数组buf中的i个元素打印到终端。而这个步骤与硬件有关,这就需要限制程序执行的权限。而write中的最后一行int INT_VECTOR_SYS_CALL便是通过系统来调用函数sys_call来执行下一步操作。
8.3.4分析syscall.
syscall.通过调用字符显示驱动子程序,从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
8.4.1异步异常-键盘的中断处理
当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。
8.4.2调用read函数
这里调用了一个read函数,通过系统调用读取按键ASCII码,直到接受到回车键才返回。这个read函数是将整个缓冲区都读到了buf里面,返回值是缓冲区的长度。我们可以发现,如果buf长度为0,getchar才会调用read函数,否则是直接将保存的buf中的最前面的元素返回。
8.5本章小结
本章介绍了Linux的IO设备管理方法;并且简述了Unix IO接口及其函数;最后对printf以及getchar的实现进行了分析。
(第8章1分)
结论
Hello的一生
- 编写:在键盘鼠标等I/O设备下,编写hello.c源程序,并文本文件的形式存储在主存内。
- 预处理:预处理器(cpp)根据以字符#开头的命令进行修改生成新的源程序文本hello.i。
- 编译:编译器(ccl)将文本文件hello.o翻译成汇编程序文本文件hello.s。
- 汇编:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序二进制文本文件hello.o。
- 链接:链接器(ld)将hello.o与其他函数的目标文件合并,生成hello.out可执行目标程序。
- 运行:在Shell中输入./hello.out 120L030321 zwh 1运行该程序
- 创建子进程:Shell进程调用fork函数为其创建子进程,然后Shell调用execve函数,execve又调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数。
- 执行指令:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流。hello在执行的过程中可能会遇到异常和信号以及命令,执行异常信号的处理流程。
- 内存申请和访问:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址,printf会调用malloc向动态内存分配器申请堆中的内存。
- 结束运行:Shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello.i : hello.c经过预处理得到的文本文件
hello.s : hello.i经过编译器得到的汇编程序文本
hello.o : hello.s经过汇编器得到的可重定位的目标程序
objdump_o.txt : hello.o反汇编生成的文本文件
hello.out : hello.o链接后生成的可执行目标程序
objdump_out.txt : hello.out反汇编生成的文本文件
附录图
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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] Randal E.Bryant,David R.O’Hallaron. 深入理解计算机系统[M]. 机械工业出版社,2016.7
(参考文献0分,缺失 -1分)