大作业

题 目程序人生-Hellos P2P

专 业 计算学部

学  号

班级

学生

指 导 教 师

计算机科学与技术学院

2022年5月

摘 要

文章通过对hello.c程序在Linux系统下的生命周期进行追踪,逐步分析其在预处理、编译、汇编、链接生成可执行文件中的变化表现,并在Ubuntu系统下执行,观察其加载、运行、终止、回收的过程,实现了对程序翻译的系统性了解,以及对其运行机制的系统性的分析与概述。

关键词:计算机系统;编译系统;进程管理;存储

目 录

第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.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:即From Program to Process。如下图1,hello程序从源文件hello.c开始,经过预处理器cpp的预处理,得到hello.i文件,这是一个经修改了的源程序;hello.i文件再经过编译器cc1处理,得到文本文件hello.s;hello.s再经过汇编器as处理,被翻译成机器语言指令,打包成二进制文件hello.o;再经过链接器ld,将hello.o文件和一些必要的系统目标文件组合起来,就得到了可执行目标文件hello。接着,用户在shell输入./hello即可执行该文件。Shell会通过一系列操作将代码加载到内存,并调用fork函数创建一个新的进程,再调用execve函数,将hello程序加载到该子进程中执行。

图1 从源文件到可执行文件

020:即From Zero to Zero。接上述过程,shell执行execve函数将程序加载到进程中,进行虚拟内存映射,将程序载入物理内存中执行,后开始执行目标代码,CPU为运行的hello分配时间片来执行逻辑控制流,运行结束后,父进程回收这个子进程,内核删除相关的数据结构,释放资源,hello归于zero。

1.2 环境与工具

硬件环境:X64 CPU;2GHz;4GRAM;256Disk;

软件环境:Windows10 64位;Vmware 16;Ubuntu 20.04 LTS 64位;

工具:gcc;edb;as;cpp;cc1;gedit;GNU READELF等

1.3 中间结果

hello.i 预处理后得到的文件;

hello.s编译后的汇编文件;

hello.o 汇编得到的可重定位目标文件;

hello 链接得到的可执行文件;

hello.elf 用readelf读取hello.o得到的ELF格式信息;

hello.txt 反汇编hello.o得到的反汇编文件

helloprog.elf 由hello可执行文件生成的.elf文件

helloprog.txt 反汇编hello可执行文件得到的反汇编文件

1.4 本章小结

本章是对hello程序“一生”的过程的简述,主要围绕P2P(从程序到进程)和020(从无到有到无)的过程来进行简略介绍,并给出了实验时的软硬键环境、开发调试工具等基本信息。

(第1章0.5分)

第2章 预处理

2.1预处理的概念与作用

概念:程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位——预处理记号用来支持语言特性(如C/C++的宏调用)。

作用:预处理程序读入所有包含的文件以及待编译的源代码,然后生成源代码的预处理版本,会根据以字符#开头的命令来修改原始的C程序。比如,会读取头文件的相应内容,并将其直接插入到程序文本中;用实际值来替换掉“#define”定义的字符串;根据“#if”后的条件来决定需要编译的代码;此外还有#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)等的处理。

2.2在Ubuntu下预处理的命令

命令:gcc hello.c -E -o hello.i或者cpp hello.c hello.i

图2.1 预处理命令

2.3 Hello的预处理结果解析

经预处理得到了hello.i文件,使用gedit查看hello.i文件部分内容,见下图:

图2.2.1 hello.i部分内容

图2.2.2 hello.i部分内容

源文件的预处理删去了hello.c文件中的注释内容,引入了头文件,对宏进行了展开,得到了一个三千多行的代码,该代码仍是C程序。

2.4 本章小结

本章主要介绍了预处理的相关概念和具体作用,并结合hello.c文件的预处理进行实际的结果分析:根据以字符#开头的命令来修改原始的C程序,比如宏定义的扩展、头文件的代码引入等。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

