目录

摘要

第1章 概述

1.1 Hello简介

1.1.1 P2P:From Program to Process

1.1.2 020:From Zero-0 to Zero-0

1.2 环境与工具

1.2.1 硬件环境

1.2.2 软件环境

1.2.3 开发工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.1.1 概念

2.1.2 作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.1.1 概念

3.1.2 作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.3.1 数据及数组

3.1.2 赋值

3.1.3 类型转换

3.1.4 算术操作

3.1.5 关系操作及控制转移

3.1.6 函数操作

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.1.1 概念

4.1.2 作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.1.1 概念

5.1.2 作用

5.2 在Ubuntu下链接的命令

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

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

6.1.1 概念

6.1.2 作用

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

6.2.1 作用

6.2.2 处理流程

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 hello的存储管理

7.1 hello的存储器地址空间

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

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

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

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

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

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

7.9动态存储分配管理

7.10本章小结

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.2.1 Unix I/O接口

8.2.2 Unix I/O函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献

摘要

本文围绕hello的“程序人生”,对hello“From Program to process”和“From Zero-0 to Zero-0”两个过程展开分析,hello.c从预处理、编译、汇编、链接、fork一系列操作从程序变成进程,再通过调用execve函数加载进程、对控制流的管理、内存空间的分配、异常的处理、对I/O设备的调用、shell父进程回收等一系列操作从无到有再到无。回顾hello的一生,便是一次深入理解计算机系统的历程。

关键词:程序;进程;存储;I/O;程序人生

第1章 概述

1.1 Hello简介

1.1.1 P2P:From Program to Process

在Unix系统上,GCC编译器驱动程序读取源程序文件hello.c,并把它翻译成一个可执行目标文件hello,这个过程可分为四个阶段——预处理器(cpp)将源程序hello.c修改成hello.i文本文件,编译器(ccl)将hello.i翻译成汇编程序hello.s,汇编器(as)将hello.s翻译成机器语言指令,并将这些指令打包成可重定位目标程序hello.o,连接器(ld)将hello.o与库函数相链接生成可执行目标程序hello。执行该程序时,操作系统调用fork创建一个子进程,此时hello.c就从program变成了一个process。

1.1.2 020:From Zero-0 to Zero-0

execve函数加载并运行可执行目标文件hello,操作系统为其分配虚拟内存空间,在物理内存与虚拟内存之间建立映射。执行过程中,虚拟内存为进程提供独立的空间,数据从磁盘传输到CPU中,TLB、分级页表等保障了数据的高效访问,I/O管理与信号处理共同实现了hello的输入输出。程序运行结束后,shell父进程负责回收hello进程,对应的虚拟空间以及相关数据结构被释放,hello进程便经历了从无到有再到无的过程。

1.2 环境与工具

1.2.1 硬件环境

X64 CPU;2GHz;2G RAM;256GHD Disk 以上

1.2.2 软件环境

Windows10 64位以上;VirtualBox 11以上;Ubuntu 16.04 LTS 64位

1.2.3 开发工具

Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc

1.3 中间结果

hello.c:源程序文件

hello.i:hello.c预处理后的源程序文件

hello.s:hello.i编译后的汇编程序

hello.o:hello.s汇编后的可重定位目标文件

elf.txt:hello.o的ELF文件

hello_o2s.s:hello.o的反汇编代码

hello:hello.o链接后的可执行目标文件

hello_elf.txt:hello的ELF文件

hello_objdump.s:hello的反汇编代码

1.4 本章小结

本章简述了hello的“一生”,即P2P和020,并简单列出此次大作业所需的环境和工具,以及hello.c所生成的中间结果文件。

第2章 预处理

2.1 预处理的概念与作用

2.1.1 概念

在C语言中,预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。

2.1.2 作用

1.宏替换:将宏名替换为字符串或数值;

2.文件包含:预处理器读取头文件中的内容,并直接插入到程序文本中;

3.条件编译:根据条件编译指令决定需要编译的代码;

4.删除注释。

2.2在Ubuntu下预处理的命令

