ICS大作业--hello程序人生

										摘  要

Hello是每个程序员第一个接触的程序,在本文中利用计算机系统所学的知识,基于Linux平台,通过gcc、objdump、gdb、edb等工具,从c源程序的预处理开始,跟踪分析程序编译、汇编、链接、进程管理、存储管理、I\O管理整个过程,完整的参与hello的程序人生。走过C语言程序生命周期的四季,见证shell命令之春,fork进程之夏,加载执行之秋,回收释放之冬,感悟计算机系统之美。

关键词:计算机系统 汇编 链接 进程 虚拟内存

										**第1章 概述**

1.1 Hello简介
在linux中,hello.c经过cpp的预处理、ccl的编译、as的汇编、ld的链接最终成为可执行目标程序hello,在shell中键入相应命令行参数后,shell解析命令并构建argc,argv[]和envp[]参数列表,shell再调用fork()函数创建一个子进程,子进程获得与父进程完全相同的虚拟存储空间的一个备份。将解析得到的argc,argv[]和envp[]参数列表传给execve()函数作为参数,execve()函数启动加载器,在当前的上下文中开始执行可执行文件hello第一条指令即将控制转移到子进程,于是hello便从Program摇身一变成为Process,这便是P2P的过程。
开始执行程序后,随着虚拟存储的访问程序开始载入物理内存,然后进入 main函数执行目标代码,CPU通过上下文切换为运行的hello分配时间片执行逻辑控制流。当hello进程运行结束后,内核将子进程的退出状态传递给父进程,父进程shell(或init进程)负责回收hello进程,内核删除相关数据结构,以上全部便是020的过程。
1.2 环境与工具
硬件环境:Intel Core i5-8300H x64CPU 2.3GHZ,8G RAM,128G SSD +1T HDD.
软件环境:Ubuntu18.04.1 LTS
开发与调试工具:gdb,gcc,as,ld,edb,readelf,HexEdit
1.3 中间结果
hello.i 预处理之后文本文件
hello.s 编译之后的汇编文件
hello.o 汇编之后的可重定位目标执行
hello 链接之后的可执行目标文件
hello.o.objdump Hello.o的反汇编文件
hello.objdump Hello的反汇编文件

1.4 本章小结
程序有四季,往来知春秋。Hello波澜壮阔的人生画卷即将展开。。。

(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
预处理:预处理程序(CPP)对源程序中以字符#开头的命令进行处理。
作用:c预处理程序为cpp(C Preprocessor),主要用于C语言编译器对各种预处理命令进行处理,包括对头文件的包含,宏定义的扩展,条件编译的选择等,例如,对于#include指示的处理结果,就是将相应的.h文件的内容插入到源程序文件中。

2.2在Ubuntu下预处理的命令
预处理命令:
在这里插入图片描述

具体过程:
ICS大作业--hello程序人生_第1张图片
ICS大作业--hello程序人生_第2张图片
2.3 Hello的预处理结果解析
ICS大作业--hello程序人生_第3张图片
Hello.i文件部分截图

解析:预处理主要是对各种预处理命令进行处理,具体包括对头文件的包含,宏定义的扩展,条件编译的选择等。预处理主要是根据#符号进行处理,在hello中,对于#include指示的处理结果,就是将相应的.h文件的内容插入到源程序文件中,上图中的主要是对stdio.h unistd.h stdlib.h的依次展开,之后再对宏定义进行拓展,再进行条件编译的选择,即根据条件值来决定是否执行包含其中的逻辑。
2.4 本章小结
预处理开启程序编译之路,#符谱写hello人生初章。字字句句头文件,点点滴滴宏定义,条件编译一线牵。

(第2章0.5分)

				**第3章 编译**

3.1 编译的概念与作用
编译:编译程序(CCL)对预处理后的源程序进行编译,生成一个汇编语言源程序文件。
作用:主要是将C语言文件翻译为汇编语言文件。C编译器在进行具体的程序翻译之前,会先对源程序进行词法分析和语法分析,然后根据分析的结果进行代码优化和存储分配,最终把C语言源程序翻译为汇编语言程序。

3.2 在Ubuntu下编译的命令
编译命令:
在这里插入图片描述

编译过程:
ICS大作业--hello程序人生_第4张图片
ICS大作业--hello程序人生_第5张图片

3.3 Hello的编译结果解析
部分编译结果:
ICS大作业--hello程序人生_第6张图片

汇编指令:
指令 含义
.file 声明源文件
.text 以下是代码段
.globl 声明一个全局变量
.data 表明在.data节
.section .rodata 以下是.rodata节
.align 声明对指令或者数据的存放地址进行对齐的方式
.type 用来指定是函数类型或是对象类型
.size 声明大小
.long、.string 声明一个long、string类型

结果解析:
3.3.1 整数(int 型)

1.全局变量 sleepsecs:
ICS大作业--hello程序人生_第7张图片
由代码可见,首先在.text节中被声明为global变量,之后在.data节中声明对齐方式为4字节对齐,为对象类型,声明大小为4字节,另外设置为long类型其值为2(long类型在linux下与int相同为4B,将int声明为long应该是编译器偏好)。
2.局部变量 i: 在栈中分配内存,只读段和读写数据段均不做声明。如下所示,64位下通过rbp分配栈空间给i
在这里插入图片描述

	3.命令行参数 argc: 由shell解析后构造出,传递给execve作为参数启动加载器,存放在main的栈帧之上。
	4.立即数:在代码段中,运行程序时放入栈或寄存器中。

3.3.2 数组
Argv和envp: 由shell解析后构造出,传递给execve作为参数启动加载器,存放在main的栈帧之上。
ICS大作业--hello程序人生_第8张图片

3.3.3 字符串
ICS大作业--hello程序人生_第9张图片
Printf函数的命令行格式串,首先声明在.rodata节中,再声明类型为string,分别用.LC0和.LC1指代

3.3.4 赋值
程序中涉及的赋值操作有:
1.int sleepsecs=2.5 :因为sleepsecs是全局变量,所以直接在.data节中将sleepsecs声明为值2的long类型数据。
2.i=0:整型数据的赋值使用mov指令完成,在栈或寄存器中分配存储空间,根据数据的大小不同使用不同后缀

3.3.5 类型转换(隐式)
在这里插入图片描述
隐式类型转换,将float转换为int型。当在double或float向int进行类型转换的时候,程序改变数值和位模式的原则是:值会向零舍入,直接舍掉小数点后的部分。如果溢出则为NAN, 与Intel兼容的微处理器指定位模式[10…000]为整数不确定值,故会产生一个不确定值。

3.3.6 算术操作
在这里插入图片描述
i++为运算操作,在汇编中通过add或lea实现,在程序中为:
在这里插入图片描述

3.3.7 关系操作
在这里插入图片描述
在这里插入图片描述
< , !=为关系操作,在汇编中通过cmp,test,set等实现,例如cmp a,b,通过(b-a)设置标志位,如果结果为负则SF置为1,结果为0则ZF置为1,有符号数进位则CF置为1,无符号数溢出则OF置为1(还要考虑取反的影响),再根据设置好的符号为逻辑运算后确定比较结果。
在这里插入图片描述
在这里插入图片描述

3.3.8 数组/指针/结构操作
在这里插入图片描述
argv数组是shell由命令行参数构造出的,传递给main函数的第二个参数,具体在栈中的内存映像见3.3.2中的图。在汇编中,栈如下:
32(rbp)
28 i
24
20
16
12 argc
8
4
0(rsp) 指向argv

通过rbp在栈中寻找argv的首地址,由于是char *类型,故分别加8加16得到argv[1],argv[2],取内存内容后分别放入rsi和rdx,传给printf函数作为参数。
ICS大作业--hello程序人生_第10张图片

3.3.9 控制转移
在这里插入图片描述
在这里插入图片描述
在汇编中通过j进行控制转移,由cmp和j语句构成条件判断语句,在这个程序中,
在这里插入图片描述
在这里插入图片描述
由rbp在栈中分别找到argc和i,用cmp设置符号位后,再根据符号为进行跳转,从而实现控制流的转移。

3.3.10 函数操作
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
1.main函数
在这里插入图片描述
Main符号首先声明在.text节中,再声明为global变量,类型为函数类型。两个参数argc和argv[]由shell构造好后,传入execve作为参数,调用加载器,在之前的上下文中运行程序,故main的栈帧上方就是argc和argv[](envp[]),具体在栈中的内存映像见3.3.2中的图。
2.printf函数
在这里插入图片描述
ICS大作业--hello程序人生_第11张图片
第一次调用printf函数,由于只输出一个字符串,实际调用的是puts函数(更快),在汇编程序开头表明了.LC0指代第一个字符串,将它的地址放入rdi中构造好参数后,调用Puts函数。
第二次调用printf函数,分别用rdi,rsi,rdx构造好三个参数后,调用printf函数。
3.exit函数
在这里插入图片描述
exit(x)则将x作为参数放在rdi中,再控制转移调用exit函数即可
4.sleep函数
在这里插入图片描述

将参数放到rdi中,再调用sleep函数即可。
5.getchar函数
在这里插入图片描述
由于getchar没有参数,故直接调用getchar进行控制流转移即可。

3.4 本章小结
编译后在文件头部对各个变量的属性进行解释,之后才是汇编源程序。
在用rbp作为帧指针的汇编程序中,首先将rsp赋给rbp再保存rbp值,由rsp分配栈空间,rbp值不变,作为索引为各变量分配栈空间并进行查找。了解了这一点后可以方便的画出栈结构并理解汇编程序与C语言源程序的对应关系。
最是文本留不住,风骚汇编谱新篇。手握bp摘星辰,栈帧频仍一纸定。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
汇编:汇编程序(as)对汇编语言源程序进行汇编,生成一个扩展名为.o的可重定位目标文件。
作用:将编译生成的汇编语言代码转换为机器语言代码。
4.2 在Ubuntu下汇编的命令
汇编命令:
在这里插入图片描述
汇编过程:
ICS大作业--hello程序人生_第12张图片
ICS大作业--hello程序人生_第13张图片
4.3 可重定位目标elf格式
ICS大作业--hello程序人生_第14张图片
ICS大作业--hello程序人生_第15张图片
ICS大作业--hello程序人生_第16张图片
ICS大作业--hello程序人生_第17张图片
ICS大作业--hello程序人生_第18张图片
ICS大作业--hello程序人生_第19张图片

分析:

  1. ELF头:以16B的序列Magic开始,Magic描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。32位的ELF头数据结构如下:
    ICS大作业--hello程序人生_第20张图片
    在32 位 ELF 头的数据结构中, 字段 e_ident是一个长度为16 的字节序列, 其中, 最开始的4字节用来标识是否为ELF 文件, 第一个字节为Ox7F, 后面三个字节分别为 ’ E ’ 、 L’ 、’ F ’ 。再后面的12 个字节中, 主要包含一些标识信息, 例如, 标识是32 位还是64 位格式、标识数据按小端还是大端方式存放、标识 ELF 头的版本号等。字段 e_type 用于说明目标文件的类型是可重定文件、可执行文件、共享库文件, 还是其他类型文件。字段e_ machine 用于指定机器结构类型,如从 32、SPARC V9 、AMD64 等。字段 e_ version 用千标识目标文件版本。字段 e_entry用于指定系统将控制权转移到的起始虚拟地址 (入口点), 如果文件没有关联的入口点, 则为零。例如,对于可重定位文件, 此字段为0。字段 e_ehsize 用千说明ELF 头的大小 (以字节为单位)。字段e_shoff 指出节头表在文件中的偏移扯(以字节为单位)。字段 e_shentsi ze 表示节头表中一个表项的大小(以字节为单位,)所有表项大小相同。字段e_shnum 表示节头表中的项数。因此 e_shentsize和 e_shnum 共同指定了节头表的大小(以字节为单位)。仅 ELF 头在文件中具有固定位置, 即总是在最开始的位置, 其他部分的位置由ELF 头和节头表指出, 不需要具有固定的顺序。

2. 节头表
节头表由若干个表项组成,每个表项描还相应的一个节的节名,位置和长度等信息,目标文件中的每个节都有一个表项与之对应。
3. 重定位节
1…rela.text:.text节相关的可重定位信息。当链接器将某个目标文件和其他目标文件组合时,. text节中的代码被合并后,一些指令中引用的操作数地址信息或跳转目标指令位置信息等都可能要被修改。通常,调用外部函数或者引用全局变量的指令中的地址字段需要修改。
如下图,.rela.text节中记录了各个函数名和变量名对应的偏移,值,类型,符号值等信息。
ICS大作业--hello程序人生_第21张图片

各个属性的意义如下表:
偏移量 需要进行重定向的代码在.text或.data节中的偏移位置,8个字节。
信息 包括symbol和type两部分,其中symbol占前4个字节,type占后4个字节,symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位的类型
类型 重定位到的目标的类型
符号值 重定向到的目标的值
符号名称 重定向到的目标的名称
加数 计算重定位位置的辅助信息,共占8个字节

2..rela.en_frame:应该为.en_frame节相关的可重定位信息。其他和上面类似。

3.rela.data:一般而言,任何已初始化的全局变量,如果他的初始值是一个全局变量地址或者外部定义函数的地址,都需要修改。但是这个源程序中没有,故此节省略。

4.4 Hello.o的结果解析
ICS大作业--hello程序人生_第22张图片
ICS大作业--hello程序人生_第23张图片

1.机器语言的构成,与汇编语言的映射关系:可以看出,机器语言就是一堆16进制序列(实际是01序列),指令和寄存器由特定的值代表,立即数由小端表示的16进制数表示,由指令分隔开连续的16进制序列,从而与汇编语言一一对应。
2. 机器语言中的操作数与汇编语言不一致:每条指令对应的01序列的含义有不同的规定,例如push %rbp,指令为55H = 01010101B,其中高5位01010为push的操作码,为小端法,即A0,后三位101为rbp的编号,为5。再例如leave指令为C9H = 11001001B,没有显示操作数,故8位都是指令操作码。
3. hello.o的反汇编与hello.s的对比:
对比: 1.反汇编出的代码没有全局变量的信息
2.分支转移:反汇编出的代码跳转处都采用相对寻址,而在编译出的.s文件里用标志代替。因为标志只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
3.操作数:反汇编出的代码中数字采用十六进制,而编译出的.s文件里数字采用十进制
4. 函数调用:在.s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。这是因为hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text节中为其添加重定位条目,等待静态链接的进一步确定。

4.5 本章小结
汇编过程利用汇编语言与机器语言的一一映射关系将汇编语言转换为机器语言,生成可重定位目标文件。可重定位目标文件开启了目标文件阶段,使得机器可以直接识别并执行。
汇编不是无情物,化作机器更护花。

(第4章1分)

第5章 链接
5.1 链接的概念与作用

链接:将关联到的所有目标代码文件结合到一起,形成一个具有统一地址空间的可执行文件的过程称为链接。
作用:1.模块化。它能使一个程序被划分为多个模块,由不同的程序员进行编
写,而且可以构建公共的函数库以提供给不同的程序重用
2.效率高。每个模块可以分开编译在程序修改时只需重新编译修改过的源程序文件,再重新链接,时间效率更高
3,提高了空间利用率。源程序文件中无需包含共享库的所有代码,只要直接调用即可,而且可执行文件运行时的内存中,也只需要包含所调用的函数的代码而不需要包含整个共享库,空间利用率高。

5.2 在Ubuntu下链接的命令
链接的命令:
在这里插入图片描述
链接的过程:
在这里插入图片描述

5.3 可执行目标文件hello的格式
ICS大作业--hello程序人生_第24张图片
ICS大作业--hello程序人生_第25张图片
ICS大作业--hello程序人生_第26张图片

起始位置	大小

只读内存段(代码段) 0x400000 0x710
读/写内存段(数据段) 0x600000 0x42
不加载到内存的符号表和调试信息 0x0 0x954

5.4 hello的虚拟地址空间
用edb打开,可以看到再Data Dump中,自虚拟地址0x400000开始,到0x400fff结束,这之间每个节(.interp ~ .eh_frame节)的排列(开始结束)即为上图中Address中声明。在0x400fff之后虚拟地址段0x6000000x602000存放的是.dynamic.shstrtab节。
ICS大作业--hello程序人生_第27张图片

可执行文件在内存中连续存放,因而这些连续的片段被映射到虚拟地址空间中的一个存储段,程序头表(也称段头表)用于描述这种映射关系,一个表项说明一个连续的片段或者一个特殊的节。32位(64位类似)的程序头表中每个表项具有以下数据结构:
ICS大作业--hello程序人生_第28张图片

p_type(对应下图中的Type)描述存储段的类型或特殊节的类型。例如,是否为可装入段 ( PT_LOAD),是否是特殊的动态节 ( PT_DYNAMIC) ,是否是特殊的解释程序节 ( PT_INTERP) 。p_offset (对应下图中的offset)指出本段的首字节在文件中的偏移地址。p_ vaddr (对应下图中的VirtAddr)指出本段首字节的虚拟地址。p_paddr (对应下图中的PhysAddr)指出本段首字节的物理地址,因为物理地址由操作系统根据情况动态确定, 因而该信息通常是无效的。p_filesz(对应下图中的FileSiz)指出本段在文件中所占的字节数,可以为0。p_memsz(对应下图中的MemSiz)指出本段在存储器中所占字节数,也可以为0。p_flags(对应下图中的Flags)指出存取权限。p_align(对应下图中的Align) 指出对齐方式,用 一个模数表示, 为 2 的正整数幕, 通常模数与页面大小相关,若页面大小为4KB , 则模数为2^12
ICS大作业--hello程序人生_第29张图片

5.5 链接的重定位过程分析
ICS大作业--hello程序人生_第30张图片
部分反汇编结果
Hello的反汇编结果与hello.o的反汇编结果相比,多了以下的节:
_init 程序初始化代码
gmon_start call_gmon_start函数初始化gmon profiling system,这个系统是在编译程序时加上-pg选项,程序通过gprof可以输出函数调用等信息
_dl_relocate_static_pie 静态库链接
.plt 动态链接-过程链接表
Puts(等函数)@plt 动态链接各个函数
_start 编译器为可执行文件加上了一个启动例程
__libc_csu_init 程序调用libc库用来对程序进行初始化的函数,一般先于main函数执行
_fini 当程序正常终止时需要执行的代码

链接的过程:
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。链接器将上述函数加入。
2.函数调用:链接器解析重定条目时发现对外部函数调用的类型为R_X86_64_PLT32的重定位,此时动态链接库中的函数已经加入到了PLT中,.text与.plt节相对距离已经确定,链接器计算相对距离,将对动态链接库中函数的调用值改为PLT中相应函数与下条指令的相对地址,指向对应函数。对于此类重定位链接器为其构造.plt与.got.plt。
3…rodata引用:链接器解析重定条目时发现两个类型为R_X86_64_PC32的对.rodata的重定位(printf中的两个字符串),.rodata与.text节之间的相对距离确定,因此链接器直接修改call之后的值为目标地址与下一条指令的地址之差,指向相应的字符串。

基本的重定位类型有:1.R_386_PC32:相对寻址方式 2.R_386_32:绝对寻址方式。此处以相对寻址方式为例解析重定位过程。
重定位过程:
信息(r_info)可分为高位的索引值和低位的重定位类型。如下图中,以第二个表项为例,偏移量(r_offset)为0x1b,类型为R_386_PC32,即为相对寻址方式,索引值为0xb,故所引用的符号为符号表中的第11项,即为puts。
ICS大作业--hello程序人生_第31张图片
下图为hello.o的反汇编结果,可以看出符号puts从.text节中偏移量为0x1b处开始,类型为R_386_PC32,与前面分析的一致。不妨假设puts在main函数之后0x29处,则puts的地址为0x8048380+0x29 = 0x80483a9,对齐后为0x80483aa。
ICS大作业--hello程序人生_第32张图片
转移目标地址计算公式为:转移目标地址 = Pc+ 偏移地址。Call指令中的重定位地址就是偏移地址,故重定位值 = 转移目标地址-PC。转移目标地址就是符号puts定义的首地址,前面计算为0x80483aa,PC值为0x8048380+0x1f = 0x804839f,所以,重定位值应该为0x80483aa-0x804839f = 0xb,即重定位代码应改为e8 0b 00 00 00。
由上图中可以看出,e8 00 00 00 00,puts的地址初始值(init)为0x0。
由上面分析可以总结出公式:重定位值 = ADDR(r_sym) – ((ADDR(.text)+r_offset) – init)
5.6 hello的执行流程
edb观察hello的执行流程,其调用与跳转的各个子程序名或程序地址如下:
程序名称 程序地址
ld-2.27.so!_dl_start 0x7fce 8cc38ea0
ld-2.27.so!_dl_init 0x7fce 8cc47630
hello!_start 0x400500
libc-2.27.so!__libc_start_main 0x7fce 8c867ab0
-libc-2.27.so!__cxa_atexit 0x7fce 8c889430
-libc-2.27.so!__libc_csu_init 0x4005c0
hello!_init 0x400488
libc-2.27.so!_setjmp 0x7fce 8c884c10
-libc-2.27.so!_sigsetjmp 0x7fce 8c884b70
–libc-2.27.so!__sigjmp_save 0x7fce 8c884bd0
hello!main 0x400532
hello!puts@plt 0x4004b0
hello!exit@plt 0x4004e0
*hello!printf@plt –
*hello!sleep@plt –
*hello!getchar@plt –
ld-2.27.so!_dl_runtime_resolve_xsave 0x7fce 8cc4e680
-ld-2.27.so!_dl_fixup 0x7fce 8cc46df0
–ld-2.27.so!_dl_lookup_symbol_x 0x7fce 8cc420b0
libc-2.27.so!exit 0x7fce 8c889128

5.7 Hello的动态链接分析
1.加载时进行动态连接
动态链接过程如上图所示。整个过程被分成两步:首先,进行静态链接以生成部分链接的可执行目标文件 hello , 该文件中仅包含共享库(包括指定的共享目标文件 mylib.so 和默认的标准共享库文件libc.so) 中的符号表和重定位表信息,而共享库中的代码和数据并没有被合并到 hello 中;然后,在加载hello 时,由加载器将控制权转移到指定的动态链接器,由动态链接器对共享目标文件libc.so、mylib.so和hello 中的相应模块内的代码和数据进行重定位并加载共享库,以生成最终的存储空间中完全链接的可执行目标,在完成重定位和加载共享库后,动态链接器把控制权转移到程序hello。在执行hello的过程中,共享库中的代码和数据在存储空间的位置一直是固定的。
ICS大作业--hello程序人生_第33张图片
2.还可以在程序运行时进行动态链接,不做展开。
3.延迟加载
动态库是在进程启动的时候加载进来的,加载后,动态链接器需要对其作一系列的初始化,如符号重定位(动态库内以及可执行文件内),这些工作是比较费时的,特别是对函数的重定位,所以延迟重定位可以提高效率,具体来说,就是应该等到第一次发生对该函数的调用时才进行符号绑定 – 此谓之延迟绑定。
延迟绑定的实现步骤如下:
a.建立一个 GOT.PLT 表,该表用来放全局函数的实际地址,但最开始时,该里面放的不是真实的地址而是一个跳转。
b.对每一个全局函数,链接器生成一个与之相对应的影子函数,如 puts@plt。
c.所有对 puts的调用,都换成对 puts@plt 的调用。
ICS大作业--hello程序人生_第34张图片
dl_init
ICS大作业--hello程序人生_第35张图片
dl_init调用前后GOT的变化
跳转到这个地址,发现正式动态链接库的入口地址。
ICS大作业--hello程序人生_第36张图片
GOT[2]指向的地址
为了验证延迟绑定的实现,可以查看printf调用前后printf@plt的指令跳转地址,也就是对应GOT中的值,可以发现,调用后确实链接到了动态库。
5.8 本章小结
链接器位于编译器、指令集体系结构和操作系统的交叉点上,涉及指令系统、代码生成、机器语言、程序转换和虚拟存储管理等诸多概念,囊括三种目标文件格式,分为静态链接和动态链接两种,主要需要符号解析和重定位两方面的工作。
大贤者(ld)合并捕食者和暴食者(可重定位目标文件)诞生暴食之王(可执行目标文件),大贤者(可执行目标文件)加载时动态链接魔王进化(动态库)进化成智慧之王,智慧之王(可执行目标文件)运行时动态链接暴食之王(动态库)诞生虚空之神,恭喜hello殿下(萌王)。

(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程:进程的经典定义就是一个执行中程序的实例。简单来说,进程是程序的一次运行过程,更确切地说,进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,具有动态含义。
作用:“进程”的引入为应用程序提供了以下两方面的抽象:一个独立的逻辑控制流和一个私有的虚拟地址空间。每个进程拥有一个独立的逻辑控制流,使得程序员以为自己的程序在执行过程中独占使用处理器;每个进程拥有一个私有的虚拟地址空间,使得程序员以为自己的程序在执行过程中独占存储器。
6.2 简述壳Shell-bash的作用与处理流程
功能:1.它接收用户命令,能解释用户输入的命令,将它传递给内核,还可以然后调用相应的应用程序
2.调用其他程序,给其他程序传递数据或参数,并获取程序的处理结果;
3.在多个程序之间传递数据,把一个程序的输出作为另一个程序的输入;
4.Shell 本身也可以被其他程序调用。

流程:1.shell命令行解释器输出命令行提示符,接受用户命令
2.解析命令,构建argv,envp参数列表和参数个数argc
3. 如果是内置命令则立即执行,否则fork子进程
4.以构建的argc,arhv,envp为参数调用execve以启动加载器,从而在当前进程上下文中加载并运行程序
5.shell接受键盘输入信号,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
过程: 1.shell命令行解释器输出命令行提示符,接受用户命令
2.解析命令,构建argv,envp参数列表和参数个数argc
3.fork子进程,新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,这就意味着,当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程与子进程之间最大的区别在于它们拥有不同的PID。
4.以构建的argc,arhv,envp为参数调用execve以启动加载器,从而在当前进程上下文中加载并运行程序

6.4 Hello的execve过程
可执行文件执行时,会通过加载器进行加载。在Linux/Unix中,可以通过execve()启动加载器,execve()函数的功能是在当前进程的上下文中加载并运行一个新程序。execve () 函数的用法如下:
在这里插入图片描述
该函数的具体运行如下:该函数用来加载并运行可执行目标文件filenam,可带参数列表argv[]和环境变量列表envp[],若出现错误,如找不到指定文件filename,则返回-1,并将控制权返回给调用程序;若函数执行成功,则不返回,最终将控制权传递到可执行文件的main函数。此时main函数的虚拟内存中用户栈结构如下:
ICS大作业--hello程序人生_第37张图片

加载过程如下:
1.shell 命令行解释器输出一个命令行提示符(如:unix >),并开始接受用户输入的命令行。
2.当用户在命令行提示符 后输入命令行 "./ hello [ Enter] "后,开始对命令行进行解析,获得各个命令行参数并构造传递给函数 execve () 的参数列表argv ,将参数个数送 argc。
3.调用函数fork (),创建一个子进程,新创建的子进程获得与父进程完全相同的虚拟存储空间中的一个备份,包括只读段、可读写数据段、堆以及用户栈等 。
4.以第2 步命令行解析得到的参数个数argc、参数列表 argv以及全局变量 environ作为参数,调用函数 execve () ,从而实现在当前进程(新创建的子进程)的上下文中加载并运行 hello程序。在函数execve () 中,通过启动加载器执行加载任务,将可执行目标文件 hello 中的.text、. data、.bss节等内容加载到当前进程的虚拟地址空间(实际上并没有将 hello 文件中的代码和数据从磁盘读入主存,而是修改了当前进程上下文中关于存储映像的一些数据结构)。当加载器执行完加载任务后,便开始转到 hello 程序的第一条指令执行,从此, hello 程序开始在一个进程的上下文中运行。

6.5 Hello的进程执行
上下文:进程的物理实体(代码和数据等)和支持进程运行的环境合称为进程的上下文
上下文切换:操作系统通过处理器调度让处理器轮流执行多个进程。实现不同进程中指令交替执行的机制称为进程的上下文切换。
进程时间片:连续执行同一个进程的时间段称为时间片( time slice )。
时间片轮转处理器调度:每个时间片结束时,通过进程的上下文切换,换一个新的进程到处理器上执行 ,从而开始一个新的时间片,这个过程称为时间片轮转处理器调度。
ICS大作业--hello程序人生_第38张图片
进程的上下文切换具体实现:
上下文切换发生在操作系统调度一个新进程到处理器上运行时,它需要完成以下三件事:1.将当前进程的寄存器上下文保存到当前进程的系统级上下文的现场信息中;2.将新进程系统级上下文中的现场信息作为新的寄存器上下文恢复到处理器的各个寄存器中;3.将控制转移到新进程执行。这里,一个重要的上下文信息是PC的值.当前进程被打断的断点处的 PC作为寄存器上下文的一部分被保存在进程现场信息中,这样,下次该进程再被调度到处理器上执行时,就能从现场信息中获得端点处的PC值,从而能从断点处开始执行。
具体到Hello进程,则首先由shell通过加载器加载可执行目标文件hello,由操作系统完成上下文切换,从而进入hello进程(用户态),hello进程调用sleep函数后进入内核态,内核处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,上下文切换后控制转移到shell进程,定时器到时后发送一个中断信号,由信号处理函数完成处理,将hello进程从等待队列中移出重新加入到运行队列,从而进行上下文切换进入到Hello进程,10次调用sleep函数则重复以上过程10次。
当hello调用getchar的时候,实际是执行输入流是stdin的系统调用read(通过syscall调用),hello之前运行在用户模式,syscall调用read之后进入陷阱,此时进行上下文切换,开始执行shell进程,在进行相应处理后(键盘输入后),再进行上下文切换,转回执行hello进程。
ICS大作业--hello程序人生_第39张图片

6.6 hello的异常与信号处理
可能出现的异常种类:中断 故障(缺页故障,调用缺页异常处理函数即可) 可能出现的信号:SIGINT SIGSTP

正常执行:运行结束后hello进程被回收
ICS大作业--hello程序人生_第40张图片

Ctr-Z: 当按下ctrl-c之后,shell父进程收到SIGSTP信号,信号处理程序将hello进程挂起,放到后台,ps看到hello进程并没有被回收,jobs找到hello的job号,fg 1将hello进程调到前台执行
ICS大作业--hello程序人生_第41张图片

Ctr-C: 当按下ctrl-c之后,shell父进程收到SIGINT信号,由信号处理函数结束hello,并回收hello进程。
ICS大作业--hello程序人生_第42张图片

6.7本章小结
进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,每个进程都有其独立的逻辑控制流和私有的虚拟地址空间。操作系统通过进程的处理器调度,进行上下文切换,使得系统在当前进程的执行过程中发生了一个异常控制流。对于进程运行中的各种异常,相应的异常处理程序会向当前进程发送一个特定的信号,当前进程接收信号后调用相应的信号处理程序进行处理。
一片shell(贝壳)几多愁,解析构造参数流(解析命令,构造参数列表)。Fork完后又加载(调用fork,execve),唯有hello空悠悠(hello的代码和数据并没有加载到内存)。

(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。CPU通过地址总线的寻址,找到真实的物理内存对应地址。在前端总线上传输的内存地址都是物理内存地址。
逻辑地址:CPU启动保护模式后,这样程序访问存储器所使用的逻辑地址称为虚拟地址。简而言之就是虚拟内存空间中的地址。
虚拟地址:同逻辑地址。
线性地址:线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。

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

ICS大作业--hello程序人生_第43张图片

逻辑地址包含 16位的段选择符和32位的段内偏移量。转换过程中MMU首先根据段选择符中的TI 确定选择全局描述符表 ( GDT) 还是局部描述符表 ( LDT) 。若 TI = 0 , 选用GDT; 否则,选用 LDT。确定描述符表后,再通过段选择符内的13 位索引值,从被选中的描述符表中找到对应的段描述符。因为每个段描述符占 8 个字节,所以位移量为索引值乘 8,加上描述符表首地址(其中, GDT 首地址从 GDTR 的高 32 位获得,LDT首地址从 LDTR 对应的LDT 描述符cache 中高32 位获得),就可以确定选中的段描述符的地址,从中取出32 位的基地址 ( B31 - BO), 与逻辑地址中 32 位的段内偏移量相加,就得到 32 位线性地址。MMV 在计算线性地址 LA 的过程中,可以根据段的限界和段的访问权限判断是否“地址越界”或“ 访问越权”,以实现存储保护。
通常情况下 , MMU并不需要到主存中去访问 GDT 或 LDT, 而只要根据段寄存器对应的描述符cache中的基地址、限界和访问(存取)权限来进行逻辑地址到线性地址的转换,如图所示。
ICS大作业--hello程序人生_第44张图片
逻辑地址中32位的段内偏移量即是有效地址 EA,它由指令中的寻址方式来确定如何得到。从上图可看出,IA- 32 中有效地址的形成方式有以下几种:偏移量、基址、变址、比例变址、基址加偏移量、基址加变址、基址加比例变址、基址加变址加偏移量、基址加比例变址加偏移量等。比例变址时,变址值等于变址寄存器的内容乘以比例因子。

7.3 Hello的线性地址到物理地址的变换-页式管理
IA-32和x86-64采用段页式虚拟存储管理方式,通过分段方式完成逻辑地址到线性地址的转换后,再进一步通过分页方式将线性地址转换为物理地址。
下图所示的是分页部件将线性地址转换为物理地址的基本过程,为了解决页表过大的问题,采用了两级页表方式。
ICS大作业--hello程序人生_第45张图片

从图 6. 41 可看出,在一个两级页表分页方式中,线性地址由三个字段组成, 它们分别是10 位页目录索引 ( DIR ) 、10 位页表索引 ( PAGE ) 和12位页内偏移量( OFFSET)。页目录项和页表项的格式如图:
在这里插入图片描述
页目录项和页表项中部分字段的含义简述如下。
P: P= 1表示页表或页在主存中;p = 0表示页表或页不在主存中,此时发生页故障(缺页异常),需将页故障线性地址记录在CR2 中。操作系统在处理页故障时会将缺失的页表或页从磁盘装入主存中,并重新执行引起页故障的指令。
R/W: 该位为0时表示页表或页只能读不能写;为1时表示可读可写。
UI S : 该位为0时表示用户进程不能访问;为1时允许用户进程访问。该位可以保护操作系统所使用的页不受用户进程的破坏 。
PWT: 用来控制页表或页对应的cache写策略是全写( write through )还是回写( write back)。
PCD: 用来控制页表或页能否被缓存到cache中。
A : A= 1表示指定页表或页被访问过,初始化时操作系统将其清0。利用该标志,操作系统可清楚地了解哪些页表或页正在使用,一般选择长期未用的页或近来最少使用的页调出主存。由MMU在进行地址转换时将该位置1。
D: 修改位或称脏位( dirty bit)。该位在页目录项中没有意义,只在页表项中有意义。D =1 表示页被修改过;否则说明页面内容未被修改,因而在操作系统将页面替换出主存时,无需将页面写入磁盘。初始化时操作系统将其清0 , 由MMU 在进行写操作的地址转换时将该位置 l。
页目录项和页表项中的高20位是页表或页在主存中的首地址对应的页框号, 即首地址的高20位。每个页表的起始位置都按 4KB 对齐。
从图中可看出,线性地址向物理地址的转换过程如下:首先,根据控制寄存器 CR3 中给出的页目录表首地址找到页目录表,由DIR字段提供的10位页目录索引找到对应的页目录项,每个页目录项大小为 4B; 然后,根据页目录项中 20 位基地址指出的页表首地址找到对应 页表,再根据线性地址中间的页表索( PAGE字段)找到页表中的页表项;最后, 将页表项中的20 位基地址和线性地址中的 12 位页内偏移量组合成 32 位物理地址。上述转换过程中10 位的页目录索引和10 位的页表索引都要乘以4 , 因为每个页目录项和页表项都是 32 位,占4 个字节。由千页目录索引和页表索引均为10位;每个页目录项和页表项占 用 4 个字节,因此页目录表和页表的长度均为 4 KB, 并分别含有1024个表项。这样,对于 12 位偏移地址,32 位的线性地址所映射的物理地址空间是1024 x 1024 x4KB = 4GB。

7.4 TLB与四级页表支持下的VA到PA的变换
ICS大作业--hello程序人生_第46张图片
如上图所示(图中为二级页表,只要将主存框内改为4级页表即可),首先(以32位为例,64位也是一样)在TLB中查找是否有页表项,将虚拟地址根据页的大小分为虚拟页号和页内地址,再根据TLB的条目数分为标记位和组索引,在TLB(在cache里)中遍历各个条目(由页表基址寄存器值+组索引),如果标记位相同且有效位为1则读出相应的物理页号,和页内地址拼接则得出物理地址。
如果TLB中没有找到符合的条目,即TLB缺失,则在主存中查找四级页表。将虚拟页号根据四级页表的页表项数量分为四个虚拟页号,根据四个虚拟页号(前三个为页目录索引,第四个为页表索引),在四级页表中找出物理页号,如果相应页表条目为已缓存,则读出物理页号和页内地址相拼接得到物理地址。否则做缺页处理。
综合过程如下:
ICS大作业--hello程序人生_第47张图片
7.5 三级Cache支持下的物理内存访问
ICS大作业--hello程序人生_第48张图片
讨论组相联下的读取,其他类似。
得到物理地址后,根据cache块大小和行(组)数将物理地址分为标记位,组索引和块内地址,如果组索引下的cache行标记位相同,有效位为1,则命中,读出cache行中第块内地址个处的字节。如果不命中,则向下一级缓存(或主存)取相应的cache行(主存行),此时如果原来的组中有空闲行,则直接替换,如果没有空闲行,则需要替换,常用的替换策略有随机替换,LRU,LFU等。
7.6 hello进程fork时的内存映射
Shell通过调用函数fork()创建一个子进程,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,新创建的子进程获得与父进程完全相同的虚拟存储空间中的一个备份,包括只读段,可读写数据段,堆以及用户栈等。

7.7 hello进程execve时的内存映射
在函数execve () 中,通过启动加载器执行加载任务,将可执行目标文件 hello 中的.text、. data、.bss节等内容加载到当前进程的虚拟地址空间(实际上并没有将 hello 文件中的代码和数据从磁盘读入主存,而是修改了当前进程上下文中关于存储映像的一些数据结构)。当加载器执行完加载任务后,便开始转到 hello 程序的第一条指令执行。
ICS大作业--hello程序人生_第49张图片
具体步骤有:
1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
3.映射共享区域, hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
5.将main的参数传入栈,如图,将构造好的argc,argv[],envp[],写入栈作为参数。
ICS大作业--hello程序人生_第50张图片

7.8 缺页故障与缺页中断处理
首先要分清缺页故障和段故障:在主存中查找页表时,如果相应页表条目有效位为0且物理页号为NULL,则该页表条目处于未分配,此时应该是属于段故障的一种,相应异常处理程序为终止;如果相应页表条目有效位为0但是物理页号指向磁盘,则为真正的缺页故障,此时调用相应的异常处理程序,从磁盘装入相应页到内存并更新页表,再返回到故障指令开始执行。
ICS大作业--hello程序人生_第51张图片
缺页故障为返回到当前指令

7.9动态存储分配管理
动态存储分配管理由动态内存分配器完成。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。堆是一个请求二进制零的区域,它紧接在未初始化的数据区后开始,并向上生长(向更高的地址)。分配器将堆视为一组不同大小的块的集合来维护。
每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显示地被应用程序所分配。
一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
动态内存分配器从堆中获得空间,将对应的块标记为已分配,回收时将堆标记为未分配。而分配和回收的过程中,往往涉及到分割、合并等操作。
动态内存分配器的目标是在对齐块的基础上,尽可能地提高吞吐率及空间占用率,即减少因为内存分配造成的碎片。其实现常见的数据结构有隐式空闲链表、显式空闲链表、分离空闲链表,常见的放置策略有首次适配、下一次适配和最佳适配。
为了更好的介绍动态存储分配的实现思想,以隐式空闲分配器的实现原理为例进行介绍。
在这里插入图片描述
隐式空闲链表分配器的实现涉及到特殊的数据结构。其所使用的堆块是由一个子的头部、有效载荷,以及可能的一些额外的填充组成的。头部含有块的大小以及是否分配的信息。有效载荷用来存储数据,而填充块则是用来对付外部碎片以及对齐要求。基于这样的基本单元,便可以组成隐式空闲链表。
通过头部记录的堆块大小,可以得到下一个堆块的大小,从而使堆块隐含地连接着,从而分配器可以遍历整个空闲块的集合。在链表的尾部有一个设置了分配位但大小为零的终止头部,用来标记结束块。
当请求一个k字节的块时,分配器搜索空闲链表,查找足够大的空闲块,其搜索策略主要有首次适配、下一次适配、最佳适配三种。
一旦找到空闲块,如果大小匹配的不是太好,分配器通常会将空闲块分割,剩下的部分形成一个新的空闲块。如果无法搜索到足够空间的空闲块,分配器则会通过调用sbrk函数向内核请求额外的堆内存。
当分配器释放已分配块后,会将释放的堆块自动与周围的空闲块合并,从而提高空间利用率。为了实现合并并保证吞吐率,往往需要在堆块中加入脚部进行带边界标记的合并。

7.10本章小结
虚拟存储机制的引入,使得每个进程具有一个一致的、极大的、私有的虚拟地址空间。虚拟地址空间按照等长的页来划分,主存也相应划分。通过页表建立虚拟页和主存之间的对应关系。虚拟存储器有分页式、分段式、段页式三种。虚拟地址需转换为物理地址才能访存,为减少访问内存中页表的次数,通常将活跃页的页表项放到一个高速缓存TLB中。

(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
文件就是一个字节序列,所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
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.关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

系统级I/O函数:

  1. create系统调用
    用法:int creat(char•name, mode_t perms) ;

第一个参数 name为需创建的新文件的名称,是一个表示路径名和文件名的字符串;第二个参数perms用于指定所创建文件的访问权限,共有 9 位,分别指定文件拥有者、拥有者所在组成员以及其他用户各自所拥有的读、写和执行权限。通常用一个8进制数字中的三位分别表示读、写和执行权限,例如,perms = 0755, 表示拥有者具有读,写和执行权限,而拥有者所在组成员和其他用户都只有读和执行权限。正常情况下,该函数返回一个文件描述符,若出错,则返回- 1。若文件巳经存在,则该函数将把文件截断为长度为0的文件,也即,将文件原先的内容全部丢弃,因此,创建一个已存在的文件不会发生错误。
2. open系统调用
用法:int open( char name, int flags , mode_t perms);
除了默认的标准输入、标准输出和标准错误三种文件是自动打开以外,其他文件必须用相应的函数显式创建或打开后才能读写,可以用 open 系统调用显式打开文件。正常情况下,open () 函数返回一个文件描述符,它是一个用以唯一标识被打开文件的非负整数,若出错,则返回- 1。第一个参数name 为需打开文件的名称,是一个表示路径名和文件名的字符串;第二个参数flags 指出用户程序将会如何访问这个打开文件。第三个参数perms 用于指定所创建文件的 访问权限,通常在open () 函数中该参数总是 0 ,除非以创建方式打开,此时,参数flags中应带有O_C REAT 标志。不以创建方式打开一个文件时,若文件不存在,则发生错误。对于不存在的文件,可用 creat 系统调用来打开。
3. read系统调用
用法:ssize_t read(int fd, void *buf, size_t n);
该函数功能是将文件肛中从当前读写位置k开始读取n个字节到 buf 中,读操作后文件当前读写位置为k + n。假定文件长度为m, 当k + n > m 时,则真正读取的字节数为m - k < n , 并且读操作后文件当前读写位置为文件尾 。函数返回值为实际读取 字节数,因而,当 m = k(EOF)时,返回值为 0;出错时返回值为 - 1。

  1. write系统调用
    用法:ssize_t write(int fd, const void *buf, size_t n);
    该函数功能是将 buf中的n字节写到文件fd中,从当前读写位置k处开始写入。返回值为实际写入字节数 m , 写入后文件当前读写位置为 k + m。对于普通的磁盘文件,实际写入字节数 m 等于指定写入字节数 n。出错时返回值为 - 1。对于 read 和 write 系统调用,可以一次读(写) 任意字节,例如,每次读(写)一个字节或一个物理块大小,如一个磁盘 扇区 (512 字节)或一个记录大小等。显然,按照一个物理块大小来读(写) 比较好,可以减少系统调用的次数。有些情况下 , read和 write 真正读(写)的字节数比用户程序设定的所需字节数要少,这 种 情况并不被看成是一种错误 。通常,在读(写)磁盘文件时,除非遇到 EOF, 否则不会出现这种情况。但是,当读写的是终端设备文件、网络套接字文件、UNIX 管道、Web 服务 器等时,都可能出现这种情况。
  2. lseek系统调用
    用法:long lseek(int fd, long offset , int origin );
    当随机读写一个文件的信息时,当前读写位置可能并非正好是马上要读或写的位置,此时,需要用 lseek () 函数来调整文件的当前位置。第一个参数fd 指出需调整位置的文件;第二个参数指出相对字节数; 第三个参数origin指出offset相对的基准,可以是文件开头 (origin = 0 ) 、当前位置 ( origin = 1 ) 和文件末尾 ( origin= 2 ) 。
    6.stat/fstat系统调用
    用法: int stat(const name, struct statbuf);
    int fstat(int fd, struct stat *buf ) ;
    文件的所有属性信息 ,包括文件描述符、文件名、文件大小、创建时间、当前读写位置等都由操作系统内 核来维护, 这些信息也称为文件的元数据( metadata ) 。用户程序可以通过 stat ()或£stat () 函数来查看文件元数据。stat 第一个参数指出的是文件名,而fstat指出的是文件描述符,这两个函数除了第一个参数类型不同外,其他方面全部一样。
    7.close系统调用
    用法:lose(int fd ) ;
    该函数的功能就是关闭文件fd。

8.3 printf的实现分析

ICS大作业--hello程序人生_第52张图片
如图所示,假定用户程序中有一个语句调用了库函数 printf () , 在 printf () 函数中又通过一系列的函数调用,最终转到调用write () 函数。在write () 函数对应的指令序列中,一定有一条用于系统调用的陷阱指令,即system_call。该陷阱指令执行后,进程就从用户态陷人到内核态执行。Linux中有一个系统调用的统一入口, 即系统调用处理程序 system_call()。CPU执行陷阱指令后,便转到system_call ()的第一条指令执行在system_call()中,将根据RAX寄存器中的系统调用号跳转到当前系统调用对应的系统调用务例程 sys_write () 去执行。system_call()执行结束时,从内核态返回到用户态下的陷阱指令后面一条指令继续执行 。
write通过执行syscall指令实现了对系统服务的调用,从而使内核执行打印操作。内核会通过字符显示子程序,根据传入的ASCII码到字模库读取字符对应的点阵,然后通过vram(显存)对字符串进行输出。显示芯片将按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),最终实现printf中字符串在屏幕上的输出。
8.4 getchar的实现分析
getchar定义在stdio.h文件中,我们在stdio.h中可以找到其相关的定义
在这里插入图片描述
getc()的函数原型如下:
在这里插入图片描述
getchar函数实际上就是getc(stdin),即标准输入下的getc()。通过调用read函数返回字符。其中read函数的第一个参数是描述符fd,0代表标准输入。第二个参数输入内容的指针,这里也就是字符c的地址,最后一个参数是1,代表读入一个字符。read函数的返回值是读入的字符数,如果为1说明读入成功,那么直接返回字符,否则说明读到了buf的最后。
read函数同样通过sys_call中断来调用内核中的系统函数。键盘中断处理子程序会接受按键扫描码并将其转换为ASCII码后保存在缓冲区。然后read函数调用的系统函数可以对缓冲区ASCII码进行读取,直到接受回车键返回。

8.5本章小结
Linux将I/O输入都抽象为了文件,并提供相应的Unix I/O接口,用户可以通过这些接口实现输入与输出。
用户系统通常通过调用编程语言提供的库函数或者操作系统提供的API函数来实现I/O操作,这些函数最终都会调用系统调用的封装函数,通过封装函数中的陷阱指令使用户进程从用户态转到内核态执行。
历尽九九八十一难,始得修来不灭金身。

(第8章1分)
结论
Hello一路走来,真的很不容易。

  1. 预处理,C语言编译器对各种预处理命令进行处理,包括对头文件的包含,宏定义的扩展,条件编译的选择等,将hello.c转换为hello.i。
  2. 编译,将C语言文件hello.i翻译为汇编语言文件hello.s
  3. 汇编,将汇编语言代码文件hello.s转换为可重定位目标文件hello.o
  4. 链接,将多个可重定位目标文件hello.o、libc.a等经过符号解析和重定位结合,形成一个具有统一地址空间的可执行目标文件hello
  5. 运行,在shell下输入命令./hello 1170300916 pyx,shell解析命令并构造参数列表
  6. Fork子进程,shell调用fork创建一个子进程,具有和父进程完全相同的虚拟存储空间备份
  7. 加载,shell将构造好的参数列表传给execve作为参数,启动加载器并开始执行hello的第一条指令,实现在当前进程上下文中运行hello,
  8. 执行,cpu通过上下文切换分配时间片
  9. 访存,hello程序运行中需要的代码和数据,通过虚拟地址在TLB和主存页表中查找转换为相应物理地址,再在cache和主存中读取
  10. 异常处理,如果通过键盘输入导致外部中断,相应异常处理程序发送信号给进程,进程调用相应信号处理函数
  11. 回收,shell父进程回收进程,内核删除为这个进程创建的所有数据结构。
    (结论0分,缺失 -1分,根据内容酌情加分)

附件
hello.i 预处理之后文本文件
hello.s 编译之后的汇编文件
hello.o 汇编之后的可重定位目标执行
hello 链接之后的可执行目标文件
hello.o.objdump Hello.o的反汇编文件
hello.objdump Hello的反汇编文件
hello.c C源文件

参考文献
[1] 兰德尔E.布莱恩特 大卫R.奥哈拉伦. 深入理解计算机系统(第3版).
机械工业出版社. 2018.4.
[2] 袁春风 计算机系统基础 机械工业出版社,2018.
[3] ZK的博客. read和write系统调用以及getchar的实现.
https://blog.csdn.net/ww1473345713/article/details/51680017. 2016-6-15.
[4] 虚拟地址、逻辑地址、线性地址、物理地址:
https://blog.csdn.net/rabbit_in_android/article/details/49976101
[5] Pianistx. [转]printf 函数实现的深入剖析.
https://www.cnblogs.com/pianist/p/3315801.html. 2013-09-11.
[6] gcc 简单的 hello-world 到底连接了什么
https://blog.csdn.net/hejinjing_tom_com/article/details/32325749
[7] 动态链接
https://blog.csdn.net/shuange3316/article/details/79221941
[8] 动态链接原理分析
https://blog.csdn.net/shenhuxi_yu/article/details/71437167
[9] printf 函数实现的深入剖析
https://www.cnblogs.com/pianist/p/3315801.html

你可能感兴趣的:(ics大作业)