程序人生-Hello’s P2P
第1章 概述
1.1
Hello简介
我是Hello,我是每一个程序猿的初恋(羞
羞……),我是第一个第一个玩 P2P和020的
From Program to Process:
当hello一行一行的键入.c文件,它的一生就此开始,经过编译预处理器(cpp)的编译预处理变成.i文件,经过ccl的编译变成.s文件,as的汇编变成可重定位目标文件.o,链接器(ld)的链接产生可执行目标文件hello,在shell中键入启动命令,shell为hello进行fork子进程,hello便真正实现了From
Program to Process
O2O:From Zero-0 to Zero -0:
之后shell进行execve,先删除当前虚拟地址的用户部分已存在的数据结构,为hello的代码、数据、bss和栈区域创建新的区域结构,然后映射共享区域,设置程序计数器,映射虚拟内存,然后加载物理内存,,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构,以上全部便是020的过程。
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
1.2 环境与工具
硬件环境
X64 CPU;2GHz;2G RAM;256GHD Disk 以上
1.2.2 软件环境
Visual Studio 2010 64位以上;GDB/OBJDUMP;DDD/EDB等
1.2.3 开发工具
Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位,vim, gcc , as , ld , edb , readelf , HexEdit.
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
1.4 本章小结
本章作为大作业的开头,简述了Hello的P2P,020的整个过程,基本上概述了hello一生的经过几部分,这些部分解析所用到的工具和文件,对整个hello一生列出一个框架结构
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:编译预处理器(cpp)根据以字符#开头命令,修改原始c程序。比如hello.c中第六行,#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并直接插入程序文本,就得到了另一个C程序,以.i为文件扩展名
作用:
1.将hello.c中stdio.h,stdlib.h,unistd.h等#为开头的头文件读取到新的.i文件C程序中比如hello.c中第六行,#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并直接插入程序文本,就得到了另一个C程序,以.i为文件扩展名
2.删除注释,比如将hello.c文件中1-4行注释删除
3.执行宏定义(宏展开),用实际值替代宏定义字符串
4.条件编译,当某些语句希望在条件满足之后才编译,例如格式:
(1)#ifdef 标识符,程序段1,#else,程序段2,#endif
当表达式1成立时,编译程序段1,当不成立时,编译程序段2。
使用条件编译可以使目标程序变小,运行时间变短。
预编译使问题或算法的解决方案增多,有助于我们选择合适的解决方案
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
打开hello.i文件,hello.i程序已经扩展成3188行,main函数在hello.i文件最下面,自3099行开始,在3099行之前为hello.c中stdio.h,stdlib.h,unistd.h等#为开头的头文件读取到的.i文件C程序,三个头文件依次展开,cpp打开/usr/include/stdio文件,进一步解析替换stdio.h文件中的#define定义的宏定义字符串,然后进一步解析条件判断语句,判断其中包含的逻辑。cpp递归展开使得所有宏定义,以及条件判断语句进行展开解析
2.4 本章小结
本章主要介绍里预处理的定义概念以及功能作用,并通过hello.c以及预处理后hello.i的文件对比;对hello预处理结果进行解析,深入的了解了预编译过程的具体实现,以及特征
预处理程序把这些C程序通过预处理的命令处理好、替换好然后交给编译程序,使下一步编译可以顺利进行,提高接下来一步的执行效率。就好现象做饭一样,预处理程序就好像是择菜洗菜一样,处理好C程序之后再进行下一步,编译(切菜)。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
作用:
1.编译进程将C语言程序转换为汇编语言程序,汇编语言非常有用,他为不同高级语言的不同编译程序提供通用输出语言。
2.编译器处理流程:词法分析,语法分析,中间代码,目标代码,表格管理,出错处理,
3.编译的基本功能是把源程序(高级语言)翻译成目标程序。但是,作为一个具有实际应用价值的编译系统,除了基本功能之外,还应具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用以及人-机联系等重要功能。①语法检查:检查源程序是否合乎语法。如果不符合语法,编译程序要指出语法错误的部位、性质和有关信息。编译程序应使用户一次上机,能够尽可能多地查出错误。②调试措施:检查源程序是否合乎设计者的意图。为此,要求编译程序在编译出的目标程序中安改、程序执行时所经历的线路等。这些信息有助于用户核实和验证源程序是否表达了算法要求。③修改手段:为用户提供简便的修改源程序的手段。编译程序通常要提供批量修改手段(用于修改数量较大或临时不易修改的错误)和现场修改手段(用于运行时修改数量较少、临时易改的错误)。④覆盖处理:主要是为处理程序长、数据量大的大型问题程序而设置的。基本思想是让一些程序段和数据公用某些存储区,其中只存放当前要用的程序或数据;其余暂时不用的程序和数据,先存放在磁盘等辅助存储器中,待需要时动态地调入。⑤目标程序优化:提高目标程序的质量,即占用的存储空间少,程序的运行时间短。依据优化目标的不同,编译程序可选择实现表达式优化、循环优化或程序全局优化。目标程序优化有的在源程序级上进行,有的在目标程序级上进行。⑥不同语言合用:其功能有助于用户利用多种程序设计语言编写应用程序或套用已有的不同语言书写的程序模块。最为常见的是高级语言和汇编语言的合用。这不但可以弥补高级语言难于表达某些非数值加工操作或直接控制、访问外围设备和硬件寄存器之不足,而且还有利于用汇编语言编写核心部分程序,置一些输出指令以便在目标程序运行时能输出程序动态执行情况的信息,如变量值的更以提高运行效率。⑦人-机联系:确定编译程序实现方案时达到精心设计的功能。目的是便于用户在编译和运行阶段及时了解内部工作情况,有效地监督、控制系统的运行。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
应截图,展示编译过程!
3.3 Hello的编译结果解析
3.3.1指令解析
//全局变量声明sleepsecs
sleepsecs:
.LC0:
347216213345256266347220252357274201″
//声明一个string类型
.LC1:
3.3.2数据类型
Hello.c文件中数据类型有int型,数组argv[],输出的字符串
Hello.s文件中处理的数据类型有整数,数组,字符串。
- 整数
(1)C程序中int sleepsecs=2.5;全局变量,编译器处理时将其存放在.data节中;Hello.s文件中首先定义为全局变量,存放在.data中,对齐方式为4,设置为long类型,值为2,
(2)C程序中int i:编译器将局部变量储存在寄存器或者栈中
cmpl $9,
-4(%rbp) //i存储在栈上空间-4(%rbp)中;i占据了栈中的4B,
(3)int argv[]:作为参数传入
(4)立即数:直接编码到汇编代码中;比如:subq $32, %rsp;cmpl $9, -4(%rbp);
2.字符串
Hello.s文件汇编程序中的字符串为
字符串都声明在.section .rodata //.rodata,只读数据节
printf(“Usage: Hello 1171430615 王家琪!n”);//输出格式化参数
.string “Usage: Hello 1171430615
347216213345256266347220252357274201”
字符串被编码成UTF-8格式,一个汉字在utf-8编码中占三个字节,一个代表一个字节
printf(“Hello %s
%sn”,argv[1],argv[2]); //输出格式化参数
3.数组,char*大小为8字节,
C程序中int
main(int argc,char *argv[]),argv作为char类型指针的数组,指针指向存放着字符指针的连续空间,起始地址为argv,函数在访问数组时分为argv[1],argv[2],在hello.s文件中使用两次%rax分别为两个数组首地址
按照起始地址argv大小8字节计算数据地址取数据
3.3.3类型转换,赋值,算术和逻辑运算操作
(1)类型转换
C程序中int
sleepsecs=2.5;(全局变量),将浮点数类型转换为int型。
浮点型数据强转为int类型,程序改变数值和位模式的原则为向零舍入
sleepsecs: .long 2 //2.5舍为2
(2)赋值操作
- int sleepsecs=2.5;(全局变量)
sleepsecs: .long 2
//2.5舍为2;.data节中将sleepsecs声明为值2的long类型数
- int i; for(i=0;i<10;i++)
i=0; // movl $0,
-4(%rbp)
mov指令完成,根据数据的大小不同使用不同后缀
整数操作数 b : 1字节、w :2 字节、l :4 字节、q :8字节
浮点型操作数 s : 单精度浮点数、 l :双精度浮点数
(3)算术和逻辑运算操作
i++运算操作
addl $1,
-4(%rbp)
对计数器i自增,程序指令addl,后缀l代表操作数大小为4字节
subq $32,
%rsp
addq $16,
%rax
对栈针进行移动操作
leaq .LC1(%rip),
%rdi
加载有效地址指令leaq;并计算LC1的段地址%rip+.LC1;并将地址传递到%rdi
3.3.4条件分支(关系控制跳转,循环)
C程序中判断argv不等于3 ,if(argc!=3)
汇编指令为
cmpl $3,
-20(%rbp) //利用cmpl指令,设置argv-3为条件码
leaq .LC0(%rip),
%rdi
.L2:
.L4: //循环体中操作,for循环中指令
.L3:
7, 8
3.3.5过程(函数调用,参数传递)
过程机制:
传递控制1.调用:转到过程代码的起始处2.结束:回到返回点
传递数据1.过程参数2.返回值
内存管理1.过程运行期间申请2.返回时解除分配
过程内容:
1.栈结构;2.调用约定 传递控制 传递数据 管理局部数据;3.递归
传递控制:
1.栈结构支撑过程调用,返回;
- 过程调用 call func_label;返回地址入栈(Push);跳转到func_label (函数名字就是函数代码段的起始地址)
3.返回地址:紧随call指令的下一条指令的地址
- 过程返回 re;从栈中弹出返回地址(pop);跳转到返回地址
函数调用过程数据流:传递数据
管理局部数据
1.进入过程时申请空间1.生成代码,构建栈帧2.包括call指令产生的push操作
2.当返回时解除申请 结束代码,清理栈帧 包括ret指令产生的pop操作
Hello.s文件中函数过程操作:
main函数:
控制传递:main函数被系统启动函数__libc_start_main调用,call指令将下一条指令地址入栈,然后跳转到main函数执行
控制数据:调用过程向main函数传递参数argc和argv,分别使用%rdi和%rsi存储
movl %edi,
-20(%rbp)
movq %rsi,
-32(%rbp)
函数出口
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret //函数正常出口为return 0,将%eax设置0返回
管理局部数据
分配栈空间,释放栈空间
pushq %rbp // %rbp记录栈底指针
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp,
%rbp
.cfi_def_cfa_register 6
subq $32,
%rsp //函数分配栈帧空间在%rbp之上
movl %edi,
-20(%rbp)
movq %rsi,
-32(%rbp)
函数调用结束
leave //恢复栈空间为调用之前的状态,然后ret返回
ret
相当于pop,mov指令
printf函数:
leaq .LC0(%rip),
%rdi
call puts@PLT
第一次printf将%rdi设置为“Usage: Hello 1171430615 王家琪n”字符串的首地址
只有一个字符串参数,所以call puts@PLT
leaq .LC1(%rip),
%rdi
movl $0, %eax
call printf@PLT
第二次printf设置%rdi为“Hello %s %sn”的首地址,设置%rsi为argv[1],%rdx为argv[2];第二次printf使用call printf@PLT
exit函数:
movl $1, %edi //传递数据:将%edi设置为1
call exit@PLT //控制传递
sleep函数:
movl sleepsecs(%rip),
%eax //传递数据:将%eax设置为sleepsecs
movl %eax,
%edi //传递数据:将%eax中值传给%edi;
call sleep@PLT //控制传递
getchar函数:
call getchar@PLT //控制传递
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.4 本章小结
Hello.c文件经过洗菜处理(编译预处理),终于在编译阶段进行了“切菜处理”
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:
作用:
汇编器也是编译器,只是它和我们熟知的编译器的有略微的差别。汇编器处理的“高级语言”是汇编语言,输出的是机器语言二进制形式。因此,对于汇编器的构造,实质上和编译器大同小异,也都需要进行词法分析、语法分析、语义处理、符 表管理和代码生成(机器代码)等阶段。
汇编器结构
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
或者
应截图,展示汇编过程!
4.3 可重定位目标elf格式
获得hello.o文件的ELF格式
ELF头:在文件的开始,保存了路线图,描述了该文件的组织情况。以一个16字节序列Magic(ELF文件魔数)开始,Magic描述了生成该文件的系统字节大小和字节序列;ELF头剩下部分包含帮助链接器语法分析和解释目标文件信息;包括ELF头的大小,目标文件的类型(如可重定位,可执行,共享),机器类型(如x86-64),节头部表的文件偏移,以及节头部表中的条目的大小和数量;
- 节头部表(Section header table)(节头表):
记录每个节的节名、偏移和大小
不同节的位置和大小由节头部表描述,其中目标文件每一个节都有一个固定大小的条目;
可重定位目标文件中,每个可装入节的起始地址总是0
.text节:编译后的代码部分
.rodata节:只读数据,如printf 格式串、switch 跳转表等
.data节:已初始化的全局变量
.bss节:未初始化全局变量,仅是占位符,不占据任何实际磁盘空间。区分初始化和非初始化是为了空间效率
.symtab节:存放函数和全局变量(符 表)信息,它不包括局部变量
重定位节
.rel.text节的重定位信息,用于重新修改代码段的指令中的地址信息
当链接器把这个可重定向目标文件与其他文件链接时,需要修改这些位置的地址信息,一般而言,任何调用外部函数或者应用全局变量的指令都需要进行重定位修改;
.rel.data节的重定位信息,用于对被模块使用或定义的全局变量进行重定位的信息,一般而言,任何已经初始化的全局变量,如果它的初始值为一个全局变量地址或者外部定义函数的地址,都需要被修改;
Hello.o文件中的8条重定位信息分别对应的是:.L0(第一个printf中的字符串),puts函数,.L1(第二个printf中的字符串),
printf函数, sleepsecs(全局变量),sleep函数、getchar函数
偏移量:需要重定位代码在.text .data.节中的偏移位置,8字节
信息:symbol和type两部分,其中symbol占前4个字节,type占后4个字节,symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位的类型
类型:重定位到目标类型
R X86_ 64 PC32。 重定位一个使用32位PC相对地址的引用。一个PC相对地址就是距程序计数器(PC)的当前运行时值的偏移量。当CPU执行一条使用PC相对寻址的指令时,它就将在指令中编码的32位值加上PC的当前运行时值,得到有效地址(如call指令的目标),PC值通常是下一条指令在内存中的地址。
R X86_ 64 _32。重定位一个使用32位绝对地址的引用。通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改
符 名称+加数:重定向到的目标的名称+计算重定位位置的辅助信息,共占8个字节(Addend)
重定位过程将由链接器进行,接下来进行简要的介绍:
将多个代码段与数据段分别合并为一个单独的代码段和数据段
计算每个定义的符 在虚拟地址空间中的绝对地址
将可执行文件中符 引用处的地址修改为重定位后的地址信息
链接器根据info信息向.symtab节中查询链接目标的符 ,例如由info.symbol=0x05,可以发现重定位目标链接到.rodata的.L1,重定位条目为r
然后类型为R X86_ 64 PC32。,重定位一个使用32位PC相对地址的引用,一个PC相对地址就是距程序计数器(PC)的当前运行时值的偏移量
r.offset=0x18,
r.symbol=.rodata, r.type=R_X86_64_PC32, r.addend=-4
需要重定位的.text节中的位置为S1,设重定位的目的位置S2
指向S1的指针= s +r.offset
S1的运行时地址= ADDR(s) + r.offset
运行时值*refptr =(unsigned) (ADDR(r.symbol) + r.addend-refaddr)
ADDR(r.symbol)计算dst的运行时地址,在本例中,ADDR(r.symbol)获得的是dst的运行时地址,因为需要设置的是绝对地址,即dst与下一条指令之间的地址之差,所以需要加上r.addend=-4
.debug节:调试用符 表(gcc -g)
.strtab节:包含symtab和debug节中符 及节名
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
4.4 Hello.o的结果解析
objdump获得hello.o文件反汇编代码;
对比hello.s文件中汇编代码,
Hello.s文件中主函数
1.条件分支变化:反汇编产生的跳转指令,不再是跳转到段名称.L2 .L3…而是确定的相对偏移地址(链接器重定位), 段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在
2.函数调用
在.s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。这是因为hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text节中为其添加重定位条目,等待静态链接的进一步确定。
3.数据访问:
全局变量访问,在反汇编中对.rodata中printf的格式串的访问需要通过链接时重定位的绝对引用确定地址,在汇编代码相应位置仍为占位符表示,对.data中已初始化的全局变量sleepsecs为0x0+%rip的方式访问hello.s中访问方式为sleepsecs+%rip
在hello.s文件中,访问rodata(printf中的字符串),使用段名称+%rip,在反汇编代码中0+%rip,因为rodata中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全0并添加重定位条目
objdump -d
-r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
4.5 本章小结
本章通过查看hello.o文件ELF格式,对重定位项目分析和使用Objdump反汇编代码与hello.s文件汇编语言进行对比,深入了解了hello.s到hello.o的汇编过程
Hello.c文件记过编译预处理,编译,汇编,经过多次优化装换为低级语言,终于要进行链接过程;生成可执行文件操作了;
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合称为一个单一文件的过程,这个文件可被加载(复制)到内存并执行,这个文件可被加载到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行,链接是由链接器的程序自动执行
作用:链接器使分离编译成为可能;一个大型应用程序不再需要组织巨大的源文件,而是可以分解为更小,更好管理的模块,可以独立修改和编译这些模块,当我们改变这些模块中的一个时,只需简单编译,重新链接使用。
注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令
链接命令:
ld
-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
/usr/lib/gcc/x86_64-linux-gnu/7/crtbegin.o
/usr/lib/gcc/x86_64-linux-gnu/7/crtend.o
/usr/lib/x86_64-linux-gnu/crtn.o
hello.o -lc -z
relro -o hello
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
通过readelf获得hello的ELF格式文件
ELF 文件包括三个索引表:ELF header,Program header
table,Section header table;
1)ELF
header:在文件的开始,保存了路线图,描述了该文件的组织情况。
2)Program
header table:告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序头部表,可重定位文件不需要这个表。
3)Section header able:包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。用于链接的目标文件必须包含节区头部表,其他目标文件可以有,也可以没有这个表
4)hello的ELF格式文件无两个.rel节(无需重定位)
1.ELF头部表
ELF头中字段e_entry给出执 行程序时第一条指令的地址, 而在可重定位文件中,此字段为0(与可重定位ELF文件不同的地方)
2.节头部表:描述目标文件的节
Section Headers对hello中所有的节信息进行了声明,包括大小Size以及在程序中的偏移量Offset,大小、全体大小、旗标、链接、信息、对齐等信息,根据Section Headers中的信息可以定位各个节所占的区间。其中地址一般是程序被载入到虚拟地址的起始地址
3.程序头表(段头表)(segment header table)
是一个结构数组;将连续的文件节映 射到运行时内存段
- .init节:
用于定义_init函数,该函数用来进行可执行目标文件开始执行时的初始化工作
(可重定位ELF文件没有.init节)
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.4 hello的虚拟地址空间
用edb打开hello可执行程序
在Data Dump窗口观察hello程序被加载到0x00400000——0x00400ff0段中
程序头表在执行的时候被使用,它告诉链接器运行时加载的内容并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的大小、位置、标志、访问权限和对齐方面的信息。在下面可以看出,程序包含8个段:
PHDR:保存程序头表。起始地址0x400000偏移0x40字节处、大小为0x1c0字节
INTERP:指定在程序已经从可执行文件映射到内存之后,必须调用的解释器(如动态链接器)。位于内存起始位置0x400000偏移0x200字节处,大小为0x1c个字节,记录了程序所用ELF解析器(动态链接器)的位置位于: /lib64/ld-linux-x86-64.so.2
LOAD:表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了常量数据(如字符串)、程序的目标代码等。
第一段:(包括ELF头、程序头表、.init、 .text和.rodata节),映射到虚拟地址0x00400000开始长度为0x82c字节的区域 ,按0x20000=800KB对齐,具有只读/执行权限(Flg=RE),是只读代码段
第二段:.data节,映射到虚拟地址 0x00600e00开始长度为0x254字节的存储区域,在596B存储区中,前588B用.data节内容初始化,后面8B对应.bss节,初始化为0 ,按0x200000=800KB对齐,具有可读可写权限(Flg=RW),是可读写数据段
DYNAMIC:保存了动态链接器使用的信息。
NOTE:保存辅助信息。此处NOTE表示该段位于内存起始位置0x400000偏移0x21c字节处,大小为0x20个字节,该段是以‘ ’结尾的字符串,包含一些附加信息
GNU_STACK:权限标志,标志栈是否是可执行的。
GNU_RELRO:表示这段在重定位结束之后那些内存区域是需要设置只读
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
5.5 链接的重定位过程分析
使用Objdump指令获得hello反汇编文件
与hello.o反汇编文件相比,hello反汇编相比,hello反汇编地址为虚拟内存地址,并且多了许多节和子程序:
Section to Segment mapping:
段节…
00
01 .interp //保存ld.so的路径
02
.interp .note.ABI-tag .hash .gnu.hash .dynsym .dynstr .gnu.version
.gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame
03 .init_array .fini_array .dynamic .got
.got.plt //动态链接中过程链接表PLT和全局偏移量表GOT的间接调转
.data .bss
04 .dynamic //存放被ld.so使用的动态链接信息
05
.note.ABI-tag //Linux下特有的section
06
07 .init_array .fini_array .dynamic .got
Disassembly
of section .init //程序初始化需要执行的代码
0000000000400488 <_init>
Disassembly
of section .plt: //动态链接-过程链接表
00000000004004a0
<.plt>
00000000004004b0
puts@plt
00000000004004c0 printf@plt
00000000004004d0 getchar@plt
00000000004004e0 exit@plt
00000000004004f0 sleep@plt
调用的子函数
Disassembly
of section .text: //hello主函数代码段
0000000000400500 <_start>
0000000000400530 <_dl_relocate_static_pie>
0000000000400530 <_dl_relocate_static_pie>
0000000000400540 <deregister_tm_clones>
00000000004005e7
…
0000000000400670 <__libc_csu_init>
00000000004006e0 <__libc_csu_fini>
Disassembly
of section .fini: //当程序正常终止时需要执行的代码
00000000004006e4 <_fini>
在使用ld链接命令时,动态链接器为64的/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o中主要定义了程序入口_start、初始化函数_init,_start程序调用hello.c中的main函数,libc.so是动态链接共享库,其中定义了hello.c中用到的printf、sleep、getchar、exit函数和_start中调用的__libc_csu_init,__libc_csu_fini,__libc_start_main
函数调用过程中;链接器解析重定条目时发现对外部函数调用的类型为R_X86_64_PLT32的重定位,此时动态链接库中的函数已经加入到了PLT中,.text与.plt节相对距离已经确定,链接器计算相对距离,将对动态链接库中函数的调用值改为PLT中相应函数与下条指令的相对地址,指向对应函数。对于此类重定位链接器为其构造.plt与.got.plt;
.rodata引用:链接器解析重定条目时发现两个类型为R_X86_64_PC32的对.rodata的重定位(printf中的两个字符串),.rodata与.text节之间的相对距离确定,因此链接器直接修改call之后的值为目标地址与下一条指令的地址之差,指向相应的字符串
objdump -d
-r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它;为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用got中地址跳转到目标函数
延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT [0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT [0]和GOT [1]包含动态链接器在解析函数地址时会使用的信息。GOT [2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目
Hello 的ELF格式文件中查找到,.got.plt起始位置为0x00601000
在调用dl_start之前0x601008后的16个字节均为0
调用_start之后发生改变,0x601008后的两个8个字节分别变为:0x7fbe330ea170、0x7fbe328680,其中GOT [0](对应0x600e28)和GOT [1](对应0x7fbe330ea170)包含动态链接器在解析函数地址时会使用的信息。GOT [2](对应0x7fbe328680)是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数;
0x7fbe330ea170指向重定位表:
0x7fbe328680指向重定位表:
在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问跳转时GOT地址为下一条指令,将函数序 压栈,然后跳转到PLT[0],在PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序 和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
本章通过解释链接的概念及作用,和分析hello的ELF格式,以及hello的虚拟导致空间,重定位过程,执行流程,和动态连接过程,深入学习了hello.o 可重定位文件到hello可执行文件的流程,和链接的各个过程
懵懵懂懂的我笨笨磕磕的将hello world一字一键敲进电脑存成hello.c(Program),经过预处理、编译、汇编、链接,历经艰辛,Hello一个完美的生命诞生了。
然后在壳(Bash)里,伟大的OS(进程管理)为hello进程fork(Process),为hello进程execve, mmap,分时间片,让hello得以在Hardware(CPU/RAM/IO)上驰骋(取指译码执行/流水线等)
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是一个执行中的程序的实例,系统中每一个程序都运行在某个进程的上下文中,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动过程调用的指令和本地变量。
作用:
6.2 简述壳Shell-bash的作用与处理流程
Shell的作用:
1.shell是计算机用来解释你输入的命令然后决定进行何种处理的程序。Shell 是指一种应用程序,Shell应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务
2.可交互,和非交互的使用shell。在交互式模式,shell从键盘接收输入;在非交互式模式,shell从文件中获取输入。
3.shell提供了少量的内置命令,以便自身功能更加完备和高效。
4.shell除了执行命令,还提供了变量,流程控制,引用和函数等,类似高级语言一样,能编写功能丰富的程序。
Shell处理流程:
由输入设备读取命令(键盘或者文件)
将输入字符串转为计算机可以了解的机械码(分割命令字符串),然后执行它。
- Shell程序先识别输入的指令的序列,解析这个这个命令,看是否是内置命令,是的话直接执行该命令,子进程中加载并执行这个文件。否则调用相应的程序为其分配子进程并运行
6.3 Hello的fork进程创建过程
fork进程:父进程通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时。子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程最大的区别在于他们有不同的PID
1.调用一次,返回两次;在父进程中fork会返回子进程的PID(总为非零值),在子进程中fork会返回0 ;2.并行执行:父进程与子进程是并发运行的独立进程。内核能够以任何方式交替执行他们逻辑控制流中的指令;3.相同但是独立地址空间;4.共享文件
Shell程序fork简单进程图
6.4 Hello的execve过程
fork函数子进程之后,子进程调用execve函数,在当前进程上下文加载的上下文中加载并运行一个新的hello程序,
execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行hello程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。
加载器虚拟内存映像为:
最后加载器设置PC指向_start地址,_start最终调用hello中的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存
Hello栈结构:
6.5 Hello的进程执行
6.5.1逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程
6.5.2并发流:一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发地执行。多个流并发地执行的一般现象被称为并发。一个进程和其他进程轮流运行的概念被称为多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫时间分片
6.5.3私有地址空间:提供一种假象,含香我们的程序独立的使用内存系统;一个程序的空间中某个地址相关联的内存字节是不能被其他进程读或者是写的;
6.5.4用户模式和内核模式:处理器通过某个控制寄存器中的一个模式位来提供限制一个应用可以执行的指令以及它可以访问的地址空间范围的功能。该寄存器描述了当前进程享有的特权。当设置了模式位时,进程就运行在内核模式中。没有设置模式位时,进程就运行在用户模式中。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存;用户模式的进程不允许和执行特权指令、也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据
6.5.5上下文切换:较高形式的异常控制流来实现都任务;内核为每个进程维持一个上下文,上下文就是内核重新启动的一个被强占的进程所需的状态。由包括通用目的寄存器、浮点寄存器、程序计数器、用户站、状态寄存器、内核栈和各种内核数据结构
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这个决策就叫做调度,是由内核中称为调度器的代码处理的。在内和调度了一个新的进程运行后,它就抢占当前进程,并使用上文所述的上下文切换的机制将控制转移到新的进程,上下文切换机制:1.保存当前上下文;2.回复某个先前被抢占的进程的上下文;3.将控制传递给新进程
Hello上下文进程切换:
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.6 hello的异常与信 处理
hello执行过程中出现的异常种类可能会有:中断、陷阱、故障、终止。
中断是异步发生的,是来自处理器外部的I/O设备的信 的结果。硬件中断的异常处理程序被称为中断处理程序。
陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则处理程序返回到内核中的abort例程,abort例程会终止引起故障的应用程序。
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者SRAM位被损坏时发生的奇偶错误。终止处理
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!