哈尔滨工业大学计算机系统大作业——hello的一生

本文详细探讨了hello程序从源代码到执行的完整过程,包括预处理、编译、汇编、链接等阶段,以及在Ubuntu环境下的实现。文章深入分析了进程管理、存储管理(虚拟内存、页表、TLB、Cache)和I/O管理,揭示了操作系统如何利用底层原理实现这些功能。此外,还介绍了hello进程的fork、execve操作,以及动态链接库的加载和内存映射。
摘要由CSDN通过智能技术生成

摘  要

本文拟通过分析一个hello.c完整的生命周期,详细介绍了程序从最初的C文件到可执行的目标文件的整个过程,需要经历预处理、编译、汇编、链接等操作;并详细分析了运行hello程序时系统的进程管理、存储管理与I/O管理的原理与机制到最后hello被回收的整个过程。并从计算机底层逻辑分析操作系统利用了什么样的原理,怎样实现这些过程的。

关键词:预处理,编译,汇编,链接,进程,虚拟内存,I/O,hello                          

目  录

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

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

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

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

1.4 本章小结............................................................................... - 4 -

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

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

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

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

2.4 本章小结............................................................................... - 5 -

第3章 编译................................................................................... - 6 -

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

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

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

3.4 本章小结............................................................................... - 6 -

第4章 汇编................................................................................... - 7 -

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

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

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

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

4.5 本章小结............................................................................... - 7 -

第5章 链接................................................................................... - 8 -

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

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

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

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

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

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

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

5.8 本章小结............................................................................... - 9 -

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

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

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

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

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

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

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

6.7本章小结.............................................................................. - 10 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结............................................................................ - 12 -

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

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

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

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

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

8.5本章小结.............................................................................. - 13 -

结论............................................................................................... - 14 -

附件............................................................................................... - 15 -

参考文献....................................................................................... - 16 -

第1章 概述

1.1 Hello简介

P2P:hello刚开始由程序员通过外部输入设备键盘输入并保存为.c文件hello.c,成为了一个program。然后通过预处理器处理,编译器编译出汇编,再通过汇编器将汇编代码翻译成二进制机器语言,最后使用连接器将汇编器生成的目标文件和库链接为一个可执行文件,最后在Bash壳里,为其fork一个子进程。

020:shell为此子进程execve,映射虚拟内存,使用TLB,4级页表、3级Cache找到对应物理内存,然后内核调度分配时间片。通过IO管理和信号处理,使进程结果输出在屏幕。程序运行结束后,shell父进程负责回收hello进程,内核删除与之相关的数据结构。

1.2 环境与工具

软件环境:Windows 11 64位;Vmware Workstation 16;Ubuntu22.04-2 64位

硬件环境:11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz  16G RAM  

          198G SSD  1T Disk

开发与调试工具:gcc,GDB,EDB,vim,readelf

1.3 中间结果

1:hello.c:源程序;

2:hello.i:预处理之后的文本文件;

3:hello.s:编译之后的汇编文件;

4:hello.o:汇编之后的可重定位目标文件;

5:hello:链接之后的可执行目标文件;

6:hello.elf:hello.o的elf格式,查看hello.o的各节信息;

7:hello.ob:hello.o的反汇编文件;

8:hello.out:hello的反汇编文件。

1.4 本章小结

介绍本次实验的大致内容,以及实验环境、工具,罗列出实验过程中需要的文件及其大致作用。

第2章 预处理

2.1 预处理的概念与作用

1.概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。由预处理器对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位,预处理记号用来支持语言特性。

2.作用:

1)实现条件编译,通过预处理可以实现部分代码在某些条件下的选择性编译,条件编译指令如#ifdef,#ifndef,#else,#elif,#endif等。

2)实现宏定义,在预处理阶段用定义的实际数值将宏替换。

3)实现头文件引用,将头文件的内容复制到源程序中以实现引用。

4)实现注释,将c文件中的注释从代码中删除。

5)实现特殊符号的使用。如处理#line、#error、#pragma以及#等。

2.2在Ubuntu下预处理的命令

应截图,展示预处理过程!

进入终端,进入hello.c对应目录下,用gcc -E hello.c -o hello.i对hello.c进行预处理

2.3 Hello的预处理结果解析

产生了hello.i文件

比较hello.i文件和hello.c文件,我们可以发现源程序中中的头文件<stdio.h>等被处理。

2.4 本章小结

本章先介绍了预处理的概念和作用,然后通过具体例子,在Ubuntu中用gcc指令对hello.c预处理后变为hello.i的过程简要分析

第3章 编译

3.1 编译的概念与作用

注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序

概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,包含一个汇编语言程序。以高级程序程序编写的源程序作为输入,以汇编语言的目标程序作为输出。

作用:把预处理文件进行词法分析、语法分析、语义分析、优化,从C语言等高级语言转换为成计算机可识别的汇编语言,比.i预处理文件更容易让机器理解。同时,在编译时还会进行语法检查和一定的优化。

3.2 在Ubuntu下编译的命令

用指令:gcc -s hello.c -o hello.s

3.3 Hello的编译结果解析

产生hello.s 文件

3.3.1数据

1)字符串常量

程序中有两个在printf中的字符串被存放在只读数据段.rodata中

 2)局部变量

循环中的i用来控制循环,编译器将其放入栈中存储

3)立即数

在代码中直接显示

4)main函数参数

Main函数的两个参数整型变量argc和字符指针argv分别存储在rdi和rsi上,使用时将其放入栈中

3.3.2赋值

 将循环中的控制变量i赋值,赋一个立即数0,循环开始

3.3.3算术操作

for(i = 0; i <5; i++)中有算术操作i++,在汇编代码中通过add来实现

       3.3.4类型转换

       sleep(atoi(argv[3]));  atoi是把字符串转换成整型数的一个函数。在汇编中调用atoi函数进行。

       3.3.5关系操作

       argc!=4;以及i<5,两个关系判断。

        第一个:

      

这里cmpl比较,通过比较结果设置条件码,以此判断是否跳转至分支中。

第二个:

这里cmpl比较判断循环次数是否达到要求,是则跳出循环,否则继续循环

3.3.6数组操作

字符串数组argv[]储存命令行各个参数,指向数组argv的指针用于访问到字符串数组argv[],数组存储于栈上的一段连续的空间内,访问时只需要起始地址和偏移量。这里加16后对应argv[2],将argv[2]的值取出放入寄存器rdx中;再对指针argv解引用,加8后对应argv[1],将argv[1]的值取出放入寄存器rsi中,调用printf函数进行输出。

3.3.7控制转移指令

第一个:

判断是否等于4,相等则ZF置为0跳转到.L2中执行,不相等则继续向下执行。

第二个:

判断i是否小于等于4,符合则跳转至.L4,不符合则继续向下执行

3.3.8函数操作

这里涉及的函数有main函数(argv[]和argc两个参数);printf函数(参数为两个字符串);exit函数(参数为1);atoi函数(参数为argv[3]);sleep函数(参数为atoi(argv[3])返回值);getchar函数;

 

 

 

 

 

函数需要调用时,在汇编中使用call来调用,调用前将参数放入rdi,rsi等寄存器中

在函数中定义局部变量,局部变量都存储在栈中

函数结束时,将rax寄存器中的内容作为返回值

3.4 本章小结

本章主要讲述了编译阶段编译器如何处理各种数据和操作,并结合具体实例hello.s文件理解这个过程,了解各个C语言中的数据和操作是如何以汇编语言的形式展现出来的。而通过理解这种机制,我们也可以将汇编语言翻译为C语言进行反汇编工作,理解高级语言和汇编语言之间的转化关系。

第4章 汇编

4.1 汇编的概念与作用

概念:汇编器将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。

作用:汇编过程将原来的语言转变为机器可直接识别并执行的二进制语言,将机器语言指令打包保存在hello.o中。

4.2 在Ubuntu下汇编的命令

指令:gcc -c hello.s -o hello.o

4.3 可重定位目标elf格式

在Ubuntu下生成可重定位目标elf格式的命令为readelf -a hello.o > hello.elf。

1.ELF头:

ELF头有一个16字节的Magic序列,这个序列描述了声称该文件的系统的字大小和字节顺序。剩下的部分包含帮助连接器语法分析和解释目标文件的信息。包括ELF头的大小、目标文件类型、机器类型、字节头部表的文件偏移和节头部表中条目的大小和数量;

2.节头部表

记录了每个节的名称、类型、属性(读写权限)、在ELF文件中占的度、对齐方式和偏移量。

3.重定位节

.rela.text节存放着代码的重定位条目。当链接器将目标文件与其他目标文件结合时,会结合这个节,修改.text节中相应的信息。rela.eh_frame节是.eh_frame节的重定位信息;

4.符号表

用来存放程序中定义和引用的函数或全局变量的信息。每个可重定位目标模块m都有一个符号表,它包含m定义和引用的符号的信息。符号表是由汇编器构造的,使用编译器输出到汇编语言.s文件中的符号。

其中name是符号名称,以整型变量存放,是字符串表.strtab中的字节偏移,指向符号的以null结尾的字符串名字。

Value是符号地址,是距离定义目标的节的起始位置的偏移,对于可执行目标文件来说是一个绝对的运行时地址。

Type时符号类型,表明符号的类型,通常要么是数据,要么是函数。

Binding是符号范围,表明符号是本地的还是全局的。

Section是分配目标,是一个到节头部表的索引,表明对应符号被分配至目标文件的某个节

ABS代表不该被重定位的符号

UNDEF代表未定义的符号,即本模块中引用的外部符号

COMMON表示还未被分配位置的未初始化的数据目标

Size是目标大小

4.4 Hello.o的结果解析

       指令:objdump -d -r hello.o

可以看到,hello.o 的反汇编与hello.s大致相同,只有几个方面的表达略有出入:

分支转移:反汇编的代码中,跳转指令的操作数数是具体的地址,hello.s是.L3一类的代码段名称

函数调用:在hello.s中,函数调用是call加调用的函数名,而反汇编文件是call加下一条指令的地址。

访问字符串常量:和跳转指令一样,反汇编使用的是地址

机器语言是由01构成的二进制代码表示的计算机能直接识别和执行的一种机器指令系统指令的集合,机器指令由操作码,操作数地址和操作结果的存储地址三部分组成。而每一条汇编语言都可以用机器指令表示。

4.5 本章小结

本章主要介绍了从hello.s到hello.o的汇编过程,查看分析了hello.o可重定位目标文件的ELF格式代码。使用objdump反汇编生成的代码与hello.s进行对比,简要了解了从汇编语言到机器语言的映射关系。

5章 链接

5.1 链接的概念与作用

概念:链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。在早期的计算机系统中,链接是手动执行的。在现代系统中,链接是由叫做链接器的程序自动执行的。

作用:把可重定位目标文件和命令行参数作为输入,产生一个完全链接的,可以加载运行的可执行目标文件,使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。

5.2 在Ubuntu下链接的命令

指令:ld -o hello -dynamic-linkker /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

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

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

1.ELF头

与hello.o文件的ELF头区别在:文件类型不同,hello的是EXEC,hello.o的是REL;程序的入口点不同,main函数不是在0x0开始的;节头数量发生变化

2.节头部表

节头部表对hello所有的节信息进行了声明,其中包括大小Size,偏移量Offset以及程序载入内存的虚拟地址的起始地址Address。因此根据节头部表中的信息我们可以利用HexEdit定位各个节所占的区间;

3.重定位节

4.符号表

可以看到,可执行目标文件的符号表表项数目(51 entries)多于可重定位目标文件的表项数目(18 entries)。一方面,可执行目标文件中加入了与调试、加载、动态链接相关的节,使得表示节的符号数增多;另一方面,由于链接器对可重定位目标文件中的符号进行了进一步解析,加入了若干系统调用。

5.4 hello的虚拟地址空间

   .init节虚拟地址:0000000000401000,与此处显示的地址相同。

 

 .text

 

5.5 链接的重定位过程分析

      

    (1)函数地址发生变化:hello中main的起始地址为401000;而hello.o中main的起始地址是从0开始,这也就导致了其他函数变量等地址的不同。

(2)call指令发生了变化:hello中, call指令后接的是绝对地址(有函数名);hello.o中call后接的是相对偏移地址。

(3)hello增加了hello.o中是不存在的节.init、.plt、.fini。

(4).链接增加新的函数:在hello中链接加入了在hello.c中用到的库函数,如exit、printf、sleep、getchar等函数。

重定位过程如下:

  1.重定位节和符号定义:链接器将所有类型的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,这一步完成时,程序中每个指令和全局变量都有唯一的运行时地址;

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

  3.重定位条目:当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目。重定位条目放在.rel.text节。

5.6 hello的执行流程

执行hello后,第一个调用的程序是:_dl_start。地址是0x7f249d2b8df0

然后是_dl_init。地址是0x7f249d2c8c10

继续运行,进入_start。地址是0x4010f0

进入_libc_start_main。地址是0x7f249d0d0fc0

进入_cxa_atexit。地址是0x7f249d0f3e10

进入csu_init。地址是0x4011c0

进入_setjmp。地址是0x7f249d0efcb0

进入_sigsetjmp。地址是0x7f249d0efbe0

然后进入main函数。地址是0x401125

5.7 Hello的动态链接分析

动态链接采用了延迟加载的策略,即在调用函数时才进行符号的映射。使用全局偏移量表GOT+过程链接表PLT实现函数的动态链接。GOT是数据段的一部分,存放函数目标地址,为每个全局函数创建一个副本函数,并将对函数的调用转换成对副本函数调用。和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息,GOT[2]是动态链接器ld-linux.so模块中的入口点。PLT是代码段的一部分,使用GOT中地址跳转到目标函数。  

GOT运行时地址为0x403ff0,PLT的运行时地址为0x404000。

在edb中观察此处内存:

dl_init前:

dl_init后:

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

5.8 本章小结

介绍链接的概念与作用,在Ubuntu下将hello.o进行了预处理得到hello文件,经过查看elf格式、反汇编hello并与hello.o的反汇编结果对比、详细具体地分析了重定位的过程与结果、比较动态链接前后内存中相关项目的变化,观察到了链接的过程与作用。

6章 hello进程管理

6.1 进程的概念与作用

       概念:进程是对处理器、主存和I/O设备的抽象表示。是一个执行中的程序的实例,也是系统进行资源分配和调度的基本单位。一般情况下,包括文本数据、数据区域和堆栈。文本区域存储处理器执行的代码,数据区域存储变量和进程执行期间动态分配的内存,堆栈区域存储着活动过程中调用的指令和本地变量。

       作用:每次运行程序时,shell创建一新进程,在这个进程的上下文切换中运行这个可执行目标文件。应用程序也能够创建新进程,并且在新进程的上下文中运行它们自己的代码或其他应用程序。进程提供给应用程序的关键抽象:一个独立的逻辑控制流,如同程序独占处理器;一个私有的地址空间,如同程序独占内存系统。

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

作用:Shell是一个命令解释器,它解释用户输入的命令并且把它们送到内核。Linux系统上的所有可执行文件都可以作为Shell命令来执行。当用户提交了一个命令后,Shell首先判断它是否为内置命令,如果是就通过Shell内部的解释器将其解释为系统功能调用并转交给内核执行;若是外部命令或实用程序就试图在硬盘中查找该命令并将其调入内存,再将其解释为系统功能调用并转交给内核执行。

处理流程:

1.终端进程读取用户由键盘输入的命令行;

2.分析命令行,获取命令行参数,并构造传递给execve的argv向量;

3.检查第一个命令行参数是否是一个内置的shell命令,如果不是则fork创建子进程;

4.在子进程中,用2获得的参数,调用execve执行指定程序;

5.如果用户要求后台运行,则shell返回,否则shell使用waitpid等待作业终止后返回。

