计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 120L020512
班 级 2003004
学 生 黄鹏程
指 导 教 师 史先俊
计算机科学与技术学院
2022年5月
摘 要
本次大作业旨在通过对hello程序生命历程中各个环节的实验与分析,将计算机系统课程的整体知识进行串联与复现,从而加深对课程内容的理解。
关键词:计算机;汇编;进程;存储管理;
目 录
第1章 概述…………………………………………………………………………………………………. – 4 –
1.1 Hello简介…………………………………………………………………………………………… – 4 –
1.2 环境与工具………………………………………………………………………………………….. – 4 –
1.3 中间结果……………………………………………………………………………………………… – 4 –
1.4 本章小结……………………………………………………………………………………………… – 5 –
第2章 预处理……………………………………………………………………………………………… – 6 –
2.1 预处理的概念与作用……………………………………………………………………………. – 6 –
2.2在Ubuntu下预处理的命令………………………………………………………………….. – 6 –
2.3 Hello的预处理结果解析……………………………………………………………………… – 6 –
2.4 本章小结……………………………………………………………………………………………… – 7 –
第3章 编译…………………………………………………………………………………………………. – 9 –
3.1 编译的概念与作用……………………………………………………………………………….. – 9 –
3.2 在Ubuntu下编译的命令…………………………………………………………………….. – 9 –
3.3 Hello的编译结果解析…………………………………………………………………………. – 9 –
3.4 本章小结……………………………………………………………………………………………. – 14 –
第4章 汇编……………………………………………………………………………………………….. – 15 –
4.1 汇编的概念与作用……………………………………………………………………………… – 15 –
4.2 在Ubuntu下汇编的命令…………………………………………………………………… – 15 –
4.3 可重定位目标elf格式………………………………………………………………………. – 15 –
4.4 Hello.o的结果解析……………………………………………………………………………. – 17 –
4.5 本章小结……………………………………………………………………………………………. – 18 –
第5章 链接……………………………………………………………………………………………….. – 19 –
5.1 链接的概念与作用……………………………………………………………………………… – 19 –
5.2 在Ubuntu下链接的命令…………………………………………………………………… – 19 –
5.3 可执行目标文件hello的格式……………………………………………………………. – 19 –
5.4 hello的虚拟地址空间……………………………………………………………………….. – 21 –
5.5 链接的重定位过程分析………………………………………………………………………. – 22 –
5.6 hello的执行流程………………………………………………………………………………. – 24 –
5.7 Hello的动态链接分析……………………………………………………………………….. – 25 –
5.8 本章小结……………………………………………………………………………………………. – 26 –
第6章 hello进程管理…………………………………………………………………………… – 27 –
6.1 进程的概念与作用……………………………………………………………………………… – 27 –
6.2 简述壳Shell-bash的作用与处理流程……………………………………………….. – 27 –
6.3 Hello的fork进程创建过程………………………………………………………………. – 28 –
6.4 Hello的execve过程…………………………………………………………………………. – 28 –
6.5 Hello的进程执行………………………………………………………………………………. – 28 –
6.6 hello的异常与信号处理……………………………………………………………………. – 29 –
6.7本章小结……………………………………………………………………………………………. – 31 –
第7章 hello的存储管理……………………………………………………………………….. – 32 –
7.1 hello的存储器地址空间……………………………………………………………………. – 32 –
7.2 Intel逻辑地址到线性地址的变换-段式管理……………………………………….. – 32 –
7.3 Hello的线性地址到物理地址的变换-页式管理………………………………….. – 33 –
7.4 TLB与四级页表支持下的VA到PA的变换………………………………………… – 34 –
7.5 三级Cache支持下的物理内存访问……………………………………………………. – 34 –
7.6 hello进程fork时的内存映射…………………………………………………………… – 35 –
7.7 hello进程execve时的内存映射……………………………………………………….. – 36 –
7.8 缺页故障与缺页中断处理…………………………………………………………………… – 36 –
7.9动态存储分配管理……………………………………………………………………………… – 36 –
7.10本章小结………………………………………………………………………………………….. – 37 –
第8章 hello的IO管理………………………………………………………………………… – 38 –
8.1 Linux的IO设备管理方法………………………………………………………………….. – 38 –
8.2 简述Unix IO接口及其函数……………………………………………………………….. – 38 –
8.3 printf的实现分析………………………………………………………………………………. – 38 –
8.4 getchar的实现分析…………………………………………………………………………… – 40 –
8.5本章小结……………………………………………………………………………………………. – 40 –
结论……………………………………………………………………………………………………………. – 40 –
附件……………………………………………………………………………………………………………. – 42 –
参考文献…………………………………………………………………………………………………….. – 43 –
第1章 概述
1.1 Hello简介
P2P:
如图1.11,为c语言代码源文件,即hello.c变成可执行文件hello的过程。预处理器对源文件进行宏替换、条件编译的预处理操作后,生成hello.i文件;.i文件检查语法后生成汇编文件hello.s;汇编文件经过汇编被转换为机器码,生成可重定位文件hello.o;然后连接器将源代码中用到的库函数与可重定位文件合并为可执行文件hello;我们在shell中键入命令后,其fork子进程、调用execve加载hello并运行。
图1.11 P2P过程
O2O:
在shell中fork子进程后调用execve加载并执行hello,分配虚拟内存空间并映射到物理内存;随后依照CPU中逻辑控制流开始执行;在程序结束后,shell通过hello父进程或祖先进程将其回收,释放内存空间。
1.2 环境与工具
硬件环境:处理器Intel(R) Core(TM) i5-8300H CPU @ 2.30GHz 2.30 GHz;RAM 8GB; 系统类型:系统类型:64位操作系统,基于x64的处理器;
软件环境:Windows10 64位;Ubuntu 20.04
开发与调试工具:gcc,as,ld,vim,edb,readelf,VS
1.3 中间结果
hello.i:预处理得到的文本文件
hello.s:编译得到的汇编文件
hello.o:汇编得到的可重定位目标文件
hello:链接得到的可执行文件
objdump_hello.s:hello反汇编得到的代码
1.4 本章小结
本章在对P2P、O2O的介绍中概括了hello从诞生到执行再到死亡的过程;给出本次作业的实验环境与用到的中间结果。
第2章 预处理
2.1 预处理的概念与作用
概念:
程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。预处理中会展开以#起始的行,试图解释为预处理指令。包括#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。
作用:
将源文件中用#include形式声明的文件复制至新的程序中;
用实际值替换用#define 定义的字符串,即将宏定义进行替换;
2.2在Ubuntu下预处理的命令
gcc -m64 -no-pie -fno-PIC hello.c -E -o hello.i如下图2.21,生成文件如图2.22;
图2.21 执行命令
图2.22 生成文件
2.3 Hello的预处理结果解析
打开hello.i,发现程序已经扩展为3060行,如图2.31,hello.c中main函数代码出现在3047行以后,在此之前的代码为.c源文件中含有三个库:#include 、#include 、#include 的展开,将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,如图2.32。
图2.31 main函数
图2.32 预处理#include
2.4 本章小结
本章使用gcc -m64 -no-pie -fno-PIC hello.c -E -o hello.i将hello.c预处理为hello.i,发现hello.i中插入了大量源文件包含的库文件
第3章 编译
3.1 编译的概念与作用
概念:
编译是指编译器做词法分析、语法分析、语义分析等,在检查无错误后,将代码翻译成汇编语言的过程。编译器将文本文件 hello.i 翻译成文本文件 hello.s。
作用:
- 语法分析:将不符合语法规则的记号识别出其位置并产生错误提示语句;
- 代码优化:指对程序进行多种等价变换,变为功能等价,但占用资源更少的代码;
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s
图3.21 编译的命令
图3.22 生成hello.s
3.3 Hello的编译结果解析
3.3.1 数据
- 常量
数字:如图3.11,3.12源文件中的常量4与8被作为立即数保存在图3.13、3.14中,hello.s的代码节.data中;
图 3.11
图 3.12
图 3.13 立即数4
图 3.14 立即数7(借助7来判断小于8)
字符串:如图3.15,源文件中的字符串”用法: Hello 学号 姓名 秒数!\n”被保存在.rodata节中,因为其为只读字符串;
图 3.15 保存在.rodata的字符串
- 变量
局部变量:源文件中使用局部变量i:
for ( i = 0; i < 8 ; i++ )
被保存在栈中%rsp-4位置,如图3.16,该位置每次加1后与7进行比较,依此决定是否再次进入循环。
图 3.16 寄存器中的局部变量i
3.3.2 赋值
上文中,局部变量i被赋初值为0,而我们已经知道了它的存储位置,故容易找到其在hello.s中的赋值语句,如图3.21
图 3.21 i赋初值为0
3.3.2 算术操作
对于上文提到的循环,步长为1,每次i自增1,易找到其在hello.s中的操作如图3.22
图 3.22 i++
3.3.3 关系操作、控制转移
源文件中出现了两次关系判断,如图3.31中的13行、17行:
图 3.31
第13行在hello.s中对应操作如图3.32,为argc与4进行比较,若相等则进行跳转操作:
图 3.32 判断相等与跳转操作
上图中,24行为argc与4进行比较,比较的结果保存在寄存器中,25行je根据比较结果决定是否跳转到.L2;
图 3.33 循环中i的比较与跳转操作
3.3.4 数组/指针/结构操作
指针数组char *argv[ ] 首地址保存在-32(%rbp)位置,如图3.41,print函数打印argv[1]与argv[2],则在第35、38行分别将数组首地址加上偏移获得数组元素;
图 3.41
3.3.5 函数操作
参数传递:第1~6个参数储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,剩下的参数保存在栈当中。
调用函数:每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器%ebp指向当前的栈帧的底部(高地址),寄存器%esp指向当前的栈帧的顶部(低地址)。调用函数的栈底将会被保存,而栈顶将作为被调用函数的栈底。
函数返回:函数返回值保存在%ax中。
下对hello.s中函数分析:
- main函数
参数传递:传入参数argc和argv[],分别用寄存器%rdi和%rsi存储
函数调用:C程序总是从mian函数开始执行
函数返回:结束时更改%eax为0
图 3.51 main入口
图 3.52 main出口
- print函数
参数传递: call printf时传入了 argv[1]和argc[2]的地址
图 3.53 调用printf时传入的argv[1]和
argc[2]保存在%rdx、%rsi中
- exit()函数
参数传递:将%edi 设置为 1 - atoi()函数
参数传递:将%rdi 设置为 argv[3] - sleep()函数
参数传递:将%edi 设置为atoi处理后的argv[3] - exit()函数
参数传递:将%edi 设置为 1,执行exit(1)
3.4 本章小结
编译是指编译器做词法分析、语法分析、语义分析等,在检查无错误后,将代码翻译成汇编语言的过程。本章对hello.i编译后得到的hello.s进行分析,探究了编译器处理C语言的各个数据类型以及各类操作的过程。
第4章 汇编
4.1 汇编的概念与作用
概念:把汇编语言翻译成机器语言的过程称为汇编。
作用:用汇编语言编写的程序,机器不能直接识别,要由一种程序将汇编语言翻译成机器语言,这种起翻译作用的程序叫汇编程序。
4.2 在Ubuntu下汇编的命令
gcc -m64 -no-pie -fno-PIC hello.s -c -o hello.o
图4.21 汇编的命令
图 4.22 汇编得到hello.o
4.3 可重定位目标elf格式
readelf -a hello.o > helloo.elf 生成文本文件
ELF头:
以16B的序列 Magic 开始,Magic描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型、 字节头部表(section header table)的文件偏移,及节头部表中条目的大小和数量等信息。根据头文件的信息,可以知道该文件是可重定位目标文件,有14个节。
图 4.31 ELF头
节头:
描述了.o文件中出现的各个节的类型、位置、所占空间大小等信息。
图 4.32 节头
重定位节:
当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置。它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到最终未知未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并到可执行文件时如何修改新的引用。
本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf,slepsecs,sleep,getchar这些符号。
图 4.33 重定位节
符号表:
符号表中存放程序定义和引用的函数和全局变量的信息。但其中不包含局部变量条目。
图 4.34 符号表
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
4.4 Hello.o的结果解析
图 4.41 hello.o反汇编
将其与hello.s进行对照分析:
- 操作数:hello.s中操作数为十进制,而反汇编代码中为十六进制;
- 分支转移:hello.s中地址使用段名称如 je .L2,而反汇编代码中则使用相对偏移地址,如 je 2d
; - 函数调用:hello.s中,call指令使用的是函数名称,反汇编代码中call指令使用相对偏移地址。原因是hello.s中调用的函数都是共享库中的函数,故需要通过等待调用动态链接将重定位的函数目标地址链接到共享库程序中,最终需通过动态链接器确定函数的运行时地址;
- 除上述以外,二者没有什么不同,这表明了汇编语言能与机器码建立一一对应的关系。
4.5 本章小结
把汇编语言翻译成机器语言的过程称为汇编,hello.s汇编得到hello.o后,我们阅读了ELF文件,然后通过hello.o反汇编得到的代码与hello.s进行比较,发现机器码与汇编语言的映射关系,以及它相对汇编语言所具有的特点。
第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
作用:节省源程序空间;将可重定位文件连接成为可执行文件。
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.21 链接的命令
图 5.22 生成可执行文件
5.3 可执行目标文件hello的格式
ELF文件头:
图5.31 ELF文件头
节头:
节头部表中包含了hello中所有节的信息,其中包括名称、类型、大小、地址和偏移量等信息,其中地址为程序被载入到虚拟地址的起始地址,偏移量为各个节在程序中的偏移量。根据节头部表的信息可以使用HexEdit定位各个节的起始位置及大小。
图5.32 节头
程序头:
elf可执行文件易加载到内存,可执行文件连续的片被映射到连续的内存段,程序头部表描述了这一映射关系。程序头部表包括各程序头的偏移量、内存地址、对其要求、目标文件与内存中的段大小及运行时访问权限等信息。
图5.33 程序头
重定位节:
重定位节包含.text节中一些需对目标进行重定位的函数信息,链接器把函数的目标位置文件与其他目标文件组合在一起时,需要修改这些函数的位置。
图5.34 重定位节
5.4 hello的虚拟地址空间
在edb的memory regions窗口中,可以看到hello的虚拟地址空间,如图5.41,由0x400000到0x405000;
图5.41 memory regions窗口
edb加载hello后, Data Dump 窗口可以查看加载到虚拟地址中的 hello 程序,如图5.42;
图5.42 Data Dump 窗口
根据5.3中的节头部表中的地址,可在edb中找到各个节。例如:.text节的地址为0x4010f0,大小为0x145,用edb查找结果如图5.43;
图5.43
.data节的地址为0x404040,大小为0x4,如图5.44
图5.44
.rodata节的地址为0x402000,大小为0x3b,如图5.45
图5.45
5.5 链接的重定位过程分析
使用命令:objdump -d -r hello >helloobjdump.txt,获得hello的反汇编代码,如图5.51;
图5.51 hello反汇编
对照hello与hello.o,不同点有:
- 在hello.o中,main函数地址从0开始,hello.o中保存的是相对偏移地址;而在hello中main函数0x401125开始(如图5.52),即hello中保存虚拟内存地址,对hello.o中的地址进行了重定位。
图5.52 hello的main函数
- ELF描述文件总体格式,发现它包括了程序的入口点,即程序运行时执行的第一条指令的地址。由于可执行文件是完全链接的,因此没有rel节。
- hello中多了.init节和.plt段,如图5.53。.init节定义函数_init,用于程序的初始化代码,还有初始化程序执行环境;.plt段为程序执行时的动态链接。所有重定位条目都被修改为确定的运行时内存地址。
图5.53 .init节.plt段
- 在hello中链接加入了在hello.c中用到的函数,如exit、printf、sleep、getchar等函数。
链接过程:
经以上分析可知,链接就是将多个可重定位目标文件合并到一起,生成可执行文件。链接需要进行符号解析、重定位及计算符号引用地址三个步骤。
重定位:
重定位将合并输入模块。并为每个符号分配运行地址。重定位由两个步骤组成:重定位节与符号定义、重定位节中的符号引用。
定位节与符号定义,链接器将相同类型的节合并为同一类型的新的聚合节,此后链接器将运行时内存地址赋值新的聚合节、输入模块定义的每个节,还有输入模块定义的每个符号。
重定位节中的符号引用,链接器修改代码节与数据节中对每个符号的引用,使他们指向正确的运行地址,这一步依赖hello.o中的重定位条目。
5.6 hello的执行流程
Edb逐步执行并记录调用的函数,如图5.61
图5.61 即将调用hello!_init
得到调用函数顺序如下:
ld-2.27.so!_dl_start
ld-2.27.so!_dl_init
hello!_start
libc-2.27.so!__libc_start_main
-libc-2.27.so!__cxa_atexit
-libc-2.27.so!__libc_csu_init
hello!_init
libc-2.27.so!_setjmp
-libc-2.27.so!_sigsetjmp
–libc-2.27.so!__sigjmp_save
hello!main
hello!puts@plt
hello!exit@plt
hello!printf@plt
hello!atoi@plt
hello!sleep@plt
hello!getchar@plt
ld-2.27.so!_dl_runtime_resolve_xsave -ld-2.27.so!_dl_fixup
–ld-2.27.so!_dl_lookup_symbol_x
libc-2.27.so!exit
5.7 Hello的动态链接分析
在elf文件中可以找到动态链接调用的函数的位置,如图5.71:
图5.71 函数位置
动态链接调用的函数的位置为0x404000,进入edb内存窗口查看:
图5.71 init之前
图5.72 init之后
对于变量而言,利用代码段和数据段的相对位置不变的原则计算得到正确地址。对于库函数而言,需要plt与got合作,plt初始存的是一批代码,它们跳转到got所指示位置,接着调用链接器。初始时got里面存的都为plt的第二条指令,随后链接器会修改got,当下一次再次调用plt时,指向的就是正确的内存地址。plt就能跳转到正确的区域。
5.8 本章小结
本章分析了hello的链接过程,hello.o经过链接生成可执行文件hello,通过对比hello反汇编文件与hello.o之间的差别,我们可以总结出重定位的一些特点。在edb中逐步调试hello,我们能看到hello逐个调用的函数。
第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
作用:
(1) 给程序创造这样的假象: 我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。
(2) 处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell是系统的用户界面,它接收并解释用户输入的命令,再将其送入内核执行。
处理流程:
- 读取从键盘输入的命令;
- 判断命令是否正确,并判断命令是否为内置命令:
若为内置命令则立即执行;
否则将命令行的参数改造为系统调用execve()内部处理所要求的形式终端进程调用fork()来创建子进程,自身则用系统调用wait()来等待子进程完成;
- 当子进程运行时,它调用execve()根据命令的名字指定的文件到目录中查找可行性文件,调入内存并执行这个命令;
- 如果命令行未尾有后台命令符号&终端进程不执行等待系统调用,而是立即发提示符,让用户输入下一条命令;如果命令末尾没有&则终端进程要一直等待;
- 当子进程完成处理后,向父进程报告,此时终端进程被唤醒,做完必要的判别工作后,再发提示符,让用户输入新命令。
6.3 Hello的fork进程创建过程
父进程在读取命令后,首先判断该命令是否为内置命令,若非内置命令,则会调用fork命令创建子进程。子进程除PID外,与父进程完全一致,获得与父进程虚拟地址空间相同但独立的副本,其用户栈、寄存器、代码段等也与父进程一致,子进程可以读写父进程打开的任何文件。Fork函数在父进程中返回子进程PID,在子进程中则返回0。
6.4 Hello的execve过程
当创建了一个新运行的子进程后,子进程调用execve函数在当前子进程的上下文中加载并运行hello程序。execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到hello,execve才会返回到调用程序。所以execve调用一次且从不返回。
argv是一个参数字符串指针数组,argv[0]是可执行目标文件的名字,而envp的元素则指向环境变量。
6.5 Hello的进程执行
进程给hello程序提供的关键抽象:
- 一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器。
- 一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用内存系统。
上下文信息:由程序正确运行所需的状态组成的,它包括存放在内存中的代码与数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合;
进程时间片:多个逻辑控制流并发执行时,其中一个进程执行它的控制流的一部分的一个时间段叫做一个时间片;
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
进程调度过程:
以hello为例,作为一个独立的进程与其他进程并发执行,内核为hello维持一个上下文,在hello的某个时间片内,若内核判断它已经运行了足够长的时间,那么内核可以决定抢占hello进程,并重新开始一个之前被抢占了的进程,并使用上下文切换的机制将控制转移到新的进程,该机制具体执行分为三步:1)保存当前进程的上下文,2)恢复被抢占进程被保存的上下文,3)将控制转移给这个新的进程;这样,内核就完成了对hello与其他进程的调度。
用户态与核心态转换:
Hello初始是在用户模式中的,进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。当hello执行过程中异常发生时,如键盘按下ctrl-c,控制传递到异常处理程序,处理器将用户模式变为内核模式。处理器运行在内核模式中,当他返回到用户代码时,处理器就把模式从内核模式改回用户模式,以上。
6.6 hello的异常与信号处理
下表为hello运行过程中可能出现的异常种类:
异常类别 | 可能的诱发原因 | 处理方式 |
中断 | 收到I/O设备的信号,如键盘输入 | 处理器读取异常号,调用中断处理程序,返回下一条指令 |
陷阱 | Hello的父进程执行syscall指令fork一个hello | 陷阱处理程序在内核态中完成fork工作,返回syscall之后的指令 |
故障 | 缺页异常 | 缺页异常处理程序从磁盘加载适当的页面,然后重新执行当前指令 |
终止 | 出现DRAM或SRAM位损坏的奇偶错误 | Abort例程终止该程序 |
表6.61 hello执行中出现的异常
Hello执行中输入命令及其运行结果:
乱按:
图6.61 乱按输入结果
如图6.61,将屏幕的输入缓存到缓冲区。乱码被认为是命令,并自动执行光标选中行;
Ctrl-Z + 其他命令:
图6.62 Ctrl-Z + ps、jobs、fg
如图6.62,按下CtrlZ后hello停止运行,并输出其作业号为1,此时输入ps,屏幕输出三个进程号及其名字;然后输入jobs,屏幕输出作业信息;最后输入fg加上hello的作业号1,hello继续执行。
图6.63 Ctrl-Z + Ctrl-C、kill
如图6.63,hello运行中按下ctrl C,然后输入ps,并未发现hello,说明其已经结束;然后再次运行hello,输入ctrl Z,使用ps得知hello进程号为2621,然后再输入kill -9 2621杀死hello,此时ps发现已找不到hello,证明其被杀死。
6.7本章小结
本章对进程展开研究,hello在处理器中可能与其他进程并发执行,这就会涉及进程调度与上下文维护。进程运行过程中可能会发生各种异常,针对不同的异常有着不同的处理方式,但都是在内核态完成对异常的处理。进程同样会对信号做出响应,例如在shell中输入crtl z时hello会停止运行,而输入fg命令又会恢复其运行状态。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:
逻辑地址即程序中的段地址,逻辑地址由两部份组成,段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。
线性地址:
线性地址是逻辑地址到物理地址之间的一个中间层变换,程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么MMU内存管理单元会在内存映射表里寻找与线性地址对应的物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。
虚拟地址:
虚拟地址是CPU保护模式下的一个概念,保护模式是80286系列和之后的x86兼容CPU操作模式,在CPU引导完操作系统内核后,操作系统内核会进入一种CPU保护模式,也叫虚拟内存管理,在这之后的程序在运行时都处于虚拟内存当中,虚拟内存里的所有地址都是不直接的,所以你有时候可以看到一个虚拟地址对应不同的物理地址,比如hello进程里的call函数入口虚拟地址是0x001,而另一个进程也是,但是它俩对应的物理地址却是不同的,操作系统采用这种内存管理方法
物理地址:
物理地址就是内存中每个内存单元的编号,这个编号是顺序排好的,物理地址的大小决定了内存中有多少个内存单元,物理地址的大小由地址总线的位宽决定。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段偏移量加上基地址的和,构成线性地址。其中,段偏移量为逻辑地址的组成部分;基地址存储在段描述符表中,该表存储有多个描述符,每个描述符都描述了某个段的起始位置与大小等信息;而逻辑地址中的另一部分:段标识符的高13位为段选择符,段选择符能对应上段描述表中的一个描述符。
综上,逻辑地址到线性地址的变换过程为,取逻辑地址的段标识符中的段选择符,到段描述表中找到对应的描述符,描述符中存有段开始的线性地址,即段基址;段基址加上逻辑地址中的段偏移量就是线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址即hello程序虚拟地址空间中的虚拟地址,虚拟内存空间与物理内存空间都被划分为页,与页号相对应。虚拟地址由虚拟页号 + 虚拟页偏移量组成。页表是建立虚拟页号与物理页号映射关系的表结构,页表项包含有有效位、物理页号、磁盘地址等信息。如下图7.31
图7.31 物理内存与虚拟内存在页表上的对应
(图片来源CSDN)
虚拟页号 + 页表起始地址能找到相对应的页表项,页表起始地址存储在页表基址寄存器中,页表项存储的页表状态有三种:未分配,已缓冲,未缓冲。当对应状态为已缓冲时,说明虚拟页所对应的物理页已经存储在内存中,此时页表项存储的物理页号 + 物理页偏移量即为物理地址,而物理页偏移量与虚拟页偏移量相同,可以从虚拟地址中直接得出。页表项状态为已缓冲时,对应页式管理过程如下图7.32
图7.32 页表项状态为已缓冲的页式管理过程
(图片来源CSDN)
当页表项中状态为未缓存时,若要读取该页,会引发缺页中断异常,缺页异常处理程序根据页置换算法,选择出一个牺牲页,如果这个页面已经被修改了,则写出到磁盘上,最后将这个牺牲页的页表项有效位设置为0,存入磁盘地址。缺页异常程序处理程序调入新的页面,如果该虚拟页尚未分配磁盘空间,则分配磁盘空间,然后磁盘空间的页数据拷贝到空闲的物理页上,并更新页表项状态为已缓存,更新物理页号,缺页异常处理程序返回后,再回到发生缺页中断的指令处,重新按照页表项命中的步骤执行。
7.4 TLB与四级页表支持下的VA到PA的变换
多级页表可以减小翻译地址时的时间开销。多级页表中,页表基址寄存器存储一级页表的地址,1到3的页表的每一项存储的下一级页表的起始地址,4级页表的每一项存储的是物理页号或磁盘地址。解析VA时,其前m位vpn1寻找一级页表中的页表项,接着一次重复k次,在第k级页表获得了页表条目,将PPN与VPO组合获得物理地址PA。
7.5 三级Cache支持下的物理内存访问
高速缓存的组织方式如图7.51,高速缓存的结构将地址划分成了t个标记位、s个组索引位和b个块偏移位。当cpu执行一条读内存字w的指令时,它会首先向一级cache请求这个字,如果缓存命中,那么高速缓存会很快将该字返回给cpu,若不命中,则向下一级缓存发起请求。
图7.51 高速缓存组织方式
以组相联高速缓存为例,判断缓存是否命中,然后取出字的过程分为三步:
- 组选择
高速缓存从w的地址中抽取出s个组索引位,这些位被解释为一个对应于一个组号的无符号整数,用于在缓存中进行组选择。
- 行匹配
确定了缓存中的组i后,缓存将搜索组中的每一行,直到某行标记位与地址中的标记位一致,如果能找到这样的一行,那么即为命中。
如果w不在组中的任何一行,那么就是缓存不命中,缓存会从下一级存储空间(例如L1的下一级为L2)中取出包含这个字的块,并依照特定的行替换策略将该行放入缓存中,行替换策略保证被替换行的被引用概率最低。
- 字选择
在命中的行中,使用块偏移选中字w返回给cpu。
7.6 hello进程fork时的内存映射
当fork函数被父进程调用时,内核为子进程创建各种数据结构,并分配它唯一的一个PID。为给这个新进程创建虚拟内存,它创建当前进程的mm_struct、区域结构与页表的原样副本;它将两个进程中的每个页面都标记为只读,并把两个进程中的每个区域结构都标记为私有的写时复制。
当fork在子进程中返回时,其现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程的任一者进行后续写操作时,写时复制机制就会创建新页面,也就为每个进程保持私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
在子进程中调用execve函数加载hello时,将完成以下工作
1)删除已存在的用户区域。
2)映射私有区域
3)映射共享区域
4)设置程序计数器(PC)
图7.71 虚拟内存空间(来源CSDN)
最后exceve设置当前进程的上下文中的程序计数器,指向代码区域的入口点。而下一次调度这个进程时,他将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
当指令引用一个地址,而与该地址相应的物理页面不在内存中,即PTE中的有效位是0,所以MMU出发了一次异常,会触发缺页故障,内核调用缺页处理程序。通过查询页表PTE可以知该页在磁盘的位置。缺页处理程序从指定的地址加载页面到物理内存中,然后更新PTE。再将控制返回给引起缺页故障的指令。当该指令再次执行时,相应的物理页面已加载在内存中,因此能够命中。
7.9动态存储分配管理
所有动态申请的内存都存在堆上面,用户通过保存在栈上面的一个指针来使用该内存空间。动态内存分配器维护着堆,堆顶指针是brk。有两种风格,一种叫显式分配器,使用两个函数,malloc和free,分别用于执行动态内存分配和释放。
malloc的作用是向系统申请分配堆中指定size个字节的内存空间。也就是说函数返回的指针为指向堆里的一块内存。并且,操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序申请时,就会遍历该链表,然后寻找第一个空间大于所申请空间的堆结点,将该结点从空闲结点链表中删除后,将该结点的空间分配给到程序。在使用malloc()分配内存空间后,需释放内存空间,否则就会出现内存泄漏。
free()释放的是指针指向的内存,而不是指针。指针并没有被释放,它仍然指向原来的存储空间。因此指针需要手动释放,指针是一个变量,只有当程序结束时才被销毁。释放了内存空间后,原本指向这块空间的指针仍然存在。但此时指针指向的内容为垃圾,是未定义的。因此,释放内存后要把指针指向NULL,防止该指针后续被解引用。
7.10本章小结
本章主题是hello的存储管理,在不同的存储空间中有着不同的地址,虚拟地址空间中有虚拟地址,物理存储空间中有物理地址,而程序中使用的是逻辑地址,段式管理将逻辑地址转换为线性地址,页式管理将线性地址转换为物理地址。处理器借助地址向内存请求数据,多级高速缓存让计算机整体的存储结构既拥有趋近于L1cache的高速,又有巨大的存储空间。当指令引用一个地址,而与该地址相应的物理页面不在内存中时,就要调用缺页故障处理程序。此外,本章还分析了hello进程fork与execve时的内存映射,并在最后介绍了动态存储分配管理的方法与原理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m个字节的序列:
B0,B1,B2……Bk……Bm-1
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix I/O 接口:
- 打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
- 改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行 seek,显式地将改变当前文件位置 k。
- 读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置 k 开始,然后将k增加到 k+n,给定一个大小为m字节的而文件,当 k>=m时,触发 EOF。类似一个写操作就是从内存中复制 n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
- 关闭文件:内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
8.3 printf的实现分析
研究printf的实现,首先来看看printf函数的函数体
int printf(const char fmt, …)
{
int i;
char buf[256];
va_list arg = (va_list)((char)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串长度。
接下来我们追踪write:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
这里是给几个寄存器传了几个参数,然后以int结束。将栈中的参数放入寄存器,ecx为字符个数,ebx存放第一个字符地址。
再来看看sys_call的实现:
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG – P_STACKBASE], eax
cli
ret
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
完成了printf。
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)” />}
当使用getchar时,程序发生陷阱的异常。当按键盘时会产生中断。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux I/O设备的基本概念和管理方法,简述Unix IO接口及其函数,以及分析printf 函数和 getchar 函数的实现。
结论
Hello的一生:
- 程序员用双手将他塑造——hello.c;
- 经过预处理:宏替换、插入库函数他变成了hello.i;
- 编译器将代码变为汇编语言,他变成了hello.s;
- 汇编器把他变成了进制可重定位目标文件hello.o;
- Hello.o链接得到了hello,一个可执行文件;
- Shell中父进程调用fork子进程作为他的载体,子进程中execve函数加载并运行新程序hello;
- 在他的虚拟内存中,MMU为他翻译地址,CPU为他分配时间片,管理上下文;
- 他若遇到了异常有内核帮他处理,若收到ctrl Z则由shell将他挂起;
- hello执行printf函数时会调用malloc向动态内存分配器申请堆中的内存;
- hello调用sleep函数时,会陷入内核模式,处理休眠请求主动释放当前进程,内核进行上下文切换将当前进程的控制权交给其他进程。当sleep函数调用完成时,内核执行上下文切换将控制传递给当前进程。
- 退出状态是他最后的遗言,父进程将他回收,他的痕迹也随之湮灭于0与1的海洋中。
Hello的一生结束了,可我的修行才刚刚开始。计算机如同集结人类智慧的山峰,CSAPP将我引到了这山脚下,让我得以一窥其巍峨。长路漫漫,归途何期?我认为程序员的求索之路是无穷无尽的,无数的科学家将计算机缔造,未来也将由后人不断发展,屏幕上闪动的光标,既是艺术的绽放,更是技术的硕果。而我应该做的,无非虚怀若谷,小心谨慎地一路登攀。
附件
hello.c 源文件
hello.i 预处理后的文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位文件
hello 链接后的可执行文件
helloelf.txt hello的elf文件
hellooelf.txt hello.o的elf文件
helloobjdump.txt hello的反汇编代码
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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.
[7] Randal E. Bryant. 深入理解计算机系统[M],第三版,龚奕利,机械工业出版社,2016:1-640.
[8] 王爽. 汇编语言[M]. 第3版,清华大学出版社,2013:14-172.
[9] CSDN博客 https://blog.csdn.net/daocaokafei/article/details/116207148?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165235562016781683965613%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=165235562016781683965613&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_click~default-1-116207148-null-null.142^v9^pc_search_result_control_group,157^v4^new_style&utm_term=%E8%99%9A%E6%8B%9F%E5%9C%B0%E5%9D%80&spm=1018.2226.3001.4187