哈工大2023春计算机系统大作业——程序人生-Hello’s P2P

计算机系统

大作业

题目:程序人生-Hello’s P2P
专业:未来技术学院
学号:2021110788
班级:21WL026
学生:左镕畅
指导教师:吴锐

摘 要
本文对hello.c源程序在Linux下运行的生命历程进行了描述。在这个过程中,分别运行C预处理器、C编译器、汇编器,进行预处理、编译、汇编,得到了hello.i、hello.s、hello.o文件,最后利用链接器得到了可执行文件hello。随后对hello的进程运行、内存管理等过程进行探索,更深入理解Linux系统下的存储层次结构、异常控制流、虚拟内存等相关内容。随着对hello生命历程的探索,hello的“生命”也走向了尽头。
关键词:计算机系统;汇编;虚拟内存;进程管理

目录

第1章 概述

1.1 Hello简介

P2P:全称为From Program to Progress,从程序到进程。这个看似简单的过程需要经过预处理、编译、汇编、链接等一系列的复杂动作才可以生成一个可执行文件。在运行时,我们打开Shell,通过输入./hello,使Shell创建新的进程用来执行hello。操作系统会使用fork()产生子进程,然后通过execve()将其加载,不断进行访存、内存申请等操作执行程序。最后,在程序结束返回后,由父进程或祖先进程进行回收,程序结束。
020:全称为From 0 to 0,从无到终。Hello的出生是由操作系统进行存储管理、地址翻译、内存访问,通过按需页面调度来开始这段生命。父进程或祖先进程的回收也标志着它生命的结束。

1.2 环境与工具

1.2.1 硬件环境

处理器:Intel Core i7 10th GEN
系统类型:X64 CPU; 2GHz; 16G RAM; 256G HD Disk

1.2.2 软件环境

Windows10家庭版
VMware Workstation pro2022
Ubuntu22.04

1.2.3 开发与调试工具

gedit+gcc
VSCode
Edb

1.3 中间结果

文件名 功能
hello.c 源程序
hello.i 预处理后的文件
hello.s 汇编文件
hello.o 可重定位目标执行文件
hello 可执行文件
hello.elf hello.o的ELF格式
hello1.txt hello.o的反汇编语言
hello2.txt hello的反汇编语言
hello1.elf hello的ELF格式

1.4 本章小结

本章简述了Hello程序的一生,可以发现计算机系统课程的学习和hello的生命轨迹基本重合一致。本章还简要说明了实验的软、硬件环境以及编译处理工具,是整体文章的布局脉络。

第2章 预处理

2.1 预处理的概念与作用

2.1.1 预处理的概念

预处理是指在编译阶段之前对.c源文件进行预处理的过程。预处理器会根据预处理指令对源代码进行一系列转换和替换,生成新的源代码文件(ASCII码的中间文件.i文件),这个过程叫做预处理。

2.1.2 预处理的作用

  1. 宏定义:通过宏定义来定义常量、函数等,使程序更加易读、易维护。
  2. 条件编译:通过条件编译来控制程序的行为,使程序具有更强的灵活性和可移植性。
  3. 文件包含:通过文件包含来引入其他程序文件中定义的变量、函数等,避免重复定义和提高代码的复用性。
  4. 注释处理:去除程序中的注释,减小程序的体积,提高程序的执行效率。
  5. 预编译指令:可以通过预编译指令来指定编译器的行为,如优化等级、编译选项等,以达到更好的程序性能。

2.2 在Ubuntu下预处理的命令

在终端中输入cpp hello.c > hello.i,即可生成.i文件

图2.2.1 预处理命令

图2.2.2 预处理生成文件

2.3 Hello的预处理结果解析

hello.c的预处理结果为hello.i,直接打开hello.i文件观察发现它共有3121行,最后的14行为hello.c中的main函数。相较于hello.c,hello.i中已经没有了注释,同时.c文件中的头文件(stdio.h、unistd.h、stdlib.h等)也被加载到了.i文件中。

图2.3.1 hello.i文件部分内容

2.4 本章小结

本章首先介绍了预处理的定义与作用,再以hello.c文件为例,展示了.c文件的预处理的过程,最后结合预处理生成的.i程序对预处理结果进行了分析。

第3章 编译

3.1 编译的概念与作用

3.1.1 编译的概念

