P2P: From Program to Process
在编译器的处理下,hello.c文件经历预处理、编译、汇编、链接,四个步骤,变为可执行文件(program)然后由shell为其创建一个新的进程(process)并运行它。
020: From Zero To Zero
在它还没有被执行的时候(Zero),shell先为其映射出虚拟内存,然后在开始运行进程时写入其中,进程结束后由shell回收。
硬件环境:
X64CPU; 8GHz; 8GRAM; 1TB HD
软件环境:
Windows10 64位;VMware14.12; Ubuntu 16.04 LTS 64位
使用工具:
codeblocks,objdump,gdb,edb,hexedit
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
(hello的故事就这样开始了)
预处理器根据以字符#开头的命令,修改原始的c程序。预处理器会读取头文件的内容并直接插入程序文本中,替换宏定义,清理注释,并以.i作为新的到的文件的扩展名。
作用:使其规格至能被汇编器正确编译为汇编文件
linux 下命令为 gcc -E xxx.c -o xxx.i
意思为将xxx.c 预处理并将结果放在xxx.i
编译结果
图2.2.1编译结果
预处理结束后文件大了很多
图2.3.1hello.i文件属性
打开文件发现主要有以下几种文本结构
描述动态库位置
图2.3.2hello.i文件部分截图
声明变量类型
图2.3.3 hello.i中声明部分
函数声明
图2.3.4 函数声明部分
以及修改过的hello.c部分
图2.3.5 源码部分
预处理是对c文件文本上的处理,简单但是十分重要,它补全了编译所需信息。
(于是hello拿走了全村所有的[头文]剑,出发了,但他好像…只带了声明。)
编译是指.i文件在编译器的作用下生成相应的汇编文件(.s)的过程。
这个过程将较偏向自然语言的c文件,转换为偏机器语言的汇编文件,为下一步的汇编生成机器码创造了条件,同时也保持了一定的可读性,和微弱的可移植性。
gcc -S xxx.i -o xxx.s
图3.2.1编译结果
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.3.1 全局变量 sleepsecs:
汇编中
c文件中
全局变量在汇编文件中直接被标在.globl段,并在之后data声明其值,由于其为int类型所以给其分配的值为2
(师傅[编译器]拿出小刀,并推荐hello练辟邪剑法)
3.3.2 字符串
字符串被放在rodata节中如下图所示
3.3.3 常数
被表示为立即数
3.3.4 关系表达式
利用比较操作,并根据比较得来的条件码完成对算式的求解
1 argc != 3
2 i < 10
3.3.5 if
采用条件控制跳转的方式来实现
3.3.6 for
采用条件控制跳转的方式来实现,并每次进行i++
3.3.7 ++
采用add $1完成该运算
3.3.8 函数操作
利用call,完成对函数的调用,并在这之前完成对参数的处理
利用ret实现return
3.3.9 局部变量
int I 被放在寄存器中
本章主要阐述了编译器是如何处理C语言的各个数据类型以及各类操作的,
(hello:我变强了,也变长了)
图 4.2.1汇编结果
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
ELF格式如下图所示:
图4.3.1 ELF文件格式
使用readelf -S得到各节信息如下图所示
图4.3.2 hello.o中的各段信息
重定位分析:
用readelf -r获取hello.o的重定位信息其结果如下
1 .rela.text:
2. rela.en_frame:
图 4.3.3 4.3.4 hello.o中重定位信息
info 标注了重定位类型和重定位信息位置(符号表序号), offset标注了需要重定位信息的地方
(以下格式自行编排,编辑时删除)
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
机器语言由 指令码+操作数 构成
图4.4.1比较结果
机器语言在很大程度上保留汇编文件的逻辑,但在以下方面仍有区别
1 分支转移 .o中已经为函数跳转预留了空间,而在.s中这些被保留为段名称
2 函数调用:在.s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。因为hello.c中调用的函数都是共享库中的函数,需要通过动态链接器才能确定函数的运行时执行地址,在汇编的时候,将call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text节中为其添加重定位条目,等待静态链接的进一步确定。
3全局变量访问:在.s文件中,访问rodata(printf中的字符串),使用段名称+%rip,在反汇编代码中0+%rip,因为rodata中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全0并添加重定位条目。
本章介绍了hello从hello.s到hello.o的汇编过程,介绍了elf文件的结构,比较了hello.o反汇编代码与hello的差异
(hello:我现在都不知道我要说啥[rodata还没被ld重定位])
链接(linking)是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。链接可以执行于编译时(compile time),也就是在源代码被编译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到内存并执行时;甚至于运行时(run time),也就是由应用程序来执行。链接是由叫做链接器(linker)的程序执行的,链接器使得分离(separate compilation)编译成为可能。
图5.2.1 链接命令
图5.3.1 可执行目标hello的各段信息
可以看出,比起前边hello.o中的elf文件这里的条目明显变多了,但格式仍未有太大变化
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
打开后可以发现进程被装在以0x400000开始的位置如下图所示:
图5.4.1 hello开始位置
之后我们看其他段的位置:
图5.4.2 其他段信息
其中:
PHDR保存程序头表。
INTERP指定在程序已经从可执行文件映射到内存之后,必须调用的解释器(如动态链接器)。
LOAD表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了常量数据(如字符串)、程序的目标代码等。
DYNAMIC保存了由动态链接器使用的信息。
NOTE保存辅助信息。
GNU_STACK:权限标志,标志栈是否是可执行的。
GNU_RELRO:指定在重定位结束之后那些内存区域是需要设置只读
可知其分别在0x40000和0x60000存放内容,这与5.3中的偏移量有一点冲突.但实际上原位置还是有一份副本的.
(以下格式自行编排,编辑时删除)
反编译hello
观察发下多了以下章节:
.init 程序初始化执行的代码
.plt 静态连接的连接表
.plt.got 保存函数引用的地址
.fini 程序正常终止时执行的代码
其他的区别: 如下图所示
图5,5,1 反编译文件比较
其具体过程如下: 在使用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。之后,链接器解析重定条目时发现两个类型为R_X86_64_PC32的对.rodata的重定位(printf中的两个字符串),.rodata与.text节之间的相对距离确定,于是链接器直接修改call之后的值为目标地址与下一条指令的地址之差,使其指向相应的字符串。
_dl_start 地址:0x7ff806de3ea0
_dl_init 地址:0x7f75c903e630
_start 地址:0x400500
_libc_start_main 地址:0x7fce59403ab0
_cxa_atexit 地址:0x7f38b81b9430
_libc_csu_init 地址:0x4005c0
_setjmp 地址:0x7f38b81b4c10
_sigsetjmp 地址:0x7efd8eb79b70
_sigjmp_save 地址:0x7efd8eb79bd0
main 地址:0x400532 (puts 地址:0x4004b0 exit 地址:0x4004e0)
print 地址:0x4004c0
sleep 地址:0x4004f0 (以上两个在循环体中执行10次)
getchar 地址:0x4004d0
_dl_runtime_resolve_xsave 地址:0x7f5852241680
_dl_fixup 地址:0x7f5852239df0
_uflow 地址:0x7f593a9a10d0
exit 地址:0x7f889f672120
dl_init之前
图5.7.1 dl_init之前
之后:
图 5.7.2 dl_init之后
说明调用动态连接器之后got被写入正确地址之后在访问就可以访问到动态连接的函数了
本章主要介绍连接的过程以及重定位具体实现,比较了链接前后文件有哪些不同.
(hello:我终于神功大成,可以出去闯荡一番了)
(第5章1分)
进程是一个执行中的程序的实例,它好像是独占地使用处理器和内存。处理器就好像无间断地一条条执行我们程序中的命令。最后我们程序中的代码和数据好像是系统内存中唯一的对象。
这种假象便是进程这种抽象带来的。
(hello:我当年可是住着8GB的大房子。[hello妄想中])
shell是一个交互型的应用级程序,它代表用户运行其他程序
处理流程如下:
1读取终端输入命令
2解析命令,并直接执行内部命令
3 分配新进程运行外部命令
4 程序运行期间要接受信号,并产生相应的反应
5 回收已结束的进程
hello在运行过程中shell调用fork(),内核会创建一个新的进程,给与其新的PID,并为其分配与其父进程相同的上下文,然后在两个进程中fork()返回不同的值,至此为创建的hello的fork()进程创建成功。
在6.3中的fork()得到新进程后shell使用execve(“hello”,argv,envp)调用驻留在内存中被称为启动加载器的操作系统代码来执行hello程序
以下为其执行步骤:1 删除已存在的用户区域
2 映射私有区域
为新程序的代码、数据、bss、和栈区创建新的区域结构,这些区域都是请求二进制零的
3 映射共享区域
4 设置PC
处理后的虚拟内存如下图所示
图 6.5.1 hello的虚拟内存分布
然后在内核的调度下,hello便开始被执行。
在讲述进程进行过程中,我们不妨先来回忆下一些定义:
逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成
hello进程中有如下片段
其中调用sleep时会引发上下文切换(其余的时候也会发生上下文切换,但是由于不可控就不在这里叙述了)
其过程上下文与状态转换如下图所示。
图6.5.1 上下文切换
于是hello执行过程为 判断参数是否合法->反复输出等待(上下文切换发生)->等待用户输入一个字符->结束->被shell回收
1 程序正常结束 shell收到SIGCHILD信号然后将其回收
图 6.6.1 测试1
可以看出shell的确会回收正常结束的进程
2 不停乱按
图 6.6.1 测试2
不停乱按是没有效果的,shell对等待的过程的这种信号应该是默认屏蔽
3 回车 同 2
4 Ctrl+c
图 6.6.1 测试3
hello进程当场去世。对于这种信号,shell会结束前台进程。
(hello:为什么?为什么?!我明明经历了这么多,一个Ctrl+z就把沃….不要啊)
5 Ctrl+z +ps/jobs/pstree/fg/kill
图 6.6.1 测试4 图 6.6.1 测试5
图 6.6.1 测试6
shell对于这种信号行为为挂起前台。
(hello:你们竟然把沃抓起来,还各种调戏,还不如一个CTRL+C杀了我算了)
本章介绍了进程的概念和作用,描述了shell如何在用户和系统内核之间建起一个交互的桥梁,以及hello是怎么被运行和回收的。
(hello的作为程序一生到这里就结束了,接下来,让我们跟随考古学家去挖一挖hello的坟吧[笑])
(以下格式自行编排,编辑时删除)
逻辑地址:逻辑地址指由程序产生的与段相关的偏移地址部分。在有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。hello中hello.o\hello.s中的左侧标记地址便是一种逻辑地址
线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,其偏移量加上基地址就是线性地址。hello的反汇编文件中看到的地址(即逻辑地址)中的偏移量,加上对应段的基地址,便得到了hello中内容对应的线性地址。
虚拟地址:使用虚拟寻址时,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址在被送至内存前先转换成适当的物理地址。它数值上等于先行地址
物理地址:计算机系统的主存被组织成一个M个连续字节大小的单元组成的数组,每字节都有一个独立的物理地址。hello中在内存的数据每字节对应一个地址
(以下格式自行编排,编辑时删除)
逻辑地址由段选择符和偏移量组成,线性地址为段首地址与逻辑地址中的偏移量组成。其中,段首地址存放在段描述符中。而段描述符存放在描述符表中,也就是GDT(全局描述符表)或LDT(局部描述符表)中。
段选择符如下:
图7.2.1 段描述符的格式
其过程如下图所示:
图7.2.2 变换过程
(以下格式自行编排,编辑时删除)
将hello的线性地址看成VPN 和VPO两部分,物理地址看成PPN和PPO
图7.3.1 两种地址之间关系
其中VPO和PPO是相同的可以直接转换.
而从VPN到PPN则先要通过VPN和页表基址寄存器(PTBR)访问相应常驻内存的页表得到物理页号PPN.
图7.3.2 访问模式
若访问失败,则引发缺页异常,之后会选择一个牺牲页,将其写回磁盘然后加载我们访问的目标进入磁盘.若无法加载则引发段错误.
图7.3.2缺页处理
图7.4.1 TLB与四级页表支持下的VA到PA的变换
在知道虚拟地址之后首先回去访问TLB,查询目标是否在TLB中,查询信息如下,以TLBI作为组索引,TLBT作为tag访问,若命中则直接获取PPN
在TLB不命中的情况下,对于36位VPN来说MMU将其分为4份9位偏移量,第一份和CR3控制寄存器合成一级页表中的目标PTE地址,这个PTE指向你需要的二级页表首地址加上第二份偏移量获得第二个页表中目标PTE的地址,以此类推,并在第四级页表得到PPN,之后将该信息写入TLB
如图所示,MMU将物理地址发给L1缓存,缓存从物理地址中取出缓存偏移CO、缓存组索引CI以及缓存标记CT。若缓存中CI所指示的组有标记与CT匹配的条目且有效位为1,则检测到一个命中,读出在偏移量CO处的数据字节,并把它返回给MMU,随后MMU将它传递给CPU。若不命中,则需到低一级Cache(若L3 cache中找不到则到主存)中取出相应的块将其放入当前cache中,重新执行对应指令,访问要找的数据。
图7.5.1 cache
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给他一个唯一的pid。为了给这个新进程创建虚拟内存,他创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有写时复制。当fork从新进程返回,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,也就为每个进程保持了私有地址空间的抽象概念。
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间的细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放。这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
为了分配这些块需要一些特殊结构,而有时为了满足一些特殊的分配方式还需要一些其他的数据结构,例如 隐式\显式空闲链表,头部脚部标签等等.而为了更好的满足吞吐量和内存利用率的需求还需要一些算法辅助,比如基于红黑树的适配
本章主要介绍了存储器地址空间、逻辑地址到线性地址的变换-段式管理机制、线性地址到物理地址的变换-页式管理机制、TLB与四级页表支持下的VA到PA的变换的具体过程、三级Cache支持下的物理内存访问、fork时的内存映射、execve时内存映射、缺页故障与缺页中断处理机制 和 动态存储分配管理机制。
(第7章 2分)
设备的模型化:文件
设备管理:unix io接口
所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应的文件的度和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单的、低级的应用接口,称为Unix I/O。
以下为被统一的操作:1打开文件
2进程的三个初始打开文件
3改变文件位置
4 读写文件
5 关闭文件
其函数分别为
1 打开文件
2 关闭文件
3 读和写文件
4 读取文件元数据
5 I/O重定向
printf代码如下:
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
getchar源码如下:
图8.4.1 getchar源码
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
本章节描述了linux的I/O机制,分析了printf和getchar函数的实现。
即了解了hello是怎么向你打招呼的
(第8章1分)
hello的一生是被安排的明明白白的一生
1 通过文本编辑器写完代码,形成c文件
2 被预处理器预处理为hello.i
3 被编译器处理成汇编文件
4 与其他可重定位文件和动态链接库链接成为可执行文件
5 在shell中被分配一个进程
6 fork() + execve()让内核给他分配好了空间,3级cache让其可以高速访问内存,优秀的动态存储分配体系让其运行无阻
6.5 执行时函数调用机制,和中断机制让字符输在屏幕上
7 执行过程中被内核安排进程切换以及信号处理
8 执行结束后被shell回收
hello虽然简单,但从代码到屏幕上的一句问候,确是我们一代代人的心血
我们从未看轻过hello,从未忘记他,一代又一代的新鲜血液,不都是从helloworld学起的么
hello.c 实验源文件
hello_1_i.i 预处理文件
hello_1_s.s 汇编文件
hello_1_o.o 由hello_1_s.s编译得到的可重定位二进制文件
hello_objdump_s.s 由hello_1_o.o反编译得到的汇编文件
hello_2_objdump.s 由hello反编译得到的汇编文件
hello 由hello_1_o.o链接的到的可执行文件
为完成本次大作业你翻阅的书籍与网站等
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.
[7] https://www.cnblogs.com/pianist/p/3315801.html
[8] https://www.cnblogs.com/wangcp-2014/p/5146343.html
[9] https://blog.csdn.net/tuxedolinux/article/details/80317419
[10] https://www.cnblogs.com/zengkefu/p/5452792.html
[11] https://blog.csdn.net/gdj0001/article/details/80135196\
[12] https://blog.csdn.net/weixin_41506416/article/details/85328814