计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机
学   号 1190200721
班   级 1936602
学 生 张少卿
指 导 教 师 刘宏伟

计算机科学与技术学院
2021年6月

摘 要

一个c语言程序到它完整地在计算机系统中实现需要经历过许多过程。首先是要将.c文件转换为可执行文件。这其中经历了预处理、编译、汇编、链接4个过程,形成可执行文件后,计算机就能读懂我们的.c代码去执行其中的语句。
计算机在执行文件时候又有许多学问。例如:进程的概念,计算机存储的管理,IO的管理。一个文件的执行其背后是计算机许许多多不同且重要的功能合作完成的。

关键词:预处理;汇编;进程;存储管理;IO管理;

文章目录

  • 摘 要
  • 第1章 概述
    • 1.1 Hello简介
    • 1.2 环境与工具
    • 1.3 中间结果
    • 1.4 本章小结
  • 第2章 预处理
    • 2.1 预处理的概念与作用
    • 2.2在Ubuntu下预处理的命令
    • 2.3 Hello的预处理结果解析
    • 2.4 本章小结
  • 第3章 编译
    • 3.1 编译的概念与作用
    • 3.2 在Ubuntu下编译的命令
    • 3.3 Hello的编译结果解析
      • 3.3.1 变量/常量 and 赋初值/不赋初值 and 类型转换
      • 3.3.2 sizeof
      • 3.3.3 算术操作:+ - * / % ++ -- 取正/负 符合
      • 3.3.4逻辑/位操作
      • 3.3.5关系操作
      • 3.3.6数组/指针/结构操作
      • 3.3.7 循环
      • 3.3.8函数
    • 3.4 本章小结
  • 第4章 汇编
    • 4.1 汇编的概念与作用
    • 4.2 在Ubuntu下汇编的命令
    • 4.3 可重定位目标elf格式
    • 4.4 Hello.o的结果解析
    • 4.5 本章小结
  • 第5章 链接
    • 5.1 链接的概念与作用
    • 5.2 在Ubuntu下链接的命令
    • 5.3 可执行目标文件hello的格式
    • 5.4 hello的虚拟地址空间
    • 5.5 链接的重定位过程分析
    • 5.6 hello的执行流程
    • 5.7 Hello的动态链接分析
    • 5.8 本章小结
  • 第6章 hello进程管理
    • 6.1 进程的概念与作用
    • 6.2 简述壳Shell-bash的作用与处理流程
    • 6.3 Hello的fork进程创建过程
    • 6.4 Hello的execve过程
    • 6.5 Hello的进程执行
    • 6.6 hello的异常与信号处理
    • 6.7本章小结
  • 第7章 hello的存储管理
    • 7.1 hello的存储器地址空间
    • 7.2 Intel逻辑地址到线性地址的变换-段式管理
    • 7.3 Hello的线性地址到物理地址的变换-页式管理
    • 7.4 TLB与四级页表支持下的VA到PA的变换
    • 7.5 三级Cache支持下的物理内存访问
    • 7.6 hello进程fork时的内存映射
    • 7.7 hello进程execve时的内存映射
    • 7.8 缺页故障与缺页中断处理
    • 7.9动态存储分配管理
    • 7.10本章小结
  • 第8章 hello的IO管理
    • 8.1 Linux的IO设备管理方法
    • 8.2 简述Unix IO接口及其函数
    • 8.3 printf的实现分析
    • 8.4 getchar的实现分析
    • 8.5本章小结
  • 附件
  • 参考文献

第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:Hello的生命从hello.c开始,我们的hello.c首先经过第一道关卡——预处理,预处理器cpp将它预处理,将它变形为hello.i。于是我们的hello.i懵懵懂懂地来到了第二关卡——编译,编译器“咔咔咔”将它彻底改头换面,我们的hello.i就变为了hello.s。可是我们的发廊总裁“汇编器”对它还是不太满意。于是稍加修改。Hello.o文件问世。最后,hello.o满意地来到了最后一关。链接器ld将一身衣服“库函数”穿戴在它的身上,我们的hello就从原来的乡下小子hello.c变为了大明星hello可执行目标文件,实现P2P过程。
020:子进程调用execve,映射虚拟内存并载入物理内存,进入程序入口处开始执行,同时,CPU为运行的hello分配时间片并执行逻辑控制流。在中途调用异常处理函数处理可能出现的异常。最后当hello进程终止,父进程shell将回收hello,接着内核删除相关数据结构的整个过程叫做020。

1.2 环境与工具

硬件:Intel® Core™ i5-9300H CPU @ 2.40GHz 8G RAM
虚拟机VUbuntu18.4
开发工具:gcc ld readelf gedit objdump edb gdb hexedit
Visual Studio 2019;CodeBlocks 64位Mware Workstation Pro15.0

1.3 中间结果

Hello 可执行文件
Hello.i 预处理后文件
Hello.s 编译后文件
Hello.o 汇编后文件
Hello_.s 反汇编文件

1.4 本章小结

本章简要介绍了hello的P2P,020的过程,列出了运行的环境和使用的工具,以及中间结果。

第2章 预处理

2.1 预处理的概念与作用

预处理概念:预处理器(cpp)根据以字符#开头的命令,修改原始的c程序。例如:#include<stdio.h>告诉预处理器读取系统头文件stdio.h的内容,并把它插入到程序文本中。得到一个新的c程序,通常是以.i命名。
预处理作用:
1.处理文件包含
2.处理宏定义
3.处理注释
4.处理条件编译
5.处理一些特殊控制指令

2.2在Ubuntu下预处理的命令

gcc hello.c -E -o hello.i

2.3 Hello的预处理结果解析

.i文件的开头:

其中数字1、 2、 3、 4表示:
1:表示新文件的开始
2:表示返回一个文件(包含另外一个文件之后)
3:表示一下文本来自系统头文件,因此应该抑制某些警告
4:表示一下文本应该被视为包含在隐式的extern“C”块中。
Stdio.h文件预处理后:

定义了些奇怪的符号:

关键字extern标示变量或者函数的定义在别的文件中:

可以发现.i文件中有许多的extern。
拉到.i文件的最下方,可以发现我们的函数体,其中#的注释都被删除了。

2.4 本章小结

预处理是为了将我们原始的.c文件拓展成更大的c文件,添加了许多的代码是为了方便后续编译器的操作。

第3章 编译

3.1 编译的概念与作用

编译的概念:编译器(ccl)将文本文件.i翻译成文本文件.s,它包含了一个汇编语言程序。
编译的作用:
1.扫描(词法分析)
将源代码程序输入扫描器,将源代码的字符序列分割成一系列记号。
2.语法分析
基于词法分析得到的一系列记号,生成语法树。
3.语义分析
由语义分析器完成,指示判断是否合法,并不判断对错。又分:静态语义:隐含浮点型到整形的转换,会报warning;动态语义:在运行时才能确定。
4.源代码优化(中间语言生成)
中间代码(语言)使得编译器分为前端和后端,前端产生与机器(或环境)无关的中间代码,编译器的后端将中间代码转换为目标机器代码,目的:一个前端对多个后端,适应不同平台。
5.代码生成,目标代码优化
编译器后端主要包括:代码生成器:依赖于目标机器,依赖目标机器的不同字长,寄存器,数据类型等。
目标代码优化器:选择合适的寻址方式,左移右移代替乘除,删除多余指令。

3.2 在Ubuntu下编译的命令

编译的命令:gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析

第一行解释了文件的由来
第二行解释了全局变量名
第四行解释了对齐的大小是4字节

这些应该是说明了这些变量存储的位置。
例如:LC0存储了字符串“Usage: Hello 学号 姓名!”,其存储在.string节中。

3.3.1 变量/常量 and 赋初值/不赋初值 and 类型转换

全局变量:全局变量一般都存储在.rodata节
Hello.c中的Sleepsecs全局变量存放在.long节和.rodata中,存储的值是2,但我们观察c程序,发现其赋值是2.5。所以在这里有一个隐式的转换!将2.5转为int的2!
局部变量:赋初值局部变量一半都存储在.bss节中,未赋初值的局部变量则不存放在某个位置,要是用时才存放在寄存器中
在hello.c中局部变量i在前面的编译语句中没有找到,我们观察对i的调用是在循环中,所以我们去找寻相关的位置。
i就保存在这个位置,看来局部变量在这里是存储在了栈中-4(%rbp)位置。

3.3.2 sizeof

Sizeof函数一般是在编译的时候就处理完成了,它在编译的时候就将变量类型对应的立即数改写到汇编指令中。

3.3.3 算术操作:+ - * / % ++ – 取正/负 符合

算术操作都是在寄存器的基础上完成的。以hello.c为例,在hello.c中出现了++运算。它执行是在循环中,我们去循环体中寻找:

++就是把对应的寄存器+1,同理–就是把对应的寄存器-1。
从上面的例子我们很容易猜到加法运算的指令就是add,至于其他算术操作指令如下:

    • Sub;
    • imul;
  • / 一般是通过加减法来实现;
  • % 同除法;
  • NEG 取负
  • 复合 拆分开一步一步计算

3.3.4逻辑/位操作

同样也是对寄存器进行操作。
与& and;
或| or;
异或^ xor;
非~ not;
左移<<:
算术左移 sal;
逻辑左移 shl;
右移>>:
算术左移 sar;
逻辑左移 shr;

3.3.5关系操作

在hello.c中的循环体中就有关系操作,一般是用cmp进行比较然后设定条件码,然后用j*语句执行相关的语句

