程序人生-Hello’s P2P[HITICS-大作业]

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术学院
学   号 1180301006
班   级 1803010
学 生 宋永玺    
指 导 教 师 史先俊

计算机科学与技术学院
2019年12月
摘 要
一个简单的hello程序,从其诞生到它程序生命的终止,竟是如此曲折。它的一生经历了预处理、编译、汇编、链接,终于长大。可是真正的独立的程序是不存在的,譬如人之于世,总有人情往来。程序也要在系统进程管理、存储管理、I/O管理的协助下,启程(fork),闯荡(执行指令、访存等)、并在它们的帮助下克服(利用异常控制流等)它这一生中重重的困难(各种中断,异常),才能看透它这一生中所有的迷,向世界宣告自己的存在(I/O)。最终又无言离开,以
赤子之心(进程正常回收)或是眷恋不舍(僵尸进程)。
关键词:P2P;O2O;程序的一生;预处理;编译;汇编;链接;系统进程管理;I/O管理;存储管理;
大抵世界上所有的人都是一样:澄澈地来到世间,又野心勃勃的闯荡天涯,嬉笑怒骂。最终无言离去,以赤子之心或是眷恋不舍。

目 录

第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 5 -
第2章 预处理 - 6 -
2.1 预处理的概念与作用 - 6 -
2.2在UBUNTU下预处理的命令 - 6 -
2.3 HELLO的预处理结果解析 - 8 -
2.4 本章小结 - 11 -
第3章 编译 - 12 -
3.1 编译的概念与作用 - 12 -
3.2 在UBUNTU下编译的命令 - 12 -
3.3 HELLO的编译结果解析 - 13 -
3.4 本章小结 - 19 -
第4章 汇编 - 20 -
4.1 汇编的概念与作用 - 20 -
4.2 在UBUNTU下汇编的命令 - 20 -
4.3 可重定位目标ELF格式 - 20 -
4.4 HELLO.O的结果解析 - 23 -
4.5 本章小结 - 25 -
第5章 链接 - 26 -
5.1 链接的概念与作用 - 26 -
5.2 在UBUNTU下链接的命令 - 26 -
5.3 可执行目标文件HELLO的格式 - 26 -
5.4 HELLO的虚拟地址空间 - 29 -
5.5 链接的重定位过程分析 - 32 -
5.6 HELLO的执行流程 - 39 -
5.7 HELLO的动态链接分析 - 40 -
5.8 本章小结 - 41 -
第6章 HELLO进程管理 - 42 -
6.1 进程的概念与作用 - 42 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 42 -
6.3 HELLO的FORK进程创建过程 - 42 -
6.4 HELLO的EXECVE过程 - 43 -
6.5 HELLO的进程执行 - 44 -
6.6 HELLO的异常与信号处理 - 45 -
6.7本章小结 - 48 -
第7章 HELLO的存储管理 - 49 -
7.1 HELLO的存储器地址空间 - 49 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 49 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 49 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 50 -
7.5 三级CACHE支持下的物理内存访问 - 51 -
7.6 HELLO进程FORK时的内存映射 - 51 -
7.7 HELLO进程EXECVE时的内存映射 - 52 -
7.8 缺页故障与缺页中断处理 - 53 -
7.9动态存储分配管理 - 54 -
7.10本章小结 - 58 -
第8章 HELLO的IO管理 - 59 -
8.1 LINUX的IO设备管理方法 - 59 -
8.2 简述UNIX IO接口及其函数 - 59 -
8.3 PRINTF的实现分析 - 60 -
8.4 GETCHAR的实现分析 - 64 -
8.5本章小结 - 65 -
结论 - 65 -
附件 - 66 -
参考文献 - 67 -

第1章 概述
1.1 Hello简介
Hello的P2P:文本文件hello.c通过预处理、编译、汇编、链接等过程生成可执行目标文件.hello
Hello的O2O:在shell中,通过./hello命令运行该程序,shell识别出该命令后,为程序fork进程,然后execve(加载)进程,CPU将程序映射到内存(mmp),为其分配时间片,使其可以在硬件(CPU/RAM/IO)上运行。在程序运行过程中,CPU访问数据需要MMU将虚拟地址转化为物理地址,其中TLB、4级页表作为“催化剂”起到了加速效果,在访存的过程中,三级cache巧妙的结构又使得CPU访问数据速度加快。同时,系统的IO管理和信号处理又辅佐hello程序的运行,使其能够在运行过程中处理各种信号。最终hello的效果呈现在了屏幕上。而当程序运行结束或者被Ctrl+Z发出的信号终止时,hello”死”去了。后事(回收进程)便交由shell或者init处理。如此,hello的一切就尘归尘(020)、土归土(020)了
1.2 环境与工具
硬件和软件环境:
处理器 IntelCore i7-8750H CPU @2.20GHz 2.21GHz
RAM 8.00GB
Windows10 64位操作系统
开发与调试工具:
VMWARE 14;Ubuntu 18.04
EDB;GDB;VSCODE;VIM.Objdump
readelf
1.3 中间结果

文件名 作用
hello.i Hello.c的预处理文件
Hello.s 编译器编译hello.i产生的编译文件
Hello.o 汇编器汇编Hello.s产生的可重定向汇编文件
Hello 链接器链接Hello.o产生的可执行文件
Hello.od 反汇编hello.o产生的文件,查看汇编代码
Hello.d Hello的反汇编文件,查看链接器的链接
Hello.oelf Hello.o的ELF文件,用来查看其各节信息
Hello.elf Hello的ELF文件,用来查看其各节信息

1.4 本章小结
本章简短有力地介绍了hello程序P2P,O2O的一生,列出了使用的软硬件环境和开发工具。并且通过分步的方法生成了hello.c生成可执行文件过程中的中间文件,使用readelf工具得到了ELF文件。