概念:可通过编译器(cc1)将文本文件.i翻译成.s文本文件,它包含一个汇编语言程序。

作用:它是将高级语言程序转化为机器可直接识别处理执行的的机器码的中间步骤,会进行语法检查、目标程序优化等。

3.2 在Ubuntu下编译的命令

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

图3.1 编译命令

3.3 Hello的编译结果解析

3.3.1汇编初始部分

图3.2.1 hello.s部分代码

图3.2.2 hello.s部分代码

文件的开头是一些声明:.file声明源文件;.test是代码段;.section.rodata是只读数据段;.align声明对指令或数据的存放地址的对齐方式;.string声明字符串;.globl声明全局变量;.type声明一个符号是函数类型还是数据类型;.size声明大小。

3.3.2数据

(1)数字常量:源代码中有将参数argc和常数4比较,4便以立即数的形式直接用于比较了;exit的1也同样以立即数的形式出现。

图3.3 数字常量

(2)局部变量:main函数里有声明一个局部变量i,它被存储在了栈中(图3.4.1);参数argc和argv也是局部变量,同样被存储在了栈里(图3.4.2)。

图3.4.1 局部变量i

图3.4.2 参数argc及argv

(3)字符串常量:源程序中的两个printf的参数都是字符串常量分别别为:”用法: Hello 学号 姓名 秒数!\n”、”Hello %s %s\n”。这两个字符串都存储在只读数据段中。

图3.5 符串常量

3.3.3赋值

在源代码中,for循环部分将i赋值0,对应汇编代码中的movl $0 -4(%rbp)指令(见图3.4.1),由于i为int型操作数,用movl来赋值“双字”。

3.3.4类型转换

源代码中有引用atoi函数,将argv[3]由字符串转换成了整型:

图3.6 类型转换

3.3.5算术操作与逻辑操作

在该代码文件中出现的算术操作有以下几种:

  1. subq $32, %rsp:对栈指针的减法操作,开辟了一个32字节的栈空间;
  2. addq $16, %rax:修改地址偏移量;
  3. addl $1, -4(%rbp):实现i++

其余整数算术操作见下图。

图3.7 整数算术操作

3.3.6 关系操作以及控制转移指令

关系操作即一些判断大于、等于、不小于之类的关系判断操作。在源代码中,有将参数argc与4比较,以及将i与8比较,其在汇编语言中的情况见下图:

图3.8 关系操作

先用CMP指令设置条件码,再结合不同的JXX控制转移指令得到比较的效果,若满足控制转移条件,则跳转到对应的跳转目标。控制转移指令一般出现在循环语句和条件语句中。

3.3.7数组

源程序的第二个参数argv[]是一个指针数组,被存在了栈中(见图3.4.2)。该数组的每个元素都是一个只想字符类型的指针,在汇编代码中被两次传递给了printf函数。

图3.9 数组

3.3.8函数操作

函数调用:假设P调用Q,Q执行后返回P,这个过程包含下面一个或多个机制:

(1)传递控制:进行过程 Q 的时候,程序计数器必须设置为 Q 的代码的起始地址,然后在返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的地址。

(2)传递数据:P 必须能够向 Q 提供一个或多个参数,Q 必须能够向 P 中返回一个值。

(3)分配和释放内存:在开始时,Q 可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。

hello程序中的函数有main、puts、exit、printf、atoi、sleep、getchar函数。

在程序入口处调用了main函数,其余函数都是通过call指令来调用的。注意,在源代码中并未调用puts函数,它是由于第一个printf函数只用来输出一串字符串,而被系统优化成了puts函数。

3.4 本章小结

本章介绍了编译的概念和作用,编译将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做了准备。此外,本章结合hello.s文件,对汇编代码中的一些C数据与操作进行了解析,介绍了编译器如何处理各个数据类型以及各类操作。

