程序人生-Hello’s P2P

题     目  程序人生-Hellos P2P  

专       业   计算学部   

学     号        2023113237        

班   级         23L0518         

学       生           田宇航   

指 导 教 师           史先俊       

计算机科学与技术学院

20255

摘  要

本论文以“Hello”程序为研究对象,深入探讨其从诞生到运行的完整生命周期,全方位解析程序在计算机系统中的运作原理。本文从程序开发的环境与工具搭建出发,详细阐述预处理、编译、汇编、链接等关键环节的概念、操作命令及结果解析,揭示程序代码如何逐步转化为可执行文件。在程序执行阶段,深入剖析进程管理、存储管理与 IO 管理机制,涵盖进程创建与执行流程、地址空间变换、内存映射、缺页处理,以及 Linux 系统的IO设备管理和函数实现等内容。通过对“Hello”程序的全流程分析,全面展现计算机系统中程序运行的底层逻辑与复杂过程,为理解程序设计与系统架构提供清晰且深入的视角。总体阐述了计算机整体系统组成。

关键词:程序;计算机系统;复杂过程;                            

目  录

第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" 程序的生命周期始于源码文件,经过预处理、编译、汇编和链接四个阶段生成可执行文件,再通过进程管理、存储管理和 IO 操作实现运行时的端到端交互。

P2P(编译阶段):

预处理:预处理器对源码进行宏替换、头文件包含和条件编译,生成.i文件。

编译:编译器将预处理后的代码翻译为汇编语言,生成.s文件。

汇编:汇编器将汇编代码转换为机器指令,生成可重定位目标文件.o。

链接:链接器将.o文件与标准库链接,解析符号引用,完成地址重定位,生成可执行文件。

O2O(执行阶段):

进程创建:Shell 通过fork()创建子进程,复制父进程地址空间。

程序加载:子进程调用execve()加载 "Hello" 可执行文件,通过段页式存储管理建立虚拟地址到物理地址的映射。

内存映射:线性地址通过 TLB 快速转换,访问物理内存中的指令和数据,缺页时触发中断处理。

IO交互:程序通过write()系统调用将 "Hello" 字符串输出到标准输出设备,内核通过文件描述符操作驱动程序完成物理 IO。

1.2 环境与工具

硬件环境:13th Gen Intel(R) Core(TM) i7-13700H   2.40 GHz,RAM 16.0 GB

软件环境:Windows 10 64位;Ubuntu 20.04 LTS 64;

开发工具:GCC,Codeblocks,GDB,objdump

1.3 中间结果

名称

作用

hello.c

hello程序c语言源文件

hello.i

hello.c预处理生成的文本文件

hello.s

由hello.i编译得到的汇编文件

hello.o

hello.s汇编得到的可重定位目标文件

elf.txt

readelf生成的hello.o的elf文件

hello

hello.o和其他文件链接得到的可执行目标文件

hello1.elf

readelf生成的hello的elf文件

hello.dis

包含可执行文件的反汇编代码,用于逆向分析。

1.4 本章小结

本章根据hello的一生介绍了hello的P2P和020过程,分析hello了需要的软硬件环境和开发工具,还罗列出了中间结果文件的名字及其作用。


第2章 预处理

2.1 预处理的概念与作用

预处理是 C 语言等编程语言在编译过程中的第一个阶段[7]。在这个阶段,预处理器会处理源代码中以 “#” 开头的预处理指令,如#define、#include、#ifdef等。简单来说,预处理就是将要包含的文件插入原文件中、将宏定义展开、根据条件编译命令选择要使用的代码,最后将这些代码输出到一个中间文件中等待进一步处理结果就得到了另一个C程序,通常是以.i作为文件扩展名。

2.2在Ubuntu下预处理的命令

gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i

(图2.2)

2.3 Hello的预处理结果解析

预处理器cpp处理以#开头的语句,因为<stdio.h >,<unistd.h>,<stdlib.h>都是标准文件库,所以它会在linux系统中的环境变量下寻找这三个库,它们在/usr/include下,cpp将这三个库直接插入代码中。因为这三个库中还有#define #ifdef等,所以cpp还需要将这些展开,所以hello.i最后没有#define

(图2.3)

2.4 本章小结

本章讲述了预处理的概念与作用,并使用gcc的预处理命令gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i将hello.c文件预处理为hello.i文件,对hello.c来说是cpp读取了这三个系统头文件的内容,并把它插入此程序文本中,然后得到了另一个C程序hello.i。


第3章 编译

3.1 编译的概念与作用

编译是将高级编程语言(如 C、C++)编写的源代码转换为低级机器语言(汇编或机器码)的过程。在这个阶段,编译器会对预处理后的代码进行词法分析、语法分析、语义分析、优化,并生成目标代码。        

3.2 在Ubuntu下编译的命令

gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s

(图3.2)

3.3 Hello的编译结果解析

(图3.3)

3.3.1数据