第2章 预处理
2.1 预处理的概念与作用
概念:源代码被翻译成程序,被规定分为若干有序阶段,通常前几个阶段由预处理器实现,预处理会展开以#起始的行,试图将它解释为预处理指令。主要有一下几个方面的处理:
① 宏定义指令,如 #define a b 这种伪指令,预编译所要做的是将程序中的所有 a 用 b 替换,但作为字符串常量的 a 则不被替换。还有 #undef,则将取消对某个宏的定义,使以后该串的出现不再被替换
② 条件编译指令,如 #ifdef, #ifndef, #else, #elif, #endif等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉
③ 头文件包含指令,如 #include 。该指令将头文件中的定义统统都入到它所产生的输出文件中,以供编译程序对之进行处理
④ 特殊符号,预编译程序可以识别一些特殊的符号。例如在源程序中出现的 LINE 标识将被解释为当前行号(十进制数)#error则是错误指令。预编译程序对于在源程序中出现的这些串将用合适的值进行替换
作用:使得源代码在不同的执行环境中被方便的修改或者编译。
2.2在Ubuntu下预处理的命令
Gcc -E hello.c >> hello.i(重定向输出到hello.i文件中)
Gcc -E hello.c (输出结果到屏幕上)
程序人生-Hello’s P2P[HITICS-大作业]_第1张图片
程序人生-Hello’s P2P[HITICS-大作业]_第2张图片
输出6229行
程序人生-Hello’s P2P[HITICS-大作业]_第3张图片
2.3 Hello的预处理结果解析
程序人生-Hello’s P2P[HITICS-大作业]_第4张图片
Hello.c程序中的预处理指令有三条。根据预处理的定义,对hello.c的预处理应该是预处理器寻找到这三个头文件的定义,并将其中的内容全部加载到hello.i文件当中。
查看发现的确将这几个文件当中的各种定义放到了hello.i当中。通过对比/user/include路径下的头文件具体内容可以验证
程序人生-Hello’s P2P[HITICS-大作业]_第5张图片
(hello.i当中部分内容)
程序人生-Hello’s P2P[HITICS-大作业]_第6张图片
(hello.i当中的部分内容)

Linux系统存放头文件的地方:
程序人生-Hello’s P2P[HITICS-大作业]_第7张图片
(对比图):
程序人生-Hello’s P2P[HITICS-大作业]_第8张图片
可以看到在hello.i中的确引入了stdio.h中的定义.同理unistd.h和stdlib.h中的定义也被引入了。
2.4 本章小结
预处理并不是C/C++语言的特性,而是编译器的特性,预处理器将源代码中用预处理指令#引入的文件或者宏定义等,引入到源代码文件中,形成了hello.i预处理文件,当然这还需要进一步的优化(编译)。

第3章 编译
3.1 编译的概念与作用
编译:编译是编译器将预处理文本翻译为汇编文本的过程。
作用:编译器通过以下几个程序将预处理程序代码优化改进。
① 扫描程序(scanner)
执行词法分析:将从实际源程序中得到的字符流收集到token(记号)的有意义单元中。这个过程可以理解为扫描程序执行类似单词拼写的任务。
② 语法分析程序(parser)
语法分析程序从扫描程序中获取记号形式的源代码,并完成定义程序结构的语法分析。语法分析的结果被表示为分析树(parser tree)或语法树(syntax tree).
③ 语义分析程序(semantic analyzer)
首先,程序的语义确定程序的运行,但是像C语言这种程序设计语言具有在执行之前被确定而不易由语法表示和由分析程序分析的特征。这种特征被称为静态语义(static semantic),语义分析程序就是负责分析静态语义程序,而相对而言的“动态”语义具有只有在程序执行时才能确定的特性,这部分工作则不由编译器完成。
④ 源代码优化程序(source code optimizer)
在语义分析之后,编译器可能只依赖于源代码(根据编译器特性决定)对代码进行改进优化.比如可能会用左右移来代替乘法除法等。
3.2 在Ubuntu下编译的命令
Gcc -S hello.i>>hello.s
程序人生-Hello’s P2P[HITICS-大作业]_第9张图片
3.3 Hello的编译结果解析
3.3.1 处理局部变量
在这里插入图片描述

通过对比源程序和汇编后文件,可以发现未初始化的局部变量i被暂存在-4(%rbp)寄存器中,默认被初始化为0,并且未被分配空间,放在.rodata节中,只有在需要使用的时候才为其分配空间。

3.3.2处理逻辑运算符
程序人生-Hello’s P2P[HITICS-大作业]_第10张图片
①对比源程序和汇编后文件,hello.c中用到了!=这个逻辑运算符,在这里被处理为cmpl比较语句和je跳转语句的小模块,当参数条件满足(等于4)的时候,跳到.L2节,与3.3.1中提到的一致,局部变量i要被使用到因此为其申请内存空间并赋初值;当不满足参数条件时,逻辑和c代码一致,先是(leaq)加载printf函数(实际上编译器将它处理为puts)要用的参数,然后调用puts输出信息,调用exit函数结束进程。
②for循环中存在一个 < 的逻辑运算符。
程序人生-Hello’s P2P[HITICS-大作业]_第11张图片
在.s文件中,对for循环的处理是一个cmpl+jle的比较跳转模式,当i<=7的时候循环继续,跳转到.L4中,在L4表中,首先是准备Prinf函数所需的两个参数,蓝色框的汇编代码是确定argv[1]和argv[2]的相对位置,红色箭头引出的代码部分是传参过程。(64位下前函数调用的前两个参数寄存器为%rdx和%rdi)
3.3.3算术操作
Hello程序中涉及到的算术操作是for循环控制变量i的自增操作,操作符为++。这里对它的处理也十分简单,使用了addq指令对 变量i (-4(%rbp))直接加一的方法。
程序人生-Hello’s P2P[HITICS-大作业]_第12张图片
3.3.4数组指针操作
Hello程序中访问了argv数组中的三个元素。在汇编文件中是通过改变基址偏移量来访问相应元素的。
程序人生-Hello’s P2P[HITICS-大作业]_第13张图片
如图所示,图中蓝框标出的便是argv数组的起始地址-32(%rbp),通过对临时寄存器%rax加8字节来访问argv[1],加16字节访问argv[2],加24字节访问argv[3]。
3.3.5函数操作
Hello程序中涉及到的函数有四个。
① exit函数:
exit函数只有一个参数1,64位汇编代码中用%rdi作为第一个参数寄存器
程序人生-Hello’s P2P[HITICS-大作业]_第14张图片
我们可以从图中看到的确使用movl语句为%edi寄存器赋值1.然后使用call指令调用exit。该函数调用后直接退出进程。
② atoi函数:
该函数为一个库函数,可以将字符串转化为int型数字,返回值为int型。
程序人生-Hello’s P2P[HITICS-大作业]_第15张图片
结合3.3.4中的分析可知汇编代码中通过偏移量+基址的方式得到argv[3]将其保存到参数寄存器%edi中,然后调用atoi函数。
③ sleep函数:
该函数实际上会暂时挂起进程。其函数原型为unsigned int sleep (unsigned int seconds),它以atoi函数的返回值作为参数,我们知道函数调用完成后的返回值寄存器为%eax,可以从汇编代码中看到,的确是以%eax的值作为sleep的参数使用的。

