程序人生-Hello’s P2P

 

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业       计算学部                

学     号        120L021131        

班     级         2003001     

学       生         武子茜     

指 导 教 师          史先俊       

计算机科学与技术学院

2022年5月

摘  要

本文以Hello程序为重点,解释了Linux上Hello程序的生命周期,分析了从hello.c源文件到预处理、编译、构建、链接、执行和终止的整个过程,并结合课程中获得的知识描述了Linux操作系统如何管理Hello程序、存储和I/O。 整个文本涵盖了计算机系统课程的主要骨架,对课程中所学的主要知识点进行了分类和总结。

关键词:操作系统;计算机系统课程

目  录

第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简介

  1. P2P

P2P是指from program to process,即从程序到进程。在Linux系统中,源代码文件hello.c(program)先经cpp预处理生成文本文件hello.i,hello.i经cc1编译生成汇编文件hello.s,hello.s经ld链接生成可执行程序文件hello。最后在shell中键入命令(./hello)后,操作系统(OS)的进程管理为其fork创建子进程 (process)。

  1. 020

020是指from zero to zero。Hello程序执行前,不占用内存空间(第一个0)。P2P过程后,子进程首先调用execve,依次进行虚拟内存映射、物理内存载入;随后进入主函数执行程序代码,程序调用各种系统函数实现屏幕输出信息等功能;最终程序结束,shell父进程回收此子进程,其相关的所有内存中的状态信息和数据结构被清除(第二个0)。以上即为020的全部过程。

1.2 环境与工具

硬件环境:X64 CPU2GHz8G RAM256GHD Disk

软件环境:Windows11 64位;Vmware 14Ubuntu 16.04 LTS 64位;

开发与调试工具:gcc, readelf, objdump, edb, ld, gedit

1.3 中间结果

文件名称

说明

hello.i

hello.c经预处理得到的ASCII文本文件

hello.s

hello.i经编译得到的汇编代码ASCII文本文件

hello.o

hello.s经汇编得到的可重定位目标文件

hello_elf.txt

hello.o经readelf分析得到的文本文件

hello_dis.txt

hello.o经objdump反汇编得到的文本文件

hello

hello.o经链接得到的可执行文件

hello1_elf.txt

hello经readelf分析得到的文本文件

hello1_dis.txt

hello经objdump反汇编得到的文本文件

1.4 本章小结

本章首先以Hello程序为例,简要解释了P2P、O2O的概念,随后提供了我使用的环境及工具的相关信息,最后以表格形式介绍了完成本论文过程中用到的所有中间文件。

第2章 预处理

2.1 预处理的概念与作用

  1. 预处理的概念

预处理是在编译前进行的处理,此过程中会扫描源代码,检查包含预处理指令的语句和宏定义并进行相应的替换,还会删除程序中的注释和多余的空白字符等,最终产生调整后的源代码提供给编译器。

  1. 预处理的作用

预处理能够实现文件包含、条件编译、布局控制和宏替换等功能,使得程序更完备,为程序的编译过程做好准备。

2.2在Ubuntu下预处理的命令

Ubuntu的命令行下输入指令:

gcc -E hello.c -o hello.i

则当前目录下会生成文件hello.i

 

图2- 1

图2- 2

 

    1. Hello的预处理结果解析

首先是源代码文件的一些相关信息

图2- 3

 

然后是预处理的扩展内容

图2- 4(部分截图)

 

Stdio.h是标准库文件,预处理把以#include指定的三个文件的源代码进行递归展开替换,最终的hello.i文件删除并替换原有的这部分内容,剩下的代码照搬。

2.4 本章小结

本章主要探讨了预处理的概念、作用和命令,分析了hello.c源代码文件的预处理过程和结果。C语言预处理一般由预处理器进行,主要完成四项工作:宏展开、文件包含复制、条件编译处理和删除注释及多余空白字符,为之后的编译等流程奠定了基础。

第3章 编译

3.1 编译的概念与作用

  1. 概念

编译是指对经过预处理之后的源程序代码进行分析检查,确认所有语句均符合语法规则后将其翻译成等价的中间代码或汇编代码(assembly code)的过程。在此处指编译器将hello.i翻译成hello.s

  1. 作用

