计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021112557
班 级 21w0312
学 生 赵晨希
指 导 教 师 史先俊
计算机科学与技术学院
2022年5月
本论文通过hello.c程序,对在CSAPP课程中所学知识进行整理,在Ubuntu虚拟机Linux系统下进行所有操作,运用Linux系统的工具,分析hello程序的一生。hello程序实现了P2P,hello实现从program到progress,在program阶段,他经历了hello.c,hello.i,hello.s,hello.s,hello.o,最终成为可执行文件hello,进入progress阶段。这一阶段中,hello运行在操作系统中,被其抽象为进程。系统利用异常控制流控制进程的运行,通过虚拟内存为hello分配运行空间,提供接口其与OI设备的通信。
关键词:hello,计算机系统,linux,程序人生
目 录
第1章 概述
1.1 Hello简介
Hello从源程序Hello.c开始,经过预处理器变为Hello.i,然后在编译器的处理后变为Hello.s的汇编程序,然后通过汇编器,生成了Hello.o的可重定位目标程序,经链接器链接生成可执行的Hello目标程序。
可执行目标程序Hello在服务于软硬件交互的操作系统上运行,其被抽象为进程。系统利用异常控制流控制进程的运行,通过虚拟内存为Hello分配运行空间,提供接口其与IO设备的通信。
1.2 环境与工具
1.2.1 硬件环境
11th Gen Intel(R) Core(TM) i5-11300H @ 3.10GHz 3.11 GHz
1.2.2 软件环境
Windows11+VMwareWorkstation 16 pro
Ubuntu 22.04 LTS 64
1.2.3 开发工具
GCC,EDB,GDB,Objdump,readelf,Visual Studio 2022
1.3 中间结果
1.4 本章小结
本章简要介绍了Hello的P2P,020过程,记录了实验时的软硬件环境,以及hello.c到hello的中间文件并阐述了各个文件的功能。
第2章 预处理
2.1 预处理的概念与作用
1.预处理的概念
预处理即在编译之前进行的处理,指在程序源代码被翻译为目标代码的过程中,对源程序替换修改的过程。一般而言,由预处理器对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。
2.预处理的作用
预处理的作用是从系统的头文件包中将头文件的源码插入到目标文件中,在编译代码前首先将标识符替换好,确保程序的完整性,生成.i文件后再进行接下来的编译工作。
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
hello.c由23行扩展至3091行,hello.i删除了hello.c中注释的部分,hello.c中三个#include语句被替换为从第1行开始至3078行结束的代码。
2.4 本章小结
本章简要介绍了预处理的概念和作用,并在Ubuntu下对hello.c进行预处理得到了hello.i文件,并对该结果进行了解析。
第3章 编译
3.1 编译的概念与作用
1.编译的概念
编译器将预处理后的hello.i编译成汇编语言程序hello.s。编译的过程实质上是把预处理文件进行词法分析、语法分析、语义分析、优化,从C语言等高级语言转换为汇编语言程序。
2.编译的作用
编译生成的hello.s汇编语言程序文本文件比预处理文件更容易让机器理解,其采用的汇编语言更符合机器的工作逻辑,是生成可执行文件的一步关键过程。
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1 数据
常量
字符串:如下两个函数中的字符串被存储在 .rodata节中
存储如下:
变量
局部变量存储在寄存器或栈中。程序中有局部变量 int i
在汇编代码中如下:
i被存储在栈中,-4(%rbp)的位置。
3.3.2 操作
算术操作
在循环操作中,使用 i++ 自增操作,每次循环结束后对 i 加1,对栈上存储i 的位置加1。
关系操作
1) 程序第13行中判断argc是否等于4,源代码为:
汇编代码为:
2) 程序第17行中判断 i 是否小于9,源代码为:
汇编优化为 i <= 8,汇编代码为:
控制转移
在使用比较关系操作进行判断后,程序将按判断结果经如下代码跳转至L2或L4,进入if语句或继续进入循环
数组/指针/结构操作
主函数main的参数中有指针数组char *argv[],argv[0]指向输入程序的路径和名称,argv[1]和argv[2]分别表示两个字符串。
main函数参数存储:
argv数组实现汇编代码:
函数操作
1) main函数
参数传递:传入参数argc,argv[],分别用寄存器%edi和%rsi存储
函数调用:被系统启动函数调用
函数返回:设置%eax为0,并返回
2) printf函数
i. call puts@PLT
参数传递:传入字符串参数首地址
函数调用:if判断满足条件后被调用
ii. call printf@PLT
参数传递:传入 argv[1]和argc[2]的地址
函数调用:for循环中被调用
3) exit函数
参数传递:传入参数1
函数调用:if判断满足条件后被调用
- atoi函数
参数传递:将argv[3]的值从栈中取出到%rdi中,再传入atoi函数中
函数调用:for循环中被调用
- sleep函数
参数传递:将atoi函数的返回值从%eax中存到%edi中,作为参数传给sleep
函数调用:for循环中被调用
6) getchar函数
函数调用:在for循环结束后被调用
3.4 本章小结
本章简要介绍了编译的概念和作用,并在Ubuntu下对hello.c进行编译得到了hello.s文件,并对该编译结果进行了解析,说明了编译器是怎么处理C语言的各个数据类型以及各类操作的。
第4章 汇编
4.1 汇编的概念与作用
1.汇编的概念
汇编是通过汇编器,把汇编语言翻译成机器语言的过程。
2.汇编的作用
通过汇编过程把汇编代码转换成计算机能直接识别的机器代码,把指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
1.ELF头
ELF头中,列出了Magic数,文件类型,数据组织格式,ELF 文件头版本号,操作系统类型等信息
- 节头部表
描述了.o文件中出现的各个节的类型、位置、所占空间大小等信息。
.text:已编译程序的机器代码
.rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
.data:已初始化的全局和静态C变量。
.bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。
.rodata:只读数据,比如printf中的格式串。
.comment:包含版本控制信息
.note.:这种格式的节区中包含注释信息,有独立的格式
.eh_frame:处理异常。
.rela.eh_frame:.eh_frame的重定位信息。
.symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
.strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。
.shstrtab:此节区包含节区名称
- 重定位节
表述了各个段引用的外部符号等,在链接时,需要通过重定位节对这些位置
的地址进行修改。链接器会通过重定位条目的类型判断该使用什么养的方法计算正确的地址值,通过偏移量等信息计算出正确的地址。
- 符号表
存放在程序中定义和引用的函数和全局变量的信息。
4.4 Hello.o的结果解析
代码左边多了机器码;
hello.s中的操作数为十进制,hello.o反汇编代码中的操作数为十六进制;
call跳转指令,在hello.s文件中,直接加上跳转函数名,在反汇编文件中,加上了跳转的相对偏移地址,函数在链接之后才能确定执行的地址,因此在.rela.text节中为其添加了重定位条目。
4.5 本章小结
本章简要介绍了汇编的概念和作用,并在Ubuntu下进行汇编得到.o文件,然后分析了可重定位目标elf格式,然后使用objdump进行反汇编并与.s文件进行比较,更加深入地理解机器语言与汇编语言的关系。
第5章 链接
5.1 链接的概念与作用
1.链接的概念
链接程序链接目标程序和用于标准库函数的代码,链接目标程序和由计算机的操作系统提供的资源,把在不同的目标文件中编译的代码收集到一个可直接执行的文件中。
将模块化编写的程序链接起来,成为一个整体,实现程序功能。这样一来,源程序文件中无需包含共享库的所有代码,可执行文件运行时的内存中只需要包含所调用的函数的代码。极大地方便了程序员,并且增强了程序的空间利用率。
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
- ELF文件头
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
- 节头部表
节头部表描述了25个节的相关信息,与hello.o的节头部表相比,多出来的部分:
.interp:动态链接器在操作系统中的位置不是由系统配置决定,也不是由环境参数指定,而是由ELF文件中的 .interp段指定。该段里保存的是一个字符串,这个字符串就是可执行文件所需要的动态链接器的位置,常位于/lib/ld-linux.so.2。
.dynamic:该段中保存了动态链接器所需要的基本信息,是一个结构数组,可以看做动态链接下 ELF文件的“文件头”。存储了动态链接会用到的各个表的位置等信息。
.dynsym:该段与 “.symtab”段类似,但只保存了与动态链接相关的符号,很多时候,ELF文件同时拥有 .symtab 与 .synsym段,其中 .symtab 将包含 .synsym 中的符号。该符号表中记录了动态链接符号在动态符号字符串表中的偏移,与.symtab中记录对应。
.dynstr:该段是 .dynsym 段的辅助段,.dynstr 与 .dynsym 的关系,类比与 .symtab 与 .strtab的关系 hash段:在动态链接下,需要在程序运行时查找符号,为了加快符号查找过程,增加了辅助的符号哈希表,功能与 .dynstr 类似
.rel.dyn:对数据引用的修正,其所修正的位置位于 “.got”以及数据段(类似重定位段 “rel.data”)
.rel.plt:对函数引用的修正,其所修正的位置位于 “.got.plt”。
- 符号表
符号表存放的内容为:在程序中定义和引用的函数和全局变量的信息。存在编号Num、Value、Size、Type、Bind、Vis、Ndx、Name字段。而且与hello.o文件相比,其中的函数名被替换为更为详细的内容,如puts被替换为puts@@GLIBC_2.2.5,说明其找到了该函数的定义。而且与hello.o相比,多了一个.dynsym节。
- 重定位信息
同样的,与hello.o相比,程序多出来了一个节:.rela.dyn,存放和动态链接相关的信息,是对于数据引用的修正,修正.got及数据段,同时与hello.o相比,程序的许多函数变成了类似 puts@GLIBC_2.2.5+0的形式,说明其在相关的库中找到了这个函数,是对函数引用的修正,用于修正.got.plt。
5.4 hello的虚拟地址空间
查看 ELF 格式文件中的 Program Headers,它告诉链接器运行时加载的内容,并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的各方面的信息。
程序包含:
PHDR 保存程序头表;
INTERP 指定在程序已经从可执行文件映射到内存之后,必须调用的解释器;
LOAD 表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了常量数据、程序的目标代码等;
DYNAMIC 保存了由动态链接器使用的信息;
NOTE 保存辅助信息;
GNU_STACK,权限标志,用于标志栈是否是可执行;
GNU_RELRO,指定在重定位结束之后哪些内存区域是需要设置只读。
5.5 链接的重定位过程分析
分析hello与hello.o的不同:
在hello.o中,地址为相对偏移地址;在hello中,地址为可以由CPU直接访问的虚拟内存地址;
hello的反汇编文件比hello.o的反汇编文件多了_init,.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt,_start,_dl_relocate_static_pie,__libc_csu_init,__libc_csu_fini,_fini等节和需要用到的库函数;
hello.o将lea后的操作数置为0,并添加重定位条目。
链接的过程:
链接器将各个目标文件组装在一起,文件中的各个函数段按照一定规则累积在一起。
结合hello.o的重定位项目,分析hello中对其怎么重定位的:
重定位节和符号定义,连接器将所有相同类型的节合并为同一类型的新的节,然后将运行内存地址赋给这个新的节和输入模块定义的每个节以及输入模块定义的每个符号;
重定位节中的符号引用,连接器修改代码节和数据节中对每个符号的引用,使之指向正确的运行地址
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
0x401000 _init>
0x401020 .plt>
0x401030 puts@plt>
0x401040 printf@plt>
0x401050 getchar@plt>
0x401060 atoi@plt>
0x401070 exit@plt>
0x401080 sleep@plt>
0x401090 _start>
0x4010c0 _dl_relocate_static_pie>
0x4010c1 main>
0x401150 __libc_csu_init>
0x4011b0 __libc_csu_fini>
0x4011b4 _fini>
5.7 Hello的动态链接分析
在elf文件中:
进入edb查看:
edb执行init之前
edb执行init之后
共享链接库代码是一个动态的目标模块,在程序开始运行或者调用程序加载时,可以自动加载该代码到任意的一个内存地址,并和一个在目标模块内存中的应用程序链接了起来,这个过程就是对动态链接的重定位过程。一开始地址的字节都为0,调用_init函数之后GOT内容产生变化,指向正确的内存地址,下一次调用跳转时可以跳转到正确位置。
5.8 本章小结
本章简要介绍了链接的概念和作用,并在Ubuntu下分析了hello的ELF格式以及虚拟地址空间是如何进行分配的,介绍了重定位和动态链接的过程。
第6章 hello进程管理
6.1 进程的概念与作用
1.进程的概念
进程的经典定义是一个执行中程序的实例,系统的每个程序都运行在某个进程的上下文。而上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存里的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。
2.进程的作用
通过进程,我们会得到一种假象,好像我们的程序是当前唯一运行的程序,我们的程序独占处理器和内存,我们程序的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash的作用:
Shell是一个命令解释器,是用户使用Linux的桥梁,它解释由用户输入的命令并且把它们送到内核,提供用户与内核进行交互操作的一种接口,并且允许用户编写由shell命令组成的程序。
处理流程:
用户输入键盘信号,shell应该接受这些键盘输入信号,并对这些信号进行相应处理:
从终端读入输入的命令,将输入字符串切分获得所有的参数;
shell对用户输入命令进行解析,判断是否为内置命令,如果是则立即执行;
若不是内置命令,则会检查是否为一个可执行文件,如果是,则会fork子进程,启动加载器在当前进程中加载并运行程序;
如果不是内置命令且无法找到这个可执行文件,则会显示一条错误信息;
如果程序是前台运行程序,则调用等待函数等待前台作业结束;否则将程序转入后台,直接开始下一次用户输入命令;
6.3 Hello的fork进程创建过程
在shell中输入./hello,shell会对输入的的命令进行解析,发现不是内置命令则会调用当前目录下的这个可执行文件,此时shell就会为hello去fork一个子进程,创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当shell调用fork时,hello可以读写父进程打开的任何文件。父子进程最大的区别在于他们有不同的PID。
6.4 Hello的execve过程
execve函数定义是这样的:int execve(const char *filename, const *argv[], const char *envp[]);execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。execve函数若成功则调用一次从不返回。execve函数会在当前进程的上下文中加载并运行我们的可执行目标文件,删除子进程现有的虚拟内存段,创建一组新的段(栈与堆初始化为0),并建立虚拟地址空间和磁盘文件的映射,新的代码与数据段被初始化为可执行文件的内容,然后跳到_start,直到缺页中断(回到磁盘中读,加载至物理内存,再把编号写进虚拟地址空间和物理地址空间的映射表中)随程序进行逐渐建立虚拟内存和物理内存之间的映射。值得注意是的是,execve函数是覆盖当前进程的地址空间,并没有创建一个新的进程,因的程序仍有相同的PID,并且继承了调用execve函数时已打开的所有文件描述符。
6.6 hello的异常与信号处理
1.hello执行过程中可能出现四类异常:中断、陷阱、故障和终止。
(1)中断是异步发生的,是来自外部I/O设备的信号的结果,中断处理程序对其进行处理,返回后继续执行调用前待执行的下一条代码,就像没有发生过中断一样。
(2)陷阱是同步发生的,是有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。
(3)故障是同步发生的,是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。
- 终止是同步发生的,是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将控制返回给一个abort例程,该例程会终止这个应用程序。
2.信号处理
1不停乱按,包括回车
如果乱按过程中没有回车,这个时候只是把输入屏幕的字符串缓存起来,如果输入最后是回车,getchar读回车,并把回车前的字符串当作shell输入的命令。
2 Ctrl+Z
输入Ctrl+Z会发送一个SIGTSTP信号给前台进程组的每个进程,结果是停止前台进程——即hello程序。
3 Ctrl+C
输入Ctrl+C会让内核发送一个SIGINT信号给到前台进程组中的每个进程,结果是终止前台进程——即hello程序,通过ps命令发现这时hello进程已经被回收。
4 Ctrl+Z后可以运行ps jobs pstree fg kill 等命令
Ctrl+Z使前台进程暂停,我们将逐个尝试ps jobs pstree fg kill 等命令:
(1)Ctrl+Z后运行ps
ps会列出当前正在执行的进程(包括暂停的进程),此时可以看到进程的名称和PID号
(2)Ctrl+Z后运行jobs
暂停的程序被正确的显示,而且后面列出了相应的参数,且标记为“Stopped”
(3)Ctrl+Z后运行pstree
可以看到进程树之间的关系,谁创建了谁,即哪个进程是父进程,哪个是子进程
(4)Ctrl+Z后运行fg
fg的功能是使第一个后台作业变为前台,而第一个后台作业是hello,所以输入fg 后hello程序又在前台开始运行,并且是继续刚才的进程,输出剩下的7个字符串。
(4)Ctrl+Z后运行kill
先使用ps查看hello进程的PID然后使用kill -9 11516强制杀死hello进程,再次用ps查看,发现hello程序已经被终止。
6.7本章小结
本章介绍了进程的概念和作用,讲述了shell的基本操作,还总结了shell是如何利用fork新建子进程,如何利用execve加载进程,如何进行上下文切换。重点分析了hello的异常种类,并且利用具体事例分析了其信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:
源代码经过预处理、编译、汇编后出现在汇编程序中地址,包含在机器语言中用来指定一个操作数或一条指令的地址。每一个逻辑地址都由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。
线性地址:
地址空间是一个非负整数的有序集合,如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间。线性地址就是线性地址空间中的地址。是程序中的虚拟内存地址。
虚拟地址:
在采用虚拟内存的系统中,CPU从一个有N = 2n个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间,里面的地址就是虚拟地址。
物理地址:
在实际的物理空间中的地址,叫物理地址。CPU通过地址总线的寻址,找到真实的物理内存对应地址。在前端总线上传输的内存地址都是物理内存地址。地址翻译会将hello的一个虚拟地址转化为物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符: 段内偏移量。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,Base字段表示包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。
1.先检查段选择符中的TI字段,以决定段描述符保存在哪一个描述符表中;
2.由于一个段描述符是8字节长,因此她在GDT或LDT内的相对地址是由段选择符的最高13位的值乘以8得到;
3.Base + offset,得到要转换的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
对于页式管理,我们首先要了解:线性地址(虚拟地址)由虚拟页号VPN和虚拟页偏移VPO组成。
而从线性地址到物理地址的变换,过程如下:
首先,MMU从线性地址中抽取出VPN,并且检查TLB,看他是否因为前面某个内存引用缓存了PTE的一个副本。
TLB从VPN中抽取出TLB索引和TLB标记,查找对应组中是否有匹配的条目。
若命中,将缓存的PPN返回给MMU;若不命中,MMU需从页表中的PTE中取出PPN。
若得到的PTE无效或标记不匹配,就产生缺页,内核需调入所需页面,重新运行加载指令;若有效,则取出PPN。
最后将线性地址中的VPO与PPN连接起来就得到了对应的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
先将VA中的VPN分成三段,根据TLBT和TLBI,在TLB中寻找对应的PPN,如果没有找到,即为缺页,就需要到页表中去找。
接着将VPN分成更多段(4段),CR3是对应的L1PT的物理地址,然后一步步递进往下寻址,越往下一层每个条目对应的区域越小,寻址越细致,在经过4层寻址之后找到相应的PPN然后和VPO拼接起来得到PA。
7.5 三级Cache支持下的物理内存访问
得到物理地址PA之后,根据cache大小组数的要求,将PA拆分成CT(标记)、CI(索引)、CO(偏移量),用CI位进行索引,如果匹配成功且valid值为1,则为命中,根据偏移量在L1cache中取数;如果未命中就去二级和三级cache中重复以上步骤,命中后返回结果。
7.6 hello进程fork时的内存映射
mm_struct(内存描述符):描述了一个进程的整个虚拟内存空间。
vm_area_struct(区域结构描述符):描述了进程的虚拟内存空间的一个区间。
在用fork创建虚拟内存的时候,要经历以下步骤:
创建当前进程的mm_struct,vm_area_struct和页表的原样副本。
两个进程的每个页面都标记为只读页面。
两个进程的每个vm_area_struct都标记为私有,这样就只能在写入时复制。
7.7 hello进程execve时的内存映射
execve 函数的函数声明为int execve(char *filename ,char *argv[], char *envp[]);
execve 函数在当前进程中加载并运行filename程序,用其有效地替代了当前程序。而这需要以下几个步骤:
删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。
映射共享区域。如果filename程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
当再次调度这个进程时,它将从入口点开始执行。Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
先判断这个缺页的虚拟地址是否合法,那么遍历所有的合法区域结构,如果这个虚拟地址对所有的区域结构都无法匹配,那么就返回一个段错误(segment fault)。接着查看这个地址的权限,判断一下进程是否有读写改这个地址的权限。若都不是,就是正常缺页,则选择一个页面牺牲然后换入新的页面并更新到页表。
7.9动态存储分配管理
基本方法:维护一个虚拟内存区域"堆",分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的,需要时选择一个合适的内存块进行分配。
1.记录空闲块,可以选择隐式空闲链表,显示空闲链表,分离的空闲链表和按块大小排序建立平衡树
2.放置策略,可以选择首次适配,下一次适配,最佳适配。
3.合并策略,可以选择立即合并,延迟合并。
4.需要考虑分割空闲块的时机,对内部碎片的忍耐阈值。
7.10本章小结
本章介绍了hello 的存储器地址空间、 intel 的段式管理、 hello 的页式管理,在指定环境下介绍了 VA 到 PA 的变换,以及进程fork和execve内存映射的内容,还有缺页问题和动态存储分配管理的问题。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
文件的类型:
1.普通文件(regular file):包含任意数据的文件。
2.目录(directory)(文件夹):包含一组链接的文件,每个链接都将一个文件名映射到一个文件。
3.套接字(socket):用来与另一个进程进行跨网络通信的文件
4.命名通道
5.符号链接
6.字符和块设备
设备管理:unix io接口
1.打开和关闭文件
2.读取和写入文件
3.改变当前文件的位置
8.2 简述Unix IO接口及其函数
打开文件:open()函数
打开一个已经存在的文件,若文件不存在则创建一个新的文件。
关闭文件:close()函数
通知内核关闭这个文件,内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中。
读取文件:read()函数
从当前文件位置复制字节到内存位置,如果返回值<0则说明出现错误。
写入文件:write()函数
从内存复制字节到当前文件位置,如果返回值<0则说明出现错误。
改变文件位置:lseek()函数
文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。
8.3 printf的实现分析
printf函数的函数内容:
在形参列表里有这么一个符号“...”这个是可变形参的一种写法。当传递参数的个数不确定时,就可以用这种方式来表示。
第5行va_list arg = (va_list)((char*)(&fmt) + 4); va_list表示一个字符指针。 C语言中,参数压栈的方向是从右往左。也就是说,当调用printf函数的适合,先是最右边的参数入栈。fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素。fmt也是个变量,它的位置,是在栈上分配的,它也有地址。对于一个char *类型的变量,它入栈的是指针,而不是这个char *型变量。所以在栈里他们就有占同样的空间大小(因为存放的是地址),地址占4字节,所以 (char*)(&fmt) + 4) 表示的是...中的第一个参数。
然后下一句 i = vsprintf(buf, fmt, arg);
vsprintf的实现:
首先不关注具体内容。仅关注pritnf要做什么,它接受一个格式化的命令,并把指定的匹配的参数格式化输出。而i = vsprintf(buf, fmt, arg);vsprintf返回的是一个长度,返回的是要打印出来的字符串的长度,而printf中后面的一句:write(buf, i);能推出即把buf中的i个元素的值写到终端。
write函数的作用便是将字符数组buf中的i个元素打印到终端。而这个步骤与硬件有关,这就需要限制程序执行的权限。而write中的最后一行int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。
call save,是为了保存中断前进程的状态。其他的实现太过复杂,简单来说它的功能就是显示格式化了的字符串。以上就是printf的底层实现。
然后会调用字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。最后显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
显示芯片按照刷新频率逐行读取vram,并通过
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.4 getchar的实现分析
getchar() 用于读取用户从键盘输入的单个字符,它有一个整型的返回值,当发生读取错误的时候,返回整型值-1,当读取正确的时候,它会返回用户从键盘输的第一个字符的ASCII码,当程序调用getchar时.运行程序时就等着用户从按键输入,用户输入的字符被存放在键盘缓冲区中,直到用户按回车为止(回车字符也放在缓冲区中),当用户键入回车之后,getchar才开始从输入流中每次读入一个字符,输入的字符不只一个的时候,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完之后,才等待用户按键,getchar函数输入数字也按字符处理,单个的getchar函数输入多于一个字符时,只接收第一个字符。
再底层一点来看,这里其实是一个异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。然后getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章简述了Linux的I/O设备管理方法和Unix接口及相应函数,然后简单的分析了printf和getchar的函数实现。
结论
hello经历过程:
预处理:将hello.c调用的所有外部的库拓展到hello.i文件中;
编译:将hello.i编译得到汇编代码文件hello.s;
汇编:将hello.s汇编成为二进制可重定位目标文件hello.o;
链接:将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello;
运行:shell中运行hello;
创建子进程:shell进程调用fork函数创建子进程;
运行程序:shell调用execve函数,execve调用启动加载器,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数;
执行指令:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流;
访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址;
动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存;
信号:如果运行途中键入Ctrl-C、Ctrl-Z则调用shell的信号处理函数分别停止、挂起;
结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。
感想:
要成为优秀的程序员或工程师,应该更加了解程序的底层实现,了解程序是如何从一个.c文件到可执行文件以及文件如何在计算机中运行的,只有了解学习了这些,我们才能更好的优化我们的程序,成为更好的工程师。
附件
Hello.i:预处理后的文件
Hello.o:汇编之后的可重定位文件
Hello.s:编译之后的汇编文件
Hello_disass.s:hello的反汇编代码
Helloelf.txt:hello.o的elf格式
Elf_hello.txt:hello的elf格式
参考文献
[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.
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021112557
班 级 21w0312
学 生 赵晨希
指 导 教 师 史先俊