程序人生——HIT CSAPP大作业

计算机系统

大作业

题目

程序人生——Hello’s P2P

专业

计算机专业

学号

7203610625

班级

2036014

学生

李凡丁

指导教师

刘宏伟

计算机科学与技术学院

2021年5月

摘  要

    一个程序的真正的工作流程绝对不仅仅是我们按下F5或者F9之后就结束了,正相反,属于这个程序的一生才刚刚开始;我们看到终端输出框中“请按任意键继续”这种字样就满足地离去,却殊不知这个程序已经在这短短的几毫秒中度过了自己的一生。程序员的浪漫不是写出多么天花乱坠的代码,而是发自内心地了解这些与我们朝夕相处的程序,它们的一生究竟都经历些什么。P2P——From Program to Process,这个早就听过不知道多少遍的术语,又究竟蕴藏着什么样的内涵。

    本文将从最简单的程序——hello谈起,讲述一个程序从我们按下运行键开始直至运行结束,进程被回收时所经历的所有过程。借助《深入理解计算机系统》这本书的研究顺序,结合Codeblocks、edb,甚至是最基本却又最有学问在其中的Shell,研究明白从hello登场到谢幕,都经历了什么。

    预处理、编译、汇编、链接,这是hello作为Program经历的过程。看似简单的四个名词让计算机读懂hello,并能够让hello有机会在计算机上形成自己的进程。从汇编代码的阅读,再到反汇编代码的解读,我们尽全力在了解hello这一生的每一步都做了些什么,究竟计算机在作何尝试让hello绽放其生命的色彩。

    从进程管理,到存储管理,hello此刻作为进程,在其短暂的一生中也会在计算机里面留下自己的痕迹,我们要尝试着去查找这个痕迹,这样才不会如hello所说,它了无牵挂离去却没有人记得它。

    I/O则切实可行给了我们用户和hello交互的机会,这一刻我们切实体会到了hello的存在。

    对于hello的总览,让我们第一次懂得了程序员的浪漫,也让我们更加清晰地知道了每日与我们朝夕相处的程序的运行流程,也许这就是hello带给我们的最宝贵的财富。

关键词:P2P、预处理、编译、汇编、链接、进程管理、存储管理、I/O;                           

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分

目  录

第1章 概述... - 4 -

1.1 Hello简介... - 4 -

1.2 环境与工具... - 4 -

1.3 中间结果... - 5 -

1.4 本章小结... - 5 -

第2章 预处理... - 6 -

2.1 预处理的概念与作用... - 6 -

2.2在Ubuntu下预处理的命令... - 6 -

2.3 Hello的预处理结果解析... - 7 -

2.4 本章小结... - 7 -

第3章 编译... - 8 -

3.1 编译的概念与作用... - 8 -

3.2 在Ubuntu下编译的命令... - 8 -

3.3 Hello的编译结果解析... - 9 -

3.4 本章小结... - 12 -

第4章 汇编... - 13 -

4.1 汇编的概念与作用... - 13 -

4.2 在Ubuntu下汇编的命令... - 13 -

4.3 可重定位目标elf格式... - 13 -

4.4 hello.o的结果解析... - 16 -

4.5 本章小结... - 18 -

第5章 链接... - 19 -

5.1 链接的概念与作用... - 19 -

5.2 在Ubuntu下链接的命令... - 19 -

5.3 可执行目标文件hello的格式... - 19 -

5.4 hello的虚拟地址空间... - 23 -

5.5 链接的重定位过程分析... - 24 -

5.6 hello的执行流程... - 25 -

5.7 Hello的动态链接分析... - 25 -

5.8 本章小结... - 26 -

第6章 hello进程管理... - 27 -

6.1 进程的概念与作用... - 27 -

6.2 简述壳Shell-bash的作用与处理流程... - 27 -

6.3 Hello的fork进程创建过程... - 28 -

6.4 Hello的execve过程... - 28 -

6.5 Hello的进程执行... - 29 -

6.6 hello的异常与信号处理... - 30 -

6.7本章小结... - 33 -

第7章 hello的存储管理... - 34 -

7.1 hello的存储器地址空间... - 34 -

7.2 Intel逻辑地址到线性地址的变换-段式管理... - 34 -

7.3 Hello的线性地址到物理地址的变换-页式管理... - 35 -

7.4 TLB与四级页表支持下的VA到PA的变换... - 35 -

7.5 三级Cache支持下的物理内存访问... - 36 -

7.6 hello进程fork时的内存映射... - 37 -

7.7 hello进程execve时的内存映射... - 38 -

7.8 缺页故障与缺页中断处理... - 39 -

7.9动态存储分配管理... - 39 -

7.10本章小结... - 40 -

第8章 hello的IO管理... - 41 -

8.1 Linux的IO设备管理方法... - 41 -

8.2 简述Unix IO接口及其函数... - 41 -

8.3 printf的实现分析... - 43 -

8.4 getchar的实现分析... - 43 -

8.5本章小结... - 44 -

结论... - 45 -

附件... - 46 -

参考文献... - 47 -

第1章 概述

1.1 Hello简介

       每一个程序员的梦的开始都是Hello,当Hello World这一行字符串显示在运行终端当中的时候就证明程序员的生涯正式开始了。在我高中学竞赛的时候,很多人在最终退役的时候也会选择再次提交Hello World问题来结束他们的计算机竞赛生涯,这也就是说Hello也会是程序员生涯的结束。可只有程序员才知道,他们梦的开始,却开始地并不简单。

       一个简简单单的程序背后包含着各种软件与硬件的交互,以及这二者与系统之间的交互。从我们敲完代码并按下F5(亦或是F9)的那一刻,我们就把一切交给了计算机,从编译到执行,再到与我们交互的输入,最后得到了输出。这一切都是计算机内部多种机制共同作用的结果。我们将从Hello的自述中介绍的两点对于Hello进行一个简介:

1. P2P

        P2P是一个英文缩写+翻译形成的说法,全称是’From Program to Process’,翻译过来就是从程序到进程。以Hello为例,Hello最开始的形态是Hello.c,使我们在编译器中编写出的代码,也就是program。而后经过预处理、编译、汇编、链接四个过程成为可执行文件Hello。这个时候我们运行Hello,shell会进行解析命令行参数,初始化环境变量等一系列操作。然后会调用fork函数创建进程,execve函数运行函数,通过内存映射,分配空间等让Hello与其他进程并发进行,到这里Hello就顺利变成了进程。这就是Hello的一生:P2P的过程。

2. 020

        020同样也是一种翻译形成的说法,全称是’From Zero to Zero’,,翻译过来就是从0到0。在程序运行之前,在计算机中并没有什么,所以是从0开始,而后在运行的时候拥有了自己的进程,也在内存中存储了相关信息。但最终在进程终止之后,这一切都会被回收并释放,最终什么都没有留下,所以是以0结束。这就是Hello从0到0的一生了。

1.2 环境与工具

·硬件工具:

        12th Gen Intel(R) Core(TM) i5-12400F

        16GB RAM(15.8GB可用)

        512GB SSD+1TB HDD

·软件工具:

       Windows 11 家庭中文版 21H2

       VMware® Workstation 16 Pro 16.2.2

       Ubuntu 20.04

·开发者与调试工具:

       gcc、edb、vim、readelf、objdump

1.3 中间结果

文件名称

文件说明

hello.i

预处理生成的文本文件

hello.s

.i文件编译后得到的汇编语言文件

hello.o

.s文件汇编后得到的可重定位目标文件

hello

.o经过链接生成的可执行目标文件

hello_obj.s

可执行文件经过链接反汇编的汇编语言文件

disasm_hello.s

.o经过反汇编生成的汇编语言文件

elf.txt

.o的elf文本文件

hello.elf

hello的elf文件

图表 1:中间文件

1.4 本章小结

       本章从P2P和020两个方面对于hello进行了简短的介绍,并介绍了什么是P2P和020。其次说明了一个程序从开始运行到形成进程,再到最终被回收的详细过程,这也是本次大作业的一个基本的脉络。最后对于本次大作业的环境和一些用到的文件进行了表述。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

·预处理的概念:

       预处理指的是在程序的源代码被编译之前,通过预处理器对于程序源代码的可编译性进行提升的代码处理过程。值得注意的是,这个过程只进行一些为了之后的编译而分割或者处理特定代码的操作,而并不对于代码进行解析。一般的处理为支持宏调用。

·预处理的作用

        1.文件包含:

        对于#include的处理是一种常见的预处理,预处理的方法是复制其引用的文件进入程序的文本之中。

        2.条件编译:

        对于#if、#ifdef、#undef、#ifndef等的处理是一种常见的预处理,预处理的方法是直接加入语句对于相关部分进行限定。

        布局控制:

        对于#pragma的处理是一种常见的预处理,其主要的作用是为编译程序提供非常规的控制流类型。

        宏定义:

        对于#define的处理是最为常见的预处理,这种预处理实现了定义符号变量,函数功能,重命名,字符串拼接等相关处理,预处理的方法非常简单,就是在程序文本之中进行直接替换即可。

2.2在Ubuntu下预处理的命令

图表 2:预处理的命令

2.3 Hello的预处理结果解析

        我们使用gedit查看预处理的结果文件hello.i如下:

图表 3:hello.c的预处理结果

       观察到所有的#include等语句全部被替换,取而代之的是一些路径以及用到的相关语句被插入了该文本。并且在代码中插入注释后进行重新预处理并向下观察可以发现,预处理同时也删除了所有的注释信息。

2.4 本章小结

        本章介绍了预处理的概念、作用以及实现方式。而后基于hello.c在Ubuntu下的预处理查看并分析了预处理的结果。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

·编译的概念

       编译程序的工作就是通过编译器的词法分析和语法分析,先行确认是否存在不符合语法规则的情况,而后进行语义分析,将上述的预处理结果翻译成汇编代码。

编译包括以下基本流程:

  1. 语法分析:编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,方法分为两种:自上而下分析法和自下而上分析法。
  2. 中间代码:源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码。
  3. 代码优化:指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。
  4. 目标代码:生成是编译的最后一个阶段。目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。此处指汇编语言代码,须经过汇编程序汇编后,成为可执行的机器语言代码。

·编译的作用

       编译为不同的高级语言对应的不同编译器提供了一个通用的输出语言,这样能够使得计算机对于这些高级语言形成一套相同的处理办法。

3.2 在Ubuntu下编译的命令

图表 4:编译的命令

3.3 Hello的编译结果解析

        我们使用gedit打开编译的结果文件hello.s如下:

图表 5:hello.c的编译结果

        接下来从一些角度进行该汇编语言的解读。

3.3.1 数据初始化

 

图表 6:汇编语言——输出字符串

       这一部分能够看出Hello的后面是两个%s类型的字符串,对应着我们输入的前两个变量:学号和姓名。

图表 7:汇编语言——循环变量

       这个位置就是循环变量i声明以及变化,用以完成循环的操作其中也能够看出i所存储的位置是在-4(%rbp)的位置。

       这一条在下文的存储中不再赘述。

3.3.2 变量

 

图表 8:汇编语言——变量存储

       可以看出共计两个变量,其中argc被存储在了-20(%rbp)的位置,argv被存储在了-32(%rbp)的位置。

3.3.3 赋值

 

图表 9:汇编语言——变量赋值

       根据上文,结合这段汇编代码可知把存储在-4(%rbp)位置的i赋值为0。其中movl中的后缀l是经常用的,表示操作的对象是四个字节,其余的后缀还有b,w,q,分别对应一个,两个,八个字节。

3.3.4 类型转换

       atoi函数把一个字符串类型转换至一个整数类型。

       这句代码并不是重点,在此不过多赘述。

3.3.5 算术操作

       在本代码中,涉及到的算术运算只有i++,用以推动循环的进行。i++对应到的汇编语言如下图:

 

图表 10:汇编语言——算术运算

       图片中的call行是循环的最后一条语句,addl行即为对于循环变量i进行操作。这一句即为i++对应的汇编语句。

3.3.6 关系操作(比较运算)

       在本代码中共计出现两次比较,在下文分别进行列写。

 

图表 11:汇编语言——关系操作1

       结合上文分析可知,cmpl行比较的是存储于-20(%rbp)的参数(argc)与4的大小,对应到的是程序中argc!=4的语句。

图表 12:汇编语言——关系操作2

       结合上文分析可知,cmpl行比较的是存储于-4(%rbp)的参数(i)与7的大小,对应到的是循环结束的判定,(i<8也即i≤7)。

       这种关系操作都会结合jump指令进行跳转,这一部分将在下文进行详细分析。