例如将i和9进行比较,若小于等于则进入.L4块。以下是所有j*指令:
指令 同义名 跳转条件 描述
Jmp 1 直接跳转
Je Jz ZF 相等/零
Jne Jnz ~ZF 不相等/非零
Js SF 负数
Jns ~SF 非负数
Jg Jnle ~(SF^OF)&ZF 大于(有符号)
Jge Jnl ~(SF^OF) 大于等于(有符号)
Jl Jnge SF^OF 小于(有符号)
Jle Jng (SF^OF)|ZF 小于等于(有符号)
Ja Jnbe CF&ZF 超过(无符号)
Jae Jnb ~CF 超过相等(无符号)
Jb Jnae CF 低于(无符号)
Jbe Jna CF|ZF 低于等于(无符号)

3.3.6数组/指针/结构操作

数组和指针都是以地址来进行操作,在hello.c中有argv数组和argv。在hello.c中argv的地址存放在了-32(%rbp)的位置(具体分析看下面的函数操作)。

所以%rax存放的就是argv[0]的地址。所以%rax+16=argv[2],%rax+8=argv[1],这里的8是一个字节,16是两个字节,刚好是char的大小的倍数。

3.3.7 循环

很显然cmpl指令就是循环体内部的比较语句,如果-4(%rbp)小于等于9则进入.L4块,这是一种guarded-do写法。接下来我们来观察.L4块。

.L4是循环中的语句(具体分析看下面函数调用),两个函数执行完毕后,第一次循环结束,i++,于是addl指令就执行了该操作。然后再此进入.L3块。

3.3.8函数

在hello.c中有许多的函数调用的例子
函数的参数传递:
Main函数

这两条指令执行了main函数的参数传递,函数的参数传递通常是保存在%rdi和%rsi寄存器中。根据.c文件,我们清楚在%edi中保存的是第一个参数argc,在%rsi中保存的是第二个参数*argv。

然后是函数的调用:

我们知道循环中主要是printf和调用sleep函数。这里就涉及了数组的偏移操作,我们知道数组首地址存放在-32(%rbp)中,所以%rax存放的就是argv[0]的地址。所以%rax+16=argv[2],%rax+8=argv[1],这里的8是一个字节,16是两个字节,刚好是char的大小。再将.LC1存放于%edi寄存器,参数设置完毕,接下来就是调用printf函数了。执行完后再将sleepsecs存放于%edi寄存器调用sleep函数。
函数的调用有两种方式:1. 直接调用,2. 间接调用。直接调用的目标是作为指令的一部分编码,间接调用涉及了相对寻址,相对于call指令的下一条指令的地址。
函数的返回return:

当循环体结束后,调用getchar函数,由于return 0,将0存放于%eax寄存器,因为%eax是指定的返回函数存储的寄存器。然后leave和ret,函数调用结束返回。Leave是对栈的操作,取出数据然后%rsp+8/+16。

3.4 本章小结

编译这一步,将hello.c文件从高级语言转变到了汇编语言,我们发现,汇编语言是机器代码的文本表示,更方便跟机器“沟通”。在这一步后,汇编器才能理解我们的程序,从而将我们的程序转变为更底层的机器语言。

第4章 汇编

4.1 汇编的概念与作用

汇编的概念:汇编器(as)将hello.s翻译成机器语言指令,并把这些指令打包成一种叫做可重定位目标程序。将结果保存在目标文件hello.o中
汇编的作用:
将汇编指令转成机器可以直接识别的机器指令

4.2 在Ubuntu下汇编的命令

汇编的指令:gcc -c hello.s -o hello.o

4.3 可重定位目标elf格式

指令:readelf -a hello.o

相关的解读
Magic:魔数 E45 L4C F46
Data数据:补码表示,小端
Entry point address入口点地址:ELF起始位置
Start of program headers程序头起点:程序头表起始位置
Start of section headers:偏移量
Number of program headers:表象个数
Size of section headers节头大小:每个节的大小
节头数量:节个数
字符串表索引头:.strtab节的位置
2.节头部表,不同节的位置和大小都是节头部表描述的,其中每个节都一个固定大小的条目。

相关的说明:
大小:节的字节数
偏移量:节相对于自己的起始位置的偏移量(起始位置跟对齐有关)
对齐:限制了节的起始地址,4就是代表了对齐最小单位是1000。
3.重定位信息

在链接的时候需要对符号重新定义,定义的信息就来自于重定位节。可以看到函数和全局变量都在其中。
4.符号表

存放了程序中定义的全局变量和函数的信息。

4.4 Hello.o的结果解析

在hello.o的反汇编出来的汇编语言中,可以发现其将符号解析并增加了重定位信息,例如原本的.L2块中的jmp .L3转换为了地址。

还有就是全局变量sleepsecs和函数调用。

由于每条指令都有了自己的相对地址,所以分支转移函数调用就可以直接用相关指令的相对地址来进行跳转,同时增加了重定位信息,方便后续链接的时候修改。

4.5 本章小结

在进行汇编过程后,hello.s文件转换为了二进制可重定位文件hello.o,在hello.o中我们hello.s本来是符号的地方都进行了解析转换为了一串地址。所以hello.o文件与hello.s文件有着不同之处,同时也更有利于机器理解。

