程序人生-Hello’s P2P
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 2023******
班 级 23L0511
学 生 ***
指 导 教 师 史先俊
计算机科学与技术学院
2025年5月
摘 要
本报告以“程序人生-Hello\'s P2P”为题,详细分析了从C语言源代码(hello.c)到可执行程序(hello)的完整编译流程,以及程序从加载到终止的全生命周期。报告基于Ubuntu 22.04 LTS环境,使用GCC工具链,通过预处理、编译、汇编和链接四个阶段生成可执行文件,并深入探讨了进程管理、存储管理和I/O管理等计算机系统的核心机制。
在预处理阶段,通过宏替换、头文件展开和条件编译等操作,生成了中间文件hello.i。编译阶段将hello.i转换为汇编代码hello.s,展示了词法分析、语法分析和代码优化的过程。汇编阶段生成可重定位目标文件hello.o,并解析了ELF格式的结构。链接阶段通过静态和动态链接机制,将hello.o与库文件合并为可执行文件hello,实现了符号解析和地址重定位。
报告进一步分析了hello程序的进程管理,包括进程创建(fork)、程序加载(execve)和进程调度,并探讨了异常与信号处理的机制。在存储管理部分,详细阐述了逻辑地址到物理地址的转换过程,包括段式管理、页式管理、TLB和Cache的作用,以及动态内存分配的实现。最后,简要介绍了Linux的I/O设备管理方法和Unix I/O接口。
通过本次实验,深入理解了计算机系统的工作原理,包括编译流程、进程管理、存储层次和I/O操作等核心概念,为后续系统级编程和优化奠定了基础。
关键词:编译流程;进程管理;存储管理;ELF格式;动态链接;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述................................................................................... - 4 -
1.1 Hello简介............................................................................ - 4 -
1.2 环境与工具........................................................................... - 4 -
1.3 中间结果............................................................................... - 5 -
1.4 本章小结............................................................................... - 5 -
第2章 预处理............................................................................... - 7 -
2.1 预处理的概念与作用........................................................... - 7 -
2.2在Ubuntu下预处理的命令................................................ - 8 -
2.3 Hello的预处理结果解析.................................................... - 8 -
2.4 本章小结............................................................................... - 9 -
第3章 编译................................................................................. - 10 -
3.1 编译的概念与作用............................................................. - 10 -
3.2 在Ubuntu下编译的命令.................................................. - 10 -
3.3 Hello的编译结果解析...................................................... - 10 -
3.4 本章小结............................................................................. - 14 -
第4章 汇编................................................................................. - 15 -
4.1 汇编的概念与作用............................................................. - 15 -
4.2 在Ubuntu下汇编的命令.................................................. - 15 -
4.3 可重定位目标elf格式...................................................... - 16 -
4.4 Hello.o的结果解析........................................................... - 19 -
4.5 本章小结............................................................................. - 21 -
第5章 链接................................................................................. - 22 -
5.1 链接的概念与作用............................................................. - 22 -
5.2 在Ubuntu下链接的命令.................................................. - 22 -
5.3 可执行目标文件hello的格式......................................... - 23 -
5.4 hello的虚拟地址空间....................................................... - 28 -
5.5 链接的重定位过程分析..................................................... - 29 -
5.6 hello的执行流程............................................................... - 29 -
5.7 Hello的动态链接分析...................................................... - 32 -
5.8 本章小结............................................................................. - 34 -
第6章 hello进程管理.......................................................... - 36 -
6.1 进程的概念与作用............................................................. - 36 -
6.2 简述壳Shell-bash的作用与处理流程........................... - 36 -
6.3 Hello的fork进程创建过程............................................ - 37 -
6.4 Hello的execve过程........................................................ - 38 -
6.5 Hello的进程执行.............................................................. - 38 -
6.6 hello的异常与信号处理................................................... - 36 -
6.7本章小结.............................................................................. - 40 -
第7章 hello的存储管理...................................................... - 45 -
7.1 hello的存储器地址空间................................................... - 45 -
7.2 Intel逻辑地址到线性地址的变换-段式管理. 错误!未定义书签。
7.3 Hello的线性地址到物理地址的变换-页式管理 错误!未定义书签。
7.4 TLB与四级页表支持下的VA到PA的变换.. 错误!未定义书签。
7.5 三级Cache支持下的物理内存访问...... 错误!未定义书签。
7.6 hello进程fork时的内存映射............... 错误!未定义书签。
7.7 hello进程execve时的内存映射........... 错误!未定义书签。
7.8 缺页故障与缺页中断处理........................ 错误!未定义书签。
7.9动态存储分配管理.................................... 错误!未定义书签。
7.10本章小结.................................................. 错误!未定义书签。
第8章 hello的IO管理....................................................... - 54 -
8.1 Linux的IO设备管理方法................................................. - 54 -
8.2 简述Unix IO接口及其函数.............................................. - 54 -
8.3 printf的实现分析.............................................................. - 56 -
8.4 getchar的实现分析.......................................................... - 56 -
8.5本章小结.............................................................................. - 57 -
结论............................................................................................... - 57 -
附件............................................................................................... - 59 -
参考文献....................................................................................... - 60 -
第1章 概述
1.1 Hello简介
在程序开发过程中,P2P(Program to Program)指的是从源代码到最终可执行程序的全过程。以 hello.c 为例,当我们在终端中运行如下命令:
gcc -o hello hello.c
就会经历一系列的编译流程,最终生成一个可以直接运行的 hello 程序。整个编译流程大致分为以下四个阶段:
1. 预处理(Preprocessing)
这一阶段主要处理以 # 开头的预处理指令,比如 #include 。编译器会将头文件中的内容直接插入到源文件中,得到一个新的源代码文件,通常以 .i 结尾,例如 hello.i。这个文件仍然是纯文本格式。
2. 编译(Compilation)
编译器将 hello.i 翻译成汇编语言文件 hello.s,过程中会完成词法分析、语法分析、语义分析、生成中间代码、优化等一系列步骤,这些内容在编译原理课程中会深入学习。
3. 汇编(Assembly)
汇编器将 hello.s 转换成机器代码,生成目标文件 hello.o。这个文件虽然是二进制格式,但它还不是最终可以运行的程序,因为其中可能引用了其他目标文件中定义的函数(比如 printf),需要进一步链接。
4. 链接(Linking)
链接器会把 hello.o 和标准库中已有的目标文件(如 printf 所在的 printf.o)合并,生成最终的可执行文件 hello。到这里,这个程序已经可以被系统加载并执行了。
O2O(Zero to Zero)强调的是从系统运行状态出发:最初内存中没有该程序的任何内容,到程序执行完毕后资源被释放、重新归零的过程。
当我们在 Linux 命令行中敲下 ./hello 并回车时,操作系统中运行的 shell 会负责解析这条命令。若该命令不是 shell 的内置指令,它就会被认为是一个可执行程序。此时,shell 会调用 fork 创建一个子进程,并通过 execve 系统调用将 hello 程序加载到新进程中。
加载过程中,内核会通过内存映射将程序代码加载到内存中,同时为其分配独立的地址空间。随后,CPU 开始执行程序指令,内存管理模块负责虚拟地址到物理地址的转换,TLB 和缓存系统则提升了访问速度
当程序运行结束,内核会接收到相应的终止信号,启动回收机制:释放内存、关闭文件描述符、清理进程上下文等。这样一来,hello 程序所占用的系统资源被完全释放,仿佛从未存在过,也就是从“零”回到“零”的过程。
1.2 环境与工具
硬件环境:
处理器:13th Gen Intel(R) Core(TM) i7-13700H 2.40 GHz
机带RAM:16.0 GB (15.7 GB 可用)
系统类型:64 位操作系统, 基于 x64 的处理器
软件环境:Windows11 64位, VMware,Ubuntu 22.04.5 LTS
开发与调试工具:gcc, as, ld, vim, edb, readelf, vs,vs code等
1.3 中间结果
Hello.i 预处理后得到的文本文件
Hello.s 编译后得到的汇编语言文件
Hello.o 汇编后得到的可重定位目标文件
Hello 链接之后得到的可执行目标文件
Hello.c C文件 初始文件
1.4 本章小结
本章首先介绍了 hello 程序从源代码到可执行文件的 P2P(Program to Program)流程,以及从程序加载到执行再回归初始状态的 O2O(Zero to Zero)执行过程,阐明了整个过程的设计思路和实现方式。接着,说明了完成本实验所需的硬件环境、软件平台和开发工具,并对实验过程中生成的各类中间文件(如 .i、.s、.o 等)的命名方式和各自功能进行了详细说明。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
2.1.1 预处理的概念
在 C 语言的编译过程中,预处理是编译器处理源代码前的第一步,由预处理器负责完成。它的主要任务包括宏定义替换、头文件引入、条件编译以及行号控制等操作。预处理器通过以 # 开头的一些特定指令来实现这些功能,常见的指令包括 #define、#include、#if 等。预处理完成后,生成的中间代码会被交给编译器进行下一阶段的处理。
宏替换:使用 #define 定义的宏会在预处理阶段直接在文本中进行替换。比如,#define PI 3.14159,之后程序中凡是出现 PI 的地方,都会被替换成 3.14159。
文件包含:通过 #include 指令,可以将其他文件的内容插入当前源文件,比如 #include 会把标准输入输出头文件的内容纳入当前文件中,便于使用其中的函数。
条件编译:允许根据不同条件控制某段代码是否参与编译,常用于跨平台开发或调试配置。示例:
#ifdef DEBUG
printf(\"Debugging mode\\n\");
#endif
若定义了 DEBUG 宏,该代码段将被编译,否则会被忽略。
行控制:使用 #line 指令可以修改编译器输出中所显示的行号和文件名,常见于自动生成代码的场景。比如:
#line 100 \"myfile.c\"
告诉编译器从这行起,当前文件为 \"myfile.c\",当前行为第 100 行。
2.1.2 预处理的作用
预处理器并不会对代码的具体逻辑做深入分析,它更多地是对代码文本进行替换和组织。这一阶段的主要作用包括:
提升代码可读性和维护性:通过宏定义和头文件包含,能够有效减少代码重复。比如把常用的结构体、函数声明放在头文件中统一管理,能够保证多个源文件中的一致性,也方便日后修改和维护。
简化代码编写:宏的使用可以让一些重复性高或结构复杂的代码简化成一个标识符,提高书写效率和代码整洁度。
支持跨平台开发:条件编译使得我们可以根据不同平台的需求选择性地编译某些代码段,比如在 Windows 和 Linux 系统中引入不同的头文件或调用不同的函数。
方便调试和开发:通过宏开关来控制调试信息的输出,例如在调试阶段打开 DEBUG 宏输出日志,发布时关闭该宏减少输出,做到灵活切换。
增强程序的灵活性:预处理提供了构建多种编译配置的能力,比如生成不同版本的程序(开发版、测试版、正式版),不需要修改代码主体,只需通过宏定义控制不同功能模块的启用。
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
如图所示,经过预编译过程后,文件从24行扩展到3092行。在其中,文件中所有的注释已经消失。完成了对头文件的展开,对宏定义的替换等内容。
从上图可以看出,在程序的前方为提取出stdio.h中程序所需要的头文件定义声明的部分,其中包含了其他头文件的展开以及extern引用外部符号的部分,以及利用typedef来定义变量类型别名。
2.4 本章小结
本章主要讲解了程序在进入编译前的预处理阶段所涉及的处理流程和内容。通过对源文件的预处理操作,展示了如何使用相关指令将宏定义替换为实际内容、如何对条件编译语句(如 #if)进行解析,以及如何去除无关项(如注释)等。通过这些步骤,原始源代码被转换为更适合编译器进一步处理的中间代码形式。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
在编译流程中,编译阶段指的是将预处理完成后的源文件(通常为 .i 文件)翻译成汇编语言程序(即 .s 文件)的过程。这个过程由编译器前端完成,是整个编译系统中的关键步骤之一。
编译的主要任务包括:
- 词法分析:将源代码拆解成基本的记号(token),如关键字、标识符、常量等。
- 语法分析:分析代码结构是否符合语言语法规则,并构建语法树。
- 语义分析:检查代码的语义正确性,比如变量是否已定义、类型是否匹配等。
- 中间代码生成与优化:将语法树转化为中间形式,并在此基础上进行优化,提高代码运行效率。
- 目标汇编代码生成:最终输出与平台相关的汇编代码文件(.s),为下一阶段的汇编做好准备。
通过这一过程,程序从高级语言转化为了底层的汇编语言描述,便于之后由汇编器生成机器指令。编译阶段不仅将语法正确的 C 代码转换为汇编语言,还能对代码逻辑进行初步优化,提高程序运行效率。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1汇编初始部分
在main函数前有一部分字段展示了节名称:
.file 声明源文件
.text 表示代码节
.section .rodata 表示只读数据段
.align 声明对指令或者数据的存放地址进行对齐的方式
.sting 声明一个字符串
,globl 声明全局变量
.type 声明一个符号的类型
3.3.2 数据部分
(1)字符串程序有两个个字符串存放在只读数据段中,如图:
Hello.c中唯一的数组是main函数中的第二个参数(即char**argv),数组的每个元素都是一个指向字符类型的指针。数组起始地址存放在栈中-32(%rbp),被两次调用作为参数传到printf中。
如图,分别将rax是设置为两个字符串的起始地址:
(2)参数argc
参数argc是main函数的第一个参数,被存放在寄存器%edi中,由语句
movl %edi, -20(%rbp)可见寄存器%edi地址被压入栈中,而语句
cmpl $5, -20(%rbp)可知该地址上的数值与立即5判断大小,从而得知argc被存放在寄存器并被压入栈中。
(3)局部变量
程序中的局部变量只有i,我们根据movl $0, -4(%rbp)可知局部变量i是被存放在栈上-4(%rbp)的位置。
3.3.3 全局函数
hello.c中只声明了一个全局函数int main(int arge,.char*argv[]),我们通过汇编代码可知。
3.3.4赋值操作
hel1o.c中的赋值操作有for循环开头的i-0,该赋值操作体现在汇编代码上,则是用mov指令实现,如图:。由于int型变量i是一个32位变量,使用movl传递双字实现。
3.3.5算术操作
hello.c中的算术操作为for循环的每次循环结束后i++,该操作体现在汇编代码则使用指令add实现。问样,由于变量i为32位,使用指令addl。指令如下:
3.3.6关系操作
hello.c中存在两个关系操作,分别为:
- 条件判断语句if(argc!=5):汇编代码将这条代码翻译为:
使用了cmp指令比较立即数5和参数argc大小,并且设置了条件码。根据条件码,如果不相等则执行该指令后面的语句,否则跳转到.L2。
- 在for循环每次循环结束要判断一次i<9,判断循环条件被翻译为:
同(1),设置条件码,并通过条件码判断跳转到什么位置。
3.3.7控制转移指令
设置过条件码后,通过条件码来进行控制转移,在本程序中存在两个控制转移:
(1)
判断argc是否为5,如果不为5,则执行if语句,否则执行其他语句,在汇编代码中则表现为如果条件码为1,则跳到.L2,否则执行cmpl指令后的指令。
(2)
在for循环每次结束判断一次i<9,翻译为汇编语言后,通过条件码判断每次循环是否跳转到.L4。而在for循环初始要对i设置为0,如下:
然后直接无条件跳转到.L3循环体。
3.3.8函数操作
(1) main函数分析
参数传递:main 函数的参数为 int argc, char* argv[],用于接收命令行传入的参数。参数的地址和对应的值在前文已有详细说明。
函数调用:通过 call 指令实现对函数的调用。调用过程中,目标函数的地址会被压入栈中,然后程序跳转至对应函数执行。在 main 函数中,依次调用了 printf、exit 和 sleep 等函数。
局部变量:定义了局部变量 i,用于 for 循环控制。关于该变量在内存中的存储地址与实际数值,在之前的分析中也已经介绍过。
(2) printf函数分析
参数传递:printf 在调用时分别传入了 argv[1] 和 argv[2] 作为参数。
函数调用细节:该函数被调用了两次。第一次将寄存器 %rdi 设为字符串 \"用法:Hello学号姓名 秒数!\\n\" 的地址;第二次则传入 \"Hello %s %s\\n\" 字符串的地址。具体参数传递方面,使用 %rsi 寄存器来传送 argv[1],%rdx 用于传送 argv[2]。这些寄存器的用途在前面已有阐述。
(3)exit函数
参数传递与函数调用:
将rdi设置为1,再使用call指令调用函数。
(4)atoi、sleep函数
参数传递与函数调用:
可见,atoi函数将参数argv[3]放入寄存器%rdi中用作参数传递,简单使用call指令调用。
然后,将转换完成的秒数从%eax传递到%edi中,edi存放sleep的参数,再使用call调用。
(5)getchar函数
无参数传递,直接使用call调用即可。
3.3.9类型转换
atoi函数将字符串转换成sleep函数所需的整型参数。
3.4 本章小结
本章主要介绍了C编译器将预处理后的文件 hello.i 转换成汇编代码文件 hello.s 的全过程。首先简要阐述了编译的定义与作用,接着展示了相关的编译指令。通过对生成的 hello.s 文件中汇编代码的分析,详细探讨了数据处理、函数调用、赋值操作、算术与关系运算、控制跳转以及类型转换等关键环节,比较了源代码与对应汇编实现之间的异同。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
汇编(Assembly)是将汇编语言代码转换为机器语言代码的过程。汇编语言是一种介于高级语言(如C、Python)和机器码之间的低级语言,使用助记符和符号来代表机器指令及其操作数,通常紧密依赖于特定的计算机体系结构。要让计算机执行汇编代码,必须通过汇编器(Assembler)将其翻译成机器码。
汇编语言的主要特点包括:
与硬件紧密结合:每条汇编指令通常直接对应计算机指令集架构(ISA)中的一条机器指令。
底层控制能力:能够对寄存器、内存地址、输入输出端口等硬件资源进行精细控制。
高效性能:得益于其低级特性,汇编代码可以被优化得非常高效,适合性能要求极高的场景。
4.1.2 汇编的作用
代码转换:将汇编语言指令翻译为计算机可执行的机器码,完成程序的最终转化。
直接硬件控制:允许程序员精准管理硬件资源,这对于操作系统、驱动开发和嵌入式系统等地方尤为重要。
性能优化:借助汇编语言,开发者能严格控制指令执行顺序和资源使用,提升关键代码的运行效率,广泛应用于图形处理、信号处理及实时系统等。
代码调试与优化:通过分析汇编代码,程序员能够深入理解程序执行细节,优化编译器生成的机器码,同时更有效地定位和修复问题。
维护遗留系统:许多旧系统和软件采用汇编语言编写,维护这些系统需要具备汇编相关知识。
4.2 在Ubuntu下汇编的命令
在Ubuntu系统下,对hello.s进行汇编的命令为:
gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
4.3 可重定位目标elf格式
在shell中输入readelf -a hello.o > hello.elf 指令获得 hello.o 文件的 ELF 格式:
4.3.1 生成ELF格式的可重定位目标文件
在编译过程中,源代码经过汇编器处理后会被转换为一种标准格式的目标文件,通常为 ELF(Executable and Linkable Format,可执行与可链接格式)。该格式被广泛应用于 Unix/Linux 系统中,用于保存可执行文件、目标代码以及共享库。
典型的 ELF 可重定位目标文件结构包含以下几个部分:
- ELF Header(ELF 头部):包含文件的基本信息,如标识符、文件类型、目标体系结构、节区表(Section Header Table)的位置、每个表项的大小及数量等。
- .text 节区:存放编译后的程序指令,即代码段。
- .rodata 节区:用于存放只读数据,如字符串常量。
- .data 节区:存储已经初始化的全局变量与静态变量。
- .bss 节区:用于未初始化的全局变量和静态变量,实际文件中不占空间,仅在程序运行时分配内存。
- .symtab 节区(符号表):记录程序中定义或引用的函数、变量等符号信息。
- .rel.text 节区:列出 .text 节中需要重定位的位置,即需要在链接阶段进行地址修正的指令或数据。
- .debug 节区:调试相关信息,保存程序中局部变量的定义、数据类型信息等,供调试器使用。
这种结构使得目标文件在链接阶段能够高效地组合成最终可执行文件,并支持调试、优化等后续操作。
4.3.2 查看ELF格式文件的内容
(1)ELF头
ELF头(ELF header)以一个l6字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。ELF头展示如下:
(2)节头(section header)
记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。
(3)重定位节
.rel.text节是一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改,而调用本地函数的指令不需修改。可执行目标文件中不包含重定位信息。如图,需要重定位的内容如下:
(4) 符号表
ELF 文件中的 .symtab 节保存了程序的符号表信息。该节区由若干个符号条目构成,每个条目描述一个全局变量或函数的相关信息,无论这些符号是程序中定义的还是外部引用的。
值得注意的是,.symtab 符号表仅记录全局符号的信息,并不包含局部变量的详细内容。局部变量的调试信息一般存放在 .debug 节区,用于配合调试器查看。
这个符号表在链接过程中非常关键,它帮助链接器识别和解析符号之间的引用关系,如函数调用或全局变量访问等。
4.4 Hello.o的结果解析
在shell中输入 objdump -d -r hello.o > hello.asm 指令输出hello.o的反汇编文件,并与第3章的hello.s文件进行对照分析。
(1)增加机器语言
每一条指令增加了一个十六进制的表示,即该指令的机器语言。例如,在hello.s中的一个cmpl指令表示为
而在反汇编文件中表示为
(2)操作数进制
反汇编文件中的所有操作数都改为十六进制。如(1)中的例子,立即数由hello.s中的$5变为了$0x5,地址表示也由-20(%rbp)变为-0x14(%rbp)。可见只是进制表示改变,数值未发生改变。
(3)分支转移
反汇编的跳转指令中,所有跳转的位置被表示为主函数+段内偏移量这样确定的地址,而不再是段名称(例如.L3)。例如下面的jmp指令,反汇编文件中为
而hello.s文件中为
(4)函数调用
反汇编文件中对函数的调用与重定位条目相对应。观察下面两个call指令调用函数,在hello.s中为
而在反汇编文件中调用函数为
在可重定位文件中call的后面不再是函数名称,而是一条重定位条目指引的信息。
4.5 本章小结
本章主要讲解了汇编的基本概念及其在程序编译过程中的作用,展示了如何将 hello.s 文件汇编成 hello.o。通过对 hello.o 文件的结构进行分析,重点介绍了 ELF 格式的可重定位目标文件的组成部分,包括 ELF 头信息、各类节区以及节头表。特别对其中的重定位条目进行了详细解析。最后,借助反汇编工具对 hello.o 进行了反汇编,并结合机器指令与原始汇编代码 hello.s 进行比对,从另一视角加深了对汇编过程和重定位机制的理解。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
5.1.1链接的概念
链接(Linking)是指将多个目标文件(object files)和库文件(library files)合并生成可执行文件(executable)或共享库(shared library)的过程。目标文件是源代码经过编译后生成的中间文件,包含机器指令和数据。链接过程由专门的工具——链接器(Linker)完成。
链接主要分为两种方式:
1.静态链接(Static Linking):在程序编译阶段,将所有依赖的目标文件和库文件直接整合到一个独立的可执行文件中。静态链接生成的可执行文件无需外部依赖,所有代码和数据均已包含在内。
2.动态链接(Dynamic Linking):在程序运行时,才将所需的共享库(如DLL或SO文件)加载到内存并与可执行文件关联。动态链接生成的可执行文件体积较小,但需要依赖外部的共享库文件。
注:这里的链接特指从 hello.o 目标文件生成 hello 可执行文件的过程。
5.1.2链接的作用
• 实现代码复用:
通过将通用功能封装为库文件(静态库或动态库),可以在不同程序中重复使用,避免重复开发相同代码,提升开发效率。
• 支持模块化编程:
链接使得程序可以拆分为多个模块,每个模块能够单独编译和测试。这种模块化方式便于团队协作和代码维护。
• 完成符号解析与地址分配:
链接器负责解析程序中的符号(如函数名和全局变量),确定其内存地址,并修正跨模块的引用关系,确保程序能正确访问各模块的代码和数据。
• 优化程序性能:
链接器能够进行全局优化,例如移除未被引用的代码段、合并重复数据等,从而减小可执行文件体积并提升运行效率。
• 管理外部依赖:
链接器处理程序与库之间的依赖关系,确保所有外部函数和变量能被正确链接,简化程序的构建流程。
• 便于程序更新:
动态链接机制允许程序在运行时加载最新版本的库文件,无需重新编译整个程序即可实现功能更新,降低了维护成本。
5.2 在Ubuntu下链接的命令
在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
5.3 可执行目标文件hello的格式
使用readelf解析hello的ELF格式,得到hello的节信息和段信息:
(1)ELF头(ELF Header)
hello1.elf与hello.elf的ELF头在信息结构上保持高度一致,均以16字节的Magic标识开头,该标识定义了文件生成系统的字长和字节序特征。紧随其后的部分包含了链接器解析目标文件所需的关键元数据。对比两个文件可见,hello1.elf保留了hello.elf中的基础特征(包括Magic值和文件类别等),但在具体参数上有所变化:文件类型发生转变,程序头尺寸有所扩展,节头数量相应增加,同时明确了程序的入口地址。这些变化反映了从中间文件到可执行文件的转换过程中链接器所执行的关键操作。
(2)节头
在链接过程中,链接器会对各目标文件的节区信息进行整合处理。通过readelf工具可以查看每个节区的详细参数,包括其存储大小、文件偏移量以及访问权限等属性。当执行链接操作时,链接器会将不同目标文件中类型相同的节区进行合并重组,形成最终可执行文件中的大段结构。在此过程中,链接器会根据合并后段的新尺寸和位置信息,对所有符号引用进行重新定位,确保程序运行时能够正确访问各个符号的实际地址。这种节区合并与地址重定位机制是链接过程的核心功能之一。
(3)程序头
程序头表(Program Header Table)是 ELF 文件中一个关键的数据结构,它由一系列固定格式的条目组成,每个条目定义了系统加载和执行程序时所需的各个段(Segment)的属性和布局信息。这些条目详细描述了每个段在文件中的偏移位置、内存加载地址、访问权限(如可读、可写、可执行)以及段的大小等关键参数。操作系统加载器(Loader)正是依赖这些信息,才能正确地将程序的不同部分映射到内存地址空间,并为程序的执行做好初始化准备。程序头表中的每个条目都对应一个特定的内存段,这些段可能包含代码、数据或其他运行时必需的信息。
(4)Dynamic section
(5)Symbol table
符号表(Symbol Table)是ELF文件中的关键数据结构,它记录了程序中定义和引用的所有符号信息,包括函数名、变量名及其内存地址等元数据。该表为链接器和加载器提供了符号定位和重定位所需的全部参考信息,确保程序在链接和运行时能正确解析所有符号引用。每个需要参与重定位处理的符号都在符号表中明确声明,包含其名称、类型、绑定属性以及所在节区等关键属性。
5.4 hello的虚拟地址空间
观察程序头的LOAD可加载的程序段的地址为0x400000。如图:
使用edb打开hello从Data Dump窗口观察hello加载到虚拟地址的情况,查
看各段信息。如图:
程序从地址0x400000开始到0x401000被载入,虚拟地址从0x4000000x400f0结束,根据5.3中的节头部表,可以通过edb找到各段的信息。
如.interp节,在hello.elf文件中能看到开始的虚拟地址:
在edb中找到对应的信息:
同样的,我们可以找到如.text节的信息:
5.5 链接的重定位过程分析
5.5.1 分析hello与hello.o的区别
在Shell中使用命令objdump -d -r hello > hello1.asm生成反汇编文件hello1.asm
与第四章中生成的hello.asm文件进行比较,其不同之处如下:
(1)链接后函数数量增加
链接后的反汇编文件hello2.asm中,多出了.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等函数的代码。这是因为动态链接器将共享库中hello.c用到的函数加入可执行文件中。
(2)函数调用指令call的参数发生变化
在链接过程中,链接器解析了重定位条目,call之后的字节代码被链接器直接修改为目标地址与下一条指令的地址之差,指向相应的代码段,从而得到完整的反汇编代码。
(3)跳转指令参数发生变化
在链接过程中,链接器解析了重定位条目,并计算相对距离,修改了对应位置的字节代码为PLT 中相应函数与下条指令的相对地址,从而得到完整的反汇编代码。
5.5.2 重定位过程
重定位由两步组成:
(1)重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的聚合节。然后链接器将运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。至此程序中每条指令和全局变量都有唯一的运行内存地址。
(2)重定位节中的符号引用。这一步中链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。
(3)重定位过程地址计算方法如下:
5.6 hello的执行流程
5.6.1过程记录
通过edb的调试,一步一步地记录下call命令进入的函数。
(I)开始执行:_start、_libe_start_main
(2)执行main:_main、printf、_exit、_sleep、getchar
(3)退出:exit
5.6.2子程序名、地址
程序名 程序地址
_start 0x4010f0
_libc_start_main 0x2f12271d
main 0x401125
_printf 0x4010a0
_sleep 0x4010e0
_getchar 0x4010b0
_exit 0x4010d0
5.7 Hello的动态链接分析
动态链接的核心机制是将程序模块化拆分,在运行时才完成链接。由于共享库的加载地址不确定,编译器无法预知库函数的运行时地址,因此会生成重定位记录,由动态链接器在加载时解析。延迟绑定技术通过全局偏移表(GOT)和过程链接表(PLT)实现,其中GOT的起始地址在hello.elf中位于0x404000。
GOT表位置在调用dl_init之前0x404008后的16个字节均为0:
调用了dl_init之后字节改变成为:
动态链接的地址解析采用两种机制:
对于变量访问:基于代码段与数据段的固定相对偏移进行计算,确保地址正确性。
对于库函数调用:通过PLT(过程链接表)和GOT(全局偏移表)协同工作实现延迟绑定:
PLT包含跳转代码,初始指向GOT对应条目
GOT初始存储PLT第二条指令地址
首次调用时触发动态链接器解析真实地址并更新GOT
后续调用直接通过GOT跳转至目标函数
5.8 本章小结
本章系统阐述了链接的基本概念及其核心功能,通过对比分析hello可执行文件与hello.o目标文件,深入揭示了链接与重定位的关键处理流程。主要内容包括:详细解读ELF文件格式各组成部分的结构意义,完整跟踪hello程序的执行过程,以及重点剖析动态链接的实现机制。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1进程的概念
进程是操作系统进行资源分配和调度的基本单位,代表程序的一次执行实例。它构成一个独立的执行环境,主要包含以下核心要素:
1.执行代码:程序指令的二进制表示
2.内存结构:由代码段、数据段、动态分配的堆和函数调用的栈组成
3.进程描述符(PCB):记录进程ID、运行状态、寄存器值等元数据
4.系统资源:包括打开的文件、网络连接等I/O资源
每个进程通过唯一的PID标识,在系统中隔离运行,拥有独立的虚拟地址空间和资源分配。
6.1.2 进程的作用
进程的核心功能主要体现在以下方面:
1.资源管理机制
作为系统资源分配的基本单位,进程实现了对CPU、内存和文件等资源的有效管控,确保各进程资源独立分配与使用
2.并发执行能力
支持多进程并发运行,提升系统整体吞吐量。典型场景如前台交互进程与后台计算进程的并行执行
3.隔离保护特性
通过虚拟内存等机制实现进程间严格隔离,防止单个进程故障或攻击波及其他进程,保障系统稳定性
4.任务调度基础
作为调度基本单元,系统基于进程优先级和状态实施调度策略,确保多任务处理的公平性与响应速度
5.并行计算支持
在多核架构下,通过多进程实现真正并行,充分发挥硬件计算潜力
6.进程间通信(IPC)
提供管道、共享内存等多种IPC机制,支持进程间的数据交换与协作
6.2 简述壳Shell-bash的作用与处理流程
Shell 是操作系统的命令解释器,作为用户与内核交互的接口。主流Shell包括Bash、Zsh等,其中Bash作为Bourne Shell的增强版,已成为Linux/macOS的标准Shell。
核心功能:
- 命令解析与执行 - 将用户指令转换为系统调用
- 脚本支持 - 提供自动化任务处理能力
- 进程管理 - 支持应用程序的启动与控制
- 环境配置 - 管理变量和工作路径等运行环境
- 数据流处理 - 支持I/O重定向和管道操作
Bash工作流程:
- 初始化阶段:读取配置文件(/etc/profile等)建立运行环境
- 交互循环:
- 显示提示符($/#)
- 读取并解析命令(处理变量替换等)
- 执行流程:
- 内建命令直接运行
- 外部命令创建子进程执行
- 脚本逐行解释执行
- 处理I/O重定向和管道
- 等待命令结束(前台任务)或后台运行
- 信号处理:响应中断等系统信号
6.3 Hello的fork进程创建过程
Shell通过fork()系统调用创建子进程时,会生成一个与父进程高度相似但独立的新进程。这个子进程会获得父进程用户空间的全量副本,包括:
- 代码段和数据段的完整拷贝
- 堆和栈内存空间的独立复制
- 共享库的相同映射
- 所有已打开文件描述符的副本
父子进程的关键区别在于各自拥有唯一的进程ID(PID)。fork()的独特之处在于:
- 父进程调用一次
- 系统返回两次(父进程获得子进程PID,子进程获得0)
- 父子进程进入并发执行状态
在hello程序的执行场景中,由于子进程在前台运行,父进程shell会主动挂起,等待hello进程执行结束后才恢复。这种机制保证了命令行交互的连贯性。
6.4 Hello的execve过程
当Shell通过fork创建子进程后,execve函数会在该子进程的上下文中加载并执行新程序(如hello)。该函数接收三个关键参数:
- 目标程序路径(filename)
- 参数数组(argv)
- 环境变量数组(envp)
execve的执行机制包含以下关键步骤:
- 清除原进程空间:完全移除当前进程的用户空间内容(包括代码和数据段)
- 构建新内存映射:
- 私有区域:建立新的代码段、数据段、.bss段和栈空间(采用写时复制技术)
- 共享区域:加载所需的共享库
- 控制权转移:将执行起点设置为新程序的入口地址
该函数具有\"不成功便不返回\"的特性除非目标程序加载失败,否则永远不会返回到调用点。这种设计实现了进程执行内容的完全替换,同时保持了进程ID等元信息不变。
6.5 Hello的进程执行
进程调度与执行机制详解
- 逻辑控制流与时间片
现代操作系统通过时间片轮转实现多进程并发执行。每个进程获得独立的逻辑控制流(PC值序列),处理器物理控制流被划分为多个时间片,各进程交替占用CPU执行指令。这种交错执行机制形成了并发的假象。 - 核心调度机制
进程调度通过三层机制协同工作:
- 上下文切换:保存/恢复进程运行状态
(1) 保存当前进程上下文(寄存器、PC值等)
(2) 恢复目标进程保存的上下文
(3) 移交控制权至目标进程 - 调度算法:基于优先级、时间片等策略选择就绪进程
- 模式转换:用户态与内核态切换
- 特权模式转换
处理器通过模式位区分执行权限:
- 内核模式:可执行特权指令,访问全部内存空间
- 用户模式:受限执行环境
转换仅能通过异常触发(系统调用/中断/故障),此时:
(1) 硬件自动切换到内核模式
(2) 内核执行调度决策
(3) 完成上下文切换后返回用户模式
- 完整调度流程示例
当时间片耗尽时:
(1)时钟中断触发模式转换
(2)内核保存进程A上下文
(3)调度器选择进程B
(4)恢复进程B上下文
(5)返回用户模式执行进程B
该机制确保了:
- 进程隔离性(独立的逻辑控制流)
- 公平性(时间片轮转)
- 安全性(特权指令保护)
- 高效性(快速上下文切换)
6.6 hello的异常与信号处理
6.6.1异常的分类
异常类别
触发原因
同步/异步
返回行为
典型示例
中断(Interrupt)
外部设备信号(如定时器、I/O完成)
异步
总是返回到下一条指令
键盘输入、硬件定时器中断
陷阱(Trap)
程序主动请求(系统调用)
同步
返回到下一条指令
fork()、read()等系统调用
故障(Fault)
可恢复的错误(如缺页、除零)
同步
可能重新执行当前指令
页错误(Page Fault)、除零异常
终止(Abort)
不可恢复的致命错误
同步
不返回,终止进程
硬件错误(如内存校验错误)
6.6.2异常的处理方式
6.6.3运行结果及相关命令
(1)正常运行状态
在程序正常运行时,打印7次提示信息,以输入回车为标志结束程序,并回收进程。
(2)运行时按下Ctrl + C
按下Ctrl + C,Shell进程收到SIGINT信号,Shell结束并回收hello进程。
(3)运行时按下Ctrl + Z
按下Ctrl + Z,Shell进程收到SIGSTP信号,Shell显示屏幕提示信息并挂起hello进程。
(4)对hello进程的挂起可由ps和jobs命令查看,可以发现hello进程确实被挂起而非被回收,且其job代号为1。
(5)在Shell中输入pstree命令,可以将所有进程以树状图显示:
(6)输入kill命令,则可以杀死指定(进程组的)进程:
(7) 输入fg 1则命令将hello进程再次调到前台执行,可以发现Shell首先打印hello的命令行命令,hello再从挂起处继续运行,打印剩下的语句。程序仍然可以正常结束,并完成进程回收。
6.7本章小结
本章围绕计算机系统中的进程管理与Shell机制展开深入探讨,以hello程序为研究载体系统性地阐述了以下核心内容:首先从理论层面剖析了进程的基本概念及其在资源管理中的关键作用,详细解读了Shell作为命令解释器的工作原理与处理流程;其次通过完整的实例分析,逐步演示了hello程序从进程创建、加载到执行的全生命周期过程;最后针对程序运行中可能触发的各类异常情况以及不同输入条件下的输出结果进行了全面的技术解析与说明。(第6章2分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
- 逻辑地址(Logical Address)
编程视角的地址空间,是程序代码中直接使用的内存引用。例如字符串\"Hello\"在程序中的引用位置可能表示为0x0040,这是开发者可见的抽象地址。 - 线性地址(Linear Address)
通过段式内存管理转换后的地址。计算方式为:
线性地址 = 段基址(如0x1000) + 逻辑地址偏移量(如0x0040)
示例结果为0x1040 - 虚拟地址(Virtual Address)
现代操作系统中通常指代经过分页转换前的地址。通过页表机制将线性地址映射到虚拟内存空间,例如可能将0x1040映射为0x2040,实现虚拟内存扩展。 - 物理地址(Physical Address)
内存硬件的实际寻址位置。通过MMU完成虚拟地址到物理地址的最终转换,例如0x2040→0x3040。
地址转换全流程示例(以\"Hello\"字符串为例):
① 程序引用逻辑地址:0x0040
② 段转换:0x1000(段基址) + 0x0040 = 0x1040
③ 页表映射:0x1040 → 0x2040(虚拟地址)
④ 物理映射:0x2040 → 0x3040(物理地址)
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式内存管理通过将程序划分为多个逻辑段(代码段、数据段等)来实现内存管理。每个段作为独立单元由段表管理,表中记录段号、起始地址、长度等关键信息。程序访问内存时使用的逻辑地址包含16位段选择符和段内偏移量,其中段选择符的高13位用于索引段描述符表,低3位存储控制信息。
系统通过两级描述符表实现段管理:全局描述符表(GDT)存储系统级段描述符和各任务的局部描述符表(LDT)指针;每个任务独有的局部描述符表(LDT)则包含该任务的私有段描述符和门描述符。地址转换时,CPU先通过段选择符定位段描述符,获取段基址后与偏移量相加得到线性地址。这种机制既实现了程序模块化隔离,又支持系统资源的统一管理。
段式管理图示如下:
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存系统采用磁盘存储空间作为扩展,将内存空间组织为连续的字节单元数组。该系统通过分页机制管理内存资源,将虚拟地址空间划分为固定大小的虚拟页,物理内存则对应划分为相同大小的物理页框架。
核心管理组件页表由页表条目(PTE)构成数组结构,每个PTE包含两个关键字段:有效位标识该虚拟页当前是否已加载到DRAM中,地址字段则记录对应的物理页起始位置。当CPU访问的虚拟页未驻留内存(有效位未设置)时,系统触发缺页异常,从磁盘加载所需页面到物理内存。
内存管理单元(MMU)负责执行地址转换过程,通过查询页表实现虚拟地址到物理地址的动态映射。该机制既扩展了可用内存空间,又保持了访问效率,是现代操作系统的关键技术支撑。
下面为页式管理的图示:
7.4 TLB与四级页表支持下的VA到PA的变换
Core i7处理器采用四级页表层次结构进行虚拟地址到物理地址的转换。当CPU产生虚拟地址(VA)后,MMU首先使用VPN高位作为TLB标记和索引来查询TLB缓存。若TLB命中则直接获得物理地址;若未命中则启动四级页表查询:从CR3寄存器指向的第一级页表开始,依次通过VPN1至VPN4字段逐级定位下级页表,最终在第四级页表中获取物理页号(PPN),再与页内偏移(VPO)组合形成物理地址(PA)。整个转换过程由MMU硬件自动完成,其中TLB缓存可显著加速频繁访问地址的转换效率,而四级页表结构则支持处理巨大的虚拟地址空间。该机制在保证灵活性的同时实现了高效地址转换,是现代处理器内存管理的关键设计。工作原理如下:
多级页表的工作原理展示如下:
7.5 三级Cache支持下的物理内存访问
如图为高速缓存存储器组织结构:
高速缓存的结构将m个地址位划分成了t个标记位,s个组索引位和b个块偏移位:
在高速缓存的工作机制中,当处理器访问内存时,首先通过地址解码确定目标数据对应的缓存组。系统会检查该组内所有缓存行的有效位和标记位:若存在有效位为1且标记位匹配的缓存行,则产生缓存命中,可直接读取数据;若未找到符合条件的缓存行则发生缓存不命中。此时缓存控制器会启动填充操作,从下级存储器层次(如主存)中读取包含目标数据的完整缓存块,并根据既定的替换策略(如LRU算法会选择最近最久未使用的行)将其存入对应组的某个缓存行中。这个缓存填充过程既考虑了组索引确定的物理位置约束,又通过智能替换策略维护了缓存的空间利用率,从而在硬件层面实现了高效的内存访问加速。整个流程完全由缓存控制器自动管理,对程序执行透明。
7.6 hello进程fork时的内存映射
当进程调用fork()系统调用时,内核会执行以下操作来创建新进程:首先为新进程分配唯一的进程标识符(PID)并构建必要的进程控制块等数据结构;接着采用写时复制(CoW)技术高效复制父进程的虚拟内存空间,包括完整复制mm_struct内存描述符、虚拟内存区域(vm_area_struct)和页表结构,使得子进程初始拥有与父进程完全相同的虚拟内存视图。关键之处在于,内核并不会立即复制物理内存页,而是将父子进程的页表项都标记为只读。当任一进程尝试执行写操作时,会触发页错误异常,此时内核才会真正复制目标物理页,并更新相应进程的页表项为可写状态。这种机制既保证了进程间内存空间的隔离性,又避免了不必要的内存复制开销,实现了高效且安全的进程创建。整个过程对用户程序完全透明,子进程从fork()返回时即拥有与父进程调用fork()时刻完全一致但独立的内存空间状态。
7.7 hello进程execve时的内存映射
execve函数通过内核的加载器机制在当前进程上下文中加载并执行目标程序hello,实现程序替换。该过程包含以下关键步骤:
首先清除现有用户空间结构,释放原程序占用的所有虚拟内存资源。随后建立新的内存映射:为代码段(.text)和数据段(.data)创建基于hello文件的私有映射(采用写时复制机制),.bss段映射到匿名文件并初始化为零,堆栈空间同样初始化为零长度区域。对于动态链接的共享库(如libc.so),则建立共享内存映射,使其可被多个进程共同访问。
最后,加载器将进程的程序计数器(PC)重定向至代码段入口点,完成执行上下文的切换。整个过程保持进程ID不变,仅替换内存映像和执行上下文,确保程序能够无缝接管当前进程的执行流。这种机制既实现了程序的完全替换,又维持了进程管理的一致性。
7.8 缺页故障与缺页中断处理
- 虚拟地址合法性检查
- 内核首先检查触发缺页的虚拟地址是否属于进程的合法地址范围
- 若地址非法,立即触发段错误(Segmentation Fault)并终止进程
- 访问权限验证
- 检查当前操作(读/写/执行)是否匹配该内存区域的权限设置
- 若权限不足(如尝试写入只读页面),触发保护异常(Protection Fault)终止进程
- 页面置换处理
- 通过页面置换算法(如LRU)选择牺牲页
- 若牺牲页被修改过(脏页),先将其内容写入交换空间
- 从磁盘载入请求的目标页面到物理内存
- 更新页表项,建立新的虚拟地址到物理地址的映射
- 恢复执行
- 将控制权返还给用户进程
- 重新执行原先触发缺页的指令
- 此时指令可正常访问已加载到内存的目标页面
7.9动态存储分配管理
动态内存分配器管理着进程的虚拟内存区域中的堆空间,将堆组织成一系列已分配或空闲的内存块。根据内存释放方式的不同,分配器可分为显式和隐式两种类型。显式分配器如C语言的malloc,要求程序员手动释放内存;而隐式分配器(垃圾收集器)则自动回收不再使用的内存块。
在实现方式上,隐式空闲链表通过每个块头部的长度字段隐式连接空闲块,整个堆空间包括头部、有效载荷、填充和尾部。分配器通过遍历所有块来间接访问空闲块,并使用特殊标记的终止块作为结束标志。分配时采用首次适配、下次适配或最佳适配策略查找合适空间,必要时分割大块以减少内部碎片;释放时则利用边界标记合并相邻空闲块。
显式空闲链表则采用更直接的组织方式,将空闲块通过嵌入的指针(如前驱和后继指针)显式连接成链表结构。这种链表可以按后进先出或地址顺序维护,前者将新释放块插入链表头部实现快速操作,后者保持地址有序但需要线性时间查找插入位置。两种实现方式各有优劣,隐式链表实现简单但操作效率较低,显式链表操作高效但实现更复杂,都体现了内存管理在空间效率和时间效率之间的权衡。
7.10本章小结
本章以hello程序为例,系统性地阐述了计算机系统中的存储管理机制。首先深入剖析了地址空间的核心概念,包括逻辑地址(程序员视角的地址)、线性地址(段式转换后的地址)、虚拟地址(分页前的地址)和物理地址(实际内存位置)之间的区别与转换关系,并通过具体示例演示了从程序代码到硬件存储的完整地址转换流程。
重点探讨了虚拟内存的实现机制,详细解析了hello进程运行过程中虚拟地址到物理地址的映射原理。特别研究了进程创建(fork)时的写时复制技术,以及程序加载(execve)时的内存映射过程,包括用户区域清除、私有/共享区域建立和程序计数器设置等关键操作。这些机制共同构成了现代操作系统高效、安全的存储管理体系,既保证了进程间的隔离性,又实现了资源的合理共享。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
将设备映射为文件,允许Linux内核使用一个底层的API,称为Unix IO。 对文件有以下操作:
open:打开指定路径的文件或设备,返回文件描述符用于后续操作
close:释放文件描述符及相关资源,终止文件访问
read:从文件描述符对应的文件中读取数据到内存缓冲区
write:将内存缓冲区数据写入文件描述符对应的文件
lseek:调整文件操作偏移量,支持随机访问文件内容
8.2 简述Unix IO接口及其函数
- 文件描述符机制
- 内核通过非负整数文件描述符管理文件访问
- 标准文件描述符:
- STDIN_FILENO (0):标准输入
- STDOUT_FILENO (1):标准输出
- STDERR_FILENO (2):标准错误输出
- 核心操作函数
(1) open()
功能:打开/创建文件
原型:int open(const char *path, int flags, mode_t mode)
参数:
- path:文件路径
- flags:打开方式(O_RDONLY等)
- mode:创建文件时的权限位
返回值:最小可用文件描述符
(2) close()
功能:关闭文件
原型:int close(int fd)
参数:目标文件描述符
返回值:操作状态(0成功/-1失败)
(3) read()
功能:读取文件内容
原型:ssize_t read(int fd, void *buf, size_t count)
参数:
- fd:文件描述符
- buf:数据缓冲区
- count:读取字节数
返回值:实际读取字节数(0表示EOF)
(4) write()
功能:写入文件内容
原型:ssize_t write(int fd, const void *buf, size_t count)
参数:同read()
返回值:实际写入字节数
(5) lseek()
功能:调整文件偏移量
原型:off_t lseek(int fd, off_t offset, int whence)
参数:
- offset:偏移量
- whence:基准位置(SEEK_SET等)
返回值:新偏移量/-1(错误)
- 文件位置管理
- 初始偏移量:文件开头(0)
- 读写操作自动更新偏移量
- lseek()支持随机访问定位
注:所有函数声明包含在头文件中,遵循POSIX标准规范。
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
字符显示输出全流程:
- 数据格式化阶段
- vsprintf函数完成显示信息的格式化处理
- 生成包含完整控制字符的字符串缓冲区
- 系统调用阶段
- 通过write系统调用进入内核态
- 触发软中断(x86:int 0x80)或直接执行syscall指令
- 内核处理程序接管控制流
- 字符渲染阶段
- 驱动层访问ASCII字模库
- 将字符转换为点阵图形数据
- 写入显示存储器(VRAM)
- 存储每个像素点的RGB色彩值
- 按显示分辨率组织为二维矩阵
- 物理输出阶段
- 显示控制器按刷新率(如60Hz)扫描VRAM
- 通过视频接口(如HDMI/DP)逐行传输像素数据
- 液晶面板根据RGB信号驱动每个子像素单元
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
键盘输入处理通过异步中断机制实现。当按键触发硬件中断后,键盘驱动将扫描码转换为ASCII码并存入系统缓冲区;用户程序通过read系统调用读取缓冲区内容,直到检测到回车符才返回数据。整个过程实现了硬件信号到应用数据的异步转换和传递。
8.5本章小结
本章系统阐述了Linux系统中I/O设备的管理架构与实现原理,重点剖析了Unix I/O标准接口的设计思想及其核心操作函数。通过深入分析printf格式化输出和getchar字符输入这两个典型函数的实现机制,揭示了从用户层系统调用到底层设备驱动的完整处理流程。具体涵盖以下技术要点:
- Unix I/O抽象模型将各类设备统一抽象为文件描述符
- 标准I/O函数库与系统调用接口的层级关系
- 字符设备输入输出的缓冲管理机制
- 用户空间与内核空间的交互方式
(第8章 选做 0分)
结论
Hello 程序在计算机系统中的完整执行过程
1. 程序编写与编译
使用高级语言(如C)编写 hello.c,经 预处理→编译→汇编→链接 生成可执行文件 hello。
关键步骤:
编译(gcc -S):生成汇编代码 .s
汇编(gcc -c):生成目标文件 .o(含机器码 + 重定位信息)
链接(ld):合并 .o 与库文件,生成可执行文件 hello(含入口地址 _start)
2. 进程创建(fork)
Shell 调用 fork() 创建子进程,复制父进程的虚拟内存(采用 写时复制,COW)。
子进程获得独立 PID,但初始内存映射与父进程相同。
3. 程序加载(execve)
子进程调用 execve(\"hello\"),内核加载器:
清除 原进程内存映射
建立新映射:代码段(.text)、数据段(.data)、.bss(零初始化)、栈/堆
动态链接:加载共享库(如 libc.so)
设置 PC:跳转到 _start(程序入口)
4. 指令执行与内存访问
CPU 取指:按 PC 从内存读取指令(若缺页则触发 缺页异常,由内核加载缺失页)。
地址转换(MMU 完成):
虚拟地址 → 物理地址(通过页表 + TLB 加速)
若 TLB 未命中,查询 四级页表(Core i7)。
5. 输入/输出(printf)
格式化:vsprintf 将 \"Hello, World\\n\" 转为字符串缓冲区。
系统调用:write(1, buf, len) 触发 int 0x80/syscall,进入内核态。
显示输出:
内核将数据传递至 显卡驱动
驱动将 ASCII 码转为 像素点阵(字模库)
写入 VRAM,由显示器按刷新率扫描输出。
6. 进程终止
main 返回后调用 exit(),内核回收资源(内存、文件描述符等)。
Shell 父进程通过 wait() 获取子进程状态。
对计算机系统设计的感悟与创新思考
1.分层抽象的价值
从高级语言到机器指令,从虚拟内存到物理地址,计算机系统通过 多层抽象 隐藏复杂性。
启发:设计系统时应明确各层职责(如“一切皆文件”的 Unix 哲学)。
2.性能与资源的权衡
写时复制(COW):减少 fork 的内存开销
TLB + 多级页表:平衡地址转换速度与空间占用
启发:在缓存、并发等场景中需 量化评估 时空开销。
3.创新方向
更智能的缓存策略:结合机器学习预测内存访问模式,优化页替换算法(如改进 LRU)。
轻量级动态链接:减少共享库加载延迟(如按需分段加载)。
安全增强:
硬件辅助的 内存隔离(如 Intel MPK)
系统调用过滤(如 seccomp)防御恶意 IO 操作。
(结论0分,缺失-1分)
附件
文件名
功能
hello.c
源程序
hello.i
预处理后得到的文本文件
hello.s
编译后得到的汇编语言文件
hello.o
汇编后得到的可重定位目标文件
hello.elf
用readelf读取hello.o得到的ELF格式信息
hello.asm
反汇编hello.o得到的反汇编文件
hello1.asm
反汇编hello可执行文件得到的反汇编文件
hello
可执行文件
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] Linux内核官方文档. 进程管理与内存映射机制. https://www.kernel.org/doc/html/latest/
[2] Ulrich Drepper. 如何编写共享库. 2006. https://www.akkadia.org/drepper/dsohowto.pdf
[3] Randal E.Bryant, David R.O\'Hallaron. 深入理解计算机系统(第三版). 机械工业出版社,2016.
[4] 程序员Buddy的博客. ELF文件格式解析. https://www.cnblogs.com/buddy916/p/10291845.html
[5] 钢琴家的技术博客. Linux进程地址空间探究. https://www.cnblogs.com/pianist/p/3315801.html
[6] FanZhiDong的博客. 动态链接原理剖析. https://www.cnblogs.com/fanzhidongyzby/p/3519838.html
[7] DiaoHaiWei的技术笔记. 计算机系统异常处理机制. https://www.cnblogs.com/diaohaiwei/p/5094959.html
[8] CSDN技术博客. x86-64体系结构下的系统调用实现. Ubuntu系统预处理、编译、汇编、链接指令_ubuntu 对话语料库怎么预处理-CSDN博客
[9] Randal E.Bryant David R.O\'Hallaron.深入理解计算机系统(第三版).机械工业出版社,2016.
(参考文献0分,缺失 -1分)