ToB企服应用市场:ToB评测及商务社交产业平台

标题: 哈工大计算机系统2024大作业——Hello的程序人生 [打印本页]

作者: 小小小幸运    时间: 2024-7-12 01:17
标题: 哈工大计算机系统2024大作业——Hello的程序人生

计算机系统


大作业



题     目  程序人生-Hellos P2P 

专       业         人工智能       

学     号        2022112040      

班   级          2203601          

学       生         郄东昕       

指 导 教 师            吴锐     







计算机科学与技术学院

20245


摘  要

        本篇论文的目标是解释C语言程序怎样从源代码转换为可执行文件。以hello.c程序为例,本文详细分析了计算机在天生hello可执行文件的预处理、编译、汇编、链接、进程管理等整个生命周期。Hello从最开始的C语言源代码,会先颠末他“机生”的第一步——预处理;接着会继续变化,从一个青涩的.i文件变化成更能让呆板理解的.s汇编文件;随着Hello的一步一步成长,他会颠末汇编、链接等一系列的动作处理,变成一个可执行文件。这也标记着它即将迈入“机生”的一个新阶段。在下一个阶段中,它会和操作系统举行交谈,操作系统像它的伯乐一样,给他开辟进程,提供虚拟内存和独立的地点空间;给它分别时间片、逻辑控制流来让它操作系统上畅游,末了随着进程的结束,停止这短暂而辉煌的机生。
        本文不但理论上探讨了Hello一生中所履历的具体阶段,还现实演示了它们的操作和结果,阐述了计算机系统的工作原理和体系布局,帮助读者更深入地理解和掌握C语言程序的编译和执行过程。

关键词:计算机系统;计算机体系布局;汇编语言;链接        

目  录


第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中心结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果分析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果分析

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果分析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

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

5.4 hello的虚拟地点空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

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

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.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献

第1章 概述

1.1 Hello简介

HelloWorld想必是每一个程序员的发蒙程序,在诸如Pycharm,CodeBlocks这样的IDE中。我们创建新项目时,项目文件中默认的程序便是输出“Hello,world!”。这个看似非常简朴的程序,实在是早先第一个实现的P2P。
P2P:并不是Peer-to-peer的简称哦!
这里面的P2P指的是From Program to Process。指从hello.c(Program)变为运行时进程(Process)。要让hello.c这个C语言程序运行起来,必要先把它变成可执行文件,这个变化过程有四个阶段:预处理,编译,汇编,链接,编写完成的hello.c文件,首先颠末预处理器预处理天生hello.i文件;再颠末编译器编译,天生汇编代码文件hello.s;再颠末汇编器翻译成一个重定位目标文件hello.o;末了使用链接器将多个可重定位目标文件组合起来,形成一个可执行目标文件hello。(可指定为.out文件)
完成后就得到了可执行文件,然后就可以在shell中执行它,shell会给它分配进程空间。
020:即From Zero to Zero。指最初内存并无hello文件的相关内容,shell用execve函数启动hello程序,把虚拟内存对应到物理内存,并从程序入口开始加载和运行,进入main函数执行目标代码,程序结束后,shell父进程回收hello进程,内核删除hello文件相关的数据布局。在execve函数执行hello程序后,内核为其映射虚拟内存、分配物理内存;程序开始执行,内核为程序分配时间片执行逻辑控制流。当hello运行结束,由shell回收hello进程,删除有关的数据布局。
1.2 环境与工具

硬件环境:
处理器:12th Gen Intel(R) Core(TM)i7-12700H   2.70 GHz
机带RAM:16.0GB
系统类型:64位操作系统,基于x64的处理器
软件环境:Windows11 64位 版本号24H2,VMware,Ubuntu 18.04 LTS
开辟与调试工具:Visual Studio 1.89.0;vim,gidit ,objdump,edb,gcc,readelf等开辟工具
1.3 中心结果

列出你为编写本论文,天生的中心结果文件的名字,文件的作用等。
hello.c:原始hello程序的C语言代码
hello.i:预处理过后的hello代码
hello.s:由预处理代码天生的汇编代码
hello.o:二进制目标代码
hello:举行链接后的可执行程序
hello_asm.txt:反汇编hello.o得到的反汇编文件
hello1_asm.txt:反汇编hello可执行文件得到的反汇编文件

1.4 本章小结

本章首先先容了hello的P2P,020流程,包括流程的设计思绪和实现方法;然后,详细分析了本实行所需的硬件配置、软件平台、开辟工具以及本实行天生的各个中心结果文件的名称和功能。
第2章 预处理

2.1 预处理的概念与作用

2.1.1预处理概念
预处理的概念:顾名思义,预处理是指在源程序被编译器处理之前,cpp根据源文件中的宏定义、条件编译等命令对源文件作以修改,执行一些预处理命令,如宏替换,头文件包含,条件编译,注释删除等。请注意,此过程发生在天生二进制代码之前。
2.1.2预处理作用
作用:总的来说,预处理阶段的作用是为编译器提供一个颠末处理的源代码文件,以便举行后续的编译工作。这个过程并不对程序的源代码举行分析,但它把源代码分割或处理成为特定的单位。
2.2在Ubuntu下预处理的命令

在终端输入gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i ,回车,这样便在原目次天生了hello.i文件。

2.3 Hello的预处理结果分析

在Linux下我们使用gedit打开hello.i文件,我们对比了源程序和预处理后的程序。结果显示,观察发现,此中的注释已经消失,前一部分的代码为被加载到程序中的头文件;程序的末了一部分与hello.c中的main函数完全相同。除了预处理指令被扩展成了三千多行之外,源程序的其他部分都保持不变,分析.c文件的确是被修改过了。

图2.3-1 hello.i文件内容一览

在main函数代码出现之前的大段代码源自于的头文件<stdio.h>  <unistd.h>  <stdlib.h> 的依次睁开。

以 stdio.h 的睁开为例:#include指令的作用是把指定的头文件的内容包含到源文件中。stdio.h是“standard input & output”(尺度输入输出库)的头文件,它包含了用于读写文件(scanf())、尺度输入输出(printf())的函数原型和宏定义等内容。
当预处理器碰到#include<stdio.h>时,它会在系统的头文件路径下查找stdio.h文件,一般在/usr/include目次下,然后把stdio.h文件中的内容复制到源文件中。stdio.h文件中大概另有其他的#include指令,比如#include<stddef.h>或#include<math.h>等,这些头文件也会被递归地睁开到源文件中。
预处理器不会对头文件中的内容做任何计算或转换,只是简朴地复制和替换。

2.4 本章小结

本章讲述了在linux环境中,怎样用命令对C语言程序举行预处理,以及预处理的含义和作用,接着以hello.c为例,演示了在Ubuntu下怎样预处理程序,并对结果举行分析。通过分析,我们可以发现预处理后的文件hello.i包含了尺度输入输出库stdio.h的内容,以及一些宏和常量的定义,另有一些行号信息和条件编译指令。
第3章 编译

3.1 编译的概念与作用

3.1.1 编译的概念
编译是指将人类可读的源代码翻译成汇编代码的过程。编译器通过前端举行词法分析、语法分析等,然后颠末优化器举行代码优化,最终由后端将代码翻译汇编代码。编译的过程包括翻译、优化、检查和天生可执行文件等步调,以确保程序的准确性和性能。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到天生汇编语言程序。
3.1.2 编译的作用
编译的作用是产生汇编语言文件,并交给呆板执行。除此之外,编译器另有一些其他功能,例如语法检查等。
3.2 在Ubuntu下编译的命令

打开终端,输入 gcc -m64 -no-pie -fno-PIC -S hello.c -o hello.并回车,结果如下:

图3.2-1 天生hello.s的终端截图

3.3 Hello的编译结果分析

3.3.0先备知识——数据布局与算术操作
在分析下面的汇编代码之前,我们必要先了解数据存储的格式以及寄存器的存储布局,Intel数据类型令16bytes为字,32bytes为双字,各种数据类型的巨细一级寄存器的布局如下所示:


变量类型

Intel数据类型

汇编代码后缀

巨细(字节)

char

字节

b

1

short


w

2

int

双字

l

4

long

四字

q

8

char *

四字

q

8

float

单精度

s

4

double

双精度

l

8

表3.3.0-1 数据类型表

图3.3.0-2 寄存器字长表示图
此外,对于算术运算,汇编语言的规定如下:
INC D

DEC D

NEG D

NOT D

D←D + 1

D←D - 1

D← -D

D← ~D

加1

减1

取负

取补

ADD S, D

SUB S, D

IMUL S, D

D←D + S

D←D - S

D←D * S




表3.3.0-2 汇编语言基本算术操作

例如下图,下图的操作便是将rsp中的值减去立即数32。至于立即数则会在后文详细先容。
 

跳转指令会根据条件码当前的值来举行相应的跳转。比较常见的是直接跳转,在hello.s中也有体现,如下图所示。cmpl指令判定寄存器中的值和立即数4的巨细关系,设置条件码,再举行je。je的含义是jump if equal,也就是说,如果此时的条件码所表示含义为相等,则会跳转到相应的.L2指令行。因此,跳转指令用来实现条件分支。


call指令用来举行函数的调用。如下图所示的示例,call调用了puts函数和exit函数。call指令会先将函数的返回地点压入运行时栈中,之后跳转到相应的函数代码段举行执行。执行结束通过ret指令返回。

图3.3.0-4 call指令调用实例


编译过程是整个过程构建的核心部分,编译乐成之后,源代码会从文本形式转换为呆板语言。
我们使用vim打开hello.s,使用vim而不使用系统自带gedit的缘故原由是,vim可以或许主动高亮差别种类的汇编代码。接下来我们将对hello.s中出现的汇编指令举行详细的先容。
 

图3.3.0-6 使用vim打开hello.s

3.3.1 文件信息记载
 

图3.3.1-1 hello.s开头信息

首先是记载文件相关信息的汇编代码。第一部分的汇编代码有一部分是以.作为开头的代码段。这些代码段是引导汇编器和毗连器工作的伪指令。这段代码对我们来说没有什么意义,通常可以忽略这些代码,但对汇编器和毗连器缺是非常紧张的,为之后链接过程使用。伪代码的具体含义见下表。

伪代码

含义

.file

声明源文件(此处为hello.c)

.text

声明代码节

.section

文件代码段

.rodata

只读文件(Read-only)

.align

数据指令地点对齐方式(此处为8对齐)

.string

声明字符串(此处声明白LC0和LC1)

.globl

声明全局变量

.type

声明变量类型(此处声明为函数类型)

表3.3.1-2 hello.s伪代码具体含义

3.3.2 对局部变量的操作
hello中涉及局部变量操作的地方主要是源程序中的第10行int i。

图3.3.2-1 hello.c截图

在对应的汇编语言中,局部变量会被存储在栈上。当进入主函数main时,会在栈上申请一段空间来供局部变量使用。当局部变量使用完毕之后,这段申请空间会被释放。如下图所示,在红框中,栈指针向下移动了32个字节,在栈中为局部变量i保留了空间。

3.3.3 对字符串常量的操作
在main函数之前前,在.rodata处的.LC0和.LC1已经存储了字符串常量,标记该位置是代码是只读的。
在main函数中使用字符串时,得到字符串的首地点(leaq相当于转移操作),如下图红框。

3.3.4 对立即数的操作
在汇编语言中,立即数用“$”后加数字表示,如下图所示。

3.3.5 赋值操作
赋值操作很简朴,用movq指令即可,例如将a寄存器的值赋值给b寄存器,用movq a b(以8字节为例)。比如对局部变量i的赋值:




3.3.6 参数传递——对main的参数argv的传递
在main函数的开始部分,由于后面还会使用到%rbp数组,以是先将%rbp(帧指针)压栈生存起来。在下图红框地区,代码通过mov指令将栈指针镌汰32位,然后分别将%rdi和%rsi的值存入栈中。由此我们知道,%rbp-20和%rbp-32的位置分别存了argv数组和argc的值。

3.3.7 对数组的操作
对数组的操作,都是先找到数组的首地点,然后加上偏移量即可。例如在main中,调用了argv[1]和argv[2],在汇编代码中,每次将%rbp-32的的值即数组首地点传%rax,然后将%rax分别加上偏移量24和16,得到了argv[1]和argv[2],再分别存入对应的寄存器%rsi和%rdx作为第二个参数和第三个参数,之后调用printf函数时使用。调用完printf后同样,在偏移量为32时,取得argv[3]并存入%rdi作为第一个参数在调用函数atoi使用。

3.3.8 对函数的调用与返回
(1)main函数
参数传递:该函数的参数为int argc,,char*argv[]。
函数调用:通过使用call内部指令调用语句举行函数调用,而且将要调用的函数地点数据写入栈中,然后主动跳转到这个调用函数内部。main函数里调用了printf、exit、sleep函数。
局部变量:使用了局部变量i用于for循环。具体局部变量的地点和值都在前面阐述过。
(2)printf函数
汇编代码如下图:
 


