程序人生-Hello’s P2P

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业        人工智能         

学     号       2021110683        

班     级        2103602           

学       生         徐柯炎          

指 导 教 师          郑贵滨          

计算机科学与技术学院

2023年5月

摘  要

本文从头开始讲述了编程界的元老“hello”传奇的一生,通过了解hello的预处理、编译、汇编和链接的过程知晓了传奇的诞生,又通过程序对控制流的管理、内存空间的分配、信号的处理、对 I/O 设备的调用来彻底了解hello从创建到结束那精彩的一生。

通过了解到一个伟大的程序以及可执行目标文件是如何一步一步诞生的。接着又跟踪了hello作为进程诞生到消失的过程,从而理解系统中进程是如何运作的。

从hello的一生可以映射出庞大的计算机系统中的各种知识,让我们跟着传奇的hello,通过下面的实验和分析来探寻可执行文件的诞生,以及作为进程从诞生到消失的过程,进而加深对计算机系统的深入理解。

关键词:计算机系统、P2P、020、预处理、编译、汇编、链接、进程                             

目  录

第1章 概述... - 4 -

1.1 Hello简介... - 4 -

1.2 环境与工具... - 4 -

1.3 中间结果... - 5 -

1.4 本章小结... - 5 -

第2章 预处理... - 6 -

2.1 预处理的概念与作用... - 6 -

2.2在Ubuntu下预处理的命令... - 6 -

2.3 Hello的预处理结果解析... - 7 -

2.4 本章小结... - 9 -

第3章 编译... - 10 -

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

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

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

3.4 本章小结... - 16 -

第4章 汇编... - 17 -

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

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

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

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

4.5 本章小结... - 22 -

第5章 链接... - 23 -

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

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

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

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

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

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

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

5.8 本章小结... - 30 -

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

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

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

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

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

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

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

6.7本章小结... - 38 -

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

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

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

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

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

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

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

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

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

7.9本章小结... - 40 -

结论... - 41 -

附件... - 42 -

参考文献... - 43 -

第1章 概述

1.1 Hello简介

P2P: From Program to Process

P2P就是From Program to Process,即从程序到进程的过程。用户利用高级语言编辑程序形成hello.c,首先通过预处理器(cpp)对源程序进行预处理,得到修改了的源程序hello.i,接着,编译器(cc1)将其翻译为汇编程序hello.s,然后经过汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序hello.o,接下来,经过链接器,将调用的C标准库中的函数(如printf等)对应的预编译好了的目标文件以某种方式与hello.o文件进行合并,得到可执行目标程序hello。最后,Hello通过shell运行加载,fork产生子进程,于是hello.c完成了从program到process的过程。

图 1.1.1 程序编译过程

020: From Zero-0 to Zero -0

020就是From Zero to Zero,指的是hello在系统中从开始到结束的过程。首先,hello在shell中用fork()创建子进程,再用execve加载可执行目标程序hello;接着映射虚拟内存,先删除当前虚拟地址已存在的数据结构,为hello的代码、数据、bss等创建区域,然后映射共享区域,设置程序计数器;然后程序开始载入物理内存,进入CPU处理,CPU为执行文件hello分配时间片,进行取指、译码、执行等流水线操作。内存管理器和CPU在执行过程中通过L1、L2、L3三级缓存和TLB多级页表在物理内存中高效地取出数据,在描述符与接口的帮助下通过I\O系统进行输出。多方合作配合之下,hello完成执行。在程序结束后,父进程会对执行完毕的进程进行回收,内核删除hello在系统中的所有痕迹,释放内存空间。这时hello就由无又变成了无,完成了020的过程。

1.2 环境与工具

硬件环境

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

软件环境

Windows10 64位;Vmware 14;Ubuntu 16.04 LTS 64位;

开发与调试工具

gcc, readelf, objdump, edb, ld, gedit, vim

1.3 中间结果

hello.c          C语言源文件

hello.i          预处理产生的文件

hello.s          编译产生的汇编代码文件

hello.o         汇编产生的可重定位目标文件

hello.o.elf    hello.o文件的ELF格式

asm.txt        hello.o反汇编生成的文本文件

hello            链接产生的可执行目标文件

hello.elf       hello文件的ELF格式

asm_2.txt     hello反汇编生成的文本文件

1.4 本章小结

本章简单介绍了hello程序P2PFrom Program to Process)及020From Zero-0 to Zero-0)的过程,并说明了本次大作业所需的硬件环境、软件环境以及开发工具和中间生成的文件信息。

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:

预处理是指预处理器(cpp)根据以字符#开头的命令,修改原始的C程序,从而形成完整的文件。预处理后的文件后缀为(.i),如hello.c预处理后成为hello.i。预处理的本质实际是文本替换,处理过后的文件同hello.c一样都是由ASCII码组成的文件,即文本文件。

C语言的预处理主要有三个方面的内容:

  1. 宏定义(#define 标识符 文本);
  2. 文件包含(#include "文件名");
  3. 条件编译(#ifdef,#else,#endif)。

预处理的作用:

预处理可以对C语言中以#开头的语句进行处理,并对注释进行删除:

  1. 可以进行宏定义的替换,用常量来替换标识符(如#define 标识符 文本);
  2. 处理文件包含,将包含的文件插入到程序文本中(如#include "文件名");
  3. 条件编译,选择符合条件的代码送至编译器编译,删除条件不符合的代码,进而实现有选择地执行相关操作(如#ifdef,#else,#endif)
  4. 实现特殊控制指令。(如#error)
  5. 删除C语言源程序中所有的注释;

2.2在Ubuntu下预处理的命令

预处理的命令为:gcc -E hello.c -o hello.i

在虚拟机上预处理的过程如下:

图 2.2.1 预处理的命令

经过预处理的hello.i文件如下:

图 2.2.2 生成的hello.i文件

2.3 Hello的预处理结果解析

Hello.i共3060行,main函数在第3047行,hello.i将头文件中的内容引入,例如声明函数、定义结构体、定义变量、定义宏等内容。可以发现预处理将头文件中的内容全部放到hello.i文件中,方便编译等操作。

main函数如下图所示,通过与源文件对比我们发现,这里的main函数和hello.c文件中的主函数代码是相同的,可知预处理阶段并没有对代码段进行处理。但是发现hello.i中删除了注释部分,这也说明了预处理确实会删除注释部分的内容。

图 2.3.1 hello.i中main函数部分

也可以看到源文件中没有的部分,如下图:

图 2.3.2 hello.i新增部分

可以看出,新增的部分中有在源程序中引用的stdio.h,unistd.h和stdlib.h的代码,它们都是在预处理器的作用下,被直接插入程序文本之中。他们在预处理的过程中都需要被直接插入到hello.i文件中,这会使得文件代码量与源程序相比大大增加。

以下是hello.i中引入的定义,结构体以及外部声明。

图 2.3.3 hello.i中引入的定义

图 2.3.3 hello.i中引入的结构体

图 2.3.4 hello.i中引入的外部声明

2.4 本章小结

本章主要介绍了预处理的概念以及预处理在五个方面的作用与功能,并在虚拟机下将hello.c文件经过预处理生成了hello.i文件,并且对照hello.c与hello.i分析了预处理产生的文件与源文件的相同与不同之处,加深了对预处理整个过程的理解。

第3章 编译

3.1 编译的概念与作用

编译的概念:

编译是指对以便于人编写、阅读和维护的高级程序设计语言为基础所编写的源代码程序,进行语法检查,若语法无误则将源代码翻译成较为底层的汇编代码,形成汇编语言程序,即文本文件hello.i通过编译器(ccl)的帮助,翻译成汇编语言程序hello.s的过程。

编译的作用:

编译的作用是将以高级程序设计语言为基础所编写的源代码程序翻译为汇编语言程序。在这个过程中,会进行以词法分析、语法分析、语义分析来生成汇编语言程序,且编译器可能在这个过程中根据编译选项对程序进行一些适当的优化。在汇编程序里的指令将更加贴近机器指令,也更为贴近计算机执行程序时的过程,从而深入理解程序在机器层面上的底层实现。

3.2 在Ubuntu下编译的命令

预处理的命令为:gcc -E hello.c -o hello.i

在虚拟机上编译的过程如下:

图 3.2.1 编译的命令

Hello.i经过编译后形成的文件hello.s如下:

图 3.2.2 编译产生的hello.i文件

3.3 Hello的编译结果解析

3.1.1 数据

  1. 数字常量

图 3.3.1 数字常量

其中argc!=4中的4是由立即数$4来实现的,for循环中的i<8是由$7来实现的。

  1. 字符串常量

在hello.s文件中可以发现

图 3.3.2 字符串常量

如图,根据所学知识文件hello.s中将字符串常量存储在只读段.rodata段,该结果表示编译对中文字符进行了utf-8编码,并存储在只读代码区的.rodata节,在程序运行时会直接通过寻址找到常量。

关于输出字符串常量的对应关系如下图:

图 3.3.3 字符串常量

通过两张图的对比可以看出,打印字符串常量时,编译器将语句翻译为先将字符串存放的地址存入寄存器%rdi,再进行打印。

  1. 变量

不同类型的变量在不同位置定义,局部变量在堆上进行定义和释放,初始化的全局变量和静态变量定义在只读代码区的.bss节,已初始化的全局和静态变量定义在只读代码区的.data节。

在hello.c中,存在局部变量i,编译器进行编译的时候将局部变量i会放在堆栈中,如下图所示:

图 3.3.4 变量

上图中%edi为传入的参数argc。

图 3.3.5 变量

传入参数char *argv[]存放在栈中,其中首地址存放在栈中-32(%rbp)的位置,argv[1]地址为-32(%rbp)+8,argv[2]地址为-32(%rbp)+16,argv[3]地址为-32(%rbp)+24。在hello.s中的使用如上图所示。

图 3.3.6 变量

由上图可知,局部变量int i存放在栈中-4(%rbp)的位置。

3.3.2 赋值

编译器将赋值的操作主要为对相应的寄存器或栈进行赋值。在hello.c中,我们对i赋初值,根据下图,对i:movl    $0, -4(%rbp)

局部变量赋值采取MOV指令,根据不同大小的数据类型有movb、movw、movl、movq等。

图 3.3.7 赋值

3.3.3 算术操作

对于主要的算数操作,+转换成add,-转换成sub,*转换成imul,/转换成div。

如下图,程序中的算数操作i++hello.s文件中的汇编代码翻译为用add指令通过操作数$1来实现,通过3.3.2的分析可以知道栈中-4(%rbp)的位置存放的是局部变量i,每次执行这条指令,实现的是i自增1。

图 3.3.8 算数操作

3.3.4 类型转换

隐式转换:隐式转换就是系统默认的、不需要加以声明就可以进行的转换数据类型自动提升。显示转换:程序通过强制类型转换运算符将某类型数据转换为另一种类型。

3.3.5关系操作

如下图,hello.c中有argc!=4,汇编语言为cmpl $4, -20(%rbp)。

hello.c中有i<10汇编语言为cmpl   $8, -4(%rbp)。

所以,编译器通常通过将比较编译为cmp指令实现。根据不同的数据大小,有cmpb、cmpw、cmpl和cmpq。比较之后,通过jmp系列指令跳转。

图 3.3.9 关系操作

3.3.6数组/指针/结构操作

如下图,编译器对源代码中数组的操作往往翻译为对地址的加减操作,其中首地址存放在栈中-32(%rbp)的位置,argv[1]地址为-32(%rbp)+$8,argv[2]地址为-32(%rbp)+$16,argv[3]地址为-32(%rbp)+$24即进行了地址的加减操作以访问数组。

图 3.3.10 数组/指针/结构操作

3.3.7控制转移

下图中第一个为条件跳转,当argc=4时才发生跳转,对应的c语言代码为if(argc!=4),第二个为无条件跳转,当i赋初值为0后跳转进入循环体。

 

图 3.3.11 控制转移

下图的也是条件跳转,当循环次数i<8时,不断通过跳转重复执行循环体中的内容。

图 3.3.12 控制转移

3.3.8函数操作

在程序中一共有6个函数调用,如下图,分别为printfexitprintfsleepatoi,getchar。

图 3.3.13 函数操作

从下图可以看到,在C语言源程序中执行该命令用的是printf函数,由于打印的是一个单纯的字符串,因此编译器对它进行了优化,改用puts函数进行打印,所以hello.s调用puts()函数。首先将调用函数所需要的参数,即需要打印的字符串常量的地址存放在寄存器%rdi中,然后执行call puts@PLT指令打印字符串。

对于第二个函数调用,即exit可以看到函数的第一个参数1保存在%edi中。

图 3.3.14 函数操作

如下图,对于第三个函数调用printf%rdi中存放第一个参数,%rsi中存放第二个参数。

图 3.3.15 函数操作

如下图,对于第四个和第五个函数调用,参数都存放在%rdi中,其中sleep的参数为atoi的返回值。

图 3.3.16 函数操作

如下图,关于第六个函数调用,由于getchar无参数输入,所以不用寄存器传参。

图 3.3.17 函数操作

3.4 本章小结

本章主要介绍了编译的概念和功能,包括将高级语言指令翻译为汇编语言指令,同时对与编译器的优化解释做了一些解释。同时还在虚拟机下将hello.i文件编译生成了hello.s文件。

接着按照C语言的不同数据与操作类型,分析了源程序hello.c文件中的语句是怎样转化为hello.s文件中的语句的。其中数据类型包括数字常量、字符串常量和局部变量;操作类型包括赋值、算术操作、关系操作、数组\指针\结构操作控制转移以及函数调用,让我收益匪浅,进一步了解了汇编代码和c语言的关系,增强了读汇编代码的能力。

第4章 汇编

4.1 汇编的概念与作用

汇编的概念:

汇编是指将汇编语言程序经过编译器(as)转化为二进制的机器语言指令,把这些指令打包成可重定位目标程序的格式,并将这些指令保存在目标文件.o中。即文本文件hello.s通过汇编器(as)转换为可重定位目标程序(二进制)hello.o的过程。但由于生成的是二进制文件,且于机器指令密切相关,因此可移植性会变差。

汇编的作用:

汇编的作用是把汇编语言翻译成机器语言,用二进制码0、1来对汇编语言中的符号进行翻译,即让它成为由机器指令构成的、机器可以直接识别的程序。最后把这些指令打包成可重定位目标程序的格式,并保存在目标文件.o中。

汇编还为之后的链接打下基础,方便了链接的过程。

4.2 在Ubuntu下汇编的命令

汇编的命令为:gcc -C hello.s -o hello.o

在虚拟机上汇编过程如下:

图 4.2.1 汇编的命令

通过命令生成的hello.o文件如下图所示。

图 4.2.2 汇编生成的文件hello.o

4.3 可重定位目标elf格式

查看hello.o的ELF格式可以通过在命令行中输入如下命令:

readelf  -a hello.o >hello.o.elf

此命令将hello.o的ELF格式输入到hello.o.elf中,方便查看。

 

图 4.3.1 查看ELF格式

4.3.1 ELF

hello.o的ELF格式的开头是ELF头,它以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息:包括ELF头的大小、目标文件的类型(如可重定位、可执行或共享的)、处理器体系结构、节头部表的文件偏移,以及节头部表中条目的大小和数量。

 

图 4.3.2 ELF头

4.3.2 节头目表

ELF文件格式中的节头部表描述了目标文件中不同节的类型、地址、大小、偏移等信息,以及可以对各部分进行的操作权限。

hello.o文件的节头目表如下图:

图4.3.3 节头目表

4.3.3 重定位节

ELF文件格式中的重定位节包含两个部分:.rela.text节与.rela.eh_frame节。

图 4.3.4 重定位节

当链接器把这个目标文件和其他文件组合时,需要修改表中的这些位置。一般,调用外部函数或者引用全局变量的指令都需要修改。

由上图可知,在hello.o的重定位节中包含了main函数调用的puts、exit、printf、sleep、getchar函数以及全局变量sleepsecs,还有只读区域.rodata节。表格记录了它们的偏移量、信息、类型、符号值、符号名称及加数。另用rela.eh_frame记录了.text的信息。表头信息含义为:

偏移量:需要重定位的信息的字节偏移位置(代码节/数据节)

信息:重定位目标在.symtab中的偏移量和重定位类型

类型:表示不同的重定位类型

符号值:符号的数值

符号名称:被重定位时指向的符号

加数:偏移

若重定义类型为R_X86_64_PC32,重定位一个使用32位PC相对地址的引用。若若重定义类型为R_X86_64_32,重定位一个使用32位绝对地址的引用。根据重定位条目和重定位算法即可得到相应的重定位位置。

4.3.4 符号表

ELF文件格式中的符号表用于存放程序中定义和引用的函数和全局变量的信息。

图 4.3.5 符号表

4.4 Hello.o的结果解析

通过用命令行objdump -d -r hello.o > asm.txthello.o的反汇编保存于文件asm.txt中,如下图所示。

                                                  图 4.4.1 objdump反汇编

图 4.4.2  asm.txt反汇编文件

通过对比hello.s和asm.txt可以发现有许多不同之处。

4.4.1 总体

hello.s中包含.type .size .align以及.rodata只读数据段等信息,而反汇编文件asm.txt中只有函数的相关内容。

4.4.2 数字

如下图,在汇编过程中,数字的进制发生了改变,从十进制转换为了十六进制,对应机器的二进制数。

图4.4.3 数字区别

4.4.2 对字符串常量的引用

在汇编过程中对于字符串常量的引用的表达不同。

图 4.4.4 字符串常量的应用区别

通过分析可知,hello.s中是用的全局变量所在的那一段的名称加上%rip的值,而重定位目标文件hello.o中用的是0%rip的值。

4.4.3 分支转移

在汇编过程后对于分支跳转的表达形式不同。

图 4.4.5 分支转移的区别

可以看到hello.s通过段名进行分支转移,跳转指令后用对应的段的名称表示跳转位置,而在hello.o的反汇编代码中每个段都有明确的地址,跳转指令后用相应的绝对或者相对地址表示跳转位置。

4.4.4 函数调用

在汇编过程后对于函数调用的表达方式也有所不同。

图 4.4.6 函数调用的区别

可以看到,hello.s中直接调用函数的名称,而hello.objdump中利用下一条地址相对函数起始地址的偏移量,链接重定位后才能确定地址。

4.5 本章小结

本章主要介绍了汇编的的概念以及汇编的作用,主要是将汇编语言程序经过编译器(as)转化为二进制的机器语言指令,并把这些指令打包成可重定位目标程序的格式,并保存在目标文件.o中。

通过readelf分析了hello.o的ELF格式,用readelf等列出了其各节的基本信息,包括ELF头、节头部表、重定位节以及符号表的功能与包含的信息。

最后用objdump反汇编生成了asm.txt文本文件,并分析了该反汇编程序与汇编语言程序hello.s中语句的对应关系。从数字进制、字符串常量的引用、分支转移以及函数调用的不同四个方面分析了二者的关系。

第5章 链接

5.1 链接的概念与作用

链接的概念:

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

链接可以执行于编译时,也就是源代码被翻译成机器代码时,也可以执行于加载时,也就是程序被加载器加载到内存并执行时,甚至执行于运行时,在现代系统中,链接是由叫做链接器的程序自动执行的。

链接的作用:

链接的作用是将经过汇编生成的可重定位目标文件(hello.o)或若干可重定位目标文件外加链接库合并成为一个可执行目标文件(hello)。

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

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 链接的命令

通过命令生成的文件hello如下图所示:

图 5.2.2 链接生成的可执行文件hello

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

通过指令readelf -a hello>hello.elf查看目标文件hello的ELF格式。

图5.3.1 可执行文件ELF节的信息

从图中可以看出执行文件hello的ELF的文件格式。

其中,第一列为节的名称,第二列为节的类型,第三列为节开头地址,第四列为节的偏移量。

5.4 hello的虚拟地址空间

通过readelf查看hello的程序头,可以发现其中的虚拟地址在EDB的Data Dump中都能找到相应的位置,且大小也相对应。

在EDB的symbol窗口,可以查看各段对应的名称以及各段的起始位置与结束的位置,与5.3中所展示出来的elf格式展示出来的相对应。

通过Data Dump通过地址进行查找的结果(下图为一个示例)。

图 5.4.1 虚拟地址的对应

根据上图可知,程序头包含了以下信息:

PHDR:程序头表

INTERP:程序执行前需要调用的解释器

LOAD:程序目标代码和常量信息

DYNAMIC:动态链接器所使用的信息

NOTE::辅助信息

GNU_STACK:使用系统栈所需要的权限信息

GNU_RELRO:保存在重定位之后只读信息的位置

其余的节的内容是存放在0x00400fff后面。

5.5 链接的重定位过程分析

通过指令objdump -d -r hello > asm_2.txt将可执行文件hello反汇编到文本文件asm_2.txt中。

图 5.5.1 反汇编指令

图 5.5.2 生成的反汇编文件asm_2.txt

5.5.1 代码量

从图中可以看出,可执行文件反汇编生成的代码量远大于可重定位目标文件反汇编生成的代码量。

图 5.5.3 代码量

5.5.2 函数

在hello.o的反汇编程序中,只有main函数,没有调用的函数段;经过链接过程后,原来调用的C标准库中的代码都被插入了代码中,并且每个函数都被分配了各自的虚拟地址。

图 5.5.4 函数

5.5.3 指令分配虚拟地址

从图中可以看出在hello.o的反汇编程序中, main函数中的所有语句前面的地址都是从main函数开始从0开始依次递增的,而经过链接后,每一条语句都被分配了虚拟地址。

图 5.5.5 分配虚拟地址

5.5.4 字符常量的引用

从图中可以看到,在hello.o的反汇编程序中,字符串常量的位置是用0加%rip的值来表示的,这是由于当时字符串常量并未分配虚拟内存,而在hello的反汇编程序中,因为字符串常量都有了相应的位置,所以用实际的相对下一条语句的偏移量加%rip即PC的值来描述其位置。

图 5.5.6 字符常量

5.5.5 函数调用

从图中可以看出,在hello.o的反汇编程序中,由于当时函数未被分配地址,所以调用函数的位置都用call加下一条指令地址来表示,而在hello的反汇编程序中,由于各函数已拥有了各自的虚拟地址,所以在call后加其虚拟地址来实现函数调用。

图 5.5.7 函数调用

5.5.6 跳转指令

从图中可以看出,在hello.o的反汇编程序中,对于跳转指令,在其后加上目的地址,为main从0开始对每条指令分配的地址,而在hello的反汇编程序中,由于各语句拥有了各自的虚拟地址,所以同样加上目的地址,但这里是每条指令的虚拟地址。

图 5.5.8 跳转指令

5.5.7 对于链接的过程的总体分析

首先,链接的过程主要分为两个过程:

1. 符号解析:符号解析解析目标文件定义和引用符号,并将每个符号引用和一个符号定义相关联。

2. 重定位:编译器和汇编器生成从0开始的代码和数据节。而链接器通过把每个符号定义与一个虚拟内存地址相关联,从而将这些代码和数据节重定位,然后链接器会修改对所有这些符号的引用,使得它们指向这个虚拟内存地址。

从上述对比图可以看出hello中每条指令都对应了一个虚拟地址,而且对每个函数,全局变量也都它关联到了一个虚拟地址,在函数调用,全局变量的引用,以及跳转等操作时都通过虚拟地址来进行,从而执行这些指令。

5.6 hello的执行流程

  1. 从加载hello到_start,到call main,以及程序终止的所有过程如下:
  2. _dl_start 地址:0x7f5b162a0100
  3. _dl_init 地址:0x7f5b162a0df0
  4. _start 地址:0x4010f0
  5. _libc_start_main 地址:0x403ff0
  6. _cxa_atexit 地址:0x7f5b162a8a60
  7. _libc_csu_init 地址:0x4011c0
  8. _main 地址:0x401125
  9. 若argc!=4:puts 地址:0x401090,exit 地址:0x4010d0,此时输出窗口打印出“用法: Hello 学号 姓名 秒数!”,程序终止。
  10. 在循环体中:print 地址:0x4010a0,sleep 地址:0x4010e0
  11. getchar 地址:0x4004b0
  12. _dl_runtime_resolve_xsave 地址:0x7f5b162428d0
  13. _dl_fixup 地址:0x7f5b1623ace0
  14. exit 地址:0x7f5b162c2650
  15. 最后程序终止。

5.7 Hello的动态链接分析

延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:

PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。

GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。

调用 dl_init 之前的全局偏移表:

图 5.7.1 调用dl_init之前

调用 dl_init 之后的全局偏移表:

图 5.7.2 调用dl_init之后

由图可知,如上图所示,和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]是动态链接器ld-linux.so模块中的入口点。

5.8 本章小结

本章介绍了链接的概念和功能,在虚拟机下对hello.o文件进行了链接,分析可执行文件hello的ELF格式及其虚拟地址空间,并对重定位、动态链接进行深入的分析。

第6章 hello进程管理

6.1 进程的概念与作用

进程的概念:

进程就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器地内容、程序计数器、环境变量以及打开文件描述符的集合。

进程的作用:

每次用户通过向 shell 输入一个可执行目标文件的名字,运行程序时,shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。

进程提供了两个抽象:①进程提供独立的逻辑控制流,好像我们的程序独占地使用处理器;②提供一个私有的地址空间,好像我们的程序独占地使用内存系统。这使得使CPU被科学有效地划分成多个部分以并行地运行多个进程。

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

作用:shell 是一个交互型的应用级程序,它代表用户运行其他程序。

处理流程:

1. 从终端读入命令

2. 切分字符串得到各种参数

3.判断是否为内置命令

3.1 若是,立即执行

3.2 若不是,调用相应的程序为其分配子进程并运行

4. 接受键盘输入信号,并对这些信号进行相应处理

6.3 Hello的fork进程创建过程

进程的创建:父进程通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时。子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程最大的区别在于他们有不同的id。fork后调用一次返回两次,在父进程中fork会返回子进程的PID,在子进程中fork会返回0;父进程与子进程是并发运行的独立进程。内核能够以任何方式交替执行他们逻辑控制流中的指令。

对于hello:

图6-1.进程创建

6.4 Hello的execve过程

创建进程后,在子进程中通过判断pid即fork()函数的返回值,判断处于子进程,则会通过execve函数在当前进程的上下文中加载并运行一个新程序,即 hello 程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置 PC 指向_start 地址,_start 最终调用 hello 中的 main 函数。execve 函数加载并运行可执行目标文件 filename, 且带参数列表 argv 和环境变量列表 envp 。只有当出现错误时,例如找不到 filename, execve 才会返回到调用程序。所以,与 fork 一次调用返回两次不同, execve 调用一次并从不返回。

下图为用户栈的典型结构。

图 6.4.1 用户栈的典型结构

6.5 Hello的进程执行

时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。

用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

逻辑控制流:使用调试器单步执行程序时,会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称逻辑流。即逻辑控制流是一个进程中PC值的序列。

上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。

上下文切换:如下图所示。

图6.5.1进程的上下文切换

当开始运行hello时,内存为hello分配时间片,如一个系统运行着多个进程,那么处理器的一个物理控制流就被分成了多个逻辑控制流,它们轮流使用处理器,并发执行。然后在用户态下执行并保存上下文。如果在此期间内发生了异常或系统中断,则内核会休眠该进程,并在核心态中进行上下文切换,控制将交付给其他进程。

当hello 执行到 sleep时,hello 会休眠,再次上下文切换,控制交付给其他进程,一段时间后再次上下文切换,恢复hello在休眠前的上下文信息,控制权回到 hello 继续执行。

接着hello在循环结束后,程序调用getchar(),hello又从用户模式进入内核模式,并再次上下文切换,控制交付给其他进程。最终,内核从其他进程回到 hello进程,在return后进程结束。

6.6 hello的异常与信号处理

6.6.1 正常运行

在进程中会依次输出八行Hello 2021110683 xky,且每一行之间休眠一秒在打印下一行。循环结束后让getchar()读入回车结束运行。

图 6.6.1 正常运行

6.6.2 不停乱按(不包括回车)

在程序执行过程中不断乱按,会将乱按的字符打印在屏幕上,对程序没有其它影响。循环结束后让getchar()读入回车结束运行。

图 6.6.2 不停乱按(不包括回车)

6.6.3 不停乱按(包括回车)

从下图看出,用户依次输入一个回车,sd,一个回车,sdf,而getchar()只会读走第一个回车,后面的字符就会先存在输入缓冲区中,等待程序运行结束后被终端读走,于是产生了以下的输出。

图 6.6.3 不停乱按(包括回车)

6.6.4 在程序中键入Ctrl-Z

可以看到,在程序运行过程中键入Ctrl+Z会让程序产生中断异常,向前台程序发送信号SIGSTP,这时hello会接收到信号SIGSTP并运行信号处理程序让hello进程暂时停止,并打印相关信息。

图 6.6.4 Ctrl+Z

在shell中输入ps命令,结果如下。在其中可以找到被挂起的hello程序。

图 6.6.5 Ctrl+Z后ps

在shell中输入jobs命令,结果如下。打印出被挂起的hello的相关信息。

图 6.6.6 Ctrl+Z后jobs

在shell中输入pstree命令,结果如下。

图 6.6.7 Ctrl+Z后pstree

在其中可以找到hello进程。

图 6.6.8 Ctrl+Z后pstree

在shell中输入fg命令,结果如下。可以发现,fg命令让被挂起的hello进程继续运行,在终端打印剩下未打印的七个Hello 2021110683 xky,接着getchar()读入回车结束进程。

图 6.6.9 Ctrl+Z后fg

先在shell中输入ps查看进程hello的PID,接着输入kill -9 4021来发送信号SIGKILL给进程5527,从而杀死该进程。接着再输入ps查看当前进程,发现hello进程已被杀死。

图 6.6.10 Ctrl+Z后kill

6.6.5 在程序中键入Ctrl-C

可以看到,在程序运行过程中键入Ctrl+C会让程序产生中断异常,向前台程序发送信号SIGINT。通过下图,看似和键入Ctrl+Z效果相同,其实不然。

图 6.6.11 Ctrl+C

接着我们输入ps查看当前进程,发现进程hello已经不存在了,说明hello进程在SIGINT信号的作用下强行被终止,并被父进程shell回收了。

图 6.6.12 Ctrl+C后ps

6.7本章小结

本章介绍了进程的概念和作用,简述了Shell-bash作用与处理流程,说明了hello的fork进程创建过程与execve过程,同时结合了进程上下文信息、进程时间片、用户态与核心态转换等,介绍了hello是如何在shell中作为一个子进程执行的。

最后,在异常与信号处理的分析过程中,我们使用ps、jobs、pstree、fg、kill等命令,测试了进程执行过程中按键盘出现的异常和产生的信号,深入理解了信号与异常的过程。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:逻辑地址是指由程序产生的与段相关的偏移地址部分,是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。

线性地址:地址空间是一个非负整数地址的有序集合,如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间,即hello里面的虚拟内存地址。

虚拟地址:这是对整个内存的抽象描述。它是相对于物理内存来讲的,可以直接理解成“不直实的”,“假的”内存,即hello里面的虚拟内存地址。

物理地址:计算机系统的主存被组织成一个由M 个连续的字节大小的单元组成的数组。每字节都有一个唯一的物理地址,即hello在运行时虚拟内存地址对应的物理地址。

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

一个逻辑地址由两部分组成,段标识符和段内偏移量。

图 7.2.1 段标识符

段标识符如上图所示。段标识符由 16 位字段组成。

索引:前 13 位为索引号,索引号是段描述符的索引,很多个描述符,组成了一个数组,叫做段描述表,可以通过段描述标识符的前 13 位,在这个表中找到一个具体的段描述符,这个描述符就描述了一个段,每个段描述符由八个字节组成。

TI如果 TI 是 0。「描述符表」是「全局描述符表(GDT)」,如果 TI 是 1。「描述符表」是「局部描述表(LDT)」

RPL段的级别。为 0,位于最高级别的内核态。为 11,位于最低级别的用户态。在 linux 中也仅有这两种级别。

Base字段:表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。

所以,只需要看段标识符的前13位,通过索引在段描述符表中找到对应的段描述符,从而可以得到Base字段,最后将开始位置的线性地址与段内偏移量相加,就能得到相应的线性地址。

以下是具体的转化步骤:

1. 给定一个完整的逻辑地址,其形式为[段选择符:段内偏移地址]

2. 看段选择符 T1,知道要转换的是 GDT 中的段还是 LDT 中的段,通过寄存器得到地址和大小。

3. 取段选择符中的 13 位,再数组中查找对应的段描述符,得到 BASE,就是基地址。

4. 线性地址等于基地址加地址偏移量。

对于偏移量,基址寄存器还是变址寄存器有不同的计算方法,后者需要经过乘比例因子等处理。

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

线性地址到物理地址的变换由页式管理实现,它通过分页机制对虚拟内存空间进行分页。计算机会利用页表,通过MMU来完成从虚拟地址到物理地址的转换。而页表是一个页表条目(PTE)的数组,存储在内存中,将虚拟页地址映射到物理页地址。

如下图所示,一个虚拟地址包含两个部分:一个虚拟页面偏移(VPO),其中VPO和PPO是相同的。以及一个虚拟页号(VPN)。MMU利用VPN选择适当的PTE,完成虚拟地址到物理地址的转变。如果命中,就能得到物理地址。如果不命中,会触发缺页故障,调用缺页处理子程序进行处理。

图 7.3.1 线性地址到物理地址的变换

线性地址到物理地址的变换的具体步骤为:

  1. 从CR3寄存器中取出进程页目录的基地址,再得到页目录项,然后由页目录项得到页表基地址。
  2. MMU以VPN为索引,选择适当的PTE。

3. 将页表条目中的物理页号和虚拟地址中的VPO串联起来,就能得到相应的物理地址。

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

TLBTranslation Lookaside Buffer),俗称快表,是MMU中的一个小的具有高相联度的集合,实现虚拟页码向物理页码的映射。

图 7.4.1 虚拟地址中用以访问TLB的组成部分

TLB通过虚拟地址VPN部分进行索引,分为索引(TLBI)与标记(TLBT)两个部分。这样,MMU在读取PTE时会直接通过TLB,如果不命中再从内存中将PTE复制到TLB。

图 7.4.2 多级页表

根据上图,我们可知MMU如何使用四级页表来将虚拟地址翻译成物理地址。CPU产生虚拟地址VA,虚拟地址VA传送给MMU,MMU使用VPN的高位作为TLBT和TLBI,去TLB中寻找匹配。如果命中,则可以直接得到物理地址PA。反之则去查询页表。36 位的 VPN 划分为 4 个 9 位的片,每个片对应一个页表的偏移量。从CR3确定第一级页表基地址,而一到三级页表中存放的数据是指向下一级页表的基地址,这样通过每一级的页表基地址以及分段的VPN就可以逐步访问到第四级页表,而第四级页表中装的才是物理页号,从四级页表读出的物理页号再与VPO组合即可得到物理地址PA。

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

图 7.5.1 组相联高速缓存

如上图,一般cache采用折中的方案,即组相联高速缓存的结构。组相联高速缓存每个组内可以多于一个缓存行,总体逻辑类似于直接映射高速缓存,不同之处在于行匹配时每组有更多的行可以尝试匹配,遍历每一行。如果不命中,有空行时也就是冷不命中则直接存储在空行;如果没有空行也就是冲突不命中,则替换已有行,通常有LFU(最不常使用)、LRU(最近最少使用)两者替换策略。

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

三级Cache支持下的物理内存访问的过程如上图。CPU 发出一个虚拟地址在 TLB 里搜索,如果命中,直接发送到 L1cache 里;如果没有命中,就现在加载到表里之后再发送过去。

当我们获得物理地址之后,先取出组索引对应位,在一级cache中寻找对应组。如果存在,则比较tag位,相等后检查valid是否为1。如果都满足则cache命中,取出值传给CPU,否则按顺序对二级cache和三级cache、内存进行相同操作,直到出现命中。然后再一级一级写回,如果有空位则直接写回,否则采用替换算法驱逐出一块后再写回。

7.6 hello进程fork时的内存映射

图7.6.1 fork进程

当 fork 函数被 shell 调用时,内核为 hello 进程创建各种数据结构,并分配给它一个唯一的 PID 。为了给 hello 进程创建虚拟内存,它创建了 hello 进程的 mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当 fork 在 hello 进程中返回时,hello 进程现在的虚拟内存刚好和调用 fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行包含可执行目标文件hello中的程序,加载、运行 hello 需要以下步骤:

1. 删除已存在的用户区域。删除 shell 虚拟地址的用户部分中的已存在的区域结构。

2. 映射私有区域。为 hello 的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text 和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello 中。栈和堆区域也是请求二进制零的,初始长度为零。

3. 映射共享区域。如果 hello 程序与共享对象(或目标)链接,比如标准 C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

4. 设置程序计数器(PC) 。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

经过这个内存映射的过程,在下一次调度hello进程时,就能够从hello的入口点开始执行了。

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

图 7.8.1 缺页

DRAM 缓存不命中称为缺页。上图为缺页之前我们的示例页表的状态。CPU 引用了 VP 3 中的一个字,VP 3 并未缓存在 DRAM 中。地址翻译硬件从内存中读取 PTE 3, 从有效位推断出 VP 3 未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在 PP3 中的 VP4 。如果 VP4 已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改 VP4 的页表条目,反映出 VP 4 不再缓存在主存中这一事实。

7.9本章小结

本章结合书上第六章和第九章的知识,主要介绍了存储器的地址空间,讲述了虚拟地址、物理地址、线性地址、逻辑地址的概念,以及逻辑地址到线性地址和线性地址到物理地址的变换。接着介绍了进程fork和execve时的内存映射,并详细描述了系统如何应对缺页异常,加深了我对虚拟内存的理解。

结论

hello的一生主要经过以下过程:

1. 程序员通过 I/O 设备在编译器中通过高级语言写下 hello.c,并存储在内存中;

2. 预处理器通过对头文件、注释的处理得到修改了的文本文件 hello.i。hello.i中头文件与注释以外的源代码保持不变,注释被被编译器删除,而代码量比hello.c大大增加;

3. 编译器翻译成汇编语言得到 hello.s。在这个过程中,高级语言被翻译为机器逻辑下的汇编语言;

4. 汇编器处理得到可重定位目标文件 hello.o。hello.o是一个二进制文件,可以查看它的ELF格式和相应的反汇编代码;

5. 链接器将 hello.o 和如 printf.o 的其他可重定位目标文件链接得到可执行目标文件 hello;

6.在 shell 里运行hello程序;

7. fork 创建子进程,shell 调用;

8. 运行程序,调用 execve。这时,hello就由程序(program)变成了一个进程(process);

9. 执行指令,为 hello 分配时间片,hello 执行自己的逻辑控制流;

10.三级cache访问内存,将虚拟地址映射成物理地址;

11. 信号、异常控制流,hello 对不同的信号会执行不同操作;

12. kill hello,回收子进程;

这一生,和人一样,从出生到入土为安,hello在计算机系统之中度过了精彩的一生。

理解与感悟:

纵观hello的一生,我发现计算机系统也是有生命的,这一生也充满了很多故事。这一生我们理解起来很艰难,但是当我们以实验的形式来拆解hello的一生,会发现这其中蕴含计算机系统的无限魅力。我们可以通过对hello一生的追溯来学习计算机对一个程序的处理过程,有助于我们以后写出更好的代码,并且在对 hello 一步步操作的学习中我们加深了对计算机系统的理解。

附件

hello.c          C语言源文件

hello.i          预处理产生的文件

hello.s          编译产生的汇编代码文件

hello.o         汇编产生的可重定位目标文件

hello.o.elf    hello.o文件的ELF格式

asm.txt        hello.o反汇编生成的文本文件

hello            链接产生的可执行目标文件

hello.elf       hello文件的ELF格式

asm_2.txt     hello反汇编生成的文本文件

参考文献

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

[2] 段页式访存——逻辑地址到线性地址的转换

https://www.jianshu.com/p/fd2611cc808e

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值