CSAPP-大作业

啦啦啦,终于搞完了

如下:

 

 

 

 

计算机系统

 

大作业

 

题     目  程序人生-Hello’s P2P 

专       业         计算机           

学     号               

班     级               

学       生              

指 导 教 师                   

 

 

 

 

 

 

计算机科学与技术学院

2019年12月

摘  要

Hello,一个十分简单的程序,每个学编程的人都曾照着课本懵懵懂懂的写过。但是hello这个程序又不那么简单,从诞生到最后清除,hello也是经历过“大风大浪”的,从预处理到编译,又从编译到汇编,再链接……hello经历了每个程序都会经历的一切。从hello程序出发,触摸程序在计算机当中的脉搏,感受它精彩而又普通的一生。

关键词:计算机系统;hello程序;linux;编译;汇编。                           

 

 

 

 

 

 


 

目  录

 

第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的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

P2P: From Program to Process,也就是从一个程序变成一个进程的过程。我们通过高级语言(C语言)创建一个C程序即hello.c,hello.c再通过GCC编译系统驱动程序经过cpp预处理器,ccl编译器,as汇编器,ld连接器,就被成功地翻译成了一个可执行文件hello。并且被放在了磁盘上。要想在系统上执行该可执行文件,我们通过在shell中输入./hello,系统(OS)就为hello fork()一个子进程。这样就完成了从程序变成进程地操作。

O2O:from zero to zero,shell为hello生成子进程,再用execve()加载程序,创建虚拟内存空间,将数据加载到虚拟内存空间中去,内核为程序分配时间片执行逻辑控制流,直至程序结束。Shell回收该进程,清空该程序所占的空间,删除有关的数据结构,抹去所有痕迹。也就完成了O2O的过程。

1.2 环境与工具

硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk

软件环境:Windows10 64位;Vmware 15;Ubuntu 16.04 LTS 64位

工具:GCC,EDB,GDB,Dev-C++

1.3 中间结果

hello.c

源程序

hello.i

hello.c 预处理后产生的文件

hello.s

hello.c 预处理后产生的文件

hello.o

hello.s 汇编后的可重定位目标文件

hello

hello.o 经过链接后的可执行目标文件

hellooelf.txt

hello.o 的 ELF 格式文件

hhelloelf.txt

hello 的 ELF 格式文件

fhelloo.txt

hello.o 的反汇编代码文件

fhhello.txt

hello 的反汇编代码文件

1.4 本章小结

本章介绍了Hello的P2P过程、020过程,详细介绍了完成本论文的所用的软件环境、硬件环境、开发与调试工具,罗列出了完成本论文的中间结果

 


第2章 预处理

2.1 预处理的概念与作用

预处理的概念:预处理器(cpp)根据以字符#开头的语句,修改原始的C程序。

预处理的作用:

1.将源文件中以”include”格式包含的文件复制到编译的源文件中。

2.用实际值替换用“#define”定义的字符串。

3.根据“#if”后面的条件决定需要编译的代码。

合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

2.2在Ubuntu下预处理的命令

使用命令gcc -E hello.c -o hello.i就可以生成预处理完的文件hello.i

                          图2.1   预处理命令

2.3 Hello的预处理结果解析

图2.2  .i文件内容

从文本中可得经过预处理我们的hello.c变成了特别长的hello.i文件,其中有许多看不懂的代码,但可知都是C语言的代码,只在最后显示了我们本来的代码。根据预处理的功能不难推断出前面大部分的代码应该是处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。如hello.c中的#include<stdio.h>,#include<unistd.h>,#include<stdlib.h>。

              图2.3   原代码部分

2.4 本章小结

本章介绍了预处理的概念和功能,展示了hello.c变成hello.i的过程大致hello.i的大致内容。


第3章 编译

3.1 编译的概念与作用

编译的概念:利用编译程序将原高级语言程序通过词法分析,语法分析,语义检查和中间代码生成,代码优化等翻译成目标程序的汇编格式。

汇编的作用:通过产生汇编语言的方式,能使代码面向机器并较好的发挥机器的特性,并且能够让代码已更高效地效率执行。

3.2 在Ubuntu下编译的命令

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

                          图3.1   编译指令

3.3 Hello的编译结果解析

