目录
第1章 概述
1.1 Hello简介
P2P:From Program to Process
高级语言编写的hello.c文件经过预处理器预处理得到hello.i文件,将带有#的代码以及其他宏进行替换,然后经过编译器编译得到hello.s汇编文件,将高级语言翻译为汇编语言。hello.s文件经过汇编器被汇编成机器语言,生成可重定位的文件hello.o,最后经过链接器将hello.o与库函数相链接即可生成可执行程序hello。执行该程序时,操作系统调用fork创建一个子进程,然后调用execve函数加载该程序的进程,使得hello得以运行。
020: From Zero-0 to Zero -0
Hello产生子进程后,通过execve进行加载,先删除当前虚拟地址已存在的数据结构,为hello的代码、数据、bss等创建区域,然后映射共享区域,设置程序计数器,进入main函数,CPU分配时间片执行逻辑控制流。执行过程中,虚拟内存为进程提供独立的空间;存储结构层层递进,让数据从磁盘传输到CPU中;TLB、分级页表等也为数据的高效访问提供保障;I/O设备通过描述符与接口实现了hello的输入输出。多方面合作配合之下,hello完成执行。然后,shell回收hello进程,删除hello的所有痕迹,释放运行中占用的内存空间。这就完成了020过程。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;8G RAM;256GHD Disk
软件环境:Windows11 64位;Vmware 14;Ubuntu 16.04 LTS 64位;
开发与调试工具:gcc, readelf, objdump, edb, ld, gedit
1.3 中间结果
hello.i | hello.c的预处理文件 |
hello.s | hello.i的编译文件 |
hello.o | hello.s的汇编文件 |
elf.txt | hello.o文件的ELF格式 |
helloelf.txt | hello文件的ELF格式 |
hello | 可执行文件 |
hello.s | hello的反汇编文件 |
1.4 本章小结
本章简单介绍了hello从程序到执行,再到结束的整个过程,并说明了本次应用的工具和中间生成的文件信息。
第2章 预处理
2.1 预处理的概念与作用
概念:
预处理是指将程序源代码进行一部分修改的过程。预处理需要预处理器(cpp),它会根据以字符#开头的命令(一般在源代码的顶部),修改原始的c程序,并得到以.i为文件扩展名的新文件。
作用:
预处理能够实现文件包含、条件编译、布局控制和宏替换等功能,使得程序更完备,为程序的编译过程做好准备。
2.2在Ubuntu下预处理的命令
在Ubuntu的命令行下键入指令:
gcc -E hello.c -o hello.i
则当前目录下会生成文件hello.i。
2.3 Hello的预处理结果解析
首先,预处理将源程序的注释删去,将以#include指定的三个文件的源代码内容添加到hello.i中。然后将剩下的代码照搬进hello.i中。
总的来说,原代码经过预处理后,执行了相关的文件包含、条件编译、布局控制和宏替换,方便编译器将程序翻译为汇编语言。
2.4 本章小结
本章简单介绍了预处理,包括在Ubuntu下的演示。预处理过程实现了对原代码的文件包含、条件编译、布局控制和宏替换等操作,.i能够用于下一阶段的编译过程。
第3章 编译
3.1 编译的概念与作用
概念:
编译是指编译器将预处理得到的.i文件编译生成汇编代码,即生成ASII码形式汇编语言文件.s文件的过程。
编译主要包括词法分析、语法分析、语法制导翻译、中间代码生成、运行存储分配、代码优化等基本流程。
作用:
编译将高级程序语言转换为更加接近机器语言的汇编语言,为接下来的汇编做好准备。
3.2 在Ubuntu下编译的命令
在Ubuntu的命令行下键入指令:
gcc -S hello.i -o hello.s
则当前目录下会生成文件hello.s。
3.3 Hello的编译结果解析
3.3.1数据
(1)常量
字符串常量存储在只读数据段.rodata中,其中中文以utf-8进行编码,即按\三位数的方式存储。
一些涉及到的数字常量4、8等等存储在.text中。
(2)变量
局部变量i,类型为整型,存储在栈中,其存储的地址为(%rbp-4),程序开始时初始化为0,之后每次循环加1。作为局部变量,只有程序开始执行时才对其赋值。
(3)参数
main函数有两个参数,整型的argc和字符数组argv。在程序开始时,位于(%rbp-20)的argc传入寄存器%edi,位于(%rbp-32)的argv传入寄存器%rsi。
3.3.2 赋值
在for循环时,有奖i首先赋值为0的操作。在汇编代码中,用mov指令,将立即数传入代表i的地址处,完成赋值操作。
3.3.3 算术操作
每次循环完成,局部变量i都需要加1以继续循环。在汇编代码中,用add指令,向代表i的地址处加上立即数1,完成加法操作。
3.3.4 关系操作和控制转移
在汇编代码中,用cmp指令和jmp跳转指令来达到这种控制。
程序中,有if语句来判断argc与4的大小关系。由于argc存储在(%rbp-20)中,cmpl指令比较argc和4的大小,如果相等(je)就直接进入循环。
在循环中,需要判断i与8的大小关系,来确定是否继续循环。cmpl指令比较i和7的大小,如果i小于等于(jle)就继续循环,否则结束循环。
3.3.5 数组
程序中对数组argv的寻址被编译为基址+偏移量的寻址方式。我们已知,argv的基址存放在(%rbp-32)中,在首地址上+8,+16得到argv[1],argv[2]的地址。
3.3.6 函数
(1)main函数
参数传递:参数为argc和argv[],分别用寄存器%rdi和%rsi存储。
函数执行:内核执行c程序时调用特殊的启动例程,并将启动例程作为程序的起始地址,从内核中获取命令行参数和环境变量地址,执行main函数。
函数返回:main函数有两个返回。若argc不等于4,程序将会从exit退出;若argc等于4,程序将会正常返回0结束。
(2)printf函数
参数传递:第一个printf函数只传入了字符串参数的首地址;
第二个printf函数传入了 argv[1]和argc[2]的地址。
函数执行:第一个printf函数if判断后若argc不等于4调用;
第二个printf函数for循环中被调用。
(3)exit函数
参数传递:传入的参数为1。
函数执行:if判断后如果argc不等于4调用。
(4)atoi函数
参数传递:传入参数argv[3],用基址+24来确定地址。
函数执行:for循环中被调用。
函数返回;返回值作为sleep函数的参数。
(4)sleep函数
参数传递:传入参数atoi(argv[3])。
函数执行:for循环中被调用。
(5)getchar函数
函数执行:在main函数中被调用。
3.4 本章小结
本章主要介绍了编译的概念和作用,通过示例展示hello.i编译为hello.s的具体操作;阐述了源代码中的各种操作及数据类型在hello.s汇编语言中的具体体现,如常量、变量、赋值、关系操作、控制转移、数组、函数等具体操作。主要对编译命令和编译结果进行解析,并加以说明。
第4章 汇编
4.1 汇编的概念与作用
概念:
汇编器将汇编语言文件.s文件翻译为机器语言指令,生成可重定位目标文件.o文件的过程称为汇编。
作用:
将汇编语言转化为机器语言指令,方便计算机理解执行。
4.2 在Ubuntu下汇编的命令
键入指令:
gcc -c hello.s -o hello.o
生成目标文件hello.o。
4.3 可重定位目标elf格式
ELF文件的格式为
键入指令 readelf -a hello.o > elf.txt
ELF头:
ElF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
节头部表:
节头部表描述了目标文件的节,包含了各节的名字、类型、地址、偏移量等信息,包含了13个节,如图所示。
重定位节:
重定位节描述了各个段引用的外部符号,在链接时,需要通过重定位对其地址进行修改。如图需要重定位的符号有puts、exit、printf等等。
符号表:
符号表存放了程序中定义和引用的函数和全局变量的信息。
4.4 Hello.o的结果解析
objdump -d -r hello.o分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
操作数上:反汇编中用16进制表示,.s文件中用十进制表示;
分支跳转函数调用:反汇编都用于main地址的偏移来表示,.s文件的分支跳转用L表示,.s文件函数调用用函数名字来表示。
以下是反汇编全部代码
4.5 本章小结
本章介绍了汇编的概念与作用,并分析了汇编产生的可重定位目标elf格式,介绍了各节的信息,并对反汇编文件与上一节生成的.s文件进行了对比分析。
第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 -a hello > helloelf.ext
ELF头:
节头部表:
记录了每个节的名称、大小、类型、地址和偏移量。
5.4 hello的虚拟地址空间
使用edb加载hello程序,可在data dump窗口查看到加载到虚拟地址中的hello程序。查看5.3中的节头部表,可以得知.text起始地址在0x4010f0;再比如.rodata起始地址为0x402000,利用edb查看可以看到Hello在这里。
可以利用edb查看到所有段的存储内容。
5.5 链接的重定位过程分析
不同:
1.hello比hello.o多了.init、.plt等段;
2. 在hello中链接加入了在hello.c中用到的库函数,如exit、printf、sleep、getchar等函数。
3. hello.o中的相对偏移地址(0加上重定位条目)变成了hello中的虚拟内存地址。
4.在函数调用时,hello中填充完整了hello.o缺失的地址部分,也即通过hello.o中的重定向条目确定了实际的虚拟地址。
5.6 hello的执行流程
ld-2.30.so!_dl_start | 0x7fc37a781de0 |
ld-2.30.so!_dl_init | 0x7fc37a7910b0 |
libc-2.30.so!__libc_start_main | 0x7fb9b0abc0f0 |
libc-2.30.so!__cxa_atexit+0 | 0x7fe7d0d220e0 |
hello!__libc_csu_init | 0x401130 |
libc-2.30.so!_setjmp | 0x7fe7d0d1e060 |
hello!main | 0x4011a5 |
hello!puts@plt | 0x401090 |
hello!exit@plt | 0x4010d0 |
*hello!print@plt | 0x4010a0 |
*hello!atoi@plt | 0x4010c0 |
*hello!sleep@plt | 0x4010e0 |
*hello!getchar@plt | 0x4010b0 |
libc-2.30.so!exit | 0x7fed189ccd40 |
5.7 Hello的动态链接分析
延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
5.8 本章小结
本章主要了解温习了在linux中链接的过程。介绍了链接的概念和功能,分析可执行文件hello的ELF格式及其虚拟地址空间,并对重定位、动态链接进行深入的分析。通过查看hello的虚拟地址空间,并且对比hello与hello.o的反汇编代码,更好地掌握了链接与之中重定位的过程。
第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程的经典定义就是一个执行中的程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量、以及打开文件描述符的集合。
作用:
在现代系统上运行一个程序时,我们会得到一个假象,就好像我们的程序是系统中当前运行的唯一的程序一样,我们的程序好像是独占地使用处理器和内存,处理器就好像时无间断地一条接一条地执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象都是通过进程的概念提供给我们的。每次用户通过向 shell 输入一个可执行目标文件的名字,运行程序时, shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。
6.2 简述壳Shell-bash的作用与处理流程
作用:
(1)可交互,和非交互的使用shell。在交互式模式,shell从键盘接收输入;在非交互式模式,shell从文件中获取输入。
(2)shell中可以同步和异步的执行命令。在同步模式,shell要等命令执行完,才能接收下面的输入。在异步模式,命令运行的同时,shell就可接收其它的输入。重定向功能,可以更细致的控制命令的输入输出。另外,shell允许设置命令的运行环境。
(3)shell提供了少量的内置命令,以便自身功能更加完备和高效。
(4)shell除了执行命令,还提供了变量,流程控制,引用和函数等,类似高级语言一样,能编写功能丰富的程序。
(5)shell强大的的交互性除了可编程,还体现在作业控制,命令行编辑,历史命令,和别名等方面
处理流程:
(1)从终端读入输入的命令;
(2)将输入字符串切分获得所有的参数;
(3)如果是内置命令则立即执行;
(4)否则调用相应的程序执行;
(5)shell 应该接受键盘输入信号,并对这些信号进行相应处理。
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时。子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程最大的区别在于他们有不同的id。fork后调用一次返回两次,在父进程中fork会返回子进程的PID,在子进程中fork会返回0;父进程与子进程是并发运行的独立进程。内核能够以任何方式交替执行他们逻辑控制流中的指令。
hello的fork进程创建过程:
6.4 Hello的execve过程
创建进程后,在子进程中通过判断pid即fork()函数的返回值,判断处于子进程,则会通过execve函数在当前进程的上下文中加载并运行一个新程序,即 hello 程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置 PC 指向_start 地址,_start 最终调用 hello 中的 main 函数。execve 函数加载并运行可执行目标文件 filename, 且带参数列表 argv 和环境变量列表 envp 。只有当出现错误时,例如找不到 filename, execve 才会返回到调用程序。所以,与 fork 一次调用返回两次不同, execve 调用一次并从不返回。
6.5 Hello的进程执行
上下文:内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户态与核心态转换:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程默认处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
hello在前台printf中,由于调用sleep,进入后台内核模式,开始sleep2.5s,然后中断信号,再回到前台继续执行下一个操作。
6.6 hello的异常与信号处理
程序在运行时,可能会遇到几类异常,如来自I/O设备的信号导致的中断、同步的异常等等。
不停乱按:
回车:
Ctrl-Z:
Ctrl-C:
Ctrl-z后可以运行ps:
Ctrl-z后可以运行jobs:
Ctrl-z后可以运行pstree:
Ctrl-z后可以运行fg:
Ctrl-z后可以运行kill:
6.7本章小结
本章介绍了进程的概念和作用,shell的基本操作以及各种内核信号和命令,并总结了shell 新建子进程的过程、execve的执行进程以及hello进程如何在内核和前端中反复跳跃运行的。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:在计算机中,访内指令给出的地址(操作数)叫逻辑地址,也叫相对地址。
逻辑地址由两个16位的地址分量构成,一个为段基值,另一个为偏移地址。例如hello汇编语言中的地址操作数。
线性地址:是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
虚拟地址:虚拟地址是Windows程序时运行在386保护模式下,这样程序访问存储器所使用的逻辑地址称为虚拟地址,与实地址模式下的分段地址类似,虚拟地址也可以写为“段:偏移量”的形式,这里的段是指段选择器。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。计算机系统的主存被组织成一个由M 个连续的字节大小的单元组成的数组。每字节都有一个唯一的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段标识符和段内偏移量。段标识符由 16 位字段组成,前 13 位为索引号。索引号是段描述符的索引,而所有的段描述符都存放在一个描述符表中(描述符表分为全局描述符表GDT和局部描述符表LDT)。看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小,我们就可以通过段描述标识符的前 13 位,在这个表中找到一个具体的段描述符,从而获取某个段的首地址。然后再将从段描述符中获取到的首地址与逻辑地址的偏移量相加就得到了线性地址。
段式管理:
按照程序自身的逻辑关系划分为若干个段,每个段都有一个段名,段从0开始编址。以段为单位进行分配内存,每个段在内存中占连续空间,但各段之间可以不相邻。分段系统的逻辑地址结构由段号(段名)和段内地址(段内偏移量)所组成。
7.3 Hello的线性地址到物理地址的变换-页式管理
VM系统通过将虚拟内存分割为称为虚拟页的大小固定的块,类似的,物理内存被分割为物理页。系统通过操作系统软件、MMU中的地址翻译硬件和一个存放在物理内存中叫做页表的数据结构来完成缓存各种操作。页表将虚拟页映射到物理页,每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。
页表是一个页表条目的数组。虚拟地址空间中的每个页在页表中一个固定的偏移量处都有一个PTE。PTE包含了一位有效位和一个字段,有效位表明了该虚拟页当前是否被缓存在DRAM中。
n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO)和一个n-p位的虚拟页号(VPN)。MMU利用VPN来选择适当的PTE。将页表条目中的物理页号和VPO串联起来就是相应的物理地址。因为物理和虚拟页面都是P字节的,所以物理页面偏移和虚拟页面偏移是相同的。
7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。
36位VPN被划分为四个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含L1页表的物理地址,VPN1提供到一个L1PET的偏移量,这个PTE包含L2页表的基地址,VPN2提供到一个L2PTE的偏移量,以此类推。
7.5 三级Cache支持下的物理内存访问
CPU发送一条虚拟地址,随后MMU按照上述操作获得了物理地址PA。根据cache大小组数的要求,将PA分为CT(标记)、CS(组索引)、CO(块偏移)。根据CS寻找到正确的组,比较每一个缓存是否标记位有效以及CT是否相等。如果命中就直接返回想要的数据,如果不命中,就依次去L2,L3,主存判断是否命中,当命中时,将数据传给CPU同时更新各级cache的cacheline,如果cache已满则要驱逐替换。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给他一个唯一的PID。同时为这个新进程创建虚拟内存。新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当这两个进程中的任一个后来进行写操作时,就会创建新页面。因此,也就为每个进程保持了私有空间地址的抽象概念。
7.7 hello进程execve时的内存映射
execve 函数在当前进程中加载并运行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。加载并运行 hello 需要以下几个步骤:
1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2)映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text 和.data 区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
3)映射共享区域,hello程序与共享对象 libc.so 链接,libc.so 是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4)设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。Linux 将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
DRAM 缓存不命中称为缺页(page fault).图为缺页之前我们的示例页表的状态。CPU 引用了VP3中的一个字,VP3并未缓存在 DRAM 中。地址翻译硬件从内存中读取PTE 3, 从有效位推断出VP3未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4。如果 VP4 已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改 VP4 的页表条目,反映出 VP 4 不再缓存在主存中这一事实。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap) 。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址) 。对于每个进程,内核维护着一个变量brk, 它指向堆的顶部。
分配器将堆视为一组不同大小的块(block) 的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
基本方法:这里指的基本方法应该是在合并块的时候使用到的方法,有最佳适配和第二次适配还有首次适配方法,首次适配就是指的是第一次遇到的就直接适配分配,第二次顾名思义就是第二次适配上的,最佳适配就是搜索完以后最佳的方案,当然这种的会在搜索速度上大有降低。
策略:这里的策略指的就是显式的链表的方式分配还是隐式的标签引脚的方式分配还是分离适配,带边界标签的隐式空闲链表分配器允许在常数时间内进行对前面块的合并。这种思想是在每个块的结尾处添加一个脚部,其中脚部就是头部的一个副本。如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。显式空间链表就是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继指针,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。为了分配一个块,必须确定请求的大小类,并且对适当的空闲链表做首次适配,查找一个合适的块。如果找到了一个,那么就(可选地)分割它,并将剩余的部分插入到适当的空闲链表中。如果找不到合适的块,那么就搜索下一个更大的大小类的空闲链表。如此重复,直到找到一个合适的块。如果空闲链表中没有合适的块,那么就向操作系统请求额外的堆内存,从这个新的堆内存中分配出一个块,将剩余部分放置在适当的大小类中。要释放一个块,我们执行合并,并将结果放置到相应的空闲链表中。
7.10本章小结
本章介绍了存储器的地址空间,讲述了虚拟地址、物理地址、线性地址、逻辑地址的概念,还有进程fork和execve时的内存映射,并详细描述了系统如何应对缺页异常,最后描述了malloc的内存分配管理机制。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m字节序列。所有的I/O设备(例如网络,磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix I/O接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为K 。
4.读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号” 。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k 。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix I/O函数:
1.进程是通过调用open 函数来打开一个已存在的文件或者创建一个新文件的:
int open(char *filename, int flags, mode_t mode);
open 函数将filename 转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位。返回:若成功则为新文件描述符,若出错为-1。
2.进程通过调用close 函数关闭一个打开的文件。
int close(int fd);
返回:若成功则为0, 若出错则为-1。
3.应用程序是通过分别调用read 和write 函数来执行输入和输出的。
ssize_t read(int fd, void *buf, size_t n);
read 函数从描述符为fd 的当前文件位置复制最多n 个字节到内存位置buf 。返回值-1表示一个错误,而返回值0 表示EOF。否则,返回值表示的是实际传送的字节数量。返回:若成功则为读的字节数,若EOF 则为0, 若出错为-1。
ssize_t write(int fd, const void *buf, size_t n);
write 函数从内存位置buf 复制至多n 个字节到描述符fd 的当前文件位置。图10-3 展示了一个程序使用read 和write 调用一次一个字节地从标准输入复制到标准输出。返回:若成功则为写的字节数,若出错则为-1。
8.3 printf的实现分析
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
va_list的定义为typedef char *va_list,这说明它是一个字符指针。我们发现,printf调用了两个外部函数,一个是vsprintf,还有一个是write。
通过查看这两个函数,可知vsprintf函数的作用是将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。write函数是将buf中的i个元素写到终端的函数。
所以,vsprintf 的作用就是格式化。它接受确定输出格式的格式字符串 fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。接着从 vsprintf生成显示信息,到 write 系统函数,直到陷阱系统调用 int 0x80 或 syscall。显示芯片按照刷新频率逐行读取 vram,并通过信号线向液晶显示器传输每一个点(RGB 分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Unix I/O,通过LinuxI/O设备管理方法以及Unix I/O接口及函数了解系统级I/O的底层实现机制,并通过对printf和getchar函数的底层解析加深对Unix I/O以及异常中断等的了解。
结论
hello的“一生”主要经过:
- 程序员通过 I/O 设备在编译器中编写hello.c,并存储在内存中;
- 预处理器通过对头文件、注释的处理得到修改了的文本文件 hello.i;
- 编译器翻译成汇编语言得到 hello.s;
- 汇编器处理得到可重定位目标文件 hello.o;
- 链接器将 hello.o 和如 printf.o 的其他可重定位目标文件链接得到可执行目标文件 hello;
- 在 shell 里运行hello程序;
- fork 创建子进程,shell 调用;
- 运行程序,调用 execve;
- 执行指令,为 hello 分配时间片,hello 执行自己的逻辑控制流;
- 三级cache访问内存,将虚拟地址映射成物理地址;
- 信号、异常控制流,hello 对不同的信号会执行不同操作;
- kill hello,回收子进程;
随着大作业将本学期学习的计算机系统每一章都串联了起来,深感计算机的神奇。原本晦涩难懂的专业词汇现在都变成了脑海里具体的实现,hello的“一生”看似短暂,却也已经将计算机系统的奥妙之处展示出来。感谢《深入理解计算机系统》这本书,陪伴我走过了本学期的计算机系统。
附件
hello.i | hello.c的预处理文件 |
hello.s | hello.i的编译文件 |
hello.o | hello.s的汇编文件 |
elf.txt | hello.o文件的ELF格式 |
helloelf.txt | hello文件的ELF格式 |
hello | 可执行文件 |
hello.s | hello的反汇编文件 |
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1]张璜.(2018).基于支持移动终端的C语言在线编译系统开发研究. 佳木斯职业学院学报(01),386-387.
[2]王德超.(2014).C/C++编译系统内存分配分类比较探究. 软件(02),85-87.
[3]童亚拉.(2010).分布式编译的方法和系统研究. 计算机技术与发展(05),79-82.