HIT计算机系统大作业——程序人生-Hello’s P2P
摘要
计算机系统是由硬件和系统软件组成的,它们共同工作来运行应用程序。每一个程序的生命周期开始于程序员创建的文本文件——源文件,经过预处理、编译、汇编、链接之后,生成可执行文件便可被加载到内存中,由系统调用创建进程,协同其他软硬件即可让程序真正跑起来。本文通过解读hello.c程序的生命周期,具体展开对每个阶段的探索,并借助Ubuntu 20.04进行实际操作,进而更好地了解计算机系统。
关键词:计算机系统;程序;进程;
第1章 概述
1.1 Hello简介
P2P:From Program to Process
Hello程序的生命周期是从一个源程序(或者说源文件)开始的,即程序员通过编辑器创建并保存的文本文件,文件名是hello.c。在Linux系统上,编译器驱动文件GCC实现文本文件到可执行文件的转化。首先是预处理器(cpp)对文本文件进行修改,再经过编译器(ccl)处理转化成汇编文件,然后汇编器(as)进行重定位生成可重定位目标程序,最后经过链接器(ld)的链接操作得到可执行目标程序。在壳中,bash为其fork创建新的进程
O2O: From Zero-0 to Zero-0
execve函数在当前进程的上下文中加载并运行一个新程序,再经过mmp程序对自己做虚拟页映射,之后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序不再使用时,会被内核回收。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 20.04 LTS 64位/优麒麟 64位
开发与调试工具:gcc,edb,readelf,HexEdit
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
| 文件 | 作用 |
|---|---|
| hello.i | hello.c预处理之后源程序(文本) |
| hello.s | hello.i经过ccl处理的汇编程序(文本) |
| hello.o | hello.s汇编后的可重定位目标程序(二进制) |
| hello | hello.o链接后的可执行目标程序(二进制) |
| hello.out | hello反汇编后的可重定位文件 |
| hello.elf | hello.o文件生成的elf信息 |
| hello.txt | hello.o链接后objdump反汇编得到的汇编文本 |
| helloS.txt | hello.o经过objdump反汇编得到的汇编文本 |
1.4 本章小结
本章大致介绍了hello程序从文本文件到可运行的进程,最后程序不使用时被回收的整个过程,并列出了使用的环境和工具,展示了所有的中间结果。
第2章 预处理
2.1 预处理的概念与作用
- 预处理的概念
预处理器(cpp)根据以字符 #开头的命令(宏操作),修改原始的C程序。结果得到的是结果修改的C程序,通常是以.i为文件扩展名。 - 预处理的作用
(1) 预处理功能是C语言特有的功能,它是在对源程序正式编译前由预处理程序完成的。程序员在程序中用预处理命令来调用这些功能;
(2) 宏定义是用一个标识符来表示一个字符串,这个字符串可以是常量、变量或表达式。在宏调用中将用该字符串代换宏名;
(3) 宏定义可以带有参数,宏调用时是以实参代换形参。而不是“值传送”。
(4) 为了避免宏代换时发生错误,宏定义中的字符串应加括号,字符串中出现的形式参数两边也应加括号;
(5) 文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件;
(6) 条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率;
(7) 使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计;
2.2在Ubuntu下预处理的命令
对hello.c文件进行预处理的命令是:gcc -E -o hello.c -o hello.i
图 1 预处理命令
2.3 Hello的预处理结果解析
图 2 预处理结果
结果分析:查看预处理的结果,发现源文件中的宏进行了宏展开,宏调用中将用字符串代换宏名,而且代码的行数显著增加;
2.4 本章小结
本章介绍了预处理的概念和作用,并在ubantu下将hello.c文件预处理生成了hello.i文件,具体观察并简单分析的宏展开的结果。
第3章 编译
3.1 编译的概念与作用
- 编译的概念
编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包括一个汇编语言程序。 - 编译的作用
(1) 汇编语言为不同高级语言的不同编译器提供了通用的输出语言;
(2) 扫描(词法分析),语法分析,语义分析,源代码优化(中间语言生成),代码生成,目标代码优化;
3.2 在Ubuntu下编译的命令
对hello.i文件进行汇编的命令是:gcc -S hello.i -o hello.s
图 3 编译命令
3.3 Hello的编译结果解析
- 编译文件的声明——伪指令
图 4 编译文件声明
(1) .file:声明源文件;
(2) .text:代码节;
(3) .section:声明节头部表;
(4) .rotate:只读代码节;
(5) .align:声明数据或指令的地址对其方式;
(6) .string:声明字符串;
(7) .global:声明全局变量;
(8) .type:声明一个符号是数据类型还是函数类型;
- 局部变量
图 5 源代码
主函数中定义了一个局部变量i,使用了分支判断语句和循环语句;
图 6 局部变量i
3. 数据传送指令
| 指令 | 作用 |
|---|---|
| movb S,D | 传送字节 |
| movw S,D | 传送字 |
| movl S,D | 传送双字 |
| movq S,D | 传送四字 |
数据传送指令的源操作数指定的值是一个立即数,存储在寄存器或者内存中,目的操作数指定一个位置,要么是一个寄存器,要么是一个内存地址。
4. 算数操作
| 算数操作 | 作用 |
|---|---|
| leaq S,D | 加载有效地址 |
| INC D | 加1 |
| DEC D | 减1 |
| NEG D | 取负 |
| ADD S,D | 加 |
| SUB S,D | 减 |
| XOR S,D | 异或 |
| OR S,D | 或 |
| ADD S,D | 与 |
算数操作可以对寄存器/内存中的数据进行需要的操作,其中leap的指令形式是从内存读数据到寄存器,但实际上它根本就没有引用内存。
- 跳转指令
| 跳转指令 | 作用 |
|---|---|
| jmp Label | 直接跳转 |
| jmp *Operand | 间接跳转 |
| je Label | 相等/零 |
| jne Label | 不相等/非零 |
| jg Label / ja Label | 大于 |
| jl Label / jb Label | 小于 |
跳转指令会导致执行切换到程序中一个全新的位置,而跳转指令的执行是有条件的,它们根据条件码的组合,或者跳转,或者继续执行代码序列的中的下一条指令。
- 对hello.s的具体分析
(1) 寄存器访问信息
图 7 hello.s-1
%edi存储着main函数第一个参量,%rsi存储着main函数的第二个参量;
(2) 条件语句和循环语句的实现
完成寄存器的数据传输后,首先是对argc的判断:
- 如果argc!=4,把.L0段的数据传输到%rdi,然后调用puts函数输出,再调用exit(0)退出程序;
- 如果argc==4,直接跳转至.L2执行,定义变量i后,跳转至.L3,对i大小进行判断(进入for循环):
- 如果i>8,调用getchar()读入字符
- 如果i<8,执行.L4,对于printf函数传入argv[2] argv[1],两个参数,对于sleep函数,传入参数atio(argv[3]);
图 8 hello.s -2
3.4 本章小结
本章简要介绍了数据传输指令,算数运算和跳转指令等,并以此为基础对照hello.c源文件,简要解析了hello.s文件的实现过程。
第4章 汇编
4.1 汇编的概念与作用
- 汇编的概念
汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序,并把结果保存再hello.o文件中; - 汇编的作用
汇编语言的大部分语句直接对应机器指令,执行速度快,效率高,代码体积小,在某些存储器容量有限;
4.2 在Ubuntu下汇编的命令
对hello.s文件进行预处理的命令是:gcc hello.s -c -o hello.o
图 9 汇编命令
4.3 可重定位目标elf格式
-
对hello.o使用命令:readelf -h hello.o
图 10 ELF Header -
ELF Header文件:
(1) ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
(2) 节头——记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐
图 11 节头
(3) 重定位节——偏移量,信息,类型,符号值及符号加数
图 12 重定位节
(4) 符号表——存放在程序中定义和引用的函数和全局变量的信息
图 13 符号表
4.4 Hello.o的结果解析
-
反汇编指令:objdump -d -r hello.o > helloS.txt
-
反汇编结果
图 14 hello.o反汇编结果 -
分析hello.o的反汇编,与第3章的 hello.s进行对照分析:
(1) 函数调用
反汇编代码使用的是<main+0x25>等确定的地址,需要添加重定位条目等待链接,而汇编代码函数调用时直接使用函数名称;
(2) 分支转移
反汇编代码使用的是确定的地址(主函数+段内偏移量),而汇编代码中直接使用的是.L1等助记符,段名称在汇编语言中是便于编写的助记符,汇编之后转化成对应的确定地址;
(3) 全局变量
反汇编代码访问全局全局变量0x0(%rip),而汇编代码是.LC0(%rip);
4.5 本章小结
本章讲述了汇编的概念作用,并对hello.s文件进行汇编生成hello.o文件。在详细介绍ELF头后,介绍了hello.o的可重定位目标格式文件。最后对hello.o文件进行反汇编操作,得到的结果和汇编文件进行对比,求同存异。
第5章 链接
5.1 链接的概念与作用
- 链接概念
链接操作实际上是给系统中已有的某个文件指定另外一个可用于访问它的名称,链接器将程序调用的函数与标准库的函数链接合并,结果得到hello文件,它是一个可执行目标文件,可以加载到内存中,由系统执行。 - 链接作用
(1) 静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。
(2) 动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在Windows的管理下,才在应用程序与相应的DLL之间建立链接关系。当要执行所调用DLL中的函数时,根据链接产生的重定位信息,Windows才转去执行DLL中相应的函数代码。
5.2 在Ubuntu下链接的命令
对hello.o文件进行链接的命令是:
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
图 15 链接命令
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
图 16 hello的ELF格式
-
ELF头(与汇编文件的不同之处)
(1) 类型不同
hello的文件类型是EXEC(可执行文件),hello.o的文件类型是REL(可重定位文件)
(2) 入口点地址
hello的入口点地址是0x4010f0,hello.o的入口点地址是0x0
(3) 程序头大小
hello: 64(bytes) hello.o:0
(4) 节头数量
hello: 27 hello.o:14 -
节头部表
节头部表对hello的节信息进行了声明,包括不同节的大小,类型,地址(虚拟地址的起始地址)以及偏移量。
图 17 hello的ELF Header -
重定位节
重定位是连接符号引用与符号定义的过程,说明如何修改其节内容的信息。通过此信息,可执行文件和共享目标文件可包含进程的程序映像的正确信息。重定位项即是这些数据;
图 18 hello的重定位节 -
符号表
存放再程序中定义和引用的函数和全局变量的信息;
图 19 hello的符号表
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
hello虚拟地址空间.ELF的起始段位0x00400000,
图 20 hello起始段
对照之前的ELF Header,得知.text的虚拟地址空间首地址为0x004010f0:
图 21 hello-.text
.init的虚拟地址空间首地址为0x00401000
图 22 hello-.init
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
图 23 hello的反汇编命令
- hello的反汇编代码和hello.o的不同:
(1) hello的反汇编代码中函数调用和跳转指令不再是使用“主函数+段内偏移量”的形式,而是使用虚拟地址空间中的具体地址,说明hello已经完成了重定位;
图 24 hello反汇编-1
(2) hello的反汇编代码中还出现了除之外的,<.plt>等节,而且hello引用了静态库里的atoi,puts等函数;
图 25 hello的反汇编-2 - 重定位过程
(1) 重定位节和符号定义
链接器将所有相同类型的节合并为同一类型的新的聚合节。当这一步完成时,程序中的每一条指令和全局变量都有唯一的运行时内存地址。
(2) 重定位节中的符号引用
依赖于可重定位目标模块中重定位条目的数据结构,链接器修改代码节和数据节中对每个符号的引用,使用它们指向正确的运行时地址。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
程序名 程序地址
libc-2.31.so!_libc_start_main 0xf7d1:bdf0
libc-2.31.so!_cxa_atexit 0xf7d1:be38
libc-2.31.so!_setjmp 0xf7d1:be9e
libc-2.31.so!_exit 0xf7d1:beec
5.7 Hello的动态链接分析
共享库是一个目标模板,在运行时或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来,这个过程成为动态链接,是由一个在内存中的动态链接器的程序来执行的。
- PIC数据引用
在数据段开始的地方创建一个全局偏移量表,在GOT中,每个被这个目标模块引用的全局数据目标都有一个8字节条目。编译器还为GOT中每个条目生成一个重定位记录。
图 26 全局偏移量表 - PIC函数调用
在调用共享库函数时,编译器不能预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定,将过程地址的绑定推迟到第一次调用该过程时。
图 27 PLTGOT的大小信息
5.8 本章小结
在本章中主要介绍了链接的概念与作用,并且详细讲述了hello.o链接生成可执行目标文件hello的过程,详细介绍了hello.o的ELF格式和各个节的含义,依次分析了hello的虚拟地址空间、重定位过程、可执行目标文件hello的格式、执行流程、动态链接分析
第6章 hello的进程管理
6.1 进程的概念与作用
- 进程的概念
进程是操作系统对一个正在运行的程序的一种抽象。 - 进程的作用
进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序一样,独占地使用处理器和内存。处理器好像是无间断的执行程序中的指令,程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
- Shell:一个用C语言编写的程序,通过Shell用户可以访问操作系统内核服务;
- 作用:Shell既是一种命令语言,又是一种程序设计语言。作为命令语言,它交互式地解释和执行用户输入的命令;作为程序设计语言,它定义了各种变量、参数、函数、流程控制等等。它调用了系统核心的大部分功能来执行程序、建立文件并以并行的方式协调各个程序的运行;
- 处理流程
(1)交互Shell(bash)fork/exec一个子Shell(sh)用于执行脚本,父进程bash等待子进程sh终止;
(2)sh读取脚本中的cd …命令,调用相应的函数执行内建命令,改变当前工作目录为上一级目录;
(3)sh读取脚本中的ls命令,fork/exec这个程序,列出当前工作目录下的文件,sh等待ls终止;
(4)ls终止后,sh继续执行,读到脚本文件末尾,sh终止;
(5)sh终止后,bash继续执行,打印提示符等待用户输入;
6.3 Hello的fork进程创建过程
- fork函数在创建进程的过程中,子进程会得到父进程用户级虚拟地址空间相同但独立的一份副本,其中包括代码、数据段、堆、共享库以及用户栈。除此之外,子进程还会得到与父进程任何打开文件描述符相同的副本,而父进程和子进程之间最大的差别就是它们PID不同。
图 28 fork函数创建子进程
6.4 Hello的execve过程
- execve:加载并运行程序
- int execve(char *filename, char *argv[], char *envp[])
(1)在当前进程中载入并运行程序: loader加载器函数
(2)filename:可执行文件目标文件或脚本
(3)argv:参数列表,惯例:argv[0]==filename
(4)envp:环境变量列表 - execve过程
Loader删除子进程现有的虚拟内存段,创建一组新的段(栈与堆初始化为0),并将虚拟地址空间中的页映射到可执行文件的页大小的片chunk,新的代码与数据段被初始化为可执行文件的内容,然后跳到_start………… 除了一些头部信息实际没读文件,直到缺页中断
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
图 29 hello 运行结果
6.6 hello的异常与信号处理
异常是指为响应某个事件将控制权转移到操作系统内核中的情况
- 异常的种类及其处理
(1) 中断
中断是异步发生的,是来此处理器外部的I/O设备的信号的结果。硬件中断发生时,在当前指令完成后,控制传递给异常处理程序,执行结束后中断处理程序运行,返回下一条指令。
(2) 陷阱
陷阱是有意的同步发生的异常,是执行一条指令的结果。陷阱最重要的用途是用户程序和内核之间提供一个过程一样的接口,叫做系统调用。但陷阱处理程序结束后也会返回原程序控制流的下一条指令。
(3) 故障
故障是由错误引起的,也是同步发生的,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的absort例程,进而终止引起故障的应用程序。
(4) 终止
终止是不可恢复的致命错误造成的同步发生的结果,通常是一些硬件错误,终止处理程序从不将控制返回给应用程序。 - 信号的种类及其处理(hello执行可能出现的信号)
(1) SIGINT
默认:终止——相应事件:来自键盘的中断
(2) SIGKILL
默认:终止——相应事件:杀死进程
(3) SIGSEGV
默认:终止——相应事件:无效的内存引用(段故障)
(4) SIGALRM
默认:终止——相应事件:来自alarm函数的定时器信号
(5) SIGCHLD
默认:忽略——相应事件:一个子进程停止或者终止
(6) SIGCONT
默认:忽略——相应事件:继续进程如果该进程停止 - 键盘输入指令
(1) 正常运行
图 30 hello 运行结果
(2) 回车——随意输出字符,会导致程序结束运行
图 31 hello-命令行输入回车
(3) Ctrl-Z——键盘上输入Ctrl-Z后,内核会向进程发送一个SIGSTP信号到前台进程组中的每个进程。默认情况下,结果是停止(挂起)前台作业。
图 32 hello-Ctrl-Z/ps/jobs/pstree
- ps——前台进程组中各个进程的情况
- jobs——进程中作业的情况
- pstree——以树的形式展示所有的进程
- kill——杀死第n个进程kill -9 n
图 33 hello-kill
(4) Ctrl-C——键盘上输入Ctrl-C后,内核会发送一个SIGINT信号到前台进程组中的每个进程。默认情况下,结果是终止前台作业。
图 34 hello-Ctrl-C
6.7本章小结
本章回顾了进程的定义和作用,并简要讲述了shell和bash的作用与处理流程,接着详细介绍了fork创建进程的过程、execve加载并运行程序的过程、hello进程执行的过程以及hello的异常与信号处理过程。
第7章 hello的存储管理
7.1 hello的存储器地址空间
- 逻辑地址
逻辑地址是指CPU所生成的地址,是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段描述符:段内偏移量]。逻辑地址用来指定一个操作数或者是一条指令的地址。 - 虚拟地址
虚拟地址是由程序产生的由段描述符和段内偏移量组成的地址。这两部分组成的地址并不能直接访问物理内存,而是要通过分段地址的变化处理后才会对应到相应的物理内存地址。 - 线性地址
线性地址是逻辑地址到物理地址变换之间的中间层。程序代码产生的逻辑地址(或说是段中的偏移地址),加上相应段的基地址就生成了一个线性地址。 - 物理地址
物理地址是加载到内存地址寄存器中的地址,内存单元的真正地址。在前端总线上传输的内存地址都是物理内存地址,编号从0开始一直到可用物理内存的最高端。每个字节都有对应的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
- 段式管理
逻辑地址+段的基地址->线性地址==虚拟地址 - 实现逻辑的地址像线性地址的转换
被选中的段描述符先被送至描述符cache,每次从描述符cache中取32位段基址,与32位段内偏移量(有效地址)相加得到线性地址。 - Linux中的分段机制
若把运行在用户态的所有Linux进程使用的代码段和数据段分别称为用户代码段和用户数据段;把运行在内核态的所有Linux进程使用的代码段和数据段分别称为内核代码段和内核数据段,则初始化时,这四个段描述符都存放在GDT中。每个段都被初始化在0~4GB的线性地址空间中。
图 35 逻辑地址转换为线性地址
7.3 Hello的线性地址到物理地址的变换-页式管理
-
页式管理
虚拟地址(线性地址)->物理地址 -
虚拟页面
VM系统通过将虚拟内存分割为称为虚拟页的大小固定的块来处理磁盘上数据分割块的问题。
未分配的:VM系统还未分配(或者创建)的页。未分配的块没有任何数据和它们关联,因此也就不占任何磁盘空间。
缓存的:当前已缓存在物理内存中的已分配页。
未缓存的:未缓存在物理内存中的已分配页。
图 36 虚拟页和物理页中间的映射 -
页表
页表是一个页表条目的数组,将虚拟页地址映射到物理页地址。它是DRAM中的每个进程都使用的核心数据结构。
图 37 页表 -
页面处理操作
(1)页命中——虚拟内存中一个字存在于物理内存中(即DRAM缓存命中)
(2)缺页——虚拟内存中的字不在物理内存中 (DRAM 缓存不命中)
(3)缺页处理——地址翻译硬件从内存中读取PTE,从有效位判断对应的VP未被缓存,这次该程序会选择一个牺牲页,如果牺牲页已经被修改,则内核就会将它复制回磁盘
(4)分配页面——当操作系统分配一个新的虚拟内存页时,VP的分配过程是在磁盘上创建空间并更新PTE,使它指向磁盘上这个新创建的页面图 38 缺页处理
7.4 TLB与四级页表支持下的VA到PA的变换
- TLB
TLB,即翻译后备缓冲器,是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块,可实现虚拟页码向物理页码的映射,对于页码数很少的页表可以完全包含在TLB中。 - 四级页表支持下的VA到PA的变换
图 39 四级页表
四级页表将虚拟地址翻译成物理地址,36位VPN被划分成四个9位的片,每个片被用作一个页表的偏移量。CR3寄存器包含L1页表的物理地址。VPN1提供到一个L1 PTE的偏移量,这个PTE包括L2页表基地址。VPN2提供到一个L2 PTE的偏移量,以此类推。而虚拟地址的VPO直接作为物理地址的PPO。
图 40 地址翻译及物理访存
7.5 三级Cache支持下的物理内存访问
- 物理内存访问
(1) 定位组
取出物理地址的组索引位,根据组索引位得到组数;
图 41 组相联高速缓存
(2) 检查集合中的任何行是否有匹配的标记
在高速缓存在搜索组内每一行,寻找有效行,其标记与地址中的标志相匹配;
(3) 定位组成功 + 行有效: 命中
如果高速缓存找到了这样一行,那么就表明命中;
(4) 定位从偏移开始的数据
命中之后,根据块偏移提供了的第一个字节,把这个字节的内容取出返回给CPU即可;
(5) 不命中的结果
如果高速缓存不满足,那么需要访问下一级缓存;
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
加载并运行当前程序的步骤:
- 删除已存在的用户区域
- 映射私有区域
为新程序的代码、数据、bss和栈区域创建新的区域结构; - 映射共享区域
如当前程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间的共享区域; - 设置程序计数器
7.8 缺页故障与缺页中断处理
- 缺页故障:DRAM缓存不命中称为缺页
- 缺页中断处理:地址翻译硬件从内存中读取PTE,从有效位判断对应的VP未被缓存,这次该程序会选择一个牺牲页,如果牺牲页已经被修改,则内核就会将它复制回磁盘。
7.9动态存储分配管理
- 动态内存分配器维护着一个进程的虚拟内存区域,而内核维护着一个变量brk,它指向堆的顶部,分配器将堆视为一组大小不同块的集合,;
- 分配器有两种基本风格,两个风格都要求应用显式地分配块;
显式分配器:要求应用显式地释放任何已分配的块;
隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用; - 分配器的要求:
(1) 无法控制分配块的数量或大小
(2) 立即响应 malloc 请求
(3) 必须从空闲内存分配块
(4) 必须对齐块,使得它们可以保护任何类型的数据对象
(5) 只能操作或改变空闲块
(6) 一旦块被分配,就不允许修改或移动它了 - 实现:
(1) 记录空闲块
(2) 寻找空闲块分配新块
(3) 分割内存块
(4) 合并内存块 - 带边界标签的隐式空闲链表分配器原理
(1) 边界标记是在块首有头部的基础上,再在每个块的结尾处添加一个脚部,那么分配器就可以检查脚部,来判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。
(2) 适配(找到一个空闲块)->分割(分配空闲块)->合并(合并空闲块) - 显式空间链表的基本原理
采用双向链表而不是隐式空闲链表,使得首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。
(1) 策略一:后进先出的顺序维护链表
(2) 策略二:按照地址顺序来维护链表
7.10本章小结
本章主要介绍hello的存储器的地址空间,运用段式管理把逻辑地址到线性地址,运用页式管理把线性地址转化为物理地址,接着介绍了cache下的物理访存,fork时的内存映射,execve时的内存映射,最后介绍了缺页故障与缺页中断处理以及动态存储分配管理
第8章 hello的I/O管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m字节的序列:B0,B1,B2……Bm
设备接入系统后都是以文件形式存在的,IO分别是input和output的英文首字符,分别代表输入和输出。文件没有打开时,是存放再块设备中的文件系统里的,这样的文件叫做静态文件。
硬盘管理的时候是以文件为单位的,操作系统最初拿到的信息是文件名,最终得到的是文件内容。第一步就是去查询硬盘内容管理表,这个管理表中以文件为单位记录了各个文件的各种信息,每个文件一个信息列表(inode),每个信息列表(inode)有一个数字编号,对应一个结构体,结构体中记录了各种信息。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
-
Unix IO接口
打开关闭文件: open() close();
读写文件:read() write();
改变当前文件的位置seek();
Unix shell创建的每个进程都以三个打开的文件开始,与终端相关联:
(1)标准输入standard input;
(2)标准输出standard output;
(3)标准误差standard error; -
Unix 函数:
(1) open() / close()函数
int open(char* filename,int flags,mode_t mode)函数
功能:打开存在的文件或者创建新的文件;
返回值:filename 转换成的一个文件描述符;
int close(fd)
功能:关闭所打开的文件;
返回值:操作结果;
(2) read() /write() 函数
ssize_ t read (int fd,void *buf,size_ tn)
返回:若成功则为读的字节数,若EOF则为0,若出错则为-1;
read函数从描述符为fd的当前文件位复制最多n个字节到内存位置buf,返回值为-1表示一个错误,返回值为0表示EOF,否则,返回值为实际传送的字节数量。
ssize_ t write (int fd,void *buf,size_ _tn)
返回:若成功则为读的字节数,若出错则为-1;
Write函数从描述符为buf的当前文件位置复制最多n个字节到描述符buf的当前位置。
8.3 printf的实现分析
图 42 printf函数体
(1) va_list是一个字符指针,(char*)(&fmt) + 4) 表示的是…中的第一个参数,fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素;
(2) vsprintf返回的是要打印出来的字符串的长度,其作用就是格式化,它接受确定输出格式的格式字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出;
(3) 在printf中调用系统函数write(buf,i)将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall;
(4) yscall将字符串中的字节从寄存器中通过总线复制到显卡中,显存中存储的是字符的ASCII码,然后字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中;
(5) 字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(1) getchar函数的书写格式:c=getchar();
(2) 作用:从系统隐含的输入设备(如键盘)输入一个字符;
(3) 具体实现:
- 异步异常-键盘中断的处理:当用户从输入设备(如键盘)进行输入时,通过调用键盘中断处理子程序,接受按键扫描码转成ascii码,将其保存到系统的键盘缓冲区中;
- getchar()等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了Linux的IO设备管理办法,Unix IO接口及其函数,并对printf和getchar的具体实现进行了分析。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
- hello.c
程序员编写的源程序,是一个二进制文本文件,并且其字符都是用ASCII编码表示; - hello.i
hello.c经过预处理阶段生成hello.i,hello.i是经过宏展开的文本文件; - hello.s
hello.i经过编译阶段生成hello.s,hello.s是一个汇编语言程序; - hello.o
hello.s经过汇编阶段生成hello.o,hello.o是二进制文件,是程序的指令编码; - hello
hello.o与可重定位目标文件和动态链接库链接成为可执行文件hello; - 创建进程
操作系统为hello程序的运行创建一个新的进程,这个新的进程可以通过execve()加载并运行该程序,也可以通过fork()为父进程创建子进程,子进程与父进程共享相同的代码操作,但是独立运行的。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
计算机系统的设计思想和实现都是基于抽象实现的。
文件是对I/O设备的抽象,虚拟内存是对主存和磁盘I/O的抽象表示,进程则是对处理器、内存和I/O设备的抽象表示。抽象的使用是计算机科学中最为重要的概念之一。学习掌握理解抽象的思维方式,对计算机的学习有着巨大帮助,是每一名计算机人应该重视的思维模式。
附件
| 文件 | 作用 |
|---|---|
| hello.i | hello.c预处理之后源程序(文本) |
| hello.s | hello.i经过ccl处理的汇编程序(文本) |
| hello.o | hello.s汇编后的可重定位目标程序(二进制) |
| hello | hello.o链接后的可执行目标程序(二进制) |
| hello.out | hello反汇编后的可重定位文件 |
| hello.elf | hello.o文件生成的elf信息 |
| hello.txt | hello.o链接后objdump反汇编得到的汇编文本 |
| helloS.txt | hello.o经过objdump反汇编得到的汇编文本 |
参考文献
[1] 深入理解计算机系统原书第3版 Randal E.Bryant David R. O’Hallaron
[2] https://www.cnblogs.com/pianist/p/3315801.html
[3] https://blog.csdn.net/xiaoguaihai/article/details/8705992
[4] https://docs.huihoo.com/c/linux-c-programming/
本文以Hello程序为例,详细解析从源文件到可执行文件的全过程,包括预处理、编译、汇编、链接等关键阶段,并探讨进程管理、存储管理和I/O管理等内容。

被折叠的 条评论
为什么被折叠?



