2021-06-23

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机
学   号 1190200311
班   级 1903007
学 生 田翔宇    
指 导 教 师 吴锐

计算机科学与技术学院
2021年5月
摘 要
通过对hello.c的概述;hello.c的预处理;编译;汇编;链接;一直到hello.c的进程;存储;IO管理。通过了解hello的一生,将计算机系统这门课的知识串联起来,加深我们对知识的理解。
关键词:hello;预处理;编译;汇编;链接;进程;存储;虚拟内存;I/O;

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

目 录

第1章 概述 - 5 -
1.1 HELLO简介 -5 -
1.2 环境与工具 - 5 -
1.3 中间结果 - 5 -
1.4 本章小结 - 6 -
第2章 预处理 - 7 -
2.1 预处理的概念与作用 - 7 -
2.2在UBUNTU下预处理的命令 - 7 -
2.3 HELLO的预处理结果解析 - 7 -
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的结果解析 - 18 -
4.5 本章小结 - 20 -
第5章 链接 - 21 -
5.1 链接的概念与作用 - 21 -
5.2 在UBUNTU下链接的命令 - 21 -
5.3 可执行目标文件HELLO的格式 - 21 -
5.4 HELLO的虚拟地址空间 - 26 -
5.5 链接的重定位过程分析 - 27 -
5.6 HELLO的执行流程 - 32 -
5.7 HELLO的动态链接分析 - 32 -
5.8 本章小结 - 33 -
第6章 HELLO进程管理 - 35 -
6.1 进程的概念与作用 - 35 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 35 -
6.3 HELLO的FORK进程创建过程 - 35 -
6.4 HELLO的EXECVE过程 - 36 -
6.5 HELLO的进程执行 - 36 -
6.6 HELLO的异常与信号处理 - 36 -
6.7本章小结 - 38 -
第7章 HELLO的存储管理 - 39 -
7.1 HELLO的存储器地址空间 - 39 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 39 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 39 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 40 -
7.5 三级CACHE支持下的物理内存访问 - 40 -
7.6 HELLO进程FORK时的内存映射 - 41 -
7.7 HELLO进程EXECVE时的内存映射 - 41 -
7.8 缺页故障与缺页中断处理 - 41 -
7.9动态存储分配管理 - 41 -
7.10本章小结 - 42 -
第8章 HELLO的IO管理 - 43 -
8.1 LINUX的IO设备管理方法 - 43 -
8.2 简述UNIX IO接口及其函数 - 43 -
8.3 PRINTF的实现分析 - 44 -
8.4 GETCHAR的实现分析 - 45 -
8.5本章小结 - 45 -
结论 - 45 -
附件 - 47 -
参考文献 - 48 -

第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:hello.c分别经过预处理器cpp的预处理,、编译器ccl的编译,汇编器as的汇编依次生成生成hello.i文件,hello.s文件、,生成hello.o文件、最后使用链接器ld进行链接最终成为可执行目标程序hello. 在shell中键入启动命令后,shell为其fork产生一个子进程,然后hello便从程序变为了进程。
020:execve在父进程中fork一个子进程后,在子进程中调用exec函数启动新的程序,映射入内存,操作系统为这个进程分时间片。当该进程的时间片到达时,操作系统设置CPU上下文环境,并跳到程序开始处。当指令将“Hello World\n”字符串中的字节从主存经一系列cache复制到寄存器文件,再从寄存器文件中经I/O管理复制到显示设备,最终显示在屏幕上。程序执行完毕,shell负责回收这个子进程,此时hello不再占用内存。
1.2 环境与工具
硬件环境:Intel® Core™ i7-9750H CPU @ 2.60GHz
软件环境: Ubuntu16.04 LTS 64位/优麒麟 64位
开发与调试工具:vim,gcc,as,ld,edb,readelf,HexEdit

1.3 中间结果
hello.i hello.c预处理器后的程序
hello.s 编译器生成的编译程序
hello.o 可重定位目标程序
hello 可执行目标程序
elf.txt hello.o的elf格式
hello.txt 可执行hello的elf格式