3.3.7 数组

       仅有一个argv数组,数组的第一元素存储在了-32(%rbp)。

3.3.8 控制转移

(1) if语句

       判断argc是否为4,若是执行后续操作,若不是则直接跳出if执行后文。

 

图表 13:汇编语言——if控制转移

(2) for循环语句

       每次循环前判断i<8是否成立,成立则执行for循环内部语句,不成立则跳出循环执行后文。

 

图表 14:汇编语言——for控制转移

       由于循环体本身与控制转移关系不大,故在此不对于循环体本身做过多赘述。

3.3.9 函数操作

(1) main函数

       传参为argc和argv,如上文分析,分别存储在-20(%rbp)和-32(%rbp)中。返回值存储在%eax中,如下图。

 

图表 15:汇编语言——main函数操作

(2) printf函数

       该函数直接通过call进行调用。

  

图表 16:汇编语言——两次printf函数操作

(3) exit函数

       该函数同样通过call直接进行调用。

 

图表 17:汇编语言——exit函数操作

       对应到的是main函数中的exit(1)语句。

(4) atoi函数

        该函数同样通过call直接进行调用。

        该函数的作用是把一个字符串转换成整数类型。

 

图表 18:汇编语言——atoi函数操作

(5) sleep函数

       该函数同样通过call直接进行调用,传参数量为1,是休眠的秒数。

 

图表 19:汇编语言——sleep函数操作

(6) getchar函数

       该函数同样通过call直接进行调用,不用传参,直接使用即可。

 

图表 20:汇编语言——getchar函数操作

3.4 本章小结

        本章讲述了编译的概念和作用,并结合hello.c的编译结果对于汇编语言中的各部分以及各种操作进行了详细的说明,从而能够完全理解hello.s这份汇编代码中的每一部分,也对于编译结果加深了理解,也能够运用得更加自如。通过本章也为后面章节的深入分析与作业打下基础。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

·汇编的概念:

       把汇编代码翻译成及其语言指令,把这些指令打包成为可重定位目标程序的格式,并形成.o文件的过程就是汇编。.o文件是一个二进制文件,它所包含的17个字节是函数main的指令编码。这也就导致了如果我们在文本编辑器中尝试打开hello.o文件看到的将是乱码。

       汇编的过程由汇编器来完成,即汇编器的工作是把汇编代码转变成机器可以执行的指令。由于每一条汇编语句都基本对应一条机器指令,所以会变得过程不必分析语法、语义等,只需要按照翻译表一一对照着翻译就可以了,汇编器的工作还是相对简单的。

·汇编的作用

       汇编的过程将汇编代码转换为计算机能够理解并执行的二进制机器代码,这个二进制机器代码是程序在本机器上的机器语言的表示,从而程序真正可以被执行。

4.2 在Ubuntu下汇编的命令

图表 21:汇编的命令

4.3 可重定位目标elf格式

4.3.1 通过命令导出hello.o的elf文件

图表 22:elf.txt

4.3.2 elf

       elf头以一个16字节的序列开始,这个序列是对于声称该文件系统下一些字的大小等信息的描述。而后包含一些能够帮助链接器语法分析并解释目标文件的信息,包括elf头的大小、目标文件的版本、机器类型、节头部表的文件偏移、以及节头部表中条目的大小和数量。下图是elf头的代码:

图表 23:elf头代码

4.3.3 节头表

       节头表描述了.o文件中每一个节出现的位置、大小,目标文件中的每一个节都有一个固定大小的条目。下图是节头表的代码:

图表 24:节头表代码

4.3.4 重定位节

       重定位节包含了在代码中使用的一些外部变量等信息,在链接的时候需要根据重定位节的信息对于某些变量符号进行修改。链接的时候链接器会根据重定位节的信息对于外部变量符号决定选择何种方法计算正确的地址,例如通过偏移量等信息计算。本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf,slepsecs,sleep,getchar这些符号同样需要与相应的地址进行重定位。重定位部分的代码如下:

图表 25:重定位节代码

4.3.5 符号表

       .symtab是一个符号表,它存放于程序中定义和引用的函数和全局变量的信息。例如本程序中的getchar、puts、exit等函数名都需要在这一部分体现。符号表的代码如下:

图表 26:符号表代码

       其中除了main函数中符号具有type外,剩余的都是notype,证明对于这些函数的链接还未完成。

       还有对于hello.c的type是file,这与其他符号表中符号的类型都不一样。

4.4 hello.o的结果解析

        使用命令objdump进行对于hello.o的反汇编得到汇编代码disasm_hello.s。

        objdump -d -r hello.o > disasm_hello.s

        使用gedit将其打开得到的结果如下:

图表 27:hello.o的反汇编结果

       通过对于disasm_hello.s与一开始的hello.s,我们能够发现如下的不同点:

4.4.1. 分支转移

       在上面分析hello.s中,跳转指令的地址直接记为段名称,例如.L2、.L3等,但是在disasm_hello.s中,跳转的目标是具体的地址,在机器代码中体现为目标指令地址与当前指令下一条指令的地址差。

图表 28:disasm_hello.s与hello.s的不同——分支转移

4.4.2. 函数调用

       在上面分析hello.s中,call后面直接就能够读取函数的名称,但是在我们反汇编得到的disas_hello.s中,call的目标地址却是当前的指令的下一条指令。这是因为hello.c中调用的函数,例如getchar、printf、exit等都是共享库的函数,这些都需要动态链接器的作用才能够确认函数运行的时候的执行地址,但介于在汇编成为机器语言之后还没有进行上述操作,所以现在call指令后面对应的相对地址被设置为了0(此时目标地址就是下一条指令)。

       然后通过添加重定位信息,等待静态链接进一步确定。

       介于上述的原因,目前从call后面读取到的讯息并不可靠,也不会是我们即将调用函数的有效执行地址。

       此外,所有函数的地址都是以main函数的地址进行基准,然后通过main函数的相对偏移地址进行使用的,因为其余函数只有在链接之后才能确定运行执行的地址。故它们也对应着重定位条目。

       函数调用部分代码如下:

图表 29:disasm_hello.s与hello.s的不同——函数调用

       其中把对应到的下一条指令的地址圈出来了。

4.4.3. 全局变量访问

       出现的情况与函数调用类似。在hello.s中直接通过段名称+%rip访问rodata,但是在disasm_hello.s中由于不知道rodata的数据地址,所以只能先写成0+%rip进行访问,而后在后续的操作中利用重定位和链接来实现对于rodata的访问。全局变量访问的部分代码如下:

 

图表 30:hello.s的全局变量访问

图表 31:disasm_hello.s的全局变量访问

4.4.4. 数的表示

       hello.s里面的数是十进制表示,disasm_hello.s里面的数是十六进制表示。

4.4.5. 分支转移

       hello.s中在跳转时直接使用的是段的名称,disasm_hello.s中使用的是相对便宜的地址,也就是间接地址。

4.5 本章小结

        本章对于汇编的结果进行了详尽的介绍。通过汇编器的操作后,汇编语言转化为了机器语言,hello.o可重定位的目标文件的生成也为最后的链接做好了准备。我们从两个角度来认识了hello.o这个文件,首先是可重定位目标elf格式文件,我们对于elf头、节头表、重定位节和符号表分别分析了代码,与其他一系列工作联系最紧密的部分是重定位节。然后我们基于这种重定位的工作原理对于hello.o的反汇编与hello.c的汇编进行了对比,更深刻地认识了重定位和链接的作用,这也为下一章的工作做好了铺垫。

(第41分)

5章 链接

5.1 链接的概念与作用

·链接的概念

       链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,也是源代码能够成功运行的最后一项前置工作。组合成的单一文件可以被加载、复制到内存并执行。链接有多种执行的模式,可以在编译时候执行,也可以在加载的时候执行,甚至可以在运行的时候执行。这三种执行模式分别对应着三个阶段,源代码被翻译成机器代码的过程;程序被加载器加载到内存并执行的过程;应用程序接盘进程的过程。随着科技的创新,现在的链接工作是由链接器来自动执行的。

·链接的作用

       链接使得分离编译成为了可能。不再非要把冗长的程序写在一个源文件中,而是可以分解为许多的小文件,把工程模块化。并且这样操作可以使得每个模块可以独立修改,独立操作,修改后只需要重新编译即可,其余的工作交给链接器就可以了。而不是像之前一样没修改的部分也要被迫参与重新编译,这样大大节省了相关资源。

5.2 在Ubuntu下链接的命令

图表 32:链接的命令

       这样经过预处理、编译、汇编、链接就得到了源程序的最终可执行文件。

5.3 可执行目标文件hello的格式

5.3.0 相关命令

       使用命令:readelf -a hello > hello.elf

图表 33:hello.elf文件

       通过初步观察可以发现从整体的格式到内容,这个hello.elf和上面的elf.txt基本相同,就是hello.elf的内容相较于elf.txt多了许多。下面进行重点分析:

5.3.1. elf

       hello.elf的elf头和elf.txt的elf头所包含的信息种类基本相同,仍然是从Magic开始,到帮助链接器语法分析和解释目标文件的信息。不同的是程序头大小和节头数量都得到了增加,更重要的是获得了入口地址。hello.elf的elf头代码如下:

图表 34:elf头代码

5.3.2. 节头表

       相较于elf.txt,内容更加丰富详细。hello.elf的节头表部分代码如下:

图表 35:节头表部分代码

       尝试分析可以发现,当完成链接之后程序中的一些文件就被添加进来了,这也证明我们的链接是有效的。此外,每一节都有了实际地址也可证明这一点。

5.3.3. 重定位节

图表 36:重定位节代码

       可以发现hello.elf的重定位节与elf.txt的重定位节的名字以及内容都完全不一样,现在的所有加数都是0,这能够证明在链接环节确实完成了我们上文提到的各种重定位效果。

5.3.4. 符号表

图表 37:符号表的部分代码

       与elf.txt不同的是,main函数以外的符号也拥有了type,这证明我们完成了链接工作。相同的是符号表的功能仍然没有发生变化,所有重定位需要引用的符号都在其中说明。

5.3.5. 程序头

       程序头是一个结构数组,描述了系统准备程序执行所需的段或者其他信息。程序头部分的代码如下:

图表 38:程序头代码

       程序头描述了可执行目标文件的连续的片与连续的虚拟内存段之间的映射关系。从程序头中可以看到根据可执行目标文件的内容初始化为了两个内存段,分别为只读内存段(代码段)和读写代码段(数据段)。

5.4 hello的虚拟地址空间

·与节头表对应的部分:

图表 39:edb的Date Dump信息

       可以发现程序的虚拟地址从0x401000开始到0x402000结束,这与.elf文件的节头表部分正好完全对应。

       此外查看symbols选项也能发现程序各部分的地址都可以与.elf的节头表完全对应。

图表 40:edb的Symbols信息

·与elf头对应的部分

        通过查看地址为0x400000的位置对应到了elf头的magic部分,向后阅读也能读到字符串的部分。

5.5 链接的重定位过程分析

        链接过程主要包括了地址和空间分配、符号决议和重定向

        符号决议:有时候也被叫做符号绑定、名称绑定、名称决议、或者地址绑定,其实就是指用符号来去标识一个地址。

        重定位:重新计算各个目标的地址过程叫做重定位。

        hello-o-objdump中的地址是相对偏移地址,hello-objdump中的地址是虚拟地址,而且跳转和函数调用都以虚拟地址。因为hello反汇编的结果中,由于链接过程中重定位而加入进来各种函数、数据,许多开始的函数和调用的函数填充在main函数之前,main函数的位置发生改变。而这些call函数引用全局变量,和跳转模块值时地址也有所变化。

        最基本的链接叫做静态链接,(Linux:.o)目标文件和库一起链接形成最后的可执行文件。库其实就是一组目标文件的包,就是一些最常用的代码变异成目标文件后打包存放。最常见的库就是运行时库,它是支持程序运行的基本函数的集合。

        hello-objudmp中增加了许多外部链接的共享函数库,例如printf@plt以getchar@plt等等。

        链接器把 hello.o 中的偏移量加上程序在虚拟内存中的起始地址得到了可直接访问的地址。

图表 41:hello的反汇编代码

5.6 hello的执行流程

名称

地址

_init

0x401000

.plt

0x401020

puts@plt

0x401030

exit@plt

0x401060

_printf_chk_@plt

0x4010b0

sleep@plt

0x401070

getc@plt

0x4010e0

_start

0x4010f0

main

0x4011d6

_libc_csu_init

0x401260

_libc_csu_fini

0x4012d0

_fini

0x4012d8

图表 42:各个子程序名以及地址

