计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 1190201728
班 级 1903011
学 生 左斐
指 导 教 师 史先俊
计算机科学与技术学院
2021年5月
本论文将CSAPP课程所学内容通过hello小程序的一生,对我们所学进行全面的梳理与回顾。我们主要在Ubuntu下进行相关操作,合理运用了Ubuntu下的操作工具,进行细致的历程分析,目的是加深对计算机系统的了解
关键词:hello程序,知识梳理,计算机系统,ubuntu
(摘要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.P2P简介
首先,程序员通过编辑文本或者某IDE(Codeblocks或vs)创建c语言代码,即hello.c,然后hello.c经过预处理器cpp得到hello.i(ASCII码),接着通过ccl(编译器)将hello.i转为hello.s,再通过运行as(汇编器)得到可重定位目标文件hello.o,最后通过ld(连接器)将hello.o变为hello可执行程序。通过在命令行输入./hello启动程序,shell创建子进程,hello单独成为一个进程。
2.O2O简介
首先,通过调用fork函数产生子进程,然后系统调用execve函数,并进行mmp(虚拟内存映射),同时为hello进程分配时间片执行取指,译码,执行,访存,写回,更新PC等操作;MMU则用来解决虚拟地址到物理地址的转换,通过TLB等加速访问过程,IO管理与信号处理综合软硬件处理信号,待程序结束,shell回收hello子进程,内核将hello的痕迹消除,hello的一生就此结束。
1.2 环境与工具
硬件工具:Intel Core i7 x64CPU 16G RAM 512G SSD
软件工具: Ubuntu 18.04.1 LTS
开发者与调试工具:edb,gcc,gdb,readelf,HexEdit,ld,codeblocks
1.3 中间结果
文件 | 作用 | 章节 |
hello.i | 预处理得到的文件 | 预处理 |
hello.s | ASCII汇编语言文件 | 编译 |
hello.o | 可重定位目标文件 | 汇编 |
hello | 可执行文件 | 链接 |
elf1.txt | hello.o的elf文件 | 汇编 |
elf2.txt | hello的elf文件 | 链接 |
hello1.txt | hello.o反汇编文本文件 | 汇编 |
hello2.txt | hello反汇编文件 | 链接 |
1.4 本章小结
本章简要介绍了hello.c的P2P与O2O,并列举了所用到的环境和工具,然后对所使用的所有中间文件的名字,作用以及章节进行了一个汇总、
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
1.预处理的概念
预处理,即预先的处理,即在编译之前的处理,由预处理程序完成,一般指在程序源代码被翻译成目标代码过程中生成二进制代码之前的过程,C语言提供多种与处理功能,如宏定义,文件包含和条件编译等等。
预处理过程对源代码进行初步转换产生新的源代码给编译器,预处理过程还会删除程序中的注释和多于空白字符。
2.预处理的作用
插入所有#include命令指定文件并扩展#define宏定义。
主要包含三部分内容:1.宏定义;2.文件包含;3.条件编译。
- 宏定义:预处理程序中的#define标识符文本。
- 文件包含:预处理程序中的#include,将头文件与源文件连接成一个原文件。
- 条件编译:#if和#endif,#ifdef和#ifndef判断条件。
2.2在Ubuntu下预处理的命令
cpp hello.c > hello.i
2.3 Hello的预处理结果解析
查看hello.i文本文件如下图:
根据上图可以看出hello.c经过预处理变成了ASCII码中间文本文件,这中间预处理器完成了头文件的展开,宏替换和条件编译。Hello.i的篇幅多大上千行,看一下具体操作:
- stdio的展开开始于第13行,结束与第691行,如下图:
- unistd的展开开始于694行,结束于2035行
- stdlib的展开开始于2040行,结束于3041行
这些造成了hello.i的巨大篇幅。
2.4 本章小结
本章介绍了预处理的概念和作用,结合实际程序分析了预处理的过程,包括宏替换、头文件引入、删除注释、条件编译等。并且我们对hello.i的内容进行了解析,从而解释了hello.i的巨大篇幅。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
1.编译的概念
编译是将源代码生成汇编代码的过程,即将预处理产生的AICII码中间文件翻译成一个ASCII汇编语言文件,具体来说即为将hello.i变为hello.s,这里编译会进行词法分析,语法分析,优化等操作。
2.编译的作用
将高级语言程序转化为机器语言的中间步骤。包括1.词法分析;2.语法分析;3.优化。
- 词法分析:对输入的字符串进行处理,形成源程序语言所允许的记号,同时标记不规范记号,产生错误提示。
- 语法分析:以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等
- 优化:将代码转化为运行时间更短或占用资源更少的等价代码。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
1.文件分析
.filet | 声明源文件 |
.text | 声明以下是代码段 |
.globl | 声明一个全局变量 |
.secetion .rodata | 声明以下是rodata节 |
.align | 声明对指令或者数据的存放地址进行对齐的 方式 |
.long | 声明一个long类型 |
.string | 声明一个string类型 |
.size | 声明大小 |
.type | 声明是函数类型还是对象类型 |
2.常量
两个printf参数为字符串常量,如下图:
在编译生成的hello.s中,z这两个字符串被LC0和LC1指示并存放在.rodata段
3局部变量
i(循环计数),如下图
通过hello.s中可以看到,i被存在%rbp-4的内存地址处,addl没以循环对i+1;然后和7比较来决定是否退出循环,i存放在栈上,并通过栈指针(%rsp)偏移量进行寻址。
其他局部变量argv和argc同样存放在栈上,通过栈指针偏移寻址。
- 数据类型
编译器根据数据的类型来选取不同的寄存器和不同的指令,但是编译完成时,所有的类型信息都不存在了,无法判断某个数据的类型。
- 赋值操作
即对循环变量i赋初值,如下图
在这里局部变量的赋初值通过mov类的数据传送指令,后缀取决于操作数的字节大小,b一个字节,w两个字节,l四个字节。Q八个字节,由于i为4个字节,所以用movl。
- 算术操作
这里的算术操作出现在循环体中i每次增加1的时候,即i++。
这里转化为add类的加法指令,用立即数1实现每次增加1,其他算术操作还有sub,imul,inc,dec等等。
- 判断操作
共出现了两次判断操作
- 在if中判断argc取值是否不等于4,编译得到下图:
使用cmpl指令将argc与4及逆行不叫,设置条件码,je根据条件码决定是否跳转。
- 在for循环中判断结束条件,即判断i是否小于8,得到下图:
使用cmpl指令将i和9进行比较,并设置条件码,jle根据条件码判断是否跳转。
- 数组操作
在访问argv元素时,通过argv[1]和argv[2]访问字符指针数组中元素。如下图:
采用首地址+偏移量的方式访问,数组首地址在%rbp-32的位置,通过将首地址+8得到argv[1]地址,+16得到argv[2]的地址,指针数组的大小为8个字节。
- 函数操作
共进行了六次函数的调用,用call指令进行跳转,如下图:
- 参数传递
大部分参数通过寄存器实现,最多传递6个参数,寄存器顺序为rdi,rsi,rdx,rcx,r8,r9,多余参数压栈。
第一个函数用rdi传递
第二个函数用edi传递
第三个函数通过rdi,rsi,rdx传递
第四个函数用rdi传递
第五个函数用edi传递
第六个参数无参数
3.4 本章小结
本章介绍了编译的概念和作用,并针对hello.s分析了编译器怎样处理C语言的各种数据和各类操作。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
1.汇编的概念
驱动程序运行汇编器as将代码语言翻译成机器语言,即hello.s翻译成hello.o,即可重定位目标文件。
2.汇编的作用
将高级语言转化为机器可直接识别执行的代码,汇编器将指令打包为可重定位目标程序的格式,并将结果保存在.o目标文件中,.o为一个二进制文件,它包含程序的指令编码。
4.2 在Ubuntu下汇编的命令
as hello.s -o hello.o
4.3 可重定位目标elf格式
1.读取可重定位目标文件
输入readelf -a hello.o >elf1.txt,将elf可重定位目标文件输出定向到elf文本文件中,如下图:
2.
ELF头:以 16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的。
如图,该序列描述了生成该文件的系统字大小和字节顺序,该字节序列为7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00,描述了系统的字的大小为8字节,字节顺序为小端序。ELF头中包含了ELF头的大小:64字节;目标文件的类型:REL(可重定位文件);机器类型:Advanced Micro Devices X86-64, 节头部表的文件偏移:1240;节头部表中条目的数量:13
3头部表
(1).text节,已编译程序的机器代码,大小为0x92字节,类型为PROGBITS,偏移量为0x40,标志位AX
(2)rela.text节,text节位置列表,大小为0xc0字节,类型为RELA,偏移量为0x388,标志位I
(3)data节,已初始化的全局和静态C变量,大小为0x0字节,类型为PROGBITS,偏移量为0xd2,标志为WA
(4)bss节,未初始化的全局和静态C变量,和所有被初始化为0的全局或静态变量,大小为0x0,类型为NOBITS,偏移量为0xd2,标志为WA
(5)rodata节,只读数据,大小为0x33字节,类型为PROGBITS,偏移量为0xd8,标志为A
(6)comment节,包含版本控制信息,大小为0x2b字节,类型为PROGBITS,偏移量为0x10b,标志为MS。
(7)note.GNU_stack节:标记可执行堆栈,大小为0x0字节,类型为PROGBITS,偏移量为0x136
(8)note.gnu.propert节,大小为0x20字节,类型为NOTE,偏移量为0x138
(9)eh_frame节:处理异常,大小为为0x38字节,类型为PROGBITS,偏移量为0x158,标志为A
(10)rela.eh_frame节:.eh_frame节的重定位信息,大小为0x18字节,类型为RELA,偏移量为0x448,标志为I
(11)symtab节:一个符号表,存放在程序中定义和引用的函数和全局变量的信息。大小为0x1b0字节,类型为SYMTAB,偏移量为0x190
(12)strtab节:一个字符串表,包括.symtab和.debug节中的符号表,以及节头部中的节名字。大小为0x48字节,类型为STRTAB,偏移量为0x340
(13)shstrtab节:包含节区名称,大小为0x74字节,类型为STRTAB,偏移量为0x460.
4.符号表
符号表存放程序中定义和引用的函数和全局变量的信息,每个符号表是一个条目的数组,每个条目包括value:距定义目标的节的起始位置的偏移;size:目标的大小;type:指明数据还是函数;bind:表示符号是本地的还是全局的等等
该符号表一共描述了18个符号,而对于函数main,Ndx=1表明它在.text节,value=0表明它在.text节中偏移量为0的地方,size=146表明大小为146字节,bind=GLOBAL表明它是全局符号,type=FUNC:表明它是函数。puts、exit、printf、sleep和getchar都是外部的库函数,需要在链接后才能确定
5重定位节
汇编器遇到对最终位置未知的目标引用,会产生一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用,每个重定位条目包括:
- offset:需要被修改的引用的节偏移
- symbol:标识被修改引用应该指向的符号
- type:重定位类型,告知链接器如何修改新的引用
- attend:一些重定位要使用它对被修改引用的值做偏移调整
- 两种最基本的重定位类型包括R_X86_64_PC32(重定位使用32位PC相对地址的引用)和R_X86_64_32(重定位使用32位绝对地址的引用)
该重定位rela.text一共描述了8个重定位条目
重定位节.rela.eh_frame描述了1个重定位条目
4.4 Hello.o的结果解析
objdump -d -r hello.o >hello1.txt
与hello.s进行比较可以发现
- hello.s中的汇编指令被映射到二进制的机器语言。机器语言完全是二进制代码构成的,机器可以直接根据二进制代码执行对应的操作
- 不同的汇编指令被映射到不同的二进制功能码,而汇编指令的操作数也被映射成二进制的操作数
- 还有一些操作数不一样的情况,比如:在机器语言中,call指令后是被调函数的PC相对地址。在这里,由于调用的函数都是库函数,需要在动态链接后才能确定被调函数的确切位置,因此call指令后的二进制码为全0,同时需要在重定位节中添加重定位条目,在链接时确定最终的相对地址。
4.5 本章小结
本章对汇编结果进行了详尽的介绍。与我们的hello.o文件相结合,介绍了汇编的概念与作用,以及在Ubuntu下汇编的命令。同时本章主要部分在于对可重定位目标elf格式进行了详细的分析,侧重点在重定位项目上。同时对hello.o文件进行反汇编,将hello1和生成的hello.s文件进行了对比。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
1.链接的概念
链接是将各种代码和数据片段收集并组合为单一文件的过程,这个文件可以被加载(复制)到内存并执行
2.链接的作用
(1)链接可以执行于编译时,也就是源代码被翻译成机器代码时;
(2)链接可以执行于加载时,即程序被加载器加载到内存并执行时
(3)链接可以执行于运行时,也就是由应用程序来执行
(4)链接使得分离编译成为可能。更便于我们维护管理,我们可以独立的修改和编译我们需要修改的小的模块
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.3 可执行目标文件hello的格式
readelf -a hello > elf2.txt 查看可执行目标文件hello的ELF格式。
- ELF头
以 16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的。
如图,该序列描述了生成该文件的系统字大小和字节顺序,该字节序列为7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00,描述了系统的字的大小为8字节,字节顺序为小端序。ELF头中包含了ELF头的大小:64字节;目标文件的类型:REL(可重定位文件);机器类型:Advanced Micro Devices X86-64, 节头部表的文件偏移:14208,节头部表中条目的数量:27.
- 节头部表
hello的节头部表一共描述了27个不同节的位置、大小等信息,比hello.o多了14个,各节的起始地址由偏移量给出,同时也给出了大小等信息
- 程序头部表
程序头部表描述了可执行文件的连续的片映射到连续的内存段的映射关系。包括目标文件的偏移、段的读写/执行权限、内存的开始地址、对齐要求、段的大小、内存中的段大小等
第一个LOAD,Offset说明段的偏移量为0;VirtAddr说明映射到的虚拟内存段的开始地址是0x400000;FileSiz说明段的大小为0x5c0字节,Memsiz说明内存中的段大小也是0x5c0字节,Flags为R,;Align说明段的对齐要求为0x1000.
- 符号表
hello的符号表一共描述了51个符号,比hello.o多出33个符号,多出的符号都是链接后产生的库中的函数以及一些必要的启动函数
hello中还多出了一个动态符号表,表中的符号都是共享库中的函数,需要动态链接,如下图:
- 重定位节
如图,原先的rela.text节消失,说明链接的过程已经完成了对.rela.text的重定位操作。hello中出现了6个新的重定位条目。这些重定位条目都和共享库中的函数有关,因为此时还没有进行动态链接,共享库中函数的确切地址仍是未知的,因此仍然需要重定位节,在动态链接后才能确定地址。
5.4 hello的虚拟地址空间
使用edb加载hello,可以看出,段的虚拟空间从0x401000开始,到0x401240结束
对于.interp节,节头部表中给出了它的偏移量为0x2e0,大小为0x1c字节,因此它的虚拟地址空间就从0x4003e0开始,在edb中查看该虚拟内存地址,可以看出,.interp节确实在这个位置。
对于其他的节同理
5.5 链接的重定位过程分析
objdump -d -r hello > hello2.txt
与hello.o的不同之处如下:
- 在hello.o中,只存在main函数的汇编指令;而在hello中,由于链接过程中发生重定位,引入了其他库的各种数据和函数,以及一些必需的启动/终止函数,因此hello中除了main函数的汇编指令外,还包括大量其他的指令
- hello中的汇编代码已经使用虚拟内存地址来标记
- 之前汇编的过程中,汇编器遇到对最终位置未知的目标引用,会产生一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。因此在链接的过程中,链接器会根据重定位条目以及已知的最终位置对修改指令的二进制码,这个过程就是重定位的过程
- R_X86_64_PC32(重定位使用32位PC相对地址的引用)
- R_X86_64_32(重定位使用32位绝对地址的引用)
5.6 hello的执行流程
401000 <_init>
401020 <.plt>
401080
401090
4010a0
4010b0
4010c0
4010d0 <_start>
401100 <_dl_relocate_static_pie>
401105
401190 <__libc_csu_init>
401200 <__libc_csu_fini>
401208 <_fini>
5.7 Hello的动态链接分析
延迟绑定:GNU编译系统使用一种称为延迟绑定的技术将过程地址的绑定推迟到第一次调用该过程时,延迟绑定是通过两个数据结构之间的交互来实现的,分别是GOT和PLT,GOT是数据段的一部分,而PLT是代码段的一部分。PLT与GOT的协作可以在运行时解析函数的地址,实现函数的动态链接。
如图,可以查看到每个PLT条目。
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要为其添加重定位记录,并等待动态链接器处理。为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT 中存放函数目标地址,PLT使用 GO T中地址跳转到目标函数。
5.8 本章小结
本章结合实验中的hello可执行程序依此介绍了链接的概念及作用,在Ubuntu下链接的命令行;并对hello的elf格式进行了详细的分析对比,同时注意到了hello的虚拟地址空间知识;并通过反汇编hello文件,将其与hello.o反汇编文件对比,详细了解了重定位过程;遍历了整个hello的执行过程,在最后对hello进行了动态链接分析。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
1.进程的概念
进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
2.进程的作用
(1)现代计算机中,进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序 一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行 我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象
(2)次用户通过向shell 输入一个可执行目标文件的名字,运行程序时, shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序
(3)进程提供给应用程序两个关键抽象:一个独立的逻辑控制流;一个私有的地址空间
6.2 简述壳Shell-bash的作用与处理流程
1.shell的作用
Shell是用户与操作系统之间完成交互式操作的一个接口程序,它为用户提供简化了的操作
2.处理流程
1. 将用户输入的命令行进行解析,分析是否是内置命令
2. 若是内置命令,直接执行;若不是内置命令,则bash在初始子进程的上下文中加载和运行它
3. 本质上就是shell在执行一系列的读和求值的步骤,在这个过程中,他同时可以接受来自终端的命令输入
6.3 Hello的fork进程创建过程
执行中的进程调用fork()函数,就创建了一个子进程。其函数原型为pid_t fork(void);对于返回值,若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1。
- 对于hello进程。我们终端的输入被判断为非内置命令,然后shell试图在硬盘上查找该命令,并将其调入内存,然后shell将其解释为系统功能调用并转交给内核执行
- hell执行fork函数,创建一个子进程。hello程序就开始运行。
- Linux将复制父进程的地址空间给子进程,因此,hello进程就有了独立的地址空间
6.4 Hello的execve过程
在前文提到的shell父进程创建的子进程中,子进程会调用execve函数,其参数主要是两个二级指针char **argv , char **envp,其作用主要是给出加载程序的参数。只有在加载文件出现错误的时候,例如找不到目标文件,execve函数才会返回,否则就直接执行程序,不再返回。
hello子进程通过execve系统调用启动加载器。加载器删除子进程所有的虚拟地址段,并创建一组新的代码、数据、堆段。新的栈和堆段被初始化为0,通过将虚拟地址空间中的页映射到可执行文件的页大小的片(chunk),新的代码和数据段被初始化为可执行文件中的内容,最后加载器跳到_start地址,它最终调用hello的main 函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,此时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
6.5 Hello的进程执行
1.逻辑控制流
所谓逻辑控制流,就是控制程序的逻辑行为,一步一步的流向,可以使用流程图来表现这种行为的流动。控制流一般分成正常控制流和异常控制流,正常就是一切按预期的方向发展,异常就是控制流的突变
2.上下文
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等构成。此外, 还包括进程打开的文件描述符等等
3.调度
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被强占的进程。这种决策就叫调度
- 上下文切换
在内核调度了一个新的进程运行时,它就抢占当前进程,并使用一种上下文切换的机制来控制转移到新的进程
6.6 hello的异常与信号处理
1.异常
1)中断:来自处理器外部的I/O设备的信号的结果
2)陷阱:有意的,执行指令的结果
3)故障:当前进程发生了一些错误,而这种错误有可能可以被修复
4)终止:发生了一个不可以被修复的错误
2.正常运行状态
按下Ctrl+z
按下Ctrl+Z之后,进程会收到一个SIGSTP 信号,使得当前的hello进程被挂起
用ps指令查看其进程PID为4784,用jobs查看此时hello的后台 job号是1,调用指令fg 1将其调回前台
按下Ctrl+c
此时进程收到一个SIGINT 信号,一次结束 hello。我们输入ps指令,发现查询不到hello进程的PID,输入指令jobs,发现也没有对应作业,因此hello进程被彻底终止
中途乱按
程序只会将其记录在输入缓冲区,不会影响程序的运行
Kill命令
通过kill指令向所在的挂起的进程发出终止指令,在此之后,通过ps指令无法找到对应的进程,对应的jobs指令也无法找到作业,说明进程已经被终止
6.7本章小结
异常控制流发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制。
1)在硬件层,异常是由处理器中的事件触发的控制流中的突变
2)在操作系统层,内核用ECF提供进程的基本概念。
3)在操作系统和应用程序之间的接口处,应用程序可以创建子进,等待他们的子进程停止或者终止,运行新的程序,以及捕获来自其他进程的信号。
同时还有四种不同类型的异常:中断,故障,终止和陷阱。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
1. 逻辑地址
指由程序hello产生的与段相关的偏移地址部分
2. 线性地址
是逻辑地址到物理地址变换之间的中间层
3虚拟地址
逻辑地址称为虚拟地址
4.物理地址
指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
个段的首地址就会被储存在各自的段描述符里面,所以的段描述符都将会位于段全局描述符表中(每个段的全局描述符表一个局部称为gdgdt和另一个局部的的段描述符表一个局部称为ldldt),通过段选择符我们可以快速寻找到某个段的段全局描述符。逻辑上段地址的偏移量结构就是段选择符+偏移量。
段选择符的索引位组成和定义如下,分别指的是索引位(index),ti,rpl,当索引位ti=0时,段描述符表在rpgdt中,ti=1时,段描述符表在rpldt中。而索引位index就类似一个数组,每个元素内都存放一个段的描述符,索引位首地址就是我们在查找段描述符时再这个元素数组当中的索引。一个段描述符的首地址是指含有8个元素的字节,我们通常可以在查找到段描述符之后获取段的首地址,再把它与线性逻辑地址的偏移量进行相加就可以得到段所需要的一个线性逻辑地址
intel储存器寻找
逻辑地址->线性地址
7.3 Hello的线性地址到物理地址的变换-页式管理
基地址 | AVL | 0 | 0 | D | A | PCD | PWT | U/S | R/W | P |
1)P:1表示页表或页在主存中;P=0表示页表或页不在主存,即缺页,此时需将页故障线性地址保存到CR2。
2)R/W:0表示页表或页只能读不能写;1表示可读可写。
3)U/S:0表示用户进程不能访问;1表示允许访问。
4)PWT:控制页表或页的cache写策略是全写还是回写(Write Back)。
5)PCD:控制页表或页能否被缓存到cache中。
6A:1表示指定页表或页被访问过,初始化时OS将其清0。利用该标志,OS可清楚了解哪些页表或页正在使用,一般选择长期未用的页或近来最少使用的页调出主存。由MMU在进行地址转换时将该位置1。
7D:修改位(脏位dirty bit)。页目录项中无意义,只在页表项中有意义。初始化时OS将其清0,由MMU在进行写操作的地址转换时将该位置1。
8高20位是页表或页在主存中的首地址对应的页框号,即首地址的高20位。每个页表的起始位置都按4KB对齐。
7.4 TLB与四级页表支持下的VA到PA的变换
1. 使用K级页表的地址翻译
对于其中各表位,解释如下
每个条目引用一个 4KB子页表:
1)P: 子页表在物理内存中 (1)不在 (0).
2)R/W: 对于所有可访问页,只读或者读写访问权限.
3)U/S: 对于所有可访问页,用户或超级用户 (内核)模式访问权限.
4)WT: 子页表的直写或写回缓存策略.
5)A: 引用位 (由MMU 在读或写时设置,由软件清除).
6)PS: 页大小为4 KB 或 4 MB (只对第一层PTE定义).
7)Page table physical base address: 子页表的物理基地址的最高40位 (强制页表 4KB 对齐)
8)XD: 能/不能从这个PTE可访问的所有页中取指令。
P: 子页表在物理内存中 (1)不在 (0).
R/W: 对于所有可访问页,只读或者读写访问权限.
U/S: 对于所有可访问页,用户或超级用户 (内核)模式访问权限.
WT: 子页表的直写或写回缓存策略.
A:引用位 (由MMU 在读或写时设置,由软件清除).
D: 修改位 (由MMU 在读和写时设置,由软件清除)
Page table physical base address: 子页表的物理基地址的最高40位 (强制页表 4KB 对齐)
XD: 能/不能从这个PTE可访问的所有页中取指令.
7.5 三级Cache支持下的物理内存访问
- 得到了物理地址VA,首先使用物理地址的CI进行组索引,对8路的块分别匹配 CT进行标志位匹配。如果匹配成功且块的valid标志位为1,则命中hit。然后根据数据偏移量 CO取出数据并返回
- 若没找到相匹配的或者标志位为0,则miss。那么cache向下一级cache,这里是二级cache甚至三级cache中寻找查询数据。然后逐级写入cache
- 在更新cache的时候,需要判断是否有空闲块。若有空闲块(即有效位为0),则写入;若不存在,则进行驱逐一个块
7.6 hello进程fork时的内存映射
1. 虚拟内存和内存映射解释了fork函数如何为hello进程提供私有的虚拟地址空间
2. fork为hello的进程创建虚拟内存
3. 在hello进程中返回时,hello进程拥有与调用fork进程相同的虚拟内存
4. 随后的写操作通过写时复制机制创建新页面
7.7 hello进程execve时的内存映射
1. 在bash中的进程中执行了如下的execve调用:execve("hello",NULL,NULL);
2. execve函数在当前进程中加载并运行包含在可执行文件hello中的程序,用hello替代了当前bash中的程序
加载并运行hello的几个步骤:删除已存在的用户区域,映射私有区域,
映射共享区域,设置程序计数器
3. exceve做的最后一件事是设置当前进程的上下文中的程序计数器,是指指向代码区域的入口点。而下一次调度这个进程时,他将从这个入口点开始执行。Linux将根据需要换入代码和数据页面
7.8 缺页故障与缺页中断处理
流程:
- 处理器生成一个虚拟地址,并将它传送给MMU
- MMU生成PTE地址,并从高速缓存/主存请求得到它
- 高速缓存/主存向MMU返回PTE
- PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序
- 缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘
- 缺页处理程序页面调入新的页面,并更新内存中的PTE
- 缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中
7.9动态存储分配管理
1. 显式分配器必须在严格的约束条件下工作
2. 分配器的编写应该实现:吞吐率最大化;内存使用率最大化
3. 我们需要注意这几个问题:空闲块组织方式;放置策略;分割策略;合并策略
4. 带边界标记的隐式空闲链表可以提高空闲块合并效率;显式空闲链表可以有效地实现空闲块的快速查找与合并等操作;分离空闲链表采用大小类的方式标记空闲块;分离适配方法快速而且内存使用效率较高
31 3 | 210 |
块大小(头部) | a/f |
pred(祖先) | |
succ(后继) | |
填充(可选) | |
块大小(脚部) | a/f |
显示空闲链表结构
块大小(头部) | a/f |
有效载荷 (只包括已分配的块) | |
填充(可选) | |
块大小(脚部) | a/f |
带边界标签的隐式空闲链表
5. 适配块策略:首次适配或下一次适配或最佳适配。首次适配利用率较高;下一次适配时间较快;最佳适配可以很好的减少碎片的产生。我们在分离适配的时候采取的策略一般是首次适配,因为对分离空闲链表的简单首次适配的内存利用效率近似于整个堆的最佳适配的利用效率
6. malloc就是采用的是分离适配的方法
7.10本章小结
1. 拟内存是对主存的一个抽象。支持虚拟内存的处理器通过使用一种叫做虚拟内存寻址的间接形式来引用主存。处理器产生一个虚拟地址,在被发送到主存之前,这个地址被翻译成一个物理地址。从虚拟地址空间到物理地址空间的地址翻译要求硬件和软件紧密合作。专门的硬件使用页表来翻译虚拟地址,而页表的内容是由操作系统提供的
2. 虚拟内存提供三个功能:简化了内存保护;简化了内存管理;在主存中自动缓存最近使用的存放在磁盘上的虚拟地址空间的内容
3. 地址翻译的过程必须和系统中的所有的硬件缓存的操作集成在一起
4. 内存映射为共享数据、创建进程以及加载程序提供了一种高效的机制
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化
文件:所有的I/O设备都被模型化为文件,甚至内核也被映射为文件
unix io接口:
这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。
8.2 简述Unix IO接口及其函数
接口
- 打开文件
一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息,应用程序只需要记住这个描述符
- 改变当前文件位置
对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k
- 读写文件
个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号” 。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k
- 关闭文件
当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源
函数
- 打开和关闭文件
函数原型:int open(char* filename,int flags,mode_t mode)
为新文件描述符,否则返回-1
flags:O_RDONLY(只读),O_WRONLY(只写),O_RDWR(可读写)
mode:指定新文件的访问权限位。
关闭文件函数原型:int close(fd)
返回值:成功返回0,否则为-1
- 读和写文件
函数原型:ssize_t read(int fd,void *buf,size_t n)
返回值:成功则返回读的字节数,若EOF则为0,出错为-1
描述:从描述符为fd的当前文件位置复制最多n个字节到内存位置buf
写文件函数原型:ssize_t wirte(int fd,const void *buf,size_t n)
返回值:成功则返回写的字节数,出错则为-1
描述:从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置
8.3 printf的实现分析
1printf的函数体
- 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;
- }
我们发现函数体内部调用了函数vsprintf,那么我们再继续看一下vsprintf函数。
其中va_list的定义被定义为字符指针。
2vsprintf函数(在printf函数内部调用
- int vsprintf(char *buf, const char *fmt, va_list args)
- {
- char* p;
- char tmp[256];
- va_list p_next_arg = args;
- for (p=buf;*fmt;fmt++) {
- if (*fmt != '%') {
- *p++ = *fmt;
- continue;
- }
- fmt++;
- switch (*fmt) {
- case 'x':
- itoa(tmp, *((int*)p_next_arg));
- strcpy(p, tmp);
- p_next_arg += 4;
- p += strlen(tmp);
- break;
- case 's':
- break;
- default:
- break;
- }
- }
- return (p - buf);
- }
函数描述:vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
3系统函数write
反汇编追踪write函数
- write:
- mov eax, _NR_write
- mov ebx, [esp + 4]
- mov ecx, [esp + 8]
- int INT_VECTOR_SYS_CALL
发现反汇编语句中的int INT_VECTOR_SYS_CALL,它表示要通过系统来调用sys_call这个函数。
4sys_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
函数功能:显示格式化的字符串。将要输出的字符串从总线复制到显卡的显存中。
5字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
6显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。而我们要传输的“hello 1172510217 张景润”就会被打印输出在显示器上。
8.4 getchar的实现分析
1. 运行到getchar函数时,程序将控制权交给os。当你键入时,内容进入缓寸并在屏幕上回显。按enter,通知 os输入完成,这时再将控制权在交还给程序
2. 异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区
3. getchar调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回
8.5本章小结
Linux提供了少量的基于unix I/O模型的系统级函数。他们允许应用程序打开(open),关闭(close),读(read),写(write)文件,提取文件的元数据,以及执行I/O的重定向。
看似简单的printf函数其实底层实现非常复杂,他调用了函数vsprintf和系统调用write,而之后有调用了sys_call函数
(第8章1分)
结论
1,hello.c经过预编译,拓展得到hello.i文本文件
2,hello.i经过编译,得到汇编代码hello.s汇编文件
3,hello.s经过汇编,得到二进制可重定位目标文件hello.o
4,hello.o经过链接,生成了可执行文件hello
5,bash进程调用fork函数,生成子进程;并由execve函数加载运行当前进程的上下文中加载并运行新程序hello
6,hello的变化过程中,会有各种地址,但最终我们真正期待的是PA物理地址。
7,hello再运行时会调用一些函数,比如printf函数,这些函数与linux I/O的设备模拟化密切相关
8,hello最终被shell父进程回收,内核会收回为其创建的所有信息
(结论0分,缺失 -1分,根据内容酌情加分)
附件
文件 | 作用 | 章节 |
hello.i | 预处理得到的文件 | 预处理 |
hello.s | ASCII汇编语言文件 | 编译 |
hello.o | 可重定位目标文件 | 汇编 |
hello | 可执行文件 | 链接 |
elf1.txt | hello.o的elf文件 | 汇编 |
elf2.txt | hello的elf文件 | 链接 |
hello1.txt | hello.o反汇编文本文件 | 汇编 |
hello2.txt | 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.
[7]printf 函数实现的深入剖析. https://www.cnblogs.com/pianist/p/3315801.html
[8]深入了解计算机系统第三版
(参考文献0分,缺失 -1分)