哈尔滨工业大学CSAPP大作业-2022春

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业    人工智能(未来技术) 

学     号         7203610202      

班     级           2036015       

学       生          熊杨逸磊       

指 导 教 师            史先俊          

计算机科学与技术学院

2022年5月

摘  要

本文主要阐述了hello所经历的一生,从预处理开始,hello经历了编译、汇编、链接等过程,最终成为了可执行文件。通过Shell键入hello,将hello加载运行,8次输出hello+学号+姓名,当输入一些内置命令时作出反应,其他乱按时无任何反应。同时本文继续向下探讨了hello的存储管理和I/O管理,对计算机系统有了进一步了解。

关键词:计算机系统;预处理;编译;汇编;链接;进程管理;存储管理;I/O管理;                           

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

目  录

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

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

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

1.3 中间结果............................................................................................................ -5 -

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

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

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

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

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

2.4 本章小结............................................................................................................ - 7 -

第3章 编译................................................................................................................ - 9 -

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

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

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

3.4 本章小结.......................................................................................................... - 12 -

第4章 汇编.............................................................................................................. - 13 -

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

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

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

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

4.5 本章小结.......................................................................................................... - 18 -

第5章 链接............................................................................................................... - 20-

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

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

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

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

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

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

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

5.8 本章小结.......................................................................................................... - 27 -

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

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

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

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

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

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

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

6.7本章小结.......................................................................................................... - 33 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结........................................................................................................ - 41 -

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

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

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

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

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

8.5本章小结.......................................................................................................... - 46 -

结论............................................................................................................................ - 46 -

附件............................................................................................................................ - 47 -

参考文献.................................................................................................................... - 48 -

第1章 概述

1.1 Hello简介

P2P:即From program to process,由程序到进程的过程。以下是Hello的P2P过程:

1.利用Code blocks、Visual Studio等编程软件或Ubuntu中的文本编辑器对老师发下的hello.c模板按要求进行修改,得到hello.c的源程序(文本)。

2.利用C预处理器(C Preprocessor)/cpp对hello.c进行预处理,得到修改后的源程序hello.i(文本)。

3.利用C编译器(C Compiler)/ccl对hello.s进行编译,生成汇编程序hello.s(文本)。

4.利用C汇编器/as将hello.s翻译成一个可重定位目标程序hello.o(二进制)。

5.将hello.o和系统目标文件利用链接器ld合并起来,得到可执行目标文件hello(二进制)。

6.向shell输入./hello运行程序,shell利用fork函数创建一个带有自己虚拟地址空间的新进程(利用mmap函数创建虚拟内存),再用execve函数将内核控制权交给进程,在虚拟内存中映射,在当前进程的上下文中加载并运行一个程序。

020:即0 to 0,即程序从由无到有再到被清除的过程。以下是Hello的020过程:

1.程序员从0开始编辑hello的源程序,对它进行预处理、编译、链接等方式形成可执行文件hello,hello被系统加载运行。

2.hello运行结束后,系统清理程序运行时所占用的内存,释放运行程序时占用的物理资源如CPU、缓存等,程序运行的痕迹被系统清除,重新回到0。

1.2 环境与工具

硬件环境:Intel(R) Core(TM) i7-10510U CPU;1.8GHz;16G RAM;512G HD Disk 以上。

软件环境:Windows 10 家庭中文版;Vmware15.5.6;Ubuntu20.04.4 LTS 64位。

工具:EDB,CodeBlocks20.03 64位,Microsoft Visual Studio2022 64位,Objdump。

1.3 中间结果

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

1.4 本章小结

本章主要对hello的P2P和020过程进行了简单的讲解,并提供了实验时所使用的硬件、软件、工具和中间结果等信息。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。

作用:根据源代码的预处理指令修改代码,预处理器读取系统头文件的内容把它直接插入程序文本中,用实际值和代码替换宏和常量标识符,生成通常以.i作为文件扩展名的程序文本。

2.2在Ubuntu下预处理的命令

gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析

以上是hello.i的部分截图,通过查看hello.i的文件可以看到程序由23行扩展为3000多行,但是仍然是由C语言编写的。头文件的内容被加入文本中,hello.c中的宏被展开,大量声明的函数、定义的结构体、定义的变量等内容被加入文本中。

2.4 本章小结

本章主要介绍了预处理的概念、作用,对hello.c进行了预处理操作,得到hello.i文件,查看并简单分析了hello.i。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

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

作用:编译进行词法分析、语法分析、语义检查和中间代码生成、代码优化、目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析

3.3.1 汇编初始部分

.file:声明源文件文件名。

.text:.text定义一个代码段,处理器开始执行代码的地方,代表后面是代码。

.section .rodata:只读数据段。

.align 8:以8位对指令和数据的存放地址进行对齐。

.global:声明全局变量。

.type:声明一个符号的类型。

.string:声明一个字符串。

3.3.2 数据

1.字符串:程序中的两个字符串都被存储在只读数据段中:

main函数中的第二个参数为一个数组,数组的每一个元素都是指向字符串类型的指针。数组的起点在栈中的-32(%rbp)的位置,被两次调用找参数调给printf函数:

2.整数类型int argc:main函数中的第一个参数,是用户传给main函数的参数,被存在栈中:

3.局部变量i:main函数定义了一个局部变量i,编译器编译时会把局部变量放入栈中,i被存放在-4(%rbp)中:

4.立即数:立即数直接体现在文本中,如argc != 4中的4:

   

3.3.3 赋值

hello中的赋值操作为对局部变量赋值:i = 0。赋值语句通过mov指令来实现:

3.3.4 类型转换

hello中利用atoi函数将argv[3]中的字符串转换成整数类型:

3.3.5 算术操作

hello中的算术操作为对已赋初值的局部变量i实施i++的操作,即i = i + 1,i是int类型,在汇编代码中用addl实现此操作:

3.3.6 关系操作

1.hello中if(argv != 4)是一个条件跳转判断语句,其被编译为cmpl $4, -20(%rbp),如果argv=4就跳转至.L2:

2.hello中for语句中i < 8循环的条件,其被编译为cmpl $7, -4(%rbp),如果i <= 7时就跳转至.L4:

3.3.7 数组操作

hello中的数组操作为数组char *argv[]:main函数中的第二个参数为一个数组,数组的每一个元素都是指向字符串类型的指针。数组的起点在栈中的-32(%rbp)的位置:

3.3.8 控制转移

汇编语言中设置了条件码,然后根据条件码来进行控制程序的跳转。

1.if条件判断语句中判断agrv是否等于4,不等于继续执行.LFB6的指令,等于则跳转至.L2:

2.先对i赋初值,然后无条件地跳入判断.L3中,再进入for循环语句中判断i是否小于等于7,是的话跳转至.L4,不是的话继续执行.L3的指令:

3.3.9 函数操作

hello中涉及的函数有:main函数、printf函数、exit函数、sleep函数、atoi函数、getchar函数。

各个函数的参数:main:argc、argv[];printf:字符串;exit:1;sleep:atoi(argv[3]);atoi:agrv[3];getchar:空。

各个函数的返回值:所有函数的返回值都被放入了%eax寄存器中。

各个函数的参数传递(值/地址):先调用一个寄存器,将参数的值传入寄存器,再用call指令跳转到调用函数的地址。

3.4 本章小结

本章主要介绍了编译的概念、作用,对hello.i进行了编译操作,得到hello.s文件,文件由C语言文件变为汇编语言文件,分别从数据、赋值、类型转换、算术操作、关系操作、数组/指针/结构操作、控制与函数操作等方面对hello.s进行分析,进一步理解编译的机制。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

概念:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并把结果保存在目标文件hello.o中。Hello.o文件时一个二进制文件,它包含的17个字节是函数main的指令编码。

作用:通过汇编过程将汇编语言转换成机器指令,使其在链接(ld)后能被机器识别并执行。