要包括词法分析、语法分析、语法制导翻译、中间代码生成、运行存储分配、代码优化等基本流程。

3.2 在Ubuntu下编译的命令

在Ubuntu的命令行下输入指令:

gcc -S hello.i -o hello.s

则当前目录下会生成文件hello.s

 

图3- 1

 

图3- 2

3.3 Hello的编译结果解析

  1. 数据
    1. 常量

printf函数中用到的格式字符串、输出字符串被保存在.rodata段。

图3- 3

 

if条件判断值在.text段,运行时使用。

 

图3- 4

    1. 变量
      1. 局部变量
        1. I作为局部变量,且为整型数据存储在栈中。

 

图3- 5

    1. 参数
      1. Main函数的两个参数,argc(整型)和 argc(字符数组)。程序开始时前者传到%edi,后者传到%rsi

 

图3- 6

  1. 赋值

i= 0赋值mov*,由于数据类型不同有movb:一个字节;movw:两个字节;movl:四个字节;movq:八个字节。

 

图3- 7

  1. 算术操作

在for循环体中,对循环变量i的更新使用了++自增运算,汇编代码翻译成addl指令(4字节int型对应后缀“l”)。

 

图3- 8

  1. 关系操作和控制转移
    1. 程序中if条件判断处的关系操作与控制转移:

 

图3- 9

             je使用cmpl设置的条件码(ZF),若ZF = 0,说明argc等于4,控制转移至.L2(for循环部分,程序主体功能);若ZF = 1,说明argc不等于3(即执行程序时传入的参数个数不符合要求),继续执行输出提示信息并退出。

    1. 程序中for循环终止条件判断涉及的关系操作与控制转移

 

图3- 10

与if类似,此处jle使用cmpl设置的条件码(ZF SF OF),若(SF^OF) | ZF = 1,说明循环终止条件不成立(变量i的值小于或等于7),控制转移至.L4,继续执行循环体;若(SF^OF) | ZF = 0,则循环终止条件成立(变量i的值达到8),不再跳转至循环体开始位置,继续向后执行直至退出。

  1. 函数
  1. Main
    1. 参数传递:int argc, char *argv[]

相关汇编代码:

 

图3- 11

由此可见,第一个参数通过寄存器EDI传递,第二个参数通过寄存器RSI传递,这一步将两个参数写入栈空间。

    1. 函数调用:

被启动函数调用,hello.s中没有体现,但为汇编器进行相关处理提供了信息。

相关汇编代码:

图3- 12

 

此部分汇编指令标记了程序入口等信息,应该是提供给汇编器。

    1. 函数返回:正常情况返回0,参数个数不正确返回1。

正常情况相关汇编代码:

 

图3- 13

返回1情况(调用exit()函数,第34、35行):

图3- 14

 

  1. Printf 参数
    1. 参数传递:需要输出的字符串

 

图3- 15

 

图3- 16

    1. 函数调用:主函数通过call指令调用。
    2. 函数返回:返回值被忽略。
  1. Exit
    1. 参数传递:退出状态值(int类型)

 

图3- 17

    1. 函数调用:主函数通过call指令调用。
    2. 函数返回:函数不返回,直接退出程序。
  1. atoi
    1. 参数传递:传入参数argv[3],用基址+24来确定地址。

 

图3- 18

    1. 函数调用:for循环中被调用。
    2. 函数返回;返回值作为sleep函数的参数。
  1. Getchar
    1. 参数传递:无。
    2. 函数调用:主函数通过call指令调用,相关汇编代码如下(第57行):

 

图3- 19

    1. 函数返回:返回char类型值,在此程序中被忽略。

3.4 本章小结

本章介绍了编译的概念、编译的流程,并根据编译器处理hello.i文件得到hello.s文件的过程,具体分析了编译Hello程序的结果。编译阶段对源程序进行分析和检查,确保所有语句符合语法规则,然后将其转换为等效的汇编代码表示(中间代码)。

第4章 汇编

4.1 汇编的概念与作用

  1. 概念

