计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 网络空间安全
学 号 2021111161
班 级 2103901
学 生 李睿博
指 导 教 师 吴锐
计算机科学与技术学院
2023年5月
本文以hello.c程序的一生为线索,从其原始程序开始,依次分析其预处理、编译、汇编、链接、加载与执行直到终止并回收的过程。本文基于《深入了解计算机系统》一书,以Ubuntu系统为研究环境,对hello.c的一生进行分析,以此串连起CSAPP全书的知识并加以实践,深入体会整个计算机系统体系结构。
关键词:计算机系统;程序的一生;Linux ;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 5 -
3.2 在Ubuntu下编译的命令............................................................................. - 6 -
4.2 在Ubuntu下汇编的命令............................................................................. - 7 -
5.2 在Ubuntu下链接的命令............................................................................. - 8 -
5.3 可执行目标文件hello的格式.................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 10 -
6.3 Hello的fork进程创建过程..................................................................... - 10 -
6.6 hello的异常与信号处理............................................................................ - 10 -
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 -
8.1 Linux的IO设备管理方法.......................................................................... - 13 -
8.2 简述Unix IO接口及其函数....................................................................... - 13 -
第1章 概述
1.1 Hello简介
1.1.1 hello的P2P
Hello的P2P是指hello从Program(程序)到Progress(进程)的过程,hello会经过预处理、编译、汇编、链接最终成为可执行目标文件hello,接着在shell下输入命令./hello 2021111161 李睿博 1,执行hello,shell会fork出一个子进程,并execve把hello载入进程上下文中开始执行,至此hello完成了从程序变成进程的过程。
1.1.2 hello的020
Hello的020是指hello从一开始在内存中并无内容,from 0,再到编译为可执行目标文件,被shell调用fork、execve函数加载、执行,直到终止回收释放空间,to 0,尘归尘土归土的过程。
1.2 环境与工具
处理器:Intel® Core™ i7-8550U CPU @ 1.80GHz 1.99GHz
RAM:16.00GB
系统类型:64位操作系统
软件环境:Windows10 64位;Ubuntu 16.04
开发与调试工具:
Linux下:gcc,as,ld,vim,edb,readelf,codeblocks
Windows下:Visual Studio
1.3 中间结果
文件名 | 作用 |
hello.i | 预处理后的文本文件 |
hello.s | 编译后的汇编语言文件 |
hello.o | 汇编后的可重定位目标文件 |
hello.asm | 可重定位目标文件的反汇编 |
hello.elf | 可重定位目标文件的ELF文件 |
hello_.asm | 可执行目标文件的反汇编 |
hello_.elf | 可执行目标文件的ELF文件 |
1.4 本章小结
本章主要介绍了hello的P2P与020的概念,并阐述了本文所基于的软硬件环境,同时给出了本文研究过程产生的中间文件及作用。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
2.1.1 预处理的概念
预处理是指在程序编译之前对源代码进行处理的过程。预处理器可以读取源代码文件,执行预处理指令,然后生成一个被编译器用来生成可执行代码的.i文件。
在C语言中,预处理器会扫描源代码文件,并执行以“#”开头的指令,例如#include和#define。这些指令可以在源代码中引入外部文件或定义宏,以便在编译时使用。预处理器生成的.i文件可以进一步被编译器处理,生成最终的可执行代码。
以hello.c为例,在预处理过程中,预处理器会扫描源代码文件,并执行#include指令,将系统头文件包含到文件中,同时会用实际值替换#define定义的串,并删去注释与空白字符。得到的.i文件仍然是一个文本文件。
2.1.2 预处理的作用
1.宏替换:预处理器可以通过宏定义来将代码中的标识符替换为指定的文本。这样可以使代码更加简洁和易于阅读,同时也可以减少代码中的重复部分。
2.文件包含:预处理器可以通过#include指令将一个源代码文件包含到另一个源代码文件中。这样可以将代码分成多个文件,使其更加模块化和易于管理。
3.条件编译:预处理器可以通过条件编译指令如#define和#ifdef等来控制编译过程中的代码执行。这样可以使程序在不同的编译环境下执行不同的代码,或者根据不同的条件编译不同的代码。
4.编译指令:预处理器还可以通过编译指令如#pragma等来控制编译器的行为,例如优化级别、内存对齐等。
2.2在Ubuntu下预处理的命令
Ubuntu中进行预处理的命令为:cpp hello.c>hello.i
正在上传…重新上传取消正在上传…重新上传取消
图1 预处理命令的执行结果
2.3 Hello的预处理结果解析
正在上传…重新上传取消正在上传…重新上传取消
图 2 预处理文件展示
观察到生成的hello.i文件被拓展到了3060行,相比hello.c文件文本量大幅增加,而从3047行开始才是main函数部分,在这之前的部分为#include命令指定的头文件展开,预处理器会在环境变量中寻找这些头文件,然后删除#include指令,并将找到的头文件加载到hello.i文件中。同时可以观察到hello.c中的注释语句也被删除了。
2.4 本章小结
本章对预处理的概念与作用进行了解析,并在Ubuntu中执行预处理命令,以hello.c转变到hello.i的过程对预处理结果进行了解析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
3.1.1 编译的概念
编译器将预处理器生成的中间文件作为输入,将其转换为汇编代码。编译器会进行语法分析、语义分析、类型检查、优化等操作,以确保生成的汇编代码能正确地实现源代码的逻辑功能。
3.1.2 编译的作用
将预处理后的文本文件翻译为汇编语言程序,同时可以进行语法分析、语义分析、类型检查、优化等操作,为之后生成机器代码做准备。
3.2 在Ubuntu下编译的命令
在Ubuntu下进行编译的命令是:gcc -S hello.i -o hello.s
正在上传…重新上传取消正在上传…重新上传取消
图 3 编译命令的执行结果
3.3 Hello的编译结果解析
3.31 文件信息记录
正在上传…重新上传取消正在上传…重新上传取消
图 4 hello.s的文件信息
表格 1 hello.s的文件结构
内容 | 含义 |
.file | 表明源文件 |
.text | 代码段 |
.section/.rodata | 存放只读变量 |
.align | 对齐方式:8字节对齐 |
.string | 字符串 |
.global | 全局变量 |
.type | 声明main是函数类型 |
3.3.2 对局部变量int的操作
正在上传…重新上传取消正在上传…重新上传取消
图 5 对局部变量int的操作
局部变量int i存储在栈上,在进入main函数时会在栈上申请一部分空间用于存储,等到执行结束后释放空间。在汇编代码中可以看到,首先跳转到.L3,让栈指针%rbp-4为局部变量i申请空间,然后再跳转到.L4进行接下来的操作。
3.2.3 对立即数的操作
正在上传…重新上传取消正在上传…重新上传取消
图 6 对立即数的操作
对立即数直接使用$ +数字即可。
3.2.4 对字符串的操作
正在上传…重新上传取消正在上传…重新上传取消
图 7 字符串的存储位置
正在上传…重新上传取消正在上传…重新上传取消
图 8 加载字符串的首地址
在.rodata部分的LC0及LC1中存储了字符串常量,在后续main函数使用时,通过lea指令加载字符串的首地址。
3.2.5 赋值操作
对局部变量的赋值通过mov指令完成,不同宽度的数据类型对应不同的mov指令。对于局部变量i,int类型的赋值用四字节的movl指令完成。
3.2.6 对数组的操作
正在上传…重新上传取消正在上传…重新上传取消
图 9 对数组的操作
在汇编代码中,对数组的操作是通过找到数组的基地址,再加上偏移量访存进行操作的。图中对应main函数中对argv数组的操作,首先将栈中的数组首地址传入rax,再将rax加上偏移量,例如加立即数8,对应argv[1]地址,再进行访存得到实际值。
3.2.7 对调用函数的操作
正在上传…重新上传取消正在上传…重新上传取消
图 10 对调用函数传参的操作
调用函数时,前六个参数通过寄存器来传递,若有其他的参数通过栈来传递。在main函数对printf函数进行调用时,传递了三个参数,第一个参数是字符串被存入%rdi,其余两个参数为argv[2],argv[3],通过%rsi和%rdx传递。
调用时需要保存当前寄存器的值,分为调用者保存和被调用者保存。函数返回时,返回值存于%rax。
3.2.8 对循环的操作
Main函数中使用的for循环,通过一个循环变量每次循环自增一,并与循环条件比较后进行跳转实现的。如图中所示,%rbp-4中存放循环变量,每个循环后加一,并于7进行比较,只有在小于等于7的时候才会继续循环,否则退出循环。
正在上传…重新上传取消正在上传…重新上传取消
图 11 对循环的操作
3.4 本章小结
本章主要介绍了编译的概念与作用,并结合hello.s的汇编代码,剖析了编译的过程以及编译器如何对各种数据类型和操作进行处理的。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
汇编是指将汇编语言程序作为输入,将汇编语言指令翻译成机器语言指令,并将生成的目标代码输出为可执行文件。
4.1.2 汇编的作用
1. 将汇编语言指令翻译成机器语言指令:汇编器将汇编语言程序中的助记符(如MOV、ADD等)翻译成相应的机器语言指令,如“MOV AX, BX”可以翻译成“8B C3”。
2. 解析符号:汇编器会解析汇编语言程序中的符号(如函数名、变量名等),然后进行重定位,将其映射到相应的内存地址,以便生成正确的目标代码。
3.生成目标文件:汇编器会将翻译后的机器语言指令、符号和重定位信息等内容组合成目标文件,该文件可以被链接器(ld)用于生成可执行文件。
4.2 在Ubuntu下汇编的命令
在Ubuntu下汇编的命令为:gcc -c hello.s -o hello.o
正在上传…重新上传取消正在上传…重新上传取消
图 12 汇编命令的结果
4.3 可重定位目标elf格式
4.3.1 可重定位目标文件的一般格式
节头 | 含义 |
ELF头 | 描述系统字大小和字节顺序,以及ELF头大小、目标文件类型等 |
.text | 已编译程序的机器代码 |
.rodata | 只读数据 |
.data | 已初始化的全局变量和局部静态变量 |
.bss | 未初始化的全局变量和局部静态变量 |
.symtab | 符号表 |
.rel.text | .text节的重定位信息 |
.rel.data | .data节的重定位信息 |
.debug | 调试符号表 |
.line | 原始程序的行号和机器指令之间的映射 |
.strtab | 字符串表 |
节头表 | 每个节的节名、偏移和大小 |
4.3.2 使用readelf分析hello.o
正在上传…重新上传取消正在上传…重新上传取消
图 13 readelf命令执行结果
4.3.2-1 ELF头
正在上传…重新上传取消正在上传…重新上传取消
图 14 ELF头部分
ELF文件的头部是用来描述整个文件的元数据结构,它包含了文件的基本信息,如文件类型、目标机器类型、入口地址、节头表偏移量等,提供了对整个ELF文件的描述和管理。ELF文件帮助链接器和加载器正确地处理ELF文件,从而实现可执行文件或共享库的正确链接和加载。
4.3.2-2 节头
正在上传…重新上传取消正在上传…重新上传取消
图 15 节头部分
ELF文件的节头部分描述了文件中各个节的数据信息,包括节的名称、类型、大小、偏移量、对齐方式、访问权限和链接属性等。ELF文件中的不同类型的节在链接和加载过程中会被处理成不同的方式,因此节头部分对于程序的链接和加载过程具有重要的作用。
4.3.2-3 重定位节
重定位节是用来描述程序在链接时需要进行的地址修正操作,它包含了需要进行地址修正的符号、修正类型、修正偏移量等信息。通过重定位节,链接器可以将各个模块中的符号引用与符号定义进行关联,并生成正确的重定位表,确保程序在运行时能够正确地访问和调用各个符号。
以.rela.text为例,偏移量是指需要进行重定向的代码在.text节的偏移位置;信息包括symbol和type两部分,前一部分是目标在.symtab中的偏移量,type表明重定位的类型,在CSAPP中介绍了相对重定位和绝对重定位两种,在该文件中只涉及相对重定位(PC32与PLT32是同一种重定位方式);加数为进行重定位时也要参与计算的一个量。
书中给出了相对重定位的计算公式,为:
refaddr = ADDR(s) + r.offset
*refptr = ADDR(r.symbol) + r.addend – refaddr
正在上传…重新上传取消正在上传…重新上传取消
图 16 重定位节部分
4.3.2-4 符号表
正在上传…重新上传取消正在上传…重新上传取消
图 17 符号表部分
4.4 Hello.o的结果解析
正在上传…重新上传取消正在上传…重新上传取消
图 18 反汇编命令执行结果
使用objdump -d -r hello.0 > hello.asm命令生成hello.o的反汇编文件。
对比反汇编文件与汇编文件,存在以下几点不同:
- 列出了对应的机器语言:机器代码在左侧用16进制显示出来,由操作码和操作数组成。
- 函数调用需要进行重定位操作:例如20行的call语句,对应机器码为0,并给出了相对重定位的标志,说明需要在链接时进行相对重定位,确定函数在内存中的位置。
- 跳转指令不再使用段名称:在反汇编文件中,跳转指令的机器码直接使用目标地址与当前地址之差,不再使用段名称跳转。
- 对全局变量的访问需要进行重定位:访问.rodata节的数据时,机器语言的操作码为0,需要进行重定位。
除此以外,反汇编代码与汇编代码基本一致。
正在上传…重新上传取消正在上传…重新上传取消
图 19 部分反汇编文件
4.5 本章小结
本章主要介绍汇编的概念与作用,以及生成的可重定位目标文件的基本格式与内容,同时对比了汇编文件与反汇编文件,分析其中的差异与原因。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
5.1.1 链接的概念
链接是指将多个目标文件或共享库组合成一个可执行文件或共享库的过程。分为静态链接和动态链接,主要任务是将各个目标文件中的符号引用与符号定义关联起来,生成符号表,并进行地址重定位操作。
5.1.2 链接的作用
通过链接,程序员可以将程序分成多个模块,每个模块负责不同的功能,从而降低程序的复杂度,并提高代码的可读性和可维护性。链接还可以将符号引用与符号定义关联起来,计算出符号的实际地址,并将符号地址写入可执行文件或共享库中,从而完成地址重定位操作。
5.2 在Ubuntu下链接的命令
在Ubuntu下链接的命令为:
ld -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 /usr/lib/gcc/x86_64-linux-gnu/9/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello
正在上传…重新上传取消正在上传…重新上传取消
图 20 链接命令执行结果
5.3 可执行目标文件hello的格式
通过readelf -a hello >hello_.elf 指令生成hello的ELF格式文件,为了与可重定位目标文件进行区分,命名为hello_.elf。
可执行目标文件格式 |
ELF头 |
段头部表 |
.init |
.text |
.rodata |
.data |
.bss |
.symtab |
.debug |
.line |
.strlab |
节头部表 |
5.3.1 ELF头
与hello.elf相比较,hello_.elf的基本信息如Magic、类别等未发生改变,但其类型改为可执行文件,同时程序头大小和节头数量有所增加,并生成了入口点地址。
正在上传…重新上传取消正在上传…重新上传取消
图 31 ELF头
5.3.2 节头
节头包含文件中各个节的信息,包括名称、类型、地址、偏移量等,与hello.elf相比并无太大区别,而内容在连接后变得更加丰富了。
正在上传…重新上传取消正在上传…重新上传取消
图 32 节头部分展示
5.3.3 程序头
程序头是链接后新生成的,它包含了程序运行所需的各种信息,例如段的大小、对齐方式、读写权限等,操作系统在加载程序时会根据程序头的描述进行加载和对齐,从而使程序能够正确地运行。
正在上传…重新上传取消正在上传…重新上传取消
图 33 程序头部分展示
5.3.4 动态链接段
其中包含了25个地址入口。
正在上传…重新上传取消正在上传…重新上传取消
图 34 动态链接段
5.3.5 符号表
保存了重定位所需符号的声明。
正在上传…重新上传取消正在上传…重新上传取消
图 35 符号表
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间是从40000开始的,这是Linux操作系统的规定。
正在上传…重新上传取消正在上传…重新上传取消
图 36 hello的虚拟地址空间
对照节头表可以观察到.init节的入口就是程序的入口——401000,同时.text代码段的起始地址为4010f0,在edb中查看如下。
正在上传…重新上传取消正在上传…重新上传取消
图 37 .text段的虚拟地址空间
5.5 链接的重定位过程分析
使用objdump -d -r hello >hello_.asm命令对hello进行反汇编,为避免和可重定位目标文件的反汇编冲突,命名为hello_.asm。
正在上传…重新上传取消正在上传…重新上传取消
图 38 hello的反汇编执行结果
5.5.1 对比hello与hello.o
(1)执行链接后反汇编代码由原来的53行增到了249行,其中函数数量增加了许多,这是因为在链接时,动态链接器将共享库中的函数导入进了可执行文件中。
正在上传…重新上传取消正在上传…重新上传取消
图 39 链接后新增的部分函数
(2)函数调用指令与跳转指令的参数进行了更改,在链接过程中,链接器进行了重定位,通过计算相对地址修改了对应的操作数。
正在上传…重新上传取消正在上传…重新上传取消
图 40 调用指令与跳转指令的参数被修改
5.5.2 链接过程与重定位
通过对比hello.asm与hello_.asm,可以观察到链接就是链接器(ld)将各个目标文件(各种.o文件)组装在一起,文件中的各个函数段按照一定规则累积在一起,并生成新的可执行文件。重定位会从.o可重定位目标文件提供的重定位条目将函数调用和控制流跳转的地址进行运算,填写为最终的地址。
5.6 hello的执行流程
程序执行顺序 | 程序地址 |
_init | 401000 |
.plt | 401020 |
puts@plt | 401030 |
printf@plt | 401040 |
getchar@plt | 401050 |
atoi@plt | 401060 |
exit@plt | 401070 |
sleep@plt | 401080 |
_start | 401090 |
_dl_relocate_static_pie | 4010c0 |
main | 4010c1 |
_lib_csu_init | 401150 |
_libc_csu_fini | 4011b0 |
_fini | 4011b4 |
5.7 Hello的动态链接分析
正在上传…重新上传取消正在上传…重新上传取消
图 41 ELF中.got和.got.plt的地址
正在上传…重新上传取消正在上传…重新上传取消
图 42 执行.init前的地址
正在上传…重新上传取消正在上传…重新上传取消
图 43 执行.init后的地址
观察发现执行.init函数后发生变化的内容对应的是GOT条目。库函数动态链接时需要PLT和GOT一起完成,当程序调用动态链接库中的函数时,它实际上是调用了PLT中的代码。PLT中的代码会加载GOT,GOT中保存了动态链接库中函数的地址。这样就可以使用GOT和PLT进行动态链接。
5.8 本章小结
本章主要介绍链接的过程,并通过反汇编代码与汇编代码的对比,深入体会可重定位目标文件、可执行目标文件的结构,并分析链接中符号解析、重定位以及动态链接的过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 进程的概念
进程是指正在运行的程序实例。每个进程都有自己的内存空间、寄存器集合、栈、堆等资源,它们可以独立地运行、调度、中断和恢复。
6.1.2 进程的作用
进程是计算机系统中最基本的资源分配单位,它可以在操作系统的支持下实现并发执行、资源管理、进程间通信、并行计算和分时操作系统等功能。进程是一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。同时是一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。
其基本功能是解释并运行用户的指令,允许用户输入命令并与操作系统交互,重复如下处理过程:
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
(3)如果不是内部命令,调用fork( )创建新进程/子进程
(4)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(5)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid等待作业终止后返回。
(6)如果用户要求后台运行(如果命令末尾有&号),则shell返回;
6.3 Hello的fork进程创建过程
在终端输入“./hello 2021111161 李睿博”后,shell会解析这条命令行,首先判断是否为内置命令,hello并不是内置命令,于是shell会调用fork函数创建一个新的子进程用来运行程序hello,子进程与父进程共享相同但独立的虚拟地址空间,但他们的pid不同。当子进程运行结束后,父进程会进行回收,释放他所占空间。
6.4 Hello的execve过程
当Shell调用fork创建新的子进程后,会通过execve函数,将hello程序的代码段、数据段、堆栈等信息加载到进程上下文中,并将进程的PC指向程序的入口点。在这个过程中,控制权从操作系统传递到hello,hello开始执行自己的代码。同时,execve也会将命令行参数和环境变量传递给heloo,并为其创建一个新的用户空间栈。执行结束后,该进程的资源会被回收。
6.5 Hello的进程执行
(以下格式自行编排,编辑时删除)
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
进程hello在最开始运行时,处于用户态,直到它调用主函数中的系统调用函数sleep或exit,这时操作系统会将hello进程的上下文进行保存,然后切换至内核态进行系统调用函数的执行,执行结束后再切换回用户态,将控制交给hello进程并恢复上下文。进程调度就是进程hello的运行中不断进行上下文的切换,与其他进程交替进行控制,hello进程会被分成一个个时间片。
正在上传…重新上传取消正在上传…重新上传取消
图 44 上下文切换的过程
6.6 hello的异常与信号处理
6.6.1异常类型:
类别 | 原因 | 异步/同步 | 返回行为 |
中断 | 来自I/O设备的信号 | 异步 | 总是返回到下一条指令 |
陷阱 | 系统调用 | 同步 | 总是返回到下一条指令 |
故障 | 潜在可恢复的错误 | 同步 | 可能返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
6.6.2 正常运行
程序正常运行会打印八次信息,输入回车后结束程序并回收。
正在上传…重新上传取消正在上传…重新上传取消
图 45 程序正常运行
6.6.3 SIGINT信号的处理
按下Ctrl+C,进程会收到SIGINT信号,进程被终止。之后输入ps与jobs也无法找到hello进程,说明hello进程收到SIGINT后会终止并被回收。
正在上传…重新上传取消正在上传…重新上传取消
图 46 SIGINT终止hello程序
6.6.4 SIGSTP信号的处理
按下Ctrl+Z,进程会受到SIGSTP信号,hello进程会停止,被挂起。此时输入ps与jobs,仍然可以找到hello进程,说明hello进程收到SIGSTP信号后会被挂起,处于等待状态。再次输入fg 1会将其调回前台继续正常执行。
正在上传…重新上传取消正在上传…重新上传取消
图 47 SIGSTP停止hello程序
正在上传…重新上传取消正在上传…重新上传取消
图 48 输入pstree命令的部分结果
6.6.5 KILL信号的处理
输入Ctrl+Z后,进程被停止,再输入kill后,进程会被终止并回收,此时输入ps和jobs后,hello进程显示被杀死,说明kill信号被处理。
正在上传…重新上传取消正在上传…重新上传取消
图 49 KILL杀死hello程序
6.6.6 一通乱输
在程序执行时键盘随意输入,会被认为是命令缓存到缓冲区,同时不会影响hello程序的正常执行。
正在上传…重新上传取消正在上传…重新上传取消
图 50 一通乱输
6.7本章小结
本章主要介绍进程的概念与其作用,并阐述了shell的工作机制。结合hello进程,探讨了fork与execve的原理以及进程的执行过程,并在shell中运行hello程序,测试其对不同异常与信号的处理方式。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1四种地址的概念
逻辑地址是程序中使用的地址。在程序中,逻辑地址通常是指程序代码、数据或堆栈中的地址,它们是由编译器或链接器生成的,并且通常是相对于程序的起始地址计算的。逻辑地址是程序中使用的地址,不是实际的物理地址。
线性地址是逻辑地址转换为物理地址之前的地址,也称为虚拟地址。在操作系统中,每个进程都有自己的地址空间,其中包含了该进程可以使用的所有内存地址。线性地址是进程中使用的地址,它是相对于进程的起始地址计算的。线性地址可以通过段和页的映射关系来转换为物理地址。
在CSAPP中,虚拟地址和线性地址的概念是相同的。
物理地址是实际的内存地址,也称为物理空间地址。它是CPU访问内存时使用的地址,是实际存储在内存中的数据的地址。物理地址是由硬件生成的,它与逻辑地址、线性地址和虚拟地址没有直接关系。
7.1.2 hello的存储器地址空间
在hello中,程序的代码、数据和堆栈都存储在进程的地址空间中。当程序执行时,它使用的是逻辑地址。逻辑地址会通过链接器生成的符号表转换为线性地址,然后通过操作系统的页表映射机制转换为虚拟地址。最后,虚拟地址会通过页表映射机制转换为物理地址,然后在实际的物理内存中读取或写入数据。
7.2 Intel逻辑地址到线性地址的变换-段式管理
7.2.1 段式管理的概念:
在Intel x86架构中,使用了段式管理来实现逻辑地址到线性地址的转换。具体来说,每个段都有一个段描述符,它包含了段基址、段限长和一些访问权限信息。当CPU访问内存时,它会使用段选择符来选择需要访问的段描述符,并将段基址和偏移量组合成线性地址。
7.2.2 段式管理下地址映射的具体步骤:
具体步骤是:CPU使用段选择符从GDT或LDT中选择相应的段描述符。然后段描述符中的段基址和逻辑地址中的偏移量相加,得到线性地址。CPU会检查线性地址是否超出了段的限长。如果超出了限长,就会触发异常。接着检查段描述符中的访问权限信息,如果访问权限不足,也会触发异常。如果一切正常,CPU将线性地址转换为物理地址,并进行内存访问操作。
7.3 Hello的线性地址到物理地址的变换-页式管理
7.3.1 页式管理的概念:
页式管理将内存空间划分为固定大小的页,每个页可以单独进行映射和访问控制,从而更好地保护系统的安全性。在页式管理中,线性地址会被划分为虚拟页号和页内偏移量。虚拟页号会通过页表进行映射,转换为物理页号,然后再加上页内偏移量,得到物理地址。
正在上传…重新上传取消正在上传…重新上传取消
图 10 虚拟地址通过页表映射为物理地址
7.3.2 页式管理下的地址映射具体过程:
在hello中,程序的线性地址会通过页表映射机制转换为物理地址。具体的线性地址到物理地址的变换过程如下:
程序使用线性地址来访问内存中的数据。线性地址由虚拟页号和页内偏移量组成。通过页表查找虚拟页号对应的物理页号,并检测有效位,如果为1则发生页命中,直接获取物理页号并和页偏移量进行拼接得到物理地址;若不为1则发生缺页,此时需要调用缺页处理程序调入新的页,并再次进行查找获取PPN,与VPO组成PA。
7.4 TLB与四级页表支持下的VA到PA的变换
7.4.1 变换过程:
首先将VPN拆分为TLBT与TLBI,以TLBI为索引在TLB中查找页表项,如果发生命中则直接得到PPN,与VPO拼接得到PA。若不命中,则仍需到四级页表中查找页表项,VPN被分为四段,通过VPN1找到PTE为二级页表的基地址,再通过VPN2找到三级页表的基地址,直到在四级页表查找到PPN,与VPO拼接为PA。
正在上传…重新上传取消正在上传…重新上传取消
图 52 Intel I7的映射过程
7.4.2 采用TLB与四级页表的效果
减少内存访问开销:TLB是一种快速缓存,它存储了最近使用过的虚拟页和物理页框的映射关系。当CPU需要访问内存时,TLB可以直接提供物理页框号,从而减少访问四级页表的开销,提高内存访问的速度。
提高内存访问速度:TLB的访问速度比四级页表快很多,因为TLB通常存储在CPU的芯片中,而四级页表通常存储在主存中。因此,采用TLB和四级页表的地址转换机制可以大大提高内存访问的速度。
提高存储器利用率:采用四级页表可以将物理内存划分成多个页框,从而提高存储器的利用率。每个页框可以被映射到不同的虚拟页,而不同的进程可以共享同一个物理页框,从而减少内存的浪费。
7.5 三级Cache支持下的物理内存访问
7.5.1具体过程
将虚拟地址映射为物理地址后,将其分为CT、CI、CO三项,分别为标记为、组索引、组内偏移。首先通过CI查询L1 Cache,若对应CO处的字块标记位匹配,则发生命中,直接取走即可。否则不命中,需要进一步向L2、L3乃至主存进行访问,访问成功后,需要将字块填入更高级的Cache中,若发生冲突,则依照冲突替换策略进行替换。
7.5.2 采用三级Cache的效果
在物理内存访问的过程中,CPU会先访问高速缓存,如果缓存中没有需要的数据,才会访问主存。因此,高速缓存对于提高计算机的性能非常重要。在三级缓存的支持下,CPU可以更快地访问内存中的数据,从而提高计算机的性能。
正在上传…重新上传取消正在上传…重新上传取消
图 52 Intel I7三级Cache下的内存访问
7.6 hello进程fork时的内存映射
在hello中,当进程调用fork函数时,操作系统会创建一个新的子进程,并将hello程序的内存映像复制到子进程的内存空间中。由于操作系统采用了写时复制技术,父子进程的内存映射是相同的,但是它们实际上指向的是不同的物理内存页面。当子进程或父进程需要修改内存中的数据时,操作系统会为它们分配一个新的物理内存页框,并将需要修改的数据复制到新的物理内存页框中。这样,新进程和原进程就可以独立地修改它们各自的内存空间,而不会影响到对方。当进程退出时,它们所占用的物理页面会被释放,操作系统会回收这些物理页面以供其他进程使用。
7.7 hello进程execve时的内存映射
当hello进程调用execve系统调用时,进程的内存映射映射过程可以包括以下步骤:
1.删除已存在的用户区域:删除当前进程虚拟地址的用户部分中的已存在的区域(段)结构。
2.映射私有区域:为新程序的文本、数据、bss和栈区域创建新的区域(段)结构。
3.映射共享区域:如果hello.out程序与共享库链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器:设置当前进程上下文中的程序计数器,使之指向文本段的入口点。
在这个过程中,新的可执行文件将会被映射到进程的虚拟地址空间中,它们通常会被映射到不同的段中,例如代码段、数据段、堆、栈等。每个段都有相应的访问权限和保护级别,例如代码段是只读的,数据段是可写的,堆和栈是可读写的。
7.8 缺页故障与缺页中断处理
7.8.1 缺页故障的概念:
缺页故障是指当CPU试图访问一个当前不在物理内存中的虚拟页时产生的异常。在这种情况下,操作系统会将缺页故障转换为缺页中断,并将控制权传递给内核,进行相应的处理。
7.8.2 缺页中断处理的过程:
1. 中断处理程序:当CPU产生缺页中断时,操作系统会调用中断处理程序来处理该中断。中断处理程序通常会保存当前进程的上下文信息,并将控制权传递给操作系统内核。
2. 查找页表:操作系统会查找当前进程的页表,以确定需要加载的虚拟页是否存在于物理内存中。如果该虚拟页已经在物理内存中,那么操作系统会更新页表项并重新启动进程。否则,操作系统会执行下一步操作。
3. 选择牺牲页:如果需要加载的虚拟页不存在于物理内存中,那么操作系统需要选择一个牺牲页来替换。它的内容会被换出到磁盘中,以便腾出空间来加载新的虚拟页。一般采用LRU替代策略。
4. 从磁盘中读取页:操作系统会从磁盘中读取需要加载的虚拟页,并将它们映射到一个物理页框中。如果物理内存中没有空闲的物理页框,操作系统需要先选择一个牺牲页并将其换出,然后再将新的虚拟页加载到该物理页框中。
5. 更新页表:操作系统会更新当前进程的页表,将虚拟页和物理页框的映射关系记录到页表项中,并重新启动进程。
6. 恢复进程状态:操作系统会恢复当前进程的上下文信息,包括程序计数器、寄存器、栈指针等,并将控制权传递给进程,使其继续执行。
7.9本章小结
本章主要以hello的存储地址空间为线索,介绍了几种地址空间,并介绍了不同管理方式下对虚拟地址到物理地址的转换,以及采用Cache的内存访问。最后重新回顾了虚拟内存下的进程fork、execve内存映射以及缺页处理。
(第7章 2分)
结论
当我们写了一个hello程序时,按下运行键,他的背后经历了什么呢?
hello要过的第一关是预处理,在预处理器那里,他会把自己头顶的#include命令摘下来,把头文件的内容全部添到自己身上,变成一个预处理文本文件hello.i。
然后就到了编译器手中,编译器会进行词法分析与语法分析,把hello变得面目全非,从原先还看起来像人话的高级程序语言,翻译成了不人不机器的汇编语言文件hello.s。在这过程中,hello也会变得更好,编译器可以对他进行一些优化。
下一步是汇编,汇编器会把汇编程序翻译成机器语言,这下hello再也不能被普通人类认出来了,它会被打包成可重定位目标文件,变成hello.o。
接着要过的是链接器,链接器会把hello进行符号解析与重定位,把他升级为可执行目标文件hello。
既然都可以执行了,那就该送到shell里干活了。Shell会调用fork函数,生成一个子进程,再由execve函数将hello加载到这个进程的上下文中,然后开始运行。
hello也需要有一间自己的房子,他的逻辑地址会一步步转化为物理地址,最后通过访存找到自己的地址空间,拿到自己的小数据。
程序的一生哪能没有坎坷,若是遇到了异常,就需要调用异常处理程序,若是遇到了信号,也需要对信号进行处理。
当时间的长河慢慢流过,hello的比特跳动停止了,在被父进程回收并释放掉空间的那一刻,他是否会回想起那个少年hello.c的模样呢?
CSAPP是一本真正的计算机科学的好教材,经过对这本书的学习,今后的编程不应该只是简简单单的完成需求,而是要从底层一步步考虑,要编出一个好的、安全的、高效的程序。同时目光应该放远,程序员不该只是码农,若有可能的话,应该当码仙,像代码仙人一样,用比特的跳动实现科学的进步,乃至人类的进步。写完这篇论文,也是对自己的勉励,今后不可懈怠,持续努力。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
文件名 | 作用 |
hello.i | 预处理后的文本文件 |
hello.s | 编译后的汇编语言文件 |
hello.o | 汇编后的可重定位目标文件 |
hello.asm | 可重定位目标文件的反汇编 |
hello.elf | 可重定位目标文件的ELF文件 |
hello_.asm | 可执行目标文件的反汇编 |
hello_.elf | 可执行目标文件的ELF文件 |
(附件0分,缺失 -1分)
参考文献
[1] Randal E. Bryant , David R. O ’Hallaron. Computer Systems A Progammer’s Perspective . 2016.
(参考文献0分,缺失 -1分)