图1-在Ubuntu下的预处理过程

2.3 Hello的预处理结果解析

打开预处理后的hello.i文件,发现由原来的23行扩展到3060行。其中头文件中的内容被直接插入到文本文件中,出现大量声明函数、定义结构体、定义变量、定义宏等内容,.c文件中的注释也被删除,但main函数没有改变。

图2-hello.i文件部分代码行截图

2.4 本章小结

本章介绍了预处理的概念和作用,并在Ubuntu中进行预处理,并分析了预处理前后源文件的差别。

第3章 编译

3.1 编译的概念与作用

3.1.1 概念

编译器(ccl)将文本文件hello.c翻译成文本文件hello.s,它包含一个汇编语言程序。

3.1.2 作用

通过以下五个阶段把人们熟悉的高级语言语言换成计算机能解读、运行的低级语言——词法分析,语法分析,语义检查和中间代码生成,代码优化,目标代码生成。

1.词法分析:对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。

2.语法分析:以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最后看是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构,程序是最终的一个语法单位。

3. 语义检查和中间代码生成:由语义分析器完成,指示判断是否合法,并不判断对错。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码。

4.代码优化:对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。

5. 目标代码生成:把语法分析后或优化后的中间代码变换成目标代码。

3.2 在Ubuntu下编译的命令

图3-在Ubuntu下的编译过程

3.3 Hello的编译结果解析

3.3.1 数据及数组

在hello.i的main函数中,常量包括printf函数中打印的两个字符串常量和if条件、for循环里的数字常量。其中,如图4所示,字符串常量被存储在./rotate段,如图5所示,数字常量被存储在.text段中,且作为立即数出现。

图4-hello.s字符串常量相关代码段

图5-hello.i(左)与hello.s数字常量对比

代码中并没有全局变量,局部变量分别是函数参数argc和argv,以及for循环中的i;其中argc表示参数argv的个数,分析图6、图7可知,argc、数组argv以及i均存储在栈中,argc地址为-20(%rbp); argv首地址为-32 (%rbp),每个参数加8(对应图7绿色方框),即argv[k]=-(32+8k)(%rbp),所以数组取值的操作离不开首地址偏移量;i地址为-4(%rbp)。

图6-局部变量argc

图7- hello.i(左)与hello.s部分代码对照

3.1.2 赋值

如图7左上及右边红色方框所示,左边的hello.i中,需对i进行赋值,即将0赋给i,在汇编代码中对应第一行“movl $0, -4(%rbp)”。

3.1.3 类型转换

如图7蓝色方框所示,hello.i中类型转换体现在“atoi(argv[3])”上,即将字符串参数转换为整型参数,需调用atoi函数,在汇编代码中对应为“call atoi”。

3.1.4 算术操作

如图7右边红色方框,for循环中每次循环i都要自加1,在汇编代码中则由“addl $7, -4(&rbp)”实现。

3.1.5 关系操作及控制转移

如图6及图7右边红色方框所示,if条件中需要用到“!=”和“<”这样的关系操作,if-else或者是否跳出for循环则是控制转移,而在汇编代码中,关系操作往往与控制转移(跳转指令)一起出现,即“cmp”后会跟上“jXX”,跳不跳转则需看cmp设置的条件码,或者是直接跳转指令“jmp”,例如图6中,首先将立即数4与 -20(%rbp)对应值(即argc)进行对比,若等于,即“je”就代表相等则跳转至.L2。

3.1.6 函数操作

hello.i中一共涉及5个函数调用,即main、printf、exit、sleep、getchar,在汇编语言中,寄存器一般都有特定用途,例如%rax存储返回值,%rdi存储第一个参数,%rsi存储第二个参数等。其次,若有多个参数,则将参数存储在栈中,如图8所示,调用函数前都有mov指令,设置不同的参数,最典型的就是调用atoi和sleep函数前都将寄存器%rax中的值传送到储存第一个参数的寄存器%rdi中,而像getchar这样没有参数的函数,则无需执行以上操作。

图8-hello.s中函数调用参数传递