5.7 Hello的动态链接分析

         动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而非静态链接一样把所有程序模块都链接成一个单独的可执行文件。(注意形成可执行文件和执行程序,生成可执行文件,在执行程序时仍会修改)

        plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次plt就能跳转到正确的区域。延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:

        PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。

        GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。

        可以在elf文件中找到后进入edb查看:

图表 43:dl_init之前

图表 44:dl_init之后

5.8 本章小结

        本章主要内容为将hello程序进行链接,对得到的文件格式以及虚拟地址空间进行了分析,本章主要使用的工具为edb,通过使用edb以及阅读文件的elf可以看到程序的执行过程以及在其中涉及到的程序地址动态链接内容与相关函数调用,同时我们还分析了动态链接前后程序的变化,使我们对这一过程有了更为直观的认识。这样我们也就成功获得了hello的可执行文件,下一步hello就要转变为process(进程)了,这是它P2P的重要的一步。

(第51分)

6章 hello进程管理

6.1 进程的概念与作用

·进程的概念

        一个执行中程序的实例,系统中的每个程序都运行在某个进程的上下文之中。上下文是由程序正确运行所需的状态组成的。这种状态包括存放在内存中的程序的代码以及数据、栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

·进程的作用

       进程能够提供给应用程序的一些关键抽象,比如:

       (1) 一个独立的逻辑控制流,它提供一个假象,好像程序能够独占使用处理器。

       (2) 一个私有的地址空间,它提供一个假象,好像程序能够独占使用内存系统。

       此外,通过进程可以更方便于描述和控制程序的并发执行,实现操作系统的并发性和共享性,更好实现CPU、时间、内存等资源的分配和调度。

       通过进程的使用可以做到在一个时间段内有多个程序并行,其中它们的资源均为独立分配,调度均为独立接受,运行也是独立的、互不打扰的。这样更方便实现各种控制与管理。

6.2 简述壳Shell-bash的作用与处理流程

·shell的功能:

  1. 读取输入并解析命令行。
  2. 替换特别字符,例如通配符和历史命令符等。
  3. 设置管道、重定向以及后台处理。
  4. 处理信号。
  5. 程式执行的相关设置。
  6. 其他。

·shell的处理流程:

  1. shell首先从命令行中找到特殊字符(元字符),将元字符翻译成间隔符号。从而实现元字符对于命令行的划分(注:Shell中的元字符:SPACE、TAB、NEWLINE、’&’、’;’、’(’、’)’、’<’、’>’、’|’)。
  2. 程序块被处理,检查其中是否是shell所引用的关键字。
  3. 当程序块被确定之后,shell根据aliases文件中的列表依次检查命令的第一个单词,当这个单词出现在aliases表中的时候,执行替换操作,并回到步骤(1),重新进行程序块的划分。
  4. shell对于符号’~’进行替换。
  5. shell对于所有前置’$’的变量进行替换。
  6. shell将命令行中的内嵌命令表达式替换为命令,多采用$(command)的方法进行标记。
  7. shell计算采用$(expression)进行标记的算术表达式。
  8. shell依据栏位分割符号将命令字符串重新划分为新的程序块(注:shell中缺省的栏位分割符号:SPACE、TAB、’\n’)。
  9. shell执行通配符的替换(注:shell中的通配符:’*’、’?’、’[’、’]’)。
  10. shell把所有从处理结果中用到的注释删除,并且按照如下顺序进行命令的检查:
    1. 内建命令。
    2. shell函数(由用户自己定义的)。
    3. 可执行的脚本文件(需要寻找文件以及PATH路径)。
  11. 初始化所有的输入输出重定向。执行命令。

·bash

       bash是最常用的一种shell,是一种特殊的交互工具,一种命令行解释器。

6.3 Hello的fork进程创建过程

       (1) 代参执行当前目录下的可执行文件(本作业中即为hello),父进程会通过fork函数创建一个新的运行的子进程hello。

        (2) 子进程获取了父进程的上下文,包括栈信息、通用寄存器、程序计数器、环境变量和打开的文件相同的一份副本。两个进程本质上的不同就是子进程与父进程有着不同的PID。由于这种父进程与子进程的从属关系,子进程可以读取父进程打开的任何文件。

        (3) 当子进程运行结束的时候,对其执行回收。若其父进程仍存在,则由其父进程执行回收,否则由init进程执行回收。

6.4 Hello的execve过程

       execve过程发生在调用fork创建新的子进程之后。作用是在当前进程的上下文中加载并运行一个新的程序:hello。execve函数没有返回值,因为其没有返回这一过程。当且仅当出现错误的时候execve函数才会返回至调用程序。

       子进程通过execve系统调用加载器删除子进程现有的虚拟内存段,并创建一组新的关于hello程序的代码、数据、堆和栈段。新的栈和堆段被初始化成0.通过将虚拟地址空间中的页映射到hello的页大小的片,新的代码和数据段被初始化为可执行文件的内容。

       最后跳转至hello的开始地址,开始执行main函数。

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

6.5.1 相关概念

·上下文信息

        上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构。操作系统内核上下文切换来实现多任务。上下文切换包括:

        1.保存当前进程的上下文

        2.恢复某个先前被抢占的进程被保存的上下文

        3.将控制传递给这个新恢复的进程。

·进程时间片

        一个进程执行它的控制流的一部分的每一时间段叫做时间片。

·调度

        在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。调度(scheduling),是由内核中称为调度器(scheduler)的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,

·用户态核心态转换(用户模式和内核模式)

        一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权,也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。

6.5.1 hello程序的原理

        hello程序运行的相关原理图如下所示。

 图表 45:hello程序运行的相关原理图

       通过以上的概念介绍,我们可以知道在hello程序中,主要的切换和进程调度发生在sleep函数位置,所以执行sleephello进程会处于用户模式并一直顺序执行,但是当执行sleep函数以后会被切换到到内核模式中休眠两秒,将其加入等待序列中,当两秒钟结束后则会中断并切换到用户模式执行之前被挂起的程序,直至遇到下次切换。

6.6 hello的异常与信号处理

·当输入回车(输入随机字符串时)

图表 46:异常——输入回车、随机字符串

       可以发现回车和随机字符串对于程序的运行并没有影响,终端中会出现所有相关的输入,此外如果随机字符串搭配回车一起输入,每个回车会被终端识别为一条新的指令,并不会影响到现有程序运行。

