2021-06-28

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机类
学   号 1190200405
班   级 1903005
学 生 孙旭   
指 导 教 师 史先俊

计算机科学与技术学院
2021年5月
摘 要
本文借助Linux环境下开发工具,历经hello.c源代码的编写、cpp预处理生成hello.i文件、cll编译生成hello.s汇编语言文件、as汇编生成hello.o可重定位目标文件、ld链接生成hello可执行目标文件,再经由shell执行进程等过程,详细描绘了hello在计算机中孕育,出生,成长及消亡的灿烂一生。
关键词:hello;计算机系统;编译;汇编;链接;进程;内存;信号

目 录

第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简介
1.1.1 计算机系统的术语简述Hello的P2P,020的整个过程。
1.1.1.1 P2P:From Program to Process
(1)编辑源代码:通过编辑器编写源代码 .c文件.
(2)预处理阶段:预处理器cpp根据以字符#开头的命令,修改原始的程序。
(3)编译阶段:编译器ccl将文本文件hello.i翻译成文本文件hello.s。
(4)汇编阶段:汇编器as将hello.s翻译成机器语言二进制指令,并把这些 内容打包成一种叫做可重定位目标程序的格式,结果保存在hello.o中。
(5)链接阶段:链接器ld的作用是合并程序中用到的其他函数的指令,得 到 最终的可执行目标程序/文件。(如图1.1-1)
1.1.1.2 020:From Zero-0 to Zero-0
(1)通过shell输入文件名,shell通过execve在fork创建的一个新的子进 程 中加载hello。
(2)通过虚拟内存机制为hello规划空间,创建新的区域结构保存代码段、 已 初始化数据段、bss、栈等。
(3)CPU从代码段中不断取出代码、数据段中不断取出数据、调度器为 hello规划进程执行的时间片,通过I/O管理控制输入输出。
(4)运行结束,shell父进程回收hello,内核中删除hello相关数据,hello完成 020过程。

图1.1-1 P2P过程

1.2 环境与工具
1.2.1使用的软硬件环境以及开发与调试工具。
(1)硬件环境:
X64 CPU ,2.50GHz , 8G RAM
(2)软件环境:Windows 10 64位 ,Vmware 14 ,Ubuntu 16.04 LTS 64 位
开发工具:gcc + gedit , Codeblocks , gdb edb
1.3 中间结果
1.3.1生成的中间结果文件的名字,文件的作用等。
文件名称 功能
hello.i 预处理之后文本文件
hello.s 编译之后的汇编文件
hello.o 汇编之后的可重定位目标执行文件
hello.o.txt hello.o 的反汇编代码
hello 链接之后的可执行目标文件
hello.elf.txt hello.o 的 ELF 格式
hello.txt hello.o 的反汇编代码

1.4 本章小结
从整体上介绍了hello程序的孕育,即预处理、编译、链接等,概述了hello程序的执行过程。