编译过程是将预处理后的代码转换为汇编语言代码的过程。预处理后的代码中包含了宏定义、条件编译、文件包含等预处理指令,这些指令需要被编译器进一步处理,生成可执行的汇编语言代码。

3.1.2 编译的作用

  1. 语法检查:编译器会对源代码进行语法检查,确保代码符合语法规范,如括号匹配、语句结束符等。
  2. 语义检查:编译器会对源代码进行语义检查,确保代码的正确性和可靠性,如类型检查、变量声明检查、函数调用检查等。
  3. 代码优化:编译器会对源代码进行优化,使程序更加高效、快速和可靠,如常量折叠、循环展开、内联函数等。
  4. 中间代码生成:编译器会将源代码转换为中间代码,以便后续的优化和目标代码生成。
  5. 目标代码生成:编译器会将中间代码转换为目标代码,即汇编代码,以便后续的汇编和链接。
  6. 调试支持:编译器会生成调试信息,以便程序出错时能够进行调试和排错。

3.2 在Ubuntu下编译的命令

打开终端输入命令gcc -S hello.c -o hello.s实现编译。

图3.2.1 编译命令

图3.2.2 编译结果

3.3 Hello的编译结果解析


图3.3.1 hello.s文件内容

3.3.1 字符串常量

hello.c中需要打印的字符串的编译结果如下图所示

图3.3.2 字符串常量编译结果

3.3.2 局部变量

main函数中的局部变量int被存在栈中,它在编译文件中的表示如下图所示

图3.3.3 局部变量编译结果

3.3.3 算数操作

算数操作++的编译结果如下图所示,通过汇编指令addl来实现

图3.3.4 算数操作++编译结果

3.3.4 关系操作

关系操作<的编译解析如下图所示,在.s文件中i<7被解析为i<=7

图3.3.5 关系操作<编译结果
关系操作!=的编译解析如下图所示

图3.3.6 关系操作!=编译结果

3.3.5 数组操作

数组的下标[]访问的编译解析如下图所示

图3.3.7 数组操作编译结果

3.3.6 控制转移

main函数中的控制转移for的汇编解释如下图所示,它是通过比较指令和跳转指令实现的

图3.3.8 控制转移编译结果

3.3.7 函数操作

如下图所示,图片展现了main函数的参数传递。由于还会使用到%rbp,所以先将%rbp压栈保存起来。并将栈指针减少32位,然后分别将%rdi和%rsi的值存入栈中。由此可知,%rbp-20和%rbp-32的位置分别存了argv数组和argc的值。

图3.3.9 参数传递
函数调用的编译解析如下图所示,分别为printf和sleep函数的调用,均使用call指令进行调用
其中printf调用前先取得argv数组的第二个和第三个元素,并放入寄存器%rsi和%rdx,然后取得了字符串的地址,并存入了%rdi中作为第一个参数
atoi和sleep函数调用前先取得argv存入%rdi作为第一个参数,然后调用atoi函数,并将返回值存到%rax中,在将%rax的值存到%rdi作为sleep的第一个参数

图3.3.10 函数调用
函数的返回值存在%rax寄存器中

3.3.9 赋值操作

通过movq指令是吸纳赋值操作,如下图所示为部分赋值操作

图3.3.11 赋值操作

3.4 本章小结

本章先介绍编译的定义和作用,并通过hello.s汇编语言文件和hello.c源程序的比较,对编译进行了解析,进一步指出编译器是如何处理各种数据类型和各类操作的。

第4章 汇编

4.1 汇编的概念与作用

4.1.1 汇编的概念

汇编将程序员编写的易于理解和编写的汇编语言代码转换为计算机能够理解和执行的机器语言代码。

4.1.2 汇编的作用

  1. 将汇编语言源代码转换为机器语言指令,使计算机能够执行程序。
  2. 检查源代码中的语法错误和逻辑错误,并提示程序员进行修正。
  3. 生成可执行文件或目标文件,包括可执行文件、库文件、目标文件等,以便于程序员进行后续的操作。
  4. 实现宏汇编等高级汇编技术,提高程序员的编程效率和代码可读性。

4.2 在Ubuntu下汇编的命令

在终端中输入命令gcc -c hello.s -o hello.o对.s文件进行汇编操作,得到.o文件

图4.2.1 汇编命令

图4.2.2 汇编结果

4.3 可重定位目标elf格式

终端输入命令readelf -a hello.o > hello.elf获得ELF格式文件如下图所示

在这里插入图片描述
图4.3.1 elf文件生成命令
终端输入readelf -S hello.o查看hello.o的节头表,它描述了每个节的节名文件偏移、大小、访问属性、对齐方式等

图4.3.2 节头表
终端输入readelf -h hello.o查看hello.o的ELF头

在这里插入图片描述
图4.3.3 ELF头
readelf -s hello.o查看符号表,Name为符号名称,Value是符号相对于目标节的起始位置偏移,Size为目标大小,Type是类型,数据或函数,Bind表示是本地还是全局
在这里插入图片描述
图4.3.4 符号表
readelf -r hello.o查看重定位节,其中重定位节.rela.text包含.text节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。以下8条重定位信息对.L0、puts函数、exit函数、.L1、printf、sleepsec、sleep、getchar函数进行了重定位声明

图4.3.5 重定位节

4.4 Hello.o的结果解析

在终端输入objdump -d -r hello.o可以查看hello.o文件的反汇编结果,如下图所示

图4.4.1 hello.o反汇编结果
通过与第三章的hello.s进行对比,可以发现有以下区别:
1. 分支跳转:.s文件中的分支跳是使用段名作为标识,而.o文件中使用地址的偏移量来标识。实际上段名称只是在汇编语言中的助记符,这个助记符就像是注释一样,在汇编成机器语言之后不存在,而换为了确定的地址
2. 函数调用:与分支跳转类似,hello.s中使用函数名来进行标识的,而hello.o使用当前位置的下一条指令,但因为被调用的函数仅有定义没有实现(在其它库中实现),现在不能确定地址,需要链接接后才能确定其地址

图4.4.2 hello.s文件
汇编语言与机器语言的映射关系是一一对应的,因此,汇编语言的指令可以直接转换成机器语言的指令,机器语言的指令也可以用汇编语言的助记符表示,两者之间是相互转化的关系。在编程过程中,汇编语言可以提高程序员的编程效率和代码可读性,同时也可以更好地理解计算机内部的运行机制

4.5 本章小结

本章首先介绍了汇编的概念和作用,再以hello.s到hello.o的汇编过程为例,说明.o文件中包含的信息,并通过对比说明从汇编语言映射到机器语言需要实现的转换

第5章 链接

5.1 链接的概念与作用

5.1.1 链接的概念

链接是将目标文件和库文件组合成可执行文件的过程。在链接过程中,链接器会解决符号引用,将目标文件中引用的符号与库文件中定义的符号进行匹配。如果找不到匹配的符号,则会产生链接错误。

5.1.2 链接的作用

链接使得分离编译成为可能,使我们可以独立的修改和编译我们需要修改的小的模块

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 链接命令

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

命令readelf -a hello
1. elf头:
在这里插入图片描述
图5.3.1 elf头
2. 节头

图5.3.2 节头
3. 程序头

图5.3.3 程序头
4. 重定位节

图5.3.4 重定位节
5. 符号表

图5.3.5 符号表

5.4 hello的虚拟地址空间

虚拟地址空间的起始地址为0x400000,结束地址为0x400ff0
在这里插入图片描述
图5.4.1 edb界面
与此同时,根据5.3里的Section头部表,我们可以在edb中找到对应节的虚拟空间地址


图5.4.2 节头表与edb虚拟地址空间对比

5.5 链接的重定位过程分析

终端输入命令objdump -d -r hello,并将结果与hello.o对比发现:
1. 相比hello.o,hello中增加了hello.c用到的库函数,例如printf
2. hello中也比hello.o多了很多节,如_init、puts@plt等
3. hello.o的地址从0开始,是相对地址,而hello的地址从0x400000开始,是重定位之后的虚拟地址
4. hello的跳转指令和call指令使用的均为绝对地址,而hello.o中使用的是相对地址
链接的过程如下:
1. 将所有目标文件中的符号名和地址导入全局符号表中
2. 对全局符号表中的符号名进行符号解析,确保每个符号名只有一个定义
3. 对每个目标文件进行重定位,将相对地址转换为绝对地址
4. 将所有目标文件中的代码和数据段合并成一个可执行文件或共享库
5. 对合并后的可执行文件或共享库进行地址重定位,将全局符号表中的符号地址更新为正确的地址
6. 最后生成可执行文件或共享库,供操作系统加载和执行
hello.o的重定位过程:
1. 编译器在编译源代码时,将代码中的绝对地址转换为相对地址。
2. 链接器将hello.o与其他目标文件或库文件进行链接,生成可执行文件或共享库。
3. 链接器对hello.o进行重定位,将代码和数据段中的相对地址转换为绝对地址。重定位过程中,链接器会将hello.o中的每个符号引用与全局符号表中的相应符号定义进行匹配,并计算出符号引用的绝对地址。
4. 如果hello.o中存在未解决的符号引用,则链接器会报告链接错误。
5. 链接器对合并后的可执行文件或共享库进行地址重定位,将全局符号表中的符号地址更新为正确的地址。
6. 最后生成可执行文件或共享库,供操作系统加载和执行。

