HIT-ICS2022大作业-程序人生-Hello’s P2P

计算机科学与技术学院

2021年5月

摘  要

本文通过一个简单的小程序hello.c 从产生到死亡的一生,来介绍 Linux 系统下的程序从代码到运行再到最后终止过程的底层实现进行了分析,描述了与之相关的计算机组成与操作系统的相关 内容。过 gccobjdumpgdbedb 等工具对一段程序代码预处理、编译、汇编、 链接与反汇编的过程进行分析与比较,并且通过 shell 及其他 Linux 内置程序对进 程运行过程进行了分析。研究内容对理解底层程序与操作系统的实现原理具有一定指导作用。

关键词:链接,进程管理,计算机组成原理…

                         

目  录

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

5.3 可执行目标文件hello的格式

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

6.2 简述壳Shell-bash的作用与处理流程

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 hello的存储管理

7.1 hello的存储器地址空间

7.2 Intel逻辑地址到线性地址的变换-段式管理

7.3 Hello的线性地址到物理地址的变换-页式管理

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

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

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

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

7.9动态存储分配管理

7.10本章小结

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献


第1章 概述

1.1 Hello简介

已知一个hello.c 文件,由预处理器cpp寻找以#开头的命令,然后修改这个程序变成hello.i文件;编译器cll将预处理文件变成可读的汇编语言程序hello.s

然后汇编器 cs hello.s 变成机器码,将其打包成可重定位的目标文件 hello.o。hello.o不能直接打开但是可以使用 objdump 来反汇编查看。

再使用链接器ld合并,获得执行目标文件 hello。在 Linux 系统中内置命令行解释器 shell 加载运行 hello 程序,经过 fork 创建一个新的进程hello 完成了 p2p 过程。

Shell 使用 execve 来加载运行 hello,为其分配虚拟内存空间,构建虚拟内存映射,MMU 组织各级页表与 cache给予 hello 想要的所有信息,CPU 给予hello时间片并控制逻辑流。最后,由shell 回收 hello 的进程, 内核清除与 hello 相关的所有内容,完成 O2O 的过程。

1.2 环境与工具

X64 CPU;2GHz;2G RAM;256GHD Disk

Ubuntu16.04 LTS VirtualBox 11

vimgccasldedbreadelfobjdump

Visual Studio Code

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

文件名

文件作用

hello.c

源程序

hello.i

预处理文件

hello.s

编译生成的汇编文件

hello.o

可重定位目标文件

hello.elf

hello.o的ELF格式

objhello.txt

hello的反汇编代码文本文件

Rehello.s

hello.o的反汇编代码文件

hello

hello.o链接后得到的可执行文件

1.4 本章小结

本章简述了hello程序的运行过程和结束过程,并且说明了使用的环境和工具。

最后对研究过程中中间产生的文件作用进行说明。

(第1章0.5分)


第2章 预处理

2.1 预处理的概念与作用

概念:

程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,

生成二进制代码之前的过程。

作用:

1: 宏定义:用一个标识符来表示一个字符串,这个字符串可以是常量、变量或表达式。在宏调用中将用该字符串代换宏名。

2:文件包含:它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。

3:条件编译:允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。

2.2在Ubuntu下预处理的命令

重用的预处理命令有gcc -E hello.c -o hello.i 或 cpp hello.c > hello.i 。

应截图,展示预处理过程!

图1-预处理命令产生.i文件

图2-.i文件部分截图

2.3 Hello的预处理结果解析

对hello.c预处理结果产生hello.i文件。查看hello.i文件可以看出在原有代码的基础上将#开头的内容进行引入了,如函数声明、结构体定义、定义宏变量等。

如果头文件的内容中仍然有#开头的语句则递归地进行展开,指导没有含#的语句。

除此外的代码部分仍然是可读的c程序。

2.4 本章小结

本章就hello.c的编译,先引入预处理的概念,再在虚拟机上实际操作得到hello.i文件并进行分析。

(第2章0.5分)


第3章 编译

3.1 编译的概念与作用

概念:编译是指编译器将文本文件 hello.i 翻译成文本文件 hello.s 的过

