ICS大作业论文

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机类
学   号 1190201923
班   级 1903008
学 生 郭科信    
指 导 教 师 吴锐

计算机科学与技术学院
2021年5月
摘 要
本文从hello的诞生开始,通过hello的生成和消失过程来理解《深入理解计算机系统》这本书,以及hello.c在linux系统下的声明周期

关键词:CSAPP;预处理;编译;汇编;链接;进程;OS存储管理;IO管理

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

目 录

第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 4 -
第2章 预处理 - 5 -
2.1 预处理的概念与作用 - 5 -
2.2在UBUNTU下预处理的命令 - 5 -
2.3 HELLO的预处理结果解析 - 5 -
2.4 本章小结 - 5 -
第3章 编译 - 6 -
3.1 编译的概念与作用 - 6 -
3.2 在UBUNTU下编译的命令 - 6 -
3.3 HELLO的编译结果解析 - 6 -
3.4 本章小结 - 6 -
第4章 汇编 - 7 -
4.1 汇编的概念与作用 - 7 -
4.2 在UBUNTU下汇编的命令 - 7 -
4.3 可重定位目标ELF格式 - 7 -
4.4 HELLO.O的结果解析 - 7 -
4.5 本章小结 - 7 -
第5章 链接 - 8 -
5.1 链接的概念与作用 - 8 -
5.2 在UBUNTU下链接的命令 - 8 -
5.3 可执行目标文件HELLO的格式 - 8 -
5.4 HELLO的虚拟地址空间 - 8 -
5.5 链接的重定位过程分析 - 8 -
5.6 HELLO的执行流程 - 8 -
5.7 HELLO的动态链接分析 - 8 -
5.8 本章小结 - 9 -
第6章 HELLO进程管理 - 10 -
6.1 进程的概念与作用 - 10 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 10 -
6.3 HELLO的FORK进程创建过程 - 10 -
6.4 HELLO的EXECVE过程 - 10 -
6.5 HELLO的进程执行 - 10 -
6.6 HELLO的异常与信号处理 - 10 -
6.7本章小结 - 10 -
第7章 HELLO的存储管理 - 11 -
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 -
7.9动态存储分配管理 - 11 -
7.10本章小结 - 12 -
第8章 HELLO的IO管理 - 13 -
8.1 LINUX的IO设备管理方法 - 13 -
8.2 简述UNIX IO接口及其函数 - 13 -
8.3 PRINTF的实现分析 - 13 -
8.4 GETCHAR的实现分析 - 13 -
8.5本章小结 - 13 -
结论 - 14 -
附件 - 15 -
参考文献 - 16 -

第1章 概述
1.1 Hello简介
程序猿通过文本编辑器将代码写入hello.c 当中,通过预处理器cpp进行预处理,之后使用ccl编译器将文本文件hello.i翻译成文本文件hello.s(汇编程序),接下来,汇编器(as)将hello.s 翻译成机器语言指令,把这些指令打包成可重定向的目标文件,然后链接器ld将每个.o文件进行链接。
将文件名输入到shell当中,shell程序会将字符逐一读入寄存器,再把它存放到内存当中,当敲入回车之后,shell执行一系列指令来加载可执行的hello文件,这些指令将hello目标文件中的代码和数据从磁盘复制到主存,shell为其创建一个进程,这就是P2P 的过程。
然后shell为其execve映射虚拟内存。进入程序入口后,程序开始加载物理内存,然后进入main函数执行目标代码。 CPU为正在运行的hello分配时间片以执行逻辑控制流。程序完成后,shell父进程负责恢复hello进程,内核删除相关的数据结构。以上就是020的过程。
1.2 环境与工具
硬件环境:

			(图1.2.1    电脑硬件配置)

