文章目录

  • 第1章 概述
    • 1.1 Hello简介
      • P2P:From Program to Process
      • 020:From Zero-0 to Zero-0
    • 1.2 环境与工具
    • 1.3 中间结果
    • 1.4 本章小结
  • 第2章 预处理
    • 2.1 预处理的概念与作用
      • 2.1.1 概念
      • 2.1.2 作用
    • 2.2 在Ubuntu下预处理的命令
    • 2.3 Hello的预处理结果解析
    • 2.4 本章小结
  • 第3章 编译
    • 3.1 编译的概念与作用
      • 3.1.1 概念
      • 3.1.2 作用
    • 3.2 在Ubuntu下编译的命令
    • 3.3 Hello的编译结果解析
      • 3.3.1数据
        • 3.3.1.1常量
        • 3.3.1.2变量
      • 3.3.2赋值
      • 3.3.3类型转换
      • 3.3.4算术操作
      • 3.3.5关系操作
      • 3.3.6数组
      • 3.3.7控制转移
      • 3.3.8函数操作
    • 3.4 本章小结
  • 第4章 汇编
    • 4.1 汇编的概念与作用
      • 4.1.1 概念
      • 4.1.2 作用
    • 4.2 在Ubuntu下汇编的命令
    • 4.3 可重定位目标elf格式
    • 4.4 Hello.o的结果解析
    • 4.5 本章小结
  • 第5章 链接
    • 5.1 链接的概念与作用
      • 5.1.1 概念
      • 5.1.2 作用
    • 5.2 在Ubuntu下链接的命令
    • 5.3 可执行目标文件hello的格式
    • 5.4 hello的虚拟地址空间
    • 5.5 链接的重定位过程分析
    • 5.6 hello的执行流程
    • 5.7 Hello的动态链接分析
    • 5.8 本章小结
  • 第6章 hello进程管理
    • 6.1 进程的概念与作用
      • 6.1.1 概念
      • 6.1.2 作用
    • 6.2 简述壳Shell-bash的作用与处理流程
      • 6.2.1作用
      • 6.2.2 处理流程
    • 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.9.1 隐式空闲链表
      • 7.9.2 显式空闲链表
    • 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简介

P2P:From Program to Process

   高级语言(这里是C语言)编写的hello.c文件经过预处理器预处理得到hello.i文件,将带有#的代码以及其他宏进行替换,然后经过编译器编译得到hello.s汇编文件,将高级语言翻译为汇编语言。hello.s文件经过汇编器被汇编成机器语言,生成可重定位的文件hello.o,最后经过链接器将hello.o与库函数相链接即可生成可执行程序hello。执行该程序时,操作系统调用fork创建一个子进程,然后调用execve函数加载该程序的进程,使得hello得以运行。

020:From Zero-0 to Zero-0

   程序加载前,没有任何有关该程序的结构或空间,因此为Zero-0。程序加载时,操作系统为其分配虚拟内存空间,在物理内存与虚拟内存之间建立映射。程序执行时,根据映射关系将程序载入物理内存,操作系统为其分配时间片,并通过页表、cache等机制使得程序运行加速,运行结束后hello进程被回收,对应的虚拟空间以及相关数据结构被释放,最后又变成了Zero-0。

1.2 环境与工具

   软件环境:Windows 10 64位,Ubuntu 20.04 LTS 64位,VMware Workstation Pro 16
   硬件环境:Intel® Core™ i7-8565U CPU @ 1.80GHz 1.99GHz 8G RAM
   开发工具:Visual Studio 2019, gcc, gdb, VSCode

1.3 中间结果

   hello.c:用C语言编写的源代码
   hello.i:将hello.c预处理后生成的文件
   hello.s:hello.i被编译器编译后生成的汇编文件
   hello.o:hello.s被汇编器汇编后生成的可重定位目标文件
   hello:链接器链接可重定位文件后得到的可执行文件

1.4 本章小结

   本章介绍了P2P和020的过程,列出了实验的软硬件环境以及开发工具,并展示了中间结果及其作用。

第2章 预处理

2.1 预处理的概念与作用

2.1.1 概念

  预处理是指在进行编译的第一遍扫描之前所做的工作,在C语言中,预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。

