程序人生-Hello’s P2P

程序人生-Hello’s P2P


摘 要
关键词:深入了解计算机系统 ;hello world;程序的生命周期
本论文通过详细分析一个hello程序在Ubuntu系统下的从代码到执行结束的整个过程,从与处理、编译、汇编、链接、进程管理、存储管理和IO管理这几个方面,结合CSAPP课本相关章节,分析hello过程出现的各种现象,理解计算机的各种行为,力求达到课本知识与实践结合,融会贯通的效果.
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

目 录

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

第1章 概述
1.1 Hello简介
Hello程序的生命周期是从一个源程序,或者说源文件的,即程序员通过编辑器创建并保存的文本文件,文件名是hello.c.
在操作系统中hello.c经过预处理器cpp的预处理,生成hello.i文件、编译器ccl的编译生成hello.s文件、汇编器as的汇编,生成hello.o文件、连接器ld的链接最终成为可执行目标程序hello.
然后在shell里面输入 ./hello 后,系统会将这些字符读入到寄存器中,开始将Hello目标文件中的代码和数据从磁盘复制到内存,调用fork函数创建一个新的子进程,子进程通过execve系统调用加载器.操作系统保存shell的上下文,创建一个新的hello进程的上下文,并将控制权转给新的进程.
利用虚拟内存技术,新的代码段和数据段被初始化为hello目标文件的内容.然后,加载器跳转到_start地址,之后会来到 main 函数的地址.
程序从内存读取指令字节,在执行阶段执行指令指明的操作.在流水线化的系统中,待执行的程序被分解成几个阶段,每个阶段完成指令的一部分.Hello就这样被执行了.
Hello进程终止后,操作系统恢复shell进程的上下文,并将控制权传回shell,shell进程等待下一个输入.

1.2 环境与工具
硬件环境:Intel Core i7-6700HQ x64CPU 8G RAM 256G SSD +1T HDD.
软件环境:Ubuntu18.04.1 LTS
开发与调试工具:codeblocks,gcc,gdb,readelf,HexEdit,ld
1.3 中间结果
文件名 文件作用
hello.i 预处理器修改了的源程序,分析预处理器行为
hello.s 编译器生成的编译程序,分析编译器行为
hello.o 可重定位目标程序,分析汇编器行为
hello 可执行目标程序,分析链接器行为
elf.txt hello.o的elf格式,分析汇编器和链接器行为
objdump.txt hello.o的反汇编,主要是为了分析hello.o
5.3helloelf.txt 可执行hello的elf格式,作用是重定位过程分析
5.5helloobjdump.txt 可执行hello的反汇编,作用是重定位过程分析

1.4 本章小结
本章主要简要介绍了hello程序P2P,020的过程列出了实验中生成的中间文件,列出了实验使用的硬件条件,系统版本.