(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

概念:汇编是指汇编器(as)将.s文件翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在.o目标文件的过程。作用:将编译器产生的汇编语言进一步翻译为计算机可以理解的二进制机器语言,生成.o文件。

4.2 在Ubuntu下汇编的命令

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

图4.1 汇编命令

4.3 可重定位目标elf格式

用readelf -a -W hello.o > hello.elf命令得到hello.elf文件:

图4.2 用readelf命令得到hello.o的ELF格式文件

接下来为其各节的基本信息:

4.3.1 ELF头

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

图4.3 ELF头

4.3.2节头部表

节头部表包含了文件中出现的各个节的名称、类型、属性、地址、偏移量、大小等信息,目标文件中每个节的都有一个固定大小的条目(entry)。

图4.4 节头部表

该文件的节头部表中包含的信息具体见图4.4,依次为:

(0)无效段,类型为NULL。

(1).text节:内容为已编译程序的机器代码。

(2).rela.text节:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。

(3).data节:包含已初始化的全局和静态C变量。

(4).bss节:包含未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。

(5).rodata节:包含只读数据,如printf语句中的格式串和开关语句的跳转表等。

(6).comment节:存储的是编译器的版本信息。

(7).note.GNU_stack节:标记可执行堆栈。

(8).note.gnu.property节。

(9).eh_frame节:处理异常。

(10).rela.eh_frame节:保存.eh_frame节的重定位信息。

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

(12).strtab节:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。

(13).shstrtab节:该节保存着节名称。

4.3.3重定位节

当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置。它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。

图4.5 重定位节

所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.re1.text中。已初始化数据的重定位条目放在.rel.data中。

图4.5是重定位节的具体内容。需要重定位的是各个调用的函数和只读数据内容。其中,offset是需要被修改的引用的节偏移;symbol标识被修改引用应该指向的符号;type告知链接器如何修改新的引用;addend是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。ELF定义了32种不同的重定位类型,两种最基本的重定位类型包括R_X86_64_PC32(重定位使用32位PC相对地址的引用)和R_X86_64_32(重定位使用32位绝对地址的引用)。

4.3.4 符号表

符号表是由汇编器构造的,使用编译器输出到汇编语言.s文件中的符号。

图4.6 符号表

name是字符串表中的字节偏移,指向符号的以nu11结尾的字符串名字。value 是符号的地址。对于可重定位的模块来说,value是距定义目标的节的起始位置的偏移。对于可执行目标文件来说,该值是一个绝对运行时地址。size是目标的大小(以字节为单位)。 type通常要么是数据,要么是函数。符号表还可以包含各个节的条目,以及对应原始源文件的路径名的条目。所以这些目标的类型也有所不同。binding字段表示符号是本地的还是全局的。

4.4 Hello.o的结果解析

用objdump -d -r hello.o> hello.txt 反汇编,得到hello.txt文件。

hello.o的反汇编得到的汇编代码基本与hello.s一致,每个操作代码都可以一一对应。此外,两者之间有一些细小的差别:

(1)立即数 在.s文件中,立即数直接以十进制数表示,而在反汇编得到的文件中,立即数以十六进制表示。

(2)指令表示 反汇编的指令mov、add、sub等后面没有对字大小的表示,如w,l,q等;但.s文件中有这些表示。而call指令在反汇编中表示为callq。

(3)分支转移 在.s文件中,跳转指令的目标地址直接记为段名称,如.L2,.L3等;而反汇编代码跳转指令的操作数使用的是确定的地址。

(4)函数调用 在hello.s文件中,call之后直接跟着函数名称,而在反汇编文件中,call后跟着的是直接的地址偏移。

图4.7 hello.o的反汇编结果

图4.8 hello.s代码段

4.5 本章小结

本章介绍了汇编的概念与作用,并依据实际操作将hello.s文件翻译为hello.o文件,并生成hello.o的ELF格式文件hello.elf,对ELF格式文件的具体结构进行了详细的分析。此外,将hello.o文件反汇编并与hello.s文件进行了对照分析,并就二者的差异进行说明。

(第4章1分)

5链接

5.1 链接的概念与作用

概念:链接是将各种代码和数据片段收集并合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。

作用:链接可以通过符号解析、重定位等工作将多个.o文件和静态库文件、动态链接库等合并得到一个执行文件,这使得分离编译成为可能,从而可以避免因某一个模块的小改动而需要重新编译所有文件的麻烦。

5.2 在Ubuntu下链接的命令

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

5.1 链接命令

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

用readelf -a -W hello > helloprog.elf命令得到helloprog.elf文件:

图5.2 用readelf命令得到hello.o的ELF格式文件

hello的ELF格式文件的各节的基本信息:

5.3.1 ELF头

图5.3.1 ELF头

5.3.2 节头部表

图5.3.2 节头部表

其比hello.o文件多了一些段,各段的基本信息均已在上图中列出,不再赘述。

5.3.3 程序头

图5.3.3 程序头

5.3.4 动态节

图5.3.4 动态节

5.3.5 重定位表

图5.3.5 重定位表

5.3.6 符号表

图5.3.6 符号表

5.4 hello的虚拟地址空间

使用edb加载hello,通过Data Dump可以查看到本进程的虚拟地址空间各段信息。结合5.3的内容,可以定位到各节的信息。

程序头表(图5.5)中load表明程序段开始的地址为0x400000,见下图。

图5.4.1 edb中Data Dump部分示图

下图是.text节和.got节的信息:

图5.4.2 .text节(地址0x4010f0)

图5.4.3 .got节(地址0x403ff0)

5.5 链接的重定位过程分析

用命令objdump -d -r hello> helloprog.txt得到helloprog.txt文件。

相比于hello.o的反汇编文件,helloprog.txt中除了main函数,还有其调用的库函数(图5.5.1)和一些新增节,如.init节、.plt节(图5.5.2)。

图5.5.1 hello反汇编中的库函数(部分)

链接的过程:链接主要分两个过程:符号解析和重定位。

符号解析:目标文件定义和引用符号,每个符号对应于一个函数、-个全局变量或一个静态变量(即C语言中任何以static属性声明的变量)。符号解析可以将每个符号引用正好和一个符号定义关联起来。

重定位:编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义于一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。重定位由下面两步组成:

重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,来自所有输入模块的.data节被全部合并成一个节,这个节成为输出的可执行目标文件的.data节。然后,链接器运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号.当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。

重定位节中的符号引用。在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中的重定位条目。代码的重定位条目被放在.rel.text中,已初始化的 数据的重定位条目放在.rel.data中。

图5.5.2 hello反汇编中的新增节(部分)

重定位的算法见下图:

图5.5.3 重定位算法

对于hello在执行重定位,以hello.o反汇编文件第16行内容来举例说明。该行有一个重定位条目:类型为R_X86_64_PC32,offset为0x1c,addend为-4,symbol为.rodata(见图5.3.4);ADDR(main)和ADDR(.rodata)均可在hello的重定位表或者hello执行文件的反汇编文件中得到,再利用图5.3.3中6~8行算法即可得到更新的内容(一个相对地址)。

图5.5.4 重定位条目

5.6 hello的执行流程

子程序名

程序地址

hello!_start

0x4010f0

hello!main

0x401125

hello!puts@plt

0x401030

hello!printf@plt

0x401040

hello!atoi@plt

0x401060

hello!sleep@plt

0x401080

hello!getchar@plt

0x401050

hello!exit@plt

0x401070

表5.6 hello执行流程

5.7 Hello的动态链接分析

在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,于是需要一条重定位记录由动态链接加载时来处理。动态链接器使用PLT和GOT来延迟绑定。

由图5.3.2的节头部表可知,.got.plt的起始位置是0x404000,在调用dl_init前后,其值发生了改变(图5.7),即实现了在运行时的链接。

图5.7.1 .got.plt内容(改变前)

图5.7.2 .got.plt内容(改变后)

5.8 本章小结

本章主要介绍了链接的概念与作用,对链接得到的可执行文件得的ELF格式文件进行详细分析,并于hello.o的文件进行比较,从而更加具体地表现了链接的执行操作和功能,并具体地介绍、表现了其重定位的原理和过程。

(第5章1分)

6hello进程管理

6.1 进程的概念与作用

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

作用:进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。它提供给应用程序两个关键抽象:1)一个独立的逻辑控制流,提供一个程序独占处理器的假象;2)一个私有的地址空间,提供一个程序独占地使用内存系统的假象。

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

Shell是一个交互型应用级程序,代表用户运行其他程序。如Windows下的命令行解释器,cmd、powershell,图形界面的资源管理器。Linux下的Terminal/tcsh、bash等等,也包括图形化的GNOME桌面环境。Shell是信号处理的代表,负责各进程创建与程序加载运行及前后台控制,作业调用,信号发送与管理等。

处理流程:

1、读取用户由键盘输入的命令行。

2、分析命令,以命令名作为文件名,并将其它参数改造为系统调用execve( )内部处理所要求的形式。

3、终端进程调用fork( )建立一个子进程。

4、终端进程本身调用waitpid()来等待子进程完成(如果是后台命令,则不等待)。当子进程运行时调用execve(),子进程根据文件名到目录中查找有关文件,调入内存,执行这个程序。

5、如果命令末尾有&,表示后台执行,此时shell不会等待它完成;否则shell会等待其完成。当子进程完成工作后,向父进程报告,此时终端进程醒来,作必要的判别工作后,终端发出命令提示符,重复上述处理过程。

6.3 Hello的fork进程创建过程

在shell上输入./hello命令时,先判断该命令是否为内置命令,如果是内置命令则立即对其进行解释;不是否则将其看成一个可执行目标文件,再调用fork创建一个新进程并在其中执行。新创建的子进程鸡湖但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本。二者之间的最大的区别是它们有不同的PID。

6.4 Hello的execve过程

execve函数在当前进程的上下文中加载并运行一个新程序。execve函数加载并运行可执行目标文件,且带参数列表rgv和环境变量列表envp。只有当出现错误时,execve才会返回到调用程序。

在子进程调用该函数时,它将删除该进程的代码和地址空间内的内容并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行该程序;接着映射私有区域,为新程序的代码、数据等创建新的区域结构;然后映射共享区域,比如动态链接到程序中的标准C库libc.so等;再是设置程序计数器,使之指向代码区域的入口。

6.5 Hello的进程执行

在fork了一个子进程之后,该进程有了一个独立的逻辑控制流。在运行过程中,该程序正常执行。在调用sleep函数后,为了最大化利用处理器资源,会进行上下文切换,进入内核模式切换到其他进程,再换为用户模式执行那个进程。同时,将hello进程加入等待队列,进入内核模式,并开始计时。当计时结束时,sleep

函数返回,发送一个中断信号,此时再进入内核模式执行中断处理,使得hello进

图6.1 进程上下文切换

程重新被调度,将其从等待队列中移出,并内核模式转为用户模式,继续执行其

逻辑控制流。

6.6 hello的异常与信号处理

6.6.1 正常执行,执行八次后回车返回:

图6.2正常执行结果

6.6.2 执行过程中随意乱按,可以正常结束:

图6.3 随意乱按结果

在程序执行过程中乱按所造成的输入均缓存到stdin,当getchar的时候读出一个’\n’结尾的字串。hello结束后,stdin中的其他字串会当做Shell的命令行输入。

6.6.3 CTRL+Z

图6.4.1 ctrl+z结果

输入ctrl+z后,Shell进程收到SIGSTP信号,挂起前台作业(图6.4.1)。

图6.4.2 crtl+z后的部分命令操作

由图6.6.2可知,该进程并没有被回收,而是运行在后台下,job号为1,用fg命令调到前台后,程序进行被执行了。调用kill命令杀死进程(图6.4.3)。

图6.4.3 kill命令使用

6.6.4 CTRL+C

图6.5 ctrl+c

