[实例+图解] 一文形象生动地让你理解linux

1,332 阅读37分钟

摘 要

关键词:LINUX;预处理;编译;链接;进程管理;存储管理;IO管理

本文的主要内容是介绍了在linux环境下hello程序从预处理到编译再到链接,最后执行的全过程以及进程管理,存储管理及IO管理的实现方式。本文结合hello程序的生成详细地讲述了预处理、编译、汇编、进程管理、存储管理、IO管理的概念、作用、命令等。本文的目的是帮助程序员了解在C语言的“外衣”下,程序是如何从产生、预处理、编译、汇编,到最后被执行的。对于深入了解操作系统和计算机编译原理具有重要意义。

第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

P2P: Hello.c经过cpp的预处理,ccl的编译、as的汇编、ld的链接最终成为可执行目标程序Hello,在shell中键入启动命令后,shell通过fork产生子进程, hello便从program变成了process

020: shell通过execve映射虚拟内存,进入程序入口后载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流,当结束后,shell父进程负责回收hello进程

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的工具

软件环境:Visual studio Community2017, Windows10 64位, Vmware 14;Ubuntu 16.04 LTS 64位;

硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上

开发与调试工具:vim,gcc,as,ld,edb,readelf,HexEdit GDB/OBJDUMP;DDD/EDB等

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

文件名称 文件属性
hello.i 预处理得到的文本文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位目标执行
hello 链接之后的可执行目标文件
hello.objdmp hello.o的反汇编代码
hello.elf hello.o的ELF格式
hello1.elf hello的 ELF格式
hello.objdmp hello的反汇编代码
hello.txt 分析重定位过程时的可执行文件hello反汇编代码

1.4 本章小结

本章介绍根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。列出使用的软硬件环境和开发与调试工具。列出了生成的中间结果文件的名字,文件的作用等

第2章 预处理

2.1 预处理的概念与作用

2.11 概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。由预处理器对程序源代码文本进行处理,把源代码分割或处理成为特定的单位,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析。

2.12 作用: 1: 宏定义。宏定义是用一个标识符来表示一个字符串,这个字符串可以是常量、变量或表达式。在宏调用中将用该字符串代换宏名。 2:文件包含。文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。 3:条件编译。条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。

2.2 在Ubuntu下预处理的命令

预处理命令: gcc –E hello.c > hello.i

图2.2 在Ubuntu下预处理的命令
图2.2 在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

在这里插入图片描述
图2.3 hello.i文件

用文本编辑器打开hello.i,main函数的预处理解析结果如上图。 在main函数前出现的是stdio.h unistd.h stdlib.h头文件。 .i程序中是没有#define的,并使用了大量的#ifdef #ifndef的语句。 预处理指令会对条件值进行判断来决定是否执行包含其中的逻辑。

2.4 本章小结

本章介绍了预处理的概念与作用、命令,并展示了Hello的预处理结果解析。

第3章 编译

3.1 编译的概念与作用

  1. 编译的概念:利用编译程序从源语言编写的源程序产生目标程序的过程,用编译程序产生目标程序。 编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。
  2. 编译的作用:把高级语言变成计算机可以识别的2进制语言,词法分析、语法分析、语义检查和中间代码生成、代码优化、目标代码生成。

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s

在这里插入图片描述
图3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

在linux用文本编辑器打开hello.s查看编译结果

数据段 作用
.text 已编译程序的机器代码
.rodata 只读数据
.data 已初始化的全局C变量
.bss 未初始化和初始化为0的全局C变量。在目标文件中这个节不占据实际的空间,仅仅是一个占位符
.symtab 一个符号表,它存放在程序中定义和引用的函数和全局变量的信息
.rel.text 一个.text节中位置的列表
.rel.data 被模块引用或定义的任何全局变量的重定位信息
.debug 一个调试符号表。只有以-g选项调用编译驱动程序才会得到这张表
.line 原始C源程序中的行号和.text节中机器指令之间的映射。只有以-g选项调用编译驱动程序时才会得到这张表
.strtab 一个字符串表,内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。

字符串表是以null结尾的字符串序列

3.31 数据

(1)字符串:

在这里插入图片描述
图3.311 字符串

(2)整数 sleepsecs

在这里插入图片描述
图3.312 整数 sleepsecs

3.32 赋值

(1) 全局变量sleepsecs =2

在这里插入图片描述
图3.321

(2) 局部变量i =0

在这里插入图片描述
图3.322 局部变量i =0

3.33 类型转换

隐式类型转换的是:int sleepsecs=2.5,将浮点数类型的2.5转换为int类型