1.常量

 给i赋初值0,i与10比较,i加1,输出argv[1],argv[2]时的下标1,2,以及argv[3]中的下标3,argc != 5

整数/浮点常量:直接嵌入到指令中。

(图3.3.1.1.1  argc != 5)

(图3.3.1.12  i= 0)

(图3.3.1.2.1 地址偏移常量)

字符串常量:存储在只读数据段(如 .rodata 或 .data 段),并通过地址引用。“用法: Hello 学号 姓名 秒数!\n”"Hello %s %s %s\n"

(图3.3.1.2.2 字符串)

2. 变量

int argc是局部变量存储在寄存器或者栈中,在程序中声明,为其分配栈空间。

(图3.3.1.2.3)

i是int型4字节局部变量

(图3.3.1.2.4)

3.3.2赋值操作符(=)

赋值操作符用于将右侧表达式的值存储到左侧变量中。i = 0

(图3.3.2)

3.3.3算术操作

i++为算数操作,addl 是32位整数加法,-4(%rbp) 是变量 i 的栈地址

(图3.3.3.1)

 argv[1]等价于 *(argv + 1);argv[2] *(argv + 2);argv[3]*(argv + 3);argv[4]*(argv + 4)指针加法通过 addq 实现。char* 步长为1,char**(argv)步长为8

(图3.3.3.2)

3.3.4关系操作

argc != 5比较栈上的 argc(-20(%rbp))和立即数 5,设置标志寄存器

图(3.3.4.1)

i<10,在汇编代码中转换为了i<=9,比较栈上的 i(-4(%rbp))和立即数 9

图(3.3.4.2)

3.3.5数组/指针操作

char *argv[],通过寄存器 %rsi 传入

(图3.3.5)

3.3.6类型转换

atoi(argv[4])用到了强制转换,它把字符串转换成整型数

(图3.3.6)

3.3.7控制转移

if (argc != 5)比较 argc 和 5,设置标志寄存器(ZF=1 如果相等)。je .L2若 ZF=1(即 argc == 5),跳转到 .L2(循环开始);否则继续执行错误处理逻辑。

(图3.3.7.1)

for (i = 0; i < 10; i++);jmp .L3跳过循环体,直接执行条件检查(for 循环的初始条件检查)。cmpl $9, -4(%rbp)比较 i 和 9(因为 i < 10 等价于 i <= 9)。jle .L4
若 i <= 9(ZF=1 或 SF≠OF),跳回循环体 .L4。

(图3.3.7.2)

3.3.8函数操作

printf 调用:格式字符串地址通过 edi传递,argv[1]/[2]/[3] 分别存入 rsi、rdx、rcx。movl $0, %eax 表示没有浮点参数

(图3.3.8.1)

exit(1):直接终止进程,不返回到调用者

(图3.3.8.2)

atoi(argv[4]):argv[4] 通过 rdi 传递

(图3.3.8.3)

sleep():调用sleep(),首先是将atoi转换后的值保存到%edi中,然后调用sleep。

(图3.3.8.4)

getchar():调用getchar(),没有参数。

(图3.3.8.5)

3.4 本章小结

在本章内容中,首先对从源文件hello.i转换为汇编文件hello.s的具体过程展开了深入剖析,细致地阐述了这一转换步骤中的关键环节与内在逻辑。随后,进一步对hello.s文件内的程序代码进行了详尽的解读,不仅涵盖了C语言中丰富多样的数据类型,如整型、浮点型、字符型等的定义、特点及使用方式,还深入分析了各类操作指令,包括算术运算指令、逻辑运算指令、数据传输指令等在代码中的具体应用与实现机制。经过这样的分析过程,能够明显发现其相较于原始状态,更加贴近计算机底层的运行原理,与机器的交互和关联也更为紧密,有助于我们从更深层次理解程序的执行机制与本质。


第4章 汇编

4.1 汇编的概念与作用

概念:汇编是将汇编语言源文件(.s)转换为包含机器语言二进制代码目标文件(.o)的过程,汇编器将汇编指令翻译为机器指令代码,并生成符号表等信息。

作用:实现汇编语言到机器语言的转换;生成可重定位代码,便于多模块链接;可对指令序列进行优化;检查源文件语法错误,辅助程序调试。

4.2 在Ubuntu下汇编的命令

gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o

(图4.2)

4.3 可重定位目标elf格式

4.3.1.ELF头

ELF 头是 ELF 文件开头的一段固定格式的数据结构,它包含了整个 ELF 文件的元数据信息,例如文件的类型(可执行文件、共享对象文件、目标文件等)、机器架构类型(如 x86、ARM 等)、ELF 头的大小、程序头表和节头表的位置和大小等。

图(4.3.1)

4.3.2节头部表

节头部表中描述其他节的位置和大小,还包括包括节的名称、类型、地址、偏移量、对齐等。

(图4.3.2)

4.3.3.rel.text和.rel.data