执行过程中输入ctrl+c,Shell进程收到SIGINT信号,结束并回收hello进程。

6.7本章小结

本章介绍了进程的概念和作用、shell的一般处理流程和异常与信号的处理,并根据具体的hello程序,对fork、execve函数的原理与执行过程进行了详细分析,以及对异常与信号的处理进行了具体操作验证。

(第6章1分)

7hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:逻辑地址用来指定一个操作数或者是一条指令的地址。由一个段标识符加上一个指定段内相对地址的偏移量组成。比如hello.o文件反汇编得到的hello.txt文件里的地址就是逻辑地址。
线性地址:也叫虚拟地址,是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生段中的偏移地址,加上相应段的基地址就生成了一个线性地址。

虚拟地址:即线性地址。
物理地址:是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。若没有分页,物理地址就是hello的线性地址。

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

逻辑地址由段标识符和段内偏移量组成。段标识符是一个16位长的字段,称为段选择符,其前13为是一个索引号,可以用来确定当前使用的段描述符在描述符表中的位置;第14位称为表指示器,可以用来判断用全局段描述符表还是局部段描述符表,为0,则用全局段描述符表,为1,则用局部段描述符表;最后两位

表示请求者特权级00最高,11最低。

对于一个逻辑地址,要得到其线性地址,第一步先看表指示器,判断用全局段描述符表还是局部段描述符表;接着取段选择符前13位查找到对应段描述符,确定基地址;最后再加上相对偏移量即得到线性地址。

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

线性地址到物理地址的变换是通过对虚拟地址内存空间进行分页的分页机制来完成的。

页表是一个页表条目(Page Table Entry)的数组,虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个PTE。假设每个PTE由一个有效位和一个n位地址字段组成,有效位表明该虚拟页是否被缓存在DRAM中。如果设置了有效位,那么地址字段表示DRAM中相应的物理页的起始位置;如果没有设置有效位,那么一个空地址表示这个虚拟页还未被分配。否则,这个地址指向该虚拟页在磁盘的起始位置。

线性地址,也即虚拟地址,由虚拟页号(VPN)和虚拟页偏移量(VPO)组成;

物理地址由虚拟物理页偏移号(PPN)和对应的物理页偏移量(PPO)组成。下图展示了MMU如何利用页表来实现地址翻译。

图7.1 使用页表的地址翻译

CPU中的一个控制寄存器,页表基址寄存器指向当前页表。MMU利用VPN来选择适当的PTE。例如,VPN 0选择PTE 0,VPN 1选择PTE 1,以此类推。将页表条目中的物理页号和虚拟地址中的VPO串联起来就得到了相应的物理地址。,此外,因为物理和虚拟页面都是相同字节的,所以物理页面偏移PPO和VPO是相同的。

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

以Core i7为例,其采用四级页表的层次结构,每个进程有它私有的页表层次结构。

图7.2 Core i7地址翻译概况

当MMU翻译每一个虚拟地址时,它还会更新另外两个内核缺页处理程序会用到的位。每次访问一个页时,MMU都会设置A位,称为引用位。内核可以用这个引用位来实现它的页替换算法。每次对一个页进行了写之后,MMU都会设置D位,又称修改位或脏位。修改位告诉内核在复制替换页之前是否必须写回牺牲页。内核可以通过调用一条特殊的内核模式指令来清除引用位或修改位。

下图给出了Corei7MMU如何使用四级的页表来将虚拟地址翻译成物理地址。36位VPN被划分成四个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含L1页表的物理地址。VPN1提供到一个LlPTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2PTE的偏移量,以此类推。

(图片过大)

图7.3 Core i7页表翻译

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

在获得物理地址之后,将其分为标记位、组索引、块偏移三部分。先进行组索引,在L1中寻找对应的组。如果找到了,则看标记位和有效位,若标记位相等且有效位为1.则命中,取出值传给CPU,否则依序对L2、L3、内存进行如上操作,直到出现命中。然后再一级一级地传到上面,如果上层有空闲块则将目标块放置到空闲块中,否则将缓存中的某个块驱逐,将目标块放到被驱逐块的位置。

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

