计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 信息安全
学 号 2022111759
班 级 2203202
学 生 李达鹏
指 导 教 师 史先俊
计算机科学与技术学院
2024年5月
本文通过介绍 hello 程序的 P2P 和 O20 过程,深入分析了程序的整个生命周期,使我们对计算机系统有了更深入的理解。
首先,hello.c 程序编写完成后,经过 C 预处理器预处理生成 hello.i 文件;随后 C 编译器将其翻译为汇编语言文件 hello.s;接着汇编器将其转换为可重定位目标文件 hello.o;然后连接器将刚生成的 hello.o 文件与系统目标文件结合,生成可执行目标文件 hello,这就是 P2P 过程。
接下来,shell 创建子进程运行 hello 程序,涉及进程管理、内存管理和 I/O 管理,最后 hello 进程被回收,这就是 O20 过程。
关键词:hello;P2P;020;计算机系统
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述................................................................................... - 4 -
1.1 Hello简介............................................................................ - 4 -
1.2 环境与工具........................................................................... - 4 -
1.3 中间结果............................................................................... - 4 -
1.4 本章小结............................................................................... - 4 -
第2章 预处理............................................................................... - 5 -
2.1 预处理的概念与作用........................................................... - 5 -
2.2在Ubuntu下预处理的命令................................................ - 5 -
2.3 Hello的预处理结果解析.................................................... - 5 -
2.4 本章小结............................................................................... - 5 -
第3章 编译................................................................................... - 6 -
3.1 编译的概念与作用............................................................... - 6 -
3.2 在Ubuntu下编译的命令.................................................... - 6 -
3.3 Hello的编译结果解析........................................................ - 6 -
3.4 本章小结............................................................................... - 6 -
第4章 汇编................................................................................... - 7 -
4.1 汇编的概念与作用............................................................... - 7 -
4.2 在Ubuntu下汇编的命令.................................................... - 7 -
4.3 可重定位目标elf格式........................................................ - 7 -
4.4 Hello.o的结果解析............................................................. - 7 -
4.5 本章小结............................................................................... - 7 -
第5章 链接................................................................................... - 8 -
5.1 链接的概念与作用............................................................... - 8 -
5.2 在Ubuntu下链接的命令.................................................... - 8 -
5.3 可执行目标文件hello的格式........................................... - 8 -
5.4 hello的虚拟地址空间......................................................... - 8 -
5.5 链接的重定位过程分析....................................................... - 8 -
5.6 hello的执行流程................................................................. - 8 -
5.7 Hello的动态链接分析........................................................ - 8 -
5.8 本章小结............................................................................... - 9 -
第6章 hello进程管理.......................................................... - 10 -
6.1 进程的概念与作用............................................................. - 10 -
6.2 简述壳Shell-bash的作用与处理流程........................... - 10 -
6.3 Hello的fork进程创建过程............................................ - 10 -
6.4 Hello的execve过程........................................................ - 10 -
6.5 Hello的进程执行.............................................................. - 10 -
6.6 hello的异常与信号处理................................................... - 10 -
6.7本章小结.............................................................................. - 10 -
第7章 hello的存储管理...................................................... - 11 -
7.1 hello的存储器地址空间................................................... - 11 -
7.2 Intel逻辑地址到线性地址的变换-段式管理................... - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理............. - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换.................... - 11 -
7.5 三级Cache支持下的物理内存访问................................ - 11 -
7.6 hello进程fork时的内存映射......................................... - 11 -
7.7 hello进程execve时的内存映射..................................... - 11 -
7.8 缺页故障与缺页中断处理................................................. - 11 -
7.9动态存储分配管理.............................................................. - 11 -
7.10本章小结............................................................................ - 12 -
第8章 hello的IO管理....................................................... - 13 -
8.1 Linux的IO设备管理方法................................................. - 13 -
8.2 简述Unix IO接口及其函数.............................................. - 13 -
8.3 printf的实现分析.............................................................. - 13 -
8.4 getchar的实现分析.......................................................... - 13 -
8.5本章小结.............................................................................. - 13 -
参考文献....................................................................................... - 16 -
第1章 概述
1.1 Hello简介
1) 程序的生命周期始于一个高级 C 语言程序 hello.c,由程序员用 C 语言编写。
2) 预处理器(cpp)根据以字符 # 开头的命令,对原始程序进行预处理生成 hello.i 文件。
3) 编译器(ccl)将 hello.i 翻译生成汇编语言文件 hello.s 文件。
4) 接着,汇编器(as)将 hello.s 翻译成可重定位目标文件 hello.o。
5) 链接器(ld)负责处理 hello.o 和系统目标文件的合并,生成一个可执行目标文件(或简称为可执行文件) hello。
(参考自《深入理解计算机系统》)
P2P流程如下图所示:
6) 在 shell 中输入执行 hello 的命令后,shell 解析命令行,通过 fork 新建一个子进程来执行 hello,此时 hello 从程序转换为进程。
7) 程序开始时,一切为 0,初始为 zero。shell 为新建的子进程调用 execve,进行虚拟内存映射,进入程序入口后开始将程序载入物理内存,接着进入 main 函数执行目标代码,CPU 以流水线形式读取并执行指令,执行逻辑控制流。操作系统负责进程调度,为进程分配时间片。在执行过程中,通过 L1、L2、L3 高速缓存、TLB、多级页表等进行存储管理,使用 I/O 系统进行输入输出。当程序运行结束后,shell 父进程负责回收 hello 进程,内核删除相关数据结构,一切重新归零,这就是 O20 过程。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:X64 CPU;2.6GHz;16G RAM;256GHD Disk
软件环境:Windows10 64位;Vmware 16;Ubuntu 16.04 LTS 64位
开发与调试工具:gcc,gdb,edb,readelf,HexEdit
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c 源程序
hello.i 预处理后文件,由C预处理器产生,用于分析预处理过程。
hello.s 编译后的汇编文件,由C编译器产生,用于分析编译过程。
hello.o 汇编后的可重定位目标执行文件,由汇编器产生,用于分析汇编过程。
hello 链接后的可执行文件,由链接器产生,用于分析链接过程。
hello_elf.txt hello.o的ELF格式,用于分析可重定位目标文件hello.o。
hello_elf2.txt hello的ELF格式,用于分析可执行目标文件hello。
1.4 本章小结
本章利用计算机系统的术语描述了一个hello程序P2P、020的整个过程,给出了实验所用的环境和工具的基本信息,列出了编写论文过程中生成的所有文件和其对应作用
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
2.1.1 概念:预处理器(cpp)根据以字符 # 开头的命令,修改原始的 C 程序。
2.1.2 作用:根据源代码中的预处理指令修改源代码,预处理将系统头文件中的源码插入目标文件中,同时将宏和常量标识符替换为相应的代码和值,生成扩展名为 .i 的预处理后文件。例如,在 hello.c 中,第一行的 `#include <stdio.h>` 命令告诉预处理器读取系统头文件 `stdio.h` 的内容,并直接插入到程序文本中。
图2-1 C语言预处理指令表
2.2在Ubuntu下预处理的命令
Ubuntu下预处理命令为:gcc -E hello.c -o hello.i
图2-2 预处理
预处理后得到hello.i文件。
2.3 Hello的预处理结果解析
图2-3预处理文本
打开预处理后文件,发现hello.i文件为纯文本文件,并且有3060行,说明原.c文件中的宏进行了宏展开,头文件中的内容被包含进该文件中,例如声明函数、定义结构体、定义变量、定义宏等内容。
2.4 本章小结
本章介绍了预处理的概念和作用,在虚拟机的ubuntu系统下动手进行了预处理操作,并且分析了得到的预处理文件
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
1 概念:编译器(ccl)将文本文件 hello.i 翻译为文本文件 hello.s,其中包含一个汇编语言程序。
2 作用:编译器将高级语言转换为计算机可识别的二进制语言。计算机只能理解0和1,编译器将人们熟悉的语言转换为二进制形式。编译过程包括五个阶段:词法分析、语法分析、语义检查和中间代码生成、代码优化以及目标代码生成。其中,词法分析和语法分析是最重要的阶段,也称为源程序分析,检测语法错误并提供相应提示信息。
-词法分析:处理由字符组成的单词,逐个字符地扫描源程序,生成单词符号串,将源程序表示为单词符号串的中间形式。
- 语法分析:以单词符号为输入,检查单词符号串是否形成符合语法规则的语义单位,例如表达式、赋值、循环等。最终检查是否形成符合要求的程序,按照语言的语法规则对每条语句进行分析和检查。语法规则可用上下文无关文法来描述。
- 代码优化:对程序进行等价转换,以生成更有效的目标代码。转换后的程序不改变运行结果,但可以使程序的运行时间减少、占用存储空间更小。
在编译过程中,如果发现源程序中有错误,编译器会报告错误的性质和位置。通常情况下,编译器仅进行语法检查和最基本的语义检查,而不检查程序的逻辑。3.2 在Ubuntu下编译的命令
在Ubuntu下编译的命令:gcc -S hello.i -o hello.s
图3-1 编译处理
生成了汇编文件hello.s 。
3.3 Hello的编译结果解析
图3-2 编译结果
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.3.0编译指令:
指令 | 内容 |
.file | 声明源文件 |
.text | 声明代码段 |
.data | 声明数据段 |
.rodata | 只读数据 |
.globl | 全局变量 |
.size | 声明大小 |
.type | 指定类型 |
.align | 声明对指令或数据的存放地址进行对齐的方式 |
3.3.1数据
- 常量
图3-3 常量
两个printf的参数是字符串常量,存储在下图所示 .string 中。
图3-4 常量
- 变量
程序中设置了i作为循环条件,i是一个变量,编译器编译时用movl指令将这个局部变量放在堆栈中。如下面,i被放在栈上%rbp-4的位置,初始值为0;
图3-5变量
3.3.2赋值
图3-6变量
对于循环计数变量i,编译器在设置i时就设置其初始值为0。
图3-7变量
3.3.3类型转换
图3-8类型转换
atoi函数将字符型argv[4]转换为整型数,如下。
图3-9类型转换
3.3.4算术操作
编辑
图3-10算术操作
hello.c源程序中只包含一次算术操作,出现在循环变量i每次增加1的时候。算术操作为++。
算术操作++代表自增1的运算,编译时转化成add类的加法指令,使用立即数1来实现每次增加1(由前面分析可知,%rbp-4处为变量i),如下。
图3-11 算术操作
3.3.5关系操作
- 在if中判断argc的取值是否不等于5
图3-12 关系操作
编译时使用cmpl指令将argc和3进行比较,并设置条件码。跳转指令je根据条件码决定是否跳转。
图3-13 关系操作
图3-14关系操作
在for循环中判断结束条件,即判断i是否小于8。编译器编译时使用cmpl指令,将i和7进行比较并设置条件码。跳转指令jle代表小于等于7时跳转,与小于8时跳转等效。这里进行比较的值是7而不是8,与编译的过程中进行了优化有关。
3.3.6数组/指针/结构操作
图3-15指针操作
argv数组是传入的参数,储存在栈上。初始地址位于%rbp-32,利用argv的地址加i*8,就能得到argv[i]。
图3-16指针操作
3.3.7控制转移
图3-17 控制转移操作
根据汇编器的汇编指令判断argc等于4时,跳转到 .L2 处
图3-17 控制转移操作
2)循环内的转移被编译为,循环变量小于等于7时,跳转到.L4指令处。
图3-17 控制转移操作
3.3.8函数操作
1)函数调用
调用printf函数
图3-18函数操作
调用sleep函数和atoi函数
图3-19 函数操作
调用getchar函数
函数调用会被翻译为call
图3-20函数操作
2)函数参数传递
参数的传递:大部分的参数传递通过寄存器实现,通过寄存器最多传递6个参数,按照顺序依次为%rdi、%rsi、%rdx、%rcx、%r8、%r9。多余的参数通过栈来传递。对于函数printf第一次出现,只有一个参数,通过寄存其%edi传递:
图3-21函数操作传递
对于函数printf第二次出现,分别通过寄存器%rdi、%rsi、%rdx传递。
图3-22 函数参数传递
对于函数sleep,只有一个参数,通过寄存器%edi传递。
图3-23函数参数传递
对于函数getchar没有参数,无需传递.
3.4 本章小结
本章介绍了编译的概念、作用和对应指令,在Ubuntu下汇编得到了hello.s文件。然后对该文件进行了分析,介绍了C语言中赋值操作、类型转换、关系操作、数组/指针操作、控制转移和函数操作等对应的汇编语言的指令。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
4.1.1 概念:汇编器(as)将 hello.s 翻译成机器语言指令,并将这些指令打包成一种可重定位目标程序格式,结果保存在目标文件 hello.o 中。
4.1.2 作用:目标文件 hello.o 是一个二进制文件,包含指令对应的二进制机器语言,这种二进制代码能够被计算机理解和执行。汇编器的作用是将汇编语言转换为最底层的、计算机可理解的机器语言,从而实现程序的执行。4.2 在Ubuntu下汇编的命令
图4-1汇编操作
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
1)在Ubuntu中利用readelf命令来查看ELF,并将其重定位为文本文件。
readelf -a hello.o > hello_elf.txt
图4-2汇编的重定位
2)一个ELF可重定位目标文件可包含如下内容:
图4-3 重定位目标文件包含内容
- 文件内容
ELF头起始于一个16字节的目标序列,如图中所示,其中的 Magic 字段描述了生成该文件的系统的字大小和字节顺序。对于 hello.o 文件,这个16字节序列为 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00,表示系统的字大小为 8 字节,字节顺序为小端序。
ELF头的其余部分包括用于辅助链接器语法分析和解释目标文件的信息。其中包括 ELF 头的大小、目标文件类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。以 hello.o 为例,ELF头中包含了以下信息:ELF 头大小为 64 字节;目标文件类型为 REL(可重定位文件);机器类型为 Advanced Micro Devices X86-64;节头部表的文件偏移为 1240 字节;节头部表中条目数量为 14。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
图4-5反汇编内容
在将hello.s中的汇编语言转换为机器语言时,汇编指令被映射到不同的二进制功能码,操作数也被映射到二进制的形式,以使机器能够识别。每条汇编语句都有对应的机器语言形式,反之亦然,从而实现相互转换。然而,这两种表示方式存在一些不同之处:
1) 操作数表示
在hello.s中,立即数使用十进制表示,而在机器语言中,立即数以十六进制表示。
2) 分支转移:
在hello.s中,分支转移(跳转指令)直接使用助记符(如.LC0,.LC1)来进行跳转,在机器语言中,跳转使用确定的地址,而不再使用助记符来标识跳转位置。
3) 函数调用:
在hello.s中,函数调用通过在call指令后面加上函数名来实现,而在机器语言中,call指令后面是被调用函数的PC相对地址。观察到call指令后跟着一串0的情况,这是因为调用的是库函数,需要经过动态链接以确定函数地址,因此在重定位节中要添加相应的重定位条目,在链接时才能确定最终调用函数的相对地址。4.5 本章小结
本章介绍了汇编的概念与作用,在ubuntu下实际进行了汇编操作,生成了hello.o文件,进而分析汇编过程。然后还用readelf命令查看了hello.o文件的ELF格式,将两个文件对比分析了汇编语言和机器代码的差异。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合成单个文件的过程,这个文件可以被加载到内存并执行。
作用:链接在软件开发中扮演着重要角色,使得分离编译成为可能。通过模块化设计和编程,软件开发可以更高效地进行分组工作。当需要修改或调试时,只需修改特定模块,重新编译并重新链接即可,而不需要重新编译整个程序。即使对于像 hello 这样简单的小程序,链接的作用也是非常重要的。
5.2 在Ubuntu下链接的命令
Linux下使用链接器 (ld) 链接的命令为:
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-1 链接命令
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
在可执行目标文件中,其各类信息与在可重定位目标文件中其实是差不多的,故我们这里一些基本概念就不多叙述。
5.3.1 ELF头
查看hello的ELF头:readelf -a hello
图 5-2 ELF头
我们可以看到可文件的类型为可执行文件,入口点地址为0x4010f0,以及其他的基本信息。比如节头部表的偏移量,ELF头的大小等
5.3.2 节头部表
以下是部分的节头部表
图 5-3 部分节头部表
可以看到,其基本内容和之前还是很相似的,但链接器将各个文件对应的段都合并了,并且重新分配并计算了相应节的类型、位置和大小等信息。通过节头部表我们就可以知道各个节的地址和大小了。而这些我们都可以到其反汇编文件中得到验证。
5.3.3 符号表
以下是部分符号表
图 5-4 符号表
5.3.4 段节
图 5-5 段节
5.3.5 Dynamic section
图 5-6 动态节
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
图 5-7 edb内存空间截图
该图展示了edb中data dump的默认显示地址范围,是从0x401000到0x402000.然后来看一下hello的程序头表确认它所对应的段
图 5-8 程序头表
程序头表描述了每个段在文件中的位置、大小以及它被放进内存后所在的位置和大小。程序头表有一列是type类型,该类型用来精确的描述各段。PHDR保存程序头表,INTERP指定程序从可执行文件映射到内存之后,必须调用的解释器(就像java需要java虚拟机解释一样)。这个解释器可以通过链接其他程序库,来解决符号引用。LOAD表示一个需要从二进制文件映射到虚拟地址空间的段,其中保存了常量数据(字符串等)、程序目标代码。DYNAMIC保存了动态链接器(前面interp指定的解释器)使用的信息。GNU_STACK:权限标志,用于标志栈是否是可执行。GNU_RELRO:指定在重定位结束之后哪些内存区域是需要设置只读。
而每一段的Filesiz关键字和Memsiz关键字给出了每一段的文件大小和占用内存的大小,PhysAddr关键字给出的是每个段的物理地址,offset是偏移值,剩下的flags,align给出的是其它的信息,这样我们就知道了一个可执行程序每个段在内存中的位置和其所占空间的大小。
而通过分析,我们可以知道第二个LOAD段是代码段,有读和执行权限,开始于内存地址0x401000处,总共的内存大小为0x27d字节,并且被初始化为可执行目标文件的头也是0x27d字节,其中包括ELF头,程序头部表以及.init、.text和.rodata节。
第四个LOAD段是数据段,有读和写权限,开始于内存地址0x403e50处,总共的内存大小为0x1fc字节,并用从目标文件中偏移0x2e10处开始的.data节中的0x1fc字节初始化。
这是我们再根据反汇编的代码和内存空间去比较发现是相同的。
我们这里也来对比一下第四个LOAD段的内存空间来看看
图 5-10 edb内存查看
可以看到在0x403e50以前存储的都是0,而在之后存储了一些内容,当然我们这里也很难知道存储的是什么,但能说明的确是从这一段开始的,这验证了我们的想法。
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
- hello新增了许多的函数相关的代码
图5-11 hello反汇编文件
这是通过重定位实现的。
- 新增了节
我们可以看到hello明显多了很多其他的节,比如.init .plt等。
图 5-12 反汇编新增节
init是为了初始化程序,而plt是由于共享库链接时生成的。
- 函数跳转转移
在函数调用时,由于hello是重定位后的可执行程序,因此其调用函数时,所调用的地址就是函数所在的确切地址,而hello.o则是调用一些相对的地址。
- 常量,全局变量的调用
在将字符串常量作为参数传递时,我们可以看到它直接使用了其地址来读取,而在hello.o则是要通过重定位来实现。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
我们主要拿书上提到的两种主要重定位类型进行分析。
第一种是重定位的绝对引用,这种相对简单。
图 5-13 可重定位条目分析
这是在hello.o中传递字符串常量时出现的重定位条目(第一个),我们可以看到mov指令开始于节偏移0x19的位置,包括1字节的操作码0xbf,后面跟着对该字符串常量地址的32位绝对引用的占用符。通过重定位条目我们可以知道,它的引用符号为.rodata,addend为0。下面我们去看.rodata中的地址。
图 5-14 .rodata验证
上图的信息量很大,我们首先通过readelf -x .rodata hello去查看hello的.rodata段的十六进制形式,可以看到是从0x402000开始的,但为什么不是下面的结果不是0x402000呢?这一点我也不太确定,但我们可以看到从0x402008开始有大量的信息存储,比如e794a8e6我们去查hello.s中看到的赋值序列,上网查询得知,这是汉字的八进制转义序列,每三个作为一个编码,转换为十六进制可以发现二者恰好对应,说明从0x402008开始存储的正是该字符串常量。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
由于对edb操作不是很熟悉,我采用的是gdb的方式,我们首先通过前面说到的入口点,即_start处的地址0x4010f0,在此处设置断点
图 5-16 gdb设置断点
之后我们单步开始运行,然后看它跳转的函数及其地址。
程序名 | 程序地址 |
_start | 0x4010f0 |
_libc_start_main_impl | 0x7ffff7c29dc0 |
__GI___cxa_atexit | 0x7ffff7c458c0 |
__new_exitfn | 0x7ffff7c456d0 |
_init | 0x401000 |
_dl_audit_preinit@plt | 0x7ffff7c286d0 |
(跳至一个空的,没名字的地方) | 0x7ffff7c28350 |
同上 | 0x7ffff7c28000 |
_dl_runtime_resolve_xsavec | 0x7ffff7fd8d30 |
_dl_fixup | 0x7ffff7fd5e70 |
_dl_lookup_symbol_x | 0x7ffff7fcf0d4 |
do_lookup_x | 0x7ffff7fce3f0 |
check_match | 0x7ffff7fce24x |
strcmp | 0x7ffff7fea224 |
之后开始不断返回直到_dl_audit_preinit | |
__libc_start_call_main | 0x7ffff7c29d10 |
_setjmp | 0x7ffff7c421e0 |
_sigsetjmp | 0x7ffff7c42110 |
__sigjmp_save | 0x7ffff7c42190 |
main | 0x401125 |
printf | 0x4010a0 |
atoi | 0x401191 |
sleep | 0x4010e0 |
getchar | 0x4010b0 |
返回至__libc_start_call_main | |
__GI_exit | 0x7ffff7c455f0 |
__run_exit_handlers | 0x7ffff7c45390 |
__GI___call_tls_dtors | 0x7ffff7c45d60 |
_dl_fini | 0x7ffff7fc9040 |
___pthread_mutex_lock | 0x7ffff7c97ef0 |
_dl_audit_activity_nsid | 0x7ffff7fde250 |
_dl_sort_maps | 0x7ffff7fd6730 |
_fini | 0x4011b4 |
_dl_audit_objclose | 0x7ffff7fde570 |
最终跳至__GI_exit退出程序 |
整个过程其实调用了很多未知的函数,但总结来说过程是
_start -> __libc_start_main -> init -> main -> exit.
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
动态链接是一项有趣的技术。让我们考虑一个简单的事实,printf,getchar这样的函数实在使用的太过频繁,因此如果每个程序链接时都要将这些代码链接进去的话,一份可执行目标文件就会有一份printf的代码,这是对内存的极大浪费。为了遏制这种浪费,对于这些使用频繁的代码,系统会在可重定位目标文件链接时仅仅创建两个辅助用的数据结构,而直到程序被加载到内存中执行的时候,才会通过这些辅助的数据结构动态的将printf的代码重定位给程序执行。即是说,直到程序加载到内存中运行时,它才知晓所要执行的代码被放在了内存中的哪个位置。
这种有趣的技术被称为延迟绑定,将过程地址的绑定推迟到第一次调用该过程时。而那两个辅助的数据结构分别是过程链接表(PLT)和全局偏移量表(GOT),前者存放在代码段,后者存放在数据段。
图 5-17 节头部表查看.got节
我们首先通过前面的节头部表获得got的地址,
可以看到.got的位置是0x403ff0,我们去查相应的内存空间。
图 5-18 运行前got内存空间
可以看到在运行之前是没什么内容的,而当我们运行之后,我们来看看它的变化:
图 5-19 运行后got内存空间
通过前后对比:在执行_dl_init之前,.got是全0的;在执行_dl_init之后,.got变成了相应的值。因此推测_dl_init作用是:初始化程序,给其赋上调用的函数的地址,使这些被调用的函数链接到了动态库。
5.8 本章小结
本章介绍了链接的概念与内容,分析了hello的elf格式,查看了hello的虚拟地址空间,对链接的重定位过程进行了分析,并验证了其重定位条目的引用等,并使用gdb和edb分析了hello的执行流程和动态链接分析。
在本章中呢,链接器ld通过可重定位目标文件中的数据结构,解析每个文件中的符号,仔细比对了符号的定义和引用,最终为每个符号的引用都找到了正确的符号定义的位置。而重定位的过程更加需要小心谨慎,链接器需要在特定的位置修改值,使得程序在运行时能够指哪打哪而不会偏差。毕竟在cpu中哪怕是一个字节的偏差,失之毫厘,差之千里。因此可以说,链接的过程同样是充满着各种艰难困苦的。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1概念:进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文(context)中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
6.1.2作用:进程提供给应用程序两个关键抽象:
·1)一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
2)一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1作用:shell是一个交互型应用级程序,为使用者提供操作界面。Shell接受用户命令,然后调用相应的应用程序。Shell作为系统得用户界面,提供了用户与内核进行交互操作的接口
6.2.2处理流程
Shell主要功能是解释命令。
1)首先对用户输入的命令进行解析,判断命令是否为内置命令
2)如果为内置命令,调用内置命令处理函数;如果不是内置命令,就创建一个子进程,将程序在该子进程的上下文中运行。
3)判断为前台程序还是后台程序,如果是前台程序则直接执行并等待执行结束,如果是后台程序则将其放入后台并返回。
4)同时Shell对键盘输入的信号和其他信号有特定的处理。
6.3 Hello的fork进程创建过程
在ubuntu终端输入对应的命令 ./hello 120L02041 接下来shell会读入输入的命令,然后首先判断hello不是一个内置的shell命令。所以下一步要找到当前所在目录下是否有可执行文件hello。找到hello后,shell自动调用fork( )函数让父进程创建一个新的运行的子进程。新创建的子进程得到与父进程用户级虚拟地址空间相同但是独立的一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。
父进程和新创建的子进程之间最大的区别在于他们有不同的PID。
6.4 Hello的execve过程
1)子进程通过execve系统调用启动加载器。
2)加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。
3)通过将虚拟地址空间中的页映射到可执行文件的页大小的片(chunk),新的代码和数据段被初始化为可执行文件的内容。
4)最后,加载器跳转到_start地址,它最终会调用应用程序的main函数。
除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到 CPU引用一个被映射的虚拟页时才会进行复制,此时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
上下文:系统中的每个程序都运行在某个进程的上下文(context)中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
并发流:一个逻辑流的执行在时间上与另一个流重叠。
用户模式和内核模式:为了使操作系统内核提供一个无懈可击的进程抽象,处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。一个运行在内核模式的进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。用户模式中的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度(scheduling),是由内核中称为调度器(scheduler)的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。
在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换包括三步
- 保存当前进程的上下文
- 恢复某个先前被抢占的进程被保存的上下文
- 将控制传递给这个新恢复的进程。
以hello的进程执行为例。fork()函数生成子进程,在进程调用execve函数之后,进程已经为hello程序分配了新的虚拟的地址空间,最初hello运行在用户模式下。
当hello进程调用sleep时,进程变为内核模式。这时hello进程被挂起,内核会选择调度sleep进程,通过上下文切换保存hello进程的上下文,将控制传递给sleep。sleep函数中定时器的时间到了后会发送中断信号,hello进程又变成运行状态,这时hello进程就可以等待内核调度它。
当内核再次调度hello进程时,恢复保存的hello进程的上下文,就可以从刚才停止的地方继续执行了。
当hello调用getchar的时候同样会陷入内核模式,由于getchar需要来自键盘的DMA传输,时间很长,因此内核不会等待DMA完成,而是去调度其他进程。当DMA完成后,会向处理器发送中断信号,进入内核模式,内核知道DMA完成了,就可以再次调度hello进程了。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
- 异常介绍
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令或终止
终止 不可恢复的错误 同步 不会返回
- 各命令结果
- 不停乱按
不停乱按,没有输入回车的情况下,输入的字符串被缓存到缓冲区,没有任何反应;输入回车,回车前的字符被认为是输入的命令,返回未找到命令。
- 回车
“回车”显示为空行,由后面命令输入提示符多出来好几行可以推断,回车被存储到缓冲区,输出完成后shell再对其反应
- Ctrl-C
发送SIGINT信号给前台进程组的每个进程,终止了前台进程,即终止hello进程。
- Ctrl-Z等,Ctrl-z后可以运行ps jobs pstree fg kill
Ctrl-Z会发送SIGTSTP信号给前台进程组的每个进程,结果也是停止前台作业
6.7本章小结
本章了解了hello进程的执行过程。在hello运行过程中,内核对其调度,异常处理程序为其将处理各种异常。每种信号都有不同的处理机制,对不同的shell命令,hello也有不同的响应结果。可以说,进程管理就是为了约束程序的运行而存在的。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
(1)逻辑地址:指由程序产生的段内偏移地址。
(2)线性地址:指虚拟地址到物理地址变换的中间层,是处理器可寻址的内存空间(称为线性地址空间)中的地址。程序代码产生的逻辑地址加上相应段基址就成了一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换产生物理地址。若是没有采用分页机制,那么线性地址就是物理地址。
(3)虚拟地址:是由程序产生的由段选择符和段内偏移地址组成的地址。
(4)物理地址:指内存中物理单元的集合。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址=段选择符+偏移量
每个段选择符大小为16位,段描述符为8字节。每个段的首地址都存放在自己的段描述符中,而所有的段描述符都存放在一个描述符表中。而要想找到某个段的描述符必须通过段选择符才能找到。
通过段选择符我们就可以找到我们想要的段描述符,从而获取某个段的首地址,然后再将从段描述符中获取到的首地址与逻辑地址的偏移量相加就得到了线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
分页机制把线性地址空间和物理地址空间分别划分为大小相同的块。这样的块称之为页。通过在线性地址空间的页与物理地址空间的页之间建立的映射,分页机制实现线性地址到物理地址的转换。
虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。利用页表来管理虚拟页,页表就是一个页表条目(PTE)的数组,每个PTE由一个有效位和一个n位地址字段组成。
Cpu首先会生成一个虚拟地址,传递给MMU,MMU利用VPN获取PPN与保持和VPO不变的PPO组成物理地址
7.4 TLB与四级页表支持下的VA到PA的变换
每个条目引用一个 4KB子页表:
P: 子页表在物理内存中 (1)不在 (0).
R/W: 对于所有可访问页,只读或者读写访问权限.
U/S: 对于所有可访问页,用户或超级用户 (内核)模式访问权限.
WT: 子页表的直写或写回缓存策略.
A: 引用位 (由MMU 在读或写时设置,由软件清除).
PS: 页大小为4 KB 或 4 MB (只对第一层PTE定义).
D:修改位,告知是否写回
Page table physical base address: 子页表的物理基地址的最高40位 (强制页表
4KB 对齐)
XD: 能/不能从这个PTE可访问的所有页中取指令.
虚拟地址的VPN被划分为VPN1,VPN2,VPN3,VPN4。CR3寄存器中有L1页表的地址,根据VPN1能够在L1页表找到相应PTE,得到L2页表的基地址,一次类推,最终我们得到物理地址的PPN。
7.5 三级Cache支持下的物理内存访问
物理地址PA被分成3块,CT(标记)、CI(索引)、CO(偏移),之后在L1中寻找,若命中,则返回对应块偏移的数据。否则,L1不命中,我们需要前往L2,L3甚至是主存中得到对应的数据
7.6 hello进程fork时的内存映射
Fork函数为新进程创建虚拟内存,他会创建当前进程的的mm_struct, vm_area_struct和页表的原样副本。将两个进程中的每个页面都标记为只读,两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW),在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存,随后的写操作通过写时复制机制创建新页面。
7.7 hello进程execve时的内存映射
它首先删除已存在的用户区域,之后创建新的区域结构,其中的代码和初始化数据映射到.text和.data区(目标文件提供),.bss和栈映射到匿名文件。hello程序与共享对象链接,这些对象动态链接到hello程序,然后映射到用户虚拟地址空间。再设置PC,指向代码区域的入口点
7.8 缺页故障与缺页中断处理
(1)段错误: 访问一个不存在的页面,此时会中断程序执行。
(2)正常缺页,会选择牺牲页,换入新的页面并进行页表更新,完成缺页处理。
(3)保护异常: 例如,违反许可,写一个只读的 页面(Linux 报告 Segmentation fault
7.9动态存储分配管理
Printf会调用malloc,下面简述动态内存管理的基本方法与策略。
当程序运行时,如果需要额外的虚拟内存时,可以用动态内存分配器来申请内存。动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap),假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。
对于每个进程,内核维护着一个变量 brk,它指向堆的顶部。分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
隐式空闲链表分配中,内存块的基本结构如下:
而对于显式空间链表,真实的操作系统实际上使用的是显示空闲链表管理。它的思路是维护多个空闲链表,每个链表中的块有大致相等的大小,分配器维护着一个空闲链表数组,每个大小类一个空闲链表,当需要分配块时只需要在对应的空闲链表中搜索就好了。
其一种实现的基本结构如下:
7.10本章小结
本章介绍了现代操作系统的灵魂:存储器地址空间、段式管理、页式管理,VA 到 PA 的变换、物理内存访问, hello 进程 fork 时和 execve 时的内存映射、缺页故障与缺页中断处理、包括隐式空闲链表和显式空闲链表的动态存储分配管理。这些巧妙的设计使得我们的 hello 最终得以运行。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
一个Linux 文件就是一个m个字节的序列:B,B1, …,B。 , …,Bm-1。所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输人和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行:
1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/О设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件< unistd.h>定义了常量sTDINFILENO、STDOUT_FILENO和 STDERR_FILENO,它们可用来代替显式的描述符值。
- 改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为k。
- 读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置é开始,然后将é增加到k十n。给定一个大小为m字节的文件,当k≥m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
- 关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
8.2 简述Unix IO接口及其函数
1)打开文件用open函数
int open(char *filename , int flags ,mode_t mode )
open函将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件:
O_RDONLY 只读
O_WRONLY 只写
O_RDWR 可读只写
- 关闭文件用close函数
int close(int fd) - 读和写文件
ssize_t read (int fd ,void *buf ,size_t n)
ssize_t write(int fd ,const void *buf ,size_t n)
应用程序分别调用read和write函数来执行输入和输出。
Read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。
write函数从内存位置buf复制至多n个字节到描述符fd的当前位置。下面是一个使用read和write调用一次一个字节地从标准输入复制到标准输出地程序:
图8-1输入输出程序示例
8.3 printf的实现分析
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
printf函数的函数体如下:
int printf(const char *fmt, ...)
{
int i;
char buf[256];
va_list arg = (va_list)((char*)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
vsprintf函数根据格式串fmt,并结合args参数产生格式化之后的字符串结果保存在buf中,并返回结果字符串的长度。
write函数将buf中的i个字符写到终端,由于i保存的是结果字符串的长度,因此write将格式化后的字符串结果写到终端。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
第一次调用getchar时,需要从键盘输入,但如果输入了多个字符,之后的getchar会直接从缓冲区中读取字符。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
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;
}
8.5本章小结
本章首先叙述了Linux的IO设备管理方法和Unix IO接口及其函数的有关知识,并分析了printf和getchar函数的实现。
结论
至此,hello 终于走完了它的一生,让我们为它的一生做个小结:
Hello程序的生命周期开始于程序员把其内容输入到文本编辑器中:字符数据经过总线最终被传输到寄存器,并在文件被关闭后保存到磁盘。
接下来将源文件通过gcc编译器预处理,编译,汇编,链接,最终完成一个可以加载到内存执行的可执行目标文件。一系列操作,为hello.c一个空壳注入了活的灵魂。
接下来通过shell输入文件名,shell通过fork创建一个新的进程,然后在子进程里通过execve函数将hello程序加载到内存。虚拟内存机制通过mmap为hello规划了一片空间,调度器为hello规划进程执行的时间片,使其能够与其他进程合理利用cpu与内存的资源。此时的它,才真正成为系统中独立的个体,往回看,逻辑控制流、虚拟地址、malloc的高效管理、异常与信号管理,这些都为它的驰骋拥有更加广阔的天地。
然后,cpu一条一条的从hello的.text取指令执行,不断从.data段去除数据。异常处理程序监视着键盘的输入。hello里面的一条syscall系统调用语句使进程触发陷阱,内核接手了进程,然后执行write函数,将一串字符传递给屏幕io的映射文件。文件对传入数据进行分析,读取vram,然后在屏幕上将字符显示出来。可以说,Unix I/O 打开它与程序使用者交流的窗口。
直至最后hello“垂垂老矣”,运行至最后一刻,程序运行结束,__libc_start_main 将控制转移给内核,Shell 回收子进程,内核删除与它相关的所有数据结构,它在这个世界的所有痕迹至此被抹去。
从键盘上敲出hello.c的源代码程序不过几分钟,从编译到运行,从敲下gcc到终端打印出hello信息,可能甚至不需要1秒钟。
但回首这短短的1秒,却惊心动魄,千难万险,其中的每一阶段无不汇集凝结了人类几十年的智慧与心血!
高低电平传递着信息,这些信息被复杂而严谨的机器逻辑捕捉。cpu不知疲倦的取指与执行。对于hello的实现细节,哪怕把这篇论文再扩充一倍仍讲不清楚。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c 源程序
hello.i 预处理后文件,由C预处理器产生,用于分析预处理过程。
hello.s 编译后的汇编文件,由C编译器产生,用于分析编译过程。
hello.o 汇编后的可重定位目标执行文件,由汇编器产生,用于分析汇编过程。
hello 链接后的可执行文件,由链接器产生,用于分析链接过程。
hello_elf.txt hello.o的ELF格式,用于分析可重定位目标文件hello.o。
Hello_elf2.txt hello的ELF格式,用于分析可执行目标文件hello。
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.
(参考文献0分,缺失 -1分)