程,也就是编译器对预处理器的输出进行编译,生成汇编语言的代码(.s文件)。

作用:把源程序(高级语言)翻译成机器能读懂执行的语言。

除了基本功能之外,编译器一般还具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化等便利功能。

3.2 在Ubuntu下编译的命令

编译命令:gcc -S hello.i -o hello.s

图3-编译产生hello.s文件

3.3 Hello的编译结果解析

图4-编译结果截图

3.3.1 数据

hello.c中一共有int型,字符串型,数组三种数据类型。

  1. int i

图5-i的定义

图6-i的使用

图7-汇编代码中的i

可以看到i是个没有初始化的整型变量,在for中用作计数变量。编译器没有在定义时专门声明其的值,而是在循环时将i存在-4(%rbp)中,可以看出i占据4个字节。

  1. int argc

图8-局部变量argc

argc是从shell输入的参数个数,是main函数的第一个参数,通常用%rdi保存。

  1. 立即数

各种程序中出现的常数,在汇编中直接以$xx形式出现。

  1. 字符串

图9-c程序中出现的字符串

图10-汇编中两个字符串的声明

由于在Linux下汉字使用utf-8编码,所以在汇编中看到的是\xxx的形式。下面的%s表示用户在shell输入的两个参数(argv[1]和argv[2])

  1. 数组 char* argv[]

图11-在使用argv时计算地址后取出数组值

argv是main的第二个参数,是char指针的数组。保存在寄存器%rsi中。

在访问数组元素时,由起始地址argv依次+8或8的倍数来访问数组元素。在图11中上两个(%rax),%rax分别是argv[1]和argv[2]的取值的过程。

3.3.2 赋值

hello.c中的赋值操作只有对循环变量i赋初值i=0。

图12-对存在寄存器中的i进行赋初值0

要注意的是,i在定义时没有赋初值,而是在for中使用时给赋了初值,这里汇编代码中只在后面的操作中出现了赋值。

3.3.3 算数操作

hello.c中只有一处运算操作,就是在for中每次的i++。用addl给存在-4(%rbp)中的i加1,然后再检查循环条件。

图13-for中的i++

3.3.4 关系操作

C 语言中关系操作的种类有关系操作:== 、!= 、> 、 < 、 >= 、 <=。

hello.c中有两处关系操作,分别是argc!=3和i<8.

    

图14-汇编中两个位置的关系操作

汇编中cmp的功能是在两个参数相减后改变标志寄存器存的值,不保存计算结果,然后其他指令再利用标志寄存器的值来得知比较的结果。

3.3.5 数组/指针/结构操作

hello.c中涉及到数组和指针的操作就是对char指针数组argv[]进行访问。还是看到图11:先在首地址基础上加上数据元素的大小得到数组中某一位存放的位置,再用(%rax)取存放在这里的指针指向位置的值。

3.3.6 控制转移

C 语言中的控制转移就是,if,switch,while,case,continue,break等

语句。

hello.c中的控制转移有两处:

  1. if(argc!=4),条件判断语句判断argc是否等于4。等于与否都会跳转到某一个位置。在汇编这里如果argc等于4则跳转到.L2位置。

图15-汇编中判断argc是否和4相等

  1. for中的循环条件i<8.在每次循环结束时检查i是否满足条件。

图16-汇编中判断i是否小于等于7

由于这里i是整数,所以i小于8和小于等于7等价。如果满足条件就跳转到.L4位置。

3.3.7 函数操作

hello.c中总共有5个函数:main(),printf(),exit(),sleep(),getchar()和atoi()。

  1. main()

即主函数。是程序最开始执行的函数。开始被存放到.text节,标记为函数。有两个参数int argc和字符指针argv[],在shell中输入,分别存在%rdi和%rsi中。

图17-main函数

  1. printf()

图18-c中的printf函数

printf在hello.c中一共调用了两次。参数是字符串,函数的作用也是输出字符串。

图19-hello.s中出现的puts

第一个printf由于参数只有一个字符串,在hello.s中被优化修改成了puts。

图20-hello.s中出现的printf

第二个printf有三个参数,分别是输出的字符串,argv[1]和argv[2].

  1. exit()

