计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 人工智能(未来技术模块)
学 号 7203610709
班 级 2036014
学 生 陈潇凯
指 导 教 师 刘宏伟
计算机科学与技术学院
2021年5月
本文旨在通过分析hello.c的在Linux系统下的整个生命周期,探讨和学习hello.c的预处理、编译、汇编、链接的主要过程。结合所学知识进一步理解和阐述hello程序的进程、存储和I/O管理。
关键词:计算机系统;P2P;预处理;编译;汇编;链接
目 录
2.2在Ubuntu下预处理的命令............................................................................. - 6 -
5.3 可执行目标文件hello的格式...................................................................... - 20 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 26 -
6.3 Hello的fork进程创建过程......................................................................... - 27 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................... - 31 -
7.3 Hello的线性地址到物理地址的变换-页式管理......................................... - 31 -
7.4 TLB与四级页表支持下的VA到PA的变换................................................ - 32 -
7.5 三级Cache支持下的物理内存访问............................................................. - 32 -
7.6 hello进程fork时的内存映射..................................................................... - 33 -
7.7 hello进程execve时的内存映射................................................................. - 33 -
7.8 缺页故障与缺页中断处理.............................................................................. - 34 -
8.2 简述Unix IO接口及其函数.......................................................................... - 36 -
第1章 概述
1.1 Hello简介
P2P:在Linux系统下,通过编译器编写hello.c文件,得到源程序后,通过C预处理器(cpp)将hello.c进行预处理生成hello.i文件;通过C编译器(ccl)将hello.i翻译生成汇编语言文件hello.s;通过汇编器(as)将hello.s翻译打包为可重定位的hello.o;最后通过连接器将其与库函数链接,得到可执行文件hello。然后在shell中输入./hello,fork一个新的子进程,再通过调用execve函数加载hello程序,即实现了由程序(Program)到程序(progress)的转变,即P2P。
020:execve将程序加载到新创建的进程中,对虚拟内存进行映射,将程序载入物理内存中执行,然后CPU为该进程分配时间片。程序执行结束后,向父进程发送SIGCHLD信号,由父进程对其进行回收。hello进程从无到有空间进行运行再到空间被回收即为020(From Zero to Zero)。
1.2 环境与工具
硬件环境:处理器 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz 2.59 GHz
机带 RAM 16.0 GB (15.8 GB 可用)
软件环境:Windows 10 64位;Ubuntu20.04;VMWare
使用工具:Codeblocks;edb;Objdump;HexEdito
1.3 中间结果
文件名 | hello.i | hello.s | hello.o | hello |
文件来源 | hello.c的预处理结果 | hello.i编译后的汇编文件 | hello.s翻译成可重定位文件 | 可执行文件 |
文件名 | helloo.obj | helloo.elf | hello.obj | hello.elf |
文件来源 | hello.o的反汇编结果 | hello.o的ELF格式文件 | hello的反汇编结果 | hello的ELF格式文件 |
本章介绍了hello的P2P、020过程,大致讲述了hello程序的“一生”,并给出了进行试验时的软硬件环境及使用的工具。
第2章 预处理
2.1 预处理的概念与作用[7、8]
预处理的概念:在程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。这个过程并不对程序的源代码进行解析,但把源代码分割或处理成为特定的单位。
预处理的作用:处理以#开头的预处理指令,如头文件的引入、宏定义、注释的删除、条件编译等,提高了C语言的通用性。
2.2在Ubuntu下预处理的命令
在Linux系统中,对hello.c文件进行预处理的命令为gcc -E -o hello.i hello.c
图2-1 预处理命令以及生成文件
从上图可看出,在使用这个指令后,得到了一个名为hello.i的文件。
2.3 Hello的预处理结果解析
用gedit查看hello.i的部分内容如下
图2-2 hello.i文件中的部分内容
粗读hello.i文件即可发现,原本hello.c中的内容在转化为hello.i的过程中被大大增多了。其中,hello.i对于原文件中的宏进行了宏展开,且内容包含了头文件中的内容,删除了注释等。
2.4 本章小结
本章对于hello.c进行了预处理,介绍了预处理的概念、作用和使用方式,并对预处理的结果进行了分析,发现预处理的处理内容包括头文件引入、宏替换、注释删除等。
第3章 编译
3.1 编译的概念与作用[9、10]
编译的概念:编译指的是将某一种程序设计语言写的程序翻译成等价的另一种语言的程序,这儿的编译是指从.i到.s,即预处理后的文件到生成汇编语言程序。
编译的作用:编译程序的基本功能是把源程序(高级语言)翻译成目标程序。但是,作为一个具有实际应用价值的编译系统,除了基本功能之外,还应具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用以及人-机联系等重要功能。
3.2 在Ubuntu下编译的命令
在Linux系统中,对hello.i文件进行编译的命令为gcc -S hello.i -o hello.s
图3-1 编译命令以及生成文件
从上图可看出,在使用这个指令后,得到了一个名为hello.s的文件。
3.3 Hello的编译结果解析
3.3.1 汇编指令:汇编程序中以.为开头的名称并不是指令的助记符,不会被翻译成机器指令,而是给汇编器一些特殊指示。在hello.s中出现的伪指令包括[11、12]:
伪指令 | 作用 | 伪指令 | 作用 |
.file | 声明源文件 | .text | 只读可执行代码段 |
.section .rodata | 制度数据 | .globl | 声明一个全局变量 |
.align | 对指令或数据的存放地址进行对齐 | .string | 声明一个字符串 |
.type | 声明符号类型 | .long | 声明一个长整型 |
.size | 声明大小 |
3.3.2 数据
1)常量处理
查找hello.c可知在本程序中,printf输出的两个字符串都是常量,而在hello.s中,可以看到这两个字符串被.LC0与.LC1表示。
图3-2 hello.s中常量的声明
2)局部变量
在hello.c中,我们声明了一个整型变量i,而阅读hello.s的代码后,我们可以发现其被存储在-4(%rbp)位置。
图3-3 hello.s中局部变量的声明与存储
3)函数参数
在hello.c中,argc和argv[]作为传输给main函数的参数,在hello.s中分别被存入寄存器%edi和%rsi中,通过栈地址加偏移量进行访问。
图3-4 hello.s中函数参数的存储
4)立即数
例如hello.c文件中if(argc!=4)中的4在hello.s文件中表示为下图中的$4。
图3-5 hello.s中立即数的声明
3.3.3 赋值
在汇编语言中,赋值操作通常使用mov指令来完成,其用法为mov b,a,作用是把a的值传递给b,这类指令可以对立即数和寄存器之间操作,根据其传递的字节数量,可以分为movb(传递一个字节)、movw(传递两个字节)、movl(传递四个字节)、movq(传递八个字节),在hello.c文件中,有赋值操作令int i=0,因为整型占四个字节,所以应该用movl进行数据传输,在hello.s中相关代码如下:
图3-6 hello.s中的赋值操作
3.3.4 类型转换
在hello.c中涉及类型转换的代码为:
图3-7 hello.c中的类型转换
其将argv[3]中的内容强制转换成了int型,而在hello.s中,调用函数完成改内容。
图3-8 hello.s中的类型转换
3.3.5 算数操作
1)加法运算
在汇编语言中,加法运算通过add指令完成,用法为add a,b,作用是把a与b相加,结果赋值给后一位,根据其传递字节数量,add指令也可分为addb、addw、addl、addq四种。
图3-9 hello.s中的加法运算
2)减法运算
在汇编语言中,减法通过sub指令完成,其用法与add一致。
图3-10 hello.s中的减法运算
3.3.6 栈运算
在.s文件中,对栈的运算有弹栈(pop)和压栈(push)两种,其也可以通过b、w、l、q控制传送字节数。
图3-11 hello.s中的栈运算
3.3.7 关系操作
与C语言中通过<、>等符号判断两个数之间的关系不同,在汇编语言中需要两行代码来实现相同的功能,一是cmpl指令,其用法是cmpl a,b,其作用是选择两个需要进行比较的操作数,一般不单独使用;二则是跳转函数,起到判断和跳转的作用。
图3-12 hello.s中的关系操作
常见的跳转指令包括[13]:
指令 | 描述 | 指令 | 描述 |
jmp | 直接跳转 | jne | 不相等时跳转 |
je | 相等时跳转 | js | 负数时跳转 |
jns | 非负数时跳转 | jg | 有符号大于时跳转 |
jge | 有符号大于等于时跳转 | jle | 有符号小于等于时跳转 |
jl | 有符号小于时跳转 | ja | 无符号大于时跳转 |
jb | 无符号小于时跳转 |
3.3.8 取址运算[14]
lea指令是mov指令的变种,其能根据括号里的源操作数来计算地址,再将地址加载到目标寄存器中。
图3-13 hello.s中的取址运算
3.3.9 控制转移
在C语言中,有if、for、break、continue等一系列控制程序运行方向的语句,然而在汇编语言中,程序的跳转全部通过上文的跳转指令完成,当符合跳转条件时则跳转,不符合条件则继续执行后续语句。
3.3.10 函数操作
1)函数调用
.c文件中调用了printf、exit、sleep、atoi、getchar等函数,在.s文件中,通过call指令对这些函数进行调用。
图3-14 hello.s中的函数调用
2)函数返回
在汇编语言中,通过在函数结果添加ret指令来实现函数的返回,由于hello.c中这个例子中,除了main函数外没有其他自己编译的函数,因此只能看到main函数的返回。
图3-15 hello.s中的函数返回
3.4 本章小结
本章介绍了编译的概念与作用以及如何对.i文件进行编译。除此之外,本章分析了再hello.s中出现的各类语句,包括但不限于数据声明、数据赋值、类型转换、算术操作、关系操作等。经过本章的操作,hello程序从C语言代码被结构成了低级的汇编语言。
第4章 汇编
4.1 汇编的概念与作用
汇编的概念[15]:把汇编语言翻译成机器语言的过程称为汇编。用汇编语言编写的程序,机器不能直接识别,要由一种程序将汇编语言翻译成机器语言,这种起翻译作用的程序叫汇编程序,汇编程序是系统软件中语言处理的系统软件。
汇编的作用:将汇编代码转换成计算机能读懂并执行的二进制程序。
4.2 在Ubuntu下汇编的命令
在Linux系统中,对hello.s文件进行汇编的命令为gcc hello.s -c -o hello.o
图4-1 汇编命令以及生成文件
4.3 可重定位目标elf格式[16]
首先使用readelf -a hello.o > helloo.elf指令得到.elf文件。
图4-2 由hello.o生成相关的.elf文件
打开helloo.elf文件,对其各节进行进一步分析:
1)ELF头
图4-3 .elf文件的ELF头
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头的其他部分则包含了帮助链接器语法分析和解释目标文件的信息。其中可以发现hello.o是个可重定位文件;没有段头表;节头表的起始位置为1240;文件中共有14节。
2)节头
图4-4 helloo.elf的节头部分
节头包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。分析上图,节头表的第一项为空,其他13项对应13个不同节的相关信息。信息包括名称、类型、大小、地址、偏移量等。
3)重定位节
图4-5 helloo.elf的重定位节
重定位节中包含.text节中需要进行重定位的信息,当链接器将这个文件和其他文件链接时,需要更新这个位置的信息,即对上图最右端的八个内容进行重定位。
接下来介绍上图中出现的几个名词:
偏移量:需要被修改的引用的节偏移
信息:包含符号和类型两个部分,符号占前四个字节,类型占后四个字节
符号:标识被修改引用应该指向的符号
类型:重定位类型
加数,对被修改引用的值做偏移调整
4)符号表
图4-6 helloo.elf的符号表
从这个表中可以读出某个符号的编号,名称,偏移量和这个符号是局部的还是全局的。
4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
使用odjdump -d -r hello.o > helloo.objdump命令得到.objdump文件
图4-7 反汇编命令以及生成文件
打开.objdump文件进行进一步分析
图4-8 .objdump文件内部代码
将其与第三章中的.s文件进行比较后,发现两者的不同之处主要集中于以下几点:
1)函数调用不同:在hello.s中使用call+函数名对于函数进行调用,而在hello.o中使用call+地址进行函数的调用,其中的地址为函数相对于main的偏移量。
图4-9 .objdump文件中的函数调用
2)数据进制不同:hello.s中采取十进制,而hello.o反汇编中采用的是十六进制。
3)分支转移方式不同:在hello.s中可以采取助记符L0、L1进行跳转,而机器不能读取助记符,故hello.o反汇编后采取的转移方式是直接跳转到对应的地址。
图4-10 .o文件反汇编后的分支转移方式
4.5 本章小结
本章介绍了汇编的概念与作用以及如何进行汇编,并将.o文件转换成ELF文件,对其中的ELF头、节头、重定位节、符号表中的元素与内容进行了较为细致的介绍。此外还将.o文件进行反汇编,将其与汇编前的.s文件进行对比,阐述了二者内容的区别,对机器语言在其中发挥的作用有了初步的了解。
第5章 链接
5.1 链接的概念与作用[17]
链接的概念:通过链接器将目标文件(或许还有库文件)链接在一起生成一个完整的可执行程序。链接程序的主要工作是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另一个文件中的定义连接起来。
链接的作用:链接器的存在使得分离编译成为可能。因为链接,一个大型的应用程序可以被分解成更小更好管理的模块并进行独立的修改和编译。
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的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
使用readelf -a hello > hello.elf得到hello的ELF文件,打开hello.elf文件阅读
1)ELF头
图5-2 hello.elf的ELF头
与helloo.elf相比,可执行文件hello的ELF文件ELF头除了程序起始位置等信息有所不同之外(如节头的起始位置),还需注意其类型修改为了EXEC,即可执行文件。
2)节头
图5-3 hello.elf的节头
其中各类信息的含义与4.3中一致,在此不再赘述
3)程序头
图5-4 hello.elf中的程序头
程序头反映了目标文件的偏移、段的读写/执行权限、对齐要求、开始地址等信息。
4)重定位节
图5-5 hello.elf的重定位节
对比可发现,hello.elf中已经不存在helloo.elf中的.rela.text,说明其已经重定位完成。而.rela.plt中的六个重定位条目由符号名称可知与调用的函数相关,这是因为这些函数的具体地址在动态链接前是未知的。
5)符号表
相较于hello.o的符号表,hello的符号表多了33个条目,共有51个符号,增加的部分为过程中生成的库函数以及启动需要的函数。
图5-6 hello的符号表
5.4 hello的虚拟地址空间
通过edb --run hello运行hello
通过查看data dump图标可以发现虚拟地址从0x401000~0x400ff0。
图5-7 data dump起始位置
而根据5.3中节头处的信息,我们可以用edb查看各个节,例如.init节,起始位置是0x401000,查看edb可知。
图5-8 .init节在edb中的体现
5.5 链接的重定位过程分析
通过objdump -d -r hello > hello.objdump得到hello的反汇编文件。
图5-9 hello的反汇编指令与生成文件
可以发现hello的反汇编文件有192行,远长于hello.o反汇编文件的53行。
比较hello与hello.o的反汇编文件,可以发现不同之处主要集中于如下几点:
1)地址表示不同:hello.o的反汇编代码地址是从0开始的,而hello的反汇编代码地址使用虚拟地址,从0x401000开始,可见经过链接,每个符号拥有了确定的地址。
2)函数汇编代码的引入:hello.o的反汇编代码中只有main的汇编代码,而hello中除了main函数的代码,还有很多其他函数的汇编代码,如puts函数:
图5-10 hello中的puts函数
hello的重定位过程[18]:
1)重定位段和符号定义:在这个步骤,链接器将所有相同类型的段都整合进入一个新的聚合的段中。例如,所有的输入模块中的.data段都会被整合进入输出的可执行文件的.data段中。链接器接下来为新生成的段,原模块中的段以及原模块中的符号赋予运行时地址。当这步完成后,程序中的每一个指令和全局变量都有了一个独一无二的运行时地址内存地址。
2)用段重定位符号引用:在这步中,链接器会修改代码和数据段中的每个符号引用,这时符号引用会指向正确的运行时地址。为了执行这步,链接器依赖一种在可重定位目标文件中的数据结构,称为重定位实体(relocation entries)
5.6 hello的执行流程
(以下格式自行编排,编辑时删除)
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
图5-11 执行流程
函数的调用顺序
地址 | 函数名 |
0x401125 | hello!main |
0x401090 | hello!.plt+0x70 |
0x401030 | hello!puts@plt |
0x401020 | hello!.plt |
7f14cedae5d0 | libc_2.31.so!_IO_file_xsputn |
7f14cedafd80 | libc_2.31.so!_IO_file_overflow |
7f14cedb0e80 | libc_2.31.so!_IO_doallocbuf |
7f14ceda0c70 | libc_2.31.so!_IO_doallocate |
5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。ELF在进行动态链接时,会在第一次调用其他模块的函数时进行链接,需要动态链接的函数被存放在.plt片段,可以通过.got.plt的位置进行确定。
查看表头,可以发现.got.plt的起始位置是0x404000,进入edb,找到0x404000地址,在.init前,地址为:
图5-12 .init前的地址
执行程序,在.init后,地址变化:
图5-13 .init后的地址
5.8 本章小结
本章介绍了链接的概念与作用,详细说明了hello.o链接生成一个可执行文件的过程。此外,本章还分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接分析等内容。
第6章 hello进程管理
6.1 进程的概念与作用[19]
(以下格式自行编排,编辑时删除)
进程的概念:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
进程的作用:进程可以解决在一个系统并发执行多个任务。
6.2 简述壳Shell-bash的作用与处理流程[20]
Shell是一个命令行解释器,它为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序,用户可以用Shell来启动、挂起、停止甚至时编写一些程序。Shell还是一个功能相当强大的编程语言,易编写,易调试,灵活性较强。Shell是解释执行的脚本语言,在Shell中可以直接调用Linux系统命令。而其中Shell-bash就是Shell的一个分类。
Shell-bash的作用:
1)命令别名与快捷键
2)历史命令
3)输出重定向
4)多命令顺序执行
5)通配符
Shell-bash的处理流程:
1)按行读取命令
2)处理引用问题:双引号和单引号内的字符将失去其原有意义,除了$、”、和\。
3)将输入的一行字符串按照;分割成多个命令
4)处理特殊字符
5)变量替换:将带$符号的变量替换成变量内容。
6)将命令行分割成执行命令和参数:分割的原则是任何空白都将作为分隔符。
7)执行命令:如果命令是一个函数或内置指令,则直接执行;否则创建新的bash子进程处理。
6.3 Hello的fork进程创建过程
(以下格式自行编排,编辑时删除)
当在shell上输入./hello命令时,由于该命令不是内置指令,因此父进程调用fork创建一个子进程并在其中执行。在这个过程中,新创建的子进程具有与父进程相同的代码、数据段、堆、用户栈等信息,但他们具有不同的PID,虚拟地址也相互独立。
图6-1 hello的fork进程创建
6.4 Hello的execve过程
execve的作用为在当前进程的上下文中加载并运行一个新程序。在执行fork指令得到子进程之后使用解析后的命令行参数调用execve,execve调用启动加载器来执行hello程序。
execve在运行时执行以下四个步骤:
1)删除已经存在的用户区域
2)映射私有区域:为新程序的代码、数据、bss和栈区域创建新的区域结构
3)映射共享区域:例如将hello程序与标准C库libc.so链接。
4)设置PC:设置当前进程的上下文中的程序计数器,使之指向代码区域的入口处,方便下次调用。
6.5 Hello的进程执行
(以下格式自行编排,编辑时删除)
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.5.1 上下文信息:上下文就是内核重新启动一个被抢占的进程所需的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
6.5.2 进程时间片[21]:操作系统的任务调度时采用时间片轮转的抢占式调度方式,也就是一个任务执行一小段时间后强制暂停去执行下一个任务,每个人物轮流执行。任务执行的一小段时间叫做时间片。
6.5.3 用户态和核心态:
1)用户态:在用户模式,进程不允许执行特权指令,不允许直接引用地址空间中内核态的代码和数据。
2)核心态:在核心态,可以执行任何命令、访问系统中任何内存位置。
6.5.4 上下文切换:在一个进程正在执行时,内核调度了另一个新的进程运行。在上下文切换时,需要保存以前进程的上下文,读取新恢复进程被保存的上下文,并把控制传递给新恢复的进程。
6.5.5 hello进程的执行
hello在运行时处于用户态,在属于它的时间片结束时,其会休眠(sleep)并返回核心态,通过上下文切换开始另一个进程。直到其再次被分配时间片后,其从核心态返回用户态继续执行,如此反复与其他进程交替占用CPU。
6.6 hello的异常与信号处理
(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
异常可以分为四类,分别是:中断、陷阱、故障和终止。在hello的执行过程中,四种异常都有可能出现。当异常被异常处理程序处理后,根据异常的类型,可能出现下列三种情况中的一种:
1)继续执行当前指令
2)执行下一条指令
3)终止出现异常的程序
6.6.1 正常运行
图6-2 正常运行结果
6.6.2 ctrl-Z
图6-3 按下ctrl-Z结果
按下ctrl-Z后会停止前台的任务。
6.6.3 ctrl-Z后输入ps、jobs等命令
图6-3 ctrl-Z后使用ps、jobs与pstree的结果
可以看出我们可以使用ps查看进程情况,发现其没有结束,PID为7167;也可以使用pstree看到其程序的具体位置。
除此之外我们还可以使用fg将其调至前台继续运行。
图6-4 ctrl-Z后使用fg使之继续运行
6.6.4 ctrl-C
图6-5 按下ctrl-C后的结果
可以从ps的输出中发现,hello程序的PID值已不可查,说明其已经被终止。
6.6.5 乱按,包括回车
图6-6 乱按+回车后的输出结果
6.7本章小结
本章介绍了进程的概念和作用,并较为细致的介绍了Shell的功能和处理过程,阐述了fork和execve的函数功能与使用情况,最后还展示了hello进程的执行情况与异常信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1)逻辑地址:在程序运行时由中央处理单元生成的内容的地址称为逻辑地址。逻辑地址是内部和编程使用的,不唯一。
2)线性地址:也成为虚拟地址。线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,再加上基地址就是线性地址。
3)物理地址:进程及其内容放置在主内存或硬盘中的地址,是内存中的内存单元实际地址。该地址不能直接由用户程序访问或查看,是地址变换的最终结果
7.2 Intel逻辑地址到线性地址的变换-段式管理[23]
一个逻辑地址由段标识符和段内偏移量组成。段标识符是一个16位长的字段(段选择符)。可以通过段标识符的前13位(索引号),直接在段描述符表中找到一个具体的段描述符。
给出一个完成的逻辑地址,根据段选择符确定要转换的段存放于全局段描述符表中还是局部段描述符表中,再根据相应寄存器得到其地址和大小。然后根据索引号查找到对应的段描述符,进而的到基地址。将基地址与逻辑地址中的段内偏移量相加即可得到线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理[24]
线性地址即虚拟地址(VA)到物理地址(PA)的转换通过分页机制完成,即对虚拟地址内存空间进行分页。
接下来介绍页式管理的一些基本概念:
1)页框:将内存空间分成一个个大小相等的分区,每个分区就是一个页框。
2)页框号:每一个页框有一个编号,这个编号就是页框号,从0开始。
3)页面:将进程分割成和页框大小相等的一个个区域,也叫页。
4)页号:每一二个页面有一个编号,叫做页号,从0开始。
操作系统会以页框为单位为各个进程分配内存空间,进程的每一个页面分别放入一个页框中,也就是进程的页面和内存的页框具有一一对应的关系。
实现由线性地址到物理地址的变换由以下三步组成:
1)确定一个进程内的页对应物理内存中的起始地址a值
2)确定进程页内地址b值
3)逻辑地址对应的实际物理地址即为c=a+b
7.4 TLB与四级页表支持下的VA到PA的变换
现代CPU中都有一个TLB(Translation Lookaside Buffer),即“快表”。
处理器生成一个虚拟地址,并将其传送给内存管理单元(MMU)。MMU用虚拟页号(VPN)向TLB请求对应的页表条目(PTE),如果命中,则跳过之后的几步。MMU生成PTE地址(PTEA).,并从高速缓存/主存请求得到PTE。如果请求不成功,MMU向主存请求PTE,高速缓存/主存向MMU返回PTE。PTE的有效位为零, 因此 MMU触发缺页异常,缺页处理程序确定物理内存中的牺牲页 (若页面被修改,则换出到磁盘——写回策略)。缺页处理程序调入新的页面,并更新内存中的PTE。缺页处理程序返回到原来进程,再次执行导致缺页的指令。
图7-1 TLB
7.5 三级Cache支持下的物理内存访问
获得物理地址之后,将其分为标记、组索引、块偏移三个部分。先取出组索引对应位,在L1中寻找对应组。如果存在,则通过标记和Cache的有效位来判断内容是否在Cache中,如果命中则通过块偏移读取所需数据,如果不命中则再下一级Cache(L2、L3、主存)中寻找。寻找到所需内容后,向上一级逐级返回至L1。
图7-2 三级Cache
7.6 hello进程fork时的内存映射
fork函数调用执行时,内核为新进程创建虚拟内存、创建各种数据结构、分配新且唯一的pid、创建当前子进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记成只读,并将两个进程中的每个区域结构都标记为私有的写时复制。fork返回时,新进程的虚拟内存地址和最初的虚拟内存地址相同。
7.7 hello进程execve时的内存映射
在6.4节中,本文已经给出execve的加载步骤,如下:
1)删除已经存在的用户区域
2)映射私有区域:为新程序的代码、数据、bss和栈区域创建新的区域结构,其中虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。
3)映射共享区域:例如将hello程序与标准C库libc.so链接。
4)设置PC:设置当前进程的上下文中的程序计数器,使之指向代码区域的入口处。
图7-3 execve函数
7.8 缺页故障与缺页中断处理
缺页故障:当指令引用一个虚拟地址,而与该虚拟地址对应的物理页面不存在于内存中,就发出发缺页故障。
缺页中断处理:
1)检查虚拟地址是否合法
2)如果不合法,触发段错误,程序终止;如果合法,进一步检查进程是否有读写或执行该区域页面的权限
3)如果无权限则触发保护异常,程序终止;如果有权限,则内核选择一个牺牲页,更新PTE
4)再次执行导致缺页的命令,CPU将再一次引起缺页的虚拟地址发送给MMU。这一次因为虚拟页面已经存储在物理内存中,所以会命中。
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
通常,编译器在编译时都可以根据变量(或对象)的类型知道所需内存空间的大小,从而为他们分配确定的存储空间。这种内存分配称为静态存储分配;而有些操作对象所需要的内存只在程序运行时才能确定,这样编译时就无法为他们预定存储空间,只能在程序运行时,系统根据运行时的要求进行内存分配,这种方法称为动态存储分配。所有动态存储分配都在堆区中进行,由动态内存分配器进行维护。
分配器的种类分为显示分配器和隐式分配器,其中显示分配器要求应用显示地释放任何已分配的块;隐式分配器则是在检测到已分配块不再被程序使用时释放块。
分配器的具体操作过程分为[25]:
1)放置已分配块:当一个应用请求一个k字节的块时,分配器搜索空闲链表。查找一个足够大可以放置所请求的空闲块。分配器搜索方式的常见策略是首次适配、下一次适配和最佳适配。
2)分割空闲块:一旦分配器找到一个匹配的空闲块,就必须做一个另策决定,那就是分配这个块多少空间。分配器通常将空闲块分割为两部分。第一部分变为了已分配块,第二部分变为了空闲块。
3)获取额外的堆内存:如果分配器不能为请求块找到空闲块,一个选择是合并那些在物理内存上相邻的空闲块,如果这样还不能生成一个足够大的块,分配器会调用sbrk函数,向内核请求额外的内存。
4)合并空闲块:分配器释放一个已分配块时,要合并相邻的空闲块,合并时间由分配器决定。
7.10本章小结
本章介绍了有关内存管理的知识,其中包括hello程序的存储方式,逻辑地址、线性地址与物理地址之间的转换方式。此外还阐述了三级Cache的物理内存访问、fork与execve时的内存映射、缺页故障与缺页中断处理、动态内存分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
Linux 系统和其他 UNIX 系统一样,IO 管理比较直接和简洁。所有的I/O设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读写来执行。
8.2 简述Unix IO接口及其函数
Unix IO接口:IO接口使得所有的输入和输出都能以一种统一且一致的方式来执行,其具有的功能如下:
1)打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备。Shell创建的每个进程都有三个打开的文件,即标准输入、标准输出与标准错误。
2)改变当前文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0。
3)读写文件:一个读操作就是从文件复制n(n>0)个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF;一个写操作就是从内存中复制n(n>0)个字节到一个文件,从当前文件位置k开始,然后更新k。
4)关闭文件:当应用完成了对文件的访问之后,通知内核关闭这个文件。
Unix IO函数:
1)int open(char* filename,int flags,mode_t mode):通过调用open函数打开一个存在的文件或创造一个新文件。其中filename被转化为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符;flags 参数指明了进程打算如何访问这个文件;mode参数指定了新文件的访问权限位。
2)int close(fd):fd是需要关闭的文件的描述符(C中表现为指针),close 返回操作结果
3)ssize_t read(int fd,void *buf,size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
4)ssize_t wirte(int fd,const void *buf,size_t n),write 函数从内存位置 buf 复制至多 n个字节到描述符为 fd的当前文件位置。
8.3 printf的实现分析
printf代码:
图8-1 printf的函数体
可以看到其中调用了一个vsprintf与write函数,其内容为:
图8-2 vsprintf函数
write函数的内容如下:
图8-3 write函数
可以看出vsprintf程序按照fmt格式结合参数args生成格式化后的字符串并返回其长度;write函数将buf中的i个元素写入终端。
8.4 getchar的实现分析
图8-4 getchar的函数
可以发现getchar函数是通过系统函数read实现的,其提供read从缓冲区中读入一行并返回第一个字符。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的IO设备管理方法、IO接口及函数,阐述了printf和getchar函数的实现分析.
结论
hello经历的过程:
- 编写hello.c的源程序,这是hello程序的诞生。
- 对hello.c进行预处理,得到hello.i。
- 对hello.i进行编译处理,得到hello.s。
- 对hello.s进行汇编处理,得到可重定位文件hello.o。
- 对hello.o进行链接,得到了可执行文件hello。
- 通过shell,hello程序被运行。
- 为了运行输入命令,shell-bash调用fork函数生成子进程。
- execve函数加载当前进程的上下文并运行新进程hello。
- 运行阶段,内核负责调度进程,并对异常信号进行处理,MMU、TLB等多各要素协同工作完成对内存的管理,Unix IO完成程序与文件之间的交互。
10)终止阶段,hello进程被shell回收,内核删除为hello创建的所有数据结构。
附件
文件名 | hello.i | hello.s | hello.o | hello |
文件来源 | hello.c的预处理结果 | hello.i编译后的汇编文件 | hello.s翻译成可重定位文件 | 可执行文件 |
文件名 | helloo.obj | helloo.elf | hello.obj | hello.elf |
文件来源 | hello.o的反汇编结果 | hello.o的ELF格式文件 | hello的反汇编结果 | hello的ELF格式文件 |
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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.
[8] 预处理在c语言中的作用,C语言(预处理)_殷浩天的博客-CSDN博客
[9]编译程序_百度百科
[10] 编译的基本概念_Vic·Tory的博客-CSDN博客_编译的概念
[11] 处理器startup.s 常见汇编指令,伪指令解释 .globl _start .section .data .text .align_大吉机器人的博客-CSDN博客
[13] (3)汇编语言之跳转指令_yuzhong_沐阳的博客-CSDN博客_汇编je
[14] leaq c 汇编语言,汇编语言lea指令使用方法解析_芬格尔 m~~~的博客-CSDN博客
[15] 汇编 什么意思_百度知道
[16] Linux | 简单认识认识ELF文件_嵌入式大杂烩的博客-CSDN博客
[17] 生成可执行程序四个步骤:预处理、编译、汇编、链接_WJ8871的博客-CSDN博客_如何链接程序生成可执行文件
[18] 7.7-11 重定位过程描述+可执行目标文件的加载+共享库动态链接_你回到了你的家的博客-CSDN博客_重定位过程
[19] 进程(一段程序的执行过程)_百度百科
[20] Shell(bash) 介绍_liaowenxiong的博客-CSDN博客_shell中bash
[21] 进程,时间片,并发与并行_little-peter的博客-CSDN博客_进程时间片