摘 要
本论文从hello.c程序出发,阐述hello.c在linux系统中生命周期。具体阐述计算机内的各部分知识,并将之有机结合成计算机系统这门课程的主要框架。
关键词:hello.c,计算机系统,linux
涉及内容
Hello程序在计算机中的历程,可以归结如下:
- 向shell键入翻译指令。hello.c经预处理,编译,汇编,链接生成可执行文件hello。
- 向shell键入执行指令。Shell调用fork为其创建子进程,而后调用execve调用启动加载器,开始执行该可执行目标文件。
- 进入main函数后CPU依据指令,访问内存(进行虚拟内存的使用),与其他进程在调度下合理运行,彼此不干扰。
- 当执行完毕,Shell回收子进程,内核删除为这个进程创建的所有 数据结构。
请移步我的github
https://github.com/yangzqy/CSAPP
以下排版比较混乱,具体文件可到github上查看。
计算机系统
大作业
摘 要
本论文从hello.c程序出发,阐述hello.c在linux系统中生命周期。具体阐述计算机内的各部分知识,并将之有机结合成计算机系统这门课程的主要框架。
关键词:hello.c,计算机系统,linux
目 录
第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 -
第8章 HELLO的IO管理 - 13 -
8.1 LINUX的IO设备管理方法 - 13 -
8.2 简述UNIX IO接口及其函数 - 13 -
8.3 PRINTF的实现分析 - 13 -
8.4 GETCHAR的实现分析 - 13 -
8.5本章小结 - 13 -
结论 - 14 -
附件 - 15 -
参考文献 - 16 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P(From Program to Process):在linux中,通过在shell中输入命令,调用编译器驱动程序,将源程序文件hello.c翻译为一个可执行目标文件hello。该翻译过程可分四个阶段:预处理,编译,汇编,链接。而后,在shell中输入启动该可执行文件的命令,shell会为其fork,产生子进程。
020(From Zero-0 to Zero-0): 在向shell中输入启动命令之前,为“0”。 在shell中输入启动该可执行文件的命令,shell会为其fork,产生子进程,分配内存资源,execve映射虚拟内存,进入程序入口后程序开始载入物理内存,之后进入main函数执行代码,cpu为该进程分配时间片执行逻辑控制流。当程序运行结束后,shell回收hello子进程,内核删除相关数据结构。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:intel(R)Core(TM)i7-8750H CPU @ 2.20GHz 2.21Ghz
软件环境:ubuntu 18.04
开发与调试工具:gcc,edb,gedit,readelf,Hexedit
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
中间结果文件 文件作用
hello.i 预处理得到的文件
hello.s ASCII汇编语言文件
hello.o as得到可重定位目标文件
hello.obj 反汇编得到的文本文件
hello.elf hello.o的elf文件
hello ld得到可执行目标文件
Hello1.elf hello的elf文件
1.4 本章小结
本章概括讲述hello的P2P以及020过程,该实验的环境信息以及中间结果。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器cpp根据以字符#开头的命令(宏定义,条件编译),修改原 始的 C 程序,将引用的所有库展开合并成为一个完整的文本文件。
具体而言。
- 将源文件前部用#include 形式声明的文件复制到新的程序中。
- 根据#define的定义,将被代替字符替换为目标字符
2.2在Ubuntu下预处理的命令
图2.1
2.3 Hello的预处理结果解析
预处理生成的hello.i文件的长度已经由hello.c的23行扩展为3113行。且main函数开始于3100行。
在这3100行以前,为对stdio.h unistd.h stdlib.h的依次展开。
图2.2
图2.3
2.4 本章小结
本章阐明预处理阶段的概念和作用,并且给出具体例子。
第3章 编译
3.1 编译的概念与作用
编译器将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序,这个过程称为编译。
编译的作用是将源程序翻译为汇编程序。
3.2 在Ubuntu下编译的命令
图3.1
3.3 Hello的编译结果解析
3.3.0 总体介绍
.file:源文件名;.globl:全局变量;.data:数据段;.align:对齐方;.type:指定是对象类型或是函数类型;.size:大小;.long:长整型;.section .rodata:下面是.rodata节;.string:字符串;.text:代码段
3.3.1 数据
(1) 整型数据
- int i:,由如图所示汇编语句以及hello.c源程序可知:储存在栈上空间-4(%rbp)的位置上,占有四个字节。
图3.2
- int argc:作为main函数的第一个参数,储存在寄存器%edi中。
(2)字符串
本实验使用的字符串有:“用法: Hello 学号 姓名 秒数!\n"以及"Hello %s %s\n”。
1)“用法: Hello 学号 姓名 秒数!\n”:是printf的输出格式化参数,在hello.s文件中声明如下图,位于.LC0处,字符串被编码为UTF-8编码,其中每一个汉字占有3个字节。
2)“Hello %s %s\n”:同第一个字符串。位于.LC1处。
图3.3
3.3.2 赋值
本实验中仅对i有赋值操作,由3.3.1节可知int i存放在栈上空间-4(%rbp)处,故而直接进行mov操作即可。
。
3.3.3 算术操作
本实验中仅对i有算术操作,由3.3.1节可知int i存放在栈上空间-4(%rbp)处,故而直接进行add操作即可。
3.3.4 关系操作
(1) argc != 4.在此处由汇编语句:movl %edi, -20(%rbp),先将argc 的值存入栈中-20(%rbp)的位置,而后使用cmp指令将之与立即数4比较,设置条件码寄存器。
图3.4
(2) i < 8。由3.3.1节可知int i存放在栈上空间-4(%rbp)处,故而直接进行cmp指令将之与立即数7作比较,设置条件码寄存器即可。
图3.5
3.3.5 数组
本实验使用的数组为char *argv[]。
对于该数组而言,其作为main的第二个参数传入,其首地址存放在寄存器%rsi中。
3.3.6 控制转移
(1)比较argc是否等于4,若是,跳转到.L2部分。
图3.6
(3) 比较i是否小于等于7,若是,跳转到.L4部分。
图3.7
(4) 无条件跳转语句
图3.8
3.3.7 函数操作
(1)main函数
1)传递控制。main由系统启动函数__libc_start_main调用call指令启动。
2)传递数据。main由系统启动函数__libc_start_main通过%rdi以及%rsi传递参数argc和argv。返回值由%rax储存。
(2)printf函数
1)传递控制。printf函数由main函数调用call指令启动。
2)传递数据。
对于printf(“用法: Hello 学号 姓名 秒数!\n”);而言
printf函数由main函数通过%rdi传递输出格式化参数。
对于printf(“Hello %s %s\n”,argv[1],argv[2]);而言
printf函数由main函数通过%rdi,%rsi,%rdx传递输出格式化参数,以及argv[1],argv[2]。
(3)对于atoi函数。
1)传递控制。atoi函数由main函数调用call指令启动。
2)传递数据。atoi函数由main函数调用%rdi传递参数。
(4)对于sleep函数。
1)传递控制。sleep函数由main函数调用call指令启动。
2)传递数据。sleep函数由main函数调用%edi传递参数。
3.4 本章小结
本章主要阐述了编译阶段的概念和作用。并且结合例子详细说明了hello.c与hello.s之间的联系。
第4章 汇编
4.1 汇编的概念与作用
汇编器将汇编程序翻译成机器语言指令,并把这些指令打包成可重定向目标程序的格式,并将结果保存在目标文件中。
4.2 在Ubuntu下汇编的命令
图4.1
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
使用 readelf -a hello.o > hello.elf 将hello.o的各节信息输出到hello.elf文件中。
4.3.1 ELF头(ELF.header)
ELF头:ELF头(ELF.header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小,目标文件的类型,机器类型,节头部表的文件偏移,异界节头部表中条目的大小和数量等等。
图4.2
4.3.2节头部表(Section Headers)
节头部表(Section Headers)包含了文件中出现的各个节的信息。
图4.3
4.3.3重定位节.rela.text
重定位节.rela.text,一个.text节中位置的列表,当连接器把这个目标文件和其他文件组合时,需要修改这些位置。
该节中每个条目包含的信息有:
(1) Long offset:需要被修改的引用的节偏移。
(2) Long type:告知链接器如何修改新的应用。
(3) Long symbol:标识修改引用应该指向的符号。
(4) Long addend:一个有符号常数,对应某些类型的重定位的偏移调整值。
图4.4
4.3.4.symtab节
.symtab节,一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
图4.5
4.3.5.rela.eh_frame节
.rela.eh_frame节eh_frame 节的重定位信息。
图4.6
4.4 Hello.o的结果解析
二者的主要差别如下:
(1) 操作数形式:在hello.s中的操作数为十进制,而hello.o的反汇编代码中为16进制。
(2) 分支转移:在跳转语句中,hello.s中的跳转指令后为跳转目标的段名称,而hello.o的反汇编代码中为跳转目标的相对偏移的地址。
(3) 函数调用:在call语句中,hello.s中call指令后为函数的名称,而在hello.o的反汇编代码中为下一条指令的相对偏移的地址。
图4.7
图4.8
4.5 本章小结
本章介绍了汇编的概念及其作用,以本次实验的程序为例,着重分析了汇编产生的文件的结构以及各部分作用,对汇编前的文件与汇编后的文件进行了详细的对比。
第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。
5.2 在Ubuntu下链接的命令
图5.1
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
(1)ELF格式的分析:
1)可执行目标文件hello的格式类似于可重定位目标文件的格式,ELF头描述文件的总体格式。
2)区别于可重定位目标文件的格式,可执行目标文件包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。.text、.rodata和.data节与可重定位目标文件中的节是类似的,除了这些节已经被重定位到它们最终的运行时的内存地址外。.init节定义了一个小函数_init,程序初始化代码会调用它。因为可执行文件时完全连接的,所以无.rel节。
(2)各段的基本信息:
从下图中,我们可以读出各段的起始地址,大小等信息。
图5.2
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
在虚拟地址空间(0x400000到0x401000)中,各段的排列,大小,起始地址均与5.3中ELF节头所声明的相同。
图5.3
5.5 链接的重定位过程分析
(1)hello与hello.o的不同
1) hello文件中增加了额外的节,这些节中包含 hello.c运行需要的函数。例如:printf,exit等。
2) 每一条语句的地址都已经由hello.o中的相对偏移地址变为虚拟地址。
Call指令的字节表示由00 00 00 00变为需要的偏移地址,call指令的汇编表示由call后为下一条指令的偏移地址变为需要调用函数的虚拟地址。
(2)由(1)分析得出链接的过程为:
1) 确定所需要的库中的可重定位目标文件,同时判断源文件是否可以正确运行。
2) 整合源可重定位目标文件和所需要的可重定位目标文件为一个文件,进行运行时地址赋予。
图5.4
(3)重定位具体分析:
下面详细说明重定位的过程。
重定位分两种类型:(1)R_X86_64_PC32(2)R_X86_64_32
(1) R_X86_64_PC32(重定位PC相对引用)
refptr = s +r.offset (1)
refaddr = ADDR(s) + r.offset (2)
*refptr = (unsigned) (ADDR(r.symbol) + r.addend - refaddr)(3)
其中ADDR(s)每个节的运行时地址。(ADDR(r.symbol)为每个节中某个符号的运行时地址。
(2) R_X86_64_32(重定位绝对引用)
*refptr = (unsigned) (ADDR(r.symbol) + r.addend)
举本次实验中字符串(即下图中框中所示)来进行说明:
可知refaddr = ADDR(s) + r.offset = ADDR(main) + 0x18 = 0x40059a
*refptr = (unsigned) (ADDR(r.symbol) + r.addend - refaddr) = 0x400698 – 4 + 40059a = 0xfa
恰好符合。
图5.5
图5.6
5.6 hello的执行流程
(以下格式自行编排,编辑时删除)
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
0x4004c0 init;
0x4004f0 puts@plt;
0x400500 printf@plt;
0x400510 getchar@plt;
0x400520 atoi@plt
0x400530 exit@plt;
0x400540 sleep@plt;
0x400550 _start;
0x400582 main;
0x400610 __libc_csu_init;
0x400680 __libc_csu_fini;
0x400684 _fini;
5.7 Hello的动态链接分析
图5.7:执行ld_init之前
图5.8:执行ld_init之后
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表 PLT+全局偏移量表 GOT 实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
我们可以观察到,在执行ld_init之前,global_offset表全0,执行ld_init之后被赋值。
5.8 本章小结
本章详细说明了链接的概念和作用,首先阐述了可执行文件的基本结构,通过观察可执行文件与可重定位文件的不同,明白了链接的作用,而后分析了虚拟地址空间、重定位过程、执行流程、动态链接过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个执行中的程序的实例。
进程将会提供给应用程序两个抽象:
1) 一个独立的逻辑控制流。
2) 一个私有的地址空间。
6.2 简述壳Shell-bash的作用与处理流程
Shell的作用:shell是一个应用程序,它可以使得用户访问操作系统内核的服务。
Shell的处理流程:
1)从终端读入输入的命令。
2)将输入字符串切分获得所有的参数。
3)如果是内置命令则立即执行。
4)否则调用相应的程序为其分配子进程并运行。
5)shell 应该接受键盘输入信号,并对这些信号进行相应处理。
6.3 Hello的fork进程创建过程
当向shell中输入一个命令后,shell会解析该命令,如果该命令不是一个内置的shell命令时,shell将会认为该命令为执行以此命令为名称的可执行文件。
终端程序调用fork创建一个新的子进程。新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,这就意味着,当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程与子进程之间最大的区别在于它们拥有不同的PID。
6.4 Hello的execve过程
当fork之后,子进程调用 execve 函数(传入命令行参数)在当前进程的上下文中加载并运行一个新程序即目标程序,execve 调用驻留在内存中的被称为启动加载器的操作系统代码来执行 hello 程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚 拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置 PC 指向_start地址,_start最终调用 hello 中的 main 函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到 CPU 引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
结合虚拟内存和内存映射过程,可以更详细地说明exceve函数实际上是如何加载和执行程序Hello,需要以下几个步骤:
1.删除已存在的用户区域。
2.映射私有区域。为Hello的代码、数据、bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时复制的。
3.映射共享区域。比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
6.5.1 简单信息介绍
逻辑控制流:一系列程序计数器 PC 的值的序列叫做逻辑控制流,进程是轮流 使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占 (暂时挂起),然后轮到其他进程。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的 代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任 何命令,并且可以访问系统中的任何内存位置。
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。
在某个进程执行过程中,处理器将会限制该进程所能执行的指令和需要访问的地址空间。处理器通常会用某个控制寄存器中的一个位为来提供上述操作。当未设置模式位时,此时,进程处于用户模式,该进程只能执行和访问有限的指令和地址空间。当设置了该标志位时,进程处于内核模式,进程就可以执行指令集中的任意指令和可以访问系统中的任何内存空间。
当一个程序需要由用户模式进入内核模式,只能通过异常来实现。
在进程执行过程中,如果该进程没有被抢占,则会一直执行。如果发生抢占,则进行上下文切换。内核可以决定是否抢占当前进程,并重新开始一个先前被抢占的进程,这种决策就是调度。
上下文切换是由内核中调度器完成的,当内核调度新的进程运行后,它就会抢占当前进程,并进行1)保存以前进程的上下文2)恢复新恢复进程被保存的上下文,3)将控制传递给这个新恢复的进程来完成上下文切换。
6.6 hello的异常与信号处理
图一为程序正常执行的结果,最后将进程回收。
图二为在进程正常执行的过程中,从键盘输入Ctrl-C的结果,父进程收到SIGINT信号,终止hello进程的运行,最终回收该进程。
图三为在进程正常执行的过程无规律输入的结果,这些字符不会影响进程的执行,而是会放入缓冲区,而当进程需要输入执行,会在缓冲区中找到’\n’,进行输入。
图四为在进程正常执行的过程中,从键盘输入Ctrl-C的结果,hello进程被挂起。而后输入ps命令查看进程信息;jobs命令列出当前shell环境中已启动的任务状态;pstree命令是以树状图显示进程间的关系;kill发送信号给一个进程或多个进程。
图1
图2
图3
图4
(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.7本章小结
本章阐述了进程概念以及进程的执行,以及异常处理等内容。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:包含在机器语言中用来指定一个操作数或一条指令的地址。每一个逻辑地址都由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。
线性地址:逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。是hello中的虚拟内存地址。
虚拟地址:一个带虚拟内存的系统中,CPU从一个有N=2^n个地址空间中生成虚拟地址。虚拟地址就是反汇编hello文件内提供的地址。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。简单而言,就是文件在内存存储位置的按字节为单位的标号。
7.2 Intel逻辑地址到线性地址的变换-段式管理
最初 8086 处理器的寄存器是 16 位的,为了能够访问更多的地址空间但不改变寄存器和指令的位宽,所以引入段寄存器,8086 共设计了 20 位宽的地址总线,通过将段寄存器左移 4 位加上偏移地址得到 20 位地址,这个地址就是逻辑地址。将内存分为不同的段,段有段寄存器对应,段寄存器有一个栈、一个代码、两个数据寄存器。
分段功能在实模式和保护模式下有所不同。
实模式,即不设防,也就是说逻辑地址=线性地址=实际的物理地址。段寄存器存放真实段基址,同时给出 32 位地址偏移量,则可以访问真实物理内存。
在保护模式下,线性地址还需要经过分页机制才能够得到物理地址,线性地址也需要逻辑地址通过段机制来得到。段寄存器无法放下 32 位段基址,所以它们被称作选择符,用于引用段描述符表中的表项来获得描述符。描述符表中的一个条目描述一个段,构造如下:
图7.1
Base:基地址,32 位线性地址指向段的开始。Limit:段界限,段的大小。 DPL:描述符的特权级 0(内核模式)-3(用户模式)。
所有的段描述符被保存在两个表中:全局描述符表GDT和局部描述符表LDT。
gdtr 寄存器指向 GDT 表基址。
段选择符构造如下:
图7.2
TI:0 为 GDT,1 为 LDT。Index 指出选择描述符表中的哪个条目,RPL 请求特权级。
所以在保护模式下,分段机制就可以描述为:通过解析段寄存器中的段选择符在段描述符表中根据 Index 选择目标描述符条目 Segment Descriptor,从目标描述符中提取出目标段的基地址 Base address,最后加上偏移量 offset 共同构成线性地址 Linear Address。保护模式时分段机制图示如下:
图7.3
当 CPU 位于 32 位模式时,内存 4GB,寄存器和指令都可以寻址整个线性地址空间,所以这时候不再需要使用基地址,将基地址设置为 0,此时逻辑地址=描述符=线性地址,Intel 的文档中将其称为扁平模型(flat model),现代的 x86 系统内核使用的是基本扁平模型,等价于转换地址时关闭了分段功能。在 CPU 64 位模式中强制使用扁平的线性空间。逻辑地址与线性地址就合二为一了。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(书里的虚拟地址 VA)到物理地址(PA)之间的转换通过分页机制完成。而分页机制是对虚拟地址内存空间进行分页。
首先 Linux 系统有自己的虚拟内存系统,其虚拟内存组织形式如图 7.5,Linux 将虚拟内存组织成一些段的集合,段之外的虚拟内存不存在因此不需要记录。内核为 hello 进程维护一个段的任务结构即图中的 task_struct,其中条目 mm 指向一个 mm_struct,它描述了虚拟内存的当前状态,pgd 指向第一级页表的基地址(结合一个进程一串页表),mmap 指向一个 vm_area_struct 的链表,一个链表条目对应一个段,所以链表相连指出了 hello 进程虚拟内存中的所有段。
图7.4
系统将每个段分割为被称为虚拟页(VP)的大小固定的块来作为进行数据传输的单元,在 linux 下每个虚拟页大小为 4KB,类似地,物理内存也被分割为物理页(PP/页帧),虚拟内存系统中 MMU 负责地址翻译,MMU 使用存放在物理内存中的被称为页表的数据结构将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。
如图,不考虑TLB与多级页表(在 7.4 节中包含这两者的综合考虑),虚拟地址分为虚拟页号 VPN 和虚拟页偏移量 VPO,根据位数限制分析(可以在 7.4 节中看到分析过程)可以确定 VPN 和 VPO 分别占多少位是多少。通过页表基址寄存器 PTBR+VPN 在页表中获得条目 PTE,一条 PTE 中包含有效位、权限信息、物理页号,如果有效位是 0+NULL 则代表没有在虚拟内存空间中分配该内存,如果是有效位 0+非 NULL,则代表在虚拟内存空间中分配了但是没有被缓存到物理内存中,如果有效位是 1 则代表该内存已经缓存在了物理内存中,可以得到其物理页号 PPN,与虚拟页偏移量共同构成物理地址 PA。
图7.8
7.4 TLB与四级页表支持下的VA到PA的变换
在 Intel Core i7 环境下研究 VA 到 PA 的地址翻译问题。前提如下:虚拟地址空间 48 位,物理地址空间 52 位,页表大小 4KB,4 级页表。TLB 4 路 16 组相联。CR3 指向第一级页表的起始位置(上下文一部分)。
解析前提条件:由一个页表大小 4KB,一个 PTE 条目 8B,共 512 个条目,使用 9 位二进制索引,一共 4 个页表共使用 36 位二进制索引,所以 VPN 共 36 位,因为 VA 48 位,所以 VPO 12 位;因为 TLB 共 16 组,所以 TLBI 需 4 位,因为 VPN 36 位,所以 TLBT 32 位。
如图 7.7,CPU 产生虚拟地址 VA,VA 传送给 MMU,MMU 使用前 36 位 VPN 作为 TLBT(前 32 位)+TLBI(后 4 位)向 TLB 中匹配,如果命中,则得到 PPN(40bit)与 VPO(12bit)组合成 PA(52bit)。
如果 TLB 中没有命中,MMU 向页表中查询,CR3 确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出 PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到 PPN,与 VPO 组合成 PA,并且向 TLB 中添加条目。
如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。
图7.9
7.5 三级Cache支持下的物理内存访问
前提:只讨论 L1 Cache 的寻址细节,L2 与 L3Cache 原理相同。L1 Cache 是 8 路 64 组相联。块大小为 64B。
解析前提条件:因为共 64 组,所以需要 6bit CI 进行组寻址,因为共有 8 路,因为块大小为 64B 所以需要 6bit CO 表示数据偏移位置,因为 VA 共 52bit,所以CT 共 40bit。
在上一步中我们已经获得了物理地址 VA,如图 7.8,使用 CI(后六位再后六位)进行组索引,每组 8 路,对 8 路的块分别匹配 CT(前 40 位)如果匹配成功且块的 valid 标志位为 1,则命中(hit),根据数据偏移量 CO(后六位)取出数据返回。
如果没有配成功或者匹配成功但是标志位是 1,则不命中(miss),向下一级缓存中查询数据(L2 Cache->L3 Cache->主存)。查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略 LFU 进行替换。
图7.10
7.6 hello进程fork时的内存映射
当 fork 函数被 shell 进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的 PID,为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
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 做的最后一件事情就是设置当前进程
上下文的程序计数器,使之指向代码区域的入口点。
图7.11
7.8 缺页故障与缺页中断处理
缺页故障是一种常见的故障,当指令引用一个虚拟地址,在 MMU 中查找页表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出的时候就会发生故障。其处理流程遵循下图所示的故障处理流程。
图7.12
故障处理流程缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU 重新启动引起缺页的指令,这条指令再次发送 VA 到MMU,这次 MMU 就能正常翻译 VA 了。
7.9动态存储分配管理
printf 函数会调用 malloc,下面简述动态内存管理的基本方法与策略:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
一、 带边界标签的隐式空闲链表
1) 堆及堆中内存块的组织结构:
图7.13
在内存块中增加 4B 的 Header 和 4B 的 Footer,其中 Header 用于寻找下一个 blcok,Footer 用于寻找上一个 block。Footer 的设计是专门为了合并空闲块方便的。因为 Header 和 Footer 大小已知,所以我们利用 Header 和 Footer 中存放的块大小就可以寻找上下 block。
2) 隐式链表
所谓隐式空闲链表,对比于显式空闲链表,代表并不直接对空闲块进行链接,而是将对内存空间中的所有块组织成一个大链表,其中 Header 和 Footer 中的 block 大小间接起到了前驱、后继指针的作用。
3) 空闲块合并
因为有了 Footer,所以我们可以方便的对前面的空闲块进行合并。合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。对于四种情况分别进行空闲块合并,我们只需要通过改变 Header 和 Footer 中的值
就可以完成这一操作。
二、 显示空间链表基本原理
将空闲块组织成链表形式的数据结构。堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个 pred(前驱)和 succ(后继)指针,如下图:
图7.14
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。
维护链表的顺序有:后进先出(LIFO),将新释放的块放置在链表的开始处,使用 LIFO 的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比
LIFO 排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。
7.10本章小结
本章主要介绍了 hello 的存储器地址空间、intel 的段式管理、hello 的页式管理,以 intel Core7 在指定环境下介绍了 VA 到 PA 的变换、物理内存访问,还介绍了 hello 进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等内容。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有的 IO 设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。
8.2 简述Unix IO接口及其函数
Unix I/O 接口统一操作:
1) 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
2) Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
3) 改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行 seek,显式地将改变当前文件位置 k。
4) 读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文件,当 k>=m 时,触发 EOF。类似一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。
5) 关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
Unix I/O 函数:
1) int open(char* filename,int flags,mode_t mode) ,进程通过调用 open 函数来打开一个存在的文件或是创建一个新文件的。open 函数将 filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位。
2) int close(fd),fd 是需要关闭的文件的描述符,close 返回操作结果。
3) ssize_t read(int fd,void *buf,size_t n),read 函数从描述符为 fd 的当前文件位置赋值最多 n 个字节到内存位置 buf。返回值-1 表示一个错误,0 表示 EOF,否则返回值表示的是实际传送的字节数量。
4) 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;
char buf[256];
va_list arg = (va_list)((char)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
(char*)(&fmt) + 4) 表示的是…可变参数中的第一个参数的地址。而vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。接着从vsprintf生成显示信息,到write系统函数,直到陷阱系统调用 int 0x80或syscall。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成 ASCII 码,保存到系统的键盘缓冲区之中。
getchar 函数落实到底层调用了系统函数 read,通过系统调用 read 读取存储在键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar 进行封装,大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了printf 函数和 getchar 函数。
结论
Hello程序在计算机中的历程,可以归结如下:
- 向shell键入翻译指令。hello.c经预处理,编译,汇编,链接生成可执行文件hello。
- 向shell键入执行指令。Shell调用fork为其创建子进程,而后调用execve调用启动加载器,开始执行该可执行目标文件。
- 进入main函数后CPU依据指令,访问内存(进行虚拟内存的使用),与其他进程在调度下合理运行,彼此不干扰。
- 当执行完毕,Shell回收子进程,内核删除为这个进程创建的所有 数据结构。
附件
列出所有的中间产物的文件名,并予以说明起作用。
中间结果文件 文件作用
hello.i 预处理得到的文件
hello.s ASCII汇编语言文件
hello.o as得到可重定位目标文件
hello.obj 反汇编得到的文本文件
hello.elf hello.o的elf文件
hello ld得到可执行目标文件
Hello1.elf hello的elf文件
参考文献
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.
本文深入探讨了Hello程序从源代码到可执行文件的全过程,包括预处理、编译、汇编、链接等阶段,以及在Linux系统中作为进程的生命周期管理,涵盖了存储管理、IO管理和虚拟内存使用等关键概念。
1907

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



