正在上传…重新上传取消
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机类
学 号 120L022101
班 级 2003012
学 生 刘千钰
指 导 教 师 郑贵滨
计算机科学与技术学院
2021年5月
hello程序是几乎所有程序员的第一个程序,它的代码实现非常简单,但实际在计算机中运行时,非常复杂,他几乎需要计算机上大部分的硬件和软件协同合作。
hello程序的生命周期是从一个高级C语言程序开始的,然而为了在系统上运行hello.c程序,每条C语句都必须被其他程序转化为一系列的低级机器语言指令。然后这些指令按照一种成为可执行目标程序的格式打好包,并以二进制磁盘文件的形式存放起来。后面会详细介绍hello程序从源文件经过预处理,编译,汇编,链接成为可执行文件的过程,还有hello的进程管理,存储管理还有io管理。
关键词:C语言;深入理解计算机系统;预处理;编译;汇编;链接;进程管理;存储管理;虚拟内存;IO管理。
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:From Program to Process
Hello程序的生命周期是从一个源程序即程序员通过编辑器创建并保存的文本文件,文件名是hello.c。.c文件经过预处理器cpp的预处理生成hello.i文件,经过编译器ccl的编译生成hello.s文件,经过汇编器as的汇编生成hello.o文件,最后使用链接器ld进行链接最终成为可执行目标程序hello,当我们在shell中输入字符串.\hello并使用回车后,shell通过一系列指令的调用将输入的字符读入到寄存器中,之后将Hello目标文件中的代码和数据从磁盘复制到主存。调用fork产生一个子进程,然后hello便从程序变成了一个进程。
正在上传…重新上传取消
O2O: From Zero-0 to Zero-0
Shell通过execve,在子进程中加载hello。为hello创建新的区域结构,然后映射虚拟内存,设置程序计数器,使之指向代码段的入口点,载入物理内存。之后从main函数开始执行,CPU为hello分配时间片执行逻辑控制流。执行完成后shell父进程会回收hello进程,并且内核删除hello所有痕迹。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7/10 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位 以上;
开发工具:Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c 初始源程序
hello.i 预处理后的文本文件
hello.s 编译后的汇编文件
hello.o 汇编之后的可重定位目标程序
hello 由gcc -m64 -no-pie -fno-PIC hello.c -o命令生成,链接后可执行目标文件
1.4 本章小结
在第一章,简述了P2P和020的过程,介绍了软件硬件环境和开发工具,生成的中间结果文件。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。
预处理的作用:
- 处理宏定义指令
预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外,通常把排除在外的语句转换成空行。
2.处理条件编译指令
条件编译指令如#ifdef,#ifndef,#else,#elif,#endif等。 这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
3.处理头文件包含指令
头文件包含指令如#include "FileName"或者#include 等。 该指令将头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
- 处理特殊符号
预编译程序可以识别一些特殊的符号。 例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
2.2在Ubuntu下预处理的命令
输入命令:gcc -E hello.c -o hello.i
正在上传…重新上传取消
2.3 Hello的预处理结果解析
(以下格式自行编排,编辑时删除)
正在上传…重新上传取消
可以看到预处理以后hello.i比hello.c大了很多,从527bytes变成了64.7kb,可见预处理工作中对文本做了很大的改动和补充。
正在上传…重新上传取消
可以打开hello.i看到main函数前面多了很多东西,但是main函数没有改变。
2.4 本章小结
介绍了hello.c到hello.i预处理过程,分析了预处理结果,加深了对预处理的理解。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
编译的概念:编译器将文本文件翻译成汇编文本文件,即.s文件。分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。
编译的作用:编译器会在编译阶段对代码的语法进行检查,如果出现了语法上的错误,会在这一阶段直接反馈回来,造成编译的失败。如果在语法语义等分析过后,不存在问题,编译器会生成一个过渡的代码,也就是汇编代码,在随后的步骤中,汇编器可以继续对生成的汇编代码进行操作。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
正在上传…重新上传取消
3.3 Hello的编译结果解析
3.3.1汇编指令介绍:
.file:声明源文件
.text:代码节
.section:
.rodata:只读代码段
.align:数据或者指令的地址对其方式
.string:声明一个字符串(.LC0,.LC1)
.global:声明全局变量(main)
.type:声明一个符号是数据类型还是函数类型
3.3.2对字符串常量的处理
hello.c中的字符串常量:
正在上传…重新上传取消
在hello.s中以上字符串储存在主程序之外的区域:在hello.s中,字符串类型数据共出现两次,保存在程序的.rodata段中。
正在上传…重新上传取消
两个字符串被调用的地方:
正在上传…重新上传取消
3.3.3main函数参数的存储
在hello.c中main接受两个参数,分别是四字节的int类型argc,和char*数组argv。
正在上传…重新上传取消
在hello.s中,他们分别储存在%edi和%rsi中。
正在上传…重新上传取消
3.3.4关系操作
在hello.c中的一个条件控制语句argc!=4
正在上传…重新上传取消
在hello.s 中,将argc的值与4相比。若相等则跳转到.L2处,否则继续执行。
正在上传…重新上传取消
在hello.c中的一个条件控制语句i<8
正在上传…重新上传取消
在hello.s此处为第二处关系操作,利用-4(%rbp)处的值与7相比,控制循环的出口。
正在上传…重新上传取消
3.3.5循环控制
在hello.c中的循环
正在上传…重新上传取消
在hello.s中
正在上传…重新上传取消
L2为初始化的操作,给-4(%rbp)赋值为0,就是初始化i=0
正在上传…重新上传取消
L3是判断循环是否结束的操作,通过每次给-4(%rbp)加一来记录循环的次数,循环8次以后退出循环。
正在上传…重新上传取消
L4是循环体的操作,依次执行循环体内的操作。
3.3.6数组操作
hello.c在for语句中取了数组argv中的值。
正在上传…重新上传取消
在hello.s中
正在上传…重新上传取消
通过L4中对addq $7, %rax的操作,可得argv中元素为八个字节。此处分别调用了argv[1]和argv[2],可以看到%rax又执行了加24的操作,可以看出此时取argv[3]中的元素,并将其取到%rax中,并放入%rdi中作为参数,调用atoi函数。并将返回结果%eax放入%edi中,调用sleep函数。
3.3.7算数操作
hello.c中对i++的算数操作
正在上传…重新上传取消
hello.s中的表示
正在上传…重新上传取消
3.3.8调用函数
hello.c在main函数里面调用了printf,exit,atoi,sleep,getchar,put等函数
正在上传…重新上传取消
getchar函数不用传入参数,可以直接调用。
正在上传…重新上传取消
调用exit函数:将1作为参数放在%edi中作为参数传递。
正在上传…重新上传取消
调用atoi函数:把argv[3](字符串)通过%rdi传递给atoi函数。
调用sleep函数:通过%eax赋值给%edi,将返回值储存在%eax中,使用ret指令返回。
正在上传…重新上传取消
调用printf函数:通过寄存器将字符串常量的地址传递给函数。
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.4 本章小结
这里我们主要介绍了编译器是如何将文本编译成汇编代码的。可以发现,编译器并不是死板的按照我们原来文本的顺序,逐条语句进行翻译下来的。编译器在编译的过程中,不近会对我们的代码做一些隐式的优化,而且会将原来代码中用到的跳转,循环等操作操作用控制转移等方法进行解析。最后生成我们需要的hello.文件。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
汇编的概念:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。
汇编的作用:将汇编代码转变成机器可以识别的二进制格式。
4.2 在Ubuntu下汇编的命令
命令:gcc hello.s -c -o hello.o
正在上传…重新上传取消
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
使用命令 readelf -a hello.o 可以重定位目标文件hello.o的ELF格式。
4.3.1 ELF Header(ELF头)
如图,ELF可重定位目标文件中首先出现的是ELF头,以16字节的序列Magic开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助连接器语法分析和解释目标文件的信息。其中包含ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
正在上传…重新上传取消
4.3.2Section Headers(节头部表)
节头部表描述了不同节的位置和大小,其中目标文件中每个节都有一个固定大小的条目。具体的描述包括节的名称(Name)、类型(Type)、地址(Address)和偏移量等(offset)。
正在上传…重新上传取消
正在上传…重新上传取消
4.3.3Key to flags(重定位条目)
重定位条目包含了链接时重定位所需的全部信息:需要被重定位的代码在其段中的偏移、该段代码所对应的符号在符号表中的索引以及重定位类型、重定位时被使用到的加数。
.rela.text节是一个.text节中位置的列表。当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者应用全局变量的指令都需要修改。“Offset”是需要被修改的引用的字节偏移(在代码节或数据节的偏移),“Info”指示了重定位目标在.symtab中的偏移量和重定位类型,“Type”表示不同的重定位类型,例如图中R_X86_64_PC32就表示重定位一个使用32位PC相对地址的引用。“Sym.name”表示被修改引用应该指向的符号,“Addend”是一个有符号常数,一些类型的重定位要用他对被修改引用的值做偏移调整。
正在上传…重新上传取消
正在上传…重新上传取消
4.3.4 Symbol(符号表)
存放在程序中定义和引用的函数和全局变量的信息。
正在上传…重新上传取消
4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
正在上传…重新上传取消
正在上传…重新上传取消
反汇编代码与hello.s比较,可以发现主要有这些差异:
1,由机器语言的构成,hello.s文件中还有许多的伪代码,而反汇编代码中没有。
机器语言:二进制的机器指令的集合。
汇编语言:主体是汇编指令,是机器指令便于记忆的表示形式,为了方便程序员读懂和记忆的语言指令。
- 从main函数开始(起始地址为0),每一条机器指令获得了地址。可以看到,hello.s中所有10进制表示的立即数,在hello.o中转为使用16进制表示。
例如
正在上传…重新上传取消
- 使用重定向条目替换全局变量和外部函数。因为变量还有函数的真正地址需要经过重定位才能确定,所以这里都用0替代。
正在上传…重新上传取消
- 分支转移:hello.s文件中分支通过使用段名称跳转,而在hello.o的反汇编代码中,使用<函数名 + 偏移量>的形式,通过地址跳转。
正在上传…重新上传取消
正在上传…重新上传取消
5,函数调用:在hello.s文件中,函数调用call只需要加函数名称,在hello.o反汇编代码中,call则是使用了当前指令的下一个字节。原因是因为该函数迟绑定,该函数为共享库中函数,只有运行时,动态链接器作用后才能确定相应的PLT条目地址。
正在上传…重新上传取消
正在上传…重新上传取消
4.5 本章小结
这里我们介绍了汇编的概念和作用,查看了可重定位目标elf格式,分析了hello.o的结果,比较了反汇编代码与.s的区别,汇编让代码变的让机器能读懂,让我更了解汇编了。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
注意:这儿的链接是指从 hello.o 到hello生成过程。
链接的概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
链接的作用:链接使分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们该边这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
命令:ld -o hello -dynamic-linker /lib64ld-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的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
在终端中输入指令readelf -a hello.out 打印可执行文件的ELF格式。
1,ELf头,它描述了可执行文件的总体格式,其中有关于执行的入口等信息
正在上传…重新上传取消
- 节头部表,对可执行文件的所有节信息进行了声明,包括大小,偏移量等信息。
正在上传…重新上传取消
正在上传…重新上传取消
正在上传…重新上传取消
- 程序头
正在上传…重新上传取消
- 段节
正在上传…重新上传取消
正在上传…重新上传取消
- 重定位节
正在上传…重新上传取消
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
使用edb打开hello的可执行文件后,可以在Data Dump中找到hello的虚拟地址空间。可以发现ELF被映射到了0x401000
正在上传…重新上传取消
在hello.out的ELF文件程序头可以看到各段的虚拟地址
正在上传…重新上传取消
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.5.1函数代码段
在hello.c中,我们只能看到main函数的代码段,但是在可知向文件hello.out中,还出现了其他代码段,并且每个函数以及每条指令都有了虚拟地址,如下图所示:
正在上传…重新上传取消
5.5.2 重定位节和符号定义
链接器将所有的同类型的节合并成一个新的节。然后链接器将内存地址赋给新的节,以及每个符号。
正在上传…重新上传取消
5.5.3全局变量和函数重定位
在hello.o中,用0+%rip的值来表示全局变量的位置,同时使用重定位条目表明后续得到真正的值的方法。而在hello中,所有全局变量都得到了自己的虚拟地址。同样被重定位的还有在main中调用的函数。
正在上传…重新上传取消
5.5.4 main函数的变化
在hello.o中main函数的起始地址是0x0000000000000000但是在hello中,main函数有了起始地址变成了虚拟地址。而且每一条指令的地址也随之偏移。
正在上传…重新上传取消
5.5.5跳转指令
在hello中跳转指令的操作数为跳转目的地的虚拟地址:
正在上传…重新上传取消
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
调用start函数,调用__libc_start_main函数,调用libc-2.27.so!__cxa_atexit,调用__libc_csu_init,调用__libc_csu_init ,调用_init函数,调用libc-2.27.so!_setjmp函数,调用-libc-2.27.so!_sigsetjmp函数,调用main,调用puts,调用exit
子程序名 | 地址 |
_init | 0x00401000 |
puts@plt | 0x00401030 |
print@plt | 0x00401040 |
getchar@plt | 0x00401050 |
atoi@plt | 0x00401060 |
exit@plt | 0x00401070 |
sleep@plt | 0x00401080 |
_start | 0x004010f0 |
_dl_relocate_static_pie | 0x0040111f |
deregister_tm_clones | 0x00401130 |
register_tm_clones | 0x00401160 |
_do_global_dtors_aux | 0x004011a0 |
frame_dummy | 0x004011d0 |
main | 0x004011d6 |
_libc_csu_init | 0x00401270 |
_libc_csu_fini | 0x004012e0 |
libc-2.31.so!__libc_start_main | 0x7f6d972d6fc0 |
libc-2.31.so!__cxa_atexit | 0x7f6d972f9e10 |
Libc-2.31.so!_setjmp | 0x7f6d972f5cb0 |
Libc-2.31.so!_sigsetjmp | 0x7f6d972f5be0 |
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
GOT表:
概念:每一个外部定义的符号在全局偏移表(Global offset Table)中有相应的条目,GOT位于ELF的数据段中,叫做GOT段。
作用:把位置无关的地址计算重定位到一个绝对地址。程序首次调用某个库函数时,运行时连接编辑器(rtld)找到相应的符号,并将它重定位到GOT之后每次调用这个函数都会将控制权直接转向那个位置,而不再调用rtld。
PLT表:
过程连接表(Procedure Linkage Table),一个PLT条目对应一个GOT条目
当main()函数开始,会请求plt中这个函数的对应GOT地址,如果第一次调用那么GOT会重定位到plt,并向栈中压入一个偏移,程序的执行回到_init()函数,rtld得以调用就可以定位printf的符号地址,第二次运行程序再次调用这个函数时程序跳入plt,对应的GOT入口点就是真实的函数入口地址。
动态连接器并不会把动态库函数在编译的时候就包含到ELF文件中,仅仅是在这个ELF被加载的时候,才会把那些动态函库数代码加载进来,之前系统只会在ELF文件中的GOT中保留一个调用地址。
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。
正在上传…重新上传取消
在dl_init调用之后, 0x601008和0x601010处的两个8B数据分别发生改变为0x7fd9 d3925170和0x7fd9 d3713680,其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]指向动态链接器ld-linux.so运行时地址。
正在上传…重新上传取消
在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问跳转时GOT地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。
5.8 本章小结
本章详细讨论了链接的概念与作用、在Ubuntu下链接的命令,以及可执行目标文件hello的格式,详细分析可执行目标文件和可重定位目标文件的区别。详细解释了hello虚拟地址空间。细致分析了链接的重定位过程。模拟hello的执行流程,并深入探讨了hello的动态链接。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
进程的作用:进程为用户提供以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash的作用:Shell是一个命令行解释器,它为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序,用户可以用Shell来启动、挂起、停止甚至编写一些程序。Shell还是一个功能相当强大的编程语言,易编写,易调试,灵活性较强。Shell是解释执行的脚本语言,在Shell中可以直接调用Linux系统命令。bash提供了一个图形化界面,提升交互速度。
Shell-bash的处理流程:
- 从终端或控制台获取用户输入命令
- 将用户输入命令进行解析,判断输入命令是否为内置命令
- 若是内置命令,则直接执行;若不是内置命令,则bash在初始子进程上下文中加载和运行它
- 判断程序的执行状态,若为前台进程则等待进程结束;否则直接将进程放在后台执行,继续等待用户下一次输入。
6.3 Hello的fork进程创建过程
内核态:不可抢占的
用户态:可抢占,可以进行调度的
上述所有的初始化操作都是在内核态执行的,这么做的目的是,内核初始化过程是不能被中断的,在内核态运行可以保证这一点。
切换到用户态以后,便开始创建进程了:
Shell(父进程)通过fork
函数创建一个新的运行的子进程。新的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈.子进程进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork
时,子进程可以读写父进程中打开的任何文件。
正在上传…重新上传取消
6.4 Hello的execve过程
在子进程中通过系统调用execve()可以将新程序加载到子进程的内存空间。这个操作会丢弃原来的子进程execve()之后的部分,而子进程的栈、数据会被新进程的相应部分所替换。即除了进程ID之外,这个进程已经与原来的进程没有关系了。
在shell中执行Hello程序。它也是首先调用execve()。这个系统调用的实际上Hello进程的父进程是shell进程,它是shell进程fork出来的一个子进程然后执行execve之后在执行的Hello。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
- 逻辑控制流:每个程序都会有一个程序计数器的值的序列,这个序列中的值的转变的序列就叫做控制流。
- 进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
- 用户态和核心态:用户态就是提供应用程序运行的空间,为了使应用程序访问到内核管理的资源例如CPU,内存,I/O。内核必须提供一组通用的访问接口,这些接口就叫系统调用。运行在内核态的进程可以“为所欲为”。我们所说的内核,它本质上是一种特殊的软件程序
- 上下文信息:执行进程所需要的信息状态
hello的进程执行:首先执行Hello程序,进程处于用户模式。当输入的参数不是3个时,会调用exit函数,终止并回收Hello进程;当输入的参数为3个时,会调用sleep函数,进程休眠,此时内核会保存Hello进程的上下文。之后再恢复进程的上下文,并开始进程。休眠了固定时间后,sleep函数传递一个信号,使控制传递给内核,恢复hello进程的上下文,执行hello进程。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.6.1异常的分类:
中断:中断是异步发生的,是来自处理器外部的 I/O
设备的信号的结果。硬件中断不是由任何一条专门的指令造成的,从这个意义上来说它是异步的。硬件中断的异常处理程序常常称为中断处理程序。
陷阱:陷阱是有意的异常,是执行一条指令后的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
故障:故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果故障处理程序能够修正这个错误,它就将控制返回给引起故障的指令,从而重新执行它,否则,处理程序返回到内核中的abort 例程,abort 例程会终止引起故障的应用程序。
终止:终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如 DRAM或者 SRAM 位被损坏时发生的奇偶错误。终止处理程序不会将控制返回给应用程序。
6.6.2 hello运行实例
1,正常运行
正在上传…重新上传取消
- 不停乱按
正在上传…重新上传取消
与无异常的情况相似,但是在第一个回车按下前的所有输入字符都被作为输入读入,其余当作新的shell指令,但没有任何影响。、
3,pstree,以树状图显示进程间的关系
正在上传…重新上传取消
- Ctrl+Z
- 输入ctrl + Z,将会发送一个SIGTSTP信号给shell,使hello进程挂起。
正在上传…重新上传取消
输入ps指令查看进程,发现hello进程被挂起
输入jobs指令,发现被停止的hello进程
正在上传…重新上传取消
输入fg重新运行
正在上传…重新上传取消
输入kill杀死进程
正在上传…重新上传取消
6.7本章小结
本章介绍并分析了进程的创建,程序的运行,上下文的切换,并发程序的机理,介绍了一些异常,信号和处理函数。描述了一些命令行的作用和shell工作的简单原理。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:指由程序产生的段内偏移地址。逻辑地址与虚拟地址二者之间没有明确的界限。从hello的反汇编代码中看到的地址,就是逻辑地址,它们需要通过计算,即加上对应段的基地址才能得到真正的地址
线性地址:指虚拟地址到物理地址变换的中间层,是处理器可寻址的内存空间(称为线性地址空间)中的地址。程序代码会产生逻辑地址,或者说段中的偏移地址,加上相应段基址就成了一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换产生物理地址。若是没有采用分页机制,那么线性地址就是物理地址。hello的反汇编文件中看到的地址(即逻辑地址)中的偏移量,加上对应段的基地址,便得到了hello中内容对应的线性地址。
虚拟地址:是由程序产生的由段选择符和段内偏移地址组成的地址。这2部分组成的地址并不能直接访问物理内存,而是要通过分段地址的变化处理后才会对应到相应的物理内存地址。hello中看到的地址加上对应段基地址的值是虚拟地址。
物理地址:指内存中物理单元的集合,他是地址转换的最终地址,进程在运行时执行指令和访问数据最后都要通过物理地址来存取主存。hello的物理地址是通过虚拟地址对应来的,在物理地址可以访问内存的值。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。
7.3 Hello的线性地址到物理地址的变换-页式管理
CPU的页式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址。从管理和效率的角度出发,线性地址被分为以固定长度为单位的组,称为页(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个tatol_page[2^20]的大数组,共有2的20个次方个页。这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址——对应的页的地址。另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。
7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU生成一个虚拟地址,MMU就必须查阅一个PTE,为提升速度,在MMU中增加了一个PTE的缓存——TLB(翻译后备缓冲器)。
使用TLB进行地址翻译时,会将VPN解释为两部分:TLB标记,与TLB索引。
由TLBI,访问TLB中的某一组。遍历该组中的所有行,若找到一行的标记等于TLBT,且有效位valid为1,,则缓存命中,该行存储的即为PPN;若未找到一行的tag等于TLBT,或找到但该行的valid为0,则缓存不命中。进而需要到页表中找到被请求的块,用以替换原TLB表项中的数据。
四级页表缓存不命中后,VPN被解释成从低位到高位的4段,从高地址开始,第一段VPN作为第一级页表的索引,用以确定第二级页表的基址;第二段VPN作为第二级页表的索引,用以确定第三级页表的基址;第三段VPN作为第三级页表的索引,用以确定第四级页表的基址;第四段VPN作为第四级页表的索引,若该位置的有效位为1,则第四级页表中的该表项存储的是所需要的PPN的值。
在上述过程中,只要有一级页表条目的有效位为0,下一级页表就不存在,也就是产生缺页故障了,需要到内存中加载。从页表中取出的PPN加上与VPO相同的PPO就构成了物理地址PA。
7.5 三级Cache支持下的物理内存访问
经过前面的过程,hello的物理地址已经得知,现在需要访问该物理地址。在现代计算机中,存储器被组织成层次结构,因为这样可以最大程度地平衡访存时间和存储器成本。所以在CPU在访存时并不是直接访问内存,而是访问内存之前的三级cache。已知Core i7的三级cache是物理寻址的,块大小为64字节。LI和L2是8路组相联的,而L3是16路组相联的。
得到了52位物理地址,接下来CPU把地址发送给L1,因为L1块大小为64字节,所以B=64,b=6。又L1是8路组相联的,所以S=8,s=3。标记位t有52-6-3=43位,即是得到的52位物理地址的前43位。首先,根据物理地址的s位组索引索引到L1 cache中的某个组,然后在该组中查找是否有某一行的标记等于物理地址的标记并且该行的有效位为1,若有,则说明命中,从这一行对应物理地址b位块偏移的位置取出一个字节,若不满足上面的条件,则说明不命中,需要继续访问下一级cache,访问的原理与L1相同,若是三级cache都没有要访问的数据,则需要访问内存,从内存中取出数据并放入cache。
7.6 hello进程fork时的内存映射
Shell通过调用fork的函数可以让进程内核自动创建新的进程,这个新的进程拥有各自新的数据结构,并且被内核分配了唯一的pid。它有着自己独立的虚拟内存空间。
虚拟内存和内存映射解释了fork函数如何为hello进程提供私有的虚拟地址空间。fork为hello的进程创建虚拟内存,创建当前进程的的mm_struct,vm_area_struct和页表的原样副本;两个进程中的每个页面都标记为只读;两个进程中的每个区域结构都标记为私有的写时复制,在hello进程中返回时,hello进程拥有与调用fork进程相同的虚拟内存。随后的写操作通过写时复制机制创建新页面。
并且它还拥有自己独立的逻辑控制流,它同样可以拥有当前已经可以打开的各类文件信息和页表的原始数据和样本,为了有效保护进程的私有数据和信息,同时为了节省对内存的消耗,进程的每个数据区域都被内核标记起来作为写时复制。
正在上传…重新上传取消
7.7 hello进程execve时的内存映射
删除已存在的用户区域:删除当前进程虚拟地址的用户部分中的已存在的区域结构。
映射私有区域:为新程序hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
映射共享区域:如果hello程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。
正在上传…重新上传取消
7.8 缺页故障与缺页中断处理
正在上传…重新上传取消
- 处理器将虚拟地址发送给 MMU, MMU 使用内存中的页表生成PTE地址
- 有效位为零, 因此 MMU 触发缺页异常
- 缺页处理程序确定物理内存中牺牲页 (若页面被修改,则换出到磁盘)
- 缺页处理程序调入新的页面,并更新内存中的PTE
- 缺页处理程序返回到原来进程,再次执行缺页的指令
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
动态内存分配,顾名思义就是可以使我们更加灵活的分配和运用内存,动态内存的开辟的释放都是按照我们自身的需要进行的,我们通常直接使用的内存是在栈上的,开辟和释放内存都是由系统自动进行的,而动态内存分配的内存是在堆上。在堆上开辟的内存,不会被系统自动释放,只能通过我们自己释放或者当程序结束的时候被自动回收,只要我们有指向由我们申请在堆上的空间的指针,我们就能对它进行修改数据和使用。
动态储存分配管理使用动态内存分配器进行。动态内存分配器维护一个进程的虚拟区域,堆。堆是一个不同大小块的集合,每个块由连续虚拟内存组成,分为已分配和未分配两种。未分配的块会保持状态直到被分配,已分配的块会供程序使用直到被释放。动态内存的分配一般分为显式空闲链表管理和隐式空闲链表管理两种。
显式空闲链表在空闲块中使用指针连接空闲块,仅仅需要关注空闲块,数据结构如下:
正在上传…重新上传取消
对于每个块都需要知道块大小和分配状态,这样可能需要两个words,但是由于块是对齐的,比如8字节对齐,则块大小的低三位恒为0,这样就可以使用低三位来存储分配状态。在实际设计中,往往使用最低位标记已分配/未分配状态。
正在上传…重新上传取消
7.10本章小结
本章介绍了hello的内存管理,虚拟内存很抽象,intel的段式管理、hello的页式管理,以及TLB与四级页表支持下的VA到PA的变换过程和三级Cache支持下的物理内存访问。还阐述了hello进程fork和execve时的内存映射、缺页故障的处理流程和动态存储分配器的管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
所有的I/O设备都被模型化为文件,甚至内核也被映射为文件。
一个Linux文件就是一个m字节的序列。所有的输入、输出都被认为时对相应文件的读和写来执行。
文件的类型有:
普通文件:包含任何数据,分成文本文件、二进制文件。
目录:包含一组链接的文件。每个连接都将一个文件名映射到一个文件
套接字:用于与另一个进程进行跨网络通信的文件
设备管理:unix io接口
将I/O设备模型化为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使所有的输入、输出都能以一种统一且一致的方式来执行。
我们可以对文件的操作有:打开关闭操作open和close;读写操作read和write;改变当前文件位置lseek等
8.2 简述Unix IO接口及其函数
Unix I/O 接口的几种操作:
(1)打开文件:程序要求内核打开文件,内核返回一个小的非负整数(描述符),用于标识这个文件。程序在只要记录这个描述符便能记录打开文件的所有信息。
(2)shell在进程的开始为其打开三个文件:标准输入、标准输出和标准错误。
(3)改变当前文件的位置:对于每个打开的文件,内核保存着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作显式地设置文件的当前位置为k。
(4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会出发一个称为EOF的条件,应用程序能检测到这个条件,在文件结尾处并没有明确的EOF符号。
(5)关闭文件:内核释放打开文件时创建的数据结构以及占用的内存资源,并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
UnixI/O函数:
(1)intopen(char* filename, int flags, mode_t mode);open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
(2)intclose(int fd);关闭一个打开的文件,返回操作结果。
(3)ssize_t read(int fd, void *buf, size_t n);read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
(4)ssize_t write(int fd, const void *buf,size_t);write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
8.3 printf的实现分析
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
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;
}
va_list arg = (va_list)((char*)(&fmt) + 4);
va_list的定义:typedef char *va_list
这说明它是一个字符指针。其中的: (char*)(&fmt) + 4) 表示的是第一个参数。
C语言中,参数压栈的方向是从右往左。当调用printf函数的适合,先是最右边的参数入栈。fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素。fmt也是个变量,它的位置,是在栈上分配的,它也有地址。 对于一个char *类型的变量,它入栈的是指针,而不是这个char *型变量。
VSprintf函数
int vsprintf(char *buf, const char *fmt, va_list args)
{
char* p;
char tmp[256];
va_list p_next_arg = args;
for (p=buf;*fmt;fmt++) {
if (*fmt != '%') {
*p++ = *fmt;
continue;
}
fmt++;
switch (*fmt) {
case 'x':
itoa(tmp, *((int*)p_next_arg));
strcpy(p, tmp);
p_next_arg += 4;
p += strlen(tmp);
break;
case 's':
break;
default:
break;
}
}
return (p - buf);
}
vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
异步异常-键盘中断的处理:在用户按键盘时,键盘接口获得一个键盘扫描码,这时会产生一个中断的请求,键盘会中断处理子程序。接受按键扫描码转成ASCII码,保存到系统的键盘缓冲区。getchar函数通过调用read系统函数,通过系统调用读取按键的ASCII码,直到接收到回车才返回这个字符串。getchar函数读取一个字符。
8.5本章小结
本章介绍了Linux中I/O设备的管理方法,UnixI/O接口和函数,并且具体分析了printf和getchar函数。
(第8章1分)
结论
hello程序是几乎所有程序员的第一个程序,它的代码实现非常简单,但实际在计算机中运行时,非常复杂,他几乎需要计算机上大部分的硬件和软件协同合作。
通过本次大作业,使我对hello的一生有了更深刻的了解。
首先经历cpp预处理器的预处理,将所有宏定义等递归展开到hello.i文件中,消除所有的注释;然后经历ccl编译器的编译,将hello.i编译为汇编代码;随后经过汇编器as的汇编,将hello.s会变为可重定位文件hello.o,且此时是二进制机器指令;最后经过ld链接器与其他必要的可重定位文件、共享库链接,并留下动态链接接口,在加载时动态链接。生成了可执行文件hello。
在shell中输入指令,shell调用fork函数,生成子进程,并由execve函数在当前进程即子进程的上下文中加载新程序hello。CPU为hello分配一个时间片,在这个时间片中,hello程序按顺序执行自己的控制逻辑流。当程序执行需要数据时,就会通过多级存储器层次结构层层访问。hello中的访存操作需要经历逻辑地址到线性地址到物理地址的变换。若此时hello遇到了信号与异常的影响,会触发系统调用异常。
hello在运行时会调用一些函数,比如printf等,这些函数与linux I/O的设备模型化密切相关。最后hello会被shell的父进程回收,内核会回收为其创建的信息。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c 初始源程序
hello.i 预处理后的文本文件
hello.s 编译后的汇编文件
hello.o 汇编之后的可重定位目标程序
hello 由gcc -m64 -no-pie -fno-PIC hello.c -o命令生成,链接后可执行目标文件
(附件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.
完成大作业的网址:[转]printf 函数实现的深入剖析 - Pianistx - 博客园printf 函数实现的深入剖析
(3条消息) 操作系统复习:12.缺页中断以及内存页面置换算法_Java从跨平台到跨行业的博客-CSDN博客_操作系统如何处理缺页中断和缺段中断
(3条消息) gcc生成的unix可执行文件与默认生成的a.out区别_Wayne557的博客-CSDN博客_gcc默认生成的可执行文件名
(3条消息) 动态内存分配--隐式空闲链表_小尘_OnMyWay的博客-CSDN博客_隐式空闲链表
(3条消息) 显式空闲链表和隐式空闲链表_数据结构静态链表_weixin_39927623的博客-CSDN博客
(参考文献0分,缺失 -1分)