哈工大 计算机系统大作业 Hello的一生

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学   号
班   级
学 生    
指 导 教 师 史先俊

计算机科学与技术学院
2023年4月
摘 要
“Hello World”是每个程序员再熟悉不过的一个字符串,本文通过展现一个hello.c程序的诞生、运行再到死亡的历程,探究hello一生中背后的奥秘。从写下hello.c的源代码,到预处理、编译、汇编、链接再到进程开始后的进程、存储、以及I/O管理,通过分析每一阶段背后发生的细节,来让我们与hello进行一次邂逅,以深入了解小小hello背后庞大精妙的计算机系统。

关键词:计算机系统;预处理;编译;汇编;链接;进程;存储;I/O;

目 录

第1章 概述 - 4 -
1.1 Hello简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 4 -
第2章 预处理 - 5 -
2.1 预处理的概念与作用 - 5 -
2.2在Ubuntu下预处理的命令 - 5 -
2.3 Hello的预处理结果解析 - 5 -
2.4 本章小结 - 5 -
第3章 编译 - 6 -
3.1 编译的概念与作用 - 6 -
3.2 在Ubuntu下编译的命令 - 6 -
3.3 Hello的编译结果解析 - 6 -
3.4 本章小结 - 6 -
第4章 汇编 - 7 -
4.1 汇编的概念与作用 - 7 -
4.2 在Ubuntu下汇编的命令 - 7 -
4.3 可重定位目标elf格式 - 7 -
4.4 Hello.o的结果解析 - 7 -
4.5 本章小结 - 7 -
第5章 链接 - 8 -
5.1 链接的概念与作用 - 8 -
5.2 在Ubuntu下链接的命令 - 8 -
5.3 可执行目标文件hello的格式 - 8 -
5.4 hello的虚拟地址空间 - 8 -
5.5 链接的重定位过程分析 - 8 -
5.6 hello的执行流程 - 8 -
5.7 Hello的动态链接分析 - 8 -
5.8 本章小结 - 9 -
第6章 hello进程管理 - 10 -
6.1 进程的概念与作用 - 10 -
6.2 简述壳Shell-bash的作用与处理流程 - 10 -
6.3 Hello的fork进程创建过程 - 10 -
6.4 Hello的execve过程 - 10 -
6.5 Hello的进程执行 - 10 -
6.6 hello的异常与信号处理 - 10 -
6.7本章小结 - 10 -
第7章 hello的存储管理 - 11 -
7.1 hello的存储器地址空间 - 11 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -
7.5 三级Cache支持下的物理内存访问 - 11 -
7.6 hello进程fork时的内存映射 - 11 -
7.7 hello进程execve时的内存映射 - 11 -
7.8 缺页故障与缺页中断处理 - 11 -
7.9动态存储分配管理 - 11 -
7.10本章小结 - 12 -
第8章 hello的IO管理 - 13 -
8.1 Linux的IO设备管理方法 - 13 -
8.2 简述Unix IO接口及其函数 - 13 -
8.3 printf的实现分析 - 13 -
8.4 getchar的实现分析 - 13 -
8.5本章小结 - 13 -
结论 - 14 -
附件 - 15 -
参考文献 - 16 -

第1章 概述
1.1 Hello简介

P2P:
指先手动编写hello.c的源程序,然后调用cpp预处理生成hello.i,ccl编译生成hello.hello.s,汇编器as汇编生成hello.o再通过链接器ld链接,生成hello可执行文件。之后在shell中调用hello可执行文件,便可以创建一个新的process,调用execve进行加载运行

图 1 从hello.c到hello可执行文件

020 :
shell使用execve函数为其fork子进程,运行hello程序,映射虚拟内存,并从程序入口开始载入物理内存,设置程序计数器,再进入main函数执行目标代码,CPU控制hello运行的时间片,执行逻辑控制流,I/O管理hello的工作,使其产生屏幕上的输出。最后shell将退休的hello回收,OS中的hello将被删除。
1.2 环境与工具
硬件环境:
处理器:AMD Ryzen 5 5625U, 2301Mhz, 16G RAM, 512G SSD
软件环境:
WIN11, Ubuntu 22.02
工具:
vim, gcc, objdump, edb, readelf
1.3 中间结果
hello.c 源代码

hello.i 预处理后的文本文件

hello.s 编译之后的汇编文件

hello.o 汇编之后的可重定位目标执行文件

hello 链接之后的可执行文件

hello1.elf hello.o的ELF格式

hello.elf hello的ELF格式
1.4 本章小结
本章节简述了P2P 和 020 的含义,列出了测试环境和工具和中间结果的文件名和文件作用。从整体上大致介绍了hello的一生,并且列出了做本次作业的软硬件环境以及工具和生成的中间文件。