程序人生-Hello’s P2P[HITICS-大作业]_第16张图片
④ getchar函数 函数原型为int getchar(void)。
它没有参数,原因是因为它是从stdin标准输入流中读入一个字符的函数,已经有了默认的流参数stdin了。其返回值是一个整型数,是用来表示字符用的。
因此它在汇编代码中也是不需要使用到参数寄存器的。不管是从源程序中还是汇编代码中看,都是在getchar调用完成之后直接retrun0的。
程序人生-Hello’s P2P[HITICS-大作业]_第17张图片

3.3.6 隐式强制转换。
事实上C在以下四种情况下会进行隐式转换:
1、算术运算式中,低类型能够转换为高类型。
2、赋值表达式中,右边表达式的值自动隐式转换为左边变量的类型,并赋值给他。
3、函数调用中参数传递时,系统隐式地将实参转换为形参的类型后,赋给形参。
4、函数有返回值时,系统将隐式地将返回表达式类型转换为返回值类型,赋值给调用函数。
在程序中,通过查看被调用函数的函数原型. int atoi(const char *nptr);
unsigned int sleep (unsigned int seconds),以及int main(int argc,char *argv[]),可以知道在该程序的函数调用中发生了两次隐式强制类型转换:①char * 2 const char *;②int 2 unsigned int;
程序人生-Hello’s P2P[HITICS-大作业]_第18张图片
按照源程序的逻辑,如果我们将隐式强制转换写为显式强制转换,即sleep((unsigned)atoi((cost char *)argv[3))。可反应在汇编代码中,我们并不能直接从指令序列看出有任何的强制转换过程。
通过了解const类型定义
程序人生-Hello’s P2P[HITICS-大作业]_第19张图片
我们知道,这里的argv[3]实际上无法被修改,在程序中也没有修改它的地方,仅仅从汇编指令层级我们暂时看不出什么原因。
再分析int2unsigned int 的转换,同样也无法从指令层级得知其具体操作。
通过查询书籍,得知从位级角度来讲,有些强制转换是不需要产生转换用汇编代码的。

同理,char2const char的强制转化在位级层面实际上是没有“数据修改”意义的,因此,反应在汇编层级也就看不出什么具体的强制转换代码了。
3.4 本章小结
概述了编译的概念和一般流程,详细分析了hello.s中C语言的各种数据与操作,并且加深了对隐式转换问题的理解。

第4章 汇编
4.1 汇编的概念与作用

概念:汇编指把汇编语言翻译成机器语言的过程,此处将hello.s中的汇编语言指令翻译为了可重定位目标程序(机器指令)
作用:将汇编指令翻译为机器可执行的机器指令。
4.2 在Ubuntu下汇编的命令
Gcc -c hello.s
程序人生-Hello’s P2P[HITICS-大作业]_第20张图片

4.3 可重定位目标elf格式
使用readelf -a hello.o>>hello.oelf生成hello.o的ELF文件。
程序人生-Hello’s P2P[HITICS-大作业]_第21张图片
如图为hello.o的ELF文件头,列出了程序节的基本信息。
本程序中共13个节头
程序人生-Hello’s P2P[HITICS-大作业]_第22张图片
查看重定位节信息
程序人生-Hello’s P2P[HITICS-大作业]_第23张图片
由.rela.text重定位节可知:.text节中需要重定位的信息有puts exit printf atoi sleep getchar和.rodata中的.LC0、.LC1.
.rela.eh_frame节则是.eh_frame的重定位节

4.4 Hello.o的结果解析
objdump -d -r hello.o >>hello.od
得到hello.o的反汇编代码,与hello.s对照
程序人生-Hello’s P2P[HITICS-大作业]_第24张图片
① :机器语言的构成:
机器语言完全由二进制0、1构成,此处显示格式为16进制。并且每两个16进制数组成一个字节编码,对应于一个运算符或者操作数。
② :机器语言与汇编语言的映射关系
将hello.o反汇编后得到的代码与hello.s大致相同,存在差异。
Ⅰ.分支转移:机器语言中jmp后面跟的是地址,而.s文件中后面使用了.L2、.L4这样的代码段标签。
Ⅱ.函数调用:.s文件中call指令后面跟的是函数名的符号,而反汇编得到的代码中call的后面是一个待重定位的相对地址,比如puts
程序人生-Hello’s P2P[HITICS-大作业]_第25张图片
可以看到call(e8)后面的地址为全0,后面的标签表示它的相对位置在21位置,需要在链接器链接之后确定e8后面的地址字节。
4.5 本章小结
本章分析了hello.o的elf格式,简介了汇编器对hello.s的处理结果,通过对比hello.o反汇编后的文本和hello.s的异同,分析了机器语言和汇编语言的映射关系。

第5章 链接
5.1 链接的概念与作用
概念:
链接器通过符号解析(Symbol resolution)和重定位两个过程,赋予符号表中的符号唯一定义,并将被链接文件中的分散的代码片段组织起来,生成一个完全链接的可执行对象文件的过程。
作用:
① 链接模块化(零散)的程序,可以提高常见功能/库的复用率,使得分离编译成为可能。
②常用的函数和功能可以封装成库,以供链接时使用,因此修改程序代码时,只需要重新编译被改动的文件,其他的程序模块不受影响,从而提高了效率
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

程序人生-Hello’s P2P[HITICS-大作业]_第26张图片
5.3 可执行目标文件hello的格式
命令: readelf -a Hello>>Hello.elf
Hello 的ELF头:
程序人生-Hello’s P2P[HITICS-大作业]_第27张图片
可以看到其程序头大小和数量均有增加。
各节头如下:
程序人生-Hello’s P2P[HITICS-大作业]_第28张图片

节头(Section Headers)给出Hello程序中各节的信息,包括该节的大小(Size),该节在程序代码中的偏移量(offset)等。利用这些信息我们可以利用诸如Hexedit等工具在机器码中找到各节的起始和终止位置。其中地址为程序被载入到虚拟地址中的起始地址。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
命令 edb –run Hello
程序人生-Hello’s P2P[HITICS-大作业]_第29张图片
起始地址为0x400000;终止地址为0x400ff0
程序人生-Hello’s P2P[HITICS-大作业]_第30张图片
对比ELF文件中程序头(Program Header)提供的信息
程序人生-Hello’s P2P[HITICS-大作业]_第31张图片
64位程序头结构体数据类型如图:
程序人生-Hello’s P2P[HITICS-大作业]_第32张图片
其中
p_type说明了本程序头所描述的段的类型或者如何解析本程序头的信息。
p_flags代表本段属性,为三个书信PF_X(0x01),PF_W(0x02),PF_R(0x04)的组合。
p_offset给出本段内同相对于文件开头的偏移量
p_vaddr给出本段内容的开始位置在进程空间中的虚拟地址
p_paddr给出本段内容的开始位置在进程空间中的物理地址。
p_files给出本段内容在文件中的大小,单位是字节,可以是0
p_memsz给出本段内容在内存镜像中的大小,单位是0,可以是0
p_align设置页面对其要求
对于Hello的ELF程序头中出现的各个信息分析如下:
1.PT_LOAD:代表这段是要加载或映射进内存里的,对于一个动态链接的可执行ELF文件,一般包含两个这样的段,Hello程序中的确如此。
① 程序的代码段; ②全局变量数据段和动态链接信息
可以在上图中看到请求动态链接库的一段注释,证明的确分为上述两种信息。
2.PT_DYNAMIC:代表这部分保存着的动态链接信息,比如程序运行时所需要的共享库的列表。
在EDB中找到其对应的虚拟地址0x600e50,发现的确对应着一些信息,只不过我们无法直接解读:
程序人生-Hello’s P2P[HITICS-大作业]_第33张图片
3.PT_INTERP:该部分是指向解析器路径的ascii字符串,以0x0结尾i,这个字符串是ELF解析器的路径,这种段类型只对于可执行文件又意义,当出现在共享文件中的时候,没有意义;在一个ELF文件中,它最多出现一次,而且其必须在其他课装载的表项之前。
4.PT_NOTE:辅助信息、
5.GNU_EH_FRAME:保存异常信息
6.GNU_STACK:保存系统栈所需要的权限信息
7.GNU_RELRO:保存在重定位之后的只读信息
5.5 链接的重定位过程分析
5.5.1Hello和hello.o的不同
①ELF节头数量不同
对比hello.o 和Hello 的ELF文件中的节头信息。可以发现Hello的ELF文件中多出了13个节头。
程序人生-Hello’s P2P[HITICS-大作业]_第34张图片
(hello.o的节头表)
程序人生-Hello’s P2P[HITICS-大作业]_第35张图片

多出的节头表是.
interp,.note.ABI-tag,.hash,.gnu.hash,.dynsym,.gnu.version,.gnu.version_r,.rela.dyn,rela.plt,.init,.plt,fini,.got,.got.plt。
这些都来自于动态共享库,ld的过程改变了原本hello.o中各节的顺序(引入的插入到了原来的序列当中)

② Dissassembly of section.text中函数个数不同
分别反汇编hello.o和Hello,然后查看汇编代码节:
程序人生-Hello’s P2P[HITICS-大作业]_第36张图片
发现hello.o中main函数的地址并没有确定,而在链接之后确定了地址,一些具体函数的调用在hello.od(hello.o objdump得到)中只是以相对位置表现,而在Hello.d中确定了绝对关系。
如puts函数的调用:
(hello.od中)
在这里插入图片描述
在这里插入图片描述
(Hello.d中)
此外,链接的过程从共享库中得到了程序需要的函数
(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)

其中/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。链接器将上述函数加入
程序人生-Hello’s P2P[HITICS-大作业]_第37张图片
5.5.2链接过程
链接器链接的过程分为两个步骤,第一步是符号解析,第二步是重定位
1)符号解析
无法单从反汇编的结果得知是如何进行符号解析的。但是我们知道符号有强弱之分。
强符号:函数和初始化的全局变量(该hello.c中没有,但是引入的外部库中有函数符号)
弱符号:未初始化的全局变量(无)
一般地链接器处理强弱符号的时候遵守以下规则:
① 不能出现多个同名的强符号,不然会出现链接错误;
②如果有同名的强符号和弱符号,选择强符号,也就意味着弱符号相对无效;
③多个弱符号情况下,随机选择(可能根据链接器的不同设置而不同)
2)重定位
Hello生成过程中链接了共享库,我们反汇编(objdump -d -r hello.o>>hello.od;(objdump -d -r Hello>>Hello.d)Hello和hello.o来查看链接器具体如何工作:
程序人生-Hello’s P2P[HITICS-大作业]_第38张图片
我们看到编译器用类似: R_x86_64_PC32 .rodata -0x4这样的语句来标记重定位的入口,其中R_x86_64_PC32与我们在使用链接命令时指定的动态链接器的型别有关,而留下的一串0则是在链接过程中让链接器填上对应的实际内存地址的。
查看链接后的Hello的反汇编代码:
程序人生-Hello’s P2P[HITICS-大作业]_第39张图片
发现的确被填充了实际内存地址。
此外,使用ld命令链接的时候指定要链接的共享库,而我们在hello.c中调用的一些函数在此时属于外部引用,链接器会扫描共享库,得到hello.c中调用的函数(如sleep),将其加入程序。因此我们也可以看到Hello.d中多出了相关函数的节(.plt)
程序人生-Hello’s P2P[HITICS-大作业]_第40张图片
通过反汇编共享库中的一个.o文件,发现链接器填充进Hello中的一部分节的确是在这个.o文件当中的.
程序人生-Hello’s P2P[HITICS-大作业]_第41张图片
这样也可以推理得知,链接器在扫描共享库,得到所需函数的同时,还应当会计算.text节和.plt节的相对位置,以便加入,而具体偏移量又包含在节头信息中,链接器只需要解析它们就可以了。
类比CSAPP中动态链接的例程图
程序人生-Hello’s P2P[HITICS-大作业]_第42张图片
我们的Hello程序链接流程也大致相同,只不过可重定向的对象文件等不同了。
5.6 hello的执行流程
使用指令edb --run ./hello 1180301006 syx 2
程序执行过程中调用的函数(程序名和程序地址如下,顺序执行)
程序名 程序地址
Ld-2.27.so!_start 0x7f64:9fd6c090
Ld-2.27.so!_dl_start_user 0x7f64:9fd6c098
Ld-2.27.so!_dl_init 0x7f61:7da68630
Hello!_start 0x4005d0
Hello!main 0x4006b7
Hello!_printf_chk 0x4006f7
Hello!strtol 0x40070a
Hello!sleep 0x400711
Hello!_printf_chk 0x4006f7
Hello!strtol 0x40070a
Hello!sleep 0x400711
Hello!_printf_chk 0x4006f7
Hello!strtol 0x40070a
Hello!sleep 0x400711
Hello!_printf_chk 0x4006f7
Hello!strtol 0x40070a
Hello!sleep 0x400711
Hello!_printf_chk 0x4006f7
Hello!strtol 0x40070a
Hello!sleep 0x400711
Hello!_printf_chk 0x4006f7
Hello!strtol 0x40070a
Hello!sleep 0x400711
Hello!_printf_chk 0x4006f7
Hello!strtol 0x40070a
Hello!sleep 0x400711
Hello!_printf_chk 0x4006f7
Hello!strtol 0x40070a
Hello!sleep 0x400711
Hello!_IO_getc 0x400725
Libc-2.27.so!exit 0x7f61:7d688b99

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
对于动态共享链接库中 PIC 函数,编译器没有办法预测函数的运行时地址,所 以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代 码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表 PLT+全局偏移量 表 GOT 实现函数的动态链接,GOT 中存放函数目标地址,PLT 使用 GOT 中地址 跳转到目标函数。下面是程序在run前后的(即执行dl_init函数前后)的GOT表变化情况。
程序人生-Hello’s P2P[HITICS-大作业]_第43张图片
根据这个信息去查看.got.plt
程序人生-Hello’s P2P[HITICS-大作业]_第44张图片

运行完成dl_init之后
程序人生-Hello’s P2P[HITICS-大作业]_第45张图片

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
本章中介绍了链接的概念和作用,通过分析hello程序的虚拟空间,链接过程(重定位),执行流程,动态链接,介绍了动态链接的过程。

第6章 hello进程管理
6.1 进程的概念与作用
进程即运行中的程序实例,系统中的每个进程均运行在进程的上下文中。上下文即程序需要正确运行所需要的状态,它包括存放在内存中的程序代码、数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
进程给每一个应用提供了两个非常关键的抽象:①逻辑控制流;②私有地址空间。
逻辑控制流通过称为上下文切换(context switching)的内核机制让每个程序都感觉自己在独占处理器。私有地址空间则是通过称为虚拟内存(virtual memory)的机制让每个程序都感觉自己在独占内存。这样的抽象使得具体的进程不需要操心处理器和内存的相关适宜,也保证了在不同情况下运行同样的程序能得到相同的结果。
6.2 简述壳Shell-bash的作用与处理流程
Shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户的命令并把它送入内核去执行
功能:管道,输入输出重定向,执行后台程序

处理流程一般为四部分
输入-》解析-》扩展-》执行
输入:分为交互模式和非交互模式。前者使用readline库,从终端获得输入。后者输入一般来自文件。
解析:解析阶段主要工作为词法分析和语法解析,词法分析指分析器从readline或者其他输入获取字符行,根据元字符(| & ( ) space tab ;)将它们分割成word,并根据上下文环境标记这些word
扩展:扩展阶段对应于单词的各种变换,最终得到可用于执行的命令。一般有如下流程:大括号扩展-》波浪号扩展-》变量/命令/进程/数学扩展-》单词分割->路径扩展-》引用移除
执行;对于不同类型的命令,执行方式有差异
命令类型有:复合命令、管道命令、简单命令、
对于更一般的情况(命令位于磁盘文件系统之上的情况):
1.执行fork()创建子进程;2.执行重定向;3执行execve(),控制权一脚给操作系统;4内核判断该文件是否为可执行格式5若可执行,则调用相应哈桑农户或者解释器执行;6若不可执行。判断该文件:若有可执行权限并且不是目录,则认为该文件是一个脚本,调用默认解释器解释执行该文件内容。
6.3 Hello的fork进程创建过程
在终端中输入执行命令 ./Hello 1180301006 syx 2,shell首先判断这是不是一个内置命令,发现它是一个当前目录下的可执行目标文件。这时,终端调用fork函数为Hello创建一个新的子进程,这个子进程与父进程几乎完全相同。子进程得到与父进程用户级虚拟地址空间相同的副本,包括代码、数据、堆、共享库和用户栈子进程还可以得到与父进程任何打开文件描述符相同的副本,意味着子进程可以读写父进程中打开的任何文件。子进程和父进程最大的区别就是拥有不同的PID。此外,父进程和子进程是并发运行的独立的进程,内核通过交替执行它们逻辑控制流中的指令改变其指令执行的顺序。
如图为Hello在shell-bash中的fork过程
程序人生-Hello’s P2P[HITICS-大作业]_第46张图片
6.4 Hello的execve过程
程序人生-Hello’s P2P[HITICS-大作业]_第47张图片
首先我们要知道execve函数是在当前进程的上下文中加载一个进程,和fork不同,它并未创建一个新的进程,而是覆盖了原先利用fork函数创建的进程。并且execve函数只有在调用失败之后才会返回-1,这与fork函数调用一次返回两次是不同的,它调用一次从不返回。
当我们在shell中输入 ./Hello 1180301006 syx 2的时候,shell先进行命令解析,判断Hello是一个当前目录下的可执行目标文件,按6.3中的fork过程创建子进程,然后通过execve函数调用启动加载器,加载器会删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的堆和栈的段被初始化为0,通过虚拟地址空间中的页映射到可执行文件的页大小的片。新代码被初始化为可执行文件的内容。最后加载器跳转到_start处运行程序。
程序人生-Hello’s P2P[HITICS-大作业]_第48张图片
Execve加载并运行Hello的进程图。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
时间分片的概念:各个并发执行的进程,轮流在处理器上执行,一个进程执行它控制流的一部分,这部分控制流称为时间分片。
上下文切换的过程:
① 保存当前进程的上下文
② 恢复某个先前被抢占的进程保存的上下文
③ 将控制传递给当前新恢复的进程
如前文所述,逻辑控制流通过上下文切换的内核机制来调度当前进程和其它进程,形成一种各进程独占内存的假象。
对于Hello程序来说,它的进程被抢断的时刻我们认为在sleep发生的时候
程序人生-Hello’s P2P[HITICS-大作业]_第49张图片
当Hello进程调用sleep函数是,实际上是挂起了Hello进程,此时系统内核可执行其他的进程,当sleep调用完毕的时候,一个中断信号被发送给正在执行的其他进程,这个进程挂起(或终止),返回到Hello进程。
6.6 hello的异常与信号处理
回车:
程序人生-Hello’s P2P[HITICS-大作业]_第50张图片
在运行过程中回车不影响输出,不会让进程终止或者挂起,只是显示在shell上面每次printf输出的位置改变。
Ctrl-Z:
程序人生-Hello’s P2P[HITICS-大作业]_第51张图片
按下Ctrl-Z之后进程挂起,可以调用ps jobs pstree等命令查看进程和任务,按下fg恢复运行。
Man kill查看kill方法
程序人生-Hello’s P2P[HITICS-大作业]_第52张图片
然后使用kill杀死hello进程
程序人生-Hello’s P2P[HITICS-大作业]_第53张图片
发现当按下Ctrl+Z的时侯使用kill直接杀死了该进程。
程序人生-Hello’s P2P[HITICS-大作业]_第54张图片
Ctrl-C:程序人生-Hello’s P2P[HITICS-大作业]_第55张图片

进程终止。
信号:对于shell中的进程来说,分为两类:前台进程和后台进程。同一个时刻前台进程只能有一个(即当前运行的进程),后台进程可以有多个。对于前台进程来说,当我们执行完成之后,shell将其回收。但是对于后台进程来说,并不能确定其完成的具体时间,如果一个父进程早于其子进程终止时,该子进程在终止之后因为没有其父进程回收便成了僵尸进程,并且会因此造成内存泄漏。但是系统可以以来与异常控制流中的通知机制:信号来提醒进程一个事件已经发生,该进程接收到这个通知之后,如果它定义了相关信号的处理函数,那么该处理函数将会被执行,否则将执行系统默认的处理函数。值得注意的是,同意时间一个进程只能接收一个信号,其它后续被发送过来的信号会简单地被丢弃,即信号不是排队的。
常用的信号简介:
程序人生-Hello’s P2P[HITICS-大作业]_第56张图片
对于上述对Hello程序运行过程中发送的各种信号,在此表中是SIGINT、SIGKILL、由系统内核发送的信号有SIGALRM等。
6.7本章小结
本章介绍了Hello程序是如何在shell中运行的:以进程形式。介绍了shell-bash的作用和一般处理流程。介绍了进程中两个关键的抽象:逻辑控制流和私有空间。并且通过分析hello进程的创建,加载和运行进一步介绍了上下文切换的机制,通过运行hello程序,介绍了异常控制流的通知机制信号。

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量, 表示为 [段标识符:段内偏移量]
对于hello程序来说即是hello.o里面的相对偏移地址
线性地址:逻辑地址经过段机制后转化为线性地址,为{描述符:偏移量}的组合形式。分页机制中线性地址作为输入。
虚拟地址:CPU在寻址的时候,是按照虚拟地址来寻址,然后通过MMU(内存管理单元)将虚拟地址转换为物理地址。即在前文中看到的hello的虚拟内存地址。
物理地址:计算机主存被组织成由M个连续字节大小的内存组成的数组,每个字节都有一个唯一的地址,该地址被称为物理地址。对于hello来说是在其运行时由MMU根据虚拟内存映射到的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在 Intel 平台下,逻辑地址(logical address)是 selector:offset 这种形式,selector 是 CS 寄存器的值,offset 是 EIP 寄存器的值。如果用 selector 去 GDT( 全局描述符表 ) 里拿到 segment base address(段基址) 然后加上 offset(段内偏移),这就得到了 linear address。我们把这个过程称作段式内存管理。
程序人生-Hello’s P2P[HITICS-大作业]_第57张图片
7.3 Hello的线性地址到物理地址的变换-页式管理
按照7.2描述
如果再把 linear address 切成四段,用前三段分别作为索引去PGD、PMD、Page Table里查表,最终就会得到一个页表项(Page Table Entry),那里面的值就是一页物理内存的起始地址,把它加上 linear address 切分之后第四段的内容(又叫页内偏移)就得到了最终的 physical address。我们把这个过程称作页式内存管理。
7.4 TLB与四级页表支持下的VA到PA的变换
CPU的页式内存管理单元,负责把一个线性地址,转换为物理地址。Linux将虚拟内存组织成一些段的集合,段之外的虚拟内存不存在因此不需要记录。内核为hello进程维护一个段的任务结构即图中的task_struct,其中条目mm指向一个mm_struct,它描述了虚拟内存的当前状态,pgd指向第一级页表的基地址(结合一个进程一串页表),mmap指向一个vm_area_struct的链表,一个链表条目对应一个段,所以链表相连指出了hello进程虚拟内存中的所有段
程序人生-Hello’s P2P[HITICS-大作业]_第58张图片
7.5 三级Cache支持下的物理内存访问
首先CPU给出VA
VA经过MMU地址翻译:
MMU用VPN到TLB中找寻PTE,若命中,得到PA;若不命中,利用VPN(多级页表机制)到内存中找到对应的物理页面,得到PA。
PA分成PPN和PPO两部分。我们利用其中的PPO,将其分成CI和CO,CI作为cache组索引,CO作为块偏移,PPN作为tag。Cache的读写命中\不命中都有对应的处理流程(第六章)。
程序人生-Hello’s P2P[HITICS-大作业]_第59张图片
总体图。
7.6 hello进程fork时的内存映射
内存映射是在调用进程的虚拟地址空间创建一个新的内存映射。
内存映射分为2种:
• 文件映射:将一个普通文件的全部或者一部分映射到进程的虚拟内存中。映射后,进程就可以直接在对应的内存区域操作文件内容!
• 匿名映射:匿名映射没有对应的文件或者对应的文件时虚拟文件(如:/dev/zero),映射后会把内存分页全部初始化为0。
当多个进程映射了同一个内存区域时,它们会共享物理内存的相同分页。通过fork()创建的子进程也会继承父进程的映射副本。
如果多个进程都会同一个内存区域操作时,会根据映射的特性,会有不同的行为。映射特征可分为私有映射和共享映射:
• 私有映射:映射的内容对其他进程不可见。对于文件映射来说,某一个进程在映射内存中改变文件的内容不会反映到被映射的底层文件中。内核会使用copy-on-write(写时复制)技术来解决这个问题:只要有一个进程修改了分页中的内容,内核会为该进程重新创建一个新的分页,并将需要修改的内容复制到新分页中。
• 共享映射:某一个进程对共享的内存区域操作都对其他进程可见。对于文件映射,操作的内容会反映到底层文件中。
7.7 hello进程execve时的内存映射
加载hello程序的时候会进行以下操作
首先execve会删除已经存在的用户区域:即当前进程虚拟地址用户部分的区域结构
其次,要为新程序的代码、数据.bss和栈区创建新的区域结构,同时将它们标记为私有的写时复制文件,分别将它们映射到如图所示的地方,其中代码设数据被映射到.data,.text节,.bss是私有的,请求二进制0的,被映射到匿名文件,用户栈也一样。它们的初始长度均为0.
另外,hello要与引入的共享库链接,这部分映射到共享库的内存映射区域。
除此之外还需要设置PC,即当前进程上下文中的PC,指向程序入口。
程序人生-Hello’s P2P[HITICS-大作业]_第60张图片

7.8 缺页故障与缺页中断处理
缺页故障处理:当CPU发送一个VA,发现对应的物理地址不在内存中,必须从磁盘中取出,就会发生故障,缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令。
缺页中断处理:缺页故障会调用系统内核当中的缺页异常处理程序。由该程序选择一个牺牲页面,判断该页面有没有被修改,如果已经被修改了,内核就将它复制回磁盘。但无论哪种情况,内核都将修改在DRAM中关于它的页表条目,使它的记录不在表中,并将之前请求的虚拟页面从磁盘复制到内存,添加一条记录在页表中,更新PTE。然后返回重新启动导致缺页的命令,这次再发送虚拟地址给MMU就不会引起缺页故障了。

7.9动态存储分配管理
动态内存分配器维护者一个进程的虚拟内存区域,称为堆。它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护者一个变量brk,它指向堆的顶部
程序人生-Hello’s P2P[HITICS-大作业]_第61张图片
分配器将堆视为一组不同大小的块的集合来维护。每一个块就是一个连续的虚拟内存片(chunk),保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,知道它显式的被应用所分配,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
不同结构组织的堆效率不同
造成堆利用效率低的原因是碎片现象,碎片分为两种:

  1. 内部碎片:当一个已分配块比有效载荷的,由对齐要求产生
  2. 外部碎片:空闲内存合起来足够满足分配请求,但是处于不连续的内存片中

分配器有两种基本分格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
① 显式分配器,要求应用显式地释放任何已分配的块。
② 隐式分配器,它要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。它也被叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。

我们正在组织数据结构的时候,给每个块的结尾处添加一个脚部(footer),脚部就是头部的一个副本。如果每个块都包括这样一个脚部。那么分配器就可以通过检查它前面的块的脚部来判断这个块的起始位置以及分配状态。这个块总是在当前块开始位置一个字(4bytes)的距离。
一个块的结构图示
程序人生-Hello’s P2P[HITICS-大作业]_第62张图片
当采用这种块的结构组织方式时,分配器的合并策略有4中情况。
① 前面块和后面块已分配:此时无法合并,直接返回当前块指针。
② 前面块未分配并且后面块已分配:此时可以和前面的块合并,合并后块的大小为前面块的大小与当前块的大小之和,同时修改头部(前面块的)和脚部(当前块的),然后brk指针指向前一块,返回指针。
③ 前面块已分配并且后面块未分配:此时可以和后面的块合并,道理同②,但是brk指针位置不做改变。
④ 前后块均未被分配:三个块一起合并,块大小为三者之和,需要修改前面块的头部和后面块的脚部,并将brk指向前面的块。
合并策略还会被使用在分配器释放空间的操作当中。
隐式空闲链表的一个特点是,它给了程序一个空闲块的主体,采用边界标签技术的隐式空闲链表有一个恒定的形式:
在这里插入图片描述
这种方式是将空闲块组织为某种形式的显式数据结构。根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。通过使用合理的数据结构,我们可以让分配器维护这个数据结构的操作时间达到常数级别。
比如我们使用一个双向链表来组织堆:
程序人生-Hello’s P2P[HITICS-大作业]_第63张图片
我们在原来的空闲块的有效载荷部分分出数据结构所需要的指针域,这里是双向链表因此分离出一个前驱和后继指针。
分配器维护这个数据结构的时候,如果采取后进先出的方式维护链表,并且使用首次适配的放置策略,再使用边界标记技术,那么合并,释放,插入操作都可以达到常数级别。
显式空闲链表的组织方式相比与隐式更通用,因为更多的时候,我们是不知道堆块的具体数量的,一些堆块数量预先就知道的,可以针对它使用一个隐式的空闲链表分配器。
显式空闲链表虽然使用合适的策略和技术能够让操作速度提升,但是是以更多的内部碎片为代价的。
7.10本章小结
本章介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理、介绍了三级cache支持下物理内存的访问过程。对于hello进程的fork、execve、内存映射、缺页故障和缺页中断以及动态存储技术中两种分配器。让我们对hello程序的存储有了更深的理解。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备管理:所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。
设备的模型化:所有的 IO 设备都被模型化为文件。
8.2 简述Unix IO接口及其函数
Unix I/O 接口统一操作:
1) 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
2) Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标 准错误。
3) 改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。
4) 读写文件:一个读操作就是从文件复制 n>0个字节到内存,从当前文件位置 k 开始,然后将 k增加到k+n,给定一个大小为 m 字节的而文件,当 k>=m 时,触发 EOF。类似一个写操作就是从内存中复制 n>0个字节到一个文件,从当前文件位置 k开始,然后更新 k。
5) 关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
Unix I/O 函数:
1)进程通过调用 open 函 数来打开一个存在的文件或是创建一个新文件的。 open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在 进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访 问这个文件,mode 参数指定了新文件的访问权限位。
程序人生-Hello’s P2P[HITICS-大作业]_第64张图片
2)fd 是需要关闭的文件的描述符,close 返回操作结果。
在这里插入图片描述
3)read 函数从描述符为 fd 的当前文件位置赋值最多 n 个字节到内存位置 buf。返回值-1 表示一个错误,0 表示 EOF,否则返回值表示的是实际传送的字节数量。
write 函数从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置。

