计算机系统大作业
计算机科学与技术学院
2020年3月
摘 要
关键词:hello;计算机系统;程序;
本文以包含基本框架的hello C程序为例,从预处理到生成可执行文件,从加载内存产生进程到进程被回收,使用edb,objdump,readelf工具,对每个步骤进行分析,将程序与操作系统以及物理硬件的操作间的关系进行了实践,加深对计算机系统的理解。
目 录
第1章 概述
第2章 预处理
第3章 编译
第4章 汇编
第5章 链接
第6章 hello进程管理
第7章 hello的存储管理
第8章 hello的IO管理
附件
参考文献
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P是From Program to Process,Hello.c从一个.c文件,经过预处理,编译,汇编,链接得可执行文件,再通过系统创建一个新进程并且把程序的内容加载,实现了由程序到进程的转化。
020是From Zero-0 to Zero-0,通过创建子进程来执行程序,子进程通过execve执行这个可执行目标文件,中间经过异常、信号、虚拟内存管理等步骤,最终运行结束,称为僵尸进程,被父进程回收,在内存中消失。
1.2 环境与工具
硬件环境
64cpu 2.20GHz 16G RAM 2TB Disk
软件环境
Window10 64位;Vmware15;ubuntu19.04 64位;
开发工具
readelf objdump edb gedit
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.i 预处理得到的中间结果
hello.s hello.i编译后得到的汇编语言文本文件
hello.o hello.s汇编后得到的可重定位目标文件
Hello.o_text hello.o的代码段
hello 链接后得到的可执行目标文件
hello_text hello的代码段
hello_out objdump -a查看的内容
第2章 预处理
2.1 预处理的概念与作用
ISO C规定程序由源代码被翻译分为若干有序的阶段(phase),通常前几个阶段由预处理器实现,主要处理#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)
预编译的主要作用如下:
●将源文件中以”include”格式包含的文件复制到编译的源文件中。
●用实际值替换用“#define”定义的字符串。
●根据“#if”后面的条件决定需要编译的代码。
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
在.i文件里,前面有头文件的函数变量的声明代码,如下图常用的printf函数
在最后是我们的代码,看到注释部分已经删去了,如下图。
第3章 编译
3.1 编译的概念与作用
概念:将一个由高级语言程序格式的文本文件翻译成一个执行完全相同操作的汇编语言程序格式的文本文件的过程。
作用:将c语言代码转化为汇编代码,是代码转变为可执行文件的中间过程。
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1数据
1.常量
程序printf中使用了2个字符串常量,在rodata存储
2.局部变量
对局部变量i的第一次使用是在for循环赋初值0,对应的汇编语句用movl赋值0。
3.3.2 赋值
For循环里对i赋初值0,汇编使用movl实现。
3.3.3算数操作
For循环里的++操作汇编使用addl+1。
3.3.4关系操作
关系操作常使用cmp(cmpl)和j(跳转)的组合
Hello.c中使用了2个关系操作
对应的cmpl,je 和cmpl,jle
3.3.5 数组/指针/结构操作
数组指针结构的位置是连续的存储空间,通过起始位置和偏移位置就能够定位到目标位置。
Hello中使用了argv指针数组,每个指针占用8位,汇编语言如下,
先把起始位置给rax,rax加上偏移量,最后把值赋给目标位置。
3.3.6控制转移
控制转移常在if else for while switch中使用
Hello中使用了if和for控制,对应的汇编语句如下图,
绿色框内语句是if的判断和跳转语句
蓝色框里是for的判断跳转语句,i++操作在执行完黄色语句后执行,之后进入判断语句。
3.3.7函数操作
1.参数传递
X86-64在调用函数之前,会将前六个参数传递给寄存器,如果有剩余的参数,则压入栈中。
2.函数调用
函数在调用前会把返回地址(即当前栈的栈底)压入栈中,使得函数调用结束之后能够返回上一级函数。
Hello调用了4个函数;
Puts读入参数字符串地址,exit读入返回值1;printf读入rdx,rsi,rdi;
Sleep读入参数edi,表示睡眠时间。
3.函数返回
调用函数执行结束之后,若无返回值则直接返回,若有返回值则将返回值放入%rax寄存器之后返回到上一级函数栈。
第4章 汇编
4.1 汇编的概念与作用
概念:将汇编语言程序转化为二进制的机器语言程序,并将每个.s文件打包成可以被重定位的可重定位目标文件。此时的可重定位文件无法直接打开。
作用:生成了机器可以直接开始分析的机器指令。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
节头表显示各个部分的基本信息。
.o文件的section如下:
ELF头:包含文件结构说明信息
.text节:目标代码部分。
.rodata节:只读数据部分。
.data节:已初始化的全局变量。
.bss节:未初始化的全局变量。
.symtab节:符号表。
.rel.text节:.text节相关的可重定位信息。
.rel.data节:.data节相关的可重定位信息。
.debug节:调试用符号表。
.line节:C源程序中的行号和.text节中机器指令之间的映射。
.strtab节:字符串表。
Elf头显示文件的基本信息,如小端存储,可重定位文件等。
Data节没有需要重定位的内容,text里有重定位的内容,如下:
包含了调用的所有外部函数和rodata,因为链接后的需要合并成一个rodata。
Offset表示相对偏移,Info的高24位说明了所引用的符号索引,低8位为对应的重定位类型,Type即为对于类型的具体表示;重定位前值为0;Sym.Name即为绑定的符号名,Addend即为偏移。
因为相对偏移的定义,运行这条指令时,PC的值其实为下一条指令的地址,因此PC真正的值和我们目前想进行重定位的值有4个字节的偏移,所以Addend整体-4.
查看text代码,看到调用的函数都是00000000的常量。
symtab表,用来存放在程序中定义和引用的函数和全局变量的信息。每个可重定位目标文件在.symtab中都有一张符号表。
4.4 Hello.o的结果解析
.o文件是二进制文件,加入了对应的elf格式信息;同时每一条汇编代码都对应着一条由01序列所表示的机器代码,同时最左方标注了相对地址。
跳转指令和函数调用等指令,在汇编的注释中都表示为相对偏移,包括全局字符串的访问,在.s中的跳转是使用自己生成的标记(L2,L3,L4),函数调用PLT表示需要链接的外部函数。
第5章 链接
5.1 链接的概念与作用
链接是将相关的代码函数和数据组合成为一个完整文件的过程,这个文件可直接加载到内存执行。链接操作可以在编译,加载或者运行时处理。
5.2 在Ubuntu下链接的命令
ld链接使用的动态链接器为ld-linux-x86-64.so.2,同时添加了crti.o,crt1.o,crtn.o等系统目标文件,将程序入口_start、初始化函数_init,通过_start程序调用hello.c中的main函数开始程序,libc.so是动态链接共享库,包括使用到的printf,sleep等库函数。
5.3 可执行目标文件hello的格式
对比hello和hello.o 的elf header,
文件类型分别是EXEC(可执行文件)和REL(可重定位文件)
Hello有入口地址0x401090 hello.o是0,无法进入
程序头大小和节头大小增加了
节头里没有了需重定位的text和data节,增加了节,其中,
.got 用于动态链接,保存代码段中引用动态链接库中数据的地址,一般在运行时确定
.got.plt 类似于.got,保存代码段中引用动态链接库中函数的地址,一般在运行时确定
.plt 用于动态链接时的延迟绑定,运行时,当遇到动态链接库中的函数或数据时,会跳到这个段内,用于解析地址
.rela.dyn 用于查看.got段和数据段(一般是.bss)需要修正的符号
.rela.plt 用于查看需要修正.got.plt中的数据
5.4 hello的虚拟地址空间
401090开始的text节,也是程序的入口。
402000开始的rodata节,能看到存储的2个字符串
5.5 链接的重定位过程分析
对比看到,main函数的地址改变为虚拟内存的地址,调用函数时,也从空地址变为实际地址(这里的存储的地址是相对偏移,实际地址计算出来是401030和下图的结果一致)。
同时连接将使用到的外部函数也加载到代码段,如puts,printf。
5.6 hello的执行流程
程序调用的外部函数的地址在代码段
0x7fdb2065d030
0x7fdb2066b9e0
0x401090<hello!_start>
0x7fdb2047ca80<libc-2.29.so!_libc_start_main+0>
0x4010c1<hello!main>
<hello!puts>
<hello!exit>
<hello!printf>
<hello!atoi>
<hello!sleep>
<hello!getchar>
5.7 Hello的动态链接分析
GOT全称global offset table,是.data段的一部分。用于存储动态链接器的地址,got节的地址为0x403ff0
dl_init前,got存储0,还没有链接
dl_init后,got有了存储内容,链接器可以使用
动态链接是把程序拆分成各个相对独立部分,在程序运行时进行链接,生成完整的程序。在形成可执行文件时,需要对比动态链接库检查外部函数,如果函数有动态链接的标志且动态链接库里有该函数,那么就不会对这个函数符号重定位,而是留在装载运行时再进行。
第6章 hello进程管理
6.1 进程的概念与作用
进程(Process)是操作系统中的一个基本概念,通常指的是运行中的程序,它包含着一个运行程序所需要的资源。进程之间是相对独立的,操作系统就是利用进程把工作划分为多个独立的区域。
6.2 简述壳Shell-bash的作用与处理流程
Shell:一般我们是用图形界面和命令去控制计算机,真正能够控制计算机硬件(CPU、内存、显示器等)的只有操作系统内核(Kernel),由于安全、复杂、繁琐等原因,用户不能直接接触内核,需要另外再开发一个程序,让用户直接使用这个程序;该程序的作用就是接收用户的操作(点击图标、输入命令),并进行简单的处理,然后再传递给内核,内核和用户之间就多了一层“中间代理”,Shell 其实就是一种脚本语言,也是一个可以用来连接内核和用户的软件,我们编写完源码后不用编译,直接运行源码即可。
bash是shell的一种,在早年的UNIX年代,发展者众多,所以就有许多不同的版本,例如Bourne shell(sh),这也是必然的,每种shell都有其应用的需求,很难说孰好孰坏。而在Linux中默认的shell就是Bourne-Again shell(简称bash)。
Shell从标准输入或脚本中读取的每行称为一个管道行,它包含一个或多个由0个或多个管道字符(|)分隔的命令。对每一个管道行,进行12个步骤的处理。
-1. 将命令行分成由 元字符(meta character) 分隔的 记号(token):
元字符包括 SPACE, TAB, NEWLINE, ; , (, ), <, >, |, &
记号 的类型包括 单词,关键字,I/O重定向符和分号。
-2. 检测每个命令的第一个记号,看是否为不带引号或反斜线的关键字。如果是一个 开放的关键字,如if和其他控制结构起始字符串,function,{或(,则命令实际上为一复合命令。shell在内部对复合命令进行处理,读取下一个命 令,并重复这一过程。如果关键字不是复合命令起始字符串,而是如then等一个控制结构中间出现的关键字,则给出语法错误信号。
-3. 依据别名列表检查每个命令的第一个关键字。如果找到相应匹配,则替换其别名定义,并退回第一步;否则进入第4步。
-4. 执行大括号扩展,例如a{b,c}变成ab ac
-5. 如果位于单词开头,用$HOME替换。使用usr的主目录替换~user。
-6. 对任何以符号$开头的表达式执行参数(变量)替换
-7. 对形如$(string)或者string
的表达式进行命令替换
这里是嵌套的命令行处理。
-8. 计算形式为$((string))的算术表达式
-9. 把行的参数替换,命令替换和算术替换 的结果部分再次分成单词,这次它使用$IFS中的字符做分割符而不是步骤1的元字符集。
-10. 对出现*, ?, [ ]对执行路径名扩展,也称为通配符扩展
-11. 按命令优先级表(跳过别名),进行命令查寻。
先作为一个特殊的内建命令,接着是作为函数,然后作为一般的内建命令,最后作为查找$PATH找到的第一个文件。
-12. 设置完I/O重定向和其他操作后执行该命令。
6.3 Hello的fork进程创建过程
进程的创建采用fork函数:pid_t fork(void);
创建的子进程先复制父进程虚拟地址空间相同但是独立的一份副本,可以共享文件,然后作为新的进程运行。他们的最大区别是PID不同。两者并发执行。
6.4 Hello的execve过程**
execve函数在当前进程的上下文中加载并运行一个新程序,即调用加载器,加载器将可执行目标文件的代码和数据从磁盘中复制到内存,然后跳转到入口点来运行程序。程序正常运行时,execve函数调用一次不返回。
fork函数与execve函数合作,可以实现在shell中创建子进程,并且在子进程中运行新的程序。
6.5 Hello的进程执行
Linux 系统中的每个程序都运行在一个进程上下文中,有自己的虚拟地址空间。当shell运行一个程序时,父shell进程生成一个子进程,它是父进程的一个复制。子进程通过execve系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片(chunk),新的代码和数据段袚初始化为可执行文件的内容。最后,加载器跳转到_start地址,它最终会调用应用程序的main函数。
6.6 hello的异常与信号处理
异常是异常控制流的一种形式,它一部分由硬件实现,一部分由操作系统实现。异常就是控制流中的突变,用来相应处理器状态中的某些变化。
异常可以分为四类:
在hello执行过程中,在shell输入属于中断,对不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令的验证如下:
在运行时随意输入或者输入回车时,将键盘输入存储后继续执行hello,hello结束后在处理之前的信号。
Ctrl+C进程收到SIGINT信号,默认直接终止,ps查看已经不在进程列表里。
Ctrl+Z进程收到SIGTSTP信号,系统默认停止,直到收到SIGCONT
停止后,进程存在,状态为stopped
fg命令使停止的进程收到SIGCONT信号,重新在前台运行。
停止状态下,kill杀死hello进程,ps查看hello进程仍存在,fg命令运行停止的进程,显示terminated,进程已经终止。
pstree查看进程树,包含所有进程。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:由程序产生的与段相关的偏移地址部分,逻辑地址由两个16位的地址分量构成,一个为段基值,另一个为偏移量。
线性地址:是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址后加上基地址就是线性地址。
非负整数地址的有序集合:{0, 1, 2, 3 … }
物理地址:计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组,每一个字节都有一个唯一的物理地址。
M = 2m 个物理地址的集合{0, 1, 2, 3, …, M-1}
虚拟地址:由CPU生产,经过MMU转换可以转换为物理地址,虚拟地址实际上就是一种线性地址。
N = 2n 个虚拟地址的集合 ===线性地址空间{0, 1, 2, 3, …, N-1}
逻辑地址出现在hello.o的反汇编代码中,虚拟地址出现在hello反汇编代码中,因为在连接阶段进行了重定位操作,指令以及大部分引用都已经被映射到虚拟内存空间中。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址用段来描述,段分为2个部分:段选择符和段描述符。
1.段选择符
TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)
RPL=00,为第0级,位于最高级的内核态,RPL=11,为第3级,位于最低级的用户态,第0级高于第3级
高13位-8K个索引用来确定当前使用的段描述符在描述符表中的位置
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字段中)
这样从段寄存器的段选择符通过描述符表转变为段描述符,再到线性地址,完成转变。
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存到物理内存通过索引变换。在内存中,页表就是作为索引的数据结构。因此,我们可以让每个进程都有一个页表,页表中的每一项都记录着该进程中对应的一页所投影到的物理地址、是否有效、还有一些其他信息等。
然而多级的页表具有好的查找速度,因此一般使用的是多级页表,转变过程如下。
7.4 TLB与四级页表支持下的VA到PA的变换
Translation Lookaside Buffer (TLB)翻译后备缓冲器是MMU中一个小的具有高相联度的集合,能够实现虚拟页码向物理页码的映射,它对于页码数很少的页表可以完全包含在TLB中,从而减少了常用地址的转变时间,减少了页表查找不到的缺页处理。从VA到PA的变换如下图,MMU 使用虚拟地址的 VPN 部分来访问TLB,标记位用于匹配索引集合中的行,索引(TLBI)用于查找具体的地址,如果查找到,就可以直接访问物理地址,不需要再访问页表。
以Corei7位例,访存的过程如图。
7.5 三级Cache支持下的物理内存访问
物理地址(PA)分成PPN和PPO。可将PPO进一步分成CI和CO,CI作为cache组索引,CO作为块偏移,PPN作为标记。
三级cache支持下的物理内存访问过程:
1.CPU产生一个虚拟地址VA
2.MMU利用虚拟地址(VA)中的VPN从TLB中取出相应的PTE。若命中,MMU将PTE中的PPN与虚拟地址(VA)中的VPO串联,得到物理地址(PA)。
若不命中,利用VPN多级页表机制到内存中找对应的页表条目(PTE),MMU翻译进而得到物理地址(PA)。
3.MMU将翻译成的物理地址发送到高速缓存/主存;高速缓存采用LI,L2,L3三级cache。
4.高速缓存/主存将所请求的数据字返回给CPU。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进场创建各种数据结构,并分配唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,也就为每个进程保持了私有地址空间的概念。
7.7 hello进程execve时的内存映射
execve的步骤如下:
1.删除已存在的用户区域
2. 创建新的区域结构
3. 私有的、写时复制
4. 代码和初始化数据映射到.text和.data区(目标文件提供)
5. .bss和栈堆映射到匿名文件 ,栈堆的初始长度0
6. 共享对象由动态链接映射到本进程共享区域
7. 设置PC,指向代码区域的入口点
7.8 缺页故障与缺页中断处理
缺页其实就是DRAM缓存未命中。当我们的指令中对取出一个虚拟地址时,若我们发现对该页的内存访问是合法的,而找对应的页表项式发现有效位为0,则说明该页并没有保存在主存中,出现了缺页故障
此时进程暂停执行,内核会选择一个主存中的一个牺牲页面,如果该页面是其他进程或者这个进程本身页表项,则将这个页表对应的有效位改为0,同时把需要的页存入主存中的一个位置,并在该页表项储存相应的信息,将有效位置为1。然后进程重新执行这条语句,此时MMU就可以正常翻译这个虚拟地址了。
7.9动态存储分配管理
动态内存分配器(例如malloc)维护着一个进程的虚拟内存区域,称为堆。对于每个进程,内核维护着一个变量brk,指向堆的顶部。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配,要么是空闲的。
分配器有两种基本风格,两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
-1.显示分配器:要求应用显示释放任何已分配的块。
-2.隐式分配器:也叫垃圾收集器,会自动监测不再使用的块将其释放回收。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/O设备都被模型化为文件,内核也被映射为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口称为Unix I/O。
8.2 Unix IO接口及其函数
linux 提供如下 IO 接口函数:
read 和 write – 最简单的读写函数;
readn 和 writen – 原子性读写操作;
recvfrom 和 sendto – 增加了目标地址和地址结构长度的参数;
recv 和 send – 允许从进程到内核传递标志;
readv 和 writev – 允许指定往其中输入数据或从其中输出数据的缓冲区;
recvmsg 和 sendmsg –结合了其他IO函数的所有特性,并具备接受和发送辅助数据的能力。
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系统函数,直到陷阱系统调用int0x80或syscall。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息),并通过信号线向液晶显示器传输每一个点(RGB分量)。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回完整的字符串。
结论
编写,通过键盘将C代码键入hello.c
预处理,将hello.c调用的所有外部库复制合并,对部分#类指令处理
编译,将hello.i编译成为汇编文件hello.s,语句转化为汇编语言
汇编,将hello.s会变成为可重定位目标文件hello.o,是二进制但不可执行的文件
链接,将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello
创建子进程:shell进程调用fork为hello创建子进程
运行程序:shell调用execve,execve调用启动加载器,进入程序入口,程序载入内存,执行main函数。
访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址进行访问。
动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。
中断:执行到getchar时需等待通过shell和Unix IO键入字符,回车进行一次读取,进程继续执行。
结束:main函数执行结束后,shell父进程回收子进程,程序执行结束。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
计算机系统从底层的01操作和存储到最上层的可读性强的C语言,通过多层的关联抽象,从机器指令到汇编语句,再从汇编语句到C语言,这是运行程序前的步骤;进程是执行程序的抽象,虚拟内存,虚拟地址对物理内存和物理地址的抽象。通过这些巧妙的处理,将计算机的基础功能(01位运算和存储)与复杂运算关联起来,帮助更多的人理解和参与其中,这种关联抽象的方法在很多方面都可以应用。
参考文章
[1] https://blog.csdn.net/weixin_42432281/java/article/details/88392219
[2] https://www.jianshu.com/p/a702a01db5c7
[3] https://blog.csdn.net/weixin_34148340/article/details/91912492
[4] https://blog.csdn.net/shiny_hy/article/details/103771000.
[5] https://blog.csdn.net/cobracanary/java/article/details/85454080.
[6] https://www.jianshu.com/p/f2490a452960
[7] https://www.cnblogs.com/pianist/p/3315801.html
[8] https://blog.csdn.net/ww1473345713/article/details/51680017