hello的一生

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学   号
班   级 1703005
学 生    
指 导 教 师 吴锐

计算机科学与技术学院
2018年12月
摘 要
对于我们程序员来说习以为常的hello,它的一生确实曲曲折折的,从hello.c通过键盘鼠标等I/O设备书写,然后通过文件的方式储存在主存里面。预处理器进行预处理,编译器翻译成汇编语言,汇编器汇编成可重定位二进制代码,链接器链接,接着是进程管理即创建子进程,调用启动加载器,执行指令,然后存储管理访问内存,动态内存申请,异常情况的处理等等,最后结束,父进程回收子进程等等.
关键词: 预处理,编译,汇编, 链接,进程管理, 存储管理, IO管理

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

目 录

第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 4 -
第2章 预处理 - 5 -
2.1 预处理的概念与作用 - 5 -
2.2在UBUNTU下预处理的命令 - 5 -
2.3 HELLO的预处理结果解析 - 5 -
2.4 本章小结 - 5 -
第3章 编译 - 6 -
3.1 编译的概念与作用 - 6 -
3.2 在UBUNTU下编译的命令 - 6 -
3.3 HELLO的编译结果解析 - 6 -
3.4 本章小结 - 6 -
第4章 汇编 - 7 -
4.1 汇编的概念与作用 - 7 -
4.2 在UBUNTU下汇编的命令 - 7 -
4.3 可重定位目标ELF格式 - 7 -
4.4 HELLO.O的结果解析 - 7 -
4.5 本章小结 - 7 -
第5章 链接 - 8 -
5.1 链接的概念与作用 - 8 -
5.2 在UBUNTU下链接的命令 - 8 -
5.3 可执行目标文件HELLO的格式 - 8 -
5.4 HELLO的虚拟地址空间 - 8 -
5.5 链接的重定位过程分析 - 8 -
5.6 HELLO的执行流程 - 8 -
5.7 HELLO的动态链接分析 - 8 -
5.8 本章小结 - 9 -
第6章 HELLO进程管理 - 10 -
6.1 进程的概念与作用 - 10 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 10 -
6.3 HELLO的FORK进程创建过程 - 10 -
6.4 HELLO的EXECVE过程 - 10 -
6.5 HELLO的进程执行 - 10 -
6.6 HELLO的异常与信号处理 - 10 -
6.7本章小结 - 10 -
第7章 HELLO的存储管理 - 11 -
7.1 HELLO的存储器地址空间 - 11 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 11 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -
7.5 三级CACHE支持下的物理内存访问 - 11 -
7.6 HELLO进程FORK时的内存映射 - 11 -
7.7 HELLO进程EXECVE时的内存映射 - 11 -
7.8 缺页故障与缺页中断处理 - 11 -
7.9动态存储分配管理 - 11 -
7.10本章小结 - 12 -
第8章 HELLO的IO管理 - 13 -
8.1 LINUX的IO设备管理方法 - 13 -
8.2 简述UNIX IO接口及其函数 - 13 -
8.3 PRINTF的实现分析 - 13 -
8.4 GETCHAR的实现分析 - 13 -
8.5本章小结 - 13 -
结论 - 14 -
附件 - 15 -
参考文献 - 16 -

第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
1.2 环境与工具
硬件环境:
Intel® Core™ i5-6300HQ CPU
8GB RAM
Nvidia GeForce GTX 960M
128GB SSD
软件环境:
Windows 10
Ubuntu 16.04.3
UltraEdit
VirtualBox
codeblocks
调试工具:
Edb
Gdb
Codeblocks
UltraEdit
readelf
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

hello.c: hello的源文件
hello.i:hello的预处理文件
hello.s:hello的编译文件
hello.o:hello的可重定位目标文件
hello:hello的可执行目标文件(静态)
hellodynamic:hello的可执行目标文件(动态)
hellooelf.txt:hello.o的elf信息
helloodump.txt:hello.o的反汇编信息
helloelf.txt:hello的elf信息
hellodump.txt:hello的反汇编信息
hellosymbols.txt:hello的符号表
pstree.txt:hello执行时的进程树