参数传递:printf函数调用参数argv[1],argv[2]。
函数调用:该函数调用了两次。第一次将寄存器%rdi设置为待传递字符串"用法:Hello学号 姓名 秒数!\n"的起始地点;第二次将其设置为“Hello %s  %s\n”的起始地点。具体已在前面讲过。使用寄存器%rsi完成对argv[1]的传递,用%rdx完成对argv[2]的传递。
(3)exit函数
汇编代码如下图:

(4)atoi函数
汇编代码如下图:

atoi函数将参数argv[3]放入寄存器%rdi中用作参数传递,简朴使用call指令调用。atoi函数用于将字符串转换为整数。
(5)sleep函数
汇编代码如下图:

代码将转换完成的秒数从%eax传递到%edi中,edi存放sleep的参数,再使用call调用。Sleep函数可以让程序休眠一段时间。
(6)getchar函数
汇编代码如下图:

getchar函数无参数,用于获取单个字符。
(7)puts函数
汇编代码如下图:

在Og选项下,单独输出固定字符串的printf函数被编译器优化成了puts函数。leaq .LC0(%rip), %rdi这一行将.LC0的地点加载到%rdi寄存器中。.LC0是一个包含要输出的字符串的位置,它使用相对地点寻址。具体可见下图的伪代码段。

3.3.9 for循环

movq -32(%rbp), %rax:这一行将存储在-32(%rbp)的变量的值加载到%rax寄存器中。
addq $24, %rax:这一行将%rax寄存器中的值增加24,用于获取arg[1]的地点。
movq (%rax), %rcx:这一行argv[1]加载到%rcx寄存器中。
movq -32(%rbp), %rax:这一行重复了第2步的操作,重新加载数组的指针到%rax寄存器中。
addq $16, %rax:这一行增加了%rax中的值,通常用于访问数组中的另一个元素。
movq (%rax), %rdx:这一行将数组中的另一个元素加载到%rdx寄存器中。
紧接着,程序重复了上述步调,获取了argv[2]和argv[3]的值,分别存储在%rdx和%rsi寄存器中。
这之后,函数继续执行for循环内部的内容。
addl $1, -4(%rbp):将循环计数器加1,通常是通过修改存储在-4(%rbp)处的值来实现。接着,函数回到.L4标记处,继续执行循环体。这些汇编指令重复执行循环体,直到循环条件不再满意(即i < 10)。
3.4 本章小结

这一章详细地先容了C编译器怎样把hello.c文件转换成hello.s文件的过程,扼要分析了编译的含义和功能,演示了编译的指令,并通过分析天生的hello.s文件中的汇编代码,探讨了数据处理,函数调用,赋值、算术、关系等运算以及控制跳转、类型转换等方面,比较了源代码和汇编代码分别是怎样实现这些操作的。
第4章 汇编

4.1 汇编的概念与作用

4.1.1 汇编的概念
汇编是指汇编器(assemble)将包含汇编语言的.s文件翻译为呆板语言指令,并把这些指令打包成为一个可重定位目标文件的格式,天生目标文件.o文件。.o文件是一个二进制文件,包含main函数的指令编码。
4.1.2 汇编的作用
  计算机只能辨认处理呆板指令程序,汇编过程将汇编语言程序翻译为了呆板指令,进一步向计算机可以或许执行操作的形式迈进,便于计算机直接举行分析处理。
简朴的来说,汇编之后我们能从汇编代码得到一个可重定位目标文件,以便后续举行链接。
4.2 在Ubuntu下汇编的命令

在hello.s的目次下打开终端,输入gcc -c hello.s -o hello.o并回车,这样便可以天生hello.o目标文件。

4.3 可重定位目标elf格式

4.3.1 elf头
由于hello.o文件是一个目标文件,因此无法直接使用vim打开。我们在终端输入readelf -h hello.o来分析elf文件头,结果如下。

ELF头以一个16字节的序列(Magic,魔数)开始,这个序列描述了天生文件的系统的字的巨细和字节序次。ELF头剩下部分的信息包含帮助毗连器语法分析和解释目标文件的信息。此中包括ELF头的巨细、目标文件的类型和呆板类型等。例如上图中,Data表示了系统采用小端法,文件类型Type为REL(可重定位文件),节头数量Number of section headers为13个等信息。
4.3.2 section头
在终端输入readelf -S hello.o查看节头,结果如下。

可以发现,hello.o中一共有13个节,8个重定位条目,7个全局符号。在这些重定位条目中,有两个对应rodata节中的数据地点,显然它们是printf使用的那两个字符串地点。别的6个重定位条目都是被call指令调用过的函数地点。
夹在ELF头和节头部表之间的都为节,包含了文件中出现的各个节的语义,包括节类型、位置和巨细等信息。各部分含义如下表格所示:

名称
包含内容含义
.text
已编译程序的呆板代码
.rodata
只读数据,比如printf语句中的格式串和开关语句的跳转表。ro是Read-only(只读)的简称。
.data
已初始化的全局变量和静态C变量
.bss(Block Started by Symbol)
未初始化的全局变量和静态C变量,以及所有初始化为0的全局变量和静态C变量。在目标文件中,这个节并不会占据现实空间,只是一个占位符,未初始化变量并不必要占据任何现实的磁盘空间,在运行时再从内存分配变量,初始值为0。这种延迟分配的机制可以节省大量内存空间,尤其是当程序中存在大量全局或静态变量时。
.symtab
符号表,存放了程序中定义和引用的函数和全局变量的信息。
.rel.text
一个.tex节中位置的列表
.rel.data
被模块引用或定义的所有全局变量的重定位信息
.debug
一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。只有以-g选项调用编译器驱动程序时,才可以或许得到这张表。
.line

原始C源程序中的行号和.text节中呆板指令之间的映射

.strtab和.shstrtab

一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字(生存在shstrtab中)。字符串表就是以null结尾的字符串序列。

表4.3.2-2 各节信息

4.3.3 符号表
             
在终端输入命令readelf -s hello.o并回车,结果如下。

在符号表中,Num为某个符号的编号,Name是符号的名称。Size表示他是一个位于.text节中偏移量为0处的146字节函数。Bind表示这个符号是本地的还是全局的,由上图可知main函数名称这个符号变量是本地的。

4.3.4 可重定位段信息

在终端输入readelf -r hello.o并回车可查看可重定位段信息,结果如下:


