哈工大2024春计算机系统程序人生大作业

计算机系统大作业

 

计算机科学与技术学院

2024年5月

摘  要

本文通过对简单的hello程序从编写代码到进程结束的过程进行分析,探讨Linux下程序完整的处理、运行机制,从预处理、编译、汇编、链接到hello执行过程中的进程管理、存储管理、IO管理,解释了Linux系统对典型C语言程序的处理机制。

关键词:计算机系统;Linux;C语言;                           

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

目  录

第1章 概述..................................................................................... - 4 -

1.1 Hello简介............................................................................ - 4 -

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

1.3 中间结果................................................................................ - 4 -

1.4 本章小结................................................................................ - 5 -

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

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

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

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

2.4 本章小结.............................................................................. - 10 -

第3章 编译................................................................................... - 11 -

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

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

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

3.4 本章小结.............................................................................. - 17 -

第4章 汇编................................................................................... - 18 -

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

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

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

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

4.5 本章小结.............................................................................. - 22 -

第5章 链接................................................................................... - 24 -

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

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

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

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

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

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

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

5.8 本章小结.............................................................................. - 33 -

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

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

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

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

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

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

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

6.7本章小结.............................................................................. - 39 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结............................................................................ - 45 -

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

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

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

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

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

8.5本章小结.............................................................................. - 48 -

结论............................................................................................... - 48 -

附件............................................................................................... - 49 -

参考文献....................................................................................... - 50 -

第1章 概述

1.1 Hello简介

程序员编写hello.c源文件后,对其进行预处理、编译、汇编、链接操作,分别根据以字符#开头的命令修改原始的C程序,生成汇编代码,转为更底层的机器码,引用动态链接库,最终生成hello可执行文件。在执行hello程序时,分析其进程管理:父进程调用fork函数创建子进程,在子进程中调用execve函数加载hello程序;存储管理:分析对比逻辑地址、线性地址、虚拟地址、物理地址,以及实际执行过程中TLB与四级页表机制、主存与三级cache机制如何调入调出页面;IO管理:调用printf函数和getchar函数。

1.2 环境与工具

1.2.1 硬件环境

处理器:13th Gen Intel(R) Core(TM) i7-13700H   2.40 GHz

机带RAM:16.0 GB (15.6 GB 可用)

1.2.2 软件环境

Windows 11 家庭中文版

Vmware 17 Pro

Ubuntu 22.04.4 LTS 64位

1.2.3 开发工具

vscode

gcc、g++

vi/vim/gedit

1.3 中间结果

hello.c:源代码

hello.i:预编译后的文件

hello.s:编译后的文件

hello.o:汇编后的代码

hello.out:链接后的文件

helloelf.txt:hello.o的elf文件

hello2.txt:hello.o的反汇编代码

helloelf2.txt:hello的elf文件

hello.txt:通过对hello.out反汇编生成的代码

hello3.txt:hello的反汇编代码

1.4 本章小结

本章总体上概述了hello程序从源代码到执行结束的生命周期过程,并列出了开发环境与开发工具,以及各个阶段生成的中间文件和其作用。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

2.1.1预处理的概念

预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如拷贝#include包含的文件代码,#define宏定义的替换,条件编译等,就是为编译做的预备工作的阶段,主要处理#开始的预编译指令,预编译指令指示了在程序正式编译前就由编译器进行的操作,可以放在程序中的任何位置。结果得到另一个C程序,通常是以.i作为文件扩展名。

2.1.2预处理的作用

1.头文件的包含:#include指令能够告诉预处理器读取源程序中所引用的系统的源文件,并且将这一段代码直接插入到程序文件中,最终保存为.i文件中。比如hello.c中第1行的#include <stdio.h>,命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插人程序文本中。

2.define宏定义的替换:#define机制包含了一个规定,允许把参数替换到文本中,这种实现通常称为宏或者定义

3.条件编译的处理:条件编译的功能使得我们可以 按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件。

2.2在Ubuntu下预处理的命令

Ubuntu下利用gcc对C语言程序进行预处理使用gcc -E参数,命令

gcc -E hello.c -o hello.i

是将hello.c程序代码进行预处理,结果输出到hello.i文件当中。使用命令

gcc -m64 -Og -no-pie -fno-PIC hello.c -E -o hello.i

意味着令gcc 生成 64 位的代码, 使用-Og级别的优化,保持调试信息尽可能完整,并避免那些可能使调试更困难的优化,-no-pie告诉 gcc 不要生成位置无关的可执行文件,-fno-PIC告诉 gcc 不要生成 PIC 代码。

图 2-1 预处理命令及结果文件

图 2-2 预处理文件与源文件对比

2.3 Hello的预处理结果解析

查看文件大小,可以发现hello.i文件比hello.c文件大小大很多。

图 2-3 文件大小对比

hello.i文件开头部分出现大量头文件的路径,预处理器读取 stdio.h、unistd.h、stdlib.h中的内容,并且根据读入的顺序依次进行内容的展开,递归地找到所有引入的这些头文件,并最终形成hello.i文件。