1.4 本章小结
即使是对于再简单的hello,也同样涉及到了关于计算机系统的从硬件到软件的各个部分的共同配合才能实现一个hello的功能.因此我们要根据hello来了解计算机系统的各个组成部分,从顶层到底层,然后再由底层到顶层,这样我们就进一步了解计算机系统是如何构造和运作的,便于我们更加深刻的了解计算机系统.
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
(以下格式自行编排,编辑时删除)
预处理是指在编译之前的预处理阶段,预处理程序对程序进行的操作. 预处理器根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。
1.将所有的#define删除,并展开所有的宏定义;
2.处理所有的预编译指令,例如:#if,#elif,#else,#endif;
3.处理#include预编译指令,将被包含的文件插入到预编译指令的位置;
4.添加行号信息文件名信息,便于调试;
5.删除所有的注释:// /**/;
6.保留所有的#pragma编译指令,因为在编写程序的时候,我们经常要用到#pragma指令来设定编译器的状态或者是指示编译器完成一些特定的动作。
生成.i文件。
包括:1. 宏定义 2. 文件包含(文件包含是指把指定文件的全部内容包括到当前源程序文件中。) 3. 条件编译(指在特定的条件下,对满足条件和不满足条件的情况分别进行处理——满足条件时编译某些语句,不满足条件时编译另一些语句。)

预处理的主要作用仅仅是对程序代码文本进行替换操作,如将以#include格式包含的文件内容复制到编译的源文件中,用实际值替换#define定义的宏,以及根据#if的条件决定需要编译的代码。预处理过后程序代码中的预处理指令会被删除。所以预处理器的输出是原程序的一个编辑后的、不包含指令的版本。

2.2在Ubuntu下预处理的命令
预处理命令:
gcc -E -o hello.i hello.c

2.3 Hello的预处理结果解析

(以下格式自行编排,编辑时删除)
Hello.i内容如下:

首先预处理程序将#include <stdio.h>、#include <unistd.h>、#include <stdlib.h>三个文件中的内容拷贝到预处理文件中,即为以上.c的源文件以前的内容,3100行到最后则是直接复制源文件的内容。后面的一部分是直接复制源文件的内容。如果遇到头文件中包含预处理命令,则递归地将内容复制进去,直到预处理文件中不包含任何预处理命令为止。

还有像

1 “/usr/include/stdc-predef.h” 1 3 4

1 “/usr/include/stdio.h” 1 3 4

27 “/usr/include/stdio.h” 3 4

1 “/usr/include/features.h” 1 3 4

367 “/usr/include/features.h” 3 4

1 “/usr/include/x86_64-linux-gnu/sys/cdefs.h” 1 3 4

410 “/usr/include/x86_64-linux-gnu/sys/cdefs.h” 3 4

1 “/usr/include/x86_64-linux-gnu/bits/wordsize.h” 1 3 4

411 “/usr/include/x86_64-linux-gnu/sys/cdefs.h” 2 3 4

368 “/usr/include/features.h” 2 3 4

391 “/usr/include/features.h” 3 4

1 “/usr/include/x86_64-linux-gnu/gnu/stubs.h” 1 3 4

10 “/usr/include/x86_64-linux-gnu/gnu/stubs.h” 3 4

1 “/usr/include/x86_64-linux-gnu/gnu/stubs-64.h” 1 3 4

11 “/usr/include/x86_64-linux-gnu/gnu/stubs.h” 2 3 4

392 “/usr/include/features.h” 2 3 4

28 “/usr/include/stdio.h” 2 3 4

等等
这些内容都有一个固定的格式:
2.4 本章小结
通过本章我们了解到了预处理.预处理是整个编译过程的第一步,将.c文件转化为.i文件.在这个过程中仅仅是将一些.c文件里面用到的、涉及到的函数库头文件复制到程序源文件中、在程序代码中替换宏定义,并根据条件编译的条件保留相应的内容, 直到预处理文件中不包含任何预处理命令为止,就这样完成了预处理的指令。

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
概念:
编译是指把ascii码形式的高级程序设计程序书写的源程序文件,翻译成等价的ascii码形式的汇编语言格式的文件。通常,源程序文件是一个经过预处理过后的.i文件。在编译器将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。这个过程称为编译,同时也是编译的作用。

3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析
3.3.1 数据
hello.s中用到的C数据类型常量有字符串常量和数字常量;程序中出现了两个变量:分别是全局变量sleepsecs和局部变量i。

1.程序中的字符串常量:
分别是"Usage: Hello 学号 姓名!\n"和"Hello %s %s\n",他们个printf传入的输出格式化参数,在hello.s中声明如下图所示

2.程序中的数字常量
四个数字常量分别是3、1、0、10,在汇编语句中以立即数的形式存储。如下:
3
1
9
0
movl $0, %eax 0
3.程序中的全局变量
int sleepsecs:sleepsecs在C程序中被声明为全局变量,且已经被赋值,编译器处理时在.data节声明该变量,.data节存放已经初始化的全局和静态C变量。

4.程序中的局部变量
局部变量i没有特殊声明,使用时仅仅是使用-4(%rbp)这个栈上的空间来保存一下i的内容。如下:
movl $0, -4(%rbp)
addl $1, -4(%rbp)
cmpl $9, -4(%rbp)

3.3.2赋值
程序中涉及的赋值操作有:

  1. int sleepsecs=2.5 :因为sleepsecs是全局变量,所以直接在.data节中将sleepsecs声明为值2的long类型数据。
  2. 将i的初始值赋值为0,即

3.3.3类型转换
在hello.c里面出现一次类型转换,即int sleepsecs=2.5,将浮点数类型的2.5转换为int类型。因为将float或者double类型的数强制转化为int类型时,会进行舍入,即将小数点之后的数全部舍去.因此编译器直接将sleepsecs的值赋为2.

3.3.4算术操作
程序中涉及的算数操作有:
1.i++,对计数器i自增,使用程序指令addl,即addl $1, -4(%rbp)来实现i的加1
2.汇编中使用leaq .LC1(%rip),%rdi,使用了加载有效地址指令leaq计算LC1的段地址%rip+.LC1并传递给%rdi

3.3.5 关系操作
程序中涉及的关系运算为:
1.判断argc是否不等于3。在汇编语言中中使用cmpl $3,-20(%rbp),计算argc-3然后设置条件码,为下一步je利用条件码进行跳转作准备。
2.判断i是否小于10。在汇编语言中使用cmpl $9,-4(%rbp),计算i-9然后设置条件码,为下一步jle利用条件码进行跳转做准备。

3.3.6数组操作
程序中涉及数组的是:char *argv[] (int main(int argc,char *argv[])),函数执行时输入的命令行,argv作为存放char指针的数组同时是第二个参数。如下图所示

3.3.7控制转移

  1. if (argv!=3):当argv等于3时执行内部代码,当argv不等于3的时候执行接下来的代码。如下图所示

当argv小于3时不执行跳转,执行下面的代码(即if里面的代码),如果等于3的时候就会跳到.L2去执行
2.for(i=0;i<10;i++),当i<10时一直循环,否则跳出循环,汇编代码如下图

通过比较i和9的大小,如果i<=9时那么就会跳进循环体,即跳进.L4,执行循环体内部的内容

3.3.8函数操作
hello程序有一个主函数main函数,共调用四个函数:printf(),exit(),sleep()和getchar(),.其中printf()函数调用两次。在调用之前,都会先将参数转移到到存储参数的寄存器中,再调用那个地址的函数。他们对应的汇编代码分别如下:
leaq .LC0(%rip), %rdi
call puts@PLT

movl $1, %edi
call exit@PLT

leaq .LC1(%rip), %rdi
movl $0, %eax
call printf@PLT

movl sleepsecs(%rip), %eax
movl %eax, %edi
call sleep@PLT

call getchar@PLT

函数返回:
hello程序中只有main函数的返回。由于返回值是0,所以将0传进%eax中,接着执行leave,将栈帧收回,接着执行ret,结束该函数并且将栈顶的返回地址弹出到%eip,然后按照%eip的指令地址继续执行,即从hello中退出。汇编代码如下:
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc

4 本章小结
通过本章我们了解了编译器是如何处理.c文件,怎么对其进行编译处理,尤其是c语言的各种数据与操作是如何在汇编里面表示以及实现的.通过编译处理,程序从高级语言的程序变成了较为底层的汇编语言程序,从机器的角度描述了这个程序的行为.让我们更好的去理解.
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
汇编,是将由ascii码存储的汇编文件,转化为二进制形式的可重定位目标文件的过程。通常,源文件是一个经过编译的.s文件。汇编器(as)将.s汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中,.o文件是一个二进制文件,它包含程序的指令编码。这个过程称为汇编,亦即汇编的作用。
4.2 在Ubuntu下汇编的命令
汇编命令:
gcc -c -o hello.o hello.s