软件环境:VMware
Ubuntu64位
objdump,gdb,hexedit,gcc
1.3 中间结果
名称 作用
hello.c 源代码文件
hello.i cpp预处理产生的文件
hello.s ccl编译产生的汇编代码
hello.o as将hello.s翻译成的机器语言指令
hello 可执行文件
4.4asm.txt 4.4readelf hello.o产生的反汇编代码
5.5asm.txt 重定位文件的反汇编代码
1.4 本章小结
简要介绍了hello的P2P,020的整个过程以及实验的环境、工具和中间产物
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
预处理其(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c程序中第一行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h 的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以.i作为文件拓展名
C语言的预处理主要有三个方面的内容: 1.宏定义; 2.文件包含; 3.条件编译。 预处理命令以符号“#”开头。
2.2在Ubuntu下预处理的命令
Linux> cpp hello.c > hello.i生成预处理文件
应截图,展示预处理过程!

(图2.2.1 cpp产生的hello.i文件中调用stdio.h的代码)

(图2.2.2 cpp产生的hello.i文件中调用unistd.h的代码)

(图2.2.3 cpp产生的hello.i文件中调用stdlib.h的代码)

可以从这三张截图中看出,预处理hello.c 的过程中在系统目录中找到了所对应的三个调用库,并将在接下来的编译阶段使用。
2.3 Hello的预处理结果解析
hello.c含有三条预处理指令
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
作用是文件包含,即包含stdio.h unistd.h stdlib.h 三个文件,可以从截图中找到这三个文件的根目录,
在hello.i 文件中我们还可以看到,某些常量也有了它的缺省定义如char,hello.i文件当中还含有库中的代码段,即被解析出的代码段。
2.4 本章小结

通过预处理指令处理的hello.c 文件生成了hello.i文件,并将一些代码加载到了hello.i文件当中

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
编译器将文本文件hello.i翻译成文本文件hello.s ,它包含一个汇编语言程序。该程序包含了函数main的定义。
汇编语言是非常有用的,因为它位不同高级语言的不同编译器提供了通用的输出语言。
3.2 在Ubuntu下编译的命令
gcc -S hello.i > hello.s

(图3.2.1 hello.s汇编代码1)

(图3.2.2 hello.s汇编代码2)
3.3 Hello的编译结果解析
1.全局变量的定义
printf 的格式段的定义
.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”(该string对应的是用法: Hello 学号 姓名 秒数!\n)
.LC1:
.string “Hello %s %s\n”

2.main函数
.text 在text节中定义
.globl main 声明全局符号global
.type main, @function 将main声明为函数
随后定义main:标签,后跟main函数伪汇编指令

3.int i
i变量的定义,它存放在-4(%rbp)的位置,以便等会进入循环时使用

4.if(argc!=4)
这是一条判断语句,所对应的汇编代码为
cmpl $4, -20(%rbp)
je .L2
可以看出如果argc存放在-20(%rbp)当中,并且与立即数4进行比较,如果与4相等则跳到代码段.L2处,否则继续运行该代码段的接下来的部分

  1. printf(“用法: Hello 学号 姓名 秒数!\n”);
    这是调用了printf函数进行输出,对应的汇编代码为:
    leaq .LC0(%rip), %rdi
    call puts@PLT
    意思是:将.LC0段中的内容存放在rdi当中,并且调用puts@PLT函数,来格式化输出该字符串,最后在将edi的值赋成1

    6.exit(1)
    movl $1, %edi
    call exit@PLT
    参数为1,执行exit函数

7.for(i=0;i<8;i++)
接4,如果argc==4,则进行L2代码段的执行,其中就包括了i的初始化
所对应的汇编语句为:
movl $0, -4(%rbp)
jmp .L3
进入.L3之后,可以发现进行了i与7的比较,即:
cmpl $7, -4(%rbp)
jle .L4
如果i比7小,则进入循环体内,即代码段L4当中,否则退出循环

在代码段L4当中,我们可以发现一条语句:
addl    $1, -4(%rbp)
该汇编代码进行了i的自增1
  1. printf(“Hello %s %s\n”,argv[1],argv[2]);
    .L4当中:

movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
将argv[2]的地址赋值给rdx

2.
movq	-32(%rbp), %rax
addq	$8, %rax
movq	(%rax), %rax
将argv[1]的地址赋值给rax

3.

movq %rax, %rsi
leaq .LC1(%rip), %rdi
movl $0, %eax
call printf@PLT
将rax 中的值赋值给rsi,便于之后的函数调用,并将.LC1中的值加载到rdi中,给eax赋值,调用函数printf
9. sleep(atoi(argv[3]));
1.
movq -32(%rbp), %rax
addq $24, %rax
movq (%rax), %rax
将argv[3]的值赋值到rax 当中
2.
movq %rax, %rdi
call atoi@PLT
将rax 的值存入rdi当中并调用函数atoi
3.
movl %eax, %edi
call sleep@PLT
将atoi的返回值eax存入edi当中,并调用函数slepp@PLT

10. getchar();
	  结束循环的时候调用函数getchar
		call	getchar@PLT
11. 	return 0;
movl	$0, %eax
leave
.cfi_def_cfa 7, 8
		ret
		将0赋值给eax并退出main函数

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

3.4 本章小结
本节涉及到的指令全部为gun汇编程序(gas)的伪汇编指令,相比最后的汇编指令内容更为精简,方便阅读、分析。程序将常量放入.rodata节,初始化全局变量放入.data节,通过标签定义和跳转等方式定义许多操作,为后序的汇编和链接生成可执行文件准备。

(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
概念:
把汇编语言翻译成机器语言的过程称为汇编
作用:
将汇编语言翻译成机器语言,便于机器执行。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s
应截图,展示汇编过程!

		(图4.2.1    在linux下使用汇编命令生成hello.o文件)

4.3 可重定位目标elf格式
使用指令readelf -a hello.o 来显示可重定位文件的所有信息
1.ELF 头,包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。

(图4.3.1 ELF头)

2.节头部表,包含了文件中出现的各个节的语义,包括节的名称,类型、位置和大小等信息。

(图4.3.2 节头部表(部分))

3.重定位节,包含了.text 节中需要重定位的信息,当这个目标文件和其他文件组合时,需要修改这些位置。

(图4.3.3 重定位节)
重定位信息分别是对.L0(第一个printf中的字符串),puts函数,exit函数,.L1(第二个printf中的字符串),printf函数,sleep函数,getchar函数进行重定位声明。

4.4 Hello.o的结果解析
通过 objdump -d -r hello.o 生成的反汇编代码如下
hello.o: 文件格式 elf64-x86-64
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 83 ec 20 sub $0x20,%rsp
c: 89 7d ec mov %edi,-0x14(%rbp)
f: 48 89 75 e0 mov %rsi,-0x20(%rbp)
13: 83 7d ec 04 cmpl $0x4,-0x14(%rbp)
17: 74 16 je 2f <main+0x2f>
19: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 20 <main+0x20>
1c: R_X86_64_PC32 .rodata-0x4
20: e8 00 00 00 00 callq 25 <main+0x25>
21: R_X86_64_PLT32 puts-0x4
25: bf 01 00 00 00 mov $0x1,%edi
2a: e8 00 00 00 00 callq 2f <main+0x2f>
2b: R_X86_64_PLT32 exit-0x4
2f: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
36: eb 48 jmp 80 <main+0x80>
38: 48 8b 45 e0 mov -0x20(%rbp),%rax
3c: 48 83 c0 10 add $0x10,%rax
40: 48 8b 10 mov (%rax),%rdx
43: 48 8b 45 e0 mov -0x20(%rbp),%rax
47: 48 83 c0 08 add $0x8,%rax
4b: 48 8b 00 mov (%rax),%rax
4e: 48 89 c6 mov %rax,%rsi
51: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 58 <main+0x58>
54: R_X86_64_PC32 .rodata+0x22
58: b8 00 00 00 00 mov $0x0,%eax
5d: e8 00 00 00 00 callq 62 <main+0x62>
5e: R_X86_64_PLT32 printf-0x4
62: 48 8b 45 e0 mov -0x20(%rbp),%rax
66: 48 83 c0 18 add $0x18,%rax
6a: 48 8b 00 mov (%rax),%rax
6d: 48 89 c7 mov %rax,%rdi
70: e8 00 00 00 00 callq 75 <main+0x75>
71: R_X86_64_PLT32 atoi-0x4
75: 89 c7 mov %eax,%edi
77: e8 00 00 00 00 callq 7c <main+0x7c>
78: R_X86_64_PLT32 sleep-0x4
7c: 83 45 fc 01 addl $0x1,-0x4(%rbp)
80: 83 7d fc 07 cmpl $0x7,-0x4(%rbp)
84: 7e b2 jle 38 <main+0x38>
86: e8 00 00 00 00 callq 8b <main+0x8b>
87: R_X86_64_PLT32 getchar-0x4
8b: b8 00 00 00 00 mov $0x0,%eax
90: c9 leaveq
91: c3 retq

机器语言是直接用二进制代码指令表达的计算机语言,指令是用0和1组成的一串代码,它们有一定的位数,并分成若干段,各段的编码表示不同的含义。
每一条汇编语句被映射为若干二进制指令码,将机器语言的每一条指令符号化:指令码代之以记忆符号,地址码代之以符号地址。
在汇编语言中,操作数用十进制表示,而在机器语言中,用十六进制表示,如hello.o中:
机器语言中命令
8: 48 83 ec 20 sub $0x20,%rsp
在汇编语言hello.s中对应为
subq $32, %rsp
在汇编语言中,分支跳转、函数调用,使用标签定位位置,而在机器语言中使用地址+偏移量计算要跳转到的实际地址。

如hello.o中:
84: 7e b2 jle 38 <main+0x38>
86: e8 00 00 00 00 callq 8b <main+0x8b>
在hello.s中对应为:
jle .L4
call getchar
4.5 本章小结
汇编语言在高级语言和机器语言的转换当中起着至关重要的作用。
(第4章1分)

第5章 链接
5.1 链接的概念与作用
链接的概念:链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存中并执行,也就是从.o文件到可执行文件的过程。链接是由链接器的程序自动执行的。链接包含符号解析和重定位两步。链接器将每个符号引用与符号的定义相关联,将符号在可重定位文件的位置重定位至可执行文件的位置。
链接的作用:可以将一个大型的应用程序分解成更小、更好管理的模块,可以独立地修改和编译这些模块,当我们改变这些模块中的一个时,只需要简单地重新编译,并重新链接应用,而不必重新编译其他文件。
注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在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 hello.o -lc /usr/lib/x86_64-linux-gnu/crtn.o -o hello

(图5.2.1 使用链接命令)
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

(图5.3.1 节头部表)
通过readelf -a hello得到了hello的ELF 格式,hello共由26个段组成,从.interp到.shstrtab。在地址这一栏中我们可以看到各段的起始地址,在大小一栏中即可得到各段的大小。

重定位节.rela.text:

(图5.3.2 重定位节)

符号表.symtab

(图5.3.3 符号表)
5.4 hello的虚拟地址空间
代码开始段为0x400000

(图5.4.1 使用edb查看hello)
查看可知为ELF头文件开始的地方,
通过查看edb,看出hello的虚拟地址空间开始于0x400000,结束与0x400ff0

(图5.4.2 使用edb找到hello 的text段的开始位置)
此处为.text段开始的地方。0x4010f0
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
通过分析hello与hello.o的不同,说明链接的过程。可以发现以下不同的地方:
(1)hello反汇编的代码有确定的虚拟地址,也就是说已经完成了重定位,而hello.o反汇编代码中代码的虚拟地址均为0,未完成可重定位的过程。如下图的对比

图5.5.1 hello的汇编代码

5.5.2hello.o的汇编代码

hello重定位的过程:
(1)重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。

(2)重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。

(3)重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目。代码的重定位条目放在.rel.txt。
5.6 hello的执行流程
(以下格式自行编排,编辑时删除)
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
_dl_start
_dl_init
_start
_libc_start_main
_init
_main
_printf
_exit
_sleep
_getchar
_dl_runtime_resolve_xsave
_dl_fixup
_dl_lookup_symbol_x
exit

5.7 Hello的动态链接分析
在执行_dl_start 之前寄存器的值

	(图5.7.1 )

在执行_dl_start之后寄存器的值为:

(图5.7.2)

5.8 本章小结
链接的过程,是将原来的只保存了你写的函数的代码与代码用所用的库函数合并的一个过程。在这个过程中链接器会为每个符号、函数等信息重新分配虚拟内存地址,方法就是用每个.o文件中的重定位节与其它的节想配合,算出正确的地址。同时,将你会用到的库函数加载(复制)到可执行文件中。这些信息一同构成了一个完整的计算机可以运行的文件。链接让我们的程序做到了很好的模块化,我们只需要写我们的主要代码,对于读入、IO等操作,可以直接与封装的模块相链接,这样大大的简化了代码的书写难度。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
概念:进程的经典定义就是一个执行中程序的实例
作用:
每次用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。
进程提供给应用程序两个关键抽象:
1)提供一个假象,好像我们的程序独占地使用处理器
2)一个私有的地址空间,提供一个假象,好像我们的程序独占地使用内存系统

6.2 简述壳Shell-bash的作用与处理流程
shell是一个交互型的应用级程序,它代表用户运行其他程序。
shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。
求值步骤:
1.调用parseline函数解析以空格分隔的命令行参数,构造argv向量传递给execve;
2.调用builtin_command函数,检查第一个命令行参数是否是一个内置的shell命令,如果是,它就立即解释这个命令,并返回值1;
3.如果builtin_command函数返回0,那么shell创建一个子进程,并在子进程中执行所请求的程序。如果用户要求在后台运行该程序,那么shell返回到循环的顶部,等待下一个命令行。否则,shell使用waitpid函数等待作业终止。当作业终止时,shell就开始下一轮迭代。
(以下格式自行编排,编辑时删除)
6.3 Hello的fork进程创建过程
在bash中输入 ./hello 1190201923 郭科信 1 并敲击回车后,bash解析此条命令,发现./hello不是bash内置命令,于是在当前目录尝试寻找并执行hello文件。此时bash使用fork函数创建一个子进程(这个子进程得到与父进程用户级虚拟地址空间相同但是独立的一份副本),并更改这个子进程的进程组编号。并准备在这个子进程执行execve。(以下格式自行编排,编辑时删除)
6.4 Hello的execve过程
在新创建的子进程中,execve函数加载并运行hello,且带参数列表argv和环境变量envp。在execve加载了hello之后,它调用_start,_start设置栈,并将控制传递给新程序的主函数。
6.5 Hello的进程执行
在输入合适参数执行hello程序之后,hello进程一开始运行在用户模式。内核为hello维持一个上下文,它由一系列的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构(比如页表、进程表、文件表)。

		(图6.5.1 逻辑控制流的图示)

在hello运行时,也有一些其它进程在并行地运行,这些进程的逻辑流的执行时间与hello的逻辑流重叠,称为并发流。而一个进程和其它进程轮流运行的概念叫作多任务,一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。
不久hello调用printf与sleep,这两个函数引发系统调用,系统调用使得进程从用户模式变为内核模式,处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式,而执行sleep系统调用时,内核可能会执行上下文切换而非将控制返回给hello进程。在切换的第一部分中,内核代表hello在内核模式下执行指令,然后在某一时刻,它开始代表另一个进程在内核模式下执行指令,在切换之后,内核代表那个进程在用户模式下执行指令。
而这个切换过程可以分为三个步骤

保存当前进程的上下文
恢复某个先前被抢占的进程被保存的上下文
将控制传递给这个新恢复的进程。

这时我们说内核调度了一个新的进程,在内核调度了一个新的进程后,它就抢占了当前进程。
不仅仅是系统调用会导致上下文切换,中断也会。当hello执行了一段时间(通常是1-10ms)后,定时器引发的中断也会导致内核执行上下文切换并调度一个新的进程。
接下来的8秒中,内核继续执行上下文切换,轮流运行hello与其它进程,十次循环结束后,hello返回,程序终止。

6.6 hello的异常与信号处理

6.6.1 SIGINT 中断信号
当用户键入ctrl+c时会产生这个信号,接受这个信号,程序默认终止,如果有已经定义的handler,则会执行handler。

图6.6.1 在hello运行过程中输入ctrl-c

6.6.2 SIGTSTP 停止信号
当用户键入ctrl+z时会产生这个信号,接受这个信号的默认行为是中止程序,这个默认行为不可更改。

图6.6.2 在hello运行过程中输入ctrl-z

6.6.3 SIGKILL 终止信号
使用kill -9向hello发出这个信号,接受这个信号的默认行为是终止程序,这个默认行为不可更改。

图6.6.3 在hello停止后使用kill命令杀死进程并使用jobs命令查看

6.6.4 其他命令的使用
6.6.4

图6.6.4.1 在停止hello后用ps查看

图6.6.4.2 使用pstree查看未终止的hello
可以看到将进程停止以后,使用pstree命令能在进程树当中看到hello,而终止程序以后,就看不到了(如图)

图6.6.4.3 使用pstree查看已终止的hello

6.7本章小结
本章中阐述了进程的概念以及他在计算机中具体是如何在使用的。其次,还介绍了如何利用shell这个平台来对进程进行监理调用或发送信号等一系列操作。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
①物理地址是用于内存芯片的单元寻址,与处理器和CPU连接的地址总线相对应。即地址总线能够切实寻址的地址,与物理内存一一对应,对应到hello程序来说就是hello在真正被执行时机器访问用地址总线访问的hello的地址
②逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址可以表示为[段选择符:偏移地址]。对应到hello程序来说,你可以说某个变量的地址是xxx,这就是它的逻辑地址,是它相对于hello进程的数据段的偏移地址,与物理地址相干。但在Intel实模式下逻辑地址CS:EA =>物理地址CS*16+EA有这样的转换关系。
③虚拟地址指有程序产生的由段选择符和段内偏移地址组成的地址,是一种抽象,我们看到的hello的地址如0x400582等都是虚拟地址,每个进程都会有这样一个独立的虚拟地址空间,访问时先给出逻辑地址,需要再转换为虚拟地址,再通过虚拟地址到物理地址的映射转换为物理地址实现寻址。
④线性地址指虚拟地址到物理地址变换的中间层,是处理器可寻址的内存空间中的地址。程序代码会产生逻辑地址,也就是段中的偏移地址,加上相应的段基址就成了线性地址。可表示为[段描述符:段偏移]形式。如果开启了分页机制,那么线性地址需要再经过变换,转为为物理地址。如果无分页机制,那么线性地址就是物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。索引号,可以理解为数组的下标——而它将会对应一个数组,它又是什么的索引呢?这就是“段描述符(segment descriptor)”,段描述符具体地址描述了一个段(对于“段”这个字眼的理解:我们可以理解为把虚拟内存分为一个一个的段。比如一个存储器有1024个字节,可以把它分成4段,每段有256个字节)。这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
7.3 Hello的线性地址到物理地址的变换-页式管理
内存管理单元MMU中的地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。操作系统负责维护页表的内容,以及在磁盘与DRAM之间来回传送页。
页表是一个页表条目PTE的数组。虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个PTE。每个PTE有一个有效位和一个n位地址字段组成。有效位表明了该虚拟页当前是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置,这个物理页中缓存了该虚拟页。如果没有设置有效位,那么一个空地址表示这个虚拟页还未被分配。否则,这个地址就指向该虚拟页在磁盘上的起始位置。
7.4 TLB与四级页表支持下的VA到PA的变换
虚拟地址被划分成4个VPN和1个VPO。每个VPNi都是一个到第i级页表的索引,其中1≤i≤k。第j级页表中的每个PTE,1≤j≤k-1,都指向第j+1级的某个页表的基址。第k级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问k个PTE。和只有一级的页表结构一样,PPO和VPO是相同的,

图7.4.1 地址翻译概况

7.5 三级Cache支持下的物理内存访问
当我们通过MMU得到了物理地址后,我们就需要去内存里去相应的数据,当从内存直接取数据速度太慢,计算机利用cache(高度缓存)来加快访存速度。它位于CPU与内存之间,访问速度比内存块很多,需要从内存里取数据时,先考虑是否在cache里有缓存。图 是一个典型的cache结构。
图7.5 高速缓存

图7.5.1 高速缓存的地址示例

根据高速缓存的大小,我们把物理地址分割成这些部分,其中S = 2^s,B = 2^b,剩下的t位都是标记位,得到一个物理地址后,通过组索引部分可以确定在cache里的哪一组,通过标记位确定看是否与组里的某一行标记相同,如果有,通过块偏移位确定具体是哪个数据块,从而得到我们的数据。如果没有找到,则需要从内存里去数据,并找到cache里的一行替换,对于L1,L2这样的组相联cache,替换策略通常有LFU(最不常使用),LRU(最近最少使用)。
7.6 hello进程fork时的内存映射

当fork函数被新进程调用时,内核为新进程创建各种数据结构,并分配给它唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行文件hello中的程序,加载并运行hello时出现的内存映射有:
映射私有区域 为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区。Bss区域时请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
映射共享区域 如果hello程序与共享对象(或目标链接),比如C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

