哈工大计算机系统大作业:hello的一生

摘  要

    Hello world程序几乎是每一个程序员刚开始接触编程时会写的第一个程序,但就是这样一个在当时看起来相当简单的程序,在经过了一个学期计算机系统课程的学习后的今天再看,却发现其中蕴含着很多知识。从鼠标点击“编译并运行”的按钮到在屏幕上显示出来的输出这看似短暂的一瞬,其实经历的是一个漫长而又复杂的过程,需要多个软件、硬件的协同配合。今天,在学习了计算机系统的一系列知识后,是时候对hello这个“简单”程序背后的复杂机制一探究竟了!

    简而言之,当hello的源代码写完被保存成.c文件之后,需要依次经过预处理器生成.i文件、编译器生成.s文件、汇编器生成.o文件、链接器生成可执行文件,并将生成的可执行文件保存至硬盘处。当要运行该程序时,操作系统会为其先创建进程,然后调用execve函数将程序加载到进程的上下文中,而将程序从硬盘调入内存以及cache的过程又涉及到虚拟内存和存储器层次结构方面的知识,当这一切都准备就绪后,在进程中运行可执行程序,然后才能得到最终的输出。

关键词:计算机系统;预处理;编译;汇编;链接;进程管理;内存管理                           

目  录

第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的预处理结果解析... - 6 -

2.4 本章小结... - 9 -

第3章 编译... - 10 -

3.1 编译的概念与作用... - 10 -

3.2 在Ubuntu下编译的命令... - 10 -

3.3 Hello的编译结果解析... - 10 -

3.4 本章小结... - 16 -

第4章 汇编... - 17 -

4.1 汇编的概念与作用... - 17 -

4.2 在Ubuntu下汇编的命令... - 17 -

4.3 可重定位目标elf格式... - 17 -

4.4 Hello.o的结果解析... - 22 -

4.5 本章小结... - 22 -

第5章 链接... - 23 -

5.1 链接的概念与作用... - 23 -

5.2 在Ubuntu下链接的命令... - 23 -

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

5.4 hello的虚拟地址空间... - 26 -

5.5 链接的重定位过程分析... - 27 -

5.6 hello的执行流程... - 29 -

5.7 Hello的动态链接分析... - 31 -

5.8 本章小结... - 32 -

第6章 hello进程管理... - 33 -

6.1 进程的概念与作用... - 33 -

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

6.3 Hello的fork进程创建过程... - 33 -

6.4 Hello的execve过程... - 34 -

6.5 Hello的进程执行... - 34 -

6.6 hello的异常与信号处理... - 35 -

6.7本章小结... - 37 -

第7章 hello的存储管理... - 38 -

7.1 hello的存储器地址空间... - 38 -

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

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

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

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

7.6 hello进程fork时的内存映射... - 39 -

7.7 hello进程execve时的内存映射... - 40 -

7.8 缺页故障与缺页中断处理... - 41 -

7.9本章小结... - 41 -

结论... - 42 -

附件... - 43 -

参考文献... - 44 -

第1章 概述

(0.5分)

1.1 Hello简介

Hello的P2P:程序员先编写出hello的原始C语言代码,然后经过预处理、编译、汇编、链接得到可执行文件,再由shell为hello创建进程并加载可执行文件,得到一个运行中的hello进程,于是hello便从代码变成了运行着的进程(from program to process)。

Hello的020:shell为hello创建进程并加载hello的可执行文件,为其提供了虚拟地址空间等进程上下文,实现了hello的从无到有的过程。Hello在运行时会遇到诸多的异常与信号,对存储器的访问也会涉及诸多机制,还可能通过中断和IO端口与外设进行交互。最终,hello正常退出或收到信号后终止,这会使得操作系统结束hello进程,释放其占用的一切资源,返回至shell,实现了hello从有道无的过程。这便是hello的从无到有再到无的过程(from zero to zero)。

1.2 环境与工具

硬件环境: CPUIntel 12900H32GB内存,1 TB SSD

系统环境: 虚拟机:Ubuntu 20.04VMWare Workstation 17

工具: 文本编辑器vim,反汇编工具edb 1.3,反汇编工具objdump,编译环境gcc等。

1.3 中间结果

       1、源代码hello.c

       2、经过预处理器生成的修改了的源文件hello.i

       3、经过编译器生成的汇编语言文件hello.s

       4、经过汇编器生成的可重定位目标文件hello.o

       5、经过链接器生成的可执行目标文件hello

       6、hello.o通过objdump得到的反汇编文件hello.o.txt

       7、hello通过objdump得到的反汇编文件hello.txt

1.4 本章小结

       本章主要介绍了hello.c程序P2P020的过程,列出了本次实验所需的环境和工具以及过程中所生成的中间结果。

第2章 预处理

(0.5分)

2.1 预处理的概念与作用

       C语言编译器的预处理是将原始的代码按照带有“#”号的预处理语句进行扩展,例如在#include处插入文件,把#define的宏进行替换,根据条件选择#if内的代码等。可以将预处理的过程看作是一个文本替换的过程。

      具体来说,预处理器对源代码进行预处理的过程如下:

  1. 预处理器(cpp)将所有的#define删除,并且展开所有的宏定义。
  2. 处理所有的条件预编译指令,比如#if、#ifdef、#elif、#else、#endif等。
  3. 处理#include预编译指令,将被包含的文件直接插入到预编译指令的位置。
  4. 删除所有的注释。
  5. 添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
  6. 保留所有的#pragma编译器指令,因为编译器需要使用它们。 

2.2在Ubuntu下预处理的命令

在终端中输入命令gcc -E hello.c -o hello.i,即可生成一个经过预处理后的生成文件hello.i。

2.3 Hello的预处理结果解析

经过预处理后,生成的文件被扩展至3091行,其中源代码在文件的最后(3078-3091行),可以看到源代码其实只占扩展后代码的很小一部分。

而扩展后的文件中其余大部分代码都是对库的引用以及对要用到的数据结构和函数的定义。

扩展文件的开头是对要用到库的引用:

然后是对数据结构和函数的定义:

(对函数的定义)

(对数据结构的定义)

2.4 本章小结

本章通过对源代码执行预处理指令,得到了预处理后生成的扩展文件hello.i,通过查看文件的内容可以看出预处理器对源代码进行了大面积的扩展操作,使得源代码在扩展后的文件中只占很小的一部分。得到的扩展文件仍然是合法的C代码,除了源代码外,还包括了大量的对库文件的引用以及对数据结构和函数的声明。

第3章 编译

2分)

3.1 编译的概念与作用

编译器(ccl)对预处理完的文本文件hello.i进行扫描(词法分析)、语法分析、语义分析、源代码优化、代码生成、目标代码优化,将原始的C语言语法格式的代码转换为面向CPU的机器指令,生成文本文件hello.s,其内容是汇编语言的文本形式。

3.2 在Ubuntu下编译的命令

在终端输入命令gcc -S hello.i -o hello.s -fno-PIC -no-pie -m64,即可得到一个经过编译生成的文件hello.s。

3.3 Hello的编译结果解析

3.3.1 数据

3.3.1.1 常量

1、字符常量

源代码hello.c中printf输出的字符串(14行)和输出时的格式控制字符串(18行),分别存放在hello.s中.L0(6行)和.L1(8行)处,它们都位于代码段的.rodata节中。

注:由于linux下字符编码方法的不同,在windows下正常显示的汉字在linux下会无法显示(6行)。

2、其他常量

还有一些常量在汇编代码中会直接以立即数的形式出现,如13行的整型常量4,在汇编代码中显示如下:

3.3.1.2 变量

1、局部变量

局部变量存储在寄存器或栈中。

hello.c中有一个局部变量i(11行),查看汇编代码可以看到它被存放在了栈中地址为%rbp-4的位置处(31行),53行将i与7比较大小,54行检验其是否小于等于7,对应于hello.c中17行for循环中i<8的终止条件。

3.3.2 赋值操作

汇编语言通过mov指令来实现C语言代码中的赋值操作,如在hello.c的for循环中对局部变量i赋初值为0(17行),在hello.s中是通过movl指令实现的(31行)。

3.3.3 算术操作

hello.c的for循环中局部变量的自增操作:i++,在hello.s中是通过addl指令实现的(51行)。

3.3.4 数组/指针操作

main函数的参数中有一个字符串数组argv,在源代码的18、19两行中需要访问这个数组中的元素。

