计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 数据科学与大数据技术
学 号 2021112608
班 级 2103501
学 生 贺荣奇
指 导 教 师 史先俊
计算机科学与技术学院
2023年4月
程序的一生在程序员的眼里是透明的,这篇文章我们将从hello在linux下的运行的一生来分析,介绍了预处理、编译、汇编、连接、回收的整个过程及原理,同时也涉及到存储管理、进程管理、i/o控制。
关键词:预处理;编译;汇编;链接;进程管理;存储管理;linux;I/O
(摘要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程序最初以hello.c的文件存放在磁盘中,之后通过我们编译器驱动器(gcc)的编译下,首先进行预处理,生成hello.i文件,主要是展开头文件并将定义的宏赋值。然后经过编译步骤,生成汇编代码,也就是hello.s文件。之后通过汇编器(AS)对文件进行进一步转化,生成hello.o文件(可重定位目标文件)之后再通过链接器(LD)链接,生成可执行文件。 在shell中进入对应目录并输入./hello运行hello程序,shell会为这个程序fork子进程,此时由Program转变成了Process。
(020)文件的运行,首先在终端(Terminal)下,输入命令gcc hello.c,完成对上述文件的生成(中间文件不保留),得到hello.out文件,而后输入命令./hello.out,这时shell会创建一个子进程(fork()),将程序hello.out加载之内存中该子进程的上下文中(execve()),通过流水线化的CPU来处理该程序。在程序执行结束之后,盖子进程会变为一个僵死进程,发送一个信号(SIGCHLD)给shell,shell会来回收该子进程(将其从内存中删除),操作系统内核删除其相关信息,释放它所占用的内存空间。
1.2 环境与工具
硬件环境:X64 CPU intel core i7
软件环境:VMware ,Ubuntu22.04
开发工具:vim,gedit,codeblocks,edb
1.3 中间结果
作用 | |
Hello.c | 源程序 |
Hello.i | 预处理后的文件 |
Hello.s | 汇编文件 |
Hello.o | 可重定位目标执行文件 |
Hello | 可执行文件 |
Hello.elf | Hello.o的elf文件 |
Hello_asm.txt | Hello.o的反汇编 |
Hello1_asm.txt | Hello的反汇编 |
Hello1.elf | Hello的elf |
1.4 本章小结
在这一章中,我们开始了对HELLO的一生的剖析,翻译了p2p和020在程序中的意思。介绍了中间结果和实验环境。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
与处理的概念:程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。
预处理的作用:源文件经过预处理可以得到便于编译器工作的.i文件
2.2在Ubuntu下预处理的命令
使用gcc -E hello.c -o hello.i
图2.1预处理命令
2.3 Hello的预处理结果解析
图2.2hello.i文件
这里可以看到预处理后的hello.i文件有3091行。
这是因为头文件中的代码以函数的形式编入了此文件中。搜索后发现果然如此。说明了三个头文件stdio.h、stdlib.h、unistd.h均已经被预处理。
图2.3hello.i中头文件部分
2.4 本章小结
本章介绍了预处理的概念以及作用(包括头文件的展开、宏替换、去掉注释、条件编译等),在ubuntu上输入命令进行测试,然后分析了hello.c到hello.i的过程和代码变化。
学习到了预处理的使用方法及好处:合理的使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
3.1.1概念:
c语言是对汇编语言的抽象,汇编语言是对硬件的抽象,编译过程是整个程序构建的核心部分,会将源代码由文本形式转换成机器语言,编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的.s汇编代码文件,文件内是汇编语言。
3.1.2作用:
把源程序(高级语言)翻译成汇编语言。其包括以下几个步骤:
1、词法分析:词法分析是编译器的第一个步骤,它也被称为扫描。词法分析器通过读取源程序的字符流对其进行扫描,并且把它们组成有意义的词素(lexeme)序列,并产生词法单元(token) 作为输出,传递给语法分析。
词素是 token 的实例,词法分析器的主要任务就是从源程序中读取字符并产生 token。token 的结构为<token-name, attribute-value>,token-name代表分析的抽象符号,attribute-value指向符号表中该条目的位置。
符号表能够记录源程序中使用变量的名称,并收集和每个名称相关的属性信息,例如变量的存储分配、类型、作用域,对于过程方法还有参数类型、传递方法、返回类型。符号表为每个变量创建一个记录条目,编译器可以迅速查找到记录并对其进行存取。
2.语法分析:语法分析又称为 解析。语法分析器使用词法单元的第一个分量来创建树形的中间表示–语法树。树中的每个非叶节点都表示一个运算,其左右子节点表示运算的分量。编译器的后续步骤都会使用这个语法结构来帮助分析源程序,并声称目标程序。
3、中间代码:源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码。
4、目标代码生成与优化:代码生成器将中间代码转成机器代码,这个过程是依赖于目标机器的,因为不同的机器有着不同的字长、寄存器、数据类型等。最后目标代码优化器对目标代码进行优化,比如选择合适的寻址方式、使用位移来代替乘除法、删除出多余的指令等。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
图3.1编译命令
图3.2文件生成
3.3 Hello的编译结果解析
3.3.1数据
(1)常量
观察初始C语言代码
图3.3源程序代码
发现初始的常量有argv这个4,以及i这个5.找到在.s文件中对应的
图3.4常量一
这里直接转换为立即数4,与变量作比较。另一个5在
图3.5常量2
这里是立即数4与变量作比较如果小于等于进入循环。
字符串:原文中有两个字符串
图3.6常量3
被存成了只读字符串。
(2).变量
全局变量、静态变量:本代码中没有。
局部变量:
图3.7变量1源
使用是在for循环中使用。所以我们只要找循环判断条件的出口处就能找到这个变量,
图3.8变量1
得知是存在了栈中,通过栈顶地址加上偏移量来访问。
hello.c中的其他局部变量还包括argc和argv,同样地,它们都存放在栈上的,并通过相对栈顶(%rsp)的偏移量来访问。
图3.8栈
3.3.2赋值
hello.c中只有一处赋值,就是在for循环中对i赋初值0
图3.9赋值源
在前面我们已经确定i存在-4(%rbp)中,查找代码得知
图3.10赋值
这里是用mov指令的方式进行赋值操作的,是直接将立即数0赋给i。而这里因为是int型的数据,是4个字节,故使用movl指令。
3.3.3算术操作
for循环中有明显的算术操作i++
图3.11算术操作
观察是通过add指令进行的,同样因为是int型变量,所以使用的是addl
3.3.4关系操作
共有两个关系操作
图3.12关系操作源
一个是不等于,一个是小于。在.s文件中我们能找到
图3.13关系操作
均是通过cmp指令完成的。
3.3.5数组操作
传进来的是数组指针*agrv,后面在调用printf函数以及atoi函数时也使用了数组中的值正在上传…重新上传取消
图3.14数组操作
这里发现是将这个数组存到了栈中,调用时也是使用了数组的首地址加偏移量的方式进行的。
3.3.6控制转移
与关系操作那里是关联的。就是通过前面的cmp判断变量的关系,然后通过jne、jle等指令进行跳转。
3.3.7函数操作
图3.15函数操作源
本代码中共计使用了五种函数调用。
观察调用函数的汇编代码
图3.16函数操作
发现调用函数使用的是call指令,同时,在每次调用之前都会进行一个movl操作,我们得知这是传参数的行为,函数的参数储存在rdi、rsi等寄存器中,返回值储存在rax寄存器中。
3.4 本章小结
本章围绕编译操作展开,首先介绍了编译的概念和作用,之后在Linux中将hello.i编译成hello.s,并对编译后的hello.s进行分析。分析hello.c中的各个内容到汇编代码中的形式与内容。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编的概念: 汇编程序是指把汇编语言书写的程序翻译成与之等价的机器语言程序的翻译程序。汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序。也就是说,汇编器会把输入的汇编指令文件重新打包成可重定位目标文件,并将结果保存成.o文件。它是一个二进制文件,包含程序的指令编码。
汇编的作用: 它的作用也很明晰了,就是完成从汇编语言文件到可重定位目标文件的转化过程。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
gcc hello.s -c -o hello.o
图4.1汇编命令
(以下格式自行编排,编辑时删除)
应截图,展示汇编过程!
4.3 可重定位目标elf格式
4.3.1ELF头
使用readelf -h hello.o查看elf头
图4.2ELF头
以一个 16B 的大小的magic 数字开头,这个数描述了该文件的系统的字的大小和字节顺序。剩下的部分包含帮助链接器语法分析和解释目标文件的信息,包括 ELF 头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量等信息。
4.3.2查看节点头(section head)
使用readelf -S hello.o指令查看节点头
4.3节点头
其中ELF头和节头之间是各种节点。
其中
名称 | 内容含义 |
.text | 已编译程序的机器代码 |
.rodata | 制度数据 |
.data | 已初始化的全局和静态C变量 |
.bss | 未初始化的全局和静态C变量以及所有被初始化为零的全局和静态变量 |
.rel.data | 被模块引用或定义的所有全局变量的重定位信息 |
.rel.text | 一个.tex节中位置的列表 |
.symtab | 一个符号表 |
.strtab | 一个字符串表 |
.line | 原始C中的行号和.text中的映射 |
.debug | 一个调试符号表 |
4.3.3查看符号表
使用readelf -s hello.o查看符号表
图4.4符号表
其中,name是符号名称、value是符号相对起始位置的偏移量、type是类型,标记符号是数据还是函数、bind表明符号是局部的还是全局的、Size表明目标大小。Ndx该字段是一个到节头部表的索引,表明对应符号被分配至目标文件的某个节;其中有三个特殊的伪节,他们在节头部表中是没有条目的:ABS代表不该被重定位的符号,und代表未被定义的符号,即引用的外部符号。
4.3.4 可重定位段信息
使用readelf -r hello.o
图4.5可重定位段
其中偏移量是需要被修改的引用的节偏移量,符号值标识被修改引用应该指向的符号。类型分为两种一种是R_X86_64_32(重定位一个使用32位PC相对地址的引用。通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。)和R_X86_64_PLT32(重定位一个使用32位PC相对地址的引用。通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。)两种,作用是告知连接器如何修改新的引用,而加数是一个有符号常数,一些类型的重定位要使用它对被修改的引用的值做偏移调整。
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
4.4 Hello.o的结果解析
使用objdump -d hello.o>hello_asm.txt指令将.o文件反汇编,将结果输出到txt文件中这是反汇编的结果
图4.6反汇编代码
这是一串反汇编的代码与.s文件中的代码十分相似。不同的是在这个txt内容中的左边有着一串十六进制的代码,这是右边对应的汇编语言的机器语言代码。这些机器代码是二进制机器指令的集合,每一条机器代码都对应一条机器指令,到这儿才是机器真正能识别的语言。每一条汇编语言都可以用机器二进制数据来表示,汇编语言中的操作码和操作数以一种相当于映射的方式和机器语言进行对应,从而让机器能够真正理解代码的含义并且执行相应的功能。
同时在分支转移函数调用时,在汇编语言内容中使用的是跳转到标识符,而在汇编语言中则是直接指明了跳转的地址。同时在汇编语言中立即数是十进制显示的、而在反汇编代码中是使用16进制。
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
4.5 本章小结
本章首先介绍了汇编的概念和作用,接着通过实操,对hello.s文件进行汇编,生成ELF可重定位目标文件hello.o,接着使用readelf工具,通过设置不同参数,查看了hello.o的ELF头、节头表、可重定位信息和符号表等,通过分析理解可重定位目标文件的内容。最后将其与hello.s比较,分析不同,并说明机器语言与汇编语言的一一对应关系。
(第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.2生成文件
使用指令结束之后生成hello的可执行文件。
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
包括各段的起始地址,大小等信息。
图5.3ELF头
type类型为exec表明hello是一个可执行目标文件,由之前的14个section变为现在的27个section
输入readelf -S hello可以看到节头sectionheaders的具体信息
图5.4节点头
Section Headers:节头部表,记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。 因此根据 Section Headers 中的信息我们就可以用 HexEdit 定位各个节所占的区间(起始位置,大小)。其中地址是程序被载入到虚拟地址的起始地址。
输入指令readelf -s hello可以看到hello.elf中Symbol table的具体信息,结果如下:
图5.5符号表
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.4 hello的虚拟地址空间
图5.6程序头
观察程序头:分析程序头LOAD可加载的程序段的地址为0x400000
从edb打开hello从Data Dump窗口观察hello加载到虚拟地址的状况
edb界面如下
图5.7EDB窗口
具体观察data dump窗口
图5.8、5.9DATADUMP
发现虚拟地址从0x400000到0x401ff0结束。根据的节头部表,可以通过edb找到各个节的信息。
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
5.5 链接的重定位过程分析
图5.9重定位指令
使用objdump -d -r hello指令得到hello的反汇编文件。发现与hello,o文件的反汇编有所不同
1.节的数目
与hello.o的反汇编文件对比发现,hello_asm1.txt中多了许多节。
hello_asm.txt中只有一个.text节,而且只有一个main函数,函数地址也是默认的0x000000.
hello_asm1.txt中有.init,.plt,.text三个节,而且每个节中有很多函数。库函数的代码都已经链接到了程序中,程序各个节变的更加完整,跳转的地址也具有参考性。
如图
图5.10各种信息
明显后来生辰的这个反汇编文件中多了几个节
这些节分别是
hello比hello.o多出的节头表:
.interp 保存ld.so的路径
.note.ABI.tag Linux下特有的section
.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 初始化过的全局变量或者声明过的函数
2.内容差异
hello_asm.txt中虚拟地址没有明确,均为0,没有完成可重定位的过程
图5.11section
而在hello_asm1.txt中
图5.12hello_asm1.tx
都有了明确的虚拟地址,说明已经完成了重定位。
重定位就是把程序的逻辑地址空间变换成内存中的实际物理地址空间的过程。它是实现多道程序在内存中同时运行的基础。重定位有两种,分别是动态重定位与静态重定位。
hello重定位的过程:
(1)重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
(2)重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。
(3)重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目。利用在.rela.data和.rela.txt节中保存的重定位信息,来修改对各个符号的引用,即修改他们的地址。
!!!
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.6 hello的执行流程
1.开始执行:start、libc_start_main
2.执行main:main、printf、exit、sleep、_getchar
3.退出:exit
程序名及地址:
_start 0x4010f0
_libc_start_main 0x2f12271d
main 0x401125
_printf 0x401040
_exit 0x401070
_sleep 0x401080
_getchar 0x401050
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完成的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。
在进行动态链接前,首先要进行静态链接,生成部分链接的可执行目标文件hello。动态链接采用了延迟加载的策略,即在调用函数时才进行符号的映射。使用偏移量表got+过程链接表plt实现函数的动态链接。got中存放函数目标地址,为每个全局函数创建一个副本函数,并将对函数的调用转换成对副本函数调用。.got全局偏移表 .plt程序链接表
观察elf文件中.got的起始位置
图5.13elf中Got
为0x403ff0
在edb中查看
图5.14edb查询
发现在调用dl_init之前的0x403ff0后的16个字节均为0
图5.15调用后内容
在调用之后发现,发生了变化存入了地址。
5.8 本章小结
本章首先介绍了链接的概念和作用,详细说明了可执行目标文件的结构,及重定位过程。并且以可执行目标文件hello为例,具体分析了各个段、重定位过程、虚拟地址空间、执行流程等。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。通过进程的概念提供给我们一个假象,就好像我们的程序是系统中运行的唯一的程序;程序好像独占地使用处理器和内存;处理器好像是无间断地一条接一条地执行程序中的指令;程序的代码和数据好像是系统内存中唯一的对象。
其中上下文是由程序正确运行所需的状态组成的,包括存放在内存中的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。
6.2 简述壳Shell-bash的作用与处理流程
shell是指为使用者提供操作界面的软件,是一个交互型应用级程序,它接收用户命令,然后调用相应的应用程序。shell是系统的用户界面,提供了用户与内核进行交互操作的接口。
shell的作用:shell最重要的功能是命令解释,可以说shell是一个命令解释器。Linux系统上的所有可执行文件都可以作为shell命令来执行,同时它也提供一些内置命令。此外,shell还包括通配符、命令补全、命令历史、重定向、管道、命令替换等很多功能。
shell的处理流程:
①从终端读入输入的命令。
②将输入字符串切分获得所有的参数。
③检查第一个命令行参数是否是一个内置的shell命令,如果是则立即执行。
④如果不是内部命令,调用fork( )创建新进程/子进程执行指定程序。
⑤shell应该接受键盘输入信号,并对这些信号进行相应处理。
6.3 Hello的fork进程创建过程
进程的创建采用fork函数:pid_t fork(void);创建的子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程fork时,子进程可以读取父进程中打开的任何文件。
父进程与创建的子进程之间最大的区别在于它们有不同的PID。子进程中,fork返回0。因为子进程的PID总是为非零,返回值就提供一个明确的方法分辨程序是在父进程还是在子进程中。
在终端中输入命令行./hello 2021112608 贺荣奇 1后,首先shell对我们输入的命令进行解析,由于我们输入的命令不是一个内置的shell命令,因此shell会调用fork()创建一个子进程,子进程几乎但不完全与父进程相同。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的但是独立的一份副本,拥有不同的PID。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序,原型为
:int execve(const char *filename, const char *argv[], const char *envp[])
程序会加载运行可执行文件filename,而且是带着参数列表argv和环境变量列表envp,当然在错误的时候会返回(也仅在错误时返回),或者是在没有找到filename时会返回。所以,execve()函数调用一次且不会返回。
execve函数加载可执行文件,创建一组新的代码、数据、堆栈,设置pc指向_start的地址并调用main函数。
6.5 Hello的进程执行
进程提供给程序的抽象:
- 独立的逻辑控制流,造成一种进程自己使用逻辑控制流的假象。
- 独立的地址空间,造成进程认为地址空间被他独占的假象
上下文信息:操作系统使用上下文切换的异常控制流来实现多任务。上下文分为用户级上下文和系统级上下文。每个进程的虚拟空间地址和进程本身一一对应,也就意味着这个上下文其实就是进程能够正常运作所需的一堆状态。如堆栈数据等这些都存放在虚拟地址空间中,由于CPU仅仅只能同时处理一个程序,然而实际情况中,我们并不满足于只做一个工作,所以我们使用上下文切换的方法,实现一种交替式的并行操作。
进程时间片:书接上文,其实就是在这一时间段内是由这个进程执行的一个时间段就交做一个时间片。
内核模式与用户模式:处理器使用一个寄存器的一个模式位来设置一个应用可以执行的指令的权限和访问地址空间的限制。
hello进程执行过程:
开始运行在用户模式,当调用sleep后陷入内核模式,内核处理休眠请求释放该进程,将其移入等待序列中,当休眠时间结束后会发送给内核一个中断信号,内核进行中断处理,将hello从等待序列中移除,就绪后hello就可以继续执行了。
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.6 hello的异常与信号处理
异常类别 | 异常类别 | 处理方式 |
中断 | 收到i/o信号 | 读取异常号,调用中断处理程序,返回下一条指令 |
陷阱 | Hello的父进程执行syscall指令fork一个hello | 陷阱处理程序在内核态中完成fork工作,返回syscall之后的指令 |
故障 | 缺页异常 | 从磁盘中加载适当页,然后重新加载指令 |
终止 | 出现DRAM或SRAM位损坏的奇偶错误 | Abort例程终止改程序 |
6.6.1乱按:
图6.1乱按
可以发现乱按并不会影响程序正常运行。
6.6.2回车:
图6.2回车
由图中得知,回车也不会影响进程工作。
6.6.3 Ctrl-Z:
图6.3C+Z
发现按下Ctrl+Z后程序停止。并显示:已停止
此时我们输入ps:
图6.4查看ps
发现hello其实还在进程列表中。
我们再输入jobs
图6.5查看jobs
显示如上
输入pstree:
图6.6查看pstree
综上内容,我们得出结论,ctrl+z后程序虽然提示已停止,其实并没有终止,而是相当于挂在了后台。
输入kill 4330
图6.7 kill
6.6.4 Ctrl+C:
图6.8C+C
没有显示已停止但是退出了程序。
查看ps后发现后台并没有hello说明已经终止。
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.7本章小结
本章介绍了进程的概念和作用,以及壳Shell-bash的作用与处理流程,调用 fork 创建新进程,调用 execve函数执行 hello,最后介绍了执行过程中的异常与信号处理。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
我们首先说明说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:由程序产生的段偏移地址。它由一个段标识符和一个段偏移量组成。使用段标识符作为GDT/LDT的下标,查询表格以获得段地址。分段地址+末端偏移量=线性地址
线性地址:一个有序的非负整数地址集。如果这时的地址是连续的,就说这个空间是一个线性地址空间。它是逻辑地址向物理地址转化过程中的一步。
虚拟地址:在保护模式下,程序在虚拟内存中运行。虚拟内存被组织成一个存储在磁盘上的N个连续的字节大小的单元阵列。每个字节都有一个唯一的虚拟地址,作为数组的索引。虚拟地址由VPO(虚拟页偏移)、VPN(虚拟页号)、TLBI(TLB索引)和TLBT(TLB标签)组成。
物理地址:计算机系统的主存储器被组织成一个由M个连续的字节大小的单元组成的阵列,每个字节有一个唯一的物理地址,例如,第一个字节地址是0,第二个地址是1,以此类推。物理地址空间对应于系统中的M个字节的物理内存。{0,1,2......M-1}.
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成:段标识符,段内偏移量。段标识符是一个16位的字段,被称为段选择符。其中前13位是一个索引号。可以通过段标识符的索引号,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。很多个段描述符,就组了一个数组,叫“段描述符表”,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符。每一个段描述符由8个字节组成。
Base字段,它描述了一个段的开始位置的线性地址。Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表”中,用段选择符中的T1字段来判断用全局段描述符表还是局部段描述符表,=0,表示用全局段描述符表,=1,表示用局部段描述符表。
给定一个完整的逻辑地址段选择符+段内偏移地址,看段选择符的T1为0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。使用段选择符中的前13位,在这个数组中查找到对应的段描述符,即可得到它的基地址,基地址Base + offset即要转换的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址即虚拟内存(VA)到物理地址(PA)之间的转换通过分页机制完成,而分页机制是对虚拟空间进行分页。
操作系统将虚拟页作数据传输的基本单元,linux下每个虚拟页大小为4kb,物理内存也被分割为物理页,MMU负责翻译,能够将虚拟地址映射到物理地址。
虚拟地址(n位)包括两个部分:一个p位的虚拟页内偏移(VPO VIRTURAL PAGE OFFSET)和n-p位虚拟页号(VPN),MMU利用VPN选择适当的PTE根据PTE我们得知虚拟页的信息,如果樊村了,就将页表中对应的物理页号与VPO连接起来就是一个物理地址,如果为缓存那么将导致一个缺页故障,详见第六章。
7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,MMU就要查阅PTE,很慢!我们整了一个TLB快表来改善。TLB通过虚拟地址VPN部分进行所以,分为索引TLBI和标记TLBT两个部分。这样MMU直接读取PTE,不命中时再将PTE中的复制到TLB中,相当于多了一级。
7.5 三级Cache支持下的物理内存访问
获得物理地址之后,先取出组索引对应位,在L1中寻找对应组。如果存在,则比较标志位,相等后检查有效位是否为1.如果都满足则命中取出值传给CPU,否则按顺序对L2cache、L3cache、内存进行相同操作,直到出现命中。然后再一级一级向上传,如果有空闲块则将目标块放置到空闲块中,否则将缓存中的某个块驱逐,将目标块放到被驱逐块的位置。
7.6 hello进程fork时的内存映射
Shell通过调用fork()函数自动创建一个新的进程,这个进程有新的数据结构,并且内核给他分配了一个唯一的pid。同时有着自己的独立的虚拟内存空间,独立的逻辑控制流,除此以外还有与父进程相同的区域结构、页表等的一份副本,也可以访问任何父进程已经打开的文件。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间。
7.7 hello进程execve时的内存映射
在bash中的进程中执行了如下的execve调用:execve("hello",NULL,NULL);execve函数在当前进程中加载并运行包含在可执行文件hello中的程序,用hello替代当前bash中的程序。删除已存在的用户区域,然后映射私有区域,映射共享区域,为了设置程序计数器,最后要用exceve设置当前进程的上下文中的程序计数器到代码区域的入口点。
7.8 缺页故障与缺页中断处理
如果程序执行过程中发生了缺页故障,则内核会调用缺页处理程序。程序首先会检查虚拟地址是否合法,如果不合法则触发一个段错误,将程序终止。然后检查进程是否有读、写或执行该区域页的权限,如果不具有则触发保护异常,程序终止。两步检查都无误后,内核将选择一个牺牲页,如果该页被修改过则将其交换出去,换入新的页并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。
7.9动态存储分配管理
所有动态申请的内存都存在堆上面,用户通过保存在栈上面的一个指针来使用该内存空间。动态内存分配器维护着堆,堆顶指针是brk。有两种风格,一种叫显式分配器,使用两个函数,malloc和free,分别用于执行动态内存分配和释放。
malloc的作用是向系统申请分配堆中指定size个字节的内存空间。也就是说函数返回的指针为指向堆里的一块内存。并且,操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序申请时,就会遍历该链表,然后寻找第一个空间大于所申请空间的堆结点,将该结点从空闲结点链表中删除后,将该结点的空间分配给到程序。在使用malloc()分配内存空间后,需释放内存空间,否则就会出现内存泄漏。
free()释放的是指针指向的内存,而不是指针。指针并没有被释放,它仍然指向原来的存储空间。因此指针需要手动释放,指针是一个变量,只有当程序结束时才被销毁。释放了内存空间后,原本指向这块空间的指针仍然存在。但此时指针指向的内容为垃圾,是未定义的。因此,释放内存后要把指针指向NULL,防止该指针后续被解引用。
7.10本章小结
本章主题是hello的存储管理,在不同空间中有着不同的地址,虚拟空间中是虚拟地址,在物理空间中是物理地址。程序使用逻辑地址,在操作系统中进行两种地址的转换来使用,同时分析了hello的内存映射、缺页故障与缺页中断处理和动态存储分配管理等。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有IO设备都被模型化为文件,所有的输入和输出都能被当做相应文件的读和写来执行。
设备管理:Linux内核有一个简单、低级的接口,成为Unix I/O,是的所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix I/O接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想 要访问一个 I/O 设备
2.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函数:
1.打开文件:int open(char* filename,int flags,mode_t mode)
Filename:文件名
Flags:文件权限
Mode:指定新文件的访问权限位
返回值:成功则返回文件描述符,失败则返回-1.
2.关闭文件:int close(int fd)
Fd:文件描述符
返回值:成功则返回0,失败则返回-1
3.读文件ssize_t read(int fd,void *buf,size_t n)
Buf:存储要写的数据
n:写入的长度,单位:字节
返回值:成功则返回写入的字节数,失败则返回-1。
4.写文件ssize_t wirte(int fd,const void *buf,size_t n)
Buf:存储要写的数据
n:读出的长度,单位:字节
返回值:成功则返回读出的字节数,失败则返回-1。
8.3 printf的实现分析
图8.1printf源码
printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。printf用了两个外部函数,一个是vsprintf,还有一个是write。
vsprintf函数作用是接受确定输出格式的格式字符串fmt(输入)。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
write函数将buf中的i个元素写到终端。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar代码部分:
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时,程序发生陷阱的异常。当按键盘时会产生中断。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf和getchar函数的实现。
(第8章1分)
结论
hello所经历的过程:
- 通过编辑器编写形成最初的.c文件,是hello的源程序。
- 使用预处理器cpp将.c文件预处理形成.i文件
- 使用编译器ccl将其进行翻译生成汇编语言文件.o文件
- 使用汇编器将其翻译成一个可重定位目标文件hello.o
- 使用连接器程序ld将hello.o与系统目标文件结合起来,创建了一个可执行文件hello。
- 运行hello程序
- 通过shell输入./hello,shell通过fork函数创建了一个新的进程,之后调用execve映射虚拟内存,通过mmap为hello程序开创了一片空间。
- CPU从虚拟内存中的.text,.data节取代码和数据,调度器为进程规划时间片,有异常时触发异常处理子程序。
- 程序运行结束时,父进程回收hello进程和它创建的子进程,内核删除相关数据结构。
深切感悟:
从大一开始学习C语言,每一个操作都看似不难,编译指令仅仅只需要我们按一下编译按钮,这个按钮的背后是复杂的指令是前辈智慧的结晶!我要学习的东西还很多,要继续加油继续努力!
(结论0分,缺失 -1分,根据内容酌情加分)
附件
文件名 | 作用 |
Hello.c | 源程序 |
Hello.i | 预处理后的文件 |
Hello.s | 汇编文件 |
Hello.o | 可重定位目标执行文件 |
Hello | 可执行文件 |
Hello.elf | Hello.o的elf文件 |
Hello_asm.txt | Hello.o的反汇编 |
Hello1_asm.txt | Hello的反汇编 |
Hello1.elf | Hello的elf |
列出所有的中间产物的文件名,并予以说明起作用。
(附件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.
(参考文献0分,缺失 -1分)