秃顶路人的计算机系统大作业Hello

  • 摘要
  • 第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文件结构
      • 3.3.2 数据
      • 函数
      • 3.3.4 运算,赋值和转移操作
    • 3.4 本章小结
  • 第4章 汇编
    • 4.1 汇编的概念与作用
    • 4.2 在Ubuntu下汇编的命令
    • 4.3 可重定位目标elf格式
      • 4.3.1 ELF头
      • 4.3.2 节头
      • 4.3.3 重定位节
      • 4.3.4 符号表
    • 4.4 Hello.o的结果解析
    • 4.5 本章小结
  • 第5章 链接
    • 5.1 链接的概念与作用
    • 5.2 在Ubuntu下链接的命令
    • 5.3 可执行目标文件hello的格式
      • 5.3.1 ELF头
      • 5.3.2 节头
      • 5.3.3 符号表
      • 5.3.4 重定位节
    • 5.4 hello的虚拟地址空间
    • 5.5 链接的重定位过程分析
    • 5.6 hello的执行流程
    • 5.7 Hello的动态链接分析
      • 5.7.1 动态链接现象描述
      • 5.7.2 原因分析
    • 5.8 本章小结
  • 第6章 hello进程管理
    • 6.1 进程的概念与作用
    • 6.2 简述壳Shell-bash的作用与处理流程
    • 6.3 Hello的fork进程创建过程
    • 6.4 Hello的execve过程
    • 6.5 Hello的进程执行
      • 6.5.1 逻辑控制流与时间片
      • 6.5.2 上下文切换与进程调度
      • 6.5.3 用户模式与内核模式
      • 6.5.4 hello的调度过程
    • 6.6 hello的异常与信号处理
      • 6.6.1 异常与信号类型
      • 6.6.2 运行测试
    • 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.9.1动态内存分配
      • 7.9.2碎片与隐式空闲链表
    • 7.10本章小结
  • 第8章 hello的IO管理
    • 8.1 Linux的IO设备管理方法
    • 8.2 简述Unix IO接口及其函数
      • 8.2.1 Unix IO接口
      • 8.2.2 Unix IO函数
    • 8.3 printf的实现分析
    • 8.4 getchar的实现分析
    • 8.5本章小结
  • 结论
  • 附件
  • 参考文献

照作业要求将我墨迹四天多才写完的大作业上传到互联网上给大家借鉴
这门课学的真不好,可以说是边学边写的,如果有人想抄我的最好看看再抄

摘要

本文根据hello.c从编写到执行历程,由其中涉及到的每个节点逐一入手分析论述,从现象究其内因,由一系列中间文件推至到对存储系统、操作系统、机器语言、执行逻辑的原理阐述,将简单的hello程序作为万千计算机程序的缩影,从基本运行逻辑映射到计算机程序应用的宏观解析,作为在csapp结课之际,利用全部所学知识对计算机程序进行一个源于技术角度的反思复习,串联起各章节对不同层次系统的认识,形成一个对计算机系统的立体模型,这代表着知识体系的构建,也是初级程序员迈向成熟计算机学者的一个有意义的里程碑。

关键词: 计算机系统;csapp;程序周期;

第1章 概述

1.1 Hello简介

P2P(From Program to Process)
Hello程序的生命周期历经了从Program程序命令到Process程序进程的历程:由程序员编写的文本文件hello.c文件,经过cpp预处理器生成修改过源程序hello.i文件,再经过ccl编译器生成汇编文本hello.s文件,再经过as汇编器生成可重定位目标程序hello.o文件,最后经过ld链接器生成可执行目标程序hello。再由程序员通过shell输入字符串指令.\hello,shell会将目标文件的代码和数据从磁盘复制到主存,为这个程序fork一个子进程,再使用execve将其加载到进程中执行。

020(From Zero to Zero)
Hello程序经历了从不存在到诞生,到最后执行完再被抹去的历程:Shell通过execve执行这个程序,映射虚拟内存,进入程序入口后载入物理内存,进入main函数执行目标代码,CPU为其分配时间片和执行逻辑控制流。执行阶段把这个程序分解成几个阶段,分别执行对应指令,最后将输出从主存复制到寄存器文件,再复制到显示设备,运行终止后会被Shell的父进程回收,恢复上下文,将控制权交还给Shell。

1.2 环境与工具

硬件环境:Intel Core i7 X64 CPU; 2.60GHz; 16G RAM; 512GHD DISK
软件环境:Windows10 64位;VMware Workstation Pro;Ubuntu 20.04.4
开发与调试工具:egb,gcc,vim,as,ld,readelf

1.3 中间结果

 hello.c 源文件
 hello.i 预处理后的文件
 hello.s 编译后的汇编文件
 hello.o 汇编后的可重定位目标文件
 hello hello的可执行文件
 hello.elf hello文件的ELF格式文件
 hello.o.elf hello.o的ELF格式文件
 hello_obj.txt hello文件的反汇编文件
 hello.o_obj.txt hello.o的反汇编文件