在hello.s中,先将寄存器%rbp的值入栈保存起来(16行),然后将%rsp的值传送给%rbp(19行),21行中%rbp的值减32,表示栈指针的移动,而22、23两行表明main函数的第一个参数argc从%edi中存入栈的地址为%rbp-20位置处,第二个参数argv,即字符串数组中第一个字符串argv[0],从%rsi中存入栈的地址为%rbp-32位置处。

hello.c中的18行需要访问argv[1]和argv[2],由于argv是字符串数组,因此argv中的元素是字符串的地址,而在x86中一个地址占8字节,而由上所述,argv[0]的地址是%rbp-32,由此可以计算出,argv[1]、argv[2]、argv[3]的地址分别为%rbp-24、%rbp-16、%rbp-8。通过查看hello.s的汇编代码可以印证这一点,34行中先把argv[0]的地址%rbp-32传送给%rax,然后将其加16(35行),即%rax的值变为%rbp-16,此时表示的是argv[2]的地址,然后再通过%rax的值访问内存即可得到字符串argv[2](还是个地址),将其作为.c文件中18行printf函数的第三个参数传给%rdx(36行)。37-40行以类似的方式将字符串argv[1]作为printf函数的第二个参数传给%rsi,而44-47行也以类似的方式将字符串argv[3]作为.c文件中19行atoi函数的参数传给%rdi。

3.3.5 控制转移&关系操作

C语言中的控制转移(如if语句等)往往是和关系操作一起出现的,而在汇编语言中,这通常是用cmp指令和相应的条件跳转指令搭配实现的,如hello.c中13行的if (argc != 4)就包含了if语句和!=操作符,而这在hello.s中是通过cmpl和je指令一起实现的(24、25行)。

3.3.6 函数操作

3.3.6.1 参数传递

在汇编语言中函数的参数是通过寄存器和栈传递的,如hello.c中18行的printf函数有3个参数,它们在hello.s中分别被存在了寄存器%edi、%rsi、%rdx中(36、40、41行)。

3.3.6.2 函数调用

在设置好函数的参数后,通过call指令实现函数的调用,如hello.s中43行调用函数printf。

3.3.6.3 函数返回

函数调用的最后一条指令是函数返回,通过ret指令来实现,如hello.s中59行在main函数的所有内容被执行完后,通过ret指令完成函数的返回。

3.4 本章小结

本章介绍了hello.i文件编译成hello.s文件的过程,详细分析了原始的.c文件中各部分变量、常量、算术运算、数组、控制转移、关系操作以及函数调用在汇编语言中是什么样子。这些生成的汇编代码已经没有了高级语言语句的面貌,而是变成了一条条机器指令的文本形式,它们已经能够在指令级层面控制CPU了,并且还能够被人类所理解,可以说是介于程序员与机器之间的桥梁。

第4章 汇编

2分)

4.1 汇编的概念与作用

经过编译后得到的文件hello.s虽然已经被翻译成了机器指令,但仍是机器无法识别的机器指令的文本形式,而汇编就是用汇编器将hello.s翻译成可以被机器直接执行的机器码形式,然后把这些二进制形式的机器指令打包成可重定位目标文件的格式,得到一个二进制文件hello.o

4.2 在Ubuntu下汇编的命令

在终端中输入命令gcc -c hello.s -o hello.o -fno-PIC -no-pie -m64,即可得到一个由汇编器生成的可重定位目标文件hello.o。

4.3 可重定位目标elf格式

hello.o文件在x86-64 Linux系统中使用可执行可链接格式(Executable and Linkable FormatELF),典型的ELF可重定位目标文件格式如下:

4.3.1 ELF

通过命令readelf -h hello.o可以查看ELF头的相关信息:

EFL头以16字节的序列Magic开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息。

EFL头中还包括程序的入口点地址,也就是程序运行时要执行的第一条指令的地址为0x0,查看hello.o的反汇编代码(在终端输入命令objdump -s -d hello.o > hello.o.txt可以反汇编hello.o,并将结果保存到hello.o.txt中),发现程序运行时第一条指令的地址确实为0x0

4.3.2 节头部表

通过命令readelf -S hello.o来查看节头部表:

节头部表描述了hello.o中文件中各个节的语义,包括节的类型、位置、大小和对齐等信息。由于是可重定位目标文件,所以每个节的地址都从 0 开始。

它还利用Flags来描述各节的读写权限:

4.3.3 符号表

使用命令readelf -s hello.o来查看.symyab节中ELF的符号表:

它存放在程序中定义和引用的全局变量和函数的信息,如4-10行显示的是在程序中用到的函数,包括main函数、exit函数、printf函数等。

4.3.4 重定位条目

使用命令readelf -r hello.o查看hello.o的重定位条目:

当汇编器生成hello.o后,它并不知道数据和代码最终将放在内存中的什么位置,它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,重定位条目给出了符号相对节的首地址的偏移量、重定位类型(PC相对引用或绝对引用)以及符号名称等信息,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码重定位条目放在.rela.plt中,已初始化数据的重定位条目放在.rela.dyn中。

4.4 Hello.o的结果解析

使用命令objdump -s -d hello.o > hello.o.txt可以反汇编hello.o,并将结果保存到hello.o.txt中,查看其与编译生成的hello.s有何不同:

不同点:

  1. 分支跳转:在hello.s中,分支跳转的目标位置是通过.L1.L2这样的助记符来实现的,而在hello.中,跳转的目标位置是指令的地址。
  2. 函数调用:在hello.s中,call后面的目标函数是它的函数名,而在hello.o中,call后面的是目标函数相对main函数首地址的偏移量,即相对偏移地址。

4.5 本章小结

本章分析了汇编的过程,并分析了ELF头、节头部表、符号表以及重定位条目中所包含信息的含义,同时还比较了反汇编hello.o生成的文件与编译生成的hello.s有何不同之处。

5章 链接

1分)

5.1 链接的概念与作用

链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。

链接器在软件开发中扮演着一个关键的角色,因为它使得分离编译成为可能。我们不用将一个大型应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。即使对hello这样一个非常简单的小程序,链接的作用也是巨大的。

5.2 在Ubuntu下链接的命令

Linux下使用ld链接的命令是ld -o hello -dynamic -linker /lib64/ld-linux-x86-64.so.2 /user/lib/x86_64-linux-gnu/ctr1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /sur/lib/x86_64-linux-gnu/crtn.o

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

5.3.1 ELF

用命令readelf -h hello查看hello的ELF头:

可以看到程序的类型变成了EXEC(可执行文件),入口点地址也变为了0x4010f0

5.3.2 节头部表

链接器将各个文件对应的段都合并了,并且重新分配并计算了相应节的类型、大小、偏移量等信息。

从中可以看到.text节的地址为0x4010f0,这与在ELF头中看到的程序的入口点地址相符。

5.3.3 符号表

hello的符号表分成了两部分,第一部分.dynsym中的是需要用到的在共享库中符号的信息,这些符号表条目在动态链接时会发挥作用。

第二部分.symtab中包含的是已经被重定位的符号,可以看到它们的Value值已经从0变成了相应的地址。

5.4 hello的虚拟地址空间

       使用edb加载hello,可以看到hello的虚拟地址起始位0x401000。

       与5.3节中的节头部表进行比对,比如.data节的起始地址为0x404048,查看这个内存单元的内容:

5.5 链接的重定位过程分析

使用命令objdump -s -d hello > hello.txt,对可执行文件hello进行反汇编,并将结果保存到hello.txt中,查看其与hello.o.txt有何不同。

不同点:

1、新增函数

链接后,加入了许多需要用到的库函数,如printf、atoi、sleep等,但同时也可以看到,由于是动态链接,因此每个函数下面并不是函数内容的汇编代码,而是一条间接跳转指令,运行时调用函数会跳转到共享库的相应位置处。

2、新增节

新增了.init节和.plt节,其中.init节定义了一个小函数_init,程序的初始化代码会用到它,而.plt节表示的是过程链接表,由于链接时选择生成的是位置无关代码(PIC),因此需要过程链接表使代码可以被加载到内存的任何位置而无需链接器修改。

3、函数调用与跳转

由于hello已经是重定位完成的可执行文件,因此每个函数调用(如280行call printf)和跳转指令(如264行je)后面的目标地址都是确切的虚拟地址。

5.6 hello的执行流程

当在Shell中运行hello时,Shell会调用驻留在内存中的加载器来运行它。当加载器运行时,它创建如图的内存映像:

ELF头部表的引导下,加载器将可执行文件的片复制到代码段和数据段。接下来加载器跳转到程序的入口点,也就是_start函数的地址。这个函数是在系统目标文件ctrl.o中定义的。_start函数调用系统启动函数__libc_start_main,该函数定义在libc.so中。它初始化执行环境,调用用户层的main函数,处理main函数的返回值,并且在需要的时候把控制返回给内核。