2.1.2 作用

  可用来将多个源文件连接成一个源文件进行编译,便于程序的修改、阅读、移植和调试,便于实现模块化的程序设计。

  其主要功能如下:

  1. 宏定义:预处理工作将宏名替换为字符串或数值等,即进行宏替换;
  2. 文件包含:将包含文件的内容复制到包含语句(#include)处,得到新的文件;
  3. 条件编译:根据条件编译指令决定需要编译的代码。

2.2 在Ubuntu下预处理的命令

gcc hello.c -E -o hello.i

2.3 Hello的预处理结果解析

  打开hello.i文件,发现原来的文件已经被扩展为3065行。其中,注释被去掉,而#include包含的文件被引入,出现了大量引用文件的目录、结构体、typedef以及extern标识的外部文件的变量声明。具体来说,预处理器到默认路径下寻找include的文件,发现文件中仍有include、define和ifdef,于是继续将include包含的文件引入,将define的变量替换为实际值,并根据#define对ifdef内的内容进行有选择地判断是否引入,最终形成了不包含注释、define、include、ifdef的hello.i文件,下图为该文件的部分截图:

而main函数的代码没有被改变:

2.4 本章小结

  本章介绍了预处理的概念和作用,并通过在Ubuntu中对hello.c进行预处理观察了预处理的过程,并解析了预处理的结果。

第3章 编译

3.1 编译的概念与作用

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

3.1.1 概念

  编译是指编译器将文本文件.i(预处理后的文件)翻译成文本文件.s(汇编代码文件)的过程

3.1.2 作用

  编译器对.i文件进行词法分析,对由字符组成的单词进行处理,产生一个个单词符号;
  然后进行语法分析,以但此符号作为输入,分析单词符号串中是否形成符合语法规则的语法单位,生成中间代码;
  之后进行代码优化,对中间代码进行多种等价变换,生成更为有效的目标代码;
  最后生成目标文件,即.s汇编语言文件。

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析

3.3.1数据

3.3.1.1常量

  从下图所示的汇编代码中可以看出,printf中的两个字符串被存储在.rodata段

  下面四张图分别是源代码和对应的汇编代码,可以看出数字常量是存储在.text段作为立即数出现的:



3.3.1.2变量

  全局变量:sleepsecs作为初始化的全局变量,在编译时被存放在.data段中,如下图:

如下图,argc参数被存储在寄存器rdi中,而argv地址存储在了寄存器rsi中,

如下图,局部变量i存储在栈中,开始时初始化为0,每次循环都加1:

3.3.2赋值

  首先对全局变量sleepsecs进行赋值,在main函数之前即完成,将初值放在.data段内
  然后是对局部变量i进行赋值,如图xxx,用mov指令将其对应的栈所在的内容赋值为0。

3.3.3类型转换

  对sleepsecs进行了隐式的类型转换,如图,初始化值为2.5,但其类型为int,因此在将其存放入.data段时截断成了2:

3.3.4算术操作

  在for循环内部对i进行了加1操作,汇编指令用add指令实现这一操作,即对栈内相应存储位置所存数据加1,体现如下:

3.3.5关系操作

  如下图所示,第一个if使用!=判断argc是否与3相等,在汇编代码中,使用cmp+je指令进行该不等关系的判定,即cmp设置条件码,je读取条件码并进行相应的跳转:


  如下图所示,for循环内部使用<判断i与9的关系,同样地,在汇编代码中,使用cmp+jle指令进行该小于关系的判定,即cmp设置条件码,jle读取条件码并进行相应的跳转:

3.3.6数组

  在printf语句中,引用了数组argv[1], argv[2],如图:

  经过上面的分析,我们知道,argv地址被存放在rsi中,后来又被存放入了-32(%rbp)中,因此在调用printf函数前,如图,汇编指令先将argv[2]放入了%rdx,即argv+16所指向的值;又将argv+8得到的地址存入了%rsi中。总的来看,取数组中值的操作实际上就是数组首地址加所选元素的相对于首地址的偏移。

3.3.7控制转移

  if(argc!=3)-else的控制转移:
  在汇编指令中,if(argc!=3)-else由两条指令替换,即cmp判断参数与3的大小情况,并设置条件码,由je取出条件码,如果与3相等,则跳转到.L2,否则继续执行下一条指令:

  for(i=0;i<10;i++)的控制转移:
  与if类似,该语句的控制转移同样由两条汇编指令完成,利用cmp对参数与9进行比较,并设置条件码,由jle将条件码取出,如果参数小于等于9,则跳转到.L4继续进行循环体的执行,否则执行下一条指令:

3.3.8函数操作

  对printf和exit的调用:
  由下图可见,在调用printf前,将printf的参数,这里第一张图是字符串常量地址存入%rdi,第二张图是字符串常量地址和两个命令行参数地址依次存入%rdi、%rsi、%rdx作为参数,然后使用call指令调用printf函数,该函数自动到寄存器中取出参数并执行:


  同样地,调用exit前使用mov指令将1存入寄存器rdi作为参数,然后使用call指令调用exit函数:

  调用sleep函数时,同样使用mov指令将参数存入%rdi,然后使用call指令调用sleep:

  对getchar函数的调用则不同,它无需参数,因此直接调用:

  最后,main函数返回前,将返回值0存入默认的返回值寄存器rax,然后使用leave恢复调用者栈帧,清理被调用者栈帧,最后使用ret指令返回:

3.4 本章小结

  本章介绍了编译的概念和作用,并通过实验将hello.i编译为hello.s,对其编译结果进行了解析。

第4章 汇编

4.1 汇编的概念与作用

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

4.1.1 概念

汇编指的是汇编程序将用汇编语言书写的程序翻译成与之等价的机器语言指令,把这些指令打包成可重定位目标程序的格式即.o文件的过程。

4.1.2 作用

  1. 扫描源程序,根据符号的定义和使用,收集符号的有关信息到符号表中;
  2. 根据收集的符号信息,将源程序中的符号化指令逐条翻译为相应的机器指令。其中涉及到检查语法的正确性,如果正确,则将源程序翻译成等价机器语言程序,生成可重定位的目标程序.o;如果语法有错,则输出错误信息。

4.2 在Ubuntu下汇编的命令

gcc -c hello.s -o hello.o

4.3 可重定位目标elf格式

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

  ELF头:以16字节序列开始,描述了生成该文件系统的字的大小和字节顺序,除此之外,还包含了文件类型、文件版本、程序入口等等信息,如下图:

  节头部表:描述目标文件的节,包含了各节的名字、类型、地址、偏移量等信息,其描述了13个节,分别是:
.text, .rela.text, .data, .bss, .rodata, .comment, .note.GNU-stack, .note.gnu.propert, .eh_frame, .rela.en_frame, .symtab, .strtab, .shstrtab,如下图:

  重定位节:描述了各个段引用的外部符号等,在链接时,需要通过重定位节对其地址进行修改。如下图,在本文件中,需要重定位的符号有puts, exit, prinft, sleepsecs, sleep, getchar和.rodata中的某些数据:

  符号表:存放在程序中定义和引用的函数和全局变量的信息,包括value、size、type、bind、vis、ndx、name等信息,如下图:

4.4 Hello.o的结果解析

  objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
  hello.s与hello.o的反汇编有几点不同之处:
  首先,立即数使用的进制不同,hello.s使用十进制,而hello.o的反汇编使用十六进制,与机器语言一致:

  其次,两份文件的组织形式不同,hello.s文件全部都是汇编代码,开头还包含常量、变量的描述,而hello.o的反汇编代码则在汇编代码前还有相对应的机器码的十六进制表示,且没有了对于变量和常量的描述:


  分支转移、调用标识符不同:hello.s在跳转或调用时,call,je等直接跟的是段名或函数名,如.L2,puts等,而hello.o的反汇编代码则使用的是十六进制相对地址:


  而在总体上看,机器语言全部是由二进制码构成的,与汇编语言是一一对应的关系,即一条汇编语言对应一串机器码指令。

4.5 本章小结

  本章介绍了汇编的概念与作用,并对汇编产生的可重定位目标elf格式尽心饿了解析,介绍了各节的信息,并对反汇编文件与上一节生成的.s汇编文件进行了对比分析。

第5章 链接

5.1 链接的概念与作用

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

5.1.1 概念

  链接器将可重定位目标程序外加库链接为一个可执行文件,即.o文件中的未定义的变量、函数等与定义它们的.o文件进行合并。

5.1.2 作用

  1. 合并符号表,进行符号解析,将代码和数据模块象征性地放入内存;
  2. 符号地址重定位,决定数据和指令标签的地址;
  3. 修补内部和外部引用;
  4. 生成可执行文件。

5.2 在Ubuntu下链接的命令

  使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
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的格式

  分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
  ELF头:以16字节序列开始,描述了生成该文件系统的字的大小和字节顺序,除此之外,还包含了文件类型、文件版本、程序入口等等信息,如下图:


  节头部表:描述目标文件的节,包含了各节的名字、类型、地址、偏移量等信息,其描述了26个节,较hello.o多出了13个节:


  程序头表:列出了12个段,提供了各段在虚拟地址空间和物理地址空间的位置、大小、标志、访问授权和对齐方面的信息


  节段映射:

  动态节:

  重定位节:描述了各个段引用的外部符号等的偏移、类型、名字等信息

  符号表:存放在程序中定义和引用的函数和全局变量的信息

  版本信息:

5.4 hello的虚拟地址空间

  使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
  由5.3可知.interp段位于0x4002e0,大小为0x1c,于是到相应地址找,如图:

  由5.3可知.rodata段位于0x402000,大小为0x2f,于是到相应地址找,法相两行printf中的字符串存于其中,如图:


  其余段都可以依此找到。

5.5 链接的重定位过程分析

  objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
  通过观察hello和hello.o反汇编代码的不同,我们可以看出以下几点:

  1. hello的反汇编代码中增加了新的函数和节,hello.o中原本只有.text段和main函数,而在hello的反汇编代码中又新增了.init、.plt、.fini等段,且增加了init、exit、printf等系统函数;

  2. 原先hello.o中等待重定位的调用在hello中得到了计算并填充;

  1. hello.o反汇编中从0开始的相对地址在hello中变成了从0x40100虚拟地址;


      从上述对比我们可以看出,链接实际上是链接器将可重定向目标文件组合在一起,其中的函数、段按照一定的顺序排列,进行符号解析,并为待重定位的符号计算地址并填充、完善最终信息得到可执行程序的过程。

  重定位由两步组成:

  1. 重定位节和符号定义:链接器将所有相同类型的节合并为同一类型的新的聚合节;
  2. 重定位节中的符号引用:链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。
      对于hello.o来说,首先其.data等节与其他可重定位目标文件的相应类型的节合并为同一类型的新的聚合节,然后链接器修改代码节和数据节中每个符号的引用,例如图5.16,可以看到,原先在hello.o中未被重定位的符号和函数在hello中重定位后将地址填入了相应的位置:

5.6 hello的执行流程

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

子程序名 子程序地址
ld-2.31.so! dl_init 0x7f1bf1a3d8c0
hello!_start 0x4010d0
hello!_libc_csu_init 0x401190
hello!_init 0x401000
hello_main 0x401105
hello!puts@plt 0x401030
hello!exit@plt 0x401060
hello!printf@plt 0x401040
hello!sleep@plt 0x401070
hello!getchar@plt 0x401050
hello!_fini 0x401208

5.7 Hello的动态链接分析

  分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
  我们通过elf文件中的描述找到.got.plt所在的内存位置,观察其值的变化


  对于库函数而言,需要plt与got共同作用得到正确的地址,plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次再调用plt时,指向的就是正确的内存地址,plt就能跳转到正确的区域。

5.8 本章小结

  本章介绍了链接的概念和作用,并在Ubuntu下使用链接命令得到了一个可执行程序,分析了该程序的elf格式、虚拟地址空间,并以该程序为例,解析了链接的重定位过程、执行流程、动态连接过程。

第6章 hello进程管理

6.1 进程的概念与作用

6.1.1 概念

  进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
  进程提供给应用程序一个假象,即应用程序在系统中是当前运行的唯一程序,它独占地使用处理器和内存。

6.1.2 作用

进程提供给应用程序的关键抽象:
  一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
  一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。

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

6.2.1作用

  Shell-bash的主要作用是解释用户输入的命令行,运行其他程序。
  Shell是一个命令行解释器,它为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序,用户可以用Shell来启动、挂起、停止甚至时编写一些程序。Shell还是一个功能相当强大的编程语言,易编写,易调试,灵活性较强。Shell是解释执行的脚本语言,在Shell中可以直接调用Linux系统命令。
  bash是Bourne Again shell的缩写,它是Linux操作系统缺省的shell,是Bourne shell的扩展,与Bourne shell完全向后兼容,并且在Bourne shell的基础上增加、增强了很多特性。Bash放在/bin/bash中,它有许多特色,可以提供如命令补全、命令编辑和命令历史表等功能,它还包含了很多C shell和Korn shell中的优点,有灵活和强大的编程接口,同时又有很友好的用户界面。

6.2.2 处理流程

  1. 从终端读入命令
  2. 检查命令是否是内部命令
  3. 若不是再检查是否是一个应用程序
  4. 在搜索路径里寻找该应用程序
  5. 若命令不是一个内部命令且路径中不存在该可执行文件,显示错误信息
  6. 若找到命令,将该内部命令或应用程序分解为系统调用传给内核

6.3 Hello的fork进程创建过程

  在命令行键入hello运行命令,shell判断该命令是可执行文件,于是调用fork函数给hello程序创建一个进程并分配一个标识符,子进程得到与父进程用户级虚拟地址空间和任何打开文件描述符相同的副本,但他们有不同的PID。

6.4 Hello的execve过程

  fork函数创建完进程后,调用execve函数到我们输入的目标路径中寻找hello文件,找到后创建一个内存映像,为该程序的栈区域创建新的区域结构将可执行文件的片复制到代码段和数据段等。然后为共享库建立映射空间,整个内存映射空间的结构如下图所示。最后设置当前进程上下文的程序计数器,将其指向入口函数,并将控制传递给新程序的主函数。

6.5 Hello的进程执行

基本概念:
  结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
  时间片是指分时操作系统分配给每个正在运行的进程微观上的一段CPU时间;
  进程上下文指的是用户进程传递给内核的参数以及内核要保存的变量和寄存器值和当时的环境等;
  中断上下文指的是硬件通过触发信号,导致内核调用中断处理程序,进入内核空间的过程中需要将一些参数传递给内核,内核通过这些参数进行中断处理,而硬件传递过来的参数和内核需要保存的一些其他环境即为中断上下文;
  内核态:在CPU高执行级别下,代码可以执行特权指令,访问任意的物理地址;
  用户态:在CPU相应的低级别执行状态下,代码的掌控范围会受到限制,只能在对应级别允许的范围内活动;

从用户态到内核态切换可以通过三种方式:

  1. 系统调用;
  2. 异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,就会触发切换;
  3. 外设中断:当外设完成用户的请求时,会向CPU发送中断信号;

处理器总处于以下状态中的一种:

  1. 内核态,运行于进程上下文,内核代表进程运行于内核空间;
  2. 内核态,运行于中断上下文,内核代表硬件运行于内核空间;
  3. 用户态,运行于用户空间。

hello的执行:
  在execve将程序加载完毕之后,hello处于用户态,打印字符串”Hello 1190202121 李忠根”,然后调用sleep函数,该函数为系统函数,因此会产生用户态到内核态的切换,此时hello进程被移除运行队列,加入到等待队列,计时器计时开始,hello进程的上下文被保存,内核进行上下文的切换(切换示意图如下图所示),将控制转移给其他进程,该进程恢复上下文开始继续运行。当计时时间达到预定时间后,会发送中断信号中断当前进程,进而又触发上下文转换,切换回hello进程的上下文。
  当hello调用getchar函数时,实际是调用的系统函数read,因此进入内核态,内核中的陷阱处理程序请求来自键盘缓冲区的直接存储器访问。此时进行上下文的切换,执行其他进程,在键盘缓冲区到内存的传输完成之后,引发中断信号,切换回hello进程的上下文继续执行。

6.6 hello的异常与信号处理

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

  当处理器检测到有事件发生时,它就会通过一张叫做异常表的跳转表,进行一个间接过程调用(异常),到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序),当异常处理程序完成处理后,根据引起异常的事件的类型会发生以下 3 种情况中的一种:

  1. 处理程序将控制返回给当前指令
  2. 处理程序将控制返回给下一条指令
  3. 处理程序终止被中断的程序