在列出的信息中,偏移量表示必要被修改的引用的节偏移,符号值标识被修改引用应该指向的符号。类型告知毗连器怎样修改新的引用,加数是一个有符号常数,一些类型的重定位要使用它对被修改的引用的值做偏移调整。

4.4 Hello.o的结果分析

在终端输入readelf -d -r hello.o 并回车可对hello.o文件举行反汇编,得到结果如下:


我们不难发现,反汇编得到的结果与hello.s中的汇编代码基本一致,但是还是存在一些出入:在每条指令的前面出现了一组组由16进制数字组成的代码,这就是呆板代码。呆板代码才是计算机真正可以辨认的语言。
这些呆板代码是二进制呆板指令的集合,每一条呆板代码都对应一条呆板指令。每一条汇编语言都可以用呆板二进制数据来表示,汇编语言中的操作码和操作数以一种相当于映射的方式和呆板语言举行对应,从而让呆板可以或许真正理解代码的含义而且执行相应的功能。呆板代码与汇编代码差别的地方在于:
在汇编语言中使用跳转指令只必要在后面加上标识符便可以跳转到标识符所在的位置,而呆板语言颠末翻译直接通过长度为一个字节的PC(Program Counter,程序计数器)相对地点举行跳转。
跳转至.L3的代码对比

左侧为反汇编代码,右侧为汇编代码


        2.函数调用方面
在汇编代码hello.s中,函数调用直接标上了函数的名称。。而在反汇编代码中,call目标地点是当前指令的下一条指令地点。这是由于hello.c中调用的函数都是共享库(如stdio.h,stdlib.h)中的函数,如puts,exit,printf,atoi,sleep等,必要等待链接器举行链接之后才能确定响应函数的地点。因此,呆板语言中,对于这种不确定地点的调用,会先将下一条指令的相对地点全部设置为0,然后在.rel.text节中为其添加重定位条目,等待链接时确定地点。
调用getchar函数的对比

左侧为反汇编代码,右侧为汇编代码

        3.伪指令部分
反汇编代码中,原来出现在汇编代码开头的伪指令全部消失了。
代码开头对比

左侧为反汇编代码,右侧为汇编代码

        4.立即数部分
原本十进制的立即数都变成了二进制。这个很好理解,输出的文件是二进制的,对于objdump来说,直接将二进制转化为十六进制比价方便,也有利于程序员以字节为单位观察代码。
左侧为反汇编代码,右侧为汇编代码

4.5 本章小结

本章对汇编的概念、作用、可重定向目标文件的布局及对应反汇编代码等举行了较为详细的先容。颠末汇编阶段,汇编语言代码转化为呆板语言,天生的可重定位目标文件(hello.o)为随后的链接阶段做好了预备。完本钱章内容的过程加深了我对汇编过程、ELF格式以及重定位的理解。
5链接

5.1 链接的概念与作用

5.1.1 链接的概念
链接是将各种代码和数据片断和搜集并组成成为一个单一文件的过程,这个文件可被加载(复制 )到内存并执行。链接可以在编译时,也就是在源代码被翻译成呆板代码时直接执行;也可以执行于加载时,也就是在程序被记载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。
5.1.2 链接的作用
链接可以将程序调用的各种静态链接库和动态毗连库整合到一起,完善重定位目次,使之成为一个可运行的程序。同时,链接的主要作用就是使得分离编译成为大概,从而不必要将一个大型的应用程序组织成一个巨大的源文件,而是可以将其分解成更小的、更好管理的模块,可以独立的修改和编译这些模块。
5.2 在Ubuntu下链接的命令

在终端中,输入ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o并回车,结果如下图所示:

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

    可执行目标文件的格式类似于可重定位目标文件(hello.o)的格式,但稍有差别。ELF头中字段e_entry给出执行程序时的第一条指令的地点,而在可重定位文件中,此字段为0。可执行目标文件多了一个程序头表,也称为段头表,是一个布局数组。可执行目标文件还多了一个.init节,用于定义_init函数,该函数用来执行可执行目标文件开始执行时的初始化工作。由于可执行目标文件不必要重定位,以是比可重定位目标文件少了两个.rel节。
与第四章使用的方法相似,我们仍然可以使用readelf查看hello文件的ELF头,节头部表,符号表。
5.3.1 ELF头
在终端中输入readelf -h hello并回车,查看hello文件的ELF头,如下图所示:

我们不难发现,hello的ELF头中Type处显示的是EXEC,表示时可执行目标文件,这与hello.o差别。hello中的节的数量为30个。
5.3.2 Section头
在终端中输入readelf -S hello并回车,查看hello文件的Section头,如下图所示:

  Section表对hello中所有信息举行了声明,包括了巨细、偏移量、起始地点以及数据对齐方式等信息。根据始地点和巨细,我们就可以计算节头部表中的每个节所在的地区。

5.4 hello的虚拟地点空间

我们在终端中输入edb并回车,打开edb界面,在File-Open中选择hello文件,点击open,界面显示如下:

我们可以看到,hello的可执行部分(代码段)起始地点为0x400000,结束地点为0x400ff0。 
 

由5.3节我们又可以得知,.interp段的起始地点为400200。使用edb查询可得到如下结果。 
 
由5.3节我们可得知,.init的起始地点为0x4004c0,在edb中查询地点可以得到如下图的结果: 
 
由5.3节我们可得知,.text的起始地点为0x400550,在edb中查询地点可以得到如下图的结果: 
 
由5.3节我们可得知,.rodata的起始地点为0x4006a0,在edb中查询地点可以得到如下图的结果:
 
由5.3节我们可得知,.eh_frame的起始地点为0x4006a0,在edb中查询地点可以得到如下图的结果:
 
 
5.5 链接的重定位过程分析

在终端输入命令objdump -d -r hello并回车,查看hello可执行文件的反汇编条目,结果如下:
 
我们可以观察到,hello的反汇编代码与hello.o的返汇编代码在布局和语法上是基本相同的,只不过hello的反汇编代码多了非常多的内容,我们通过比较差别来看一下区别:

1.虚拟地点差别,hello.o的反汇编代码虚拟地点从0开始,而hello的反汇编代码虚拟地点从0x400000开始。这是由于hello.o在链接之前只能给出相对地点,而hello在链接之后得到的是绝对地点。

hello的反汇编结果

hello.o的反汇编结果

2.反汇编节数差别,hello.o只有.text节,里面只有main函数的反汇编代码。而hello在main函数之前加上了链接过程中重定位而加入的各种在hello中被调用的函数、数据,增加了.init,.plt,.plt.sec等节的反汇编代码。

