ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

2021-06-29

2021-06-29 23:31:06  阅读:179  来源: 互联网

标签:文件 06 函数 程序 29 地址 2021 进程 hello


计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机专业
学   号 1190201722
班   级 1936603
学 生 武晏峰  
指 导 教 师 刘宏伟

计算机科学与技术学院
2021年6月
摘 要
本文在linux操作系统下对C语言程序hello.c的运行全过程进行了分析。分析了从c文件转化为可执行文件过程中的预处理、编译、汇编和链接阶段,和可执行文件执行过程中的进程管理、存储空间管理和I/O管理的原理。
(摘要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的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:From Program to Process
在编译器的处理下,hello.c文件经过预处理、编译、汇编、链接变为可执行文件(program),然后由shell为其建立一个新的进程(process)并运行他。
020:From Zero to Zero
在他还没有被执行的时候(zero),在shell通过fork为其创建新的子进程后,通过exceve在进程的上下文中加载并运行hello,把他映射到虚拟内存,并载入物理内存,在CPU下执行,在程序运行结束后,父进程会对其进行回收,内核把他从系统中清除(zero)。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位
开发工具:GDB/OBJDUMP;EDB;gedit+gcc;CodeBlocks 64位等。
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
1.4 本章小结
本章简要介绍了Hello的P2P,020的整个过程以及实验的环境、工具和中间产物。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
预处理是指在进行第一次编译之前所做的工作,预处理器根据以字符#开头的命令,如#define(宏定义),#include(文件包含),#ifdef(条件编译),修改原始的C程序。例如将头文件从库中提取出来插入到程序文本中,得到完整的源程序,通常以.i作为文件的扩展名。
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析
可见hello.i与hello.c相比,代码大大增多,而源程序hello.c中除注释和头文件部分位于hello.i的最后。

Cpp到默认的环境变量下寻找stdlib.h,打开/usr/include/stdlib.h,其中可能仍然会有#define语句,cpp对此进行递归展开,最终hello.i文件中只有对外部变量的声明,函数声明,没有宏定义。

2.4 本章小结
本章介绍了hello.c在编译前需要做的准备工作,预处理的内容与结果。
cpp(预处理器)将hello.c转换为hello.i文件。

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
编译是指将预处理后的程序转化为特定的汇编程序的过程。输入.i文件,输出.s文件。
这个过程将较偏向自然语言的c文件,转换为偏机器语言的汇编文件,为下一步的汇编生成机器码创造了条件,同时也保持了一定的可读性,和微弱的可移植性。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析

1.程序中的字符串分别是:
1)“Usage: Hello 学号 姓名!\n”,printf传入的格式化参数。在hello.s中声明如下图,注意到字符串使用UTF-8的格式编码的,一个汉字在UTF-8中占三个字节。
2)“Hello %s %s\n”,仍然是由printf函数传入的格式化参数,hello.s声明如下。
2.数字常量:hello.s中出现的常量有以下几个地方:对全局变量赋值2,将argc与3比较,循环中每次给i加1,循环中止条件i<10的判断。

3.数字变量:
全局变量:有一个全局变量int sleepsecs,可见它作为全局变量被放在.data节中,设置了大小为4字节,并初始化为2。

局部变量:中局部变量int argc存放在栈中-20(%rbp)的位置,通过与3的比较操作可以找到它;局部变量int i存放在栈中-4(%rbp)的位置,从与9的比较和循环中每次加1的操作可以找到它;还有一个局部变量数组char argv[],可以通过L4(循环部分)中输出函数前的两次取值找到argv[1],argv[2]的位置。

4.赋值:赋值操作共有两个,一个是对全局变量sleepsecs的赋值,源程序里令int sleepsecs = 2.5。而因为sleepsecs为整型变量,所以编译时直接对其赋值为2。另一个是对局部变量i赋值,之前已经得知i存在栈中-4(%rbp)的位置。
5.类型转换:对全局变量sleepsecs的赋值存在一个隐式类型转换。int sleepsecs = 2.5因为它把一个浮点数赋给整型变量,所以它会把浮点数2.5强制转换为2(浮点数转整数时向零舍入)。
6.算术操作:存在一个算术操作i++,即在每次循环中对变量i加1,之前已经得知i存在栈中-4(%rbp)的位置,那么通过add每次对-4(%rbp)中内容加1即可。
7.关系操作:存在两个关系操作,第一个是判断argc!=3,即将argc(栈中-20(%rbp)的内容)与3通过cmp进行比较。第二个是判断i<10,即将i(栈中-4(%rbp)的内容)与9通过cmp进行比较(即判断i<=9)。
8.数组/指针/结构操作:存在一个对数组argv的操作,在printf函数中引用了数组argv的两个元素argv[1],argv[2], 可以通过L4(循环部分)中输出函数前的两次取值找到argv[1],argv[2]的位置。
9.控制转移:
第一处是判断argv是否等于3,若不等于,则继续执行,若等于,则跳转至L2处(循环前对i初始化)继续执行。
第二处是对i初始化为0后的无条件跳转,以跳到L4,即循环部分代码。
第三处是判断是否达到循环终止条件(i<10),这里用i与9进行比较,若小于等于则跳回L4重复循环,否则执行循环外的下一步。这里将i<10的比较改为了与其等价的i<=9。
10.函数调用:
共有三次函数调用,第一次调用puts函数输出一个字符串常量,参数存在%rdi中;
第二次调用printf函数输出字符串常量以及两个局部变量数组的元素,字符串常量作为参数1存在%rdi中,两个数组元素作为参数2、3分别存在%rsi和%rdx中。
第三次调用sleep函数,以sleepsecs为参数,参数存在%edi中。
3.4 本章小结
汇编语言是高级语言和机器语言的中介,一方面具有可读性,但是不像高级语言那样易懂,但是一方面反映了机器的一些特征,汇编语言一定程度上翻译了指令集体系的架构。但是从高级语言到汇编语言的映射转化是不容易的。
ccl(编译器)将hello.i转换成hello.s文件。

(第3章2分)

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

汇编器as将hello.s翻译成机器语言指令,把这些指令打包成一种叫可重定位目标程序的格式,并将结果保存在目标文件hello.o中。(hello.o是一个二进制文件)。
作用:把汇编语言翻译成机器语言,用二进制码代替汇编语言中的符号,即让它成为机器可以直接识别的程序。
4.2 在Ubuntu下汇编的命令
as hello.s -o hello.o

4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
readelf -a hello.o
ELF可重定位目标文件中首先是ELF头,它以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息:包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。

然后是节头部表,它描述了不同节的位置和大小,目标文件中的每一个节都有一个固定大小的条目。

重定位项目,其中.rela.text节是一个.text节中位置的列表。当链接器把这个目标文件和其他文件组合时,需要修改这些位置。.rela.eh_frame节包含了对en_frame节的重定位信息。其中,Offset是需要被修改的引用的字节偏移(在代码节或数据节的偏移),Info指示了重定位目标在.symtab中的偏移量和重定位类型,Type表示不同的重定位类型,例如图中R_X86_64_PC32就表示重定位一个使用32位PC相对地址的引用。Sym. Name表示被修改引用应该指向的符号,Append用于一些类型的重定位要使用它对被修改引用的值做偏移调整。可见下图中,在链接时需要对.rodata中的两个字符串常量(用于printf函数中),全局变量sleepsecs,以及函数puts,exit,printf,sleep,getchar进行重定位。

符号表,它存放了程序中定义和引用的函数和全局变量的信息(不包含局部变量的条目)。

4.4 Hello.o的结果解析
通过objdump可以得到hello.o的反汇编代码,在汇编时从main开始(地址为0)依次为每一行指令都分配了一个地址。可以在下图中看到汇编所得到的机器语言,机器语言由二进制的操作码和操作数构成,图中给出了一个示例。每一条汇编指令能翻译成一条对应的机器指令,汇编语言可以看作是二进制机器语言的助记符。