·输入Ctrl-C的时候

图表 47:异常——输入Crtl-C

        当向hello程序输入Ctrl-C后,会导致中断异常产生SIGINT信号,向子进程发出SIGKILL信号终止并回收,进程会终止。

·输入Crtl-Z的时候

图表 48:异常——输入Ctrl-Z

       当向hello程序输入Ctrl-Z后,会导致中断异常产生SIGSTP信号,进程被挂起,与输入Ctrl-C结果不同。此时分别输入ps,jobs,pstree,fg,kill指令进行查看相关信息。

图表 49:异常处理——ps指令

       可以看出hello进程并未停止,而是被挂起。在进程列表中仍然可以看到hello这个进程。

       究其原因是SIGTSTP是一个暂时停止的信号,如果该进程接收到一个SIGCONT信号就会继续运行。故该进程只是被挂起,而并没有如上文所提被父进程或者是init进程被回收。

图表 50:异常处理——jobs指令

       通过本指令验证hello进程确实被挂起,处于停止的状态。

图表 51:异常处理——pstree指令

       可以通过调出进程树来查看所有进程的情况。

图表 52:异常处理——fg指令

       fg指令的用处是使第一个后台作业变成前台作业,正巧这里hello是第一个后台作业,所以fg会使得hello回到前台并完成运行。

图表 53:异常处理——kill指令

       kill指令成功杀死进程hello,从进程列表以及提示的信息中均可得知。

6.7本章小结

       本章从进程的管理来认识hello。我们从进程的概念、作用,以及我们操纵进程的shell和bash的处理流程入手,了解了进程的相关工作原理。而后我们分析了与进程息息相关的两个函数——fork和execve的作用和工作机理,并把这些都应用到了对于hello进程的管理之中。我们尝试了发送不同的异常信号以及不同的针对SIGTSTP信号的异常解决方法,对我们的理论知识得到了验证。通过这一章,我们对于hello程序的进程管理有了形象的认识。

(第61分)

7章 hello的存储管理

7.1 hello的存储器地址空间

        逻辑地址(logical address)包含在机器语言指令中用来指定一个操作数或一条指令的地址。它促使程序员把程序分成若干段。每一个逻辑地址都由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。对应于hello.o中的相对偏移地址。

        线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。

        虚拟地址(Virtual Address)就是在程序运行中程序中使用的逻辑地址,程序通常运行在虚拟地址空间中。由于是段式存储模式,所以虚拟地址是二维的,用段基址和段内位移表示。

        物理地址(Physical Address)是线性地址经过页式变换得到的实际内存地址,这个地址被送到地址总线上,定位实际要访问的内存单元。计算机系统的贮存被组织称一个有M个连续的字节大小的单员组成的数组。每个字节都有一个唯一的物理地址。

        在hello程序中,需要将各个指令的虚拟地址变为物理地址并完成各种操作,具体的过程为:先将hello虚拟地址或逻辑地址通过运算映射等方式得到线性地址,而后线性地址再通过页式管理变换的方式转变为物理地址,从而实现hello程序的相关执行。

7.2 Intel逻辑地址到线性地址的变换-段式管理

        逻辑地址由段选择符和偏移量组成,线性地址为段首地址与逻辑地址中的偏移量组成。其中,段首地址存放在段描述符中,而段描述符存放在描述符表中。描述符表分为GDT(全局描述符表)和LDT(局部描述符表)两种。

段式管理的特点:

        (1) 段式管理以段为单位分配内存,每一段分配一个连续的内存区。

        (2) 由于各段的长度互不相等,所以这些存储区的大小也都各不一样。

        (3) 同一进程包含的各段之间不要求连续。

        (4) 段式管理的内存分配与释放在作业或者进程的执行过程中动态执行。

7.3 Hello的线性地址到物理地址的变换-页式管理

        计算机利用页表,通过MMU(内存管理单元)来完成从虚拟地址到物理地址的转换。

        线性地址即虚拟地址,用VA来表示。VA被分为虚拟页号(VPN)与虚拟页偏移量(VPO),CPU取出虚拟页号,通过页表基址寄存器来定位页表条目,在有效位为1时,从页表条目中取出信息物理页号(PPN),通过将物理页号与虚拟页偏移量(VPO)结合,得到由物理地址(PPN)和物理页偏移量(PPO)组合的物理地址。

7.4 TLB与四级页表支持下的VA到PA的变换

图表 54:虚拟地址中用以访问TLB的组成部分

       TLB是一个小的、虚拟寻址的缓存,其中每一行都保存有一个单个PTE组成的块。TLB通常有高度的相连度,用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。关键点在于所有的地址翻译步骤都是在芯片的MMU执行的,因此速度非常快。一般的处理流程为:

  1. CPU产生一个虚拟地址
  2. MMU从TLB中取出相应的PTE
  3. MMU将这个虚拟地址翻译成一个物理地址,并将其发送至高速缓存、主存
  4. 高速缓存、主存将所请求的数据字返回给CPU

        当进程运行时,它的页目录表地址被加载到CR3控制寄存器中。36位VPN被划分成四个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含L1页表的物理地址,CPU通过CR3得到页目录表的地址,进行地址变换。变换的过程是:先用页目录号作为索引,在页目录表中定位对应的表项,从中得到页表的页帧号。同样再以页表号为索引在页表中找到对应的页表项,从中得到被映射地址的页帧号。即:VPN1提供到一个L1PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2PTE的偏移量,以此类推同样的方法依次向下完成,页帧号与页内位移相拼即得到物理地址。

7.5 三级Cache支持下的物理内存访问