3.34 算术操作

在这里插入图片描述
图3.340 算术操作符号

(1) 相加操作

addq	$16, %rax
addq	$8, %rax

在这里插入图片描述
(2) 相减操作

subq	$32, %rsp

在这里插入图片描述
3.35 控制转移
在这里插入图片描述
图3.340 指令助记符

(1)比较i<10是否成立,若成立继续循环,否则退出循环

在这里插入图片描述
图3.341 i<10对应的汇编代码

3.36 函数操作

a) int main(int argc, char *argv[])

(1)参数传递:从内核中获取命令行参数和环境变量地址

(2)函数调用:内核执行程序时调用特殊的启动例程,执行main函数

(3)函数返回:当命令行参数数量不为3时输出提示信息并调用exit(1)退出main函数;当命令行参数数量为3执行循环和getchar函数后return 0的方式退出函数。

argc: 传给main()的命令行参数个数 argv: 命令行参数字符型指针数组的首地址

b) exit()

(1)参数传递:getchar()函数无参数

(2)函数传递:main函数通过call指令调用getchar()

(3)函数返回:返回值类型为int,如果成功返回用户输入的ASCII码,出错返回-1

c) printf()

在这里插入图片描述
图3.363 printf汇编代码

(1)参数传递:getchar()函数无参数 (2)函数传递:main函数通过call指令调用getchar() (3)函数返回:返回值类型为int,如果成功返回用户输入的ASCII码,出错返回-1

d) sleep()

在这里插入图片描述
图3.364 sleep汇编代码

(1)参数传递:getchar()函数无参数 (2)函数传递:main函数通过call指令调用getchar() (3)函数返回:返回值类型为int,如果成功返回用户输入的ASCII码,出错返回-1

e) getchar()

在这里插入图片描述
图3.365 getchar汇编代码

(1)参数传递:getchar()函数无参数 (2)函数传递:main函数通过call指令调用getchar() (3)函数返回:返回值类型为int,如果成功返回用户输入的ASCII码,出错返回-1

3.37关系操作

(1)argc!=3

在这里插入图片描述
图3.371 !=汇编代码

(2)i<10

在这里插入图片描述
图3.371 <汇编代码

3.4 本章小结

本章介绍了编译的概念与作用,展示了编译命令的使用,对编译结果解析,并说明编译器处理C语言的各个数据类型以及各类操作的过程。

第4章 汇编

4.1 汇编的概念与作用

  1. 概念:把汇编语言翻译成机器语言的过程称为汇编。在汇编语言中,用助记符代替操作码,用地址符号或标号代替地址码。通过用符号代替机器语言的二进制码,可以把机器语言变成汇编语言。
  2. 作用:将汇编语言翻译成机器语言。

编译 VS 汇编 编译:将高级语言程序变成计算机能识别的二进制语言 汇编:将汇编语言翻译成机器语言

4.2 在Ubuntu下汇编的命令

汇编的命令:as hello.s -o hello.o

在这里插入图片描述
图4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

(1) ELF头

在这里插入图片描述
图4.311 ELF头

ELF头包括一个16字节的序列、ELF头的大小、目标文件的类型(如可重定位、可执行或共享的)、机器类型(如x86-64)、 节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。 其结构体表示:

#define EI_NIDENT 16   typedef struct{   unsigned char e_ident[EI_NIDENT];   Elf32_Half e_type;   Elf32_Half e_machine;   Elf32_Word e_version;   Elf32_Addr e_entry;   Elf32_Off e_phoff;   Elf32_Off e_shoff;   Elf32_Word e_flags;   Elf32_Half e_ehsize;   Elf32_Half e_phentsize;   Elf32_Half e_phnum;   Elf32_Half e_shentsize;   Elf32_Half e_shnum;   Elf32_Half e_shstrndx; }Elf32_Ehdr;

数据格式:

在这里插入图片描述
图4.312 数据格式

(2) 节头部表:文件中出现的各个节的语义,包括节的类型、位置和大小

在这里插入图片描述
图4.32 节头部表

根据节头部表可知,当号=1,符号在.text;当号=3,符号在.data,以此类推。 三个特殊伪节: ABS:不该被重定位的符号,如main()函数。 UND:其它文件中定义,本文件中引用的符号,如swap()函数。 COM:还未分配位置的未初始化数据目标,如buf2,它最终放在.bss。 (3) 重定位节

(a)普通重定位由以下数据结构定义:

typedef struct { Elf32_Addr r_offset; //指定需要重定位的项的位置 Elf32_Word r_info; //提供了符号表中的一个位置,包括重定位类型信息。 r_info == int symbol:24,type:8; } Elf32_Rel;

(b)在ELF定义了32种不同的重定位类型,其中最基本的两种是:

R X86_ 64 PC32。 重定位一个使用32位PC相对地址的引用。一个PC相对地址就是距程序计数器(PC)的当前运行时值的偏移量。当CPU执行一条使用PC相对寻址的指令时,它就将在指令中编码的32位值加上PC的当前运行时值,得到有效地址(如call指令的目标),PC值通常是下一条指令在内存中的地址。 R X86_ 64 _32。 重定位一个使用32位绝对地址的引用。通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。

(c)代码重定位条目放在.rel.text中。已经初始化数据的重定位条目放在.rel.data中。 main.c源文件引用了一个全局sleepsecs符号。 sleepsecs的重定位类型为相对重定位 并且由图4.33(1)可以得到:sleepsecs的r_offset : 000000000060重定位的字节处, 由图4.33(2)可以得到:sleepsecs的大小为4个字节 计算sleepsecs的重定位后的地址:Result = S-P+A A代表加数值,S是符号表中保存的符号的值,P代表重定位的位置偏移量

在这里插入图片描述
图4.331重定位节

在这里插入图片描述
图4.332

(4) 符号表:存放着程序中定义和引用函数和全局变量的信息,不包含局部变量的条目

在这里插入图片描述
图4.34 符号表

Value:在对应节的偏移。 Size:目标大小。 Type:是数据或函数。 Bind:本地或全局。 Vis:预留。 Ndx:符号所在的节,其实是节头部表中条目的索引。 Name:符号名,为空的为链接器内部使用的本地符号,可以忽略。

4.4 Hello.o的结果解析

用命令行得到,比较hello.objdump与hello.o,进行对照分析

在这里插入图片描述
图4.40 命令行

(1) hello.objdump记录了文件格式和.text代码段: 而hello.s中除了记录了文件格式和.text代码段还包括.type .size .align以及.rodata

在这里插入图片描述

图4.41 hello.objdump与hello.s文件内容对比

(2) 分支转移:

hello.objdump跳转中地址为已确定的实际指令地址; hello.s跳转中地址为助记符如.L2,通过使用例如.L2等的助记符进行跳转。

在这里插入图片描述
图4.42 hello.objdump与hello.s分支转移对比 (3)函数调用 在.s文件中,call的地址是函数名称,如puts@PLT, 而在反汇编程序中,call的目标地址是指令,如callq 21 <main+0x21>。因为hello.c中调用的函数都是共享库中的函数,共享库函数调用需要通过链接时重定位才能确定地址
在这里插入图片描述
图4.43 hello.objdump与hello.s函数puts调用对比 (4)全局变量访问 hello.objdump使用0+%rip访问全局变量sleepsecs,如lea 0x0(%rip),%rdi。hello.s使用段名称+%rip访问全局变量sleepsecs,如leaq .LC0(%rip), %rdi
在这里插入图片描述
图4.44 hello.objdump与hello.s全局变量sleepsecs访问对比

4.5 本章小结

本章介绍了汇编的概念与作用,在linux下进行汇编的指令,可重定位目标文件elf的格式,将hello.o的结果解析与hello.s进行对照分析,分析了机器语言的构成以及与汇编语言的映射关系。

第5章 链接

5.1 链接的概念与作用

链接的概念:Linux 链接分两种,一种被称为硬链接(Hard Link),另一种被称为符号链接(Symbolic Link)。默认情况下,ln 命令产生硬链接。 (1)硬连接指通过索引节点来进行连接。在 Linux 的文件系统中,保存在磁盘分区中的文件不管是什么类型都给它分配一个编号,称为索引节点号(Inode Index)。在 Linux 中,多个文件名指向同一索引节点是存在的。 (2)软连接。软链接文件是一个特殊的文件。在符号连接中,文件实际上是一个文本文件,其中包含的有另一文件的位置信息。 作用:链接操作给系统中已有的某个文件指定另外一个可用于访问它的名称。我们可以为这个新的文件名指定不同的访问权限。链用户可以利用链接直接进入被链接的目录。即使删除这个链接,也不会破坏原来的目录。硬连接的作用是允许一个文件拥有多个有效路径名,用户就可以建立硬连接到重要文件,以防止“误删”的功能。

5.2 在Ubuntu下链接的命令

使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件 链接的命令:ld -o OUTPUT /lib/crt0.o hello.o –lc 链接的命令行: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.2 在Ubuntu下链接的命令

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

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。 使用命令行readelf -a hello > hello1.elf生成hello1.elf文件 节头表中包含了各段的起始地址,大小等信息。

在这里插入图片描述
图5.31 节头表
在这里插入图片描述
图5.32 节头表

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

(1) 分析程序头部表。

PHDR:程序头表 INTERP:程序执行前需要调用的解释器 LOAD:程序目标代码和常量信息 DYNAMIC:动态链接器所使用的信息 NOTE::辅助信息 GNU_EH_FRAME:保存异常信息 GNU_STACK:使用系统栈所需要的权限信息 GNU_RELRO:保存在重定位之后只读信息的位置 VirtAddr:本段首字节的虚拟地址 PhysAddr指出本段首字节的物理地址 pFileSiz指出本段在文件中所占的字节数,可以为0 MemSiz指出本段在存储器中所占字节数,可以为0 Flags指出存取权限,Align指出对齐方式

在这里插入图片描述
图5.41 程序头部表

(2) 在edb查看hello的虚拟地址空间的各段信息

在这里插入图片描述
图5.42 hello的虚拟地址空间

(3) 程序头与Datadump的映射关系:例如PHDR对应的虚拟内存地址是0x400000—— 0x4001c0

在这里插入图片描述
图5.43 程序头与Datadump的映射关系

5.5 链接的重定位过程分析

通过命令行objdump –d –r hello > hello.txt得到反汇编文件hello.txt。

(1) hello的反汇编结果与hello.o的反汇编结果相比,hello.txt多了以下节头表:

_init 程序初始化代码 gmon_start call_gmon_start函数初始化 gmon profiling system,程序通过gprof可以输出函数调用等信息 _dl_relocate_static_pie 静态库链接 .plt 动态链接-过程链接表 Puts(等函数)@plt 动态链接各个函数 _start 编译器为可执行文件加上了一个启动例程 __libc_csu_init 程序调用libc库用来对程序进行初始化的函数,一般先于main函数执行 _fini 当程序正常终止时需要执行的代码

(2) 函数个数:在使用ld命令链接的时候,指定了动态链接器为64的/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o中主要定义了程序入口_start、初始化函数_init,_start程序调用hello.c中的main函数,libc.so是动态链接共享库,链接器加入了以下函数printf、sleep、getchar、exit函数和_start中调用的__libc_csu_init,__libc_csu_fini,__libc_start_main。

函数调用:链接器解析重定条目时发现对外部函数调用的类型为R_X86_64_PLT32的重定位,此时动态链接库中的函数已经加入到了PLT中,.text与.plt节相对距离已经确定,链接器计算相对距离,将对动态链接库中函数的调用值改为PLT中相应函数与下条指令的相对地址,指向对应函数。 rodata引用:链接器解析重定条目时发现两个类型为R_X86_64_PC32的对.rodata的重定位(printf中的两个字符串),.rodata与.text节之间的相对距离确定,因此链接器直接修改call之后的值为目标地址与下一条指令的地址之差,指向相应的字符串。

(3) 重定位过程。hello反汇编文件中对应全局变量已通过重定位绝对引用被替换为固定地址。

5.6 hello的执行流程

(以下格式自行编排,编辑时删除) 使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。 ld-2.27.so!_dl_start ld-2.27.so!_dl_init hello!_start libc-2.27.so!__libc_start_main libc-2.27.so!__cxa_atexit libc-2.27.so!__libc_csu_init hello!_init libc-2.27.so!_setjmp libc-2.27.so!_sigsetjmp libc-2.27.so!__sigjmp_save hello!main hello!puts@plt hello!exit@plt hello!printf@plt hello!sleep@plt hello!getchar@plt ld-2.27.so!_dl_runtime_resolve_xsave ld-2.27.so!_dl_fixup ld-2.27.so!_dl_lookup_symbol_x libc-2.27.so!exit

5.7 Hello的动态链接分析

(1)编译器无法确定动态链接库中的函数地址,因为动态链接库中的函数在程序执行的时候才会确定地址。GNU编译系统采用延迟绑定技术来解决动态库函数模块调用的问题。

(2)延迟绑定通过全局偏移量表(GOT)和过程链接表(PLT)实现。

(a)PLT是一个数组,其中每个条目是16字节代码。每个库函数都有自己的PLT条目,PLT[0]是一个特殊的条目,跳转到动态链接器中。从PLT[2]开始的条目调用用户代码调用的函数。

(b)GOT同样是一个数组,每个条目是8字节的地址,和PLT联合使用时,GOT[2]是动态链接在ld-linux.so模块的入口点,其余条目对应于被调用的函数,在运行时被解析。每个条目都有匹配的PLT条目。

(3)延迟绑定的实现步骤如下: a.建立一个 GOT.PLT 表,用来放全局函数的实际地址 b.对每一个全局函数,链接器生成一个与之相对应的函数,如 puts@plt。 c.所有的puts都换成对 puts@plt。

(4)下面分析在dl_init调用前后,项目的内容的变化 a)dl_init调用前

在这里插入图片描述
图5.71 dl_init调用前GOT条目 b)dl_init调用后, GOT条目初始时指向其PLT条目的第二条指令的地址
在这里插入图片描述
图5.72 dl_init调用后GOT条目

5.8 本章小结

本章介绍了链接的概念作用,分析hello的ELF格式和虚拟地址空间,通过实例分析了hello的动态链接、执行流程、重定位过程、加载以及运行时函数调用顺序,深入理解链接和重定位的过程。

第6章 hello的进程管理

6.1 进程的概念与作用

进程的概念:进程是正在运行的程序的实例,是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。进程是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。 进程的作用:进程提供两个假象,程序独占地使用处理器和程序在独占地使用系统内存。

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

Shell-bash的作用:shell和其他软件一样都是和内核打交道,直接服务于用户。但和其他软件不同,shell主要用来管理文件和运行程序。 处理流程:shell对命令行的处理流程 (1)读取输入的命令行。 (2)解析引用并分割命令行为各个单词,其中重定向所在的单词会被保存下来,直到扩展步骤(5)结束后才进行相关处理。 (3)检查命令行结构。 (4)对第一个单词进行别名扩展。 (5)进行各种扩展。扩展顺序为:大括号扩展;波浪号扩展;参数、变量和命令替换、算术扩展;单词拆分;文件名扩展。 (6)引号去除。 (7)搜索和执行命令。 (8)返回退出状态码。

6.3 Hello的fork进程创建过程

普通的系统调用,调用一次就返回一次,而fork()调用一次,会返回两次,一次是父进程,另一个是子进程,互不干扰,调用的先后顺序由操作系统的调度算法决定。子进程永远返回0,父进程则返回子进程的ID。

fork的进程图为:

在这里插入图片描述
图6.3 fork的进程图

6.4 Hello的execve过程

execve 函数加载并运行可执行目标文件 filename, 且带参数列表 argv 和环境变量列表 envp 。只有当出现错误时,例如找不到 filename, execve 才会返回到调用程序。所以,与 fork 一次调用返回两次不同, execve 调用一次并从不返回。

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

(1)上下文及上下文切换:进程的物理实体(代码和数据等)和支持进程运行的环境。系统通过处理器调度让处理器轮流执行多个进程,实现不同进程中指令交替执行的机制称为进程的上下文切换。

(2)进程时间片:连续执行同一个进程的时间段称为时间片

(3)用户态与核心态转换:处理器通过某个控制寄存器中的一个模式位来提供限制一个应用可以执行的指令以及它可以访问的地址空间范围的功能。当设置了模式位时,进程就运行在内核模式中。没有设置模式位时,进程就运行在用户模式中。

(4)Hello进程调度的过程以及用户态与核心态的转换 调度是在进程执行的某些时刻,内核可以决定抢占当前进程并重新开始一个先前被抢占了的进程的决策。在切换的第一部分中,内核代表进程A在内核模式下执行指令。然后在某一时刻,shell加载可执行目标文件hello。在上下文切换之后,内核代表进程hello在用户模式下执行指令。之后进程hello在用户模式下运行,直到磁盘发出一个中断信号,执行一个从进程hello到进程A的上下文切换,将控制返回给进程A,进程A继续运行,直到下一次异常发生。

在这里插入图片描述
图6.5 Hello进程调度的过程以及用户态与核心态的转换

6.6 hello的异常与信号处理

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。 程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。 Hello执行过程出现的异常为:中断、故障 会产生的信号为:SIGSTP 来自终端的停止信号,SIGINT 来自键盘的中断

(1) 正常终止

在这里插入图片描述
图6.61 正常终止

(2) Ctrl + C 当按下ctrl-c之后,shell父进程收到SIGINT信号,信号处理程序结束hello,并回收hello进程。

在这里插入图片描述
图6.62 按Ctrl + C时

(3) Ctrl + Z 当按下ctrl-z之后, (a)shell父进程收到SIGSTP信号, (b)信号处理程序打印并将hello进程挂起, (c)通过ps命令看到hello进程没有被回收, 通过jobs命令看到hello进程的号为1, 通过pstree命令可以看出:之后调用fg 1将其调到前台,执行相应命令行

