HIT计算机系统大作业-程序人生-Hello’s P2P

计算机系统大作业

题 目 程序人生-Hello’s P2P
专 业 计算机
学   号 1190202002
班   级 1903010
学 生 李艺峰    
指 导 教 师 史先俊
  
计算机科学与技术学院
2021年6月

目录

第1章 概述

1.1 Hello简介

P2P: From Program to Process。源程序文本hello.c经过预处理器cpp的预处理变为修改了的源程序hello.i,再经过编译器ccl的编译变为汇编程序hello.s,然后由汇编器as进行汇编变为二进制文件可重定位目标程序、之后链接器ld链接上c标椎库中的函数成为可执行目标程序hello。
在这里插入图片描述

图 1-1
在shell中输入启动命令./hello 1190202002 李艺峰 120,shell解析命令行参数,获取argc、argv,argv[1]为学号,argv[2]为姓名,argv[3]为延时运行时间,循环输出8次。解析命令行参数过程调用fork函数创建进程、execve函数运行函数,通过内存映射、分配空间让hello拥有自己的空间和时间,与其他程序并发地运行。程序Program转换为进程Process,即P2P。
在这里插入图片描述在这里插入图片描述

图 1-2
020: From Zero-0 to Zero-0。shell通过execve加载并执行hello,映射虚拟内存,execve()将程序计数器置为程序入口点,进入程序入口后程序载入物理内存,然后进入 main函数执行目标代码,CPU为执行文件hello分配时间周期,执行逻辑控制流,每条指令在流水线上取值、译码、执行、访存、写回、更新PC。当程序运行结束后,shell父进程回收子进程,释放hello的内存并且删除有关进程上下文。hello从无倒有再到0的过程就是From Zero-0 to Zero-0,即O2O。

1.2 环境与工具

硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows10 64位;Vmware 15.5.0;Ubuntu 20.04.2 LTS 64位
开发与调试工具:GCC,EDB,Hexedit,Objdump,readelf, Code:Blocks

1.3 中间结果

hello.c:源程序文本文件
hello.i:预处理后的文本文件
hello.s:汇编程序文本文件
hello.o:可重定位目标程序(二进制文件)
hello:链接后的可执行目标文件

1.4 本章小结

 本章阐述了P2P和O2O的完整过程。并且介绍实验的硬软件环境、开发和调试工具,和中间生成的文件。

第2章 预处理

2.1 预处理的概念与作用

预处理阶段。预处理器(cpp)根据以字符+开头的命令,修改原始的C程序。比如hello.c中开头的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。

2.2在Ubuntu下预处理的命令

gcc -E hello.c -o hello.i
在这里插入图片描述

图 2.2-1预处理

2.3 Hello的预处理结果解析

预处理器(cpp)根据以字符+开头的命令,修改原始的C程序。读取stdio.h 、stdlib.h 、和unistd.h中的内容,将内容直接插入程序文本中。对于插入的内容,如果有字符+开头的命令,也要读取内容,并将内容插入程序文本中。最终的.i程序将所有#define,#include等头文件对应内容放入.i文件中。

2.4 本章小结

介绍了预处理的命令以及如何进行预处理生成hello.i,预处理后的结果是什么,hello.i中内容都代表了什么。

第3章 编译

3.1 编译的概念与作用

将预处理文本文件进行词法分析、语法分析、语义分析、优化,从C语言等高级语言翻译成机器更好理解的汇编语言。汇编语言能让程序员更好地从底层理解程序的实现,为不同高级语言的不同编译器提供了通用的输出语言。

3.2 在Ubuntu下编译的命令

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

图 3-1编译生成汇编语言

3.3 Hello的编译结果解析

3.3.1数据

常量:c语言如图
在这里插入图片描述

图 3-2C语言代码
汇编代码开头部分如下

在这里插入图片描述

图 3-3
可知,常量为Printf中的内容。
printf中的常量“用法: Hello 学号 姓名 秒数!\n”对应汇编代码.LC0部分
在这里插入图片描述

每3个字节对应一个汉字字符。英文和其他对应一个ASICC码。
"Hello %s %s\n"对应.LC1部分

变量:
局部变量:i,argc,argv[1],argv[2],argv[3]
根据汇编代码

在这里插入图片描述
在这里插入图片描述

图 3-4汇编代码

argc在寄存器%edi中,入栈-20(%rbp)位置后与4比较,相等才跳转到.L2输出学号,姓名。不等输出“用法: Hello 学号 姓名 秒数!\n”,退出。
argv地址在寄存器%rsi中,使用时放在栈-32(%rbp)位置。argv[1]地址为8+%rsi),argv[2]地址16+%rsi,argv[3]为24+%rsi。
i是循环计数器,初始值为0,放在栈-4(%rbp)处,每次循环加1,与7进行比较。

3.3.2赋值

向i赋初值为0,存在栈-4(%rbp)处,每次循环加1,与7比较,小于等于7则循环。for(i=0;i<8;i++)

3.3.3类型转换

使用atoi函数将argv[3]中sleep的时间由字符串显示转换成int型的数字。
汇编中也调用了atoi函数。

3.3.4算术操作

只有i++的算术操作,通过movl $0, -4(%rbp) addl $1, -4(%rbp)实现。第一句赋初值0,第二句每次循环加1.

3.3.5逻辑/位操作

这在此代码中并未体现

3.3.6关系操作

argc!=4,argc在寄存器%edi中,入栈-20(%rbp)位置。通过cmpl $4, -20(%rbp)
je .L2 进行比较argc和4的关系,相等跳走,不等继续运行。
i<8,i是循环计数器,初始值为0,放在栈-4(%rbp)处,cmpl $7, -4(%rbp)比较i与7的关系。jle .L4,小于等于7要继续循环。否则跳出

3.3.7数组/指针/结构操作

有一字符数组argv
在这里插入图片描述

图 3-4字符数组
argv[1]存学号,argv[2]存姓名,argv[3]存sleep时间。
在这里插入图片描述

图 3-5汇编字符数组
argv地址在寄存器%rsi中,使用时放在栈-32(%rbp)位置。所以根据数组的寻址规则argv[1]地址为8±32(%rbp),argv[2]地址16±32(%rbp),argv[3]为24±32(%rbp)。汇编语言中先将argv[2]放在%rdx,再将argv[1]放入%rsi,最后将.LC1内容放入%rdi。这三个寄存器为传参寄存器,传入printf函数中,进行显示Hello 学号,姓名。然后将argv[3]放在%rdi中,传入atoi函数转换成int型,最后传入sleep函数。

3.3.8控制转移

1.if(argc!=4),由前面可知,argc在-20(%rbp)处,所以用cmpl $4, -20(%rbp)
je .L2比较argc和4,若相等跳转到.L2,进行循环输出Hello 学号 姓名。
不等,继续运行,输出“用法: Hello 学号 姓名 秒数!”后以exit(1)退出.

2.for(i=0;i<8;i++)<8。
在这里插入图片描述

图 3-6i循环
i是循环计数器,初始值为0,放在栈-4(%rbp)处,跳到.L3后cmpl $7, -4(%rbp)比较i与7的关系。jle .L4,小于等于7要跳到.L4继续循环,每次.L4循环完成addl $1, -4(%rbp),向i加1。直至i>7,出循环。

3.3.9函数操作

1.printf函数,直接用 call printf@PLT进行调用,传参寄存器会将要输出的内容传递给此函数。
2.exit(1)函数,退出函数。movl $1, %edi call exit@PLT可以以1或0状态,1为非正常状态退出,0为正常状态退出。movl $1, %edi call exit@PLT。将状态1或0传给传参寄存器%edi,%edi会传给exit函数,以相应状态退出。
3.Sleep()函数。movl %eax, %edi call sleep@PLT休眠函数,需要数字型参数。将数字传给传参寄存器%edi,%edi会传给sleep函数,休眠延时相应的时间。
4.atoi()函数。movq %rax, %rdi call atoi@PLT类型转换函数,将字符型数字转换成int型。将字符串传给传参寄存器%rdi,%rdi会传给atoi函数,进行相应的转换。
5.getchar()函数。call getchar@PLT,直接调用,等待读取键盘输入。