图表 55:Core i7的内存系统

        在Intel Core i7处理器的cache的每个CPU芯片有四个核,每个核有自己私有的L1 i-cache、L1 d-cache和L2统一的高速缓存。所有的核共享片上L3统一的高速缓存。这个层次结构的一个有趣的特性是所有的SRAM高速缓存存储器都在CPU芯片上。L1、L2和L3高速缓存是物理寻址的,块大小为64字节。L1和L2是8路组相联的,而L3是16路组相联的。其运作原理为使用更快的存储设备来保留从较慢的存储设备读取的数据的副本。当数据需要从较慢的存储设备读写时,缓存允许读写首先在较快的存储设备上完成,从而提高系统的响应能力。

        对于三级缓存的相关介绍如下:

        一级缓存内置在CPU中,并以与CPU相同的速度运行,使其更有效率。一级缓存越大,CPU的效率越高,但是由于CPU的内部结构,一级缓存的容量非常小。

        二级缓存用于中和一级缓存和内存间的速度。CPU调用缓存从一级缓存开始,随着处理器速度的增加,一级缓存的大小难以满足需求,因此必须增加相应的二级缓存。二级缓存相比一级缓存慢,但是它比一级缓存有更多的空间。它主要用于一级缓存和内存之间的临时数据交换。

        三级缓存被设计用来从二级缓存中读取丢失的数据。在具有三级缓存的CPU中,只有大约5%的数据需要从内存中调用,这进一步提高了CPU的效率。它的工作原理是使用更快的存储设备来保留从较慢的存储设备读取的数据副本。当数据需要从较慢的存储设备读取和写入时,缓存将允许读写首先在较快的设备上完成,从而提高系统的响应能力。

7.6 hello进程fork时的内存映射

       6.3节已经提到了有关fork被调用的时候会做什么。

       新进程现在的虚拟内存刚好和调用fork时的虚拟内存相同。当这两个进程中的任一个后来进行写操作的时候,写时的复制机制就会创建新页面。这样就为每个进程保持了私有地址空间新的抽象概念。

       写时的复制机制是说,当故障处理程序注意到保护异常是由于进程试图写私有的写时复制区域中的一个页面而引起的,它就会在物理内存中创建这个页面的一个新副本,更新页表条目指向这个新的副本,然后恢复这个页面的可写权限,如下图所示:

图表 56:一个私有的写时复制对象

7.7 hello进程execve时的内存映射

        execve函数的区域映射包含两部分,分别是私有区域的映射和共享区域的映射。

·映射私有区域

        为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。

·映射共享区域

        如果hello程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

图表 57:加载器映射用户地址空间区域

7.8 缺页故障与缺页中断处理

        假设MMU在试图翻译某个虚拟地址A时,出发了一个缺页。这个异常导致控制转移到内核缺页处理程序,处理程序随后就执行下面的步骤:

        1.判断虚拟地址A是否合法。也就是说,A在某个区域结构定义的区域内吗?却也异常处理程序搜索区域结构的链表,将A与每一个区域结构的头和尾相比较,如果没有匹配到任何结果,说明是不合法的,于是报出段错误。实际上系统采用更快的方式搜索链表,借助了树的数据结构。

        2.判断进行的内存访问是否合法。也就是说进程是否有读、写或者执行这个区域内页面的权限?如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。

        3.此时,如果已知这个缺页是由对合法的虚拟地址进行合法的操作造成的,就开始处理缺页。选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的命令,这条指令将再次发送AMMU。这次MMU就能正常地翻译A,而不会再产生缺页中断了。

7.9动态存储分配管理

·基本原理:

       当运行时需要额外虚拟内存的时候,使用动态内存分配器会使得用户更加方便,同时也能够具有更好的可移植性。动态内存分配器维护着一个进程的虚拟内存区域,这个我们称为堆。堆是一个请求二进制零的区域,紧接着未初始化的数据区域后面开始,向着更高的地址生长。对于每个进程来说,内核维护着一个变量brk指向堆顶。分配器会将堆看做一组不同大小的块的集合来维护,每个块就是一个连续的虚拟内存片,可能是已经分配的,也可能是空闲的。已经分配的块显式地保留,供应用程序使用;空闲块可以被用来分配,在被分配之前,它将保持空闲。一个已经分配的块会保持已分配状态,直至其被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

·分配器:

       共有两种分配器,显示分配器和隐式分配器。具体两种分配器的特点和相应的原理如下:

        显式分配器(explicit allocator),要求应用显式地释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。

        隐式分配器(implicit allocator),另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbage collector),而自动释放未使用的已分配的块的过程叫做垃圾收集(garbage collection)。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。

7.10本章小结

        在本章中我们对于hello的各种地址概念进行了介绍,然后从变换过程中的段式管理与页式管理入手,阐述了这种作用机制是如何在hello中运行的。四级列表,三级缓存,这些都是实现虚拟地址到物理地址的变换的必需品,也会在获得了物理地址之后,完成对于物理地址的访问这一环节有所作用。我们又重新再认识了fork和execve这两个函数,这次是从内存映射的角度。缺页故障与缺页中断时如何处理,动态内存存储又是如何分配管理的。我们通过以上的内容实现了Hello程序的正常运行,到这里hello也快走完了它的一生。

(第7 2分)

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

·IO协调工作的方式:

  1. 对于设备来说,由两部分组成:机械部分和电子控制部分。
  2. 操作系统对于IO设备的管理主要分为三部分:逻辑IO、设备驱动程序和中断服务程序。其中设备驱动程序完成对不同设备的各种控制,对应用层提供接口;中断服务程序的作用是当设备结束的时候向cpu发出中断信号。

·IO管理的目标和任务:

  1. 按照用户的请求,控制设备的各种操作,完成IO设备和内存的数据交换,最终完成IO请求。
  2. 建立方便统一而且独立于设备的接口。
  3. 充分利用各种技术提高cpu与设备、设备与设备之间的并行工作能力。

·IO控制方式:

  1. 轮询方式:不断查看IO设备的状态信息并且做出回应。
  2. 中断方式:当设备完成操作的时候对cpu进行中断。
  3. DMA:不通过cpu、内存和IO设备直接进行数据交换。

·IO软件的设计:

  1. 分层的设计思想:底层是对硬件的控制和操作,高层是逻辑的操作。
  2. 用户级IO软件:用户执行输入输出系统调用,对于IO数据进行格式化。
  3. 设备无关的软件层。
  4. 设备驱动层。
  5. 中断处理层:当IO操作完成的时候,唤醒设备驱动程序进程,进行中断处理。

8.2 简述Unix IO接口及其函数

·Unix IO接口:

       打开文件:内核返回一个非负整数的文件描述符,通过对此文件描述符对文件进行所有操作。

       Linux Shell创建的每个进程开始地时候都有三个打开的文件:标准输入(文件描述符0)、标准输出(文件描述符1)和标准出错(文件描述符2)。头文件<unistd.h>中定义了相关的常量可用来代替显式的描述符值。

       应用程序通过seek操作,改变当前的文件位置:利用文件开始的位置作为文件的偏移量,可以设置文件的当前位置为k。

       读操作:从文件复制n个字节到内存,从当前文件位置k开始,将k增加到k+n。

       写操作:从内存复制n个字节到文件,当前文件位置是k,更新k。

       关闭文件:当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中。

