计算机系统 大作业
题        目  程序人生-Hello’s P2P 专         业   计算学部 学   号   1190201018 班   级   1936603 学        生  李昆泽     指 导 教 师 刘宏伟               计算机科学与技术学院 2021年6月

摘 要

本文从多个方面详细分析了hello程序在Linux系统下从诞生到执行结束的整个过程,并且结合课本的相关章节,运用相关的操作工具,对hello在整个过程中出现的各种现象、结果进行了分析与测试,力求加深对计算机系统的理解。

关键词:Linux;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.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简介

P2P:
       GCC编译器驱动程序读取源程序文件并把它翻译成一个可执行目标文件。下图展示了编译系统把hello.c的源程序转化为可执行文件hello的完整过程。

在预处理阶段,预处理器cpp读取需要的系统头文件内容,并把它直接插入程序文本中,结果得到hello.i。
       在编译阶段,编译器ccl间文本文件hello.i翻译成hello.s,这是一个汇编语言的程序。
       在汇编阶段,汇编器as将hello.s翻译成机器语言指令,并将结果保存在目标文件hello.o中,它以可重定位目标程序的格式存储。
       在链接阶段,链接器ld需要将一些库函数合并到hello.o的程序中,最终得到hello的可执行文件。
       用户在Ubuntu shell键入./hello启动此程序,shell调用fork函数为其产生子进程,hello便成为了进程(process)。
O2O:
       OS的进程管理调用fork函数产生子进程,调用execve函数,进行虚拟内存映射(mmp),并为运行的hello分配时间片以执行取指译码流水线等操作;OS的储存管理以及MMU解决VA到PA的转换,cache、TLB、页表等加速访问过程,IO管理与信号处理综合软硬件对信号等进行处理;程序结束时,shell回收hello进程,内核将其所有痕迹从系统中清除。

1.2 环境与工具

硬件环境:Intel Core i7-9750H CPU;2.60GHz;8GB RAM
软件环境:Windows10 64位;Ubuntu 16.04 LTS 64位
开发与调试工具: gcc;edb;gdb;objdump;readelf;codeblocks

1.3 中间结果

文件名 文件作用
hello.i 预处理器生成的文件,分析预处理器行为
hello.s 编译器生成的汇编语言程序,分析编译器行为
hello.o 可重定位目标程序,分析汇编器行为
hello 可执行目标程序,分析链接器行为
hello.elf hello.o的elf格式,分析汇编器和链接器行为
hello.asm hello.o的反汇编,主要是为了分析hello.o
_hello.elf 可执行文件hello的elf格式,作用是重定位过程分析
_hello.asm 可执行文件hello的反汇编,作用是重定位过程分析

1.4 本章小结

本章主要简要介绍了hello程序P2P、020的过程,列出了实验中生成的中间文件,列出了实验使用的软硬件环境、调试工具等等。

第2章 预处理

2.1 预处理的概念与作用

概念:根据以符号“#”开头的预处理命令,将所需系统头文件的内容插入到程序文本中,它是在编译之前进行的处理。
作用
(1)宏定义:将宏名替换为对应文本;
(2)文件包含:根据以字符#开头的命令,修改原始的C程序。主要执行的操作是获取所需的系统头文件,并把它直接插入程序文本中, 该过程递归进行,及被包含的文件可能还包含其他文件。
(3)条件编译:对于满足if条件的代码进行筛选,只有满足的代码才进行编译。

2.2在Ubuntu下预处理的命令

预处理命令:cpp hello.c > hello.i

2.3 Hello的预处理结果解析

使用notepad++打开hello.i文件,可以发现整个文件已经被扩展成了3127行。而前面的3000多行就是.c文件中包含的头文件,这里体现的就是预处理器根据以字符“#”开头的命令,修改原始的C程序的结果。

2.4 本章小结

本章主要介绍了预处理的概念与作用。对hello.c执行预处理的命令,并结合生成的hello.i文件,解析了hello的预处理结果。
       预处理过程是后续所有操作的基础,是不可或缺的重要过程。

第3章 编译

3.1 编译的概念与作用

编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。该程序包含函数main的定义,这个过程称为编译。
       编译的作用就是将高级语言源程序翻译成等价的目标程序,并且进行语法检查、调试措施、修改手段、覆盖处理、目标程序优化等步骤。

3.2 在Ubuntu下编译的命令

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

3.3 Hello的编译结果解析

3.3.1 数据

  1. 变量
    (1)全局变量
    在程序中sleepsecs被声明为int类型全局变量,且已经被赋值。由于.data节存放已经初始化的全局和静态C变量,编译器首先将sleepsecs在.text代码段中声明为全局变量,然后在.data节设置对齐方式(.align)为4字节对齐,设置类型(.type)为对象,设置大小(.size)为4字节,设置为long类型(.long),其值为2,如下图所示。

           我们发现全局变量sleepsecs的类型本来是int,却被转化为了long类型。这是因为隐式转换的规则,并且int转化为long不会丢失数据。
    (2)局部变量
           编译器将局部变量存储在寄存器或者栈空间中。在hello.s中编译器将i存储在栈上空间-4(%rbp)中,如下图所示。

    (3)对于int argc
           argc是我们main函数的第一个参数。分析.s文件,我们发现argc首先被保存在了寄存器%edi中,又由于argc后续需要参与判断,编译器还将argc赋值给了-20(%rbp)。
    (4)字符串
           从源程序中可以看出,程序中主要出现了两个字符串——“Usage: Hello 学号 姓名!\n”和“Hello %s %s\n”。argv[]中也保存的是字符串,这个我们在后续分析数组的时候再具体分析。
           在hello.s中我们可以看到字符串“\345\255\246\345\217\267 \345\247\223\345\220\215\357\274\201”,这些其实是“学号 姓名”的UTF-8格式。
           我们可以看到,这两个字符串都是在.rodata声明的,如下图所示。

  2. 常量
           这里的常量主要指常数立即数。常数立即数是直接在汇编代码中存在的,以“$+常数”的形式出现在汇编代码中。
    3.3.2 赋值
           源程序中的赋值操作主要有:int sleepsecs=2.5 、 i=0 和 i++。
           对于int sleepsecs = 2.5,根据前一部分的分析,sleepsecs是全局变量,并且直接在.data节中就已经将sleepsecs 声明为值为2的long类型数据(隐式转换)。
           对于i=0。在hello.s文件中是通过汇编语句movl $0, -4(%rbp)将立即数赋值给局部变量i的。这里使用的是“movl”,这是因为局部变量i是int类型的数据,占4个字节,如下图所示。

           而对于i++,在汇编代码中是通过语句addl $1, -4(%rbp)实现的,这里-4(%rbp)中保存的是i的值,因此通过addl达到每次循环让i加1的目的。

3.3.3 类型转换

源程序中用到的类型转换主要是隐式类型转换,即int sleepsecs=2.5,将浮点数2.5转化为int类型的整数2,然后由于编译器缺省,int类型又被转换为了long类型。
       值得注意的是, 2.5被隐式转换之后,变成了2而不是3。这是由于当 double 或 float 向 int 进行类型转换的时候,程序遵循向零舍入的原则。

3.3.4 算术操作

汇编语言中算术操作的相关指令如下。

        具体到我们的源程序中,算术操作有i++(即i=i+1),这个是通过汇编语句addl $1, -4(%rbp)实现的。除此之外,还有汇编语句subq $32, %rsp。这里对栈指针进行减法操作,目的是开辟一段新的栈空间。

3.3.5 关系操作

关系操作的主要汇编指令如下。

比较和测试指令不修改任何寄存器的值,只是设置条件码。汇编代码中主要有两处涉及到关系操作,分别是cmpl $3, -20(%rbp)和cmpl $9, -4(%rbp)。
        第一处对应的源代码是argc!=3,汇编代码将其优化为如果argc==3则跳转至后续的语句,如下图所示。

        第二处对应的源代码是i<10,这里汇编代码将它优化为了i<=9,编译器会计算-4(%rbp)-9,并设置条件码,随之jle语句利用这些条件码,进行相应的跳转处理。

3.3.6 数组/指针/结构操作

源代码中出现的数组主要是argv[],在向main函数传参时,通过movq %rsi, -32(%rbp)进行参数的传递,把argv数组的首地址保存在栈中。
        在后面的循环中,读取了argv中的元素,分别读取了argv[1]和argv[2],这是通过下述汇编代码实现的。

        首先将数组的首地址放到%rax中,然后将它加8或者16,分别获取argv[1]和argv[2]的地址(argv中保存的是指针,占8个字节),然后再通过movq (%rax), %rdx的方式将%rax里保存的地址处的值转移到%rdx中。

3.3.7 控制转移

控制转移常常是配合指令CMP和TEST存在的。汇编代码中有两处出现了控制转移。第一处如下图所示。

这里对应的是源代码中的argc!=3,但编译器把它优化为了如果argc==3,则跳转至.L2。
        第二处如下图所示。

        这里能很明显的看出是一个循环,体现了编译器一种jump to middle的翻译方法,-4(%rbp)保存的是i的值,如果i<=9就跳转到.L4,执行循环,否则就执行后面的语句。

3.3.8 函数操作

(1)main函数
函数调用:main函数被系统启动函数 __libc_start_main调用,call指令将下一个指令的地址压入栈中,然后跳转到main执行。
参数传递:向main函数传递的参数是argc和argv,分别使用%rdi(%edi)和%rsi存储。
函数返回:函数设置%eax为0后就正常退出,使用leave退出。
(2)printf函数
第一次函数调用:printf函数在具体的汇编代码中被优化为puts函数。
第一次参数传递:首先将rdi赋值为字符串“Usage: Hello 学号 姓名! \n”字符串的首地址(leaq .LC0(%rip), %rdi),然后调用了puts函数,将字符串参数传入。
第二次函数调用:这次是直接调用printf函数。
第二次参数传递:这次需要传递3个参数,%rdi保存的是“Hello %s %s\n”的首地址,%rsi保存的是argv[1],%rdx保存的是argv[2]。
(3)sleep函数
函数调用:通过汇编语句call sleep@PLT调用。
参数传递:传入参数的过程为movl sleepsecs(%rip), %eax和movl %eax, %edi,对应于全局变量sleepsecs。
(4)getchar函数
函数调用:通过汇编语句call getchar@PLT调用。
(5)exit函数
函数调用:通过汇编语句call exit@PLT调用。
参数传递:通过汇编语句movl $1, %edi将%edi寄存器内容设置为1。