.rel.text(重定位节) 一个.text节中位置的列表,包含了.text节中需要进行重定位的信息。.rel.data是被模块引用或定义的所有全局变量的重定位信息。链接时,需要重定位函数位置(exit, printf, atoi, sleep, getchar)和.rodata中LC0和LC1两个字符串中的信息。

(图4.3.3)

4.3.4符号表

.symtab一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。Name是字符串中的字节偏移,指向符号的以null结尾的字符串名字,value是据定义目标的节的起始位置偏移,size是目标的大小(以字节为单位)。Type是符号的种类,有函数、数据、文件等,Binding简写为bing,表示符号是本地的还是全局的,符号表还可以包含各个节的条目,以及对应原始源文件的路径名的条目。

(图4.3.4)

4.4 Hello.o的结果解析

反汇编结果如下。

(4.4.1)

hello.s:

(图4.4.2)

反汇编和hello.s在代码段很相像,但是反汇编左侧增加了汇编语言对应的机器语言指令。机器语言是由0/1所构成的序列,在终端显示为16进制表示的。汇编中movl $0, -4(%rbp),反汇编中为c7 45 fc 00 00 00 00

分支转移:

hello.s分支转移目标位都是使用.L*表示,hello.o反汇编之后,目标位置变成具体的地址。段名称在hello.s只是助记符,在hello.o机器语言中不存在。.s 文件:使用符号标签(如 .L2),由汇编器在生成目标文件时计算偏移量。反汇编:显示实际跳转的相对偏移量(如 0e 表示跳过14字节)。

函数调用:

hello.s中函数调用是call+函数名,在反汇编文件中目标地址变成了当前的PC值,因为都调用外部函数,所以需要在链接阶段重定位。

操作数:

hello.s中立即数为十进制,反汇编文件中都是二进制的,在终端显示中所有立即数都是十六进制。

4.5 本章小结

本章聚焦于程序编译与底层分析,先是深入剖析从 hello.s 逆向追溯至 hello.c 的过程,揭示高级语言与汇编语言之间的转换逻辑。接着,对 hello.s 对应的 ELF 文件展开细致解读,深入挖掘其文件头、节区等关键信息。同时,将 hello.s 与 hello.o 的反汇编代码进行对比,通过逐条指令的比对,精准阐述汇编指令与机器指令在表现形式、执行原理上的异同之处。基于此,后续环节将通过链接操作,使 hello.o 最终转化为可执行文件,完成从代码到可运行程序的蜕变。

5链接

5.1 链接的概念与作用

概念:

从 hello.o 到 hello(可执行文件)的链接过程,是将编译生成的目标文件 hello.o 与所需的库文件、其他目标文件进行组合,并解决文件内部符号引用、调整内存地址等问题,最终生成可在操作系统上直接运行的二进制可执行文件的过程。

作用:

符号解析与绑定:hello.o 中存在对函数、变量等符号的引用,链接时会将这些未定义的符号与标准库、其他相关目标文件中的符号定义进行匹配,确定每个符号的实际内存地址,确保程序能正确调用相关功能。

重定位:hello.o 里的代码和数据地址是相对地址,链接器根据可执行文件的内存布局,对其进行重定位,将代码和数据调整到合适的绝对内存地址,保证程序运行时能准确访问指令和数据。

合并段与生成布局:将 hello.o 中的代码段、数据段等与其他文件的同类段合并,生成可执行文件最终的内存段布局,明确各部分在内存中的位置和大小。

整合库功能:将 hello.o 运行所需的库函数代码整合进来,比如 C 程序常用的标准 I/O 库函数,使程序具备完整功能,无需重新编写基础功能代码。

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

(图5.2)

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

1.ELF头

hello(可执行文件)与hello.o(目标文件)的 ELF 头基础信息类型相同,但hello的 ELF 头增加了程序入口点地址,用于指定程序执行的首条指令位置。同时,hello新增程序头表,且节与段的数量和结构经过整合优化,还包含动态链接等信息,而这些是hello.o所不具备的。

(图5.3.1)

2. 节头表

从 `hello.o` 到 `hello` 的链接过程中,`hello.o` 里原本相对独立的节在 `hello` 中有了实际内存地址。这些地址信息连同节的大小、偏移量等属性,清晰描述了每个节在文件中的位置与特征。 链接器在处理时,会将各文件的同类节合并为段。例如,多个 `hello.o` 及库文件中的代码节(`.text`)合并成可执行文件中的代码段,数据节(`.data`、`.bss`)合并为数据段。随后,链接器依据段的大小和偏移量,对符号地址进行重定位,确保程序运行时,各符号(函数、变量等)能准确访问到对应代码与数据。

(图5.3.2)

3.重定位节

与之前的重定位节完全不同,这是链接重定位的结果。

(图5.3.3)

4.符号表

链接进行符号解析后,并未在main中定义的符号也有了类型,符号表无需加载到内存。

(图5.3.4)

5.程序头

程序头部表描述了可执行文件的连续的片与连续内存段的映射,可执行目标文件的内容初始化两个内存段,代码段和数据段。