3.跳转指令差别,hello.o中的跳转指令后加的主要是汇编代码块前的标号,而hello中的跳转指令后加的则是具体的地点,但相对地点没有发生变化。


 
5.6 hello的执行流程

1.使用edb执行hello,首先,最初的程序地点会在0x7fa4:917dc090处,这里是hello使用的动态链接库ld-2.2.27.so的入口点_dl_start:
        
        2.然后,程序跳转到_dl_init,在颠末了一系列初始化后,跳到hello的程 序入口点_start;
        3.然后程序通过call指令跳到动态链接库ld-2.27.so的_libc_start_main 处,这个函数会举行一些必要的初始化,并负责调用main函数;
        4. 下一步,程序调用动态链接库中的__cxa_atexit函数,它会设置在程序结束时必要调用的函数表;
5. 然后返回到__libc_start_main继续,然后调用hello可执行文件中的__libc_csu_init函数,这函数是由静态库引入的,也是做一些初始化的工作;
6. 然后程序返回到__libc_start_main继续,紧接着程序调用动态链接库里的_setjmp函数,设置一些非本地跳转;
7.然后返回到__libc_start_main继续,正式开始调用main函数;
8. 由于我们在edb运行hello的时候并未给出额外的命令行参数,因此它会在第一个if处通过exit(1)直接结束程序;
9. 通过hello本身携带的exit函数,程序会跳转;
10. 之后,在举行了若干操作后,程序退出。

5.7 Hello的动态链接分析

程序调用一个有共享库定义的函数时,编译器无法预测函数在运行时的具体地点,由于定义这个函数的共享模块大概可以被加载到任何位置。因此,编译系统采用延迟绑定,将过程地点的绑定推迟到第一次调用该过程的时候。
延迟绑定必要用到两个数据布局:GOT(Global Offset Table,全局偏移表)和PLT(Procedure Linkage Table,过程链接表)。
.plt:PLT是一个数组,此中每个条目是16字节代码。PLT[0]是一个特别条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
.got:GOT是一个数组,此中每个条目是8字节地点。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在分析函数地点时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地点必要在运行时被分析。每个条目都有一个相匹配的PLT条目。
一次调用某个函数时,程序不是直接调用,而是调用进入函数所在的PLT条目,第一条PLT指令通过GOT举行间接跳转,每个GOT条目初始时都指向其对应的PLT条目标第二条指令,这个间接跳转只是简朴将控制传送回函数所在的PLT条目标下一条指令。之后将函数的ID压入栈中之后,函数所在的PLT条目跳转到PLT[0],末了PLT[0]通过GOT[1]间接地把动态链接器的一个参数压入栈中,然后通过GOT[2]简介跳转进入动态链接器。动态链接器通过使用两个栈条目来确定函数的运行时位置,再将控制传递给函数。
后续调用时,则可以不消通过GOT[4]的跳转将控制给到函数。
hello在动态毗连器加载前后的重定位是不一样的,在加载之后才举行重定位。

5.8 本章小结

本章节扼要先容了链接的相关过程,首先扼要阐述了链接的概念和作用,给出了链接在Ubuntu系统下的指令。之后研究了可执行目标文件hello的ELF格式,并通过edb调试工具查看了虚拟地点空间和几个节的内容,之后依据重定位条目分析了重定位的过程,并借助edb调试工具,研究了程序中各个子程序的执行流程,末了则借助edb调试工具通过对虚拟内存的查取,分析研究了动态链接的过程。通过链接,hello.o与它依靠的所有的库联合在一起形成了一个可执行文件,在这个可执行文件中,所有的运行时位置都已经确定,可以被复制到内存里并运行了。
6hello进程管理

6.1 进程的概念与作用

进程的概念:进程是计算机中的程序关于某数据集合上的一次运行运动,是系统举行资源分配和调度的基本单位,是操作系统布局的底子。
进程的作用:在运行一个进程时,我们的这个程序看似是系统当中唯一一个运行的程序,进程的作用就是提供给程序两个关键的抽象:1.独立的逻辑控制流,即程序独占使用处理器的假象。2.私有的地点空间,即程序独占使用内存系统的假象。
6.2 简述壳Shell-bash的作用与处理流程

Shell是一种交互型程序,用于代表用户运行其他程序。
Shell的处理流程如下:首先对命令行参数求值,判定命令行是否为空,如果不为空则判定第一个命令行参数是不是一个内置的命令,如果是一个内置命令则直接执行,否则检查是否是一个应用程序。之后在搜索路径里寻找这些应用程序,如果键入的命令不是一个内部命令而且路径里没有找到这个可执行文件,则会显示一条错误信息。如果可以或许乐成找到命令,那么该内部命令大概应用程序将会被分解为系统调用并传递给linux内核。
6.3 Hello的fork进程创建过程

Shell调用fork创建子进程。新创建的子进程险些但不完全与父进程相同。子进程得到与父进程用户级虚拟地点空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,因此fork后子进程可以读写父进程中打开的任意文件。父进程和创建的子进程最大的区别在于其PID差别。
fork会被父进程调用一次,返回两次,父进程与创建的子进程并发执行。执行hello时,fork后的进程在前台执行,因此创建它的父进程shell暂时挂起等待hello进程执行完毕。

6.4 Hello的execve过程

shell通过fork创建一个子进程后,execve函数在当前进程的上下文中加载并运行一个新程序即hello。
Execve必要三个参数:可执行目标文件名filename、参数列表argv、环境变量列表envp。这些都由shell构造并传递。除非找不到filename,否则execve不会返回。(调用一次,(正常情况下)从不返回)
调用execve会将这个进程执行的原本的程序完全替换,它会删除已存在的用户地区,包括数据和代码;然后,映射私有区:为Hello的代码、数据、.bss和栈地区创建新的地区布局,所有这些地区都是私有的、写时才复制的;之后映射共享区;末了把控制传递给当前的进程的程序入口。
6.5 Hello的进程执行

6.5.1逻辑控制流
逻辑控制流是一个PC值的序列,PC值就是程序计数器的值,这些值与可执行目标文件的指令大概包含在运行时动态链接到程序的共享对象中的指令一一对应。
6.5.2 时间分片
在现代计算机体系中,进程是轮流使用处理器的,每个进程都执行它的流的一部分,然后被抢占(暂时挂起),再轮到其它进程。一个逻辑流的执行在时间上与另一个流重叠被称为并发流,这两个流并发运行。
多个流并发执行的概念被称为并发。一个进程与其他进程轮流运行的概念称为多任务。一个进程执行其控制流一部分的每一个时间段叫做时间片,多任务也就被称作是时间分片。
6.5.3 用户模式与内核模式
为了保护操作系统内核,处理器在某一个控制寄存器中的一个模式位,设置模式位时,进程就运行在内核模式中,否则运行在用户模式。内核模式的代码可以无穷定地访问所有处理器指令集以及全部内存和 I/O 空间。如果用户模式的进程要享有此特权,它必须通过系统调用向装备驱动程序或其他内核模式的代码发出哀求。别的,用户模式的代码允许发生缺页,而内核模式的代码则不允许。
运行程序代码初始时都是在用户模式中的,当发生中断故障或系统调用的异常时,进程从用户模式变化为内核模式。当异常发生时,控制传递到异常处理程序,处理器将模式变化为内核模式。内核处理程序运行在内核模式中,当它返回到应用程序代码时,处理器把模式从内核模式改回到用户模式。
6.5.4 进程上下文切换
上下文就是内核重新启动一个被抢占的进程所必要的状态,它由通用目标寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据布局等对象的值构成。进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这样的决定叫做调度,由内核中的调度器的代码处理。在这个抢占过程中必要用到上下文切换,上下文切换生存当前进程的上下文,恢复先前某个被抢占的上下文,并将控制传递给新恢复的进程。

6.6 hello的异常与信号处理

6.6.1异常类型
运行时异常:如除以零、空指针引用等,这类异常会导致程序瓦解。
资源异常:如文件未找到、内存不敷等,这类异常通常必要程序举行适当的错误处理。
输入异常:用户输入了不符合程序要求的数据。
6.6.2. 产生的信号
SIGINT:当用户按下Ctrl+C时发送,通常用于中断程序。
SIGTSTP:当用户按下Ctrl+Z时发送,用于暂停程序。
SIGTERM:哀求程序停止的正常信号。
6.6.3具体信号处理与命令
1.乱按字

可以看到,在键盘中乱打字并没有改变printf的输出,不影响程序的正常运行。
   2.按Ctrl+Z

Ctrl+Z的功能是向进程发送SIGSTP信号,进程接收到该信号之后会将该作业挂起,但不会回收。下图显示了,PID为4826的hello进程仍然在运行中。

运行jobs指令,我们可以得知hello的后台job id=1。
 

我们再调用fg命令。fg命令用于将后台作业(在后台运行的大概在后台挂起的作业)放到前台终端运行。运行结果如下。我们发现,挂起前后统共的输出次数仍为10次。
 
3.Ctrl-C
在上一步之后,我们在键盘中输入Ctrl+C,Ctrl-C命令内核向前台发送SIGINT信号,停止了前台作业。

4.不绝按回车
 
我们发现,在hello执行过程中不绝按回车,不但在printf输出时会显示出回车,在hello进程执行完毕后,我们可以看出回车的信息也同样发送到了shell中,使shell举行了若干次的革新换行。
6.7本章小结

本章概述了hello进程大致的执行过程,阐述了进程、shell、fork、execve等相关概念,之后从逻辑控制流、时间分片、用户模式/内核模式、上下文切换等角度详细分析了进程的执行过程。并在运行时实行了差别形式的命令和异常,每种信号都有差别处理机制,针对差别的shell命令,hello会产生差别响应。
 
7hello的存储管理

7.1 hello的存储器地点空间

物理地点:CPU通过地点总线的寻址,找到真实的物理内存对应地点。CPU对内存的访问是通过毗连着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地点都是物理内存地点。
逻辑地点:程序代码颠末编译后出现在汇编程序中地点。逻辑地点由选择符(在实模式下是描述符,在保护模式下是用来选择描述符的选择符)和偏移量(偏移部分)组成。
线性地点:线性地点空间是一个非负整数的集合。逻辑地点颠末段机制后转化为线性地点,为描述符:偏移量的组合形式。在调试hello时,gdb中查看到的就是线性地点,大概虚拟地点。
虚拟地点:虚拟地点空间是0到N的所有整数的集合(N是正整数),是线性地点空间的有限子集。分页机制以虚拟地点为桥梁,将硬盘和物理内存联系起来。
7.2 Intel逻辑地点到线性地点的变更-段式管理

在 Intel 平台下,逻辑地点是 selectorffset 这种形式,selector 是 CS 寄存器的值,offset 是 EIP 寄存器的值。
CS寄存器(代码段寄存器): CS寄存器存储了当前执行的指令所在的代码段的起始地点。它是一个16位寄存器,指示了代码在内存中的位置。CS寄存器的值与代码段的段基址相关,形成了代码段的起始物理地点。
EIP寄存器:用来存储CPU要读取指令的地点,CPU通过EIP寄存器读取即将要执行的指令。每次CPU执行完相应的汇编指令之后,EIP寄存器的值就会增加。
如果用 selector 去 GDT( Global Descriptor Table,全局描述符表 ) 里拿到 segment base address(段基址) 然后加上 offset(段内偏移),这就得到了 linear address(线性地点)。这个过程就称作段式内存管理。
逻辑地点由段标识符和段内偏移量组成。段标识符是一个16位长的字段(段选择符),可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
全局的段描述符,放在GDT中,一些局部的段描述符,放在“LDT(Local Descriptor Table,局部段描述符表)”中。
给定一个完备的逻辑地点段选择符+段内偏移地点,看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地点和巨细。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,就得到了其基地点。再由基地点加上偏移量的值,便得到了线性地点。

7.3 Hello的线性地点到物理地点的变更-页式管理

线性地点(VA)到物理地点(PA)之间的转换通过分页机制完成。分页机制类似主存和Cache之间的分块机制,分页机制对虚拟地点和物理内存举行分页,页的巨细通常是4KB到2M(因时而异,时过境迁,页的巨细有所差别)。在x86-64呆板上,虚拟地点空间的N是2的48次方,有256TB,比正常的硬盘大得多。
在分页机制中,硬盘空间的每个字节到虚拟地点空间的每个字节存在映射关系,且这个映射是单射。虚拟地点空间和硬盘空间都以字节为单位,从0开始编地点号。设硬盘空间为H,虚拟地点空间为V,设他们之间的映射关系为
,f是单射,则
于是,我们知道了物理地点中某个地点所在页与虚拟空间的页的对应关系,也就知道了物理地点中某个地点所在页与硬盘中某个页的对应关系。
物理地点中某个地点所在页与虚拟空间的页的对应关系要通过什么来记载呢?分页机制中使用一个叫做页表的数据布局来记载这些关系,页表也是存储在内存中的,是由操作系统维护的。实在DRAM到Cache中也是类似机制,只不过DRAM到Cache的高速缓存机制是用硬件实现的。
每个进程都有一个页表,页表中的每一项,即PTE(页表条目),记载着该对应的虚拟地点空间的那一页是否有效(即是否有对应的物理内存上的页),物理页的起始位置或磁盘地点,访问权限等信息。PTE根据差别的映射状态也被分别为三种状态:未分配、未缓存、已缓存。
未分配:虚拟内存中未分配的页
未缓存:已经分配但是还没有被缓存到物理内存中的页
已缓存:分配后缓存到物理页块中的页

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