再如图9所示,C语言中main函数返回0,那么汇编代码中则对应为“movl $0, %eax”,“leave”,“ret”,即将0传送到%eax,然后使用leave恢复调用者栈帧,清理被调用者栈帧,最后使用ret指令返回。

图9- hello.s中函数返回值

3.4 本章小结

本章围绕编译操作展开,首先介绍了编译的概念和作用,之后在Linux中将hello.i编译成hello.s,并对编译后的hello.s进行分析。

第4章 汇编

4.1 汇编的概念与作用

4.1.1 概念

汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。

4.1.2 作用

将汇编指令翻译成机器可以直接读取分析的机器指令,即二进制,用于后续的链接。

4.2 在Ubuntu下汇编的命令

图10-在Ubuntu下的汇编过程

4.3 可重定位目标elf格式

在Linux 终端中输入“readelf -a hello.o > ./elf.txt”这一命令,查看hello.o的elf文件。

首先是ELF头,如图11所示包括16字节标识信息、文件类型、机器类型、节头表偏移、节头表的表项大小以及表项个数。

图11-ELF头

其次是节头表,节头表是ELF可重定位目标文件中最重要的部分内容,如图12所示,节头表描述每个节的节名、在文件中的偏移、大小、访问属性、对齐方式等。

图12-节头表

重定位节是一个.text节中位置的列表,包含.text节中需要进行重定位的信息,在链接时用于重新修改代码段的指令中的地址信息。如图13所示,需要重定位的信息有puts,exit,printf,atoi,sleep,getchar及.rodata中的模式串。

图13-重定位节

.symtab存放在程序中定义和引用的函数和全局变量(符号表)信息,如图14所示,包括Value、Size、Type、Bind、Vis、Ndx、Name等信息。

图14-符号表

4.4 Hello.o的结果解析

在Linux 终端中输入“objdump -d -r hello.o > hello_o2s.s”这一命令,查看hello.o的反汇编文件并分析。

首先如图15所示,hello.s中的操作数用十进制表示,而 hello.o的反汇编代码中的操作数用十六进制表示。

图15-汇编语言(左)与机器语言操作数的区别

如图16所示,hello.s在跳转时,跳转指令(je)跟的是段名(.L2),而hello.o的反汇编代码则使用的是十六进制相对地址。

图16-汇编语言(左)与机器语言分支转移的区别

如图16所示,hello.s在函数调用时,函数调用指令(call)跟的是函数名(puts),而hello.o的反汇编代码则使用的是十六进制相对地址。

图17-汇编语言(左)与机器语言函数调用的区别

汇编语言与机器语言组织形式不同,hello.s开头包含常量、变量的描述,而hello.o的反汇编代码则在每条指令前还有相对应的机器码的十六进制表示,且没有变量和常量的描述。其余两者基本一致,每条指令都是一一对应关系。

4.5 本章小结

本章围绕编译操作展开,首先介绍了汇编的概念和作用,之后在Linux中将hello.s汇编成hello.o,查看并分析了hello.o的elf文件以及对比hello.s与hello.o的反汇编代码的区别及映射关系。

第5章 链接

5.1 链接的概念与作用

5.1.1 概念

链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被到内存并执行。

5.1.2 作用

链接器使得分离编译成为可能。不用将一个大型的应用程序组织为巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。

5.2 在Ubuntu下链接的命令

图18-在Ubuntu下的链接过程

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

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

在Linux 终端中输入“readelf -a hello > hello_elf.txt”这一命令,查看hello的elf文件。

首先是ELF头,如图19所示,与hello.o的ELF头相似,不做过多描述。

图19-hello的ELF头

如图20所示,节头表是描述目标文件的节,包含了26个节的名字、类型、地址、偏移量等信息,比hello.o多出了13个节。

图20-hello的节头

如图21所示,程序头表是一个结构数组,反映可执行文件的连续的片被映射到连续的内存段的内存段分的映射关系。

图21-hello的程序头

如图22所示的节段映射,说明了在链接过程中,将多个代码段与数据段分别合并成一个单独的代码段和数据段,并根据段的大小以及偏移量重新设置各个符号的地址。

