关注

程序人生

计算机系统原理

大作业

题     目  程序人生-Hellos P2P  

专       业        

学     号             

班   级               

学       生       yyx      

指 导 教 师               

计算学部

2025年9月

摘  要

本文以经典程序 “Hello World” 为研究对象,基于计算机系统原理完整分析其从源码到执行的全生命周期过程。研究覆盖预处理、编译、汇编、链接四大核心环节,深入探讨进程管理、存储管理、IO 管理等系统底层机制,还原 Hello 程序在 Ubuntu 环境下的 “P2P(预处理 - 编译 - 汇编 - 链接)” 与 “O2O(源码到运行)” 全流程。通过实验验证与工具分析,揭示计算机系统软硬件协同工作的内在逻辑,为理解系统底层运行机制提供实践参考。

关键词:计算机系统原理;编译;链接;进程管理;存储管理;IO 机制                            

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分

自媒体发表截图

目  录

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

5.3 可执行目标文件hello的格式

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

6.2 简述壳Shell-bash的作用与处理流程

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 hello的存储管理

7.1 hello的存储器地址空间

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管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献


第1章 概述

1.1 Hello简介

Hello 程序的“P2P”过程即预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)、链接(Linking)的流水线式处理流程:源码经预处理展开宏与头文件,编译转换为汇编语言,汇编生成机器指令级目标文件,链接整合依赖库形成可执行文件。其 “O2O” 过程则是从源码(Online)到运行(Offline):用户通过 Shell 触发执行,系统完成进程创建、地址映射、IO 交互,最终在终端输出 。

1.2 环境与工具

处理器:Intel(R) Core(TM) i9-14900HX (2.20 GHz)

机带RAM 16.0 GB (15.6 GB 可用)

系统类型 64 位操作系统, 基于 x64 的处理器

操作系统 Windows 11 专业版(搭载 VMware Workstation 17 Pro 虚拟机)

虚拟机系统:Ubuntu 20.04.4 LTS(64 位)

编译工具链:GCC 9.0.4(GNU Compiler Collection)

调试工具:GDB 9.2(GNU Debugger)

分析工具:readelf 2.34、objdump 2.34

1.3 中间结果

hello.c 原始源码文件

hello.i 预处理后文件,展开头文件、宏定义,删除注释

hello.s 编译后汇编文件,将 C 语言转换为 x86-64 汇编指令

hello.o 汇编后可重定位目标文件,包含机器指令与重定位信息

hello.out 最终可执行文件

hello 最终可执行文件

hello 最终可执行文件,经链接整合后可直接运行

1.4 本章小结

明确了 Hello 程序的研究核心的 “P2P” 与 “O2O” 流程,梳理了实验所需的软硬件环境与工具链,介绍了各环节生成的中间文件及功能。

(第1章0.5分)
第2章 预处理

2.1 预处理的概念与作用

预处理是编译过程的第一步,由预处理器完成,作用是对源码进行文本级转换,为编译阶段提供纯净、完整的输入文件。核心功能包括:展开 #include 头文件(将标准库或自定义头文件内容嵌入源码)、替换 #define 宏定义(文本替换,无类型检查)、删除注释、处理条件编译指令。

2.2在Ubuntu下预处理的命令

 cpp hello.c > hello.i

图 1预处理命令

2.3 Hello的预处理结果解析

进行了头文件展开,宏替换,注释删除,完成文本替换。

图 2 预处理结果

2.4 本章小结

本章阐述了预处理的概念与文本转换核心作用,验证了 Ubuntu 环境下 GCC 预处理命令的执行流程,通过对比源码与预处理结果,明确了头文件展开、宏替换等关键操作的效果。

(第2章0.5分)


第3章 编译

3.1 编译的概念与作用

编译是预处理后到汇编前的关键阶段,由编译器(gcc 的 cc1 组件)完成,核心作用是将预处理后的.i文件(C 语言源码)转换为汇编语言.s文件。该过程包含词法分析(识别关键字、标识符、常量)、语法分析(检查语法正确性,生成抽象语法树)、语义分析(检查类型匹配、变量作用域)、优化(分为机器无关优化如常量传播,机器相关优化如指令重排),最终将高级语言逻辑映射为特定架构的汇编指令