在bash进程中执行了execve(“hello”,NULL,NULL);调用,正如6.4节所述,execve函数在当前进程中加载并运行包含在可执行文件hello中的程序,用hello替代了当前bash中的程序。加载并运行经历了以下几步:1)删除已存在的用户区域;2)映射私有区域;3)映射共享区域;4)设置程序计数器(PC)。经历上述步骤后,PC已经指向了代码区域的入口点,下次调度这个进程时,它将从这个入口点开始执行。

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

DRAM缓存不命中被称为缺页。当发生缓存不命中后,会触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,若该页被修改了,就把它复制回磁盘,再换入新的页面并更新页表。在缺页处理程序重新启动导致缺页的指令之后,该指令将从内存中正常地读取字,而不会再产生异常了。

7.9动态存储分配管理

已经分配给用户的的内存区统称为占用块,还未分配出去的内存区统称为空闲块。用户提出存储需求,系统就会分配空闲块出去。随着用户运行结束,其占用的区域就又变成了空闲块,在这个过程中,内存块就变成了占用部分和空闲部分交互的情况。

对于这种情况,我们需要一种方法来组织空闲块、选择空闲块新分配的块、处理分配块后的剩余部分、合并被释放的空闲块。

组织空闲块:

堆中的空闲块通过头部中的大小字段隐含地连接,分配器通过遍历堆中所有的块来间接遍历整个空闲块的集合。隐式空闲链表的结构如图7.4,堆块结构见图7.5。

图7.4 隐式空闲链表

图7.5 堆块结构

放置已分配的块:

当一个用户请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可放置所请求块的空闲块。有三种搜索策略:1)首次适配,即从头开始搜索空闲链表,选择第一个合适的空闲块;2)下一次适配,同首次适配一样,只是开始位置为上一次查询结束的位置;3)最佳适配,检查每个空闲块,选择适合所需的最小空闲块。

分割空闲块:

一旦分配器找到一个匹配的空闲块,它就必须做另一个策略决定,那就是分配这个空闲块中多少空间。一个选择是用整个空闲块。虽然这种方式简单而快捷,但是主要的缺点就是它会造成内部碎片。如果放置策略趋向于产生好的匹配,那么额外的内部碎片也是可以接受的。然而,如果匹配不太好,那么分配器通常会选择将这个空闲块分割为两部分。第一部分变成分配块,而剩下的变成一个新的空闲块。

合并空闲块:

采取使用带边界标记的堆块的格式,在堆块的末尾为其添加一个脚部,其为头部的副本。添加脚部之后,分配器就可以通过检查前面一个块的脚部,判断前面一个块的起始位置和状态。从而实现快速合并,减小性能消耗。

7.10本章小结

本章依次介绍了hello 的存储器地址空间、intel 的段式管理、hello 的页式管、地址翻译、物理内存访问,hello进程fork、execve时的内存映射、缺页故障与缺页中断处理、动态存储的分配管理的相关知识点。

(第7章 2分)

8hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件。所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入输出都被当做对相应文件的读和写来执行。

设备管理:unix io接口。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。

8.2 简述Unix IO接口及其函数

8.2.1 Unix I/O接口

Unix I/O接口使得所有的输入输出都能以一种统一且一致的方式来执行:

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

2)Linux Shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。

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

4)读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

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

8.2.2 Unix I/O函数

1)int open(char* filename,int flags,mode_t mode)

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

2)int close(int fd);

调用close函数,关闭一个打开的文件。成功返回0,出错返回-1。

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

read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回实际传送的字节数量。

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

write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。

8.3 printf的实现分析

printf函数的函数体:

图8.1 printf函数

printf函数调用的vsprintf函数见图8.2。

vsprintf函数将所有的参数内容格式化之后存入buf,返回格式化数组的长度。write函数将buf中的i个元素写到终端。

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

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

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

图8.2 vsprintf函数

8.4 getchar的实现分析

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)” />}

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

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

getchar函数会从stdin输入流中读入一个字符。调用getchar时,会等待用户输入,输入回车后,输入的字符会存放在缓冲区中。第一次调用getchar时,需要从键盘输入,但如果输入了多个字符,之后的getchar会直接从缓冲区中读取字符。getchar的返回值是读取字符的ASCII码,若出错则返回-1。

8.5本章小结

本章主要介绍了linux的IO设备管理方法和及其接口和函数,对printf函数和getchar函数的底层实现做了基本的了解与概括。

(第8章1分)

结论

hello的“一生”从被程序员用高级语言写成hello.c源文件开始,其所经历的过程如下:

1)预处理

用预处理器cpp对hello.c源文件进行处理,根据以字符#开头的命令来修改原始的C程序,通过将外部头文件内容直接插入到文本等,生成hello.i文件。

2)编译

用编译器cc1将文本文件hello.i翻译成hello.s文件,该步骤是将高级语言程序转化为机器可直接识别处理执行的的机器码的中间步骤,会进行语法检查、目标程序优化等操作,得到汇编语言代码。

3)汇编

用汇编器as将hello.s文件翻译成机器语言指令,并将这些指令打包成可重定位目标程序文件hello.o。该步实现了将汇编语言翻译成计算机可以读懂的二进制机器语言。

4)链接

通过链接器ld将hello.o文件和动态链接库等链接整合成可执行文件hello,该步骤中主要两步是符号解析和重定位。

5)加载运行

在shell中键入./hello指令并赋予参数,bash进程调用fork函数生成子进程,并将在子进程中通过execve函数把代码和数据加载入虚拟内存空间。

6)执行程序

进程被调度后,CPU为hello分配时间片,在一个时间片中,hello获得对CPU的使用权,开始逐步执行其代码。在执行过程中,其会经历访存、动态内存申请、信号的接收管理等,如,在调用printf函数时,会调用molloc函数进行动态内存申请;在调用sleep函数时,会接收到信号被挂起等。

7)终止回收

子进程结束后,shell父进程会回收子进程,内核删除为hello进程创建的所有数据结构,实现从零来,归零去。

对hello“一生”的跟踪解析,也串起了csapp课程的整本书的学习内容,使我对于课程内容的体系结构有了系统性的了解,也加强了我对于课程钟的许多知识细节的把握能力。

附件

列出所有的中间产物的文件名,并予以说明起作用。

hello.c 源文件

hello.i 预处理后得到的文件;

hello.s编译后的汇编文件;

hello.o 汇编得到的可重定位目标文件;

hello 链接得到的可执行文件;

hello.elf 用readelf读取hello.o得到的ELF格式信息;

hello.txt 反汇编hello.o得到的反汇编文件

helloprog.elf 由hello可执行文件生成的.elf文件

helloprog.txt 反汇编hello可执行文件得到的反汇编文件

参考文献

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

[2] Pianistx.printf 函数实现的深入剖析[EB/OL].2013[2021-6-9].

[转]printf 函数实现的深入剖析 – Pianistx – 博客园.

[3] Internals. 内存地址转换与分段 [EB/OL].2009

内存地址转换与分段_drshenlei的博客-CSDN博客

[4] 数据结构.动态内存管理(内存的分配与回收)详解[EB/OL].2022

https://blog.csdn.net/ccc369639963/article/details/122408585

目录

第1章 概述

1.1 Hello简介

1.2 环境与工具

硬件环境:X64 CPU;2GHz;4GRAM;256Disk;

软件环境:Windows10 64位;Vmware 16;Ubuntu 20.04 LTS 64位;

工具:gcc;edb;as;cpp;cc1;gedit;GNU READELF等

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

正在上传…重新上传取消

图2.2.1 hello.i部分内容

图2.2.2 hello.i部分内容

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

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.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献