8.3 printf的实现分析
首先我们看printf函数的实现
程序人生-Hello’s P2P[HITICS-大作业]_第65张图片
看到它的参数列表使用了可变参数列表的编程方式。也就是说printf的参数个数是不确定的。
通过课程的学习,我们早已经知道函数调用的时候参数压栈的顺序是参数列表从右往左的,这符合栈先进后出的特点。
分析
在这里插入图片描述
这一句我们可以结合栈地址增长的特点来确定arg实际上是printf可变参数列表中的第一个参数。
接着分析下一句
在这里插入图片描述
这里调用了vsprintf函数,通过查询C库函数文档中对于vsprintf的描述,可以得知这个函数来自于,其函数原型为:
在这里插入图片描述
函数功能为:将arg按格式format写入字符串str中。在百度百科的描述中该函数中的arg参数位于数组中。数组的元素会被插入主子符串的百分比(%)符号处。并且该函数是逐步执行的。在第一个%符号中插入arg1,在第二个%符号处,插入arg2,依次类推。
其实看到这里的描述,再结合平时使用printf函数时的经验,我们已经大致明白了printf可变参数列表的实现原理。但是还是看一下vsprintf函数的具体实现,看一个具体的例子
程序人生-Hello’s P2P[HITICS-大作业]_第66张图片
该vsprintf的实现中只实现了对16进制的格式化,它返回的时一个要打印处来的字符串的长度。这个返回的长度值在printf函数中被writ()函数使用。
那么接下来printf函数的实现就与write相关了。
反汇编追踪write
程序人生-Hello’s P2P[HITICS-大作业]_第67张图片
在GitHub上幸运的搜索到它的定义在哪个文件中:
程序人生-Hello’s P2P[HITICS-大作业]_第68张图片

