程序人生: Hello's P2P

摘  要

本文以一个简单的C程序——hello为例,分析了一个程序在基于x86-64平台上运行的64位现代Linux操作系统上从源代码到运行的全过程,系统地分析了预处理、汇编、编译、链接、进程创建、虚拟内存、高速缓存、Unix 标准I/O设备这些方面,是对计算机系统这门课程的总结。

关键词:计算机系统;编译;虚拟内存;进程管理;I/O设备管理                           

 

 

 

 

 


 

目  录

 

第1章 概述........................................................................................................ - 4 -

1.1 Hello简介................................................................................................. - 4 -

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

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

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

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

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

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

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

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

第3章 编译...................................................................................................... - 12 -

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

5.8 本章小结.................................................................................................. - 43 -

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

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

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

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

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

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

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

6.7本章小结.................................................................................................. - 49 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结................................................................................................ - 54 -

第8章 hello的IO管理............................................................................ - 56 -

8.1 Linux的IO设备管理方法..................................................................... - 56 -

8.2 简述Unix IO接口及其函数.................................................................. - 56 -

8.3 printf的实现分析................................................................................... - 56 -

8.4 getchar的实现分析............................................................................... - 57 -

8.5本章小结.................................................................................................. - 57 -

结论.................................................................................................................... - 58 -

附件.................................................................................................................... - 59 -

参考文献............................................................................................................ - 60 -

 


第1章 概述

1.1 Hello简介

P2P(Program to Process,从程序到进程)的过程:

  1. 预处理

在Hello的一个完整的编译过程中,第一步是预处理代码。

在预处理阶段,编译器驱动程序(在本次实验中是GCC)首先调用C预处理器(cpp),读取输入的hello.c源文件,进行预处理。预处理器删除代码的所有注释,并识别所有以#开头的行,进行相应的操作,例如:

a)       将#include宏所指示文件内容插入到这一行。

b)      将#define A B宏定义语句之后的所有A简单地替换为B。如果A是形如A(x)形式的宏函数,则用x位置的表达式(简单地)替换x。

c)       检查#ifdef A、#ifndef A所引用的宏A是否定义了,根据A事实上是否被定义过,来决定是否保留这个if块中的语句。

预处理阶段结束时,预处理器将生成中间文件hello.i,供编译阶段使用。

  1. 编译

在编译阶段,C编译器(ccl)读取文件hello.i,将其翻译为指定平台的汇编代码hello.s。

  1. 汇编

在汇编阶段,汇编器(as)将汇编代码文件hello.s翻译为机器代码,保存为可重定位目标文件hello.o。

  1. 链接

在链接阶段,链接器(ld)将收集程序使用的所有可重定位目标文件,进行组合、重定位,生成一个可执行目标文件。在本次实验中,有hello.o、printf.o,后者是C标准库提供的printf函数的实现,这个函数与一些其他函数一起,被存在一个后缀为.a的存档文件中。最后获得的可执行文件可以被加载到内存中的适当位置并直接执行,无需再做修改。

  1. 执行

在Shell中键入可执行文件的路径:./hello。按下回车后,Shell判断到这是一个外部的可执行文件,所以它调用fork()生成一个自身的子进程,然后在子进程中调用execve(),,内核将清除现有进程(即子进程)所有的用户区域、将新进程(hello的一个实例)的私有区域(数据、代码、bss、栈)映射到hello文件中的特定区域(bss和栈、堆区域是请求二进制零的,映射到匿名文件)。这些段并没有被立刻加载到内存中。直到程序第一次访问时,内核会通过缺页中断处理子程序,将所需内存的页面换入物理内存。接着,内核映射进程的共享区域,将程序使用到的动态链接对象映射到用户虚拟地址空间的共享区域。最后,execve设置当前进程上下文中程序计数器RIP的值为代码的入口函数。这个入口函数会在一些必要的处理(如运行时动态链接)后调用main()函数,进而执行我们为hello编写的代码。

020(Zero to Zero,从零到零)的过程:

首先,Shell调用fork()函数,内核创建一个当前Shell进程的副本,操作系统为新进程创建一个进程结构体、划分内存空间、创建虚拟内存映射、根据优先级分配CPU时间片,新进程开始在CPU上执行。接着,Shell调用execve()函数,这个函数通过系统调用陷入内核。内核在Shell子进程的上下文中清除旧的私有区域,并将hello的私有区域映射到当前进程用户虚拟地址空间的特定区域。内核创建打开文件描述符、映射共享内存区域、设置RIP的值为入口函数的第一条指令的地址,入口函数引导程序进入main()函数,执行用户代码。用户代码中printf()函数调用系统调用write()、通过后者陷入内核,内核将字符串写入标准输出流stdout并刷新。随后,流中的新字符被写入虚拟内存中映射的显示内存,图形适配器将新字符写入图形缓冲区、以特定的刷新率输出到显示器,用户可以看到程序的输出。hello执行完成后,在main函数中返回。hello执行完毕,就进入已终止的状态。然后,Shell得知hello运行完毕,回收它在内存中的数据,内核给父进程(Shell)发送一个SIGCHLD信号,同时将hello在系统中的痕迹删除。至此,hello的一次运行完毕。

1.2 环境与工具

软件环境:

l  Ubuntu 19.04 x64(内核版本:5.3.0,在VMware® Workstation 15.5.0 Pro中虚拟化执行,4个虚拟化处理器核心,宿主机操作系统为Windows 10 1809 x64)

硬件环境:

l  CPU: AMD Ryzen R5 3600

l  内存: DDR4 16G(x2) 3000

l  芯片组: B450M

开发、调试工具:

l  Visual Studio Code 1.40.2

l  Vim 8.1

l  GCC 9.2.1 20191008

l  GDB 8.3

l  EDB 1.0.0

1.3 中间结果

所有的中间产物:

l  hello.i:由hello.c,经C预处理器处理得到的预处理代码文件。

l  hello.s:由hello.i,经汇编器汇编得到的汇编代码文件。

l  hello.o:由hello.s,经编译器翻译得到的可重定位目标文件。

l  hello:由hello.o及相关动态链接库,经链接器链接得到的可执行目标文件。

l  hello1.s:由hello.o,经objdump反汇编得到的反汇编结果。

l  hello2.s:由hello,经objdump反汇编得到的反汇编结果。

1.4 本章小结

本章在操作系统内核的层面概括地描述了hello程序从源代码转化为可执行目标文件、从执行到结束的整个过程,为后续章节的进一步描述做准备。

 


第2章 预处理

2.1 预处理的概念与作用

  1. 概念

预处理是程序编译流程的第一步。预处理器查找代码中的所有预处理执行,并在预处理时执行。预处理指令一般为替换代码中的内容,如#include将这个指令替换为它所指定文件的内容、#define将它之后所有被它定义的宏替换为定义的值、#ifdef、#ifndef则根据它指定的宏是否被定义,来决定是否将它和#endif所包含的代码段插入到当前位置。

  1. 作用

使用预处理指令可以让程序的组成架构更加清晰。例如,C语言的模块常常以文件为单位,模块之间的耦合远远弱于模块内部的函数间的耦合。当编写大型程序时,将程序分离为多个各司其职的模块,并使用#include使用模块,可以极大地提升开发效率、减轻开发、调试和维护的工作量。

#define宏定义则常常被用作定义常数。如果要修改这个常数,只需要修改一个位置的量即可。

#ifdef则被用作调试。在编译发布版本的程序时,可以通过取消定义调试宏,来屏蔽调试相关的函数,比手动注释代码更加方便。

2.2在Ubuntu下预处理的命令

  1. 使用命令 gcc -E hello.c -o hello.i 调用gcc编译器套件,让gcc调用预处理器,生成预处理文件hello.i

 

图 1

  1. 查看hello.i文件:

 

图 2

 

图 3

 

图 4

2.3 Hello的预处理结果解析

经过预处理操作,我们得到了hello.i文件。可以发现,预处理器向源文件中引入了所有使用到的头文件,如stdio.h,这些头文件中包含必要的数据类型声明(图2)、结构体定义(图3)、外部函数声明(图4)。预处理为编译做准备,因为预处理得到的C代码是完整的,因此可以被翻译为汇编语言。

2.4 本章小结

本章介绍了预处理操作的概念和作用,并展示了hello的预处理实例。我们从中可以发现,hello.i包括了hello.c使用的所有外部头文件的代码内容。预处理器补全了代码中引用的所有必要代码,从而使得hello.c这一部分的代码可以被编译器编译,同时又保持了hello.c和外部库之间的独立性:它们可以被编译为分离的文件。


第3章 编译

3.1 编译的概念与作用

概念:编译是指根据高级语言代码生成在目标平台上等价的低级语言代码,通常是人类可读的汇编代码。在编译阶段,GCC编译套件调用C编译器(ccl),编译器读取文件hello.i,将其翻译为指定平台的汇编代码hello.s。

作用:计算机只能执行机器语言程序。高级语言程序不能在机器上直接运行,如果要运行高级语言程序,必须将其翻译为机器语言。编译器通过语法分析、词法分析等复杂算法,理解高级语言程序的流程,将其翻译为对应的等价机器指令代码,即汇编代码。

3.2 在Ubuntu下编译的命令

使用指令 gcc -S hello.i -o hello.s 来生成hello.i对应的汇编代码,以ASCII码格式保存在hello.s文件中。

 

图 5

 

图 6

3.3 Hello的编译结果解析

  1. 数据类型

a)       int(unsigned):int型数据在汇编代码中被编译为32位有符号数。

 

图 7

 

图 8

 

图 9

图7为将变量i赋值为0的语句。$0表示常数0。

由于编译器的优化,代码的形式在功能不变的前提下发生了变化。部分语句的常数(如18、19行的数组下标)在编译时被隐藏在指针操作内部,所以在此不予分析。

图8为给寄存器edi赋整数值1的语句,作为调用exit函数的参数。

图9为for循环中的循环条件语句i<8(被编译器等价变换为i<=7)。由图7和图9的代码可以看出,局部变量i事实上被存储在运行时栈中,程序使用相对于rbp的偏移量来寻址这个变量。程序在栈中保留出sizeof(int)大小的空间,如果没有初始化,程序将什么都不做,直至第一次写入,才真正修改它的值。

b)      char*:字符串实质上是char类型的数组,通常用指向第一个字符的指针来表示。从图6中可以看出,程序中的两个字符串常量"用法: Hello 学号 姓名 秒数!\n"和"Hello %s %s\n"分别以.LC0、.LC1表示,并被指示储存在汇编后程序的.rodata节内。

  1. 流程控制

a)       判断

 

图 10

图10为hello.c文件的内容。如图,源程序中第13行处有一个判断语句,如果argc的值不为4,程序将进入判断的分支,否则程序将跳过分支、继续执行。

b)      循环

 

图 11

 

图 12

图11、图12为hello.s的部分内容,24行的cmpl指令将比较两个32位整数,实际上是将两数做差,然后设置标志位寄存器。下一行的jle指令读取OF和ZF位的值,如果操作数1大于等于操作数2,则跳转到.L4处执行。.L4对应的代码块即为for循环体。每次循环结束时,程序就执行到53行的判断语句,来决定是否继续循环。

  1. 赋值操作

a)       赋值

如图12,第51行的语句实现了i++语句,即变量i自增1,使用addl指令将32位整数加到变量i上。

b)      引用、解引用

如图10第10行,main函数的第二个参数是一个char型指针的数组,参数传递时,事实上只传递了指向数组第一个元素的指针。图12第37~40行的汇编代码对应着图10第18行C代码中的argv[1]表达式,该表达式访问了一个指针数组,对数组的第二个元素求值。汇编代码中,对数组的访问通过移动指向首元素的指针、并使用mov指令解引用来实现。

c)       变量的初始化

hello.c程序中没有初始化的变量。计数器变量i在17行被赋值0,对应的汇编代码在第31行。

  1. 函数调用

为了方便,现在列出hello源程序中的所有函数调用的C代码和汇编码:

 

 

 

 

a)       参数传递

  Linux x64平台下,调用函数时通过寄存器传递参数。前四个参数使用的寄存器依次为rdi rsi rdx rcx,这个事实可以从上图中看出。

b)      调用