4.3 可重定位目标elf格式
使用readelf -a hello.o > hello.elf 指令获得hello.o文件的ELF格式.Hello.o的elf格式组成如下:
1.ELF头:ELF头中包含有整个ELF文件的基本信息,包括Magic Number(魔数)版本、平台、程序入口以及各个部分(ELF头、程序头、节头)的大小。

2.节头部表,包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。

3.程序头:用来构造进程映像的目标文件必须具有程序头部表,而可重定位目标文件则没有这个头。故显示“本文件中没有程序头。”。

  1. 重定位节.rela.text ,一个.text节中位置的列表,包含.text节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。图中重定位信息分别是对各个函数的声明.

4.4 Hello.o的结果解析
使用 objdump -d -r hello.o > hello.objdump获得反汇编代码。

以下为hello.s的内容:

观察之后我们发现他们的主要差别为:
1.分支转移:反汇编代码跳转指令的操作数使用的不是像je或者jep到.L3这类的,这些在汇编成机器语言之后显然不存在,在汇编语言中这些都是特定的地址,因此在要跳转的时候直接跳转到那个函数对应的地址即可。
2.函数调用:在.s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。这是因为hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text节中为其添加重定位条目,等待静态链接的进一步确定。

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
4.5 本章小结
通过这个章节我们知道了hello.s文件经过汇编到hello.o的过程,以及hello.o的elf文件的组成还有反汇编后与hello.s文件的差别。同时说明机器语言是与汇编语言之间有着很多的相似处的。
(第4章1分)

第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。链接是由叫做链接器的程序自动执行的。链接器使得分离编译成为可能。
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.3 可执行目标文件hello的格式
使用readelf -a hello > hello2.elf 命令生成hello程序的ELF格式文件。

节头信息:

程序头信息:

5.4 hello的虚拟地址空间
通过使用edb加载hello

我们很容易看到自虚拟地址开始到结束,这之间每个节排列即开始结束同图5.2中Address中声明。

查看ELF格式文件中的程序头, 程序头表在执行的时候被使用,它告诉链接器运行时加载的内容并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的大小、位置、标志、访问权限和对齐方面的信息。

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。   

5.5 链接的重定位过程分析
使用objdump -d -r hello > hello1.objdump 获得hello的反汇编代码。

Hello1.objdump:
Hello.objdump:

很明显可以看出来与hello.o反汇编文本hello.objdump相比,在hello1.objdump中多了许多节
.interp保存ld.so的路径
.note.ABI-tagLinux下特有的section
.hash符号的哈希表
.gnu.hashGNU拓展的符号的哈希表
.dynsym运行时/动态符号表
.dynstr存放.dynsym节中的符号名称
.gnu.version符号版本
.gnu.version_r符号引用版本
.rela.dyn运行时/动态重定位表
.rela.plt.plt节的重定位条目
.init程序初始化需要执行的代码
.plt动态链接-过程链接表
.fini当程序正常终止时需要执行的代码
.eh_frame包含异常展开和源语言信息。
.dynamic存放被ld.so使用的动态链接信息
.got动态链接-全局偏移量表-存放变量
.got.plt动态链接-全局偏移量表-存放函数
.data初始化了的数据
.comment一串包含编译器的NULL-terminated字符串
重定位的过程:链接器在完成符号解析以后,就把代码中的每个符号引用和正好一个符号定义关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。然后就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时的地址。在hello到hello.o中,首先是重定位节和符号定义,链接器将所有输入到hello中相同类型的节合并为同一类型的新的聚合节。
然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每一个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。然后是重定位节中的符号引用,链接器会修改hello中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址。

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.6 hello的执行流程
程序地址 程序名
0x00007f1d21c53c30: ld-2.23.so!_start
0x00007f1d21c53c38: ld-2.23.so!_dl_start_user
0x00007f1d21c549b0: ld-2.23.so!_dl_start
0x00007f1d21c63740: ld-2.23.so!_dl_init
0x00000000004004d0: hello!_start
0x00007f1d218a9740: libc-2.23.so!__libc_start_main
0x00007f1d218c3280: libc-2.23.so!__cxa_atexit
0x0000000000400580: hello!__libc_csu_init
0x0000000000400430: hello!_init
0x00007f1d218be250: libc-2.23.so!_setjmp
0x00007f1d21c6e610: ld-2.23.so!__sigsetjmp
0x00007f1d218be210: libc-2.23.so!__sigjmp_save
0x00000000004004fa: hello!main
0x0000000000400460: hello!puts@plt
0x00000000004004a0: hello!exit@plt
*hello!printf@plt –
*hello!sleep@plt –
*hello!getchar@plt –
0x7fce 8cc4e680ld-2.27.so!_dl_runtime_resolve_xsave
0x7fce 8cc46df0-ld-2.27.so!_dl_fixup
0x7fce 8cc420b0–ld-2.27.so!_dl_lookup_symbol_x
0x7fce8c889128 libc-2.27.so!exit

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。如图

