计统大作业

计算机系统大作业

题目程序人生-Hello’s P2P
专业计算学部
学号120L020227
班级2003003
学生马浚杰
指导老师史先俊

计算机科学与技术学院
2022年5月
摘 要:
文章对hello程序的整个生命周期进行分析,将hello的人生分为7个阶段进行处理,对各个阶段进行分析和深度挖掘,并联系教材书本以求达到知行合一,熟练掌握。

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

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

第1章 概述

1.1 Hello简介

1.通过编辑器编写hello.c文件,得到hello.c的源程序。
2.运行C预处理器(cpp)将hello.c进行预处理生成hello.i文件。
3.运行C编译器(ccl)将hello.i进行翻译生成汇编语言文件hello.s。
4.运行汇编器(as)将hello.s翻译成一个可重定位目标文件hello.o。
5.运行链接器ld将hello.o和系统目标文件组合起来,创建了一个可执行目标文件hello。(如图所示)

6.通过shell输入./hello,shell通过fork函数创建新的进程,然后调用了execve对虚拟内存进行映射,通过mmap为hello开辟一片空间。
7.中央处理器CPU从虚拟内存中的.text,.data截取代码和数据,调度器为进程规划时间片,在发生异常时触发异常处理子程序。
1.2 环境与工具
硬件环境:Intel® Core™ i7-10875H CPU 2.30 GHz;16GRAM;1024Disk
软件环境:Windows11 64位;Vmware 16;Ubuntu 20.04 LTS 64位
工具:codeblocks;gdb;Objdump;HexEdito
1.3 中间结果
在这里插入图片描述

1.4 本章小结
本章总体介绍了hello程序“一生”的过程,以及进行实验时的软硬件环境及开发与调试工具等基本信息。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用
1.概念:预处理器根据以字符”#”开头的命令,处理hello.c源程序。
2.作用:根据源代码中的预处理指令修改源代码,预处理从系统头文件中将头文件的源码插入到目标文件中,最终生成hello.i文件。

2.2在Ubuntu下预处理的命令

Linux中预处理hello.c文件的命令是:gcc -E -o hello.i hello.c
在这里插入图片描述

2.3 Hello的预处理结果解析

结果解析:经过预处理后,hello.c被处理成为hello.i文件。打开文件后发现hello.i文件中文件内容大大增加,且仍为可阅读的c语言程序文本文件。hello.i文件对hello.c程序中的宏进行了宏展开,该文件包含了头文件中的内容。如果代码中有#define命令还会对相应符号进行替换。
2.4 本章小结
本章介绍了预处理的相关概念和作用,进行实际操作查看了hello.i文件,是对源程序进行补充和替换
(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

· 概念:编辑器将文本文件hello.i翻译成文本文件hello.s,即一个汇编语言程序。
· 作用:把源程序翻译成目标程序,进行词法分析和语法分析,分析过程中发现有语法错误,给出提示信息

3.2 在Ubuntu下编译的命令

命令:gcc -S hello.i -o hello.s
在这里插入图片描述

3.3 Hello的编译结果解析
3.3.1汇编初始部分
节名称 作用
.file 声明源文件
.text 代码节
.section.rodata 只读数据段
.globl 声明全局变量
.type 声明一个符号是函数类型还是数据类型
.size 声明大小
.string 声明一个字符串
.align 声明对指令或者数据的存放地址进行对齐的方式
3.3.2 数据
1)字符串
程序中有两个字符串,这两个字符串都在只读数据段中,如图所示:

hello.c中唯一的数组是作为main函数的第二个参数,数组的每个元素都是一个指向字符类型的指针。数组的起点存放在栈中-32(%rbp)的位置,被两次调用找参数传给printf函数。
在这里插入图片描述

如图所示,这两个字符串作为printf的参数。
2)局部变量

main函数声明了一个局部变量i,编译器进行编译的时候将局部变量i放入堆栈中。i被放置在栈中-4(%rbp)的位置,如下图所示。
在这里插入图片描述

3)参数argc

参数argc是作为用户传给main函数的参数。同样被放置到堆栈之中了。

4)数组char*argv[]

char*argv[]是main函数的第二个参数,数组的起始地址存放在-32(%rbp)的位置,数组中的每一个元素都是一个指向字符类型的指针,在内存中被两次调用穿给printf函数。

5)立即数

立即数直接体现在汇编代码中。

3.3.3全局函数

hello.c声明了一个函数int main(int argc,char *argv[]),通过阅读汇编代码我们发现此函数是一个全局函数,如图所示。
在这里插入图片描述

3.3.4赋值操作

hello中的赋值操作主要有:i=0,而这个操作在.s文件中汇编代码主要使用mov指令来实现。mov指令根据操作数的字节大小分为:

movb:一个字节赋值,movw:两个字节赋值,

movl:四个字节赋值,movq:八个字节赋值。

3.3.5算术操作

hello.c中的算数操作主要有i++,i是int类型,在汇编代码中用addl实现此操作。
在这里插入图片描述

3.3.6关系操作

1)hello.c中 “if(argc!=4)”,这是一个条件判断语句,在进行编译时,被编译为:cmpl$4, -20(%rbp)。比较后设置条件码,根据条件码判断是否需要跳转。
在这里插入图片描述

2)hello.c源程序中的for循环条件是for(i=0;i<8;i++),该条指令被编译为cmpl$7,-4(%rbp)。同样在判断后设置条件码,为下一步的jle利用条件码跳转做准备。
在这里插入图片描述

3.3.7控制转移指令

汇编语言中设置了条件码,然后根据条件码来进行控制程序的跳转。通过阅读hello.c 的汇编代码,我们发现有如下控制转移指令。

1)判断argc是否等于4。如果等于4,则不执行if语句;反之if不等于4,则执行后续的语句,对应的汇编代码为:
在这里插入图片描述

2)for循环中,每次都要判断i是否小于8来决定是否继续循环。先对i进行赋初值,然后无条件跳转至判断条件的.L3中,然后判断i是否符合循环的条件,若符合则直接跳转到.L4中。这一部分的汇编代码为:
在这里插入图片描述

3.3.8函数操作

hello.c中涉及的函数操作主要有以下几个:main,printf,exit,sleep,和getchar函数。main函数的参数是argc和*argv,printf函数的参数是字符串,exit函数的参数是1,sleep函数的参数是atoi(argv[3])。所有函数的返回值都会存储在%eax寄存器中。函数的调用与传参的过程是给函数传递参数需要先设定一个寄存器,将参数传给这个设定的寄存器后,再通过call来跳转到调用的函数开头的地址。

3.3.9类型转换

hello.c中的atoi(argv[3])将字符串类型转换为整形。int、float、double、short、char之间可以进行相互转化。

3.4 本章小结
本章主要介绍了编译器处理c语言程序的基本过程,编译器分别从c语言的数据,赋值语句,类型转换,算术操作,逻辑/位操作,关系操作,控制转移与函数操作这几点进行分析,hello程序从高级的c语言变成了低阶的汇编语言
(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

1.概念:汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程,汇编器(as)将.s汇编程序翻译成机器语言并将这些指令打包成可重定目标程序的格式存放在.o目标文件中,.o 文件是一个二进制文件,它包含程序的指令编码。
2.作用:将汇编语言翻译成机器语言,使其在链接后能够被机器识别并执行。
4.2 在Ubuntu下汇编的命令
gcc -c -o hello.o hello.s
应截图,展示汇编过程!
在这里插入图片描述

4.3 可重定位目标elf格式
典型的ELF可重定位目标文件
在linux下生成hello.o文件的elf格式命令:readelf -a hello.o > hello.elf
在这里插入图片描述

分析hello.elf中的内容:
LF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: REL (可重定位文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x0
程序头起点: 0 (bytes into file)
Start of section headers: 1232 (bytes into file)
标志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 14
Section header string table index: 13

节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000092 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000380
00000000000000c0 0000000000000018 I 11 1 8
[ 3] .data PROGBITS 0000000000000000 000000d2
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 000000d2
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 000000d8
0000000000000033 0000000000000000 A 0 0 8
[ 6] .comment PROGBITS 0000000000000000 0000010b
0000000000000024 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 0000012f
0000000000000000 0000000000000000 0 0 1
[ 8] .note.gnu.propert NOTE 0000000000000000 00000130
0000000000000020 0000000000000000 A 0 0 8
[ 9] .eh_frame PROGBITS 0000000000000000 00000150
0000000000000038 0000000000000000 A 0 0 8
[10] .rela.eh_frame RELA 0000000000000000 00000440
0000000000000018 0000000000000018 I 11 9 8
[11] .symtab SYMTAB 0000000000000000 00000188
00000000000001b0 0000000000000018 12 10 8
[12] .strtab STRTAB 0000000000000000 00000338
0000000000000048 0000000000000000 0 0 1
[13] .shstrtab STRTAB 0000000000000000 00000458
0000000000000074 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)

There are no section groups in this file.

本文件中没有程序头。

There is no dynamic section in this file.

重定位节 ‘.rela.text’ at offset 0x380 contains 8 entries:
偏移量 信息 类型 符号值 符号名称 + 加数
00000000001c 000500000002 R_X86_64_PC32 0000000000000000 .rodata - 4
000000000021 000c00000004 R_X86_64_PLT32 0000000000000000 puts - 4
00000000002b 000d00000004 R_X86_64_PLT32 0000000000000000 exit - 4
000000000054 000500000002 R_X86_64_PC32 0000000000000000 .rodata + 22
00000000005e 000e00000004 R_X86_64_PLT32 0000000000000000 printf - 4
000000000071 000f00000004 R_X86_64_PLT32 0000000000000000 atoi - 4
000000000078 001000000004 R_X86_64_PLT32 0000000000000000 sleep - 4
000000000087 001100000004 R_X86_64_PLT32 0000000000000000 getchar - 4

重定位节 ‘.rela.eh_frame’ at offset 0x440 contains 1 entry:
偏移量 信息 类型 符号值 符号名称 + 加数
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0

The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported.

Symbol table ‘.symtab’ contains 18 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 9
9: 0000000000000000 0 SECTION LOCAL DEFAULT 6
10: 0000000000000000 146 FUNC GLOBAL DEFAULT 1 main
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND GLOBAL_OFFSET_TABLE
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND exit
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND atoi
16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sleep
17: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND getchar

No version information found in this file.

Displaying notes found in: .note.gnu.property
所有者 Data size Description
GNU 0x00000010 NT_GNU_PROPERTY_TYPE_0
Properties: x86 feature: IBT, SHSTK
1)ELF头 :ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
2)节头:记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。
3)重定位节:重定位节保存的是.text节中需要被修正的信息(任何调用外部函数或者引用全局变量的指令都需要被修正),调用外部函数的指令和引用全局变量的指令需要重定位,调用局部函数的指令不需要重定位。Hello程序中需要被重定位的有printf、puts、exit、sleep、sleepseces、getchar和.rodata中的.L0和.L1。
.rela.eh_frame节是.eh_frame节的重定位信息。
4)符号表:.symtab,一个符号表,它存放在程序中定义和引用的函数和全局变量的信息,一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
命令:objdump -d -r hello.o>hello1.txt
在这里插入图片描述

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
结果解析:与hello.s的差异

1)分支转移
hello.s
在这里插入图片描述

hello1.txt
在这里插入图片描述

反汇编代码跳转指令的操作数使用的不是段名称,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
2)对函数的调用与重定位条目对应
hello.s
在这里插入图片描述

hello1.txt
在这里插入图片描述

在可重定位文件中call后面不再是函数的具体名称,而是一条重定位条目指引的信息。而在反汇编文件中可以看到,call后面直接加的是偏移量。

3)立即数变为十六进制格式

hello.s
在这里插入图片描述

hello1.txt
在这里插入图片描述

在编译文件中,立即数全部是以16进制表示的,因为16进制与2进制之间的转换比十进制更加方便,所以都转换成了16进制。

4.5 本章小结
本章对应的主要是hello.s汇编到hello.o的过程。在本章中,我们查看了hello.o的可重定位目标文件的格式,使用反汇编查看hello.o经过反汇编过程生成的代码并且把它与hello.s进行比较,分析和阐述了从汇编语言进一步翻译成为机器语言的汇编过程。
(第4章1分)

第5章 链接

5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。
作用:链接器在软件开发过程中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。
5.2 在Ubuntu下链接的命令
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
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 > hello1.elf
在这里插入图片描述

在这里插入图片描述

1)ELF头:hello的文件头和hello.o文件头的不同之处如下图标记所示,hello是一个可执行目标文件,有27个节。

2)节头:对 hello中所有的节信息进行了声明,包括大小和偏移量。
3) 重定位节.rela.text:
4)符号表.symtab:
5.4 hello的虚拟地址空间
分析程序头LOAD可加载的程序段的地址为0x400000
在这里插入图片描述

通过edb加载hello程序,打开Data Dump查看hello加载到虚拟地址的状况,并查看各段信息。
在这里插入图片描述

在0x401000~0x402000段中,程序被载入,虚拟地址0x401000开始,到0x401ff0结束,根据5.3中的节头部表,可以通过edb找到各个节的信息,比如.txt节,虚拟地址开始于0x4010f0,大小为0x145.

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
5.5 链接的重定位过程分析
命令: objdump -d -r hello > hello2.txt

与hello.o的反汇编文件对比发现,hello2.txt中多了许多节。hello1.txt中只有一个.text节,而且只有一个main函数,函数地址也是默认的0x000000。hello2.txt中有.init,.plt,.text三个节,而且每个节中有很多函数。库函数的代码都已经链接到了程序中,程序各个节变的更加完整,跳转的地址也具有参考性。
在这里插入图片描述

hello的重定位过程:

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

2)重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。

3)重定位条目当编译器遇到对最终位置未知的目标引用时,它会生成一个重定位条目。代码的重定位条目放在.rel.txt中。

5.6 hello的执行流程
①开始执行:_start、_libc_start_main

②执行main:_main、_printf、_exit、_sleep、_getchar

③退出:exit

程序名 程序地址

_start 0x4010f0

_libc_start_main 0x2f12271d

main 0x401125

_printf 0x401040

_exit 0x401070

_sleep 0x401080

_getchar 0x401050
5.7 Hello的动态链接分析

共享库:共享库是一个目标模块,在加载或运行时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程被称为动态链接,是由一个叫做动态链接器的程序来执行的。共享库也称为共享目标。

动态链接的基本思想:先把程序按照模块拆分成各个相对独立的部分,在程序运行时将这些相对独立的部分链接在一起形成一个完整的程序。

动态延迟绑定:动态的链接器在正常工作的时候链接器采取了延迟绑定的连接器策略,由于静态的编译器本身无法准确的预测变量和函数的绝对运行时的地址,动态的连接器需要等待编译器在程序开始加载时在对编译器进行延迟解析,这样的延迟绑定策略叫做动态延迟绑定。

延迟绑定是通过got和plt实现的。Got是数据段的一部分,而plt是代码段的一部分。

.got.plt起始表的位置为0x404000。

GOT表调用dl_init前0x404008后的16个字节均为0;
在这里插入图片描述

调用dl_init后的.got.plt:

在这里插入图片描述

从图中可以看到.got.plt的条目已经发生变化。
5.8 本章小结

本章研究了链接的过程。通过edb查看hello的虚拟地址空间,对比hello与hello.o的反汇编代码,深入研究了链接的过程中重定位的过程。

(第5章1分)

第6章 hello进程管理

6.1 进程的概念与作用
(1)概念:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

2)作用:进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
6.2 简述壳Shell-bash的作用与处理流程
(1)Shell-bash的作用:Linux系统中,Shell是一个交互型应用级程序,为使用者提供操作界面,接收用户命令,然后调用相应的应用程序。
(2)Shell-bash的处理流程:

① 在shell命令行中输入命令:$./hello
② shell命令行解释器构造argv和envp;
③ 调用fork()函数创建子进程,其地址空间与shell父进程完全相同,包括只读代码段、读写数据段、堆及用户栈等
④ 调用execve()函数在当前进程(新创建的子进程)的上下文中加载并运行hello程序。将hello中的.text节、.data节、.bss节等内容加载到当前进程的虚拟地址空间
⑤ 调用hello程序的main()函数,hello程序开始在一个进程的上下文中运行。
6.3 Hello的fork进程创建过程
在终端中输入./hello 学号 姓名 1命令后,shell会处理该命令,判断出不是内置命令,则会调用fork函数创建一个新的子进程,子进程几乎但不完全与父进程相同。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的但是虚拟地址独立、PID也不相同的一份副本。
6.4 Hello的execve过程
当创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序,即hello程序,需要以下步骤:

①删除已存在的用户区域。删除之前进程在用户部分中已存在的结构。

②创建新的代码、数据、堆和栈段。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。

③映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。

④设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.5.1 逻辑控制流和时间片:

进程的运行本质上是CPU不断从程序计数器 PC 指示的地址处取出指令并执行,值的序列叫做逻辑控制流。操作系统会对进程的运行进行调度,执行进程A->上下文切换->执行进程B->上下文切换->执行进程A->… 如此循环往复。 在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。在一个程序被调运行开始到被另一个进程打断,中间的时间就是运行的时间片。

6.5.2用户模式和内核模式:

Shell使得用户可以有机会修改内核,所以需要设置一些防护措施来保护内核,如限制指令的类型和可以作用的范围。

6.5.3上下文切换

如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程,上下文就是内核重新启动一个被抢占的进程所需要的状态,是一种比较高层次的异常控制流。

6.5.4调度

在对进程进行调度的过程,操作系统主要做了两件事:加载保存的寄存器,切换虚拟地址空间。

6.5.5用户态与核心态转换

为了能让处理器安全运行,需要限制应用程序可执行指令所能访问的地址范围。因此划分了用户态与核心态。

核心态可以说是拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
6.6 hello的异常与信号处理
6.6.1hello的正常运行状态
在这里插入图片描述

6.6.2按下CTRL-Z
在这里插入图片描述

输入ctrl-z默认结果是挂起前台的作业,hello进程并没有回收,而是运行在后台下,用ps命令可以看到,hello进程并没有被回收。此时他的后台 job 号是 1,调用 fg 1 将其调到前台,此时 shell 程序首先打印 hello 的命令行命令, hello 继续运行打印剩下的 8 条 info,之后输入字串,程序结束,同时进程被回收,如下图。

6.6.3按下CTRL-C

在这里插入图片描述

在键盘上输入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业,用ps查看前台进程组发现没有hello进程,如图所示。

6.6.4不停乱按
在这里插入图片描述

无关输入被缓存到stdin,并随着printf指令被输出到结果。
6.7本章小结
本章了解了hello进程的执行过程。在hello运行过程中,内核对其调度,异常处理程序为其将处理各种异常。每种信号都有不同的处理机制,对不同的shell命令,hello也有不同的响应结果。
(第6章1分)

第7章 hello的存储管理

7.1 hello的存储器地址空间
1)逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 段标识符:段内偏移量。
2)线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式,分页机制中线性地址作为输入。
3)虚拟地址:就是线性地址。
4)物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在 Intel 平台下,逻辑地址(logical address)是 selector:offset 这种形式,selector 是 CS 寄存器的值,offset 是 EIP 寄存器的值。如果用 selector 去 GDT( 全局描述符表 ) 里拿到 segment base address(段基址) 然后加上 offset(段内偏移),这就得到了 linear address。我们把这个过程称作段式内存管理。
一个逻辑地址由段标识符和段内偏移量组成。段标识符是一个16位长的字段(段选择符)。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。
给定一个完整的逻辑地址段选择符+段内偏移地址,看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,就得到了其基地址。Base + offset = 线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址即虚拟地址(VA)到物理地址(PA)之间的转换通过分页机制完成,而分页机制是对虚拟地址内存空间进行分页。

系统将虚拟页作为进行数据传输的单元。Linux下每个虚拟页大小为4KB。物理内存也被分割为物理页, MMU(内存管理单元)负责地址翻译,MMU使用页表将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。

n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令

7.4 TLB与四级页表支持下的VA到PA的变换
7.4.1 翻译后备缓冲器
每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。
7.4.2 多级页表:
将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。
7.4.3 VA到PA的变换
处理器生成一个虚拟地址,并将其传送给MMU。MMU用VPN向TLB请求对应的PTE,如果命中,则跳过之后的几步。MMU生成PTE地址(PTEA).,并从高速缓存/主存请求得到PTE。如果请求不成功,MMU向主存请求PTE,高速缓存/主存向MMU返回PTE。PTE的有效位为零, 因此 MMU触发缺页异常,缺页处理程序确定物理内存中的牺牲页 (若页面被修改,则换出到磁盘——写回策略)。缺页处理程序调入新的页面,并更新内存中的PTE。缺页处理程序返回到原来进程,再次执行导致缺页的指令。在多级页表的情况下,无非就是不断通过索引 – 地址 – 索引 - 地址重复四次进行寻找。
7.5 三级Cache支持下的物理内存访问
获得物理地址之后,先取出组索引对应位,在L1中寻找对应组。如果存在,则比较标志位,相等后检查有效位是否为1.如果都满足则命中取出值传给CPU,否则按顺序对L2cache、L3cache、内存进行相同操作,直到出现命中。然后再一级一级向上传,如果有空闲块则将目标块放置到空闲块中,否则将缓存中的某个块驱逐,将目标块放到被驱逐块的位置。
7.6 hello进程fork时的内存映射
在shell输入命令行后,内核调用fork创建子进程,为hello程序的运行创建上下文,并分配一个与父进程不同的PID。同时为这个新进程创建虚拟内存,创建当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。
7.7 hello进程execve时的内存映射
execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。 它可能会自动覆盖当前进程中的所有虚拟地址和空间,删除当前进程虚拟地址的所有用户虚拟和部分空间中的已存在的代码共享区域和结构,但它不会自动创建一个新的代码共享进程。
加载并运行 hello 需要以下几个步骤:
删除当前进程虚拟地址中已存在的用户区域
映射私有区域,为新程序的代码、数据、bss和栈创建新的区域结构。
映射共享区域,将hello与libc.so动态链接,然后再映射到虚拟地址空间中的共享区域。
设置当前进程上下文程序计数器(PC),使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的,在指令请求一个虚拟地址时,MMU中查找页表,如果这时对应得物理地址没有存在主存的内部,我们必须要从磁盘中读出数据。在虚拟内存的习惯说法中,DRAM缓存不命中成为缺页。在发生缺页后系统会调用内核中的一个缺页处理程序,选择一个页面作为牺牲页面。具体流程如下
1)处理器生成一个虚拟地址,并将它传送给MMU
2)MMU生成PTE地址,并从高速缓存/主存请求得到它
3)高速缓存/主存向MMU返回PTE
4)PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
5)缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
6)缺页处理程序页面调入新的页面,并更新内存中的PTE。
7)缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。
7.9动态存储分配管理
动态内存管理的基本方法与策略:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆,系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高地址)。对于每个进程,内核维护着一个变量brk,它指向对的顶部。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显示地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显示地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显示执行的。要么是内存分配器自身隐式执行的。分配器有两种基本风格。两种风格都要求应用显示地分配块。他们的不同之处在于由哪个实体来负责释放已分配的块。
1.显示分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器。
2.隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。
①隐式空闲链表的堆块格式:
在这里插入图片描述

②隐式空闲链表的带边界标记的堆块格式:
在这里插入图片描述

使用边界标记的堆块的格式其中头部和脚部分别存放了当前内存块的大小与是否已分配的信息。通过这种结构,隐式动态内存分配器会对堆进行扫描,通过头部和脚部的结构实现查找。
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。维护链表的顺序有:后进先出(LIFO),将新释放的块放置在链表的开始处,使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比LIFO排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。
7.10本章小结
本章介绍了存储器地址空间、段式管理、页式管理,VA 到 PA 的变换、物理内存访问, hello 进程fork时和execve 时的内存映射、缺页故障与缺页中断处理、包括隐式空闲链表和显式空闲链表的动态存储分配管理。(第7章 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化: Linux将文件所有的I/O设备都模型化为文件,甚至内核也被映射为文件。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。Linux就是基于Unix I/O实现对设备的管理。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix IO接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux Shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。
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)
进程通过调用close函数关闭一个打开的文件,fd是需要关闭的文件的描述符。
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的实现分析
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述 vsprintf函数将所有的参数内容格式化之后存入buf,返回格式化数组的长度。write函数将buf中的i个元素写到终端。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析
当程序调用getchar时,程序等待用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车(回车也在缓冲区中)。

当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
异步异常-键盘中断的处理:当用户按键时触发键盘终端,操作系统将控制转移到键盘中断处理子程序,中断处理程序执行,接受按键扫描码转成ascii码,保存到系统的键盘缓冲区,显示在用户输入的终端内。当中断处理程序执行完毕后,返回到下一条指令运行。
8.5本章小结
本章介绍了Linux的IO设备管理方法,Unix IO接口及其函数,分析了printf函数和getchar函数的实现。
(第8章1分)
结论
用计算机系统的语言,总结hello的一生:

预处理 hello.c预处理到hello.i文本文件
编译 hello.i编译到hello.s汇编文件
汇编 hello.s汇编到二进制可重定位目标文件hello.o
链接 hello.o链接生成可执行文件hello
创建子进程 bash进程调用fork函数,生成子进程
加载程序 execve函数加载运行当前进程的上下文中加载并运行新程序hello
访问内存 hello的运行需要地址的概念,虚拟地址是计算机系统最伟大的抽象
交互 hello的输入输出与外界交互,与linux I/O息息相关
终止 hello最终被shell父进程回收,内核会收回为其创建的所有信息

附件

hello.c 源程序

hello.i 预处理后文件

hello.s 编译后的汇编文件

hello.o 汇编后的可重定位目标执行文件

hello 链接后的可执行文件

hello.elf hello.o的ELF格式

hello1.txt hello.o的反汇编

hello2.txt hello的反汇编代码

hello1.elf hello的ELF格式
(附件0分,缺失 -1分)

参考文献

为完成本次大作业你翻阅的书籍与网站等
[1] 深入理解计算机系统原书第3版-文字版.pdf
[2] https://www.cnblogs.com/diaohaiwei/p/5094959.html 内存管理
[3] https://www.cnblogs.com/pianist/p/3315801.html printf函数的实现分析
[4] https://blog.csdn.net/drshenlei/article/details/4261909 内存地址的转换
[5] https://blog.csdn.net/yueyansheng2/article/details/78860040 fork创建子进程
(参考文献0分,缺失 -1分)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值