3.3.1数据:首先我们在C语言程序中可看到总共有以下三个数据

      1)int sleepsecs=2.5

      在程序中,sleepsecs被定义为全局变量,object类型,大小为4字节,并且存放在.rodata节中,见图3.2。

                   图3.2  关于变量sleepsecs的声明

并且在函数中被显式地引用,见图3.3。

                 图3.3 sleepsecs变量的引用

      2)int argc;

    argc是函数main()的参数,表示argv[]数组中非空指针的数量。在函数中存放在栈中%rbp – 0x20的位置,见图3.4。

                  图3.4  int argc

3)int i;

int i是main函数定义的一个局部变量,存放在栈中%rbp -0x4的位置,见图3.5。

              图3.5 int i

3.3.2赋值

      赋值对应的语句有两处,见图3.6。

      

                     图3.6 对i,sleepsecs的赋值

3.3.3类型转换

     程序中sleepsecs发生了类型转换,并且是隐式类型转换。将浮点数2.5向偶数舍入变成2。

3.3.4 算术操作

      有一个i++的操作,对应图3.7中代码。

   

                        图3.7   i的加法操作

3.3.5 关系操作

       比较argc与3的关系和比较i与9的大小,见图3.8。

                       图3.8   argc!=3 与 i<10

3.3.6 数组/指针/结构操作

  char* argv[],其中%rax = %rbp – 0x32存放argv的地址,然后(%rax + 16)就是argv[2],而(%rax + 8)就是argv[1],具体代码见图3.9。

                    图3.9 数组操作

3.3.7 控制操作

      有三个控制操作:1.比较argc与3,如果相等,执行.L2代码。2.比较i和9的大小,如果相等跳出循环,否则继续循环。3.直接跳入执行.L3代码。

                  图3.10 控制操作

3.3.8 函数操作

看似程序只使用了C函数,其实还调用了许多其他的函数,1.printf(),通过call指令,调用puts@PLT。2.exit(),通过call指令,调用exit@PLT。3.sleep(),通过call指令,调用sleep@PLT。4.getchar(),通过call指令,调用getchar@PLT。

 在hello中,第一个参数被保存在%edi,第二个参数被保存在%rsi中,被调用的函数通常将返回值保存在寄存器%eax中。

           图3.10   函数操作

3.4 本章小结

      本章介绍了hello.i文件编译的过程,解析了hello.s中的内容,阐述了与.c文件中C语句的对应关系。


第4章 汇编

4.1 汇编的概念与作用

汇编的概念:把汇编语言书写的程序翻译成与之等价的机器语言程序的翻译程序。

汇编的作用:汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序,使得程序更容易理解。

4.2 在Ubuntu下汇编的命令

采用的汇编的命令为gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o

                                                        图4.1 汇编命令

4.3 可重定位目标elf格式

1.ELF头

                                                       图4.2  ELF头

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

2.节头部表

                                                图4.3  节头部表

节头部表描述了不同节的大小,类型,名字,地址和偏移量等信息。

3.符号表

                                                       图4.4  符号表

4..data和.rodata内容

                                         图4.5  .data和.rodata内容

data 节是已初始化的全局和静态 C 变量。

.rodata 节是只读数据。如 printf 语句中的格式串和开关语句的跳转表。

5.重定位信息

                           图4.6  重定位节.rela.text和.rela.eh_frame

我们一项一项的来看:

1.偏移量:偏移量即变量相对.text的偏移量。例如sleepsecs,偏移量为0x60,所以引用sleepsecs的运行时地址为ADDR(.text)+offset。

2.信息:信息分为两部分即Typr和Symbol,还是以sleepsecs为例,Type为0x02, 即 R_X86_64_PC32 类型, Symbol为 0x9,与符号表(图.4.4)对应。如果Type为0x04,则就是R_X86_64_PLT32。

3.加数:我们在计算重定位时,会使用公式 *refptr = (unsigned)(ADDR(r.symbol) + addend – refaddr),其中addend就是这里的加数。根据其他地址,我们可以推出:*refptr = 0x601044+(-4)–(0x400592)= 0x200aae。

4.4 Hello.o的结果解析

                                                图4.7 hello.o反汇编文件

1) 机器语言的构成:

      机器语言是用二进制代码表示的计算机能直接识别和执行的一种机器

指令的集合。一条指令就是机器语言的一个语句,它是一组有意义的二进