但是对于int INT_VECTOR_SYS_CALL究竟是什么我们还不清楚。在一篇BLOG中我们了解到了它的实现和大致功能:

表示要通过系统来调用sys_call这个函数(至于汇编代码已超出我现在的知识范围)
其实现如下:
程序人生-Hello’s P2P[HITICS-大作业]_第69张图片
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.。
Syscall将字符串中的字节从寄存器复制到显卡的显存中,即字符显示驱动子程序。

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),于是Hello程序要输出的字符串就被显示到屏幕上了。
另外值得一提的是,前面给出的这种实现里面的write和linux中的有差别,csapp中介绍到的write是基于linux对于I/O的抽象是文件,所以它的参数列表的第一个参数就是一个文件描述符fd。虽然不同操作系统下有所差别,但实现的思想大致相同。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章讲述了Linux对于I/O的设备管理方法,Unix I/O接口及其函数,分析了printf函数和getchar函数的实现。尤其是对printf函数的分析,虽然可能不到位,但是至少可以明白可变参数列表的一些底层实现。对于更底层的write函数也有了一定的了解。

结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
Hello程序经历的过程:

  1. 程序的编写
  2. 预处理 展开以#起始的行,试图将它解释为预处理指令,
  3. 编译 将预处理文本翻译为汇编文本,并对预处理代码进行优化改进
  4. 汇编 将汇编指令翻译为机器可执行的机器指令
  5. 链接 链接器通过符号解析(Symbol resolution)和重定位两个过程,赋予符号表中的符号唯一定义,并将被链接文件中的分散的代码片段组织起来,生成一个完全链接的可执行对象文件
  6. 进程管理 进程作为程序运行的实例,给每一个应用提供了两个非常关键的抽象:①逻辑控制流;②私有地址空间。
  7. 存储管理 CPU上的内存管理单元MMU根据页表将CPU生成的虚拟地址翻译成物理地址,进行相应的页面调度。在这个过程中,TLB,三级cache结构的使用加快了访存速度。
  8. I/O管理 所有的输入和输出都被当做对相应文件的读和写来执行,正如我们在分析printf的过程中所见到的一样,我们在程序中调度的函数往往基于特别底层的函数实现,而这些函数的实现又依赖于系统的I/O管理。
    All in all,我将这次大作业称之为Hell漫游,以致敬CSAPP中漫游计算机系统一章。通过Hello程序的一生,我们几乎遍历了计算机系统中所有关键的设计和实现,在游历的过程中对计算机系统的一些概念更加清晰,希望能够逐渐建立起自己关于计算机系统的一个认知体系。

附件
列出所有的中间产物的文件名,并予以说明起作用。

文件名 作用
hello.i Hello.c的预处理文件
Hello.s 编译器编译hello.i产生的编译文件
Hello.o 汇编器汇编Hello.s产生的可重定向汇编文件
Hello 链接器链接Hello.o产生的可执行文件
Hello.od 反汇编hello.o产生的文件,查看汇编代码
Hello.d Hello的反汇编文件,查看链接器的链接
Hello.oelf Hello.o的ELF文件,用来查看其各节信息
Hello.elf Hello的ELF文件,用来查看其各节信息

参考文献

1.https://www.zhihu.com/question/29918252

  1. https://www.cnblogs.com/pianist/p/3315801.html
  2. https://baike.baidu.com/item/vsprintf/6390873
    https://github.com/pangxiezhou/PangXie/blob/master/kernel/syscall/syscall.asm

你可能感兴趣的:(大学时光)