计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号
班 级
学 生
指 导 教 师 刘宏伟
计算机科学与技术学院
2021年5月
Hello World程序几乎是每个程序员第一次接触编程学习到的内容。hello.c程序历经预处理、编译、汇编、链接四个环节,最终生成可执行文件hello。而后,shell为hello创建子进程,可执行文件被加载到内存中执行。在hello执行的过程中,硬件和操作系统分工协同,共同完成程序的执行。
本文通过对hello程序执行的各个环节的探究,展示了计算机系统的各个组成部分是如何分工合作,共同完成hello的P2P过程。
关键词:计算机系统;P2P;预处理;编译;进程
目 录
第1章 概述
1.1 Hello简介
在Unix系统中,GCC驱动程序读取源文件hello.c,经过预处理,编译,汇编和链接四个阶段,最终生成一个可执行目标文件hello,并保存在磁盘上。
为了执行hello,操作系统为hello分配了虚拟内存,shell为hello创建了一个新的子进程,并在这个进程中调用execve函数,加载器将hello从磁盘中加载到内存,并将PC值设置为程序的入口。
然后,CPU从内存中读取指令并执行,操作系统调度程序进行上下文切换。四级页表和三级cache分工协作,共同完成地址的翻译和数据的读取。
程序执行完毕后,由父进程shell将子进程回收,hello的生命周期结束了。
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;2GHz;2G RAM;256GHD Disk 以上
Windows10 64位以上;VirtualBox 11以上;Ubuntu 16.04 LTS 64位
1.2.2 软件环境
Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc
1.2.3 开发工具
Ubuntu,gcc,edb
1.3 中间结果
中间文件 | 作用 |
hello.i | 经过预处理的源程序,用于编译器的翻译 |
hello.s | 汇编程序,用于汇编 |
hello.o | 可重定位目标文件,用于链接 |
hello | 可执行目标文件,可以加载到内存中执行 |
elf.txt | hello.o对应的elf文本文件 |
dump1.txt | hello.o 的反汇编文本文件 |
elf2.txt | hello对应的elf文本文件 |
dump2.txt | hello 的反汇编文本文件 |
1.4 本章小结
本章介绍了hello的P2P流程,给出了开发中用到的工具,同时介绍了中间文件的名称和作用。
第2章 预处理
2.1 预处理的概念与作用
2.1.1 预处理的概念
在hello程序中,预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如 hello . c中第1行的 #include<stdio. h> 命令告诉预处理器读取系统头文件stdio . h 的内容,并把它直接插入程序文本中。结果就得到了另一个 C 程序。预处理后的程序通常是以.i作为文件扩展名。
2.2.2 预处理的作用
预处理器(cpp)将头文件的内容插入到程序文本中,方便编译器进行下一步的编译。
2.2在Ubuntu下预处理的命令
Linux系统中对hello.c进行预处理的命令是gcc -E hello.c -o hello.i,执行过程如下图所示:
图2-1:linux下预处理命令执行过程
2.3 Hello的预处理结果解析
图2-2:hello.i文件部分截图
对比hello.c和hello.i的文件大小,可以看到,文件从1kb增长到了64kb。在hello.i中,源代码被放到了文件的最后,文件前面插入了系统头文件stdio.h unistd.h和stdlib.h的源代码,说明头文件的内容被展开并插入到程序中。
2.4 本章小结
本章介绍了预处理的概念和作用,并对预处理生成的hello.i文件进行初步分析。
第3章 编译
3.1 编译的概念与作用
3.1.1 编译的概念
编译是指编译器将文本文件hello.i翻译成文本文件hello.s,即将预处理后的文件转换成汇编语言程序
3.3.2 编译的作用
编译将高级语言指令翻译为统一的低级机器语言指令,便于机器处理。在生成汇编语言的过程中,同时对代码进行优化,发现和报告错误。
3.2 在Ubuntu下编译的命令
Linux下编译的命令为gcc -s hello.i -o hello.s
图3-1:编译过程展示
3.3 Hello的编译结果解析
下面将分类讨论编译器如何处理各种数据类型和操作:
3.3.1 对数据的处理
编译器将局部变量保存在寄存器或栈中,并用栈指针+偏移量的形式进行所以进行索引。hello.c程序有两个局部变量,分别是参数个数argc和循环索引i,分别保存在相对栈指针%rsp偏移量为20和4的内存空间中。
在程序中,指针就是一个地址值,同样保存在栈或者寄存器中。
图3-2 对数据的处理
同时,编译器将常量表示为立即数的形式,无全局变量和静态变量
3.3.2 对赋值运算的处理
一种常见的赋值操作是使用mov指令,如图3-2所示
图3-2 hello.s中的mov指令
3.3.3 对类型转换的处理
源代码中有显式类型转换atoi函数将字符串转换为常量,通过调用函数实现
图3-3 atoi函数调用
3.3.4 对算术操作的处理
使用算术操作指令进行算数操作
图3-4 add指令
3.3.5 对关系操作和控制转移的处理
编译器通常使用cmp指令和条件跳转共同完成控制转移,例如原程序中的for(i=0;i<8;i++)语句被转换为下面的指令
图3-5 cmp指令+条件跳转
3.3.6 对函数的处理
编译器使用call指令调用函数,call后接函数名称。
函数调用参数传递规则:X86-64中,过程调用传递参数规则为第1~6个参数依次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9寄存器中,剩下的参数通过栈传递。
图3-6 函数调用
3.4 本章小结
本章介绍了编译的概念和作用,并分析了hello.s文件,可以看到,源文件中的不同代码段被翻译为了不同的汇编指令。
第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
驱动程序运行汇编器as,将汇编语言文件hello.s翻译成可重定位目标文件hello.o的过程
4.1.2 汇编的作用
将文本文件翻译为机器可读懂的二进制文件,以便于执行链接
4.2 在Ubuntu下汇编的命令
Linux下的汇编指令:as hello.s -o hello.o
图4-1 汇编过程展示
4.3 可重定位目标elf格式
下面分析hello.o的ELF格式,并用readelf等列出其各节的基本信息。
readelf指令:readelf -a hello.o > ./elf.txt
4.3.1 ELF头
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
图4-2 hello.o的ELF头
4.3.2 节头部表
节头部表描述不同节的位置和大小信息,其中目标文件的每个节都有一个固定大小的条目。如图所示,hello.o共有14个条目,代表了14个不同的节。
图4-3 hello.o的节头部表
4.3.3 符号表
.symtab节中包含的符号表,符号表存放在程序中定义和引用的全局变量的信息。如图4-4所示,在符号表中可以看到函数定义或引用的sleep,getchar等符号
图4-4 hello.o的符号表
4.3.4 .rela.text节
.rela.text包含代码的重定位条目。offset 是需要被修改的引用的节偏移。symbol 标识被修改引用应该指向的符号; type告知链接器如何修改新的引用。addend 是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。
图4-4 hello.o的.rela.text节
4.4 Hello.o的结果解析
使用objdump -d -r hello.o指令,得到hello.o的反汇编
图4-5 hello.o反汇编过程
将hello.o的反汇编与hello.s进行对照分析,可以看出其明显差异如下:
4.4.1 操作数的差异
汇编语言使用10进制操作数,而机器语言使用十六进制操作数
4.4.1 分支转移的差异
hello.s使用代码的标号进行直接跳转,如图4-6所示,控制直接跳转到.L2处
图4-6 hello.s的跳转指令
而hello.o的反汇编中使用相对地址进行跳转,如图所示
图4-7 hello.o的反汇编中的跳转
4.4.3 函数调用的差异
hello.s中使用call+函数名的方式调用函数,如图4-8所示
图4-8 hello.s的函数调用
而hello.o的反汇编中使用call+函数地址调用函数(由于此时尚未进行重定位,所以对函数的调用采用了重定位条目的形式)
图4-8 hello.o的反汇编中的跳转
4.5 本章小结
本章介绍了将文本文件进行汇编生成可执行目标文件的过程。同时对生成的ELF文件进行了分析,可以看到,ELF文件由ELF头,节头部表以及夹在二者之间的若干节构成。此外还将汇编后的文件与汇编前的文件进行对比,并观察到二者在分支转移和函数调用等方面差异显著。
第5章 链接
5.1 链接的概念与作用
5.1.1 链接的概念
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载到内存执行。
5.1.2 链接的作用
链接将独立的可重定位目标模块组织成统一的可执行目标文件。在软件开发中扮演重要的角色,它使得分离编译成为可能。我们不用将一个大型的应用程序组织称为一个巨大的源文件,而是可以把它们分解成更好管理的模块。
5.2 在Ubuntu下链接的命令
5.2.1 Linux下的链接命令
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.2.2 链接过程
如下图所示:
图5-1 链接过程展示
5.3 可执行目标文件hello的格式
5.3.1 ELF头
图5-2 hello文件的ELF头
5.3.2 节头部表
从节头部表可以得到每个节的位置和大小信息,比如.text节的开始位置是0x4010f0,大小为145个字节,如图5-3所示
图5-3 hello文件节头部表
5.3.3 程序头部表
程序头部表描述了可执行文件到内存的映射关系,如图5-4所示
图5-4 hello文件程序头部表
5.4 hello的虚拟地址空间
5.4.1 虚拟地址空间概述
图5-5展示了一个x86-64 Linux进程的虚拟地址空间的组织结构。地址空间底部是保留给用户程序的,包括通常的代码、数据、堆和栈段。代码段总是从地址0x400000开始。地址空间顶部保留给内核(操作系统常驻内存的部分)。地址空的这个部分包含内核在代表进程执行指令时(比如当应用程序执行系统调用时)使用的码、数据和栈。
图5-5 一个标准linux进程的虚拟内存结构
5.4.1 栈
使用edb查看hello程序,可以看到虚拟空间的栈向下增长,栈指针位于地址00007ffdbdec6030
图5-5 edb中展示的栈结构
5.4.2 数据和代码段
图5-5 edb中展示的数据和代码段
数据和代码段从地址0x400000开始。查看可执行文件的节头部表可知,程序的入口点.init节开始于地址0x401000处,这与上图显示的初始地址相吻合。
5.5 链接的重定位过程分析
5.5.1 hello和hello.o的差异
新增节:hello相比于hello.o,新增了init,plt等节,atoi,sleep等函数的反汇编代码也包含在节中,如图5-6所示
图5-6 hello中新增的节
地址:hello中使用虚拟内存地址,如图5-7所示
图5-7 hello中的虚拟地址
符号引用 :在hello.o函数中,由于没有进行重定位,所以使用诸如“R_X86_64_PC32 .rodata+0x22”的重定位条目显示在引用的后面一行上;而在hello中,重定位条目被替换为真实的虚拟内存寻址。
图5-8 hello中call指令后接虚拟地址
5.5.2 对hello.o的重定位过程分析
链接器使用一种叫做重定位算法的方法来解析hello.o中的引用。在hello.o文件中,汇编器为每个引用产生一个重定位条目,显示在引用的后面一行上。这些重定位条目告诉链接器对该符号的引用要使用哪种具体算法。
图5-9 重定位算法展示
5.6 hello的执行流程
5.6.1 程序执行的全过程
使用edb执行hello,程序初始位置位于0x7f632c643100。
1. 从加载到进入main函数的过程
开始时,经过一系列执行,程序首先跳转到子程序hello!_start,该子程序位于地址0x4010f0处。随后通过callq *0x2ed2(%rip) 指令跳转到位于地址0x7f38faffefc0的“Libc-2.31.so!_libc_start_main”子程序,在子程序中,通过call *%rax指令跳转到main函数,地址为0x401125。
2. 从main函数到程序执行完毕
进入main函数,程序按照源代码顺序依次执行,在执行的过程中分别调用不同的子程序,子程序名称和地址见下表
5.6.2 各子程序展示
子程序名称 | 地址 |
Hello!start | 0x4010f0 |
libc-2.31.so!_libc_start_main | 0x7f38faffefc0 |
Hello!main | 0x401125 |
<puts@plt> | 0x401090 |
<printf@plt> | 0x4010a0 |
<getchar@plt> | 0x4010b0 |
<atoi@plt> | Ox4010c0 |
<exit@plt> | 0x4010d0 |
<sleep@plt> | 0x4010e0 |
5.7 Hello的动态链接分析
下面展示了动态链接前后的变化:
首先在elf文件中找到:
动态链接前:
动态链接后:
5.8 本章小结
本章介绍了链接的概念和作用,同时分析了hello程序的虚拟地址空间、重定位过程、执行流程、动态链接过程。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1进程的概念
进程的经典定义就是一个执行中程序的实例,是操作系统对一个正在运行程序的抽象。
6.1.2 进程的作用
操作系统通过进程提供一种假象,就好像系统上只有这个程序在运行。程序看上去是独占地使用处理器、主存和 I / O 设备。处理器看上去就像在不间断地一条接一条地执行程序中的指令,即该程序的代码和数据是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell是一个交互性的应用级程序,代表用户运行其他程序
6.2.2 处理流程
shell读取用户输入的命令行,解析该命令行并代表用户运行程序,包括使用fork函数创建一个新的进程,并在新的进程上下文中运行程序。
6.3 Hello的fork进程创建过程
在命令行输入“./hello 7203610410 xukun 1”后,shell解析命令行,并发现该命令行的第一个单词不是shell的内置命令(在这里是./hello),那么shell就会假设这是一个可执行文件的名字,然后加载并运行这个文件。Shell会根据输入的命令行的剩余部分构造参数列表,并构造最终会传递给execve的argv向量。
shell使用fork创建一个新的子进程,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用 fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
6.4 Hello的execve过程
在新创建的子进程中,shell调用execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。参数列表在shell解析的过程中创建, argv变量指向一个以null结尾的指针数组,其中每个指针都指向一个参数字符串。按照惯例,argv [0]是可执行目标文件的名字。环境变量的列表是由一个类似的数据结构表示的,envp变量指向一个以null结尾的指针数组,其中每个指针指向一个环境变量字符串,每个串都是形如name = value的名字-值对。
execve函数代码如下:
图6-1 execve函数代码
6.5 Hello的进程执行
6.5.1 基本概念
上下文:内核为每个进程维持一个上下文,上下文就是内核重新启动一个被抢占的进程所需的状态。
调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。
上下文切换:在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片
6.5.2 hello的进程执行
本例中有两个并发的进程: shell进程和 hello 进程。最开始,shell进程在运行,即等待命令行上的输入。当我们让它运行hello程序时,shell通过系统调用,来执行我们的请求,系统调用会将控制权传递给操作系统。操作系统保存shell进程的上下文,创建一个新的hello进程及其上下文,然后将控制权传给新的hello进程。在hello进程的执行过程中,在某些时刻,hello程序调用sleep函数显式请求进程休眠,此时内核执行上下文切换,从用户模式转换到内核模式,抢占hello进程。随后,在其他进程执行一段时间后,内核做出决定将控制返回给hello进程继续执行。如此反复直到hello进程终止后,操作系统恢复shell进程的上下文,并将控制权传给它,shell进程会继续等待下一个命令行输入。
6.6 hello的异常与信号处理
hello执行过程中会出现异步中断异常和系统调用,中断即来自外部I/O设备的信号打断进程。同时会产生诸如SIGINT,SIGQUIT,SIGTSTP等信号。
6.6.1 执行过程中键入Ctrl-z
当在键盘中敲入Ctrl-z时,内核会向前台进程组的每个进程发送SIGTSTP信号,hello进程捕获信号并停止,如下图所示。
图6-2 执行过程中键入Ctrl-z
6.6.2 停止后输入ps/jobs/pstree等命令
ps/jobs/pstree等命令是shell的内置命令,shell执行并实现相应指令的功能。
图6-3 停止后输入jobs命令,列出所有的jobs
图6-4 停止后输入pstree命令
图6-5 停止后输入ps命令
6.6.3 停止后输入fg命令
fg指令是shell的内置命令,当在键盘中敲入fg时,内核会向前台进程组的每个进程发送SIGCONT信号,此时被停止的hello进程被唤醒并继续运行
图6-5 停止后输入fg命令
6.6.4 停止后输入kill命令
kill是shell的内置命令,当在键盘中敲入kill时,会显示地从从进程向目标进程发送指定的信号,如图所示,是向hello所在进程发送SIGKILL信号杀死该进程。
图6-6 停止后输入kill命令
6.6.5 执行过程中键入Ctrl-c
当在键盘中敲入Ctrl-c时,内核会向前台进程组的每个进程发送SIGINT信号,hello进程捕获信号并终止,等待回收。
图6-7 执行过程中键入Ctrl-c
6.6.5 执行过程中不停乱按和键入回车
乱按的字符会保存在缓冲区内,等待getchar()函数读取
图6-8 执行过程中不停乱按
图6-8 执行过程中键入回车
6.7本章小结
本章从进程和上下文的角度分析了hello的执行过程,以及在执行过程中程序对异常的处理流程。
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1 逻辑地址
逻辑地址是用户编程时使用的地址,分为段地址和偏移地址两部分。在hello程序中,指的是hello.o中的地址。
7.1.2 线性地址
线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。
7.1.3 物理地址
物理地址是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址,这里指的是hello保存在主存上的实际地址。
7.1.4 虚拟地址
虚拟地址指的是程序在虚拟内存空间的地址,这里就是hello的虚拟地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
linux是通过分段机制,将逻辑地址转化为线性地址。通过数据段寄存器ds,可以找到此进程数据段所在的段描述符,再通过段描述符找到相应的线性地址。
段描述符是描述段的属性的数据结构,长度一般为8字节。
给定一个完整的逻辑地址段选择符+段内偏移地址,看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,就得到了其基地址。从逻辑地址到线性地址的全部流程如下图所示:
图7-1 从逻辑地址到线性地址的全部流程
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址怎么对应到物理地址是通过分页机制来完成的,具体的说,就是通过页表查找来对应物理地址。
分页的基本原理是把线性地址分成固定长度的单元,称为页(page)。页内部连续的线性地址映射到连续的物理地址中。X86每页为4KB。为了能转换成物理地址,我们需要给CPU提供当前任务的线性地址转物理地址的查找表,即页表(page table),页表存放在内存中。
在保护模式下,控制寄存器CR0的最高位PG位控制着分页管理机制是否生效,如果PG=1,分页机制生效,需通过页表查找才能把线性地址转换物理地址。如果PG=0,则分页机制无效,线性地址就直接作为物理地址。
为了实现每个任务的平坦的虚拟内存和相互隔离,每个任务都有自己的页目录表和页表。
7.4 TLB与四级页表支持下的VA到PA的变换
core i7支持48位虚拟地址空间和52位物理地址空间,采用四级页表层次结构。如图所示,展示了VA到PA的变换。CPU首先生成一个虚拟地址,并将其拆分为36位的VPN部分和12位的VPO部分。
首先查找TLB,MMU从虚拟地址中抽取VPN部分,并将其解读为标记(TLBT)和索引(TLBL)两部分,用于对TLB进行组选择和行匹配。然后检查TLB,若命中,则直接将缓存的PPN返回,与VPO连接起来,这就形成了物理地址,完成了VA到PA的变换。
若TLB不命中,那么MMU必须从页表中的PTE取出PPN。此时,36位VPN被划分成四个9位的片,每个片被用作到一个页表的偏移量。比如前9位就被用作到一级页表的偏移量,以此类推。前3层页表中,每个页表的PTE都指向下一个页表的基址。第四层页表中,每个PTE包含某个物理页面的PPN。分层顺序查找页表,将得到的PPN与VPO连接起来,形成物理地址,完成了VA到PA的变换。
图7-2 TLB与四级页表支持下的VA到PA的变换
7.5 三级Cache支持下的物理内存访问
当MMU生成物理地址后,将获得的物理地址拆分为CT(标记位)CI(组索引),CO(块偏移)。
组索引位是一个到组的索引。第一个组是组0,第二个组是组1,依此类推。组索引位被解释为一个无符号整数、它告诉我们这个字必须存储在哪个组中。一旦我们知道了这个字必须放在哪个组中,A中的个标记位就告诉我们这个组中的哪一行包含这个字(如果有的话)。当且仅当设置了有效位并且该行的标记位与地址 A 中的様记位相匹配时,组中的这一行才包含这个字。一旦我们在由组索引标识的组中定位了由标号所标识的行,那么块偏移位给出了在B个字节的数据块中的字偏移。
整个流程分为三步:组选择,行匹配,字抽取。
图7-3 Intel core i7 高速缓存层次结构
图7-4 三级Cache支持下的物理内存访问
7.6 hello进程fork时的内存映射
7.6.1 私有对象的内存映射
私有对象使用一种叫做写时复制的巧妙技术被映射到虚拟内存中。对于每个映射私有对象的进程,相应私有区域的页表条目都被标记位只读,并且区域结构被标记位私有的写时复制。
7.6.2 hello进程fork时的内存映射
当shell调用fork函数为hello创建新的进程时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虛拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
当在新的进程中调用execve函数时,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
1. 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2. 映射私有区域。为新程序的代码、数据、 bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data 区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。下图概括了私有区域的不同映射。
3. 映射共享区域。hello程序与共享对象链接,比如标准C库1ibc. So,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4. 设置程序计数器(PC)。 execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。 Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
7.8.1 缺页故障
在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页。
7.8.2 缺页中断处理
处理缺页要求硬件和操作系统内核协作完成。当发生缺页故障时,MMU触发了一次异常,传递CPU中的控制到操作系统中的缺页异常处理程序,处理程序随后执行下面的步骤:
1. 判断虚拟地址A是否合法。
处理程序首先判断A是否在某个区域结构定义的区域内。为了回答这个问题,缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end 做比较。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止这个进程。这个情况在下图中标识为“1”。
因为一个进程可以创建任意数量的新虚拟内存区域,所以顺序搜索区域结构的链表花销可能会很大。因此在实际中,Linux使用某些我们没有显示出来的字段, Linux在链表中构建了一棵树,并在这棵树上进行査找。
2. 判断试图进行的内存访问是否合法
处理程序需要判断进程是否有读、写或者执行这个区域内页面的权限。例如,这个缺页可能是由一条试图对这个代码段里的只读页面进行写操作的存储指令造成的。如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。这种情况在图9-28中标识为2。
3. 最终处理
此刻,内核知道了这个缺页是由于对合法的虚拟地址进行合法的操作造成的。它是这样来处理这个缺页的:选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU 重新启动引起缺页的指令,这条指令将再次发送A到MMU。这次,MMU就能正常地翻译A,而不会再产生缺页中断了。
7.9动态存储分配管理
7.9.1 动态内存管理的基本方法和策略
使用动态内存分配器是一种常见的动态内存管理方法。分配器按照释放方式不同分为显式分配器和隐式分配器。
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)。堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk,它指向堆顶。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
7.9.2 malloc和free函数
在hello程序中,printf中使用malloc函数,程序可以通过调用malloc函数分配块。
7.10本章小结
本章讲解了hello的存储器空间,并给出了 TLB与四级页表支持下的VA到PA的变换和三级Cache支持下的物理内存访问过程的介绍。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
(以下格式自行编排,编辑时删除)
(第8章1分)
结论
逐条总结hello所经历的过程:
1. hello.c经过预处理生成hello.i文件
2. hello.i经过编译生成hello.s汇编文件
3. hello.s经过汇编生成可重定位目标文件hello.o
4. hello.o经过链接生成可执行目标文件hello.out
5. shell进程调用fork函数生成子进程
6. 在子进程内调用execve函数执行hello
7. CPU执行指令,在屏幕上显示结果
8. 操作系统调度进程,进行上下文切换
9. 在TLB、四级页表和三级cache的合作下完成hello程序与内存的交互
在计算机硬件和操作系统分工合作下,完成了hello程序的执行,每一步执行都显示出计算机设计师们的奇思妙想和科学技术的飞速发展。
附件
中间文件 | 作用 |
hello.i | 经过预处理的源程序,用于编译器的翻译 |
hello.s | 汇编程序,用于汇编 |
hello.o | 可重定位目标文件,用于链接 |
hello | 可执行目标文件,可以加载到内存中执行 |
elf.txt | hello.o对应的elf文本文件 |
dump1.txt | hello.o 的反汇编文本文件 |
elf2.txt | hello对应的elf文本文件 |
dump2.txt | hello 的反汇编文本文件 |
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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分)