·Unix IO函数:

  1. open函数
    1. 功能描述:用于打开或者创建文件,在打开或者创建文件的时候可以指定文件的属性以及用户的权限等各种参数。
    2. 函数原型:int open(const char *pathname,int flags,int perms)
    3. 参数:pathname:被打开的文件名;flags:文件打开方式。
    4. 返回值:成功返回文件描述符,失败返回-1。
  2. close函数
    1. 功能描述:用于关闭一个被打开的文件。
    2. 所需头文件:#include<unistd.h>。
    3. 函数原型:int closed(int fd)。
    4. 参数:fd文件描述符。
    5. 函数返回值:成功返回0,失败返回-1。
  3. read函数
    1. 功能描述:从文件读取数据。
    2. 所需头文件:#include<unistd.h>
    3. 函数原型:ssize_t read(int fd, void *buf, size_t count);
    4. fd:将要读取的数据的文件描述词。buf:指缓冲区,即读取的数据会被放到这个缓冲区中去。count:表示调用一次read操作,应该读多少数量的字符。
    5. 返回值:返回读取的字节数;0(读到EOF);-1(出错)
  4. write函数
    1. 功能描述:向文件写入数据。
    2. 所需头文件:#include<unistd.h>
    3. 函数原型:ssize_t write(int fd, void *buf, size_t count);
    4. 返回值:成功则返回写入文件的字节数;-1(出错)。
  5. lseek函数
    1. 功能描述:用于在指定文件描述符中将文件指针定位到相应位置。
    2. 所需头文件:#include<unistd.h>,#include<sys/type.h>
    3. 函数原型:off_t lseek(int fd, off_t offset,int whence);
    4. 参数:fd:文件描述符;offset:偏移量,每一个读写操作所需要移动的距离,单位是字节,可正可负。正数代表向前移,负数代表向后移。
    5. 返回值:成功返回当前位移,失败返回-1。

8.3 printf的实现分析

·printf函数的原型如下:

int printf(const char *fmt, ...)

{

    int i;

    char buf[256];

    va_list arg = (va_list)((char*)(&fmt) + 4);

    i = vsprintf(buf, fmt, arg);

    write(buf, i);

    return i;

}

        ((char*)(&fmt) + 4)表示的是可变参数中的第一个参数的地址。而vsprintf的作用就是格式化。它接收确定输出格式的格式化字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。接着从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

        字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

        显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

        getchar是读入函数的一种。它从标准输入里读取下一个字符,相当于getc(stdin)。返回类型为int型,为用户输入的ASCII码或EOF。getchar可用宏实现:#define getchar() getc(stdin)。getchar有一个int型的返回值。当程序调用getchar时.程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdin流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。

        异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

        getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

        本章介绍了 Linux 的 I/O 设备的基本概念和管理方法,以及Unix I/O 接口及其函数。最后分析了printf 函数和 getchar 函数的工作过程,从而完成了hello这一生所需的全部事情。

(第81分)

结论

本文讲述了hello的一生。

  1. 键盘输入程序,得到hello.c源文件,hello诞生了
  2. 预处理阶段:预处理器(cpp)根据以字符#开头的命令,修改原始C程序,生成hello.i
  3. 编译阶段:编译器(ccl)将文本文件hello.i 翻译成文本文件hello.s,它包含一个汇编语言程序。
  4. 汇编阶段:接下来,汇编器(as)将hello.s 翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o 中。
  5. 链接阶段:连接器(ld)合并标准C 库和hello.o 生成可执行文件hello
  6. 运行程序:利用I/O的管理机制,在终端输入./hello 7203610625 lfd 1, 1是程序睡眠的时间。shell进程调用forkhello创建一个子进程,随后调用execve启动加载器,加载映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数。
  7. 执行:CPU为其分配时间片,在一个时间片中hello享有CPU资源,顺序执行自己的控制逻辑流。
  8. 内存:MMU 将程序中使用的虚拟内存地址通过页表映射成物理地址。printf 会调用 malloc 向动态内存分配器申请堆中的内存。
  9. 进程结束:shell父进程回收子进程。

        计算机系统是计算机专业中可以说是最底层的东西,却也是万物的基础。没有汇编语言的阅读基础,我们永远也不能知道C语言转换为机器语言之后是什么样子的,我们也永远不能知道与我们朝夕相处的计算机究竟在了解着什么样的语言。没有链接这项技术,我们的代码似乎永远都会是冗长的,难以编译与理解的。没有进程管理,我们的电脑或许总是一团糟,RAM内存似乎可能永远都会不够用。没有I/O管理,纵然再优秀的程序,我们都无法与其形成真正意义上的交互。

        感谢hello这短暂却又璀璨的一生,巩固了我这学期学习计算机系统的大部分知识与脉络,更重要的是教会了我学计算机要永远从底层,从原理理解所有发生的事情,这样才能真正学会计算机,学懂计算机。

(结论0分,缺失 -1分,根据内容酌情加分)

附件

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

文件名称

文件说明

hello.i

预处理生成的文本文件

hello.s

.i文件编译后得到的汇编语言文件

hello.o

.s文件汇编后得到的可重定位目标文件

hello

.o经过链接生成的可执行目标文件

hello_obj.s

可执行文件经过链接反汇编的汇编语言文件

disasm_hello.s

.o经过反汇编生成的汇编语言文件

elf.txt

.o的elf文本文件

hello.elf

hello的elf文件

图表 58:所有的中间文件

(附件0分,缺失 -1分)

参考文献

为完成本次大作业你翻阅的书籍与网站等

[1] Randal E. Bryant , David R. O’Hallaron , 深入理解计算机系统(第三版),机械工业出版社  

[2] Thomas H. Cormen , Charles E. Leiserson , Ronald L. Rivest , Clifford Stein,算法导论(原书第三版),机械工业出版社

[3] sigprocmask函数总结_big_bit的博客-CSDN博客_sigprocmask函数

[4] [转]printf 函数实现的深入剖析 - Pianistx - 博客园

[5] Link Options (Using the GNU Compiler Collection (GCC))

[6] (7条消息) 代码逆向(一)——寻找main函数入口_yangbostar的博客-CSDN博客_ida找不到main函数

(参考文献0分,缺失 -1分)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值