马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
摘 要
本文围绕经典的Hello步伐,详细阐明了它在Linux操作系统环境下的完整生命周期。从源代码hello.c文件开始,我们跟踪分析了它经历的预处置惩罚、编译、汇编、链接,一直到最终实行和结束终止的全过程。同时,我们结合操作系统课程中学习的知识,详细解释了Linux系统如何对Hello步伐实施管理和控制。在进程管理方面,系统如何创建、调度和终止Hello步伐进程。在存储管理方面,系统如何为Hello步伐分配内存空间,加载其代码和数据。在I/O管理方面,系统如那边理Hello步伐的输入与输出等。全文内容系统而全面地回首和梳理了CSAPP课程学习的所有焦点知识点。
关键词:计算机系统;计算机体系结构;操作系统管理;计算机系统课程;
目 录
第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 本章小结................................................................................................. - 8 -
第3章 编译..................................................................................................... - 9 -
3.1 编译的概念与作用................................................................................. - 9 -
3.2 在Ubuntu下编译的命令..................................................................... - 9 -
3.3 Hello的编译结果解析.......................................................................... - 9 -
3.4 本章小结..................................................................... 错误!未定义书签。
第4章 汇编................................................................................................... - 18 -
4.1 汇编的概念与作用............................................................................... - 18 -
4.2 在Ubuntu下汇编的命令................................................................... - 18 -
4.3 可重定位目标elf格式....................................................................... - 18 -
4.4 Hello.o的结果解析............................................................................ - 20 -
4.5 本章小结............................................................................................... - 21 -
第5章 链接................................................................................................... - 22 -
5.1 链接的概念与作用............................................................................... - 22 -
5.2 在Ubuntu下链接的命令................................................................... - 22 -
5.3 可实行目标文件hello的格式.......................................................... - 22 -
5.4 hello的虚拟地址空间........................................................................ - 25 -
5.5 链接的重定位过程分析....................................................................... - 26 -
5.6 hello的实行流程................................................................................ - 28 -
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的非常与信号处置惩罚.................................................................... - 33 -
6.7本章小结............................................................................................... - 37 -
第7章 hello的存储管理....................................................................... - 38 -
7.1 hello的存储器地址空间.................................................................... - 38 -
7.2 Intel逻辑地址到线性地址的变换-段式管理.................................... - 38 -
7.3 Hello的线性地址到物理地址的变换-页式管理.............................. - 39 -
7.4 TLB与四级页表支持下的VA到PA的变换..................................... - 39 -
7.5 三级Cache支持下的物理内存访问.................................................. - 41 -
7.6 hello进程fork时的内存映射.......................................................... - 42 -
7.7 hello进程execve时的内存映射...................................................... - 42 -
7.8 缺页故障与缺页停止处置惩罚................................................................... - 43 -
7.9本章小结............................................................................................... - 43 -
结论................................................................................................................. - 44 -
附件................................................................................................................. - 45 -
参考文献......................................................................................................... - 46 -
第1章 概述
1.1 Hello简介
Hello的P2P(From Program to Process)是指,Hello步伐从可实行步伐(program)变为运行时进程(process)的过程。hello.c文件先后经过预处置惩罚、编译、汇编和链接四个阶段,最终天生可实行目标步伐hello。它在Linux系统中的转变过程具体为:预处置惩罚阶段通过cpp处置惩罚hello.c;编译阶段通过ccl将预处置惩罚结果编译为汇编代码;汇编阶段通过as将汇编代码转为目标文件;链接阶段通过ld将目标文件和库文件链接天生可实行目标步伐hello。这就是“P2P” 的完整过程。
Hello的020(From Zero to Zero)是指,初始时内存中没有Hello步伐的任何内容(from zero),当我们在Shell中输入./hello命令启动Hello步伐时,系统通过fork创建Hello步伐进程,然后通过execve系统调用将Hello步伐载入内存,开始实行相关代码。 Hello步伐运行结束后,系统回收Hello步伐进程并删除内存中的Hello步伐数据。这标志着Hello步伐回到了开始的“零”状态,其生命周期结束(to zero)。 总之,Hello步伐以“零”状态开始,经历运行期的高峰,最后再次回到“零”状态结束。这就是“020”的完整生命历程。
1.2 环境与工具
硬件:
CPU: Intel(R) Core(TM) i7-10870H CPU @ 2.20GHz 2.21 GHz
RAM:32.0 GB (31.8 GB 可用)
软件:
Windows10 64位
Oracle VM VirtualBox 6.1.16 r140961 (Qt5.6.2)
Ubuntu 18.04.5 LTS 64位
调试工具:
Visual Studio Code;
gedit,gcc,notepad++,readelf, objdump, hexedit,edb
1.3 中央结果
列出你为编写本论文,天生的中央结果文件的名字,文件的作用等。
表格1 中央结果
文件名称
| 说明
| hello.i
| hello.c经预处置惩罚得到的ASCII文本文件
| hello.s
| hello.i经编译得到的汇编代码ASCII文本文件
| hello.o
| hello.s经汇编得到的可重定位目标文件
| hello_elf.txt
| hello.o经readelf分析得到的文本文件
| hello_dis.txt
| hello.o经objdump反汇编得到的文本文件
| hello
| hello.o经链接得到的可实行文件
| hello1_elf.txt
| hello经readelf分析得到的文本文件
| hello1_dis.txt
| hello经objdump反汇编得到的文本文件
| 表 1
1.4 本章小结
本章简要先容了hello步伐 的P2P,020的具体含义,同时列出了论文研究时采用的具体软硬件环境、中央结果。
第2章 预处置惩罚
2.1 预处置惩罚的概念与作用
2.1.1 预处置惩罚的概念
预处置惩罚是编译过程的第一个阶段,它的重要功能是处置惩罚源代码文件中以“#”开始的预处置惩罚指令。预处置惩罚指令用于在实际编译之前修改源代码,好比说导入头文件、定义宏等。预处置惩罚器是一种处置惩罚文本的步伐,它直接处置惩罚源代码文件中的预处置惩罚指令,然后产生已经处置惩罚过的源代码,这些代码再由编译器编译成目标代码。即,预处置惩罚实际上是在编译之前对源代码进行的一种处置惩罚。除此之外,预处置惩罚过程还会删除步伐中的注释和多余的空缺字符。预处置惩罚通常得到另一个以.i作为拓展名的C步伐。
2.2.2预处置惩罚的作用
2.2在Ubuntu下预处置惩罚的命令
在Ubuntu系统下,进行预处置惩罚的命令为:
cpp hello.c > hello.i
运行截图如下:
图0 命令截图
2.3 Hello的预处置惩罚结果解析
在Ubuntu系统下打开hello.i:
图 1 hello.i文件部分截图
可以发现:本来28行的hello.c经预处置惩罚后,扩展成了共3105行的hello.i文件。此中:main函数主步伐在3092行至3105行出现:
图 2 main函数在hello.i文件中位置
在main函数内代码出现之前,头文件 stdio.h、unistd.h 和 stdlib.h 会依次睁开。以stdio.h为例,CPP会先删除指令#include<stdio.h>,并在Ubuntu系统的默认环境变量中探求 stdio.h。最终,它会打开路径/usr/include/stdio.h下的stdio.h文件。若stdio.h文件中利用了#define语句,则按照上述流程继承递归地睁开,直到所有#define语句都被解释替换掉为止。此外,CPP还会删除步伐中的注释和多余的空缺字符,并对一些值进行替换。stdio.h等头文件的路径截图如下所示:
图 3 路径截图
Stdio.h等在计算机中路径截图
2.4 本章小结
本章重要先容了预处置惩罚的概念及作用,并结合Ubuntu系统下hello.c文件实际预处置惩罚之后得到的hello.i步伐对预处置惩罚结果进行相识析。C语言预处置惩罚一样平常由预处置惩罚器(cpp)进行,它重要完成四项工作:宏睁开、文件包含复制、条件编译处置惩罚和删除注释及多余空缺字符。这些工作为之后的编译等流程奠基了底子。
第3章 编译
3.1 编译的概念与作用
3.1.1 编译的概念
编译是指C编译器ccl通过词法分析和语法分析,将合法指令翻译成等价汇编代码的过程。在这个过程中,编译器将文本文件 hello.i 翻译成汇编语言文件 hello.s。hello.s中以文本的形式描述了一条条低级呆板语言指令。
3.1.2 编译的作用
编译的作用将高级语言源代码转换为计算性能够直接实行的呆板语言。编译器通过一系列步调,如词法分析、语法分析、中央代码天生、代码优化和目标代码天生,将源代码转换为更靠近呆板语言的形式。即,编译的作用是通过一系列步调让源代码更靠近呆板语言,它是汇编阶段翻译成呆板语言的前提。
3.2 在Ubuntu下编译的命令
命令为:gcc -S hello.i -o hello.s
图 4 编译命令截图
3.3 Hello的编译结果解析
此部分是重点,说明编译器是怎么处置惩罚C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
hello.s文件部分截图如下所示:
图 5 hello.s 文件部分截图
图 6 文件结构部分截图
对hello.s文件整体结构分析如下表:
内容
| 含义
| .file
| 源文件
| .text
| 代码段
| .global
| 全局变量
| .data
| 存放已经初始化的全局和静态C 变量
| .section .rodata
| 存放只读变量
| .align
| 对齐方式
| .type
| 表现是函数类型/对象类型
| .size
| 表现大小
| .long .string
| 表现是long类型/string类型
| 表 2 文件结构表
在hello.s中,涉及的数据类型包罗以下三种:整数,字符串,数组。下面对每种数据类型依次进行分析。
在原hello.c中,多次出现整数常量(立即数),
图 7 代码中立即数示意
在hello.s中,可以看到涉及的整数常量被直接插入汇编代码,不需寻址。
以立即数32为例,这就是将栈增长了4个存储单元,为后续储值做预备:
图 8 立即数举例
原hello.c中共输出两个字符串:
图 9 原代码中字符串示意
可以看到,它们被存放于.rodata中。\XXX为UTF-8编码,一个汉字对应三个字节:
图 10 字符串在hello.s中位置示意
步伐中涉及的数组为char *argv[],即函数的第二个参数。在hello.s中,其首地址生存在栈中。访问时,通过寄存器寻址的方式访问。
图 11 数组在hello.s中示意
此中,20~22行是先将栈拓展出4个存储单元,然后将edi、rsi中数据存放进栈中;35行是通过寄存器寻址访问(rdx是栈帧指针,指向栈帧底部)。
(1)int sleepsecs=2.5
在C语言源步伐中,将2.5赋值给一个int类型变量会导致隐式类型转换,结果为2。在hello.s文件中,这一转换体如今将sleepsecs声明为值为2的long类型数据,位于.data节中。
(2)int i
对局部变量的赋值在汇编代码中通过mov指令完成。具体利用哪条mov指令由数据的大小决定,如图所示:
后缀
| b
| w
| l
| q
| 大小(字节)
| 1
| 2
| 3
| 4
| 表 3 后缀说明
常量类型在3.3.1中已作详细叙述,本小节讨论变量类型。
这个函数里声明了一个int类型局部变量 i。局部变量在寄存器或栈中储存。i被分配在栈中,大小是四个字节,刚幸亏栈最底部。i在hello.s中如下图:
图 12 局部变量在hello.s中示意图
这个函数没有利用全局变量。已初始化的全局变量存放在.data节中,它们在文件中占据空间,并在步伐加载时被加载到内存中。未初始化的全局变量则存放在.bss节中,它们在文件中不占据空间,而是在利用时在内存中分配并初始化为0。另:静态变量的处置惩罚方式与全局变量相同。
Hello.c中并没有进行显式类型转换,但是存在隐式类型转换。
hello.c中利用了atoi函数,该函数作用是把一个数字字符串转换为对应的整数。通过调用函数传参实现类型转换:
图 13 atoi函数实现隐式类型转换
在汇编指令中,算数操作可以达到多种目标。既可以对数进行加减乘除的操作,也可以将地址进行运算并传入另一地址。算数操作指令重要包罗:
指令
| 效果
| leaq s,d
| d=&s
| inc d
| d+=1
| dec d
| d-=1
| neg d
| d=-d
| add s,d
| d=d+s
| sub s,d
| d=d-s
| imulq s
| r[%rdx]:r[%rax]=s*r[%rax](有符号)
| mulq s
| r[%rdx]:r[%rax]=s*r[%rax](无符号)
| idivq s
| r[%rdx]=r[%rdx]:r[%rax] mod s(有符号) r[%rax]=r[%rdx]:r[%rax] div s
| divq s
| r[%rdx]=r[%rdx]:r[%rax] mod s(无符号) r[%rax]=r[%rdx]:r[%rax] div s
| 表 4 汇编指令中的算数操作
而hello.s中设计的算数操作有:
- subq $32, %rsp //开辟main函数栈帧
- addq $16, %rax //数组寻址
- addq $8, %rax //数组寻址
- leaq .LC1(%rip), %rdi //取地址操作
- addl $1, -4(%rbp) //i++
在hello.s中,具体涉及的关系操作包罗:
1. argc!=4
图 14 指令示意
argc!=4对应的汇编代码
此处-20(%rbp)即存放argc的地址。根据关系式的结果,会设置条件码的值,后续根据条件码的值来进行控制跳转。
图 15 指令示意
此处-4(%rbp)即存放i的地址。当i<=7时,进行跳转。
数组操作在3.3.1中进行了详细叙述。
在hello.s中,具体涉及的关系操作包罗:
图 16 源代码中if()结构截图
可以看到,该处if()结构是判断argc的值是否为4,如果不为4,则输出"用法: Hello 学号 姓名 秒数!\n"。
在汇编代码中,此处对应的是:
图 17 hello.c中if()结构截图
通过argc与4做比力来设置条件码,然后je指令读取条件码,若相称则前往实行.L2节,若不相称则继承次序实行。
图 18 源代码中for()循环结构截图
可以看到,这个for()循环结构是判断i的值是否小于8,若小于则实行printf和sleep操作并使i自增,若大于即是则跳出循环。
在汇编代码中,此处对应的是:
图 19 汇编代码中for()循环截图
通过比力i与7的大小来设置条件码,然后根据结果来决定是否跳转。当i=8时,结束循环,实行之后的指令。
在该代码中,步伐入口处,调用了main 函数,其在hello.s中标注为@function函数类型。之后又调用 puts,printf,sleep,exit,getchar 函数,对函数的调用都通过call指令进行。
一样平常来说,调用函数时进行的操作有:
操作
| 作用
| 传递控制
| 进行过程 Q 的时间,PC必须设置为 Q 的代码的起始地址,然后在返回时,要把PC设置为 P 中调用 Q 后面那条指令(Q+1)的地址。
| 传递数据
| P 必须能够向 Q 提供一个或多个参数,Q 必须能够向 P 中返回一个值。
| 分配和释放内存
| 在开始时,Q 大概需要为局部变量分配空间,而在返回前,又必须释放这些空间
| 表 5 调用函数时需进行的操作
可以发现,在hello.s中,第一处printf被优化为puts:
图 20 puts函数截图
第二处printf函数调用为:
图 21 printf函数截图
Printf的参数数量可变,在这里只需要用到一个参数,即一个字符串常量,于是将其地址放入%rdi中,向被调用函数传递,然后通过call指令来调用puts函数。
在hello.s中,调用exit函数的代码如图所示:
图 22 exit函数截图
利用寄存器EDI传递参数(整数值1),调用exit()函数以状态1退出。
3.4 本章小结
本章先容了编译的概念与作用。编译阶段分析查抄源步伐,确认所有的语句都符合语法规则后将其翻译成等价的汇编代码(中央代码)表现。它为后续将步伐转化为二进制呆板码做预备。本章以hello.s文件为例,先容了编译器如那边理各种数据类型和操作,并验证了大部分数据和操作在汇编代码中的实现方式。
第4章 汇编
4.1 汇编的概念与作用
4.4.1 汇编的概念
汇编是指汇编器(assembler)将汇编语言步伐(如hello.s)翻译成呆板语言指令,并将这些指令打包成可重定位目标文件(如hello.o)的过程。hello.o是一个二进制编码文件,它包含步伐的呆板指令编码。汇编器可以通过直接运行或驱动步伐运行来完成这一过程。
汇编的作用是:将汇编语言翻译为呆板语言,并将相关指令以可重定位目标步伐格式生存在.o文件中。
4.2 在Ubuntu下汇编的命令
在Ubuntu下汇编的命令为:
图 23 汇编命令截图
4.3 可重定位目标elf格式
首先,在shell中输入readelf -a hello.o > hello.elf 指令获得 hello.o 文件的 ELF 格式:
图 24 hello.elf在文件夹中截图
下面对hello.elf进行分析:
图 25 ELF头
ELF头以 16字节序列 Magic 开始,描述了天生该文件的系统的字的大小和字节次序,剩下的部分包罗帮助链接器语法分析和解释目标文件的信息,此中包罗 ELF 头大小、目标文件类型、呆板类型、节头部表的文件偏移,以及节头部表中条目标大小和数量等相关信息。
图 26 节头
节头叙述的是文件中出现的各个节的意义,包罗节的类型、位置和大小等信息。
图 27 重定位节
重定位节记录了各段引用的符号相关信息,在链接时,需要通过重定位节对这些位置的地址进行重定位。在原汇编代码中的地址在汇编后都会被赋予在步伐实际实行时所需的寄存器、内存地址等信息。链接器会通过重定位条目标类型判断如何计算地址值并利用偏移量等信息计算出正确的地址。
图 28 符号表
符号表(.symtab)生存着定位、重定位步伐中符号定义和引用的信息,即,符号表存放步伐中定义和引用的函数和全局变量的信息。
4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
利用命令:objdump -d -r hello.o 将hello.o反汇编:
图 29 反汇编指令
与hello.s对照,对于二者不同之处,分析如下表:
不同点
| 分析
| 分支转移
| 在hello.s中,跳转指令的目标地址直接记为段名称,如.L2,.L3等。
在hello.asm中,跳转的目标为具体的地址,在呆板代码中体现为目标指令地址与当前指令下一条指令(PC=PC+1)的地址之差。
| 函数调用
| 在hello.s文件中,call之后直接跟着函数名称。而在反汇编得到的hello.asm中,call 的目标地址是当前指令的下一条指令。
| 全局变量访问
| 在hello.s 文件中,利用段名称+%rip访问 rodata(printf 中的字符串)。
在hello.asm中,利用 0+%rip进行访问。
| 表 6 不同点
4.5 本章小结
本章详细先容了汇编的概念、作用、可重定向目标文件的结构和反汇编代码。在汇编阶段,汇编语言代码通过汇编器(assembler)汇编被转化为呆板语言,天生的可重定位目标文件(hello.o)为后续的链接阶段做好了预备。通过在Ubuntu下实际操作将hello.s文件翻译为hello.o文件,并天生hello.o的ELF格式文件hello.elf,我们研究了ELF格式文件的具体结构。通过比力hello.o的反汇编代码(生存在hello.asm中)与hello.s中的代码,我们相识了汇编语言与呆板语言的异同之处。完本钱章内容的过程加深了我们对汇编过程、ELF格式和重定位的明白。
第5章 链接
5.1 链接的概念与作用
5.1.1 链接的概念
链接是指通过链接器(Linker),将步伐编码与数据块网络并整理成为一个单一文件,天生完全链接的可实行的目标文件(windows系统下为.exe文件,Linux系统下一样平常省略后缀名)的过程。
5.1.2 链接的作用
提供了一种模块化的方式,可以将步伐编写为一个较小的源文件的聚集,且实现了分开编译更改源文件,从而淘汰整体文件的复杂度与大小,增长容错性,也方便对某一模块进行针对性修改。
5.2 在Ubuntu下链接的命令
利用ld的链接命令,具体命令为: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/7/crtbegin.o /usr/lib/gcc/x86_64-linux-gnu/7/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o hello.o -lc -z relro -o hello
图 30 毗连命令截图
5.3 可实行目标文件hello的格式
在Shell中输入命令 readelf -a hello > hello1.elf 天生 hello 步伐的 ELF 格式文件,生存为hello1.elf(与第四章中的elf文件作区分):
图 31 天生hello1.elf命令截图
分析hello1.elf文件:
图 32 ELF头
hello2.elf中的ELF头与hello.elf中的ELF头包含的信息种类基本相同,以 描述了天生该文件的系统的字的大小和字节次序的16字节序列 Magic 开始,剩下的部分包含帮助链接器语法分析和解释目标文件的信息。与hello.elf相比力,hello2.elf中的基本信息未发生改变(如Magic,类别等),而类型发生改变,步伐头大小和节头数量增长,而且获得了入口地址。
图 33 节头(部分截图)
hello2.elf中的节头包含了文件中出现的各个节的语义,包罗节的类型、位置、偏移量和大小等信息。与hello.elf相比,其在链接之后的内容更加丰富详细。
图 34 步伐头
步伐头部分是一个结构数组,描述了系统预备步伐实行所需的段或其他信息。
图 35 段到节的映射关系
这段代码显示了 ELF 文件中的段到节的映射关系。它列出了每个段中包含的节。
图 36 Dynamic section
图 37 symbol table部分截图
符号表中生存着定位、重定位步伐中符号定义和引用的信息,所有重定位需要引用的符号都在此中声明(此处仅截取部分展示)。
5.4 hello的虚拟地址空间
利用edb加载hello,如下图:
图 38 edb加载hello
图 39 data dump示意
由截图可知,步伐被载入至地址0x400000~0x401000中,在该地址范围内,每个节的地址都与前一节中节对应的 Address 相同。
根据edb查看的结果,在地址空间0x400000~0x400fff中存放着与地址空间0x400000~0x401000相同的步伐,在0x400fff之后存放的是.dynamic到.shstrtab节的内容。
5.5 链接的重定位过程分析
利用命令:objdump -d -r hello > hello2.asm。(与前hello.asm作区分)
天生的hello2.asm部分截图如下所示:
图 40 hello2.asm部分截图
结合hello.o的重定位项目,有如下分析:
- 可以发现,链接后函数数量增长。因为动态链接器将共享库中hello.c用到的函数加入可实行文件中,所以在hello2.asm中可看到.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等函数的代码。
图 41 函数在hello2.asm中代码
- 函数调用指令call的参数发生变化。在链接过程中,链接器解析了重定位条目,call之后的字节代码被链接器直接修改为目标地址与下一条指令的地址之差,指向相应的代码段,从而得到完整的反汇编代码。
图 42 call指令示意
- 跳转指令参数发生变化。在链接过程中,链接器解析了重定位条目,并计算相对距离,修改了对应位置的字节代码为PLT 中相应函数与下条指令的相对地址,从而得到完整的反汇编代码。
图 43 跳转指令示意
5.6 hello的实行流程
利用edb实行hello,列出其调用与跳转的各个子步伐名或步伐地址如下表:
步伐名称
| 步伐地址
| ld-2.27.so!_dl_start
| 0x7fce8cc38ea0
| ld-2.27.so!_dl_init
| 0x7fce8cc47630
| hello!_start
| 0x400500
| libc-2.27.so!__libc_start_main
| 0x7fce8c867ab0
| -libc-2.27.so!__cxa_atexit
| 0x7fce8c889430
| -libc-2.27.so!__libc_csu_init
| 0x4005c0
| hello!_init
| 0x400488
| libc-2.27.so!_setjmp
| 0x7fce8c884c10
| -libc-2.27.so!_sigsetjmp
| 0x7fce8c884b70
| --libc-2.27.so!__sigjmp_save
| 0x7fce8c884bd0
| hello!main
| 0x400532
| hello!puts@plt
| 0x4004b0
| hello!exit@plt
| 0x4004e0
| *hello!printf@plt
| --
| *hello!sleep@plt
| --
| *hello!getchar@plt
| --
| ld-2.27.so!_dl_runtime_resolve_xsave
| 0x7fce8cc4e680
| -ld-2.27.so!_dl_fixup
| 0x7fce8cc46df0
| --ld-2.27.so!_dl_lookup_symbol_x
| 0x7fce8cc420b0
| libc-2.27.so!exit
| 0x7fce8c889128
| 表 7
表7 子步伐名和步伐地址
5.7 Hello的动态链接分析
由于编译器无法预测函数的运行时地址,因此需要添加重定位记录并等候动态链接器处置惩罚。为了制止在运行时修改调用模块的代码段,链接器采用了耽误绑定策略。动态链接器利用过程链接表(PLT)和全局偏移量表(GOT)来实现函数的动态链接。GOT 中存储着函数的目标地址,而 PLT 则利用 GOT 中的地址跳转到目标函数。在加载时,动态链接器会重定位 GOT 中的每个条目,使其包含目标的正确绝对地址。
.got与.plt节生存着全局偏移量表GOT,其内容从地址0x601000开始。通过edb查看,在dl_init调用前,其内容如下:
图 44 调用前示意
调用后:
图 45 调用后示意
通过比力,我们可以发现,0x601008 到 0x601017 之间的内容发生了变化,这对应着全局偏移量表(GOT)中的 GOT[1] 和 GOT[2] 的内容。GOT[1] 生存着指向已加载共享库的链表地址,而 GOT[2] 则是动态链接器在 ld-linux.so 模块中的入口。因此,在接下来实行步伐的过程中,就可以利用过程链接表(PLT)和全局偏移量表(GOT)进举措态链接。
5.8 本章小结
本章围绕可重定位目标文件hello.o链接天生可实行目标文件hello的过程,首先详细先容、分析了链接的概念、作用及具体工作。通过readelf命令得到了链接后的hello可实行文件的ELF格式文本hello2.elf,据此分析了hello2.elf与hello.elf的异同;随后通过edb验证了hello的虚拟地址空间与节头部表信息的对应关系,分析了hello的实行流程。最后根据反汇编文件hello2.asm与hello.asm的比力,加深了对重定位与动态链接的明白。
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个正在运行的步伐的实例,系统中的每一个步伐都运行在某个进程的上下文中,是操作系统对一个正在运行的步伐的一种抽象。
给应用步伐提供两个关键抽象:
- 一个独立的逻辑控制流,提供一个假象,好像步伐独占地利用处置惩罚器
- 一个私有地址空间,提供一个假象,好像步伐独占地利用内存系统
6.2 简述壳Shell-bash的作用与处置惩罚流程
Shell 是一个用C语言编写的交互型应用步伐,代表用户运行其他步伐。Shell 应用步伐提供了一个界面,用户可以通过这个界面进行系统的基本操作,访问操作系统内核的服务。
从Shell终端读入输入的命令,切分输入字符串,获得并识别所有的参数。若输入参数为内置命令,则立即实行。若输入参数并非内置命令,则调用相应的步伐为其分配子进程并运行。若输入参数非法,则返回错误信息。处置惩罚完当前参数后继承处置惩罚下一参数,直到处置惩罚完毕。
6.3 Hello的fork进程创建过程
打开Shell,输入命令./hello 2021112802 why,带参数实行天生的可实行文件。
图 46 实行中示意
根据shell的处置惩罚流程,键入命令(./hello 2021112802 why)后,shell判断其不是内部指令,即会通过fork函数创建子进程。子进程与父进程近似,会得到一份与父进程用户级虚拟空间相同且独立的副本——包罗数据段、代码、共享库、堆和用户栈等,父进程打开的文件,子进程也可读写。二者之间最大的不同在于PID的不同。fork函数被调用一次会返回两次,在父进程中,fork函数返回子进程的PID,在子进程中,fork函数返回0。当子进程运行结束时,父进程如果仍然存在,则实行对子进程的回收,否则就由init进程回收子进程。
6.4 Hello的execve过程
在调用 `fork` 函数创建新的子进程后,子进程会调用 `execve` 函数来加载并运行一个新步伐 `hello`。execve函数在加载了hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新步伐的主函数,该主函数原型如下:
int main(int argc, char **argv, char *envp)
execve函数的实行过程会覆盖当进步程的地址空间,但并没有创建一个新进程。新的步伐仍然有相同的PID,而且继承了调用execve函数时已打开的所有文件描述符。`execve` 函数不会返回,它会删除该进程的代码和地址空间内容并将其初始化,然后通过跳转到步伐的入口点来运行该步伐。它会将私有区域(如打开的文件、代码段和数据段)和公共区域映射到地址空间中。然后,加载器会跳转到步伐的入口点,即将 PC 设置为 `_start` 地址。最终,`_start` 函数会调用 `hello` 步伐中的 `main` 函数,完成子进程中的加载。
6.5 Hello的进程实行
图 47 hello步伐实行示意
在步伐运行时,Shell 会为hello步伐创建一个子进程,该子进程与 Shell 具有独立的逻辑控制流。如果hello进程不被抢占,它会正常实行;如果被抢占,它会进入内核模式进行上下文切换,然后转入用户模式并调度其他进程。当 hello调用sleep 函数时,为了最大化利用处置惩罚器资源,sleep 函数会向内核发送请求将 hello挂起,并进行上下文切换。此时,hello进程会从运行队列中移除并加入等候队列,并开始计时。当计时结束时,sleep 函数返回并触发一个停止,使得 hello进程重新被调度并从等候队列中移出。此时,hello进程就可以继承实行其逻辑控制流。
6.6 hello的非常与信号处置惩罚
打印8次提示信息,输入回车即可结束步伐,并回收进程。
图 48 实行中不按键时
运行时按了3次回车,可以看到,步伐运行时多打印了3行空行,最后步伐结束时也提示了3次结束信息。步伐可以正常结束。
图 49 运行中按回车
Shell进程收到SIGINT信号,Shell结束并回收hello进程。步伐可以正常结束。
图 50 运行中按Ctrl+C
Shell进程收到SIGSTP信号,Shell显示屏幕提示信息并挂起hello进程。
图 51 运行中按Ctrl+Z
对hello进程的挂起可由ps和jobs命令查看,可以发现hello进程确实被挂起而非被回收,且其job代号为1。
图 52 ps命令和jobs命令
输入kill命令:kill -9 %1.
(题外话:我不小心输成了kill -1 %9,结果Ubuntu锁屏要求再次输入密码。查资料可知:`kill` 命令用于向进程发送信号。`-9` 是一个信号选项,表现发送 `SIGKILL` 信号,该信号会立即终止进程。`%1` 表现要发送信号的进程是当前 shell 的第一个作业。因此,`kill -9 %1` 命令的意思是立即终止当前 shell 的第一个作业。而 `kill -1 %9` 命令中的 `-1` 是一个信号选项,表现发送 `SIGHUP` 信号,该信号通常用于通知进程终端已断开毗连。`%9` 表现要发送信号的进程是当前 shell 的第九个作业。因此,`kill -1 %9` 命令的意思是向当前 shell 的第九个作业发送 `SIGHUP` 信号。)
再次ps可以发现,hello进程已经被杀死。
图 53 kill命令
输入fg-1命令(进程挂起时):可以看到,shell继承实行hello剩余的打印命令。
图 54 fg-1命令
乱按后如果加enter在步伐实行过程中乱按所造成的输入均缓存到stdin,当getchar的时间读出一个’\n’末端的字串(作为一次输入),hello结束后,stdin中的其他字串会当做Shell的命令行输入。
如果不加enter,则仅在输出时连带输出乱按的结果。
图 55 运行中不停乱按(按enter时)
图 56 运行中不停乱按(不按enter时)
图 57 pstree命令及其结果部分截图
图 58 终止hello前pstree对应部分截图
图 59 终止hello后pstree对应部分截图
6.7本章小结
本章先容了进程的概念与作用,以及Shell-bash的基本概念。针对进程,在这一章中根据hello可实行文件的具体示例研究了fork,execve函数的原理与实行过程,并给出了hello带参实行情况下各种非常与信号处置惩罚的结果。在hello步伐运行的过程中,内核对其进行进程管理,决定何时进行进程调度,在接收到不同的非常、信号时,还要及时地进行对应的处置惩罚。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址是指由步伐产生的与段相关的偏移地址部分,逻辑地址由选择符和偏移量两部分组成。具体而言,其为hello.asm中的相对偏移地址。
逻辑地址经过段机制转化后为线性地址,其为处置惩罚器可寻址空间的地址,用于描述步伐分页信息的地址。具体以hello而言,线性地址标志着 hello 应在内存上哪些具体数据块上运行。
根据CSAPP教材,虚拟地址即为上述线性地址(VPN+VPO).
CPU通过地址总线的寻址,找到真实的物理内存对应地址(PPN+PPO).
7.2 Intel逻辑地址到线性地址的变换-段式管理
为了充分利用内存空间,Intel 8086 设计了四个段寄存器来生存段地址:代码段寄存器(CS)、数据段寄存器(DS)、堆栈段寄存器(SS)和附加段寄存器(ES)。当步伐要实行时,需要确定代码、数据和堆栈所占用的内存位置,并通过设置 CS、DS 和 SS 段寄存器来指向这些起始位置。通常 DS 是固定的,而 CS 根据需要进行修改。因此,步伐可以在可寻址空间小于 64K 的情况下被写成恣意大小。但是,步伐和数据的组合大小受到 DS 所指向的 64K 的限定,这就是 COM 文件不得大于 64K 的原因。
段寄存器是为了实现内存分段管理而设置的。计算机需要对内存进行分段,以便分配给不同的步伐利用。描述内存分段时需要提供以下信息:段的大小、段的起始地址和段的管理属性(如禁止写入、禁止实行和系统专用等)。
在保护模式下,段寄存器的唯一目标是存放段选择符。其前 13 位是索引号,后 3 位包含一些硬件细节(尚有一些隐蔽位)。寻址方式为:利用段选择符作为下标,在 GDT/LDT 表中查找段地址,然后将段地址加上偏移地址得到线性地址。
在实模式下,段寄存器包含段值。访问存储器时,处置惩罚器会引用相应的某个段寄存器并将其值乘以 16,形成 20 位的段基地址。然后将段基地址加上偏移量得到线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(VA)到物理地址(PA)之间的转换通过对虚拟地址内存空间进行分页的分页机制完成。
通过7.2节中的段式管理过程,可以得到了线性地址/虚拟地址,记为VA。虚拟地址可被分为两个部分:VPN(虚拟页号)和VPO(虚拟页偏移量),根据计算机系统的特性可以确定VPN与VPO的具体位数,由于虚拟内存与物理内存的页大小相同,因此VPO与PPO(物理页偏移量)一致。而PPN(物理页号)则需通过访问页表中的页表条目(PTE)获取,如下图所示。
图 60 页式管理的地址变换示意
若PTE的有用位为1,则发生页掷中,可以直接获取到物理页号PPN,PPN与PPO共同组成物理地址。
若PTE的有用位为0,说明对应虚拟页没有缓存到物理内存中,产生缺页故障,调用操作系统的内核的缺页处置惩罚步伐,确定捐躯页,并调入新的页面。再返回到原来的进程,再次调用导致缺页的指令。此时发生页掷中,获取到PPN,与PPO共同组成物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
针对Intel Core i7 CPU研究VA到PA的变换。
Intel Core i7 CPU的基本参数如下:
- 虚拟地址空间48位(n=48)
- 物理地址空间52位(m=52)
- TLB四路十六组相连
- L1,L2,L3块大小为64字节
- L1,L2八路组相连
- L3十六路组相连
根据上述信息,我们可以得知 VPO 和 PPO 都有 12 位,因此 VPN 为 36 位,PPN 为 40 位。单个页表大小为 4KB,PTE 大小为 8 字节,因此单个页表有 512 个页表条目,需要 9 位二进制进行索引。而四级页表则需要 36 位二进制进行索引,对应着 36 位的 VPN。TLB 共有 16 组,因此 TLBI 需要 4 位,而 TLBT 则需要 36-4=32 位。
图 61 TLB与四级页表支持下的VA到PA的变换
如图所示,CPU 产生虚拟地址 VA 并将其传送至 MMU。MMU 利用前 36 位 VPN 来在 TLB 中进行匹配(前 32 位为 TLBT,后 4 位为 TLBI)。如果掷中,则得到 PPN(40 bit)并与 VPO(12 bit)组合成物理地址 PA(52 bit)。如果 TLB 没有掷中,则 MMU 向页表中查询。CR3 确定第一级页表的起始地址,VPN1(9 bit)确定在第一级页表中的偏移量,并查询出 PTE。如果 PTE 在物理内存中且权限符合,则实行下一步确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到 PPN 并与 VPO 组合成 PA,并向 TLB 中添加条目。多级页表的工作原理如下图所示:
图 62 多级页表现意
如果在查询 PTE 的过程中发现它不在物理内存中,则会引发缺页故障。如果发现权限不敷,则会引发段错误。
7.5 三级Cache支持下的物理内存访问
(以下格式自行编排,编辑时删除)
cache结构示意图为:
图 63 缓存结构示意图
图 64 物理地址结构示意图
由于三级缓存的工作原理基本相同,因此以 L1 缓存为例,先容在三级缓存支持下的物理内存访问。L1 缓存的基本参数如下: 8 路 64 组相连,块大小为 64 字节。
根据 L1 缓存的基本参数,我们可以分析得知:
- 块大小为 64 字节,因此需要 6 位二进制索引,即块偏移为 6 位。
- 共有 64 组,因此需要 6 位二进制索引,即组索引为 6 位。
- 剩余的标志位需要 PPN+PPO-6-6=40 位。
因此,L1 缓存可以划分为以下部分(从左到右):
CT(40 bit)CI(6 bit)CO(6 bit)
在前面的章节中,我们已经通过虚拟地址 VA 转换得到了物理地址 PA。首先利用 CI 进行组索引,每组有 8 路,对这 8 路的块分别匹配 CT(前 40 位)。如果匹配乐成且块的 valid 标志位为 1,则掷中(hit),并根据数据偏移量 CO 取出相应数据并返回。
如果没有匹配乐成或者匹配乐成但标志位为 0,则不掷中(miss),并向下一级缓存请求数据(请求次序为 L2 缓存→L3 缓存→主存,如果仍然不掷中则继承向下一级请求)。查询到数据后,需要对数据进行读入。一种简朴的放置策略是:如果映射到的组内有空闲块,则直接放置在空闲块中;如果当前组内没有空闲块,则产生冲突(evict),采用 LFU 策略进行替换。
7.6 hello进程fork时的内存映射
当 `fork` 函数被父进程(即 shell)调用时,内核会为新进程(即将加载并实行 `hello` 步伐的进程)创建各种数据结构,并分配给它一个唯一的 PID。为了为新进程创建虚拟内存,内核会创建当进步程的 `mm_struct`、区域结构和页表的副本。它会将两个进程中的每个页面都标志为只读,并将两个进程中的每个区域结构都标志为私有的写时复制。
当 `fork` 在新进程中返回时,新进程的虚拟内存与调用 `fork` 时的虚拟内存相同。当这两个进程中的恣意一个进行写操作时,写时复制机制就会创建新页面,从而为每个进程保持私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
要加载并运行hello,execve函数需要实行以下步调:
1. 删除当进步程hello虚拟地址用户部分中已存在的区域结构。
2. 为新步伐的代码、数据、bss和栈区域创建新的私有的、写时复制的区域结构。
3. 若hello步伐与共享对象或目标链接,则将这些对象动态链接到hello步伐,然后再映射到用户虚拟地址空间中的共享区域内。
4. 设置步伐计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页停止处置惩罚
发生一个缺页非常后,引发缺页故障,控制会转移到内核的缺页处置惩罚步伐。判断虚拟地址是否合法,若不合法,则产生一个段错误,然后终止这个进程。
若操作合法,则缺页处置惩罚步伐从物理内存中确定一个捐躯页,若该捐躯页被修改过,则将它换出到磁盘,换入新的页面并更新页表。当缺页处置惩罚步伐返回时,CPU 再次实行引起缺页的指令,将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面如今缓存在物理内存中,所以就会掷中,主存将所请求字返回给处置惩罚器。
7.9本章小结
本章重要先容了hello 的存储器地址空间、intel 的段式管理、hello 的页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页停止处置惩罚、动态存储分配管理。
结论
hello步伐的一生(P2P, program to progress; 020, from zero to zero)经历了预处置惩罚、编译、汇编、链接、加载运行、实行指令、访存、动态申请内存、信号处置惩罚和终止并被回收等过程。
- 在预处置惩罚阶段,预处置惩罚器cpp将hello.c中include的所有外部的头文件头文件内容直接插入步伐文本中,完成字符串的替换,方便后续处置惩罚。
- 在编译阶段,编译器ccl通过词法分析和语法分析,将合法指令翻译成等价汇编代码。
- 在汇编阶段,汇编器as将hello.s汇编步伐翻译成呆板语言指令,并把这些指令打包成可重定位目标步伐格式。
- 在链接阶段,链接器ld通过链接器,将hello的步伐编码与动态链接库等网络整理成为一个单一文件。
- 在加载运行阶段,打开Shell,在此中键入 ./hello 2021112802 why 1,终端为其fork新建进程,并通过execve把代码和数据加载入虚拟内存空间,步伐开始实行。
- 在实行指令阶段,在该进程被调度时,CPU为hello其分配时间片,在一个时间片中,hello享有CPU全部资源,PC寄存器一步一步地更新,CPU不停地取指,次序实行自己的控制逻辑流。
- 在访存阶段,内存管理单元MMU将逻辑地址,一步步映射成物理地址,进而通过三级高速缓存系统访问物理内存/磁盘中的数据。
- 在动态申请内存阶段,printf会调用malloc向动态内存分配器申请堆中的内存。
- 在信号处置惩罚阶段,进程时刻等候着信号,如果运行途中键入ctr-c ctr-z,则调用shell的信号处置惩罚函数分别进行停止、挂起等操作。
- 在终止并被回收阶段,Shell父进程等候并回收hello子进程,内核删除为hello进程创建的所有数据结构。
附件
文件名
| 功能
| hello.i
| 预处置惩罚后得到的文本文件
| hello.s
| 编译后得到的汇编语言文件
| hello.o
| 汇编后得到的可重定位目标文件
| hello.elf
| 用readelf读取hello.o得到的ELF格式信息
| hello1.elf
| 用readelf读取链接后的可实行目标文件得到的ELF格式信息
| hello.asm
| 反汇编hello.o得到的反汇编文件
| hello2.elf
| 由hello可实行文件天生的.elf文件
| hello2.asm
| 反汇编hello可实行文件得到的反汇编文件
| 表 8 天生的文件示意图
参考文献
[1] Randal E.Bryant, David O'Hallaron. 深入明白计算机系统[M]. 机械工业出书社.2018.4
[2] Tanenbaum, A. S., & Woodhull, A. S. (2006). Operating systems: design and implementation (3rd ed.). Prentice Hall. ³
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |