计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 未来技术学院人工智能
学 号 2022113437
班 级 111111
学 生 苏海晗
指 导 教 师 郑贵滨
计算机科学与技术学院
2024年5月
本文是对Hello.c程序的介绍,即使是最简单的.c程序也要经历过复杂的系统处理才可成为可执行的程序并被操作系统执行。预处理、编译、汇编、链接缺一不可,OS的fork、execve、mmap为程序运行保驾护航,为了让其运行地更快,整个计算机的存储机制纷繁但高效,链接起了键盘、主板、显卡与屏幕,最后还有Bash始终为hello.c的正确运行提供支撑,本文将从计算机角度阐释其从源程序到可执行程序的转化,并讨论其在OS的运行过程,加深读者对计算机系统的理解。
关键词:计算机系统;汇编;进程;存储;
目录
1.1.1 Program to Process. - 3 -
6.2 简述壳Shell-bash的作用与处理流程... - 22 -
6.3 Hello的fork进程创建过程... - 23 -
7.2 Intel逻辑地址到线性地址的变换-段式管理... - 30 -
7.3 Hello的线性地址到物理地址的变换-页式管理... - 31 -
7.4 TLB与四级页表支持下的VA到PA的变换... - 31 -
7.5 三级Cache支持下的物理内存访问... - 32 -
7.6 hello进程fork时的内存映射... - 32 -
7.7 hello进程execve时的内存映射... - 33 -
第1章 概述
1.1 Hello简介
1.1.1 Program to Process
1. 预处理阶段
在预处理阶段,主要处理以#开头的预处理指令,并进行宏替换。
2. 编译阶段
在编译阶段,预处理后的文件被编译器转换成汇编代码(Assembly Code),该过程主要由编译器完成。编译器将根据语法和语义的检查生成相应的汇编代码,该代码使用特定的汇编语言表示程序的操作和数据。
3. 汇编阶段
汇编器(Assembler)将汇编代码翻译成机器代码(Machine Code),这是计算机能够直接执行的指令。每条汇编语句都对应于一条机器指令或者多条指令,具体取决于计算机体系结构。
4. 链接阶段
链接阶段由链接器(Linker)完成,其主要任务是将编译得到的目标文件(Object Files)和所需的库文件链接在一起,生成最终的可执行文件。
接下来计算机就可以运行这个hello文件了。之后在计算机的Bash(shell)中,OS会为hello创建子进程(fork),这样,在计算机系统中,hello就有了自己独一无二的进程(Process),在这个进程中hello便可以运行。
1.1.2 从零到零
程序从无到有,通过编写、编译、链接等步骤最终形成可执行文件(从零到一),执行后进程终止,资源被回收(从1回到0),整个过程再次回到初始状态。
1.2 环境与工具
1.2.1 硬件环境
X64 CPU; 3.4GHz; 16G RAM; 1024GHD Disk
图1 设备规格
1.2.2 软件环境
Windows11 64位;Ubuntu 22.04;
1.2.3开发工具
Visual Studio Code; vi/vim/gpedit+gcc;
1.3 中间结果
hello.c 源程序
hello.i 预处理后的修改的C程序
hello.s 汇编程序
hello.o 可重定位目标文件
hello 可执行目标文件
Objdump_hello.o hello.o的反汇编文件
Objdump_hello hello的反汇编文件
elf_hello hello的ELF格式
elf_hello.o hello.o的ELF格式
1.4 本章小结
对Program to Process和0 to 0的过程有了大概了解,熟悉了系统的配置、软件与硬件,同时大致了解了后续需要做的工作。
第2章 预处理
2.1 预处理的概念与作用
2.1.1概念
指的是在编译或解释程序代码之前,通过预处理器对源代码进行处理的过程。预处理器通常是一个程序,其主要作用是根据预处理指令(以特定符号或者关键字开头的命令)对源代码进行文本替换或者其他形式的修改,以生成最终的编译代码或者解释所需的代码。
能够对源程序.c文件中出现的以字符“#”开头的命令进行处理,包括宏定义#define、文件包含#include、条件编译#ifdef等,最后将修改之后的文本进行保存,生成.i文件。
2.1.2作用
使代码可以进行代码模块化和重用;宏定义和宏替换;条件编译;错误处理和调试;简化复杂表达式;平台和环境适配,使得程序员能够更加灵活和高效地管理和编写代码,同时提高了代码的可维护性和可移植性。
2.2在Ubuntu下预处理的命令
图2 预处理命令
预处理的命令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
图3 预处理结果
通过预处理,我们可以很直观地发现hello.i当中的代码量相对于源程序剧增。这种剧增就是因为#预处理命令将头文件的程序、宏变量、特殊符号等插入到hello.c中。
2.4 本章小结
本节阐述了预处理的概念和作用。并在Ubuntu执行了预处理命令,生成了hello.i文件,查看hello.i文件,更好地理解了预处理的概念和作用。
第3章 编译
3.1 编译的概念与作用
3.1.1 编译的概念
编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。
3.2 在Ubuntu下编译的命令
图4 编译命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
.file源文件(指从hello.i汇编得来)
.text代码节
.rodata制度代码段
.align代码对齐方式
.global全局变量
.type声明一个符号是数据类型还是函数类型
.string声明了两个字符串分别为.LC0;.LC1
3.1.1数据
1.无宏变量等:只有一个全局(global)为main函数
图5 main函数
2.字符串
图6 字符串
字符串实际上存储在某地址当中,当取出字符串时,需要加载字符串所在地址。
图7 字符串加载地址
3.局部变量
将栈指针-4,为局部变量i开辟4字节空间。
图8 局部变量
4.数组
图9 数组
Argv[]数组。
5.赋值操作:
图10 赋值
将寄存器中的数据或者立即数,加载(mov)到寄存器(或指针)中,其中movl,movq分别表示它们操作的数据大小不同:l表示双字,数据占4个字节;q表示四字,数据占8个字节。
6.算术操作
图11 算数
给指针所存储数据加1并存入该指针。
7.关系操作
图12 比较指令
cmp比较指令,指令执行的结果是返回条件码。
8.控制跳转
图13 跳转指令
在关系操作指令的下一条指令即为跳转指令,跳转指令根据cmp指令返回的条件码进行跳转,实现控制跳转。
9.函数操作
图14 调用sleep函数
图15 调用getchar函数
通过call指令调用过程;
分别调用了头文件提供的printf,sleep,getchar函数。
3.4 本章小结
用高级语言进行编辑时,忽略了程序实际的执行细节,本章从hello.i->hello.s,直观地看到了编译的结果,并将其与C源程序的代码结合起来,理解汇编语言发挥的作用,以过往的实验经历,也可以很熟练地将汇编代码与对应的C语言代码对照。
第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o是一个二进制文件,它包含的17个字节是函数main的指令编码。到.o即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
图16 汇编命令
命令:as hello.s -o hello.o
4.3 可重定位目标elf格式
在经过汇编和链接之后,会产生两种目标文件,分别为:可重定位目标文件和可执行目标文件。目标文件在不同的系统或平台上的具有不同的命名格式。在Unix和x86_64 linux上称为ELF(Executable and Linkable Format).
ELF文件格式提供了两种不同的视角。其中,在汇编器和链接器看来,ELF文件是由Section Header Table描述的一系列Section的集合,而在ELF文件的头部,还有ELF Header描述了体系结构和操作系统等基本信息,并指出Section Header Table和Program Header Table在文件中的什么位置。
所以我们在这里查看可重定位目标ELF格式,首先我们查看ELF Header以及Section Header Table
ELF Header
命令:readelf -h hello.o
图17 readelf命令
从ELF Header我们可以看到hello.oELF格式的一些基本信息。比如main函数的指令编码、操作系统的版本,节头文件的起始地址等等。
Section Header Table
命令:readelf -S hello.o
在Section Header Table中,我们可以看到各Section的描述信息,其中.text和.data是我们在汇编程序中声明的Section,而其它Section是汇编器自动添加的。
之后我们通过readelf -a hello.o探查ELF文件中能探查的节。其中除了ELF Header和Section Header Table,还有.rela.text和.symtab
图18 readelf命令
.rela.text:
.rela.text包含需要重定位的信息,当链接器链接.o文件时,会根据重定位节的信息计算正确的地址,重定位.rela.text中的信息。
从下图中我们可以看到.rela.text中的不同类型的信息。
图19 .rela.text
图20 symtab:
symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
4.4 Hello.o的结果解析
图21 objdump -d -r hello.o的结果
每行代码末尾指令基本相同,但在每条指令前面都会有一串十六进制的编码。hello.s是由汇编语言组成的,相对于计算机能识别的机器级指令,汇编代码仍是抽象语言;而反汇编得到的代码不仅仅有汇编代码,还有机器语言代码。机器语言代码是计算机可识别执行的,是一种纯粹的二进制编码。
机器指令由操作码和操作数构成,从反汇编代码我们可以很清楚地看到,每一条汇编语言操作码都可以用机器语言二进制编码表示,所以可以将所有的汇编代码与二进制机器语言建立一一对应的映射关系。至于操作数,可以直观地机器语言与汇编语言的不同之处。
- 分支转移
hello.s的分支转移是通过指令jmp或je, jne, ... ,等直接跳转到某一段代码
而在反汇编文件中,并不存在汇编语言中的代码段地址,而是直接跳转在当前过程的起始地址加上偏移量得到的直接目标代码地址
- 函数调用
在汇编文件中,call指令直接调用函数,call后紧跟函数的名字。
而在反汇编文件中,我们可以看到call调用的目标指令为call指令的下一个指令。这是因为,在汇编之后、链接之前的hello.o文件中是机器语言文件,但它是缺少调用C库函数的机器语言文件。所以还需要链接将调用的函数的.o文件与hello.o链接到一起,才能得到最终的可执行目标文件。所以在call指令的后面,会留下为链接准备的空间,等待链接器的下一步的重定位、链接。
4.5 本章小结
本章通过将.s汇编为.o文件,了解从汇编程序到可重定位目标程序(二进制)的过程。同时通过查看ELF表,查看了其中的各项内容。又将.o反汇编,通过和汇编程序相对比,了解了他们的不同,也了解了机器代码的逻辑。
第5章 链接
5.1 链接的概念与作用
5.1.1概念
链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以用于编译时,即将源代码翻译为机器码时,加载时,即程序被加载器加载到内存并执行时,还可执行于运行时,也就是用应用程序执行。现代系统中,链接是由叫做链接器(linker)程序自动执行的。
5.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
图22 Ubuntu下链接
5.3 可执行目标文件hello的格式
图23 Ubuntu下链接
图24 readelf
命令:readelf -h hello
命令:readelf -S hello
5.4 hello的虚拟地址空间
图25 edb
使用edb可以查看hello虚拟地址空间。从ELF开始,我们可以知道起始地址为0x400000;
图26 edb
对应的起始地址为0x401090:
5.5 链接的重定位过程分析
hello反汇编文件中,每行指令都有唯一的虚拟地址,而hello.o的反汇编没有。这是因为hello.o经过链接,已经完成重定位,每条指令的地址关系已经确定。
图27 地址关系
在hello反汇编文件中,出现了很多hello.o中没有的过程。这同样是经过重定位,然后链接的结果,链接器将需要重定位的call调用的过程的.o连接到hello当中,并确定下地址关系。
在链接器完成符号解析之后,就把代码中的每个符号引用和正好一个符号定义关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小,然后便可以进行重定位。
汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置,也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以当汇编器遇到对最终位置未知的目标饮用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text中。已初始化数据的重定位条目放在.rel.data中。
5.6 hello的执行流程
- _dl_start、_dl_init
- _start、_libc_start_main
- _main、_printf、_exit、_sleep、_getchar、_dl_runtime_resolve_xsave、_dl_fixup、_dl_lookup_symbol_x
- Exit
程序名称&地址
ld-2.27.so!_dl_start 0x7fb85a93aea0
ld-2.27.so!_dl_init 0x7f9612138630
hello!_start 0x400582
lib-2.27.so!__libc_start_main 0x7f9611d58ab0
hello!puts@plt 0x4004f0
hello!exit@plt 0x400530
5.7 Hello的动态链接分析
在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定(lazybinding),将过程地址的绑定推迟到第一次调用该过程时。
延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
通过hello的ELF文件,可以看到GOT和PLT节的起始地址:
图28 起始地址
图29 GOT_before
图30 GOT_after
图31 PLT_before
图32 PLT_after
5.8 本章小结
本章通过分析ELF文件和edb调试以及各种比较,逐步探寻了链接的过程,分析了链接前后程序的异同,链接是将Program转变成Process对文件进行的最后一步操作。链接就像是一个管家,将所有零碎的文件统一起来,汇聚到一个项目中。链接之后,我们最终得到了可以运行的可执行目标文件。
第6章 hello进程管理
6.1 进程的概念与作用
狭义定义:进程就是一段程序的执行过程。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
进程提供给用户一种假象:就好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。
6.2 简述壳Shell-bash的作用与处理流程
shell就是一种壳程序,避免用户直接与OS内核沟通
shell的作用是将我们的指令翻译给OS内核,让内核来进行处理,并把处理的结果反馈给用户。(Windows下的壳程序就是图形化界面)
shell的存在使得用户不会直接操作OS,保证了OS的安全性。
简单来说,shell就是Linux下的命令行解释器,我们写指令的地方。
1.Shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符 号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示:
SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |
2.程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。
3.当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令 的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程 回到第一步重新分割程序块tokens。
4.Shell对~符号进行替换。
5.Shell对所有前面带有$符号的变量进行替换。
6.Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用$(command) 标记法。
7.Shell计算采用$(expression)标记的算术表达式。
8.Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割 符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB 和换行符号。
9.Shell执行通配符* ? [ ]的替换。
10.shell把所有从处理的结果中用到的注释删除,并且按照下面的顺序实行命令的检查:
A.内建的命令
B. shell函数(由用户自己定义的)
C.可执行的脚本文件(需要寻找文件和PATH路径)
11.在执行前的最后一步是初始化所有的输入输出重定向。
12.执行命令。
6.3 Hello的fork进程创建过程
父进程通过fork函数创建一个新的运行的子进程;子进程中,fork返回0,父进程中,返回子进程的PID;
在Linux系统中,fork()函数用于创建一个新的进程。创建的新进程是原来进程的子进程,会将当前进程的内存内容完整地复制到内存的另一个区域,包括进程代码、数据、堆栈以及打开的文件描述符等。父子进程之间是独立的,各自拥有一份代码。运行hello程序时,在shell中输入./hello,此时OS就会fork创建一个子进程来运行这一程序。
6.4 Hello的execve过程
execve函数是一个非常重要且常用的函数。它的作用是加载并执行一个新的可执行文件,将当前进程替换为新的程序。
当调用execve函数时,操作系统首先根据filename指定的路径和名称找到对应的可执行文件。然后,操作系统创建一个新的进程,并将该可执行文件加载到新进程的内存空间中。接下来,操作系统将新进程的参数和环境变量设置为argv和envp指定的内容。最后,操作系统启动新进程的执行,从新程序的入口点开始执行代码。
如果execve函数成功执行,它将不会返回到调用者,因为整个进程的上下文已经被替换。如果发生错误,execve函数将返回-1,并设置errno变量以指示具体的错误类型。
6.5 Hello的进程执行
进程提供给应用程序两个关键抽象:一个独立的逻辑流控制;一个私有的地址空间。
在计算机系统中,通常有多个程序同时运行,但进程让用户看起来当前计算机是独占地使用处理器。用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一的对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列就是逻辑控制流。
图33 进程关系
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新打开一个先前被抢占的进程。这种决策就叫做调度。
上下文就是内核重新启动一个被抢占的进程所需的状态,内核为每一个进程维持一个上下文。在内核调度了一个新的进程运行后,他就抢占当前进程,并使用上下文切换机制来将控制转移到新的进程。
处理器提供一种限制一个应用可以执行的指令以及它可以访问的地址空间范围的机制,并通过设置一种模式位来提供这种功能。在没有设置模式位时,进程就处于用户模式;设置了模式位,进程就处于内核模式,并允许执行特权指令。
当内核代表用户执行系统调用时,可能会发生上下文切换。如果系统调用因为等待某个时间而阻塞,那么内核可以让当前进程休眠,切换到另一个进程。
图34 进程切换
6.6 hello的异常与信号处理
图35 异常种类
异步异常时由处理器外部的 I/O 设备中的事件产生的。同步异常是执行一条指令的直接产物。
中断时异步发生的,来自处理器外部的I/O设备的信号结果。剩下的异常类型是同步发生的,是执行当前指令的结果,我们把这类指令叫做故障指令。
陷阱最重要的用途是用在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用
从程序员的角度看,系统调用和普通的函数调用是一样的。然而,它们的实现非常不同。普通函数运行在用户模式中,用户模式限制了函数可以执行的指令的类型,而且它们只能访问与调用函数相同的栈。系统调用在内核模式中,内核模式允许系统调用执行特权指令,并访问定义在内核中的栈。
故障由错误引起,它可能能够被故障处理程序修正。根据故障是否能够被修复,故障处理程序要么重新执行引起故障的指令,要么终止。
一个经典的故障示例是缺页异常,当指令引用一个虚拟地址,而与该地址相对应的物理页面不在内存中,因此必须从磁盘中读取时,就会发生故障。一个页面就是虚拟内存中的一个连续的块(典型的是4KB),缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令。当指令再次执行是,相应的物理页面已经驻留在内存中了,指令就可以没有故障的运行完成了。
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者 SRAM 位被损坏时发生的奇偶错误。终止处理程序从不将控制返回给应用程序,处理程序将控制返回给一个abort例程,该例程会终止这个应用程序。
图36 常见异常处理信号
正常运行结果:
图37 正常输入
乱按:
图38 乱按
不影响当前进程执行;
Ctrl + Z(发出SIGTSTP信号):
图39 Ctrl + Z
进程直接停止。
Ctrl + C(发出SIGINT信号):进程中断。
图40 Ctrl + C
Ctrl - z后运行ps:
图41 Ctrl + z ps
Ctrl-z后运行jobs:
图42 Ctrl + z jobs
Ctrl-z后运行pstree:
图43 Ctrl + z pstree
Ctrl-z后运行fg:
图44 Ctrl + z fg
Ctrl-z后运行kill:
图45 Ctrl + z kill
6.7本章小结
本章介绍了进程的概念和作用,进程被誉为计算机领域最伟大、最成功的概念之一,通过进程,计算机能够实现同时运行多个程序,还观察了hello进程的创建,执行,终止以及各个命令的执行,如进程树,ps等。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1.物理地址:是指在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果,是内存储器中的实际有效地址,也叫作绝对地址,从0开始顺序编排,直到其支持的最大存储单元。处理器使用物理地址访问主存储器芯片。
2.逻辑地址:是在有地址变换功能的计算机中,访内指令给出的地址(操作数)叫逻辑地址,也叫相对地址,也就是机器语言指令中,用来指定一个操作数或是一条指令的地址。要经过寻址方式的计算和变换才得到物理地址。一个逻辑地址由两部份组成,段标识符:段内偏移量。同时处理器内部以及程序员编程时采用的地址为逻辑地址。
3.虚拟地址:是由程序产生的由段选择符和段内偏移地址组成的地址。这两部分组成的地址并没有直接访问物理内存,而是要通过分段地址的变换处理后才会对应到相应的物理内存地址。操作系统利用存储管理单元MMU将逻辑地址映射为线性地址(虚拟地址)。hello的反汇编文件中的0x401000就是一种虚拟地址。
4.线性地址:是逻辑地址到物理地址变换之间的中间层。(在分段部件中基地址加上段中的偏移地址就是逻辑地址)
例如,当hello程序调用printf函数时,虚拟地址通过页表转换为物理地址,CPU最终访问这个物理地址来执行函数。
7.2 Intel逻辑地址到线性地址的变换-段式管理
1、逻辑地址=段选择符+偏移量;
2、每个段选择符大小为16位,段描述符为8字节(注意单位);
3、GDT为全局描述符表,LDT为局部描述符表;
4、段描述符存放在描述符表中,也就是GDT或LDT中;
5、段首地址存放在段描述符中;
1)使用段选择符中的偏移值(段索引)在GDT或LDT表中定位相应的段描述符.(仅当一个新的段选择符加载到段寄存器中是才需要这一步)
2)利用段选择符检验段的访问权限和范围,以确保该段可访问。
3)把段描述符中取到的段基地址加到偏移量(也就是上述汇编语言汇中直接出现的操作地址)上,最后形成一个线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟地址(也就是线性地址)由虚拟页号VPN与虚拟页偏移量VPO组成;物理地址由物理页号PPN和物理页偏移量PPO组成。
虚拟内存被分割为成为虚拟页的大小固定的块来解决虚拟内存的存储问题。页式管理将虚拟地址与内存地址建立一一对应的页表。
PTBR指向当前页表。MMU利用VPN 来选择适当的PTE。例如VPN 0选择PTE 0。将页表条目中物理页号PPN与虚拟地址的VPO串联起来,就得到相应的物理地址。其中由于虚拟地址与物理地址的偏移量大小相同,所以PPO和VPO是相同的。
7.4 TLB与四级页表支持下的VA到PA的变换
Core i7采用四级页表层次结构,以Core i7的地址翻译为例:
36的VPN被划分成四个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含L1页表的物理地址。VPN1提供到一个L1 PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2 PTE的偏移量,以此类推。最后得到PPN,而PPO与VPO 仍相同。
7.5 三级Cache支持下的物理内存访问
由虚拟地址翻译得到物理地址之后,将物理地址分为缓存偏移CO、缓存组索引CI以及缓存标记CT。
首先,利用组索引CI来寻找我们的地址是否在Cache中有对应的组;然后利用标记CT来判断我们的内容是否在Cache中。若命中,则访问我们的物理地址;若不命中,则进行下一层Cache的索引、访问,以此类推。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要一下几个步骤:
- 删除已存在的用户区域;
- 映射私有区域:为新程序hello的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的;
- 映射共享区域:如果hello程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内;
- 设置程序计数器(PC):execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
操作系统启动后,在内存中维护着一个虚拟地址表,进程需要的虚拟地址在虚拟地址表中记录。一个程序被加载运行时,只是加载了很少的一部分到内存,另外一部分在需要时再从磁盘载入。被加载到内存的部分标识为“驻留”,而未被加载到内存的部分标为“未驻留”。操作系统根据需要读取虚拟地址表,如果读到虚拟地址表中记录的地址被标为“未驻留”,表示这部分地址记录的程序代码未被加载到内存,需要从磁盘读入,则这种情况就表示"缺页"。这个时候,操作系统触发一个“缺页”的硬件陷阱,系统从磁盘换入这部分未“驻留”的代码。
引入了分页机制(也就有了缺页机制),则系统只需要加载程序的部分代码到内存,就可以创建进程运行,需要程序的另一部分时再从磁盘载入并运行,从而允许比内存大很多的程序同时在内存运行。
7.9动态存储分配管理
动态内存管理也叫动态内存开辟。指在程序运行时,根据需要动态地分配和释放内存空间的过程。它允许程序在运行时根据实际情况来动态地请求分配内存,以满足不同大小和数量的数据存储需求。
动态内存管理设计操作:
1) 内存分配:当程序需要更多的内存空间来存储数据时,它可以通过动态内存分配来请求一块合适大小的内存空间。malloc、calloc...
2) 内存释放:当不再需要某个内存空间时,程序可以通过动态内存释放将该空间归还给系统,以便其他部分可以使用。Free
常见的动态内存管理方法:
1 ) 堆分配:在堆中分配内存空间,可以使用诸如malloc、calloc、realloc等函数进行分配,并使用free函数进行释放。
2 ) 操作系统提供的动态内存管理:操作系统也提供了一些机制来管理进程的动态内存,例如Unix/Linux系统中的brk和sbrk系统调用,以及Windows系统中的HeapAlloc和HeapFree函数。
策略:显式空闲链表 隐式空闲链表。
7.10本章小结
本章主要介绍了hello程序的存储管理,辨析了逻辑地址、线性地址、虚拟地址和物理地址的关系;查看了存储器从逻辑地址到线性地址到物理地址的变换;更深层次的理解了页表、Cache、内存映射的概念,对fork、execve有了新的理解视角;又介绍了动态内存管理的基本方法和策略。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件 一个Linux文件就是一个m个字节的序列
设备管理:unix io接口所有的I/O设备(例如网络、磁盘和中断)都被模型化为文件,而所有的输入和输出都被当作相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。
8.2 简述Unix IO接口及其函数
Unix I/O接口是Unix系统中的核心部分,包括了文件读写、网络通讯等功能。常见的I/O系统调用包括:open、read、write、connect等。此外,UIO允许用户空间程序直接访问设备资源,减少了内核和用户空间之间的数据拷贝和上下文切换次数,提高了性能。
Open 打开一个已存在的文件或者创建一个新文件的 int open(char *filename, int flags, mode_t mode)
Close 关闭一个打开的文件 int close(int fd)
read和write 执行输入和输出的 ssize_t read(int fd, void *buf, size_t n)
lseek 应用程序能都显示地修改当前文件的位置
8.3 printf的实现分析
windows下的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;
}
va_list是一个字符指针,((char*)(&fmt) + 4) 表示的是...中的第一个参数,然后printf函数调用了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函数返回的是一个长度 ,是要打印出来的字符串的长度。vsprintf的作用就是格式化。它接收确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化的输出。
接下来,我们来看wirte函数。write函数的功能就是执行一个写操作。以我们学过的知识可知,写操作是计算机的底层操作,是对计算机硬件进行的操作。所以我们跟踪write,得到:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
之后,我们来看INT_VECTOR_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
sys_call实现很麻烦,我们不妨去观察我们想要的事情:sys_call只有实现一个功能:显示格式化了的字符串。
8.4 getchar的实现分析
getchar是stdio.h中的库函数,它的作用是从stdin流中读入一个字符,也就是说,如果stdin有数据的话不用输入它就可以直接读取了,第一次getchar时,确实需要人工的输入,但是如果你输了多个字符,以后的getchar再执行时就会直接从缓冲区中读取了。
实际上是,我们键入的字符首先存到缓冲区,然后在由getchar函数读取,知道用户按下Enter回车键。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章简述IO设备管理方法及Unix I/O函数,对Linux环境下函数的执行进行了详细叙述,包括打开文件、关闭文件、读写文件、修改文件位置。而后我们又分析了printf和getchar函数的实现,对I/O设备的管理有进一步的理解。
结论
预处理:
通过预处理器,将hello.c中include的外部的头文件头文件插入程序文本中,完成字符串的替换,生成hello.i。
编译
通过编译器,对词法分析和语法进行分析,将合法指令翻译成等价汇编代码,将hello.i 翻译成汇编语言文件 hello.s。
汇编
通过将汇编器,将文本翻译成机器语言指令,把指令打包成可重定位目标程序格式,生成hello.o 文件。
链接
通过链接器,将hello.o的程序编码与动态链接库等收集整理成为一个单一文件,生成完全链接的可执行的目标文件hello。
加载运行
打开Shell,在其中键入 ./hello,终端使用fork函数创建进程,使用execve函数进行代码和数据的加载。
访存
内存管理单元MMU将逻辑地址,一步步映射成物理地址,通过多级缓存来访问物理内存、磁盘中的数据。
信号处理
进程无时无刻不在等待着信号,针对各种信号有不同的应对策略。
回收
shell父进程等待并回收hello子进程,内核将为hello进程消耗的资源释放。
计算机系统的设计需要合理协调系统各个部分,既要保证每个模块的正确运行,又要尽量的提高每个模块的执行速度,以及互相合作的效率。计算机系统是软件与硬件的统一,我们对计算机系统设计和实现的理解,既不能抛开硬件,也不能不顾软件层面。
附件
hello.c 源程序
hello.i 预处理后的修改的C程序
hello.s 汇编程序
hello.o 可重定位目标文件
hello 可执行目标文件
Objdump_hello.o hello.o的反汇编文件
Objdump_hello hello的反汇编文件
elf_hello hello的ELF格式
elf_hello.o hello.o的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.