4.2 在Ubuntu下汇编的命令

gcc -c hello.s -o hello.o

在Ubuntu中生成hello.o的ELF格式:readelf -a hello.o > hello.elf

1.ELF头:ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的子的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位的、可执行或共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。

2.节头:记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。

3.重定位节:重定位是连接符号引用与符号定义的过程。例如,程序调用函数时,关联的调用指令必须在执行时将控制权转移到正确的目标地址。可重定位文件必须包含说明如何修改其节内容的信息。通过此信息,可执行文件和共享目标文件可包含进程的程序映像的正确信息。重定位项即是这些数据。

.rela.txt保存的是.txt节需要重定位的内容。本程序要被重定位的是.rodata中的.LC0和.LC1、puts、exit、printf、atoi、sleep、getchar。

.rela.eh_frame是.eh_frame的重定位信息。

4.符号表:.symtab,一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。每个可重定位目标文件在.symtab中都一张符号表。与编译器中的符号表不同,.symtab符号表不包含局部变量的条目。

4.4 Hello.o的结果解析

利用objdump -d -r hello.o > hello_o.text将hello.o的反汇编文件输入在文本中:

机器语言和汇编语言的不一致之处:

1.分支转移函数:

hello.o反汇编后分支跳转操作数是对main的相对地址,而hello.s则是段名称,这是因为段名称只是汇编语言中便于书写的帮助记忆的标符,实际中并不存在,转化为机器语言后再反汇编自然只能得到地址:

hello.s:

hello.o反汇编文件:

2.立即数:

立即数由十进制数变为十六进制数,对于机器来说十六进制显然比十进制更方便,而对于编写汇编语言的人来说十进制更好理解。

hello.s

hello.o反汇编文件:

3.对函数的调用:

call后调用的操作数不再是具体的函数名称,而是相较main函数的地址偏移量,同时下面标注有函数名称

hello.s:

hello.o反汇编文件:

4.5 本章小结

本章主要介绍了汇编过程的概念、作用,对hello.s进行了汇编,得到hello.o,分析了hello.o的ELF头、各节节头、节头部表、符号表和重定位节,比较了hello.s和hello.o反汇编的不同之处,分析了机器语言和汇编语言的映射关系。

(第41分)

5章 链接

5.1 链接的概念与作用

概念:链接(linking)是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可用于编译时、加载时甚至运行时。

作用:链接使得分离编译成为可能。不用将一个大型的应用程序组织为一个巨大的源文件,可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。

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

再利用objdump -d -r hello.o > hello.text将hello的反汇编文件输入在文本中。

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

利用readelf -a hello > hello out.elf生成hello的ELF文件:

1.ELF表头:hello从hello.o的REL(可重定位文件)变成了EXEC(可执行文件),其入口点地址由0x0变为0x4010f0,程序头起点由0变为64,Start of section headers、Size of program headers、Number of program headers、Number of section headers、Section header string table index也都有了不同程度的改变。

   

2.节头:记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。

3.重定位节:.rela.dyn、.rela.plt、.dynsym

4.符号表:.symtab

5.4 hello的虚拟地址空间

使用edb加载hello:

根据ELF文件中程序头的分析,可知程序入口在0x0000000000400000:

通过Data Dump查看各段的虚拟地址情况:

通过将ELF文件和Data Dump交互分析可知虚拟地址开始于0x400000,终止于0x404048:

查看表头可以得到各节的虚拟地址和大小,并进入edb进行验证,如.txt节起始地址为0x4010f0,大小为145:

其余各节也能用上述方法验证虚拟地址和大小。

5.5 链接的重定位过程分析

objdump -d -r hello > hello.txt,已在5.2中给出截图。

将hello反汇编文件和hello.o的反汇编文件进行对比可以得出:

1.hello已完成重定位工作,因为hello的反汇编文件中有确定的虚拟地址,而hello.o的反汇编文件中地址均为0x00000:

2.hello反汇编文件比hello.o反汇编文件多出了许多节、函数,而hello.o的反汇编文件只有main函数,这是链接之后的结果:

以上可知hello中是通过给定具体的虚拟地址和添加必要的节、函数(库函数)来实现重定位。下面列举一些节的作用:

.init:程序初始化需要的代码

.interp:保存ld 动态库的路径

.plt:动态链接-过程链接表

.rela.plt:.plt的重定位文件

hello的重定位过程:

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

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

两个概念:

重定位条目:当汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中。

重定位符号引用:链接器的重定位算法的伪代码如下:

5.6 hello的执行流程

1.加载hello

2.开始执行hello:_start call __libc_start_main

3.执行main:puts@plt、printf@plt、getchar@plt、atoi@plt、sleep@plt

4.终止hello:exit@plt

程序名   ——   程序地址

_start            04010f0

__libc_start_main  0x2ed2(%rip)

puts@plt         0401090

printf@plt        04010a0

getchar@plt      04010b0

atoi@plt         04010c0

sleep@plt        04010e0

exit@plt         04010d0

main            0401125

5.7 Hello的动态链接分析

动态链接:共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接。编译器没有办法预测一个由共享库定义的函数运行时的地址,因为定义它的共享模块在运行时可以加载到任意位置。GNU利用延迟绑定技术解决该问题,延迟绑定用到两个数据结构过程链接表(PLT)、全局偏移量表(GOT)。

通过查看ELF表表头,查到got的相关信息:

可以看到GOT在调用dl_init之前的0x0403f0的之后的16个字节均为0:

调用dl_init之后GOT、GOT.PLT发生改变:

5.8 本章小结

    本章主要介绍了链接的概念和作用,并对hello.o实行了链接,得到了hello。通过将hello的反汇编文件和hello.o的反汇编文件进行对比,进一步分析了重定位的过程。将hello加载edb中,分析hello的虚拟空间地址以及对hello的动态链接进行分析。

(第51分)

6章 hello进程管理

6.1 进程的概念与作用

概念:一个执行中程序的实例。

作用:运行程序时,会得到一个假象,就好像我们的程序是系统中当前运行的唯一的程序一样。程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条地执行我们程序中的指令。最后,我们程序中的代码和数码好像是系统内存中唯一的对象。

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

作用:shell是一个交互型的应用级程序,它代表用户运行其他程序。

处理流程:

1.读取从键盘输入的命令;

2.判断命令是否正确,且将命令行内的参数改造为系统调用函数execve函数内部处理所要求的形式;

3. 终端进程调用fork函数来创建子进程,自身则调用wait函数来等待子进程完成;

4.当子进程运行时,它调用execve根据命令的名字指定的文件到目录中查找可行性文件,调入内存并执行这个文件;

5.如果命令行末尾有后台命令符号&,终端进程不执行等待系统调用,而是立即发命令符。让用户输入下一条命令;如果命令行末尾没有后台命令符号&,则终端进程要一直等待。当子进程完成处理后,向父进程报告,此时终端进程被唤醒,做完必要的判断工作之后,再发提示符,让用户输入新的命令。

6.3 Hello的fork进程创建过程

终端通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全和父进程相同。子进程得到与父进程用户级虚拟地址空间相同但是独立的一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同PID。

以hello为例,当向终端输入./hello 7203610202 熊杨逸磊 1时,shell解析命令行中输入的命令不是内置命令,所以调用fork函数创建子进程。

6.4 Hello的execve过程

当创建了子进程了之后,execve函数在当前进程的上下文中加载并运行hello程序:

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

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

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

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

6.5 Hello的进程执行

下面先介绍几个概念:

1.逻辑控制流:进程可以向每个程序提供一种假象,好像它在独占地使用处理器。如果想单步执行程序,会看到一系列的程序计数器(PC)的值,这个PC值得序列叫做逻辑控制流。下图中的关键点在于进程是轮流使用处理器的。每个进程执行它的流的一部分,然后被抢占(暂时挂起),然后轮到其他进程。

2.并发流:一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发地运行。如果两个流并发地运行在不同的处理器核或者计算机上,那么称它们为并行流,它们并行地运行。

3.时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。

4.私有地址空间:进程为每个程序提供一种假象,好像它独占地使用系统地址空间。一般而言,和这个空间某个地址相关联的那个内存字节是不能被其他进程读或写的,从这个意义上说,这个地址空间是私有的。

5.用户模式和内核模式:处理器通常用控制寄存器的一个模式位来提供两种模式。当设置了模式位时,进程就运行在内核模式中,其可以执行指令集中的任何指令,并且可以访问系统中的任何位置;当没有设置模式位时,进程就运行在用户模式中,此时进程不允许执行特权指令,用户程序必须通过系统调用接口间接地访问内核代码和数据。

6.上下文切换:内核为每个进程维持上下文。上下文就是内核重新启动一个被抢占地进程所需的状态,包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构。上下文切换首先保存当前进程的上下文,然后恢复某个先前被抢占的进程被保存的上下文,最后将控制传递给这个新恢复的程序。

接下来分析hello的进程执行:在进程调用execve函数,先为hello进程分配了一个私有地址空间,将hello的代码和数据分别映射在.text和.data中。一开始hello进程运行在用户模式,正常输出Hello 7203610202 熊杨逸磊。然后,进程调用了sleep函数,进程进入内核模式,内核处理休眠状态,把进程挂起,等待请求的时间量,同时将控制权由hello交给其他进程。当请求的时间量已经到了,hello进入用户模式,夺回控制权,继续运行。当hello调用getchar时,初始运行在用户模式种,直到它通过执行系统调用read陷入到内核。内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且安排在键盘缓冲区完成从键盘到内存的数据传输后,键盘中断处理器,此时hello又回到用户模式,夺回控制权。

   

6.6 hello的异常与信号处理

1.出现的异常:

中断:执行hello时,来自I/O设备的信号中断运行,如键盘输入:Ctrl+c;

陷阱:hello调用sleep、getchar函数会出现;

故障:hello执行时可能有潜在可恢复的错误;

终止:hello执行时发生不可恢复的错误。

2.产生的信号:

SIGINT:来自键盘的中断,如输入Ctrl+c;

SIGKILL:程序被杀死;

SIGALRM:来自alarm函数的定时器信号,在sleep函数调用时会用到;

SIGCHILD:一个子进程即hello停止或终止;

SIGTSTP:从终端停止hello。

3.处理方法:

(1)键盘输入Ctril-z后进程被停止,挂入后台:

(2)键盘输入ps后可以查看进程,可以看到hello并没有被回收,而是在后台中:

(3)键盘输入jobs后看到作业列表中有hello,其job号为1,已经停止了:

(4)键盘输入fg 1将hello放入前台,hello继续运行直到一共打印8条Hello 7203610202 熊杨逸磊:

(5)键盘输入Ctrl+c,内核向进程发送一个SIGINT信号,进程被中断:

(6)键盘不停乱按对hello进程的输出并未有任何影响:

6.7本章小结

本章主要介绍了进程的概念和作用,简述壳Shell-bash的作用与处理流程,讲述了hello的fork创建子进程的过程和hello的execve过程,分析了hello的进程执行情况,对hello执行中可能出现的异常和信号进行了简单分析,并且键盘输入多种指令,查看指令输入后hello进程的执行情况。

(第61分)

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。由两部份组成,段标识符和段内偏移量。

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

虚拟地址:即线性地址。

物理地址:放在寻址总线上的地址。放在寻址总线上,如果是读,电路根据这个地址每位的值就将相应地址的物理内存中的数据放到数据总线中传输。如果是写,电路根据这个地址每位的值就在相应地址的物理内存中放入数据总线上的内容。物理内存是以字节(8位)为单位编址的。

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

