Hello程序在从编写到执行结束这一生命周期中经历了预处理、编译、汇编、链接、进程管理、存储管理和IO管理这些过程。而这些过程的执行,也少不了操作系统、壳等软件和硬件的支撑。本文结合CSAPP课程中所学知识,使用Linux系统下的工具逐步分析hello的一生,研究hello.c程序文件的P2P和020过程。
关键词:计算机系统,Linux系统,P2P,020
目 录
第1章 概述
1.1 Hello简介
1.1.1 P2P(From Program to Process)
P2P过程是指高级程序语言文件转化为可执行进程的过程。hello.c在该过程中经历预处理、编译、汇编、链接四个阶段转变为可执行程序,然后被shell通过系统调用fork和execeve函数创建为进程。
预处理:hello.c在预处理器(cpp)中经过头文件包含、宏展开、条件编译、行链接等操作后,生成预处理后的中间文件hello.i。
编译:hello.i文件在C编译器(ccl)中被翻译为汇编语言程序,保存在文本文件hello.s中。
汇编:在汇编器(as)中hello.s被翻译为机器语言指令并打包保存形成可重定位目标文件hello.o。
链接:链接器(ld)将程序中的用到的库与hello.o进行合并形成可执行程序文件hello。
1.1.2 020(From Zero to Zero)
020过程是指将可执行文件载入内存并运行的过程。在此过程中,shell调用fork函数创建程序进程,然后调用execve函数将hello文件加载到内存中,通过映射得到虚拟地址空间实现hello从无到有的过程。内核中进程控制器为hello进程分配时间运行,经历大量异常和信号,读写访问存储器,与IO设备交互直到运行结束。程序运行完毕后父进程回收子进程 ,内核删除相关数据,释放虚拟空间地址,实现从有到无得过程。
1.2 环境与工具
硬件环境:X64 Intel i7-12700H CPU; 2300MHz; 16G RAM; 1.5THD disk
软件环境:Windows11 64 位;Virtualbox 7.0,;Ubantu 18.04 LTS 64 位
开发与调试工具:Visual Studio Code; gedit+gcc;gdb; readelf; objdump等
1.3 中间结果
hello.c | 源文件 |
hello.i | 预处理文件 |
hello.s | 汇编文件 |
hello.o | 可重定位目标文件 |
hello | 目标文件 |
hello.elf | 目标文件的ELF格式文件 |
hello_o.elf | hello.o的ELF格式文件 |
hello.asm | 目标文件的反汇编文件 |
hello_o.asm | hello.o的反汇编文件 |
1.4 本章小结
本章简述了hello 的P2P和020 过程,介绍了完成大作业的软硬件环境和开发调试工具,列举了过程中生成的中间文件和其作用。
第2章 预处理
2.1 预处理的概念与作用
2.1.1概念:预处理阶段是编译过程中的第一个阶段,其在实际编译之前对源代码进行处理。预处理器会对源代码进行一系列的操作,将源代码转换为适合编译器处理的形式。
2.1.2作用:
1.宏替换:预处理器会根据源文件中定义的宏进行替换。通过#define定义的宏会在预处理阶段被展开,将代码中的宏名称替换为对应的值。
2.头文件包含:预处理器会处理#include指令,将指定的头文件内容插入到源文件中。这样可以引入头文件中的声明和定义,方便代码的编写和维护。
3.条件编译:预处理器会根据条件编译指令(如#if、#ifdef、#ifndef、#elif、#else和#endif)来控制编译过程中代码的包含与排除,以实现在不同条件下编译不同的代码。
4.注释移除:预处理器会移除源文件中的注释,包括//和/* */形式的注释,以便后续编译器处理。
5.行连接:预处理器会将使用反斜杠\进行的行连接操作合并为一行,使代码更易阅读和维护。
6.符号解析:预处理阶段还会进行符号解析,识别和记录所有的符号,为它们分配内存地址或者生成符号表。
2.2在Ubuntu下预处理的命令
预处理命令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
由图2-3可知,源程序hello.c为24行,预处理得到的hello.i文件拓展为3106行。预处理操作有:一是去掉注释,二是对stdio.h、unistd.h、stdlib.h等头文件进行展开,删去头文件引用内容,将main函数放在文件末尾。
图2-3 hello.c和hello.i部分代码
2.4 本章小结
本章介绍了预处理的概念和作用和Ubantu下的预处理指令,并将hello.c源程序内容和预处理得到的hello.i程序内容进行对比,结合实例分析了预处理阶段的过程及作用。
第3章 编译
3.1 编译的概念与作用
3.1.1概念:
编译阶段是编译过程中的第二个阶段,其在预处理阶段之后,将经过预处理后的源代码转换为汇编代码。编译器会对源代码进行词法分析、语法分析和语义分析,生成中间表示形式,并最终生成目标代码。编译的主要目的是提高程序的执行效率,使程序更加稳定和安全。
3.1.2作用:
1.词法分析:编译器首先会进行词法分析,将源代码分解为词法单元(token),如关键字、标识符、运算符等。
2.语法分析:编译器会进行语法分析,根据语法规则将词法单元组合成语法结构,生成抽象语法树(Abstract Syntax Tree,AST)。
3.语义分析:编译器会进行语义分析,检查代码是否符合语言规范,进行类型检查、作用域检查等,以确保程序的正确性。
4.优化:编译器可能会进行优化操作,包括常量折叠、循环展开、内联函数等,以提高程序的性能和效率。
5.代码生成:最终,编译器会将经过优化后的中间代码生成目标机器代码,即将高级语言代码转换为特定硬件架构可执行的机器代码。
3.2 在Ubuntu下编译的命令
图3-2 hello.i编译
编译命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1 伪指令
图3.1.1 hello.s部分伪指令
汇编程序的伪指令以“.”开头,作用是指导汇编器和链接器的工作。
.file:声明源文件名称
.text: 表示代码段
.section: 指定接下来指令和数据所属的段
.rodata: 只读数据段
.align: 声明存放地址的对齐方式
.string:声明程序用到的字符串
.global: 全局变量
.type: 声明main是一个函数
3.3.2 数据
1.常量:包括数字常量和字符串常量,数字常量是立即数形式,字符串常量以只读数据形式存储在.rodata节。
图3-3-2-1 hello.s常量数据举例
2.变量:包括循环变量i,i也是局部变量、main函数参数整数argc、字符指针数组argv[],二者为外部变量。
图3-3-2-2 hello.s变量数据
3.3.3 数值操作
1.赋值:赋值指令为mov,例如把1赋值给%edi 寄存器。
2.算数操作:加法,add指令,对循环变量i累加。
3.3.4 关系操作
比较:cmp指令,将(%rbp-4)与立即数9进行比较,判断循环条件
3.3.5 数组/栈/指针操作
movq指令,在输出以及调用atoi函数时从栈上连续位置(argv[])获得数据进行处理。
3.3.6控制转移
hello.s包含if条件分支及for循环分支引起的跳转。用cmpl指令。
If条件跳转:cmpl判断argc 是否等于5,是则通过je指令跳转到.L2节;否则不执行跳转顺序执行。
For循环跳转:cmpl判断i是否小于等于9,若是则通过jle指令跳转到.L4节,继续执行循环内容;否则跳过jle结束循环顺序执行。
3.3.7 函数操作
调用函数前先将相应数据传递到%edi、%rdi寄存器作为函数参数。
调用函数用call指令
函数返回时用leave指令释放堆栈,然后用ret指令返回。
3.4 本章小结
本章介绍了编译的概念及作用,并结合hello.s实例分析各种数据结构和操作如何通过汇编代码实现。
第4章 汇编
4.1 汇编的概念与作用
4.1.1概念:
汇编阶段是编译过程中的第三个阶段,其在编译阶段之后,将生成的中间代码(通常是汇编代码)转换为目标机器代码。汇编器会将汇编指令翻译成特定硬件架构的机器指令,生成目标机器代码文件。
4.1.2作用:
1.汇编:汇编器会将编译生成的汇编代码翻译成特定硬件架构的机器指令。这个过程包括将汇编指令转换为相应的二进制表示形式,并生成与特定硬件架构相关的目标代码文件。
2.符号解析:汇编器会对汇编代码中的符号进行解析,识别并记录所有的符号(如变量名、函数名等),并为它们分配内存地址或者生成对应的符号表。
3.生成目标文件:汇编器最终会将生成的目标机器代码转化为目标文件,通常是以.o或.obj为扩展名的文件。这个目标文件包含了程序的部分代码,但还需要链接器将其与其他目标文件和库文件链接在一起,生成最终的可执行文件。
4.2 在Ubuntu下汇编的命令
图4-2 hello.s汇编
汇编指令:gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC hello.s -o hello.o
4.3 可重定位目标elf格式
输入命令readelf hello.o -a > hello_o.elf 获得hello.o的ELF格式。对hello_o.elf文件结构进行分析。
图4-3 hello.o生成ELF格式
4.3.1 ELF头
图4-3-1 ELF头信息
ELF头起始为一个16字节序列magic,前四个字节7f 45 4c 46分别对应ascii码中del(删除)、字符E、字符L、字符F。magic序列前四个字节作用为操作系统加载可执行文件时会通过确认magic序列是否正确来决定是否加载。第五个字节02表示ELF文件类型为64位,第六个字节01表示字节序为小端法,第七个字节01表示版本号。
ELF头剩下部分的信息包括目标文件类型为可重定位文件,系统架构为X86-64,节头部表的文件偏移量为1144bytes,大小为64字节以及包含条目的大小和数量等。
4.3.2 节头
图4-3-2 ELF节头信息
节头表包含的内容有各节的名称、类型、地址、偏移量、大小、旗标、链接、信息和对齐等,hello_o.elf包含13个节。
4.3.3 重定位节
图4-3-3 ELF重定位节信息
重定位节包含ELF的重定位条目,重定位条目用于指导链接器将目标文件合成可执行文件时如何修改这些位置。Hello.c中代码的重定位条目被保存在.rela.text中,.text代码段的重定位条目在.rela_frame中。重定位条目包含信息由有偏移量、信息、类型、符号值、符号名称、加数等。
4.3.4 符号表
符号表存放程序中定义和引用的函数和全局变量的信息。name代表的是字符串中的字节偏移,value是距定义目标的节的起始位置的偏移,size是目标的大小,type是类型(数据或函数);bind表示符号是本地的还是全局的。
图4-3-3 ELF符号表信息
4.4 Hello.o的结果解析
图4-4 hello.o反汇编与hello.s对比
反汇编命令:objdump -d -r hello.o。二者内容基本相同,反汇编代码在汇编代码的基础上增加了机器代码,说明机器代码与汇编指令时一一对应的。在以下部分二者存在区别:
- 分支转移:hello.s中分支转移地址依靠节头如.L3来标识,而在反汇编代码中,分支转移直接跳转到目的地址。
- 函数调用:hello.s中,用call+<函数名称>指令实现函数调用,而在反汇编程序中是call指令后是下条指令的地址。
- 数据访问:hello.s中,通过.LC0访问.rodata节中的数据,而在反汇编代码中通过$0x0访问数据节。
通过对比也可以得出结论:汇编语言和机器语言一一映射的。
4.5 本章小结
本章介绍了hello.s经过汇编器的得到可重定位文件hello.o的过程,并且使用readelf命令得到了hello.o中ELF的节头表、符号表、可重定位条目等信息,最后将hello.s与hello.o反汇编得到的汇编程序进行比较,分析二者的异同。
第5章 链接
5.1 链接的概念与作用
5.1.1概念:
链接阶段是编译过程中的最后一个阶段,其在汇编阶段之后,将编译生成的目标文件和库文件链接在一起,生成最终的可执行文件。链接器会解析目标文件之间的引用关系,将它们组合成一个完整的可执行程序。
5.1.2作用:
1.符号解析:链接器会对目标文件中的符号进行解析,包括全局符号和外部符号,以确定它们的实际地址或者生成对应的符号表。
2.地址和空间分配:链接器会为每个符号分配内存地址,解决符号之间的引用关系,生成最终的可执行代码所需的地址映射。
3.重定位:链接器会对目标文件中的地址进行重定位,将相对地址转换为绝对地址,以确保程序在内存中能正确执行。
4.库链接:链接器会将程序需要的库文件链接进来,将程序所需的函数和数据与库文件中的实现进行关联,生成最终的可执行文件。
5.生成可执行文件:最终,链接器会将经过符号解析、地址分配、重定位和库链接处理后的目标文件生成最终的可执行文件,该文件包含了完整的程序代码和数据,可以直接在特定平台上执行。
5.2 在Ubuntu下链接的命令
图5-2 hello.o链接
链接命令: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.ohello.o/usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
5.3 可执行目标文件hello的格式
5.3.1 ELF 头
图5-3-1 ELF头信息
用readelf -h hello命令查看hello的ELF头。Hello的ELF头与hello.o_elf的基本相同,区别在文件类型由REG变为EXEC(可执行文件),而且程序头大小和节头数量增加,入口点地址不再为0。
5.3.2 节头
图5-3-2 ELF节头信息
用readelf -S hello命令查看hello的节头表。链接后,节头数量增加,每条包含信息种类不变。
5.3.3 重定位节
图5-3-3 ELF重定位节信息
用readelf -r hello命令查看hello的重定位节。链接后,重定位节内容变化为执行过程中需要通过动态链接调用的函数,同时类型也发生改变。
5.3.4 符号表
图5-3-4 ELF符号表信息
用readelf -s hello命令查看hello的符号表。链接后,符号表条增加,包含各目标文件中符号定义及引用信息。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息。观察发现程序占有0x40000~ 0x400ff0的地址空间。虚拟空间起始位置是0x400000,根据5.3中入口地址及各节偏移量即可查看各节内容。
图5-4 hello的虚拟地址空间起始位置
5.5 链接的重定位过程分析
调用objdump -d -r hello命令,得到hello反汇编程序。并观察main函数部分并与hello.o反汇编代码对比。
图5-5 反汇编对比(左hello.o,右hello)
观察两段反汇编代码发现有如下差异:
- 虚拟地址不同,hello.o的反汇编代码虚拟地址从0开始,而hello的反汇编代码虚拟地址从0x4004c0开始。这是因为hello.o在链接之前只能给出相对地址,而hello在链接之后得到的是绝对地址。
- 反汇编节数不同,hello比hello.o多出了许多文件节以及外部链接的函数,如printf函数、.init节和.plt节等。
- 跳转指令不同,hello.o中的跳转指令后加的主要是汇编代码块前的标号,而hello中的跳转指令后加的则是具体的地址。
由此可分析出重定位是将多个单独的代码节和数据节合并,将符号从它们在.o文件中的相对位置重新定位到可执行文件中最终绝对的内存位置,并用新位置更新对各节中对这些符号的引用。
5.6 hello的执行流程
使用EDB执行hello, 右键点击analyze here可以得到所有程序过程。
图5-6 EDB运行hello
程序名称 地址
ld-2.27.so!_dl_start 0x7ffe68c3a148
ld-2.27.so!_dl_init 0x7fce8cc47630
hello!_start 0x401090
hello!_init 0x401000
ld-2.27.so!.plt 0x401020
hello!main 0x4010c1
hello!puts@plt 0x401030
hello!printf@plt 0x401040
hello!getchar@plt 0x401050
hello!atoi@plt 0x401060
hello!exit@plt 0x401070
hello!sleep@plt 0x401080
hello!_dl_relocate_static_pie 0x4010c0
-libc-2.27.so!__libc_csu_init 0x4005c0
libc-2.27.so!exit 0x7fce8c889128
5.7 Hello的动态链接分析
程序调用一个有共享库定义的函数时,编译器无法预测函数在运行时的地址。因此,编译系统采用延迟绑定,将过程地址的绑定推迟到第一次调用该过程的时候。动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。PLT是一个数组,其中每个条目是16字节代码;GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。PLT使用 GOT中地址跳转到目标函数。
查询hello得到.got起始位置0x403ff0,.got.plt起始位置为0x404000。
程序调用dl_init之前,先查看0x404000位置的内容:
在调用dl_init后,可以看到对应内容发生了变化。
5.8 本章小结
本章节简要介绍了链接的相关过程,首先简要阐述了链接的概念和作用,给出了链接在Ubuntu系统下的指令。之后研究了可执行目标文件hello的ELF格式,之后依据重定位条目分析了重定位的过程,并借助edb调试工具,研究了程序中各个子程序的执行流程,最后则借助edb调试工具通过对虚拟内存的查取,分析研究了动态链接的过程。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1概念:"进程"是一个重要的概念,它代表着正在运行的程序的实例。在计算机系统中,进程是程序在执行过程中的实体。
6.2.2 作用:
1.程序的执行实体:进程是程序在执行过程中的实体,它包含了程序的代码、数据、执行状态等信息。每个正在运行的程序都对应着一个或多个进程。
2.资源分配和管理:进程是操作系统进行资源分配和管理的基本单位。操作系统为每个进程分配内存空间、CPU时间片、文件描述符等资源,并负责管理这些资源的分配和释放。
3.并发执行:进程使得多个程序能够并发执行,即在同一时间内同时运行多个程序。操作系统通过对进程进行调度,实现了多任务的并发执行。
4.通信与同步:进程之间可以进行通信和同步操作,这使得不同的进程能够协同工作、共享数据,实现复杂的任务和功能。
5.安全性和隔离:进程之间是相互隔离的,每个进程拥有自己的地址空间和资源,这样可以确保进程之间的安全性和稳定性。
6.2 简述壳Shell-bash的作用与处理流程
壳(Shell)是计算机操作系统的用户界面,它允许用户与操作系统进行交互,执行命令并管理文件系统和其他系统资源。其中,bash(Bourne Again SHell)是一种常见的Unix和Linux系统上使用的壳程序,它的作用和处理流程如下:
作用:
1.提供用户界面:bash作为壳程序提供了用户与操作系统进行交互的界面,用户可以通过命令行或脚本来执行各种操作系统的功能。
2.解释和执行命令:bash接收用户输入的命令,并将其解释为系统调用或其他操作,然后执行这些命令以完成用户的要求。
3.管理文件系统和进程:bash可以用于管理文件和目录,以及启动、停止和管理系统中的进程。
4.支持脚本编程:bash支持编写脚本,用户可以将一系列的命令组合成一个脚本文件,以便重复执行或自动化执行特定任务。
处理流程:
1.提示符:bash通常会显示一个提示符,等待用户输入命令。
2.读取输入:用户输入命令后,bash会读取并解释这些命令。
3.判断命令是否为内置命令,是则立即执行,否则调用fork()来创建子进程,自身调用wait()来等待子进程完成,同时在程序执行期间始终接受键盘输入信号,并对输入信号做相应处理。
4.当子进程运行时,调用execve()函数,同时根据命令的名字指定的文件到目录中查找可行性文件,调入内存并执行这个命令。
5.当子进程完成处理后,向父进程shell报告,此时终端进程被唤醒,做完必要的判别工作后,再发提示符,让用户输入新命令。
6.3 Hello的fork进程创建过程
在命令行输入./hello命令运行hello程序,此命令不是内置命令,因此Bash通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同,子进程得到父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,也就意味着父进程调用fork函数时,子进程能够读写父进程打开的任何文件。父进程和新创建的子进程之间最大的区别在于其不同的PID。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。execve函数一次调用而从不返回。
6.5 Hello的进程执行
1.上下文切换:
当操作系统需要切换当前运行的进程到另一个进程时,就会进行上下文切换。这个过程包括保存当前进程的执行状态(如寄存器的值、程序计数器等)并加载下一个进程的执行状态,以确保进程能够继续执行。
上下文切换是由操作系统内核负责管理的,它会涉及到切换内存映射、切换进程控制块等操作,因此是一个开销较大的操作。
2.时间分片:
时间分片是操作系统调度策略的一种,它将CPU的执行时间划分成小的时间片段,每个进程在一个时间片段内执行一定的时间。当一个时间片段结束时,操作系统会进行上下文切换,将CPU分配给另一个进程继续执行。这样可以实现多个进程之间的轮流执行,从而实现并发执行的效果。
3.用户态与核心态:
CPU在执行指令时有不同的权限级别,通常分为用户态(用户模式)和核心态(内核模式)两种。
用户态下,进程只能访问受限的资源,不能直接操作硬件,这样可以确保进程不能对系统造成破坏。而在核心态下,操作系统内核拥有对系统资源的完全控制权,可以执行特权指令、访问系统资源等
图6.5 进程上下文切换机制的示意图
6.6 hello的异常与信号处理
1.乱按
在程序运行时乱打字,不影响程序的正常运行。随著printf的调用保留在了输出结果中。
2. ctrl-Z
进程收到SIGSTP信号,hello停止,此时进程并未回收,而是后台运行,通过ps指令可以对其进行查看,还可以通过fg指令将其调回前台。
- ctrl-C
进程收到SIGINT信号,hello终止。在ps中查询不到此进程及其PID。
- kill
挂起的进程被杀死,在ps中无法查到到其PID。
- pstree
用树状图显示所有进程结构。
6.7本章小结
本章简要介绍了hello进程大致的执行过程,阐述了进程、shell、fork、execve等相关概念,之后从上下文切换、时间分片、用户态与核心态等角度详细分析了进程的执行过程。并在运行时尝试了不同形式的命令和异常,每种信号都有不同处理机制,针对不同的shell命令,hello会产生不同响应。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1.逻辑地址:
逻辑地址是指程序中使用的地址,它是相对于程序自身的地址空间来说的,是程序员编写程序时使用的地址。在程序中使用的逻辑地址在编译和链接过程中会被转换成相应的虚拟地址。
2.线性地址:
线性地址是指经过分段机制转换后的地址,它是虚拟地址空间中的地址,是CPU在执行指令时使用的地址。线性地址是通过分段机制将逻辑地址转换成的,它包含了段选择子和偏移量。
3.虚拟地址:
虚拟地址是指程序中使用的地址,经过地址转换机制转换后在虚拟内存空间中的地址。虚拟地址是在程序运行时使用的地址,它提供了一个抽象的、独立的地址空间,使得每个进程都认为自己拥有整个内存空间。
4.物理地址:
物理地址是指内存中实际的地址,它是RAM芯片上的地址。当程序需要访问内存中的数据时,虚拟地址会经过地址映射机制转换成对应的物理地址,然后才能在内存中找到相应的数据。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在Intel x86 架构中,逻辑地址到线性地址的变换是通过段式管理实现的。段式管理是一种内存管理方式,它将内存划分为多个段,每个段都有自己的基地址和限长。在Intel x86 架构中,段式管理的实现涉及到以下几个步骤:
1.逻辑地址:在程序执行时,CPU生成的地址是逻辑地址。逻辑地址是程序中使用的地址,它是相对于程序自身的地址空间来说的。
2.段寄存器:在x86 架构中,有一些特殊的寄存器,如CS(代码段寄存器)、DS(数据段寄存器)、SS(堆栈段寄存器)等,它们存储了段选择子,用于指示当前程序使用的段。
3.段选择子:段选择子是一个16位的值,它包含了段的索引和段的特权级别等信息。
4.段描述符:每个段都有一个对应的段描述符,段描述符包含了段的基地址、限长、访问权限等信息。
5.段式变换:当CPU生成逻辑地址时,它会使用段选择子来访问全局描述符表或局部描述符表中的段描述符。CPU使用段选择子中的索引值在描述符表中找到对应的段描述符,然后使用段描述符中的基地址和偏移量来计算出线性地址
6.线性地址是通过段式变换后得到的地址,它是CPU在执行指令时使用的地址。线性地址是经过分段机制转换后的地址,它包含了段的基地址和偏移量。
7.3 Hello的线性地址到物理地址的变换-页式管理
将程序的逻辑地址空间和物理内存划分为等长的页面,这种分配方式便于维护,且不容易产生碎块。在页式存储管理方式中地址结构由两部构成,前一部分是虚拟页号(VPN),后一部分为虚拟页偏移量(VPO)。如图所示,页号用于在页表中查找对应的页表项,页表项中包含了该虚拟页所映射的物理页号以及访问权限等信息,通过将物理页号和页内偏移量组合得到最终的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
1.页全局目录:首先,处理器检查页全局目录(PGD)中的条目。如果所需的虚拟地址在TLB中不存在,处理器将查询PGD以找到相应的页目录(PD)的基地址。
页上级目录:处理器使用虚拟地址的高级别位索引到页上级目录(PUD)。如果所需的虚拟地址在TLB中不存在,处理器将查询PUD以找到相应的页中间目录(PMD)的基地址。
2.页中间目录:处理器使用虚拟地址的中级别位索引到页中间目录(PMD)。如果所需的虚拟地址在TLB中不存在,处理器将查询PMD以找到相应的页表(PT)的基地址。
3.页表:处理器使用虚拟地址的低级别位索引到页表(PT),找到相应的页框(page frame)的物理地址。
4.TLB检查:在每个步骤中,处理器都会检查TLB以查看所需的虚拟地址是否已经在TLB中缓存。如果存在,则直接使用TLB中的物理地址进行访问。
5.物理地址:经过上述步骤后,处理器获得了目标页框的物理地址,与虚拟地址的低级别位相加,得到最终的物理地址(PA)。
通过TLB和四级页表的支持,CPU可以快速地将虚拟地址转换为物理地址,从而实现对内存的访问。
7.5 三级Cache支持下的物理内存访问
三级缓存是一种采用多级缓存的存储体系,可以提高计算机内存访问速度和效率。在三级缓存的架构中,缓存分为L1、L2和L3三级,每一级缓存都有不同的容量和访问速度。
当CPU需要访问内存时,首先在L1缓存中查找数据,先找组索引位,然后与标志位对比。如果L1缓存中未命中,则需要从存储层次结构中的下一层(即L2缓存)查找。如若仍未命中,则会继续在L3缓存中查找。如果在L3缓存中也未命中,则会从主存中获取数据。
如果在三级缓存中找到了需要的数据,则可以直接访问缓存中的数据,从而提高访问速度。如果在三级缓存中没有找到,则需要从主存中获取数据,并将数据存入三级缓存中,以便下次访问时可以更快地获取数据。
7.6 hello进程fork时的内存映射
当fork函数被调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本,并将两个进程中的每个界面都标记为只读,将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存搞好和调用fork时存在的虚拟内存相同。这两个进程中的任意一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
当一个进程使用execve()系统调用时,它会替换当前进程的内存映像为一个新的程序。execve()系统调用将加载新的程序到进程的地址空间,并释放旧程序的内存映射。以下是execve()系统调用后进程的内存映射情况:
代码段:被替换为新程序的代码段。这是新程序的执行代码。
数据段:被替换为新程序的数据段。这包括全局变量和静态变量等。
堆:堆区域也会被替换为新程序所需的堆区域。新程序可以使用malloc()、free()等函数来分配和释放内存。
栈:栈区域也会被替换为新程序所需的栈区域。这包括保存函数调用的返回地址和局部变量等。
execve()系统调用用于在当前进程中加载并执行一个新的程序,它会替换进程的内存映像,并释放旧程序的内存映射。这使得进程能够执行不同的程序,并为其提供独立的内存空间。
7.8 缺页故障与缺页中断处理
缺页故障指访问一个虚拟地址的数据时,对应的物理地址不在内存中,从而引发的异常情况。页面命中完全是由硬件完成的,而处理缺页异常是由硬件和操作系统内核协作完成的。
当发生缺页故障时,处理器会向操作系统发出缺页中断信号,缺页处理程序确定物理内存中的牺牲页(若页面被修改,则换出到磁盘),而后调入新的页面,并更新内存中的PTE。最后缺页处理程序返回到原来进程,再次执行导致缺页的指令,从而完成内存访问。
7.9动态存储分配管理
动态内存分配主要使用了动态内存分配器,动态内存分配器维护了一个进程的虚拟内存区,称为堆。分配器将堆是为一组不同大小的块的集合来维护,每个块都是一个连续的虚拟内存片,虚拟内存片要么是已分配的,要么是空闲的。已分配的块显式地保留从而为供应应用程序使用。空闲块可以用来分配,空闲块在被应用分配之前都会保持空闲状态,而已分配的块在被释放之前都会保持已分配状态。释放要么由应用程序显式执行,要么是内存分配器自身隐式执行的。
分配器有两种基本风格:显示分配器:要求应用显式地释放任何已分配的块
隐式分配器要求分配器监测一个已分配块何时不再被应用程序所使用。并在不被使用时释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用地已分配的块的过程叫做垃圾收集。
7.10本章小结
本章简要介绍存储相关的知识。介绍不同地址概念以及他们之间的转换,讲述fork和execve函数的存储映射,最后介绍缺页处理和动态内存分配。
结论
Hello的一生经历坎坷,人生阶段如下:
预处理:hello.c在预处理器(cpp)中经过头文件包含、宏展开、条件编译、行链接等操作后,生成预处理后的中间文件hello.i。
编译:hello.i文件在C编译器(ccl)中被翻译为汇编语言程序,保存在文本文件hello.s中。
汇编:在汇编器(as)中hello.s被翻译为机器语言指令并打包保存形成可重定位目标文件hello.o。
链接:链接器(ld)将程序中的用到的库与hello.o进行合并形成可执行程序文件hello。
进程载入:通过Bash键入命令./hello 2021112845 zzx 3,操作系统为程序fork新进程并通过execve加载代码和数据到为其提供的私有虚拟内存空间,程序开始执行。
进程控制:由进程调度器对进程进行时间片调度,并通过上下文切换实现hello的执行,程序计数器(PC)更新,CPU按顺序取指,执行程序控制逻辑。
内存访问:内存管理单元MMU将逻辑地址逐步转换成物理地址,通过三级Cache访问物理内存/磁盘中的数据。
信号处理:进程接收信号,调用相应的信号处理函数对信号进行终止、停止、前/后台运行等处理。
进程回收:Shell等待并回收子进程,内核删除为进程创建的所有资源。
通过本次大作业,我认识到计算机系统中程序的运行涉及到的远远比我之前认识的要复杂得多,我们日常使用计算机中每一个简单的操作背后都有丰富的理论知识和复杂得软硬件架构作为支撑。我们要学号计算机知识,就要全身心投入,追根究底,在前人伟大的构思的基础上精进知识,力求创新。
附件
hello.c | 源文件 |
hello.i | 预处理文件 |
hello.s | 汇编文件 |
hello.o | 可重定位目标文件 |
hello | 目标文件 |
hello.elf | 目标文件的ELF格式文件 |
hello_o.elf | hello.o的ELF格式文件 |
hello.asm | 目标文件的反汇编文件 |
hello_o.asm | hello.o的反汇编文件 |
参考文献
[1]RANDALE.BRYANT, DAVIDR.O’HALLARON. 深入理解计算机系统[M]. 机械工业出版社, 2011.
[2] https://ysyx.oscc.cc/slides/hello-x86.html
摘 要
Hello程序在从编写到执行结束这一生命周期中经历了预处理、编译、汇编、链接、进程管理、存储管理和IO管理这些过程。而这些过程的执行,也少不了操作系统、壳等软件和硬件的支撑。本文结合CSAPP课程中所学知识,使用Linux系统下的工具逐步分析hello的一生,研究hello.c程序文件的P2P和020过程。
关键词:计算机系统,Linux系统,P2P,020
目 录
第1章 概述
1.1 Hello简介
1.1.1 P2P(From Program to Process)
P2P过程是指高级程序语言文件转化为可执行进程的过程。hello.c在该过程中经历预处理、编译、汇编、链接四个阶段转变为可执行程序,然后被shell通过系统调用fork和execeve函数创建为进程。
预处理:hello.c在预处理器(cpp)中经过头文件包含、宏展开、条件编译、行链接等操作后,生成预处理后的中间文件hello.i。
编译:hello.i文件在C编译器(ccl)中被翻译为汇编语言程序,保存在文本文件hello.s中。
汇编:在汇编器(as)中hello.s被翻译为机器语言指令并打包保存形成可重定位目标文件hello.o。
链接:链接器(ld)将程序中的用到的库与hello.o进行合并形成可执行程序文件hello。
1.1.2 020(From Zero to Zero)
020过程是指将可执行文件载入内存并运行的过程。在此过程中,shell调用fork函数创建程序进程,然后调用execve函数将hello文件加载到内存中,通过映射得到虚拟地址空间实现hello从无到有的过程。内核中进程控制器为hello进程分配时间运行,经历大量异常和信号,读写访问存储器,与IO设备交互直到运行结束。程序运行完毕后父进程回收子进程 ,内核删除相关数据,释放虚拟空间地址,实现从有到无得过程。
1.2 环境与工具
硬件环境:X64 Intel i7-12700H CPU; 2300MHz; 16G RAM; 1.5THD disk
软件环境:Windows11 64 位;Virtualbox 7.0,;Ubantu 18.04 LTS 64 位
开发与调试工具:Visual Studio Code; gedit+gcc;gdb; readelf; objdump等
1.3 中间结果
hello.c | 源文件 |
hello.i | 预处理文件 |
hello.s | 汇编文件 |
hello.o | 可重定位目标文件 |
hello | 目标文件 |
hello.elf | 目标文件的ELF格式文件 |
hello_o.elf | hello.o的ELF格式文件 |
hello.asm | 目标文件的反汇编文件 |
hello_o.asm | hello.o的反汇编文件 |
1.4 本章小结
本章简述了hello 的P2P和020 过程,介绍了完成大作业的软硬件环境和开发调试工具,列举了过程中生成的中间文件和其作用。
第2章 预处理
2.1 预处理的概念与作用
2.1.1概念:预处理阶段是编译过程中的第一个阶段,其在实际编译之前对源代码进行处理。预处理器会对源代码进行一系列的操作,将源代码转换为适合编译器处理的形式。
2.1.2作用:
1.宏替换:预处理器会根据源文件中定义的宏进行替换。通过#define定义的宏会在预处理阶段被展开,将代码中的宏名称替换为对应的值。
2.头文件包含:预处理器会处理#include指令,将指定的头文件内容插入到源文件中。这样可以引入头文件中的声明和定义,方便代码的编写和维护。
3.条件编译:预处理器会根据条件编译指令(如#if、#ifdef、#ifndef、#elif、#else和#endif)来控制编译过程中代码的包含与排除,以实现在不同条件下编译不同的代码。
4.注释移除:预处理器会移除源文件中的注释,包括//和/* */形式的注释,以便后续编译器处理。
5.行连接:预处理器会将使用反斜杠\进行的行连接操作合并为一行,使代码更易阅读和维护。
6.符号解析:预处理阶段还会进行符号解析,识别和记录所有的符号,为它们分配内存地址或者生成符号表。
2.2在Ubuntu下预处理的命令
图2-2 hello.c预处理
预处理命令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
由图2-3可知,源程序hello.c为24行,预处理得到的hello.i文件拓展为3106行。预处理操作有:一是去掉注释,二是对stdio.h、unistd.h、stdlib.h等头文件进行展开,删去头文件引用内容,将main函数放在文件末尾。
图2-3 hello.c和hello.i部分代码
2.4 本章小结
本章介绍了预处理的概念和作用和Ubantu下的预处理指令,并将hello.c源程序内容和预处理得到的hello.i程序内容进行对比,结合实例分析了预处理阶段的过程及作用。
第3章 编译
3.1 编译的概念与作用
3.1.1概念:
编译阶段是编译过程中的第二个阶段,其在预处理阶段之后,将经过预处理后的源代码转换为汇编代码。编译器会对源代码进行词法分析、语法分析和语义分析,生成中间表示形式,并最终生成目标代码。编译的主要目的是提高程序的执行效率,使程序更加稳定和安全。
3.1.2作用:
1.词法分析:编译器首先会进行词法分析,将源代码分解为词法单元(token),如关键字、标识符、运算符等。
2.语法分析:编译器会进行语法分析,根据语法规则将词法单元组合成语法结构,生成抽象语法树(Abstract Syntax Tree,AST)。
3.语义分析:编译器会进行语义分析,检查代码是否符合语言规范,进行类型检查、作用域检查等,以确保程序的正确性。
4.优化:编译器可能会进行优化操作,包括常量折叠、循环展开、内联函数等,以提高程序的性能和效率。
5.代码生成:最终,编译器会将经过优化后的中间代码生成目标机器代码,即将高级语言代码转换为特定硬件架构可执行的机器代码。
3.2 在Ubuntu下编译的命令
图3-2 hello.i编译
编译命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1 伪指令
图3.1.1 hello.s部分伪指令
汇编程序的伪指令以“.”开头,作用是指导汇编器和链接器的工作。
.file:声明源文件名称
.text: 表示代码段
.section: 指定接下来指令和数据所属的段
.rodata: 只读数据段
.align: 声明存放地址的对齐方式
.string:声明程序用到的字符串
.global: 全局变量
.type: 声明main是一个函数
3.3.2 数据
1.常量:包括数字常量和字符串常量,数字常量是立即数形式,字符串常量以只读数据形式存储在.rodata节。
图3-3-2-1 hello.s常量数据举例
2.变量:包括循环变量i,i也是局部变量、main函数参数整数argc、字符指针数组argv[],二者为外部变量。
图3-3-2-2 hello.s变量数据
3.3.3 数值操作
1.赋值:赋值指令为mov,例如把1赋值给%edi 寄存器。
2.算数操作:加法,add指令,对循环变量i累加。
3.3.4 关系操作
比较:cmp指令,将(%rbp-4)与立即数9进行比较,判断循环条件
3.3.5 数组/栈/指针操作
movq指令,在输出以及调用atoi函数时从栈上连续位置(argv[])获得数据进行处理。
3.3.6控制转移
hello.s包含if条件分支及for循环分支引起的跳转。用cmpl指令。
If条件跳转:cmpl判断argc 是否等于5,是则通过je指令跳转到.L2节;否则不执行跳转顺序执行。
For循环跳转:cmpl判断i是否小于等于9,若是则通过jle指令跳转到.L4节,继续执行循环内容;否则跳过jle结束循环顺序执行。
3.3.7 函数操作
调用函数前先将相应数据传递到%edi、%rdi寄存器作为函数参数。
调用函数用call指令
函数返回时用leave指令释放堆栈,然后用ret指令返回。
3.4 本章小结
本章介绍了编译的概念及作用,并结合hello.s实例分析各种数据结构和操作如何通过汇编代码实现。
第4章 汇编
4.1 汇编的概念与作用
4.1.1概念:
汇编阶段是编译过程中的第三个阶段,其在编译阶段之后,将生成的中间代码(通常是汇编代码)转换为目标机器代码。汇编器会将汇编指令翻译成特定硬件架构的机器指令,生成目标机器代码文件。
4.1.2作用:
1.汇编:汇编器会将编译生成的汇编代码翻译成特定硬件架构的机器指令。这个过程包括将汇编指令转换为相应的二进制表示形式,并生成与特定硬件架构相关的目标代码文件。
2.符号解析:汇编器会对汇编代码中的符号进行解析,识别并记录所有的符号(如变量名、函数名等),并为它们分配内存地址或者生成对应的符号表。
3.生成目标文件:汇编器最终会将生成的目标机器代码转化为目标文件,通常是以.o或.obj为扩展名的文件。这个目标文件包含了程序的部分代码,但还需要链接器将其与其他目标文件和库文件链接在一起,生成最终的可执行文件。
4.2 在Ubuntu下汇编的命令
图4-2 hello.s汇编
汇编指令:gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC hello.s -o hello.o
4.3 可重定位目标elf格式
输入命令readelf hello.o -a > hello_o.elf 获得hello.o的ELF格式。对hello_o.elf文件结构进行分析。
图4-3 hello.o生成ELF格式
4.3.1 ELF头
图4-3-1 ELF头信息
ELF头起始为一个16字节序列magic,前四个字节7f 45 4c 46分别对应ascii码中del(删除)、字符E、字符L、字符F。magic序列前四个字节作用为操作系统加载可执行文件时会通过确认magic序列是否正确来决定是否加载。第五个字节02表示ELF文件类型为64位,第六个字节01表示字节序为小端法,第七个字节01表示版本号。
ELF头剩下部分的信息包括目标文件类型为可重定位文件,系统架构为X86-64,节头部表的文件偏移量为1144bytes,大小为64字节以及包含条目的大小和数量等。
4.3.2 节头
图4-3-2 ELF节头信息
节头表包含的内容有各节的名称、类型、地址、偏移量、大小、旗标、链接、信息和对齐等,hello_o.elf包含13个节。
4.3.3 重定位节
图4-3-3 ELF重定位节信息
重定位节包含ELF的重定位条目,重定位条目用于指导链接器将目标文件合成可执行文件时如何修改这些位置。Hello.c中代码的重定位条目被保存在.rela.text中,.text代码段的重定位条目在.rela_frame中。重定位条目包含信息由有偏移量、信息、类型、符号值、符号名称、加数等。
4.3.4 符号表
符号表存放程序中定义和引用的函数和全局变量的信息。name代表的是字符串中的字节偏移,value是距定义目标的节的起始位置的偏移,size是目标的大小,type是类型(数据或函数);bind表示符号是本地的还是全局的。
图4-3-3 ELF符号表信息
4.4 Hello.o的结果解析
图4-4 hello.o反汇编与hello.s对比
反汇编命令:objdump -d -r hello.o。二者内容基本相同,反汇编代码在汇编代码的基础上增加了机器代码,说明机器代码与汇编指令时一一对应的。在以下部分二者存在区别:
- 分支转移:hello.s中分支转移地址依靠节头如.L3来标识,而在反汇编代码中,分支转移直接跳转到目的地址。
- 函数调用:hello.s中,用call+<函数名称>指令实现函数调用,而在反汇编程序中是call指令后是下条指令的地址。
- 数据访问:hello.s中,通过.LC0访问.rodata节中的数据,而在反汇编代码中通过$0x0访问数据节。
通过对比也可以得出结论:汇编语言和机器语言一一映射的。
4.5 本章小结
本章介绍了hello.s经过汇编器的得到可重定位文件hello.o的过程,并且使用readelf命令得到了hello.o中ELF的节头表、符号表、可重定位条目等信息,最后将hello.s与hello.o反汇编得到的汇编程序进行比较,分析二者的异同。
第5章 链接
5.1 链接的概念与作用
5.1.1概念:
链接阶段是编译过程中的最后一个阶段,其在汇编阶段之后,将编译生成的目标文件和库文件链接在一起,生成最终的可执行文件。链接器会解析目标文件之间的引用关系,将它们组合成一个完整的可执行程序。
5.1.2作用:
1.符号解析:链接器会对目标文件中的符号进行解析,包括全局符号和外部符号,以确定它们的实际地址或者生成对应的符号表。
2.地址和空间分配:链接器会为每个符号分配内存地址,解决符号之间的引用关系,生成最终的可执行代码所需的地址映射。
3.重定位:链接器会对目标文件中的地址进行重定位,将相对地址转换为绝对地址,以确保程序在内存中能正确执行。
4.库链接:链接器会将程序需要的库文件链接进来,将程序所需的函数和数据与库文件中的实现进行关联,生成最终的可执行文件。
5.生成可执行文件:最终,链接器会将经过符号解析、地址分配、重定位和库链接处理后的目标文件生成最终的可执行文件,该文件包含了完整的程序代码和数据,可以直接在特定平台上执行。
5.2 在Ubuntu下链接的命令
图5-2 hello.o链接
链接命令: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.ohello.o/usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
5.3 可执行目标文件hello的格式
5.3.1 ELF 头
图5-3-1 ELF头信息
用readelf -h hello命令查看hello的ELF头。Hello的ELF头与hello.o_elf的基本相同,区别在文件类型由REG变为EXEC(可执行文件),而且程序头大小和节头数量增加,入口点地址不再为0。
5.3.2 节头
图5-3-2 ELF节头信息
用readelf -S hello命令查看hello的节头表。链接后,节头数量增加,每条包含信息种类不变。
5.3.3 重定位节
图5-3-3 ELF重定位节信息
用readelf -r hello命令查看hello的重定位节。链接后,重定位节内容变化为执行过程中需要通过动态链接调用的函数,同时类型也发生改变。
5.3.4 符号表
图5-3-4 ELF符号表信息
用readelf -s hello命令查看hello的符号表。链接后,符号表条增加,包含各目标文件中符号定义及引用信息。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息。观察发现程序占有0x40000~ 0x400ff0的地址空间。虚拟空间起始位置是0x400000,根据5.3中入口地址及各节偏移量即可查看各节内容。
图5-4 hello的虚拟地址空间起始位置
5.5 链接的重定位过程分析
调用objdump -d -r hello命令,得到hello反汇编程序。并观察main函数部分并与hello.o反汇编代码对比。
图5-5 反汇编对比(左hello.o,右hello)
观察两段反汇编代码发现有如下差异:
- 虚拟地址不同,hello.o的反汇编代码虚拟地址从0开始,而hello的反汇编代码虚拟地址从0x4004c0开始。这是因为hello.o在链接之前只能给出相对地址,而hello在链接之后得到的是绝对地址。
- 反汇编节数不同,hello比hello.o多出了许多文件节以及外部链接的函数,如printf函数、.init节和.plt节等。
- 跳转指令不同,hello.o中的跳转指令后加的主要是汇编代码块前的标号,而hello中的跳转指令后加的则是具体的地址。
由此可分析出重定位是将多个单独的代码节和数据节合并,将符号从它们在.o文件中的相对位置重新定位到可执行文件中最终绝对的内存位置,并用新位置更新对各节中对这些符号的引用。
5.6 hello的执行流程
使用EDB执行hello, 右键点击analyze here可以得到所有程序过程。
图5-6 EDB运行hello
程序名称 地址
ld-2.27.so!_dl_start 0x7ffe68c3a148
ld-2.27.so!_dl_init 0x7fce8cc47630
hello!_start 0x401090
hello!_init 0x401000
ld-2.27.so!.plt 0x401020
hello!main 0x4010c1
hello!puts@plt 0x401030
hello!printf@plt 0x401040
hello!getchar@plt 0x401050
hello!atoi@plt 0x401060
hello!exit@plt 0x401070
hello!sleep@plt 0x401080
hello!_dl_relocate_static_pie 0x4010c0
-libc-2.27.so!__libc_csu_init 0x4005c0
libc-2.27.so!exit 0x7fce8c889128
5.7 Hello的动态链接分析
程序调用一个有共享库定义的函数时,编译器无法预测函数在运行时的地址。因此,编译系统采用延迟绑定,将过程地址的绑定推迟到第一次调用该过程的时候。动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。PLT是一个数组,其中每个条目是16字节代码;GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。PLT使用 GOT中地址跳转到目标函数。
查询hello得到.got起始位置0x403ff0,.got.plt起始位置为0x404000。
程序调用dl_init之前,先查看0x404000位置的内容:
在调用dl_init后,可以看到对应内容发生了变化。
5.8 本章小结
本章节简要介绍了链接的相关过程,首先简要阐述了链接的概念和作用,给出了链接在Ubuntu系统下的指令。之后研究了可执行目标文件hello的ELF格式,之后依据重定位条目分析了重定位的过程,并借助edb调试工具,研究了程序中各个子程序的执行流程,最后则借助edb调试工具通过对虚拟内存的查取,分析研究了动态链接的过程。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1概念:"进程"是一个重要的概念,它代表着正在运行的程序的实例。在计算机系统中,进程是程序在执行过程中的实体。
6.2.2 作用:
1.程序的执行实体:进程是程序在执行过程中的实体,它包含了程序的代码、数据、执行状态等信息。每个正在运行的程序都对应着一个或多个进程。
2.资源分配和管理:进程是操作系统进行资源分配和管理的基本单位。操作系统为每个进程分配内存空间、CPU时间片、文件描述符等资源,并负责管理这些资源的分配和释放。
3.并发执行:进程使得多个程序能够并发执行,即在同一时间内同时运行多个程序。操作系统通过对进程进行调度,实现了多任务的并发执行。
4.通信与同步:进程之间可以进行通信和同步操作,这使得不同的进程能够协同工作、共享数据,实现复杂的任务和功能。
5.安全性和隔离:进程之间是相互隔离的,每个进程拥有自己的地址空间和资源,这样可以确保进程之间的安全性和稳定性。
6.2 简述壳Shell-bash的作用与处理流程
壳(Shell)是计算机操作系统的用户界面,它允许用户与操作系统进行交互,执行命令并管理文件系统和其他系统资源。其中,bash(Bourne Again SHell)是一种常见的Unix和Linux系统上使用的壳程序,它的作用和处理流程如下:
作用:
1.提供用户界面:bash作为壳程序提供了用户与操作系统进行交互的界面,用户可以通过命令行或脚本来执行各种操作系统的功能。
2.解释和执行命令:bash接收用户输入的命令,并将其解释为系统调用或其他操作,然后执行这些命令以完成用户的要求。
3.管理文件系统和进程:bash可以用于管理文件和目录,以及启动、停止和管理系统中的进程。
4.支持脚本编程:bash支持编写脚本,用户可以将一系列的命令组合成一个脚本文件,以便重复执行或自动化执行特定任务。
处理流程:
1.提示符:bash通常会显示一个提示符,等待用户输入命令。
2.读取输入:用户输入命令后,bash会读取并解释这些命令。
3.判断命令是否为内置命令,是则立即执行,否则调用fork()来创建子进程,自身调用wait()来等待子进程完成,同时在程序执行期间始终接受键盘输入信号,并对输入信号做相应处理。
4.当子进程运行时,调用execve()函数,同时根据命令的名字指定的文件到目录中查找可行性文件,调入内存并执行这个命令。
5.当子进程完成处理后,向父进程shell报告,此时终端进程被唤醒,做完必要的判别工作后,再发提示符,让用户输入新命令。
6.3 Hello的fork进程创建过程
在命令行输入./hello命令运行hello程序,此命令不是内置命令,因此Bash通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同,子进程得到父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,也就意味着父进程调用fork函数时,子进程能够读写父进程打开的任何文件。父进程和新创建的子进程之间最大的区别在于其不同的PID。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。execve函数一次调用而从不返回。
6.5 Hello的进程执行
1.上下文切换:
当操作系统需要切换当前运行的进程到另一个进程时,就会进行上下文切换。这个过程包括保存当前进程的执行状态(如寄存器的值、程序计数器等)并加载下一个进程的执行状态,以确保进程能够继续执行。
上下文切换是由操作系统内核负责管理的,它会涉及到切换内存映射、切换进程控制块等操作,因此是一个开销较大的操作。
2.时间分片:
时间分片是操作系统调度策略的一种,它将CPU的执行时间划分成小的时间片段,每个进程在一个时间片段内执行一定的时间。当一个时间片段结束时,操作系统会进行上下文切换,将CPU分配给另一个进程继续执行。这样可以实现多个进程之间的轮流执行,从而实现并发执行的效果。
3.用户态与核心态:
CPU在执行指令时有不同的权限级别,通常分为用户态(用户模式)和核心态(内核模式)两种。
用户态下,进程只能访问受限的资源,不能直接操作硬件,这样可以确保进程不能对系统造成破坏。而在核心态下,操作系统内核拥有对系统资源的完全控制权,可以执行特权指令、访问系统资源等
图6.5 进程上下文切换机制的示意图
6.6 hello的异常与信号处理
1.乱按
在程序运行时乱打字,不影响程序的正常运行。随著printf的调用保留在了输出结果中。
2. ctrl-Z
进程收到SIGSTP信号,hello停止,此时进程并未回收,而是后台运行,通过ps指令可以对其进行查看,还可以通过fg指令将其调回前台。
- ctrl-C
进程收到SIGINT信号,hello终止。在ps中查询不到此进程及其PID。
- kill
挂起的进程被杀死,在ps中无法查到到其PID。
- pstree
用树状图显示所有进程结构。
6.7本章小结
本章简要介绍了hello进程大致的执行过程,阐述了进程、shell、fork、execve等相关概念,之后从上下文切换、时间分片、用户态与核心态等角度详细分析了进程的执行过程。并在运行时尝试了不同形式的命令和异常,每种信号都有不同处理机制,针对不同的shell命令,hello会产生不同响应。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1.逻辑地址:
逻辑地址是指程序中使用的地址,它是相对于程序自身的地址空间来说的,是程序员编写程序时使用的地址。在程序中使用的逻辑地址在编译和链接过程中会被转换成相应的虚拟地址。
2.线性地址:
线性地址是指经过分段机制转换后的地址,它是虚拟地址空间中的地址,是CPU在执行指令时使用的地址。线性地址是通过分段机制将逻辑地址转换成的,它包含了段选择子和偏移量。
3.虚拟地址:
虚拟地址是指程序中使用的地址,经过地址转换机制转换后在虚拟内存空间中的地址。虚拟地址是在程序运行时使用的地址,它提供了一个抽象的、独立的地址空间,使得每个进程都认为自己拥有整个内存空间。
4.物理地址:
物理地址是指内存中实际的地址,它是RAM芯片上的地址。当程序需要访问内存中的数据时,虚拟地址会经过地址映射机制转换成对应的物理地址,然后才能在内存中找到相应的数据。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在Intel x86 架构中,逻辑地址到线性地址的变换是通过段式管理实现的。段式管理是一种内存管理方式,它将内存划分为多个段,每个段都有自己的基地址和限长。在Intel x86 架构中,段式管理的实现涉及到以下几个步骤:
1.逻辑地址:在程序执行时,CPU生成的地址是逻辑地址。逻辑地址是程序中使用的地址,它是相对于程序自身的地址空间来说的。
2.段寄存器:在x86 架构中,有一些特殊的寄存器,如CS(代码段寄存器)、DS(数据段寄存器)、SS(堆栈段寄存器)等,它们存储了段选择子,用于指示当前程序使用的段。
3.段选择子:段选择子是一个16位的值,它包含了段的索引和段的特权级别等信息。
4.段描述符:每个段都有一个对应的段描述符,段描述符包含了段的基地址、限长、访问权限等信息。
5.段式变换:当CPU生成逻辑地址时,它会使用段选择子来访问全局描述符表或局部描述符表中的段描述符。CPU使用段选择子中的索引值在描述符表中找到对应的段描述符,然后使用段描述符中的基地址和偏移量来计算出线性地址
6.线性地址是通过段式变换后得到的地址,它是CPU在执行指令时使用的地址。线性地址是经过分段机制转换后的地址,它包含了段的基地址和偏移量。
7.3 Hello的线性地址到物理地址的变换-页式管理
将程序的逻辑地址空间和物理内存划分为等长的页面,这种分配方式便于维护,且不容易产生碎块。在页式存储管理方式中地址结构由两部构成,前一部分是虚拟页号(VPN),后一部分为虚拟页偏移量(VPO)。如图所示,页号用于在页表中查找对应的页表项,页表项中包含了该虚拟页所映射的物理页号以及访问权限等信息,通过将物理页号和页内偏移量组合得到最终的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
1.页全局目录:首先,处理器检查页全局目录(PGD)中的条目。如果所需的虚拟地址在TLB中不存在,处理器将查询PGD以找到相应的页目录(PD)的基地址。
页上级目录:处理器使用虚拟地址的高级别位索引到页上级目录(PUD)。如果所需的虚拟地址在TLB中不存在,处理器将查询PUD以找到相应的页中间目录(PMD)的基地址。
2.页中间目录:处理器使用虚拟地址的中级别位索引到页中间目录(PMD)。如果所需的虚拟地址在TLB中不存在,处理器将查询PMD以找到相应的页表(PT)的基地址。
3.页表:处理器使用虚拟地址的低级别位索引到页表(PT),找到相应的页框(page frame)的物理地址。
4.TLB检查:在每个步骤中,处理器都会检查TLB以查看所需的虚拟地址是否已经在TLB中缓存。如果存在,则直接使用TLB中的物理地址进行访问。
5.物理地址:经过上述步骤后,处理器获得了目标页框的物理地址,与虚拟地址的低级别位相加,得到最终的物理地址(PA)。
通过TLB和四级页表的支持,CPU可以快速地将虚拟地址转换为物理地址,从而实现对内存的访问。
7.5 三级Cache支持下的物理内存访问
三级缓存是一种采用多级缓存的存储体系,可以提高计算机内存访问速度和效率。在三级缓存的架构中,缓存分为L1、L2和L3三级,每一级缓存都有不同的容量和访问速度。
当CPU需要访问内存时,首先在L1缓存中查找数据,先找组索引位,然后与标志位对比。如果L1缓存中未命中,则需要从存储层次结构中的下一层(即L2缓存)查找。如若仍未命中,则会继续在L3缓存中查找。如果在L3缓存中也未命中,则会从主存中获取数据。
如果在三级缓存中找到了需要的数据,则可以直接访问缓存中的数据,从而提高访问速度。如果在三级缓存中没有找到,则需要从主存中获取数据,并将数据存入三级缓存中,以便下次访问时可以更快地获取数据。
7.6 hello进程fork时的内存映射
当fork函数被调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本,并将两个进程中的每个界面都标记为只读,将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存搞好和调用fork时存在的虚拟内存相同。这两个进程中的任意一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
当一个进程使用execve()系统调用时,它会替换当前进程的内存映像为一个新的程序。execve()系统调用将加载新的程序到进程的地址空间,并释放旧程序的内存映射。以下是execve()系统调用后进程的内存映射情况:
代码段:被替换为新程序的代码段。这是新程序的执行代码。
数据段:被替换为新程序的数据段。这包括全局变量和静态变量等。
堆:堆区域也会被替换为新程序所需的堆区域。新程序可以使用malloc()、free()等函数来分配和释放内存。
栈:栈区域也会被替换为新程序所需的栈区域。这包括保存函数调用的返回地址和局部变量等。
execve()系统调用用于在当前进程中加载并执行一个新的程序,它会替换进程的内存映像,并释放旧程序的内存映射。这使得进程能够执行不同的程序,并为其提供独立的内存空间。
7.8 缺页故障与缺页中断处理
缺页故障指访问一个虚拟地址的数据时,对应的物理地址不在内存中,从而引发的异常情况。页面命中完全是由硬件完成的,而处理缺页异常是由硬件和操作系统内核协作完成的。
当发生缺页故障时,处理器会向操作系统发出缺页中断信号,缺页处理程序确定物理内存中的牺牲页(若页面被修改,则换出到磁盘),而后调入新的页面,并更新内存中的PTE。最后缺页处理程序返回到原来进程,再次执行导致缺页的指令,从而完成内存访问。
7.9动态存储分配管理
动态内存分配主要使用了动态内存分配器,动态内存分配器维护了一个进程的虚拟内存区,称为堆。分配器将堆是为一组不同大小的块的集合来维护,每个块都是一个连续的虚拟内存片,虚拟内存片要么是已分配的,要么是空闲的。已分配的块显式地保留从而为供应应用程序使用。空闲块可以用来分配,空闲块在被应用分配之前都会保持空闲状态,而已分配的块在被释放之前都会保持已分配状态。释放要么由应用程序显式执行,要么是内存分配器自身隐式执行的。
分配器有两种基本风格:显示分配器:要求应用显式地释放任何已分配的块
隐式分配器要求分配器监测一个已分配块何时不再被应用程序所使用。并在不被使用时释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用地已分配的块的过程叫做垃圾收集。
7.10本章小结
本章简要介绍存储相关的知识。介绍不同地址概念以及他们之间的转换,讲述fork和execve函数的存储映射,最后介绍缺页处理和动态内存分配。
结论
Hello的一生经历坎坷,人生阶段如下:
预处理:hello.c在预处理器(cpp)中经过头文件包含、宏展开、条件编译、行链接等操作后,生成预处理后的中间文件hello.i。
编译:hello.i文件在C编译器(ccl)中被翻译为汇编语言程序,保存在文本文件hello.s中。
汇编:在汇编器(as)中hello.s被翻译为机器语言指令并打包保存形成可重定位目标文件hello.o。
链接:链接器(ld)将程序中的用到的库与hello.o进行合并形成可执行程序文件hello。
进程载入:通过Bash键入命令./hello 2021112845 zzx 3,操作系统为程序fork新进程并通过execve加载代码和数据到为其提供的私有虚拟内存空间,程序开始执行。
进程控制:由进程调度器对进程进行时间片调度,并通过上下文切换实现hello的执行,程序计数器(PC)更新,CPU按顺序取指,执行程序控制逻辑。
内存访问:内存管理单元MMU将逻辑地址逐步转换成物理地址,通过三级Cache访问物理内存/磁盘中的数据。
信号处理:进程接收信号,调用相应的信号处理函数对信号进行终止、停止、前/后台运行等处理。
进程回收:Shell等待并回收子进程,内核删除为进程创建的所有资源。
通过本次大作业,我认识到计算机系统中程序的运行涉及到的远远比我之前认识的要复杂得多,我们日常使用计算机中每一个简单的操作背后都有丰富的理论知识和复杂得软硬件架构作为支撑。我们要学号计算机知识,就要全身心投入,追根究底,在前人伟大的构思的基础上精进知识,力求创新。
附件
hello.c | 源文件 |
hello.i | 预处理文件 |
hello.s | 汇编文件 |
hello.o | 可重定位目标文件 |
hello | 目标文件 |
hello.elf | 目标文件的ELF格式文件 |
hello_o.elf | hello.o的ELF格式文件 |
hello.asm | 目标文件的反汇编文件 |
hello_o.asm | hello.o的反汇编文件 |
参考文献
[1]RANDALE.BRYANT, DAVIDR.O’HALLARON. 深入理解计算机系统[M]. 机械工业出版社, 2011.
[2] https://ysyx.oscc.cc/slides/hello-x86.html