计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 未来技术模块
学 号 2023111680
班 级 23WLR14
学 生 姜虹伯
指 导 教 师 吴锐
计算机科学与技术学院
2025年5月
本文以“程序人生-Hello's P2P”为主题,系统探究了Hello程序从代码编写到执行的全生命周期,深入分析了计算机系统中各层次的核心机制。通过GCC工具链对Hello程序进行预处理、编译、汇编和链接,生成可执行文件,并结合ELF格式解析、进程管理、存储管理及异常处理等理论,揭示了程序在操作系统与硬件协作下的运行原理。具体包括:
预处理阶段:宏替换与头文件插入生成.i文件;
编译阶段:将C代码转换为汇编指令,解析机器级表示;
汇编与链接:生成可重定位目标文件.o,并通过动态链接生成可执行文件;
进程管理:探讨Shell的fork与execve机制,分析进程创建、上下文切换及信号处理;
存储管理:解析逻辑地址到物理地址的转换过程,包括段式管理、页式管理及TLB优化;
异常处理:验证Ctrl-C(SIGINT)和Ctrl-Z(SIGTSTP)对进程的控制作用。
实验表明,程序的高效执行依赖于操作系统对资源的精细化调度与硬件支持,体现了计算机系统分层设计的协同性与复杂性。本文通过理论与实践结合,为理解程序底层运行机制提供了完整案例。
关键词:预处理;编译;链接;进程管理;虚拟内存;ELF格式;异常信号
(摘要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的execv程.............................................................. - 10 -
6.5 Hello的进程e过执行...................................................... - 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简介
Hello这个程序在敲入hello.c这几个字符开始,便走上了一条辉煌的计算机系统之路。在这段程序中,首先进入程序的入口点main,并接受命令行参数argc和argv。这些参数传入的数值由操作系统的外壳Shell解析和处理,Shell会通过fork系统调用创建一个新进程,将执行权交给它。程序随后进入了预处理、编译、汇编和链接阶段,最终成为一个可以在操作系统管理下运行的可执行文件。在运行过程中,程序会通过操作系统的存储管理(如虚拟地址到物理地址的映射)和内存管理单元MMU在硬件上得到调度,进程的执行由操作系统分配时间片,CPU根据指令流水线逐条执行,数据在RAM和Cache之间传递,高效地完成指令的取出、译码和执行。虽然程序的输出在屏幕上显示的只是一个简单的Hello,但其背后所涉及的硬件和软件协作却是复杂而高效的,IO管理、信号处理以及进程调度等机制确保了程序的流畅执行。最终,程序完成使命后,操作系统将回收资源,释放内存,进程结束,而你在屏幕上看到的,只是程序在短暂而完美的生命周期中的一瞬间。整个过程,正如O2O(From Zero-0 to Zero-0),从无到有,再从有到无,展示了计算机系统的辉煌与伟大。
1.2 环境与工具
硬件信息:
CPU:12th Gen Intel(R) Core(TM) i9-12900H
内存:16G
显卡:RTX3060
操作系统:
Window11
开发环境:
Linux下的VS code,Windows下的Codeblocks
运行环境:
gccs
1.3 中间结果
文件名 | 功能 |
hello.c | 源程序 |
hello.i | 预处理后的文本文件 |
hello.s | 编译后产生的汇编语言文件 |
hello.o | 汇编后产生的二进制可重定位目标文件 |
hello | 链接后的可执行文件 |
hello_elf.txt | hello的ELF格式文件的文本 |
hello_obj.txt | hello的反汇编文件的文本 |
1.4 本章小结
本章简单概述了hello.c文件的内容,完成本次大作业所需的环境与工具,及列出了完成本论文过程中所生成的中间结果文件及其作用。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:
预处理是编译过程的第一步,主要任务是对源代码中的宏定义、文件包含、条件编译等指令进行处理。
预处理的作用:
它可以处理代码中的宏定义,将宏名称替换为其定义的内容;将 #include 指令引用的头文件内容插入到源文件中;根据条件编译指令,决定哪些代码段需要编译,哪些代码段被忽略;将宏定义的函数替换为其对应的代码;处理文件引用中的路径问题,确保文件正确引用等。
通过这些操作,预处理器就将源代码处理为一个干净的、适合编译器进一步处理的文件。
2.2在Ubuntu下预处理的命令
使用gcc -E hello.c -o hello.i在ubuntu下进行了预处理,转换成了预处理后的输出文件,如图1。
图1 预处理操作
2.3 Hello的预处理结果解析
对于hello.i文件,我在打开之后发现其中我标记的注释被删去了,hello.i中会将代码中所有的宏进行替换(如果有宏),同时将头文件的所有内容插入进来。
2.4 本章小结
本章中我进行了对hello.c的预处理,将它转换为了.i文件即预处理后的输出文件,对一个代码进行预处理代表它走进了编译的第一步。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念:
编译是将源代码转换成计算机能够执行的机器代码或中间代码的过程。它是软件开发中的关键步骤,目的是将程序员编写的高层次代码转化为机器能够理解和执行的低层次代码。
编译的作用:
它可以将源代码转换成机器可以执行的代码,使得程序可以在计算机上运行;编译过程中的各个阶段能够帮助检测代码中的错误,并给出详细的错误信息,帮助程序员在编译时就发现并修复问题;编译器通过优化过程,能够提升程序的性能,减少运行时间;编译还可以生成不同平台的目标代码,使得同一份源代码可以在不同的操作系统或硬件平台上运行。
3.2 在Ubuntu下编译的命令
使用gcc -S hello.i -o hello.s对hello.i进行编译,将其变为hello.s,操作如图2。
图2 编译操作
3.3 Hello的编译结果解析
使用vim打开hello.s,这部分就是汇编代码,下面对汇编代码进行分析。
3.3.1 工作伪指令
首先是main函数前的工作伪指令部分如图3,此部分是一些以.开头的代码段,它们并不直接对应机器指令。它们通常由汇编器在编译过程中转换为一组实际的机器指令。工作伪指令通常用于简化编程,提供更高层次的抽象,以便程序员编写更易于理解和调试的代码。
伪指令不直接映射到 CPU 的硬件指令上,而是汇编器为了提高程序编写的便利性而引入的伪命令。它们在编译时会被转换为实际的机器指令,通常会由多个硬件指令组成,包括数据定义伪指令(.data,.word,.byte,.space)、跳转伪指令(bne,beq,b)、栈操作伪指令(move)、操作系统调用伪指令(syscall)、常用计算伪指令(li,la)等。
图3 main函数伪指令部分
本次论文中出现的伪指令及其含义如下表1所示:
伪指令 | 含义 |
.file | 声明源文件(hello.c) |
.text | 声明代码节 |
.section | 文件代码段 |
.rodata | Read-only(只读文件) |
.align | 数据指令地址对齐方式(此处为8对齐) |
.string | 声明字符串(此处声明了LC0和LC1) |
.globl | 声明全局变量(main) |
.type | 声明变量类型(此处声明为函数类型) |
表1 本文出现的伪指令
3.3.2 入栈与出栈
入栈使用pushq指令,出栈用popq指令。pushq可以将四个字压入栈,popq可以将几个字弹出栈。
3.3.3 数据传送指令
数据传送指令是程序中最为常见的指令,使用MOV指令对数据进行传送,本次所用代码主要出现了movq、movl指令,除此之外较为常用的还有movb、movzx等,下表举出部分例子。
传送指令 | 作用 |
mov | 传输数据到寄存器或内存中 |
movl | 用于32位长字数据的移动 |
movw | 用于16位数据的移动 |
movb | 用于8位字节数据的移动 |
movq | 用于64位数据的传输(常用于64位系统) |
movzx | 将较小的数据类型零扩展到更大的寄存器中 |
movsx | 用于将带符号的小数据类型扩展到更大的寄存器 |
movaps | 用于浮点数数据的移动, |
movsd | 用于双精度浮点数的移动 |
表2 数据传送举例
3.3.4 数据
立即数:
立即数是指在程序中直接写入的常数值,它不依赖于内存或寄存器的地址,通常作为操作数直接嵌入指令中。它与数据的存储位置无关,通常用于简单的数值赋值、运算或其他操作。立即数在汇编代码中通常以$为标志,如图4所示。
图4 立即数举例
寄存器变量:
寄存器变量是指在程序中声明的一种特殊变量,旨在指示编译器将该变量存储在CPU的寄存器中,而不是内存中。由于寄存器的访问速度远高于内存,因此将变量存储在寄存器中可以提高程序的执行效率。
字符串:
.LC0和.LC1作为两个字符串变量被声明,如图5所示。
图5 字符串举例
3.3.5 数据格式
介绍一下各种数据格式,Intel中16bytes为字,21bytes为双字。
变量类型 | Intel数据类型 | 汇编代码后缀 | 大小(字节) |
char | 字节 | b | 1 |
short | 字 | w | 2 |
int | 双字 | l | 4 |
long | 四字 | q | 8 |
char * | 四字 | q | 8 |
float | 单精度 | s | 4 |
double | 双精度 | l | 8 |
表3 数据格式
3.3.6 算术操作
算术运算也是十分常用的一些指令类,部分指令如下表。
指令 | 作用 |
ADD A,B | 将寄存器B的值加到寄存器A中,结果存储在A中 |
SUB A, B | 将寄存器B的值从寄存器A中减去,结果存储在A中 |
MUL B | 寄存器A与寄存器B中的值相乘,结果存储在AX和DX中 |
IMUL A, B | 将寄存器A与寄存器B中的值相乘,结果存储在AX和DX中 |
DIV B | 将AX寄存器的值除以B寄存器的值,商存储在AX中,余数存储在DX中 |
IDIV B | 将AX寄存器中的值除以B寄存器的值,商存储在AX,余数存储在DX |
INC A | 将A寄存器中的值加1 |
DEC A | 将A寄存器中的值减1 |
表4 算术运算
3.3.7 跳转语句
跳转指令会根据条件码当前的值来进行相应的跳转。比较常见的是直接跳转,在hello.s中也有体现,本文中出现了je,如果此时的条件码所表示含义为相等,则会跳转到相应的指令行,跳转指令常用于条件分支。
3.3.8 函数调用
Call指令用来进行函数调用,如下图6所示,call调用了print函数,它会先将函数的返回地址压入运行时栈中,之后跳转到相应的函数代码段进行执行。执行结束通过ret指令返回。
图6 函数调用举例
3.3.9 条件控制
汇编语言中,一些指令会改变条件码,例如cmpl指令和setl指令。这种指令一般不会单独使用,会根据比较结果进行后续操作,如下图7所示。
图7 条件控制举例
此图中将将寄存器中存储的值和立即数5进行比较,设置条件码,然后进行跳转或者其他操作。
3.3.10 逻辑操作
逻辑操作常见的有两类,一类是加载有效地址,一类是位移操作。加载有效地址指令类似于MOV类指令,它的作用是将有效地址写入到目的操作数,相当于C语言中大家所熟知的取址操作,可以为以后的内存引用产生指针。位移操作顾名思义就是将二进制数进行整体左移或者右移,如下图8所示。
图8 逻辑操作举例
3.4 本章小结
在本章中,我对hello.i文件进行编译,将其转为了hello.s文件,并查看了hello的机器级表示,同时也对汇编指令做出了一些简单的介绍。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:
汇编是将汇编语言程序翻译成计算机可以理解并执行的机器语言的过程。汇编器是完成这种翻译工作的软件工具,它将程序员编写的汇编代码转化为对应平台的机器指令。
汇编的作用:
- 汇编将人类可读的汇编语言代码转化为机器语言代码,使得计算机能够直接执行。
- 汇编过程将汇编代码转换为目标文件.obj,然后再通过链接器将多个目标文件合并生成最终的可执行文件如.exe文件等。
3.汇编还可以生成机器指令,这些指令可以直接控制硬件。
4.2 在Ubuntu下汇编的命令
使用gcc hello.s -c -o hello.o指令进行汇编,如图9所示。
图9 汇编操作
4.3 可重定位目标elf格式
4.3.1 ELF头
首先使用指令readf -h hello.o查看可重定位目标文件的elf形式的ELF头,如下图10所示。
图10 ELF头
接下来对ELF头进行分析。
ELF头首先是一个16个字的Magic,它描述了生成文件的系统的字的大小及字节的顺序,magic首先包含四个字节0x7f,这是ELF文件格式的标志,确保文件以“7F”开始,后面的ELF表明文件是一个ELF格式文件。剩下部分的信息包含帮助连接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型和机器类型等,可从上图看出系统使用小端法,文件为类型为REL(可重定位文件)等。
4.3.2 节头
接下来使用readelf -S hello.o查看节头。
节头由节和节头部表组成,首先是节,它包含了文件中出现的各个节的语义,包含节的类型、位置和大小等信息如下图11所示,接下来列出其各部分的含义如表5所示。
图11 节头
名称 | 包含内容含义 |
.text | 已编译程序的机器代码 |
.rodata | 只读数据 |
.data | 已初始化的全局和静态C变量 |
.bss | 未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量 |
.symtab | 一个符号表,存放一些在程序中定义和引用的函数和全局变量的信息 |
.rel.text | 一个.tex节中位置的列表 |
.rel.data | 被模块引用或定义的所有全局变量的重定位信息 |
.debug | 一个调试符号表 |
.line | 原始C源程序中的行号和.text节中机器指令之间的映射 |
.strtab | 一个字符串表(包括.symtab和.debug节中的符号表) |
表5 各节的含义
4.3.3 符号表
使用命令readelf -s hello.o查看符号表,如下图12所示:
图12 符号表
其中Num表示符号的编号,Name是符号的名称。Size表示它是一个位于.text节中偏移量为0处的163字节函数。Bind是指这个符号是本地的还是全局的,Vis表示符号的可见性,DEFAULT意味着符号具有默认的可见性设置,Ndx表示索引字段,用于标记符号所关联的段或节,UND 表示符号是未定义的,通常需要在链接时解析,ABS 表示符号的地址是绝对的,不需要进行重定位,数字值如 1 和 5 表示符号所在的具体段索引,指向符号所在的内存区域。
4.3.4 可重定位信息
使用readelf -r hello.o查看可重定位信息,如下图13所示。
图13 可重定位信息
里面包含需要被修改的引用的节偏移、信息、类型、被修改引用应该指向的符号、符号名称+加数,加数是一个有符号常数,一些类型的重定位要使用它对被修改的引用的值做偏移调整。
4.4 Hello.o的结果解析
使用objdump -d -r hello.o对hello.o进行反汇编,得到反汇编结果如下图14:
图14 反汇编结果
得到的反汇编文件中的汇编代码与hello.s中的汇编代码相同,还包含了机器语言。
机器语言的构成:
机器语言本质上是计算机能直接执行的代码,它是二进制指令,通常以16进制的形式表示,是计算机硬件层面上操作的语言,机器语言是硬件执行的最终指令。
通常机器码包含操作码和操作数两个部分,操作码告诉计算机执行什么样的操作,操作数指定操作对象例如寄存器、内存位置、立即数等。
机器语言与汇编语言的映射关系:
在汇编语言和机器语言之间的映射关系中,汇编语言指令需要经过汇编程序的转换,才能变成机器能够执行的二进制机器码。例如,汇编指令中的操作数可能是寄存器、内存地址或常量,而这些在机器码中都以特定的二进制形式进行编码。分支指令、跳转指令和函数调用等汇编指令在转换成机器码时,通常会涉及到内存地址、偏移量或栈操作。机器语言与汇编语言不一致的地方主要在下面几个方面:
1.分支与转移指令:
在汇编语言中,分支指令通常使用符号化的目标地址或标签,例如 JMP 0x1000 或 JMP LABEL,这些标签或者地址通常是相对地址或绝对地址。但是,机器语言中的分支指令通常使用相对偏移量来表示跳转目标。即使汇编语言中使用的是绝对地址,在机器语言中,跳转指令的偏移量通常是相对于当前指令的位置。这种转换需要在编译时进行地址计算,以确保跳转正确。
2.函数调用与返回
在汇编语言中,函数调用指令会指定目标函数的地址,而函数返回指令会从栈中弹出返回地址。这些指令中的操作数通常是符号化的,即目标地址或标签。然而,机器语言中的调用指令通常会包含目标地址的偏移量或直接的内存地址。栈操作也是在机器语言中由特定的指令和二进制编码来实现的,汇编语言中的栈操作符号(如 PUSH)也会被转化为相应的机器码操作。
3.相对地址与绝对地址的转换
汇编语言中的跳转指令和函数调用指令,可以使用绝对地址或标签。这些地址或标签在汇编时通常会被解析为具体的目标地址。然而,机器语言中的指令一般要求使用相对地址或偏移量。这意味着跳转和调用指令的机器码中,目标地址实际上是相对当前指令位置的偏移量,而非一个固定的绝对地址。为了转换为正确的机器码,编译器需要根据程序的当前位置计算偏移量。
总而言之,汇编语言中的操作数更加可读且直观,而机器语言则要求更为精确的内存位置、偏移量或二进制值。
4.5 本章小结
在本章,进行了将hello.s转换为hello.o的过程。对hello.o的ELF头、节头、符号表、可重定位信息进行了分析,同时也对其反汇编文件进行了分析,解释了为什么hello.o文件相对更让机器易于理解。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接的概念
链接是在编译之后、程序执行之前进行的过程。它的核心任务是将一个程序中不同的代码模块(如函数、变量等)和库文件连接在一起,形成最终的可执行文件。链接的目标是确保程序中各个部分能够相互访问和调用。
链接的作用:
1.符号解析: 在程序开发过程中,程序员编写的代码通常会调用其他模块中的函数或使用其他模块中的变量。链接的一个重要作用是将这些符号(如函数名、变量名)与具体的内存地址绑定在一起,确保程序中的每个符号都能够找到对应的地址。
2.库文件的连接: 程序中可能会使用到一些外部的库文件(如标准库或第三方库)。链接过程会将这些库文件中的代码和函数与程序代码合并,确保程序能够访问这些库提供的功能。
3.地址分配: 链接还负责将程序各个部分的内存地址分配好。在程序的每个模块被编译时,它们的地址可能是相对的(未确定的)。链接会根据程序的需求,将每个模块的符号地址转化为实际的内存地址,从而确保程序运行时能正确找到和访问这些地址。
4.生成可执行文件: 链接的最终结果是一个可执行的二进制文件,计算机可以直接运行。链接器会将所有的目标文件(包括程序模块和库文件)整合成一个完整的可执行文件,其中包含了程序的所有代码和数据。
5.2 在Ubuntu下链接的命令
使用了ld的链接命令对其进行链接,命令如下:
ld -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 /usr/lib/gcc/x86_64-linux-gnu/9/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello
链接结果如下图15所示:
图15 链接操作
5.3 可执行目标文件hello的格式
将hello的elf格式文件存入hello_elf.txt中,打开hello_elf.txt文本文件查看各段的基本信息。
5.3.1 ELF头
ELF头如图16所示,Hello文件的ELF头和hello.o文件的ELF头与提供的内容基本相同,而hello文件的ELF头将文件类型改为了EXEC可执行文件,并且为程序分配了入口点地址和程序头起点,其余无差别。
图16 ELF头
5.3.2 节头
节头如图17所示所示:
图17 节头
节头部分展示了文件中各个节的名称,大小,类型,地址和偏移量等信息,而链接后的各个节的地址信息相较之前更加具体完善。
5.3.3 程序头
程序头部分如图18所示,它是一个结构数组,描述了系统准备程序执行所需的段或其他信息。
图18 程序头
5.3.4 段节
图19 段节
它展现了ELF文件中的节信息和段信息,如图19所示。
5.3.5 重定位节
图20 可重定位节
链接后,重定位部分的各个符号的偏移量更加明确,类型也更加具体,符号名称也发生了改变。
5.3.6 符号表
符号表部分如图21所示:
图21 符号表
与之前相比增加了很多符号,此处包含符号的的定义和引用。
5.4 hello的虚拟地址空间
使用gdb/edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
使用gdb指令加载hello查看各段信息如图22所示:
图22 hello各段信息
由上图可以看到ELF格式可执行文件不同段及其内存地址范围,包含每个段特定的功能和作用。
5.5 链接的重定位过程分析
hello与hello.o的不同:
1.增加了一些hello程序中需要调用的外部函数,放入.plt.sec节中,如printf,getchar,atoi,exit,sleep等
图23 hello的部分函数部分
2.增加了.init节,.plt节,存放init函数和plt函数。
图24 hello的.init节和.plt节
3. hello.o中的相对偏移量地址变成了hello中的虚拟内存地址。
图25 hello中的相对偏移量变化
4.函数调用的地址从hello.o中的相对偏移量地址变成了函数调用地址。
图26 函数调用的地址
链接的过程:
通过上面的比较可以看出,链接就是将外部函数进行调用,并将其进行重定位。
重定位的实现:
在hello.o的elf文件中给出了函数相对elf头的相对偏移量。而hello的elf头中给出了程序首地址,根据首地址和相对偏移量即可对所有的函数进行重定位。
5.6 hello的执行流程
hello中所有子进程及其程序地址如下图27所示:
图27 hello中所有子进程及其程序地址
5.7 Hello的动态链接分析
动态共享库是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,兵和一个程序链接起来,这个过程就是动态链接。
把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
.plt:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
.got:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
hello在动态连接器加载前后的重定位是不一样的,在加载之后才进行重定位。
图28 hello的.plt
.plt如图28,如下图29是.got相关值的获取:
图29 .got的相关值
5.8 本章小结
本章介绍了连接的过程。解释了程序是如何进行重定位的操作,把相同类型的数据放在同一个节的过程,同时也说明了链接的工作原理。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:
进程是计算机中执行程序的基本单位,是程序在计算机中的一次执行过程。每个进程都由操作系统管理,并拥有自己的独立地址空间、内存和资源。一个进程可以包含多个线程,这些线程共享进程的资源。
进程的作用:
1.资源管理: 操作系统通过进程管理来分配、回收计算机的各种资源,如CPU、内存、文件等。
2.并发执行: 通过进程,操作系统能够实现程序的并发执行,即使是多个程序在同一时间运行,也能让它们互不干扰。
3.保护与隔离: 每个进程拥有独立的地址空间和资源,避免了一个进程崩溃影响到其他进程的执行。
4.调度与协调: 操作系统可以根据进程的优先级、资源需求等,进行合理的进程调度,确保系统高效运行。
6.2 简述壳Shell-bash的作用与处理流程
Shell是操作系统的命令解释器,它提供了用户与操作系统之间交互的接口。Bash是一种常用的Shell,它提供了命令行界面,允许用户输入命令并执行。Bash可以解释用户输入的命令,并将其传递给操作系统进行执行。
作用:
1.命令解析: Bash接收用户的命令,并将其解析为操作系统可以理解的格式。
2.程序执行: Bash执行命令并返回结果。如果是内部命令,Bash会直接处理;如果是外部命令,Bash会通过调用系统的execve函数来启动新进程。
3.环境管理: Bash管理用户的环境变量(如PATH、HOME等),控制命令的搜索路径和配置。
4.脚本支持: Bash也支持脚本编程,用户可以通过编写脚本自动化一些常见操作。
处理流程:
1.用户输入命令。
2.Bash解析命令,分离命令和参数。
3.如果是内部命令,直接执行。
4.如果是外部命令,Bash会在指定路径中查找命令,并创建新进程执行。
5.Bash返回命令执行的结果或错误信息。
6.3 Hello的fork进程创建过程
在Linux等类Unix操作系统中,进程是通过系统调用fork()来创建的。fork()系统调用会创建一个子进程,该子进程是调用进程的副本,如下是fork过程:
1.父进程调用 fork(): 在Hello程序中,当调用 fork() 时,操作系统会创建一个新的进程(子进程)。该子进程几乎是父进程的完全副本,包括代码、数据等。
2.返回值区别: fork() 的返回值在父进程和子进程中不同。在父进程中,fork() 返回子进程的PID,而在子进程中,fork() 返回0。
3.父子进程执行不同代码: 根据 fork() 返回值的不同,父进程和子进程可能会执行不同的代码。子进程通常会在此时执行自己的任务,父进程则继续执行原有的任务。
6.4 Hello的execve过程
execve()是一个系统调用,用于执行一个新的程序,并用新的程序替换当前进程的镜像。这个过程是将当前进程的内存空间完全替换成一个新的程序。
execve过程:
1.删除已存在的用户区域
2.映射私有区:为 hello 的代码、数据、.bss 和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的
3.映射共享区:比如 hello 程序与共享库 libc.so 链接
4.设置 PC:exceve() 做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点
5.execve() 在调用成功的情况下不会返回,只有当出现错误时,例如找不到需要执行的程序时,execve() 才会返回到调用程序
6.5 Hello的进程执行
6.5.1 逻辑控制流
在操作系统中,进程的执行是由逻辑控制流来管理的。尽管看似每个进程都在独占CPU并且顺序执行,实际上操作系统通过时间分片将CPU时间片段化,多个进程轮流使用CPU。在hello程序执行的过程中,它并不是一直占据CPU,而是与其他进程交替执行。例如,hello程序可能在执行时被操作系统暂停一段时间,然后操作系统会调度其他进程执行,最后再将控制权交还给hello程序继续执行。
6.5.2 并发流与时间片
如果多个进程的执行时间存在重叠,就可以称它们为并发流。在hello程序执行时,它也有可能与其他程序并发执行。操作系统通过时间片机制将CPU时间分配给各个进程,每个进程执行一个时间片的部分指令后,可能被操作系统暂停并让其他进程执行。因此,在hello程序执行的过程中,它和其他进程之间是并发的。比如在某一时刻,hello程序在执行时,系统也可能同时执行其他进程(如文件操作程序、浏览器等)。
6.5.3 内核模式和用户模式
在执行hello程序时,操作系统会使得程序在不同的模式下运行,具体分为用户模式和内核模式。
用户模式:当hello程序刚开始运行时,它会在用户模式下执行。这意味着它不能直接操作硬件,也无法访问内核中的资源。如果在此模式下执行了不允许的操作(例如进行非法内存访问或操作硬件),会导致程序异常或崩溃。
内核模式:在hello程序执行过程中,如果涉及到某些特权操作(例如I/O操作、系统调用等),程序会陷入内核模式。在内核模式下,程序有更高的权限,可以直接访问操作系统核心的资源和服务。
6.5.4 上下文切换
在hello程序运行的过程中,操作系统可能会发生上下文切换。当操作系统决定暂停hello程序并切换到另一个进程时,它会保存当前进程的状态(如寄存器值、程序计数器等),然后恢复其他进程的状态。上下文切换是一个开销较大的操作,因为它涉及到对多个进程的状态保存和恢复,但这是实现多任务并发执行的基础。在hello程序的执行过程中,上下文切换可能会发生多次,尤其是在多进程同时运行的情况下。
6.5.5 Hello的执行
hello程序从Shell启动时,会处于用户模式开始执行。在执行过程中,操作系统的内核会不断进行上下文切换,配合调度器在多个进程之间进行时间片分配。hello程序在运行时,如果需要进行一些系统级操作(如打印输出到终端),可能会触发系统调用,进而进入内核模式执行这些操作。若hello程序在运行过程中收到信号(例如一个定时器信号),它会进入内核模式来处理该信号的信号处理程序,处理完毕后再恢复到用户模式继续执行。
6.6 hello的异常与信号处理
6.6.1 Ctrl-c
在hello运行时输入ctrl-c,会向内核发送SIGINT信号,将前台运行的所有进程终止,输入ps指令可以看到hello进程已终止,如图30。
图30 发送ctrl-c的效果
6.6.2 Ctrl-z
输入Ctrl-z时,向内核发送SIGTSTP信号,将前台运行的所有进程挂起,输入ps指令可以看到hello进程已挂起,输入jobs,可以看到hello的作业已停止,如图31所示。
图31 发送ctrl-z的效果
图32 hello的进程位置
输入pstree可以显示出hello的进程位置,如图32所示。
图33 杀死进程
输入fg 1,可以将hello转回前台作业,继续执行,输入kill -9 3139,强制将hello进程杀死,如图33所示。
6.6.3 乱按
图34 乱按结果显示
乱按shell会记录输入的字符,结束后会将字符作为指令输入。
6.7本章小结
本章介绍了shell的概念和作用,根据hello程序运行的情况分析了通过fork创建进程,通过execve加载运行程序,以及进程的异常信号处理等内容。
(第6章2分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
在CPU中当然不是仅仅只有hello一个进程,而是许多进程共享CPU和主存资源。那么为了使内存管理更加高效,操作系统提出了一种十分重要的抽象,即虚拟内存。它有如下的好处:1)可以有效使用主存;2)可以简化内存管理;3)可以提供独立的地址空间。接下来详细介绍相关概念。
1.逻辑地址
逻辑地址是由程序在执行过程中生成的地址,它反映的是程序代码对内存的访问方式。程序员编写代码时使用的地址就是逻辑地址。它由编译器生成,通常与程序的源代码直接相关。对于hello程序,逻辑地址是程序在运行时访问的内存地址。
它是程序员看到的地址,是程序的抽象视图,在hello程序中,编译器为每个变量、函数等分配一个逻辑地址。当程序访问这些变量时,它使用这些逻辑地址。
2.线性地址
线性地址是经过分段映射后得到的地址,它位于段地址转换过程之后。现代操作系统通常使用分页机制将线性地址转换为物理地址。虽然程序使用的是逻辑地址,但操作系统会将其转换为线性地址,然后再由硬件将其映射到物理内存中。
它是内存地址映射过程中的中间地址,当hello程序通过指令访问内存时,CPU将逻辑地址先转换为线性地址,再通过分页机制进行进一步转换。
3.虚拟地址
虚拟地址是操作系统为每个进程提供的一个独立的内存空间地址。每个进程运行时,都认为自己有从零开始的一块完整内存区域。操作系统和硬件通过虚拟内存管理技术,将虚拟地址映射到物理内存上。每个进程都有独立的虚拟地址空间,进程之间互不干扰。
它是操作系统为进程提供的隔离内存空间的地址,hello程序运行时,操作系统会将其分配一块虚拟内存地址空间。程序访问的所有地址都是虚拟地址,系统会通过页表将虚拟地址映射到物理内存。
4.物理地址
物理地址是计算机系统中的实际物理内存地址。物理内存指的是计算机硬件中的内存芯片。在虚拟内存管理下,虚拟地址通过操作系统的内存管理单元(MMU)被映射到物理地址。
物理地址是计算机硬件实际的内存地址。当hello程序运行时,CPU会使用内存管理单元(MMU)将虚拟地址转换为物理地址,然后通过总线访问内存中的数据。hello程序通过操作系统将虚拟内存中的数据映射到物理内存中,最终在物理内存中读取或写入数据。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在现代计算机系统中,段式管理是一种内存管理机制,它使用段寄存器来管理程序的内存访问。在Intel x86架构中,逻辑地址(或称为段内地址)到线性地址的转换涉及到段的选择和偏移量的计算。下面简单介绍一下这一过程。
段式管理在Intel架构中的作用是通过段选择子和偏移量来表示和管理内存,逻辑地址转换为线性地址时,段选择子帮助选出对应的段描述符,段描述符提供段的基址,最终将段基址和偏移量相加得到线性地址。这种管理方式使得程序能够以一种较为抽象的方式进行内存管理,同时也能确保内存访问的隔离和保护。
7.3 Hello的线性地址到物理地址的变换-页式管理
在页式管理中,线性地址到物理地址的转换是通过分页机制实现的。首先,线性地址被划分为两部分:高位部分作为页表索引,低位部分作为页内偏移。操作系统维护着一个页表,它将每个虚拟页号映射到一个物理页号。具体的转换过程是,首先通过页表查找得到线性地址对应的物理页号,然后将页内偏移直接加到该物理页号上,得到最终的物理地址。这种方式使得内存的管理更加灵活和高效,同时实现了内存的虚拟化,使得每个进程可以拥有独立的地址空间。
7.4 TLB与四级页表支持下的VA到PA的变换
在四级页表支持下,虚拟地址(VA)到物理地址(PA)的转换通过PGD、PUD、PMD和PTE四个层级逐步查找,最后根据物理页帧地址和虚拟地址中的页内偏移计算出物理地址。这一过程涉及多次内存访问,可能导致延迟。为了提高效率,TLB作为高速缓存存储了最近访问的虚拟地址到物理地址的映射。当需要转换的虚拟地址已经在TLB中时,系统可以直接从缓存中提取物理地址,避免了重新查找四级页表,从而显著加速了地址转换过程,减少了访问延迟。TLB的引入大大提高了系统的性能,特别是在频繁访问相同虚拟地址时,极大地优化了内存管理的效率。
7.5 三级Cache支持下的物理内存访问
物理地址由CT,CI,CO三部分组成。CT为缓存标记位;CI为缓存组索引;CO为缓存偏移。
使用CI进行组索引,每组8路,对8路的块分别匹配CT如果匹配成功且块的valid标志位为1,则命中,根据数据偏移量CO取出数据返回。如果没有匹配成功或者匹配成功但是标志位是1,则不命中,向下一级缓存中查询数据。查询到数据之后,可以采取多种放置策略进行解决。例如LFU策略,若有空闲页则放置,若无,则寻找使用次数最少的页覆盖放置。
7.6 hello进程fork时的内存映射
当一个进程执行 fork() 系统调用时,操作系统会创建一个新的子进程,子进程会复制父进程的所有内存空间。初始情况下,父子进程的虚拟内存空间完全相同。为了优化效率,操作系统通常采用“写时复制”(Copy-On-Write, COW)技术。在这种机制下,父子进程共享相同的物理内存页,直到其中一个进程尝试修改内存中的内容时,操作系统才会将该内存页复制一份给需要修改的进程,从而保证父子进程的独立性。因此,fork() 不会立即进行大规模的内存复制,而是仅在有修改发生时才进行复制,减少了内存开销和复制的时间。
7.7 hello进程execve时的内存映射
execve() 是 Linux 中一个常见的系统调用,用于加载一个新的可执行文件并替换当前进程的内存映射。当一个进程调用 execve() 时,原有的进程空间(堆栈、数据、代码等)会被完全清除,并加载新的可执行文件。操作系统会为新程序创建新的虚拟内存空间,并且将程序代码、数据和堆栈等加载到新的内存区域。execve() 会从磁盘读取新的程序映像,将其映射到进程的虚拟地址空间中,更新页表,并启动新的程序运行。此过程通常包括:
1.加载程序的代码段到内存。
2.初始化程序的堆和栈。
3.配置新的堆栈环境、命令行参数和环境变量。
7.8 缺页故障与缺页中断处理
缺页故障(Page Fault)是指程序访问的虚拟地址并未映射到物理内存中,导致的一个异常。此时,CPU 会触发一个中断,进入操作系统的缺页中断处理程序。缺页故障的原因可能是程序访问了尚未加载到内存的页面,或者访问了未分配的内存页。
缺页中断处理过程包括以下几个步骤:
1.触发缺页中断:当程序访问一个尚未映射的虚拟地址时,CPU 会触发缺页故障中断。
2.检查虚拟地址是否合法:操作系统首先检查访问的虚拟地址是否有效,如果地址非法(例如访问了一个非法或越界的地址),操作系统会终止程序并返回错误。
3.检查页面是否可用:如果虚拟地址合法,操作系统会查看该页面是否已经分配并加载到内存。如果没有,则需要通过磁盘 I/O 将页面从交换空间或磁盘加载到物理内存。
4.加载页面并更新页表:操作系统会从磁盘或交换空间读取页面数据,并将其加载到物理内存中。接着,更新页表,将虚拟地址与物理页框的映射关系加入页表。
5.恢复程序执行:当页表更新完成后,控制权返回到程序,程序可以继续执行访问该页面的操作。
7.9动态存储分配管理
7.10本章小结
本章主要介绍了 hello 的存储器地址空间、intel 的段式管理、页式管理,逻辑地址到线性地址到物理地址的变换,进程运行fork、execve 时的内存映射、缺页故障与缺页中断处理
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
8.2 简述Unix IO接口及其函数
8.3 printf的实现分析
8.4 getchar的实现分析
8.5本章小结
(第8章 选做 0分)
结论
hello经历的过程:
首先程序员将hello代码从键盘输入,之后这个代码需要进行接下来的步骤:
1.预处理(cpp):使用预处理器将 hello.c 文件进行处理,输出修改后的 hello.i 文件。
2.编译(ccl):将 hello.i 文件转化为汇编代码,通过 gcc -S 命令生成 hello.s 汇编文件。
3.汇编(as):通过汇编器将 hello.s 文件转化为目标文件,使用 gcc -c 命令生成 hello.o。
4.链接(ld):将 hello.o 文件与其他可重定位目标文件和动态库链接,生成最终的可执行文件 hello。
5.运行:在终端中输入命令 ./hello 2023111680 姜虹伯 13354598077 2 来执行生成的可执行文件。
6.创建进程:当终端检测到输入的指令不是 shell 内建命令时,它会调用 fork 函数来创建一个新的子进程,并将其用作程序执行。
7.加载程序:Shell 通过调用 execve 函数来启动程序,加载器会映射虚拟内存到物理内存,程序开始运行,并进入 main 函数。
8.执行指令:CPU 为进程分配时间片,并在一个时间片内执行 hello 程序的指令,按照顺序完成程序的控制流程。
9.访问内存:内存管理单元(MMU)会将程序使用的虚拟内存地址通过页表转换为物理地址。
10.信号管理:在程序执行过程中,若用户输入 Ctrl+c,内核会发送 SIGINT 信号终止进程;输入 Ctrl+z 时,内核会发送 SIGTSTP 信号,将进程挂起;而通过输入 kill -9 %1,可以强制杀死挂起的进程。
11.终止:子进程完成执行后,内核会安排父进程回收子进程并获取其退出状态,同时删除子进程相关的数据结构并释放内存。
感悟:
计算机系统的设计是一个非常综合且富有挑战性的领域。从硬件到软件,每一层设计都不仅关乎技术的实现,更体现了设计者对用户需求、系统可扩展性、效率和安全性的深刻思考。回顾过去的发展历程,我们可以看到计算机系统从早期单一的任务执行,到如今的复杂多层次、多领域协作,技术的进步往往是从解决具体痛点、优化体验开始的。通过本次大作业,我深入理解到了计算机如何系统化地执行一个程序。从用户到系统内核,一道道复杂但是精致的工序成就了现代高性能的计算机,这背后都是人们智慧的结晶和严谨的逻辑。
(结论0分,缺失-1分)
附件
文件名 | 功能 |
hello.c | 源程序 |
hello.i | 预处理后的文本文件 |
hello.s | 编译后产生的汇编语言文件 |
hello.o | 汇编后产生的二进制可重定位目标文件 |
hello | 链接后的可执行文件 |
hello_elf.txt | hello的ELF格式文件的文本 |
hello_obj.txt | hello的反汇编文件的文本 |
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] Randal E.Bryant David R.O'Hallaron.深入理解计算机系统(第三版).机械工业出版社
[2] [转]printf 函数实现的深入剖析 - Pianistx - 博客园
[3] linux2.6 内存管理——逻辑地址转换为线性地址(逻辑地址、线性地址、物理地址、虚拟地址) - 刁海威 - 博客园
(参考文献0分,缺失 -1分)