逻辑地址=段选择符+偏移量,逻辑地址一共有 48 位。前 16 位是段选择符。可以通过段选择符,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。段描述符存放在描述符表中,也就是GDT或LDT中。通过查看段选择符的TI来判断选择哪个描述符表,TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)。被选中的段描述符先被送至描述符cache,每次从描述符cache中取32位段基址,与32位段内偏移量(有效地址)相加得到线性地址。

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

线性地址即虚拟地址(VA)到物理地址(PA)的变换通过分页方法来进行。VM系统通过将虚拟内存分割为称为虚拟页的大小固定的块,类似地,物理内存被分割为物理页。CPU芯片上叫做内存管理单元(MMU)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址。

具体执行操作如下:

当页命中时:处理器生成一个虚拟地址,并将其传送给MMU;MMU 使用内存中的页表生成PTE地址;MMU 将物理地址传送给高速缓存/主存;高速缓存/主存返回所请求的数据字给处理器。

当发生缺页时:处理器将虚拟地址发送给 MMU;MMU 使用内存中的页表生成PTE地址;有效位为零, 因此 MMU 触发缺页异常;缺页处理程序确定物理内存中牺牲页 (若页面被修改,则换出到磁盘);缺页处理程序调入新的页面,并更新内存中的PTE;缺页处理程序返回到原来进程,再次执行缺页的指令。

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

翻译后备缓冲器(TLB):每次CPU产生一个虚拟地址,MMU就必须查阅一个PIE,以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会要求从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就下降到1个或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓冲器。

多级页表:虚拟地址被划分成k个VPN和1个VPO。每个VPNi都是一个到第i级页表的索引。

TLB与四级页表支持下的VA到PA的变换:开始时,MMU从虚拟地址中抽取出VPN,并检查TLB,看它是否因为前面的某个内存引用缓存了PTE的一个副本。TLB从VPN中抽取出TLBT和TLBI,进行匹配,如果命中,将缓存的PPN返回给MMU,得到PPN和VPO组成的PA;如果不命中,TLB 中没有命中,MMU 向页表中查询确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出 PTE,如果在物理内存 中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查 询到 PPN,与 VPO 组合成 PA,并且向 TLB 中添加条目。如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。

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

Cache1和Cache2、Cache3工作方式相同,故以Cache1为例:

因为每个页面是2^6 = 64字节,所以L1 Cache的低6位作为PPO,高6位作为PPN。这样L1 Cache每个块中都是4个字节,所以块的低2位作为块偏移(CO),接下来4位来表示组索引(CI),剩下的作为标记(CT)。

MMU发送物理地址给L1 Cache,L1 Cache从物理地址中取出CO、CI、CT。如果标记与CT相匹配,为命中,读出CO处的数据字节,把它返回给MMU,MMU把它传递回CPU。如果不命中,那么需要从存储层次结构中的下一层取出被请求的块,然后将新的块存储在组索引位所指示的组中的一个高速缓存行中。一种简单的 放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块, 产生冲突(evict),则采用最近最少使用策略 LFU 进行替换。

7.6 hello进程fork时的内存映射

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

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

7.7 hello进程execve时的内存映射

当创建了子进程了之后,execve函数在当前进程的上下文中加载并运行hello程序:

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

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

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

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

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

缺页故障:当指令引用了一个虚拟地址,而与该地址相对应的物理页面不在内存中,因此必须从磁盘中取出时,就会发生故障。

缺页中断处理:缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中了,指令就可以没有故障地运行完成了。具体来说,就是若操作合法,则缺页处理程序从物理内存中确定一个牺牲页,若该牺牲页被修改过,则将它换出到磁盘,换入新的页面并更新页表。当缺页处理程序返回时,CPU 再次执行引起缺页的指令,将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,主存将所请求字返回给处理器。

7.9动态存储分配管理

当进程运行时需要额外虚拟内存时,用动态内存分配器更方便、可移植性更好。

动态内存分配器维护着一个称为堆的进程的虚拟内存区域。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放可以由应用程序显式执行或内存分配器自身隐式执行。

分配器有两种基本风格:

显式分配器:要求应用显式地释放任何已分配的块,比如说C标准库提供一种叫做malloc程序包的显示分配器。

隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的过程叫做垃圾收集。

显式分配器的要求和目标:

1.处理任意请求序列;2.立即响应请求;3.只使用堆;4.对齐块;5.不修改已分配的块。

目标1:最大化吞吐率;目标2:最大内存利用率。

隐式空闲链表:

假设块的格式如下图所示,我们可以将堆组织为一个连续的已分配块和空闲块的序列,这种结构被称为隐式空闲链表:

(1)当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大可以放置所请求块的空闲块。分配器执行这种搜索方式是由放置策略决定的。

(2)分割空闲块:一旦分配器找到一个匹配的空闲块,它就必须做另一个策略决定,那就是分配这个空闲块中多少空间。

(3)获取额外的堆内存:如果分配器不能为请求块找到合适的空闲块时,一个选择是通过合并那些在内存中物理上相邻的空闲块来创建一些更大的空闲块;如果上一个选择行不通,分配器就会通过调用sbrk函数,向内核请求额外的堆内存。分配器将额外的内存转化成一个大的空闲块,将这个块插入到空闲链表中,然后将被请求的块放置在这个新的空闲块中。

(4)合并空闲块:当分配器释放一个已分配块时,可能会引起一种现象,叫做假碎片,就是许多可用的空闲块被切割成为小的、无法使用的空闲块。为了解决这个问题,任何实际的分配器都必须合并相邻的空闲块,这个过程称为合并。利用边界标记,允许在常数时间内进行对前面块的合并。

显式空闲链表:显示空闲链表是将空闲块组织为某种形式的显示数据结构。堆可以组织成一个双向空闲链表,每个空闲块中,都包含一个pred(前驱)指针和succ(后继)指针。

使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线 性时间减少到了空闲块数量的线性时间。一种方法使用后进先出的顺序维护链表,将新释放的块在链表的开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要 线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比 LIFO 排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。一般而言,显式链表的缺点是空闲块必须足够大,这就导致了更大的最小块。

7.10本章小结

本章主要介绍hello的存储器地址空间、四种地址,阐述了Intel逻辑地址到线性地址的变换-段式管理、Hello的线性地址到物理地址的变换-页式管理、TLB与四级页表支持下的VA到PA的变换、三级Cache支持下的物理内存访问、hello进程fork时的内存映射、hello进程execve时的内存映射、缺页故障与缺页中断处理以及重点介绍了动态存储分配管理。

(第7 2分)

8章 hello的IO管理

8.1 Linux的IO设备管理方法

一个Linux文件就是一个m字节的序列:

B0,B1,B2……Bm

所有的 IO 设备(如网路、磁盘、终端)都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都被当做相应文件的读和写来执行。

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

Unix I/O 接口:

1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。

2.Shell 创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。头文件< unistd.h> 定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可用来代替显式的描述符值。

3.改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行 seek,显式地将改变当前文件位置 k。

4.读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文 件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文 件,当 k>=m 时,触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。类似一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。

5.关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

Unix I/O 函数:

1.int open(char* filename,int flags,mode_t mode) ,进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。 open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。

2. int close(fd),fd是需要关闭的文件的描述符。

3. ssize_t read(int fd,void *buf,size_t n),read 函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1 表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。

4. ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。

8.3 printf的实现分析

查看windows系统下的printf.c:

在形参列表里有这么一个:...,这个是可变形参的一种写法。当传递参数的个数不确定时,就可以用这种方式来表示。

va_list arg = (va_list)((char*)(&fmt) + 4);

va_list的定义:

    typedef char *va_list

这说明它是一个字符指针。其中的: (char*)(&fmt) + 4) 表示的是...中的第一个参数。

继续来看vsprintf(buf, fmt, arg)是什么函数:

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

write,写操作,把buf中的i个元素的值写到终端。

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

sys_call的实现如下:

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或sys_call将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码,进一步字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar函数代码实现如下:

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

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

