摘要
本文讨论了与编程和软件开发相关的几个关键概念和过程。首先介绍了链接的概念和作用,它是将代码和数据片段组合成单一文件的过程,使得分离编译成为可能,从而可以更好地管理和修改模块。接下来探讨了进程的概念和作用,进程是正在执行的程序的实例,包括程序的代码、数据和运行状态。然后讨论了Shell的作用和处理流程,它是用户与操作系统之间的交互界面,并负责进程的创建、程序加载、前后台控制和信号处理等任务。本文还涉及了执行新程序的过程,包括execve函数的调用和进程上下文的转换。最后,介绍了逻辑地址、线性地址、虚拟地址和物理地址的概念,以及多级页表和缓存对虚拟地址到物理地址的转换过程。这些概念和过程在软件开发和计算机系统的运行中起着重要的作用,帮助理解和管理程序的执行和内存的使用。
关键词:链接、进程、Shell、执行、缓存、模块、代码、数据、程序加载
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
1.1.1 P2P
程序的生命周期始于一个高级C语言程序。为了在系统上运行hello.c程序,需要将每条C语句转化为一系列低级机器语言指令。首先,hello.c源程序经过预处理器(cpp)转化为修改后的hello.i源程序文本。预处理器执行诸如宏展开、条件编译等操作,生成一个经过处理的源代码文件。
接下来,编译器(cc1)将hello.i编译为汇编程序文本hello.s。编译器执行词法分析、语法分析和语义分析等步骤,将高级语言代码转换为汇编语言代码。
然后,汇编器(as)将hello.s转换为可重定位目标程序二进制文件hello.o。汇编器将汇编语言代码转化为机器语言指令,并生成与机器硬件体系结构兼容的目标文件。
同时,链接器(ld)将hello.o与共享链接库(如printf.o)一起进行链接,生成最终的可执行目标程序二进制文件hello。链接器的任务包括解析符号引用、符号重定位和地址分配等,以将不同目标文件和库文件整合在一起,形成可执行文件。
在Shell中输入./hello后,程序通过fork()函数创建新的进程,并使用execve加载程序。fork()函数用于复制当前进程,创建一个新的子进程。子进程在execve调用中加载可执行文件,并取代当前进程的内存空间,从而执行相应的程序。这个过程涉及到虚拟内存映射(mmap),其中内核将可执行文件的内容映射到进程的虚拟地址空间。
通过这一过程,程序从程序(program)转变为进程(process),实现了从P2P(程序到进程)的转换。进程是正在运行的程序的实例,它具有独立的内存空间和系统资源,并由操作系统进行调度和管理。
1.1.2 020
在Shell中,通过调用fork()函数创建子进程,然后使用execve进行虚拟内存映射,分配物理内存,并在代码段中执行程序,实现了包括打印printf等操作。在这个过程中,内存管理器和CPU利用L1、L2和L3高速缓存以及TLB(Translation Lookaside Buffer)和多级页表来提高数据访问速度。
当调用fork()函数时,操作系统会为子进程创建一个独立的虚拟地址空间,但实际的物理内存并没有复制,而是采用了写时复制(copy-on-write)的技术。这意味着在子进程修改内存内容之前,它们与父进程共享相同的物理内存页面。
通过execve函数,子进程的虚拟内存空间被映射到物理内存中,并加载可执行文件的代码段到内存中的相应位置。当子进程执行程序时,CPU从内存中获取指令并逐条执行,包括执行printf等打印操作。
为了提高数据访问速度,现代计算机系统通常采用多级缓存和页表结构。L1、L2和L3高速缓存是位于CPU内部的快速存储器,用于暂时存储频繁访问的数据和指令。TLB是一个快速查找表,用于将虚拟内存地址转换为物理内存地址,以减少访问页表的次数。多级页表是一种层次化的数据结构,用于将虚拟内存地址映射到物理内存地址。
当程序运行完成后,Shell会回收子进程,内核最后删除相关数据结构,从系统中清除与子进程相关的资源。这样,整个过程就实现了进程的创建、执行和终止,即所提到的“020”的过程。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1.2.1 硬件环境
CPU:Intel Core i7
1.2.2 软件环境
Windows11 专业版;VMware 17;Ubuntu 20.04
1.2.3 调试工具
Visual Studio2017;codeblocks 64位;gcc
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c:源程序
hello.i:预处理后文件
hello.s:编译后的汇编文件
hello.o:可重定位目标程序
hello.out:可执行目标程序
hello_o.elf:hello.o的ELF格式
hello_o.txt:hello.o的反汇编代码
hello.elf:hello的ELF格式
hello.txt:hello的反汇编代码
1.4 本章小结
1.本章主要介绍了hello程序的p2p过程和020过程
2.介绍了硬件环境、软件环境和开发工具
3.描述了中间文件的名字和作用
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理器(cpp)是编译过程中的第一个阶段,根据以字符#开头的命令,对原始的C程序进行修改和处理。它主要执行以下几个功能:
宏定义和常量标识符替换:预处理器通过使用#define指令,将宏定义替换为相应的代码片段。宏定义可以是函数、常量或表达式,通过在程序中使用宏名称,预处理器会将其展开为对应的代码。
文件包含(#include):预处理器使用#include指令来读取其他文件的内容,并将其插入到当前程序文本中。典型的例子是使用#include<stdio.h>来包含系统头文件stdio.h,这样程序可以使用stdio.h中定义的函数和变量。
预处理指令:预处理器还支持其他预处理指令,如条件编译指令(如#ifdef、#ifndef、#if、#else、#endif),用于根据条件选择性地包含或排除一些代码片段。
注释和空白字符删除:预处理器会删除程序中的注释(以//或/* */形式出现)和多余的空白字符,以减小程序的大小和简化后续编译阶段的处理。
通过这些处理步骤,预处理器可以修改原始的C程序,生成一个经过处理的中间文件(通常以.i作为文件扩展名),这个文件会被编译器进一步处理和编译。预处理器的主要目的是为了准备好进行后续编译所需的代码和环境。
2.2 在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
在经过预处理后,代码的长度大大的增加,达到了3060行
当我们在寻找main函数时,我们可以在预处理后的.i文件中进行查找。该文件是经过预处理器处理后的中间文件。
在.i文件中,你会发现两个函数的代码几乎相同,唯一的区别是.i文件删除了#include部分的内容和注释部分的内容。这与之前分析预处理器的作用是一致的,预处理器会将#include指令所引用的内容直接插入程序文本中,并删除注释和多余的空白字符。
继续查找,你会发现.i文件中引用了stdio.h头文件。这是因为在原始的C程序中,通过#include<stdio.h>指令包含了stdio.h头文件,其中定义了输入输出相关的函数和变量。在预处理阶段,预处理器会将stdio.h的内容插入程序文本中,从而使得程序可以使用stdio.h中定义的函数和变量。
通过预处理器的处理,我们可以看到预处理阶段的一些效果:宏替换、文件包含以及注释和空白字符的删除。这些步骤为后续的编译阶段提供了经过处理和准备好的代码和环境,使得程序可以顺利进行编译和执行。
2.4 本章小结
1.预处理可以初步翻译源文件,进行宏替换,文件包含,删除注释等操作
2.熟悉了Ubuntu下预处理的命令
3.解析了hello.c预处理的结果,并和前面的理解进行对比和验证
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译是软件开发中的一个重要阶段,它将源代码翻译成机器能够执行的形式。在这里,我们来讨论编译器(如cc1)将文本文件hello.i翻译成文本文件hello.s的过程。
编译器的主要任务是将高级语言代码转换为等价的中间代码表示或汇编代码,使得计算机能够理解和执行这些指令。
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
字符串常量:
对应
printf("用法: Hello 学号 姓名 秒数!\n");
printf("Hello %s %s\n",argv[1],argv[2]);
关系操作:
对应:if(argc!=4)
算数操作:
和
对应:for(i=0;i<8;i++)
局部变量:
将argc存入-20(%rbp),将argv[]存入-32(%rbp)
数组、指针:
再次可见,将argv[]存入-32(%rbp),后续对应基地址+8、+16,sleep对应+24
赋值:
int i存入-4(%rbp),i=0,movl对应“双字”mov
类型转换:
call atoi@PLT;
对应:atoi(argv[3])
函数操作:
call puts@PLT;
call exit@PLT;
call printf@PLT;
call atoi@PLT;
call sleep@PLT;
call getchar@PLT;
参数传递:
在其中main函数传递参数为int argc,char *argv[],printf函数传递参数为argv[1], argv[2],exit函数传递参数为1,sleep函数传递参数为atoi(argv[3]),getchar函数无传递参数。
函数返回:
main函数通过ret返回了0,其他的函数的返回值保存在%rax寄存器中。
3.4 本章小结
1.了解并介绍了编译的概念和作用
2.熟悉了在Ubuntu下编译的指令
3.在S文件中找到了C语言的数据与操作的编译表示,进一步熟悉了汇编指令
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编是将汇编语言代码转换为机器语言指令的过程。在这里,我们来探讨汇编器(如as)将文本文件hello.s翻译成机器语言指令,并将其打包成可重定位目标程序的格式,并将结果保存在目标文件hello.o中。
汇编的主要作用是将汇编语言翻译成机器指令,这些指令是计算机能够直接执行的指令。汇编器将汇编语言代码转换为机器指令,并生成目标文件,为后续的链接和执行提供了准备。目标文件中包含了可重定位的指令编码和相关的符号信息,可以与其他目标文件进行链接,形成最终的可执行目标程序。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
ELF(Executable and Linkable Format)是一种用于可执行文件、目标文件和共享库的文件格式。在ELF文件中,它的头部包含了一些描述文件信息的字段。其中,ELF头的开始部分是一个16字节的序列,描述了生成该文件的系统的字的大小和字节顺序。
节头部表(Section Header Table)是ELF文件中的一个重要部分,用于描述目标文件中不同节的信息。每个节头部表条目对应一个节,包含了关于该节的类型、地址、大小、偏移等信息。节头部表的条目数和大小在ELF头中有相应的字段指定。
在链接过程中,链接器会将所有相同类型的节合并为同一类型的新的聚合节。例如,来自不同输入模块的.data节会被合并为输出的可执行目标文件的一个.data节。这个过程称为节的重定位(Section Relocation)。
本程序中,需要进行重定位的是printf、puts、exit、sleep、getchar以及.rodata节中的.L0和.L1等符号。这些符号需要在链接过程中被解析和赋予运行时内存地址。
符号表(Symbol Table)是链接器中的一种数据结构,用于存储模块定义和引用的符号的信息。在链接器的上下文中,可以将符号分为三种不同类型:全局符号、外部符号、局部符号。符号表记录了这些不同类型的符号的名称、地址、大小和其他属性信息。链接器在处理模块之间的符号引用和定义时,会使用符号表来解析符号,并进行正确的链接和重定位,以确保程序的符号引用能够正确解析到符号的定义位置。
4.4 Hello.o的结果解析
分支转移:
由分析知S文件中用的是段名称,如图中为.L3,而在反汇编代码中发现跳转为80<main+0x80>发现为一个相对的地址而不是助记符。
函数调用:
发现在可重定位文件中call后不是函数的具体名称,而是一条重定位条目指引的信息。而汇编文件中直接加的是相对地址
数字进制:
反汇编代码中为16进制
4.5 本章小结
1.了解并介绍了汇编的概念和作用
2.熟悉了在Ubuntu下反汇编的指令
3.了解了elf文件各个部分的作用如ELF头、节头部表、符号表和可重定位节等
4.对比了hello.o和hello.s之间的差距,分析了汇编语言和机器语言的对应关系
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,使得该文件能够被加载到内存并执行。在现代系统中,这个过程通常由称为链接器(Linker)的程序自动执行。
链接的主要作用是解决分离编译(Separate Compilation)所带来的模块化开发的需求。而不是将一个大型的应用程序组织为一个巨大的源文件,链接器允许我们将程序分解为更小、更易管理的模块,每个模块可以独立地进行修改和编译。当我们修改其中一个模块时,只需重新编译该模块,并将其重新链接到其他已编译的模块上,而不必重新编译整个程序。
5.2 在Ubuntu下链接的命令
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
ELF头效果和汇编时基本一样,唯一的不同是hello文件ELF头中type变为EXEC格式,原来为REL格式
节头部表、重定位节和符号表也相同
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
datadump从0x400000开始,最初是ELF表,和前ELF相同,包含了此段的相关信息。
根据节头部表可以知道个各节所在的位置,如.text节在偏移量为0x40的地方,即0x400040
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
变为192行
hello_.s中调用相应函数时使用的是已经进行重定位的虚拟地址,区别于未经过链接中的只有指令
在进行链接过程时,hello_.s文件中新增了hello.c代码中使用的库函数,例如exit、printf、sleep、atoi、getchar等函数。这使得程序的各个节变得更加完整,并包含了这些库函数的代码和数据。而未链接的hello_o.s文件中则没有这些库函数的代码和数据。
通过重定位节和符号定义、重定位节中的符号引用这两个重定位步骤,链接器能够确保程序中的每条指令和全局变量都具有正确的运行时地址。这使得程序能够顺利执行,并正确调用和访问所需的库函数和全局变量。重定位过程的完成使得程序的各个模块之间能够正确地链接和协作,最终生成可执行文件或共享库等输出文件。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
ld-2.27.so!_dl_start
ld-2.27.so!_dl_init
hello!_star
libc-2.27.so!__libc_start_main
-libc-2.27.so!__cxa_atexit
-libc-2.27.so!__libc_csu_init
hello!_init
libc-2.27.so!_setjmp
libc-2.27.so!exit
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
由hello的elf文件可知,got表是0x404000为首地址,大小为0x48的表
查询gdb可以发现,在运行dl_init之前
运行后变为如上图所示
共享库(Shared Library)是一种目标模块,可以在运行或加载时加载到任意内存地址,并与在内存中的程序进行链接。与静态库不同,共享库的链接发生在程序运行时,而不是编译时。这意味着共享库可以独立于程序进行开发和编译,并且可以在不同的程序之间共享和复用。
在调用共享库函数时,编译器无法预测函数的运行时地址,因为定义该函数的共享库模块在运行时可以加载到任何内存位置。为了解决这个问题,使用了延迟绑定(Lazy Binding)的方法。
5.8 本章小结
1.了解并介绍了链接的概念和作用
2.熟悉了在Ubuntu下链接的指令
3.了解了elf文件各个部分的作用如ELF头、节头部表、符号表和可重定位节等并和hello.o的进行比较
4.在edb中查找了hello的虚拟空间地址,并分析了运行程序前的链接过程和整个程序的执行流程
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是指在操作系统中正在执行的程序的实例。每个进程都是独立的,具有自己的内存空间、寄存器集合、程序计数器和其他与执行相关的状态信息。进程是操作系统进行任务调度和资源分配的基本单位。
进程的作用是实现并管理程序的执行。每个程序在运行时都需要一个进程来提供执行环境和资源支持。进程为程序提供了一个隔离的执行环境,使得不同程序可以同时运行而互不干扰。
6.2 简述壳Shell-bash的作用与处理流程
Shell是一种用于与操作系统交互的命令行解释器或图形界面的应用程序。它充当用户与计算机系统之间的接口,接收用户输入的命令,并将其传递给相应的程序进行执行。Shell具有以下作用:
用户界面:Shell提供了用户与操作系统之间的交互界面。用户可以通过在Shell中输入命令来执行各种操作,如运行程序、管理文件和目录、设置系统参数等。
程序执行:Shell可以执行用户输入的命令或脚本,并将其传递给相应的程序进行处理。它可以调用系统内置的命令,也可以执行外部程序或脚本文件。
程序加载和运行控制:Shell负责加载和运行程序。它可以根据用户的输入启动和管理进程,并提供前台和后台程序的控制,如程序的挂起、继续、终止等。
作业调度:Shell可以接收和管理作业。作业是一系列相关的任务或命令,Shell可以根据用户的需求调度和管理这些作业的执行,包括作业的启动、停止、暂停、恢复等。
信号处理:Shell作为进程管理的代表,负责处理各种信号。它可以接收来自操作系统或其他程序发送的信号,并根据信号的类型执行相应的操作,如终止程序、忽略信号、捕获信号等。
Shell的处理流程一般包括以下步骤:
从终端读入输入的命令:Shell会读取用户在终端输入的命令或指令。
切分参数:Shell会解析输入的字符串,并将其切分为命令和参数,以便进行后续处理。
内置命令执行:如果输入的命令是Shell内置的命令,Shell会直接执行相应的操作,而不需要调用外部程序。
调用程序执行:如果输入的命令是外部程序或脚本文件,Shell会调用相应的程序进行执行。它会在系统中查找并加载程序,然后将命令传递给程序进行处理。
处理键盘输入信号:Shell作为交互式程序,需要接收并处理键盘输入的信号。它可以响应特定的按键或组合键,执行相应的操作,如中断程序、退出Shell等。
6.3 Hello的fork进程创建过程
输入如图所示命令,首先shell对输入的命令进行解析,在输入以后会为程序创建一个进程(使用fork函数),由于是子进程,因此与父进程共用一个虚拟地址空间,但是进程PID并不相同
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序,它会替换当前进程的地址空间,将新程序的代码、数据、堆和栈段映射到进程的虚拟地址空间中。执行execve函数后,当前进程的代码、数据和状态都会被新程序取代,从而开始执行新程序的指令。
当运行hello程序时,操作系统会为新程序创建一个新的虚拟地址空间。在这个虚拟地址空间中,操作系统将加载可执行代码和数据,将其从磁盘复制到内存中。虚拟地址空间还可以包括堆和栈段,用于存储动态分配的内存和函数调用的局部变量。
此外,虚拟地址空间还可以映射共享区域,这使得多个进程可以共享相同的内存区域,从而实现进程间的通信和数据共享。
最后,操作系统会将控制权转移到新程序的第一条指令,从而开始执行新程序的逻辑。
需要注意的是,execve函数在成功时不会返回,而是直接开始执行新程序。只有在发生错误时,execve函数才会返回-1,表示执行失败。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
进程调度是操作系统中的重要任务之一,它决定了哪些进程能够获得处理器的使用权以及使用的时间片长度。调度器根据一定的策略从就绪队列中选择一个进程,将处理器的控制权分配给它,使其在处理器上执行一段时间。
当运行hello程序时,Shell会创建一个新的进程,并在该进程的上下文中运行可执行目标文件。进程开始时处于用户态(用户模式)中,只能执行受限的操作。进程从用户态转换到内核态的唯一途径是通过异常,如中断、故障或系统调用。当异常发生时,控制权被传递给异常处理程序,处理器将模式从用户态转换为内核态。处理器在内核态执行异常处理程序,可以执行更高特权级的操作。
例如,在hello程序中,调用sleep函数或getchar函数会触发系统调用,这将导致进程从用户态切换到内核态。操作系统的内核会处理这些系统调用,并执行相应的操作,如暂停进程一段时间或等待用户输入
内核有权利决定何时抢占当前运行的进程,并重新启动先前被抢占的进程。这种决策称为调度决策。当一个进程被抢占时,当前进程的上下文将被保存,以便稍后恢复执行。然后,被抢占的进程的上下文将被恢复,并将控制权传递给该进程,使其继续执行
调度器的工作包括选择合适的进程进行调度,保存和恢复进程的上下文,以及处理进程切换的细节。它使用各种调度算法和策略来平衡系统的性能、响应时间和公平性,以满足不同的需求。
总结起来,进程调度涉及选择合适的进程并分配处理器时间片,通过上下文切换在不同进程间切换执行。用户态和内核态之间的转换通过异常和系统调用实现。调度器的任务是决定进程切换的时机,并保存和恢复进程的上下文。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
回车:
对程序运行没有影响,但是运行完循环后会自动输入回车,因此程序直接终止
Ctrl+Z:
停止进程,发送信号SIGSTP,父进程接收到信号并进行处理
使用ps指令发现进程并没有终止
输入fg后又会继续进行被挂起的进程
输入jobs可以观察到hello程序当前的状态
输入pstree可以查看当前进程树,找到hello程序发现是在terminal的bash下的
Kill:
使用kill发送信号9(SIGKILL)给进程17385,负的PID会导致信号被发送到进程组PID中的每个进程,终止以后再ps发现已经没有hello进程
Ctrl-C:
Ctrl-C发送信号2(SIGINT),是来自键盘的中断,默认行为是终止进程,因此程序直接退出并被回收
乱按:
6.7本章小结
1.了解了进程的概念与作用
2.熟悉了shell-bash的作用和处理流程
3.了解了fork函数如何创建进程,execve函数如何执行函数
4.了解了多种信号的处理和异常中断
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址是指在段式存储管理机制下,由段基址和段偏移量组成的地址。在这种相对寻址方式下,逻辑地址是相对于段基址的偏移量,用于访问存储器中的数据或指令。在hello程序的反汇编代码中,给出的地址是逻辑地址,它是基于段式存储管理的相对地址。
线性地址是在分段机制与分页机制结合的情况下产生的地址。当分段和分页同时使用时,线性地址是逻辑地址到物理地址转换的中间结果。它是一个连续的地址空间,是在逻辑地址和物理地址之间的一个抽象概念。在hello程序的反汇编代码中,通过偏移量可以得知地址是线性连续的,属于线性地址。
虚拟地址是在使用虚拟存储器技术时生成的地址。它是由CPU生成的用于访问主存的中间地址,需要通过内存管理单元(MMU)硬件将虚拟地址转换为物理地址。虚拟地址空间是程序在运行时的地址空间,它可以大于物理内存的实际空间。在hello程序中,代码段的地址0x400000是虚拟地址,需要经过MMU的转换映射到物理地址。
物理地址是计算机系统中主存的实际物理地址,它对应于内存中的存储单元。物理地址是由MMU翻译后得到的最终地址,用于实际的数据或指令访问。在hello程序中,MMU将虚拟地址翻译成物理地址,以便访问实际的存储单元。
综上所述,逻辑地址是相对于段基址的偏移量,线性地址是逻辑地址到物理地址转换的中间结果,虚拟地址是CPU生成的用于访问主存的中间地址,物理地址是最终的实际地址。这些地址在不同的内存管理机制下进行转换和映射,以实现程序的正确执行和存储器的管理。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在Intel的段式管理中,逻辑地址通过段描述符进行转换为线性地址。每个段描述符占据8个字节,其中包含了有关段的基址、长度和类型等信息。
段描述符的格式如下:
段的基址:由B31-B24、B23-B16和B15-B0构成,共32位。基址可以是4GB空间中的任意地址,用于指定段的起始位置。
段的长度:由L19-L16和L15-L0构成,共20位。如果G位为0,表示段的长度单位为字节,段的最大长度为1M(2^20)。如果G位为1,表示段的长度单位为4KB,段的最大长度为4G(4KB * 2^20)。
段的类型:指示段是代码段还是数据段,以及可读写权限等。
假设我们将段的基址设置为0,段的长度设置为4G,这样就构成了一个从0地址开始覆盖整个4G空间的段。通过索引可以定位到相应的段描述符,从而获取段的基址。将段基址与偏移量相加,就得到了线性地址,也就是虚拟地址。
当访存指令中给出的逻辑地址通过段描述符转换为线性地址后,线性地址会被放置到地址总线上,从而成为物理地址。需要注意的是,逻辑地址与基址加偏移构成的层次式地址是不同的概念。逻辑地址是经过段式管理转换后的地址,用于访问内存中的数据或指令。
总结而言,通过段描述符的基址和偏移量的组合,可以得到线性地址,即虚拟地址。逻辑地址是经过段式管理转换后的地址,最终会映射到物理地址上,用于实际的数据访问。这种机制可以实现对4GB空间的灵活管理和保护。
7.3 Hello的线性地址到物理地址的变换-页式管理
获取页表基址:通过页表基址寄存器(Page Table Base Register,PTBR)获取页表的基址。页表是一种数据结构,用于存储虚拟页号与物理页号的映射关系。
虚拟地址解析:根据虚拟地址,提取虚拟页号和虚拟页偏移量。虚拟页号用于在页表中查找对应的页表项,而虚拟页偏移量与物理页偏移量相同。
查询页表项:使用虚拟页号作为索引,在页表中查找对应的页表项。每个页表项包含有效位、物理页号等信息。
判断有效位:检查页表项中的有效位。如果有效位为有效(有效位被设置),则表示虚拟页号与物理页号的映射有效,并且可以获取物理页号。
物理地址生成:将物理页号与虚拟页偏移量组合,形成物理地址。物理地址表示在物理内存中的实际位置。
地址转换完成:此时,虚拟地址已经成功转换为物理地址。可以使用物理地址来进行实际的数据读取或写入操作。
如果页表项的有效位为无效(有效位未被设置),则表示虚拟页号没有与物理页号进行映射,即发生了缺页(Page Fault)。这时会触发缺页中断,操作系统会根据缺页中断处理程序的逻辑,将缺失的页从辅存(如硬盘)加载到内存中,并更新页表,使得虚拟地址能够成功转换为物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
1.CPU产生一个虚拟地址
2.MMU从TLB中取出相应的PTE
3.MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存
4.高速缓存/主存将所请求的数据字返回给CPU
多级页表结构:多级页表由多个级别的页表组成,每个级别的页表大小相等。通常使用两级页表结构作为示例进行说明,其中一级页表称为目录表(Page Directory),二级页表称为页表(Page Table)。
分级索引:地址转换通过多级索引进行。例如,对于一个32位的虚拟地址,可以将其划分为多个部分,如目录索引、页表索引和页内偏移量。通过目录索引找到目录表中对应的目录项(Page Directory Entry),再通过页表索引找到页表中对应的页表项(Page Table Entry)。
索引与物理页框映射:目录项和页表项中存储着与物理内存页框的映射关系。例如,页表项中存储着物理页框号,用于将虚拟地址映射到物理地址。
多级页表的灵活性:多级页表的一个重要特点是它的灵活性。只有在需要时,才会创建和加载二级页表,从而减少了内存消耗。例如,当某个虚拟页被访问时,如果对应的二级页表不存在,操作系统会进行相应的页调入(Page In)操作,将二级页表加载到内存中,然后继续进行地址转换。
缓存页表项:由于多级页表结构中只有最常使用的页表项会被缓存在主存中,而其他页表项只在需要时才被加载到主存,从而减少了主存的压力。这种策略使得主存的资源可以更加高效地利用。
7.5 三级Cache支持下的物理内存访问
MMU查询页表:当CPU发出一个虚拟地址时,MMU会首先查询页表以获取对应的页表项(Page Table Entry,PTE)。MMU使用虚拟页号(VPN)从页表中查找对应的PTE。
物理地址生成:从页表项中获取物理页号(Physical Page Number,PPN),并与虚拟地址中的页内偏移量(Virtual Page Offset,VPO)组合形成物理地址。
缓存访问:MMU将生成的物理地址发送给缓存。在缓存中,物理地址被拆分为缓存偏移量(Cache Offset,CO)、缓存组索引(Cache Index,CI)和缓存标记(Cache Tag,CT)。
命中检测:缓存通过比较缓存组索引和缓存标记,判断是否命中。如果缓存组索引与标记匹配,则表示发生了命中。
数据返回:如果发生了命中,缓存将从偏移量位置读取对应的数据字节,并将其返回给MMU。随后,MMU将数据传递回CPU,供后续处理使用。
如果在缓存中发生了命中,可以快速地获取所需数据,这是因为缓存具有更快的访问速度和较短的访存延迟。而如果发生了未命中(缓存失效),则需要从主存中获取相应的数据,这会引入较长的访存延迟。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,操作系统会为新进程创建一系列的数据结构,并为它分配一个唯一的进程标识符(PID)。为了创建新进程的虚拟内存,内核会创建当前进程的内存管理结构(mm_struct)、区域结构(vma)以及页表的副本。
在刚开始时,新进程的虚拟内存与调用fork函数时的进程完全相同。为了实现写时复制(Copy-On-Write,COW)机制,内核将这两个进程中的每个页面标记为只读,并将每个区域结构标记为私有的。这样,当其中一个进程尝试进行写操作时,COW机制会触发。
具体来说,当某个进程尝试修改一个只读页面时,操作系统会为该进程创建一个新的页面,并将新的数据复制到该页面中。这样,原始页面仍然是只读的,并且新页面成为进程的私有页面。通过这种方式,每个进程都保持了一个独立的地址空间,尽管它们最初共享相同的物理页面。
这种写时复制机制的好处是节省了内存开销。在fork函数调用后,新进程可以共享相同的物理页面,直到其中一个进程尝试进行写操作。这样可以减少内存的冗余拷贝,提高了系统的效率。
7.7 hello进程execve时的内存映射
删除已存在的用户区域:在执行execve函数之前,当前进程的虚拟地址空间中可能存在先前映射的用户区域。这些区域结构会被删除,释放相应的资源。
映射私有区域:在execve函数中,为新程序的代码、数据、bss和栈区域创建新的区域结构。这些区域都是私有的,并且采用写时复制的机制。
代码区域(.text):映射到hello文件中的可执行代码部分。
数据区域(.data):映射到hello文件中的初始化数据部分。
bss区域:映射到匿名文件,并被初始化为二进制零。bss区域包含在hello程序中,它用于存储未初始化的全局和静态变量。
栈和堆区域:在execve函数调用时,栈和堆的初始大小为零。它们在后续的程序执行中可以根据需要进行动态调整。
映射共享区域:如果hello程序链接了共享对象(或共享库),那么这些对象会被动态链接到该程序中,并映射到用户虚拟地址空间中的共享区域内。共享区域允许多个进程共享相同的代码和数据,以节省内存空间。
设置程序计数器(PC):在execve函数完成后,当前进程的上下文被更新,包括设置程序计数器(PC)。PC指向代码区域的入口点,即新程序的起始指令,以便开始执行新程序的代码。
7.8 缺页故障与缺页中断处理
DRAM缓存不命中即称为缺页,如图所示,需要返回磁盘中找到需要的数据并修改页表。这时会找到一个牺牲页替换这个页表
如图所示修改后重新执行刚刚触发中断的程序,此时会命中,程序正常进行。
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
7.10本章小结
本章主要介绍了hello的存储器地址空间、Intel逻辑地址到线性地址的变换-段式管理、Hello的线性地址到物理地址的变换-页式管理、TLB与四级页表支持下的VA到PA的变换、三级Cache支持下的物理内存访问,分析了hello进程fork时的内存映射,hello进程execve时的内存映射、缺页故障与缺页中断处理和动态存储分配管理。
(第7章 2分)