Linux x64平台下的函数调用通过call语句实现。call L语句事实上等价于push %rip和jmp L,也就是先将当前指令的下一条指令地址保存在栈中,然后跳转到函数入口执行。

c)       返回及返回值

 

当函数执行完毕时,需要结束,返回到调用者的下一条命令开始执行;有的函数还需要返回一个值给调用者。返回是通过ret指令实现的,它将栈顶的地址弹出(函数执行完毕时,栈恢复到call指令刚刚执行后的状态),将rip寄存器的值设置为那条地址。返回值则是通过寄存器rax实现的。如果需要返回的数据类型可以放入64位寄存器,则将其值存储在rax中(或其低位,如eax、ax、al),否则,将其内存地址存入寄存器rax中。调用者通过读取此寄存器来获得返回值。图为main函数尾部的部分代码,56行将返回值设置为0,59行的ret指令从main中返回。特别地,第49行的代码将atoi函数的返回值作为参数传递给sleep函数,这是通过寄存器间的赋值实现的。

3.4 本章小结

本章描述了编译器依据hello.c C源程序生成的汇编码中,hello程序的数据类型、赋值、函数调用、参数传递、返回值是如何实现的。

编译是从C语言程序生成可执行文件的第二步,它将预处理后的C语言代码转换为与机器指令一一对应的、人类可读的汇编代码。在计算机被发明的早期,为了给计算机编程,人们需要直接书写机器代码,后来才有了这种更便于人类阅读的汇编代码——它事实上是机器代码的另一种书写形式。如果要运行一个编译型高级语言程序,必须将其代码转换为机器代码,汇编代码就是一个中间形式。随着时间的推移,编程语言也变得更加复杂,但是编译仍然是整个流程中不可缺少的一个重要环节。编译后的程序离变成机器代码只有一步之遥——汇编。


第4章 汇编

4.1 汇编的概念与作用

  1. 概念:汇编器(as)读取编译产生的汇编代码文件.s,将其中的每一条ASCII编码的汇编代码翻译为对应的二进制机器语言指令,并将这些指令按照特定的结构组织为可重定位目标程序。可重定位目标程序的格式与平台相关,Linux使用ELF格式。
  2. 作用:汇编将汇编代码翻译为二进制机器代码,后者是机器可以理解的形式。这种二进制代码经过组合,并修改必要的部分以适合现代操作系统的虚拟内存机制、动态链接,即可在计算机上运行。

4.2 在Ubuntu下汇编的命令

使用 as hello.s -o hello.o 命令汇编hello.s文件,并将结果保存在hello.o文件中。

 

图 13

4.3 可重定位目标elf格式

    ELF文件格式如图14所示:

 

图 14

使用指令 readelf -a hello.o 调用readelf工具查看hello.o文件的ELF格式信息,结果如图15、图16所示:

 

图 15

 

图 16

下面对输出的前两部分进行分析:

  1. ELF头(ELF Header):如图15,ELF头以一个16字节的Magic开始,Magic的前4字节指明这是一个ELF格式的文件。其余字节描述了如何读取这个文件。
  2. 节头部表(Section Headers):如图14,节头部表位于ELF文件的末尾。ELF文件由许多节和ELF头、头部表组成。如图16,节头部表描述了ELF文件各节的名字、类型、内存映像地址、大小、文件偏移量、对齐字节数、标志等信息。例如,存放代码的.text节从文件的0x40处开始,大小为0x92字节,对齐到1字节。

4.4 Hello.o的结果解析

 

图 17, hello.o的反汇编结果

 

图 18, 汇编前的hello.s文件

 

图 19, 汇编前的hello.s文件

  1. 机器语言的构成:如图17,机器代码由指令码和操作码组成,分别指示指令的类型和操作的类型。
  2. 机器语言与汇编语言的映射关系

a)       每个机器代码都对应着一个汇编代码,然而有些机器代码可以写成多种汇编指令,比如je和jz。有些指令对于不同寄存器有不同的机器代码,如25行、38行和40行的mov指令。

b)      立即数:立即数在汇编语言中以C语言格式书写的数字,前面加上一个$组成。而在机器语言中,立即数被表示为补码形式或者无符号数形式,并以目的机器的字节序存储。立即数被解读为有符号数(补码)还是无符号数(无符号编码)由具体指令决定,在内存中无法区分。

c)       在分支转移、函数调用类的指令中,目的地址常常以相对当前RIP的偏移量表示。编译后的汇编代码中填入了具体的目的地址,而编译前这些地址是以标签的形式表示的,如第30行、第33行的.L2、.L4。图18第25行的指令je .L2被编译为图17第17字节开始的指令je 2f。指令的十六进制机器码为74 16,其中74表示指令类型为je,16则为跳转目标相对于当前RIP的偏移量。由于执行指令时RIP首先被更新,所以此时RIP为19,即下一条指令的第一个字节。0x19+0x16=0x2f,即为我们需要的跳转目标。类似地,第36字节开始的指令eb 48,为一个跳转指令。eb指明指令类型,48即为跳转的相对地址(偏移量),将当前RIP=0x38加上0x48,即得到目的地址0x80。

d)      参数和局部变量:调用参数存储在上一个函数的栈帧内,或者存储在寄存器中。局部变量存储在作用域函数的栈帧中,以相对于ebp(栈帧)的方式访问。

e)       函数调用:汇编前,call指令的参数为调用的函数名,而机器代码中,call指令的参数被替换为调用的函数地址。由于本程序使用动态链接编译,

4.5 本章小结

汇编器将汇编代码翻译为机器代码,以目标文件的形式保存。同时由于链接的需要,目标文件是可重定位的,其中保留了一些需要重定位的内容,并在文件中的一个特殊的节:.rel.*重定位节内,指出了所有需要重定位的条目位置。汇编是承前启后的一步,用户代码在这一步后完全变为了二进制格式,数字型常数均被转换为补码、无符号数、IEEE.754格式的二进制格式,程序被进一步翻译为二进制形式。经过接下来的链接之后,目标文件将变为可以直接执行的格式。
第5章 链接

5.1 链接的概念与作用

概念:链接是将代码和数据片段组合成为一个单一文件的过程,这个文件可被直接加载到内存中执行。在现代系统中,链接是由链接器自动执行的。

作用:链接使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。

5.2 在Ubuntu下链接的命令

使用命令

ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o

将hello.o与相关库函数动态链接为hello可执行文件,并且不生成位置无关代码,如图20。

 

图 20

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

 

图 21

 

图 22

 

图 23

 

图 24

使用命令 readelf -a hello 调用readelf工具查看链接后hello的可执行文件结构信息,如图21~图24。其中,图21、图22列出了hello文件的头部信息及各节的信息,可知hello共有25节。从表中还可读出各节的名称、类型、起始偏移量、大小、标志、对齐字节数、内存映像中的地址等信息。

 

图 25

5.4 hello的虚拟地址空间

由5.3的分析,hello在执行后的内存映像应该按照段头部表描述的格式来分布。我们使用edb查看hello在内存中的虚拟地址空间中各段对应地址的内容,分析如下(图片在对应说明文字下方):

  1. 切换到工作目录,使用命令 edb –run ./hello 调用EDB执行hello程序。
  2. 跳转到内存地址0x400000,如图,0x400000~0x4002df的内容为ELF头部

 

  1. 跳到0x4002e0,如图,0x4002e0~0x4002ff为.interp节,对齐到1字节(即没有对齐)。该节指出了进程加载时所需的动态链接器ld-linux.so的位置。

 

  1. 0x400300~0x40031f 0x400320~0x400340分别是.note.gnu.propert节和.note.ABI-tag节:

 

  1. .hash和.gnu.hash节内容为空,此处略去。
  2. 0x400398~0x40046f为.dynsym节,指出了程序的动态链接符号:

 

  1. 0x400470~0x4004cb为.dynstr节:

 

  1. 0x4004cc~0x4005b0为.gnu.version节和.gnu.version_r节:

 

  1. 0x401000~0x40101b为.init节。该节存放hello进程初始化的代码,这段代码最终将调用main函数:

 

 

  1. 0x401020~0x401090为.plt节,0x401090~0x4010f0为.plt.sec节。.plt节存放PLT表,与运行时调用动态链接符号有关。该表为jmp语句组成的跳转表,属于只读代码段:

 

  1. 0x4010f0~0x401235为.text段,属于只读代码段,存放主程序文件的机器代码:

 

 

 

 

  1. .fini段:略
  2. 0x402000~0x40203b:.rodata段

 

  1. .eh_frame .dynamic .got .got_plt .data段:

 

5.5 链接的重定位过程分析

使用objdump -d -r hello.o > hello1.s、objdump -d -r hello > hello2.s分别将hello.o与hello的反汇编结果保存至hello1.s、hello2.s中,对比如下:

 

图 26

如图26,左侧为链接前的反汇编代码,右侧为链接后的反汇编代码。可以发现,main()函数中对局部变量的访问为相对于RIP寻址,所以在链接时不需要重定位;调用外部函数时两者有不同,区别在于:链接前调用外部符号对应的语句(如左侧0x51处的lea语句,访问全局变量;0x5d处的call语句,调用printf函数),地址为全零占位,因为这些全局符号在内存中的实际地址无法确定,需要在链接时生成PLT表,并将表中的地址填入相应位置;程序运行时,第一次调用相应语句,PLT会将动态符号的真实地址填入GOT表,以后的调用中,PLT将直接查询GOT表条目,即实现了动态链接过程。

编译时的链接为静态链接,即将所用.o文件相应只读代码段、数据段分别合并为一个整体的只读代码段和数据段,并根据链接器自己生成的符号表,重定位每个符号引用的地址。

5.6 hello的执行流程

使用EDB运行hello,从第一条语句开始单步跟踪。略去一些不重要的跳转,结果如下:

指令

跳转前地址

跳转后地址

说明

call

00007fe2:c0c51103

00007f72:c0c51de0

ld-2.30.so

jmp

00007fe2:c0c51e62

00007f72:c0c51e8f

ld-2.30.so

jmp

00007fe2:c0c5221d

00007fe2:c0c51e99

ld-2.30.so

call

00007fe2:c0c51fe9

00007fe2:c0c5c810

ret

00007fe2:c0c5c871

00007fe2:c0c51fee

call

00007fe2:c0c52037

00007fe2:c0c6b4d0

jmp

00007fe2:c0c6b5f1

00007fe2:c0c6b6c8

rax

jmp

00007fe2:c0c6b5f1

00007fe2:c0c6b790

rax

jmp

00007fe2:c0c6b5f1

00007fe2:c0c6b800

rax

jmp

00007fe2:c0c6b5f1

00007fe2:c0c6b768

rax

jmp

00007fe2:c0c6b5f1

00007fe2:c0c6b840

rax

jmp

00007fe2:c0c6b5f1

00007fe2:c0c6b610

rax

jmp

00007fe2:c0c6b5f1

00007fe2:c0c6b820

rax

jmp

00007fe2:c0c6b5f1

00007fe2:c0c6b610

rax

jmp

00007fe2:c0c6b5f1

00007fe2:c0c6b7e0

rax

jmp

00007fe2:c0c6b5f1

00007fe2:c0c6b610

rax

jmp

00007fe2:c0c6b6c0

00007fe2:c0c6b866

call

00007fe2:c0c6b866

00007fe2:c0c68e20

call

00007fe2:c0c6b86d

00007fe2:c0c6ccd0

call

00007fe2:c0c6ba82

00007fe2:c0c6b130

call

00007fe2:c0c6b920

00007fe2:c0c69380

call ld-2.30.so!__tunable_get_val

call

00007fe2:c0c6b92f

00007fe2:c0c69380

call ld-2.30.so!__tunable_get_val

call

00007fe2:c0c6b94a

00007fe2:c0c69380

call ld-2.30.so!__tunable_get_val

call

00007fe2:c0c6b965

00007fe2:c0c69380

call ld-2.30.so!__tunable_get_val

call

00007fe2:c0c6b99d

00007fe2:c0c69380

call ld-2.30.so!__tunable_get_val

call

00007fe2:c0c6b9b1

00007fe2:c0c69380

call ld-2.30.so!__tunable_get_val

call

00007fe2:c0c6b9d6

00007fe2:c0c71b90

call

00007fe2:c0c6b9e4

00007fe2:c0c6cd20

call

00007fe2:c0c6ba19

00007fe2:c0c525d0

rbp

call

00007fe2:c0c51135

00007fe2:c0c610b0

call

00007fe2:c0c6112c

00007fe2:c0c60f90

call

00000000:00401118

00000000:00403ff0

libc-2.30.so!__libc_start_main

call

00007fe2:c0a6f13d

00007fe2:c0a920e0

libc-2.30.so!__cxa_atexit

call

00007fe2:c0a6f16c

00000000:00401270

rbp=<hello!__libc_csu_init>

call

00000000:0040129c

00000000:00401000

hello!_init

call

00000000:004012b9

00000000:004011d0

call qword [r15+rbx*8]

jmp

00000000:004011d4

00000000:00401160

hello!register_tm_clones

call

00007fe2:c0a6f192

00007fe2:c0a8e060

libc-2.30.so!_setjmp

call

00007fe2:c0a6f1e1

00000000:004011d6

hello!main

call

00000000:004011f4

00000000:00401090

hello!.plt+0x70

jmp

00000000:00401094

00000000:00401030

hello!puts@plt

jmp

00000000:00401039

00000000:00401020

hello!.plt 开始绑定GOT

jmp

00000000:00401026

00007fe2:c0c67a20

ld-2.30.so

call

00007fe2:c0c67a99

00007fe2:c0c606e0

call

00007fe2:c0c607b2

00007fe2:c0c5b950

call

00000000:004011fe

00000000:004010d0

jmp

00000000:004010d4

00000000:00401070

hello!exit@plt

jmp

00007f10:1af29ada

00007f10:1ad53d40

libc-2.30.so!exit

call

00007f10:1af2341b

00000000:004012e8

call

00007f10:1ad53cbd

00007f10:1adeffb0

libc-2.30.so!_exit

5.7 Hello的动态链接分析

hello调用动态链接函数依靠PLT、GOT来实现。.got.plt节中存有被惰性解析(lazily resolved)的节,因此可以观察到调用plt函数前后.got.plt节内容的变化。

查看objdump输出,可知.got.plt被加载到0x404000地址处,长度为0x48 Byte。重新运行hello,用EDB跟踪查看0x404000~0x404047处内存内容变化:

图27为程序刚刚开始运行时.got.plt节的内容:

 

图 27

将.got.plt视作一个64位指针数组GOT,则GOT[0]和 GOT[1]包含动态连接器在解析函数地址时会使用的信息。GOT[2]是动态连接器在 ld-linux.so模块中的入口点。图28为GOT[1] 、GOT[2]初始化后的状态:

 

图 28

查看GOT[2],确实指向ld-2.30.so内的链接器函数:

 

图 29

 

图 30

如图,不使用参数运行hello,程序在输出一条文本后就结束。由于printf没有格式化参数,第14行代码被编译器优化为puts。对应的汇编代码如下:

 

图 31

如图31,在puts语句处的call目标被替换为调用.plt节内的一个小函数。

 

 

 

程序在执行到00007f95:0ef49a99处的函数调用时,将puts对应的GOT条目更新为真正的puts函数入口地址:

 

对应puts函数代码如下:

 

在入口处设置INT3断点,让程序恢复运行,暂停后查看RIP寄存器,符号结果印证了我们的结论,GOT[3]就是真正的puts函数在内存中的地址:

 

由于动态链接库可以被加载到内存中的任意位置,所以在真正运行前,程序是不可能知道puts函数入口的真正虚拟地址的。为了能够进入puts函数,程序必须在函数调用前将其地址动态解析到全局偏移量表内(GOT),为了避免不必要的符号解析、加速程序启动,部分符号是惰性解析的,也就是直到第一次调用才解析这个符号。在我们的例子中,puts函数被惰性解析了。

5.8 本章小结

本章深入hello程序运行的汇编级细节,发现了许多表面上不曾观察到的惊人细节:hello程序可谓是麻雀虽小五脏俱全,它的内存映像具有与大型程序一样的格式:有ELF头、有数据段、代码段,段内还有存放全局变量和GOT的.data节、存放代码的.text节、存放PLT的.plt节……在程序加载时,加载器需要调用动态链接器ld-linux.so(我们这里是ld-2.30.so),动态链接器加载hello将要用到的(几乎)所有动态链接库,调用libc_start_main函数,最终调用我们C语言里编写的main函数。main函数调用的puts函数,实际上是puts@plt,后者通过PLT进行跳转,完成puts函数的动态符号解析。PLT和GOT的分工合作完成了相应的符号解析。相应GOT条目(GOT[2])没有被初始化,又跳转到初始化代码,动态解析puts的GOT条目,将真正的puts函数的入口地址写入GOT。main函数返回后,libc_start_main函数进行收尾工作,最终调用_exit函数来释放资源,hello才完成了它的一次运行。


第6章 hello进程管理

6.1 进程的概念与作用

概念:进程提供了一个计算机的抽象。进程就是程序的一个实例,这个实例拥有独立的虚拟地址空间,独立的时间片,对于进程自身来说,它好像独占整个CPU、独占整个内存地址空间。这样的抽象是由操作系统内核的进程管理模块实现的。

作用:进程给程序开发提供了一个简单的模型,允许在一个CPU上同时运行多个程序,并将这些程序的数据隔离成独立的虚拟地址空间,减小了程序因冲突而出错的机率,为动态链接提供了一个平台,开发人员无需考虑其他程序对本程序的影响,因为进程的虚拟内存之间相互隔离。

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

作用:Bash是一种Linux Shell,提供了一个字符型人机交互界面(CLI),允许用户借助Shell运行应用程序或者调用Shell的内置命令。

处理流程:

a)       Shell从标准输入读取字符串,获取用户输入的命令

b)      判断用户输入的命令是否为内置命令,如果是,跳到c)执行,否则,跳到d)执行

c)       解析内置命令,并执行

d)      将用户输入的命令视作外部可执行文件,判断该文件是否存在。如果不存在,报错;否则继续执行e)

e)       调用fork系统函数,创建一份Shell进程的子进程。在子进程中,调用execve,将指定的可执行文件加载到本进程的虚拟地址空间、替换子Shell进程执行。

f)        父进程Shell判断用户是否要将其置为后台进程(结尾是否有&符),如果是则跳到a),继续请求下一条输入;否则等待此进程结束。在等待过程中,如果子进程的运行状态改变(如:进入后台、暂停、被中断执行),向标准输出写入提示信息。当此进程正常退出时,释放它的资源,跳到a),请求下一条输入。

6.3 Hello的fork进程创建过程

当Shell判断hello不是一个内置命令、并且它确实存在与磁盘上时,它首先会创建一个自身进程的副本作为子进程,这个操作依靠fork实现。fork创建一个与自身进程几乎完全一样的副本子进程,子进程的虚拟地址空间与父进程完全一致,两者共享一份共享内存页面,私有页面被标记为写时复制,只有当子进程对其修改时才真正在内存中复制。同时,子进程还获得了父进程的所有文件描述符,因此子进程可以读写父进程打开的任何文件,包括标准输入、标准输出。子进程和父进程拥有独立的虚拟地址空间、PID、时间片,拥有共同的进程组,它们在操作系统层面上是两个独立的进程。图32为这个过程的拓扑图:

 

图 32

6.4 Hello的execve过程

调用的fork函数会在父进程、子进程中各返回一次,在父进程的返回值为子进程的PID,在子进程中的返回值为0。进程发现自己是子进程时,则会调用execve函数加载将要执行的可执行目标文件。execve在当前进程的上下文加载并运行hello程序,具体操作如下:

  1. 删除子进程现有的虚拟内存段。
  2. 创建新的代码段、数据段、堆、用户栈的虚拟内存段和虚拟内存页。
  3. 将hello的数据段、代码段映射到合适的位置。
  4. 将RIP设置到ld-linux.so动态链接器的链接函数。

动态链接器进行如下操作:

  1. 从.interp段中读取需要加载的动态链接库。
  2. 将所需目标文件以共享方式映射到虚拟内存中的共享库区域。
  3. 调用函数__libc_start_main,后者最终将调用main()函数

6.5 Hello的进程执行

进程上下文:每个进程有一个当前的执行环境,叫做上下文。进程的执行被打断后,必须将上下文恢复到打断之前的内容,进程才能继续执行。进程的上下文包括通用目的寄存器、浮点寄存器、程序计数器(RIP)、状态寄存器(RFLAGS)、进程的全局页目录指针(GPD,一级页表指针,存放在CR3寄存器内)、内核栈、私有的内核数据结构(如mm_struct,task_struct)。

进程时间片:一个CPU上运行多个进程,是通过进程间的快速切换实现的,称作进程调度。每个进程都被分配了一小段时间,在这段时间内占用CPU,如果时间用尽时进程还在运行,内核会抢占这个进程,保存它的上下文,切换到另一个进程执行。

用户模式与内核模式的切换:进程调度可以被系统调用、IO操作、计时器中断触发。异常处理程序常在内核模式下执行,使用内核栈。从进程A切换到进程B时,内核会代表进程A在用户模式下执行一段时间,然后切换到内核模式执行一段时间,接着切换到进程B,并在内核模式下执行一段时间,最后在进程B下以用户模式执行。整个过程中,内核总是代表某个进程执行,而不存在独立的内核进程。图33为《深入理解计算机系统》一书中描述进程切换的插图。

 

图 33

6.6 hello的异常与信号处理

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

 hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

 程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

异常可分为四类:中断、陷阱、故障和终止。

  1. 缺页异常:CPU第一次访问堆栈、第一次访问代码时,对应的页还没有被加载到物理内存中。这时,MMU会触发缺页保护故障,调用操作系统的缺页处理子程序,后者最终将所需的页面读入物理内存,重启刚才的指令。
  2. 硬件计时器中断:这个计时器信号是周期性的,用来支持进程的时间片轮转,防止一个进程占用CPU过久。这个计时器信号触发的异常处理子程序会调用进程管理模块的进程切换操作,保存当前的上下文、恢复下一个进程的上下文,并将控制权交给下一个进程。
  3. I/O中断:当用户按下键盘的按键时,会触发一个中断,程序此时可以针对用户的输入做出处理。
  4. SIGTSTP:用户按下了Ctrl-Z组合键。此时进程会收到SIGTSTP信号,被暂停执行,内核不再为其分配时间片,处于stopped状态。
  5. SIGINT:用户按下了Ctrl-C组合键。此时进程会收到SIGINT信号,进程收到信号后会退出。

 

6.7本章小结

本章介绍了hello被Shell加载的过程,以及Shell运行时通过信号与Shell、与用户、与内核通信的原理。用户通过特殊的组合键向前台进程hello发送信号,hello收到信号后改变自身的运行状态;父进程Shell检测到子进程hello进程变化后,做出相应操作,如输出提示信息。hello被挂起后,进程仍然存在,资源仍未被释放,只不过处于挂起状态,不再被分配CPU时间,因而表现为暂停执行。hello在终止执行后,需要由父进程Shell进行回收,在回收前,hello处于僵死状态(zombie),仍然占用系统资源。


第7章 hello的存储管理

7.1 hello的存储器地址空间

线性地址空间:线性地址空间是连续的地址空间,如hello的整个虚拟地址空间。

逻辑地址:相对于段寻址的地址,分为段基址和段偏移量两部分。

物理地址:计算机物理内存上存储单元的硬件地址,与虚拟地址相对,在实模式下访问内存使用的地址。hello中无法直接使用物理地址寻址,因为此时CPU处于保护模式中,hello只能访问自己进程的地址空间中的非内核页面。

虚拟地址:相对于每个进程私有的、独占的虚拟地址空间的地址,在保护模式下访问内存使用的地址。虚拟地址空间保存在磁盘中。连续的虚拟地址空间缓存在离散的物理内存页中,经由页表映射而来。hello程序中涉及到的所有内存地址均为虚拟地址。

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

段式管理下,地址表示形式为:段地址:段偏移地址

段式寻址为历史遗留问题。8086 CPU使用这种方式来访问大于64K的内存。物理地址=段地址*16+段偏移地址。

在80286中,引进了保护模式的概念,段寄存器改名为段选择子。操作系统初始化GDT和LDT两个描述符表,应用程序在访问内存时,需要使用段选择子在GDT或LDT内查询段基址,再加上段偏移地址,即得到物理地址。保护模式下,应用程序使用的内存段相互隔离,因此安全性大大提高。

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

虚拟内存被视为一个数组,储存在磁盘上的一个文件内。虚拟内存中的每个字节都和虚拟地址空间内的一个虚拟地址一一对应,也就是数组的索引。文件内数组的内容被缓存在内存中,磁盘上的数据被分割成虚拟页,这些虚拟页和高速缓存中的块类似,是磁盘和内存之间的传输单元。虚拟页的大小和物理页的大小相同。如图34。

 

图 34

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

64位虚拟地址空间可以寻址的范围非常大,现在最新的64位Linux操作系统支持48位虚拟地址空间,可以寻址256TB内存空间。对应这么大的空间,需要一张很大的页表。事实上,现有的程序只能使用其中很小的一部分,所以页表的很多空间实际上被浪费掉了。我们只需要保存页表的很小一部分,这就是多级页表出现的原因。

在48位虚拟内存地址、四级页表的虚拟内存系统下,虚拟地址的高36位被分成4个9bit,当作四级页表的四个虚拟页号(VPN)。最低的12bit为最后一级虚拟页上的偏移(VPO)。这样,每张页表大小都为4KB,由于程序具有局部性,访问的页表条目(PTE)可以被缓存在TLB中,可以加速多级页表的查询,使得其并不比单级页表慢很多。如图35。

 

图 35

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

较快的存储器往往更贵而且更小,为了平衡成本和速度,存储器被组织成存储器层次结构,经常用到的数据被存储在级别更高、速度更快的存储器中,相比之下使用频率更小的数据被存储在级别更低、容量更大的存储器中。

随着微电子技术的进步,CPU与内存的速度差距逐渐增大,高速缓存应运而生,被用来弥补这个差距。CPU在读写内存时,会试图先从高速缓存中读写数据。如果这个数据在高速缓存中,则直接返回;否则,就向下一级高速缓存中查询。现代Intel Core i7 CPU具有三级高速缓存,如果在这三级中都未命中,则只能从内存中读取相应数据,再从高速缓存中驱逐一个合适的行,将读取的高速缓存行写入缓存;如果是向内存写入数据的情况,上述流程类似,只不过在写回机制下,向内存的写入要被推迟到这一行被驱逐出高速缓存时才进行,这样可以减小数据总线中的数据流量。

由于高速缓存需要从硬件层面并行查找多组数据,相联度高的缓存制造起来很困难,所以常用的高速缓存为组相联高速缓存:物理地址被分割为标志块、组索引和块偏移三部分,组索引相同的地址被映射到同一个组中,标志块用于确认该高速缓存行是否匹配,块偏移用于确定所需的字节的具体位置。

7.6 hello进程fork时的内存映射

当fork函数创建Shell进程的一个副本时,它并不将Shell的虚拟页全部复制一份,相反,共享页被标记为共享的,在物理内存中只有一份页面;私有页被标记为写时复制的,也就是说,在子进程修改它的私有页前,两个进程共享同一个页面,直到修改时才创建独立的副本。这样更加节省物理内存。如图36。

 

图 36

7.7 hello进程execve时的内存映射

execve函数调用内核的启动加载器代码,在当前进程中清除已有的虚拟内存页、加载目标可执行文件的数据段、代码段到虚拟地址空间,用hello程序代替当前程序,并将RIP设置为动态链接器或者.init段中定义的入口函数。详细步骤如下:

  1. 删除已存在的用户区域、当前进程的用户地址空间中已存在的区域。
  2. 映射私有区域。将可执行文件中适当位置映射到只读代码段和读写段中的.data节,将.bss节映射到适当大小的匿名文件(即请求二进制零),将堆、用户栈映射到初始长度为零的匿名文件。
  3. 映射共享区域。动态链接器将共享对象加载到虚拟内存中的共享区域内。
  4. 设置RIP,将其指向到代码区域的入口函数。

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

缺页故障:当CPU试图访问一个没有缓存在物理内存中的页面时,MMU会触发一个保护故障,即缺页故障。缺页异常处理程序会从物理内存中选择一个适当的牺牲页,将这个页写回到磁盘,然后将需要的页面从磁盘中载入内存,CPU重启引起缺页的指令,MMU正常翻译并返回物理地址。

7.9动态存储分配管理

动态内存分配器从堆中分配内存块,提供给应用程序使用。堆是虚拟内存中的一个区域,分配器使用某种策略在堆上分配内存块。块在使用完毕后必须释放的内存分配器,称作显示分配器;会自动检测块是否不再使用的分配器,称作隐式分配器。隐式分配器自动释放不再可用的块的过程称作垃圾收集。

下面介绍带边界标签的隐式空闲链表分配器:

 

块的头部和尾部完全相同,尾部的作用是从它下面的块定位这个块。堆常见的管理方式有隐式空闲链表和显式空闲链表。由于块在堆中顺序分布,所以块可以组织为一个链表,其中夹杂着空闲块和已分配块;如果跳过已分配块,则可以以O(n)的复杂度遍历所有的空闲块,这种组织方式成为隐式空闲链表。

事实上,我们只需要知道空闲块相关的信息,并不关心一个已分配块的位置和大小。所以,我们可以在有效载荷部分重叠加入一个指针,这个指针将空闲块串联成一个新的链表,称作显式空闲链表。遍历显式空闲链表比遍历隐式空闲链表要快很多,因为已分配块不在链表中。如果将空闲块按照大小排序,并按大小分类,组织多个表头,则可以进一步减小搜索空闲块的时间。这种组织方式称作分离适配。

GCC的动态内存分配器使用分离适配模式。

7.10本章小结

本章介绍了hello的内存管理机制、段式内存管理、虚拟内存、多级页表、进程加载时fork和execve函数的处理流程、缺页处理机制和应用级内存块管理:动态内存分配器。可见,hello要想执行,还离不开完善的虚拟内存机制、进程管理机制和动态内存分配机制。


第8章 hello的IO管理