(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
2.1.1预处理概念
以“#”号开头的预处理指令如包含#include,宏定义制定#define等,在源程序中这些指令都放在函数之外,而且一般放在源文件的前面 ,所谓预处理其实就是在编译的第一遍扫描之前的所作的工作,预处理是C语言的一个重要的功能,它由预处理程序单独完成。当对一个源文件进行编译时,系统自动引用预处理程序,预处理在源代码编译之前对其进行的一些文本性质的操作,对源程序编译之前做一些处理,生成扩展的C源程序。

2.1.2预处理的作用
预处理命令由#(hash字符)开头, 它独占一行, #之前只能是空白符.预处理对其 中的伪指令(以#开头的指令)和特殊符号进行处理。
(1)头文件包含指令:
预处理器读取系统头文件xxx.h的内容,并将其直接插 入程序文本中。如:stdio.h、 unistd.h、 stdlib.h等。
(2)宏定义指令:用实际值替换用#define 定义的字符串。
(3)条件编译指令:如#ifdef、#ifndef、#else、#elif、#endif等等。这些伪指令 的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行 处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
(4)特殊符号:预编译程序可以识别一些特殊的符号。例如在源程序中出现的 LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的.C 源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行 替换。

2.2在Ubuntu下预处理的命令
(1)使用指令:gcc -E hello.c -o hello.i 执行预处理(如图2.2-1)

图2.2-1 Ubuntu下预处理

2.3 Hello的预处理结果解析
(1)执行结果(如图2.3-1)

图2.3-1 预处理结果
(2)结果分析:
1.由527字节扩展至64732字节,说明在与处理过程中,将头文件stdio.h、 unistd.h、stdlib.h进行了读取插入生成预处理文件的操作。
2.main函数并无变化,在预处理过程中,只对头文件处理,并不改变mian 函数。

2.4 本章小结
通过对hello文件预处理的详细操作即分析,较为直观的了解了预处理阶段,理解其操作方式及作用。

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
(1)编译的概念:
编译就是将编写的高级语言转换成计算机可识别的汇编指令的过程。
(2)编译的作用:
编译器ccl通过词法分析、语法分析、语义检查及中间代码生成、代码优化、 目标代码生成五个阶段将预处理过的hello.i文件编译成汇编文件hello.s。

3.2 在Ubuntu下编译的命令
(1)使用gcc -S hello.i -o hello.s 指令编译(如图3.2-1)

图3.2-1 程序编译
3.3 Hello的编译结果解析
3.3.1汇编初始部分解析
(1)观察汇编文件前几行(如图3.3-1)

图3.3-1 汇编文件
(2)文件解析 (如表3.3-1)
表3.3-1 汇编解析
指令 功能
.file 声明源文件
.text 代码段
.section .rodata 以下是 rodata 节
align 声明对指令或者数据的存放地址进行对齐的方式
.string 字符串
.globl 声明一个全局变量
.type 用来指定是函数类型或是对象类型

(3)其他指令声明:
.size 生命大小
.long 长整形数据

3.3.2数据类型
程序中共涉及到三种数据类型,分别是整数类型、字符串类型、数组。
(1)整数
1.局部变量:
int i为局部变量,并且未初始化,在运行时为其分配空间,存储在栈中,使 用寄存器对其进行传递。在栈或寄存器中对其进行修改,如图,初始时赋值 为0,存于栈空间-4(%rbp)中:

2.立即数:
以立即数形式存在的整数类型直接硬编解在汇编代码中。
(2)字符串:

字符串常量存放在.text节,调用字符串时传递其地址。
第一次调用printf(“用法: Hello 学号 姓名 秒数!\n”)函数时将字符串"用法: Hello 学号 姓名 秒数!\n"地址传递给寄存器rdi:

第二次调用printf(“Hello %s %s\n”,argv[1],argv[2]);函数时,将字符串 "Hello %s %s\n"地址传递给寄存器rdi:

(3)数组
应用数组的时候将数组元素的首地址加载到堆栈中,利用相对寻址的方式查 找使用数组中的每一个元素:

使用的是字符串指针,每个元素占8个字节,故第i个元素所处位置通过指 针偏移查找时计算公式:首地址+8*i。通过对临时寄存器%rax加8来访问 argv[1], 加16访问argv[2],加24访问argv[3]。
3.3.3操作指令
(1)mov
在定义整型变量i时,通过指令movl $0, -4(%rbp)对储存在-4(%rbp)处的i 进行赋值:

move指令根据传送字节大小的不同共分为四种movb(字节)、movw(字)、 movl(双字)、movq(四字)。
(2)add i++,对计数器i进行自增操作。
(3)sub S D,将D-S存入D中。

(4)lea 汇编中使用:

传递地址。
(5)cml 比较操作,设置条件码

(6)jxx jump根据条件吗进行跳转,执行目标代码,

(7)call 以相对寻址的方式跳转执行下一条指令:

3.3.4sizeof
(1)sizeof为单目运算符,获取某个数据类型所占的字节数,例如i为整型变量,为4字节、argc为整形变量,为4字节,*argv为指针型字符串,其中每个元素所占字节数为8字节。

3.3.5算数操作
(1)对i的加法运算,
(2)对数组访问时偏移地址操作,进行加法运算,

3.3.6关系操作

(1)通过比较存储在-4(%rbp)处的i与立即数7,确定是否跳转执行其他指令, 对应于.c源程序中for循环中的i<8比较操作。

(3)将4与 -20(%rbp)处的值进行比较,等效与.c源程序argc!=4。

3.3.7数组/指针操作
(1)通过对字符串指针数组argv寻址的方式来读取参数,在.L4中,通过mov 以及add的偏移寻址,对argv的数组元素进行访问。argv数组初始地址为 -32(%rbp),在访问数组中元素时,通过对临时寄存器%rax加8访问argv[1], 加16访问argv[2],加24访问argv[3]。(如图3.3-2)

图3.3-2 寻址读参

3.3.8 控制转移
(1)LFB6中,判断-20(%rbp)中的所存数据是否等于4,若不相等,则跳转至 L.2,分析可知,对应.c源程序:(如图3.3-3 )

图3.3-3 判断是否等于四

(2) 判断-4(%rbp)中所存数据是否小于等于7,如果是,则跳转至.L4,继续循环,分析可知,对应.c源程序(图3.3-4)

图3.3-4 判断是否小于等于七

3.3.9 函数操作
(1)运行时栈的分配

  1. 假设过程P(调用者)调用过程Q(被调用者),则Q的参数放在P的栈帧中。
  2. 另外,当P调用Q时,P中的返回地址被压入栈中,形成P的栈帧的末 尾。
  3. 返回地址就是当程序从Q返回时应该继续执行的地方。(如图3.3-5)
    (2)函数调用及参数传递
  4. 64位操作系统传递参数的基本规则为:如果小于7个参数,用寄存器来传 递参数,如果大于等于七,则会使用栈来传递。
  5. 六个寄存器传递参数顺序为:%rdi,%rsi,%rdx,%rcx,%r8,%r9。
    (3)hello程序调用函数
  6. main函数:
    函数调用:被_libc_start_main调用,call指令将其地址压入栈,然后跳转执 行 main函数。
    参数传递:传入参数argc和argv,分别用寄存器%rdi和%rsi存储。
    函数返回:设置%eax为0并且返回,对应return 0 。
    2.printf函数:
    函数调用:第一次 printf 使用call puts@PLT;第二次 printf 使用 call 3.printf@PLT。
    参数传递:第一次 printf 将%rdi 设置为"用法: Hello 学号 姓名 秒数!\n"字符串的首地址。第二次 printf 设置%rdi 为“Hello %s %s\n” 的首地址,设置%rsi 为 argv[1],%rdx 为 argv[2]。
    4.exit函数:
    函数调用:if判断条件满足后并在if内容基本执行完成后被调用,对应于汇 编文件中语句call exit@PLT。
    参数传递:传入的参数为1,并将%edi 设置为 1,再执行退出命令sleep函 数。
    函数调用:for循环下被调用,对应于汇编文件中的call sleep@PLT。
    参数传递:传入参数atoi(argv[3])函数的返回值。
    5.atoi函数:
    函数调用:使用atoi@PLT调用函数。
    参数传递:将%rax值复制到%rdi,以%rdi寄存器传递参数。
    参数返回:返回整形值。
    6.getchar
    函数调用:在main中被调用,对应于汇编文件中的call gethcar@PLT。

图3.3-5 运行时栈分配
3.4 本章小结
本章内容主要通过对hello程序由.c文件转换成.s文件的分析,揭示了更低层级上程序实现过程–汇编指令,例如赋值,类型转换,算数操作,指针操作等。汇编语言是介于高级语言与机器语言的桥梁,起到连通两者的作用。认识了解汇编语言,对于代码的优化及bug的排除具有深厚优势,值得认真钻研。

(第3章2分)

第4章 汇编
4.1 汇编的概念与作用

(1)概念与作用
汇编器(as)将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定 位目标程序的格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制 文件,它包含程序的指令编码。
4.2 在Ubuntu下汇编的命令
(1)使用gcc -c hello.s -o hello.o 指令汇编(如图4.2-1)

图4.2-1 汇编指令
4.3 可重定位目标elf格式
(1)hello.o的ELF格式、各节的基本信息
应用指令:readelf -a hello.o 查看hello.o ELF格式信息。
应用指令:readelf -a hello.o > hello.elf.txt 打印hello.o ELF格式信息。
1.ELF头:
ELF头以一个16字节的magic序列开始,这个序列描述了生成该文件的系 统的字的大小与字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解 释目标文件信息。其中包括ELF头的大小、目标文件的类型(如可重定位、 可执行或者可共享的)、机器类型(如x86-64)、节头部表的文件偏移,以 及节头部表条目的大小和数量。不同节的位置和大小是由节头部表描述的, 其中目标文件中每个节都有一个固定大小的条目。(如图4.3-1)

图4.3-1 ELF头信息

2.节头:
包含文件中出现的各个节的语义,包括节的类型、位置和大小信息。(如图 4.3-2)
3.重定位节:
偏移量:需要被修改的引用的节的偏移。
信息:包括 symbol 和 type 两部分, 其中 symbol 占前 4 个字节, type 占后 4 个字节,symbol 代表重定位到的目标在.symtab中的偏移量,type 代 表重定位的类型。
类型:告知链接器如何修改新的引用。两种最基本的重定位类型,一为 R_X86_64_PC32 ,重定位一个使用32位PC相对地址的引用;二为, R_X86_64_32 ,重定位一个使用32位PC绝对地址的引用。
符号名称:重定位目标的名称。
加数:一个有符号常熟,一些类型的重定位要使用它对被修改引用的值做便 宜调整。
七条重定位信息分别对应.L0 、puts函数、exit函数、.L1、printf函数、atoi 函数、sleep函数、getchar函数。(如图4.3-3)
4.Symbol Table:
目标文件的符号表中包含用来定位、重定位程序中符号定义与引用的信息-- 程序中定义和引用的函数和全局变量的信息。符号表索引是对此数组的索 引。.symtab符号表不包含局部变量的条目。(如图4.3-4)

图4.3-2 节头

图4.3-3 重定位节

图4.3-4 符号节

4.4 Hello.o的结果解析
(1)查看反汇编信息
指令:objdump -d -r hello.o 查看hello.o反汇编。
指令:objdump -d -r hello.o > hello.o.txt 打印hello.o反汇编(如图4.4-1)

图4.4-1 反汇编信息

(2)反汇编结果(图4.4-2)

图4.4-2 hello.o 对照 hello.s
1.操作数变化:
hello.s中的十进制操作数转变成了hello.o反汇编中的十六进制数。
2.跳转语句:
跳转语句已经完成地址计算,由hello.s中的符号表示转变成了hello.o反汇编 中的确切地址,并通过相对寻址偏移跳转。
3.函数:
hello.s中的‘call+函数名’变成了hello.o反汇编中的‘call+下条指令’,最 终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语 言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设 置为全0。
4.全局变量的引用:
在hello.s文件中,访问rodata,使用段名称+%rip;在反汇编代码中0+%rip, 因为rodata中数据地址也是在运行时确定,故访问也需要重定位。所以在汇 编成为机器语言时,将操作数设置为全0并添加重定位条目。
4.5 本章小结
本章通过对重定位目标文件转化.elf格式,简介了汇编器对汇编的处理结果,hello.s与hello.o.elf的对比,分析了机器语言和汇编语言的映射关系。了解到了从汇编语言到机器语言需要做的转换。

(第4章1分)

第5章 链接
5.1 链接的概念与作用
(1)链接的概念和作用:
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件 可被加载到内存并执行。链接可以执行于编译时,也就是在源代码被编译成 机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执 行时;甚至于运行时,也就是由应用程序来执行。链接是由叫做链接器的程 序执行的。
5.2 在Ubuntu下链接的命令
(1)使用链接指令链接
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.2-1)

