哈尔滨工业大学计算机系统大作业——程序人生-Hello’s P2P
Posted zyqWTF
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了哈尔滨工业大学计算机系统大作业——程序人生-Hello’s P2P相关的知识,希望对你有一定的参考价值。
目录
1.1.1 P2P:From Program to Process
1.1.2 020:From Zero-0 to Zero-0
摘要
本文围绕hello的“程序人生”,对hello“From Program to process”和“From Zero-0 to Zero-0”两个过程展开分析,hello.c从预处理、编译、汇编、链接、fork一系列操作从程序变成进程,再通过调用execve函数加载进程、对控制流的管理、内存空间的分配、异常的处理、对I/O设备的调用、shell父进程回收等一系列操作从无到有再到无。回顾hello的一生,便是一次深入理解计算机系统的历程。
关键词:程序;进程;存储;I/O;程序人生
第1章 概述
1.1 Hello简介
1.1.1 P2P:From Program to Process
在Unix系统上,GCC编译器驱动程序读取源程序文件hello.c,并把它翻译成一个可执行目标文件hello,这个过程可分为四个阶段——预处理器(cpp)将源程序hello.c修改成hello.i文本文件,编译器(ccl)将hello.i翻译成汇编程序hello.s,汇编器(as)将hello.s翻译成机器语言指令,并将这些指令打包成可重定位目标程序hello.o,连接器(ld)将hello.o与库函数相链接生成可执行目标程序hello。执行该程序时,操作系统调用fork创建一个子进程,此时hello.c就从program变成了一个process。
1.1.2 020:From Zero-0 to Zero-0
execve函数加载并运行可执行目标文件hello,操作系统为其分配虚拟内存空间,在物理内存与虚拟内存之间建立映射。执行过程中,虚拟内存为进程提供独立的空间,数据从磁盘传输到CPU中,TLB、分级页表等保障了数据的高效访问,I/O管理与信号处理共同实现了hello的输入输出。程序运行结束后,shell父进程负责回收hello进程,对应的虚拟空间以及相关数据结构被释放,hello进程便经历了从无到有再到无的过程。
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;2GHz;2G RAM;256GHD Disk 以上
1.2.2 软件环境
Windows10 64位以上;VirtualBox 11以上;Ubuntu 16.04 LTS 64位
1.2.3 开发工具
Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc
1.3 中间结果
hello.c:源程序文件
hello.i:hello.c预处理后的源程序文件
hello.s:hello.i编译后的汇编程序
hello.o:hello.s汇编后的可重定位目标文件
elf.txt:hello.o的ELF文件
hello_o2s.s:hello.o的反汇编代码
hello:hello.o链接后的可执行目标文件
hello_elf.txt:hello的ELF文件
hello_objdump.s:hello的反汇编代码
1.4 本章小结
本章简述了hello的“一生”,即P2P和020,并简单列出此次大作业所需的环境和工具,以及hello.c所生成的中间结果文件。
第2章 预处理
2.1 预处理的概念与作用
2.1.1 概念
在C语言中,预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。
2.1.2 作用
1.宏替换:将宏名替换为字符串或数值;
2.文件包含:预处理器读取头文件中的内容,并直接插入到程序文本中;
3.条件编译:根据条件编译指令决定需要编译的代码;
4.删除注释。
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
打开预处理后的hello.i文件,发现由原来的23行扩展到3060行。其中头文件中的内容被直接插入到文本文件中,出现大量声明函数、定义结构体、定义变量、定义宏等内容,.c文件中的注释也被删除,但main函数没有改变。
2.4 本章小结
本章介绍了预处理的概念和作用,并在Ubuntu中进行预处理,并分析了预处理前后源文件的差别。
第3章 编译
3.1 编译的概念与作用
3.1.1 概念
编译器(ccl)将文本文件hello.c翻译成文本文件hello.s,它包含一个汇编语言程序。
3.1.2 作用
通过以下五个阶段把人们熟悉的高级语言语言换成计算机能解读、运行的低级语言——词法分析,语法分析,语义检查和中间代码生成,代码优化,目标代码生成。
1.词法分析:对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。
2.语法分析:以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最后看是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构,程序是最终的一个语法单位。
3. 语义检查和中间代码生成:由语义分析器完成,指示判断是否合法,并不判断对错。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码。
4.代码优化:对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。
5. 目标代码生成:把语法分析后或优化后的中间代码变换成目标代码。
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1 数据及数组
在hello.i的main函数中,常量包括printf函数中打印的两个字符串常量和if条件、for循环里的数字常量。其中,如图4所示,字符串常量被存储在./rotate段,如图5所示,数字常量被存储在.text段中,且作为立即数出现。
代码中并没有全局变量,局部变量分别是函数参数argc和argv,以及for循环中的i;其中argc表示参数argv的个数,分析图6、图7可知,argc、数组argv以及i均存储在栈中,argc地址为-20(%rbp); argv首地址为-32 (%rbp),每个参数加8(对应图7绿色方框),即argv[k]=-(32+8k)(%rbp),所以数组取值的操作离不开首地址偏移量;i地址为-4(%rbp)。
3.1.2 赋值
如图7左上及右边红色方框所示,左边的hello.i中,需对i进行赋值,即将0赋给i,在汇编代码中对应第一行“movl $0, -4(%rbp)”。
3.1.3 类型转换
如图7蓝色方框所示,hello.i中类型转换体现在“atoi(argv[3])”上,即将字符串参数转换为整型参数,需调用atoi函数,在汇编代码中对应为“call atoi”。
3.1.4 算术操作
如图7右边红色方框,for循环中每次循环i都要自加1,在汇编代码中则由“addl $7, -4(&rbp)”实现。
3.1.5 关系操作及控制转移
如图6及图7右边红色方框所示,if条件中需要用到“!=”和“<”这样的关系操作,if-else或者是否跳出for循环则是控制转移,而在汇编代码中,关系操作往往与控制转移(跳转指令)一起出现,即“cmp”后会跟上“jXX”,跳不跳转则需看cmp设置的条件码,或者是直接跳转指令“jmp”,例如图6中,首先将立即数4与 -20(%rbp)对应值(即argc)进行对比,若等于,即“je”就代表相等则跳转至.L2。
3.1.6 函数操作
hello.i中一共涉及5个函数调用,即main、printf、exit、sleep、getchar,在汇编语言中,寄存器一般都有特定用途,例如%rax存储返回值,%rdi存储第一个参数,%rsi存储第二个参数等。其次,若有多个参数,则将参数存储在栈中,如图8所示,调用函数前都有mov指令,设置不同的参数,最典型的就是调用atoi和sleep函数前都将寄存器%rax中的值传送到储存第一个参数的寄存器%rdi中,而像getchar这样没有参数的函数,则无需执行以上操作。
再如图9所示,C语言中main函数返回0,那么汇编代码中则对应为“movl $0, %eax”,“leave”,“ret”,即将0传送到%eax,然后使用leave恢复调用者栈帧,清理被调用者栈帧,最后使用ret指令返回。
3.4 本章小结
本章围绕编译操作展开,首先介绍了编译的概念和作用,之后在Linux中将hello.i编译成hello.s,并对编译后的hello.s进行分析。
第4章 汇编
4.1 汇编的概念与作用
4.1.1 概念
汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。
4.1.2 作用
将汇编指令翻译成机器可以直接读取分析的机器指令,即二进制,用于后续的链接。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
在Linux 终端中输入“readelf -a hello.o > ./elf.txt”这一命令,查看hello.o的elf文件。
首先是ELF头,如图11所示包括16字节标识信息、文件类型、机器类型、节头表偏移、节头表的表项大小以及表项个数。
其次是节头表,节头表是ELF可重定位目标文件中最重要的部分内容,如图12所示,节头表描述每个节的节名、在文件中的偏移、大小、访问属性、对齐方式等。
重定位节是一个.text节中位置的列表,包含.text节中需要进行重定位的信息,在链接时用于重新修改代码段的指令中的地址信息。如图13所示,需要重定位的信息有puts,exit,printf,atoi,sleep,getchar及.rodata中的模式串。
.symtab存放在程序中定义和引用的函数和全局变量(符号表)信息,如图14所示,包括Value、Size、Type、Bind、Vis、Ndx、Name等信息。
4.4 Hello.o的结果解析
在Linux 终端中输入“objdump -d -r hello.o > hello_o2s.s”这一命令,查看hello.o的反汇编文件并分析。
首先如图15所示,hello.s中的操作数用十进制表示,而 hello.o的反汇编代码中的操作数用十六进制表示。
如图16所示,hello.s在跳转时,跳转指令(je)跟的是段名(.L2),而hello.o的反汇编代码则使用的是十六进制相对地址。
如图16所示,hello.s在函数调用时,函数调用指令(call)跟的是函数名(puts),而hello.o的反汇编代码则使用的是十六进制相对地址。
汇编语言与机器语言组织形式不同,hello.s开头包含常量、变量的描述,而hello.o的反汇编代码则在每条指令前还有相对应的机器码的十六进制表示,且没有变量和常量的描述。其余两者基本一致,每条指令都是一一对应关系。
4.5 本章小结
本章围绕编译操作展开,首先介绍了汇编的概念和作用,之后在Linux中将hello.s汇编成hello.o,查看并分析了hello.o的elf文件以及对比hello.s与hello.o的反汇编代码的区别及映射关系。
第5章 链接
5.1 链接的概念与作用
5.1.1 概念
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被到内存并执行。
5.1.2 作用
链接器使得分离编译成为可能。不用将一个大型的应用程序组织为巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
在Linux 终端中输入“readelf -a hello > hello_elf.txt”这一命令,查看hello的elf文件。
首先是ELF头,如图19所示,与hello.o的ELF头相似,不做过多描述。
如图20所示,节头表是描述目标文件的节,包含了26个节的名字、类型、地址、偏移量等信息,比hello.o多出了13个节。
如图21所示,程序头表是一个结构数组,反映可执行文件的连续的片被映射到连续的内存段的内存段分的映射关系。
如图22所示的节段映射,说明了在链接过程中,将多个代码段与数据段分别合并成一个单独的代码段和数据段,并根据段的大小以及偏移量重新设置各个符号的地址。
在hello的ELF格式后面,还有动态节,重定位节,符号表,版本信息等内容,这里就不一一列出。
5.4 hello的虚拟地址空间
在Linux下使用edb加载hello,查看hello进程的虚拟地址空间各段信息。例如在ELF格式中,如图23显示.rodata段的地址为402000,那么在edb的Data Dump中找到地址0x402000并查看,如图24所示,可以看到printf里面的内容“Hello %s %s”,以此类推,还可以看到各个段详细信息。
5.5 链接的重定位过程分析
在Linux 终端中输入“objdump -d -r hello > hello_objdump.s”这一命令,查看hello的反汇编文件并分析。
首先,在hello的反汇编文件中,相比只有.text节和main函数的hello.o的反汇编文件,多了更多的节和函数,例如图25所示的.init节和_init函数,同时printf、puts等函数也有了具体的代码,说明在链接过程中,加入了代码中调用的一些库函数。
其次,如图26所示hello的反汇编文件从0开始的相对地址在hello.o的反汇编文件中变成了从0x40100虚拟地址。
最后,如图27所示,hello的反汇编文件在跳转、函数调用时没有重定位条目,而是直接使用虚拟地址,说明重定位的一个过程是链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。其调用与跳转的各个子程序名如下表。
序号 | 子程序名 |
1 | ld-2.31.so!_dl_start |
2 | ld-2.31.so!_dl_init |
3 | hello!_start |
4 | hello!__libc_csu_init |
5 | hello!_init |
6 | hello!main |
7 | hello!_dl_relocate_static_pie |
8 | hello!puts@plt |
9 | hello!exit@plt |
10 | hello!printf@plt |
11 | hello!sleep@plt |
12 | hello!getchar@plt |
13 | hello!__libc_csu_fini |
5.7 Hello的动态链接分析
在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它;为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。其中PLT是一个数组,其中每个条目是16字节代码。PLT [0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT [0]和GOT [1]包含动态链接器在解析函数地址时会使用的信息。GOT [2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
通过elf文件中的描述找到.got.plt所在的内存位置,观察其值的变化,如图28、29所示。在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问跳转时GOT地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。
5.8 本章小结
本章围绕链接操作展开,首先介绍了链接的概念和作用,之后在Linux中将hello.o链接得到hello,查看并分析了hello的elf elf格式、虚拟地址空间,并以该程序为例,解析了链接的重定位过程、执行流程、动态连接过程。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 概念
进程指程序的一次运行过程。更确切的说,进程是具有独立功能的一个程序关于某个数据集合的一次运行活动,因而进程具有动态含义。同一个程序处理不同的数据就是不同的进程。进程是OS对CPU执行的程序的运行过程的一种抽象。进程有自己的生命周期,它由于任务的启动而创建,随着任务的完成(或终止)而消亡,它所占有的资源也随着进程的终止而释放。
6.1.2 作用
进程提供给应用程序的关键抽象——一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器;一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 作用
shell是一个交互型应用级程序,代表用户运行其他程序。shell提供了一个界面,用户通过访问这个界面访问操作系统内核的服务。Bash则是Linux操作系统缺省的shell。
6.2.2 处理流程
1.从终端读入输入的命令;
2.将输入字符串切分获得所有的参数;
3.如果是内置命令则立即执行;
4.否则调用相应的程序执行;
5.shell 应该接受键盘输入信号,并对这些信号进行相应处理。
6.3 Hello的fork进程创建过程
在命令行键入hello运行命令,shell判断该命令是可执行文件,于是调用fork 函数创建一个新的运行的子进程,子进程中,fork返回0;父进程中,返回子进程的PID;新创建的子进程几乎但不完全与父进程相同——
1.子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本;
2.子进程获得与父进程任何打开文件描述符相同的副本;
3.最大区别:子进程有不同于父进程的PID。
6.4 Hello的execve过程
fork函数创建完进程后,调用execve函数到目标路径中寻找hello文件,并创建一个内存映像,为该程序的栈区域创建新的区域结构将可执行文件的片复制到代码段和数据段等。然后为共享库建立映射空间。最后设置当前进程上下文的程序计数器,将其指向入口函数,并将控制传递给新程序的主函数。
6.5 Hello的进程执行
控制寄存器中有一个模式位,当设置了模式位时,进程就运行在内核模式中,此时可以执行指令集中的任何模式,并且可以访问系统中的任何内存位置;没有设置模式位时,进程就运行在用户模式中,不允许执行如停止处理器、改变模式位或发起一个I/O操作等特权指令。
运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法时通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把内核模式改回到用户模式。
内核为每个进程维持一个上下文。上下文就是内核重新启动一个被抢占了的进程。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换——①保存当前进程的上下文,②恢复某个先前被抢占的进程被保存的上下文,③将控制传递给这个新恢复的进程。
在hello进程中,调用sleep函数或者getchar函数都需要进行上下文切换,由用户模式切换到系统模式。
6.6 hello的异常与信号处理
在Linux终端运行hello并键盘输入ctrl+C(发送一个SIGINT信号给hello进程,使hello终止)、ctrl+Z(发送一个SIGTSTP信号给前台程序,即hello,使其被挂起)、ps(列出当前所有进程)、jobs(显示当前暂停的进程)、pstree(查看进程树)、fg(使hello任务在前台继续进行)、kill(发送一个SIGINT信号给hello进程,使hello终止)指令并查看对应结果,结果如图30-32所示。
摘 要
本文主要阐述了hello程序在Linux系统下的生命周期,hello程序在经过预处理、汇编、编译、链接生成可执行文件的全过程。并结合上课学习的知识详细阐述计算机系统对hello进行进程管理、储存管理以及I/O管理,探索hello生命周期的探索。
关键词:预处理;汇编;编译;链接;进程管理;储存管理;I/O管理
第1章 概述
1.1 Hello简介
P2P(from program to process):
在Linux环境下,hello的源程序,即hello.c经过cpp的预处理得到hello.i文件、经过cc1编译生成hello.s的汇编文件、经过as的处理成为可重定位目标文件hello.o、最后由ld链接生成可执行文件hello。通过shell输入./shell,shell通过fork函数创建了一个子进程,子进程执行execve函数加载hello。至此,完成由源程序转换为被执行进程的过程。
O2O(from zero-0 to zero-0):
在execve执行hello程序后,内核为hello进程映射虚拟内存。在hello进入程序入口后,hello相关的数据就被内核加载到物理内存中,hello程序开始正式被执行。为了让hello正常执行,内核还需要为hello分配时间片、逻辑控制流。最后,当hello运行结束,终止成为僵尸进程后,由shell负责回收hello进程,删除与hello有关的数据内容。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位
开发与调试工具:gcc,vim,edb,readelf,HexEdit
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.i 预处理之后的程序(文本)
hello.s 汇编语言程序(文本)
hello.o 可重定位目标程序(二进制)
hello 可执行目标程序(二进制)
1.4 本章小结
本章概述了hello从源程序到可执行程序的过程以及shell执行hello的过程,列出了环境以及中间结果信息。
第2章 预处理
2.1 预处理的概念与作用
预处理:
预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。
预处理作用:
合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。以#includ为例,预处理器(cpp)把程序中声明的文件复制到这个程序中,具体到hello.c的就是#include <unistd.h> 、#include <stdlib.h>、#include<stdio.h>。cpp把这些头文件的内容插入到程序文本中,方便编译器进行下一步的编译。结果就是得到了另一个c程序,通常得到的程序以.i作为文件扩展名。
2.2在Ubuntu下预处理的命令
Linux中hello.c文件进行预处理的命令是:gcc hello.c -E -o hello.i
应截图,展示预处理过程!
图 1 2.2
2.3 Hello的预处理结果解析
图 2 2.3-1,2
经过预处理后hello.c文件转化为hello.i文件,打开后发现内容增加到3000+行,且仍为可以阅读的C语言程序文本文件。对原文件中的宏进行了宏展开,头文件中的内容被包含进该文件中。例如声明函数、定义结构体、定义变量、定义宏等内容。另外,如果代码中有#define命令还会对相应的符号进行替换。
2.4 本章小结
本章介绍了预处理的相关概以及作用,并展示在ubuntu下用gcc对.c文件进行预处理,分析文本的前后变化。
第3章 编译
3.1 编译的概念与作用
编译:
编译是指利用编译程序从预处理文本文件(.i)产生汇编程序(.s)的过程。这个过程就是把完成预处理后的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。
编译的作用:
编译程序的基本功能是把源程序(高级语言)翻译成目标程序。除了基本功能之外,编译程序还具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用以及人际联系等重要功能。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
在ubuntu下对.i文件进行编译的指令是:gcc -S hello.i -o hello.s
应截图,展示编译过程!
图 3 3.2-1
3.3 Hello的编译结果解析
3.3.0 汇编代码
图 4 3.3.0-1,2,3
.file:声明源文件
.text:代码节
.section:
.rodata:只读代码段
.align:数据或者指令的地址对其方式
.string:声明一个字符串(.LC0,.LC1)
.global:声明全局变量(main)
.type:声明一个符号是数据类型还是函数类型
3.3.1 数据
1.字符串
程序中有两个字符串,由上图可知,这两个字符串都在只读数据段中,分别如图所示:
图 5 3.3.1-1
2.局部变量i
局部变量i被放在栈中,通过rbp的相对偏移来访问。
图 6 3.3.1-2
3. 主函数传递参数argc、argv
符号数argc和字符型数组指针argv,根据寄存器使用规则,这两个参数分别通过%edi和%esi传递。在程序最开始,为main函数建立栈帧,并完成参数传递。argc存放于%rsp-20;argv作为main函数的参数,数组的元素都是指向字符类型的指针,起始地址存放在栈中-32(%rbp)的位置,被两次调用找参数传给printf。
图 7 3.3.1-3,4
3.3.2全局函数
图 8 3.3.2-1
从 hello.c 可以看出 hello.c 描述并编译了全局函数 int main( intargc, char*argv[])后,将main函数中使用的字符串常量也保存到数据区域。
3.3.3赋值操作
程序中的赋值操作主要有:i=0这条赋值操作在汇编代码主要使用mov指令来实现,而根据数据的类型又有好几种不一样的后缀
movb:一个字节
movw:两个字节
movl:四个字节
movq:八个字节
图 9 3.3.3-1
处理c语言中翻译过来的赋值操作,在汇编代码中还有通过以lea(地址传递)来赋值的方式。
图 10 3.3.3-2
3.3.4算术操作
c代码中的算术操作有i++,在汇编代码中是通过add来实现的:
图 11 3.3.4-1
除了c代码中的算数操作,在汇编代码中还有通过sub(减操作):
图 12 3.3.4-2
3.3.5关系操作
(1) argc! = 3; 判断条件句子的条件: 编译 argc! = 3时,本命令语如下: cmpl $3,-20 (% rbp ) , 此命令包含代码设置, 并判断是否需要分区 。
图 13 3.3.5-1
2)i<8, hello.c以判断循环为条件,在汇编代码中编译如下: 计算cmpl $7,-4(% rbp), i-7后设置条件代码, 准备在下一个 jle 利用条件代码跳跃。
图 14 3.3.5-2
3.3.6控制转移指令
在c源程序中的控制转移有if语句和for循环,在汇编代码中这两者都是通过条件跳转指令来完成的。如图:
图 15 3.3.6-1,2
3.3.7函数操作
在64位系统中,参数的传递首先是通过寄存器,顺序是rdi、rsi、rdx、rcx、r8、r9,其余参数压栈。
函数的调用使用的是call语句,如果用立即数驱动call语句就必须要计算函数所在位置和rip的相对偏移量。
图 16 3.3.7-1,2,3,4
可以看到,我们汇编代码中的函数调用用的都是call加上函数名的方法。
函数一般都通过ret指令返回,返回去往往要通过leave函数等方式进行堆栈平衡,返回值一般都存放在rax中,如图:
图 17 3.3.7-5
3.3.8类型转换
Sleep函数的参数为int值,而argv为字符串数组,在hello.c中用atoi将字符串转化成int型,在hello.s中用call语句调用atoi函数强制处理该类型转换。
图 18 3.3.8-1
3.4 本章小结
本章主要讲述了在编译阶段,编译器如何处理各种操作与数据,以及c语言种各种操作和类型对应的汇编语言,帮助我们理解汇编语言与c语言之间的转化。
第4章 汇编
4.1 汇编的概念与作用
汇编指的是汇编器ad将.s文件翻译成机器语言,并将其生成后缀名为.o的可重定位目标文件的过程。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
gcc hello.s -c -o hello.o
应截图,展示汇编过程!
图 19 4.2-1
4.3 可重定位目标elf格式
(1) ELF Header : 用命令 : readelf -h hello.o
ELF Header: 以 16B 序列 Magic 启动, 说明 Magic 创建此文件的系统的字体大小和字节顺序 。 ELF 头条的剩余部分包括链接器语法分析和#标题显示对象文件的大小、目标文件的种类、机器类型、字节头桌的文件偏移、节点头桌的项目大小和个数等。
图 20 4.3-1
(2) Section Headers:用命令:readelf -S hello.o
Section Headers:节头部表,包含了文件中出现的各个节的语义,包括节 的类型、位置和大小等信息。 由于是可重定位目标文件,所以每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,同时可以观察到,代码是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。
图 21 4.3-2
(3)查看符号表.symtab :命令readelf -s hello.o
.symtab: 存放程序中定义和引用的函数和全局变量的信息。name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。
图 22 4.3-3
(4)重定位节.rela.text
重定位节 :. text 节点位置列表 。 当目标文件和链接将其它文件组合起来时, 您必须更改 。
重定位节. rela.text 的符号信息 :
Offset: 包含要更改的引用节的偏移Info: symbol 和 type 两个部分 。 symbol是前四节拍,type是后四节拍,
symbol: 更改了标志, 指向了它们的符号,
type: 初始化类型
Type: 链接提示您如何修改新程序 。
Attend: 有符号常数。部分重整行用于更改引用值Name: 重命名目标 。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
图 23 4.4-1,2,3
汇编的代码和hello.s比较的结果,汇编的命令语没有区别。 只是汇编代码不仅标注了汇编代码,还标注了机器代码。 机器语言软件是李镇洙机器的命令语集合,是纯粹的李镇洙数据所表现的语言,是计算机能够正确识别语言。机器指令由操作代码和操作数组成,汇编语言是直接表现CPU动作的形成,是最接近CPU运转原理的语言。每个汇编操作代码都可以用机器的二进制数据来表示,进而可以使所有的汇编(操作代码和操作数)和二进制语言建立一个个映射的关系,因此可以将汇编转换成机器语言。
(1)分支转移: 汇编的移动指令是。 L3, 不是短路,而是汇编时容易写出的帮助,因此汇编语言后就不存在了。
(2)函数调用: 如果函数在.s 文件中呼出, 函数名称将保持原样, 反向编程程序中的 call 目标地址是当前命令 。这是因为从 hello.c 呼出的函数是共享库中的函数, 因此只能通过动态链接来执行, 当汇编成为汇编语言时, 对于这个不确定的函数呼出, call 命令的相对地址设置为 0, 并在下一个命令中设置下列命令 。 在 rela.text 栏目中再次添加静态链接, 然后等待下一个链接 。
4.5 本章小结
本章对hello.s进行了汇编,生成了hello.o可重定位目标文件,并且分析了可重定位文件的ELF头、节头部表、符号表和可重定位节,比较了hello.s和hello.o反汇编代码的不同之处,分析了从汇编语言到机器语言的一一映射关系。
第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代 码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。
链接是由叫做链接器的程序执行的。链接器使得分离编译成为可能,极大地方便了模块化编程。
注意:这儿的链接是指从 hello.o 到hello生成过程。
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
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
图 24 5.2-1
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.3.1 ELF Header
图 25 5.3.1-1
可以看到,可执行文件的ELF头与可重定位目标文件的ELF头有以下几个不同:
1.可执行文件的类型不再是REL而是EXEC。
2.程序的入口点不一样,因为连接上了库文件,使得main函数不再是从0x0开始。同理节头的开始位置也发生了变化。
3.节头的数量产了变化。
5.3.2节头部表Section Headers
图 26 5.3.2-1
可以看到与hello.o不同,在可执行文件中经过重定位每个节的地址不再是0,而是根据自身大小加上对齐规则计算的偏移量。
比如.hash的地址,计算方式是.note.ABI-tag的地址0x40021c加上.note.ABI-tag的大小0x20,得到0x40023c,再对.hash要求的8字节对齐进行调整,得到最终地址0x40024。
5.3.3符号表.symtab
图 27 5.3.3-1
在可执行文件中多出了.dynym节。这里面存放的是通过动态链接解析出的符号,这里我们解析出的符号是程序引用的头文件中的函数。
5.3.4重定位节
图 28 5.3.4-1
重定位节的偏移量与hello.o已经完全不一样了。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
通过查看edb,看出hello的虚拟地址空间开始于0x400000,结束与0x400ff0:
图 29 5.4-1,2
通过5.3.2的节头部表可以找到各节信息,例如:
.text节是从0x4010f0开始的:
.rodate节是从0x402000开始的:
图 30 5.4-3,4
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
(1)在hello.o中call、jmp指令后紧跟着的是相对地址,而hello中紧跟的是虚拟内存的确定地址,原因在于链接器完成了重定位过程,可以确定运行时的地址
图 31 5.5-1
(2)在hello中增加了一些在hello.o中没有的函数,这些都是在hello.c中没有定义却直接使用的函数,这些函数定义在共享库中,在链接时完成了符号解析和重定位,如printf、sleep等。
图 32 5.5-2
综上所述,重定位的大体过程是链接器ld将所有链接文件中相同的节合并,并按照要求计算新的偏移地址赋值给新的节。同时链接器按链接指令的顺序搜索符号表,查找符号引用。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
子程序名 地址
ld-2.31.so!_dl_start 0x7f8586bea770
ld-2.31.so!_dl_init 0x7f8586bea9a0
hello!_start 0x4010f0
libc-2.31.so!_libc_start_main 0x7f634a96ce60
hello!printf@plt 0x401040
hello!sleep@plt 0x401080
hello!getchar@plt 0x401050
libc-2.31.so!exit 0x7f634a7540d0
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
动态链接采用了延迟加载的策略,即在调用函数时才进行符号的映射。使用偏移量表GOT和过程链接表PLT的协同工作实现函数的动态链接。GOT中存放函数目标地址,PLT使用GOT中的地址跳转到目标函数。
程序开始后,通过执行dl_init可以修改PLT和GOT,下面是执行dl_init之前的PLT内容:
图 33 5.7-1
执行dl_init之后的内容:
图 34 5.7-2
从图中第二行可以看到变化。
5.8 本章小结
本章介绍了链接的概念和作用,详细介绍了hello.o如何成为可执行的目标文件,详细介绍了hello.o的ELF形式和各节的意义,分析了hello的虚拟地址空间、重置进程、运行进程和动态链接过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情 况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数 据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动 过程调用的指令和本地变量。
作用:进程为用户提供了以下假象:
(1) 我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。
(2) 处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。
其基本功能是解释并运行用户的指令,重复如下处理过程:
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
(4)如果不是内部命令,调用fork( )创建新进程/子进程
(5)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(6)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait…等待作业终止后返回。
7)如果用户要求后台运行(如果命令末尾有&号),则shell返回;
6.3 Hello的fork进程创建过程
终端程序通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的 逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
6.4 Hello的execve过程
当创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,加载并运行需要以下几个步骤:
(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
(2)映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
(3)映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
(4)设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
进程提供给应用程序的抽象:
(1) 一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器
(2) 一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用CPU内存。
hello进程的执行是依赖于进程所提供的抽象的基础上,下面阐述操作系统所提供的的进程抽象:
①逻辑控制流::一系列程序计数器 PC 的值的序列叫做逻辑控制流,进程是轮流 使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占 (暂时挂起),然后轮到其他进程。
②并发流:一个逻辑流的执行时间与另一个流重叠,成为并发流,这两个流成为并发的运行。多个流并发的执行的一般现象成为并发。
③时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
④私有地址空间:进程为每个流都提供一种假象,好像它是独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个内存字节是不能被其他进程读或者写的,在这个意义上,这个地址空间是私有的。
⑤用户模式和内核模式::处理器通常使用一个寄存器提供两种模式的区分,该寄 存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的 代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任 何命令,并且可以访问系统中的任何内存位置。
⑥上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。
⑦上下文切换:当内核选择一个新的进程运行时,则内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程:
1) 保存以前进程的上下文
2)恢复新恢复进程被保存的上下文,
3)将控制传递给这 个新恢复的进程 ,来完成上下文切换。
以hello为例,hello程序在调用了sleep程序后会陷入内核状态,内核可能会进行上下文切换。程序运行到getchar的时候,内核也会进行上下文切换,让其他进程运行。除了这些,系统还会为hello程序分配时间片,即使没有执行到getchar或者sleep函数,只要hello时间片被用完,系统就会判断当前程序以及执行够久了,从而进行上下文切换,将处理器让给其他进程。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
(1)异常和信号异常可以分为四类:中断、陷阱、故障、终止。异常的同步异步指的是异常的发生和程序的关系。比如从键盘输入crtl+c作为异步异常与程序的执行没有关系。而缺页异常这样的同步异常是随着程序的执行产生的。
图 35 6.6-1
(2)键盘上各种操作导致的异常
1.正常运行结果:
图 36 6.6-2
2.按下ctrl+z结果:
图 37 6.6-3
Ctrl+z效果是挂起前台作业,hello进程运行在后台
图 38 6.6-4
输入ps命令可发现,hello进程并未被回收,且在后台工作号为1,输入fg 1命令将hello进程调至前台,此时 shell 程序首先打印 hello 的命令行命令, hello 继续运行打印剩下的“hello 120L021504 乔江洋”,程序结束,再次输入ps命令,发现进程被回收。
图 39 6.6-5
3.按下ctrl+c结果:
图 40 6.6-7
输入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业,用ps查看前台进程组发现没有hello进程。
图 41 6.6-8
4.胡乱输入结果:
图 42 6.6-9
不停乱按,可以发现,乱按只是将屏幕的输入缓存到 stdin,当 getchar 的时候读出一个“\\n”结尾的字串(作为一次输入),其他字串会当做 shell 命令行输入。
6.7本章小结
在本章中,阐述进程的定义与作用,同时介绍了 Shell 的一般处理流程和作用,并且着重分析了调用 fork 创建新进程,调用 execve函数 执行 hello,hello的进程执行,以及hello 的异常与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:逻辑地址指的是在汇编代码中通过偏移量+段基址得到的地址,与物理地址不同。在hello反汇编代码中我们能够看到的就是逻辑地址。
线性地址:线性地址就是虚拟地址,具体见下。
虚拟地址:虚拟地址是逻辑地址计算后的结果,同样不能直接用来访存,需要通过MMU翻译得到物理地址来访存。在hello反汇编代码计算后就能得到虚拟地址。
物理地址:计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每个字节都有一个唯一的物理地址。第一个字节的地址为0,写下来的字节地址为1,再下一个为2,以此类推。虚拟地址通过MMU翻译后得到物理地址。在hello中通过翻译得到的物理地址来得到我们需要的数据。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,表示具体的是代码段寄存器还是栈段寄存器抑或是数据段寄存器。
图 43 7.2-1
索引号就是“段描述符(segment descriptor)”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这句话很关键,说明段标识符的具体作用,每一个段描述符由8个字节组成。
Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。那究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT,GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址也就是虚拟地址,我们一般通过页表来获得虚拟地址到物理地址的映射。
页表是一个关于页表条目PTE的数组。页表条目由有效位和物理页号组成。
一个虚拟页只有如下三个状态:
未分配的:VM系统还未分配(或者创建)的页。未分配的块没有任何数据和它们相关联,因此也就不占用任何内存。
缓存的:当前已缓存在物理内存中的已分配页。
未缓存在虚拟内存中的已分配页。
如图:
图 44 7.3-1
结合以上两点,我们就可以按下图的方法通过页表将虚拟页映射到物理页上。
图 45 7.3-2
接下来我们来讨论地址的翻译,由于接下来要分析多级页表,因此在这里我只论述一级页表的情况。
我们将n为的虚拟地址拆分成p为的虚拟页面偏移VPO和n-p位的VPN。我们通过VPN找到页表,并通过页表来获得虚拟页号,将m-p位的物理页号和p位的虚拟页面偏移组合在一起(虚拟页面偏移等价于物理页面偏移,因为物理内存映射的是虚拟内存的一整页。)就得到了m位的物理地址。如图:
图 46 7.3-3
7.4 TLB与四级页表支持下的VA到PA的变换
在 Intel Core i7 环境下研究 VA 到 PA 的地址翻译问题。前提如下: 虚拟地址空间 48 位,物理地址空间 52 位,页表大小 4KB,4 级页表。TLB 4 路 16 组相联。CR3 指向第一级页表的起始位置(上下文一部分)。 解析前提条件:由一个页表大小 4KB,一个 PTE 条目8B,共 512 个条目,使 用 9 位二进制索引,一共 4 个页表共使用 36 位二进制索引,所以 VPN 共 36 位, 因为 VA 48 位,所以 VPO 12 位;因为 TLB 共 16 组,所以 TLBI 需 4 位,因为 VPN 36 位,所以 TLBT 32 位。
如图,CPU 产生虚拟地址 VA,VA 传送给 MMU,MMU 使用前 36 位 VPN 作为 TLBT(前 32 位)+TLBI(后 4 位)向 TLB 中匹配,如果命中,则得到 PPN (40bit)与 VPO(12bit)组合成 PA(52bit)。 如果 TLB 中没有命中,MMU 向页表中查询,CR3 确定第一级页表的起始地 址,VPN1(9bit)确定在第一级页表中的偏移量,查询出 PTE,如果在物理内存 中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查 询到 PPN,与 VPO 组合成 PA,并且向 TLB 中添加条目。如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。
图 47 7.4-1
7.5 三级Cache支持下的物理内存访问
Cache的访问并不复杂,对Cache的访问需要把一个物理地址分为标记、组索引、块偏移三个部分。首先我们通过组索引来找到我们的地址在Cache中所对应的组号,再通过标记和Cache的有效位来判断我们的内容是否在Cache中。若命中则通过块偏移读取我们要的数据,若不命中则从下一级Cache中寻找(下一级Cache不一定真的是Cache,比如对L3来说,它的下一级Cache就是主存)。
先来讨论一级Cahce,Core i7CPU的L1 Cache大小为32kb,每组八路,每个块大小为64字节。通过计算可以得出这个Cahce一共有64组。而我们知道,i7CPU的物理地址是52位,因此我们可以分析出这个Cache对物理地址的划分如图:
图 48 7.5-1
通过MMU将虚拟地址转化成物理地址后,计算机就通过提取中的组索引在L1中搜索组,再通过标记位匹配。如果匹配成功且有效位是1,则将块偏移指向的块中的内容交还给CPU,否则未命中,需要从下一级Cache中在重复上述操作。当我们找到内容后需要将内容写回我们的L1中,如果L1中没有空闲块,即有效位为0的块则需要牺牲一块内容,我们通常采用LRU算法来进行这一过程。对L2、L3的访问也是这样,因此就不再赘述。
7.6 hello进程fork时的内存映射
当 fork 函数被 shell 进程调用时,内核为新进程创建各种数据结构,并分配给 它一个唯一的 PID,为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只 读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序.加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构.
2.映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的.代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零.
3.映射共享区域, hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内.
4.设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点.
7.8 缺页故障与缺页中断处理
缺页故障:当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。通过查询页表PTE可以知道虚拟页在磁盘的位置。缺页处理程序从指定的位置加载页面 到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中,因此指令可以没有故障的运行完成。故障处理具体流程如图所示:
图 49 7.8-1
7.9动态存储分配管理
7.9.1 动态分配器介绍:
虽然可以使用低级的mmap和munmap函数来创建和删除虚拟内存区域,但是C程序员还是会觉得当运行时需要额外虚拟内存时,用动态内存分配器更方便,也有更好的可移植性。
动态内存分配器维护着一个进程的虚拟内存域,称为堆。堆每个进程,内核维护着一个变量brk,它指向堆的顶部。
分配器有两种基本风格。两种风格都要求应用显示地分配块。它们的不同之处在于由哪个实体负责释放已分配的块。
显式分配器:
要求应用显式地释放任何已分配的块。例如,c标准库提供一种叫做malloc程序包的显式分配器。c程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。c++中的new和delete操作符与c中的malloc和free相当。
隐式分配器:
另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集,例如Lisp,ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
7.9.2 带边界标签的隐式空闲链表分配器
带边界标签的隐式空闲链表与普通的空闲链表不同,一个块除了是由一个字的头部、有效载荷、可能的一些额外的填充组成外,还有一个与头部相同的脚部组成。头部和脚部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是0。因此,我们只需要内存大小的29个高位,释放剩余的3位来编码其他信息。在这种情况中,我们用其中的最低位(已分配位)来指明这个块是已分配的还是空闲的。
头部后面就是应用调用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。需要填充有很多原因。比如,填充可能是分配器策略的一部分,用来对付外部碎片。或者也需要用它来满足对齐要求。
我们称这种结构称为隐式空闲链表,是因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。在带边界标签的隐式空闲链表中,我们的脚部就标记了一个块的结束。
合并的时候分配器就可以通过检查脚部来检查前一块的状态和大小了。
7.9.3 显式空间链表
将空闲块组织为某种形式的显示数据结构是一种更好的方法,因为根据定义,程序不需要一个空闲块的主体,所以实现空闲链表数据结构的指针可以存放在这些空闲块的主体里面。
显式空闲链表是将对组织成双向链表。在每个空闲块的主体中,都包含一个pred(前驱)和succ(后继)指针。
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于空闲链表中块的排序策略。
一种方法是用后进先出(LIFO)的顺序维护链表,将新释放的块放置在链表的开始处。另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。
另一种方法是按照地址顺序来维护链表,其中链表中的每一个块的地址都小于它后一个块的地址,在这种情况下释放一个块需要线性时间的搜索来定位合适的前驱。
7.10本章小结
本章主要介绍了hello的存储器的地址空间,介绍了四种地址空间的差别和地址的相互转换。同时介绍了hello的四级页表的虚拟地址空间到物理地址的转换。阐述了三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m字节的序列:
B0,B1,B2……Bm
所有的 IO 设备(如网路、磁盘、终端)都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都被当做相应文件的读和写来执行。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
8.2.1 Unix I/O接口:
根据8.1中描述的Unix I/O接口的概念,我们可以确定I/O接口需要有如下结构功能:
打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。
改变当前文件的位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。
读写文件:一个读操作就是从文件复制n > 0个字符到内存,从当前文件位置k开始,然后k += n。对给定一个大小为m字节的文件,当k>=m时执行读操作会出发一个称为EOF的条件。
关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。
8.2.2 Unix I/O函数:
1、打开文件函数:int open(char *filename, int flags, mode_t mode);flag参数为写提供一些额外的指示,mode指定了访问权限。
2、关闭文件函数:int close(int fd);fd是打开文件时的返回值。
3、读文件函数:ssize_t read(int fd, void *buf, size_t n);
4、写文件函数:ssize_t write(int fd, const void *buf, size_t n);
8.3 printf的实现分析
首先来看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实际上就是typedef后的char*。而va_list arg = (va_list)((char*)(&fmt) + 4);这句操作实际上就是得到了…中的第一个量。
之后我们调用vsprintf函数。vsprintf函数将我们需要输出的字符串格式化并把内容存放在buf中。并返回要输出的字符个数i。然后调用系统函数write来在屏幕上打印buf中的前i个字符,也就是我们要输出的格式串。
调用write系统函数后,程序进入到陷阱,系统调用 int 0x80或syscall等,通过字符驱动子程序打印我们的线性。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
最后程序返回我们实际输出的字符数量i。
8.4 getchar的实现分析
getchar 的源代码为:
图 50 8.4-1
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成 ASCII 码,保存到系统的键盘缓冲区之中。
getchar 函数落实到底层调用了系统函数 read,通过系统调用 read 读取存储在 键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar 进行封装, 大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数的实现。
结论
用计算机系统的语言,逐条总结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,
进而调用信号处理程序进行处理。
终止: 输出完10遍对应的字符串后,执行getchar,等待用户输入,输 入字符按下回车后,hello进程终止。
回收: hello进程终止后发送SIGCHLD信号给shell,shell将其退出状态 进行回收,最后内核从系统中删除hello所有的信息
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
计算机系统的设计和实现是一门抽象的艺术,从底层的二进制到处理器、内存、I/O设备,一直在强调和运用抽象的概念,将抽象运用到了极致,接触了计算机系统才真正知道,一个简单的hello程序从代码到实现需要经历如此繁杂的步骤,这其中凝聚的是多少代计算机人的智慧和付出。
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c 源程序(文本)
hello.i 预处理之后的程序(文本)
hello.s 汇编语言程序(文本)
hello.o 可重定位目标程序(二进制)
hello 可执行目标程序(二进制)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 深入理解计算机系统(原书第3版)/(美)兰德尔·E.布莱恩特等著;龚奕利,贺莲译.
[2] https://www.cnblogs.com/diaohaiwei/p/5094959.html
[3] https://www.cnblogs.com/pianist/p/3315801.html
[4] https://blog.csdn.net/weixin_44819348/article/details/103772429
以上是关于哈尔滨工业大学计算机系统大作业——程序人生-Hello’s P2P的主要内容,如果未能解决你的问题,请参考以下文章
哈尔滨工业大学计算机系统大作业——程序人生-Hello’s P2P