计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021112598
班 级 21W0312
学 生 王盛
指 导 教 师 史先俊
计算机科学与技术学院
2022年5月
本文通过介绍hello程序从源代码,经过预处理、编译、汇编、链接等过程生成可执行程序,分析程序运行过程中的进程管理、存储管理等,描述了计算机系统的部分工作原理。
关键词:计算机系统;编译;存储器;
目 录
第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 -
参考文献....................................................................................... - 16 -
第1章 概述
1.1 Hello简介
hello的P2P:源代码hello.c是存储在磁盘上的Program,首先经预处理器处理得到hello.i文件,然后经编译得到hello.s,经汇编得到二进制代码hello.o,最后经链接器链接生成可执行目标程序hello,在shell中输入命令即可为其创建进程(Progress)并执行程序。
hello的020:在shell中输入命令后,shell调用fork函数为该程序创建进程,然后通过execve在进程的上下文中加载运行hello,将进程映射到虚拟内存空间,加载所需的物理内存。然后在CPU的分配下,指令进入CPU流水线执行。执行结束后,父进程回收该进程,内核清除该进程的信息,hello的进程就结束了。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
软件环境:
Windows 11; VMware® Workstation 16 Pro; Ubuntu 20.04.5 LTS;
硬件环境:
12th Gen Intel(R) Core(TM) i5-12500H 2.50 GHz; X64 CPU; 16.0GB RAM;
开发工具:
CodeBlocks 20.03-r11997;vim;
1.3 中间结果
文件名 | 作用 |
hello.c | 预处理后的代码 |
hello.s | 编译后的代码 |
hello.o | 汇编后的二进制代码 |
hello | 可执行目标文件 |
Disas_hello.s | hello.o的反汇编代码 |
hello.txt | hello的elf文件信息 |
hello_objdump.s | 以反汇编格式显示hello的重定位部分 |
elf.txt | hello.o的elf文件信息 |
1.4 本章小结
本章提纲挈领地介绍了本实验hello程序的P2P、O2O过程中使用的环境与工具,以及使用到的中间结果及其作用。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第1行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。
作用:处理头文件、宏定义、条件编译、特殊符号,删除注释。
2.2 在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
预处理得到的.i文件共3060行,而源代码仅23行,阅读hello.i代码发现,源代码main()函数在hello.i文件的第3047行处,前面的代码则是源代码中库文件内容的展开,而源代码中的注释部分则未出现在hello.i文件中。
图 1 hello.c代码
图2 hello.i中的main()
图3 hello.i中的头文件信息
2.4 本章小结
本章介绍了预处理的概念与作用、在Ubuntu下预处理的命令,展示并分析了hello.c预处理结果hello.i与源代码的关系。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:将某一种程序设计语言写的程序翻译成等价的另一种语言。
作用:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
生成的文件hello.s共80行,具体分析如下:
3.3.1 数据
- 常量
数字常量存储在.text段中:
字符串常量存储在.rodata段中:
- 变量
局部变量存储在栈或寄存器中。源代码中的局部变量i存储在-4(%rbp)处:
局部变量argc存储在-20(%rbp)处:
局部变量argv是保存输入字符串的数组,存储在栈中:
3.3.2 赋值
for循环开始时给存储在-4(%rbp)的局部变量i赋初值0:
3.3.3 算术操作
for循环每次令i++:
3.3.4 关系操作
判断argc!=4是否成立:
源代码:
汇编代码:
for循环中判断i<9是否成立:
源代码:
汇编代码(判断i<=8是否成立):
3.3.5 数组/指针/结构操作
for循环中的printf函数调用了argv[1]和argv[2]:
源代码:
汇编代码:
atoi函数中调用了argv[3]
源代码:
汇编代码:
3.3.6 控制转移
源代码:
汇编代码:
3.3.7 函数调用
调用printf:
调用atoi函数:
3.4 本章小结
本章主要介绍了编译的概念与作用、Ubuntu下编译的命令,分析了hello.i编译生成的hello.s文件中的各类数据与操作。总而言之,编译就是词义分析、判断代码是否合法后将其转换为汇编代码的过程。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序(relocatable object program)格式,并将结果保存在hello.o文件中。其中hello.o是二进制文件,其包含的17个字节是main函数的指令编码。
作用:将汇编代码根据特定的转换规则转换为二进制代码。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
4.3.1 生成elf.txt的命令
在命令行中输入readelf -a hello.o > ./elf.txt,生成elf.txt文件
4.3.2 ELF头
ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。本程序的ELF头如下:
4.3.3 节头
节头描述了hello.o文件中每一个节出现的位置、大小,目标文件的每一个节都有一个固定大小的条目。本程序的节头表如下:
4.3.4 重定位节
重定位节中包含了代码中使用的一些外部变量等信息,链接时需根据重定位节的信息,对外部变量符号选择方法计算正确地址。本程序中需要重定位的信息有.rodata中的模式串,puts,printf,sleep,getchar,exit等符号需要与相应的地址进行重定位。本程序的重定位节如下:
4.3.5 符号表
符号表.symtab存放程序中定义和引用的函数和全局变量的信息。本程序中的puts,exit,printf,atoi,sleep,getchar等函数名均在此有所体现,具体符号表如下:
4.4 Hello.o的结果解析
在命令行中输入命令objdump -d -r hello.o>Disas_hello.s,生成Disas_hello.s文件,内容如下:
将Disas_hello.s与hello.s对比可以发现:
- 分支转移不同:对于条件跳转,hello.s中给出的是段名,Disas_hello.s的跳转命令后是跳转的目标地址。
- 进制不同:hello.s中数字以十进制表示,Disas_hello.s中数字以十六进制表示。
- 函数调用不同:hello.s中call指令后是调用函数名,Disas_hello.s中call指令使用的是相对偏移地址。
(以下格式自行编排,编辑时删除)
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
4.5 本章小结
本章主要介绍了汇编的概念与作用、在Ubuntu下汇编的指令、可重定位目标elf的格式等,通过与hello.s的对比分析了hello.o的反汇编结果。
第5章 链接
5.1 链接的概念与作用
概念:链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程。
作用:方便分离编译,使人们可以将大型应用程序分解成更小、更好管理的模块,其中模块可被独立地修改和编译。
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>hello.txt
5.3.1 ELF头
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。本程序的ELF头如下:
5.3.2 节头
节头描述了hello.o文件中每一个节出现的位置、大小,目标文件的每一个节都有一个固定大小的条目。本程序的节头表如下:
5.3.3 重定位节
5.3.4 符号表
5.4 hello的虚拟地址空间
使用edb加载hello,hello的虚拟地址空间分配情况在Data Dump栏中展示,内容如下:
hello从虚拟地址0x400000处载入,入口点地址为0x4010f0:
.rodata的地址则为0x402000:
5.5 链接的重定位过程分析
在命令行中输入:objdump -d -r hello > hello_objdump.s
对比可知,hello.o的地址从0开始,而链接后地址确定,hello已重定位,反汇编代码地址从0x400000开始;hello的反汇编代码中含有hello.o反汇编代码中没有的init,printf,puts等函数,这是链接时加入的;hello中增加了.init和.plt节和一些节中定义的函数;hello.o中用0+%rip表示全局变量的位置,而在hello中因为已定位,所以用一个确定的值加%rip表示全局变量的位置;hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址,这是由于hello.o中对于函数还未进行定位,只是在.rel.text中添加了重定位条目,而hello进行定位之后自然不需要重定位条目。
链接过程主要分为符号解析和重定位:
符号解析:目标文件定义和引用符号,符号解析将每个符号引用和一个符号定义关联起来。
重定位:编译器和汇编器生成从0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样的重定位。
5.6 hello的执行流程
403ff0 <__libc_start_main@GLIBC_2.2.5>
4011c0 <__libc_csu_init>
401000 <_init>
4011c0 <__libc_csu_init>
401125 <main>
401090 <puts@plt>
401020 <.plt>
4010d0 <exit@plt>
401020 <.plt>
5.7 Hello的动态链接分析
当程序调用由共享库定义的函数时,编译器无法预测函数的地址,但编译系统提供了延迟绑定的方案,讲过程地址绑定推迟至第一次调用该过程时。通过GOT和过程链接表PLT的写作解析函数地址。加载时,动态链接器重定位GOT中的每个条目,使其包含正确的绝对地址。通过观察edb可知dl_init后.got.plt节发生的变化。
elf中.got.plt节的内容:
执行init前:
执行init后:
5.8 本章小结
本章主要介绍了链接的概念与作用、在Ubuntu下链接的命令、可执行目标文件hello的格式、hello的虚拟地址空间,分析了链接的重定位过程、可执行目标文件hello的执行流程以及hello的动态链接。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是执行中程序的抽象。
作用:
1. 运行程序时,shell会创建一个新的进程,然后在这个新进程的上下文切换中运行这个可执行目标文件。应用程序也能够创建新进程,并且在新进程的上下文中运行它们自己的代码或其他应用程序。
2. 进程提供给应用程序的关键抽象:一个独立的逻辑控制流,好像程序独占处理器;一个私有的地址空间,好像程序独占内存系统。
6.2 简述壳Shell-bash的作用与处理流程
作用:解释并执行用户指令。
处理流程:
- 读取用户输入的命令
- 检查输入的命令是否为内部命令,是则立即执行,不是则再检查是否为应用程序;
- 在搜索路径或环境变量中寻找这些应用程序;
- 如果输入的命令不是内部命令且没有在搜索路径中找到可执行文件,则报错;
- 如果找到了可执行文件,则该内部命令或应用程式被分解为系统调用传给Linux内核,然后由内核完成相应工作。
6.3 Hello的fork进程创建过程
- 给新进程分配一个标识符
- 在内核中分配一个PCB,将其挂在PCB表上
- 复制它的父进程的环境(PCB中大部分的内容)
- 为其分配资源(程序、数据、栈等)
- 复制父进程地址空间里的内容(代码共享,数据写时拷贝)
- 将进程置成就绪状态,并将其放入就绪队列,等待CPU调度。
6.4 Hello的execve过程
- 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
- 映射私有区域。为新程序的代码、数据、bss 和栈区域创建新的区域结构。
- 映射共享区域。
- 设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
进程调度:即使在系统中通常有许多其他程序在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一的对应于包含在运行时动态链接到程序的共享对象中的指令。这个 PC 的序列叫做逻辑控制流,或者简称逻辑流。进程是轮流适用处理器的,每个进程执行它的流的一部分,然后被抢占,然后轮到其他进程。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
内核模式转变到用户模式:操作系统内核使用上下文切换来实现多任务。内核为每个进程维持一个上下文,它是内核重启被抢占的进程所需的状态,包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构的值。
进程执行到某些时刻,内核可决定抢占该进程,并重新开启一个先前被抢占了的进程,这种决策称为调度。内核调度一个新的进程运行后,通过上下文切换机制来转移控制到新的进程:1)保存当前进程上下文;2)恢复某个先前被抢占的进程被保存的上下文 3)将控制转移给这个新恢复的进程。当内核代表用户执行系统调用时,可能会发生上下文切换,这时就存在着用户态与核心态的转换。
6.6 hello的异常与信号处理
正常运行:
- 发送信号ctrl+z:进程收到信号SIGTSTP
进程暂停:
输入fg %1使该进程返回前台
- 发送信号ctrl+C:进程收到信号SIGINT
进程被终止,已彻底结束:
- 不停乱按
无关输入被送入缓存区,输入到屏幕上
- kill命令
可以用kill传输信号SIGKILL使进程终止,或传输其他信号
- pstree命令
6.7本章小结
本章主要介绍了进程的概念与作用、壳Shell-bash的作用与处理流程、fork进程的创建过程、Hello的execve过程、Hello进程的执行及异常与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址指由程序产生的与段相关的偏移地址部分,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。从hello的反汇编代码中看到的地址,它们需要通过计算,通过加上对应段的基地址才能得到真正的地址,这些便是hello中的逻辑地址。
线性地址:是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑地址,hello的反汇编文件中看到的地址(即逻辑地址)中的偏移量,加上对应段的基地址,便得到了hello中内容对应的线性地址。
虚拟地址:有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。
物理地址:是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。在hello的运行中,在访问内存时需要通过CPU产生虚拟地址,然后通过地址翻译得到一个物理地址,并通过物理地址访问内存中的位置。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在Intel平台下的实模式中,逻辑地址为:CS:EA,CS 是段寄存器,将CS里的值左移四位,再加上EA就是线性地址。
而保护模式下,要用段描述符作为下标,到GDT(全局描述符表)/LAT(局部描述符表)中查表获得段地址,段地址+偏移地址就是线性地址。
段描述符是一个16位字长的字段,如图:
TI位指示选择GDT还是LDT,前13位作为索引来确定段描述符在描述符表中的位置。从段描述符和偏移地址得到线性地址的过程如图:
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
线性地址(虚拟地址)由虚拟页号VPN和虚拟页偏移VPO组成。首先,MMU从线性地址中抽取出VPN,并且检查TLB,看他是否因为前面某个内存引用缓存了PTE的一个副本。TLB从VPN中抽取出TLB索引和TLB标记,查找对应组中是否有匹配的条目。若命中,将缓存的PPN返回给MMU。若不命中,MMU需从页表中的PTE中取出PPN,若得到的PTE无效或标记不匹配,就产生缺页,内核需调入所需页面,重新运行加载指令,若有效,则取出PPN。最后将线性地址中的VPO与PPN连接起来就得到了对应的物理地址。
图4 Intel Core i7地址翻译
7.4 TLB与四级页表支持下的VA到PA的变换
CPU产生虚拟地址VA,虚拟地址VA传送给MMU,MMU使用VPN高位作为TLBT和TLBI,向TLB中寻找匹配。如果命中,则得到物理地址PA。如果TLB中没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成物理地址PA,添加到PLT。
图5 多级页表管理
7.5 三级Cache支持下的物理内存访问
得到物理地址后,将物理地址分为CT(标记位)、CI(组索引)和CO(块偏移)。根据CI查找L1缓存中的组,依次与组中每一行的数据比较,有效位有效且标记位一致则命中。如果命中,直接返回想要的数据。如果不命中,就依次去L2、L3缓存判断是否命中,命中时将数据传给CPU同时更新各级缓存。
7.6 hello进程fork时的内存映射
在Shell输入命令行后,内核调用fork创建子进程,为hello程序的运行创建上下文,并分配一个与父进程不同的PID。通过fork创建的子进程拥有父进程相同的区域结构、页表等的一份副本,同时子进程也可以访问任何父进程已经打开的文件。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,
用hello程序有效地替代了当前程序。加载并运行hello需要以下步骤:
- 删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
- 映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
- 映射共享区域,hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置程序计数器(PC),execv()做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
当指令引用一个虚拟地址,而与该地址相对应的物理页面不在内存中,因此必须从磁盘中取出时,就会发生缺页故障。缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中了,指令就可以没有故障地运行完成了。其中CPU硬件的执行步骤为:
- 处理器生成一个虚拟地址,并把它传送给MMU。
- MMU生成PTE地址,并从高速缓存/主存请求得到它。
- 高速缓存/主存向MMU返回PTE。
- PTE 中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
- 缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘。
- 缺页处理程序页面调入新的页面,并更新内存中的PTE。
- 缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,在MMU执行了图6中的步骤之后,主存就会将所请求字返回给处理器。
图6 缺页的操作
7.9动态存储分配管理
动态内存分配器维护者一个进程的虚拟内存区域,称为堆(heap)。分配器将堆视为一组不同的大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。分配器有两种基本风格,两种风格都是要求显示的释放分配块,二者的区别在于负责释放已分配块的实体不同。
- 显式分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器。
- 隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。
动态内存分配主要有两种基本方法与策略:
1、带边界标签的隐式空闲链表分配器管理
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷以及一些可能的额外填充和一个字的尾部组成的。在隐式空闲链表中,空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,间接地遍历整个空闲块的集合。一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以合并空闲块,其中便利用隐式空闲链表的边界标记来进行合并。
显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。放置策略与上述放置策略一致。
7.10本章小结
本章主要介绍了hello的存储器的地址空间,介绍了四种地址空间的差别和地址的相互转换、hello的四级页表的虚拟地址空间到物理地址的转换,阐述了三级cache的物理内存访问、进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等。
结论
hello的历程:
- 预处理,将hello.c调用的外部库、头文件整合成hello.i文件;
- 编译,将hello.i编译成汇编语言文件hello.s;
- 汇编,将hello.s转换成可重定位目标文件hello.o;
- 链接,将hello.o与可重定位目标文件和动态链接库链接生成可执行目标文件hello;
- 进程管理,shell读取指令后向系统发送信号,利用fork函数为hello创建一个新的子进程,然后用execve在进入程序入口后载入物理内存,程序开始运行;
- 存储管理,MMU将CPU传递的虚拟地址翻译为物理地址,再根据物理地址在内存或磁盘中查找数据;通过调用malloc,从堆中申请内存;
- 运行结束后,父进程shell回收子进程资源,内核删除该子进程的数据。
现代计算机系统历经数十年的发展,其功能已经相对完善。前人的工作已经使我们可以方便的实现具有一定功能的程序,但计算机系统在其中所做的工作却是纷繁复杂的。从hello.c到可执行目标文件hello,人们只需要写下并不复杂的代码并在shell中依次输入gcc hello.c -o hello、./hello的指令,几秒后便可得到想要的结果。然而shell收到上述指令后计算机系统所做的工作,本篇论文却只能以不短的篇幅简略描述其中的部分。路漫漫其修远兮,要更深入的理解计算机系统,还需进一步研究、学习。
附件
文件名 | 作用 |
hello.c | 预处理后的代码 |
hello.s | 编译后的代码 |
hello.o | 汇编后的二进制代码 |
hello | 可执行目标文件 |
Disas_hello.s | hello.o的反汇编代码 |
hello.txt | hello的elf文件信息 |
hello_objdump.s | 以反汇编格式显示hello的重定位部分 |
elf.txt | hello.o的elf文件信息 |
参考文献
[1] DECONX. 计算机系统漫游 | Hello, world 的一生[EB/OL]. (2022-09-30)https://zhuanlan.zhihu.com/p/513307151.
[2](美)布赖恩特(Bryant,R.E.)等. Computer Systems:A Programmer's Perspective [M].北京:机械工业出版社,2016:769.