1)可以看出,汇编语言中操作数是十进制的,而机器语言反汇编得到的操作数是十六进制的。
2)对全局变量(即字符串常量)的引用,汇编语言中是用的全局变量所在的那一段的名称加上%rip的值,而hello.o中用的是0加%rip的值,因为当前为可重定位目标文件,之后还需经过重定位方可确定其具体位置,所以这里都用0来代替。
3)对分支转移,hello.s的汇编语言中在跳转指令后用对应段的名称(如.L3)表示跳转到的位置,而hello.o中因为每行指令都被分配了对应的地址(从main函数第一条指令地址为0开始),在跳转指令后用跳转目的的地址来表示跳转到的位置。
4)函数调用,hello.s中的汇编语言在函数调用时,在call指令后用函数的名字表示对其调用,而反汇编指令在call指令后加上下一条指令的地址来表示,观察机器语言,发现其中操作数都为0,即函数的相对地址为0,因为再链接生成可执行文件后才会生成其确定的地址,所以这里的相对地址都用0代替。
4.5 本章小结
在汇编过程中,hello实现了由汇编语言到机器语言的转变,hello第一次称为了机器可以读懂的代码,它的每条指令得到了一个暂时的地址,并通过在不同地址间的跳转把程序连接成了一个整体。hello也第一次由文本程序变成了二进制程序。
as(汇编器)将hello.s转换成hello.o文件。

(第4章1分)

第5章 链接
5.1 链接的概念与作用
链接器,将程序调用的外部函数(.o文件)与当前.o文件以某种方式并,并得到./hello可执行目标文件的的过程成为链接。且该二进制文件可被加载到内存,并由系统执行。
链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。基于此特性的改进,以提高程序运行时的时间、空间利用效率。链接是由叫做链接器的程序执行的。链接器使得分离编译成为可能。它将巨大的源文件分解成更小的模块,易于管理。我么可以通过独立地修改或编译这些模块,并重新链接应用,不必再重新编译其他文件。
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的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
可通过readelf -a来查看可执行目标文件hello的ELF文件各节基本信息(包含在节头部表中),他描述了不同节的位置和大小等基本信息。其中,它的第一列按地址顺序列出了各段的名称及大小,第三列列出来各段的起始地址,最后一列列出来各段的偏移量。

5.4 hello的虚拟地址空间
通过readelf查看hello的Program Headers,可发现其中列出的虚拟地址在edb的Data Dump中都能找到对应的位置。

5.5 链接的重定位过程分析
通过readelf查看hello的Program Headers,可发现其中列出的虚拟地址在edb的Data Dump中都能找到对应的位置。

5.6 hello的执行流程
1)在hello.o中,我们看不到各函数的代码段,而在hello中,存在了各个函数的代码段并且,并且每个函数(以及其每条)指令都有了对应的虚拟地址。
2)在hello.o中main函数的起始地址为0,往后依次得到每条指令的简单地址,而hello中每条指令都拥有一个虚拟地址,main函数也不是从0开始了。

