摘 要
本文是2023年春季学期哈尔滨工业大学计算机系统课程的课程陈诉,介绍了给定的hello.c程序的编译、执行过程的详细内容,对课程内容进行了融合。
关键词:编译;进程;计算机;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 本章小结............................................................................... - 7 -
第3章 编译................................................................................... - 8 -
3.1 编译的概念与作用............................................................... - 8 -
3.2 在Ubuntu下编译的命令.................................................... - 8 -
3.3 Hello的编译效果解析........................................................ - 8 -
3.4 本章小结............................................................................. - 13 -
第4章 汇编................................................................................. - 14 -
4.1 汇编的概念与作用............................................................. - 14 -
4.2 在Ubuntu下汇编的命令.................................................. - 14 -
4.3 可重定位目标elf格式...................................................... - 14 -
4.4 Hello.o的效果解析........................................................... - 16 -
4.5 本章小结............................................................................. - 18 -
第5章 链接................................................................................. - 19 -
5.1 链接的概念与作用............................................................. - 19 -
5.2 在Ubuntu下链接的命令.................................................. - 19 -
5.3 可执行目标文件hello的格式......................................... - 19 -
5.4 hello的假造地址空间....................................................... - 24 -
5.5 链接的重定位过程分析..................................................... - 25 -
5.6 hello的执行流程............................................................... - 28 -
5.7 Hello的动态链接分析...................................................... - 29 -
5.8 本章小结............................................................................. - 30 -
第6章 hello进程管理.......................................................... - 32 -
6.1 进程的概念与作用............................................................. - 32 -
6.2 简述壳Shell-bash的作用与处理流程........................... - 32 -
6.3 Hello的fork进程创建过程............................................ - 32 -
6.4 Hello的execve过程........................................................ - 32 -
6.5 Hello的进程执行.............................................................. - 33 -
6.6 hello的非常与信号处理................................................... - 33 -
6.7本章小结.............................................................................. - 36 -
第7章 hello的存储管理...................................................... - 37 -
7.1 hello的存储器地址空间................................................... - 37 -
7.2 Intel逻辑地址到线性地址的变换-段式管理.................. - 37 -
7.3 Hello的线性地址到物理地址的变换-页式管理............. - 37 -
7.4 TLB与四级页表支持下的VA到PA的变换................... - 38 -
7.5 三级Cache支持下的物理内存访问................................ - 38 -
7.6 hello进程fork时的内存映射......................................... - 39 -
7.7 hello进程execve时的内存映射..................................... - 39 -
7.8 缺页故障与缺页中断处理................................................. - 39 -
7.9本章小结.............................................................................. - 40 -
结论............................................................................................... - 41 -
附件............................................................................................... - 42 -
参考文献....................................................................................... - 43 -
第1章 概述
1.1 Hello简介
hello的P2P过程:
程序员编写hello.c,随后hello.c颠末预处理、编译、汇编、链接生成可执行链接文件hello;用户在shell中键入命令启动hello,shell解析命令并随后fork出子进程,调用execve()启动hello。
hello的020过程:
execve()删除shell子进程的用户区域,为hello映射私有内存区域,随后进行动态链接,映射共享内存区域,将控制权交给hello;hello在执行时与OS共同,由OS完成系统调用、进程调度等工作,处理故障、收发信号;hello最终退出,OS吸收到进程终止的消息,向父进程shell发送SIGCHLD信号,shell随后回收hello进程,使其不再占用资源。
1.2 情况与工具
硬件情况:
x86-64 CPU(AMD Ryzen 7 7700X @ 4.50GHz); 32GiB RAM(DDR5-5200); 300GiB SSD(Dedicated for Ubuntu)
软件情况:
Windows Subsystem for Linux(Ubuntu 22.04.2 LTS); GLIBC 2.35
开辟情况:
VS Code 1.78.2; GCC 11.3.0; EDB 1.3.0
1.3 中心效果
hello.i:颠末预处理的C语言hello程序
hello.s:颠末编译的汇编语言hello程序
hello.o:颠末汇编的hello可重定位目标文件
hello:颠末链接的hello可执行链接文件
elfdump_rel:readelf读出的hello.o的信息
dump_rel:objdump读出的hello.o的反汇编效果
elfdump_exec:readelf读出的hello的信息
dump_exec:objdump读出的hello的反汇编效果
1.4 本章小结
这里对利用的软硬件情况以及利用的文件进行了概括,也介绍了hello的P2P, 020过程。
第2章 预处理
2.1 预处理的概念与作用
预处理是C程序编译过程中最初的步调,预处理器将以#开头的预处理命令进行处理,并对原始C文件进行更换,生成颠末预处理的C程序。
其主要作用为:
将#include指令包罗的文件到场C文件中;
将代码中出现的由预处理指令定义的宏进行更换;
根据条件编译指令#if #ifdef等删除不合条件的代码,确定需要编译的代码。
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i
2.3 Hello的预处理效果解析
程序由原来的24行扩展为了3092行,这是由于程序第6-8行的三条#include指令在预处理时被递归地展开为对应头文件的内容,代码中的所有宏被更换为了其定义值;预处理器同时还标记了所涉及到的头文件的信息。
在代码的最后为main()函数,由于其代码中没有宏定义,预处理器将其保持原样。
2.4 本章小结
在预处理时,预处理器处理代码中的预处理命令,并对代码中的宏定义进行更换。对于#include命令,预处理器表明对应包罗文件的路径,并用文件的内容更换#include命令;对于条件编译命令,编译器判断条件是否满足,若满足则保留代码,若不满足则将对应部门的代码删除。
第3章 编译
3.1 编译的概念与作用
编译是将预处理后的C程序转换为汇编程序的过程。它产生了程序的机器级表示,抹除了不同高级语言间的差异。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s -m64 -no-pie -fno-PIC
3.3 Hello的编译效果解析
3.3.1 编译举动简述
编译器在这一阶段将预处理过的程序代码翻译为汇编指令,并初步安排数据存放的位置,产生符号/标签,形成机器级表示程序的雏形。指令中的typedef和各数据范例在这里已经消失,其操作被细化为机器级的数据操作。
3.3.2 函数调用
在进行函数调用时,程序起首保存Caller-saved registers,然后构造被调用函数所利用的参数,接着发起函数调用;在控制由被调用函数传递返来之后,函数再进行后续的操作。
按照System V x86-64 ABI的调用约定,当被调用函数参数个数不多于6时,参数通过寄存器传递,按%rdi、%rsi、%rdx、%rcx、%r8、%r9的次序装入;而当参数个数大于6时,剩下的参数按从右至左的次序入栈。在寄存器的保存上,除%rax用于传递整数参数的返回值、上述六个寄存器用于传递参数而已由调用者(Caller)保存、%rsp是栈指针寄存器“自己保存自己”外,还有%r10、%r11是由调用者保存,剩下的寄存器均由被调用者(Callee)保存,负责在退出前将其状态规复至调用时的状态。浮点数参数自成一套体系,我们在这里不进行讨论。
hello.s中共出现了6处函数调用,已用同样的颜色标出了C代码和汇编代码的对应部门,其中汇编代码部门包罗参数预备和调用指令,橙色框出的是依据调用约定保存的Callee-saved register,深绿色框出的是Caller-saved register,暗赤色框出的是返回值的设置操作。
这里有一些值得关注的点:
- main()被调用后参数存放在%rdi和%rsi中,由于调用其他函数后还需用到其中的数据,两个寄存器被存入了当前函数的栈帧;
- 由于利用了%rbp,其原值被存入了栈帧,并通过leave指令在返回前规复;
- GCC将输出常量字符串的printf()优化为了puts(),即文件中的printf("用法: Hello 学号 姓名 秒数!\n");其参数为标签LC0,对应字符串“用法: Hello 学号 姓名 秒数!\n”;
- printf("Hello %s %s\n",argv[1],argv[2])的第一个参数为标签LC1,对应字符串“Hello %s %s\n”;
- 对sleep()和atoi()的调用阐明白函数参数为另一个函数返回值的调用流程,即先调用内层函数后调用外层函数;
- 函数返回值0由立即数产生,存放在%rax中;
- 当进行函数调用时,call指令自动将下一条指令的PC值,即返回地址压入栈中;当函数返回时,ret指令从%rsp处取出返回地址,并将PC设置为返回地址。
3.3.3 数据访问及存储
hello.c中的数据以三种形式存放:1.作为局部变量存放在函数栈帧中;2.作为常量存放在.rodata中;3.以立即数形式作为指令操作数存放在.text中。
在main()函数执行起始,函数为自己分配32字节栈帧,存放函数中的局部变量及被保存的寄存器数据。函数中共有3个局部变量:argc、argv以及i。
main()的栈帧布局如下:
还有一部门常量以立即数形式作为指令操作数.text中,比如循环终止条件i<8中的8(这里编译器利用了jle而非jl,故立即数为7):
另一部门常量存放在.rodata中,拥有自己的标签:
在汇编程序中,有无符号的操作靠指令范例区分或不区分(对于两种操作等价的指令,如整数加法),数据范例则依靠指令操作数长度进行区分,如对int型数据的操作利用movl,而对char *型数据的操作利用movq。对于布局体等数据范例的操作也细化到成员级别,不再将其作为整体看待。
对于数组等依靠指针进行访问的数据布局,程序通过基址-变址寻址访问其中的元素:
这里的操作将argv[2]存放到%rdx中:-32(%rbp)处存放着argv,程序先将其取到%rax中,随后将其+16,由于指针范例巨细为8字节,这样%rax中存放的就是argv[2]的地址;接着利用地址将argv[2]取到%rdx中即可。固然,后两条指令也可以归并成一条指令movq 16(%rax), %rdx。程序中其余对数组的操作大同小异,不再附加阐明。
3.3.4 数据运算
hello.c中的数据运算主要涉及赋值、比力、算数运算。
赋值操作利用mov指令实现,其操作数长度与数据范例有关,例如这条指令出现在for循环的初始条件处,将立即数0赋给int型变量i,由于int型巨细为4字节,指令为movl:
比力操作可利用专门的比力指令进行。hello.c中出现了两次比力,利用cmp指令完成;该指令设置状态位供后续指令利用,在hello.c中即为je和jle:
这两条指令起首判断argc是否为4,若判断创建则状态寄存器的ZF位置1,je指令随即跳转到.L4;若不创建则ZF位置0,je指令不跳转。
算术运算利用专门的算数指令完成。在hello.c中利用了加法对i做自增操作,利用add指令实现:
至于表达式的求值,编译器起首在内部将表达式分解成一系列简朴表达式,然后将简朴表达式翻译成汇编指令,利用寄存器存放中心效果,最终实现表达式的求值。比如要将%rdx + %rdi * %rcx的值赋给%rax,编译器起首在内部进行词法分析,将表达式变换为%rax = ((%rdi * %rcx) + %rdx);再进行一些优化减少需要保存的寄存器个数,利用%rax而不是其余寄存器存放中心效果,最终得到汇编指令movq %rdi, %rax; imulq %rcx, %rax; addq %rdx, %rax。固然,若有被调用者保存的寄存器的值被更改,在求值前后还要保存和规复其原值。编译器会选择合适的寄存器存放中心变量,使得保存和规复的开销最小。
3.3.5 控制转移
在hello.c中涉及到的控制转移布局有if语句和for循环。
if语句通过条件判断和条件跳转指令实现,在hello.c中如下:
cmpl进行条件判断并设置状态位,je根据状态位决定是否进行跳转。若cmpl条件创建,即argc != 4,状态寄存器的ZF位置1,je将直接跳转到.L2,否则不进行跳转。
for循环起首为变量赋初值,然后在迭代前进行条件判断。
这里movl起首为i赋初值0,然后无条件跳转到.L3进行条件判断,条件创建则跳转到.L4,否则继承向下执行退出循环。
3.3.6 范例转换
在hello.c中有一处隐式范例转换:
atoi()函数的返回值是int范例,而sleep()函数的参数是unsigned int范例。C标准并未规定此时范例转换的举动,本机运行情况的实现保持位表示稳定,因此没有特殊的指令进行转换,只是转换前后对数据的解读方式发生了变化。
3.4 本章小结
编译将预处理后的C代码转换为汇编程序。typedef等语句在此时消失不见,留下的只有函数代码和程序运利用用的数据。程序的机器级表示已表现出雏形。
第4章 汇编
4.1 汇编的概念与作用
汇编将汇编指令转化为二进制的机器指令,将汇编程序转化为目标文件。这之后生成的文件不再为人类直接可读,而需要借助工具才可阅读。重定位条目和符号表在这时生成。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o -m64 -no-pie -fno-PIC
4.3 可重定位目标elf格式
这是一个x86-64可重定位目标文件。它没有程序头表,节头表偏移量为1048字节,共14节。
各节根本信息如图所示,可以发现.bss节巨细为0,而由于没有全局变量和静态局部变量,.data节巨细也为0:
重定位条目如下:
该文件共有8个重定位条目,分别对应:mov对字符串.L0的引用、call对puts()的引用、call对exit()的引用、mov对.LC1的引用、call对printf()的引用、call对atoi的引用、call对sleep()的引用、call对getchar()的引用。
符号表如下:
该文件共有11个符号,其中4个局部符号,7个全局符号,包罗了各节的符号、定义的函数的符号、引用的外部函数的符号。其中全局符号中main是强符号,引用的外部函数均为弱符号。
4.4 Hello.o的效果解析
相较于hello.s,反汇编中不再含有标签、对齐要求等标记,它们被用于形成符号表、重定位条目、ELF文件的各节等内容;指令操作数中的标签已被操作数更换,文件内的相对引用操作数值已经确定,而对外部符号和本地及外部数据的引用地址则以0添补,并附有相应的重定位条目,期待链接时进行重定位。
机器指令由操作码和地址码两部门构成。在x86-64上,操作码位于指令开头,长度可变;其后可跟零到多个地址码,即操作数,用于指明指令操作的源和目标。一条机器指令对应一条汇编指令,然而操作数并不完全相同,有些机器指令操作数为0,但对应汇编指令操作数并不为0。这是因为这些指令引用的目标地址尚未确定,需要等到链接时确定地址后才可填写操作数。每一个这样的操作数都有一个重定位条目,其纪录了必要的重定位信息,在链接时对符号进行重定位后即依据重定位条目对这些操作数进行重新生成,产生正确的操作数。
4.5 本章小结
汇编生成了机器指令,使得程序真正向可由机器执行的状态转变。生成的目标文件各种数据已分门别类进行封装,符号表和重定位条目也已产生。
第5章 链接
5.1 链接的概念与作用
链接将可重定位目标文件转化为可执行文件。程序中用到的所有目标文件被整合成一个可执行文件,各符号的位置被确定下来,汇编中无法确定的操作数通过重定位修改为正确的操作数。链接对于软件开辟十分重要,它使得模块化编译成为可能,大大减少了开辟软件时的编译工作量,也方便了软件的维护。链接可以是静态的,在编译时执行,也可以是动态的,在程序加载或运行时执行。
5.2 在Ubuntu下链接的命令
ld -o hello hello.o /lib/x86_64-linux-gnu/libc.so.6 /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o /usr/lib/gcc/x86_64-linux-gnu/11/crtbegin.o /usr/lib/gcc/x86_64-linux-gnu/11/crtend.o -dynamic-linker /lib64/ld-linux-x86-64.so.2
5.3 可执行目标文件hello的格式
这是一个x86-64可执行文件,程序头表偏移量为64字节,节头表偏移量为14080字节,共12段、30节,入口点地址为0x401180。
各节根本信息如下图所示,可以发现各节的运行时地址已经分配、GOT、PLT等布局也已生成;而相较于hello.o,.text等节的巨细均有所增加,这是因为在链接环节各目标文件的相应节被归并导致的。
程序头表以及各节到各段的映射关系如下图所示:
重定位条目如下,这些条目是用于动态链接的重定位的,其中.rela.dyn中的条目对应.got中的数据,而.rela.plt中的条目对应.got.plt中的数据。究竟上,.rela.dyn中的条目在加载时即重定位,.rela.plt中的条目则是延迟绑定,二者都对应CSAPP中的统一的GOT表:
符号表如下,可以发现所有动态链接符号的状态均为UND,_start对应了程序的入口点,_end指示了程序数据段的结束位置,即program break,堆区从这里开始:
5.4 hello的假造地址空间
假造地址分配如下:
可以发现每个区域巨细至少是4KB,按照4KB对齐,这恰好是运行情况假造内存的页巨细;来自hello文件的区域有四个,分别对应hello中属性为LOAD的四个段,即程序头、代码、只读数据、可读写数据;还有四个区域来自动态链接器,为程序加载时动态链接的共享库。剩下的三个区域有一个是用户栈,还有两个用于在用户态下进行快速系统调用。
5.5 链接的重定位过程分析
反汇编分析hello,文件中出现了CRT中定义的为维护程序执行情况所需的附加函数,如_init();PLT也已出现;hello.o中缺失的操作数已经全部被更换为到现实函数、变量地址或对应GOT、PLT的引用,能够正确执行:
在链接时,ld起首对符号表中所有引用的符号进行解析;然后重定位节和符号定义,为所有指令和变量分配运行时地址;再根据目标文件中的重定位条目重定位节中的符号引用,产生可执行文件。在加载时,loader调用动态链接器修改GOT中动态链接条目,进行一部门动态链接;剩下的动态链接则在调用相应函数时由PLT调用动态链接器修改GOT条目完成,即延迟绑定。
5.6 hello的执行流程
这里我clone了glibc的源代码,利用与本机情况一致的2.35版本进行分析。
在程序加载时,shell调用execve(),loader发现hello含一个INTERP段,程序需要动态链接器,于是从程序头表中找到动态链接器的位置,加载动态链接器并进行预备工作,当预备工作完成返回用户空间时,控制权返回到了表明器的入口处。故也可以说execve()返回一次,返回到新程序的入口。对于静态链接的程序,其直接返回到程序的_start符号指示的位置,或说_start()。
在程序加载后,PC起首指向hello利用的动态链接器/lib64/ld-linux-x86-64.so.2的入口点_dl_start(),这个地址在每次加载时都不同,可能是Linux缓解缓冲区溢出攻击的手段。_dl_start()对bootstrap_map进行初始化,随后调用_dl_start_final(),_dl_start_final()颠末一系列处理后在_dl_start_user()中调用_dl_init(),修改部门动态链接函数的GOT,随后由_dl_start_user()通过跳转指令jmp跳转到_start():
_start()接着通过GOT条目跳转到动态链接的__libc_start_main(),这个函数会进行一些初始化工作、调用_init()函数,在合适的时候调用__libc_start_call_main()函数,由此跳转到main()。
在此处我们并不关心main()的举动,只关心其退出流程。由于执行时没有给出参数,程序第13行argc != 4判断为真,在调用printf()(优化为puts())输出利用方式后函数即利用exit(1)退出。exit()为动态链接,利用PLT通过GOT进行调用,在执行完_fini()等收尾工作后调用libc提供的_exit(),通过系统调用通知系统进程退出。
5.7 Hello的动态链接分析
在动态链接时,链接器依据符号的重定位条目修改GOT表的对应条目。hello的GOT表地址为0x403FF0-0x404047,在_dl_init()执行前,GOT内容如下:
在_dl_init()执行后,GOT内容如下:
可以发现0x403FF0、0x404008、0x404010处的GOT条目被修改了。其中0x403FF0处的条目是__libc_start_main()的地址,而其余两个条目则与动态链接器有关。0x404018-0x0x404047处的六个条目为延迟绑定的函数,其初始值指向各自PLT条目标链接部门。当调用这里的函数时,控制流起首传递给对应的PLT条目,然后由PLT条目中的代码读取GOT中存储的函数地址进行跳转。当函数第一次调用时,代码跳转到PLT条目中的链接部门,调用动态链接器完成链接、修改GOT,随后将控制权传递给被调用的函数。
5.8 本章小结
通过链接,可重定位目标文件形成了真正可执行的程序。汇编中无法确定的操作数地址在链接时得以确定,GOT和PLT使得位置无关代码成为可能。链接将软件开辟模块化,使得运行时情况能方便的为程序员提供底层支持,也减少了编译的资源占用;动态链接进一步缩小了程序所占用的空间,也为软件的开辟带来了方便:应用热更新技术就是基于动态链接实现的。
第6章 hello进程管理
6.1 进程的概念与作用
按经典定义,进程是一个执行中程序的实例。系统中每个程序都运行在某个进程的上下文中。进程为应用程序提供独立的逻辑控制流和私有的地址空间,让程序似乎是独占地利用处理器和内存,也方便了相同程序的多次启动。
6.2 简述壳Shell-bash的作用与处理流程
Shell为用户提供用户界面,它提供了系统和用户之间的接口,利用户能够通过较清晰的方式调用操作系统和应用程序提供的功能。
用户输入命令后,shell解析命令并作出相应的处理:起首判断命令是否为内部命令,若是则由内部提供的处理函数进行处理;否则以为命令意图为启动外部程序,通过系统调用交由操作系统进行处理。在处理中还要注意恰当的非常处理。
6.3 Hello的fork进程创建过程
当用户在shell中键入启动hello的命令后,shell对命令进行解析,发现用户意图创建新作业,于是预备好必要的数据后启动子进程进行处理:shell利用fork()创建子进程,子进程与父进程拥有相同但是相互独立的地址空间,且共享已打开的文件。然而,两个进程的PID不同,fork()的返回值也不同,通常情况下PGID也不同。在父进程中,fork()返回子进程PID,父进程以此确认自己是父进程,维护作业列表并依据用户意图决定作业在前台还是后台执行;在子进程中,fork()返回0,子进程以此确认自己是子进程,调用setpgid()获取独立的进程组,并进行用户意图的操作。
6.4 Hello的execve过程
Shell解析命令时发现用户意图启动外部应用,于是预备好argv、情况变量等数据,子进程获取独立进程组后调用execve(),装载argv、情况变量。execve()删除子进程用户部门的区域布局,并为新程序的各装载段创建新的区域布局,进行可选的动态链接,最后将控制传递给新程序的_start()函数,开始新程序的执行。
6.5 Hello的进程执行
在hello执行时,其看起来是独占地利用处理器和内存。然而,它并不能完全独占计算机资源,否则其他进程将无法运行。这就涉及到进程的切换问题和资源的分配问题。
内核为每个进程维护一个上下文,它包罗重新启动一个被抢占的进程所需的信息,包罗寄存器、PC、栈等;操作系统内核中的调度器决定每个时候由什么进程利用计算机的资源。当因为某些原因,例如内核代表进程执行系统调用、定时器中断等,调度器决定将资源分配给其他进程利用,于是进行上下文切换:操作系统保存当前进程的上下文,再规复新调度进程的上下文,最后将控制传递给新调度的进程,完成上下文切换,新进程开始运行。
一个进程执行它的控制流的一部门的每一时间段叫做时间片。调度器负责给每个进程分配时间片。当进程运行了一段时间,通常是足够的定时器中断之后,就可能发生上下文切换。
每个进程可能工作在两个模式下:用户模式和内核模式。内核模式中的进程可以执行一些用户模式进程不能执行的特权指令,也能访问用户模式进程不能直接访问的内核区代码和数据。通常的用户程序运行在用户模式下,而内核运行在内核模式下。在进行上下文切换时,内核代表用户程序执行一些操作,起首在用户模式下执行指令,之后切换到内核模式开始切换上下文;上下文切换完成后,内核继承代表新进程在内核模式下执行一些操作,再切换到用户模式执行一些操作,随后控制权返回给新进程。
6.6 hello的非常与信号处理
思量hello为独一无二的进程,即之前内存中不存在hello的内存映像。
hello刚开始执行时,由于内存中不存在hello的数据,处理器产生缺页非常,将利用的数据对应的虚存页读入主存,随后程序重新执行引起非常的指令;当hello运行了一段时间时,计时器可能发出计时器中断,于是处理器在执行完当前指令后跳转到中断处理程序进行处理,随后返回到下一条指令继承执行,也可能进行上下文切换;当hello需要进行系统调用时,程序执行syscall指令陷入内核,由非常处理程序进行适当的操作,随后返回到syscall的下一条指令继承执行。这些过程不会产生信号。
而当用户在程序执行时进行输入,则可能由内核发送信号到前台进程组中的进程。
6.6.1 随机输入
在程序执行时进行随机输入并不会导致程序执行举动的改变,输入的数据会被打印在屏幕上,并在程序运行结束后由终端进行处理。当用户按键时,键盘发出中断信号,处理器相应中断,并将输入的字符读入stdin缓冲区、输出给终端,随后将控制返回给hello。由于hello并不从缓冲区中读取输入,这些字符一直存在缓冲区中,直到进程退出,shell成为前台进程并担当输入,将这些字符当成用户输入进行解析。
6.6.2 Ctrl+Z
当按下Ctrl+Z时,键盘发出中断,处理器跳转到中断处理程序,内核发现这是一个预定义的组合键,于是按照定义的举动向前台进程组发送SIGTSTP信号并返回;在返回进入用户模式时,内核检查发现前台进程hello有待处理且未被阻塞的SIGTSTP信号,且对应的进程举动需要进入信号处理程序,于是控制权被传递给SIGTSTP对应的信号处理程序,信号被捕捉,处理程序将进程停止,期待SIGCONT信号的到来。这时前台进程变成了shell,shell开始期待用户输入命令。
当利用fg、kill -SIGCONT pid等指令时,进程吸收到SIGCONT信号,在下一次被调度到进入用户模式时内核发现有未处理的信号并进行处理,规复进程的执行。
当利用kill -SIGKILL pid指令时,进程吸收到SIGKILL信号,下一次被调度到进入用户模式时内核处理信号,而该信号对应的举动是终止进程,于是进程被终止。
6.6.3 Ctrl+C
当按下Ctrl+C时,颠末一系列处理进程吸收到SIGKILL信号,进程被终止。
6.7本章小结
进程是计算机科学中最深刻、最乐成的概念之一,它为应用程序提供独立的逻辑控制流和私有的地址空间,让程序似乎是独占地利用处理器和内存,也为多任务的实现提供了方便。操作系统对进程进行调度,并利用上下文来保存及规复进程的状态;非常和信号等机制构建起了多任务系统的框架。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址在计算机体系布局中指从应用程序角度看到的存储器单位。在x86/x86-64情况下,逻辑地址用于分段寻址、段页式寻址,以段标识符:段内偏移量的形式存在。它不同于物理地址,需颠末转化产生物理地址才气用于现实访存。
线性地址由段基址+偏移量构成,颠末地址翻译可得到用于访问内存的物理地址。在未开启分页机制时,线性地址直接作为物理地址访存;但当代Linux系统在运行时现实上将段基址置为0而不利用段页/分段寻址,即flat mode,这样产生的线性地址现实上就等于逻辑地址,如果继承将其作为物理地址访存势必造成程序间的冲突,因而引入了假造地址的概念,将线性地址与物理地址解耦。
当开启分页机制时,线性地址不再直接对应物理地址,而对应假造地址。假造地址位于假造地址空间中,连续的假造地址被分割为一个个巨细固定的块,称为假造页。假造地址以页为单位映射到对应的物理页,或者未被映射而存储在磁盘上。
物理地址即数据在内存中现实存储的地址,处理器最终以此访存。
7.2 Intel逻辑地址到线性地址的变换-段式管理
处理器利用其段式内存管理单位将逻辑地址转换为线性地址。
通过读取逻辑地址的段标识符,段式内存管理单位从段形貌符表(GDT和LDT)中找到该段的段形貌符,从中找到段基址,并与段内偏移量相加得到线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
开启分页机制后,处理器的MMU将假造地址转换为物理地址。在Linux中线性地址即为假造地址。
MMU解析假造地址得到假造页号和页内偏移量,通过假造页号查询页表得到物理页号,最终将物理页号与页内偏移量归并产生物理地址。下图是假造地址的构成:
7.4 TLB与四级页表支持下的VA到PA的变换
MMU起首解析假造地址,得到地址对应的业内偏移量和页表号。由于页表有四级,故产生四个页表号,通常四级次序对应假造地址位从高到低的四段。下图为四级页表下假造地址的构成:
MMU接着从一级页表号开始查询对应的页表条目:从CR3寄存器中取出一级页表基址,利用一级页表号查询一级页表中对应的PTE,得到二级页表的基址;再利用二级页表号查询二级页表中对应的PTE,得到三级页表的基址;以此类推,直到在四级页表中查询到对应的PTE,得到假造页对应的物理页号。
在查询过程中,MMU起首在TLB中查询对应PTE是否被缓存,若被缓存(TLB掷中)则直接取出PTE,否则在Cache/主存中继承探求PTE。若查询过程中发现对应PTE有效位为0则MMU产生缺页故障,中止翻译。
在TLB中,页号被解析为TLBI和TLBT,TLB利用TLBI选定对应的组,再在组中各行匹配对应的TLBT。若TLBT匹配乐成且对应行有效则TLB掷中,返回PTE;否则TLB不掷中,需在缓存/主存中继承查询。下图为VPN与TLBT、TLBI的对应:
7.5 三级Cache支持下的物理内存访问
在得到物理地址后,处理器开始进行内存访问。
处理器起首查询各级Cache中是否缓存了想要查询的数据:从L1 Cache开始,处理器查询数据是否被缓存;若L1 Cache miss则在L2 Cache中查询;若L2 Cache miss则在L3 Cache中查询,若L3 Cache miss则在主存中取数据。主存中是一定存储了需要的数据的,否则地址翻译时就会产生缺页故障,得不到物理地址。
在每级Cache查询过程中,Cache将物理地址进行解析,得到CT、CI和CO;随后Cache根据CI匹配对应的Cache组,再在组中各行匹配对应的CT。若CT匹配乐成且行有效则Cache掷中,根据CO找到需访问数据并返回;否则Cache不掷中,在下一级缓存/主存中继承查询。下图为物理地址与CT、CI、CO的对应关系:
若访存操作要写内存且Cache掷中则面对一个问题:怎么更新缓存层次较低的数据副本?不同Cache接纳不同计谋,常用的两种分别称为直写和写回法,直写法在更新对应的Cache块后立即更新低层次的数据副本,写回法则在数据地点行被更换时才更新低层次的数据副本。
Cache不掷中分为读不掷中和写不掷中。在读不掷中时,Cache将访问的数据读入Cache中,若Cache组满则通过算法选出更换行进行更换;写不掷中的计谋较复杂,主要分为写分配和非写分配两种:写分配将数据加载到Cache中并更新Cache,非写分配则直接更新较低层次中的数据。通常而言,直写Cache是非写分配的,而写回Cache是写分配的。
7.6 hello进程fork时的内存映射
当shell调用fork()函数时,内核为新进程创建各种数据布局,并为其分配唯一的PID。Shell当前的mm_struct、区域布局和页表被原样复制,新旧进程的每个页面均被标记为只读、每个区域均标记为私有的写时复制。当两个进程中的任一个进行写操作时触发非常,写时复制机制创建新页面,内存映像便产生了差异。
7.7 hello进程execve时的内存映射
当shell子进程调用execve()函数时,已存在的用户区域被删除,新程序的代码、数据、bss、栈等区域的区域布局被创建为私有区域。在映射的私有区域中,代码区域映射为程序的代码段;数据区域映射为.data节、GOT等;bss区域映射为匿名文件,哀求二进制零;堆栈在起始时长度为零,也哀求二进制零。
随后控制权交给动态链接器,动态链接器映射共享区域并进行一部门动态链接,构建动身序开始执行时的内存映像,最终将控制权交给程序。
7.8 缺页故障与缺页中断处理
当访存指令中MMU地址翻译得到无效页表,比如在程序刚开始执行,内存中没有数据时,MMU会产生缺页故障,将控制权传递给内核非常处理程序。非常处理程序利用mm_struct中的数据判断当前访问是否合法,若不合法则判断是否为堆栈增加引起的,若不是则触发段错误,向进程发送SIGSEGV信号,否则分配新页;若合法则判断权限是否满足,若不满足则触发保护非常,若满足则正式处理缺页:将当前页面加载到内存中并更新页表,若主存满则选择一个捐躯页换出再进行加载。当处理程序完成处理后,处理器重新执行引起缺页故障的访存指令,MMU正常翻译物理地址,完成访存。
7.9本章小结
存储器提供了数据储存的场所,复杂的地址翻译机制使进程实现成为可能,为进程提供了可能宏大于机器现实内存的内存空间。假造内存节省了内存占用,写时复制大大低落了资源开销,也为优化提供了更多的可能。
结论
在程序员编写完hello.c后,hello.c便交由编译器进行编译。
hello.c颠末预处理,预处理命令、宏定义消失,外部库代码被复制、代码被展开,得到hello.i。
hello.i颠末编译得到汇编程序,展示出机器级表示程序的雏形,生成hello.s。
hello.s颠末汇编,汇编指令转化为机器指令,形成可重定位目标文件hello.o,符号表和重定位条目在此时产生。
hello.o颠末链接,多个模块的数据被综合到一起,形成了最终的可执行文件hello,GOT和PLT在此时产生。
当在shell中输入hello启动程序时,shell解析命令并fork出子进程,调用execve()装载hello,完成私有区域的映射,并由动态链接器进行动态链接完成内存映射,最后将控制权交给hello。
hello运行时通过系统提供的系统调用完成I/O、内存分配等操作;在运行时可能会被抢占,进行上下文切换;也会经历故障,由操作系统进行处理;遇到信号时,内核调用进程的信号处理程序进行处理;在访存时,复杂的访存机制为进程提供了统一的抽象。
hello完成运行后退出,shell吸收到SIGCHLD信号,回收进程的资源。
计算机系统是一个复杂的整体,软硬件在其中精密协同,分工共同,完成各种任务。多年来计算机范畴的创新不断,计算机系统变得越来越完善,越来越易用。这个范畴仍在不断发展,新的概念仍在不断涌现,拥有着顽强的生命力:即使是拥有久长汗青的x86体系布局也在演进,Intel提出了x86S ISA,意在让x86-64更加符合现实需求。而在新指令集方面,RISC-V正在迅速发展,前景广阔。感谢计算机范畴的研究者和从业者们,正是他们的工作让计算机从概念变成现实、从实行室走进千家万户,深刻改变了我们的生活。
感谢CMU提供了这样一门高质量的ICS课程,也感谢工大老师的倾情辅导。这门课让我对计算机的运行原理有了更加深入的熟悉,也锻炼了我的编程本领。
附件
hello.i:颠末预处理的C语言hello程序
hello.s:颠末编译的汇编语言hello程序
hello.o:颠末汇编的hello可重定位目标文件
hello:颠末链接的hello可执行链接文件
参考文献
[1] Randal E, Rryant, David R. O’Hallaron. 深入明白计算机系统(原书第3版)[M]. 北京: 机械工业出版社, 2016.
[2] Intel. Intel® 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D, and 4[EB/OL]. [2023-05-23]. Intel® 64 and IA-32 Architectures Software Developer Manuals.
[3] GNU Project. Source code of The GNU C Library[DB/OL]. [2023-05-22]. https://www.gnu.org/software/libc/sources.html.
[4] GNU Project. How libc startup in a process works on GNU Hurd[DB/OL]. [2023-05-22]. startup.
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |