计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 未来技术学院 人工智能模块
学 号 2022112635
班 级 22WL027
学 生 潘晗
指 导 教 师 郑贵滨
计算机科学与技术学院
2024年5月
为了更好地总结并应用《计算机系统》这门课程的知识,本文以hello.c这一简单的C语言文件为研究对象,在Ubuntu操作系统下深入研究了它的编译、链接、加载、运行、终止和回收过程,即系统而深入地了解了hello.c的一生。同时,通过一个简单的hello.c,本文也将计算机系统的整个体系串联起来,融会贯通并学以致用。
关键词: 计算机系统;hello程序;程序生命周期;编译;链接;进程管理;存储管理;IO管理
目 录
6.2 简述壳Shell-bash的作用与处理流程... - 26 -
6.3 Hello的fork进程创建过程... - 26 -
7.2 Intel逻辑地址到线性地址的变换-段式管理... - 32 -
7.3 Hello的线性地址到物理地址的变换-页式管理... - 33 -
7.4 TLB与四级页表支持下的VA到PA的变换... - 34 -
7.5 三级Cache支持下的物理内存访问... - 34 -
7.6 hello进程fork时的内存映射... - 35 -
7.7 hello进程execve时的内存映射... - 36 -
第1章 概述
1.1 Hello简介
1.1.1 Hello的P2P
P2P,Program to Process,即hello.c文件从可执行程序到正在运行的进程的整个过程。在Ubuntu系统下,从C语言文件到可执行目标文件,hello.c文件依次经历cpp(C预处理器,C Pre-Processor)预处理,ccl(C编译器,C Compiler)编译,as(汇编器,Assembler)汇编以及ld(链接器,Linker)链接四个过程。随后,向shell程序输入命令./hello后,shell程序调用fork()函数产生子进程。这样,hello就从Program变成了Process。
1.1.2 Hello的020
020,From 0 to 0,即从一开始系统中没有任何与hello.c相关的内容,到最后hello被回收的过程。在shell程序中输入./hello命令后,shell程序将调用fork()函数并通过调用execve()函数在进程的上下文中加载并运行hello。然后系统将进程映射到虚拟内存,并加载需要的物理内存。执行hello时,相关指令进入CPU流水线执行。执行结束后,父进程将回收这一进程,内核将清除这一进程的相关信息,这一进程就结束了。hello.c从0到0,挥一挥手,不带走一片云彩。
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;3.2GHz;16G RAM;512GHD Disk
1.2.2 软件环境
Windows11 64位;Vmware 17;Ubuntu 22.04 LTS 64位
1.2.3 开发工具
Visual Studio 2022 64位;
gcc ;vim;gdb
1.3 中间结果
中间结果如表1所示。
表1 中间结果
作用 | |
hello.c | 源程序文本文件 |
hello.i | 预处理后的文本文件 |
hello.s | 汇编后的二进制文件 |
hello.o | 可重定位目标文件 |
hello | 可执行目标文件 |
hello1.elf | 由hello.o生成的.elf文件 |
hello2.elf | 由hello生成的.elf文件 |
hello1.s | hello.o的反汇编文件 |
hello2.s | hello的反汇编文件 |
1.4 本章小结
本章从P2P和020两方面介绍了Hello,并列出了研究hello的程序人生的软硬件环境和开发工具,以及本文生成的中间结果文件和它们的作用。
第2章 预处理
2.1 预处理的概念与作用
2.1.1 预处理的概念
预处理是指在源代码编译之前先由预处理器(Preprocessor)进行的处理步骤。预处理器通常会对源代码进行一些替换、添加或者删除操作,以便为后续的编译阶段做准备。
常见的预处理指令包括宏定义、条件编译、文件包含等。通过宏定义,程序员可以在代码中定义一些符号或者简单的代码片段,方便后续代码的使用和维护。条件编译是通过指定条件来控制不同代码块的编译,从而实现代码的灵活性。文件包含可以通过在代码中包含其他文件的内容,实现代码的复用和模块化。
2.1.2 预处理的作用
① 处理头文件:形如#include<stdio.h>的命令告诉预处理器读取系统有文件stdio.h的内容,并把它直接插入程序文本中。
② 处理宏定义:对于#define指令,进行宏替换;对于代码中所有使用宏定义的地方使用符号表示的实际值替换定义的符号。
③ 处理条件编译:根据可能存在的#ifdef来确定程序需要执行的代码段。
④ 处理特殊符号:预编译程序可以识别一些特殊的符号,并在后续过程中进行合适的替换。
⑤ 删除注释:删除c语言源程序中的注释部分。
2.2在Ubuntu下预处理的命令
Ubuntu下预处理的命令为gcc -E hello.c -o hello.i,如图1所示。
图1 Ubuntu下预处理命令
2.3 Hello的预处理结果解析
如图2(a)所示,hello.c源程序文件共24行,但hello.i文件有超过3000行。main()函数前是大段的头文件stdio.h、unistd.h、stdlib.h以及这三个头文件通过#include插入的其他头文件的展开。如图2(b)所示,hello.i文件的末尾是hello.c源程序,但可以发现源程序文件中的注释已经消失。
图2(a) hello.i文件的开头部分
图2(b) hello.i文件的结尾部分
2.4 本章小结
本章介绍了预处理的概念和作用,并以hello.c的预处理为例分析了预处理器对源程序文件的处理,例如头文件展开、去除注释等。
第3章 编译
3.1 编译的概念与作用
3.1.1 编译的概念
编译是ccl(C编译器,C Compiler)通过语法和词法分析,将合法的指令转换成等价的汇编指令的过程。通过编译,ccl将文本文件hello.i转换成二进制汇编语言文件hello.s。
3.1.2 编译的作用
① 词义分析:扫描器将源代码中的字符序列分割为一系列c语言中的符合语法要求的字符单元。
② 语法分析:基于词法分析得到的字符单元生成语法分析树。
③ 语义分析:在语法分析完成之后由进行语义分析,以判断指令是否是合法的c语言指令。
④ 生成中间代码:中间代码可使编译程序的逻辑更加明确,以便提升下一步代码优化的效果。
⑤ 代码优化:根据用户指定的不同优化等级对代码进行安全的、等价的优化,以便提升代码在执行时的性能。
⑥ 生成代码:经过上面的所有过程后,ccl将会生成一个汇编语言代码文件,也就是我们最后得到的hello.s文件,这一文件中的源代码将以汇编语言的格式呈现。
3.2 在Ubuntu下编译的命令
Ubuntu下编译的命令为gcc hello.i -S -o hello.s,如图3所示。
图3 Ubuntu下编译命令
3.3 Hello的编译结果解析
3.3.1 文件结构分析
Hello的编译结果如图4所示。对hello.s文件的整体分析如表2所示。
图4 Hello的编译结果
表2 hello.s的整体结构
含义 | |
.file | 源文件 |
.text | 代码段 |
.section .rodata | 存放只读变量 |
.global | 存放全局变量 |
.data | 存放已初始化的全局变量和static变量 |
.align | 对齐方式 |
.type | 函数类型/对象类型 |
.size | 大小 |
.long .string | long类型/string类型 |
3.3.2 数据
① 常量
(1)数字常量
如图5所示,源代码中的数字常量储存在.text段,例如比较时使用的数字变量,以及循环时使用的循环比较变量等。
图5 hello.s中的数字常量
(2)字符串常量
如图6所示,在printf等函数中使用的字符串常量储存在.rotate段中。
图6 hello.s中的字符串常量
② 变量
(1)局部变量
局部变量储存在栈中的某一个位置或直接储存在寄存器中。源代码中的局部变量共有:循环变量i,argc和argv。如图7所示,i储存在栈中地址为-4(%rbp)的位置。如图8所示,argc表示程序运行时输入变量的个数,储存在栈中地址为-20(%rbp)的位置。如图9所示,argv是一个保存输入变量的数组,储存在栈中。
图7 hello.s中的局部变量i
图8 hello.s中的局部变量argc
图9 hello.s中的局部变量argv
3.3.3 赋值
源代码中的循环对局部变量i进行了赋值操作。如图10所示,对于局部变量i,每次循环结束的时候都对它进行+1操作。
图10 hello.s中对局部变量i的赋值操作
3.3.4 类型转换
在源程序中,对argv数组进行了从字符串到整数的类型转换。如图11所示,在hello.s中,程序跳转到PLT(Procedure Linkage Table)中保存atoi函数入口地址的位置,并开始执行atoi函数。
图11 hello.s中的类型转换
3.3.5 算术操作
因为局部变量i是循环变量,所以在每一次循环中都要修改这个值。hello.s中的汇编语句如图12所示。
图12 hello.s中的算术操作
3.3.6 关系操作
源程序中的关系操作有:对于argc是否等于5的判断和对于i是否小于10的判断。如图13所示,当argc等于5时进行条件跳转。如图14所示,当i大于等于9时进行条件跳转。
图13 hello.s中对于argc的关系操作
图14 hello.s中对于i的关系操作
3.3.6 数组/指针/结构体操作
① 数组操作
源程序中只有对于argv数组的操作。如图15所示,argv储存的三个值都存放在栈中:argv[1]的储存地址是-8(%rbp),argv[2]的储存地址是-16(%rbp),argv[3]的储存地址是-24(%rbp)。
图15 hello.s中的数组操作
3.3.7 函数调用
源程序中的调用的函数有:main、printf、exit、sleep和getchar。在64位系统中,第1~6个参数依次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,其余的参数保存在栈中。
① main函数
如图16所示,调用main函数时传入参数argc和argv,其中argv储存在栈中,argc储存在%rdi中。源程序通过return 0从main函数返回,而在hello.s中则是调用了exit(1)。
图16 hello.s中的main函数
② printf函数
如图17(a)和图17(b)所示,第一次调用printf函数时只传入了字符串参数首地址,并在满足if条件时调用。而第二次调用时for传入了 argv[1]、argc[2]和argc[3]的地址,并在for循环条件满足时调用。
图17(a) hello.s中第一次调用printf函数
图17(b) hello.s中第二次调用printf函数
③ exit函数
如图18所示,当满足if条件时传入参数1并调用exit函数。
图18 hello.s中的exit函数
④ sleep函数
如图19所示,sleep函数在for循环条件满足时被调用。
图19 hello.s中的sleep函数
⑤ getchar函数
如图20所示,当循环结束时调用getchar函数。
图20 hello.s中的getchar函数
3.4 本章小结
本章介绍了编译的概念和作用,并结合hello.s中出现的C语言的数据与操作分析了ccl对C语言的各个数据类型及各类操作的处理过程。
第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
汇编是指as(汇编器,Assembler)将以.s结尾的文本文件转换成机器指令并生成以.o结尾的可重定位目标文件的过程。
4.1.2 汇编的作用
汇编将汇编语言根据特定的转换规则转换为机器指令,便于后续链接器对可重定位目标文件进行重定位操作。
4.2 在Ubuntu下汇编的命令
Ubuntu下汇编的命令为gcc -c hello.c -o hello.o,如图21所示。
图21 Ubuntu下的汇编命令
4.3 可重定位目标elf格式
因为汇编器生成的hello.o是二进制文件,无法使用vim查看,所以在Ubuntu的终端输入readelf -a hello.o > hello1.elf命令,在hello1.elf中查看elf头的相关信息。
4.3.1 ELF头
如图22所示,ELF头以一个16字节的序列开始。该序列描述了生成该文件系统下的字的大小以及一些其他信息。剩余部分则包含了便于链接器进行语法分析和解释目标文件的相关信息,如ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,节头部表中条目的大小和数量等。
图22 可重定位目标文件的ELF头
4.3.2 节头
如图23所示,节头描述了hello.o文件中每个节的名称、地址、大小、偏移量等信息。
图23 可重定位目标文件的节头
4.3.3 重定位节
如图24所示,重定位节包含了源代码中使用的外部变量等信息。这些信息便于链接器在链接时对这些变量或符号的位置进行修改。在Hello中,需要重定位的信息有exit、printf、sleep、getchar等函数的位置等。源程序的重定位类型仅有R_X86_64_PC32(PC相对寻址)和R_X86_64_PLT32(使用PLT表寻址)两种,而未出现R_X86_64_32(绝对寻址)。
图24 可重定位目标文件的重定位节
4.3.4 符号表
如图25所示,符号表包含了在源程序中定义和引用的函数和全局变量的相关信息。
图25 可重定位目标文件的符号表
4.4 Hello.o的结果解析
在Ubuntu终端中输入objdump -d -r hello.o > hello1.s命令,在文本文件hello1.s中查看hello.o的反汇编结果。
如图26所示,与hello.s对照,在hello.o的反汇编结果中,hello.o的每条机器指令都对应着一条汇编指令。而不同之处如下:
(1)反汇编结果中带有重定位信息,如R_X86_64_PLT32等。
(2)在调用函数时,反汇编结果使用相对偏移量代替hello.s中的函数名。
(3)对于数据的表示,反汇编结果使用十六进制,而hello.s使用十进制。
(4)对于分支转移,反汇编结果使用跳转的目标地址代替hello.s中的.L2等代码块的名字。
图26 hello.o的反汇编结果
4.5 本章小结
本章介绍了汇编的概念和作用,通过可重定位目标文件的elf格式查看了汇编结果并通过反汇编的结果与hello.s文件的对比从侧面了解了汇编的作用。
第5章 链接
5.1 链接的概念与作用
5.1.1 链接的概念
链接是指ld(链接器,Linker)将各种不同文件的源代码和数据块等文件收集并组合成一个单一的可执行目标文件的过程。执行链接操作的时机有:编译时、加载时和运行时。
5.1.2 链接的作用
ld将已完成编译的若干可重定位目标文件合并成为一个可执行目标文件,使得各个文件的独立编译成为可能。当改变各个文件模块中的一个时,只需简单重新编译它并重新链接即可,不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
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,如图27所示。
图27 Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
在Ubuntu终端中输入readelf -a hello > hello2.elf命令,在hello2.elf文件中查看由链接器生成的可执行目标文件hello。
5.3.1 ELF头
如图28所示,ELF头列出可执行目标文件各段的基本信息,如各段的起始地址、大小等。
图28 可执行目标文件的ELF头
5.3.2 节头
如图29所示,节头包含了可执行目标文件中各节的大小、偏移量等信息。链接器链接时将各个文件的相同段合成一段,并且根据该合成段的大小以及偏移量重定位各个符号的地址。
图29 可执行目标文件的节头(部分)
5.3.3 重定位节
如图30所示,可执行目标文件的重定位节与可重定位目标文件的重定位节不同。前者的每个重定位条目的符号名称变为具体的函数名且加数均为0,说明链接过程中确实实现了重定位。
图30 可执行目标文件的重定位节
5.3.4 符号表
如图31所示,对比可重定位目标文件中的符号表,可执行目标文件中的符号表增加了来自.dynsym的符号,同时原本.symtab中的符号数量也有所增加。
图31 可执行目标文件的符号表(部分)
5.4 hello的虚拟地址空间
如图32所示,使用edb加载hello,并在Data Dump窗口中查看该进程的虚拟空间的各段信息。源程序是从地址0x401000开始,在地址0x402000处结束。通过起始处的ELF标识,可获取从可执行文件时加载的信息。
图32 使用edb查看虚拟空间的信息
5.5 链接的重定位过程分析
在Ubuntu的终端中输入objdump -d -r hello > hello2.s命令,在hello2.s文件中查看hello的反汇编结果。hello的反汇编结果与hello.o的反汇编结果的不同之处有:
① 如图33所示,链接器加入了puts、exit等库函数以及它们的虚拟地址。
图33 hello的反汇编结果1
② 如图34所示,链接器加入了一些在hello.o中需要重定位的符号或变量的虚拟地址。
图34 hello的反汇编结果2
③ 如图35所示,hello的反汇编结果中增加了.init节和.plt节,且无重定位节。
图35 hello的反汇编结果3
通过5.5节中的分析可知,链接分为符号解析和重定位两个过程。
① 符号解析:在可重定位目标文件中定义和引用符号,而连接过程中的符号解析则将每个符号引用和一个符号定义关联起来。
② 重定位:链接器通过把每个符号定义与一个内存位置关联起来,从而重定位可重定位目标文件中的数据节和代码节等,然后修改这些符号的引用,使得它们指向这个内存位置。
5.6 hello的执行流程
使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。
如图36所示,由edb执行hello的结果可知,从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)调用与跳转的各个子程序名或程序地址如下表所示。
表3 hello的执行流程
程序名称 | 程序地址 |
<_init> | 0x401000 |
<.plt> | 0x401020 |
<puts@plt> | 0x401090 |
<printf@plt> | 0x4010a0 |
<getchar@plt> | 0x4010b0 |
<atoi@plt> | 0x4010c0 |
<exit@plt> | 0x4010d0 |
<sleep@plt> | 0x4010e0 |
<_start> | 0x4010f0 |
<_dl_relocate_static_pie> | 0x401120 |
<main> | 0x401125 |
<_fini> | 0x4011c8 |
5.7 Hello的动态链接分析
在elf文件中找到.got的地址,并使用edb在dl_init处设置断点,分析在dl_init前后该地址的变化情况,如图36(a)和(b)所示。
当程序调用一个由共享库定义的函数时,由于编译器无法获得函数的地址,所以编译系统将过程地址的绑定推迟到第一次调用该过程时进行,即通过GOT和过程链接表PLT的协作来解析函数的地址。在加载时,动态链接器会重定位GOT中的每个条目,使它包含正确的绝对地址,而PLT中的每个函数负责调用不同函数。
图36(a) dl_init前
图36(b) dl_init后
5.8 本章小结
本章介绍了链接的概念和作用,通过gdb调试、反汇编等方式了解了链接器的作用以及不同的链接方法。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 进程的概念
进程是一个执行中程序的实例。系统中的每个程序都运行在某个进程的由程序正确运行所需的状态组成的上下文中。
6.1.2 进程的作用
进程给应用程序提供关键抽象:
① 一个独立的逻辑控制流:提供一个程序独占处理器的假象
② 一个私有的地址空间:提供一个程序独占地使用内存系统的假象
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 Shell-bash的作用
Shell-bash执行一系列的读取或求值操作后终止。读取操作是读取来自用户的一个命令行;求值操作是解析命令行,并根据解析结果运行程序。
6.2.2 Shell-bash的处理流程
① 对用户输入的命令进行解析,判断命令是否为内置命令。
② 如果为内置命令,调用内置命令处理函数;如果不是内置命令,就创建一个子进程,将程序在该子进程的上下文中运行。
③ 判断为前台程序还是后台程序,如果是前台程序则直接执行并等待执行结束,如果是后台程序则将其放入后台并返回。
④ 同时对键盘输入的信号和其他信号有特定的处理。
6.3 Hello的fork进程创建过程
① 父进程通过调用fork函数创建一个新的运行的子进程:新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的一份副本,包括代码和数据段、堆、共享库以及用户栈。这就意味着当父进程fork时,子进程可以读取父进程中打开的任何文件。
② 父进程和子进程之间的最大区别是他们有不同的PID:在父进程中,fork返回子进程的PID。在子进程中,fork返回0,返回值提供一个明确的方法来分辨程序是父进程还是在子进程中执行。
6.4 Hello的execve过程
execve函数:int execve(const char *filename, const char *argv[], const char envp[]),其作用是在当前进程的上下文中加载并运行一个程序。
execve函数加载并运行可执行文件filename,带参数列表argv和环境变量列表envp且execve调用一次并从不返回。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
上下文信息:进程执行过程中保存的一组状态信息,用于描述进程当前的执行状态和环境,包括寄存器状态、进程标识符等。
进程时间片:一个固定的时间单位,用于限制每个进程在CPU上执行的时间长度。在操作系统中,为了实现公平的资源分配和高效的任务调度,通常采用时间片轮转调度算法。每个进程被分配一个固定的时间片,当进程执行完当前时间片后,操作系统会将CPU资源切换给下一个进程,以便实现多任务切换。
用户态:操作系统中进程执行的一种模式,与内核态相对。在用户态下,进程只能访问用户空间的内存和资源,不能直接访问内核空间的资源和执行内核级别的操作。
核心态:操作系统中进程执行的一种模式,与用户态相对。在核心态下,进程可以直接访问内核空间的资源和执行内核级别的操作。
进程调度的过程:如图37所示,即使系统中通常有许多其他程序在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。如果使用调试器单步运行程序,会看到一系列的程序计数器(PC)的值,这些值唯一对应于包含在运行时动态链接到程序的共享对象中的指令。这个PC的序列叫做逻辑控制流。进程轮流占用处理器,每个进程执行它的流的一部分,然后轮到其他进程。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策是调度,它是由内核中调度器的代码处理的。当内核选择一个新的进程运行时,内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
图37 进程调度示意图
6.6 hello的异常与信号处理
6.6.1 hello执行过程中的异常类型
hello执行过程中的异常类型如表4所示。
表4 hello执行过程中的异常类型
异常类型 | 原因 | 返回操作 |
中断 | 来自I/O设备的信号 | 返回到出现异常的下一条机器指令 |
陷阱 | 有意的异常 | 返回到出现异常的下一条机器指令 |
故障 | 潜在可恢复的错误 | 可能返回到引起异常的当前机器指令 |
终止 | 不可恢复的错误 | 不会返回 |
6.6.2 hello的正常执行效果
如图38所示,hello正常运行时会按照输入打印10次个人信息且在输入任意一个字符后结束并被回收。
图38 hello正常运行的效果
6.6.3 不停乱按
如图39所示,屏幕输入将被缓存到缓冲区。乱按输入内容会被认为是命令,不影响当前进程后续的执行。
图39 不停乱按的结果
6.6.4 按下Ctrl-z
如图40所示,当hello在运行时按下Ctrl-z,系统会产生中断异常信号。hello的父进程会接收到信号SIGSTP,系统执行相应的信号处理程序。结果是程序被挂起,并打印相关挂起信息。
图40 按下Ctrl-z的结果
① 按下Ctrl-z后运行ps命令:如图41所示,在按下Ctrl-Z后运行ps命令,终端打印出了各进程的pid,其中包括之前挂起的进程hello。
图41 在按下Ctrl-z后执行ps命令的结果
② 按下Ctrl-z后运行jobs命令:如图42所示,在按下Ctrl-z后执行jobs命令,终端打印出被挂起进程组的jid,其中包括之前被挂起的hello。它以被挂起的标志Stopped被标识。
图42 在按下Ctrl-z后执行jobs命令的结果
③ 按下Ctrl-z后运行pstree命令:如图43所示,按下Ctrl-z后运行pstree命令,终端打印系统中所有进程的树状结构,显示进程的父子关系和层级。
图43 按下Ctrl-z后运行pstree命令的结果(部分)
④ 按下Ctrl-z后运行fg命令:如图44所示,按下Ctrl-z后运行fg命令,系统将之前挂起的hello重新调到前台来执行,打印出剩余部分,然后输入任意字符,程序运行结束,进程被回收。
图44 按下Ctrl-z后运行fg命令的结果
⑤ 按下Ctrl-z后运行kill命令:如图45所示,重新运行hello,可知它的pid为4668。按下Ctrl-z后输入kill -9 4668命令,则hello进程被杀死。
图45 按下Ctrl-z后运行kill命令的结果
6.6.5 按下Ctrl-c
如图46所示,按下Ctrl-c后,进程接收到SIGINT信号,被彻底杀死。
图46 按下Ctrl-c后的结果
6.7本章小结
本章介绍了进程的概念和作用,简述了Shell-bash的作用和处理过程、Hello fork和execve的过程,并结合hello的实际运行过程分析了不同的异常与信号处理过程。
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1 逻辑地址
逻辑地址是计算机内存中的虚拟地址,用于程序的编写和执行。
7.1.2 线性地址
线性地址是在操作系统管理下的虚拟地址,也称为虚拟地址。它在逻辑地址和物理地址之间的一种转换形式。
7.1.3 虚拟地址
虚拟地址是指在计算机系统中由操作系统分配给进程或线程的地址空间。它是程序员在编程时使用的地址,与实际的物理地址是分开的,需要通过内存管理单元(MMU)或转换表等机制进行地址转换,以映射到物理内存中的实际存储位置。如图35所示,在反汇编文件中显示的均为虚拟地址。
7.1.4 物理地址
物理地址是计算机存储系统中真实的内存地址,它表示RAM(随机存取存储器)中的实际存储位置。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在Intel处理器架构把一个程序分成若干个段进行存储,每个段都是一个逻辑实体,即段式管理。段式管理是通过段表进行的,包括段号、段起点等。程序通过分段划分为代码段、数据段等。
逻辑地址是程序源码编译后所形成的跟实际内存没有直接联系的地址,即在不同的机器上使用相同的编译器来编译同一个源程序则其逻辑地址是相同的,但在不同机器上生成的线性地址是不相同的。
一个逻辑地址是两部分组成的,包括段标识符和段内偏移量。段标识符是由一个16位长的字段组成的,称为段选择符。前13位是一个索引号,后3位为一些硬件细节。索引号即是“段描述符”的索引,段描述符具体地址描述了一个段,很多个段描述符就组成了段描述符表。通过段标识符的前13位直接在段描述符表中找到一个具体的段描述符。
如图47所示,整个系统只有一个全局描述符表(GDT),它包含操作系统使用的代码段、数据段、堆栈段的描述符以及各任务、各程序的LDT(局部描述符表)段。而每个任务或程序有一个独立的LDT,它包含该任务或程序私有的代码段、数据段、堆栈段的描述符以及该任务或程序使用的门描述符。
图47 段式管理示意图
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是一种内存空间存储管理的技术,它将各进程的虚拟空间划分成若干个长度相等的页,把内存空间按页的大小划分成片或者页面,然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
如图48所示,虚拟地址由虚拟页号VPN和虚拟页偏移VPO组成。首先MMU从虚拟地址中抽取出VPN,并且检查TLB,判断它是否因为前面某个内存引用缓存了PTE的一个副本。TLB从VPN中抽取出TLB索引和TLB标记,查找对应组中是否有匹配的条目。若命中,则将缓存的PPN返回给MMU。否则,MMU需从页表中的PTE中取出PPN,若得到的PTE无效或标记不匹配,就产生缺页,内核需调入所需页面,重新运行加载指令,若有效,则取出PPN。最后将线性地址中的VPO与PPN连接起来就得到了对应的物理地址。
图48 页式管理示意图
7.4 TLB与四级页表支持下的VA到PA的变换
如图49所示,在四级页表中,36位的VPN被划分成四个9位的片段,每个片段被用作到一个页表的偏移量。CR3寄存器中存放了一级页表的物理地址。VPN1提供了相对于一级页表起始地址的偏移量,这些页表条目包含二级页表的基地址。VPN2提供了相对于二级页表起始地址的偏移量,三级页表和四级页表如此类推。
图49 TLB与四级页表支持下的VA到PA的转换示意图
7.5 三级Cache支持下的物理内存访问
通用的高速缓存的结构如图50所示,其中每个地址位被划分成了t个标记位,s个组索引位和b个块偏移位。
根据地址位信息,首先进行组定位。然后进行行定位,即检查该组中是否有行匹配且该行的有效位为1。如果成立,则缓存命中,返回定位到的数据。否则,缓存不命中,需要从存储器层次结构的下一层中取出被请求的块,然后将新的块存储在组索引位指示组中的一个高速缓存行中。其中具体替换哪一行取决于替换策略。
图50 高速缓存的结构示意图
7.6 hello进程fork时的内存映射
hello进程fork时首先为新进程创建虚拟内存,包括创建当前进程的mm_struct、vm_area_struct和页表的原样副本,且两个进程中的每个页面都标记为只读,两个进程中的每个区域结构都被标记为私有的写时复制。
如图51所示,在新进程中返回时,新进程拥有与父进程相同的虚拟内存。其写操作通过写时复制机制创建新页面。
图51 私有的写时复制机制
7.7 hello进程execve时的内存映射
如图52所示,hello进程execve时首先删除已存在的用户区域,然后创建新的区域结构。后者包括将代码和初始化数据映射到.text和.data区,将.bss和栈映射到匿名文件。随后设置PC,使其指向代码区域的入口。Ubuntu系统根据需要换入代码和数据页面。
图52 execve时的内存映射
7.8 缺页故障与缺页中断处理
如图53所示,当MMU在试图翻译某个虚拟地址VA时触发了一个缺页故障,那么这个异常会导致控制转移到内核的缺页处理程序:
① 判断这个虚拟地址VA是否在某个区域结构定义的区域内:如果这个VA不合法,那么缺页处理程序就会触发一个段错误,进而终止这个进程。
② 判断该进程是否有读、写或者执行这个区域内页面的权限:如果试图进行的访问不合法,那么缺页处理程序会触发一个保护异常并终止进程。
③ 如果缺页是由于对合法的虚拟地址进行合法的操作造成的,那么内核会选择一个牺牲页面。如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送VA到MMU。这次就不会出现缺页中断异常了。
图53 缺页故障与缺页中断处理
7.9动态存储分配管理
动态存储分配管理时一种内存管理方法,即对于内存空间的分配、回收等操作在进程执行过程中进行,以便更好地适应系统的动态需求,提高内存利用率。
分配器分为两种:
① 显示分配器:要求应用显示地释放任何已分配的块。
② 隐式分配器:要求分配器检测一个已分配的块何时不再被程序所使用,那么就释放这个块。
动态存储分配管理的基本方法与策略如下:
① 带边界标签的隐式空闲链表分配器管理:带边界标记的隐式空闲链表的每个块由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成。当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个符合大小的空闲块来放置这个请求块。
② 显示空间链表管理:显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。
7.10本章小结
本章介绍了地址空间的分类、段式管理与页式管理、VA到PA的转换、物理内存访问机制、fork与execve时的内存映射、缺页故障及其处理以及动态存储分配管理等内容。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
8.1.1 设备的模型化
文件,即所有的I/O设备都被视为文件,而所有的输入输出都被视为对相应文件的读、写操作。
8.1.2 设备管理
unix io接口,这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口。
8.2 简述Unix IO接口及其函数
8.2.1 Unix I/O接口
① 打开文件:一个应用程序通过要求内核打开相应的文件,表示它想要访问一个I/O设备。内核返回一个小的非负整数(描述符),并在后续对此文件的所有操作中标识这个文件。
② 读写文件:读操作即从文件复制n个字节到内存当前文件位置k处。若给定一个大小为m字节的文件,那么当k>=m时的读操作会触发一个EOF。
③ 关闭文件:内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符地址中。
8.2.2 Unix I/O函数
① open函数:进程通过调用open函数来打开一个已存在的文件或者创建一个新文件。
② close函数:进程通过调用close函数来关闭一个打开的文件。
8.3 printf的实现分析
printf函数的函数体如图54所示。其中,va_list是一个字符指针,(char*)(&fmt + 4)表示fmt后的第一个参数的地址。如图55所示,vsprintf函数返回值是要打印出来的字符串的长度,其作用是格式化,即产生格式化的输出并保存在buf中。最后的write函数即为写操作,把buf中的i个元素的值输出到终端。
图54 printf函数的函数体
图55 vsprintf函数的函数体
如图56所示,追踪write函数可知,其中的int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call函数。在write函数中可以理解为其功能为显示格式化了的字符串。字符显示驱动子程序从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
图56 追踪write函数
图57 sys_call函数的实现
8.4 getchar的实现分析
getchar函数的函数体如图58所示。在getchar函数中声明了几个静态变量:缓冲区buf,缓冲区的最大长度BUFSIZ,指向缓冲区的首地址的bb指针。getchar调用read函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。read将缓冲区读入到buf中,并将长度送给n,再重新令bb指针指向buf。最后返回buf中的第一个字符。如果长度n<0,则出现EOF报错。其中,对于异步异常-键盘中断的处理方式是键盘中断处理子程序,它接受按键扫描码转成ascii码,并保存到系统的键盘缓冲区。
图58 getchar函数的函数体
8.5本章小结
本章介绍了Linux的I/O设备管理方法、Unix I/O接口及函数,并具体分析了printf函数和getchar函数的实现方式。
结论
hello的一生:
(1)预处理:
cpp将hello.c中的所有头文件内容直接插入到程序中,并去掉注释等内容,生成hello.i文件。
(2)编译:
ccl通过语法分析和词法分析将合法的C语言指令转换成等效的汇编指令,生成hello.s文件。
(3)汇编:
as将汇编指令转换成等价的机器指令,并将这些指令转换成可重定位目标文件的格式,便于后续的重定位,生成hello.o文件。
(4)链接:
ld将hello的代码和动态链接库等文件整理成一个文件,进行符号解析和重定位后生成可执行目标文件hello。
(5)加载:
在Ubuntu终端中将./hello命令输入Shell-bash程序中,Shell-bash程序调用fork函数为hello创建新进程,并调用execve函数将代码段和数据段等映射到虚拟内存。程序执行时在通过缺页中断处理和私有写时复制等机制换入代码页或数据页。
(6)执行指令:
处理器为hello进程分配调度时间片。在该调度时间片内,hello进程享有处理器的全部资源,并顺序地执行其逻辑控制流。
(7)访问内存:
MMU将虚拟地址翻译成物理地址,并通过三级高速缓存访问内存或磁盘上的数据。
(8)信号处理:
当hello进程被切换回来时,它会使用默认方式或自定义的信号处理函数对接收到的信号进行处理。
(9)终止和回收:
Shell-bash等待并回收hello进程,回收后内核会删除hello进程相关的所有信息。
感悟:
通过《计算机系统》这门课程和hello的一生,我从编译的四个阶段、程序的加载和运行、内存管理、信号处理等方面更加深入地了解了计算机系统的设计与实现思想,这将十分有利于我日后在深入理解计算机系统的基础上写出更正确、更快的程序。
附件
文件名称 | 作用 |
hello.c | 源程序文本文件 |
hello.i | 预处理后的文本文件 |
hello.s | 汇编后的二进制文件 |
hello.o | 可重定位目标文件 |
hello | 可执行目标文件 |
hello1.elf | 由hello.o生成的.elf文件 |
hello2.elf | 由hello生成的.elf文件 |
hello1.s | hello.o的反汇编文件 |
hello2.s | hello的反汇编文件 |
参考文献
[1] Randal E.Bryant David R.O’Hallaron. 深入理解计算机系统(第三版). 机械工业出版社,2016.
[2] https://www.cnblogs.com/pianist/p/3315801.html