第5章 链接

5.1 链接的概念与作用

链接的概念:链接器(ld)将多个.o文件合并,得到一个可执行目标文件(或者简称为可执行文件),可以被加载到内存中,由系统执行。
链接的好处:

  1. 模块化:
    一个程序可以分成很多源程序文件
    可以构建共享函数库
  2. 效率高
    时间上,可分开编译
    空间上,无需包含共享库所有代码

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的格式

这里ELF所显示的信息与hello.s的ELF的显示的都是一样的就不再解读,不过我们可以发现hello中节头的数量比hello.s的多得多。
各节头的信息:

节头多了许多,不过阅读的方式没有改变。主要关注地址,偏移量和对齐
共享库的地址:

5.4 hello的虚拟地址空间

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

我们的首要目标是.interp段的内容。有节头部表,可以知道该段的起始地址为0x402e0,那我们现在来查看该段的内容,我们发现其内容为:/lib64/ld-linux-x86-64.so.2。我们知道这是链接器的信息。

接下来搜索.dynstr节,结果为:.libc.so.6.exit.puts.printf.getchar.atoi.sleep.libc_strat_main.GLIBC_2.2.5.gmon_start。显然这些都是共享库的信息。

再来查看.rodata节,里面是字符串常量。

5.5 链接的重定位过程分析

相比于hello.o的反汇编,hello的反汇编多了许多函数,而不仅有main。例如_init、puts、printf等等。同时在hello的反汇编中,地址全部转换为了虚拟地址,起始从400000开始,而不是0开始。

而原先hello.s反汇编中的重定位信息也都使用上改为了符号所对应的虚拟地址,由于符号和指令的相对位置是不变的,所以当求出指令的地址时,符号的地址也可以由这个重定位信息计算出来。

5.6 hello的执行流程

ld-2.31.so!_dl_start
ld-2.31.so!_dl_init
hello!_start 0x400500
libc-2.31.so!__libc_start_main
-libc-2.31.so!__cxa_atexit
-libc-2.31.so!__libc_csu_init
hello!_init
libc-2.31.so!_setjmp
-libc-2.31.so!_sigsetjmp
–libc-2.31.so!__sigjmp_save
hello!main
hello!puts@plt
hello!exit@plt
*hello!printf@plt
*hello!sleep@plt
*hello!getchar@plt
ld-2.31.so!_dl_runtime_resolve_xsave
ld-2.31.so!_dl_fixup
ld-2.31.so!_dl_lookup_symbol_x
libc-2.31.so!exit

5.7 Hello的动态链接分析

(以下格式自行编排,编辑时删除)
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
hello程序对动态链接库的引用,利用代码段和数据段之间距离不变这一个事实,因此代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量。
GNU编译系统使用了一种有趣的延迟绑定技术来解决动态库函数模块调用的问题,将过程地址的绑定推迟到了第一次调用该过程时。
延迟绑定通过全局偏移量表(GOT)和过程链接表(PLT)的交互实现。如果一个目标模块调用定义在共享库中的任何函数,那么就有自己的GOT和PLT。前者是数据段的一部分,后者是代码段的一部分。下图是PLT数组。

GOT是一个数组,其中每个条目是8字节地址。和PLT 联合使用时, GOT[O] 和GOT[l] 包含动态链接器在解析函数地址时会使用的信息。GOT[2] 是动态链接器在ld-linux.so 模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT 条目。
下面以printf为例子来解释动态链接:
第一次调用printf:
第一步:程序进入printf对应的PLT条目。
第二步:第一条PLT指令通过相对应的GOT进行简介跳转,因为每个GOT条目初始都指向它对应的PLT条目的第二条指令,这个简介跳转知识简单地把控制传送回PLT[2]中的下一条指令。
第三步:把printf的ID压入栈中,PTL跳转到PTL[0]
第四步:PTL[0]通过GOT[1]间接地吧动态链接器的一个参数压入栈中,然后通过GOT[2]间接跳转进动态链接器中。动态连接器使用两个栈条目来确定printf的运行地址,将地址重写回printf对应的GOT,再把控制传递给printf。
后续再调用printf时,GOT的间接跳转会直接将控制转移到printf。

Printf调用前GOT表

Printf调用后GOT表

5.8 本章小结

链接器将我们需要的.o文件合并成一个可执行文件,这样的文件就可以在shell中执行了,我们的文件成功从原来的.c文件转换为了一个能在计算机中执行的文件。

第6章 hello进程管理

6.1 进程的概念与作用

进程的概念:进程是计算机科学中最深刻、最成功的概念之一。进程是正在运行的程序的实例。
进程的作用:

  1. 一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
    2.一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。

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