6.3 Hello的fork进程创建过程

 

我们在shell上输入./hello 2021110957 艾可翼 1,由于这不是一个内置的shell命令,所以shell会认为hello是一个可执行目标文件,找到当前所在目录下的可执行文件hello,并执行它。

shell通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是独立的一个副本,包括代码段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。

6.4 Hello的execve过程

fork之后,子进程调用execve函数,该函数在新创建的子进程上下文中加载并运行hello程序。execve函数加载并执行可执行目标文件filename,带参数列表argv和环境变量列表envp。只有发生错误时execve才会返回调用程序。因此execve调用一次且从不返回。

加载并运行hello步骤:

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

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

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

4.设置程序计数器PC,设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。

6.5 Hello的进程执行

一个进程执行他的控制流的一部分的每一时间段叫做时间片。处理器通常用某个控制寄存器的一个模式位来提供用户模式和内核模式的功能。设置了模式位时,进程就运行在内核模式中,该进程可以执行指令集中的任何指令,可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中,不允许执行特权指令。

上下文切换:操作系统内核使用一种称为上下文切换(context switch)的较高层形式的异常控制流来实现多任务。上下文切换机制是建立在较低层异常机制之上的。内核为每个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。

在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程的决定叫做调度。上下文切换的流程是:保存当前进程的上下文,恢复某个先前被抢占的进程被保存的上下文,最后将控制传递给这个新恢复的进程。过程如图所示:

对于hello程序。hello在刚运行时,内核为其保存一个上下文,进程在用户模式下运行,没有异常或中断信号的产生,hello就将一直正常的执行。如果产生了异常或者系统中断时,内核将启用调度器休眠当前进程,并在内核模式中完成上下文切换,将控制传递给其他进程。

当程序在执行sleep函数时,系统调用显式的请求让调用进程休眠,调度器抢占当前进程,并产生上下文切换,将控制转移到新的进程。计数器到时后,产生一个中断信号,中断当前正在运行的进程,进行上下文切换恢复hello的上下文信息,控制会回到hello进程中。

当循环结束后,程序调用getchar函数,hello之前运行在用户模式,进行read调用后掉入内核,内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且完成缓冲区到内存的数据传输后,引发一个中断信号,此时内核从其他进程上下文切换回hello进程。

6.6 hello的异常与信号处理

1.hello执行过程中出现的异常:

1)中断:来自I/O设备的信号,异步发生,中断处理程序对其进行处理,返回后继续执行调用前待执行的下一条代码;

2)陷阱:有意的异常,是执行一条指令的结果,调度后也会返回到下一条指令,用来调度内核服务进行操作,帮助程序从用户模式切换到内核模式;

3)故障:由错误引起,它可能被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则终止程序;

4)终止:不可恢复的致命错误造成的结果,处理程序会将控制返回到一个abort例程,该例程会终止这个应用程序。

2.可能出现的信号:

3.hello对各种信号的处理的分析

1)执行中乱按键盘,内容保存在缓冲区,hello结束后作为命令行参数

2)按下ctrl-c,会发送SIGINT信号,信号处理程序终止并回收进程

3)按下ctrl-z,shell收到SIGSTP信号,该信号处理函数是打印屏幕回显,将hello挂起,通过ps命令可以看到hello进程并未被回收,状态是Stopped,使用fg命令可以将其调至前台,此时shell程序首先打印hello的 命令行命令,然后继续打印剩下的信息。再按下ctrl-z,将进程挂起。这时候再执行一个hello程序,可以看到区别于第一个程序是[2]。

 

6.7本章小结

本章主要介绍了进程的概念和作用,阐述了shell的作用和处理流程。重点描述了hello进程调用fork函数创建子进程和execve函数执行进程的过程。最后分析了hello程序执行过程中可能会出现的异常以及对这些异常的处理。

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:逻辑地址指由程序产生的与段相关的偏移地址部分,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。就是hello.o里面的相对偏移地址。

线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。

虚拟地址: CPU启动保护模式后,程序运行在虚拟地址空间中。hello里就是的虚拟内存地址。