图21-hello.s中的exit

在hello.c中,如果输入的参数个数不是刚好3个则调用exit函数终结程序。调用后返回参数并结束程序返回操作系统。通常exit(0)表示程序正常退出,exit(1)或exit(-1)表示程序异常退出。这里exit的参数是1,表示程序异常退出。

  1. sleep()

图22-hello.s中的sleep

sleep函数的作用是将程序挂起一段时间,参数就是挂起的时间。这里的参数是atoi的返回值。

  1. getchar()

图23-hello.s中的getchar

getchar没有参数,在循环结束后直接用call调用getchar。

  1. atoi()

图24-hello.s中的atoi

atoi函数的作用是将字符串转成整型。这里的参数是argv[3].

3.3.8 类型转换

程序中的类型转换是用函数atoi()完成的,作用是将字符串转换为int型。这里把argv[3]转成整型后作为参数传给sleep了。

3.4 本章小结

本章主要介绍了编译器是如何处理C语言中的各种各样的数据类型和操作的。 分析hello.c和hello.s来认识两者前后处理的结果关系。

(第3章2分)


第4章 汇编

4.1 汇编的概念与作用

概念:汇编大多是指汇编语言,汇编程序。把汇编语言翻译成机器语言的过程称为汇编。

作用:将.s 汇编程序翻译成机器语言指令, 把这些指令打包成可重定位目标程序的

格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制文件。它包含程序的指令编码,使得机器能够识别。

4.2 在Ubuntu下汇编的命令

命令:as hello.s -o hello.o

图25-汇编得到hello.o

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

readelf -a hello.o > hello.elf 先得到hello.o文件的ELF格式。

图26-得到hello.elf

图27-ELF头的意义作用

再打开hello.elf:

图28-hello.o的ELF头

  1. ELF 头(ELF feader)以一个 16 字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括 ELF 头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如 x86-64)、节头部表的 文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。
  2. 节头部。如图28中下面“节头:”后面的内容。可以看到,节头部表描述了 hello.o 文件各个节的类型、位置和大小等信息。
  3. 重定位节:.rela.text 是一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息。当链接器把这个目标文件和其他文件组合时,需要修改这些位置。

图29中上面的8条重定位信息分别是对.L0(第一个printf中的字符串)、puts函数、exit函数、.L1(第二个printf中的字符串)、printf函数、atoi函数、sleep函数、getchar函数进行重定位声明。

图29-重定位节

(4)符号表 symtab:符号表中包含用于重定位的信息,符号名称、符号是全

局还是局部,并标识了符号的对应类型。

图30-符号表部分

4.4 Hello.o的结果解析

objdump -d -r hello.o  分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

图31-对hello.o进行反汇编生成Rehello.s

通过和hello.s进行对比,可以找到以下区别:

图32-hello.s和Rehello.s的对比

  1. 立即数:hello.s中立即数是十进制,Rehello.s是十六进制。
  2. 分支转移:hello.s中jmp跳转后面接的都是.L1,.L2等这种段名称,在Rehello.s中接的是具体的地址。
  3. 函数调用:hello.s中的call接的是函数名称,在Rehello.s中接的是下一条指令的地址。这是因为hello.c中调用的都是共享库中的函数,在链接后才能确定最终地址。
  4. 全局变量:

图33-hello.s中使用全局变量.LC0

图34-Rehello.s中对应位置

在hello.s中直接在赋值时调用了全局变量.LC0,在Rehello.s中.LC0的值被存放在.rodata节中,且是绝对地址的应用。这是因为汇编器在汇编时会对每一个全局符号的引用产生一个重定位条目。这些重定位条目所指定的符号会在后面链接器中被重定位,计算出真正的地址并生成最终的可执行文件。

5.其他:由机器语言反汇编得到的Rehello.s与hello.s在其他地方基本相同,可以看出汇编语言和机器码有着一一对应的映射关系。

4.5 本章小结

本章介绍了汇编的过程,通过指令由.s到.o文件。 还可以在ELF 头中查看这个文件的基本信息,通过查看这张表可以得到段基址、文件节头数、符号表等等多样的信息。然后对hello.o进行反汇编与原先的 hello.s 对比,分析了汇编语言与机器码的关系。