3)对于全局变量的引用,hello.o中用0加上%rip的值来表示全局变量的位置,因为当时并未对全局变量进行定位,而在hello中,因为全局变量都有了确定的位置,所以用实际的相对偏移加%rip的值来描述其位置。
4)对于函数的调用,因为hello.o中尚未对函数定位,所以在调用时都用call加下一条指令地址来表示,而hello中各函数已拥有了各自的虚拟地址,所以在call后加其虚拟地址来实现函数调用。
5)对于跳转指令,hello.o中在其后加上目的地址,为main从0开始对每条指令分配的地址;而hello中同样加上目的地址,但这里是每条指令的虚拟地址。
6)_dl_start 地址:0x7ff806de3ea0
_dl_init 地址:0x7f75c903e630
_start 地址:0x400500
_libc_start_main 地址:0x7fce59403ab0
_cxa_atexit 地址:0x7f38b81b9430
_libc_csu_init 地址:0x4005c0
_setjmp 地址:0x7f38b81b4c10
_sigsetjmp 地址:0x7efd8eb79b70
_sigjmp_save 地址:0x7efd8eb79bd0
main 地址:0x400532
(puts 地址:0x4004b0
exit 地址:0x4004e0) (argc!=3时)
print 地址:0x4004c0
sleep 地址:0x4004f0 (以上两个在循环体中执行10次)
getchar 地址:0x4004d0
_dl_runtime_resolve_xsave 地址:0x7f5852241680
_dl_fixup 地址:0x7f5852239df0
_uflow 地址:0x7f593a9a10d0
exit 地址:0x7f889f672120
5.7 Hello的动态链接分析
当程序调用一个由共享库定义的函数时,编译器无法预测这个函数运行时的地址,因为定义它的共享模块在运行时可以加载到任何位置。这时,编译系统提供了延迟绑定的方法,将过程地址的绑定推迟到第一次调用该过程时。他通过GOT和过程链接表PLT的协作来解析函数的地址。在加载时,动态链接器会重定位GOT中的每个条目,使它包含正确的绝对地址,而PLT中的每个函数负责调用不同函数。那么,通过观察edb,便可发现dl_init后.got.plt节发生的变化。
通过readelf可以发现.got.plt节在地址为0x601000的地方开始。而它后面的.data节从地址0x601040开始。那么中间部分便是.got.plt的内容。
5.8 本章小结
本章介绍了链接的概念和作用,分析了hello的格式、虚拟地址空间、重定位过程、执行流程和动态链接分析。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是一个执行中程序的实例。系统中每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
作用:在现代系统上运行一个程序时,我们会得到一个假象,好像我们的程序是系统中唯一运行的程序一样。我们的程序好像独占处理器和内存。处理器好像无间断地一条接一条执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象是通过进程的概念提供的。进程提供给应用程序的关键抽象:1)一个独立的逻辑控制流,提供一个程序独占处理器的假象;2)一个私有的地址空间,提供一个程序独占地使用内存系统的假象。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行,求值步骤解析命令行,并代表用户运行程序。
处理流程:shell打印一个命令行提示符,等待用户在stdin上输入命令行,然后对命令行求值,即解析以空格分隔的命令行参数,第一个参数被假设为要么是一个内置的shell命令名,马上就会解释这个命令并执行相应操作;要么是一个可执行目标文件,会通过fork创建一个新的子进程,并在新的子进程的上下文中通过execve加载并运行这个文件。如果用户要求在后台运行该程序,那么shell返回到循环的顶部,等待下一个命令行。否则,shell使用waitpid函数等待作业终止并回收。当作业终止时,shell开始下一轮的迭代。
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的运行的子进程。调用fork函数后,新创建的子进程几乎但不完全与父进程相同:子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本(包括代码、数据段、堆、共享库以及用户栈),子进程获得与父进程任何打开文件描述符相同的副本,这意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。fork被调用一次,却返回两次,子进程返回0,父进程返回子进程的PID。子进程有不同于父进程的PID。
6.4 Hello的execve过程
exceve函数在当前进程的上下文中加载并运行一个新程序。exceve函数加载并运行可执行目标文件,并带参数列表和环境变量列表。只有当出现错误时,exceve才会返回到调用程序,否则,exceve调用一次且从不返回。在exceve加载了可执行目标文件后,他调用启动代码,启动代码设置栈,将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序,由此将控制传递给新程序的主函数。
6.5 Hello的进程执行
系统中通常有许多程序在运行,那么进程会为每个程序提供一个好像它在独占地使用处理器的假象。这时依赖于进程提供的独立的逻辑控制流(由上下文切换机制提供)。如一个系统运行着多个进程,那么处理器的一个物理控制流就被分成了多个逻辑控制流,每个进程1个。这些逻辑流的执行是交错的,它们轮流使用处理器,会存在并发执行的现象。其中,一个进程执行它的控制流的一部分的每一时间段叫做时间片。这样的机制使进程在执行时仿佛独占了处理器。
处理器用某个控制寄存器中的一个模式位来限制一个应用可以执行的指令以及它可以访问的地址空间范围。没有设置模式位时,进程运行在用户模式中,它必须通过系统调用接口才可间接访问内核代码和数据;而设置模式位时,它运行在内核模式中,可以执行指令集中的任何指令,访问系统内存的任何位置。异常发生时,控制传递到异常处理程序,由用户模式转变到内核模式,返回至应用程序代码时,又从内核模式转变到用户模式。
操作系统内核使用上下文切换来实现多任务。内核为每个进程维持一个上下文,它是内核重启被抢占的进程所需的状态,包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构的值。
进程执行到某些时刻,内核可决定抢占该进程,并重新开启一个先前被抢占了的进程,这种决策称为调度。内核调度一个新的进程运行后,通过上下文切换机制来转移控制到新的进程:1)保存当前进程上下文;2)恢复某个先前被抢占的进程被保存的上下文3)将控制转移给这个新恢复的进程。当内核代表用户执行系统调用时,可能会发生上下文切换,这时就存在着用户态与核心态的转换。
6.6 hello的异常与信号处理
hello执行过程中会出现的异常有:
中断:他由处理器外部的I/O设备的信号引起(如Ctrl-Z,Ctrl-C),可能产生信号SIGSTP,它会将程序挂起,直到有下一个SIGCONT信号;也可能产生信号SIGINT,它会将进程终止。
1)运行程序
在终端运行程序,打印十次Hello 姓名 学号后,输入hello(任意)回车,程序执行完成,进程被回收。

2)运行时不停乱按(包括回车)
发现他会把乱按的字符打印出来,按回车它会换一行,但是这些都不影响程序的正常执行,因为当程序执行时他不会受到外部输入的影响,它会阻塞这些操作产生的信号,而因为之前将大量字符(包括回车)输入到了屏幕上,所以最后不用自己再输入字符来结束程序,而是直接读取之前的输入。

3)运行程序时按Ctrl-Z
程序运行时按Ctrl-Z,这时,产生中断异常,它的父进程会接收到信号SIGSTP并运行信号处理程序,然后便发现程序在这时被挂起了,并打印了相关挂起信息。

4)运行程序时按Ctrl-C
运行hello时按Ctrl-C,会导致一个中断异常,从而内核产生信号SIGINT,父进程受到它后,向子进程发生SIGKILL来强制终止子进程hello并回收它。这时在运行ps,可以发现并没有进程hello,可以说明他已经被终止并回收了。

6.7本章小结
hello开始真正在系统上运行时,离不开shell给它提供的平台,也离不开进程机制的支持和各种信号的通知。从创建进程,到在进程中加载程序,信号以及上下文切换使其可以自如的运行在计算机中,就好像独占了整个CPU。而当hello的进程生命结束,同样需要各种信号与系统的配合来对它进行终止,回收。程序的高效运行离不开异常、信号、进程等概念,正是这些机制支持hello能够顺利地在计算机上运行。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址指由程序产生的与段相关的偏移地址部分。在有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。从hello的反汇编代码中看到的地址,它们需要通过计算,即加上对应段的基地址才能得到真正的地址,这些便是hello中的逻辑地址。
线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,其偏移量加上基地址就是线性地址。hello的反汇编文件中看到的地址(即逻辑地址)中的偏移量,加上对应段的基地址,便得到了hello中内容对应的线性地址。
虚拟地址:使用虚拟寻址时,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址在被送至内存前先转换成适当的物理地址。虚拟地址转化成物理地址的过程叫做地址翻译。在linux中,虚拟地址数值树等于线性地址,即hello中看到的地址加上对应段基地址的值。
物理地址:计算机系统的主存被组织成一个M个连续字节大小的单元组成的数组,每字节都有一个独立的物理地址。它是物理内存中实际对应的地址,在hello的运行中,在访问内存时需要通过CPU产生虚拟地址,然后通过地址翻译得到一个物理地址,并通过物理地址访问内存中的位置。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由段选择符和偏移量组成,线性地址为段首地址与逻辑地址中的偏移量组成。其中,段首地址存放在段描述符中。而段描述符存放在描述符表中,也就是GDT(全局描述符表)或LDT(局部描述符表)中。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(虚拟地址)由虚拟页号VPN和虚拟页偏移VPO组成。首先,MMU从线性地址中抽取出VPN,并且检查TLB,看他是否因为前面某个内存引用缓存了PTE的一个副本。TLB从VPN中抽取出TLB索引和TLB标记,查找对应组中是否有匹配的条目。若命中,将缓存的PPN返回给MMU。若不命中,MMU需从页表中的PTE中取出PPN,若得到的PTE无效或标记不匹配,就产生缺页,内核需调入所需页面,重新运行加载指令,若有效,则取出PPN。最后将线性地址中的VPO与PPN连接起来就得到了对应的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
虚拟地址VA虚拟页号VPN和虚拟页偏移VPO组成。若TLB不命中时,VPN被划分为四个片,每个片被用作到一个页表的偏移量,CR3寄存器包含L1页表的物理地址。VPN1提供到一个L1 PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2 PTE的偏移量,依次类推。最后在L4页表中对应的PTE中取出PPN,与VPO连接,形成物理地址PA。
7.5 三级Cache支持下的物理内存访问
MMU将物理地址发给L1缓存,缓存从物理地址中取出缓存偏移CO、缓存组索引CI以及缓存标记CT。若缓存中CI所指示的组有标记与CT匹配的条目且有效位为1,则检测到一个命中,读出在偏移量CO处的数据字节,并把它返回给MMU,随后MMU将它传递给CPU。若不命中,则需到低一级Cache(若L3 cache中找不到则到主存)中取出相应的块将其放入当前cache中,重新执行对应指令,访问要找的数据。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给他一个唯一的pid。为了给这个新进程创建虚拟内存,他创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有写时复制。当fork从新进程返回,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要:1)删除已存在的用户区域:删除当前进程虚拟地址的用户部分中的已存在的区域结构。2)映射私有区域:为新程序hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。3) 映射共享区域:如果hello程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。4) 设置程序计数器(PC) :设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
对虚拟内存来说,DRAM缓存不命中称为缺页。如下例所示,CPU引用了VP3中的一个字,而VP3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE3,因为有效位0,所以并未缓存,引发了缺页异常,调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,这里以存放在PP3中的VP4为例。若VP4被修改了,那么内核会将它复制回磁盘。内核会修改VP4的页表条目,反映出VP4以不在主存中。然后,内核从磁盘复制VP3到内存中PP3位置,然后处理程序返回,重新启动导致缺页的指令。这时,VP3已存在主存中,不会在导致缺页,可以正常读取。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
1)隐式空闲链表:空闲块通过头部中的大小字段隐含地连接着。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。
放置策略:首次适配、下一次适配、最佳适配。
首次适配从头开始搜索空闲链表,选择第一个合适的空闲块。下一次适配从上一次查询结束的地方开始。最佳适配检查每个空闲块,选择适合所需请求大小的最小空闲块。
合并策略:立即合并、推迟合并。立即合并就是在每次一个块被释放时,就合并所有的相邻块;推迟合并就是等到某个稍晚的时候再合并空闲块。
2)显式空闲链表:每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。使用双向链表使首次适配的时间减少到空闲块数量的线性时间。
空闲链表中块的排序策略:一种是用后进先出的顺序维护链表,将新释放的块放置在链表的开始处,另一种方法是按照地址顺序来维护链表,链表中每个块的地址都小于它后继的地址。
分离存储:维护多个空闲链表,每个链表中的块有大致相等的大小。将所有可能的块大小分成一些等价类,也叫做大小类。分离存储的方法:简单分离存储和分离适配。
7.10本章小结
本章讨论了存储器地址空间,段式管理、页式管理,TLB与四级页表支持下的VA到PA的变换,三级Cache支持下的物理内存访问,hello进程fork时和execve时的内存映射,缺页故障与缺页中断处理和动态存储分配管理。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
1.打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux shell创建的每个进程开始时都有三个打开的文件:标准输入、标准输出和标准错误。
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.关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
函数:
1.int open(char *filename, int flags, mode_t mode)
进程通过调用open函数来打开一个已存在的文件或者创建一个新文件。open函数将filename转换为一个文件描述符,而且返回描述符数字。flags参数指明了进程打算如何访问这个文件。mode参数指定了新文件的访问权限位。
2.int close(int fd)
进程通过调用close函数关闭一个打开的文件。
3.ssize_t read(int fd, void *buf, size_t n)
应用程序通过调用read函数来执行输入。read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,返回值0表示EOF。否则返回值表示的是实际传送的字节数量。
4.ssize_t write(int fd, const void *buf, size_t n)
应用程序通过调用write函数来执行输出。write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
8.3 printf的实现分析
printf的代码:

  1. int printf(const char *fmt, …)
  2. {
  3.   int i;   
    
  4.   char buf[256];   
    
  5.   va_list arg = (va_list)((char*)(&fmt) + 4);   
    
  6.   i = vsprintf(buf, fmt, arg);   
    
  7.   write(buf, i);   
    
  8.  return i;   
    
  9. }

vsprintf的代码:

  1. int vsprintf(char *buf, const char *fmt, va_list args)
  2. {
  3.  char* p;   
    
  4.  char tmp[256];   
    
  5.  va_list p_next_arg = args;   
    
  6.  for (p=buf;*fmt;fmt++) {   
    
  7.  if (*fmt != '%') {   
    
  8.  *p++ = *fmt;   
    
  9. continue;   
    
  10. }   
    
  11. fmt++;   
    
  12. switch (*fmt) {   
    
  13. case 'x':   
    
  14. itoa(tmp, *((int*)p_next_arg));   
    
  15. strcpy(p, tmp);   
    
  16. p_next_arg += 4;   
    
  17. p += strlen(tmp);   
    
  18. break;   
    
  19. case 's':   
    
  20. break;   
    
  21. default:   
    
  22. break;   
    
  23. }   
    
  24. }   
    
  25. return (p - buf);   
    
  26. }

write的代码:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL

sys_cal:

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 

syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
1.int getchar(void)
2. {
3. static char buf[BUFSIZ];
4. static char *bb = buf;
5. static int n = 0;
6. if(n == 0)
7. {
8. n = read(0, buf, BUFSIZ);
9. bb = buf;
10. }
11. return(–n >= 0)?(unsigned char) *bb++ : EOF;
12. }

