计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 1190201121
班 级 1903006
学 生 周晨宇
指 导 教 师 史先俊
计算机科学与技术学院
2021年6月
“Hello world!”应该是每一个学习写程序的人写出的第一个代码。程序员们大多知到如何写代码,却不一定知到代码在计算机中是如何运行的。本论文将以最简单的helloworld程序为例,阐述程序在计算机中的运行,以帮助进一步理解计算机系统工作原理。
关键词:helloworld;程序人生;计算机系统;
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 5 -
3.2 在Ubuntu下编译的命令............................................................................. - 6 -
4.2 在Ubuntu下汇编的命令............................................................................. - 7 -
5.2 在Ubuntu下链接的命令............................................................................. - 8 -
5.3 可执行目标文件hello的格式.................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 10 -
6.3 Hello的fork进程创建过程..................................................................... - 10 -
6.6 hello的异常与信号处理............................................................................ - 10 -
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 -
8.1 Linux的IO设备管理方法.......................................................................... - 13 -
8.2 简述Unix IO接口及其函数....................................................................... - 13 -
第1章 概述
1.1 Hello简介
P2P,意即 From Program to Process 。program是指程序,也可指编程这一过程。process则是指将这个程序通过一步步的整理,处理,最终产生一个能被机器识别,并运行的可执行文件。
在linux中,hello.c程序要先经过cpp的预处理,这一过程将#定义下的程序进行直接插入的工作,进行最原本的修改后,生成hello.i文件。再经过ccl的编译,将hello.i的内容翻译为汇编语言,并得到hello.s文件。然后通过as的汇编,修改成为可重定位的目标程序hello.o。ld的链接最终成为将库中的.o文件与此文件链接可执行目标程序hello,在shell中键入启动命令后,shell通过fork函数产生子进程。
1.2 环境与工具
硬件环境:X64 CPU ,2.50GHz , 8G RAM
软件环境:Windows 10 64位 ,Vmware 15 ,Ubuntu 16.04 LTS 64 位
开发工具:gcc + gedit , Codeblocks , gdb edb
1.3 中间结果
hello.c :hello源代码
hello.i :预处理后的文本文件
hello.s :hello.i编译后的汇编文件
hello.o :hello.s汇编后的可重定位目标文件
hello :链接后的可执行文件
1.4 本章小结
本章对hello程序进行了简单的介绍,讲述了本文主要的内容是追踪hello.c从一个源文件成为最终可执行文件并加载执行的过程。
第2章 预处理
2.1 预处理的概念与作用
在嵌入式系统编程中不管是内核的驱动程序还是应用程序的编写,涉及到大量的预处理与条件编译,这样做的好处主要体现在代码的移植性强以及代码的修改方便等方面。因此引入了预处理与条件编译的概念。预处理(或称预编译)是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。预处理指令指示在程序正式编译前就由编译器进行的操作,可放在程序中任何位置。
预处理是C语言的一个重要功能,它由预处理程序负责完成。当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。C语言提供多种预处理功能,主要处理#开始的预编译指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
预编译的主要作用如下:
1、将源文件中以”include”格式包含的文件复制到编译的源文件中。
2、用实际值替换用“#define”定义的字符串。
3、根据“#if”后面的条件决定需要编译的代码。
2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
预处理的行为是由指令控制的。这些指令是由#字符开头的一些命令。
#define指令定义了一个宏---用来代表其他东西的一个命令,通常是某一个类型的常量。预处理会通过将宏的名字和它的定义存储在一起来响应#define指令。当这个宏在后面的程序中使用到时,预处理器”扩展”了宏,将宏替换为它所定义的值。
#include指令告诉预处理器打开一个特定的文件,将它的内容作为正在编译的文件的一部分“包含”进来。例如:下面这行命令:
#include<stdio.h>指示预处理器打开一个名字为stdio.h的文件,并将它的内容加到当前的程序中。
预处理器的输入是一个C语言程序,程序可能包含指令。预处理器会执行这些指令,并在处理过程中删除这些指令。预处理器的输出是另外一个程序:原程序的一个编辑后的版本,不再包含指令。预处理器的输出被直接交给编译器,编译器检查程序是否有错误,并经程序翻译为目标代码
在main函数被定义之前,预处理器(cpp)首先读取到的是头文件stdio.h 、stdlib.h 和unistd.h。根据读取到的内容,cpp将三个系统头文件从库中读出,并依次展开为具体的代码,这一过程是直接的,机械的,生成的文件hello.i仍旧是一个C程序。我们以stdio.h的展开为例,通过打开usr/include/stdio.h,阅读其内容,发现了其中还含有#开头的宏定义等。对此,预处理器会对此继续递归展开,使得最终的.i程序中没有#define的语句,并且针对#开头的条件编译语句,cpp根据#if后面的条件决定需要编译的代码。
由于顺序的原因,main函数在.i文件的最后部分,前面的部分分别是.c源程序文件名、命令行参数、环境变量,对包含的.h头文件的预处理,用绝对路径将其替代,标准C库中一些数据类型的声明,结构体的定义,对引用的外部函数的声明等内容。可以发现的是,这一过程虽然简单,却实际上工作
2.4 本章小结
这一阶段我们进行了预处理的过程,预处理过程进行替换代码文本的工作,是编译前的准备步骤,没有检查源程序语法正确性,没有对语法进行修改,只是单纯地将源程序在内容上扩充,是从.c源程序到可执行文件的第一步。
第3章 编译
3.1 编译的概念与作用
编译既可以指利用编译程序从源语言编写的源程序产生目标程序的过程,又可以指用编译程序产生目标程序的动作。 编译就是把高级语言变成计算机可以识别的2进制语言,计算机只认识1和0,编译程序把人们熟悉的语言换成2进制的。编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。整个过程将便于人编写、阅读、维护的高级计算机语言所写作的源代码程序,翻译为计算机能解读、运行的低阶机器语言的程序,也就是可执行文件。编译器将原始程序作为输入,翻译产生使用目标语言的等价程序。源代码一般为高级语言,如Pascal、C、C++、C# 、Java等,而目标语言则是汇编语言或目标机器的目标代码,有时也称作机器代码。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1 开头部分
.file:源文件名
.globl:全局变量
.data:数据段
.align:对齐方式
.type:指定是对象类型或是函数类型
.size:大小
.long:长整型
.section .rodata:下面是.rodata节
.string:字符串
.text:代码段
3.3.2 数据部分
hello.s中C语言的数据类型主要有:局部变量、指针数组。
1.int argc:argc是函数传入的第一个int型参数,存储在%edi中。
2.char* argv:argv是函数获知的指针数组。
3. int i;局部变量,通常保存在寄存器或是栈中。根据movl $0, -4(%rbp)操作可知i的数据类型占用了4字节的栈空间。
4.常量,hello.s中一些如同4、1等常量以立即数形式($4、$1……)出现。
3.3.3 赋值
这个语句的作用是对局部变量i的赋值,等价于i=0。
3.3.4 算术操作
此语句对应for循环中的i++操作。
3.3.5 关系操作
(1)此语句是for循环中的控制部分,对应i<8语句的作用。
(2)此语句对应argc!=4,在读取*argv[]数组时起到作用。
3.3.6 数组/指针/结构操作
*argv[]数组是一个指针数组,数组元素是指针变量,指针变量存储的是字符串的地址,即指针数组存储了字符串的地址,argv[i]即为指针数组中第i个字符串的地址。char* 数据类型占8个字节,由(%rax)和%rax+8得到argv[1]和argc[2]两个字符串。
3.3.7控制转移
此语句是for循环中的控制部分,对应i<8语句的作用。
3.3.8函数操作:
1.main函数:
参数传递:传入参数argc和argv[],分别用寄存器%rdi和%rsi存储。
函数调用:被系统启动函数调用。
函数返回:设置%eax为0并且返回,对应return 0 。
2.printf函数:
参数传递:call puts时只传入了字符串参数首地址;for循环中call printf时传入了 argv[1]和argc[2]的地址。
函数调用:if判断满足条件后调用,与for循环中被调用。
3.exit函数:
参数传递:传入的参数为1,再执行退出命令
函数调用:if判断条件满足后被调用.
4.sleep函数:
参数传递:传入参数atoi(argv[3]),
函数调用:for循环下被调用,call sleep
5.getchar函数:
函数调用:在main中被调用,call getchar
3.4 本章小结
本章通过编译时,以hello为例,讲述了编译器是怎么处理C语言的各个数据类型以及各类操作的实际应用说明,像是数据:常量、变量(全局/局部/静态)的存放,通过movx的赋值 = ,算术操作:+、 - 、++,关系操作: != 、<=,数组/指针的引用:A[i]、*p 控制转移:if、for的使用,函数操作:参数传递(地址/值)、函数调用()、函数返回 return的实现等等,这个过程检查了语法。
第4章 汇编
4.1 汇编的概念与作用
汇编程序是指把汇编语言书写的程序翻译成与之等价的机器语言程序的翻译程序。汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序。汇编语言是为特定计算机或计算机系列设计的一种面向机器的语言,由汇编执行指令和汇编伪指令组成。采用汇编语言编写程序虽不如高级程序设计语言简便、直观,但是汇编出的目标程序占用内存较少、运行效率较高,且能直接引用计算机的各种设备资源。它通常用于编写系统的核心部分程序,或编写需要耗费大量运行时间和实时性要求较高的程序段。
4.2 在Ubuntu下汇编的命令
汇编命令:gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
1.ELF头描述了生成该文件的系统的字的大小和字节顺序,并且包含帮助链接器语法分析和解释目标文件的信息。
2.节头部表描述了不同节的位置和大小,其中目标文件中每个节都有一个固定大小的条目。具体的描述包括节的名称、类型、地址和偏移量等。
3.当汇编器生成一个目标模块是,它并不知道数据和代码最终将放在内存中的什么位置,它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行目标文件时如何修改这个引用。代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中。
两种最基本的重定位类型:
R_X86_64_PC32 :重定位一个使用32位PC相对地址的引用。
R_X86_64_32 :重定位一个使用32位PC绝对地址的引用。
4.symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
对比hello.s文件和反汇编代码,主要有以下的差别
1. 操作数:hello.s中的操作数时十进制,hello.o反汇编代码中的操作数是十六进制。
2. 分支转移:跳转语句之后,hello.s中是.L2和.L3等段名称,而反汇编代码中跳转指令之后是相对偏移的地址。
3. 函数调用:hello.s中,call指令之后直接是函数名称,而反汇编代码中call指令之后是函数的相对偏移地址。因为函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
4. 全局变量的访问:在hello.s文件中,对于.rodata和sleepsecs等全局变量的访问,是$.LC0和sleepsecs(%rip),而在反汇编代码中是$0x0和0(%rip),是因为它们的地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。
4.5 本章小结
由于汇编语言的指令与机器语言的指令大体上保持一一对应的关系,汇编算法采用的基本策略是简单的。通常采用两遍扫描源程序的算法。第一遍扫描源程序根据符号的定义和使用,收集符号的有关信息到符号表中;第二遍利用第一遍收集的符号信息,将源程序中的符号化指令逐条翻译为相应的机器指令。具体的翻译工作可归纳为如下几项:用机器操作码代替符号操作;用数值地址代替符号地址;将常数翻译为机器的内部表示;分配指令和数据所需的存储单元。除了上述的翻译工作外,汇编程序还要考虑:处理伪指令,收集程序中提供的汇编指示信息,并执行相应的功能。为用户提供信息和源程序清单。汇编的善后处理工作,随目标语言的类型不同而有所不同。有的直接启动执行,有的先进行连接装配。如果具有条件汇编、宏汇编或高级汇编功能时,也应进行相应的翻译处理。本章通过汇编器(as)将hello.s编译成机器语言指令,再通过readelf查看可重定位目标程序文件,通过对elf文件结构的分析,获得相关数据的运行时地址,以及不同节的、条目的大小、偏移量等信息。同时,通过.s文本文件与由机器语言反汇编获得的汇编代码比较,易得.s文件中,通过注记符寻址和经反汇编后,重定位表示的地址信息差异。
第5章 链接
5.1 链接的概念与作用
链接时将各种代码和数据片段收集并组合成为一个单一的文件的过程,这个文件可被加载(复制)到内存并执行。通过链接器,将程序调用的外部函数(.o文件)与当前.o文件以某种方式合并,并得到./hello可执行目标文件的的过程成为链接。且该二进制文件可被加载到内存,并由系统执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。基于此特性的改进,以提高程序运行时的时间、空间利用效率。链接是由叫做链接器的程序执行的。链接器使得分离编译成为可能。它将巨大的源文件分解成更小的模块,易于管理。我么可以通过独立地修改或编译这些模块,并重新链接应用,不必再重新编译其他文件。
5.2 在Ubuntu下链接的命令
使用ld的链接命令:ld链接命令: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的格式
与可重定位文件结构类似。ELF头中给出了一个16字节序列,描述了生成该文件系统字的大小和字节顺序。其余的为帮助链接器进行语法分析和解释目标文件的信息,包括ELF文件的大小,节头部的起始位置,程序的入口地点,目标文件的类型,机器类型,节头部表的文件偏移,以及节头部表中条目的大小与数量。
5.4 hello的虚拟地址空间
在edb中打开hello,通过Data Dump查看hello程序的虚拟地址空间各段信息。在Memory Regions选择View in Dump可以分别在Data Dump中看到只读内存段和读写内存段的信息。
5.5 链接的重定位过程分析
objdump -d -r hello
分析hello与hello.o的不同:
1.链接增加了新的函数:
在hello中链接加入了在hello.c中用到的库函数,如exit、printf、sleep、getchar等函数。
2.增加的节:
hello中增加了.init和.plt节,和一些节中定义的函数。
3.函数调用:
hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
4.地址访问:
hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于某些地址的定位是不明确的,其地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。
说明链接的过程:
根据hello和hello.o的不同,分析出链接的过程为:
链接就是链接器(ld)将各个目标文件(各种.o文件)组装在一起,文件中的各个函数段按照一定规则累积在一起。
结合hello.o的重定位项目,分析hello中对其怎么重定位的:
反汇编见附件objdump.txt与objdump_o.txt。
重定位过程合并输入模块,并为每个符号分配运行时地址,主要有以下两步:
1.重定位节和符号的定义。
在这一步中,链接器将所有相同类型的节合并成同一类型的新聚合节。包括hello.o在内的所有可重定位目标文件中的.data节全被合并成一个节——输出的可执行目标文件hello中的.data节。
然后,连接器将运行时的内存地址赋入新节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,hello中每条指令和变量都有唯一的运行时内存地址了。
2.重定位节中的符号引用。链接器依赖于hello.o中的重定位条目,修改代码节和数据节中对每个符号的引用,使得它们指向正确运行时的地址。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
DEBUG [Analyzer] adding: hello!_init <0x0000000000401000>
DEBUG [Analyzer] adding: hello!puts@plt <0x0000000000401030>
DEBUG [Analyzer] adding: hello!printf@plt <0x0000000000401040>
DEBUG [Analyzer] adding: hello!getchar@plt <0x0000000000401050>
DEBUG [Analyzer] adding: hello!atoi@plt <0x0000000000401060>
DEBUG [Analyzer] adding: hello!exit@plt <0x0000000000401070>
DEBUG [Analyzer] adding: hello!sleep@plt <0x0000000000401080>
DEBUG [Analyzer] adding: hello!_start <0x0000000000401090>
DEBUG[Analyzer] adding: hello!_dl_relocate_static_pie <0x00000000004010c0>
DEBUG [Analyzer] adding: hello!main <0x00000000004010c1>
DEBUG [Analyzer] adding: hello!__libc_csu_init <0x0000000000401150>
DEBUG [Analyzer] adding: hello!__libc_csu_fini <0x00000000004011b0>
DEBUG [Analyzer] adding: hello!_fini <0x00000000004011b4>
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
首先在节头部表中找到GOT的首地址——0x00403ff0,在edb中查看其变化
发现其从起始地址开始16字节全为0,调用_dl_init函数后,GOT中内容就发生了变化。
其中,GOT[[2]]是动态链接器在ld-linux.so模块中的入口点,也就是对应地址。
5.8 本章小结
链接性是程序编译时,程序中的名字(name,也可称标识符identifier)在作用域中不同位置的出现能够绑定到同一对象或函数。链接性描述了名字在整个程序或单独编译单元中能否绑定到同一实体(entity)。本章主要理解了Ubuntu下链接的过程,链接就是是将各种代码和数据片段收集并组合成一个单一文件的过程。通过查看hello的虚拟地址空间,并且对比hello.o和hello的反汇编代码,更好地掌握了链接尤其是重定位的过程,但是我们知道链接并不止于此,hello会在它运行时要求动态链接器加载和链接某个共享库,而无需在编译时将那些库链接到应用中。
第6章 hello进程管理
6.1 进程的概念与作用
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。狭义上,进程是正在运行的程序的实例,广义上即是指一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元。
进程的实现与作用:
1.每次用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在新进程的上下文中运行它们自己的代码或其他应用程序。
2.进程提供给应用程序的关键抽象:一个独立的逻辑控制流,好像我们的程序独占地使用处理器;一个私有的地址空间,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
1.shell的作用
Shell俗称壳,是指“为使用者提供操作界面”的软件。它接收用户命令,然后调用相应的应用程序。同时它又是一种程序设计语言。作为命令语言,它交互式解释和执行用户输入的命令或者自动地解释和执行预先设定好的一连串的命令;作为程序设计语言,它定义了各种变量和参数,并提供了许多在高级语言中才具有的控制结构,包括循环和分支实际上Shell是一个命令解释器,它解释由用户输入的命令并且把它们送到内核。不仅如此,Shell有自己的编程语言用于对命令的编辑,它允许用户编写由shell命令组成的程序。Shell编程语言具有普通编程语言的很多特点,比如它也有循环结构和分支控制结构等,用这种编程语言编写的Shell程序与其他应用程序具有同样的效果
2.shell的处理流程
Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。
其基本功能是解释并运行用户的指令,重复如下处理过程:
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令,如果不是内部命令,调用fork( )创建新进程/子进程
(4)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(5)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait...)等待作业终止后返回。
(6)如果用户要求后台运行(如果命令末尾有&号),则shell返回;
6.3 Hello的fork进程创建过程
一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。
子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。注意,子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间。
根据shell的处理流程,可以推断,输入命令执行hello后,父进程如果判断不是内部指令,即会通过fork函数创建子进程。子进程与父进程近似,并得到一份与父进程用户级虚拟空间相同且独立的副本——包括数据段、代码、共享库、堆和用户栈。
6.4 Hello的execve过程
execve()函数的作用是在当前进程(新创建的子进程)的上下文中加载并运行hello程序。将hello中的.text节、.data节、.bss节等内容加载到当前进程的虚拟地址空间
execve函数在加载并运行可执行目标文件Hello,且带列表argv和环境变量列表envp。该函数的作用就是在当前进程的上下文中加载并运行一个新的程序。
只有当出现错误时,例如找不到Hello时,execve才会返回到调用程序,这里与一次调用两次返回的fork不同。
在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下的原型:
int main(int argc , char **argv , char *envp);
结合虚拟内存和内存映射过程,可以更详细地说明exceve函数实际上是如何加载和执行程序Hello:
1.删除已存在的用户区域(自父进程独立)。
2.映射私有区:为Hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的。
3.映射共享区:比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
4.设置PC:exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
进程上下文信息:
进程的上下文由程序正确运行所需的状态组成,这个状态包括存放在内存中的程序的代码和数据,还包括它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
进程时间片:
分时操作系统分配给正在运行的进程的一段CPU时间。
调度的过程:
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
以执行sleep函数为例,sleep函数请求调用休眠进程,sleep将内核抢占,进入倒计时,当倒计时结束后,hello程序重新抢占内核,继续执行。
用户态与核心态转换:
为了能让处理器安全运行,不至于损坏操作系统,必然需要先知应用程序可执行指令所能访问的地址空间范围。因此,就存在了用户态与核心态的划分,核心态可以说是“创世模式”,拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
hello执行过程中会出现的异常:
中断:信号SIGTSTP,默认行为是 停止直到下一个SIGCONT;
终止:信号SIGINT,默认行为是 终止程序运行。
过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
1.乱按:
运行时的乱按并不会影响程序的运行,但因为程序中存在getchar函数,按到回车键时,getchar会读入回车符及之后字符串,导致在程序运行完成后,将这些字符串作为shell的命令行输入。
2.ctrl+c:将使得程序终止并被回收。
3.Ctrl+z:这将导致程序挂起,暂停运行
4.ctrl+z后ps:挂起后,ps命令列出当前系统中的进程(包括僵死进程)
5.ctrl+z后pstree:与ps相似,但pstree命令是以树状图显示进程间的关系
6.ctrl+z,jobs:jobs命令列出 当前shell环境中已启动的任务状态
7.crtl+z后,fg:将挂起程序调回前台继续运行。
8.ctrl+z与ps后,kill:输入kill -9 PID,杀死进程。
6.7本章小结
本章了解了hello进程的执行过程,主要是hello的创建、加载和终止,通过键盘输入,对hello执行过程中产生信号和信号的处理过程有了更多的认识,对使用linux调试运行程序也有了更多的心得。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:包含在机器语言中用来指定一个操作数或一条指令的地址。每一个逻辑地址都由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。就是hello.o里相对偏移地址。
线性地址:逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。是hello中的虚拟内存地址。
虚拟地址:一个带虚拟内存的系统中,CPU从一个有N=2^n个地址空间中生成虚拟地址。虚拟地址其实就是线性地址。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。地址翻译会将hello的一个虚拟地址转化为物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
这是指把一个程序分成若干段进行存储,每一段都是一个逻辑实体。段式管理的产生与程序的模块化有极大关系。段式管理通过段表进行,包括段号或段名、段起点、装入位、段的长度等。
此外,段式管理还需要主存占用区域表、主存可用区域表。
在段式存储管理系统中,除了为每个段分配一个连续的分区便于找寻外,进程中的各个段可以不连续地存放在内存的不同分区中。
之后在程序加载时,操作系统为所有段分配其所需内存,物理内存的管理采用动态分区的管理方法,这能够利用碎片化的空间。
段式管理是不连续分配内存技术中的一种,最大特点在于非常直观,按程序段、数据段等有明确逻辑含义的“段”来分配内存空间。克服了页式的、硬性的、非逻辑划分给保护和共享与支态伸缩带来的不自然性。
段另一个好处就是可以充分实现共享和保护,便于动态申请内存,管理和使用统一化,便于动态链接,其缺点是有碎片问题。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。
将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
优点
1、由于它不要求作业或进程的程序段和数据在内存中连续存放,从而有效地解决了碎片问题。
2、动态页式管理提供了内存和外存统一管理的虚存实现方式,使用户可以利用的存储空间大大增加。这既提高了主存的利用率,又有利于组织多道程序执行。
缺点
1、要求有相应的硬件支持。例如地址变换机构,缺页中断的产生和选择淘汰页面等都要求有相应的硬件支持。这增加了机器成本。
2、增加了系统开销,例如缺页中断处理机,
3、请求调页的算法如选择不当,有可能产生抖动现象。
4、虽然消除了碎片,但每个作业或进程的最后一页内总有一部分空间得不到利用果页面较大,则这一部分的损失仍然较大。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB,即转译后备缓冲器,是用于缩短虚拟寻址时间的小缓存,其每一行都保存着PTE块,这就使得如果请求的虚拟地址在TLB中存在,它将很快地返回匹配结果。
如果没有TLB,对线性地址的访问,就需要首先从PGD中获取PTE(第一次内存访问),在PTE中获取页框地址(第二次内存访问),最后访问物理地址,总共需要3次RAM的访问,远比一次访问要麻烦得多。
变换过程可以分成以下几步:
首先将CPU内核发送过来的32位VA[31:0]分成三段,前两段VA[31:20]和VA[19:12]作为两次查表的索引,第三段VA[11:0]作为页内的偏移,查表的步骤如下:
⑴从协处理器CP15的寄存器2(TTB寄存器,translation table base register)中取出保存在其中的第一级页表(translation table)的基地址,这个基地址指的是PA,也就是说页表是直接按照这个地址保存在物理内存中的。
⑵以TTB中的内容为基地址,以VA[31:20]为索引值在一级页表中查找出一项(2^12=4096项),这个页表项(也称为一个描述符,descriptor)保存着第二级页表(coarse page table)的基地址,这同样是物理地址,也就是说第二级页表也是直接按这个地址存储在物理内存中的。
⑶以VA[19:12]为索引值在第二级页表中查出一项(2^8=256),这个表项中就保存着物理页面的基地址,我们知道虚拟内存管理是以页为单位的,一个虚拟内存的页映射到一个物理内存的页框,从这里就可以得到印证,因为查表是以页为单位来查的。
⑷有了物理页面的基地址之后,加上VA[11:0]这个偏移量(2^12=4KB)就可以取出相应地址上的数据了。
这个过程称为Translation Table Walk,Walk这个词用得非常形象。从TTB走到一级页表,又走到二级页表,又走到物理页面,一次寻址其实是三次访问物理内存。注意这个“走”的过程完全是硬件做的,每次CPU寻址时MMU就自动完成以上四步,不需要编写指令指示MMU去做,前提是操作系统要维护页表项的正确性,每次分配内存时填写相应的页表项,每次释放内存时清除相应的页表项,在必要的时候分配或释放整个页表。
7.5 三级Cache支持下的物理内存访问
现代MMU一般使用四级页表来将虚拟地址翻译成物理地址。经过先前的翻译以后,我们就得到了物理地址PA。
现分析三级cache支持下的物理内存访问:
如图,以L1 d-cache的介绍为例,L2和L3同理。
L1 Cache是8路64组相联。块大小为64B。因此CO和CI都是6位,CT是40位。根据物理地址(PA),首先使用组索引CI,每组8路,分别匹配标记CT。如果匹配成功且块的有效位是1,则命中,根据块偏移CO返回数据。
如果没有匹配成功,或者匹配成功但是标志位是0,则不命中,向下一级缓存申请请求的块,然后将新的块存储在组索引指示的组中的一个高速缓存行中。
一般而言,如果映射到的组内有空闲块,则直接放置,否则必须驱逐出一个现存的块,一般采用最近最少被使用策略LRU进行替换。
7.6 hello进程fork时的内存映射
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
在当前进程中的程序执行了execve(”a.out”,NULL, NULL)调用时,execve函数在当前程序中加载并运行包含在可执行文件a.out中的程序,用a.out代替了当前程序。加载并运行a.out主要分为一下几个步骤:
1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构;
2.映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零;
3.映射共享区域, hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内;
4.设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
DRAM 缓存不命中称为缺页,即虚拟内存中的字不在物理内存中。缺页导致页面出错,产生缺页异常。缺页异常处理程序选择一个牺牲页,然后将目标页加载到物理内存中。最后让导致缺页的指令重新启动,页面命中。
7.9动态存储分配管理
动态内存分配是重要的,因为经常知道程序运行才知道某些数据结构的大小。
可以用低级的mmap和munmap函数来创建和删除虚拟内存区域。但动态内存分配器可以使用更方便,可移植性也更好。他维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,有已分配和空闲两种状态。
分配器有两种风格:
1、显示分配器:要求应用显示释放已分配的块。
2、隐式分配器:也叫垃圾收集器,会自动监测不再使用的块将其释放回收。
造成堆利用效率低的原因是碎片现象,碎片分为两种:
1、内部碎片:当一个已分配块比有效载荷的,由对齐要求产生
2、外部碎片:空闲内存合起来足够满足分配请求,但是处于不连续的内存片中
显然动态内存分配器需要解决分配块,合并块的要求,这就需要我们设计合适的数据结构来减少碎片,下面介绍两种:
隐式空闲链表
显示空闲链表
7.10本章小结
本章前半章主要讲述了linux下的存储管理,虚拟地址到物理地址的映射过程,翻译过程,以及系统是通过什么怎么样辅助地址翻译的过程的(TLB,四级页表,三级cache),后半章主要讲述了缺页故障的处理过程以及动态内存分配器工作原理原因及必要性等等,理解这些对于我们对系统存储方面的理解有很大的帮助。
第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).关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
Unix I/O函数:
1).int open(char* filename,int flags,mode_t mode) ,进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
2).int close(fd),fd是需要关闭的文件的描述符,close返回操作结果。
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生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
研究printf的实现,首先来看看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;
}
首先用vsprintf进行格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
然后在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)
最终就在屏幕上出现了我们所需要的字符串
8.4 getchar的实现分析
异步异常-键盘中断的处理::当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成 ASCII 码,保存到系统的键盘缓冲区之中。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数。
了解系统级IO是重要的,有时你除了使用Unix I/O 以外别无选择。在某些重要的情况中,使用高级I/O 函数不太可能,或者不太合适。例如,标准I/O 库没有提供读取文件元数据的方式,例如文件大小或文件创建时间。同时这对于我们理解网络编程和并行流是很有帮助的。
结论
hello所经历的过程:
C语言实现--文本编辑器编写完毕保存时对扩展名的修改,诞生源程序
预处理--将hello.c源程序所有调用的外部库扩展到该源程序中诞生hello.i
编译--处理hello.i文件编译成为hello.s汇编语言文件
汇编--处理hello.s文件汇编成为hello.o可重定位文件
链接--处理hello.o文件链接外部连接库生成hello可执行文件
运行--linux终端shell下键入“./hello 学号 姓名”,运行可执行文件
创建子进程--shell通过fork函数创建子进程
子进程运行程序--shell调用execve,execve调用启动加载器,加映射虚拟内存,虚拟地址映射到物理地址,运行到main函数
执行指令--CPU逐步执行hello中机器语言指令
内存访问--访问内存空间
信号处理--遇到shell中的个别信号进入信号处理程序
回收子进程--程序执行完毕后交由父进程回收子进程,结束以及执行
Hello程序只是计算机学习中的一个最基础的部分,理解该程序的运行有助于我们今后的学习,完成报告的过程中我们又温故而知新,学习了很多新东西。
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.i 经过预处理后的hello
hello.s 经过编译后的汇编语言文件
hello.o 经过汇编后的可重定位目标文件
hello.out 经过连接后生成的课执行目标文件
参考文献
- 《深入理解计算机系统》 Randal E.Bryant David R.O’Hallaron 机械工业出版社
- 百度百科