哈工大计算机系统2022春大作业

计算机系统

大作业

计算机科学与技术学院
2022年5月
摘 要
本文根据Hello的自白,以计算机系统的相关术语,简述hello程序在Linux系统下的生命周期,简述hello程序的一生。从最开始的hello.c经过一系列处理,包括预处理、编译、汇编、链接等,生成可执行文件的过程,从加载到运行、终止和回收的过程。在Linux系统下,跟随《计算机系统》课程的脚步,走完hello程序的一生,对计算机系统有更深层次的理解。

关键词:计算机系统;预处理;编译;汇编;链接;进程;存储;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,world!”也许就是最美丽的语言。如此的程序,到底是如何从几行文字变成屏幕上的显示,且看下面的一步步介绍。
此部分需要介绍两个术语,即P2P、020。
P2P,From Program to Process,即从程序走向进程的过程。以由IDE自动生成的最优美的程序源文件hello.c为例,hello.c经过预处理器cpp的处理生成了hello.i文件,再经过编译器ccl处理生成了汇编程序hello.s,紧接着由汇编器as处理,产生了可重定位目标程序hello.o,在这时候,hello已经成长为程序员很难读懂的文件–二进制文件了,之后许许多多的可重定位目标程序,如printf.o,他们汇聚在一起,通过链接器ld的整合,生成了可执行目标程序hello。此时,P2P的过程完成了一半。当hello程序运行时,操作系统会将该文件加载到内存当中,通过fork函数为其产生子进程,再通过调用execve函数加载该进程。整个过程,就是P2P。
020,From 0 to 0,即程序从无到有再到无的过程。仍然以hello程序为例。程序员从0开始创建,编写好hello.c程序文本,经过整个P2P过程,shell为子进程映射虚拟内存,进入程序入口后,程序开始载入物理内存,随后再进入main函数执行程序,CPU为运行的hello执行逻辑控制流。当程序运行结束后,shell父进程会回收hello进程,释放占用的内存、物理资源等,至程序运行的痕迹全部被扫清后,便回到了0。整个过程,就是020。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:Intel® Core™i7-10510U CPU @ 1.80GHz
软件工具:Windows 10 家庭中文版、Oracle VM VirtualBox、Ubuntu-20.04.4
开发与调试工具:CodeBlocks、edb、gcc、gdb、readelf、objdump
1.3 中间结果
文件名 作用
hello.i 预处理器cpp修改后的源程序,分析预处理器的行为
hello.s 编译器ccl生成的编译程序,分析编译器的行为
hello.o 汇编器as生成的可重定位目标程序,分析汇编器的行为
hello.txt 使用objdump工具生成的反汇编代码文本(汇编处)
hello1.txt 使用objdump工具生成的反汇编代码文本(链接处)
hello.elf hello.o的elf格式
hello1.elf hello的elf格式
hello 链接器ld生成的可执行目标程序,分析链接器的行为

1.4 本章小结
本章主要对hello的一生进行了简要概括,介绍了hello程序的大致运行过程,并以hello程序为例阐述了P2P以及020两个过程的含义,介绍了本次作业使用的软硬件环境及开发与调试工具,最后简述了本次作业涉及到的文件及其作用。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用
预处理的概念:
预处理是C语言的一个重要功能,它由预处理器cpp负责完成。当对一个源文件进行编译时,系统将自动引用预处理器对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。以hello程序为例,C语言提供多种预处理功能,主要处理以#开始的预编译指令,如宏定义(#define M 1024)、文件包含(#include <stdio.h>)、条件编译(#ifdef)等,包括展开头文件、宏替换、去掉注释、条件编译等等。
预处理的作用:
根据源代码中的预处理指令修改源代码,预处理从系统的头文件包中将头文件的源码插入到目标文件中,宏和常量标识符已全部被相应的代码和值替换,最终生成.i文件,相比于原来的文件更便于阅读、修改、移植和调试,也有利于模块化程序设计。

2.2在Ubuntu下预处理的命令
命令:gcc -E -o hello.i hello.c
截图:
在这里插入图片描述
2.3 Hello的预处理结果解析
在这里插入图片描述

结果分析:
经过预处理后,hello.c文件生成了hello.i文件。打开hello.i文件查看,发现相比于hello.c文件,内容大大增加,但仍然是属于人为可阅读的C语言文本。
经过hello.c和hello.i文件对比分析,预处理对原程序进行了宏展开、头文件中的内容被包含进hello.i文件中(如函数声明、结构体的定义、变量定义、宏定义等等),如果有#define命令还会对其符号进行相应的替换。
2.4 本章小结
本章主要介绍了预处理的概念及作用,同时静态演示生成hello.i文件指令,并与原文本hello.c比较,分析预处理的行为。
(第2章0.5分)

第3章 编译

3.1 编译的概念与作用
编译的概念:
编译是指利用编译器ccl从预处理文件(.i文件)产生汇编程序(.s文件)的过程。详细地说,以hello程序为例,这个过程是把hello.i文件,通过一系列词法分析、语法分析、语义分析以及优化,生成相应的汇编文件hello.s文件。
编译的作用:
检查有误语法错误。在预处理文件转化为汇编文件的过程中,会经过一系列的词法分析、语法分析,存在语法错误则会给出提示信息;
对原程序进行优化。在预处理文件转化为汇编文件的过程中,会经过一系列的优化,生成与原程序等价的程序,但是运行起来效率更高;
文本语言的转换。在预处理文件转化为汇编文件的过程中,代码会被从高级编程语言转化为适宜机器理解的汇编语言;
提高程序员工作效率。程序员可以用更易于人理解的高级语言,编写蕴含复杂、更高级逻辑的代码,而不是用实现难度更大的汇编语言编写,提高效率。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.c -o hello.s
截图:
在这里插入图片描述
3.3 Hello的编译结果解析
3.3.1 编译部分结果展示
在这里插入图片描述
3.3.2 汇编初始部分解析
节名称 作用
.file 源文件声明
.text 代码节
.rodata 只读数据段
.align 对齐方式
.string 声明一个字符串
.globl 声明全局变量
.type 声明一个符号是函数类型/数据类型
.size 声明大小
.section 把代码划分为若干段
3.3.3 数据解析
1.字符串:
根据汇编代码的.string可知,存在两个字符串,如下
在这里插入图片描述
前两个字符串通过leaq传入%rdi,分别是printf的函数参数,分别对应输出内容即输出格式。
在这里插入图片描述
2.整型变量i:
main函数声明一个局部变量i(整型),编译器进行编译时将局部变量i放在堆栈中。如下图所示,通过movl指令将局部变量i放在-4(%rbp)位置。由mov指令类型为movl、数字为立即数0即64为操作环境可以推测出i是4个字节的int类型。
在这里插入图片描述
3.整型变量argc:
作为main函数的参数,借助寄存器argc被放在堆栈中。
4.立即数:
立即数m以$m的形式出现,如下图所示。
在这里插入图片描述
在这里插入图片描述
5.数组char *argv[]:
hello.c中唯一的数组是main函数的第二个参数,数组中的每个元素是char类型的指针。数组的起始地址通过movq指令存放在-32(%rbp)位置,被调用传递给printf函数。在这里插入图片描述

3.3.4 全局函数
通过.type main, @function以及.globl main可知main函数是程序中的全局函数。在这里插入图片描述

3.3.5 赋值操作
本程序的赋值操作主要有i=0,而这条代码对应的汇编指令是mov类型,mov类型的指令有四种具体指令,对应的操作数字节如下表所示
指令 操作数字节
movb 一个字节
movw 两个字节
movl 四个字节
movq 八个字节
3.3.6 算术操作
在汇编语言中,算术操作指令如下表所示。
指令 效果 描述
leaq S D D<- &S 加载有效地址
INC D D<- D+1 加1
DEC D D<- D-1 减1
NEG D D<- -D 取负
NOT D<- ~D 取补
ADD S,D D<- D+S 加
SUB S,D D<- D-S 减
IMUL S,D D<- DS 乘
XOR S,D D<- D^S 异或
OR S,D D<- D|S 或
AND S,D D<- D&S 与
SAL k,D D<- D<<k 左移
SHL k,D D<- D<<k 左移
SAR k,D D<- D>>k 算术右移
SHR k,D D<- D>>k 逻辑右移
imulq S R[%rdx]:R[%rax]<- S
R[%rax] 无符号乘法
mulq S R[%rdx]:R[%rax]<- S*R[%rax] 有符号乘法
cqto R[%rdx]:R[%rax]<- 符号扩展(R[%rax]) 转化为八字
idivq S R[%rdx]:R[%rax]<- R[%rax] mod S
R[%rdx]:R[%rax]<- R[%rax] ÷ S 有符号除法
divq S R[%rdx]:R[%rax]<- R[%rax] mod S
R[%rdx]:R[%rax]<- R[%rax] ÷ S 无符号除法
查看hello.s文本,发现有如下种类的操作:
1.subq指令,借助sub类型指令从栈中开辟空间在这里插入图片描述
2.addq指令,根据上下文可知,借助add类型指令修改地址偏移量在这里插入图片描述
3.leaq指令,借助leaq指令,将%rip所指内容传递给%rdi,根据上下文,是printf函数的参数在这里插入图片描述
4.addl指令,add指令类
在这里插入图片描述
3.3.7 关系判断操作
关系判断操作通过cmp类型指令(会设置条件码)实现,查看hello.s汇编文本,发现有如下关系判断操作:
1.cmql $4, -20(%rbp),通过上下文可知,是判断argc与3的大小关系,为后面的je跳转进行准备。
在这里插入图片描述
2.cmpl $7, -4(%rbp),通过上下文可知,是判断i与7的大小关系,为后面的jle跳转进行准备。
在这里插入图片描述
3.3.8 控制转移操作
控制转移操作通过之前设置的条件码进行转移,查看hello.s汇编文本,发现有如下两个控制转移操作,对应3.3.7的关系转移操作。
1.if(argc!=3),如果arg不等于3,则不进行跳转;否则跳转至.L2处。
在这里插入图片描述
2.for(i=0;i<=7;i++),每次循环前都与7比较,小于等于则继续进行,大于7则跳转到.L4处。
在这里插入图片描述

3.3.9 函数操作
在C语言中,调用函数需要进行如下操作(函数X调用函数Y):
1.传递操作:
进行函数Y的时候,程序计数器要设置为Y的代码起始地址;当函数即将返回时,程序计数器要设置为调用Y后面的指令地址。
2.传递数据:
函数X必须能够向函数Y传递一个或多个参数;函数Y必须能够向函数X返回一个返回值。
3.分配和释放空间:
在函数Y开始时,可能需要为局部变量分配一定的空间;在函数Y返回前,需要释放函数Y开始时分配的空间。
在Linux 64位操作系统中,函数若有6个以内的参数需要传递进去,则按顺序依次靠%rdi、%rsi、%rdx、%rcx、%r8、%9寄存器传递;若超过6个需要传递的参数,则多出来的参数依靠栈进行传递。
hello.s中使用的函数有main函数、puts函数、printf函数、exit函数、sleep函数、atoi函数、getchar函数。main函数的参数为argc和argv;两次printf函数的参数分别是两个字符串;exit函数的参数是1;sleep函数的参数为atoi(argv[3])。
在这里插入图片描述 在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
汇编中调用函数都通过call指令进行调用,函数参数的返回值都存储在%eax中。
3.3.10 类型转换
在hello.s中通过atoi(argv[3])进行类型转换,将字符类型(char)转化为整型(int)
在这里插入图片描述
3.4 本章小结
本章简单介绍了编译的概念以及作用,较为详细地以hello.s汇编文本为参考,介绍了在C语言中的各种数据、各种操作以及对应的汇编指令。同时,还介绍了一些汇编指令的一些规则。从上述几个方面,以实例简要分析了编译器的行为。
(第3章2分)

第4章 汇编