页表是 PTE(页表条目)的数组,它将虚拟页映射到物理页,每个 PTE 都有一个有效位和一个 n 位地点字段,有效位表明该虚拟页是否被缓存在 DRAM 中。虚拟地点分为两个部分,虚拟页号(VPN,Virtual Page Number)和虚拟页面偏移量(VPO,Virtual Page Offset)。此中VPN必要在PTE中查询对应,而VPO则直接对应物理地点偏移(PPO)。
TLB(translation lookaside buffer,地点转换后备缓冲器,习惯称之为“快表”)是一个位于MMU(Memory Management Unit,内存管理单元)中,关于PTE的一个缓存。TLB是一个小的、虚拟寻址的缓存,此中每一行均生存了一个由单个PTE组成的块。TLB有高度的相联性,可以或许加快地点翻译,而多级页表可以或许对页表举行压缩,便于大量存储。
在从VA翻译得到PA的过程中,MMU首先用VPN向TLB申请哀求对应的PTE,如果掷中,那么直接跳过后面的步调;之后MMU天生PTE地点,从高速主存哀求得到PTE,高速缓存或主存会向MMU返回PTE。若PTE有效位为0,分析缺页,MMU触发缺页异常,缺页处理程序确定物理内存中的牺牲页(若页面修改,则换出到磁盘)。之后缺页处理程序调入新的页面,并更新PTE。之后却也处理程序返回原进程,并重新执行导致缺页的指令。

 四级页表是一种多级页表,多级页表的主要目标是用于压缩页表。在地点翻译过程中,虚拟的地点页号VPN被分为了k个,每一个VPNi都是一个指向第i级页表的索引。当

时,都是指向第j+1级的某个页表。第k级页表中的每个PTE包含某个物理页面的PPN,大概时一个磁盘块的地点。为构造物理地点,MMU必要访问k个PTE,之后才能确定PPN。Intel Core i7采用的是一个四级页表条理布局,每个VPNi有9位,当未掷中时,36位的VPN被分为VPN1、VPN2、VPN3、VPN4,每个VPNi被用作到一个页表的偏移量。CR3寄存器包含L1 页表的物理地点,VPN1提供到一个L1 PTE的偏移量,这个PTE包含某个L2页表的基址。VPN2提供到这个L2页表中某个PTE的偏移量,以此类推。末了得到的L4 PTE包含了必要的物理页号,和虚拟地点中的VPO毗连起来就得到相应的物理地点。
7.5 三级Cache支持下的物理内存访问

通过内存地点的组索引获得值,如果对应的值是data则像L1 d-cache对应组中查找,如果是指令,则向L1 i-cache对应组中查找。将L1对应组中的每一行的标记位举行对比,如果相同而且有效位为1则掷中,获得偏移量,取出相应字节,否则不掷中,向下一级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中,即DRAM缓存不掷中被称为缺页。当CPU引用了一个页表条目中的一个字,而该页表条目并未被缓存在DRAM中,地点翻译硬件从内存中读取该页表条目,从有效位为0可以判定尚未被缓存,进而触发缺页异常。
缺页中断处理:缺页异常调用缺页异常处理程序,该程序会选择一个牺牲页,如果这个牺牲页在DRAM中已被修改,那么就将他写回磁盘,之后将引用的虚拟页复制到内存中的原来牺牲页所在位置,并对页表条目举行更新,随后返回。当异常处理程序返回时,它会重新启动缺页的指令,该指令会把导致缺页的虚拟地点重发送到地点翻译硬件。此时必要调用的虚拟页已经缓存到主存中了,则页掷中可以由地点翻译硬件正常处理。

 
7.9动态存储分配管理

首先,我们必要明确一下动态存储分配管理的概念:在程序运行时程序员使用动态内存分配器,例如调用malloc函数从而获得虚拟内存。分配器将堆(heap)视为一组差别巨细的块(blocks)的集合来维护。每个块要么是已分配的,要么是空闲的。

7.9.1堆
动态内存分配器维护着一个进程的虚拟内存地区,称为堆。

在内存中的碎片和垃圾被回收之后,内存中就会有空余的空间被闲置出来。这些空间偶然会比较小,但是积少成多,操作系统不知道怎么使用这些空间,就会造成许多的浪费。为了记载这些空闲块,采用隐式空闲链表和显式空闲链表的方法实现这一操作。

7.9.2隐式空闲链表

首先了解几个概念:

首次适配 (First fit): 从头开始搜索空闲链表,选择第一个合适的空闲块: 搜索时间与总块数(包括已分配和空闲块)成线性关系。在靠近链表起始处留下小空闲块的“碎片”。

下一次适配 (Next fit): 和首次适配相似,只是从链表中上一次查询结束的地方开始。比首次适应更快: 制止重复扫描那些无用块。一些研究表明,下一次适配的内存使用率要比首次适配低得多。

最佳适配 (Best fit): 查询链表,选择一个最好的空闲块;适配,剩余最少空闲空间。保证碎片最小——进步内存使用率,通常运行速度会慢于首次适配。

在隐式空闲链表工作时,如果分配块比空闲块小,可以把空闲块分为两部分,一部分用来承装分配块,这样可以镌汰空闲部分无法使用而造成的浪费。隐式链表采用界限标记的方法举行双向归并。脚部与头部是相同的,均为 4 个字节,用来存储块的巨细,以及表明这个块是已分配还是空闲块。同时定位头部和尾部,是为了可以或许以常数时间来举行块的归并。无论是与下一块还是与上一块归并,都可以通过他们的头部或尾部得知块巨细,从而定位整个块,制止了从头遍历链表。但与此同时也显著的增加了额外的内存开销。他会根据每一个内存块的脚部界限标记来选择归并方式,如下图:

7.9.3 显式空闲链表
显式空闲链表只记载空闲块,而不是来记载所有块。它的思绪是维护多个空闲链表,每个链表中的块有大致相等的巨细,分配器维护着一个空闲链表数组,每个巨细类一个空闲链表,当必要分配块时只必要在对应的空闲链表中搜索。