getchar函数调用read函数,将整个缓冲区都读到buf里,并将缓冲区的长度赋值给n。返回时返回buf的第一个元素,除非n<0。异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章简述了Linux的I/O设备管理机制,Unix I/O接口及函数,并简要分析了printf函数和getchar函数的实现。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
1)GCC编译器驱动程序读取源程序文件hello.c。
2)预处理器cpp将其预处理为一个修改了的源程序hello.i(如读取并插入头文件内容等)。
3)编译器ccl将其翻译成汇编语言程序hello.s。
4)汇编器as将其翻译成机器语言指令,得到可执行目标文件hello.o。
5)链接器ld将重定位目标文件链接为可执行目标文件hello。
6)在shell中输入运行hello的指令,shell通过fork为其创建新的进程。
7)通过execve将hello程序加载并运行。把它映射到对应虚拟内存区域,并依需求载入物理内存。
8)在CPU的帮助下,它的指令被一步步执行,实现它拥有的功能。
9)在程序运行结束后,父进程会对其进行回收,内核把它从系统中清除。
这样,hello便完成了它的程序人生。
计算机系统的设计复杂而严密,对内存,CPU等各个实现都有着精密的处理设计,以涵盖在系统运行时可能遇到的各种情况。计算机系统的运行需要内存,CPU,信号等机制的密切配合,来实现在系统上正确而又高效地运行程序。
(结论0分,缺失 -1分,根据内容酌情加分)

附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.i 预处理后修改了的源程序
hello.s 汇编生成的hello的汇编程序
hello.o 编译生成的hello的可重定位目标程序
hello 链接生成的hello的可执行目标程序
asm.txt hello.o的反汇编文件
(附件0分,缺失 -1分)

参考文献
[1] 动态链接原理分析 https://blog.csdn.net/shenhuxi_yu/article/details/71437167
[2] printf 函数实现的深入剖析https://www.cnblogs.com/pianist/p/3315801.html
[3] getchar函数浅谈 https://blog.csdn.net/zhuangyongkang/article/details/38943863
[4]https://baike.baidu.com/item/逻辑地址/3283849?fr=aladdin
[5]https://baike.baidu.com/item/线性地址
[6]https://baike.baidu.com/item/虚拟地址
[7]https://www.cnblogs.com/huangwentian/p/7487670.html
[8]https://blog.csdn.net/youyou519/article/details/82659007
为完成本次大作业你翻阅的书籍与网站等
(参考文献0分,缺失 -1分)

标签:文件,06,函数,程序,29,地址,2021,进程,hello
来源: https://blog.csdn.net/ACSSSSSSS/article/details/118346136

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有