1.4 本章小结

第一章概括性地从P2P和O2O角度简要叙述了hello文件从构建到执行的整个流程,同时还介绍了接下来内容讲解所必要的硬件与软件环境,以及会用到的各种中间文件。

第2章 预处理

2.1 预处理的概念与作用

概念:在程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码过程中,生成二进制代码文件之前的过程,系统引用预处理程序来解析相关字符“#”开头的相关预处理命令,修改完善原始的.c程序。
作用:处理条件编译(#endif),宏定义(#define),头文件包含(#include),行控制(#line)等预处理指令,使用相应的代码、指令和值对上述进行插入替换,同时过滤不必要代码病删除注释等内容,最后生成.i文件。

2.2 在Ubuntu下预处理的命令

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

2.3 Hello的预处理结果解析


可以明显发现文件内容被大篇幅扩充,在主函数前有三千多行,并且插入内容仍然是可读的C语言文本,可以发现是将头文件展开,添加了各种宏定义、结构体定义等内容,还插入了很多行符号(如# xxx)。

2.4 本章小结

第二章介绍了基本的预处理定义和作用,以及如何进行实操,并且通过对实际.i文件的阅读,探索预处理具体都对文件做了怎样的改动。

第3章 编译

3.1 编译的概念与作用

概念:通过对文件的语法分析、词法分析、语义检查、代码生成和代码优化一系列操作,编译器将预处理得到的.i文件转换成汇编文本hello.s,并且其间出现错误会给出相关提示信息。
作用:把处理过的源程序翻译成目标程序,是高级语言向机器代码转换的重关键步骤。

3.2 在Ubuntu下编译的命令

命令:gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析

3.3.1文件结构

 .file 声明源文件
 .text 声明代码段
 .data 声明数据段
 .align 声明对指令或数据存放地址对齐的方式
 .type 声明函数类型或数据类型
 .size 声明大小
 .section.rodata 只读数据段
 .globl 全局变量

3.3.2 数据

字符串
存放在.rodata部分,作为参数传给printf

两次引用

数组


参数列表*argv的首地址为-32(%rbp),调用其中第二个和第三个参数时,对地址进行了+8和+16的操作

局部变量

可以看出来在-4(%rbp)的位置存贮了一个局部变量,且初始值为0,在后续程序中,多次对-4(%rbp)这个变量进行运算和比较操作

立即数
直接在程序中使用$加数字体现的均为立即数,这里不截图举例了

函数

  • 在使用call调用某函数时,程序计数器%rip会设置为函数的起始地址,函数执行完毕后,程序计数器%rip指向调用函数后一条指令的地址。

  • 函数通常会有多个返回值并保存在相应的寄存器里(如一个返回值则%rax / %eax)。

  • 在调用函数时通过栈指针为函数分配局部空间,在执行完毕后会释放。

  • Mian函数【全局函数】
    传入参数int argc,char *argv[],分别存在寄存器%rdi和%rsi中

  • Exit函数
    传入参数 $1

  • Printf参数
    传入参数.LC1(%rip),即字符串“hello %s %s \n”

  • Atoi函数
    传入参数.LC1(%rip),即字符串“hello %s %s \n”

  • Puts函数
    传入参数.LC0(%rip)

  • Sleep函数
    传入参数为Atoi的返回值,处理过的argc[3]

  • Getchar函数
    没有传参

3.3.4 运算,赋值和转移操作

  • Addx与subx实现加减运算
  • Movx操作实现赋值
    (b一字节;w二字节;l四字节;q八字节)
  • Cmpx结合Jx实现条件控制转移操作
    通过比较来对应相关条件判断语句,如果满足跳转条件则跳转
    通过跳转与判断的多重结合使用来实现原函数的for循环语句:
    临时变量-4(%rbp)从初始值0开始,不断执行循环体语句每次加1,直到大于7时跳出循环

3.4 本章小结

第三章主要围绕着汇编器把经过预处理得到的hello.i翻译成机器语言的一系列汇编指令,并且将其打包处理从而生成hello.s文本文件的过程,介绍程序主要涉及到的文件结构、数据内容、运算操作、函数和条件转移对应的机器代码并且分析了相关的含义和用法规则。

第4章 汇编

4.1 汇编的概念与作用

概念:把汇编语言文件转换成二进制可重定位文件的过程。
作用:汇编器as将.s汇编程序指令编码打包成一个可重定位目标程序的.o格式文件,将汇编语言转换成机器语言,使其能在后续的的链接运行过程中能被机器识别。

4.2 在Ubuntu下汇编的命令

命令:gcc -c hello.s -o hello.o

4.3 可重定位目标elf格式

命令:readelf -a hello.o > hello.o.elf

4.3.1 ELF头

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

4.3.2 节头

节头表包含了各节的名称、大小、类型、地址、偏移量等相关信息,每个节地址都是从0开始用于后续处理的重定位,且有不同的读取权限和对齐大小,如.text段可执行不可写,.data段不可执行可写而.rodata不可执行且不可写。

4.3.3 重定位节

重定位节保存了.text节中需要被修正进行重定位的信息,在后续链接器处理该文件与其他文件时利用这些数据进行修改,并且可以观察到,主要的重定位条目都是文件中调用的的函数和.rodata中保存的数据。

4.3.4 符号表

符号表保存着程序中定义和引用的函数与全局变量的信息(局部变量保存在栈中不在该表中),包括每个符号的起始偏移值(可执行目标文件的绝对运行地址)、大小、类型、作用域、名称等信息。

4.4 Hello.o的结果解析

命令:objdump -d -r hello.o / objdump -s -d hello.o > hello.o_obj.txt



机器语言的构成:
在每条汇编语言的前部加入了对应的机器代码,语言与代码有一一对应的映射关系,这些二进制数据组成了机器指令的操作码和操作数,可以直接被机器识别,同时每行开头的相对地址也对应了后续链接的相关处理。
与汇编的区别:

  • 立即数进制
    .s文件中均采用十进制而.o采用十六进制
  • 操作指令
    基本操作指令表示被操作字的大小的尾缀没有了
    Movq > > Mov
  • 函数调用
    不再使用具体的函数名调用,而是使用相对地址的重定位条目指引,基址+偏移量
  • 跳转分支
    调转指令的操作数不再是段名称(.Lx),而是和函数调用一样的地址

4.5 本章小结

第四章介绍了汇编主要概念与作用,从生成的可重定位文件内容入手分析,通过ELF文件介绍并分析了相关数据及作用,通过比较反汇编结果与.s的汇编结果内容,详细分析了汇编语言与机器语言的对应关系及异同。

第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

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

命令:readelf -a hello > hello.elf

5.3.1 ELF头

具体内容与.o的ELF文件大体一致,只是文件类型由REL变为EXEC,
填充了程序头的大小和数量,并更新了节头部表的相关信息

5.3.2 节头

相对于.o的节头,对所包含的节信息有显著的增加,并且对节的分类也更详细,补充了之前都为0的各节地址偏移量作为程序被载入到虚拟地址的起始位置。

5.3.3 符号表

相较于.o更新了起始地址偏移值。

重定位算法:
Redaddr = ADDR(s) + r.offset
*refptr = (unsigned)(ADDR(r.symbol) + r.addend - refaddr)

5.3.4 重定位节

相较于.o此时hello的重定位节已经完成了重定位处理,其偏移量和信息值得到了更新,相关符号的信息也得到了补充。

5.4 hello的虚拟地址空间

通过用edb查看hello,可知其虚拟内存中ELF起始于 0x40000

对照5.3.2节头可知:

.text起始于地址0x4010f0,偏移量0x10f0,大小0x145

.fini起始于地址0x401238,偏移量0x1238,大小0xd

5.5 链接的重定位过程分析

命令:objdump -s -d hello > hello_obj.txt

差异分析:

  • 跳转与函数调用的地址
    相比于编译的.s反汇编文件中的跳转到.Lx和直接调用函数名,以及相比于可重定位的.o反汇编文件中的相对地址的调用与跳转,经过链接的hello文件反汇编中所有的地址指引都是经过重定位得到的确定的虚拟地址。

  • 更完整的内容
    可重定位.o的反汇编文件中,代码主体部分只有一个main函数,而经过链接的hello程序反汇编文件中,代码主体部分加入了所有被调用的函数的汇编代码。

  • 额外的节

    可以发现hello的反汇编文件比.o的反汇编文件显著增加了许多节。

新增节的名称 描述
.init 程序初始化需要执行的代码
.plt 过程连接表
.dynamic 存放被ld.so使用的动态链接信息
.data 初始化了的数据
.comment 一串包含编译器的NULL-terminated字符串
.interp 保存ld.so的路径
.note.ABI-tag Linux下特有部分
.hash 符号的哈希表
.gnu.hash GNU拓展的符号的哈希表
.dynsym 存放.dynsym节中的符号名称
.gnu.version 符号版本
.gun.version_r 符号引用版本
.rela.dyn 运行时/动态重定位条目
.rela.plt .plt节重定位条目
.fini 当程序正常终止时需要执行的代码
Hello的重定位过程
  • 连接器将所有相同类型的节合并为同一类型的新的聚合节,然后连接器将运行时内存地址赋给聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,这一步完成时,程序中每条指令和全局变量都有唯一的地址了。
  • 连接器修改代码节和数据节中对每个符号的引用,是他们指向正确的运行时地址。
  • 当编译器遇到对最终位置未知的目标引用时,它会生成一个重定位条目放在.rel.txt中。
  • 对绝对引用和相对引用分别有其重定位算法

5.6 hello的执行流程

使用edb的analyze,可以在命令行中观察到如下函数名称及其地址:

以及hello内部依次调用的函数:

5.7 Hello的动态链接分析

5.7.1 动态链接现象描述


在运行前通过edb的SymbolViewer搜索.got定位到地址0x40400


减去0x10则地址为0x403ff0,正好与反汇编文件中结果相对应


记录0x40400虚拟地址处的内容

定位到dl_init,设置断点,运行程序到dl_init下一步

回到刚刚hello.got指向的内存,可以发现其内容发生显著变化

5.7.2 原因分析

如果一个目标模块调用定义在共享库中的任何函数,那么他就有自己的全局变量表(GOT)和过程链接表(PLT),而其中GOT表是数据段的一部分,GOT[0]和GOT[1]包含着动态连接器在解析函数地址时会使用的信息,GOT[2]为动态连接器在模块ld-linux.so模块中的入口点,其余每个条目对应一个被调用的函数,因此在运行程序到断点之后,当共享模块被调用,推迟了的地址绑定开始解析,随之got更新其中地址内容,即得到了上述的现象。

5.8 本章小结

第五章从链接的概念与作用切入,先从可执行文件的ELF格式文件内容分析,再比照经过链接重定位处理的虚拟内存,从而描述静态库的原理;再从反汇编文件的角度对比印证链接的具体操作内容并详细解析了重定位操作的过程;最后执行过程角度,先通过程序执行流程与调用各库的函数展现链接作用,再通过在调用函数前后设置断点,用edb观察全局变量表内容的变化,来具体透彻地感知动态链接共享库。

第6章 hello进程管理

6.1 进程的概念与作用

概念:经典定义为一个执行中程序的抽象,广义上除了程序本身还包括相关的地址空间、文本区域、数据区域、堆栈等内容。
作用:使得当在现代系统上运行一个程序时,形成一个程序独占处理器,一个私有空间独占内存,以及指令与程序为依次连续执行的,便于人理解操作系统的假象。

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

作用:实际上Shell是一个命令解释器,它解释由用户输入的命令并且把它们送到内核。它还有自己的编程语言用于对命令的编辑,它允许用户编写由shell命令组成的程序。

处理流程:Shell首先检查命令是否是内部命令,若不是则检查是否是一个应用程序,然后在搜索路径中寻找这些应用程序。如果键入的不是内部命令也不没有在路径中找到相应可执行文件,则显示错误信息,如果能成功找到命令,该内部命令或应用程序将被分解为系统调用传给Linux内核。

6.3 Hello的fork进程创建过程

当用户通过命令行输入./hello时候,shell会判断指令不是内置命令,于是会认定其为可执行程序,继而为其fork一个子进程,子进程会得到与父进程虚拟地址空间相同但独立的一份副本,包括代码和数据段、堆、共享库以及用户栈,还获得与父进程打开文件描述符相同的副本,但是内核会给子进程分配一个唯一的、与父进程不同的PID。

6.4 Hello的execve过程

当shell创建了hello的子进程之后,会调用execve函数,获取参数列表中的filename,将命令行作为新的argv,并传入环境变量envp,只有当出现错误时execve才会返回到调用程序,否则每次调用后将不返回。
Execve过程中给更多有关虚拟内存和映射的概念解释会在下文7.7中说明。

6.5 Hello的进程执行

6.5.1 逻辑控制流与时间片

进程的运行本质是CPU不断从程序计数器PC指示的地址处取出值并执行,这些值唯一地对应包含于程序的可执行文件中的指令,或是包含在运动时动态链接到程序的共享对象的指令,这个值的PC序列即逻辑控制流,而在同一时间段内,逻辑流的执行是交错的,每个进程轮流适用处理器,执行他的逻辑流的一部分,然后被强占(暂时挂起),再轮到其他进程,一个进程执行其控制流的一部分的每一段时间叫时间片,每个进程的执行通常由多个时间片构成。

6.5.2 上下文切换与进程调度

内核为每个进程维持一个上下文,而Hello并非操作系统中在某一时刻运行的唯一进程,为了优化CPU利用率,当一个进程等待或发生阻塞时,内核不会在这个时间内等待什么都不做,取而代之地会休眠当前进程,将CPU交给另一个进程,当另一个进程运行到收到相应信号时,内核会判定该进程以及运行足够时间,于是执行上下文切换,将CPU交换给之前的进程,以此类推。

6.5.3 用户模式与内核模式

通过设置某个控制寄存器中的一个模式位来区分进程是运行在用户模式还是运行在内核模式中,用户模式的进程不允许执行特权指令(如停职处理器、改变模式位,发起I/O操作、直接引用地址空间中内核区的代码和数据),必须通过系统调用接口间接地访问内核代码和数据。可以通过中断、故障等异常切换到内核模式,这个概念为操作系统内核提供了一个无懈可击的进程抽象。

6.5.4 hello的调度过程

起初Hello程序运行于用户模式,当其调用sleep函数后进入内核模式,内核处理休眠请求而主动释放当前进程,并且将hello的进程从运行队列转移到挂起等待的序列,定时器开始计时,此时上下文切换将控制器交给其他进程,当计时器计时结束会发送中断信号,内核接收到信号会执行中断处理,再将hello重新调入运行队列,从而hello继续执行自己的逻辑控制流。

6.6 hello的异常与信号处理

6.6.1 异常与信号类型

  • 中断 来自I/O设备的信号 异步 总是返回下一条指令

  • 陷阱 有意的异常 同步 总是返回下一条指令

  • 故障 潜在可恢复的错误 同步 可能返回当前指令

  • 终止 不可恢复的错误 同步 不会返回

  • SIGNINT/SIGTSTP:
    当hello执行过程中用户输入Ctrl+C / Ctrl+Z时,操作系 统会给所有前台进程发送一个SIGINT / SIGTSTP信号,当shell捕获到这个信号后,会检查前台的进程(组)并通过kill函数给前台进程组中的每个进程(这里是hello)都发送相应信号,此时hello没有设置对以上信号的处理程序,所以根据默认操作会被停止/挂起,继而内核会给父进程(shell)发送一个SIGCHLD信号。

  • SIGCHLD:
    每当hello有子进程挂起/终止时,内核会给父进程shell发送一个SIGCHLD信号,shell捕获到信号会调用SIGCHLD处理程序回收子进程。通过tinyshell的模拟实现可以窥伺到其内部特征:父进程会获取到子进程的退出状态,对于不同的状态会给出不同的处理方式,若子进程为停止则只修改状态,若为终止则会被回收。

6.6.2 运行测试

  • 运行程序
    【图片有学号姓名会暴露余则成的身份所以我李涯不传了】

  • Ctrl+Z
    【图片有学号姓名会暴露余则成的身份所以我李涯不传了】

  • 用pstree和ps查看当前挂起进程
    【图片有学号姓名会暴露余则成的身份所以我李涯不传了】

  • 用fg %1 恢复前台进程hello并用jobs查看进程
    【图片有学号姓名会暴露余则成的身份所以我李涯不传了】

  • Enter / 乱按
    【图片有学号姓名会暴露余则成的身份所以我李涯不传了】
    【图片有学号姓名会暴露余则成的身份所以我李涯不传了】

    Shell没有给出响应,忽略了非法输入

  • Ctrl-C 并用pstree查看
    【图片有学号姓名会暴露余则成的身份所以我李涯不传了】

    可以看到pstree中bash下的hello进程已经被回收清除了

6.7本章小结

第六章从与程序运行的有关的一系列重要复杂概念入手,从多个理论维度讲述进程层面上的hello运行原理,再辅之以实际操作,得到相应现象与结果用于证实其机制,使得可以更加深刻透彻、且整合地了解在操作系统内部每个程序完整的开始运行以及终止。

第7章 hello的存储管理

7.1 hello的存储器地址空间

  • 逻辑地址:相对于应用程序而言的概念,程序经过编译后出现在汇编代码中的地址,逻辑地址用指定一个操作数或一条指令的地址,只有一个段标识符加一个指定段内相对地址的偏移量,形式为 [段选择符:段内偏移量]。
  • 线性地址:是逻辑地址和物理地址转换之间的中间层,由段地址中的偏移地址加上全局描述表GDT中存储的真正的相应段的基址构成。
  • 虚拟地址:相对于真实的物理地址而言的概念,实际上与线性地址含义相同,用于直接访问主存以及被转换为物理地址访问内存,现代处理器使用这种方式寻址。
  • 物理地址:计算机系统主存被组织成一个由M个连续字节大小的单元组成的数组,每个字节都有一个唯一的物理地址,是CPU访问内存的最自然的方式,早期计算机使用这种方式寻址。

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

一个逻辑地址由段标识符和段内偏移量两部分组成,段标识符由一个16位字段组成,前13位为索引号,后3位包含一些硬件细节,表示具体是代码段寄存器、栈段寄存器还是数据段寄存器,在Intel平台下,用段标识符去全局描述符表GDT中的得到段基址再加上段偏移量从而获得线性地址的过程就是段式管理。

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

线性地址(即虚拟地址)到物理地址的转换通过分页机制完成,VM系统通过将虚拟内存分割为大小相同的块(即虚拟页)来实现页式管理,具体地,内存管理单元MMU用虚拟地址前部分的虚拟页号VPN访问页表查询PTE,得到物理页号PPN,再结合页偏移量PPO得到物理地址,从而访问到高速缓存/内存。

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

  • TLB(translaion lookaside buffer 快表)
    TLB是一个小的、虚拟寻址的缓存,其中的每一行都保存着一个由单个PTE组成的块,用于消除每次CPU向MMU查询PTE翻译地址可能发生的不命中而产生的巨大时间开销,相当于给页表添加了一个更小更快的高速缓存,充分利用好局部性原理,MMU可以直接从TLB中查询PTE,如果命中就会直接得到物理地址,如果不命中再去高速缓存/内存中查询。
  • 多级页表
    为了减少驻留在内存中的页表大小,使用具有层次结构的多级页表来实现空间压缩,这种方法从两个角度减小了内存要求,第一,如果一级页表的一个PTE全是空的,那么相应的二级页表根本就不存在,这相当于一种巨大的潜在节约,因为通常情况下虚拟空间的大部分都会是未分配的;第二,只有一级页表才需要总是在主存中,VM可以在需要时创建、调入页面或调出二级页表,这就减少了主存的压力,只有常用的二级页表才会缓存在主存中。
  • VA-PA变换过程
    虚拟地址由虚拟页号VPN和虚拟偏移VPO组成,MMU从线性地址中获得VPN,首先检查TLB,如果之前存在内存引用在TLB中缓存了PTE,则直接命中,可以根据TLBT和TLBI把PPN返回给MMU;如果不命中,则MMU需要访问主存,从页表中的PTE获得PPN,如果发生缺页,则继续访问磁盘并调入新的页面,最后得到PPN,结合VPO获得物理地址。
    将多级页表的引入后,前半部分变换过程不变,在没有命中TLB而访问主存中页表的部分,步骤改为逐级根据页表起始地址和偏移量查询PTE最后得到PPN。

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

如果寄存器和主存之间没有其他缓存设备,会使得每次MMU进行物理寻址都会有着巨大的时间开销,因此引入多级高速缓存来优化访存性能:MMU先根据索引位和标记位在L1cache中寻找,若不存在再逐级访问L2cache和L3cache,同时会将命中的数据依次再返回给上一级cache进行缓存,最终将命中的数据返回给CPU,多级缓存充分地发挥了程序设计的空间局部性和时间局部性优势,有效提升了访存性能。

7.6 hello进程fork时的内存映射

在shell中输入命令后,程序内核调用fork创建子进程,为hello程序的执行创建各种数据结构,并分配其一个唯一的、不同于PID,同时为这个子进程创建虚拟内存(包括当前进程的mm_struct副本、区域结构和页表的原样副本),它将两个进程的中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记位私有写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此也就为每个进程保持了私有地址空间的抽象概念,也保证了在执行过程中,多数的只读操作无需创建物理内存的副本,不会引起额外的内存开销,也节省了创建的时间开销。

7.7 hello进程execve时的内存映射

Shell在fork子进程后立即调用execve加载运行hello程序,包括以下步骤:

  1. 删除已经存在的用户区域,主要包括当前进程虚拟地址的用户部分已经存在的区域与结构。
  2. 映射私有区域,为新程序的代码数据、bss和栈区域创建新的区域结构,且均为私有的、写时复制的。其中代码和数据区域被映射为hello中的.text和.data段,bss区域是请求二进制零的,映射到匿名文件,其长度包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
  3. 映射共享区域:如果hello程序与共享对象/目标链接(如libc.so),那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
  4. 设置程序计数器PC,execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使其指向代码区域的入口点。

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

DRAM缓存不命中称为缺页,当CPU引用一个虚拟地址而该虚拟地址并未缓存在DRAM中(通常通过页表中的有效位判断),就会引发缺页故障,这时会调用内核中的缺页异常处理程序,选择一个牺牲页,用请求的页替换掉牺牲页,并更新页表PTE,异常处理程序返回,重启导致缺页的指令,再次将该虚拟地址发送到地址翻译硬件,页命中继而正常处理。

7.9动态存储分配管理

7.9.1动态内存分配

动态内存分配器维护着一个进程的虚拟内存区域称为堆,系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高地址)。对于每个进程,内核维护着一个变量brk指向堆的顶部。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显示地保留为供应用程序使用,空闲块可用来分配。空闲块保持空闲,直到它显示地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放或是应用程序显示执行,或是内存分配器自身隐式执行。
分配器有两种基本风格,都要求显示分配块,区别在于显示分配器EA要求显示调用与显示释放,而隐式分配器IA(亦垃圾收集器GC)要求检测不再被程序使用的分配块而进行自动释放。

7.9.2碎片与隐式空闲链表

为了解决对齐等原因导致的内部空间碎片和不连续导致的外部空间碎片,提升利用空闲块的时间效率和空间效率,隐式空闲链表结构被应用,这样空闲块通过头部中的大小字段隐含连接着,可以只通过头部保存的信息确定分配情况以及寻找下一个块的起始位置。
在放置已分配块搜索空闲链表时,分配器提供多做用于搜索的放置策略,包括但不限于首次适配,下一次适配和最佳适配,应用在不同情况下各有优势。
为了处理由存在多个不可用的小空闲块导致的假碎片现象,可以使用合并空闲块策略,包括立即合并和推迟合并两种策略,同时为了实现低时间复杂度的合并操作还需要牺牲一定内存开销为每个块的结尾附加脚部边界标记,并且前边的已分配的/空闲块合并为当前块新增的低位时,前部块就不再需要脚部,这样可以一定程度上节约空间开销(空闲块仍然需要脚部)。

7.10本章小结

第七章逐级从概念深入,叙述流程辅之以原理分析,具体透彻地叙述存储管理中最重要、最抽象的部分之一——虚拟内存,这部分论述没有涉及到实际操作案例也是因为其内容相对比较深奥复杂,只能从常见用处与浅层逻辑概念入手进行一个笼统的认识,但无论是虚拟内存的理念,还是其对于程序和内存还有安全方面的功效增益,都是十分有意义且值得深入研究的。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

Linux把所有IO设备都模型化为字节序列,即文件。所有的输入和输出都被当作对相应文件的读和写来执行。Linux提供了Unix IO接口函数,使得所有的输入和输出以统一的方式进行。
设备的模型化:文件
设备管理:unix io接口

8.2 简述Unix IO接口及其函数

8.2.1 Unix IO接口

  • 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
  • Linux Shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。
  • 改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
  • 读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
  • 关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件,作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放他们的内存资源。

8.2.2 Unix IO函数

  • Open函数和close函数

int open(char *filename, int flags, mode_t mode);
int close(int fd);

open函数将filename转换为一个文件描述符(用数字表示)。返回的数字总是进程中没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
进程通过调用close函数关闭一个打开的文件,参数为open返回的文件描述符。

  • Read函数和write函数
    应用程序通过调用read和write函数来执行输入输出。

ssize_t read(int fd, void *buf, size_t n);
ssize_t write(int fd, void *buf, size_t n);

read函数从描述符为fd的文件复制最多n个字节到内存位置buf。函数返回-1说明遇到错误,返回0表示EOF。否则,返回读取的字节数量。
write函数从内存位置buf处至多复制n个字节到文件描述符为fd的文件中,若函数返回-1说明遇到错误,否则返回实际写入的字符数量。

8.3 printf的实现分析

【该节内容基于对所列提示网址的内容转述】

  • printf主体代码
  • Vsprintf函数的作用就是格式化。它接受确定输出格式的格式字符串fmt,并将相应的格式结果存到buf,而i 通过函数调用获得的是一个长度,即要打印出来的字符串的长度。
  • 在Linux下,write通过执行syscall系统调用实现了对系统服务的调用,从而使内核执行打印操作。内核会通过字符显示子程序,根据传入的ASCII码到字模库读取字符对应的点阵,然后通过vram(显存)对字符串进行输出。
  • 显示芯片将按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),最终实现printf中字符串在屏幕上的输出。

【原提示】
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

当程序调用getchar(),程序等待用户输入。用户输入的每个字符实际上是一个中断,其触发事件为键盘按下,行为是将按下的对应字符保存到输入缓冲区。当按下的字符为回车时,中断处理程序将结束getchar并返回读入的第一个字符。

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

8.5本章小结

第八章笼统概括地了解了一下常见基本IO相关内容,由于课内在这部分没有进行过多讲解,所以主要是基于先前对常用IO函数的了解和根据给定的提示网址和提示信息进行分析叙述。文件系统是操作系统与用户交互的最后一步,具有较大的时间效应,相对于底层概念更好理解、更易于实践。

结论

Hello的一生从用户编辑.c文件的代码开始,经过编译器的预处理,解析文件中的预处理指令,用相关代码进行替换与添加,形成了.i文本文件;继而由编译器处理,将高级语言翻译成汇编语言,生成.s汇编文件,初步走入硬件层面;再经过汇编器将汇编语言转换为机器语言,将.s程序编码打包成.o可重定位目标文件;再经由链接器处理将hello与其他共享库文件等调用与执行需要的文件收集结合成单一文件,并进行重定位确定绝对地址,继而生成可执行文件hello;此时用户可以通过命令行指令./hello执行程序,shell接收到指令后为调用fork函数为hello创建子进程、构建上下文、创建虚拟内存,再立刻调用execve函数执行hello程序,hello程序运行过程中有关访存的动作,会通过将虚拟地址经过一系列步骤转换成物理地址,再进行相关读和写的操作,最后经IO交互,读入用户在程序进行时的输入,并将程序结果通过显示器返回,最终执行完毕发生SIGCHLD信号然后被父进程回收。
Csapp是一门具有极高学习价值的课,从计算机各个方面由浅入深的解析相关原理并论述知识内容,会让学者从交互与计算的表面,深入认识到机器逻辑与存储系统,由其是进程与虚拟内存有关的抽象概念,违反用户人之常事略微晦涩,而又十分巧妙具有很大探究价值,由此程序员的感知不只是基于现成逻辑的性能变现,还有深入底层逻辑的推动性能进步发展,这个看似计算机行业过饱和的时代更多人都只是在冰川表面运作,而水下具有无尽潜能的未探索领域也预示着会有很大的创新余地以及,这个行业高级从业人员的饱和还差的很远很远。

附件

 hello.c 源文件
 hello.i 预处理后的文件
 hello.s 编译后的汇编文件
 hello.o 汇编后的可重定位目标文件
 hello hello的可执行文件
 hello.elf hello文件的ELF格式文件
 hello.o.elf hello.o的ELF格式文件
 hello_obj.txt hello文件的反汇编文件
 hello.o_obj.txt hello.o的反汇编文件

参考文献

[1] Computer Systems- A Programmer’s Perspective, 3rd ed. - Randal E. Bryant, David R. O’Hallaron
2https://www.cnblogs.com/pianist/p/3315801.html
[3] https://zhuanlan.zhihu.com/p/391714998
[4] https://blog.csdn.net/Haomione/article/details/122731447
[5] https://blog.csdn.net/yueyansheng2/article/details/78860040
[6] https://blog.csdn.net/drshenlei/article/details/4261909

秃顶路人的计算机系统大作业Hello相关推荐

  1. HIT计算机系统大作业

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

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

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

  3. 程序人生-哈工大计算机系统大作业2022春

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学    号 7203610401 班    级 2036012 学       生 王元辰 指 导 ...

  4. 哈尔滨工业大学计算机系统大作业--程序人生

    计算机系统   大作业 题     目  程序人生-Hello's P2P      专       业   计算机科学与技术        学    号        2021110xxx      ...

  5. HIT深入理解计算机系统大作业

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机 学    号 120L021909 班    级 2003006 学       生 邢俊文 指 导 ...

  6. 哈工大计算机系统大作业——hello P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学   号 班   级 学       生 指 导 教 师 刘宏伟 计算机科学与技术学院 2021年 ...

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

    计算机系统 大作业 题     目 程序人生-Hello's P2P 计算机科学与技术学院 2021年6月 摘  要 本文介绍了hello的整个生命过程.利用gcc,gdb,edb,readelf,H ...

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

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学    号 120L020512 班    级 2003004 学       生 黄鹏程 指 导 ...

  9. 2022哈工大计算机系统大作业——程序人生

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 人工智能(未来技术) 学   号 120L020301 班   级 2036011 学       生 张思远 ...

