赞
踩
计算机系统
大作业
题 目 :程序人生-Hello’s P2P
专 业 :计算学部
学号 :7203610517
班级 :2036013
学 生 :李梓琦
指 导 教 师 :刘宏伟
计算机科学与技术学院
2021年5月
摘 要
摘要是论文内容的高度概括,应具有独立性和自含性,即不阅读论文的全文,就能获得必要的信息。摘要应包括本论文的目的、主要内容、方法、成果及其理论与实际意义。摘要中不宜使用公式、结构式、图表和非公知公用的符号与术语,不标注引用文献编号,同时避免将摘要写成目录式的内容介绍。
关键词:程序预处理,编译,汇编,链接
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
P2P:From Program to Process
先用c语言编写hello.c文件,再用编译器(cpp)预处理得到hello.i文件,接着编译(cc1)得到hello.s汇编文件,再用汇编器(as)将文件打包为hello.o可重定位文件,再经过链接器id将文件与库函数链接得到hello可执行文件,执行hello,系统调用fork函数为其创建一个子进程,之后调用execve函数加载进程。以上就是P2P全部内容。
020:From Zero-0 to Zero-0
Execuve执行hello程序后,系统先删除当前虚拟地址的数据结构并为hello创建新的区域结构,之后将hello相关数据加载到物理内存之中,接着正式执行hello。当hello运行结束后,由shell回收hello进程,并由内核删除相关数据结构。
(1)硬件环境:X64 CPU;2GHz;4GRAM;256Disk
(2)软件环境:Windows10 64位;Vmware 10;Ubuntu 16.04 LTS 64位
(3)使用工具:Codeblocks;Objdump;Gdb;Hexedit
文件名 文件作用
hello.c 程序的源代码
hello.i hello.c文件预处理后的文本文件
hello.s hello.i文件编译后的文件
hello.o hello.s汇编后的可重定位文件
hello hello.s链接后的可重定位文件
本章主要介绍了hello文件的P2P,020过程,列举了实验的软硬件环境与开发工具和中间结果文件,起到了概述的作用。
(第1章0.5分)
\
概念:程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。
作用:最常见的预处理是C语言和C++语言。ISO C和ISOC++都规定程序由源代码被翻译分为若干有序的阶段(phase),通常前几个阶段由预处理器实现。预处理中会展开以#起始的行,试图解释为预处理指令(preprocessing directive) ,其中ISOC/C++要求支持的包括#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。
Linux中对hello.c文件进行预处理的命令是:gcc -E -o hello.i hello.c
大作业目录下会新增一个hello.i文件:
以下是hello.c截图:
hello.i截图:
预处理命令生成hello.i文件,打开该文件发现,文件内容被拓展为3000多行,但是文件内容仍为c语言文本文件。增加的文本内容其实是三个头文件中的内容,如库函数、定义变量、定义宏等,说明预处理其实是对源程序中的宏进行了宏展开。
本章介绍了预处理的概念及作用,列举了Linux系统中的预处理命令,查看并对比了hello.c与hello.i文件,hello.i实际是对hello.c中的头文件中的内容进行补充和替换后的结果。
(第2章0.5分)
\
概念:编译器将文本文件hello.i翻译成汇编语言程序hello.s
作用:将高级语言程序翻译成汇编语言目标程序,在编译过程中可以检查代码中的错误,同时编译生成的汇编语言文件可以进一步被翻译为机器可识别的机器码。
在ubuntu下对文件进行编译的指令是:gcc -S -o hello.s hello.i
编译生成的hello.s文件截图
在源文件中的两个字符串是常量,分别是"用法: Hello 学号 姓名
秒数!\\n"和"Hello %s %s\\n"。
在hello.s中,这两个常量字符串分别用.LC0和.LC1表示,而且这两个字符串都在只读数据段.rodata中
在源文件中有两个整数int i和int argc
int i:因为i有加法操作而且i会和8比较大小,因此定位i的位置,可知i被保存在被存储在%rbp-4的内存地址处,占用四个字节,在汇编程序中可以看到每次循环都将i加一,再将i与7作比较。
int argc:
因为argc是main函数的第一个参数,所以参数在寄存器%edi中,从22行可以看出它在栈-20(%rbp)中
源文件中的数组为char \*argv\[\],是一个字符指针数组,大小为8个字节,数组作为main函数的第二个参数被保存在rsi中
若要访问数组,则要先获取数组的首地址,再计算数组的偏移量,移动栈指针。
源程序中的赋值操作只有i = 0这一个,在汇编中如下所示:
汇编中用movl语句实现,将i的值赋为0,因为是int类型所以占4个字节。
源程序中的类型转换有一处,为用atoi函数将字符类型转变为整形传递给sleep函数。
汇编中调用atoi函数进行类型转换,之后将返回值%eax传入%edi之后传入sleep函数。
(1)第一个关系操作是判断argc的值是否不等于4,在汇编中使用cmpl指令将argc的值和立即数4做比较,如果不等于则执行je跳转。
(2)第二个关系操作是在for循环中判断i是否小于8,并作为循环结束标志汇编代码使用cmpl将i的值与7作比较,并决定是否执行跳转,比较值在编译过程中被优化为7。
源程序中的数组操作是访问argv[1]和argv[2]这两个数组中的参数,数组的访问格式是首地址+偏移量,在34行中,数组的首地址位于-32(%rbp),35行中将首地址加16获得argv[2]的地址,元素值保存在%rdx中,第38行中首地址加8获得argv[1]的地址,元素值保存在%rax中
** 1. argc与4作比较后的控制转移**
汇编代码用cmpl指令将argc和4作比较,之后根据条件码决定是否执行je跳转指令。
2. i与8做比较作为for循环的终止条件
汇编指令将i与7作比较,根据条件码确定是否执行跳转。比较值7是编译器优化后的结果。
调用函数操作如下(P在调用Q时):
1. 传递控制:开始执行函数时,首先得将PC设置为代码的起始地址,而返回时也要将PC设置为P调用完Q后的下一条语句。
2. 参数传递:P必须向Q提供一个或多个参数,而Q也能向P返回0个或者1个参数。
3. 分配和释放内存:在Q执行前为其分配合适的空间,而Q返回后释放为其分配的空间。
main函数有两个参数,argc和argv\[\],分别被储存在edi和rsi中
atoi函数的功能是将字符串转化为整数,%rdi中的值为函数参数,函数返回值储存在%eax中
函数的参数为%edi中的值,也就是atoi函数的返回值
函数的参数有三个,分别储存在%rdx,%rsi,%rdi中
exit函数用于终止程序,函数将1传给%edi后,调用exit函数终止程序。
本章介绍了编译的概念和作用和Linux下编译的指令,重点分析了汇编程序的数据操作,逻辑操作和函数操作等。
(第3章2分)
\
(1)概念:汇编是指从 .s 到 .o即编译后的文件到生成机器语言二进制程序的过程。.o可重定位目标文件包含指令对应的二进制机器语言,这种二进制代码能够被计算机理解并执行。
(2)作用:将汇编语言进一步翻译为计算机可以理解并执行的机器语言。
Linux下汇编命令为:gcc hello.s -c -o hello.o
命令:readelf -a -W hello.o > hello.elf
(1) ELF头:由一个16字节的序列开始,此序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助连接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件类型、机器类型、字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。
(2)节头:记录了文件中出现的各个节的名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐等信息。
(3)重定位节:保存的是.text节中需要被修正的信息在,也就是代码的重定位条目,重定向节的信息告诉链接器在将目标文件合并成可执行文件时如何修改函数引用。每个重定位条目包括offset:需要被修改的引用的节偏移;symbol:标识被修改引用应该指向的符号;type:重定位类型,告知链接器如何修改新的引用;attend:一些重定位要使用它对被修改引用的值做偏移调整。本程序需要被重定位的是puts、exit、rodata中的.L0和.L1、printf、atoi、sleep、getchar。
(4) 符号表:存放在程序中定义和引用的函数和全局变量的信息。
命令:objdump -d -r hello.o
机器语言是机器能直接识别的程序语言或指令代码,无需经过翻译,每一操作码在计算机内部都有相应的电路来完成它,或指不经翻译即可为机器直接理解和接受的程序语言或指令代码。机器语言使用绝对地址和绝对操作码。不同的计算机都有各自的机器语言,即指令系统。从使用的角度看,机器语言是最低级的语言。而汇编语言的构成是汇编指令,将机器指令转化为便于程序员记忆的表示形式。主要与机器指令在指令的表示方法上有所不同。
hello.o的反汇编代码与hello.s文件主要有以下区别:
1. 分支转移:反汇编代码中,跳转指令是直接通过地址进行跳转,通过主函数加上段内偏移量得到要跳转函数的地址。而在汇编代码中,分支转移直接通过如.LC0,.LC1等助记符进行跳转,即直接跳转到助记符标明的位置。
2. 函数调用:汇编代码通过直接调用函数名称来调用函数。反汇编代码中,call指令后是被调函数的地址,通过main函数+偏移量直接定位函数。由于hello.c中调用的函数都是库函数,需要在动态链接后才能确定被调函数的执行时的地址。在.rela.text节中为其添加重定位条目等待链接。原因是调用函数时,机器只能识别地址,不能识别名称。
3. 访问全局变量:在汇编文件中,全局变量由类似.LC0(%rip)的形式来访问,反汇编中用0x0(%rip),这是因为.rodata节中的地址还没有确定,所以先初始化为0,并在.rela.text节中添加重定位条目。
叙述了汇编的概念和作用和汇编文件的操作命令。之后分析了elf文件的组成及各部分的作用,将反汇编文件与hello.s文件作比较,理解了机器语言和汇编语言的映射关系。
(第4章1分)
\
概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是在由应用程序来执行。
作用:链接使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。
命令: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
生成文件如下:
命令:readelf -h hello
ELF 头:信息包括类型,系统架构,大小,入口点位置等信息。
节头:
程序头:节头包含各个节的大小、偏移量和其他属性。
重定位节:重定位节告诉链接器在将目标文件合并成可执行文件时如何修改函数引用。
符号表:
动态符号表:
用edb打开hello
1.Data Dump窗口显示程序虚拟地址从0x401000开始到0x402000结束
2.查看.interp节
右键Data Dump窗口,选择Goto
Expression,输入.interp节的地址,即可查到其所在位置
2.查看.init节
可见保存Linux动态共享库的路径
3.查看.plt节
命令:objdump -d -r hello
hello文件中节的数量更多,hello中增加了.init、.plt、.plt.sec等节,而hello.o中仅有.text节
hello中jmp跳转的地址是CPU可以直接寻址的地址,hello.o是用地址+偏移量来间接寻址的。hello.o:
hello:
hello中的函数调用是CPU可以直接寻址的地址,不再是main+偏移地址。
hello:
hello.o:
重定位过程:链接器利用在重定位条目中保存的重定位信息,来修改需要被修改的引用,即修改他们的地址。
使用edb执行hello:
函数 | 地址 |
---|---|
ld-2.31.so!_dl_start | 0x00007f8e3839b260 |
ld-2.31.so! dl_init | 0x00007f8e383aa340 |
hello!_start | 0x004010f0 |
ld-2.31.so!_libc_start_main | 0x004010a0 |
libc-2.31.so! cxa_atexit | 0x00007f8e38220580 |
hello!_libc_csu_init | 0x004011c0 |
hello!_init | 0x00401000 |
libc-2.31.so!_setjmp | 0x00007f8e38220550 |
libc-2.31.so!_sigsetjmp | 0x00007f8e38220540 |
libc-2.31.so!__sigjmp_save | 0x00007a8e38220510 |
hello_main | 0x00401125 |
hello!puts@plt | 0x00401030 |
hello!exit@plt | 0x00401070 |
hello!printf@plt | 0x00401040 |
hello!sleep@plt | 0x00401080 |
hello!getchar@plt | 0x00401050 |
ld-2.31.so!_dl_runtime_resolve_avx | 0x00007f1172b951f0 |
libc-2.31.so!exit | 0x00007f80023e5530 |
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
.got.plt起始表的位置为0x404000
调用前:
调用后:
调用之后, 地址0x404010处的两个8字节的数据分别发生改变。
本章介绍了链接的概念和作用,生成了hello可执行文件,分析了hello的虚拟地址空间,重定位过程,执行过程。简述了动态链接的原理
(第5章1分)
\
定义:进程是计算机科学中最深刻、最成功的概念之一。进程的经典定义是一个执行中程序的实例,系统的每个程序都运行在某个进程的上下文。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存里的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。
作用:通过进程,我们会得到一种假象,好像我们的程序是当前唯一运行的程序,我们的程序独占处理器和内存,我们程序的代码和数据好像是系统内存中唯一的对象。处理器就好像是无间断地一条接一条地执行我们程序中的指令。
作用:是一种交互型的应用级程序,是Linux的外壳,提供了一个界面,用户可以通过这界面访问操作系统内核。
处理流程:
shell读取终端输入的命令
将输入字符串分割获得所有的参数
如果是内置命令,则立即执行这个命令
否则创建一个新的子进程并运行这个命令
接收键盘输入的信号并处理IO设备输入的信号
终端程序通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是相互独立,两者有独立的代码段、段、数据段、共享库以及用户栈等。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程有不同的PID。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的
逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
以我们的hello为例,当我们输入 ./hello 7203610517 李梓琦 1
的时候,首先shell对我们输入的命令进行解析,由于我们输入的命令不是一个内置的shell命令,因此shell会调用fork()创建一个子进程,,得到与父进程完全相同的数据空间,栈,堆等资源,程序开始执行。。
当创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序,即hello程序,需要以下步骤:
删除已存在的用户区域。删除之前进程在用户部分中已存在的结构。
创建新的代码、数据、堆和栈段。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
(1)逻辑控制流和时间片
进程的运行本质上是CPU不断从程序计数器 PC
指示的地址处取出指令并执行,值的序列叫做逻辑控制流。操作系统会对进程的运行进行调度,执行进程A->上下文切换->执行进程B->上下文切换->执行进程A->…
如此循环往复。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。在一个程序被调运行开始到被另一个进程打断,中间的时间就是运行的时间片。
用户模式和内核模式
Shell使得用户可以有机会修改内核,所以需要设置一些防护措施来保护内核,如限制指令的类型和可以作用的范围。
上下文切换
如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程,上下文就是内核重新启动一个被抢占的进程所需要的状态,是一种比较高层次的异常控制流。
调度
在对进程进行调度的过程,操作系统主要做了两件事:加载保存的寄存器,切换虚拟地址空间。
用户态与核心态转换
为了能让处理器安全运行,需要限制应用程序可执行指令所能访问的地址范围。因此划分了用户态与核心态。核心态可以说是拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
对于hello的进程执行,具体过程如下:键盘输入./hello 7203610517 李梓琦
首先shell通过加载器加载可执行目标文件hello,操作系统进行上下文切换,切换到hello的进程中,此时为用户态,执行完相应函数后,调用sleep函数,进入内核态,当sleep的时间完成后时定时器发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,上下文切换再进入hello进程,回到用户态。
1.异常和信号异常可以分为四类:中断、陷阱、故障、终止
2.键盘上各种操作导致的异常:
(1)程序正常执行结果
(2)执行过程中按回车
程序在执行后自动终止
输入Ctrl-C会让内核发送一个SIGINT信号给前台进程组的每个进程,终止前台进程。
输入Ctrl-Z会发送一个SIGTSTP信号给前台进程组的每个进程,结束前台作业。
fg命令使一个后台作业变为前台作业,因此hello程序又开始执行。
如果乱按过程中没有回车,则只是把输入屏幕的字符串缓存起来。如果输入最后是回车,使用getchar函数读回车,并把回车前的字符串当作shell输入的命令
本章介绍了进程的概念及作用,以及壳Shell-bash的作用与处理流程,分析了fork
函数创建新进程,调用execve函数执行
hello,分析了hello的进程执行,以及hello 的异常与信号处理。
(第6章1分)
\
(1)逻辑地址:在有地址变换功能的计算机中,访内指令给出的地址 (操作数)
叫逻辑地址,也叫相对地址。逻辑地址用来指定一个操作数或指令,它由选择符和偏移量组成。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为
[段地址:偏移地址]。
(2)线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式,分页机制中线性地址作为输入。
(3)CPU启动保护模式后,程序运行在虚拟地址空间中。与物理地址相似,虚拟内存被组织为一个存放在磁盘上的N个连续的字节大小的单元组成的数组,其每个字节对应的地址成为虚拟地址。虚拟地址包括VPO(虚拟页面偏移量)、VPN(虚拟页号)、TLBI(TLB索引)、TLBT(TLB标记)。
(4)物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。
逻辑地址由段标识符和段内偏移量两部分组成。段标识符由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号,是对段描述符表的索引,每个段描述符由8个字节组成,具体描述了一个段。后3位包含一些硬件细节,表示具体是代码段寄存器还是栈段寄存器还是数据段寄存器等。通过段标识符的前13位,可以直接在段描述符表中索引到具体的段描述符。每个段描述符中包含一个Base字段,它描述了一个段的开始位置的线性地址。将Base字段和逻辑地址中的段内偏移量连接起来就得到转换后的线性地址。
对于全局的段描述符,放在全局段描述符表中,局部的(每个进程自己的)段描述符,放在局部段描述符表中。给定逻辑地址,看段选择符的最后一位是0还是1,用于判断选择全局段描述符表还是局部段描述符表。
计算机使用页表来完成虚拟地址到物理地址的转换。
处理器生成一个虚拟地址,并把它传送给MMU。
MMU生成PTE地址,并从高速缓存中请求得到它。
高速缓存向MMU返回PTE。
MMU构造物理地址,并把它传送给高速缓存。
高速缓存返回锁清秋的数据字给处理器。
TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单一PTE组成的块。TLB通常有高的相联度,从虚拟地址中的页号提取出组选择和行匹配的索引和标记字段。
TLB变换步骤:
CPU产生一个VA。
MMU在根据VPN在TLB中搜索PTE。
若命中,MMU取出相应的PTE,根据PTE将VA翻译成PA。
若没命中,则通过多级页表查询PTE是否在页中。
若在页中,找到对应的PIE,MMU将VA翻译成PA;若没有在页中,则进行缺页处理。
四级页表支持:在四级页表层次结构的地址翻译中,虚拟地址被划分为4个VPN和1个VPO。每个第i个VPN都是一个到第i级页表的索引,第j级页表中的每个PTE都指向第j+1级某个页表的基址,第四级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问四个PTE。
获得物理地址之后,先取出组索引对应位,在L1中寻找对应组。如果存在,则比较标志位,相等后检查有效位是否为1.如果都满足则命中取出值传给CPU,否则按顺序对L2cache、L3cache、内存进行相同操作,直到出现命中。然后再一级一级向上传,如果有空闲块则将目标块放置到空闲块中,否则将缓存中的某个块驱逐,将目标块放到被驱逐块的位置。
当fork函数为hello进程创建虚拟内存时,内核为新进程创建各种数据结构,并分配给它一个唯一的
PID,为了给这个新进程创建虚拟内存,它创建了当前进程的
mm_struct、区域结构和页表的原样副本。并且将这两个进程的每个页面标记为只读,以及两个两个进程中每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。因此,也就为每个进程保持了私有地址空间的抽象概念。
execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运
行包含在可执行目标文件 hello 中的程序,用 hello
程序有效地替代了当前程序。
它可能会自动覆盖当前进程中的所有虚拟地址和空间,删除当前进程虚拟地址的所有用户虚拟和部分空间中的已存在的代码共享区域和结构,但它不会自动创建一个新的代码共享进程。
加载并运行过程如下:
删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存
在的区域结构。
映射私有区域,为新程序的代码、数据、bss和栈创建新的区域结构。
映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链
接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
设置程序计数器
。execve最后的操作就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
缺页故障:DRMA缓存不命中称为缺页,当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。
缺页故障处理程序:
检查虚拟地址是否合法,如果不合法则触发一个段错误,程序终止。
检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,程序终止。
缺页处理程序选择物理内存中的牺牲页(若页面被修改,则换出到磁盘)。
缺页处理程序调入新的页面到内存,并更新PTE。
将控制转移给hello进程,再次执行触发缺页故障的指令。
动态内存分配器维护者一个进程的虚拟内存区域,成为堆。分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。分配器有两种基本风格。
分配器有两种基本风格:显式分配器和隐式分配器。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
显式分配器:要求应用显式地释放任何已分配的块。例如,C语言提供了malloc程序包的显式分配器。在C程序中,调用malloc函数可以分配一个块,调用free函数可以释放一个块。
要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。例如,Java语言就依赖垃圾收集来释放已分配的块。
本章叙述了hello的内存管理,虚拟地址、物理地址、线性地址、逻辑地址的区别以及它们之间的变换模式,以及段式、页式的管理模式,fork和execve函数的内存映射,以及缺页故障和缺页中断管理机制,最后还介绍了动态内存的相关知识。
(第7章 2分)
\
设备的模型化:所有IO设备都被模型化为文件甚至是内核,所有的输入和输出都能被当做相应文件的读和写来执行。
设备管理:unix
io接口,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix
I/O。Linux就是基于Unix I/O实现对设备的管理。
(1) 打开文件。一个应用程序通过要求内核打开相应的文件,来告知系统它想要访问一个
I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
(2) shell在进程的开始为其打开三个文件:标准输入、标准输出和标准错误。
(3) 改变当前文件的位置:对于每个打开的文件,内核保存着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作显式地设置文件的当前位置为k。
(4) 读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
(5) 关闭文件:内核释放打开文件时创建的数据结构以及占用的内存资源,并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
(1)进程是通过调用open函数来打开一个存在的文件或者创建一个新文件的,函数声明如下:
int open(char *filename, int flags, mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件。mode参数指定了新文件的访问权限位。作为上下文的一部分,每个进程都有一个umask,它是通过调用umask函数来设置的。当进程通过带某个mode参数的open函数调用来创建一个新文件时,文件的访问权限位被设置成mode&~umask。
(2)进程通过调用close函数关闭一个打开的文件。函数声明如下:
int close(fd)
fd是需要关闭的文件的描述符,成功返回0错误返回EOF。
(3) 进程通过调用分别调用read和write函数来执行输入和输出的。函数声明如下:
ssize_t read(int fd, void *buf, size_t n);
ssize_t write(int fd, const void *buf, size_t n);
read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。若发生错误则返回-1,0表示EOF,否则返回值表示的是实际传送的字节数量。write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置,成功则返回写的字节数,出错则返回-1。
(4) 通过调用lseek函数,应用程序能够显式地修改当前文件的位置。函数声明如下:
off_t lseek(int handle, off_t offset, int fromwhere);
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;
}
printf函数的作用是接受一个fmt的格式,然后将匹配到的参数按照fmt格式输出。它用到了两个外部函数,一个是vsprintf,还有一个是write。
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函数将所有的参数内容格式化之后存入buf,返回格式化数组的长度。write函数将buf中的i个元素写到终端。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用int0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
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时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。
当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。
异步异常-键盘中断的处理:当用户按键时触发键盘终端,操作系统将控制转移到键盘中断处理子程序,中断处理程序执行,接受按键扫描码转成ascii码,保存到系统的键盘缓冲区,显示在用户输入的终端内。当中断处理程序执行完毕后,返回到下一条指令运行。
本章主要介绍了Linux的IO设备管理方法、Unix
IO接口及其函数,分析了printf函数和getchar函数的源码和实现原理。
(第8章1分)
hello的一生:
预处理:hello.c预处理到hello.i文本文件;
编译:hello.i编译到hello.s汇编文件;
汇编:hello.s汇编到二进制可重定位目标文件hello.o;
链接:hello.o链接生成可执行文件hello;
创建子进程:bash进程调用fork函数,生成子进程;
加载程序:execve函数加载运行当前进程的上下文中加载并运行新程序hello;
访问内存:hello的运行需要地址的概念,虚拟地址是计算机系统最伟大的抽象;
交互:hello的输入输出与外界交互,与linux I/O息息相关;
终止:hello最终被shell父进程回收,内核会收回为其创建的所有信息。
个人感想:通过这次的大作业,我了解了hello程从预处理到链接再到执行后终止的流程,加深了个人对于底层知识的了解,希望以后也能将这些底层知识运用到项目中。
(结论0分,缺失 -1分,根据内容酌情加分)
\
文件名 | 文件类型 |
---|---|
hello.c | 源程序 |
hello.i | 预处理后文件 |
hello.s | 编译后的汇编文件 |
hello.o | 汇编后的可重定位目标执行文件 |
hello | 链接后的可执行文件 |
hello.elf | hello.o的ELF格式 |
hello2.elf | hello的ELF格式 |
(附件0分,缺失 -1分)
\
为完成本次大作业你翻阅的书籍与网站等
深入理解计算机系统原书第3版-文字版.pdf
https://www.cnblogs.com/pianist/p/3315801.html
http://word.wd1x.com/
(参考文献0分,缺失 -1分)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。