计算机系统大作业 程序人生-Hello’s P2P 2024秋

                                      

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业  人工智能模块(2+X领域)

学     号  2022113217            

班     级  22WL021              

学       生  叶子钦               

指 导 教 师  刘宏伟                

计算机科学与技术学院

2024年5月

摘  要

本论文旨在全面解析一个简单的"Hello, World!"程序在Linux操作系统中的生命周期。研究内容包括源代码的编写、预处理、编译、汇编、链接,直至生成可执行文件的全过程。通过实际操作和分析,论文揭示了计算机系统如何将文本文件转化为可执行程序,并深入探讨了进程管理、存储管理和I/O管理等关键操作系统概念。

本研究采用实践操作和系统命令分析的方法,对每个阶段生成的中间文件进行了详细解读。例如,通过使用gcc编译器和gdb调试器等工具,展示了程序从源代码到机器语言指令的转换,以及操作系统如何通过内存管理单元(MMU)和页表进行地址转换和管理。

论文的成果不仅加深了对程序编译和运行过程的理解,而且通过分析printf和getchar函数的实现,展示了系统调用和硬件交互的工作机制。此外,通过"hello"程序的执行案例,论文详细阐述了进程创建、内存映射、动态链接和信号处理等系统行为。

研究的理论意义在于提供了计算机系统核心工作原理的深刻洞察,而实际意义则体现在对软件开发流程的全面掌握,为深入学习操作系统和编程语言提供了坚实的基础。

关键词:计算机系统;程序生命周期;编译系统;进程管理;Linux环境;

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分

目  录

第1章 概述... - 4 -

1.1 Hello简介... - 4 -

1.2 环境与工具... - 5 -

1.3 中间结果... - 5 -

1.4 本章小结... - 5 -

第2章 预处理... - 6 -

2.1 预处理的概念与作用... - 6 -

2.2在Ubuntu下预处理的命令... - 6 -

2.3 Hello的预处理结果解析... - 7 -

2.4 本章小结... - 8 -

第3章 编译... - 9 -

3.1 编译的概念与作用... - 9 -

3.2 在Ubuntu下编译的命令... - 9 -

3.3 Hello的编译结果解析... - 10 -

3.4 本章小结... - 16 -

第4章 汇编... - 17 -

4.1 汇编的概念与作用... - 17 -

4.2 在Ubuntu下汇编的命令... - 17 -

4.3 可重定位目标elf格式... - 18 -

4.4 Hello.o的结果解析... - 21 -

4.5 本章小结... - 23 -

第5章 链接... - 24 -

5.1 链接的概念与作用... - 24 -

5.2 在Ubuntu下链接的命令... - 24 -

5.3 可执行目标文件hello的格式... - 25 -

5.4 hello的虚拟地址空间... - 29 -

5.5 链接的重定位过程分析... - 29 -

5.6 hello的执行流程... - 31 -

5.7 Hello的动态链接分析... - 32 -

5.8 本章小结... - 32 -

第6章 hello进程管理... - 33 -

6.1 进程的概念与作用... - 33 -

6.2 简述壳Shell-bash的作用与处理流程... - 33 -

6.3 Hello的fork进程创建过程... - 34 -

6.4 Hello的execve过程... - 35 -

6.5 Hello的进程执行... - 35 -

6.6 hello的异常与信号处理... - 36 -

6.7本章小结... - 38 -

第7章 hello的存储管理... - 39 -

7.1 hello的存储器地址空间... - 39 -

7.2 Intel逻辑地址到线性地址的变换-段式管理... - 39 -

7.3 Hello的线性地址到物理地址的变换-页式管理... - 40 -

7.4 TLB与四级页表支持下的VA到PA的变换... - 41 -

7.5 三级Cache支持下的物理内存访问... - 42 -

7.6 hello进程fork时的内存映射... - 43 -

7.7 hello进程execve时的内存映射... - 43 -

7.8 缺页故障与缺页中断处理... - 44 -

7.9动态存储分配管理... - 45 -

7.10本章小结... - 46 -

第8章 hello的IO管理... - 47 -

8.1 Linux的IO设备管理方法... - 47 -

8.2 简述Unix IO接口及其函数... - 48 -

8.3 printf的实现分析... - 48 -

8.4 getchar的实现分析... - 51 -

8.5本章小结... - 52 -

结论... - 53 -

附件... - 54 -

参考文献... - 55 -

第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

P2P的过程就是从文本文件到可执行文件的过程,hello.c的源程序(即文本)program,经过预处理器(cpp)修改成为hello.i文件,接着通过编译器成为汇编程序,然后通过汇编器转化为可重定位目标文件hello.o,最后通过链接器变为我们所熟知的可执行目标程序hello,通过shell fork和execve产生process。

020:从无到有到无,在shell调用fork execve产生进程后,程序映射相应的虚拟内存,在经过程序入口和main函数后运行结束,程序终止,父进程shell回收子进程,原来占据的内存也全被释放。

编写程序:首先,程序员会使用计算机系统的编辑器(Editor)编写一个名为hello.c的程序。这个程序是一个简单的Hello World程序,用来向用户输出"Hello, World!"。

预处理:接下来,程序会经过预处理(Preprocessing)阶段。在这个阶段,预处理器会处理程序中的预处理指令,例如#include和#define,将它们替换为实际的代码。

编译:预处理完成后,程序会被编译器(Compiler)编译成机器语言。编译器将程序翻译成计算机能够理解和执行的指令集。

汇编:编译完成后,程序会被汇编器(Assembler)处理。汇编器将编译器生成的机器语言指令转换成可执行的机器码。

链接:汇编完成后,程序会被链接器(Linker)处理。链接器将程序中引用的外部函数和库文件与程序的目标文件进行链接,生成最终的可执行文件。

进程管理:在操作系统(OS)中,可执行文件被加载到内存中,并由操作系统创建一个进程(Process)来运行程序。操作系统为进程分配资源,如CPU时间片、内存空间和I/O设备等。

执行:一旦进程被创建,操作系统会按照程序的指令顺序执行程序。程序会利用CPU、内存和I/O设备等硬件资源进行计算和操作。

存储管理:操作系统与内存管理单元(MMU)一起工作,将程序中的虚拟地址(Virtual Address)转换为物理地址(Physical Address)。操作系统使用页表、缓存和页面交换等技术来管理内存,以提高程序的性能。

I/O管理:程序可能需要与外部设备进行交互,如键盘、主板、屏幕等。操作系统负责管理和调度这些I/O操作,并处理来自外部设备的中断和信号。

结束:当程序执行完毕或被终止时,操作系统会回收进程所占用的资源,并将进程从内存中移除。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

软硬件环境:

操作系统:Ubuntu 16.04 LTS 64位 ,开发环境:Visual Studio Community 2017 ,模拟器:Vmware 14 ,硬件环境:X64 CPU,2GHz,2G RAM,256G HD Disk

开发与调试工具:,文本编辑器:vim ,编译器:gcc ,汇编器:as ,链接器:ld ,调试器:edb ,反汇编器:readelf ,二进制编辑器:HexEdit ,调试工具:GDB/OBJDUMP ,图形化调试工具:DDD/EDB

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

hello.c :hello源代码

hello.i :预处理后得到的文本文件

hello.s :编译后得到的汇编语言文件

hello.o :汇编后得到的可重定位目标文件

hello.elf :用readelf读取 hello.o得到的 ELF 格式信息

hello.asm :反汇编 hello.o 得到的反汇编文件

hello2.elf :由hello 可执行文件生成的.elf文件

hello2.asm :反汇编 hello 可执行文件得到的反汇编文件

1.4 本章小结

本章详细阐述了Hello程序的生命周期,包括编写、编译、链接、执行等关键步骤,并介绍了编写论文过程中所依赖的软硬件环境和开发调试工具,为理解软件开发流程提供了全面视角。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理的概念

预处理是编译过程中的一项重要步骤,它在编译器开始编译代码之前进行。预处理器的主要作用是处理源代码中以 # 开头的指令,这些指令不会被编译器直接翻译成机器码,而是会被预处理器进行处理。

预处理指令主要包括以下几类:

宏定义: 使用 #define 指令定义宏,可以将一段代码替换成一个简短的名称,方便代码的编写和维护。

条件编译: 使用 #ifdef、#ifndef、#else、#endif 等指令控制代码的编译,可以根据不同的编译环境编译不同的代码。

文件包含: 使用 #include 指令将其他文件包含到当前文件中,方便代码的重用。

其他指令: 还有一些其他的预处理指令,例如 #pragma、#error 等,用于控制编译过程或输出错误信息。

预处理的作用

预处理的作用主要包括:

简化代码: 通过宏定义可以简化代码,提高代码的可读性和可维护性。

提高代码的可移植性: 通过条件编译可以根据不同的编译环境编译不同的代码,提高代码的可移植性。

方便代码的重用: 通过文件包含可以方便代码的重用,减少代码的重复编写。

控制编译过程: 通过其他预处理指令可以控制编译过程,例如输出错误信息或进行一些特殊的处理。

2.2在Ubuntu下预处理的命令

在 Ubuntu 下,可以使用 gcc 编译器进行预处理。gcc 编译器提供了 -E 选项,可以只进行预处理而不进行编译和链接。

以下是一些使用 gcc 进行预处理的命令示例:

预处理一个名为 hello.c 的文件,并将预处理后的结果输出到 hello.i 文件:

gcc -E hello.c -o hello.i

预处理一个名为 hello.c 的文件,并将预处理后的结果输出到标准输出:

gcc -E hello.c

预处理一个名为 hello.c 的文件,并定义一个名为 DEBUG 的宏:

gcc -E -DDEBUG hello.c

预处理一个名为 hello.c 的文件,并包含一个名为 header.h 的头文件:

gcc -E -I/path/to/header.h hello.c

注意:

-o 选项用于指定输出文件。

-D 选项用于定义宏。

-I 选项用于指定头文件搜索路径。

图1 预处理过程

2.3 Hello的预处理结果解析

预处理阶段生成的hello.i文件扩展至3092行,其内容由原始hello.c文件与多个库文件的合并构成,文件中间部分包含了内部函数的声明,随后是stdio.h、unistd.h和stdlib.h头文件的完整源代码,而原始程序的代码位于文件的最后部分。

展开的具体流程概述如下(以stdio.h为例):CPP先删除指令#include<stdio.h>,并到 Ubuntu系统的默认的环境变量中寻找 stdio.h,最终打开路径/usr/include/stdio.h 下的 stdio.h文件。若 stdio.h 文件中使用了#define 语句,则按照上述流程继续递归地展开,直到所有#defne语句都被解释替换掉为止。除此之外,CPP还会进行删除程序中的注释和多余的空白字符等操作,并对一些值进行替换。

图2 预处理结果

2.4 本章小结

本章着重阐述了预处理的定义及其重要性,同时深入分析了hello程序预处理后的成果,即hello.i文件的结构。预处理不仅仅是文本的扩展,更是对文本的完善,确保了最终代码能够在操作系统环境中顺利执行。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译是指将用高级编程语言编写的源代码转换成机器语言的过程。机器语言是计算机能够直接理解和执行的指令集,而高级编程语言则是人类更容易理解和使用的语言。

编译器的作用:

1. 将高级语言代码翻译成机器语言: 编译器会分析源代码,将其语法和语义转化为计算机能够理解的指令。

2. 优化代码: 编译器会对代码进行优化,例如删除冗余代码、调整指令顺序等,以提高程序的执行效率。

3. 生成可执行文件: 编译器最终会生成一个可执行文件,包含了经过翻译和优化的机器语言指令,以及程序运行所需的资源。

编译的必要性:

 计算机只能理解机器语言: 计算机无法直接理解高级编程语言,需要将其转换成机器语言才能执行。

 提高程序执行效率: 编译器可以对代码进行优化,生成更高效的机器语言指令,从而提高程序的执行速度。

 增强程序的可移植性: 编译器可以将代码编译成不同平台的机器语言,从而使程序能够在不同的计算机系统上运行。

3.2 在Ubuntu下编译的命令

Linux下使用gcc编译的命令为:

gcc -S hello.i -o hello.s

图3 编译过程

3.3 Hello的编译结果解析

 

图4 编译结果

3.3.1 数据

在 hello.s中,涉及的整数有:

1 int sleepsecs

查看C语言文件可知,sleepsecs为int型全局变量,已被初始化赋值

在编译过程中生成的`hello.s`文件里,编译器在`.text`段定义了`sleepsecs`作为全局可访问的变量。在`.type`段中,它被指定为`object`类型的数据。而在`.size`段,该变量的尺寸被声明为4字节,并且被初始化为值2。具体实现细节如下所示:

图4 sleepsecs的情况

2 int i

编译器负责将局部变量`i`存储于寄存器或栈帧中。由于`i`是函数内部的局部变量,它不会在文件的任何实际节中占用空间,而是在程序执行期间存在于运行时的栈上。对`i`的所有操作都是直接在寄存器或栈上执行的。

在`hello.s`汇编文件中,我们观察到局部变量`i`被分配了4字节的内存空间。这表明编译器为`i`在栈上预留了相应的存储位置,以便在函数执行过程中保存和修改其值。

图5 i的情况

3 立即数

立即数在汇编语言中以$x来表示,如立即数4表示为$4。

3.3.2 赋值

       hello.c中的赋值操作主要通过MOV类指令实现,其形式如图所示:

图6 MOV类指令操作

3.3.3 类型转换

       在C语言源代码中,执行了一个隐式类型转换操作:(int sleepsecs = 2.5)将浮点数值2.5赋给了一个整型(`int`)变量`sleepsecs`。在这个转换过程中,浮点数的小数部分被截断,导致值2.5向零方向舍入,最终结果为整数2。

3.3.4 算术操作

       汇编语言中,算术操作包括如下:

图7 算术操作表

在hello.s中,具体涉及的算数操作包括:

1.subq $32,%rsp: 开辟栈帧

2. addg $16,%rax:  修改地址偏移量

3.addl $1,-4(%rbp):  实现 i++的操作

3.3.5 逻辑/位操作

逻辑操作和位操作是计算机编程中对数据进行处理的两种基本运算方式,它们在底层硬件和高级编程语言中都有广泛应用。

逻辑操作主要涉及布尔代数的基本原则,包括以下几种基本操作:

 AND(与):当两个比较的位都是1时,结果为1。

 OR(或):当两个比较的位中至少有一个是1时,结果为1。

 NOT(非):反转所有的位,将1变为0,将0变为1。

 NOR(或非):或操作后取反。

逻辑操作通常用于控制程序流程,如条件判断和循环控制。

位操作则直接对整数的二进制位进行操作,包括:

 位与(&):两个操作数中相对应位都为1时,结果位才为1。

 位或(|):两个操作数中相对应位至少有一个为1时,结果位为1。

 位非(~):反转操作数的每一位。

 位异或(^):两个操作数的对应位相同为0,不同为1。

 位左移(<<):将位模式向左移动指定的位数,左边超出的位被丢弃,右边低位补0。

 位右移(>>):将位模式向右移动指定的位数,右边超出的位被丢弃,左边高位根据符号位填充。

3.3.6 关系操作

在hello.s中,具体涉及的关系操作包括:

1. argc!=3:

检查 argc是否不等于3。在hello.s中,使用 cmpl$3,-20(%rbp),比较 argc

与3的大小并设置条件码,为下一步je利用条件码进行跳转作准备。

图8 检查argc!=3

2. i<10:

检查i是否小于10。在hello.s中,使用cmpl $9,-4(%rbp)比较i与9的

大小,然后设置条件码,为下一步jle利用条件码进行跳转做准备。

图9 检查i<10

3.3.7 数组操作

在`int main(int argc, char *argv[])`函数定义中,参数`argv`是一个指向字符指针数组的指针,该数组包含了输入字符串的地址。参数`argc`表示这些输入字符串的数量。

如图10展示的那样,程序通过传递`argv[1]`和`argv[2]`给`printf`函数,以及传递`argv[3]`给`atoi`函数,来实现参数的传递。在访问数组元素时,采用基地址加上偏移量的方式来定位数组中的具体元素。可以观察到,`argv`数组的首地址存储在寄存器`rsi`中,而`argc`的值存储在寄存器`rdi`中。在栈帧中,`argv`数组的首地址可以通过从`%rbp`减去32字节获得,而`argc`可以通过从`%rbp`减去20字节获得。通过这种方式,可以访问`argv[1]、argv[2]和argv[3]。

图10 数组操作

3.3.8 控制转移

程序中控制转移的具体表现有两处:

1. if(argc!=3):

当argc不等于3时,执行函数体内部的代码。在hello.s中,使用cmpl

$3,-20(%rbp),比较 argc与3是否相等,若相等,则跳转至.L2,不执行

后续部分内容;若不等则继续执行函数体内部对应的汇编代码。

图11 控制转移

2. for(i=0;i<10;i++)

当i<10 时进行循环,每次循环i++。在hello.s中,使用cmpl $9,-4(%rbp)

比较i与9是否相等,在i<=9时继续循环,进入.L4,i>9 时跳出循环。

图12 循环的情况

3.3.9 函数操作

       在图13中,`hello.c`程序依次执行了对`printf`、`atoi`和`sleep`函数的调用。在这个过程中,`argv[1]`和`argv[2]`这两个参数被分别加载到寄存器`rsi`和`rdx`中。而`argv[3]`的值则被放入寄存器`rdi`。接着,`atoi`函数的返回值,它存储在寄存器`rax`中,被用作`sleep`函数的参数,再次加载到寄存器`rdi`中以完成调用。这个过程展示了程序在运行时如何通过寄存器传递函数参数。

图13 函数操作

3.4 本章小结

编译是将高级编程语言代码转换成机器语言的过程,是计算机系统运行程序的关键步骤。编译器通过翻译、优化和生成可执行文件,使程序能够被计算机理解和执行,并提高程序的效率和可移植性。

原始C程序定义了一个接受命令行参数的main函数,该函数首先检查参数数量是否正确,随后进入一个循环,循环体内打印信息并根据提供的秒数暂停程序执行。

汇编代码部分,我们注意到以点(.)开头的指令主要是汇编器和链接器的伪操作指令,它们指导编译过程但不影响最终程序的执行逻辑。例如,.file指明了源文件名,.text和.rodata分别代表代码段和只读数据段,.globl声明了全局可见的符号,而.type和.align则分别指明了符号的类型和数据的对齐方式。

此外,我们注意到汇编代码中的字符串常量使用了转义序列表示,这在解释时需要特别注意字符编码的问题。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

汇编语言是一种低级编程语言,它与机器语言非常接近,但比机器语言更易于理解和使用。以下是对汇编概念和作用的简要概述:

概念

汇编语言:是一种用助记符表示的机器语言,它提供了一种人类可读的方式来编写机器指令。

助记符:是汇编语言中用于表示机器指令的简短文本符号,它们被汇编器转换成对应的机器码。

作用

1. 接近硬件:汇编语言允许程序员直接控制硬件,进行位级操作和寄存器级别的编程。

2. 性能优化:由于其接近硬件的特性,汇编语言可以用来编写性能关键部分的代码,以实现优化。

3. 系统编程:在操作系统、驱动程序等系统软件的开发中,汇编语言是必不可少的。

4. 硬件接口:用于编写与硬件接口直接交互的代码,如初始化代码和中断处理程序。

5. 教学和学习:汇编语言是理解计算机体系结构和低级操作原理的重要工具。

6. 特定应用:在需要特定硬件操作或特殊性能要求的应用中,汇编语言提供了必要的控制。

4.2 在Ubuntu下汇编的命令

Linux下使用gcc编译的命令为:

gcc -m64 -no-pie -fno-PIC -c -o hello.o hello.s

图14 汇编过程

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位

项目分析。

首先,在shell中输入readelf -a hello.o>hello.elf指令获得 hello.o 文件的ELF格式:

图15 ELF格式生成

其结构分析如下:

  1. ELF头

以16字节的Magic序列作为起始,该序列提供了关于创建文件的系统所使用的字大小和字节序的信息。ELF(可执行和可链接格式)头的其余部分包含了辅助链接器进行语法分析和目标文件解释的数据。这些数据包括ELF头部自身的大小、目标文件的类别、机器架构类型、节头表在文件中的偏移量,以及节头表中条目的尺寸和总数等关键信息。

图16 ELF头情况

  1. 节头

包含了文件中出现的各个节的意义,包括节的类型、位置和大小等信息.

图17 节头

  1. 重定位节.rela.text

在`.text`节中,存在一个位置列表,该列表记录了需要在重定位过程中进行调整的`.text`节内的信息。在链接器将这个目标文件与其它文件合并时,这些位置将被更新。

具体来说,这里列出了8项重定位信息,它们分别对应于:`.L0`(第一个`printf`调用中使用的字符串)、`puts`函数、`exit`函数、`L1`(第二个`printf`调用中使用的字符串)、`printf`函数、变量`sleepsecs`、`sleep`函数以及`getchar`函数。这些声明指出了在链接过程中需要对这些引用进行重定位的点。

图18 节.rela.text

  1. 重定位节.rela.eh frame

图19 重定位节.rela.eh frame

  1. 符号表

符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明.

图20 符号表情况

4.4 Hello.o的结果解析

使用objdump -d -r hello.o > hello.asm 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

通过对比hello.asm与hello.s可知,两者在如下地方存在差异:

1.分支转移:

在 hello.s中,跳转指令的目标地址直接记为段名称,如.L2,.L3等。而在

反汇编得到的 hello.asm中,跳转的目标为具体的地址,在机器代码中体现

为目标指令地址与当前指令下一条指令的地址之差。

图21 分支转移

2.函数调用

在`hello.s`汇编源文件中,`call`指令后面直接跟随的是被调用函数的名称。然而,在从二进制文件反汇编得到的`hello.asm`文件中,`call`指令指向的是紧随其后的指令地址。这种情况发生的原因是,在`hello.c`源代码中所调用的函数均来自于共享库,它们的确切运行时地址需要在程序运行时通过动态链接器来解析。

在汇编编译成机器码的过程中,对于那些地址尚未确定的函数调用,`call`指令的相对地址被设置为全零(这时目标地址正好是下一条指令)。接着,在`.rela.text`节中为这些调用添加了重定位条目,以便在后续的静态链接阶段能够准确地确定它们的地址。

图22 函数调用

3.全局变量访问

在`hello.s`文件中,访问`rodata`段(例如`printf`函数中的字符串)时,使用的是段名称结合`%rip`寄存器的方式。相对地,在反汇编生成的`hello.asm`文件中,访问`rodata`段时使用的是`0+%rip`的形式。这背后的原理与函数调用的重定位机制相似,因为在运行时才能确定`rodata`段中数据的确切地址,所以访问这些数据同样需要进行重定位。

在汇编代码转换成机器码的过程中,对于这些待确定地址的数据访问,操作数被暂时设置为0,并使用`%rip`寄存器来计算实际的地址。同时,为了在链接时能够正确解析这些地址,会在`.rela.rodata`节中为这些数据访问添加相应的重定位条目。这样,在程序的最终链接阶段,链接器会根据这些重定位信息来修正数据访问指令,确保它们指向正确的地址。

图23 全局变量访问

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

4.5 本章小结

本章描述汇编语言的基本概念及其重要性,并在Ubuntu操作系统上进行了实际的操作演示。通过将hello.s汇编源文件编译成hello.o目标文件,并进一步生成了hello.o的ELF格式文件hello.elf,本章深入分析了ELF文件格式的结构特点。

此外,本章还通过对比hello.o的反汇编代码(存储于hello.asm文件中)与原始的hello.s汇编代码,揭示了汇编语言与机器语言之间的相似性和差异。这一过程不仅加深了对汇编语言特性的理解,也增进了对计算机底层工作原理的认识。

(第41分)

第5章 链接

5.1 链接的概念与作用

链接是程序编译过程中的一个重要环节,它的作用是将多个编译后生成的目标文件(.o文件)或者库文件组合成一个单一的可执行文件或库文件。以下是链接的概念与作用的概述:

 概念

链接:是将编译生成的多个目标文件和库文件进行合并,解决它们之间的依赖关系,生成最终可执行文件或库文件的过程。

目标文件:编译源代码后生成的中间文件,包含了源代码转换为的机器指令,但尚未形成最终的可执行文件。

库文件:一组已经编译好的目标文件的集合,可以被多个程序共享使用。

作用

1. 解决外部引用:链接器解析程序中的外部符号引用,将它们与定义这些符号的目标文件或库文件连接起来。

2. 地址和空间分配:链接器为程序中的代码和数据分配内存地址和空间。

3. 生成可执行文件:将分散的目标文件合并成一个可执行文件,使得程序可以被操作系统加载和执行。

4. 符号解析:链接器处理程序中的符号(变量和函数名)的可见性和作用域问题。

5. 优化程序性能:通过优化代码和数据的布局,链接器有助于提高程序的加载速度和运行效率。

6. 动态链接:支持程序在运行时动态加载和链接库文件,使得程序更加灵活和模块化。

7. 错误检测:链接过程中可以发现一些编译时未检测到的错误,如未定义的引用或不一致的符号定义。

5.2 在Ubuntu下链接的命令

在 Ubuntu 下链接的命令如下:

ld -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 \

/usr/lib/gcc/x86_64-linux-gnu/11/crtbegin.o \

hello.o \

-lc \

/usr/lib/gcc/x86_64-linux-gnu/11/crtend.o \

/usr/lib/x86_64-linux-gnu/crtn.o \

-z relro -o hellorelro -o hello

链接过程如下:

图24 链接过程

5.3 可执行目标文件hello的格式

在Shell 中输入命令 readelf -a hello>hello2.elf生成hello 程序的 ELF 格式文件,保存为he11o2.elf(与第四章中的elf文件作区分)

图25 生成ELF文件

  1. ELF

`hello2.elf`文件中的ELF头与`hello.elf`文件中的ELF头在信息类型上大致相同,都是以一个16字节的Magic序列开始,这个序列定义了生成文件的系统的字大小和字节顺序。紧接着的部分包含了对链接器进行语法分析和目标文件解释至关重要的信息。与`hello.elf`相比,`hello2.elf`在基本信息方面(例如Magic值和文件类别等)保持不变,但在某些方面有所差异:文件类型有所改变,程序头的尺寸增加,节头表中的条目数量也有所增加,此外,`hello2.elf`还获得了一个入口地址。

图26 ELF头情况

2.节头

`hello2.elf`文件的节头部分详细描述了文件中所有节的语义特性,涵盖了节的类型、所在位置、文件中的偏移以及节的尺寸等关键信息。相较于`hello.elf`,在经过链接过程之后,`hello2.elf`的节头包含了更为丰富和详尽的数据(这里只展示了部分内容)。这表明`hello2.elf`在链接过程中可能整合了更多的资源或进行了更复杂的处理,从而使得其节头信息相较于`hello.elf`更为充实。

图27 节头情况

3.程序头

程序头部分是一个结构数组,描述了系统准备程序执行所需的段或其他信

息。

图28 程序头情况

4.动态区域

图29 动态区域部分

5.符号表

图30 符号表

5.4 hello的虚拟地址空间

根据计算机系统的特性,程序被载入至地址0x400000~0x401000中。在该地址范围内,每个节的地址都与前一节中节对应的Address相同。根据edb查看的结果,在地址空间 0x400000~0x400中存放着与地址空间0x400000~0x401000相同的程序,在0x400之后存放的是.dynamic.shstrtab节的内容。 

图31 加载虚拟地址的程序代码

5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。

结合hello.o的重定位项目,分析hello中对其怎么重定位的。

在 Shell 中使用命令objdump -d -r hello >hello2.asm生成反汇编文件 hello2.asm

图32 生成hello2.asm文件

       1.在链接过程结束后,`hello2.asm`反汇编文件显示出函数数量的增加,其中新增了`.plt`(Procedure Linkage Table)段,以及`puts@plt`、`printf@plt`、`getchar@plt`、`exit@plt`、`sleep@plt`等函数的条目。这些新增的函数条目是因为动态链接器将`hello.c`程序中引用的共享库函数集成到了最终的可执行文件中。`.plt`是一个特殊的段,用于存储动态链接时函数调用的存根(stub),它允许程序在运行时解析和跳转到正确的函数地址。这个过程是动态链接的一个关键部分,它使得程序能够使用共享库中定义的功能,而无需在编译时静态地包含这些函数的代码。

图33 链接后的函数

2. 在链接过程中,链接器执行了对函数调用指令`call`的重要修改。原先的`call`指令后面跟随的是被调用函数的名称或符号,而在链接之后,这些指令被更新为具体的地址信息。链接器通过解析重定位条目,将`call`指令的目标修改为调用点与被调用函数实际地址之间的偏移量。这样,`call`指令就指向了正确的代码段,完成了从符号名称到具体内存地址的转换。

图34 call指令的参数

3. 链接器将跳转指令的参数从原始的符号引用更改为指向程序链接表(Procedure Linkage Table, PLT)中相应函数的存根的相对地址。这个存根包含了跳转到实际函数地址的代码。通过这种方式,跳转指令被赋予了正确的目标地址,使得在程序执行时能够正确地跳转到动态链接库中相应的函数实现。

这些修改后的跳转指令构成了完整的反汇编代码,确保了程序在运行时能够通过PLT存根正确地调用动态链接库中的函数。这个过程是动态链接的关键部分,允许程序在运行时解析外部函数调用,提高程序的灵活性和可扩展性。

图35 跳转指令的参数

5.6 hello的执行流程

图36 edb加载hello

从加载hello到_start,到call main,至程序终止的所有过程,调用与跳转的各个子程序如下:

<ld-2.31.so!_dl_start>

<ld-2.31.so!_dl_init>

<hello!_start>

<libc-2.31.so!__libc_start_main>

<hello!main>

<hello!printf@plt>

<libc-2.31.so! printf >

<hello!atoi@plt>

<libc-2.31.so! atoi >

<libc-2.31.so! strtoq >

<hello!sleep@plt>

<libc-2.31.so! sleep >

<libc-2.31.so! nanosleep >

<libc-2.31.so! clock_nanosleep >

<libc-2.31.so! _IO_file_xsputn>

<hello!getchar@plt>

<libc-2.31.so!getchar>

5.7 Hello的动态链接分析

在动态链接之前,程序首先经过静态链接阶段,生成一个部分链接的可执行目标文件`hello`。在这个阶段,共享库中的代码和数据并没有直接合并到`hello`文件中。相反,当`hello`被加载执行时,动态链接器负责对共享目标文件中的代码和数据进行重定位,加载所需的共享库,并最终生成一个完全链接的可执行目标文件。在`hello`中,函数如`printf`、`sleep`、`atoi`等,都是通过动态链接在运行时与源程序建立联系的。

由于在编译时无法预知共享库函数的具体运行时地址,因此需要为这些函数调用生成重定位记录。这些记录由动态链接器在程序加载时进行解析。在Linux系统中,采用延迟绑定技术,将函数地址的绑定操作推迟到程序首次调用该函数时进行。

延迟绑定的实现依赖于两个关键的数组:GOT(Global Offset Table)和PLT(Procedure Linkage Table)。

- PLT的大小为每个条目16字节,其中PLT的第一个条目(PLT[0])是特殊的,用于跳转到动态链接器中。其他的每个条目都用于调用一个特定的函数。

- GOT由一系列8字节大小的地址条目组成。GOT的前几个条目(GOT[0]和GOT[1])包含了动态链接器用来解析函数地址的信息。GOT中的第三个条目(GOT[2])指向动态链接器在`ld-linux.so`模块中的入口点。GOT中的其余条目则对应于程序中调用的每个函数,这些函数的地址将在程序运行时由动态链接器解析并填充。

通过这种机制,程序能够在运行时动态地解析和链接到共享库中的函数,从而实现代码的共享和内存的有效利用。

5.8 本章小结

本章中介绍了链接的概念与作用、并得到了链接后的hello 可执行文件的ELF格式文本 hello2.elf,据此分析了hello2.elf与hello.elf的异同;之后,根据反汇编文件 hello2.asm与hello.asm的比较,加深了对重定位与动态链接的理解。

(第51分)

第6章 hello进程管理

6.1 进程的概念与作用

进程是计算机科学中的一个基本概念,它代表了计算机系统中一个正在执行的程序的实例。以下是进程的概念和作用的概述:

概念

进程:是操作系统进行资源分配和调度的基本单位,是程序在数据集合上的一次运行活动。

程序:是一组静态的指令集合,存储在磁盘或其他存储介质上,等待被执行。

进程映像:进程被创建时,操作系统会为它创建一个进程映像,包括代码段、数据段和堆栈等。

作用

1. 资源分配:操作系统通过进程为程序分配所需的资源,如CPU时间、内存空间、I/O设备等。

2. 隔离性:每个进程都有自己独立的地址空间,一个进程的运行不会直接影响到其他进程。

3. 并发性:多个进程可以并发执行,提高了系统资源的利用率和系统的吞吐量。

4. 调度:操作系统的调度程序负责决定哪个进程获得CPU资源,实现多任务处理。

5. 执行环境:为程序提供了一个执行环境,包括程序计数器、寄存器集合和堆栈。

6. 数据保护:通过内存管理单元(MMU)实现数据保护,防止进程间非法访问内存。

7. 错误隔离:一个进程的崩溃不会直接导致系统或其他进程的崩溃,提高了系统的稳定性。

8. 通信:进程间通过各种通信机制(如管道、消息队列、共享内存等)交换数据。

6.2 简述壳Shell-bash的作用与处理流程

Shell 是操作系统的命令行界面,它为用户提供了与操作系统交互的手段。Bash(Bourne Again SHell)是Shell的一种实现,广泛用于Unix和Linux操作系统。以下是 Bash Shell 的作用与处理流程的简述:

作用

1. 命令解释:Bash 读取用户输入的命令,将其转换成操作系统能理解的指令。

2. 脚本执行:允许用户编写和执行Shell脚本,实现自动化任务。

3. 环境配置:通过配置文件(如`.bashrc`和`.profile`)设置用户的环境变量和启动程序。

4. 文件操作:提供命令来创建、复制、移动和删除文件及目录。

5. 程序管理:允许用户启动、停止和监控后台进程。

6. 管道和重定向:支持管道操作,将一个程序的输出作为另一个程序的输入;支持输入输出的重定向。

7. 文本处理:提供文本处理工具,如`grep`、`sed`和`awk`,用于文本搜索、替换和报告。

8. 网络通信:提供网络相关命令,如`ssh`和`scp`,用于远程登录和文件传输。

处理流程

1. 启动:当用户登录系统或打开终端时,Bash Shell 被启动。

2. 提示符:显示提示符,等待用户输入命令。

3. 命令读取:用户输入命令后,Bash 读取这些命令。

4. 命令解析:Bash 解析命令及其参数,检查语法和命令格式。

5. 命令执行:Bash 根据解析结果执行相应的程序或命令。

6. 输出结果:命令执行后的结果被输出到终端。

7. 错误处理:如果命令执行出错,Bash 会显示错误信息。

8. 历史记录:用户输入的命令被保存在历史记录中,可以使用历史命令进行检索。

9. 脚本解释:如果用户运行Shell脚本,Bash 会按顺序解释并执行脚本中的命令。

10. 退出:用户可以通过输入`exit`命令或关闭终端来退出Bash Shell。

6.3 Hello的fork进程创建过程

打开 Shell,输入命令./hello 2022113217 叶子钦 17689752003 3

图37 程序执行情况

带参数执行生成的可执行文件。fork 进程的创建过程如下:首先,带参执行当前目录下的可执行文件hello,父进程会通过 fork 函数创建一个新的运行的子进程hello。子进程获取了与父进程的上下文,包括栈、通用寄存器、程序计数器,环境变量和打开的文件相同的一份副本。子进程与父进程的最大区别是有着跟父进程不一样的PID,子进程可以读取父进程打开的任何文件。当子进程运行结束时,父进程如果仍然存在,则执行对子进程的回收,否则就由init 进程回收子进程。

6.4 Hello的execve过程

进程通过调用`execve`函数来在其当前进程环境中启动并运行`hello`程序。`execve`函数负责在内存中加载`hello`程序,并执行其代码。该函数接受三个参数:程序的可执行文件名`filename`、传递给程序的参数数组`argv`以及环境变量数组`envp`。

在调用`execve`后,加载器会替换子进程现有的虚拟内存空间,用新的程序代码、数据段、堆和栈来替换它们。新的栈和堆被初始化为零值。代码和数据段通过内存映射的方式,从可执行文件中加载并初始化。最后,加载器配置程序计数器(PC)指向程序的入口点`_start`,从而开始执行`hello`程序的`main`函数。

值得注意的是,在加载过程中,除非CPU访问了特定的虚拟页,否则不会从磁盘实际复制数据到内存。这种按需加载(也称为懒加载)的方式提高了程序加载的效率。

6.5 Hello的进程执行

为了提高效率,系统能够同时处理多个进程,这些进程将轮流获得处理器的执行时间。操作系统的内核为每个进程维护一个上下文信息,其中包含了所有必要的状态信息,如寄存器状态、程序计数器和用户栈,以便在进程被中断后能够恢复执行。为了确保进程抽象的完整性,处理器通过限制程序的指令执行能力和可访问的内存地址空间。

在执行`hello`程序的过程中,如果遇到需要从磁盘读取数据的操作,这通常需要较长时间,系统将通过触发异常来切换到内核模式。这时,操作系统将进行进程调度,选择另一个之前被挂起的进程来继续执行。当这个进程运行了一定时间后,系统将再次进行调度,将CPU时间分配回`hello`进程,此时`hello`将从内核模式切换回用户模式继续执行。处理器使用特定的寄存器位来设置其运行模式,从而控制其对资源的访问权限。

6.6 hello的异常与信号处理

通过调试,可在hello程序运行过程时对于异常与信号的处理:

1.乱按键盘

如图所示,在程序运行时乱按键盘,触发中断,hello进程并没有接收到信号,乱序输入的字符串被认为是命令,缓存在stdin中。

38 乱按键盘过程

2.按下Ctrl + Z

程序运行时按下Ctrl + Z,hello进程接收到SIGSTP信号被挂起。使用命令ps查看后台进程,发现hello的PID是11064;再使用命令jobs查看当前作业,此时hello的job号为1;输入命令pstree查看进程树;输入fg向进程发送信号SIGCONT将其调回前台继续运行;输入kill向进程发送SIGKILL信号,终止hello进程。

图39 pstree结构图

3.按下Ctrl + C

如图所示,在程序运行时按下Ctrl + Chello进程因收到SIGINT信号而终止。

图40 终止过程

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

 程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

6.7本章小结

本章深入探讨了进程的基本概念及其在操作系统中的重要性,并概述了Shell的作用以及它的工作流程。通过以一个简单的hello程序为例,本章详细分析了Shell如何利用forkexecve函数来创建新的进程并执行程序。

(第61分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

当我们运行`hello`程序时,计算机内存中会发生以下地址转换:

逻辑地址:是程序在编译时生成的地址,它与程序的代码和数据直接相关,但此时并不对应实际的物理内存位置。

虚拟地址:当程序运行时,操作系统为它创建一个虚拟地址空间,逻辑地址被转换成虚拟地址。`hello`程序中的虚拟地址让程序认为自己独占整个内存。

线性地址:在某些系统中,虚拟地址可能会首先转换为线性地址。这是虚拟地址到物理地址转换的一个中间步骤。

物理地址:最终,虚拟地址通过内存管理单元(MMU)转换为物理地址,这是实际内存条上的地址,CPU通过这些地址访问数据。

简单来说,`hello`程序使用逻辑地址来访问内存,操作系统将它转换成虚拟地址,再通过硬件转换成实际的物理地址,这样程序就能够读取和写入数据到内存中。这个过程对程序是透明的,由操作系统和硬件自动处理。

7.2 Intel逻辑地址到线性地址的变换-段式管理

在Intel架构的系统中,逻辑地址被分解为两部分:选择器(selector)和偏移量(offset)。在这种表示法中:选择器(selector):它是一个索引值,用于在全局描述符表(GDT)中查找对应的段描述符,从而获取段的基地址。在图中,这个选择器存储在代码段寄存器CS中。偏移量(offset):它指定了在选定段内的相对位置。在图中,这个偏移量存储在指令指针寄存器EIP中。

通过将选择器从GDT中检索出的段基地址与偏移量相加,就可以得到实际的线性地址。这个过程是Intel平台下段式内存管理的核心机制,它允许系统将逻辑地址转换为CPU可以直接使用的线性地址。简而言之,Intel平台通过结合段寄存器中的选择器和EIP中的偏移量,来确定数据在内存中的确切位置。

图41 Intel地址变化示意图

7.3 Hello的线性地址到物理地址的变换-页式管理

线性地址(VA)到物理地址(PA)之间的转换通过对虚拟地址内存空间进行分页的分页机制完成。通过 7.2节中的段式管理过程,可以得到了线性地址/虚拟地址,记为VA。虚拟地址可被分为两个部分:VPN(虚拟页号)和VPO(虚拟页偏移量),根据计算机系统的特性可以确定 VPN与VPO的具体位数,由于虚拟内存与物理内存的页大小相同,因此 VPO与 PPO(物理页偏移量)一致。而PPN(物理页号)则需通过访问页表中的页表条目(PTE)获取,如下图所示。

若PTE的有效位为1,则发生页命中,可以直接获取到物理页号PPN,PPN与PPO 共同组成物理地址。

若PTE的有效位为0,说明对应虚拟页没有缓存到物理内存中,产生缺页故障,调用操作系统的内核的缺页处理程序,确定牺牲页,并调入新的页面。再返回到原来的进程,再次调用导致缺页的指令。此时发生页命中,获取到PPN,与PPO 共同组成物理地址。

图42 Hello的线性地址到物理地址的变换-页式管理

7.4 TLB与四级页表支持下的VA到PA的变换

针对 Intel Core i7 CPU 研究 VA 到 PA 的变换。

Intel Core i7 CPU 的基本参数如下:

1.虚拟地址空间 48 位(n=48)

2.物理地址空间 52 位(m=52)

3.TLB 四路十六组相连

4. L1,L2,L3 块大小为 64 字节

5.L1,L2 八路组相连

6.L3十六路组相连

7.页表大小 4KB(P=4x1024=2^12),四级页表,页表条目(PTE)大小8字节

由上述信息可以得知,VPO与PPO有p=12位,故VPN为36位,PPN为40位。单个页表大小4KB,PTE大小8字节,则单个页表有512个页表条目,需要9位二进制进行索引,而四级页表则需要36位二进制进行索引,对应着36位的 VPN。TLB有16组,故TLBI有t=4位,TLBT有36-4=32 位。

图43 TLB与四级页表支持下的VA到PA的变换

如图所示,CPU产生虚拟地址VA,并将其传送至MMU,MMU使用前36位 VPN作为TLBT(前 32位)+TLBI(后4位)在 TLB 中进行匹配,若命中,则得到PPN(40bit)与VPO(12bit)组合成物理地址PA(52bit)。若TLB没有命中,则 MMU 向页表中查询,由CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,则执行下一步确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与 VPO 组合成PA,并向TLB中添加条目。

若查询 PTE的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。

7.5 三级Cache支持下的物理内存访问

三级Cache支持下的物理内存访问是一种多层次的缓存体系结构,旨在提高处理器访问数据的速度。以下是对这一概念的简要说明:

1. L1 Cache(一级缓存):这是最快的缓存,直接集成在处理器核心上。它包含数据和指令缓存,用于存储最近访问过的数据和指令。

2. L2 Cache(二级缓存):比L1 Cache慢一些,但仍然非常快。它通常与一个或多个处理器核心共享,提供更大的缓存空间。

3. L3 Cache(三级缓存):是最大的缓存,也是最慢的,但仍然比主存快很多。它被所有处理器核心共享,用于存储大量频繁访问的数据。

在三级Cache体系中,物理内存访问的过程如下:

当处理器需要访问数据时,首先在L1 Cache中查找。如果L1 Cache未命中(即数据不在L1 Cache中),处理器会在L2 Cache中继续查找。如果L2 Cache也未命中,处理器最后会在L3 Cache中查找。如果在L3 Cache中仍然未命中,处理器将不得不从慢速的物理内存(RAM)中获取数据,并将数据加载到缓存中以供将来使用。

这种分层的缓存策略可以显著提高数据访问速度,因为它减少了对慢速物理内存的访问次数。同时,它还通过缓存一致性协议确保所有处理器核心看到的数据是一致的。

7.6 hello进程fork时的内存映射

当`hello`进程执行`fork`系统调用时,会发生以下内存映射相关的操作:

1. 复制内存空间:`fork`调用会复制父进程的整个内存空间到子进程。这包括代码段、数据段、堆和栈。

2. 创建新的地址空间:尽管子进程起初拥有与父进程相同的内存内容,但它拥有自己的地址空间和独立的进程标识符(PID)。

3. 独立的栈:尽管`fork`会复制父进程的栈,但子进程的栈顶会有所不同,以确保系统调用返回值(如`fork`调用返回的子进程PID)在父进程和子进程中是独立的。

4. 共享文件映射:如果父进程有文件或其他资源的内存映射,子进程通常会共享这些映射,但对它们的修改可能会触发写时复制(Copy-On-Write, COW)机制。

5. 共享库映射:动态链接库的代码段通常是共享的,这意味着父子进程可能共享同一物理内存页,但每个进程有自己的虚拟地址空间。

6. 环境变量和参数:子进程继承父进程的环境变量和命令行参数,这些信息通常存储在栈或特定的内存区域。

7. 执行新程序:在`fork`之后,如果子进程调用了`exec`系列函数来执行一个新程序,那么新程序的代码和数据将替换子进程的原有内存内容,但文件描述符和环境变量等设置可能会保留。

简而言之,`fork`操作创建了一个新的进程,它拥有父进程内存的一个副本,但具有独立的地址空间和PID。这允许父子进程在隔离的环境中运行,同时共享某些资源以提高效率。

7.7 hello进程execve时的内存映射

当`hello`进程执行`execve`系统调用时,它会加载并运行一个新的程序,这一过程会对内存映射产生以下影响:

1. **替换进程映像**`execve`调用将当前进程的内存映像替换为新程序的内存映像。这意味着新程序的代码和初始化数据将加载到内存中。

2. **创建新的代码段**:新程序的代码将被映射到进程的地址空间中,通常位于内存的较高区域。

3. **初始化数据段**:新程序的全局和静态变量将被初始化并映射到数据段。

4. *堆和栈重置**新程序的堆和栈将被创建并初始化。栈通常从进程的栈顶向下增长,堆则是从栈底向上增长。

5. **环境变量和参数传递**`execve`调用允许传递新的环境变量和参数给新程序,这些信息将被设置在新程序的栈中。

6. **关闭或继承文件描述符**:新程序可以选择关闭所有继承自父进程的文件描述符,或者继承和使用它们。

7. **写时复制**:如果新程序尝试写入继承自父进程的只读内存页,将触发写时复制(Copy-On-Write, COW)机制,为新程序创建该页的私有副本。

8. **共享库加载**如果新程序依赖于共享库,这些库将被加载到进程的地址空间中,通常是通过内存映射文件实现的。

9. **程序计数器重置**:新程序的程序计数器(PC)将被设置为新程序的入口点,通常是`_start`符号指向的位置。

`execve`调用替换了当前进程的几乎所有内存内容,加载新程序的代码、数据、堆和栈,并准备执行新程序的`main`函数。这一过程为新程序提供了一个干净的执行环境。

7.8 缺页故障与缺页中断处理

缺页故障(Page Fault)和缺页中断处理是内存管理中的重要概念,特别是在使用虚拟内存的系统中。以下是对这两个概念的简要说明:

缺页故障(Page Fault

 定义:当程序尝试访问的页面不在物理内存中,而是在磁盘上时,将触发缺页故障。这是虚拟内存管理中的常见现象。

 原因:可能是因为程序访问了一个尚未加载到内存的页面,或者页面已经被交换(swap)出去。

缺页中断处理

 中断:缺页故障会导致一个中断,操作系统的内存管理器需要处理这个中断。

 处理过程:

  1. 识别故障地址:操作系统首先确定哪个虚拟地址导致了缺页故障。

  2. 页面查找:在磁盘上查找对应的页面,这可能涉及到I/O操作。

  3. 页面置换:如果内存已满,内存管理器需要根据某种算法(如最近最少使用LRU)选择一个或多个页面来置换。

  4. 加载页面:将磁盘上的页面加载到物理内存中。

  5. 更新页表:更新页表项,将新的物理页面与导致缺页的虚拟地址关联起来。

  6. 重新执行:处理完缺页中断后,操作系统会重新执行导致缺页故障的指令。

特点

 性能影响:缺页故障可能会显著影响程序的性能,因为它涉及到磁盘I/O操作,这比内存访问要慢得多。

 优化:现代操作系统使用各种技术来减少缺页故障的影响,如预加载和局部性原理的利用。

缺页故障是当程序访问的内存页面不在物理内存中时发生的现象,操作系统需要通过中断处理来加载缺失的页面,并更新内存管理数据结构,然后程序才能继续执行。这个过程是虚拟内存管理的关键部分,对系统性能有重要影响。

7.9动态存储分配管理

动态内存管理涉及操作系统或运行时环境如何有效分配和回收程序运行时所需的内存。以下是对动态内存管理方法和策略的另一种表述:

动态内存分配器负责管理进程虚拟内存中的一个区域,称为堆。堆被组织成不同大小的内存块集合,每一块都是一个连续的内存区域,可以是已分配的,也可以是空闲的。已分配的块被程序使用,而空闲的块则保留用于将来的分配。一旦分配,块保持为已分配状态,直到程序明确释放它,或者分配器在某些情况下自动回收它。

动态内存分配器通常有两种基本类型:

显式链表:这种类型的分配器要求程序员负责释放所有之前请求的内存块。程序员必须明确调用释放函数来归还内存。

隐式链表:在这种类型的分配器中,分配器负责监控内存使用情况,并在检测到分配的内存块不再被程序引用时自动释放它们。这个过程称为垃圾收集,它自动回收不再使用的内存资源。

动态内存管理通过分配器来确保程序在运行时可以请求和释放内存,这要求对内存块的分配和回收策略有明确的控制,无论是通过程序员的显式操作还是通过分配器的隐式垃圾收集机制。

1.隐式链表

堆中的空闲块通过头部中的大小字段隐含地连接,分配器通过遍历堆中所有的块,从而间接遍历整个空闲块的集合。

对于隐式链表,其结构如下:

图44 隐式链表的结构

2.显式链表

在每个空闲块中,都包含一个前驱(pred)与后继(succ)指针,从而减少了搜索与适配的时间。

图45 显示链表的结构

3. 带边界标记的合并

采取使用边界标记的堆块的格式,在堆块的末尾为其添加一个脚部,其为头部的副本。添加脚部之后,分配器就可以通过检查前面一个块的脚部,判断前面一个块的起始位置和状态。从而实现快速合并,减小性能消耗

4.分离存储

维护多个空闲链表,其中,每个链表的块具有相同的大小。将所有可能的块大小分成一些等价类,从而进行分离存储。

7.10本章小结

本章主要介绍了hello的存储器地址空间、intel的段式管理、hello 的页式管

理,VA到PA的变换、物理内存访问,hello进程fork、execve 时的内存映射、

缺页故障与缺页中断处理、动态存储分配管理。

(第7 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

在Linux操作系统中,I/O设备管理采用一些特定的方法来实现设备与应用程序之间的交互。以下是Linux I/O设备管理方法的概述:

设备的模型化:文件

1. 设备文件:在Linux中,所有的I/O设备都被抽象为文件系统里的文件。这些设备文件通常位于`/dev`目录下。

2. 特殊文件:设备文件分为两种类型:块设备文件和字符设备文件。块设备文件支持随机访问,而字符设备文件则是顺序访问。

3. 设备驱动程序:每个设备文件都由一个设备驱动程序管理,驱动程序是操作系统与硬件之间的接口。

设备管理:UNIX I/O接口

1. 标准I/O:UNIX系统提供了一套标准I/O库,应用程序通过这个库来执行打开、读取、写入和关闭设备文件的操作。

2. 文件描述符:当应用程序打开一个设备文件时,操作系统会返回一个文件描述符,它是用于后续I/O操作的索引。

3. 无差别访问:UNIX I/O接口提供了一种无差别的访问方式,应用程序可以像操作普通文件一样操作设备文件。

访问控制

1. 权限:设备文件的访问权限由操作系统控制,只有具有适当权限的用户才能访问特定的设备。

2. 设备节点:每个设备文件都有一个主设备号和次设备号,这些标识符帮助操作系统确定使用哪个设备驱动程序。

缓冲和缓存

1. 缓冲区:操作系统使用缓冲区来暂存从设备读取的数据或待写入设备的数据。

2. 缓存:为了提高性能,操作系统会缓存频繁访问的数据,减少对物理设备的直接访问。

错误处理

1. I/O错误:I/O操作可能会失败,操作系统会返回错误代码给应用程序。

2. 信号:某些I/O错误可能会通过信号机制通知应用程序。

异步I/O

1. 非阻塞I/O:应用程序可以执行非阻塞I/O操作,即使操作不能立即完成也不会挂起程序。

2. 异步通知:操作系统可以通知应用程序I/O操作何时完成,例如通过信号或回调函数。

Linux的I/O设备管理方法通过将设备视为文件,并使用标准的UNIX I/O接口,为应用程序提供了一种统一和灵活的方式来访问和管理各种硬件设备。这种方法简化了设备驱动程序的开发,并允许应用程序以一致的方式进行I/O操作。

8.2 简述Unix IO接口及其函数

Unix I/O接口提供了一组标准函数,允许应用程序对文件和设备进行输入输出操作。以下是Unix I/O接口及其常用函数的简述:

  1. open:打开一个文件或设备,返回一个文件描述符用于后续操作。
  2. close:关闭一个通过open打开的文件描述符,释放相关资源。
  3. read:从指定的文件描述符读取数据,将数据存入指定的缓冲区。
  4. write:向指定的文件描述符写入数据,数据从指定的缓冲区取出。
  5. lseek:移动文件描述符的读写位置,可以设置文件的当前读写位置。
  6. fsync:强制将文件描述符关联的所有未写入磁盘的数据立即写入。
  7. ioctl:用于设备特殊控制的函数,可以执行非标准的设备特定操作。
  8. stat 和 fstat:获取文件状态,如大小、权限、最后访问时间等。
  9. chmod 和 fchmod:改变文件或文件描述符的权限。
  10. chown 和 fchown:改变文件或文件描述符的所有者和组。
  11. unlink:删除文件,即使文件被打开也可以被删除。
  12. rename:重命名文件。
  13. access:检查文件的可访问性,如是否存在和是否有执行权限。
  14. pipe:创建一个管道,用于进程间通信。
  15. select 或 poll:等待一个或多个文件描述符的状态变化,如可读、可写或有错误。
  16. mmap:将文件或其他对象映射到内存中,实现文件和内存的直接交互。

这些函数构成了Unix系统I/O操作的基础,它们通过系统调用实现,为应用程序提供了与操作系统交互的接口。Unix I/O接口的设计哲学是一切皆文件,这意味着对设备和文件的I/O操作使用相同的函数集。这种一致性简化了编程,并允许程序员使用统一的方法处理不同类型的I/O操作。

8.3 printf的实现分析

查看windows系统下printf的函数体

图46 printf 的函数体

形参列表中的…是可变形参的一种写法,当传递参数的个数不确定时,用这种方式来表示。

va list 的定义:typedef char *va list,说明它是一个字符指针,其中(char*)(&fimt)+ 4)即 arg 表示的是...中的第一个参数。

再进一步查看 windows系统下的vsprintf函数体:

则知道 vsprintf程序按照格式fimt 结合参数 args 生成格式化之后的字符串,并返回字串的长度。

在 printf中调用系统函数 write(buf,i)将长度为i的 buf输出。write 函数如下:

printf函数的功能为接受一个格式化命令,并按指定的匹配的参数格式化输出

故i= vsprintf(buf,fimt,arg)是得到打印出来的字符串长度,其后的 write(buf,i)是将buf中的i个元素写到终端。

因此,vsprintf的作用为接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,进而产生格式化输出

图47 vsprintf函数体

       再进一步对 write 进行追踪:

图48 write的情况

这里给几个寄存器传递了参数,然后以一个intINT VECTOR SYSCALL 结束。INT VECTOR SYSCALL代表通过系统调用syscall,查看syscall 的实现:

图49 syscall的实现情况

       syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的 ASCII码,符显示驱动子程序:从ASCI 到字模库到显示 vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取 vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

1. 键盘中断处理

 异步异常:现代操作系统能够处理异步事件,如键盘中断。当用户按下键盘上的一个键时,会触发一个中断。

 键盘中断处理子程序:操作系统中的键盘驱动程序会响应这个中断。它读取键盘的扫描码,并将其转换成对应的ASCII码或Unicode码。

 键盘缓冲区:转换后的字符被保存到操作系统的键盘缓冲区中,等待被读取。

2. `read`系统函数

 `getchar`通常通过调用`read`函数来实现。`read`是一个系统调用,用于从指定的文件描述符读取数据。

 在`getchar`的上下文中,`read`被用来从标准输入(文件描述符0)读取一个字符。

3. 系统调用机制

 与`printf`类似,`read`通过系统调用机制请求操作系统提供服务。在Linux系统中,这可以通过`int 0x80`软件中断或`syscall`指令来实现。

4. 等待输入

 `getchar`通常在调用`read`时设置为阻塞模式,这意味着如果缓冲区中没有可用的字符,程序将等待直到有输入到达。

 当用户按下键盘上的键时,字符被放入缓冲区,`read`调用将读取该字符并返回。

5. 回车键的处理

 `getchar`函数通常会持续等待,直到遇到特定的输入,如按下回车键(ASCII码为`\n`或`\r`),这时它将返回读取的字符并结束。

6. 缓冲和流控制

 在某些实现中,`getchar`可能与终端的行缓冲模式有关,这意味着它会收集一系列字符直到遇到换行符或回车符。

8.5本章小结

本章主要介绍了 linux的IO设备管理方法和及其接口和函数,对 printf函数和getchar 函数的底层实现有了基本了解。

(第81分)

结论

Hello的一生经历了如下过程

1. 预处理阶段:源代码文件`hello.c`经过预处理器处理,包含必要的函数库,生成预处理后的文件`hello.i`。

2. 编译阶段:文件`hello.i`通过编译器转换,产出汇编语言文件`hello.s`。

3. 汇编阶段:汇编器将`hello.s`中的汇编指令转换成机器语言,创建出可重定位的目标文件`hello.o`。

4. 链接阶段:`hello.o`与其它目标文件及动态库链接,形成最终的可执行文件`hello`。

5. 运行命令输入:用户在命令行中输入`./hello 2022111614 王昀潼 2`,启动程序。

6. 子进程创建:Shell使用`fork`系统调用来创建`hello`程序的子进程。

7. 程序加载与执行:Shell通过`execve`系统调用加载程序,配置虚拟内存并开始执行程序的入口点。

8. 物理内存访问:内存管理单元(MMU)将`hello`程序的虚拟地址转换为对应的物理地址。

9. 动态内存分配:`printf`函数通过调用`malloc`,向系统申请动态内存。

10. 异常和信号处理:在程序运行期间,用户按下`Ctrl + C/Z`将产生信号,由Shell进行异常和信号处理。

11. 子进程回收:程序`hello`执行完毕后,Shell回收子进程资源,操作系统清除该进程的数据结构。

感想:学习计算机系统是一次深入探索计算机内部运作的旅程。它不仅让我理解了从底层硬件到高级软件的整个工作流程,还让我对数据如何在系统中流动、程序如何被执行有了清晰的认识。通过这门课程,我学习了处理器的工作原理、存储器的层次结构、操作系统的核心概念,以及网络通信的基础。这些知识帮助我构建了一个全面的视角来看待计算机科学,让我意识到每一个编程决策都可能对系统性能产生深远的影响。

学习计算机系统也强化了我的问题解决能力。面对复杂的系统问题时,我学会了如何分析问题的根本原因,并运用适当的工具和技术来解决它们。这种能力不仅限于计算机科学领域,它是一种通用的、可迁移的技能,对我的个人和职业发展都有着不可估量的价值。

这门课程也激发了我对技术创新的热情。了解到计算机系统的发展历史和当前的前沿技术,我更加渴望参与到这个不断进步的领域中,为推动技术的发展贡献自己的力量。总的来说,学习计算机系统是一次宝贵的经历,它不仅增长了我的知识,也拓宽了我的视野。

(结论0分,缺失 -1分,根据内容酌情加分)

附件

hello.c :hello源代码

hello.i :预处理后得到的文本文件

hello.s :编译后得到的汇编语言文件

hello.o :汇编后得到的可重定位目标文件

hello.elf :用readelf读取 hello.o得到的 ELF 格式信息

hello.asm :反汇编 hello.o 得到的反汇编文件

hello2.elf :由hello 可执行文件生成的.elf文件

hello2.asm :反汇编 hello 可执行文件得到的反汇编文件

(附件0分,缺失 -1分)

参考文献

为完成本次大作业你翻阅的书籍与网站等

[1]  林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.

[2]  辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.

[3]  赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).

[4]  谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.

[5]  KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.

[6]  CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.

[7]  https://www.cnblogs.com/fishpro/p/spring-boot-study-helloworld.html

[8]  Tanenbaum, A. S., & Bos, H. (2015). 深入理解计算机系统 (原书第3). 机械工业出版社.

(参考文献0分,缺失 -1分)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值