(第4章1分)


5链接

5.1 链接的概念与作用

概念:链接是一个技术,允许从多个目标文件创建程序。链接可以在程序生命的不同时间发生:编译时、加载时或运行时。

作用:由于可以把多个小的模块链接创建成一个程序,所以可以独立地分开编译修改这些模块,最后只需要简单地重新编译、链接应用而不用编译别的没有修改的部分。

5.2 在Ubuntu下链接的命令

链接命令:

ld -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/gcc/x86_64-linux-gnu/9/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello

图35-链接产生hello

5.3 可执行目标文件hello的格式

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

图36-readelf下各个节头部分信息

ELF头中包含了程序节表大小、节表数量、数据存储方式、文件类型、程序入口点等与文件整体框架有关的信息。

节头表图36中列举了所有节头以及其名称、大小、类型、开始地址、偏移量等信息。

5.4 hello的虚拟地址空间

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

图37-edb中Data Dump查看hello的虚拟地址空间

在edb中Memory Regions找到hello最开始的起始地址,并在Dump中打开,可以看到开头就是ELF头如图37.接下来根据5.3找其他节:

图38-.interp节

从图38可以看出.interp节存的是Linux动态共享库的路径。

图39-.dynstr节

从图39可以看出.dynstr节存的是动态符号表。

图40-.rodata节

从图40可以看出.rodata节存的是要输出的字符串。

图41-Memory Regions

其他节也可以使用这个方法到相应位置去查看虚拟地址空间内容。

5.5 链接的重定位过程分析

使用objdump -d -r hello > objhello.txt:

图42-反汇编得到objhello.txt

图43-Rehello.s与objhello.txt的对比

可以看到大体上并没有什么不同,不同的地方在于objhello.txt中把地址由相对偏移变为了CPU可直接访问的虚拟地址(如图43)。重定位的过程就是链接器把hello.o中的偏移量加上程序在虚拟内存中的起始地址就得到了objhello.txt中的一个个地址。

还有就是objhello.txt在开头部分多了.init,.fini,.plt.got节。其中.init 节是程序初始化需要执行的代码,.fini 是程序正常终止时需要执行的代码,.plt 和.plt.got节分别是动态链接中的过程链接表和全局偏移量表。

链接的过程如下:

(1)在使用 ld 命令链接的时候,指定了动态链接器为 64 的/lib64/ldlinux-x86-64.so.2,crt1.o、crti.o、crtn.o 中主要定义了程序入口_start、初始化函数_init,_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_PLT32 的重定位,此时动态链接库中的函数已经加入到了 PLT 中,.text与.plt 节相对距离已经确定,链接器计算相对距离,将对动态链接库中函数的调用值改为 PLT 中相应函数与下条指令的相对地址,指向对应函数。对于此类重定位链接器为其构造.plt 与.got.plt。

(3)链接器解析重定条目时发现两个类型为 R_X86_64_PC32 的对.rodata 的重定位(printf 中的两个字符串),.rodata 与.text 节之间的相对距离确定,因此链接器直接修改 call 之后的值为目标地址与下一条指令的地址之差,使其指向相应的字符串。

5.6 hello的执行流程

程序名

程序地址

ld-2.31.so!_dl_start

0x00007f49f61a4df0

ld-2.31.so!_dl_init

0x00007f49f61b4c10

hello!_start

0x4010f0

libc-2.31.so!__libc_start_main

0x00007fede557efc0

libc-2.31.so!__cxa_atexit

0x00007fede55a1e10

ld-2.31.so!_dl_fixup

0x00007f49f5fe0bb0

hello!__libc_csu_init

0x401270

hello!_init

0x401000

hello!register_tm_clones

0x401160

libc-2.31.so!_setjmp

0x00007fede559dcb0

libc-2.31.so!__sigsetjmp

0x00007fede559dbe0

hello!main

0x4011d6

通过edb逐步调试得到的程序调用顺序和地址如上。