图7.7
7.8 缺页故障与缺页中断处理

图7.8
假设MMU在试图翻译某个虚拟地址A时,触发了一个缺页。这个异常导致控制转移到内核的缺页处理程序,处理程序随后就执行下面的步骤:
1)虚拟地址A时合法的吗?换句话说,A在某个区域结构定义的区域内吗?为了回答这个问题,缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start 和vm_end 做比较。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止这个进程。这个在图中标识为1
因为一个进程可以创建任意数量的新虚拟内存区域,所以顺序搜索区域结构的链表花销可能会比较大。因此在实际中,Linux使用某些我们没有显示出来的字段,Linux 在链表中构建了一棵树,并在这棵树上进行查找。
2)试图进行的内存访问是否合法?换句话说,进程是否有读、写或者执行这个区域内页面的权限?例如,这个缺页是不是由一条试图对这个代码段里的只读页面进行写操作的存储指令造成的?这个缺页是不是因为一个运行在用户模式中的进程试图从内核虚拟内存中读取字造成的?如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。这种情况在图中标识为2
3)此刻,内核知道了这个缺页是由于对合法的虚拟地址进行合法的操作造成的。它是这样来处理这个缺页的:选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU。这次,MMU就能正常地翻译A,而不会产生缺页中断了。
7.9动态存储分配管理

图7.9

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为 一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已 分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用 来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已 分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分 配器自身隐式执行的。 分配器分为两种基本风格:显式分配器、隐式分配器。

显式分配器:要求应用显式地释放任何已分配的块。例如C程序中的malloc和free。

隐式分配器:要求分配器检测一个已分配块何时不再使用,那么 就释放这个块, 自动释放未使用的已经分配的块的过程叫做垃圾收集。 例如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的内存。

带边界标签的隐式空闲链表分配器原理
一个块是由一个字的头部、有效载荷,以及可能的一些额外的填充组成的。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配还是空闲的。如果我们强加一个双字的对齐约束条件,那么块的大小就总是8的倍数,且块的大小的最低3位总是零。因此,我们只需要内存大小的29个高位,释放剩余的3位来编码其他信息。在这种情况中,我们用其中的最低位来指明这个块是已分配还是空闲的。例如,假设我们有一个已分配的块,大小为24字节。那么它的头部将是0x19.
头部后面就是应用调用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。需要填充有很多原因。比如,填充可能是分配器策略的一部分,用来对付外部碎片。或者也需要用它来满足对齐要求

显式空间链表的基本原理
堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred和succ指针,如下图所示:

图7.10
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从快中枢的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可以是个常熟,这取决于我们所选择的空闲链表中块的排序策略。

7.10本章小结
本章介绍了存储器的地址空间,物理地址,逻辑地址,线性地址,虚拟地址的概念。还有进程fork和execve时内存映射的内容。描述了系统如何应对那些缺页异常,最后阐述了malloc的动态存储分配管理,其中包括隐式和显式空闲链表的内存管理。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix I/O接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为K 。
4.读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“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(int fd)进程通过调用close函数关闭一个已打开的文件,关闭一个已关闭的描述符会出错。
  3. size_t read(int fd,void *buf,size_t n)
    进程通过调用read函数执行输入功能。read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,返回值0表示EOF。否则返回值表示实际传送的字节数量
  4. size_t write(int fd,const void buf,size_t n)进程通过调用write函数执行输出功能。write函数从内存位置buf赋值至多n个字节到描述符fd的当前文件位置。read和write函数传送的字节在某些情况下会比应用程序要求的少,这些不足值不表示有错误。
    8.3 printf的实现分析

图8.3
这里…是可变形参,参数个数不确定时使用。那我们怎么知道具体形参个数?
这里va_list是一个字符指针,(char*)(&fmt) + 4) 表示的是…中的第一个参数。
我们再来看vsprintf

图8.3.2 vsprintf 的代码
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
printf接受一个格式化的命令,并把指定的匹配的参数格式化输出。i = vsprintf(buf, fmt, arg);vsprintf返回的是打印出来的字符串的长度。printf中后面的一句:write(buf, i);调用系统函数write,把buf中的i个元素的值写到终端。
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
这里是给几个寄存器传递了几个参数,然后一个int结束。这样的int表示要调用中断门了。通过中断门,来实现特定的系统服务。int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。
sys_call的功能是显示格式化了的字符串。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析

图8.4.1 getchar 的代码
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了hello的IO管理,Linux的IO设备管理方法,Unix IO接口及其函数,printf的实现分析,getchar的实现分析。
(第8章1分)

结论
1)hello.c由用户在空白文本编辑界面编写代码而来。
2)gcc 通过cpp预处理器将hello.c文件转为hello.i
3)编译器ccl编译,翻译成文本文件hello.s
4)汇编器as将hello.s翻译成机器语言指令,把这些文件打包为可重定位目标程序,将该结果保存在hello.o文件当中。
5)链接器ld将printf.o和hello.o文件链接生成可执行目标程序hello
6)bash中,操作系统调用fork函数创建子进程
7)使用mmap把hello映射至虚拟内存。
8)虚拟地址被TLB,Cache,页表转化为物理地址
9)execve函数被调用加载可执行文件hello;
10)hello被分时操作系统分配时间片,其指令被取指译码执行
最终hello被执行,在OS当中以进程的形式运行,终止后被父进程回收,被内核清除,在运行期间用户可以通过键盘指令对hello程序进行操作(如终止),hello被清除后,又回到了0的状态。
感悟:通过本次大作业,我更加全面系统的了解了这门课程,对书中的知识有了更加全面的认识。同时感受到了计算机系统的复杂性以及严密性。我们一个程序的成功运行需要多少计算机硬件和软件的共同配合。
(结论0分,缺失 -1分,根据内容酌情加分)

附件
名称 作用
hello.c 源代码文件
hello.i cpp预处理产生的文件
hello.s ccl编译产生的汇编代码
hello.o as将hello.s翻译成的机器语言指令
hello 可执行文件
4.4asm.txt 4.4readelf hello.o产生的反汇编代码
5.5asm.txt 重定位文件的反汇编代码

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

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] Bryant,R.E. 深入理解计算机系统
[2] LINUX 逻辑地址、线性地址、物理地址和虚拟地址
转:https://www.cnblogs.com/zengkefu/p/5452792.html
[3] Linux内核中的printf实现
https://blog.csdn.net/u012158332/article/details/78675427
[4] printf函数实现的深入剖析 https://www.cnblogs.com/pianist/p/3315801.html
[5] 虚拟地址、逻辑地址、线性地址、物理地址https://blog.csdn.net/rabbit_in_android/article/details/49976101
[6] 多级页表的原理https://blog.csdn.net/forDreamYue/article/details/78887035
[7] 内存管理策略:页式、段式、段页式https://blog.csdn.net/dreamer841119554/article/details/79965279
(参考文献0分,缺失 -1分)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值