HIT-ICS2022大作业

 

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业   计算机科学与技术      

学     号     2021###004                  

班     级      21W0312                 

学       生           FYY       

指 导 教 师       史先俊                  

计算机科学与技术学院

2022年5月

摘  要

   本文将以hello程序为例,介绍程序从源代码到生成最终的可执行文件过程中经历的一系列步骤,包括预处理,编译,汇编,链接等过程。在可执行文件的执行的过程中,又会涉及到壳为其创建新的子进程,加载并且运行该程序的具体过程,以及异常,信号处理的相关问题,最后我们将讨论hello的存储管理问题。在这个过程中,借助于gcc,edb,objdump等工具,我们对于从源代码到程序运行的整个过程的讨论也不断深入,借助于对hello程序的分析,我对于现代计算机系统的理解也更为深入了。

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

目  录

第1章 概述............................................................................... - 5 -

1.1 Hello简介............................................................................. - 5 -

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

1.3 中间结果............................................................................... - 6-

1.4 本章小结.............................................................................. - 6 -

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

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

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

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

2.4 本章小结............................................................................ - 10 -

第3章 编译............................................................................. - 11 -

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

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

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

3.4 本章小结............................................................................ - 18 -

第4章 汇编............................................................................. - 19 -

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

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

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

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

4.5 本章小结............................................................................ - 26 -

第5章 链接............................................................................. - 27 -

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

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

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

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

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

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

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

5.8 本章小结............................................................................ - 37 -

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

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

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

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

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

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

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

6.7本章小结............................................................................. - 46 -

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

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

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

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

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

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

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

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

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

7.9动态存储分配管理............................................................. - 55 -

7.10本章小结........................................................................... - 58 -

结论........................................................................................... - 59 -

附件........................................................................................... - 60 -

参考文献................................................................................... - 61 -

第1章 概述

1.1 Hello简介

   程序的生命周期是从程序源文件经理预处理(由预处理器cpp完成),编译(由编译器cc1完成),汇编(由汇编器as完成),链接(由链接器ld完成)最终生成可执行文件并在操作系统上加载,执行,直到进程被回收的整个过程。程序的生命周期可以分为P2P和O2O两个部分,下面分别介绍一下这两个部分。

  1. hello的P2P(from program to process)过程

hello的P2P过程,即from program to process,即从源文件到进程的过程,这中间会经历预处理,编译,汇编,链接,以及shell调用fork函数创建新进程五个步骤。下图1-1-1展示了其中的前四个步骤:

     图1-1-1  对hello.c源文件预处理 编译 汇编 链接的过程

 

在预处理阶段,预处理器会执行文件包含,宏替换,布局控制等等处理过程形成hello.i文本文件,然后编译器会把hello.i中的文本转化成汇编语言,形成汇编语言源程序hello.s(依然是文本文件),在这个过程中编译器会依据指令进行一些保守的优化,接下来,汇编器会将汇编语言源程序转换为机器代码,生成可重定位目标文件hello.o文件(二进制文件),最后链接器会执行链接过程,生成可执行目标文件hello。

接下来,通过shell输入./hello,shell通过fork函数创建新的进程,并且把程序内容加载,实现由程序到进程的转化

  1. hello的O2O(from zero-0 to zero-0)过程:

hello的O2O过程是指程序被加载到内存执行,直到被回收的过程,程序运行前,shell调用execve函数将hello程序加载到相应的上下文中,将程序内容载入物理内存,然后,加载器会从_start的地址开始,来到main 函数的入口地址,之后运行main函数,程序运行结束后,父进程回收子进程,释放虚拟内存空间,删除相关内容,操作系统恢复shell的上下文,控制权重回shell,shell继续等待下一个命令行输入,这就是hello程序的O2O过程。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

硬件环境:

测试机型号为Swift SF514-55TA

处理器信息:11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz   2.42 GHz

如下图1-2-1为处理器详细信息:

   图1-2-1  处理器详细信息

 

高速缓存:

如图1-2-3为高速缓存详细信息:

 

图1-2-3   高速缓存详细信息

RAM:

如图1-2-4为RAM详细信息:

 

图1-2-4  RAM详细信息

软件环境:

本实验虚拟机用来完成程序的调试与测试工作,实体机用于完成论文,两者具体信息如下表:

虚拟机

实体机

操作系统名称

Ubuntu 20.04.2 LTS

Windows 11 家庭中文版

操作系统类型

64位

64位

内部版本

3.36.8

22000.1219

调试工具:

CPU-Z:用于查看实体机的硬件环境

edb :用来查看hello的虚拟内存映射,反汇编代码

gcc :用来编译hello文件(这里的编译是指程序从源文件到可执行目标文件的过程)

objdump:一组反汇编命令,用于查看hello的各节内容

readelf:查看ELF文件格式

1.3 中间结果

hello.c:C源文件

hello.i:预处理后生成的文本文件,可以看出预处理器的一部分功能

hello.s:汇编语言源程序,用于分析编译器执行的功能

hello.o:可重定位目标文件,用于分析汇编器功能

hello_Elf.txt: readelf读取的hello.o的elf文件内容,用来研究hello.o的ELF文件结构

hello_back.txt:objdump生成的hello.o的反汇编文件,用于比较汇编文件hello.s的异同,还用来比较与hello的反汇编文件hello_out_text_Elf.txt的异同

hello_out_Elf.txt:readelf生成的hello(可执行目标文件)的elf文件内容,用来和hello.o(可重定位目标文件)的elf结构文件作比较

hello_out_text_Elf.txt:objdump生成的hello的反汇编文件

1.4 本章小结

本章介绍了hello程序的生命周期(包括其P2P以及O2O过程),介绍了本次大作业任务所用到的软硬件环境以及各种调试工具,还有本次实验的中间结果等等,后面的章节会对hello程序生命周期各个阶段展开具体详细的分析。

第2章 预处理

2.1 预处理的概念与作用

预处理概念:预处理由预处理程序负责完成,预处理器(cpp)根据以字符“#”开头的命令,修改原始的C程序,C语言提供了多种预处理功能,如宏定义,文件包含,条件编译等等。

预处理作用:

(1)文件包含:#include是一种最为常见的预处理,比如hello.c中“#include<stdio.h>”命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。

(2) 宏替换:#define,它具有很多功能与用法,例如定义符号常量,对符号重新命名,定义函数等等

(3)布局控制:#progma,主要功能是为编译程序提供非常规的控制流信息。

(4)条件编译:#if,#ifndef,#ifdef,#endif,#undef等主要是在预处理时进行有条件的挑选,防止对文件重复包含。

2.2在Ubuntu下预处理的命令

方法1:通过gcc –E选项来进行预处理,-o选项规定了预处理命令输出文件的名字,图2-1展示了方法1预处理命令以及结果。

 

                 图2-1    方法1预处理命令以及结果

方法2:调用预处理器命令cpp进行预处理,如下图2-2展示了方法2预处理命令及结果。

 

                 图2-2    方法2预处理命令以及结果

2.3 Hello的预处理结果解析

 

 

       图2-3    方法1预处理得到的hello.i(部分)

 

                    图2-4 方法2预处理得到的hello.i(部分)

可以发现,两种方法得到的预处理结果完全相同,而且hello.i相比于hello.c长度大大增加,hello.i有3091行(如图所示),stdio.h,unistd.h,stdlib.h,三个头文件以及被复制到hello.i之中,注释完全消失,最后一部分的main函数(如图)与hello.c中相同,并且hello.i仍旧为可读的文本文件。

2.4 本章小结

本章介绍了预处理的概念与作用,并且以hello.c为实例展示了预处理的两种命令以及其输出结果(hello.i)文件,这个预处理过程展示了预处理的其中一项功能——文件包含,且hello.i仍是可读的文本文件。下一步将由编译器对hello.i文件进行处理生成汇编文件。

第3章 编译

3.1 编译的概念与作用

编译的概念:编译器(cc1)将文本文件hello.i翻译为文本文件hello.s,它通常包含一个汇编语言程序。

编译的作用:(1)由于汇编语言更符合机器行为,因此将C代码经过编译过程转化为汇编语言源程序有助于下一步将汇编文件经过汇编过程进一步转化为机器语言。(2)在编译的过程中,编译器可以根据指示对源代码进行适当程度的优化(可选的优化等级包括-Og,-O1,-O2,-O3等等)。       

3.2 在Ubuntu下编译的命令

通过gcc –S选项来进行预处理,-o选项规定了编译命令输出文件的名字,图3-1展示了编译命令以及结果。

 

             图3-1     编译命令以及结果

3.3 Hello的编译结果解析

3.3.1数据操作

(1)局部变量:

  程序中定义并使用了一个局部变量,作为循环计数器使用,即int i

  下面图3-3-1是i赋初值操作的汇编代码与对应的源程序

 

 

 

    图3-3-1   i赋初值操作的汇编代码与对应的源程序

下面图3-3-2对应于i自增以及与边界值进行比较的汇编代码与源程序:

 

 

    图3-3-2   i自增以及与边界值进行比较的汇编代码与源程序 

这两幅图说明局部变量i在栈内被分配了空间,其处在-4(%rbp)的位置

(2)字符串常量

此程序中存在两个字符串常量,分别为“用法: Hello 学号 姓名 秒数!\n”与“Hello %s %s\n”,都是作为printf函数的输出格式串使用的,下面图3-3-3是他们在汇编文件中对应的部分:

图3-3-3    字符串常量在汇编文件中对应的部分

可以观察到字符串常量位于.rodata节,将来链接时会被映射到只读代码段,所以该部分程序只具有读权限,不具有写权限,并且字符串常量使用的是utf-8编码方式。.align  8 表明该节的对齐方式是8字节对齐。下面图3-3-4说明调用字符串常量时使用的是PC相对寻址方式:

 

 

 

         图3-3-4   可以看出调用字符串常量时使用的是PC相对寻址方式

   (3)整型常量

         整形常量以立即数形式存放在代码部分,下面图3-3-4是三个例子:

    

 

 

 

图3-3-4  代码中的立即数(示例)

   (4)main函数参数:

main函数参数主要有:①整型argc(记录字符型数组指针*argv[]数组的元素个数)

                    ② 字符型指针数组首地址argv

下面先解析一下argc:

在C源代码中的位置如右图

 

                            图3-3-5   C代码中引用argc的位置

对应的在汇编程序中的位置:

 

                            图3-3-6   上述C语句对应的汇编代码

据此可以看出,argc存放在栈内 -20(%rbp)的位置。

接下来解析一下argv:图3-3-7展示了C代码中引用argv的位置:

    

 

                 图3-3-7     C代码中引用argv的位置

(以下解析部分较为详细,不仅是针对argv的解析,同时也是对于数组操作,函数调用等等过程的解析,由于这几个要素联系紧密,所以放在一起进行了一个详细的解析如下)

下面图3-3-8是这段C代码中printf函数调用过程对应的汇编程序部分:

 

              图3-3-8     printf函数调用过程对应的汇编程序部分

   第35行将字符型指针数组首地址argv的值放入寄存器%rax之中,第36行将该地址值加16,再将更新后的地址值重新放入%rax中,因此此时%rax中存放的是argv[2]的地址,第37行取出& argv[2]处的值,即argv[2],并将argv[2]放入寄存器%rdx之中,作为调用函数printf的第三个参数,第38行重复35行的操作,第39行将该地址值加8,再将更新后的地址值重新放入%rax中,因此此时%rax中存放的是argv[1]的地址,第40-41行取出& argv[1]处的值,即argv[1],并将argv[1]放入寄存器%rsi之中,作为调用函数printf的第二个参数,第42-43行通过PC相对寻址,将输出格式字符串首地址放入寄存器%rdi之中,作为调用函数printf的第一个参数,第45行调用printf函数。

下面图3-3-9是调用atoi函数的汇编代码:

 

               图3-3-9   调用atoi函数的汇编代码

   第46行将字符型指针数组首地址argv的值放入寄存器%rax之中,第47行将该地址值加24,再将更新后的地址值重新放入%rax中,因此此时%rax中存放的是argv[3]的地址,第48-49行取出& argv[3]处的值,即argv[3],并将argv[3]放入寄存器%rdi之中,作为调用函数atoi的第一个参数(也是唯一一个参数)。第50行调用atoi函数。

下面图3-3-10是调用sleep函数的汇编代码:

             

 

           图3-3-10   调用sleep函数的汇编代码

如图,第51行将上述atoi函数的返回值放入寄存器%edi之中,作为调用函数sleep的参数,然后在第52行调用sleep函数。

至此,完成了对main函数参数的解析过程

3.3.2赋值操作:

  C代码中的赋值语句如图3-3-11:

                  

 

                 图3-3-11      C代码中的赋值语句

对应的汇编代码部分如图3-3-12:

              

 

               图3-3-12   赋值语句对应的汇编代码部分

可以看出,在汇编语言中,赋值语句是通过mov指令完成的

3.3.3数组操作:

Hello程序中对于数组的操作集中在对于argv数组进行的操作,具体如下:

以下图3-3-13是数组操作对应的C程序的部分:

            

 

              图3-3-13   数组操作对应的C程序的部分

以下图3-3-14是这一部分对应的汇编程序的部分:

            

 

        图3-3-14  数组操作对应的汇编程序的部分

这一部分的汇编代码的解析在对argv的解析过程中以及具体说明过,因此不再赘述,需要另外注意的是从这段汇编代码中我们可以看出汇编程序对于数组的访问方式为“基址+偏移量”,而该偏移量对应于C程序中的数组索引,这种方法我们称为比例变址寻址(法)。

3.3.4算术操作:

程序中只有一处进行了算术操作,即for循环的i自增运算,其对应的C代码部分和汇编代码部分分别如图3-3-15与图3-3-16所示:

                     

 

                   图3-3-15  i自增运算对应的C代码

 

                   图3-3-16  i自增运算对应的汇编代码

此处i自增运算编译器使用add指令实现,其实也可以通过inc指令实现自增操作。

3.3.5控制转移与关系操作:

本部分将分别从if语句和for循环语句的角度介绍关系操作以及建立在关系操作基础上的控制转移:

  1. if语句

hello程序中if 语句用于检测传入参数的个数是否符合要求,如果不符合要求会输出提示符并且直接终止程序,符合要求才会进行下一步循环与打印的过程,对应的C程序如下图3-3-17所示:

 

图3-3-17  if语句对应的C程序部分

可以观察到,这个if语句的特点是只有if分支而没有else分支,我们再来看这个语句翻译成的汇编程序:

            

 

 

图3-3-18  if语句对应的汇编程序

可以看出在汇编代码中,先比较了argc与4的大小关系,如果二者相等,则直接跳转到L2处继续执行,如果二者不等,那么先调用puts函数打印提示字符串,再调用exit函数退出,下面的伪代码可以较好的体现汇编代码的思想:

               

 

             图3-3-19  说明汇编代码思想的伪代码

  1. for循环语句

hello程序中for循环语句用来循环间隔某一固定时间并且打印相同的输出内容9次,下面图3-3-20是其对应的C程序部分:

           

 

               图3-3-20   for循环对应的C程序部分

下面图3-3-21是其对应的汇编程序部分:

 

图3-3-21   for循环对应的汇编程序部分

   可以看出,在L2部分第32行代码对i(存放在-4(%rbp)处)进行了初始化操作,为其赋初值0,然后跳转到L3处,将i与8进行比较,如果i£8,那么再跳转到L4处,并且进行接下来调用printf函数以及atoi函数和sleep函数的操作,因此该汇编程序使用的是guarded-do型控制流(在循环开始前先进行条件检查,如果不符合则直接退出循环),关于L4中函数调用的汇编代码解析将在函数调用部分详细说明。到第52行sleep函数调用结束后,53行汇编代码实现i自增操作,然后再接着执行55行比较操作,如果依然满足i£8,那么再跳转到L4处,如此循环往复直至不再满足i£8,则顺序执行57行(没有截在图中)。

  1. 关系操作:

Hello程序中有两处涉及到关系操作,对应的C程序与汇编程序如图3-3-21:

 

 

 

 

       

图3-3-21  关系操作对应的C程序以及汇编程序

 

          图3-3-22     关系操作与跳转指令的关系

如图,关系操作会设置条件码,而跳转指令会根据相应的条件码确定是否进行跳转操作(具体如上图3-3-22)

3.3.5函数操作

Main函数调用前面设计argc 与argv的解析过程以及详细叙述过,在此不再赘述

(1)printf的调用

下面图3-3-23是这段C代码中printf函数调用过程对应的汇编程序部分:

 

              图3-3-23     printf函数调用过程对应的汇编程序部分

   第35行将字符型指针数组首地址argv的值放入寄存器%rax之中,第36行将该地址值加16,再将更新后的地址值重新放入%rax中,因此此时%rax中存放的是argv[2]的地址,第37行取出& argv[2]处的值,即argv[2],并将argv[2]放入寄存器%rdx之中,作为调用函数printf的第三个参数,第38行重复35行的操作,第39行将该地址值加8,再将更新后的地址值重新放入%rax中,因此此时%rax中存放的是argv[1]的地址,第40-41行取出& argv[1]处的值,即argv[1],并将argv[1]放入寄存器%rsi之中,作为调用函数printf的第二个参数,第42-43行通过PC相对寻址,将输出格式字符串首地址放入寄存器%rdi之中,作为调用函数printf的第一个参数,第45行调用printf函数。

(2)atoi函数的调用与返回

下面图3-3-24是调用atoi函数的汇编代码:

 

               图3-3-24  调用atoi函数的汇编代码

   第46行将字符型指针数组首地址argv的值放入寄存器%rax之中,第47行将该地址值加24,再将更新后的地址值重新放入%rax中,因此此时%rax中存放的是argv[3]的地址,第48-49行取出& argv[3]处的值,即argv[3],并将argv[3]放入寄存器%rdi之中,作为调用函数atoi的第一个参数(也是唯一一个参数)。第50行调用atoi函数。返回值将保存在寄存器%eax之中。

   (3)sleep函数的调用

下面图3-3-25是调用sleep函数的汇编代码:

             

 

           图3-3-25   调用sleep函数的汇编代码

如图,第51行将上述atoi函数的返回值放入寄存器%edi之中,作为调用函数sleep的参数,然后在第52行调用sleep函数。

(4)getchar函数的调用

下面图3-3-26展示了getchar()函数的调用:

            

 

           图3-3-26   getchar()函数的调用

在函数调用过程中,参数个数小于等于6个的部分,会依次使用寄存器%rdi,%rsi,%rdx,%rcx,%r8,%r9来传递,而多于6个的参数会使用栈来传递,函数的返回值会存放在寄存器%rax之中。

3.4 本章小结

编译过程将文本文件hello.i转化为汇编语言源程序hello.s(仍然是可读的文本文件),为下一步将汇编文件经过汇编过程进一步转化为机器语言做好准备,因为汇编语言比起C源程序具有与机器代码更严格的对应关系,其处理思路也更为贴近真实的机器处理过程,同时,在编译的过程中,编译器会根据指示对源代码进行适当程度的优化。

第4章 汇编

4.1 汇编的概念与作用

汇编的概念:
汇编指的是汇编器(as)将编译过程生成的汇编语言源程序(.s文件)依照汇编指令编码为可重定位目标文件(二进制文件)(.o文件)的过程,即将汇编语言翻译为二进制机器代码的过程。

汇编的作用:

将汇编语言源程序转化为机器指令,为下一步链接生成可执行文件做好准备,汇编过程也将生成可重定位目标文件的结构信息。

4.2 在Ubuntu下汇编的命令

可以通过as指令或者gcc的-c选项来执行汇编操作,如图4-2-1所示

 

 

         图4-2-1  执行汇编操作的两种指令及生成的可重定位目标文件

4.3 可重定位目标elf格式

Linux系统使用可执行可链接格式(ELF)对可重定位目标文件进行组织,具体结构如下图4-3-1所示:

                    

 

                  图4-3-1  ELF可重定位目标文件的结构

其中各部分具体说明如下:

.text:已编译程序的机器代码

.rodata:只读数据

.data:已初始化的全局和静态C变量

.bss:未初始化的全局和静态C变量

.symtab:符号表

.rel.text:代码段重定位信息表

.rel.data:数据段重定位信息表

.debug:调试符号表

.line:C代码行号与机器码行号映射表

.strtab:字符串表

在终端输入命令readelf –a hello.o > hello_Elf.txt,则hello_Elf.txt即为hello.o的ELF文件格式的内容,下面将分别介绍该文件的主要内容:

4.3.1 ELF头

ELF头部分如图4-3-2所示:

 

           图4-3-2   hello_Elf.txt中ELF头的部分

如上图,第二行处有一个16字节的魔数(magic)序列,其中0x45 0x4c 0x46分别是英文字母E F L的ASCII编码,第4行data项说明该文件中的数据都是采用补码,小端方式存储,第8行说明了目标文件的格式(目标文件有三种,可重定位目标文件/可执行目标文件/共享库目标文件),而图中写的是REL(Relocatable file)对应的是可重定位目标文件,第11行程序入口地址,由于该文件是可重定位目标文件,所以该地址是0,经过链接过程将该文件重定位后生成的可执行文件中,这个地址才有意义,第13行是节头表的偏移量(即节头表的起始地址),第15行是这个ELF头的大小,可以看出是64字节,第18行是节头表每一项的大小,可以看出是64字节,第19行是节头表项数,可以看出节头表有14项,由于该文件是可连接视图而非可执行视图,不涉及具体的装入内存空间的过程,所以不涉及反映段与虚存映射关系的程序头表,即该文件没有程序头表。

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

4.3.2节头部表:

节头部表部分如图4-3-3所示:

                 

 

                图4-3-3   hello_Elf.txt文件的节头部表

节头部表中有每一个节的名称,大小(size),类型,虚拟内存地址(Address),访问权限(Flags),是否链接(Link),偏移量(offset),对齐方式(align)等信息,第53行到第57行解释了各符号的含义。

下面以.data节为例具体说明,由于hello.c中没有涉及到全局变量,所以.data节的大小size为0,虚拟内存地址为0x0000000000000000,这是因为hello.o是一个可重定位目标文件,还没有经过链接重定位过程,所以每一个节的虚拟内存起始地址(Address)都是0,而Flags位被标记为WA(可写可分配),偏移offset为0x000000d8,即十进制的216。

4.3.3  .symtab节:

.symtab节在hello_Elf.txt文件中的部分如下图4-3-4所示:

 

图4-3-4    .symtab节

符号表是由汇编器构造的,使用编译器输出到汇编语言.s文件中的符号。.symtab节中包含ELF符号表。这张符号表包含一个条目的数组。如图4-3-5展示了每个条目的格式:

 

图4-3-5      每个条目的格式

具体说明如下:

name:字符串表中的字节偏移,指向符号的以null结尾的字符串名字。

value:符号的地址。对于可重定位模块来说,value是距定义目标的节的起始位置的偏移。对于可执行目标文件来说。该值是一个绝对运行时地址。

size: 目标的大小(以字节为单位)

type: 一般情况下,要么是函数(function),要么是数据(data)

binding:表示符号是本地的还是全局的

section: 表示对应符号被分配到目标文件的某个节,该字段也是一个到节头部表的索引,有三个特殊的伪节,他们在节头部表中没有条目,分别是ABS(代表不该被重定位的符号)UNDEF(代表未定义的符号,也就是在本目标模块中引用,但在其他地方定义的符号),COMMON(表示还未被分配位置的未初始化的数据目标),对于COMMON符号,value字段给出对齐要求,而size字段给出最小的大小。

例如全局符号main大小为152字节,类型为函数(Type=Func),属于全局变量(global),定义在索引为1的节中,即.text节之中

4.3.4 .rela.text节

.rela.text节如下图4-3-6所示

 

                图4-3-6  .rela.text节

ELF重定位条目的格式如下图4-3-7所示:

 

                   图4-3-7   ELF重定位条目的格式

Offset:需要被修改的引用的节偏移

Symbol:标识被修改引用应该指向的符号

Type:告知链接器如何修改新的引用。ELF定义了32种不同的重定位类型,其中R_X86_64_PLT32和R_X86_64_PC32都代表32位PC相对地址的引用,而R_X86_64_32表示使用的是32位绝对地址的引用[1].

Info:高24位为所引用的符号索引,低8位为对应的是重定位类型

Addend:一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。

下面以.rela.text段第一个条目为例具体说明一下重定位条目各部分的意义:

首先使用命令objdump –d –r hello.o > hello_back.txt得到.text段的反汇编版本

下面图4-3-8对应的是.rela.text段第一个条目(即偏移offset为0x1c处的引用)对应的引用位置的反汇编语句:

 

图4-3-8     .rela.text段第一个条目对应的引用位置的反汇编语句

分析该结构可知,该条目使用的是PC相对寻址方式,对应的符号是printf的输出格式串“Hello %s  %s”,并且该符号定义在.rodata段,于是继续用命令objdump –s hello.o > hello_back_full.txt,得到.rodata段的反汇编内容,如图4-3-9所示:

 

图4-3-9 .rodata段的反汇编内容

可以看出printf输出格式串“Hello %s  %s”确实在.rodata段中

4.4 Hello.o的结果解析

使用命令objdump -d -r hello.o > hello_back.txt,得到hello.o文件的反汇编版本

如图4-4-1所示:

 

     图4-4-1   hello.o的反汇编的指令与反汇编文件hello_back.txt

4.4.1 汇编版本与反汇编版本的对比分析:

下图4-4-2对比了汇编版本hello.s(左边)中的内容与反汇编版本hello_back.txt(右边)中的内容:

 

 

图4-4-2 对比汇编版本hello.s(上)与反汇编版本hello_back.txt(下)

观察上图可以发现:

  1. 在汇编版本中只有汇编语句,而在反汇编版本中,在反汇编语句的左边还有对应的机器代码(以十六进制形式表示)。
  2. 在汇编版本中,操作数都是以十进制形式给出(见左图红色标注),而在反汇编版本之中,操作数以十六进制形式给出(见右图红色标注)
  3. 在汇编版本中,分支转移指令都是以助记符形式表示的(如左图黄色标注),而在反汇编版本之中,分支转移指令都是以相对地址形式表示的(如右图黄色标注),这主要是因为汇编代码还未对代码段分配相应的运行时内存,因此只能用助记符来表示分支转移的目的
  4. 在汇编版本中,函数调用指令都是直接借助函数名完成的(如左图蓝色标注),而在反汇编版本之中,函数调用指令call后面跟着的是下一条指令的地址,借助于下一条指令的地址与PC相对寻址方式找到被调用函数的入口,从而执行函数调用,这是因为反汇编代码已经完成了符号解析,函数名被记录在符号表.symtab中,且其相应的引用重定位信息都存在.rela.text之中(这一部分前面已经解析过),链接过程会根据分配的虚拟内存位置以及重定位条目的提示信息将这些引用的重定位信息补充完成,所以反汇编版本不必使用函数名,而可以借助PC相对寻址方式找到被调函数的入口地址,执行函数调用过程。
      1. 汇编代码与机器代码的映射:

本文只说明Y86-64机器中汇编代码与机器代码的映射关系(如图4-4-3所示):

 

图4-4-3  Y86-64机器中汇编代码与机器代码的映射关系

如图所示,每条指令需要1-10个字节不等,这取决于需要哪些字段。每条指令的第一个字节表明指令的类型,这个字节分为两个部分,每部分4位,高4位为代码(code)部分,低4位为功能(function)部分,如图,代码值的范围是0-0xB。功能值只有在一组相关指令共用一个代码时才有用(指明是整数操作(OPq),数据传送条件(cmovXX)或是分支条件(jXX)),有的指令有附加的寄存器指示符字节,指定一个或两个寄存器(当只需要一个寄存器操作数时,另一个寄存器指示符设为OxF),此外,有的指令还需要一个八字节常数。

 X86-64指令编码比上述简化版本更为紧凑,它允许在不同的指令中寄存器字段的位置不同,而且x86-64可以将常数值编码为1,2,4或者8个字节。

4.5 本章小结

本章首先介绍了汇编的概念与作用,然后演示了进行汇编过程的两种命令行输入,然后详细介绍了汇编生成的可重定位目标文件的各个具体的组成部分,接着通过objdump反汇编工具得到了hello.o文件的反汇编版本hello_back.txt,并且比较了汇编版本hello.s与反汇编版本hello_back.txt的差异,最后介绍了机器指令与汇编指令的映射。

经过汇编过程生成的可重定位目标文件还需要经过进一步的链接过程生成可执行目标文件才可以装入内存执行。

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

链接结果如图5-2-1所示:

 

图5-2-1   链接命令及生成的可执行目标文件hello如图

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

使用终端命令readelf –a hello > hello_out_Elf.txt,那么在文件hello_out_Elf.txt中即可得到可执行文件hello的ELF文件格式的输出,如下图5-3-1所示:

 

图5-3-1  使用readelf指令得到可执行文件hello的ELF文件格式的输出

如图5-3-2为一个典型的ELF可执行目标文件的结构:

 

图5-3-2     一个典型的ELF可执行目标文件的结构

可以看到从结构上来讲,ELF可执行目标文件比ELF可链接目标文件多了一个段头部表,也成为程序头表(program headers),程序头表是反映可执行文件的连续的片与连续的内存段之间的映射关系,此外,在ELF可执行目标文件中有一个.init节,这个节用来完成可执行目标文件开始执行时的初始化工作,另外,由于可执行目标文件以及完成了重定位,所以其中不再包含两个.rel节。

下面将对比hello.o的ELF文件结构介绍hello的ELF文件结构:

5.3.1  ELF头

如图5-3-3所示即为ELF头:

图5-3-3   ELF头

对比可以观察到,第8行type项是EXEC(executable file),这与hello.o文件ELF文件的ELF头中这一项是不同的,此时文件类型已经变成了可执行目标文件。第11行entry point address也有了具体的内存地址,而不再是0,因为此时文件已经被重定位到虚拟内存中,所以程序有了具体的入口地址,第12,16,17行(分别是程序头表的偏移,程序头表的每一项的大小,程序头表的项数)关于程序头表program headers的项都不再是0,因为ELF可执行目标文件有程序头表,第13行节头表的偏移也改变了,此外,第19行节头表的项数也变为30项

5.3.2节头部表

如图5-3-4即为节头部表部分:

 

 

                        图5-3-4  节头表

可以发现,节头表中很多节的Address项都不再为零,而是有了具体的内容,这是因为所有要在执行时加载进内存的节都被分配了具体的虚拟内存地址。

相比于hello.o文件,hello的ELF文件中多了几个节,他们按照功能可以分为如下三类:

①与加载相关:.init节与.fini节

②与动态链接相关:.dynsym节 .dynstr节 .rela.dyn节 .rela.plt节 .plt.plt节 .sec.fini节 .eh_frame节 .dynamic节 .got节 .got.plt节

③与编译器的调试功能相关:.note.gnu.propert节 .note.ABI-tag节 .hash节 .gnu.hash节 .gnu.version_r节 .gnu.version节

5.3.3 程序头表

如下图5-3-5所示即为程序头表:

 

                  图5-3-5   程序头表部分

可以看到该程序头表中有12个表项,其中有四个可装入段(Type=LOAD),offset代表各段的偏移(起始地址),virtaddr与physaddr分别代表各段的虚拟地址与物理地址,二者相等,filesize与memsize不一定相等,因为.bss节在ELF文件中不占实际空间,只是一个占位符,但是.bss节在装入内存空间时需要虚拟内存为其分配空间,flags表示的是每个段的访问权限,align代表各段的对齐方式,由图可以看出,四个可装入段都是4KB字节对齐。

以第一个可装入段为例,其在ELF文件中对应的是0x0-0x5f0个字节,而在内存中对应的是从0x400000开始的0x5f0个字节,访问权限为R,即为只读,而对齐方式为0x1000,即为4KB字节对齐,因此该段实际就是只读代码段

5.3.4 符号表.symtab节

如下图5-3-6即为.symtab节

     

 

                        图5-3-6   .symtab节

可以发现,由于加入了系统调用以及与调试,加载,动态链接相关的节,因此该符号表比hello.o的ELF文件格式的符号表表项要多出许多。

  

5.4 hello的虚拟地址空间

首先,使用edb调试hello,可以看到,hello的虚拟内存地址空间是从0x400000开始,到0x404ff0结束,如下图5-4-1所示,这段地址空间内存储着hello的全部信息。   

 

 

               图5-4-1   hello的地址空间

 

 

               图5-4-2   .interp节的节头部表

根据图5-4-2可知,.interp节在内存中的位置起始于0x4002e0,长度为0x1c

下图5-4-3使用edb工具查看了这个内存位置

               图5-4-3   .interp节对应的内存位置 

 

 

               图5-4-4   .text节的节头部表

根据图5-4-4可知,.text节在内存中的位置起始于0x4010f0,长度为0x17e

下图5-4-5使用edb工具查看了这个内存位置

 

图5-4-5   .text节对应的内存位置 

 

                 图5-4-6   .rodata节的节头表

根据图5-4-6可知,.rodata节在内存中的位置起始于0x402000,长度为0x3b

下图5-4-7使用edb工具查看了这个内存位置

 

     图5-4-7   .rodata节对应的内存位置 

如图5-4-7,在.rodata节对应的内存位置中可以看到两个printf函数的输出格式串

5.5 链接的重定位过程分析

objdump -d -r hello > hello_out_text_Elf.txt对hello进行反汇编,结果如下图5-5-1:

 

 

         图5-5-1  hello的反汇编结果

下面具体说明重定位过程以及hello.o的反汇编结果与hello的反汇编结果的不同之处:

重定位过程分为两步:

  1. 重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的聚合节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了
  2. 重定位节中的符号引用。在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。

下图5-5-2为重定位算法:

 

图5-5-2  重定位算法

下面结合hello程序中的一条语句具体说明一下程序的重定位过程以及hello.o反汇编版本与hello的反汇编版本的不同之处:

对应的C语句如图5-5-3:

 

         图5-5-3  即将说明的C语句

如图5-5-4对应这个语句的可重定位目标文件的汇编代码:

 

           图5-5-4  对应上述语句的hello.o文件的汇编代码

    

如图5-5-5对应这个语句的可执行目标文件的汇编代码:

 

           图5-5-5  对应上述语句的hello文件的汇编代码

如图5-5-6对应的是在这个重定位过程中要用到的重定位条目:

 

           图5-5-6  在这个重定位过程中要用到的重定位条目

如图5-5-7对应的是部分符号表信息:

 

图5-5-7   部分符号表信息(用红色标注出来的即将会用到)

首先,对比图5-5-4与图5-5-5可以发现,可重定位目标文件中,对应lea与call指令的机器代码都只有指令的代码部分与功能部分,与地址的相关信息都设为0等待重定位时候结合重定位条目补充,而在图5-5-5中经过重定位之后,这个相对地址能够被确定下来,因此填在了原来为0的位置处。

上述第一个重定位条目对应的偏移为0x1c,结合hello.o的汇编语句,可以发现其对应的符号引用位置在lea语句的位置,这个符号引用重定位方式为PC相对地址引用(type=R_X86_64_PC32),结合info字段可以看出这个引用捆绑的符号是第3个符号,再结合符号表中的第3个符号可以发现这个符号定义在.rodata节,而且重定位条目告诉我们这个符号在.rodata节的起始位置,那么可以知道这个符号为第一个printf输出格式串。结合hello的反汇编文件,可以发现,执行到lea语句时,程序计数器中的值为下一条语句的首地址0x4011f6,而相对偏移为0x0e12(机器代码中采用小端法存储),二者相加等于0x402008,和图5-5-5中所显示的目标地址是吻合的。

下面说一下函数puts的重定位过程,结合重定位条目可以知道这个引用的重定位方式也是PC相对引用(type= R_X86_64_PLT32),结合hello的反汇编文件,可知执行到call语句时,程序计数器中保存的是下一条指令的起始地址,0x4011fe,而相对偏移(即指令代码e8后面跟着的部分)为0xfffffe92(机器代码采用小端法存储),由于这是一个补码,其对应的数字其实是0x-16e,将0x4011fe与0x-16e相加,结果时0x401090,与图5-5-5中所显示的目标地址也是吻合的。

5.6 hello的执行流程

如下表所示:

子程序名

子程序地址

      hello!_start

0x4010f0

      hello!_init

0x401000

      hello!frame_dummy

0x4011d0

      hello!register_tm_clones

0x401160

      hello!main

0x4011d6

      hello!puts@plt

0x401090

      hello!exit@plt

0x4010d0

      hello!printf@plt

0x4010a0

      hello!atoi@plt

0x4010c0

      hello!sleep@plt

0x4010e0

      hello!getchar@plt

0x4010b0

      hello!exit@plt

0x4010d0

    hello!___do_global_dtors_aux

0x4011a0

    hello!deregister_tm_clones

0x401130

       hello!_fini

0x401270

5.7 Hello的动态链接分析

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。

在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定,将过程地址的绑定推迟到第一次调用该过程时。

延迟绑定是通过全局偏移量表(GOT)和过程连接表(PLT)实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:

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

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

如下图5-7-1所示,根据hello 可执行目标文件可知,GOT运行时地址为0x403ff0,PLT的运行时地址为0x404000。

 

             图5-7-1    查看.got以及.plt地址

程序调用dl_init前,使用edb查看地址0x404000处的内容,如下图5-7-2所示:

 

     图5-7-2   程序调用dl_init前地址0x404000处的内容

程序调用dl_init后,使用edb查看地址0x404000处的内容,如下图5-7-3所示:

 

      图5-7-3   程序调用dl_init后地址0x404000处的内容

GOT表的内容在调用_start之后发生改变,0x404008后的两个8个字节分别变为:0x7ff7ba9612e0、0x7ff7ba93bd30,其中GOT[0]和GOT[1]z在动态链接器在解析函数地址时会用到。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,改变后的GOT表如图5-7-3所示,GOT[2]对应部分是共享库模块的入口点。

5.8 本章小结

本章首先介绍了链接的概念与链接的作用,以及在乌班图下链接的命令,然后对比分析了可执行目标文件hello(可执行目标文件)的ELF文件格式与hello.o(可重定位目标文件)的ELF文件格式的不同之处,然后介绍了hello的虚拟地址空间,分析了其链接的重定位过程并且举了例子,另外还列表分析了hello的执行流程,并分析了其动态链接过程。

在后面的章节中,将会继续分析hello程序执行过程的细节。

6章 hello进程管理

6.1 进程的概念与作用

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

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

进程的作用:

为用户提供一种对处理器、内存、I/O设备的抽象,并向应用程序提供以下关键抽象:

1.一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。

2.一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。


 

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

Shell的作用:shell是一个交互型应用级程序,也是一个命令行解释器,是一个以用户态方式运行的终端进程,代表用户运行其他的程序

Shell的处理流程:(1)读取用户通过键盘输入的命令行

                 (2)切分命令行字符串,构造argc字符串指针数组

                 (3)检查命令行参数是否为内置的命令,如果是,立即执行并返回

                 (4)如果不是shell的内置命令,调用fork函数创建新的子进程

                 (5)在新创建的子进程中,调用execve函数加载并执行程序

                 (6)如果用户输入的命令行末尾为&,则该程序应该在后台运行

                  (7)如果用户没有在命令行末尾加&字符,则该程序应该在前台运行,即shell使用waitpid等相关函数等待前台作业终止后返回。

6.3 Hello的fork进程创建过程

父进程通过调用fork函数创建一个新的运行的子进程。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段,堆,共享库以及用户栈,当父进程调用fork时,子进程可以读写父进程中打开的任何文件,二者最大区别在于他们有不同的PID,fork函数被调用一次却会返回两次,一次是在父进程中,返回子进程的PID,一次是在子进程中,返回值为0,由于子进程的PID总是非零,因此返回值就提供一个方法来分辨程序是在父进程还是在子进程中执行。子进程与父进程是并发运行的独立进程。

6.4 Hello的execve过程

当创建了一个子进程之后,子进程调用execve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,加载并运行需要以下几个步骤:

①去除已存在的用户区域:删除当前进程虚拟地址的用户部分中已存在的区域结构。

②映射私有区域:为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区;bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中;栈和堆区域也是请求二进制零的,初始长度为零。

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

④设置程序计数器(PC):execve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

具体如图6-4-1所示:

 

图6-4-1    加载器是如何映射用户地址空间的区域的

另外还要注意,只有当出现错误时,例如找不到filename,execve才会返回到调用程序,调用成功不会返回。与fork不同,fork一次调用两次返回,execve一次调用从不返回。

6.5 Hello的进程执行

下面先介绍几个概念帮助理解hello程序是如何运行的:

(1)逻辑控制流:前面提到过,进程为应用程序提供的一个关键抽象的其中一方面就是一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。一系列程序计数器PC的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占 (暂时挂起),然后轮到其他进程。

(2)私有地址空间:进程为每个流都提供一种假象,好像它是独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个内存字节是不能被其他进程读或者写的,在这个意义上,这个地址空间是私有的。

(3)并发流与时间片:两个流如果在执行的时间上有所重叠,那么我们就说这两个流是并发流,每个流执行一部分的时间就叫做时间分片。例如图6-5-1中,我们可以说进程A和进程B并发,进程A也和进程C并发,但是进程B和进程C就不是并发的。

 

 

图6-5-1   一个并发流的示例

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

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

(6)上下文切换:进程在运行时会依赖一些信息和数据,包括通用目的寄存器、浮点寄存器等的状态,这些进程运行时的依赖信息成为进程的上下文。而在进程进行的某些时刻,操作系统内核可以决定抢占当前的进程,并重新开始一个新的或者之前被抢占过的进程,这一过程成为调度。而抢占进程前后由于进程发生改变依赖信息也变得不同,这个过程就是上下文切换。

如图6-5-2为一个上下文切换的示例:

 

图6-5-2    一个上下文切换的示例

介绍完相关概念后,来具体介绍一下hello的执行过程:

首先,进程调用execve函数,将hello的.txt节与.data节分别映射到新的为hello程序分配的虚拟地址空间的只读代码段与读写数据段

在hello程序一开始运行时,其处于用户模式下,然后开始第一次输出hello 2021111004 樊宇宇,然后开始执行sleep函数(在示例图6-5-3中给定的是每次休眠1秒),这时候hello进程就会陷入内核模式,内核在休眠的时间段不会什么都不做,而是会执行上下文切换过程,调度其他进程执行,将控制权交给新的进程,同时定时器开始计时,当时间到预先设定的时长以后(在示例图6-5-3中为1秒),定时器会给发送一个终端信号,内核判定其他进程已经运行了足够长的时间,就执行一个从其他进程到hello进程的上下文切换,将控制重新返回给hello进程,进程hello继续执行下一次输出,如此循环九次。

在第九次执行sleep函数之后,程序执行一个getchar()函数,实际执行的输入流是stdin的系统调用函数read,hello最后一次执行完sleep函数后重新返回了用户模式,系统调用read的进行又使进程陷入了内核模式,内核中对应于read的陷阱处理程序请求来自键盘缓冲区的DMA输入(键盘被抽象的看成是一个文件),这中间的数据传输需要等待一个较长的时间,内核会执行上下文切换调度其他的进程,当从键盘缓冲区到内存的数据传输完成后,会发出一个中断信号,内核判断其他进程已经运行了足够长的时间,执行上下文切换重新调度进程hello,继续执行hello的逻辑控制流。

图6-5-3给出了一个hello程序执行的示例:

 

图6-5-3    hello程序执行的示例

6.6 hello的异常与信号处理

异常可以分为四类:中断,陷阱,故障和终止,下面表格展示了对应于各类的产生原因,异步/同步,返回行为:

类别

原因

异步/同步

返回行为

中断

来自I/O设备的信号

异步

总是返回到下一条指令

陷阱

有意的异常

同步

总是返回到下一条指令

故障

潜在可恢复的错误

同步

可能返回到当前指令

终止

不可恢复的错误

同步

不会返回

  1. 中断类型:

在进程运行的过程中,我们施加一些I/O输入,比如说敲键盘,就会触发中断。系统会陷入内核,调用中断处理程序,然后返回。如图6-6-1所示

                  图6-6-1   中断处理

  1. 陷阱类型:

陷阱和系统调用是一码事,用户模式无法进行的内核程序,便通过引发一个陷阱,陷入内核模式下再执行相应的系统调用。如图6-6-2:

 

 

图6-6-2  陷阱处理

  1. 故障类型:

常见的故障就是缺页故障。在hello中如果我们使用的虚拟地址相对应的虚拟页面不在内存中,就会发生此类缺页故障。故障是可能会被修复的,例如缺页故障触发的故障处理程序,会按需调动页面,再返回到原指令位置重新执行。但对于无法恢复的故障则直接报错退出。如图6-6-3:

 

图6-6-3   故障处理

  1. 终止类型

如果遇到一个硬件错误,那对于程序来说是相当致命的无法修复的,导致结果就是触发致命错误,终止hello程序的运行。如图6-6-4

 

图6-6-4  终止处理

如下表所示是在hello程序执行过程中可能会产生的信号的ID,名称,默认行为以及相应的事件:

ID

名称

默认行为

相应事件

2

SIGINT

终止

来自键盘的中断

9

SIGKILL

终止

杀死程序(该信号不能被捕获不能被忽略)

11

SIGSEGV

终止

无效的内存引用(段故障)

14

SIGALRM

终止

来自alarm函数的定时器信号

17

SIGCHLD

忽略

一个子进程停止或者终止

18

SIGCONT

忽略

继续进程如果该进程停止

19

SIGSTOP

停止

不是来自终端的停止信号

20

SIGTSTP

停止

来自终端的停止信号

  1. 运行过程中按下Ctrl+Z的情况:

按下Ctrl+Z,内核会发信号SIGTSTP给前台进程组中的每一个进程,导致前台作业停止(挂起),如图6-6-5所示:

 

                  图6-6-5  在程序运行过程中按下Ctrl+Z

此时使用ps命令,可以看到hello进程并没有被回收,而且其后台作业号为1,如图6-6-6所示:

 

     图6-6-6  使用ps命令可以看出hello进程只是被挂起  并没有终止、

使用jobs命令,也可以看到被暂停的进程,如图6-6-7所示:

 

    图6-6-7  使用jobs命令查看被暂停的进程

使用命令fg 1可以使hello进程重新变为前台进程,继续执行如图6-6-8所示:

 

         图6-6-8  hello进程重新回到前台执行

在hello进程执行结束以后,使用ps命令重新查看进程表,发现hello进程已经被回收,不再出现在进程表中,如图6-6-9所示:

 

        图6-6-9   说明hello进程已经被回收而不再出现在进程表中

  1. 运行过程中按下Ctrl+C的情况:

如图6-6-10所示为程序运行时按下Ctrl+C的情况,内核会给前台进程组中每一个进程发送SIGINT信号,默认情况下,前台作业会被终止,接着使用ps命令查看进程表,发现hello进程确实以及终止并被回收

 

                图6-6-10   运行过程中按下Ctrl+C的情况

(3)随机输入无意义字符或按下回车键的情况:如图6-6-11所示

 

图6-6-11   随机输入无意义字符或者按下回车键的情况

如上图所示,随机输入无意义字符包括回车,不会影响hello进程的运行,随机输入的字符串(包括回车),都会被缓冲到stdin,但是缓冲区的第一个字符(在本例中是一个回车字符)会被getchar函数从缓冲区读入,其他的字符串会被当成shell命令行处理,如上图6-6-11所示。

(4)运行kill命令:

如图6-6-12即为使用kill命令发送9号信号(SIGKILL信号)给已暂停的hello进程(PID=14878),将其杀死的过程:

 

         图6-6-12  调用kill指令的示例图

6.7本章小结

本章首先介绍了进程的概念和作用,介绍了shell-bash的作用与处理过程,以及fork进程创建的过程与进程调用execve在当前进程中加载并且执行程序的过程,然后介绍了进程执行的过程中关键的概念并且结合hello进程进行具体的分析,最后介绍了hello的异常与信号处理相关的基本概念并且结合hello程序进行了具体分析

7章 hello的存储管理

7.1 hello的存储器地址空间

内存中地址的概念有以下四个:逻辑地址,线性地址,虚拟地址与物理地址,下面分别介绍一下这几个基本概念:

逻辑地址:程序经过编译后出现在汇编代码或者机器代码中的地址,用来指定一个操作数或者一条指令的地址。

线性地址:是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。

虚拟地址:也就是线性地址。

物理地址:用于内存芯片级的单元寻址,CPU通过地址总线的寻址,它是在地址总线上,以电子形式存在的,使得数据总线可以访问主存的某个特定存储单元的内存地址

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

逻辑地址一般由两部分组成,即段标识符与段内偏移量,下面重点介绍一下段标识符,段描述符与段描述符表:

  1. 段标识符:

 

 

图7-2-1  段标识符示意图

如图7-2-1,段标识符是一个16位的字段,又叫做段选择符,包括一个13位的索引字段,1位的TI字段,与两位的RPL字段,其中,TI字段用于选择描述附表,TI为0代表选择的是全局描述符表,反之,TI为1代表选择的是局部描述符表;RPL字段用于表示CPU的当前特权级,RPL为00,表示位于内核态,若RPL为11表示位于用户态;占13位的索引字段用来在段描述符表中确定当前使用的段描述符的具体位置。

  1. 段描述符

段描述符可以分为两类,一类是用户的代码段和数据段描述符,另外一类是系统控制段描述符

  1. 段描述符表

段描述符表由段描述符组成,可以分为三类,全局描述符表GDT(用来存放系统每个任务都可能需要访问的描述符,例如用户代码段,数据段,内核代码段,数据段等),局部描述符表LDT与中断描述符表IDT

 

图7-7-2   从逻辑地址到虚拟地址(也叫线性地址)转换的示意图

如图7-7-2所示,从逻辑地址转换到线性地址需要经过下面几步:首先,逻辑地址被分为16位的段选择符与32位的段内偏移量(offset),然后根据段选择符的TI字段,选择全局描述符表还是局部描述附表(gdtr or ldtr),然后根据index(即段选择符的索引字段)选择段描述符,其中包含被选段的基地址(baseaddr),将被选段的基地址(baseaddr)与段内偏移量(offset)相结合,得到虚拟地址(线性地址)

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

在进行从线性地址到物理地址的变换时,有一个很重要的数据结构叫做页表,页表将虚拟页映射到物理页,每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表,下面先介绍一下页表的结构:

图7-3-1  页表的基本结构

如图7-3-1所示,页表就是一个页表条目(PTE)的数组,虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个PTE。每个PTE由一个有效位和一个n位地址字段组成(事实上,还包括其他内容例如设置访问权限的位等等,为了简化问题,在此先不予讨论)。有效位表明了该虚拟页当前是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置,这个物理页中缓存了该虚拟页。如果没有设置有效位,那么一个空地址表示这个虚拟页还未被分配,否则,这个地址就指向该虚拟页在磁盘上的起始位置。

下面介绍一下地址翻译过程:

形式上,地址翻译是一个N元素的虚拟地址空间(VAS)中的元素和一个M元素的物理地址空间(PAS)中元素的映射:

             MAP:VAS®PASÈÆ

这里,MAP(A)=A’,如果虚拟地址A处的数据在PAS的物理地址A’处,MAP(A)= Æ如果虚拟地址A处的数据不在物理内存中,下图7-3-2展示了MMU如何利用页表来实现这种映射。CPU中一个控制寄存器,页表基址寄存器(PTBR)指向当前页表。n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO)和一个(n-p)位的虚拟页号(VPN)。MMU利用VPN来选择适当的PTE,将PTE中的物理页号(PPN)和虚拟地址中的VPO串联起来,就得到相应的物理地址。[2]

 

图7-3-2  使用页表的地址翻译

图7-3-3展示了当页面命中时,CPU硬件执行的步骤:

  1. 处理器生产一个虚拟地址,并把它传送给MMU

    (2)MMU生成PTE地址,并从高速缓存/主存请求得到它

(3)高速缓存/主存向MMU返回PTE

(4)MMU构造物理地址,并把它传送给高速缓存/主存

(5)高速缓存/主存返回所请求的数据字给处理器

 

                    图7-3-3  页面命中的情况

图7-3-4展示了页面不命中时的情况,页面不命中时,处理缺页要求硬件和操作系统内核协作完成:

(1)(2)(3)与页面命中的前三步相同

(4)PTE有效位是0,MMU出发缺页异常,传递CPU中的控制流到操作系统内核中的缺页异常处理子程序

(5)缺页处理子程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘

(6)缺页处理自行页面调入新的页面,并更新内存中的PTE

(7)缺页处理子程序返回到原来的进程,再次执行导致缺页的命令

        

 

                      图7-3-4   缺页的情况

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

(1)为什么要使用多级页表代替单一页表?

在32位系统中,地址空间有32位,假设每个页面大小为4KB,每个PTE大小为4字节,那么即使所引用的只是虚拟地址空间中很小的一部分,也总是需要一个4MB的页表驻留在内存中,对于地址空间为64位的系统而言,问题将变得更加复杂。

如图7-4-1是一个两级页表的示意图:

 

图7-4-1  一个两级页表的示意图

采用多级页表的方法从两方面减少了内存的需求。(1)如果一级页表中的一个PTE是空的,那么相应的二级页表就根本不会存在。(2)只有一级页表才需要总是存储在主存中;虚拟内存系统可以在需要时创建、调入、调出二级页表

(2)快表(TLB)

每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。

图7-4-2  虚拟地址中用以访问TLB的组成部分

 

TLB是一个小的,虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。如图7-4-2所示,用于族选择和行匹配的索引和标记字段是从虚拟地址的虚拟页号(VPN)中提取出来的,如果TLB有T=2^t个组,那么TLB索引(TLBI)是由VPN的t个最低位组成的,而TLB标记(TLBT)是由VPN中剩余的位组成的。下图7-4-3(a)与7-4-3(b)分别为TLB命中与不命中的操作图:

 

          图7-4-3  TLB命中与不命中的操作图

 

       图7-4-4 Corei7地址翻译情况(只看方框里的部分)

 

图7-4-5 Corei7页表翻译

如图7-4-4(只看方框里的地址翻译部分),与图7-4-5,可以看出MMU在进行地址翻译时,会先访问TLB,如果TLB命中,则直接利用PTE构造对应的物理地址。如果TLB不命中,会访问四级页表,如图所示,虚拟地址的VPN被划分为相等大小(这样做有助于优化页表性能)的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。CR3寄存器包含L1页表的物理地址。VPN1提供一个到L1PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供一个到L2PTE的偏移量,以此类推。

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

 

         图7-5-1   Corei7地址翻译情况(主要看右边框框里的部分)

如图7-5-1(主要看框框里的部分)Cache支持的物理内存访问大致分为以下几步:

(1)VA被分割为VPN和VPO,VPN用于查询页表得到PPN,VPO直接作为PPO传递,PPN与PPO组合得到PA

(2)L1 Cache对PA进行分解,将其分解为标记(CT)、组索引(CI)、块偏移(CO)(注意在这一步中将CI与CO组合起来正好是PPO是处于性能考虑的,因为查找物理页号PPN需要一段时间,所以在这段时间内可以直接根据PPO得到CI与CO并且先去进行行匹配与组选择,提高了并行性)

(3)L1 Cache根据CI选择L1 Cache中的组,根据组有效位与CT(标志位)判断是否命中,若命中则根据CO(组偏移)选择组中的块,并将其返回到CPU;否则在下一级缓存中重复上述步骤

7.6 hello进程fork时的内存映射

当 fork 函数被 shell 进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只 读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存系统。当这两个进程中任一个后来进行写操作时,写时复制机制就会创建新页面,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。 它可能会自动覆盖当前进程中的所有虚拟地址和空间,删除当前进程虚拟地址的所有用户虚拟和部分空间中的已存在的代码共享区域和结构,但它不会自动创建一个新的代码共享进程。

如图7-7-1加载并运行 hello 需要以下几个步骤:

(1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。

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

(3)映射共享区域,hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

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

 

             图7-7-1  加载器是如何映射用户地址空间的区域的

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

页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的,在指令请求一个虚拟地址时,MMU中查找页表,如果这时对应得物理地址没有存在主存的内部,我们必须要从磁盘中读出数据。在虚拟内存的习惯说法中,DRAM缓存不命中成为缺页。在发生缺页后系统会调用内核中的一个缺页处理程序,选择一个页面作为牺牲页面。如图7-8-1,具体流程如下:

(1)处理器生成一个虚拟地址,并将它传送给MMU

(2)MMU生成PTE地址,并从高速缓存/主存请求得到它

(3)高速缓存/主存向MMU返回PTE

(4)PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。

(5)缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。

(6)缺页处理程序页面调入新的页面,并更新内存中的PTE。

(7)缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。

 

图7-8-1  缺页的情况以及处理流程

7.9动态存储分配管理

相关基本概念:

动态内存分配器维护着一个进程的虚拟内存区域,称为堆,内核中维护着一个变量brk,指向堆的顶部。分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。

分配器有以下两种基本风格,两种风格都是要求显示的释放分配块。

  1. 显示分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器。
  2. 隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。

在维护动态内存的过程中,碎片现象是造成堆利用率很低的主要原因,当存在未使用的内存,但不能用来满足分配请求时,就会发送这种现象。碎片也有两种形式:(1)内部碎片:已分配块比有效载荷大。(2)外部碎片:空闲内存合计起来足够满足一个分配请求,但没有一个单独的空闲块足够大可以处理这个请求。

显示分配器必须在以下约束条件下工作:

(1)处理任意请求序列:分配器不可以假设分配和释放请求的顺序

(2)立即响应请求:不允许分配器为了提高性能重新排列或缓冲请求

(3)只是用堆:分配器使用的任何非标量数据结构必须保存在堆中

(4)对齐块:分配器必须对齐块

(5)不修改已分配的块:分配器只能操作空闲块

隐式空闲链表:

隐式空闲链表区别块的边界、已分配块和空闲块的方法如下图7-9-1所示:

 

图7-9-1   一个简单的堆块的格式

这种情况下,一个块是由一个字的头部、有效载荷,以及可能的填充组成。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。块的头最后一位指明这个块是已分配的还是空闲的。

头部后面是应用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。块的格式如下图7-9-2所示,空闲块通过头部块的大小字段隐含的连接着,所以我们称这种结构就隐式空闲链表。

 

   图7-9-2   用隐式空闲链表来组织堆。阴影部分是已分配块。没有阴影的部分为空闲块。

隐式空闲链表在动态内存分配中,有以下四个基本操作:放置已分配块、分割空闲块、获取额外堆内存、合并空闲块,下文中会对其分别介绍。

  1. 放置已分配的块:当一个应用请求一个k字节的块时,分配器搜索空闲链表。查找一个足够大可以放置所请求的空闲块。分配器搜索方式的常见策略是首次适配、下一次适配和最佳适配。
  2. 分割空闲块:一旦分配器找到一个匹配的空闲块,就必须做一个另策决定,那就是分配这个块多少空间。分配器通常将空闲块分割为两部分。第一部分变为了已分配块,第二部分变为了空闲块。
  3. 获取额外堆内存如果分配器不能为请求块找到空闲块,一个选择是合并那些在物理内存上相邻的空闲块,如果这样还不能生成一个足够大的块,分配器会调用sbrk函数,向内核请求额外的内存。
  4. 合并空闲块合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。对于四种情况分别进行空闲块合并,我们只需要通过改变头部的信息就能完成合并空闲块。Knuth提出了一种采用边界标记的技术快速完成空闲块的合并。

显示空闲链表是将空闲块组织为某种形式的显示数据结构。如下图7-9-3所示。堆被组织为一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继的指针。

 

图7-9-3  使用双向空闲链表的堆块的格式

显示空闲链表的优势在于其使用双向链表的结构,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。

分离空闲链表:

分离空闲链表的核心思想是分离存储,即维护多个空闲链表,其中每个链表的块有大致相等的大小,实现有两种基本方法:简单分离存储和分离适配。C语言的malloc函数实现方法介绍显示空闲链表加分离适配。

在分离空闲链表的基础上,我们还可以进一步将其维护成AVL树或红黑树的结构,使其效率达到最优。

7.10本章小结

本章首先以hello为例介绍了四种地址空间,然后介绍了intel逻辑地址到线性地址的变换,以及线性地址到物理地址的变换,以及TLB与四级页表支持下的VA到PA的变换,三级Cache支持下的物理内存访问,接着介绍了hello进程fork与execve时的内存映射,最后介绍了缺页中断处理的相关知识以及动态存储分配管理的相关知识。

结论

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

预处理过程:预处理器对hello.c源程序进行诸如文件包含,宏替换,布局控制等等处理过程形成hello.i文本文件

编译过程:编译器对hello.i进行处理,将其处理为汇编语言源程序hello.s(因为汇编语言本质上与机器代码具有很好的对应关系),并且在这个过程中对程序进行一些保守的优化

汇编过程:汇编器把汇编语言源程序hello.s经过汇编过程转化为机器代码,生成可重定位目标文件hello.o(二进制文件)

链接过程:链接器将hello.o引用的符号进行解析和重定位,最终形成可执行目标文件hello

上述过程执行结束以后,我们在终端中执行hello程序,shell会先调用fork函数创建一个新进程,然后调用execve函数在这个新进程下加载并且运行程序hello,为hello创建新的代码数据堆栈段,CPU为hello分配一个时间片,将程序计数器指向hello代码,开始执行hello程序。在加载运行阶段,还会完成一部分动态链接的任务。待hello程序执行结束后,进程会回收子进程,内核会删除该进程所创建的所有信息。

至此,hello的生命周期结束。

通过学习计算机系统这门课,我对于现代计算机系统的架构和工作原理有了一定的理解,并且这种理解在这门课的四次LAB与大作业的完成中被一步步加深,并且我的实践能力与理论知识的加强同步进行,有助于今后我的学习科研更好的展开。

附件

hello.c:C源文件

hello.i:预处理后生成的文本文件,可以看出预处理器的一部分功能

hello.s:汇编语言源程序,用于分析编译器执行的功能

hello.o:可重定位目标文件,用于分析汇编器功能

hello_Elf.txt: readelf读取的hello.o的elf文件内容,用来研究hello.o的ELF文件结构

hello_back.txt:objdump生成的hello.o的反汇编文件,用于比较汇编文件hello.s的异同,还用来比较与hello的反汇编文件hello_out_text_Elf.txt的异同

hello_out_Elf.txt:readelf生成的hello(可执行目标文件)的elf文件内容,用来和hello.o(可重定位目标文件)的elf结构文件作比较

hello_out_text_Elf.txt:objdump生成的hello的反汇编文件

参考文献

[1]  https://blog.youkuaiyun.com/drshenlei/article/details/4261909

[2]  [美]兰德尔E.布莱恩特. 深入理解计算机系统.3版 [M] 龚奕利,贺莲译 北京:机械工业出版社,2016.7

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值