图5.2-1 链接
5.3 可执行目标文件hello的格式
(1)列出其各段的基本信息,包括各段的起始地址,大小等信息。
应用指令:readelf -a hello 查看hello的ELF格式。
应用指令:readelf -a hello > hello.elf.txt 打印hello的ELF的信息。(如图5.3-1)
图5.3-1 列出信息指令

1.elf头:(如图5.3-2)

图5.3-2 elf头

3.可执行文件头部表:(如图5.3-3 5.3-4 5.3-5)

图5.3-3 可执行文件头部表

图5.3-4 可执行文件头部表

图 5.3-5 可执行文件头部表

4.程序头部表:(如图5.3-6)

图5.3-6 程序头部表

5.Dynamic section:(如图5.3-7)
图 5.3-7 Dynamic section
6.重定位节:(如图5.3-8)

图5.3-8 重定位节表

7.符号节表:(如图5.3-9 5.3-10 5.3-11)

图5.3-9 符号节表
图5.3-10 符号节表

图5.3 -11 符号节表

5.4 hello的虚拟地址空间
PHDR:保存程序头表
INTERP:动态链接器的路径
LOAD:可加载的程序段
DYNAMIN:保存了由动态链接器使用的信息
NOTE保存辅助信息
GNU_STACK:标志栈是否可执行
GNU_RELRO:指定重定位后需被设置成只读的内存区域(如图5.4-1)

