通过hello程序在Linux系统下走完它平凡却又伟大的一生,探讨hello源程序的预处理、编译、汇编、链接、生成可执行文件并运行的主要过程。同时说明系统是如何实现对hello程序进行进程管理,存储管理和I/O管理。
关键词:预处理;编译;汇编;链接;进程;存储;I/0;
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
1.最初通过编辑器编写hello的程序建立.c文件,得到hello.c的源程序。
2.运行C预处理器(cpp)将其进行预处理生成hello.i文件。
3.运行C编译器(ccl)将其进行翻译生成汇编语言文件hello.s。
4.运行汇编器(as)将其翻译成一个可重定位目标文件hello.o。
5.运行链接器程序ld将hello.o和系统目标文件组合起来,创建了一个可执行目标文件hello
6.通过shell输入./shell,shell通过fork函数创建了一个新的进程,之后调用execve映射虚拟内存,通过mmap为hello程序开创了一片空间。
7.CPU从虚拟内存中的.text,.data节取代码和数据,调度器为进程规划时间片,有异常时触发异常处理子程序。
8.程序运行结束时,父进程回收hello进程和它创建的子进程,内核删除相关数据结构。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1.硬件环境:X64 CPU;2GHz;4GRAM;256Disk
2.软件环境:Windows10 64位;Vmware 10;Ubuntu 20.04 LTS 64位
3.工具:codeblocks;gdb;Objdump;HexEditor
1.3 中间结果
文件名称 功能
hello.c 源程序
hello.i 预处理后文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位目标执行文件
hello 链接后的可执行文件
hello.elf hello.o的ELF格式
hello1.txt hello.o的反汇编
hello2.txt hello的反汇编代码
hello1.elf hello的ELF格式
1.4 本章小结
本章总体介绍了hello程序“一生”的过程,以及进行实验时的软硬件环境及开发与调试工具等基本信息。
第2章 预处理
2.1 预处理的概念与作用
1.预处理概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。
2.预处理作用:根据源代码中的预处理指令修改源代码,预处理从系统的头文件包中将头文件的源码插入到目标文件中,宏和常量标识符已全部被相应的代码和值替换,最终生成.i文件。
2.2在Ubuntu下预处理的命令
Linux中hello.c文件进行预处理的命令是:gcc -E -o hello.i hello.c
2.3 Hello的预处理结果解析
经过预处理之后,hello.c变为hello.i文件,打开该文件可以发现,文件变为3000多行内容大大增加,且仍为可以阅读的C语言程序文本文件。对原程序中的宏进行了宏展开,头文件中的内容被包含进该文件中。例如声明函数、定义结构体、定义变量、定义宏等内容,如果代码中有#define命令还会对相应的符号进行替换。
2.4 本章小结
本章介绍了预处理的相关概念和作用,进行实际操作查看了hello.i文件,是对源程序进行补充和替换。
第3章 编译
3.1 编译的概念与作用
1.编译的概念:编辑器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。
2.编译的作用:把源程序翻译成目标程序,进行词法分析和语法分析,分析过程中发现有语法错误,给出提示信息。
3.2 在Ubuntu下编译的命令
编译的命令:gcc -S hello.i -o hello.s
(以下格式自行编排,编辑时删除)
应截图,展示编译过程!
3.3 Hello的编译结果解析
3.3.1汇编初始部分
节名称 作用
.file 声明源文件
.text 代码节
.section.rodata 只读数据段
.globl 声明全局变量
.type 声明一个符号是函数类型还是数据类型
.size 声明大小
.string 声明一个字符串
.align 声明对指令或者数据的存放地址进行对齐的方式
3.3.2数据
字符串 “hello world”
局部变量 21行将eax寄存器的值返回到内存
参数 argc
数组 argv
立即数 直接体现在代码中
3.3.3全局函数
第7行声明全局函数main
3.3.4函数操作
调用函数时有以下操作:(假设函数P调用函数Q)
①传递控制:调用过程Q的时候,程序计数器(%rip)必须设置为函数Q代码的起始地址,然后在返回时,要把程序计数器(%rip)设置为P中调用Q后面那条指令的地址。
②传递数据:函数P必须能够向函数Q提供一个或多个参数Q必须能够向P中返回一个值。
③分配和释放内存:在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。
hello.c中的函数操作:
main函数:参数是int argc,char *argv[]
printf函数:参数是argv[1],argv[2]
exit函数:参数是1
sleep函数:参数是atoi(argv[3])
getchar函数:无参数
3.3.5关系操作
hello.c中argc!=3;是条件判断语句,进行编译时,这条指令被编译为:cmpl $3,-20(%rbp),在比较之后还设置了条件码,根据条件码判断是否需要跳转。
3.4 本章小结
本章主要介绍了编译器处理c语言程序的基本过程,函数从源代码变为等价的汇编代码,编译器分别从c语言的数据,赋值语句,类型转换,算术操作,逻辑/位操作,关系操作,控制转移与函数操作这几点进行分析,通过理解了这些编译器编译的机制,我们可以很容易的将汇编语言翻译成c语言,提高了反向工程的能力。
第4章 汇编
4.1 汇编的概念与作用
1.汇编的概念
汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,将结果保存在目标文件hello.o中,hello.o文件是一个二进制文件,它包含函数main的指令编码,这个过程称为汇编。
2.汇编的作用
将汇编代码转换为机器指令,使其在链接后能被机器识别并执行.
4.2 在Ubuntu下汇编的命令
命令为:gcc -c -o hello.o hello.s
(以下格式自行编排,编辑时删除)
应截图,展示汇编过程!
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
在linux下生成hello.o文件elf格式的命令:readelf -a hello.o > hello.elf
①ELF头:ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
②节头:记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。
③重定位节:
.rela.text,保存的是.text节中需要被修正的信息;任何调用外部函数或者引用全局变量的指令都需要被修正;调用外部函数的指令需要重定位;引用全局变量的指令需要重定位; 调用局部函数的指令不需要重定位;在可执行目标文件中不存在重定位信息。本程序需要被重定位的是printf、puts、exit、sleepsecs、getchar、sleep和.rodata中的.L0和.L1。
.rela.eh_frame节是.eh_frame节重定位信息。
④符号表:.symtab,一个符号表,它存放在程序中定义和引用的函数和全局变量的信息,一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。
4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
命令:objdump -d -r hello.o > hello1.txt
4.5 本章小结
本章对hello.s进行了汇编,生成了hello.o可重定位目标文件,并且分析了可重定位文件的ELF头、节头部表、符号表和可重定位节,比较了hello.s和hello.o反汇编代码的不同之处,分析了汇编语言与机器语言的对应关系。
第5章 链接
5.1 链接的概念与作用
1.链接的概念:
链接是将各种代码和数据片段收集并合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。
2.链接的作用:
链接器在软件开发过程中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。
(以下格式自行编排,编辑时删除)
注意:这儿的链接是指从 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
(以下格式自行编排,编辑时删除)
出现权限不够问题,用chmod修改无用
使用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/9/crtbegin.o hello.o -lc
/usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o
hello
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
命令:readelf -a hello > hello1.elf
1.ELF头:hello的文件头和hello.o文件头的不同之处如下图标记所示,hello是一个可执行目标文件,有27个节
2.节头:对 hello中所有的节信息进行了声明,包括大小和偏移量
3.重定位节.rela.text:
4.符号表.symtab:
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
分析程序头LOAD可加载的程序段的地址为0x400000
使用edb打开hello从Data Dump窗口观察hello加载到虚拟地址的状况,并查看各段信息。
在0x400000~0x401000段中,程序被载入,虚拟地址0x400000开始,到0x400fff结束,根据5.3中的节头部表,可以通过edb找到各个节的信息,比如.txt节,虚拟地址开始于0x4010f0,大小为0x145.
5.5 链接的重定位过程分析
执行objdump -d -r hello>hello2.txt
与hello.o的反汇编文件对比发现,hello2.txt中多了许多节。hello1.txt中只有一个.text节,而且只有一个main函数,函数地址也是默认的0x000000.hello2.txt中有.init,.plt,.text三个节,而且每个节中有很多函数。库函数的代码都已经链接到了程序中,程序各个节变的更加完整,跳转的地址也具有参考性。
hello比hello.o多出的节头表:
.interp 保存ld.so的路径
.note.ABI.tag Linux下特有的section
.note.gnu.build-i 编译信息表
.gnu.hash gnu的扩展符号hash表
.dynsym 动态符号表
.dynstr 动态符号表中的符号名称
.gnu.version 符号版本
.gnu.version_r 符号引用版本
.rela.dyn 动态重定位表
.rela.plt .plt节的重定位条目
.init 程序初始化
.plt 动态链接表
.fini 程序终止时需要的执行的指令
.eh_frame 程序执行错误时的指令
.dynamic 存放被ld.so使用的动态链接信息
.got 存放程序中变量全局偏移量
.got.plt 存放程序中函数的全局偏移量
.data 初始化过的全局变量或者声明过的函数
文件截图:
hello重定位的过程:
(1)重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
(2)重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。
(3)重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目,代码的重定位条目放在.rel.txt中
(4)重定位过程的地址计算算法如图所示:
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
①开始执行:_start、_libc_start_main
②执行main:_main、_printf、_exit、_sleep、_getchar
③退出:exit
程序名 程序地址
_start 0x4010f0
_libc_start_main 0x2f12271d
main 0x401125
_printf 0x401040
_exit 0x401070
_sleep 0x401080
_getchar 0x401050
5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。
延迟绑定是通过GOT和PLT实现的,根据hello ELF文件可知,GOT起始表位置为0x404000如图:
8 本章小结
本章主要介绍了链接的概念与作用,链接可分为符号定义和重定位,了解了可执行文件的ELF格式,分析了hello的虚拟地址空间,重定位过程,执行过程,动态连接过程,对链接有了更深的理解。
第6章 hello进程管理
6.1 进程的概念与作用
经典定义就是一个执行中程序的实例。广义定义是进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
2.进程的作用:
进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
6.3 Hello的fork进程创建过程
在终端中输入命令行./hello 1190201016 石衍 1后,首先shell对我们输入的命令进行解析,由于我们输入的命令不是一个内置的shell命令,因此shell会调用fork()创建一个子进程,子进程几乎但不完全与父进程相同。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的但是独立的一份副本,拥有不同的PID。
6.4 Hello的execve过程
(以下格式自行编排,编辑时删除)
当调用fork()函数创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,需要以下步骤:
①删除已存在的用户区域。删除之前进程在用户部分中已存在的结构。
②创建新的代码、数据、堆和栈段。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
③映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
④设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
(以下格式自行编排,编辑时删除)
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
hello程序的执行是依赖于进程所提供的抽象的基础上,进程提供给应用程序的抽象有:
1.一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器
2. 一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用CPU内存。
操作系统所提供的进程抽象:
①逻辑控制流:如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称为逻辑流。
②上下文切换:如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程,上下文就是内核重新启动一个被抢占的进程所需要的状态,是一种比较高层次的异常控制流。
③时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
④用户模式和内核模式:shell使得用户可以有机会修改内核,所以需要设置一些防护措施来保护内核,如限制指令的类型和可以作用的范围。
⑤上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。
hello进程的执行:在进程调用execve函数之后,进程已经为hello程序分配了新的虚拟的地址空间,最初hello运行在用户模式下,输出hello 1190201016 石衍,然后调用sleep函数进程进入内核模式,运行信号处理程序,之后再返回用户模式。运行过程中,cpu不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用cpu,实现进程的调度。
6.6 hello的异常与信号处理
1.异常和信号异常种类
类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令或终止
终止 不可恢复的错误 同步 不会返回
6.7本章小结
本章阐述进程的定义与作用,同时介绍了 Shell 的一般处理流程和作用,并且着重分析了调用 fork 创建新进程,调用 execve函数 执行 hello,hello的进程执行,以及hello 的异常与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
1.逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 段标识符:段内偏移量。
2.线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式,分页机制中线性地址作为输入。
3.虚拟地址:就是线性地址。
4.物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由段标识符与段内偏移量组成。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。
索引号就是“段描述符(segment descriptor)”的索引,段描述符具体地址描述了一个段,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,就可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符。
Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。什么时候该用GDT,什么时候该用LDT是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT,GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址到物理地址的转换是通过分页完成的。首先CPU通过生成一个虚拟地址来访问主存,这个虚拟地址被送到内存之前首先转换为物理地址,CPU上的内存管理单元(MMU)利用主存中的查询表来动态翻译虚拟地址。
虚拟地址转换为物理地址时需要检查页表中虚拟页是否缓存在物理内存中,若不缓存在物理内存,则查询该虚拟页在磁盘的位置。页表就是页表条目的数组,页表条目由有效位和地址字段组成。这样就建立了页表到虚拟内存或物理内存的映射。
7.4 TLB与四级页表支持下的VA到PA的变换
Core i7采用四级页表的层次结构。CPU产生VA,VA传送给MMU,MMU使用VPN高位作为TLBT和TLBI向TLB中寻找匹配。如果命中,则得到PA。如果TLB中没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成PA,添加到PLT。
7.5 三级Cache支持下的物理内存访问
1. 先选择虚拟地址的组索引位,找到对应的组。再使用行匹配将虚拟地址的标记和组中各行的标记位比较。命中的条件是高速缓存行有效且标记位匹配。
2. 高速缓存命中后,使用块偏移找到需要字节的块内偏移位置,将其取出返回给CPU。
3. 若产生了不命中的现象,那么需要从L2中寻找所请求的块,若找到就将其存储在cache的一行中。一般使用最近最少使用的替换策略进行替换。若没找到还需要到L3,主存中重复以上过程。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,同时为这个新进程创建虚拟内存。
它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。因此,也就为每个进程保持了私有空间地址的抽象概念。
————————————————
7.7 hello进程execve时的内存映射
1)在bash中的进程中执行了如下的execve调用:execve("hello",NULL,NULL);
2)execve函数在当前进程中加载并运行包含在可执行文件hello中的程序,用hello替代了当前bash中的程序。
下面是加载并运行hello的几个步骤:
3)删除已存在的用户区域。
4)映射私有区域
5)映射共享区域
6)设置程序计数器(PC)
exceve做的最后一件事是设置当前进程的上下文中的程序计数器,是指指向代码区域的入口点。而下一次调度这个进程时,他将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
假设MMU在试图翻译某个虚拟地址A时,触发了一个缺页。这个异常导致控制转移到内核的缺页处理程序,处理程序随后就执行下面的步骤:
1)虚拟地址A是合法的吗?即A在某个区域结构定义的区域内吗?为了回答这个问题,缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止这个进程。这个情况在下图中标识为“1”。
因为一个进程可以创建任意数量的新虚拟内存区域,所以顺序搜索区域结构的链表花销可能会很大。因此在实际中,Linux 使用某些我们没有显示出来的字段,Linux 在链表中构建了一棵树,并在这棵树上进行查找。
2)试图进行的内存访问是否合法?换句话说,进程是否有读、写或者执行这个区域内页面的权限?例如,这个缺页是不是由一条试图对这个代码段里的只读页面进行写操作的存储指令造成的?这个缺页是不是因为一个运行在用户模式中的进程试图从内核虚拟内存中读取字造成的?如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。这种情况在下图中标识为“2”。
3)此刻,内核知道了这个缺页是由于对合法的虚拟地址进行合法的操作造成的。它是这样来处理这个缺页的:选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU。这次,MMU就能正常地翻译A,而不会再产生缺页中断了。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。对于每个进程,内核维护一个变量brk,它指向堆的顶部。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。例如malloc。
隐式分配器:也叫做垃圾收集器,要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
7.10本章小结
本小节讨论了存储空间的管理,首先解释了四种地址的概念,逻辑地址、线性地址、虚拟地址、物理地址,接着描述了逻辑地址到线性地址再到物理地址的转换过程。再用TLB和四级页表的实例描述了va到pa的转变过程,说明了如何使用物理内存访问cache。在将进程使用到的fork和execve的内存映射情况加以描述。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix IO接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux Shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
4.读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件,作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放他们的内存资源。
Unix I/O函数:
①int open(char* filename,int flags,mode_t mode)
进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
②int close(fd)
进程通过调用close函数关闭一个打开的文件,fd是需要关闭的文件的描述符。
③ssize_t read(int fd,void *buf,size_t n)
read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
④ssize_t wirte(int fd,const void *buf,size_t n)
write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的IO设备管理方法,Unix IO接口及其函数,分析了printf函数和getchar函数的实现。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
1.输入:将hello.c代码从键盘输入。
2.预处理(cpp):将hello.c进行预处理,将c文件调用的所有外部的库展开合并,
生成hello.i文件。
3.编译(ccl):将hello.i文件进行翻译生成汇编语言文件hello.s。
4.汇编(as):将hello.s翻译成一个可重定位目标文件hello.o。
5.链接(ld):将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程
序hello,至此可执行hello程序正式诞生。
6.运行:在shell中输入./hello 1190201016 石衍 1
7.创建子进程:由于终端输入的不是一个内置的shell命令,因此shell调用fork ()
函数创建一个子进程。
8.加载程序:shell调用execve函数,启动加载器,映射虚拟内存,进入程序入口
后程序开始载入物理内存,然后进入main函数。
9.执行指令:CPU为进程分配时间片,在一个时间片中,hello享有CPU资源, 顺
序执行自己的控制逻辑流。
10.访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
11.动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。
12.信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进
程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
13终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。
对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法:
抽象对计算机系统是很重要的概念,底层信息用二进制来抽象表示,进程是对处
理器、主存和I/O设备的抽象,虚拟内存是对主存和磁盘设备的抽象,文件是对I/O
设备的抽象,等等。另外,存储器的“过渡”策略也十分精妙,由于CPU的处理
速度比主存快得多,为了减少“供不应求”的现象,在CPU和主存之间增加了一
级,二级,三级cache大大提高了CPU访问主存的速度。
附件
列出所有的中间产物的文件名,并予以说明起作用。
附件
文件名称 功能
hello.c 源程序
hello.i 预处理后文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位目标执行文件
hello 链接后的可执行文件
hello.elf hello.o的ELF格式
hello1.txt hello.o的反汇编
hello2.txt hello的反汇编代码
hello1.elf hello的ELF格式
参考文献
[1] 深入理解计算机系统原书第3版-文字版.pdf
[2] https://www.cnblogs.com/diaohaiwei/p/5094959.html
[3] https://www.cnblogs.com/pianist/p/3315801.html
[4] https://blog.csdn.net/drshenlei/article/details/4261909
摘 要
通过hello程序在Linux系统下走完它平凡却又伟大的一生,探讨hello源程序的预处理、编译、汇编、链接、生成可执行文件并运行的主要过程。同时说明系统是如何实现对hello程序进行进程管理,存储管理和I/O管理。
关键词:预处理;编译;汇编;链接;进程;存储;I/0;
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
1.最初通过编辑器编写hello的程序建立.c文件,得到hello.c的源程序。
2.运行C预处理器(cpp)将其进行预处理生成hello.i文件。
3.运行C编译器(ccl)将其进行翻译生成汇编语言文件hello.s。
4.运行汇编器(as)将其翻译成一个可重定位目标文件hello.o。
5.运行链接器程序ld将hello.o和系统目标文件组合起来,创建了一个可执行目标文件hello
6.通过shell输入./shell,shell通过fork函数创建了一个新的进程,之后调用execve映射虚拟内存,通过mmap为hello程序开创了一片空间。
7.CPU从虚拟内存中的.text,.data节取代码和数据,调度器为进程规划时间片,有异常时触发异常处理子程序。
8.程序运行结束时,父进程回收hello进程和它创建的子进程,内核删除相关数据结构。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1.硬件环境:X64 CPU;2GHz;4GRAM;256Disk
2.软件环境:Windows10 64位;Vmware 10;Ubuntu 20.04 LTS 64位
3.工具:codeblocks;gdb;Objdump;HexEditor
1.3 中间结果
文件名称 功能
hello.c 源程序
hello.i 预处理后文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位目标执行文件
hello 链接后的可执行文件
hello.elf hello.o的ELF格式
hello1.txt hello.o的反汇编
hello2.txt hello的反汇编代码
hello1.elf hello的ELF格式
1.4 本章小结
本章总体介绍了hello程序“一生”的过程,以及进行实验时的软硬件环境及开发与调试工具等基本信息。
第2章 预处理
2.1 预处理的概念与作用
1.预处理概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。
2.预处理作用:根据源代码中的预处理指令修改源代码,预处理从系统的头文件包中将头文件的源码插入到目标文件中,宏和常量标识符已全部被相应的代码和值替换,最终生成.i文件。
2.2在Ubuntu下预处理的命令
Linux中hello.c文件进行预处理的命令是:gcc -E -o hello.i hello.c
2.3 Hello的预处理结果解析
经过预处理之后,hello.c变为hello.i文件,打开该文件可以发现,文件变为3000多行内容大大增加,且仍为可以阅读的C语言程序文本文件。对原程序中的宏进行了宏展开,头文件中的内容被包含进该文件中。例如声明函数、定义结构体、定义变量、定义宏等内容,如果代码中有#define命令还会对相应的符号进行替换。
2.4 本章小结
本章介绍了预处理的相关概念和作用,进行实际操作查看了hello.i文件,是对源程序进行补充和替换。
第3章 编译
3.1 编译的概念与作用
1.编译的概念:编辑器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。
2.编译的作用:把源程序翻译成目标程序,进行词法分析和语法分析,分析过程中发现有语法错误,给出提示信息。
3.2 在Ubuntu下编译的命令
编译的命令:gcc -S hello.i -o hello.s
应截图,展示编译过程!
3.3 Hello的编译结果解析
3.3.1汇编初始部分
节名称 作用
.file 声明源文件
.text 代码节
.section.rodata 只读数据段
.globl 声明全局变量
.type 声明一个符号是函数类型还是数据类型
.size 声明大小
.string 声明一个字符串
.align 声明对指令或者数据的存放地址进行对齐的方式
3.3.2数据
字符串 “hello world”
局部变量 21行将eax寄存器的值返回到内存
参数 argc
数组 argv
立即数 直接体现在代码中
3.3.3全局函数
第7行声明全局函数main
3.3.4函数操作
调用函数时有以下操作:(假设函数P调用函数Q)
①传递控制:调用过程Q的时候,程序计数器(%rip)必须设置为函数Q代码的起始地址,然后在返回时,要把程序计数器(%rip)设置为P中调用Q后面那条指令的地址。
②传递数据:函数P必须能够向函数Q提供一个或多个参数Q必须能够向P中返回一个值。
③分配和释放内存:在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。
hello.c中的函数操作:
main函数:参数是int argc,char *argv[]
printf函数:参数是argv[1],argv[2]
exit函数:参数是1
sleep函数:参数是atoi(argv[3])
getchar函数:无参数
3.3.5关系操作
hello.c中argc!=3;是条件判断语句,进行编译时,这条指令被编译为:cmpl $3,-20(%rbp),在比较之后还设置了条件码,根据条件码判断是否需要跳转。
3.4 本章小结
本章主要介绍了编译器处理c语言程序的基本过程,函数从源代码变为等价的汇编代码,编译器分别从c语言的数据,赋值语句,类型转换,算术操作,逻辑/位操作,关系操作,控制转移与函数操作这几点进行分析,通过理解了这些编译器编译的机制,我们可以很容易的将汇编语言翻译成c语言,提高了反向工程的能力。
第4章 汇编
4.1 汇编的概念与作用
1.汇编的概念
汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,将结果保存在目标文件hello.o中,hello.o文件是一个二进制文件,它包含函数main的指令编码,这个过程称为汇编。
2.汇编的作用
将汇编代码转换为机器指令,使其在链接后能被机器识别并执行.
4.2 在Ubuntu下汇编的命令
命令为:gcc -c -o hello.o hello.s
(以下格式自行编排,编辑时删除)
应截图,展示汇编过程!
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
在linux下生成hello.o文件elf格式的命令:readelf -a hello.o > hello.elf
①ELF头:ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
②节头:记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。
③重定位节:
.rela.text,保存的是.text节中需要被修正的信息;任何调用外部函数或者引用全局变量的指令都需要被修正;调用外部函数的指令需要重定位;引用全局变量的指令需要重定位; 调用局部函数的指令不需要重定位;在可执行目标文件中不存在重定位信息。本程序需要被重定位的是printf、puts、exit、sleepsecs、getchar、sleep和.rodata中的.L0和.L1。
.rela.eh_frame节是.eh_frame节重定位信息。
④符号表:.symtab,一个符号表,它存放在程序中定义和引用的函数和全局变量的信息,一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。
4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
命令:objdump -d -r hello.o > hello1.txt
4.5 本章小结
本章对hello.s进行了汇编,生成了hello.o可重定位目标文件,并且分析了可重定位文件的ELF头、节头部表、符号表和可重定位节,比较了hello.s和hello.o反汇编代码的不同之处,分析了汇编语言与机器语言的对应关系。
第5章 链接
5.1 链接的概念与作用
1.链接的概念:
链接是将各种代码和数据片段收集并合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。
2.链接的作用:
链接器在软件开发过程中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。
(以下格式自行编排,编辑时删除)
注意:这儿的链接是指从 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
(以下格式自行编排,编辑时删除)
出现权限不够问题,用chmod修改无用
使用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/9/crtbegin.o hello.o -lc
/usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o
hello
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
命令:readelf -a hello > hello1.elf
1.ELF头:hello的文件头和hello.o文件头的不同之处如下图标记所示,hello是一个可执行目标文件,有27个节
2.节头:对 hello中所有的节信息进行了声明,包括大小和偏移量
3.重定位节.rela.text:
4.符号表.symtab:
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
分析程序头LOAD可加载的程序段的地址为0x400000
使用edb打开hello从Data Dump窗口观察hello加载到虚拟地址的状况,并查看各段信息。
在0x400000~0x401000段中,程序被载入,虚拟地址0x400000开始,到0x400fff结束,根据5.3中的节头部表,可以通过edb找到各个节的信息,比如.txt节,虚拟地址开始于0x4010f0,大小为0x145.
5.5 链接的重定位过程分析
执行objdump -d -r hello>hello2.txt
与hello.o的反汇编文件对比发现,hello2.txt中多了许多节。hello1.txt中只有一个.text节,而且只有一个main函数,函数地址也是默认的0x000000.hello2.txt中有.init,.plt,.text三个节,而且每个节中有很多函数。库函数的代码都已经链接到了程序中,程序各个节变的更加完整,跳转的地址也具有参考性。
hello比hello.o多出的节头表:
.interp 保存ld.so的路径
.note.ABI.tag Linux下特有的section
.note.gnu.build-i 编译信息表
.gnu.hash gnu的扩展符号hash表
.dynsym 动态符号表
.dynstr 动态符号表中的符号名称
.gnu.version 符号版本
.gnu.version_r 符号引用版本
.rela.dyn 动态重定位表
.rela.plt .plt节的重定位条目
.init 程序初始化
.plt 动态链接表
.fini 程序终止时需要的执行的指令
.eh_frame 程序执行错误时的指令
.dynamic 存放被ld.so使用的动态链接信息
.got 存放程序中变量全局偏移量
.got.plt 存放程序中函数的全局偏移量
.data 初始化过的全局变量或者声明过的函数
文件截图:
hello重定位的过程:
(1)重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
(2)重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。
(3)重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目,代码的重定位条目放在.rel.txt中
(4)重定位过程的地址计算算法如图所示:
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
①开始执行:_start、_libc_start_main
②执行main:_main、_printf、_exit、_sleep、_getchar
③退出:exit
程序名 程序地址
_start 0x4010f0
_libc_start_main 0x2f12271d
main 0x401125
_printf 0x401040
_exit 0x401070
_sleep 0x401080
_getchar 0x401050
5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。
延迟绑定是通过GOT和PLT实现的,根据hello ELF文件可知,GOT起始表位置为0x404000如图:
8 本章小结
本章主要介绍了链接的概念与作用,链接可分为符号定义和重定位,了解了可执行文件的ELF格式,分析了hello的虚拟地址空间,重定位过程,执行过程,动态连接过程,对链接有了更深的理解。
第6章 hello进程管理
6.1 进程的概念与作用
1.进程的概念:
经典定义就是一个执行中程序的实例。广义定义是进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
2.进程的作用:
进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
6.3 Hello的fork进程创建过程
在终端中输入命令行./hello 1190201016 石衍 1后,首先shell对我们输入的命令进行解析,由于我们输入的命令不是一个内置的shell命令,因此shell会调用fork()创建一个子进程,子进程几乎但不完全与父进程相同。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的但是独立的一份副本,拥有不同的PID。
6.4 Hello的execve过程
(以下格式自行编排,编辑时删除)
当调用fork()函数创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,需要以下步骤:
①删除已存在的用户区域。删除之前进程在用户部分中已存在的结构。
②创建新的代码、数据、堆和栈段。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
③映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
④设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
(以下格式自行编排,编辑时删除)
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
hello程序的执行是依赖于进程所提供的抽象的基础上,进程提供给应用程序的抽象有:
1.一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器
2. 一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用CPU内存。
操作系统所提供的进程抽象:
①逻辑控制流:如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称为逻辑流。
②上下文切换:如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程,上下文就是内核重新启动一个被抢占的进程所需要的状态,是一种比较高层次的异常控制流。
③时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
④用户模式和内核模式:shell使得用户可以有机会修改内核,所以需要设置一些防护措施来保护内核,如限制指令的类型和可以作用的范围。
⑤上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。
hello进程的执行:在进程调用execve函数之后,进程已经为hello程序分配了新的虚拟的地址空间,最初hello运行在用户模式下,输出hello 1190201016 石衍,然后调用sleep函数进程进入内核模式,运行信号处理程序,之后再返回用户模式。运行过程中,cpu不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用cpu,实现进程的调度。
6.6 hello的异常与信号处理
1.异常和信号异常种类
类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令或终止
终止 不可恢复的错误 同步 不会返回
6.7本章小结
本章阐述进程的定义与作用,同时介绍了 Shell 的一般处理流程和作用,并且着重分析了调用 fork 创建新进程,调用 execve函数 执行 hello,hello的进程执行,以及hello 的异常与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
1.逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 段标识符:段内偏移量。
2.线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式,分页机制中线性地址作为输入。
3.虚拟地址:就是线性地址。
4.物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由段标识符与段内偏移量组成。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。
索引号就是“段描述符(segment descriptor)”的索引,段描述符具体地址描述了一个段,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,就可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符。
Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。什么时候该用GDT,什么时候该用LDT是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT,GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址到物理地址的转换是通过分页完成的。首先CPU通过生成一个虚拟地址来访问主存,这个虚拟地址被送到内存之前首先转换为物理地址,CPU上的内存管理单元(MMU)利用主存中的查询表来动态翻译虚拟地址。
虚拟地址转换为物理地址时需要检查页表中虚拟页是否缓存在物理内存中,若不缓存在物理内存,则查询该虚拟页在磁盘的位置。页表就是页表条目的数组,页表条目由有效位和地址字段组成。这样就建立了页表到虚拟内存或物理内存的映射。
7.4 TLB与四级页表支持下的VA到PA的变换
Core i7采用四级页表的层次结构。CPU产生VA,VA传送给MMU,MMU使用VPN高位作为TLBT和TLBI向TLB中寻找匹配。如果命中,则得到PA。如果TLB中没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成PA,添加到PLT。
7.5 三级Cache支持下的物理内存访问
1. 先选择虚拟地址的组索引位,找到对应的组。再使用行匹配将虚拟地址的标记和组中各行的标记位比较。命中的条件是高速缓存行有效且标记位匹配。
2. 高速缓存命中后,使用块偏移找到需要字节的块内偏移位置,将其取出返回给CPU。
3. 若产生了不命中的现象,那么需要从L2中寻找所请求的块,若找到就将其存储在cache的一行中。一般使用最近最少使用的替换策略进行替换。若没找到还需要到L3,主存中重复以上过程。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,同时为这个新进程创建虚拟内存。
它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。因此,也就为每个进程保持了私有空间地址的抽象概念。
————————————————
7.7 hello进程execve时的内存映射
1)在bash中的进程中执行了如下的execve调用:execve("hello",NULL,NULL);
2)execve函数在当前进程中加载并运行包含在可执行文件hello中的程序,用hello替代了当前bash中的程序。
下面是加载并运行hello的几个步骤:
3)删除已存在的用户区域。
4)映射私有区域
5)映射共享区域
6)设置程序计数器(PC)
exceve做的最后一件事是设置当前进程的上下文中的程序计数器,是指指向代码区域的入口点。而下一次调度这个进程时,他将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
假设MMU在试图翻译某个虚拟地址A时,触发了一个缺页。这个异常导致控制转移到内核的缺页处理程序,处理程序随后就执行下面的步骤:
1)虚拟地址A是合法的吗?即A在某个区域结构定义的区域内吗?为了回答这个问题,缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止这个进程。这个情况在下图中标识为“1”。
因为一个进程可以创建任意数量的新虚拟内存区域,所以顺序搜索区域结构的链表花销可能会很大。因此在实际中,Linux 使用某些我们没有显示出来的字段,Linux 在链表中构建了一棵树,并在这棵树上进行查找。
2)试图进行的内存访问是否合法?换句话说,进程是否有读、写或者执行这个区域内页面的权限?例如,这个缺页是不是由一条试图对这个代码段里的只读页面进行写操作的存储指令造成的?这个缺页是不是因为一个运行在用户模式中的进程试图从内核虚拟内存中读取字造成的?如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。这种情况在下图中标识为“2”。
3)此刻,内核知道了这个缺页是由于对合法的虚拟地址进行合法的操作造成的。它是这样来处理这个缺页的:选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU。这次,MMU就能正常地翻译A,而不会再产生缺页中断了。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。对于每个进程,内核维护一个变量brk,它指向堆的顶部。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。例如malloc。
隐式分配器:也叫做垃圾收集器,要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
7.10本章小结
本小节讨论了存储空间的管理,首先解释了四种地址的概念,逻辑地址、线性地址、虚拟地址、物理地址,接着描述了逻辑地址到线性地址再到物理地址的转换过程。再用TLB和四级页表的实例描述了va到pa的转变过程,说明了如何使用物理内存访问cache。在将进程使用到的fork和execve的内存映射情况加以描述。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix IO接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux Shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
4.读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件,作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放他们的内存资源。
Unix I/O函数:
①int open(char* filename,int flags,mode_t mode)
进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
②int close(fd)
进程通过调用close函数关闭一个打开的文件,fd是需要关闭的文件的描述符。
③ssize_t read(int fd,void *buf,size_t n)
read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
④ssize_t wirte(int fd,const void *buf,size_t n)
write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的IO设备管理方法,Unix IO接口及其函数,分析了printf函数和getchar函数的实现。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
1.输入:将hello.c代码从键盘输入。
2.预处理(cpp):将hello.c进行预处理,将c文件调用的所有外部的库展开合并,
生成hello.i文件。
3.编译(ccl):将hello.i文件进行翻译生成汇编语言文件hello.s。
4.汇编(as):将hello.s翻译成一个可重定位目标文件hello.o。
5.链接(ld):将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程
序hello,至此可执行hello程序正式诞生。
6.运行:在shell中输入./hello 1190201016 石衍 1
7.创建子进程:由于终端输入的不是一个内置的shell命令,因此shell调用fork ()
函数创建一个子进程。
8.加载程序:shell调用execve函数,启动加载器,映射虚拟内存,进入程序入口
后程序开始载入物理内存,然后进入main函数。
9.执行指令:CPU为进程分配时间片,在一个时间片中,hello享有CPU资源, 顺
序执行自己的控制逻辑流。
10.访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
11.动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。
12.信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进
程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
13终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。
对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法:
抽象对计算机系统是很重要的概念,底层信息用二进制来抽象表示,进程是对处
理器、主存和I/O设备的抽象,虚拟内存是对主存和磁盘设备的抽象,文件是对I/O
设备的抽象,等等。另外,存储器的“过渡”策略也十分精妙,由于CPU的处理
速度比主存快得多,为了减少“供不应求”的现象,在CPU和主存之间增加了一
级,二级,三级cache大大提高了CPU访问主存的速度。
附件
列出所有的中间产物的文件名,并予以说明起作用。
附件
文件名称 功能
hello.c 源程序
hello.i 预处理后文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位目标执行文件
hello 链接后的可执行文件
hello.elf hello.o的ELF格式
hello1.txt hello.o的反汇编
hello2.txt hello的反汇编代码
hello1.elf hello的ELF格式
参考文献
[1] 深入理解计算机系统原书第3版-文字版.pdf
[2] https://www.cnblogs.com/diaohaiwei/p/5094959.html
[3] https://www.cnblogs.com/pianist/p/3315801.html
[4] https://blog.csdn.net/drshenlei/article/details/4261909