下面用edb执行hello,逐步查看程序的执行过程:

首先,最初的程序地址会在0x7f366a3962b0处,这里是hello使用的动态链接库ld-2.31.so的入口点_dl_start

然后,程序跳转到_dl_init,在经过了一系列初始化后,跳到hello的程序入口点_start

然后程序通过一个间接call指令跳到动态链接库ld-2.31.so的__libc_start_main处,这个函数会进行一些初始化,并负责调用main函数;

然后它会调用动态链接库中的__cxa_atexit函数,它会设置在程序结束时需要调用的函数表;

然后返回到__libc_start_main继续,然后调用hello可执行文件中的__libc_csu_init函数,这函数是由静态库引入的,也是做一些初始化的工作;

然后返回到__libc_start_main继续,然后调用动态链接库里的_setjmp函数,应该是设置一些非本地跳转;

然后返回到__libc_start_main继续,就要开始调用main函数了;

由于我们在edb运行hello的时候并未给出额外的命令行参数,因此它会在第一个if处通过exit(1)直接结束程序;

之后,在进行了若干操作后,程序退出。

5.7 Hello的动态链接分析

在进行动态链接前,首先进行静态链接,生成部分链接的可执行目标文件hello。此时共享库中的代码和数据没有被合并到hello中。只有在加载hello时,动态链接器才对共享目标文件中的相应模块内的代码和数据进行重定位,加载共享库,生成完全链接的可执行目标文件。

hello程序在加载可执行文件时,自动加载了动态链接库ld-2.31.sohello程序的一些地方可能会引用这个动态链接库里的符号(比如函数调用),这一机制是通过PLT表和GOT表实现的,它们的每一个条目都对应动态链接库中的符号引用。通过readelf中我们可以在节头部表里看到关于PLTGOT的信息(它们对应两个节):

在程序一开始,先执行_dl_start和_dl_init,_dl_init能够修改PLT和GOT,这一过程相当于“注册”动态链接库的符号,使得hello在后面的正常运行中能够引用它们,实现诸如间接跳转等行为。

使用edb来验证这一过程,监测0x404000地址处PLT的数据变化。

这是调用_dl_init之前的:

这是调用_dl_init之后的:

可以看到PLT表的内容发生了变化。

5.8 本章小结

本章我们详细分析了链接的过程,比较了hello.txt和hello.o.txt之间的不同,分析了hello程序的执行流程,包括动态链接是如何进行的。我们会发现静态库和动态链接库的部分在我们看不见的地方起到了很大的作用,所以hello这一程序背后确实比表面上要复杂的多。正是有了链接机制,我们才能十分方便地借助库来编写程序,使其正常地在操作系统提供的平台上运行。

6章 hello进程管理

1分)

6.1 进程的概念与作用

进程是程序在执行中的一个实例,系统的每个程序都运行在某个进程的上下文中。上下文是由程序运行时的一些状态组成的,这些状态包括存放在内存里的程序的代码、数据、栈、寄存器、所占用的资源等。

进程这一机制提供了一种假象,使得每个程序都似乎独占CPU和内存,但实际上同一时刻可能有很多进程在逻辑上并发运行,这是现代多任务操作系统的基础。

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

shell是一种交互型应用级程序,它可以代表用户运行其它程序,用户可以通过shell和操作系统内核进行交互。shell是信号处理的代表,负责各进程的创建、程序的加载运行以及前后台控制、作业调用、信号发送与管理等。

总的来说,shell的处理流程就是不断读入用户输入的命令行,然后解析并处理,将这个命令行视作一个新的作业,创建关于这个作业的子进程,将其置于前台或后台运行。

以hello程序为例,shell的具体处理流程如下:

  1. 在Shell中输入hello程序的路径和需要的参数;
  2. Shell判断用户输入的是否为内置命令,hello不是内置命令,被认为是一个可执行目标文件;
  3. Shell根据命令行的内容构造argv和envp;
  4. Shell使用fork函数创建子进程,调用execve函数在新创建的进程的上下文中加载hello程序,将 hello中的.text节、.data节、.bss节等内容加载到当前进程的虚拟地址空间;
  5. execve函数调用加载器,然后加载和运行动态链接器进行动态链接,再跳转到程序的入口点,开始执行_start函数,这样hello程序便正式开始运行了。

6.3 Hello的fork进程创建过程

在shell接受到./hello这个命令行后,它会对其进行解析,发现是加载并运行一个可执行文件的命令,于是它会先创建一个对应./hello的作业,再用fork函数创建一个子进程,这个子进程与父进程几乎与父进程完全相同,它们有着相同的代码段、数据段、堆、共享库以及栈段,但它们的pid不同,因此可以进行区分。然后父进程(即shell主进程)会将新创建的子进程用setpgid函数放在一个新的进程组中,这样这个进程组就对应./hello这个作业,shell可以通过向进程组中所有进程发信号的方式管理作业。

6.4 Hello的execve过程

在子进程被创建出来后,它会去调用execve函数来加载可执行文件到当前进程,子进程原来的用户区域将会被删除,然后通过虚拟内存机制将可执行文件hello中的各个段映射到对应的代码段、数据段等地址空间,这样就加载了hello的新的用户区域。然后execve函数会运行动态链接器加载hello用到的共享库(比如上面提到过的ld-2.31.so),也是通过虚拟内存映射的方式将其加载到用户区域相应的空间。最后,子进程的程序将直接跳转到hello的入口点,开始执行hello程序。

6.5 Hello的进程执行

进程正常运行是依赖于其上下文的,上下文是由程序正确运行的状态组成的,这些状态包括存放在内存里的程序的代码、数据、栈、寄存器、所占用的资源等,在程序正常运行的时候,这些上下文状态绝不能被异常破坏。

然而,在某一时间段往往有多个进程在并发执行,当前在CPU运行的进程每隔一段时间就需要切换至其它进程,因此进程是需要不断进行切换的。操作系统通过给每个进程分配时间片的方式,决定了前进程能够执行它的控制流的连续的一段时间。当CPU认为一个进程已运行了足够长的时间后,便会执行上下文切换,将当前进程的上下文保存,恢复某个之前进行的尚未结束的进程的上下文,然后开始继续执行这个进程。

当发生中断时可能会发生上下文切换。假设进程A初始时运行在用户模式下,而从进程A上下文切换到进程B时,内核先代表进程A在内核模式下执行指令,然后在某一时刻代表进程B执行指令,此时仍处于内核模式下,在切换完成后,进程B又会在用户模式下继续运行。以hello程序为例,假设hello进程现在正在运行,突然发生了由主板上的时钟芯片引发的时钟中断,然后处理器会从用户模式立刻转入到内核模式,控制流转入操作系统内核程序,内核会将hello进程目前的上下文暂时保存起来,挂起hello进程,然后通过进程调度程序找到要切换的进程B,加载B被保存的上下文,将控制流交给进程B,切换完成后处理器又重新转入到用户模式。

当执行系统调用时也可能发生上下文切换,根据hello程序的内容,在hello程序被执行的时候,初始时正常运行,然后hello调用sleep函数,这时sleep通过syscall引发异常(陷阱),转入内核模式,内核保存hello的上下文,然后让hello进程休眠,转去执行其它进程。等到休眠时间到了的时候,此时时钟中断使得控制流从其它进程跳到内核,内核发现hello进程的休眠时间到了,就把hello解除休眠状态。之后在应当进行进程切换的时候,恢复hello进程的上下文,控制流转入hello进程,切换完成后处理器再从内核模式转为用户模式。

6.6 hello的异常与信号处理

hello程序执行过程中会出现以下几类异常:

1、乱按普通字符或回车

在hello正在运行的过程中,乱按字符或回车发现字符可以正常地被输出在屏幕上,但没有任何作用。这里应该是按下普通按键,产生键盘中断,然后切换到内核模式,内核就知道按下了某个字符,然后将字符输出到屏幕上,再切换回hello进程。

2、Ctrl+Z

      键盘输入Ctrl+Z会停止当前的进程,shell返回到接受命令行的状态。按下Ctrl+Z会产生键盘中断,然后转入内核模式,内核识别出Ctrl+Z,它分析Ctrl+Z的接收方应当是shell,然后内核向shell发送SIGTSTP信号,shell把信号转发到当前运行的前台作业./hello对应的进程由于没有设置信号处理程序,所以hello进程接收到信号后执行默认行为,即停止进程。

