hello的一生

关键词:计算机系统;功能;流程;P2P;O2O;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简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

屏幕前的你,你好,我叫hello,我接待过每一个编程初学者,他们中的大多数人在看了我第一眼后,就将我抛弃了。我仿佛看到他们的眼神中从当初的向往、好奇、惊喜,再到迷惑、无奈、失望,最后和我的其他小伙伴sum、sort、matrix等玩耍去了。在这里,我对他们的轻视表示无比的气愤,殊不知,我曾经也辉煌过,我可是第一个经历P2P和O2O的程序啊!

P2P:将写好的程序保存成源程序(文本)hello.c(program)。通过预处理器(cpp)生成hello.i文件,再通过编译器(cc1)生成汇编程序(文本)hello.s,经过编译器的处理后生成可重定位目标程序(二进制)hello.o,最后hello.o和printf.o在链接器(ld)作用下生成可执行目标程序(二进制)hello,就此Hello诞生了。然后我们通过在bash中运行它,OS为这个Hello fork一个进程(process)。

O2O:然后在这个子进程中使用execve指令运行它,使用mmap函数为其创建一个新的虚拟内存区,OS再为这个进程分配时间片,使得Hello可以在X86-64上进行取指、译码、执行。对Hello进行内存映射,开始运行,从物理内存中取出代码与数据,通过IO在屏幕上输出信息,执行完后由bash回收,结束Hello的一生。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

X64 CPU;2GHz;2G RAM;256GHD Disk;

Windows10 64位;

Vmware Workstation Pro;Ubuntu 16.04 LTS 64位

gcc,ld,readelf,objdump,gdb,edb

1.3 中间结果

hello.ihello的预处理文件

hello.shello的汇编文件

hello.ohello的可重定位文件

hellohello的可执行文件

o_elf.txtreadelf -a hello.o重定向生成的文本文件

o_obj.txtobjdump -d -r hello重定向生成的文本文件

x_elf.txtreadelf -a hello重定向生成的文本文件

x_elf.txtobjdump -d -r hello重定向生成的文本文件

1.4 本章小结

通过hello的自白,我们大致了解了,从hello.c被编写,经过预处理、编译、汇编、链接生成可执行文件Hello,然后在shell中运行,最后被系统回收,结束它的一生。这不仅意味着一个小小程序hello的一生(P2P,O2O),还代表着计算机科学探索路上的艰辛与无时无刻不散发出的智慧之光。

(第1章0.5分)


第2章 预处理

2.1 预处理的概念与作用

概念:预处理器根据以字符#开头的命令,修改原始的C程序。比如hello.c中第一行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。

作用:因为有了预处理这步,所以程序员在编写一些代码时,可以很方便地加上一些头文件,然后调用其中的函数,这样可以提高编写的效率,同时也增加了代码的简洁程度。然而编写的程序并没有完全使用调用的库中的所有函数,所以预处理可以帮助找到编写的程序中所引用的一些函数的定义,同时也可以检查一些错误,比如使用了未定义的函数、变量等。在预处理后,生成的.i文件中每一个引用都有其定义,这保证了这个程序的完整性,便于后续操作。

2.2在Ubuntu下预处理的命令

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

2.3 Hello的预处理结果解析

第一页

最后一页

可见原先的三个头文件都被替换了。

2.4 本章小结

预处理阶段看上去并没有什么高深过人之处,不过是简单的替换嘛,但事实上,预处理虽然简单但是很重要,它是hello一生中的第一道工序,为后续的编译、汇编、链接等操作打下基础。正是预处理这样笨拙的替换使得我们可以非常方便地在编写程序时调用一些函数、常量。

(第2章0.5分)


第3章 编译

3.1 编译的概念与作用

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

概念:编译器(cc1)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。汇编将我们人类比较好懂的变成语言翻译成与机相近的汇编语言。

作用:它为不同的高级语言的不同编译器提供了通用的输出语言,且贴近机器语言。

