2022hit计算机系统大作业
摘 要
本论文结合csapp课本研究了hello.c这一简单c语言文件在Linux系统下从诞生到结束的整个生命周期,以其原始程序开始,依次深入研究了预处理、编译、链接、加载、运行、终止和回收的过程,深入了解了hello.c的“一生”。通过对hello.c的漫游对计算机系统知识有了更深刻的了解。
关键词:计算机系统;程序生命周期;Linux;hello程序
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述... - 4 -
1.1 Hello简介... - 4 -
1.2 环境与工具... - 4 -
1.3 中间结果... - 5 -
1.4 本章小结... - 5 -
第2章 预处理... - 6 -
2.1 预处理的概念与作用... - 6 -
2.2在Ubuntu下预处理的命令... - 6 -
2.3 Hello的预处理结果解析... - 6 -
2.4 本章小结... - 7 -
第3章 编译... - 8 -
3.1 编译的概念与作用... - 8 -
3.2 在Ubuntu下编译的命令... - 8 -
3.3 Hello的编译结果解析... - 8 -
3.3.1按文件结构分析... - 8 -
3.3.2数据类型... - 9 -
3.3.3赋值操作... - 10 -
3.3.4类型转换... - 10 -
3.3.5算术操作... - 10 -
3.3.6关系操作... - 11 -
3.3.7数组/指针/结构操作... - 11 -
3.3.8控制转移... - 11 -
3.3.9函数操作... - 12 -
3.4 本章小结... - 14 -
第4章 汇编... - 15 -
4.1 汇编的概念与作用... - 15 -
4.2 在Ubuntu下汇编的命令... - 15 -
4.3 可重定位目标elf格式... - 15 -
4.4 Hello.o的结果解析... - 18 -
4.5 本章小结... - 19 -
第5章 链接... - 20 -
5.1 链接的概念与作用... - 20 -
5.2 在Ubuntu下链接的命令... - 20 -
5.3 可执行目标文件hello的格式... - 20 -
5.4 hello的虚拟地址空间... - 24 -
5.5 链接的重定位过程分析... - 25 -
5.6 hello的执行流程... - 27 -
5.7 Hello的动态链接分析... - 27 -
5.8 本章小结... - 28 -
第6章 hello进程管理... - 29 -
6.1 进程的概念与作用... - 29 -
6.2 简述壳Shell-bash的作用与处理流程... - 29 -
6.3 Hello的fork进程创建过程... - 29 -
6.4 Hello的execve过程... - 30 -
6.5 Hello的进程执行... - 31 -
6.6 hello的异常与信号处理... - 32 -
6.7本章小结... - 35 -
第7章 hello的存储管理... - 36 -
7.1 hello的存储器地址空间... - 36 -
7.2 Intel逻辑地址到线性地址的变换-段式管理... - 36 -
7.3 Hello的线性地址到物理地址的变换-页式管理... - 37 -
7.4 TLB与四级页表支持下的VA到PA的变换... - 38 -
7.5 三级Cache支持下的物理内存访问... - 39 -
7.6 hello进程fork时的内存映射... - 40 -
7.7 hello进程execve时的内存映射... - 40 -
7.8 缺页故障与缺页中断处理... - 41 -
7.9动态存储分配管理... - 41 -
7.10本章小结... - 43 -
第8章 hello的IO管理... - 44 -
8.1 Linux的IO设备管理方法... - 44 -
8.2 简述Unix IO接口及其函数... - 44 -
8.3 printf的实现分析... - 45 -
8.4 getchar的实现分析... - 47 -
8.5本章小结... - 47 -
结论... - 48 -
附件... - 49 -
参考文献... - 50 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1. CPU:inter core i5 1035G1 CPU @ 1.00GHz
2. RAM:16.00GB 系统类型:64位操作系统,基于x64的处理器
windows11 64位,VMware® Workstation 15 Pro,Ubuntu 20.04
Visual Studio2022 64bit、gcc、gedit、edb、readef
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名称 |
文件作用 |
hello.i |
预处理之后文本文件 |
hello.s |
编译之后的汇编文件 |
hello.o |
汇编之后的可重定位目标执行 |
hello |
链接之后的可执行目标文件 |
helloo.objdmp |
Hello.o 的反汇编代码 |
hello.elf |
Hello.o 的 ELF 格式 |
hello.objdmp |
Hello 的反汇编代码 |
hello2.elf |
Hello的 ELF 格式 |
1.4 本章小结
本章主要介绍了Hello自白中P2P和020的含义,实验所需的环境和工具,以及中间结果。
第2章 预处理
2.1 预处理的概念与作用
2.2在Ubuntu下预处理的命令
在Ubuntu下预处理的命令:gcc Hello.c -E -o Hello.i
2.3 Hello的预处理结果解析
打开hello.i发现程序扩展到了3060行,行数大幅增加,而main函数出现在3047行之后。
2.4 本章小结
本章主要讲述了预处理的概念和作用,Ubuntu下预处理的命令,以及结合实际程序对预处理结果的解析。
第3章 编译
3.1 编译的概念与作用
作用:将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做准备。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
在Ubuntu下编译的命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1按文件结构分析
指令 |
含义 |
.file |
源文件 |
.text |
代码段 |
.section .rodata |
只读变量 |
.globl |
声明一个全局变量 |
.type |
用来指定是函数类型或是对象类型 |
.size |
声明大小 |
.long、.string |
声明一个 long、string 类型 |
.align |
声明对指令或者数据的存放地址进行对齐的方式 |
3.3.2数据类型
编译器将局部变量存储在寄存器或者栈空间中。在hello.s中编译器将i存储在栈空间-4(%rbp)中。
argc是main函数的参数之一,64位编译下,由寄存器传入,进而保存在堆栈中。
程序中涉及的数组为char *argv[],即函数的第二个参数。在hello.s中,数组的首地址保存在栈中。访问时,通过寄存器寻址的方式访问。
3.3.3赋值操作
程序中仅有一个赋值操作i=0,使用mov指令完成,而使用不同的后缀可以对不同大小的值进行操作。
后缀 |
b |
w |
l |
q |
大小(字节) |
1 |
2 |
3 |
4 |
3.3.4类型转换
hello.s中只出现了一次类型转换:使用atoi函数将字符串(输入的秒数)转换成对应的整型数。
3.3.5算术操作
2. addq $8, %rax(或者$16,%rax):修改地址偏移量
3.3.6关系操作
(1)argc!=4:判断argc不等于4。hello.s中使用cmpl $4,-20(%rbp),计算argc-4然后设置条件码,再je根据条件码决定是否跳转。
(2)i<8:检查i是否小于8。在hello.s中,使用cmpl $8, -4(%rbp)比较i与7的大小,然后设置条件码,再jle根据条件码决定是否跳转。
3.3.7数组/指针/结构操作
3.3.8控制转移
1.if(argc!=4):if判断argc的取值后的控制转移。编译时使用cmpl指令将argc和4进行比较,并设置条件码。跳转指令je根据条件码决定是否跳转。控制转移由指令je完成。
3.3.9函数操作
- 函数表达式:函数作为表达式中的一项出现在表达式中,以函数返回值参与表达式的运算。这种方式要求函数是有返回值的。
- 函数语句:函数调用的一般形式加上分号即构成函数语句。
- 函数实参:函数作为另一个函数调用的实际参数出现。这种情况是把该函数的返回值作为实参进行传送,因此要求该函数必须是有返回值的。
- 传递控制:进行过程 Q 的时候,程序计数器必须设置为 Q 的代码的起始地址,然后在返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的地址。
- 传递数据:P 必须能够向 Q 提供一个或多个参数,Q 必须能够向 P 中返回一个值。
- 分配和释放内存:在开始时,Q 可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。
1 |
2 |
3 |
4 |
5 |
6 |
7 及以上 |
%rdi |
%rsi |
%rdx |
%rcx |
%r8 |
%r9 |
栈 |
函数调用:main 函数被系统启动函数__libc_start_main 调用,call 指令将下一条指令的地址 dest 压栈,然后跳转到 main 函数。
参数传递:向main函数传递的参数是argc和argv,分别使用%rdi(%edi)和%rsi存储。
函数返回:函数设置%eax为0后就正常退出,使用leave退出。
第一次函数调用:这一次printf函数在具体的汇编代码中被优化为puts函数。
第一次参数传递:首先将rdi赋值为字符串“Usage: Hello 学号 姓名! \n”字符串的首地址(leaq .LC0(%rip), %rdi),然后调用了puts函数,将字符串参数传入。
第二次参数传递:显然这里需要传递3个参数,%rdi保存的是“Hello %s %s\n”的首地址,%rsi保存的是argv[1],%rdx保存的是argv[2]。
参数传递:通过movq %rax, %rdi完成参数传递,%rid保存的是argv[3]。
参数传递:通过movl %eax, %edi完成参数传递,%rid保存的是atoi(argv[3])。
函数调用:通过汇编语句call getchar@PLT调用。
参数传递:通过汇编语句movl $1,%edi将%edi寄存器内容设置为1。
3.4 本章小结
本章主要讲了编译的概念和作用,以及实现编译的指令,并且以hello.s为例行对数据、赋值、类型转换、算术操作、关系操作、数组/指针/结构操作以及控制转移和函数操作进行了详细的分析和研究。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编是指汇编器(assembler)将以.s结尾的汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在.o目标文件中的过程。
作用:将以.s结尾的汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在.o目标文件中。
注意:这儿的汇编是指从 .s 到 .o即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
在Ubuntu下汇编的命令:as hello.s -o hello.o
4.3 可重定位目标elf格式
首先,在shell中输入readelf -a hello.o > hello.elf 指令获得 hello.o 文件的 ELF 格式:
2.节头:也就是节头部表,包含了文件中出现的各个节的语义,包括节的类型、地址、大小、偏移量等信息。
attend:一些重定位要使用它对被修改引用的值做偏移调整;
ELF定义了32种不同的重定位类型,两种最基本的重定位类型包括R_X86_64_PC32(重定位使用32位PC相对地址的引用)和R_X86_64_32(重定位使用32位绝对地址的引用)。
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
4.4 Hello.o的结果解析
使用objdump -d -r hello.o>helloo.objdump 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
与hello.s对比可知,除去格式上的差别外,二者总体上是相似的,主要差别如下:
1.立即数的变化:hello.s中的立即数都是用10进制数表示的。但是在objdump的结果中,由于转换成了二进制代码,因此立即数都是用16进制数表示的。
4.5 本章小结
第5章 链接
5.1 链接的概念与作用
作用:链接使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,我们可以独立的修改和编译那些更小的模块,这也更加便于我们维护和管理我们的代码。
注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
与链接前的ELF 头比较,基本信息未发生改变(如Magic,类别等),而类型发生改变,程序头大小和节头数量增加,并且获得了入口地址。
2.节头:hello2.elf中的节头包含了文件中出现的各个节的语义,包括节的类型、位置、偏移量和大小等信息。与链接前的hello.elf相比,其在链接之后的内容更加详细(多了13个节)。
3.程序头部表描述了可执行文件的连续的片映射到连续的内存段的映射关系。包括目标文件的偏移、段的读写/执行权限、内存的开始地址、对齐要求、段的大小、内存中的段大小等。
5.4 hello的虚拟地址空间
使用edb加载hello,可以看到进程的虚拟地址空间各段信息。可以看出,段的虚拟空间从0x400000开始,到0x400fff结束。
通过观察edb的Symbols小窗口,我们发现从虚拟地址从0x400000开始和5.3节中的节头表几乎是一一对应的。
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
使用objdump -d -r hello>hello.objdump指令生成hello的反汇编文件
2.hello.objdump比helloo.objdump多了许多节,不仅有很多helloo.objdump没有的节(例如.init节,.plt节等),而且.text节中的内容也比之前更多。
5.6 hello的执行流程
程序名称 |
程序地址 |
hello!_start |
0x4010f0 |
libc-2.31.so!__libc_start_main |
0x7f5c62888fc0 |
-libc-2.31.so!__cxa_atexit |
0x7f5c628abe10 |
hello!__libc_csu_init |
0x4011c0 |
hello!_init |
0x401000 |
libc-2.31.so!_setjmp |
0x7f5c628a7ca0 |
-libc-2.31.so!_sigsetjmp |
0x7f5c628a7be0 |
hello!main |
0x401125 |
hello!puts@plt |
0x401030 |
hello!exit@plt |
0x401070 |
*hello!printf@plt |
-- |
*hello!sleep@plt |
-- |
*hello!getchar@plt |
-- |
libc-2.31.so!exit |
0x7f5c928aba70 |
5.7 Hello的动态链接分析
由于无法预测函数的运行时地址,对于动态共享链接库中 PIC 函数,编译器需要添加重定位记录,等待动态链接器处理。链接器采用延迟绑定的策略,防止运行时修改调用模块的代码段。
动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。.got与.plt节保存着全局偏移量表GOT
5.8 本章小结
本章主要介绍了链接的概念与作用、hello的ELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程和动态链接过程。
第6章 hello进程管理
6.1 进程的概念与作用
1. 一个独立的逻辑控制流,提供一个假象,好像程序独占地使用处理器
2. 一个私有地址空间,提供一个假象,好像程序独占地使用内存系统
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell-bash是一个交互型应用级程序,代表用户运行其他程序。它是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并把它送入内核去执行。
6.3 Hello的fork进程创建过程
打开Shell,输入命令./hello 120L020403 刘洲 1,带参数执行生成的可执行文件。
1. hello 不是一个内置的shell命令,所以解析之后终端程序判断./hello的语义为执行当前目录下的可执行目标文件hello。
2. 之后终端程序首先会调用 fork 函数创建一个新的运行的子进程,新创建的子进程几乎父进程相同,但不完全与相同。
3. 父进程与子进程之间最大的区别在于它们拥有不同的 PID。子进程得到与父进程用户级虚拟地址空间相同的一份副本,当父进程调用 fork 时,子进程可以读写父进程中打开的任何文件。
6.4 Hello的execve过程
- 为子进程调用函数fork之后,子进程调用execve函数(传入命令行参数)在当前进程的上下文中加载并运行一个新程序hello。
- 为执行hello程序加载器、删除子进程现有的虚拟内存段,execve 调用驻留在内存中的、被称为启动加载器的操作系统代码,并创建一组新的代码、数据、堆和栈段。
- execve 函数从不返回,它将删除该进程的代码和地址空间内的内容并将其初始化,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC指向_start 地址,_start 最终调用 hello中的 main 函数。
- 除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到 CPU 引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器。
一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用CPU内存。
hello进程的执行是依赖于进程所提供的抽象的基础上,下面阐述操作系统所提供的的进程抽象:
- 逻辑控制流:一系列程序计数器 PC 的值的序列叫做逻辑控制流,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程,进程轮流使用处理器,。
- 时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
- 用户模式和内核模式:处理器通常使用一个寄存器描述了进程当前享有的特权,对两种模式区分。设置模式位时,进程处于内核模式,该进程可以访问系统中的任何内存位置,可以执行指令集中的任何命令;当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据。
- 上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态。它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
1. 当调用 sleep 之前,如果 hello 程序不被抢占则顺序执行,假如发生被抢占的情况,则进行上下文切换
2. 上下文切换是由内核中调度器完成的,当内核调度新的进程运行后,它就会抢占当前进程,并进行
3. hello初始运行在用户模式,在hello进程调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程,并将 hello 进程从运行队列中移出加入等待队列
4. 定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到设定的时间时发送一个中断信号,
5. 进入内核状态执行中断处理,将 hello 进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常。
陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常
终止:终止时不可恢复的错误,在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。
SIGINT:键入Ctrl-C后内核向hello进程发送,终止程序。
SIGSTP:键入Ctrl-Z后内核向hello进程发送,停止直到下一个SIGCONT。
SIGCONT:键入fg后内核向hello进程发送,若停止则继续执行。
SIGKILL:键入kill -9 <PID>后内核向hello进程发送,终止程序。
1.正常执行hello程序的结果:当程序执行完成之后(以键入回车结束),进程回收。
当按下 ctrl-z之后,shell的父进程收到 SIGSTP 信号,信号处理函数的逻辑是打印屏幕回显、将hello进程挂起。
通过 ps 命令我们可以看出 hello 进程其实没有被回收,此时他的后台job号是1。
通过调用fg 1将其调到前台,此时shell 程序首先打印hello的命令行命令,hello继续运行打印剩下的6条信息,之后键入回车,程序结束,同时进程被回收。
3.在程序打印2条信息之后按下ctrl-c :当按下ctrl-c之后,shell父进程收到SIGINT信号,信号处理函数的逻辑是结束hello,并回收hello进程。
4.在程序运行时按回车,会多打印几处空行,程序可以正常结束。
6.7本章小结
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址(Logical Address)是指由程序产生的与段相关的偏移地址部分,是相对应用程序而言的,如hello.o中代码与数据的相对偏移地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
计算机需要对内存分段,以分配给不同的程序使用(类似于硬盘分页)。在描述内存分段时,需要有如下段的信息:1.段的大小;2.段的起始地址;3.段的管理属性(禁止写入/禁止执行/系统专用等)。
段寄存器的唯一目的是存放段选择符,其前13位是一个索引号,后面3位包含一些硬件细节(还有一些隐藏位,此处略)。
寻址方式为:以段选择符作为下标,到GDT/LDT表(全局段描述符表(GDT)和局部段描述符表(LDT))中查到段地址,段地址+偏移地址=线性地址。
段寄存器含有段值,访问存储器形成物理地址时,处理器引用相应的某个段寄存器并将其值乘以16,形成20位的段基地址,段基地址·段偏移量=线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(VA)转换为物理地址(PA)是通过对虚拟地址存储空间进行分页来完成的。
若PTE的有效位为1,则发生页命中,可以直接获取到物理页号PPN,PPN与PPO共同组成物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
7.5 三级Cache支持下的物理内存访问
首先选择虚拟地址的组索引位,找到对应的组。再使用行匹配将虚拟地址的标记和组中各行的标记位比较。命中的条件是高速缓存行有效且标记位匹配。
高速缓存命中后,使用块偏移找到需要字节的块内偏移位置,将其取出返回给CPU。
7.6 hello进程fork时的内存映射
7.7 hello进程execve时的内存映射
- 删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
- 映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长度为零。
- 映射共享区域,hello程序与一些共享对象或目标链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障是一种常见的故障,当指令引用一个虚拟地址,在 MMU 中查找页表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出的时候就会发生故障。
图 58 缺页故障处理
缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU 重新启动引起缺页的指令,这条指令再次发送 VA 到MMU,此时MMU就能正常翻译VA了。
7.9动态存储分配管理
动态内存管理的基本方法与策略介绍如下:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
具体而言,分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
下面介绍动态存储分配管理中较为重要的概念:
1.隐式链表
堆中的空闲块通过头部中的大小字段隐含地连接,分配器通过遍历堆中所有的块,从而间接遍历整个空闲块的集合。
对于隐式链表,其结构如下:
图 59 隐式链表
2.显式链表
在每个空闲块中,都包含一个前驱(pred)与后继(succ)指针,从而减少了搜索与适配的时间。
显式链表的结构如下:
图 60 显式链表
3.带边界标记的合并
采取使用边界标记的堆块的格式,在堆块的末尾为其添加一个脚部,其为头部的副本。添加脚部之后,分配器就可以通过检查前面一个块的脚部,判断前面一个块的起始位置和状态。从而实现快速合并,减小性能消耗。
4.分离存储
维护多个空闲链表,其中,每个链表的块具有相同的大小。将所有可能的块大小分成一些等价类,从而进行分离存储。
7.10本章小结
本章主要介绍了hello 的存储器地址空间、intel的段式管理、hello的页式管理, TLB与四级页表支持下VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理以及动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,所有的输入输出都被当作对相应文件的读和写来执行。
设备管理:unix io接口
这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix I/O 接口统一操作:
- 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备。内核返回一个小的非负整数,叫做描述符。描述符在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
- Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
- 改变当前的文件位置:内核保持着每个打开的文件的一个文件位置k。k初始为0。这个文件位置k表示的是从文件开头起始的字节偏移量。应用程序能够通过执行seek,显式地将改变当前文件位置 k,例如各种fread或fwrite。
- 读写文件:读操作就是从文件复制n>0个字节到内存。从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的文件。当k>=m时,触发EOF。写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k=k+n。
- 关闭文件:内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
Unix I/O 函数:
- int open(char* filename,int flags,mode_t mode) ,进程通过调用open函数来打开一个存在的文件或是创建一个新文件的(即fopen的内层函数)。open函数将filename(文件名,含后缀)转换为一个文件描述符(C中表现为指针),并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件(读或写或两者兼具),mode参数指定了新文件的访问权限位(只读等)。
- int close(fd),fd是需要关闭的文件的描述符(C中表现为指针),close 返回操作结果。
- ssize_t read(int fd,void *buf,size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
- ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
首先看一下printf的源代码:
int printf(const char *fmt, ...)
{
int i;
va_list arg = (va_list)((char *)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
这里va_list是char类型的指针,表示arg是...中的第一个参数的地址。另外,我们发现在printf函数里分别调用了vsprintf和write函数,下面对这两个函数一一分析。
csprintf的代码:
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。用格式字符串对个数变化的参数进行格式化,产生格式化输出写入buf供系统调用write输出时使用。
vsprintf函数中也调用了write函数,write函数如下:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
在write函数中,将栈中的参数放入寄存器,ecx存放字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA表示通过系统调用sys_call。从这里也可以看出write是一个系统函数。下面是sys_call的汇编代码:
sys_call:
/*
* ecx中是要打印出的元素个数
* ebx中的是要打印的buf字符数组中的第一个元素
* 这个函数的功能就是不断的打印出字符,直到遇到:'\0'
* [gs:edi]对应的是0x80000h:0采用直接写显存的方法显示字符串
*/
xor si,si
mov ah,0Fh
mov al,[ebx+si]
cmp al,'\0'
je .end
mov [gs:edi],ax
inc si
loop:
sys_call
.end:
ret
sys_call函数实现的功能就是把将要输出的字符串从总线复制到显卡的显存中。显存中存储的是字符的ASCII码。字符显示驱动子程序通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。至此,就完成了对printf函数的分析,也完成了字符串的整个输出过程。
8.4 getchar的实现分析
当程序运行至getchar函数时,程序通过系统调用read等待用户键入字符并按回车键(通知系统输入完成),一种getchar函数的实现如下:
#include "sys/syscall.h"
#include <stdio.h>
int getchar(void)
{
char c;
return (read(0,&c,1)==1)?(unsigned char)c:EOF
//EOF定义在stdio.h文件中
}
当用户键入回车之后,getchar通过系统调用read从输入缓冲区中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1。
异步异常——键盘中断(用户输入)的处理:键盘中断处理子程序接受按键扫描码并转成ASCII码,保存在系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接收到回车键才返回。
8.5本章小结
本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数的实现。
(第8章1分)
结论
虽然hello是一个非常简单的程序,但需要计算机系统各方面的协同工作,经历了复杂的过程,漫游了整个宏大的计算机世界:
- 预处理:hello.c源代码文件通过C语言预处理器的预处理,将hello.c中include的所有外部的头文件头文件内容直接插入程序文本中,完成字符串的替换,得到了调整、展开后的ASCII文本文件hello.i;
- 编译:通过词法分析和语法分析,将合法指令翻译成等价的汇编代码。通过编译过程,编译器将hello.i 翻译成汇编语言文件 hello.s;
- 汇编:将hello.s汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,然后可以得到可重定向目标文件hello.o;
- 链接:通过链接器,将hello的程序编码与动态链接库等整合成为一个单一文件,生成可执行的目标文件hello;
- 运行:用户在shell-bash中键入执行hello程序的命令后,shell-bash解释用户的命令,找到hello可执行目标文件并为其执行fork创建新进程,fork得到的新进程通过调用execve完成在其上下文中对hello程序的加载,hello开始执行;
- 运行程序:shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序,开始载入物理内存,然后CPU进入main函数执行程序。
- 执行指令:CPU为hello分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流;
- 访问内存:内存管理单元MMU将逻辑地址,一步步映射成物理地址,进而通过三级高速缓存系统访问物理内存/磁盘中的数据;
- 动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存;
- 信号:如果运行途中键入ctr-c、ctr-z,则调用shell的信号处理函数分别停止、挂起;
- 结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
文件名称 |
文件作用 |
hello.i |
预处理之后文本文件 |
hello.s |
编译之后的汇编文件 |
hello.o |
汇编之后的可重定位目标执行 |
hello |
链接之后的可执行目标文件 |
helloo.objdmp |
Hello.o 的反汇编代码 |
hello.elf |
Hello.o 的 ELF 格式 |
hello.objdmp |
Hello 的反汇编代码 |
hello2.elf |
Hello的 ELF 格式 |
参考文献
[1] Randal E.Bryant, David O'Hallaron. 深入理解计算机系统[M]. 机械工业出版社.2018.4
[3] 汇编语言(算术运算类指令总结)_全糖去冰不加料的博客-CSDN博客_汇编语言逻辑运算指令
[5] Dynamic Section - Linker and Libraries Guide
[7] UNIX系统文件IO函数_KomaCC的博客-CSDN博客
[8] [转]printf 函数实现的深入剖析 - Pianistx - 博客园
[9] Linux - shell壳脚本 - TBHacker - 博客园
[10] printf背后的故事 - Florian - 博客园
[11] ELF文件头更详细结构_梦想之家xiao_chen的博客-CSDN博客
2022hit计算机系统大作业相关推荐
- 计算机系统大作业:Hello's P2P
计算机系统大作业 题 目 程序人生-Hello's P2P 专 业 计算机科学与技术 指 导 教 师 史先俊 计算机科学与技术学院 2019年12月 摘 要 本文介绍了一个.c源程序在计算机系统中从编 ...
- 哈工大2022计算机系统大作业
计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算机 指 导 教 师 吴锐 计算机科学与技术学院 2021年5月 摘 要 本文介绍了hello程序的一生. ...
- 2022计算机系统大作业——程序人生-Hello’s P2P
计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算机 学 号 120L021716 班 级 2003005 学 生 蔡泽栋 指 导 ...
- 哈尔滨工业大学计算机系统大作业2022春
计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算机 学 号 120L02**** 班 级 2003005 学 生 无敌飞龙 指 导 教 ...
- 哈工大csapp计算机系统大作业
计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算学部 学 号 120L022009 班 级 2003007 学 生 李沛聪 指 导 教 ...
- 哈工大计算机系统大作业
计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算机 学 号 120L022123 班 级 2003007 学 生 鹿鸣 指 导 教 师 ...
- 哈工大2022计算机系统大作业---程序人生
计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算机类 学 号 120L021923 班 级 2003006 学 生 甄自镜 指 导 ...
- HIT计算机系统大作业
计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算学部 学 号 120L022305 班 级 2003008 学 生 XXX 指 导 ...
- 哈工大计算机系统大作业——程序人生-Hello’s P2P
计算机系统 大作业 题 目程序人生-Hello's P2P 专 业 计算机科学与技术 学 号120L022401 班 级 200300 ...
最新文章
- swift_046(Swift map,filter, reduce方法)
- js /jquery停止事件冒泡和阻止浏览器默认事件
- 使用google云(GCP)二次利用安装kali Linux(kali browser)
- 消息中间件—简谈Kafka中的NIO网络通信模型
- 在线实时大数据平台Storm单机部署
- percona mysql.cnf_Percona MySQL5.6 半同步复制
- Python实现ORM
- 设计模式学习笔记——原型(Prototype)框架
- java 平均分配算法_java 分配算法
- 【蓝桥杯单片机】实战训练:基于15单片机的距离测试及电压数据采集响应系统(超声波测距、上位机、DAC输出)
- Java实现hsql_java – 从类创建HSQL创建表查询
- ubuntu iptables 共享上网
- oracle Expdp带条件,Oracle 11g expdp中query参数的使用
- 捷联惯导系统学习2.3(方向余弦阵微分方程)
- vue2 确认框 MessageBox 弹框 删除确认取消
- C# 测试网络速度例子
- 麻省理工学院——人工智能公开课06
- 导数、偏导数、梯度、方向导数、梯度下降、二阶导数、二阶方向导数
- 在线组态工具 html,组态工具-界面组件
- 超详细!动态规划详解分析(典型例题分析和对比,附源码)
热门文章
- CCS 修改字体大小
- 【Hgame2022】第一周misc和web题解
- Linux之 prefix 命令
- python2.7安装pytorch_Pytorch如何安装,Linux安装Pytorch,OSX安装Pytorch教程
- 大数据毕设/课设 - 数据大屏监控可视化设计与实现
- 微信小程序自定义card图文组件
- 7种常见分布的数学期望及其证明
- A Strong Baseline and Batch Normalization Neck for Deep Person Re-identification(论文笔记)(2019CVPR)
- android图片压缩不失真实战
- BlockingQueue drainTo()