本文将详细介绍在Linux环境下,从源代码文件hello.c开始,经过预处理、编译、汇编、链接等多个步骤,直至生成可执行程序并运行的完整过程。同时,我们还将探讨与进程、存储管理和I/O管理相关的一些内容。
关键词: 计算机系统;Linux;GCC;hello.c;预处理;编译;汇编;链接;进程管理;存储管理;I/O管理。
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
第1章 概述
1.1 Hello简介
Hello的P2P是Problem to Process的过程,从编写源代码文件hello.c开始,用预处理器对源代码进行处理,生成预处理后的代码文件hello.i;预处理后的代码文件编译为汇编代码文件hello.s;汇编器将汇编代码文件转换为机器语言代码文件hello.o;链接器将机器语言代码文件和相关的库文件链接为可执行文件;执行该文件时,OS为该文件fork产生进程。
在020中,程序开始执行,OS为其映射到虚拟内存;进程执行目标代码,并被OS分配时间片,最终在硬件上实现;进程执行完毕,OS使内存等恢复到进程执行之前的状态。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:VMware Workstation 16 Pro以上,Ubuntu 18.04以上
开发调试工具:CodeBlocks 64位;gcc;EDB
1.3 中间结果
Hello.c: 源文件
Hello.i: 预处理输出文件
Hello.s: 编译输出文件
Hello.o:可重定位的目标文件
Hello:可执行的目标文件
1.4 本章小结
本章总体介绍了hello.c的一生,包括生成的各个文件以及实验的环境等基本信息。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:预处理是指在编译源代码之前,对源代码文件进行的一系列处理操作。
作用:将源代码文件中的宏定义、条件编译、头文件包含等预处理指令展开,并在源代码中插入相应的代码。
2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c -o hello.i,如图2.2
2.3 Hello的预处理结果解析
打开生成的hello.i文件:如图2.3
对于源文件hello.c进行预处理后,生成的hello.i文件中已经将源文件中的注释删除,并将宏展开后插入了相关头文件的内容。对源文件中的所有预处理指令(以#开头的指令)进行处理。这些指令可以包括宏定义(#define)、条件编译(#ifdef、#ifndef、#if等)和其他预处理指令。对于宏定义,预处理器会将源文件中定义的宏进行展开,以便在编译时可以直接使用宏定义的值,而不需要进行重复的替换工作。
2.4 本章小结
本章主要进行了预处理过程,生成了hello.i文件,并对内容进行了解释。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:编译是指编译器将 C语言源代码转换为汇编语言代码,包括生成汇编指令、变量和符号表等信息
作用:进行语法和语义的检查,并将高级语言转化为计算机能够执行的汇编代码。在这个过程中,编译器会对代码进行优化,以提高程序的执行效率。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
命令:gcc -m64 -Og -S -no-pie -fno-PIC hello.c -o hello.s,如图3.2
3.3 Hello的编译结果解析
程序hello.s的内容如下:如图3.3
3.3.1 数据
1.字符串,图3.3.1-1
2.局部变量i,for循环中初始为0,每次进行一次i++操作。图3.3.1-2
3.main函数:参数argc和argv作为用户传给main的参数,也是被放到堆栈中,刚开始分别储存在寄存器%edi和%rsi中,并在主函数中将其压栈,同样通过%rbq的偏移量来访问。图3.3.1-3
4.数组:对数组中的某一个元素的引用是指向数组第一个元素的指针加上相对偏移量,如图3.3.1-4
3.3.2赋值
只存在一个i=0的赋值操作。因为int类型,所以用movl,如图3.1.2-1
3.3.3对函数的操作
调用使用call语句,将要传入的参数存进一些寄存器(如图3.3.3-1),函数返回用ret实现,如图3.3.3-2
3.3.4控制转移
根据前面cmpl的命令设置的条件码进行跳转,如图3.3.4-1
3.3.5比较操作
判断argc!=4,以及i<8的控制如图3.3.5-1
3.4 本章小结
本章介绍了有.i文件到.s文件编译的概念和作用,对生成的文件分析了数据,赋值,函数,控制转移,比较操作的具体语句。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:将汇编语言程序翻译成机器语言程序的过程,在汇编过程中,汇编器将汇编代码转换成对应的机器指令,并生成目标文件,.o作为扩展名。
作用:将高级语言编写的源代码转换成机器能够执行的指令,它是将高级语言程序转换成可执行机器语言程序的一个必要步骤。由于机器语言是由0和1组成的指令代码,直接编写和阅读机器语言程序非常困难,因此汇编语言程序是一种更接近机器语言的中间形式。
4.2 在Ubuntu下汇编的命令
命令:gcc -m64 -Og -c -no-pie -fno-PIC hello.c -o hello.o如图4.2
4.3 可重定位目标elf格式
可重定位目标文件的 ELF 格式如图4.3.1:
通过readelf具体分析可重定位目标文件 hello.o的elf头:如图4.3.2
如图所示,ELF 头是一个 ELF格式的目标文件中的一个特殊节区,用于描述文件的基本信息。从第一行可以看出,它是文件的第一个节区,以一个16字节的序列开始,描述了生成该文件的系统的字的大小(Data)和字节顺序,以及ELF 头剩余部分的大小(Size of this header)和版本号。ELF
头包含的信息包括文件类型(Type)、机器类型(X86-64)等。
此时深入分析ELF头的具体信息:使用readelf-S命令,如图4.3.3:如图所示,可以看到节头部表的文件偏移以及节头部表中条目的大小和数量。共有15个节头,从偏移量0x510开始,从左至右依次对应号,名称,类型,地址,偏移量。
此时使用readelf -shello.o查看符号表的信息:如图4.3.4:符号表中存放着程序定义和引用的全局变量和函数。同样由于还未进行链接重定位,偏移量Value还都是0。
重定位节的分析:使用objdump -rhello.o查看,如图4.3.5,重定位节包含了.text文件中需要重定位的信息,而可执行文件中不包含重定位节。在这里涉及到了R_X86_64_PC32和R_X86_64_32,PC32就是重定位一个使用32位PC相对地址的引用。一个PC相对地址就是距程序计数器(PC)的当前运行时值的偏移量。当CPU执行一条使用P℃相对寻址的指令时,它就将在指令中编码的32位值加上P℃的当前运行时值,得到有效地址。32是重定位一个使用32位绝对地址的引用。通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。
4.4 Hello.o的结果解析
使用objdump -d -r hello.o分析hello.o的反汇编如图4.4.1:对比hello.s的汇编文件,有以下几个不同点:
-
操作数指令格式不同:反汇编器通常以16进制的方式显示指令,汇编代码中是十进制。而且反汇编中每条加入了对应的地址。
-
调用不同:汇编代码中,函数的调用一般使用call指令,后面紧跟着函数的标签名或地址。在反汇编代码中,函数的调用一般通过相对偏移来实现,也就是相对于当前指令的地址和目标函数的地址之间的偏移量。
-
分支跳转不同:反汇编代码中,分支跳转指令通常使用相对地址进行跳转,这个相对地址是相对于当前指令的地址计算出来的,所以需要对主函数的地址进行相对偏移。在汇编代码中,通常会将代码分成多个代码块(如.L4、.L3),然后使用标签来标识每个代码块的入口地址,分支跳转指令中使用这些标签作为目标地址进行跳转。
-
除此之外,基本相同。
4.5 本章小结
本章介绍了程序的汇编过程,分析了可重定位目标文件的内容以及反汇编代码与汇编代码的不同之处。为链接打下了基础。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:将多个目标文件和库文件合并为一个可执行文件或共享库的过程。在编译过程中,一个程序通常被分为多个源文件,每个源文件编译成一个目标文件。链接器将多个目标文件组合起来,解决它们之间的引用关系,并生成一个可执行文件或共享库,使得程序能够被正确地执行。
作用:解决符号引用和重定位问题,以及对目标文件和库文件进行组合和优化。包括符号表的合并、符号的重定位、以及符号的弱化以及链接器对重定位表的解析和修改操作。
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如图5.2.1生成了hello文件。
5.3 可执行目标文件hello的格式
使用命令readelf -a hello >hello.elf生成hello.elf来查看ELF格式,如图5.3.1所示
- ELF Header:如图5.3.2
ELF头增加了program headers的相关条目,描述虚拟内存到内存的映射关系,Type也变成了EXEC格式。
- Section Headers:与可重定位目标文件的节头部表的格式一致,只是从原本的13个条目拓展到27个条目,如图5.3.3与图5.3.4
- Program Headers:在可执行文件的加载过程中,程序头部表描述了可执行文件中各个段(段是连续的片)如何映射到内存中的连续内存段。每个段具有以下属性:
a. Offset:目标文件中的偏移量,表示段在目标文件中的位置。
b. VirtAddr/PhysAddr:虚拟内存地址/物理内存地址,表示段在内存中的起始位置。
c. Align:对齐要求,表示段在内存中的对齐方式。
d. FileSiz:目标文件中段的大小,表示段在目标文件中的字节数。
e. MemSiz:内存中段的大小,表示段在内存中的字节数。
f. Flag:运行时访问权限,表示段在运行时的访问权限。
如图5.3.5所示:
- 重定位节:在动态链接时,对于共享库的函数位置还不能确定,因此需要进行重定位节(RelocationSection)的处理。重定位节包含了需要在程序加载时进行地址修正的信息,它记录了共享库中的符号引用与实际地址之间的关系。当程序加载共享库时,动态链接器会根据重定位节的信息对共享库进行地址修正,以便正确地将符号引用与实际函数位置关联起来。重定位节的处理是在程序加载时进行的,它是动态链接的一部分。通过重定位节的修正,程序能够正确地定位共享库中的函数位置,并顺利执行相关的代码逻辑。如图5.3.6所示:
- 符号表:可执行文件相比于可重定位目标文件拥有更多的符号,并包含了必要的启动函数,以便程序在执行时能够正确加载和链接所需的函数和库,并完成必要的初始化工作。如图5.3.7与图5.3.8所示:
图5.3.7
- 动态链接条目:在动态链接时,重要的是要注意"Dynamicsection"部分,其中包含了动态链接的相关信息和属性。每个条目都具有标签(Tag)和相应的类型(Type)以及名称或值(Name/Value)。这些动态链接条目包含了程序运行时加载和链接共享库所需的重要信息。通过解析这些条目,动态链接器能够正确加载共享库并进行重定位,以实现程序的正确执行。如图5.3.9所示:
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
如图5.4.1所示:关于可执行文件 hello 的内存布局。前三行表示 hello 的 LOAD类型段,根据权限的不同,分为可读可执行、只读和可读可写三个部分。接下来的四行表示libc 和 ld占用的内存,它们也具有相同的结构,这部分内存是共享区域。在共享区域之前应该有堆区域,但是由于hello 没有使用 malloc动态申请内存,所以堆段为空。在共享区域之下是栈段和系统映射区域。
表明可执行文件 hello提供了程序所需的信息,并且这些信息包含在前三行指示的虚拟内存空间中。其他部分的内存布局由于内存使用效率等原因,在加载阶段进行动态处理,并且依赖于可执行文件提供的信息。
5.5 链接的重定位过程分析
- objdump -d -r hello (图5.5.1)以及其重定位条目(图5.5.2)与objdump -d -r hello.o(图5.5.3)的区别与分析:在可重定位目标文件hello.o中,重定位条目用于指示链接器在最终生成的可执行文件中如何填充符号的地址或跳转目标的地址。这些重定位条目包含了对静态符号和动态符号的引用。对于静态符号引用,链接器会将其填充为最终程序中该符号所在的地址。这样,当程序执行时,可以直接跳转到正确的地址执行相应的代码。对于动态符号引用,链接器会在重定位条目中填写对应的地址。此时通过过使用命令readelf -r hello,可以查看 hello可执行文件中的重定位条目。这些条目通常位于 .got 和 .got.plt节中,它们是动态链接所使用的重要部分。这些重定位条目描述了需要在运行时进行动态链接和重定位的符号和函数调用。因此重定位条目在可重定位目标文件中指示了链接器如何填充符号地址或跳转目标地址。静态符号引用直接填充为地址,而动态符号引用填充为对应的PLT 表条目,最终由动态链接器进行处理和重定位
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/05e490d635e3589159f0a37f51092ebe.png)
5.6 hello的执行流程
-
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
-
过程分析:首先调用了 ld-2.27.so!_dl_start,然后进入ld-2.27.so!_dl_init,再进入 Hello!_start。如图5.6.1所示。main函数中,按照顺序调用以下函数:hello!puts@plt,hello!exit@plt,hello!printf@plt,hello!sleep@plt,hello!getchar@plt,如图5.6.2所示。最后调用ld-2.27.so!_dl_runtime_resolve_xsave,ld-2.27.so!_dl_fixup,ld-2.27.so!_dl_lookup_symbol_x,libc-2.27.so!exit,退出程序
5.7 Hello的动态链接分析
-
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
-
过程分析:在进行动态链接之前,需要进行静态链接来生成部分链接的可执行目标文件hello。这个过程中,共享库中的代码和数据并没有被合并到 hello中,而是作为独立的共享目标文件存在。当加载 hello时,动态链接器负责对共享目标文件中的相应模块的代码和数据进行重定位,并加载共享库,最终生成完全链接的可执行目标文件。这个过程是由动态链接器完成的。在程序运行时,只有在需要调用共享库中的函数时才进行实际的链接操作。这样可以延迟链接的时间,减少加载和启动时间,并且只链接实际使用到的函数,提高了运行效率。在GOT 中存放函数的目标地址,而 PLT 则使用 GOT中的地址进行跳转到目标函数。通过延迟绑定和使用PLT+GOT,动态链接器能够在程序运行时动态解析符号并进行重定位,实现了函数的动态链接和加载共享库的功能。如图5.7.1和图5.7.2为函数之前与函数之后。
5.8 本章小结
本章对链接的概念和作用进行了介绍,并通过分析可执行文件 hello 的 ELF
格式文件,详细解释了其虚拟地址空间、重定位过程和执行效果的处理操作。这些内容有助于理解链接的工作原理和可执行文件的结构。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
-
概念:进程是计算机中正在运行的程序的实例。它是操作系统进行资源分配和调度的基本单位,可以看作是一个独立的执行流。每个进程都有自己的内存空间、程序代码、数据、打开的文件、运行时堆栈以及其他的系统资源。
-
作用:并发执行,资源分配,交互与通信,实现多任务,每个进程拥有自己的资源和环境,防止彼此之间的干扰和数据共享带来的安全问题,操作系统可以对进程进行管理,包括创建、终止、调度、挂起、恢复等操作,保证进程的正确执行和资源的合理利用。
6.2 简述壳Shell-bash的作用与处理流程
-
作用:Shell的作用是接收用户的命令输入,并将这些命令传递给操作系统内核进行执行。Bash是shell的一种变形。
-
处理流程
a. 用户输入命令:用户在命令行界面输入命令,并按下回车键。
b. 程序读取用户输入的命令,并进行解析。
c. 对输入的命令进行解析,将命令名称和参数分开,并对参数进行处理。
d. 根据命令名称查找可执行文件的路径,找到对应的程序或命令,并将参数传递给该程序进行执行。如果是内置命令(如cd、echo等),则会直接执行内置命令。
6.3 Hello的fork进程创建过程
- Hello程序开始执行,首先会执行主函数`main()`。
- 在`main()`函数中,当遇到`fork()`函数调用时,操作系统创建一个新的进程
- fork()`函数的返回值不同于父进程和子进程。在父进程中,`fork()`函数的返回值是子进程的进程ID(PID),而在子进程中,返回值是0。
- 父进程和子进程会继续执行相同的代码,但是通过返回值的不同,可以在程序中区分父进程和子进程。
- 在子进程中,可以根据需要执行特定的操作。子进程是父进程的一个副本,它会从`fork()`函数的调用点开始执行。子进程独立于父进程,并且拥有自己的资源和数据空间。
- 父进程和子进程之间的执行是并发的,它们可能会以任意顺序执行。操作系统负责调度和管理进程的执行。
- 父进程可以通过子进程的进程ID来监控和管理子进程的执行。例如,可以使用`wait()`函数等待子进程的结束,并获取子进程的退出状态。
- 程序继续执行,父进程和子进程可以根据需要执行各自的逻辑。
- 当程序执行完毕或遇到`exit()`函数时,父进程和子进程都会退出,并返回控制权给操作系统。
6.4 Hello的execve过程
总述:execve在运行时需要以下四个步骤:删除已经存在的用户区域、映射私有区域、映射共享区域、设置程序计数器。具体如下:
- 在Hello程序中,当程序需要执行另一个可执行文件时,可以调用`execve()`函数。
- 调用`execve()`函数时,需要提供要执行的可执行文件的路径和命令行参数。这些参数包括可执行文件的路径以及传递给可执行文件的参数列表。
- `execve()`函数会在当前进程的上下文中加载并执行指定的可执行文件。它会覆盖当前进程的代码、数据和堆栈,并将控制权转移到新的可执行文件中。
- 在执行`execve()`函数后,当前进程的内容被新的可执行文件替换,原来的程序代码和数据将不再执行。
- 新的可执行文件开始执行,并按照其自身的逻辑运行。它可以使用`argc`和`argv`参数来获取命令行参数,并执行相应的操作。
- 当新的可执行文件执行完毕或遇到`exit()`函数时,它会退出,并返回控制权给操作系统。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
-
进程调度:当Hello进程被加载到内存中并准备执行时,操作系统会为其分配一段时间片,即允许它运行一定的指令。
-
进程执行:Hello进程的上下文信息被加载,包括程序计数器指向Hello进程的下一条要执行的指令。
-
用户态与核心态转换:Hello进程运行在用户态,只能执行受限的指令和访问有限的资源。
-
总之,在Bash调用execve函数之后,Hello程序被加载并开始在用户模式下运行。当Hello程序调用sleep函数时,它会进行系统调用,导致进程从用户模式切换到内核模式。在内核模式下,操作系统接管了执行权,它会将Hello进程挂起(即暂停执行),而不是等待sleep函数的休眠时间结束。相反,内核会开启一个定时器,并在设定的时间到达时触发中断信号。当定时器中断信号触发时,处理器会进入中断处理程序。在中断处理程序中,操作系统会根据调度算法选择另一个就绪状态的进程,并进行上下文切换。这意味着当前Hello进程的上下文信息将被保存,包括程序计数器、寄存器状态和其他相关信息。一旦上下文切换完成,操作系统将恢复另一个进程的上下文,并将控制权转移到该进程,使其开始执行。这个新的进程可能是系统中的另一个用户进程或操作系统内部的进程。在Hello进程被挂起期间,定时器不断运行,触发中断,使得操作系统能够进行多个进程之间的切换,实现并发执行。当Hello进程再次获得执行机会时,操作系统会恢复其上下文信息,并让其继续从上次被中断的地方开始执行。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
- 异常的类别
类别 | 原因 | 异步/同步 | 返回行为 |
---|---|---|---|
中断 | 来自I/O设备的信号 | 异步 | 返回到下一条指令 |
陷阱/系统调用 | 有意的异常 | 同步 | 返回到下一条指令 |
故障 | 潜在可恢复的错误 | 同步 | 可能返回当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
在Hello程序中,当调用`sleep()`函数时,会触发系统调用陷阱,进入内核态执行相应的操作。`sleep()`函数用于让进程暂停执行一段指定的时间。当调用`sleep()`函数时,进程会进入内核态,操作系统会计算时间并进行相应的处理,然后将控制权切换到其他进程上。在指定的时间到达后,I/O设备定时器会发出中断异常,操作系统会捕获该中断并处理,然后将控制权切换回Hello进程,使其继续执行。
- 信号类型:以下的信号大多可以出现,把进程从内核模式切换为用户模式时,会检查进程没被堵塞的待处理的信号,收到信号后会触发并执行信号处理程序。如图6.6.1
- 异常与信号的处理,以输入./hello 2021111056 csh 1为例子
a. 按回车:出现换行如图6.6.2
b. 按ctrl+c:终止程序,如图6.6.3
c. 按ctrl+z:挂起程序,输出提示hello进程Stopped,如图6.6.4
d. 运行ps:给出当前进程情况, hello的PID为4472.如图6.6.5
e. 运行jobs:显示出hello只是被挂起,未终止。如图6.6.6
f. 运行pstree:给出进程树, hello进程的位置如图6.6.7所示:
g. 运行fg:fg指令使得jobs中最后的任务在前台继续运行,因此可以看到hello进程继续运行到结束。如图6.6.8所示
h. 运行kill:向进程ID为4536的进程发送了SIGKILL信号。进程被终止并且在进程列表中不再显示。可以看到运行的hello程序被终止。如图6.6.9所示
6.7本章小结
本章我们介绍了进程的概念和作用,以及Shell作为用户与操作系统之间的接口的处理机制。在Hello程序的执行过程中,我们重点讨论了fork和execve函数的作用,以及异常和信号的处理
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
-
在Hello程序中,我们可以结合逻辑地址、线性地址、虚拟地址和物理地址的概念进行说明。
a. 逻辑地址是由程序生成的与段相关的偏移地址部分。在C语言中,使用指针变量的取址操作(&)可以得到指针变量的值,该值就是逻辑地址,它相对于当前进程的数据段。逻辑地址由段标识符和段内偏移量组成。
b. 线性地址是逻辑地址和物理地址之间的地址。通过将逻辑地址的段地址和偏移地址相加,我们可以得到线性地址。线性地址本质上是一个非负整数地址的有序集合,即{0,1, 2, 3, …}
c. 虚拟地址类似于线性地址,它是虚拟的,不存在于实际的物理内存中。我们可以将虚拟地址理解为经过逻辑地址计算后得到的地址。虚拟地址的大小由字长决定。
d. 物理地址是实际的物理内存地址,即在前端总线上传输的地址,也称为绝对地址。 -
结合Hello程序,反汇编可执行程序后,我们可以得到的地址是段内偏移,例如0x00401000等,这些是虚拟地址。将代码段的地址与虚拟地址相加,我们可以得到线性地址。值得注意的是,在Linux系统中,段的概念被弱化,段基址通常为0。然后,通过虚拟地址和内存管理单元(MMU)的处理,才能得到真实的物理地址。
-
因此,在Hello程序的执行过程中,逻辑地址转换为线性地址,线性地址再经过MMU转换为虚拟地址,最后通过MMU的地址映射机制将虚拟地址转换为物理地址,使得程序能够在实际的物理内存中执行。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理是一种内存管理机制,它将内存划分为若干个大小不同的段,并为每个段分配一个唯一的标识符,称为段号。每个段都可以包含一个连续的逻辑地址空间。
-
基本原理:内存被划分为多个段,每个段具有独立的地址空间。段表是用于映射逻辑地址到物理地址的数据结构,每个段在段表中都有一个条目,记录了段的起始地址和长度等信息。当程序需要访问某个逻辑地址时,通过逻辑地址中的段号找到对应的段表条目。根据段表条目中的信息,将逻辑地址中的段内偏移量与段的起始物理地址相加,得到物理地址。最后,使用得到的物理地址进行内存访问操作。
-
数据结构:段表是一个数据结构,用于存储每个段的信息。每个段表条目包含段的起始地址、段的长度和其他控制信息。如图7.2.1
- 地址变换:当程序生成逻辑地址时,它由两部分组成:段号和段内偏移量。首先,根据段号在段表中查找对应的段表条目。如果找到了对应的段表条目,就可以从中获取段的起始物理地址和长度。将段的起始物理地址与段内偏移量相加,得到物理地址。最后,使用物理地址进行内存访问操作。如图7.2.2
7.3 Hello的线性地址到物理地址的变换-页式管理
Hello的线性地址到物理地址的变换是通过页式管理实现的。
-
页式管理是一种内存管理机制,将内存划分为固定大小的页框,并将逻辑地址空间划分为相同大小的页。每个页都具有唯一的页号。
-
页式管理的原理如下:
a. 将逻辑地址划分为两部分:页号和页内偏移。
b. 通过页表进行地址映射。页表是一个数据结构,用于记录每个页的映射关系。
c. 当程序生成逻辑地址时,从逻辑地址中提取页号和页内偏移。
d. 使用页号查找页表,找到对应的页表条目。
e. 条目中包含了对应页的物理地址的基址和其他控制信息。
f. 页表条目中的物理地址基址与页内偏移相加,得到物理地址。
g. 最后,使用得到的物理地址进行内存访问操作。 -
总结:在Hello的执行过程中,当使用线性地址访问内存时,会先进行页表的查找和地址变换,将线性地址转换为物理地址。这样可以将逻辑地址空间划分为一系列大小相同的页,而不需要连续的物理内存。页式管理提供了更灵活的内存分配和管理方式,可以实现虚拟内存的功能,允许将进程的逻辑地址空间映射到物理内存或磁盘上的页面。这样,程序可以使用连续的逻辑地址空间,而不必关心物理内存的实际分布情况。同时,页式管理也引入了额外的管理开销,如页表的维护和地址变换的开销。
-
页式管理方式的优点:没有外碎片;一个程序不必连续存放;便于改变程序占用空间的大小(主要指随着程序运行,动态生成的数据增多,所要求的地址空间相应增长)。
-
页式管理方式的缺点:要求程序全部装入内存,没有足够的内存,程序就不能执行
7.4 TLB与四级页表支持下的VA到PA的变换
-
在使用TLB(Translation Lookaside Buffer)和四级页表支持的情况下,进行虚拟地址(VA)到物理地址(PA)的变换过程如下:
a. 当程序生成虚拟地址时,先将虚拟地址划分为四部分:页目录索引、页表索引、页内偏移和页内偏移的页面偏移。
b. 首先,从处理器的TLB中查找页表项。TLB是一个高速缓存,用于存储最近访问的页表项,以加快地址转换速度。如果TLB中存在对应的页表项,则跳到步骤 e如果TLB中没有对应的页表项,那么需要进行多级页表的访问。
c. 使用页目录索引从页目录中获取页表的物理地址。
d. 使用页表索引从页表中获取页的物理地址。
e. 将页的物理地址与页内偏移的页面偏移相加,得到最终的物理地址。
f. 将物理地址用于访问内存中的数据。
g. 如果需要更新TLB,将刚才查找到的页表项加载到TLB中,以便下次访问时加速地址转换。 -
TLB的作用是缓存最近使用的页表项,以减少对多级页表的访问次数,从而提高地址转换的速度。当处理器发起内存访问时,首先查找TLB以确定物理地址。如果在TLB中找到对应的页表项,就直接使用该项进行地址转换,避免了对页表的访问。如果在TLB中未命中,就需要通过多级页表进行地址转换。
-
使用多级页表的好处是可以将虚拟地址空间分层管理,不需要将整个页表全部加载到内存,节省了内存空间。同时,多级页表的层次结构使得查找页表项的时间复杂度降低,加快了地址转换的速度。
综上所述,TLB与四级页表共同支持虚拟地址到物理地址的变换,提高了地址转换的效率和内存管理的灵活性。
7.5 三级Cache支持下的物理内存访问
- 在三级Cache支持下的物理内存访问过程如下:(如图7.5.1)
a. 当处理器需要读取或写入物理内存中的数据时,首先检查最内层的一级缓存(L1Cache)。L1 Cache是距离处理器最近的高速缓存,速度非常快,通常是与处理器核心集成在一起的。
b. 如果所需的数据在L1 Cache中命中(即缓存中已经存在),则直接从L1Cache中读取或写入数据,并且该访问被视为缓存命中(Cache Hit)。
c. 如果所需的数据不在L1Cache中(即缓存未命中),则继续检查更大容量的二级缓存(L2 Cache)。
d. 如果数据在L2 Cache中命中,则从L2Cache中读取或写入数据,并将数据传递给L1Cache。这样,下次相同的数据访问就可以在L1Cache中命中,提高访问速度。
e. 如果数据在L2 Cache中未命中,则继续检查更大容量的三级缓存(L3Cache)。
f. 如果数据在L3 Cache中命中,则从L3Cache中读取或写入数据,并将数据传递给L2 Cache和L1Cache。这样,下次相同的数据访问可以在L1Cache中命中或在L2Cache中命中。
g. 如果数据在L3Cache中未命中,则需要从主存(物理内存)中读取或写入数据。这个过程称为缓存未命中(CacheMiss)。
h. 在缓存未命中的情况下,处理器会向内存控制器发出请求,将数据从主存加载到L3Cache、L2 Cache和L1 Cache中的相应位置。
i. 一旦数据加载到缓存中,处理器就可以从缓存中读取或写入数据,并且将缓存中的数据与主存中的数据保持一致。
-
三级Cache的存在使得处理器在访问内存时可以利用多级缓存层次结构,从而减少对主存的访问次数,提高数据访问速度。不同级别的缓存具有不同的容量和访问速度,最内层的L1 Cache速度最快但容量较小,而最外层的L3 Cache容量较大但速度相对较慢。
-
通过层层缓存的访问过程,处理器可以在不频繁地访问主存的情况下,从更接近处理器的缓存中快速获取所需的数据,提高了数据访问效率和整体系统性能。
7.6 hello进程fork时的内存映射
-
当`hello`进程执行`fork()`系统调用时,会创建一个新的子进程,该子进程与父进程共享相同的内存映射。(如图7.6.1)
-
父进程的内存映射:
a. 代码段:包含`hello`程序的可执行指令。
b. 数据段:包含全局变量和静态变量。
c. 堆:用于动态分配内存的区域,通过`malloc()`等函数进行管理。
d. 栈:用于函数调用和局部变量的区域。 -
子进程的内存映射:
a. 代码段:与父进程相同,共享相同的可执行指令
b. 数据段:与父进程相同,共享相同的全局变量和静态变量。
c. 堆:与父进程相同,共享相同的堆空间。
d. 栈:子进程会创建一个独立的栈空间,用于函数调用和局部变量。 -
在`fork()`调用完成后,父进程和子进程拥有相同的代码段和数据段,但是在堆和栈方面有一些差异。子进程会复制父进程的堆和栈的内容,但是它们是独立的,修改一个进程的堆或栈不会影响到另一个进程。虽然父进程和子进程共享相同的内存映射,但它们拥有各自独立的虚拟地址空间。这意味着它们在使用相同的内存地址时,实际上指向不同的物理内存。这种写时复制(Copy-on-Write)的机制使得父进程和子进程可以独立地进行修改,避免了不必要的内存复制操作。
7.7 hello进程execve时的内存映射
- 在`hello`进程调用`execve()`函数加载并运行新的可执行文件时,涉及以下大致的内存映射步骤:
a. 清除原有的内存映射:在执行`execve()`之前,旧的内存映射会被清除,包括代码段、数据段、堆和栈等。旧的可执行文件的内容将被替换为新的可执行文件。
b. 加载新的可执行文件:`execve()`函数会根据传入的可执行文件路径,加载新的可执行文件到内存中。这包括将可执行文件的代码段、数据段和其他段加载到适当的内存位置。
c. 设置新的内存映射:执行`execve()`后,操作系统会根据新的可执行文件的格式和需要的内存资源,为该进程创建新的内存映射。这包括代码段、数据段、堆、栈以及其他可能的段。
d. 初始化程序状态:在加载新的可执行文件后,操作系统会初始化程序的状态,包括设置程序计数器(PC)指向新的代码段入口点,设置堆栈指针(SP)等。这样,执行流将转移到新的可执行文件的入口点开始执行。
7.8 缺页故障与缺页中断处理
- 缺页故障是指当访问虚拟地址的页面不在内存中时发生的异常情况。当MMU(内存管理单元)尝试将虚拟地址翻译为物理地址时,发现对应的页表项(Page TableEntry,PTE)不存在或者无效,就会引发缺页异常。下面是缺页故障的处理过程:
a. 缺页异常触发:当MMU尝试访问虚拟地址A对应的页面时,发现该页面的PTE不存在或者无效,触发缺页异常。
b. 异常处理程序调用:在发生缺页异常时,操作系统内核会调用缺页异常处理程序来处理该异常。
c. 异常处理程序执行:缺页异常处理程序首先会检查虚拟地址是否合法,包括地址是否在进程的地址空间范围内。如果虚拟地址非法,则触发段错误,导致程序终止。
d. 选择牺牲页及页面置换:如果选择的牺牲页已经被修改过(脏页),需要将其写回到磁盘交换空间。然后,从磁盘中将新的页面换入内存,并更新页表中的对应项。
e. 更新页表:在牺牲页替换和新页面换入后,异常处理程序会更新页表中对应虚拟地址的PTE,使其指向新的物理页面。
f. 重新执行引发缺页的指令:异常处理程序将控制权交还给引发缺页的指令所在的进程,并重新执行该指令。由于现在已经将页面加载到内存中,再次访问该虚拟地址就能正常找到页面。
7.9 动态存储分配管理
- 动态内存管理的基本方法与策略如下:
a. 放置已分配块:当应用程序请求分配一块内存时,动态内存分配器会搜索空闲链表,寻找一个足够大的空闲块来放置所请求的内存块。
b. 分割空闲块:当找到一个足够大的空闲块时,需要决定将其分割成多大的块来满足请求。可以选择将整个空闲块分配给请求,但这会导致内部碎片。另一种策略是将空闲块分割为两部分,一部分用于分配给请求,剩余部分变成新的空闲块。
c. 获取额外的堆内存:如果分配器无法找到足够大的空闲块来满足请求,它会通过调用sbrk函数向内核请求额外的堆内存。分配器会将这个额外的内存转换为一个大的空闲块,并将其插入到空闲链表中,然后将请求的内存放置在这个新的空闲块中。
d. 合并空闲块:分配器释放一个已分配的内存块时,它会合并相邻的空闲块。合并的时机可以选择立即合并或推迟合并。合并过程将当前空闲块与前面和后面的空闲块合并成一个更大的空闲块,以减少内存碎片。
7.10 本章小结
本节介绍了不同地址空间的概念与关系,以及逻辑地址到虚拟地址再到物理地址的转换过程。重点讨论了段式管理和页式管理的原理与实现,包括MMU、TLB、PTE和Cache等关键概念。我们还解释了fork和execve函数在内存映射中的作用,以及动态内存分配管理的机制与策略。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的I/O设备都被模型化为文件,而所有的输入和输出都被当作相应文件的读和写来完成,这种将设备优雅的映射为文件的方式,允许Linux内核引出一个简单的,低级的应用接口,称为UnixI/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
-
`open`:打开文件,将文件名转换为文件描述符,并返回一个小的非负整数作为描述符,用于标识该文件。通过记录描述符,程序可以管理打开的文件
-
`close`:关闭文件,释放文件相关的数据结构和内存资源,并将描述符返回给可用的描述符池。在进程终止时,内核会自动关闭所有打开的文件。
-
`read`:从文件中读取数据,将最多n个字节复制到指定的内存位置buf,并返回实际传送的字节数。返回值-1表示错误,0表示文件结束(EOF)。
-
`write`:向文件中写入数据,将最多n个字节从内存位置buf复制到文件的当前位置,并返回实际写入的字节数。
这些函数通过文件描述符来操作文件,可以进行文件的打开、读取、写入和关闭等操作。它们提供了对文件的基本控制和数据处理能力,使程序能够进行文件的操作和数据的传输。UnixI/O接口是Unix-like操作系统中常用的文件操作接口,被广泛应用于Unix、Linux和类Unix系统。
8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int
0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
- 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获得输出时格式化对应的值,然后看vsprinf函数,vsprinf函数利用格式fmt以及上一步获得的args参数,返回的是要打印出来的字符串的长度。然后是write函数:
write:
mov eax,NR write
mov ebx,[esp+4]
mov ecx,[esp+8]
int INT VECTOR SYS CALL
此函数可以将buf输出,再通过syscall函数将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
8.4 getchar的实现分析
(异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。)
- Getchar()函数如下:
int getchar(void){
static char buf[BUFSIZ];
static char*bb=buf:
static int n=0;
if(n==0){
n=read(0,buf,BUFSIZ);
bb=buf;
}
return (--n>=0)?(unsigned char)*bb++:EOF;
}
-
getchar函数在底层实现中通常会使用系统函数read来从标准输入(键盘)读取数据。
-
当用户按下键盘时,键盘接口会生成一个键盘扫描码,并产生一个中断请求。操作系统会响应这个中断请求,并调用相应的中断处理程序(键盘中断子程序)。键盘中断子程序会从键盘接口获取键盘扫描码,并将其转换成对应的ASCII码。然后,它会将ASCII码存储到系统的键盘缓冲区中。
-
getchar函数会从键盘缓冲区中读取一个字符,并将其返回。如果读取失败或没有可用字符,则返回EOF(通常为-1),表示读取结束或出现误。
-
通过这种方式,异步异常(键盘中断)的处理机制使得程序能够实时响应用户的键盘输入,并将输入的字符传递给相应的函数进行处理。这样,用户可以与程序进行交互,进行输入和输出操作。
8.5 本章小结
本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了printf 函数和 getchar 函数。
(第8章1分)
结论
-
编写代码:我们使用编程语言编写Hello程序,其中包含了一条指令,用于输出"Hello,World!"。
-
编译:完成代码编写后,我们需要将Hello程序交给编译器进行处理,将源代码转换为机器码文件,以便计算机能够理解和执行。
-
链接:编译后的机器码文件可能需要与其他依赖的库文件进行链接,以生成最终的可执行文件,使得程序能够在计算机上独立运行。
-
加载:操作系统负责将可执行文件加载到内存中,为Hello程序分配所需的运行资源,例如内存空间和其他必要的系统资源。
-
运行:Hello程序开始在处理器上执行,处理器按照指令的顺序逐步执行程序中的操作,包括计算、跳转和存储等。
-
内存映射:在程序执行过程中,可能涉及到对内存的读取和写入操作。此时,计算机系统将逻辑地址转换为物理地址,以便程序能够正确地访问内存中的数据。
-
IO操作:Hello程序可能需要进行输入和输出操作,例如使用printf函数将"Hello,World!"输出到标准输出设备,如终端。这样用户就可以看到程序输出的结果。
-
终止:当Hello程序执行完所有指令后,它会正常终止,并释放占用的资源。
感悟:计算机系统的复杂性和精妙之处常常会让我感到惊讶和敬畏。一个简单的helloworld是如此的复杂。学习计算机系统能够帮助我们深入了解计算机的工作原理、底层架构和运行机制,从而更好地理解和应用计算机科学和技术。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
Hello.c | hello源程序 |
---|---|
Hello.i | hello的预处理结果 |
Hello.s | hello.i的汇编结果 |
Hello.o | hello.s翻译成的可重定位文件 |
Hello | 可执行文件 |
Helloo.elf | hello.o的ELF格式文件 |
Hello.obj | hello的反汇编结果 |
Hello.elf | hello的ELF格式文件 |
(附件0分,缺失 -1分)
参考文献
[1] Randal E.Bryant,David R.O’Hallron.深入理解计算机系统(原书第3版).北京:机械工业出版社,2016.7
[2] https://stackoverflow.com.
[3] https://blog.csdn.net
[4] https://www.cnblogs.com/pianist/p/3315801.html
[5] https://www.cnblogs.com/shijiezhenmei/p/3659346.html
[6] https://zhuanlan.zhihu.com/p/467232104
(参考文献0分,缺失 -1分)