CSAPP大作业

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业        信息安全        

学     号        2022110550     

班     级        2203202        

学       生        王宇航         

指 导 教 师        史先俊          

计算机科学与技术学院

2024年5月

摘  要

本论文旨在详细分析Hello程序从源代码到可执行文件的全过程,包括预处理、编译、汇编和链接等环节。通过在Ubuntu环境下进行一系列操作,逐步展示每个阶段的具体步骤和输出结果。采用实验方法,利用Visual Studio 2022和gcc等工具,对每个生成的中间文件进行解析和对比,深入探讨其内在机制。本研究的成果不仅提供了对C语言程序编译流程的全面理解,还揭示了操作系统在进程管理、内存管理和输入输出管理中的关键作用。理论与实践相结合的研究方法证明了系统学习编译原理和操作系统底层机制的重要性,对提高编程能力和系统优化具有实际意义。

关键词:Hello程序;预处理;编译;汇编;链接;进程管理;内存管理;

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

目  录

第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简介

P2PFrom Program to Process):

Hello.c到可执行文件的过程如图所示:

其过程分为预处理(生成hello.i)、编译(生成hello.s)、汇编(生成hello.o)、链接(生成最终的hello可执行文件)。

020From Zero-0 to Zero-0):

在程序开始执行前,操作系统尚未加载程序的代码和数据到内存中。shell调用fork()函数新建一个进程,通过execve函数来加载并运行hello,建立虚拟内存到物理内存的映射,加载完后执行hello中的代码。程序结束后由父进程对子进程进行回收,内核清空该进程的信息,进程结束。

1.2 环境与工具

硬件环境:

      处理器:12th Gen Intel(R) Core(TM) i7-12700H   2.70 GHz

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

软件环境:

      Windows 11 64位,VMware Workstation Pro17,Ubuntu 20.04

开发与调试工具:Visual Studio 2022,objump edb gcc等工具

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

hello.c

源程序

hello.i

经预处理后得到的文件

hello.s

编译后得到的汇编文件

hello.o

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

hello.elf

用readelf读取hello.o得到的ELF格式信息

hello.asm

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

hello1.asm

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

hello

可执行文件

1.4 本章小结

       简要描述了hello程序的P2P和020的过程,介绍了进行实验的环境,说明了对实验中产生的中间结果文件及其作用。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:

在C语言的程序中可包括各种以符号#开头的编译指令,这些指令称为预处理命令,预处理命令属于C语言编译器。

预处理的作用:

为编译器准备源代码,使其更加适合编译器的需要。预处理是编译过程的一个独立阶段,通常在编译器开始解析源代码之前完成。通过预处理,程序员可以编写更加灵活和可移植的代码,同时减少编译时的错误和警告。

2.2在Ubuntu下预处理的命令

预处理命令:gcc hello.c -E -o hello.i

2.3 Hello的预处理结果解析

原程序只有二十多行,但是经预处理后被扩展到了三千多行。源程序在预处理后并没有变化,但是前面增加了头文件信息。

前面填充的信息来自c头文件中引用的stdio.h,unistd.h,stdlib.h。在预处理后对其进行了展开,而在这三个头文件中也引用了其他头文件,递归展开后使得.i文件含有如此多的内容。

2.4 本章小结

本章介绍了预处理的概念及作用。展示了在Linux环境下进行预处理操作的过程。具体分析了预处理操作后的文件内容。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译的概念:

编译是编程语言编译流程中的核心环节,它涉及将预处理后的高级语言代码转换成低级的汇编语言程序。在这个过程中,编译器执行语法和语义分析,确保代码的正确性,同时进行优化以提高程序的执行效率。

编译的作用:

将人类可读的高级语言代码转换成机器或操作系统能够理解的低级代码,为后续的汇编和链接阶段做准备。这一过程对于将源代码转化为可执行程序至关重要,它不仅确保了代码的正确性,还通过优化提高了程序的性能。

3.2 在Ubuntu下编译的命令

编译的命令:gcc hello.i -S -o hello.s

3.3 Hello的编译结果解析

3.3.1 初始部分

      .file 声明源文件

      .text 代码节

      .section 分段

      .rodata 只读代码段

      .align 地址对齐方式

      .string 声明字符串

      .globl 声明全局变量

      .type 声明符号类型(数据或函数)

3.3.2 数据与操作

      字符串常量:

      两个printf字符串中的内容,存放在了只读数据段中。

      起始地址:

      if中的判断条件5:

      for循环中i的限制值10:

jle是小于等于9跳转,等同于小于10跳转。

变量:

全局变量:

      

       变量名称为main,类型是函数

局部变量:

       main函数中定义的变量i

      

      

       i被存放在了-4(%rbp)位置的栈上,赋值为0

参数:

      

      

       argc作为第一个参数储存在寄存器%edi中,argv作为第二个参数储存在%rsi中

算数操作:

      

       for循环中对i进行++的操作

关系操作:

      

      

       对argc进行判断,若等于5则跳转

      

      

       for循环后对i进行判断,若小于等于9则继续循环

控制转移:

      

       判断argc是否为5,若是,则跳过if语句中的内容执行后续指令,若否,则执行if语句

      

       与上述指令相似

      

       对i初始化,无条件跳转到.L3段,即for循环段

函数操作:

       main函数:

              参数传递:

             

              函数调用:使用call命令进行函数调用,并将main函数地址写入栈中

              局部变量:在函数中定义了i变量用以for循环

              函数返回:返回0

       printf函数:

              参数传递:

              函数调用:在main函数中被调用

              第一次:

              第二次:

              无局部变量及返回值

       exit函数:

              参数传递与函数调用:

             

       sleep函数:

              参数传递与函数调用:

             

       atoi函数:

              参数传递与函数调用:

             

       getchar函数:

              可以直接调用,无参数传递

             

3.4 本章小结

本章介绍了编译器进行编译的作用及概念,展示了在Linux系统下进行编译的过程。对编译产生的结果文件中内容进行了深入分析。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

       汇编的概念:

汇编是一种编程语言翻译过程,它将使用助记符表示的汇编语言代码转换为机器可执行的二进制指令。这个过程由汇编器完成,它解析汇编语言源文件(通常是.s文件),生成目标代码(通常是.o文件),这些目标代码随后可以被链接器进一步处理,生成最终的可执行程序。

       汇编的作用:

汇编语言的作用在于它提供了一种方式,让程序员能够接近硬件层面进行编程,从而实现对计算机硬件的精确控制。这在需要优化程序性能、开发系统级软件、编写操作系统和驱动程序等场景中尤为重要。

4.2 在Ubuntu下汇编的命令

汇编的命令:gcc -c hello.s -o hello.o

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

首先输入readelf -a hello.o > hello.elf指令得到hello.o文件的ELF格式

对ELF格式进行分析:

  1. ELF头:

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

  1. 节头:

描述每个节的节名、在文件中的偏移、大小、访问属性、对齐方式等

  1. 重定位节:

ELF文件中的重定位节是链接器用来在程序加载或链接时调整代码和数据地址的关键部分。这些节包含了必要的信息,以确保程序中的符号引用能够正确解析为内存地址。每个重定位条目都包含了偏移量、符号索引和重定位类型等信息,这些信息指导链接器如何进行地址的转换和修正。通过分析这些重定位节,可以深入了解程序的链接过程,帮助进行调试、性能优化和理解程序的内部机制。

  1. 符号表:

ELF文件中的符号表包含了程序中定义和引用的所有符号的列表,如全局变量名、函数名和常量等。每个符号条目通常包含符号的名称、值(或地址)、大小、类型、绑定属性(如全局或局部)和可见性等信息。

4.4 Hello.o的结果解析

以下格式自行编排,编辑时删除

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

输入指令:

得到hello.o的反汇编代码:

hello.s的代码:

机器语言:

      反汇编代码中增加了十六进制机器语言的表示

操作数进制关系:

      反汇编代码中的操作数都为十六进制,而hello.s中都为十进制

分支转移:

      反汇编代码中跳转位置为主函数加段内偏移量,而hello.s中为段名称

函数调用:

      反汇编代码中的call指令后,与上相同,不再是函数名称,而hello.s中为

函数名称

4.5 本章小结

本章介绍了汇编的功能与含义,展示了在Linux系统中如何对hello.s进行汇编成为hello.o文件及生成hello.elf。详细分析了ELF文件的每一个段部分。在对比中发现hello.o与反汇编代码的区别,从而更好地理解机器语言。

(第41分)

第5章 链接

5.1 链接的概念与作用

链接的概念:

链接是编译过程的最后阶段,它涉及将一个或多个编译后的中间目标文件(如hello.o)组合成一个单一的可执行文件(如hello)。链接器负责符号解析,地址分配,库整合,重定位,以及最终生成可执行文件。

链接的作用:

链接确保了程序中的所有符号引用都能正确解析到它们的定义,为代码和数据分配了内存地址,整合了所需的库,执行了必要的重定位,最终生成了一个可以被操作系统加载和执行的完整程序。

注意:这儿的链接是指从 hello.o 到hello生成过程。

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

使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件

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

       命令:readelf -a hello > hello1.elf

  1. ELF头

与hello.elf中的ELF头包含信息基本相同

  1. 节头

描述了各个节的大小、偏移量及其他属性。链接时,链接器会将文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址

  1. 程序头

程序头部分是一个结构数组,描述了系统准备程序执行所需的段或其他信息。

  1. 段节

  1. 重定位节

5.4 hello的虚拟地址空间

   使用edb加载hello,查看本进程的虚拟地址空间各段信息

程序的地址是从0x401000开始的,根据节头部表,我们可以找到各段的信息。

如:

5.5 链接的重定位过程分析

使用objdump -d -r hello.o > hello.asm和objdump -d -r hello > hello1.asm指令分别为hello.o与hello生成反汇编代码

不同点:

       比较两段代码发现hello1.asm内容明显多于hello.asm。在hello1.asm中加入了.plt、puts@plt、printf@plt、getchar@plt、atoi@plt、exit@plt、sleep@plt等函数。

重定位:

  1. 重定位节和符号定义。在这一步中,链接器将所有相同类型的节聚合为同一类型的新的节。然后将符号从它们的在.o文件的相对位置重新定位到可执行文件的最终绝对内存位置。连接器将运行时内存地址赋给新的聚合节,输入模块定义的每个节,以及输入模块定义的每个符号。
  2. 重定位节中的符号引用。链接器依赖于hello.o中的重定位条目,修改代码节和数据节中对每个符号的引用,使得它们指向正确运行时的地址。

5.6 hello的执行流程

在edb中对程序一步步调试,同时查看Symbols表

       开始执行时:start 0x4010f0 _libc_start_main

       执行时:main 0x401125 _printf 0x401040 _getchar 0x401050 _sleep 0x401080

       终止:exit 0x401070

使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。

5.7 Hello的动态链接分析

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。延迟绑定是通过GOT和PLT实现的,根据hello1.elf文件,GOT起始表位置为:0x404000:

       调用dl_init之前

       调用了dl——init之后:

       对于库函数而言,需要plt、got合作。plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次再调用plt时,指向的就是正确的内存地址。接下来执行程序的过程中,就可以使用过程链接表plt和全局偏移量表got进行动态链接。

5.8 本章小结

本章介绍了链接的概念与作用,展示了怎样在Linux下进行链接,详细讲述了ELF格式下的内容,利用edb工具使文件虚拟地址空间更加清晰明了。最后以hello程序为例对重定位过程、执行过程和动态链接进行分析。

(第51分)

第6章 hello进程管理

6.1 进程的概念与作用

       进程的概念:

进程是操作系统分配资源和调度执行的基本单位,它代表了程序在计算机上的一次动态执行实例。每个进程拥有独立的内存空间,可以包含一个或多个线程,线程共享进程的资源。进程通过系统调用与操作系统交互,实现程序的并发执行。

       进程的作用:

进程的作用在于为程序提供隔离的执行环境,确保系统的稳定性和安全性。它允许多个程序同时运行而互不干扰,通过操作系统的调度管理,实现了资源的合理分配和任务的并发处理。进程的创建、执行和终止是操作系统管理的核心功能之一,它使得计算机能够高效地处理复杂任务。

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

       作用:

Shell-bash,即Bourne Again Shell,是UNIX和Linux系统中的主要命令行解释器,它为用户提供了一个强大的交互式界面,使得用户能够执行命令、管理文件、控制程序执行,并编写自动化脚本,从而提高操作效率和系统管理的灵活性。

       处理流程:

当用户在bash Shell中输入命令时,bash首先解析命令和参数,然后在系统路径中搜索对应的可执行文件,执行该命令并处理任何输入输出重定向,等待命令执行完成后,bash会捕获命令的退出状态并将其反馈给用户,随后继续等待新的命令输入,形成一个循环的处理流程。

6.3 Hello的fork进程创建过程

       fork()通过复制父进程的资源来生成一个几乎完全相同的子进程,子进程拥有独立的PID,并且fork()在父进程中返回子进程的PID,在子进程中返回0,从而允许父进程和子进程执行不同的代码或继续相同的任务,但具有不同的行为和状态。

6.4 Hello的execve过程

int execve(char *filename, char *argv[], char *envp[])

在当前进程中载入并运行一个新程序:

filename:可执行文件

目标文件或脚本(用#!指明解释器,如 #!/bin/bash)

argv:参数列表,惯例:argv[0]==filename

envp:环境变量列表

"name=value" strings (e.g., USER=droh)

getenv, putenv, printenv

覆盖当前进程的代码、数据、栈

保留:有相同的PID,继承已打开的文件描述符和信号上下文

Called once and never returns(调用一次并从不返回)

6.5 Hello的进程执行

进程创建:首先,通过fork()系统调用创建一个新的进程。fork()复制父进程的地址空间和资源到子进程,子进程获得一个新的进程标识符(PID)。

用户态与核心态:操作系统通常有两个执行级别,用户态和核心态。用户态是普通应用程序运行的状态,核心态是操作系统内核运行的状态,具有对硬件和系统资源的完全控制权。

进程上下文:每个进程都有自己的上下文,包括程序计数器(指示下一条指令的位置)、寄存器集合、堆栈、内存管理信息等。进程上下文切换是操作系统在不同进程间切换时保存和加载这些信息的过程。

进程时间片:现代操作系统通常采用时间片轮转调度算法,每个进程被分配一个时间片,在这个时间片内,进程可以执行。时间片用完后,操作系统会调度另一个进程运行。

进程调度:操作系统的调度器负责决定哪个进程获得CPU时间。调度器会根据进程的优先级、状态(如就绪、运行、阻塞)和时间片等信息来选择下一个要执行的进程。

上下文切换:当调度器选择一个新的进程运行时,会发生上下文切换。当前进程的状态(包括寄存器和程序计数器等)被保存,新进程的状态被加载到CPU中。

用户态与核心态转换:进程在执行系统调用(如read(), write(), open()等)时,会从用户态切换到核心态。系统调用请求操作系统内核执行特定的服务,如访问文件系统或网络通信。完成后,控制权返回给用户态的进程。

进程终止:进程执行完毕后,会通过exit()系统调用或类似的机制来终止。操作系统回收其资源,并通知父进程进程已经终止。

进程间通信:在多进程环境中,进程可能需要相互通信或同步。操作系统提供了多种机制,如管道、消息队列、共享内存和信号量等,来支持进程间通信。

整个进程执行和调度过程是操作系统内核精心管理的结果,确保了系统的稳定性、效率和响应性。通过合理的调度策略和上下文管理,操作系统能够支持多任务处理,允许多个进程同时运行,尽管在任何给定时刻,只有一个进程在CPU上执行。

6.6 hello的异常与信号处理

       异常的种类:

处理方法:

       中断:

       陷阱:

故障:

       终止:

运行结果:

  1. 正常运行:

程序打印10次

  1. Ctrl+C

程序收到SIGINT信号,结束进程

  1. 不停乱按

在键盘中按下的字会被当作getchar()的输入

  1. Ctrl+Z

程序收到SIGSTP信号,暂时挂起进程

       运行ps查看

       运行jobs

Hello进程确实被挂起而没有被回收

       运行pstree,查看树状图的进程显示

       运行fg 1,进程被调到前台继续执行直至打印完成被回收

       运行kill,可以杀死进程

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

 程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

6.7本章小结

本章介绍了进程的概念与作用,简述了壳Shell-bash的作用与处理流程,详细分析了hello的fork进程创建过程、execve过程、进程执行过程。在结尾介绍了一些常见异常和其信号处理方法。

(第61分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

1.逻辑地址:

这是程序在其代码中直接使用的地址,逻辑地址是相对于程序的,它们不直接对应于物理内存中的地址。

2.线性地址:

线性地址是CPU在没有启用分页机制时使用的地址,它是虚拟地址空间中的一个直接映射,通常在简单的或旧的系统中使用。在现代操作系统中,线性地址通常通过内存管理单元(MMU)转换为物理地址。

3.虚拟地址:

虚拟地址是现代操作系统中使用的一种抽象,它允许每个进程拥有自己的地址空间,通过MMU转换成物理地址,这允许多个进程安全地运行在同一台机器上,而不会相互干扰。

4.物理地址:

物理地址是实际内存芯片上的地址,是CPU最终用来访问RAM的地址。

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

在Intel架构中,段式管理是一种内存管理技术,它通过段寄存器和段基址来实现逻辑地址到线性地址的转换。段式管理允许程序通过分段的方式来组织和访问内存,每个段可以独立地被访问和保护。以下是段式管理中逻辑地址到线性地址转换的基本步骤:

段选择:

在段式管理中,每个段都有一个段选择器(Segment Selector),它包含了段的索引和访问权限信息。

程序通过加载段寄存器(如CS - Code Segment,DS - Data Segment等)来选择相应的段。

段基址:

当一个段被选中后,操作系统会提供一个段基址(Base Address),这是段在物理内存中的起始地址。

偏移量:

逻辑地址由两部分组成:段内偏移量(Offset)和段选择器。

段内偏移量是程序代码中使用的相对地址,它指示了从段的起始位置开始的偏移量。

地址转换:

为了得到线性地址,CPU将段基址与段内偏移量相加。

线性地址 = 段基址 + 段内偏移量

段界限检查:

在访问内存之前,CPU会检查生成的线性地址是否超出了段的界限。

如果超出界限,CPU会触发一个段界限违规异常。

段属性检查:

CPU还会检查段属性,以确保访问权限和类型符合要求。

最终的线性地址:

如果一切检查都通过,那么生成的线性地址就是程序可以访问的内存地址。

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

在现代的Intel处理器中,页式管理是主要的内存管理技术。页式管理通过将虚拟地址空间划分为固定大小的页,并将其映射到物理内存中的帧来实现线性地址到物理地址的转换。以下是页式管理中线性地址到物理地址转换的基本步骤:

页目录:

每个进程都有自己的页目录(Page Directory),页目录包含了页表的入口。

页表:

页表(Page Table)包含了页(Page)的入口,页表进一步将线性地址映射到物理地址。

页目录项(PDE):

线性地址的一部分用于索引页目录项,以找到对应的页表。

页表项(PTE):

页目录项指向页表,线性地址的另一部分用于索引页表项,以找到对应的页。

页偏移:

线性地址的最低部分是页内偏移量,它指示了页内的具体位置。

地址转换:

CPU通过页目录项找到页表的物理地址。

然后通过页表项找到页的物理地址(页帧号)。

最后,将页帧号与页内偏移量相加,得到最终的物理地址。

页命中和页缺失:

如果页表项有效且页在物理内存中(页命中),CPU可以直接进行地址转换。

如果页不在物理内存中(页缺失),CPU会触发一个缺页异常,操作系统会将缺失的页加载到物理内存中,并更新页表。

转换过程如下:

页目录项地址 = 页目录基址 + 页目录项索引 * 页目录项大小

页表项地址 = 页目录项指向的页表基址 + 页表项索引 * 页表项大小

物理地址 = 页表项指向的页帧号 * 页大小 + 页内偏移

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

在现代Intel处理器中,虚拟地址(VA)到物理地址(PA)的转换通常涉及使用四级页表和转换后备缓冲器(TLB,也称为快表)。以下是在四级页表支持下,结合TLB,虚拟地址到物理地址转换的步骤:

四级页表结构:

随着虚拟地址空间的增大,Intel引入了四级页表结构,以减少页表本身所需的内存空间。

四级页表包括:全局页目录(PGD)、页目录(PD)、页表(PT)和页帧(PF)。

虚拟地址分割:

虚拟地址被分割为多个部分,用于索引每一级页表和作为页内偏移。

例如,在x86-64架构中,虚拟地址通常被分为:全局页目录索引、页目录索引、页表索引、页帧索引和页内偏移。

TLB查找:

当访问内存时,CPU首先会在TLB中查找虚拟地址对应的条目。

TLB是一种高速缓存,用于存储最近或频繁访问的页表项,以加快地址转换的速度。

TLB命中:

如果在TLB中找到了对应的条目(TLB命中),CPU可以直接从TLB获取物理地址,并完成内存访问。

TLB缺失:

如果TLB中没有找到对应的条目(TLB缺失),CPU将执行四级页表查找:

使用虚拟地址的全局页目录索引来访问全局页目录,获取页目录的物理地址。

使用页目录索引来访问页目录,获取页表的物理地址。

使用页表索引来访问页表,获取页帧的物理地址。

将页帧索引与页帧的物理地址结合,再加上页内偏移,得到完整的物理地址。

页表项检查:

在每次页表查找时,CPU会检查页表项的有效性和权限,确保访问是合法的。

更新TLB:

如果页表查找成功,CPU会将新的页表项添加到TLB中,以便将来的访问可以更快地完成。

缺页处理:

如果在页表中发现页帧不存在(即缺页),CPU将触发一个缺页异常,操作系统会处理这个异常,将缺失的页加载到物理内存中,并更新页表项。

最终的物理地址:

一旦获得了页帧的物理地址,CPU就可以将页帧索引与页内偏移相结合,得到最终的物理地址,完成内存访问。

四级页表和TLB的结合使用,使得虚拟地址到物理地址的转换更加高效,尤其是在处理大量内存访问时,可以显著减少CPU的等待时间和系统的整体延迟。

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

三级缓存(通常称为L1、L2和L3缓存)是现代处理器架构中用于提高内存访问速度的高速缓存层次结构。以下是三级缓存支持下物理内存访问的基本过程:

L1缓存(一级缓存):

L1缓存是最快的缓存,分为数据缓存(Data Cache)和指令缓存(Instruction Cache)。

当CPU执行指令或访问数据时,首先在L1缓存中查找。

L2缓存(二级缓存):

如果L1缓存未命中,CPU会在L2缓存中查找。

L2缓存通常比L1缓存大,但访问速度稍慢。

L3缓存(三级缓存):

如果L1和L2缓存都未命中,CPU会在L3缓存中查找。

L3缓存是最大的,可以为多个核心提供共享缓存,但访问速度比L1和L2缓存慢。

缓存一致性:

在多核心处理器中,缓存一致性协议(如MESI协议)确保所有核心中的缓存数据是一致的。

缓存命中:

如果在任何级别的缓存中找到了所需的数据(缓存命中),CPU可以直接使用缓存中的数据,这大大减少了访问延迟。

缓存缺失:

如果在所有级别的缓存中都未找到所需的数据(缓存缺失),CPU将从物理内存中加载数据。

物理内存访问:

当缓存缺失时,CPU通过内存控制器向物理内存发起访问请求。

内存控制器负责将物理地址转换为内存条上的地址,并从DRAM中读取数据。

数据加载:

从物理内存加载的数据首先被存储在缓存中,然后CPU才能访问它。

为了提高效率,通常一次会加载一个缓存行的数据,而不仅仅是单个数据项。

缓存替换策略:

当缓存满了之后,需要决定哪些数据被替换出缓存。常见的替换策略包括最近最少使用(LRU)等。

预取技术:

现代处理器还使用预取技术,根据访问模式预测未来可能需要的数据,并提前从物理内存加载到缓存中。

三级缓存的设计旨在减少CPU访问物理内存的次数,因为物理内存的访问速度远低于缓存。通过在多个级别的缓存中存储数据,处理器可以更快地访问数据,从而提高整体性能。在物理内存访问过程中,缓存的层次结构和替换策略对于系统性能至关重要。

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

       删除已存在的用户区域,创建新的区域结构,代码和初始化数据映射到.text和.data区(目标文件提供),.bss和栈映射到匿名文件

设置PC,指向代码区域的入口点,Linux根据需要换入代码和数据页面

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

第一步检查虚拟地址是否合法,如果不合法则触发一个段错误,终止这个进程。

第二步检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,程序终止。

第三步确认了是合法地址并且是符合权限的访问,内核选择一个牺牲页面,如果该页面被修改过则将其交换出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。

7.9动态存储分配管理

动态存储分配是程序在运行时请求和释放内存的过程。操作系统和运行时库提供了一系列的机制来管理这一过程。

栈分配:

局部变量通常在栈上分配,当函数被调用时,其局部变量会自动分配在调用栈上。当函数返回时,这些变量会被自动释放。

堆分配:

动态内存分配通常通过堆来进行。程序可以使用如malloc()、calloc()、realloc()和free()这样的函数来请求和释放堆内存。

malloc()用于分配指定大小的内存块,返回一个指向这块内存的指针。

calloc()用于分配并初始化一个数组,将所有字节初始化为0。

realloc()用于调整已分配内存块的大小。

free()用于释放之前分配的内存,使其回到可用内存池。

内存分配算法:

为了有效管理内存,操作系统和运行时库实现了多种内存分配算法,如:

首次适应算法:从头开始搜索,直到找到足够大的空闲内存。

最佳适应算法:选择最合适大小的空闲内存块,这有助于减少内存碎片。

最差适应算法:选择最大的空闲内存块,这有助于减少搜索时间。

内存池:

为了提高内存分配和释放的效率,可以使用内存池。内存池预先分配一大块内存,并管理其中的小块内存分配。

垃圾收集:

在一些高级语言中,如Java和C#,运行时环境提供了垃圾收集机制,自动回收不再使用的内存。

内存碎片管理:

长时间运行的程序可能会遇到内存碎片问题,即内存中存在许多小的、不连续的空闲区域。这可能影响内存分配的效率。一些内存分配器会尝试通过压缩或整理内存来减少碎片。

内存保护:

为了防止程序访问非法内存,操作系统提供了内存保护机制,如页表中的访问权限位。

内存越界检测:

一些开发环境提供了越界检测工具,帮助开发者发现数组越界和缓冲区溢出等错误。

使用自定义分配器:

在某些高性能或嵌入式系统中,开发者可能会实现自定义的内存分配器,以满足特定的性能或资源限制要求。

7.10本章小结

本章介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,TLB与四级页表支持下的VA到PA的变换、三级cache支持下物理内存访问。具体分析了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

(第7 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

一个 Linux 文件 就是一个 m 字节的序列:B0 , B1 , .... , Bk, .... , Bm-1

所有的I/O设备都被模型化为文件,I/O操作可看作对相应文件的读或写。

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

Unix操作系统中的I/O(输入/输出)接口是用于管理进程与文件、设备或其他进程之间的数据交换。Unix I/O模型基于文件描述符的概念,文件描述符是一个整数,用于标识一个打开的文件或其他类型的I/O通道。Unix I/O接口的一些关键函数及其功能:

open: 用于打开一个文件,并返回一个文件描述符。如果文件不存在且指定了创建标志,则会创建一个新文件。open函数的原型如下:

close: 关闭指定文件描述符的文件。关闭文件后,文件描述符将不再可用。

read: 从指定的文件描述符中读取数据。它将数据从文件读取到指定的缓冲区中,并返回实际读取的字节数。

write: 向指定的文件描述符写入数据。它将数据从指定的缓冲区写入文件,并返回实际写入的字节数。

lseek: 用于改变文件描述符的文件位置指针,可以移动到文件的任何位置进行读写操作。

dup 和 dup2: 用于复制文件描述符。dup创建文件描述符的一个副本,而dup2可以指定新文件描述符的编号。

8.3 printf的实现分析

       (1)vsprintf

      

       这个函数的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

       (2)write

             

给几个寄存器传递了几个参数,然后一个int结束,int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。

       (3)syscall

      

ecx中是要打印出的元素个数,ebx中的是要打印的buf字符数组中的第一个元素,不断的打印出字符,直到遇到:'\0'。

(4)最终:

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),实现printf的输出。

8.4 getchar的实现分析

       getchar代码大概如下:

       bb作为缓冲区的开始,在n为0的情况下从缓冲区读取BUFSIZ个字节。返回时如果n大于0,那么返回缓冲区第一个字符,否则EOF。

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章介绍了Linux的IO设备管理方法及IO接口与函数,分析了printf函数和getchar函数的具体实现。

(第81分)

结论

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

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

hello所经历的过程

  1. 预处理,hello.c经过预处理器cpp后,生成一份修改了的源程序文本hello.i。
  2. 编译,hello.i经过编译器ccl,翻译为一份汇编语言的文件hello.s。
  3. 汇编,hello.s经过汇编器as,成为一份可重定位目标文件hello.o。
  4. 链接,可重定位目标文件hello.o与动态链接库相结合,生成可执行文件hello。
  5. 在shell中输入指令./hello启动程序。
  6. 调用fork()函数创建一个子进程。
  7. 调用execve函数,启动加载器,通过mmap建立虚拟内存映射;设置当前进程的上下文中的程序计数器,使之指向程序入口处,进入main函数。
  8. hello输出信息时需要调用printf和getchar,他们的实现需要调用Unix I/O中的write和read函数,这又需要借助系统调用I/O。
  9. 当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。
  10. hello的旅程正式进入终点。

感悟:

通过对Hello程序编译和执行过程的深入研究,我们加强了对编译原理和操作系统机制的理解,原来一个小小的程序也有“波澜壮阔”的一生,这提醒我们在探索更深入的层次的路上也要时刻注意每一份细节的实现,规格严格,功夫到家,为技术进步和社会发展贡献真正的力量。

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

附件

列出所有的中间产物的文件名,并予以说明起作用。

hello.c

源程序

hello.i

经预处理后得到的文件

hello.s

编译后得到的汇编文件

hello.o

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

hello.elf

用readelf读取hello.o得到的ELF格式信息

hello.asm

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

hello1.asm

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

hello

可执行文件

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

参考文献

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

[1]  Randal E.Bryant David R.O'Hallaron.深入理解计算机系统(第三版).机械工业出版社,2016.

[2]  https://blog.csdn.net/twi_twi/article/details/124841679

[3]  https://blog.csdn.net/immortalist/article/details/118188486

[4]  https://www.cnblogs.com/Zhengsh123/p/15857501.html

[5]  https://github.com/HITLittleZheng/HITCS/tree/main/

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值