计算机系统大作业:Hello's P2P
计算机系统大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
指 导 教 师 史先俊
计算机科学与技术学院
2019年12月
摘 要
本文介绍了一个.c源程序在计算机系统中从编译到运行到终止的全过程,并以此为依托,概述了计算机系统的一些重要机制,包含了计算机科学的一些经典思想。本文详细介绍了从.c源文件生成可执行文件的预处理、编译、汇编、链接四个阶段,同时介绍了计算机系统的两个及其重要的抽象机制:进程和虚拟地址空间,并简要的概述了I/O管理。
关键词:计算机系统、编译、进程、虚拟地址空间
目 录
第1章 概述… - 4 -
1.1 Hello简介… - 4 -
1.2 环境与工具… - 4 -
1.3 中间结果… - 4 -
1.4 本章小结… - 4 -
第2章 预处理… - 5 -
2.1 预处理的概念与作用… - 5 -
2.2在Ubuntu下预处理的命令… - 5 -
2.3 Hello的预处理结果解析… - 5 -
2.4 本章小结… - 5 -
第3章 编译… - 6 -
3.1 编译的概念与作用… - 6 -
3.2 在Ubuntu下编译的命令… - 6 -
3.3 Hello的编译结果解析… - 6 -
3.4 本章小结… - 6 -
第4章 汇编… - 7 -
4.1 汇编的概念与作用… - 7 -
4.2 在Ubuntu下汇编的命令… - 7 -
4.3 可重定位目标elf格式… - 7 -
4.4 Hello.o的结果解析… - 7 -
4.5 本章小结… - 7 -
第5章 链接… - 8 -
5.1 链接的概念与作用… - 8 -
5.2 在Ubuntu下链接的命令… - 8 -
5.3 可执行目标文件hello的格式… - 8 -
5.4 hello的虚拟地址空间… - 8 -
5.5 链接的重定位过程分析… - 8 -
5.6 hello的执行流程… - 8 -
5.7 Hello的动态链接分析… - 8 -
5.8 本章小结… - 9 -
第6章 hello进程管理… - 10 -
6.1 进程的概念与作用… - 10 -
6.2 简述壳Shell-bash的作用与处理流程… - 10 -
6.3 Hello的fork进程创建过程… - 10 -
6.4 Hello的execve过程… - 10 -
6.5 Hello的进程执行… - 10 -
6.6 hello的异常与信号处理… - 10 -
6.7本章小结… - 10 -
第7章 hello的存储管理… - 11 -
7.1 hello的存储器地址空间… - 11 -
7.2 Intel逻辑地址到线性地址的变换-段式管理… - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理… - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换… - 11 -
7.5 三级Cache支持下的物理内存访问… - 11 -
7.6 hello进程fork时的内存映射… - 11 -
7.7 hello进程execve时的内存映射… - 11 -
7.8 缺页故障与缺页中断处理… - 11 -
7.9动态存储分配管理… - 11 -
7.10本章小结… - 12 -
第8章 hello的IO管理… - 13 -
8.1 Linux的IO设备管理方法… - 13 -
8.2 简述Unix
IO接口及其函数… - 13 -
8.3 printf的实现分析… - 13 -
8.4 getchar的实现分析… - 13 -
8.5本章小结… - 13 -
结论… - 14 -
附件… - 15 -
参考文献… - 16 -
第1章 概述
1.1 Hello简介
打开gedit,在键盘上一个个字符的输入,存成hello.c源文件。
源程序hello.c经过预处理、编译、汇编、链接四个阶段生成可执行目标文件hello。
在shell里输入命令行:./hello
1180910123 禹棋赢,shell解析命令行,用fork创建进程,用execve在新进程中加载和运行hello,用mmap映射虚拟页面。运行时,CPU分给它时间片,执行它的指令,运行时涉及I/O管理和信号处理。
当程序运行结束后,shell调用waitpid回收僵死的进程,内核删除hello相关的数据结构,自此再无hello的存在。
1.2 环境与工具
1.2.1 硬件环境
X64 CPU; 2.80GHz;
8G RAM;1T +128G HD Disk;
1.2.2 软件环境
Windows10 64位;VMware 15 ;
Ubuntu 19.04 64位
1.2.3 开发工具
Visual studio
2019 64位;Codeblocks 64位;gedit/gcc/ld/readelf/edb
1.3 中间结果
hello.i
预处理之后的文本文件
hello.s
编译之后的汇编文件
hello.o
汇编之后的二进制可重定位目标文件
hello
链接后的可执行目标文件
hellooobj
hello.o的反汇编文件
helloobj
hello的反汇编文件
hellooelf
hello.o的elf相关信息
helloelf
hello的elf相关信息
1.4 本章小结
本章简介了hello’s P2P、020的全过程,介绍了完成这次大作业的环境和工具,列出了产生的中间结果。
第2章 预处理
2.1 预处理的概念与作用
预处理阶段,预处理器根据以字符#开头的命令,修改原始的C程序。预处理器扩展源代码,插入所有用#include命令指定的文件,扩展所有用#define声明的宏。
图2.1 头文件
例如hello.c源文件的这三行,以#include开头,预处理器会读取这三个系统头文件的内容,并把它直接插入程序文本。
作用:提高编程效率。
2.2在Ubuntu下预处理的命令
命令行为:gcc -E -m64 -no-pie -fno-PIC hello.c -o hello.i
图2.2.1 预处理命令
预处理后,由hello.c文件生成hello.i文件。
图2.2.2 生成hello.i文件
2.3 Hello的预处理结果解析
图2.3.1 hello.i文件(部分)
图2.3.2 .c源文件
由图2.3.1和图2.3.2可见,预处理后,hello.i文件有三千多行,而源文件只有二十行左右。
图2.3.3 .i文件(1)
这些来描述头文件的位置。
图2.3.4 .i文件(2)
预处理后的文件还有很多typedef。
图2.3.5 .i文件(3)
预处理后的文件有很多extern,跟随着一些可能用到的函数名。
2.4 本章小结
预处理会让我们的源程序多很多内容!
本章介绍了预处理阶段的概念以及作用,linux下预处理的命令行,分析了预处理生成的.i文件。
第3章 编译
3.1 编译的概念与作用
编译阶段:编译器将.i文件翻译成汇编代码文件,即.s文件。.s文件中包含一个汇编语言程序。
作用:汇编语言为不同的高级语言不同的编译器提供了通用的输出语言,不同的高级语言编译后可以生成一样的汇编语言。
3.2 在Ubuntu下编译的命令
命令行为:gcc -S -m64 -no-pie -fno-PIC hello.i -o hello.s
图3.2.1 编译命令行
编译后,由hello.i文件生成hello.s文件。
图3.2.2 生成.s文件
3.3 Hello的编译结果解析
3.3.1 数据类型
常量:数值型常量以立即数的形式。
图3.3.1.1 常量4的汇编形式
如图所示,C语言语句为argc和常量4作比较,在汇编语言中,常量4是以立即数的形式出现的,即$4,其他的数值型常量同理。
还有一种常量存放在只读数据区,例如printf函数的格式串
图3.3.1.2 只读数据区的常量
局部变量:在栈中分配空间。
图3.3.1.2 分配局部变量的C语言指令及对应的汇编指令
如图所示,分别是分配局部变量的C语言指令和汇编指令。可见变量i的地址是%rbp-4,由此可知局部变量被分配在栈中。
3.3.2 算术操作
共有如下常用的算术操作:
指令
效果
leaq S, D
D = &S
INC D
DEC D
NEG D
NOT D
D = D + 1
D = D – 1
D = -D
D = ~D
ADD S, D
SUB S, D
IMUL S, D
XOR S, D
OR S, D
AND S, D
D = D + S
D = D – S
D = D * S
D = D ^ S
D = D | S
D = D & S
SAL k, D
SHL k, D
SAR k, D
SHR k, D
D = D <<
k
D = D <<
k
D = D >>
k(算术)
D = D >>
k(逻辑)
程序中只涉及了加法操作ADD:汇编语言指令为add 操作数1, 操作数2。将操作数1和操作数2相加,并将结果放在操作数2原来的位置。
图3.3.2 加法操作
这里的加法是每次循环后变量i自己加一的操作,汇编语言为add语句,操作数1为立即数1。
3.3.3 条件分支
条件分支if通过比较指令和跳转指令的组合来实现。
比较指令:CMP S1,S2 比较S2与S1的大小,并设置相关条件码。跳转指令有如下:
指令
描述
jmp
跳转
je、jz
jne、jnz
相等时跳转
不相等时跳转
js
jns
负数时跳转
非负数时跳转
jg、jnle
jge、jnl
jl、jnge
jle、jng
大于时跳转(有符号数)
大于等于时跳转
小于时跳转
小于等于时跳转
ja、jnbe
jae、jnb
jb、jnae
jbe、jna
超过时跳转(无符号数)
超过或等于时跳转
低于时跳转
低于或等于时跳转
图3.3.3 条件分支
如图,C语言源代码是一个if条件分支,若argc不等于4时,执行if条件中的内容。编译成汇编语言后,cmp语句将argc(存放在%rbp-0x20处)与4比较,等于4时,跳转到标号为.L2的地方执行if分支之后的语句,若不等于4不跳转。
3.3.4 循环
程序中涉及了for循环,for循环汇编代码的通用形式如下:
loop:
body-statement;(for循环内部的内容)update-expr;(更新变量)if (条件成立)go to loop;(循环)
done;
图3.3.4 for循环的C语言和汇编语言
汇编语言中,使用cmp和条件跳转语句结合来判断条件是否成立、循环是否继续,成立则跳转到.L4执行循环体内的内容,不成立则跳出循环体继续执行之后的内容。
3.3.5 过程、函数调用
函数的前两个命令总是,将旧的rbp值压栈,将新的rbp指向现在函数的栈底。当需要局部变量或者需要栈空间时,通过减少栈指针rsp来分配空间:。
调用函数使用call语句,分为两步:1、返回地址压栈;2、跳转到被调用函数执行。
参数传递:前6个参数分别通过寄存器%rdi、%rsi、%rdx、%rcx、%r8、%r9,超过6个的参数通过栈传递。
图3.3.5.1 参数传递
这是调用printf和exit函数的参数传递,可见是使用call语句进行函数调用,第一个参数都是通过寄存器edi传递,这里还有一个有趣的地方,由于printf函数要打印的是一个字符串,编译器进行优化这里直接调用puts函数。
函数返回:函数返回值放在寄存器rax中,返回时使用ret语句,分为两步:1、弹出返回地址;2、跳转到返回地址处的指令执行。
图3.3.5.2 函数返回值
sleep函数以atoi函数的返回值作为参数,传参给sleep时,是将寄存器eax的值传递给edi,即返回值存放在寄存器rax中,传参时第一个参数传给寄存器edi。
3.3.6 数组
汇编语言会这样访问数组:mov (rb, ri, s), ra,其中s是比例因子,一般是数组元素的大小;rb是数组基址,即首元素的地址;ri是偏移,即元素的索引。
图3.3.6 访问数组
我们的程序中访问数组的方式略有不同,因为这个数组是作为参数传递的,传递的argv相当于是数组的首地址,数组的元素是指针,大小为8。由于数组元素大小为8,汇编语言第二行的add指令将rax加16就相当于让rax是数组的2号元素的首地址,由于此时rax已是2号元素的地址,传递时直接(%rax)即可访问数组的2号元素。访问argv[1]同理。
3.4 本章小结
本章介绍了编译阶段的作用,以及编译的命令行。对我们的源程序hello.c编译后得到的汇编语言程序中的各种数据、操作做了详细的解析。
第4章 汇编
4.1 汇编的概念与作用
汇编阶段:汇编器将hello.s文件转化成二进制目标代码文件hello.o,即可重定位目标文件。二进制目标代码是机器代码的一种形式,它包含所有指令的二进制形式,即机器语言指令,但还未填入全局值的地址。
作用:将汇编语言转换成机器可以理解的机器语言指令。
4.2 在Ubuntu下汇编的命令
命令行为:gcc -c -m64 -no-pie -fno-PIC
hello.s -o hello.o
图4.2.1 汇编命令行
汇编后,由hello.s文件生成hello.o文件
图4.2.2 汇编生成hello.o文件
4.3 可重定位目标elf格式
ELF可重定位目标文件的格式如下:
ELF头
.text
.rodata
.data
.bss
.symtab
.rel.text
.rel.data
.debug
.line
.strtab
节头部表
使用readelf -a指令查看elf文件所有信息。
1、ELF头
包括生成该文件的系统的字大小、字节顺序、补码表示;还有目标文件类型、机器类型、节头部表的偏移、节头部表中条目的大小和数量等。
图4.3.1 ELF头
2、节头部表
节头部表描述了不同的节的位置和大小,每个节在节头部表中都有一个固定大小的条目。
图4.3.2 节头部表
3、重定位信息
这是.rel.text节,包含代码的重定位条目。每个重定位条目有如下的格式:offset是需要被修改的引用的节偏移、sym表示被修改引用应该指向的符号、addend是一个有符号常数,有时需要它对被修改引用的值做偏移调整、type告知链接器如何修改引用,是相对地址引用还是绝对地址引用。
图4.3.3 重定位节
4、符号表
存放在程序中定义和引用的函数和全局变量的信息。
图4.3.4 符号表
4.4 Hello.o的结果解析
1、机器语言
机器语言是二进制格式的,图中红色圈出来以及下面的16进制值都是以字节形式表示的机器语言。x86-64的机器指令的长度从1到15个字节不等,每条汇编语言都对应唯一的机器指令,从某个给定的位置开始,机器指令也可以被唯一地解码成汇编指令。
图4.4.1 机器语言的构成
2、分支跳转
在汇编语言中,跳转位置是使用的如.L3之类的标记,而机器语言反汇编后,跳转位置则是一个确定的地址。
图4.4.2 分支跳转对比
3、函数调用
在汇编代码中,函数调用call后面直接跟着函数名,而在机器代码反汇编的结果中,函数调用的地址是下一条机器指令的地址,机器代码中的地址全是零,由于程序中调用的函数都是共享库中的函数,故在加载时才能确定函数的地址。
图4.4.3 函数调用对比
4、对全局的只读代码段的引用
汇编代码中对.rodata节的引用类似于分支跳转,使用一个符号,如.LC0;机器代码中,机器指令是全0,并附上一个重定位条目,等待链接时确定具体地址。
图4.4.4 引用全局数据对比
4.5 本章小结
本章简述了汇编的概念,介绍了汇编的命令行,分析了ELF可重定位目标文件的格式,解析了对汇编后的文件hello.o进行反汇编之后的内容,并与hello.s文件进行了对比。
第5章 链接
5.1 链接的概念与作用
链接阶段:链接器将可重定位目标代码文件和一些库函数的代码合并,产生最终的可执行目标文件hello,其可以直接被复制到内存执行。链接可以执行于编译时,也可以执行于加载时,也可以在运行时由应用程序来执行。
作用:将各种代码和数据片段组合成一个单一文件,这个文件可以被加载到内存执行,使得分离编译成为可能,可以将大型的应用程序分解成小的模块。
5.2 在Ubuntu下链接的命令
ld的链接命令行如下:
图5.2 链接的命令行
5.3 可执行目标文件hello的格式
用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
hello的ELF格式如下:相比于可重定位文件的ELF格式多了段头部表(描述了可执行文件中的片到连续的内存段的映射关系)、.init节,少了用于重定位的rel节。
ELF头
段头部表
.init
.text
.rodata
.data
.bss
.symtab
.debug
.line
.strtab
节头部表
段头部表列出了各段的基本信息,offset是相应节的文件偏移,virtAddr是虚拟地址,physAddr是物理地址,align是对齐要求,filesz是在文件中的大小,memsz是内存中段的大小,flags是权限。
图5.3.1 节头部表
图5.3.2 段头部表
5.4 hello的虚拟地址空间
从段头部表可见,程序在虚拟地址空间的起始地址是0x400000,用edb查看验证:
图5.4.1 程序起始
由段头部表,有一个段的权限是RE,就是只读、可执行,可知此段是代码段,edb查看此段内容:
图5.4.2 代码段
段头部表中有一type为load并且权限是可读写的数据段,用edb查看此段内容:
图5.4.3 数据段
5.5 链接的重定位过程分析
1、地址不同,hello.o反汇编文件中函数的起始地址是0,hello反汇编文件中的地址则是一个具体的虚拟地址;
2、在hello的反汇编文件中,一些调用,例如函数调用,被填上了具体的偏移或是地址,在hello.o的反汇编文件中则全是0,等待重定位;
3、hello的反汇编文件中多了nop指令,这是为了让代码满足对齐要求,方便放置下一个代码块。
链接过程:
1、符号解析:将每个符号引用正好和一个符号定义关联起来;
2、重定位:所有相同类型的节合并成同一类型的新的聚合节,链接器将运行时内存地址付赋给每个节、每个符号,此时程序中每条指令、每个符号都有唯一的运行时内存地址了;再依据重定位条目,修改对每个符号的引用。
重定位:此重定位条目会告诉链接器修改偏移量为0x1b处的相对引用。此时链接器已知main的地址为0x4010c1,puts函数的地址为0x401030,则引用处地址为0x4010c1+0x1b=0x4010dc,相对偏移为:puts的地址+addend-引用处地址,即0x401030-0x4-0x4010dc=-0xb0,转换成补码表示就是ff ff ff 50,小端模式填入即可。绝对寻址极其简单,直接填入地址即可。
图5.5 重定位相关
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
加载hello到_start之间调用_dl_start和_dl_init函数;main函数之前会调用_init函数初始化代码;main调用一些库函数如puts;程序终止调用exit函数。
加载hello:_dl_start、_dl_init;
main函数之前:_start、__libc_start_main、__cxa_aexit、_init、_setjmp、_sigsetjmp;
main函数:main、puts、exit、printf、sleep、getchar;
main函数之后:exit。
5.7 Hello的动态链接分析
动态链接会把库函数的文本和代码加载进来,hello程序是在加载时进行动态链接。在加载,_dl_init之前,文件中并没有库函数的代码、数据。_dl_init之后,库函数的代码、数据被加载进文件,用edb即可找到库函数printf的代码
图5.7 加载前后文件的区别
5.8 本章小结
经过本章,一个可执行目标文件hello终于诞生了!
本章介绍链接阶段,介绍了链接的概念、命令行,介绍了可执行目标文件的ELF格式,分析了hello的虚拟地址空间、各段的地址,详细介绍了链接的重定位过程,分析了hello的从加载到终止的流程,分析了动态链接前后哪些项目发生了变化。
第6章 hello进程管理
6.1 进程的概念与作用
进程的基本定义就是一个执行中程序的实例。系统中每个程序都运行在某个进程的上下文中。上下文由程序正确运行所需的状态组成,这个状态包括放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量及打开文件描述符的集合。
作用:提供了两个关键的抽象:1、独立的逻辑控制流,好像程序在独占地使用处理器;2、私有的地址空间,好像程序独占的使用内存。
6.2 简述壳Shell-bash的作用与处理流程
shell是一个交互型的应用级程序,它代表用户运行其他程序。
shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。
处理流程:
循环:读、求值
求值步骤:1、调用parseline函数,解析以空格分隔的命令行参数,构造最终会传递给execve的argv向量。第一个参数若是一个内置的shell命令名,会立刻解释这个命令;若是一个可执行目标文件,shell会创建一个新的子进程,并在其上下文中加载运行这个文件。若最后一个参数是&符,parseline返回1,表示在后台运行此程序。否则返回0,并在前台执行此程序,且shell等待其完成。
2、调用builtin_command函数,该函数检查第一个命令行参数是否是内置的shell命令。若是,它立即解释这个命令并返回1,否则返回0。若builtin_command返回0,shell会创建一个新的子进程,并在其上下文中加载运行所请求的程序。若用户在后台运行该程序,shell返回到循环顶部,等待下一个命令行。否则,shell使用waitpid等待作业终止,当作业终止时再i返回到循环顶部
6.3 Hello的fork进程创建过程
shell解析命令行,发现第一个参数是./hello而不是内置命令,就调用fork创建子进程,并在这个进程中执行hello程序。
fork创建一个子进程,子进程与父进程除了PID几乎完全相同。fork返回两次,在父进程中返回子进程的pid,子进程中返回0;子进程与父进程有完全相同的虚拟地址空间,但是是独立的,对一个进程私有的变量做的改变不会反映到另一个进程之中;子进程与父进程并发执行;子进程继承父进程打开的文件。
6.4 Hello的execve过程
execve在当前进程的上下文中加载并运行一个新的程序。execve调用启动加载器,加载器删除子进程现有的虚拟内存段,创建一组新的代码、数据、堆栈段,新的代码、堆栈段被初始化为可执行文件的内容,堆栈段被初始化为0。最后跳转到_start地址,最终会调用main函数。除了一些头部信息,在加载过程中没有任何磁盘到内存的信息复制。
6.5 Hello的进程执行
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
当进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策叫做调度。通过上下文切换的机制来转移控制:1、保存当前进程的上下文;2、回复即将调度的进程的上下文;3、将控制转移给呗调度的进程。
例如从进程A切换到进程B,切换之前,进程A在用户模式下执行指令,在某一时刻,进程A陷入内核,若A因为等待某事件发生阻塞,内核可以在此时决定执行从A到B的上下文切换,切换之前,A在用户模式下执行指令,切换的第一部分中,内核代表进程A在内核模式下执行,在某一时刻,内核代表进程B在内核模式下执行,切换完成之后,进程B在用户模式下执行。
6.6 hello的异常与信号处理
程序正常运行如下:程序结束之前有个getchar函数需要读一次输入缓冲区。
图6.6.1 正常运行截图
运行时按ctrl+c,内核会发送SIGINT信号,终止进程。
图6.6.2 运行时按ctrl+c
运行时按ctrl+z,内核会发送SIGSTOP让进程停止,此时使用ps可看到进程hello仍然存在,用jobs可见此作业的状态,fg命令可让停止的进程hello再次到前台运行。
图6.6.3 运行时按ctrl+z
程序运行时夹杂着空格乱按,由下图可见,程序运行的过程中乱按就是输入到输入缓冲区,第一次回车之前的内容会让getchar读走,之后所有的输入都留在输入缓冲区里,之后如果再有回车的话就会当成是之后的shell命令行了。
图6.6.4 夹杂着空格乱按
6.7本章小结
本章介绍了进程的概念、作用,介绍了shell的处理流程,介绍了fork、execve执行的过程,介绍了上下文切换的相关内容,以及异常与信号处理的相关内容。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址是段地址:段偏移;线性地址是段地址+段偏移,线性地址空间是非负整数地址的有序集合;虚拟地址空间是N=2n个非负整数虚拟地址的有序集合;物理地址空间是M=2m个物理地址的有序集合。
汇编语言程序hello.s中的地址就是逻辑地址,hello.o文件反汇编出来的地址就是线性地址,hello文件反汇编出来的地址是虚拟地址,访问主存时,使用的是物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段寄存器(16位),用于存放段选择符:CS(代码段):程序代码所在段;SS(栈段):栈区所在段;DS(数据段):全局静态数据区所在段;其他3个段寄存器ES、GS和FS可指向任意数据段。
段选择符各字段含义:
图7.2.1 段选择符
TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)
RPL=00,为第0级,位于最高级的内核态,RPL=11,为第3级,位于最低级的用户态,第0级高于第3级,高13位-8K个索引用来确定当前使用的段描述符在描述符表中的位置。
段描述符的定义(IA32):
图7.2.2 段描述符
B31~B0: 32位基地址; L19~L0:20位限界,表示段中最大页号
G:粒度。G=1以页(4KB)为单位;G=0以字节为单位。因为界限为20位,故当G=0时最大的段为1MB;当G=1时,最大段为4KB×220 =4GB
D:D=1表示段内偏移量为32位宽,D=0表示段内偏移量为16位宽
P:P=1表示存在,P=0表示不存在。Linux总把P置1,不会以段为单位淘汰
DPL:访问段时对当前特权级的最低等级要求。因此,只有CPL为0(内核态)时才可访问DPL为0的段,任何进程都可访问DPL为3的段(0最高、3最低)
S:S=0系统控制描述符,S=1普通的代码段或数据段描述符
TYPE:段的访问权限或系统控制描述符类型
A:A=1已被访问过,A=0未被访问过。(通常A包含在TYPE字段中)。
逻辑地址到线性地址的转换:
被选中的段描述符先被送至描述符cache,每次从描述符cache中取32位段基址,与32位段内偏移量(有效地址)相加得到线性地址,即由逻辑地址转换为线性地址是基址+偏移的形式。
图7.2.3 逻辑地址到线性地址的转换
段寄存器无法放下32位段基址,所以它们被称作选择符,用于引用段描述符表中的表项来获得描述符。
所有的段描述符被保存在两个表中:全局描述符表GDT和局部描述符表LDT。gdtr寄存器指向GDT表基址。
通过解析段寄存器中的段选择符在段描述符表中根据Index选择目标描述符条目,从目标描述符中提取出目标段的基地址,最后加上偏移量共同构成线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
将各进程的虚拟空间划分成若干个长度相等的页,页式管理把内存空间按页的大小划分成片或者页面,然后建立把页式虚拟地址与内存地址一一对应的页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
地址变换的步骤如下:
1、将线性地址传给MMU,MMU根据虚拟页号计算页表项地址;
2、MMU请求得到页表项,得到页表项中的物理页号;
3、将物理页号和线性地址中的页偏移串联得到物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
1、使用VPN在TLB中查找页表项,若TLB命中,取出页表项中的PPN,再把PPN和VPO串联起来得到物理地址;
2、若TLB不命中,使用四级页表查找页表项,CR3寄存器中存放一级页表的物理地址,VPN被分成四块,VPN1是在一级页表的偏移,这个一级页表中的PTE包含一个二级页表的基址,VPN2是在二级页表的偏移,以此类推,最终得到一个PTE,若PTE有效位为1,则取出PPN,和VPO串联得到物理地址;若PTE有效位为0,则调用缺页异常处理子程序。
7.5 三级Cache支持下的物理内存访问
首先,将虚拟地址的12位VPO部分拆成6位的组索引和6位的块偏移。MMU查找PTE时,L1cache利用组索引查找相应的组,并且读出组中的8个标记和相应的数据,等到MMU找到了PPN,cache把PPN和8个标记进行比较匹配,若在L1cache中不命中,将物理地址传下去查找L2cache和L3cache。
7.6 hello进程fork时的内存映射
内核为新进程创建各种数据结构,并分配给它一个唯一的PID,给新进程创建当前进程的mm_struct、区域结构和页表的原样副本,将两个进程中的每个页面标记为只读,并将每个区域结构都标记为私有的写时复制。fork返回时,新进程的虚拟内存正好和原进程虚拟内存相同,当这两个进程的任一个后来进行写操作时,写时复制会创建新页面,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
1、删除已存在的用户区域、区域结构;
2、映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域都是私有的,写时复制的。代码和数据区域被映射为文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件。堆和栈请求二进制0,初始长度为0;
3、映射共享区域;
4、设置PC,设置上下文中的PC,使之指向代码区域入口点。
7.8 缺页故障与缺页中断处理
缺页故障:DRAM不命中称为缺页,当MMU请求到页表项PTE后,发现有效位为0,即DRAM未缓存此页,则MMU会触发缺页故障,这是由按需页面调度的策略决定的。
缺页中断处理:
1、虚拟地址A合法吗?缺页处理子程序搜索区域结构的链表,将A和每一个vm_start和vm_end比较,若不合法则触发段错误;
2、进行的内存访问合法吗?即判断进程是否具有相应权限,依据区域结构里的vm_prot,不合法则触发保护异常;
3、判断完成后,若都合法则在物理内存选择一个牺牲页面,若这个页面被修改过,则将它交换出去,换入新的页面并更新页表,然后缺页异常处理子程序返回,CPU重新执行引起缺页的指令。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。例如C的malloc包。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集,隐式分配器也叫垃圾收集器。
隐式空闲链表:每个块由大小一个字的头部、有效载荷和填充组成。块头部记录块大小,低位可以记录块是已分配还是空闲,填充用来满足对齐要求等。由于合并前一个块的需要,可以在空闲块加上一个脚部,内容和头部一模一样。
放置已分配块的策略:
首次适配:从头开始搜索链表,选择第一个合适的空闲块;
下一次适配:每次从上一次查询结束的位置开始搜索;
最佳适配:搜索每个空闲块,选择适合所需请求大小的最小空闲块。
显示空闲链表:将空闲块组织成链表,每个空闲块中包含一个前驱和后继指针,用来将空闲块连起来,这样可以把分配一个块的时间从总块数地线性时间降到空闲块块数的线性时间,但提高了块大小,潜在的提高了内部碎片的程度。
垃圾收集:将内存视为一张有向可达图,节点被分成一组根节点和一组堆节点。根节点对应不在队中的位置,它们中包含指向堆中的指针,若堆中某节点p是不可达的,则p就是垃圾,需要被回收。
7.10本章小结
本章主要介绍了hello的存储器地址空间、段式管理及页式管理,即逻辑、线性、物理地址的转换,介绍了页表的缓存——TLB、多级页表,并详细的描述了TLB与四级页表支持下VA到PA的转换,介绍了物理内存访问,还介绍了虚拟内存机制下fork、execve的实质,介绍缺页故障与缺页中断处理的具体行为,最后概述了动态存储分配管理。
虚拟地址机制,让计算机世界变得井井有条,复杂而又有趣!
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
所有的I/O设备都被模型化未文件,而所有的输入和输出都被当作对相应文件的读和写来执行。
设备管理:Unix I/O接口
这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix
IO接口及其函数
Unix I/O接口统一操作:
打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
Linux shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量.应用程序能够通过执行seek,显式地将改变当前文件位置k。
读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的而文件,当k>=m时,触发一个EOF条件。类似地,写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
关闭文件,当应用完成了对文件的访问后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
Unix I/O函数:
打开和关闭文件:
int
open(char* filename, int flags, mode_t mode) ,进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位;
int close(int
fd),fd是需要关闭的文件的描述符,close返回操作结果;
读写文件:
ssize_t
read(int fd, void *buf, size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量;
ssize_t
wirte(int fd, const void *buf, size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置;
改变当前文件位置:lseek()。
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;
}
首先arg获得第二个不定长参数,即输出的时候格式化串对应的值。
再看i = vsprintf(buf, fmt, arg),vsprintf的代码:
int
vsprintf(char *buf, const char *fmt, va_list args)
{
char* p;char tmp[256];va_list p_next_arg = args;for (p = buf; *fmt; fmt++){if (*fmt != '%') //忽略无关字符{*p++ = *fmt;continue;}fmt++;switch (*fmt){case 'x': //只处理%x一种情况itoa(tmp, *((int*)p_next_arg));
//将输入参数值转化为字符串保存在tmp
strcpy(p, tmp); //将tmp字符串复制到p处p_next_arg += 4; //下一个参数值地址p += strlen(tmp); //放下一个参数值的地址break;case 's':break;default:break;}}return (p - buf); //返回最后生成的字符串的长度
}
vsprintf返回的是要打印出来的字符串的长度,其实看看printf中后面的一句:write(buf, i); write:写操作,把buf中的i个元素的值写到终端。
所以说:vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
在printf中调用系统函数write(buf,i)将长度为i的buf输出。write函数如下:
write:
mov eax, _NR_write mov ebx, [esp + 4]mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall,查看syscall的实现:
sys_call:
call save push dword [p_proc_ready]stipush ecxpush ebxcall [sys_call_table + eax * 4]add esp, 4 * 3mov [esi + EAXREG - P_STACKBASE], eaxcliret
syscall将字符串中的字节“Hello 1180910123禹棋赢”从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。
显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
经过一系列软硬件的配合,最终我们想要用printf打印的字符串“Hello
1180910123禹棋赢”就显示在了屏幕上!
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。
getchar调用系统调用函数read,通过系统调用读取按键的ASCII码,当接受到回车键时,返回整个缓冲区的字符串,然后getchar函数在其中读取一个字符的内容。
8.5本章小结
本章主要介绍了Linux的I/O设备管理方法,简述了Unix I/O接口及其函数,分析了printf函数和getchar函数的具体实现,分析了printf函数的源代码。
结论
1、编写源文件,编程,通过键盘输入,创建hello.c;
2、预处理阶段,预处理器扩展源代码,插入所有用#include命令指定的文件,扩展所有用#define声明的宏。将hello.c调用的所有外部的库展开合并到一个hello.i文件中。
3、编译阶段,将hello.i编译成为汇编文件hello.s;
4、汇编阶段,将hello.s汇编成为二进制可重定位目标文件hello.o;
5、链接阶段,将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello;
6、运行程序:在shell中输入./hello 1180910123 禹棋赢,shell解析命令行,然后调用fork和execve函数创建子进程并在子进程中加载运行hello程序;
7、信号:如果运行途中键入ctr-c ctr-z则调用shell的信号处理函数分别停止、挂起;
8、执行指令:CPU为其分配时间片,进程、虚拟空间给hello提供两个假象:好像在独占的使用处理器,好像在独占的使用内存;
9、动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存;
10、圆满的谢幕:运行结束,shell回收子进程,操作系统内核删除相关的数据结构。
自此,一个hello程序落幕,一本700多页的CSAPP也缓缓谢幕,computer system和我都知道,hello,你曾经来过!
附件
hello.i
预处理之后的文本文件
hello.s
编译之后的汇编文件
hello.o
汇编之后的二进制可重定位目标文件
hello
链接后的可执行目标文件
helloo.obj
hello.o的反汇编文件
hello.obj
hello的反汇编文件
helloo.elf
hello.o的elf相关信息
hello.elf
hello的elf相关信息
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] https://www.cnblogs.com/pianist/p/3315801.html
printf函数实现的深入剖析
[2] https://blog.csdn.net/Zllvincent/article/details/84033694 键盘中断的处理
[3] https://blog.csdn.net/rabbit_in_android/article/details/49976101
虚拟地址、逻辑地址、线性地址、物理地址
[4] 深入理解计算机系统.Randal
E. Bryant,David R. O’Hallaron.
计算机系统大作业:Hello's P2P相关推荐
- 2021计算机系统大作业 CSAPPHello‘s P2P
摘 要 本文基于CSAPP和计算机系统基础课程中的内容,通过hello.c这一简单程序的生命周期,讨论了计算机中的预处理.编译.汇编.链接.进程管理.存储管理.IO管理等内容. 关键词: 计算机:编译 ...
- 计算机系统大作业——程序人生P2P
- 2022计算机系统大作业——程序人生-Hello’s P2P
计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算机 学 号 120L021716 班 级 2003005 学 生 蔡泽栋 指 导 ...
- [ HIT - CSAPP ] 哈尔滨工业大学 - 计算机系统 - 期末大作业“Hello‘s P2P’”
计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算学部 学 号 120L020731 班 级 2003005 学 生 吴少杰 指 导 ...
- 哈工大计算机系统大作业——程序人生-Hello’s P2P
计算机系统 大作业 题 目程序人生-Hello's P2P 专 业 计算机科学与技术 学 号120L022401 班 级 200300 ...
- HIT CS:APP 计算机系统大作业 《程序人生-Hello’s P2P》
HIT CS:APP 计算机系统大作业 程序人生-Hello's P2P Hello的自白 我是Hello,我是每一个程序猿¤的初恋(羞羞--) l却在短短几分钟后惨遭每个菜鸟的无情抛弃(呜呜--), ...
- 计算机系统大作业 程序人生-Hello’s P2P
计算机系统大作业 我是Hello,我是每一个程序猿¤的初恋(羞羞--) 却在短短几分钟后惨遭每个菜鸟的无情抛弃(呜呜--),他们很快喜欢上sum.sort.matrix.PR.AI.IOT.BD.MI ...
- HIT计算机系统大作业-程序人生-Hello’s P2P
计算机系统大作业 ** 由于采用静态部署,需要看图片详细分析的小伙伴请移步个人博客网站:** 个人博客 题目:程序人生-Hello's P2P 学号: 姓名:熊峰 摘要: hello程序作为最简单的. ...
- 哈工大计算机系统大作业——hello P2P
计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算学部 学 号 班 级 学 生 指 导 教 师 刘宏伟 计算机科学与技术学院 2021年 ...
最新文章
- 服务器信息采集协议,服务器信息采集
- basequickadapter详解_BaseRecyclerViewAdapter(持续更新!)
- 关于计算机软件系统的知识,二、计算机软件系统基本知识
- spring定时器,定时器一次执行两次的问题
- python中pop函数的用法_python中pop()函数怎么用
- spring MVC 及 AOP 原理
- 探索软件设计模式(二)
- 从△走进OO,走进策略模式
- 【人脸识别】基于matlab GUI PCA+SVM人脸识别(准确率)【含Matlab源码 823期】
- python车辆型号识别_基于Tensorflow的车辆检测和车型识别
- Unity粒子特效系列-闪星星的宝箱
- ReviewBoard+SVN配置强制code review
- 计算机公式复制填充的操作,办公小技巧:解决Excel公式自动填充问题
- mysql根据出生日期计算年龄并查询
- JZOJ 7.10B组第一题 可见点数
- TestCenter IGMP Proxy组播测试(bridge)
- 英维克,上市只是一个新的起点
- 13. Redis底层实现 List
- 零基础怎么自学软件测试?分享五个宝藏网站,自学简直不要太轻松了
- ICLR 2018 有什么值得关注的亮点?
热门文章
- Android学习总汇
- linux改键盘映射
- poj 2051 Argus
- oracle的一些学习
- cn.cw.gps.domain.VisitReport.setVisitID([Ljava.lang.String;)]
- Codeforces Round #342 (Div. 2)
- Safe Or Unsafe(hdu2527)哈弗曼VS优先队列
- 关闭或修改 IIS 443 端口
- 一个拼凑sql,输出变量的 存储过程
- 使用VMware虚拟磁盘管理工具调整磁盘大小