制代码,指令的基本格式如,操作码字段和地址码字段,其中操作码指明

了指令的操作性质及功能,地址码则给出了操作数或操作数的地址。

2)与汇编语言的映射关系:

      汇编指令是机器指令便于记忆的书写格式。每行的操作码都与其后的

汇编指令一一对应。

3)操作数:

      反汇编代码中的立即数是十六进制数,而 hello.s 文件中的数是十进制的。

      寄存器寻址两者相同。内存引用 hello.s 中会用伪指令(如.LC0)代替,而反汇编则是基址加偏移量寻址:0x0(%rip)。

4)条件分支:

      在hello.s文件中,所有的跳转指令后都会接.Lx一类的段名称,但在反汇编操作得到的汇编代码中,跳转指令后只会接对应的地址。

5)函数调用:

      在hello.s中,用call指令进行调用函数时,总会在call指令后直接加上函数名,而在反汇编得到的汇编代码中,call指令后会跟着的是下一条指令的地址(一般以main地址加偏移量的形式给出)。

4.5 本章小结

本章介绍了将hello.s变成hello.o的过程,分析了ELF格式的各部分内容和意义,比较了.o文件反汇编代码与.s文件的不同之处。


第5章 链接

5.1 链接的概念与作用

链接的概念:链接是把各种代码和数据片段链接到一块的一个过程。这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时。也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。在早期的计算机系统中,链接是手动执行的。在现代系统中,链接是由叫做链接器的程序自动执行的。

链接的作用:链接的出现使得我们可以分离编译,在软件开发中起到重要作用。

5.2 在Ubuntu下链接的命令

命令如图5.1

                                                 图5.1  链接命令

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

使用readelf -a命令列出各段信息:

1.ELF头

                                         图5.2 ELF头

2.节头部表

                                         图5.3 节头部表

3.程序头

                                         图5.4    程序头

4.段节

                                                图5.5   段节

5. Dynamic section

                           图5.6  Dynamic section

6.重定位节

                                        图5.7  重定位节

7.符号表

                                           图5.8  符号表

5.4 hello的虚拟地址空间

 

                           图5.9  edb中Data Dump窗口

根据5.4程序头部表,可以看到根据可执行文件目标文件的内容初始化两个内存块。

第一个LOAD告诉我们这个(代码段)段有读/执行权限,开始于内存 0x400000 处,总共的内存大小是 0x81c,并且被初始化为可执行目标文件的头 0x81c 个字节,其中包括 ELF 头,程序头部表以及.init、.text 和.rodata 节。这段的地址是 0x400000-0x40081c。

第二个LOAD告诉我们二个段(数据段)有读/写访问权限,开始于 0x600e00 处,总的内存大小是 0x258 字节,并从目标文件中偏移 0xe00 处开始的.data 节中的 0x254 个字节初始化。该段剩下的 4 个字节对应于运行时将被初始化为 0 的.bss 数据。这个段对应地址是 0x600e00-0x601058。如图5.10

                           图5.10 数据段

然后的DYNAMIC段,描述了一些动态内存的信息。起始地址是 0x600e00,内存大小共 0x1e0 字节,终止地址是 0x600fe0。

5.5 链接的重定位过程分析

命令objdump -d -r hello

                                         图5.11  可执行文件的反汇编

这样出来的文件会比hello.o的不同,hello.o中只有.text节,而这里有.init,.plt,.finl。其中.init节包含进程初始化时要执行的程序指令,.plt节包含函数连接表,.fiinl节包含进程终止时要执行的程序指令。

链接的过程:

1. 空间与地址分配,扫描所有的输入目标文件,获得它们的各个段的长度、属性和位置,并且将这些输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。再将它们合并。

2. 符号解析与重定位。使用上一步收集到的所有信息,读取输入目标文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。

结合 hello.o 的重定位项目,分析 hello 中对其怎么重定位的。 书中描述的重定位算法如图 5.12。

                                  图5.12  关于重定位算法

以 sleepsces 为例。

1) 引用 sleepsecs 的运行时地址是 refaddr = ADDR(s)+offset = 0x400592;

2) 该条目是 R_X86_64_PC32 类型,是重定位 PC 相对引用。