Shell:shell 是一个交互型应用级程序,代表用户运行其他程序。是系统的用户界面,提供了用户与内核进行交互操作的一种接口。他就收哟洪湖输入的命令并把它送入内核去执行。
2.功能:其实 shell 也是一支程序,它由输入设备读取命令,再将其转为计算机可以了解的机械码,然后执行它。各种操作系统都有它自己的 shell,以 DOS 为例,它的 shell 就是 command.com 文件。如同 DOS 下有 NDOS,4DOS,DRDOS 等不同的命令解译程序可以取代标准的 command.com ,UNIX 下除了 Bourne shell(/bin/sh)外还有 C shell(/bin/csh)、Korn shell(/bin/ksh)、Bourne again shell(/bin/bash)、Tenex C shell(tcsh)等其它的 shell。UNIX/linux 将 shell 独立于核心程序之外,使得它就如同一般的应用程序, 可以在不影响操作系统本身的情况下进行修改、更新版本或是添加新的功能。
Shell 是一个命令解释器,它解释由用户输入的命令并且把它们送到内核。不仅如此,Shell 有自己的编程语言用于对命令的编辑,它允许用户编写由 shell 命令组成的程序。Shell 编程语言具有普通编程语言的很多特点,比如它也有循环结构和分支控制结构等,用这种编程语言编写的 Shell 程序与其他应用程序具有同样的效果。
3.处理流程: shell 首先检查命令是否是内部命令,若不是再检查是否是一个应用程序(这里的 应用程序可以是 Linux 本身的实用程序,如 ls 和 rm,也可以是购买的商业程序, 如 xv,或者是自由软件,如 emacs)。然后 shell 在搜索路径里寻找这些应用程序(搜 索路径就是一个能找到可执行程序的目录列表)。如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息。如果能够成 功找到命令,该内部命令或应用程序将被分解为系统调用并传给 Linux 内核。

6.3 Hello的fork进程创建过程

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

6.4 Hello的execve过程

Execve函数在当前的进程的上下文中加载并运行一个新程序。Execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。所以,execve调用一次并从不返回。
子进程调用execve函数,在当前进程的上下文中加载并运行一个新程序即hello程序,execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行hello程序,并映射私有区域,为程序的代码,数据,bss,栈区域创建新的区域结构。新的栈和堆段被初始化为零,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC指向_start地址,_start最终调用hello中的main函数。。注意,execve函数再当前进程的上下文中加载并运行一个新程序。它会覆盖当前进程的地址空间,但是并没有创建一个新进程。新进程仍然有相同的PID,并继承了调用exceve函数时已打开的所有文件。

6.5 Hello的进程执行

进程调度的过程:操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。内核为每一个上下文维持一个上下文,上下文就是内核重新启动一个被抢占的进程所需的状态。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫调度。
在内核调度一个新的进程,上下文切换,实现

  1. 保存当前进程的上下文
  2. 恢复某个先前被抢占的进程被保存的上下文
  3. 将控制传递给这个心回复的进程
    进程时间片:一个继承执行它的控制流的一部分的每一时间段叫做时间片,当时间片的时间用尽后,若当前进程还没执行完毕,控制会转移给内核,有内核选择是否仍执行该进程。
    用户态与核心态转换:处理器使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权。如果没有设置模式位,进程就处于用户模式;设置模式位,进程就处于内核模式。用户模式的进程不允许执行特权指令,不允许直接应用地址空间中内核区内的代码和数据;内核模式下该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

6.6 hello的异常与信号处理

乱按包括回车:

乱按的字符会保存在缓冲区内,如果在sleep这期间我们输入了字符和回车回车,则当我们的hello进程结束后,这些字符会被执行,例如图中的sd、fa、123等

Ctrl+c:会直接终止当前的进程

Ctrl+z:会停止当前的进程

停止后输入ps:

输出了执行进程的PID,TTY,TIME和我们输入的CMD。
停止后输入jobs:

显示了我们当前的所有进程,包括停止的。
停止后输入pstree:

停止后输入fg:

将我们的hello进程调度会前台继续执行。
停止后执行kill -n 9 21378:

Hello进程被杀死。

6.7本章小结

本章介绍了进程与shell执行的相关知识。进程是计算机中最伟大的概念,有了进程才有了我们现在所身处的计算机世界。不同的进程在内核的调度下执行者自己的指令,而没有错误。在进程中遇到异常时,操作系统有着自己的方式来处理。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:在有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。
线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
虚拟地址:CPU启动保护模式后,程序运行在虚拟地址空间中,也就是程序在磁盘中使用的地址。Hello可执行文件反汇编后,每条指令前的地址就是虚拟地址
物理地址:放在寻址总线上的地址。放在寻址总线上,如果是读,电路根据这个地址每位的值就将相应地址的物理内存中的数据放到数据总线中传输。如果是写,电路根据这个地址每位的值就在相应地址的物理内存中放入数据总线上的内容。物理内存是以字节(8位)为单位编址的。

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

逻辑地址由段选择符和偏移量组成,线性地址为段首地址和逻辑地址中的偏移量组成。其中,段首地址存放在段描述符中。而段描述符存放在段描述符表中。
一般逻辑地址实际是由48位组成,前16位包括[段选择符],后32位[段内偏移量]。其中段指的是可执行文件中的代码段,数据段等等。
段选择符用于寻找段描述符表,段内偏移量是指令地址相对于段基址的偏移量。
段选择符的16位格式如下:

