计算机系统
大作业
计算机科学与技术学院
2024年5月
摘 要
本文通过对 Hello.c 文件进行预处理、编译、汇编、链接的单步执行操作,深入查看反汇编代码、中间文件及 I/O 结果,全方位跟踪一个典型 C 语言程序从代码编写到最终运行的完整生命周期。在此过程中,详细剖析软件工具链各环节的功能实现,以及硬件资源如何协同配合,从而揭示程序运行背后软、硬件交互的深层机制。
关键词:计算机系统;汇编;链接;存储;I/O;
(摘要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 –
结论……………………………………………………………………………………………………………. – 14 –
附件……………………………………………………………………………………………………………. – 15 –
参考文献…………………………………………………………………………………………………….. – 16 –
第1章 概述
1.1 Hello简介
P2P(From Program to Process)
程序阶段(Program):程序员将Hello程序以文本形式逐字逐句敲入编辑器,保存为hello.c文件。此时的Hello仅仅是存储在磁盘上的一段文本程序,它是静态的,由程序员赋予了初始的逻辑和结构。
预处理与编译:通过预处理器(Cpp)对hello.c文件进行预处理,处理如宏定义、头文件包含等操作。接着,编译器(Compiler)将预处理后的代码翻译为汇编代码,这一过程将高级语言代码转换为计算机能够理解的低级指令形式。
汇编:汇编器(AS)把汇编代码转换为机器可执行的目标文件,这个目标文件包含了机器指令和数据,但还不能直接运行。
链接:链接器(LD)将目标文件与其他必要的库文件进行链接,生成一个完整的可执行文件。至此,Hello程序从一个文本文件转变为一个可以在操作系统上运行的实体。
进程阶段(Process):在操作系统的壳(Bash)中,用户执行可执行文件时,操作系统的进程管理模块会为Hello程序创建一个新的进程。具体来说,通过fork系统调用复制父进程的资源,然后使用execve系统调用加载并执行Hello程序的代码。同时,操作系统的内存管理单元(MMU)会为进程分配虚拟地址空间,通过多级页表(如 4 级页表)将虚拟地址映射到物理地址,TLB(快表)和 Cache(高速缓存)会加速地址转换和数据访问。CPU 通过取指、译码、执行的流水线操作,按照时间片调度机制执行Hello程序的指令。在执行过程中,输入输出管理模块负责处理Hello程序与外部设备(如键盘、屏幕)的交互,信号处理机制则处理程序运行过程中可能出现的各种异常和信号。当Hello程序执行完毕,操作系统会回收该进程所占用的资源,完成从程序到进程的完整生命周期。
O2O(From Zero – 0 to Zero – 0)
诞生:从最初的空白状态(Zero – 0),程序员编写代码,经过一系列的编译、链接等操作,Hello程序从无到有,成为一个可执行的程序。
运行:在操作系统的支持下,Hello程序以进程的形式在硬件上运行,与外部设备进行交互,完成其功能。
结束:程序运行结束后,操作系统回收其占用的所有资源,Hello再次回归到空白状态(Zero – 0),就像赤条条来去无牵挂,不带走一片云彩。
1.2 环境与工具
硬件环境:
处理器:AMD Ryzen 9 7940H w/ Radeon 780M Graphics 4.00 GHz
机带RAM:16.0GB
系统类型:64位操作系统,基于x64的处理器
软件环境:Windows11 64位 版本号23H2,VMware,Ubuntu
开发与调试工具:Visual Studio 2022;vim,gidit ,objdump,edb,gcc,readelf等开发工具
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c:原始hello程序的C语言代码
hello.i:预处理过后的hello代码
hello.s:由预处理代码生成的汇编代码
hello.o:二进制目标代码
hello:进行链接后的可执行程序
hello_asm.txt:反汇编hello.o得到的反汇编文件
hello1_asm.txt:反汇编hello可执行文件得到的反汇编文件
1.4 本章小结
本小节将围绕两方面展开论述:其一,深入阐述 hello 项目在 P2P 与 O2O 模式下的具体运行流程;其二,详细说明实验所依托的软硬件环境、调试开发工具,同时对论文研究过程中生成的中间结果文件名称及用途进行系统性梳理,确保研究过程可回溯、可验证。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:在程序进入正式编译流程前,需要经历一个重要阶段 —— 预处理,即对代码实施一系列特定处理操作。预处理器作为执行预处理任务的核心程序,一般由编译器集成提供,其主要功能是解析代码文件并执行相应处理指令。
作用:预处理过程中,宏定义替换是一项关键操作。宏定义本质上是具有复用特性的代码片段,通过宏定义,开发人员可以减少重复性代码书写,提升代码的可读性与可维护性。预处理器会在编译启动前,将代码中的宏定义按规则进行替换,从而精简代码内容。此外,预处理器还具备其他重要功能:它能够实现文件内容的嵌套包含,将程序分割为多个源文件,优化代码结构;同时支持条件编译,依据设定的条件选择性地包含或排除部分代码。以常见的 C 语言源文件 hello.c 为例,文件首行的 #include<stdio.h> 指令,即指示预处理器读取系统标准输入输出头文件 stdio.h 的内容,并将其嵌入到当前程序代码中,为后续程序的编译与运行奠定基础 。
2.2在Ubuntu下预处理的命令
在终端输入gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i 回车,在原目录生成了hello.i文件
图1 预处理hello.c的命令
2.3 Hello的预处理结果解析
图2 hello.i文件内容
hello.i文件中,仍然有源程序代码,而且对宏进行了展开,比如#include指令和#define指令。同时预处理器也不会对头文件中的内容做任何计算或转换,只是简单地复制和替换。
2.4 本章小结
本章节聚焦于 Linux 操作系统环境,系统阐述了 C 语言程序预处理的相关概念、具体操作指令及其重要作用。通过以典型的 hello.c 程序为研究对象,在 Ubuntu 平台上开展实际操作演示,详细展示了 C 语言程序预处理的完整流程。经深入分析预处理后生成的 hello.i 文件可知,其不仅整合了标准输入输出库 stdio.h 的全部内容,还包含宏定义、常量声明、行号标识等信息,同时保留了条件编译指令,这些元素共同构成了预处理后程序的新形态,为后续的编译环节提供了重要基础。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:编译是把预处理后的文件(.i)转换为汇编语言程序(.s)的过程。编译器会进行词法分析、语法分析、语义分析等,将代码先转换为中间表示形式,再生成汇编语言。
作用:其作用包括提高代码可移植性,使程序能在不同平台运行;优化代码执行效率,通过各种优化手段让程序运行更快;发现代码错误,帮助程序员及时修正;将高级语言转换为机器能理解的语言,让计算机可以执行程序。
3.2 在Ubuntu下编译的命令
图3.s文件的生成
3.3 Hello的编译结果解析
3.3.1 字符串处理
在 C 语言里,字符串通常以字符数组的形式存在,以空字符 '\\0' 作为结束标志。在汇编代码中,字符串被定义于 .rodata 段,此段属于只读数据段。
分析:.string 伪指令用来定义字符串常量,编译器把字符串存储在只读数据段,且以空字符结尾。
图4字符串处理
3.3.2 函数调用
在 C 语言中,函数调用是常见操作。在汇编代码里,借助 call 指令来实现函数调用。
分析:call 指令会把当前指令的下一条指令地址压入栈中,然后跳转到被调用函数的入口地址。@PLT 表明这是一个通过过程链接表(PLT)进行的动态链接函数调用。
图5函数的调用
3.3.3 栈帧管理
在 C 语言中,函数调用时会创建栈帧来保存局部变量和函数调用的上下文信息。在汇编代码里,借助 pushq、movq、subq、leave 等指令来管理栈帧。
分析:pushq %rbp 把旧的基址指针压入栈中,movq %rsp, %rbp 把当前栈指针赋值给基址指针,subq $32, %rsp 为局部变量分配栈空间,leave 指令用于恢复栈帧。
图6栈帧管理
3.3.4 条件判断与循环
在 C 语言中,条件判断和循环是控制程序流程的重要手段。在汇编代码里,通过 cmpl、je、jle 等指令来实现条件判断和循环。
分析:cmpl 指令用于比较两个操作数,je 指令在相等时跳转,jle 指令在小于等于时跳转。
图7条件判断与循环
3.3.5 数据传递
在 C 语言中,函数调用时需要传递参数。在汇编代码里,参数通常通过寄存器或者栈来传递。
分析:movq 和 movl 指令用于在寄存器之间或者寄存器与内存之间移动数据,从而实现参数传递。
图8数据传递
3.3.6 整数类型处理
在 C 语言中,整数类型是基本的数据类型。在汇编代码里,通过 movl、addl 等指令来处理整数类型。
分析:movl 指令用于移动 32 位整数,addl 指令用于对 32 位整数进行加法操作。
图9整数类型处理
综上所述,编译器在处理 C 语言的各个数据类型和操作时,会把高级语言的代码转换为底层的汇编指令,通过栈帧管理、寄存器操作、函数调用等方式来实现程序的功能。
3.4 本章小结
在本章里,我们对汇编指令展开了简要说明,并探究了 “Hello” 程序的机器级实现方式。稍加思索后不难发现,这些汇编指令与 C 语言代码语句之间存在着明确的对应联系。也就是说,汇编指令和 C 语言代码语句并非相互孤立,而是有着内在的关联。并且,依据某个程序的汇编代码,我们还能够反向推导出与之对应的 C 语言程序的大致结构和逻辑,进而深入理解程序在不同语言层面的表现形式和运行机制
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:汇编指的是将编译后的汇编语言文件(.s)转化为机器语言二进制程序(.o)的过程。汇编器会对汇编语言代码逐行进行处理,把其中的助记符(如 mov、add 等)替换成对应的机器指令,同时处理好标号、常量等,最终生成目标文件。汇编的
作用:生成可执行基础:生成的目标文件是后续链接成可执行程序的基础,没有汇编得到的目标文件,程序就无法在计算机上运行。
硬件适配:能将与特定硬件架构相关的汇编代码转化为该硬件可识别和执行的机器码,确保程序能在相应硬件上正常工作。
代码优化检查:在汇编过程中,可以对代码进行一些简单的优化,也能发现汇编代码里存在的语法错误等问题。
4.2 在Ubuntu下汇编的命令
gcc hello.s -c -o hello.o
图10.o文件的生成
4.3 可重定位目标elf格式
4.3.1ELF头
.o 文件属于目标文件,和 Windows 系统下的.obj 文件性质类似。由于其存储的是二进制机器码及相关数据结构,若直接使用 Vim 等文本编辑器打开,会呈现出大量乱码,无法直观解读。因此,为了获取有效信息,我们可以查看可重定位目标文件的 ELF格式内容。通过执行readelf -h hello.o命令,就能读取 ELF 文件头信息,具体显示结果如下:
图11ELF头信息
ELF 头的起始是一个 16 字节的 Magic 魔数序列,它如同文件的 “身份标签”,清晰标注了生成该文件的系统字长规格以及字节存储顺序。这 16 个字节蕴含的信息,就像是为系统识别文件特性亮起的第一盏灯。ELF 头的其余部分则是一份详尽的 “文件说明书”,其中的各项信息专为辅助连接器解析和理解目标文件而设计。这些内容涵盖了 ELF 头自身的尺寸、目标文件的类型属性、适配的机器架构类型等关键参数。以实际查看的结果为例,“Data” 字段标识出系统采用小端字节序存储数据;“Type” 字段明确文件属于 REL 类型,即具备可重定位特性;而 “Number of section headers” 则显示该文件包含 13 个节头,为后续对文件内部结构的解析提供重要依据 。
4.3.2Section头
图12 Section头信息
夹在ELF头和节头部表之间的都为节,包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。各部分含义如下:
图13 各部分语义
4.3.3符号表
图14符号表信息
符号信息里,“Num” 作为每个符号独一无二的编号,如同符号的专属身份证号码;“Name” 则直观呈现符号的具体名称,方便使用者快速识别。以 main 函数对应的符号为例,“Size” 字段显示它是一个存储于.text 节,从偏移量 0 处开始,长度达 146 字节的函数,就像在地图上精准标注出函数在文件中的 “坐标” 和 “大小”。而 “Bind” 字段则用于界定符号的作用域,通过查看可知,main 函数名称对应的符号变量属性为本地作用域,意味着该符号的可见范围仅限于当前目标文件内部,这一特性对理解程序中变量和函数的调用机制至关重要。
4.3.4可重定位段信息
图15可重定位段信息
在可重定位目标文件的处理中,offset 指的是文件内需要调整引用的具体位置,就像在一栋大楼里给某个房间标注的精确房号,指明了修改操作的 “目的地”。Sym. 记录着这个引用最终应指向的符号,类似于为房间分配的主人名称,明确引用的 “归属对象”。
Type 则如同操作指南,向连接器说明修改引用的具体方法。不同的 Type 对应不同的修改策略,例如有些需要直接替换,有些则需根据特定规则调整。Addend 是一个带正负号的常数,在部分类型的重定位过程中,它就像一个 “微调器”,通过对引用值进行偏移调整,确保引用能够准确无误地指向目标,让程序各部分在链接时无缝对接 。
4.4 Hello.o的结果解析
使用objdumo -d -r hello.o命令对hello.o可重定位文件进行反汇编,得到的反汇编结果如下
图16 hello.o的反汇编结果
我们对 hello.o 的反汇编文件并不陌生,它的汇编代码与 hello.s 汇编文件一致。不过,反汇编文件中还夹杂着机器代码,这些机器代码是二进制机器指令的集合,是机器真正能识别的语言。实际上,每一条汇编语言都能用机器二进制数据表示,汇编语言里的操作码和操作数与机器语言存在映射关系,借此机器才能理解代码含义并执行相应功能。机器代码和汇编代码存在以下差异:
分支跳转
汇编语言的分支跳转语句通过标识符(如 je .L2 )指明跳转目标;而机器语言经转换后,直接采用对应地址实现跳转。
函数调用
在汇编语言.s 文件里,函数调用直接写函数名。但在.o 反汇编文件中,call 指令的目标地址是当前指令的下一条指令地址。这是由于 hello.c 调用的多为共享库函数,其地址需链接时才能确定。所以在机器语言中,对于这类地址不确定的调用,先将下一条指令的相对地址设为 0 ,并在.rela.text 节添加重定位条目,待链接阶段确定地址。
4.5 本章小结
本章阐述了 Hello 从 hello.s 转换为 hello.o 的过程。在这部分内容里,对 hello.o 的 ELF 头、Section 头以及符号表展开剖析,得以洞察 Hello 更深层次的信息。此外,还对 hello.o 的反汇编文件进行解读,对比了相较于 hello.s 文件 ,.o 文件如何能让机器更好地理解代码。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:从 hello.o 到 hello 的生成过程中,链接是将目标文件(如 hello.o )和所需的库文件等,通过特定的程序(链接器)组合成一个可执行文件(hello)的过程 。链接器会处理目标文件中的符号引用,把各个目标文件的代码和数据片段按照一定规则整合起来,并确定程序运行时各部分的内存地址等信息。
作用:
符号解析与重定位:目标文件中存在对其他函数、变量等的符号引用,链接时链接器会解析这些符号,找到它们在其他目标文件或库中的实际位置,并修正目标文件中对这些符号的引用地址,这个过程叫重定位。例如 hello.c 中调用 printf 函数,在 hello.o 中只是对 printf 的符号引用,链接时确定其在标准库中的实际地址,让程序知道去哪里调用该函数 。
合并段和分配空间:将各个目标文件的代码段、数据段等合并在一起,形成可执行文件最终的内存映像布局。同时为这些段分配运行时的内存地址,规划好程序运行时各部分数据和代码在内存中的位置。
库链接与整合:若程序依赖外部库(如 hello.c 调用标准库函数依赖 C 标准库),链接过程会把库中相关的代码和数据整合到可执行文件中,使程序能正常调用库函数,实现相应功能。
生成可执行文件:把所有相关的目标文件、库文件等整合处理后,生成一个完整的、操作系统能够识别和加载运行的可执行文件,完成从源文件到可运行程序的关键转换。
5.2 在Ubuntu下链接的命令
在终端中,输入ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o回车,结果如图所示:
图17 终端中使用ld指令进行链接
5.3 可执行目标文件hello的格式
5.3.1 ELF头
图18 hello的ELF头
文件的Type发生了变化,从REL变成了EXEC(Executable file可执行文件),节头部数量也发生了变化,变为了27个。
5.3.2 Section头
图19 hello的Section头
在hello文件里,节头部表涵盖了诸多关键信息,像大小(Size)、偏移量(Offset)、起始地址(Address)以及数据对齐方式(Align)等。依据起始地址与大小这两项数据,便能算出节头部表中各个节所处的区域。
5.3.3 符号表
图20 hello的符号表
链接操作完成后,能明显观察到符号表内的符号数量大幅攀升。这表明在链接过程中,大量来自其他库函数的符号被引入,并整合进了符号表。
5.3.4
图21 hello的可重定位信息
5.4 hello的虚拟地址空间
图21 hello起止虚拟地址
hello虚拟地址空间的起始地址为0x401000,结束地址为0x401ff0。
图22 init和其在虚拟内存中对应
根据5.3.2节里面的Section头部表,我们可以找到对应的节的其实空间对应位置,例如.init初始化节,起始位置地址为0x401000在edb中有其对应位置lo虚拟地址空间的起始地址为0x401000,结束地址为0x401ff0。
5.5 链接的重定位过程分析
图23 hello的反汇编
1. 分析hello与hello.o的不同,说明链接的过程
文件格式与内容差异:
hello.o:是目标文件,通常是编译源文件后生成的,包含未解析的符号引用。它的格式是 ELF(可执行与可链接格式),但其中的代码和数据还未进行最终的地址定位和符号解析 。例如,函数调用的目标地址在未链接时是不确定的,只是预留了占位符,等待链接阶段确定。
hello:是可执行文件,经过链接器处理后得到。链接器会将hello.o与其他必要的库文件(如 C 标准库等,若程序使用了库函数 )进行合并。在这个过程中,会解析目标文件中的符号引用,确定函数和变量的实际地址,完成地址重定位。
链接过程说明
符号解析:链接器首先扫描所有的目标文件(这里是hello.o )和库文件,构建一个全局符号表。对于hello.o中未定义的符号(比如调用的外部函数 ),链接器会在库文件中查找其定义。例如,如果hello.c中调用了printf函数,hello.o中printf的引用在链接时会被解析到 C 标准库中printf函数的实际位置。
重定位:在确定了所有符号的地址后,链接器会修改目标文件中与地址相关的指令和数据。比如hello.o中函数调用指令的目标地址占位符,会被替换为实际的函数入口地址。objdump -d -r hello中的 “-r” 选项显示的重定位信息,就记录了这些地址调整的信息。链接器根据重定位表中的信息,对代码和数据中的地址进行修正,使程序在运行时能正确访问函数和变量。
2. 结合hello.o的重定位项目,分析hello中对其怎么重定位的
重定位项目查看:通过objdump -r hello.o可以查看hello.o的重定位项目。重定位表中每一项通常包含重定位地址、重定位类型和相关符号信息。例如,可能会有对某个外部函数调用地址的重定位项,记录了在代码中的哪个位置需要调整地址,以及该地址对应的符号(函数名)。
hello中的重定位过程分析:在链接生成hello可执行文件时,链接器根据hello.o的重定位表进行操作。对于重定位表中的每一项,链接器会根据符号解析得到的符号实际地址,计算出需要调整的偏移量。然后,将hello.o中对应位置的地址值进行修改。比如,若hello.o中有一条指令是调用函数func,重定位表指出该调用指令的地址需要修正,链接器在找到func的实际地址后,会将调用指令中的目标地址修改为func的真实入口地址,从而完成重定位,确保程序运行时指令能正确跳转到目标函数。
5.6 hello的执行流程
图24 Hello的执行流程
5.7 Hello的动态链接分析
在程序启动前(尚未进行动态链接实际操作时 ),这些动态链接库还未映射到进程地址空间。而在程序启动过程中,动态链接器(如ld – linux.so )会负责将程序依赖的动态链接库加载到内存,并进行符号解析和重定位等操作。通过对比启动前后 “Memory Map” 窗口中的内容,可以看到动态链接库逐渐被映射进来,并且程序代码段、数据段等与动态链接库交互相关的地址信息也发生了变化(如函数调用地址被修正为动态链接库中实际函数的地址 )。
图25 EDB中重定位
5.8 本章小结
本章对链接过程做了简要介绍。开篇简述链接的概念与作用,给出 Ubuntu 系统下的链接指令。随后,对可执行目标文件 hello 的 ELF 格式展开研究,利用 edb 调试工具查看虚拟地址空间及若干节的内容。接着,依据重定位条目剖析重定位过程,同时借助 edb 调试工具探究程序里各子程序的执行流程。最后,还是借助 edb 调试工具,通过对虚拟内存的读取,分析动态链接过程。经链接,hello.o 与它所依赖的全部库整合,生成可执行文件。在该可执行文件中,所有运行时地址均已确定,能够被载入内存并运行。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程是计算机中程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位 。简单来说,当一个程序被加载到内存中并开始执行,它就成为了一个进程。例如,我们双击打开一个游戏程序,系统会为这个游戏程序创建一个进程,使其在内存中运行。
作用:
资源分配:操作系统通过进程来分配系统资源,如 CPU 时间、内存空间、磁盘 I/O 等。每个进程都能获得一定份额的资源,以保证其正常运行。比如多个办公软件同时运行,系统会根据进程需求合理分配内存,让它们都能正常读写数据。
调度执行:进程是 CPU 调度的对象。操作系统依据特定的调度算法,在不同进程间切换 CPU 使用权,使得多个进程能看似 “同时” 执行。像我们在电脑上同时运行音乐播放器、浏览器等程序,CPU 会在这些进程对应的任务间快速切换,让我们感觉它们在同时工作。
隔离保护:进程拥有自己的虚拟地址空间,这实现了不同进程间的隔离。一个进程无法随意访问其他进程的地址空间,保障了程序运行的稳定性和安全性。例如,恶意程序不能随意篡改其他正常程序的数据,因为它们处于不同的进程地址空间中。
6.2 简述壳Shell-bash的作用与处理流程
作用:
bash 是用户与系统内核的交互接口,通过简单指令实现文件操作等功能;可解释执行用户输入命令,将其转为系统调用;支持脚本编程,实现任务自动化。
处理流程:
先读取用户输入命令,再解析命令语法、识别参数,接着在 PATH 指定路径查找可执行文件,区分内外部命令 ,最后创建子进程执行命令,父进程等待子进程完成后继续等待新命令输入。
6.3 Hello的fork进程创建过程
在 Shell 环境下,借助 fork 系统调用能够生成子进程。新生成的子进程与父进程极为相似,但并非完全一致。子进程会获取一份和父进程用户级虚拟地址空间相同却彼此独立的副本,其中涵盖代码段、数据段、堆、共享库以及用户栈等关键部分。同时,子进程还会继承父进程所有打开文件描述符的副本,这使得在 fork 操作完成后,子进程能够对父进程已打开的任何文件进行读写操作。父进程与子进程最显著的差异体现在进程标识符(PID)上,二者的 PID 各不相同。
fork 调用仅在父进程中触发一次,但会产生两次返回结果。此后,父进程和新创建的子进程将以并发的方式执行各自的任务。当执行 hello 程序时,由于 fork 创建的子进程在前台运行,负责创建它的父进程 shell 会暂时处于挂起状态,直至 hello 进程执行结束才会恢复运行。
图26 内存空间示意图
6.4 Hello的execve过程
当 Hello1 进程创建完成后,会调用 execve 函数来加载并运行指定程序。execve 函数的独特之处在于,它在当前进程的运行环境中加载新程序并使其投入运行,一旦成功执行便不再返回,具体执行步骤如下:
首先,它会清除当前进程已有的用户区域数据。接着,开始构建新的区域结构:为 hello 程序的代码、数据、.bss 段以及栈区域分别创建私有区域,这些区域采用写时复制技术,在实际写入操作前共享数据,以此提升内存使用效率。随后,进行共享区域映射,将 hello 程序与所需的共享库(如 libc.so)进行链接,让程序能够调用库中的函数。最后,execve 函数会将当前进程上下文的程序计数器设置为新加载程序代码区域的入口地址,至此新程序开始正式执行。值得注意的是,只有在执行过程中遭遇错误,比如无法找到要执行的程序文件时,execve 函数才会返回调用处并报告错误信息,若执行顺利则不会返回到调用程序 。
6.5 Hello的进程执行
6.5.1 逻辑控制流
逻辑控制流本质上是程序计数器(PC)值所构成的序列。这些 PC 值与可执行目标文件中的指令,或是运行时动态链接到程序的共享对象指令一一对应。在程序运行过程中,PC 值不断跳转,依序指向不同指令,形成了一条连贯的指令执行轨迹,驱动程序逐步完成各项任务。
6.5.2 时间分片
在当下计算机系统架构中,进程并非独占处理器,而是以轮流使用的方式运行。每个进程执行自身逻辑控制流的一部分后,会被系统暂时中断(即抢占),为其他进程让出处理器使用权。当一个逻辑流的执行时间与另一个逻辑流出现部分重叠,它们便构成了并发流,呈现出同时执行的 “假象”,这种多个流同时执行的现象被称为并发。而进程交替运行的机制则被称为多任务,其中,进程每次执行控制流的时间区间,就叫做时间片,因此多任务也常被称作时间分片。这种机制有效提升了处理器利用率,让用户感觉多个程序能同时运行。
6.5.3 用户模式与内核模式
为确保操作系统内核的安全性,处理器的特定控制寄存器设有模式位。当模式位被置位时,进程处于内核模式;未置位时,则运行在用户模式。处于内核模式的代码拥有最高权限,可不受限制地访问所有处理器指令集、全部内存空间以及 I/O 设备。而用户模式下的进程若想使用这些特权资源,必须通过系统调用向设备驱动程序或其他内核模式代码发起请求。此外,用户模式代码允许出现缺页情况,但内核模式代码则不允许,以保障内核运行的稳定性。
程序代码启动之初,均处于用户模式。一旦发生中断、故障或系统调用等异常事件,进程会从用户模式切换至内核模式。异常发生时,控制权转交至异常处理程序,处理器同步将模式切换为内核模式。异常处理程序在内核模式下执行,完成处理后返回应用程序代码时,处理器再将模式切回用户模式。
6.5.4 进程上下文切换
进程上下文是内核恢复一个被中断进程运行所需的全部状态信息,涵盖通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈,以及各类内核数据结构等。在进程执行期间,内核可依据调度策略,决定暂停当前进程,转而恢复先前被中断的进程继续执行,这一决策过程由内核中的调度器负责。而实现进程切换的关键操作便是上下文切换:系统先保存当前进程的上下文信息,再恢复此前被中断进程的上下文,并将控制权交给恢复后的进程,以此确保进程交替运行的流畅性与正确性。
6.6 hello的异常与信号处理
6.6.1 异常类型
在程序运行过程中,异常可主要划分为以下几类:
运行时异常:此类异常源于程序执行时的非法操作,典型情况如尝试除以零,或者使用未经初始化的空指针进行解引用操作。一旦发生这类异常,程序往往会因无法继续正常执行而崩溃,例如在计算中出现int result = 10 / 0; ,或者对未指向有效内存的指针int *ptr; *ptr = 5;进行操作时。
资源异常:这是由于程序所需资源出现问题引发的异常。当程序尝试访问不存在的文件,如调用open函数打开一个根本不存在的文件路径,或者系统内存资源不足,无法为程序分配足够内存以满足其运行需求时,就会触发此类异常。这类异常需要程序具备完善的错误处理机制,以避免程序异常终止或产生数据丢失等问题。
输入异常:当用户提供的数据不符合程序预先设定的格式或取值范围时,输入异常便会产生。例如,程序要求用户输入一个正整数,但用户输入了负数或字符串,此时程序需要能够识别并妥善处理这类不符合预期的输入数据。
6.6.2 产生的信号
在程序运行期间,会有多种信号发挥作用,常见的有:
SIGINT:当用户在终端界面按下Ctrl+C组合键时,系统会向正在运行的程序发送SIGINT信号。该信号主要用于紧急中断程序的执行,比如在运行一个长时间执行的脚本程序时,用户希望立即停止它,便可通过此操作实现。
SIGTSTP:若用户按下Ctrl+Z,系统将发送SIGTSTP信号,其作用是暂停程序的运行。此时,程序会暂时停止执行,进入后台挂起状态,用户可以通过后续指令恢复程序运行或进行其他操作 。
SIGTERM:这是一个用于请求程序正常终止的信号。通常在系统关闭或其他需要结束程序运行的正常场景下,系统会向程序发送SIGTERM信号,期望程序能够进行必要的资源清理和数据保存等操作后,平稳地结束运行。
6.6.3具体信号处理与命令
1.乱按字
图27 乱按键盘
2.按Ctrl+Z
图28 按Ctrl+Z
Ctrl+Z的功能是向进程发送SIGSTP信号,进程接收到该信号之后会将该作业挂起,但不会回收。
图29 输入ps
该图显示PID为6624的hello进程还在运行中
图30 输入jobs
该图显示hello的后台job id为1
图31 输入fg
fg命令用于将后台作业(在后台运行的或者在后台挂起的作业)放到前台终端运行。
图32 输入kill
结束进程
3. 按Ctrl+C
图33 输入Ctrl+C
Ctrl-C命令内核向前台发送SIGINT信号,终止了前台作业。
4.不停按回车
图34 不停输入回车
在hello执行过程中不停按回车,不仅在printf输出时会显示出回车,在hello进程执行完毕后,可以看到回车的信息也同样发送到了shell中,使shell进行了若干次的刷新换行。
6.7本章小结
在本章的学习中,我们深入探究了 Hello 进程的运行机制,同时也接触到了一系列与进程相关的知识和概念。进程为程序的运行营造了两个至关重要的 “假象” ,即逻辑控制流和私有地址空间。通过对 Hello 进程的剖析,我们能更清晰地认识到,逻辑控制流让程序在运行时仿佛拥有了独立的、连续的执行路径,如同舞台上的主角按既定剧本演绎;而私有地址空间则像给程序划分了专属领地,让它可以安心地存储和操作数据,不用担心与其他程序相互干扰,极大地保障了程序运行的安全性与稳定性。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.2 Intel逻辑地址到线性地址的变换-段式管理
在 Intel 平台的内存管理机制中,逻辑地址采用selector:offset的二元结构呈现。其中,selector存储于 CS(代码段寄存器)内,offset则对应EIP(指令指针寄存器)的数值 。
CS 寄存器作为 16 位的关键部件,承载着当前执行指令所在代码段的起始标识信息。其值并非直接的物理地址,而是用于索引代码段的段基址,与段基址共同定位代码段在内存中的起始位置;EIP 寄存器则是 CPU 的 “导航仪”,时刻记录着下一条待执行指令的地址。每当 CPU 完成一条汇编指令的执行,EIP 寄存器便会自动更新,指向下一条指令,确保程序指令的有序执行。
将 selector 作为 “钥匙”,在GDT(全局描述符表)中检索对应的segment base address(段基址),再与 offset 相加,即可得到linear address(线性地址),这一过程便是段式内存管理的核心运作逻辑。逻辑地址由段标识符与段内偏移量构成,其中 16 位的段标识符(段选择符),其前 13 位充当索引 “坐标”,可精准定位段描述符表中特定的段描述符,该描述符详细记录了段的属性与位置信息。
段描述符根据作用范围不同,存储在不同的表中:全局性质的段描述符存放于 GDT,而局部性质的则存储在LDT(局部描述符表)。当给定完整的 “段选择符 + 段内偏移地址” 逻辑地址时,可通过段选择符中的T1 标志位判断检索目标:T1 = 0 时,表明需访问 GDT 中的段描述符;T1 = 1 则指向 LDT。随后,借助对应寄存器获取表的地址与大小,利用段选择符前 13 位索引,即可提取段描述符中的段基地址,最终与偏移量相加,完成逻辑地址到线性地址的转换。
图35 Intel X86 架构存储器的寻址示意图
7.3 Hello的线性地址到物理地址的变换-页式管理
在 Intel x86-64 架构体系下,线性地址(Virtual Address,VA)向物理地址(Physical Address,PA)的转换依托于分页机制实现。这一机制与主存和 Cache 间的分块策略存在异曲同工之妙,均通过将地址空间划分为固定大小的页来管理存储资源。页的规格并非一成不变,在不同技术发展阶段,其容量通常介于 4KB 至 2MB 区间浮动。以 x86-64 机器为例,虚拟地址空间规模达 2 的 48 次方,对应 256TB 的海量寻址范围,远超常规硬盘存储容量。
分页机制构建起硬盘空间与虚拟地址空间的精确映射体系,该映射以字节为基本单元,遵循严格的单射关系,确保每个硬盘字节与虚拟地址空间字节一一对应。假设硬盘空间记为 H,虚拟地址空间为 V,二者间映射关系由函数 f 表征,通过 f 可建立起物理地址页、虚拟地址页及硬盘存储页之间的三重关联。
那么,物理地址页与虚拟地址页的对应关系如何维系?分页系统引入页表这一核心数据结构,该表由操作系统负责维护并驻留在内存中。值得注意的是,尽管 DRAM 与 Cache 间的高速缓存机制同样基于分块原理,但后者主要依赖硬件逻辑实现,与分页机制在管理方式上形成软硬互补。
每个进程均配备专属页表,其中的页表条目(Page Table Entry,PTE)作为关键存储单元,详细记录着虚拟地址页的多重属性:不仅包含该页是否有效(即是否已映射至物理内存页),还涵盖对应物理页基地址或磁盘存储位置,以及读写执行等访问权限信息。依据映射状态差异,PTE 可划分为三大类别:
未分配状态:对应虚拟内存中尚未被进程占用的空白页;
未缓存状态:已分配给进程,但尚未调入物理内存的页,当前数据仍存储于磁盘;
已缓存状态:已成功映射至物理内存页框,实现数据的快速存取。
图36 虚拟页面分配示意图
7.4 TLB与四级页表支持下的VA到PA的变换
在现代计算机系统的内存管理架构中,页表作为虚拟地址到物理地址映射的核心数据结构,采用 PTE(页表条目)数组形式实现地址空间转换。每个 PTE 包含有效位和 n 位地址字段,有效位用于标识对应虚拟页是否已缓存至 DRAM。虚拟地址被划分为两部分:虚拟页号(VPN,Virtual Page Number)用于索引 PTE 数组,虚拟页偏移量(VPO,Virtual Page Offset)直接映射为物理地址偏移量(PPO)。
为加速地址转换过程,MMU(内存管理单元)内置 TLB(Translation Lookaside Buffer,地址转换后备缓冲器)作为 PTE 的高速缓存。TLB 是一种小型、虚拟寻址的高速缓存,采用高度相联设计,每行存储单个 PTE 块。这种设计显著提升了地址翻译效率,而多级页表结构则通过分层索引机制实现页表空间压缩,适应大规模地址管理需求。
VA 到 PA 的转换流程如下:
TLB 查询阶段:MMU 首先利用 VPN 检索 TLB,若命中则直接获取 PTE,跳过后续步骤;
内存访问阶段:若 TLB 未命中,MMU 根据多级页表结构生成 PTE 地址,从主存或高速缓存读取对应 PTE;
有效性检查阶段:检查 PTE 有效位,若为 0 则触发缺页异常;
缺页处理阶段:操作系统介入,选择牺牲页(若页面已修改则写回磁盘),调入新页面,更新 PTE;
地址合成阶段:将 PTE 中的物理页号(PPN)与 VPO 拼接,生成最终物理地址。
四级页表机制作为多级页表的典型实现,进一步优化了页表空间利用率。在这种结构中,36 位 VPN 被拆分为 4 个 9 位索引(VPN1~VPN4),对应四级页表层次:
CR3 寄存器存储 L1 页表基址,VPN1 作为偏移量定位 L1 PTE;
L1 PTE 指向 L2 页表基址,VPN2 索引 L2 PTE;
依此类推,L3 PTE 指向 L4 页表,VPN4 定位最终 L4 PTE;
L4 PTE 包含物理页号(PPN),与 VPO 组合形成完整物理地址。
这种分层索引机制将连续的页表空间离散化存储,仅在需要时分配下级页表,有效减少了空闲页表项占用的内存空间。以 Intel Core i7 为代表的现代处理器广泛采用这种设计,在保证地址转换效率的同时,大幅降低了页表内存开销。
7.5 三级Cache支持下的物理内存访问
在计算机存储系统的数据访问流程中,依据内存地址的组索引信息检索对应数据。当索引获取的数据为普通数据类型时,系统定向至 L1 数据缓存(L1 d-cache)的相应组进行查找;若数据属于指令范畴,则转而访问 L1 指令缓存(L1 i-cache)对应组。
具体查找过程中,系统会对 L1 缓存指定组内的所有行进行遍历,逐一比对标记位信息。若某行标记位与目标地址标记完全匹配,且该行有效位状态为 1,则判定为缓存命中。此时,系统依据地址中的偏移量字段,精准定位并提取所需字节数据。若遍历完 L1 缓存组内所有行均未满足上述命中条件,则判定为缓存未命中。在此情况下,系统将按照缓存层级架构,依次向下一级缓存发起查找请求。当各级缓存均无法命中时,最终将访问请求转发至物理内存,完成数据读取操作。
7.6 hello进程fork时的内存映射
当fork函数被触发执行,内核会为新生成的进程构建一系列关键数据结构,并赋予其独一无二的进程标识符(PID)。在虚拟内存创建环节,内核采取了特殊策略:将当前进程的mm_struct、区域结构以及页表进行完整复刻,为新进程构建初始虚拟内存环境。同时,内核将两个进程中所有页面标记为只读属性,并且将每个区域结构设定为私有且启用写时复制(Copy-on-Write, CoW)机制。
当fork函数在新进程中返回时,新进程的虚拟内存布局与fork调用瞬间的原进程虚拟内存完全一致。在后续执行过程中,只要两个进程中的任意一方尝试对共享页面进行写操作,写时复制机制便会立即介入,创建全新的物理页面。通过这种方式,系统能够在保障数据共享效率的同时,维持每个进程私有地址空间的独立性与完整性。
7.7 hello进程execve时的内存映射
execve函数通过调用内核中的启动加载器代码,实现将可执行目标文件hello加载并运行于当前进程环境,从而替换原有的程序执行内容。这一过程主要包含以下核心步骤:
清理原有用户区域:系统首先移除当前进程虚拟地址空间中已存在的用户区域结构,释放对应资源,为新程序的加载腾出空间。
映射私有内存区域:针对新程序的代码、数据、未初始化数据(bss)以及栈空间,分别创建独立的区域结构。这些新区域均采用私有且写时复制的属性进行管理。其中,代码和数据区域直接映射至hello文件中的.text和.data段;bss 段则通过映射匿名文件实现,其大小由hello文件预先指定;栈和堆区域同样基于匿名文件映射,初始长度为零,后续根据实际需求动态扩展。
映射共享内存区域:由于hello程序在链接过程中引用了共享对象libc.so,系统会将该共享库映射至用户虚拟地址空间的共享区域,确保程序能够正常调用共享库中的函数资源。
重置程序计数器:execve执行的最后一步是更新当前进程上下文的程序计数器(PC),使其指向新程序代码区域的入口地址,为程序的正确执行奠定基础。
7.8 缺页故障与缺页中断处理
缺页故障:当 CPU 尝试访问的虚拟页尚未缓存在 DRAM 中,即发生 DRAM 缓存未命中的情况,这一现象被称为缺页。具体而言,当 CPU 访问页表条目时,如果该条目未被缓存至 DRAM,地址翻译硬件会从内存中读取对应的页表条目。一旦检测到页表条目中的有效位为 0,便可以判定该虚拟页尚未被缓存,进而触发缺页异常。
缺页中断处理:缺页异常触发后,系统将调用缺页异常处理程序进行处理。该程序首先会从现有物理页面中选择一个作为牺牲页。若牺牲页在 DRAM 中已被修改,处理程序会先将其内容写回磁盘进行保存,以确保数据一致性。随后,系统将所需的虚拟页从磁盘复制到内存中原本牺牲页的位置,并同步更新对应的页表条目。处理完成后,异常处理程序返回,重新启动引发缺页的指令。此时,由于目标虚拟页已成功缓存至主存,地址翻译硬件能够正常完成页命中处理,使程序得以继续执行。
图37 缺页异常处理流程
7.9动态存储分配管理
动态内存管理主要有隐式空闲链表和显式空闲链表两种基本方法与策略,具体如下:
隐式空闲链表
数据结构:隐式空闲链表通常为每个块设置头部和脚部标签,标签内容相同,用于记录块的大小和分配状态等信息。每个块一般由头部、脚部、有效载荷和可能的额外填充组成,对于一些经过优化的链表,已分配的块可以不设置脚部。所有块通过头部和脚部中的大小字段连接,分配器可借此依次遍历整个堆,以一个设置了已分配位且大小为零的终止头部作为结束块的特殊标记。
分配策略:当应用请求一个 k 字节的块时,分配器会搜索空闲链表,寻找能放置所请求块的足够大的空闲块,有首次适配、下一次适配和最佳适配三种放置策略。分配完成后,可通过分割空闲块来减少内部碎片。
回收策略:在释放已分配块时,分配器会利用隐式空闲链表的边界标记来合并相邻的空闲块,以提高内存利用率。
显式空闲链表
数据结构:将空闲块组织成某种显式数据结构,通常是双向链表。每个空闲块中包含前驱和后继指针,用于在链表中定位相邻的空闲块。
维护策略:可以按后进先出的顺序维护链表,把最新释放的块放在链表开头,这样分配内存时优先使用新释放的块,能提高局部性。也可按照地址顺序维护链表,即链表中每个块的地址都小于它的后继地址,不过这种方式下释放一个块需要线性时间搜索来定位合适的前驱。
7.10本章小结
本章围绕 Hello 程序与操作系统的交互机制展开,深入剖析了程序运行过程中关键的内存管理技术。首先,详细探讨了 Hello 程序的存储器地址空间布局,结合 Intel 架构下的段式管理策略,解析了逻辑地址到线性地址的转换过程;进而引入页式管理机制,揭示了线性地址如何通过分页系统映射为物理地址的完整流程。
在地址翻译技术方面,着重阐述了 TLB(地址转换后备缓冲器)如何通过高速缓存页表条目,显著提升地址转换效率;同时对多级缓存架构进行分析,说明其如何协同工作以优化数据访问性能。此外,还系统介绍了动态内存管理的核心要点,包括内存分配与回收策略,以及如何应对程序运行过程中的内存需求变化。
最后,聚焦于动态存储分配管理机制,从底层原理出发,阐释了操作系统如何高效管理内存资源,确保 Hello 程序及其他进程在运行时能够安全、有效地使用内存,从而保障整个系统的稳定运行与性能优化
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
在 Linux 系统中,IO 设备管理依托于两大核心设计理念 —— 设备的模型化与统一的设备管理接口,通过巧妙的抽象设计实现对各类硬件设备的高效管控。
设备的模型化:文件
Linux 系统采用 “一切皆文件” 的设计哲学,将所有 IO 设备抽象为文件模型。无论是磁盘、键盘、显示器等硬件设备,还是管道、套接字等软件资源,均被视作特殊文件。这种抽象方式赋予设备与普通文件一致的操作语义,用户和应用程序可以使用相同的文件操作接口(如open、read、write、close)对设备进行访问 。例如,对磁盘设备的读写操作与普通文件的读写并无本质差异,通过文件描述符即可完成数据交互。这种模型化设计极大简化了应用开发,开发者无需针对不同设备编写专属驱动接口,降低了编程复杂度,同时增强了系统的通用性和可移植性。
设备管理:Unix IO 接口
Linux 系统沿用 Unix IO 接口作为设备管理的统一规范。Unix IO 接口通过一系列系统调用(system call)实现对设备的操作与控制,这些接口主要包括:
打开与关闭设备:open系统调用用于初始化设备连接,获取文件描述符,建立应用与设备的通信通道;close系统调用则用于释放设备资源,关闭连接 。
数据读写操作:read系统调用从设备读取数据至内存缓冲区,write系统调用将内存缓冲区中的数据写入设备。例如,对网络设备的数据包收发、对串口设备的字符传输,均可通过这两个接口完成。
设备控制:ioctl系统调用提供了设备特定的控制功能,用于配置设备参数、获取设备状态等。例如,调整网卡的 MTU(最大传输单元)、设置串口的波特率等操作,均需借助ioctl实现 。
通过 Unix IO 接口,Linux 内核将复杂的设备操作细节封装在内核空间,应用程序仅需通过标准接口即可访问各类设备。同时,内核通过文件系统、驱动程序等模块协同工作,实现对设备资源的分配、调度和保护,确保多任务环境下设备访问的安全性与高效性。这种分层管理架构使得 Linux 系统既能兼容丰富的硬件设备,又能维持简洁统一的编程接口,成为现代操作系统设备管理的经典范式。
8.2 简述Unix IO接口及其函数
open函数 函数原型:int open(const char *pathname, int flags, …)
功能描述:该函数用于打开或创建一个文件。在实际应用中,无论是访问普通文件、设备文件,还是管道、套接字等特殊文件类型,open函数都是建立访问通道的第一步。
参数详解:
pathname:表示待操作文件的路径名称,支持绝对路径与相对路径两种形式。
flags:是一系列用于指定文件打开方式的标志参数。例如,O_RDONLY表示以只读模式打开文件,O_WRONLY表示以只写模式打开,O_RDWR表示以读写模式打开。此外,还包含O_CREAT(文件不存在时创建新文件)、O_TRUNC(打开时截断文件内容)等特殊标志。
mode:该参数为可选参数,仅在创建新文件时生效,用于设置文件的访问权限,常见取值如0644(表示所有者可读可写,组用户和其他用户可读)。
返回值:执行成功时,返回一个非负整数作为文件描述符,后续可用于其他 I/O 操作;执行失败时,返回-1,并设置errno变量记录具体错误原因。
close函数 函数原型:int close(int fd)
功能描述:close函数用于关闭已打开的文件描述符,释放与该文件描述符关联的系统资源,如文件表项、文件锁等。当程序不再需要对文件进行读写操作时,应及时调用close函数关闭文件描述符,避免资源泄露。
参数详解:fd代表需要关闭的文件描述符,该描述符通常由open函数返回。
返回值:操作成功时返回0;若失败则返回-1,同时errno会被设置为对应的错误码,如EBADF表示传入的文件描述符无效。
read函数 函数原型:ssize_t read(int fd, void *buf, size_t count)
功能描述:从指定的文件描述符fd所关联的文件或设备中,读取最多count字节的数据,并将其存储到缓冲区buf中。在读取过程中,遇到文件末尾或设备数据传输结束时,实际读取的字节数可能小于count。
参数详解:
fd:为已打开的文件描述符,标识数据读取的来源。
buf:是用于存放读取数据的缓冲区,其类型为void *,可适配各种数据类型的存储需求。
count:指定了本次读取操作期望读取的最大字节数。
返回值:成功执行时,返回实际读取的字节数;若返回0,表示已到达文件末尾;若返回-1,表示读取操作失败,errno会记录具体的错误信息,如EAGAIN表示非阻塞模式下无数据可读。
功能描述:将缓冲区buf中连续的count字节数据写入到文件描述符fd对应的文件或设备中。写入操作的目标可以是普通文件、管道、套接字等支持写操作的实体。
参数详解:
fd:指定数据写入的目标文件描述符,该描述符必须是以可写模式打开。
buf:存储待写入数据的缓冲区,由于数据在写入过程中不会被修改,因此参数类型为const void *。
count:表示要写入的字节数。
返回值:成功执行后,返回实际写入的字节数;若返回-1,则表明写入操作失败,errno会记录具体的错误原因,例如EBADF表示文件描述符无效,ENOSPC表示设备空间不足 。
8.3 printf的实现分析
int printf(const char *fmt,…)
{
int i;
char buf[256];
va_list arg = (va_list)((char*)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
printf工作流程 printf按格式fmt结合参数args生成格式化字符串。先借助vsprintf依据fmt对可变参数格式化,存入缓冲区;再用write将缓冲区内容输出到终端等设备 。write接收文件描述符、数据缓冲区、字节数,成功返回写入字节数,失败返回 -1 并设errno 。
系统调用 调用write会触发系统调用,如通过int 0x80或syscall指令进入内核态,内核执行实际写入操作。
字符显示底层 字符显示驱动把 ASCII 转为字模,存入显示vram。显示芯片按刷新频率读vram,向显示器传点的 RGB 信息来显示字符
从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从键盘缓冲区读取 ASCII 码,仅在读取到回车符时才会返回结果。read函数执行时,会一次性将键盘缓冲区中的所有内容读入程序的缓冲区。倘若程序缓冲区原本就存有数据,便不会再次调用read函数,而是直接返回缓冲区最前端的字符。
在处理异步异常方面,键盘中断起着关键作用。键盘中断处理子程序负责接收按键产生的扫描码,并将其转换为 ASCII 码,随后把转换后的 ASCII 码存储至系统的键盘缓冲区中。
像getchar这类函数,在调用read系统函数时,需通过系统调用的方式从键盘缓冲区读取按键对应的 ASCII 码,直至接收到回车键,才会结束读取并返回相应结果。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章围绕 hello 程序的 I/O 管理机制展开。开篇阐述了 I/O 设备被抽象为文件这一特性,接着讲解 I/O 设备管理手段 ——Unix IO 接口。对 Unix IO 接口进行说明后,又介绍了其相关函数。基于此,进一步剖析了 printf 和 getchar 函数的实现原理。
(第8章1分)
结论
预处理阶段:借助 cpp 预处理器对 hello.c 文件进行处理,完成宏替换、头文件嵌入等操作,生成扩展后的源代码文件 hello.i。
编译阶段:编译器将 hello.i 文件翻译为汇编语言程序 hello.s,实现高级语言到汇编语言的转换。
汇编阶段:汇编器 as 对 hello.s 进行处理,将汇编指令转化为机器码,生成可重定位目标文件 hello.o。
链接阶段:链接器整合多个可重定位目标文件,解决符号引用等问题,最终生成可执行目标文件 hello。
进程创建与加载:在 shell 中输入指令后,shell 调用 fork 函数创建 hello 的子进程,随后 execve 函数加载并运行 hello 程序,将其映射到虚拟内存区域,按需载入物理内存。
输入输出交互:程序通过 printf、getchar 等函数实现输入与输出功能,与 Linux 系统的 I/O 设备进行交互,完成数据的读取与展示。
指令执行与回收:程序中的每条指令在 CPU 流水线中依次执行,执行结束后,父进程对其进行资源回收,内核将其从系统中清除,完成程序生命周期。
上述流程构成了 Hello 程序完整的生命周期,计算机系统设计与实现中,保障程序执行准确性是优化性能的基石,缓存、流水线、超标量等设计均以此为依托。
此次实践如同对本学期知识的全景复盘,让我深入理解计算机系统设计与实现的精妙之处。从代码到程序运行,硬件与软件环环相扣、精密协作,展现出计算机科学独特的魅力,也激发我在专业领域不断探索、追求真理的热情。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.
[7] Randal E.Bryant David R.O'Hallaron.深入理解计算机系统(第三版).机械工业出版社,2016.
[8] https://www.cnblogs.com/pianist/p/3315801.html
(参考文献0分,缺失 -1分)
评论前必须登录!
注册