第2章 预处理
2.1 预处理的概念与作用
源程序预处理是指在实际的编译过程之前对源代码进行一系列的处理步骤。这个过程由预处理器(cpp)完成,目的是对源代码进行一些文本上的替换和操作,以便为编译器提供更加适合处理的代码。
主要操作有:
宏替换: 预处理器可以执行宏替换,将源代码中的宏(预定义的代码片段)用其实际的定义替换。这样可以提高代码的可维护性和可读性。
文件包含: 预处理器可以处理源代码中的#include指令,将其他文件的内容嵌入到源文件中。这样可以将代码分割成多个文件,提高代码的组织性和可重用性。
条件编译: 预处理器可以根据条件编译指令(如#ifdef、#ifndef、#if等)选择性地包含或排除代码块。这对于根据不同的编译条件生成不同的代码版本非常有用。
符号替换: 预处理器可以进行符号替换,将代码中的符号用其定义的值替换。这有助于消除魔法数字,提高代码的可维护性。
注释删除: 预处理器可以删除源代码中的注释,减小编译后的代码体积,提高编译效率。
2.2在Ubuntu下预处理的命令
指令:gcc -E hello.c -o hello.i

图 2 hello的预处理指令
2.3 Hello的预处理结果解析
使用vim查看hello.i,在预处理后,宏和特殊符号被替换了, 同时在源代码中填充了代码,使篇幅增加。

图 3 hello.i文件
2.4 本章小结
本章描述了预处理的概念和作用,用介绍了预处理指令的使用,解析了预处理之后生成的hello.i文件。
第3章 编译
3.1 编译的概念与作用
编译是将高级编程语言的源代码翻译成目标平台上的机器语言或中间代码的过程。编译器负责执行这个翻译任务,将源代码转换成计算机能够理解和执行的形式,也就是机器代码。
作用如下:
生成可执行代码: 主要作用是将高级编程语言的源代码转换为目标平台上的可执行文件。这个可执行文件包含了机器语言或中间代码,可以在计算机上运行。
错误检测: 编译过程中编译器会检测源代码中的语法错误、类型错误等问题,帮助开发者在运行之前发现并纠正这些错误。
优化: 编译器会进行一些优化,提高生成的机器语言的执行效率。这包括对代码的调整、消除冗余、提高访问内存的效率等。
平台独立性: 生成的可执行文件通常与源代码无关,这使得同一份源代码可以在不同的计算机平台上编译为可执行文件,提高了程序的可移植性。
隐藏源代码: 编译后的可执行文件通常不包含源代码,因此可以保护源代码的知识产权,防止源代码被直接查看和修改。
加速执行: 编译过程中的优化可以使生成的机器代码更加高效,加速程序的执行速度。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.c -o hello.s

图 4 编译命令
3.3 Hello的编译结果解析

图 5 hello编译结果解析
变量分为全局变量、静态变量、局部变量。已初始化的全局变量和静态变量存放在.data节,未初始化的全局变量和静态变量存放在.bss节,而局部变量存放在栈中管理。在此程序中,没有全局变量和静态变量,所以文件最开始没有.data和.bss。
.file表明源文件
.text代码段
.section/.rodata存放只读变量
.align对齐方式:8字节对齐
.string字符串
.global全局变量
.type声明main是函数类型
3.3.1 立即数

图 6 立即数
$加数字代表直接使用该立即数
3.3.2局部变量:

图 7 局部变量
说明将源程序的i存在-4(%rbp)
3.3.3 算术运算

图 8 算术运算
每次循环完i++,汇编语言中用addl指令来实现
3.3.4 关系操作

图 9 关系操作
如cmp将i与7比较,如果小于等于7则继续进行循环。
3.3.5 函数调用

图 10 函数调用
通过call来实现对atoi函数,sleep函数和getchar函数等的调用
3.3.6 控制转移
通过jump指令实现

图 11 控制转移
如该指令即立刻跳转到.L3处继续执行。
3.3.7 数组操作

图 12 数组操作
字符指针数组 char *argv[]:存储用户输入的命令行信息地址
该数组中每个元素大小为8bit,argv既是数组名也是数组的首地址
图中分别为argv[1],argv[2]的实现
3.3.8 赋值操作

图 13 赋值操作
对局部变量的赋值通过mov指令完成,不同宽度的数据类型对应不同的mov指令。对于局部变量i,int类型的赋值用四字节的movl指令完成。

3.4 本章小结
本章主要介绍了编译的概念与作用,并结合hello.s的汇编代码,剖析了编译的过程以及编译器如何对各种数据类型和操作进行处理的。

第4章 汇编
4.1 汇编的概念与作用
汇编语言是介于高级编程语言和机器语言之间的一种语言。每一条汇编语句通常都对应于计算机中的一条机器指令。程序员可以使用助记符和符号来表示指令、寄存器和内存地址,而不是直接使用二进制代码。汇编程序将汇编语言翻译成机器码,然后计算机可以执行这些指令。
作用:
底层控制: 汇编语言允许程序员直接控制计算机的底层硬件。每个汇编语句对应于一条特定的机器指令,这样程序员可以更精细地控制程序的执行流程。
系统编程: 汇编语言常用于编写操作系统、驱动程序和其他需要直接访问硬件的系统级软件。这是因为它允许对硬件进行底层控制。
嵌入式系统: 汇编语言在嵌入式系统中得到广泛应用,因为它允许对资源进行更精确的管理,同时生成更紧凑和高效的代码。
性能优化: 在一些对性能要求极高的应用中,程序员可能会使用汇编语言来手动优化关键部分的代码,以获得更好的执行效率。

4.2 在Ubuntu下汇编的命令
在Ubuntu下汇编的命令为:gcc -c hello.s -o hello.o

图 14 汇编命令
4.3 可重定位目标elf格式
使用readelf读可重定位文件

图 15 readelf 指令
4.3.1 ELF头

图 16 ELF头
每个可执行和共享对象文件都有一个elf头,它的主要目的是定位文件的其他部分。它在文件的开头由一个16字节的Magic序列开始。这个序列包含了一些元数据,例如指定了该文件中字的大小和其他一些有用的信息。ELF头部剩余的部分包括了一些额外的数据,它们对于链接器来说是非常重要的。其中包括ELF头部的大小、文件的类型(例如可执行文件、共享库或可重定位文件)、机器的类型(例如x86-64)、以及节头表的偏移量和表中条目的数量和大小等信息。这些数据都对于解析和链接目标文件来说是至关重要的
4.3.2 节头

图 17 节头
ELF文件的节头部分描述了文件中各个节的数据信息,包括节的名称、类型、大小、偏移量、对齐方式、访问权限和链接属性等。ELF文件中的不同类型的节在链接和加载过程中会被处理成不同的方式,因此节头部分对于程序的链接和加载过程具有重要的作用。
4.3.3 重定位节

图 18 重定位节
重定位节.rela.text,一个.text节中位置的列表,包含.text节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
8条重定位信息分别是对.L0(第一个printf中的字符串)、puts函数、exit函数、.L1(第二个printf中的字符串)、printf、sleepsec、sleep、getchar函数进行重定位声明。
以.rela.text为例,偏移量是指需要进行重定向的代码在.text节的偏移位置;信息包括symbol和type两部分,前一部分是目标在.symtab中的偏移量,type表明重定位的类型,在CSAPP中介绍了相对重定位和绝对重定位两种,在该文件中只涉及相对重定位(PC32与PLT32是同一种重定位方式);加数为进行重定位时也要参与计算的一个量。

书中给出了相对重定位的计算公式,为:
refptr = s + r.offset
refaddr = ADDR(s) + r.offset
*refptr = ADDR(r.symbol) + r.addend – refaddr
而下面的部分是符号表(symbol table)。符号表中包含用来定位、重定位程序中符号定义和引用的信息。符号表索引是对此数组的索引。索引0表示表中的第一表项,同时也作为定义符号的索引。

4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

图 19 hello.o的结果解析(1)

图 20 hello的结果解析(2)
由上可知:
机器语言的构成:机器语言指令是一种二进制代码,由操作码和操作数两部分组成。操作码规定了指令的操作,是指令中的关键字,不能缺省。操作数表示该指令的操作对象。
与汇编语言的映射关系:将反汇编代码与机器语言相比较,可以看出反汇编代码不仅显示汇编代码,还显示机器代码,机器代码能够直接被电脑识别。反汇编器使用的指令命名规则与汇编代码的部分有区别
机器语言中的操作数与汇编语言不一致:hello.s中操作数为十进制,hello.o反汇编代码中操作数为16进制
分支转移:反汇编代码跳转指令的操作数使用的不是段名称如.L3,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
函数调用:在.s文件中,函数调用之后直接跟着函数名称,而在反汇编序中,call的目标地址是当前下一条指令。这是因为hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call 指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text节中为其添加重定位条目,等待静态链接的进一步确定。
4.5 本章小结
本章主要介绍汇编的概念与作用,以及生成的可重定位目标文件的基本格式与内容,同时对比了汇编文件与反汇编文件,分析其中的差异与原因。
第5章 链接
5.1 链接的概念与作用
链接(Linking)是指将程序的多个模块或文件组合在一起,以创建一个完整的可执行程序或库的过程。
作用:
模块化: 允许程序被拆分成多个模块或文件,这样每个模块可以独立开发、编译和测试。链接将这些模块组合成一个整体,提高了代码的组织性和可维护性。
代码重用: 动态链接库(DLL)等机制使得多个程序可以共享相同的代码库,减小了程序的体积,提高了代码的重用性。
符号解析: 在链接阶段,编译器将符号(如函数和变量名)与其对应的地址进行关联,从而解析程序中的符号引用。
地址重定向: 将模块的地址信息调整为相对于整个程序的正确位置,以确保各个模块之间的调用和引用能够正确地连接。
库链接: 将程序与外部库(静态库或动态库)链接在一起,以便程序可以使用这些库中提供的函数和资源。
优化: 链接过程中还可以进行一些优化,例如删除未使用的代码、合并相同的代码块等,以减小可执行文件的大小并提高运行效率。
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

图 21 链接命令
5.3 可执行目标文件hello的格式
通过readelf -a hello >hello1.elf 指令生成hello的ELF格式文件,为了与可重定位目标文件进行区分,命名为hello1.elf。

图 22 生成hello1.elf

5.3.1 ELF头

图 23 hello1.elf的ELF头
与hello.elf相比较,hello_.elf的基本信息如Magic、类别等未发生改变,但其类型改为可执行文件,同时程序头大小和节头数量有所增加,并生成了入口点地址。
5.3.2 节头

图 24 hello1.elf的节头
节头包含文件中各个节的信息,包括名称、类型、地址、偏移量等,与hello.elf相比并无太大区别,而内容在连接后变得更加丰富了。
5.3.3 程序头:

图 25 hello1.elf的程序头
程序头是链接后新生成的,它包含了程序运行所需的各种信息,例如段的大小、对齐方式、读写权限等,操作系统在加载程序时会根据程序头的描述进行加载和对齐,从而使程序能够正确地运行。
5.3.4 符号表

图 26 hello1.elf的符号表
符号表保存了程序实现或使用的所有全局变量和函数
5.4 hello的虚拟地址空间

图 27 edb运行hello

图 28 Data Dump
.init段起始地址为0x401000,.text段的起始地址为0x4010f0,.rodata段的起始地址为0x402000,.data起始地址为0x404048,也都可以在edb中找到,可以得到edb与之前的elf文件是相对应的。
5.5 链接的重定位过程分析
使用objdump -d -r hello命令查看hello反汇编代码

可以看出hello反汇编的代码有明确的虚拟地址,完成了重定位,并且hello反汇编代码中多出了其他节如.init和.plt节,在hello中链接加入了在hello.c中用到的库函数,如exit、printf、sleep。

图 29 hello反汇编代码(1)

图 30 hello反汇编代码(2)

图 31 hello反汇编代码(3)

根据hello和hello.o的不同,可以得出链接过程为:链接就是链接器将各个目标文件组装在一起,文件中的各个函数段按照一定规则累积在一起。
5.6 hello的执行流程
401000 <_init>

401020 <.plt>

401030 puts@plt

401040 printf@plt

401050 getchar@plt

401060 atoi@plt

401070 exit@plt

401080 sleep@plt

401090 <_start>

4010c0 <_dl_relocate_static_pie>

4010c1

401150 <__libc_csu_init>

4011b0 <__libc_csu_fini>

4011b4 <_fini>
5.7 Hello的动态链接分析
在elf中查看与动态链接相关的段:

图 32 elf中查看与动态链接相关的段
通过调试可以看出共享链接库代码是动态的目标模块,对动态链接的重定位过程就是在程序开始运行或者调用程序加载时,自动加载该代码到任意的一个内存地址,并和一个在目标模块内存中的应用程序链接起来。在plt和got中分别存放着链接器的目标变量和函数的运行时地址。一个动态的链接器通过静态的过程偏移链接表plt+got链接器实现函数的动态过程链接,这样它就包含了正确的绝对运行时地址。
5.8 本章小结
本章主要介绍链接的过程,并通过反汇编代码与汇编代码的对比,深入体会可重定位目标文件、可执行目标文件的结构,并分析链接中符号解析、重定位以及动态链接的过程。

第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程是计算机中正在运行的程序的实例。它是操作系统分配资源和执行任务的基本单位。每个进程都有自己的内存空间、执行状态和所需的系统资源,它是是os对spu执行的程序的运行过程的一种抽象。
作用:进程是程序在计算机中执行的实体。操作系统通过创建进程来运行程序,并将程序的指令和数据加载到进程的内存空间中。进程执行指令,处理数据,实现程序的功能,并且每个进程都运行在独立的内存空间中,互相隔离。这样可以确保进程之间的数据和状态不会相互干扰,好像程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
作用:
1.提供命令行界面:bash允许用户通过命令行输入指令来与操作系统进行交互,执行各种操作,如运行程序、管理文件、配置系统等。
2.执行脚本:bash可以解释执行脚本文件,这些脚本文件包含一系列的命令和逻辑,可以自动化执行复杂的任务。
3.环境配置:bash提供了环境变量的管理功能,可以设置和修改系统的环境变量,影响用户会话和程序的运行环境。
处理流程:
1.当用户打开一个终端或启动一个新的shell会话时,bash会显示一个提示符,等待用户输入命令。
2.bash会读取用户在命令行中输入的内容,包括命令和参数。
3.bash对用户输入的命令进行解析,识别命令的名称和参数。
4.一旦命令被解析,bash会启动相应的程序或执行相应的操作。它会调用内核提供的系统调用来执行命令,操作文件、进程等。
5.命令执行完成后,bash将命令的输出结果显示在终端上,供用户查看。
6.一条命令执行完成后,bash会再次显示提示符,等待用户输入下一条命令。这个过程将一直循环进行,直到用户主动退出或关闭shell会话。
6.3 Hello的fork进程创建过程
先判断./Hello是否是内置命令,他不是内置命令,则通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是相互独立的副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程的PID是不同的,是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
6.4 Hello的execve过程
当Shell调用fork创建新的子进程后,会通过execve函数,将hello程序的代码段、数据段、堆栈等信息加载到进程上下文中,并将进程的PC指向程序的入口点。在这个过程中,控制权从操作系统传递到hello,hello开始执行自己的代码。同时,execve也会将命令行参数和环境变量传递给hello,并为其创建一个新的用户空间栈。执行结束后,该进程的资源会被回收。
6.5 Hello的进程执行

(以下格式自行编排,编辑时删除)
进程上下文信息:
进程上下文信息是操作系统中用于管理和描述进程状态的关键数据集合。它包含了进程执行所需的各种关键信息,以确保在进行进程切换时能够保存和恢复进程的执行状态。这些信息包括寄存器状态,如通用寄存器、程序计数器和栈指针,用于记录当前执行位置和执行状态;内存管理信息,如内存布局、地址空间映射和页表,用于管理进程的内存资源;文件描述符,用于记录进程打开的文件和网络连接信息;运行时堆栈,保存函数调用和返回过程中的局部变量和调用上下文信息等。

通过维护和切换进程上下文信息,操作系统能够在不同进程之间实现高效的调度和执行,从而实现多任务处理和资源共享。上下文切换包括保存当前进程或线程的执行状态和环境信息,并加载下一个要执行的进程或线程的执行状态和环境信息,使得多个进程或线程可以交替执行,共享处理器资源,并实现并发执行的效果。
进程时间片:
进程时间片是操作系统中用于调度进程执行的固定时间段。每个进程被分配一个时间片,在该时间片内执行其指令和任务。当时间片用完后,操作系统会触发上下文切换,将处理器控制权转移到下一个就绪的进程上。
进程调度以及用户态与核心态的转换:
在我们的Hello程序中,当执行到sleep系统调用时,会触发陷阱异常。这将导致程序从用户模式切换到内核模式,程序进入休眠状态,操作系统将控制权转移到其他进程。sleep时间结束后,内核会收到一个信号,然后进入内核状态来处理异常,Hello程序得以重新回到用户模式。
接着,当执行getchar函数时,该函数使用read系统调用来读取输入。这再次触发上下文切换,将程序从用户模式切换到内核模式,以执行read系统调用。读取完成后,控制权再次交回给Hello程序,程序继续执行后续的操作。通过这些上下文切换,程序在用户模式和内核模式之间进行交替,以完成不同的系统调用和处理异常的操作。
6.6 hello的异常与信号处理
异常可分为以下四类:
类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 系统调用 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令
终止 不可恢复的错误 同步 不会返回

6.6.1 正常运行

图 33 hello正常运行
6.6.2 回车

图 34 hello带回车运行
可以发现该方式并不会影响程序的正常运行,因为它们都会被最后的getchar函数读取,并覆盖掉。

6.6.3 Ctrl-C

图 35 hello运行中使用Ctrl-C
可以看到进程直接被停止了,这会发送SIGINT信号给前台进程组的每个进程,结果是终止前台进程。

6.6.4Ctrl-Z
图 36 hello运行中使用Ctrl-Z
我们发现该程序被挂起了,因为这样会发送SIGTSTP信号给前台进程组的每个进程,结果是停止前台作业。

图 37 ps命令
ps:可以显示当前的所有进程以及他们的PID

图 38 jobs命令
jobs:会打印出所有当前的作业,可以看出当前的作业是hello进程,且状态是已停止

图 39 pstree命令(1)

图 40 pstree命令(2)

图 41 pstree命令(3)

图 42 pstree命令(4)

图 43 pstree命令(5)
pstree:使用pstree命令将所有进程以树状图形式显示

图 44 fg命令
fg:把后台停止的hello进程恢复在前台运行。

图 45 kill命令
使用kill -9 -hello的进程号可以杀死进程。

图 46 hello被回收
再次查看,hello已经不存在在jobs列表,说明已经被回收了。
6.7本章小结
本章主要介绍进程的概念与其作用,并阐述了shell的工作机制。结合hello进程,探讨了fork与execve的原理以及进程的执行过程,并在shell中运行hello程序,测试其对不同异常与信号的处理方式。

第7章 hello的存储管理
7.1 hello的存储器地址空间
1.逻辑地址:逻辑地址是指程序在逻辑上使用的地址,通常是指程序中指令或数据的地址。在Hello程序中,逻辑地址可以是指向变量的指针或函数的入口地址。
2.线性地址:线性地址是指逻辑地址经过地址转换后得到的地址。在操作系统的虚拟内存管理中,线性地址是一个中间的地址空间,它通过分页或分段机制将逻辑地址转换为线性地址。线性地址通常是连续的地址空间。
3.虚拟地址:虚拟地址是指线性地址经过虚拟内存管理机制转换后得到的地址。虚拟地址空间是给进程提供的独立的地址空间,每个进程都有自己的虚拟地址空间。在Hello程序中,进程使用的地址都是虚拟地址。
4.物理地址:物理地址是指最终在内存中的实际地址,它是CPU发送给内存控制器的地址。在虚拟内存管理中,虚拟地址需要经过地址映射,将虚拟地址转换为物理地址,以便访问实际的内存数据。
7.2 Intel逻辑地址到线性地址的变换-段式管理

7.2.1 段式管理的概念:
在Intel x86架构中,使用了段式管理来实现逻辑地址到线性地址的转换。具体来说,每个段都有一个段描述符,它包含了段基址、段限长和一些访问权限信息。当CPU访问内存时,它会使用段选择符来选择需要访问的段描述符,并将段基址和偏移量组合成线性地址。
7.2.2 段式管理下地址映射的具体步骤:
具体步骤是:CPU使用段选择符从GDT或LDT中选择相应的段描述符。然后段描述符中的段基址和逻辑地址中的偏移量相加,得到线性地址。CPU会检查线性地址是否超出了段的限长。如果超出了限长,就会触发异常。接着检查段描述符中的访问权限信息,如果访问权限不足,也会触发异常。如果一切正常,CPU将线性地址转换为物理地址,并进行内存访问操作。

图 47 Intel逻辑地址到线性地址的变换-段式管理
7.3 Hello的线性地址到物理地址的变换-页式管理

7.3.1 页式管理的概念:

页式管理将内存空间划分为固定大小的页,每个页可以单独进行映射和访问控制,从而更好地保护系统的安全性。在页式管理中,线性地址会被划分为虚拟页号和页内偏移量。虚拟页号会通过页表进行映射,转换为物理页号,然后再加上页内偏移量,得到物理地址。
7.3.2 页式管理下的地址映射具体过程:
在hello中,程序的线性地址会通过页表映射机制转换为物理地址。具体的线性地址到物理地址的变换过程如下:
程序使用线性地址来访问内存中的数据。线性地址由虚拟页号和页内偏移量组成。通过页表查找虚拟页号对应的物理页号,并检测有效位,如果为1则发生页命中,直接获取物理页号并和页偏移量进行拼接得到物理地址;若不为1则发生缺页,此时需要调用缺页处理程序调入新的页,并再次进行查找获取PPN,与VPO组成PA。

图 48 线性地址到物理地址的变换-页式管理
7.4 TLB与四级页表支持下的VA到PA的变换

TLB是一个小的,虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块,从TLB中获取物理地址,需经过:CPU产生一个虚拟地址;MMU从TLB中取出相应的PTE;MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存;高速缓存/主存将所请求的数据字返回给CPU。四级页表中包含了一个地址字段,它里面保存了40位的物理页号,这就要求物理页的大小要向 4kb对齐。四级页表每个表中均含有512个条目,第四级页表每个条目对应4kb区域;第三级页表每个条目对应2MB区域;第四级页表每个条目对应1GB区域;第四级页表每个条目对应512GB区域。将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。解析VA,利用前m位vpn1寻找一级页表位置,接着一次重复k次,在第k级页表获得了页表条目,将PPN与VPO组合获得PA。

图 49 TLB与四级页表支持下的VA到PA的变换
7.5 三级Cache支持下的物理内存访问
根据CS寻找到正确的组 L1 cache中的某个组,在该组中查找是否有某一行的标记等于物理地址的标记并且该行的有效位为 1,若有,则命中,从这一行对应物理地址 b 位块偏移的位置取出一个字节,若有效位不为1,则说明不命中,需要继续访问下一级 cache,若三级 cache 都没有要访问的数据,则需要访问内存,从内存中取出数据并放入cache。

图 50 三级Cache支持下的物理内存访问
7.6 hello进程fork时的内存映射
虚拟内存和内存映射解释fork函数如何为每个新进程提供私有的虚拟地址空间,在shell运行hello进程时,shell为hello进程创建虚拟内存创建当前进程的的mm_struct, vm_area_struct和页表的原样副本,两个进程中的每个页面都标记为只读两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW)在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存随后的写操作通过写时复制机制创建新页面。
7.7 hello进程execve时的内存映射
当hello进程调用execve系统调用时,进程的内存映射映射过程可以包括以下步骤:
1.删除已存在的用户区域:删除当前进程虚拟地址的用户部分中的已存在的区域(段)结构。
2.映射私有区域:为新程序的文本、数据、bss和栈区域创建新的区域(段)结构。
3.映射共享区域:如果hello.out程序与共享库链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