图 2-4 hello.i文件解析

中间部分是大量变量和函数的声明,包括大量的typedef和结构体定义,主要是对外部库文件中函数的extren声明。

图 2-5 hello.i文件解析

文件末尾为主函数,没有进行变动,hello.c文件开头注释部分消失。

图 2-6 hello.i文件解析

2.4 本章小结

本章概述了预处理环节的概念和作用,预处理器(cpp)根据以字符#开头的命令,修改原始的C程序,使用预处理命令生成预处理后的hello.i文件,对比源文件hello.c发现引入较多外部库,删去注释部分,文件大小增大很多。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

3.1.1编译的概念

编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。

3.1.2编译的作用

1、将高级语言转换为汇编语言,为不同高级语言的不同编译器提供了通用的输出语言。

2、编译程序在编译过程中会进行代码优化,生成更加高效的汇编代码,从而提高程序的执行效率。

3、在编译过程中,编译程序会进行词法分析、语法分析和语义检查,检查源代码中是否存在语法或语义错误,并给出相应的错误提示信息。

3.2 在Ubuntu下编译的命令

Ubuntu下利用gcc对C语言程序进行预处理使用gcc -E参数,命令

gcc -S hello.i -o hello.s

是将hello.i程序代码进行编译,结果输出到hello.s文件当中。使用命令

gcc -m64 -Og -no-pie -fno-PIC hello.i -S -o hello.s

意味着令gcc 生成 64 位的代码, 使用-Og级别的优化,保持调试信息尽可能完整,并避免那些可能使调试更困难的优化,-no-pie告诉 gcc 不要生成位置无关的可执行文件,-fno-PIC告诉 gcc 不要生成 PIC 代码。

图 3-1 编译命令及结果文件

3.3 Hello的编译结果解析

3.3.1常量

字符串常量,位于开头伪指令中。

图 3-2 字符串常量位置

3.3.2变量

无全局变量,局部变量i被放在寄存器%ebp中,该语句对应for循环中将i赋值为0。

图 3-3 变量i位置

3.3.3表达式

表达式argc!=5,在hello.s中被表达为如下语句,cmpl语句设置条件码后,利用jne跳转语句若!=5则跳转到.L6。

图 3-4 表达式argc!=5

3.3.4类型

movq代表对8字节数据操作,movl代表对4字节数据操作,解析数据类型通过汇编指令的选择和操作数的大小来体现。

图 3-5 不同类型数据

3.3.5赋值

局部变量i保存在寄存器%ebp中,使用movl指令对其赋值初始值0。

图 3-6 变量i赋值

3.3.6算数操作

每次for循环中使用addl指令对局部变量i数值+1。

图 3-7 变量i运算

3.3.7关系操作

for循环结束条件i<10对应汇编指令中,cmpl语句设置条件码后,利用jle跳转语句若<=9则跳转到.L3。

图 3-8 跳转指令判断

3.3.8数组

argv数组中argv[1], argv[2], argv[3]保存在栈帧上,分别在%rbp为起始地址,偏移量为+8, +16, +24位置处。

图 3-9 数组位置

3.3.9控制转移

控制转移使用cmp指令和jxx跳转指令实现,for循环中使用addl指令对局部变量i数值+1,循环结束条件i<10对应汇编指令中,cmpl语句设置条件码后,利用jle跳转语句若<=9则跳转到.L3。

图 3-10 跳转控制转移

3.3.10函数操作

在64位系统下,函数传参是通过寄存器及堆栈实现的。第1个至第6个参数通过寄存器%rdi, %rsi, %rdx, %rcx, %r8, %r9传入,第7个及以后的参数通过堆栈传参。argv数组中argv[1], argv[2], argv[3],分别在%rbp为起始地址,偏移量为+8, +16, +24位置处,并放入对应寄存器中,调用printf函数中使用到的参数均通过寄存器传参。

图 3-11 函数传参

使用call函数调用函数,如调用printf函数和sleep函数。

图 3-12 函数控制转移

ret 指令用于将程序的控制权返回到调用该函数的位置,指令.cfi_endproc为函数结束的标记,它用于通知调试器和其他工具函数的结束位置。

图 3-13 函数返回

3.4 本章小结

本章概述了编译环节的概念和作用,结合hello.s文件,对C语言中数据类型、赋值操作、算术操作、关系操作、数组、函数调用的汇编指令分别分析,直观地看到了编译的结果。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

4.1.1汇编的概念

汇编器(as)将hello.s文件翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件,通常是.o文件中,该文件包含了机器指令的二进制表示以及相关的符号信息。

4.1.2汇编的作用

汇编阶段将汇编语言程序转化为计算机硬件可以直接执行的机器语言程序,可以为链接阶段做准备,作为链接阶段的重要输入。

4.2 在Ubuntu下汇编的命令

Ubuntu下利用gcc对C语言程序进行预处理使用gcc -c参数,命令

gcc -c hello.s -o hello.o

是将hello.s程序代码进行汇编,结果输出到hello.o文件当中。使用命令

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