1.4 本章小结
本章主要介绍了hello程序的P2P、020的过程并列出实验基本信息。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
预处理的基本概念:在源代码编译之前对源代码进行的处理。
c语言的预处理主要包括有三个预处理方面的基本定义内容:1.宏的定义;2.文件的包含;3.条件的编译。c语言预处理的命令以原始的字符符号"#"开头。
c语言的预处理器(cpp)根据以原始的c字符串中#开头的预处理命令,修改原始的c语言程序。在c语言和c/c++中常见的编译程序需要进行预处理的程序指令主要有:#define(宏定义),#include(源文件包含),#error(错误的指令)等。通过一个预处理命令使得一个源代码编译程序可以在不同的程序运行语言环境中被各种语言编译器方便的进行编译。
预处理的作用效果:以#include为例,预处理器(cpp)把程序中声明的文件复制到这个程序中,具体到hello.c的就是#include <unistd.h> #include <stdlib.h>#include<stdio.h>。cpp把这些头文件的内容插入到程序文本中,方便编译器进行下一步的编译。结果就是得到了另一个c程序,通常得到的程序以.i作为文件扩展名。
2.2在Ubuntu下预处理的命令
命令:gcc hello.c -E -o hello.i

图2.2.1
2.3 Hello的预处理结果解析
预处理后

图2.3.1
我们可以发现,原本仅仅只有几行的头文件hello.c已经被程序扩展到了数千行, 头文件被写入,define被替换,而原本的内容则在文件的最下面。

2.4 本章小结
本章首先介绍了预处理的定义与作用、之后结合预处理之后的hello.i程序对预处理的结果进行了简单分析。

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
编译,就是把代码转化为汇编指令的过程。 编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s

图3.2.1
3.3 Hello的编译结果解析
.file “hello.c”
.text
.section .rodata
.align 8
.LC0:
.string “\347\224\250\346\263\225: Hello \345\255\246\345\217\267 \345\247\223\345\220\215 \347\247\222\346\225\260\357\274\201”
.LC1:
.string “Hello %s %s\n”
.text
.globl main
.type main, @function
main:
.LFB6:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
cmpl $4, -20(%rbp)
je .L2
leaq .LC0(%rip), %rdi
call puts@PLT
movl $1, %edi
call exit@PLT
.L2:
movl $0, -4(%rbp)
jmp .L3
.L4:
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
leaq .LC1(%rip), %rdi
movl $0, %eax
call printf@PLT
movq -32(%rbp), %rax
addq $24, %rax
movq (%rax), %rax
movq %rax, %rdi
call atoi@PLT
movl %eax, %edi
call sleep@PLT
addl $1, -4(%rbp)
.L3:
cmpl $7, -4(%rbp)
jle .L4
call getchar@PLT
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE6:
.size main, .-main
.ident “GCC: (Ubuntu 9.2.1-9ubuntu2) 9.2.1 20191008”
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,“a”
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string “GNU”
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:

一.数据

  1. 字符串类型string

图3.3.1
这是程序的两个字符串
字符以UTF-8编码的格式存在只读的数据段(rodata)。
2. 局部变量 int i
局部数据空间的每个变量被认为储存在一个栈的寄存器或者栈中
3. argc
作为第一个参数存在存在寄存器edi中
4. 立即数
数字前带有$号
5. 数组argv[]
图3.3.2
这个数组文件中存放的指针是输入字符串的指针,程序中的字符串argv[1]和程序中的argv[2]的字符串地址分别被rax分两次进行读取.
图3.3.3
6. main函数
图3.3.4
hello.c声明了一个全局函数int main(int argc,char *argv[]),经过编译之后,main函数中使用的字符串常量也被存放在数据区。
二.操作
1.赋值操作
图3.3.5
MOV 指令将源操作数复制到目的操作数。作为数据传送(data transfer)指令,它几乎用在所有程序中。在它的基本格式中,第一个操作数是目的操作数,第二个操作数是源操作数。其中,目的操作数的内容会发生改变,而源操作数不会改变。
2,算术操作

图3.3.6
add、sub、adc、sbb、inc、dec、cmp、imul、idiv、aaa等都是算术运算指令,这些指令实现存器和内存中的数据的算数运算。
它们的执行结果影响标志寄存器的sf、zf、of、cf、pf、af位
CF(进位标志) =1 算术操作最高位产生了进位或借位 =0 最高位无进位或借位 ;
PF(奇偶标志) =1 数据最低8位中1的个数为偶数 =0 数据最低8位中1的个数为奇数;
AF(辅助进位标志) =1 D3→D4位产生了进位或借位 =0 D3→D4位无进位或借位;
ZF(零标志) =1 操作结果为0 =0 结果不为0;
SF(符号标志) =1 结果最高位为1 =0 结果最高位为0;
OF(溢出标志) =1 此次运算发生了溢出 =0 无溢出。
3.关系操作与控制转移
这部分在hello.c中为判断argc!=4,在这里被编译为如下图所示,它会根据结果得出条件码,影响之后je的跳转
图3.3.7
图3.3.8
i<8在hello.c作为判断循环条件,在汇编代码被编译为如下图所示,计算i-7然后设置条件码,为下一步jle利用条件码进行跳转做准备。

图3.3.9
3.函数操作
函数是一种过程,过程提供了一种封装代码的方式,用一组指定的参数和可选的返回值实现某种功能。
hello.c涉及的函数操作有:
main函数,printf,exit,sleep ,getchar函数
main函数的参数是argc和argv;
过程:
(1)pushq将%rbp压入栈中,将%rsp的值赋给%rbp。
(2)subq减小栈指针从而为局部变量分配空间,然后将%edi和%rsi压入栈中(即传入两个参数)。
(3)在main函数的最后,leave指令将%rsp和%rbp恢复,随机ret指令返回到调用者。
两次printf函数的参数为main函数的那俩个参数
exit参数是1
sleep函数参数是atoi(argv[3])
函数的返回值存储在%eax寄存器中。
3.4 本章小结
本章介绍了编译的概念与作用,通过对hello.s的解析,读懂汇编语句,了解汇编语句下:数据:常量、变量(全局/局部/静态)、表达式、类型、宏,赋值语句,类型转换,算数操作,逻辑/位操作,关系操作,控制转移以及函数操作的基本知识。
通过编译,我们已将原始.c文件生成为汇编语言程序,距离可执行目标文件又进一步。

(第3章2分)

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

汇编器把这些hello.s翻译成机器语言的指令,并且把这些机器语言指令编码打包成一个可重定位目标程序的指令编码格式,结果指令编码保存在了hello.o中。
hello.o文件是一个简单的二进制指令编码文件,它可以包含目标程序的所有指令进行编码。此外汇编器还可以通过翻译生成计算机能直接自动识别和控制指令执行的一种二进制语言,也即机器语言。
4.2 在Ubuntu下汇编的命令
命令:gcc hello.s -c -o hello.o
图4.2.1
4.3 可重定位目标elf格式
命令:readelf -h hello.o

图4.3.1
ELF头以一个16字节的序列开始,这个magic描述了生成该头文件的系统的字的大小和字节顺序。剩下的部分包含帮助连接器语法分析和解释目标文件的信息。其中包括ELF头的大小,目标文件的类型,机器类型,节头部表的文件偏移,以及节头部表中条目的大小和数量。
命令:
readelf -S hello.o

图4.3.2
图4.3.3
节头部表,包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。 由于是可重定位目标文件,所以每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,同时可以观察到,代码是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。
命令:readelf -s hello.o

图4.3.4
.symbol:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
命令:readelf -a hello.o > elf.txt

图4.3.5
重定位节:一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
重定位节.rela.text中各项符号的信息:
Offset:需要被修改的引用节的偏移Info:包括symbol和type两个部分,symbol在前面四个字节,type在后面四个字节,
symbol:标识被修改引用应该指向的符号,
type:重定位的类型
Type:告知链接器应该如何修改新的应用
Attend:一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整Name:重定向到的目标的名称。
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o

图4.4.1

图4.4.2和图4.4.3
机器语言是由二进制编码构成的语言,机器语言中的指令结构一般由操作码和地址码两部分组成。
通过观察,我们可以发现,反汇编生成的汇编代码,每一行前面都有一串16进制的数字。这些数字就是每一行汇编代码对应的机器代码。同时.s中采用10进制数,而反汇编中使用16进制数。同时,每一个汇编语句几乎都对应一条机器指令,根据汇编指令和机器指令的对照表一一翻译即可。
总结:
1、从代码上看汇编和反汇编的指令基本一样。
2、汇编操作的立即数是十进制,二反汇编操作的立即数是十六进制(即就是左边序列的某个值)。
3、汇编和反汇编操作(寄存器或存储器)的地址也不同。
分支转移
由原来的使用段名改为使用相对寻址,即下一条指令的地址加偏移量,从而得到绝对地址。
函数调用
在.s文件中,函数调用使用函数名直接调用,而在反汇编程序中,call调用下一条指令的地址。
4.5 本章小结
本章针对hello.s汇编到hello.o。在本章的分析中,我们查看了hello.o的可重定位目标文件的格式,使用反汇编查看hello.o经过反汇编过程生成的代码并且把它与hello.s进行比较,分析和阐述了从汇编语言进一步翻译成为机器语言的汇编过程。
(第4章1分)

第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段手机并组合成为1个单一文件的过程,这个文件可被加载到内存执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。
作用:不用将一个大型的应用程序组织为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.2.1
5.3 可执行目标文件hello的格式
命令:readelf -a hello > hello.txt

ELF头
命令:readelf -h hello

图5.3.1
类型从可重定位变为了可执行
节头部表
命令:readelf -S hello

图5.3.2

图5.3.3和图5.3.4
节头部表包含了应用程序的每个节,图中清楚的则列出了它们的详细信息详细信息.其中头部表包括:名称,类型,地址,偏移量,大小,全体大小,链接,信息,对齐。我们可以用hexedit定位各个全体字节所占的大小区间。
.symtab
命令:readelf -s hello

图5.3.5图 5.3.6 图5.3.7 图5.3.8
重定位节

图5.3.9
5.4 hello的虚拟地址空间
图5.4.1
hello进程自00400000开始:ELF头
图5.4.2
于00400ff0结束
与节头部表对照
举例:Interp:
图5.4.3
5.5 链接的重定位过程分析
命令:objdump -d -r hello

图5.5.1

图5.5.2

图5.5.3

图5.5.4

图5.5.5

图5.5.6

图5.5.7

图5.5.8

图5.5.9
hello.o中的相对偏移地址到了hello中变成了虚拟内存地址
hello中相对hello.o增加了许多的外部链接来的函数。
hello相对hello.o多了很多的节类似于.init,.plt等
hello.o中跳转以及函数调用的地址在hello中都被更换成了虚拟内存地址。
重定位过程:链接器在完成符号解析,把代码中的每个符号引用和正好一个符号定义关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小,开始重定位步骤。在这个步骤中,将合并输入模块,并为每个符号分配运行时的地址。从hello.o到hello,首先是重定位节和符号定义,链接器将所有输入到hello中相同类型的节合并为同一类型的新的节。然后,链接器将运行时内存地址赋给新的节,赋给输入模块定义的每个节,以及赋给输入模块定义的每一个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。然后是重定位节中的符号引用,链接器会修改hello中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址。
5.6 hello的执行流程

图5.6.1
ld-2.27.so!_dl_start—
ld-2.27.so!_dl_init—
hello!_start—0x400550
hello!_init—0x4004c0
hello!main—0x400582
hello!puts@plt–0x4004f0
hello!exit@plt–0x400530
hello!printf@plt–0x400500
hello!sleep@plt–0x400540
hello!getchar@plt–0x400510
sleep@plt—0x400540
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
程序调用一个由共享库定义的函数。编译器无法预测这个函数运行时的地址,因为定义它的共享模块在运行时可以加载到任意位置,故GNU使用延迟绑定,将过程地址的绑定推迟到第一次调用该过程。这样第一次调用的过程中运行时开销很大,但其后的每次调用只会花费一条指令和一个简洁的内存引用。
延迟绑定使用两个数据结构:GOT和PLT(过程链接表)。

