2019年12月
摘 要
任何一个程序从产生到执行再到死亡,都经历了一个复杂的处理过程,其中涉及了全部计算机的关键知识,通过hello的生命历程,将学习一个程序从预处理、编译、汇编、链接、shell分配、内存回收以及其中函数调用部分内容的知识。这是每个程序员的必由之路。
关键词:程序的各个阶段、程序员的必由之路、程序运行的相关知识
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第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 本章小结 - 5 -
第3章 编译 - 6 -
3.1 编译的概念与作用 - 6 -
3.2 在UBUNTU下编译的命令 - 6 -
3.3 HELLO的编译结果解析 - 6 -
3.4 本章小结 - 6 -
第4章 汇编 - 7 -
4.1 汇编的概念与作用 - 7 -
4.2 在UBUNTU下汇编的命令 - 7 -
4.3 可重定位目标ELF格式 - 7 -
4.4 HELLO.O的结果解析 - 7 -
4.5 本章小结 - 7 -
第5章 链接 - 8 -
5.1 链接的概念与作用 - 8 -
5.2 在UBUNTU下链接的命令 - 8 -
5.3 可执行目标文件HELLO的格式 - 8 -
5.4 HELLO的虚拟地址空间 - 8 -
5.5 链接的重定位过程分析 - 8 -
5.6 HELLO的执行流程 - 8 -
5.7 HELLO的动态链接分析 - 8 -
5.8 本章小结 - 9 -
第6章 HELLO进程管理 - 10 -
6.1 进程的概念与作用 - 10 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 10 -
6.3 HELLO的FORK进程创建过程 - 10 -
6.4 HELLO的EXECVE过程 - 10 -
6.5 HELLO的进程执行 - 10 -
6.6 HELLO的异常与信号处理 - 10 -
6.7本章小结 - 10 -
第7章 HELLO的存储管理 - 11 -
7.1 HELLO的存储器地址空间 - 11 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 11 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -
7.5 三级CACHE支持下的物理内存访问 - 11 -
7.6 HELLO进程FORK时的内存映射 - 11 -
7.7 HELLO进程EXECVE时的内存映射 - 11 -
7.8 缺页故障与缺页中断处理 - 11 -
7.9动态存储分配管理 - 11 -
7.10本章小结 - 12 -
第8章 HELLO的IO管理 - 13 -
8.1 LINUX的IO设备管理方法 - 13 -
8.2 简述UNIX IO接口及其函数 - 13 -
8.3 PRINTF的实现分析 - 13 -
8.4 GETCHAR的实现分析 - 13 -
8.5本章小结 - 13 -
结论 - 14 -
附件 - 15 -
参考文献 - 16 -
第1章 概述
1.1 Hello简介
P2P:hello.c经过预处理、编译、汇编、链接、shell执行的过程由程序变为进程。
O2O:hello由0到开始执行变成一个进程,再到结束生命,所占内存被回收为0的过程。
1.2 环境与工具
X64 CPU;2GHz;8G RAM;1THD Disk
Windows 10 64位;Deepin 15.01 64位
Edb、visual studio code、g++、gcc、cpp
1.3 中间结果
Hello.i:完成预处理的相关内容
Hello.s:完成汇编的相关内容
Hello.o:完成编译的相关内容
Hello:完成链接和进程管理相关内容
Hellodisam.txt:用来保存临时的objdump重定向文件,与编译和链接部分相关
Helloelf.txt:用来保存临时的readelf重定向文件,与汇编和编译部分相关
Hellotemp.txt:用来保存临时的objdump重定向文件,与编译部分相关
1.4 本章小结
虽说是概述,但我是在完成全部内容之后来写的,本章小结也是全文的总结,烦请移步总结。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理是在源程序到被翻译成二进制程序的之间一个过程,预处理器将把源程序转换为经过修改的“源程序”。
它的作用是处理以“#”开头的语句,在C和C++中,它分为这样的几个模块:1.#define部分,预处理对这一部分执行define之后的符号替换,将把程序中的指定符号替换为该符号被指定的值;2.#include部分,预处理对这一部分执行包含行为,将根据预设的行为去寻找include后面由<>或””包括起来的头文件,将它们与源程序整合起来;3.#ifdef…等内容,这是条件编译的部分,预编译将处理相对应的语句,设置条件;4.#error部分,编译遇到错误信息会报错;5.#pragma部分,十分复杂,暂无能力理解;6.#undef,撤销宏定义部分,与1对应。
预编译还会处理注释部分,经过预编译后我们的hello.c将进化成为hello.i。
2.2在Ubuntu下预处理的命令
cpp -o hello.i hello.c
图2-2-1 预处理hello.c
2.3 Hello的预处理结果解析
图2-3-1 hello.i的部分代码1
2-3-2 hello.i的部分代码2
2-3-3 hello.c的部分代码
可以看到,原先hello.c中的#include部分,被预编译器cpp处理了,接下来的两千多行代码是原先stdio.h、unistd.h、stdlib.h文件加载来的内容。而这三千行的代码中,也不再包含注释的内容。在最后的几行,可以看到hello.c的主程序是没有被预处理器改动的。
2.4 本章小结
预处理是hello.c从program(程序)到process(进程)的第一步,在这一步里,hello.c发生了小小改变,成为了hello.i,抛弃了注释,得到了更多的信息(指头文件的内容、宏定义的内容(这部分hello没有,不过如果是拥有#define语句的程序,那么就有这一部分)),准备好接受编译器的处理了。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译是将经历过预处理的“源程序”转换为一致的汇编语言程序的过程。
在编译器工作的途中经历了这几个阶段:1.源程序分析:分析.i文件,检查词法、语法,如果发现错误则报错;2.进行可能的优化;3.汇编代码生成。
经历过预处理的程序,结构更加清晰,信息更加丰富,编译器能够统一地将它们翻译成约定好的汇编语言程序以便于机器的执行。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3-2-1 编译hello.i
3.3 Hello的编译结果解析
3.3.1编译器对数据的处理
- 常量
3-3-1(a 常量
我们可以看到,LC0、LC1字符串是常量,代码执行对它们的引用。对于LC0我们还看到,编译器将汉字按照编码规则转换成了编码。
3-3-1(b 常量
立即数也是常量,编译器对这类常量的处理方式是直接编入流程之中,图中$开头的数据就是。
2. 变量
结合图2-3-3的hello.c的部分代码和图3-3-1(b的内容,我们来看下面这张图:
其中由2-3-3知,sleepsecs是一个全局变量,它被初始化为2.5,i是一个局部变量,它被用在for循环中当条件。存储在栈上,由3-3-1(b我们知道hello.s给i在栈上分配了4个字节的空间。而sleepsecs很奇怪,已初始化的全局变量应该在汇编阶段放入.data段,而hello.s应该告诉汇编器怎么做,根据图3-3-1(a,hello.s将其归入.rodata段,我不明白为什么这么做。经过查阅发现,编译器生成的文件伪代码内容只能由程序解释,因此我无从得知它们对sleepsecs做了什么。
3. 表达式
形如i++之类的式子,编译器解析完它们的作用后生成对应的代码。
4. 类型
编译器根据类型的不同,进行标注,比如i变量,编译器直接把它的类型int转换为int的大小,在栈上给i变量根据类型的大小腾出一块空间。
3.3.2编译器对操作的处理
- 赋值操作
3-3-2(a i的赋值
以i为例,参考图3-3-2(a,编译器使用movl指令将立即数1赋值给了i。
而sleepsecs的赋值由图3-3-1(a给出,直接定义了值,进行了一次舍入。
3.3.3编译器对算数操作的处理
编译器使用add立即数和寄存器以及栈上内容,完成了对hello.c的算数操作的处理。
3.3.4编译器对关系和控制转移的操作
参考i的使用,编译器通过cmp指令来生成设置标志位的代码,然后程序将根据标志位执行不同的代码。
3.3.5编译器对函数操作
3-3-5 函数调用
函数调用设计了传递函数,在hello.s中只体现了传递第一个参数使用edi寄存器,然后执行call指令调用函数,实际上函数参数是按照顺序依次由:edi、esi、edx、ecx、e8、e9、栈部分(由后向前逐个压栈)进行传递的,hello.s没能体现这些。而返回值一般用eax寄存器,这里亦没有体现。
3.4 本章小结
通过对hello.s文件的观察分析,解释了编译器对hello.i的改动,这些改动由于规则的确定性有了参考价值,对于其它程序经历编译器的结果可以有一个初步预知。通过这一步,程序转换成为了更亲近机器的代码,渐渐不能被人所理解的过程中,程序更能被机器执行了。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编是将汇编程序转化为二进制文件的过程。
这一过程是为了让机器能够更好地执行代码。
4.2 在Ubuntu下汇编的命令
gcc -m64 -no-pie -fno-PIC hello.c -o hello.o
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
- elf头
ELF头包含了文件的许多重要信息,包括数码表示形式、目标文件的类型、节头的数量、机器类型等。
4-3-1 hello.o的ELF头信息
2.节头部表
表中包含了各节的名称、大小、对齐等信息。
4-3-2 hello.o节头部表
3.可重定位节
包含了可重定位节的相关信息,如下:
4-3-3 hello.o可重定位节信息
4.符号表部分
包含了程序中设计到的全局变量和函数等。
4-3-4 符号表部分信息
4.4 Hello.o的结果解析
4-4-1 对比图片
反汇编的内容明显更加丰富。
首先,有关分支转移,hello.s中还是比较贴近自然语言的,条件跳转的符号也是更加抽象的,而反汇编出来的代码中,抽象的符号由具体的地址来替代了。
其次,函数调用也是类似的情况,只不过换了一个寻找函数位置的方式。
最后,关于全局变量的寻找,是0 + rip的值,这是因为需要在程序实际运行之中我们才能知道rodata被重定位到什么地方了。
4.5 本章小结
在这一章,着重关注了hello.o文件的相关信息,对比了反汇编和汇编代码,加深了汇编代码是贴近自然语言的过渡语言这一事实,了解了二者之间的部分差异。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程。
作用:1.使分离编译变成可能;2.用来整合不同程序的代码片段。
------------《深入理解计算机系统》第三版
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-2-1 链接
5.3 可执行目标文件hello的格式
5-3-1 elf头
Elf头和之前类似,不再重复,与之前不同的是程序有了入口点地址、程序的节头的数量不同、字符串表索引节头量不同等。
5-3-2 节头
与4中相比,多了几个节的信息,地址也加入了。
5-3-3 程序头
阅读节头,英语单词不难,包括了偏置量、虚拟地址、物理地址、文件大小、内存大小、标志、对齐方式。
5-3-4 符号表
与4中的分析相同,少了一些条目,引入了更多的符号(4中的内容只有hello程序自定义的几个符号,而这里则把hello调用的其它头文件的函数的符号解析一并放入了)。
5.4 hello的虚拟地址空间
5-4-1 edb的data dump部分内容截图(a
根据5.3的elf头信息,我们查看0x4004e0地址处的内容(在elf头中,这里被标志为入口点地址),很遗憾是一堆乱码。
再来根据5.3的节头信息看一看,看第一个节(.interp)的信息,可以得到是0x4004e0,找到它,如下:
5-4-2 edb的data dump部分内容截图(b
这是一个外部链接进来的文件的标识。根据5.3的节头,我们可以一个一个的寻找到对应节在该进程data dump的信息。
5.5 链接的重定位过程分析
5-5-1 hello反汇编和hello.o反汇编的对比(a
最明显的一点就是,链接好的hello反汇编的结果多了许多节(如.plt和.init等),而hello.o的反汇编仅仅有.text的内容。也出现了更多外部的函数。
5-5-2 hello反汇编和hello.o反汇编的对比(b
从这张图我们还可以看到,原先相对地址的地方换成了虚拟地址的形式。
通过以上几点,再加上查阅各个节内容的作用后,得到以下有关链接的结论:
- 链接为程序中使用但是未定义的符号提供了外部的定义(如printf函数之类的)。
- 关于函数和只读数据(printf的格式化字符串等)调用的方面,由于所用的函数都被编辑进入了程序,那么相对地址可以确认,虚拟地址也可以分配好,区别就体现出来了。
5.6 hello的执行流程
对于edb的操作不熟悉,未能完成该项任务。
5.7 Hello的动态链接分析
对于edb的操作不熟悉,未能完成该项任务。
5.8 本章小结
这个章节介绍了链接相关知识,回顾了edb工具的使用方法,分析了链接前后程序的区别等。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的经典定义是一个执行中程序的实例。
进程的作用是提供给了应用程序两个关键抽象:1.一个独立的逻辑控制流;2.一个私有的地址空间。
------------------《深入理解计算机系统》
6.2 简述壳Shell-bash的作用与处理流程
作用:代表用户运行其它程序。
处理流程:读取来自用户的一个命令行、求值步骤解析命令行并代表用户运行程序。
------------------《深入理解计算机系统》
6.3 Hello的fork进程创建过程
父进程创建子进程。子进程几乎但不完全与父进程相同。子进程得到与父进程用户虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程获得与父进程任何打开文件描述符相同的副本,这就意味着父进程调用fork时,子进程可以读写父进程任何打开的任何文件。父进程和新创建的子进程之间最大的区别在于他们拥有不同的PID。
---------------------《深入理解计算机系统》
由此我们可以得出:hello的fork执行过程中,shell-bash也这样打开了一个副本,供接下来的execve函数使用。
6.4 Hello的execve过程
Execve函数在当前进程的上下文中加载并运行一个新的程序。Execve加载并运行可执行目标文件filename,且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。
---------------------《深入理解计算机系统》
结合6.3,shell-bash执行了fork,为hello的运行创造了一个环境,输入的命令行中,execve将找到hello文件的位置,根据传递进来的参数去运行hello,运行过程中,原先shell-bash的子进程的上下文内容都将被替换,原先的进程空间里面仅保留文件描述符表,取而代之的是hello相关的上下文。
6.5 Hello的进程执行
在sleep函数的时候,hello的进程将收到一个挂起的信号,而在此之前程序是正常执行的,所以这里我们着重来看sleep函数附近的内容。
当挂起的时候,shell将hello挂起,执行了调度,前台运行其它程序,那么这里涉及到了一次用户态到系统态再到用户态的切换(这一过程蕴含了许多东西,其中最重要的一点就是原先hello相关的上下文的保存问题),切换的结果是hello挂起,其它程序前台化。而sleepsecs秒数后,shell又收到一次信号,只是这一次信号是让hello变为前台,因此再一次执行前述的内容。
6.6 hello的异常与信号处理
(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6-6-1 pstree
Pstree执行在两次间隔之间,我认为原因是shell对hello执行了6.5所述的操作。
6-6-2 ps与Ctrl + Z
该条指令在hello挂起之后,ps证明了hello只是被停止。
6-6-3 jobs
进一步证明收到ctrlz的hello只是被停止。
6-6-4 fg
fg是shell的内置命令,执行时将hello给重新放到前台,hello继续执行。
Ctrl+C则是直接终止。
6.7本章小结
这一章是hello故事的一个阶段的结束了,到此为止,hello已经完成了“P2P”的部分,真正地从一个程序到一个进程了。本章则是hello成为进程的相关内容的复习。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:源程序中使用的地址,比如hello.s的调用中体现的
虚拟地址:链接后程序中使用的地址,比如hello.o的反汇编里面显示的
物理地址:主存被划分的地址,hello运行时的实际的在主存上的地址内容
7.2 Intel逻辑地址到线性地址的变换-段式管理
关于逻辑地址和虚拟地址的关系我不好评价,目前没能获得相关知识。我只能假设虚拟地址与逻辑地址存在某种映射。
而线性地址是有别于分段得到的二维地址的,那么经查,有如下关系:段地址*16+偏移地址,这样得到的地址便是线性地址。因此这部分内容要做的就是将段地址转换成线性的。
又查得,线性地址就是虚拟地址,那么这个任务变为了由逻辑地址转换为虚拟地址的过程。由此,段式管理:逻辑地址–》虚拟地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
由7.2,这里是虚拟地址转换为物理地址的过程,那么涉及到VPN、VPO、PPN、PPO之间的关系。CPU部件中的MMU解析虚拟地址,通过访问主存获取PTE条目表,查询表得到PPN,而VPO又与PPO相同,据此可以得到物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB又称快表,用来存储PTE,结合7.3的内容,四级页表实际上是将VPN进行分段,主存中常驻一级页表,低级页表的信息由临时查询得到,高一级页表或指向第一级页表或指向物理地址,借由这样的层级结构,最终可以查到PPN,再由7.3中内容得到PA。
7.5 三级Cache支持下的物理内存访问
解析PA,将PA分为CT、CI、CO,根据索引CI找到对应的组,在根据CT进行碰撞,如果命中则根据CO取字,否则执行替换策略,访问下一层缓存的内容。
7.6 hello进程fork时的内存映射
当fork函数被shell进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
---------------------《深入理解计算机系统》
其中私有的写时复制机制将在两个进程中任意一个将要修改内存的时候复制一份新副本供修改而不去动共享的内容。
7.7 hello进程execve时的内存映射
Exeve函数在当前进程中加载并运行包含在可执行目标文件a.out中的程序,用a.out程序有效地代替了当前程序。加载并运行a.out需要以下几个步骤。
1.删除已经存在地用户区域,即删除当前进程虚拟地址地用户部分中的已经存在的区域结构。
2.映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些结构都是私有的、写时复制的。代码和数据区域被映射为a.out文件中的.test和.data区。Bss区域是请求二进制零的,映射到匿名文件,其大小包含在a.out中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域,如果a.out程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC) execve做的最后一件事情就是设置当前进程上下文的程序计数器,并使之指向代码区域的入口点。
---------------------《深入理解计算机系统》
7.8 缺页故障与缺页中断处理
对页的访问如果不命中那么MMU触发缺页故障,shell调用相关程序决定牺牲块和换上来的新块,执行完毕则交还给异常发生处的代码继续执行。
7.9动态存储分配管理
有显式分配器和隐式分配器之分,调用了malloc则说明这是一个显式的分配。
那么对于块的结果有隐式空闲链表和显式空闲链表和分离式空闲链表等分别;策略上又有首次适配、下一次适配、最优适配等策略;具体实现上还可以有红黑树优化等操作;实际问题中还要考虑合并、吞吐量的问题。
总之要达到对内存的块进行分配、记录、控制的效果。
7.10本章小结
本章是O2O的部分,hello在本章从0到有,在从有到0,靠得是本章的内存分配机制。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
文件分为多种:
- 普通文件:编码上可能有不同,但是对内核来说就是一串二进制数。
- 目录:包含一组指向下一个文件的链接的文件。
unix io接口包括打开和关闭文件、读和写文件以及改变当前文件的位置。
8.2 简述Unix IO接口及其函数
Open()函数:这个函数回打开一个已经存在的文件或者创建一个新的文件,可以添加参数只读,只写和可读可写。Close()函数:这个函数关闭一个已经打开的文件。read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf返回值-1表示一个错误,返回值为0表示EOF。否则返回值表示的是实际传送的字节数量。write函数从内存buf位置复制至多n个字节到描述符fd的当前文件位置。
8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
已阅读博客内容,我个人的理解是通过vsprintf来生成完成目标格式的内容,在用write函数写到stdout里面。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
这部分不是特别理解。
8.5本章小结
本章是IO相关的一些知识。介绍了几个函数的实现过程。
(第8章1分)
结论
Hello经历了预处理、编译、汇编、链接、shell分配进程管理、调用各类函数、最终死亡被内存回收的过程。
这是一项浩大而复杂的工程,一个小小的hello经历了难以想象的伟大历程,在程序人生路上,我还有很长的路要走。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
Hello.i:完成预处理的相关内容
Hello.s:完成汇编的相关内容
Hello.o:完成编译的相关内容
Hello:完成链接和进程管理相关内容
Hellodisam.txt:用来保存临时的objdump重定向文件,与编译和链接部分相关
Helloelf.txt:用来保存临时的readelf重定向文件,与汇编和编译部分相关
Hellotemp.txt:用来保存临时的objdump重定向文件,与编译部分相关
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 布莱恩特、奥哈拉伦. 深入理解计算机系统
[2] 预处理https://baike.baidu.com/item/%E9%A2%84%E5%A4%84%E7%90%86/7833652
[3] 编译https://baike.baidu.com/item/%E7%BC%96%E8%AF%91/1258343
(参考文献0分,缺失 -1分)