3)链接器会确定 ADDR(r.symbol) = ADDR(sleepsces) = 0x601044。所以 *refptr=(unsigned)(ADDR(r.symbol)+addend– refaddr)=0x601044+(-4)–(0x400592)= 0x200aae. 在运行时,PC+*refptr = 0x400596+0x200aae = 0x601044. 就确定了 sleepsecs 的位置。

                     图5.13 sleepsecs的重定位

5.6 hello的执行流程

                                                图5.14   edb执行hello

调用的子程序及其地址:

子程序名

程序地址

_init

0x400488

.plt

0x4004a0

puts@plt

0x4004b0

printf@plt

0x4004c0

getchar@plt

0x4004d0

exit@plt

0x4004e0

sleep@plt

0x4004f0

__libc_csu_init

0x4005c0

__libc_csu_fini

0x400630

_fini

0x400634

5.7 Hello的动态链接分析

通过edb调试,找到dl_init的位置,并在前后设置断点,如图5.15所示

                                                        图5.15  在 dl_init的前后位置设置断点

在hello的反汇编代码里面找到_GLOBAL_OFFSET_TABLE(全局偏移表)的地址,见图5.16

                                                 图5.16  全局偏移表的地址

在执行_dl_init之前,_GLOBAL_OFFSET_TABLE的值,见图5.17

                                          图5.17   全局偏移表的地址1

在执行_dl_init之后,_GLOBAL_OFFSET_TABLE的值,见图5.18

                                          图5.18   全局偏移表的地址2

通过前后对比:在执行_dl_init之前,_GLOBAL_OFFSET_TABLE(图5.17黑框所示)是全0的;在执行_dl_init之后,_GLOBAL_OFFSET_TABLE(图5.18黑框所示)变成了相应的值。_dl_init作用是:初始化程序,给其赋上调用的函数的地址,使这些被调用的函数链接到了动态库。

5.8 本章小结

       本章介绍了链接的概念与内容,分析了hello的elf格式,查看了hello的虚拟地址空间,对链接的重定位过程进行了分析,分析了hello的执行流程和动态链接分析。

第6章 hello进程管理

6.1 进程的概念与作用

进程的概念:进程的经典定义就是一个执行中程序的实例。

进程的作用:在现代系统上运行一个程序时,我们会得到一个假象,就好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条地执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。这些都是进程带给我们的。

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

       为了让进程接收到用户的操作,但是由于许多原因,用户并不能够直接接触内核,所以通过Shell这个程序来接收用户的命令,这个既简化了用户的操作,又保护了内核。

       Shell的作用:调用其他程序,并且获取其处理结果;在多个程序之间传递参数;被其他程序调用。

       Shell的处理流程:

       1. 解析输入的命令行参数;

2. 该命令是内置的 shell 命令则马上解释该命令;

3. 若该命令是一个可执行目标文件,会在一个新的子进程的上下文中 execve并运行这个文件。

4. 运行过程中,shell 还会处理异常,如从键盘输入 Ctrl+C 终止子进程;

5. 进程终止后,shell 负责回收该子进程。

6.3 Hello的fork进程创建过程

当我们在shell中键入./hello XXXXXXXXXX 的时候,有shell的流程我们可以知道,shell会先判断是不是内部指令,发现不是之后,会运行hello程序,并通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。

6.4 Hello的execve过程

fork 创建新的子进程后,内核调用 execve 函数在子进程中加载并运行包含在

可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。加载并运

行 hello 需要以下几个步骤:

1) 删除已存在的用户区城。删除当前进程虚拟地址的用户部分中的已存

在的区域结构。

2) 映射私有区城。为新程序的代码、数据、bss 和栈区域创建新的区域结

构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射

为 hello 文件中的.text 和.data 区。bss 区域是请求二进制零的,映射到匿名

文件,其大小包含在 hello 中。栈和堆区域也是请求二进制零的,初始长

度为零。图 6.3 概括了私有区域的不同映射。

3) 映射共享区城。如果 hello 程序与共享对象(或目标)链接,比如标准

C 库 libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用

户虚拟地址空间中的共享区域内。

4) 设置程序计数器(PC)。execve 做的最后一件事情就是设置当前进程

上下文中的程序计数器,使之指向代码区域的入口点,也就是_start 函数

的地址。下一次调度这个进程时,它将从这个入口点开始执行。Linux 将根据需要换入代码和数据页面。_start 函数调用系统启动该函数__libc_start_main。它初始化执行环境,调用用户层的 main 函数,处理 main 函数的返回值,并且在