3.2 在Ubuntu下编译的命令

应截图,展示编译过程!

3.3 Hello的编译结果解析

此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析

3.3.1全局变量,赋值

sleepsecs是一个赋非零值的全局变量,保存在.data段。

定义了一个强全局符号main,参数argc和argv。

main作为主函数保存在.text段中

argc保存在寄存器rdi中,argv保存在寄存器rsi中,它们俩都在.rodata段

3.3.2局部变量

局部变量i保存在栈上,且未赋初值。

可以通过movl指令来将立即数0($)赋给i。

movl $0,4(%rsp)

3.3.3 if语句和‘!=’

这里的条件语句判断,通过cmple比较来实现。

如果等于3,则跳转,否则向下执行,最后调用exit(1)退出。

3.3.4 for语句,‘<’,‘++’

for语句的实现:一开始i是0,addl $1,-4(%rbp),让i自增,然后

cmpl $9,-4(%rbp)来生成条件码,最后通过jle 来条件跳转。

‘<’是通过cmpl指令来实现的。

这里的i++操作,可以通过addl $1指令或INC来对i进行自增。

addl $1 -4(%rbp)

3.3.5调用函数

从反汇编文件我们可以看出,由于输出的内容是一个纯文本,所以系统自动优化为puts。

%rdx,%rdi,%rsi为printf参数,然后调用printf。

这里值得注意的是,用全局变量sleepsecs作为sleep函数的参数,所以需要用内存操作数,先将sleepsecs赋给寄存器,再作为参数,最后调用sleep。

可以看到当jle条件不满足时,调用getchar。

这里把立即数1赋给%edi,然后exit退出。

与exit类似的,把1赋给%eax,然后ret退出。

3.3.6类型隐式转换

这里sleepsecs的值为2.5,是个float类型,但由于函数sleep()的参数要求是int类型的,所以float类型隐式转换成了int类型,小数部分舍去,2.5变成了2。

3.4 本章小结

这一章描述了关于编译的概念与作用,其命令行,以及对一些C语言的数据与操作的解析。在这个文件中我们可以很常见的看到汇编代码,这个一种很底层的代码,这种代码虽然简单,笨拙,但若我们能很好地读懂它们,理解它们这么写的原理,那么我们离计算机底层的一些工作原理就不远了。总之,汇编代码是我们理解计算机系统路上一个强有力的工具。

(第3章2分)


第4章 汇编

4.1 汇编的概念与作用

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

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

4.2 在Ubuntu下汇编的命令

应截图,展示汇编过程!

4.3 可重定位目标elf格式

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

read -a hello.o

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

ELF头

这个ELF头表明这是个64位的机器,字节顺序为小端序,可重定位,入口地址0x0,节头大小64字节。

节头表

重定位部分

重定位部分有.rela.text和.rela.eh_frame。

由上图可知,.rodata段中的两个字符串,puts,exit,printf,sleepsecs,sleep,getchar需要重定位。最左边的是偏移量,最右边的是addend。对于重定位一个使用32位PC相对地址的引用时。如果已经获得具体的最终运行时地址,和对应段的位置后,你可以通过偏移量和addend来推算出最终重定位引用的十六进制值。而对于绝对引用,则重定位引用的十六进制值就是最终运行时地址加上addend。

符号表

4.4 Hello.o的结果解析

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

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

hello.o的反汇编

hello.s

分支转移:在hello.s中采用.L1 .L2的方式来跳转,而在hello.o的反汇编中是通过main和相对偏移量来进行跳转的

函数调用:我们在hello.s中可以看到,当调用一个函数时,比如printf时,后面为@PLT,而在hello.o的反汇编中是通过main和相对偏移量来调用函数的,并且地址为0,需要重定位,因为是外部定义的函数,需要在链接时才能确定地址。

对字符串的取址操作:在hello.s中对printf时用.LC0来表示字符串地址,而hello.o反汇编中用0表示,因为还没重定位,无法确定其地址。

4.5 本章小结

通过汇编器(as),我们从hello.s获得了hello.0可重定位目标文件,我们可以通过readelf来查看相关的许多信息,我们还可以用objdump来对hello.o进行反汇编,获得未重定位时的汇编代码文件,为下文符号解析和重定位打下基础。

(第4章1分)


5链接

5.1 链接的概念与作用

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

链接器ld将可重定位目标文件hello.o以及其他系统目标文件结合起来,最终创建一个可执行目标文件。链接的两个主要任务是符号解析和重定位,符号解析将目标文件中的每个全局符号都绑定到一个唯一的定义,而重定位确定每个符号的最终内存地址,并修改对那些目标的引用。

链接的方式繁多,比如与静态库链接,与共享库链接。链接时的时机也有很多,比如编译后生成可执行文件前链接,比如将可执行文件加载后运行前链接,也可以在运行时进行链接,链接的功能非常强大。链接器可以帮助我们模块化的编程,同时也方便了我们修改一些bug,比如库打桩技术。我们还可以使用AR工具来自己实现一个静态库,构建一个自己专属的函数小仓库。动态链接可以方便用户更新一些软件的新版本,也可以帮助构建高性能的Web服务器等等。

5.2 在Ubuntu下链接的命令

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

ld-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

/usr/lib/gcc/x86_64-linux-gnu/7/crtbegin.o

/usr/lib/gcc/x86_64-linux-gnu/7/crtend.o

/usr/lib/x86_64-linux-gnu/crtn.o hello.o -lc -z relro -o hello

可见gcc帮了我们多大的忙。。

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

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

ELF头

这是ELF头,我们可以看到它与可重定位文件的elf头比较相似,但又有不同。比如类型处,这里是“EXEC(可执行文件)”,而且节头数也变多了。这加载可执行文件时,加载器跳转到这个程序的入口点,也就是_start函数的地址。这个函数调用系统启动函数__libc_start_main,而这个启动函数可以初始化执行环境,调用用户层的main函数,处理main函数的返回值,并且在需要的时候把控制返回给内核。我们可看到这个入口点的地址为0x400520,而64位程序的代码是从地址0x400000开始,这两地址之间是有一定距离的,存放着一些其他数据。这张图还告诉了我们节头部表的文件偏移量为6616bytes,以及一些其他信息~。

节头表

程序头

段节

这里可以看到共享库libc.so。

重定位节 offset 0x400

符号表

其他信息

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

根据程序头,PHDR的虚拟地址为0x400040,大小为0x1f8;INTERP的虚拟地址为0x400238,大小为0x1c;第一个LOAD的地址为0x400000,大小为0x880;

PHDR 0x400040~0x40023

INTERP 0x400238~0x400260

 

第一个LOAD 0x400000~0x400880

 

其他段同理可得。

5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。

结合hello.o的重定位项目,分析hello中对其怎么重定位的。

多了一个.init节,.init节定义了一个小函数,叫做_init,程序的初始化代码会调用它。因为可执行文件是完全链接的,所以它不再需要.rel节

还可以发现原本是0,等待重定位的地方都已经重定位好了。

许多函数,比如sleep,exit,getchar等也因为链接而进入了这个可执行文件中。

重定位分为相对地址和绝对地址。绝对地址就是直接引用程序运行时地址,而PC相对地址引用的是一个偏移量,在跳转时PC的地址加上这个偏移量就是将要跳转到的地址,所以叫PC相对地址的引用。

5.6 hello的执行流程

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

名称

地址

ld-2.27.so!_dl_start

0x7ffff17d27a0

ld-2.27.so! dl_init

0x7f9e58629630

hello!_start

0x400500

ld-2.27.so!_libc_start_main

0x7f9f48249ab0

libc-2.27.so! cxa_atexit

0x7f4523fd3af7

hello!_libc_csu_init

0x7f87ffe13e26

hello_main

0x400530

hello!puts@plt

0x400621

hello!exit@plt

0x40062b