索引:描述符表的索引
TI:如果 TI 是 0。「描述符表」是「全局描述符表(GDT)」,如果 TI 是 1。「描述符表」是「局部描述表(LDT)」
RPL:段的级别。为 0,位于最高级别的内核态。为 11,位于最低级别的用户态。在 linux 中也仅有这两种级别。
流程大致如下图:

其中GDT和LDT的首地址,存放在用户不可见的起存起中。
这样子我们就拿到了我们要的段基地址,再加上我们的偏移量,得到了线性地址。

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

从线性地址到物理地址简单来说就是下面这张图:

虚拟地址分为两部分:虚拟页号(VPN)以及和虚拟页偏移量(VPO),其中虚拟页偏移量与物理也偏移量是相同的,也就是说我们只需要找到物理页号(PPN)就可以得到我们想要的物理地址。
那么物理页号存放在哪呢?它就存放在PTE页表中。我们通过VPN去页表中寻找,如果命中则取出物理页号,如果不命中,则替换该位置的PTE,再取出我们需要的物理页号。
当然实际过程没这么简单,这里面还涉及了TLB的小缓存,一种关于PTE的缓存,称为翻译后备缓冲器。一般会先去TLB中寻找物理页号,若不命中则去PTE中寻找。这点接下来详细介绍。

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

我们知道虚拟地址分为VPN和VOP,以intel Core i7为例,Core i7 支持48位虚拟地址和52位物理地址。以下图来解释:

48位的虚拟地址被划分为36位的VPN和12位的VPO。
而对于TLB来说,36位的VPN有可以看成是32位的TLBT(标记)和4位的TLBI(组索引)。若命中则取出其中的PPN。若不命中,则用36位的VPN去页表中寻找,页表分为4级页表,每一页的页表对应9位的VPN片。每个片被用作到一个页表的偏移量。而2,3,4级的页表基地址由前一级的页表存储,1级的页表基地址由CR3提供。如果在PTE中仍然不命中,则替换该页。取出PPN后和VPO合并由此得到了物理地址。

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

在我们得到物理地址后,就需要去访问物理内存,物理地址被分为3个部分,分别是CT(高速缓存标记),CI(高速缓存组索引),CO(高速缓存块偏移)。
CT用来判断我们需要的内存块是否在缓存中,CI用来定位高速缓存中的组标号,CO是缓存块中的偏移。

仍以该图为例,64组,所以CI=6,每一块64字节,所以CO=6,那么CT=52-6-6=40。如果在L1cache中命中,则取出结果,如果未命中则去L2,L3,和主存中寻找。

7.6 hello进程fork时的内存映射

Mm_struct(内存描述符):描述了一个进程的整个虚拟内存空间。
Vm_area_struct(区域结构描述符):描述了进程的虚拟内存的一个空间。
在fork创建虚拟内存的时候,要经历一下步骤:

  1. 创建当前进程的mm_struct,vm_area_struct和页表的原样副本
  2. 两个进程的每个页面都标记为只读页面。
  3. 两个进程的每个vm_area_struct都标记为私有,这样就只能在写入时复制

7.7 hello进程execve时的内存映射

  1. 删除已存在的用户区域
  2. 映射私有区域(.malloc,.data,.bss,.text)
  3. 映射共享区域(.libc.so.data, lib.so.text)
    4.设置程序计数器PC,指向代码区域的入口

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

缺页故障:

  1. 段错误,虚拟地址不合法
  2. 保护异常,内存访问不合法

缺页中断处理:MMU在试图翻译某个虚拟地址A时,触发了一个缺页。这个异常导致控制转移到内核的缺页处理程序,执行以下步骤:

  1. 判断虚拟地址A是否合法,如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止这个进程。
  2. 判断试图进行的内存访问是否合法,如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。
  3. 此时,内核知道了这个缺页是由于对合法的虚拟地址进行合法的操作造成的。于是,内核会选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这次MMU就能正常地翻译A,而不会引起缺页中断。

7.9动态存储分配管理

动态内存分配器为我们提供额外的虚拟内存。
动态内存分配器维护者一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。

分配器有两种基本的风格,两种风格都要求应用显示地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
显示分配器,要求应用显示地释放任何已分配的块。例如,c标准库的malloc程序包
隐式分配器,要求分配器检测一个已分配块合适不再被程序所使用,那么久释放这个块。隐式分配器也叫作垃圾收集器。

  1. 隐式空闲链表