3.4 本章小结

本章解释了编译的概念与作用,如何在lunix下进行编译成汇编代码。以及一个简单的c语言程序hello的各个部分,在汇编语言中如何实现,变量解析,算术/关系/结构操作,控制转移,函数调用等如何实现。

第4章 汇编

4.1 汇编的概念与作用

汇编器(as) 将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program) 的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件, 它包含字节是函数的指令编码。这种机器语言,是机器能直接识别的程序语言或指令代码,无需经过翻译,每一操作码在计算机内部都有相应的电路来完成它,或指不经翻译即可为机器直接理解和接受的程序语言或指令代码。一条指令就是机器语言的一个语句,它是一组有意义的二进制代码,指令的基本格式如,操作码字段和地址码字段,其中操作码指明了指令的操作性质及功能,地址码则给出了操作数或操作数的地址。

4.2 在Ubuntu下汇编的命令

gcc -c hello.s -o hello.o
在这里插入图片描述

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

4.3.1ELF头

在这里插入图片描述

图 4-1ELF头
ELF头显示了生成该文件的系统为64位系统,数据存储为补码小端存储。还有版本,OS、ABI等信息。此外还包括程序的类型为可重构的,系统架构X86-64,入口点地址0x0,程序头起点0,段头部起点1240字节,头部大小64字节,段头部大小64字节,段头部数量14,段头部字符串表下标13。

在这里插入图片描述

4.3.2节头

图 4-2节头

节头部表显示了不同节的名称、大小、类型、全体大小、地址、旗标、链接、偏移量、信息、对齐等信息。

4.3.3重定位节

在这里插入图片描述

图 4-3重定位节
当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置.它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text中。已初始化数据的重定位条目放在.rel.data中。

两种最基本的重定位类型:
R_X86_64_PC32 :重定位一个使用32位PC相对地址的引用。计算重定位地址:refaddr = ADDR(s) + r.offset;
*refptr = (unsigned) (ADDR(r.symbol) + r.addend – refaddr)
R_X86_64_32 :重定位一个使用32位PC绝对地址的引用。计算重定位地址:
*refptr = (unsigned) (ADDR(r.symbol) + r.addend);

如第一个.rodata,为相对地址引用。refaddr=ADDR(s) + 0x1c,ADDR(s)由链接器确定。*refptr=(unsigned) (ADDR(r.symbol) -4 -refaddr),ADDR(r.symbol)由链接器确定。这样就能在链接后得到可重定位地址。其他puts,exit,printf,sleep等函数或节地址也可由以上方法计算。

4.3.4符号表

在这里插入图片描述

图 4-4符号表
符号表存放全局符号(非静态全局变量和C函数),外部符号(在其他模块中定义的非静态全局变量和C函数)和局部符号(静态全局变量和c函数)。符号表中有名字,值,大小,类型等信息。可以看到sleep,getchar,puts,exit等函数均在其中。

4.4 Hello.o的结果解析

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

在这里插入图片描述在这里插入图片描述

图 4-5hello.o反汇编与汇编对比
反汇编后与汇编语言基本相同按从前到后的顺序都能一一对应,有几点不同之处。
1.反汇编后不再显示.LC0,.L2等位置标志,所以进行跳转时进行相对地址的跳转。且反汇编中不再显示知道汇编器和链接器工作的伪指令。
2.反汇编后所有的数采用16进制,不再是汇编时的十进制。
3.反汇编调用函数采用call 函数在符号表中重定位条目的方式,因为未链接,无法确定地址。不再像汇编语言直接call 函数名。
4.在汇编语言中,对于全局变量的访问,是leaq .LC0(%rip), %rdi,而在反汇编代码中是lea 0x0(%rip),%rdi ,是因为它们的地址需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。

4.5 本章小结

本章解释了汇编的概念与作用,如何进行汇编与反汇编,二者有何区别。以及ELF中各部分表示什么,如何进行重定位,符号表又代表着什么。加深了对于机器语言和汇编语言之间的理解。

第5章 链接

5.1 链接的概念与作用

链接(linking) 是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时(compile time) , 也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time) , 也就是在程序被加载器(load-er)加载到内存并执行时;甚至执行于运行时(runtime) , 也就是由应用程序来执行。在现代系统中, 链接是由叫做链接器(linker) 的程序自动执行的。链接器在软件开发中扮演着一个关键的角色, 因为它们使得分离编译(separate com-pi lation) 成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。

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-1链接

5.3 可执行目标文件hello的格式

各段的基本信息,包括各段的起始地址,大小等信息。

在这里插入图片描述在这里插入图片描述

图 5-2各段信息

5.4 hello的虚拟地址空间

在这里插入图片描述

图 5-3edb中各段对应的地址
如图,hello!_init段在0x401000处, 虚拟地址空间从0x40100开始,与5.3中.init地址相同。

在这里插入图片描述

图 5-4edb查看init

hello!_start在地址0x4010f0处,代表hello程序从此处开始。与5.3中.text段地址相同。从0x4010f0到0x401235均为.text段。

在这里插入图片描述在这里插入图片描述

图 5-5edb查看start
从0x401020开始的以下内容对应.plt段

在这里插入图片描述

图 5-6edb查看plt
而.plt.sec段则与hello!.plt+0x70对应,地址为0x401090
在这里插入图片描述

图 5-7edb查看.plt.sec
还有其他段地址,可查看5-3图中地址,自行对照。

5.5 链接的重定位过程分析

(以下格式自行编排,编辑时删除)
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

图 5-8反汇编hello与hello.c对比(左为hello)
分析二者区别。
1.hello.o尚未链接,未重定位地址,仅有.text段(main函数部分)。而hello进行了链接,重定位地址空间,包含.init、.plt、.plt.sec、.text(main函数和链接的函数)、.fini段。
2.hello.o未重定位地址空间,反汇编代码虚拟地址从0开始,main函数地址为0。而hello进行了重定位,反汇编代码从0x401000开始,main函数地址为0x401125。
3.函数调用call在hello.o中仅显示此函数的可重定位条目,因为尚未定位地址。
hello中则call 函数的绝对地址,机器码中为相对地址,进行函数调用。

根据
R_X86_64_PC32 :重定位一个使用32位PC相对地址的引用。计算重定位地址:refaddr = ADDR(s) + r.offset;
*refptr = (unsigned) (ADDR(r.symbol) + r.addend – refaddr)
R_X86_64_32 :重定位一个使用32位PC绝对地址的引用。计算重定位地址:
*refptr = (unsigned) (ADDR(r.symbol) + r.addend);
进行重定位地址计算。
由图可知,ADDR(s)=0x401125。例如计算puts函数相对地址:
refaddr = 0x401125+0x21=0x40146
*refptr = (unsigned) (0x401090-0x4- 0x401146)=0xffffff46,小端法与46 ff ff ff相同。
其他函数按此方法计算也均相同。

在这里插入图片描述在这里插入图片描述在这里插入图片描述

图 5-9相对地址计算

5.6 hello的执行流程

使用edb打开hello,以1190202002 李艺峰 2作为输入参数。
在这里插入图片描述

图 5-10edb加载hello
在这里插入图片描述

图 5-11执行到_start

在这里插入图片描述

图5-12运行main函数

在main函数中进行显示hello 学号 姓名,循环8次,每次2秒。然后跳出循环。
在这里插入图片描述

图 5-13循环8次,跳出循环
等待输入后,继续向下执行,然后退出。
在这里插入图片描述

图 5-14调用函数名或地址

如图,调用了.plt+0x70,0x4010d0,0x4010a0,0x4010c0,0x4010e0,0x4010b0。
在这里插入图片描述

图5-15各函数的地址
可有上图查看对应地址的函数名。

5.7 Hello的动态链接分析

动态链接后_GLOBAL_OFFSET_TABLE应发生相应的变化。如下图所示。

在这里插入图片描述在这里插入图片描述

图 5-16dl_init前

在这里插入图片描述在这里插入图片描述

图 5-17dl_init后

5.8 本章小结

本章深入理解了链接的概念与作用,如何对一个可重定位目标文件进行重定位,了解了重定位地址计算方法。辨析了链接前后有哪些不同之处。理解了hello.o与hello进行反汇编后的区别。

第6章 hello进程管理

6.1 进程的概念与作用

概念:
狭义:进程是正在运行的程序的实例。
广义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

作用:
在现代系统上运行一个程序时,我前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是们会得到一个假象,就好像我们的程序是系统中当无间断地一条接一条地执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象都是通且过进程的概念提供给我们的。系统中的每个程序都运行在某个进程的上下文(context)中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。每次用户通过向shell输人一个可执执行目标文件的名字, 运行程序时, shell就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。

6.2 简述壳Shell-bash的作用与处理流程

作用:Shell最重要的功能是命令解释,从这种意义上说,Shell是一个命令解释器。Linux系统上的所有可执行文件都可以作为Shell命令来执行。
处理流程:当用户提交了一个命令后,Shell首先判断它是否为内置命令,如果是就通过Shell内部的解释器将其解释为系统功能调用并转交给内核执行;若是外部命令或实用程序就试图在硬盘中查找该命令并将其调入内存,再将其解释为系统功能调用并转交给内核执行。在查找该命令时分为两种情况:(1)用户给出了命令的路径,Shell就沿着用户给出的路径进行查找,若找到则调入内存,若没找到则输出提示信息;(2)用户没有给出命令的路径,Shell就在环境变量PATH所制定的路径中依次进行查找,若找到则调入内存,若没找到则输出提示信息。

6.3 Hello的fork进程创建过程

用户通过shell输人一个可执行目标文件的名字hello,运行程序时,shell就会
通过父进程调用fork函数创建一个新的运行的子进程hello,int fork(void)子进程返回0,父进程返回子进程hello的PID。新创建的子进程几乎但不完全与父进程相同:子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本(代码、数据段、堆、共享库以及用户栈)。子进程获得与父进程任何打开文件描述符相同的副本, 子进程有不同于父进程的PID。fork函数:被调用一次,却返回两次。

6.4 Hello的execve过程

execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到hello,execve才会返回到调用程序。所以,与fork一次调用返回两次不同,execve调用一次并从不返回。参数列表是用数据结构表示的。argv变量指向一个以null结尾的指针数组,其中每个指针都指向一个参数字符串。按照惯例,argv[0]是可执行目标文件的名字。环境变量的列表是由一个类似的数据结构表示的。envp变量指向一个以null结尾的指针数组,其中每个指针指向一个环境变量字符串,每个串都是形如“name=value”的名字-值对。
在execve加载了hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下形式的原型
int main(int argc,charargv,charenvp);
或者等价的int main(int argc,charargv[,charenvp[]) ;

6.5 Hello的进程执行

最初只有shell进程在运行,即等待命令行上的输入。当我们输入
./hello 1190202002 李艺峰 2 让它运行hello程序时, Shell首先判断其为外部程序,Shell就沿着给出的路径进行查找到hello,通过调用一个专门的函数,即系统调用,来执行我们的请求,系统调用会将控制权传递给操作系统。操作系统保存shell进程的上下文, 创建一个新的hello进程及其上下文, 然后将控制权传给新的hello进程。hello进程终止后, 操作系统恢复shell进程的上下文,并将控制权传回给它,shell进程会继续等待下一个命令行输入。
在这里插入图片描述

图 6-1上下文切换
如图所示, 从一个进程到另一个进程的转换是由操作系统内核(kernel) 管理的。内核是操作系统代码常驻主存的部分。当应用程序需要操作系统的某些操作时,比如读写文件, 它就执行一条特殊的系统调用(system call) 指令, 将控制权传递给内核.然后内核执行被请求的操作并返回应用程序。注意,内核不是一个独立的进程。相反,它是系统管理全部进程所用代码和数据结构的集合。

6.6 hello的异常与信号处理

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
1.不停乱按,包括回车。
不按到命令不会产生什么影响,再按回车后会将之前的输入作为getchar(),回车之后再输入并回车,会作为之后的命令。
在这里插入图片描述

图 6-2乱按,包括回车
2.Ctrl—Z,运行各命令
Ctrl+z会将当前前台作业hello挂起到后台。
然后ps命令列出当前系统中的进程(bash,hello,ps,包括僵死进程)和他们的pid,time,tty。
jobs命令会列出当前shell环境中已启动的任务状态。hello任务处于停止状态。
pstree以树状图显示进程间的关系。
fg将挂起的作业在前台继续运行。
kill -9 -4567(hello pid),将hello杀死并回收。
ps命令列出当前系统中的进程,不在包含hello.

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

图 6-3Ctrl+z后运行各命令
3.Ctrl+c后运行命令
Ctrl+c导致内核发送一个SIGINT信号到前台进程组中每个进程。默认情况下,结果是终止前台作业。将hello程序终止。
ps查看系统进程,hello被回收。
fg,jobs均无效。
具体过程均如下图所示

在这里插入图片描述

图 6-4Ctrl+c后运行命令

6.7本章小结

本章进行了进程的研究,理解了进程的创建和回收,以及进程的上下文切换。对于产生信号和信号的处理过程有了更多的了解。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:在机器语言中用来指定操作数或指令的地址,由段选择符和段内偏移量组成,偏移量指明了从段开始的地方到实际地址之间的距离。就是hello.o里相对偏移地址。
线性地址:以段描述符作为下标,到GDT/LDT表查表获得段地址, 段地址+偏移地址=线性地址。非负整数地址的有序集合。
虚拟地址:一个带虚拟内存的系统中,CPU从一个有N=2^n个地址空间中生成虚拟地址。虚拟地址其实就是线性地址。
物理地址:主存被组织成一个由M个连续字节大小的单元组成的数组,每一个字节都有一个物理地址。用于内存芯片级单元寻址。地址翻译会将hello的一个虚拟地址转化为物理地址。

7.2 Intel逻辑地址到线性地址的变换-段式管理

逻辑地址包括前16位的段选择符和后32位的段内偏移量。
前16位段选择符前13位为索引,14位TI,TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)。 最后两位RPL,RPL=00,为第0级,位于最高级的内核态,RPL=11,为第3级,位于最低级的用户态。如图
在这里插入图片描述

图 7-1段选择符
全局描述符表GDT:只有一个,用来存放系统内每个任务都可能访问的描述符,例如,内核代码段、内核数据段、用户代码段、用户数据段以及TSS(任务状态段)等都属于GDT中描述的段。局部描述符表LDT:存放某任务(即用户进程)专用的描述符。
根据TI的值,选择全局描述符表GDT或局部描述符表LDT,被选中的段描述符表先被送至描述符cache,根据索引每次从描述符cache中取32位段基址,与32位段内偏移量(有效地址)相加得到线性地址。

7.3 Hello的线性地址到物理地址的变换-页式管理

在这里插入图片描述

图 7-2虚拟地址到物理地址
MMU利用VPN来选择适当的PTE。例如,VPN 0选择PTE 0,VPN 1选择PTE 1,以此类推。将页表条目中物理页号和虚拟地址中的VPO串联起来,就得到相应的物理地址。注意,因为物理和虚拟页面都是P字节的,所以物理页面偏移和VPO是相同的。

7.4 TLB与四级页表支持下的VA到PA的变换

在这里插入图片描述

图 7-3四级页表变换

使用4级页表层次结构的地址翻译。虚拟地址被划分成为4个VPN和1个VPO。每个VPN i都是一个到第i级页表的索引,其中1≤i≤4。第j级页表中的每个PTE,1≤j≤k-1,都指向第j+1级的某个页表的基址。第4级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问4个PTE。

7.5 三级Cache支持下的物理内存访问

一个虚拟地址,首先以VPN为TLB索引和标识去TLB寻找对应的PPN。如果命中直接MMU获取,PPN+VPO获得物理地址;没命中先通过虚拟地址和页表计算出物理地址。在L1中找对应的物理地址,命中返回对应结果给CPU;不命中则去 L2找,命中返回对应结果给CPU,不命中再去L3,命中返回对应结果给CPU,不命中再去主存寻找。

7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时, 内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存, 它创建了当前进程的mmstruct、、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制当fork在新进程中返回时, 新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
在这里插入图片描述

图 7-4私有的写时复制对象

7.7 hello进程execve时的内存映射

删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为a.out文件中的.text和.deta区。bss区域是请求二进制零的, 映射到匿名文件, 其大小包含在a.out中。和堆区域也是请求二进制零的,初始长度为零。图9-31概括了私有区域的不同映射。
映射共享区域。如果a.out程序与共享对象(或目标) 链接, 比如标准C库libc.SD:那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的人口点。下-次调度这个进程时, 它将从这个人口点开始执行.Linux将根据需要换入代码和数据页面。

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

DRAM缓存不命中称为缺页。CPU引用了页中的一个字, 但此页并未缓存在DRAM中。地址翻译硬件从内存中读取PTE , 从有效位推断出此页未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页。如果牺牲页已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改牺牲页对应的页表条目,牺牲页不再缓存在主存中。将引用的页放在主存中,更新页表。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk(读做“break”) , 它指向堆的顶部。分配器将堆视为一组不同大小的块(block) 的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
在这里插入图片描述

图 9-5堆

显式分配器(explicit alloc at or),要求应用显式地释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用nmalloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和和delete操作符与C中的malloc和free相当。
隐式分配器(implicit aloc at or),另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbage collec-to r),而自动释放未使用的已分配的块的过程叫做垃圾收集(garbage collection)。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。

隐式空闲链表:一个块由一个字的头部、有效载荷,以及可能的一些额外的填充组成,以及这个块是已分配的还是空闲的。头部:是由块大小+标志位(a已分配/f空闲);有效载荷:实际的数据。
放置已分配的块:当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大可以放置所请求块的空闲块。分配器执行这种搜索的方式是由放置策略(placement policy)确定的。首次适配从头开始搜索空闲链表,选择第一个合适的空闲块。下一次适配和首次适配很相似、只不过不是从链表的起始处开始每次搜索,而是从上一次查询结束的地方开始,最佳适配检查每个空闲块,选择适合所需请求大小的最小空闲块。
分割空闲块:如果匹配不太好,那么分配器通常会选择将这个空闲块分割为两部分。第一部分变成分配块,面剩下的变成一个新的空闲块。
获取额外的堆内存:如果不能生成一个足够大的块,或者如果空闲块已经最大程度地合并了,那么分配器就会通过调用sbrk函数,向内核请求额外的堆内存。分配器将额外的内存转化成一个大的空闲块,将这个块插人到空闲链表中,然后将被请求的块放置在这个新的空闲块中。
合并空闲块:当分配器释放一个已分配块时,可能有其他空闲块与这个新释放的空闲块相邻。将相邻的空闲块合并。
带边界标记的合并:在每个块的结尾处添加一个脚部(footer,边界标记),其中脚部就是头部的一个副本。如果每个块包括这样–个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。
显示空闲链表:空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面,例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。
分离的空闲链表:就是维护多个空闲链表,其中每个链表中的块有大致相等的大小。分配器维护着一个空闲链表数组,每个大小类一个空闲链表,按照大小的升序排列。当分配器需要一个大小为n的块时,它就搜索相应的空闲链表。如果不能找到合适的块与之匹配,它就搜索下一个链表,以此类推。
7.10本章小结
本章深入理解了虚拟内存和物理内存的含义和对应关系,学会了如何通过虚拟内存翻译为物理内存。同时对于fork,execve执行时内存的映射关系有了更深刻的理解。对于内存分配器工作原理有了更加全面系统的认识。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
一个Linux文件就是一个m个字节的序列:Bo,By,…,B,,…,B.-1所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输人和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式, 允许Linux内核引出一个简单、低级的应用接口, 称为Unix I/O, 这使得所有的输入和输出都能以一种统一且一致的方式来执行:
打开文件。一个应用程序通过要求内核打开相应的文件,来宜告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
Linux shel创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件

8.2 简述Unix IO接口及其函数

open函数:int open(char *filename,intflags, mode_t1ode) ;
返回:若成功则为新文件描述符,若出错为-1。
open函数将filename转换为一个文件描述等,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件。mode参数指定了新文件的访问权限。

close函数:int close(int fd);
返回:若成功则为0,若出错为-1。
关闭一个打开的文件。
read函数:ssize_t read(int fd, void *buf, size_t n);
返回:成功则为读取的字节数;若EOF则为0;出错为-1。
read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。
write函数:ssize_t write(int fd, void *buf, size_t n);
返回:成功则为写的字节数;出错为-1。
write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
lseek函数:应用程序能够显示的修改当前文件的位置。

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;
    }
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返回要打印出来的字符串的长度。
write:写操作,把buf中的i个元素的值写到终端。

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

sys_call显示格式化了的字符串
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 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)?(unsigned char)*bb++:EOF;
}

返回:该函数以无符号 char 强制转换为 int 的形式返回读取的字符,如果到达文件末尾或发生读错误,则返回 EOF。
当程序调用getchar时.程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdin流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才等待用户按键。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章理解了IO设备对文件的管理方法,以及对应的函数使用,了解了printf函数和getchar函数的实现。

结论

用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
hello经历的过程:
1.用IO设备写出hello.c文本文件,并以文件的方式储存在主存中。
2.hello.c被预处理器cpp处理为hello.i文件
3.hello.i被ccl编译为hello.s汇编文件
4.hello.s被as汇编成可重定位目标文件hello.o
5.链接器ld将hello.o和外部文件链接,进行重定位,生成可执行文件hello
6.在shell输入命令./hello 1190202002 李艺峰 2 后,父进程通过fork函数为hello创建进程
7.通过加载器,调用execve函数,删除原来的进程内容,加载我们现在进程的代码,数据等到进程自己的虚拟内存空间。通过exceve加载并运行hello
8.运行过程中,CPU解释并执行存储在主存中的指令。通过内存管理单元MMU、翻译后备缓冲器TLB、多级页表机制将虚拟地址翻译为物理地址,物理地址通过三级cache、主存得到对应的内容,将内容返回给CPU。
9.hello在运行过程中会有异常和信号等,异常处理机制保证了hello对异常信号的处理,使程序平稳运行
10.printf会调用malloc通过动态内存分配器申请堆中的内存
11.getchar()通过IO进行交互。
12.运行完毕,shell父进程回收hello子进程,内核删除为hello创建的所有数据结构

深切感悟:通过这门课的学习,我对于计算机如何工作有了更加底层,更加精确的认识。这对于我今后的学习是至关重要的。今后遇到各种问题,能更加地从底层的角度来看待问题,解决问题。很多问题以前只知其然,学完这门课真正地知其所以然。

附件

列出所有的中间产物的文件名,并予以说明起作用。
hello.c:源程序文本文件
  hello.i:预处理后的文本文件
  hello.s:汇编程序文本文件
  hello.o:可重定位目标程序(二进制文件)
hello:链接后的可执行目标文件

参考文献

[1] Randal E.Bryant David R.O’Hallaron 深入理解计算机系统 北京:机械工业出版社,2017
[2]https://www.cnblogs.com/pianist/p/3315801.html
[3]https://baike.baidu.com/item/getchar/919709
[4]汇编语言(面向机器的程序设计语言)_百度百科 (baidu.com)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值