计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 1190600419
班 级 1903006
学 生 王子睿
指 导 教 师 史先俊
计算机科学与技术学院
2021年5月
本文主要阐述hello程序P2P 020的全过程,介绍了从.c文件一直到可执行文件的编译过程 和对运行hello时计算机内部进程管理,存储管理和IO管理的深入分析。
关键词: 预处理;编译;汇编;链接;进程;存储;I/O
目 录
2.2在Ubuntu下预处理的命令............................................................................. - 5 -
5.3 可执行目标文件hello的格式....................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 10 -
6.3 Hello的fork进程创建过程........................................................................ - 10 -
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 -
8.2 简述Unix IO接口及其函数.......................................................................... - 13 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:From Program to Process 用户通过编辑器生成hello.c程序,然后通过编译器预处理(C预处理器)获得hello.i文件,接着通过编译器cll得到汇编语言hello.s汇编语言文件,接着用汇编器器as讲hello.s编译为课程点位的hello.o目标文件,再通过链接器ld与库函数等链接生成可执行目标文件hello。接着用户就可以执行hello运行程序,操作系统fork子进程,此时便完成了From Program to Process
020:From Zero-0 to Zero-0 操作系统调用execve再fork产生的子进程中加载hello,映射虚拟内存。为hello分配时间块执行控制流。
进程终止后操作系统回收hello进程,删除hello的数据。此时便完成了From Zero-0 to Zero-0
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件工具:、
Win CPU i7内核6 逻辑处理器12 内存8G Linux 内核4 内存2g磁盘空间20g
软件工具:Win10 64位操作系统 linux虚拟机 ubuntu 20版本
开发者与调试工具:VS2019 Codeblocks64位
gcc,gdb,edb,objdump等
1.3 中间结果
中间结果 | 文件作用 |
hello.i | 源程序经过预处理得到的文本文件 |
hello.s | 经过编译后得到的汇编文件 |
hello.o | 经过汇编后得到的可重定位目标程序 |
hello | 经过链接后得到的可执行目标程序 |
hello.elf | hello.o的ELF文件 |
hello1.elf | hello的ELF文件 |
hello.objdump | hello.o的反汇编文件 |
hello1.objdump | hello的反汇编文件 |
1.4 本章小结
本章大致介绍了P2P 020 ,对之后的中间结果和文件名给出了总结,阐述了需要的开发环境和工具。
第2章 预处理
2.1 预处理的概念与作用
预处理的概念: 以“#”号开头的预处理指令如包含#include,宏定义制定#define等,在源程序中这些指令都放在函数之外,而且一般放在源文件的前面 ,所谓预处理其实就是在编译的第一遍扫描之前的所作的工作,预处理是C语言的一个重要的功能,它由预处理程序单独完成,当对一个源文件进行编译时,系统自动引用预处理程序,预处理在 源代码编译之前对其进行的一些文本性质的操作,对源程序编译之前做一些处理,生成扩展的C源程序
预处理的作用:1:将头文件中的内容(源文件之外的文件)插入到源文件中
2:进行了宏替换的过程,定义和替换了由#define指令定义的符号
3:删除掉注释的过程,注释是不会带入到编译阶段
4:条件编译
2.2在Ubuntu下预处理的命令
预处理过程
编译命令:gcc -E hello.c -o hello.i
生成hello.i 文件
hello.i截图
2.3 Hello的预处理结果解析
可以发现hello.i文件的篇幅巨大,有3060行,比源文件hello.c增加了很多代码,而再3047行到3060行是我们的main函数,所以可以推断出前面的约3000行是展开的,对#include
#include
#include 进行了宏展开我们发现了是因为预处理工作进行了头文件的展开。
2.4 本章小结
介绍了预处理的概念与作用,发现预处理主要由预处理器完成,通过具体的hello.c文件预处理过程详细了解了预处理的命令和功能,宏展开和去掉注释。其主要功能为对原程序进行大致处理
第3章 编译
3.1 编译的概念与作用
编译的概念:编译就是 编译器(ccl)将文本hello.i翻译成文本文件hello.s,编译过程就是将预处理得到的hello.i文件翻译成ASCII汇编语言文件hello.s的过程。
编译的作用:将预处理好的高级语言程序编译成对应的汇编语言。词法分析,将源代码程序输入扫描器,将源代码的字符序列分割成一系列记号;语法分析,生成语法树;判断对错,语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
3.2 在Ubuntu下编译的命令
编译命令为 gcc -S hello.i -o hello.s 在该目录下生成 hello.s文件
3.3 Hello的编译结果解析
3.3.1 指示符和含义分析
.filet 声明源文件
.text 代码段
.globl 声明全局变量
.secetion .rodata rodata节
.align 声明数据和指令等的对齐的方式
.long 声明long类型
.string 声明string类型
.type 声明函数类型和对象类型
.size 声明字节大小
3.3.2 数据
3.3.2.1 int i
对 int i 为局部变量,汇编语言中编译器把它存储在栈中,按照hello.s文件锁给出的,将i存储在栈上空间-4(%rbp)中
3.3.2.2数组argv[] 数组argv[]为指向char的指针的数组,在汇编语言中如下将其存放.。
从其中我们能得出的信息有 argv[]的元素大小占据8个字节。要想对argv[1],argv[2]的访问,则对argv[]的首地址argv进行加8字节 16字节即可得对argv[1],argv[2]的访问。
3.3.2.3 立即数
对立即数的使用是通过$ 立即数直接进行调用即可,在hello.s中如下
3.3.2.4 int argc
argc是hello.s的形式参数,因为int main(int argc,char *argv[])所以argc是第一个参数,存储在%edi寄存器里面。
通过观察hello.s 我们可以发现下面的 movl %edi,-20(%rbp) 则说明了在该部分我们把argc参数传递到了栈中的这个位置
3.3.2.5 字符串
我们有两个字符串 "用法: Hello 1190600419 王子睿 秒数!\n"和"Hello %s %s\n",argv[1],argv[2] 该字符串在汇编语言中表示如图所示
字符串都.rodata中被声明了,并且汉字通过UTF-8格式表示。
3.3.3 赋值
3.3.3.1 i=0和 i++赋值操作
对i=0: 在汇编语言中我们可以发现,通过语句movl $0, -4(%rbp)把0赋值给局部变量 i
对i++:在汇编语言中我们可以发现,通过语句addl $1, -4(%rbp)把0赋值给局部变量 i,每一次对-4(%rbp),也就是局部变量i的位置进行+1 以达到i++的目的。
截图如下:
3.3.4条件分支
3.3.4.1 if(argc!=3)
该分支判断argc与3的关系情况,通过汇编语句cmpl $3, -20(%rbp)来进行条件判断,和je .L2进行相关的调转操作,代码截图如下,argc存储在-20(%rbp)之中
3.3.4.2 for 循环中i与8的比较
该分支判断i与8的关系情况,通过汇编语句cmpl $7, -4(%rbp)来进行条件判断,和jle .L 4 进行相关的调转操作,代码截图如下.i存储在 -4 (%rbp)之中
3.3.5算术操作
算数操作指令如下图所示
3.3.5.1 i++运算操作
通过addl $1, -4(%rbp)通过add指令对变量i加一。
3.3.6 逻辑操作
3.3.6.1 取值操作
通过汇编语句 leaq .LC0(%rip), %rdi。加载有效地址,求出LC1的段地址:%rip+.LC1,将该值赋值给%rdi。
3.3.6.2 移动栈帧
汇编语句 subq $32, %rsp 移动栈帧,%rsp是栈顶指针,指向栈顶元素的地址,对栈指针进行减法操作,可以增加一段栈空间,大小为减去的数的大小,subq $32, %rsp,所以开辟增加了32字节的栈空间。
3.3.7 关系操作
关系操作的指令如下: 1. CMP S1,S2 2. TEST S1, S2 3 . SETX D
CMP S1,S2执行的操作为S2-S1,然后对条件码进行比较并且跳转。需要注意的是,CMP和TEST不修改寄存器的值,二十修改条件码的值,然后通过条件码的值比较判断并且跳转。Set的根据条件码的组合,把目标字节设置为0或者1。这就是关系操作指令的大致内容。
3.3.7.1 cmpl $3, -20(%rbp)
该判断语句原型是if(argc!=3)
该分支判断argc与3的关系情况,通过汇编语句cmpl $3, -20(%rbp)来进行条件判断,和je .L2进行相关的调转操作,代码截图如下,argc存储在-20(%rbp)之中
3.3.7.2cmpl $9, -4(%rbp),接下来执行jle .L4
该关系操作原型为for 循环中i与8的比较
该分支判断i与8的关系情况,通过汇编语句cmpl $7, -4(%rbp)来进行条件判断,和jle .L 4 进行相关的调转操作,代码截图如下.i存储在 -4 (%rbp)之中
3.3.8 数组/指针/结构操作
在汇编语言中,各类操作都由基础的mov指令来实现,所以大部分有关的操作是通过数据传送mov指令实现的。
3.3.8.1
汇编语句movl %edi, -20(%rbp) 作用:将寄存器%edi存储的内容赋值给-20(%rbp)指针指向的地址
汇编语句movq %rsi, -32(%rbp) 作用:寄把存器%rsi存储的内容赋值给-32(%rbp)指针指向的地址
这些是对指针的操作
3.3.8.2对数组的操作
argv[1]和argv[2]在汇编语言中变成了如下表述形式。
通过movq -32(%rbp),%rax等汇编语句对数组argv[]进行操作。
3.3.9 控制转移
控制转移的内容在本程序中和条件转移类似,都是通过CMP进行跳转和操作。
3.3.9.1cmpl $3, -20(%rbp)
该判断语句原型是if(argc!=3)
该分支判断argc与3的关系情况,通过汇编语句cmpl $3, -20(%rbp)来进行条件判断,和je .L2进行相关的调转操作,代码截图如下,argc存储在-20(%rbp)之中
3.3.9.2cmpl $9, -4(%rbp),接下来执行jle .L4
该关系操作原型为for 循环中i与8的比较
该分支判断i与8的关系情况,通过汇编语句cmpl $7, -4(%rbp)来进行条件判断,和jle .L 4 进行相关的调转操作,代码截图如下.i存储在 -4 (%rbp)之中
3.3.10 函数操作
3.3.10.1 参数传递(地址/值)。
是向函数中传递多个参数的操作
对于本体hello.s来说,main函数的函数形参有2个,main(int argc,char *argv[]), 在汇编代码中传送参数要借助寄存器,函数把将要传入的俩个参数argc *argv[]参数储存在%edi和%rsi中,接着在栈上保存。
3.3.10.2 函数调用
在本题的hello中,主函数调用了print 函数,在hello.s汇编语言中
通过如下的指令对Print进行调用
主函数也调用了sleep函数。在main函数内部,被多次调用,通过汇编语句call sleep@PLT 如图所示,进行调用
3.3.10.3函数返回
leave ret为函数返回的指令。leave恢复栈空间为之前的状态,然后 ret 返回
3.4 本章小结
本章介绍了编译的概念与作用,以及在Ubuntu下编译的指令,并且分析了hello.s中各个部分编译为成汇编以后的存储方式和表示。接着对生成的汇编语言hello.s进行数据赋值操作,类型转换,算术和位级操作,关系操作,指针/数组/结构操作 ,控制转移,函数操作的分析。
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:从汇编语言(hello.s)到机器语言(hello.o)的过程叫做汇编,汇编的工具时汇编器as ,。汇编的结果维可重定位目标文件
汇编的作用: 把汇编语言翻译成机器能够读懂的可重定向目标程序,方便后续的直接执行,利于机器执行,提高效率 。
4.2 在Ubuntu下汇编的命令
命令为:gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
命令为readelf -a hello.o >hello.elf
输入后在hello.elf 中产生如下格式
ELF头 |
.text节 |
.rodata节 |
.data节 |
.bss节 |
.symtab节 |
.rel.text节 |
.rel.data节 |
.debug节 |
.line节 |
.strtab节 |
节头表Section header |
ELF可重定位目标文件的表格
4.3.1 ELF头
ELF以一个16字节序列Magic起始,给出了当前系统字节大小和顺序。
其余部分有解释目标文件信息; ELF头的大小,目标文件的类型,机器类型(如x86-64),节头部表的文件偏移,以及节头部表中的条目的大小和数量。
ELF头
4.3.2节头表
节头如下
用来描述各个节的位置和大小,对于目标文件每一个节都有其固定大小和位置
4.3.3 符号表
符号表 它存放在程序中定义和引用的函数和全局变量的信息。
4.3.4 重定位节.rel.text节
一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
4.3.5 .rela.eh_frame节
分为R X86_ 64 PC32和R X86_ 64 _32进行重定位计算。
4.3.6 各个节的信息和意义
.text:已编译程序的机器代码。
.rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表。
.data:已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss中。
.bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态C变量。
.symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息
.rel.text节:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改
.rel.data节的重定位信息,用于对被模块使用或定义的全局变量进行重定位的信息,一般而言,任何已经初始化的全局变量,如果它的初始值为一个全局变量地址或者外部定义函数的地址,都需要被修改。
.debug节:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。
节头部表:节头表包括节名称,节的类型,节的属性(读写权限),节在ELF文件中所占的长度以及节的对齐方式和偏移量。
4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
不一致的地方:
- 分支转移语言的不同
在hello.s中,转移位置标记的L1,L2,在hello.o中被换成了相对偏移地址,即在hello.s中,通过标记符号L1L2进行跳转,而在hello,o中通过地址进行跳转
- 函数调用的不同
在hello.s汇编文件中,进行函数调用时,call后跟的是函数名称
而在hello.o中call的地址是目标地址的下一条指令,并且需要.rela.text节中的重定位条目,等待链接的的进一步得出调用函数的地址
- 变量的表示和访问方式不同
对于全局变量的访问来说,在hello.s汇编文件中,全局变量通过段地址+%rip来完成程序对全局变量的访问
而在hello.o生成的反汇编文件中,全局变量通过0+%rip来完成程序对全局变量的访问,0作为占位等后续修改,等.rodata节中的数据运行时确定该值为多少
4.5 本章小结
本章主要研究和介绍了从hello.s到hello.o的汇编过程,通过readelf -a hello.o >hello.elf来查看hello.o的可重定位目标文件ELF的格式,分析其组成和功能,并且使用objdump -d -r hello.o获得反汇编代码hello.objdump和hello.s进行比较其不同的地方,使得对汇编过程和ELF可重定位目标文件更加深入的理解。
第5章 链接
5.1 链接的概念与作用
概念: 本题中链接是从可重定位目标文件hello.o到可执行目标文件hello的过程,利用的工具为链接器。实际过程是:将各种代码和数据片段收集并组合称为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
作用: 链接使分离编译成为可能;一个大型应用程序不再需要组织巨大的源文件,而是可以分解为更小,更好管理的模块,可以独立修改和编译这些模块,方便管理和维护。也方便减少错误,提高编程效率。
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
或者采取gcc hello.o -o hello
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
命令:readelf -a hello >hello1.elf
ELF分析
ELF头 |
.text节 |
.rodata节 |
.data节 |
.bss节 |
.symtab节 |
.rel.text节 |
.rel.data节 |
.debug节 |
.line节 |
.strtab节 |
节头表Section header |
ELF可执行目标文件的表格
1)ELF头
2)节头部表:
对hello中节信息进行了声明和大小位置声明,有大小Size,偏移量Offset,大小、链接、对齐等相关信息信息
- 程序头部表:数组
反映程序可执行文件与其他之间的对应关系
5)符号表、
记录了符号 本地符号全局符号等,和它们存放的类型,偏移量等值
5.4 hello的虚拟地址空间
使用edb加载hello的窗口如下:
在edb中,对于Data Dump窗口,可以查看hello程序加载到虚拟地址中的hello程序。它的虚拟地址由0x400000开始,一直到0x400fff结束,这之间的每一个节对应ELF里面的每一个节头表。
Data Dump如图所示,程序从0x00400000地址加载,自0x400fff结束。
其中每个节与ELF中程序头相互对应。
PHDR是程序头表,INTERP是在程序运行之前调用的解释器。
5.5 链接的重定位过程分析
命令:objdump -d -r hello>hello1.objdump ,把可执行文件反汇编为hello1.objdump进行分析。
hello1.objdump比起 hello.objdump来说,它的地址为虚拟地址,并且多了更多的由于链接而添加的子函数。
部分hello1.objdump代码
在链接过程中,链接器通过/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o这些命令,定义了初始化的函数_init,程序入口_start,main函数。libc.so库定义了hello程序中需要的printf 函数等等。
所以使得在反汇编中,我们可以看到在hello的反汇编中,多了printf等链接的代码,它们已经通过链接库加载到了函数里面,而hello.o的反汇编中由于没有经过链接,所以其反汇编中没有对printf等函数的定义。
链接器解析重定位条目时可以发现哪些需要重定位和它们各自的类型,从而可以确定elf节的相对距离。
具体来说,比如
通过链接,在hello1.objdump中增加了外部链接的共享库函数,比如如上图所示,增加了printf@plt共享库函数, puts@plt共享库函数和getchar@plt函数 图中第一行所述,.plt.sec 其中就是添加了该库从而增加了这些函数。
5.6 hello的执行流程
打开edb并且使用edb执行hello,
加载流程中:
程序的名称 | 程序的地址 |
ld -2.27.so!_dl_start | 00007fee :aed07093 |
ld-2.27.so!_dl_init | 00007f5a :aed5d0c5 |
hello!_start | 400500 |
libc-2.27.so!__libc_start_main | 7efb ff100ab0 |
hello!printf@plt | 4004c0 |
hello!sleep@plt | 4004f0 |
hello!getchar@plt | 4004d0 |
libc-2.27.so!exit | 7efbff122120 |
hello!_int | 00000000:00400688 |
libc-2.27.so!__exit | 00007ff1:36889168 |
5.7 Hello的动态链接分析
在进行链接和调用共享函数库之前,对于一些外部函数,编译器不知道该函数运行地址。所以该引用会生成一条重定位 记录存放在ELF节中,当开始链接时,我们加载它,从而确定该函数运行的地址等相关信息。链接器通过使用过程链接表PLT和全局偏移量表GOT来实现函数的链接工作。
GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。
PLT:PLT是一个数组。每个被链接调用的库函数都有属于自己一个的PLT条目。其中的信息帮助调用一个在某处被需要且在该函数库里面的函数。
GOT:GOT是一个数组,每个条目都有一个相匹配的PLT条目。
dl_init函数调用后执行前后内存的变化。
在PLT中使用的jmp,执行完目标函数之后的返回地址为最近call指令下一条的指令地址,即在main中的调用完成地址。
在之后的函数调用时首先跳转到PLT实行.plt逻辑,第一次访问跳转时GOT地址为下一条指令,将函数序号压栈,然后跳转到PLT【0】,在PLT【0】中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时的地址,重写GOT,再讲控制传递给目标函数。
如果之后还需要调用该函数,则在第一次访问时即可直接调转到目标函数完成调用。
5.8 本章小结
本章通过对可执行文件hello的反汇编等来解释链接的方式,概念和作用,通过分析hello的ELF格式, hello的虚拟空间,了解了其虚拟空间的意义,并且将hello.o可重地位目标文件的反汇编文件与hello可执行目标文件的反汇编文件对比,深入具体了解了重定位过程和作用。最后通过遍历hello的执行过程与动态链接分析。从而对于链接的过程,作用掌握。
第6章 hello进程管理
6.1 进程的概念与作用
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。
进程的作用:提供一个独立逻辑控制流,它提供一种假象,好像我们运行的每个程序独占的使用处理器。提供一个私有地址空间,它提供一种假象,含香我们的程序独立的使用内存系统。通过这样的抽象,某个程序中的指令,某个程序中的代码和数据在运行时在它们自己看来,好像是系统运行着的中唯一的对象。有利于抽象和分析。
6.2 简述壳Shell-bash的作用与处理流程
壳Shell-bash的作用:shell是用户与操作系统之间完成交互式操作的一个接口。Shell会执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。
处理流程:
1)shell打印一个命令行提示符,对用户输入的命令行进行解析。
2)解析命令行之后,shell分析命令行参数是不是一个内置命令。
3)如果是内置命令,直接执行该命令即可。
4)如果不是内置命令,shell建立一个子程序,在子程序中执行,bash在初始子进程中上下文加载运行该命令。
6.3 Hello的fork进程创建过程
Fork进程调用fork函数,则创建了一个子进程。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。只有PID不同。
在调用一次fork()中会返回两次值。
父进程子进程执行先后不确定,在不同系统,不同机器,乃至不同次执行,其先后顺序都不确定。
地址空间独立但是相同。有各自私有地址空间。
6.4 Hello的execve过程
函数原型int exeve(const char *filename, const char *argv[], const char *envp[]);execve()用来执行参数filename字符串所代表的文件路径,第二个参数是利用指针数组来传递给执行文件,并且需要以空指针(NULL)结束,最后一个参数则为传递给执行文件的新环境变量数组。
execve() 系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。
execve过程图
6.5 Hello的进程执行
1)进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片time slice
2)逻辑控制流:PC值的序列叫做 逻辑控制流(逻辑流) ,每个进程代表一个逻辑控制流。 处理器的一个 物理控制流 被分为各个进程的逻辑控制流。 一个逻辑流的执行在时间上与另一个流重叠,称为 并发流 。 多个流并发地执行的一般现象被称为 并发 。 如果两个流并发地运行在不同的处理器核或者计算机上,那么就称它们为 并行流 。
3)用户态和核心态转换: 用户模式就是执行应用程度代码,访问用户空间;内核模式就是执行内核代码,访问内核空间(当然也有权限访问用户空间)。用户程序调用系统 API 函数称为系统调用(System Call);发生系统调用时会暂停用户程序,转而执行内核代码(内核也是程序),访问内核空间,这称为内核模式(Kernel Mode)。. 用户空间保存的是应用程序的代码和数据,是程序私有的,其他程序一般无法访问。. 当执行应用程序自己的代码时,称为用户模式(User Mode)。. 计算机会经常在内核模式和用户模式之间相互转换。
4)上下文切换:上下文切换指的是内核(操作系统的核心)在CPU上对进程或者线程进行切换。上下文切换过程中的信息被保存在进程控制块。上下文的切换流程如下
(1)挂起一个进程,将这个进程在CPU中的状态(上下文信息)存储于内存的PCB中。
(2)在PCB中检索下一个进程的上下文并将其在CPU的寄存器中恢复。
(3)跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行)并恢复该进程。
6.6 hello的异常与信号处理
中断、陷阱、故障、终止:
中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。硬件中断不是由任何一条专门的指令造成的,从这个意义上来说,它是异步的,而其他异常时同步发生的,是执行当前指令的结果。
陷阱是有意的异常,是执行一条指令的结果。陷阱最重要的用途是在用户程序和内核之间提供一个向过程一样的接口。
故障由错误情况引起,它可能能够被故障处理程序修正。如果错误能够修正,它就将控制返回到引起故障的命令,否则将返回到内核中的abort例程,终止引起故障的应用程序。
终止是不可恢复的致命错误造成的影响,通常是硬件错误。终止从不将控制返回给应用程序。
异常是允许操作系统提供进程的概念所需要的基本结构快。进程的经典定义就是一个执行中的程序的实例。系统中的每个程序都是运行在某个进程的上下文中,上下文由程序正确运行所需的状态组成,状态包括存放在存储器中的程序代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
程序正常执行
按下CTRL+Z:
按下回车:
按下Ctrl-C:
Ctrl-z后运行ps:Ctrl-z后运行jobs:
Ctrl-z后运行pstree:
Ctrl-z后运行fg
Ctrl-z后运行kill
1)ctrl+c属于中断异常
按下ctrl + c之后,父进程收到SIGINT或者SIGSTP信号,于是要处理信号,信号处理函数的逻辑是结束hello,并回收hello进程。
2)fg信号属于恢复信号。
按下fg后,内核发送SIGCONT信号,已经被挂起的程序hello重新运行。
6.7本章小结
本章从fork evecve出发,介绍了hello运行情况和运行时状态情况。
接着通过输入命令介绍异常信号处理的几种情况,分为中断、陷阱、故障、终止,对Hello运行时输入一些命令,观察其运行时进程状态,hello运行中可能的信号处理。通过对hello的具体分析我们深入了解了信号处理与异常等知识。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址是指由程序产生的与段相关的偏移地址部分。在hello中,就是由hello产生的与段相关的偏移地址部分
线性地址:逻辑地址到物理地址变换之间的中间层,它的地址空间是一个非负整数地址的有序集合,它的地址空间中的整数是连续的。在Hello中。程序hello的代码会产生逻辑地址,加上对应该段的基地址就变成了线性地址
虚拟地址:虚拟内存时由存放在磁盘上的续的字节单元组成的空间。逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。
物理地址:是真实地址,寻找物理地址需要通过CPU的地址总线的寻址,找到真实大的存储地址。这个物理地址也就是Hello运行需要的实际物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
分段过程的实质就是逻辑地址 -------> 线性地址 的过程
把逻辑地址划分成 段选择符+段描述符的判别符(TI)+地址偏移量 ,判断TI字段,如果时局部段描述符(ldt) 改为段描述符+地址偏移量的形式,这样可以得到线性地址。如果是全局段描述符(gdt) 则对应的改为段描述+地址偏移量,从而达到线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址可以分为VPN(虚拟页号)+VPO(虚拟页偏移),同时VPN(虚拟页号)可以分为TLBT(TLB标记)+TLBI(TLB索引),在TLB里搜寻对应的PPN(物理页号),缺页时,查找对应的PPN(物理页号),找到PPN之后,将其与VPO虚拟页偏移),组合变为PPN(物理页号)+VPO(虚拟页偏移)就是需要的物理地址。
地址翻译是一个N元素的虚拟地址空间(VAS)中的元素和一个M元素的物理地址空间(PAS)中元素的映射。CPU中存在一个控制寄存器为页表基址寄存器指向当前页表。n位的虚拟地址包含着p位的虚拟页面偏移和(n-p)位的虚拟页号。MMU利用VPN来选择适当的PTE,将页表条目中的物理页号和虚拟地址中的VPO串联起来就得到相对应的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
MMU有段描述符(1M)(还有页描述符大页(64KB)小页(4KB)和极小页(1KB))
我们这里说段页表的建立。
比如32位CPU,4G的寻址空间可分为4096个段(4G/1MB)
所以可以建立4096个对应的关系而实际的内存肯定没到4G(VA-PA 可多对一)
所以首先要在内存中指定存放该对应表的实际位置(可通过CP15协处理器指定)
CR3 寄存器包含Ll页表的物理地址。VPN 1 提供到一个Ll PET 的偏移量,这个PTE 包含L2 页表的基地址。VPN 2 提供到一个L2 PTE 的偏移量 …依次类推,我们得到VA到PA的变换
7.5 三级Cache支持下的物理内存访问
先得到物理地址VA,通过CI进行组索引,对各个块匹配 CT进行标志位匹配操作。如果匹配失败则不命中,若匹配成功命中。然后 CO值来取出对应的数据返回。若不命中,则cache向下一级逐层寻找,若还找不到则继续向下一层。
当更新cache的时候,若有空闲块则直接把需要的数据写入空闲块即可;若没有空闲块,则根据局部性驱逐一个块,把它擦掉然后重新写入一个块进去,即可。
7.6 hello进程fork时的内存映射
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写操作通过写时复制机制创建新页面
7.7 hello进程execve时的内存映射
在bash中的进程中执行了如下的execve调用:execve("hello",NULL,NULL);
相应的,execve 函数在shell中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。
加载并运行hell:删除已存在的用户区域。映射私有区域。映射共享区域。设置程序计数器(PC)
7.8 缺页故障与缺页中断处理
缺页中断:
(1) 保留进程上下文
(2)判断内存是否有空闲可用帧?若有,则获取一个帧号No,转(4) 启动I/O过程。若无,继续(3)
(3)腾出一个空闲帧,即:
(3)-1调用置换算法,选择一个淘汰页PTj。
(3)-2 PTj(S)=0 ; //驻留位置0
(3)-3 No= PTj (F); //取该页帧号
(3)-4 若该页曾修改过,则
(3)-4-1 请求外存交换区上一个空闲块B ;
(3)-4-2 PTj(D)=B ;//记录外存地址
(3)-4-3启动I/O管理程序,将该页写到外存上。
(4)按页表中提供的缺页外存位置,启动I/O,将缺页装入空闲帧No中。
(5)修改页表中该页的驻留位和内存地址。PTi(S)=1 ; PTi(F) =No。
(6)结束。
7.9动态存储分配管理
该空间如图所示,为红色部分
动态内存分配器维护着一个进程的虚拟内存区域。
动态内存分配器的目标是吞吐率最大化和内存使用率最大化。
分配器分为两种基本风格:显式分配器和隐式分配器。
31 | 210 |
块大小(头部) | a/f |
pred(祖先) | |
succ(后继) | |
填充) | |
块大小(脚部) | a/f |
显示空闲链表结构
使用边界标记的堆块的格式
简单的堆块的格式
7.10本章小结
本章主要介绍了hello的存储空间问题,由此引申出储存器的地址空间,挨个介绍了虚拟地址、物理地址、线性地址、逻辑地址等相关概念,以及它们彼此关系和转化,如何根据某种地址转化为物理地址的问题,并介绍了三级Cache支持下的物理内存访问以及hello的fork execve运行时的空间情况。当空间分配出现问题了该怎么办呢?接着开始了解系统应该如何处理缺页异常,介绍内存分配机制和如何快速更好的回收内存的问题。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件。
所有的I/O设备都被模型化为文件,包括硬件设备。每一个设备都是一个文件
所有的输入输出都作为对文件的操作。内核也可以被映射为文件
设备管理:unix io接口
unix I/O : 将设备映射为文件的方式,允许 Unix内核引出一个简单、低级的应用接口。 Linux/unix IO的系统调用函数很简单,它只有5个函数:open (打开)、close (关闭)、read (读)、write (写)、lseek (定位)。
8.2 简述Unix IO接口及其函数
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。int open(char* filename,int flags,mode_t mode)
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为K 。
4.读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号” 。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k 。ssize_t read(int fd,void *buf,size_t n)和ssize_t wirte(int fd,const void *buf,size_t n)
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
8.3 printf的实现分析
分析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;
}
代码位置:D:/~/funny/kernel/printf.c
在形参列表里有这么一个token:...
这个是可变形参的一种写法。
当传递参数的个数不确定时,就可以用这种方式来表示。
很显然,我们需要一种方法,来让函数体可以知道具体调用时参数的个数。,当调用printf函数的适合,先是最右边的参数入栈。
fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素。
fmt也是个变量,它的位置,是在栈上分配的,它也有地址。
对于一个char *类型的变量,它入栈的是指针,而不是这个char *型变量。
再看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':
itoa(tmp, *((int*)p_next_arg));
strcpy(p, tmp);
p_next_arg += 4;
p += strlen(tmp);
break;
case 's':
break;
default:
break;
}
}
return (p - buf);
}
接受一个格式化的命令,并把指定的匹配的参数格式化输出。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
追踪write:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
位置:d:~/kernel/syscall.asm
sys_call的实现:
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
位置:~/kernel/kernel.asm
8.4 getchar的实现分析
当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码。
并且产生一个中断请求,抢占当前进程运行。进入中断子程序。
中断子程序先从键盘接口读取该按键扫描码。将该扫描码转换成ASCII码。保存到系统缓冲区之中。
对于缓冲区,我们有:全缓冲、行缓冲和不带缓冲。getchar()会从输入缓冲区去读取内容,也就是说我们把所有的内容都输入完成并且按下了Enter键后,while循环才开始工作,每一次getchar()从输入缓冲区读取一个字符,然后如果不是换行符就输出。
8.5本章小结
本章主要介绍了IO设备管理,并且深入分析Unix IO接口 printf函数和getchar函数,对于看上去很简单的printf 和getchar 也有很复杂的背景,printf需要调用vsprintf write sys_call才能实现,而getchar需要进行中断处理等操作。并不是表面上那么简单。
结论
Hello的一生:
- 程序员编写hello.c源程序,让它输出 HELLO WORLD!
为了运行,还需要经过多次处理才能让我们的计算机运行该程序
2.hello.c通过预处理(由预处理器cpp完成),把注释省去,扩展宏定义等方式,得到hello.i文本文件
3. hello.i通过编译(由编译器ccl完成),得到汇编文件hello.s
4.hello.s通过汇编(由汇编器as完成),得到可重定位目标文件hello.o
5.hello.o通过链接(由链接器ld完成),生成了可执行文件hello
此时的hello已经算是成年了! 可以顺利由机器运行!
接着我们把它放到计算机中运行。
6. 在shell中输入./hello 1190600419 王子睿 此时我们的hello开始进入计算机度过它的余生。
7. shell进程调用fork为其创建子进程,接着调用execve,通过execve调用启动加载器,加映射虚拟内存
8.CPU给它分配时间片,MMU把它的虚拟内存地址映射成计算机上的实际物理空间。
9.在运行过程中会调用printf等函数,也需要设备IO管理操作
此时我们的hello已经结束了它的一生。
接下来会由父进程回收它。
10.hello被shell回收,删除它的所有数据
感悟
计算机系统可以算上人类智商和工业能力两者顶尖的结合。
不管是把物理地址抽象,还是抽象出进程,此类抽象无一不让人获益良久。
而从从无到有,通过那些抽象的概念,最终创造出这么一台功能完备的计算机系统,又是多少前人的心血和结晶呢。
计算机系统这样的一座大厦,值得我们学习很久很久
附件
中间结果 | 文件作用 |
hello.i | 源程序经过预处理得到的文本文件 |
hello.s | 经过编译后得到的汇编文件 |
hello.o | 经过汇编后得到的可重定位目标程序 |
hello | 经过链接后得到的可执行目标程序 |
hello.elf | hello.o的ELF文件 |
hello1.elf | hello的ELF文件 |
hello.objdump | hello.o的反汇编文件 |
hello1.objdump | hello的反汇编文件 |
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] ]Randal E. Bryant, David R. O'Hallaon. 深入理解计算机系统. 第三版.
[2] o0慢节奏0o 博客园 《《深入理解计算机系统》第八章复习总结》《深入理解计算机系统》第八章复习总结 - o0慢节奏0o - 博客园 (cnblogs.com)
[3]杨博东 《执行新程序》 execve()
execve()https://blog.csdn.net/yangbodong22011/article/details/50197785
[4] madao756 段页式访存——逻辑地址到线性地址的转换 https://www.jianshu.com/p/fd2611cc808e
[5] 百度百科对相关概念的解释 https://baike.baidu.com/
[6] 王楼小子 MMU段式映射(VA -> PA)过程分析 https://www.cnblogs.com/wanglouxiaozi/p/9676087.html
[7]Robot_9 操作系统---(35)缺页中断与缺页中断处理过程
https://blog.csdn.net/qq_43101637/article/details/106646554
[8]山中水寒 Linux/Unix中系统级IO 博客园
www.cnblogs.com/whc-uestc/p/4365507.html