A. 找到一个空闲块,有以下适配方法:
首次适配 (First fit):从头开始搜索空闲链表,选择第一个合适的空闲块。此时搜索时间与总块数是线性关系,且倾向在靠近链表起始处留下小空闲块的“碎片”,增加对较大块的搜索时间
下一次适配 (Next fit):和首次适配相似,是从链表中上一次查询结束的地方开始,这种适配比首次适应更快,可以避免重复扫描那些无用块。
最佳适配 (Best fit):查询链表,检查每一个空闲块,选择适合所需请求大小的最小空闲块,保证碎片最小,提高内存利用率,运行速度通常会慢于首次适配。
B. 分割 (splitting):申请空间比空闲块小,可以把空闲块分割成两部分。

C. 释放并分配:清除已分配标志,合并相邻的空闲块,和下一个空闲块合并或者双向合并。

  1. 显示空闲链表

显式空闲链表采用的方式是维护空闲块链表,而不是所有块。在空闲块中储存前/后指针,而不仅仅是大小,此外还需要边界标记,用于块合并。幸运的是,只需跟踪空闲块,因此可以使用有效载荷区域。
A.维护显式空闲链表方法:
LIFO(last-in-first-out)策略:后进先出法。将新释放的块放置在链表的开始处。此方法优点是简单,常数时间,缺点是研究表明碎片比地址顺序法更糟糕。
地址顺序法(Address-ordered policy):按照地址顺序维护链表。addr(前一个块) < addr(当前回收块) < addr(下一个块)。此方法优点是研究表明碎片要少于LIFO,缺点是需要搜索。
3. 分离的空闲链表:
分离存储,是一种流行的减少分配时间的方法。一般思路是将所有可能的块大小分成一些等价类/大小类。
分配器维护着一个空闲链表数组,每个大小类一个空闲链表,按照大小的升序排列。
维护方法:
A. 简单分离存储
每个大小类的空闲链表包含大小相等的块,每个块的大小就是这个大小类中最大元素的大小。
B. 分离适配
每个空闲链表是和一个大小类相关联的,并且被组织成某种类型的显示或隐式链表,每个链表包含潜在的大小不同的块,这些块的大小是大小类的成员。
(以下格式自行编排,编辑时删除)
4. 块按大小排序
在每个空闲块中使用一个带指针的平衡树,并使用长度作为权值。

7.10本章小结

本章首先介绍了4种存储器的地址模式,然后是地址变换的方法,例如:逻辑地址到线性地址的变换,再从线性地址到物理地址的变换,这对应了如何将虚拟内存的数据映射到了物理内存中。其中线性地址到物理地址的变换设计了TLB缓存和4级页表,这两者的存在是为了提高转换的效率以及降低内存空间的使用。得到物理地址后,我们就需要根据物理地址去内存中寻找我们需要的内容。为了提高访问速率,三级Cache由此出现。接下来介绍了fork和execve的内存映射以及遇到缺页时,内核该怎么处理。最后就是内存的动态管理,这其中涉及了显示分配器和隐式分配器两种方式。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件
设备管理:unix io接口
一个Linux文件就是一个m个字节的序列B0,B1,……,Bm-1。所以的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这个设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得输入和输出都能以一种统一且一致的方式的来执行。

8.2 简述Unix IO接口及其函数

打开文件:进程是通过调用open函数来打开一个已存在或者创建一个新文件的
int open(char *filename, int flags, mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,flags参数也可以是一个或者更多位掩码的或,给写提供一些额外的指示。mode参数指定了新文件的访问权限位。
关闭文件:进程通过调用close函数关闭一个打开的文件。
int close(int fd);
关闭一个已关闭的描述符会出错。
读和写文件:应用程序是通过分别调用read和write函数来执行输入和输出的。
ssize_t read(int fd, void *buf, size_t n);
ssize_t write(int fd, const void *buf, size_t n);
read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF,否则返回值表示的是实际传送的字节数量。
write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。

8.3 printf的实现分析

(以下格式自行编排,编辑时删除)

我们知道printf函数的参数是不确定的,所以我们就需要确定具体的参数个数。
va_list的定义: ,说明它是一个字符指针。
(char *)(&fmt)+4表示的是第二个参数,也就是……中的第一个参数。因为&fmt表示的是fmt的地址,是char *类型的指针,其大小为4,所以我们加上4后表示的就是第二个参数的首地址。

vsprintf的作用是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。然后sys_call显示格式化了的字符串。
内核会通过字符显示子程序,根据传入的ASCII码到字模库读取字符对应的点阵,然后通过vram(显存)对字符串进行输出。显示芯片将按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),最终实现printf中字符串在屏幕上的输出。

8.4 getchar的实现分析

