计算机系统
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 120L022013
班 级 2003007
学 生 王炜栋
指 导 教 师 吴锐
计算机科学与技术学院
2021年5月
本文阐述了hello.c程序,从源代码到可执行目标文件,再到执行进程,最终被终止并回收的过程。具体描述了它的处理全过程,包括P2P(program to progress)和020.全面阐释了计算机的底层实现
关键词:预处理;编译;汇编语言;机器语言;可重定位目标文件ELF格式;链接;重定位;动态链接;shell;进程;存储;虚拟内存;I/O;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
P2P:Program:用户在编辑器或IDE中键入代码得到程序。Process:在Linux中,hello.c经过C预处理其cpp的预处理成为文本文件hello.i, C编译器ccl的编译成为汇编文件hello.s,汇编其as的汇编成为二进制可重定 位目标文件hello.o,最终经过 ld的链接,成为可执行程序 hello。
020:O2O: From Zero-0 to Zero-0;OS进程管理通过fork和execve产生子进程和加载并运行程序,进行虚拟内存的映射。CPU通过取指,译码,执行,访存,写回,更新PC的操作,一条一条执行指令。涉及到输入输出时,IO管理对函数进行处理。运行结束后,shell回收hello进程,并消除相关的数据等痕迹。
1.2 环境与工具
硬件工具:X64 Intel Core i5-8300H CPU,2.30GHz,8G RAM,512GHD DISK
软件工具:Windows10 64位,VMware Workstation 16 Pro,Ubuntu 20.04.2.0
开发与调试工具:Codeblocks,gcc gdb,vim,readelf,objdump等
1.3 中间结果
Hello.s .i编译后得到的汇编语言文件
Hello.o .s汇编后得到的可重定位目标文件
Hello.elf hello.o的elf文件
Hello.asm .o经过反汇编生成的汇编语言文件
Hello .o经过链接生成的可执行目标文件
Linkhello.elf Hello的elf文件
Obj_hello.s .o经过链接反汇编的汇编语言文件
Hello.c 源代码
1.4 本章小结
本章对hello进行了一个简介,并对其运行流程进行了一个总体的概括。然后介绍了所有操作的环境与工具,以及描述了中间产生的文件信息。
第2章 预处理
2.1 预处理的概念与作用
2.1.1:预处理的概念
C语言对源程序处理的四个步骤:预处理、编译、汇编、链接。预处理是在程序源代码被编译之前,由预处理器(Preprocessor)对程序源代码进行的处理。这个过程并不对程序的源代码语法进行解析,但它会把源代码分割或处理成为特定的符号为下一步的编译做准备工作。例如#include、#define、#if,分别可以在正式编译前进行引入外部库文件、进行宏定义、如果后接语句为真则添加#if与#endif之间的代码进行编译。
2.1.2:预处理的作用
1:宏定义(如#define) 2:文件包含(#include) 3:条件编译。(#if、#else、#endif)4.:删除注释
2.2在Ubuntu下预处理的命令
预处理命令:gcc -E -o hello.i hello.c
如图2.1所示
图2.1
2.3 Hello的预处理结果解析
进行预处理后,我们发现在文件夹中生成了“hello.i”文件,如图2.2所示
图2.2
我们发现经过预处理后,原本几行的代码扩充到了3060行,他的作用是处理了所有的预编译操作,即宏定义、文件包含、条件编译、删除注释;
接下来的3060行代码又将完成什么样的功能呢?
2.4 本章小结
本章借用Ubuntu的Terminal对Hello.c进行预编译的操作解释了预编译的概念和作用。通过解析hello.c与hello.i的差异直观比对出预编译的处理结果。
第3章 编译
3.1 编译的概念与作用
3.1.1编译的概念:
编译,就是把代码转化为汇编指令的过程,汇编指令只是CPU相关的,也就是说C代码和python代码,代码逻辑如果相同,编译完的结果其实是一样的。即将高级语言程序转为机器能读懂的语言,在本文中编译是将hello.i转为hello.s
3.1.2编译的作用:
- 主要作用:1.扫描(词法分析)2.语法分析:编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,方法分为两种:自上而下分析法和自下而上分析法。3.中间代码:源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码。4.代码优化:指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。5.目标代码:生成是编译的最后一个阶段。目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。此处指汇编语言代码,须经过汇编程序汇编后,成为可执行的机器语言代码。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
编译结果如下图3.1所示
图3.1
3.3 Hello的编译结果解析
3.3.1:文件结构
图3.2
表格 hello.s文件结构
内容 | 含义 |
.file | 源文件 |
.text | 代码段 |
.global | 全局变量 |
.data | 存放已经初始化的全局和静态C 变量 |
.section .rodata | 存放只读变量 |
.align | 对齐方式 |
.type | 表示是函数类型/对象类型 |
.size | 表示大小 |
.long .string | 表示是long类型/string类型 |
3.3.2:数据类型
1)String类型
图3.3
- 整数
1.立即数,通常用$n,n表示为定义立即数数值
图3.4
2.int型
如图3.4所示,hello中定义的int型数据占据了4字节大小的地址空间,且初始赋值为立即数$0
- 局部变量
通过观察下图3.5代码,发现hello.c中使用的计数功能临时变量i被储存在-4(%rbp)中,相当于在这里定义了一个局部变量。
图3.5
- 全局函数
图3.6
3.3.3:类型转换
如下图3.7所示,调用atoi函数将字符型转为int整型变量
图3.7
3.3.4:赋值操作
如图3.4,将立即数0赋值给地址为%rbp-4的变量i,其占用4个字节
3.3.5:数组(栈)操作
如图3.8,movq的作用是开辟一个32字节的空间用来存放数组,首地址-32(%rbp)存放在%rax中,每8个字节是一个数值,即-24(%rbp)就是数组argv[1];
图3.8
3.3.6:算术操作
如图3.9,addl可以用作向地址中所存数值加立即数,本图中为向-4(%rbp)中所存数值加1
图3.9
类似的,也可以通过addl或subq进行加减运算、地址偏移、开辟空间等操作
3.3.7:关系操作(比较跳转)
如图3.10,我们在程序中常常会遇到if、elif、else等条件函数,其汇编代码的呈现形式就是判别跳转,cmpl的作用是将后面两个参数进行比较、je表示相等跳转(jump equal)、还有类似的如js、ja、jne、jbe等
图3.10
3.3.8:函数操作
如下图3.11,汇编语言中已经编译成功的函数可以通过“call 函数名”的方式调用
图3.11
hello.c涉及的函数操作有:main、printf、exit、sleep 、getchar、
其中:
图3.12
main函数的参数是argc和argv[];两次printf函数的参数是那两个字符串;exit参数是1,sleep函数参数是atoi(argv[3])。函数的返回值存储在%eax寄存器中。
3.4 本章小结
本章主要介绍了编译阶段编译的基本概念和作用,通过在Ubuntu上对hello.i编译为hello.s的操作展示汇编代码,并以hello.s为例理解了一些汇编指令及代码,通过理解编译机制,我们也更容易理解高级语言程序的编译原理。
第4章 汇编
4.1 汇编的概念与作用
汇编语言定义:是一种用符号书写的、基本操作与机器指令相对应的、并遵循一定语法规则的计算机语言;
引入汇编语言的目的:为了克服机器语言的缺点,人们采用助记符表示机器指令的操作码,用变量代替操作数的存放地址等,这样就形成了汇编语言;
汇编源程序:用汇编语言所提供的指令(伪指令)编写的程序;
汇编程序:这种把汇编源程序翻译成目标程序的语言加工程序;
汇编指令:用便于记忆、并能描述指令功能的符号表示机器指令→汇编指令(符号指令);
助记符:表示指令操作码的符号,一般用英语单词或缩写。指令的操作数也用符号表示;
汇编语言的特点:汇编指令与机器指令一一对应,但相对机器语言易于理解、掌握,当我们用低级语言编写程序时使用汇编语言而不用机器语言。
汇编的概念:汇编语言是一种符号语言,比机器语言容易理解和掌握,也容易调试和维护。但是,汇编语言源程序要翻译成机器语言程序才可以由计算机执行。这个翻译的过程称为汇编;
汇编的作用:将汇编语言程序撰写为机器语言程序的翻译程序,使其连接后能使机器识别
4.2 在Ubuntu下汇编的命令
作用:将hello.s转为hello.o文件
图4.1
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
readelf -a hello.o >hello.elf,通过重定位将hello.o转为.elf文件
图4.2
打开.elf文件,由于截图限制展示部分.elf内容
图4.3
- ELF头:
以一个16位的Magic序列开始,描述生成该文件的字大小和字节顺序,剩下部分包括帮助链接器语法分析和解释目标文件的信息,还有节头部表中条目的大小和数量。
图4.4
- 节头部表:
记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。
图4.5
- 重定位节:
一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
偏移量:重定位入口的偏移,记录了需要重定位的项的位置;
信息:包含symbol与type两个信息,分别存在高8位与低16o位中,前者用于记录该符号在符号表中的索引,提供了需要插入到重定位位置的数据;后者用于记录符号的重定义类型,提供了重定位后的地址的计算方法;
类型:告知链接器应该如何修改新的应用;
符号值:符号值是每个符号待修改的新偏移量;
符号名称:给出需要重定位的符号的名称;
图4.6
- 符号表:
存放程序中定义的全局变量和函数的信息。name记录目标名称,value记录符号地址,size记录目标大小,type记录目标类型,是函数还是数据,bind表示全局还是本地。
图4.7
4.4 Hello.o的结果解析
执行objdump -d -r hello.o >hello.asm后得到hello.o的反汇编
图4.8
Disassembly of section .text:
0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 83 ec 20 sub $0x20,%rsp
c: 89 7d ec mov %edi,-0x14(%rbp)
f: 48 89 75 e0 mov %rsi,-0x20(%rbp)
13: 83 7d ec 04 cmpl $0x4,-0x14(%rbp)
17: 74 16 je 2f <main+0x2f>
19: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 20 <main+0x20>
1c: R_X86_64_PC32 .rodata-0x4
20: e8 00 00 00 00 callq 25 <main+0x25>
21: R_X86_64_PLT32 puts-0x4
25: bf 01 00 00 00 mov $0x1,%edi
2a: e8 00 00 00 00 callq 2f <main+0x2f>
2b: R_X86_64_PLT32 exit-0x4
2f: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
36: eb 48 jmp 80 <main+0x80>
38: 48 8b 45 e0 mov -0x20(%rbp),%rax
3c: 48 83 c0 10 add $0x10,%rax
40: 48 8b 10 mov (%rax),%rdx
43: 48 8b 45 e0 mov -0x20(%rbp),%rax
47: 48 83 c0 08 add $0x8,%rax
4b: 48 8b 00 mov (%rax),%rax
4e: 48 89 c6 mov %rax,%rsi
51: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 58 <main+0x58>
54: R_X86_64_PC32 .rodata+0x22
58: b8 00 00 00 00 mov $0x0,%eax
5d: e8 00 00 00 00 callq 62 <main+0x62>
5e: R_X86_64_PLT32 printf-0x4
62: 48 8b 45 e0 mov -0x20(%rbp),%rax
66: 48 83 c0 18 add $0x18,%rax
6a: 48 8b 00 mov (%rax),%rax
6d: 48 89 c7 mov %rax,%rdi
70: e8 00 00 00 00 callq 75 <main+0x75>
71: R_X86_64_PLT32 atoi-0x4
75: 89 c7 mov %eax,%edi
77: e8 00 00 00 00 callq 7c <main+0x7c>
78: R_X86_64_PLT32 sleep-0x4
7c: 83 45 fc 01 addl $0x1,-0x4(%rbp)
80: 83 7d fc 07 cmpl $0x7,-0x4(%rbp)
84: 7e b2 jle 38 <main+0x38>
86: e8 00 00 00 00 callq 8b <main+0x8b>
87: R_X86_64_PLT32 getchar-0x4
8b: b8 00 00 00 00 mov $0x0,%eax
90: c9 leaveq
91: c3 retq
以下说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等:
通过比较反汇编的代码和hello.s,可以发现二者在语句、语法、语义上没有过多区别,但反汇编代码除了显示汇编代码以外,还会显示机器代码。此外还有以下差异
- 分支转移:汇编代码的分支转移是通过形如L0、L1等助记符的段名称进行转移,而在机器语言中不通过段名称转移,而是通过确切的地址,表示为主函数加段内偏移。
- 函数调用:汇编代码的call后面直接跟函数的名称,而在反汇编代码中,call后面加由主函数加偏移量构成的下一条指令的地址,因为机器语言中调用的函数在共享库中,无法确定位置,所以相对位置为0,,在重定位表中为其设置偏移量,等待进一步确认。
- 一一映射关系:每一条汇编代码都可以用二进制机器指令表示,每一条机器指令由操作码和操作数构成,从而建立起一一对应关系。
4.5 本章小结
本章主要介绍了汇编的概念及作用、Ubuntu下终端的指令,通过对hello.o重定位到的.elf格式与之前的hello.s进行比对得出的差异进行分析后探究了汇编语言到机器语言的一一映射关系
第5章 链接
5.1 链接的概念与作用
链接的概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。
链接的作用:将程序从.o文件转化为可执行文件。使分离编译成为可能。
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
如图5.1所示,执行指令后生成了hello文件
图5.1
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
执行指令:
readelf -a hello > linkhello.elf
执行后如图5.2所示生成了linkhello.elf文件
图5.2
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
- Elf头:
ELF Header在编译后与原来的汇编后的表的格式是一模一样的,仍然以一个16字节大小的Magic头开始,给出了操作系统和编译器辨别此文件为ELF二进制库,但是会发现type从REL变为EXEC,节点数量变为27个
图5.3
- 节头部表:
Linkhello.elf中的节头包含了文件中出现的各个节的语义,包括节的类型、位置、偏移量和大小等信息。与hello.elf相比,其在链接之后的内容更加丰富详细
图5.4
- 重定位表:
重定位表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明
图5.5
- 符号表:
符号表存放程序中定义的函数和局部变量的信息
1.Name:符号名称2.Value:符号相对于目标节起始位置偏移3.Size:目标的大小4.Type:类型,全局变量或函数5.Bind:表明是本地的还是全局的
图5.6
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
使用edb打开hello可执行文件,可以在edb的Data Dump窗口看到hello的虚拟地址空间分配的情况,具体内容截图如下:
图5.7
可通过Symbols Viewer查看具体节点位置
图5.8
例如.sleep@plt在0x401080,在datadump中找到对应位置,类似的,也可以通过查表寻找到其他节点。
图5.9
5.5 链接的重定位过程分析
命令:objdump -d -r hello > obj_hello.s
执行命令,发现obj_hello.s已经出现在指定位置上
图5.10
打开obj_hello.s,观察其与hello.o的差异
图5.11
从图中不难看出,在代码数量上obj_hello.s远超过hello.o,这是因为obj_hello.s增加了一些系统级函数调用,同时也根据连接完成了符号的重定位。
链接(linking)是将各种代码块组合成一个可被载入内存并执行的文件的过程。链接过程可以在编译时(compile time)进行,此时源代码已经被翻译成了机器指令;也可以在载入时(load time)进行,此时程序已经被加载入了内存并且被加载器(loader)执行;也可以在运行时(run time)进行,此时链接过程是由应用程序进行的。在早期的操作系统中,链接过程需要手动完成,而在现在的系统中,链接过程是由称为链接器(linker)的程序完成的。链接器在软件的发展中起到了很重要的作用,因为它允许独立编译(separate compilation)。现在我们可以将一个大程序分解为较小的且更便于管理的小模块。我们对这些模块进行单独的修改和编译,而不是将整个程序编写为一个大的源文件。当我们改变这些小模块后,只需要将改变的部分简单的进行重新编译链接,而不需要重新编译其他文件。为了生成可执行文件,静态链接器必须执行如下的动作:
符号解析(symbol resolution):在目标文件中我们定义并引用了符号(symbol)。符号解析的目的是将每个符号引用和具体的符号定义结合在一起。
重定位(relocation):编译器和汇编器会从地址0开始生成数据section和代码section。链接器通过将具体的内存地址和每个定义的符号结合在一起来重定位这些section。接下来链接器会修改所有指向这些符号的引用,使得这些引用指向分配的内存地址。
那么结合一下hello.o与obj_hello.s的差异看看链接是如何完成的;
1)链接增加新的函数:
在hello中链接加入了在hello.c中用到的库函数,如exit、printf、sleep、getchar等函数。
2)增加的节:
hello中增加了.init和.plt节,和一些节中定义的函数。
3)函数调用:
hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
4)地址访问:
hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于某些地址的定位是不明确的,其地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。
我们得出链接实际上是通过链接器将各个.o文件串联在一起,组装成一个新文件的过程
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
图5.12
如图5.12使用edb执行hello后,我们通过查看Symbols可以找到其调用与跳转的各个子程序名或程序地址。如图5.13.
图5.13
5.7 Hello的动态链接分析
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表plt+全局偏移量表got实现函数的动态链接,got中存放函数目标地址,plt使用got中地址跳转到目标函数。
首先打开linkhello.elf文件找到got的存放地址为0x404000如下图5.14
图5.14
在调用前404008后的16个字节都是0,存放PLT中函数调用下一条指令
图5.15
调用后变为7f9ba6a2e190、7f9ba6a17ae0两个地址
图5.16
5.8 本章小结
本章介绍了链接的概念和作用,对链接后生成的可执行文件hello的elf格式文件进行了分析,分析了hello的重定位过程、执行过程、动态链接分析的各种处理操作。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1进程的概念
相较于程序,进程是动态的概念,比如gcc -o pro生成的这个pro文件,当他运行起来以后,系统中就会多一个进程。进程是一个正在运行的程序的实例,系统中的每一个程序都运行在某个进程的上下文中。
6.1.2进程的作用
给应用程序提供两个关键抽象:
- 一个独立的逻辑控制流,提供一个假象,好像程序独占地使用处理器
- 一个私有地址空间,提供一个假象,好像程序独占地使用内存系统
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash的基本作用是一个交互型应用级程序,代表用户运行其他程序,接收用户输入的命令并把它送入内核去执行。它交互性地解释和执行用户输入的命令,能够通过调用系统级的函数或功能执行程序、建立文件、进行并行操作等。包括历史、别名、快捷键、输入输出、执行顺序、管道符。
6.2.2:Shell-bash的处理流程
1.终端进程读取用户由键盘输入的命令行。
2.分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
3.检查第一个命令行参数是否是一个内置的shell命令
4.如果不是内部命令,调用fork()创建新进程/子进程
5.在子进程中,用步骤2获取的参数,调用execve()执行指定程序。
6.如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid等待作业终止后返回。
7.如果用户要求后台运行(如果命令末尾有&号),则shell返回;
6.3 Hello的fork进程创建过程
终端程序通过调用fork()函数创建一个子进程,子进程与父进程几乎完全相同且并发独立运行,子进程执行期间父进程等待子进程完成。
fork进程的创建过程如下:首先,带参执行当前目录下的可执行文件hello,父进程会通过fork函数创建一个新的运行的子进程hello。子进程获取了与父进程的上下文,包括栈、通用寄存器、程序计数器,环境变量和打开的文件相同的一份副本。子进程与父进程的最大区别是有着跟父进程不一样的PID,子进程可以读取父进程打开的任何文件。当子进程运行结束时,父进程如果仍然存在,则执行对子进程的回收,否则就由init进程回收子进程。
6.4 Hello的execve过程
调用函数fork创建新的子进程之后,子进程会调用execve函数,在当前进程的上下文中加载并运行一个新程序hello。execve 函数从不返回,它将删除该进程的代码和地址空间内的内容并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行该程序。将私有的区域映射进来,例如打开的文件,代码、数据段,然后将公共的区域映射进来。后面加载器跳转到程序的入口点,即设置PC指向_start 地址。_start函数最终调用hello中的 main 函数,这样,便完成了在子进程中的加载。
6.5 Hello的进程执行
一系列程序计数器 PC 的值的序列叫做逻辑控制流。由于进程是轮流使用处理器的,同一个处理器每个进程执行它的流的一部分后被抢占,然后轮到其他进程。处理器使用一个寄存器提供两种模式的区分。用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据;内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。为了能让处理器安全运行,不至于损坏操作系统,必然需要先知应用程序可执行指令所能访问的地址空间范围。因此,就存在了用户态与核心态的划分,核心态可以说是“创世模式”,拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
以执行sleep函数为例,sleep函数请求调用休眠进程,sleep将内核抢占,进入倒计时,当倒计时结束后,hello程序重新抢占内核,继续执行。
图6.1
当hello调用getchar的时候,实际落脚到执行输入流是stdin的系统调用read,hello之前运行在用户模式,在进行read调用之后陷入内核,内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。此时进入内核模式,内核执行上下文切换,切换到其他进程。当完成键盘缓冲区到内存的数据传输时,引发一个中断信号,此时内核从其他进程进行上下文切换回hello进程。而我们看到的输出结果如下:
图6.2
6.6 hello的异常与信号处理
6.6.1 hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
异常可以分为四类:中断(interrupt)、陷阱(trap)、故障(fault)和终止(abort),下表对这些类别的属性做了小结 :
类别 | 原因 | 异步/同步 | 返回行为 |
中断 | 来自I/O设备的信号 | 异步 | 总是返回到下一条指令 |
陷阱 | 有意的异常 | 同步 | 总是返回到下一条指令 |
故障 | 潜在可恢复的错误 | 同步 | 可能返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
中断:中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。硬件中断不是由任 何一条专门的指令造成的,从这个意义上来说它是异步的。硬件中断的异常处理程序常常 称为中断处理程序(interrupt handler) 。图6.3概述了一个中断的处理
图6.3
陷阱:陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序 将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一 样的接口,叫做系统调用。图6.4概述了一个系统调用的处理
图6.4
故障:故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控 制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故 障的指令,从而重新执行它。否则,处理程序返回到内核中的abort例程,abort例程会 终止引起故障的应用程序。图6.5概述了一个故障的处理
图6.5
终止:终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者 SRAM位被损坏时发生的奇偶错误。终止处理程序从不将控制返回给应用程序。如图6.6所示,处理程序将控制返回给一个abort例程,该例程会终止这个应用程序。
图6.6
6.6.2:以hello为例的异常与信号处理
- 不停乱按
我们可以发现不停乱按只是将乱按的字符作为输入存在stdin中,当 getchar 的时候读出一个’\n’结尾的字串(作为一次输入)
图6.7
- 回车
我们可以发现在程序进行中按回车会打印出空行,且会存在缓冲区,在程序正常结束时执行之前输入的Enter操作,如图6.8
图6.8
- Ctrl-C
在程序运行中按Ctrl-C,可以发现程序终止,shell收到sigint信号,回收hello进程
图6.9
- Ctrl-Z
输入CTRL+Z:在输出过程中按下CTRL+Z,程序中止并退出,此时调用ps指令查看后台进程,发现程序并未终止,而是中止存放在后台,如图6.10
图6.10
此时可以输入pstree查看进程树,如图6.11
图6.11
还可以输入fg回到前台继续执行如图6.12
图6.12
6.7本章小结
通过本章的学习,我们了解了进程的概念与作用、体会了壳Shell-bash的作用与处理流程、利用Hello的fork进程学习创建过程、研究了Hello的execve过程、明白了Hello的进程执行以及hello的异常与信号处理
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址
是指由程式产生的和段相关的偏移地址部分。逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。例如,你在进行C语言指针编程中,能读取指针变量本身值(&操作),实际上这个值就是逻辑地址,他是相对于你当前进程数据段的地址,不和绝对物理地址相干。只有在Intel实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,Cpu不进行自动地址转换);逻辑也就是在Intel保护模式下程式执行代码段限长内的偏移地址(假定代码段、数据段如果完全相同)。以hello为例,其为hello反汇编代码中的相对偏移地址。
线性地址
也叫虚拟地址(virtual address)。是逻辑地址到物理地址变换之间的中间层。程式代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。Intel 80386的线性地址空间容量为4G(2的32次方即32根地址总线寻址)。
物理地址
用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。
是指目前CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由段标识符和相对位置偏移两部分组成。段标识符是一个16位长的字段,称为段选择符。其中前13位是索引号,后三位表示是代码段寄存器还是数据段寄存器还是栈寄存器。可以通过索引号在段描述表中找到一个具体的段描述符,描述了一个段。给出逻辑地址后,通过段选择符中的T1字段确定是全局段描述表还是局部段描述表,之后通过索引号找到具体的段描述符,得到其基地址,再加上相对位置偏移,完成了从逻辑地址到线性地址的变换,即完成了段式管理。
段式管理特点:
1.段式管理以段为单位分配内存,每段分配一个连续的内存区。
2.由于各段长度不等,所以这些存储区的大小不一。
3.同一进程包含的各段之间不要求连续。
4.段式管理的内存分配与释放在作业或进程的执行过程中动态进行。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址空间的页到物理地址空间的页之间的映射用表来描述。由于4G的地址空间划分为1M个页,因此,如果用一张表来描述这种映射,那么该映射表就要有1M个表项,若每个表项占用4个字节,那么该映射表就要占用4M字节。为避免映射表占用如此巨大的存储器资源,所以80386把页映射表分为两级。
页映射表的第一级称为页目录表,存储在一个4K字节的物理页中。页目录表共有1K个表项,其中,每个表项为4字节长,包含对应第二级表所在物理地址空间页的页码。页映射表的第二级称为页表,每张页表也安排在一个4K字节的页中。每张页表都有1K个表项,每个表项为4字节长,包含对应物理地址空间页的页码。由于页目录表和页表均由1K个表项组成,所以使用10位的索引就能指定表项,即用10位的索引值乘以4加基地址就得到了表项的物理地址。
下图显示了由页目录表和页表构成的页映射表结构。从图中可见,控制寄存器CR3指定页目录表;页目录表可以指定1K个页表,这些页表可以分散存放在任意的物理页中,而不需要连续存放;每张页表可以指定1K个物理地址空间的页,这些物理地址空间的页可以任意地分散在物理地址空间中。需要注意的是,存储页目录表和页表的基地址是对齐在4K字节边界上的。
图7.1
7.4 TLB与四级页表支持下的VA到PA的变换
前提如下: 虚拟地址空间 48 位,物理地址空间 52 位,页表大小 4KB,4 级页表。TLB 4 路 16 组相联。CR3 指向第一级页表的起始位置(上下文一部分)。 解析前提条件:由一个页表大小 4KB,一个 PTE 条目8B,共 512 个条目,使 用 9 位二进制索引,一共 4 个页表共使用 36 位二进制索引,所以 VPN 共 36 位, 因为 VA 48 位,所以 VPO 12 位;因为 TLB 共 16 组,所以 TLBI 需 4 位,因为 VPN 36 位,所以 TLBT 32 位。
CPU 产生虚拟地址 VA,VA 传送给 MMU,MMU 使用前 36 位 VPN 作为 TLBT(前 32 位)+TLBI(后 4 位)向 TLB 中匹配,如果命中,则得到 PPN (40bit)与 VPO(12bit)组合成 PA(52bit)。 如果 TLB 中没有命中,MMU 向页表中查询,CR3 确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出 PTE,如果在物理内存 中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查 询到 PPN,与 VPO 组合成 PA,并且向 TLB 中添加条目。如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。
图7.2
7.5 三级Cache支持下的物理内存访问
MMU将物理地址发给L1缓存,缓存从物理地址中取出缓存偏移CO、缓存组索引CI以及缓存标记CT。若缓存中CI所指示的组有标记与CT匹配的条目且有效位为1,则检测到一个命中,读出在偏移量CO处的数据字节,并把它返回给MMU,随后MMU将它传递给CPU。若不命中,则需到低一级Cache(若L3 cache中找不到则到主存)中取出相应的块将其放入当前cache中,重新执行对应指令,访问要找的数据。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给他一个唯一的pid。为了给这个新进程创建虚拟内存,他创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有写时复制。当fork从新进程返回,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要:
- 删除已存在的用户区域:删除当前进程虚拟地址的用户部分中的已存在的区域结构。
- 映射私有区域:为新程序hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。
- 映射共享区域:如果hello程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置程序计数器(PC) :设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
缺页故障:当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。
图7.3
缺页中断处理:确认出物理内存中的牺牲页,如果已被修改,则放入虚拟内存,调出新的页面,更新内存中的页表条目,返回到原先的进程中,重新执行引起缺页故障的命令,重新在内存管理单元中查找页表,发现造成缺页故障的物理地址已在物理内存中,命中。
图7.4
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆.系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址) .对于每个进程,内核维护着一个变量brk, 它指向堆的顶部。
图7.5
分配器将堆视为一组不同大小的块的集合来维护.每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的.已分配的块显式地保留为供应用程序使用.空闲块可用来分配.空闲块保持空闲,直到它显式地被应用所分配.一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格. 两种风格都要求应用显式地分配块.它们的不同之处在于由哪个实体来负责释放已分配的块
显式分配器(explicit allocator):
要求应用显式地释放任何已分配的块.例如,C标准库提供一种叫做malloc程序包的显式分配器.C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块.C++中的new和delete操作符与C中的malloc和free相当.
隐式分配器(implicit allocator):
要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块.隐式分配器也叫做垃圾收集器(garbage collector),而自动释放未使用的已分配的块的过程叫做垃圾收集( garbage collection).例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
一个块是由一个字的头部,有效载荷,以及可能的一些额外的填充组成的.头部编码了这个块的大小,以及这个块是已分配的还是空闲的.如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是零.因此,我们只需要内存大小的29个高位,释放剩余的3位来编码其他信息.在这种情况中,我们用其中的最低位(已分配位)来指明这个块是已分配的还是空闲的。
7.10本章小结
虚拟内存是对主存的一个抽象。支持虚拟内存的处理器通过使用一种叫做虚拟寻址的间接形式来引用主存。处理器产生一个虚拟地址,在被发送到主存之前,这个地址被翻译成一个物理地址。从虚拟地址空间到物理地址空间的地址翻译要求硬件和软件紧密合作。专门的硬件通过使用页表来翻译虚拟地址,而页表的内容是由操作系统提供的。
虚拟内存提供三个重要的功能。第一,它在主存中自动缓存最近使用的存放磁盘上的虚拟地址空间的内容。虚拟内存缓存中的块叫做页。对磁盘上页的引用会触发缺页,缺页将控制转移到操作系统中的一个缺页处理程序。缺页处理程序将页面从磁盘复制到主存缓存,如果必要,将写回被驱逐的页。第二,虚拟内存简化了内存管理,进而又简化了链接、在进程间共享数据、进程的内存分配以及程序加载。最后,虚拟内存通过在每条页表条目中加入保护位,从而了简化了内存保护。
地址翻译的过程必须和系统中所有的硬件缓存的操作集成在一起。大多数页表条目位于L1高速缓存中,但是–个称为TLB的页表条目的片上高速缓存,通常会消除访问在Ll上的页表条目的开销。
现代系统通过将虚拟内存片和磁盘上的文件片关联起来,来初始化虚拟内存片,这个过程称为内存映射。内存映射为共享数据、创建新的进程以及加载程序提供了一种高效的机制。应用可以使用mmap函数来手工地创建和删除虚拟地址空间的区域。然而,大多数程序依赖于动态内存分配器,例如 malloc,它管理虚拟地址空间区域内一个称为堆的区域。动态内存分配器是一个感觉像系统级程序的应用级程序,它直接操作内存,而无需类型系统的很多帮助。分配器有两种类型。显式分配器要求应用显式地释放它们的内存块。隐式分配器(垃圾收集器)自动释放任何未使用的和不可达的块。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m个字节的序列:
B0,B1,…,Bk ,…,Bm - 1
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行:
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(1)打开文件:一个应用程序要求内核打开相应文件,来访问I/O设备。会返回一个描述符,内核记录该文件所有信息,程序只需要记住该描述符。
(2)linux shell 创建的每个进程开始时都有三个打开的文件:标准输入 、标准输出和标准错误。头文件定义了常量来代替显式的描述符值。
(3)改变文件位置:内核对于每个文件保持一个初始为0的文件位置,该位置标示从头部开始的文件的偏移量。程序可以通过函数修改此文件位置。
(4)读写文件:读操作是从当前文件位置开始,复制相应数量的字节到内存,写操作则是从内存读入相应数量的字节到当前文件位置,然后更新文件位置。
(5)关闭文件:一个应用程序完成对文件访问后,要求内核关闭相应文件,内核会删除期间创建的数据结构,并恢复描述符。
Unix IO函数:
(1)打开函数:int open(char* filename,int flags,mode_t mode)将filename文件转为操作符,mode为访问权限
(2)关闭函数:int close(fd)关闭fd文件,返回操作结果
(3)读函数:ssize_t read(int fd,void *buf,size_t n)从fd文件复制n个字节到buf处
(4)写函数:ssize_t wirte(int fd,const void *buf,size_t n)从buf处复制n个字节给fd文件
8.3 printf的实现分析
printf的函数体
int printf(const char *fmt, …)
{
int i;
char buf[256];
va_list arg = (va_list)((char*)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
我们先通过printf的源代码来探究printf是如何实现格式化输出的。首先我们看到va_list arg = (va_list)((char*)(&fmt) +4)这条语句,其中va_list的定义为 typedef char*va_list,这说明va_list是一个字符指针,其中(char*)(&fmt) +4表示的是这个字符数组中的第一个元素。(注意:这是在32位的环境下,64位需要将4改为8,因为sizeof(void*)= 8)之后我们继续看 i = vsprintf(buf, fmt,arg);这条语句引出我们下一个要研究的函数,vsprintf(buf, fmt,arg)函数,看看这个函数是什么功能,下面是vsprintf函数的源码
int vsprintf(char *buf, const *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的功能是对我们要输出的内容进行格式化。vsprintf函数将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。write函数将buf中的i个元素写到终端。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
程序调用getchar后,等待用户按键,将输入的字符储存在电脑的缓冲区,等待键入回车。键入回车后,getchar从stdio流中每次读取一个字符,如果输入不止一个字符,则保存在缓冲区中,等待后需getchar调用,直到缓冲区清空后,等待用户按键。
异步异常-键盘中断的处理:按键后,不仅产生该按键的码,还产生一个中断信号,使正在运行的程序中断后运行一个子程序,读取按键的码并转为ASCII码保存到缓冲区内。都结束后回到下一条指令。
8.5本章小结
本章介绍了 Linux 的 I/O 设备的基本概念和管理方法,以及Unix I/O 接口及其函数。最后分析了printf 函数和 getchar 函数的工作过程。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
编写hello.c的源程序
预处理:从hello.c生成hello.i文件,将文件中调用的库展开并入文件中。
编译:从hello.i生成hello.s文件,将高级语言转为汇编语言。
汇编:从hello.s生成hello.o文件,机器语言写的可以重定位的文件。
链接:将hello.o经重定位和动态链接生成可执行文件hello
运行:在shell中输入命令,终端为其fork新建进程,并通过execve把代码和数据加载入虚拟内存空间,程序开始执行;
加载:shell 调用 execve()函数,execve()函数 调用启动加载器,映射到虚拟内存,程序开始加载到物理内存,然后进入 main 函数。
执行命令:hello顺序执行自己的逻辑控制流
动态内存分配:通过malloc函数申请动态内存
信号:Ctrl+c,发送SIGINT信号给进程并终止前台作业。Ctrl+z时,发送SIGTSTP信号给进程,并将前台作业停止挂起。
终止:子进程结束后,父进程回收子进程,并回收内存空间。
感悟:一个简简单单的hello.c的程序,背后却要经历那么复杂和漫长的一生。
原来一个短短几行的hello程序背后却是那么的不简单,不平凡。计算机系统的设计者是多么巧妙构思,帮助我们简化了程序运行的过程,让计算机成为简单易用的工具。我明白了时代的进步离不开这些敢打敢拼艰苦奋斗的科学家们,无数次的更新迭代是他们绞尽脑汁的结果。我们不仅要学习他们的智慧结晶,更要学习他们不畏艰难的意志品质,才能深刻理解计算机科学,让计算机更好的、更便捷的为人类所使用。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
Hello.c 源代码
Hello.i 预处理生成的文本文件
Hello.s .i编译后得到的汇编语言文件
Hello.o .s汇编后得到的可重定位目标文件
Hello.elf hello.o的elf文件
Hello.asm .o经过反汇编生成的汇编语言文件
Hello .o经过链接生成的可执行目标文件
Linkhello.elf Hello的elf文件
Obj_hello.s .o经过链接反汇编的汇编语言文件
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
- Randal E. Bryant, David R. O'Hallaon. 深入理解计算机系统. 第三版. 北京市:机械工业出版社[M]. 2018: 1-737
[2]https://blog.youkuaiyun.com/will130/article/details/50917166?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165235876916780366591062%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=165235876916780366591062&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-2-50917166-null-null.142^v9^control,157^v4^control&utm_term=%E9%80%BB%E8%BE%91%E5%9C%B0%E5%9D%80%E3%80%81%E7%BA%BF%E6%80%A7%E5%9C%B0%E5%9D%80%E3%80%81%E8%99%9A%E6%8B%9F%E5%9C%B0%E5%9D%80%E3%80%81%E7%89%A9%E7%90%86%E5%9C%B0%E5%9D%80&spm=1018.2226.3001.4187
[3]https://blog.youkuaiyun.com/weixin_41019383/article/details/98207386?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165235934416781667899564%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=165235934416781667899564&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-5-98207386-null-null.142^v9^control,157^v4^control&utm_term=+%E7%BA%BF%E6%80%A7%E5%9C%B0%E5%9D%80%E5%88%B0%E7%89%A9%E7%90%86%E5%9C%B0%E5%9D%80%E7%9A%84%E5%8F%98%E6%8D%A2-%E9%A1%B5%E5%BC%8F%E7%AE%A1%E7%90%86&spm=1018.2226.3001.4187
[4] [转]printf 函数实现的深入剖析 - Pianistx - 博客园
(参考文献0分,缺失 -1分)