(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
程序设计语言的预处理的概念:根据以符号“#”开头的预处理命令,在编译之前进行的处理.
C语言的预处理主要有三个方面的内容:
1.宏定义
2.文件包含
3.条件编译
作用:
文件包含:根据以字符#开头的命令,修改原始的C程序.比如hello.c中第1行的#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中, 该过程递归进行,及被包含的文件可能还包含其他文件.
宏定义:将宏名替换为文本,即在对相关命令或语句的含义和功能作具体分析之前就要换,例如
#define PI 3.1415926,把程序中全部的标识符PI换成3.1415926
条件编译:有些语句希望在条件满足时才编译.例如
#ifdef
标识符
程序段1
#else
程序段2
#endif
当标识符已经定义时,程序段1才参加编译.
结果就得到了另一个C程序,通常是以.i作为文件扩展名.
2.2在Ubuntu下预处理的命令
命令:cpp hello.c > hello.i
在这里插入图片描述
图2.2.1 Ubuntu下预处理的命令

2.3 Hello的预处理结果解析
使用notepad++打开hello.i文件,可以发现整个文件已经被扩展成了3118行
在这里插入图片描述
图2.3.1 Ubuntu下预处理的结果

而前面的3000行就是.c文件中包含的头文件,这里体现的就是预处理器根据以字符#开头的命令,修改原始的C程序.
在这里插入图片描述
图2.3.2 包含的头文件

2.4 本章小结
本章主要介绍了预处理的概念与作用,执行预处理的命令,并结合hello.i文件,解析了hello的预处理结果.
可见预处理过程是只后所有操作的基础,是无可或缺的重要过程.

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序.该程序包含函数main的定义,这个过程称为编译.
编译的作用就是将高级语言源程序翻译成等价的目标程序,并且进行语法检查、调试措施、修改手段、覆盖处理、目标程序优化等步骤.

3.2 在Ubuntu下编译的命令
输入gcc –S hello.i –o hello.s
在这里插入图片描述

图3.2.1 Ubuntu下编译的命令

3.3 Hello的编译结果解析
3.3.1数据的声明和赋值

  1. Int型数据sleepsecs
    在程序中被声明为long类型全局变量,且已经被赋值,.data节存放已经初始化的全局和静态C变量,所以编译器处理时在.data节声明该变量
    声明:
    在这里插入图片描述
    赋值:
    在这里插入图片描述

  2. 局部变量 int i
    编译器将局部变量存储在寄存器或者栈空间中.在hello.s中编译器将i存储在栈上空间-4(%rbp)中
    对i赋值:
    在这里插入图片描述
    3.3.2类型转换
    在对int 数据sleepsecs的赋值过程有一次隐式类型转换,.c文件中int sleepsecs=2.5;但是2.5会被改成2,原因是当在double或float向int进行类型转换的时候,值会向零舍入.例如1.999将被转换成1,-1.999将被转换成-1.
    其他类型转换原则如下:(摘自书本)
    在这里插入图片描述
    图3.3.1 强制类型转换介绍

3.3.3算数操作
汇编指令如下:
在这里插入图片描述
图3.3.1 算数操作汇编指令

编译器将i++编译成
在这里插入图片描述
3.3.4逻辑/位操作
编译器将argc!=3编译成
在这里插入图片描述
je指令在上一个比较语句结果是相同时生效
3.3.5关系操作
编译器将i<10编译成
在这里插入图片描述
小于等于9,符合条件跳转
3.3.6数组/指针/结构操作
数组char *argv[]
是一个指针数组,其中每个元素指向函数执行时输入的各个命令行参数,起始地址argv存在%rsi中,在main中被存入栈中.
在这里插入图片描述
然后通过%rbp+(偏移量)的方式读取:
在这里插入图片描述
3.3.7控制转移

  1. if(argc!=3)这个条件判断被编译成
    在这里插入图片描述
    je指令在上一个比较语句结果是相同时执行跳转,实现了这个if判断

  2. for(i=0;i<10;i++)这个循环语句被编译成
    在这里插入图片描述

-4(%rbp)就是存放的i,每次执行+1,cmpl指令把i和立即数9比较,小于等于9,符合条件,jle指令执行跳转,实现了这个for循环.

3.3.8函数操作
1.main函数
函数调用:Main函数被.call调用(__libc_start_main),call指令将下一个指令的地址压入栈中,然后跳转到main执行.
参数传递:向main函数传递的参数argc和argv,分别使用%rdi和%rsi存储
函数返回:函数设置%eax为0后就正常退出leave.
在这里插入图片描述

2.printf函数
函数调用&参数传递:两次.
第一次:
在这里插入图片描述
将%rdi设置为“Usage: Hello 学号姓名!\n”字符串的首地址.
在这里插入图片描述
其中.LC0(%rip)为:
在这里插入图片描述
然后call puts@PLT(只有一个字符串参数)
第二次:
在这里插入图片描述
将%rdi设置%rdi为“Hello %s %s\n”的首地址
在这里插入图片描述
其中.LC1(%rip)为
在这里插入图片描述
然后call printf@PLT

3.sleep函数
函数调用&参数传递:只有一个参数,将%edi设置为想要休眠的秒数传参,然后call sleep@PLT:
在这里插入图片描述

4.getchar函数
函数调用&参数传递:无参数,直接call getchar@PLT

3.4 本章小结
本章节主要叙述了编译器是如何处理c语言程序的,结合c语言中的各种数据类型,各类运算,各类函数操作,逐个分析编译器的具体行为.
在编译阶段,编译器将高级语言编译成汇编语言.汇编语言是直接面向处理器的语言.汇编语言指令是机器指令的一种符号表示,而不同类型的CPU 有不同的机器指令系统,也就有不同的汇编语言,所以,汇编语言程序与机器有着密切的关系.
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将.s汇编程序翻译成机器语言,把这些机器语言指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中,这个过程就叫做汇编.
汇编的作用就是翻译生成机器语言,因为机器语言是计算机能直接识别和执行的一种语言.
在这里插入图片描述
4.2 在Ubuntu下汇编的命令
命令:as hello.s -o hello.o
在这里插入图片描述
图4.2.1Ubuntu下汇编的命令

4.3 可重定位目标elf格式
指令:readelf -a hello.o > elf.txt 获得hello的elf文件并重定向到elf.txt
在这里插入图片描述

图4.3.1 Ubuntu下汇编的命令

打开elf.txt

  1. ELF头
    Elf头以一个16进制的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息.不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目.

在这里插入图片描述
图4.3.2 ELF头部分

  1. 节头部表:
    在这里插入图片描述
    图4.3.3节头部表部分
    包含文件中出现的各个节的语义,包括节的类型、位置和大小等信息.
  2. 重定位节 ‘.rela.text’
    在这里插入图片描述
    图4.3.4重定位节部分

其中
偏移量:需要进行重定向的代码在.text或.data节中的偏移位置.
信息:包括symbol和type两部分,其中symbol占前4个字节,type占后4个字节,symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位的类型:由信息决定的替换类型
符号名称:重定位目标的名字
加数:重定位过程需要加减的常量
4. 符号表.symtab
在这里插入图片描述
图4.3.5重定位节部分

以sleepsecs为例:
链接器根据info信息向.symtab节中查询链接目标的符号,由info.symbol=0x09,可以发现重定位目标链接到sleepsecs处.
重定位条目r由四个字段组成:
r.offset=0x60
r.symbol=sleepsecs
r.type=R_X86_64_PC32
r.addend=-4,
R_X86_64_PC32重定位算法摘抄书本如下:
在这里插入图片描述
图4.3.5重定位算法

4.4 Hello.o的结果解析
命令:objdump -d -r hello.o > objdump.txt
在这里插入图片描述
图4.4.1 Hello.o的生成指令
机器语言指的是二进制的机器指令集合,而机器指令是由操作码和操作数构成的.汇编语言的主体是汇编指令.汇编指令和机器指令的差别在于指令的表示方法上,汇编指令是机器指令便于记忆的书写格式.
在对比两个文件后,汇编器在汇编hello.s时:

函数调用…s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是全0.这是因为hello.c中调用的函数都需要通过动态链接器才能确定函数的运行时地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0,然后在.rela.text节中为其添加重定位条目,等待被修改.
全局变量访问和分支转移:在.s文件中的段名称会被修改为0并且添加重定位条目.
反汇编代码跳转指令的操作数使用的不是段名称如.L3,因为段名称只是在汇编语言中便于编写的助记符.
4.5 本章小结
本章介绍了hello从hello.s到hello.o的汇编过程,查看了hello.o的elf,使用objdump工具得到反汇编代码,和之前得到的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.2.1 在Ubuntu下链接的命令

5.3 可执行目标文件hello的格式
输入指令:readelf -a hello > 5.3helloelf.txt
在这里插入图片描述
图5.3.1 生成可执行目标文件hello

打开文件可以看到:
节头部表:
在这里插入图片描述
图5.3.2 节头部表部分
在这里插入图片描述
图5.3.2 节头部表部分(续)

可见,节头部表中包含了hello中的每一个节.和其对应的详细信息.包括:
名称,类型,地址,偏移量,大小,全体大小,旗标,链接,信息,对齐.

5.4 hello的虚拟地址空间
使用edb打开hello程序,通过edb的Data Dump窗口查看加载到虚拟地址中的hello程序.
在这里插入图片描述
图5.4.1 edb打开hello程序

举例.interp
在节头部表信息如下:

在这里插入图片描述
图5.4.2 在节头部表的信息
在edb中:
在这里插入图片描述
图5.4.3 edb查看

该节的位置,大小都和节头部表一致,其他类推.

5.5 链接的重定位过程分析
输入命令:objdump -d -r hello > 5.5helloobjdump.txt
在这里插入图片描述
图5.5.1 hello的反汇编
1.在使用ld命令链接的时候,指定了动态链接器为64的/lib64/ld-linux-x86-64.so.2, _start程序调用hello.c中的main函数,libc.so是动态链接共享库,其中定义了hello.c中用到的printf、sleep、getchar、exit函数和_start中调用的__libc_csu_init,__libc_csu_fini,__libc_start_main.链接器将上述函数加入.
2.将所有的R_X86_64_PC32和R_X86_64_PLT32替换成计算好的地址
计算如下:
以sleepsecs为例:
链接器根据info信息向.symtab节中查询链接目标的符号,由info.symbol=0x09,可以发现重定位目标链接到sleepsecs处.
重定位条目r由四个字段组成:
r.offset=0x60
r.symbol=sleepsecs
r.type=R_X86_64_PC32
r.addend=-4,
R_X86_64_PC32重定位算法摘抄书本如下:
在这里插入图片描述
图5.5.2 重定位算法

Refaddr=0x400532+0x60=0x400592
*refptr=(unsigned)(ADDR(r.sleepsecs)+r.addend-refaddr)==0x601044+(-0x4)-0x400592=(unsigned) 0x200aae,
验证一下:
在这里插入图片描述
其他重定位类似.

5.6 hello的执行流程
函数 地址
ld-2.27.so!_dl_start 0x7fce 8cc38ea0
ld-2.27.so!_dl_init 0x7fce 8cc47630
hello!_start 0x400500
libc-2.27.so!__libc_start_main 0x7fce 8c867ab0
-libc-2.27.so!__cxa_atexit 0x7fce 8c889430
-libc-2.27.so!__libc_csu_init 0x4005c0
hello!_init 0x400488
libc-2.27.so!_setjmp 0x7fce 8c884c10
-libc-2.27.so!_sigsetjmp 0x7fce 8c884b70
–libc-2.27.so!__sigjmp_save 0x7fce 8c884bd0
hello!main 0x400532
hello!puts@plt 0x4004b0
hello!exit@plt 0x4004e0
*hello!printf@plt
*hello!sleep@plt
*hello!getchar@plt
ld-2.27.so!_dl_runtime_resolve_xsave 0x7fce 8cc4e680
-ld-2.27.so!_dl_fixup 0x7fce 8cc46df0
–ld-2.27.so!_dl_lookup_symbol_x 0x7fce 8cc420b0
libc-2.27.so!exit 0x7fce 8c889128

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程.请列出其调用与跳转的各个子程序名或程序地址.
5.7 Hello的动态链接分析
对于动态共享链接库中位置无关代码,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理.

为了引用全集变量PIC,编译器利用了数据段与代码段的距离是一个运行时常量的事实,在数据段开始的地方创建一个表,叫做全局偏移量表GOT,每个被这个目标模块引用的全局数据目标都有一个8字节条目,还会生成一个重定位记录,在加载时,动态链接器会重定位GOT中的每个条目,使得它包含正确的绝对地址.

假设程序调用一个由共享库定义的函数,编译器没有办法预测函数的运行时地址,动态链接器在程序加载时再解析它.称为延迟绑定.

在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码,GOT存放的是PLT中函数调用指令的下一条指令地址.
在这里插入图片描述
图5.7.1 edb中hello程序PLT表
在这里插入图片描述
图5.7.2 edb中hello程序PLT表

在函数调用时,首先跳转到PLT执行.plt中操作,第一次访问跳转时GOT地址为下一条指令,将函数序号入栈,然后跳转到PLT[0],之后将重定位表地址入栈,访问动态链接器,在动态链接器中使用在栈里保存的函数序号和重定位表计算函数运行时的地址,重写GOT,返回调用函数.之后如果还有对该函数的访问,就不用执行第二次跳转,直接参看GOT信息.

5.8 本章小结
在本章中主要介绍了链接的概念与作用、hello的ELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程.
链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译(separate compilation)成为可能.我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块.当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件.
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
概念:进程就是一个执行中程序的实例.每次用户通过向shell 输入一个可执行目标文件的名字,运行程序时, shell 就会创建一个新的进程.
作用:进程为用户提供以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象.
6.2 简述壳Shell-bash的作用与处理流程
Shell的作用: Shell 是指一种应用程序,提供了一个界面,用户通过这个界面访问操作系统内核的服务.
处理流程:Shell从终端读入输入命令.如果是内置命令则立即执行,否则调用相应的程序为其分配子进程并运行,总之就是对其求值.

6.3 Hello的fork进程创建过程
Shell(父进程)通过fork 函数创建一个新的运行的子进程.新的子进程几乎但不完全与父进程相同.子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈.子进程进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork 时,子进程可以读写父进程中打开的任何文件.
在这里插入图片描述
图6.3.1 hello的fork过程

6.4 Hello的execve过程
execve 函数加载并运行可执行目标文件filename, 且带参数列表argv 和环境变量列表envp .只有当出现错误时,例如找不到filename, execve 才会返回到调用程序.所以,与fork 一次调用返回两次不同, execve 调用一次并从不返回.
execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段.新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容.最后加载器设置PC指向_start地址,_start最终调用main函数.除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制.直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存.
在这里插入图片描述
图6.3.1 书本关于加载器的扩展知识

6.5 Hello的进程执行

用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,这个模式中,硬件防止特权指令的执行,并对内存和I/O空间的访问操作进行检查.设置模式位时,进程处于内核模式,一切程序都可运行.任务可以执行特权级指令,对任何I/O设备有全部的访问权,还能够访问任何虚地址和控制虚拟内存硬件.
上下文信息:上下文程序正确运行所需要的状态,包括存放在内存中的程序的代码和数据,用户栈、用寄存器、程序计数器、环境变量和打开的文件描述符的集合构成.

Hello进程执行分析:
Hello起初在用户模式下运行,在hello进程调用sleep之后转入内核模式,内核休眠,并将hello进程从运行队列加入等待队列,定时器开始计时2s,当定时器到时,发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列, hello进程继续执行.
在这里插入图片描述
图6.5.1 模式切换过程
6.6 hello的异常与信号处理
正常结束:
在这里插入图片描述
图6.6.1 hello正常运行过程
中途按下ctrl+z,程序挂起,对应的是shell接收到SIGSTP信号,
在这里插入图片描述
图6.6.2 hello挂起
输入ps指令:可以发现hello进程没有结束
在这里插入图片描述
图6.6.3 显示运行程序

输入jobs查看hello和jid
在这里插入图片描述
图6.6.4 输入jobs查看hello和jid

输入pstree -p 2857 以树状图显示进程间的关系
在这里插入图片描述
图6.6.5 以树状图显示进程间的关系

输入fg 1,就可以让hello继续进行,输出还没有输出的内容.
在这里插入图片描述
图6.6.6 让hello继续进行
如果在运行到一半的时候输入ctrl-c,就会发现进程立即结束了.
在这里插入图片描述
图6.6.7 结束hello
此时输入ps后台也没有hello了
在这里插入图片描述
图6.6.8 显示运行程序

这是因为shell收到SIGINT信号,就结束了hello进程.
6.7本章小结
在本章中,介绍了进程的定义和作用,介绍了Shell的处理流程,介绍了shell如何调用fork创建新进程,如何调用execve执行hello,分析了hello的进程执行,还研究了hello的异常与信号处理.
异常控制流发生在计算机系统的各个层次.在硬件层,硬件检测到的事件会触发控制突然转移到异常处理程序.在操作系统层,内核通过上下文切换将控制从一个用户进程转移到另一个用户进程.在应用层,一个进程可以发送信号到另一个进程,而接收者会将控制突然转移到它的一个信号处理程序.一个程序可以通过回避通常的栈规则,并执行到其他函数中任意位置的非本地跳转来对错误做出反应.

(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序代码经过编译后出现在 汇编程序中地址.逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址.
线性地址&虚拟地址:逻辑地址经过段机制后转化为线性地址(虚拟地址),是逻辑地址到物理地址变换之间的中间层.在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址.是一个32位无符号整数,可以用来表示高达4GB的地址,也就是,高达4294967296个内存单元.线性地址通常用十六进制数字表示,值得范围从0x00000000到0xfffffff)程序代码会产生逻辑地址,通过逻辑地址变换就可以生成一个线性地址.如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址.如果没有启用分页机制,那么线性地址直接就是物理地址.
物理地址:CPU地址总线传来的地址,由硬件电路控制(现在这些硬件是可编程的了)其具体含义.物理地址中很大一部分是留给内存条中的内存的,但也常被映射到其他存储器上(如显存、BIOS等).在没有使用虚拟存储器的机器上,虚拟地址被直接送到内存总线上,使具有相同地址的物理存储器被读写;而在使用了虚拟存储器的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到存储器管理单元MMU,把虚拟地址映射为物理地址.
7.2 Intel逻辑地址到线性地址的变换-段式管理
知识背景:
1、逻辑地址=段选择符+偏移量
2、每个段选择符大小为16位,段描述符为8字节(注意单位).
3、GDT为全局描述符表,LDT为局部描述符表.
4、段描述符存放在描述符表中,也就是GDT或LDT中.
5、段首地址存放在段描述符中.
每个段的首地址都存放在自己的段描述符中,而所有的段描述符都存放在一个描述符表中(描述符表分为全局描述符表GDT和局部描述符表LDT).而要想找到某个段的描述符必须通过段选择符才能找到.
在这里插入图片描述
图7.2.1 段选择符格式
段选择符由三个部分组成,从右向左依次是RPL、TI、index(索引).先来看TI,当TI=0时,表示段描述符在GDT中,当TI=1时表示段描述符在LDT中.
再来看一下index部分.我们可以将描述符表看成是一个数组,每个元素都存放一个段描述符,那index就表示某个段描述符在数组中的索引.
假设GDT的起始位置是0x00020000,而一个段描述符的大小是8个字节,由此我们可以计算出段描述符所在的地址:0x00020000+8*index,从而我们就可以找到我们想要的段描述符,从而获取某个段的首地址,然后再将从段描述符中获取到的首地址与逻辑地址的偏移量相加就得到了线性地址.

在这里插入图片描述
图7.2.2 地址翻译过程

7.3 Hello的线性地址到物理地址的变换-页式管理
CPU的页式内存管理单元负责把一个线性地址转换为物理地址.从管理和效率的角度出发,线性地址被划分成固定长度单位的数组,称为页(page).例如,一个32位的机器,线性地址可以达到4G,用4KB为一个页来划分,这样,整个线性地址就被划分为一个2^20次方的的大数组,共有2的20次方个页,也就是1M个页,我们称之为页表,改页表中每一项存储的都是物理页的基地址.
类似地,物理内存也被分割为物理页(PP/页帧),是分页单元将所有的物理内存都划分成了固定大小的单元为管理单位,其大小一般与内存页大小一致.
如果不考虑TLB与多级页表,虚拟地址分为虚拟页号VPN和虚拟页偏移量VPO.通过页表基址寄存器PTBR+VPN在页表中获得条目PTE,一条PTE中包含有效位、权限信息、物理页号,如果有效位是0+NULL则代表没有在虚拟内存空间中分配该内存,如果是有效位0+非NULL,则代表在虚拟内存空间中分配了但是没有被缓存到物理内存中,如果有效位是1则代表该内存已经缓存在了物理内存中,可以得到其物理页号PPN,与虚拟页偏移量共同构成物理地址PA.
在这里插入图片描述
图7.3.1 线性地址到物理地址

当页面命中时CPU硬件执行的步骤:
第1步:处理器生成一个虚拟地址,并把它传送给MMU.
第2步: MMU生成PTE地址,并从高速缓存/主存请求得到它.
第3步:高速缓存/主存向MMU返回PTE.
第4步:MMU构造物理地址,并把它传送给高速缓存/主存.
第5步:高速缓存/主存返回所请求的数据字给处理器.
在这里插入图片描述
图7.3.2 页面命中时CPU硬件行为
7.4 TLB与四级页表支持下的VA到PA的变换

36位VPN被划分成四个9位的片,每个片被用作到一个页表的偏移量.CR3寄存器包含L1页表的物理地址.VPN1提供到一个L1 PET的偏移量,这个PTE包含L2页表的基地址.VPN2提供到一个L2 PTE的偏移量,以此类推.

在这里插入图片描述
图7.4.1 Intel Core I7中,VA到PA的变换
在对地址翻译的讨论中,我们描述了一个顺序的两个步骤的过程,1)MMU将虚拟地址翻译成物理地址,2)将物理地址传送到L1高速缓存.然而,实际的硬件实现使用了一个灵活的技巧,允许这些步骤部分重叠,因此也就加速了对L1高速缓存的访问.例如,页面大小为4KB的系统上的一个虚拟地址有12位的VPO,并且这些位和相应物理地址中的PPo的12位是相同的.因为八路组相联的、物理寻址的L1高速缓存有64个组和大小为64字节的缓存块,每个物理地址有6个(log264)缓存偏移位和6个(log264)索引位.这12位恰好符合虚拟地址的VPO部分,这绝不是偶然!当CPU需要翻译一个虚拟地址时,它就发送VPN到MMU,发送VPO到高速L1缓存.当MMU向TLB请求一个页表条目时,L1 高速缓存正忙着利用VPO位查找相应的组,并读出这个组里的个标记和相应的数据字.当MMU从TLB得到PPN时,缓存已经准备好试着把这个PPN与这8个标记中的一个进行匹配了.

7.5 三级Cache支持下的物理内存访问

在这里插入图片描述
图7.5.1 Intel Core I7高速缓存层次结构

获得了物理地址VA之后,使用CI(倒数7-12位)进行组索引,每组8路,对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1,则命中,根据数据偏移量CO(后6位)取出数据返回.
如果没有匹配成功或者匹配成功但是标志位是1,则不命中,向下一级缓存中查询数据(L2 Cache->L3 Cache->主存),查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略LFU(Least frequently used)进行替换.也就是替换掉最不经常访问的一次数据,示意图如下:
在这里插入图片描述
图7.5.2 三级Cache支持下的物理内存访问

7.6 hello进程fork时的内存映射
当fork函数被shell进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本.它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制.当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用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.7.1 加载器是如何映射用户空间的区域的
7.8 缺页故障与缺页中断处理
缺页时的操作
第1步:处理器生成一个虚拟地址,并把它传送给MMU.
第2步: MMU生成PTE地址,并从高速缓存/主存请求得到它.
第3步:高速缓存/主存向MMU返回PTE.
第4步:PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序.
第5步:缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘.
第6步:缺页处理程序页面调人新的页面,并更新内存中的PTE.
第7步:缺页处理程序返回到原来的进程,再次执行导致缺页的指令,CPU将地址重新发送给MMU.因为虚拟页面现在已经缓存在物理内存中,所以会命中,主存将所请求字返回给处理器.
在这里插入图片描述
图7.8.1 页面命中和缺页的操作

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆.系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址) .对于每个进程,内核维护着一个变量brk, 它指向堆的顶部.
分配器将堆视为一组不同大小的块的集合来维护.每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的.已分配的块显式地保留为供应用程序使用.空闲块可用来分配.空闲块保持空闲,直到它显式地被应用所分配.一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的.
分配器有两种基本风格. 两种风格都要求应用显式地分配块.它们的不同之处在于由哪个实体来负责释放已分配的块