(以下格式自行编排,编辑时删除)

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章介绍了Unix I/O接口以及I/O函数,分析了printf和getchar的实现。
(第8章1分)
结论
Hello的一生:
1.程序员通过I/O设备,往计算机里敲入代码,编写出hello.c文件
2.hello.c在预处理器下经过预处理形成hello.i文件
3.hello.i在编译器下经过编译形成hello.s文件
4.hello.s在汇编器下经过汇编形成hello.o文件
5.hello.o在链接器下与其他.o文件链接形成hello可执行文件
6.shell为hello文件分配空间形成进程,到前台执行
7.shell通过fork()函数创建一个子进程,在子进程中通过execve函数加载hello城西,建立hello可执行文件到虚拟内存的映射。
8.在执行hello时,发生了缺页中断,触发了缺页中断处理程序。
9.内核将hello文件的虚拟内存映射为物理内存,将虚拟地址翻译为物理地址。再根据这个物理地址去Cache/主存内读取数据、指令。
10.printf执行,将格式化结果显示到了shell中
11.hello进程终止,向父程序shell发送SIGCHLD信号
12.将hello进程进行回收
感悟:一个文件的执行,不像我们看起来的那么简单,他需要涉及许多复杂且伟大的过程,许多创新性地概念。例如进程的提出
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
(结论0分,缺失 -1分,根据内容酌情加分)

附件

Hello 可执行文件
Hello.i 预处理后文件
Hello.s 编译后文件
Hello.o 汇编后文件
Hello_.s 反汇编文件

参考文献

为完成本次大作业你翻阅的书籍与网站等
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[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分)

CSAPP大作业论文 程序人生相关推荐

  1. csapp大作业:程序人生

    程序人生 摘 要 文章从编译系统.进程管理.存储.I/O等角度对hello程序的生命周期展开探索,展示了最简单的程序p2p,o2o的过程.通过该程序的分析,进一步理解计算机系统的工作原理,将所学内容融 ...

  2. 哈工大CSAPP大作业:程序人生-Hello’s P2P

    ​     计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学    号 120L021818 班    级 2003006 学       生 秦 ...

  3. 【计算机系统】ICS大作业论文-程序人生-Hello’s P2P

    计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算机科学与技术专业 学 号 200111026 班 级 20级计算机9班 学 生 陈弘毅 指 导 教 师 郑贵滨 计算机科学与技 ...

  4. 哈尔滨工业大学计算机系统大作业论文-程序人生-Hello‘s P2P

    文章目录 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的概念与作用 2.2在Ubuntu下预处理的命令 2.3 Hell ...

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

    题     目 程序人生-Hello's P2P 专       业 计算学部 学   号 120L022028 班   级 2003007 学       生 杨建中 指 导 教 师 吴锐 计算机科 ...

  6. HIT CS:APP 计算机系统大作业 《程序人生-Hello’s P2P》

    HIT CS:APP 计算机系统大作业 程序人生-Hello's P2P Hello的自白 我是Hello,我是每一个程序猿¤的初恋(羞羞--) l却在短短几分钟后惨遭每个菜鸟的无情抛弃(呜呜--), ...

  7. HIT CSAPP大作业论文

    计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算机类 学 号 1190201816 班 级 1903012 学 生 樊红雨 指 导 教 师 史先俊 计算机科学与技术学院 20 ...

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

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

  9. [计算机系统]大作业-hello程序人生

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学    号 1190300505 班    级 1903011 学       生 张晟哲 指 导 ...

最新文章

  1. 【Java】面试官灵魂拷问:if语句执行完else语句真的不会再执行吗?
  2. 超分辨率:将背景和人脸分离 ,人脸、背景分别做增分后将人脸贴回背景图
  3. 为 VUE 项目添加 PWA 解决发布后刷新报错问题
  4. 如何将java.util.Date转换为java.sql.Date?
  5. boostrap 鼠标滚轮滑动图片_BootStrap 轮播插件(carousel)支持左右手势滑动的方法(三种)...
  6. Dubbo-go 源码笔记(一)Server 端开启服务过程
  7. 2014中国企业面对的五大挑战
  8. [译]写程序更快、更好、更便宜的艺术
  9. VB2010 的隐式续行(Implicit Line Continuation)
  10. 库克宣布苹果将捐款帮助山西
  11. Java文件类– java.io.File
  12. FTP协议的命令与返回码
  13. 1047: 对数表 ZZULIOJ
  14. 关系代数表达式的优化
  15. FPS游戏方框透视基本原理
  16. 魔百和CM201-1-YS易视腾代工-免拆机-线刷固件包
  17. 明确数据分析目标的 3 个步骤
  18. leetcode笔记(五)809. Expressive Words
  19. 按键精灵电脑版对接百度ai,告别字库(文字识别篇)
  20. java中缓存的原理

热门文章

  1. LAYUIADMIN.V1.2.1开发框架
  2. html左文右图布局方法,Divi主题如何实现博客列表拆分布局【图左文右】
  3. 京东购物车(动态)网页搭建,利用JavaScript实现
  4. 买到不喜欢的 iOS 软件不用怕!图解如何申请 App Store 退款
  5. 计算机设备停用代码22,Win7电脑提示由于该设备有问题Windows已将其停止(代码43)怎么办?...
  6. 通过运营技能地图使用Python整理用户指标及订单跟踪
  7. 再就业工程--写在南京摩托罗拉研发中心被关闭之时
  8. 龙芯 mysql_龙芯是什么架构
  9. NAT技术基础与配置(华为设备)
  10. 将WSL2作为生产力工具