在这里插入图片描述

图6.63 按Ctrl + Z时

(4) 中途乱按 中途乱按不导致异常和产生信号

在这里插入图片描述
图6.64 中途乱按时

6.7 本章小结

本章首先介绍了进程的概念与作用,并简述壳Shell-bash的作用与处理流程,讲解了Hello的fork进程创建过程和execve过程,以及Hello的进程是如何执行的,如何处理hello的异常与产生的信号

第7章 hello的存储管理

7.1 hello的存储器地址空间

(1) 逻辑地址:是指由程式产生的和段相关的偏移地址部分。 在反汇编hello得到的调用puts函数的指令是call 21<main + 0x21>,逻辑地址是[puts的代码的段标识符:21<main + 0x21>]

在这里插入图片描述
图7.1 puts函数的地址 (2) 线性地址:是逻辑地址到物理地址变换之间的中间层。程式代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。 (3)虚拟地址:也叫线性地址,是一个不真实的地址。 (4)物理地址:是指出目前CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址,用于内存芯片级的单元寻址,与地址总线相对应。

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

在这里插入图片描述
图7.2 逻辑地址到线性地址的变换-段式管理

1)基本原理。 在段式存储管理中,将程序的地址空间划分为若干个段,在段式存储管理系统中,为每个段分配一个连续的分区,而进程中的各个段可以不连续地存放在内存的不同分区中。程序加载时,操作系统为所有段分配其所需内存,物理内存的管理采用动态分区的管理方法。在为某个段分配物理内存时,可以采用首先适配法、下次适配法、最佳适配法等方法。在回收某个段所占用的空间时,要注意将收回的空间与其相邻的空间合并。段式存储管理也需要硬件支持,实现逻辑地址到物理地址的映射。 2)段式管理的数据结构。 为了实现段式管理,操作系统需要进程段表、系统段表和空闲段表来实现进程的地址空间到物理内存空间的映射。 3)段式管理的地址变换。 在段式管理系统中,其逻辑地址由段号和段内地址两部分组成。处理器会查找内存中的段表,由段号得到段的首地址,加上段内地址,得到实际的物理地址,从而完成逻辑地址到物理地址的映射。

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

(1)页式存储管理的基本原理: 1)分页存储器将主存划分成多个大小相等的页架; 2)程序的逻辑地址分成页; 3)不同的页可以放在不同页架中,不需要连续 4)页表用于维系进程的主存完整性

在这里插入图片描述
图7.31 进程页表 (2)页式存储管理的逻辑地址由两部分组成:
在这里插入图片描述
图7.32页式存储管理的逻辑地址 (3) 页式存储管理的物理地址由两部分组成:
在这里插入图片描述
图7.33 页式存储管理的物理地址 (4) 页式存储管理的地址转换思路:
在这里插入图片描述
图7.34 从逻辑地址映射到物理地址 (5) 页的共享:页式存储管理能够实现多个进程共享程序和数据,包括数据共享和程序共享 (6)页式虚拟存储管理的基本思想:把进程全部页面装入虚拟存储器,执行时先把部分页面装入实际内存,然后根据执行行为,动态调入不在主存的页,同时进行必要的页面调出 7.4 TLB与四级页表支持下的VA到PA的变换
在这里插入图片描述
图7.4 VA到PA的映射过程 (1) 首先介绍以下VA和PA。VA:virtual address称为虚拟地址,PA:physical address称为物理地址。MMU是内存管理单元。MMU将VA翻译成为PA发到CPU芯片的外部地址引脚上,也就是将VA映射到PA中。MMU将VA映射到PA是以页为单位的,对于32位的CPU,通常一页为4k,物理内存中的一个物理页面称页为一个页框。 (2) TLB与四级页表支持下的VA到PA的变换和TLB与二级页表支持下的VA到PA的变换的原理相同,为了结合实例分析,下面介绍二级页表的变换。如图7.4,首先将CPU内核发送过来的32位VA[31:0]分成三段,前两段VA[31:20]和VA[19:12]作为两次查表的索引,第三段VA[11:0]作为页内的偏移,查表的步骤如下: a)从协处理器CP15的寄存器2(TTB寄存器)中取出保存在其中的第一级页表的基地址PA b)以TTB中的内容为基地址,以VA[31:20]为索引值在一级页表中查找出一项,一级页表中保存着第二级页表的基地址。 c)以VA[19:12]为索引值在第二级页表中查出一项,第二级页表中保存着物理页面的基地址,从这里可以印证一个虚拟内存的页映射到一个物理内存的页框,因为查表是以页为单位来查的。 d)有了物理页面的基地址之后,加上VA[11:0]偏移量就可以取出相应地址上的数据

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

(以下格式自行编排,编辑时删除)

在这里插入图片描述
图7.5 CPU访问内存时的硬件操作顺序 (1) 以VA为索引到cache中查找是否缓存了要读取的数据,如果cache中已经缓存了该数据则直接返回给CPU内核,如果cache中没有缓存该数据,则发出PA从物理内存中读取数据并缓存到cache中,同时返回给CPU内核。cache不只是缓存CPU内核所需要的数据,同时缓存相邻的数据。 (2) 高速缓存确定一个请求是否命中,然后抽取出被请求的字的过程,分为三步,(1)组选择、(2)行匹配、(3)字抽取。 下面是物理内存的读策略和写策略。 a) 直接映射高速缓存E=1,即每组只有一行。组选择是通过组索引位标识组。高速缓存从w的地址中间抽取出s个组索引位,这些位被解释为一个对应于一个组号的无符号整数,来进行组索引。行匹配中,确定了某个组i,接下来需要确定是否有字w的一个副本存储在组i包含的一个高速缓存行里,因为直接映射高速缓存只有一行,如果有效位为1且标志位相同则缓存命中,根据块偏移位即可查找到对应字的地址并取出;若有效位为1但标志位不同则冲突不命中,有效位为0则为冷不命中,此时都需要从存储器层次结构下一层取出被请求的块,然后将新的块存储在组索引位指示的组中的一个高速缓存行中。 b) 组相联高速缓存每个组都会保存多余一个的高速缓存行,组选择与直接映射高速缓存的组选择一样,通过组索引位标识组。行匹配时需要找遍组中所有行,找到标记位有效位均相同的一行则缓存命中;如果CPU请求的字不在组的任何一行中,则缓存不命中,选择替换时如果存在空行选择空行,如果不存在空行则通过替换策略替换其中一行。 c) 全相联高速缓存只包含一个组,其行匹配和字选择与组相联高速缓存中一样 d)写策略:分为直写和写回。

7.6 hello进程fork时的内存映射

函数fork()若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID。内核为子进程创建各种数据结构,并分配给它一个唯一的PID,新创建的子进程获得与父进程完全相同的虚拟存储空间中的一个备份这个进程的每个页面都标记为只读。

7.7 hello进程execve时的内存映射

在这里插入图片描述
图7.7进程的内存映像 execve函数加载并运行hello需要以下几个步骤: 1.删除已存在的用户区域 2.映射私有区域,为新程序创建所有新的区域结构 3.映射共享区域 4.设置当前进程上下文的程序计数器

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

(1)缺页中断及处理:在主存中查找页表时相应页表条目有效位为0且物理页号为NULL,则该页表条目处于未分配,属于缺页中断。缺页中断的异常处理程序为终止 (2)缺页故障及处理:在主存中查找页表时相应页表条目有效位为0但是物理页号指向磁盘,属于缺页故障,缺页故障的异常处理程序是从磁盘装入相应页到内存并更新页表,再返回到故障指令开始执行。

7.9 动态存储分配管理

在这里插入图片描述
图7.9块的表示图 (1)实现动态内存分配要考虑空闲块组织、放置、分割和合并。 (2)分配器分为两种:显式分配器、隐式分配器。显式分配器:要求应用显式地释放任何已分配的块。隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。隐式空闲链表的优点是简单,缺点是任何操作的开销,例如放置分配的块,要求空闲链表的搜索与堆中已分配块和空闲块的总数呈线性关系。 (3)当接收到一个内存分配请求时,从头开始遍历堆,找到一个空闲的满足大小要求的块,若有剩余,将剩余部分变成一个新的空闲块,更新相关块的控制信息。调整起始位置,返回给用户。释放内存时,仅需把使用情况标记为空闲即可。 (4)搜索可以满足请求的空闲块时,策略有以下几种:首次适应法、最佳适应法、最坏适应法和循环首次适应法

7.10 本章小结

本章结合hello介绍了逻辑地址、线性地址、虚拟地址、物理地址的概念,对段式管理与页式管理进行比较分析,分析了进程 fork 和 execve 时的内存映射的内容,描述了系统应对缺页异常的方法,最后描述了 malloc 的内存分配管理机制

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

IO设备管理方法:一个Linux文件就是一个m字节的序列:B1,B2,……,Bk,……,Bm-1。所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得所有输入和输出都能以一种统一且一致的方式来执行:a)打开文件b)Linux Shell创建的每个进程开始时都有三个打开的文件:标准输入、标准输出、标准错误。c)改变当前文件的位置。d)读写文件。e)关闭文件。

8.2 简述Unix IO接口及其函数

(1) 打开和关闭文件。open函数的函数原型是int open(char * path,int flags,mode_t mode) open函数将filename转换成一个文件描述符,并返回描述符数字。进程是通过调用open函数来打开一个已经存在的文件或者创建一个新文件。最后进程通过调用close函数关闭一个打开的文件。close函数原型是int close(int fd) (2)读和写文件应用程序是通过分别调用read和write函数来执行输入和输出的。read函数函数原型是ssize_t read(int fd ,void* buf , size_t n),从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。write函数函数原型是ssize_t write(int fd , const void* buf,size_t n),从内存位置buf复制最多n个字节到描述符为fd的当前文件位置。 (3)lseek函数off_t lseek(int fd, off_t offset , int whence)
应用程序通过lseek函数能够显示地修改当前文件的位置

8.3 printf的实现分析

(1)首先来看看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是一个字符指针 (2)printf函数中调用了vsprintf函数, 来看看vsprintf(buf, fmt, arg)的代码

int vsprintf(char *buf, const char *fmt, va_list args) 
   { 
    char* p; 
    char tmp[256]; 
    va_list p_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返回的是要打印出来的字符串的长度 (3)然后看printf中的一句:write(buf, i);printf函数调用了write函数,把缓冲区的元素的值打印。

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

在write () 函数对应的指令序列中,有用于系统调用的陷阱指令system_call。write通过执行syscall指令调用系统服务,执行打印操作。内核会通过字符显示子程序,根据传入的ASCII码到字模库读取字符对应的点阵,然后通过vram对字符串进行输出。显示芯片将按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点,最终在终端输出字符串。

8.4 getchar的实现分析

(1) 首先来看一下getchar函数:getchar由宏实现:#define getchar() getc(stdin),从标准输入里读取下一个字符,返回类型为int型,为用户输入的ASCII码或EOF。

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;
}

(2)可以看到n=read(0,buf,BUFSIZ);语句调用了read函数。read函数可以通过sys_call中断来调用内核中的系统函数。键盘中断处理子程序会接受按键扫描码并将其转换为ASCII码后保存在缓冲区,然后对缓冲区ASCII码进行读取直到接受回车键返回。

8.5本章小结

本章介绍了Linux的IO设备管理方法,并简述了Unix IO接口及其函数,对printf和getchar的实现进行分析。

结论

在linux环境下hello程序从预处理到编译再到链接,最后执行的全过程以及进程管理,存储管理及IO管理的实现方式。

hello经历的过程如下:

(1)首先通过各种预处理命令对C程序进行处理,由hello.c得到hello.i。

(2)通过编译由hello.i得到hello.s

(3)通过汇编由hello.s得到hello.o

(4)通过链接得到可执行目标文件hello,然后运行hello,在shell下输入命令./hello 1170300826 ,shell调用fork创建子进程,然后将构造好的参数列表传给execve作为参数,启动加载器并开始执行hello

(5)访问虚拟内存,通过虚拟地址在TLB和主存页表中查找转换为相应物理地址,从在虚拟内存中读取hello程序所需要的数据

(6)异常处理,对中断产生信号进行处理

(7)回收回收进程

附件

列出所有的中间产物的文件名,并予以说明起作用。

文件名称 文件属性
hello.i 预处理得到的文本文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位目标执行
hello 链接之后的可执行目标文件
hello.objdmp hello.o的反汇编代码
hello.elf hello.o的ELF格式
hello1.elf hello的 ELF格式
hello.objdmp hello的反汇编代码
hello.txt 分析重定位过程时的可执行文件hello反汇编代码

参考文献

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

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

[2] 袁春风 计算机系统基础 机械工业出版社,2018.

[3] 关于unix系统接口 普通文件io的小结 www.cnblogs.com/chentest/p/…

[4] printf 函数实现的深入剖析 www.cnblogs.com/pianist/p/3…

[5] getchar百度百科 baike.baidu.com/item/getcha…

[6] LINUX 逻辑地址、线性地址、物理地址和虚拟地址 www.cnblogs.com/zengkefu/p/…

[7] shell解析命令行的过程以及eval命令 www.cnblogs.com/f-ck-need-u…

如果有收获?希望来个两连击,给更多的人看到这篇文章 1、关注我的原创微信公众号「程序猿的进阶」,主要是IT与竞赛 2、创作不易,顺便点个赞呗,可以让更多的人看到这篇文章,激励一下我这个小白