3、通过kill命令向指定进程发送指定信号

由上图可知,当前有2个停止的hello进程,其pid分别为10296、10297,通过kill命令向pid为10296的进程发送序号为9的信号,即SIGKILL,这个进程捕获信号后执行信号的默认行为,即终止进程。然后我们用ps命令可以看到这个进程已被杀死,用jobs命令也可以看到已经没有了对应的作业。

4、Ctrl+C

用命令fg使停止的作业[2]继续作为前台作业运行,此时进程hello收到SIGCONT信号,继续运行。然后输入Ctrl+C,同输入Ctrl+Z时类似,内核向shell发送SIGINT信号,shell把信号转发到当前运行的前台作业./hello对应的进程进程接收到信号,执行默认行为,终止进程。用命令ps可以看到这个进程确实已经终止了。

6.7本章小结

本章详细分析了在进程中运行hello程序的整个过程,从创建新进程,到加载并运行程序,然后分析了进行进程的上下文切换时背后的原理,最后对hello程序在执行过程中会遇到的几类异常以及对不同信号的处理进行了分析。由此可以看到在进程中运行程序仍然是一个表面简单但背后机制复杂的过程。我们需要明白,hello程序在运行的时候绝对不是像表面那样独占CPU与内存,而是通过操作系统的进程调度机制与其它进程并发地运行。

7章 hello的存储管理

2分)

7.1 hello的存储器地址空间

逻辑地址是程序直接使用的地址,它表示为“段:偏移地址”的形式,由一个段选择子(一般存在段寄存器中)再加上段内的偏移地址构成。

线性地址(或者叫虚拟地址)是虚拟内存空间内的地址,它对应着虚拟内存空间内的代码或数据,表示为一个64位整数。

物理地址是真正的内存地址,CPU可以直接将物理地址传送到与内存相连的地址信号线上,对实际存在内存中的数据进行访问。物理地址决定了数据在内存中真正存储在何处。

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

对于一个以“段:偏移地址”形式给出的逻辑地址,CPU将会通过其中的16位段选择子定位到GDT/LDT中的段描述符,通过这个段描述符得到段的基址,与段内偏移地址相加得到的64位整数就是线性地址。这就是CPU的段式管理机制,其中段的划分,也就是GDT和LDT都是由操作系统内核控制的。

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

虚拟地址空间会被分为若干页(通常大小为4KB),即分页机制。CPU对于一个线性地址会取它的高若干位,通过它们去存储在内存中的页表里查询对应的页表条目,得到这个线性地址对应的物理页起始地址,然后与线性地址的低位(页中的偏移)相加就得到物理地址。

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

为了节省页表的空间,CPU一般采用多级页表的机制,即上一级页表中的条目指向下一级页表。在一般的x86-64模式下,CPU采用四级页表,线性地址被按位划分为5部分,前4个部分分别作为该级页表的索引,最低的12位作为页的偏移地址。CPU会逐级地查找到对应物理页的PTE,从而得到物理地址。

而且为了优化对于页表的查找效率,CPU还提供了专门用于页表的缓存TLB,即翻译后备缓存器,这样CPU可以将近期常用的PTE缓存到TLB中,从而减少对内存的访问。

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

由于CPU对内存的访问较慢,CPU为内存访问提供了更快的三级Cache缓存,之前访问过的内存块将被暂存在缓存中。当CPU对一个物理地址进行访问的时候,首先去看L1 Cache里是否有对应内存块,若有则直接访问L1 Cache,否则去看L2 Cache……(CPU会根据cache的结构,将物理地址分为标记位、组索引、块内偏移三段,然后根据一定规则将这三段的内容与Cache相应的部分进行匹配,看需要访问的内存块是否已缓存进了Cache中)若三级Cache里都没有对应内存块,那么CPU将会直接访问物理内存,并将物理内存中的块加载到L3 L2 L1 三级Cache中。若加载的过程中需要替换Cache中的内存块,则使用最近最少访问(LRU)策略替换掉Cache中的某个内存块。

7.6 hello进程fork时的内存映射