需要的时候把控制返回到内核。

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

上下文信息:内核为每个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程己打开文件的信息的文件表。

进程时间片:分时操作系统分配给每个正在运行的进程微观上的一段CPU时间。在抢占内核中,指的是从进程开始运行到被强占的时间。

调度的过程:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。

以执行sleep函数为例,sleep函数请求调用休眠进程,sleep将内核抢占,进入倒计时,当倒计时结束后,hello程序重新抢占内核,继续执行,其过程如下图6.1所示。

                                         图6.1 进程上下文的切换

6.6 hello的异常与信号处理

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

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

异常的类别:

                                         图6.2 异常类型

信号的类别:

                                                图6.3  信号类型

1.先正常运行:

                                                              图6.4  正常运行

2.运行中输入当打印了两行时,如果键入 Ctrl+C,内核会发送一个 SIGINT 信号给前台进程 组中的每个进程,终止前台作业,即终止 hello 进程。然后 shell 会回收该进程。如图 6.5。

                                  图6.5  键入 Ctrl+C

3. 键入 Ctrl+Z。会发送一个 SIGTSTP 信号到前台进程组中的每个

进程,挂起前台作业,即挂起进程 hello。使用 jobs 命令查看前台进程;使用 pstree 查看 hello 进程所在位置, 如图 6.7;使用 fg 命令,让 hello 程序在前台运行,如图 6.8;使用 kill 命令杀死 hello 进程,shell 回收该进程,如图 6.9.

                                  图6.6  键入 Ctrl+Z

             图6.7 使用pstree指令查看hello进程所在位置

.

 图6.8 使用 fg 命令,使 hello 程序在前台运行

图 6.9 使用 kill 指令杀死 hello 进程

图6.10  乱按的情况

6.7本章小结

本章我们详细分析了 hello 程序是如何被 fork、execve 和执行的,并探讨了 hello进程运行过程中可能的异常和信号处理。


第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:是指在计算机体系结构中是指应用程序角度看到的内存单元(memory cell)、存储单元(storage element)、网络主机(network host)的地址。是由程序产生的关于段偏移地址。

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

虚拟地址:程序访问存储器所使用的逻辑地址称为虚拟地址。

物理地址:计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组,每一个字节单元给以一个唯一的存储器地址,称为物理地址。

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

一个逻辑地址是由段选择符和偏移地址组成的。段选择符存放在段寄存器(16 位)。前 13 位是索引号,后面 3 位包含一些硬件细节。如图7.1

                   图7.1  段选择符各字段含义

段描述符是一种数据结构,实际上就是段表项,分两类:1.用户的代码段和数据描述符,2.系统控制段描述符,包括特殊系统控制段描述符和控制转移类描述符。描述符表实际上就是段表,有段描述符组成,有三种类型:1.全局描述符表GDT,2.局部描述符表LDT,3.中断描述符表IDT。

过程:给定一个完整的 48 位逻辑地址[段选择符(16 位):段内偏 移地址(32 位)],1、看段选择符的T1=0还是1,是0要转换 GDT 中的段,是1转换 LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。2. 拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,就得到了32位段基地址。3、把32位段基地址和32位段内偏移量相加,就是要转换的线性地址了。

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

7.3.1 Intel的页式管理页式

内存管理单元负责把一个线性地址转换为物理地址。从管理和效率的角度出发,

线性地址被划分成固定长度单位的数组,称为页(page)。例如,一个32位的机器,线性地址可以达到4G,每页4KB,这样,整个线性地址就被划分为2^20页,称之为页表,该页表中每一项存储的都是物理页的基地址为了能够尽可能的节约内存,CPU在页式内存管理方式中引入了两级的页表结构,这种页式管理方式中,第一级的页表称之为“页目录”,用于存放页表的基地址;第二级才是真正的“页表”用于存放物理内存的基地址。

7.3.2 Hello的线性地址到物理地址的变换

①从CR3中取出进程的页目录的地址(操作系统在负责进程的调度的时候,将这个地址装入对应的CR3地址寄存器),取出其前20位,得到页目录的基地址;

②根据上述基地址以及线性地址的前10位,进行组合得到线性地址的前十位的索引对应的项在页目录中地址,根据该地址可以取到该地址上的值,该值就是二级页表项的基地址

③根据②取到的页表项的基址,取其前20位,将线性地址的10-19位左移2位,按照与②相同的方式进行组合就可以得到线性地址对应的物理页框在内存中的地址在二级页表中的地址的起始地址,根据该地址向后读四个字节就得到了线性地址对应的物理页框在内存中的地址在二级页表中的地址,然后取该地址上的值就得到线性地址对应的物理页框在内存中的基地址;

④根据③取到的基地址,取其前20位得到物理页框在内存中的基址,再根据线性地址最后的12位的偏移量得到具体的物理地址,取该地址上的值就是最后要得到值(博客);

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

       TLB 是一个小的、虚拟寻址的缓存,其中每一行都保留着一个由单个 PTE 组

成的块。在 Core i7 中,TLB 索引有4位,TLB 标记有32位。故TLB有16组,并且每组4个条目。

       如图7.2,TLB 与四级页表支持下的 VA 到 PA 的变换过程:

1. CPU 产生一个 48 位虚拟地址

2. MMU 从虚拟地址中抽取 36 位的 VPN,并且检查 TLB,看它是否因为前

面某个内存引用缓存了 PTE 的一个副本。TLB 从 VPN 中抽取 TLB 索引和

TLB 标记,TLB 索引选择组,TLB 标记匹配每一行的标记位,看能否找到

有效的匹配。如果命中就将缓存的 PPN 返回给 MMU;

3. 如果 TLB 不命中,MMU 就需要从主存中取出相应的 PTE。36 位 VPN被

划分为 4 个 9 位的片,每个片被作用到一个页表的偏移量。CR3 控制寄存器指向第一级页表 L1 PET 的起始位置。VPN1 提供一个到 L1 PTE 的偏移量,这个 PTE 包含 L2 页表的基地址。VPN2 提供一个 L2 PTE 的偏移量,以此类推。直到找到 L4 中的指定 PTE。

4. 如果得到的 PTE 是无效的,就产生一个缺页,内核必须调入合适的页面,

重新运行刚才的指令。

5. 最后 MMU 得到了来自PTE的PPN 和来自虚拟地址的VPO,把它们连接,

就形成了物理地址。

                                                 图7.2   Core i7 地址翻译

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

1.先在第一级缓存中寻找要找的数据,利用TLBI找到TLB中对应的组,再比较TLBT,若相同且有效为为1,则要找的数据就是该组的数据。

2.否则,在第二级缓存中寻找,找到后需要再将其缓存在第一级,若有空闲块,则放置在空闲块中,否则根据替换策略选择牺牲块。

3.否则,在第三级缓存中寻找,找到后需要缓存在第一,二级,若有空闲块,则放置在空闲块中,否则根据替换策略选择牺牲块。

4. 否则,在第四级缓存中寻找,找到后需要缓存在第一,二,三级,若有空闲块,则放置在空闲块中,否则根据替换策略选择牺牲块。

7.6 hello进程fork时的内存映射

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

                                         图7.3  Linux组织虚拟内存

7.7 hello进程execve时的内存映射

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

                                  图7.4   加载器映射用户空间区域

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

缺页故障:在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页。例如:CPU引用了VP3中的一个字,VP3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE3,从有效位推断出VP3未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4。如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4的页表条目,反映出VP4不再缓存在主存中这一事实。(见书564页)

缺页中断处理:接下来,内核从磁盘复制VP3到内存中的PP3,更新PTE3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但是现在VP3已经缓存在主存中了,那么也命中也能由地址翻译硬件正常处理了。

7.9动态存储分配管理

当程序运行时,如果需要额外的虚拟内存时,可以用动态内存分配器来申请

内存。

动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap),假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量 brk,它指向堆的顶部。

分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个

连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地

保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被

应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是

应用程序显式执行的,要么是内存分配器自身隐式执行的。

        图7.5     堆

隐式分配器的两种实现原理:

1. 带边界标签的隐式空闲链表分配器原理。

任何实际的分配器都需要一些数据结构,允许它来区别块边界,以及区别已分配快和空闲块。可以将这些信息嵌入块本身。如图7.6中a。但是,这种只带头部的隐式空闲链表结构会使得合并前面块耗费大量时间,可以采用带标签的隐式空闲链表。

由 Knuth 提出的边界标记的技术,允许在常数时间内进行对前面块的合并。 在每个块的结尾添加一个脚部,其中脚部就是头部的一个副本。如果每个块包括 这样的一个脚部,那么分配器就可以通过检查它的脚部,判断前一个块的起始位 置和状态,这个脚部总是在距当前块开始位置一个字的距离。

2. 显式空间链表的基本原理

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

                                          图7.6  显式

7.10本章小结

       本章我们了解了各种地址的含义,。分析了 Intel CPU 的段式管理和页式管理,

解析了在 linux 系统下处理器产生的虚拟地址是如何被翻译成物理地址的,以及如何根据物理地址访问 cache 和内存,还介绍了 linux 系统虚拟内存的组织和异常处理,最后简单了解了系统的动态内存分配。


第8章 hello的IO管理

8.1 Linux的IO设备管理方法

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

8.2 简述Unix IO接口及其函数

8.2.1 I/O接口操作

①打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。

②Linux shell创建的每个进程开始时都有三个打开的文件:标准输入、标准输出和标准错误。

③改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0.这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。

④读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

⑤关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。(见书P622~P623)

8.2.2 I/O函数

①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函数来执行输出。write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。(见书P624~P626)

8.3 printf的实现分析

      

                     图8.1   printf函数的实现

                     图8.2   vsprintf函数的实现

        图8.3 write函数的汇编代码

          图8.4    sys_call的实现

vsprintf接受确定输出格式的格式字符串fmt,产生格式化输出,并返回要打印的字符串的长度。到write系统函数先给寄存器传参数,然后调用sys_call,sys_call将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

                  图8.5  getchar函数的实现

异步异常-键盘中断的处理:当用户按键时,会产生一个 SIGINT 信号,键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。(回车字符也放在缓冲区中)。当用户键入回车之后,getchar 才开始从 stdio 流中每次读入一个字符。getchar 函数的返回值是用户输入的字符的ASCII 码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续 getchar 调用读取。也就是说,后续的 getchar 调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才等待用户按键。

8.5本章小结

       本章详细描述了Linux的I/O设备管理机制,Unix I/O接口及函数,分析了printf函数和getchar函数的实现。

结论

       Hello的一生的重大事件:

1.用户通过高级语言编写代码保存至hello.c中,并且用GCC编译器驱动程序读取源文件。

2.预处理器(cpp)对hello.c进行预处理,变成hello.i;

3.编译器(ccl)对hello.i进行翻译,变成文本文件hello.s,属于汇编语言程序。

4.汇编器(as)对hello.s进行翻译,变成机器语言指令,再将其打包成可重定位目标文件,保存至目标文件hello.o中。

5.链接器(ld),静态链接将外部代码添加至可执行文件中;动态链接设置过程连接表PLT和全局偏移量表GOT,在运行时引用相关代码。最后生成hello可执行目标文件。

6.用户输入./hello…系统通过fork,execve等加载运行hello,运行完后,有回收这个进程,然后从系统中删除他的所有痕迹。


附件

hello.c

源程序

hello.i

hello.c 预处理后产生的文件

hello.s

hello.c 预处理后产生的文件

hello.o

hello.s 汇编后的可重定位目标文件

hello

hello.o 经过链接后的可执行目标文件

hellooelf.txt

hello.o 的 ELF 格式文件

hhelloelf.txt

hello 的 ELF 格式文件

fhelloo.txt

hello.o 的反汇编代码文件

fhhello.txt

hello 的反汇编代码文件


参考文献

为完成本次大作业你翻阅的书籍与网站等

[1]  Randal E. Bryant & David R. O’Hallaron. 深入理解计算机系统[M]. 北京:机械工业出版社,2019.

[2]  博客:https://blog.csdn.net/qq_28849009/article/details/103697334

[3]   printf 函数实现的深入剖析https://www.cnblogs.com/pianist/p/3315801.html

[4]  链接命令:https://www.cnblogs.com/zhai1997/p/11790051.html

[5]  ELF: https://www.jianshu.com/p/aa3f35a15782

[6]  博客:https://www.shuzhiduo.com/A/qVdeYA38dP/

[7]  地址有关:https://blog.csdn.net/do2jiang/article/details/4512417

[8]  百度百科.

猜你喜欢

转载自www.cnblogs.com/hitd1/p/12115305.html
今日推荐