驱动程序运行(或直接运行)汇编器as,将汇编语言程序(这里指hello.s)翻译成机器语言指令,并将这些指令打包成可重定位目标文件(hello.o)的过程称为汇编,hello.o是二进制编码文件,包含程序的机器指令编码。

  1. 作用

驱动程序运行(或直接运行)汇编器as,将汇编语言程序(这里指hello.s)翻译成机器语言指令,并将这些指令打包成可重定位目标文件(hello.o)的过程称为汇编,hello.o是二进制编码文件,包含程序的机器指令编码。

4.2 在Ubuntu下汇编的命令

在Ubuntu的命令行下输入指令:

gcc -c hello.s -o hello.o

则当前目录下会生成文件hello.o

图4- 1

 

 

图4- 2

4.3 可重定位目标elf格式

  1. Readelf命令

使用命令:readelf -a hello.o > hello_elf.txt

将hello.o中ELF格式相关信息重定向至文件hello_elf.txt。

 

图4- 3

  1. ELF头

 

图4- 4

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

  1. 节头目表

此部分列出了hello.o中的14个节的名称、类型、地址、偏移量、大小等信息。具体内容(hello_elf.txt文件第22 ~ 52行)如下:

 

图4- 5

  1. 重定位节

重定位节记录了各段引用的符号相关信息,在链接时,需要通过重定位节对这些位置的地址进行重定位。链接器会通过重定位条目的类型判断如何计算地址值并使用偏移量等信息计算出正确的地址。

 

图4- 6

  1. 符号表

符号表(.symtab)存放在程序中定义和引用的函数和全局变量的信息。

 

图4- 7

4.4 Hello.o的结果解析

在Ubuntu的命令行下输入指令:

objdump -d -r hello.o > hello_dis.txt

则当前目录下会生成文件hello_dis.txt

 

图4- 8

反汇编代码:

 

图4- 9

分析:二者总体相同,但也有一些细微的差异:

  1. 分支控制转移不同:对于跳转语句跳转的位置,hello.s中是代码块的名称,而反汇编代码中跳转指令跳转的位置是相对于main函数起始位置偏移的地址(相对地址);
  2. 函数调用表示不同:hello.s中,call指令使用的是函数名,而反汇编代码中call指令使用的是待链接器重定位的相对偏移地址,这些调用只有在链接之后才能确定运行时的实际地址,因此在.rela.text节中为其添加了重定位条目;
  3. hello.s中的全局变量、printf字符串等符号被替换成了待重定位的地址;
  4. 数的表示不同:hello.s中的操作数均为十进制,而hello.o反汇编代码中的操作数被转换成十六进制;
  5. hello.s中提供给汇编器的辅助信息在反汇编代码中不再出现,可能是在汇编器处理过程中被移除。

4.5 本章小结

本章对汇编的概念、作用、可重定向目标文件的结构及对应反汇编代码等进行了较为详细的介绍。经过汇编阶段,汇编语言代码转化为机器语言,生成的可重定位目标文件(hello.o)为随后的链接阶段做好了准备。完成本章内容的过程加深了我对汇编过程、ELF格式以及重定位的理解。

第5章 链接

5.1 链接的概念与作用

  1. 概念

链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时、执行时甚至运行时。在现代系统中,链接是由叫做链接器的程序自动执行的。

在此处,链接是指将可重定向目标文件hello.o与其他一些文件组合成为可执行目标文件hello。

  1. 作用

链接使分离编译成为可能,我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解成更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。

5.2 在Ubuntu下链接的命令

在ubuntu下输入命令

ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o hello.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o -o hello

 

图5- 1

    1. 可执行目标文件hello的格式

在ubuntu下输入命令

readelf -a hello > hello1_elf.txt

将hello中ELF格式相关信息重定向至文件hello1_elf.txt

图5- 2

5.4 hello的虚拟地址空间

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

 

图5- 3

查看5.3中的节头部表,可知.text开始地址在0x4010f0.rodata起始地址为0x402000

5.5 链接的重定位过程分析   

在ubuntu中输入命令

objdump -d -r hello > hello1_dis.txt

