本文通过分析名为"hello.c"的程序在Linux系统中的完整生命周期,即如何通过预处理、编译、汇编、链等关键过程变化为可执行程序。同时,探讨了Linux操作系统在进程管理、存储管理方面的作用。回顾和梳理本学期所学的计算机系统知识,了解程序的底层实现过程。
关键词:计算机系统;程序生命周期;Linux;Hello程序
目 录
6.2 简述壳Shell-bash的作用与处理流程... - 28 -
6.3 Hello的fork进程创建过程... - 28 -
7.2 Intel逻辑地址到线性地址的变换-段式管理... - 34 -
7.3 Hello的线性地址到物理地址的变换-页式管理... - 34 -
7.4 TLB与四级页表支持下的VA到PA的变换... - 35 -
7.5 三级Cache支持下的物理内存访问... - 36 -
7.6 hello进程fork时的内存映射... - 36 -
7.7 hello进程execve时的内存映射... - 37 -
第1章 概述
1.1 Hello简介
Hello的P2P(Program to Process)过程是指从程序到进程的转变过程。首先,编写hello.c源文件,然后经过预处理、编译、汇编和链接的过程,生成可执行文件。当shell执行hello程序时,会通过fork创建子进程,并通过execve系统调用启动加载器,加载hello进程。加载器会为进程分配内存空间,并初始化代码、数据、堆和栈段。最终,加载器跳转到_start地址,调用应用程序的main函数,完成hello程序的执行。
Hello的020(From Zero to Zero)过程是指从程序加载到回收的完整执行过程。通过Shell运行hello命令时,创建子进程并加载hello程序。CPU分配时间片和控制流,TLB和分页机制提高数据访问速度。通过硬件输入输出进行操作和结果显示。执行完毕后,父进程回收hello进程,内核清除信息,恢复到初始状态。操作系统管理资源、信号处理和进程控制,实现进程充分利用系统资源。Hello的020通过从零到零占用的过程,展示进程在执行后回到无占用内存空间状态。Hello结束运行。
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;2.6GHz;16G RAM;512GHD Disk
1.2.2 软件环境
Windows11 64 位; Vmware16; Ubuntu 22.04
1.2.3 开发工具
Visual Studio 2022 64位 ,Code Blocks 64位, vi/vim/gedit+gcc
1.3 中间结果
表1 程序运行中间结果
文件名 | 作用 |
hello.i | 预处理hello.c生成的文件 |
hello.s | 对hello.i 进行编译生成的文件 |
hello.o | 对hello.s 进行汇编生成的文件 |
hello.elf.txt | 对hello.o 进行readelf生成的文件 |
asm.txt | 对hello 进行objdump反汇编生成的文件 |
hello | 可执行目标文件 |
asm1.txt | 对hello进行objdump反汇编生成的文件 |
hello.txt | 对hello 进行readelf生成的文件 |
1.4 本章小结
本章主要介绍了P2P和020的概念,表明本次作业的环境和工具,列举了程序运行过程的中间文件。
第2章 预处理
2.1 预处理的概念与作用
(1) 概念:
预处理器cpp根据以字符#开头的命令(宏定义、条件编译、文件包含),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。
(2) 作用:
宏定义:预处理程序中的#define 标识符文本,预处理工作也叫做宏展开:将宏名替换为文本(这个文本可以是字符串、可以是代码等)。
文件:预处理程序中的#include,将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。
2.2在Ubuntu下预处理的命令
命令行:cpp hello.c > hello.i (或gcc -E hello.c -o hello.i)
生成hello.i文件
图1:预处理生成hello.i
2.3 Hello的预处理结果解析
Hello的预处理结果hello.i共有3060行,远多于hello.c。
图2-1:hello.i的末尾部分
main函数在文件最后的位置(上图3047行),在此之前为被插入的各种头文件和会用到的函数的声明。
图2-2:hello.i中的头文件声明
将hello.c宏展开,将#include <stdio.h>,#include <unistd.h>,#include <stdlib.h>文件的内容加入文本。
图2-3:hello.i中的函数声明
2.4 本章小结
本章论述了预处理的概念和作用,并在linux下对hello.c进行了预处理操作,分析了预处理的生成结果hello.i的构成等。
第3章 编译
3.1 编译的概念与作用
概念:编译就是通过编译器程序,把.i文件变成.s文件,即计算机易识别的机器语言。
作用:把适合人阅读的高级程序代码,转变成机器易于阅读的机器语言。同时人也可以读懂机器语言,从而对程序进行性能上的提升。
3.2 在Ubuntu下编译的命令
命令行:gcc -S -o hello.s hello.i
生成hello.s文件
图3-1:编译生成hello.s
3.3 Hello的编译结果解析
3.3.1 数据
(1)常量
.rodata节标注标明下面数据为只读数据, .text则是程序的代码段;字符常量多为字符或字符串. .string表示的字符串为字符串常量;$32是数字常量,$表示立即数。
图3-2:hello.s中的字符常量
图3-3:hello.s中的数字常量
(2)局部变量
局部变量通常保存在寄存器或栈中, 通过机器语言指令赋值,当函数返回时,局部变量在栈中的空间会被释放.hello.c程序中有三个局部变量:i、argc以及argv,下图为 int i。
图3-4:hello.s中的局部常量
(3)函数参数
main函数将argc和argv作为局部变量存储在栈中,其中argc为main函数命令行参数的个数,argv[]为存放参数字符串指针的数组。Argc作为第一个参数,存在%edi中; argv作为第二个参数,存在%rsi中。函数参数的本质也是局部变量,其只在调用的函数中作用。
图3-5:hello.s中的函数参数
3.3.2 赋值
用赋值表达式,使用MOV指令将一个值赋值给一个地址,其操作数可以是立即数、寄存器或内存地址。下图对应i = 0。
图3-6:hello.s中的赋值操作
3.3.3 类型转换
调用atoi函数,将字符串类型转化为整型(int)
图3-7:hello.s中的类型转换
3.3.4算术操作
使用一元或二元运算符将操作数连接起来的表达式即位算术表达式。下图实现算术表达式i++,执行了一个加法操作。
图3-8:hello.s中的算术操作
3.3.5关系操作
关系判断表达式由关系运算符组成,运算结果为布尔类型。使用cmp类指令对两个操作数的值进行比较,根据比较结果进行跳转。
图3-9:hello.s中的关系操作(argc!=4)
图3-10:hello.s中的关系操作(i<4)
3.3.6数组/指针/结构操作
argv是字符串指针数组,其有三个元素,分别是argv[1]、argv[2]、argv[3]基地址偏移分别为16个字节,8个字节,24个字节。
图3-11:hello.s中的数组
3.3.7控制转移
条件转移,如果i<9则跳转到.L4
图3-12:hello.s中的条件转移
3.3.8函数操作
(1)参数传递
调用函数之前,编译器会将参数存储在寄存器中,以供调用的函数使用。
(2) 函数调用
图3-13:hello.c源代码
- 14行, 调用printf函数, 编译器采用调用puts函数
图3-14:hello.s的puts函数实现printf函数
- 15行,调用exit函数,结束进程。
图3-15:hello.s的exit函数
- 18行,调用 printf函数,实现printf(“Hello %s %s\n”,argv[1],argv[2])
图3-16:hello.s的printf函数
- 调用atoi函数, 将字符串类型转化为整型(int)
图3-17:hello.s的atoi函数
⑤ 19行,调用sleep函数, 将atoi函数的返回作为sleep的参数调用,实现让函数进入x秒休眠,如果在休眠结束前停止则会返回剩余时间。
图3-18:hello.s的sleep函数
- 21行,调用getchar函数
图3-19:hello.s的getchar函数
(3)函数返回
程序使用汇编指令ret从调用的函数中返回,并且还原栈帧,并且函数的返回值存放在寄存器%rax中。
图3-20:hello.s的函数返回
3.4 本章小结
本章介绍了编译的概念与作用, 并在linux下对hello.i进行了编译操作,分析了编译的生成结果hello.s的数据, 赋值,算术操作,关系操作,数组/指针/结构操作,控制转移,函数操作方面的内容。
第4章 汇编
4.1 汇编的概念与作用
概念: 汇编语言编写的汇编文件(.s),然后通过相应的汇编程序将它们转换成机器语言二进制文件(.o)的过程。
作用: 指将汇编语言文件翻译为二进制机器语言文件,并将其打包为可重定位目标文件的格式。
4.2 在Ubuntu下汇编的命令
命令行:as hello.s -o hello.o
生成hello.o文件
图4-1:汇编生成hello.o
4.3 可重定位目标elf格式
命令行:readelf -a hello.o>hello.elf.txt
生成hello.o可重定位目标的文件,存入hello.elf.txt中。
图4-2:把hello.o可重定位目标的文件存入txt文件
(1)ELF头的开头是16字节的序列,描述了系统的字大小和字节顺序,接下来的字段,提供了文件的详细信息,包括ELF头大小、目标文件类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
图4-3:hello.elf.txt的elf头
(2)节头是文件中出现的各个节的信息,包括节的类型、地址和偏移量等信息具体如下图
图4-4:hello.elf.txt的节头
(3)重定位节由两部分组成
①.rela.text节:是一个.text节中位置的列表, 包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,要修改这些位置。下图中,由上到下需要被重定位的依次是.rodata中的模式串 .puts,exit,printf,atoi,sleep,getchar函数。
②.rela.eh_frame节:包含.eh_frame节中需要进行重定位的信息。
图4-5:hello.elf.txt的重定位节
(4)符号表中列出了程序中所有定义和引用的全局变量,程序中定义的局部变量和类型,以及函数等信息:
图4-6:hello.elf.txt的符号表
4.4 Hello.o的结果解析
命令行:objdump -d -r hello.o >asm.txt
生成hello.o的反汇编文件,存入asm.txt中。
图4-7:生成hello.o的反汇编文件
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
对比反汇编文件asm.txt内容与汇编程序hello.s内容,存在一些不同之处。
- 操作数:
反汇编代码中的操作数是16进制, 且反汇编代码省略了指令结尾的q ,如下图中的sub $0x20;而在hello.s中操作数是十进制,表示为subq $32,。
图4-8:反汇编文件和hello.s的操作数区别
- 分支转移:
反汇编代码中跳转指令后就是要跳转的精确位置,如下图的je 2f<main+0x2f>;而hello.s中分支转移目标的目标位置用段名.L表示,如下图je .L2 。
图4-9:反汇编文件和hello.s的条件跳转区别
图4-10:反汇编文件和hello.s的无条件跳转区别
- 函数调用:
反汇编代码中,函数的调用使用的是call指令加上待引用函数的首地址的形式。并且在.rel.text节中为其添加了重定位条目,用于以后确定物理地址。如下图中的callq 25 <main+0x25> // 21: R_X86_64_PLT32 puts-0x4而在hello.s中函数调用方式为call指令后直接引用函数名称,如call puts。
图4-11:反汇编文件和hello.s的函数调用区别
4.5 本章小结
本章介绍了汇编的概念与作用,将hello.s被转化为hello.o,并生成hello.o可重定位目标的文件,存入hello.elf.txt中。并分析了elf的结构。然后对所得的hello.o文件进行反汇编得到asm.txt,并对asm.txt和hello.s进行对比,分析了操作数, 分支转移, 函数调用方面的不同。
第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文件
图5-1:链接生成hello文件
5.3 可执行目标文件hello的格式
图5-2:生成hello的elf文件存于hello.elf.txt
与hello.o可重定位目标的文件elf相比,elf头中的文件类型变成了EXEC(可执行目标文件)。
图5-3:hello. txt的ELF头
hello. txt节头部表也和hello.o的很相似,节头部表中条目的大小增加为27条。
图5-4:hello. txt的节头
图5-5:hello. txt的程序头
图5-6:hello. txt的重定位节
与hello.o文件相比,其中的函数名被替换为更为详细的内容,如puts找到了该函数的定义,被替换为puts@GLIBC_2.2.5,多了一个.dynsym节
图5-7:hello. txt的符号表.dynsym
其余的符号表如下:
图5-8:hello. txt的符号表
5.4 hello的虚拟地址空间
使用edb加载hello,如下图所示:
图5-9在edb中加载hello
使用edb加载hello,查看本进程的虚拟地址空间各段信息,edb中显示的虚拟地址和elf文件中看到的虚拟地址是对应相同的。
图5-10虚拟地址对应
.init节: 该段代表程序的开始,存放着指令的机器码.可知该段地址从0x401000开始,偏移量为0x1000.
5.5 链接的重定位过程分析
命令行:objdump -d -r hello > asm1.txt
生成hello的反汇编文件,存入asm1.txt中。
图5-12:生成hello的反汇编文件
对比反汇编文件asm1.txt内容与反汇编文件asm.txt内容,存在一些不同之处。
- 代码起始位置:
hello的反汇编代码从0x401000处开始,而hello.o的反汇编代码从0开始.
图5-13:asm1.txt与asm.txt代码起始位置不同
- 引入函数
asm.txt (hello.o的反汇编代码)只有main函数,而asm1.txt (hello的反汇编代码)中增加了许多通过重定位过程添加进来的函数内容
图5-14:asm1.txt与asm.txt引入函数不同
- 函数调用
asm.txt (hello.o的反汇编代码)的函数调用指令只能用偏移量进行跳转,而asm1.txt (hello的反汇编代码)中有已分配好虚拟地址,用call指令直接指出跳转的虚拟地址。
图5-15:asm1.txt与asm.txt函数调用不同
- 调用.rodata数据
asm.txt(hello.o的反汇编代码)中用0x0(%rip)代替静态字符串的位置;而Hello完成了重定位,如下图asm1.txt(hello的反汇编代码)中具体标记出了静态字符串的位置。
图5-16:asm1.txt与asm.txt调用.rodata数据不同
链接的过程主要分为两个过程:符号解析和重定位。
符号解析时解析目标文件定义和引用符号,并建立每个符号引用和符号定义之间的关联。
重定位时先重定位节和符号定义,把相同类型的节合并,并为其分配内存。接下来进行符号引用的重定位,修改代码和数据中对符号的引用,使得他们指向正确地址。
5.6 hello的执行流程
- ld-2.23.so!_dl_start
- ld-23.so! dl_init
- hello!_start
- hello!__libc_start_main
- libc-2.23.so!__libc_start_main
- libc-2.23.so! cxa_atexit
- hello!__libc_csu_init
- hello!_init
- libc-2.23.so!_setjmp
- libc-2.23.so!_sigsetjmp
- hello!main
- hello!puts@plt
- hello!exit@plt
- hello!printf@plt
- hello!sleep@plt
- hello!getchar@plt
- ld-2.23.so!_dl_runtime_resolve_avx
- libc-2.23.so!exit
5.7 Hello的动态链接分析
当程序调用一个由共享库定义的函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。但这需要链接器修改调用模块的代码段,GNU编译系统使用一种称为延迟绑定的技术将过程地址的绑定推迟到第一次调用该过程时。
延迟绑定是通过两个数据结构之间的交互来实现的,分别是GOT和PLT,GOT是数据段的一部分,而PLT是代码段的一部分。PLT与GOT的协作可以在运行时解析函数的地址,实现函数的动态链接。
过程链接表PLT是一个数组,每个条目是16字节代码。PLT[0]是一个特殊条目,跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。
图5-17:调用dl_init前的情况
图5-18:调用dl_init后的情况
5.8 本章小结
本章介绍了链接的概念和作用,链接的命令,分析了可执行目标文件hello的格式,通过edb展示了hello的虚拟地址空间,分析链接重定位过程,分析hello的执行流程,动态链接过程。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程的经典定义就是一个执行中程序的实例,系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存中的程序的代码和数据、它的栈、通用目的寄存器的内容,程序计数器、环境变量,以及打开文件描述的集合。
作用:提供给应用程序两个关键抽象,①一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器,②一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
作用:
Shell是一种命令行解释器,它为应用程序的执行提供一个界面,用户通过这个界面访问操作系统内核的服务,shell读取用户输入的字符解释并执行。
处理流程:
(1)shell从终端读入用户输入的命令。
(2)将输入字符串进行划分以获得所有参数。
(3)判断是否是内置命令,是则立即执行,否则调用相应的程序为其分配子进程并运行。
(4)Shell接受键盘输入信号并对这些信号进行相应处理。
6.3 Hello的fork进程创建过程
在终端输入./hello的输入命令后,shell进行命令行解释,由于命令行第一个参数不是内置shell命令,shell会调用fork函数创建一个子进程并执行可执行程序hello。新创建的子进程几乎与父进程相同,子进程得到与父进程用户及虚拟地址空间相同的但是独立的一份副本,包括代码和数据段、堆、共享库以及用户栈,子进程还获得与父进程任何打开文件,描述符相同的副本,这就意味着,当父进程调用fork函数时,子进程可以读写父进程中打开的任何文件。
6.4 Hello的execve过程
execve函数带参数列表argv和环境变量列表envp。execve函数若成功则调用一次从不返回。execve函数会在当前进程的上下文中加载并运行我们的可执行目标文件,删除子进程现有的虚拟内存段,创建一组新的段(栈与堆初始化为0),并建立虚拟地址空间和磁盘文件的映射,在程序头部表的引导下,加载器将可执行文件的片复制到代码段和数据段,之后,加载器跳转到程序的入口:_start函数的地址。_start函数调用系统启动函数,_libc_start_main(该函数定义在libc.so里),之后初始化环境,调用用户层的main函数,处理main函数返回值,并且在需要的时候返回给内核。值得注意是的是,execve函数是覆盖当前进程的地址空间,并没有创建一个新的进程,因的程序仍有相同的PID,并且继承了调用execve函数时已打开的所有文件描述符。
6.5 Hello的进程执行
(1) 上下文信息:上下文信息是操作系统内核重新启动一个挂起的进程所需要恢复的状态。它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的信息构成。
(2)时间片;一个进程执行它的控制流的一部分的每一时间段叫做时间片。
(3)进程调度:在进程执行过程中,操作系统内核可以决定抢占当前进程,并重新开始一个先前被挂起的进程,这样的一种决策叫做进程调度。当抢占进程时,要完成以下三个任务:①保存之前进程的上下文;②恢复要执行的新进程的上下文;③把控制转让给新恢复的进程完成上下文切换。
(4)用户模式和内核模式:处理器通常使用一个寄存器来区分两种模式,这个寄存器描述了当前进程的权限情况。简单来说,两种模式有不同的“权限”,用户模式权限较低,不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;内核模式权限较高,可以执行任何命令,并且可以访问系统中的任何内存位置。
(5)进程执行与用户态核心态转换:当开始运行hello时,内存为hello分配时间片,若一个系统同时运行多个进程,则它们轮流使用处理器,物理控制流被划分成多个交错的逻辑控制流,存在并发执行的现象。然后在用户态下执行并保存上下文。如果在此期间内发生了异常或系统中断,则内核会休眠该进程,并在核心态中进行上下文切换,把控制权让给其他进程。当hello进程执行到sleep时,hello会进入休眠状态,此时再次进行上下文切换,控制交付给其他进程,一段时间后hello休眠结束,此时再次完成上下文切换,恢复休眠前的上下文信息,此时控制权送回hello并继续执行。循环结束后,程序调用 getchar() ,hello从用户模式进入内核模式,并再次上下文切换,控制交付给其他进程。最后,内核会从其他进程回到 hello 进程。
图6-1:进程上下文切换的流程
6.6 hello的异常与信号处理
6.6.1 hello的异常
图6-2:hello执行过程中会出现的几类异常
6.6.2 hello的信号处理
hello执行过程中可能出现的信号有:SIGINT、SIGKILL、SIGSEGV、SIALARM、SIGCHLD等。
(1)无操作:
图6-3 无操作
(2)不停乱按,包括回车:
如果只是单纯的不停乱按,那么仅仅只会把输入的字符缓存起来,但是如果输入回车,那么就会把回车之前的字符当作输入的命令
图6-4 不停乱按,包括回车
(3)Ctrl-C
输入Ctrl+C会发送一个SIGINT信号给到前台进程组的每个进程,结果是终止前台进程.结果hello程序被终止了.
图6-5 Ctrl-C
(4)Ctrl-Z
输入Ctrl-Z会发送一个SIGSTP信号给前台进程组的每一个进程,结果是停止前台进程。结果hello程序被停止了.
图6-6 Ctrl-Z
(5)Ctrl-z后运行ps
可以看到被停止的进程hello的名称和pid
图6-7 Ctrl-Z后运行ps
(6)Ctrl-z后运行jobs
暂停的程序被正确的显示,而且被标记为已停止.
图6-8 Ctrl-Z后运行jobs
(7)Ctrl-z后运行pstree
可以清楚看到各个进程之间的关系,即那个进程是父,哪个进程是子.
图6-9 Ctrl-Z后运行pstree
(8)Ctrl-z后运行fg
fg的功能是使第一个后台作业变为前台,而第一个后台作业是hello,所以输入fg 后hello程序又在前台开始运行,并且是继续刚才的进程,输出剩下的5个字符串
图6-10 Ctrl-Z后运行fg
(9)Ctrl-z后运行kill
先用ps查看hello的pid,然后输入kill -9 29289,再次用ps查看,发现hello已经被终止.hello进程被杀死了。
图6-11 Ctrl-Z后运行kill
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:是指由程序hello产生的与段相关的偏移地址部分,hello.o文件中的地址就是逻辑地址。
线性地址:是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑地址——也就是段中的偏移地址,它加上相应段的基地址就会生成线性地址。
虚拟地址:是由CPU生成的一个仰赖访问主存的地址,它会被送到内存之前先转换成适当的物理地址,地址翻译的任务由CPU芯片上的内存管理单元MMU来承担,MMU会用存放在主存中的查询表来动态翻译虚拟地址,该表的内容由操作系统管理。在linux系统中,只会分页而不会分段,因此对于我们的hello程序逻辑地址几乎就是虚拟地址。
物理地址:是主存上被组织成一个由M个连续的字节大小的单元组成的数组,每个字节都有一个唯一的物理地址,hello的物理地址来自于虚拟地址的地址转换,它也是程序运行的最终地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
转换的具体过程:首先,给定一个完整的逻辑地址,它的格式如下:[段选择符:段内偏移地址]然后,看段选择符的T1=0还是1,知道当前要转换是全局段描述符表(存储全局的段描述符),还是局部段描述符表(存储进程自己的段描述符),再根据相应寄存器,得到其地址和大小,得到一个数组。最后,拿出段选择符中前13位,查找到对应的段描述符,进而找到基地址,base+offset得到线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址到物理地址的变换由页式管理实现,它通过分页机制对虚拟内存空间进行分页,然后把页式虚拟地址与物理内存地址建立一一对应页表,并用相应的硬件地址变换机构(MMU)来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。页表是一个由页表项(PTE)组成的数组,存储在内存中,将虚拟页地址映射到物理页地址。
如下图所示,一个虚拟地址(VA)包含两个部分:虚拟页号(VPN)和虚拟页偏移量(VPO),其中VPO和PPO(物理页偏移量)是相同的。MMU利用VPN选择适当的PTE,如果PTE的有效位为1,也即PTE命中,则直接将PTE中存储的物理页号(PPN)和虚拟地址中的虚拟页偏移量(VPO)串联起来就得到一个相应的物理地址。如果页表项(PTE)不命中,则会触发缺页故障,调用缺页处理子程序进行相应处理。
图7-1:使用页表的地址翻译
7.4 TLB与四级页表支持下的VA到PA的变换
首先在TLB中查找PTE,若能直接找到则直接得到对应的PPN,具体的操作是将VPN看作TLBI和TLBT,前者是组号,后者是标记,根据TLBI去对应的组找,如果TLBT能够对应的话,则能够直接得到PTE,进而得到PPN。
其中若是在TLB中找不到对应的条目,则应去多级页表中查找,VPN被分为了四块。有一个叫做CR3的寄存器包含L1页表的物理地址,VPN1提供到了一个L1
PET的偏移量,这个PTE包含L2页表的基地址,VPN2提供到一个L2
PTE的偏移量。依次类推,最终找到页表中的PTE,得到PPN。
而VPO和PPO相等,最终的PA等于PPN+PPO。
图7-2:TLB与四级页表支持下的地址翻译
7.5 三级Cache支持下的物理内存访问
对于一个虚拟地址请求,首先将去TLB寻找,看是否已经在TLB中缓存。如果命中的话就直接MMU获取,没有命中的话就先在结合多级页表,得到物理地址,去cache中找,到了L1里面以后,寻找物理地址又要检测是否命中,不命中则紧接着寻找下一级cache L2,接着L3。这里就是使用到CPU的高速缓存机制了,逐级向下找,直到找到对应的内容。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,操作系统内核为新进程创建各种数据结构,并分配给它一个唯一的 PID,然后通过以下步骤为其创建虚拟内存:
①创建当前进程的的mm_struct、区域结构和页表的原样副本;
②将两个进程中的每个页面都标记为只读;
③将两个进程中的每个区域结构都标记为私有的写时复制;
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个在后面进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve 函数的函数声明为int execve(char *filename ,char *argv[], char *envp[]);加载hello并执行需要以下几个步骤:
(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
(2)映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。
(3)映射共享区域。如果filename程序与共享对象(或目标)链接,那么这些对象是动态链接到这个程序的,然后映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
当再次调度这个进程时,它将从入口点开始执行。Linux将根据需要换入代码和数据页面。
图7-3:用户栈典型组织结构
7.8 缺页故障与缺页中断处理
当发生缺页中断时,系统的处理流程如下图所示:
①处理器将虚拟地址发送给MMU;
②MMU生成PTE地址(PTEA),并从高速缓存/主存请求得到它;
③高速缓存/主存向MMU返回PTE;
④PTE有效位为0, MMU触发缺页异常,传递CPU中的控制到操作系统内核的缺页异常处理程序;
⑤缺页处理程序确定出物理内存中的牺牲页 ,若页面被修改,则把它写回到磁盘中。
⑥缺页处理程序调入新的页面,并更新内存中的PTE;
⑦缺页处理程序返回到原进程,再次执行导致缺页的指令,CPU将VA重新送给MMU并执行相应访问操作,此时将不会再出现缺页的情况。
图7-4 缺页操作
7.9本章小结
本章介绍了储存器的地址空间,讲述了虚拟地址、物理地址、线性地址、逻辑地址的概念,还有进程fork和execve时的内存映射的内容。描述了系统如何应对缺页异常。
结论
Hello的一生可以总结为以下几个阶段:
- 源程序编写:Hello诞在文本编辑器或IDE中编写C语言代码,生成了最初的hello.c源程序。Hello在这里被赋予了它最初的存在
- 预处理:Hello进入预处理阶段,预处理器解析宏定义、文件包含和条件编译等指令,生成一个ASCII码的中间文件hello.i。
- 编译:Hello通过编译器进行编译,将C语言代码转换为汇编指令,生成一个ASCII汇编语言文件hello.s。
- 汇编:汇编器将汇编指令翻译成机器语言,并生成重定位信息,生成可重定位目标文件hello.o。
- 链接:链接器进行符号解析、重定位和动态链接等操作,将可重定位目标文件hello.o与其他目标文件链接在一起,生成一个可执行目标文件hello。现在,Hello终于可以真正地被执行了。
- 运行阶段:通过shell命令运行hello程序时,shell调用fork函数创建子进程,为hello程序提供执行环境。子进程中通过execve函数加载hello程序,进入hello的程序入口点,hello开始运行。
- 内存管理和协作:在运行阶段,操作系统的内核负责调度进程,并对可能产生的异常和信号进行处理。硬件组件如MMU、TLB、多级页表、cache和DRAM内存等与操作系统协作,共同完成内存的管理。
- I/O交互:Hello程序可以利用操作系统提供的Unix I/O功能与文件进行交互。
- 终止:Hello进程运行结束后,由shell负责回收终止的hello进程,操作系统内核删除为hello进程创建的所有数据结构。Hello短暂又灿烂一生在这里结束。
通过分析Hello的一生,我们对计算机系统的设计与实现有了深刻的认识:Hello从诞生到结束,经历了诸多阶段,在硬件、操作系统和软件的相互协作配合下,终于完美地完成了它的使命。这让我们深刻认识到,一个复杂的系统需要多方面的协作配合才能更好地实现功能。
附件
文件名 | 作用 |
hello.i | 预处理hello.c生成的文件 |
hello.s | 对hello.i 进行编译生成的文件 |
hello.o | 对hello.s 进行汇编生成的文件 |
hello.elf.txt | 对hello.o 进行readelf生成的文件 |
asm.txt | 对hello 进行objdump反汇编生成的文件 |
hello | 可执行目标文件 |
asm1.txt | 对hello进行objdump反汇编生成的文件 |
hello.txt | 对hello 进行readelf生成的文件 |
参考文献
[1] Randal E.Bryant, David O'Hallaron. 深入理解计算机系统[M]. 机械工业出版社.2018.4
[2] Pianistx.printf 函数实现的深入剖析[EB/OL].2013[2021-6-9].
https://www.cnblogs.com/pianist/p/3315801.html.
[3] 梦想之家xiao_chen.ELF文件头更详细结构[EB/OL].2017[2021-6-10].
https://blog.csdn.net/qq_32014215/article/details/76618649.