4.1 汇编的概念与作用
汇编的概念:
汇编是指汇编器as将.s文件翻译成机器语言指令,并将这些指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中的过程。其中.o是二进制文件,包含main函数的指令编码。
汇编的作用:
将汇编代码转换为机器指令,将结果存放在.o文件中,使其在链接后可被机器识别并执行。
4.2 在Ubuntu下汇编的命令
命令:gcc hello.s -c -o hello.o
截图:
在这里插入图片描述
4.3 可重定位目标elf格式
Linux下生成elf文件命令:readelf -a hello.o > hello.elf
在这里插入图片描述
查看生成的hello.elf文件,分析其中的内容:
4.3.1 ELF头:
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统下字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,如ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移、节头部表中条目的大小和数量等。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。ELF头内容如下图所示。

4.3.2 节头表Section Headers:
记录各个节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息及对齐。目标文件的每一个节都有一个固定大小的条目。节头表内容如下图所示。
在这里插入图片描述
4.3.3 重定位节.rela.text:
重定位节,用来保存.text节里需要被修正的信息。调用外部函数或引用全局变量的指令都需要被修正,而调用局部函数的指令不需要被修正。另外,在可执行目标文件中不存在重定位信息。其中,Offset是需要被修改的引用节的偏移、Info包括symbol和type两部分(symbol在前四个字节,type在后四个字节)、symbol标识被修改引用应指向的符号、type为重定位类型、Attend是一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整、Name为重定向所到的目标名称。
在hello.s转换为hello.o的过程中,需要重定位的有.rodata的只读模式串、puts、exit、printf、atoi、sleep、getchar等。重定位节的内容如下图所示。在这里插入图片描述

4.3.4 符号表.symatb:
符号表,用来存放程序中定义和引用的函数、全局变量的信息。其中,Value是符号相对于目标节的起始位置偏移(可执行目标文件,则为绝对运行的地址)、Size为目标大小、Type说明是数据还是函数、Bind说明是本地符号还是全局符号。符号表的内容如下图所示。
在这里插入图片描述

4.4 Hello.o的结果解析
Linux下命令:objdump -d -r hello.o > hello.txt
在这里插入图片描述

查看生成的hello.txt文件,内容如下。
在这里插入图片描述
在这里插入图片描述

比较生成的hello.txt以及之前生成的hello.s,二者存在如下差异:
1.分支转移:
在hello.s中,跳转指令的目标地址使用的是段名称,例如.L2、.L3;在hello.txt文件中,跳转指令的目标地址使用的是具体地址,具体体现为二者地址差值。
在这里插入图片描述
在这里插入图片描述

2.函数调用:
在hello.s中,函数调用call指令紧接的是函数符号,如call puts@PLT;在hello.txt文件中,函数调用使用callq指令紧跟重定位条目指引的信息。

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

3.立即数格式:
在hello.s中,立即数都以十进制格式展示,如-20(%rbp),-32(%rbp);在hello.txt文件中,立即数都以十六进制格式展示,对应的如-0x14(%rbp),-0x20(%rbp)
在这里插入图片描述
在这里插入图片描述

4.全局变量的访问:
在hello.s中,全局变量通过.LC0(%rip)进行访问;在hello.txt文件中,通过0x0(%rip),全局变量需要重定位,需要以0x0进行初始化并添加重定位条目。

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

4.5 本章小结
本章简要介绍了汇编的概念和作用,并通过readelf、objdump等工具,生成可重定位目标elf格式文件、反汇编hello.txt文件,并较为详细地分析elf格式文件各部分内容、比较反汇编hello.txt及hello.s的区别。从上述几个方面,分析了汇编器的行为。
(第4章1分)

第5章 链接

5.1 链接的概念与作用
链接的概念:
链接,指链接器ld,将各种代码和数据片段收集并组合为一个单一文件的过程。此文件可以被加载到内存并执行。链接可以在多个阶段内执行:在编译时执行,即源代码被编译成机器代码的阶段;在加载时执行,即程序被加载器加载到内存并执行;在运行时执行,即由应用程序来执行。
链接的作用:
链接的重要性体现在它使得应用程序分离的编译过程成为了可能,可以把一个规模较大的应用程序和链接器分开进行编译,单独的部分修改时单独编译对应部分,不必整体重新编译,提高了规模较大的程序的编写效率。
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的格式
命令:readelf -a hello > hello1.elf
在这里插入图片描述

(文件名打错了,重新生成一下hello1.elf和hello.elf)
查看刚刚生成的hello1.elf文件,分析该可执行目标文件。
1.ELF头:
查看hello1.elf文件,与hello.o生成的hello.elf文件的ELF头包含信息的种类大致相同,其中hello1.elf的Type为EXEC(Executable file,可执行文件),Number of section headers为27,Number of program headers为12;hello.elf文件中的ELF对应项分别为REL,14,0。除此之外,程序的入口地址也由0x0更改为0x4010f0(其他不同不在此一一列举)
在这里插入图片描述

2.Section Headers:
查看hello1.elf文件的Section Hearder部分,其包含了文件中出现的各个节的语义(节的名称Name、类型Type、地址Address、偏移量Offset及大小Size等)。与hello.elf相比,其种类更多。
在这里插入图片描述

3.重定位节.rela.text:
在这里插入图片描述

4.Program Headers:
查看hello1.elf文件的Program Headers部分,此部分描述了系统执行程序需要的段或其他信息。
在这里插入图片描述

5.Dynamic section:
在这里插入图片描述

6.Symbol table.symtab:
查看hello1.elf文件中的Symbol table的.symtab部分,此部分是保存所有重定位符号定义和引用的信息。
在这里插入图片描述

5.4 hello的虚拟地址空间
通过在edb中查看,在Data Dump处可以得出hello的虚拟地址空间始于0x401000,结束语0x401ff0,如下图所示。
在这里插入图片描述
在这里插入图片描述
根据之前的Section Headers部分,在edb可以找到各个节的信息,如.text节,虚拟地址开始于0x4010f0,大小为0x145。
在这里插入图片描述
5.5 链接的重定位过程分析
命令:objdump -d -r hello > hello1.txt
截图:
在这里插入图片描述
通过分析hello与hello.o的不同,可以发现以下不同的地方:
1.hello反汇编代码有确定的虚拟地址,代表已经完成了重定位;hello.o反汇编代码的虚拟地址为0,代表未完成重定位过程。
在这里插入图片描述
在这里插入图片描述
2.hello反汇编代码中新增很多节及函数的汇编代码,这些节都有一定的功能和含义。
在这里插入图片描述
下表展示部分节及其描述。
节名称 描述
.init 程序初始化所需执行代码
.plt 动态链接-过程链接表
.dynamic 存放被ld.so使用的动态链接信息
.data 初始化的数据
.comment 一串包含编译器的NULL-terminated字符串
.interp 保存ld.so的路径
.note.ABI-tag Linux下特有的section
.hash 符号的哈希表
.gnu.hash GNU拓展的符号的哈希表
.dynsym 存放.dynsym节中的符号名称
.gnu.version 符号版本
.gnu.version_r 符号引用版本
.rela.dyn 运行时/动态重定位表
.rela.plt .plt节的重定位条目
.fini 程序正常终止时需要执行的指令
.eh_frame 程序执行错误时需要执行的指令
3.hello的重定位过程:
重定位节和符号定义链接器将所有类型相同的节合并在一起,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,以及赋给输入模块定义的每个和符号,当这一步完成时,程序中每个指令和全局变量都有唯一运行时的地址。
重定位节中的符号引用这一步中,链接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖与可重定位目标模块中重定位条目这一数据结构。
重定位条目当编译器遇到对最终位置位置的目标引用时,会生成一个重定位条目,代码的重定位条目放在.rela.text中。
重定位过程的地址计算算法如下图所示。
在这里插入图片描述
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。
在这里插入图片描述
hello执行流程可大致分为如下部分:开始执行部分,_start、_libc_start_main;执行main部分,_main、_printf、_exit、_sleep、_getchar;退出部分,exit。
其调用与跳转的各个子程序名或程序地址如下。
在这里插入图片描述
在这里插入图片描述
5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分为彼此相互独立的部分,在程序运行时才把它们链接在一起形成一个完整的程序,不像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
在调用共享库函数时,编译器无法预测函数运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,再用动态链接器在程序加载时解析它,这也称动态延迟绑定,通过got和plt实现(got是数据段的一部分,plt是代码段的一部分)
根据hello的ELF文件可知,got的其实位置为0x404000。
在这里插入图片描述
下图为程序运行前的got处内容。
在这里插入图片描述
下图为程序运行之后的got处内容。
在这里插入图片描述
通过两者对比可知,此处内容发生了改变。此处修改为动态链接器在解析函数地址时会用到的信息。
5.8 本章小结
本章简要减少了链接的概念和作用,分析了hello对应的ELF格式文件hello1.elf,并对比了hello1.elf与hello.elf的不同。在分析链接的过程中,回顾了重定位、动态链接、可重定位目标文件elf格式等与链接相关的内容。在加深对链接的理解的同时,可执行文件hello已经诞生。
(第5章1分)

第6章 hello进程管理

