CSAPP大作业

在这里插入图片描述

计算机科学与技术学院
2021年5月
摘 要
当我们在文本编译器上编译好了一段C语言代码hello,想要在Linux系统下运行它,他先后经历了预处理、编译、汇编、链接这些过程,将hello.c翻译成可执行的目标文件hello。当我们在shell中输入./hello时,解析命令行参数发现该命令非内置命令,将其视为可执行文件。加载和运行过程调用fork函数创建进程、execve函数运行函数,通过内存映射、分配空间等手段让hello拥有自己的空间和时间,与其他程序并发地运行。
通过段式管理、页式管理,各存储器联动,CPU为执行文件hello分配时间周期,执行逻辑控制流,每条指令在流水线上取值、译码、执行、访存、写回、更新PC。内存管理单元和CPU处理器在执行过程中通过L1、L2、L3高速缓存和TLB、多级页表在物理内存中存取数据、指令,通过I/O系统输入输出。当程序运行结束时,shell回收进程,释放hello的内存并且删除有关进程上下文,至此hello结束了它辉煌的一生。

关键词:编译;可执行目标文件;逻辑控制流;

目 录

第1章 概述 - 4 -
1.1 Hello简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 5 -
1.4 本章小结 - 5 -
第2章 预处理 - 6 -
2.1 预处理的概念与作用 - 6 -
2.2在Ubuntu下预处理的命令 - 6 -
2.3 Hello的预处理结果解析 - 7 -
2.4 本章小结 - 8 -
第3章 编译 - 9 -
3.1 编译的概念与作用 - 9 -
3.2 在Ubuntu下编译的命令 - 9 -
3.3 Hello的编译结果解析 - 10 -
3.4 本章小结 - 16 -
第4章 汇编 - 17 -
4.1 汇编的概念与作用 - 17 -
4.2 在Ubuntu下汇编的命令 - 17 -
4.3 可重定位目标elf格式 - 17 -
4.4 Hello.o的结果解析 - 19 -
4.5 本章小结 - 26 -
第5章 链接 - 27 -
5.1 链接的概念与作用 - 27 -
5.2 在Ubuntu下链接的命令 - 27 -
5.3 可执行目标文件hello的格式 - 28 -
5.4 hello的虚拟地址空间 - 34 -
5.5 链接的重定位过程分析 - 35 -
5.6 hello的执行流程 - 38 -
5.7 Hello的动态链接分析 - 38 -
5.8 本章小结 - 40 -
第6章 hello进程管理 - 41 -
6.1 进程的概念与作用 - 41 -
6.2 简述壳Shell-bash的作用与处理流程 - 41 -
6.3 Hello的fork进程创建过程 - 42 -
6.4 Hello的execve过程 - 42 -
6.5 Hello的进程执行 - 43 -
6.6 hello的异常与信号处理 - 44 -
6.7本章小结 - 47 -
第7章 hello的存储管理 - 48 -
7.1 hello的存储器地址空间 - 48 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 48 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 49 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 50 -
7.5 三级Cache支持下的物理内存访问 - 52 -
7.6 hello进程fork时的内存映射 - 54 -
7.7 hello进程execve时的内存映射 - 54 -
7.8 缺页故障与缺页中断处理 - 54 -
7.9动态存储分配管理 - 55 -
7.10本章小结 - 58 -
第8章 hello的IO管理 - 59 -
8.1 Linux的IO设备管理方法 - 59 -
8.2 简述Unix IO接口及其函数 - 59 -
8.3 printf的实现分析 - 61 -
8.4 getchar的实现分析 - 61 -
8.5本章小结 - 62 -
结论 - 62 -
附件 - 64 -
参考文献 - 65 -

第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:
Hello程序是在Linux系统中,通过使用vim等文档编辑器编写C语言代码得到的程序也就是program,编写完成后保存就可以得到hello.c文件。
得到.c文件后,使用命令gcc -m64 -Og -no-pie -fno-PIC hello.c -o hello通过gcc编译器驱动程序读源程序文件hello.c,通过预处理(cpp)、编译器(ccl)、汇编器(as)以及连接器(ld),将hello.c翻译成可执行的目标文件hello。
在shell中输入命令行./hello执行hello,shell解析命令行参数,并构造argv以及环境变量envp,通过解析命令行参数发现该命令不是内置命令,将其视为可执行文件。由于最后一个参数不是’&’符号,程序不在后台运行,hello程序在前台运行。调用fork()函数创建子进程,其地址空间与shell父进程地址空间完全相同。调用execve()函数在当前进程的上下文加载并运行hello程序。通过内存映射、分配空间让hello拥有自己的空间和时间,与其他程序并发地运行。至此程序Program转换为进程Process,即P2P。
  
020:
这里的020指的是Process在内存中From Zero to Zero。产生子进程后shell调用execve()函数,在当前进程的上下文中加载并运行hello程序,将hello中的.data节、.text节、.bss节等内容加载到当前进程的虚拟地址空间。调用hello程序的main()函数,hello程序开始在该进程的上下文中运行。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构,这便是020的过程。

1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:
X64 CPU;2GHz;8GB RAM;512G HD Disk;

软件环境:
Windows10 64位;VirtualBox 6.1;Ubuntu 20.04 LTS 64位;