hello!printf@plt

0x400665

hello!sleep@plt

0x400669

hello!getchar@plt

0x400678

libc-2.27.so!exit

0x7fce8c888134

5.7 Hello的动态链接分析

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

假设程序调用一个由共享库定义的函数,编译器无法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任何位置,GNU编译系统使用延迟绑定的技术来解决这一问题。

全局偏移量表GOT

在GOT中,每个被这个目标模块引用的全局数据目标(过程或全局变量)都有一个8字节条目。编译器还为GOT中每个条目生成一个重定位记录。在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。每个引用全局目标的目标模块都有自己的GOT。初始时,每个GOT条目都对应PLT条目的第二条指令。

过程链接表PLT

PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责一个具体的函数。PLT[1]调用系统启动函数(__libc_start_main),它初始化执行环境,调用main函数并处理其返回值。从PLT[2]开始的条目调用用户代码调用的函数。

可知GOT起始地址为0x601000

运行到dl_init前

在调用dl_init之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码,GOT存放的是PLT中函数调用指令的下一条指令地址。在调用dl_init前,0x601008和0x601010处的两的8Bytes的数据都是0。

运行到dl_init后

在dl_init调用之后, 0x601008和0x601010处的两个8Bytes的数据发生了改变(依次为.plt节需要重定位的函数的运行时地址),由此来确定调用的函数地址, GOT[2]指向动态链接器ld-linux.so模块的入口点。

一开始,函数调用进入数组PLT中对应的条目,第一条PLT指令通过在数组GOT中对应条目进行间接跳转,把这个函数的ID压入栈中,从对应PLT条目调到PLT[0]。PLT[0]通过GOT[1]间接地把动态链接器的一个参数压入栈中,然后通过GOT[2]间接跳转进动态链接器中。动态链接器使用两个栈条目来确定函数的运行时位置,用这个地址来对GOT中对应条目进行重写,再把控制传递给这函数,后续再调用这个函数时,对应GOTA条目的间接跳转会将控制直接转移给这个函数。

5.8 本章小结

本章简单介绍了链接的概念作用、命令,分析了可执行文件和可重定位文件的一些不同之处。同时,对hello虚拟空间的情况,重定位情况以及动态链接的一些方面做了解析。

(第5章1分)


6hello进程管理

6.1 进程的概念与作用

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

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

shell 是一个交互型应用级程序,它代表用户运行其他程序。它通过分析用户输入的命令行来执行不同的操作。

流程:首先shell接收到一串命令行,对它开始解析。如果argv[0]是个内置的命令,则调用内置命令函数,如果不是内置命令,那么就创建一个子进程,然后在子进程中用execve指令来运行这一文件。如果命令行中带有“&”,则表示在后台运行,否则我们要一直等待知道命令完成或者子进程中文件执行结束。

6.3 Hello的fork进程创建过程

shell通过使用fork函数来创建子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到父进程用户级的虚拟地址空间相同的一份副本,包括代码和数据段、堆、共享栈及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,但是它们的PID不同。在输入运行hello程序的命令后,shell开始对命令行进行解析,判断出hello不是内置命令后,shell调用fork函数创建子进程来运行hello程序。

6.4 Hello的execve过程

shell在使用fork创建子进程之后,(用setpgid(0,0)将其放在一个不同于父进程的进程组中)然后在子进程中使用execve函数。execve函数在当前进程中加载并运行包含在可执行文件hello中的程序,用hello程序替代了当前程序。加载并运行hello需要以下几个步骤:

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

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

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

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

execve只在加载失败时返回-1,否则不返回。

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

控制流通过上下文切换从一个进程到另一个进程。一个进程执行它的控制流的一部分的每一时间段叫做进程时间片。在用户模式中运行应用程序代码。然后通过中断、故障或陷入系统调用等异常,进程从用户模式变为内核模式。当异常发生时,处理器将异常传递到异常处理程序,变为内核模式,异常处理程序返回时变回到用户模式。

一开始运行Hello时,进程处于用户模式Hello,在调用sleep函数时进入内核模式。内核模式中,内核将Hello加入等待队列,计时器开始计时,内核上下文切换到用户模式去运行另一个进程了,而不是仅仅等待什么都不做。当计时器达到2s后发出中断信号,进程进入内核模式,然后通过上下文切换进入用户模式运行Hello。

6.6 hello的异常与信号处理

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

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

乱按:

键盘输入一些字符,并没有什么影响,程序还是正常运行。

回车

每按下一次回车,屏幕输出一串空行。

Ctrl-C:

用户按下ctrl-c后,内核会向当前进程组中的每个进程发送SIGINT信号,将这个进程组中的进程全部终止。在接收到这个信号后,进程由用户模式变成内核模式,这个转送给信号处理程序,这个程序捕获SIGINT信号,并默认把当前进程终止。

Ctrl-Z:

用户按下ctrl-z后,内核会向当前进程发送SIGTSTP信号,在接收到这个信号后,进程由用户模式变成内核模式,将这个信号转送给信号处理程序,这个程序捕获SIGTSTP,然后默认把当前进程停止直到下一个SIGCONT信号的到来。

ps:

ps是一个内置的函数,它可以查看当前正在运行的一些进程(部分)。

jobs:

jobs也是一个内置的函数,它可以查看任务列表。

 

pstree:

内置函数,可以查看进程树。

fg:

内置函数,可以用来将一些停止的进程恢复到前台运行。它向处于停止状态的进程发送SIGCONT信号,恢复这些进程,并在前台开始运行。

kill:

内置函数,可以向其他进程(包括自己)发送信号,kill [参数] [pid],其中kill -9是向进程发送SIGKILL信号,杀死该pid的进程。

6.7本章小结

通过软硬件的合作提供的底层异常机制,并通过上下文切换我们可以将进程从用户模式变成内核模式,实现并行等操作,从而对该进程遇到的一些状况进行应对和分析并解决,还可以提高运行的效率,比如在一个进程需要等待时去执行另一项进程。另一方面,信号与信号处理函数的机制使得用户在与进程交互的过程中更加方便。我们还可以通过修改信号处理函数等操作来达到某些目的。

(第6章1分)


7hello的存储管理

7.1 hello的存储器地址空间

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。

逻辑地址是指由程序产生的与段相关的偏移地址部分,又称为绝对地址,在hello中就是各部分在段内的偏移地址。

线性地址:逻辑地址到物理地址变换之间的中间层。CPU在保护模式下,在分段部件中逻辑地址是段中的偏移地址,然后加上段的基址就称为了线性地址。即hello里面的虚拟内存地址。

虚拟地址:CPU未开启分页功能时,线性地址就被当做最终的物理地址来用;若开启了分页功能,则线性地址就叫作虚拟地址。

物理地址就是内存单元的绝对地址。在hello程序中就是虚拟内存地址经过翻译后获得的地址。

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

:用户编制的程序可以由一个主程序、若干个子程序、符号表、栈以及数据等若干段组成,每一段都有独立、完整的逻辑意义,每一个段的长度可以不同。 
段式存储管理:以段为单位进行存储空间的管理。段内地址是连续的,段与段之间的地址是不连续的。

操作:逻辑地址可以分成段选择符、段描述符的判别符以及地址偏移量的形式,首先通过判别福来确定这个段描述符是局部(LDT)还是全局(GDT),然后将其组合成段描述符+地址偏移量的形式,形成线性地址。

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