6.1 进程的概念与作用
进程的概念:
进程的经典定义是一个执行中程序的实例。在运行一个程序时,得到一个假象,好像程序是系统中当前运行的唯一程序,好像独占地使用处理器和内存,处理器好像是无间断地一条一条执行指令,代码和数据好像是系统内存中唯一的对象一样。
进程指程序的一次运行过程。更确切说,进程是具有独立功能的一个程序关于某个数据集和的一次运行活动,具有动态含义。同一个程序处理不同的数据就是不同的进程。进程是OS对CPU执行的程序的运行过程的一种抽象。
进程的作用:
进程的作用与其给应用程序提供的两个关键抽象有关,如下所示:
一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash的作用:
Shell提供了一个界面,用户访问此界面可以访问操作系统内核的服务。Shell实际上是一个命令解释器,它解释由用户输入的命令并且把它们送到内核。Shell有自己的编程语言用于对命令的编辑,它允许用户编写由shell命令组成的程序。Shell编程语言具有普通编程语言的很多特点,比如它也有循环结构和分支控制结构等,用这种编程语言编写的Shell程序与其他应用程序具有同样的效果。
Shell-bash的处理流程:
首先,从终端读取用户输入的命令;其次,将输入的命令(字符串)分割获取所需要的所有参数;之后,判断是否是内置命令(包括系统的命令、安装应用对应的命令等),若是则立即执行此命令,若不是再看是否是应用程序,是则调用相应的程序执行,不是则说明不是内置命令且路径无对应可执行文件,产生错误信息;最后,接受键盘的输入信号,根据信号进行相应的处理。
6.3 Hello的fork进程创建过程
用户在终端输入对应的指令(./hello 120L021905 郎朗),此时shell会读取输入的指令进行对应的如下操作:
判断hello不是一个内置的shell指令,调用应用程序,找到当前目录下的hello文件,准备执行hello文件;Shell自动调用fork函数为父进程创建一个新的子进程,该子进程会得到与父进程完全相同但是独立的一个副本(包括代码段、数据段、共享库及用户栈等),子进程与父进程是并发运行的,他们最大的不同是PID不同。在子进程运行期间,父进程默认等待子进程完成,期间子进程可以读取父进程打开的任何文件。
6.4 Hello的execve过程
当创建了一个子进程之后,子进程会调用exceve函数,在当前的子进程的上下文加载并运行一个新的程序(hello程序),步骤如下所示:
首先,删除已存在的用户区域。这一步骤会删除之前进程在用户部分已存在的结构;
之后,为hello程序创建新的数据(包括代码、数据、堆和栈等),这些新创建的数据都是私有的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt与.data区,其他的区域,如.bss区域(请求二进制零的,映射匿名文件)、栈和堆区域(请求二进制零的,初始的长度为0)等也都在此时完成相应的操作;
随后,映射共享区域。在此步骤中,如果hello程序要完成与共享对象的链接,则通过动态链接与hello程序关联,再通过映射到用户虚拟地址的共享区域;
最后,设置程序计数器。在这一步骤中,需要设置当前进程的上下文中的程序计数器,使得它指向代码区域的入口,从而能够正常运行程序。
6.5 Hello的进程执行
为了更好的理解hello的进程执行,下面介绍一些系统相关的进程抽象。
6.5.1 逻辑控制流
在系统中通常有许多其他程序在运行,进程可以向每个程序提供一种假象,好像它在独占地使用处理器。如果想用调试器单步执行程序,则会看到一系列的程序计数器PC的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流。
6.5.2 并发流与进程时间片
一个逻辑流的执行在时间上与另一个流重叠,称为并发流。注意,这里要求是时间上重叠,没有必要完全重合。一个进程执行它的控制流的一部分的每一时间段叫做时间片,这些时间片可能是部分顺序的,但是只要整体上是重叠的即可。
6.5.3 用户模式与内核模式
为了使操作系统内核提供一个无懈可击的进程抽象,处理器必须提供一种机制,限制一个应用可以执行的之类以及它可以访问的地址空间范围。处理器通常是用某个控制寄存器中的一个模式位提供此功能,该寄存器描述了进程当前向右的特权。当设置了模式位时,进程就运行在内核模式中。当没有设置模式位时,进程就运行在用户模式中。
内核模式下,进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置;用户模式下,进程不允许执行特权指令(如停止处理器、改变位模式、发起I/O操作等),不允许直接引用地址空间中内核区内的代码和数据,需要通过系统调用接口间接进行访问,
6.5.4 上下文信息
内核为每个进程维持了一个上下文。上下文是内核重新启动一个被抢占的进程所需要的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构(如描述地址空间的页表、包含有关当前进程信息的进程表、包含进程已打开文件的信息的文件表)。
6.5.5 进程调度
在进程执行到某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就是进程调度,由内核中的调度器(代码)进行处理。当内核选择一个新的进程运行时,则内核调度了这个进程。
6.5.6 上下文切换
在内核调度一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。
保存当前进程的上下文、恢复某个先前被抢占的进程被保存的上下文、将控制传递给这个新恢复的进程。
当内核代表用户执行系统调用时,可能会发生上下文切换。如果系统调用因为等待某个事件发生而阻塞,则内核可以让当前进程休眠,切换到另一个进程;另外,中断可能引发上下文切换。
在这里插入图片描述
6.5.7 用户态与核心态转换
为了能够让处理器安全运行,需要限制应用程序可执行指令能访问到的地址范围。根据此,划分了用户态与核心态。核心态有最高的访问权限,处理器以一个寄存器描述当前进程的特权。进程只有故障、中断或者陷入系统调用时才会得到内核访问权限,其他情况下始终再用户权限内,从而保证了系统的安全性。
经过上述几个进程抽象介绍,下面再进行hello进程的分析:
在进程调用execve函数后,进程为hello程序分配了它对应的地址空间,将hello的对应数据分配给对应的地址空间的部分。在用户模式下,hello程序输出hello 120L021905 郎朗,紧接着hello程序调用了sleep函数,引发了上下文切换,从而进入内核模式,内核处理sleep函数对应的休眠请求释放该进程。随后,内核进行上下文切换,当前的进程控制权被转交给其他的进程。当收到了一个中断信号时,出发上下文切换,进入内核模式处理中断,将hello程序唤醒,继续执行自己的逻辑流。
6.6 hello的异常与信号处理
首先,在程序中不进行任何外部输入,在最后需要以回车结束,结果如下:
在这里插入图片描述
其次,在输出过程中输入多个回车(三个),则会在最后多出两个空白的行,结果如下:
在这里插入图片描述
之后,在输出信息的中途,通过键盘输入Ctrl+C,shell收到SIGINT信号,shell结束并回收了hello进程,结果如下:
在这里插入图片描述
然后,在输出信息的中途,通过键盘输入Ctrl+Z,shell收到SIGSTP信号,在控制台输出提示信息,挂起当前hello进程,结果如下:
在这里插入图片描述
在运行程序中,被挂起的进程可以有jobs进行查询信息,结果如下:
在这里插入图片描述
而通过kill(后面有部分参数)可以将目标进程杀死,结果如下:
在这里插入图片描述
通过ps可以进程状态,发现上一步通过kill杀死进程,结果如下:
在这里插入图片描述
通过pstree可以查看当前所有进程,并以树状图进行显示,结果如下:
在这里插入图片描述
通过fg,可以将被挂起的进程重新调回,继续运行,结果如下:
在这里插入图片描述
最后,在程序输出信息的过程中不停乱按,shell将会把这些输入缓存到stdin,当getchar的时候读出一个‘\n’结尾的字符串,结束hello程序,stdin中其他字符串被作为下一次shell命令行输入(可以解释之前输入多个空格,在最后多出两行的现象),结果如下:
在这里插入图片描述

6.7本章小结
本章简要介绍了进程的概念与作用、shell-bash的作用与处理流程、由fork函数创建hello进程的过程、execve过程及hello进程执行的相关概念,并通过实际操作,相应分析hello程序对异常信号的处理。总体来说,进程是程序的实体,换句话来说,进程是运行起来的程序。在本章,对进程、异常处理、信号等有进一步认识,而可运行的程序在此时诞生了。
(第6章1分)

第7章 hello的存储管理

7.1 hello的存储器地址空间
逻辑地址:
逻辑地址是由程序产生的与段相关的偏移地址部分,由选择符和偏移量两部分组成。逻辑地址也称相对地址,以hello程序为例,hello.asm的相对偏移地址是逻辑地址。
线性地址:
线性地址是逻辑地址向物理地址转化过程中的一步,是逻辑地址经过段机制转化得到的,是处理器可寻址空间的地址,用来描述程序分页信息的地址,分页机制中线性地址作为输入。
虚拟地址:
虚拟地址就是线性地址,是保护模式下程序访问存储器使用的地址,是现代系统的一个抽象概念。
物理地址:
物理地址是加载到内存地址寄存器的地址,内存单元真正的地址。CPU通过地址总线寻址,找到真实的物理内存对应地址。
在这里插入图片描述
以hello程序为例,main函数的入口地址为0x401125,此地址即为逻辑地址的偏移量部分,偏移量加上代码段的段地址即可得到main函数的虚拟地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由两部分组成,段标识符:段内偏移量。段标识符是一个16位场的字段组成,前13位是索引号,后3为包含硬件细节。所有的段由描述符描述,多个描述符可以组成一个数组(段描述表),其中段描述符的BASE字段对转化十分重要。
在这里插入图片描述
BASE字段描述了段开始位置的线性地址,可以利用索引号从GDT(全局段描述表,T1=0)或LDT(局部段描述符表T1=0)中获取。
整个变换过程可以分为三个部分:根据段选择符的T1值得到要转换的段在GDT(0)中还是LDT(1)中;从段选择符中取出对应的段描述符,确定BASE基地址;BASE加上段偏移量得到线性地址。
段式管理以段位单位,每个段会分配到一个连续的内存区,这些区域的大小不一,进程中的各个段可以不连续的存放在不同的分区。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址就是虚拟地址,一般通过分页机制完成虚拟地址到物理地址的映射,而分页机制是对虚拟地址内存空间进行分页。
系统将虚拟页作为进行数据传送的单元,在Linux下每个虚拟页大小为4KB。物理内存被分为物理页,MMU(内存管理单元)进行地址翻译,它使用页表将虚拟页映射到物理页上。
在这里插入图片描述
CPU中的一个控制寄存器PTBR指向当前页表。n位的虚拟地址包含两个部分,一个p位的虚拟页面偏移VPO,一个n-p位的虚拟页号VPN。MMU利用VPN来选择适当的PTE。根据PTE我们可以得到虚拟页的信息。如果虚拟页已经缓存,则直接将页表条目的物理页号和VPO串联得到相应的物理地址;如果未缓存,则触发缺页故障,调用程序将虚拟页从磁盘中加载到内存,在执行后续指令。
在这里插入图片描述
页式管理并没有要求程序段和数据连续存储,能够充分地利用内存空间,但是依靠一定的硬件设备,并且会增加一定的系统开销,如缺页情况下的处理。
7.4 TLB与四级页表支持下的VA到PA的变换
此部分以Intel Core i7 CPU为例,阐述VA到PA的变换过程。其中,该CPU的相关参数如下所示:
虚拟地址空间48位、物理地址空间52位、TLB四路十六组相连、L1、L2、L3块大小为64字节、L1与L2都是八组相连、L3十六组相连、页表大小4KB,四级页表,页表条目PTE为8字节。
TLB即Transfer Look-aside Table,为高速地址变址换从,用于加速对页表的访问。对于一次存取,TLB会有三种情况:hit直接访问内存取数据、miss但在内存中有对应页、miss内存中没有对应页发生缺页。
CPU产生虚拟地址VA传送给MMU,MMU使用VPN高位作为TBLT和TLBI,向TLB中寻找匹配。如果命中,则得到物理地址PA;如果未命中,则MMU查询页表,CR3确定第一级页表起始地址,VPN1确定第一级页表偏移量,查询出PTE,直至在第四级页表中找到PPN,与VPO组合成物理地址PA,添加至PLT。
在这里插入图片描述
一到三级页表中存放的数据是指向下一级页表的首地址,并非物理页号,而第四级页表装的就是物理页号,因此需要查询到第四级页表,再进行链接。
在这里插入图片描述
7.5 三级Cache支持下的物理内存访问
Cache的访问需要把一个物理地址分为标记、组索引、块偏移三个部分。通过组索引找到地址对应Cache的组号,再通过标记及Cache有效位来判断Cache是否保存有效内容。如果匹配成功则命中,通过块偏移读取数据;如果匹配失败则不命中,在下一级存储空间进行寻找。在L1找不到,则去L2;在L2找不到则去L3;在L3找不到则去主存。
Core i7 CPU的L1 Cache为32kb,八组相连,每个块大小为64字节。L1 Cache共有64组,而Core i7 CPU的物理地址为52位,则L1 Cache划分为标记位大小为40,组索引大小为6,块偏移大小为6。
MMU将虚拟地址转化为物理地址后,计算机通过读取组索引在L1搜索对应组,以标记位进行匹配。若匹配成功且有效位为1(有效),则将块偏移指向的块内容传输给CPU;否则未命中,去下一级缓存重复上述匹配操作。
当找到对应内容后,需要将此内容进行缓存,即写入Cache中。以L1 Cache为例,在L1未匹配到,L2中匹配到内容。如果L1内有空闲块,则将内容写入空闲块中;如果没有空闲块,需要进行替换。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,分配唯一的PID。创建mm_strcut、区域结构和页表的原样副本,进行虚拟内存创建。它将两个进程中的每个页面都标记为只读权限,将两个进程中的每个区域结构都标记位私有的写时赋值。当两个进程中的任何一个后来进行写操作时,写时复制机制会创建新的页面,也就为每个进程保存了私有的地址空间。
其中,写时复制是私有对象使用的一种技术。一个私有对象开始声明周期的方式基本和共享对象一样,在物理内存中只保存私有对象的一份副本。对于每个映射私有对象的进程,相应私有区域的页表条目都被标记为只读,且区域结构被标记位私有的写时复制。只要没有进程试图写它自己的私有区域,它们就可以继续共享物理内存中对象的一个单独副本。
在这里插入图片描述
7.7 hello进程execve时的内存映射
execve函数加载并运行可执行文件需要以下几个步骤:
删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构;
映射私有区域。为新程序的代码、数据、bbs和栈区域创建新的区域结构。所有这些新的区域结构都是私有的、写时复制的。代码和数据区域被映射为可执行文件的.text区和.data区,bss区是请求二进制零的,映射到匿名文件,大小包含在可执行文件中。栈和堆区域也是请求二进制零的,初始长度为0;

在这里插入图片描述
映射共享区域。如果hello程序与共享对象(或目标)链接,如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内;
设置程序计数器PC。execve做的最后一件事是设置当前进程上下文中程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障是当引用一个相应的虚拟地址,但虚拟地址不在内存有缓存时,会触发缺页故障。
当发生缺页故障时,内核会调用缺页处理程序。首先,检查虚拟地址是否合法,如果不合法则触发一个段错误,程序终止;其次,检查进程是否有读、写或执行该区域页面的权限,如果没有权限则触发保护异常,程序终止;最后,内核会根据规则选择一个牺牲页。如果牺牲页被修改过,则将其交换出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。
在这里插入图片描述
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆heap。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(更高的地址)。对于每个进程,内核维护着一个变量brk,指向堆顶。
在这里插入图片描述
分配器将堆视为一组不同大小的块的集合来维护。每个块是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显示地保留为供应用程序使用。空闲块可以分配。空闲块保持空闲,直至被显示应用分配。一个已分配的块保持已分配状态,直至被释放,要么是应用程序显示释放,要么是内存分配器隐式释放。
分配器有两种基本风格,都要求应用显示地分配块。它们不同之处在于由哪个实体来负责释放已分配的块。显示分配器要求应用显示地释放任何已分配的块。例如C标准库的malloc,C语言通过malloc函数分配一个块,通过free函数来释放一个块。C++中new和delete与之对应;隐式分配器要求检测一个已分配块何时不再被程序使用,则释放此块。Lisp、ML及Java之类的高级语言依靠其释放已分配的块。
隐式空闲链表:
在这里插入图片描述
带边界标记的隐式空闲链表的每个块由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成。空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆的所有块,间接地遍历整个空闲块的集合。
隐式空闲链表的优点是简单,显著的缺点是任何操作的开销较大。
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大可以放置所请求块的空闲块。分配器执行这种搜索的方式由放置策略决定,常见的策略有首次适配、下一次适配和最佳适配。一旦分配器找到一个匹配的空闲块,需要做分配空闲块中多少空间的决定。可以选择用整个空闲块,也可以将空闲块分割为两个部分,一部分使用,另一部分处于空闲状态。另外,分配器也可以合并一些相邻的空闲块。
显示空闲链表:
隐式空闲链表由于块分配与块堆成线性关系,因此对于通用的分配器,它是不合适的。更好的处理方法是将空闲块组织为某种显示的数据结构。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个祖先pred和后继succ指针。
在这里插入图片描述

在显示空闲链表中,可以采用后进先出LIFO的顺序维护链表,将最新释放的块放置在链表的开始处。使用LIFO 的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,释放一个块可以在常数时间内完成;也可以采取地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址,释放一个块需要线性时间的搜索来定位合适的前驱,平衡点在于按照地址排序的首次适配比LIFO顺序的首次适配有更高的内存利用率,接近最佳适配的利用率。
一般而言,显示链表的缺点是空闲块必须足够大,以包含所有需要的指针以及头部和可能的脚部,潜在地提高了内部碎片的程度。
分离的空闲链表:
一个使用单向空闲块链表的分配器需要与空闲块数量呈线性关系的时间来分配。通常减少分配时间的方法是分离存储,即维护多个空闲链表,每个链表中的块有大致相等的大小。有两种基本的分离存储方法,分别是简单分离存储和分离适配。
简单分离存储,每个大小类的空闲链表包含大小相等的块,每个块的大小就是这个大小类中最大元素的大小。此方法有很多优点:分配和释放块都是很快的常数时间操作、每个片都是大小相等的块,不必分割与合并、每个已分配块不需要头部和脚部。但是,也有显著的缺点,容易造成内部和外部碎片,某些引用模式会引起极多的外部碎片。
分离适配,分配器维护着一个空闲链表的数组。每个空闲链表是和一个大小类相关联的,并且被组织成某种类型的显示或隐式链表。每个链表包含潜在的大小不同的块,这些块的大小是大小类的成员。分离适配方法是一种常见的选择,C标准库提供的GNU malloc包采用的是这种方法。搜索时间减少了,内存利用率也得到了了改善。
7.10本章小结
本章简要介绍程序是如何组织存储器的。以hello程序为例,介绍了其存储器的地址空间,简单区分了四种地址空间并分析了它们的相互转换。除此之外,还以Intel Core i7 CPU为环境,介绍了hello程序的VA到PA的变换、三级Cache下的物理内存访问、进程中不同阶段的内存映射、在地址访问时可能出现的缺页故障及缺页中断处理,最后介绍了童泰存储分配管理。在本章,hello程序已经渐渐演变为一个鲜活的进程了。
(第7章 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的IO设备都被模型化为文件,而所有的输入和输出都被当做相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得所有的输入和输出都能以一种统一且一致的方式来执行,即打开文件、改变当前的文件位置、读写文件、关闭文件。
8.2 简述Unix IO接口及其函数
Unix I/O接口:
1.打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需要记住这个描述符。
2.Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO/STDOUT_FILENO和STDERR_FILENO,它们可以用来代替显示的描述符值。
3.改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显示地设置文件的当前位置为k。
4.读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k≥m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。
5.关闭文件:当应用程序完成了对文件的访问之后,它就通知内核关闭这个文件。作为相应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭打所有打开的文件并释放它们的内存资源。
Unix I/O函数:
1.打开文件函数:int open(char *filename,int flags,mode_t mode);调用open函数打开一个存在的文件或是创建一个新文件(不存在该文件)。Open函数将filename转换为一个文件描述符,返回该描述符。其中flags指明进程如何访问此文件(O_RDONLY为只读,O_WRONLY为只写,O_RDWR为可读可写,O_CREAT为文件不存在时创建它的一个截断的空文件,O_TRUNC为文件存在时截断它,O_APPEND为每次写操作前,设置文件位置到文件的结尾处),而mode则指定新文件的访问权限。
2.关闭文件函数:int close(int fd);调用close函数,关闭一个打开的文件,返回操作结果。
3.读文件函数:ssize_t read(int fd,void *buf,size_t n);read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。
4.写文件函数:ssize_t write(int fd,const void *buf,size_t n);write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
5.off_t lseek(int fd,off_t offset,int whence);调用lseek函数,应用程序能够显示地修改当前文件的位置。如果成功,返回新的文件偏移量;否则返回-1。
8.3 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;}

由于传递参数不确定个数,因此用…表示。函数中的va_list实际上是typedef后的char*,arg变量是得到printf函数的第二个参数,按照按时fmt结合参数arg生成格式化后的字符串并返回其长度。
观察到printf函数内部调用了两个函数,分别是vsprintf和write。

int vsprintf(char *buf, const char *fmt, va_list args)
{
    char *p;
    chartmp[256];
    va_listp_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的作用是格式化。它接受确定输出格式的字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

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

write函数将栈中参数赋值给寄存器,ecx中存放字符个数,ebx则存放第一个字符的地址。
查看sys_call的实现:

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中。显示芯片按照一定的刷新频率逐行读取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;
}

