计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机类
计算机科学与技术学院
2019年12月
摘 要
本文是基于linux对C语言程序hello.c的从编译运行到终止的整个P2P(From Program to Process)和O2O(O2O: From Zero-0 to Zero-0)过程进行了深入分析。涵盖了从C语言代码文件转化为linux下的可执行文件过程中的预处理、编译、汇编和链接阶段,和可执行文件过程中的进程管理、存储管理和I/O管理的原理的内容。
关键词: 程序编译,存储管理,进程管理,I/O管理 ,计算机系统
(摘要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简介
P2P的定义: From Program to Process
即在OS中通过预处理、编译、汇编、链接,运行逐个过程让Hello从代码到可执行文件再在shell里面执行并通过execve和fork生成进程的过程。
O2O的定义: From Zero-0 to Zero-0。
计算机系统-Editor+Cpp+Compiler+AS+LD + OS + CPU/RAM/IO等
程序从零开始,在编译器中编写,通过GCC预处理、编译、汇编、链接生成可执行文件,在OS中的Shell执行,利用execve加载hello,开始映射虚拟内存,CPU通过又MMU利用虚拟地址访问物理地址,如果页面不在内存中再从硬盘中调入。当程序结束后,Shell作为父进程回收子进程hello,同时回收其在内存中占用的空间
1. // 大作业的 hello.c 程序
2. // gcc -m64 -no-pie -fno-PIC hello.c -o hello
3. // 程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等。
4. // 可以 运行 ps jobs pstree fg 等命令
5.
6. #include <stdio.h>
7. #include <unistd.h>
8. #include <stdlib.h>
9.
10. int sleepsecs=2.5;
11.
12. int main(int argc,char *argv[])
13. {
14. int i;
15.
16. if(argc!=3)
17. {
18. printf("Usage: Hello 学号 姓名!\n");
19. exit(1);
20. }
21. for(i=0;i<10;i++)
22. {
23. printf("Hello %s %s\n",argv[1],argv[2]);
24. sleep(sleepsecs);
25. }
26. getchar();
27. return 0;
28. }
Hello程序的代码
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;2.30GHz;16G RAM;1THD Disk
1.2.2 软件环境
Windows10 64位;Vmware 14;Ubuntu 18;
1.2.3 开发工具
Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位
1.3 中间结果
hello.c hello程序源代码
hello.i:经过预处理的hello.c生成的代码文本
hello.s:hello.i 经过编译过程生成的汇编代码
hello.o: hello.s经过汇编之后生成的可重定位目标文件
hello :经过链接后的可执行文件
hello_objd.txt:链接后的hello可执行文件经过反汇编生成的代码
ELFout.txt:链接后的hello可执行文件经过readelf读取的ELF信息
1.4 本章小结
本章对hello做了简介,描述了什么是P2P和O2O,列出了本次任务的环境和工具,并且描述了任务过程中出现的中间结果和成的中间结果文件的名字,文件的作用。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位——(用C/C++的术语来说是)预处理记号(preprocessing token)用来支持语言特性(如C/C++的宏调用)
在预处理阶段。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如#include < stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。
2.2在Ubuntu下预处理的命令
指令:gcc -E hello.c -o hello.i
其中,-E选项指只进行预处理操作,而-o选项指定了输出的文件
2.3 Hello的预处理结果解析
如图为hello.c的大小
预处理后的hello.i的大小
显然hello文件的大小增长了许多,说明其中增添了代码
Hello.c中只有原本的代码,而在Hello.i中出现了其他的代码,分析为stdio.h中的代码,而源代码中的include语句消失了,则这些预处理命令被预处理器处理并在文件中插入代码
原来Hello.c中的代码则处于Hello.i中的最后面的部分
2.4 本章小结
本章是关于在编译的时候如何对hello.c 源代码进行预处理操作,在linux中预处理由预处理器(cpp)执行,预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。并读取系统头文件stdio.h的内容,并把它直接插人程序文本中。结果就得到了另一个C程序,而通常是以.i作为文件扩展名。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
在编译阶段,编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。即从高级语言到汇编语言的过程。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
其中,-S选项指只进行编译操作,而-o选项指定了输出的文件
3.3 Hello的编译结果解析
打开hello.c的效果
说明编译器是怎么处理C语言的各个数据类型以及各类操作?
3.3.1:数据:常量、变量(全局/局部/静态)、表达式、类型、宏
常量:如循环
这些常量在汇编代码中以立即数的方式存在,如图所示
变量
- 全局变量
int sleepsecs=2.5;
全局变量sleepsecs定义为int型数值为2.5,在编译后由于int型和long型大小相同,而其实整数,故编译器自动将其类型由int转化为了long,数值大小为2,被定义为全局变量globl,储存在.data段中,其占用内存是4字节,对齐方式被设为4
- 局部变量
int i;
通常储存在栈或者是寄存器中,在这个程序中,i被存放在栈中
而argc作为传入main的参数
同样储存在栈中
3. 字符串
代码中出现的字符串
存放在.rodata节的两个字符串,
3.3.2赋值 = ,逗号操作符,赋初值/不赋初值
赋值:
1.对全局变量的赋值:
全局变量的赋初值直接在汇编后代码
2.对局部变量的赋值:
对循环变量i的赋值
使用的是movl语句,局部变量i保存在栈中,在内存中位置为: -4(%rbp),对局部变量i赋初值为0
3.3.3类型转换(隐式或显式)
在代码中
该语句经编译器处理后为:
发现其被编译器隐式类型转换由int型转换成了long型,而2.5是浮点数,编译器选择对其向偶数舍入,把2.5舍入为2
3.3.4算术操作:+ - * / % ++ – 取正/负± 复合“+=”等
在代码中,出现的算数操作
即为将i的数值加一
在汇编代码中使用
是使用
addl $1, -4(%rbp)
语句实现的
3.3.5关系操作
第一个是判定argc是否等于3
其实现如图所示:
然后是判定循环变量是否小于10
其中-20(%rbp)存放的就是argc
其中-4(%rbp)存放的是循环变量i
则实现逻辑判定是利用cmpl语句实现的,通过比较来设定条件码,然后利用 jle来根据设置的条件码判断是否进行跳转操作
3.3.6数组/指针/结构操作
在代码中对数组的引用如图所示:
在汇编代码中
argv[1]和argv[2]分别表示两个字符串,而根据调用语句的传参我们可以知道汇编代码是如何访问参数的
则对argv[2]的访问:M[%rbp-32+16]
对argv[1]的访问:M[%rbp-32+8]
可以看出这个数组存放在栈中
3.3.7控制转移:if/else switch for while do/while ?: continue break
控制转移语句在汇编代码中的体现:
for语句中的控制流转移同理
汇编代码:
cmpl $9, -4(%rbp)
jle .L4
判定如果i<=9的话跳转到L4重复循环直到i>9,然后跳出循环执行下面的语句。
3.3.8函数操作:参数传递(地址/值)、函数调用()、函数返回 return
在汇编代码中的体现
对
printf(“Usage: Hello 学号 姓名!\n”);
语句的体现:
其中:通过寄存器转递参数,要传递的字符串"Usage: Hello 学号 姓名!\n"LC0(%rip),将其地址传入%rdi寄存器中,调用printf()函数,经过编译器优化后为puts()函数,而无返回值
对
exit(1);
体现:
1作为参数通过edi寄存器传入,然后调用exit()函数,无返回值
对
通过%rdi传入要输出的语句的地址,作为第一个参数
通过%rsi传入第二个参数,argv[1],其寻址在3.3.5节中有提到,
通过%rdx传入第三个参数argv[2],原理同上;函数无返回值
在
中,
先将sleepsecs(%rip)存到%eax寄存器中,再将%eax寄存器中内容转存到%edi作为传入的第一个参数,最后调用函数sleep()
无返回值
最后看
直接调用即可,无参数也无返回值
3.4 本章小结
在编译阶段,编译器(ccl)将高级语言文本文件翻译成汇编语言文本文件,是P2P过程预处理后的下一步,本章分别介绍了Linux下编译的方法和编译后C语言各种数据与操作发生的各种变化。
第4章 汇编
4.1 汇编的概念与作用
汇编阶段。汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o 文件是一个二进制文件,无法用普通的文本编译器打开 。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
其中,-c选项指只进行汇编操作,而-o选项指定了输出的文件
4.3 可重定位目标elf格式
典型的ELF可重定位目标文件
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
ELF头
ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。夹在ELF头和节头部表之间的都是节。-一个典型的ELF可重定位目标文件包含下面几个节:
节头
节头部表描述了不同节的位置和大小,目标文件中每个节都有一个固定大小的条目。
1. 节头:
2. [号] 名称 类型 地址 偏移量
3. 大小 全体大小 旗标 链接 信息 对齐
4. [ 0] NULL 0000000000000000 00000000
5. 0000000000000000 0000000000000000 0 0 0
6. [ 1] .text PROGBITS 0000000000000000 00000040
7. 0000000000000081 0000000000000000 AX 0 0 1
8. [ 2] .rela.text RELA 0000000000000000 00000338
9. 00000000000000c0 0000000000000018 I 10 1 8
10. [ 3] .data PROGBITS 0000000000000000 000000c4
11. 0000000000000004 0000000000000000 WA 0 0 4
12. [ 4] .bss NOBITS 0000000000000000 000000c8
13. 0000000000000000 0000000000000000 WA 0 0 1
14. [ 5] .rodata PROGBITS 0000000000000000 000000c8
15. 000000000000002b 0000000000000000 A 0 0 1
16. [ 6] .comment PROGBITS 0000000000000000 000000f3
17. 0000000000000024 0000000000000001 MS 0 0 1
18. [ 7] .note.GNU-stack PROGBITS 0000000000000000 00000117
19. 0000000000000000 0000000000000000 0 0 1
20. [ 8] .eh_frame PROGBITS 0000000000000000 00000118
21. 0000000000000038 0000000000000000 A 0 0 8
22. [ 9] .rela.eh_frame RELA 0000000000000000 000003f8
23. 0000000000000018 0000000000000018 I 10 8 8
24. [10] .symtab SYMTAB 0000000000000000 00000150
25. 0000000000000198 0000000000000018 11 9 8
26. [11] .strtab STRTAB 0000000000000000 000002e8
27. 000000000000004d 0000000000000000 0 0 1
28. [12] .shstrtab STRTAB 0000000000000000 00000410
29. 0000000000000061 0000000000000000 0 0 1
30. Key to Flags:
31. W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
32. L (link order), O (extra OS processing required), G (group), T (TLS),
33. C (compressed), x (unknown), o (OS specific), E (exclude),
34. l (large), p (processor specific)
重定位节
重定位节 '.rela.text’节中一共有8个条目,给出了偏移量、信息、类型、符号值、符号名称,加数等信息。
重定位节 ‘.rela.eh_frame’ 中一共有1个条目,同样有各种信息
符号表
.symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
symtab符号表不包含局部变量的条目。
4.4 Hello.o的结果解析
objdump -d -r hello.o
分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
1. Disassembly of section .text:
2.
3. 0000000000000000 <main>:
4. 0: 55 push %rbp
5. 1: 48 89 e5 mov %rsp,%rbp
6. 4: 48 83 ec 20 sub $0x20,%rsp
7. 8: 89 7d ec mov %edi,-0x14(%rbp)
8. b: 48 89 75 e0 mov %rsi,-0x20(%rbp)
9. f: 83 7d ec 03 cmpl $0x3,-0x14(%rbp)
10. 13: 74 16 je 2b <main+0x2b>
11. 15: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 1c <main+0x1c>
12. 18: R_X86_64_PC32 .rodata-0x4
13. 1c: e8 00 00 00 00 callq 21 <main+0x21>
14. 1d: R_X86_64_PLT32 puts-0x4
15. 21: bf 01 00 00 00 mov $0x1,%edi
16. 26: e8 00 00 00 00 callq 2b <main+0x2b>
17. 27: R_X86_64_PLT32 exit-0x4
18. 2b: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
19. 32: eb 3b jmp 6f <main+0x6f>
20. 34: 48 8b 45 e0 mov -0x20(%rbp),%rax
21. 38: 48 83 c0 10 add $0x10,%rax
22. 3c: 48 8b 10 mov (%rax),%rdx
23. 3f: 48 8b 45 e0 mov -0x20(%rbp),%rax
24. 43: 48 83 c0 08 add $0x8,%rax
25. 47: 48 8b 00 mov (%rax),%rax
26. 4a: 48 89 c6 mov %rax,%rsi
27. 4d: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 54 <main+0x54>
28. 50: R_X86_64_PC32 .rodata+0x1a
29. 54: b8 00 00 00 00 mov $0x0,%eax
30. 59: e8 00 00 00 00 callq 5e <main+0x5e>
31. 5a: R_X86_64_PLT32 printf-0x4
32. 5e: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 64 <main+0x64>
33. 60: R_X86_64_PC32 sleepsecs-0x4
34. 64: 89 c7 mov %eax,%edi
35. 66: e8 00 00 00 00 callq 6b <main+0x6b>
36. 67: R_X86_64_PLT32 sleep-0x4
37. 6b: 83 45 fc 01 addl $0x1,-0x4(%rbp)
38. 6f: 83 7d fc 09 cmpl $0x9,-0x4(%rbp)
39. 73: 7e bf jle 34 <main+0x34>
40. 75: e8 00 00 00 00 callq 7a <main+0x7a>
41. 76: R_X86_64_PLT32 getchar-0x4
42. 7a: b8 00 00 00 00 mov $0x0,%eax
43. 7f: c9 leaveq
44. 80: c3 retq
(此处为Y86指令集数字和寄存器之间的关系)
机器语言和汇编语言的映射
机器语言的构成,
机器的指令指令是CPU能直接识别并执行的指令,它的表现形式是二进制编码。
通常由操作码和操作数两部分组成,操作码指出该指令所要完成的操作,即指令的功能,操作数指出参与运算的对象,以及运算结果所存放的位置等。
汇编前后代码的区别
经过汇编之后,hello.o得到了重定位信息,符号表等ELF格式信息, 使得程序发生了如下几种变换
- 为了便于机器执行程序,所有的操作数都由常见的十进制变为了较为适宜机器的16进制
- 对于函数的调用,在汇编前的代码中是直接调用某个函数,而经过汇编后主要通过PC(程序计数器)的转移到一个相对当前指令便宜的某一个地址
- 对于跳转,和函数调用类似,也由之前的调转到某一个标签变为了跳转到某一个偏移地址
- 对于全局变量的引用,某些全局变量因为他们的地址需要在运行后才能确定,所以访问需要重定位;在汇编后的代码,所以在汇编后,这些操作数都被置于零,添加重定位条目。
4.5 本章小结
汇编阶段。汇编器(as)将hello.s翻译成机器语言指令,为P2P过程中的链接做了准备,链接后即可生成可执行文件,本章节介绍了Linux下的汇编和可重定向文件 ,和经过汇编前后代码的一些区别。
第5章 链接
5.1 链接的概念与作用
链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。执行和其它可重定位目标文件的链接后可以得到我们需要的可执行文件
5.2 在Ubuntu下链接的命令
ld hello.o -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 /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
ELF头
ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。
1. 节头:
2. [号] 名称 类型 地址 偏移量
3. 大小 全体大小 旗标 链接 信息 对齐
4. [ 0] NULL 0000000000000000 00000000
5. 0000000000000000 0000000000000000 0 0 0
6. [ 1] .interp PROGBITS 0000000000400270 00000270
7. 000000000000001c 0000000000000000 A 0 0 1
8. [ 2] .note.ABI-tag NOTE 000000000040028c 0000028c
9. 0000000000000020 0000000000000000 A 0 0 4
10. [ 3] .hash HASH 00000000004002b0 000002b0
11. 0000000000000034 0000000000000004 A 5 0 8
12. [ 4] .gnu.hash GNU_HASH 00000000004002e8 000002e8
13. 000000000000001c 0000000000000000 A 5 0 8
14. [ 5] .dynsym DYNSYM 0000000000400308 00000308
15. 00000000000000c0 0000000000000018 A 6 1 8
16. [ 6] .dynstr STRTAB 00000000004003c8 000003c8
17. 0000000000000057 0000000000000000 A 0 0 1
18. [ 7] .gnu.version VERSYM 0000000000400420 00000420
19. 0000000000000010 0000000000000002 A 5 0 2
20. [ 8] .gnu.version_r VERNEED 0000000000400430 00000430
21. 0000000000000020 0000000000000000 A 6 1 8
22. [ 9] .rela.dyn RELA 0000000000400450 00000450
23. 0000000000000030 0000000000000018 A 5 0 8
24. [10] .rela.plt RELA 0000000000400480 00000480
25. 0000000000000078 0000000000000018 AI 5 19 8
26. [11] .init PROGBITS 0000000000401000 00001000
27. 0000000000000017 0000000000000000 AX 0 0 4
28. [12] .plt PROGBITS 0000000000401020 00001020
29. 0000000000000060 0000000000000010 AX 0 0 16
30. [13] .text PROGBITS 0000000000401080 00001080
31. 0000000000000131 0000000000000000 AX 0 0 16
32. [14] .fini PROGBITS 00000000004011b4 000011b4
33. 0000000000000009 0000000000000000 AX 0 0 4
34. [15] .rodata PROGBITS 0000000000402000 00002000
35. 0000000000000030 0000000000000000 A 0 0 4
36. [16] .eh_frame PROGBITS 0000000000402030 00002030
37. 00000000000000fc 0000000000000000 A 0 0 8
38. [17] .dynamic DYNAMIC 0000000000403e50 00002e50
39. 00000000000001a0 0000000000000010 WA 6 0 8
40. [18] .got PROGBITS 0000000000403ff0 00002ff0
41. 0000000000000010 0000000000000008 WA 0 0 8
42. [19] .got.plt PROGBITS 0000000000404000 00003000
43. 0000000000000040 0000000000000008 WA 0 0 8
44. [20] .data PROGBITS 0000000000404040 00003040
45. 0000000000000008 0000000000000000 WA 0 0 4
46. [21] .comment PROGBITS 0000000000000000 00003048
47. 0000000000000023 0000000000000001 MS 0 0 1
48. [22] .symtab SYMTAB 0000000000000000 00003070
49. 0000000000000498 0000000000000018 23 28 8
50. [23] .strtab STRTAB 0000000000000000 00003508
51. 0000000000000150 0000000000000000 0 0 1
52. [24] .shstrtab STRTAB 0000000000000000 00003658
53. 00000000000000c5 0000000000000000 0 0 1
54.
节头表
节头部表描述了不同节的位置和大小,目标文件中每个节都有一个固定大小的条目。
程序头
段节
动态部分
重定位节
动态符号表
1. Symbol table '.symtab' contains 49 entries:
2. Num: Value Size Type Bind Vis Ndx Name
3. 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
4. 1: 0000000000400270 0 SECTION LOCAL DEFAULT 1
5. 2: 000000000040028c 0 SECTION LOCAL DEFAULT 2
6. 3: 00000000004002b0 0 SECTION LOCAL DEFAULT 3
7. 4: 00000000004002e8 0 SECTION LOCAL DEFAULT 4
8. 5: 0000000000400308 0 SECTION LOCAL DEFAULT 5
9. 6: 00000000004003c8 0 SECTION LOCAL DEFAULT 6
10. 7: 0000000000400420 0 SECTION LOCAL DEFAULT 7
11. 8: 0000000000400430 0 SECTION LOCAL DEFAULT 8
12. 9: 0000000000400450 0 SECTION LOCAL DEFAULT 9
13. 10: 0000000000400480 0 SECTION LOCAL DEFAULT 10
14. 11: 0000000000401000 0 SECTION LOCAL DEFAULT 11
15. 12: 0000000000401020 0 SECTION LOCAL DEFAULT 12
16. 13: 0000000000401080 0 SECTION LOCAL DEFAULT 13
17. 14: 00000000004011b4 0 SECTION LOCAL DEFAULT 14
18. 15: 0000000000402000 0 SECTION LOCAL DEFAULT 15
19. 16: 0000000000402030 0 SECTION LOCAL DEFAULT 16
20. 17: 0000000000403e50 0 SECTION LOCAL DEFAULT 17
21. 18: 0000000000403ff0 0 SECTION LOCAL DEFAULT 18
22. 19: 0000000000404000 0 SECTION LOCAL DEFAULT 19
23. 20: 0000000000404040 0 SECTION LOCAL DEFAULT 20
24. 21: 0000000000000000 0 SECTION LOCAL DEFAULT 21
25. 22: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
26. 23: 0000000000000000 0 FILE LOCAL DEFAULT ABS
27. 24: 0000000000403e50 0 NOTYPE LOCAL DEFAULT 17 __init_array_end
28. 25: 0000000000403e50 0 OBJECT LOCAL DEFAULT 17 _DYNAMIC
29. 26: 0000000000403e50 0 NOTYPE LOCAL DEFAULT 17 __init_array_start
30. 27: 0000000000404000 0 OBJECT LOCAL DEFAULT 19 _GLOBAL_OFFSET_TABLE_
31. 28: 00000000004011b0 1 FUNC GLOBAL DEFAULT 13 __libc_csu_fini
32. 29: 0000000000404044 0 NOTYPE WEAK DEFAULT 20 data_start
33. 30: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.2.5
34. 31: 0000000000404040 4 OBJECT GLOBAL DEFAULT 20 sleepsecs
35. 32: 0000000000404048 0 NOTYPE GLOBAL DEFAULT 20 _edata
36. 33: 00000000004011b4 0 FUNC GLOBAL HIDDEN 14 _fini
37. 34: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@@GLIBC_2.2.5
38. 35: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
39. 36: 0000000000404044 0 NOTYPE GLOBAL DEFAULT 20 __data_start
40. 37: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getchar@@GLIBC_2.2.5
41. 38: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
42. 39: 000000000040202c 4 OBJECT GLOBAL DEFAULT 15 _IO_stdin_used
43. 40: 0000000000401150 93 FUNC GLOBAL DEFAULT 13 __libc_csu_init
44. 41: 0000000000404048 0 NOTYPE GLOBAL DEFAULT 20 _end
45. 42: 0000000000401140 1 FUNC GLOBAL HIDDEN 13 _dl_relocate_static_pie
46. 43: 0000000000401110 43 FUNC GLOBAL DEFAULT 13 _start
47. 44: 0000000000404048 0 NOTYPE GLOBAL DEFAULT 20 __bss_start
48. 45: 0000000000401080 129 FUNC GLOBAL DEFAULT 13 main
49. 46: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@@GLIBC_2.2.5
50. 47: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sleep@@GLIBC_2.2.5
51. 48: 0000000000401000 0 FUNC GLOBAL HIDDEN 11 _init
5.4 hello的虚拟地址空间
使用EDB打开hello可执行文件,在Data Dump区可以看到Hello的虚拟空间
即为0x0000000000401000-0x0000000000402000
5.5 链接的重定位过程分析
1.对于包含的函数:在hello.o反汇编生成的代码中,只有main函数
而在链接后生成的反汇编代码中,出现了调用的其他函数,比如
printf,puts,getchar,exit,sleep等
2.对于函数调用和跳转:在hello.o反汇编生成的代码中,跳转都是一个偏移量,并在后面加上重定位条目,而在链接后生成的反汇编代码中,函数调用和跳转拥有了实际上的虚拟地址和函数
链接前的函数调用,调用地址为空,添加重定位条目
链接后的函数调用,拥有了实际的虚拟地址和函数名,不再需要重定位条目
3.增加了节:在hello.o反汇编生成的代码中,只有.text一个节
而在链接后生成的反汇编代码中,又添加了.init节和.fini节
4.地址引用和全局变量的引用:在hello.o反汇编生成的代码中,对全局变量的引用是通过重定位符号来描述的
而在链接后生成的反汇编代码中,随着链接的完成
有些需要在运行时确定的地址得到确定的变量被确定了虚拟内存地址
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
程序地址 程序名
0000000000401000 <_init>:
0000000000401030 puts@plt:
0000000000401040 printf@plt:
0000000000401050 getchar@plt:
0000000000401060 exit@plt:
0000000000401070 sleep@plt:
0000000000401080 :
0000000000401110 <_start>:
0000000000401140 <_dl_relocate_static_pie>:
0000000000401150 <_libc_csu_init>
00000000004011b0 <_libc_csu_fini>
00000000004011b4 <_fini>:
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
对动态链接项目:global_offset table(GOT):
可知GOT的地址为0x404000,在执行dl_init前包含16个为0的字节
dl_init()前:
dl_init()后:
经过EDB调试发现:0x404000处的GOT中的16个字节在dl_init操作执行后被赋上相应的数值,其数值为相应的内存地址偏移量,则dl_init操作包含将GOT表在的偏移量数值加载到内存中相应的位置中去。
抄出数值:为 0x7f81b348f190和0x7f81b347a200,分别对应GOT[1]和GOT[2]
GOT[1] 包含动态链接器在解析函数地址时会使用的信息,对应0x7f81b348f190
GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,对应0x7f81b347a200
5.8 本章小结
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。执行和其它可重定位目标文件的链接后可以得到我们需要的可执行文件,本章主要介绍了Linux下的链接,可执行目标文件hello的格式和链接的重定位过程与Hello的动态链接过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的,上下文(context)中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
进程的作用是给在在运行一个程序时,得到一个假象,就好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条地执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象都是通过进程的概念提供给我们的。
6.2 简述壳Shell-bash的作用与处理流程
Shell的作用
shell是一种命令解释器,指为操作系统的使用者提供操作界面,它接收用户命令,然后调用相应的应用程序,交互式解释和执行用户输入的命令或者自动地解释和执行预先设定好的一连串的命令
shell-bash的处理流程
1.读取用户由键盘输入的命令行,对其进行语法检查,如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息
2.如果是内部命令直接执行
3.若不是内部命令,则是可执行程序,分析命令,以命令名作为文件名,并将其它参数改造为系统调用execve( )内部处理所要求的形式,传给Linux内核。
4.终端进程调用fork( )建立一个子进程。
5.终端进程本身调用wait4()来等待子进程完成(如果是后台命令,则不等待)。当子进程运行时调用execve(),子进程根据文件名到目录中查找有关文件,调入内存,执行这个程序。
6.如果命令末尾有&,则终端进程不用执行系统调用wait4(),立即发提示符,让用户输入下一条命令;否则终端进程会一直等待,当子进程完成工作后,向父进程报告,此时中断进程醒来,作必要的判别工作后,终端发出命令提示符,重复上述处理过程。
6.3 Hello的fork进程创建过程
当shell读取到执行hello程序时候,shell会分析命令,以命令名作为文件名,并将其它参数改造为系统调用execve( )内部处理所要求的形式,传给Linux内核。
然后终端进程调用fork( )建立一个子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
最后终端进程本身调用waitpid()来等待子进程完成(如果是后台命令,则不等待)。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。
execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到hello,execve 才会返回到调用程序。所以,与fork一次调用返回两次不同,execve调用一次并从不返回。
在execve加载了hello之后,调用启动代码libc_start_main。启动代码设置栈,并将控制传递给新程序的主函数。
execve过程执行以下内容:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss 和栈区域创建新的区域结构。所有这些
3.新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的. text和.data区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。概括了私有区域的不同映射。
4.映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
5.设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
hello是一个进程,在执行的时得到一个抽象,就好像hello是系统中当前运行的唯一的程序一样。Hello拥有一个独立的逻辑控制流,就像程序独占地使用处理器,同时hello拥有一个私有的地址空间,就像我们的程序独占地使用内存系统。
逻辑控制流和时间片
内核为每个进程(例如hello)维持一个上下文(context)。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用- -种称为上下文切换的机制来将控制转移到新的进程,
上下文切换的过程
1)保存当前进程的上下文,
2)恢复某个先前被抢占的进程被保存的上下文,
3)将控制传递给这个新恢复的进程。
一个逻辑流的执行在时间上与另一个流重叠,称为并发流;这两个流被称为并发地运行。
进程时间片
多个流并发地执行的一般现象被称为并发。一个进程和其他进程轮流运行的概念称为多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。
上下文切换的过程
用户态与核心态转换
当内核代表用户执行系统调用时,可能会发生上下文切换。如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程。比如,如果一个read系统调用需要访问磁盘,内核可以选择执行上下文切换,运行另外一个进程,而不是等待数据从磁盘到达。另一个示例是sleep系统调用,它显式地请求让调用进程休眠。般而言, 即使系统调用没有阻塞,内核也可以决定执行上下文切换,而不是将控制返回给调用进程。
运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷人系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
异常:
在hello程序中和shell的执行中
会出现:
异常
1.中断:当输入Ctrl-Z,进程收到信号SIGSTP,为暂停运行,直到收到SIGCONT
2.终止:当输如Ctrl-C,信号SIGINT,进程终止
信号:
信号SIGSTP:通过Ctrl-Z,为暂停运行直到收到下一个SIGCONT
信号SIGINT:通过Ctrl-C,进程终止
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
- 直接运行,不再执行任何操作:
程序会正常运行直到结束,程序结束后进程被回收
2. 运行后输入Ctrl-C
shell收到来自键盘输入的终止信号,经过信号处理程序后终止hello进程,并回收进程空间
3. 运行后输入Ctrl-Z
shell收到来自键盘输入的SIGSTP,暂停hello进程运行直到收到下一个SIGCONT
- 进程暂停后输入jobs
会打印当前shell执行的进程的pid,状态,和名称
5.进程暂停后输入ps
ps命令会打印出当前系统的的进程的各种信息
6.进程暂停后输入pstree
输入pstree指令后会查看进程树之间的关系,即哪个进程是父进程,哪个是子进程,可以清楚的看出来是谁创建了谁。
7. 进程暂停后输入fg
fg指令将本来在后台挂起的hello进程恢复执行
- 进程暂停后输入kill指令
首先在进程暂停后利用ps获取hello的pid
然后输入kill -9 -pid来发送信号给hello进程使其无条件终止
9.进程运行过程中不停乱按键盘
在运行过程中输入abcdefghijk……发现输入不会影响进程的运行,当按到回车键时,之前输入的字符会被读入缓冲区等待getchar处理,回车后再输入的字符会被当做是输入shell的命令。
6.7本章小结
本章主要介绍了hello的进程管理有关的内容:首先介绍了进程和shell
的概念和作用,和hello如何被fork与execve执行,最后介绍了hello的异常与信号的处理,结合hello的运行和执行各种操作观察执行的结果。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:
是指由程序产生的与段相关的偏移地址部分。用段基址(段地址)和段内偏移量(偏移地址)来表示,段基址确定它所在的段居于整个存储空间的位置,偏移量确定它在段内的位置。
线性地址:
如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间,为了简化讨论,我们总是假设使用的是线性地址空间。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
虚拟地址:
在一个带虚拟内存的系统.中,CPU从一个有N=2^n个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间。
物理地址:
计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组,每个字节都对应具体的物理地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
在段式存储管理中,将程序的地址空间划分为若干个段(segment),这样每个进程有一个二维的地址空间。在前面所介绍的动态分区分配方式中,系统为整个进程分配一个连续的内存空间。而在段式存储管理系统中,则为每个段分配一个连续的分区,而进程中的各个段可以不连续地存放在内存的不同分区中。程序加载时,操作系统为所有段分配其所需内存,这些段不必连续,物理内存的管理采用动态分区的管理方法。
在为某个段分配物理内存时,可以采用首先适配法、下次适配法、最佳适配法等方法。
在回收某个段所占用的空间时,要注意将收回的空间与其相邻的空间合并。
段式存储管理也需要硬件支持,实现逻辑地址到物理地址的映射。
程序通过分段划分为多个模块,如代码段、数据段、共享段:
Linux x86-64内存映像
–可以分别编写和编译
–可以针对不同类型的段采取不同的保护
–可以按段为单位来进行共享,包括通过动态链接进行代码共享
这样做的优点是:可以分别编写和编译源程序的一个文件,并且可以针对不同类型的段采取不同的保护,也可以按段为单位来进行共享。
总的来说,段式存储管理的优点是:没有内碎片,外碎片可以通过内存紧缩来消除;便于实现内存共享。缺点与页式存储管理的缺点相同,进程必须全部装入内存。
在段式管理系统中,整个进程的地址空间是二维的,即其逻辑地址由段号和段内地址两部分组成。为了完成进程逻辑地址到物理地址的映射,处理器会查找内存中的段表,由段号得到段的首地址,加上段内地址,得到实际的物理地址。这个过程也是由处理器的硬件直接完成的,操作系统只需在进程切换时,将进程段表的首地址装入处理器的特定寄存器当中。这个寄存器一般被称作段表地址寄存器。
7.3 Hello的线性地址到物理地址的变换-页式管理
在一个带虚拟内存的系统.中,CPU从一个有N=2^n个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间。
概念上而言,虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每字节都有一个唯一的虚拟地址,作为到数组的索引。磁盘上数组的内容被缓存在主存中。和存储器层次结构中其他缓存一样,磁盘(较低层)上的数据被分割成块,这些块作为磁盘和主存(较高层)之间的传输单元。VM系统通过将虛拟内存分割为称为虛拟页的 大小固定的块来处理这个问题。每个虚拟页的大小为P=2^p字节。类似地,物理内存被分割为物理页,大小也为P字节(物理页也被称为页帧)。
下图展示了MMU如何利用页表来实现这种映射。CPU中的-一个控制寄存器,页表基址寄存器指向当前页表。n位的虚拟地址包含两个部分:一个p位的虛拟页面偏移和一个(n- p)位的虚拟页号。MMU利用VPN来选择适当的PTE。例如,VPN 0选择PTE 0,VPN 1选择PTE 1,以此类推。将页表条目中物理页号(Physical Page Number, PPN)和虚拟地址中的VPO串联起来,就得到相应的物理地址。注意,因为物理和虚拟页面都是P字节的,所以物理页面偏移(Physical Page Offset,PPO)和 VPO是相同的。
在任意时刻,虚拟页面的集合都分为三个不相交的子集:
1.未分配的: VM系统还未分配(或者创建)的页。未分配的块没有任何数据和它们相关联,因此也就不占用任何磁盘空间。
2.缓存的:当前已缓存在物理内存中的已分配页。
3.未缓存的:未缓存在物理内存中的已分配页。.
线性地址到物理地址的变换
第1步:处理器生成一个虚拟地址,并把它传送给MMU。
第2步:MMU生成PTE地址,并从高速缓存/主存请求得到它
第3步:高速缓存/主存向MMU返回PTE。
如果命中:
第4步:MMU构造物理地址,并把它传送给高速缓存/主存。
第5步:高速缓存/主存返回所请求的数据字给处理器。
如果不命中:
第4步:PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
第5步:缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘。
第6步:缺页处理程序页面调人新的页面,并更新内存中的PTE。.
第7步:缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虛拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,主存就会将所请求字返回给处理器。
7.4 TLB与四级页表支持下的VA到PA的变换
对Intel Core i7处理器来说:
如图所示:首先取出48位虚拟地址,对其进行划分,其中前36位位VPN(虚拟页号),后12位位VPO(虚拟页偏移),而在TLB内存翻译加速下,VPN又可以划分位32位的TLBT和4位的TLBI,首先4位的TLBI确定VPN在TLB中对应的组,而后在组中直接寻找匹配的TLBT,如果得到匹配,TLB命中,直接取出对位置的PPN和VPO组合生成物理地址,如果没有命中,VPN再到多级页表PTE中逐级查询对应的PPN,然后和VPO组合生成物理地址。
7.5 三级Cache支持下的物理内存访问
如7.4中的图片所示:先将52位的物理地址划分位40位的块标记,6位的组索引,和6位的块偏移,首先根据6位的组索引确定对应的组号,在族中在进行对应块标记的匹配,如果匹配到对应的块,再根据块偏移从当前的cache块中对应位置调出到CPU,如果没有匹配到,则到下一级的缓存中调入对应的cache块,放入当前级即可。
7.6 hello进程fork时的内存映射
当终端进程调用fork函数创建子进程hello的时候,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,内核为新进程创建了虚拟内存,对父进程的页表,子进程的每个页面都被标记为只读。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虛拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve 函数在当前进程中加载并运行包含在可执行目标文件hello时中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss 和栈区域创建新的区域结构。所有这些
3.新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的. text和.data区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。概括了私有区域的不同映射。
4.映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
5.设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
假设MMU在试图翻译某个虛拟地址A时,触发了一个缺页。这个异常导致控制转移到内核的缺页处理程序,处理程序随后就执行下面的步骤:
1)检查虚拟地址的合法性
缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start 和vm_end做比较。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止这个进程。
2)检查进程是否有读、写或者执行这个区域内页面的权限
例如,这个缺页是不是由一条试图对这个代码段里的只读页面进行写操作的存储指令造成的?这个缺页是不是因为一个运行在用户模式中的进程试图从内核虚拟内存中读取字造成的?
如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程
3)如果这个缺页是由于对合法的虚拟地址进行合法的操作造成的。它将处理这个缺页:
首先选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU。这次,MMU就能正常地翻译A,而不会再产生缺页中断了。
当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。缺少的页面已经缓存在主存中了,那么页命中也能由地址翻译硬件正常处理了
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap),假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk(读做“break"),它指向堆的顶部,分配器将堆视为–组不同大小的块(block)的集合来维护。每个块就是一一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
1.显式分配器(explicit allocator),要求应用显式地释放任何已分配的块。例如,C标准库提供一-种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放-一个块。C++中的new和delete操作符与C中的malloc和free相当。
2.隐式分配器(implicitallocator),另–方面,要求分配器检测-一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbage collecor)而自动释放未使用的已分配的块的过程叫做垃圾收集( garbage collection)。
例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
对于显式分配器必须在一些相当严格的约束条件下工作:
1.处理任意请求序列。一个应用可以有任意的分配请求和释放请求序列,只要满足约束条件:每个释放请求必须对应于一个当前已分配块,这个块是由一个以前的分配请求获得的。因此,分配器不可以假设分配和释放请求的顺序。例如,分配器不能假设所有的分配请求都有相匹配的释放请求,或者有相匹配的分配和空闲请求是嵌套的。
2.立即响应请求。分配器必须立即响应分配请求。因此,不允许分配器为了提高性能重新排列或者缓冲请求。
3.只使用堆。为了使分配器是可扩展的,分配器使用的任何非标量数据结构都必须保存在堆里。
4.对齐块(对齐要求)。分配器必须对齐块,使得它们可以保存任何类型的数据对象。
5.不修改已分配的块。分配器只能操作或者改变空闲块。特别是,-旦块被分配了,就不允许修改或者移动它了。因此,诸如压缩已分配块这样的技术是不允许使用的。
7.10本章小结
本章主要介绍了Hello运行后的储存管理有关的内容,是hello的O2O过程中的部分,主要分为hello的物理和虚拟储存器地址,和TLB与四级页表的翻译,和3级Cache对hello运行时候的加速,以及hello进程在被shell fork和execve时内存的映射,和调用某些函数(例如printf)时候的动态内内存分配等问题
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
一个Linux文件就是一个m个字节的序列:
Bo,B1,B2……Bm-1
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输人和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输人和输出都能以一种统一且一致的方式来执行:
8.2 简述Unix IO接口及其函数
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
进程是通过调用open函数来打开一个已存在的文件或者创建一个新文件的:
open函数将filename转换为-一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件.
2.Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件< unistd.h>定义了常量STDIN_FILENO、STDOUT 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。
应用程序是通过分别调用read和write函数来执行输入和输出的。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
进程通过调用close函数关闭一个打开的文件。
函数汇总:
1.open函数:
进程是通过调用open函数来打开一个已存在的文件或者创建一个新文件的:
open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件:
1)O_RDONLY:只读
2)O_WRONLY:只写
3)O_RDWR:可读可写
2.read和write函数:应用程序是通过分别调用read和write函数来执行输入和输出的。
read函数从描述符为fd 的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。
write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
- close函数:进程通过调用close函数关闭一个打开的文件。
关闭一个已关闭的描述符会出错。
4.lseek函数,应用程序能够通过调用lseek函数显示地修改当前文件的位置
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
https://www.cnblogs.com/pianist/p/3315801.html
先看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) 表示的是…中的第一个参数。
再看vsprintf(buf, fmt, arg)函数:
1. int vsprintf(char *buf, const char *fmt, va_list args)
2. {
3. char* p;
4. char tmp[256];
5. va_list p_next_arg = args;
6.
7. for (p=buf;*fmt;fmt++) {
8. if (*fmt != '%') {
9. *p++ = *fmt;
10. continue;
11. }
12.
13. fmt++;
14.
15. switch (*fmt) {
16. case 'x':
17. itoa(tmp, *((int*)p_next_arg));
18. strcpy(p, tmp);
19. p_next_arg += 4;
20. p += strlen(tmp);
21. break;
22. case 's':
23. break;
24. default:
25. break;
26. }
27. }
28.
29. return (p - buf);
30. }
vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
write系统函数:
1. write:
2. mov eax, _NR_write
3. mov ebx, [esp + 4]
4. mov ecx, [esp + 8]
5. int INT_VECTOR_SYS_CALL
再找到INT_VECTOR_SYS_CALL的实现:
init_idt_desc(INT_VECTOR_SYS_CALL, DA_386IGate, sys_call, PRIVILEGE_USER);
int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数
1. sys_call:
2. call save
3.
4. push dword [p_proc_ready]
5.
6. sti
7.
8. push ecx
9. push ebx
10. call [sys_call_table + eax * 4]
11. add esp, 4 * 3
12.
13. mov [esi + EAXREG - P_STACKBASE], eax
14.
15. cli
16.
17. ret
于是我们可知直到printf函数执行流程如下:
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
getchar是读入函数的一种。它从标准输入里读取下一个字符,相当于getc(stdin)。返回类型为int型,为用户输入的ASCII码或EOF。
getchar函数原型:可以发现其调用了系统函数read,通过系统调用读取按键ascii码,直到接受到回车键才返回。
1. int getchar(void)
2. {
3. static char buf[BUFSIZ];
4. static char* bb=buf;
5. static int n=0;
6. if(n==0)
7. {
8. n=read(0,buf,BUFSIZ);
9. bb=buf;
10. }
11. return (--n>=0)?(unsigned char)*bb++:EOF;
12. }
getchar可用宏实现:
#define getchar() getc(stdin)。
getchar有一个int型的返回值。当程序调用getchar时.程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdin流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才等待用户按键。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了Linux下IO设备的管理,和Linux的接口的简述,又介绍了printf和getchar这两个输出函数的实现,在hello的O2O过程中占最后的输出部分,但也是很重要。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
Hello这个程序从c语言代码开始,作为高级语言,C语言让人能够很好的理解程序要做什么,对机器来说却不是很友好,而整个编译的过程就是让生成机器能够理解的机器码——一个有限长的0,1字符串;首先我们需要预处理阶段,让我们能够方便的调用一些已经封装打包好的库函数,作为程序员我们不需要具体的理解它怎么实现而是知道它的输入输出机制。经过预处理阶段后生成机器更容易理解的汇编代码——一种助记符代替机器指令的操作码语言,而后汇编阶段再将汇编代码变成由机器码组成的可重定位目标文件,而这个可执行文件中有许多地方是空缺而需要重定位的,就需要我们通过连接把它们从蓝图变为现实——机器可以直接运行的二进制可执行文件。
而整个编译的阶段也像是计算机科学进步的阶段:从0,1打孔到机器码再到汇编等低级语言,最后到能让更多人理解的高级语言。在未来如果又出现了一种比高级语言更为先进的语言种类,高级语言也可能会变成未来编译的一个中间语言吧。
在生成可执行文件后,hello的旅途还没有结束,如果说它的前半段是P2P的话,那么后半段就是O2O了,在shell里面OS为它调度给它以施展身手的舞台,从fork再execve,分配虚拟内存,往来不息的信号精准控制着一个又一个进程,机器背后的各种各样的硬件更是为了让hello能够以更快的速度运行竭尽所能:CPU用川流不息的流水线马不停蹄的运行一条又一条指令,TLB、多级页表、3级Cache,页面文件让hello进程在处理器缓存内存硬盘中来往自如,I/O管理让hello能够以一种更直观的方式来到我们身边….
这就是hello的一生。
通过一学期对计算机系统的深入学习,使我真正地能够用系统的思维去解决问题和优化问题的解决方案,遇到问题能够从程序员的角度来思考,如何能够利用系统知识来编写出更好的程序,同时学习一个高效的计算机系统应该做些什么。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c hello程序源代码
hello.i:经过预处理的hello.c生成的代码文本
hello.s:hello.i 经过编译过程生成的汇编代码
hello.o: hello.s经过汇编之后生成的可重定位目标文件
hello :经过链接后的可执行文件
hello_objd.txt:链接后的hello可执行文件经过反汇编生成的代码
ELFout.txt:链接后的hello可执行文件经过readelf读取的ELF信息
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] (美)布赖恩特(Bryant,R.E.)深入理解计算机系统(第三版) 机械工业出版社2015
[2] [转]printf 函数实现的深入剖析
https://www.cnblogs.com/pianist/p/3315801.html
[3]百度百科getchar (计算机语言函数) https://baike.baidu.com/item/getchar/919709?fr=aladdin
.
[2] 南方铁匠,操作系统内存管理——分区、页式、段式管理、段页式2017-11-13 https://blog.csdn.net/hit_shaoqi/article/details/78516508
(参考文献0分,缺失 -1分)