图5.4-1 程序头

(1)使用edb查看各段信息(如图5.4-2)

图5.4-2 edb查看信息

从上向下依次为:代码区、数据区、共享区、栈、只读数据区
与5.3相比少了重定位段、符号表、等。
5.5 链接的重定位过程分析
(1)查看hello反汇编:(如图5.5-1)
图5.5-1 hello反汇编

(2)对比hello.o重定位项目:(如图5.5-2 5.5-3 5.5-4 5.5-5 5.5-6)

图 5.5-2 对比hello.o重定位项目

图 5.5-3 对比hello.o重定位项目

图 5.5-4 对比hello.o重定位项目

图 5.5-5 对比hello.o重定位项目

图 5.5-6 对比hello.o重定位项目

hello的反汇编文件中多了其他的库函数,比如printf@plt。
main部分代码相同,但是全局符号的重定位条目已经被具体地址替代
(3)重定位过程分析事例:(如图5.5-4 5.5-7)

图5.5-7 重定位事例分析

1.main地址为401125+1c:(如图5.5-8)

图5.5-8 main地址

2.refaddr = ADDR(s)+r.offset = ADR (main)+r.offset=0x401125+0x1c=0x401141
*refptr = (unsigned) (ADDR(r.symbol) + r.addend -refaddr) = ADDR(strl) + r.addendrefaddr = 0x402008+(0x-4) -refaddr = 0x402004 -0x 401141 = 0xec3

4.因此,hello反汇编中显示如下:(如图5.5-9)

