哈尔滨工业大学计算机系统大作业

计算机系统

大作业

题     目  程序人生-Hello’s P2P   

专       业        信息安全               

学     号        2022110620               

班     级        2203202              

学       生         任柏润            

指 导 教 师          史先俊             

计算机科学与技术学院

2024年5月

摘  要

Hello程序,作为编程新手的起点,其简单性掩盖了背后复杂的计算机系统流程。从编程的视角看,它仅是一个基础的输出指令;然而,从计算机系统的层面剖析,hello程序经历预处理、编译、汇编、链接等多个环节,才最终转变为可执行文件。当这个文件被加载到内存中并执行时,它实际上穿越了计算机系统设计的多个精妙层面。本文将从hello.c这个简单的程序出发,全面阐述它在Linux系统下的生命周期——从程序代码到运行进程的P2P旅程,以及从启动到结束的020过程。通过这一详尽的剖析,我们不仅能够深入理解hello程序的运行机制,更能洞悉一般程序在计算机系统中的生命历程,从而欣赏到计算机系统设计的卓越与匠心。

关键词:计算机系统,计算机体系结构,程序的生命历程                           

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

a

目  录

第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 本章小结............................................... - 9 -

第3章 编译.................................................. - 10-

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

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

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

3.4 本章小结............................................. - 15 -

第4章 汇编.................................................. -16 -

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

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

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

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

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

第5章 链接................................................. - 23 -

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

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

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

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

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

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

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

5.8 本章小结............................................. - 31 -

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

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

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

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

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

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

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

6.7本章小结.............................................. - 37 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结............................................. -42 -

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

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

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

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

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

8.5本章小结.............................................. - 47 -

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

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

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

第1章 概述

1.1 Hello简介

1.1.1 P2P

从程序到进程,分为预处理、编译、汇编、链接、执行五个阶段。其中预处理阶段是指源程序hello.c经过预处理器cpp的预处理,根据以#开头的命令(如#include <stdio.h>)修改原始的C程序,生成hello.i。编译阶段是指编译器将预处理后的文本文件hello.i翻译成汇编语言文本文件hello.s,该文件中每条语句以一种标准的文本格式确切地描述一条低级机器语言指令。汇编阶段是指汇编器将hello.s中的汇编语言指令翻译成机器语言指令。这些指令被打包成可重定位目标程序的格式,并将结果保存在目标文件hello.o中。链接阶段是指链接器负责将hello.o和程序中调用的其他目标文件(如printf.o)合并,得到一个可执行目标文件hello,这个文件可以被加载到内存中并由系统执行。执行阶段是指在Linux系统中,通过内置命令行解释器shell加载运行hello程序,shellhello程序创建一个新的子进程,并在该进程中执行程序。

1.1.2 020

020,即From zero to zero,是hello程序的生命周期。系统先为hello程序分配虚拟空间,并将程序从磁盘载入物理内存中执行。进入main函数执行目标代码。当hello程序执行完成后,它向父进程发送一个SIGCHLD信号,父进程回收hello进程,释放其占用的所有资源,并删除相关的进程上下文。hello程序从有到无,完成整个生命周期。

1.2 环境与工具

硬件环境:X64 CPU,2.60GHZ,32.0G RAM,950GHD Disk

软件环境:Windows11 64位,Vmware 17 Pro,ubuntu-22.04.4-desktop-amd64

开发工具:Visual studio 2022;Codeblocks 64位

调试工具:objdump,edb,gdb等

1.3 中间结果

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

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

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

hello.elf: hello.o的elf格式

hello:链接后得到的可执行目标文件

Hello.elf:hello的elf格式

Hello.asm: 反汇编hello得到的反汇编文件

1.4 本章小结

本章介绍了hello程序的P2P,020的流程,bing 解释了流程中每一步骤的操作原理,说明了本次论文的实验过程中运用的软硬件环境和开发工具、调试工具,以及产生的中间结果文件的名字和作用。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

2.1.1 概念

在编译源代码之前,对源代码进行的一系列操作或转换,这些操作包括头文件展开,去注释、宏替换和条件编译,目的是将#开头的命令(如#include <stdio.h>)转换为实际代码中的内容。

2.1.2 作用

头文件展开:将头文件的内容复制到代码文件中,以便进行函数,变量的调用。

去注释:编译器将注释全部替换为空格

宏替换:将程序所使用的宏定义在源代码出现的任何位置都进行替换

条件编译:对一部分内容指定编译条件。在预处理阶段,如果使用条件编译的代码块满足条件,则保留;如果不满足条件,则预处理器就会将代码裁剪,使其不会在后续阶段被编译。

2.2在Ubuntu下预处理的命令

在命令行中使用命令:gcc -E hello.c -o hello.i对hello.c进行预处理,得到预处理文件hello.i

2.3 Hello的预处理结果解析

hello.c:

hello.i的头尾部内容:

对比hello.i、hello.c发现,hello.i相比于hello.c,代码行数扩展到了3000多行,其中main函数被放在了文件的最后,而hello.c中文件头部的注释段被转换为了空行。而hello.i中剩余部分则为hello.c中头文件的具体内容的展开,这些头文件被预处理器在系统目录下查找,将找到的头文件如stdio.h的内容复制到hello.i中。

2.4 本章小结

本章介绍了预处理的概念和作用,并在linux环境下实现了对hello.c文件的预处理,并对预处理结果hello.i的内容进行了对比分析,得到结论:预处理会删除源文件中的注释,并且将引用头文件中的内容复制到源文件中。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

3.1.1 编译的概念

