2022spring hitcsapp hellosp2p bykhr

计算机系统

 

大作业​​​​​​​

摘  要

Helloworld是几乎是所有编程学习者亲手实现并运行的第一个程序,也往往是最不引起大家重视的一个程序。但在我们不断探索编程中会遇到各种各样的问题,这使得我们不得不开始探寻比编写程序更低层的那些东西。这时,我们的老朋友——helloworld程序就又派上了用场。本文将以helloworld这一最简单的程序为例,从头梳理一个计算机程序从代码编辑器到运行进程的过程

在编译源文件的过程中,gcc通过调用cpp/cc1/as/ld,将C语言源文件进行预处理、编译、汇编、链接,最终形成可执行目标文件hello,由存储器保存在磁盘中。运行进程时,操作系统为其分配虚拟地址空间,提供异常控制流等强大的工具,Unix I/O为其提供与程序员和系统文件交互的方式。本文通过分析Hello程序从代码编辑器到运行进程的过程,对计算机系统编译源文件、运行进程等机制进行较深入的分析和介绍。。

关键词:CSAPP;计算机系统;C语言;                            

(摘要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简介

在编译源文件的过程中,gcc通过调用cpp/cc1/as/ld,将C语言源文件进行预处理、编译、汇编、链接,最终形成可执行目标文件hello,由存储器保存在磁盘中。运行进程时,操作系统为其分配虚拟地址空间,随着一连串的缺页故障,hello被逐渐地载入物理内存。操作系统提供异常控制流等强大的工具,不断对系统中运行着的进程进行调度。Unix I/O为其提供与程序员和系统文件交互的方式,让它不再孤单。当程序从main中返回,意味着程序的终止。之后,shell作为其父进程会负责将其回收,操作系统内核删除相关数据结构,释放其占据的资源,hello的一生就此结束。

1.2 环境与工具

硬件:Intel Core i5 2GHz;2G RAM(虚拟机);512GHD Disk 

软件:Vmware 12.1.2;Ubuntu 20.04;macOS Catalina 10.15.7

开发工具:vi/vim/gedit+gcc,edb, gdb, as, ld, readelf 

1.3 中间结果

预处理后文件hello.i

编译后的汇编文件hello.s

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

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

Hello.o的反汇编代码 objhello.txt

Hello的反汇编代码 hello.out

使用readelf得到的elf文件直接在终端上查看,没有保存

1.4 本章小结

本章对hello的一生进行了简要的介绍和描述,介绍了P2P的整个过程,介绍了本计算机的硬件环境、软件环境、开发工具,介绍了为编写本论文的中间文件的名称和其作用。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理器(cpp)根据以字符#开头的命令,修改原始的c程序。插入所有用#include命令指定的文件,并扩展所有用#define声明指定的宏。结果得到另一个C程序,通常以.i作为文件扩展名。

预处理的作用在我看来是将我们编写程序时简化掉的那一部分工作补全,得到一个有自包含性的,对外部依赖更少的更“完整”的c程序,为接下来的进一步处理做好准备。

2.2在Ubuntu下预处理的命令

命令:gcc -E hello.c -o hello.i

图1 预处理指令和结果

2.3 Hello的预处理结果解析

    打开hello.i文件可见程序行数变成了3000多行,多出的内容包括对基本数据类型的定义和头文件包含的函数如printf等,但这些函数前面都声明了extern。原本的.c程序主体在.i文件的最后。

2.4 本章小结

    本章介绍了预处理的流程和目的,并做了ubuntu系统下的演示与预处理结果的简单分析。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

    编译器(ccl)将c语言文本文件hello.i翻译成汇编语言文本文件hello.s,其中包含一个汇编语言程序。大致分为语法分析,语义分析,中间代码生成,代码优化,代码生成几步。

    其主要作用是将高级语言程序翻译成汇编程序。

3.2 在Ubuntu下编译的命令

Gcc -S hello.i -o hello.s

图2 编译命令和结果

3.3 Hello的编译结果解析

所有以’.’开头的都是指导汇编器和链接器工作的伪指令。我们通常可以忽略这些行。

3.3.1 字符串

本程序中我们使用的的字符串都在只读数据段中,分别位于.LC0和.LC1.

3.3.2 局部变量i

观察比对汇编代码与c语言程序,循环的边界条件是i<8,注意到代码中有movl $0, -4(%rbp), addl $1, -4(%rbp)和cmpl $7, -4(%rbp),与循环变量的迭代和检查相对应,可知局部变量i存储在栈上的-4(%rbp)处。

3.3.3 立即数

立即数用来表示常数值,顾名思义写出来就用,书写方式为”$”后面跟一个用标准C表示法表示的数。

3.3.4 main函数及其参数

函数名称在汇编代码中仍有体现,且函数名会以.globl main的形式表明其是全局函数。函数有两个参数,其中整型argc存储方式与循环变量类似,观察代码发现位于-20(%rbp);

另一个参数是字符指针数组*argv[],其起始地址存储在-32(%rbp),原程序中需要在printf前调用argv[1]和argv[2],通过add 语句再起始地址上加上偏移量实现。

3.3.5 数据传送操作

这段程序中之后循环体中的i=0一个赋值操作,具体靠汇编指令mov实现:movl $0, -4(%rbp)

3.3.6 算术操作

C程序中显式的算数操作只有循环变量迭代的i++;,通过add 指令实现,在汇编程序中还有减法指令sub,具体的算数和逻辑指令见下图:

3.3.7 比较操作

程序中涉及到循环条件的判定需要用到比较操作,使用了cmpl $7, -4(%rbp),比较语句会设置条件码,根据条件码不同下面的程序可能会有不同的运行顺序。

3.3.8跳转操作

跳转指令会导致执行切换到程序中一个全新的位置。在汇编代码中,这些跳转的目的地通常用一个标号指明。同时跳转指令也分为无条件的和有条件的,有条件的跳转会检查当前的条件码来确定是否执行,如本程序中检查循环变量是否<8的语句后就有条件跳转语句 jle .L4.

3.3.9 调用函数

调用函数在汇编语言中使用call指令,本程序中调用函数的指令都形如 call 函数名@PLT

对于需要参数的函数,其第1,2,3,4个参数分别存在%rdi,%rsi,%rdx,%rbx中。函数的返回值会存在%rax中。

3.3.10类型转换

hello.c中涉及的类型转换是:atoi(argv[3])

3.4 本章小结

本章介绍了编译阶段编译器如何将c语言转换成相应的汇编语言,为我们理解程序在计算机中具体如何运行提供了帮助。

(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。目标代码是机器代码的一种形式,它包含所有指令的二进制表示,但是还没有填入全局值的地址。

4.2 在Ubuntu下汇编的命令

gcc hello.s -c -o hello.o

图3 汇编命令和结果

4.3 可重定位目标elf格式

    首先使用命令readelf -h hello.o查看ELF头。ELF头以一个16字节的序列Magic开始,Magic描述了生成该文件的系统 的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。

图4 hello.o的elf头

    使用readelf -S hello.o查看Section Headers,节头部表包含了各个节的名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐信息。因为是可重定位文件,所以所有地址都为0;可以通过偏移量找到各节在文件中的具体位置。其中代码段是可执行不可写的,数据段可写不可执行,只读数据段不可写也不可执行。

图5 hello.o的section headers

查询重定位节.rela.text

图6 hello.o的ELF文件的.rela.text节

   偏移量指需要被修改的饮用节的偏移,信息的前四个字节表示被修改引用应该指向的符号,后四个字节表示重定位类型。类型的值决定链接器修改新应用的方式。加数在重定位时要使用它对被修改引用的值做偏移调整。

4.4 Hello.o的结果解析

    

图7 hello.o的反汇编代码与原始汇编代码比对,左侧为反汇编结果,右侧为原始汇编代码。

机器语言是纯粹的二进制代码,由操作码和操作数组成,能够被计算机直接识别。如左侧图片所示,机器语言与汇编语言是一一对应的,只有操作码和操作数的形式是显示为二进制码还是文字的区别。

比较两者的区别:1. 在分支转移上,汇编代码的操作数是像.L2这样的段名称,而反汇编代码中使用的是地址(函数起始地址+偏移量)

2. 在汇编代码中,函数调用之后直接使用函数名称,反汇编代码中,call的操作数也是一个地址。因为hello.c中调用的函数 都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。

4.5 本章小结

本章介绍了对hello程序汇编的方法,简单查看分析了其ELF文件,并将对.o文件的反汇编代码与原始汇编代码做了简单对比。

(第4章1分)

5章 链接

5.1 链接的概念与作用

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

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

图8 链接的命令与结果

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

    

图9 hello的ELF头

Hello的ELF头与hello.o的ELF头中只有两个条目不同,分别是类型和section header数。Hello是一个exec可执行文件,有27个节。

    

图10,11 hello的section headers

查看hello的节头部表,注意到与hello.o不同,这里的每一节都有确定的地址,这个地址是程序被载入到虚拟地址各段的起始地址。此外我们也能获得各节所占空间,偏移量等信息。

重定位信息:

    

图12 hello的重定位信息

符号表:

图13 hello的符号表

5.4 hello的虚拟地址空间

在edb中打开hello,按照section header中的地址查询各节地址。

.interp:

图14 .interp节

.text:

图15 .text节

.rodata:

图16 .rodata节

.data:

图17 .data节

5.5 链接的重定位过程分析

使用命令:objdump -d -r hello >hello.out,获得hello的反汇编代码,比对发现hello的反汇编代码有确定的虚拟地址,这说明其已经完成了重定位。而且hello反汇编出的内容要更多。

hello重定位的过程:

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

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

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

5.6 hello的执行流程

名称

地址

ld-2.23.so!_dl_start

0x00007f8dec5b79b0

ld-2.27.so! dl_init

0x00007f8dec5c6740

hello!_start

0x004004d0

ld-2.27.so!_libc_start_main

0x00400480

libc-2.27.so! cxa_atexit

0x00007f8dec226280

hello!_libc_csu_init

0x00400580

hello!_init

0x00400430

libc-2.27.so!_setjmp

0x00007f8dec221250

libc-2.27.so!_sigsetjmp

0x00007f8dec221240

libc-2.27.so!__sigjmp_save

0x00007fa8dec221210

hello_main

0x004004fa

hello!puts@plt

0x00400460

hello!exit@plt

0x004004a0

hello!printf@plt

0x00400470

hello!sleep@plt

0x004004b0

hello!getchar@plt

0x00400490

ld-2.23.so!_dl_runtime_resolve_avx

0x00007f8dec5cd870

libc-2.27.so!exit

0c00007f6002de35b0

5.7 Hello的动态链接分析

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。

    在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定(lazybinding),将过程地址的绑定推迟到第一次调用该过程时。

延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:

PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。

GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。

从ELF文件中可以找到.got节,我们可以在edb中运行程序,并查看这部分内存的变化。

GOT表位置在调用dl_init之前0x404000后的16个字节均为0:

图18 调用dl_init之前的got表内容

调用_start之后发生改变:

图19 调用_start后的got表内容

5.8 本章小结

    本章介绍了链接的概念和作用以及hello.o链接的过程。并具体分析了hello的elf文件内容,以及重定位过程、执行流程、动态链接过程,最终获得了可直接运行的hello程序。

(第5章1分)

6章 hello进程管理

6.1 进程的概念与作用

进程的经典定义就是一个执行中程序的实例。

它提供给我们一种假象,好像程序是系统中当前唯一运行的程序,程序独占使用处理器和内存。处理器好像是无间断地执行程序中的指令。

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

    Shell是一个交互应用级程序,代表用户运行其他程序

shell应用程序提供了一个界面,用户通过访问这个界面访问操作系统内核的服务。  

处理流程:从终端读入输入的命令->将输入字符串切分获得所有的参数->如果是内置命令则立即执行->否则调用相应的程序执行->shell 应该接受键盘输入信号,并对这些信号进行相应处理

每次用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个新进程的上下文中运行这个目标可执行文件。应用程序可以使用fork()函数来创建新进程。

终止状态的进程会被其父进程回收。当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程。

如果一个父进程终止了,内核会安排init进程成为它的孤儿进程的养父。

6.3 Hello的fork进程创建过程

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

    在运行hello时我们在终端输入的第一部分./hello,这不是一个内置命令,所以shell会使用fork()函数创建一个新进程。

6.4 Hello的execve过程

Execve函数在刚才fork()创建的进程的上下文中加载并运行hello程序,覆盖当前进程的代码,数据,栈,删除子进程现有的虚拟内存段,新的栈和堆段被初始化为零,新的代码和数据段被初始化为可执行文件中的内容。但是保留原有的PID。我们输入的指令是./hello 120L022226 张可承 2, 这些输入会存入参数列表argv,惯例argv[0]是文件名,其余各项在hello运行时会有所体现。注意当execve()调用成功时是没有返回值的。

6.5 Hello的进程执行

上下文就是内核重新启动一个被抢占的进程所需的状态,它由一些对象的值组成。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被强占了的进程,这种决策就叫做调度。一个进程执行它的控制流的一部分的每一时间段叫做时间片。

在hello程序中,程序每打印一次后都会调用sleep()函数,这一系统调用显式地请求让调用进程休眠,这时会上下文切换到内核模式,等到sleep规定的秒数到了时将中断并上下文切换回到hello.

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

程序执行效果如图:

图20 hello的正常运行效果

6.6 hello的异常与信号处理

异常分为四类:中断、陷阱、故障、终止

    对于hello程序而言,特定键盘输入一定会导致中断,执行sleep函数时一定会导致陷阱。故障和终止在正常运行时一般不会发生,但也要考虑如内存损坏这样的特殊情况。

键盘输入Ctrl+C:内核发送一个SIGINT信号到前台进程,终止前台作业

图21 输入Ctrl+C的运行效果

Ctrl+Z将会发送一个SIGTSTP信号给当前执行的前台进程组,使hello进程挂起。

Ctrl+Z后使用fg指令继续运行

图22 输入Ctrl+Z 后fg

Ctrl+Z后使用kill指令终止进程

图23 Ctrl+Z后使用kill

Ctrl+Z后使用ps和pstree:

图24 Ctrl+Z后使用ps和pstree的运行效果

乱按键盘:

图25 乱按键盘的运行效果

6.7本章小结

本章介绍了进程的概念与作用,shell的作用与工作流程,fork和execve函数在执行hello时有何作用,并分析了hello的进程执行与异常和信号处理。

(第6章1分)

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。

线性地址:也叫虚拟地址,和逻辑地址类似,也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件也是内存的转换前地址。

虚拟地址:也就是线性地址。

物理地址:用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应。可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上的抽象。

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

 Intel 平台下,逻辑地址(logical address) selector:offset 这种形式,selector  CS 寄存器的值,offset  EIP 寄存器的值。如果用 selector  GDT( 全局描述符表 ) 里拿到 segment base address(段基址然后加上offset(段内偏移),这就得到了 linear address。我们把这个过程称作段式内存管理。

一个逻辑地址由段标识符和段内偏移量组成。段标识符是一个16位长的字段(段选择符)。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。

全局的段描述符,放在全局段描述符表(GDT)”中,一些局部的段描述符,放在局部段描述符表(LDT)”中。

给定一个完整的逻辑地址段选择符+段内偏移地址,看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,就得到了其基地址。Base + offset = 线性地址。

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

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

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

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

    多级页表将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。

图26 多级页表

    处理器生成一个虚拟地址,并将其传送给MMU。MMU用VPN向TLB请求对应的PTE,如果命中,则跳过之后的几步。MMU生成PTE地址(PTEA).,并从高速缓存/主存请求得到PTE。如果请求不成功,MMU向主存请求PTE,高速缓存/主存向MMU返回PTE。PTE的有效位为零, 因此 MMU触发缺页异常,缺页处理程序确定物理内存中的牺牲页 (若页面被修改,则换出到磁盘——写回策略)。缺页处理程序调入新的页面,并更新内存中的PTE。缺页处理程序返回到原来进程,再次执行导致缺页的指令。

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

CPU发送一条虚拟地址,随后MMU按照7.4所述的操作获得了物理地址PA。根据cache大小组数的要求,将PA分为CT(标记位)CI(组索引),CO(块偏移)。根据CI寻找到正确的组,依次与每一行的数据比较,有效位有效且标记位一致则命中。如果命中,直接返回想要的数据。如果不命中,就依次去L2,L3,主存判断是否命中,命中时将数据传给CPU同时更新各级cache的储存。

图27 core i7地址翻译概况

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。 加载并运行 hello 需要以下几个步骤:

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

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

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

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

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

缺页故障:当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。通过查询页表PTE可以知道虚拟页在磁盘的位置。缺页处理程序从指定的位置加载页面 到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中,因此指令可以没有故障的运行完成。

7.9动态存储分配管理

动态内存分配器动态内存分配器维护者一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的。分配器主要分为显式分配器和隐式分配器。

下面将介绍一些基本方法与策略

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

2、带边界标记的隐式空闲链表:一个块是由一个字的头部、有效载荷、可能的一些额外的填充,以及在块的结尾处的一个字的脚部组成的。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。

3、适配块策略有以下三种:首次适配:从头开始搜索空闲链表,选择第一个合适的空闲块:可以取总块数(包括已分配和空闲块)的线性时间,但是会在靠近链表起始处留下小空闲块的“碎片”。下一次适配:和首次适配相似,只是从链表中上一次查询结束的地方开始,优点是比首次适应更快:避免重复扫描那些无用块。但是一些研究表明,下一次适配的内存利用率要比首次适配低得多。最佳适配:查询链表,选择一个最好的空闲块适配,剩余最少空闲空间,优点是可以保证碎片最小——提高内存利用率,但是通常运行速度会慢于首次适配。

4、合并策略则分为4种

情况1,两个邻接的块都是已分配的,因此不可能进行合并。所以当前块的状态只是简单地从已分配变成空闲。情况2,当前块与后面的块合并。用当前块和后面块的大小的和来更新当前块的头部和后面块的脚部。情况3,前面的块和当前块合并。用两个块大小的和来更新前面块的头部和当前块的脚部。情况4,要合并所有的三个块形成一个单独的空闲块,用三个块大小的和来更新前面块的头部和后面块的脚部。

5、链表的维护方式则有两种,一是后进先出的顺序,二是按照地址顺序来维护

6、最终我们采取策略的首要目的就是保证吞吐率和内存使用率的最大化

7.10本章小结

本章主要介绍了hello的存储器的地址空间,介绍了四种地址空间的差别和地址的相互转换。同时介绍了hello的四级页表的虚拟地址空间到物理地址的转换。阐述了三级cashe的物理内存访问、进程 fork 时的内存映射、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 创建的每个进程都有三个打开的文件:标准输入,标准输出,标 准错误。 (3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。

(4)读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文 件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文 件,当 k>=m 时,触发 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 是需要关闭的文件的描述符,close 返回操作结果。

(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的实现分析

从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.c预处理到hello.i文本文件

hello.i编译到hello.s汇编文件

hello.s汇编到二进制可重定位目标文件hello.o

hello.o链接生成可执行文件hello

bash进程调用fork函数,生成子进程;

execve函数加载运行当前进程的上下文中加载并运行新程序hello

hello的运行使用虚拟地址。

hello的输入输出与外界交互,与linux I/O息息相关

hello最终被shell父进程回收,内核会收回为其创建的所有信息

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

附件

预处理后文件hello.i

编译后的汇编文件hello.s

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

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

Hello.o的反汇编代码 objhello.txt

Hello的反汇编代码 hello.out

使用readelf得到的elf文件直接在终端上查看,没有保存

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

参考文献

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

[1]  深入理解计算机系统 第三版

[2]  关于查看elf文件:https://blog.csdn.net/weixin_39626089/article/details/111677584

[3]  编译的基本概念:https://blog.csdn.net/thevictory/article/details/116501441

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值