计算机系统大作业:程序人生-Hello’s P2P
Posted z2984348
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了计算机系统大作业:程序人生-Hello’s P2P相关的知识,希望对你有一定的参考价值。
本文对hello程序的生命周期进行了追踪,在Ubuntu20.04的环境下合理地利用一些工具,并结合计算机系统的相关知识来对hello.c程序进行相应的分析,以期展示出程序生成可执行文件,并最终在计算机内部运行的过程,从而理解程序在计算机内部的运行机制。
关键词:hello程序,文件,计算机系统,程序处理工具
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
P2P:
1.最初通过编辑器编写hello的程序建立.c文件,得到hello.c的源程序。
2.运行C预处理器——cpp将程序预处理生成hello.i文件。
2.1头文件的包含
2.2注释的删除
2.3宏定义的替换
2.4条件编译的选择
3.运行C编译器——ccl将其翻译成相应的后缀为S的汇编文件,这个过 程是将经过预处理之后生成的hello.i文件进一步的翻译,包括词法和语法的分 析,最终生成对应硬件平台的汇编语言。
4.调用汇编器——as将hello.s汇编成一个可重定位目标文件hello.o。
5.运行链接器程序ld将hello.o和系统目标文件组合起来,创建一个可执行目标文件hello,由存储器保存在磁盘中。(如图所示)
6.在bash下输入相关命令后即可调用fork函数为可执行文件创建子进程。
O2O:
1.在bash调用fork函数创建子进程后,调用execve函数来进行虚拟内存映射,通过mmp为hello程序开创了一片空间。
2.随着一连串的缺页故障,将hello加载到物理内存。
3.操作系统提供异常控制流和调度器等工具,为进程规划时间片
4.Unix I/O为其提供与程序员和系统文件交互的方式,让它不再孤单。当程序从main中返回,意味着程序的终止。
5.bash回收子进程,内核清除数据痕迹。
1.2 环境与工具
硬件环境:x64 CPU; 3.00GHz; 16.0GB(15.4GB可用) 机带RAM; 934.8GB存储
软件环境:版本 Windows 10 家庭中文版 版本号 20H2 操作系统内部版本 19042.1586 VMware Workstation 12 ; Ubuntu 20.04 LTS 64位;
开发与调试工具:gcc,gedit,ld,readelf,edb
1.3 中间结果
文件 | 作用 |
hello.c | hello的源程序 |
hello.i | 经过预处理后产生的文件 |
hello.s | 编译后产生的汇编文件 |
hello.o | 汇编后的可重定位目标执行文件 |
hello.elf | hello.o的ELF格式 |
hello_objdump.txt | hello.o反汇编代码文件 |
hello1.elf | hello可执行程序的ELF格式 |
hello1_objdump.txt | hello反汇编代码文件 |
1.4 本章小结
本章对hello的一生进行了简要的介绍和描述,介绍了P2P的整个过程,介绍了本计算机的硬件环境、软件环境、开发工具,介绍了为编写本论文的中间文件的名称和其作用。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:预处理是运行C语言程序的第一个步骤,为编译器核心进一步编译程序提供经过改造后的代码,完成一些编译器无法完成的处理。
作用:预处理过程读入源代码,对其中的伪指令(以#开头的指令)和特殊符号进行处理。或者说是扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。预处理过程还会删除程序中的注释和多余的空白字符。
2.2在Ubuntu下预处理的命令
预处理命令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
1.程序经过预处理之后,生成hello.i文件,文件由23行代码增加到3060行,内容增加,且仍为可以阅读的C语言程序文本文件。
2.实现了模块化的拼接,最开始的一段代码,是hello.c拼接的各种库文件;接着有对结构体的定义;对变量的定义;对内部函数的声明;最后是程序的源代码。
2.4 本章小结
本章介绍了预处理的相关概念和作用,进行实际操作对预处理生成的hello.i文件,是对源程序进行补充和替换的结果。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:指的是从.i文件到.s,即预处理后的文件到生成汇编语言程序的过程
作用:将预处理得到的文件转换为汇编文件。经过预处理得到的输出文件中只有常量,如数字、字符串、变量的定义,以及C语言的关键字,如main,if,else,for,while,,,+,-,*,,等等。编译器通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。可以达成以下效果:
- 语法分析:编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,方法分为两种:自上而下分析法和自下而上分析法
- 中间代码:源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码。
- 代码优化:指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。
- 目标代码:生成是编译的最后一个阶段。目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。此处指汇编语言代码,须经过汇编程序汇编后,成为可执行的机器语言代码。
3.2 在Ubuntu下编译的命令
编译命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1生成的hello.s文件的分析:
.file:声明源文件
.text:代码段
.rodata只读数据段
.global:声明全局变量
.align:声明对指令或者数据存放地址进行对齐的方式
.type:声明一个符号是函数类型还是数据类型
.size:声明大小
.string:声明一个字符串类型
.long:声明一个long类型
3.3.2数据:
1.常量
常数:
例如4作为立即数直接存在汇编代码中
字符串
程序中有两个字符串,这两个字符串都在.rodata节中:
第一个字符串”用法: Hello 120L020709 张瀚清 秒数!\\n”是第一个printf传入的输出格式化参数。第二个字符串是终端输入的存储在argc[]为地址的数组中的”Hello %s %s\\n”,是第二个printf传入的输出格式化参数。
2.全局符号
声明全局符号main,类型为函数
3.局部变量(包括数组,指针)
main函数声明了一个局部变量i,编译器进行编译的时候将局部变量i放在堆栈中。如图所示,局部变量i放在栈上-4(%rbp)的位置。
参数argc
参数argc作为用户传给main的参数,被放到栈中。第一个参数用%edi传递
数组:char *argv[]
char *argv[]是main函数的第二个参数,数组的每个元素都是一个指向字符类型的指针。数组的起始地址存放在栈-32(%rbp)的位置:
被两次调用传给printf
3.3.2赋值操作
hello.c中赋值操作:
for循环中i=0在汇编代码中使用mov指令实现
i++用addl实现
3.3.3算数操作
算术操作:
i++,汇编语言如上图
开辟栈空间用的subq操作:
计算参数地址用到的addq操作:
3.3.4关系操作
argc!=4;是条件判断语句,进行编译时,这条指令被编译为:cmpl $3,-20(%rbp),在比较之后还设置了条件码,根据条件码判断是否需要跳转。
i<8,作为判断循环条件指令被编译为cmpl $7,-4(%rbp),并设置条件码,为下一步jle利用条件码进行跳转做准备。
3.3.5控制转移
汇编语言中先设置条件码,然后根据条件码来进行控制转移,在hello.c中,有以下控制转移指令:
- 判断argc是否等于4,如果argc等于4,则不执行if语句,否则执行if语句,如上
- for循环中,每次判断i是否小于8来决定是否继续循环,如上
先对i赋初值然后无条件条件跳转至判断条件的.L3中,然后判断i是否符合循环的条件,符合直接跳转至循环体内部.L4中。
3.3.6函数操作
调用函数时有以下操作:(假设函数P调用函数Q)
- 传递控制:调用过程Q的时候,程序计数器(%rip)必须设置为函数Q代码的起始地址,然后在返回时,要把程序计数器(%rip)设置为P中调用Q后面那条指令的地址。
- 传递数据:函数P必须能够向函数Q提供一个或多个参数,Q必须能够向P中返回一个值。
- 分配和释放内存:在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。
在x86系统中函数参数的存储规则,第1到6个参数依次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,若有更多的参数则保存在栈中的某些位置。函数的返回值保存在%rax寄存器中。
hello.c中的函数操作:
main函数:参数是int argc,char *argv[]
被call后才能执行:即被系统启动函数__libc_start_main调用,call指令将下一条指令的地址压栈,然后跳转到main函数,完成对main函数的调用,程序结束时,调用leave指令恢复栈空间为调用之前的状态,然后ret返回。
printf/puts函数:第一次参数是字符串,第二次参数是argv[1],argv[2]
第一次:
调用puts,不调用printf,由于在条件分支内部调用的printf输出内容是一个确定的字符串,并不需要格式控制,所以编译器自行将它优化为了puts函数输出字符串。
第二次:
在循环内部,调用printf函数
exit函数:参数是1
sleep函数:参数是atoi(argv[3])(其中再调用atoi函数)
unsigned int sleep(unsigned int seconds);
int atoi(const char *str);
将argv字符串数组中的第四个元素,也就是命令行的第四个命令转成int类型,将其作为参数传入sleep函数调用。
getchar函数:无参数
3.3.7类型转换
hello.c中atoi(argv[3])将字符串类型转换为整型。
3.4 本章小结
本章主要介绍了编译器处理c语言程序的基本过程,函数从源代码变为等价的汇编代码,编译从多角度进行分析,通过理解编译器编译的机制,我们可以将汇编语言翻译成c语言,提高了反向工程的能力。同时还注意到编译器有一定的优化功能。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:
驱动程序运行汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,将结果保存在目标文件hello.o中,且是一个二进制文件,包含函数main的指令编码。
作用:
将汇编代码转换为机器指令,使其在链接后能被机器识别并执行。
4.2 在Ubuntu下汇编的命令
汇编命令:gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
典型的ELF可重定位目标文件:
生成hello.o文件elf格式命令:readelf -a hello.o > hello.elf
4.3.1 ELF头:
以一个16字节的Magic序列开始,描述了生成该文件的系统的字的大小和字节顺序。ELF64表示ELF64位的可执行程序,下一行表示补码,小端序,目标文件类型为REL(可重定位目标文件),运行机器:X86-64 AMD,节头开始为文件开始1248字节偏移处。除此之外,还包括ELF头大小,节头部表中条目的大小和数量。
4.3.2节头:
记录各节名称、类型、地址(因暂时未被分配从而均为0:用于重定位)、偏移量(节相对于文件开始的偏移)、节大小、表项大小、flags、(与其他节的)关联、附加节信息、对齐方式(2的Align次方)。还可以看到代码段可执行但不可写,数据段和只读数据段不可执行。
4.3.3重定位信息:
.rela.text保存的是.text节中需要被修正的信息,任何调用外部函数或者引用全局变量的指令都需要被修正,即需要重定位,调用局部函数的指令不需要重定位,在可执行目标文件中不存在重定位信息。本程序需要被重定位的是printf、puts、exit、sleepsecs、getchar、sleep、.rodata中的.L0和.L1。
.rela.eh_frame节是.eh_frame节重定位信息。
4.3.4符号表:
符号表(.symtab)存放程序中定义和引用的函数和全局变量的信息。name是符号名称,对应于可重定位目标模块,value是符号在对应节中的偏移量,size为目标大小,type表示函数或者数据的类型,Bind表示是本地的还是全局的。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并与第3章的 hello.s进行对照分析。
反汇编文件:
与hello.s对照分析:
两者代码总体上基本相同,只有小部分差别:
hello.s文件中只显示了汇编代码,而在反汇编代码中显示的不仅仅是汇 编代码,还有机器指令码。
1.跳转指令:
汇编用的是段名称,比如.L3,只是在汇编语言中便于编写的助记符,不 是核心代码,转换成机器语言后消失:
可重定位文件用的是确定的地址:主函数+段内偏移量(main+0x80):
2.函数调用:
汇编调用call后跟的是函数名称:
可重定位文件call后跟的是一条重定位条目指引的信息(下一条指令), 由于这些函数都是共享库函数,地址是不确定的,最终需要链接器才能确 定函数的地址,因此call指令将相对地址设置为全0,然后在.rela.text节中为 其添加重定位条目,等待链接的进一步确定:
3.全局变量:
汇编中全局变量sleepsecs的格式是段地址+%rip:
可重定位文件中全局变量的格式是0+%rip,因为.rodata节中的数据是在 运行时确定的,需要重定位,现在填0占位,并在.rela.text节中为其添加 重定位条目
4.立即数:
汇编语言中用十进制值:
可重定位文件中用十六进制值,与二进制之间的转换更加方便:
4.5 本章小结
本章对hello.s进行了汇编,生成了hello.o可重定位目标文件,分析了ELF文件的各个部分,最后用反汇编的方式分析了汇编前后的不同之处,对汇编的过程进行了分析。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:
链接是将各种代码和数据片段收集并合并成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。可以执行于编译时,也就是源代码被翻译成机器代码时;也可以执行于加载时,即程序被加载到内存并执行时;也可以执行于运行时,也就是由应用程序来执行。
作用:
将多个可重定位文件整合到一起,修改符号和引用,得到一个可执行文件。
使得分离编译成为可能,不用将大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小,更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它并重新链接应用,而不必重新编译其他文件。
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的格式
生成ELF格式文件命令:readelf -a hello > hello1.elf
5.3.1ELF头:
内容与hello.o的ELF头大致相同
不同:
文件类型变为EXEC(可执行文件);
完成链接后入口点地址确定为0x4010f0;
节点数变成了27个。
5.3.2节头:
与hello.o的节头大致相同,不过多出了部分节:
.gnu.hash:表示符号的哈希表,用于加速查找符号
.dynsym,.dynstr,.dynamic:与动态链接符号相关
.gnu.version,.gnu.version_r:与版本有关的信息
.init,.fini:存放上述初始化和收尾的代码
.plt,.plt.sec,.got.plt:与位置无关代码有关,PLT全称为Procedure Linkage Table(过程链接表),GOT全程为Global Offset Table(全局偏移量表)
5.3.3重定位节:
5.3.4符号表:
可执行文件的符号表中多了很多符号,而且额外有一张动态符号表(.dynsym)。printf、puts、getchar等C标准库函数在动态符号表和符号表中都有表项。此外与可重定位目标文件不同的是,这些符号已经确定好了运行时位置。
5.4 hello的虚拟地址空间
使用edb打开hello可执行文件:
分析程序头LOAD可加载的程序段的地址为0x400000
从Data Dump窗口能观察hello加载到虚拟地址的状况:
用Goto Expression工具能跳转到需要的位置,查看各段信息:
在0x400000~0x401000段中,程序被载入,虚拟地址从0x400000开始,到0x400fff结束,根据5.3节中的节头部表可以找到任意一个节的信息。例如,由节头表得到.interp段的地址为0x4002e0,然后在edb中查找0x4002e0处的内容,显示为动态链接器:
5.5 链接的重定位过程分析
反汇编hello文件命令:objdump -d -r hello > hello1_objdump.txt
hello与hello.o的不同:
1.可执行文件完成了重定位,所以地址是虚拟地址:
可重定位文件未完成重定位,地址为相对偏移地址。
2.在5.3中已有论述,可执行文件比可重定位文件多了许多节
3.可执行文件的反汇编代码中增加了许多外部链接的共享库函数,比如puts@plt,exit@plt,sleep@plt,getchar@plt共享库函数等。
链接过程分析:
第一步,符号解析:在这一步中将代码中的每个符号引用和正好一个符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。
第二步,重定位节和符号定义:在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,来自所有输入模块的.data节被全部合并成一个节,这个节成为输出的可执行目标文件的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。
第三步,重定位节中的符号引用:在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中的重定位条目,代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中。重定位算法如下图:
5.6 hello的执行流程
直接使用edb的analyze功能:
分析ld-2.31.so位置,得到链接过程
运行后在hello地址分析,得到函数执行过程:
5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,当程序运行时将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有模块都链接成一个单独的可执行文件。
所谓动态就是不对那些组成程序的目标文件进行链接,等到程序要运行时才进行链接,也就是把链接这个过程推迟到了运行时再进行。对于动态链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要为其添加重定位记录,当动态链接器在程序加载的时候再解析它。为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
在加载时,动态链接器会重定位GOT中的每个条目,使它包含正确的绝对地址,而PLT中的每个函数负责调用不同函数。
观察elf中.got.plt节的内容:
用edb观察地址0x404000:
得到调用dl_init之前0x404008后的16个字节均为0。
dl_init(即开始运行)后观察.got.plt:
发现.got.plt条目变化,出现了两个小端序地址。
5.8 本章小结
本章介绍了链接的概念与作用,对hello的elf格式进行了分析,介绍了hello的虚拟地址空间,重定位过程,执行过程,动态链接过程,更加熟悉链接的过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:
经典定义是一个执行中程序的实例。广义定义是进程是一个具有一定独立 功能的程序关于某个数据集合的一次运行活动。进程不只是程序代码,还包括 当前活动,如PC、寄存器的内容、堆栈、数据段,还可能包含堆。
作用:
它提供一个假象,好像我们的程序独占地使用内存系统,处理器好像是无 间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯 一的对象。
进程提供给应用程序的关键抽象:一个独立的逻辑控制流,提供一个程序 独占处理器的假象;一个私有的地址空间,提供一个程序独占地使用内存系统 的假象。
6.2 简述壳Shell-bash的作用与处理流程
作用:
Linux系统中,Shell是一个交互型应用级程序,为使用者提供操作界面, 接收用户命令,调用相应的应用程序,代表用户运行其他程序。
处理流程:
1.终端进程读取用户由键盘输入的命令行;
2.分析命令行字符串,切分并获取命令行参数,并构造传递给execve的argv 向量;
3.检查第一个命令行参数是否是一个内置的shell命令;
4.如果不是内部命令,调用fork()创建新进程/子进程执行指定程序;
5.在子进程中,用步骤2获取的参数,调用execve()执行指定程序;
6.如果命令末尾没有&号(没要求后台运行),则shell使用waitpid等待作 业终止后返回;
7.如果命令末尾有&号,则shell返回。
6.3 Hello的fork进程创建过程
当我们输入./hello时,shell发现它不是一个内置命令,于是将其判定为可执行程序,shell先创建一个对应./hello的作业,再用fork()创建一个子进程。 子进程几乎但不完全与父进程相同。通过fork函数,子进程得到与父进程用户 级虚拟地址空间相同的但是独立的一份副本,拥有不同的PID。
6.4 Hello的execve过程
当调用fork()函数创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序(hello程序),步骤如下:
1.删除虚拟地址中已存在的用户区域。
2.为新程序建立新的私有的区域结构:虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域:如果hello与共享对象链接,那么这些对象就是被动态链接到这个程序,然后再映射到用户虚拟地址空间中的共享区域。
4.设置程序计数器(PC):exceve做的最后一件事是设置当前进程的上下文中的PC,使之指向代码区域的入口点,下次调用这个进程时就从这个入口点开始执行。
6.5 Hello的进程执行
进程上下文信息:
1)概念:
进程上下文是进程执行活动全过程的静态描述。我们把已执行过的进程指令和数据在相关寄存器与堆栈中的内容称为进程上文,把正在执行的指令和数据在寄存器与堆栈中的内容称为进程正文,把待执行的指令和数据在寄存器与堆栈中的内容称为进程下文。
2)进程上下文切换流程:
1.保存当前进程的上下文。2.恢复某个先前被抢占的进程被保存的上下文。3.将控制传递给这个新恢复的进程。
3)调度:
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被强占的进程。
进程时间片:
一个进程执行他的控制流的一部分的每一个时间段叫做时间片(time slice),多任务也叫时间分片(time slicing)
用户态与核心态转换:
处理器通常使用某个控制寄存器中的一个模式位来提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。该寄存器描述了进程当前享有的特权。当设置了模式位时,进程就运行在内核模式中。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存的位置。没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,也不允许直接引用
hit csapp大作业 程序人生-Hello’s P2P
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021112810
班 级 2103103
学 生 肖芩芩
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年5月
本文遍历了hello.c在Linux下生命周期,借助Linux下系列开发工具,通过对其预处理、编译、汇编等过程的分步解读及对比来学习各个过程在Linux下实现机制及原因。并由操作系统进行进程管理、存储管理和I/O管理的全过程。以此将CSAPP课程中的内容进行全面地总结和梳理,加深对计算机系统的理解。
关键词:1.编译系统;2.Hello程序;3.进程;4.信号与异常;5.虚拟内存;6.I/O管理。
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:From Program to Process。编辑完成的hello.c程序先经过cpp预处理器的预处理得hello.i文件,ccl编译器将其编译获得hello.s文件,as汇编器再将其翻译为机器语言指令获得hello.o文件,再经过ld链接器进行链接得可执行文件hello。shell输入执行命令后,进程管理为其fork()一个子进程。即完成了P2P的过程。
020:From Zero to Zero。进程管理给hello进行execve操作,进行mmap操作将其映射到内存中,接着给运行的hello分配时间片来执行逻辑控制流。当程序运行结束后,父进程会回收hello进程,内核删除相关的数据。即完成了020的过程。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位
开发与调试工具:gcc,gdb,vim,edb,readelf,HexEdi,objdump,ldd等
1.3 中间结果
文件名称 | 文件作用 |
hello.i | 预处理之后文本文件 |
hello.s | 编译之后的汇编文件 |
hello.o | 汇编之后的可重定位目标执行 |
hello | 链接之后的可执行目标文件 |
helloo_objdmp | Hello.o 的反汇编代码 |
helloo.elf | Hello.o 的 ELF 格式 |
hello_objdmp | Hello 的反汇编代码 |
hello.elf | Hellode ELF 格式 |
1.4 本章小结
本章对hello做了总体的介绍,简述了hello的p2p和020过程,列出并介绍了本次实验的环境和工具,阐明了这次实验中产生了中间产物,是本次实验的总领部分,后文将依据本章做详细展开。
第2章 预处理
2.1 预处理的概念与作用
概念:C语言的预处理器在源代码编译之前对其进行一些文本性质的操作。它的主要任务包括删除注释、插入被#include指令包含的文件内容、定义和替换由#define指令定义的符号,同时确定代码的部分内容是否应该根据一些条件编译指令进行编译。
作用:预处理可以在在将c程序转化为s的汇编程序之前对于宏定义处理,方便后续的代码转化,并且对于在汇编中无用的注释进行处理,删去无用部分对后续操作做准备。
- 宏定义:#define 指令定义一个宏,#undef指令删除一个宏定义。
- 文件包含:#include指令导致一个指定文件的内容被包含到程序中。
- 条件编译:#if,#ifdef,#ifndef,#elif,#else和#dendif指令可以根据编译器可以测试的条件来将一段文本包含到程序中或排除在程序之外。
2.2在Ubuntu下预处理的命令
预处理命令:gcc -E hello.c -o hello.i
图1.预处理结果
2.3 Hello的预处理结果解析
图2.hello.i的代码截图
预处理后,文件变成3062行的文本文件,前面为头文件<stdio.h> <unistd.h> <stdlib.h> 的内容被复制插入进代码中,消除include宏,随后是原本的代码。可以看到,预处理后的文件更加完整和清晰,也没有了多余的注释等无用的部分。
2.4 本章小结
本章说明了P2P过程中的预处理部分,深入了解预处理器(cpp)将hello.c进行预处理,生成hello.i文件的过程,并分析其i文件的内容。了解了预处理的大致过程,也分析明确了预处理的重要性。
第3章 编译
3.1 编译的概念与作用
概念:编译就是将源语言经过词法分析、语法分析、语义分析以及经过一系列优化后生成汇编代码的过程。其以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。编译器将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。
作用:
1.语法分析:编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,方法分为两种:自上而下分析法和自下而上分析法。
2.中间代码:源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码。
3.代码优化:指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。
4.目标代码:生成是编译的最后一个阶段。目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。此处指汇编语言代码,须经过汇编程序汇编后,成为可执行的机器语言代码。编译出的目标程序通常还要经历运行阶段,以便在运行程序的支持下运行,加工初始数据,算出所需的计算结果。
3.2 在Ubuntu下编译的命令
编译命令:gcc -S hello.i -o hello.s
图3.编译过程
3.3 Hello的编译结果解析
3.3.1 hello.s文件中的伪指令
伪指令用于指导汇编器和链接器的工作。
内容 | 含义 |
.file | 源文件声明 |
.text | 代码节 |
.section .rodata | 只读代码段 |
.align | 指令或者数据的存放地址进行对齐的方式 |
.global | 声明全局符号 |
.type | 声明符号是数据类型或函数类型 |
3.3.2 rodata节数据
图4.rodata节数据
在.rodata段,我们有两个数据。.LC0存储的是第一条printf语句打印的字符串"用法: Hello 学号 姓名 秒数!\\n"。.LC1存储的是第二条printf语句所打印的字符串,其中的%s是占位符。
3.3.3局部变量
在hello.c程序main函数中定义了局部变量i,用于进行遍历操作。
编译处理会将它存储到用户栈中,可见i被赋予初值0存储在栈中-4(%rbp)的位置。
3.3.4数组
数组char *argv[]是main函数的第二个形式参数,来源于命令行输入的数据,argv是存放char指针的数组。argv数组中一个元素大小为8个字节,我们可以看到在hello.s中2次指令movq (%rax), %rdx与movq (%rax), %rax,是为了解析终端输入的命令参数。
3.3.5 赋值操作
使用数据传送命令,我们可以进行赋值操作。最简单形式的数据传输类型是MOV类,MOV有movb,movw,movl,movq。分别操作1、2、4、8字节的数据。mov操作的源操作数可以是:立即数、寄存器、内存。目的操作数可以是:寄存器、内存。x86-64规定两个操作数不能都指向内存。
3.3.6 算术运算
在循环操作中,使用了++操作符。对应的汇编代码为,对i自加,栈上存储变量i的值加1。
3.3.7 控制转移
在hello.c中,两次输出中包含常量字符串。编译处理时,由于常量字符串被提前声明,在这里输出时使用了控制转移。
图10(1)为argc与4的比较,如果相等则跳转到L2,在原程序里体现为不进入if语句,如果不相等,不跳转继续执行,体现为进入if语句。
图10(2)为for的循环条件,当i小于等于8时,跳转到L4,L4实现的时输出语句的相关操作。
3.3.8 循环
汇编中没有相应的指令存在,可以用条件测试和跳转组合起来实现循环的效果。
图11.循环控制条件
初始时,i为0,存放在栈中-4
3.3.9 类型转换
在语句sleep(atoi(argv[3]));中存在隐式类型转换。atoi函数的返回值是int型,而sleep函数的参数类型是unsigned long,存在着数据由int型向unsigned long型的转换。
3.3.10函数操作
main函数:
参数传递:传入参数argc和argv[],分别用寄存器%rdi和%rsi存储。
函数返回:设置%eax为0并且返回,对应return 0 。
main函数中调用其他函数
图12.函数调用
call表示函数调用,如上图所示,main函数调用了puts函数,exit函数,printf函数,atoi函数,sleep函数。其中exit参数是1,atoi函数的参数是argv[3],sleep函数的参数是atoi(argv[3])的返回值。
3.4 本章小结
本章说明了P2P过程中的编译部分,通过hello.s分析了c语言如何转换成为汇编代码。并对生成的汇编程序中涉及到的C语言各种数据类型和各类操作做了说明。
第4章 汇编
4.1 汇编的概念与作用
汇编概念:驱动程序运行汇编器as,将汇编语言的ascii码文件(这里是hello.s)翻译成机器语言的可重定位目标文件(hello.o)的过程称为汇编。hello.o是二进制文件。
汇编的作用:将汇编代码转变为机器指令,生成目标文件。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
图13.汇编过程
4.3 可重定位目标elf格式
1.ELF头
命令行输入:$ readelf -h hello.o
readelf可以显示ELF文件的相关内容,-h选项表示只显示header信息。
图14.elf头信息
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table) 的文件偏移,以及节头部表中条目的大小和数量。在上述表中,可以看出关于ELF header的长度这里也给出了,一共是64个字节。
2.section表
命令行输入:$ readelf -S hello.o 其中-S选项表示打印整个section表的信息
图15.section表信息
.text 已编译程序的机器代码。
.rodata 只读数据,比如printf语句中的格式串和开关语句的跳转表。
.data 已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中。
.bss 未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分已初始化和未初始化变量是为了空间效率,在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为0。
.symtab: 一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上,每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器中的符号表不同,symtab符号表不包含局部变量的条目。
.rel.text 一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非用户显式地指示链接器包含这些信息。
.rel.data 被模块引用或定义的所有全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。
.debug 一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。只有以-g选项调用编译器驱动程序时,才会得到这张表。
.line 原始C源函数程序中的行号和.text节中机器指令之间的映射。只有以-g选项调用编译器驱动程序时,才会得到这张表。
.strtab 一个字符串标,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以NULL结尾的字符串的序列。
描述目标文件的节 节头部表 不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
3.符号表
命令行输入$ readelf -s hello.o
符号表(.symtab)存放程序中定义和引用的函数和全局变量的信息。name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的
图16.符号表信息
。
4.重定位节
命令行输入$ readelf -r --relocs hello.o
图17.重定位节信息
重定位节(.rela.text): .text 节中位置的列表,包含.text节中需要进行重定位的信息,当链接器把目标文件和其他文件组合时,需要修改这些位置。
重定位节中各项符号的信息:
- Offset偏移量:需要被修改的引用节的偏移
- Info信息:包括符号和类型两个部分,符号在前面四个字节,类型在后面四个字节
- Sym.Value符号值:标识被修改引用应该指向的符号,
- Type类型:重定位的类型
- Addend加数:一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整
- Sym.Name符号名称:重定向到的目标的名称。
4.4 Hello.o的结果解析
反汇编命令:objdump -d -r hello.o
图18.反汇编代码(左)和汇编代码(右)部分内容
可以观察到,二者十分相似。但是许多地方不相同。
- 反汇编代码中不再有汇编代码中的伪节。
- 反汇编代码省去了汇编代码中标志操作数的字节大小的符号
- 在跳转时,汇编代码直接访问了.rodata节的数据,直接按函数名调用了函数,而反汇编代码中二者均是相对偏移地址,需要进行重定位得到绝对地址。
图19.反汇编部分代码
机器语言指的是用二进制代码表示的计算机能直接识别和执行的一种机器指令的集合。图19中的左侧数字序列就是每一条指令的机器代码。上图所示的机器代码是十六进制的。
4.5 本章小结
本章利用汇编操作将汇编语言转化为机器语言,可重定位目标文件已经完成,为下一步链接生成可执行文件做好准备。
第5章 链接
5.1 链接的概念与作用
链接的概念:hello程序调用了printf函数,它存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中。连接器(ld)就负责处理这种合并。结果就得到了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
图20.链接过程
5.3 可执行目标文件hello的格式
输入命令:readelf -a hello查看可执行文件中的信息内容
1.ELF 头部表
图21.ELF Header
hello与hello.o的ELF头大致相同,不同之处在于hello的类型为EXEC可执行文件,表明hello是一个可执行目标文件,有25个字节。
2.节头部表
图22.Section Header
节头部表是描述目标文件的节,各节的基本信息均在其中进行了声明,包括名称,大小,类型,全体大小,地址,旗标,偏移量,对齐等信息等。
3.重定位节
图23.重定位节
4.符号表
图24.符号表
5.4 hello的虚拟地址空间
命令行输入:edb --run hello
观察edb的Data Dump窗口。窗口显示虚拟地址由0x401000开始,到0x402000结束
图25.edb .Data Dump窗口
5.5 链接的重定位过程分析
hello反汇编代码中跳转地址是虚拟地址,完成了重定位(如图26),而hello.o反汇编代码中的是相对偏移地址,未完成重定位的过程。(如图27)
链接的重定位的过程:
重定位节和符号定义链接器将相同类型的节合并,生成ELF节。链接器将运行时的内存地址分配给生成的节,此时程序中每条指令和全局变量都有唯一的运行时地址。要合并相同的节,确定新节中所有定义符号在虚拟地址空间中的地址,还要对引用符号进行重定位,修改.text节和.data节中对每个符号的引用,需要用到在.rel_data和.rel_text节中保存的重定位信息。
首先,我们观察hello.o的反汇编代码。可以观察到,有许多地方并没有填入正确的地址,正等待进行链接。R_X86_64_PLT32表示puts函数需要通过共享库进行动态链接。在hello文件的反汇编代码中,我们发现之前的重定位地址已经被填入了正常的地址。观察elf文件信息,而401090.plt节。
hello是如何进行重定位的:
①首先计算需要被重定位的位置
refptr = .text + r.offset
②然后链接器计算出运行时需要重定位的位置:
refaddr = ADDR(.text) + r.offset
③然后更新该位置
*refptr = (unsigned) (ADDR(r.symbol) + r.addend-refaddr)
5.6 hello的执行流程
0x00007ffff7e16e20 <_init>
0x0000000000401090<_start>
0x0000000000401150<in __libc_csu_init>
0x0000000000401000<_init>
0x00000000004010c5<main>
0x0000000000401030<puts>
0x0000000000401070<exit>
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要为其添加重定位记录,并等待动态链接器处理。为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT 中存放函数目标地址,PLT使用 GO T中地址跳转到目标函数。
dl_init函数调用前后GOT信息变化截图,经动态链接,GOT条目已经改变:
5.8 本章小结
本章介绍了链接的概念及作用,对hello的elf格式进行了详细的分析,介绍了hello的虚拟地址,分析了hello的重定位过程、执行流程、动态链接过程,详细阐述了hello.o链接成为一个可执行目标文件的过程
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是一个执行中程序的实例。是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
作用:进程的概念为我们提供这样一种假象,就好像我们的程序是系统中当前运行的唯一程序一样,我们的程序好像是独占地使用处理器和内存,处理器好像是无间断地一条接一条地执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell俗称壳,是指"为使用者提供操作界面"的软件。同时它又是一种程序设计语言。作为命令语言,它交互式解释和执行用户输入的命令或者自动地解释和执行预先设定好的一连串的命令。它作为用户操作系统与调用其他软件的工具。
处理流程:
(1)从终端读入输入的命令。
(2)将输入字符串切分,分析输入内容,解析命令和参数。
(3)如果命令为内置命令则立即执行,如果不是内置命令则创建新的进程调用相应的程序执行。
(4)在程序执行期间始终接受键盘输入信号,并对输入信号做相应处理
6.3 Hello的fork进程创建过程
在终端输入命令./hello运行hello程序,由于hello不是一个内置命令,故解析后执行当前目录下的可执行目标文件hello,shell作为父进程通过fork函数为hello创建一个新的进程作为子进程。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同但独立的一份副本,包括代码和数据段、堆、共享库、用户栈。hello进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程还可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
6.4 Hello的execve过程
子进程创建后,shell调用execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。之后当出现错误时,例如找不到hello,execve才会返回到调用程序。
在execve加载了hello后,它调用启动代码,启动代码设置栈,并将控制转移给新程序的主函数main,此时用户栈已经包含了命令行参数和环境变量,进入main函数后开始逐步运行程序。
6.5 Hello的进程执行
1.逻辑控制流:
一系列程序计数器 PC 的值的序列叫做逻辑控制流。由于进程是轮流使用处理器的,同一个处理器每个进程执行它的流的一部分后被抢占,然后轮到其他进程。
2.用户模式和内核模式:
处理器使用一个寄存器提供两种模式的区分。用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据;内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
3.上下文:
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。初始时,控制流再hello内,处于用户模式。调用系统函数sleep后,进入内核态,此时间片停止。2s后,发送中断信号,转回用户模式,继续执行指令。
4.调度的过程:
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
以执行sleep函数为例,sleep函数请求调用休眠进程,sleep将内核抢占,进入倒计时,当倒计时结束后,hello程序重新抢占内核,继续执行。
5.用户模式与内核模式转换:
为了能让处理器安全运行,不至于损坏操作系统,必然需要先知应用程序可执行指令所能访问的地址空间范围。因此,就存在了用户态与核心态的划分,核心态拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
按下Ctrl+Z:进程收到 SIGSTP 信号, hello 进程挂起。用ps查看其进程PID,可以发现hello的PID是6649;再用jobs查看此时hello的后台 job号是1,调用 fg 1将其调回前台。
Ctrl+C:进程收到 SIGINT 信号,结束 hello。在ps中查询不到其PID,在job中也没有显示,hello已经被彻底结束。
中途乱按:将屏幕的输入缓存到缓冲区,乱码被认为是命令。
Kill命令:挂起的进程被终止,在ps中无法查到到其PID。
6.7本章小结
本章简述了进程、shell的概念与作用,分析了hello程序使用fork创建子进程的过程以及使用execve加载并运行用户程序的过程,运用上下文切换、用户模式、内核模式、内核调度等知识,分析了hello进程的执行过程,最后分析了hello对于异常以及信号的处理并进行了实际操作
第7章 hello的存储管理
7.1 hello的存储器地址空间
1.物理地址(physical address)
用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。
2.逻辑地址(logical address)
逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。如Hello中sleepsecs这个操作数的地址。
3.线性地址(linear address)或也叫虚拟地址(virtual address)
跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式内存管理方式就是直接将逻辑地址转换成物理地址,也就是CPU不支持分页机制。其地址的基本组成方式是段号+段内偏移地址。
在x86保护模式下,段的信息(段基线性地址、长度、权限等)即段描述符占8个字节,段信息无法直接存放在段寄存器中
以上是关于计算机系统大作业:程序人生-Hello’s P2P的主要内容,如果未能解决你的问题,请参考以下文章
哈工大计算机系统大作业 程序人生-Hello‘s P2P 020
哈工大 计算机系统大作业 程序人生-Hello’s P2P From Program to Process