7.10本章小结

本章先容了Hello和操作系统之间的交换方式。先容了hello的存储器地点空间、intel的段式管理、hello的页式管理,先容了Hello是怎样颠末地点翻译从而找到最终的物理地点。阐释了TLB加快地点翻译、多级缓存以及动态内存管理相关的要点。末了,本章先容了动态存储分配管理机制。


8hello的IO管理


8.1 Linux的IO装备管理方法

一个Linux文件就是一个m个字节的序列,所有的I/O装备都被模型化为文件,所有的输入输出都被看成是文件的读和写来执行。这种将装备优雅地映射为文件的方式,允许Linux内核引出一个简朴低级的应用接口,被称为是Unix I/O,这使得所有的输入和输出都可以或许以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数

1.Unix接口:
(1)打开文件:一个应用程序通过内核打开文件,来宣告它想访问一个I/O装备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记载有关这个打开文件的所有信息。应用程序只需记着这个描述符。
(2)I/O装备:内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记载有关这个打开文件的所有信息。应用程序只需记着这个描述符。
(3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序可以或许通过执行seek,显式地将改变当前文件位置k。
(4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个巨细为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
(5)关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件,作为响应,内核释放文件打开时创建的数据布局,并将这个描述符恢复到可用的描述符池中。无论一个进程由于何种缘故原由停止时,内核都会关闭所有打开的文件并释放他们的内存资源。
2.Unix I/O函数:
(1)int open(char *filename, int flags, mode_t mode);

进程通过调用open函数打开一个已存在的文件大概创建一个新文件。open函数将filename转换为一个文件描述符,而且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明白进程打算怎样访问这个文件:O_RDONLY:只读、O_WRONLY:只写和O_RDWR可读可写。mode参数指定了新文件的访问权限位。
(2)int close(fd):
 进程调用close函数关闭一个打开的文件,fd是必要关闭的文件的描述符。
(3)ssize_t read(int fd, void *buf, size_t n);
read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF(End Of File),否则返回值表示的是现实传送的字节数量。
(4)ssize_t write(int fd, const void *buf, size_t n);
 write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析

printf的具体函数体实现过程如下:

printf程序按照格式fmt联合参数args天生格式化之后的字符串,并返回字串的长度。printf用了两个外部函数,一个是vsprintf,另有一个是write。
vsprintf函数体如下:
vsprintf函数作用是接受确定输特别式的格式字符串fmt。用格式字符串对个数变化的参数举行格式化,产生格式化输出。write函数将buf中的i个元素写到终端。在unistd.h头文件中,我们可以找到write函数的声明:

write()函数有三个参数:

fd: 文件描述符,标识待写入的文件大概套接字。
buf: 指向要写入的数据的缓冲区。
count: 要写入的字节数。
返回值为现实写入的字节数,错误时返回-1,并设置errno。
从vsprintf天生显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照革新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析

getchar()函数的具体实现如下:


getchar函数调用了read函数,通过系统调用read读取存储在键盘缓冲区的ASCII码,直到读到回车符才返回。不过read函数每次会把所有内容读进缓冲区,如果缓冲区本来非空,则不会调用read函数,而是简朴的返回缓冲区最前面的元素。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,生存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结

本章主要先容了hello的I/O管理机制,先简述了I/O装备被抽象为文件的现象,随后先容了I/O的装备管理方法——unix IO接口,随后对unixIO接口做了先容之后,给出了Unix IO接口的相关函数,并在此底子上,对printf和getchar的实现原理举行了先容。
结论

hello这个计算机世界最基本的程序,从出生(编写完成)到死亡(进程被回收)统共履历了如下几个步调:
1、预处理(cpp)。将hello.c举行预处理,将文件调用的所有外部库文件归并睁开,天生一个颠末修改的hello.i文件。
2、编译(ccl)。将hello.i文件翻译成为一个包含汇编语言的文件hello.s。
3、汇编(as)。将hello.s翻译成为一个可重定位目标文件hello.o。
4、链接(ld)。将hello.o文件和可重定位目标文件和动态链接库链接起来,天生一个可执行目标文件hello。
5、运行。在shel1中输入./hello 2022112040 qdx 15845895165 4 并回车。
6、创建进程。终端判定输入的指令不是shell内置指令,于是调用fork函数创建一个新的子进程。
7、加载程序。shell调用execve函数,启动加载器,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数。
8、执行指令:CPU为进程分配时间片,在一个时间片中,hello享有CPU资源,序次执行自己的控制逻辑流。
9、访问内存:MU将程序中使用的虚拟内存地点通过页表映射成物理地点。
10、信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并停止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
11、停止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据布局。

当放学习计算机的初学者,在集成IDE环境中输入几行代码,点击运行便可以完成“hello,world”的输出。看似是弹指一挥间的事情,在计算机的角度看却必要履历数十个步调,完成多个模块的并行调用。从晶体管式计算机到集成芯片,从ENIAC大型机到如今到处可见的X86-64架构、arm架构计算机,从汇编语言到如今的C,python,java......无数计算机工程师前赴后继地完善着计算机的体系布局,使计算机真正成为了一门执因索果的科学。同时,也为信息革命的深化进一步添砖加瓦。
Hello的一生告诉我们,计算机科学的范畴,没有顺理成章,没有理所当然,一切看似轻易的操作都创建在前人伟大而巧妙的构思之上。计算机范畴的学习必要潜心深入、止于至善。
请骄傲地抬起头吧,程序员们。你们不但是工程师,更是科学家。同时,你们也是人类进步与发展的伟大探路者。
附件

文件名

功能

hello.c

源程序

hello.i

预处理后得到的文本文件

hello.s

编译后得到的汇编语言文件

hello.o

汇编后得到的可重定位目标文件

hello_asm.txt

反汇编hello.o得到的反汇编文件

hello1_asm.txt

反汇编hello可执行文件得到的反汇编文件

hello

可执行文件

 
参考文献

为完本钱次大作业你翻阅的书籍与网站等

[1]  林来兴. 空间控制技术[M]. 北京:中国宇航出书社,1992:25-42.
[2]  辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出书社,1999.
[3]  赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出书社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4]  谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5]  KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6]  CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.
[7] Randal E.Bryant David R.O'Hallaron.深入理解计算机系统(第三版).机器工业出书社,2016.
[8] https://www.cnblogs.com/pianist/p/3315801.html
[9]  https://www.csd.cs.cmu.edu
 
 
 
 
 
 
 
 
 
 
 
 
 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4