计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业
学 号
班 级
学 生
指 导 教 师
计算机科学与技术学院
2024年5月
本文以hello.c为例,从系统的角度展示了C代码由源文件到可执行文件,再到可执行文件运行的全过程。源程序经过预处理、编译、汇编、链接等过程生成可执行程序之后运行,而生成可执行文件并运行的过程中需要大量计算机软硬件的支持,涉及大量计算机系统的知识。本文通过回顾hello.c的一生,展示了计算机系统的运行机制,进一步加深对计算机系统知识的理解。
关键词:计算机系统;hello;预处理;编译;汇编;链接;进程;存储;IO;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
P2P:Program to Process
hello.c作为C语言程序(program),经过预处理器(cpp)进行预处理,对源代码进行一系列的处理操作,包括宏替换、文件包含、条件编译等,产生中间文件hello.i。之后文件hello.i通过编译器(ccl)进行编译,生成中间文件hello.s,它是汇编代码。接下来,汇编器(as)将 hello.s汇编成可执行的机器语言指令,把这些指令打包成可重定位目标文件hello.o。hello.o再经过链接器(ld),进行地址和空间的分配,符号决议和重定位,最后生成可执行目标文件Hello。之后在Shell的命令行键入 “./Hello”,shell调用相关函数为其创建进程(process),执行程序。
020:From zero to zero
shell命令行输入命令./hello后,Shell调用fork()函数创建子进程,再调用evecue程序来加载并运行hello程序。当程序运行结束,向父进程发送SLGCHLD信号,之后父进程会对其进行回收。hello被回收后,生命周期结束。
1.2 环境与工具
软件环境:ubuntu20.04;Windows11 64位;
硬件环境:X64 CPU 3.10 GHz 16.0 GB RAM
开发工具:codeblock, gcc, gdb, edb, objdump, readelf
1.3 中间结果
hello.i 预处理产生的文件
hello.s 将预处理文件变为汇编文件
hello.o 汇编器将汇编文件变为可重定位文件
elf.txt hello.o的ELF格式代码
hello.asm hello.o的反汇编文件,查看链接
Hello.asm Hello的反汇编文件,查看汇编代码
hello_elf.txt Hello的elf格式代码
1.4 本章小结
本章介绍了hello程序的P2P,020过程,简单概括之后的工作,并介绍了开发环境、工具与中间结果及其作用。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:C语言预处理是预处理器(cpp)在编译之前对源代码进行一系列的处理操作,包括宏替换、文件包含、条件编译等,最终生成经过预处理后的.i文件。
作用:
- 宏替换:通过#define定义宏,可以将代码替换成一个标识符,在编译时将标识符替换成对应的代码。
- 头文件的包含:通过#include指令,可以将头文件的内容包含到当前文件。
- 条件编译:根据“#ifdef”等相关指令的条件决定需要编译的代码。
- 删除注释:预处理器可以删除源代码中的注释。
2.2在Ubuntu下预处理的命令
图1.在Ubuntu下预处理命令及运行结果截图
2.3 Hello的预处理结果解析
打开hello.i,可以看到包含头文件的代码变成了头文件相关内容,包含地址,变量等内容,最后是main函数的内容。
图2.hello.i(部分)
图3.hello.i(部分)
2.4 本章小结
本章讲述了预处理的概念及作用,并对hello.c进行了预处理,生成了hello.i,简单分析了hello.i的内容。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译是指从 .i 到 .s 即预处理后的文件到汇编语言程序,这一过程中,编译器会检查代码的规范性、是否有语法错误等,以确定代码的实际工作,并进行适当的代码分析及优化。之后,编译器会生成相应的汇编文件。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
图4.在Ubuntu下编译命令及运行结果截图
应截图,展示编译过程!
3.3 Hello的编译结果解析
3.3.1头部声明:
.file 表明源文件为“hello.c”,.text表明接下来是代码段,.section .rodata表明此部分为只读数据段,.LC0 .LC1为.string声明的两个字符串,.global声明全局变量,.type声明main为函数变量,.alion 8表示对齐方式为8字节对齐。
3.3.2常量:
字符串常量,位于只读数据段(.rodata):
图5.汇编语言(部分)
3.3.3变量:
全局变量: 全局变量是定义在函数外部,整个程序范围内都可见的变量。但在本代码中并未定义全局变量,只有几个函数为全局变量,如main等。
局部变量: 局部变量只在变量所定义的作用域内有效。在本代码中,main函数内的变量均为局部变量,并存在寄存器和栈中。
图6.汇编语言(部分)
3.3.4赋值:
-20(%rbp):通过movl %edi, -20(%rbp)存储参数argc的值。
-32(%rbp):通过movq %rsi, -32(%rbp)存储参数argv的值。
-32(%rbp):通过movl $0, -4(%rbp)初始化局部变量i:
3.3.5算数操作:
表示i++。
3.3.6关系操作:
图7.汇编语言(部分)
判断argc!=5,若等于跳转至.L2。
图8.汇编语言(部分)
判断i<10,若小于跳转至.L4
3.3.7数组操作:
图9.汇编语言(部分)
表示argv[1],argv[2],argv[3], argv [4].
3.3.8控制转移:
图10.汇编语言(部分)
即for(i=0;i<10;i++)。
3.3.9函数操作:
图11.汇编语言(部分)
调用相关函数。
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.4 本章小结
本章利用相关命令生成了汇编语言文件hello.s,并对hello.s的头部声明、变量赋值、算术操作、关系操作、数组操作、控制转移、函数操作等进行了分析。
(以下格式自行编排,编辑时删除)
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编是指汇编器(as)将hello.s翻译成机器语言指令,并把这些指令打包成可重定位目标程序的格式,保存在.o文件中。即可将汇编代码文件(.s)转换成可重定位目标文件(.o)。hello.o文件是一个二进制文件,它包含的是函数main的指令编码,得到的二进制机器语言是机器可以直接理解并运行的,只要再经过链接就可以得到能够运行的完整程序了。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
图12.在Ubuntu下汇编命令及运行结果截图
应截图,展示汇编过程!
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
图13.将hello.o文件内的信息读入到.txt文件
4.3.1 elf头
图14.elf头
elf头主要包含了描述ELF文件整体结构和属性的信息,包括ELF标识、目标体系结构、节表偏移、程序头表偏移等:
4.3.2节头部表
图14.节头
节头部表包含了不同节的类型,地址,偏移大小等基本信息。.test为程序代码,.data是初始化的全局变量,.bss是未初始化的全局变量,.rodata是只读数据节,.symtab是符号节,.strtab是字符串节。
(无程序头)
4.3.3重定位节:
图15.重定位
重定位节包含了在代码中使用的一些外部变量信息,在链接的时候需要根据重定位节的信息对于某些变量符号进行修改。每个代码段或数据段都对应一个重定位表,记录了段中的这些位置,方便对它们进行查找和操作。链接的时候链接器会根据重定位节的信息对于外部变量符号决定选择何种方法计算正确的地址,例如通过偏移量等信息计算。
4.3.4符号表:
图16.符号表
符号表 .symtab列出了所有定义的符号,包括函数、变量和节。
4.4 Hello.o的结果解析
图17.生成hello.asm
图18hello.asm
不同之处:
- 进制不同:hello.asm采用16进制的编码表示立即数,hello.s为10进制。
- 跳转地址表示不同:hello.asm跳转到当前过程的起始地址加上偏移量得到的直接目标代码地址;而hello.s跳转到代码段。
- 函数调用:hello.asm跳转到目标函数的地址,hello.s跳转到目标函数的名称。
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
4.5 本章小结
本章生成了.o文件类型,对其可重定位目标elf格式进行了分析,并对比了反汇编得到的.asm文件和.s文件的区别,更加深入地理解了代码在汇编过程中发生的变化。
(以下格式自行编排,编辑时删除)
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接的概念是将多个目标文件合并为一个可执行文件的过程。链接包含符号解析和重定位两步。链接器将每个符号引用与符号的定义相关联,将符号在可重定位文件的位置重定位至可执行文件的位置。
链接的作用:链接可以将一个大型的应用程序分解成多个更小、更好管理的模块,可以实现各模块的独立修改和编译。当需要修改其中一个模块时,只需重新编译该模块并重新链接整个应用,提高了效率。
注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令
(以下格式自行编排,编辑时删除)
图19.在Ubuntu下链接命令及运行结果
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
5.3.0生成Hello的ELF代码的文本文件。
图20.生成ELF代码命令
5.3.1EFL头:
描述体系结构和操作系统等基本信息,还包括程序的入口点,即当程序运行时要执行的第一条指令的地址。
图21.ELF头
5.3.2节头:
图22.节头
保存了所有的section的信息,内容相较于之前更为详细,节增加了很多,增加了一些可执行文件特有的段如.init等。
5.3.3程序头:
图23.程序头
给出了各个段的信息。
5.3.4段节:
图24.段节
节到段的映射。
5.3.5动态段:
图25.动态段
动态段包含了一些在运行时需要的信息,例如需要的共享库,初始化和终止函数的地址等。
5.3.6重定位节:
图26.重定位节
重定位节包含了指令或数据的地址重定位所需的信息,用于在链接时进行符号解析和地址重定位。
5.3.7符号表:
图27.符号表
符号表的作用是存储程序中定义和引用的符号信息。
5.4 hello的虚拟地址空间
使用edb
图28.edb命令
图29.edb加载内容
Data Dump区域显示了虚拟地址空间的信息,可以看到hello程序从虚拟空间0x401000载入,是.init的地址,与5.3中节头部表中的地址一致。
图30.datadump
.text段存储的是已编译程序的机器代码,由节头部表可知.text段开始于0x4010f0
而在edb中,查看可得与.text地址一致。
图31..text部分
通过查阅可知,其余部分edb也与5.3部分的相对应内容地址一致。
5.5 链接的重定位过程分析
进行重定位:
图32.反汇编命令
图33.Hello.asm
不同之处
- 反汇编call后着的成了实际的地址;
- 反汇编多了许多新的节和函数;
Hello生成的反汇编文件中的跳转地址十分明确,指向了虚拟空间地址,并增加了其他所需节及函数。
链接过程:此过程包括解析符号和重定位两步。在重定位之前,汇编器在hello.o终的重定位段记录需要重定位的符号类型和偏移量。链接器通过对符号的解析,将每个符号的引用和定义相关联。之后将命令行输入的静态库链接,开始重定位,重定位过程将为每个符号分配运行时的地址。
重定位过程:链接器将所有相同类型的节合并为的新的节,然后将运行时内存地址赋给新的节,此时程序中的每条指令和变量都有唯一的运行时内存地址。然后重定位节中的符号引用,链接器修改引用,使得它们指向正确的运行时地址。
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.6 hello的执行流程
图34.edbData界面
子程序名 程序地址
_start 0x4010f0
main 0x401125
__libc_start_main 0x403ff0
puts@plt 0x401090
printf@plt 0x4010a0
getchar@plt 0x4010b0
atoi@plt 0x4010c0
exit@plt 0x4010d0
sleep@plt 0x4010e0
使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
在进行动态链接前,首先进行静态链接,生成部分链接的可执行目标文件 hello。此时共享库中的代码和数据没有被合并到hello中,加载 hello 时,动态链接器对共享目标文件中相应模块内的代码和数据进行重定位,加载共享库并生成完全链接的可执行目标文件.
当程序调用一个由共享库定义的函数时,由于编译器无法预测这时候函数的地 址是什么,因此这时,编译系统提供了延迟绑定的方法,将过程地址的绑定推迟到 第一次调用该过程时。通过 GOT 和过程链接表 PLT 的协作来解析函数的地址。
图35.运行前后改动
5.8 本章小结
本章简单介绍了链接的概念和作用,分析了可执行目标文件的ELF结构,同时将Hello生成的反汇编代码与hello.o生成的反汇编代码进行了比较,分析了两者重定位过程的不同,并且还详细说明了动态链接共享库函数的过程,探究了可重定位文件hello.o链接生成可执行文件hello的各个过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。操作系统通过进程来管理和调度程序的执行。
进程的作用:
1. 资源分配单位:进程是资源分配的基本单位。操作系统通过进程为其分配CPU时间、内存、文件、输入/输出设备等资源。每个进程拥有自己的资源集,确保各个进程独立运行。
2. 并发执行:进程使得多任务操作系统能够并发执行多个任务。通过进程的并发执行,用户可以同时运行多个程序,提高了系统的利用率和用户体验。
6.2 简述壳Shell-bash的作用与处理流程
壳(Shell)是计算机操作系统中的一个命令行解释器,它是用户与操作系统内核之间的接口。Bash(Bourne Again SHell)是一种常见的Unix和Linux系统下的壳。通过Bash壳,用户可以与操作系统进行交互,并执行各种操作和任务。
作用:命令解释和执行、环境变量管理、输入输出重定向、创建并管理进程。
处理流程:Bash从标准输入中读取用户输入的命令,分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量。检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令,如果命令是内建命令(built-in command),Shell 会直接执行该命令,如果命令不是内建命令,Shell 会在系统的 PATH 环境变量中查找该命令的可执行文件。找到命令的可执行文件,Shell 就会用fork函数创建一个子进程来执行该命令,调用execve( )执行指定程序。如果命令行末尾有后台命令符号& 终端进程不执行等待系统调用,立即返回;如果命令末尾没有& 则终端进程要一直等待。
6.3 Hello的fork进程创建过程
在命令行输入./hello 学号 姓名,shell检查该命令是否为内置命令,显然这不是内置命令。于是,shell调用fork函数创建一个新的子进程,子进程得到与父进程用户级虚拟地址空间相同的一份副本,这意味着父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大区别在于有不同的PID。接下来将hello加载到这个进程中执行。
6.4 Hello的execve过程
Shell调用execve函数加载Hello程序的可执行文件到子进程的地址空间,原有的代码和数据将被清除。并从Hello程序的main函数开始执行。这实现了从父进程到子进程执行Hello程序的转变,并将控制传递给新程序的主函数。
6.5 Hello的进程执行
逻辑控制流,并发流:
即使在系统中通常有许多其他程序在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个 PC值的序列叫做逻辑控制流,或者简称逻辑流。 一个逻辑流的执行在时间与另一个流重叠,称为并发流, 这两个流被称为并发地运行。
用户模式和内核模式的切换:
运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷人系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。
进程时间片:
进程上下文:
内核为每个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.6 hello的异常与信号处理
异常:
类别 | 原因 | 异步/同步 | 返回行为 |
中断 | 来自 I/O 设备的信号 | 异步 | 总是返回到下一条指令 |
陷阱 | 有意的异常 | 同步 | 总是返回到下一条指令 |
故障 | 潜在可恢复的错误 | 同步 | 可能返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
6.6.1正常运行:
图36.正常运行
6.6.2 乱按:
图37.乱按
程序仍然正常运行至结束,键盘输入加上回车键后会将屏幕的输入缓存到缓冲区。乱码被认为是命令,但不影响当前进程的执行,最后Hello程序的getchar() 读取缓冲区的一个字符后正常退出。
6.6.3ctrl+z
图38.ctrlz
按下ctrl+Z后,程序接到SIGSTP信号被挂起,并显示相关信息。
1.运行ps命令,可看到之前的Hello
图39.ps
- 运行jobs命令,可看到已停止的Hello
图40.jobs
- 运行pstree,可查看进程树
图41.pstree(部分)
- 运行fg
运行fg,将调回前台继续执行
图42.fg
- 运行kill
图43.kill
kill -9 %1,即将信号9(SIGKILL)发送给作业1,即Hello进程,表示杀死程序,再运行ps命令时发现Hello进程被杀死。
6.6.4Ctrl+c
图44.ctrlc
程序运行时按下Ctrl + C,hello进程因收到SIGINT信号而终止。
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.7本章小结
本章介绍了进程的概念和作用,简要介绍了shell的作用及处理流程。以Hello程序的执行为例,分析了shell通过函数fork与函数execve创建进程Hello的过程和Hello进程的执行,以及执行过程的异常和信号处理。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:包含在机器语言指令中用来指定一个操作数或一条指令的地址。它促使程序员把程序分成若干段。每一个逻辑地址都由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。对应于hello.o中的相对偏移地址。
线性地址:线性地址是逻辑地址到物理地址间的中间层,其地址空间是一个非负整数地址的有序集合,若地址空间中的整数连续,则称其为线性地址空间,hello段中的偏移地址与其基地址组合生成了一个线性地址。
虚拟地址:在保护模式下,程序运行在虚拟内存中。虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每字节都有一个唯一的虚拟地址,作为到数组的索引。
物理地址:物理地址是加载到内存地址寄存器中的地址,是数据存放在内存单元的真正地址,为CPU定位物理内存对应地址,即hello的真正地址。
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
Intel逻辑地址到线性地址的变换主要基于段式管理。逻辑地址由段选择符和偏移量组成,段选择符决定了段描述符所在的描述表(GDT或LDT)。转换过程如下:
1、确定描述表:通过段选择符的TI字段确定段描述符在GDT还是LDT中。
2、计算描述符地址:利用段选择符的index字段和描述符大小(通常为8字 节), 结合GDTR或LDTR寄存器的内容,计算出段描述符的地址。
3、获取线性地址:将逻辑地址的偏移量(offset)与段描述符中的base字段 值相加,得到线性地址。
这种变换机制允许操作系统在保护模式下对内存进行更加精细的控制和管理。
7.3 Hello的线性地址到物理地址的变换-页式管理
VM系统将虚拟内存分割为大小固定的块,称其为虚拟页,类似的,物理内存也被分割为大小固定的物理页,或称为页帧。
虚拟页面的集合被分为三种情况:
图45.虚拟页面三种情况(来自教材)
未分配的:VM系统还未分配的页,无任何相关联数据,不占用磁盘空间。
已缓存的:当前已缓存在物理内存中的已分配页。
未缓存的:未缓存在物理内存的已分配页。
页表将虚拟页映射到物理页,地址翻译硬件将虚拟地址转化为物理地址。
图46.页表(来自教材)
内存管理单元(MMU)在CPU中扮演着关键的角色,它利用页表基址寄存器来确定当前进程所使用的页表的具体位置。一旦页表被定位,MMU就会利用虚拟页号(VPN)作为索引来查找对应的页表条目(PTE)。这个PTE包含了物理页号(PPN)的信息,该信息随后与虚拟页面内的偏移量(VPO)相结合,共同构成了一个完整的物理地址。这个过程允许CPU将程序中的虚拟地址转换为实际的物理地址,从而实现对物理内存的访问。。
图47.页命中(来自教材)
7.4 TLB与四级页表支持下的VA到PA的变换
7.4.1 TLB
TLB是一种高速缓存,用于存储最近使用的虚拟地址到物理地址的映射。通过TLB,可以避免每次地址转换都进行多级页表查找,从而加速地址转换过程。
7.4.2四级页表机制
四级页表机制将虚拟地址转换为物理地址时,通过四级页表结构进行映射。
7.4.3翻译地址
图48.翻译地址(来自教材)
内存管理单元(MMU)采用四级页表来转换36位虚拟页号(VPN)。VPN被分为四个9位的部分(L1-L4),每部分作为到下一级页表的偏移量。CR3寄存器存储L1页表的物理地址。通过L4、L3、L2和L1的偏移量,MMU在各级页表中查找物理页号(PPN)。PPN与页面内偏移量结合,形成物理地址。这种结构节省了内存,提高了地址翻译效率。
7.5 三级Cache支持下的物理内存访问
物理地址由块偏移(CO)、组索引(CI)、标记(CT)三部分组成,CPU在访问物理内存时,首先会尝试在L1 Cache中快速检索所需数据。若L1 Cache未命中,则会转向L2 Cache进行查找。若L2 Cache也未能找到所需数据,CPU会进一步在L3 Cache中搜索。若L3 Cache同样未命中,CPU将从主存中加载数据至L3 Cache,并通过层次结构依次传递至L2 Cache和L1 Cache,最终使CPU能够访问这些数据。
图48.三级Cache(来自教材)
7.6 hello进程fork时的内存映射
当 fork 函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的 PID,同时记录当前进程的mm_struct、区域结构和页表的原样副本,同时进程中的每个页面标记为只读,区域设置为私有的写时复制。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
图49.fork(来自教材)
7.7 hello进程execve时的内存映射
execve 函数在当前进程中加载并运行包含在可执行目标文件hello.out中的程序,用hello.out 程序有效地替代了当前程序。
加载并运行 hello.out 需要以下几个步骤:
•删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
•映射私有区域。为新程序的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello.out 文件中的。text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello.out中。栈和堆区域也是请求二进制零的,初始长度为零。图9-31概括了私有区域的不同映射。
•映射共享区城。如果hello.out 程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
·设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
图50.execve(来自教材)
7.8 缺页故障与缺页中断处理
当出现缺页故障时,即DRAM缓存不命中,此时调用缺页处理程序,内存会确定一个牺牲页,若页面被修改,则换出到磁盘,再将新的目标页替换牺牲页写入,缺页处理程序页面调入新的页面,并更新内存中的 PTE。缺页处理程序返回到原来的进程,重启导致缺页的指令。
图50.页面命中和缺页(来自教材)
7.9动态存储分配管理
动态内存分配器管理进程的堆区域,将其视为不同大小块的集合。已分配块供应用使用,而空闲块可用于分配。
隐式空闲链表管理在空闲块头部和底部嵌入4字节元数据,其中29位记录大小,3位指示空闲状态(000空闲,001已分配)。分配器通过搜索空闲链表,采用首次适配、下一次适配或最佳适配策略来分配内存。释放时,合并相邻空闲块以减少碎片。
显式空闲链表管理则维护多个按块大小分类的空闲链表。分配器根据请求大小在相应链表中搜索。释放时,简单分离存储不合并块,而分离适配则分割并重新组织块,以平衡搜索时间与空间利用率。GNU malloc采用后者方法。
7.10本章小结
本章介绍了hello的存储管理,分析了hello的存储器地址空间,讲解了段式管理,页式管理、VA到PA的转换、物理内存访问,分析了fork与execve函数,研究了缺页故障,阐述了动态存储分配管理,加深了对hello存储管理和数据传输的理解。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的 I/O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。在Linux系统下,设备文件位于 /dev 目录下,分为字符设备文件和块设备文件。这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单、低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行:
1、打开文件。
2、改变当前的文件位置。
3、读写文件。
4、关闭文件。
模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单、低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2.2 函数
1.打开文件:
为int open(char *filename, int flags, mode_t mode);filename为文件名,flags参数指明了访问这个文件的方式,可以是只读、只写、可读可写。mode参数指定了新文件的访问权限。若open成功则返回新文件描述符,若失败则返回-1。
2.关闭文件:
为int close(fd),fd是需要关闭的文件的描述符,若成功为0,若出错为-1。
3.读文件
为ssize_t read(int fd, void *buf, size_t n)函数,从描述符为fd的文件位置复制最多n个字节至内存buf。错误返回-1,文件位置移动至EOF返回0,其余情况返回实际传送的字节数量。
4.写文件
ssize_t write(int fd, const void *buf, size_t n),write函数从内存buf复制至多n个字节到描述符为fd的文件位置,错误返回-1。
8.3 printf的实现分析
函数体:
static int printf(const char *fmt, ...)
{
va_list args;
int i;
va_start(args, fmt);
write(1,printbuf,i=vsprintf(printbuf, fmt, args));
va_end(args);
return i;
}
int vsprintf(char *buf, const char *fmt, va_list args)
{
char* p;
char tmp[256];
va_list p_next_arg = args;
for (p=buf;*fmt;fmt++) {
if (*fmt != '%') {
*p++ = *fmt;
continue;
}
fmt++;
switch (*fmt) {
case 'x':
itoa(tmp, *((int*)p_next_arg));
strcpy(p, tmp);
p_next_arg += 4;
p += strlen(tmp);
break;
case 's':
break;
default:
break;
}
}
return (p - buf);
}
相关代码
(char*)(&fmt) + 4) 表示的是...中的第一个参数,在程序的内部逻辑中,printf 函数调用vsprintf 来格式化字符串,随后这些格式化的数据被写入到一个缓冲区 buf 中,同时返回一个长度值。接下来,利用 write 函数,这些字节从内存中的寄存器被高效地复制到显卡的专用显存区域。显卡上的字符显示驱动随后会读取这些ASCII码对应的显存区域,依据内置的字模库将这些ASCII码转换成相应的点阵图像数据,并将这些数据存放到VRAM(视频随机存取存储器)中。最终,显示芯片会按照一定的刷新率逐行扫描VRAM,并通过特定的信号线将每个像素的点阵信息传输给液晶显示器,从而在屏幕上呈现出程序所期望的输出内容。
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
int getchar(void)
{
static char buf[BUFSIZ];
static char* bb=buf;
static int n=0;
if(n==0)
{
n=read(0,buf,BUFSIZ);
bb=buf;`
}
return (--n>=0)?(unsigned char)*bb++:EOF;
}
getchar函数通过调用read函数等待用户按键后读取字符,read函数的返回值是读入字符的个数,若读取出错则返回-1。read函数读取键盘缓冲区的ASCII码,直到读到回车为止,然后将整个字符串返回。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的IO设备管理,介绍了Unix IO接口及其函数,并对printf和getchar函数的实现进行了讲解。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
一个hello.c程序的生命周期,从简单中映射出C语言程序的完整旅程:
- 预处理:程序经过预处理,解析宏定义与包含的文件。
- 编译。将预处理后的文件进行一系列的:词法分析、语法分析、语义分析及优化。
- 汇编。将汇编语言转变成机器可执行的指令。
- 链接:链接器将汇编生成的目标文件与需要的库文件进行链接,生成可执行文件。
- 创建进程:shell调用fork函数为程序hello创建子进程,分配资源,设置进程上下文等。
- 加载进程:新的子进程中调用execve函数,加载并运行hello,同时进行相应的重定位和链接动态库,开始程序的执行。
- 执行进程:为hello创建虚拟内存空间,MMU把虚拟内存空间映射物理内存当中,之后printf调用malloc向动态内存分配器申请空闲块。
- IO:hello获取输入后调用printf 从系统内核到硬件输出显示相关内容。
- 异常与信号处理:当进程中出现异常,如键盘键入Ctrl+C/Z,输出相应信号给内核,进入异常处理程序。
- 回收进程。程序执行完毕,父进程回收子进程,进程结束。
感悟:通过了解hello程序的一生,我深刻理解了现代计算机系统的精妙与各个硬件软件、各个部分的之间的配合,更进一步加深了对计算机的了解。同时,我也明白了对计算机的了解与掌握不止局限于写代码,掌握了代码运行的过程与各个软硬件之间的配合可以更好地提高计算机水平,既让我加深了对本门课计算机系统知识的了解,也让我之后能更好使用计算机和应用所学到的知识。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
(附件0分,缺失 -1分)
文件名称 | 文件作用 |
hello.c | 源代码 |
hello.i | 预处理后的代码 |
hello.s | 汇编文件 |
hello.o | 可重定位目标文件 |
Hello | 链接后的可执行目标文件 |
Elf.txt | hello.o的ELF |
hello.asm | hello.o反汇编后的代码 |
hello_elf.txt | Hello的ELF |
Hello.asm | Hello的反汇编代码 |
参考文献
为完成本次大作业你翻阅的书籍与网站等
- 《深入理解计算机系统(原书第三版)》 Randal E.Bryant David R.O’Hallaron 机械工业出版社
- [printf 函数实现的深入剖析 - Pianistx - 博客园](https://www.cnblogs.com/pianist/p/3315801.html)
- [可执行目标文件的ELF格式-李庆林](https://www.liqinglin0314.com/article/468)
- Linux 逻辑地址、线性地址、虚拟地址物理地址
(https://blog.csdn.net/baidu_35679960/article/details/80463445)
(参考文献0分,缺失 -1分)