在dl_init调用之后,如下图

由图可见,在调用前后gcc上面四行的条目很明显发生了变化.

在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问跳转时GOT地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
在本章中主要介绍了链接的概念与作用、hello的ELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。让我们充分的理解了在链接是发生的一系列过程.
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程的经典定义是一个执行中的程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的代码和数据,它的栈、通用目的寄存器中的内容、程序计数器、环境变量和打开文件描述符的集合。
进程,是计算机中已运行程序的实体。进程为曾经是分时系统的基本运作单位。在面向进程设计的系统(如早期的UNIX,Linux2.4及更早的版本)中,进程是程序的基本执行实体;在面向线程设计的系统(如当代多数操作系统、Linux 2.6及更新的版本)中,进程本身不是基本运行单位,而是线程的容器。程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。
6.2 简述壳Shell-bash的作用与处理流程
Shell字面理解就是个“壳”,是操作系统(内核)与用户之间的桥梁,充当命令解释器的作用,将用户输入的命令翻译给系统执行。Linux中的shell与Windows下的DOS一样,提供一些内建命令(shell命令)供用户使用,可以用这些命令编写shell脚本来完成复杂重复性的工作。

处理流程:
1)从终端读入输入的命令。
2)将输入字符串切分获得所有的参数
3)如果是内置命令则立即执行
4)否则调用相应的程序为其分配子进程并运行
5)shell应该接受键盘输入信号,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
在终端Terminal中键入 ./hello 1170300510 chenyifei,运行的终端程序会读入这个字符串,然后对这个字符串进行解析,因为hello不是一个内置的命令所以解析之后终端程序判断执行hello,之后终端程序首先会调用fork函数创建一个新的运行的子进程,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,父进程与子进程之间最大的区别在于它们拥有不同的PID。
父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
6.4 Hello的execve过程
当fork之后,子进程调用execve函数(传入命令行参数)在当前进程的上下文中加载并运行一个新程序即hello程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。
新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。
最后加载器设置PC指向_start地址,_start最终调用hello中的main函数。
execve 函数加载并运行可执行目标文件filename, 且带参数列表argv 和环境变量列表envp 。只有当出现错误时,例如找不到filename, execve 才会返回到调用程序。所以,与fork 一次调用返回两次不同, execve 调用一次并从不返回。

6.5 Hello的进程执行
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。

加载器设置PC指向_start地址,_start最终调用hello中的main函数。
通过逻辑控制流,在不同的进程间切换。分配给某个进程的时间就叫做进程的时间片。上下文信息即重新启动一个被抢夺的进程的条件。用户态中,程序不允许执行一些特权功能,而核心态中可以,它们之间需要某些条件才能切换。
对hello进行简单的分析:
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.6 hello的异常与信号处理
1.正常运行hello程序:
下图是正常执行hello程序的结果,当程序执行完成之后,进程便被回收。

2.中途按下Ctrl-z
是在程序输出4条info之后按下ctrl-z的结果,当按下ctrl-z之后,程序马上终止.shell父进程收到SIGSTP信号,信号处理函数的逻辑是打印屏幕回显、将hello进程挂起,通过ps命令我们可以看出hello进程没有被回收,然后fg将其调到前台,然后shell程序首先打印hello的命令行命令,hello继续运行打印剩下的6条info,之后输入字串,程序结束,同时进程被回收。

3.中途按下Ctrl-c
是在程序输出5条info之后按下ctrl-c的结果,当按下ctrl-c之后,shell父进程收到SIGINT信号,信号处理函数的逻辑是结束hello,并回收hello进程。然后fg就不会继续执行未完成的任务.