3.2 在Ubuntu下编译的命令

cc1 hello.i -o hello.s

如果cc1指令不在path中需找到cc1所在位置 用/path/cc1 hello.i -o hello.s

图 3 编译命令

3.3 Hello的编译结果解析

编译出结果为下:

图 4 编译结果

3.3.1字符串常量的处理

常量:

字符串常量

存储在只读数据段.rodata中

图 5 编译结果分析

变量:局部变量在栈帧中分配

图 6 编译结果分析

3.3.2赋值操作

赋值操作:

图 7 编译结果分析

比较与跳转:

3.3.3函数调用的处理

call实现函数调用

图 8 函数调用

3.3.4函数栈帧的构建与销毁

main 函数的栈帧操作:

return进行销毁

图 9 函数构建

3.3.5分支与跳转 

控制循环:

    

3.4 本章小结

展示编译阶段从.i文件到.s文件的转换,揭示了编译器对字符串常量、整型数据、函数调用、栈帧操作的处理逻辑,体现了高级语言语法到汇编指令的映射规则,以及编译器对其的优化。编译阶段是连接高级语言与机器指令的关键,其生成的汇编代码直接决定了程序的底层执行逻辑。

(第3章2分)


第4章 汇编

4.1 汇编的概念与作用

汇编是编译后的核心步骤,由汇编器(as)完成,核心作用是将汇编语言.s文件转换为机器语言二进制的可重定位目标文件.o。该过程将汇编指令翻译为对应架构的机器码,同时处理汇编伪指令,生成包含代码段、数据段、重定位表、符号表等结构的ELF文件。.o文件中的指令已为机器可执行格式。

4.2 在Ubuntu下汇编的命令

as hello.s -o hello.o

图 10 汇编命令

4.3 可重定位目标elf格式

图 11 elf查看

图 12 elf分析

节头表详细解读:

(NULL):占位符节

.text(核心:代码段)

.rela.text(核心:代码段重定位表)

.data(数据段:已初始化全局 / 静态变量)

.bss(数据段:未初始化全局 / 静态变量)

.rodata.str1.8(只读数据:8 字节对齐字符串)

.eh_frame(异常处理帧段)

.rela.eh_frame(异常帧重定位表)

.symtab(核心:符号表)

.strtab(字符串表:符号名称)

图 13 elf分析

readelf -r 输出了两个重定位节(.rela.text、.rela.eh_frame),共 10 个重定位条目

重定位的核心意义是:解决 .o 文件中未解析的地址 / 符号,为链接阶段将 hello.o 与 libc.so 绑定做准备。偏移量:该条目对应目标节(如 .text)中的偏移地址(即需要修正的指令 / 数据位置);信息:高16位 = 符号表索引(对应 .symtab 中的符号),低 16 位 = 重定位类型;类型:R_X86_64_xxx,x86-64 架构专属重定位类型,决定链接阶段的地址修正方式;符号值:符号在 .symtab 中的初始值(.o 文件中多为 0,链接后修正为实际虚拟地址);符号名称 + 加数:需要绑定的目标符号,加数为额外地址偏移(通常为0或-4,用于指令偏移补偿)。

4.4 Hello.o的结果解析

从 objdump 输出中,每一行指令都对应一组完整的机器语言指令,机器语言由操作码和操作数两部分构成,部分指令可无操作数(如栈操作、返回指令)。

55 push %rbp 栈操作:将 %rbp 寄存器值压入栈中

c3 retq 函数返回:将程序执行流交还给调用者

e8 callq  函数调用:跳转到目标函数地址执行

48 8d lea 地址加载:计算内存地址并放入寄存器

48 89 e5 mov %rsp,%rbp e5(对应 %rbp)、隐含 %rsp  寄存器操作数

b8 00 00 00 00 mov $0x0,%eax  00 00 00 00(对应 $0x0)、%eax  立即数 + 寄存器操作数

48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi 00 00 00 00(对应 0x0)、%rip、%rdi  内存地址(相对偏移)+ 寄存器操作数

e8 00 00 00 00 callq 10 <main+0x10> 00 00 00 00对应偏移量 分支转移目标地址(相对偏移)

所以汇编语言是机器码的符号化表示。

hello.s是汇编源代码文件,而objdump的反汇编是目标文件反汇编。

两者的核心执行逻辑(main 函数的栈帧操作、参数传递、函数调用、返回)完全一样。差异集中在两个与重定位相关的指令上,恰好对应 objdump -r 显示的 2 个重定位项。

图 14 objdump -d -r hello.o的输出结果

图 15 hello.s汇编源代码文件

4.5 本章小结

本章阐述了汇编阶段的核心作用,验证了 Ubuntu 下汇编命令的执行流程,通过 readelf 和 objdump 工具分析了 hello.o 的 ELF 结构、重定位表与反汇编结果。汇编过程实现了汇编指令到机器码的转换,生成的.o文件包含完整的机器指令,但需链接阶段修复外部符号引用。机器语言与汇编语言存在一一对应的映射关系,重定位项则为后续链接提供了符号修复的关键信息。

(第4章1分)


5链接

5.1 链接的概念与作用

链接是汇编后的关键步骤,由链接器(ld)完成,核心作用是将可重定位目标文件(hello.o)与所需的系统库文件(如 libc.so)合并,解决符号引用(如puts函数),生成可执行目标文件hello。该过程包含符号解析(绑定符号定义与引用)、重定位(修正.o文件中未确定的地址)、段合并(将多个.o文件的同名段如.text、.rodata合并),最终生成结构完整、地址确定、可被系统加载执行的 ELF 文件,解决了目标文件间的依赖关系与地址引用问题。

5.2 在Ubuntu下链接的命令

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.out

图 16 链接命令

5.3 可执行目标文件hello的格式

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

图 17 readelf结果

图 18 readelf结果

5.4 hello的虚拟地址空间

    

图 19 虚拟地址空间查询

可发现与 5.3 节 ELF 段的虚拟地址一致,验证了程序头表对虚拟地址分配的定义。

5.5 链接的重定位过程分析

hello 拥有完整的动态链接相关段(.plt、.plt.got、.plt.sec),而 hello.o 无这些段,仅包含核心.text 段;

hello 的指令地址是最终有效虚拟地址(main函数位于0x1149,puts@plt 位于 0x1050),而 hello.o 的 main/puts 相关指令地址为临时偏移;

hello 中对外部函数(如 puts)的调用已绑定到 PLT(过程链接表)(callq 1050 <puts@plt>),而 hello.o 中对 puts 的调用是未解析的,指令中包含重定位占位符(通常是 0x0 或临时偏移,需后续重定位修正);

hello 包含程序运行的完整入口流程(_start0x1060,调用__libc_start_main 启动main),而 hello.o 仅包含 main 函数本身,无程序启动 / 终止的完整逻辑。

图 20 链接重定位分析

hello.o 的 .text 段从 0x0 开始(临时偏移地址,无实际运行意义,仅为编译占位);

存在两个明确的重定位项均位于 main 函数内,对应两条未完成地址修正的指令;

重定位项旁标注了关联符号和偏移修正值,这是链接器后续处理的核心依据。

lea 指令的目标是获取 .rodata 段中字符串常量的有效地址,并传递给 puts 函数。但 hello.o 编译阶段,.rodata 段的最终虚拟地址尚未确定(链接时才会分配),因此需要通过 R_X86_64_PC32 重定位,让链接器在链接阶段直接修正占位符 00 00 00 00 为有效相对偏移地址,完成静态重定位。

5.6 hello的执行流程

使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。

图 21 入口查询

入口为0x1060

图 22 准备查询

_start:

初始化进程运行环境(栈、寄存器、命令行参数等);

调用 __libc_start_main,传递 main 函数地址作为参数;

不会直接调用 main,而是通过 __libc_start_main 间接启动 main。

利用gdb中的lay asm ,bt指令

图 23 查看栈

验证完成_start → __libc_start_main → main 的完整调用链。

5.7 Hello的动态链接分析

图 24 查看got前后变化

链接前后GOT 的核心变化,也是动态链接的本质:GOT 地址的内容更新。

图 25 查询链接库的使用

动态链接的核心:编译链接时做准备,运行时才解析绑定,不打包动态库代码,依赖系统共享库;

hello 程序的动态链接核心依赖:libc.so.6(提供核心函数)和 ld-linux-x86-64.so.2(动态链接器)。

5.8 本章小结

本章详细分析了链接的概念、命令与核心流程,通过工具验证了可执行文件的 ELF 结构与虚拟地址空间分布,展示了重定位过程中符号解析与地址修正的逻辑,以及动态链接的延迟绑定机制。链接阶段解决了目标文件与库文件的依赖关系,生成了地址确定、可加载执行的文件,是程序从二进制目标文件到可运行程序的关键。

(第5章1分)


6hello进程管理

6.1 进程的概念与作用

进程是操作系统对运行中程序的抽象,是资源分配与调度的基本单位。进程本质是程序执行的上下文集合。其核心作用包括:隔离资源(通过地址空间隔离不同进程的数据与代码)、实现并发执行(操作系统通过进程切换模拟多任务)、响应用户请求(将程序逻辑转化为实际运行结果)。hello 程序被执行时,操作系统会为其创建独立进程,分配 CPU、内存等资源,直至程序终止后回收资源。

6.2 简述壳Shell-bash的作用与处理流程

在bash中用户与操作系统内核交互的命令行解释器,核心作用是解析用户输入的命令,触发对应程序的执行。

1.读取命令:通过键盘缓冲区读取用户输入的./hello字符串;命令解析:bash 解析命令为程序路径与参数;

2.创建子进程:调用fork()系统调用创建子进程,子进程复制 bash 的地址空间;

3.加载程序:子进程调用execve()系统调用,销毁原有地址空间,加载 hello 程序的 ELF 文件;

4.等待执行:父进程(bash)调用wait()系统调用,阻塞等待子进程终止;

5.回收资源:子进程执行完毕后,内核发送SIGCHLD信号,父进程回收子进程资源,bash 恢复等待用户输入。

6.3 Hello的fork进程创建过程

1.为新进程创建进程控制块(包含进程 ID、状态、优先级等信息),分配唯一 PID。

2.地址空间复制:子进程共享父进程(bash)的代码段。

3.初始化上下文:子进程继承父进程的寄存器状态,修改程序计数器(rip)为fork()返回后的指令地址,设置返回值为0。

4.进程状态转换:子进程创建后进入就绪态,等待 CPU 调度;父进程则进入等待态,等待子进程执行。

6.4 Hello的execve过程

子进程创建后,通过execve("./hello", NULL, NULL)系统调用加载 hello 程序。

1.销毁原有地址空间:子进程销毁从父进程继承的 bash 地址空间(代码段、数据段、栈等);

2.解析 ELF 文件:读取 hello 的 ELF 头部,获取程序头表信息,确定段的加载地址与权限;

3.建立新地址空间:为 hello 程序分配虚拟地址空间,将 ELF 中的.text、.rodata 等段映射到对应虚拟地址;

4.初始化栈与寄存器:设置栈指针(rsp)指向用户栈底部,将程序入口地址写入程序计数器(rip);

5.动态链接初始化:若为动态链接程序,触发动态链接器加载依赖库,完成符号绑定;

6.开始执行:execve()调用返回后,CPU 开始执行 hello 程序的指令,进程进入运行态。

6.5 Hello的进程执行

hello 进程的执行过程本质是操作系统的进程调度与上下文切换过程:

1.调度就绪:hello 进程创建后处于就绪态,等待 CPU 时间片分配;

2.上下文切换:当 CPU 空闲时,调度器选择 hello 进程,保存当前运行进程的上下文(寄存器、PC 等),加载 hello 进程的上下文;

3.用户态执行:CPU 切换至用户态,执行 hello 的用户态代码(main 函数、puts 函数等),此时进程处于运行态;

4.系统调用切换:当执行到puts函数中的write系统调用时,进程触发陷阱(trap),切换至核心态,由内核执行系统调用逻辑;

5.时间片耗尽:若 hello 进程的时间片耗尽,调度器暂停其执行,保存上下文,切换至其他就绪进程;

6.重复调度:直至 hello 程序执行完毕,进程状态转为终止态。

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

6.6 hello的异常与信号处理

 hello执行过程中会出现执行过程中,异常主要分为两类核心类型,涵盖主动触发、程序错误、系统干预,会产生SIGINT,SIGTSTP,SIGSEGV,SIGCHLD等信号。

对于 SIGINT(Ctrl+C):默认行为是终止 hello 进程,内核回收进程占用的 CPU、内存、文件描述符等资源,进程直接退出,无额外输出。

对于 SIGTSTP(Ctrl+Z):默认行为是暂停 hello 进程,将进程从运行态切换为停止态,进程暂停所有执行操作,但资源不会被回收,终端会输出 [1]+ Stopped ./hello 提示。暂停后的进程可通过 fg <作业ID> 命令恢复运行,或通过 bg <作业ID> 命令转入后台运行。

对于 SIGCHLD:默认行为是忽略该信号,bash 会通过 wait() 系列系统调用主动回收 hello 进程的僵尸资源,避免产生僵尸进程。

对于 SIGKILL(强制终止):默认行为是立即强制终止 hello 进程,且无法被进程拦截,内核直接回收进程所有资源,这是处理无响应进程的最终手段。

 程序运行过程中可以按键盘产生结果如下:

Ctrl-Z

图 27 ctrl-z

 jobs

图 28 jobs

  pstree

图 29 pstree

 fg

图 30 fg

 Kill

图 31 kill

异常与信号的处理:Linux 中信号处理通过 “中断驱动” 实现:当信号产生时(如键盘按键),内核暂停当前进程的执行,保存上下文,跳转到对应的信号处理程序);处理完成后,恢复进程上下文,继续执行原指令。

6.7本章小结

本章阐述了进程的概念与核心作用,梳理了 bash shell 处理命令的流程,详细分析了 hello 进程的创建(fork)、程序加载(execve)、执行调度与信号处理机制。进程管理是操作系统实现程序并发执行的基础,信号机制则为进程间通信与异常处理提供了高效方式,确保 hello 程序能够有序执行并响应外部交互。

(第6章2分)


7hello的存储管理

7.1 hello的存储器地址空间

hello 程序运行时涉及四种核心地址类型:

  1. 逻辑地址:程序源码中使用的地址,是编译器生成的相对地址,未经过段式或页式转换;
  2. 线性地址:逻辑地址经段式管理转换后的 32 位或 64 位地址,是段式与页式转换的中间地址;
  3. 虚拟地址:进程地址空间中的地址,由操作系统分配,与物理内存地址无直接关联,进程通过虚拟地址访问内存,无需关注物理地址分配;
  4. 物理地址:物理内存芯片的实际地址,是 CPU 通过地址总线访问内存的最终地址,由内核通过页式管理将虚拟地址转换得到。

hello 程序的虚拟地址空间范围已在第5章4节明确(如.text 段),物理地址则由内核动态分配,进程不可见。

7.2 Intel逻辑地址到线性地址的变换-段式管理

x86-64 架构下,段式管理的核心作用是权限控制与地址隔离,逻辑地址到线性地址的转换过程如下:

  1. 逻辑地址结构:逻辑地址由段选择符(16 位)与偏移量(64 位)组成;
  2. 段选择符解析:段选择符的高 13 位索引指向 GDT中的段描述符,段描述符包含段的基地址、大小、权限等信息;
  3. 地址计算:x86-64 架构下,段基地址默认为 0,因此线性地址 = 逻辑地址偏移量(用户态进程的逻辑地址与线性地址等价);
  4. 权限检查:内核验证当前进程是否有权访问该段,若权限不匹配则触发通用保护异常。

7.3 Hello的线性地址到物理地址的变换-页式管理

页式管理是将线性地址(虚拟地址)转换为物理地址的核心机制,x86-64 架构下采用 4KB 页大小,转换过程:

  1. 虚拟地址拆分:64 位虚拟地址拆分为页目录指针表索引(9 位)、页目录表索引(9 位)、页表索引(9 位)、页内偏移(12 位);
  2. 页表查找:CPU 通过 CR3 寄存器找到当前进程的页目录指针表(PML4)基址,依次通过各级索引查找页目录表(PDPT)、页目录表(PD)、页表(PT),最终找到页表项(PTE);
  3. 物理地址计算:页表项(PTE)存储该虚拟页对应的物理页框基地址(4KB 对齐),物理地址 = 物理页框基址 + 页内偏移;
  4. 缺页检查:若页表项标记为 “未分配”(P 位 = 0),则触发缺页中断(#PF),内核分配物理页框并更新页表项。

7.4 TLB与四级页表支持下的VA到PA的变换

解决四级页表查找的性能开销,CPU 通过 TLB(快表)缓存近期使用的虚拟页 - 物理页映射关系,转换流程优化如下:

  1. TLB 查找:CPU 先查询 TLB,若虚拟地址对应的页表项已缓存(TLB 命中),直接从 TLB 获取物理页框基址,跳过四级页表查找,耗时仅 1-2 个 CPU 周期;
  2. TLB 未命中:若 TLB 中无该映射(TLB 未命中),CPU 执行四级页表查找,获取物理页框基址,同时将该映射写入 TLB,供后续访问复用;
  3. 四级页表协同:TLB 与四级页表形成 “缓存 - 主存” 层级,TLB 缓存热点映射,四级页表存储完整映射,平衡性能与存储开销。

hello 程序的 main 函数与 puts 函数被频繁访问,其虚拟地址到物理地址的映射会被缓存到 TLB,大幅提升执行效率。

7.5 三级Cache支持下的物理内存访问

CPU 的三级 Cache(L1、L2、L3)用于缓存物理内存中的数据与指令,减少 CPU 访问内存的延迟(Cache 访问延迟为 ns 级,内存为百ns级),访问流程:

  1. Cache查找:CPU 通过物理地址查询 L1 Cache(按数据 / 指令分离缓存,hello 的指令缓存于L1I,数据缓存于L1D);
  2. Cache命中:若数据 / 指令已在 Cache 中(命中),直接从 Cache 读取,无需访问物理内存;
  3. Cache未命中:若未命中,依次查询 L2、L3 Cache,若均未命中则访问物理内存,将读取的数据 / 指令写入Cache(按 Cache 行大小,通常64字节),供后续访问复用;
  4. 缓存策略:采用 LRU(最近最少使用)算法替换 Cache 中的旧数据,确保热点数据(如hello的循环指令、频繁访问的变量)常驻Cache。

7.6 hello进程fork时的内存映射

hello 进程通过fork()创建时,内核采用写时复制(Copy-On-Write)机制进行内存映射,核心流程:

  1. 共享页映射:fork 初期,子进程(hello 进程)与父进程(bash)共享所有虚拟页的物理映射(代码段、数据段、库文件等),页表项标记为“只读”;
  2. 写时复制触发:当子进程执行execve()加载 hello 程序时,需要修改虚拟地址空间,此时触发写时复制 —— 内核为子进程分配新的物理页框,复制原页数据,更新子进程的页表项,将权限改为 “可写”;
  3. 独立地址空间:写时复制完成后,子进程拥有独立的物理内存映射,与父进程的地址空间完全隔离,后续修改不会影响父进程。

7.7 hello进程execve时的内存映射

execve()系统调用加载 hello 程序时,内核重新构建进程的虚拟地址空间映射,核心步骤:

  1. 销毁旧映射:释放子进程从父进程继承的虚拟页映射(bash 的代码段、数据段等);
  2. 加载 ELF 段:根据 hello 的 ELF 程序头表,将.text、.rodata 等段映射到对应的虚拟地址,设置权限;
  3. 映射库文件:动态链接器加载 libc.so 等依赖库,将库的代码段、数据段映射到进程虚拟地址空间的高地址区;
  4. 建立栈映射:分配用户栈空间,映射为R-W权限,初始化栈帧(包含命令行参数、环境变量指针);
  5. 建立堆映射:分配堆空间,用于动态内存分配。

7.8 缺页故障与缺页中断处理

缺页故障(Page Fault)是虚拟地址对应的物理页未分配或未加载到物理内存时触发的异常,hello 程序运行时的缺页中断处理流程:

  1. 缺页触发:CPU 执行指令时,若虚拟地址对应的页表项P位(存在位)为0,触发#PF(缺页中断);
  2. 中断响应:内核暂停 hello 进程,切换至核心态,保存进程上下文;
  3. 原因判断:内核检查缺页原因(如页未分配、页被换出到磁盘、权限错误);
  4. 处理缺页:分配页:内核分配物理页框,更新页表项(设置 P 位 = 1,写入物理页基址);页被换出:从磁盘交换分区加载页到物理内存,更新页表项;
  5. 恢复执行:缺页处理完成后,内核恢复 hello 进程的上下文,返回触发缺页的指令,重新执行(此时页已存在,可正常访问)。

7.9动态存储分配管理

7.10本章小结

本章详细阐述了 hello 程序运行过程中的地址类型与转换机制,包括段式管理的权限控制、页式管理的地址转换、TLB 与 Cache 的性能优化,以及 fork/execve 时的内存映射、缺页中断处理与动态内存分配策略。存储管理的核心是通过虚拟地址空间隔离进程资源,通过页式转换与缓存机制平衡内存使用效率与访问性能,确保 hello 程序能够高效、安全地访问内存资源。

(第7章 2分)


8hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

Linux 操作系统将所有 IO 设备(如键盘、显示器、磁盘)抽象为文件,采用 “文件描述符(FD)” 统一管理,核心方法包括:

  1. 设备文件化:每个 IO 设备对应 /dev 目录下的一个设备文件(如显示器对应 /dev/tty),进程通过操作设备文件间接操作 IO 设备;
  2. 统一 IO 接口:所有设备的 IO 操作(读、写、打开、关闭)均通过标准 Unix IO 函数(open、read、write、close)实现,设备差异由内核驱动隐藏;
  3. 驱动程序隔离:设备驱动程序是内核与硬件的桥梁,负责将用户态的 IO 请求转换为硬件指令(如向显示器发送显示信号),进程无需关注硬件细节;
  4. 缓冲区机制:内核为 IO 设备设置缓冲区(如键盘缓冲区、显示器输出缓冲区),减少 CPU 与设备的交互次数,提升 IO 效率。

8.2 简述Unix IO接口及其函数

Unix IO 接口是 Linux 系统提供的标准 IO 函数集合,核心函数及功能:

open(const char *path, int flags, mode_t mode):打开文件(包括设备文件),返回文件描述符(FD),flags 指定打开模式(如 O_RDONLY 读、O_WRONLY 写),mode 指定文件权限;

read(int fd, void *buf, size_t count):从文件描述符 fd 指向的文件中读取 count 字节数据,存入buf缓冲区,返回实际读取字节数;

write(int fd, const void *buf, size_t count):将 buf 缓冲区中的 count 字节数据写入 fd 指向的文件,返回实际写入字节数;

close(int fd):关闭文件描述符,释放内核分配的文件资源;

ioctl(int fd, unsigned long request, ...):执行设备特定的控制操作,适用于特殊 IO 需求。

Linux 系统默认分配三个标准文件描述符:0(STDIN,标准输入,对应键盘)、1(STDOUT,标准输出,对应显示器)、2(STDERR,标准错误,对应显示器)。hello 程序的 printf 输出默认使用 STDOUT(FD=1)。

8.3 printf的实现分析

printf 函数的核心功能是格式化输出字符串,其底层实现依赖 Unix IO 接口与系统调用,完整流程:

格式化字符串生成:printf 接收格式化字符串(如 "% s\n")与参数(如 "Hello World"),调用vsprintf函数将参数按格式嵌入字符串,生成最终要输出的字符串(如 "Hello World\n"),存储在用户态缓冲区;

触发系统调用:printf 检查输出缓冲区,若缓冲区满或包含换行符(\n),调用write系统调用,将缓冲区数据写入 STDOUT(FD=1);

系统调用处理:内核接收write请求,切换至核心态,根据 FD=1 找到对应的设备文件(/dev/tty),调用显示器驱动程序;

驱动程序执行:显示器驱动程序将字符串的 ASCII 码转换为字模库中的点阵数据(如每个字符的 RGB 颜色信息),写入显示内存(VRAM);

硬件显示:显示芯片按刷新频率逐行读取 VRAM 中的点阵数据,通过信号线传输至液晶显示器,最终在屏幕上显示字符串。

8.4 getchar的实现分析

getchar 函数的核心功能是从标准输入读取一个字符,底层依赖键盘中断与 Unix IO 接口,实现流程:

键盘中断触发:用户按下键盘按键时,键盘控制器发送中断信号(IRQ1),CPU 暂停当前进程(如 hello 程序),切换至核心态,执行键盘中断处理子程序;

扫描码转换:中断处理子程序读取键盘发送的扫描码(如按键 'a' 对应扫描码 0x1e),将其转换为 ASCII 码('a' 对应 0x61),存入内核的键盘缓冲区;

函数调用阻塞:getchar 函数调用read(0, buf, 1)系统调用,若键盘缓冲区为空,内核将 hello 进程设置为阻塞态,等待键盘输入;

数据读取:当用户按下回车键(表示输入完成),内核唤醒 hello 进程,将键盘缓冲区中的 ASCII 码读取到用户态 buf 缓冲区;

返回结果:getchar 函数从 buf 中取出第一个字符,作为返回值返回,完成字符读取。

8.5本章小结

本章阐述了 Linux IO 设备的文件化管理方法,梳理了 Unix IO 核心函数的功能,深入分析了 printf(输出)与 getchar(输入)的底层实现流程。IO 管理的核心是 “抽象与隔离”:通过文件描述符统一 IO 接口,通过驱动程序隔离硬件细节,通过缓冲区与中断机制提升 IO 效率。hello 程序的 IO 操作本质是用户态函数调用、内核态系统调用、硬件驱动执行的协同过程,体现了操作系统对 IO 设备的高效管控。

(第8章 1分)

结论

hello 程序全生命周期总结:

  1. 预处理阶段:展开头文件与宏定义,删除注释,生成纯净源码文件hello.i,为编译提供输入;
  2. 编译阶段:通过词法、语法、语义分析将 C 语言转换为汇编指令hello.s,完成高级语言到汇编的映射;
  3. 汇编阶段:将汇编指令翻译为机器码,生成可重定位目标文件hello.o,包含代码段、数据段与重定位信息;
  4. 链接阶段:解析外部符号引用,修正地址,合并段与库文件,生成可执行文件(hello),解决依赖关系;
  5. 进程创建与加载:Shell 通过 fork 创建子进程,execve 加载 hello 程序,内核构建虚拟地址空间与内存映射;
  6. 进程执行与调度:操作系统通过进程调度分配 CPU 时间片,实现虚拟地址到物理地址的转换,执行机器指令;
  7. IO 交互:通过 printf 调用 write 系统调用,将字符串输出到显示器;若包含 getchar 则等待键盘输入;
  8. 进程终止:程序执行完毕后,内核回收进程资源,返回 Shell,生命周期结束。

计算机系统设计与实现的感悟:

通过剖析 hello 程序的 “程序人生”,深刻体会到计算机系统 “分层抽象、协同工作” 的核心设计思想:

分层抽象降低复杂度:从高级语言(C)到汇编、机器码,从虚拟地址到物理地址,从 IO 函数到硬件驱动,每一层都通过抽象隐藏底层细节,让开发者与进程无需关注底层实现,仅需调用上层接口;

软硬件协同是核心:软件层面(编译器、链接器、内核)负责逻辑组织与资源管理,硬件层面(CPU、内存、IO 设备)负责指令执行与数据存储,二者通过指令集、地址总线、中断机制紧密协同,缺一不可;

基于本次研究,提出创新设计思路:

内存映射预加载:针对 hello 这类小型程序,可在 execve 阶段预加载所有段到物理内存,避免运行时缺页中断,提升启动速度;对于大型程序,则采用 “热点页优先加载” 策略,兼顾内存占用与启动效率。

(结论0分,缺失-1分)


附件

hello.c 原始源码文件

hello.i 预处理后文件,展开头文件、宏定义,删除注释

hello.s 编译后汇编文件,将 C 语言转换为 x86-64 汇编指令

hello.o 汇编后可重定位目标文件,包含机器指令与重定位信息

hello.out 最终可执行文件

hello 最终可执行文件


参考文献

为完成本次大作业你翻阅的书籍与网站等

[1]  https://www.cnblogs.com/pianist/p/3315801.html.

[2] Randal E. Bryant, David R. O'Hallaron.深入理解计算机系统[M]. 北京:机械工业出版社.

(参考文献0分,缺失 -1分

转载自CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2401_87726207/article/details/156565440

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--