开发与调试工具:
  Visual Studio 2019 ;CodeBlocks17.12;vim8/gpedit+gcc;GDB;
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c: 源程序
  hello.i: 预处理后的文本文件
  hello.s: 编译后的文本文件
  hello.o: 汇编后的可重定位目标程序
  hello.txt: hello.o的反汇编文件
  hello1.txt: hello的反汇编文件
  helloelf.txt: ELF格式下的hello.o
  hello1elf.txt: ELF格式下的hello
  hello: 链接后的可执行目标文件
1.4 本章小结
本章简述了hello.c源程序P2P和O2O的过程,并且介绍实验的硬软件环境、开发和调试工具,以及中间所生成的相关文件。

第2章 预处理
2.1 预处理的概念与作用

概念:
预处理是指程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行响应的转换。预处理过程还会删除程序中的注释和多余的空白字符。典型地,由预处理器对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。

作用:
预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就是得到一个C程序,通常是以.i作为文件扩展名。可以概括为以下几部分作用:
1.将所有的#define删除,并展开所有的宏定义;
2.处理所有的预编译指令
3.处理#include预编译指令,将被包含的文件插入到预编译指令的位置;
4.添加行号信息文件名信息,便于调试;
5.删除所有的注释;

2.2在Ubuntu下预处理的命令

Ubuntu下的预处理命令为:gcc -E hello.c -o hello.i
在这里插入图片描述图2.1 Ubuntu下预处理命令截图
在这里插入图片描述

图2.2 生成的文件截图
2.3 Hello的预处理结果解析

查看上面得到的hello.i文件,主函数main代码保持不变,处理#include预编译指令,将被包含的文件插入到预编译指令的位置。所以代码由原来的短短几十行变成了3000多行。如图2.3所示:
在这里插入图片描述

图2.3 预处理结果截图
对于扩展头文件的部分,增加了大量对结构的定义,例如typedef、struct等,由于程序本身就没有define类型的语句,我们在hello.i文件里看不出来define语句在预处理的替换。如下图2.4 2.5所示:
在这里插入图片描述

图2.4 typedef定义截图
在这里插入图片描述

图 2.5 struct定义截图
2.4 本章小结
本章主要介绍了预处理的概念、作用,通过具体在Linux环境下将hello.c文件转换为hello.i文件,查看预处理阶段主要的作用,并对hello预处理结果进行解析。

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
概念:
  编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。生成的.s文件每条语句都以一种文本格式描述了一条低级机器语言指令。编译的过程实质上是把预处理文件进行词法分析、语法分析、语义分析、优化、目标代码生成以及出错处理,把C语言转换为成机器更好理解的汇编语言。
作用:
  通过词法分析、语法分析、语义分析、优化、目标代码生成以及出错处理这六个步骤,把.i文件变成更容易让机器理解并且更容易让程序员理解的汇编(.s)文件。让程序员更好地理解编译器的优化能力、分析代码中潜在的低效性。

3.2 在Ubuntu下编译的命令
Ubuntu下的编译命令为:gcc -S hello.c -o hello.s
在这里插入图片描述

图3.1 Ubuntu下编译命令截图

图 3.2 生成文件截图

3.3 Hello的编译结果解析
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。

3.3.1数据(常量、变量(全局/局部/静态)、表达式、类型、宏 )

(1)常量
hello.c源程序如下图3.3所示:
在这里插入图片描述

图 3.3 hello.c程序截图

从中我们可以知道printf中的格式串为:用法: Hello 学号 姓名 秒数!\n
Hello %s %s\n
在汇编文本文件中,这部分定义为.LC0,.LC1,并且两字符串均存放在.rodata段中。如下图3.4所示,根据UTF-8的编码规则,汉字被编码为三个字节,其它英文、空格、换行等字符与ASCII规则相同,编码为一个字节,但汇编文件保留原字符形式。

图3.4 汇编文本文件截图

(2)变量
hello.c源程序中i、argc、argv为局部变量,argc这个参数要求与4做比较,在汇编程序中存放在寄存器edi中,比较过程中放在栈中rbp-20的位置。argv地址放在了寄存器rsi中,使用时放在栈中rbp-32的位置上,如下图3.5所示:
在这里插入图片描述

图3.5 变量截图
变量i作为循环计数器,先初始化为0,然后每次与8比较判断循环是否结束,如果小于8,就执行i++操作,如下图3.6所示:

图3.6 变量截图

3.3.2赋值
对i的赋值,在循环for语句中i=0 、i++在汇编文件中使用movl语句对i进行初始化,如下图3.7所示:
在这里插入图片描述

图 3.7 赋值操作截图

3.3.3 类型转换
  在调运sleep函数过程中使用了atoi()函数,将字符串转换为整型,将argv[3]的值由字符串转换为整型,传递给sleep函数,在.s文件中如下图3.8所示体现为call atoi@PLT
在这里插入图片描述

图3.8 类型转换截图
3.3.4 算数操作
  对i进行了i++的运算操作,如图3.9所示,使用了addl语句:
在这里插入图片描述

图3.9 算术运算截图
3.3.5 关系操作
(1) argc!=4
  使用了cmpl语句设置条件码,je语句根据条件码做出相应的跳转,如下图3.10所示:
图3.10 argc操作截图
(2)i<8 如下图3.11所示,每次进行i++后与7进行比较cmpl语句,若<=继续进行执行jle语句:

图3.11 对i操作截图

3.3.6 数组操作
在printf中使用argv[1]、argv[2],argv地址放在了寄存器rsi中,使用时放在栈中rbp-32的位置。
当argc==4时在for循环中引用了argv数组。对argv的引用:argv[0]地址从栈使用movq语句传递给rax,计算argv[2]地址,使用addq %rax +16,加后的值是argv[2]地址,并对其引用,argv[2]值存放在rdx,同理argv[1]的值存放在rsi中。如下图3.12所示:
在这里插入图片描述

图3.12 数组操作截图

3.3.7 控制转移
控制转移出现在if(argc!=4)和for(i=0;i<8;i++)
(1)if(argc!=4)控制转移
如下图3.13所示,argc存在栈-20(%rbp)中与立即数4进行比较,若是不相等继续顺序执行,调用exit;相等则跳转至.L2执行for循环。
图3.13 argc控制转移截图
(2)for(i=0;i<8;i++)控制转移
  如下图3.14所示,在.L3所示部分通过cmpl比较,jle比较,若i<=7跳转至.L4执行循环中的迭代,否则继续顺序执行下面的语句。

图3.14 for控制转移截图

3.3.8 函数操作
(1)if(argc!=4)中对printf函数以及exit函数的调用
如下图3.15所示:函数首先调用printf函数,将.rodata节的.LC0的printf格式串的地址存进寄存器rdi,作为第一个参数方便函数printf调用。再将1存进寄存器edi作为exit函数的第一参数。

图3.15 函数操作截图

(2)for循环中对printf、atoi、sleep的函数调用
如下图3.16所示:函数将argv[1]、argv[2]的地址存进寄存器rsi、rdx,将printf格式串.LC1地址存进寄存器rdi,调用printf函数,再调用atoi函数将字符串准换为整数,并作为参数传递给sleep函数,最后调用sleep函数。

图3.16 函数调用截图
(3)调用getchar函数
如下图3.17所示:函数直接调用getchar函数,调用后将存储main函数返回值的eax寄存器置0。
在这里插入图片描述

图 3.17 函数调用截图

3.4 本章小结
本节介绍了编译的概念和作用,以及编译器通过编译将.i文件转换为汇编语言的.s文件的过程。同时解析了数据、赋值、类型转换、算术操作、关系操作、数组、控制转移、函数操作的汇编表示。加深了对汇编语句的进一步理解,以及和C语言之间的对应关系。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
概念:
汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存到目标文件hello.o中。hello.o文件是一个二进制文件,包含了程序的指令编码,如果我们在文本编译器中打开hello.o文件,将看到一堆乱码。
作用:
通过汇编这个过程,将汇编代码转换为计算机能够理解并执行的二进制机器代码,这个二进制机器代码是程序在本机器上的机器语言的表示。
4.2 在Ubuntu下汇编的命令
在Ubuntu下的汇编指令为:gcc -c -m64 -no-pie -fno-PIC hello.s -o hello.o
在这里插入图片描述

图4.1 Ubuntu下汇编指令截图

图4.2 生成文件截图
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
首先使用readelf命令查看hello.o的ELF格式,指令如下:readelf -a hello.o > helloelf.txt

图4.3 Ubuntu下指令截图

图4.4 生成文件截图

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

图4.5 ELF头截图
hello.o的ELF以一个16进制序列:7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00作为ELF头的开头。ELF头的大小为64字节,目标文件的类型为rel(可重定位文件)、机器类型为Advanced Micro Devices X86-64、节头部表的文件偏移为0,以及节头部表中条目的大小。
4.3.2 节头部表(section header table)
如下图4.6所示:头部表描述了13个节的相关信息,由表可以看出:

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

(2).rela.text节:一个.text节中的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指针都需要修改。另一方面,调用本地函数的指令则不需要修改。

(3).data节:已初始化的静态和全局C变量。布局C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss中。

(4).bss节:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。类型为NOBITS,意为暂时没有存储空间,旗标为WA,即权限为可分配可写。

(5).rodata节:存放只读数据,比如printf语句中的格式串和开关语句中的跳转表。类型为PROGBITS,意思是程序数据,旗标为A,即权限为可分配。

(6).comment节:包含版本控制信息。

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

(8).note.gnu.propert 节 :类型为NOTE。

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

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

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

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

(13).shstrtab节:该区域包含节区名称。

在这里插入图片描述

图4.6 节头部表截图

4.3.3 符号表
符号表由汇编器构造,使用编译器输出到汇编语言.s文件中的符号。每一个可重定位的目标模块都有一个符号表,它包含了该模块定义和引用的符号的信息。
hello.o程序的符号表包含编号Num、Value、Size、Type、Bind、Vis、Ndx、Name字段:
在这里插入图片描述

图4.7 符号表截图

该符号表共18个符号,其中:
hello.c为文件,ABS表示不能被重定位。main类型为变量,是一个位于.text节偏移量为0,大小为146个字节的全局符号。puts、exit、printf、atoi、sleep、getchar为NOTYPE未知类型,属于未定义符号。
4.3.4 .rela.text节和.rela.eh_frame节
代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中。
ELF定义了32中不同的重定位类型,这里我们只关心其中最基本的重定位类型:
(1)R_X86_64_PC32:重定位一个使用32位PC相对地址的引用。一个PC相对地址就是据程序计数器的当前运行值的偏移量。
(2)R_X86_64_32:重定位绝对引用。重定位时使用一个32位的绝对地址的引用,通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。

如下图4.8所示:第一个、第四个重定位条目为相对PC引用,其余均为绝对引用。

重定位PC相对引用重定位算法如下:
refaddr = ADDR(s) + r.offset;
*refptr = (unsigned) (ADDR(r.symbol) + r.addend – refaddr);

重定位绝对引用重定位算法如下:
*refptr = (unsigned) (ADDR(r.symbol) + r.addend);
  在这里以.rodata的重定位为例,它的重定位地址为refptr.则应先计算引用的运行时地址refaddr=ADDR(s)+ r.offset,.rodata的offset为0x1c,ADDR(s)是由链接器确定的。然后,更新引用,refptr = (unsigned) (ADDR(r.symbol) + r.addend – refaddr), .rodata的ADDR(r.symbol)也是由链接器确定的,addend查表可知为-4,refaddr已经算出来了,所以,.rodata的重定位地址我们就可以算出来了。
在这里插入图片描述

图4.8 重定位条目截图

4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
  
首先使用命令行:objdump -d -r hello.o > hello.txt获取hello.o的反汇编,如下图所示:
在这里插入图片描述

图4.9 Ubuntu下指令截图

图4.10 生成文件截图

图4.11 反汇编文件截图
获取hello.s如下图所示:
在这里插入图片描述

图4.12 hello.s截图

图4.13 hello.s截图

对比两版本对hello.c源程序的转换,可以发现,对于具体的语句类型,汇编与反汇编几乎一致,不同点如下:
(1)形式上:hello.s前没有一串二进制数,即相应的机器码,而反汇编代码前面有与之对应的机器码。
(2)数据上,立即数在hello.s这一汇编语言文本文件中为十进制,而在反汇编代码中为十六进制,如下图4.14 、4.15所示我们可以看到两者的不同之处。

图4.14 hello.s编码截图

图4.15 反汇编编码截图

(3)跳转方式不同,在汇编代码中,跳转方式如下:

图4.16汇编代码跳转截图
反汇编代码是依据地址跳转的,例如在反汇编代码中上述代码表述如下:
在这里插入图片描述

图4.17 反汇编代码跳转截图

(4)重定位条目:汇编代码仍然采用直接声明的方式,即通过助记符,例如

图4.18 重定位截图
而反汇编代码采用重定向的方式进行跳转,机器代码在此处留下一些地址以供链接时重定向:

图4.19 反汇编代码重定位截图

4.5 本章小结
本章了解程序汇编的概念和作用,以及进行汇编的过程、阅读了程序的ELF条目,了解了汇编、反汇编这两种相近而不相同的程序表现形式,深刻理解了汇编过程的重要性。

(第4章1分)

第5章 链接
5.1 链接的概念与作用
注意:这儿的链接是指从 hello.o 到hello生成过程。
概念:
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。链接执行符号解析、重定位过程。

作用:
链接使得分离编译成为可能,我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解成更小的、更好管理的模块,可以独立的修改和编译这些模块。当我们改变这些模块时,只需简单的重新编译它,并重新链接应用,可不必要重新编译其他文件。
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 Ubuntu下指令截图

图 5.2 生成文件截图
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

首先使用readelf命令查看hello的ELF格式,指令如下:readelf -a hello > hello1elf.txt

图5.3 Ubuntu下指令截图

图5.4 生成文件截图
5.3.1 ELF头
  ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
  如下图5.5所示,hello的ELF 头相关信息如下:
在这里插入图片描述

图5.5 ELF头截图

hello.o的ELF以一个16进制序列:7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00作为ELF头的开头。ELF头的大小为64字节,目标文件的类型为REL(可重定位文件)、机器类型为Advanced Micro Devices X86-64、节头部表的文件偏移为0,以及节头部表中条目的大小。

5.3.2 节头部表(section header table)
  如下图5.6所示,节头部表描述了26个节的相关信息,与hello.o的节头部表相比,多出来的部分:

在这里插入图片描述

图5.6 节头部表截图
(1)interp段:动态链接器在操作系统中的位置不是由系统配置决定,也不是由环境参数指定,而是由ELF文件中的 .interp段指定。该段里保存的是一个字符串,这个字符串就是可执行文件所需要的动态链接器的位置,常位于/lib/ld-linux.so.2。

(2)dynamic段:该段中保存了动态链接器所需要的基本信息,是一个结构数组,可以看做动态链接下 ELF文件的“文件头”。存储了动态链接会用到的各个表的位置等信息。

(3)dynsym段:该段与 “.symtab”段类似,但只保存了与动态链接相关的符号,很多时候,ELF文件同时拥有 .symtab 与 .synsym段,其中 .symtab 将包含 .synsym 中的符号。该符号表中记录了动态链接符号在动态符号字符串表中的偏移,与.symtab中记录对应。

(4)dynstr段:该段是 .dynsym 段的辅助段,.dynstr 与 .dynsym 的关系,类比与 .symtab 与 .strtab的关系 hash段:在动态链接下,需要在程序运行时查找符号,为了加快符号查找过程,增加了辅助的符号哈希表,功能与 .dynstr 类似

(5)rel.dyn段:对数据引用的修正,其所修正的位置位于 “.got”以及数据段(类似重定位段 “rel.data”)

(6)rel.plt段:对函数引用的修正,其所修正的位置位于 “.got.plt”。

5.3.3 符号表
符号表存放在程序中定义和引用的函数和全局变量的信息。如下图5.7所示,hello程序的符号表包含编号Num、Value、Size、Type、Bind、Vis、Ndx、Name字段。

在这里插入图片描述

图5.7 符号表截图

5.3.4 程序头表

图5.8 程序头表截图

5.3.5 Section to Segment mapping

图5.9 Section to Segment mapping截图
5.3.6 Dynamic section

图5.10 Dynamic section截图
5.3.7 重定位节(动态链接库)

图5.11 重定位截图

5.3.8 版本信息

图5.12 版本信息截图
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
使用edb加载hello,查看本进程的虚拟地址空间各段信息。如图5.13所示:
在这里插入图片描述

图5.13 edb查看hello各段信息
由图5.13可知,虚拟空间从0x400000开始。由图5.6可知,.interp段地址从0x4002e0,偏移量为0x2e0对齐要求为1,故而.interp段是从地址0x4002e0开始,查看0x4002e0处的值。如图5.14所示:
在这里插入图片描述

图5.14 .interp段
.text段地址从0x4010f0,偏移量为0x10f0,大小为0x145,对齐要求为16,故而.text段是从地址0x4010f0开始,查看0x4010f0处的值。如图5.15所示:

图5.15 .text段
.rodata段地址从0x402000,偏移量为0x2000,大小为0x3b,对齐要求为8,故而.rodata段是从地址0x402000开始,查看0x402000处的值。如图5.16所示:
在这里插入图片描述

图5.16 .rodata段
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。

使用命令行:objdump -d -r hello > hello1.txt

图5.17 Ubuntu指令截图
在这里插入图片描述

图5.18 生成文件截图
(1)与hello.o的反汇编代码相比,hello.o反汇编代码虚拟地址从0开始,而hello反汇编代码是从0x401000开始的。

图5.19 hello反汇编代码截图
(2)hello.o反汇编代码就直接是.text段,然后为main函数,如下图5.20所示:
在这里插入图片描述

图5.20 hello.o反汇编代码截图
而hello反汇编的结果中,由于链接过程中重定位而加入进来各种函数、数据。如开始的函数和调用的函数填充在main函数之前。

图5.21 hello反汇编代码截图

由机器语言反汇编代码、重定位信息与程序头中显式的内存地址,得到:

ADDR(.text_main) = 0x401125
ADDR(.puts) = 0x401090
Offset = 0x21
Addend = -4

可以得到等式:
*refptr = (unsigned)(ADDR(.puts)+addend - ADDR(.text_main)- Offset)
=(unsigned)(0x401090 - 0x4 - 0x401125 - 0x21)
=(unsigned)(0xffff46e8)
如下图所示,其与机器代码的反汇编结果中的0xffff46e8一致,同理可以得到其他重定位条目。

图5.22 重定位条目截图
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
名称 地址
ld-2.23.so!_dl_start 0x00007f8dec5b79b0
ld-2.27.so! dl_init 0x00007f8dec5c6740
hello!_start 0x004010f0
ld-2.27.so!_libc_start_main 0x00401080
libc-2.27.so! cxa_atexit 0x00007f8dec226280
hello!_libc_csu_init 0x00401020
hello!_init 0x00401000
libc-2.27.so!_setjmp 0x00007f8dec221250
libc-2.27.so!_sigsetjmp 0x00007f8dec221240
libc-2.27.so!__sigjmp_save 0x00007fa8dec221210
hello_main 0x00401125
hello!puts@plt 0x00401090
hello!exit@plt 0x004010d0
hello!printf@plt 0x004010a0
hello!sleep@plt 0x004010e0
hello!getchar@plt 0x004010b0
ld-2.23.so!_dl_runtime_resolve_avx 0x00007f8dec5cd870
libc-2.27.so!exit 0c00007f6002de35b0

5.7 Hello的动态链接分析

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
动态链接项目中,查看dl_init前后项目变化。对于动态共享链接库中PIC函数,编译器加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略,将过程地址的绑定推迟到第一次调用该过程。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,初始时每个GOT条目都指向对应的PLT条目的第二条指令。如图5.23所示:
在这里插入图片描述

图5.23 动态链接
  在dl_init调用之后, 0x6008c0和0x6008c0处的两个8字节的数据分别发生改变。
  和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]是动态链接器ld-linux.so模块中的入口点。
  在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问时,GOT地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。

5.8 本章小结
本章说明了链接的概念和作用,并完成了对hello.o的链接工作。使用Ubuntu下的链接指令可以将其转换为可执行目标文件,查看李ELF文件里面的内容,其中用到了puts的重定位条目,最终分析了程序如何实现的动态链接。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程的经典定义是一个执行中程序的实例,系统的每个程序都运行在某个进程的上下文。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存里的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。

作用:
通过进程,我们会得到一种假象,就好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占的使用处理器和内存。处理器就好像是无间断得一条接一条的执行我们程序中的指令。我们程序中的代码和数据好像是系统内存中唯一的对象。

6.2 简述壳Shell-bash的作用与处理流程
在计算机科学中,Shell俗称壳,是指“为使用者提供操作界面”的软件。它类似于DOS下的command.com和后来的cmd.exe。它接收用户命令,然后调用相应的应用程序。同时它又是一种程序设计语言。作为命令语言,它交互式解释和执行用户输入的命令或者自动地解释和执行预先设定好的一连串的命令;作为程序设计语言,它定义了各种变量和参数,并提供了许多在高级语言中才具有的控制结构,包括循环和分支。
Bash是一个命令处理器,通常运行于文本窗口中,并能执行用户直接输入的命令。Bash还能从文件中读取命令,这样的文件称为脚本。和其他Unix shell 一样,它支持文件名替换(通配符匹配)、管道、here文档、命令替换、变量,以及条件判断和循环遍历的结构控制语句。包括关键字、语法在内的基本特性全部是从sh借鉴过来的。其他特性,例如历史命令,是从csh和ksh借鉴而来。总的来说,Bash虽然是一个满足POSIX规范的shell,但有很多扩展。
处理流程:
(1)用户输入命令。
(2)shell对用户输入命令进行解析,判断是否为内置命令。
(3)若为内置命令,调用内置命令处理函数,否则调用execve函数创建一个子进程进行运行。
(4)判断是否为前台运行程序,如果是,则调用等待函数等待前台作业结束;否则将程序转入后台,直接开始下一次用户输入命令。
(5)shell应该接受键盘输入信号,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程

当shell运行一个程序时,父进程通过fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和新创建的子进程之间最大的区别在于他们有不同的PID。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。execve函数加载并运行可执行文件filename(hello),且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。
Argv变量指向一个以null结尾的指针数组,其中每个指针都指向一个参数字符串。环境变量的列表是由一个类似的数据结构表示的。Envp变量指向一个以null结尾的指针数组,其中每个指针指向一个环境变量字符串。
在execve加载了hello之后,他调用启动代码,启动代码设置栈,并将控制传递给新进程的主函数。当main开始执行时,用户栈结构如下图6.1所示:
在这里插入图片描述

图6.1 用户栈结构示意图
首先是参数和函数字符串。栈往上紧随其后的是以null结尾的指针数组,其中每个指针都指向栈中的一个环境变量字符串。全局变量environ指向这些指针中的第一个envp[0]。紧随环境变量数组之后的是以null结尾的argv[]数组,其中每个元素都指向栈中的一个参数字符串。

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

操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。内核为每个进程维持一个上下文。上下文就是内核重启一个被抢占的进程所需得状态。他由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构。
当进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个决策称为调度,是由内核中称为调度器的代码处理的。
多个流并发地执行的一般现象被称为并发。一个进程和其他进轮流运行的概念称为多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。
hello程序与操作系统其他进程通过内核中调度器的调度,切换上下文,拥有各自的时间片从而实现并发运行。所以hello在sleep时就是这样切换的。
简单看hello sleep进程调度的过程:当调用sleep之前,如果hello程序不被抢占则顺序执行,假如发生被抢占的情况,则进行上下文切换来将控制转移到新的进程,上下文切换是由内核中调度器完成的,当内核调度新的进程运行后,它就会抢占当前进程,并进行1)保存以前进程的上下文2)恢复新恢复进程被保存的上下文,3)将控制传递给这个新恢复的进程,来完成上下文切换。
hello初始运行在用户模式,在hello进程调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当休眠时间到了就发送一个中断信号,此时进入内核状态执行中断处理,表示休眠时间已,此时内核执行一个从其他进程到hello进程的上下文切换,将控制返回给hello进程,hello进程就可以继续进行自己的控制逻辑流了。
当hello调用getchar的时候,实际是stdin的系统调用read,hello之前运行在用户模式,在进行read调用之后陷入内核,内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。此时进入内核模式,内核执行上下文切换,切换到其他进程。当完成键盘缓冲区到内存的数据传输时,引发一个中断信号,此时内核从其他进程进行上下文切换回hello进程。

6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.6.1不停乱按
如果乱按过程中没有回车,这个时候只是把输入屏幕的字符串缓存起来,如果输入最后是回车,getchar读回车,并把回车前的字符串当作shell输入的命令。
在这里插入图片描述

图6.2 不停乱按截图
6.6.2 Ctrl-Z
  如下图,如果输入Ctrl+Z会发送一个SIGTSTP信号给前台进程组的每个进程,结果是停止前台作业,即我们的hello程序。

图6.3 输入Ctrl-Z截图

6.6.3 Ctrl-C
  如下图,如果在程序运行过程中输入Ctrl+C,会让内核发送一个SIGINT信号给到前台进程组中的每个进程,结果是终止前台进程。
图6.4 输入Ctrl-C截图

6.6.4 Ctrl-Z后可以运行ps jobs pstree fg kill 等命令

图6.5 输入Ctrl-Z后输入ps、jobs截图

图6.6 输入Ctrl-Z后输入pstree截图
在这里插入图片描述

图6.7 输入Ctrl-Z后输入fg截图

6.7本章小结
本章介绍了进程的概念和作用,描述了shell作用与处理流程,讲述了shell的基本操作以及各种内核信号和命令。还概括了shell是如何fork新建子进程、execve如何执行进程、hello进程的上下文切换,以及hello的异常与信号处理。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:
在有地址变换功能的计算机中,访问指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。要经过寻址方式的计算或变换才得到内存储器中的物理地址。

线性地址:
也叫虚拟地址,和逻辑地址类似,也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件也是内存的转换前地址。线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。

虚拟地址:
CPU启动保护模式后,程序运行在虚拟地址空间中。注意,并不是所有的“程序”都是运行在虚拟地址中。CPU在启动的时候是运行在实模式的,Bootloader以及内核在初始化页表之前并不使用虚拟地址,而是直接使用物理地址的。

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

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

7.3 Hello的线性地址到物理地址的变换-页式管理
CPU中的一个控制寄存器,页表基址寄存器指向当前页表。n位的虚拟地址包括两部分,一个p位的虚拟页面偏移和一个n-p位的虚拟页号。MMU利用VPN来选择当前的PTE,将页表条目中物理页号和虚拟地址中的VPO串联起来,就得到了相应的物理地址。如下图7.1所示:
当前页面命中时,分为以下五个步骤:
(1)处理器生成一个虚拟地址,并把它传递给MMU。
(2)MMU生成PTE地址,并从高速缓存/主存处请求得到它。
(3)高速缓存/主存向MMU返回PTE。
(4)MMU构造物理地址,并把它传递给高速缓存/主存。
(5)高速缓存/主存返回所请求的数据字给处理器。
在这里插入图片描述

图7.1 页式管理示意图

7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,MMU就必须查阅一个PTE,以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。
TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单一PTE组成的块。TLB通常有高的相联度,从虚拟地址中的页号提取出组选择和行匹配的索引和标记字段。因为所有的地址翻译都是在芯片上的MMU中进行的,因此非常快。
多级页表:虚拟地址被划分为k个PVN 和一个VPO,每个VPN i都是一个到第i级页表的索引,第j级页表中的每个PTE,都指向第j+1级的某个页表的基址。第k级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问k个PTE。由图7.2,展示的是一个两级页表的层次结构。

图7.2 两级页表层次结构图
Core i7是四级页表进行的虚拟地址转物理地址。虚拟地址空间 48 位,物理地址空间 52 位,页表大小 4KB,4 级页表。TLB 4 路 16 组相联。CR3 指向第一级页表的起始位置。
解析前提条件:由一个页表大小 4KB,一个 PTE 条目8B,共 512 个条目,使 用 9 位二进制索引,一共4个页表共使用36位二进制索引,所以 VPN 共36 位,因为VA 48位,所以VPO 12 位;因为TLB共16组,所以 TLBI 需4位,因为VPN 36 位,所以TLBT 32位。
CPU 产生虚拟地址 VA,VA 传送给 MMU,MMU 使用前 36 位 VPN 作为 TLBT(前 32 位)+TLBI(后 4 位)向 TLB 中匹配,如果命中,则得到 PPN (40bit)与 VPO(12bit)组合成 PA(52bit)。 如果 TLB 中没有命中,MMU 向页表中查询,CR3 确定第一级页表的起始地 址,VPN1(9bit)确定在第一级页表中的偏移量,查询出 PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查 询到 PPN,与 VPO 组合成 PA,并且向 TLB 中添加条目。如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。
在这里插入图片描述

图7.3 Core i7是四级页表示意图
7.5 三级Cache支持下的物理内存访问
对于一个虚拟地址请求,首先将去TLB寻找,看是否已经在TLB中缓存。如果命中的话就直接MMU获取,没有命中的话就先在结合多级页表,得到物理地址,去cache中找,到了L1里面以后,寻找物理地址又要检测是否命中,不命中则紧接着寻找下一级cache L2,接着L3。这里就是使用到CPU的高速缓存机制了,一级一级往下找,直到找到对应的内容。大体过程如下图7.4所示:
在这里插入图片描述

图7.4 Core i7是四级页表示意图

L1 Cashe的物理访存大致过程如下:
(1) 在得到物理地址后选取物理地址中的组索引位,将二进制组索引转化为一个无符号整数,找到相应的组

(2) 把物理地址的标记位和相应的组中所有行的标记位进行比较,当物理地址的标记位和高速缓存行的标记位匹配时,而且高速缓存行的有效位是1,则高速缓存命中。

(3) 一旦高速缓存命中,我们就知道我们要找的字节在这个块的某个地方。因此块偏移位提供了第一个字节的偏移。把这个字节的内容取出返回给CPU即可

(4)不命中如果高速缓存不命中,那么需要从存储层次结构中的下一层取出被请求的块,然后将新的块存储在组索引位所指示的组中的一个高速缓存行中。一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略 LFU 进行替换。

7.6 hello进程fork时的内存映射
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve 函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
删除已存在的用户区域。删除当前进程(shell)虚拟地址的用户部分中的已存在的区域结构。
映射私有区域。为hello程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。
映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
设置程序计数器(PC) 。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
下一次调度hello进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

7.8 缺页故障与缺页中断处理
虚拟内存中,DRAM缓存不命中称为缺页。如图7-13,CPU需要引用VP3中的一个字,通过读取PTE3,发现有效位为0,说明不在内存里,这时就发生了缺页异常。缺页异常发生时,通常会调用内核里的缺页异常处理程序,该程序会选择一个牺牲页,这里是存放在PP3的VP4,如果VP4已经被修改,内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4的页表条目,反映出VP4不再缓存在内存里。
在这里插入图片描述

图7.5 缺页故障示意图
接下来,内核从磁盘复制VP3到内存中的PP3,更新PTE3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令。
缺页处理程序不是直接就替换,它会经过一系列的步骤:
虚拟地址是合法的吗?如果不合法,它就会触发一个段错误
试图进行的内存访问是否合法?意思就是进程是否有读,写或者执行这个区域的权限?
经过上述判断,这时才能确定这是个合法的虚拟地址,然后才会执行上述的替换。

7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域称为堆(heap),系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk(读做"break"),它指向堆的顶部。

图7.6 堆示意图
分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块 保持空闲,直到它显式地被应用所分配。一个已分配 的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
显式分配器(explicitallocator):要求应用显式地释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来
分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。
隐式分配器(implicitallocator):另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbagecollec­tor),而自动释放未使用的巳分配的块的过程叫做垃圾收集(garbagecollection)。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。

隐式空闲链表:
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。
在这里插入图片描述

图7.7 带边界标记的隐式空闲链表示意图
在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。
在这里插入图片描述

图7.8 隐式空闲链表示意图

显式空间链表:
显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。
在这里插入图片描述