图5.5-9 hello反汇编中显示
5.6 hello的执行流程
(1)调用与跳转的各个子程序名或程序地址。
1.载入:_dl_start、_dl_init
2.开始执行:_start、_libc_start_main
3.执行main:_main、_printf、_exit、_sleep、
_getchar、_dl_runtime_resolve_xsave、_dl_fixup、_dl_lookup_symbol_x
4.退出:exit
程序名称 地址
ld-2.27.so!_dl_start 0x7fb85a93aea0
ld-2.27.so!_dl_init 0x7f9612138630
hello!_start 0x400582
lib-2.27.so!__libc_start_main 0x7f9611d58ab0
hello!puts@plt 0x4004f0
hello!exit@plt 0x400530
5.7 Hello的动态链接分析
(以下格式自行编排,编辑时删除)
(1)动态链接介绍
1.在进行动态链接前,首先进行静态链接,生成部分链接的可执行目标文件 hello。此时共享库中的代码和数据没有被合并到hello中。加载hello时,动 态链接器对共享目标文件中的相应模块内的代码和数据进行重定位,加载共 享库,生成完全链接的可执行目标文件。
2.动态链接采用了延迟加载的策略,即在调用函数时才进行符号的映射。使 用偏移量表GOT+过程链接表PLT实现函数的动态链接。GOT中存放函数目 标地址,为每个全局函数创建一个副本函数,并将对函数的调用转换成对副 本函数调用。
(2)链接前后差异性分析
1.调用init前.got.plt(如图5.7-1)

图5.7-1 调用前

2.调用init后.got.plt(如图5.7-2)

图5.7-2 调用后
调用前后.got.plt发生变化。
5.8 本章小结
本章介绍了有关链接的相关知识,通过使用edb以及objdump的使用更好的区分了重定位目标文件与可执行文件之间的区别。通过对hello程序的分析,了解了它的虚拟地址出储存空间结构,还了解了有关链接过程,链接行为等知识。

(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用

(1)进程的概念
进程是计算机科学中对深刻最成功的概念之一,进程是操作系统对一个正在 运行的程序的一种抽象,进程的经典定义就是一个执行中程序的实例。系统 中每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的 状态组成的。这个状态包括存放在内存中的沉痼的代码和数据,她的栈、通 用目的寄存器的内容,程序计数器、环境变量以及打开文件描述符的集合。 文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的 动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
(2)进程的作用
它提供一个假象,好像我们的程序是系统当前运行的唯一的程序一样。我们 的程序好像是独占的使用处理器和内存。处理器好像无间断的一条接一条的 执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中 唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
(1)Shell-bash介绍
Shell是一个交互型应用级程序,它代表用户运行其它程序。Shell执行一系 列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤 解析命令行,代表用户运行程序。
(2)Shell-bash处理流程
1.终端进程读取用户由键盘输入的命令行。
2.分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
3.检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
4.如果不是内部命令,调用fork( )创建新进程/子进程
5.在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
6.如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或 wait…)等待作业终止后返回。
7.如果用户要求后台运行(如果命令末尾有&号),则shell返回;
6.3 Hello的fork进程创建过程
在终端中输入命令行./hello 1190200405 孙旭 后,shell会处理该命令,如果判断出不是内置命令,则会调用fork函数创建一个新的子进程。
(1)创建过程:
1.给新进程分配一个标识符
2.在内核中分配一个PCB,将其挂在PCB表上
3.复制它的父进程的环境(PCB中大部分的内容)
4.为其分配资源(程序、数据、栈等)
5.复制父进程地址空间里的内容(代码共享,数据写时拷贝)
6.将进程置成就绪状态,并将其放入就绪队列,等待CPU调度。
fork()函数,调用一次,返回二次。一旦调用fork()函数,会生成一份与源文 件一模一样的程序,这就是子进程。如果子进程创建成功,父进程的fork() 函数返回子进程的pid号码(大于0)。这时,子进程的fork()函数返回0; 但是如果子进程创建失败,父进程的fork()函数返回-1,这时子进程没有创 建,也就没有返回了。
7.此外,当子进程创建成功之后,父、子进程是2个独立的进程,通过fork 函数,子进程得到与父进程用户级虚拟地址空间相同的但是独立的一份副 本,拥有不同的PID。
6.4 Hello的execve过程
Hello程序在使用fork创建了子进程之后,在子进程中分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量,调用execve( )执行指定程序。
在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下的原型:
int main(int argc , char **argv , char *envp);
(1)执行过程
1.删除已存在的用户区域。
2.映射私有区域。为Hello的代码、数据、bss和栈区域创建新的区域结构, 所有这些区域都是私有的、写时复制的。
3.映射共享区域。如果由a.out程序与共享对象链接,比如Hello程序与标准 C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址 空间中的共享区域内。
4.设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下 文中的程序计数器,使之指向代码区域的入口点。