4.设置程序计数器:设置当前进程上下文中的程序计数器,使之指向文本段的入口点。
在这个过程中,新的可执行文件将会被映射到进程的虚拟地址空间中,它们通常会被映射到不同的段中,例如代码段、数据段、堆、栈等。每个段都有相应的访问权限和保护级别,例如代码段是只读的,数据段是可写的,堆和栈是可读写的。
7.8 缺页故障与缺页中断处理
7.8.1 缺页故障的概念:
缺页故障是指当CPU试图访问一个当前不在物理内存中的虚拟页时产生的异常。在这种情况下,操作系统会将缺页故障转换为缺页中断,并将控制权传递给内核,进行相应的处理。
7.8.2 缺页中断处理的过程:

  1. 中断处理程序:当CPU产生缺页中断时,操作系统会调用中断处理程序来处理该中断。中断处理程序通常会保存当前进程的上下文信息,并将控制权传递给操作系统内核。
  2. 查找页表:操作系统会查找当前进程的页表,以确定需要加载的虚拟页是否存在于物理内存中。如果该虚拟页已经在物理内存中,那么操作系统会更新页表项并重新启动进程。否则,操作系统会执行下一步操作。
  3. 选择牺牲页:如果需要加载的虚拟页不存在于物理内存中,那么操作系统需要选择一个牺牲页来替换。它的内容会被换出到磁盘中,以便腾出空间来加载新的虚拟页。一般采用LRU替代策略。
  4. 从磁盘中读取页:操作系统会从磁盘中读取需要加载的虚拟页,并将它们映射到一个物理页框中。如果物理内存中没有空闲的物理页框,操作系统需要先选择一个牺牲页并将其换出,然后再将新的虚拟页加载到该物理页框中。
  5. 更新页表:操作系统会更新当前进程的页表,将虚拟页和物理页框的映射关系记录到页表项中,并重新启动进程。
  6. 恢复进程状态:操作系统会恢复当前进程的上下文信息,包括程序计数器、寄存器、栈指针等,并将控制权传递给进程,使其继续执行。
    7.9动态存储分配管理
    printf 函数会调用 malloc,下面简述动态内存管理的基本方法与策略:

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保 持 空 闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放。
7.10本章小结
本章深入了解了Hello程序中的存储管理过程。在fork进程时,通过页表和写时复制技术实现了父子进程之间的内存映射,避免了完全复制内存的开销。而在execve时,通过重新映射程序的逻辑地址空间,将新的程序加载到内存中,并更新页表,实现了内存的重新映射。并且详细学习了虚拟地址与物理地址之间映射变换的全过程以及缺页故障和缺页中断处理的过程。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
一个 Linux 文件是一个 m 字节的序列,所有的 I/O 设备(如网络、磁盘和终端)都被建模为文件。每个文件都有一个类型来标明他在系统中的角色:

普通文件包含任意文件;目录是包含一组链接的文件,其中每个链接都将一个文件名映射到一个文件,这个文件可能是另一个目录;套接字是用来与另一个进程进行跨网络通信的文件。
设备管理:unix io接口
所有的 I/O 设备(如网络、磁盘和终端)都被建模为文件,而所有的输入输出都被当做相应的文件的读和写来执行,这种将设备映射为文件的方式,允许linux内核引出一个低级的,简单的应用接口称为Unix I/O
8.2 简述Unix IO接口及其函数
Unix IO接口:

(1)打开文件:一个应用程序要求内核打开相应文件,来访问I/O设备。会返回一个非负整数,称为描述符,他在后面的操作中标志这个文件,内核记录有关打开文件的所有信息,程序只需要记住该描述符。
(2)linux shell 创建的每个进程开始时都有三个打开的文件:标准输入 、标准输出和标准错误。
(3)改变文件位置:内核对于每个文件保持一个初始为0的文件位置,该位置标示从头部开始的文件的偏移量。程序可以通过seek函数,显示地修改此文件位置。
(4)读写文件:读操作是从当前文件位置开始,复制相应数量的字节到内存,写操作则是从内存读入相应数量的字节到当前文件位置,然后更新文件位置。
(5)关闭文件:一个应用程序完成对文件访问后,要求内核关闭相应文件。
8.3 printf的实现分析
首先我们需要先看printf函数的函数体:

图 51 printf函数体
其调用的vsprintf如下:

图 52 vsprintf
vsprintf将printf的参数按照各种各种格式进行分析,将要输出的字符串存在 buf中,最终返回要输出的字符串的长度。
sys_call函数通过总线将字符串中的字节从寄存器复制到显卡的显存。显存存储ASCII字符码,字符显示驱动子程序通过ASCII码在字体库中查找点阵信息,将点阵信息存储在vram中。
printf从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,终端请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后转化为ASCII码,保存在系统的键盘缓冲区之中。getchar函数落实到底层调用了系统的read函数,通过系统调用read读取在键盘缓冲区ASCII码,直到读到回车符,然后返回整个字串,getchar进行封装,大体逻辑时读取字符串的第一个字符然后返回。
8.5本章小结
本章介绍了 Linux 的 I/O 设备的基本概念和管理方法,简述了UNIX IO接口及其函数,并分析了printf 函数和 getchar 函数的实现过程。
结论

用计算机系统的语言,逐条总结hello所经历的过程:

  1. 源程序:在文本编辑器或集成开发环境中编写C语言代码,得到最初的hello.c源程序。
  2. 预处理:预处理器对源程序进行处理,生成ASCII码的中间文件hello.i。
  3. 编译:编译器将预处理后的代码翻译成汇编指令,生成ASCII汇编语言文件hello.s。
  4. 汇编:汇编器将汇编指令翻译成机器语言,并生成可重定位目标文件hello.o。
  5. 链接:链接器对可重定位目标文件进行符号解析、重定位和动态链接等操作,生成可执行目标文件hello。
  6. fork的调用:在shell中运行hello程序时,shell调用fork函数创建一个子进程,该子进程将成为hello程序的副本。
  7. execve加载程序:子进程中调用execve函数,加载hello程序的可执行目标文件,将进程的上下文切换到hello程序的入口点,hello程序开始运行。
  8. 运行阶段:操作系统内核负责调度进程的执行,并对可能发生的异常和信号进行处理。内存管理单元(MMU)、转换后备缓冲(TLB)、多级页表、缓存和动态内存分配器等组件相互协作,共同管理内存资源。同时,通过Unix I/O机制,hello程序与文件进行交互。
  9. 终止:hello进程运行结束,操作系统的内核负责回收终止的hello进程,清理其所占用的资源和数据结构。