4.中途按乱按
接下来是在程序运行中途乱按的结果,可以很容易的发现,首先可以随便按回车和其他的字符等等,不会对程序的运行造成影响.因为乱按只是将屏幕的输入缓存到stdin,当getchar的时候读出一个’\n’结尾的字串(作为一次输入),其他字串会当做shell命令行输入。

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.7本章小结
在本章中我们了解到了进程的概念与作用,并让我们从进程的创建到执行到各种异常情况的处理等等各种情况,让我们更加充分的理解了进程,也让我们理解了简单的shell一般处理流程,即调用fork创建新进程,调用execve执行hello,hello的进程执行,hello的异常与信号处理。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址是程序代码经过编译之后出现在汇编程序中的地址,由选择符和偏移量组成。
线性地址是逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的形式。
虚拟地址:其实就是这里的线性地址。
物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。 CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段标识符和段内偏移量。段标识符由16位字段组成,前13位为索引号。
索引号是段描述符的索引,很多个描述符,组成了一个数组,叫做段描述表,可以通过段描述标识符的前13位,再这个表中找到一个具体的段描述符,这个描述符就描述了一个段,每个段描述符由八个字节组成。
段描述符中的base字段,描述了段开始的线性地址,一些全局的段描述符,放在全局段描述符表中,一些局部的则对应放在局部段描述符表中。由T1字段决定使用哪个。
以下是具体的转化步骤:
1.给定一个完整的逻辑地址。
2.看段选择符T1,知道要转换的是GDT中的段还是LDT中的段,通过寄存器得到地址和大小。
3.取段选择符中的13位,再数组中查找对应的段描述符,得到BASE,就是基地址。
4.线性地址等于基地址加偏移。
7.3 Hello的线性地址到物理地址的变换-页式管理
分页管理机制通过上述页目录表和页表实现32位线性地址到32位物理地址的转换。控制寄存器CR3的高20位作为页目录表所在物理页的页码。首先把线性地址的最高10位(即位22至位31)作为页目录表的索引,对应表项所包含的页码指定页表;然后,再把线性地址的中间10位(即位12至位21)作为所指定的页目录表中的页表项的索引,对应表项所包含的页码指定物理地址空间中的一页;最后,把所指定的物理页的页码作为高20位,把线性地址的低12位不加改变地作为32位物理地址的低12位。
为了避免在每次存储器访问时都要访问内存中的页表,以便提高访问内存的速度,80386处理器的硬件把最近使用的线性—物理地址转换函数存储在处理器内部的页转换高速缓存中。在访问存储器页表之前总是先查阅高速缓存,仅当必须的转换不在高速缓存中时,才访问存储器中的两级页表。页转换高速缓存也称为页转换查找缓存,记为TLB。
在分页机制转换高速缓存中的数据与页表中数据的相关性,不是由80386处理器进行维护的,而必须由操作系统软件保存,也就是说,处理器不知道软件什么时候会修改页表,在一个合理的系统中,页表只能由操作系统修改,操作系统可以直接地在软件修改页表后通过刷新高速缓存来保证相关性。高速缓存的刷新通过装入处理器控制寄存器CR3完成

7.4 TLB与四级页表支持下的VA到PA的变换

如图给出了Core i7 MMU如何使用四级页表来将虚拟地址翻译成物理地址。36位的vn划分为4个9位的片,每个片对应一个页表的偏移量。CR3寄存器存有L1页表的物理地址。vn1提供一个到L1PET的偏移量,这个PTE包含L2页表的基地址。v*n2提供一个到L2PET的偏移量,以此类推。
通过四级页表,我们可以查找到PPN,与VPO组合成PA,或者说VPO直接对应了PPO,它们组成PA并且向TLB中添加条目。
7.5 三级Cache支持下的物理内存访问
CPU发出一个虚拟地址再TLB里搜索,如果命中,直接发送到L1cache里,如果没有命中,就现在也表里加载到之后再发送过去,到了L1中,寻找物理地址又要检测是否命中,如果没有命中,就向L2/L3中查找。这就用到了CPU高速缓存,这种机制加上TLB可以是的机器再翻译地址的时候性能得以充分发挥。

7.6 hello进程fork时的内存映射
当fork函数被shell进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。
execve函数执行了以下几个操作:
1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
3.映射共享区域。
4.设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。

7.8 缺页故障与缺页中断处理
在虚拟内存中,DRAM缓存不命中称为缺页。

如图,引用VP3时发现其不再DRAM中,除法缺页异常,调用异常处理程序,选择VP4作为牺牲页,修改VP4的页表条目。

缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送VA到MMU,这次MMU就能正常翻译VA了。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap) 。堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址) 。对于每个进程,内核维护着一个变量brk, 它指向堆的顶部。
配器将堆视为一组不同大小的块(block) 的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它被应用所分配。一个已分配的块保持已分配状态,直到它被释放。
基本方法:这里指的基本方法应该是在合并块的时候使用到的方法,有三种分别是:首次适配,下一次适配和最佳适配。首次适配是从开始处往后搜索,下一次适配是从上一次适配发生处开始搜索,最佳适配依次检查所有块,性能要比首次适配和下一次适配都要高。
策略:分为隐式空闲链表和显示空闲链表。任何实际的分配器都需要一些数据结构,允许他来区别块边界,以及区别已分配块和空闲块。大多数分配器将这些信息嵌入块本身。
隐式空闲链表:通过头部中的大小字段隐含的连接。分配器可以通过遍历堆中的所有块,从而间接地遍历整个空闲块地集合。

显示空闲链表:因为根据定义,程序不需要一个空闲块地主题,所以实现这个数据结构地指针可以存放在这些空闲块的主体里。
7.10本章小结
本章详细介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,在指定环境下介绍了VA到PA的变换、物理内存访问,还介绍了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。让我们对于hello的存储内存有了更加深刻的了解.
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这就是Unix I/O接口。
8.2 简述Unix IO接口及其函数
Unix I/O接口统一操作:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
2.Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
3.改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
4.读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5.关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

Unix I/O函数:
1.int open(char* filename,int flags,mode_t mode) ,进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
2.int close(fd),fd是需要关闭的文件的描述符,close返回操作结果。
3.ssize_t read(int fd,void *buf,size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
4.ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
可以参考https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

首先我们看一下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; 
}

在形参列表里有这么一个token,这个是可变形参的一种写法。当传递参数的个数不确定时,就可以用这种方式来表示。然后i = vsprintf(buf, fmt, arg);arg获得第二个不定长参数,即输出的时候格式化串对应的值。
我们接着来看让我们来看看vsprintf(buf, fmt, arg)是什么函数。
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结合参数args生成格式化之后的字符串,并返回字串的长度。在printf中调用系统函数write(buf,i)将长度为i的buf输出。write函数如下:
最后就是write函数了,我们先来看一下write的汇编代码:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
其中int INT_VECTOR_SYS_CALL的作用是调用sys_call这个函数,应该就是这个函数驱动了显示器,于是我们再追踪一下这个函数的反汇编:
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
可以看出代码里面的call是访问字库模板并且获取每一个点的RGB信息最后放入到eax也就是输出返回的应该是显示vram的值,然后系统显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
输入/输出(I/O) 是在主存和外部设备(例如磁盘驱动器、终端和网络)之间复制数据的过程。本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf函数和getchar函数。让我们了解了Unix I/O.而了解Unix I/O 将帮助我们理解其他的系统概念。I/O 是系统操作不可或缺的一部分.因此,我们也经常遇到I/O 和其他系统概念之间的循环依赖。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

Hello从开始创建到结束的一生终于结束了,在这个过程中主要发生了:

  1. hello.c通过键盘鼠标等I/O设备在codeblocks下书写,然后通过文件的方式储存在主存里面。
  2. 预处理器将hello.c预处理成为hello.i
  3. 编译器将hello.i翻译成汇编语言hello.s
  4. 汇编器将hello.s汇编成可重定位二进制代码hello.o
  5. 链接器将外部文件和hello.o链接成为可执行二进制文件hello
  6. shell进程调用fork为其创建子进程
  7. shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数。
  8. 执行指令,CPU为hello分配时间片,hello在一个时间片中执行自己的逻辑控制流。
  9. 访问内存,MMU将虚拟内存映射成物理地址
  10. 动态内存申请,malloc
  11. 异常情况的处理等等
  12. 结束,shell父进程回收子进程。

我们从上面的hello的一生可以看出在整个hello从开始到结束的过程中,视觉神经系统是如何进行分工合作的,如何将一个hello.c一步步的执行如何还有各种情况的处理等等,让我们更加清楚的了解到了计算机的底层架构

(结论0分,缺少 -1分,根据内容酌情加分)

附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.i 预处理之后文本文件
hello.s 编译之后的汇编文件
hello.o 汇编之后的可重定位目标执行
Hello 链接之后的可执行目标文件
Hello2.objdmp Hello.o的反汇编代码
hello.objdmp Hello的反汇编代码
hello.elf hello.o文件的ELF格式
hello2.elf hello程序的ELF格式文件
(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.
(参考文献0分,确实 -1分)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值