5.7 Hello的动态链接分析

   通过先前得知.got.plt节的开始地址为0x404000.在dl_init前后到相应位置查看内容的变化。

图44-dl_init之前的.got.plt节

图45-dl_init之后的.got.plt节

可以看到在dl_init之后出现了两个以7f开头的两个地址,这就是GOT[1]和GOT[2]。从5.6的调试经验可知这些是动态链接器的首地址。通过查看对应位置内存发现是动态链接函数。

图46-动态链接函数

5.8 本章小结

在本章中主要介绍了链接的概念与作用、hello 的 ELF 格式,分析了 hello 的

虚拟地址空间、重定位过程、执行流程、动态链接过程。

链接可以在程序的编译、加载或运行时进行。经过链接,ELF 可重定位的目标文件会变成可执行的目标文件,链接器会将静态库代码写入程序中,以及写入动态库调用的相关信息,并将地址进行重定位,从而便于后续寻址。静态库会直接写入代码,动态链接会涉及到共享库的寻址。

链接后,程序便能作为进程在虚拟内存中直接运行。

(第5章1分)


6hello进程管理

6.1 进程的概念与作用

概念:进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。

作用:进程提供给应用程序两个关键抽象:

(1)逻辑控制流 (Logical control flow)

·每个程序似乎独占地使用 CPU

·通过 OS 内核的上下文切换机制提供

(2)私有地址空间 (Private address space)

·每个程序似乎独占地使用内存系统

·OS 内核的虚拟内存机制提供

6.2 简述壳Shell-bash的作用与处理流程

作用:Shell 是一个用 C 语言编写的程序。是操作系统(内核)与用户之间的交互的桥梁,充当命令解释器的作用,将用户输入的命令翻译给系统执行。

处理流程:

1、从终端或控制台读入用户输入的命令;

2、将输入字符串切分获得所有的参数;

3、如果是内置命令则立即执行;

4、否则调用相应的程序为其分配子进程并运行;

5、shell 应该接受键盘输入信号,并对这些信号进行相应处理;

6、判断程序的执行状态是前台还是后台。若是前台就等待进程结束;否则

直接将进程放入后台执行,继续等待用户的下一次输入。

6.3 Hello的fork进程创建过程

图47-在终端运行hello

由于hello不是内置命令,所以shell会fork一个子进程来运行相应名称的程序也就是hello。在用法和结果上和直接运行原程序没有任何区别。

当shell调用fork创建子进程来运行hello时,子进程会得到与父进程用户级虚拟地址空间相同但是独立存在的一份副本,其中包括代码、数据段、堆、共享库和用户栈等。子进程还会获得与父进程任何打开文件描述符相同的副本,所以子进程可以读写父进程中的任何文件。此外父进程和子进程的PID是不同的。

6.4 Hello的execve过程

在 fork 出一个子进程之后,要执行 execve 函数在当前进程的上下文中加载并运行一个新程序。

execve 会调用驻留在内存中启动加载器来执行 hello 程序。加载器会删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。

然后新的栈和堆段会被初始化为零,通过将虚拟地址空间中的页映射到可执行文件页大小的片上,将新代码和数据段初始化为可执行文件中的内容。

最后加载器设置 PC 指向_start 地址,再由_start 最终调用 hello 中的 main 函数。除了一些头部信息,在这个加载过程中没有任何从磁盘到内存的复制操作。一直到 CPU 引用了一个被映射的虚拟页时才会进行复制。这时候操作系统会利用它的页面调度机制自动将页面从磁盘传送到内存。

6.5 Hello的进程执行

hello在刚开始运行时,内核保存一个它的上下文。进程会在用户状态下运行,若没有异常或中断信号,hello将一直正常地运行。除非hello程序被抢占顺序执行,则会进行上下文切换。

查看hello的原代码可以知道hello中是有sleep函数的。hello进程在调用sleep后会由用户状态转换为内核状态(核心态)。内核处理休眠请求主动释放当前进程,将hello进程从运行队列中移到等待队列,再让计时器开始计时,然后内核再进行上下文切换把当前进程的控制权交给其他进程。当计时器达到指定秒数也就是sleep的参数时,会发送一个中断信号中断当前正在进行的进程并再进行上下文切换,恢复hello进程在休眠前的上下文信息,把控制权交还给hello进程,hello回到用户状态。