图5.5.1 hello反汇编文件

5.6 hello的执行流程

使用edb逐步执行并记录

图5.6.1 edb界面
得到从加载hello到_start,到call main,以及程序终止的所有过程如下:
ld-2.27.so!_dl_start
ld-2.27.so!_dl_init
hello!_start
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!_sigsetjmp
-libc-2.27.so!__sigjmp_save
hello!main
hello!puts@plt
hello!exit@plt
hello!printf@plt
hello!atoi@plt
hello!sleep@plt
hello!getchar@plt
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
得到子程序名的及地址如下表所示

子程序名 子程序地址
hello!_start 0x00000000004010f0
hello!__libc_csu_init 0x0000000000401270
hello!_init 0x0000000000401000
hello!frame_dummy 0x00000000004011d0
hello!register_tm_clones 0x0000000000401160
972994hello!main 0x00000000004011d6
hello!printf@plt 0x0000000000401040
hello!atoi@plt 0x0000000000401060
hello!sleep@plt 0x0000000000401080
hello!getchar@plt 0x0000000000401050
hello!exit@plt 0x0000000000401070
hello!__do_global_dtors_aux 0x00000000004011a0
hello!deregister_tm_clones 0x0000000000401130
hello!_fini 0x00000000004012e8

5.7 Hello的动态链接分析

动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数
首先通过查看elf可以找到.got的地址为0x601000
Got表在调用dl_init前如下图所示
在这里插入图片描述
图5.7.1 dl_init调用前的虚拟地址空间
Got表在调用dl_init后如下图所示

图5.7.2 dl_init调用后的虚拟地址空间

5.8 本章小结

本章主要介绍了链接的概念与作用,通过对比hello反汇编文件与hello.o可重定位文件,详细分析了重定位过程,最后使用edb调试hello程序,分析了hello的执行流程以及动态链接过程。

第6章 hello进程管理

6.1 进程的概念与作用

6.1.1 进程的概念

进程是计算机系统中正在运行的程序的实例。它是操作系统对正在运行的程序进行管理和调度的基本单位。每个进程都有自己的地址空间、堆栈、程序计数器、寄存器等资源,同时还有自己的状态,如运行、就绪、阻塞等。

6.1.2 进程的作用

  1. 实现并发:多个进程可以同时运行,实现并发执行,提高系统的效率。
    2. 资源管理:操作系统通过进程管理来控制系统资源的分配和使用,如CPU时间、内存、磁盘、网络等。
    3. 提高系统的可靠性:每个进程都是独立的,一个进程的崩溃不会影响其他进程的正常运行。
    4. 实现进程间通信:不同进程之间可以通过进程间通信(IPC)机制进行数据交换和协作。
    5. 实现多任务处理:操作系统可以通过进程管理来实现多任务处理,使得多个任务同时执行,提高系统的效率。

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

6.2.1 shell-bash的作用

壳(Shell)是计算机操作系统中的一个命令行解释器,用于解释用户输入的命令并将其转化为操作系统可以执行的指令。其功能包括文件管理、进程管理、环境变量管理、输入输出重定向等。
Bash是一种壳(Shell)程序,是Unix和Linux操作系统中最常用的壳程序之一。

6.2.1 shell-bash的处理流程

  1. 读取用户输入的命令。
    2. 对命令进行解析,包括识别命令名和参数。
    3. 执行命令,包括调用系统程序或执行脚本。
    4. 输出命令结果。
    在执行命令时,Bash会首先查找当前工作目录下是否存在命令,如果不存在则查找系统路径下是否存在相应的程序。命令执行完毕后,Bash会将结果输出到标准输出或重定向到其他文件。