显式分配器(explicit allocator):
要求应用显式地释放任何已分配的块.例如,C标准库提供一种叫做malloc程序包的显式分配器.C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块.C++中的new和delete操作符与C中的malloc和free相当.

隐式分配器(implicit allocator):
要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块.隐式分配器也叫做垃圾收集器(garbage collector),而自动释放未使用的已分配的块的过程叫做垃圾收集( garbage collection).例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块.

隐式空闲链表:

在这里插入图片描述

图7.9.1 一个简单的堆块的格式

一个块是由一个字的头部,有效载荷,以及可能的一些额外的填充组成的.头部编码了这个块的大小,以及这个块是已分配的还是空闲的.如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是零.因此,我们只需要内存大小的29个高位,释放剩余的3位来编码其他信息.在这种情况中,我们用其中的最低位(已分配位)来指明这个块是已分配的还是空闲的.

带边界标签的隐式空闲链表:
在这里插入图片描述
图7.9.2 使用边界标记的堆块的格式

在隐式空闲链表堆块的基础上,在每个块的结尾处添加一个脚部(footer),边界标记),其中脚部就是头部的一个副本.如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离,这样就允许在常数时间内进行对前面块的合并.

显示空闲链表
一种更好的方法是将空闲块组织为某种形式的显式数据结构.因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面.例如,堆可以组织成一一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针.
在这里插入图片描述
图7.9.3 使用双向空闲链表的堆块的格式