getchar函数的实现是通过系统函数read实现的。getchar通过read函数从缓冲区读入一行,并返回读入的第一个字符。若读入失败,则返回EOF。
异步异常-键盘中断的处理:当用户通过键盘输入时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得扫描码,再将其转换为ASCII码,保存到系统的键盘缓冲区中。
getchar等调用read系统函数,通过系统调用读取按键ASCII码,直到接受到回车键才返回。
8.5本章小结
本章简要介绍了Unix的I/O管理,介绍了I/O设备管理方法、接口及其函数,深入到系统函数分析了printf、getchar函数的具体实现。Unix的I/O将输入和输出都归为文件操作。学习Unix的I/O操作有助于理解其他的系统概念。至此,hello程序算是走出了一个完整的人生。
(第8章1分)

结论

用计算机系统的语言,逐条总结hello所经历的过程:
源程序生成:现代的IDE自动生成优美的hello.c程序,他以C语言的形式展示在我们面前。
预处理过程:在预处理器cpp的作用下,hello.c的外部头文件内容被加入到文本中,完成了字符串的替换,生成了hello.i文件。
编译过程:在编译器ccl的词法分析和语法分析下,符合规定的指令被翻译成了汇编代码,组成了hello.s文件。
汇编过程:在汇编器as的作用下,hello.s中的汇编代码被翻译成了机器可懂的机器指令,生成了目标文件hello.o文件。
链接过程:在链接器ld的整合下,hello与动态链接库等部分整合为一个整体,即可执行目标文件hello。
加载、运行过程:在Linux的shell下输入./hello 120L021905 郎朗 1,shell调用系统函数fork创建子进程,通过调用execve函数将代码和数据加载到虚拟内存空间中间,hello程序开始运行。
执行程序指令过程:hello进程被调度时,CPU为hello进程分配对应的时间片,hello执行逻辑控制流,感觉在独占地使用CPU的全部资源,执行完当前指令,读取下一条指令,程序计数器PC更新,循环往复,执行完hello程序的指令。
访问内存过程:内存管理单元MMU将逻辑地址转换为线性地址,即虚拟地址,再将虚拟地址转换为物理地址。通过高速缓存系统,hello程序访问内存或磁盘(当前不在缓存中)的数据。
动态申请内存过程:hello程序调用了printf函数,而printf函数会调用malloc函数,通过动态内存分配器向堆结构中申请内存,变为已分配状态。在程序运行过程中,也可以调用free函数,通过动态内存分配器向堆结构释放指定的内存空间,变为空闲状态。在程序结束时,会自动释放申请的内存空间。
信号处理过程:hello进程时时刻刻等待着信号。在hello进程运行过程中,如果以键盘输入Crtl+C、Ctrl+Z,shell会调用信号处理函数,分别停止hello进程、挂起hello进程,通过shell输入kill、fg则可以分别杀死hello进程、唤起被挂起的hello进程。
进程终止、回收过程:shell父进程等待并回收hello子进程,内核删除为hello子进程创建的所有数据结构。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法:
在本次大作业中,我收获的东西很多,对计算机系统有了进一步的认知。
首先,计算机系统中一个十分重要的概念–抽象。在进程中,逻辑控制流是抽象,通过上下文切换,让进程感觉是独占地使用硬件;虚拟内存是抽象,为进程提供了私有独立的地址空间,互不干扰;I/O也是抽象,将输入输出操作抽象为文件操作。抽象并不像从前认为的那么复杂,它通过一系列规则、操作,能够很好地满足我们的需求,是十分有用的概念。
其次,计算机系统的设计是十分精妙的。通过Cache的设计,大大减少了计算机访问内存的次数,提高了程序的运行效率;通过动态链接,可以有效地减少程序员的工作负担。
另外,针对缓冲区溢出,随机偏移栈、金丝雀的设计十分有用。通过少量开销,大大提高了黑客的入侵难度,有效地保护计算机系统。我们设计程序的时候,也要时刻注意细节,在能力范围内尽可能保证程序的安全性。
十六进制是0~9和A、B、C、D、E、F的组合,那么是否可以扩展为三十二进制呢?10种数字和26种英文字符取前22种组成三十二进制的组合,如此可能会提高CPU处理的复杂度,那么是否可以再新增一个翻译设备,将三十二进制转换为需要的十六进制和二进制,通过三十二进制,可以节省地址表示位数,为以后可能需要的更大的地址空间做准备。
(结论0分,缺失 -1分,根据内容酌情加分)

附件

文件名 作用
hello.i 预处理器cpp修改后的源程序,分析预处理器的行为
hello.s 编译器ccl生成的编译程序,分析编译器的行为
hello.o 汇编器as生成的可重定位目标程序,分析汇编器的行为
hello.txt 使用objdump工具生成的反汇编代码文本(汇编处)
hello1.txt 使用objdump工具生成的反汇编代码文本(链接处)
hello.elf hello.o的elf格式
hello1.elf hello的elf格式
hello 链接器ld生成的可执行目标程序,分析链接器的行为

(附件0分,缺失 -1分)

参考文献

为完成本次大作业你翻阅的书籍与网站等
[1] Randal E.Bryant, David O’Hallaron. 深入理解计算机系统[M]. 机械工业出版社.2018.4
[2] https://blog.csdn.net/weixin_44176696/article/details/112536115?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165233082116782395339867%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=165233082116782395339867&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_click~default-1-112536115-null-null.142v9pc_search_result_control_group,157v4control&utm_term=TLB&spm=1018.2226.3001.4187
[3] https://blog.csdn.net/q5781800/article/details/84245437
[4]
https://blog.csdn.net/m0_50668851/article/details/114691880?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165235564516782390575274%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=165235564516782390575274&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_click~default-1-114691880-null-null.142v9pc_search_result_control_group,157v4control&utm_term=lseek%E5%87%BD%E6%95%B0&spm=1018.2226.3001.4187
[5] https://www.cnblogs.com/pianist/p/3315801.html
(参考文献0分,缺失 -1分)

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

深海质粒ABCC9

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值