6.3 Hello的fork进程创建过程

  1. 在终端中输入./hello 2021110788 左镕畅 100,运行的终端程序会对输入的命令行进行解析
    2. ./hello 不是一个内置的shell命令,所以解析之后终端程序判断./hello的语义为执行当前目录下的可执行目标文件hello,hello主函数main的参数即为终端中输入的四个字符串(以空格作为分隔)
    3. 之后终端程序首先会调用fork函数创建一个新的子进程,新创建的子进程几乎与父进程相同。fork函数会返回两次,其中0代表子进程,非0(返回父进程的pid)代表父进程
    4. 父进程与子进程之间最大的区别在于它们拥有不同的PID。子进程得到与父进程用户级虚拟地址空间相同的一份副本,当父进程调用fork时,子进程可以读写父进程中打开的任何文件
    5. 内核能够以任意方式交替执行父子进程的逻辑控制流的指令,父进程与子进程是并发而独立运行的。在子进程执行期间,父进程默认选项是显示等待子进程的完成
    6. 父进程和子进程独立运行,二者结束顺序不可知,父进程负责回收子进程

    图6.3.1 进程的地址空间

6.4 Hello的execve过程

创建子进程完成后,子进程调用execve函数在当前子进程的上下文加载并运行hello程序,具体步骤:
1. 删除之前进程在用户中已存在的结构
2. 创建新的私有的复制的代码、数据和堆栈
3. 通过标准C语言库的动态链接,映射到用户虚拟地址空间中的共享区域
4. 设置程序计数器,使之指向代码的入口

6.5 Hello的进程执行

1、上下文信息:操作系统使用一种称为上下文切换的较高层次的异常控制流来实现多任务。上下文就是进程自身的虚拟地址空间,分为用户级上下文和系统及上下文,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。每个进程的虚拟地址空间和进程本身一一对应(因此和PID一一对应)。由于每个CPU只能同时处理一个进程,而很多时候系统中有很多进程都要去运行,因此处理器只能一段时间就要切换新的进程去运行,而实现不同进程中指令交替执行的机制称为进程的上下文切换。
2、进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。

图6.5.1 进程切换过程
上图为进程A与进程B之间的相互切换。在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个过程称为调度。在此基础上,hello程序与操作系统其他进程通过操作系统的调度,切换上下文,拥有各自的时间片从而实现并发运行。程序在涉及到一些操作时,例如调用一些系统函数,内核需要将当前状态从用户态切换到核心态,执行结束后再及时改用户态,从而保证系统的安全与稳定
处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,为用户模式;设置模式为为内核模式。用户模式就是运行相应进程的代码段的内容,此时进程不允许运行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;而内核模式中,进程可以运行任何指令

6.6 hello的异常与信号处理

6.6.1 异常的种类

如下表所示,异常可以分为四类:中断、陷阱、故障和终止

类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令
终止 不可恢复的错误 同步 不会返回

6.6.2 信号的种类

信号可以被理解为通知,他通知进程系统中发生了一个某种类型的事件。每种信号类型都对应于某种系统事件,它提供了一种机制,通知用户进程发生了这些异常,并让进程执行相应的信号处理程序。Linux中信号有如下种类:

图6.6.1 常见信号

6.6.3 hello的异常与信号处理

  1. 乱按并不会影响进程执行

    图6.6.2 乱按
    2. 键入ctrl+z向进程发送信号,使前台进程挂起

    图6.6.3 ctrl+z
    3. 再键入ps发现ctrl+z只是挂起前台作业,并没有回收hello

    图6.6.4 ps
    4. 键入jobs发现hello的后台号分别为1和2(我执行并挂起了两次hello)

    图6.6.5 jobs
    5. 使用fg 1命令可以将后台任务调到前台,hello继续运行

    图6.6.6 fg
    6. 输入kill -9 11504可以发送SIGKILL信号杀死进程

    图6.6.7 kill
    7. 输入ctrl+c

    图6.68 ctrl+c

6.7本章小结

本章主要介绍了hello进程的执行过程,介绍了shell的处理流程和作用。通过在进程执行时在终端键入不同命令,研究了hello执行过程中各种操作可能引发的异常和信号处理,发现针对不同的shell命令,hello有不同的响应

第7章 hello的存储管理

7.1 hello的存储器地址空间

  1. 逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址,其格式为“段标识符:偏移地址”,例如“6123:3028000”
    2. 线性地址:逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址
    3. 虚拟地址:同线性地址
    4. 物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址,又叫实际地址或绝对地址

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

Intel平台下,逻辑地址的格式为“段标识符:段内偏移量”。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。分段机制将逻辑地址转化为线性地址的步骤如下:
1. 使用段选择符中的偏移值在GDT或LDT表中定位相应的段描述符。
2. 利用段选择符检验段的访问权限和范围,以确保该段可访问。
3. 把段描述符中取到的段基地址加到偏移量上,最后形成一个线性地址

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

线性地址即虚拟地址,虚拟内存与物理内存都被划分为页,并与页号相对应。虚拟地址由虚拟页号(VPN)+虚拟页偏移量(VPO)组成。页表是建立虚拟页号与物理页号(PPN)映射关系的表结构,页表项(PTE)包含有有效位、物理页号、磁盘地址等信息。
通过虚拟页号和页表初始地址能找到相对应的页表项,页表初始地址存储在页表基址寄存器中。页表项存储的页表状态有三种,分别为:未分配,已缓冲,未缓冲。当对应状态为已缓冲时,说明虚拟页所对应的物理页已经存储在内存中,此时页表项存储的物理页号加上物理页偏移量即为物理地址,而物理页偏移量与虚拟页偏移量相同,可以从虚拟地址中直接得出。页表项状态为已缓冲时,页式管理过程如下图所示

图7.3.1 页式管理

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

查找页表的过程也是访问内存的过程,利用局部性原理,把最近使用过的页表项缓存起来,并通过TLB(叫快表)实现。
多级页表即上一级的页表映射到下一级页表,最后一级页表映射到虚拟内存,如果下一级内容都未分配,那么页表项则为空,不映射到下一级,也不存在下一级页表,当分配时再创建相应页表,从而达到节约内存空间的效果。多级页表的实现如下图所示:

图7.4.1 TLB和四级页表

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

我们得到了物理地址之后,先在L1 Cache中寻址。物理地址被分为CT(标记)+CI(索引)+CO(偏移量)三部分,在Cache中寻址,如果L1无缓存则向L2查找,同理L2没有向L3中查找

图7.5.1 三级Cache下的物理内存访问

7.6 hello进程fork时的内存映射

当shell调用fork 函数时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct、区域结构和页表的原样副本,但是它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。通过 fork 创建的子进程拥有父进程相同的区域结构、页表等的一份副本,同时子进程也可以访问任何父进程已经打开的文件。
当 fork 在新进程中返回时,新进程现在的虚拟内存刚好和调用 fork 时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,就会触发一个保护故障。当故障处理程序注意到保护异常是由于进程试图写私有的写时复制区域中的一个页面而引起的,它就会在物理内存中创建这个页面的一个新副本,更新页表条目指向这个新的副本,然后恢复这个页面的可写权限。当故障处理程序返回时,CPU重新执行这个写操作,现在在新创建的页面上这个写操作就可以正常执行了。

7.7 hello进程execve时的内存映射

execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行 hello 需要以下几个步骤:
1. 删除当前进程虚拟地址中已存在的用户区域
2. 映射私有区域,为新程序的代码、数据、bss和栈创建新的区域结构,所有这些新的区域都是私有的、写时复制的。
3. 映射共享区域,将hello与libc.so动态链接,然后再映射到虚拟地址空间中的共享区域。
4. 设置当前进程上下文程序计数器(PC),使之指向代码区域的入口点。

图7.7.1 虚拟地址空间

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

缺页其实就是DRAM缓存未命中。当我们的指令中对取出一个虚拟地址时,若我们发现对该页的内存访问是合法的,而找对应的页表项式发现有效位为0,则说明该页并没有保存在主存中,出现了缺页故障。
此时进程暂停执行,内核会选择一个主存中的一个牺牲页面,如果该页面是其他进程或者这个进程本身页表项,则将这个页表对应的有效位改为0,同时把需要的页存入主存中的一个位置,并在该页表项储存相应的信息,将有效位置为1。然后进程重新执行这条语句,此时MMU就可以正常翻译这个虚拟地址了。

7.9动态存储分配管理

所有动态申请的内存都存在堆上面,用户通过保存在栈上面的一个指针来使用该内存空间。动态内存分配器维护着堆,堆顶指针是brk。有两种方式,其中一种叫显式分配器,使用两个函数,malloc() 和free(),分别用于执行动态内存分配和释放。
malloc()的作用是向系统申请分配堆中指定size个字节的内存空间。函数返回的指针为指向堆里的一块内存。并且,操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序申请时,就会遍历该链表,然后寻找第一个空间大于所申请空间的堆结点,将该结点从空闲结点链表中删除后,将该结点的空间分配给到程序。在使用malloc()分配内存空间后,需释放内存空间,否则就会出现内存泄漏。
free()释放的是指针指向的内存,而不是指针。指针并没有被释放,它仍然指向原来的存储空间。因此指针需要手动释放,指针是一个变量,只有当程序结束时才被销毁。释放了内存空间后,原本指向这块空间的指针仍然存在。但此时指针指向的内容为垃圾,是未定义的。因此,释放内存后要把指针指向NULL,防止该指针后续被解引用。

7.10本章小结

本章主要介绍了hello的存储器地址空间,重点分析了四级页表下的线性地址到物理地址的变换,分析了hello的内存映射、缺页故障与缺页中断处理和动态存储分配管理。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

  1. 文件系统:Linux将所有的IO设备都视为文件,包括硬盘、光驱、串口、网卡等等。每个设备都会被分配一个文件名,例如/dev/sda表示第一个SATA硬盘,/dev/ttyS0表示第一个串口。
    2. 设备驱动程序:Linux系统中有很多设备驱动程序,它们负责控制各种IO设备的工作。设备驱动程序可以通过内核模块的形式加载到内核中,也可以编译进内核中。当系统启动时,内核会自动加载所有需要的设备驱动程序。
    3. 设备文件权限:由于Linux将所有IO设备都视为文件,因此每个设备文件都有自己的权限。管理员可以使用chmod命令来修改设备文件的权限,以控制用户对设备的访问权限。
    4. 设备文件的访问方式:Linux支持多种访问设备文件的方式,包括阻塞式IO、非阻塞式IO、多路复用IO和异步IO。管理员可以根据应用程序的需要来选择不同的访问方式。
    5. 中断处理:当IO设备完成一个操作时,它会向CPU发送一个中断信号。内核会响应这个中断信号,并调用相应的设备驱动程序来处理中断。设备驱动程序可以使用DMA(直接内存访问)来加速数据传输,减少CPU的负担。
    总而言之,所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行:设备的模型化:文件,设备管理:unix io接口

8.2 简述Unix IO接口及其函数

Unix IO接口是一组用于处理文件和数据流的函数集合。这些函数包括文件读写、文件描述符管理、进程通信等操作。
常用的Unix IO函数有:
1. open():打开文件并返回文件描述符。
2. close():关闭文件描述符。
3. read():从文件描述符中读取数据。
4. write():向文件描述符中写入数据
5. lseek():移动文件描述符的读写位置。
6. dup():复制文件描述符。
7. pipe():创建一个管道。
8. select():等待多个文件描述符的IO事件。
9. poll():等待多个文件描述符的IO事件。
10. socket():创建一个套接字。
11. accept():接受一个连接请求。
12. connect():建立一个连接。

8.3 printf的实现分析


图8.3.1 printf函数
printf是一个C语言的标准库函数,用于将格式化的数据输出到标准输出流stdout(通常是屏幕)或者其他输出流中。其实现过程可以大致分为以下几个步骤:
1. 格式化字符串生成显示信息:printf函数的第一个参数是格式化字符串,其中包含了需要输出的数据类型和格式,例如%d表示输出整数,%f表示输出浮点数等等。在调用printf函数时,格式化字符串会被传递给vsprintf函数进行处理,生成实际需要显示的信息。
2. 将显示信息写入输出流:生成的显示信息需要通过输出流输出到屏幕或者其他设备上。在Unix/Linux系统中,可以使用write系统函数将信息写入文件描述符(通常是标准输出流stdout)。在Windows系统中,可以使用WriteFile函数将信息写入控制台或者文件。
3. 调用系统调用将信息传输到显示芯片:在操作系统中,系统调用是一种从用户态切换到内核态的机制,通过系统调用可以访问操作系统内核提供的功能和资源,例如文件、网络、进程等等。在将信息输出到屏幕时,需要通过系统调用将信息传输到显示芯片中,以便在屏幕上显示。在Linux系统中,可以使用int 0x80或syscall指令进行系统调用。
4. 显示芯片将信息显示在屏幕上:最后,显示芯片会按照刷新频率逐行读取vram中存储的每一个点的RGB颜色信息,并通过信号线向液晶显示器传输每一个点的RGB分量,从而在屏幕上显示出完整的信息。