不同

  1. 整体上来看,hello的反汇编代码比hello.o的反汇编代码多了一些节(如.init, .plt, .plt.sec等)。

 

图5- 4

  1. hello中加入了一些函数,如exit、printf、sleep、getchar,如下图所示:

 

图5- 5

  1.  hello中不再存在hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址

5.6 hello的执行流程

ld-2.30.so!_dl_start

0x7fc37a781de0

ld-2.30.so!_dl_init

0x7fc37a7910b0

libc-2.30.so!__libc_start_main

0x7fb9b0abc0f0

libc-2.30.so!__cxa_atexit+0

0x7fe7d0d220e0

hello!__libc_csu_init

0x401130

libc-2.30.so!_setjmp

0x7fe7d0d1e060

hello!main

0x4011a5

hello!puts@plt

0x401090

hello!exit@plt

0x4010d0

*hello!print@plt

0x4010a0

*hello!atoi@plt

0x4010c0

*hello!sleep@plt

0x4010e0

*hello!getchar@plt

0x4010b0

libc-2.30.so!exit

0x7fed189ccd40

5.7 Hello的动态链接分析

  延迟绑定通过全局偏移量表(GOT)和过程链接表(PLT)的协同工作实现函数的动态链接,其中GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。在此之后,程序调用共享库函数时,会首先跳转到PLT执行指令,第一次跳转时,GOT条目为PLT下一条指令,将函数ID压栈,然后跳转到PLT[0],在PLT[0]再将重定位表地址压栈,然后转进动态链接器,在动态链接器中使用两个栈条目确定函数运行时地址,重写GOT,再将控制传递给目标函数。以后如果再次调用同一函数,则通过间接跳转将控制直接转移至目标函数。

5.8 本章小结

本章围绕可重定位目标文件hello.o链接生成可执行目标文件hello的过程,首先详细介绍、分析了链接的概念、作用及具体工作。随后验证了hello的虚拟地址空间与节头部表信息的对应关系,分析了hello的执行流程。最后对hello程序进行了动态链接分析。在此过程中,我更加深刻地理解了链接和重定位的相关概念,复习了课程第7章的相关知识,了解了动态链接的过程及作用。

第6章 hello进程管理

6.1 进程的概念与作用

  1. 概念

进程的经典定义是一个执行中程序的实例,是操作系统对一个正在运行的程序的一种抽象。

  1. 作用

每次用户通过shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。

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

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

  1. 作用

Shell-bash是一个交互型应用级程序,代表用户运行其他程序。它是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并把它送入内核去执行

  1. 处理流程
    1. 从终端读入输入的命令;
    2. 将输入字符串切分获得所有的参数;
    3. 如果是内置命令则立即执行;
    4. 若不是则调用相应的程序执行;
    5. shell应该随时接受键盘输入信号,并对这些信号进行相应处理。

6.3 Hello的fork进程创建过程

根据shell的处理流程,键入命令后,shell判断其不是内部指令,即会通过fork函数创建子进程。子进程与父进程近似,会得到一份与父进程用户级虚拟空间相同且独立的副本——包括数据段、代码、共享库、堆和用户栈等,父进程打开的文件,子进程也可读写。二者之间最大的不同在于PID的不同。fork函数被调用一次会返回两次,在父进程中,fork函数返回子进程的PID,在子进程中,fork函数返回0。

6.4 Hello的execve过程

execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。只有出现错误时(例如找不到可执行目标文件hello),execve才会返回到调用程序,这里与调用一次返回两次的fork函数不同。

execve函数在加载了hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数原型如下:

int main(int argc, char **argv, char *envp)

execve函数的执行过程会覆盖当前进程的地址空间,但并没有创建一个新进程。新的程序仍然有相同的PID,并且继承了调用execve函数时已打开的所有文件描述符。

6.5 Hello的进程执行

上下文

内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。

进程上下文切换:

在内核调度了一个新的进程运行时,它就抢占当前进程,并使用一种上下文切换的机制来控制转移到新的进程。具体过程为:①保存当前进程的上下文;②恢复某个先前被抢占的进程被保存的上下文;③将控制传递给这个新恢复的进程。

