本文在Linux操作系统下对C语言程序hello.c 运行编译的全过程进行了细致的分析。从.c文件到可执行文件执行过程中预处理,编译,汇编和链接,学习Linux框架下整个程序下的声明周期。
关键词:P2P,O2O,程序的声明周期,进程,链接,I/O
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
6.2 简述壳Shell-bash的作用与处理流程 - 10 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -
7.7 hello进程execve时的内存映射 - 11 -
第1章 概述
1.1 Hello简介
P2P过程:即From program to progress,hello程序的生命周期是从一个高级语言程序开始的,gcc编译器驱动程序读取源程序文件hello.c,并把它翻译成一个可执行目标文件hello,经过预处理器(cpp)->编译器(ccl)->汇编器(as)->连接器(ld),生成一个hello的二进制可执行文件。然后shell会新建一个可执行的进程。
O2O过程:即From zero to zero,shell等待输入一个命令行,加载并运行hello这个文件,然后等待程序终止,hello程序在屏幕上输出他的消息,然后终止,shell回收内存空间。
1.2 环境与工具
硬件环境:
X64CPU; 8GHz; 8GRAM; 1TB HD
软件环境:
Windows10 64位;VMware14.12; Ubuntu 16.04 LTS 64位
使用工具:
gcc,gdb,edb,hexedit,objdump
1.3 中间结果
hello.i --- hello.c预处理后产生的程序文本
hello.s --- hello.i编译成汇编程序后的文本
hello.o --- hello.s汇编后的二进制文件,可重定位目标程序
hello --- hello.o链接后的二进制文件,为可执行目标程序
hello.elf --- hello.o的重定位elf格式文件
helloo.elf --- hello的elf格式文件
同时还有hello和hello.o的反汇编文件打印在屏幕上并未形成文档存储。
1.4 本章小结
本章简要介绍了hello的P2P,O2O过程,并列出本次的实验环境,中间结果,文件作用等基础问题。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理阶段:
预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。命令会告诉预处理器读取系统头文件的内容并把他直接插入程序文本中。结果是得到了另一个C程序,通常是以.i作为文件扩展名。
作用:
预处理主要有三个方面的内容,1.宏定义;2.文件包含;3.条件编译。预处理命令以符号“#”开头。
- 宏定义:格式为#define标识符文本,预处理工作也叫做宏展开,将宏名替换为文本。
- 文件包含:格式:#include<文件名>,编译时以包含处理以后的文件为编译单位,被包含的文件是源文件的一部分,修改头文件后所有包含该文件的文件都要重新编译。
- 条件编译:#ifdef 标识符 程序段1 #else 程序段2 #endif,这是其中一个格式。使用条件编译乐意使目标程序变小,运行时间变短。
预编译是问题或算法的解决方案增多,有助于我们选择合适的解决方案。
此外,还有布局控制:#pragma,这也是我们应用预处理的一个重要方面,主要功能是为编译程序提供非常规的控制流信息。
2.2在Ubuntu下预处理的命令
gcc -E -o hello.i hello.c
图2.2.1 预处理指令
在进行完预处理之后,我们可以看到文件夹里出现了新的文件hello.i
图2.2.2 预处理后产生文件
2.3 Hello的预处理结果解析
图2.2.3 hello.c源文件全部代码
hello.c为源文件,大约为28行
而预处理后的hello.i,文件被扩展到3118行。其中无用的注释等信息被忽略掉,原来的头文件也被插入到预编译指令的位置,同时文件中多出了很多类似信息:
图2.2.4 hello.i部分代码1
这是对文件中包含系统头文件的寻址和解析。
图2.2.5 hello.i部分代码2
整个过程是递归进行的,.i文件仍然是C语言可读状态的。
2.4 本章小结
本章节主要简单介绍了C程序在编译前的预处理过程,简要说明了预处理的概念和作用,对预处理过程进行演示并且对hello的预处理结果进行解析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译阶段:编译器将文本文件hello.i翻译成hello.s,它包含一个汇编语言程序,该程序包含函数main的定义。
编译概念:编译是利用编译程序从源语言编写的源程序产生目标程序的过程;用编译程序产生目标程序的动作。编译就是把高级语言变成计算机可以识别的2进制语言,计算机只认识1和0,编译程序把人们熟悉的语言换成2进制的。 编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
作用:
- 词法分析:词法分析的任务是对字符串组成的单词进行处理,从左到右逐个字符的对源程序进行扫描,产生一个个单词符号。执行词法分析的程序成为词法分析程序或扫描器。
- 语法分析:语法分析的方法分为两种:自上而下分析法和自下而上分析法。
- 中间代码:中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码,即为中间语言程序,中间语言的复杂性介于源程序语言和机器语言之间。
- 代码优化:代码优化是指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。所谓等价,是指不改变程序的运行结果。所谓有效,主要指目标代码运行时间较短,以及占用的存储空间较小。这种变换称为优化。
- 目标代码:目标代码生成阶段应考虑直接影响到目标代码速度的三个问题:一是如何生成较短的目标代码;二是如何充分利用计算机中的寄存器,减少目标代码访问存储单元的次数;三是如何充分利用计算机指令系统的特点,以提高目标代码的质量。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
图3.3.1 编译命令
图3.3.2 编译结果
3.3 Hello的编译结果解析
图3.3.1 编译结果(hello.s文件)
3.3.1 数据
变量:
在.s文件中:
3.3.2 赋值
.s文件中编译结果:
此处我们其实可以发现sleepsecs的值已经进行了转换,因为sleepsecs是整形数,而最后它输出的值其实为2.
.s文件中编译结果:
3.3.3 类型转换(显示或隐式)
由于sleepsecs被赋值为2.5(浮点数类型),而本身为int类型的,所以这里出现了隐式的类型转换,这里编译器将其隐式转换为2存入了sleepsecs。
3.3.4 关系操作与控制转移
.s文件中为:
i < 10
.s文件中为:
cmpl为一比较语句,cmpl x,y,如果x-y = 0,连接je语句,则跳转到je后的address,如果不相等,则跳转到address下一条继续执行。
而其他的==,>=, <=, <, > ,如图:
3.3.5 数组/指针/结构的操作
取数组的第i位一般是按照数组头指针加上第i位的偏移量来操作的
指针也类似,所以对指针和数组在.s中的操作为:
3.3.6 函数操作
printf("Usage: Hello 学号 姓名!\n");
在.s文件中:
printf("Hello %s %s\n",argv[1],argv[2]);
在.s文件中:
sleep(sleepsecs);
在.s文件中
3.4 本章小结
本章主要介绍了编译的概念和作用,具体分析了一个.i文件是如何通过编译器被编译成一个汇编程序的过程,并且对C语言中的数据操作等在汇编代码中进行了具体分析。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编阶段:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,在文本编辑器中打开将看到乱码。
概念:把汇编语言翻译成机器语言的过程称为汇编。
作用:由于汇编更接近机器语言,能够直接对硬件进行操作,生成的程序与其他的语言相比具有更高的运行速度,占用更小的内存,因此在一些对于时效性要求很高的程序、许多大型程序的核心模块以及工业控制方面大量应用。
4.2 在Ubuntu下汇编的命令
gcc -c -o hello.o hello.s
图4.2.1 汇编命令
图4.2.2 汇编生成文件
4.3 可重定位目标elf格式
图4.3.1 重定位elf格式命令
ELF Header:用于总的描述ELF文件各信息的段,根据文件头信息可知道该文件是重定位文件,有13个节。
Section Headers:查看头节表,从而得知各节的大小,以及各节进行的操作
Symbol table:符号表相关信息
其中重定位节有.rela.text以及.rela.eh_frame的详细信息。
4.4 Hello.o的结果解析
图4.4.1 hello.o反汇编内容
与hello.s相比,hello.o反汇编之后形成的汇编代码没有太大的差别,但是在左边出现了很多表示机器指令的位置和相对应的机器指令,均用16进制来表示机器指令。
4.4.1分支跳转
跳转语句jx或者jxx原来对应的符号在反汇编代码中都变成了相对偏移地址
4.4.2 函数调用
在hello.s里面,函数调用call后对应的函数名,而在反汇编代码中它被替换成函数的相对偏移地址。
4.5 本章小结
本章简述了hello.s汇编指令被转换为hello.o机器指令的过程,利用readelf查看hello.o的elf,通过反汇编查看了hello.o反汇编的内容,并且比较与hello.s的差别,掌握了从汇编指令到机器指令的映射。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接阶段:链接负责处理将多个文件合并到一个可执行文件中,结果得到hello文件,他是一个可执行目标文件,可以被加载到内存中,由系统执行。
5.2 在Ubuntu下链接的命令
ld /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/5/crtbeginT.o -L/usr/lib/gcc/x86_64-linux-gnu/5 hello.o -lc -lgcc -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -o hello
图5.2.1 链接命令
图5.2.2 链接形成文件
5.3 可执行目标文件hello的格式
图5.3.1 ELF Header ELF头
这里可以看出section headers节头表的数量有29个
图5.3.2 Section Headers 节头
5.4 hello的虚拟地址空间
图5.4.1 hello的虚拟地址空间在edb中的展示
图5.4.2 详细的数据段信息
5.5 链接的重定位过程分析
1.hello反编译之后的汇编代码,与hello.o相比多了.init类似的节
图5.5.1 section .init
- hello反编译代码多了很多外部链接的函数
图5.5.2 section .text部分
- hello.o中跳转地址在hello中变成虚拟内存地址
图5.5.3 虚拟内存地址部分
而重定位是链接器在完成符号解析以后,把代码中的每个符号和引用和正好一个符号定义关联起来。此时已经知道了输入模块的代码节和数据节的确切大小就可以开始重定位步骤了。
5.6 hello的执行流程
1. _dl_start
2. _dl_init
3. _start
4. _libc_start_main
5. _init
6. _main
7. _printf
8. _exit
9. _sleep
10. _getchar
11. _dl_runtime_resolve_xsave
12. _dl_fixup
13. _dl_lookup_symbol_x
14. exit
Exit 退出
5.7 Hello的动态链接分析
在edb调试前会发现原本的global_offset表为全为0的状态,在调用_dl_init之后被赋上相应偏移量的值,所以说明init是给程序赋上当前执行的内存地址偏移量,这是初始化hello的第一步。
5.8 本章小结
本章简要总结了在链接过程中对程序的一系列处理过程,经过链接,ELF可重弄定位的目标文件变成可执行的目标文件,链接后程序能够作为进程通过虚拟内存机制直接运行。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
6.2 简述壳Shell-bash的作用与处理流程
Shell 是指一种应用程序,Shell 应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
处理流程:
- 读入输入的命令
- 分析内容获得参数
- 如果是内置命令立刻执行,否则调用相关的程序执行命令
- 读取键盘输入内容并作出相应的反应
6.3 Hello的fork进程创建过程
Shell通过fork函数创建并运行一个新的子进程。在这里就是hello程序。
图6.3.1 fork进程
6.4 Hello的execve过程
在execve运行时,首先是从0x00400000开始程序的执行,先是从可执行文件中加载的内容,然后是运行的对战和共享库的存储器映射区域
6.5 Hello的进程执行
逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
所以回归看hello的进程
对于循环中的这两个语句,最初运行时处于用户模式,在hello调用sleep后进入内核模式,内核处理休眠并进行上下文切换,将当前进程的控制权交给其他进程,当达到定时2.5s时重新进入用户模式。
6.6 hello的异常与信号处理
Ctrl + Z
图6.6.1 ctrl+z操作
这个操作向进程发送一个信号让进程暂时挂起,输入ps后可以发现进程还未关闭
Ctrl + C
图6.6.2 ctrl+c操作
这个操作使程序完全终止
Fg命令:
图6.6.3 fg命令
这个命令让挂起的程序继续运行
Jobs命令:
图6.6.4
这个命令可以查看当前的关键命令(ctrl + z等),当前命令为ctrl+z挂起程序
Pstree命令
图6.6.5 pstree
这是一个进程树,将各个进程连接起来
Kill命令:
kill指令是向固定进程发送某些信号。
图6.6.6 kill命令
运行中途乱按:
图6.6.7 运行中途乱按
在运行中途乱按的字符也会同时打印在屏幕上而不影响原本的输出。
6.7本章小结
本章通过对hello运行过程中执行各种操作,强化认识了与系统链接以及异常控制等问题,同时总结了shell如何fork子进程,execve如何执行进程等相关知识。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
1.逻辑地址:是指由程序产生的与段相关的偏移地址部分
2.线性地址:是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。是一个hello运行块位置的描述。
3.虚拟地址:虚拟地址是Windows程序时运行在386保护模式下,这样程序访问存储器所使用的逻辑地址称为虚拟地址,与实地址模式下的分段地址类似,虚拟地址也可以写为“段:偏移量”的形式,这里的段是指段选择器。
4.物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理是指把一个程序分成若干个段进行存储,每个段都是一个逻辑实体。它的产生是与程序的模块化直接有关的。段式管理是通过段表进行的,它包括段号或段名、段起点、装入位、段的长度等。此外还需要主存占用区域表、主存可用区域表。
而逻辑地址由两部分组成,段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符,其中前13位是一个索引号,后面三位包含硬件细节。
具体变换方式:
1.给定一个完整的逻辑地址。
2.看段选择符T1,知道要转换的是GDT中的段还是LDT中的段,通过寄存器得到地址和大小。
3.取段选择符中的13位,再数组中查找对应的段描述符,得到BASE,就是基地址。
4.线性地址等于基地址加偏移。
7.3 Hello的线性地址到物理地址的变换-页式管理
分页管理机制通过上述页目录表和页表实现32位线性地址到32位物理地址的转换。控制寄存器CR3的高20位作为页目录表所在物理页的页码。首先把线性地址的最高10位(即位22至位31)作为页目录表的索引,对应表项所包含的页码指定页表;然后,再把线性地址的中间10位(即位12至位21)作为所指定的页目录表中的页表项的索引,对应表项所包含的页码指定物理地址空间中的一页;最后,把所指定的物理页的页码作为高20位,把线性地址的低12位不加改变地作为32位物理地址的低12位。
为了避免在每次存储器访问时都要访问内存中的页表,以便提高访问内存的速度,80386处理器的硬件把最近使用的线性—物理地址转换函数存储在处理器内部的页转换高速缓存中。在访问存储器页表之前总是先查阅高速缓存,仅当必须的转换不在高速缓存中时,才访问存储器中的两级页表。页转换高速缓存也称为页转换查找缓存,记为TLB。
在分页机制转换高速缓存中的数据与页表中数据的相关性,不是由80386处理器进行维护的,而必须由操作系统软件保存,也就是说,处理器不知道软件什么时候会修改页表,在一个合理的系统中,页表只能由操作系统修改,操作系统可以直接地在软件修改页表后通过刷新高速缓存来保证相关性。高速缓存的刷新通过装入处理器控制寄存器CR3完成
7.4 TLB与四级页表支持下的VA到PA的变换
首先CPU产生虚拟地址,然后将虚拟地址分为前36位VPN和后12位VPO。其中VPN可分为前32位TLBT和后4位TLBI来作为TLB的标记与索引,从而向TLB进行匹配。如果TLB命中,则直接从TLB中取出PPN。如果不命中,则由CR获得一级页表地址,然后根据VPN找到下一级页表,直到第四级页表,最终得到PPN。最后将VPO作为PPO然后与PPN结合,得出物理地址
7.5 三级Cache支持下的物理内存访问
首先获得物理地址VA,然后将前40位PPN作为CT,后12位PPO作为6位CI和6位CO,然后以CI和CT作为索引和标记与L1 cache进行匹配,若匹配成功且块的有效位为1,则命中,然后根据CO取出数据。若不命中,则向下一级cache中寻找,寻找方式同L1 cache。最后将数据缓存(若有空闲块则直接放置,否则选择一个块驱逐)
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新锦成常见各种数据结构,并分配给他一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。
7.7 hello进程execve时的内存映射
execve在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
3.映射共享区域, hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
在虚拟内存的习惯说法中, DRAM 缓存不命中称为缺页(page fault) 。图7-7 展示了在缺页之前我们的示例页表的状态。CPU 引用了VP 3 中的一个字, VP 3 并未缓存在DRAM 中。地址翻译硬件从内存中读取PTE 3, 从有效位推断出VP 3 未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4 。如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4 的页表条目,反映出VP 4 不再缓存在主存中这一事实。
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格:显式分配器,隐式分配器
显式分配器:要求应用显式的释放任何已分配的块
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
这里的策略指的就是显式的链表的方式分配还是隐式的标签引脚的方式分配还是分离适配,带边界标签的隐式空闲链表分配器允许在常数时间内进行对前面块的合并。这种思想是在每个块的结尾处添加一个脚部,其中脚部就是头部的一个副本。如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。显式空间链表就是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继指针,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。为了分配一个块,必须确定请求的大小类,并且对适当的空闲链表做首次适配,查找一个合适的块。如果找到了一个,那么就(可选地)分割它,并将剩余的部分插入到适当的空闲链表中。如果找不到合适的块,那么就搜索下一个更大的大小类的空闲链表。如此重复,直到找到一个合适的块。如果空闲链表中没有合适的块,那么就向操作系统请求额外的堆内存,从这个新的堆内存中分配出一个块,将剩余部分放置在适当的大小类中。要释放一个块,我们执行合并,并将结果放置到相应的空闲链表中。
7.10本章小结
本章主要介绍了hello的存储器空间地址,intel的段式管理,hello的页式管理,TLB与四级页表支持下的VA到PA的变换,物理内存访问,还有hello进程的fork和execve是的内存映射,却也故障与缺页中断处理,动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。
8.2 简述Unix IO接口及其函数
Unix I/O接口统一操作:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
2.Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
3.改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
4.读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5.关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
函数:
1.int open(char *filename, int flags, mode_t mode);open函数将filename转换为一个文件描述符并返回描述符文字,返回的描述符总是在进程中当前没有打开的最小的描述符,flags参数指明了进程打算如何访问这个文件。
- int close(int fd);进程通过调用close函数关闭打开的文件
- ssize_t read(int fd, void *buf, size_t n);read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf,返回值-1表示一个错误,而返回值0表示EOF,否则返回值表示的是实际传送的字节数量
- ssize_t write(int fd, const void *buf, size_t n);write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
8.3 printf的实现分析
Printf函数:
接受一个fmt的格式然后将匹配到的参数按照此格式输出。
同时printf调用了两个函数,vsprintf和write
Vsprintf函数:
vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
而write函数是将buf中的i个元素写到终端的函数。
相应的这个sys_call函数功能是显示格式化的字符串
所以整个printf的功能实现过程为:
从vsprintf生成显示信息,然后传递到write函数,调用sys_call函数字符显示驱动子程序字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。
显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
所以我们想要打印的字符串就显示在了屏幕上
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了Linux系统的IO设备管理方法,Unix IO接口及其函数,分析了printf和getchar函数。
(第8章1分)
结论
Hello的一生从开始到结束大约,经历了这几章和几天的过程:
- 编写,hello.c的形成
- 预处理,修改原始的.c程序将其扩展到一个hello.i文件中
- 编译,将hello.i翻译成文本文件hello.s
- 汇编,将hello.s翻译成机器语言指令,打包成可重定位目标程序,将结果保存在hello.o中
- 链接,将hello.o和其他调用的函数文件合并到hello文件中
- 运行,在shell中输入./hello 1170300205 王煜彤
- 异常控制,对运行中的hello进行操作
- 创建子进程,调用fork
- 运行程序:调用execve
- 访问内存并进行内存的动态申请
- 最后回收进行的子进程,hello的一生结束了。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello.i --- hello.c预处理后产生的程序文本
hello.s --- hello.i编译成汇编程序后的文本
hello.o --- hello.s汇编后的二进制文件,可重定位目标程序
hello --- hello.o链接后的二进制文件,为可执行目标程序
hello.elf --- hello.o的重定位elf格式文件
helloo.elf --- hello的elf格式文件
同时还有hello和hello.o的反汇编文件打印在屏幕上并未形成文档存储。
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] Randal E.Bryant. David R. O’Hallaron 深入理解计算机系统[M]. 背景:机械工业出版社,2016.
[2] https://baike.baidu.com/item/%E7%BC%96%E8%AF%91/1258343?fr=aladdin
[3] https://baike.baidu.com/item/%E9%A2%84%E5%A4%84%E7%90%86%E5%91%BD%E4%BB%A4/10204389
[4] https://baike.baidu.com/item/%E6%B1%87%E7%BC%96/627224?fr=aladdin
[5] https://baike.baidu.com/item/%E8%BF%9B%E7%A8%8B/382503?fr=aladdin
[6] https://baike.baidu.com/item/%E9%80%BB%E8%BE%91%E5%9C%B0%E5%9D%80/3283849?fr=aladdin
[7] https://baike.baidu.com/item/%E7%BA%BF%E6%80%A7%E5%9C%B0%E5%9D%80/9013682?fr=aladdin
[8] https://baike.baidu.com/item/%E8%99%9A%E6%8B%9F%E5%9C%B0%E5%9D%80/1329947?fr=aladdin
[9] https://baike.baidu.com/item/%E7%89%A9%E7%90%86%E5%9C%B0%E5%9D%80/2901583?fr=aladdin
(参考文献0分,缺失 -1分)
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机类
学 号 1170300205
班 级 1736101
学 生 王煜彤
指 导 教 师 刘宏伟
计算机科学与技术学院
2018年12月
摘 要
本文在Linux操作系统下对C语言程序hello.c 运行编译的全过程进行了细致的分析。从.c文件到可执行文件执行过程中预处理,编译,汇编和链接,学习Linux框架下整个程序下的声明周期。
关键词:P2P,O2O,程序的声明周期,进程,链接,I/O
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
6.2 简述壳Shell-bash的作用与处理流程 - 10 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -
7.7 hello进程execve时的内存映射 - 11 -
第1章 概述
1.1 Hello简介
P2P过程:即From program to progress,hello程序的生命周期是从一个高级语言程序开始的,gcc编译器驱动程序读取源程序文件hello.c,并把它翻译成一个可执行目标文件hello,经过预处理器(cpp)->编译器(ccl)->汇编器(as)->连接器(ld),生成一个hello的二进制可执行文件。然后shell会新建一个可执行的进程。
O2O过程:即From zero to zero,shell等待输入一个命令行,加载并运行hello这个文件,然后等待程序终止,hello程序在屏幕上输出他的消息,然后终止,shell回收内存空间。
1.2 环境与工具
硬件环境:
X64CPU; 8GHz; 8GRAM; 1TB HD
软件环境:
Windows10 64位;VMware14.12; Ubuntu 16.04 LTS 64位
使用工具:
gcc,gdb,edb,hexedit,objdump
1.3 中间结果
hello.i --- hello.c预处理后产生的程序文本
hello.s --- hello.i编译成汇编程序后的文本
hello.o --- hello.s汇编后的二进制文件,可重定位目标程序
hello --- hello.o链接后的二进制文件,为可执行目标程序
hello.elf --- hello.o的重定位elf格式文件
helloo.elf --- hello的elf格式文件
同时还有hello和hello.o的反汇编文件打印在屏幕上并未形成文档存储。
1.4 本章小结
本章简要介绍了hello的P2P,O2O过程,并列出本次的实验环境,中间结果,文件作用等基础问题。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理阶段:
预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。命令会告诉预处理器读取系统头文件的内容并把他直接插入程序文本中。结果是得到了另一个C程序,通常是以.i作为文件扩展名。
作用:
预处理主要有三个方面的内容,1.宏定义;2.文件包含;3.条件编译。预处理命令以符号“#”开头。
- 宏定义:格式为#define标识符文本,预处理工作也叫做宏展开,将宏名替换为文本。
- 文件包含:格式:#include<文件名>,编译时以包含处理以后的文件为编译单位,被包含的文件是源文件的一部分,修改头文件后所有包含该文件的文件都要重新编译。
- 条件编译:#ifdef 标识符 程序段1 #else 程序段2 #endif,这是其中一个格式。使用条件编译乐意使目标程序变小,运行时间变短。
预编译是问题或算法的解决方案增多,有助于我们选择合适的解决方案。
此外,还有布局控制:#pragma,这也是我们应用预处理的一个重要方面,主要功能是为编译程序提供非常规的控制流信息。
2.2在Ubuntu下预处理的命令
gcc -E -o hello.i hello.c
图2.2.1 预处理指令
在进行完预处理之后,我们可以看到文件夹里出现了新的文件hello.i
图2.2.2 预处理后产生文件
2.3 Hello的预处理结果解析
图2.2.3 hello.c源文件全部代码
hello.c为源文件,大约为28行
而预处理后的hello.i,文件被扩展到3118行。其中无用的注释等信息被忽略掉,原来的头文件也被插入到预编译指令的位置,同时文件中多出了很多类似信息:
图2.2.4 hello.i部分代码1
这是对文件中包含系统头文件的寻址和解析。
图2.2.5 hello.i部分代码2
整个过程是递归进行的,.i文件仍然是C语言可读状态的。
2.4 本章小结
本章节主要简单介绍了C程序在编译前的预处理过程,简要说明了预处理的概念和作用,对预处理过程进行演示并且对hello的预处理结果进行解析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译阶段:编译器将文本文件hello.i翻译成hello.s,它包含一个汇编语言程序,该程序包含函数main的定义。
编译概念:编译是利用编译程序从源语言编写的源程序产生目标程序的过程;用编译程序产生目标程序的动作。编译就是把高级语言变成计算机可以识别的2进制语言,计算机只认识1和0,编译程序把人们熟悉的语言换成2进制的。 编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
作用:
- 词法分析:词法分析的任务是对字符串组成的单词进行处理,从左到右逐个字符的对源程序进行扫描,产生一个个单词符号。执行词法分析的程序成为词法分析程序或扫描器。
- 语法分析:语法分析的方法分为两种:自上而下分析法和自下而上分析法。
- 中间代码:中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码,即为中间语言程序,中间语言的复杂性介于源程序语言和机器语言之间。
- 代码优化:代码优化是指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。所谓等价,是指不改变程序的运行结果。所谓有效,主要指目标代码运行时间较短,以及占用的存储空间较小。这种变换称为优化。
- 目标代码:目标代码生成阶段应考虑直接影响到目标代码速度的三个问题:一是如何生成较短的目标代码;二是如何充分利用计算机中的寄存器,减少目标代码访问存储单元的次数;三是如何充分利用计算机指令系统的特点,以提高目标代码的质量。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
图3.3.1 编译命令
图3.3.2 编译结果
3.3 Hello的编译结果解析
图3.3.1 编译结果(hello.s文件)
3.3.1 数据
变量:
在.s文件中:
3.3.2 赋值
.s文件中编译结果:
此处我们其实可以发现sleepsecs的值已经进行了转换,因为sleepsecs是整形数,而最后它输出的值其实为2.
.s文件中编译结果:
3.3.3 类型转换(显示或隐式)
由于sleepsecs被赋值为2.5(浮点数类型),而本身为int类型的,所以这里出现了隐式的类型转换,这里编译器将其隐式转换为2存入了sleepsecs。
3.3.4 关系操作与控制转移
.s文件中为:
i < 10
.s文件中为:
cmpl为一比较语句,cmpl x,y,如果x-y = 0,连接je语句,则跳转到je后的address,如果不相等,则跳转到address下一条继续执行。
而其他的==,>=, <=, <, > ,如图:
3.3.5 数组/指针/结构的操作
取数组的第i位一般是按照数组头指针加上第i位的偏移量来操作的
指针也类似,所以对指针和数组在.s中的操作为:
3.3.6 函数操作
printf("Usage: Hello 学号 姓名!\n");
在.s文件中:
printf("Hello %s %s\n",argv[1],argv[2]);
在.s文件中:
sleep(sleepsecs);
在.s文件中
3.4 本章小结
本章主要介绍了编译的概念和作用,具体分析了一个.i文件是如何通过编译器被编译成一个汇编程序的过程,并且对C语言中的数据操作等在汇编代码中进行了具体分析。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编阶段:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,在文本编辑器中打开将看到乱码。
概念:把汇编语言翻译成机器语言的过程称为汇编。
作用:由于汇编更接近机器语言,能够直接对硬件进行操作,生成的程序与其他的语言相比具有更高的运行速度,占用更小的内存,因此在一些对于时效性要求很高的程序、许多大型程序的核心模块以及工业控制方面大量应用。
4.2 在Ubuntu下汇编的命令
gcc -c -o hello.o hello.s
图4.2.1 汇编命令
图4.2.2 汇编生成文件
4.3 可重定位目标elf格式
图4.3.1 重定位elf格式命令
ELF Header:用于总的描述ELF文件各信息的段,根据文件头信息可知道该文件是重定位文件,有13个节。
Section Headers:查看头节表,从而得知各节的大小,以及各节进行的操作
Symbol table:符号表相关信息
其中重定位节有.rela.text以及.rela.eh_frame的详细信息。
4.4 Hello.o的结果解析
图4.4.1 hello.o反汇编内容
与hello.s相比,hello.o反汇编之后形成的汇编代码没有太大的差别,但是在左边出现了很多表示机器指令的位置和相对应的机器指令,均用16进制来表示机器指令。
4.4.1分支跳转
跳转语句jx或者jxx原来对应的符号在反汇编代码中都变成了相对偏移地址
4.4.2 函数调用
在hello.s里面,函数调用call后对应的函数名,而在反汇编代码中它被替换成函数的相对偏移地址。
4.5 本章小结
本章简述了hello.s汇编指令被转换为hello.o机器指令的过程,利用readelf查看hello.o的elf,通过反汇编查看了hello.o反汇编的内容,并且比较与hello.s的差别,掌握了从汇编指令到机器指令的映射。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接阶段:链接负责处理将多个文件合并到一个可执行文件中,结果得到hello文件,他是一个可执行目标文件,可以被加载到内存中,由系统执行。
5.2 在Ubuntu下链接的命令
ld /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/5/crtbeginT.o -L/usr/lib/gcc/x86_64-linux-gnu/5 hello.o -lc -lgcc -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -o hello
图5.2.1 链接命令
图5.2.2 链接形成文件
5.3 可执行目标文件hello的格式
图5.3.1 ELF Header ELF头
这里可以看出section headers节头表的数量有29个
图5.3.2 Section Headers 节头
5.4 hello的虚拟地址空间
图5.4.1 hello的虚拟地址空间在edb中的展示
图5.4.2 详细的数据段信息
5.5 链接的重定位过程分析
1.hello反编译之后的汇编代码,与hello.o相比多了.init类似的节
图5.5.1 section .init
- hello反编译代码多了很多外部链接的函数
图5.5.2 section .text部分
- hello.o中跳转地址在hello中变成虚拟内存地址
图5.5.3 虚拟内存地址部分
而重定位是链接器在完成符号解析以后,把代码中的每个符号和引用和正好一个符号定义关联起来。此时已经知道了输入模块的代码节和数据节的确切大小就可以开始重定位步骤了。
5.6 hello的执行流程
1. _dl_start
2. _dl_init
3. _start
4. _libc_start_main
5. _init
6. _main
7. _printf
8. _exit
9. _sleep
10. _getchar
11. _dl_runtime_resolve_xsave
12. _dl_fixup
13. _dl_lookup_symbol_x
14. exit
Exit 退出
5.7 Hello的动态链接分析
在edb调试前会发现原本的global_offset表为全为0的状态,在调用_dl_init之后被赋上相应偏移量的值,所以说明init是给程序赋上当前执行的内存地址偏移量,这是初始化hello的第一步。
5.8 本章小结
本章简要总结了在链接过程中对程序的一系列处理过程,经过链接,ELF可重弄定位的目标文件变成可执行的目标文件,链接后程序能够作为进程通过虚拟内存机制直接运行。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
6.2 简述壳Shell-bash的作用与处理流程
Shell 是指一种应用程序,Shell 应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
处理流程:
- 读入输入的命令
- 分析内容获得参数
- 如果是内置命令立刻执行,否则调用相关的程序执行命令
- 读取键盘输入内容并作出相应的反应
6.3 Hello的fork进程创建过程
Shell通过fork函数创建并运行一个新的子进程。在这里就是hello程序。
图6.3.1 fork进程
6.4 Hello的execve过程
在execve运行时,首先是从0x00400000开始程序的执行,先是从可执行文件中加载的内容,然后是运行的对战和共享库的存储器映射区域
6.5 Hello的进程执行
逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
所以回归看hello的进程
对于循环中的这两个语句,最初运行时处于用户模式,在hello调用sleep后进入内核模式,内核处理休眠并进行上下文切换,将当前进程的控制权交给其他进程,当达到定时2.5s时重新进入用户模式。
6.6 hello的异常与信号处理
Ctrl + Z
图6.6.1 ctrl+z操作
这个操作向进程发送一个信号让进程暂时挂起,输入ps后可以发现进程还未关闭
Ctrl + C
图6.6.2 ctrl+c操作
这个操作使程序完全终止
Fg命令:
图6.6.3 fg命令
这个命令让挂起的程序继续运行
Jobs命令:
图6.6.4
这个命令可以查看当前的关键命令(ctrl + z等),当前命令为ctrl+z挂起程序
Pstree命令
图6.6.5 pstree
这是一个进程树,将各个进程连接起来
Kill命令:
kill指令是向固定进程发送某些信号。
图6.6.6 kill命令
运行中途乱按:
图6.6.7 运行中途乱按
在运行中途乱按的字符也会同时打印在屏幕上而不影响原本的输出。
6.7本章小结
本章通过对hello运行过程中执行各种操作,强化认识了与系统链接以及异常控制等问题,同时总结了shell如何fork子进程,execve如何执行进程等相关知识。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
1.逻辑地址:是指由程序产生的与段相关的偏移地址部分
2.线性地址:是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。是一个hello运行块位置的描述。
3.虚拟地址:虚拟地址是Windows程序时运行在386保护模式下,这样程序访问存储器所使用的逻辑地址称为虚拟地址,与实地址模式下的分段地址类似,虚拟地址也可以写为“段:偏移量”的形式,这里的段是指段选择器。
4.物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理是指把一个程序分成若干个段进行存储,每个段都是一个逻辑实体。它的产生是与程序的模块化直接有关的。段式管理是通过段表进行的,它包括段号或段名、段起点、装入位、段的长度等。此外还需要主存占用区域表、主存可用区域表。
而逻辑地址由两部分组成,段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符,其中前13位是一个索引号,后面三位包含硬件细节。
具体变换方式:
1.给定一个完整的逻辑地址。
2.看段选择符T1,知道要转换的是GDT中的段还是LDT中的段,通过寄存器得到地址和大小。
3.取段选择符中的13位,再数组中查找对应的段描述符,得到BASE,就是基地址。
4.线性地址等于基地址加偏移。
7.3 Hello的线性地址到物理地址的变换-页式管理
分页管理机制通过上述页目录表和页表实现32位线性地址到32位物理地址的转换。控制寄存器CR3的高20位作为页目录表所在物理页的页码。首先把线性地址的最高10位(即位22至位31)作为页目录表的索引,对应表项所包含的页码指定页表;然后,再把线性地址的中间10位(即位12至位21)作为所指定的页目录表中的页表项的索引,对应表项所包含的页码指定物理地址空间中的一页;最后,把所指定的物理页的页码作为高20位,把线性地址的低12位不加改变地作为32位物理地址的低12位。
为了避免在每次存储器访问时都要访问内存中的页表,以便提高访问内存的速度,80386处理器的硬件把最近使用的线性—物理地址转换函数存储在处理器内部的页转换高速缓存中。在访问存储器页表之前总是先查阅高速缓存,仅当必须的转换不在高速缓存中时,才访问存储器中的两级页表。页转换高速缓存也称为页转换查找缓存,记为TLB。
在分页机制转换高速缓存中的数据与页表中数据的相关性,不是由80386处理器进行维护的,而必须由操作系统软件保存,也就是说,处理器不知道软件什么时候会修改页表,在一个合理的系统中,页表只能由操作系统修改,操作系统可以直接地在软件修改页表后通过刷新高速缓存来保证相关性。高速缓存的刷新通过装入处理器控制寄存器CR3完成
7.4 TLB与四级页表支持下的VA到PA的变换
首先CPU产生虚拟地址,然后将虚拟地址分为前36位VPN和后12位VPO。其中VPN可分为前32位TLBT和后4位TLBI来作为TLB的标记与索引,从而向TLB进行匹配。如果TLB命中,则直接从TLB中取出PPN。如果不命中,则由CR获得一级页表地址,然后根据VPN找到下一级页表,直到第四级页表,最终得到PPN。最后将VPO作为PPO然后与PPN结合,得出物理地址
7.5 三级Cache支持下的物理内存访问
首先获得物理地址VA,然后将前40位PPN作为CT,后12位PPO作为6位CI和6位CO,然后以CI和CT作为索引和标记与L1 cache进行匹配,若匹配成功且块的有效位为1,则命中,然后根据CO取出数据。若不命中,则向下一级cache中寻找,寻找方式同L1 cache。最后将数据缓存(若有空闲块则直接放置,否则选择一个块驱逐)
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新锦成常见各种数据结构,并分配给他一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。
7.7 hello进程execve时的内存映射
execve在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
3.映射共享区域, hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
在虚拟内存的习惯说法中, DRAM 缓存不命中称为缺页(page fault) 。图7-7 展示了在缺页之前我们的示例页表的状态。CPU 引用了VP 3 中的一个字, VP 3 并未缓存在DRAM 中。地址翻译硬件从内存中读取PTE 3, 从有效位推断出VP 3 未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4 。如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4 的页表条目,反映出VP 4 不再缓存在主存中这一事实。
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格:显式分配器,隐式分配器
显式分配器:要求应用显式的释放任何已分配的块
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
这里的策略指的就是显式的链表的方式分配还是隐式的标签引脚的方式分配还是分离适配,带边界标签的隐式空闲链表分配器允许在常数时间内进行对前面块的合并。这种思想是在每个块的结尾处添加一个脚部,其中脚部就是头部的一个副本。如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。显式空间链表就是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继指针,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。为了分配一个块,必须确定请求的大小类,并且对适当的空闲链表做首次适配,查找一个合适的块。如果找到了一个,那么就(可选地)分割它,并将剩余的部分插入到适当的空闲链表中。如果找不到合适的块,那么就搜索下一个更大的大小类的空闲链表。如此重复,直到找到一个合适的块。如果空闲链表中没有合适的块,那么就向操作系统请求额外的堆内存,从这个新的堆内存中分配出一个块,将剩余部分放置在适当的大小类中。要释放一个块,我们执行合并,并将结果放置到相应的空闲链表中。
7.10本章小结
本章主要介绍了hello的存储器空间地址,intel的段式管理,hello的页式管理,TLB与四级页表支持下的VA到PA的变换,物理内存访问,还有hello进程的fork和execve是的内存映射,却也故障与缺页中断处理,动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。
8.2 简述Unix IO接口及其函数
Unix I/O接口统一操作:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
2.Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
3.改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
4.读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5.关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
函数:
1.int open(char *filename, int flags, mode_t mode);open函数将filename转换为一个文件描述符并返回描述符文字,返回的描述符总是在进程中当前没有打开的最小的描述符,flags参数指明了进程打算如何访问这个文件。
- int close(int fd);进程通过调用close函数关闭打开的文件
- ssize_t read(int fd, void *buf, size_t n);read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf,返回值-1表示一个错误,而返回值0表示EOF,否则返回值表示的是实际传送的字节数量
- ssize_t write(int fd, const void *buf, size_t n);write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
8.3 printf的实现分析
Printf函数:
接受一个fmt的格式然后将匹配到的参数按照此格式输出。
同时printf调用了两个函数,vsprintf和write
Vsprintf函数:
vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
而write函数是将buf中的i个元素写到终端的函数。
相应的这个sys_call函数功能是显示格式化的字符串
所以整个printf的功能实现过程为:
从vsprintf生成显示信息,然后传递到write函数,调用sys_call函数字符显示驱动子程序字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。
显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
所以我们想要打印的字符串就显示在了屏幕上
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了Linux系统的IO设备管理方法,Unix IO接口及其函数,分析了printf和getchar函数。
(第8章1分)
结论
Hello的一生从开始到结束大约,经历了这几章和几天的过程:
- 编写,hello.c的形成
- 预处理,修改原始的.c程序将其扩展到一个hello.i文件中
- 编译,将hello.i翻译成文本文件hello.s
- 汇编,将hello.s翻译成机器语言指令,打包成可重定位目标程序,将结果保存在hello.o中
- 链接,将hello.o和其他调用的函数文件合并到hello文件中
- 运行,在shell中输入./hello 1170300205 王煜彤
- 异常控制,对运行中的hello进行操作
- 创建子进程,调用fork
- 运行程序:调用execve
- 访问内存并进行内存的动态申请
- 最后回收进行的子进程,hello的一生结束了。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello.i --- hello.c预处理后产生的程序文本
hello.s --- hello.i编译成汇编程序后的文本
hello.o --- hello.s汇编后的二进制文件,可重定位目标程序
hello --- hello.o链接后的二进制文件,为可执行目标程序
hello.elf --- hello.o的重定位elf格式文件
helloo.elf --- hello的elf格式文件
同时还有hello和hello.o的反汇编文件打印在屏幕上并未形成文档存储。
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] Randal E.Bryant. David R. O’Hallaron 深入理解计算机系统[M]. 背景:机械工业出版社,2016.
[2] https://baike.baidu.com/item/%E7%BC%96%E8%AF%91/1258343?fr=aladdin
[3] https://baike.baidu.com/item/%E9%A2%84%E5%A4%84%E7%90%86%E5%91%BD%E4%BB%A4/10204389
[4] https://baike.baidu.com/item/%E6%B1%87%E7%BC%96/627224?fr=aladdin
[5] https://baike.baidu.com/item/%E8%BF%9B%E7%A8%8B/382503?fr=aladdin
[6] https://baike.baidu.com/item/%E9%80%BB%E8%BE%91%E5%9C%B0%E5%9D%80/3283849?fr=aladdin
[7] https://baike.baidu.com/item/%E7%BA%BF%E6%80%A7%E5%9C%B0%E5%9D%80/9013682?fr=aladdin
[8] https://baike.baidu.com/item/%E8%99%9A%E6%8B%9F%E5%9C%B0%E5%9D%80/1329947?fr=aladdin
[9] https://baike.baidu.com/item/%E7%89%A9%E7%90%86%E5%9C%B0%E5%9D%80/2901583?fr=aladdin
(参考文献0分,缺失 -1分)