程序人生-Hello’sP2P
摘 要
本文简要分析了hello这一程序的生命周期,并介绍了相应的计算机系统组成成分(主要包括预处理,编译,汇编,链接,进程管理,存储管理,I/O管理几个部分)在其中的作用以及工作方法。
关键词:hello,预处理,编译,汇编,链接,进程管理,存储管理,I/O管理;
目 录
第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 4 -
第2章 预处理 - 5 -
2.1 预处理的概念与作用 - 5 -
2.2在UBUNTU下预处理的命令 - 5 -
2.3 HELLO的预处理结果解析 - 5 -
2.4 本章小结 - 7 -
第3章 编译 - 8 -
3.1 编译的概念与作用 - 8 -
3.2 在UBUNTU下编译的命令 - 8 -
3.3 HELLO的编译结果解析 - 8 -
3.4 本章小结 - 12 -
第4章 汇编 - 13 -
4.1 汇编的概念与作用 - 13 -
4.2 在UBUNTU下汇编的命令 - 13 -
4.3 可重定位目标ELF格式 - 13 -
4.4 HELLO.O的结果解析 - 16 -
4.5 本章小结 - 17 -
第5章 链接 - 18 -
5.1 链接的概念与作用 - 18 -
5.2 在UBUNTU下链接的命令 - 18 -
5.3 可执行目标文件HELLO的格式 - 18 -
5.4 HELLO的虚拟地址空间 - 24 -
5.5 链接的重定位过程分析 - 25 -
5.6 HELLO的执行流程 - 27 -
5.7 HELLO的动态链接分析 - 28 -
5.8 本章小结 - 29 -
第6章 HELLO进程管理 - 30 -
6.1 进程的概念与作用 - 30 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 30 -
6.3 HELLO的FORK进程创建过程 - 30 -
6.4 HELLO的EXECVE过程 - 30 -
6.5 HELLO的进程执行 - 30 -
6.6 HELLO的异常与信号处理 - 31 -
6.7本章小结 - 32 -
第7章 HELLO的存储管理 - 34 -
7.1 HELLO的存储器地址空间 - 34 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 34 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 34 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 34 -
7.5 三级CACHE支持下的物理内存访问 - 34 -
7.6 HELLO进程FORK时的内存映射 - 35 -
7.7 HELLO进程EXECVE时的内存映射 - 35 -
7.8 缺页故障与缺页中断处理 - 35 -
7.9动态存储分配管理 - 35 -
7.10本章小结 - 35 -
第8章 HELLO的IO管理 - 37 -
8.1 LINUX的IO设备管理方法 - 37 -
8.2 简述UNIX IO接口及其函数 - 37 -
8.3 PRINTF的实现分析 - 37 -
8.4 GETCHAR的实现分析 - 38 -
8.5本章小结 - 38 -
结论 - 38 -
附件 - 39 -
参考文献 - 40 -
第1章 概述
1.1 Hello简介
P2P是指from program to process,对原程序hello.c进行包括预处理,编译,汇编,链接的一系列操作后,形成可执行文件。执行该文件时,OS为该文件fork产生子进程(process)。
020是指from zero to zero。程序开始执行后,OS为其映射到虚拟内存,执行目标代码,然后mmap分配时间片,最终在硬件上实现。实现后由内核使内存等恢复到程序执行前的状态,即020。
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.c: 源文件
Hello.i: 预处理之后的文件
Hello.s: 编译之后的汇编文件
Hello.o:汇编输出文件
Hello:链接输出文件。
1.4 本章小结
本章简要介绍了文章的创作环境以及过程文件,简述hello程序从.c到可执行文件hello的大致过程。
第2章 预处理
2.1 预处理的概念与作用
概念:在编译之前预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。
作用:1. 宏替换,将宏名替换为文本;
2. 加载头文件;
3. 处理条件编译;
4. 处理特殊符号。
2.2在Ubuntu下预处理的命令
tu图
2.3 Hello的预处理结果解析
预处理之后hello.c文件转化为hello.i文件,阅读hello.i,可见预处理对源文件保持main部分不变,头文件部分展开,宏定义也被处理。若文件存在嵌套关系,cpp也会逐层展开,这样hello.i就可以直接被译为.s文件。
2.4 本章小结
本章主要介绍了预处理的概念和作用,并且对hello.c文件进行预处理且分析结果。
第3章 编译
3.1 编译的概念与作用
概念:将程序员所撰写的编程语言翻译成汇编语言的过程:编译器将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。其以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。
作用:通过语法检验,代码优化等过程,将程序员便于记忆和认知的编程语 言转化为机器可识别的预言。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
应截图,展示编译过程!
3.3 Hello的编译结果解析
3.3.1 汇编指令
.file:声明源文件;
.text:代码节
.rodata:只读代码段;
.align:数据或者指令的地址对齐方式;
.string:声明一个字符串(.LC0,.LC1);
.global:声明全局变量;
.type:声明一个符号是数据类型还是函数类型。
3.3.2 数据
3.3.2.1 字符串
两个字符串都在只读数据段中,作为printf函数的参数。
3.3.2.2 全局变量
函数声明一个全局变量i,编译器进行编译的时候将局部变量i放到堆栈中。
3.3.2.3 main函数
参数argc作为用户传给main的参数,也是被放到堆栈里的。
3.3.2.4 各种立即数
立即数直接体现在汇编代码中。
3.3.2.5 数组
main函数的第二个参数是数组,每一个元素都是指向字符类型的指针。.L4中能看到它的起始地址等
3.3.3 全局变量
hello.c文件中声明了全局函数int main (int argc, char *argv[]),经过编译后,main函数中使用的字符串常量也被放在数据区。.global main说明main函数是全局变量。
3.3.4 赋值
i = 0; 用mov实现,根据数据类型选择movq movl等。
3.3.5 算数
i++; 用add实现,因为是int型。
3.3.6 关系操作
3.3.6.1 argc != 3; 编译成cmpl $3, -20(%rbp)同时还有条件码判断是否需要跳转 je .L2
3.3.6.2 i < 10; 编译成 cmpl $9, -4(%rbp) 然后还设置条件码进行判断或跳转
jle .L4
3.3.7 控制转移指令
3.3.7.1
汇编为
cmpl $3, -20(%rbp)
je .L2
3.3.7.2
汇编为
.L2:
movl $0, -4(%rbp)
jmp .L3
.L3:
cmpl $9, -4(%rbp)
jle .L4
3.3.8 函数操作
调用函数时有以下操作:
传递控制:进行过程B的时候,程序计数器必须设置为B的代码的起始 地址,然后在返回时,要把程序计数器设置为A中调用 B 后面那条指令的地址。
传递数据:A必须能够向B提供一个或多个参数B必须能够向A中返回 一个值。
分配和释放内存:在开始时,B可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。
(假设函数A调用函数B)
hello.C涉及的函数操作有:
main函数,printf,exit,sleep ,getchar函数
main函数的参数是argc和argv;两次printf函数的参数恰好是那两个字符串
exit参数是1,sleep函数参数是atoi(argv[3])
函数的返回值存储在%eax寄存器中。
3.3.9 类型转换
atoi(argv[3]),将字符串类型转换为整数类型。
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析
只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.4 本章小结
本章简要介绍了编译的概念和作用,以及编译阶段中编译器如何处理各种数据和操作,以及c语言中各种类型和操作所对应的的汇编代码并对hello.c和hello.s进行分析。
第4章 汇编
4.1 汇编的概念与作用
概念:把汇编语言翻译成机器语言的过程。
作用:汇编语言的诞生是由于机器代码难以记忆,所以用助记符代替操作码形成的方便记忆的语言,这种机器不能直接识别。用程序将其翻译为机器语言后,才可以被识别运行。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
4.3.1
ELF Header以 16B 的序列 Magic 开始,Magic 描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解 ## 标题释目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。
4.3.2
Section Headers:节头部表,包含了文件中出现的各个节的语义,包括节 的类型、位置和大小等信息。 由于是可重定位目标文件,所以每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,同时可以观察到,代码是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。
4.3.3
.symtab: 存放程序中定义和引用的函数和全局变量的信息。name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。
4.3.4
重定位节.rela.text中包含.text节的重定位信息,在链接时程序将通过这些信息和代码提供的偏移找到正确的需要调用的函数地址。本程序中,需要重定位的包括.rodata节中的两个数据,全局变量sleepsecs, 函数puts, exit, printf, sleep, getchar。
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
4.4 Hello.o的结果解析
通过对比可以发现,两者汇编代码有一些不同,而这些就是汇编过程实现的操作:
1 .o文件中每条语句都有了一个偏移量,便于跳转寻址。
2 去掉了面向程序员的助记符,跳转/调用指令后的函数名/助记符替换为了相对偏移地址。由于尚未进行链接,无法确定函数地址,所以偏移暂时为零。同时,在重定位节中添加相应条目。
3 为全局变量提供偏移量寻址。由于尚未进行链接,无法确定具体地址,所以偏移暂时为零。同时,添加重定位条目,等待链接。
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
4.5 本章小结
本章简要介绍了汇编的概念和作用,提供了Ubuntu下汇编的命令。对hello.s进行了汇编,生成了hello.o可重定位目标文件,并且分析了可重定位文件的ELF头、节头部表、符号表和可重定位节,比较了hello.s和hello.o反汇编代码的不同之处。
第5章 链接
5.1 链接的概念与作用
概念:将各种代码和数据片段收集并合并成一个单一文件的过程,这个文件可以被加载到内存中执行。
作用:使分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而实可以分解为更小的、更好管理的模块,可以独立地修改和编译单一模块。
注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
以上。
通过对比发现多了一个链接信息。
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.4 hello的虚拟地址空间
在edb中打开hello
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
5.5 链接的重定位过程分析
hello重定位的过程:
重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。
重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目。代码的重定位条目放在.rel.txt。
通过本图以及对比节头信息可以发现:
1增加了一些外部函数。
2增加了包括.init, .plt, .fini在内的一些节。
3相对偏移地址变为了虚拟内存的地址。
链接时,链接器通过符号表和节头了解到.data和.text在每个文件中的偏移和大小,进行合并,然后为新的合并出来的数据和代码节分配内存,并映射虚拟内存地址。最后修改对各种符号的引用,完成重定位。
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。
在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定(lazybinding),将过程地址的绑定推迟到第一次调用该过程时。
延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
本章简要介绍了链接的概念和作用以及Ubuntu下的链接命令。并对hello文件查看链接后的文件格式,对虚拟地址分配、重定位、动态链接等并分析。
第6章 hello进程管理
6.1 进程的概念与作用
概念:操作系统对一个正在运行的程序的一种抽象。
作用:一个程序在系统上运行时,操作系统会提供一种,程序在独占这个系 统,包括处理器,主存,I/O设备的假象。处理器看上去在不间断地一条一条执行程序中的指令…这些假象都是通过进程的概念实现的。
6.2 简述壳Shell-bash的作用与处理流程
Shell指操作界面,可以接收用户命令并调用相关程序。处理流程如下:
读取输入命令并处理得到参数。
判断输入命令是内置还是外部命令,内置命令立刻执行,外部命令则调用相关程序。
根据后续输入向相应进程发送信号。
处理接收到的信号,更新进程状态。
6.3 Hello的fork进程创建过程
(Fork函数再进程的当前位置创建一个新进程,新进程具有与原进程完全相同的状态(除PID)。
创建过程如下:
为新进程复制父进程的堆栈等数据空间。
创建新进程。
6.4 Hello的execve过程
Execve函数在当前进程的上下文中加载并运行一个新程序。当读取文件出现错误时,返回原程序,否则不返回。具体步骤如下:
根据第一个参数加载文件,通过启动代码对栈进行设置,并完成控制传递;
顺序执行,用第二,三个参数调用main函数
6.5 Hello的进程执行
系统执行进程时,内核可以暂停当前进程,并启用其他进程,这个过程称为调度,而这些进程以及它们的PC值所构成的序列就是逻辑控制流。当进程被执行时,内核代码不断地根据上下文信息,时间片等进行判断,并根据其结果转移控制权,完成调度。
上下文信息:指内核重新启动一个被抢占的进程所需要的状态,由通用寄存器,浮点寄存器,程序计数器,用户栈,状态寄存器,内核栈等构成。
时间片:一个进程执行它的控制流的每一时间段。
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.6 hello的异常与信号处理
hello程序出现的异常可能有:
中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常。
陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。
故障:在执行hello程序的时候,可能会发生缺页故障。
终止:终止时不可恢复的错误,在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。
在发生异常时会发出信号,比如缺页故障会导致OS发生SIGSEGV信号给用户进程,而用户进程以段错误退出。常见信号种类如下表所示。
按下Ctrl+c的结果,在键盘上输入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业
按下 ctrl-z 的结果,输入ctrl-z默认结果是挂起前台的作业,hello进程并没有回收,而是运行在后台下,运行其他命令如下:
按下回车程序会一直执行直达用control c 停止为止。
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.7本章小结
本章简要介绍了进程的概念和作用以及shell的处理流程。同时对hello文件分析了fork、execve、进程执行、异常以及信号处理过程。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:由程序产生的与段相关的偏移地址。分为段标识符和段内偏移。
线性地址:到物理地址的过渡,分为目录索引,页索引和页内偏移。
虚拟地址:为更有效地管理内存并减少不同程序间内存冲突的问题,现代系统提供的一种对主存的抽象概念。
物理地址:在主存中的地址。
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
1.观察段选择符,0则转换的是GDT(全局)中的段,否则就是LDT(局部)中的段。
2.根据相应寄存器,找到其起始地址。
3.通过段标识符找到对应的基地址。
4.用基地址加上偏移,得到线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
1.根据线性地址前十位找到对应页表的地址。
2.根据线性地址中间十位找到对应页的起始地址。
3.页的起始地址加上线性地址最后十二位,得到物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
将VA分为四段。依次通过每段地址找到对应的PML4,PGD,PMD,PTE表,找到对应地址,组合得到PA。
7.5 三级Cache支持下的物理内存访问
物理地址分为标记,组索引和块偏移。首先,在L1中匹配组索引位,若匹配成功,则根据标记和偏移的匹配结果决定缺失或是命中。若组索引匹配不成功,则进入下一级cache,重复直至进入内存。
7.6 hello进程fork时的内存映射
Fork会为新进程(子进程)复制一个与父进程完全相同只读数据空间,并为其分配另一片内存和虚拟地址。分配时会将其标记为私有,防止过程中被父进程影响。
7.7 hello进程execve时的内存映射
调用Execve时,系统首先删除了当前进程中用户部分已有的结构,然后映射私有区域(建立新的文件结构,包括.data在内的各种节),共享区域(当前进程的动态链接),并设置PC。
7.8 缺页故障与缺页中断处理
1.段错误:地址不合法,即无法匹配到已有的区域结构中;
2.非法访问:没有应有的读写权限;
3.正常缺页:选择一页进行替换。
7.9动态存储分配管理
分配器有两种风格,显示分配器(要求应用显式地释放任何已分配的块),隐式分配器(要求分配器检测一个已分配块是否仍然需要,不需要则释放)。
分配策略:
1.空闲链表:
(1) 隐式:在每块的头,尾部增加32位存储块大小,以及是否空闲。
(2) 显式:在隐式的基础上在头部增加对前后空闲块的指针。
(3) 分离:同时维护多个空闲链表。
2.带边界标记的合并:
利用每块头尾的大小和空闲状态信息合并空闲块。
3.无合适空闲块时,申请额外的堆空间。
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
7.10本章小结
本章简要介绍了hello的存储器的地址空间,介绍了四种地址空间的差别和地址的相互转换。同时介绍了hello的四级页表的虚拟地址空间到物理地址的转换。阐述了三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
包括:1. 普通文件(Is-al第一个属性为“-”)
2. 目录文件(Is-al第一个属性为“d”)
3. 设备文件
设备管理:unix io接口
包括:1. 开关文件
2. 读写文件
3. 改变当前文件的位置
8.2 简述Unix IO接口及其函数