进程时间片

一个进程执行它的控制流的一部分的每一个时间段叫做时间片(time slice),多任务也叫时间分片(time slicing)。

进程调度:

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

hello程序调用sleep函数休眠时,内核将通过进程调度进行上下文切换,将控制转移到其他进程。当hello程序休眠结束后,进程调度使hello程序重新抢占内核,继续执行。

用户态与核心态的转换:

为了保证系统安全,需要限制应用程序所能访问的地址空间范围。因而存在用户态与核心态的划分,核心态拥有最高的访问权限,而用户态的访问权限会受到一些限制。处理器使用一个寄存器作为模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,一定程度上保证了系统的安全性。

 

图6- 1 hello程序进程执行过程示意

6.6 hello的异常与信号处理

  1. 随意输入

 

图6- 2

  1. Ctrl+c

 

图6- 3

  1. Ctrl+z

 

图6- 4

  1. Ps

 

图6- 5

  1. Jobs

 

图6- 6

  1. Fg

 

图6- 7

  1. Pstree

 

图6- 8

 

图6- 9

  1. Kill

 

图6- 10

6.7本章小结

本章主要阐述了hello的进程管理,包括进程创建、加载、执行以至终止的全过程,并分析了执行过程中的异常、信号及其处理。在hello程序运行的过程中,内核对其进行进程管理,决定何时进行进程调度,在接收到不同的异常、信号时,还要及时地进行对应的处理。本章的内容引导我复习了课程第8章——异常控制流的相关内容,使我对进程、信号及异常相关概念的理解更加深刻。

第7章 hello的存储管理

7.1 hello的存储器地址空间

  1. 逻辑地址

逻辑地址(Logical Address)是指由程序产生的与段相关的偏移地址部分,是相对应用程序而言的,如hello.o中代码与数据的相对偏移地址。

  1. 线性地址

线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。逻辑地址加上相应段的基地址就生成了一个线性地址,如hello中代码与数据的地址。

  1. 虚拟地址

有时我们也把逻辑地址称为虚拟地址(Virtual Address)。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。

  1. 物理地址

物理地址(Physical Address)是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址(hello程序运行时代码、数据等对应的可用于直接在内存中寻址的地址)。如果启用了分页机制(当今绝大多数计算机的情况),那么线性地址会使用页目录和页表中的项变换成物理地址;如果没有启用分页机制,那么线性地址就直接成为物理地址了。

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

当一个程序要执行时,就要决定程序代码、数据和堆栈各要用到内存的哪些位置,通过设定段寄存器CS,DS,SS来指向这些起始位置。通常是将DS固定,而根据需要修改CS。所以,程序可以在可寻址空间小于64K的情况下被写成任意大小。所以,程序和其数据组合起来的大小,限制在DS所指的64K内,这就是COM文件不得大于64K的原因。

段寄存器是因为对内存的分段管理而设置的。计算机需要对内存分段,以分配给不同的程序使用(类似于硬盘分页)。在描述内存分段时,需要有如下段的信息:1.段的大小;2.段的起始地址;3.段的管理属性(禁止写入/禁止执行/系统专用等)。

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

VM系统通过将虚拟内存分割为称为虚拟页的大小固定的块来处理这个问题,每个虚拟页的大小P=2p(一般为4096字节)。类似地,物理内存被分割成物理页,大小也为P字节(物理页也称为页帧)。hello进程执行时,CPU中的页表基址寄存器指向hello进程的页表,当hello进程访问其虚拟空间内的指令、数据等内容时,CPU芯片上的MMU(内存管理单元)会将对应的线性地址变换为物理地址以进行寻址访问。n位(Core i7为48位)的线性地址包含两部分:一个p位的虚拟页面偏移(VPO)和一个(n-p)位的虚拟页号。MMU利用虚拟页号(VPN)来选择适当的PTE(页表项),若PTE有效位为1,则说明其后内容为物理页号(PPN),否则缺页。而物理地址中低p位的物理页偏移量(PPO)与虚拟页偏移量(VPO)相同,PPN与PPO连接即得物理地址。

 