8.1 Linux的IO设备管理方法

I/O设备可以被抽象为文件,也就是一个字符序列,可以连续地输入、输出字符串。应用程序与设备的通信被内核抽象为Unix I/O函数,借助这组函数,内核通过驱动程序与设备通信,应用程序间接地操作设备。

8.2 简述Unix IO接口及其函数

  1. int open(char *filename, int flags, mode_t mode);

功能:打开指定的文件,存储在全局的文件表中,并返回一个文件描述符。返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件, mode参数指定了新文件的访问权限位。

  1. int close(int fd);

关闭一个打开的文件。

  1. ssize_t read(int fd, void *buf, size_t n);read函数从描述符为 fd的当前文件位置赋值最多 n个字节到内存位置buf。返回值 -1表示一个错误, 0表示 EOF,否则返回值表示的是实际传送的字节数量 。
  2. ssize_t write(int fd, const void *buf,size_t);

write函数从内存位置 buf复制至多 n个字节到描述符 fd的当前文件位置。

8.3 printf的实现分析

printf函数读取可变长参数表,调用vsprintf函数将字符串按照指定的模式格式化,并写入内存中的缓冲区。返回值为要输出字符串的函数。

要将最终的字符串写入标准输出文件,需要使用write系统调用。write的第一个参数为文件描述符,1为每个进程的标准输出文件,所以这里传递1;第二个参数为缓冲区的虚拟地址,第三个参数为缓冲区的长度。调用write系统调用后,hello程序陷入内核,内核将缓冲区的数据写入文字缓冲区(如果显示器工作在文字模式下),显示卡对每一个字符查找ASCII字型库,在视频内存(VRAM)中写入对应字符的形状,显示芯片按照刷新率读取视频内存(VRAM),以合适的编码通过视频线发送到显示器,后者在屏幕上显示最终的符号。

8.4 getchar的实现分析

当用户按下某个按键时,键盘里的芯片会将键编码为一个键盘扫描码,计算机IO接口接收到扫描码后会产生一个中断请求,调用键盘中断处理子程序,键盘中断子程序从键盘IO取得该按键的扫描码,然后将该按键扫描码转换成 ASCII码,保存到系统的键盘缓冲区中。键盘缓冲区被组织为标准输入文件,应用程序调用标准I/O函数,从这个文件中读取字符,即可获得键盘输入的字符。getchar函数的实现如下:

int getchar(void)

{

    static char buf[BUFSIZ];

    static char* bb=buf;

    static int n=0;

    if(n==0)

    {

        n=read(0,buf,BUFSIZ);

        bb=buf;

    }

    return(--n>=0)?(unsigned char)*bb++:EOF;

}

可见,getchar函数循环地从标准输入文件(文件描述符为0)中读取字符,直到读取到一个有效字符,将字符返回给用户。

8.5本章小结

本章介绍了Linux下I/O设备的抽象形式:文件。Unix还提供了配套的文件I/O函数,可以用统一的方法,读写各种各样的设备,简化了应用程序的开发设计,并且更加安全。

结论

hello的一生,短暂而又精彩:

  1. hello.c经过预处理器,得到预处理后的文件hello.i
  2. 编译器将hello.i编译为更接近机器语言的汇编代码hello.s
  3. 汇编器将hello.s翻译为机器指令,得到可重定位目标文件hello.o
  4. 链接器将hello.o与其他库函数的可重定位目标文件、存档文件进行链接,组合、重定位为一个可以直接加载到内存映像中的可执行目标文件hello
  5. 用户在Shell中键入命令,执行hello
  6. hello.o由Shell的帮助,在fork产生的Shell子进程中由execve映射相应段到虚拟内存
  7. execve加载动态链接器,后者将动态链接库映射到虚拟内存,并进行一些动态链接工作、调用hello的入口函数
  8. 第一次访问hello的代码段时,MMU产生一个缺页保护故障,调用缺页故障处理子程序,将代码页载入物理内存
  9. hello访问内存中的数据,CPU向L1 Cache发送请求,L1 Cache中发生冷不命中,请求经由L1 Cache、L2 Cache、L3 Cache层层传递,最终到达物理内存,从内存中读取一个缓存行,刷新各级缓存,返回读取的数据……
  10. hello的入口函数最终调用main函数,我们的代码终于可以在机器上执行了
  11. hello的运行状态改变时,内核会以信号的形式通知它的父进程:Shell。Shell接收信号,得知hello的运行状态已改变,从而做出相应处理。

计算机是一个设计精密而复杂的电子机器。从底层的微操作实现,到高层的大型应用程序的运行,需要许多复杂的层次结构相互协调运作,各个部件环环相扣,借助各种技巧,如高速缓存、流水线,突破工艺及技术所造成的物理限制,达到尽可能高的计算性能。同时,虚拟内存、基于编译器和解释器的高级程序设计语言、标准I/O设备接口等技术也极大地降低了开发难度,使得人们能够更方便地使用计算机,方便我们的生活。


附件

所有的中间产物:

l  hello.i:由hello.c,经C预处理器处理得到的预处理代码文件。

l  hello.s:由hello.i,经汇编器汇编得到的汇编代码文件。

l  hello.o:由hello.s,经编译器翻译得到的可重定位目标文件。

l  hello:由hello.o及相关动态链接库,经链接器链接得到的可执行目标文件。

l  hello1.s:由hello.o,经objdump反汇编得到的反汇编结果。

l  hello2.s:由hello,经objdump反汇编得到的反汇编结果。


参考文献

[1]  深入理解计算机系统 [M] Randal E. Bryant, David R. O’Hallaron

[2]  Linux Standard Base Specification 1.2 Chapter 4. Special Sections https://refspecs.linuxfoundation.org/LSB_1.2.0/gLSB/specialsections.html

[3]  What is the difference between .got and .got.plt section in ELF format? https://stackoverflow.com/questions/11676472/what-is-the-difference-between-got-and-got-plt-section

[4]  80286与保护模式 https://zhuanlan.zhihu.com/p/27401519

[5]  printf 函数实现的深入剖析 https://www.cnblogs.com/pianist/p/3315801.html

猜你喜欢

转载自www.cnblogs.com/keuin/p/hitics2019.html