6.5 Hello的进程执行
(1)用户态与核心态
为了使操作系统内核提供一个无懈可击的进程抽象,处理器必须提供一种机 制, 限制一个应用可以执行的指令以及它可以访问的地址空间范围。处理 器通常使用 某个控制寄存器的一个模式位提供两种模式的区分,该寄存器 描述了进程当前享 有的特权,当没有设置模式位时,进程就处于用户模式 中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内 核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行 指令集中的任何命令,并且可以访问系统中的任何内存位置。
(2)进程的上下文
操作系统内核使用一中称为上下文切换的较高层形式的异常控制流来实现 多任务:内核为每个进程维持一个上下文,上下文就是内核重新启动一个被 抢占的进 程所需的状态,它由一些对象的值组成,这些对象包括通用目的 寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表, 以及包含进程一打开文件的信息的文件表。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前 被抢占了的进程。这种调度决策,是由内核中调度器的代码处理的。当内核 选择一个新的进程时,内核调度了这个过程。在内核调度了一个新的进程运 行后,他就抢占当前进程,使用上下文切换机制来控制转移到新的进程,上(3)下文切换的流程(如图6.5-1):
1.保存当前进程的上下文。
2.恢复某个先前被抢占的进程被保存的上下文。
3.将控制传递给这个新恢复的进程。

图6.5-1 上下文切换流程

(4)hello的进程调度
hello 在刚开始运行时内核为其保存一个上下文, 进程在用户状态下运行。 如果没有异常或中断信号的产生,hello将继续正常地执行。如果遇到了异常 或者终端信号,那么内核便会根据上述内容执行上下文切换,将控制转换到 其它进程。在执行遇到exit函数时,以status退出状态来终止进程。在hello 运行到 sleep时,sleep函数将当前进程挂起一个指定时间(我外部输入1s), 发生上下文切换,将控制转移至另一个进程;当请求的时间量到了的时候, 再次发生上下文切换,sleep函数返回0,将控制权在转移回原先挂起的函数。 当sleep函数被一个信号中断而过早的返回时,要返回剩下的要休眠的秒数。 当hello的循环结束了之后,调用getchar函数,通过键盘缓冲区DMA 传输 产生陷阱处理程序请求,并执行上下文切换,将控制转移给其他进程。当完 成键盘缓冲区到内存的数据传输后,引发中断信号,内核从其他进程切换回 hello 进程,然后hello执行 return,进程终止。
6.6 hello的异常与信号处理

(1)出现异常情况
执行过程可能出现的异常一共有四种:中断、陷阱、故障、终止。
中断:来自I/O设备的信号,异步发生,总是返回到下一条指令。
陷阱:有意的异常,同步发生,总是返回到下一条指令。
故障:潜在可恢复的错误,同步发生,可能返回到当前指令或终止。
终止:不可恢复的错误,同步发生,不会返回。
1.正常运行:(如图6.6-1)

图6.6-1 正常运行
2.回车(如图6.6-2)

图6.6-2 回车

3.Ctrl-C终止:(如图6.6-3)

图6.6-3 Ctrl-C

4.Ctrl-Z 暂停:(如图6.6-4)
按下Ctrl-z后,shell父进程收到SIGSTP信号,信号处理函数将hello进程挂 起,并在屏幕打印信息。通过ps指令观察hello进程未被回收。

图6.6-4 Ctrl-z

通过bg 指令将hello进程调回前台作业(如图6.6-5)

图6.6-5 bg

输入jobs显示当前作业列表(如图6.6-6)

图6.6-6 jobs
输入pstree(如图6.6-7 6.6-8 6.6-9)
显示计算机正在执行的所有进程的关系。

图6.6-7 pstree

图6.6-8 pstree

图6.6-9 pstree

输入kill -9 PID,杀死进程(如图6.6-10)

图6.6-10 pstree
随意输入,乱输入只是将屏幕的输入缓存到stdin,当getchar函数读出一个’\n’ 结尾的字符串(作为第一次输入),hello结束后,stdin中的其他字符串将 会被当做shell的若干条命令行输入。(如图6.6-11)

图6.6-11 随意输入

6.7本章小结
本章介绍了进程的定义和作用,通过hello程序在shell中运行形式,介绍进程的创建、管理运行,上下文切换、信号等知识。