页式管理将各进程的虚拟空间划分成若干个长度相等的(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后将页式虚拟地址与内存地址之间建立一个一一对应的页表,并用相应的硬件地址变换机构(MMU等),来解决地址变换(翻译)问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。

操作:在获得线性地址(虚拟地址)后,我们将这个地址分成VPN和VPO,VPN表示虚拟页号,VPO表示虚拟页偏移量,我们可以通过VPN来获得PPN(物理页号),具体如下:在TLB(翻译后备缓冲器)中,将VPN分为TLBI,TLBT来寻找所求的物理页号;若不在TLB中,则去缓存中的或内存中的页表中寻找,若缺页,MMU触发一次异常,更新页表。最终将取得的PPN与VPO组合得到我们要的物理地址。

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

当我们在TLB中找不到我们要的PPN时,我们需要在页表中寻找。我们知道CR3寄存器始终指向一级页表,因此在四级页表中我们将VPN拆成4部分,在四级页表中从一级二级三级的在这三级页表目录中寻找我们的页表的地址,然后在四级的页表中,我们找到我们要的PPN,然后与VPO组合,得到PA。

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

在获得了PA之后,我们需要用这个地址去缓存中寻找我们要的数据。

首先,我们将PA拆分成CT,CI,CO,这三个分别表示标记,组索引,块内偏移量。

我们先通过组索引来确定我们的组,然后在这个组中找有效位为1的而且有对应标记的缓存行,若找到,则用块内偏移量锁定我们要的数据块,如果找不到,则到第二级(下一级)cache中去寻找,以此类推。

7.6 hello进程fork时的内存映射

mm_struct(内存描述符):描述了一个进程的整个虚拟内存空间。

vm_area_struct(区域结构描述符):描述了进程的虚拟内存空间的一个区间。

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

7.7 hello进程execve时的内存映射

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

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

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

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

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

缺页:虚拟内存中的字不在物理内存中 (DRAM 缓存不命中)。

段错误:判断这个缺页的虚拟地址是否合法,缺页处理程序搜索区域结构的链表(linux在链表中构建一颗查找树来加速查找),把这个虚拟地址和每个区域结构中的vm_start和vm_end做比较,如果虚拟地址不在合理范围内,则缺页处理程序触发一个段错误,并终止这个进程。

保护异常:判断这个进程是否有读写这个区域页面的权限,如果没有,则触发一个保护异常,并终止这个进程。

正常缺页:若以上两种情况均未发生,则内核知道这是一个正常的缺页,并选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,还如新的页面并更新页表。然后返回那个导致缺页的指令,重新执行这个指令。

7.9动态存储分配管理

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

动态内存分配器维护着一个进程的虚拟内存区域,称为(heap) 。堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址) 。对于每个进程,内核维护着一个变量brk, 它指向堆的顶部。

分配器将堆视为一组不同大小的块 (blocks)的集合来维护,每个块要么是已分配的,要么是空闲的。分配器主要分为显式分配器和隐式分配器。

策略:显式空闲链表和隐式空闲链表。

隐式空闲链表:通过头部的大小字段隐式的连接。分配器可以通过遍历堆中的所有块,从而间接地遍历整个空闲块地集合。可以通过添加脚部的方式实现隐式双向链表。寻找空闲块时可使用首次适配、下一次适配、最佳适配和分离适配等分配策略;分配空闲块时,如果块较大且找不到更合适的,则可以进行分割;释放块时需要按照四种情况合并相邻空闲块。

显式空闲链表:通过某种数据结构来管理、分配空闲块,而不去管理已分配的块。

7.10本章小结

这一章我们介绍了hello的存储器地址的原理,如果将逻辑地址转换成线性地址,如何将线性地址转换为物理地址,段式、页式的管理方式,从VA到PA再到访问缓存,还介绍了hello进程时fork和execve的内存映射,缺页与处理机制以及动态内存分配的机制。

(第7章 2分)


8hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

文件类型如下:

1、普通文件:包含任意数据的文件。

2、目录(directory):包含一组链接的文件,每个链接都将一个文件名映射到一个文件

3、套接字(socket):用来与另一个进程进行跨网络通信的文件

4、命名通道

5、符号链接

6、字符和块设备

 

设备管理:unix io接口

1.打开和关闭文件

2.读取和写入文件

3.改变当前文件的位置

8.2 简述Unix IO接口及其函数