8.4 getchar的实现分析


图8.4.1 getchar函数
在Linux系统中,getchar函数的实现依赖于read系统函数。当调用getchar函数时,它会调用read函数从标准输入流中读取一个字符,直到读取到回车符为止。read函数会将字符存储到缓冲区中,并返回读取的字节数。
在键盘输入方面,当用户按下键盘上的某个键时,键盘会向计算机发送一个中断信号,通知操作系统有一个键盘事件发生。操作系统会调用键盘中断处理程序来处理这个事件。在处理程序中,操作系统会读取键盘扫描码,并将其转换为相应的ASCII码。
转换完成后,操作系统会将ASCII码存储到系统的键盘缓冲区中。当用户调用getchar函数时,它会读取键盘缓冲区中的字符,直到读取到回车符为止。这个过程是异步的,因为键盘中断处理程序和getchar函数是在不同的进程中执行的。

8.5本章小结

本章介绍了Linux的IO设备的管理方法,以及Unix IO函数的功能,并分析了printf函数和getchar函数的实现

结论

  1. 我们程序员就像是造人的女娲,敲击在键盘上的字符就是泥土,我们使用双手在电脑里敲击一个个字符,用字符“粘合”成了hello的身体(main函数)、四肢(外部函数),给hello赋予了智慧(for循环)……创造出了一个个小泥人(hello.c源文件),从被我们创造开始,小泥人hello.c便开始了它的成长之路。
  2. 一开始,小泥人hello.c只是安静的在磁盘中睡觉,直到有一天,它被创造者——程序员唤醒了,但是hello.c和程序员身处两个世界,hello.c活在计算机世界中,计算机世界里程序员为它装配的身体、四肢、大脑好像并不适用,于是hello.c努力适应计算机世界。它经历了预处理,头文件被引入、宏被展开,变成了ASCII码中间文件hello.i,之后它又经历了编译器的处理,变为了汇编语言文件hello.s,随后有经历了汇编器的转换,变成了可重定位目标文件hello.o,最后经过链接,它终于变成了可以在计算机世界自由遨游的可执行文件hello
  3. 这个时候,程序员在shell中输入了命令“./hello 2021110788 左镕畅 1”让小生命hello开始了工作,shell通过调用fork()函数创建了子进程,又通过execve()函数映射到虚拟内存当中,并通过缺页异常将hello放入主存,又通过四级页表、多级缓存,最后hello加载到处理器内部,开展执行它的工作
  4. hello执行了自己的任务后,通过Unix IO函数把任务完成结果输出到屏幕,告诉它的创造者程序员,它完成了任务
  5. 当hello任务完成,进程执行结束后,由父进程对子进程进行回收,hello重新回到了硬盘中,等待程序员的下一次唤醒,至此,hello的一生结束,虽然它的一生如此短暂,但是却坎坷而精彩!

附件

文件名 功能
hello.c 源程序
hello.i 预处理后的文件
hello.s 汇编文件
hello.o 可重定位目标执行文件
hello 可执行文件
hello.elf hello.o的ELF格式
hello1.txt hello.o的反汇编语言
hello2.txt hello的反汇编语言
hello1.elf hello的ELF格式

参考文献

[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] 一个简单程序从编译、链接、装载(执行)的过程-静态链接 - 知乎 (zhihu.com)
[8]虚拟地址空间. 小林coding. 2021-06-27. https://www.zhihu.com/ question/290504400
[9] gcc–编译的四大过程及作用:https://blog.csdn.net/shiyongraow/article/details/81454995
[10] CSDN. 编译器工作流程详解. 2014:04-27.https://blog.csdn.net/u012491514/article/details/24590467
[11] 网络用户. 阿里云. ELF格式文件符号表全解析及readelf命令使用方法. 2018:07-19.https://www.aliyun.com/zixun/wenji/1246586.html
[12] gcc–编译的四大过程及作用:https://blog.csdn.net/shiyongraow/article/details/8145499

猜你喜欢

转载自blog.csdn.net/qq_61683908/article/details/130831118