3.4 本章小结

本章首先介绍了编译的概念与作用,以及在Ubuntu下编译的指令,然后我们具体到对hello.s文件进行数据、赋值、类型转换、算术操作、关系操作、数组/指针/结构操作以及控制转移和函数操作进行了详细的分析和研究。

第4章 汇编

4.1 汇编的概念与作用

概念:汇编器(as)将.s汇编程序翻译成机器语言,把这些机器语言指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中,这个过程就叫做汇编。
作用:汇编的作用就是将高级语言转化为机器可直接识别执行的机器指令代码文件。

4.2 在Ubuntu下汇编的命令

汇编的命令:as hello.s -o hello.o

4.3 可重定位目标elf格式

键入命令行readelf -a hello.o > hello.elf将elf可重定位目标文件输出定向到文本文件hello.elf中,如下图所示。

       ELF格式的可执行目标文件的各类信息如下:

       ELF头:以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。具体截图如下。

       .text:已编译程序的机器代码;
       .rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表;
       .data:已初始化的全局和静态C变量(局部C变量在运行时保存在栈中);
       .bss:未初始化的全局和静态C变量以及所有被初始化为0的全局和静态变量(不占据实际空间);
       .symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息,截图如下。

       .rel.text:一个.text节中位置的列表,链接器链接时会修改位置,具体截图如下。

       该节包括的内容是:偏移量、信息、类型、符号值、符号名称和加数。
       其中,偏移量表示需要进行重定向的代码在.text或.data节中的偏移位置;信息包括symbol和type两部分,其中symbol占前4个字节,type占后4个字节,symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位的类型;符号名称是重定位目标的名字;最后的加数表示重定位过程需要加减的常量。
       .rel.data:被模块引用或定位的重定位信息(需要被修改);
       .debug:一个调试符号表,其条目时程序中定义的全局变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件;
       .line:原始C源程序的行号和.text节中机器指令之间的映射;
       .strtab:一个字符串表,其内容包括 .symtab 和 .debug节中的符号表,以及节头部中的节名字。
       节头部表:节头表包括节名称,节的类型,节的属性(读写权限),节在ELF文件中所占的长度以及节的对齐方式和偏移量,具体如下图所示。

4.4 Hello.o的结果解析

命令行输入:objdump -d hello.o >hello.asm

       机器语言指的是二进制的机器指令集合,而机器指令是由操作码和操作数构成的。汇编语言的主体是汇编指令。汇编指令和机器指令的差别在于指令的表示方法上,汇编指令是机器指令便于记忆的书写格式。
       除此之外,还有如下几个具体的区别:
(1)分支转移
       hello.s文件中分支转移是使用段名称进行跳转的(见图 22),而hello.o文件中分支转移是通过地址(重定位地址)进行跳转的(见图 23)。


(2)函数调用
       hello.s文件中,函数调用call后跟的是函数名称(见图 24);而在hello.o文件中,因为这些函数都是共享库函数,它们的地址是不确定的,因此call指令将相对地址全部设置为0,然后在.rela.text节中为其添加重定位条目,等待链接的进一步确定(见图 25)。


(3)全局变量
       hello.s文件中,全局变量的地址是通过段地址+%rip确定的(见图 26);对于hello.o的反汇编来说,则是0+%rip,因为.rodata节中的数据是在运行时确定的,也需要重定位,现在填0占位,并为其在.rela.text节中添加重定位条目(见图 27)。

4.5 本章小结

本章讨论了从hello.s到hello.o的汇编过程,通过readelf命令查看了可重定位目标elf格式。除此之外,使用了objdump工具得到了hello.o的反汇编代码,并和hello.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
或 gcc hello.o -o hello
截图如下。

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

输入命令:readelf -a hello > _hello.elf,截图如下。

       节头部表截图如下图所示。

5.4 hello的虚拟地址空间

使用edb加载hello,通过观察edb的Symbols小窗口,我们发现从虚拟地址从0x400000开始和5.3节中的节头表几乎是一一对应的,如下图所示。

5.5 链接的重定位过程分析

命令行:objdump -d -r hello
       反汇编截图如下。

       下面是对hello.o和hello反汇编后结果的一些对比。
(1)_hello.asm比hello.asm多了许多文件节
       在hello.asm文件中,我们发现只有.text节。而在_hello.asm(hello的反汇编结果)中,不仅有很多其他的节(例如.init节,.plt节等),而且.text节中的内容也比之前有所增加。

(2)hello.asm中的大多是相对偏移地址,而_hello.asm文件中的地址是虚拟地址
       在原来的hello.o的反汇编文件中,所用的地址基本上是相对偏移地址。例如把main函数的地址设置为0,其他函数或者跳转在计算地址时都是在此基础上加一个相对偏移量。
       而在_hello.asm文件中,由于hello已经是可执行文件了,相关的重定位工作必须已经完成,所有虚拟地址也必须确定。尽管每次链接时动态库的虚拟地址都可能不同,但是当每次链接完成时,所有虚拟地址是唯一确定的。

(3)_hello.asm中增加了许多外部链接的共享库函数
       链接过程本身就是要把一些外部已经编译好的共享库添加到可执行文件中,所以在反汇编文件中出现这些共享库函数一点都不奇怪,截图如下所示。

       结合hello.o的重定位项目,分析hello中对其怎么重定位的。
重定位分析:
       当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置,它也不知道这个模块引用的外部函数或者全局变量的位置。所以针对最终位置未知的目标引用,它会生成一个重定位条目,告诉链接器在生成可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中。在把hello.o链接为可执行文件hello的过程中,链接器就是根据.rel_data和.rel_text节中保存的重定位信息对符号或者函数进行重定位的。具体的重定位过程我通过下面的一个例子给出。
       以sleepsecs为例,通过查看.rela.text中的重定位信息,我们得到有关sleepsecs的重定位条目如下:
       r.offset=0x60
       r.symbol=sleepsecs
       r.type=R_X86_64_PC32
       r.addend=-4
       这些字段告诉链接器修改开始于偏移量0x60处的32位PC相对引用,这样在运行时它会指向sleepsecs变量。
对应的重定位算法如下图所示。

       下面我们来手动模拟一下计算出sleepsecs虚拟地址的过程。
       由于ADDR(s) = ADDR(main) = 0x400627,r.offset = 0x60,
       所以refaddr = ADDR(s) + r.offset = 0x4005B0;
       又由于ADDR(r.symbol) = 0x601058,r.addend = -0x4,
       所以*refptr = ADDR(r.symbol) + r.addend – refaddr = 0x2009CD
       通过如下截图可验证算法的正确性。

5.6 hello的执行流程

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

子程序名 程序地址
ld-2.23.so!_dl_start 0x7f9f b3c11c33
ld-2.23.so!_dl_init 0x7f9f b3c11c65
hello!__libc_start_main@plt 0x400574
hello!puts@plt 0x400643
hello!exit@plt 0x40064d

(2)终端输入:./hello 1190201018 李昆泽

子程序名 程序地址
ld-2.23.so!_dl_start 0x7fd2 a03bcc33
ld-2.23.so!_dl_init 0x7fd2 a03bcc65
hello!__libc_start_main@plt 0x400574
hello!printf@plt(循环调用了10次) 0x400680
hello!sleep@plt(循环调用了10次) 0x40068d
hello!getchar@plt 0x40069c

5.7 Hello的动态链接分析

对于动态共享链接库中位置无关代码,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理。
       在dl_init之前,为了引用全集变量PIC,编译器利用了数据段与代码段的距离是一个运行时常量的事实,在数据段开始的地方创建了全局偏移量表GOT。每个被这个目标模块引用的全局数据目标都有一个8字节条目,还会生成一个重定位记录。在加载时,动态链接器会重定位GOT中的每个条目,使得它包含正确的绝对地址。


在之后的函数调用时,首先跳转到PLT执行.plt中操作,第一次访问跳转时,GOT 地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在 PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。

5.8 本章小结

本章首先介绍了链接的概念及作用,以及在linux下链接的命令行;接着对hello的elf格式进行了较为详细的分析,并分析了hello的虚拟地址空间;关于重定位过程的分析,本章通过对比hello和hello.o的反汇编文件,发现了它们之间的差异,了解了重定位在链接和汇编过程中的不同;除此之外,也对整个hello的执行过程进行了跟踪,在最后对hello进行了动态链接分析。

第6章 hello进程管理

6.1 进程的概念与作用

概念
狭义定义:进程是计算机科学中最深刻,最成功的概念之一。进程的经典定义就是一个执行中程序的实例,进程拥有一个独立的逻辑控制流和私有的地址空间。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
作用
       在现代计算机中,进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

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

作用
       Shell是用户与操作系统之间完成交互式操作的一个接口程序,它为用户提供简化了的操作。Shell最重要的功能是命令解释,从这种意义上说,Shell是一个命令解释器。Linux系统上的所有可执行文件都可以作为Shell命令来执行。当用户提交了一个命令后,Shell首先判断它是否为内置命令,如果是就通过Shell内部的解释器将其解释为系统功能调用并转交给内核执行;若是外部命令或实用程序就试图在硬盘中查找该命令并将其调入内存,再将其解释为系统功能调用并转交给内核执行。在查找该命令时分为两种情况:(1)用户给出了命令的路径,Shell就沿着用户给出的路径进行查找,若找到则调入内存,若没找到则输出提示信息;(2)用户没有给出命令的路径,Shell就在环境变量PATH所制定的路径中依次进行查找,若找到则调入内存,若没找到则输出提示信息。
处理流程
       Shell从终端读入输入命令。如果是内置命令则立即执行。否则调用相应的程序为其分配子进程并运行。总之就是对其求值。
1.Shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示:
SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |
2. 程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。
3. 当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程回到第一步重新分割程序块tokens。
4.Shell对~符号进行替换。
5.Shell对所有前面带有$ 符号的变量进行替换。
6.Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用$ (command)标记法。
7.Shell计算采用$ (expression)标记的算术表达式。
8.Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB 和换行符号。
9.Shell执行通配符* ? [ ]的替换。
10.shell把所有從處理的結果中用到的注释删除,並且按照下面的顺序实行命令的检查:
A. 内建的命令
B. shell函数(由用户自己定义的)
C. 可执行的脚本文件(需要寻找文件和PATH路径)
11.在执行前的最后一步是初始化所有的输入输出重定向。
12.最后,执行命令。

6.3 Hello的fork进程创建过程

父进程通过调用fork函数就创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈.子进程进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork 时,子进程可以读写父进程中打开的任何文件。其函数原型为pid_t fork(void);对于返回值,若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1。
       而对于hello来说,我们想要运行hello这个可执行文件时,需要在终端输入命令(例如 ./hello),我们输入的命令会被判断为非内置命令,然后shell试图在硬盘上查找该命令(即hello可执行程序),并将其调入内存,然后shell将其解释为系统功能调用并转交给内核执行。
       shell执行fork函数,创建一个子进程。这时候我们的hello程序就开始运行了。hello子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。于此同时Linux将复制父进程的地址空间给子进程,因此,hello进程就有了独立的地址空间。

6.4 Hello的execve过程

execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有当出现错误时,(例如找不到filename时),execve才会返回到调用程序。所以,与fork 一次调用返回两次不同,execve调用一次并从不返回。
       execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC指向_start地址,_start最终调用main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制。这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
       对于hello来说,execve首先在当前进程的上下文中加载并运行新程序hello,然后调用启动代码,启动代码设置栈,并将控制传递给新程序的主函数,该函数有以下原型:int main(int argv, char **argv, char **envp)或者等价的int main(int argc, char *argv[], char *envp)。
       当主函数开始执行时,用户栈的组织结构如下图所示。从栈底往栈顶看,首先是参数和环境字符串。紧随其后的是以null结尾的指针数组,其中每个指针都指向栈中的一个环境变量字符串。全局变量environ指向这些指针中的第一个envp[0]。紧随环境变量之后的是以null结尾的argv[ ]数组,其中每个元素都指向栈中的一个参数字符串。在栈的顶部是系统启动函数libc_start_main的栈帧。

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
       内核为每个进程维持一个上下文。上下文就是内核重新启动一个被抢占进程所需的状态。它包含进程运行所需要的一些寄存器、用户栈以及数据结构等等。在内核调度了一个新的进程运行时,它会首先保存当前进程的上下文,然后恢复某个先前被强占的进程被保存的上下文,最后将控制传递给这个新恢复的进程。
       调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被强占的进程。这种决策就叫调度(是由内核中的调度器的代码处理的)。在调度的过程中,可能会出现多个进程轮流运行的情况,这个概念就称为多任务,而多任务也叫做时间分片。
用户态与核心态转换
       进程hello一开始运行在用户模式中,直到它通过执行系统调用函数sleep或者exit时便进入到内核模式。由于sleep函数的特殊性,内核此时可能会将hello进程休眠,转而执行一个其他的进程。待内核中的处理程序完成对系统函数的调用后,再执行上下文切换,将控制返回给进程hello系统调用之后的那条语句。

6.6 hello的异常与信号处理

异常可以分为四类:中断、陷阱、故障和终止。下图对这些类别的属性做了小结。

       而在hello的执行过程中,主要会遇到中断(键盘上敲击CTRL -C或者CTRL-Z)和陷阱(系统调用)的异常;
       主要会产生SIGINT(来自键盘的中断),SIGSTP(来自终端的停止信号)等信号;
       对于中断异常,处理情况如图;

       对于陷阱,处理情况如图。

运行截图
(1)CTRL-Z

(2)ps

(3)jobs

(4)pstree


(5)fg

(6)kill

(7)输入乱码

(8)CTRL-C

异常与信号的处理
       对于CTRL-C或者CTRL-Z,键盘键入后,内核就会发送SIGINT或者SIGSTP信号。SIGINT信号默认终止前台作业,即终止程序hello,SIGSTP默认挂起前台的hello作业。
       对于fg信号,内核发送SIGCONT信号,我们刚刚挂起的程序hello重新在前台运行。
       对于kill -9 3381。内核发送SIGKILL信号给我们指定的pid(hello程序),结果杀死了hello程序。

6.7本章小结

在本章中,首先简单介绍了进程的概念和作用,并在此基础上简述了壳Shell-bash的作用与处理流程。然后对可执行文件hello进行了具体的分析,分别分析了hello的fork过程、execve过程、进程执行过程,最后分析了hello在执行过程中可能遇到的异常和信号处理,并进行了测试。

第7章 hello的存储管理

7.1 hello的存储器地址空间

(1)逻辑地址
       逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址,是由一个段标识符加上一个指定段内相对地址的偏移量。
(2)线性地址
       线性地址是逻辑地址到物理地址变换之间的中间层。hello程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。
(3)虚拟地址
       虚拟地址是程序运行在保护模式下,程序访问存储器所使用的逻辑地址称为虚拟地址。在保护模式中,在程序从磁盘加载进内存的中间加了一个中间层,即就是虚拟地址,在程序编译,链接的时候先映射进虚拟地址,在运行的时候会再映射进物理地址。这样的好处在于,在虚拟地址中,hello程序的虚拟地址,不管通过如何偏移,它都在虚拟地址中,最后再映射进物理地址,不会影响到其他的程序,起到了进程隔离,保护了其他的进程。
(4)物理地址
       物理地址用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。 这个概念应该是这几个概念中最好理解的一个,但是值得一提的是,虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽象,内存的寻址方式并不是这样。另外,如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;如果没有启用分页机制,那么hello的线性地址就直接成为物理地址了。

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

分段过程实质就是从逻辑地址到线性地址的过程。整个过程如下图所示。

       逻辑地址实际是由48位组成的,前16位是段选择符,后32位是段内偏移量。通过段选择符,我们可以获得段基址,再与段内偏移量相加,即可获得最终的线性地址。
       段选择符的16位格式如下图所示。

其中,
       索引:“描述符表”的索引;
       TI:如果 TI是0,那么描述符表是全局描述符表(GDT);如果TI是1,描述符表是局部描述表(LDT)。
       RPL:表示段的级别。为0,位于最高级别的内核态;为11,位于最低级别的用户态。在linux中也仅有这两种级别。

被选中的段描述符先被送至描述符的cache中,每次从描述符cache中取出32位段基址,再与32位段内偏移量(有效地址)相加得到线性地址,截图如下。

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

如果不考虑TLB与多级页表,虚拟地址可以分为虚拟页号VPN和虚拟页偏移量VPO。其中,VPN可以作为到页表中的索引。进而,通过页表基址寄存器(PTBR)我们可以在页表中获得条目PTE。一条PTE中包含有效位和物理页号(PPN)。如果有效位是0,则代表页面不在存储器中(缺页);如果有效位是1,则代表该内存已经缓存在了物理内存中,可以得到其物理页号PPN,再与物理页偏移量(PPO)共同构成物理地址PA。具体截图如下。

       注意,因为物理和虚拟页面都是P字节的,所以PPO和VPO是相同的。
       当页面命中时CPU硬件执行的步骤:
       第1步:处理器生成一个虚拟地址,并把它传送给MMU;
       第2步:MMU生成PTE地址,并从高速缓存/主存请求得到它;
       第3步:高速缓存/主存向MMU返回PTE;
       第4步:MMU构造物理地址,并把它传送给高速缓存/主存;
       第5步:高速缓存/主存返回所请求的数据字给处理器。

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

TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块,TLB通常具有高度的相联度。

       用以组选择和行匹配的索引和标记字段是从虚拟地址的虚拟页号中提取出来的,在TLB中寻址的过程其实和在cache中寻址的过程有点类似。下图展示了当TLB命中时(通常情况)所包括的步骤,所有的地址翻译步骤都是在芯片上的MMU中执行的,因此非常快。
       第1步:CPU产生一个虚拟地址;
       第2步和第3步:MMU从TLB中取出相应的PTE;
       第4步:MMU将这个虚拟地址翻译成一个物理地址,并将它发送到高速缓存/主存;
       第5步:高速缓存/主存将所请求的数据字返回给CPU。

四级页表
       使用层次结构的页表是为了压缩页表所占的空间。
       在Core i7 MMU中,36位VPN被划分成四个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含L1页表的物理地址。VPN1提供到一个L1 PET的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2 PTE的偏移量,以此类推。

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

通过上一节中的页表翻译,我们终于得到了物理地址PA,下面我们的工作时利用物理地址寻找相应的数据。
       首先使用物理地址的CI进行组索引(每组8路),对8个块分别对CT进行标志位的匹配。如果匹配成功且块的有效位为1,则成功命中。然后根据数据偏移量 CO取出相应的数据并返回。这里的数据是保存在L1中的,也就是一级Cache。
       如果没有命中,或者没找到相匹配的标志位,那么就会在下一级Cache中寻找,这里可能是二级Cache甚至三级Cache,只要本级Cache中没找到就要去下一级的Cache中寻找数据,然后逐级写入Cache。
       在更新Cache的时候,首先需要判断是否有有效位为0的块。若有,则直接写入;若不存在,则需要驱逐一个块(LRU策略),再进行写入。

7.6 hello进程fork时的内存映射

虚拟内存和内存映射解释了fork函数如何为hello进程提供私有的虚拟地址空间。
       当fork函数被hello进程调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello创建出的这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
       在fork在新进程中返回时,新进程的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任意一个进行写操作时,由于写时复制机制的作用,会创建一个新的页面,也就为每个进程保持了私有地址空间的概念。

7.7 hello进程execve时的内存映射

虚拟内存和内存映射在将程序加载到内存的过程中也扮演着关键的角色。假设运行在当前进程中的程序执行了如下的execve调用:
       execve(“hello”, NULL, NULL);
       加载并运行hello需要以下的几个步骤:
(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
(2)映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆地址也是请求二进制零的,初始长度为零。
(3)映射共享区域。 hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
下一次调度这个进程时,他将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

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

处理缺页要求硬件和操作系统内核协作完成,具体操作如下。
       第1步:处理器生成一个虚拟地址,并把它传送给MMU;
       第2步:MMU生成PTE地址,并从高速缓存/主存请求得到它;
       第3步:高速缓存/主存向MMU返回PTE;
       第4步:PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序;
       第5步:缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘;
       第6步:缺页处理程序页面调人新的页面,并更新内存中的PTE;
       第7步:缺页处理程序返回到原来的进程,再次执行导致缺页的指令,CPU将地址重新发送给MMU。因为虚拟页面现在已经缓存在物理内存中,所以会命中,主存将所请求字返回给处理器。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
       分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
       分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
       显式分配器(explicit allocator),要求应用显式地释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。
       隐式分配器(implicit allocator),要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbage collector),而自动释放未使用的已分配的块的过程叫做垃圾收集( garbage collection)。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
       显式分配器必须在严格的约束条件下工作,约束有:必须处理任意请求序列;立即响应请求;只使用堆;对齐块;不修改已分配的块。分配器的编写应该实现:吞吐率最大化;内存使用率最大化(两者相互冲突)。
       在分配器的具体实现中,主要有以下几种实现方法:
(1)隐式空闲链表
       隐式空闲链表的优点是简单。显著的缺点是任何操作的开销,例如放置分配的块,要求对空闲链表进行搜索,该搜索时间与堆中已分配块和空闲块的总数呈线性关系。

(2)带边界标记的隐式空闲链表
       这种方式可以允许在常数时间进行对前面块的合并,并且它对许多不同类型的分配器和空闲链表组织都是通用的。然而它也存在一个潜在的缺陷。它要求每个块都保持一个头部和一个脚部,在应用程序操作许多个小块时,会产生显著的内存开销。

(3)显式空闲链表
       堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。这样一来,会使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。显式空闲链表的缺点是空闲块必须足够大,以包含所有需要的指针,以及头部和可能的脚部。这就导致了更大的最小块大小,也潜在地提高了内部碎片的程度。

7.10本章小结

本章就hello的地址管理展开了一系列讨论。首先介绍了各类地址的概念以及在程序运行中充当的角色,接着进一步分析了从逻辑地址到线性地址的变化(段式管理),以及从线性地址到物理地址的变化(页式管理)。然后借TLB与四级页表支持下的VA到PA的变换详细分析了地址翻译的过程。紧接着分析了三级Cache支持下的物理内存访问,以及hello进程fork和execve时的内存映射,还有缺页故障与缺页中断处理的操作过程。最后通过动态存储分配管理这一节对之前的内容进行了一个整体的梳理,较为完整地阐明了动态内存分配的过程。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

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

8.2 简述Unix IO接口及其函数

Unix I/O接口
(1)打开文件
       一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息,应用程序只需要记住这个描述符。
(2)linux shell
       创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件<unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
(3)改变当前的文件位置
       对于每个打开的文件,内核保持着一个文件位置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。
(4)读写文件
       一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号”。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k。
(5)关闭文件
       当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
函数
(1)打开文件
       函数原型:int open(char *filename, int flags, mode_t mode);
       open函数将filename转换为一个文件描述符,并且返回描述符数字。flags参数指明了进程打算如何访问这个文件,mode参数则指定了新文件的访问权限位。
(2)关闭文件
       函数原型:int close(int fd);
       关闭描述符为fd的文件,关闭一个已关闭的描述符会出错。
(3)读和写文件
       函数原型:
       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的当前文件位置。返回值:成功则返回写的字节数,出错则为-1。

8.3 printf的实现分析

我们先来看printf的源码:

       这里va_list是char类型的指针,表示arg是…中的第一个参数的地址。另外,我们发现在printf函数里分别调用了vsprintf和write函数,下面对这两个函数一一分析。

       很容易看出,vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
       我们发现vsprintf函数中也调用了write函数,下面我们来追踪一下write函数的汇编代码。

       在write函数中,将栈中参数放入寄存器,ecx存放字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用sys_call。从这里我们也可以看出write是一个系统函数。下面是sys_call的汇编代码。

       sys_call函数实现的功能就是把将要输出的字符串从总线复制到显卡的显存中。显存中存储的是字符的ASCII码。字符显示驱动子程序通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。至此,就完成了对printf函数的分析,也完成了字符串的整个输出过程。

8.4 getchar的实现分析

(1)当运行到getchar函数时,程序将控制权交给os。在进行输入时,内容会先进入缓存区,并在屏幕上回显。直到我们键入Enter,通知os输入完成,这时才再将控制权交还给程序。
(2)异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
(3)getchar调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章介绍了Linux的I/O设备管理方法,简述了Unix IO接口及其函数,并对printf和getchar的实现进行了较为具体的分析。

结论

hello所经历的过程
(1)编写阶段,通过编写工具编写出hello.c;
(2)预处理阶段,预处理器cpp读取需要的系统头文件内容,并把它直接插入程序文本中,结果得到hello.i;
(3)编译阶段,编译器ccl间文本文件hello.i翻译成hello.s;
(4)汇编阶段,汇编器as将hello.s翻译成机器语言指令,并将结果保存在目标文件hello.o中;
(5)链接阶段,链接器ld需要将一些库函数合并到hello.o的程序中,最终得到hello的可执行文件;
(6)运行阶段:用户在Ubuntu shell键入./hello启动此程序,shell调用fork函数为其产生子进程,并由execve函数加载运行当前进程的上下文中加载并运行新程序hello;
(7)进程执行:内核为每个进程维持一个上下文,在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被强占的进程。所以hello进程不是一直在执行的;
(8)存储管理:在hello所经历的过程中,会产生各种地址,但最终我们真正期待的是物理地址,MMU将程序中使用的虚拟内存地址通过页表映射成物理地址;
(9)信号与异常:在hello的运行过程中可能会出现各种信号或者异常,内核会对这些信号进行处理;
(10)结束:hello最终被shell父进程回收,内核会收回为其创建的所有信息。
感悟
       这本教材的名字叫《深入理解计算机系统》,而我才刚刚入了门,远远谈不上是“深入理解”。但是仅凭目前这一点对计算机系统的初步认识,我就已经觉得这是一个无比复杂,同时也无比精妙的系统。计算机是由很多部分组成的,这些部分既有明确的分工,又相互合作,最终成就了现在的计算机。这些部分在现在看来合情合理,但是在第一台计算机出现之前,没有人知道计算机应该由哪些部分组成,应该怎样去计算。
       计算机系统这门课的学问很深,其中包含的是一代代计算机科学家的心血,这些不是仅仅通过这一门课所能够学完的,我们还需要在以后的实践中沉下心来,细细研究其中的学问。

附件

文件名 文件作用
hello.i 预处理器生成的文件,分析预处理器行为
hello.s 编译器生成的汇编语言程序,分析编译器行为
hello.o 可重定位目标程序,分析汇编器行为
hello 可执行目标程序,分析链接器行为
hello.elf hello.o的elf格式,分析汇编器和链接器行为
hello.asm hello.o的反汇编,主要是为了分析hello.o
_hello.elf 可执行文件hello的elf格式,作用是重定位过程分析
_hello.asm 可执行文件hello的反汇编,作用是重定位过程分析

参考文献

[1] Randal E. Bryant, David R. O’Hallaon. 深入理解计算机系统. 第三版. 北京:机械工业出版社[M]. 2018:1-737.
[2] 伍之昂. Linux Shell编程从初学到精通. 北京:电子工业出版社. 2011:1-59.
[3] Cjacker. Cmake Practice. 1-47.
[4] yjbjingcha. 进程控制在进程管理中的作用. https://www.cnblogs.com/yjbjingcha/p/7040290.html.
[5] madao756. 段页式访存——逻辑地址到线性地址的转换. https://www.jianshu.com/p/fd2611cc808e.
[6] madao756. 段页式访存——线性地址到物理地址的转换. https://www.jianshu.com/p/c78cdf6214b5.
[7] Pianistx. printf 函数实现的深入剖析. https://www.cnblogs.com/pianist/p/3315801.html

程序人生-Hello’s P2P(哈工大计算机系统大作业)相关推荐

  1. 程序人生-Hello’s P2P(CSAPP大作业)

    摘  要 本文介绍了程序Hello的一生.本文通过对Hello在Linux下的预处理.编译.汇编.链接等过程进行分析,详细讲解了一个程序由诞生到执行再到消亡的典型过程.虽然程序执行的过程在程序员眼中只 ...

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

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

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

    哈工大计算机系统大作业 摘要 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的概念与作用 2.2 在Ubuntu下预处理的 ...

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

    2022哈工大计算机系统大作业 目录 摘 要 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的概念与作用 2.2在Ubun ...

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

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

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

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

  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. 哈工大计算机系统大作业-程序人生-Hello’s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机科学与技术 学    号 2021110802 班    级 21w0312 学       生 黄键树 ...

最新文章

  1. 接口隔离原则_设计模式六大原则
  2. DPVS_DPVS配置说明
  3. boost::random_spanning_tree用法的测试程序
  4. vertx.FileResolver文件解析
  5. P4123-[CQOI2016]不同的最小割【网络流,分治】
  6. TCP send 阻塞与非阻塞
  7. git 远程代码回滚master
  8. Android-JNI开发系列《七》补充jni与java的数据类型的对应关系和数据类型描述符
  9. Cornerstone 4 for Mac(SVN管理工具)
  10. 查看dll是32还是64
  11. 如何理解数列极限和收敛性
  12. 1013_MISRA C规范学习笔记9
  13. java-阴历日期和阳历日期互相转换
  14. RT-Thread系统 STM32 DAC设备改进,直接调用系统DAC驱动函数设置输出电压
  15. GX works2 三菱PLC 显示注释后代码行变宽的解决方法
  16. 2020年12月计算机一级考试,5省市已公布2020年12月计算机等级考试时间,切勿错过!...
  17. Unity API通读 CustomEditor
  18. SD NAND flash使用说明
  19. 接口测试 | 接口测试入门
  20. 马克思主义与社会科学方法论

热门文章

  1. Unity(十四)Unity配置打包APK环境Android和Java
  2. Java——Lambda表达式
  3. Unity(三十九):非运行状态下脚本播放动画、Animator Override Controller、RuntimeAnimatorController
  4. 【动态规划】0-1背包递推式的剖析(通俗易懂)
  5. 智力答题查询器,适用于新英雄年代和征途
  6. 饭局上领导劝你喝酒,别说“我不能喝”,高手都用这4种拒酒话术
  7. mysql yum包,如何使用MySQL yum源来安装更新MySQL相关软件包
  8. 分布式系列三: 对象序列化
  9. imindmap之云朵技巧
  10. ABAP权限对象设计与权限检查的实现