HIT-ICS大作业

摘  要

本文通过对Hello程序完整生命周期的分析,系统阐述了计算机系统设计与实现中的核心概念和技术。研究从源代码(hello.c)到可执行文件的编译过程开始,详细描述了预处理、编译、汇编和链接四个阶段,揭示了程序如何从静态代码转变为动态进程(P2P过程)。随后,分析了Hello程序在操作系统中的运行与终止(020过程),包括进程创建、、内存映射(mmap)、资源调度以及进程退出后的资源回收。此外,本文深入探讨了存储管理机制(如虚拟地址到物理地址的转换、页表、TLB、Cache)和异常信号处理等关键技术。通过对ELF文件格式、符号表、重定位和动态链接的解析,结合实验环境(Ubuntu 22.04 LTS)下的具体实现,展示了计算机系统运行的底层原理。本文旨在通过Hello程序的案例分析,为理解计算机系统的设计与运行提供全面视角,同时强调模块化设计、性能优化与用户体验的重要性。

关键词:计算机系统;编译过程;进程管理;链接;存储管理;

目  录

第1章 概述... - 5 -

1.1 Hello简介... - 5 -

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

1.3 中间结果... - 6 -

1.4 本章小结... - 6 -

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

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

2.1.1 预处理的概念... - 7 -

2.1.2预处理的作用... - 7 -

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

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

2.4 本章小结... - 9 -

第3章 编译... - 10 -

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

3.1.1编译的概念... - 10 -

3.1.2编译的作用... - 10 -

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

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

3.3.1hello.s初始字段... - 11 -

3.3.2数据... - 12 -

3.3.3赋值... - 13 -

3.3.4类型转换... - 14 -

3.3.5算术操作... - 14 -

3.3.6关系操作... - 14 -

3.3.7数组操作... - 15 -

3.3.8控制转移... - 16 -

3.3.9函数操作... - 16 -

3.4 本章小结... - 18 -

第4章 汇编... - 19 -

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

4.1.1汇编的概念... - 19 -

4.1.2汇编的作用... - 19 -

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

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

4.3.1 elf头... - 20 -

4.3.2节头部表... - 20 -

4.3.3符号表... - 22 -

4.3.4重定位节... - 22 -

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

4.4.1分支转移... - 23 -

4.4.2函数调用... - 24 -

4.4.3操作数进制... - 25 -

4.4.4增加机器语言... - 25 -

4.5 本章小结... - 26 -

第5章 链接... - 27 -

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

5.1.1链接的概念... - 27 -

5.1.2链接的作用... - 27 -

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

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

5.3.1 elf头... - 28 -

5.3.2 节头部表... - 28 -

5.3.3符号表... - 30 -

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

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

5.5.1分析helo与helo.o反汇编区别... - 32 -

5.5.2重定位过程... - 33 -

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

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

5.8 本章小结... - 36 -

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

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

6.1.1进程的概念... - 37 -

6.1.2进程的作用... - 37 -

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

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

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

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

6.5.1上下文信息:... - 40 -

6.5.2进程时间片:... - 40 -

6.5.3进程调度:... - 40 -

6.5.4用户态和内核态:... - 40 -

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

6.6.1异常... - 41 -

6.6.2信号... - 41 -

6.6.3hello执行中的异常与信号... - 41 -

6.7本章小结... - 46 -

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

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

7.1.1逻辑地址... - 47 -

7.1.2线性地址... - 47 -

7.1.3虚拟地址... - 47 -

7.1.4物理地址... - 47 -

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

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

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

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

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

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

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

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

7.10本章小结... - 51 -

结论... - 52 -

1. hello所经历的过程... - 52 -

2.感悟... - 52 -

附件... - 54 -

参考文献... - 55 -

第1章 概述

1.1 Hello简介

Hello的P2P(Program to Process)与020(Zero-0 to Zero-0)过程,是程序从静态代码到动态进程,最终被系统回收的完整生命周期。

P2P指从hello.c源代码(Program)转变为运行时的进程(Process)。源代码经过预处理、编译、汇编和链接这四个阶段,最终生成一个二进制文件(可执行文件),这就完成了从Program(程序)阶段到下一阶段的基础铺垫。

用户在Shell(比如Bash)中发出启动指令,操作系统(OS)先利用fork系统调用复制出新的进程,然后通过execve替换进程内存映像,将之前构建好的二进制文件加载到内存中。接着,系统使用mmap将程序所需的内存区域映射到进程虚拟地址空间内。整个过程中,OS按照时间片调度模型在CPU、RAM和IO设备之间协调该进程的执行。原本静态的程序此刻在硬件平台上经历取指、译码、执行以及流水线等一系列操作,转化为了动态执行的进程。

020020描述了Hello从无到有、再从有到无的完整生命周期,即从源代码的创建到进程的终止。最初内存中并无hello文件的相关内容,Hello从一个空文件(Zero-0)开始,经过程序员的编写(hello.c)、预处理、编译、汇编和链接,最终成为可执行文件。通过系统调用从静态文件变为动态运行的进程,系统分配了CPU时间、内存资源、IO通道等一系列硬件和软件资源。此时,虚拟地址(VA)经过内存管理单元(MMU)转换为物理地址(PA),TLB、四级页表、三级Cache以及Pagefile等组件共同确保数据高速流动,各个模块协调一致,共同支持进程的运行。

程序结束后,Hello进程退出,OS释放内存、关闭文件描述符、撤销页表映射,shell父进程回收hello进程,内核删除hello文件相关的数据结构,回到了最初的状态。

1.2 环境与工具

硬件环境:

处理器:13th Gen Intel(R) Core(TM)i9-13900H  2.60 GHz 机带RAM:16.0GB

系统类型:64位操作系统,基于x64的处理器

软件环境:Windows11 64位,VMware,Ubuntu 22.04 LTS

开发与调试工具:Visual Studio 2022 64位;vim objump edb gcc readelf等

1.3 中间结果

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

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

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

hello          可执行文件

hello.elf        hello.o的ELF文件格式

hello1.elf       hello的ELF文件格式

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

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

1.4 本章小结

本章通过分析Hello程序的P2P和O2O过程,完整展示了程序从源代码到可执行文件,再到进程运行和终止的生命周期。P2P部分详细描述了源代码如何经过预处理、编译、汇编和链接生成可执行文件,并通过系统调用加载到内存成为动态进程;O2O部分则强调了Hello从空文件创建到进程终止的全过程,涉及操作系统对资源的管理与回收。此外,本章还介绍了实验所用的硬件与软件环境,然后通过说明各个中间结果文件(如预处理文件hello.i、汇编文件hello.s、可重定位目标文件hello.o,以及反汇编文件hello.asm和hello1.asm)的生成与作用,进一步展示了编译链接过程中各个阶段的转换与验证,为理解程序运行的细节提供了明确的依据。

第2章 预处理

2.1 预处理的概念与作用

2.1.1 预处理的概念

在C/C++程序的编译过程中,预处理是第一步,由预处理器(Preprocessor)负责执行。预处理的本质是处理源代码中以# 开头的预处理指令,生成一个扩展后的源代码文件,为后续的编译阶段做准备。C语言预处理后的文件通常以 .i为后缀。预处理器是一个独立的程序或模块,它在编译器进行语法分析和代码生成之前,对源代码进行初步处理。它的主要任务是根据预处理指令修改源代码内容,例如替换宏定义、包含头文件、删除注释等。预处理的结果是一个纯粹的代码文件,供编译器进一步处理。

2.1.2预处理的作用

预处理为程序构建提供灵活性与可维护性,具体作用包括:

1.头文件包含(Include):使用#includ可以将指定的头文件内容插入到当前源文件中。这种机制方便了代码的模块化和重用,使得开发者可以将公共函数声明、宏定义和类型信息集中管理,而不必在每个源文件中重复书写。

2.宏定义与替换:利用#define定义宏,预处理器会将代码中所有与宏标识符匹配的部分替换为其对应的文本。宏不仅可以简化代码书写,还能在编译之前做简单的参数化替换和计算,极大地提高代码的灵活性与可维护性。

3.条件编译:条件编译指令(如#ifdef、#ifndef、#if等)允许开发者根据不同的平台、编译配置或调试状态选择性地编译代码。这使得同一份代码能够适配不同的系统环境和需求,保证了代码的跨平台性和灵活配置。

4.代码清理与调试支持:预处理过程还会剔除掉程序中的注释,并生成一个干净的、扩展了所有宏与头文件后的代码文件(如hello.i)。这个中间结果文件为后续的编译阶段提供了标准化输入,同时也方便开发者调试和验证预处理的正确性。

2.2在Ubuntu下预处理的命令

Ubuntu下预处理命令为:gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i

预处理过程截图如下:

图2.2  预处理过程

2.3 Hello的预处理结果解析

Ubuntu下查看生成的hello.i文件,与源代码hello.c比较,发现多出了几千行的预处理扩展部分,而main函数部分没有改变,这与上文所介绍的预处理的概念和作用一致,如下图所示。

图2.3  预处理结果

在预处理结果开头可以注意到大量以 # 开头的行号指示信息,这些行号信息有助于编译器在进行后续错误报告时准确定位原始代码中的位置。预处理器首先扫描源文件hello.c,然后根据内置和命令行参数插入相应的处理结果。hello.c中包含了三个头文件:#include 、#include 和#include 。在预处理阶段,预处理器将这些头文件的内容完整地插入到hello.i文件中。

在预处理阶段,预处理器根据#include指令将这些头文件的全部内容展开,替换到对应的位置。结果就是hello.i中几千行代码就是标准库中的结构、宏定义、函数声明及其他内容(例如关于 I/O、系统调用、数值转换等的定义)。这正体现了模块化编程的优势:程序员只需包含必要的头文件,而预处理器会将所需内容全部“注入”到源代码中,为编译器提供完整的上下文。

hello.i文件中已经包含了所有宏和头文件展开后的完整代码,是编译器下一阶段——编译器前端(词法分析、语法分析等)工作的标准输入。

2.4 本章小结

通过对Hello程序的预处理过程及hello.i文件的分析,我深入理解了预处理在C编译过程中的重要性及其具体作用。预处理作为编译的第一步,由预处理器负责处理源代码中的预处理指令(如头文件包含、宏定义与替换、条件编译等),生成一个扩展后的源代码文件(如hello.i)。该文件包含了所有被包含的头文件内容和宏展开后的代码,为后续的编译阶段提供了干净、标准化的输入。

第3章 编译

3.1 编译的概念与作用

3.1.1编译的概念

编译是将高级语言的源代码转换为低级语言(如汇编语言或机器码)的过程。在这一步,编译器对预处理后的源代码进行词法和语法分析,确保代码结构正确。编译器在中间表示层面进行各种优化操作,如常量折叠、内联展开、循环优化、死代码消除等,以提高最终生成代码的性能和效率。经过上述各阶段后,编译器基于目标平台架构生成对应的汇编代码,也就是.s文件。

3.1.2编译的作用

通过语法和语义分析,编译器能捕捉到代码中的错误和潜在问题(如类型不匹配、未定义的变量等),为开发者提供详细的错误信息。这不仅保证了源代码的正确性,也为后续生成高效代码奠定基础。

生成汇编代码的过程中,编译器会依据目标硬件(如处理器架构)选择合适的指令集和优化策略。优化阶段可大幅度提升程序运行效率,同时减少资源占用。通过这些优化,程序不仅能在多个平台上正确运行,还能充分发挥硬件性能优势。

虽然汇编代码相对于高级语言来说更难直接理解,但对于系统编程、性能调优与编译器优化研究来说,生成的.s文件提供了一个中间结果,让开发者能更清楚地看到高层代码如何被转换为底层指令。通过分析汇编代码,程序员也能更好地理解机器底层运行机制,进而进行精细的性能调优。

生成汇编代码后,后续的过程会将.s文件转换成目标文件(.o),接着链接(linking)生成最终的可执行程序。编译阶段的正确与高效直接影响整个编译链的成果,因此在这个阶段对代码进行精准优化和转换是非常重要的。

3.2 在Ubuntu下编译的命令

Ubuntu下编译的命令为:gcc -m64 -no-pie -fno-PIC -S hello.c -o hello.s

编译过程截图如下:

图3.2  编译过程

3.3 Hello的编译结果解析

在hello.s文件中,编译器将C语言中的数据类型和操作转换为汇编语言中的寄存器和内存操作。

3.3.1hello.s初始字段

在hello.s的初始部分,main函数前有一部分字段展示了节名称,如下图所示。第一部分的汇编代码有一部分是以.作为开头的代码段。这些代码段是指导汇编器和连接器工作的伪指令,对用户来说通常可以忽略这些代码,但对汇编器和连接器十分重要,为之后链接过程使用。

图3.3.1  hello.s初始字段

其中.file一行为文件头,这行声明了源文件名 hello.c,表明这是由hello.c文件编译而来的。接下来的部分是文本段和只读数据段。.text指示接下来的代码段是可执行代码,.section .rodata定义了一个只读数据段,用于存放常量字符串等数据。.align8将接下来的数据对齐到8字节边界,提高内存访问效率。.LC0和.LC1分别定义了两个字符串常量,用于后续的输出。然后是全局变量和类型定义,.globl main声明main为全局符号,使其可以被链接器访问,.type main, @function声明 main是一个函数,这些指令告诉编译器和链接器如何处理main函数。

伪代码各自的具体含义见下表。

表3.3.1  hello.s初始字段解释

名称

含义

.file

声明源文件

.text

表示代码节

.section   .rodata

表示只读数据段

.align

声明对指令或数据的存放地址对齐方式

.string

声明一个字符串

.globl

声明全局变量

.type

声明一个符号的类型

3.3.2数据

3.3.2.1常量

1.立即数

对于if或for语句中的立即数,其值直接保存在.text中,如下图所示。

图3.3.2.1(a)  if语句中的立即数

2.字符串型常量

对于printf中字符串类型的常量,其被保存在.rodata中,如下图所示。

图3.3.2.1(b)  printf中字符串型常量

在main函数中使用字符串时,得到字符串的首地址(leaq相当于转移操作),如下图所示。

图3.3.2.1(c)  main函数调用字符串型常量

3.3.2.2变量

程序若有已初始化的全局变量,会存储在.data中。

1.局部变量

程序中仅有一个局部变量i,由图3.3.2.2(a)可知变量i保存在栈上-4(%rbp)的位置。

图3.3.2.2(a)  局部变量i

2.变量argc和*argv[]

argc存储在%rbp-20;*argv[]存储在%rbp-32,如图3.3.2.2(b)所示。

图3.3.2.2(b)  变量argc和*argv[]

3.3.3赋值

赋值使用mov指令,根据具体数据的大小决定使用哪一条赋值语句。比如对变量i的赋值,由于int型变量i是一个四字节变量,使用movl传递双字实现,如下图所示。

图3.3.3  对int型变量i的赋值

3.3.4类型转换

atoi函数将输入字符中秒数转换为sleep函数需要的整型参数,如下图所示。

图3.3.4  类型转换

3.3.5算术操作

算术操作通过加法、减法和比较指令实现。这些指令包括为局部变量分配栈空间的减法操作,将寄存器值增加特定数值的加法操作,递增局部变量值,以及比较局部变量与立即数大小的操作。

例如for循环中每次循环结束后i值增加1,该操作在汇编代码则使用指令add实现,由于变量i为四字节,使用指令addl,如下图所示。

图3.3.5  变量i值加1

3.3.6关系操作

使用cmp比较两个值的关系,将结果存放到条件码寄存器中。hello.c中存在两个关系操作,分别为:

条件判断语句if(argc!=5):汇编代码将中为:

图3.3.6(a)  条件判断语句if(argc!=4)

使用了cmp指令比较立即数4和参数argc大小,并且设置了条件码。根据条件码,如果不相等则执行该指令后面的语句,否则跳转到.L2。

在for循环每次循环结束要判断一次i<9,判断循环条件被翻译为:

图3.3.6(b)  条件判断语句i<9

同(1),设置条件码,并通过条件码判断跳转到什么位置。

3.3.7数组操作

对数组的操作是先找到数组的首地址,然后加上偏移量即可。例如在main中调用argv[1]和argv[2],在汇编代码中,每次将%rbp-32的的值(即数组首地址)传%rax,然后将%rax分别加上偏移量24、16和8,就得到了argv[1]、argv[2]和argv[3],再分别存入对应的寄存器%rcx、%rdx和%rsi作为第二个参数和第三个参数,之后调用printf函数时使用。调用完printf后同上在偏移量为32时,取得argv[4]并存入%rdi作为第一个参数在调用函数atoi使用,如下图所示。

图3.3.7  数组操作

3.3.8控制转移

设置过条件码后,通过条件码来进行控制转移,在本程序中存在两个控制转移,即为3.3.6节中介绍的内容。

3.3.9函数操作

函数操作首先需要为参数赋值,然后调用参数。

1.main函数

参数传递:该函数的参数为int argc,,char*argv[],分别用寄存器%rdi和%rsi存储。

函数调用:通过使用call指令调用语句进行函数调用,并且将要调用的函数地址数据写入栈中,然后自动跳转到这个调用函数内部。main函数里调用了printf、exit、sleep函数。

局部变量:使用了局部变量i用于for循环,具体局部变量的地址和值都在前面阐述过。

函数返回值:设置%eax为0并且返回,对应return 0。

2.printf函数

参数传递:printf函数调用参数argv[1]、argv[2]和argv[3]。

函数调用:将寄存器%rdi设置为待传递字符串"Hello %s %s %s\n"对应内容的起始地址。在汇编代码中如下图所示。

图3.3.9.2  printf函数调用

3.puts函数

参数传递:LC0段中储存的字符串的首地址

函数调用:调用puts函数打印字符串"用法: Hello 学号 姓名 手机号 秒数!\n"

4.exit函数

参数传递:将1置于%rdi。

函数调用:call exit

在汇编代码中如下图所示。

图3.3.9.3  exit函数调用

5.atoi函数

参数传递:将argv[4]置于%rdi。

函数调用:call atoi

函数返回值:返回值存储在%eax

在汇编代码中如下图所示。

图3.3.9.4  exit函数调用

6.sleep函数

参数传递:将atoi的返回值置于%rdi。

函数调用:call sleep

在汇编代码中如下图所示。

图3.3.9.5  sleep函数调用

7. getchar函数

函数调用:call getchar

在汇编代码中如下图所示。

图3.3.9.6  getchar函数调用

3.4 本章小结

本章探讨了编译的概念、作用以及在Ubuntu环境下进行编译的具体方法,并通过对Hello程序编译结果(hello.s文件)的详细解析,深入剖析了编译过程的核心内容。编译是将高级语言源代码(如C语言)转换为低级语言(如汇编语言或机器码)的关键步骤,其过程包括词法分析、语法分析、语义分析、优化和代码生成等阶段。编译的作用不仅在于确保源代码的语法和语义正确性,还通过优化生成高效的机器码,并适配不同硬件平台,为程序的执行奠定基础。

第4章 汇编

4.1 汇编的概念与作用

4.1.1汇编的概念

汇编是使用汇编器将汇编语言代码转换成机器语言目标文件的工具。它读取汇编语言源文件,将每条汇编指令翻译成对应的机器指令,并生成可重定位目标文件,目标文件中包含了机器指令的二进制表示以及其他相关的信息。为下一步进行链接准备条件。

4.1.2汇编的作用

计算机只能识别处理机器指令程序,汇编过程将汇编语言程序翻译为了机器指令,进一步向计算机能够执行操作的形式迈进,便于计算机直接进行分析处理。简单来说,汇编之后我们能从汇编代码得到一个可重定位目标文件,以便后续进行链接。

4.2 在Ubuntu下汇编的命令

在Ubuntu系统下,对hello.s进行汇编的命令为:gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o

汇编过程如下图所示。

图4.2  Ubuntu下汇编过程

4.3 可重定位目标elf格式

4.3.1 elf

.o文件为目标文件,直接使用文本编辑器查看会出现一大堆乱码。因此选择查看可重定位目标文件的elf形式,使用命令readelf -h hello.o查看elf头,结果如下图所示。

图4.3.1  elf头

elf头以一个16字节的序列(Magic)开始,该序列描述了生成文件的系统的字的大小和字节顺序。elf头剩下部分的信息包含帮助连接器语法分析和解释目标文件的信息,包括elf头的大小、目标文件的类型、机器类型、节头部表的文件偏移以及节头部表中条目的大小和数量等。

4.3.2节头部表

使用命令readelf -S hello.o查看节头,结果如图4.3.2所示。

elf文件中,每个节都有一个对应的节头表,用于描述和定位各个节的信息,通过节头表,可以获取关于每个节的详细信息,如名称、类型、大小、地址、偏移、读写访问权限等信息

图4.3.2  节头部表

4.3.3符号表

.symtab 节中包含 ELF 符号表,这张符号表包含一个条目的数组,存放一个程序定义和引用的全局变量和函数的信息。该符号表不包含局部变量的信息。在终端输入命令readelf -s hello.o并回车,得到符号表如下图所示。

图4.3.3  符号表

其中,Num为某个符号的编号,Name是符号的名称。Size表示其是一个位于.text节中偏移量为0处的153字节函数,Bind表示这个符号是本地的还是全局的。

4.3.4重定位节

.rel.text节是一个.text节中位置的列表,当链接器把这个目标文件和其他文件 组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的 指令都需要修改,而调用本地函数的指令不需修改。可执行目标文件中不包含重 定位信息。在终端输入readelf -r hello.o并回车可查看可重定位段信息,结果如下图所示。

图4.3.4  重定位节

在列出的信息中,偏移量表示需要被修改的引用的节偏移,符号值标识被修改引用应该指向的符号。类型告知连接器如何修改新的引用,加数是一个有符号常数,一些类型的重定位要使用它对被修改的引用的值做偏移调整。

4.4 Hello.o的结果解析

在终端输入readelf -d -r hello.o并回车,对hello.o文件进行反汇编。

可以发现,反汇编得到的结果与hello.s中的汇编代码大体一致,但是存在一些不同。在每条指令的前面出现了一组组由16进制数字组成的代码,这就是机器代码。机器代码是计算机真正可以识别的语言,是二进制机器指令的集合,每一条机器代码都对应一条机器指令。每一条汇编语言都可以用机器二进制数据来表示,汇编语言中的操作码和操作数以一种相当于映射的方式和机器语言进行对应,从而让机器能够真正理解代码的含义并且执行相应的功能。机器代码与汇编代码的不同主要在于下面的介绍。

4.4.1分支转移                      

反汇编的跳转指令中,所有跳转的位置被表示为主函数+段内偏移量这样确定 的地址,而不再是像hello.s文件中跳转的位置为段名称(详见3.3.8),如下图所示。

图4.4.1  反汇编语言中的分支转移

4.4.2函数调用

在汇编代码hello.s中,函数调用直接使用了函数的名称。而在反汇编代码中,call目标地址是当前指令的下一条指令地址。这是因为hello.c中调用的函数都是共享库(如stdio.h,stdlib.h)中的函数,如puts,exit,printf,atoi,sleep等,需要等待链接器进行链接之后才能确定响应函数的地址。因此,机器语言中,对于这种不确定地址的调用,会先将下一条指令的相对地址全部设置为0,然后在.rel.text节中为其添加重定位条目,等待链接时确定地址。

以调用getchar函数为例,在汇编代码中直接使用函数的名称,如下图所示。

图4.4.2(a)  汇编语言中调用getchar函数

而在反汇编中,call 后面不再是函数名称,而是一条重定位条目指引的信息,如下图所示。

图4.4.2(b)  反汇编中调用getchar函数

4.4.3操作数进制

汇编语言中操作数为10进制,反汇编文件中的所有操作数都改为十六进制。

4.4.4增加机器语言

每一条指令增加了一个十六进制的表示,即该指令的机器语言,如下图所示。

图4.4.4  反汇编代码中增加机器语言

4.5 本章小结

本章详细探讨了汇编的概念、作用以及在Ubuntu环境下进行汇编的具体方法,并通过对Hello程序汇编结果(hello.o文件)的深入解析,全面剖析了汇编过程的核心内容。此外,通过对hello.o文件进行反汇编,我们观察到机器代码与原始汇编代码(hello.s)的对应关系,深入理解了汇编器如何将汇编指令转换为二进制形式。汇编不仅完成了从汇编语言到机器语言的转换,还通过生成可重定位目标文件,为后续链接和程序执行提供了坚实基础。

第5章 链接

5.1 链接的概念与作用

5.1.1链接的概念

链接是将各个单独的二进制代码文件加载到同一个文件,并使之可以加载到内存中执行的一个过程。链接可以在编译时被执行,也可以在程序运行中被执行。文件表示为若干个.o文件被合并成一个单独的可执行文件(Linux下默认为.out文件)。

5.1.2链接的作用

链接的作用在于把预编译好了的若干目标文件合并成为一个可执行目标文件,使得分离编译称为可能。不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为可独立修改和编译的模块。当改变这些模块中的一个时,只需简单重新编译它并重新链接即可,不必重新编译其他文件。

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 -no-pie

       链接过程截图如下图所示

图5.2  Ubuntu下链接过程

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

5.3.1 elf

在终端输入命令readelf -h hello并回车,查看hello文件的elf头,结果如下图所示。

图5.3.1  elf头

可以看到文件的Type发生了变化,从REL变成了EXEC(可执行文件),节头部数量也发生了变化,变为了27个。

5.3.2 节头部表

使用命令readelf -S hello查看节头部表信息,如下图所示。

图5.3.2  hello文件的节头部表

elf文件中,每个节都有一个对应的节头表,用于描述和定位各个节的信息,通过节头表,可以获取关于每个节的详细信息,如名称、偏移、大小等。

5.3.3符号表

使用命令readelf -s hello查看符号表信息,如下图所示。

图5.3.3  符号表

可以发现经过链接之后符号表的符号数量显著增加,说明经过连接之后引入了许多其他库函数的符号,一并加入到了符号表中。

5.4 hello的虚拟地址空间

       使用edb打开hello文件,从数据转储窗口观察hello加载到虚拟地址的情况,查看各段信息,如下图所示。

图5.4.1  edb打开hello可执行文件

由5.3节我们可得知,.text的起始虚拟地址为0x4010f0,在edb中查询地址可以得到如下图的结果。

图5.4.2  text段

其余各段以此类推,可以在edb中查找对应的信息。 

5.5 链接的重定位过程分析

在终端中使用命令objdump -d -r hello查看hello反汇编结果,如下图所示。

图5.5  查看hello可执行文件反汇编结果

5.5.1分析helo与helo.o反汇编区别

与第四章中生成的hello.o的反汇编结果进行比较,可以观察到hello的反汇编代码与hello.o的反汇编代码在结构和语法上基本相同,但是hello的反汇编代码多了很多内容,其不同之处如下:

1.链接后函数数量增加,链接后的反汇编代码中多了.plt,puts@plt,printf@plt, getchar@plt,exit@plt,sleep@plt 等函数的代码。这是因为动态链接器将共享库中hello.c用到的函数加入可执行文件中。

2.hello反汇编代码中函数调用时不再仅仅储存call当前指令的下一条指令,而是已经完成了重定位,调用的相应函数已经有对应的明确的虚拟地址空间,如下图所示。

图5.5.1.1  函数调用的变化

3.跳转指令参数发生变化,在链接过程中,链接器解析了重定位条目,并计算相对距离,修改了对应位置的字节代码为PLT中相应函数与下条指令的相对地址,从而得到完整的反汇编代码,如下图所示。

图5.5.1.2  转移指令的变化

5.5.2重定位过程

重定位由两步组成:

1.重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的聚合节。然后链接器将运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。至此程序中每条指令和全局变量都有唯一的运行内存地址。

2.重定位节中的符号引用。这一步中链接器修改代码节和数据节中对每个符 号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。

重定位过程地址计算方法伪代码如下:

foreach section s

{

foreach relocation entry r

{

refptr = s + r.offset;

if (r.type == R_X86_64_PC32)

{

refaddr = ADDR(s) + r.offset;

*refptr = (unsigned) (ADDR(r.symbol) + r.addend - refaddr);

                     }

if (r.type ==R_X86_64_32)

*refptr = (unsigned) (ADDR(r.symbol) + r.addend);

              }

       }

5.6 hello的执行流程

通过edb调试,一步一步地记录下call命令进入的函数。查看hello执行进程,如下图所示。

图5.6.1  hello的执行进程

详细执行过程为:

1.开始执行:_start、_libe_start_main

2.执行 main:_main、printf、_exit、_sleep、getchar

3.退出:exit

5.7 Hello的动态链接分析

动态链接的基本思想是将程序的链接过程推迟到程序运行时进行,而不是在编译时完成。这种方法使得程序可以在启动时加载所需的动态链接库(如.so文件或DLL文件),而不必将库的代码在编译时静态地链接到程序中。通过动态链接,多个程序可以共享同一个库的实现,减少内存占用和磁盘空间使用,并且使得库的更新和维护更为灵活和简便。

在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。延迟绑定是通过GOT和PLT实现的,根据hello.elf文件可知,GOT起始表位置为0x404000,如下图所示。

图5.7.1  GOT起始表位置

GOT表位置在调用dl_init之前0x404008后的字节均为0,如下图所示。

图5.7.2  调用dl_init之前GOT表位置内容

调用了dl_init之后内容发生改变,如下图所示。

图5.7.3  调用dl_init之后GOT表位置内容

可以观察到,部分原为0的位变为了相应链接后的参数,说明动态链接将库的代码映射到了对应的可执行的程序中,便于进程的运行。hello在动态连接器加载前后的重定位是不一样的,在加载之后才进行重定位。

分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变化。要截图标识说明。

5.8 本章小结

本章介绍了链接的相关过程,首先阐述了链接的概念和作用,给出链接在Ubuntu系统下的指令。之后研究了可执行目标文件hello的ELF格式和可执行文件hello的反汇编代码,并通过edb调试工具查看了虚拟地址空间和几个节的内容,之后依据重定位条目分析了重定位的过程,并借助edb调试工具,研究了程序中各个子程序的执行流程,最后则借助edb调试工具通过对虚拟内存的查取,分析研究了动态链接的过程。

第6章 hello进程管理

6.1 进程的概念与作用

6.1.1进程的概念

进程是操作系统中程序的一次执行过程,也是系统进行资源分配和调度的基本单位。每个进程拥有独立的地址空间、代码、数据以及其他资源,能够独立运行,并与其他进程并发执行。每个进程由操作系统分配唯一的进程标识符(PID)。

进程的经典定义是一个执行程序中的实例,每次用户通过向shell输人一个可执行目标文件的名字,运行程序时,shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。

每个进程都有一个独立的地址空间,并由操作系统通过进程控制块(PCB)来管理。PCB 内部存储了进程的状态(就绪、运行、阻塞等)、优先级、资源分配情况以及进程标识符(PID)等信息。换句话说,进程是操作系统进行资源分配和调度的基本单位,是应用程序执行时动态产生的实体。

6.1.2进程的作用

在运行一个进程时,我们的这个程序看似是系统当中唯一一个运行的程序,进程的作用就是提供给程序两个关键的抽象:1.独立的逻辑控制流,即程序独占使用处理器的假象。2.私有的地址空间,即程序独占使用内存系统的假象。

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

Shell是一种命令解释器,它充当用户与操作系统之间的桥梁。用户可以通过它输入命令,Shell接受这些命令、解析并传递给操作系统内核来执行。除了提供一个交互式命令行界面外,Shell还支持编写脚本,从而使自动化任务变得更加简单。

Shell应用程序提供了一个界面,用户通过访问这个界面访问操作系统内核的服务,例如:

命令解释:Shell接收用户输入的命令,解析其内容,并执行相应的操作。例如,输入ls或dir(取决于操作系统)可以列出目录内容。

脚本执行:Shell支持运行脚本文件,用户可以将一系列命令写入脚本,自动化执行重复性任务。

环境管理:Shell负责管理用户环境,包括设置和修改环境变量(如PATH)、工作路径等,以确保命令和程序能正确运行。

文件操作:Shell提供多种命令用于操作文件和目录,例如创建(mkdir)、删除(rm)、移动(mv)等。

进程控制:Shell可以启动、停止和管理进程,例如通过&将任务放入后台运行,或用kill终止进程。

处理流程:

1.显示提示符,接收输入:启动后,Shell显示一个提示符等待用户输入。用户键入命令后,Shell将读取这一行或多行输入内容。

2.词法分析与语法解析:Shell将输入的命令进行词法分析,把命令行拆分成一个个独立的词(tokens),如命令名、参数、重定向符号等。接着进行语法解析,检测命令结构是否正确,同时执行变量替换、通配符展开、命令替换等扩展操作。

3.命令识别与查找:Shell根据解析结果判断命令是否为内置命令。如果是内置命令,则在当前进程中直接执行;若为外部命令,Shell会在预定义的目录(通常由环境变量PATH决定)中查找相应的可执行文件。

4.创建子进程,执行命令:对于外部命令,Shell通常会调用fork()来创建一个子进程。子进程随后使用exec()系列函数将当前进程映像替换为目标程序。通过这种方式,实现了命令的隔离执行。与此同时,如果命令需要在后台运行,Shell则不会等待子进程结束,而会直接返回提示符供用户执行其他操作。

5.输入/输出与管道设置:在执行前,Shell根据用户的重定向符号设置命令的输入输出流。比如重定向“>”将输出定向到指定文件,或者使用管道符“|”连接多个命令,形成数据流的链式处理。

6.等待与返回状态:若命令在前台执行,Shell会调用等待机制来挂起自身直到子进程执行完毕,并收集其返回值。这个返回码常用来判断命令是否顺利完成。如果命令执行失败,Shell会将错误信息显示给用户。

7.重新循环,退出:完成当前命令的执行后,Shell再次显示新的提示符,等待下一轮输入。当用户输入退出命令(如exit)或关闭终端后,Shell进程终止自身。

6.3 Hello的fork进程创建过程

首先用户在shel1界面输入指令,Shell判断该指令是否为内置命令,若不是,则父进程调用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才会返回到调用程序。execve函数启动加载器,映射虚拟内存,进入程序入口后,程序开始载入物理内存,然后进入main函数。main 函数运行时,用户栈的结构如下图所示。

图6.4  main函数运行时用户栈图示

栈底的是参数和环境字符串,往上是null结尾的指针数组,指向的是环境变量字符串,全局变量environ指向这些指针中的第一个envp[0]。环境变量指针数组后的是argv数组,指向的是参数字符串。栈的顶部是系统启动函数libc_start_main的栈帧,之后便是为main函数分配的栈帧了。

6.5 Hello的进程执行

hello程序在运行时,进程提供给应用程序的抽象有:(1)一个独立的逻辑控制 流,好像我们的进程独占地使用处理器;(2)一个私有的地址空间,好像我们的程序独占地使用 CPU 内存。

从Shell执行hello程序时,会先处于用户模式运行。在运行过程中,由内核不断进行上下文切换,配合调度器,与其他进程交替运行。如果在运行过程中收到了信号,那么就会陷入到内核中进入内核模式运行信号处理程序,之后再进行返回。

以下是对进程执行中相关概念的介绍。

6.5.1上下文信息:

上下文是内核重启一个被抢占的进程所需的状态。由通用寄存器,浮点寄存器,程序计数器,用户栈,状态寄存器,内核栈和各种内核数据结构组成。其作用是能让逻辑控制流中被暂停的进程能够重新读取之前的状态,从而好像一直都在运行。

6.5.2进程时间片:

进程时间片操作系统分配给每个进程在CPU上的处理时间。由于我们知道单核CPU某一时刻只能执行一个进程,所以想要多个进程同时进行就需要将每个进程划分为多个时间片,每次只执行某个时间片,且中间挂起的时间很短,这样就能近似地处理为所有进程同时运行的一个状态。

6.5.3进程调度:

上面我们知道多个进程同时进行时是需要进程调度的,如某些程序挂起,某些进程开始执行。

6.5.4用户态和内核态:

用户态顾名思义就是用户能够进行操作的系统状态,内核态指的就是由计算机系统内核进行操作的一个状态。hello程序最开始运行是在用户态下,此时用户可以通过键盘向程序发送命令,一般来说,只有在异常处理和信号处理的时候才会切换到内核态,如异常处理子程序和上下文切换。

6.6 hello的异常与信号处理

6.6.1异常

异常可以分为四类:中断、陷阱、故障和终止,各类异常产生原因和一些行为总结如下表所示

表6.6.1  各类异常相关介绍

类别

原因

异步/同步

返回行为

中断

来自I/O设备的信号

异步

总是返回到下一条指令

陷阱

有意的异常

同步

总是返回到下一条指令

故障

潜在可恢复的错误

同步

可能返回到当前指令

终止

不可恢复的错误

同步

不会返回

6.6.2信号

信号是一种高层的软件形式的异常,允许进程和内核中断其他进程。信号可以被理解为一条小消息,他通知进程系统中发生了一个某种类型的事件。每种信号类型都对应于某种系统事件,它提供了一种机制,通知用户进程发生了这些异常。

6.6.3hello执行中的异常与信号

1.正常运行

图6.6.3.1  hello正常运行截图

2.运行时按回车

共输入九次回车,最后一次回车被getchar()读入作为程序结束标志,前八次则会输出出来,程序结束后产生八个换行符,如下图所示。

图6.6.3.2  hello运行时按回车结果

3.运行时随便乱按

不影响程序正常输出,但是若按出来回车,则这一行按出的字符会在程序结束后进入命令行作为下一条执行的命令,如下图所示。

图6.6.3.3  hello运行时随便按的结果

4.运行时按Ctrl+C

按下Ctrl + C,Shell进程收到SIGINT信号,Shell结束并回收hello进程,如下图所示。

图6.6.3.4  hello运行中按Ctrl + C结果

5.运行时按Ctrl+Z

按下Ctrl + Z,Shell进程收到SIGSTP信号,Shell显示屏幕提示信息并挂起hello进程,如下图所示。

图6.6.3.5  hello运行中按Ctrl + Z结果

6.Ctrl+Z后ps命令

对hello进程挂起后由ps命令查看,发现hello进程确实被挂起而非被回收,如下图所示。

图6.6.3.5  Ctrl + Z挂起后ps命令执行结果

7.Ctrl+Z后jobs命令

对hello进程挂起后在Shell中输入jobs,显示后台挂起作业hello,其job代号为1,如下图所示。

图6.6.3.7  Ctrl + Z挂起后jobs命令执行结果

8.Ctrl+Z后pstree命令

对hello进程挂起后在Shell中输入pstree命令,可以将所有进程以树状图显示,如下图所示。

图6.6.3.8  Ctrl + Z挂起后pstree命令执行结果

9.Ctrl+Z后fg命令

对hello进程挂起后,输入fg可以让挂起的作业继续执行,且是从上一次挂起处继续执行(挂起前输出了三次,fg后输出了七次,加起来正好共十次),如下图所示。

图6.6.3.9  Ctrl + Z挂起后fg命令执行结果

10.Ctrl+Z后kill命令

对hello进程挂起后,发送SIGKILL信号,杀死hello进程,如下图所示。杀死后再运行fg显示进程已被杀死,验证了进程被kill终止。

图6.6.3.10  Ctrl + Z挂起后fg命令执行结果

6.7本章小结

本章首先介绍了进程的概念及其作用,然后结合fork和execve函数,概述了shell创建hello子进程的过程。随后分析了hello的进程调度,并对执行过程中的异常和信号处理进行了简单介绍。最后结合异常和信号的概念,探究了hello运行时不同形式的异常和命令,每种信号都有不同处理机制,针对不同的shell命令,hello会产生不同响应。通过对hello进程异常与信号处理的分析,我对计算机系统的进程管理有了更加深刻的认识。

第7章 hello的存储管理

7.1 hello的存储器地址空间

hello程序经过编译链接后最后以二进制数据文件的格式存储在计算机的硬盘里(也可能是SSB或者ROM),当系统运行hello时才需要从磁盘中读出,并且将对应数据映射到内存里。

映射到内存的程序的所有数据在内存中都占一块空间,为了方便系统查找调用,我们给每个空间都赋予了一个独立的地址。

7.1.1逻辑地址

在有地址变换功能的计算机中,访问指令给出的地址(操作数)叫逻辑地址, 也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的物理地址。逻 辑地址是由一个段标识符加上一个指定段内相对地址的偏移量,由程序hello产生 的与段相关的偏移地址部分。

7.1.2线性地址

线性地址是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻 辑地址,在分段部件中逻辑地址是段中的偏移地址,加上基地址就是线性地址。

7.1.3虚拟地址

虚拟地址是在程序执行时由CPU生成的地址。虚拟地址空间是一个抽象的地址范围,程序认为自己独占整个地址空间,而实际上是与其他程序共享的。CPU启动保护模式后,程序运行在虚拟地址空间中。保护模式下,hello运行在虚拟地址空间中,它访问存储器所用的逻辑地址。

7.1.4物理地址

物理地址是实际的内存硬件地址,虚拟地址通过MMU的页表映射到物理地址,是CPU最终访问的实际内存位置。在存储器里以字节为单位存储信息,每一个字节单元给一个唯一的存储器地 址,这个地址称为物理地址,是hello的实际地址或绝对地址。

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

在Intel平台下,逻辑地址是selector:offset这种形式,selector是CS寄存器的值,offset是EIP寄存器的值。

CS寄存器(代码段寄存器)存储了当前执行的指令所在的代码段的起始地址。它是一个16位寄存器,指示了代码在内存中的位置。CS寄存器的值与代码段的段基址相关,形成了代码段的起始物理地址。

EIP寄存器用来存储CPU要读取指令的地址,CPU通过EIP寄存器读取即将要执行的指令。每次CPU执行完相应的汇编指令之后,EIP寄存器的值就会增加。

如果用selector去全局描述符表里得到段基址然后加上段内偏移,这就得到了线性地址。这个过程就称作段式内存管理。

逻辑地址由段标识符和段内偏移量组成。段标识符是一个16位长的字段(段选择符),可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。

全局的段描述符放在全局描述符表中,一些局部的段描述符放在局部段描述符表中。给定一个完整的逻辑地址段选择符+段内偏移地址,看段选择符的T1=0还是1,知道当前要转换是全局描述符表中的段,还是局部段描述符表中的段,再根据相应寄存器,得到其地址和大小。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,就得到了其基地址。再由基地址加上偏移量的值,便得到了线性地址。

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

虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的 数组。VM系统将虚拟内存分割,称为虚拟页,类似地,物理内存也被分割成物理 页。利用页表来管理虚拟页,页表就是一个页表条目(PTE)的数组,每个PTE由一 个有效位和一个位地址字段组成,有效位表明了该虚拟页当前是否被缓存在DRAM中,如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起 始位置,如果发生缺页,则从磁盘读取。MMU利用页表来实现从虚拟地址到物理地址的翻译。

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

Core i7采用四级页表的层次结构,其地址翻译概况如下图所示。

图7.4  Core i7地址翻译概况图

工作时,CPU产生虚拟地址VA传送给MMU,MMU使用VPN的高位作为TLBT和TLBI,从TLB中寻找匹配。如果TLB命中,则得到物理地址PA。

如果TLB中不命中,则MMU查询页表,CR3指向第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE指向的二级页表,再通过VPN2确定在二级页表中的偏移量,查询出PTE指向的三级页表,后续以此类推,最终在第四级页表中找到PPN,与VPO组合成物理地址PA。

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

高速缓存的结构将m个地址位划分成了t位CT(标记位)、s位CI(组索引) 和b位CO(块偏移),根据CI寻找到正确的组,根据CO找到正确的块内位置,依次与每一行的数据比较,有效位有效且标记位一致则命中。如果命中,直接返回想要的数据。如果不命中,就依次去L2、L3、主存判断是否命中,命中时将数据传给CPU同时更新各级cache的储存。

7.6 hello进程fork时的内存映射

当 fork 函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给 它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_ struct、区域结构和页表的原样副本。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任何一个。后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。 加载并运行hello需要以下几个步骤:

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

2.映射私有区域。为新程序的代码、数据、.bss和栈区域创建新的区域结构, 所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,.bss区域是请求二进制零的,映射到匿名文件,其大小包含在 hello中,栈和堆地址也是请求二进制零的,初始长度为零。

3.映射共享区域。hello程序与共享对象链接,这些对象动态链接到这个程序中,然后再映射到用户虚拟地址空间中的共享区域内。

4.设置程序计数器。execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。

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

假设MMU在试图翻译某个虚拟地址A时,触发了一个缺页。这个异常导致控制转移到内核的缺页处理程序,处理程序随后就执行下面的步骤:

1.判断虚拟地址A是否合法。缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start 和vm_end做比较。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止这个进程。

因为一个进程可以创建任意数量的新虚拟内存区域,所以顺序搜索区域结构的链表花销可能会很大。因此在实际中,Linux使用某些我们没有显示出来的字段,Linux在链表中构建了一棵树,并在这棵树上进行查找。

2.判断试图进行的内存访问是否合法。即判断进程是否有读、写或者执行这个区域内页面的权限。例如,这个缺页可能由一条试图对这个代码段里的只读页面进行写操作的存储指令造成;可能因为一个运行在用户模式中的进程试图从内核虚拟内存中读取字造成。如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。

3.此刻,内核知道了这个缺页是由于对合法的虚拟地址进行合法的操作造成的。它这样来处理这个缺页:选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MIMU。这次,MMU就能正常地翻译A而不会再产生缺页中断了。

7.9动态存储分配管理

Printf会调用malloc,请简述动态内存管理的基本方法与策略。(此节课堂没有讲授,选做,不算分)

7.10本章小结

本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理, 在指定环境下介绍了虚拟地址VA到物理地址PA的转换、物理内存访问,分析了 hello进程fork时的内存映射、hello进程、execve时的内存映射、缺页故障与缺页中断处理等,对hello的存储管理有了更为深入的理解。

结论

1. hello所经历的过程

Hello最初由程序员将源代码从键盘输入,依次经过以下步骤:

1.预处理(cpp):对hello.c进行预处理,预处理器将文件调用的所有外部库文件合并展 开,生成hello.i文件。

2.编译(ccl):编译器将预处理后的文件hello.i翻译为汇编语言文件hello.s。

3.汇编(as):将汇编语言翻译为机器语言,并生成可重定位目标文件hello.o。

4.链接(ld):将hello.o可重定位目标文件和动态链接库链接起来,生成可执行目标文件hello。

5.加载运行:在shel1中输入./hello 2023112099 尚煊 15050216281 1。终端判断输入的指令不是shell内置指令,于是调用fork函数创建一个新的子进程。再调用execve函数,启动加载器,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数。

6.执行指令:CPU为进程分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流。

7.访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址,然后访问实际内存。

8.信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号 给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并 将前台作业停止挂起。

9.终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。

2.感悟

计算机系统的设计与实现不仅仅是一门工程技术,更是一种兼具艺术与科学的深刻实践。在学习这门课的过程中,我深切感受到,真正出色的系统设计需要在简洁性、扩展性与可维护性之间找到微妙平衡,同时在追求高性能与安全性时不断探索新的可能性。

优秀的系统设计离不开模块化和分层抽象。通过将系统分解为独立、可复用的模块,并设计清晰的接口和职责,可以显著降低复杂性,提高系统的可维护性和可扩展性。良好的抽象层次还能为未来的优化和扩展奠定基础。性能优化既是技术挑战,也是艺术追求,需要在高性能与可维护性之间找到平衡。在信息时代,安全与隐私是系统设计不可或缺的部分,从设计初期就应融入最小权限原则、数据加密和访问控制等措施,确保系统具备抵御威胁的韧性。

最后,优秀系统不仅要实现功能,还要关注用户体验。响应速度、易用性和界面设计直接影响用户满意度,因此设计时需从用户视角出发,追求简洁与高效。

附件

所用文件及其作用见下表。

附件表

文件名

功能

Hello.c

源代码

hello.i

预处理后得到的文本文件

hello.s

编译后得到的汇编语言文件

hello.o

汇编后得到的可重定位目标文件

hello

可执行文件

hello.elf

hello.o的ELF文件格式

hello1.elf

hello的ELF文件格式

hello.asm

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

hello1.asm

反汇编hello可执行文件得到的反汇编文件

参考文献

  1. Randal E.Bryant David R.O'Hallaron. 深入理解计算机系统(第三版).机械工业出版社,2016.
  2. https://blog.csdn.net/HongHua_bai/article/details/122288032
  3. https://blog.csdn.net/wangguchao/article/details/109002488
  4. https://www.cnblogs.com/lh03061238/p/17703253.html
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值