计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学   号 1180301017
班   级 1803010
学 生 吴畏嶙    
指 导 教 师 史先俊

计算机科学与技术学院
2019年12月
摘 要
本文从计算机底层的实现,讲述了一个简单程序hello.c从程序到进程,从无到有,最终又归于无的“一生”。对计算机系统运行程序时,编译系统的处理、进程的管理、存储的管理以及I/O的管理进行了分析和说明。
本文的主要意义:复习基础知识,结合具体程序,系统整理思路,加深强化理解。

关键词:进程,存储,“P2P”,”020”。

(摘要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简介
编写程序后,我们将程序保存为hello.c,但是此时不能直接运行,需要经过编译系统的一系列处理(预处理、编译、汇编、链接)后才能够运行(编译系统是由执行这些处理的各个程序构成的)。处理完成后hello.c就变成了可以被执行的hello可执行目标文件,这时就可以通过在linux的shell上输入”./hello”运行,然后就进入了shell(Bash)的进程管理的阶段。shell有自己的运行程序,我们的在键盘上输入./hello还有它的参数,shell负责为hello用fork生成子进程,通过execve函数调用加载器正式开始运行hello,到这里hello就从Program变成了Process。
开始执行程序以后,程序通过虚拟内存的系统被载入物理内存,然后进入main函数执行。CPU通过上下文切换为hello分配时间片生成了一个逻辑控制流,期间CPU仍会不停的进行上下文切换运行它所有已经被载入的进程,我们的hello的逻辑控制流也包括在内。CPU间歇性地沿着hello代码转化成的一大长串机器指令,通过取指、译码、执行、访存、写回、更新PC等步骤逐步运行,期间还涉及I/O输入输出。当hello运行结束后,会发送给父进程,也就是shell进程,它的退出状态,shell(或init进程)负责回收hello。到这里hello的“一生”就完结了。

1.2 环境与工具
软件环境:Windows 10,Vmware Workstation 19 Pro,Ubuntu 64
硬件环境:x64 CPU, 2.30GHz,8.00GB RAM,1TB Disk
开发与调试工具:gcc,codeblocks,gdb,
1.3 中间结果
hello.i —— 预处理完成后产生的文件,用于下一阶段的编译。
hello.s ——预处理文件对应翻译成的汇编语言格式的文本文件,用于下一阶段的汇编。
hello.o —— hello.s转化成的可重定位目标文件,经过链接后生成可执行目标文件。
hello – 可执行目标文件
1.4 本章小结
hello的“一生”很短暂,接下来我们从hello还是hello.c的时候开始谈起。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
预处理:预处理器(cpp)根据以字符’#’开头的命令,修改原始的C程序。以hello.c为例,它前几行的#include <stdio.h>,#include <unistd.h>和#include <stdlib.h>命令告诉预处理器读取系统头文件stdio.h,unistd.h和stdlib.h的内容,并把它们直接插入到程序文本中。然后我们就得到了另一个C程序,但是扩展名是“.i”。这里hello.c预处理后生成的文本文件是hello.i,linux控制台下使用的命令是cpp hello.c > hello.i或是gcc -E hello.c -o hello.i。
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i 或 cpp hello.c > hello.i

这里用的是gcc -m64 -Og -no-pie -fno-PIC -E hello.c -o hello.i

2.3 Hello的预处理结果解析


(上图是hello.i文件的·最后面部分的内容)
用文本编辑器打开hello.i文件,它的最后一部分的内容就是我们编写的hello.c的main程序的内容,前面的上千行都是stdio.h,stdlib.h等库中的各种函数以及各种全局变量和数据类型等等的声明,其中以’#’开头的行是注释,这里包含注释和空行在内,合并到hello.c的来自库的内容总共占了3000多行,但是这里合并到hello.c中的并不是各个库函数的原型,只是对它们的引用,之后的链接会负责将引用的函数与hello.o链接(详情在第五章会说明),最终生成可执行文件。

2.4 本章小结
预处理是为了下一步的编译做准备,在这一步中,我们看到gcc(cpp)将系统头文件的内容直接插入到hello.c中,形成了一个新的C语言形式的用.i扩展名保存的文件。

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
编译:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。这里是将hello.i里面的C语言翻译成了汇编语言保存到hello.s中。
3.2 在Ubuntu下编译的命令

gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s

3.3 Hello的编译结果解析
hello.i编译后产生文件hello.s,用文本编辑器打开看里面的内容。

3.3.1 对hello.s的整体认识
上面截取hello.s的开头一部分内容,其中所有以“.”开头的都是指导汇编器和链接器工作的伪指令,我们通常可以忽略这些行,况且也没有关于指令的用途以及它们与源代码之间关系的具体说明。但是这里我们可以看到不少与源代码相关的信息。
3.3.2 局部变量的声明和赋值


movl那一行就是对局部变量int i的操作,64位系统一般都是优先选择寄存器存局部变量,因为这样相对于32位采用堆栈去保存局部变量效率更高。
3.3.3 算术操作

这个就是上面的i++操作的汇编,因为循环计数器变量int i 保存在寄存器%ebx中,每次经历一次循环,%ebx加一即可
3.3.4 关系操作

这个就是比较i与7的大小,小于等于就进入循环体,也就是i<=7 在这里与
i<8等价。


argc是第一个参数,所以放在寄存器%edi中,当%edi中的值不等于4时,打印一下提示消息就退出。
3.3.5控制转移


当argc!=4时,call指令转移控制,调用puts函数,打印字符串常量,同时call exit,从主程序退出。

看到汇编代码上.L6,.L3,这些都是一些跳转的位置,通过jmp,jne跳转到这些位置,通过跳转可以实现函数调用,也可以实现循环。

3.3.6数组操作

main函数的第二个参数就是argv[],是一个指针数组,八个字节故用寄存器%rdi传参。然后赋值给%rbp,这时%rbp就获得了数组的首地址,利用栈帧去处理这个数组的元素。16(%rbp)就是argv[2],8(%rbp)就是argv[1],,24(%rbp)就是argv[3]。
3.3.7字符串



hello.c中共涉及上面两个字符串的输出,第二个是格式化输出,第一个只是输出一个字符串常量,编译器在编译时,将它们摘出,放在.rodata节中保存为只读数据,并分别用.LC1和.LC0标识它们的位置,指导链接器和汇编器工作。

3.3.8函数
3.3.8a main函数

main函数是在主程序里面定义的一个全局符号。
编译器将main函数在.text节中声明为全局的函数。在shell中执行可执行文件hello时,execve函数将argc和argv[]作为参数调用加载器传递给main函数,然后在之前的上下文中运行程序。
3.3.8b printf函数

两次调用printf函数,第一个printf函数只需要输出一个字符串常量,也就是一个数组,它将.LC0位置的字符串首地址复制过来赋值给%edi作为参数,然后调用printf输出字符串。
第二个printf函数是格式化输出,它首先像上面的printf一样,将.LC中的字符串首地址,
复制到%edi中之前如3.3.6中分析的,它将栈中的两个参数保存到寄存器%rdx和%rcx中,作为参数传递并调用printf。
3.3.8c getchar函数

将输入缓冲区的地址通过%rdi传递过去,然后调用gets来实现getchar的功能。
3.3.8d atoi函数


将argv[3]通过%rdi传递给strtol函数,来实现atoi()函数的功能。
3.3.8e sleep函数


将atoi的返回值%eax通过%rdi传递给sleep函数,来实现延时功能。

3.4 本章小结
这一阶段主要使用的是编译器。编译器(ccl)将hello.i文件中hello.c文件部分的内容翻译为汇编格式,hello.s中对包括main函数的变量进行了声明。对于hello.c中定义的函数,则在声明之后跟上它内部相应的代码。编译器此处做的各种工作一个是为了接下来汇编器将.s文件中的汇编语言转化为机器指令,再一个是为链接器确定各个需要重定位信息在文件中的位置。当然,它自己的翻译工作还是主要的。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
汇编:汇编器(as)将hello.s翻译成机器指令语言,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o是一个二进制文件,无法被shell直接通过”./”运行。
4.2 在Ubuntu下汇编的命令

as hello.s -o hello.o或是gcc -c hello.s -o hello.o
这里采用gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o

4.3 可重定位目标elf格式
readelf -a hello.o

ELF头:

1,ELF头以一个16进制字节的序列开始,这个序列描述了生成该文件的系统字的大小和字节顺序。
2, ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(可重定位、可执行或者可共享的)、机器类型(如x86-64)、节头部表的文件便宜,以及节头部表中条目的大小和数量。
3, 不同节的位置和大小是由节头部表描述的,其中目标文件中的每个节都有一个固定大小的条目。

节头部表:

重定位项目信息:

一个.text节中位置的列表。当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
符号表:

符号表,它存放在程序中定义和引用的函数和全局变量的信息。每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器的符号表不同,.symtab符号表不包含局部变量的条目。

4.4 Hello.o的结果解析

从objdump -d -r hello.o产生的反汇编代码可以看到,左侧的十六进制字节序列分别对应右侧的汇编代码。从对常数的操作可以看出机器是采用的小端表示,而且这些机器指令和汇编语言都是相对应的。
主要不同点:
反汇编产生的汇编代码中不包含对全局变量。
反汇编生成的代码与原汇编代码稍有不同,在hello.c这个程序中主要是l和q的后缀上的差别。
反汇编代码中所有的跳转都是采用的相对寻址。
反汇编代码种call的目标地址是当前的下一条指令,这是因为需要在之后链接完成才能确定函数运行的位置,目前采用的是相对寻址,并在后面添加重定位条目,以便链接器的修改。

4.5 本章小结

经过汇编,hello.c转化成了hello.o可重定位目标文件。它已经将自己准备成二进制文件,同时确定了需要从其它文件那里链接的信息,之后让链接器对把它和需要链接的内容组织起来就能正式成为一个可以运行的程序了。
(第4章1分)

第5章 链接
5.1 链接的概念与作用
链接:hello程序调用了printf等库函数,以printf函数为例,它存在于一个名为printf.o的单独的预编译好了的目标文件中,这个文件必须以某种方式合并到hello.o程序中。链接器(ld)负责这种合并,结果得到hello文件。它是一个可执行目标文件(或简称可执行文件),可以被加载到内存中由系统执行。

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


5.3 可执行目标文件hello的格式
5.3.1ELF头:

这里我们利用readelf读取到了部分elf文件的信息,我们能通过其了解到了这个文件是小端序,采用的是x86-64的系统架构,使用的是UNIX的操作系统。这里我们发现和上一节的读取到的信息有一些区别:上一节我们读取到文件的类型是REL(可重定位文件),而这一节我们读取到的文件的类型是EXEC(可执行文件)
5.3.2 节头

5.3.3 程序头


5.3.4 段节

5.3.5 动态偏移表

5.3.6 重定位节

5.3.7 符号表

5.3.8 版本符号节


5.3.9 版本需求节

5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
查看data dump,可以看到程序是从地址0x401000处开始运行的,直到地址0x401fff处。

编译和汇编完成后的代码段.text节的地址是0x401090,用edb调试运行时,rip最初指向的就是这个位置,之后就根据程序的代码运行。

5.5 链接的重定位过程分析
用objdump -d -r分别对hello和hello.o进行分析。其中hello.o的结果只有main函数的部分,但是call和lea的后面都采用+<main+偏移量>的方式,并且补充有重定位信息,。hello的结果不仅有main的部分,还有其它的调用函数的汇编代码,此外,重定位信息全部删除,不再出现在lea和call后面。
链接过程中,根据hello.o里面的重定位节的信息

举例来看,puts需要被重定位的代码段中的偏移量为0x1b,对应汇编代码的部分

“1a: e8“后面正好是main中偏移量为0x1b的地址,链接时对此处进行修改,通过相对地址确定需要修改成的地址。

5.6 hello的执行流程
由于我的edb没有关于调用函数名称的注释,跳转时只是显示跳转地址,所以结合着gdb来看。
edb加载hello以后,点击运行,rip指向0x401090,这就是代码段的开始位置。

运行到0x4010ba那行的hlt时,程序就会终止。
结合gdb来看,这个位置就是<_start>函数的开头。

然后edb往下运行,进入<_start>后,下一个调用的函数是<__libc_start_main@GLIBC_2.2.5>,此外还有<_livc_csu_init>,<_init>,<frame_dummy>,<register_tm_clones>。
之后执行main的过程中可能调用printf, exit, sleep, getchar。
main之后执行的子程序由exit, cxa_thread_atexit_impl,fini。

5.7 Hello的动态链接分析
动态链接库中的函数在程序加载的过程中才会确定地址,所以直到运行之前,包括在汇编代码中,涉及需要动态链接的库函数的寻址都是采用的PC相对引用。可以加载而不需要重定位的代码称为位置无关代码(PIC)。
hello程序对动态链接库的引用,基于这样一个事实:
无论我们在内存的何处加载一个目标模块(包括共享目标模块),数据段与代码段的距离总是保持不变。因此,代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量,与代码段和数据段的绝对内存位置无关。

  1. PIC数据引用
    编译器对全局变量PIC的引用利用了这个事实,它在数据段开始的地方创建了一个表,叫做全局偏移量表(Global Offset Table, GOT)。在GOT中,每个被这个目标模块引用的全局数据目标(过程或全局变量,在hello.c中,比如sleepsecs)都有一个8字节的条目。编译器还为GOT中的每个条目生成一个重定位记录。加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。每个引用全局目标的目标模块都有自己的GOT。
  2. PIC函数调用
    调用共享库定义的函数是,编译器无法预测函数的运行时地址,GNU编译系统采用延迟绑定的方法,将过程地址的绑定推迟到第一次调用该过程时。它是通过GOT和过程链接表(PLT)实现的。前者是数据段的一部分,后者是代码段的一部分。PLT是一个数组,其中每个条目是16字节代码。每个库函数都有自己的PLT条目,PLT[0]是一个特殊的条目,跳转到动态链接器中。从PLT[2]开始的条目调用用户代码调用的函数。
    当某个动态链接函数第一次被调用时先进入对应的PLT条目例如PLT[2],然后PLT指令跳转到对应的GOT条目中例如GOT[4],其内容是PLT[2]的下一条指令。然后将函数的ID压入栈中后跳转到PLT[0]。PLT[0]通过GOT[1]将动态链接库的一个参数压入栈中,再通过GOT[2]间接跳转进动态链接器中。动态链接器使用两个栈条目来确定函数的运行时位置,用这个地址重写GOT[4],然后再次调用函数。经过上述操作,再次调用时PLT[2]会直接跳转通过GOT[4]跳转到函数而不是PLT[2]的下一条地址。
    hello.o在链接完成后,readelf -a hello查看动态链接的部分。

    可以看到这部分将动态链接时需要的共享库,还有链接到的位置都标出来了。之后在加载过程中就会根据这些信息进行动态链接。
    下面的这一段对应printf、getchar等函数的调用,它们经过dl_init后,都变成了对共享库相对寻址的格式,并没有将共享库的代码拷贝到源码中。

5.8 本章小结
本阶段hello.o与其它的文件会合后,经过链接器整合,终于变成了一个可以运行的程序,进入调用进程的阶段。

(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程的经典定义:一个执行中程序的实例。
作用:
它提供给应用程序两个抽象,一个独立的逻辑控制流,一个私有的地址空间。系统中每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 作用
shell自己也是一个进程,但是它可以根据用户命令,作为父进程生成子进程和调用其它的程序。
shell有自己的内置指令,在这一点上与一个普通的C语言程序更为相似。
shell可以发送调用系统函数发送信号给其它进程(也可以发送给自己),自己也可以被其它程序调用。
6.2.2 处理流程
读取用户输入
将用户输入转化为适当的数据格式保存
判断是否为内置指令以及后台执行
如果不是内置指令,寻找是否有相应的可执行文件
通过execve传递参数并调用新的程序
等待并回收子程序。

6.3 Hello的fork进程创建过程
fork()函数创建子进程,返回给父进程子进程的pid,返回给子进程0。
6.4 Hello的execve过程
int execve(char *filename, char *argv[], char *envp[])

  1. 在当前进程中载入并运行程序: loader加载器函数
    filename:可执行文件
    目标文件或脚本(用#!指明解释器,如 #!/bin/bash)
    argv:参数列表,惯例:argv[0]==filename
    envp:环境变量列表
    “name=value” strings (e.g., USER=droh)
    getenv, setenv, unsetenv, printenv
  2. Loader删除子进程现有的虚拟内存段,创建一组新的段(栈与堆初始化为0),并将虚拟地址空间中的页映射到可执行文件的页大小的片chunk,新的代码与数据段被初始化为可执行文件的内容,然后跳到_start………… 除了一些头部信息实际没读文件,直到缺页中断
  3. 覆盖当前进程的代码、数据、栈
    保留:有相同的PID,继承已打开的文件描述符和信号上下文
    调用一次并从不返回,除非有错误,例如,指定的文件不存在。
    6.5 Hello的进程执行
    多个流并发地执行的一般现象被称为并发。一个进程和其他进轮流运行的概念称为多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。

内核(可以理解为操作系统的一部分)为每个进程维持一个上下文。上下文就是内核重新启动一个先前被抢占的进程所需的状态。在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个过程称为调度。
shell作为父进程产生子进程,如果hello是一个前台运行的程序,shell就会等待直到hello结束并回收hello所在的子进程。如果是后台,就继续进行其它的操作。下图的shell1中就包含了前台和后台的两个hello的进程
6.6 hello的异常与信号处理
异常总共有4类:中断,陷阱,故障和终止。
类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令
终止 不可恢复的错误 同步 不会返回

  1. 异步异常(中断)
  2. 陷阱
  3. 故障
  4. 终止
    上面这些是针对四种异常的处理流程,其中的关键是发送信号、接收信号。系统对每个信号有默认的行为,但是用户也可以通过signal函数设置一些信号的处理程序。根据处理程序的结果,决定是否返回到下一行或重新执行指令,或者是终止程序。

1,不停乱按

一开始输入./hello 1180301017 wuweilin 2。hello程序开始运行sleep函数会是程序间歇性地休眠两秒,总共8次。期间的输入有乱按的,也有的是正确的命令。可以看到,hello运行过程中,无论输入什么指令(除非是停止之类的)都不会引起变化,当运行结束后,发现之前回车间隔之间的输入都会作为shell的命令读入,应该是被放到了缓冲区中。
2,Ctrl-Z

Ctrl-Z后运行ps

Ctrl-Z后运行jobs

Ctrl-Z后运行pstree

Ctrl-Z后运行fg

输入ctrl-z后,hello的进程停止,但是没有消失,这时如果输入fg指令,hello会继续运行(不是从头开始)
Ctrl-Z后运行kill

如果停止的时候输入kill指令,shell会调用kill函数发送SIGKILL信号,终止hello的进程。
3,Ctrl-C

输入ctrl-c指令会发送信号SIGINT给hello的进程,通过信号处理程序直接将hello的进程终止。
6.7本章小结
shell调用fork函数生成子进程,之后execve函数调用hello程序,到了这里hello就从一个program真正的变成了一个process。不过要想顺利运行,hello还要面对各种潜在的异常,此外还要注意shell想不想要它活着。

(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1 物理地址
计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每个字节都有唯一的物理地址(Physical Address, PA)。CPU访问内存的最自然的方式就是使用物理地址,我们把这种方式称为物理寻址(physical addressing),
早期的PC使用物理寻址,现在的某些处理器也在使用,但是现代处理器主要还是采用的一种称为虚拟寻址的寻址形式。
7.1.2 逻辑地址[7]
逻辑地址是CPU所生成的地址,是内部和编程使用的,并不唯一。比如,当进行C语言指针的编程时,可以读取指针变量的本身值(&操作),实际上这个值就是逻辑地址,当我们用printf输出指针的值时,并不是像edb、gdb调试时输出一大长串不知道从哪里开始计数的地址值,而是从0x0开始的。逻辑地址是相当于当前进程数据段的地址,类似于偏移地址,与绝对物理地址无关。
逻辑地址 采用“段地址:偏移地址”的形式。
举例来说,逻辑地址23:8048000 段寄存器(CS等16位):偏移地址(16/32/64)
实模式下: 逻辑地址CS:EA → 物理地址CS16+EA
保护模式下:以段描述符作为下标,到GDT/LDT表查表获得段地址,
段地址+偏移地址=线性地址。
7.1.3 线性地址
如果一个地址空间中的整数是连续的,我们就说它是一个线性地址空间
7.1.4 虚拟地址
在一个带虚拟内存的系统中,CPU从一个有N=2n个地址的地址空间中生成虚拟地址,这个地址空间就成为虚拟地址空间,大小上描述为n位的地址空间。
需要注意的一点是,这里n位的地址空间对应磁盘上N个字节大小的虚拟内存,虚拟页把虚拟内存划分为P个字节大小的块。
cpu通过虚拟地址访问物理地址的示例如下。
7.2 Intel逻辑地址到线性地址的变换-段式管理
 段式管理: 逻辑地址->线性地址==虚拟地址
 页式管理: 虚拟地址->物理地址
段寄存器(16位),用于存放段选择符
CS(代码段):程序代码所在段 SS(栈段):栈区所在段
DS(数据段):全局静态数据区所在段
其他3个段寄存器ES、GS和FS可指向任意数据段
右侧是段寄存器整体结构的示意图。
下面是段选择符字段结构以及环保护的示意图。

段选择符各字段含义:
TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)。高13位-8K个索引用来确定当前使用的段描述符在描述符表中的位置。CS寄存器中的RPL字段表示CPU的当前特权级(Current Privilege Level,CPL)
环保护:内核工作在0环,用户工作在3环,中间环留给中间软件用。Linux仅用第0和第3环。RPL=00,为第0级,位于最高级的内核态,RPL=11;为第3级,位于最低级的用户态,第0级高于第3级
逻辑地址的格式为:段地址(段选择符):偏移地址。
例如,已知逻辑空间地址为2m个字节(也就是说逻辑地址的长度是m位),已知页大小是2n字节。那么一共可以有2^(m-n)个页。因此页码部分会占m-n位,之后的n位,用来存储页偏移。
逻辑地址根据段选择符找到对应的页,然后根据偏移地址确定段内偏移量,确定相应的线性地址。按照右侧这张图进行转换。
比如下面这个例子。页大小为4B,而逻辑内存为32B(8页),逻辑地址0的页号为0,页号0对应帧5,因此逻辑地址映射为物理地址5
4+0=20。逻辑地址3映射物理地址54+3=23。逻辑地址13(43+1,页号为3,偏移为1,因此帧号为2),映射到物理地址9。

7.3 Hello的线性地址到物理地址的变换-页式管理
7.3.1 虚拟内存
线性地址其实就是指虚拟地址,虚拟内存的页式管理系统的基本形式如下。
概念上而言,虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每个字节都有唯一的虚拟地址,作为到数组的索引。整个虚拟内存被划分为许多个固定大小的虚拟页,每个虚拟页的大小为P=2p字节,物理页的大小也为P字节。
上面的图中PTE是页表条目,VP是虚拟页,PP是物理页。页表常驻内存,负责将虚拟页映射到物理页。每个PTE由一个有效位和一个n位地址字段组成,有效位表示该虚拟页当前是否被缓存到DRAM(主存)中,如果设置了有效位,那么PTE的n位地址字段就表示DRAM中相应的物理页的起始位置,否则表示该虚拟页尚未被分配。这里有一个要注意的点:DRAM缓存是全相联的,所以任意的物理页可以包含任意的虚拟页。
当虚拟地址读取页表时,如果有效位为1,则表示这个虚拟页已经被缓存到DRAM中了,就可以使用对应PTE中的物理页地址PP(已经被缓存的物理页的起始地址)构造出这个字的物理地址。上述过程为页命中。
如果有效位为0,这种现象称为缺页,并且会触发一个异常。内核从磁盘的虚拟内存中复制虚拟页VP到DRAM内存中的PP,更新对应的PTE,然后返回,重新访问。过程中有可能出现牺牲页。页在磁盘和内存之间传送的活动叫做交换(包括换入,换出)或者页面调度(页面调入,页面调出)。现代系统均采用一种叫做按需页面调度的策略,即一直等待,直到不命中发生时,才换入页面。
7.3.2 虚拟内存的地址翻译
在PTE上扩展许可位可以提供更好地访问控制。
内存管理单元(MMU)每次访问数据时都要检查许可位(段错误)
下面是带有许可位的页表示例:
地址翻译相关的符号:
 Basic Parameters 基本参数
 N = 2n : 虚拟地址空间中的地址数量
 M = 2m : 物理地址空间中的地址数量
 P = 2p : 页的大小 (bytes)
 Components of the virtual address (VA) 虚拟地址组成部分
 TLBI: TLB index----TLB索引
 TLBT: TLB tag----TLB标记
 VPO: Virtual page offset----虚拟页面偏移量(字节)
 VPN: Virtual page number----虚拟页号
 Components of the physical address (PA)物理地址组成部分
 PPO: Physical page offset (same as VPO)----物理页面偏移量
 PPN: Physical page number----物理页号
虚拟地址基于页表的翻译示意图如下:
地址翻译的几种情况如下:

  1. 页面命中
  2. 缺页
    结合高速缓存和虚拟内存的操作图如下:
    7.3.3 TLB加速地址翻译
    从虚拟内存的管理方式来看,每次CPU产生一个虚拟地址,MMU都要查阅一个PTE,以便将虚拟地址翻译为物理地址。如果发生缺页,开销可能是几十到几百个周期;如果页命中,开销就下降到1至2个周期,但是即便是这么小的开销,许多系统也要试图消除。它们在MMU中包括了一个与PTE相关的小缓存,称为翻译后备缓冲器(TLB)。
    TLB的每一行都保存着一个由单个PTE组成的块,而且TLB具有高度的相联度。
    TLB的结构如下:
    TLB条目的索引和标记位是由VPN组成的。
    注意:这里TLB只是将PTE缓存,加速构建物理地址。
    TLB命中的处理
    TLB不命中的处理
    如果TLB命中,相对于直接到高速缓存中给寻找PTE要快得多。乍看起来TLB如果不命中会引发额外的内存访问,从而加大开销,但是实际上这种情况很少发生,所以还是比直接找PTE要快的。这一点主要归功于局部性。
    7.4 TLB与四级页表支持下的VA到PA的变换
    整个过程从CPU出发。cpu内部将逻辑地址转化为虚拟地址以后,传送给MMU进行地址翻译,MMU首先将VPN取出,构建TLB的标记位和组索引,然后在翻译后备缓冲器TLB中寻找对应的PTE条目。
    这里的TLB有16个组,每组4个条目(四路相联),所以构建时将VPN的低4位取出作为组索引,剩余位作为标记位。通过组索引和行匹配寻找对应的PPN。如果命中,就将其取出,构建下一步使用的PPN。
    如果TLB不命中,MMU就通过4级页表从高速缓存中取出相应的PTE,作为PPN构建物理地址,同时在TLB中更新。
    对于Core i7的内存系统,四级页表翻译的过程如下。
    多级页表的虚拟地址翻译与单级页表的主要区别在于页表的保存形式。
    根据有关部门的信息,Core i7支持48位(256TB)的虚拟内存。这里的第一级页表有P=29个指向第二级页表的PTE,每个PTE可以涵盖大小为248B/29=239B=29GB=512GB的虚拟内存片,只要对应虚拟内存片被分配了,PTE就指向下一级页表的基址,如果没有被分配,就不存在下一级页表。这对于二、三级页表也是一样的道理。到了第四级页表时,它的有效PTE就指向物理页的基址了,这时就可以作为PPN与VPO共同构建物理地址。
    多级页表从两个角度减少了内存要求。第一,如果一级页表中的一个PTE是空的,那么相应的所有低级页表都不存在,这意味着巨大的潜在节约。第二,只有一级页表才需要总是缓存在主存中,虚拟内存系统可以在需要时创建、页面调入或调出二级页表,这就减少了主存的压力;只有最经常使用的二级页表才需要缓存在主存中。

7.5 三级Cache支持下的物理内存访问
在MMU将虚拟地址翻译为物理地址以后,就会到L1-cache中寻找对应的字节。
L1-cache中字节的物理地址按照如下格式访问。

L1-cache的结构构成如下
因为L1高速缓存是直接映射高速缓存,所以每一组只有一行。在寻找对应字节时,首先根据组索引确定要访问的组,然后看有效位是否为真,为0则缓存不命中。如果有效位为1,再看缓存中的标记位是否与物理地址给出的标记位匹配,不匹配则缓存不命中。如果匹配,就根据块偏移取出被缓存的字节内容。
如果缓存不命中,就需要从低级缓存中取出包含对应字节信息的块,之后再重新访问。L2和L3高速缓存不是直接映射高速缓存,每一组内有多个行。对于它们的访问与L1类似,只是在组内需要一行行的进行标志位匹配。
7.6 hello进程fork时的内存映射
fork函数被shell调用,创建用来调用可执行文件hello的进程。
内核为hello的进程创建各种数据结构,并分配给它一个唯一的pid。同时,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在hello的进程中返回时,hello的虚拟内存刚好和调用fork时存在的虚拟内存相同。当hello和shell的进程中任意一个后来进行写操作时,写时复制机制就会创建新的页面,比如,当shell的子进程通过execve加载并运行可执行文件hello的时候。因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射
hello进程会通过execve加载可执行目标文件hello,加载并运行包含在其中的程序。整个过程包含以下步骤:
删除已经存在的用户区域,即私有的写时复制区域的用户部分的区域结构。(图7.6.1中右侧的私有的写时复制区域)
映射私有区域。为hello程序的代码、数据、bss和栈区创建新的区域结构。所有的这些新的区域都是私有的、写时复制的(图7.6.2)。代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小在hello文件中指出。

由于hello.c程序中没有未初始化的全局变量和函数,所以这部分的大小为0。栈和堆的区域也是请求二进制零的,初始长度为0.
映射共享区域。前面在链接的部分提到过,需要共享链接的对象(或目标)只有在加载并执行时才会动态链接到程序,并调整各个相对地址。这里hello中,譬如printf等等共享库的函数都会先动态链接到hello程序中,然后再映射到用户虚拟地址空间中的共享区域内部。
设置程序计数器(PC)。execve做的最后一件事就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程的时候,它将从这个入口点开始执行。
加载器对用户地址空间的映射格式如下。这里hello
7.8 缺页故障与缺页中断处理
如果虚拟地址出发了缺页异常,就会按照下图依次判断是否是1、2或3的情况。

7.9动态存储分配管理
动态存储分配管理由动态内存分配器完成。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。堆是一个请求二进制零的区域,它紧接在未初始化的数据区后开始,并向上生长(向更高的地址)。分配器将堆视为一组不同大小的块的集合来维护。
每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显示地被应用程序所分配。
一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
动态内存分配器从堆中获得空间,将对应的块标记为已分配,回收时将堆标记为未分配。而分配和回收的过程中,往往涉及到分割、合并等操作。
动态内存分配器的目标是在对齐块的基础上,尽可能地提高吞吐率及空间占用率,即减少因为内存分配造成的碎片。其实现常见的数据结构有隐式空闲链表、显式空闲链表、分离空闲链表,常见的放置策略有首次适配、下一次适配和最佳适配。
7.10本章小结
这一阶段hello从机器层面上向我们展示了它作为一个进程是如何开始的,以及它是如何获得和被分配虚拟内存的。

(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
文件就是一个字节序列,所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Linux以文件的方式对I/O设备进行读写,将设备均映射为文件。对文件的操作,内核提供了一种简单、低级的应用接口,即Unix I/O接口。
Unix I/O接口提供了以下函数供应用程序调用:
打开文件:int open(char *filename, int flags, mode_t mode);
关闭文件:int close(int fd);
读文件:ssize_t read(int fd, void *buf, size_t n);
写文件:ssize_t write(int fd, const void *buf, size_t n);
8.3 printf的实现分析
printf的函数原型如下:
int printf(const char *fmt, …)
{
int i;
char buf[256];

 va_list arg = (va_list)((char*)(&fmt) + 4); i = vsprintf(buf, fmt, arg); write(buf, i); return i;
}

其中*fmt是格式化用到的字符串,而后面省略的则是可变的形参,即printf(“%d”, i)中的i,对应于字符串里面的缺省内容。
va_start的作用是取到fmt中的第一个参数的地址,下面的write来自Unix I/O,而其中的vsprintf则是用来格式化的函数。这个函数的返回值是要打印出的字符串的长度,也就是write函数中的i。该函数会将printbuf根据fmt格式化字符和相应的参数进行格式化,产生格式化的输出,从而write能够打印。
在Linux下,write通过执行syscall指令实现了对系统服务的调用,从而使内核执行打印操作。内核会通过字符显示子程序,根据传入的ASCII码到字模库读取字符对应的点阵,然后通过vram(显存)对字符串进行输出。显示芯片将按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),最终实现printf中字符串在屏幕上的输出。

8.4 getchar的实现分析
getchar定义在stdio.h文件中,我们在stdio.h中可以找到其相关的定义
getc()的函数原型如下:
getchar函数实际上就是getc(stdin),即标准输入下的getc()。通过调用read函数返回字符。其中read函数的第一个参数是描述符fd,0代表标准输入。第二个参数输入内容的指针,这里也就是字符c的地址,最后一个参数是1,代表读入一个字符。read函数的返回值是读入的字符数,如果为1说明读入成功,那么直接返回字符,否则说明读到了buf的最后。
read函数同样通过sys_call中断来调用内核中的系统函数。键盘中断处理子程序会接受按键扫描码并将其转换为ASCII码后保存在缓冲区。然后read函数调用的系统函数可以对缓冲区ASCII码进行读取,直到接受回车键返回。
8.5本章小结
Linux将I/O输入都抽象为了文件,并提供相应的Unix I/O接口,用户可以通过这些接口实现输入与输出。hello私底下做了什么,只有经过这一步才能被我们看到。
(第8章1分)
结论
hello的一生从它被编辑成一个名为hello.c的文件开始,经过编译系统的预处理、编译、汇编、链接过程后变成了一个名为hello的可执行文件。shell通过命令行生成对子进程的调用并执行hello这个程序,为它分配虚拟内存,到这里完成了”P2P”,以及”020”的第一步。之后hello就根据自己的程序运行,期间还会有一些I/O输入输出。在hello执行完成以后,shell又负责回收hello的进程,最终hello从内存中消失,到这里”020”就算是完成了。
从计算机系统的角度来看,所有的程序的生命历程都可以认为和hello类似,虽然hello这个程序的c文件看起来很简单,但是里面蕴涵的内容却十分丰富。如果单看hello程序可能有人会问为什么计算机处理起程序来这么复杂,但是如果将把问题横向来看,无论多么复杂的程序,在计算机系统内部都可以这样处理,也就释然了。通过一些抽象的手段和模块化的设计, 使得程序不仅能运行起来,而且还能运行得快,但是我相信,不就的将来会出现更优的架构,来实现程序性能质的飞跃。
(结论0分,缺失 -1分,根据内容酌情加分)

附件
hello.c – 源文件
hello.i – 预处理完成后产生的文件,用于下一阶段的编译。
hello.s – 预处理文件对应翻译成的汇编语言格式的文本文件,用于下一阶段的汇编。
Hello.o.txt – hello.o经过objdump产生的汇编文本
Hello.out.txt – hello / hello.out经过objdump产生的汇编文本
hello.o – hello.s转化成的可重定位目标文件,经过链接后生成可执行目标文件。
hello – 可执行目标文件
(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 《深入理解计算机系统》
[2] 《鸟哥的Linux私房菜》
[3] 《深入理解Linux内核》
[4] 《Linux命令行与shell脚本编程大全》
(参考文献0分,缺失 -1分)

吴畏嶙2019大作业相关推荐

  1. c语言程序设计0039大作业答案,2019西南大学0039C语言程序设计机考大作业答案.doc...

    - PAGE 1 - 西南大学网络与继续教育学院课程考试试题卷 类别: 网教 2019年 6月 课程名称[编号]: C语言程序设计 [0039] A卷 大作业 满分:100 分 一.大作业题目 1.简 ...

  2. html大作业网页代码 ——2019凡客服装店铺商城(1页) HTML+CSS+JavaScript HTML+CSS大作业_ 服装店铺网页制作作业_购物网页设计...

    HTML5期末大作业:服装店铺商城网站设计--2019凡客服装店铺商城(1页) HTML+CSS+JavaScript 文章目录 HTML5期末大作业:服装店铺商城网站设计--2019凡客服装店铺商城 ...

  3. HTML+CSS静态页面网页设计作业——2019凡客服装店铺商城(1页) HTML+CSS+JavaScript HTML+CSS大作业_ 服装店铺网页制作作业_购物网页设计...

    HTML5期末大作业:服装店铺商城网站设计--2019凡客服装店铺商城(1页) HTML+CSS+JavaScript HTML+CSS大作业: 服装店铺网页制作作业_购物网页设计- 文章目录 HTM ...

  4. 0039c语言作业答案2020,西南大学2019年网络与继续教育[0039]《C语言程序设计》大作业试题(资料).doc...

    西南大学2019年网络与继续教育[0039]<C语言程序设计>大作业试题(资料).doc 文档编号:764150 文档页数:4 上传时间: 2019-10-12 文档级别: 文档类型:do ...

  5. HTML5期末大作业:服装店铺商城网站设计——2019凡客服装店铺商城(1页) HTML+CSS+JavaScript HTML+CSS大作业: 服装店铺网页制作作业_购物网页设计...

    HTML5期末大作业:服装店铺商城网站设计--2019凡客服装店铺商城(1页) HTML+CSS+JavaScript HTML+CSS大作业: 服装店铺网页制作作业_购物网页设计- 常见网页设计作业 ...

  6. 2019级C语言大作业 - 火柴人试炼之地

    火柴人试炼之地 C语言大作业 分享19级同学大一上学期用C语言实现的火柴人试炼之地.分步骤代码.图片音乐素材.可执行程序可以从百度网盘下载: 链接:https://pan.baidu.com/s/1X ...

  7. 哈工大计算机系统2022大作业:程序人生-Hello‘s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机类 学    号 120L022115 班    级 2003007 学       生 王炳轩 指 导 ...

  8. 哈工大计算机系统大作业——程序人生-Hello’s P2P

    计算机系统 大作业 题          目程序人生-Hello's P2P 专          业 计算机科学与技术 学       号120L022401 班          级 200300 ...

  9. 计算机系统-大作业-hello的一生-哈尔滨工业大学2020级

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学   号 120L022417 班   级 2003008 学       生 董亦轩 指 导 教 ...

最新文章

  1. 遇到这四种面试官,接了 Offer 你可能会后悔
  2. 感受野receptive field个人理解
  3. Android本地应用程序应用方式介绍
  4. 安装keepalived执行make报错的解决方法
  5. 恢复初始快捷键_CAD常用命令快捷键大全,47个快捷键50个CAD技巧,教你快速画图...
  6. Linux下安装Python3.6(可用)
  7. 浅谈C++的智能指针
  8. 基于单片机的银行排队叫号系统的设计
  9. wordpress单独html页面,wordpress独立留言板页面
  10. python解析mht文件_实现MHT文件格式的解析和内容抽取
  11. 360云盘php,360云盘外链解析php源码
  12. oracle羊毛,弃Cloudflare,薅Oracle羊毛
  13. OpenCV实践之路——方形图片对角线切割
  14. 秀一波酷炫可视化大屏!
  15. 分层测试(Layered Testing Approach)
  16. Python自动化构建雷电模拟器
  17. 用思维导图和孩子们一起了解“什么是春节”
  18. 计算机二级15年大纲,2015年下半年全国计算机二级考试MSoffice高级应用大纲
  19. keil中更改stm32芯片类型需要修改的配置
  20. win2012服务器系统要求,Windows server2012公开报价多少?安装系统有什么要求?

热门文章

  1. PMP考试技巧+前九章内容提炼整理(不定时更新)
  2. 计算机辅助药物设计:分子对接
  3. 自己创建一个小操作系统
  4. vps php mail,TMail v5.2 – PHP多域名临时电子邮件系统
  5. 小程序下拉刷新 上拉加载等多
  6. 【华人学者风采】翟成祥 伊利诺伊大学香槟分校
  7. 邓亚萍大手笔一掷20亿研发即刻搜索2年就倒闭带来的思考
  8. 高德地图各种摄像头图标_高德导航中,限速摄像头,违章摄像头,监控摄像头各有什么区别...
  9. 架构简析| 一种自动探索Minecraft的智能体
  10. (待删除)js时间日期毫秒数之间的相互转换合集