在hello程序的最后调用了一个getchar函数。当调用getchar时hello会进入内核状态。内核中的陷阱处理程序请求来自键盘缓存区的DMA传输,并执行上下文转换,把控制权交给其他进程。当收到了缓存区的信息并传输到内存后,会发送一个中断信号中止其他进程,内核再进行上下文切换换回hello进程。

最后hello运行到return,进程终止。

6.6 hello的异常与信号处理

hello在正常运行时结果如图47.

图48-乱按键盘的情况

可以看到并不会发生什么,虽然能输入东西但程序还是照常执行。因为输入的内容会被getchar函数读走并被shell当作一个命令执行,因为是乱输的所以什么也不会发生。

图49-输入了ctrl-c或ctrlz

当输入了ctrl-c程序会中止。输入了ctrl-z后hello会被挂起,用jobs命令可以看到被挂起的hello。

在pstree中查看bash下的进程可以找到hello。

图50-进程树中被挂起的hello

这时用fg1把hello调到前台继续运行,此时程序正常结束。

图51-恢复运行的hello

继续ctrl-z挂起hello,这时候可以在ps中看到挂起的hello的pid,此时用kill -9 pid来尝试杀死hello进程。

图52-kill杀死进程hello

6.7本章小结

本章介绍了 shell 的原理运行机制还有程序进程的相关概念。shell 中执行程序先是通过fork 函数及 execve 创建子进程,然后在子进程中执行程序。子进程有与父进程相同却又独立的环境,“好像”与其他进程并发执行,拥有着各自的时间片,在各种信号机制以及内核的管理下进行上下文切换,执行各自的指令。

异常分为中断、陷阱、故障和终止四类。操作系统提供了信号这一机制来实现对异常的反馈。当遇到异常时,程序能通过各种信号来调用信号处理子程序,处理各种情况。

(第6章1分)


7hello的存储管理

7.1 hello的存储器地址空间

概念:

逻辑地址:指由程式产生的和段相关的偏移地址部分,分为两个部分,一部分为段基址,另一部分为段偏移量。

线性地址:非负整数并连续的地址的有序集合,称为线性地址空间。

虚拟地址:由逻辑地址翻译得到的线性地址就叫虚拟地址。

物理地址:计算机系统的主存被组织成一个由 M 个连续的字节大小的单元组 成的数组,其每个字节都被给予一个唯一的地址,这个地址称为物理地址。物理地址用于内存芯片级的单元寻址,与处理器和 CPU 连接的地址总线相对应。

7.2 Intel逻辑地址到线性地址的变换-段式管理

段寄存器(16位):用于存放段选择符的寄存器。

图53-段寄存器中的段选择符示意

从逻辑地址到线性地址的变换,首先根据段选择符的TI部分判断需要用到的段选择符表是全局描述符表还是局部描述符表,然后根据段选择符的高13位索引到对于的描述符表中找到对应偏移量的段描述符,从中取出32位的段基址地址,把32位的段基址地址与32位的段内偏移量相加就得到了32位的线性地址。

图54-逻辑地址变换为线性地址图示

7.3 Hello的线性地址到物理地址的变换-页式管理

页表是一个页表条目的数组,将虚拟页地址映射到物理页地址。

图55-虚拟页和物理页的映射关系

图56-虚拟页表到物理页表关系

hello的线性地址到物理地址首先要查询页表。线性地址分为虚拟页号部分和虚拟页偏移量部分,查询虚拟页表根据的就是虚拟页号,先看其有效位是否为1,否则就发生缺页;然后在页表中指向的物理页号和虚拟页偏移量一起组成物理地址。

图57-线性地址到物理地址图示

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

在 Corei7 中 48 位虚拟地址分为 36 位的虚拟页号以及 12 位的页内偏移。

四级页表中包含了一个地址字段,它里面保存了 40 位的物理页号(PPN),

这就要求物理页的大小要向 4kb 对齐

四级页表每个表中均含有 512 个条目,故计算四级页表对应区域如下:

第四级页表:每个条目对应 4kb 区域,共 512 个条目;

第三级页表:每个条目对应 4kb*512=2MB 区域,共 512 个条目;

第二级页表:每个条目对应 2MB*512 = 1GB 区域,共 512 个条目;

第一级页表:每个页表对应 1GB*512 = 512GB 区域,共 512 个条目。

图58-从VA到PA的变换流程

如图58,VA中的36位VPN包括TLBT和TLBI,根据TLBI索引到对应的TLB组,并结合TLBT找到对应的行并判断TLB是否命中。若命中则取出其中的物理页号PPN,否则转到四级页表索引。

将该PPN与VA的VPO部分组合就得到了虚拟地址对应的物理地址。

以下格式自行编排,编辑时删除

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

图59-三级cache下物理内存访问

MMU 发送物理地址 PA 给 L1 缓存,L1 缓存从物理地址中抽取出缓存偏移

CO、缓存组索引 CI 以及缓存标记 CT。高速缓存根据 CI 找到缓存中的一组,并通过 CT 判断是否已经缓存地址对应的数据,若缓存命中,则根据偏移量直接从缓存中读取数据并返回;若缓存不命中,则继续从 L2、L3 缓存中查询,过程与在L1中类似。若仍未命中,则从主存中读取数据。

7.6 hello进程fork时的内存映射

fork一个hello进程时会为这个新进程提供私有的虚拟地址空间。

  1. 为新进程hello创建虚拟内存:创建当前进程的mm_struct、vm_area_struct链表和页表的原样副本;把两个进程中的每个页面都标记为只读;再把两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW)。
  2. 当从hello进程返回时,hello进程拥有与调用fork的父进程相同的虚拟内存。
  3. 随后的写操作会通过写时复制机制创建新页面。

图60-写时复制

7.7 hello进程execve时的内存映射

execve在当前进程中加载并运行hello:

  1. 删除已有页表和结构体vm_area_struct链表。
  2. 创建新的页表和结构体vm_area_struct链表。将hello的代码和初始化的数据映射到.text和.data区(目标文件提供),.bss和栈映射到匿名文件。
  3. 设置PC使其指向代码区域的入口点。

图61-execve时的内存区域示意

以下格式自行编排,编辑时删除

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

缺页故障是一种常见的故障,当指令引用一个虚拟地址,在 MMU 中查找页

表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出的时候就会发生故障。 即缓存不命中。

缺页中断处理:

  1. 缺页异常处理程序选择一个牺牲页;
  2. 换入新的页面并更新页表;
  3. 重新启动导致缺页的指令,此时页面就命中了。

7.9动态存储分配管理

图62-动态内存分配器

malloc等申请内存的函数就是使用了动态内存分配器来获得虚拟内存。动态内存分配器维护一个进程的虚拟内存区域“堆”。内核则维护着指向堆顶的变量brk。推与其他内存内容的关系可以见图61.

分配器将堆视为一组不同大小的块的结合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。

已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。malloc就会根据要申请的内存空间大小,找到空闲块并分配给应用程序。

分配器有两种类型策略:显式分配器和隐式分配器,两种风格都要求应用显式地分配块。不同在于由哪个实体来负责释放:前者要求应用显式地释放任何已分配的块,后者如果检测到已分配块不再被程序所使用,就释放这个块。malloc就是显式分配器。

7.10本章小结

本章通过分析hello的内存映射和存储管理,主要分析了虚拟内存的原理和内存映射的工作机制。虚拟内存是对主存的一个抽象,访存时地址需要从逻辑地址翻译到虚拟地址 并进一步翻译成物理地址。  

操作系统通过地址的页式管理来实现对磁盘的缓存、内存管理、内存保护等功

能。

虚拟内存为便捷的加载、进程管理提供了可能。程序运行过程中往往涉及动态

内存分配,动态内存分配通过动态内存分配器完成。

(第7章 2分)


8hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

所有的 I/O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。

8.2 简述Unix IO接口及其函数

以下格式自行编排,编辑时删除