异常可被分为四类:
  类别 原因 异步/同步 返回行为
  中断 来自I/O设备的信号 异步 总是返回到下一条指令
  陷阱 有意的异常 同步 总是返回到下一条指令
  故障 潜在可恢复的错误 同步 可能返回到当前指令
  终止 不可恢复的错误 同步 不会返回

对这四类的异常处理方式如下:
  中断:
  在当前指令完成执行之后,处理器注意到中断引脚的电压变高了,就从系统总线读取异常号,然后调用适当的中断处理程序。当处理程序返回时,它就将控制返回给下一条指令(也即如果没有发生中断,在控制流中会在当前指令之后的那条指令)。结果是程序继续执行,就好像没有发生过中断一样。

  陷阱:
  陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。

  故障:
  故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的 abort 例程,abort 例程会终止引起故障的应用程序。

  终止:
  终止处理程序从不将控制返回给应用程序。处理程序将控制返回给一个 abort 例程,该例程会终止这个应用程序。

  对hello程序进行测试:

  回车:当乱按键盘时(如回车),会直接将该字符输出到控制台上,而不会影响当前程序的运行。

  Ctrl-Z:Ctrl-Z会发送一个SIGTSTP信号给前台程序,使其停止,hello进程将被挂起,此时输入ps, jobs, fg, kill命令结果如下:

  ps:把当前的所有进程都列出

  jobs:显示当前暂停的进程

  fg:使hello任务在前台继续进行

  kill 后接-9+hello的pid向hello发送一个SIGKILL的信号,直接杀死hello进程

  Ctrl-C将发送一个SIGINT信号给hello进程,直接使得hello彻底结束,即终止。

6.7本章小结

  本章对进程的概念和作用进行了简单介绍,并简述了bash的作用以及处理流程。然后针对hello程序,介绍了调用fork、execve的过程。最后介绍了hello的进程执行和异常与信号处理的机制。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:
  在有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。

线性地址:
  线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。

虚拟地址:
  虚拟地址是Windows程序时运行在386保护模式下,这样程序访问存储器所使用的逻辑地址称为虚拟地址,与实地址模式下的分段地址类似,虚拟地址也可以写为“段:偏移量”的形式,这里的段是指段选择器。

物理地址:
  在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址,又叫实际地址或绝对地址。

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

  一个逻辑地址由两部分组成:段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。
  索引号,这里可以直接理解成数组下标,它对应的“数组”就是段描述符表,段描述符具体描述了一个段地址,这样,很多段描述符就组成段描述符表。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
  这里面,我们只用关心Base字段,它描述了一个段的开始位置的线性地址。
  Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。
  GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
  给定一个完整的逻辑地址[段选择符:段内偏移地址],看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。把Base + offset,就是要转换的线性地址了。

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

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


从线性地址到物理地址变换的过程如下:
  如图,CPU 中的一个控制寄存器,页表基址寄存器指向当前页表,n位的虚拟地址(线性地址)包含两个部分:一个p位的虚拟页面偏移和一个n-p位的虚拟页号。MMU利用VPN来选择适当的PTE,将页表条目中的物理页号和虚拟地址中的VPO串联起,就能得到相应的物理地址。

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

  虚拟地址被划分为4个VPN和1个VPO,每个VPN i都是一个到第i级页表的索引。第j级页表中的每个PTE都指向第j+1级的某个页表的基址。第4级页表的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU需要访问4个PTE,示意图如下:

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

  MMU将虚拟地址转化为物理地址后,该地址被分为标记位(CT)、组号(CS)和偏移量(CO),然后根据CS在L1中寻找对应的组,查看该组中的对应的行是否有效,以及CT是否相等,若相等则取出偏移量为CO的数据,即命中。若不命中,依次去L2、L3和主存中寻找该数据,若找到,则将该数据所在块更新至各级cache,示意图如下所示:

7.6 hello进程fork时的内存映射

  当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。私有的写时复制如下图:

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 做的最后一件事情就是设置当前进程 上下文的程序计数器,使之指向代码区域的入口点。
    下一次调度这个进程时,它将从这个入口点开始执行。Linux 将根据需要换入代码和数据页面。下图为加载器映射用户地址空间区域的图示:

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

  当指令引用一个虚拟地址,而与该地址相对应的物理页面不在内存中,因此必须从磁盘中取出时,就会发生缺页故障。
发生缺页故障之后,进入内核态,控制传递给缺页故障处理程序。
  缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中了,指令就可以没有故障地运行完成了。

7.9动态存储分配管理

  Printf会调用malloc,请简述动态内存管理的基本方法与策略。
  动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为 一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已 分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用 来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
  分配器有两种基本风格:显式分配器和隐式分配器。

7.9.1 隐式空闲链表

  一个块是由一个字的头部、有效载荷,以及可能的一些额外的填充组成。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。
  我们称这种结构就隐式空闲链表,空闲块是通过头部中的大小字段隐含地连接着。分配器可以通过遍历堆中的所有块,从而间接遍历整个空闲块的集合。
  当一个应用请求一个块时,分配器搜索空闲链表,找到合适的未分配的空间进行放置,有三种放置策略:首次适配、下一次适配和最佳适配。

7.9.2 显式空闲链表

  将空闲块组织为某种形式的显式数据结构,实现这个数据结构的指针可以存放在这些空闲块的主体里面,例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继指针。

显式链表维护的方法有两种:
  一种方法是用后进先出(LIFO)的顺序维护链表,将新释放的块放置在链表的开始处。使用 LIFO 的顺序和首次适配的放置策略,分配器会最先检査最近使用过的块。
  另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。

7.10本章小结

  本章介绍了hello的存储器地址空间,intel的段式管理和页式管理,以及四级页表支持下的虚拟地址到物理地址的变换和三级cache支持下的物理内存访问。并从内存映射的视角介绍了hello进程的fork和execve,最后介绍了缺页故障及缺页中断处理和动态内存分配。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

  所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。
  设备的模型化:文件
  设备管理:unix io接口

8.2 简述Unix IO接口及其函数

  Unix IO接口是Linux内核引出的简单低级的应用接口,这个接口的引出得益于Linux将设备映射为文件的方式,这个接口使得所有的输人和输出都能以一种统一且一致的方式来执行:
  ·打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
·改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
  ·读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为 m 字节的文件,当k>=m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号”。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
  ·关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

函数:
●int open(char *filename, int flags, mode_t mode):
  进程是通过调用 open 函数来打开一个已存在的文件或者创建一个新文件的。
  open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件;mode 参数指定了新文件的访问权限位。

●int close(int fd);
  进程可通过close函数关闭一个打开的文件。

●ssize_t read(int fd, void *buf, size)t n);
  应用程序是通过调用read函数来执行输入,read 函数从描述符为 fd 的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示 EOF。否则,返回值表示的是实际传送的字节数量。

●ssize_t write(int fd, const void *buf, size)t n);
  write 函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。

●off_t lseek(int handle, off_t offset, int fromwhere);
  通过调用lseek函数,应用程序能够显式地修改当前文件的位置。

8.3 printf的实现分析

int printf(const char *format,...)
{int i;char buf[256];va_list arg = (va_list)((char*)(&fmt) + 4);i = vsprintf(buf, fmt, arg);write(buf, i);return i;
}

  printf函数接收一个fmt格式,将匹配到的参数按照fmt格式输出,并返回字串长度。
  vsprintf接收格式字符串fmt,用该格式对参数进行格式化,将格式化后的字符串存入buf,返回字符个数,即生成了显示信息;而后调用write系统函数将buf中的i个字符输出到显示器上,在write函数执行时,陷阱-系统调用 int 0x80或syscall将字符串的ASCII码从寄存器复制到显卡的显存中
  由字符显示驱动子程序通过ASCII码在字模库中找到点阵信息存储到vram(存储每一个点的RGB颜色信息)。
  显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),由此字符串在屏幕上被打印出来。

8.4 getchar的实现分析

int getchar(void)
{static char buf[BUFSIZ];static char* bb=buf;static int n=0;if(n==0){n=read(0,buf,BUFSIZ);bb=buf;}return (--n>=0)?(unsigned char)*bb++:EOF;
}

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

8.5本章小结

  本章介绍了Linux的IO设备管理方法,并简单叙述了Unix IO接口以及相应的函数,然后对printf和getchar的实现进行了分析。

结论

  由程序员编写的高级语言程序文件经过预处理器预处理后hello.i文件,该文件再经过编译器编译得到汇编语言文件hello.s,该文件经过汇编器翻译为机器指令得到可重定位目标文件hello.o,该文件经过链接器与其需要的库相链接最终得到可执行程序hello。
  hello生成后,在shell中在命令行输入“./hello 119xxxxxxx"使其运行,该过程shell-bash将该指令解析为应用程序,调用fork函数创建一个子进程,再调用execve函数创建一个内存映像,为hello的栈区域创建新的区域结构将hello复制到代码段和数据段等。然后为共享库建立映射空间。最后设置当前进程上下文的程序计数器,将其指向入口函数,并将控制传递给新程序的主函数。
  主函数运行,检查命令行参数,符合条件则进入循环,调用printf函数在屏幕上显示字符串,然后调用系统函数sleep,内核保存当前的上下文并切换到其他进程,直到计时器达到预定时间。循环10次后,调用getchar函数等待输入。最后返回。
  hello进程终止后向内核发送一个信号,进程被回收,内存映像被清理,恢复到执行hello前的状态。
  计算机系统的设计妙不可言,其运用的多个抽象使得系统管理变得方便轻松,而软硬件相互配合共同完成工作的机制也令人拍案叫绝。其中从硬件到软件的设计都记载着无数先辈无数的心血,他们精妙地组织软硬件的配合,巧妙地设计软硬件的机制,为后人留下了一笔宝贵的技术与思想财富。而我们也将站在巨人的肩膀上,充分利用这些巧妙的机制设计出更好的程序,甚至去进一步创新这些机制,设计出更为巧妙的机制,使之能够更好地为人类服务。

附件

  • hello.c:用C语言编写的源代码,用于了解hello的执行过程;
  • hello.i:将hello.c预处理后生成的文件,用于分析预处理的过程;
  • hello.s:hello.i被编译器编译后生成的汇编文件,用于分析编译过程;
  • hello.o:hello.s被汇编器汇编后生成的可重定位目标文件,用于分析汇编过程;
  • hello:链接器链接可重定位文件后得到的可执行文件,用于查看执行流程以及进行进程中信号、异常处理的实验;
  • elfhello.txt:elf格式,用于elf格式的查看与分析;
  • hello_asm.txt:hello.o的反汇编文件,用于hello内函数、变量以及重定位等操作的分析;
  • hello.txt:hello的反汇编文件:用于hello内函数、变量以及链接等操作的分析。

参考文献

[1] Randal E.Bryant / David O’Hallaron. 深入理解计算机系统(原书第3版). 北京:机械工业出版社,2016-11.
[2] 预处理. 百度百科. https://baike.baidu.com/item/预处理.
[3] 编译. 百度百科. https://baike.baidu.com/item/编译.
[4] 汇编程序. 百度百科. https://baike.baidu.com/item/汇编程序.
[5] 链接. 百度百科. https://baike.baidu.com/item/链接.
[6] 进程. 百度百科. https://baike.baidu.com/item/进程.
[7] 飘零过客. ELF格式文件详细分析. CSDN:https://blog.csdn.net/xuehuafeiwu123/article/details/72963229,2017-06-12.
[8] Croxd. Linux下shell脚本:bash的介绍和使用(详细). CSDN: https://blog.csdn.net/weixin_42432281/article/details/88392219,2019-03-15.
[9] PacosonSWJTU. C打印函数printf的一种实现原理简要分析. CSDN: https://blog.csdn.net/PacosonSWJTU/article/details/48881265,2015-10-03.
[10] 66Kevin. C语言 getchar()原理及易错点解析. CSDN: https://blog.csdn.net/weixin_44551646/article/details/98076863,2019-08-01.

计算机系统大作业——程序人生相关推荐

  1. HIT 深入理解计算机系统 大作业 程序人生-Hello’s P2P

    HIT 深入理解计算机系统 大作业 程序人生-Hello's P2P 本论文旨在研究 hello 在 linux 系统下的整个生命周期.结合 CSAPP 课本, 通过 gcc 等工具进行实验,从而将课 ...

  2. 【2022】哈工大计算机系统大作业——程序人生Hello’s P2P

    2022哈工大计算机系统大作业--程序人生Hello's P2P 摘要 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的概 ...

  3. 2022计算机系统大作业——程序人生-Hello’s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机 学    号 120L021716 班    级 2003005 学       生 蔡泽栋 指 导 ...

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

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机类 学    号 120L021923 班    级 2003006 学       生 甄自镜 指 导 ...

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

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

  6. 哈工大2022年春季学期计算机系统大作业——程序人生

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 人工智能(未来技术) 学   号 7203610716 班   级 20WJ102 学       生 孙铭蔚 ...

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

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

  8. 2021春深入理解计算机系统大作业——程序人生

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学    号 1190200608 班    级 1903004 学       生 琚晓龙 指 导 ...

  9. HIT计算机系统大作业-程序人生-Hello’s P2P

    计算机系统大作业 ** 由于采用静态部署,需要看图片详细分析的小伙伴请移步个人博客网站:** 个人博客 题目:程序人生-Hello's P2P 学号: 姓名:熊峰 摘要: hello程序作为最简单的. ...

  10. 哈工大 2021春 计算机系统 大作业程序人生

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

最新文章

  1. 期末Linux复习容易迷糊的地方!
  2. 重新定义Wi-Fi功能,Wi-Fi 6为什么要分两步?
  3. sql server密码过期,通过SSMS修改策略报错
  4. php标准输出重定向,python标准输出重定向方式
  5. OSI七层模型详解-开放系统互联参考模型详解
  6. FBEC2021暨第六届金陀螺奖颁奖典礼盛大开幕
  7. Linux 莱特币Litcoin节点搭建
  8. (50)System Verilog 类中约束数组元素
  9. mysql数据库建表的作用_浅谈(SQL Server)数据库中系统表的作用
  10. java8 update 91 有什么用_为什么java8还在被大量使用?
  11. CRC校验(循环冗余校验)小知识
  12. 构建基于 MCU 安全物联网系统
  13. python数据结构与算法 20 递归和递归三定律
  14. 绿皮书——iOS导出微信聊天记录,并用python制作词云
  15. java流意外结束_SyntaxError:输入节点js的意外结束
  16. Opencv 实战五 图像拼接
  17. hubot+slack(hubot部分)
  18. FITC修饰药物;CY3荧光标记氟维司群/依西美坦/齐多夫定/丁二酸(琥珀酸)/醋酸卡泊芬净的定制合成
  19. QT自带QTcpServer架构分析
  20. ue4 后期处理景深_【UE4设计师】2-3后期处理效果——使用景深设置电影拍摄

热门文章

  1. 实力见证!企企通斩获「2021年软件行业应用领域领军企业」殊荣
  2. Spark使用Log4j将日志发送到Kafka
  3. ISO-8601及GMT时间格式
  4. 最优化方法:一、总论
  5. 京东计算机新书销量榜 TOP 1
  6. 为了安全起见,要求使用强SA密码。请使用SAPWD开关提供同一密码
  7. 局域网共享工具_Win10创建网络共享文件夹|设置局域网共享文件夹
  8. 用Java开发HTTP代理服务器
  9. 计算机电缆数字是什么意思,通信电缆型号及含义
  10. python实现kmeans聚类