图22-hello的节段映射

在hello的ELF格式后面,还有动态节,重定位节,符号表,版本信息等内容,这里就不一一列出。

5.4 hello的虚拟地址空间

在Linux下使用edb加载hello,查看hello进程的虚拟地址空间各段信息。例如在ELF格式中,如图23显示.rodata段的地址为402000,那么在edb的Data Dump中找到地址0x402000并查看,如图24所示,可以看到printf里面的内容“Hello %s %s”,以此类推,还可以看到各个段详细信息。

图23-hello的ELF格式中.rodata的相关信息

图24-edb的Data Dump中.rodata的详细信息

5.5 链接的重定位过程分析

在Linux 终端中输入“objdump -d -r hello > hello_objdump.s”这一命令,查看hello的反汇编文件并分析。

首先,在hello的反汇编文件中,相比只有.text节和main函数的hello.o的反汇编文件,多了更多的节和函数,例如图25所示的.init节和_init函数,同时printf、puts等函数也有了具体的代码,说明在链接过程中,加入了代码中调用的一些库函数。

图25-hello的反汇编文件中.init节和_init函数

其次,如图26所示hello的反汇编文件从0开始的相对地址在hello.o的反汇编文件中变成了从0x40100虚拟地址。

图26- hello.o(左)与hello的反汇编文件地址区别

最后,如图27所示,hello的反汇编文件在跳转、函数调用时没有重定位条目,而是直接使用虚拟地址,说明重定位的一个过程是链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。

图27- hello.o(左)与hello的反汇编文件重定位条目区别

5.6 hello的执行流程

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。其调用与跳转的各个子程序名如下表。

序号

子程序名

1

ld-2.31.so!_dl_start

2

ld-2.31.so!_dl_init

3

hello!_start

4

hello!__libc_csu_init

5

hello!_init

6

hello!main

7

hello!_dl_relocate_static_pie

8

hello!puts@plt

9

hello!exit@plt

10

hello!printf@plt

11

hello!sleep@plt

12

hello!getchar@plt

13

hello!__libc_csu_fini

5.7 Hello的动态链接分析