意味着令gcc 生成 64 位的代码, 使用-Og级别的优化,保持调试信息尽可能完整,并避免那些可能使调试更困难的优化,-no-pie告诉 gcc 不要生成位置无关的可执行文件,-fno-PIC告诉 gcc 不要生成 PIC 代码。

图 4-1 汇编命令及结果文件

4.3 可重定位目标elf格式

使用readelf -a hello.o > helloelf.txt命令,将ELF格式文件信息输入到helloelf.txt文件中。ELF文件头信息如下,描述ELF文件整体结构和属性的信息,包括魔数、版本、入口点地址、目标体系结构、节表偏移、程序头表偏移等。

图 4-2 ELF头信息

查看hello.o文件的各节主要信息,可以发现hello.文件中主要有.text段,.rela.text段,.data段,.bss段,.rodata段,.comment段等,分别列出了节的编号([Nr])、名称(Name)、类型(Type)、节在虚拟内存中的地址(Address)、重定位条目、符号表、字符串表、节偏移(Offset)、节大小(Size)、标志位(Flag)、链接索引(Link)、附加信息(Info)、对齐要求(Align)等信息。

图 4-3各节主要信息

查看hello.o文件中的.rela.text节,列出了重定位节中的9个重定位条目,这些条目在链接阶段告诉链接器在将多个对象文件链接成一个可执行文件或共享库时,需要对哪些位置进行哪些修改。

图 4-4 .rela.text节

查看hello.o文件中的.rela.eh_frame 节,包含了1个重定位条目,记录了需要进行地址重定位的异常处理框架信息。

图 4-5 .rela.eh_frame节

查看hello.o文件中的符号表.symtab节,包含了13个条目,每个条目描述了一个符号(函数、变量等)的属性,其中包含符号的编号(Num)、值或地址(Value)、大小(Size)、类型(Type)、绑定属性(Bind)、可见性(Vis)、节索引(Ndx)、名称(Name)等信息。

图 4-6 符号表.symtab节

4.4 Hello.o的结果解析

使用objdump -d -r hello.o命令分析hello.o的反汇编,对比第3章的hello.s两者的内容发现,每条汇编指令都对应一串十六进制的编码,这样的机器指令才能被计算机理解与执行,并且全部的操作数都已经换成十六进制。

hello.o中.text段的汇编代码与hello.s中的大多相同,需要重定位的各类符号在hello.o中使用了00 00 00 00作为占位符,例如在引用字符串常量时,在hello.s文件中传递.LC0地址,而在hello.o文件中采用绝对寻址方式定位到1a: R_X86_64_32       .rodata.str1.8信息所指示处。

图 4-7 反汇编指令与汇编指令对比

在调用函数时,hello.s文件直接使用call指令,调用exit函数,而在hello.o文件中采用相对寻址方式定位到29: R_X86_64_PLT32     exit-0x4信息所指示处。

图 4-8 反汇编指令与汇编指令寻址对比

4.5 本章小结

本章概述了汇编环节的概念和作用,结合hello.o文件,对应的ELF格式文件和hello.o得到的反汇编文件,对汇编得到的可重定位目标ELF格式文件头的信息,各个节中主要信息等进行理解和解析,对反汇编的到的文件信息和第3章得到的hello.s文件对比,查看寻址的不同方式。

(第41分)

5章 链接

5.1 链接的概念与作用

5.1.1链接的概念

链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。

5.1.2链接的作用

通过链接,可以将程序分解为多个模块进行单独编译,然后再将它们链接在一起,当修改程序中的某个部分时,只需要重新编译受影响的模块,而不是整个程序,从而节省了编译时间。

链接器解析.o文件中的符号引用,确定它们在实际内存中的地址,并将它们正确地连接起来,并且链接器可将多个.o文件和可能需要的库文件组合成一个可执行文件。

5.2 在Ubuntu下链接的命令

使用链接命令gcc -v -m64 -Og -no-pie -fno-PIC hello.o -o hello进行链接,查看生成的可执行文件并运行。

图 5-1 链接命令及结果文件

图 5-2 hello程序运行结果

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

使用命令readelf -a hello > helloelf2.txt,查看分析hello的ELF格式文件信息,包含31个节头信息,各段起始地址和大小信息如图中所示。

图 5-3 各节主要信息

图 5-4 各节主要信息

ELF头信息如图所示,包括魔数、版本、入口点地址、目标体系结构、节表偏移、程序头表偏移等。

图 5-5 ELF头信息

程序头表信息如图所示,提供了关于文件在内存中的布局和如何加载到内存中的信息,其中INTERP描述了用于加载该ELF文件的程序解释器,这里是/lib64/ld-linux-x86-64.so.2,用于64位x86 Linux系统;DYNAMIC描述了动态链接所需的信息,包括符号表、重定位表等,通常用于共享库和动态链接的可执行文件;LOAD描述了应该从文件中加载到内存中的段,包含文件偏移量、虚拟内存地址、文件大小和内存大小等信息。

图 5-6 程序头表信息

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,在0x401000地址处为_init过程,对比5.3中内容,与节头表中.init段开始地址0x401000相符。

图 5-7 edb下_init过程信息

在0x401000地址处为_start过程,与节头表中.text段开始地址0x4010f0相符。

图 5-8 edb下_start过程信息

在0x402000地址处为.rodata段,用于存放字符串常量。

图 5-9 edb下.rodata段信息

5.5 链接的重定位过程分析

使用objdump -d -r hello命令,将hello可执行文件反汇编,对比hello.o的重定位项目,可以发现在hello.o中使用了00 00 00 00作为占位符处均重定位到正确地址。

例如在引用字符串常量时,在hello.o文件中采用绝对寻址方式定位到1a: R_X86_64_32   .rodata.str1.8信息所指示处,而在hello文件中直接定位到0x402008地址处。

图 5-10 hello.o与hello文件寻址对比

在调用exit函数时,在hello.o文件中采用相对寻址方式定位到29: R_X86_64_PLT32 exit-0x4信息所指示处,而在hello文件中直接定位到0x 4010c0地址处。

图 5-11 hello.o与hello文件寻址对比

5.6 hello的执行流程

使用edb执行hello,从加载hello到_start,到call main,以及程序终止的所有过程中,调用与跳转的各个子程序名及程序地址如下图所示。

图 5-12各个子程序名及程序地址

图 5-13各个子程序名及程序地址

5.7 Hello的动态链接分析

在可执行文件hello中,.got 和 .got.plt 是与全局偏移表和过程链接表相关的特殊节,主要用于支持动态链接和位置无关代码。其中.got 节包含指向全局变量和函数的指针,在动态链接的环境中,这些指针在程序加载时会被解析为实际的内存地址,可在程序运行时动态地解析这些地址。

.got.plt 节是 .got 节的一个子集,专门用于存储指向 PLT的条目的指针,PLT 是动态链接器用于解析函数调用的一个结构,当程序调用一个动态链接的函数时,它首先会跳转到 PLT 中的一个条目,这个条目会负责解析并跳转到实际的函数地址,.got.plt 节中的指针指向这些 PLT 条目。在函数第一次调用后,.got.plt中的条目会更新为实际的函数地址,后续调用会直接跳转到该函数,因此可查看.got.plt中内容变化分析在动态链接前后项目内容的变化。

查看.got.plt 节地址,在edb调试中查看动态链接前后该节内容变化,可以发现该节内容发生变化。

图 5-14.got.plt 节地址

图 5-15程序运行前.got.plt 节

图 5-16程序运行后.got.plt 节

5.8 本章小结

本章概述了链接环节的概念和作用,结合hello文件,对应的ELF格式文件和hello得到的反汇编文件,对链接得到的可执行文件进行分析,查看分析hello的ELF文件头、程序头表等,使用edb加载hello,查看该进程的虚拟地址空间各段信息,以及执行流程和动态链接过程。

(第51分)

6章 hello进程管理

6.1 进程的概念与作用

6.1.1.进程的概念

进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位,也是操作系统结构的基础。进程是程序的一次执行过程,是动态的,包括创建、调度、执行和消亡。

进程的创建使用fork函数,该函数通过系统调用复制一个现有进程来创建一个全新的进程,调用fork的进程称为父进程,新产生的进程称为子进程,fork系统调用从内核返回两次,一次返回到父进程,另一次返回到子进程。

进程的消亡通过wait函数,父进程通过wait函数阻塞等待,直至子进程执行完后,等待其进程状态发生变化然后对其进行回收,同样可以使用waitpid函数实现此功能。

6.1.2进程的作用

操作系统通过进程来分配和管理系统资源,每个进程都有自己独立的地址空间、数据栈和其他系统资源,这使得不同的进程之间可以相互隔离,互不影响,保证了系统的稳定性和安全性。

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

6.2.1.shell简介

shell 是一个用户界面,允许用户向操作系统提供命令。它可以是命令行界面(CLI)或图形用户界面(GUI),但是在许多技术文档和讨论中,shell 通常指的是命令行界面。shell 作为用户与操作系统交互的桥梁,允许用户通过键入命令来执行操作,如文件管理、程序运行和系统监控等。

6.2.2.shell作用

shell 的主要作用包括命令执行、程序调用、文件系统操作、输入输出重定向和管道。用户可以通过直接在 shell 中键入命令来执行简单的任务,也可以通过编写脚本来自动化复杂的过程。

6.2.3.shell处理流程

当用户登录系统并启动shell时,shell会读取用户配置文件并等待用户输入命令。用户输入的命令会被shell解析成单词和参数,并在系统的命令搜索路径中查找对应的可执行文件或内置命令。一旦找到命令,shell会创建一个子进程来执行它(对于外部命令)或直接在当前进程中执行(对于内置命令或脚本)。执行完毕后,shell会收集命令的退出状态并显示任何输出。之后,shell会回到等待用户输入的状态,持续循环处理用户的后续命令。

6.3 Hello的fork进程创建过程

在shell中输入./hello命令执行hello程序,shell对命令进行解析后调用fork函数创建一个新的子进程,子进程得到与父进程用户级虚拟地址空间相同的一份副本,包括代码段、数据段、堆、共享库和用户栈。fork函数被调用后,会返回两次值,一次在父进程中,返回新创建的子进程的进程ID;另一次在子进程中。返回0。在子进程中执行hello程序,内核调度父进程与子进程并发执行,终端父进程等待子进程执行结束

6.4 Hello的execve过程

execve函数需要三个参数:

filename:要执行的程序的路径名,如./hello。

argv:字符串数组,表示要传递给新程序的命令行参数。

envp:字符串数组,表示新程序的环境变量,每个环境变量都是一个“name=value”格式的字符串。

创建新进程后调用execve函数执行程序时,函数读取可执行文件hello,解析其头部信息,加载其代码段、数据段和相关的动态链接库等,随后execve函数会完全替换当前进程,新的程序将使用与当前进程相同的PID、父PID、文件描述符等,后将argv和envp参数中指定的参数和环境变量传递给新的程序。hello程序被加载到内存中并替换当前进程后,hello程序开始执行main函数,执行后续程序。 该函数运行成功时不返回,失败时返回-1。

6.5 Hello的进程执行

在shell中输入./hello命令执行hello程序,shell对命令进行解析后调用fork函数创建一个新的子进程。当发生硬件中断、系统故障或者系统调用等异常时切换进入核心态,而当内核调度进程时,也会进入内核模式进行上下文切换然后再进入用户模式。

每个进程都有自己的上下文信息,包含通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和内核数据结构如页表、进程表、已打开文件的文件表等。当内核调度到hello程序所在的进程时,进行上下文切换:保存原先进程的上下文,恢复hello进程的上下文,将控制转移给 hello 进程,逻辑控制流实现从另一个进程到hello进程的切换。

子进程调用execve函数会完全替换当前进程,加载hello程序时,新的程序将使用与当前进程相同的PID、父PID、文件描述符等,后将argv和envp参数中指定的参数和环境变量传递给新的程序,操作系统将新程序的入口点设置为新进程的下一条指令。

一旦execve执行成功,子进程开始运行hello程序的代码,hello程序开始在用户态执行,如果hello程序进行系统调用,如打印输出操作,会通过系统调用进入核心态。当hello程序执行完毕,进程通过exit系统调用通知操作系统它已执行完成,父进程可以通过wait或waitpid系统调用等待子进程结束,并获取子进程的终止状态。

执行期间操作系统的调度器决定哪个进程应该运行,调度器根据调度算法和策略分配 CPU 时间,每个进程在 CPU 上运行一段时间称为一个时间片,时间片结束后,调度器可能会切换到另一个进程。当时间片结束或有更高优先级的进程需要运行时,调度器保存当前进程的上下文,并加载下一个要运行的进程的上下文。

6.6 hello的异常与信号处理

6.6.1缺页故障

开始加载执行hello程序时,操作系统仅仅为hello进程建立了虚拟内存和磁盘上的hello可执行文件之间的映射,hello可执行文件的内容还没有真正被加载到物理内存,因此取出第一条指令时会触发缺页异常,执行故障处理程序,发送信号给用户程序,然后hello程序才会被加载到物理内存。

6.6.2按下键盘Ctrl+C

在执行hello的过程中,如果按下键盘Ctrl+C,就会触发一个硬件中断,操作系统的内核执行中断处理子程序,并向hello进程发送SIGINT信号,hello进程随即终止。此时按下ps查看进程运行状态发现没有hello进程。

图 6-1按下键盘Ctrl+C

6.6.3 按下键盘Ctrl+Z

在执行hello的过程中,如果按下键盘Ctrl+Z,会向hello进程发送一个SIGTSTP信号,hello进程随即被挂起。

图 6-2按下键盘Ctrl+Z

在shell中执行ps -u指令,发现后台仍存在hello进程,并且此时hello进程状态为T,即为被挂起状态。

图 6-3执行ps -u命令

在shell中执行jobs指令,查看被挂起的作业。

图 6-4执行jobs命令

在shell中执行pstree指令,打印进程树,查看hello进程,可以看到shell进程bash是hello的父进程,单独查看bash进程树也可以观察到。

图 6-4执行pstree命令

图 6-5执行pstree命令

在shell中执行kill指令,可以看到hello进程终止。

图 6-6执行kill命令

6.6.4不停乱按

按其他按键如字母、空格、回车按键时,程序继续执行,来自键盘输入的信号被忽略,在hello进程结束后,输入的字符会被当成shell中输入的指令,这些输入被发送给shell进程处理。

图 6-7不停乱按

6.7本章小结

本章概述了进程管理的概念和作用,解释了shell原理,fork调用,execve调用,hello程序运行,信号与异常处理等机制,运行hello文件,通过键盘输入查看异常处理与信号发送处理机制。

(第61分)

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址是在有地址变换功能的计算机中,访内指令给出的地址(操作数),也称为相对地址,在程序代码中,逻辑地址用于指定一个操作数或是一条指令的地址。在hello程序中,变量、数组等的地址即为逻辑地址,如下图所示,这些地址是相对于程序自身而言的,而不是直接对应物理内存中的位置。

图 7-1 hello中逻辑地址

线性地址是逻辑地址到物理地址变换之间的中间层,是处理器可寻址的内存空间中的地址,当hello程序被加载到内存中并执行时,逻辑地址首先被转换为线性地址。如果CPU没有开启分页功能,线性地址就等同于物理地址;如果开启了分页功能,则线性地址还需要进一步转换为物理地址。

虚拟地址是由程序产生的,由段选择符和段内偏移地址组成的地址,并不直接访问物理内存,而是要通过分段地址的变换处理后才会对应到相应的物理内存地址,hello程序中展现出来的均为虚拟地址,而不是物理地址,如下图所示。

图 7-2 hello中虚拟地址

物理地址是指在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果,也是内存储器中的实际有效地址,hello程序执行时,CPU最终访问的是物理地址,对应着内存中的实际存储单元。

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

逻辑地址由两部份组成,段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符,其中前13位是一个索引号,后面3位包含一些硬件细节,表示具体的是代码段寄存器还是栈段寄存器抑或是数据段寄存器。

图 7-3 段标识符组成

索引号就是段描述符的索引,段描述符具体地址描述了一个段,多个段描述符组成“段描述符表”,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,每一个段描述符由8个字节组成,其中Base字段表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。

段选择符中的T1=0,表示用全局段描述符表GDT,T1=1表示用局部段描述符表LDT,GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。

首先检查段选择符中的T1字段,以决定段描述符保存在哪一个描述符表中,再根据相应寄存器,得到其地址和大小。由于一个段描述符8字节长,因此其在GDT或LDT内的相对地址是由段选择符的最高13位的值乘以8得到,拿出段选择符中前13位,可以查找到对应的段描述符,即可得到偏移地址,Base + Offset,即为要转换的线性地址。

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

Linux中逻辑地址等于线性地址,Linux中所有的段,如用户代码段、用户数据段、内核代码段、内核数据段等,其线性地址都是从 0x00000000 开始,这样线性地址=逻辑地址+ 0x00000000,因此逻辑地址等于线性地址。

虚拟地址到物理地址的转化由CPU和内存管理单元(MMU)共同完成,操作系统通过页表这一数据结构将虚拟地址映射到物理地址,页表通常包括页表条目(PTE),每个PTE包含一个有效位和一个地址字段,有效位指示虚拟页是否在物理内存中,地址字段则指向物理页的位置。

在分页式内存管理中,虚拟地址空间和物理地址空间被分成固定大小的页,当程序访问虚拟地址时,CPU通过页表将虚拟地址的页部分转换为物理地址的页部分,再加上虚拟地址的页内偏移量,就得到了物理地址。

如果虚拟地址对应的页表条目中的有效位为0,表示该页不在物理内存中(即缺页),此时会触发缺页异常处理程序,处理程序会在物理内存中选择一个物理页来加载该虚拟页的内容,并更新页表,然后处理程序会重新启动导致缺页的命令。

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

TLB翻译后备缓冲器(Translation Lookaside Buffer,TLB)是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相联度,用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。如果TLB有T=2t个组,那么TLB索引(TLBI)是由VPN的t个最低位组成的,而TLB标记(TLBT)是由VPN中剩余的位组成的。

图 7-4 虚拟地址组成

1、CPU产生一个虚拟地址VA并发送到MMU, MMU将VA分割成虚拟页号(VPN)与虚拟页偏移(VPO)。

2、将VPN送至TLB快表中查询PPN,先用TLBI到TLB中找到对应的组,然后比较组中每一个块的tag与TLBT,如果相等则在TLB中命中,直接拿到该VPN对应的页表条目,从页表条目中解析出物理页号PPN,并与VPO一起构成物理地址,如果TLB的对应组中每一块的tag都未与TLBT匹配成功,则需要查询内存中的页表。

3、查询页表时,将VPN分为4组VPN1~VPN4,分别对应了四级页表每一级的偏移量。其中第一级页表的起始地址放在一个固定的寄存器(CR3)中,然后以VPN1为偏移到一级页表中找到对应的页表条目,这个页表条目中保存着这个页对应的二级页表的起始地址,拿到二级页表的起始地址之后再以VPN2为偏移,找到二级页表中的对应条目,其中包含了三级页表的起始地址,以此类推,最后在四级页表的页表条目中包含了这个页的物理页号,拿到物理页号PPN后将PPN与VPO(即为PPO)组合成物理地址。如果查询过程中发生缺页异常,则需要从磁盘中进行页面调度。

4、由于页表是保存在内存中,因此查询页表时,优先在Cache中查询,查不到再到物理内存中查询,如果物理内存中也没有则需要从磁盘进行页面调度。最后拿到虚拟地址对应的物理地址之后访问物理内存,也是先从Cache中读取数据,不命中再从内存中读取,又不命中就从磁盘中调度。

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

L1 Cache:一级缓存内置在CPU内部并与CPU同速运行,可以有效地提高CPU的运行效率。

L2 Cache:二级缓存是为了协调一级缓存和内存之间的速度,当处理器的速度逐渐提升,一级缓存可能供不应求,这时就需要提升到二级缓存。

L3 Cache:三级缓存是为读取二级缓存后未命中的数据设计的。

在三级Cache(L1、L2、L3)支持下的物理内存访问过程中,CPU通过多级缓存来优化对物理内存的访问,从而提高数据处理的速度。以下是三级Cache支持下的物理内存访问的详细过程:

1、当CPU需要访问某个数据时,首先会检查L1 Cache,如果L1 Cache中不存在所需数据(即缓存未命中),则会继续检查L2 Cache。

2、如果L2 Cache中也未命中,则会检查L3 Cache。由于L3 Cache的容量比L1和L2都大,因此其命中率通常也会更高。

3、如果三级缓存都未命中,CPU则需要从物理内存中读取数据。此时,为了优化性能,CPU会将读取到的数据块同时加载到L3 Cache、L2 Cache和L1 Cache中,以便后续再次访问时能够直接从缓存中读取。

7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为给新进程创建虚拟内存,内核创建了当前进程的mm_struct、区域结构和页表的原样副本。将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:

1、删除已存在的用户区域:删除当前进程虚拟地址的用户部分中的已存在的区域结构。

2、映射私有区域:为新程序的代码、数据、.bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。

3、映射共享区域:如果hello程序与共享对象(或目标)链接,比如标准C库 libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

4、设置程序计数器(PC):execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点,下一次调度这个进程时,它将从这个人口点开始执行。

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

缺页故障指的是当程序试图访问已映射在虚拟地址空间中,但当前并未被加载在物理内存中的一个分页时,由中央处理器的内存管理单元(MMU)所发出的中断。

当缺页故障发生时,硬件会陷入内核模式,并在内核堆栈中保存程序计数器(PC)、通用寄存器和其他易失的信息,以免被操作系统破坏,之后执行缺页异常处理程序,采用某种算法从物理内存当中寻找到一个合适的牺牲页,如果这个牺牲页被修改过,就要将它写回磁盘中对应的交换文件,然后从磁盘的交换文件中读取那个要访问的页面并加载到物理内存、更新这个进程的页表。当缺页异常处理程序返回时,重新执行引起缺页异常的指令。

7.9动态存储分配管理

动态存储分配管理,是指在目标程序或操作系统运行阶段,根据程序的实际需求动态地为源程序中的变量分配存储空间,在动态内存分配管理中,分配器主要分为显式分配器和隐式分配器两大类。

显式分配器要求应用显式地分配和释放内存块,应用程序需要明确地告诉分配器何时需要内存,并在不再需要时释放它。隐式分配器要求分配器自动检测何时一个已分配的内存块不再被程序使用,并自动释放它。

分配器对空闲块的组织有几种方式:隐式空闲链表、显式空闲链表、分离的空闲链表,每种实现中又分为带标签边界的和不带标签边界的。其中带边界标签的隐式空闲链表是使用边界标签来隐含地链接空闲的内存块,每个内存块都有一个头部(包含大小信息)和一个尾部(用于标记块的结束),空闲块通过头部的信息隐含地链接在一起,形成一个链表,当需要分配内存时,分配器会搜索这个链表以找到一个足够大的空闲块,分配器可以使用不同的策略来选择要分配的空闲块。

显示空间链表是一种将空闲内存块组织为显式数据结构的技术,空闲块被组织成一个链表,其中每个空闲块都包含指向链表中下一个空闲块的指针,这种链表是显式的,因为程序可以直接访问和操作它,当需要分配内存时,分配器可以直接遍历这个链表来找到一个空闲块。

7.10本章小结

本章概述了存储管理的概念和作用,解释了逻辑地址 、线性地址、虚拟地址、物理地址的概念和区别,详细阐述了Intel逻辑地址到线性地址的变换,线性地址到物理地址的变化,三级cache访存,TLB和四级页表访存,内存映射下的fork、execve系统调用和动态内存管理。

(第7 2分)

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

在Linux中,几乎所有的资源都被抽象为文件,应用程序可以通过标准的系统调用来访问和操作这些设备,设备文件通常位于/dev目录下。Unix的IO接口,通过一组标准的系统调用来实现设备的访问和管理。

设备管理:unix io接口

Unix I/O模型是在操作系统内核中实现的。应用程序可以通过诸如 open、close、lseek、read、write 和 stat 这样的函数来访问 Unix I/O。较高级别的RIO和标准I/O 函数都是基于(使用)Unix I/O函数来实现的。

8.2 简述Unix IO接口及其函数

Unix I/O的系统调用函数很简单,它只有5个函数:open(打开)、close(关闭)、read(读)、write(写)、lseek(定位)。

8.2.1打开文件

int open(char *filename, int flags, mode_t mode);

