计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 物联网工程
学 号 2022112****
班 级 2237301
学 生 GBP
指 导 教 师 吴锐
计算机科学与技术学院
2024年5月
本论文分析了hello程序在Linux系统下的整个生命周期,依次从编译预处理,编译,汇编,链接,进程管理,存储管理的角度升入展示了hello.c文件的执行过程,并借此对计算机系统中学习到的知识进行了回顾和运用,对linx下的计算机系统有了更加深入的理解感悟。
关键词:hello.c,编译,汇编,链接,存储,进程,计算机系统
(摘要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简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
Hello的P2P是指hello.c文件从可执行程序(Program)变为运行时进程(Process)的过程。在Linux系统下,hello.c 文件依次经过cpp(C Pre-Processor,C预处理器)预处理、ccl(C Compiler,C编译器) 编译、as (Assembler,汇编器)汇编、ld (Linker,链接器)链接最终成为可执行目标程序hello(在Linux下该文件无固定后缀)。打开shell,输入命令./hello后,shell 通过fork产生子进程,hello 便从可执行程序(Program)变成为进程(Process)。
Hello的020是指hello.c文件“From 0 to 0”,初始时内存中并无hello文件的相关内容,这便是“From 0”。通过在Shell下调用execve函数,系统会将hello文件载入内存,执行相关代码,当程序运行结束后, hello进程被回收,并由内核删除hello相关数据,这即为“to 0”。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1.2.1硬件环境
1.2.2 软件环境
Windows10 64位; Vmware 14; Ubuntu20.04
1.2.3 开发工具
Visual Studio Code; vi/vim/gpedit+gcc,泰山处理器
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c 源文件
hello.i 预编译结果
hello.s 编译结果
hello.o 汇编结果
hello.o.txt hello.o反汇编结果
hello 可执行文件
hello.elf hello对应的elf 文件
helloelf.txt hello生成的elf文件对应的文本格式
1.4 本章小结
本章对hello进行了一个总体的概括,首先介绍了P2P、020的意义和过程,介绍了作业中的硬件环境、软件环境和开发工具,最后简述了从.c文件到可执行文件的过程
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理步骤是指程序开始运行时,预处理器(cpp,C Pre-Processor,C预处理器)根据以字符#开头的命令,修改原始的C程序的过程。例如,hello.c文件6到8行中的#include 命令会告诉预处理器读取系统头文件stdio.h,unistd.h,stdlib.h 的内容,并把这些内容直接插入到程序文本中。用实际值替换用#define定义的字符串。除此之外,预处理过程还会删除程序中的注释和多余的空白字符。预处理通常得到另一个以.i作为拓展名的C程序。
预处理步骤是指程序开始运行时,预处理器(cpp,C Pre-Processor,C预处理器)根据以字符#开头的命令,修改原始的C程序的过程。例如,hello.c文件6到8行中的#include 命令会告诉预处理器读取系统头文件stdio.h,unistd.h,stdlib.h 的内容,并把这些内容直接插入到程序文本中。用实际值替换用#define定义的字符串。除此之外,预处理过程还会删除程序中的注释和多余的空白字符。预处理通常得到另一个以.i作为拓展名的C程序。
2.2在Ubuntu下预处理的命令
gcc –E hello.c –o hello.i
2.3 Hello的预处理结果解析
在Linux下打开hello.i文件,可以发现hello.i程序已经拓展为3062行,行数比起hello.c文件大幅增加。其中, hello.c中的main函数相关代码在hello.i程序中对应着3047行到3062行。
2.4 本章小结
本章主要介绍了预处理的概念及作用、并结合Ubuntu系统下hello.c文件实际预处理之后得到的hello.i程序对预处理结果进行了解析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
- 编译的概念
编译是指C编译器ccl通过词法分析和语法分析,将合法指令翻译成等价汇编代码的过程。通过编译过程,编译器将文本文件 hello.i 翻译成汇编语言文件 hello.s,在hello.s中,以文本的形式描述了一条条低级机器语言指令。
- 编译的作用
将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做准备。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
(以下格式自行编排,编辑时删除)
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
z
.file | 源文件 |
.text | 代码段 |
.global | 全局变量 |
.data | 存放已经初始化的全局和静态C 变量 |
.section .rodata | 存放只读变量 |
.align | 对齐方式 |
.type | 表示是函数类型/对象类型 |
.size | 表示大小 |
.long .string | 表示是long类型/string类型 |
3.4 本章小结
本章介绍了编译的概念与作用,编译是将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做准备的过程。同时,本章以hello.s文件为例,介绍了编译器如何处理各个数据类型以及各类操作,验证了大部分数据、操作在汇编代码中的实现。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
- 汇编的概念
汇编是指汇编器(assembler)将以.s结尾的汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在.o 目标文件中的过程
- 汇编的作用
将汇编语言翻译为机器语言,并将相关指令以可重定位目标程序格式保存在.o文件中
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
应截图,展示汇编过程!
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
- ELF 头(ELF Header):
以 16字节序列 Magic 开始,其描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头大小、目标文件类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量等相关信息。
- 节头:
包含了文件中出现的各个节的意义,包括节的类型、位置和大小等信息。
- 重定位节.rela.text
一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
在这里,8 条重定位信息分别是对.L0(第一个 printf 中的字符串)、puts 函数、exit 函数、.L1(第二个 printf 中的字符串)、printf 函数、sleepsecs、sleep 函数、getchar 函数进行重定位声明。
- 重定位节.rela.eh_frame
-
4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
- 分支转移:
在hello.s中,跳转指令的目标地址直接记为段名称,如.L2,.L3等。而在反汇编得到的hello.asm中,跳转的目标为具体的地址,在机器代码中体现为目标指令地址与当前指令下一条指令的地址之差。
- 函数调用:
在hello.s文件中,call之后直接跟着函数名称,而在反汇编得到的hello.asm中,call 的目标地址是当前指令的下一条指令。这是因为 hello.c 中调用的函数都是共享库中的函数,最终需要通过动态链接器作用才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其 call 指令后的相对地址设置为全0(此时,目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接进一步确定。
- 全局变量访问:
在hello.s 文件中,使用段名称+%rip访问 rodata(printf 中的字符串),而在反汇编得到的hello.asm中,使用 0+%rip进行访问。其原因与函数调用类似,rodata 中数据地址在运行时才能确定,故访问时也需要重定位。在汇编成为机器语言时,将操作数设置为全 0 并添加相应的重定位条目。
4.5 本章小结
本章对汇编的概念与作用进行了介绍。以hello.s文件在Linux系统下通过将hello.s文件汇编为hello.o文件,并生成hello.o的ELF格式文件hello.elf为例,研究了ELF格式文件的具体结构。通过比较hello.o的反汇编代码(保存在hello.asm中)与hello.s的异同,令人深刻认识到了从汇编语言到机器语言实现的转变,和在这个过程中机器为链接所做出的准备
(第4章1分)
第5章 链接
5.1 链接的概念与作用
- 链接的概念
链接是指通过链接器(Linker),将程序编码与数据块收集并整理成为一个单一文件,生成完全链接的可执行的目标文件(windows系统下为.exe文件,Linux系统下一般省略后缀名)的过程。
- 链接的作用
提供了一种模块化的方式,可以将程序编写为一个较小的源文件的集合,且实现了分开编译更改源文件,从而减少整体文件的复杂度与大小,增加容错性,也方便对某一模块进行针对性修改注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令
(以下格式自行编排,编辑时删除)
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
根据计算机系统的特性,程序被载入至地址0x400100~0x402000中。在该地址范围内,每个节的地址都与前一节中节对应的 Address 相同。根据edb查看的结果,在地址空间0x400100~0x4001ff中存放着与地址空间0x400100~0x402000相同的程序,在0x401fff之后存放的是.dynamic到.shstrtab节的内容。
5.5 链接的重定位过程分析
(以下格式自行编排,编辑时删除)
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
链接完成过后,动态链接器将共享库中的文件加入到可执行文件种:可见函数数量有所增加,如上截图所示,可以看到新增了puts,printf,getchar,atoi,exit,sleep函数。
通过分析main函数,我们得知,call的参数发生变化,由于在链接过程中,连接器执行了重定位的操作,call的参数地址被修改为目标地址与下一条指令的地址之差。
此外,跳转指令的参数也发生了变化,这也是重定位导致的。
5.6 hello的执行流程
(以下格式自行编排,编辑时删除)
使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。
函数 | 地址 |
main | 0x0000000000401125 |
init | 0x0000000000401000 |
puts | 0x401090 |
exit@plt | 0x4010d0 |
getchar@plt | 0x4010b0 |
printf@plt | 0x4010a0 |
atoi@plt | 0x10c0 |
sleep@plt | 0x4010d0 |
exit | 0x00007ff9a7c455f0 |
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变化。要截图标识说明。
编译器在编译时无法预知函数在内存中的实际地址,因此添加重定位记录以指导动态链接器。动态链接器通过过程链接表(PLT)和全局偏移量表(GOT)在运行时解析函数地址。GOT存储函数的目标地址,而PLT使用GOT中的地址间接跳转到目标函数。动态链接器在加载时解析GOT条目,以确保包含正确的函数地址。这一过程避免了运行时对代码段的直接修改,采用了延迟绑定的策略。
查找共享库:
根据程序中 hello 可执行文件的头部信息,找到需要加载的动态库。
查找这些动态库的路径,通常包括 /lib, /usr/lib, /usr/local/lib 等。
读取程序中指定的共享库名称,并加载这些库到内存中。
链接共享库:
动态链接器将程序的需要链接的库函数和已经加载到内存中的共享库进行链接,建立起正确的调用关系。
5.8 本章小结
本章中介绍了链接的概念与作用,对链接后的函数地址和参数传递的过程进行了分析,对重定位和动态链接的过程有了更直观的展示与理解。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个正在运行的程序的实例,系统中的每一个程序都运行在某个进程的上下文中。
- 进程的作用
给应用程序提供两个关键抽象:
- 一个独立的逻辑控制流,提供一个假象,好像程序独占地使用处理器
- 一个私有地址空间,提供一个假象,好像程序独占地使用内存系统
6.2 简述壳Shell-bash的作用与处理流程
Shell 是一个用C语言编写的交互型应用程序,代表用户运行其他程序。Shell 应用程序提供了一个界面,用户可以通过这个界面进行系统的基本操作,访问操作系统内核的服务。
Shell的处理流程大致如下:
- 从Shell终端读入输入的命令。
- 切分输入字符串,获得并识别所有的参数
- 若输入参数为内置命令,则立即执行
- 若输入参数并非内置命令,则调用相应的程序为其分配子进程并运行
- 若输入参数非法,则返回错误信息
- 处理完当前参数后继续处理下一参数,直到处理完毕
- Hello的fork进程创建过程
打开Shell,输入命令./hello 2022112965 gbp 15282396630 3,带参数执行生成的可执行文件。带参执行当前目录下的可执行文件hello,父进程会通过fork函数创建一个新的运行的子进程hello。子进程获取了与父进程的上下文,包括栈、通用寄存器、程序计数器,环境变量和打开的文件相同的一份副本。子进程与父进程的最大区别是有着跟父进程不一样的PID,子进程可以读取父进程打开的任何文件。当子进程运行结束时,父进程如果仍然存在,则执行对子进程的回收,否则就由init进程回收子进程。
6.4 Hello的execve过程
用函数fork创建新的子进程之后,子进程会调用execve函数,在当前进程的上下文中加载并运行一个新程序hello。execve 函数从不返回,它将删除该进程的代码和地址空间内的内容并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行该程序。将私有的区域映射进来,例如打开的文件,代码、数据段,然后将公共的区域映射进来。后面加载器跳转到程序的入口点,即设置PC指向_start 地址。_start函数最终调用hello中的 main 函数,这样,便完成了在子进程中的加载。
6.5 Hello的进程执行
在程序运行时,Shell为hello fork了一个子进程,这个子进程与Shell有独立的逻辑控制流。在hello的运行过程中,若hello进程不被抢占,则正常执行;若被抢占,则进入内核模式,进行上下文切换,转入用户模式,调度其他进程。直到当hello调用sleep函数时,为了最大化利用处理器资源,sleep函数会向内核发送请求将hello挂起,并进行上下文切换,进入内核模式切换到其他进程,切换回用户模式运行抢占的进程。与此同时,将 hello 进程从运行队列加入等待队列,由用户模式变成内核模式,并开始计时。当计时结束时,sleep函数返回,触发一个中断,使得hello进程重新被调度,将其从等待队列中移出,并内核模式转为用户模式。此时 hello 进程就可以继续执行其逻辑控制流。
用户态与核心态转换
在执行过程中,操作系统会将 CPU 的运行模式切换为不同的特权级别,通常有用户态和核心态两种模式:
用户态(User Mode):进程在这个模式下执行,只能访问自己的内存空间,无法直接访问硬件或操作系统核心功能。
核心态(Kernel Mode):操作系统和核心模块在这个模式下执行,可以访问所有硬件和系统资源。
进程在执行系统调用(如读取文件、分配内存等)时会触发用户态到核心态的切换,操作系统会暂时提升进程的特权级别来执行必要的操作,然后再切换回用户态。
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结
- 首先运行hello程序,根据程序提醒输入,可见程序以每3s一次输出学生信息,此时键盘乱按,随机输入,程序依旧正常运行
- 当按下ctrl Z 时,进程收到SIGSTP信号,hello停止,但是进程并未回收,此时通过ps指令查看pid信息,再使用fg指令将其调回前台继续运行。
3.当输入ctrl C,进程收到SIGINT信号时,程序终止,查询不到此进程的pid
4.当输入kill指令,停止的命令被终止,无法查询pid
- pstree
-
截屏,说明异常与信号的处理。
6.7本章小结
本章介绍了进程的概念与作用。关于进程,在这一章中根据hello可执行文件的具体示例研究了fork,execve的原理与执行过程,展示了hello在执行过程中各种异常与信号的处理结果。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
- 逻辑地址
逻辑地址是指由程序产生的与段相关的偏移地址部分,逻辑地址由选择符和偏移量两部分组成。具体而言,其为hello.asm中的相对偏移地址。
- 线性地址
逻辑地址经过段机制转化后为线性地址,其为处理器可寻址空间的地址,用于描述程序分页信息的地址。具体以hello而言,线性地址标志着 hello 应在内存上哪些具体数据块上运行。
- 虚拟地址
根据CSAPP教材,虚拟地址即为上述线性地址。
- 物理地址
CPU通过地址总线的寻址,找到真实的物理内存对应地址。
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
Intel处理器从逻辑地址到线性地址的变换通过段式管理的方式实现。每个程序在系统中都保存着一个段表,段表保存着该程序各段装入主存的状况信息,包括段号或段名、段起点、装入位、段的长度、主存占用区域表、主存可用区域表等,从而方便进行段式管理。
在段寄存器中,存放着段选择符,可以通过段选择符来得到对应段首地址。段选择符的结构如下:
图 48 段选择符的情况
其包含三部分:索引,TI,RPL
索引:用来确定当前使用的段描述符在描述符表中的位置;
TI:根据TI的值判断选择全局描述符表(TI=0,GDT)或选择局部描述符表(TI=1,LDT);
RPL:判断重要等级。RPL=00,为第0级,位于最高级的内核,RPL=11,为第3级,位于最低级的用户状态;
通过一个索引,可以定位到段描述符,进而通过段描述符得到段基址。段基址与偏移量结合就得到了线性地址,虚拟地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(VA)到物理地址(PA)之间的转换通过对虚拟地址内存空间进行分页的分页机制完成。
通过7.2节中的段式管理过程,可以得到了线性地址/虚拟地址,记为VA。虚拟地址可被分为两个部分:VPN(虚拟页号)和VPO(虚拟页偏移量),根据计算机系统的特性可以确定VPN与VPO的具体位数,由于虚拟内存与物理内存的页大小相同,因此VPO与PPO(物理页偏移量)一致。而PPN(物理页号)则需通过访问页表中的页表条目(PTE)获取,如下图所示。
若PTE的有效位为1,则发生页命中,可以直接获取到物理页号PPN,PPN与PPO共同组成物理地址。
若PTE的有效位为0,说明对应虚拟页没有缓存到物理内存中,产生缺页故障,调用操作系统的内核的缺页处理程序,确定牺牲页,并调入新的页面。再返回到原来的进程,再次调用导致缺页的指令。此时发生页命中,获取到PPN,与PPO共同组成物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是一个硬件缓存,用于加速虚拟地址到物理地址的转换。在使用虚拟内存的系统中,程序生成的内存访问地址是虚拟地址(VA),而物理内存中存储数据的地址是物理地址(PA)。CPU在访问内存时,需要将虚拟地址转换为物理地址才能正确访问内存中的数据。
TLB的作用是存储最近使用过的一些虚拟地址到物理地址的映射,避免每次访存都要去查询更慢的页表。它类似于高速缓存,存储了虚拟地址的一部分(通常是页面号)到物理地址的映射关系。当CPU需要访问内存时,首先在TLB中查找虚拟地址的物理地址映射,如果命中(即找到了对应的映射),则可以直接使用这个映射来访问物理内存;如果未命中,则需要从页表中获取映射,并将其加入TLB以供将来使用。
四级页表(或多级页表)
多级页表是操作系统用来管理虚拟内存和物理内存映射的数据结构。传统的 x86 体系结构通常采用两级页表(页目录表 + 页表),而现代系统(如 x86-64)则引入了更多级别的页表来支持更大的虚拟地址空间和更高效的页表管理。一般来说,页表级别越多,能管理的虚拟地址空间就越大,但页表的深度也会增加访问时间。
转换过程如图:
CPU产生虚拟地址VA,并将其传送至MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)在TLB中进行匹配,若命中,则得到PPN(40bit)与VPO(12bit)组合成物理地址PA(52bit)。若TLB没有命中,则MMU向页表中查询,由CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,则执行下一步确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成PA,并向TLB中添加条目。
多级页表的工作原理展示如下:
7.5 三级Cache支持下的物理内存访问
CPU执行指令:当CPU需要执行一个指令或访问内存中的数据时,首先检查L1 Cache。
L1 Cache访问:如果所需数据在L1 Cache中命中(即找到了),则称为L1 Cache命中(L1 Cache Hit)。此时,CPU可以直接从L1 Cache中获取数据,这是访问速度最快的情况。
L1 Cache未命中:如果所需数据不在L1 Cache中(即L1 Cache未命中,L1 Cache Miss),则CPU会将访问请求传递给L2 Cache。
L2 Cache访问:L2 Cache会在自己的缓存中查找所需数据。如果在L2 Cache中找到(L2 Cache Hit),则称为L2 Cache命中,并将数据返回给CPU。
L2 Cache未命中:如果在L2 Cache中未找到所需数据(L2 Cache Miss),那么CPU会进一步将访问请求传递给L3 Cache。
L3 Cache访问:L3 Cache会尝试在自己的缓存中查找所需数据。如果在L3 Cache中找到(L3 Cache Hit),则称为L3 Cache命中,并将数据返回给CPU。
L3 Cache未命中:如果在L3 Cache中也未找到所需数据(L3 Cache Miss),那么CPU将最终将访问请求传递给主内存(RAM)。
访问主内存:在主内存中查找所需数据,主内存的访问速度相对较慢,通常需要数百个时钟周期才能返回数据。
7.6 hello进程fork时的内存映射
当fork函数被当前进程hello调用时,内核为新进程hello创建各种数据结构,并分配给它一个唯一的PID。为了给这个新的hello创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当着两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数加载并运行hello需要以下几个步骤:
- 删除已存在的用户区域
删除当前进程hello虚拟地址的用户部分中的已存在的区域结构。
- 映射私有区域
为新程序的代码、数据、bss和栈区域创建新的私有的、写时复制的区域结构。其中,代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
- 映射共享区域
若hello程序与共享对象或目标(如标准C库libc.so)链接,则将这些对象动态链接到hello程序,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置程序计数器
最后,execve设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
发生一个缺页异常后,控制会转移到内核的缺页处理程序。判断虚拟地址是否合法,若不合法,则产生一个段错误,然后终止这个进程。
若操作合法,则缺页处理程序从物理内存中确定一个牺牲页,若该牺牲页被修改过,则将它换出到磁盘,换入新的页面并更新页表。当缺页处理程序返回时,CPU 再次执行引起缺页的指令,将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,主存将所请求字返回给处理器。
7.9动态存储分配管理
动态内存分配器维护着一个称为堆的进程的虚拟内存区域。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放可以由应用程序显式执行或内存分配器自身隐式执行。
具体而言,分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
下面介绍动态存储分配管理中较为重要的概念:
- 隐式链表
堆中的空闲块通过头部中的大小字段隐含地连接,分配器通过遍历堆中所有的块,从而间接遍历整个空闲块的集合。
对于隐式链表,其结构如下:
- 显式链表
在每个空闲块中,都包含一个前驱(pred)与后继(succ)指针,从而减少了搜索与适配的时间。
显式链表的结构如下:
- 带边界标记的合并
采取使用边界标记的堆块的格式,在堆块的末尾为其添加一个脚部,其为头部的副本。添加脚部之后,分配器就可以通过检查前面一个块的脚部,判断前面一个块的起始位置和状态。从而实现快速合并,减小性能消耗。
- 分离存储
维护多个空闲链表,其中,每个链表的块具有相同的大小。将所有可能的块大小分成一些等价类,从而进行分离存储。
7.10本章小结
本章主要介绍了存储器地址空间、段式管理、页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得所有的输入和输出都能以一种统一且一致的方式来执行:打开文件、改变当前的文件位置、读写文件、关闭文件。
8.2 简述Unix IO接口及其函数
一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。对于Shell创建的每个进程,其都有三个打开的文件:标准输入,标准输出,标准错误。
- 改变当前的文件位置
对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
- 读写文件
一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
- 关闭文件
内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
- Unix I/O函数:
- int open(char* filename,int flags,mode_t mode)
进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
- int close(fd)
fd是需要关闭的文件的描述符,close返回操作结果。
- ssize_t read(int fd,void *buf,size_t n)
read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
- ssize_t wirte(int fd,const void *buf,size_t n)
write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
查看windows系统下的printf函数体:
形参列表中的…是可变形参的一种写法,当传递参数的个数不确定时,用这种方式来表示。
va_list的定义:typedef char *va_list,说明它是一个字符指针,其中 (char*)(&fmt) + 4) 即arg表示的是...中的第一个参数。
再进一步查看windows系统下的vsprintf函数体:
则知道vsprintf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。
在printf中调用系统函数write(buf,i)将长度为i的buf输出。write函数如下:
printf函数的功能为接受一个格式化命令,并按指定的匹配的参数格式化输出,故i = vsprintf(buf, fmt, arg)是得到打印出来的字符串长度,其后的write(buf, i)是将buf中的i个元素写到终端。
因此,vsprintf的作用为接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,进而产生格式化输出。
再进一步对write进行追踪:
这里给几个寄存器传递了参数,然后以一个int INT_VECTOR_SYS_CALL结束。INT_VECTOR_SYS_CALL代表通过系统调用syscall,查看syscall的实现:
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码,符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
getchar调用系统函数read,发送一个中断信号,内核抢占这个进程,用户输入字符串,键入回车后(字符串和回车都保存在缓冲区内),再次发送信号,内核重新调度这个进程,getchar从缓冲区读入字符。
8.5本章小结
本章主要介绍了linux的IO设备管理方法和及其接口和函数,对printf函数和getchar函数的底层实现有了基本了解(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
- 预处理
将hello.c中include的所有外部的头文件头文件内容直接插入程序文本中,完成字符串的替换,方便后续处理;
- 编译
通过词法分析和语法分析,将合法指令翻译成等价汇编代码。通过编译过程,编译器将hello.i 翻译成汇编语言文件 hello.s;
- 汇编
将hello.s汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在hello.o 目标文件中;
- 链接
通过链接器,将hello的程序编码与动态链接库等收集整理成为一个单一文件,生成完全链接的可执行的目标文件hello;
- 加载运行
打开Shell,在其中键入 ./hello 1190200208 李旻翀,终端为其fork新建进程,并通过execve把代码和数据加载入虚拟内存空间,程序开始执行;
- 执行指令
在该进程被调度时,CPU为hello其分配时间片,在一个时间片中,hello享有CPU全部资源,PC寄存器一步一步地更新,CPU不断地取指,顺序执行自己的控制逻辑流;
- 访存
内存管理单元MMU将逻辑地址,一步步映射成物理地址,进而通过三级高速缓存系统访问物理内存/磁盘中的数据;
- 动态申请内存
printf 会调用malloc 向动态内存分配器申请堆中的内存;
- 信号处理
进程时刻等待着信号,如果运行途中键入ctr-c ctr-z 则调用shell 的信号处理函数分别进行停止、挂起等操作,对于其他信号也有相应的操作;
- 终止并被回收
Shell父进程等待并回收hello子进程,内核删除为hello进程创建的所有数据结构。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
计算机系统的设计与实现是一个复杂而又充满挑战的领域,涉及硬件和软件的密切合作,以及对性能、安全性和可靠性的高要求。在设计和实现计算机系统时,理解硬件和软件之间的相互作用至关重要。通过深入分析和优化系统资源的利用,可以实现显著的性能提升。例如,结合硬件特性和软件算法优化,可以在不同层次上实现更高效的数据处理和存储管理。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello.c 源文件
hello.i 预编译结果
hello.s 编译结果
hello.o 汇编结果
hello.o.txt hello.o反汇编结果
hello 可执行文件
hello.elf hello对应的elf 文件
helloelf.txt hello生成的elf文件对应的文本格式
列出所有的中间产物的文件名,并予以说明起作用。
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 【Linux内核编程】 设备管理与文件IO操作_linux内核如何管理设备资源的-优快云博客
[2] 32|IO管理:Linux如何管理多个外设?-计算机基础实战课-极客时间
[3] Markmap
[4] csapp
hello.c 源文件
hello.i 预编译结果
hello.s 编译结果
hello.o 汇编结果
hello.o.txt hello.o反汇编结果
hello 可执行文件
hello.elf hello对应的elf 文件
helloelf.txt hello生成的elf文件对应的文本格式
(参考文献0分,缺失 -1分)
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 物联网工程
学 号 2022112965
班 级 2237301
学 生 辜彬鹏
指 导 教 师 吴锐
计算机科学与技术学院
2024年5月
摘 要
本论文分析了hello程序在Linux系统下的整个生命周期,依次从编译预处理,编译,汇编,链接,进程管理,存储管理的角度升入展示了hello.c文件的执行过程,并借此对计算机系统中学习到的知识进行了回顾和运用,对linx下的计算机系统有了更加深入的理解感悟。
关键词:hello.c,编译,汇编,链接,存储,进程,计算机系统
(摘要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简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
Hello的P2P是指hello.c文件从可执行程序(Program)变为运行时进程(Process)的过程。在Linux系统下,hello.c 文件依次经过cpp(C Pre-Processor,C预处理器)预处理、ccl(C Compiler,C编译器) 编译、as (Assembler,汇编器)汇编、ld (Linker,链接器)链接最终成为可执行目标程序hello(在Linux下该文件无固定后缀)。打开shell,输入命令./hello后,shell 通过fork产生子进程,hello 便从可执行程序(Program)变成为进程(Process)。
Hello的020是指hello.c文件“From 0 to 0”,初始时内存中并无hello文件的相关内容,这便是“From 0”。通过在Shell下调用execve函数,系统会将hello文件载入内存,执行相关代码,当程序运行结束后, hello进程被回收,并由内核删除hello相关数据,这即为“to 0”。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1.2.1硬件环境
1.2.2 软件环境
Windows10 64位; Vmware 14; Ubuntu20.04
1.2.3 开发工具
Visual Studio Code; vi/vim/gpedit+gcc,泰山处理器
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c 源文件
hello.i 预编译结果
hello.s 编译结果
hello.o 汇编结果
hello.o.txt hello.o反汇编结果
hello 可执行文件
hello.elf hello对应的elf 文件
helloelf.txt hello生成的elf文件对应的文本格式
1.4 本章小结
本章对hello进行了一个总体的概括,首先介绍了P2P、020的意义和过程,介绍了作业中的硬件环境、软件环境和开发工具,最后简述了从.c文件到可执行文件的过程
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理步骤是指程序开始运行时,预处理器(cpp,C Pre-Processor,C预处理器)根据以字符#开头的命令,修改原始的C程序的过程。例如,hello.c文件6到8行中的#include 命令会告诉预处理器读取系统头文件stdio.h,unistd.h,stdlib.h 的内容,并把这些内容直接插入到程序文本中。用实际值替换用#define定义的字符串。除此之外,预处理过程还会删除程序中的注释和多余的空白字符。预处理通常得到另一个以.i作为拓展名的C程序。
预处理步骤是指程序开始运行时,预处理器(cpp,C Pre-Processor,C预处理器)根据以字符#开头的命令,修改原始的C程序的过程。例如,hello.c文件6到8行中的#include 命令会告诉预处理器读取系统头文件stdio.h,unistd.h,stdlib.h 的内容,并把这些内容直接插入到程序文本中。用实际值替换用#define定义的字符串。除此之外,预处理过程还会删除程序中的注释和多余的空白字符。预处理通常得到另一个以.i作为拓展名的C程序。
2.2在Ubuntu下预处理的命令
gcc –E hello.c –o hello.i
2.3 Hello的预处理结果解析
在Linux下打开hello.i文件,可以发现hello.i程序已经拓展为3062行,行数比起hello.c文件大幅增加。其中, hello.c中的main函数相关代码在hello.i程序中对应着3047行到3062行。
2.4 本章小结
本章主要介绍了预处理的概念及作用、并结合Ubuntu系统下hello.c文件实际预处理之后得到的hello.i程序对预处理结果进行了解析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
- 编译的概念
编译是指C编译器ccl通过词法分析和语法分析,将合法指令翻译成等价汇编代码的过程。通过编译过程,编译器将文本文件 hello.i 翻译成汇编语言文件 hello.s,在hello.s中,以文本的形式描述了一条条低级机器语言指令。
- 编译的作用
将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做准备。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
(以下格式自行编排,编辑时删除)
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
z
.file | 源文件 |
.text | 代码段 |
.global | 全局变量 |
.data | 存放已经初始化的全局和静态C 变量 |
.section .rodata | 存放只读变量 |
.align | 对齐方式 |
.type | 表示是函数类型/对象类型 |
.size | 表示大小 |
.long .string | 表示是long类型/string类型 |
3.4 本章小结
本章介绍了编译的概念与作用,编译是将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做准备的过程。同时,本章以hello.s文件为例,介绍了编译器如何处理各个数据类型以及各类操作,验证了大部分数据、操作在汇编代码中的实现。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
- 汇编的概念
汇编是指汇编器(assembler)将以.s结尾的汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在.o 目标文件中的过程
- 汇编的作用
将汇编语言翻译为机器语言,并将相关指令以可重定位目标程序格式保存在.o文件中
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
应截图,展示汇编过程!
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
- ELF 头(ELF Header):
以 16字节序列 Magic 开始,其描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头大小、目标文件类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量等相关信息。
- 节头:
包含了文件中出现的各个节的意义,包括节的类型、位置和大小等信息。
- 重定位节.rela.text
一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
在这里,8 条重定位信息分别是对.L0(第一个 printf 中的字符串)、puts 函数、exit 函数、.L1(第二个 printf 中的字符串)、printf 函数、sleepsecs、sleep 函数、getchar 函数进行重定位声明。
- 重定位节.rela.eh_frame
-
4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
- 分支转移:
在hello.s中,跳转指令的目标地址直接记为段名称,如.L2,.L3等。而在反汇编得到的hello.asm中,跳转的目标为具体的地址,在机器代码中体现为目标指令地址与当前指令下一条指令的地址之差。
- 函数调用:
在hello.s文件中,call之后直接跟着函数名称,而在反汇编得到的hello.asm中,call 的目标地址是当前指令的下一条指令。这是因为 hello.c 中调用的函数都是共享库中的函数,最终需要通过动态链接器作用才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其 call 指令后的相对地址设置为全0(此时,目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接进一步确定。
- 全局变量访问:
在hello.s 文件中,使用段名称+%rip访问 rodata(printf 中的字符串),而在反汇编得到的hello.asm中,使用 0+%rip进行访问。其原因与函数调用类似,rodata 中数据地址在运行时才能确定,故访问时也需要重定位。在汇编成为机器语言时,将操作数设置为全 0 并添加相应的重定位条目。
4.5 本章小结
本章对汇编的概念与作用进行了介绍。以hello.s文件在Linux系统下通过将hello.s文件汇编为hello.o文件,并生成hello.o的ELF格式文件hello.elf为例,研究了ELF格式文件的具体结构。通过比较hello.o的反汇编代码(保存在hello.asm中)与hello.s的异同,令人深刻认识到了从汇编语言到机器语言实现的转变,和在这个过程中机器为链接所做出的准备
(第4章1分)
第5章 链接
5.1 链接的概念与作用
- 链接的概念
链接是指通过链接器(Linker),将程序编码与数据块收集并整理成为一个单一文件,生成完全链接的可执行的目标文件(windows系统下为.exe文件,Linux系统下一般省略后缀名)的过程。
- 链接的作用
提供了一种模块化的方式,可以将程序编写为一个较小的源文件的集合,且实现了分开编译更改源文件,从而减少整体文件的复杂度与大小,增加容错性,也方便对某一模块进行针对性修改注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令
(以下格式自行编排,编辑时删除)
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
根据计算机系统的特性,程序被载入至地址0x400100~0x402000中。在该地址范围内,每个节的地址都与前一节中节对应的 Address 相同。根据edb查看的结果,在地址空间0x400100~0x4001ff中存放着与地址空间0x400100~0x402000相同的程序,在0x401fff之后存放的是.dynamic到.shstrtab节的内容。
5.5 链接的重定位过程分析
(以下格式自行编排,编辑时删除)
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
链接完成过后,动态链接器将共享库中的文件加入到可执行文件种:可见函数数量有所增加,如上截图所示,可以看到新增了puts,printf,getchar,atoi,exit,sleep函数。
通过分析main函数,我们得知,call的参数发生变化,由于在链接过程中,连接器执行了重定位的操作,call的参数地址被修改为目标地址与下一条指令的地址之差。
此外,跳转指令的参数也发生了变化,这也是重定位导致的。
5.6 hello的执行流程
(以下格式自行编排,编辑时删除)
使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。
函数 | 地址 |
main | 0x0000000000401125 |
init | 0x0000000000401000 |
puts | 0x401090 |
exit@plt | 0x4010d0 |
getchar@plt | 0x4010b0 |
printf@plt | 0x4010a0 |
atoi@plt | 0x10c0 |
sleep@plt | 0x4010d0 |
exit | 0x00007ff9a7c455f0 |
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变化。要截图标识说明。
编译器在编译时无法预知函数在内存中的实际地址,因此添加重定位记录以指导动态链接器。动态链接器通过过程链接表(PLT)和全局偏移量表(GOT)在运行时解析函数地址。GOT存储函数的目标地址,而PLT使用GOT中的地址间接跳转到目标函数。动态链接器在加载时解析GOT条目,以确保包含正确的函数地址。这一过程避免了运行时对代码段的直接修改,采用了延迟绑定的策略。
查找共享库:
根据程序中 hello 可执行文件的头部信息,找到需要加载的动态库。
查找这些动态库的路径,通常包括 /lib, /usr/lib, /usr/local/lib 等。
读取程序中指定的共享库名称,并加载这些库到内存中。
链接共享库:
动态链接器将程序的需要链接的库函数和已经加载到内存中的共享库进行链接,建立起正确的调用关系。
5.8 本章小结
本章中介绍了链接的概念与作用,对链接后的函数地址和参数传递的过程进行了分析,对重定位和动态链接的过程有了更直观的展示与理解。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
- 进程的概念
进程是一个正在运行的程序的实例,系统中的每一个程序都运行在某个进程的上下文中。
- 进程的作用
给应用程序提供两个关键抽象:
- 一个独立的逻辑控制流,提供一个假象,好像程序独占地使用处理器
- 一个私有地址空间,提供一个假象,好像程序独占地使用内存系统
6.2 简述壳Shell-bash的作用与处理流程
作用:
Shell 是一个用C语言编写的交互型应用程序,代表用户运行其他程序。Shell 应用程序提供了一个界面,用户可以通过这个界面进行系统的基本操作,访问操作系统内核的服务。
Shell的处理流程大致如下:
- 从Shell终端读入输入的命令。
- 切分输入字符串,获得并识别所有的参数
- 若输入参数为内置命令,则立即执行
- 若输入参数并非内置命令,则调用相应的程序为其分配子进程并运行
- 若输入参数非法,则返回错误信息
- 处理完当前参数后继续处理下一参数,直到处理完毕
- Hello的fork进程创建过程
打开Shell,输入命令./hello 2022112965 gbp 15282396630 3,带参数执行生成的可执行文件。带参执行当前目录下的可执行文件hello,父进程会通过fork函数创建一个新的运行的子进程hello。子进程获取了与父进程的上下文,包括栈、通用寄存器、程序计数器,环境变量和打开的文件相同的一份副本。子进程与父进程的最大区别是有着跟父进程不一样的PID,子进程可以读取父进程打开的任何文件。当子进程运行结束时,父进程如果仍然存在,则执行对子进程的回收,否则就由init进程回收子进程。
6.4 Hello的execve过程
用函数fork创建新的子进程之后,子进程会调用execve函数,在当前进程的上下文中加载并运行一个新程序hello。execve 函数从不返回,它将删除该进程的代码和地址空间内的内容并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行该程序。将私有的区域映射进来,例如打开的文件,代码、数据段,然后将公共的区域映射进来。后面加载器跳转到程序的入口点,即设置PC指向_start 地址。_start函数最终调用hello中的 main 函数,这样,便完成了在子进程中的加载。
6.5 Hello的进程执行
在程序运行时,Shell为hello fork了一个子进程,这个子进程与Shell有独立的逻辑控制流。在hello的运行过程中,若hello进程不被抢占,则正常执行;若被抢占,则进入内核模式,进行上下文切换,转入用户模式,调度其他进程。直到当hello调用sleep函数时,为了最大化利用处理器资源,sleep函数会向内核发送请求将hello挂起,并进行上下文切换,进入内核模式切换到其他进程,切换回用户模式运行抢占的进程。与此同时,将 hello 进程从运行队列加入等待队列,由用户模式变成内核模式,并开始计时。当计时结束时,sleep函数返回,触发一个中断,使得hello进程重新被调度,将其从等待队列中移出,并内核模式转为用户模式。此时 hello 进程就可以继续执行其逻辑控制流。
用户态与核心态转换
在执行过程中,操作系统会将 CPU 的运行模式切换为不同的特权级别,通常有用户态和核心态两种模式:
用户态(User Mode):进程在这个模式下执行,只能访问自己的内存空间,无法直接访问硬件或操作系统核心功能。
核心态(Kernel Mode):操作系统和核心模块在这个模式下执行,可以访问所有硬件和系统资源。
进程在执行系统调用(如读取文件、分配内存等)时会触发用户态到核心态的切换,操作系统会暂时提升进程的特权级别来执行必要的操作,然后再切换回用户态。
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结
- 首先运行hello程序,根据程序提醒输入,可见程序以每3s一次输出学生信息,此时键盘乱按,随机输入,程序依旧正常运行
- 当按下ctrl Z 时,进程收到SIGSTP信号,hello停止,但是进程并未回收,此时通过ps指令查看pid信息,再使用fg指令将其调回前台继续运行。
3.当输入ctrl C,进程收到SIGINT信号时,程序终止,查询不到此进程的pid
4.当输入kill指令,停止的命令被终止,无法查询pid
- pstree
-
截屏,说明异常与信号的处理。
6.7本章小结
本章介绍了进程的概念与作用。关于进程,在这一章中根据hello可执行文件的具体示例研究了fork,execve的原理与执行过程,展示了hello在执行过程中各种异常与信号的处理结果。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
- 逻辑地址
逻辑地址是指由程序产生的与段相关的偏移地址部分,逻辑地址由选择符和偏移量两部分组成。具体而言,其为hello.asm中的相对偏移地址。
- 线性地址
逻辑地址经过段机制转化后为线性地址,其为处理器可寻址空间的地址,用于描述程序分页信息的地址。具体以hello而言,线性地址标志着 hello 应在内存上哪些具体数据块上运行。
- 虚拟地址
根据CSAPP教材,虚拟地址即为上述线性地址。
- 物理地址
CPU通过地址总线的寻址,找到真实的物理内存对应地址。
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
Intel处理器从逻辑地址到线性地址的变换通过段式管理的方式实现。每个程序在系统中都保存着一个段表,段表保存着该程序各段装入主存的状况信息,包括段号或段名、段起点、装入位、段的长度、主存占用区域表、主存可用区域表等,从而方便进行段式管理。
在段寄存器中,存放着段选择符,可以通过段选择符来得到对应段首地址。段选择符的结构如下:
图 48 段选择符的情况
其包含三部分:索引,TI,RPL
索引:用来确定当前使用的段描述符在描述符表中的位置;
TI:根据TI的值判断选择全局描述符表(TI=0,GDT)或选择局部描述符表(TI=1,LDT);
RPL:判断重要等级。RPL=00,为第0级,位于最高级的内核,RPL=11,为第3级,位于最低级的用户状态;
通过一个索引,可以定位到段描述符,进而通过段描述符得到段基址。段基址与偏移量结合就得到了线性地址,虚拟地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(VA)到物理地址(PA)之间的转换通过对虚拟地址内存空间进行分页的分页机制完成。
通过7.2节中的段式管理过程,可以得到了线性地址/虚拟地址,记为VA。虚拟地址可被分为两个部分:VPN(虚拟页号)和VPO(虚拟页偏移量),根据计算机系统的特性可以确定VPN与VPO的具体位数,由于虚拟内存与物理内存的页大小相同,因此VPO与PPO(物理页偏移量)一致。而PPN(物理页号)则需通过访问页表中的页表条目(PTE)获取,如下图所示。
若PTE的有效位为1,则发生页命中,可以直接获取到物理页号PPN,PPN与PPO共同组成物理地址。
若PTE的有效位为0,说明对应虚拟页没有缓存到物理内存中,产生缺页故障,调用操作系统的内核的缺页处理程序,确定牺牲页,并调入新的页面。再返回到原来的进程,再次调用导致缺页的指令。此时发生页命中,获取到PPN,与PPO共同组成物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是一个硬件缓存,用于加速虚拟地址到物理地址的转换。在使用虚拟内存的系统中,程序生成的内存访问地址是虚拟地址(VA),而物理内存中存储数据的地址是物理地址(PA)。CPU在访问内存时,需要将虚拟地址转换为物理地址才能正确访问内存中的数据。
TLB的作用是存储最近使用过的一些虚拟地址到物理地址的映射,避免每次访存都要去查询更慢的页表。它类似于高速缓存,存储了虚拟地址的一部分(通常是页面号)到物理地址的映射关系。当CPU需要访问内存时,首先在TLB中查找虚拟地址的物理地址映射,如果命中(即找到了对应的映射),则可以直接使用这个映射来访问物理内存;如果未命中,则需要从页表中获取映射,并将其加入TLB以供将来使用。
四级页表(或多级页表)
多级页表是操作系统用来管理虚拟内存和物理内存映射的数据结构。传统的 x86 体系结构通常采用两级页表(页目录表 + 页表),而现代系统(如 x86-64)则引入了更多级别的页表来支持更大的虚拟地址空间和更高效的页表管理。一般来说,页表级别越多,能管理的虚拟地址空间就越大,但页表的深度也会增加访问时间。
转换过程如图:
CPU产生虚拟地址VA,并将其传送至MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)在TLB中进行匹配,若命中,则得到PPN(40bit)与VPO(12bit)组合成物理地址PA(52bit)。若TLB没有命中,则MMU向页表中查询,由CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,则执行下一步确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成PA,并向TLB中添加条目。
多级页表的工作原理展示如下:
7.5 三级Cache支持下的物理内存访问
CPU执行指令:当CPU需要执行一个指令或访问内存中的数据时,首先检查L1 Cache。
L1 Cache访问:如果所需数据在L1 Cache中命中(即找到了),则称为L1 Cache命中(L1 Cache Hit)。此时,CPU可以直接从L1 Cache中获取数据,这是访问速度最快的情况。
L1 Cache未命中:如果所需数据不在L1 Cache中(即L1 Cache未命中,L1 Cache Miss),则CPU会将访问请求传递给L2 Cache。
L2 Cache访问:L2 Cache会在自己的缓存中查找所需数据。如果在L2 Cache中找到(L2 Cache Hit),则称为L2 Cache命中,并将数据返回给CPU。
L2 Cache未命中:如果在L2 Cache中未找到所需数据(L2 Cache Miss),那么CPU会进一步将访问请求传递给L3 Cache。
L3 Cache访问:L3 Cache会尝试在自己的缓存中查找所需数据。如果在L3 Cache中找到(L3 Cache Hit),则称为L3 Cache命中,并将数据返回给CPU。
L3 Cache未命中:如果在L3 Cache中也未找到所需数据(L3 Cache Miss),那么CPU将最终将访问请求传递给主内存(RAM)。
访问主内存:在主内存中查找所需数据,主内存的访问速度相对较慢,通常需要数百个时钟周期才能返回数据。
7.6 hello进程fork时的内存映射
当fork函数被当前进程hello调用时,内核为新进程hello创建各种数据结构,并分配给它一个唯一的PID。为了给这个新的hello创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当着两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数加载并运行hello需要以下几个步骤:
- 删除已存在的用户区域
删除当前进程hello虚拟地址的用户部分中的已存在的区域结构。
- 映射私有区域
为新程序的代码、数据、bss和栈区域创建新的私有的、写时复制的区域结构。其中,代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
- 映射共享区域
若hello程序与共享对象或目标(如标准C库libc.so)链接,则将这些对象动态链接到hello程序,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置程序计数器
最后,execve设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
发生一个缺页异常后,控制会转移到内核的缺页处理程序。判断虚拟地址是否合法,若不合法,则产生一个段错误,然后终止这个进程。
若操作合法,则缺页处理程序从物理内存中确定一个牺牲页,若该牺牲页被修改过,则将它换出到磁盘,换入新的页面并更新页表。当缺页处理程序返回时,CPU 再次执行引起缺页的指令,将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,主存将所请求字返回给处理器。
7.9动态存储分配管理
动态内存管理的基本方法与策略介绍如下:
动态内存分配器维护着一个称为堆的进程的虚拟内存区域。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放可以由应用程序显式执行或内存分配器自身隐式执行。
具体而言,分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
下面介绍动态存储分配管理中较为重要的概念:
- 隐式链表
堆中的空闲块通过头部中的大小字段隐含地连接,分配器通过遍历堆中所有的块,从而间接遍历整个空闲块的集合。
对于隐式链表,其结构如下:
- 显式链表
在每个空闲块中,都包含一个前驱(pred)与后继(succ)指针,从而减少了搜索与适配的时间。
显式链表的结构如下:
- 带边界标记的合并
采取使用边界标记的堆块的格式,在堆块的末尾为其添加一个脚部,其为头部的副本。添加脚部之后,分配器就可以通过检查前面一个块的脚部,判断前面一个块的起始位置和状态。从而实现快速合并,减小性能消耗。
- 分离存储
维护多个空闲链表,其中,每个链表的块具有相同的大小。将所有可能的块大小分成一些等价类,从而进行分离存储。
7.10本章小结
本章主要介绍了存储器地址空间、段式管理、页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得所有的输入和输出都能以一种统一且一致的方式来执行:打开文件、改变当前的文件位置、读写文件、关闭文件。
8.2 简述Unix IO接口及其函数
- 打开文件
一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。对于Shell创建的每个进程,其都有三个打开的文件:标准输入,标准输出,标准错误。
- 改变当前的文件位置
对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
- 读写文件
一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
- 关闭文件
内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
- Unix I/O函数:
- int open(char* filename,int flags,mode_t mode)
进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
- int close(fd)
fd是需要关闭的文件的描述符,close返回操作结果。
- ssize_t read(int fd,void *buf,size_t n)
read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
- ssize_t wirte(int fd,const void *buf,size_t n)
write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
查看windows系统下的printf函数体:
形参列表中的…是可变形参的一种写法,当传递参数的个数不确定时,用这种方式来表示。
va_list的定义:typedef char *va_list,说明它是一个字符指针,其中 (char*)(&fmt) + 4) 即arg表示的是...中的第一个参数。
再进一步查看windows系统下的vsprintf函数体:
则知道vsprintf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。
在printf中调用系统函数write(buf,i)将长度为i的buf输出。write函数如下:
printf函数的功能为接受一个格式化命令,并按指定的匹配的参数格式化输出,故i = vsprintf(buf, fmt, arg)是得到打印出来的字符串长度,其后的write(buf, i)是将buf中的i个元素写到终端。
因此,vsprintf的作用为接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,进而产生格式化输出。
再进一步对write进行追踪:
这里给几个寄存器传递了参数,然后以一个int INT_VECTOR_SYS_CALL结束。INT_VECTOR_SYS_CALL代表通过系统调用syscall,查看syscall的实现:
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码,符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
getchar调用系统函数read,发送一个中断信号,内核抢占这个进程,用户输入字符串,键入回车后(字符串和回车都保存在缓冲区内),再次发送信号,内核重新调度这个进程,getchar从缓冲区读入字符。
8.5本章小结
本章主要介绍了linux的IO设备管理方法和及其接口和函数,对printf函数和getchar函数的底层实现有了基本了解(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
- 预处理
将hello.c中include的所有外部的头文件头文件内容直接插入程序文本中,完成字符串的替换,方便后续处理;
- 编译
通过词法分析和语法分析,将合法指令翻译成等价汇编代码。通过编译过程,编译器将hello.i 翻译成汇编语言文件 hello.s;
- 汇编
将hello.s汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在hello.o 目标文件中;
- 链接
通过链接器,将hello的程序编码与动态链接库等收集整理成为一个单一文件,生成完全链接的可执行的目标文件hello;
- 加载运行
打开Shell,在其中键入 ./hello 1190200208 李旻翀,终端为其fork新建进程,并通过execve把代码和数据加载入虚拟内存空间,程序开始执行;
- 执行指令
在该进程被调度时,CPU为hello其分配时间片,在一个时间片中,hello享有CPU全部资源,PC寄存器一步一步地更新,CPU不断地取指,顺序执行自己的控制逻辑流;
- 访存
内存管理单元MMU将逻辑地址,一步步映射成物理地址,进而通过三级高速缓存系统访问物理内存/磁盘中的数据;
- 动态申请内存
printf 会调用malloc 向动态内存分配器申请堆中的内存;
- 信号处理
进程时刻等待着信号,如果运行途中键入ctr-c ctr-z 则调用shell 的信号处理函数分别进行停止、挂起等操作,对于其他信号也有相应的操作;
- 终止并被回收
Shell父进程等待并回收hello子进程,内核删除为hello进程创建的所有数据结构。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
计算机系统的设计与实现是一个复杂而又充满挑战的领域,涉及硬件和软件的密切合作,以及对性能、安全性和可靠性的高要求。在设计和实现计算机系统时,理解硬件和软件之间的相互作用至关重要。通过深入分析和优化系统资源的利用,可以实现显著的性能提升。例如,结合硬件特性和软件算法优化,可以在不同层次上实现更高效的数据处理和存储管理。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello.c 源文件
hello.i 预编译结果
hello.s 编译结果
hello.o 汇编结果
hello.o.txt hello.o反汇编结果
hello 可执行文件
hello.elf hello对应的elf 文件
helloelf.txt hello生成的elf文件对应的文本格式
列出所有的中间产物的文件名,并予以说明起作用。
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 【Linux内核编程】 设备管理与文件IO操作_linux内核如何管理设备资源的-优快云博客
[2] 32|IO管理:Linux如何管理多个外设?-计算机基础实战课-极客时间
[3] Markmap
[4] csapp
hello.c 源文件
hello.i 预编译结果
hello.s 编译结果
hello.o 汇编结果
hello.o.txt hello.o反汇编结果
hello 可执行文件
hello.elf hello对应的elf 文件
helloelf.txt hello生成的elf文件对应的文本格式
(参考文献0分,缺失 -1分)
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 物联网工程
学 号 2022112965
班 级 2237301
学 生 辜彬鹏
指 导 教 师 吴锐
计算机科学与技术学院
2024年5月
摘 要
本论文分析了hello程序在Linux系统下的整个生命周期,依次从编译预处理,编译,汇编,链接,进程管理,存储管理的角度升入展示了hello.c文件的执行过程,并借此对计算机系统中学习到的知识进行了回顾和运用,对linx下的计算机系统有了更加深入的理解感悟。
关键词:hello.c,编译,汇编,链接,存储,进程,计算机系统
(摘要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简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
Hello的P2P是指hello.c文件从可执行程序(Program)变为运行时进程(Process)的过程。在Linux系统下,hello.c 文件依次经过cpp(C Pre-Processor,C预处理器)预处理、ccl(C Compiler,C编译器) 编译、as (Assembler,汇编器)汇编、ld (Linker,链接器)链接最终成为可执行目标程序hello(在Linux下该文件无固定后缀)。打开shell,输入命令./hello后,shell 通过fork产生子进程,hello 便从可执行程序(Program)变成为进程(Process)。
Hello的020是指hello.c文件“From 0 to 0”,初始时内存中并无hello文件的相关内容,这便是“From 0”。通过在Shell下调用execve函数,系统会将hello文件载入内存,执行相关代码,当程序运行结束后, hello进程被回收,并由内核删除hello相关数据,这即为“to 0”。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1.2.1硬件环境
1.2.2 软件环境
Windows10 64位; Vmware 14; Ubuntu20.04
1.2.3 开发工具
Visual Studio Code; vi/vim/gpedit+gcc,泰山处理器
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c 源文件
hello.i 预编译结果
hello.s 编译结果
hello.o 汇编结果
hello.o.txt hello.o反汇编结果
hello 可执行文件
hello.elf hello对应的elf 文件
helloelf.txt hello生成的elf文件对应的文本格式
1.4 本章小结
本章对hello进行了一个总体的概括,首先介绍了P2P、020的意义和过程,介绍了作业中的硬件环境、软件环境和开发工具,最后简述了从.c文件到可执行文件的过程
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理步骤是指程序开始运行时,预处理器(cpp,C Pre-Processor,C预处理器)根据以字符#开头的命令,修改原始的C程序的过程。例如,hello.c文件6到8行中的#include 命令会告诉预处理器读取系统头文件stdio.h,unistd.h,stdlib.h 的内容,并把这些内容直接插入到程序文本中。用实际值替换用#define定义的字符串。除此之外,预处理过程还会删除程序中的注释和多余的空白字符。预处理通常得到另一个以.i作为拓展名的C程序。
预处理步骤是指程序开始运行时,预处理器(cpp,C Pre-Processor,C预处理器)根据以字符#开头的命令,修改原始的C程序的过程。例如,hello.c文件6到8行中的#include 命令会告诉预处理器读取系统头文件stdio.h,unistd.h,stdlib.h 的内容,并把这些内容直接插入到程序文本中。用实际值替换用#define定义的字符串。除此之外,预处理过程还会删除程序中的注释和多余的空白字符。预处理通常得到另一个以.i作为拓展名的C程序。
2.2在Ubuntu下预处理的命令
gcc –E hello.c –o hello.i
2.3 Hello的预处理结果解析
在Linux下打开hello.i文件,可以发现hello.i程序已经拓展为3062行,行数比起hello.c文件大幅增加。其中, hello.c中的main函数相关代码在hello.i程序中对应着3047行到3062行。
2.4 本章小结
本章主要介绍了预处理的概念及作用、并结合Ubuntu系统下hello.c文件实际预处理之后得到的hello.i程序对预处理结果进行了解析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
- 编译的概念
编译是指C编译器ccl通过词法分析和语法分析,将合法指令翻译成等价汇编代码的过程。通过编译过程,编译器将文本文件 hello.i 翻译成汇编语言文件 hello.s,在hello.s中,以文本的形式描述了一条条低级机器语言指令。
- 编译的作用
将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做准备。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
(以下格式自行编排,编辑时删除)
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
z
.file | 源文件 |
.text | 代码段 |
.global | 全局变量 |
.data | 存放已经初始化的全局和静态C 变量 |
.section .rodata | 存放只读变量 |
.align | 对齐方式 |
.type | 表示是函数类型/对象类型 |
.size | 表示大小 |
.long .string | 表示是long类型/string类型 |
3.4 本章小结
本章介绍了编译的概念与作用,编译是将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做准备的过程。同时,本章以hello.s文件为例,介绍了编译器如何处理各个数据类型以及各类操作,验证了大部分数据、操作在汇编代码中的实现。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
- 汇编的概念
汇编是指汇编器(assembler)将以.s结尾的汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在.o 目标文件中的过程
- 汇编的作用
将汇编语言翻译为机器语言,并将相关指令以可重定位目标程序格式保存在.o文件中
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
应截图,展示汇编过程!
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
- ELF 头(ELF Header):
以 16字节序列 Magic 开始,其描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头大小、目标文件类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量等相关信息。
- 节头:
包含了文件中出现的各个节的意义,包括节的类型、位置和大小等信息。
- 重定位节.rela.text
一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
在这里,8 条重定位信息分别是对.L0(第一个 printf 中的字符串)、puts 函数、exit 函数、.L1(第二个 printf 中的字符串)、printf 函数、sleepsecs、sleep 函数、getchar 函数进行重定位声明。
- 重定位节.rela.eh_frame
-
4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
- 分支转移:
在hello.s中,跳转指令的目标地址直接记为段名称,如.L2,.L3等。而在反汇编得到的hello.asm中,跳转的目标为具体的地址,在机器代码中体现为目标指令地址与当前指令下一条指令的地址之差。
- 函数调用:
在hello.s文件中,call之后直接跟着函数名称,而在反汇编得到的hello.asm中,call 的目标地址是当前指令的下一条指令。这是因为 hello.c 中调用的函数都是共享库中的函数,最终需要通过动态链接器作用才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其 call 指令后的相对地址设置为全0(此时,目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接进一步确定。
- 全局变量访问:
在hello.s 文件中,使用段名称+%rip访问 rodata(printf 中的字符串),而在反汇编得到的hello.asm中,使用 0+%rip进行访问。其原因与函数调用类似,rodata 中数据地址在运行时才能确定,故访问时也需要重定位。在汇编成为机器语言时,将操作数设置为全 0 并添加相应的重定位条目。
4.5 本章小结
本章对汇编的概念与作用进行了介绍。以hello.s文件在Linux系统下通过将hello.s文件汇编为hello.o文件,并生成hello.o的ELF格式文件hello.elf为例,研究了ELF格式文件的具体结构。通过比较hello.o的反汇编代码(保存在hello.asm中)与hello.s的异同,令人深刻认识到了从汇编语言到机器语言实现的转变,和在这个过程中机器为链接所做出的准备
(第4章1分)
第5章 链接
5.1 链接的概念与作用
- 链接的概念
链接是指通过链接器(Linker),将程序编码与数据块收集并整理成为一个单一文件,生成完全链接的可执行的目标文件(windows系统下为.exe文件,Linux系统下一般省略后缀名)的过程。
- 链接的作用
提供了一种模块化的方式,可以将程序编写为一个较小的源文件的集合,且实现了分开编译更改源文件,从而减少整体文件的复杂度与大小,增加容错性,也方便对某一模块进行针对性修改注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令
(以下格式自行编排,编辑时删除)
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
根据计算机系统的特性,程序被载入至地址0x400100~0x402000中。在该地址范围内,每个节的地址都与前一节中节对应的 Address 相同。根据edb查看的结果,在地址空间0x400100~0x4001ff中存放着与地址空间0x400100~0x402000相同的程序,在0x401fff之后存放的是.dynamic到.shstrtab节的内容。
5.5 链接的重定位过程分析
(以下格式自行编排,编辑时删除)
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
链接完成过后,动态链接器将共享库中的文件加入到可执行文件种:可见函数数量有所增加,如上截图所示,可以看到新增了puts,printf,getchar,atoi,exit,sleep函数。
通过分析main函数,我们得知,call的参数发生变化,由于在链接过程中,连接器执行了重定位的操作,call的参数地址被修改为目标地址与下一条指令的地址之差。
此外,跳转指令的参数也发生了变化,这也是重定位导致的。
5.6 hello的执行流程
(以下格式自行编排,编辑时删除)
使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。
函数 | 地址 |
main | 0x0000000000401125 |
init | 0x0000000000401000 |
puts | 0x401090 |
exit@plt | 0x4010d0 |
getchar@plt | 0x4010b0 |
printf@plt | 0x4010a0 |
atoi@plt | 0x10c0 |
sleep@plt | 0x4010d0 |
exit | 0x00007ff9a7c455f0 |
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变化。要截图标识说明。
编译器在编译时无法预知函数在内存中的实际地址,因此添加重定位记录以指导动态链接器。动态链接器通过过程链接表(PLT)和全局偏移量表(GOT)在运行时解析函数地址。GOT存储函数的目标地址,而PLT使用GOT中的地址间接跳转到目标函数。动态链接器在加载时解析GOT条目,以确保包含正确的函数地址。这一过程避免了运行时对代码段的直接修改,采用了延迟绑定的策略。
查找共享库:
根据程序中 hello 可执行文件的头部信息,找到需要加载的动态库。
查找这些动态库的路径,通常包括 /lib, /usr/lib, /usr/local/lib 等。
读取程序中指定的共享库名称,并加载这些库到内存中。
链接共享库:
动态链接器将程序的需要链接的库函数和已经加载到内存中的共享库进行链接,建立起正确的调用关系。
5.8 本章小结
本章中介绍了链接的概念与作用,对链接后的函数地址和参数传递的过程进行了分析,对重定位和动态链接的过程有了更直观的展示与理解。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
- 进程的概念
进程是一个正在运行的程序的实例,系统中的每一个程序都运行在某个进程的上下文中。
- 进程的作用
给应用程序提供两个关键抽象:
- 一个独立的逻辑控制流,提供一个假象,好像程序独占地使用处理器
- 一个私有地址空间,提供一个假象,好像程序独占地使用内存系统
6.2 简述壳Shell-bash的作用与处理流程
作用:
Shell 是一个用C语言编写的交互型应用程序,代表用户运行其他程序。Shell 应用程序提供了一个界面,用户可以通过这个界面进行系统的基本操作,访问操作系统内核的服务。
Shell的处理流程大致如下:
- 从Shell终端读入输入的命令。
- 切分输入字符串,获得并识别所有的参数
- 若输入参数为内置命令,则立即执行
- 若输入参数并非内置命令,则调用相应的程序为其分配子进程并运行
- 若输入参数非法,则返回错误信息
- 处理完当前参数后继续处理下一参数,直到处理完毕
- Hello的fork进程创建过程
打开Shell,输入命令./hello 2022112965 gbp 15282396630 3,带参数执行生成的可执行文件。带参执行当前目录下的可执行文件hello,父进程会通过fork函数创建一个新的运行的子进程hello。子进程获取了与父进程的上下文,包括栈、通用寄存器、程序计数器,环境变量和打开的文件相同的一份副本。子进程与父进程的最大区别是有着跟父进程不一样的PID,子进程可以读取父进程打开的任何文件。当子进程运行结束时,父进程如果仍然存在,则执行对子进程的回收,否则就由init进程回收子进程。
6.4 Hello的execve过程
用函数fork创建新的子进程之后,子进程会调用execve函数,在当前进程的上下文中加载并运行一个新程序hello。execve 函数从不返回,它将删除该进程的代码和地址空间内的内容并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行该程序。将私有的区域映射进来,例如打开的文件,代码、数据段,然后将公共的区域映射进来。后面加载器跳转到程序的入口点,即设置PC指向_start 地址。_start函数最终调用hello中的 main 函数,这样,便完成了在子进程中的加载。
6.5 Hello的进程执行
在程序运行时,Shell为hello fork了一个子进程,这个子进程与Shell有独立的逻辑控制流。在hello的运行过程中,若hello进程不被抢占,则正常执行;若被抢占,则进入内核模式,进行上下文切换,转入用户模式,调度其他进程。直到当hello调用sleep函数时,为了最大化利用处理器资源,sleep函数会向内核发送请求将hello挂起,并进行上下文切换,进入内核模式切换到其他进程,切换回用户模式运行抢占的进程。与此同时,将 hello 进程从运行队列加入等待队列,由用户模式变成内核模式,并开始计时。当计时结束时,sleep函数返回,触发一个中断,使得hello进程重新被调度,将其从等待队列中移出,并内核模式转为用户模式。此时 hello 进程就可以继续执行其逻辑控制流。
用户态与核心态转换
在执行过程中,操作系统会将 CPU 的运行模式切换为不同的特权级别,通常有用户态和核心态两种模式:
用户态(User Mode):进程在这个模式下执行,只能访问自己的内存空间,无法直接访问硬件或操作系统核心功能。
核心态(Kernel Mode):操作系统和核心模块在这个模式下执行,可以访问所有硬件和系统资源。
进程在执行系统调用(如读取文件、分配内存等)时会触发用户态到核心态的切换,操作系统会暂时提升进程的特权级别来执行必要的操作,然后再切换回用户态。
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结
- 首先运行hello程序,根据程序提醒输入,可见程序以每3s一次输出学生信息,此时键盘乱按,随机输入,程序依旧正常运行
- 当按下ctrl Z 时,进程收到SIGSTP信号,hello停止,但是进程并未回收,此时通过ps指令查看pid信息,再使用fg指令将其调回前台继续运行。
3.当输入ctrl C,进程收到SIGINT信号时,程序终止,查询不到此进程的pid
4.当输入kill指令,停止的命令被终止,无法查询pid
- pstree
-
截屏,说明异常与信号的处理。
6.7本章小结
本章介绍了进程的概念与作用。关于进程,在这一章中根据hello可执行文件的具体示例研究了fork,execve的原理与执行过程,展示了hello在执行过程中各种异常与信号的处理结果。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
- 逻辑地址
逻辑地址是指由程序产生的与段相关的偏移地址部分,逻辑地址由选择符和偏移量两部分组成。具体而言,其为hello.asm中的相对偏移地址。
- 线性地址
逻辑地址经过段机制转化后为线性地址,其为处理器可寻址空间的地址,用于描述程序分页信息的地址。具体以hello而言,线性地址标志着 hello 应在内存上哪些具体数据块上运行。
- 虚拟地址
根据CSAPP教材,虚拟地址即为上述线性地址。
- 物理地址
CPU通过地址总线的寻址,找到真实的物理内存对应地址。
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
Intel处理器从逻辑地址到线性地址的变换通过段式管理的方式实现。每个程序在系统中都保存着一个段表,段表保存着该程序各段装入主存的状况信息,包括段号或段名、段起点、装入位、段的长度、主存占用区域表、主存可用区域表等,从而方便进行段式管理。
在段寄存器中,存放着段选择符,可以通过段选择符来得到对应段首地址。段选择符的结构如下:
图 48 段选择符的情况
其包含三部分:索引,TI,RPL
索引:用来确定当前使用的段描述符在描述符表中的位置;
TI:根据TI的值判断选择全局描述符表(TI=0,GDT)或选择局部描述符表(TI=1,LDT);
RPL:判断重要等级。RPL=00,为第0级,位于最高级的内核,RPL=11,为第3级,位于最低级的用户状态;
通过一个索引,可以定位到段描述符,进而通过段描述符得到段基址。段基址与偏移量结合就得到了线性地址,虚拟地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(VA)到物理地址(PA)之间的转换通过对虚拟地址内存空间进行分页的分页机制完成。
通过7.2节中的段式管理过程,可以得到了线性地址/虚拟地址,记为VA。虚拟地址可被分为两个部分:VPN(虚拟页号)和VPO(虚拟页偏移量),根据计算机系统的特性可以确定VPN与VPO的具体位数,由于虚拟内存与物理内存的页大小相同,因此VPO与PPO(物理页偏移量)一致。而PPN(物理页号)则需通过访问页表中的页表条目(PTE)获取,如下图所示。
若PTE的有效位为1,则发生页命中,可以直接获取到物理页号PPN,PPN与PPO共同组成物理地址。
若PTE的有效位为0,说明对应虚拟页没有缓存到物理内存中,产生缺页故障,调用操作系统的内核的缺页处理程序,确定牺牲页,并调入新的页面。再返回到原来的进程,再次调用导致缺页的指令。此时发生页命中,获取到PPN,与PPO共同组成物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是一个硬件缓存,用于加速虚拟地址到物理地址的转换。在使用虚拟内存的系统中,程序生成的内存访问地址是虚拟地址(VA),而物理内存中存储数据的地址是物理地址(PA)。CPU在访问内存时,需要将虚拟地址转换为物理地址才能正确访问内存中的数据。
TLB的作用是存储最近使用过的一些虚拟地址到物理地址的映射,避免每次访存都要去查询更慢的页表。它类似于高速缓存,存储了虚拟地址的一部分(通常是页面号)到物理地址的映射关系。当CPU需要访问内存时,首先在TLB中查找虚拟地址的物理地址映射,如果命中(即找到了对应的映射),则可以直接使用这个映射来访问物理内存;如果未命中,则需要从页表中获取映射,并将其加入TLB以供将来使用。
四级页表(或多级页表)
多级页表是操作系统用来管理虚拟内存和物理内存映射的数据结构。传统的 x86 体系结构通常采用两级页表(页目录表 + 页表),而现代系统(如 x86-64)则引入了更多级别的页表来支持更大的虚拟地址空间和更高效的页表管理。一般来说,页表级别越多,能管理的虚拟地址空间就越大,但页表的深度也会增加访问时间。
转换过程如图:
CPU产生虚拟地址VA,并将其传送至MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)在TLB中进行匹配,若命中,则得到PPN(40bit)与VPO(12bit)组合成物理地址PA(52bit)。若TLB没有命中,则MMU向页表中查询,由CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,则执行下一步确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成PA,并向TLB中添加条目。
多级页表的工作原理展示如下:
7.5 三级Cache支持下的物理内存访问
CPU执行指令:当CPU需要执行一个指令或访问内存中的数据时,首先检查L1 Cache。
L1 Cache访问:如果所需数据在L1 Cache中命中(即找到了),则称为L1 Cache命中(L1 Cache Hit)。此时,CPU可以直接从L1 Cache中获取数据,这是访问速度最快的情况。
L1 Cache未命中:如果所需数据不在L1 Cache中(即L1 Cache未命中,L1 Cache Miss),则CPU会将访问请求传递给L2 Cache。
L2 Cache访问:L2 Cache会在自己的缓存中查找所需数据。如果在L2 Cache中找到(L2 Cache Hit),则称为L2 Cache命中,并将数据返回给CPU。
L2 Cache未命中:如果在L2 Cache中未找到所需数据(L2 Cache Miss),那么CPU会进一步将访问请求传递给L3 Cache。
L3 Cache访问:L3 Cache会尝试在自己的缓存中查找所需数据。如果在L3 Cache中找到(L3 Cache Hit),则称为L3 Cache命中,并将数据返回给CPU。
L3 Cache未命中:如果在L3 Cache中也未找到所需数据(L3 Cache Miss),那么CPU将最终将访问请求传递给主内存(RAM)。
访问主内存:在主内存中查找所需数据,主内存的访问速度相对较慢,通常需要数百个时钟周期才能返回数据。
7.6 hello进程fork时的内存映射
当fork函数被当前进程hello调用时,内核为新进程hello创建各种数据结构,并分配给它一个唯一的PID。为了给这个新的hello创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当着两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数加载并运行hello需要以下几个步骤:
- 删除已存在的用户区域
删除当前进程hello虚拟地址的用户部分中的已存在的区域结构。
- 映射私有区域
为新程序的代码、数据、bss和栈区域创建新的私有的、写时复制的区域结构。其中,代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
- 映射共享区域
若hello程序与共享对象或目标(如标准C库libc.so)链接,则将这些对象动态链接到hello程序,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置程序计数器
最后,execve设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
发生一个缺页异常后,控制会转移到内核的缺页处理程序。判断虚拟地址是否合法,若不合法,则产生一个段错误,然后终止这个进程。
若操作合法,则缺页处理程序从物理内存中确定一个牺牲页,若该牺牲页被修改过,则将它换出到磁盘,换入新的页面并更新页表。当缺页处理程序返回时,CPU 再次执行引起缺页的指令,将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,主存将所请求字返回给处理器。
7.9动态存储分配管理
动态内存管理的基本方法与策略介绍如下:
动态内存分配器维护着一个称为堆的进程的虚拟内存区域。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放可以由应用程序显式执行或内存分配器自身隐式执行。
具体而言,分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
下面介绍动态存储分配管理中较为重要的概念:
- 隐式链表
堆中的空闲块通过头部中的大小字段隐含地连接,分配器通过遍历堆中所有的块,从而间接遍历整个空闲块的集合。
对于隐式链表,其结构如下:
- 显式链表
在每个空闲块中,都包含一个前驱(pred)与后继(succ)指针,从而减少了搜索与适配的时间。
显式链表的结构如下:
- 带边界标记的合并
采取使用边界标记的堆块的格式,在堆块的末尾为其添加一个脚部,其为头部的副本。添加脚部之后,分配器就可以通过检查前面一个块的脚部,判断前面一个块的起始位置和状态。从而实现快速合并,减小性能消耗。
- 分离存储
维护多个空闲链表,其中,每个链表的块具有相同的大小。将所有可能的块大小分成一些等价类,从而进行分离存储。
7.10本章小结
本章主要介绍了存储器地址空间、段式管理、页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得所有的输入和输出都能以一种统一且一致的方式来执行:打开文件、改变当前的文件位置、读写文件、关闭文件。
8.2 简述Unix IO接口及其函数
- 打开文件
一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。对于Shell创建的每个进程,其都有三个打开的文件:标准输入,标准输出,标准错误。
- 改变当前的文件位置
对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
- 读写文件
一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
- 关闭文件
内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
- Unix I/O函数:
- int open(char* filename,int flags,mode_t mode)
进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
- int close(fd)
fd是需要关闭的文件的描述符,close返回操作结果。
- ssize_t read(int fd,void *buf,size_t n)
read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
- ssize_t wirte(int fd,const void *buf,size_t n)
write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
查看windows系统下的printf函数体:
形参列表中的…是可变形参的一种写法,当传递参数的个数不确定时,用这种方式来表示。
va_list的定义:typedef char *va_list,说明它是一个字符指针,其中 (char*)(&fmt) + 4) 即arg表示的是...中的第一个参数。
再进一步查看windows系统下的vsprintf函数体:
则知道vsprintf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。
在printf中调用系统函数write(buf,i)将长度为i的buf输出。write函数如下:
printf函数的功能为接受一个格式化命令,并按指定的匹配的参数格式化输出,故i = vsprintf(buf, fmt, arg)是得到打印出来的字符串长度,其后的write(buf, i)是将buf中的i个元素写到终端。
因此,vsprintf的作用为接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,进而产生格式化输出。
再进一步对write进行追踪:
这里给几个寄存器传递了参数,然后以一个int INT_VECTOR_SYS_CALL结束。INT_VECTOR_SYS_CALL代表通过系统调用syscall,查看syscall的实现:
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码,符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
getchar调用系统函数read,发送一个中断信号,内核抢占这个进程,用户输入字符串,键入回车后(字符串和回车都保存在缓冲区内),再次发送信号,内核重新调度这个进程,getchar从缓冲区读入字符。
8.5本章小结
本章主要介绍了linux的IO设备管理方法和及其接口和函数,对printf函数和getchar函数的底层实现有了基本了解
结论
用计算机系统的语言,逐条总结hello所经历的过程。
- 预处理
将hello.c中include的所有外部的头文件头文件内容直接插入程序文本中,完成字符串的替换,方便后续处理;
- 编译
通过词法分析和语法分析,将合法指令翻译成等价汇编代码。通过编译过程,编译器将hello.i 翻译成汇编语言文件 hello.s;
- 汇编
将hello.s汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在hello.o 目标文件中;
- 链接
通过链接器,将hello的程序编码与动态链接库等收集整理成为一个单一文件,生成完全链接的可执行的目标文件hello;
- 加载运行
打开Shell,在其中键入 ./hello 1190200208 李旻翀,终端为其fork新建进程,并通过execve把代码和数据加载入虚拟内存空间,程序开始执行;
- 执行指令
在该进程被调度时,CPU为hello其分配时间片,在一个时间片中,hello享有CPU全部资源,PC寄存器一步一步地更新,CPU不断地取指,顺序执行自己的控制逻辑流;
- 访存
内存管理单元MMU将逻辑地址,一步步映射成物理地址,进而通过三级高速缓存系统访问物理内存/磁盘中的数据;
- 动态申请内存
printf 会调用malloc 向动态内存分配器申请堆中的内存;
- 信号处理
进程时刻等待着信号,如果运行途中键入ctr-c ctr-z 则调用shell 的信号处理函数分别进行停止、挂起等操作,对于其他信号也有相应的操作;
- 终止并被回收
Shell父进程等待并回收hello子进程,内核删除为hello进程创建的所有数据结构。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
计算机系统的设计与实现是一个复杂而又充满挑战的领域,涉及硬件和软件的密切合作,以及对性能、安全性和可靠性的高要求。在设计和实现计算机系统时,理解硬件和软件之间的相互作用至关重要。通过深入分析和优化系统资源的利用,可以实现显著的性能提升。例如,结合硬件特性和软件算法优化,可以在不同层次上实现更高效的数据处理和存储管理。
附件
hello.c 源文件
hello.i 预编译结果
hello.s 编译结果
hello.o 汇编结果
hello.o.txt hello.o反汇编结果
hello 可执行文件
hello.elf hello对应的elf 文件
helloelf.txt hello生成的elf文件对应的文本格式
列出所有的中间产物的文件名,并予以说明起作用。
参考文献
[1] 【Linux内核编程】 设备管理与文件IO操作_linux内核如何管理设备资源的-优快云博客
[2] 32|IO管理:Linux如何管理多个外设?-计算机基础实战课-极客时间
[3] Markmap
[4] csapp
hello.c 源文件
hello.i 预编译结果
hello.s 编译结果
hello.o 汇编结果
hello.o.txt hello.o反汇编结果
hello 可执行文件
hello.elf hello对应的elf 文件
helloelf.txt hello生成的elf文件对应的文本格式