图7- 1

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

TLB

每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降到1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。

多级页表:

多级页表为层次结构,用于压缩页表。这种方法从两个方面减少了内存要求。第一,如果一级页表中的一个PTE是空的,那么相应的二级页表就根本不会存在;第二,只有一级页表才需要总是在主存中,虚拟内存系统可以在需要时创建、页面调出或调入二级页表,最经常使用的二级页表才缓存在主存中,减少了主存的压力。

VA到PA的变换:

对于四级页表,虚拟地址(VA)被划分为4个VPN和1个VPO。每个VPN i都是一个到第i级页表的索引。对于前3级页表,每级页表中的每个PTE都指向下一级某个页表的基址。最后一级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问k个PTE。和只有一级的页表结构一样,PPO和VPO是相同的。

 

图7- 2

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

 

图7- 3

7.6 hello进程fork时的内存映射

当fork函数被父进程(shell)调用时,内核为新进程(未来加载执行hello的进程)创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有空间地址的抽象概念。

7.7 hello进程execve时的内存映射

execve加载和运行hello程序会经过以下步骤:

  1. 删除已存在的用户区域:这里指在fork后创建于此进程用户区域中的shell父进程用户区域副本。
  2. 映射私有区域:为hello程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射到hello可执行文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
  3. 映射共享区域:hello程序与一些共享对象或目标链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
  4. 设置程序计数器(PC):设置此进程上下文中的程序计数器,使之指向hello代码区域的入口点。

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

在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页。缺页故障属于异常类别中的故障,是潜在可恢复的错误,主要处理流程可见本文6.6节中关于故障处理的部分。

缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,如果牺牲页已经被修改了,内核会将其复制回磁盘。随后内核从磁盘复制引发缺页异常的页面至内存,更新对应的页表项指向这个页面,随后返回。

缺页异常处理程序返回后,内核会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件,此次页面会命中。

 

图7- 4 缺页异常处理过程示意

7.9动态存储分配管理

动态内存分配管理使用动态内存分配器来进行。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

分配器有两种风格——显式分配器和隐式分配器。C语言中的malloc程序包是一种显式分配器。显式分配器必须在一些相当严格的约束条件下工作:①处理任意请求序列;②立即响应请求;③只使用堆;④对齐块(对齐要求);⑤不修改已分配的块。在以上限制条件下,分配器要最大化吞吐率和内存使用率。

常见的放置策略:

  1. 首次适配:从头开始搜索空闲链表,选择第一个合适的空闲块。
  2. 下一次适配:类似于首次适配,但从上一次查找结束的地方开始搜索。
  3. 最佳适配:选择所有空闲块中适合所需请求大小的最小空闲块。

这里简要介绍一些组织内存块的方法:

  1. 隐式空闲链表:空闲块通过头部中大小字段隐含连接,可添加边界标记提高合并空闲块的速度。
  2. 显式空闲链表:在隐式空闲链表块结构的基础上,在每个空闲块中添加一个pred(前驱)指针和一个succ(后继)指针。
  3. 分离的空闲链表:将块按块大小划分大小类,分配器维护一个空闲链表数组,每个大小类一个空闲链表,减少分配时间同时也提高了内存利用率。C语言中的malloc程序包采用的就是这种方法。
  4. 红黑树等树形结构:按块大小将空闲块组织为树形结构,同样有减少分配时间和提高内存利用率的作用。

7.10本章小结

本章主要关注hello程序的存储管理,介绍了不同的地址概念、地址变换与寻址、内存映射、内存分配管理等内容。不难看出,现代计算机系统为提高内存存储效率和使用率以至程序运行的效率使用了大量的机制和技术。此外,本章内容与教材第6章、第9章紧密联系,且补充了本文第6章的内容,体现了计算机系统课程的整体性。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

  1. 设备的模型化——文件

所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件。

例如:/dev/sda2文件是用户磁盘分区,/dev/tty2文件是终端。

  1. 设备管理——Unix IO接口

将设备模型化为文件的方式允许Linux内核引入一个简单、低级的应用接口,称为Unix IO,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

Unix IO接口:

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

8.3 printf的实现分析

  1. printf函数体:

int printf(const char *fmt, ...)

{

    int i;

    va_list arg = (va_list)((char *)(&fmt) + 4);

    i = vsprintf(buf, fmt, arg);

    write(buf, i);

    return i;

}

分析:

  1. printf函数调用了vsprintf函数,最后通过系统调用函数write进行输出;
  2. va_list是字符指针类型;
  3. ((char *)(&fmt) + 4)表示...中的第一个参数。

分析:vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出写入buf供系统调用write输出时使用。直到陷阱系统调用 int 0x80 syscall。显示芯片按照刷新频率逐行读取 vram,并通过信号线向液晶显示器传输每一个点(RGB 分量)。

8.4 getchar的实现分析

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章主要关注Linux系统中的I/O管理,阐述了Linux操作系统的IO设备管理方法——设备被模型化为文件并使用Unix IO接口进行文件操作,最后还分析了printf函数和getchar函数的实现。从此章的内容中我们能体会到Unix IO接口在Linux系统中的重要作用,同时也了解了作为异步异常之一的键盘中断的处理。

结论

hello程序虽然是一个简短的C语言程序,但它的产生、执行、终止和回收离不开计算机系统各方面的协同工作,具体过程如下:

  1. hello.c源代码文件通过C语言预处理器的预处理,得到了调整、展开后的ASCII文本文件hello.i;
  2. hello.i经过编译器的编译得到汇编代码文件hello.s;
  3. hello.s经过汇编器的汇编得到可重定向目标文件hello.o;
  4. hello.o经过链接器的链接过程成为可执行目标文件hello;
  5. 用户在shell-bash中键入执行hello程序的命令后,shell-bash解释用户的命令,找到hello可执行目标文件并为其执行fork创建新进程;
  6. fork得到的新进程通过调用execve完成在其上下文中对hello程序的加载,hello开始执行;
  7. hello作为一个进程运行,接受内核的进程调度(调用sleep后内核进行上下文切换,调度其他进程执行);
  8. hello执行的过程中,可能发生缺页异常等故障、系统调用等陷阱以及接收到各种信号,这些都需要操作系统与硬件设备的协同工作进行处理;
  9. hello执行的过程中会访问其虚拟空间内的指令和数据,需要借助各种硬件、软件机制来快速、高效完成;
  10. hello运行时要调用printf、getchar等函数,这些函数的实现与Linux系统IO设备管理、Unix IO接口等息息相关;
  11. hello程序运行结束后,父进程shell-bash会进行回收,内核也会清除在内存中为其创建的各种数据结构和信息。
  • 个人感悟
  1. 计算机系统的设计和实现大量体现了抽象的思想:文件是对I/O设备的抽象,虚拟内存是对主存和磁盘设备的抽象,进程是对处理器、主存和I/O设备的抽象,进程是操作系统对一个正在运行的程序的抽象等等;
  2. 计算机系统课程内容虽然繁多,但逻辑结构清晰、层次分明。完成本论文让我体会到:一个小小的hello程序就能展现计算机系统的方方面面;
  3. CSAPP这本书及计算机系统课程引领我从程序员的角度第一次系统地、全面地认识了现代操作系统的各种机制、设计和运行原理。我会在未来继续深入学习相关知识并在今后的具体实践中不断尝试使用和创新。

附件

文件名称

说明

hello.i

hello.c经预处理得到的ASCII文本文件

hello.s

hello.i经编译得到的汇编代码ASCII文本文件

hello.o

hello.s经汇编得到的可重定位目标文件

hello_elf.txt

hello.o经readelf分析得到的文本文件

hello_dis.txt

hello.o经objdump反汇编得到的文本文件

hello

hello.o经链接得到的可执行文件

hello1_elf.txt

hello经readelf分析得到的文本文件

hello1_dis.txt

hello经objdump反汇编得到的文本文件

参考文献

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

[1]  林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.

[2]  辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.

[3]  赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).

[4]  谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.

[5]  KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.

[6]  CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.

(参考文献0分,缺失 -1分)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值