物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。hello在运行时虚拟内存地址对应的物理地址。

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

一个逻辑地址由段标识符和段内偏移量两部分组成。其中段标识符是一个16位长的字段,称为段选择符,前13位是一个索引号,后面3位包含一些硬件细节。

索引号,类似数组下标,它对应的“数组”就是段描述符表,段描述符具体描述了一个段地址,多个段描述符组成段描述符表。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。其中Base字段描述了一个段的开始位置的线性地址。

全局段描述符:放在全局段描述符表(GDT中),局部段描述符:放在局部段描述符表(LDT中)

给定一个完整的逻辑地址 [段选择符:段内偏移地址] ,段选择符T1等于0或1,分别转换到GDT或LDT中的段,再根据相应寄存器,得到其地址和大小,就有了一个数组了。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,把base + offset,就是要转换的线性地址。

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

页式管理将程序的逻辑地址空间划分为固定大小的页,物理内存划分为同样大小的页框。程序加载时,可以将任意页放入内存的任意页框。这个方法需要CPU的硬件支持,来实现逻辑地址和物理地址之间的映射。页式存储管理方式中地址结构由两部分组成,分别为VPN(虚拟页号)和VPO(虚拟页偏移量)。

页式系统中进程建立时,操作系统为进程的所有页分配页框,撤销时收回。程序运行时,进程可以动态的申请空间,因此操作系统需要为申请的空间分配物理页框。为了完成这些功能,操作系统必须记录系统内存中实际的页框使用情况,在进程切换时,正确的切换两个不同的进程地址空间到物理内存空间的映射。

 

每次将虚拟地址转换为物理地址时,都会查询页表判断一个虚拟页是否缓存在DRAM的某个地方,如果不在,通过查询页表条目可以知道虚拟页在磁盘的位置。页表将虚拟页映射到物理页。

当页面命中时,CPU硬件执行的步骤。

第1步:处理器生成一个虚拟地址,并把它传送给MMU。

第2步: MMU生成PTEA地址,并从高速缓存/主存请求得到它。

第3步:高速缓存/主存向MMU返回PTE。

第4步:MMU构造物理地址,并把它传送给高速缓存/主存。

第5步:高速缓存/主存返回所请求的数据字给处理器。

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

每次CPU产生一个虛拟地址,MMU就必须查阅一个PTE,以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会要求从内存多取一次数据,代价是几十到几百个周期。

如果PTE碰巧缓存在L1中,那么开销就下降到1个或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓冲器(Translation Lookaside Buffer,TLB)。TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相联度。用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。如果TLB有T=2^t个组,那么TLB索引(TLBI)是由VPN的t个最低位组成的,而TLB标记(TLBT)是由VPN中剩余的位组成的。

下面是TLB命中和不命中的示意图

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

以一级Cache为例。L1Cache有64组,组索引位s=6,每组有8行,由于每块大小为64B,故块偏移为6,标记位为40位。

具体过程如下:

1.组选择:取出虚拟地址的组索引位,将二进制组索引转化为一个无符号整数,找到相应的族;

2.行匹配:将虚拟地址的标记位与相应组中所有行的标记位进行比较,当虚拟地址标记位和高速缓存标记位匹配,且该行有效位为1,则高速缓存命中;

3.字选择:一旦高速缓存命中,我们就知道要找的字节在这个块的某个地方,块偏移位提供了第一个字节的偏移;

4.若不命中,则需要从L2中取出被请求的块,将新的块存储在组索引位指示的组中的一个高速缓存行中。放置策略如下:有空闲块直接放置,否则采用LRU策略进行替换。

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序替代当前程序。具体过程如下:

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

2.映射私有区域,为新程序的代码、数据、bss和堆栈区域创建新的区域结构,这些区域都是私有、写时复制的。代码和数据区域被映射为hello文件的.text节和.data节,bss区域是请求二进制0的,映射到匿名文件,堆栈区域也是请求二进制0的,初始长度为0;

3.映射共享区域:hello程序与共享对象libc.so链接,libc.so是动态链接的,映射到用户虚拟地址空间中的共享区域内;

4.设置程序计数器PC:设置当前进程上下文的程序计数器,使之指向代码区域的入口点。

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

当指令引用一个虚拟地址,而与该地址相对应的物理页面不在内存中,因此必须从磁盘中取出时,就会发生缺页故障。此时程序将控制传递给缺页处理程序,缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令。当指令再次执行时,相应的物理页面以及驻留在内存中了,指令就可以没有故障地运行完成了。

7.9动态存储分配管理

1.基本原理:

在程序运行时程序员使用动态内存分配器获得虚拟内存。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的。已分配的块显式的保留供应用程序使用。空闲块保持空闲,直到显式的被应用所分配。一个已分配的块保持已分配状态,直到其被释放。

2.类型:

  1)显式分配器:要求应用显式的释放任何已分配的块;

  2)隐式分配器:应用检测到已分配块不再被程序所使用,就释放这个块。

3.常用的策略和方法有四种:

1)首次适应:首次适应策略要求空闲区按其起始地址从小到大排列,当某一用户作业要求装入内存时,存储分配程序从起始地址最小的空间区开始扫描,直到找到满足该作业要求的空闲区为止。

2)循环首次适应:在查找空闲区时,不再每次从链首开始查找,而是从上一次找到的空闲区的下一个空闲区开始查找,直到找到一个能满足要求的空闲区为止,并从中划出一块与请求大小相等的内存空间分给该作业。

3)最佳适应:该策略总是把满足要求,又使最小的空闲区分配给请求作业,即在空闲区表中,按空闲区的大小从小到大排列,建立索引,当用户作业请求内存空间时,从索引表中找到第一个满足该作业的空闲区分给它。

4)最差适应:该策略总是把最大的空闲区分配给请求作业,空闲区表(空闲区链)中的空闲分区要按大小从大到小进行排序,自表头开始查找到第一个满足要求的空闲分区分配给作业。

Printf会调用malloc,请简述动态内存管理的基本方法与策略。

7.10本章小结

本章主要介绍了hello的不同地址空间及其相互转换,hello的页式管理,TLB与四级页表支持下VA到PA的变换过程,三级Cache支持下的物理内存访问。阐述了hello进程调用fork函数,execve函数的内存映射,缺页故障的处理流程和动态存储分配器的管理基本方法和策略。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

所有的I/O设备都被模型化为文件,所有的输入和输出都被当做相应文件的读和写完成,这种将设备优雅的映射成文件的方式,允许Linux内核引出一个简单的,低级的应用接口,称为Unix I/O,这使得所有的输入输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

Unix I/O接口:

1.打开文件:程序要求内核打开文件,内核返回一个小的非负整数(描述符),用于标识这个文件。程序只要记录这个描述符便能记录打开文件的所有信息;
  2.shell在进程的开始为其打开三个文件:标准输入、标准输出和标准错误;
  3.改变当前文件的位置:对于每个打开的文件,内核保存着一个文件位置k,初始为0.这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作显式地设置文件的当前位置为k;
  4.读写文件:一个读操作就是从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会触发一个称为EOF的条件,应用程序能够检测到这个条件,在文件结尾处没有明确的EOF符号;
  5.关闭文件:内核释放打开文件时创建的数据结构以及其占用的内存资源,并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因停止时,内核都会关闭所有打开的文件并释放他们的内存资源。

Unix I/O函数:

1.int open(char* filename, int flags, mode_t mode),open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位;
  2.int close(int fd),关闭一个打开的文件,返回操作结果;
  3.ssize_t read(int fd, void *buf, size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的实际传送的字节数量;
  4.ssize_t write(int fd, const void *buf, size_t n),write函数从内存位置buf赋值至多n个字节到描述符fd的当前文件位置。

8.3 printf的实现分析

Printf函数的函数体为

printf需要做的事情是:接受一个fmt的格式,然后将匹配到的参数按照fmt格式输出。printf用了两个外部函数,一个是vsprintf,还有一个是write。

vsprintf返回所打印字符串的长度,作用是格式化,接受确定输出格式的格式字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出。

而后调用write系统函数将buf中的i个字符输出到显示器上,在write函数执行时,陷阱-系统调用 int 0x80或syscall将字符串的ASCII码从寄存器复制到显卡的显存中,由字符显示驱动子程序通过ASCII码到字模库中找到点阵信息存储到vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

 

8.4 getchar的实现分析

 C 库函数int getchar(void)从标准输入stdin获取一个字符(一个无符号字符)。该函数以无符号 char 强制转换为 int 的形式返回读取的字符,如果到达文件末尾或发生读错误,则返回 EOF。getchar调用read系统函数,通过系统调用读取按键ASCII码,直到接受到回车键才返回。调用getchar函数需要对异步异常-键盘中断进行处理——键盘中断处理子程序。接受按键扫描码转成ASCII码,保存到系统的键盘缓冲区。当程序调用getchar(),程序等待用户输入。用户输入的每个字符实际上是一个中断,其触发事件为键盘按下,行为是将按下的对应字符保存到输入缓冲区。当按下的字符为回车时,中断处理程序将结束getchar并返回读入的第一个字符。

8.5本章小结

本章简单的介绍了Unux的I/O操作是如何进行的,以及hello中设计到的两个I/O函数printf和getchar是怎么实现的。

结论

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

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

回顾hello的一生,从他被程序员一字一字的从键盘敲进电脑,再通过P2P和020阶段变为hello.c,然后经预处理器(cpp)将外部库的内容取出合并入hello.i文件中,由编译器将hello.i翻译成文本文件hello.s,然后通过汇编程序将其翻译成机器语言指令,hello.s变为可重定位目标文件hello.o,再通过链接器将hello.o文件和动态链接库链接成为可执行目标程序hello,至此hello.c才可以真正在电脑上运行。

我们在shell中输入./hello并跟上两个参数:姓名,学号,shell进程会调用fork创建子程序,然后调用execve,execve调用启动加载器,映射虚拟内存,创建新的内存区域,并创建一组新的代码、数据、堆和栈段,程序开始运行。

CPU为其分配时间片,在一个时间片中,hello有对CPU的控制权,顺序执行自己的代码,MMU将程序中使用的虚拟内存地址通过页表映射成物理地址,printf会调用malloc向动态内存分配器申请堆中的内存,如果运行途中键入ctr-c则停止,如果运行途中键入ctr-z则挂起,最后shell父进程回收子进程。

我的感悟:

Hello的这一生从诞生到结束,在计算机各个部件的相互协作配合下,终于完美地完成了它的使命。通过学习这门课,我对计算机的内部实现有了更多了解,能够更好地编写系统性能优良的代码并且规避掉一些由底层机制带来的问题和异常。这也让我认识到,一个复杂的系统需要多方面的协作配合才能更好地实现功能。同时,计算机系统提供的一系列抽象使得实际应用与具体实现相互分离,可以很好地隐藏实现的复杂性,降低了程序员的负担,使得程序更加容易地编写、分析、运行。

附件

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

附件1:hello.c:源程序;

附件2:hello.i:预处理之后的文本文件;

附件3:hello.s:编译之后的汇编文件;

附件4:hello.o:汇编之后的可重定位目标文件;

附件5:hello:链接之后的可执行目标文件;

附件6:hello.elf:hello.o的elf格式,查看hello.o的各节信息;

附件7:hello.ob:hello.o的反汇编文件;

附件8:hello.out:hello的反汇编文件。

参考文献

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

[1] RANDALE.BRYANT, DAVIDR.O‘HALLARON. 深入理解计算机系统[M]. 机械工业出版社

[2] Randal E. Bryant David R. O’Hallaron. Computer Systems A Progammer’s Perspective Third Edition

[3] https://www.cnblogs.com/diaohaiwei/p/5094959.html

[4] 段页式存储管理方式详解段页式存储管理方式详解_水无垠ZZU的博客-CSDN博客 段页式存储管理方式

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

[6] https://www.cnblogs.com/xelatex/p/3491305.html

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值