最新文章

  1. php 控制网站跳转,迅睿CMS 重写控制网站自动跳转函数
  2. 车间AP无法接入故障分析处理
  3. Excel随机排考号方法
  4. POJ 3259 Wormholes
  5. 电脑公司win11 64位旗舰版镜像文件v2021.07
  6. 排序算法之--桶排序(桶,像桶一样的排序,听起来很有趣哦0。0)
  7. Golang笔记——反射
  8. Java比较两个对象属性值变化情况,用于记录日志使用
  9. golang test测试使用
  10. java docx转pdf_在java中将docx转换为pdf
  11. 在php中cm和kg用什么表示,将g/cm³换算为kg/m³ (克每立方厘米换算为千克每立方米)...
  12. SlicePlane的Heading角度与Math.atan2(y,x)的对应转换关系
  13. math_泰勒多项式/Taylor级数常用麦克劳林级数
  14. UOS系统启动盘制作
  15. ML之FE:特征工程处理中常用的数据变换(log取对数变换等)之详细攻略
  16. 加拿大前十大学计算机硕士学费,加拿大各大学硕士学费的情况介绍
  17. mongodb的write concern
  18. java正则表达式 分词_正则表达式实现(一)
  19. effector - 必应词典
  20. 选购哪种护眼灯对眼睛好,让我们一起来看看吧

热门文章

  1. 新宜中国携手fone,踏上预算管理新征程
  2. 关于安装不上tesseract和opencv以及稀里糊涂的解决方案
  3. Mac下如何将Safari书签导入Chrome
  4. 【English】元音辅音
  5. 元组与字典——python
  6. iOS Resume【简历模板】
  7. 【安卓学习之常见问题】百度地图卫星地图精度不够
  8. Js高级-函数的理解
  9. js-array数组-slice-splice
  10. python excel库pip install_超全整理|Python 操作 Excel 库 xlwings 常用操作详解!