8.5本章小结

本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了 printf函数和 getchar函数的实现。

(第81分)

结论

hello所经历的过程:

  1. 编程:通过编程软件编写出hello.c;
  2. 预处理:预处理hello.c得到hello.i;
  3. 编译:编译hello.i得到hello.s;
  4. 汇编:汇编hello.s得到hello.o;
  5. 链接:链接hello.o与可重定位文件、动态链接库得到hello;
  6. 加载运行:通过Shell输入./hello 7203610202 熊杨逸磊 1,内核调用fork函数为hello创建一个子进程,调用execve函数把代码和数据载入虚拟空间,开始执行;
  7. 上下文切换:hello调用sleep函数后进入休眠状态,内核通过上下文切换将控制权给其他进程,hello到达设定的时间量后,内核再通过上下文切换将控制权还给hello;
  8. 动态内存申请:hello调用printf函数之后,malloc函数会向动态内存分配器申请堆中的内存;
  9. 接收信号:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
  10. 终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有 数据结构。

深切感悟:

计算机系统通过抽象层面解决问题:如,计算机系统通过虚拟内存这一上层的抽象概念进一步更加合理分配内存、以及读取数据。

计算机系统通过多层次或分级提高效率:如,计算机将常用的数据放入高速缓存;又比如,流水线多条执行指令提高CPU使用效率。

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

附件

hello.i:hello预处理后的文件

hello.s:hello.i编译后的文件

hello.o:hello.s汇编后的文件

hello:hello.o链接后的文件

hello.elf:hello.o的ELF文件

hello_o.text:hello.o的反汇编文件

hello out.elf:hello的ELF文件

hello.text:hello的反汇编文件

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

参考文献

[1]readelf命令使用说明:readelf命令使用说明_木虫下的博客-CSDN博客_readelf

[2]重定位节:重定位节 - 链接程序和库指南

[3]Shell流程详解:Shell执行流程详细解释 - 百度文库

[4]逻辑地址:逻辑地址_百度百科

[5]线性地址:线性地址_百度百科

[6]物理地址:物理地址(CPU中相关术语)_百度百科

[7] Randal E.Bryant, David R.O'Hallaron 深入理解计算机系统[M]. 北京:机械工业出版社,1992:25-42.

哈尔滨工业大学(Harbin Institute of Technology,简称“哈工大”)是中国著名的重点大学,成立于1920年,是中国最早创办的六所工科高等学府之一。其中,哈尔滨工业大学的计算机科学与技术学院一直以来都是国内知名的学院。在其中,CSAPP是哈工大计算机科学与技术学院开设的一门经典课程,全称为《深入理解计算机系统》(Computer Systems: A Programmer's Perspective)。 这门课程涵盖了计算机系统的各个方面,从高级语言编程到机器级别的细节都有涉及,深入剖析了计算机系统的内部机制,讲解了各种计算机组件的原理,如内存、处理器、I/O设备、网络等等。此外,课程内容还包括缓存、异常、程序优化、并发编程、虚拟内存等重要主题,并且还会涉及安全问题,例如注入攻击、缓冲区溢出等等。 相较于其他计算机相关的课程而言,CSAPP的特殊之处在于,它以程序员的视角,深入而生动地解释了计算机系统的工作方式和内部机制。课程强调了实践性,通过大量的例子及编程作业,学生可以实际操作并理解到具体的计算机系统的运行方式。 此外,CSAPP的教学团队非常强大,由哈工大的多位顶尖教授组成,能够保证教学质量和深度。学生通过学习这门课程,不仅可以深入了解计算机系统的各个方面,还可以提高编程能力和工程实践水平,有助于更好地应对工作中遇到的各种问题。 总之,CSAPP是哈尔滨工业大学计算机科学与技术学院开设的一门经典课程,其全面而深入的课程内容、强调实践性、优秀的教学团队等特色让其在国内享有较高声誉,对学生深入理解计算机系统、提高编程实践能力等方面,都有非常积极的作用。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值