(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
(1)地址概念
1.逻辑地址:格式为“段地址:偏移地址”,是CPU生成的地址,在内部和 编程使用,并不唯一。
2.线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制 后转化为线性地址。
3.虚拟地址:保护模式下程序访问存储器所用的逻辑地址。
4.物理地址:加载到内存地址寄存器中的地址,内存单元的真正地址。CPU 通过地址总线的寻址,找到真实的物理内存对应地址。在前端总线上传输的 内存地址都是物理内存地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
(1)图示(如图7.2-1)

图7.2-1 段式管理

(2)分析过程
1.逻辑地址是由48位组成的,前16位为段选择符,后32位为段内偏移量。
通过前十六位段选择符中的段描述符部分(TI)确定描述符表是全局描述符 表还是局部描述符表。
2.根据索引部分和GDT或者LDT的存储首地址的寄存器得的到对应表中的 段基址。
3.将段基址和段内偏移相加得到32位线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
(1)图示:(如图7.3-1)

图7.3-1 页式管理
(2)分析过程
1.具有页式结构的线性地址成为虚拟地址,故从线性地址到物理地址的变化 可看作由虚拟地址到物理地址的变化。
2.n位虚拟地址包含两个部分:p位的页面偏移(VPO)和n-p位的虚拟页号 (VPN)。
3.MMU利用VPN选择适当的PTE(页表条目),将页表条目中的物理页号 (PPN)和虚拟地址中的VPO串联起来,生成物理地址。

7.4 TLB与四级页表支持下的VA到PA的变换
(1)图示(如图7.4-1)

图7.4-1 流程图
(2)分析:
1.Core i7采用四级页表的层次结构。
2.CPU产生VA,VA传送给MMU,MMU使用VPN高位作为TLBT和TLBI 向TLB中寻找匹配。
3.如果命中,则得到PA。如果TLB中没有命中,MMU查询页表,CR3确 定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出 PTE, 执行四次,最终在第四级页表中找到PPN,与VPO组合成PA,添 加到PLT。
7.5 三级Cache支持下的物理内存访问
(1)图示(如图7.4-1)
(2)分析
1.已经获得PA,首先取组索引位(CI),在L1Cache中寻找对应组。
2.如果存在,则比较标志位,并检查对应行的有效位是否为1。
3.如果上述条件均满足则命中,否则按顺序对L2Cache、L3Cache、内存进行 相同操作,直到出现命中,逐层向上级Cache提取直到L1Cache。
4.在逐层提取过程中如有空闲块则将目标块放置到空闲块中,否则按照一定 替换策略将缓存中的某个块驱逐,将目标块放到被驱逐块的原位置。
5.最终通过L1Cache获得PA地址下的内存内容。
7.6 hello进程fork时的内存映射
在shell输入命令行后,内核调用fork创建子进程,为hello程序的运行创建 上下文,并分配一个与父进程不同的PID。通过fork创建的子进程拥有父进 程相同的区域结构、页表等的一份副本,同时子进程也可以访问任何父进程 已经打开的文件。当fork在新进程中返回时,新进程现在的虚拟内存刚好和 调用fork时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作 时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址 空间。
7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程 序。加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的 区域结构。
2.映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构, 所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其 大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
3.映射共享区域, hello程序与共享对象libc.so链接,libc.so是动态链接到 这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下 文的程序计数器,使之指向代码区域的入口点。

7.8 缺页故障与缺页中断处理

如果程序执行过程中遇到了缺页故障,则内核调用缺页处理程序。处理程序 会进行如下步骤:检查虚拟地址是否合法,如果不合法则触发一个段错误, 程序终止。然后检查进程是否有读、写或执行该区域页面的权限,如果不具 有则触发保护异常,程序终止。在两步检查都无误后,执行缺页异常处理程 序。(如图7.8-1)

图7.8-1 处理流程

  1. 处理器将虚拟地址发送给 MMU
    2.3. MMU 使用内存中的页表生成PTE地址
    4.有效位为零, 因此 MMU 触发缺页异常
  2. 缺页处理程序确定物理内存中牺牲页 (若页面被修改,则换出到磁盘)
    6.缺页处理程序调入新的页面,并更新内存中的PTE
    7.缺页处理程序返回到原来进程,再次执行缺页的指令
    7.9动态存储分配管理
    (1)分配管理介绍
    程序在运行过程中通过动态内存分配器获得虚拟内存,动态内存分配器维护 着的一个虚拟内存区域称为堆。分配器将堆视为一组不同大小的块的集合来 维护。每个块就是一个连续的虚拟内存的片,要么是已分配的,要么是空闲 的。已分配的块显式的供应程序使用。空闲块可以用来分配。已分配的块保 持已分配状态,直到它被释放。
    (2)分配器风格分类:
    显式分配器:应用程序显式执行释放已分配块
    隐式分配器:内存分配器自动检测分配块何时不再使用,并释放这个空闲块。
    (3)空闲链表实现方式:
    1.隐式空闲链表:通过头部中的大小字段隐式的连接空闲块。(如图7.9-1)
    2.显式空闲链表:在空闲块中使用指针连接。(如图7.9-2)
    3.分离的空闲链表:将所有的块分为一些等价类,称为大小类,维护多个 空闲链表,每个链表中的块有大致相等的大小。
    4.块按大小排序:例如平衡树,在每个空闲块中使用带指针的平衡树,并 用长度作为权值。

图7.9-1 隐式

图7.9-2 显示
7.10本章小结
本章简述了在计算机中的虚拟内存管理,虚拟地址、物理地址、线性地址、逻辑地址的变换方式,以及Intel段式、页式的管理模式,认识了fork和execve函数在程序运行中的作用,了解了动态存储分配模式的一般情况。

(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
设备的模型化:所有IO设备都被模型化为文件,所有的输入和输出都能被当做相应文件的读和写来执行。
设备管理:Linux内核有一个简单、低级的接口,成为Unix I/O,是的所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
(1)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.关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到 可用的描述符池中去。
(2)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的实现分析
    (1)从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80 或syscall等.
    字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB 颜色信息)。
    显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每 一个点(RGB分量)。
    前提:printf和vsprintf代码是windows下的。
    查看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;
    }
    首先arg获得第二个不定长参数,即输出的时候格式化串对应的值。
    查看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’: //只处理%x一种情况
    itoa(tmp, ((int)p_next_arg)); //将输入参数值转化为字符串保存在tmp
    strcpy(p, tmp); //将tmp字符串复制到p处
    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:
    mov eax, _NR_write
    mov ebx, [esp + 4]
    mov ecx, [esp + 8]
    int INT_VECTOR_SYS_CALL
    在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个 字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall,查看 syscall的实现:
    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
    syscall将字符串中的字节“Hello 1190200405 孙旭”从寄存器中通过总线复 制到显卡的显存中,显存中存储的是字符的ASCII码。
    字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存 储到vram中。
    显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器 传输每一个点(RGB分量)。
    于是我们的打印字符串“Hello 1190200405 孙旭”就显示在了屏幕上。
    8.4 getchar的实现分析
    (1)异步异常-键盘中断的处理
    当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一 个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序 先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码, 保存到系统的键盘缓冲区之中。getchar函数落实到底层调用了系统函数 read,通过系统调用read读取存储在键盘缓冲区中的ASCII码直到读到回车 符然后返回整个字串,getchar进行封装,大体逻辑是读取字符串的第一个字 符然后返回。
    8.5本章小结
    Linux提供了一种简单使用的抽象——将系统的IO设备抽象成文件,系统的输入和输出被抽象成文件的写和读操作。在此基础上,Linux对系统IO的操作可以以打开文件、改变文件位置、读写文件、关闭文件的操作进行。同时分析了printf和getchar的实现。