感悟:
回想刚刚翻开深入理解计算机系统的第一章——计算机系统漫游的时候,我就被眼前这广袤的世界所深深吸引。当时尚未学习过计算机系统内部细节的我,永远也不会想到这拥有强大能力的计算机背后繁芜丛杂的细节和机制是如此的枯燥、简单甚至有着各种漏洞仍未被解决,是如此的精彩、精巧甚至精美绝伦如天工斧作。这样的说法也许看似矛盾,但确是我内心真切的感受。每一处细节,每一个小小功能的构思是简单的,甚至是粗暴丑陋的,但这只是一个小小的“齿轮”,没错,当一个能用的东西出现了,那么在人类的手中,他一定会被完善,被改造,被结合,从而一级级不断上升,就像一张jpg图片的像素点一般,凭借着彼此之间的辉映,互相之间的渲染,创造出能被人眼所能感受到的美。各个“零件”,各个“齿轮”打造出一个机器,一个改造了时代,创造了奇迹,开创了新世界的机器。在hello的一生中,他诞生的那一刻,是在人类几十万年的历史里也应当是熠熠生辉光芒万丈的里程碑。预处理、编译、汇编、链接,每一步都设计的如此巧妙,每一步都显得设计周全。尽管计算机系统仍在一些方面不能尽善尽美,甚至因为运行的机制暴露出种种漏洞,但我也认为这是他的魅力所在(不完美的事物却带来了超乎时代想象的力量)。而小小的hello是由人类创造出的极具力量的工具的外现。fork,execve,虚拟内存,I/O管理,这样的思维,这样的机制正是人类高度凝练的思维的抽象,学习计算机系统不仅仅让我收获很多关于计算机的知识,他也让我感受到计算机这一行并不是想象中那么孤独,有那么多热血的人在这一行奔走,是的,在学习计算机系统中,我学会与同样的计算机人建立精神纽带,即使是在我身边,我也能看到许多令我钦佩的同学投入到计算机的学习中,那是旺盛的求知欲,我很羡慕,也很渴望。我想了解更多。每念及此,我总觉得——我不能停下前进的步伐。
附件
hello.c 源代码

hello.i 预处理后的文本文件

hello.s 编译之后的汇编文件

hello.o 汇编之后的可重定位目标执行文件

hello 链接之后的可执行文件

hello1.elf hello.o的ELF格式

hello.elf hello的ELF格式
参考文献
[1] Randal E. Bryant, David R. O’Hallaon. 深入理解计算机系统. 第三版. 北京市:机械工业出版社[M]. 2018: 1-737
[2] printf 函数实现的深入剖析.
[转]printf 函数实现的深入剖析 - Pianistx - 博客园

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值