摘 要
该论文从一个最基础的 hello.c 程序开始,遍历其从生成到运行的各个阶段,涵盖了预处理、编译、汇编、链接、进程管理、存储管理和IO管理等不同的主题。每个章节都包括了对该主题的概念与作用的阐述,以及在Ubuntu下相关命令的示例和实践结果的解析。通过以上内容,对课本教授内容进行细致的回顾,了解了一个程序的始末,对整个计算机系统有了更深入的了解。
关键词:预处理;编译;汇编;链接;进程管理;存储管理;IO管理。
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
Program to Progress: 原文件 hello.c 经过预处理器(cpp)预处理后成为 hello.i 文件;接着经过编译阶段将预处理后的文件翻译为汇编语言,由编译器完成,生成一个汇编代码文件 hello.s ;然后,再通过汇编器将汇编代码转换为机器可以执行的二进制文件 hello.o ;最后,链接器将目标文件 hello.o 与库中的所需文件组合形成可执行文件 hello 。然后即可在 bash 中运行该程序,创建了一个对应的进程,即为 progress 。
0 to 0:在 bash 中运行 hello 程序,bash 为其创建一个新的子进程,子进程中调用execve()函数,加载 hello 程序,CPU 为运行的 hello 分配时间片执行逻辑控制流。程序运行结束后回收进程,释放内存并删除程序相关的数据结构。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件:x64 AMD Ryzen 7 5800H with Radeon Graphics 3.20 GHz 16G RAM
软件:Windows10 64 位,Ubuntu 22.04
工具:gcc,vim,gdb
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名称 | 文件作用 |
hello.i | 预处理后产生文件 |
hello.s | 编译后产生文件 |
hello.o | 汇编后产生文件 |
hello | 链接产生的可执行文件 |
helloasm.txt | hello.o反汇编产生的文本文件 |
1.4 本章小结
本章类似一份总结,介绍了 hello 程序实现运行的整体流程,并介绍了本次实验使用到的软硬件和工具,最后列出了本次大作业所产生的各种中间文件。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:预处理是编译过程中的第一个阶段,其主要目的是对源代码进行处理,生成供编译器进一步处理的中间代码。预处理器负责处理源代码中的预处理指令,如 #include、#define、#ifdef、#ifndef 等,并对其进行展开或处理。预处理器并不关心语法、变量、函数等具体语言特性,它只是简单地执行指令,并将结果传递给编译器。
作用:
包含文件:使用#include 指令将其他文件的内容包含到当前文件中,便于模块化开发和代码重用。
宏替换:使用 #define 指令定义宏,可以用来表示常量、函数等,并在代码中进行替换,提高代码的可读性和灵活性。
条件编译:使用 #ifdef、#ifndef、#if 等指令对代码进行条件编译,根据不同的条件编译不同的代码,实现跨平台兼容性或调试等功能。
2.2在Ubuntu下预处理的命令
应截图,展示预处理过程!
图 1 预处理命令
2.3 Hello的预处理结果解析
预处理器根据头部指令 #include <stdio.h>、#include <unistd.h> 、#include <stdlib.h>将对应文件内容都插入到了 hello.i 文件中,同时将源代码中的注释全部去除。最后得到的 hello.i 文件达到了三千多行。
图 2 hello.c 的预处理结果
2.4 本章小结
预处理后的 hello.i 文件与原始的 hello.c 文件相比,会更大且包含更多的内容,因为它包含了所有预处理操作的结果。这些改变使得编译器能够更好地理解源代码并生成对应的目标文件。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
概念:编译是将高级语言源代码转换为目标代码(机器语言)的过程。在编译过程中,编译器将源代码翻译成与特定硬件平台兼容的目标代码,这个过程包括词法分析、语法分析、语义分析、优化和代码生成等步骤。
作用:编译的主要作用是将人类可读的高级语言代码转换为计算机可执行的机器代码,使得计算机能够理解和执行这些指令。
3.2 在Ubuntu下编译的命令
应截图,展示编译过程!
图 3 编译的命令
3.3 Hello的编译结果解析
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
hello.c 文件中出现的数据类型有常量,局部变量,全局变量,涉及的操作有数据、赋值,算术操作,关系操作,数组/指针/结构操作,控制转移以及函数操作。
3.3.1 数据
常量:
.LC0 定义了一个字符串常量,同时 hello.s 中还有以立即数表示的各种常量用来进行比较或赋值,记录方式为 $x。
图 4 常量示例
全局变量:
.globl main:这是一个全局声明指令,它声明了一个全局的符号 main。main是一个函数类型的全局变量。
图 5 全局变量
局部变量:
在 main 函数中,用为局部变量分配栈空间,分析代码可知 i 为一个局部变量。
同时有subq $32, %rsp:为局部变量分配 32 字节的栈空间。
接着movl %edi, -20(%rbp) 和 movq %rsi, -32(%rbp):将 main 函数参数 argc 和 argv 存储到栈中。
图 6 局部变量
3.3.2 赋值
通过 mov 指令进行赋值操作,例如将参数值存储到局部变量中。
图 7 赋值示例
3.3.3 算数操作
分析代码内容可知,存在一个 for 循环中,对应变量 i 每次循环进行一次加一操作。
图 8 加法操作
3.3.4 关系操作
关系操作有两种,一种是不等关系“!=”。判断参数个数是否等于 5。通过 cmp 指令进行比较操作。如果参数为 5,则跳转到 .L2 。
图 9 判断是否为5
另一种是 for 循环体中,小于关系“<”,小于 10 用小于等于 9 表示。判断 i 是否小于等于 9。如果循环变量小于等于 9,则跳转到循环体。
图 10 判断 i 是否小于等于 9
3.3.5 数组/指针/结构操作
为局部变量分配栈空间时使用到了栈指针。同时通过 add 指令对地址进行偏移,实现了数组/指针的访问即访问argv[1]到argv[3]。包括参数传递和函数调用时也涉及到指针的操作。
字符串数组首地址加载到寄存器 rax ,接着进行加 8 操作实现地址偏移,指向第二个元素,再将该指针值放入 rsi ,printf 函数调用打印对应数组元素。
图 11 指针访问数组
3.3.6 控制转移
通过条件跳转指令(如 je 和 jle)实现条件控制流程。程序中实现的有 if 判断和 for 循环。
if 判断:判断参数个数是否等于 5。通过 cmp 指令进行比较操作。如果参数为 5,则跳转到 .L2 。
图 12 if 判断
for 循环:循环体中直到满足循环变量大于9,就跳出循环,否则使用 jle 语句重新开始新一轮迭代。
图 13 for 循环控制转移
3.3.7 函数操作
函数调用:通过 call 指令调用外部函数(如 puts, exit, printf, atoi, sleep, getchar)。
图 14 调用外部函数示例
参数传递:main 函数接收到传递的两个参数,一个是 int argc,一个是 char* argv[]。
图 15 接收传递的参数
3.4 本章小结
通过对产生的汇编语言文件的分析,详细了解到编译器是如何对程序中的各种数据结构,各种操作进行理解和处理的。对其将高级语言转化为低级汇编语言的过程更加清晰。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
概念:汇编是使用汇编器(as)将汇编指令逐条映射到机器指令,使其成为机器可识别的形式。
作用:可将 hello.s 翻译为机器语言指令,并把这些指令打包成一种叫做可重定位目标程序的格式储存在 hello.o 文件中,得到一个二进制文件。
4.2 在Ubuntu下汇编的命令
应截图,展示汇编过程!
图 16 汇编命令
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。hello.o 的DLF格式,包括ELF头、节头、程序头、段节、重定位节等内容。
4.3.1 ELF头
描述内容在命令面板中均有显示,不再赘述。
图 17 ELF头
4.3.2 节头
图 18 节头
4.3.3 程序头
图 19 程序头
重定位节包括以下内容:
偏移量:指示需要进行重定位操作的代码在.text或者.data中的偏移量。
重定位类型:描述需要进行的重定位操作的类型,例如绝对地址、相对地址、符号引用等。
加数:计算重定位位置的辅助信息
信息:包括与重定位操作相关的其他数据,比如偏移量、符号信息等。
图 20 重定位节
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
机器语言的构成:机器语言是计算机可以直接执行的指令序列,由二进制数字组成。每条指令都被编码为一个特定的二进制模式,这些模式对应于不同的操作和操作数。
机器语言与汇编语言的映射关系:汇编语言是机器语言的助记符表示法,它使用易于理解和记忆的助记符来代替机器语言中的二进制指令。汇编语言程序由一系列指令和操作数组成,每条指令对应一个机器语言指令。
对照分析得到的差异点:
分支转移:反汇编代码跳转指令的操作数使用的不是段名称如.L3,段名称只是在汇编语言中便于编写的助记符,在汇编成机器语言之后就不存在,所以反汇编跳转指令的操作数是具体的地址。
函数调用:在 hello.s 文件中,进行函数调用只需直接 call 对应的函数名称,但反汇编得到的机器码中 call 的对象为下一条指令,该指令在链接器处理后通过重定位才能正确到达函数的执行地址。在汇编成为机器语言的时候,对于不确定地址的函数调用,将其 call 指令后的相对地址设置为全 0(目标地址正是下一条指令),然接着在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。
图 21 反汇编结果
4.5 本章小结
本章通过指令实现hello.s 进行汇编得到hello.o,并分析了 hello.o 的ELF格式。接着又对 hello.o 进行反汇编操作得到 helloasm.txt ,通过将其与 hello.s 对比分析了机器指令与汇编指令的不同。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
注意:这儿的链接是指从 hello.o 到hello生成过程。
概念:将多个目标文件或库文件合并成一个可执行文件或共享库的过程。链接器负责解析目标文件之间的符号引用关系,解决符号的重定位,以及生成最终的可执行文件或共享库。
作用:可以使分离编译成为现实,将一个巨大的源文件分成更小的模块进行编写,当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件。
图 22 链接命令
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
ELF头部开始是16个字节构成的魔数,接着是ELF头的大小、目标文件类型、机器类型、节头部表的文件偏移及其中条目的信息。
图 23 hello 的 ELF 头信息
节头中包括了每一节的名称、类型、地址和偏移量。
图 24 节头信息
readelf -l hello 查看 hello 的程序头部分。程序头中显示段的起始地址、大小、类型等内容。
图 25 程序头信息
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
加载 hello 到 edb ,data dump中显示虚拟地址空间的相关内容。在节头信息中选择一个类型为 .text 的段:
图 26 段信息
其在 edb 中的对应部分如下:
对比 readelf 输出和 EDB 中的虚拟地址空间信息,发现它们是一致的。从0x4010f0开始到0x4011c8。后面内容也照应readelf 提供了一个命令行的方式来查看 ELF 文件的各个段的基本信息,而 EDB 提供了一个图形化界面来查看进程的虚拟地址空间信息。两者都能够显示代码段、数据段、堆、栈等信息,并且它们在虚拟地址空间中的起始地址和大小应该是相符的。
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
hello 的反汇编文件中指令对应的有唯一的虚拟地址,而hello.o的反汇编文件中对应的是对 .text 段的偏移地址。
链接过程中,链接器在生成可执行文件时会将程序的各个部分(如代码段和数据段)分配到虚拟内存地址空间中的不同区域。hello经过链接,每条指令分配了唯一的虚拟地址。操作系统会根据链接器指定的虚拟地址空间分配情况将程序加载到适当的位置。
图 27 两种反汇编文件
5.6 hello的执行流程
使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。
edb 查看结果如下,名称与地址如图所示:
图 28 edb 查看执行流程
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变化。要截图标识说明。
动态链接项目通常包括共享库的路径、符号表、重定位表等。这些信息存储在程序的 ELF 头部及相关的段中。动态链接之后,程序会动态加载并链接共享库,这会导致程序的内存布局发生变化。使用调试器 gdb 来分析程序在动态链接前后的内存布局变化,以及动态链接项目的加载情况。下面是通过gdb查看,动态链接过程中,共享库的变化和内存信息变化:
图 29 共享库的变化
图 30 内存布局变化
5.8 本章小结
本章介绍了链接的概念与作用,并对其ELF格式进行了分析和比较,接着通过 edb 调试分析了链接的过程和重定位的方法。后面又对重定位和动态链接进行的更深入的探究,加深了相关理解。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是计算机中正在运行的程序的实例。每个进程都有自己的地址空间、内存、数据栈、文件描述符等资源。进程之间是相互独立的,彼此不受影响。
作用:能够并发执行提高了系统资源的利用率,操作系统通过进程来管理系统资源,如内存、CPU 时间等,以确保各个进程能够合理地共享和使用资源。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell是用户与操作系统内核之间的接口,用户通过壳与操作系统进行交互。bash 作为常见的 Linux 环境下的壳,可以接收用户命令并传递给操作系统执行。
处理流程:
等待用户输入: bash 不断等待用户输入命令。
解析命令: 当用户输入命令后,bash 会对命令进行解析,识别命令名称、参数等。
执行命令: bash 根据解析结果调用相应的程序或系统调用执行用户的命令。
输出结果: 执行完命令后,bash 将结果输出到屏幕上,并继续等待用户的输入。
6.3 Hello的fork进程创建过程
程序开始执行时,存在一个主进程(父进程)。在 Hello 程序中,调用 fork() 函数会创建一个新的进程。该函数会返回两次,一次在父进程中返回新创建的子进程的进程 ID,另一次在子进程中返回 0。在调用 fork() 后,父进程和子进程将执行相同的代码,但它们有各自独立的内存空间。由于 argc 不等于 5,所以会执行条件判断语句 if(argc!=5),这会导致程序输出 "用法: Hello 学号 姓名 手机号 秒数!" 并退出。在子进程中,由于 fork() 返回值为 0,子进程会执行 for 循环中的代码块。子进程会输出带有学号、姓名和手机号的 "Hello" 信息。接着子进程会调用 sleep(atoi(argv[4])),暂停指定的秒数。子进程会重复执行 for 循环,直到循环结束。父进程在执行条件判断后输出提示信息并退出;子进程在循环结束后执行 getchar(),等待用户输入字符,然后返回 0,结束执行。
6.4 Hello的execve过程
调用 execve 时,操作系统会加载并执行 hello ,构建新程序的参数列表,即 argv (字符串数组,包含了新程序的命令行参数)和 envp(字符串数组,包含了新程序的环境变量)接着开始执行新程序的代码。如果 execve 调用成功,它将不会返回到原来的程序,只有当找不到可执行目标文件时才会但回到调用程序,否则execve函数会将控制转移到新的可执行文件去执行。
调用 execve 通常是在创建子进程后,子进程调用 execve 来执行另一个程序。这样做可以实现程序的替换效果
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
在终端中输入可执行目标文件hello及其参数,操作系统会为调用fork()函数创建一个子进程。在这个新进程的上下文中调用execve函数加载可执行目标文件hello。此时,父进程fork() 返回子进程的 PID。子进程fork()返回 0。
用户模式和内核模式,设置模式位时,进程运行在内核模式,内核模式下的进程能够执行指令集中的任何指令,并且可以访问系统中任何内存位置。没有设置模式位时,进程就运行在用户模式中,用户模式下的进程不允许执行特权指令,也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。
当 hello 程序执行系统调用(如 sleep)时,会触发从用户态到核心态的转换。在核心态下,操作系统会处理系统调用,例如设置定时器以实现睡眠功能。完成后,操作系统将控制权返回到用户态,hello 程序继续执行。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
回车,Ctrl-Z以及后续命令对应结果如下:
图 31 异常处理
回车只是是结果换了一行,Ctrl-Z 传递 SIGTSTP 信号暂停当前进程执行,将其置于后台。ps 命令可查看当前进程状态,jobs显示出当前进程状态,fg 可将进程调回前台继续执行,或者使用 bg 命令将其转移到后台执行。Pstree 命令展示所有进程的树状图。
Ctrl-C结果:
图 32 Ctrl-c 结果
Ctrl-C会终止当前进程的执行。Kill可以杀死指定进程。
6.7本章小结
本章从进程角度介绍了 hello 程序的整个进程运行过程,并了解了 shell 的相关内容,对进程的知识有了更深的了解。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:逻辑地址是指程序中使用的地址,hello 程序中,逻辑地址用于访问变量、函数等程序中的各个部分。但逻辑地址根据地址中包含的段标识符和段内偏移量或者其它内容映射到对应的有效地址。
线性地址:是由逻辑地址通过段地址转换后的地址,操作系统使用页表或段表等数据结构来管理线性地址到物理地址的映射关系。
虚拟地址:在 hello 程序执行过程中,所有的逻辑地址都被转换为虚拟地址,然后由操作系统将虚拟地址映射到物理地址上。
物理地址:实际存在于计算机内存中的地址,用于访问存储在内存中的数据和指令。虚拟地址通过页表映射到物理地址,这个映射由操作系统管理。
hello 程序执行过程中,操作系统会将程序的逻辑地址转换为虚拟地址,并将虚拟地址映射到物理地址上。这样,程序就可以访问内存中的数据和指令了。这个过程是由操作系统的内存管理单元来完成的,它负责管理虚拟地址到物理地址的映射关系,以及内存的分配和回收等工作。
7.2 Intel逻辑地址到线性地址的变换-段式管理
Intel 的段式管理是一种内存管理技术,用于将逻辑地址转换为线性地址。在段式管理中,内存被划分为多个段,每个段都有一个段基址和段限长。逻辑地址由两部分组成:段选择器和偏移量。段选择器用于选择段,偏移量用于指定段内的具体地址。
段式管理通过将内存划分为多个段,并使用段描述符来管理每个段的基址和限长,实现了逻辑地址到线性地址的转换。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理中,内存被划分为固定大小的页,而逻辑地址和物理地址都被分割成相同大小的页。当CPU访问内存时,逻辑地址首先被分成页号和页内偏移。然后,页表被用来将页号映射到物理地址的页框号。页框号和页内偏移被组合成物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
当程序访问一个虚拟地址时,硬件首先尝试在TLB中查找对应的条目。
TLB是一个快速的缓存,存储了最近访问过的VA到PA的映射。
如果TLB命中(即找到了对应的条目),则可以直接从TLB获取物理地址,并完成地址转换。
如果TLB未命中,硬件需要在页表中查找VA对应的条目。
页表查找过程如下:
使用VA中的最高位索引PML4,找到PDPT的条目。
使用VA中的下一组高位索引PDPT,找到PDT的条目。
使用VA中的下一组高位索引PDT,找到PT的条目。
使用VA中的下一组高位索引PT,找到最终的页帧(Page Frame)条目。
一旦找到第3级页表中的条目,就可以从该条目中获取页帧号。页帧号与VA中的页内偏移量(offset)组合,形成完整的物理地址。物理地址 = 页帧号 * 页大小 + VA中的偏移量在x86-64架构中,页大小通常是4KB,但也可以是2MB或1GB的大页。
7.5 三级Cache支持下的物理内存访问
三级Cache下的内存访问过程:
CPU 缓存访问:首先会检查 L1 缓存。L1 缓存是距离 CPU 最近的高速缓存,通常包含 CPU 最常用的数据和指令;在 L1 缓存中未找到所需的数据或指令(未命中),则会继续在 L2 缓存中查找。
L2缓存访问:L2 缓存通常比 L1 缓存更大,但速度稍慢一些。如果在 L2 缓存中找到了数据或指令,则可以直接从 L2 缓存中读取,从而实现较快的访问速度。如果在 L2 缓存中仍未找到所需的数据或指令,则会继续在 L3 缓存中查找。
L3缓存访问:L3 缓存是一个更大、但速度相对较慢的缓存,通常位于 CPU 核心之间或与多个核心共享。L3 缓存中找到了所需的数据或指令,则可以直接从 L3 缓存中读取,虽然速度不及 L1 和 L2 缓存,但仍然比访问主存要快得多。如果在 L3 缓存中未找到所需的数据或指令,则会继续访问主存。
主存访问:主存通常是存储在计算机系统中的物理内存,速度比 CPU 缓存慢得多,但容量更大,CPU 将会向主存发出请求,将所需的数据或指令加载到缓存中,以便后续的快速访问。
7.6 hello进程fork时的内存映射
Hello函数调用fork()创建一个新的进程时,新进程会复制父进程的内存映像,包括代码段、数据段、堆和栈等。这种内存映像的复制是通过写时复制技术来实现的,这意味着父进程和子进程在初始阶段共享相同的物理内存页。
7.7 hello进程execve时的内存映射
hello 进程调用execve()函数时,它会加载一个新的程序映像到其地址空间,替换当前的进程映像。xecve()执行后,原始的"hello"进程将会完全被替换为新的程序,且新程序将会在进程的地址空间中占据主导地位。
7.8 缺页故障与缺页中断处理
缺页故障:指的是当程序试图访问一个虚拟内存页,而这个页当前不在物理内存中时发生的情况。
缺页中断处理:CPU会暂停当前程序的执行,并跳转到操作系统内核中预先定义好的缺页中断处理程序。中断处理程序会分析引起缺页中断的原因。可能的原因包括页面不在内存中、页面被换出到磁盘、页面权限错误等。中断处理程序会根据缺页原因采取适当的措施来解决问题。可能的操作包括将页面从磁盘加载到内存、将被替换的页面写回到磁盘、更新页表等。页面被成功加载到内存中,中断处理程序将更新页表,以便将虚拟地址映射到新加载的物理页面。一旦所需的页面已经在内存中并且页表已更新,中断处理程序将恢复程序的执行。程序将重新执行引发缺页中断的指令,这次可以成功访问所需的页面。
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
动态内存分配器在程序启动时通常会初始化一个内存池,这是一个预先分配的内存区域。内存池的大小可以根据需要动态调整,或者在编译时指定一个固定大小。程序需要分配内存时,它会调用动态内存分配器提供的接口函数(如malloc()或new),并传递所需内存的大小。动态内存分配器会搜索内存池,以找到足够大且未被使用的内存块来满足分配请求。它可能使用不同的算法来决定选择哪个空闲内存块,如首次适应、最佳适应或最差适应等。找到合适的空闲内存后,动态内存分配器会将其标记为已分配,并返回一个指向该内存块的指针给程序。程序不再需要某个已分配的内存块时,它会调用动态内存分配器提供的释放接口函数(如free()或delete),并传递指向该内存块的指针。动态内存分配器会将已释放的内存块标记为可用,并将其添加到空闲内存列表中,以便在后续的内存分配请求中再次使用。
7.10本章小结
本章多角度分析了 hello 程序运行时的内存分配管理,介绍了不同地址的转换方式还有几种映射方法,同时也对hello 进程的内存映射做了解释,最后介绍了缺氧中断与缺页中断处理的方式。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
Linux 中,一切都是文件的概念被广泛应用,包括硬件设备。这意味着每个设备都被抽象为一个文件,可以通过标准的文件 I/O 操作来访问和控制。设备文件通常位于 /dev 目录下,每个设备都有相应的文件代表。对这些设备文件进行读写操作,应用程序可以与硬件设备进行通信。例如,可以通过读写 /dev/sda 文件来对硬盘进行读写操作。
设备管理:unix io接口
Unix/Linux 操作系统提供了一组标准的 I/O 接口,允许应用程序通过文件描述符进行设备管理和 I/O 操作。通过这些标准的 Unix I/O 接口,应用程序可以方便地与设备进行通信,而无需了解底层硬件细节或特定设备的驱动程序实现。
8.2 简述Unix IO接口及其函数
Unix I/O 接口提供了一系列函数和系统调用,用于在 Unix/Linux 系统上进行输入输出操作。下面是一些基础的函数:
open():打开一个文件,返回文件描述符。
close():关闭一个文件描述符。
read():从文件中读取数据。
write():写入数据到文件。
lseek():在文件中移动文件指针。
fcntl():对文件描述符进行各种控制操作。
8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
对与print函数:
printf() 函数接受一个格式化字符串 fmt 和可变数量的参数,函数内部,首先定义了一个缓冲区 buf,用于存储格式化后的字符串。使用 va_list 类型的变量 arg 来处理可变数量的参数。va_list 是一个指向参数列表的指针。调用 vsprintf() 函数,将格式化后的字符串存储到缓冲区 buf 中。vsprintf() 是一个类似于 sprintf() 函数的变种,可以接受一个 va_list 类型的参数列表。调用 write() 系统函数,将缓冲区中的内容写入到标准输出流中。这里假设 write() 函数接受一个缓冲区和其大小作为参数,将缓冲区中的内容写入到标准输出流中。最后返回写入到标准输出流中的字符数目 i。
根据网页中的vsprintf程序,可知vsprintf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。vsprintf的作用为接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,进而产生格式化输出。
查看write内容:
给几个寄存器传递了参数,然后以一个int INT_VECTOR_SYS_CALL结束。INT_VECTOR_SYS_CALL代表通过系统调用syscall,查看syscall的实现:
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码,符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
getchar() 函数的实现通常是基于标准输入流的,Unix/Linux系统中,键盘输入会被当作标准输入流的数据。当你在终端中键入字符时,这些字符会被传递给操作系统内核,然后内核会触发相应的中断。操作系统的键盘中断处理程序会负责将键盘输入转换成相应的 ASCII 码,并将其存储在系统的键盘缓冲区中。
当你调用 getchar() 函数时,实际上是在请求从标准输入流中读取一个字符。getchar() 函数内部通常会调用 read() 系统调用来实现这个功能。read() 系统调用会阻塞程序的执行,直到从文件描述符中读取到指定的字节数或者发生错误。在这种情况下,文件描述符是标准输入流对应的文件描述符 0。
因此,当你调用 getchar() 函数时,它会等待键盘输入,直到你按下回车键为止。一旦你按下回车键,getchar() 函数会返回缓冲区中的下一个字符,并将其从缓冲区中移除,以便下一次调用时能够读取新的字符。
8.5本章小结
本章介绍Linux的IO设备管理,强调以文件模型化设备。Unix提供标准IO接口,包括open()、close()、read()、write()、lseek()和fcntl()等函数,可方便进行设备管理。深入探讨printf和getchar实现,对理解Unix IO接口原理有帮助。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
- 预处理: hello.c 经过预处理器对源文件hello.c进行宏展开、头文件包含、条件编译等处理生成预处理后的文件 hello.i 。
- 编译: 将预处理后的hello.i翻译成汇编代码,生成汇编代码文件 hello.s 。
- 汇编: 汇编阶段将汇编代码翻译成机器可读的目标文件,生成可重定位目标文件hello.o
- 链接: 将各个目标文件及所需的库文件合并成一个可执行文件,生成可执行文件hello。
- 进程管理: shell 运行hello程序,调用 fork() 函数创建进程,hello 在创建的子进程中运行。
- 内存管理: hello 被执行时程序的代码、数据段被加载到对应内存地址。程序加载到内存后操作系统对其进行分配,还有符号解析和重定位确保程序中的地址引用都指向正确的内存位置。
- I/O管理: 所有的I/O设备(如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对文件的读写操作。通过标准的 Unix I/O 接口,应用程序可以方便地与设备进行通信,而无需了解底层硬件细节或特定设备的驱动程序实现。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
文件名称 | 文件作用 |
hello.i | 预处理后产生文件 |
hello.s | 编译后产生文件 |
hello.o | 汇编后产生文件 |
hello | 链接产生的可执行文件 |
helloasm.txt | hello.o反汇编产生的文本文件 |
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 王爽. (2018). 汇编语言(第3版). 人民邮电出版社
[2] Hennessy, J. L., & Patterson, D. A. (2017). Computer Architecture: A Quantitative Approach (6th ed.). Morgan Kaufmann.
(参考文献0分,缺失 -1分)