(第8章1分)
结论
(1)过程
1.首先用高级语言输出生成了hello.c文件。
2.后通过cpp预处理器将hello.c文件根据以#开头的命令,修改原始C程序 得到了hello.i文件。
3.编译器ccl将文件hello.i翻译成文本文件hello.s,包含一个汇编语言程序。
4.汇编器ac将hello.s翻译成机器语言指令,并把这些指令打包成可重定位目 标文件,存入文件hello.o。
5.链接器ld将hello.o链接成为一个可执行文件,hello开始可以被建在到内 存中,由系统执行。
6.在shell中输入一个./hello 1190200405孙旭 1,shell 为hello fork 一个子 进程,并在子进程中调用 execve,加载运行 hello。
7.CPU为hello分配内存,将hello从磁盘加载到内存之中。
8.在一个时间片中,hello有自己的CPU资源,顺序执行逻辑控制流。
9.当 CPU 访问 hello 时,请求一个虚拟地址,MMU 把虚拟地址转换成物 理地址并通过三级 cache 访存。
10.hello在运行过程中会有异常和信号等,shell 为各种信号提供了各种信号 处理程序
11.Unix I/O 帮助 hello 实现了与外接显示设备以及输入设备的连接,即实现 了输出到屏幕和从键盘输入的功能。
12.shell父进程回收hello子进程,内核删除为hello创建的所有数据结构。
13.hello程序结束。

(2)感想
hello程序终于结束了他的一生。但深入理解计算机系统带给我的启发将是永久的。

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

附件

文件名称 功能
hello.i 预处理之后文本文件
hello.s 编译之后的汇编文件
hello.o 汇编之后的可重定位目标执行文件
hello.o.txt hello.o 的反汇编代码
hello 链接之后的可执行目标文件
hello.elf.txt hello.o 的 ELF 格式
hello.txt hello.o 的反汇编代码

(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] https://blog.csdn.net/weixin_30908649/article/details/99150371 线性地址转换
[2] https://www.cnblogs.com/pianist/p/3315801.html printf函数实现的深入分析
[3] https://blog.csdn.net/Pipcie/article/details/105670156 逻辑地址到线性地址
[4] https://blog.csdn.net/smile_zhangw/article/details/80917571 编译器链接原理
[5] https://blog.csdn.net/KomaCC/article/details/80282611 Unix系统级IO函数

带图具体版详见CSDN资源
链接:https://download.csdn.net/download/zijin__/19864044

(参考文献0分,缺失 -1分)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值