UNIX I/O:将设备映射为文件,允许 linux 内核引出一个简单、低级的应用接口,称为 Unix I/O,使得所有的输入和输出都能以一种统一且一致的方式来执行。

Unix I/O 接口提供了以下函数供应用程序调用:

  1. 打开文件:int open(char *filename, int flags, mode_t mode);

关闭文件:int close(int fd)。

(2)读文件:ssize_t read(int fd, void *buf, size_t n);

写文件:ssize_t write(int fd, const void *buf, size_t n)。

8.3 printf的实现分析

图63-printf函数的声明

其中形参中的...意为可变形参。当传递参数个数不确定时就可以用到这种写法。

(char*)(&fmt) + 4) 表示的是...中的第一个参数的地址。

vsprintf()是另一个函数。

图64-vsprintf的定义

可以看出vsprintf返回的是要打印出来的字符串的长度。

回到printf,看到write(buf,i)。write是系统函数。在 Linux 下,write 函数的第一个参数为 fd,也就是描述符,而 1 代表的就是标准输出。查看 write 函数的汇编实现可以发现,它首先给寄存器传递了几个参数,然后执行 int INT_VECTOR_SYS_CALL,代表通过系统调用 syscall,syscall 将寄存器中的字节通过总线复制到显卡的显存中。显示芯片按照刷新频率逐行读取 vram,并通过信号线向液晶显示器传输每一个点(RGB 分量)。由此 write 函数显示一个已格式化的字符串。vsprintf的作用就是格式化。

8.4 getchar的实现分析

异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码并产生一个中断请求。中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,再将该按键扫描码转换成 ASCII 码,保存到系统的键盘缓冲区之中。

图65-getchar函数

可以看到getchar调用了系统函数read()。read 读取存储在键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,然后getchar再对这个串进行封装。最后的效果就是读取字符串的第一个字符并返回。

8.5本章小结

本章简述了Linux的IO设备管理方法策略已经产生的一些系统函数的应用。在 Linux 中,I/O 的实现是通过 Unix I/O 函数来执行的。它把所有的 I/O 设备模型化为文件并提供统一的接口以供使用,这使得所有的输入输出都能以一种统一且一致的方式来执行。

(第8章1分)

结论

用计算机系统的语言,逐条总结hello所经历的过程。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

hello,也就是一个可执行文件的代表。

首先有一个hello.c,也就是hello的C语言源码。

想要运行源码,先是编译器对hello.c进行预处理得到hello.i,再进行编译得到hello.s,最后汇编器进行汇编得到可重定位目标文件hello.o。

链接器对hello.o进行连接得到可执行目标文件hello,这时候hello已经可以被操作系统加载执行。

当bash要执行hello时,会先fork一个新的子进程,hello就在这个子进程中execve。execve会清空当前进程的数据并加载hello,把rip指向hello的程序入口,把控制权交给hello。

虽然说是控制权交给hello,但是系统中还是有许多其他进程也是在并行运行的。hello在运行过程中由于系统调用或计时器中断会导致上下文切换,别的进程会抢占hello进程。

hello在运行过程中如果键盘输入了什么或是收到来自其他进程的信号,此时hello会调用信号处理程序来处理这些信号,可能会忽略,也可能会中止进程。

hello中调用的printf和getchar在实现时就调用了Unix I/O的函数write和read,这两个函数的实现就需要借助系统调用。

hello中的访存操作需要从逻辑地址开始,变成线性地址再转成物理地址。访问物理地址时可能内容以及被缓存至高速缓存也就是命中,或者也可能不命中要倒主存中得到数据,再或者是数据位于磁盘中等待被交换到主存。

计算机系统真是太奇妙了!

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


附件

列出所有的中间产物的文件名,并予以说明起作用。

文件名

文件作用

hello.c

源程序

hello.i

预处理文件

hello.s

编译生成的汇编文件

hello.o

可重定位目标文件

hello.elf

hello.o的ELF格式

objhello.txt

hello的反汇编代码文本文件

Rehello.s

hello.o的反汇编代码文件

hello

hello.o链接后得到的可执行文件

(附件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.

[7] CSDN网站

[8]  CSAPP 《深入理解计算机系统》

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值