本文主要阐述hello程序在Linux系统里的生命周期,探讨hello程序从hello.c经过预处理、编译、汇编、链接生成可执行文件的全过程,并结合课本的知识详细阐述计算机系统是如何对hello进行进程管理、存储管理和I/O管理,通过运用一些工具,如gdb、edb、readelf等,观察hello程序从开始到结束的生命历程。通过对hello一生周期的探索,让我们对计算机系统有更深的了解。
关键词:预处理;编译;汇编;链接;进程;存储;虚拟内存;I/O
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:在Linux系统下,hello.c经过预处理(ccp)变为hello.i(修改了的源程序),再经过编译器(ccl)编译,生成汇编程序hello.s,再经过汇编器(as)生成可重定位目标程序hello.o,之后通过连接器(ld)生成可执行目标程序hello。在shell中输入对应启动命令后,shell为其fork一个子进程,由此hello便从程序变为一个进程
020:shell为此子进程execve,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
X64 CPU;2GHz;2G RAM;256GHD Disk 以上
Windows10 64位以上;VirtualBox 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位 以上;
Visual Studio 2022 64位以上;CodeBlocks 64位;gcc; edb; gdb
1.3 中间结果
hello.i | 预处理后的文件 |
hello | 链接之后的可执行目标文件 |
hello.o | 汇编之后可重定位目标文件 |
hello.s | 编译之后的汇编文件 |
hello.elf | hello.o的elf格式 |
hello1.elf | hello的elf格式 |
hello_objdump.s | hello的反汇编代码 |
1.4 本章小结
本章对hello的一生进行了简要的介绍和描述,介绍了P2P的整个过程,介绍了本计算机的硬件环境、软件环境、开发工具,介绍了为编写本论文的中间文件的名称和其作用。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
1.预处理概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。
2.预处理作用:根据源代码中的预处理指令修改源代码,预处理从系统的头文件包中将头文件的源码插入到目标文件中,宏和常量标识符已全部被相应的代码和值替换,最终生成.i文件。
2.2在Ubuntu下预处理的命令
Linux中hello.c文件进行预处理的命令是:gcc -E -o hello.i hello.c
2.3 Hello的预处理结果解析
经过预处理之后,hello.c转化为hello.i文件,打开该文件可以发现,文件的内容显著增加,但仍为可以阅读的C语言程序文本文件。
1.可以观察到hello.c中的注释无法在hello.i中找到,对应预处理去除注释的功能。
2.代码量显著增加是由于插入了由#include指令包含的文件内容
3.预处理的功能还包括宏替换或宏展开,即定义和替换由#define指令定义的符号,但在该程序中并无体现。
4.还包括条件编译,同样无体现。
2.4 本章小结
本章介绍了预处理的概念和作用,学习了在ubuntu中用cpp指令对hello.c文件进行预处理,将其重定向到hello.i中。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译器将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。其以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。 这个过程称为编译,同时也是编译的作用。
3.2 在Ubuntu下编译的命令
编译的命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1数据
(1)常量
①数字常量:立即数直接和指令编码放在一起,放在.text(代码区)中,如hello.s中的0、1、4、7、8、16、24、32
②字符串常量:程序中涉及的字符串常量为:"用法: Hello 学号 姓名 秒数!\n"和"Hello %s %s\n",存储在.rodata中。.LCO中声明的字符串汉字编码格式是UTF-8,一个汉字占三个字节,一个\代表一个字节。
(2)变量——局部变量(hello.s中无全局变量)
局部变量是储存在栈中的某一个位置的或是直接储存在寄存器中的,对于源代码中的每一个局部变量可以进行逐一分析。局部变量共有三个:一个是循环变量i,以及argc和argv
①局部变量i:i是main函数中的局部变量,通过对源程序代码及.s文件的分析可知其存储在栈中地址为-4(%rbp)的空间上,程序运行时才对其进行赋值
②局部变量argc:局部变量argc,标志的是在程序运行的时候输入的变量的个数,存储在%edi中,在程序运行时被放入栈中地址为-20(%rbp)的位置,对于它的操作主要是与4比较之后确定printf("用法: Hello 学号 姓名 秒数!\n")是否执行
③局部变量argv:局部变量argv,是一个保存着输入变量的数组,存储在%rsi中,在程序运行时被放入栈中地址为-32(%rbp)的位置使用
3.3.2 赋值
程序中出现了三次对于变量的赋值:循环变量i,exit(1)的参数和return 0
①循环变量i:保存在栈中地址为-4(%rbp)的位置,初始赋值为0
②exit(1)的参数:该参数用寄存器%edi保存,调用exit之前,赋值为1
③return 0:在退出main函数前,将%eax赋值为0
3.3.3 类型转换
程序中并没有发生隐式的类型转换,但是调用了atoi函数将字符串转换为整型数,对应的汇编如下:先取出argv[3],将其存入%rdi,接着调用atoi,转化结果保存在%rax中
3.3.4 算数操作
①+:对于局部变量i,存储在栈中地址为-4(%rbp)的位置,由于其是循环变量,因此在每一轮的循环中都要修改这个值,每次加1:
②+:在获取argv数组里的值时,利用add指令计算对应元素的地址,以获取argv[2]为例,初始时,%rax存储的是argv[0]的地址,已知地址在内存中占用8个字节,若欲获取argv[2]的地址,则需要 argv[0]的地址+16
③-:在初始化栈时,程序将栈顶指针%rsp-32,腾出32个字节的空间用以存储新的局部变量的值
3.3.5 关系操作&控制转移
程序中一共出现了两处关系操作
①对于argc的判断:当argc=4的时候将进行条件跳转至L2,略去部分代码的执行
②对于循环变量i的判断:当i≤7的时候将进行条件跳转,重复循环中的代码
3.3.6 数组
程序中只有一个数组argv,其中-32(%rbp)这个地址里存储的是argv[0]的地址,而argv[n](1≤n≤3)的地址 = argv[0]的地址+8n ,在汇编代码中,依次取出的是argv[2],argv[1]和argv[3]
3.3.7函数调用
在这一段代码中出现了几个函数调用的情况,首先明确在X86系统中函数参数储存的规则,第1~6个参数依次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,其余的参数保存在栈中的某些位置
①main函数:
-参数:传入参数argc和argv,其中argc储存在%edi,argv首地址储存在%rsi
-返回:在源代码中最后的返回语句是return 0,因此在汇编代码中最后是将%eax设置为0并返回这一寄存器。汇编代码如下:
②puts函数:
-参数:传入参数%rdi中保存待输出的字符串的地址,.LC0(%rip)中存储的就是字符串 "用法: Hello 学号 姓名 秒数!\n" 的存储地址
③exit函数:
-参数:传入的参数为%edi,被赋值为1,执行退出命令
④sleep函数:
-参数:传入参数%edi,保存程序执行挂起的时间间隔
⑤printf函数:
-参数:传入参数%rdi中保存待输出的字符串的地址,.LC1(%rip)中存储的就是字符串 "Hello %s %s\n" 的存储地址
⑥atoi函数:
-参数:传入参数%rdi中保存待转化的字符串地址
-返回:返回值是转化的整型数结果,保存在%rax中
3.4 本章小结
本章介绍了编译的概念以及过程。通过hello函数分析了c语言如何转换成为汇编代码。介绍了汇编代码如何实现变量、常量、传递参数以及分支和循环。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程,将.s汇编程序翻译车工机器语言并将这些指令打包成可重定目标程序的格式存放在.o中。
作用:将汇编代码转换为机器指令,使其在链接后能被机器识别并执行.
4.2 在Ubuntu下汇编的命令
gcc -c -o hello.o hello.s
4.3 可重定位目标elf格式
4.3.1 readelf命令:
在linux下生成.elf格式文件的命令为:readelf -a hello.o > hello.elf,如下图所示:
4.3.2 ELF头:
包含了系统信息,编码方式,ELF头大小,节的大小和数量等等一系列信息。Elf头内容如下:
4.3.3 节头目表:
描述了.o文件中出现的各个节的类型、位置、所占空间大小等信息。
4.3.4 重定位节:
各个段引用的外部符号等在链接时需要通过重定位对这些位置的地址进行修改。链接器会通过重定位节的重定位条目计算出正确的地址。
hello.o需重定位:.rodata中的模式串,puts,exit,printf,slepsecs,sleep,getchar等符号。
4.3.5符号表:
.symtab存放在程序中定义和引用的函数和全局变量的信息。
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o
反汇编代码如下:
hello.s如下图:
通过反汇编的代码和hello.s进行比较,发现汇编语言的指令并没有什么不同的地方,只是反汇编代码所显示的不仅仅是汇编代码,还有机器代码,机器语言程序的是二进制机器指令的集合,是纯粹的二进制数据表示的语言,是电脑可以真正识别的语言。
- 数的表示:hello.s中的操作数表现为十进制,而hello.o反汇编代码中的操作数为十六进制。说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
- 控制转移:hello.s使用.L2和.LC1等段名称进行跳转,而反汇编代码使用目标代码的虚拟地址跳转。不过目前留下了重定位条目,跳转地址为零。它们将在链接之后被填写正确的位置。
- 函数调用:hello.s直接call函数名称,而反汇编代码中call的是目标的虚拟地址。但和上一条的情况类似,只有在链接之后才能确定运行执行的地址,目前目的地址是全0,并留下了重定位条目
。
4.5 本章小结
本章介绍了汇编。经过汇编器,汇编语言转化为机器语言,hello.s文件转化为hello.o可重定位目标文件。我们研究了可重定位目标文件elf格式,接触了了readelf命令、elf头、节头部表、重定位节、符号表。我们对比hello.s和hello.o,分析了汇编语言到机器语言的变化。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:链接(linking)是将各种代码和数据片段收集并合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到内存并执行时;甚至执行于运行时(run time),也就是在由应用程序来执行。
作用:链接器在软件开发过程中扮演着一个关键的角色,因为它们使得分离编译(separate compilation)成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。
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.3 可执行目标文件hello的格式
命令:readelf -a hello > hello1.elf
- ELF头:hello的文件头和hello.o文件头的不同之处如下图,hello是一个可执行目标文件,有27个节
2.节头:对 hello中所有的节信息进行了声明,包括大小和偏移量
3.重定位节.rela.text:
4.符号表.symtab:
5.4 hello的虚拟地址空间
使用edb加载hello, data dump窗口可以查看加载到虚拟地址中的hello程序。program headers告诉链接器运行时加载的内容并提供动态链接需要的信息。
5.5 链接的重定位过程分析
【命令】objdump -d -r hello > hello_objdump.s
5.5.1 新增函数:
链接加入了在hello.c中用到的库函数,如exit、printf、sleep、getchar等函数。
5.5.2 新增节:
hello中增加了.init和.plt节,和一些节中定义的函数。
5.5.3 函数调用地址:
Hello实现了调用函数时的重定位,因此在调用函数时调用的地址已经是函数确切的虚拟地址。
5.5.4控制流跳转地址:
Hello实现了调用函数时的重定位,因此在跳转时调用的地址已经是函数确切的虚拟地址。
5.5.5 链接的过程:
链接就是链接器(ld)将各个目标文件(各种.o文件)组装在一起,文件中的各个函数段按照一定规则累积在一起。从.o提供的重定位条目将函数调用和控制流跳转的地址填写为最终的地址。
5.6 hello的执行流程
使用edb执行hello,从加载hello到_start,到call main,以及程序终止的主要过程如下:
5.7 Hello的动态链接分析
在elf文件中看到.got.plt的起始表的位置在0x404000处:
进入edb查看:
利用代码段和数据段的相对位置不变的原则计算变量的正确地址。而对于库函数,需要plt、got的协作。
plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次再调用plt时,指向的就是正确的内存地址。plt就能跳转到正确的区域。
5.8 本章小结
本章研究了链接的过程。通过edb查看hello的虚拟地址空间,对比hello与hello.o的反汇编代码,深入研究了链接的过程中重定位的过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
作用:进程提供给应用程序的关键抽象:一个独立的逻辑控制流,如同程序独占处理器;一个私有的地址空间,如同程序独占内存系统。可以说,如果没有进程,体系如此庞大的计算机不可能设计出来。
6.2 简述壳Shell-bash的作用与处理流程
Shell是用户级的应用程序,代表用户控制操作系统中的任务。处理流程如下:
① 在shell命令行中输入命令:$./hello
② shell命令行解释器构造argv和envp;
③ 调用fork()函数创建子进程,其地址空间与shell父进程完全相同,包括只读代码段、读写数据段、堆及用户栈等
④ 调用execve()函数在当前进程(新创建的子进程)的上下文中加载并运行hello程序。将hello中的.text节、.data节、.bss节等内容加载到当前进程的虚拟地址空间
⑤ 调用hello程序的main()函数,hello程序开始在一个进程的上下文中运行。
6.3 Hello的fork进程创建过程
shell首先检查命令是否是内部命令,若不是再检查是否是一个应用程序(这里的应用程序可以是Linux本身的实用程序,如ls和rm,也可以是购买的商业程序,如xv,或者是自由软件,如emacs)。然后shell在搜索路径里寻找这些应用程序(搜索路径就是一个能找到可执行程序的目录列表)。如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息。如果能够成功找到命令,该内部命令或应用程序将被分解为系统调用并传给Linux内核。
6.4 Hello的execve过程
Execve的参数包括需要执行的程序(通常是argv[0])、参数argv、环境变量envp。
1. 删除已存在的用户区域(自父进程独立)。
2. 映射私有区:为Hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的。
3. 映射共享区:比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
4. 设置PC:exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
5. execve在调用成功的情况下不会返回,只有当出现错误时,例如找不到需要执行的程序时,execve才会返回到调用程序。
6.5 Hello的进程执行
6.5.1 逻辑控制流和时间片:
进程的运行本质上是CPU不断从程序计数器 PC 指示的地址处取出指令并执行,值的序列叫做逻辑控制流。操作系统会对进程的运行进行调度,执行进程A->上下文切换->执行进程B->上下文切换->执行进程A->… 如此循环往复。 在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。在一个程序被调运行开始到被另一个进程打断,中间的时间就是运行的时间片。
6.5.2 用户模式和内核模式:
用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据。
内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
6.5.3 上下文:
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
6.5.4 调度的过程:
在对进程进行调度的过程,操作系统主要做了两件事:加载保存的寄存器,切换虚拟地址空间。
6.5.5 用户态与核心态转换:
为了能让处理器安全运行,需要限制应用程序可执行指令所能访问的地址范围。因此划分了用户态与核心态。
核心态可以说是拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
6.6 hello的异常与信号处理
6.6.1 正常运行
正常运行时,hello会先从键盘读入“7203610122 李唤宇 5”,其中最后一个数字是用户指定的秒数,表示每经过5秒,hello程序会输出一串“Hello 7203610122 李唤宇”字符串。正常运行的情况如图所示。
6.6.2 ctrl-z
输入ctrl-z默认结果是挂起前台的作业,hello进程并没有回收,而是运行在后台下,如图所示用ps命令可以看到,hello进程并没有被回收。此时他的后台 job 号是 1,调用 fg 1 将其调到前台,此时 shell 程序首先打印 hello 的命令行命令, hello 继续运行打印剩下的8条info,之后输入字串,程序结束,同时进程被回收。如图
输入jobs命令,查看进程的job号,hello的job号为1。输入fg 1命令,可以将hello进程重新调回前台运行。
6.6.3 ctrl+c,在键盘上输入ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业,如图用ps查看前台进程组发现没有hello进程。
6.6.4 程序运行过程中按键盘,不停乱按,结果如图,可以发现,乱按只是将屏幕的输入缓存到 stdin,当 getchar 的时候读出一个’\n’结尾的字串(作为一次输入),其他字串会当做 shell 命令行输入。
6.7本章小结
本章了解了hello进程的执行过程。在hello运行过程中,内核对其调度,异常处理程序为其将处理各种异常。每种信号都有不同的处理机制,对不同的shell命令,hello也有不同的响应结果。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1 逻辑地址
逻辑地址(Logical Address)是指由程序产生的与段相关的偏移地址部分。在这里指的是hello.o中的内容。
7.1.2 线性地址
线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生段中的偏移地址,加上相应段的基地址就生成了一个线性地址。
7.1.3 虚拟地址
CPU启动保护模式后,程序hello运行在虚拟地址空间中。注意,并不是所有的“程序”都是运行在虚拟地址中。CPU在启动的时候是运行在实模式的,Bootloader以及内核在初始化页表之前并不使用虚拟地址,而是直接使用物理地址的。
7.1.4 物理地址
放在寻址总线上的地址。放在寻址总线上,如果是读,电路根据这个地址每位的值就将相应地址的物理内存中的数据放到数据总线中传输。如果是写,电路根据这个地址每位的值就在相应地址的物理内存中放入数据总线上的内容。物理内存是以字节(8位)为单位编址的。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在 Intel 平台下,逻辑地址(logical address)是 selector:offset 这种形式,selector 是 CS 寄存器的值,offset 是 EIP 寄存器的值。如果用 selector 去 GDT( 全局描述符表 ) 里拿到 segment base address(段基址) 然后加上 offset(段内偏移),这就得到了 linear address。我们把这个过程称作段式内存管理。
一个逻辑地址由段标识符和段内偏移量组成。段标识符是一个16位长的字段(段选择符)。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。
给定一个完整的逻辑地址段选择符+段内偏移地址,看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,就得到了其基地址。Base + offset = 线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
形式上来说,地址翻译是一个N元素的虚拟地址空间中的元素和一个M元素的物理地址空间中元素之间的映射。MMU实现了这种线性地址到物理地址的变换,它是利用页表来实现的。CPU中的一个控制寄存器,页表基址寄存器指向当前页表。n位的虚拟地址包含两个部分:一个P位的虚拟页面偏移,和一个n-p位的虚拟页号。MMU利用VPN来选择适当的PTE。例如VPN0选择PTE0,VPN1选择PTE1,以此类推。将页表条目中物理页号和虚拟地址中的VPO串联起来,就得到相应的物理地址。注意,因为物理和虚拟页面都是P字节的,所以物理页面偏移和VPO是相同的。
7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,MMU就必须查阅一个PTE,以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会要求从内存多取一次数据,代价是几十到几百个周期。如果 PTE 碰巧缓存在L1中,那么开销就下降到1 个或 2 个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU 中包括了一个关于PTE的小的缓存,称为翻译后备缓冲器。为了减少页表太大而造成的空间损失,可以使用多级页表的技术。
Core i7使用的是四级页表。在四级页表层次结构的地址翻译中,虚拟地址被划分为4个VPN和1个VPO。每个第i个VPN都是一个到第i级页表的索引,第j级页表中的每个PTE都指向第j+1级某个页表的基址,第四级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问四个PTE。
7.6 hello进程fork时的内存映射
CPU发送一条虚拟地址,随后MMU按照7.4所述的操作获得了物理地址PA。根据cache大小组数的要求,将PA分为CT(标记位)CI(组索引),CO(块偏移)。根据CI寻找到正确的组,依次与每一行的数据比较,有效位有效且标记位一致则命中。如果命中,直接返回想要的数据。如果不命中,就依次去L2,L3,主存判断是否命中,命中时将数据传给CPU同时更新各级cache的储存。
7.7 hello进程execve时的内存映射
加载并运行hello需要以下几个步骤:
- 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
- 映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
- 映射共享区域。如果hello程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器。设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。
7.8 缺页故障与缺页中断处理
页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的:
- 处理器生成一个虚拟地址,并将它传送给MMU
2.MMU生成PTE地址,并从高速缓存/主存请求得到它
3.高速缓存/主存向MMU返回PTE
4.PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
5.缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
6.缺页处理程序页面调入新的页面,并更新内存中的PTE
7.缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。
7.9动态存储分配管理
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、(可能的)额外填充以及一个字的尾部组成。
7.9.1 隐式空间链表管理
隐式空闲链表:空闲块通过头部的大小字段隐含地连接着。分配器遍历堆中所有的块,间接地遍历整个空闲块的集合。
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配和最佳适配。分配器在面对释放一个已分配块时,可以合并相邻的空闲块,其中一种简单的方式,是利用隐式空闲链表的边界标记来进行合并。
7.9.2 显式空间链表管理
显式空闲链表是将堆的空闲块组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。进行内存管理。在显式空闲链表中。可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
7.10本章小结
本章介绍了存储器地址空间、段式管理、页式管理,VA 到 PA 的变换、物理内存访问, hello 进程fork时和execve 时的内存映射、缺页故障与缺页中断处理、包括隐式空闲链表和显式空闲链表的动态存储分配管理。
(第7章 2分)
结论
hello所经历的过程:
编写:通过编辑器输入hello.c的C语言代码
预处理:预处理器对hello.c处理生成hello.i文件
编译:编译器编译hello.i将其转化成汇编语言描述的hello.s文件
汇编:汇编器将hello.s文件翻译成可重定位文件hello.o
链接:链接器将hello.o和其他目标文件进行链接,生成可执行文件hello
运行:在shell中输入./hello 1170301004 wanghang,开始运行hello程序
创建新进程:shell为hello程序fork一个新进程
加载:在新进程中调用execve函数,将hello程序映射到虚拟内存中
执行:内核调度该进程执行,进行虚拟地址的翻译,此时会发生缺页,开始加载hello代码和数据到对应的物理页中,然后开始执行。
信号处理:在hello进程运行中,按下ctrl+z、ctrl+c等将会发送信号给hello, 进而调用信号处理程序进行处理。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello.i | 预处理后的文件 |
hello | 链接之后的可执行目标文件 |
hello.o | 汇编之后可重定位目标文件 |
hello.s | 编译之后的汇编文件 |
hello.elf | hello.o的elf格式 |
hello1.elf | hello的elf格式 |
hello_objdump.s | hello的反汇编代码 |
(附件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分)