摘 要 本文对hello.c在Linux下的生命周期进行了分析。通过一些Linux平台的工具,如gcc,objdump,edb,gdb,readelf对程序代码的预处理,编译,汇编,链接,反汇编的过程进行了分析,对比。通过hello在shell中的动态链接,进程运行,内存管理,I/O管理等过程的研究来深入理解Linux系统的动态链接机制,存储层次结构等内容。对各个阶段皆有所探究,对理解底层程序与操作系统的实现原理又一定促进。 关键词:操作系统;编译;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第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的P2P(From program to program):gcc编译器驱动程序读取hello.c(文本),首先进行预处理(cpp)生成预处理程序hello.i(文本),得到的实际上还是高级语言的原程序文件,不过是对带#指令进行了处理。然后通过编译(cc1)生成汇编语言写的程序hello.s(文本),hello.s是汇编指令构成的程序。然后再对hello.s进行汇编(as)生成hello.o(二进制/机器指令),hello.o是可重定位目标程序。要得到最终机器能执行的程序,还要进行链接,最终才能生成可执行的目标程序。这就是P2P Hello的O2O(From Zero-0 to Zero -0):Shell 调用fork函数创建一个与父进程shell完全相同的子进程(只读/共享),然后调用execve函数,在当前进程的上下文中加载并运行hello程序。将hello中的.text节,.data节,.bss节等内容加载到当前进程的虚拟地址空间。简单来说就是shell进程生成子进程并调用exceve系统调用启动加载器,以装入hello程序,最后跳转到hello第一条指令执行。通过cache高效的读取指令,将程序的指令在硬件上实现,操作系统提供的虚拟内存机制维护了hello自身的空间,从而不与其他进程互相干扰,TLB,分级页表为数据在内存中的高效访问提供了支持。Hello依靠OS提供的服务(系统调用)简介访问硬件资源。hello执行完成后shell会回收hello进程,然后内核从系统中删除hello的所有痕迹。这就是O2O 1.2 环境与工具 硬件环境: 处理器:Intel Core i7-7700HQ CPU @ 2.80GHZ 磁盘:1T HDD+128 SSD 内存:8G DDR4 1.3 中间结果
hello.c:源代码,原程序文件
hello.i和hello1.i:hello.c通过预处理生成的文本文件
hello.s:hello.i经过编译器编译生成的文本文件
hello.o:hello.s进过汇编生成的二进制文件(可重定位目标文件)
hello.out:hello.o经过链接后生成的二进制文件(可执行目标文件)
asm.txt:hello.o反汇编生成的文本文件(汇编语句)
hello:由要求上的语句直接生成,即gcc -m64 -no -pie -fno -PIC hello.c -o hello 生成
out_asm.txt:由hello反汇编生成的文本文件(汇编语句)
1.4 本章小结 本章十分简要的介绍了hello程序的生命周期。同时,还列出本次实验的基本信息,包括硬件配置,环境等。介绍了研究过程中的中间结果。 (第1章0.5分)
第2章 预处理 2.1 预处理的概念与作用 1.概念 预处理指预处理器根据源文件中以”#”开头的预编译指令,包括: —删除”#define”并展开所定义的宏 —处理所有条件预编译指令,如”#if”,”#ifdef”,”#endif”等 —插入头文件到”#include”处 —删除所有注释如”//”,”/* */” —添加行号和文件名标识,以便编译器产生调试用的行号信息 —保留所有#pragma编译指令
2.作用 预处理做了代码文本打的替换工作,提供了很大的灵活性,以适应不同的计算机和操作系统环境的限制。 2.2在Ubuntu下预处理的命令 预处理有两种方式
$gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析 Hello.i
由于插入了头文件后预处理结果代码非常长,这里只截部分。我们可以看见,跟上文概念处提到的一模一样。在原有代码基础上将头文件stdio.h插入,声明函数,定义结构体,定义宏等等。 我们可以发现预处理是对代码进行了一个初步的处理,对源代码进行了一些替换。
其实这样做的主要意义便是能把用于不同环境的代码放在同一个文件中,再预处理阶段修改代码,使之适应当前的环境。
2.4 本章小结
本章较为简略的介绍了编译器预处理的相关概念。预处理是在编译前对代码进行转换与处理,使得编译得以成功进行。如实现将定义的宏展开,插入头文件内容等等。经过预处理,代码为编译做好了准备。 (第2章0.5分)
第3章 编译 3.1 编译的概念与作用 1.概念 编译过程就是将预处理后得到的hello.i预处理文件进行词法分析、语法分析、语义分析、优化等操作后,生成的汇编代码文件hello.s。而用来进行编译处理的程序称为编译器(Compiler)。 2.作用 汇编代码文件可供编译器进行生成机器码、链接等操作 3.2 在Ubuntu下编译的命令 编译的命令有3种,这里只展示其较为简单的2种
$gcc -S hello.i -o hello.s
$gcc -S hello.c -o hello.s
3.3 Hello的编译结果解析
首先说明汇编文件的伪指令:
指令 对应内容 .file 声明源文件 .text 声明代码段 .data 声明数据段 .align 声明指令及数据存放地址的对齐方式 .type 指定类型 .size 声明大小 .section .rodata 只读数据 .globl 全局变量
1.字符串 汇编语言中,常量如输出字符串被存储于.rodata中(输出字符串作为全局变量保存)。hello.s中,有两个字符串,我们可以从上图看出 字符串(1):
那么\345\255\246…这些有点莫名其妙的是什么呢,这其实是utf-8编码,中文汉字以’\345’开头,占三个字符,而全角字符’!’占两个。
字符串(2):
2整型数 2.1 hello.c中出现的全局变量sleepsecs 定义如下:
下面来解释一下sleepsecs的定义。.globl代表它是全局变量,对齐方式是.align 4,sleepsecs的size大小为4Byte,类型为.long,但是我们看源文件中是int,这个原因一般是编译器优化,编译器ccl会将int表示为long但对齐方式仍然为整型int的4字节,long类型则为双字quad,这里不再做赘述。
Rsp为栈顶指针,rbp为栈底指针
2.2main函数参数argc,是首个参数,所以保存于%edi,由movl,到-0x20(%rbp), 局部变量i,位于栈空间-0x4(%rbp),占4个字节。
3.数组
argv1 ,argv2 分别是第二个第三个参数,所以argv2 位于%rdx,地址为-0x16(%rbp),argv1 位于%rsi,地址为-0x2a(%rbp),数组首地址为-0x32(%rbp),所占字节为8
讲完了常量变量我们首先强调一下汇编的数据格式 C声明 Intel数据类型 汇编代码后缀 大小(字节) char 字节 b 1 short 字 w 2 int 双字 l 4 long 四字 q 8 char* 四字 q 8 float 单精度 s 4 double 双精度 l 8
汇编的各个操作
赋值(数据传送指令) 根据大小,分为movb,movw,movl,movq,movabsq。这里不做详细介绍,想要详细了解可以看书。上文也提到了将局部变量i = 0的代码为movl $0x0,-0x4(%rbp)。
算术操作 hello程序里的”i++”,其汇编代码为 addl $1,-4(%rbp),即为i的值加1。 这里列出部分常见的算术和逻辑操作 leaq S,D 加载有效地址 D←&S INC D 加1 D←D+1 DEC D 减1 D←D-1 NEG D 取负 NOT D 取补 ADD S,D 加 D←D+S SUB S,D 减 D←D-S IMUL S,D 乘 D←D*S XOR S,D 异或 D←D^S 还有OR或,AND与,SAL左移,SHL右移等
关系操作
CMP,它是基于S1-S2的,根据不同的数据,分为cmpb比较字节,cmpw比较字等,这里不详细描述。通过CMP比较后,会改变条件码,例如,假如两个操作数相等,那么就会将零标志设置为1。
条件码 为了方便讲述后文的条件跳转,我们这里讲解一些基本的条件码 CF 进位标志。最近的操作使得最高位产生了进位 ZF 零标志。最近操作得出的结果为0 SF 符号标志。最近的操作得到的负数 OF 溢出标志。最近的操作导致一个补码溢出
条件跳转 根据CMP指令后条件码而跳转。常见的有je(相等) 跳转条件为ZF,jne(不相等),跳转条件为~ZF。 hello.s中
就是看argc是否为3,若相等那么就跳转,若不等就不跳转,cmpl后可能会改变条件码 跳转继续向后执行代码
跳转后执行.L2
4.转移控制 call Label过程调用 call *Operand 过程调用 ret 从过程调用中返回 call 函数,相当于先压栈再跳转到调用的子程序 hello.s中 这两处都用到了转移控制,前者是若arg不为3,输出信息并执行exit函数,后者是单纯的调用sleep函数。 还有call getchar,print函数,这里不再描述
5.函数操作 hello程序中的函数操作包括main函数本身参数读取,printf调用,sleep调用,getchar调用。这里我们借用一张书上的图来描绘它们的栈帧结构
main:main函数通过 等操作为函数分配栈空间,若通过exit结束,main不会释放内存,需要通过return正常返回
exit(),printf():printf的参数是根据字符串中的输出位数量来决定的
puts():唯一值得讲解一下的,若printf函数有其他参数时,参数的传递顺序是
参数 1 2 3 4 5 6 其余 地址 %rdi %rsi %rdx %rcx %r8 %r9 getchar():getchar()函数无参数,返回值为int,成功就返回用户输入ASCII码,失败返回-1.
3.4 本章小结 本章介绍了编译器进行编译操作的相关内容。编译器实现将C语言代码转换成汇编代码,从而最终转换为机器代码。生成汇编代码的这个过程需要对数据、操作都进行对应转换,数据包括常量、变量(全局/局部/静态)、表达式、宏等,操作包括算术、逻辑、位、关系、函数等操作。从五个方面对hello.s各部分做出了相应的介绍说明
(第3章2分)
第4章 汇编 4.1 汇编的概念与作用 概念:汇编程序将汇编语言源程序转换为机器指令序列
作用:汇编器(as)将hello.s翻译成机器语言指令,这是一个可重定位文件,其在链接后能被计算机直接执行 4.2 在Ubuntu下汇编的命令
$gcc -c hello.s -o hello.o
$gcc -c hello.c -o hello.o
$as hello.s -o hello.o
4.3 可重定位目标elf格式 ELF头
节头
重定位节
重定位条目被定义为一个结构体 包括8字节的偏移量,4个字节的重定位类型,4个字节的在符号表中的偏移量,8个字节的计算重定位位置的辅助信息。 共有32个不同的重定位类型,而hello.o中出现了2个如上图 R X86_ 64 PC32。 重定位-一个使用32位PC相对地址的引用。一个PC相对地址就是距程序计数器(PC)的当前运行时值的偏移量。当CPU执行一条使用PC相对寻址的指令时,它就将在指令中编码的32位值加上PC的当前运行时值,得到有效地址(如call指令的目标),PC值通常是下一条指令在内存中的地址。
R X86_ 64 _32。 重定位一个使用32位绝对地址的引用。通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。
.symtab节
.sytmab存放着定义和引用函数和全局变量的信息 4.4 Hello.o的结果解析 hello.o反汇编: 首先用 objdump -d -r hello.o > asm.txt
hello.s:
两者异同:hello.s中有清晰的描述,如对全局变量的描述,还有.rodata只读数据段。而asm.txt中只有最基础的描述,记录了文件的格式(elf64-x86-64)和.text代码段。两者都包含main函数完整的代码,但hello.s中是由一段段的伪指令构成的,而asm.txt由汇编指令所构成,包含着完整的跳转逻辑,函数调用等,唯一的空白处是需要链接后才能确定的地址 下面详细说明各方面的差异 函数调用 在asm.txt中,call地址后为4个字节的0(8个0),这是占位符,库函数调用需要链接才能确定地址,所以这指向的是下一条地址的位置,hello.s则是直接用函数名代替 分支转移 很显然,hello.s是通过.L0等助记符表示该段完成跳转。而asm.txt跳转指令中的地址已经为已确定的实际地址。 4.5 本章小结 本章通过对汇编后产生的hello.o的可重定位的ELF格式的考察、对重定位项目的举例分析以及对反汇编文件与hello.s的对比,从原理层次了解了汇编这一过程实现的变化。
第5章 链接 5.1 链接的概念与作用 概念:链接是将多个可重定位目标文件合并以生成可执行目标文件 作用:一个大型程序可以分成很多源文件,可以构建公共函数库,可以分开编译,无需包含共享库所有代码(模块化,效率高) 5.2 在Ubuntu下链接的命令 Linux终端下进入hello.o所在文件夹,输入指令ld -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 /usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o hello.o -lc -z relro -o hello
5.3 可执行目标文件hello的格式 ELF格式
ELF头
节头部表:显示hello.out的Section Header Table中的每个Section Header Entry的信息
5.4 hello的虚拟地址空间 首先从程序头部开始看起(截图只举例部分,其余同理)
PHDR:具有读/执行权限。起始位0x400000偏移0x40字节,大小为0x1c0字节
INTERP:具有读权限。起始位0x400000偏移0x200字节,大小为0x1c
并且可以由右边的文字得到程序所用的ELF解析器位置是/lib64/……
LOAD代码段:位于0x400000处,大小为0x838字节。被初始化为可执行目标文件的头0x838字节。 LOAD数据段:有读写权限,不再位于虚拟内存中,位于0x600e10处,总大小0x250字节。其中,有4个字节对应于.bss数据,其余0x24c个字节对应.data数据, NOTE:位于0x400000偏移0x21c处,大小为0x20字节 5.5 链接的重定位过程分析 hello.o反汇编
hello.out反汇编(部分)
hello中还有其他段,如.init(程序初始化),.fini(程序结束执行),hello.o只有Disassembly of section .text。
c) 数据访问hello.o反汇编文件中,对.rodata中printf的格式串的访问需要通过链接时重定位的绝对引用确定地址,因此在汇编代码相应位置仍为占位符表示,对.data中已初始化的全局变量sleepsecs为0x0+%rip的方式访问;而hello反汇编文件中对应全局变量已通过重定位绝对引用被替换为固定地址。 5.6 hello的执行流程
加载程序 ld-2.23.so !_dl_start ld-2.23.so !_dl_init LinkAddress!_start ld-2.23.so !_libc_start_main ld-2.23.so !_cxa_atexit LinkAddress!_libc_csu.init ld-2.23.so !_setjmp 运行 LinkAddress!main 程序终止 ld-2.23.so !exit
5.7 Hello的动态链接分析 动态链接库中的函数在程序执行的时候才会确定地址,所以编译器无法确定其地址,在汇编代码中也无法像静态库的函数那样体现。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定(lazybinding),将过程地址的绑定推迟到第一次调用该过程时。 延迟绑定是通过GOT(全局偏移量表)和PLT(过程链接表)实现的。GOT是数据段的一部分,而PLT是代码段的一部分。
GOT位置 初始内容
由图GOT2 为延迟绑定入口地址
我们查看puts调用前后对应GOT的值 前
后
5.8 本章小结 本章讨论了链接过程中对程序的处理。Linux系统使用可执行可链接格式,即ELF,具有.text,.rodata等节。(第5章1分)
第6章 hello进程管理 6.1 进程的概念与作用 概念:进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 作用:“进程”概念的引入为应用程序提供了两方面的抽象:1.一个独立的逻辑控制流,以让程序员以为自己的程序在执行过程中独占处理器。2.一个私有的虚拟地址空间 6.2 简述壳Shell-bash的作用与处理流程 作用: shell作为UNIX的一个重要组成部分,是它的外壳,也是用户于UNIX系统交互作用界面。Shell是一个命令解释程序,也是一种程序设计语言。 处理流程:
读入命令行、注册相应的信号处理程序、初始化进程组。
通过paraseline函数解释命令行,如果是内置命令则直接执行,否则阻塞信号后创建相应子进程,在子进程中解除阻塞,将子进程单独设置为一个进程组,在新的进程组中执行子进程。父进程中增加作业后解除阻塞。如果是前台作业则等待其变为非前台程序,如果是后台程序则打印作业信息。
6.3 Hello的fork进程创建过程 进程的创建过程我已经在开头的O2O中较为详细的描述了,这里只讲解fork的特殊之处 fork函数有一个特点,它只被调用一次,却可以返回两次,一次是在父进程中,一次是在新创建的子进程中。父进程中返回的是子进程的PID子进程中返回0.由于子进程的PID总是非0,返回值就提供了一个明确的办法来分辨程序在哪个进程执行。 hello的fork进程创建过程
6.4 Hello的execve过程 系统为hello fork子进程之后,子进程调用execve函数,在当前进程(新创建的子进程)的上下文中加载运行hello,将hello的.text节、.data节、.bss节内容皆加载到当前进程的虚拟地址空间 6.5 Hello的进程执行 1.逻辑控制流 前面提到过多次逻辑控制流,那么它是什么。在调试器单步执行程序时,会发现一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC的值的序列叫做逻辑控制流。 2.并发流 并发与并行完全不是一个东西。一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发地执行。一个进程和其他进轮流运行的概念称为多任务。 3. 上下文 内核为每个进程维持一个上下文,上下文就是内核重新启动的一个被强占的进程所需的状态。由包括通用目的寄存器、浮点寄存器、程序计数器、用户站、状态寄存器、内核栈和各种内核数据结构。 4. 进程时间片 一个进程执行它的控制流的一部分的每一时间段叫做时间片。多任务叫做时间分片。 5. 调度 在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个过程称为调度。 6. 用户态和核心态的转换 这里可以用一张图来解释
至此,我们可以用一句话来概括Hello的执行过程。hello程序与操作系统的其他进程通过操作系统的调度,切换上下文,拥有自己的时间片,从而实现并发的运行,当需要调用系统函数或进行一些操作时,内核就需要将状态切换,执行结束时在改回用户态。 6.6 hello的异常与信号处理 hello执行过程中可能出现四类异常:中断、陷阱、故障和终止。
中断是来自I/O设备的信号,异步发生,中断处理程序对其进行处理,返回后继续执行调用前待执行的下一条代码,就像没有发生过中断。
陷阱是有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。
故障是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。
终止是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将控制返回给一个abort例程,该例程会终止这个应用程序。 键入Ctrl-z后执行ps:
显示当前进程数量和内容,以及被暂停的hello
执行jobs:
显示当前暂停的进程包括hello 执行kill
杀死进程 执行fg
恢复前台作业(hello) 执行pstree
显示进程树 CTRL+Z暂停进程CTRL+C终止进程
6.7本章小结 本章从进程的角度分别描述了hello子进程fork和execve过程,并针对execve过程中虚拟内存映像以及栈组织结构等作出说明。同时了解了逻辑控制流中内核的调度及上下文切换等机制。阐述了Shell和Bash运行的处理流程以及hello执行过程中可能引发的异常和信号处理。 (第6章1分)
第7章 hello的存储管理 7.1 hello的存储器地址空间 逻辑地址空间的格式是“段地址:偏移地址”。 在实模式下可以转换为物理地址,转换公式为:逻辑地址CS:EA=物理地址CS×16+EA 保护模式下:以段描述符作为下标,到GDT/LDT表查表获得段地址, 段地址+偏移地址=线性地址。线性地址空间是指一个非负整数地址的有序集合,例如{0,1,2,……}。在采用虚拟内存的系统中,CPU从一个有N = 2^n个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间{0,1,2,……,N-1} 而对应于物理内存中M个字节的地址空间{0, 1, 2, 3,……, M-1}则称为物理地址空间。 Intel采用段页式存储管理实现: 段式管理:逻辑地址线性地址==虚拟地址 页式管理:虚拟地址物理地址 hello中的“mov $0x400700,%r8”其中0x400700是逻辑地址编译地址,必须加上隐含的DS数据段的基地址才能构成线性空间地址。 7.2 Intel逻辑地址到线性地址的变换-段式管理 段寄存器对应着内存不同的段,有栈段寄存器(SS)、数据段寄存器(DS)、代码段寄存器(CS)和辅助段寄存器(ES/GS/FS)。 段式寄存器(16位)用于存放段选择符:CS(代码段)是指程序代码所在段;SS(栈段)是指栈区所在段;DS(数据段)是指全局静态数据所在段;其他三个段寄存器ES、GS和FS可指向任意数据段。 7.3 Hello的线性地址到物理地址的变换-页式管理 虚拟内存的概念: 虚拟内存的基本目的,就是把地址空间和主存容量的概念区分开来。程序员在地址空间里编写程序,程序在真正的内存中运行,有一个专门的机制实现地址空间和实际主存之间的映射。指令执行时,通过硬件将逻辑地址(虚拟地址)转换为物理地址。
而虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每个字节都有一个唯一的虚拟地址,作为到数组的索引。磁盘上的数据被分割成块,这些块作为磁盘和主存(较高层)之间的传输单元。虚拟页则是虚拟内存被分割为固定大小的块。物理内存被分割为物理页,大小与虚拟页大小相同。 页表:页表是一个页表条目(PTE)的数组,将虚拟页地址映射至物理页地址
地址翻译中的基本参数如下 然后地址翻译的流程如下 (以下格式自行编排,编辑时删除) 当页面命中时:
处理器生成一个虚拟地址,并将其传送给MMU
MMU 使用内存中的页表生成PTE地址,高速缓存/主存向MMU返回PTE
MMU 将物理地址传送给高速缓存/主存
高速缓存/主存返回所请求的数据字给处理器 当缺页异常时: 1) 处理器将虚拟地址发送给MMU 2) MMU 使用内存中的页表生成PTE地址 3) 有效位为零, 因此 MMU 触发缺页异常 4) 缺页处理程序确定物理内存中牺牲页(若页面被修改,则换出到磁盘) 5) 缺页处理程序调入新的页面,并更新内存中的PTE 6) 缺页处理程序返回到原来进程,再次执行导致缺页的指令
7.4 TLB与四级页表支持下的VA到PA的变换 每次CPU产生一个虚拟地址,MMU就必须查阅相应的PTE,这显然造成了巨大的时间开销,为了消除这样的开销,MMU中存在一个关于PTE的小的缓存,称为快表(TLB)。 在以上机制的基础上,如果所使用的仅仅是虚拟地址空间中很小的一部分,那么仍然需要一个与使用较多空间相同的页表,造成了内存的浪费。所以虚拟地址到物理地址的转换过程中还存在多级页表的机制:上一级的页表映射到下一级也表,直到页表映射到虚拟内存,如果下一级内容都未分配,那么页表项则为空,不映射到下一级,也不存在下一级页表,当分配时再创建相应页表,从而节约内存空间。
而使用四级页表的地址翻译,虚拟地址被划分为4个VPN和1个VPO。每个VPNi都是一个到i级页表的索引,其中1<=i<=4.第j级页表中的每个PTE,1<=j<=3,都是指向第j+1级的每个页表的基址。第4级也表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问4个PTE。 下图是i7的四级页表条目格式 (以下格式自行编排,编辑时删除)
7.5 三级Cache支持下的物理内存访问 这里针对cache的读策略和写策略进行说明: 读取数据时,首先在高速缓存中查找所需字w的副本。如果命中,立即返回字w给CPU。如果不命中,从存储器层次结构中较低层次中取出包含字w的块,将这个块存储到某个高速缓存行中(可能会驱逐一个有效的行),然后返回字w。 而有三类Cache: 直接映射高速缓存: 直接映射高速缓存每个组只有一行,当CPU执行一条读内存字w的指令,它会向L1高速缓存请求这个字。如果L1高速缓存中有w的一个缓存副本,那么就会得到L1高速缓存命中,高速缓存会很快抽取出w,并将它返回给CPU。否则就是缓存不命中,当L1高速缓存向主存请求包含w的块的一个副本时,CPU必须等待。当被请求块最终从内存到达时,L1高速缓存将这个块存放在它的一个高速缓存行里,从被存储的块中抽取出字w,然后将它返回给CPU。确定是否命中然后抽取的过程分为三步:1)组选择;2)行匹配;3)字抽取。
组选择即从w的地址中间抽取出s个索引位,将其解释为一个对应组号的无符号整数,从而找到对应的组;行匹配即对组内的唯一一行进行判断,当有效位为1且标记位与从地址中抽取出的标记位相同则成功匹配,否则就得到不命中;而字选择即在行匹配的基础上通过地址的后几位得到块偏移,从而在高速缓存块中索引到数据。 组相联高速缓存: 组相联高速缓存每个组内可以多于一个缓存行,总体逻辑类似于直接映射高速缓存,不同之处在于行匹配时每组有更多的行可以尝试匹配,遍历每一行。如果不命中,有空行时也就是冷不命中则直接存储在空行;如果没有空行也就是冲突不命中,则替换已有行,通常有LFU(最不常使用)、LRU(最近最少使用)两者替换策略。 全相联高速缓存: 全相联高速缓存只有一个组,且这个组包含所有的高速缓存行(即E = C/B)。对于全相联高速缓存,因为只有一个组,组选择变的十分简单。地址中不存在索引位,地址只被划分为一个标记位和一个块偏移。行匹配和字选择同组相联高速缓存。 写策略分两种:直写和写回 直写是立即将w的高速缓存块写回到紧接着的低一层中。虽然简单,但是只写的缺点是每次写都会引起总线流量。 写回是尽可能的推迟更新,只有当替换算法要驱逐这个更新过的块时,才把它写回到紧接着的第一次层中,由于局部性,写回能显著减少总线流量,但增加了复杂性。
7.6 hello进程fork时的内存映射 shell通过fork为hello创建新进程。当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给hello进程唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和样表的原样副本。它将两个进程中的每个页面都标记为只读,并将每个进程中的每个区域结构都标记为写时复制。当fork在新进程中返回时,新进程现在的虚拟内存刚好的和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就是为每个进程保持了私有地址空间的概念。 7.7 hello进程execve时的内存映射 execve的主要步骤:
删除已存在的用户区域
映射hello的私有区域
映射共享区域
设置程序计数器PC,,使之指向hello程序的代码入口。 经过这个内存映射的过程,在下一次调度hello进程时,就能够从hello的入口点开始执行了。 (以下格式自行编排,编辑时删除) 7.8 缺页故障与缺页中断处理
缺页异常及其处理过程:如果DRAM缓存不命中称为缺页。当地址翻译硬件从内存中读对应PTE时有效位为0则表明该页未被缓存,触发缺页异常。缺页异常调用内核中的缺页异常处理程序。缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较,如果指令不合法,缺页处理程序就触发一个段错误、终止进程。
缺页处理程序检查试图访问的内存是否合法,如果不合法则触发保护异常终止此进程。
缺页处理程序确认引起异常的是合法的虚拟地址和操作,则选择一个牺牲页,如果牺牲页中内容被修改,内核会将其先复制回磁盘。无论是否被修改,牺牲页的页表条目均会被内核修改。接下来内核从磁盘复制需要的虚拟页到DRAM中,更新对应的页表条目,重新执行导致缺页的指令。 7.9动态存储分配管理 动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为 一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已 分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。 动态内存分配器有两种,一种是隐式分配器,一种是显式的 隐式:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。而自动释放未使用的已分配的块的过程叫做垃圾收集。 显式:要求应用显式地释放任何已分配的块 带边界标签的隐式空闲链表分配器如下图
显式与隐式相近,只是分成了空闲块和分配块两种。分配块同隐式的,空闲块的有效载荷中多了两个指针(pred和succ) 放置的策略有三种:首次适配,下一次适配,最佳适配 首次适配从头开始搜索空闲链表,选择第一个合适的空闲块。下一次适配和首次适配很相似,只不过不是从链表的起始处开始每次搜索,而是从上一.次查询结束的地方开始。最佳适配检查每个空闲块,选择适合所需请求大小的最小空闲块。 7.10本章小结 本章从Linux存储器的地址空间起,阐述了Intel的段页式管理机制,以及TLB与多级页表支持下的VA到PA的转换,同时对cache支持下的物理内存访问做了说明。描述了fork和execve的内存映射,对动态分配做了系统地阐述 (第7章 2分)
第8章 hello的IO管理 8.1 Linux的IO设备管理方法 一个Linux文件就是一个m个字节的序列,所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这个设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得输入和输出都能以一种统一且一致的方式的来执行。
一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。
Linux shell创建的每个进程开始时都包含标准输入、标准输出、标准错误三个文件,供其执行过程中使用
对于每个打开的文件,内核保持着一个文件位置k,初始为0,即从文件开头起始的字节偏移量,应用程序能够通过执行seek操作来显式的改变其值。
读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
当应用完成了对文件的访问之后,它就通知内核关闭这个文件。 8.2 简述Unix IO接口及其函数 打开文件:int open(char *filename, int flags, mode_t mode);
关闭文件:int close(int fd);
读文件:ssize_t read(int fd, void *buf, size_t n);
写文件:ssize_t write(int fd, const void *buf, size_t n); 8.3 printf的实现分析 static int printf(const char fmt, …) { va_list args; int i; va_start(args, fmt); write(1,printbuf,i=vsprintf(printbuf, fmt, args)); va_end(args); return i; } (Linux下printf函数实现) va_list的定义是:typedef char * va_list,因此通过调用va_start函数,获得的arg为第一个参数的地址。 vsprintf的作用是格式化。 其中 fmt是格式化用到的字符串,而后面省略的则是可变的形参,即printf(“%d”, i)中的i,对应于字符串里面的缺省内容。
va_start的作用是取到fmt中的第一个参数的地址,下面的write来自Unix I/O,而其中的vsprintf则是用来格式化的函数。这个函数的返回值是要打印出的字符串的长度,也就是write函数中的i。该函数会将printbuf根据fmt格式化字符和相应的参数进行格式化,产生格式化的输出,从而write能够打印。
在Linux下,write函数的第一个参数为fd,也就是描述符,而1代表的就是标准输出。查看write函数的汇编实现可以发现,它首先给寄存器传递了几个参数,然后调用syscall结束。write通过执行syscall指令实现了对系统服务的调用,从而使内核执行打印操作。 在syscall函数中字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),实现printf格式化输出。 。 8.4 getchar的实现分析 int getchar(void) { char c; return (read(0,&c,1)==1)?(unsigned char)c:EOF } getchar的实现 异步异常-键盘中断的处理:键盘中断处理是底层的硬件异常,当用户按下键盘时,内核会调用异常键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar函数read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。实现读取一个字符的功能。 8.5本章小结 Linux将I/O输入都抽象成文件。提供Unix I/O接口,通过这个接口,程序能够进行输入输出,只需要知道描述符,底层硬件实现操作系统就可以实现。Linux本身提供的一些系统函数已经实现了对底层的调用,例如write函数。printf函数正是通过它间接向标准输出这个文件输出内容,它会调用syscall触发中断以内核模式对硬件进行操作。
有了I/O接口与文件这个抽象,应用程序能够很方便的调用底层,对输入与输出设备进行操作。 (第8章1分) 结论 hello的生命历程
生成阶段 预处理编译汇编链接
加载阶段 shell fork子进程execve
执行阶段 创建虚拟内存映像,MMU访存,内核通过GOT,PLT调用,动态内存分配器为调用函数分配内存,与其他进程上下文切换
终止阶段 进程终止、shell与内核对其进行回收。hello骨灰都没剩下。
hello虽然极其简单,但是它身上却凝结了着无数人的智慧。计算机的高效虽然没体现出来,但是计算机系统设计的精巧却已经让人惊讶,内核对进程的调度策略,访存的策略,虚拟存储,动态链接的执行方式,异常与信号等处理,动态内存分配器。计算机兼具高速与成本可接受这两特性皆是源于人们对这些方式的探索。让人不得不惊叹计算机系统底层实现的完美与优雅。
作为一名程序员,我们必须要了解这些,我们要做的不仅仅是能写出好的算法,掌握高级编程语言,还要做的是编写出计算机底层友好的代码,提高计算工作的效率。动态分配器中,红黑树随随便便就能比普通的方法块20多倍,这些种种告诉我们,我们要充分的了解计算机系统,即使我们并不打算以后研究这方面,我们也必须得掌握。 hello的生命结束的很快,但我们的程序员修炼之路才刚刚开始,了解hello只是第一步,未来还有很多奇妙的东西等待着我们去发掘、 (结论0分,缺少 -1分,根据内容酌情加分)
附件 1.hello.c:源代码,原程序文件
2.hello.i和hello1.i:hello.c通过预处理生成的文本文件
3.hello.s:hello.i经过编译器编译生成的文本文件
4.hello.o:hello.s进过汇编生成的二进制文件(可重定位目标文件)
5.hello.out:hello.o经过链接后生成的二进制文件(可执行目标文件)
6.asm.txt:hello.o反汇编生成的文本文件(汇编语句)
7.out_asm.txt:由hello.out反汇编生成的文本文件(汇编语句) (附件0分,缺失 -1分)
参考文献 为完成本次大作业你翻阅的书籍与网站等 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. (参考文献0分,确实 -1分)
欢迎使用Markdown编辑器
你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。
新的改变
我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:
全新的界面设计 ,将会带来全新的写作体验;
在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
全新的 KaTeX数学公式 语法;
增加了支持甘特图的mermaid语法1 功能;
增加了 多屏幕编辑 Markdown文章功能;
增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
增加了 检查列表 功能。
功能快捷键
撤销:Ctrl/Command + Z 重做:Ctrl/Command + Y 加粗:Ctrl/Command + B 斜体:Ctrl/Command + I 标题:Ctrl/Command + Shift + H 无序列表:Ctrl/Command + Shift + U 有序列表:Ctrl/Command + Shift + O 检查列表:Ctrl/Command + Shift + C 插入代码:Ctrl/Command + Shift + K 插入链接:Ctrl/Command + Shift + L 插入图片:Ctrl/Command + Shift + G
合理的创建标题,有助于目录的生成
直接输入1次# ,并按下space 后,将生成1级标题。 输入2次# ,并按下space 后,将生成2级标题。 以此类推,我们支持6级标题。有助于使用TOC
语法后生成一个完美的目录。
如何改变文本的样式
强调文本 强调文本
加粗文本 加粗文本
标记文本
删除文本
引用文本
H2 O is是液体。
210 运算结果是 1024.
插入链接与图片
链接: link .
图片:
带尺寸的图片:
居中的图片:
居中并且带尺寸的图片:
当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。
如何插入一段漂亮的代码片
去博客设置 页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片
.
// An highlighted block
var foo = 'bar' ;
生成一个适合你的列表
项目1
项目2
项目3
创建一个表格
一个简单的表格是这么创建的:
项目
Value
电脑
$1600
手机
$12
导管
$1
设定内容居中、居左、居右
使用:---------:
居中 使用:----------
居左 使用----------:
居右
第一列
第二列
第三列
第一列文本居中
第二列文本居右
第三列文本居左
SmartyPants
SmartyPants将ASCII标点字符转换为“智能”印刷标点HTML实体。例如:
TYPE
ASCII
HTML
Single backticks
'Isn't this fun?'
‘Isn’t this fun?’
Quotes
"Isn't this fun?"
“Isn’t this fun?”
Dashes
-- is en-dash, --- is em-dash
– is en-dash, — is em-dash
创建一个自定义列表
Markdown
Text-to-
HTML conversion tool
Authors
John
Luke
如何创建一个注脚
一个具有注脚的文本。2
注释也是必不可少的
Markdown将文本转换为 HTML 。
KaTeX数学公式
您可以使用渲染LaTeX数学表达式 KaTeX :
Gamma公式展示
Γ
(
n
)
=
(
n
−
1
)
!
∀
n
∈
N
\Gamma(n) = (n-1)!\quad\forall n\in\mathbb N
Γ ( n ) = ( n − 1 ) ! ∀ n ∈ N 是通过欧拉积分
Γ
(
z
)
=
∫
0
∞
t
z
−
1
e
−
t
d
t
 
.
\Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,.
Γ ( z ) = ∫ 0 ∞ t z − 1 e − t d t .
你可以找到更多关于的信息 LaTeX 数学表达式here .
新的甘特图功能,丰富你的文章
Mon 06
Mon 13
Mon 20
已完成
进行中
计划一
计划二
现有任务
Adding GANTT diagram functionality to mermaid
UML 图表
可以使用UML图表进行渲染。 Mermaid . 例如下面产生的一个序列图::
张三
李四
王五
你好!李四, 最近怎么样?
你最近怎么样,王五?
我很好,谢谢!
我很好,谢谢!
李四想了很长时间,
文字太长了
不适合放在一行.
打量着王五...
很好... 王五, 你怎么样?
张三
李四
王五
这将产生一个流程图。:
FLowchart流程图
我们依旧会支持flowchart的流程图:
Created with Raphaël 2.2.0
开始
我的操作
确认?
结束
yes
no
关于 Flowchart流程图 语法,参考 这儿 .
导出与导入
导出
如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。
导入
如果你想加载一篇你写过的.md文件或者.html文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入, 继续你的创作。