编译是将高级语言编写的源代码转换成汇编语言的过程,这个过程是由一编译器来完成的。        

3.1.2 编译的作用

编译过程将高级语言的代码转换为汇编语言,这种语言更加接近计算机可读取的机器语言,同时为不同的高级语言提供通用的输出语言,从而提高编程的效率和可移植性。编译的基本流程如下:

词法分析:将源代码分解为一系列的记号。

语法分析:根据语言的语法规则,将记号组合成语法结构(如表达式、语句和程序)。

语义分析:检查源代码的语义正确性,如类型检查、变量声明等。

中间代码生成:将语法结构转换成中间代码形式,以便于后续的优化和代码生成。

代码优化:对中间代码进行优化,以提高生成代码的质量和性能。

目标代码生成:将中间代码或优化后的代码转换成目标语言代码(如机器代码或汇编代码)。

3.2 在Ubuntu下编译的命令

在命令行中使用命令:gcc -S hello.i -o hello.s对hello.i进行编译,得到hello.s

3.3 Hello的编译结果解析

3.3.1 初始部分

本部分记录了文件的相关信息,用于指导汇编器和链接器的代码生成,其中.file声明出源文件,.text是代码段,.section   .rodata是只读数据段,.align              声明了指令或者数据的存放地址对齐的方式为8字节,.string声明了字符串,.globl声明全局变量,.type声明符号类型main是一个函数。

3.3.2 数据部分

1.常量

字符串常量:存储在只读数据段rodata节中

2.局部变量:在源程序中的int i

由该行可知,i的初值为0,存放在栈上-4(%rbp)的位置

将i和9进行比较,若i小于等于9,则跳转到.L4,.L4中的程序包括

addl $1,-4(%rbp),即对i进行+1操作,实现了源程序中的循环。

3.参数argc

main函数的第一个参数。

分析该行,结合源代码反推可知,argc被存放在栈中-20(%rbp)的位置。

由该行可知,argc被存放在%edi寄存器中,%edi中的argc被压入栈中-20(%rbp)的位置。

3.3.3 赋值操作

由movl指令实现对i的赋值,将i初始值赋值为0,即源程序的循环中的i=0。

3.3.4 算术操作

在源程序的for循环中,每次循环结束后对局部变量i+1,使用addl指令实现。

3.3.5 关系操作

在源程序中共有两处:判断参数argc是否等于5,以及循环中判断i是否小于10。

前者使用cmpl指令比较argv(存放在栈中-20(%rbp)的位置)和5,若不相等则执行后面的语句,否则跳转到.L2。

后者使用cmpl指令比较i和9的值,若小于等于9,则跳转到.L4(执行循环中的语句)。

3.3.6 数组操作

main函数的第二个参数,argv[]数组。

由图可知,数组首地址存放在-32(%rbp)的位置,其中数组中每个元素占8个字节,第一个元素存放在%rax中,第二个元素存放在%rdx中,第三个元素存放在%rsi中,第四个元素存放在%rdi中。

3.3.7 控制转移

与3.3.5中内容相似,在判断完argv与5是否相等后,若不相等,执行if语句,否则跳转到.L2。

在for循环中,比较i和9的大小,若i<=9,则跳转到.L4。

3.3.8 函数操作

1.main函数

参数传递:参数为int argc,,char*argv[],由之前的分析,argc被存放在%edi寄存器中,数组首地址存放在-32(%rbp)的位置,其中数组中每个元素占8个字节,第一个元素存放在%rax中,第二个元素存放在%rdx中,第三个元素存放在%rsi中,第四个元素存放在%rdi中。

函数调用:通过call指令调用puts,exit,printf,sleep、atoi、getchar函数

 

 

局部变量:i,存放在栈上-4(%rbp)的位置。

2.printf函数

参数传递:argv[1],argv[2],argv[3],具体内容已讲过。

函数调用:在main函数中被call指令调用。

3.exit、atoi、sleep函数

Exit函数:参数传入为1,存放在寄存器%edi中;在main函数中被调用。

Atoi函数:将参数argv[4]传入到%rdi中作为参数传递;在main函数中被调用。

Sleep函数:由atoi将argv[4]进行转化得到的操作数存放在%edi中,作为参数传递;在main函数中被调用。

3.4 本章小结

本章介绍了编译的概念和作用,并且将hello.i编译得到了hello.s,对hello.s文件进行了分析,分析内容包括初始部分、数据、赋值操作、算术操作、关系操作、数组操作、控制转移和函数操作,对比源代码和汇编代码,来分析这些操作在汇编中的实现方式。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

汇编是指汇编器将汇编语言程序hello.s翻译为机器语言指令,并将这些指令打包成可重定位目标程序的格式,将结果保存在目标文件hello.o文件中的过程。其作用是让机器语言形式的代码可以在计算机上运行。

4.2 在Ubuntu下汇编的命令

在命令行中使用命令:gcc -c hello.s -o hello.o对hello.s进行汇编,得到hello.o


4.3 可重定位目标elf格式

首先在命令行中输入readelf -a hello.o>hello.elf 指令获得hello.o文件的 ELF 格式:

打开elf格式的文件,发现如下内容:

  1. ELF头

头16个字节:描述了生成该文件的系统的字的大小和字节顺序,开头的4字节7f 45 4c 46分别对应删除ELF的ASCII码,操作系统在加载可执行文件时会确认该序列是否正确。

剩下部分包括ELF类别、版本、ELF头的大小、目标文件类型、机器类型、节头部表中条目的大小和数量。

2.节头:包含文件中各节的名称、类型、地址、偏移量、大小、旗标、链接和对齐信息等内容,在hello.elf文件中共包含14个节。

3.重定位节:当链接器将这些目标文件与其他文件结合成最终的可执行文件或库时,它需要根据重定位条目的具体类型来修改这些位置。每一条重定位条目都包含了多个关键信息,如:

偏移量:指明了在目标文件中需要修改的具体位置。

信息:关于该重定位条目的额外数据或元数据。

类型:用于标识地址计算的算法,告诉链接器如何根据该条目来计算新的地址。

符号值:与这个重定位条目相关联的符号的值。

符号名称:与该重定位条目相关联的符号的名称。

加数:在某些情况下,链接器会使用这个加数来调整计算出的地址。


4.符号表:.symtab节中包含ELF符号表,这张符号表包含一个条目的数组,存放一个程序定义和引用的全局变量和函数的信息。在符号表中,name代表的是字符串中的字节偏移,指向符号的以NULL结尾的字符串的名字;value是距定义目标的节的起始位置的偏移;size是目标的大小;type要么是数据,要么是函数;符号表还包含各个节的条目以及对应的原始源文件路径名的条目;bind表示符号是本地的还是全局的。

4.4 Hello.o的结果解析

使用objdump -d -r hello.o命令,得到对应的反汇编程序:

     

分支转移:在反汇编的跳转指令中,跳转的位置直接被表示为确定的地址,而不是段名称。

函数调用:在反汇编的函数调用中,不再调用函数名称,而是直接调用函数所在地址。

4.5 本章小结

本章介绍了汇编的概念与作用,并且将hello.s文件汇编得到hello.o文件,并且生成了ELF格式的可执行文件hello.elf。对hello.elf文件格式进行了观察,了解了每个节的内容。通过分析hello.o的反汇编内容,与hello.s进行对比,发现了两者在函数调用和分支转移的实现过程中的不同点。

(第41分)

5章 链接

5.1 链接的概念与作用

5.1.1 链接的概念

链接是一个过程,它负责将多个独立编译的代码和数据片段(通常被称为目标文件或对象文件)组合成一个单独的可执行文件或库文件。这个过程使得文件可以被加载到内存中并运行。

5.1.2 链接的作用

链接的主要作用之一是支持分离编译,这意味着开发者不需要将整个大型应用程序编写在一个巨大的源文件中。相反,他们可以将应用程序分解为多个更小、更易于管理的模块,每个模块都可以独立地进行修改和编译。通过链接这些独立编译的模块,开发者可以最终生成一个完整的应用程序,这个过程既提高了开发效率,也增强了代码的可维护性。

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,得到可执行文件hello


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

1.ELF头

hello.elf相比较,可执行文件hello产生的elf文件Hello.elf中类型由REL文件变为EXEC可执行文件,程序头大小和节头数量增加,并且获得了入口地址。

2.节头

节头数量有所增加,但各条目内包含信息种类并未发生变化。这是由于链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。


3.程序头

相比于hello.elf,Hello.elf中新增了程序头,该头描述了系统准备程序执行所需的段或其他信息。

4.动态节

该节包含了与动态链接相关的各种信息,如程序所依赖的动态库、符号表的位置、重定位表的大小等。通过其内部的标签和值,指向了ELF文件中的其他关键数据,如符号表、重定位表等。这些引用使得动态链接器能够找到并解析程序中的符号引用,确保程序能够正确运行。


5.重定位节

链接后,重定位节内容变化为执行过程中需要通过动态链接调用的函数,同时类型也发生改变。

6.符号表

符号表条目显著增加,包含了各目标文件中符号定义及引用信息。

5.4 hello的虚拟地址空间

如图所示,程序占有虚拟空间的0x401000-0x402000地址。

 

在Hello.elf中查找某一个段.text的起始地址为0x4010f0,在edb中根据地址查找到对应信息

5.5 链接的重定位过程分析

使用命令objdump -d -r hello >Hello.asm,生成hello的反汇编文件。

5.5.1 与hello.o反汇编文件不同之处

1. 多出了.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等函数的反汇编代码,这些是链接过程中因重定位而加入的各种在hello中被调用的函数。

2.跳转指令不同: 在链接过程中,链接器会处理目标文件中的重定位条目。这些条目标识了需要修改的地址位置,以便在程序运行时能够正确地引用外部函数或数据。链接器会计算这些位置与过程链接表中相应函数入口点之间的相对距离。一旦计算得出这些相对地址,链接器就会修改目标文件中相应位置的字节代码,将这些位置更新为指向PLT中对应函数的相对地址。当程序执行到这些位置时,就会跳转到PLT中的函数入口点,进而执行相应的函数。


5.5.2 重定位过程

重定位过程分为两步进行:第一步是节的重定位和符号定义。在这一步中,链接器首先会遍历所有的输入模块,并将它们中相同类型的节(如代码节、数据节等)合并成一个新的、统一的聚合节。然后,链接器会给这些新的聚合节分配运行时内存地址。同时,链接器还会为输入模块中定义的每一个符号(这些符号可能代表函数、变量等)分配唯一的运行时内存地址。通过这一步,程序的每一条指令和每一个数据项都被赋予了唯一的、确定的在运行时的内存地址。第二步是节中的符号引用重定位。在合并节并分配地址之后,链接器需要确保程序中所有对符号的引用都是正确的。这一步的目标是修改代码节和数据节中的每个符号引用,使它们指向正确的运行时地址。具体来说,链接器会查找所有对之前定义的符号的引用,并将这些引用更新为它们所指向的符号在运行时内存中的实际地址。这样,当程序在运行时访问这些符号时,就会直接跳转到或读取到正确的内存位置。通过这两个步骤,链接器确保了程序在运行时能够正确地访问其所需的代码和数据,从而实现了从源代码到可执行文件的成功转换。

5.6 hello的执行流程

执行流程中经过的函数如下:start、_libe_start_main、main、printf、_exit、_sleep、getchar、_exit。

地址分别为:0x4010f0、0x2f12271d、0x401125、0x4010a0、            0x4010e0、 0x4010b0、0x4010d0

5.7 Hello的动态链接分析

动态链接的基本思想是将程序划分为多个相对独立的模块,这些模块在程序编译时并不需要直接链接在一起,而是在程序运行时根据需要将它们动态地链接成一个完整的可执行程序。

当程序调用共享库中的函数时,由于这些函数定义在运行时才能加载的模块中,编译器在编译时无法预测这些函数的运行时地址。为了解决这个问题,编译器在编译时会对这些函数调用生成重定位记录。这些记录包含了关于如何找到并解析函数地址的必要信息。当程序被加载到内存中并执行时,动态链接器会负责解析这些重定位记录,并确定共享库中函数的实际地址。一旦动态链接器确定了函数的实际地址,它就会将这个地址存储在全局偏移量表(GOT)的数据结构中。GOT是一个在程序运行时动态分配的内存区域,它包含了共享库中所有被引用的函数和变量的地址。当程序需要调用共享库中的函数时,它会先查找GOT中相应的地址,然后跳转到该地址执行函数。根据Hello.elf文件可知,GOT起始表位置为:0x404000:

GOT表位置在调用dl_init之前内容如下:


调用后内容如下:

调用后,.got中的条目已经改变,说明动态链接完成。

5.8 本章小结

本章首先介绍了链接的概念和作用,接着生成了hello可执行文件,并将其对应的elf文件内容和hello.o的elf文件内容进行了对比,接着分析了其重定位的流程,探究了hello反汇编文件和hello.o反汇编文件的不同之处,最后利用edb调试对hello程序的虚拟地址空间使用、执行流程和动态连接过程进行了分析。

(第51分)

6章 hello进程管理

6.1 进程的概念与作用

进程是计算机系统中程序执行的一个具体实例。它代表了程序在特定数据集上的一次运行活动,是操作系统进行资源分配和调度的基本单位,同时也是操作系统结构的核心组成部分。在传统操作系统中,进程既作为资源分配的基本单位,也作为程序执行的基本单位。

进程为程序提供了一个虚拟环境,使得程序在执行时仿佛独占处理器和内存资源。这种假象使得程序开发者能够无需考虑多道程序同时执行时的复杂性,仿佛处理器正不间断地一条接一条地执行他们程序中的指令。作为执行中程序的实例,系统中的每个程序都在特定的进程上下文中运行,确保了程序执行的独立性和安全性。

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

作用:Shell是一种交互式的应用级程序,它通常被称为命令解释器或命令行界面。Shell为用户提供了一个界面,使用户能够通过输入命令来与计算机系统进行交互。

流程:首先,Shell从终端读取用户输入的命令。然后,它会分析这条命令,确定其含义和所需的操作。如果输入的命令是Shell内置的命令,Shell会立即在当前Shell进程中执行这个命令。如果命令是调用外部程序,Shell会调用fork系统调用来创建一个新的子进程。在这个子进程的上下文中,Shell会执行指定的程序。在子进程执行程序之前,Shell会判断这个程序是应该在前台运行还是后台运行。前台程序会占用终端,Shell会等待该程序执行完成后再接收下一个命令。而后台程序则会在不被用户直接监控的情况下运行,Shell会立即返回到命令提示符,使用户可以继续输入其他命令。在整个过程中,Shell还会监听从键盘输入的信号。Shell会根据接收到的信号进行相应的处理,比如终止某个进程或者执行其他特定操作。

6.3 Hello的fork进程创建过程

输入./hello命令来运行hello程序, Bash调用fork系统函数来创建一个新的子进程。在fork的执行过程中,操作系统会执行以下步骤:

  1. 为新的子进程分配一个唯一的进程标识符,并在系统内部维护的进程控制块表中添加一个新的条目。
  2. 操作系统会复制父进程的环境到子进程中,包括大部分PCB的内容,如进程状态、打开的文件描述符、环境变量等。
  3. 为子进程分配必要的资源,包括程序映像、堆、共享库以及用户栈的副本。
  4. 子进程会继承父进程打开的所有文件描述符的副本。

6.4 Hello的execve过程

当执行execve系统调用时,操作系统会首先清空当前子进程的用户空间栈,为新程序的执行做好准备。紧接着,它会将要执行的程序的命令行参数和环境变量推送到栈中,以确保新程序在启动时能够访问到这些信息。一旦参数和环境变量被压入栈中,execve会将控制权转移到新程序的入口点,通常是main函数。在这个过程中,execve还会负责加载hello程序所需的库文件到内存中,并为程序分配和初始化必要的内存空间。如果execve调用成功,那么原有进程的代码和数据段会被新程序的代码和数据完全替换,新程序将成为该进程的新映像,并开始执行。这意味着父进程的状态被新程序接管,并继续以新程序的身份运行。然而,如果在执行execve过程中发生错误,比如指定的程序不存在,execve会返回一个负值来表示执行失败。在这种情况下,原有进程将不会被终止,而是继续运行,并且错误状态可以被调用execve的父进程通过检查返回值来检测和处理。

6.5 Hello的进程执行

Hello程序运行时,操作系统提供如下抽象:

  1. 逻辑控制流

逻辑控制流代表了一个程序在执行时其指令执行的顺序。这个顺序由程序计数器(PC)的值来指示,PC值指向了当前正在执行的指令。逻辑控制流与可执行文件中的指令相对应,并决定了程序中的各个部分在何时被执行。

  1. 时间分片

在多任务操作系统中,进程不是独占处理器资源的,而是轮流使用。每个进程都会被分配一个时间片来执行其逻辑控制流的一部分。当时间片结束时,该进程会被挂起,处理器控制权会切换到另一个进程。两个或多个进程在同一时间段内各自执行其逻辑控制流的一部分,这种现象被称为并发。多任务操作通过时间分片的方式实现,允许系统同时运行多个进程。

  1. 用户模式与内核模式

为了保护操作系统的核心功能,处理器引入了模式位的概念。这个模式位设置在处理器的某个控制寄存器中,用于区分当前进程是运行在用户模式还是内核模式。

通常,用户程序在启动后首先运行在用户模式下。然而,当发生中断、故障或系统调用时,处理器会切换到内核模式,并将控制权传递给相应的内核处理程序。这些处理程序运行在内核模式下,具有访问系统资源的更高权限。当内核处理程序完成其任务后,它会将控制权返回给用户程序,并将处理器模式从内核模式切换回用户模式。这种切换确保了用户程序不能直接访问系统资源,从而提高了系统的安全性和稳定性。

  1. 上下文切换

上下文切换实际上是一个保存和恢复的过程:它首先会保存当前正在运行的进程的上下文信息(包括CPU寄存器的值、程序计数器等),然后恢复之前被暂停的进程的上下文信息,并将控制权传递给这个被恢复的进程,使其能够继续执行。

6.6 hello的异常与信号处理

1.正常运行状态:打印十次提示信息,按下回车停止进程。

2.运行时按下Ctrl+C

Shell进程收到SIGINT信号,Shell结束并回收hello进程。

3.运行时按下Ctrl+Z

Shell进程收到SIGSTP信号,显示提示信息并停止hello进程。

4.通过ps和jobs命令查看hello进程状态,发现该进程处于挂起状态而非回收状态。


5.输入pstree命令,将所有进程以树状图显示

6.输入fg 1命令,发现进程会从挂起处继续运行,打印剩下语句。运行完毕后程序正常结束。

7.不停乱按不影响程序运行,但是乱按的结果会保留在输出结果中。

6.7本章小结

本章首先介绍了进程的概念和作用,然后简述了壳Shell-bash的作用与处理流程,接着分析了fork进程的创建过程和execve过程,然后详细分析了hello程序的执行过程,最后测试了hello的各种可能的异常状况以及异常处理的结果。

(第61分)

7章 hello的存储管理

7.1 hello的存储器地址空间

  1. 逻辑地址:
       
    逻辑地址是程序在编译后,在汇编代码中用来指定操作数或指令的地址。它由两部分组成:一个段标识符和一个偏移量。这个偏移量指明了在特定段内的相对位置。因此,逻辑地址通常表示为段标识符:段内偏移量的形式。
  2. 物理地址:
        物理地址是存储器中每个字节单元的唯一标识符。以字节为单位的信息存储在内存中,每一个这样的字节都与一个独特的物理地址相对应。这个地址是实际存在于系统中的内存字节的标识,因此也被称为实际地址或绝对地址。
  3. 虚拟地址:
        CPU进入保护模式时,程序会运行在一个称为虚拟地址空间的环境中。这个虚拟地址空间包含了所有可能地址的集合,对于一个64位的机器来说,这个集合中包含了2^64种可能的地址。这些地址是程序运行时看到的地址,与实际物理内存地址不同。
  4. 线性地址:
        线性地址是逻辑地址到物理地址转换过程中的一个中间步骤。在分段架构中,逻辑地址首先被转换为线性地址。这通常是通过将逻辑地址中的段标识符转换为段基地址,然后加上段内偏移量来实现的。线性地址空间是平坦的,意味着所有的地址都是连续排列的,没有分段的概念。从线性地址到物理地址的转换通常由内存管理单元或类似的硬件组件来完成。

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

Intel平台下,逻辑地址的构成是selector(段选择符)和offset(段内偏移量)的组合。selector通常与CS(代码段)寄存器的值相对应,而offset则与EIP(指令指针)寄存器的值相对应。为了将逻辑地址转换为线性地址,系统会使用这个selector去查询全局描述符表(GDT)或局部描述符表(LDT),这个过程就是段式内存管理。

段标识符是一个16位长的字段,其中包含了段选择符。通过段选择符的前13位,系统可以在GDTLDT中定位到一个具体的段描述符。这个描述符详细描述了段的属性,包括段的基地址和大小等关键信息。

全局段描述符存放在GDT中,而一些局部的段描述符则存放在LDT中。要确定一个逻辑地址使用的是GDT中的段还是LDT中的段,系统会检查段选择符的T1位。如果T1=0,则表示当前使用的是GDT中的段;如果T1=1,则表示使用的是LDT中的段。一旦确定了使用哪个描述符表,系统就会根据段选择符中的前13位,在相应的描述符表中查找对应的段描述符。找到段描述符后,就可以从中获取段的基地址。最后,通过将这个基地址与offset相加,就可以得到线性地址。这个过程可以简单地表示为:Base(段基地址)+ offset(段内偏移量)= 线性地址。

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

为了跟踪哪些虚拟页被缓存在DRAM中,以及这些虚拟页在物理内存中的确切位置,系统使用了一个称为页表的数据结构。页表是一个数组,其中每个元素称为页表条目(PTE),每个PTE包含一个有效位和一个n位地址字段。有效位用于指示对应的虚拟页是否在DRAM中缓存,而n位地址字段则包含了物理页的地址信息。在地址转换过程中,一个关键的组件是页表基址寄存器,它存储了当前使用的页表的基地址。当CPU尝试访问一个虚拟地址时,它首先会将这个虚拟地址分解为两个部分:一个p位的虚拟页面偏移量和一个(n-p)位的虚拟页号(VPN)。如果虚拟页已经在DRAM中被缓存,那么内存管理单元(MMU)会使用VPN来从页表中检索相应的PTE。一旦PTE被找到,MMU就可以将PTE中的物理页号和虚拟地址中的虚拟页面偏移量组合起来,形成一个物理地址,从而访问目标内存位置;如果虚拟页没有被缓存,那么系统会触发一个缺页异常。此时,控制会转移到缺页异常处理程序。处理程序会确定物理内存中哪个页面可以被替换,并检查该页面是否已被修改过。如果需要,处理程序会先将牺牲页的内容写回磁盘。然后,处理程序会从磁盘中加载新的页面到物理内存中,并更新页表中的PTE以反映新的物理页地址。最后,处理程序会返回到原来的进程,并将引起缺页的虚拟地址重新发送给MMU进行地址转换,这次由于页面已被缓存,因此会成功命中。

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

TLB是内存管理单元中的一个关键组件,它作为页表条目PTE的缓存。TLB是一个小型的、虚拟寻址的缓存,其每一行都存储了一个PTE的副本。由于TLB的高度相联性,它能够在地址翻译过程中提供快速的PTE查找。

四级页表,是为了更有效地管理庞大的页表空间而设计的。在四级页表结构中,虚拟地址的页号(VPN)被分为k个部分,每个部分(VPNi)都用作指向相应级别页表的索引。当1 <= j <= k-1时,每个VPNj都指向下一级(j+1)的某个页表。在四级页表的最低级别中,每个PTE包含了一个物理页面号(PPN)或者指向磁盘块的地址。为了构建物理地址,MMU需要遍历kPTE,才能最终确定PPN

Intel Core i7为例,它采用了一个四级页表层次结构,每个VPNi包含9位。当TLB未命中时,36位的VPN会被分解为VPN1VPN2VPN3VPN4CR3寄存器保存了第一级(L1)页表的物理地址,VPN1用于定位L1页表中的PTE,该PTE包含第二级(L2)页表的基址。VPN2接着用于定位L2页表中的PTE,以此类推,直到在L4页表中找到包含所需PPNPTE。最后,将这个PPN与虚拟地址中的页面偏移量(VPO)组合,就可以得到相应的物理地址。

MMU尝试将一个虚拟地址(VA)转换为物理地址(PA)时,它首先会向TLB查询对应的PTE。如果TLB命中,则直接使用该PTE构建物理地址,跳过后续的内存访问步骤。如果TLB未命中,MMU则需要根据虚拟地址生成PTE的地址,并从高速主存中检索PTE。如果PTE的有效位为0,说明发生了缺页异常,此时MMU会触发缺页处理程序。处理程序会确定物理内存中需要替换的页面,然后加载新的页面到物理内存中,并更新相应的PTE。处理完成后,处理程序会返回到原进程,并重新执行导致缺页的指令。通过这种方式,TLB和多级页表共同确保了虚拟内存的高效管理和地址翻译的快速执行。

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

在访问内存地址时,系统首先会根据地址的组索引来确定所需的缓存行可能存储的位置。如果地址对应的标记是数据,则系统会在L1数据缓存的相应组中进行查找。相反,如果标记是指令,则会在L1指令缓存的对应组中进行查找。

一旦定位到L1缓存的相应组,系统会逐行比较缓存行中的标记位和地址中的标记位。如果两者匹配且缓存行的有效位被设置为1,则表示缓存命中。系统会从缓存行中提取出对应的偏移量,并据此取得所需的字节数据。

如果缓存行中的标记位与地址中的标记位不匹配,或者有效位为0,则表示缓存未命中。此时,系统会转向下一级缓存进行查找,如果L2缓存中也未命中,那么系统将继续向更低一级的缓存或主内存中查找,直到找到所需的数据或指令为止。

7.6 hello进程fork时的内存映射

fork()系统调用被触发时,内核会为新创建的子进程准备一系列必要的数据结构,并赋予其一个独特的进程标识符(PID)。为了保障子进程能够独立运行并拥有自己的内存空间,内核会复制父进程的内存管理结构。这些内存管理结构的复制并不是立即进行物理复制,它们被设置为只读,并且每个区域结构都被标记为写时复制。在子进程或父进程尝试对这些区域进行写操作之前,这些区域实际上是共享的。

fork()在子进程中返回时,子进程的虚拟内存布局与调用fork()时的父进程完全一致,但所有的内存页面都被设置为只读和写时复制。这种设置确保了如果任一进程试图修改其内存中的某个页面,内核将仅在写操作发生时动态地分配新的物理页面,并将旧页面的内容复制到新页面中,同时更新页表以反映这一变化。

7.7 hello进程execve时的内存映射

当使用execve函数时,当前进程会加载并执行包含在可执行目标文件a.out中的程序,从而有效地用a.out程序替换当前正在运行的程序。加载并运行a.out包括以下几个步骤:

清除现有用户空间:首先,execve会删除当前进程虚拟地址空间中已经存在的用户区域。

创建新程序区域:接下来,execve会为新程序的代码、数据、BSS以及栈区域创建新的虚拟内存区域。这些新区域是私有的,并采用写时复制技术。代码区域和数据区域将被映射到a.out文件中的.text.data段。BSS区域则是一个请求二进制零的区域,它的大小在a.out文件中指定,但实际上并不直接映射到文件,而是被映射到一个匿名文件。同样,栈和堆区域也是请求二进制零的,初始时它们大小为零。

映射共享库:如果a.out程序与共享对象链接,则这些共享对象也会被加载到进程的地址空间中。这些共享对象会被映射到用户虚拟地址空间中的共享区域,以便多个进程可以共享它们的代码和数据。

设置程序执行起点:最后,execve会设置当前进程上下文的程序计数器(PC)。程序计数器是一个特殊的寄存器,它指示了CPU下一条要执行的指令的内存地址。在这个步骤中,execve会将程序计数器设置为新程序代码区域的入口点,这样当进程恢复执行时,它就会开始执行a.out程序中的第一条指令。

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

缺页故障:当CPU尝试访问一个虚拟内存页面时,如果该页面当前并未被加载到动态随机存取存储器中,即DRAM缓存中没有该页面的副本,这种情况就被称为缺页故障。当CPU引用了一个页表条目,但地址翻译硬件发现该页表条目指向的页面并未在DRAM缓存中时,它会检查页表条目的有效位。一旦确认页面缺失,地址翻译硬件会触发一个缺页异常。

缺页中断处理:一旦缺页异常被触发,系统会调用缺页中断处理程序来处理这个问题。处理程序首先会选择一个牺牲页,用于腾出空间来加载缺失的页面。如果这个牺牲页在DRAM中的内容是修改过的,处理程序会先将其写回到磁盘,以保持数据的一致性。之后,处理程序会将缺失的虚拟页面从磁盘复制到DRAM中原来牺牲页所在的位置。完成复制后,处理程序会更新页表条目,将有效位设置为1,并标记页面的其他属性。最后,处理程序会返回,并重新启动之前导致缺页故障的指令。这次,因为所需的虚拟页面已经被加载到DRAM中,地址翻译硬件可以正常地处理页面命中,从而允许CPU继续执行指令。

7.9动态存储分配管理

动态内存分配依赖于动态内存分配器,这个分配器负责管理一个进程的特定虚拟内存区域,我们称之为。分配器将堆划分为一系列大小各异的连续内存块,这些内存块要么已经被分配给应用程序使用,要么处于空闲状态等待分配。

已分配的内存块被明确地保留下来,以供应用程序进行数据存储和操作。而空闲的内存块在它们被应用程序请求分配之前,将一直保持空闲状态,等待被使用。一旦某个已分配的内存块不再需要,它将被释放回堆中,重新变为空闲状态,等待下一次的分配。

7.10本章小结

本章介绍了hello的存储管理,先介绍了存储器的地址空间,然后介绍了段式管理,页式管理,接着详细描述了TLB和四级页表以及其支持下的VA到PA的变换机制,接着介绍了三级Cache支持下的物理内存访问,以及hello进程fork和execve下的内存映射。之后介绍了缺页故障的原理和缺页中断的处理方式,最后介绍了动态存储分配管理机制。

(第7 2分)

8章 hello的IO管理

8.1 Linux的IO设备管理方法

Linux中每个文件都被视为一个由m个字节组成的序列。这种设计不仅适用于传统的文件存储,而且进一步将所有的I/O设备都抽象化为文件的概念。通过这种设备到文件的映射方式,Linux内核实现了一个简单而基础的应用接口,即Unix I/O。这一设计使得所有输入和输出的操作都能够以统一且一致的方式进行。

8.2 简述Unix IO接口及其函数

8.2.1 unix接口

1)打开文件:当应用程序想要访问一个I/O设备时,它会通过Unix接口向内核发出打开文件的请求。内核会验证请求的合法性,并返回一个小的非负整数,即文件描述符。这个文件描述符在后续对该文件的所有操作中都会被用来标识该文件。同时,内核会记录与该打开文件相关的所有信息,如文件位置、访问权限等。应用程序只需记住这个描述符,就可以通过它来访问该文件。

2I/O设备作为文件:在Unix系统中,I/O设备都被抽象为文件的概念。当应用程序想要与这些设备进行交互时,它会像操作普通文件一样打开这些设备文件,并获得一个文件描述符。通过这个描述符,应用程序可以像读写普通文件一样来读写设备数据。

3)改变文件位置:对于每个打开的文件,Unix内核都会维护一个文件位置指针。这个指针的初始值为0,表示文件开头。应用程序可以通过执行seek操作来显式地改变当前文件位置。当进行读写操作时,数据将会从当前文件位置开始读取或写入。

4)读写文件:读操作是指从文件中复制一定数量的字节到内存中。这些字节从当前文件位置开始读取,并且读取后文件位置指针会相应地增加。类似地,写操作是指从内存中复制一定数量的字节到文件中。这些字节从当前文件位置开始写入,并且写入后文件位置指针也会相应地增加。如果文件的大小小于或等于当前文件位置指针的值,则表示已经到达文件末尾。

5)关闭文件:当应用程序完成对文件的访问后,它应该通知内核关闭该文件。作为响应,内核会释放与该文件相关的所有数据结构,并将该文件描述符返回到可用的描述符池中。此外,无论一个进程因为何种原因终止时,内核都会自动关闭该进程打开的所有文件并释放相关的内存资源。