(图5.3.5)

5.4 hello的虚拟地址空间

程序从0x00400000处开始存放。再看程序头,它告诉链接器运行时需要加载的内容,它还提供动态链接的信息。每一个表项提供各段在虚拟地址空间和物理地址空间的各方面的信息。

    

(图5.4)

Start Addr

内存段的起始虚拟地址(十六进制)

0x00400000

End Addr

内存段的结束虚拟地址(十六进制)

0x00401000

Size

段大小(字节/KB/MB)

8KB

Offset

该段在文件中的偏移量(若为文件映射)

0x00000000

Perms

权限标志:
r=可读, w=可写, x=可执行, p=私有, s=共享

r-xp(私有可读可执行)

objfile

关联的文件或设备名(匿名内存显示为 [anon] 或 [stack])

/path/to/hello

内核加载 ELF:

解析程序头表,将 LOAD 段映射到内存(mmap),设置权限(r-xp/rw-p)。

动态链接器工作:

加载共享库(如 libc.so),根据重定位表和符号表解析外部函数地址。

进程运行:

代码段(r-xp)执行指令,数据段(rw-p)存储变量,堆栈动态扩展。

5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。

结合hello.o的重定位项目,分析hello中对其怎么重定位的。

分析hello与hello.o的不同:

hello是符号解析和重定位后的结果,链接器会修改hello中数据节和代码节中对每一个符号的引用,使得他们指向正确的运行地址。

1.函数调用:

hello.o与其他库链接,hello.c中使用的库中的函数就被加载进来了,如exit、printf、sleep、getchar、atoi等函数。在hello.o中函数调用是call 0 + 重定位项,

在hello中函数调用为call <PLT地址>

(图5.5.1)

2.条件控制和函数调用地址都有改变

(图5.5.2)

  1. 全局数据访问:使用固定虚拟地址

(图5.5.3)

链接的过程:在链接过程中,链接器首先扫描各个目标文件。每个目标文件包含了编译生成的代码段(存放程序指令)、数据段(存放已初始化的数据)以及未初始化数据段(BSS 段)等。链接器按照特定的规则,将这些分散在不同目标文件中的函数段和数据段,依次累积、合并到一起。

同时,链接器还需要处理目标文件中的符号引用。符号可以是函数名、全局变量名等。在编译单个源文件时,对于未定义的符号引用(比如一个源文件调用了另一个源文件中定义的函数),编译器会暂时标记这些引用。链接器的另一个重要工作就是解析这些符号引用,找到符号的实际定义位置,并建立正确的关联,使得程序在运行时能够准确访问到相应的函数和数据。

hello重定位:有重定位PC相对引用和绝对引用,对于PC相对引用,将地址改为PC值-跳转目标位置地址。绝对引用则将地址改成该符号的第一个字节所在的地址。

5.6 hello的执行流程

使用gdb逐步调试如下

(图5.6)

地址

名称

0x401000

<_init>

0x401020

<.plt >

0x401090

<puts@plt>

0x4010a0

<printf@plt>

0x4010b0

<getchar@plt>

0x4010c0

<atoi@plt>

0x4010d0

<exit@plt>

0x4010e0

<sleep@plt>

0x4010f0

<_start>

0x401120

<_dl_relocate_static_pie>

0x401125

<main>

0x4011c0

<__libc_csu_init>

0x401230

<__libc_csu_fini>

0x401238

<_fini>

5.7 Hello的动态链接分析

   (以下格式自行编排,编辑时删除

分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变化。要截图标识说明。

从hello的elf文件可知,.got表的地址为0x0000000000403ff0

(图5.7.1)

分析动态链接前的状态

(图5.7.2)

调用_init后

(图5.7.3)

初始地址0x00600ff0全为0。程序调用共享库函数时,库可能加载到任意地址,编译时无法确定函数的最终位置,定义它的共享模块在运行时可以加载到任意位置。链接器采用延迟绑定的策略解决。仅在函数首次被调用时解析其地址,后续调用直接使用已解析的地址,减少启动开销。动态链接器使用过程链接表PLT + 全局变量偏移表GOT实现函数的动态链接,GOT中存放目标函数的地址,PLT使用该地址跳转到目标位置,其中GOT[1]指向重定位表,GOT[2]指向动态链接器ld-linux.so运行地址。

5.8 本章小结

本章聚焦程序链接技术,系统阐述其核心概念、执行流程及技术实现细节。首先明确链接的定义与核心作用,即通过链接器将多个目标文件、库文件整合为可执行文件或库文件,解决符号引用问题,构建完整程序运行体。

在程序链接过程解析中,详细说明链接器对目标文件各段(代码段、数据段等)的合并规则,以及符号解析、重定位等关键操作。通过分析 ELF(可执行与可链接格式)文件信息,直观展现链接生成可执行文件时,程序在内存布局、符号地址映射等方面发生的变化,深入理解链接对程序结构的重塑。


6hello进程管理

6.1 进程的概念与作用

概念

操作系统中正在运行的程序实例,是资源分配和调度的基本单位,包含代码、数据、堆栈及进程控制块等。

作用

系统中的每个程序都运行在某个进程的上下文中,进程提供一个假象,就好像我们的程序是系统中当前运行的唯一的程序的一样,我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接着一条地执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。

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

作用:能够调用各类程序,并向其传递数据或参数,同时获取程序执行后的处理结果。支持在多个程序间传递数据,实现将一个程序的输出作为另一个程序的输入,从而构建复杂的数据处理流程。Shell 本身也可作为被调用对象,供其他程序触发执行,满足多样化的自动化任务需求。

处理流程:

  1. 等待输入:Shell 首先在终端输出提示符(如$或#),进入待命状态,持续读取用户从终端输入的命令。
  2. 参数解析:对输入的命令行进行分析,拆解命令与参数,构造符合execve函数要求的argv参数向量。
  3. 内置命令判断:检查命令行的首个参数是否属于 Shell 内置命令(如cd、echo等),若是,则立即在当前 Shell 进程中执行。
  4. 创建子进程:若不是内置命令,Shell 通过fork系统调用创建一个子进程,将后续执行任务转移到该子进程。
  5. 程序执行:子进程再次执行步骤 2 解析参数,调用execve()函数加载并执行目标程序。
  6. 前台作业处理:若命令行末尾不存在&符号,表明该任务为前台作业,Shell 会调用waitpid函数阻塞自身,等待子进程作业执行完毕后才返回,继续等待下一条命令输入。
  7. 后台作业处理:若命令行末尾带有&符号,即后台作业,Shell 不会等待子进程执行结束,而是立即返回并显示提示符,继续接收新的用户命令 。

6.3 Hello的fork进程创建过程

输入命令执行hello后,父进程判断不是内部指令后,会通过fork创建子进程。子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟空间相同但独立的副本,包括代码和数据段、堆、共享库以及用户栈。子进程可以读写父进程中打开的任何文件,二者最大的区别在于它们有不同的PID。

子进程的 eax 寄存器(返回值)设为 0,父进程的 eax 设为子进程 PID。子进程从 fork() 返回处开始执行(与父进程相同的指令指针)。

6.4 Hello的execve过程

当调用 execve时,内核首先检查目标文件的权限和格式,随后清空当前进程的代码段、数据段和堆栈,并根据 ELF 文件头将 hello 的代码段(.text)和数据段(.data/.bss)映射到内存中。接着,内核初始化新的用户态堆栈,将命令行参数 argv 和环境变量 envp 压入栈空间,并重置寄存器状态。最终,原进程的上下文被完全替换为 hello 的程序内容,但进程 PID 保持不变,且默认保留已打开的文件描述符。若执行成功,原进程的代码逻辑彻底消失,转而从 hello 的 main 函数开始运行;若失败则返回 -1,并设置 errno 提示错误原因。整个过程通过写时复制和内存映射高效完成,实现了进程的程序替换。

6.5 Hello的进程执行

进程调度是操作系统通过 时间片轮转 和 优先级策略,在多个就绪进程间切换CPU控制权的过程,核心步骤包括:

时间片耗尽触发调度

每个进程分配一个时间片(如10ms),由时钟中断(timer interrupt)周期性检测。当进程时间片用完,CPU强制进入内核态,触发调度器(scheduler)。

保存当前进程上下文

用户态 → 内核态:通过系统调用或中断自动保存用户态寄存器(eax/rax、eip/rip等)到内核栈。保存进程控制块(PCB):将进程的运行时状态(寄存器值、内存映射、文件描述符等)存入 task_struct。

选择下一个进程

调度算法(如CFS、O(1))从就绪队列选取优先级最高的进程。检查目标进程的 mm_struct(内存上下文)和 thread_info(线程状态)。

恢复新进程上下文

加载新进程的PCB到CPU寄存器(包括页表基址 cr3)。内核态 → 用户态:通过 iret 指令恢复用户态寄存器,跳转到新进程的 eip/rip。

新进程运行

从上次中断点继续执行,直至时间片耗尽或主动让出CPU。

用户态与内核态转换

触发方式

主动:进程调用系统调用(如 fork()、execve()),通过 int 0x80 或 syscall 指令陷入内核。

被动:硬件中断(时钟中断、I/O中断)强制切换。

切换过程

CPU自动保存用户态 ss/esp、eflags、cs/eip 到内核栈。

切换到内核态栈(esp 指向内核栈顶)。

执行内核代码后,通过 iret 指令逆向恢复用户态上下文

6.6 hello的异常与信号处理

1.异常,如图所示,异常有以下四种,分别是中断、陷阱、故障、终止。

(图6.6.1)

(图6.6.2)

2.信号,信号有很多种,图中展示的是Linux上支持的30种不同类型的信号。

(图6.6.3)

3.hello正常运行状态

(图6.6.4)

4.Ctrl+Z

进程收到 SIGSTP 信号, hello 进程被挂起。ps查看它的进程PID, hello的PID是4991; jobs查看hello的后台 job号1,调用 fg 1调回前台

(图6.6.5)

  使用pstree观察进程层级关系

(图6.6.6)

kill 命令用于向进程发送信号(如终止、重启、暂停等)

(图6.6.7)

5. Ctrl+C:进程收到 SIGINT 信号,终止 hello。在ps中没有它的PID,在job中也没有,可以看出hello已经被永远地停止了。

(图6.6.8)

  1. 运行中乱按,会将屏幕的输入缓存到缓冲区,被认为是命令

6.7本章小结

本章聚焦 hello 进程的完整生命周期,详细阐述其从创建、加载到终止的全流程。用户通过键盘输入指令触发 hello 进程,系统先执行创建操作,父进程经 fork 系统调用生成子进程,并赋予其独立的用户级虚拟空间副本,包含代码段、数据段等关键部分。

随后进入加载阶段,子进程调用 execve 函数,完成可执行文件的解析、内存映射及初始化,将 hello 程序的指令与数据载入内存,做好运行准备。运行过程中,内核凭借调度算法,依据系统资源状况和进程优先级,灵活执行上下文切换,合理分配 CPU 时间片,确保 hello 进程与其他进程协同运行。

此外,hello 进程运行时会面临各类异常信号,如系统中断、程序错误等。一旦接收到异常信号,系统会立即激活相应的异常处理程序。不同异常信号对应独特的处理机制,有的会终止进程并生成核心转储文件,有的则暂停进程或忽略信号 ,最终使 hello 进程产生不同的处理结果,直至进程终止,完成整个生命周期。


7hello的存储管理

7.1 hello的存储器地址空间

地址类型

生成阶段

作用域

转换机制

逻辑地址

程序代码直接生成

分段单元输入

段基址 + 偏移量

线性地址

分段机制输出(未启用分页时)

分页单元输入

段基址固定为0时=逻辑地址

虚拟地址

分页机制输入(现代OS常用)

进程视角的连续空间

通过页表映射为物理地址

物理地址

分页机制输出

实际内存芯片地址

由MMU硬件转换

以 hello 的 printf 调用为例

逻辑地址:
call 0x400450 中的 0x400450 是逻辑地址(代码编译时确定)。

线性地址:
分段单元处理(实际未转换)→ 线性地址 0x400450。

虚拟地址:
线性地址作为分页机制输入,仍为 0x400450(用户视角)。

物理地址:
MMU通过页表将 0x400450 映射到物理地址 0x1234500,读取指令

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

Intel的段式管理将逻辑地址分为两部分:段基址和段内偏移地址。段基址是由操作系统分配的,用于定位内存中的段起始地址。段内偏移地址是指段内的偏移地址,用于定位段内的数据。在段式管理中,逻辑地址转换为线性地址的过程如下:

1.取出段基址,将段基址与段内偏移地址相加,得到段内的线性地址。

2.检查该线性地址是否超出当前段的长度。如果超出,则产生越界错误。

3.如果没有超出段的长度,则将段内的线性地址作为最终的线性地址。

在Intel的x86架构中,段基址和段内偏移地址都是16位的,最大可表示64KB的内存空间。因此,如果需要访问超过64KB的内存空间,则需要使用多个段,并通过分段机制将它们连接起来。

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

在 hello 程序的运行过程中,线性地址到物理地址的转换 通过 页式管理 机制实现,具体流程如下:

当 hello 访问一个线性地址(如 0x400500,对应代码段入口)时,CPU 的 内存管理单元(MMU) 会通过多级页表将其转换为物理地址:

页表查询:

x86-64 使用 4级页表(PGD→PUD→PMD→PTE),cr3 寄存器保存当前进程的页表基址。

将线性地址 0x400500 拆分为页目录索引(9位)、页表索引(9位)、页内偏移(12位),逐级查询页表条目(PTE)。

物理地址生成:

最终 PTE 给出物理页框号(如 0x1234),与页内偏移(0x500)组合成物理地址 0x1234500。

TLB 加速:

频繁访问的地址映射会被缓存到 TLB(快表),避免重复查表。

若页表条目标记为无效(如未分配或已换出),则触发 缺页异常(Page Fault),由内核加载数据到内存后重新执行指令。

总结:hello 的线性地址通过页表的层级映射转为物理地址,此过程由硬件(MMU)和操作系统(维护页表)协同完成,对程序完全透明。

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

TLB和四级页表是Intel处理器中用于实现虚拟地址(VA)到物理地址(PA)变换的两个重要机制。当CPU需要访问内存时,它会先查看TLB中是否存在对应的VA的映射关系。如果存在,TLB中的映射关系会被直接使用,从而加快了内存访问速度。如果不存在,CPU会进入页表查找VA对应的PA。在四级页表中,VA被分成了四个部分,通过索引页目录项和页表项获取页的物理页框号,再加上页内偏移得到PA。如果TLB中没有对应的VA的映射关系,CPU会将其加入TLB中,以便下次使用。这两个机制共同工作,使得Intel处理器可以加速VA到PA的变换,提高内存访问速度。

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

CPU 发起访问请求:当 CPU 需要读取或写入数据时,它首先会检查 L1 Cache。如果数据在 L1 Cache 中命中,CPU 可以直接从 L1 Cache 中读取或写入数据,这是最快的情况。

L1 Cache 未命中:若数据不在 L1 Cache 中,CPU 会接着检查 L2 Cache。如果 L2 Cache 命中,数据会被加载到 L1 Cache 中,然后再由 CPU 进行访问。

L2 Cache 未命中:若 L2 Cache 也未命中,CPU 会继续检查 L3 Cache。若 L3 Cache 命中,数据会依次加载到 L2 Cache 和 L1 Cache,最后供 CPU 访问。

L3 Cache 未命中:如果数据在三级 Cache 中都未命中,CPU 才会访问物理内存。此时,数据会从物理内存中读取,并依次加载到 L3 Cache、L2 Cache 和 L1 Cache 中,以便后续访问。在写入数据时,如果采用写回策略,只有当 Cache 中的数据被替换或系统需要同步数据时,才会将数据写回到物理内存中。

7.6 hello进程fork时的内存映射

当 hello 进程调用 fork() 时,Linux 内核采用 写时复制(COW) 机制高效管理内存:

创建新进程的数据结构:当hello进程调用fork时,内核会为新创建的子进程创建各种数据结构,包括进程控制块(PCB)、进程描述符等,并为子进程分配一个唯一的进程标识符(PID)。

复制内存管理结构:内核会为子进程创建一个与父进程相同的内存描述符、区域结构和页表。

子进程共享父进程的所有物理内存页,但内核将这些页标记为 只读;当父进程或子进程尝试修改共享内存时,触发缺页异常,内核为修改方分配新的物理页并复制数据,更新页表后恢复写入权限。

这种机制使得 fork() 仅需复制页表而无需立即复制物理内存,极大提升了创建速度,同时保证进程隔离性。代码段始终共享物理页,而数据段、堆栈等可写区域则在首次修改时按需复制,最终父子进程的内存完全独立。整个过程由内核透明处理,对 hello 程序无感知

7.7 hello进程execve时的内存映射

当 hello 进程执行 execve() 时,内核会彻底重构其内存空间:首先销毁原有代码、数据和堆栈等所有用户态内存映射,然后根据目标程序的 ELF 头部信息重新建立内存布局——将代码段(.text)映射为只读可执行,数据段(.data/.bss)映射为可读写,并初始化新的堆栈空间;动态链接器会加载所有依赖的共享库(如 libc.so),并在栈顶压入命令行参数和环境变量;最后将指令指针指向新程序的入口地址 _start,完成进程内容的完全替换。整个过程保持原进程 PID 不变,但原有程序代码和数据被彻底覆盖,仅保留打开的文件描述符等部分属性,若执行失败则返回错误而不影响原进程状态。

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

缺页故障是操作系统中的一种异常,发生在程序访问的内存页不在物理内存中时。当发生缺页故障时,操作系统会通过缺页中断来处理这种情况。缺页故障与缺页中断处理的介绍如下:

缺页故障

1.触发条件:当程序尝试访问一个内存地址,而这个地址对应的页不在物理内存中(即不在RAM中),而是在磁盘上的交换区或文件系统中时,就会触发缺页故障。

2.异常处理:缺页故障被视为一种异常,需要操作系统的异常处理机制来处理。

缺页中断处理的几种方式如下:

1.中断响应:当CPU检测到缺页故障时,会立即中断当前的执行流程,跳转到预设的缺页中断处理程序。  

2.查找页表:中断处理程序首先检查页表,确认发生故障的页是否真的不在物理内存中。如果页表项标记为无效,那么确实是缺页故障。

3.加载页面:如果确认是缺页故障,操作系统会从磁盘上的交换区或文件系统中读取相应的页面到物理内存中。这通常涉及到磁盘I/O操作,是处理过程中最耗时的部分。

4.更新页表:页面加载到物理内存后,操作系统会更新页表,将该页的状态标记为有效,并记录其对应的物理内存地址。

5.重新执行指令:页面加载并更新页表后,操作系统会重新执行导致缺页故障的那条指令,因为此时所需的页面已经在物理内存中了。

6.内存管理:在加载新页面到物理内存时,如果物理内存已满,操作系统可能需要执行页面置换算法(如LRU、FIFO等),选择一个或多个页面从物理内存中移除,以便为新页面腾出空间。

7.9动态存储分配管理

Printf会调用malloc,请简述动态内存管理的基本方法与策略。(此节课堂没有讲授,选做,不算分)

7.10本章小结

本章围绕hello程序展开,系统探讨其在计算机系统中的存储与运行机制,涵盖内存地址变换、数据访问及内存管理等核心环节。

剖析hello程序的存储器地址空间布局,阐述 Intel 架构下从逻辑地址经段式管理转换为线性地址,再通过页式管理将线性地址映射至物理地址的完整流程;深入解析基于 LB 与四级页表实现虚拟地址(VA)到物理地址(PA)的高效转换机制。

详细说明在三级 Cache 支持下,hello程序访问物理内存的流程,介绍 Cache 未命中、命中时的数据读取与写入策略,以及预取、一致性协议等优化技术。

进程运行与内存映射:结合hello进程,分析fork时通过写时复制(Copy - on - Write)技术实现父子进程内存共享与分离的原理;阐述execve操作时,新程序如何重新映射内存,加载代码、数据等内容。

探讨动态存储分配管理策略,以及hello程序运行过程中遇到缺页故障时,系统触发缺页中断处理、加载缺失页面的具体过程 。


8hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件是指所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。

设备管理:unix io接口是指这种将设备映射为文件的方式允许Linux内核引出一个简单、低级的应用接口,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

在write函数执行时,它会将参数加载到寄存器中,然后通过int 21h指令触发系统调用。系统调用负责将字符串中的字节从寄存器经由总线传输到显卡的显存中,显存内存储着字符的ASCII码。字符显示驱动程序根据这些ASCII码在字模库中查找对应的点阵信息,并将这些点阵信息写入到vram中。显示芯片随后以设定的刷新频率逐行扫描vram,通过信号线将每个像素的RGB颜色信息传输给液晶显示器。最终,hello程序的输出内容便在屏幕上显示出来。

8.3 printf的实现分析

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章介绍了Linux的IO设备管理方法,简述了简述Unix IO接口及其函数,分析了printf和getchar的实现。

结论

"Hello" 程序的系统级生命周期

  1. 源码生成:人类可读的高级语言文本(hello.c)通过编辑器创建,定义程序逻辑与接口。
  2. 预处理展开:预处理器(cpp)解析宏定义、头文件包含(#include),生成扩展后的纯文本文件(hello.i),消除编译期依赖。
  3. 编译翻译:编译器(gcc)将高级语言转换为汇编语言(hello.s),执行词法分析、语法分析、语义检查与代码优化。
  4. 汇编编码:汇编器(as)将汇编指令转换为机器码(hello.o),生成包含二进制指令与符号表的目标文件。
  5. 链接整合:链接器(ld)解析符号引用,合并目标文件与库文件,生成可执行文件(hello),完成虚拟地址空间映射。
  6. 进程创建:Shell 通过fork()创建子进程,复制进程上下文(PCB、内存页表、文件描述符)。
  7. 程序加载:通过execve()触发加载器,解析 ELF 格式,建立虚拟内存区域(代码段、数据段、堆、栈),映射共享库。
  8. CPU 调度:内核调度器分配时间片,CPU 通过三级 Cache 访问指令与数据,执行指令流(取指→译码→执行→写回)。
  9. 异常处理:硬件中断(如键盘输入)或软件异常(如除零错误)触发中断向量表,执行对应 ISR(中断服务程序)。
  10. 资源回收:进程通过exit()终止,父进程通过wait()回收僵尸进程,内核释放虚拟内存、关闭文件描述符。

通过本次对 hello 程序从编写到执行的全流程剖析,我深刻理解了程序生命周期的复杂性及其背后精妙的设计思想。

通过 gcc -no-pie 生成可执行文件,观察到重定位条目如何解决外部符号引用(如 atoi 的 R_X86_64_PLT32 类型),理解动态链接中 PLT/GOT 的延迟绑定机制。

进程管理fork() 的写时复制(COW)使子进程高效共享父进程内存空间execve() 彻底重构地址空间时,保留文件描述符的细节设计,通过 /proc/[pid]/maps 验证内存区域权限(如 .text 段为 r-xp)地址转换
 分析线性地址到物理地址的页式转换,结合 cr3 寄存器和多级页表(PGD→PUD→PMD→PTE),理解 TLB 加速与缺页异常的处理流程。

熟练使用 objdump -dr 分析重定位、strace 追踪系统调用、gdb 观察寄存器状态,掌握了进程调试的核心方法论。

这个看似简单的 hello 程序,实则凝聚了编译优化、内存管理、进程调度等多领域智慧。动态链接器通过 GOT[1] 定位重定位表、GOT[2] 绑定 ld-linux.so 的设计,展现了软件工程中解耦与扩展性的经典实践。这些认知将深刻影响我的系统级编程思维。


  1. 附件

名称

作用

hello.c

hello程序c语言源文件

hello.i

hello.c预处理生成的文本文件

hello.s

由hello.i编译得到的汇编文件

hello.o

hello.s汇编得到的可重定位目标文件

elf.txt

readelf生成的hello.o的elf文件

hello

hello.o和其他文件链接得到的可执行目标文件

hello1.elf

readelf生成的hello的elf文件


参考文献

[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] https://blog.csdn.net/qq_21334991/article/details/77587596

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值