2019年HIT-ICS大作业
程序人生人生-Hello‘s P2P
摘 要
本文在linux环境下,借助GDB,EDB,objdump,readelf等工具手段一步步实现hello.c程序的预处理,编译,汇编,链接。通过在shell里运行分析hello对加载,运行过程。本文依据计算机系统的知识对上述每一步骤都进行了详细的讲解。
关键词:Linux,hello,编译汇编,链接,进程,I/O管理;
第1章 概述
1.1 Hello简介
P2P过程:首先先有个hello.c的c程序文本,经过预处理->编译->汇编->链接四个步骤生成一个hello的二进制可执行文件,然后由shell新建一个进程,在那里执行。
020过程:shell执行hello,为其映射虚拟内存,开始执行hello的程序,然后在运行时发生缺页的时候载入物理内存,将其output显示到屏幕,然后hello进程结束,shell回收子进程。
1.2 环境与工具
1.2.1 硬件环境
2.8GHz Intel Core I7;16GB 2133MHz LPDDR3
1.2.2 软件环境
Ubuntu19.0.0 Vmware Fusion 11.0,macOS High Sierra(10.13.6)
1.2.3 开发工具
Codeblocks,Valgrind; gprof; GDB,edb,hexedit, xcode
1.3 中间结果
hello.i(hello.c预处理之后的程序文本)
hello.s(hello.i编译成汇编语言之后的程序文本)
hello.o(hello.s生成的二进制文件)
hello(可执行的hello二进制文件)
1.4 本章小结
本章介绍了hello,硬件环境,软件环境,开发工具和运行hello即调试开发过程中生成的产物。
第2章 预处理
2.1 预处理的概念与作用
2.1.1
预处理的概念:
预处理是对于c语言程序进行一个初步整理的过程,处理掉了所有的宏定义
删除“#define”并展开所定义的宏
– 处理所有条件预编译指令,如“#if”,“#ifdef”, “#endif”等
– 插入头文件到“#include”处,可以递归方式进行处理
– 删除所有的注释“//”和“/* */”
– 添加行号和文件名标识,以便编译时编译器产生调试用的行号信息
– 保留所有#pragma编译指令(编译器需要用)
2.1.2
预处理作用:
预处理可以在在将c程序转化为s的汇编程序之前对于宏定义处理,方便后续的代码转化,并且对于在汇编中无用的注释进行处理,删去无用部分对后续操作做准备。
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
对比两张图片可以发现预处理后代码因为插入头文件所以代码变长,添加了行号,而去注释被去除了。
2.4 本章小结
本章节简单介绍了c语言在编译前的预处理过程,简单介绍了预处理过程的概念和作用,对预处理过程进行演示。可以通过实验发现预处理是执行程序前非常重要的一步。
第3章 编译
3.1 编译的概念与作用
3.1.1
编译是编译器(ccl)将文本文件hello.i翻译成汇编语言文件hello.s的过程。
3.1.2
编译目的就是把预处理完的文件进行一系列词法分析,语法分析,语义,式其在接下来的操作中更容易被处理。
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1数据
3.3.1.1变量i
运行时出现在栈空间
3.3.2表达式
3.3.2.1赋值
运用movl通过把数放在一个位置上来进行赋值
3.3.2.2 控制转移
cmpl是一个比较语句。假设je address前面有一个语句cmpl x,y ,执行的操作是y-x,然后设置条件码。je判断的是ZF的值,如果ZF==1则表明y-x=0,则je条件达成,那么跳转到address位置,从address位置开始继续往下执行。否则的话则从je的下一条语句开始继续往下执行。
3.3.2.3循环
通过jmp到不同部分如L3,L4完成循环,循环因子存在(%rbp)-4里
3.3.2.4算数操作
通过addq完成加法
3.3.2.5关系操作
i<8变成通过cmpl指令和7比较,若小于等于就继续循环
3.3.2.6数组/指针/结构操作
printf函数里面的一系列对指针和对数组的操作编译器编译为:
3.3.3函数调用
3.3.3.1printf的调用
3.3.3.4atoi的调用
3.3.3.5sleep的调用
3.3.3.6getchar的调用
这些函数调用若需要参数都是通过rdi传参然后通过call指令将控制传递给被调函数
3.4 本章小结
本节主要介绍的是编译器通过编译由.i文件生成汇编语言的.s文件的过程,并通过解析局部变量在.s文件中的表示方法,以及各类c语言的基本语句的汇编表示,让读者更加理解高级语言的底层表示方法。汇编语言与编译过程的学习使我们真正理解计算机底层的一些执行方法。
第4章 汇编
4.1 汇编的概念与作用
4.1.1
汇编是把汇编语言翻译成机器语言的过程,并将这些指令打包成一种叫做可重定位目标程序,并将这个结果保留在(xxx.o)中。这里的xxx.o是二进制文件。汇编过程的作用是将汇编指令转换成一条条机器可以直接读取分析的机器指令。
4.1.2
通过汇编这个过程,把汇编代码转化成了计算机完全能够理解的机器代码,这个代码也是我们程序在计算机中表示。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
4.3.1 ELF头
ELF头首先以一个16字节的序列开始,这个序列描述了生成该文件的系统的 字的大小和字节顺序。剩下部分就如下图所示,列出了包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小(64字节),目标文件的类型(REL可重定位文件),机器类型(AMD X86-64),节头部表的文件偏移,以及节头部表中条目的大小和数量。
4.3.2 节
.text 节:存有编译完的代码
.rodata 节:存放只读数据,例如跳转表, …
.data 节:存放已初始化全局变量和静态变量
.bss 节:存放未初始化的全局变量和所有被初始化为0的全局变量和静态变量,这个块里的变量不占用空间,仅仅是一个占位符,“Block Started by Symbol” 符号开始的块,“Better Save Space” 更加节省空间
.symtab 节:即符号表,存放函数和静态变量名的节名称和位置
.rel.text 节:存放.text 节的可重定位信息,在可执行文件中需要修改的指令地址,需修改的指令.
.rel.data 节:存放.data 节的可重定位信息,在合并后的可执行文件中需要修改的指针数据的地址
.debug 节:存放为符号调试的信息 (gcc -g)
.strtab: 一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部的节名字。
节头表Section header tabl:存放每个节的偏移量和大小
4.3.3 符号表
每个可重定位目标模块都有一个符号表,它包含m的定义和引用的符号的信息。符号表是由汇编器构造,使用编译器输出到汇编语言.s文件中的符号。每个符号表是一个条目的数组,每个条目包含下面部分
在.rel.text里面有我们的重定位条目,这个条目能告诉链接器目标文件合并成可执行文件时如何修改引用。一个重定位条目包含以下信息:
符号名称说明这是哪个的重定位条目,偏移量是指被修改的引用的节偏移x
类型.这里有两种,R_X86_64_32意思是重定位时使用一个32位的绝对地址的引用,通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。R_X86_64_PC32意思是重定位时使用一个32位PC相对地址的引用。一个pc相对地址就是据程序计数器的当前运行值的偏移量。
4.4 Hello.o的结果解析
通过执行objdump -d -r hello.o 得到hello.o的反汇编代码,如下:
4.4.1 二进制机器码
汇编代码的左侧这些数字代表的是机器语言的二进制代码,机器语言就是由一系列的二进制数组成的,一般包括操作码字段和字节码字段。在指令集中每个指令都有自己对应的机器代码。
4.4.2跳转
汇编代码中:
机器代码及反汇编结果中:
我们可以发现je后面的跳转对象由代码段L2变成了地址并且反汇编代码给出提示是main函数+0x2b偏移处。而且这个偏移值可以由74(je的机器码)后面的16进制数和下一条指令地址得到。
4.4.3重定向提示
在hello.o的反汇编代码中我们会看到很多函数调用和数据引用,包含这些函数和数据的指令需要链接后才能被正确执行,所以与汇编代码不同,机器代码为这些函数和数据留下了一些地址以便链接后重定向。此处就是用到了rodata节中的数据,链接后的地址是PC相对寻址方式的。
4.5 本章小结
本章简述了hello.s汇编指令被转换成hello.o机器指令的过程,通过readelf查看hello.o的ELF、利用odjdump反汇编的方式查看了hello.o反汇编的内容,比较其与hello.s之间的差别。学习了汇编指令映射到机器指令的具体方式和机器指令反汇编过来得到的编码的区别。 第5章 链接
5.1 链接的概念与作用
5.1.1概念:链接是将多个可重定位目标文件组合成为一个可执行目标文件的过程,这个文件可以被加载到内存并执行。
5.1.2作用:产生一个完全链接的,可以加载运行的可执行目标文件。
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
5.3.1 ELF头
5.3.2 节头
.text: 该节中包含程序的指令代码;
.init: 该节包含进程初始化时要执行的程序指令;当程序开始运行时,系统会在进程进入主函数之前先执行这一个节中的指令代码;
.fini: 该节中包含进程终止时要执行的指令代码;当程序退出时,系统会执行这个节中的指令代码;
.bss : 该节中包含目标文件中未初始化的全局变量;一般情况下,可执行程序在开始执行时,系统会把这一段内容清零;但是在运行期间的.bss段是由系统动态初始化而成的,目标文件中的.bss节中并不包含任何内容,其长度为0,所以它的节类型为SHT_NOBITS;
.data/.data1:这两个节用于存放程序中已被初始化过的全局变量;在目标文件中,它们是要占用实际的存储空间的,这一点与.bss节不同;
.rodata/.rodata1:这两个节中包含程序中的只读数据,在程序装载时,它们一般会被装入到进程地址空间中的那些只读的段中;
.dynamic: 该节中包含动态链接信息,并且可能有SHF_ALLOC和SHF_WRITE等属性;
.dynstr : 该节中包含用于动态链接的字符串,一般是那些与符号表相关的名字;
.dynsym : 该节中包含动态链接符号表;
.got : 该节中包含全局偏移表(global offset table);
.plt : 该节中包含函数链接表(function link table);
.hash : 该节中包含一张哈希表;
.interp : 该节中包含ELF文件解析器的路径名;如果该节被包含在某个可装载的段中,那么该节的属性中应设置SHF_ALLOC标志位,苟泽不设置此标志位;
.strtab : 该节用于存放字符串,主要是那些符号表项的名字;如果一个目标文件中有一个可装载的段,并且其中含有符号表,则该节的属性中应该有SHF_ALLOC属性;
.symtab : 该节用于存放符号表;如果一个目标文件中有一个可装载的段,并且其中含有符号表,则该节的属性中应该有SHF_ALLOC属性;
.shstrtab: 该节是节名字表,含有所有其它节的名字;
.comment: 该节中包含版本控制信息;
.line : 该节中包含调试信息,包括哪些调试符号的行号,为程序指令码与源文件的行号建立联系;
.note : 该节中包含注释;
.relX/.relaX:这两个节中包含重定位信息;如果该节被包含在某个可装载的段中,则该节的属性中应设置SHF_ALLOC标志位;X表示那些需要重定位的节的名字;比如:.text节的重定位节的名字将是.rel.text或.rela.text;
5.4.3 程序头
由上图可以看出该程序头表列出了10个段,这些组成了最终在内存中执行的程序。
其还提供了各段在虚拟地址空间和物理地址空间的位置、大小、标志、访问授权和对齐方面的信息。还指定了一个类型来更精确的描述段。
PHDR保存程序头表。
INTERP指定在程序已经从可执行映射到内存之后,必须调用解释器。在这里解释器并不意味着二进制文件的内存必须由另一个程序解释。它指的是这样的一个程序:通过链接其他库,来满足未解决的引用。
通常/lib/ld-linux-so.2、/lib/ld-linux-ia-64.so.2等库,用于在虚拟地址空间中插入程序运行所需的动态库。对几乎所有的程序来说,可能c标准库都是必须映射的,还需要添加各种哭包括,GTK、数学库、libjpeg等。
LOAD表示一个从二进制文件映射到虚拟地址空间的段。其中保存了常量数据(如字符串),程序的目标代码等等。
DYNAMIC段保存了其他动态链接器(即,INTERP中指定的解释器)使用的信息。
NOTE保存了专有信息。
虚拟地址空间的各个段,填充了来自ELF文件中特定段的数据。
5.4.4 符号表
与链接前的文件类型,只不过是符号表得到了扩展。
5.4 hello的虚拟地址空间
5.4.1.PDHR:起始位置 0x400040 大小:0x230 从 0x400040 到 0x400270
5.4.2 .INTERP:起始位置 0x400270 大小:0x1c 从 0x400270 到 0x40028c
个段内保存的是动态链接库的位置,在后面edb也翻译出了这个语句对应的ASCII码。
5.4.3 .LOAD:起始位置 0x400000 大小:0x530 从 0x400000 到 0x400530
前面的截图加上
5.4.4 .LOAD:起始位置 0x401000 大小:0x26d 从 0x401000 到 0x40126d
5.4.5 .LOAD:起始位置 0x403e00 大小:0x260 从 0x403e00 到 0x404060
5.4.6 .DYNAMIC:起始位置 0x403e10 大小:0x1e0 从 0x403e10 到 0x403ff0
5.4.7 .NOTE:起始位置 0x40028c 大小:0x20 从 0x40028c 到 0x4002ac
5.4.8 .GNU_STACK:起始位置 0x0 大小:0x0
5.4.9 .GNU_STACK:起始位置 0x403e00 大小:0x200 从 0x403e00 到 0x404000
5.5 链接的重定位过程分析
objdump -d -r hello
Hello比hello.o多出来很多节,如.init和.plt。其中plt节是在链接动态库的时候使用的,init节是为了程序初始化需要执行的代码
发现hello.o从.text节开始也就是0x0,而hello从.init节开始也就是0x401000。
而且hello多出来很多被调用的函数如printf、sleep、getchar、exit。在hello中调用函数都是使用call+
重定位的实现依靠.rodata这个段,这个段保留重定位所需的信息,叫做重定位条目。链接器解析重定条目时发现两个类型为R_X86_64_PC32的对.rodata的重定位(如printf中的两个字符串,在hello.o的反汇编文件中对printf参数字符串的引用使用全 0 替代,在 hello 中则使用确定地址),.rodata与.text节之间的相对距离确定,因此链接器直接修改call之后的值为目标地址与下一条指令的地址之差,指向相应的字符串。
5.6 hello的执行流程
程序名称 程序地址
ld-2.29.so!_dl_start 0x7f7955504030
ld-2.29.so!_dl_init 0x7f79555129e0
libc-2.29.so!__libc_start_main 0x7f795550d350
hello!printf@plt 0x00000000004011cb
hello!atoi@plt 0x00000000004011de
hello!sleep@plt 0x00000000004011e5
hello!getchar@plt 0x00000000004011f4
libc-2.29.so!exit 0x7f 7ae3297800
5.7 Hello的动态链接分析
GOT表:
概念:每一个外部定义的符号在全局偏移表(Global offset Table)中有相应的条目,GOT位于ELF的数据段中,叫做GOT段。
作用:把位置无关的地址计算重定位到一个绝对地址。程序首次调用某个库函数时,运行时连接编辑器(rtld)找到相应的符号,并将它重定位到GOT之后每次调用这个函数都会将控制权直接转向那个位置,而不再调用rtld。
PLT表:
过程连接表(Procedure Linkage Table),一个PLT条目对应一个GOT条目
当main()函数开始,会请求plt中这个函数的对应GOT地址,如果第一次调用那么GOT会重定位到plt,并向栈中压入一个偏移,程序的执行回到_init()函数,rtld得以调用就可以定位printf的符号地址,第二次运行程序再次调用这个函数时程序跳入plt,对应的GOT入口点就是真实的函数入口地址。
ELF 文件对调用动态库 中的函数采用了所谓的"延迟绑定"(lazy binding)策略, 只有当该函数在其第一次被 调用发生时才最终被确认其真正的地址,因此我们不需要在调用动态库函数的地 方直接填上假的地址,而是使用了一些跳转地址作为替换,这样一来连修改动态 库和可执行程序中的相应代码都不需要进行了。
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。
在dl_init调用之后,其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]指向动态链接器ld-linux.so运行时地址。
在dl_init调用之后,其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]指向动态链接器ld-linux.so运行时地址。
5.8 本章小结
本章阐述了链接的概念与作用。分析了 hello的ELF格式、 虚拟地址空间的分配、重定位过程、执行流程和动态链接。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1概念:进程是正在运行的程序的实例。
6.1.2作用:进程的概念为我们提供这样一种假象,就好像我们的程序是系统中当 前运行的唯一程序一样,我们的程序好像是独占地使用处理器和内存。
6.2 简述壳Shell-bash的作用与处理流程
Shell 是一个命令行解释器,以用户态方式运行的终端进程。为用户提供了一个向 Linux 内核发送请求以便运行程序的界面系统级程序,其基本功能是解释并运行用户的指令,重复如下处理过程:
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
(3)如果不是内部命令,调用fork( )创建新进程/子进程
(4)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(5)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait…)等待作业终止后返回。
(6)如果用户要求后台运行(如果命令末尾有&号),则shell返回;
6.3 Hello的fork进程创建过程
在Shell中输入./hello 学号 姓名后,Shell通过调用fork 函数创建一个新的运行的子进程,也就是Hello程序。Hello进程几乎但不完全与Shell相同。Hello进程得到与Shell用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。Hello进程还获得与Shell任何打开文件描述符相同的副本,这就意味着当Shell调用fork 时,Hello可以读写Shell中打开的任何文件。Sehll和Hello进程之间最大的区别在于它们有不同的PID。
进程图:
6.4 Hello的execve过程
execve 函数在当前进程上下文加载并运行一个新程序。
execve 函数加载并运行可执行目标文件hello, 且带参数列表argv 和环境变量列表envp 。只有当出现错误时,例如找不到hello, execve 才会返回到调用程序。所以,与fork 一次调用返回两次不同, execve 调用一次并从不返回。
6.5 Hello的进程执行
hello 在刚开始运行时内核为其保存一个上下文,进程在用户状态下运行。如果没有异常或中断信号的产生,hello 将继续正常地执行。如果有异常或系统中断,那么内核控制转移到内核模式并完成上下文切换,将控制传递给其他进程。当 hello 运行到 sleep时,hello 显式地请求休眠,并发生上下文切换,控制转移给另一个进程,此时计时器开始计时,当计时器到达argv[3]时,它会产生一个中断信号,中断当前正在进行的进程,进行上下文切换,恢复 hello 在休眠前的上下文信息,控制权回到 hello 继续执行。当循环结束后,hello 调用 getchar 函数,之前 hello 运行在用户模式下,在调用 getchar 时进入内核模式,内核中的陷阱处理程序请求来自I/O传输,并执行上下文切换,并把控制转移给其他进程。当完成键盘缓冲区到内存的数据传输后,引发一个中断信号,此时内核从其他进程切换回 hello 进程,然后 hello执行 return,进程终止。
6.6 hello的异常与信号处理
可能出现的异常种类:中断 故障(缺页故障,调用缺页异常处理函数即可) 可能出现的信号:SIGINT SIGSTP
正常执行:
Ctr-Z: 当按下ctrl-z之后,shell父进程收到SIGSTP信号,信号处理程序将hello进程挂起,放到后台: