关注

哈工大计算机系统大作业(2025)——程序人生-Hello’s P2P

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业  计算机与电子信息      

学     号   2023112323           

班     级   23L0505             

学       生    刘奕               

指 导 教 师   刘宏伟               

计算机科学与技术学院

2025年5月

摘  要

本文以“程序人生-Hello's P2P”为主题,系统分析了C程序hello.c从源代码到进程执行的完整生命周期。通过预处理、编译、汇编、链接等阶段,探讨了程序在计算机系统中的转换机制与底层原理。实验基于Ubuntu环境,使用GCC工具链生成中间文件(如.i、.s、.o),并结合ELF格式分析可执行文件的结构。重点研究了进程管理、动态链接、虚拟地址空间及存储管理机制,揭示了操作系统如何通过fork、execve等系统调用加载程序,并管理其执行过程。最后,通过调试工具验证了程序的执行流程与信号处理机制。

关键词:预处理;编译;汇编;链接;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的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 -

结论............................................................................................... - 14 -

附件............................................................................................... - 15 -

参考文献....................................................................................... - 16 -

第1章 概述

1.1 Hello简介

Hello的自白描述了一个程序从菜鸟走向成熟的过程,以下简述Hello的P2P和O2O的整个过程:

P2P:From Program to Process

程序Program:Hello最初的状态是一个程序代码(hello.c)。程序员通过代码编辑器编写、存储并保存为源文件。在这个阶段,程序是静态的,处于一个被动的状态。

编译与链接:通过编译器(如GCC)对源代码进行编译,生成机器码,经过链接器(Linker)将依赖库和其他资源链接到一起,形成一个可执行文件(如hello.exe)。

加载与执行:此时,操作系统通过进程管理启动程序,执行其主函数,进程被创建并进入CPU的调度队列。操作系统通过fork()和execve()系统调用来管理和执行程序,使得它在硬件资源上得以运行。

操作系统管理:操作系统为程序分配资源,如时间片、内存等。进程会在CPU的执行单元上运行,经过指令译码、流水线等步骤执行计算任务,并通过内存管理单元(MMU)将虚拟地址(VA)映射到物理地址(PA)。

硬件支持:程序的执行过程中,硬件系统在存储管理、输入输出管理等方面协作,确保程序能够与硬件设备键盘、硬盘等进行有效互动。

进程结束:程序执行完毕后,操作系统会清理资源,确保程序退出时不会留下任何垃圾数据。此时,程序的生命周期就此结束。

Hello的O2O(Zero to Zero)生命轮回

从零开始(Zero):Hello作为磁盘上的静态文件存在,占用物理存储但未激活。此时其代码段、数据段尚未加载到内存,进程控制块等数据结构也未创建

虚拟化加载:execve()系统调用触发缺页中断,OS通过mmap()将代码数据段映射到进程虚拟地址空间。三级页表配合TLB进行VA→PA转换,物理内存按需分配

动态执行:CPU取指译码执行指令:printf()调用涉及用户态→内核态切换,通过write()系统调用向显卡缓冲区写入字符sleep()触发定时器中断,进程进入阻塞队列异常处理程序实时响应Ctrl+C(SIGINT)等信号

资源回收(Zero):程序终止后,Shell父进程通过waitpid()回收子进程,内核销毁页表映射、释放物理内存、关闭文件描述符。最终所有与Hello相关的数据结构被清除,回归初始零状态

1.2 环境与工具

硬件环境:13th Gen Intel(R) Core(TM) i9-13900HX   2.20 GHz

软件环境:Windows 11  Ubuntu 20.04.6 LTS

开发工具:codeblock,svscode

调试工具:gcc,objdump,edb

1.3 中间结果

文件名

文件作用​​

hello.i​

经过预处理的文件

hello.s

经过编译的汇编文件

hello.o

经过汇编的可重定位目标文件

hello

链接后的可执行文件

hellooelf.txt

hello.oELF

hellooasm.txt

hello.o的反汇编

helloelf.txt

helloELF

helloasm.txt

hello的反汇编

1.4 本章小结

本章介绍了Hello程序从静态代码到动态进程的完整生命周期,揭示了程序在计算机系统中的执行本质。Hello的P2O(Program to Process)过程展现了源代码如何通过编译链接转化为可执行文件,再经由操作系统fork-execve机制加载为活跃进程,最终在CPU调度和内存管理支持下完成执行。其O2O(Zero to Zero)生命周期则更深刻地呈现了程序从磁盘静态存储到内存动态运行,再回归零状态的完整轮回。实验环境采用13代Intel Core处理器与Windows/Ubuntu双系统,配合GCC工具链完成预处理、编译、汇编、链接的全流程。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

1.预处理概念

预处理是C/C++程序编译流程的第一步,由预处理器完成。其本质是文本级处理,对源代码进行文本替换、插入和删除,生成一个扩展后的中间文件,供后续编译阶段使用。预处理主要包括展开#include头文件、宏定义、删除注释等操作。预处理后,所有的头文件内容将被插入到源代码中,宏定义将会被替换为对应的内容,而代码中的注释会被去除。最终,.i文件保存的是经过预处理的纯C语言代码。

2.预处理作用

预处理是C/C++等编程语言编译流程的初始阶段,其通过#include指令将头文件内容的函数声明、结构定义直接插入源码,实现代码复用;通过#define宏定义完成文本替换,简化重复代码或实现常量抽象;借助#ifdef、#if等条件编译指令,动态选择不同平台的代码分支或调试代码,增强可移植性;同时移除所有注释以减少冗余信息,并处理#error、#pragma等特殊指令以控制编译流程或报错。这些操作最终生成一份“纯净”的中间代码,既隐藏了底层平台差异和实现细节,又通过宏展开和条件编译为开发者提供了灵活控制代码逻辑的能力。

2.2在Ubuntu下预处理的命令

gcc -E hello.c -o hello.i

图1 生成预处理文件指令

2.3 Hello的预处理结果解

预处理主要完成以下工作:

  1. 头文件递归展开:#include <stdio.h> 被替换为系统头文件的实际内容,并逐层展开其依赖的头文件
  2. 行号标记:每行以 #数字"文件路径" 开头(如 # 1 "hello.c"),记录代码的原始位置,为编译报错和调试提供定位依据。
  3. 代码结构重组:最终生成的文件以头文件内容为主体,用户代码(如 main 函数)出现在末尾,且宏 MSG 已被替换为 "Hello, World!",注释也被删除。

图2 预处理文件内容

预处理结果通过文本的层级嵌套和路径信息,直观反映了代码的依赖关系与编译器的底层处理逻辑,是理解编译流程和排查头文件问题的重要参考。

2.4 本章小结

预处理是C/C++程序编译的初始阶段,由预处理器对源代码进行文本级处理,通过展开#include头文件、替换#define宏定义、删除注释等操作,生成扩展后的中间文件(.i文件)。预处理不仅为编译器提供标准化输入,还通过文本级的逻辑控制隐藏了底层差异,是理解编译流程和排查代码问题的关键环节。

(第2章0.5分)
第3章 编译

3.1 编译的概念与作用

1.编译的概念

编译是C/C++程序构建的核心阶段,由编译器将预处理后的中间代码(.i文件)转换为如汇编代码。这一过程包括词法分析、语法分析、语义分析、中间代码生成、代码优化等步骤,最终生成汇编文件(.s文件)。编译的本质是将高级语言翻译为机器可理解的指令,同时验证代码的语法和静态语义正确性,为后续链接阶段提供基础。

2.编译的作用

语义验证:检查语法错误、类型不匹配等问题,确保代码逻辑合法

代码翻译:将高级语言转换为汇编或机器指令

性能优化:通过优化器消除冗余代码、简化计算逻辑,提升程序执行效率;
       编译阶段是连接程序员逻辑与计算机硬件的核心桥梁,决定了程序的功能正确性和运行性能。

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s

图3 生成汇编文件指令

3.3 Hello的编译结果解析

3.3.1数据

3.3.1.1 字符串常量处理

.section .rodata 中定义的 .LC0 和 .LC1 对应C代码中的字符串常量。中文字符串 "用法: Hello 学号 姓名 手机号 素数!" 被转义为UTF-8编码的八进制序列(如\347\224\250),直接写入只读数据段

printf 的格式化字符串 "Hello %s %s %s\n" 同样以.string指令静态存储,确保运行时直接引用地址

图4 字符串常量

3.3.1.2 局部变量

​栈存储:subq $32, %rsp 分配32字节栈空间

图5 局部变量处理

-20(%rbp):argc参数

-32(%rbp):argv指针

图6 局部变量处理

-4(%rbp):循环变量i(4字节int)

图7 局部变量处理

3.3.2 算术操作

将指针存储的数据加上立即数1之后再存入到指针。i++

图8 算术操作

3.3.3 关系操作

cmpl比较栈上存储的argc(-20(%rbp))和立即数5

je(jump if equal)检测ZF标志位,当argc == 5时跳转

图9 关系操作

3.3.4数组/指针/结构操作内存布局:argv是指向字符串指针数组的指针,每个元素占8字节指针运算特性:addq $8, %rax:每次增加8字节,严格遵循类型系统movq (%rax), %rsi:解引用操作获取实际字符串地址

图10 数组/指针/结构操作

3.3.5 控制转移

条件转移:cmpl执行argc - 5,设置EFLAGS寄存器je(Jump if Equal)检查ZF标志位,若为1则跳转

图11 条件转移

循环控制:jle(Jump if Less or Equal)检查(SF ≠ OF) or ZF=1

实际判断i-9 ≤ 0

图12 循环控制

无条件跳转:跳过i初始化代码,直接进入循环条件检查

图13 无条件跳转

3.3.6 函数操作

1参数传递机制

命令行参数传递:%edi(32位)传递第一个参数argc(值传递)%rsi(64位)传递第二个参数argv(指针地址传递)

内存分配:argc占用4字节(栈偏移-20)argv占用8字节(栈偏移-32)

图14 参数传递

2函数调用

调用puts函数

图15 puts函数调用

地址计算:lea指令计算字符串.LC0的RIP相对地址

PLT机制:@PLT标记通过过程链接表实现延迟绑定

系统调用exit

图16 exit函数调用

退出码通过%edi传递

3函数返回

%eax储存返回结果

Leave指令自动恢复调用者栈指针

Ret 从栈顶弹出返回地址到%rip

图17 函数返回

3.4 本章小结

编译是C/C++程序构建的核心阶段,将预处理后的中间代码(.i文件)转换为汇编指令(.s文件),涵盖词法分析、语法检查、语义验证、代码优化等关键步骤。其核心作用在于检查语法错误与类型匹配,确保代码逻辑合法;将高级语言转换为机器可理解的汇编指令,屏蔽底层硬件差异;性能优化:通过消除冗余代码、简化计算逻辑提升执行效率。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

1汇编概念

汇编是将编译器生成的汇编代码文件(.s)转换为机器语言二进制文件(.o)的过程,由汇编器完成。其核心任务是将人类可读的汇编指令逐条翻译为机器码,并生成可重定位目标文件,过程包括符号解析、指令编码、节(Section)划分等操作,最终生成包含机器指令、数据和元信息的.o文件,为后续链接阶段提供输入。汇编的本质是建立从符号化指令到二进制机器码的一对一映射,同时生成辅助链接的符号表和重定位信息。

2 汇编作用

指令转换:将汇编指令精确转换为机器码,实现CPU可执行的二进制编码。

符号管理:解析和记录符号(函数名main、全局变量),生成符号表供链接器使用。

节组织:将代码、数据、只读常量等分类存储到不同节(.text、.data、.rodata),优化内存布局。

重定位信息生成:标记需链接阶段修正的地址,确保最终程序能正确运行。

调试支持:生成行号信息(.debug_line)和变量描述(.debug_info),辅助调试工具定位问题。

汇编阶段是机器码生成的最后一步,决定了程序二进制结构的准确性和可链接性,同时不影响逻辑优化。

4.2 在Ubuntu下汇编的命令

 as hello.s -o hello.o

图18 汇编命令

4.3 可重定位目标elf格式

4.3.1 ELF头

ELF(Executable and Linkable Format,可执行与可链接格式)是一种广泛应用于Unix/Linux系统的标准文件格式,用于存储可执行程序、目标代码、共享库及核心转储文件。其核心特点包括模块化的节与段结构:节用于链接阶段的代码/数据组织(如.text、.data),段则描述运行时内存布局(如LOAD段)。ELF文件头定义了体系架构、入口地址等元信息,程序头表(Program Header)指导加载,节头表(Section Header)辅助调试和链接。ELF支持动态链接(通过.interp和.dynamic节),并具有跨平台扩展性,是现代操作系统实现进程内存管理的关键基础。

图19 ELF头信息

4.3.2节头表

节头表(Section Header Table)是ELF(可执行与可链接格式)文件中的一个关键数据结构,用于描述文件中各个节(Section)的属性和位置。每个节头表条目(Entry)对应一个节,记录其名称、类型(如代码、数据、符号表等)、内存地址、文件偏移、大小、访问权限(可读/可写/可执行)等信息。链接器(如`ld`)利用节头表合并代码和数据,而调试工具(如`gdb`)依赖其中的符号表(`.symtab`)和字符串表(`.strtab`)进行符号解析。节头表在静态链接阶段必不可少,但运行时(如加载可执行文件)可被省略(通过`strip`命令移除),此时程序头表(Program Header Table)主导内存加载。

图20 节头表

4.4 Hello.o的结果解析

1.指令基本结构

hello.s内容:
main:
  endbr64
  pushq %rbp
  movq %rsp, %rbp
  subq $32, %rsp

反汇编内容:

0:    f3 0f 1e fa                endbr64

4:    55                          push   %rbp

5:    48 89 e5               mov    %rsp,%rbp

8:    48 83 ec 20              sub    $0x20,%rsp

解析:

endbr64:机器码 f3 0f 1e fa,用于控制流完整性检查。

push %rbp:机器码 55,直接对应单字节操作码。

mov %rsp, %rbp:机器码 48 89 e5,48 为 REX.W 前缀,89 e5 表示 mov r/m64, r64。

sub $0x20, %rsp:机器码 48 83 ec 20,48 为 64 位操作,83 ec 20 表示 sub $0x20, %rsp。

2.函数参数传递

hello.s内容:

movl %edi, -20(%rbp)  ; argc 存入栈中

movq %rsi, -32(%rbp)  ; argv 存入栈中

反汇编内容:

c:    89 7d ec                mov    %edi,-0x14(%rbp)

f:     48 89 75 e0             mov    %rsi,-0x20(%rbp)

解析:

movl %edi, -0x14(%rbp):89 7d ec 对应操作,7d 表示 %edi,ec 是偏移量编码。

movq %rsi, -0x20(%rbp):48 89 75 e0,48 为 64 位前缀,75 表示 %rsi,e0 是偏移量。

3.分支转移

hello.s内容:

cmpl $5, -0x14(%rbp)

je .L2

反汇编内容:

13:  83 7d ec 05              cmpl   $0x5,-0x14(%rbp)

17:  74 19                je     32 <main+0x32>

解析:

je .L2:机器码 74 19,74 是 je 操作码,19 是相对偏移量(0x19 = 25 字节),跳转目标地址为当前指令地址 0x17 + 25 + 2 = 0x32。

操作数不一致:汇编代码中的符号 .L2 被转换为机器码中的相对偏移量,由汇编器和链接器共同解析。

4 函数调用

hello.s内容:

call puts@PLT

call exit@PLT

反汇编内容:

23:  e8 00 00 00 00           call   28 <main+0x28>

            24: R_X86_64_PLT32      puts-0x4

2d:  e8 00 00 00 00           call   32 <main+0x32>

            2e: R_X86_64_PLT32      exit-0x4

解析:

call puts@PLT:机器码 e8 00 00 00 00,其中 e8 是 call 操作码,后 4 字节为占位符(由重定位项 R_X86_64_PLT32 填充)。

重定位机制:链接时根据符号地址修正占位符,例如 R_X86_64_PLT32 表示计算 puts 函数在 PLT 中的相对地址。

5 循环结构

hello.s内容:

movl $0, -4(%rbp)

jmp .L3

.L4:

  addl $1, -4(%rbp)

.L3:

  cmpl $9, -4(%rbp)

  jle .L4

反汇编内容:

32:  c7 45 fc 00 00 00 00 movl   $0x0,-0x4(%rbp)

39:  eb 56                jmp    91 <main+0x91>

8d:  83 45 fc 01              addl   $0x1,-0x4(%rbp)

91:  83 7d fc 09              cmpl   $0x9,-0x4(%rbp)

95:  7e a4                  jle    3b <main+0x3b>

解析:

jle .L4:机器码 7e a4,7e 是 jle 操作码,a4 是偏移量(补码 0xa4 = -92),跳转目标地址为 0x95 + (-92) + 2 = 0x3b。

图21 hello.o文件反汇编

4.5 本章小结

汇编作为机器码生成的核心阶段,将汇编指令精确转换为二进制代码,并构建可重定位目标文件(.o),为链接和加载奠定基础。其核心作用体现在指令编码、符号解析、节组织与重定位信息生成,通过汇编器实现从符号化指令到机器码的一一映射。ELF格式作为标准文件结构,通过模块化的节与段设计,统一了代码、数据与元信息的存储方式,其中节头表管理静态链接所需的符号和地址,程序头表指导运行时内存布局,二者共同支撑跨平台程序的生成与执行。以Hello.o为例,汇编阶段通过地址偏移计算、操作码转换和重定位占位符实现函数调用与分支跳转的动态修正,同时保留调试信息以支持逆向分析。从参数传递的栈帧操作到循环结构的条件跳转,汇编过程严格遵循指令集架构的编码规则,确保逻辑的底层还原。

(第41分)

5章 链接

5.1 链接的概念与作用

1.链接的概念
       链接是程序构建过程中的关键步骤,负责将编译生成的多个目标文件与所需的库文件整合为单一可执行文件或动态库。其本质在于解决代码和数据的跨模块引用问题,包括符号解析、地址分配与重定位。在编译阶段,每个源文件独立生成目标文件,其中包含未解析的符号(如外部函数或全局变量);链接器通过遍历所有目标文件和库,匹配符号的定义与引用,确定每个段(如代码段、数据段)在内存中的最终布局,并修正指令中的相对地址为绝对地址。这一过程使得分散的代码模块能够协同工作,形成完整的可执行逻辑。

2.链接的作用
     链接的核心价值在于实现程序的模块化与资源共享。通过链接,开发者可以将大型项目拆分为多个独立编译的单元,提升开发效率并降低维护成本。例如,标准库函数(如printf)无需在每个程序中重复实现,链接器只需将其从系统库中动态加载或静态嵌入即可。此外,链接支持代码优化,例如静态链接器仅包含程序实际使用的库函数,避免冗余代码;动态链接则允许多个进程共享同一份库文件,减少内存占用。链接还解决了跨平台兼容性问题,通过重定位机制适配不同系统的内存地址空间,确保程序能够正确运行于目标环境。

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   

图22 链接命令

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

5.3.1 ELF头信息

图23 helloELF头信息

5.3.2 节头信息

图24  hello节头表信息

5.3.3 程序头

图25  hello程序头信息

5.3.4 符号表

图26  hello符号表

5.4 hello的虚拟地址空间

1.基础映射一致性:

程序头第一个LOAD段(0x400000,R权限)对应edb中的0x400000-0x401000,第二个LOAD段(0x401000,RE权限)对应0x401000-0x402000(r-x),

第四个LOAD段(0x403e38,RW权限)对应后续rw-区域,包含.data/.bss等可写数据

2.动态链接实现:

INTERP段指定的/lib64/ld-linux-x86-64.so.2在edb中显示为高地址区域的libc.so.6映射,这是动态链接器加载的依赖库DYNAMIC段(0x403e38)信息被正确解析,体现在libc的多个内存区域映射GNU_STACK的RW标志与截图栈区域rw-权限一致,确保栈不可执行

对照验证了ELF加载器严格按照程序头规范:将PHDR描述的结构映射到0x400040通过INTERP段加载动态链接器按LOAD段要求设置代码段(r-xp)和数据段(rw-p)权限处理DYNAMIC段完成符号解析最终形成edb所示完整的内存布局,既遵守了程序头定义,又补充了系统运行所需的额外映射区域。

图27  hello虚拟地址空间

5.5 链接的重定位过程分析

5.5.1链接过程:合并同类节:将所有目标文件的.text节合并,.data节合并等符号解析:解决所有符号引用,确保每个符号都有唯一定义重定位:根据合并后的布局修正代码和数据的引用地址。

hello.o的.text节:

0000000000000000 <main>:  

0:   55                      push   %rbp

1:   48 89 e5                mov    %rsp,%rbp

1c:   e8 00 00 00 00          call   21 <main+0x21> 

hello中的修正结果:

0000000000401125 <main>: 

401125:       55                     

push   %rbp401129:       48 89 e5          

mov    %rsp,%rbp401148:       e8 43 ff ff ff         

call   401090 <puts@plt>

5.5.2 hello重定位实现:

函数调用重定位:将R_X86_64_PLT32类型的puts调用重定位到401090(PLT条目)

401090:       ff 25 66 2f 00 00       jmp    *0x2f66(%rip)  # 404000 <puts@GLIBC>401096:       66 90                   xchg   %ax,%ax

数据引用重定位:

将.rodata引用修正为RIP相对寻址:

40113e:       48 8d 05 c3 0e 00 00    lea    0xec3(%rip),%rax # 402008

5.6 hello的执行流程

Edb查看结果

图28  EDB调用与跳转的各个子程序名或程序地址

地址

类型

说明

0x0000000000400330

数据标识

ABI版本标签

0x0000000000401000

初始化函数

全局构造函数

0x00000000004010f0

入口点

程序第一条指令位置

0x0000000000401125

用户主函数

main函数入口

0x00000000004011c8

终止函数

全局析构函数

5.7 Hello的动态链接分析

在程序调用共享库中的函数时,由于编译器无法预先确定函数地址,编译系统采用了延迟绑定机制,将地址解析推迟到首次调用时完成。这一过程通过全局偏移表(GOT)和过程链接表(PLT)的协同工作实现:动态链接器在加载时会重定位GOT中的条目,填入正确的绝对地址;而PLT中的每个条目则负责跳转到对应的目标函数。使用调试工具edb观察可发现,在dl_init执行后,.got.plt节区的内容会发生显著变化。

Edb查看执行init前:

图29  init执行前

Edb 执行init后

图30 init执行后

5.8 本章小结

在链接阶段,链接器将分散的代码段和数据段合并为一个完整的可执行文件。这种方式使得分离编译成为可能——开发者无需将所有代码集中在一个庞大的源文件中,而是可以将其拆分为多个易于管理的模块,并在最终构建时将它们链接在一起,形成一个完整的可执行程序。

生成可执行文件后,只需在 shell 中输入相应的命令,操作系统便会为其创建进程并执行该程序。

(第51分)

第6章 hello进程管理

6.1 进程的概念与作用

1进程的概念
       进程是操作系统中程序执行的基本单位,代表一个正在运行的程序实例。它不仅包含程序的代码和数据,还涵盖执行过程中所需的系统资源(如内存空间、文件句柄、CPU时间片等),以及记录运行状态的控制信息(如寄存器值、程序计数器、堆栈指针)。操作系统通过进程控制块(PCB)唯一标识和管理每个进程,为其分配独立的虚拟地址空间,确保与其他进程隔离运行。进程的生命周期包括创建、就绪、执行、阻塞、终止等状态,其执行过程由操作系统调度器协调,通过上下文切换实现多任务并发。

2进程的作用
       进程的核心作用在于实现计算机资源的有效管理与多任务并行。通过进程机制,操作系统能够将物理资源(如CPU、内存)虚拟化为多个独立的执行环境,允许用户同时运行多个程序而无需感知底层资源的竞争。例如,浏览器、文本编辑器和音乐播放器可以各自作为独立进程运行,互不干扰。进程的隔离性防止了单个程序的错误或崩溃影响整个系统,同时通过权限控制保障安全性。此外,进程间通信(IPC)机制(如管道、共享内存、消息队列)使得协作型任务能够交换数据,支持复杂应用(如数据库事务、分布式计算)的分工与协同。进程模型还为现代操作系统的稳定性、可扩展性及用户体验奠定了基础。

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

1作用

    Shell 是用户与操作系统内核之间的核心交互接口,负责解析并执行用户输入的命令或脚本。它提供了一种灵活的方式来调用系统程序、管理进程、控制文件操作,并通过环境变量和重定向机制定制运行环境。同时,Shell 支持脚本化编程,能够将复杂任务自动化,显著提升系统管理和开发的效率。此外,它还处理输入输出流,实现管道通信和文件重定向,为用户与系统资源交互提供了高效统一的桥梁。

2 处理流程

Shell的处理流程:

读取输入​:从终端或脚本获取命令输入

解析命令​:分割命令与参数,处理特殊字符(如|、>等)

执行扩展​:展开变量($var)、通配符(*)和命令替换($(cmd))

重定向处理​:设置输入/输出重定向(如>、<)

执行命令​:内置命令,直接由Shell处理(如cd、export);外部程序,通过fork()创建子进程,exec()加载程序

等待返回​:获取并显示命令执行状态

流程循环执行,直到收到退出指令(如exit或Ctrl+D)。

6.3 Hello的fork进程创建过程

当父进程执行fork()系统调用时,操作系统会生成一个并发的子进程。这个新创建的进程会继承父进程的虚拟内存空间的完整独立副本,涵盖代码段、数据段、堆内存、动态链接库以及用户栈空间。此外,子进程会复制父进程所有的文件描述符,这使得子进程能够直接访问父进程已打开的文件资源。fork()调用虽然只被执行一次,但会产生两个不同的返回值:在子进程上下文中返回0,而在父进程上下文中则返回新创建子进程的进程ID(PID)。这两个进程最本质的区别在于它们拥有各自独立的进程标识符,这也是系统区分父子进程的关键依据。

6.4 Hello的execve过程

execve函数用于在当前进程的执行环境中加载并启动一个新的可执行程序。该函数通过指定的可执行文件路径、参数列表和环境变量列表来完成程序的加载和初始化。与fork不同,execve在正常情况下不会返回到调用,只有在加载失败时才会返回错误。当execve成功执行时,它会触发以下操作序列:首先由加载器调用启动代码,该代码负责初始化运行时环境,包括设置程序栈结构;随后将可执行文件的代码段和数据段从存储设备载入内存;最终通过跳转到程序的入口地址,将执行控制权移交给新程序的main函数,实现程序的正式启动。这个过程彻底替换了原进程的执行映像,使得调用进程转变为运行全新的程序。

6.5 Hello的进程执行

操作系统通过进程调度实现多任务并发执行,为每个进程营造独占CPU的假象。当使用调试器单步跟踪程序时,观察到的程序计数器(PC)序列构成逻辑控制流,其指令可能来自可执行文件或动态链接库。由于CPU资源有限,进程需分时共享处理器:每个进程运行一段时间片后被抢占,再由调度器选择其他进程执行。这一过程涉及以下关键操作:调度决策,内核的调度器基于优先级、时间片耗尽或I/O阻塞等事件,决定暂停当前进程(抢占),并选择另一个就绪进程投入运行;上下文切换,当切换进程时,内核需保存当前进程的完整执行状态(上下文),并恢复目标进程的上下文,包括寄存器状态、内存管理信息、内核数据结构等。

上下文切换的流程:当内核决定调度新进程时,按以下步骤执行上下文切换:保存当前上下文,将当前进程的寄存器、PC等状态存入其PCB。更新调度队列,将当前进程移入就绪/阻塞队列,并选择下一个待运行进程。恢复目标上下文,从目标进程的PCB加载寄存器、内存映射等状态。切换地址空间,更新CR3寄存器以切换页表,确保进程内存隔离。跳转执行,将控制权转移至目标进程的PC指向的指令。

用户态与内核态的转换​

用户态→内核态:通过系统调用(read、write)或中断触发。CPU硬件自动保存用户态寄存器到内核栈,并跳转到内核预设的中断处理程序。

内核态→用户态:系统调用或中断处理完成后,内核恢复用户进程的上下文,CPU切换回用户态继续执行。

6.6 hello的异常与信号处理

正常情况:

图31 正常运行

乱按:将输入缓存到缓冲区。乱码被认为是命令,不影响当前进程的执行。

图32 乱按程序输出

Ctrl-c:shell进程受到信号SIGINT回收hello进程。

图33 程序退出

Ctrl-Z:进程收到 SIGSTP 信号, hello 进程挂起。

图34 程序挂起

Ps:打印出了各进程的pid

图35 各进程PID

Jobs:打印出了被挂起进程组的jid,可以看到之前被挂起的hello

图36 被挂起进程信息

Pstree:所有进程以树状图形式显示

图37 进程树状图

Fg:可以把之前挂起在后台的hello重新调到前台来执行,打印出剩余部分

图38 重新执行挂起的进程

Kill:杀死hello进程

图39 杀死hello进程

6.7本章小结

本章详细剖析了hello可执行程序从启动到终止的完整生命周期,重点阐述了进程管理的核心机制。程序执行过程涉及三个关键阶段:首先通过fork-exec机制完成进程创建与程序加载,随后在CPU调度和内存管理支持下实现程序运行,最终通过exit系统调用终止并由父进程回收资源。在整个执行周期中,操作系统通过异常处理、信号传递和中断响应等机制确保程序稳定运行。例如,键盘输入会触发中断通知进程处理I/O事件,而进程调度则通过时间片轮转实现多任务并发。这些底层机制共同构建了程序执行的可靠环境,使得hello程序能够高效地完成其功能。

(第61分)

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:由程序指令直接生成的段内偏移量,在hello反汇编代码中可见的地址表现形式需通过段式内存管理单元(MMU)进行基址叠加计算

线性地址:段基址寄存器(如CS/DS)内容与逻辑地址偏移量相加形成在启用分页机制时作为虚拟地址,否则直接等价物理地址

虚拟地址:每个进程独占的连续地址空间幻觉,hello程序可见的"内存地址"

物理地址:通过页表机制将虚拟地址转换为DRAM芯片级信号CPU地址总线传输的最终信号,直接定位内存芯片存储单元

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

逻辑地址由两部分构成:段选择符(Segment Selector),用于索引段描述符表(GDT或LDT),确定段的基础信息。偏移量(Offset),表示目标地址在段内的相对位置。

线性地址的计算方式:通过段选择符从段描述符表(GDT/LDT)中获取段基址(Base Address)将段基址与逻辑地址中的偏移量相加,得到线性地址。

段式管理的特点:

  1. 按照段分配内存每个段占据一块连续的物理内存区域
  2. 段间不要求连续同一进程的各个段(如代码段、数据段、堆、栈)可以分布在不同的物理内存区域,无需连续存放
  3. 动态内存管理段的分配与释放在进程运行时动态进行
  4. 内存保护机制通过段描述符的访问权限(DPL)​,确保用户程序不能随意修改内核段

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

在Hello程序的执行过程中,操作系统通过页式管理机制完成从线性地址到物理地址的关键转换。当CPU生成线性地址后,硬件内存管理单元(MMU)会依据多级页表结构进行自动转换:首先通过CR3寄存器定位当前进程的顶级页表,随后逐级解析页目录项和页表项,最终获取对应的物理页框号,再结合页内偏移量合成实际访问的物理地址。这为每个进程提供了独立的虚拟地址空间,使得Hello程序可以安全地运行在看似连续而实际可能离散的物理内存中。操作系统通过设置页表项的权限位实现内存保护,确保用户程序不能随意访问内核空间或其他进程的内存区域。同时,借助TLB快表缓存热门页表项来优化转换性能,当发生页缺失时则会触发缺页异常,由内核动态加载所需页面。

图40 地址翻译

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

在CPU生成虚拟地址(VA)后,内存管理单元(MMU)必须通过查询页表条目(PTE)完成虚拟地址到物理地址的转换。这一过程在未优化的情况下可能引发性能损耗,每次转换都需要额外访问内存获取PTE。为缓解此开销,现代处理器采用多级缓存策略:当PTE恰好缓存在L1缓存时,转换延迟可降至1-2个周期;优化在于MMU内置的专用缓存单元——转译后备缓冲器(TLB),它专门存储热点PTE以加速地址转换。虚拟地址(VA)由虚拟页号(VPN)和页内偏移(VPO)构成,TLB命中时直接输出物理页号(PPN)并与VPO拼接形成物理地址(PA)。当TLB未命中时,系统启动四级页表深度查询:CR3寄存器指向顶级页表(L1)的物理基址,VPN被拆分为四个索引段(VPN1-VPN4),每级索引依次定位下级页表基址——VPN1定位L1 PTE获取L2基址,VPN2定位L2 PTE获取L3基址,直至L4 PTE中提取目标PPN。最终PPN与原始VPO组合生成物理地址,同时该PTE被缓存至TLB以加速后续访问。

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

当MMU将物理地址传递给L1高速缓存时,系统会执行缓存查找操作。物理地址被解析为三个部分:缓存偏移量(CO)确定数据块内的具体位置,缓存组索引(CI)定位特定的缓存行组,而缓存标记(CT)则用于身份验证。缓存控制器会立即检查由CI指定的组中是否存在标记匹配且有效位激活的条目。如果找到符合条件的缓存行,系统会在CO指示的位置提取目标数据字节,通过MMU返回给CPU完成本次访问。当发生缓存未命中时,系统会启动层级检索机制,依次向L2缓存、L3缓存至主存储器逐级查询所需数据。在较低层存储中找到目标内容,系统会立即将数据返回给请求方,还会将其载入上一级缓存,并重新发起读取请求以优化后续访问效率。

7.6 hello进程fork时的内存映射

当前进程调用fork函数时,内核会为新进程分配一个独立的pid,并复制当前进程的各种数据。为了为新进程创建虚拟内存,系统会为其创建一个mm_struct结构、区域结构以及页表的副本。此时,两个进程的页面都被标记为只读,并且区域结构被设置为私有的写时复制模式。随着fork的执行,新进程的虚拟内存与父进程相同,形成了初步的虚拟内存映射。当任一进程进行写操作时,写时复制机制会触发新的页面创建,从而确保每个进程拥有独立且私有的地址空间。

7.7 hello进程execve时的内存映射

当bash中的进程执行execve("hello", NULL, NULL)时,进程的内存映射发生了以下变化:

1  execve函数会加载并执行"hello"可执行文件中的程序,替代了当前在bash中的程序。此时,原先的bash进程将被"hello"程序取代。

2 系统会清除已存在的用户区域。这包括释放之前bash进程所占用的内存区域,包括堆栈、数据区等。这是因为execve调用会重新加载新的程序,之前的内存空间不再适用。

3 系统会为新的程序映射私有区域,这通常包括堆区和进程的栈空间。私有区域会被映射到新的虚拟地址空间中,确保新程序能够正常运行。

4 系统会映射共享区域,包括共享库和其他共享资源。这些共享区域可以被多个进程访问,因此它们在内存中只需映射一次,避免了重复加载。

5  execve会设置程序计数器(PC),指向新程序的代码区域入口点。程序计数器会被更新,以便程序能够从正确的位置开始执行,启动"hello"程序的运行。

通过这些步骤,execve调用成功地将bash进程替换为新的"hello"进程,并使得新程序得以正常运行。

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

页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的:

1处理器生成一个虚拟地址,并将它传送给MMU

2 MMU生成PTE地址,并从高速缓存/主存请求得到它

3高速缓存/主存向MMU返回PTE

4 PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。

5 缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。

6 缺页处理程序页面调入新的页面,并更新内存中的PTE

7 缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。

7.9动态存储分配管理

7.9.1 动态内存管理的基本方法

虽然可以使用低级的mmap和munmap系统调用来创建和删除虚拟内存区域,但对于C程序员来说,当需要额外的虚拟内存时,使用动态内存分配器更加方便,同时也能提供更好的可移植性。

(1) 显式分配器

显式分配器要求应用程序显式释放所有已分配的内存块。例如,C标准库提供了一个名为malloc的显式分配器。C程序通过调用malloc函数来分配内存块,调用free函数来释放内存块。

(2) 隐式分配器

隐式分配器要求分配器能够自动检测到哪些已分配的内存块不再被使用,并且自动释放这些内存块。隐式分配器也被称为垃圾收集器,自动回收未使用的内存块的过程叫做垃圾收集。

7.9.2 动态内存管理的策略

(1) 带边界标签的隐式空闲链表

带边界标签的隐式空闲链表使用边界标签来跟踪内存块,内存块之间没有直接的指针连接。每个内存块的头部和尾部都包含边界标签,这些标签记录了块的大小和当前的状态(已分配或空闲)。

(2) 显式空闲链表

显式空闲链表使用链表结构来管理所有空闲的内存块,每个链表节点包含指向下一个空闲块和前一个空闲块的指针。这种方法可以更高效地管理空闲内存块。

7.10本章小结

在Hello程序的执行过程中,操作系统通过多级地址转换机制实现了从程序生成的逻辑地址到最终物理地址的完整映射。首先,CPU生成的逻辑地址经过段式管理转换为线性地址,再通过页式管理转换为物理地址,这一过程借助了TLB快表和四级页表结构来优化性能。现代处理器采用三级缓存体系加速物理内存访问,通过缓存偏移量、组索引和标记的匹配机制实现高效数据检索。在进程管理方面,fork操作通过写时复制技术实现父子进程内存空间的初始共享与后续独立,而execve则彻底重建进程内存映射,包括清除原空间、建立私有区和共享区、重置程序计数器等步骤。当发生缺页异常时,硬件与操作系统协同工作,通过页面替换和调入机制动态维护虚拟内存的可用性。动态内存管理则提供了显式(malloc/free)和隐式(垃圾回收)两种策略,分别采用边界标签或显式链表来组织内存块。这些机制共同构成了现代操作系统内存管理的完整体系,在保证安全隔离的同时实现了高效灵活的内存访问。

(第7 2分)

结论

Hello程序在计算机系统中的完整执行过程总结:

1 程序编写与预处理​

程序员编写hello.c源文件,通过gcc -E进行预处理,展开头文件、宏替换,生成hello.i(纯C代码)。

2 编译阶段​

编译器(gcc -S)将hello.i转换为汇编代码hello.s,完成语法检查、优化,并生成与硬件架构相关的指令。

3 汇编阶段​

汇编器(as)将hello.s翻译为机器码,生成可重定位目标文件hello.o,包含ELF格式的代码段、数据段及符号表。

4 链接阶段​

链接器(ld)合并hello.o与库文件(如libc.so),解析符号引用,重定位地址,生成可执行文件hello,支持动态链接(PLT/GOT机制)。

5 进程创建​

用户在Shell输入./hello,Shell通过fork()创建子进程,execve()加载hello的代码和数据到进程虚拟地址空间,形成独立的执行环境。

6 运行时内存管理​

操作系统通过页表(四级页表+TLB)将虚拟地址转换为物理地址,按需分配内存,处理缺页中断,动态加载代码和数据页。

7 CPU执行与系统调用​

CPU取指执行hello的指令:

printf触发write系统调用(用户态→内核态),输出到终端;

sleep通过定时器中断挂起进程;

信号(如Ctrl+C的SIGINT)由内核递送,调用信号处理程序。

8 进程终止与资源回收​

程序结束或收到终止信号后,内核释放内存、关闭文件描述符,父进程(Shell)通过waitpid()回收子进程状态,完成生命周期。

hello 程序虽小,却完整映射了计算机系统的核心机制:从代码的符号化表达到底层机器指令的转换,从进程的虚拟化执行到物理资源的动态调度,处处体现模块化、分层抽象与硬件协同的设计哲学。计算机科学凝聚了卓越前辈们的智慧,为我们打开了一个全新世界的大门!

(结论0分,缺失 -1分,根据内容酌情加分)

附件

文件名

文件作用​​

hello.i​

经过预处理的文件

hello.s

经过编译的汇编文件

hello.o

经过汇编的可重定位目标文件

hello

链接后的可执行文件

hellooelf.txt

hello.oELF

hellooasm.txt

hello.o的反汇编

helloelf.txt

helloELF

helloasm.txt

hello的反汇编

(附件0分,缺失 -1分)

参考文献

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

  1.    Randal E.Bryant David R.O'Hallaron.深入理解计算机系统(第三版).机械工业出版社,2016.
  2.   DeepSeek - 探索未至之境
  3.   LINUX 逻辑地址、线性地址、物理地址和虚拟地址LINUX 逻辑地址、线性地址、物理地址和虚拟地址 转 - zengkefu - 博客园
  4.   C语言的预处理详解C语言预处理指令详解-CSDN博客

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

原文链接:https://blog.csdn.net/2301_80196255/article/details/147989611

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

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