7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、页式管理,TLB与四级页表支持下的VA到PA的变换、三级cache支持下物理内存访问, hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等内容.
不难看出作为一个程序员,我们需要理解存储器层次结构,因为它对应用程序的性能有着巨大的影响.如果我们的程序需要的数据是存储在CPU 寄存器中的,那么在指令的执行期间,在0个周期内就能访问到它们.如果存储在高速缓存中,需要4~7 5 个周期.如果存储在主存中,需要上百个周期.而如果存储在磁盘上,需要大约几千万个周期.
熟悉这一章节的内容,可以帮助我们以后编写一些对高速缓存友好的代码,或者说运行速度更快的代码,十分重要.
(第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.Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) .头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_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 .
5.关闭文件.当应用完成了对文件的访问之后,它就通知内核关闭这个文件.作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中.无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源.
Unix I/O函数:
1.进程是通过调用open 函数来打开一个已存在的文件或者创建一个新文件的:int open(char *filename, int flags, mode_t mode);
open 函数将filename 转换为一个文件描述符,并且返回描述符数字.返回的描述符总是在进程中当前没有打开的最小描述符.flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位.返回:若成功则为新文件描述符,若出错为-1.
2.进程通过调用close 函数关闭一个打开的文件.int close(int fd);
返回:若成功则为0, 若出错则为-1.
3.应用程序是通过分别调用read 和write 函数来执行输入和输出的.
ssize_t read(int fd, void *buf, size_t n);
read 函数从描述符为fd 的当前文件位置复制最多n 个字节到内存位置buf .返回值-1表示一个错误,而返回值0 表示EOF.否则,返回值表示的是实际传送的字节数量.
返回:若成功则为读的字节数,若EOF 则为0, 若出错为-1.
ssize_t write(int fd, const void *buf, size_t n);
write 函数从内存位置buf 复制至多n 个字节到描述符fd 的当前文件位置.图10-3 展示了一个程序使用read 和write 调用一次一个字节地从标准输入复制到标准输出.返回:若成功则为写的字节数,若出错则为-1.

8.3 printf的实现分析
研究printf的实现,首先来看看printf函数的函数体
在这里插入图片描述
图8.3.1 printf函数函数体

先来看printf函数的内容: 这句:
va_list arg = (va_list)((char*)(&fmt) + 4);
va_list的定义: typedef char va_list
这说明它是一个字符指针. 其中的: (char
)(&fmt) + 4) 表示的是…中的第一个参数.
下面我们来看看下一句:
i = vsprintf(buf, fmt, arg);
vsprintf如下所示
在这里插入图片描述
图8.3.2 vsprintf函数函数体

vsprintf返回的是要打印出来的字符串的长度,vsprintf的作用就是格式化.它接受确定输出格式的格式字符串fmt.用格式字符串对个数变化的参数进行格式化,产生格式化输出最后调用write,
write函数如下:
在这里插入图片描述
图8.3.3 write函数函数体

在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
一个call save,是为了保存中断前进程的状态.
ecx中是要打印出的元素个数 ,ebx中的是要打印的buf字符数组中的第一个元素,这个函数的功能就是不断的打印出字符,直到遇到:’\0’ 停止.
就这样,syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码.字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中.显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量).于是字符串就显示在了屏幕上.
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序.接受按键扫描码转成ascii码,保存到系统的键盘缓冲区.
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回.
8.5本章小结
本章通过介绍Linux的IO设备管理方法,简述Unix IO接口及其函数,对printf和getchar的实现的具体分析,了解了Unix I/O 在系统中是一个什么样的存在,I/O 是系统操作不可或缺的一部分.有时你除了使用Unix I/O 以外别无选择.在某些重要的情况中,使用高级I/O 函数不太可能,或者不太合适.
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程.
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法.
1.编写阶段,通过文本编辑器写出hello.c.
2.预处理阶段,预处理器将hello.c include的外部的库取出合并到hello.i文件中.
3.编译阶段,文本文件hello.i翻译成文本文件hello.s.
4.汇编,将.s汇编程序翻译成机器语言指令,hello.s会变成为可重定位目标文件hello.o.
5.链接,将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello.
6.运行:在shell中输入./hello 并跟上两个参数1170300817 linzhihao.
7,运行程序:shell进程调用fork创建子进程,shell调用execve,execve调用启动加载器,加映射虚拟内存,创建新的内存区域,并创建一组新的代码、数据、堆和栈段.程序开始运行.
8.执行指令:CPU为其分配时间片,在一个时间片中,hello有对CPU的控制权,顺序执行自己的代码
10.访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址.
11.动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存.
12.信号:如果运行途中键入ctr-c则停止,如果运行途中键入ctr-z则挂起.
13.结束:shell父进程回收子进程.