图7.9 显示空闲链表组织结构
在显式空闲链表中,一种方法使用后进先出的顺序维护链表,将新释放的块在链表的开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过 的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界 标记,那么合并也可以在常数时间内完成。
按照地址顺序来维护链表,其中链 表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要 线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比 LIFO 排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。
在这里插入图片描述

图7.10 显示空闲链表
7.10本章小结
在本章中总结了有关内存管理的知识,讲述了在hello运行的64位系统中内存管理方法,虚拟内存和物理内存之间的关系,了解intel环境下的段式管理和页式管理、fork和exceve的内存映射,知道了缺页故障和缺页中断管理机制,了解了如何根据缓存或页表寻找物理内存。
(第7章 2分)

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

打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可以用来代替显式的描述符值。

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

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

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

Unix I/O接口函数:
(1)进程是通过调用open函数来打开一个存在的文件或者创建一个新文件的,函数声明如下:
int open(char *filename, int flags, mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件。
mode参数指定了新文件的访问权限位。作为上下文的一部分,每个进程都有一个umask,它是通过调用umask函数来设置的。当进程通过带某个mode参数的open函数调用来创建一个新文件时,文件的访问权限位被设置成mode&~umask。
(2)进程通过调用close函数关闭一个打开的文件。函数声明如下:
int close(int fd);
成功返回0错误返回EOF
(3)应用程序是通过分别调用read和write函数来执行输入和输出的。函数声明如下:
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的当前文件位置。
(4)通过调用lseek函数,应用程序能够显式地修改当前文件的位置。函数声明如下:
off_t lseek(int handle, off_t offset, int fromwhere);

8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
在这里插入图片描述

图8.1 printf函数
printf需要做的事情是:接受一个fmt的格式,然后将匹配到的参数按照fmt格式输出。printf用了两个外部函数,一个是vsprintf,还有一个是write。
vsprintf函数作用是接受确定输出格式的格式字符串fmt(输入)。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
write函数将buf中的i个元素写到终端。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析
在这里插入图片描述

图8.2 getchar函数
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止。
当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。

8.5本章小结
本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,对printf函数和getchar函数的实现进行了分析。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

Hello所经历的历程:
1.预处理:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行响应的转换。
2.编译:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。生成的.s文件每条语句都以一种文本格式描述了一条低级机器语言指令。
3.汇编:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存到目标文件hello.o中。
4.链接:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载到内存并执行。
5.创建进程:我们在shell上输入./hello,通过解析命令行参数,由于第一个参数不是shell的内置命令,所以shell会认为hello是一个可执行目标文件,父进程通过fork函数创建一个新的运行的子进程。
6.加载程序:execve函数在当前进程的上下文中加载并运行一个新程序。execve函数加载并运行可执行文件filename(hello),且带参数列表argv和环境变量envp。
7.运行hello时,内存管理单元MMU、翻译后备缓冲器TLB、多级页表机制、三级cache共同作用,完成对地址的请求。
8.结束:当hello运行完毕,shell父进程回收hello,内核删除为这个进程创建的所有数据结构。

感悟:通过走完hello的一生,对计算机如何处理一个程序有了彻彻底底的领悟,也将一学期学习的内容串联起来了,在头脑中形成了一个连贯的体系,受益无穷。
(结论0分,缺失 -1分,根据内容酌情加分)

附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c: 源程序
  hello.i: 预处理后的文本文件
  hello.s: 编译后的文本文件
  hello.o: 汇编后的可重定位目标程序
  hello.txt: hello.o的反汇编文件
  hello1.txt: hello的反汇编文件
  helloelf.txt: ELF格式下的hello.o
  hello1elf.txt: ELF格式下的hello
  hello: 链接后的可执行目标文件

(附件0分,缺失 -1分)

参考文献
[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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
哈尔滨工业大学(Harbin Institute of Technology,简称“哈工大”)是中国著名的重点大学,成立于1920年,是中国最早创办的六所工科高等学府之一。其中,哈尔滨工业大学的计算机科学与技术学院一直以来都是国内知名的学院。在其中,CSAPP是哈工大计算机科学与技术学院开设的一门经典课程,全称为《深入理解计算机系统》(Computer Systems: A Programmer's Perspective)。 这门课程涵盖了计算机系统的各个方面,从高级语言编程到机器级别的细节都有涉及,深入剖析了计算机系统的内部机制,讲解了各种计算机组件的原理,如内存、处理器、I/O设备、网络等等。此外,课程内容还包括缓存、异常、程序优化、并发编程、虚拟内存等重要主题,并且还会涉及安全问题,例如注入攻击、缓冲区溢出等等。 相较于其他计算机相关的课程而言,CSAPP的特殊之处在于,它以程序员的视角,深入而生动地解释了计算机系统的工作方式和内部机制。课程强调了实践性,通过大量的例子及编程作业,学生可以实际操作并理解到具体的计算机系统的运行方式。 此外,CSAPP的教学团队非常强大,由哈工大的多位顶尖教授组成,能够保证教学质量和深度。学生通过学习这门课程,不仅可以深入了解计算机系统的各个方面,还可以提高编程能力和工程实践水平,有助于更好地应对工作中遇到的各种问题。 总之,CSAPP是哈尔滨工业大学计算机科学与技术学院开设的一门经典课程,其全面而深入的课程内容、强调实践性、优秀的教学团队等特色让其在国内享有较高声誉,对学生深入理解计算机系统、提高编程实践能力等方面,都有非常积极的作用。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值