当shell使用fork创建子进程时,内核为新的子进程创建各种数据结构,并分配给子进程一个唯一的PID,为了给它创建虚拟内存空间,内核创建了当前进程的mm_struct、区域结构和页表的原样副本(同时子进程也可以访问任何父进程已打开的文件),将两个进程的页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。这样,在新进程里,最开始的时候它的虚拟内存映射和原进程的虚拟内存映射相同,但当这两个进程中的任意一个进行写操作时,写时复制机制就会创建新页面,这样两个进程的地址空间就在逻辑上私有了。

7.7 hello进程execve时的内存映射

execve函数运行驻留在内核区域的加载器,在当前进程中加载并运行hello程序,加载并运行的具体步骤如下:

1、将当前进程虚拟内存空间中的用户部分的已存在的区域结构删除;

2、为新程序hello的代码、数据、.bss段和栈段创建新的区域结构,这些新的区域都是私有且写时复制的。代码和数据区域被映射为hello可执行文件中的.text和.data节,.bss区域请求二进制0,映射到匿名文件,栈和堆被初始化为空;

3、execve会将hello链接的动态链接库(共享对象)映射到虚拟地址空间的共享区域内;

4、设置进程上下文的程序计数器的内容为hello程序的入口地址。

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

当CPU执行某条指令的内存访问时,如果页表中的PTE表明这个地址对应的页不在物理内存中,那么就会引发缺页故障,使进程切换至内核模式,执行操作系统提供的缺页中断处理程序,缺页中断处理程序能够将存在磁盘上的页以一定的替换策略加载到物理内存,并更新页表。缺页故障处理完毕后,CPU会重新执行该条指令,这一次就可以成功得到物理地址。

7.9本章小结

本章详细分析了hello程序看似简单的内存访问背后的复杂机制,包括逻辑地址到虚拟地址的转换、基于分页机制的虚拟地址到物理地址的转换、内存与三级Cache组成的层次结构以及缺页故障和缺页中断处理等机制。这其中的虚拟内存机制在访问内存操作中起着极为重要的作用,它提供的虚拟内存空间使得程序在表面上看起来像是独占了整个内存。

结论

       通过以上7章的分析,我们可以用计算机系统的术语来描述一下hello这个程序的经历:

1、程序员用文本编辑器编写最原始的源代码文件hello.c;

2、预处理器通过替换#include和#define等,将被包含的内容直接插入到预处理语句所在的位置,对hello.c的代码进行扩展,得到文件hello.i;

3、编译器将hello.i中的内容翻译为汇编语言代码,得到机器代码的文本形式构成的文件hello.s,程序开始从程序员角度进入机器层面;

4、汇编器将文本形式的汇编语言代码转换为二进制形式的可重定位目标文件hello.o,程序中使用的绝对地址将暂时保留为重定位条目,程序开始进入可以被计算机识别的机器码形态;

5、链接器进行符号解析,对hello.o中的符号进行重定位操作,同时生成程序中会用到的共享库中符号的重定位和符号表信息,为之后的动态链接做准备,得到可执行文件hello;

6、shell读入输入的命令行,对其进行解析和处理,通过fork函数创建进程,execve函数加载可执行文件hello及其所需的动态链接库,通过虚拟内存机制将可执行文件中的节映射到虚拟地址空间中;

7、在hello进程运行时,会产生诸多的异常与信号,例如键盘中断、SIGTSTP、SIGINT等,当进程中出现异常时会从用户模式切换到内核模式,然后由内核执行相应的异常处理程序,执行完后会向进程发送信号,进程接收到信号后会执行相应的信号处理程序,执行完后返回继续执行hello程序;

8、在程序hello运行时,它将使用一个属于自己的虚拟地址空间,通过分段机制、分页机制以及内存和多级Cache组成的层次结构进行内存访问;

9、最终,hello运行结束正常退出或者进程收到信号后终止,这都会使得操作系统回收hello的进程。

总而言之,我们会发现哪怕是一个小小的hello world程序的背后都是一系列复杂的机制在支撑着它,需要一系列软硬件互相之间的协同配合才能使其得以运行,而这或许便是“计算机系统”的真正含义。

附件

        1、源代码hello.c

       2、经过预处理器生成的修改了的源文件hello.i

       3、经过编译器生成的汇编语言文件hello.s

       4、经过汇编器生成的可重定位目标文件hello.o

       5、经过链接器生成的可执行目标文件hello

       6、hello.o通过objdump得到的反汇编文件hello.o.txt

        7、hello通过objdump得到的反汇编文件hello.txt

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值