在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它;为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。其中PLT是一个数组,其中每个条目是16字节代码。PLT [0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT [0]和GOT [1]包含动态链接器在解析函数地址时会使用的信息。GOT [2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。

通过elf文件中的描述找到.got.plt所在的内存位置,观察其值的变化,如图28、29所示。在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问跳转时GOT地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。

图28-dl_init前的.got.plt内容

图29-dl_init后的.got.plt内容

5.8 本章小结

本章围绕链接操作展开,首先介绍了链接的概念和作用,之后在Linux中将hello.o链接得到hello,查看并分析了hello的elf elf格式、虚拟地址空间,并以该程序为例,解析了链接的重定位过程、执行流程、动态连接过程。

第6章 hello进程管理

6.1 进程的概念与作用

6.1.1 概念

进程指程序的一次运行过程。更确切的说,进程是具有独立功能的一个程序关于某个数据集合的一次运行活动,因而进程具有动态含义。同一个程序处理不同的数据就是不同的进程。进程是OS对CPU执行的程序的运行过程的一种抽象。进程有自己的生命周期,它由于任务的启动而创建,随着任务的完成(或终止)而消亡,它所占有的资源也随着进程的终止而释放。

6.1.2 作用

进程提供给应用程序的关键抽象——一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器;一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。

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

6.2.1 作用

shell是一个交互型应用级程序,代表用户运行其他程序。shell提供了一个界面,用户通过访问这个界面访问操作系统内核的服务。Bash则是Linux操作系统缺省的shell。

6.2.2 处理流程

1.从终端读入输入的命令;

2.将输入字符串切分获得所有的参数;

3.如果是内置命令则立即执行;

4.否则调用相应的程序执行;

5.shell 应该接受键盘输入信号,并对这些信号进行相应处理。

6.3 Hello的fork进程创建过程

在命令行键入hello运行命令,shell判断该命令是可执行文件,于是调用fork 函数创建一个新的运行的子进程,子进程中,fork返回0;父进程中,返回子进程的PID;新创建的子进程几乎但不完全与父进程相同——

1.子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本;

2.子进程获得与父进程任何打开文件描述符相同的副本;

3.最大区别:子进程有不同于父进程的PID。

6.4 Hello的execve过程

fork函数创建完进程后,调用execve函数到目标路径中寻找hello文件,并创建一个内存映像,为该程序的栈区域创建新的区域结构将可执行文件的片复制到代码段和数据段等。然后为共享库建立映射空间。最后设置当前进程上下文的程序计数器,将其指向入口函数,并将控制传递给新程序的主函数。

6.5 Hello的进程执行

控制寄存器中有一个模式位,当设置了模式位时,进程就运行在内核模式中,此时可以执行指令集中的任何模式,并且可以访问系统中的任何内存位置;没有设置模式位时,进程就运行在用户模式中,不允许执行如停止处理器、改变模式位或发起一个I/O操作等特权指令。

运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法时通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把内核模式改回到用户模式。

内核为每个进程维持一个上下文。上下文就是内核重新启动一个被抢占了的进程。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。

在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换——①保存当前进程的上下文,②恢复某个先前被抢占的进程被保存的上下文,③将控制传递给这个新恢复的进程。

在hello进程中,调用sleep函数或者getchar函数都需要进行上下文切换,由用户模式切换到系统模式。

6.6 hello的异常与信号处理

在Linux终端运行hello并键盘输入ctrl+C(发送一个SIGINT信号给hello进程,使hello终止)、ctrl+Z(发送一个SIGTSTP信号给前台程序,即hello,使其被挂起)、ps(列出当前所有进程)、jobs(显示当前暂停的进程)、pstree(查看进程树)、fg(使hello任务在前台继续进行)、kill(发送一个SIGINT信号给hello进程,使hello终止)指令并查看对应结果,结果如图30-32所示。

图30-正常运行后键盘输入ctrl+C

图31-正常运行后键盘输入ctrl+Z、ps、jobs、pstree

图32-键盘输入ctrl+Z后输入fg、kill

hello运行过程中可能出现的异常及其处理方式——

1.中断:来自I/O设备的信号,异步发生,总是返回到下一条指令。在当前指令完成执行之后,处理器注意到中断引脚的电压变高了,就从系统总线读取异常号,然后调用适当的中断处理程序。当处理程序返回时,它就将控制返回给下一条指令(也即如果没有发生中断,在控制流中会在当前指令之后的那条指令)。结果是程序继续执行,就好像没有发生过中断一样。

2.陷阱:陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。

3.故障:故障由错误情况引起,它可能能够被故障处理程序修复,同步发生。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的 abort 例程,abort 例程会终止引起故障的应用程序。

4.终止:不可恢复的错误造成的结果,同步发生,不会返回。处理程序将控制返回给一个 abort 例程,该例程会终止这个应用程序。

6.7本章小结

本章围绕hello的进程管理展开,分别对进程、shell-bash、fork、execve、进程执行和异常与信号处理的机制这些进程相关操作进行详细说明。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:逻辑地址指由程序产生的与段相关的偏移地址部分,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。就是hello.o里面的相对偏移地址。

线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。

虚拟地址: CPU启动保护模式后,程序运行在虚拟地址空间中。hello里就是的虚拟内存地址。

物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。hello在运行时虚拟内存地址对应的物理地址。

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

一个逻辑地址由段标识符和段内偏移量两部分组成。其中段标识符是一个16位长的字段,称为段选择符,前13位是一个索引号,后面3位包含一些硬件细节。

索引号,类似数组下标,它对应的“数组”就是段描述符表,段描述符具体描述了一个段地址,多个段描述符组成段描述符表。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。其中Base字段描述了一个段的开始位置的线性地址。

全局的段描述符放在全局段描述符表(GDT)中,局部的段描述符放在局部段描述符表(LDT)中。GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。

给定一个完整的逻辑地址[段选择符:段内偏移地址],段选择符T1等于0或1,分别转换到GDT或LDT中的段,再根据相应寄存器,得到其地址和大小,就有了一个数组了。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,把base + offset,就是要转换的线性地址。

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

将各进程的虚拟空间划分成若干个长度相等的页,页式管理把内存空间按页的大小划分成片或者页面,然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。

图33展示了MMU如何利用页表来实现从虚拟地址空间到物理地址空间的映射。CPU 中的一个控制寄存器,页表基址寄存器指向当前页表,n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移和一个n-p位的虚拟页号。MMU利用VPN来选择适当的PTE,将页表条目中的物理页号和虚拟地址中的VPO串联起,就能得到相应的物理地址。

图33-使用页表的地址翻译

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

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

图34-使用k(4)级页表的地址翻译

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

图35描述了三级Cache支持下的物理内存访问过程。MMU将虚拟地址转化为物理地址后,该地址被分为标记位(CT)、组号(CI)和偏移量(CO),然后根据CI在L1中寻找对应的组,查看该组中的对应的行是否有效,以及CT是否相等,若相等则取出偏移量为CO的数据,即命中。若不命中,依次去L2、L3和主存中寻找该数据,若找到,则将该数据所在块更新至各级cache。

图35-Core i7地址翻译概况

7.6 hello进程fork时的内存映射

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

图36-一个私有的写时复制对象

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve 函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下4个步骤:

1.删除已存在的用户区域;

2.映射私有区域;

3.映射共享区域;

4.设置程序计数器(PC);

下一次调度这个进程时,它将从这个入口点开始执行。Linux 将根据需要换入代码和数据页面。

图37展示加载器是如何映射用户地址空间的区域的。

图37-加载器如何映射用户地址空间的区域

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

当指令引用一个虚拟地址,而与该地址相对应的物理页面不在内存中,因此必须从磁盘中取出时,就会发生缺页故障。缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令。当指令再次执行时,相应的物理页面以及驻留在内存中了,指令就可以没有故障地运行完成了。图38概述了一个故障的处理。

图38-故障处理

7.9动态存储分配管理

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

分配器有两种基本风格——

1.显式分配器:要求应用显示地释放任何已分配的块。例如C标准库提供一种叫做malloc程序包的显示分配器。在hello中,printf会调用malloc。

图39展示了一个malloc和free的实现是如何管理一个C程序的16字节的小堆。每个方框代表一个4字节的字有阴影为已分配块,无阴影为空闲块。初始时,堆是由一个大小为16个字的、双字对齐的空闲块组成的。需要指出的是p2中程序申请一个5字节的块,malloc的响应是从空闲块的前部分配一个6字的块,那个额外的字是为了保持空闲块是双字边界对齐的;p4中程序申请一个2字节的块,malloc分配在前一步中被释放了的块的一部分。

图39-用malloc和free分配和释放块

2.隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。

7.10本章小结

本章围绕hello的存储管理展开,介绍了hello的存储器地址空间,Intel的段式管理和页式管理,以及四级页表支持下的虚拟地址到物理地址的变换和三级cache支持下的物理内存访问。并从内存映射的视角介绍了hello进程的fork和execve,最后介绍了缺页故障及缺页中断处理和动态内存分配。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。

8.2 简述Unix IO接口及其函数

8.2.1 Unix I/O接口

打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。

改变当前的文件位置:对每个打开的文件,内核保持着一个文件位置k,初始位置为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能通过seek操作,显示地设置文件的当前位置为k。

读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会触发一个称为EOF的条件,应用程序能检测到这个条件。类似的。写操作就是从内存复制n>0个字节到从内存复制n个字节文件,从当前文件位置k开始,然后更新k。

关闭文件:当应用完成对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

8.2.2 Unix I/O函数

int open(char *filename, int flags, mode_t mode)

进程是通过调用 open 函数来打开一个已存在的文件或者创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,若出错则返回-1。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件;mode 参数指定了新文件的访问权限位。

int fseek(FILE *stream, long offset, int fromwhere)

通过调用fseek函数,应用程序能够显式地修改当前文件的位置。stream为指向打开的文件指针,offset是以基准点为起始点的偏移量,fromwhere为基准点。成功则返回0,失败则返回-1。

ssize_t read(int fd, void *buf, size_t n)

应用程序是通过调用read函数来执行输入的。read 函数从描述符为 fd 的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示 EOF。否则,返回值表示的是实际传送的字节数量。

ssize_t write(int fd, const void *buf, size_t n)

应用程序是通过调用write函数来执行输出的。write 函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。

int close(int fd)

进程通过调用close函数关闭一个打开的文件。若成功则返回0,否则返回-1。

8.3 printf的实现分析

图40给出了printf函数的函数体,形参列表中的省略号表示可变形参,va_list是一个字符指针, “(char*)(&fmt) + 4)”表示的是可变形参中的第一个参数,vsprintf返回所打印字符串的长度,作用是格式化,接受确定输出格式的格式字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出。而后调用write系统函数将buf中的i个字符输出到显示器上,在write函数执行时,陷阱-系统调用 int 0x80或syscall将字符串的ASCII码从寄存器复制到显卡的显存中,由字符显示驱动子程序通过ASCII码到字模库中找到点阵信息存储到vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

图40-printf函数的函数体

8.4 getchar的实现分析

C 库函数int getchar(void)从标准输入stdin获取一个字符(一个无符号字符)。该函数以无符号 char 强制转换为 int 的形式返回读取的字符,如果到达文件末尾或发生读错误,则返回 EOF。getchar调用read系统函数,通过系统调用读取按键ASCII码,直到接受到回车键才返回。调用getchar函数需要对异步异常-键盘中断进行处理——键盘中断处理子程序。接受按键扫描码转成ASCII码,保存到系统的键盘缓冲区。

8.5本章小结

本章介绍了hello的I/O设备管理方法,并简单叙述了Unix IO接口以及相应的函数,然后对printf和getchar的实现进行了分析。

结论

回顾hello的“一生”,从程序员用高级语言编写代码开始,hello便横空出世,它需要经历P2P、020,此时的hello知识一个名叫hello.c的源程序文件,此后预处理器(cpp)将源程序hello.c修改成hello.i文本文件,编译器(ccl)将hello.i翻译成汇编程序hello.s,汇编器(as)将hello.s翻译成机器语言指令,并将这些指令打包成可重定位目标程序hello.o,连接器(ld)将hello.o与库函数相链接生成可执行目标程序hello,在shell中输入命令,hello就开始了他作为进程的后半生,首先shell调用fork创建一个子进程,再调用execve函数创建一个内存映像。CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流,MMU将程序中使用的虚拟内存地址通过页表映射成物理地址,printf会调用malloc向动态内存分配器申请堆中的内存,如果运行途中键入ctrl-C、ctrl-Z则调用信号处理函数分别停止、挂起,最后,shell回收子进程,内核删除为hello创建的所有数据结构,hello也算是正式走完了P2P、020的“程序人生”。

附件

hello.c:源程序文件

hello.i:hello.c预处理后的源程序文件

hello.s:hello.i编译后的汇编程序

hello.o:hello.s汇编后的可重定位目标文件

elf.txt:hello.o的ELF文件

hello_o2s.s:hello.o的反汇编代码

hello:hello.o链接后的可执行目标文件

hello_elf.txt:hello的ELF文件

hello_objdump.s:hello的反汇编代码

参考文献

[1]  兰德尔 E. 布莱恩特,大卫 R. 奥哈拉伦. 深入理解计算机系统(第3版)[M]. 北京:机械工业出版社. 2016.7.

[2]  Pianistx. printf函数实现的深入剖析. 博客园. 2013.9. [转]printf 函数实现的深入剖析 - Pianistx - 博客园 (cnblogs.com)

注:此外还参考了CSDN和百度百科上的许多文章,无法一一列举。

哈尔滨工业大学计算机系统大作业——程序人生-Hello’s P2P相关推荐

  1. 哈尔滨工业大学计算机系统大作业-程序人生 Hello‘s P2P

    文章目录 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的作用及概念 2.2 在Ubuntu下预处理的命令 2.3 Hel ...

  2. 哈尔滨工业大学计算机系统大作业 程序人生-Hello’s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 计算机科学与技术学院 2021年5月 摘  要 本文以hello程序从hello.c到进程各个阶段所 ...

  3. 哈尔滨工业大学计算机系统大作业--程序人生

    计算机系统   大作业 题     目  程序人生-Hello's P2P      专       业   计算机科学与技术        学    号        2021110xxx      ...

  4. HIT 深入理解计算机系统 大作业 程序人生-Hello’s P2P

    HIT 深入理解计算机系统 大作业 程序人生-Hello's P2P 本论文旨在研究 hello 在 linux 系统下的整个生命周期.结合 CSAPP 课本, 通过 gcc 等工具进行实验,从而将课 ...

  5. 【2022】哈工大计算机系统大作业——程序人生Hello’s P2P

    2022哈工大计算机系统大作业--程序人生Hello's P2P 摘要 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的概 ...

  6. 哈尔滨工业大学CSAPP大作业程序人生

    计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算学部 学 号 1190202126 班 级 1936602 学 生 李映泽 指 导 教 师 刘宏伟 计算机科学与技术学院 20 ...

  7. 哈工大 计算机系统大作业 程序人生-Hello’s P2P From Program to Process

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学    号 120L020512 班    级 2003004 学       生 黄鹏程 指 导 ...

  8. 哈工大2021春计算机系统大作业 程序人生-Hello’s P2P

          计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机类 学     号 1190200613 班     级 1903004 学       生 ...

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

    计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算机类 学 号 1180300223 班 级 1803002 计算机科学与技术学院 2019年12月 摘 要 本文主要介绍了一个 ...

最新文章

  1. ThinkPHP 框架学习
  2. MASK-RCNN是什么?MASK(掩膜)又是什么?
  3. 安全 - MySQL 出现严重的密码安全漏洞,许多系统存在风险
  4. linux iostat 查看磁盘io利用率
  5. VC控件 Progress Control
  6. ESlint静态代码检测工具安装
  7. 和lua的效率对比测试_Unity游戏开发Lua更新运行时代码!
  8. 典型方法_裴礼文老师编数学分析中的典型问题与方法练习参考答案的说明
  9. unity着色器和屏幕特效开发秘笈_Oculus研发分享:开发移动VR内容时应避免的PC渲染技术...
  10. mysql主从复制同步实验_db.mysql.主从同步实验
  11. 计算机职业规划备选方案,大学生职业生涯规划-备选方案
  12. git的一些简单用法
  13. android网页去广告插件下载,Adblock Plus(去广告插件)APP增强稳定版
  14. Android获取分辨率和像素密度
  15. 交互设计师谈颠覆式创新 | Think different
  16. JavaScript 坦克大战
  17. mysql答辩会问什么_计算机科学与技术专业,毕设答辩会问什么问题?
  18. 撑不下去的时候,请看看这19张照片
  19. discuz x3.1 整站搬家换域名攻略
  20. android程序设计题a,经典Android面试题和答案

热门文章

  1. linux编辑器java_Java编辑器 BlueJ For Linux V3.1.6 免费版 下载-脚本之家
  2. xss.pwnfunction靶场
  3. 40万条用户信息被泄露,企业如何有效防范员工成内鬼?
  4. 与直男癌程序猿男友相处十招必杀技,尤其最后一招
  5. OAuth2 Server php
  6. 深度解析老年产业投资的底层逻辑
  7. ASP.NET CORE 2.0 发布到IIS,IIS如何设置环境变量来区分生产环境和测试环境
  8. LNK2019 无法解析的外部符号 main,函数 “int __cdecl invoke_main(void)“
  9. 云闪付小程序 转 微信小程序 ( vue >> mpvue >> wxApplets ) 记录
  10. 内存泄漏分析工具tMemMonitor (TMM)使用简介