表示打开一个文件或设备,并返回一个文件描述符,文件成功打开则返回文件描述符,出错则返回-1,其中参数分别表示:

filename:要打开的文件的路径名。

flags:指定打开文件时的行为,如O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)等,还可以与如O_CREAT(如果文件不存在则创建它)等标志组合使用。

mode:指定新文件的访问权限位。

8.2.2关闭文件

int close(int fd);

表示关闭一个已打开的文件或设备,成功关闭则返回0,出错则返回-1,参数fd表示要关闭的文件描述符。

8.2.3读文件

ssize_t read(int fd, void *buf, size_t n);

表示从指定的文件描述符中读取数据,并将其存储在提供的缓冲区中,返回实际读取的字节数,如果出错或已到达文件末尾,则返回-1,其中参数分别表示:

fd:要读取的文件描述符。

buf:指向用于存储读取数据的缓冲区的指针。

n:要读取的字节数。

8.2.4写文件

ssize_t write(int fd, const void *buf, size_t n);

表示将数据从提供的缓冲区写入到指定的文件描述符中,返回实际读取的字节数,如果出错或已到达文件末尾,则返回-1,其中参数分别表示:

fd:要读取的文件描述符。

buf:指向用于存储读取数据的缓冲区的指针。

n:要读取的字节数。

8.2.5定位

off_t lseek(int fd, off_t offset, int whence);

表示改变当前文件位置,从文件开头起始的字节偏移量,返回新的文件位置,如果出错,则返回-1,其中参数分别表示:

fd:要操作的文件描述符。

offset:相对于whence参数的偏移量。

whence:指定偏移量的起始位置,可以是SEEK_SET(文件开头)、SEEK_CUR(当前位置)或SEEK_END(文件末尾)。

8.3 printf的实现分析

printf首先开辟缓冲区buf[256],然后调用vsprintf和write函数,vsprintf返回要打印的字符串的长度,系统调用write把目标字符串打印到终端,通过syscall切换到内核态。期间在总线中传输的是字符的ASCII码,因此存储到显存中的也是字符的ASCII码。内核态的字符显示驱动子程序会通过这些字符串及其字体生成要显示的像素数据,将它们传到屏幕上对应区域的显示vram中,显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar 检查是否已经有可用的字符在输入缓冲区中,如果有字符可用,则直接从这个缓冲区中读取一个字符并返回,如果无字符可用,则调用read通过系统调用读取按键ASCII码。其中read系统调用会通过syscall切换到内核态,等待键盘输入硬件中断,当按下键盘后,触发键盘中断处理子程序,接受按键扫描码转成ASCII码,保存到系统的键盘缓冲区,直到用户键入回车后,getchar开始读取stdin缓冲区。

8.5本章小结

本章概述了IO管理的概念和作用,解释了Linux的IO设备管理方法以及Unix I/O中系统调用函数:open(打开)、close(关闭)、read(读)、write(写)、lseek(定位),最后分析了常用的printf函数和getchar函数。

(第81分)

结论

1、程序员编写出hello.c源文件,此时未经过任何加工处理。

2、hello.c源文件经过预处理阶段,变为hello.i文本文件。

3、hello.i文本文件经过编译阶段,变为hello.s汇编文本文件。

4、hello.s汇编文本文件经过汇编阶段,变为hello.o可重定位目标文件。

5、hello.o文件和其他引用的动态链接库中的代码经过链接阶段,生成可执行文件hello。

6、在shell中运行hello程序,输入./hello 2022113320 李季坤 15028190100 0启动hello程序。

7、父进程调用fork函数,创建子进程,调用execve函数,加载hello进程。

8、初次执行hello进程触发缺页异常,调用缺页异常处理程序将所需页面调入主存以及cache。

9、执行printf和getchar函数调用IO函数。

10、进程结束,父进程回收子进程。

在完成程序人生大作业的过程中,可以发现简单的hello程序却可以将计算机系统课程内容全部串联起来,与初次编写hello程序只是简单运行相比,对程序内部如何一步步处理,如何实际运行有了十分深刻的理解。经过一学期的学习,真正从底层上分析计算机的各种运行机制,深入了解了计算机系统的整体架构,对今后的学习有了更大的帮助和更深层次的认识。

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

附件

hello.c:源代码

hello.i:预编译后的文件

hello.s:编译后的文件

hello.o:汇编后的代码

hello.out:链接后的文件

helloelf.txt:hello.o的elf文件

hello2.txt:hello.o的反汇编代码

helloelf2.txt:hello的elf文件

hello.txt:通过对hello.out反汇编生成的代码

hello3.txt:hello的反汇编代码

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

参考文献

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

[1]  https://blog.csdn.net/Pipcie/article/details/105670156

[2]  https://blog.csdn.net/waittor/article/details/99345380

[3]  https://www.cnblogs.com/yeahwell/archive/2013/03/30/5226034.html

[4]  https://www.cnblogs.com/whc-uestc/p/4365507.html

[5]  https://www.cnblogs.com/pianist/p/3315801.html

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值