5.7 -1延迟绑定
addvec第一次被调用,延迟解析它的运行地址。
(1) 不直接调用addvec,程序调用进入PLT[2],这是addvec的PLT条目。
(2) 第一条PLT指令通过GOT[4]进行间接跳转。因为每个GOT条目初始时都指向它对应的PLT条目的第二条指令,这个间接跳转只是简单地把控制传送回PLT[2]中的下一条指令。
(3) 在把addvec的ID(0x1)压入栈中后,PLT[2]跳转到PLT[0]。
(4) PLT[0]通过GOT[1]间接地把动态链接器的一个参数压入栈中,然后通过GOT[2]间接跳转进入动态链接器中。动态链接器使用两个栈条目来确定addvec的运行时位置,用这个地址重写GOT[4],再把控制传递给addvec。
后续再调用。
(1) 和前面一样,控制传递到PLT[2]。
(2) 通过GOT[4]的间接跳转会将控制直接转移到addvec。
图5.7.1
Got起始地址为403ff0

图5.7.2
GOT表位置在调用dl_init之前0x404000后的16个字节均为0
调用后:

图5.7.3
5.8 本章小结

本章主要介绍了链接的概念与作用:是将各种代码和数据片段收集并组合成为一个单一文件的过程,分析了UBUNTU下链接的命令,通过对hello的反汇编了解了链接的中间过程,所做的事及函数的调用。了解了链接前后地址的变化以及调用函数的变化。(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
概念:进程就是一个执行中程序的实例。系统中的每个应用程序都可以运行在某个应用进程的可执行上下文中。每次程序用户可以通过向系统中的shell应用程序输入一个可执行程序的英文名字,运行这个应用程序时,shell就可能会自动创建一个新的应用进程,然后在这个新应用进程的可执行上下文中自动运行这个可执行文件,应用程序也同样可以自动创建新的可执行进程,并且在这个新进程的上下文中运行他们自己的代码或者其他的应用程序。
作用:在现代系统上运行一个程序时,我们会得到一个假象。就好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条地执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象都是通过进程的概念提供给我们的。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash的作用:shell和其他软件一样都是和内核打交道,直接服务于用户。但和其他软件不同,shell主要用来管理文件和运行程序。作为命令语言,它交互式解释和执行用户输入的命令或者自动地解释和执行预先设定好的一连串的命令。
处理流程:shell对命令行的处理流程
(1)读取输入的命令行。
(2)解析引用并分割命令行为各个单词
(3)检查命令行结构。
(4)搜索和执行命令。
(5)返回退出状态码。
6.3 Hello的fork进程创建过程
父进程通过调用fork函数来创建一个新运行的子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的一份副本,包括代码和数据段,堆,共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。它们之间的区别是拥有不同的PID。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。它会覆盖在当前进程的地址空间,但并没有创建一个新进程。新的程序仍然拥有相同的PID,并且继承调用execve函数时已打开的所有文件描述符。
execve函数加载并运行可执行文件hello,且带参数列表argv和环境变量列表envp。只有在出现错误时,例如找不到hello,execve才会返回到调用程序。所以,与fork 1次调用返回两次不同,execve调用一次并从不返回。
6.5 Hello的进程执行
因hello这个程序是在操作系统中与其他进程并行的执行,操作系统内核使用一种成为上下文切换的较高层形式的异常控制流来实现多任务。内核为每个进程维持一个上下文(内核重新启动一个被抢占的进程所需的状态,由一些对象的值组成,如:通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构)。
hello进程执行它的控制流的一部分的每一时间段叫做时间片。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这种决策就叫调度(由内核中调度器的代码处理)。
当内核选择新进程hello运行时,即内核调度了hello进程,此时hello抢占当前进程,并使用一种称为上下文切换的机制将控制转移到hello进程,此时的上下文切换:
(1)保存当前进程的上下文
(2)恢复某个先前被抢占的进程被保存的上下文
(3)将控制传递给新恢复的进程。
当内核代表用户执行系统调用时,可能会发生上下文切换。如hello程序中的sleep系统调用,显式地请求让调用进程休眠。
中断也可能引发上下文切换。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
Hello执行过程出现的异常为:中断、故障
会产生的信号为:SIGSTP 来自终端的停止信号,SIGINT 来自键盘的中断
正常结束:

图6.6.1
按ctrl-z:

图6.6.2
默认其为挂起前台的作业,hello进程并未被回收,而是挂到后台

图6.6.3
用ps命令可以看到,此时它的job为1

图6.6.4
我们用fg 1使其继续运行,进程回收

Ctrl-c:

图6.6.5
这会导致内核发送一个SIGINT信号到前台进程组的每一个进程,默认情况是终止进程,我们用ps查看,发现没有hello进程
乱按:

图6.6.6
每次循环getchar()时读入,不会影响程序进程
6.7本章小结
本章主要介绍了进程的概念与作用,了解了fork ,execve函数的功能,并且通过对hello程序的执行了解了hello的异常与信号处理。(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址,是由一个段选择符加上一个指定段内相对地址的偏移量(Offset)组成的,表示为 [段选择符:段内偏移量]。hello.c经过汇编生成的偏移地址为逻辑地址。
线性地址,跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。
虚拟地址(Virtual Address):CPU通过生成虚拟地址访问主存。hello中的虚拟内存地址。
物理地址(Physical Address):是指目前CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址即为物理地址。hello在执行过程中通过地址总线寻找到的真实地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
每个段的首地址就会被储存在各自的段描述符里面,所以的段描述符都将会位于段全局描述符表中,通过段选择符我们可以快速寻找到某个段的段全局描述符。逻辑上段地址的偏移量结构就是段选择符+偏移量。
段选择符的索引位组成和定义如下,分别指的是索引位(index),ti,rpl,当索引位ti=0时,段描述符表在rpgdt中,ti=1时,段描述符表在rpldt中。而索引位index就类似一个数组,每个元素内都存放一个段的描述符,索引位首地址就是我们在查找段描述符时再这个元素数组当中的索引。
一个段描述符的首地址是指含有8个元素的字节,我们通常可以在查找到段描述符之后获取段的首地址,再把它与线性逻辑地址的偏移量进行相加就可以得到段所需要的一个线性逻辑地址。
在分段保护模式下,分段有两种机制:段的选择符在段的描述符表->分段索引->目标段的段描述符条目->目标段的描述符基地址+偏移量=转换为线性段的基地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每字节都有唯一的虚拟地址,作为到数组的索引。磁盘上数组的内容被缓存在主存中。和存储器层次结构中的其他缓存一样,磁盘(较低层)上的数据被分割成块,这些块作为磁盘和主存(较高层)之间的传输单元。VM系统通过将虚拟内存分割为称为虚拟页(VP)的大小固定的块来处理这个问题。每个虚拟页的大小为P=2p字节。类似地,物理内存被分割为物理页,大小也为P字节。虚拟页面的集合分为:未分配的、缓存的、未缓存的。
同任何缓存一样,虚拟内存系统必须有某种方法判定一个虚拟页是否缓存在DRAM中的某个地方。如果是,系统还必须确定这个虚拟页存放在哪个物理页中。如果不命中,系统必须判断这个虚拟页存放在磁盘的哪个位置,在物理内存中选择一个牺牲页,并将虚拟页从磁盘复制到DRAM中,替换此牺牲页。
这些功能由软硬件联合提供,包括操作系统软件、MMU(内存管理单元)中的地址翻译硬件和一个存放在物理内存中叫做页表的数据结构,页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理页时,都会读取页表。操作系统负责维护页表的内容,以及在磁盘和DRAM之间传送页。

7.4 TLB与四级页表支持下的VA到PA的变换
若照上述模式,每次CPU产生一个虚拟地址并且发送给地址管理单元,MMU就必须查找一个PTE行来用将虚拟地址翻译成物理地址。为了消除这种操作带来的大量时间开销,MMU中被设计了一个关于PTE的小的缓存,称为翻译后备缓冲器(TLB),也叫快表。
TLB利用VPN进行寻址,分为组索引(TLBI)和标记(TLBT),用来区别可能映射到同一个TLB组的不同的VPN。
corei7采用四级页表层次结构,每个四级页表进程都有他自己私有的页表层次结构,这种设计方法从两个基本方面就是减少了对内存的需求,如果一级页表的pte全部为空,那么二级页表就不会继续存在,从而为进程节省了大量的内存,而且也只有一级页表才会有需要总是在一个内存中。
36位虚拟地址被寄存器划分出来组成四个9位的片,每个片被寄存器用作到一个页表的偏移量。cr3寄存器内储存了一个l1页表的一个物理起始基地址,指向第一级页表的一个起始和最终位置,这个地址是页表上下文的一部分信息。vpn1提供了到一个l1pet的偏移量,这个pte寄存器包含一个l2页表的起始基地址.vpn2提供了到一个l2pte的偏移量,一共四级,逐级以此层次类推。
7.5 三级Cache支持下的物理内存访问
(1)组选择取出虚拟地址的组索引位进行组索引
(2) 行匹配把虚拟地址的标记位与相应的组中所有行的标记位进行比较,当虚拟地址的标记位和高速缓存行的标记位匹配时,而且高速缓存行的有效位是1,则高速缓存命中。
(3) 字选择一旦高速缓存命中,把这个字节的内容取出返回给CPU。
(4)不命中:如果是高速缓存不命中,那么需要从存储层次结构中的下一层取出被请求的块,然后将新的块存储在组索引位所指示的组中的一个高速缓存行中。一种简单的 放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块, 产生冲突(evict),则采用最近最少使用策略 LFU 进行替换。
7.6 hello进程fork时的内存映射
当fork 函数被 shell 进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的 PID,为了给这个新进程创建虚拟内存,它创建了当前进程mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前代码共享进程的上下文中加载并自动运行一个新的代码共享程序,它可能会自动覆盖当前进程的所有虚拟地址和空间,删除当前进程虚拟地址的所有用户虚拟和部分空间中的已存在的代码共享区域和结构,但是它并没有自动创建一个新的代码共享进程,另外,新的运行程序仍然在堆栈中拥有相同的区域pid。
hello这个程序与当前共享的对象libc.so链接,它可能是首先动态通过链接映射到这个代码共享程序上下文中的,然后再通过映射链接到用户虚拟地址和部分空间区域中的另一个共享代码区域内。为了设置一个新的程序计数器,execve函数要做的最后一件要做的事情就是自动设置当前代码共享进程上下文的一个程序计数器,使之成为指向所有代码共享区域的一个入口点(即_start函数)。
7.8 缺页故障与缺页中断处理
缺页故障:DRAM缓存不命即为缺页。当指令引用一个虚拟地址,而与该地址相对应的物理页面不在内存中,因此必须从磁盘中取出时,就会发生缺页故障。
缺页中断处理:一个页面就是虚拟内存的一个连续的块。缺页异常调用内核中的缺页异常处理程序,缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中了,指令就可以没有故障地运行完成了。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk, 它指向堆的顶部。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用,空闲块可用来分配,空闲块保持空闲,直到它显式地被应用所分配。
一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格. 两种风格都要求应用显式地分配块.它们的不同之处在于由哪个实体来负责释放已分配的块
显式分配器(explicit allocator):
要求应用显式地释放任何已分配的块.例如,C标准库提供一种叫做malloc程序包的显式分配器.C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块.C++中的new和delete操作符与C中的malloc和free相当.
隐式分配器(implicit allocator):
要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块.隐式分配器也叫做垃圾收集器(garbage collector),而自动释放未使用的已分配的块的过程叫做垃圾收集( garbage collection).例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
一个块是由一个字的头部,有效载荷,以及可能的一些额外的填充组成的.头部编码了这个块的大小,以及这个块是已分配的还是空闲的.如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是零.因此,我们只需要内存大小的29个高位,释放剩余的3位来编码其他信息.在这种情况中,我们用其中的最低位(已分配位)来指明这个块是已分配的还是空闲的。
显示分配器:
因为根据定义,程序不需要一个空闲块主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。
使用双向链表,使首次适配发分配时间从块总数的线性时间减少到空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,取决于我们所选择的空闲链表中块的排序策略。
7.10本章小结
本章主要介绍了hello的存储地址空间,了解了从逻辑地址到线性地址的变换以及从线性地址到物理地址的变换。以及四级页表下的VA到PA的变换,了解了三级cache下的物理内存访问。能够解决缺页故障以及缺页中断的处理,了解了c语言中的malloc的具体执行规则。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个linux文件就是一个m个字节的序列:B0,B1,…,Bk,…,Bm-1
所有的I/O设备(例如网络、磁盘和终端)都被模型化成文件,而所有的输入输出都被当作对相应文件的读和写来执行。这种将设备映射为文件的方式,允许linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix I/O 接口:
(1) 打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想 要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
(2)shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
(3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置 k。
(4)读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的文件,当k>=m时,触发EOF。一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
(5)关闭文件:内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

Unix I/O 函数:
(1)int open(char* filename,int flags,mode_t mode) ,进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符是在当前进程中没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
(2)int close(fd),fd是需要关闭的文件的描述符,close返回操作结果。
(3) ssize_t read(int fd,void *buf,size_t n),read 函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
4) ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多 n个字节到描述符为fd的当前文件位置。
8.3 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;
}
首先,arg得到…中第一个参数的起始地址,然后调用vsprintf生成显示信息。
vsprintf返回的是要打印的字符串长度,即为write中的i。
接下来转到write
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
这里是给几个寄存器传递了几个参数,然后一个int结束,通过系统来调用sys_call这个函数。
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx //ecx中是要打印出的元素个数
push ebx //ebx中的是要打印的buf字符数组中的第一个元素
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分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成 ASCII 码,保存到系统的键盘缓冲区之中。
getchar 函数落实到底层调用了系统函数 read,通过系统调用read读取存储在键盘缓冲区中的ASCII码直到读到回车符然后返回整个字串,getchar进行封装, 读取字符串的第一个字符然后返回。
8.5本章小结
本章主要介绍了 Linux 的 IO 设备管理方法及其接口、Unix的IO、printf函数和 getchar函数的实现。
(第8章1分)
结论
(1)编写hello程序:通过编辑器将hello的代码输入到计算机中。
(2)预处理:通过命令将hello.c调用的所有外部的库展开合并到一个hello.i文件中
(3)编译:通过命令将hello.i编译成为汇编文件hello.s
(4)汇编:通过命令将hello.s会变成为可重定位目标文件hello.o
(5)链接:通过一系列的命令将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello
(6)运行:在shell中输入./hello 1190200311 田翔宇 1
(7)创建子进程:shell进程调用fork为其创建子进程
(8)加载程序:shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数。
(9)程序的运行过程:如果运行途中键入ctr-c,ctr-z则调用shell的信号处理函数分别停止、挂起。
(10)结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。
感悟:学习完这门课,我觉得一个系统包含的东西实在是太多了,一个在之前不屑一顾的功能,将其实现需要异常多的付出与努力,我深刻理解了自己的不足,还有许多需要学习的地方。
(结论0分,缺失 -1分,根据内容酌情加分)

附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.i hello.c预处理器后的程序
hello.s 编译器生成的编译程序
hello.o 可重定位目标程序
hello 可执行目标程序
elf.txt hello.o的elf格式
hello.txt 可执行hello的elf格式
(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] printf 函数实现的深入剖析 https://www.cnblogs.com/pianist/p/3315801.html
[2] OS之内存管理 —基本的内存管理策略
https://www.cnblogs.com/lishanlei/p/10707677.html
(参考文献0分,缺失 -1分)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值