[HIT-CSAPP]大作业:Hello的一生
计算机系统
大作业
题 目程序人生-Hello's P2P
专 业 计算学部
学 号
班 级
学 生
指 导 教 师 史先俊
计算机科学与技术学院
2021年5月
摘 要
本文通过对于hello程序的分析,从hello.c直到hello可执行程序进行逐步分析,结合课本上的知识和一些资料,在实践中把这个学期csapp中各章节知识进行融合,形成自己的语言,展示自己的收获,体现自己对于对计算机系统这门课程的理解。
关键词:计算机系统;程序的一生;
目 录
6.2 简述壳Shell-bash的作用与处理流程 - 10 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -
7.7 hello进程execve时的内存映射 - 11 -
第1章 概述
1.1 Hello简介
Hello的P2P(Program to process)过程包括了从源代码hello.c经过预处理为hello.i,编译为汇编语言文件hello.s,再汇编为可重定位文件hello.o,经过ld链接为可执行文件hello,再在shell接收到指令之后调用fork开辟进程,execve加载进入内存,就实现了完整的过程。
而"020"再在之后进行逻辑流的运行、终端,异常的处理,知道最终结束进程被父进程回收,这边完成了hello的一生。
1.2 环境与工具
硬件环境:Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz 1.99 GHz 16.0 GBRAM
软件环境:Windows 10 家庭中文版;VMWare;Ubuntu 64 18.04.05
1.3 中间结果
hello.i : 预处理后的文本文件
hello.s : 汇编文件
hello.o : 可重定位目标文件
hello : 可执行目标文件
1.4 本章小结
简述了hello的一生以及本文的软件及硬件环境和中间所用到的文件。
第2章 预处理
2.1 预处理的概念与作用
预处理中会展开以#起始的行,试图解释为预处理指令(preprocessing directive) ,其中ISO C/C++要求支持的包括
#if
#ifdef
#ifndef
#else
#elif
#endif(条件编译)
#define(宏定义)
#include(源文件包含)
#line(行控制)
#error(错误指令)
#pragma(和实现相关的杂注)
#(空指令)
预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。
2.2在Ubuntu下预处理的命令
cpp hello.c > hello.i
2.3 Hello的预处理结果解析
预处理后的文件因为库和宏定义等处理和代码的加入扩展为了3000多行。
2.4 本章小结
本章首先介绍了预处理的定义与作用、之后结合预处理之后的hello.i程序对预处理的结果进行了简单分析。第3章 编译
3.1 编译的概念与作用
编译就是把高级语言变成计算机可以识别的2进制语言的过程。
(注意:这的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序。)
编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1 数据
该代码涉及了局部变量i,参数argc和argv[],以及字符串常量" 用法: Hello 学号姓名秒数!\n" 和" Hello %s %s\n"。
其中字符串常量存放在.rodata节,为只读数据。
argc为main函数的第一个参数传入,存放在%edi中。argv[]为第二个参数,存放在%rsi中。二者均在函数中被push入栈。
局部变量i创建在栈中,位置是-4(%rbp),初始值在.L2处设为0,并且在.L3的循环中与7进行比较,每次循环+1。
3.3.2 赋值操作
代码中的赋值操作只有for循环时令局部变量i=0。
3.3.3 算数操作
循环体中每一次循环结束后要执行i++。i存放在栈中,位置为-4(%rbp)。
3.3.4 关系操作
一是判断了argv与4的大小关系。
先是将存在%edi中的argv放入栈中,再用cmpl将其与立即数4进行比较,若相等的跳转至.L2处。
二是循环体中判断i与7的大小关系。
每一次循环体运行完后比较i与7的大小,若小于7则再次循环,否则循环结束,执行外面的语句getchar()。
3.3.5 控制转移
控制转移主要涉及两条语句,分别使if操作和for循环。
If操作判断argv是否为4,若为4的话执行跳转。
而for循环采用jump-to-middle策略。执行了i=0的初始化之后先跳转到check部分,没有问题的话再跳转至中间的循环体.L4位置。
3.3.6数组操作
数组操作主要针对main的第二个参数argv[]。在printf和atoi中传入了argv[1],argv[2],argv[3]作为参数。其中printf函数传入argv[1]和argv[2]分别存储在%rsi和%rdx中(第二个和第三个参数),而argv[3]也在%edi(第一个参数)中作为atoi函数的输入。
3.3.7 函数调用
共调用了main(),printf(),exit(),sleep(),atoi(),getchar()六个函数。
其中main函数在.text节中,标记类型为函数,其两个参数分别为argc和argv[],存放在%rdi和%rsi中。
由于第一个printf函数只打印了一个字符串常量,所以程序调用了puts进行输出:。而第二个用printf传递了三个参数,分别存放在%rdi,%rsi,%rdx中。
exit函数和sleep函数的调用同样,将参数放在%edi中传入,返回结果存放在%eax中。
getchar函数则无参数传递,直接用call调用。
3.4 本章小结
本章完成了hello.i到hello.s的编译,并且针对汇编代码根据hello程序中使用的各种数据类型,运算。循环操作和函数调用等操作对hello.s程序具体分析了C语言中数据与操作的相关内容。
第4章 汇编
4.1 汇编的概念与作用
汇编程序是指把汇编语言书写的程序翻译成与之等价的机器语言程序的翻译程序。汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序。
(注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。)
4.2 在Ubuntu下汇编的命令
命令:as hello.s -o hello.o
4.3 可重定位目标elf格式
利用readelf命令将hello.o的elf重定位输出至elf.txt中。
4.3.1 elf头
elf头以一个16字节的目标序列开始,这个字节的序列主要描述了一个生成该目标文件的操作系统的目标文件大小和生成目标字节的顺序。其他的描述还包括elf头的位置和大小,目标文件的位置和类型,机器文件类型,节头部表的目标文件位置和偏移,节头部表的偏移大小和目标文件数量。不同节的目标文件位置和偏移大小都是由一个节头部表条目来描述的,其中每个目标文件中每个目标字节都有一个固定大小的节头部表条目。
4.3.2 节头部表
描述目标文件的节,描述目标文件中不同节的位置和大小。
4.3.3 重定位节
偏移量:通常对应于一些需要重定向的程序代码所在.text或.data节中的偏移位置。
信息:重定位到的目标在符号表中的偏移量
类型:代表重定位的类型,与信息相互对应。
名称:重定向到的目标的名称
加数:用来作为计算重复或定位文件位置的一个辅助运算信息,共计约占8个字节。
4.3.4 符号表(Symbol Table)
符号表:用来存放程序中定义和引用的函数和全局变量的信息。局部变量存在于栈中,并不存于这里。
符号表索引:对此数组的索引
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o
机器语言的构成:机器语言是用二进制代码表示的计算机能直接识别和执行的一种机器指令的集合,每条机器指令包括操作码域和地址码域。
机器语言与汇编语言的映射关系:由图可知,反汇编结果与汇编语言有部分不同,分支跳转的对象不再用.L0这样表示,而是用偏移地址来表示;函数调用也由call 函数名变成call一个偏移地址;
4.5 本章小结
本章完成了对 hello.s到 hello.o的汇编工作。然后将其反汇编结果与汇编代码对比,分析和阐述了从汇编语言进一步翻译成为机器语言的汇编过程。
第5章 链接
5.1 链接的概念与作用
链接操作是将各种机器代码和数据的片段进行收集并组合成一个单一的链接后的文件的编译过程,这个单一文件的链接可被应用程序加载到一个内存并加载和执行。
链接可以是执行于应用程序编译时,也就是在一个源代码被翻译成一个机器代码时;也就是可以直接执行于应用程序加载时,也就是在应用程序被一个加载器自动加载到一个内存并加载和执行时;甚至可以直接执行于应用程序运行时,也就是由一个大的应用程序连接器来加载和执行。在传统和现代的计算机操作系统中,链接的重要性是由应用程序连接器帮助应用程序自动加载和执行的。
注意:这儿的链接是指从 hello.o 到hello生成过程。
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读出hello的elf各段信息,输出至all.txt文件中。
各段信息:
elf头
节头
程序头
动态section
重定位节
Symbol Table
5.4 hello的虚拟地址空间
由data dump部分可以看出,程序是从0x400000开始加载的,结束在约0x400ff0位置。
再来看5.3中程序头部的位置
PHDR:程序头表
INTERP:程序执行前需要调用的解释器
LOAD:程序目标代码和常量信息
DYNAMIC:动态链接器所使用的信息
NOTE::辅助信息
GNU_EH_FRAME:保存异常信息
GNU_STACK:使用系统栈所需要的权限信息
GNU_RELRO:保存在重定位之后只读信息的位置
可以看到最开始是GNU_STACK,而LOAD从0x400000开始,接着是PHDR,INTERP和NOTE。最后是DYNAMIC和GNU_RELRO部分。
5.5 链接的重定位过程分析
命令:objdump -d -r hello
重定位:链接器在完成符号解析以后,就把代码中的每个符号引用和正好一个符号定义关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。然后就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时的地址。。然后是重定位节中的符号引用,链接器会修改hello中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址。
可以看到这次的汇编代码中call时的地址都变为了绝对地址,不再是最初的函数名字或相对地址。而且多了很多通过链接加进来的函数的源代码,如printf等。
5.6 hello的执行流程
ld-2.27.so!_dl_start—
ld-2.27.so!_dl_init—
hello!_start—0x400550
hello!_init—0x4004c0
hello!main—0x400582
hello!puts@plt–0x4004f0
hello!exit@plt–0x400530
hello!printf@plt–0x400500
hello!sleep@plt–0x400540
hello!getchar@plt–0x400510
libc-2.27.so!exit+0-0x40530
5.7 Hello的动态链接分析
共享链接库代码是一个动态的目标模块,在程序开始运行或者调用程序加载时,可以自动加载该代码到任意的一个内存地址,并和一个在目标模块内存中的应用程序链接了起来,这个过程就是对动态链接的重定位过程。
动态的链接器在正常工作时链接器采取了延迟绑定的链接器策略,由于静态的编译器本身无法准确预测变量和函数的绝对运行时地址,动态的链接器需要等待编译器在程序开始加载时再对编译器进行延迟解析,这样的延迟绑定策略称之为动态延迟绑定。got链接器叫做全局变量过程偏移链接表,在plt和got中分别存放着链接器的目标变量和函数的运行时地址。一个动态的链接器通过静态的过程偏移链接表plt+got链接器实现了函数的一个动态过程链接,这样一来,它就已经包含了正确的绝对运行时地址。
5.8 本章小结
本章介绍了链接的概念和作用,分析了hello的ELF格式,虚拟地址空间的分配,重定位和执行过程还有动态链接的过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。
6.2 简述壳Shell-bash的作用与处理流程
shell是一个应用程序,他在操作系统中提供了一个用户与系统内核进行交互的界面。他的处理过程一般是这样的:
读取用户的输入
分析输入内容,获得输入参数
如果是内核命令则直接执行,否则调用相应的程序执行命令
在程序运行期间,shell需要监视键盘的输入内容,并且做出相应的反应
6.3 Hello的fork进程创建过程
运行命令:./hello 1190201725 姚澜 3
判断hello不是一个内置的shell指令,所以调用应用程序,找到当前所在目录下的可执行文件hello,准备执行。Shell会自动的调用fork()函数为父进程创建一个新的子进程,子进程就会因此得到与父进程虚拟地址空间相同的一段各种的数据结构的副本。父进程与子进程最大的不同在于他们分别拥有不同的PID,父进程与子进程分别是两个并发的进程,在子进程中程序运行的这个过程中,父进程在原位置等待着程序的运行完毕。
6.4 Hello的execve过程
exceve函数在当前进程的上下文中加载并运行一个新程序。exceve函数加载并运行可执行目标文件,并带参数列表和环境变量列表。只有当出现错误时,exceve才会返回到调用程序,否则,exceve调用一次且从不返回。在exceve加载了可执行目标文件后,他调用启动代码,启动代码设置栈,将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序,由此将控制传递给新程序的主函数。
6.5 Hello的进程执行
进程向每个程序人员提供一种假象,好像他们在一个独占的程序中使用了处理器,这种处理效果的具体实现效果本身就是一个逻辑控制流,它指的是一系列可执行程序的计数器pc的值,这些计数值唯一的定义对应于那些包含在程序的可执行文件目标对象中的可执行指令,或者说它指的是那些包含在程序运行时可以动态通过链接触到可执行程序的共享文件对象的可执行指令。其中,一个进程执行它的控制流的一部分的每一时间段叫做时间片。
处理器用某个控制寄存器中的一个模式位来限制一个应用可以执行的指令以及它可以访问的地址空间范围。没有设置模式位时,进程运行在用户模式中,它必须通过系统调用接口才可间接访问内核代码和数据;而设置模式位时,它运行在内核模式中,可以执行指令集中的任何指令,访问系统内存的任何位置。异常发生时,控制传递到异常处理程序,由用户模式转变到内核模式,返回至应用程序代码时,又从内核模式转变到用户模式。
操作系统内核使用上下文切换来实现多任务。内核为每个进程维持一个上下文,它是内核重启被抢占的进程所需的状态,包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构的值。
进程执行到某些时刻,内核可决定抢占该进程,并重新开启一个先前被抢占了的进程,这种决策称为调度。内核调度一个新的进程运行后,通过上下文切换机制来转移控制到新的进程:1)保存当前进程上下文;2)恢复某个先前被抢占的进程被保存的上下文3)将控制转移给这个新恢复的进程。当内核代表用户执行系统调用时,可能会发生上下文切换,这时就存在着用户态与核心态的转换。
6.6 hello的异常与信号处理
正常执行:
异常种类:中断、陷阱、故障和终止。
乱按&回车:
屏幕上会显示按下的内容,但不影响程序的输出和执行。在执行后按下的内容会被当成指令执行。
Ctrl-C:
Ctrl-C发送SIGINT信号,当父进程收到信号时结束前台进程hello并回收。
Ctrl-Z & ps:
Ctrl-Z会发送SIGSTP信号,信号处理函数会打印屏幕回显,挂起当前进程,通过ps指令可以看到进程并没有被回收。
Ctrl-Z & jobs:
可以看到job为1的已经停止的hello。
Ctrl-Z & fg:
可以看到fg将挂起的进程继续执行了。
6.7本章小结
本章介绍了进程的概念和作用,介绍了shell如何fork、execve执行hello,系统如何在用户态与内核态间切换以处理运行时信号发送带来的中断。
第7章 hello的存储管理
7.1 hello的存储器地址空间
虚拟地址(逻辑地址):又称相对地址,是程序运行由CPU产生的与段相关的偏移地址部分。他是描述一个程序运行段的地址。在每个进程创建加载时,内核只是为进程"创建"了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和代码拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好,等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。还有进程运行过程中,要动态分配内存,比如 malloc时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。有时我们也把逻辑地址称为虚拟地址。。
物理地址:程序运行时加载到内存地址寄存器中的地址,内存单元的真正地址。他是在前端总线上传输的而且是唯一的。在 hello程序中,他就表示了这个程序运行时的一条确切的指令在内存地址上的具体哪一块进行执行。.
线性地址:是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。高端内存不能全部映射到内核空间,也就是说这些物理内存没有对应的线性地址。不过,内核为每个物理页框都分配了对应的页框描述符,所有的页框描述符都保存在mem.map,数组中,因此每个页框描述符的线性地址都是固定存在的。
7.2 Intel逻辑地址到线性地址的变换-段式管理
每个段的首地址就会被储存在各自的段描述符里面,所以的段描述符都将会位于段全局描述符表中,通过段选择符我们可以快速寻找到某个段的段全局描述符。逻辑上段地址的偏移量结构就是段选择符+偏移量。
段选择符的索引位组成和定义如下,分别指的是索引位,ti,rpl,当索引位ti=0时,段描述符表在rpgdt中,ti=1时,段描述符表在rpldt中。而索引位index就类似一个数组,每个元素内都存放一个段的描述符,索引位首地址就是我们在查找段描述符时再这个元素数组当中的索引。一个段描述符的首地址是指含有8个元素的字节,我们通常可以在查找到段描述符之后获取段的首地址,再把它与线性逻辑地址的偏移量进行相加就可以得到段所需要的一个线性逻辑地址。
在分段保护模式下,分段有两种机制:段的选择符在段的描述符表->分段索引->目标段的段描述符条目->目标段的描述符基地址+偏移量=转换为线性段的基地址。由于现代的macosx86系统内核使用的描述符是基本扁平的逻辑模型,即目标段的逻辑地址=线性段的描述符=转换为线性段的基地址,等价于描述符转换为线性地址时关闭了偏移量和分段的功能。这样逻辑段的基地址与转换为线性段的基地址就合二为一了。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(虚拟地址)由虚拟页号VPN和虚拟页偏移VPO组成。首先,MMU从线性地址中抽取出VPN,并且检查TLB,看他是否因为前面某个内存引用缓存了PTE的一个副本。TLB从VPN中抽取出TLB索引和TLB标记,查找对应组中是否有匹配的条目。若命中,将缓存的PPN返回给MMU。若不命中,MMU需从页表中的PTE中取出PPN,若得到的PTE无效或标记不匹配,就产生缺页,内核需调入所需页面,重新运行加载指令,若有效,则取出PPN。最后将线性地址中的VPO与PPN连接起来就得到了对应的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
虚拟地址VA虚拟页号VPN和虚拟页偏移VPO组成。若TLB命中时,所做操作与7.3中相同;若TLB不命中时,VPN被划分为四个片,每个片被用作到一个页表的偏移量,CR3寄存器包含L1页表的物理地址。VPN1提供到一个L1 PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2 PTE的偏移量,依次类推。最后在L4页表中对应的PTE中取出PPN,与VPO连接,形成物理地址PA。
7.5 三级Cache支持下的物理内存访问
首先CPU发出一个虚拟地址,在 TLB里面寻找。如果命中,那么将PTE发送给L1Cache,否则先在页表中更新PTE。然后再进行Ll根据PTE寻找物理地址,检测是否命中的工作。这样就能完成Cache和TLB的配合工作。﹒
L1的寻址一般是组相联的模式,通过组索引部分可以确定在cache里的哪一组,通过标记位确定看是否与组里的某一行标记相同,如果有,通过块偏移位确定具体是哪个数据块,从而得到我们的数据。如果没有找到,则需要从主存里去找数据,并选择 cache 里的一行替换,对于L1,L2这样的组相联cache,替换策略通常有LFU,LRU。
7.6 hello进程fork时的内存映射
shell通过一个调用用fork的函数可以让进程内核自动创建一个新的进程,这个新的进程拥有各自新的数据结构,并且被内核分配了一个唯一的pid。它有着自己独立的虚拟内存空间,并且还拥有自己独立的逻辑控制流,它同样可以拥有当前已经可以打开的各类文件信息和页表的原始数据和样本,为了有效保护进程的私有数据和信息,同时为了节省对内存的消耗,进程的每个数据区域都被内核标记起来作为写时复制。
7.7 hello进程execve时的内存映射
sxecxe,函数在当前进程中加载并运行新程序hello的步骤:
删除已存在的用户区域。
创建新的区域结构
私有的、写时复制。
代码和初始化数据映射到.text和.data区(目标文件提供)。
.bss和栈堆映射到匿名文件﹐栈堆的初始长度0.
共享对象由动态链接映射到本进程共享区域.
设置PC,指向代码区域的入口点。
execve为代码段和数据段分配虚拟页,并标记为无效。
.每个页面被初次引用时,虚拟内存系统会按照需要自动地调入数据页。.
7.8 缺页故障与缺页中断处理
在发生缺页中断之后,系统会调用内核中的一个缺页处理程序,选择一个页面作为牺牲页面。具体的操作过程如下:
第1步:CPU生成一个虚拟地址,并把它传送给MMU.
第2步: 地址管理单元生成PTE地址,并从高速缓存/主存请求得到它.
第3步:高速缓存/主存向MMU返回PTE.
第4步:PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序.
第5步:缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘.
第6步:缺页处理程序页面调人新的页面,并更新内存中的PTE.
第7步: 缺页处理程序返回地址到原来的缺页处理进程,再次对主存执行一些可能导致缺页的处理指令,cpu,然后将返回地址重新再次发送给处理程序mmu.因为程序中虚拟的页面现在已经完全缓存在了物理的虚拟内存中,所以处理程序会再次命中,主存将所请求字符串的返回地址发送给虚拟内存的处理器。
7.9动态存储分配管理
C程序当运行时需要额外虚拟内存时,用动态内存分配器是一种更加方便也有更好可移植性的方法。
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间的细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放。这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
在hello中,printf函数会调用malloc函数。malloc函数返回一个指针,指向大小至少为size字节的内存块。有时,当我们运行程序时,才会直到某些数据结构的大小,这样便需要使用到动态内存分配。
动态内存管理的基本方法:
当进行动态内存分配时,任何分配器都需要一些数据结构,以隐式空闲链表为例,带边界标签的隐式空闲链表中的每个块是由一个字的头部和一个字的脚部,有效载荷以及可能的额外填充组成的。头部和脚部编码了块的大小以及块是已分配还是空闲的。它们之间便是malloc时请求的有效载荷,以及为了满足8字节对齐要求的填充部分。他是通过头部脚部中的大小字段隐式连接的。在应用请求k字节的块时,分配器搜索空闲链表,查到一个足够大可以放置所请求块的空闲块。一旦其找到匹配的空闲块,就要分配空闲块的空间,若剩余部分足以形成新的空闲块,则将其分割。若分配器找不到合适的空闲块,则需要向内核额外请求堆内存,将其转化为大的空闲块,插入到空闲链表中,然后将请求块放置于此。当分配器释放一个已分配块时,可能有其他空闲块与新释放的相邻,这时需要进行合并,这时由于每个块的头部脚部记录了块是否空闲,那么便可通过检查其前面块的脚部和后面块的头部来判断是否有空闲块相邻。若是,也只需通过修改头部脚部便可进行合并。
动态内存管理的策略:
首先,对与堆中的块的组织,可以选择隐式、显示、分离空闲链表等。当分配器查找空闲块时,又可以采用不同的放置策略,如首次适配(从头开始搜索链表)、下一次适配(从上一次找到的空闲块的剩余块除法)以及最佳适配等。在分割空闲块时,又可以采用将剩余块分割为新的空闲块的策略。在合并时,可以采用带边界标记的合并,就像在上一段基本方法中所描述,通过边界标记来判断当前块周围是否也同样是空闲块,以此来判断是否需要合并。
7.10本章小结
本章介绍了储存器的地址空间,讲述了虚拟地址、物理地址、线性地址、逻辑地址的概念,还有进程fork和execve时的内存映射的内容。描述了系统如何应对那些缺页异常,最后描述了malloc的内存分配管理机制。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
文件的类型:
普通文件:包含任意数据的文件。
目录:包含一组链接的文件,每个链接都将一个文件名映射到一个文件。
套接字:用来与另一个进程进行跨网络通信的文件
命名通道
符号链接
字符和块设备
设备管理:unix io接口
打开和关闭文件
读取和写入文件
改变当前文件的位置
8.2 简述Unix IO接口及其函数
Unix IO接口的几种操作:。
1.打开文件:程序要求内核打开某文件时,内核返回一个小的非负整数(描述符)用于在后续操作中标识这个文件。程序在只要记录这个描述符便能记录打开文件的所有信息。
2.shell在进程的开始为其打开三个文件:标准输入、标准输出和标准错误。
3.改变当前文件的位置:对于每个打开的文件,内核保存着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。程序能够通过执行seek操作显式地设置文件的当前位置为k。
4.读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到 k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会发出称为 EOF的条件,应用程序能检测到这个条件,在文件结尾处并没有明确的EOF符号。
5.关闭文件:关闭文件是通知内核你要结束访问一个文件,内核释放打开文件时创建的数据结构以及占用的内存资源,并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix I/O函数:。
1. int open(char *filename, int flags, mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
2. int close(int fd);
关闭一个打开的文件。
3. size_t read(int fd, void *buf, size_t n);
read函数从描述符为纹的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
4. size_t write(int fd, const void *buf, size_t); ·
write函数从内存位置buf复制至多n个字节到描述符点的当前文件位置。
8.3 printf的实现分析
printf需要做的事情是:接受一个fmt的格式,然后将匹配到的参数按照fmt格式输出。
可以看到printf调用了vsprintf和write两个函数。
ysprintf的作用就是格式化。它接受确定输出格式的格式字符串fimt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
write函数是将buf中的i个元素写到终端的函数。
字符显示驱动子程序:从ASCII到字模库到显示vram,(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar()是 stdio.h.中的库函数,它的作用是从stdin流中读入一个字符,也就是说,如果stdin有数据的话不用输入它就可以直接读取了,第一次 getchar()时,确实需要人工的输入,但是如果你输了多个字符,以后的 getchar()再执行时就会直接从缓冲区中读取了。
实际上是输入设备->内存缓冲区->程序getchar
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章节讲述了一下linux的I/O设备管理机制,了解了开、关、读、写、转移文件的接口及相关函数,简单分析了printf和getchar函数的实现方法以及操作过程。
结论
用计算机系统的语言,逐条总结hello所经历的过程:
1. 预处理:解析替换#开头的语句,生成代码文件hello.i
2. 编译:将代码文件翻译成汇编语言,生成hello.s文件。
3. 汇编:将汇编语言转换成机器语言,生成可重定位文件 hello。
4. 链接:由链接器将可重定位文件链接,生成可执行文件 hello.
5. 创建进程、加载程序: shell收到运行./hello的指令之后,通过 fork创建子进程,并在其中由 execve.创建虚拟内存空间映射、调用高速缓存与缺页处理将hello加载进内存与cpu。"
6. 执行命令:cpu取指令,控制 hello的逻辑流进行运行,其间调用printf、getchar等函数调用IO设备,进行屏幕的显示和键盘读入。
7. 异常处理:对于运行程序时键盘输入的ctrl-c、ctrl-z等指令系统中断并调用相应的信号处理程序进行处理.
8. 结束: hello程序结束后由父进程回收子进程占用的资源,hello的痕迹从内存消失。
自此 hello的一生简单分析完成了。
在做大作业的过程中,感觉就像是复习一样回顾了一整个学期的计算机系统的课程,回顾了很多调试器的使用方法,感觉对整个程序的运行过程有了一个系统的新的认识。手中的电脑,其中简单的小小程序,要经过如此复杂的过程才最终结束执行,不禁让人感叹计算机的厉害,并且深刻的体会到学无止境。
附件
文件名 |
作用 |
hello.i |
预处理后的文本文件 |
hello.s |
编译后的汇编文件 |
hello.o |
汇编后的可重定位目标文件 |
hello |
可执行文件 |
elf.txt |
汇编后的可重定位目标文件的elf信息 |
all.txt |
可执行文件的反汇编结果,用于观察重定位结果 |
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 兰德尔·布莱恩特. 大卫·奥哈拉伦. 深入理解计算机系统机械工业出版社
[2] 动态链接原理分析 https://blog.csdn.net/shenhuxi_yu/article/details/71437167
[3] printf 函数 https://www.cnblogs.com/pianist/p/3315801.html
[4] getchar函数 https://blog.csdn.net/zhuangyongkang/article/details/38943863
[5] Linux 线性地址,逻辑地址和虚拟地址的关系
https://www.zhihu.com/question/29918252#answer-17622254
[6] 从逻辑地址到线性地址的转换流程
http://www.cnblogs.com/image-eye/archive/2011/07/13/2105765.html