1.open()函数:这个函数会打开一个已经存在的文件或者创建一个新的文件。

2.close()函数:这个函数会关闭一个打开的文件。

3.read()函数:这个函数会从当前文件位置复制字节到内存位置。

4.write()函数:这个函数从内存复制字节到当前文件位置。

5.lseek()函数:改变文件位置。

8.3 printf的实现分析

https://www.cnblogs.com/pianist/p/3315801.html

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.

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

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

8.4 getchar的实现分析

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

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

8.5本章小结

本章介绍了linux的IO设备管理方法、Unix的IO接口及其函数、printf函数的实现和getchar函数的实现。

(第8章1分)

结论

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

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

hello经历的过程:

首先,我们从键盘输入hello的C语言代码,然后将其保存为一个文本文件。

然后通过cpp对这个文本文件进行预处理,它把头文件、宏定义、条件编译等给原文替换了,最终生成一个预处理文件hello.i。

然后这个.i文件再经过cc1编译器生成一个.s文件。这个过程将这个.i文件翻译成一个ASCII汇编语言文件hello.s。

然后汇编器as将这个hello.s文件翻译成一个可重定位目标文件hello.o。

然后链接器将这个hello.o文件与共享库链接起来生成一个hello可执行目标文件。

以上是我们用户在运行这个hello文件“./hello”前需要进行的流程。
紧接着我们运行这个可执行文件,加载器loader将hello中的代码和数据从磁盘复制到内存中,实际上只是将新的虚拟内存映射到磁盘文件上,只是分配,并未缓存,真正缓存要到第一访问这个虚拟内存的段时,发生缺页才将页面调度,从磁盘将数据缓存进DRAM主存(按需页面调度)。最后运行这个程序,然后我们通过shell对这个程序进行各种折腾,最终释放了他;)。

感悟:

通过这学期对这门课的学习,我发生计算机系统不再是一个仅仅在计算机导论中提到的陌生的东西,在我的脑海中也不在那么模糊了,而是丰富多彩了起来。在这门课上,我们首先从一个源程序文本文件是如何从一个文本到一个可执行文件的,这一过程已在上文中论述了。而这一过程令我惊讶又好奇,我惊讶的是我们平常编写的程序竟然要经过这么多道工序再能最终生成我们的可执行文件,同时,我又对其中的每个部分为什么要这么做,下一步为什么要这么做产生了极大的兴趣,这种兴趣不断激励着我。同时,这门课使我明白计算机是一个整体,它的每个细节所蕴含的智慧造就了计算机这一个伟大的产物,也教会了我,如果想要做好一件事,对其中的每个步骤都要细细考究,局部的优化有时候能对整体起到重大的作用;当我们纠结于某个东西速度不够快时,不妨用空间换时间的思想来提高速度,就比如计算机的存储结构,从寄存器到SRAM到DRAM到硬盘,速度快的容量小且昂贵,速度慢的容量大便宜,正是这种多个不同规格存储器的“默契搭配”使得计算机存取的速度快上不少。学完计算机系统这一门课的部分知识后,我更加认识到学习这门课的重要性,如果作为一名程序员,对自己日常面对的家伙的内部的构造都不了解,那怎么可能在编程上更进一步,达到传说中人机合一的境界呢?

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


附件

hello.ihello的预处理文件

hello.shello的汇编文件

hello.ohello的可重定位文件

hellohello的可执行文件

o_elf.txtreadelf -a hello.o重定向生成的文本文件

o_obj.txtobjdump -d -r hello重定向生成的文本文件

x_elf.txtreadelf -a hello重定向生成的文本文件

x_elf.txtobjdump -d -r hello重定向生成的文本文件

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


参考文献

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

[1]  深入理解计算机系统. Randal E.Bryant,David R.O’Hallaron[M]. 北京:机械工业出版社,2019.3.

[2]  辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.

[3]  赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).

[4]  谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.

[5]  KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.

[6]  CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.

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

05-10 22:08