8.2.2 Unix I/O函数

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

Unix系统中,进程通过调用open函数来打开一个已存在的文件或创建一个新文件。该函数接受三个参数:filenameflags以及modeopen函数成功执行后,会将filename转换为一个文件描述符,并返回该描述符的数字值。这个返回的描述符总是当前进程中尚未使用的最小描述符。如果打开操作失败,open函数会返回一个错误码。

2int close(fd)

当进程完成对某个文件的访问后,它应该调用close函数来关闭该文件。close函数接受一个参数fd,即要关闭的文件的描述符。成功关闭文件后,close函数返回0;如果关闭操作失败,则返回-1

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

read函数用于从已打开的文件中读取数据。它接受三个参数:fd(要读取数据的文件的描述符)、buf(指向用于存储读取数据的内存位置的指针)以及n(要读取的最大字节数)。read函数从描述符为fd的文件的当前位置开始读取最多n个字节的数据,并将这些数据存储到buf指向的内存位置。如果读取成功,函数返回实际读取的字节数量;如果读取到文件末尾(EOF),则返回0;如果读取过程中发生错误,则返回-1

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

write函数用于将数据写入已打开的文件。它接受三个参数:fd(要写入数据的文件的描述符)、buf(指向包含要写入数据的内存位置的指针)以及n(要写入的最大字节数)。write函数从buf指向的内存位置复制最多n个字节的数据到描述符为fd的文件的当前位置。如果写入成功,函数返回实际写入的字节数量;如果写入过程中发生错误,则返回-1

8.3 printf的实现分析

Printf函数的函数体:

va_list的定义:typedef char *va_list, 它是一个字符指针。

 (char*)(&fmt) + 4)是…中第一个参数。

fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素。fmt也是个变量,它的位置在栈上分配,也具有地址。  对于一个char *类型的变量,它入栈的是指针,而不是这个char *型变量。

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

write系统函数:调用中断门,通过中断门,来实现特定的系统服务。其中int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。

sys_call函数:显示格式化了的字符串。

字符显示驱动子程序:

ASCII码:当使用printf函数输出一个字符时,该字符首先以ASCII码的形式存在。

字体渲染:操作系统或图形库中的字体渲染器会负责将ASCII码转换成可显示的图像。字模库中包含了每一个可打印ASCII字符的图像数据。渲染器会根据当前的字体设置和字符大小,从字模库中检索出相应的字符图像。

颜色映射:如果字符需要特定的颜色,那么渲染器还会将字符图像与颜色信息结合起来。这通常是通过将字符图像的每个像素与颜色表中的RGB值进行映射来实现的。颜色表是一个存储了RGB颜色值的表,每个颜色值对应一个索引。

VRAM:渲染器将字符图像(包括颜色信息)写入到VRAM中。VRAM是显卡上的一块内存,用于存储屏幕上每个像素的颜色信息。VRAM中的每个位置都对应着屏幕上的一个像素点。

显示芯片:显示芯片按照设定的刷新频率从VRAM中读取颜色信息,并通过信号线将这些信息传输给显示器。显示器根据接收到的RGB值来显示相应的颜色,从而在屏幕上呈现出字符。

8.4 getchar的实现分析

getchar函数:

当应用程序调用getchar函数时,它实际上是从由操作系统提供的标准I/O库中调用该函数。标准I/O库检查键盘缓冲区是否有可用的输入。如果有,getchar函数从缓冲区中读取一个字符,并将其返回给应用程序。如果没有,getchar函数会阻塞,或者根据库的实现和程序的设置返回一个错误或EOF

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

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

8.5本章小结

本章介绍了hello的I/O管理机制。首先系统地介绍了linux下I/O设备的管理方法,接着对unix接口的作用进行介绍,并且给出了unix接口的相关函数,对这些函数的功能进行介绍。最后我们对printf函数和getchar函数的实现进行了分析。

(第81分)

结论

Hello经历的过程:

1、预处理。将hello.c进行预处理,生成一个经过修改的hello.i文件。

2、编译。将hello.i文件编译成汇编语言的文件hello.s。

3、汇编。将hello.s翻译成为一个可重定位目标文件hello.o。

4、链接。将hello.o文件和可重定位目标文件和动态链接库链接起来,生成一个可执行目标文件hello。

5、运行。在命令行中运行可执行目标文件hello。

6、创建进程。终端shell调用fork函数,创建一个子进程,为程序的加载运行提供虚拟内存空间等上下文。

7、进程控制:由进程调度器对进程进行时间片调度,并通过上下文切换实现hello的执行,程序计数器(PC)更新,CPU按顺序取指,执行程序控制逻辑。

8.访问内存:MU将程序中使用的虚拟内存地址通过页表映射成物理地址。9.IO:hello通过unix接口和unix I/O函数与外界进行交互。

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

感悟:对于计算机系统的设计与实现,我深感其复杂与精妙。计算机系统的设计需追求高效、稳定与可扩展,实现则需精确、细致且具备前瞻性。

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

附件

hello.c:源程序

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

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

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

hello.elf: hello.o的elf格式

hello:链接后得到的可执行目标文件

Hello.elf:hello的elf格式

Hello.asm: 反汇编hello得到的反汇编文件

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

参考文献

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

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

[2]   解剖getchar_getchar源码-CSDN博客

[3]   C语言printf函数实现解读_printf源码-CSDN博客

[4]   https://www.cnblogs.com/fanzhidongyzby/p/3519838.html.

[5]   https://blog.csdn.net/Thewei666/article/details/129801508

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值