我的感悟:深入学习计算机系统,可以看见这个系统的复杂程度是多么高,这一切精妙的设计都是为了使用有限的机器,最快速度创造最大的价值.
而人类对于更快的计算速度,更快的读写速度,更大的存储容量的追求也从未停止,比如数据存储,从穿孔卡片,到磁鼓存储器,到.磁带存储器,到软盘存储器,到硬盘,我想,计算机发展的历史,计算机系统的演变,离不开人类对技术的渴望与追求,希望自己也能在这条路一直走下去

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

附件

文件名 文件作用
hello.i 预处理器修改了的源程序,分析预处理器行为
hello.s 编译器生成的编译程序,分析编译器行为
hello.o 可重定位目标程序,分析汇编器行为
hello 可执行目标程序,分析链接器行为
elf.txt hello.o的elf格式,分析汇编器和链接器行为
objdump.txt hello.o的反汇编,主要是为了分析hello.o
5.3helloelf.txt 可执行hello的elf格式,作用是重定位过程分析
5.5helloobjdump.txt 可执行hello的反汇编,作用是重定位过程分析

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

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] MMU和cache详解(TLB机制)https://blog.csdn.net/qq_21792169/article/details/51303477
[2] 逻辑地址到线性地址的转换https://blog.csdn.net/xuwq2015/article/details/48572421
[3] LINUX 逻辑地址、线性地址、物理地址和虚拟地址 转
https://www.cnblogs.com/zengkefu/p/5452792.html
[4] 深入了解计算机系统(第三版)2016 Bryant,R.E. 机械工业出版社
[5] printf 函数实现的深入剖析
https://blog.csdn.net/zhengqijun_/article/details/72454714

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值