关注

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

摘要
本论文围绕“程序人生 - Hello’s P2P”主题,以hello.c程序为研究对象,系统剖析其从源代码(Program)到进程(Process)的完整生命周期,诠释 “From Program to Process” 的P2P核心逻辑与“From Zero-0 to Zero-0”的020闭环流程。通过在Ubuntu环境下开展预处理、编译、汇编、链接等操作,结合GDB、readelf、objdump等工具,深入分析程序在编译链接、进程管理、存储管理及IO交互等环节的底层实现机制。研究过程验证了计算机系统软硬件协同工作的核心原理,揭示了程序从文本形式到执行实体的转化本质,为理解计算机系统的设计逻辑与运行机制提供了实践支撑。
关键词:hello程序;编译链接;进程管理;存储管理;IO 交互

第1章 概述

1.1Hello简介

        Hello程序的“P2P”是“From Program to Process”的缩写,描述了程序从源代码文本到操作系统可调度进程的完整转化过程:开发者编写的hello.c源代码(Program),经预处理、编译、汇编、链接四个阶段生成可执行文件,再由Shell触发操作系统通过fork创建进程、execve加载程序,最终在CPU、内存等硬件资源支持下执行;“020”则指“From Zero-0 to Zero-0”,即程序从无到有(源代码编写与编译生成可执行文件),再到执行完毕后资源释放、进程终止的闭环生命周期,全程依赖计算机系统的编译器、操作系统、硬件设备等协同完成。

1.2环境与工具

(1).硬件工具:

•处理器:Intel(R) Core(TM) i9-14900HX,2.20 GHz(24核32线程)

•16.0 GB 5600 MT/s

•954 GB

•Ubuntu 16.04 LTS 64位

(2).软件与工具:

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

•调试工具:GDB 12.1(GNU Debugger)、edb-debugger 1.3.0

•分析工具:readelf 2.38(ELF 文件分析)、objdump 2.38(反汇编工具)

•文本编辑:Visual Studio Code 1.85.1(含C/C++插件)

•辅助工具:Terminal(终端)、截图工具Flameshot

1.3中间结果

文件名

生成阶段

作用

hello.c

源代码阶段

程序原始文本文件,包含核心逻辑代码

hello.i

预处理阶段

预处理后的C语言文件,展开头文件、替换宏定义

hello.s

编译阶段

汇编语言文件,将预处理后的代码转化为汇编指令

hello.o

汇编阶段

可重定位目标文件(ELF格式),包含机器语言指令但未完成链接

hello

链接阶段

可执行目标文件(ELF格式),完成重定位与库链接,可被操作系统加载执行

hello_gdb.log

调试阶段

GDB调试日志,记录程序执行流程、断点信息及内存地址变化

readelf_hello_o.txt

分析阶段

hello.o的ELF格式分析结果,包含节信息、重定位表等

readelf_hello.txt

分析阶段

hello可执行文件的ELF格式分析结果,包含段信息、虚拟地址分布等

1.4本章小结

        本章明确了大作业的核心主题与研究对象,梳理了hello程序“P2P”与“020”的核心逻辑,列出了完成本次研究所需的软硬件环境、工具及各阶段生成的中间结果。通过搭建完整的开发与分析环境,为后续深入剖析预处理、编译、汇编、链接、进程管理等关键环节奠定了基础,明确了各阶段的研究重点与技术路线。

第2章 预处理

2.1预处理的概念与作用

        预处理是C语言编译流程的第一步,由预处理器(cpp)完成,其核心作用是对源代码进行文本级别的处理,为后续编译阶段做准备。预处理不涉及语法分析,仅执行简单的文本替换与指令解析,主要功能包括:展开#include头文件(将头文件内容嵌入源代码)、替换#define宏定义、处理条件编译指令(如#if、#ifdef)、删除注释、添加行号与文件名标识(便于编译错误定位)。预处理后的文件仍为C语言文本格式(.i 后缀),保留了原始代码的逻辑结构,但已消除了所有预处理指令。

2.2在Ubuntu下预处理的命令

        在Ubuntu终端中,使用GCC编译器的-E选项可仅执行预处理操作,生成预处理文件hello.i,命令:

gcc -E -m64 -Og hello.c -o hello.i

2.3Hello的预处理结果解析

        原始hello.c文件包含#include <stdio.h>、#include <unistd.h>、#include <stdlib.h>三个头文件,预处理后生成的hello.i文件体积显著增大,核心变化如下:

1.头文件展开:预处理将stdio.h、unistd.h、stdlib.h的全部内容嵌入hello.i中,包含printf、sleep、atoi、exit等函数的声明,以及FILE、NULL等宏定义;

2.注释删除:原始代码中的注释(如// 大作业的 hello.c 程序)被全部删除,仅保留有效代码;

3.行号与文件名标识:文件中插入了# 1 "hello.c"、# 1 "/usr/include/stdio.h"等标识,记录代码来源于原始文件或头文件,便于编译阶段定位错误。

2.4本章小结

        本章阐述了预处理的概念与核心作用,通过Ubuntu终端执行预处理命令生成了hello.i文件,并解析了预处理后的文件变化。预处理的核心价值在于统一代码文本格式、展开依赖头文件、消除注释与宏替换等。

第3章 编译

3.1编译的概念与作用

        编译是编译流程的核心阶段,由编译器(gcc的cc1组件)完成,其作用是将预处理后的.i文件(C语言文本)转化为汇编语言文件(.s后缀)。编译过程包含词法分析、语法分析、语义分析、中间代码生成、代码优化等多个子阶段:

•词法分析:将源代码分解为关键字、标识符、常量、运算符等词法单元;

•语法分析:根据C语言语法规则,将词法单元组合为语法树(如表达式、语句、函数等);

•语义分析:检查语法树的语义合法性(如类型匹配、变量未定义等);

•中间代码生成:将语法树转化为与机器无关的中间代码(如三地址码);

•代码优化:对中间代码进行优化(如常量折叠、循环优化),提升执行效率;

•目标代码生成:将优化后的中间代码转化为特定架构(x86-64)的汇编语言指令。

3.2在Ubuntu下编译的命令

        使用GCC编译器的-S选项可仅执行编译操作,将hello.i文件转化为hello.s 汇编文件,命令如下:

gcc -S hello.i -o hello.s

3.3Hello的编辑结果解析

        hello.s 文件包含x86-64架构的汇编指令,以下结合原始C语言代码的核心数据类型与操作,对编译结果进行分点解析:

3.3.1数据:

1.字符串:程序中有两个字符串,这两个字符串都在只读数据段中,如图所示:

2.局部变量i:main函数声明了一个局部变量i,编译器进行编译的时候将局部变量i会放在堆栈中。如图所示,局部变量i放在栈上-20(%rbp)的位置。

3.main函数:参数argc作为用户传给main的参数。也是被放到了堆栈中。

4.立即数:立即数直接体现在汇编代码中。

5.数组:char *argv[]:hello.c中唯一的数组是作为main函数的第二个参数,数组的每个元素都是一个指向字符类型的指针。数组的起始地址存放在栈中-32(%rbp)的位置,被两次调用找参数传给printf。

3.3.2全局函数:

        由hello.c可知,hello.c声明了一个全局函数int main(int argc,char *argv[]),经过编译之后,main函数中使用的字符串常量也被存放在数据区。

        这段汇编代码说明main函数是全局函数。

3.3.3赋值操作:

        程序中的赋值操作主要有:i=0这条赋值操作在汇编代码主要使用mov指令来实现,而根据数据的类型又有好几种不一样的后缀:

•movb:一个字节;

•movw:两个字节;

•movl:四个字节;

•movq:八个字节。

3.3.4算数操作:

        hello.c中的算数操作有:i++,由于是i是int类型的,因此汇编代码只用addl就能实现其他的操作有:

指令

效果

Leaq S,D

D=&S

INC D

D+=1

DEC D

D-=1

NEG D

D=-D

ADD S,D

D=D+S

SUB S,D

D=D-S

3.3.5关系操作:

1.argc!=5:是在一条件语句中的条件判断:argc!=5,进行编译时,这条指令被编译为:cmpl $5,-36(%rbp),同时这条cmpl的指令还有设置条件码的作用,当根据条件码来判断是否需要跳转到分支中。

2.i<10:在hello.c作为判断循环条件,在汇编代码被编译为:cmpl $9,-20(%rbp),计算 i-9然后设置 条件码,为下一步 jle 利用条件码进行跳转做准备。

3.3.6控制转移指令:

        汇编语言中首先设置条件码,然后根据条件码来进行控制转移,在hello.c中,有以下控制转移指令:

1.判断i是否为5,如果i等于5,则不执行if语句,否则执行if语句,对应的汇编代码为:

2. for(i=0;i<10;i++),通过每次判断i是否满足小于10来判断是否需要跳转至循环语句中,对应的汇编代码为:

3.3.7函数操作:

        调用函数时有以下操作:(假设函数P调用函数Q)

1.传递控制:进行过程Q的时候,程序计数器必须设置为Q的代码的起始 地址,然后在返回时,要把程序计数器设置为P中调用Q后面那条指令的地址;

2.传递数据:P必须能够向Q提供一个或多个参数,Q必须能够向P中返回一个值;

3.分配和释放内存:在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。

hello.c涉及的函数操作有:

main函数,printf,exit,sleep ,getchar函数;

main函数的参数是argc和argv;两次printf函数的参数恰好是那两个字符串;

exit参数是1,sleep函数参数是atoi(argv[4]);

函数的返回值存储在%eax寄存器中。

3.3.8类型转换:

        hello.c中涉及的类型转换是:atoi(argv[4]),将字符串类型转换为整数类型其他的类型转换还有int、float、double、short、char之间的转换。

3.4本章小结

        本章主要讲述了编译阶段中编译器如何处理各种数据和操作,以及c语言中各种类型和操作所对应的的汇编代码。通过理解了这些编译器编译的机制,我们可以很容易的将汇编语言翻译成c语言。

第4章 汇编

4.1汇编的概念与作用

        汇编是编译流程的第三阶段,由汇编器(as)完成,其核心作用是将编译生成的汇编语言文件(.s 后缀)转化为可重定位目标文件(.o 后缀,ELF格式)。汇编过程本质是“指令翻译”:将人类可读的汇编指令(如movl $0, -4(%rbp))转化为计算机可执行的机器语言指令(二进制编码),同时生成 ELF文件结构,包含代码段(.text)、数据段(.data)、重定位表(.rel.text、.rel.data)等关键节,记录指令、数据的存储位置及未解析的符号引用(如printf、sleep等库函数)。

4.2在Ubuntu下汇编的指令

        使用GCC编译器的-c选项可仅执行预处理、编译、汇编阶段,生成可重定位目标文件hello.o,命令如下:

gcc hello.s -c -o hello.o

4.3可重定位目标ELF格式

1.ELF Header:用命令:readelf -h hello.o

以16B的序列Magic开始,Magic描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解##标题释目标文件的信息,其中包括ELF 头的大小、目标文件的类型、机器类型、 字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。

           

2.Section Headers:命令:readelf -S hello.o

        Section Headers节头部表,包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。由于是可重定位目标文件,所以每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,同时可以观察到,代码是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。      

3.查看符号表.symtab:命令readelf -s hello.o

        .symtab存放程序中定义和引用的函数和全局变量的信息。name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。

4.重定位节:.rela.text

         重定位节:一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。

4.4Hello.o的结果解析

        使用objdump -d -r hello.o命令对hello.o进行反汇编,对比第3章的hello.s汇编文件,分析机器语言与汇编语言的映射关系及差异:

        通过反汇编的代码和hello.s进行比较,发现汇编语言的指令并没有什么不同的地方,只是反汇编代码所显示的不仅仅是汇编代码,还有机器代码,机器语言程序的是二进制机器指令的集合,是纯粹的二进制数据表示的语言,是电脑可以真正识别的语言。机器指令由操作码和操作数构成,汇编语言是人们比较熟悉的词句直接表述CPU动作形成的语言,是最接近CPU运行原理的语言。每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言(操作码和操作数)和二进制机器语言建立一一映射的关系,因此可以将汇编语言转化为机器语言,通过对机器代码的分析可以看出以下不同的地方:

(1)分支转移:反汇编的跳转指令用的不是段名称比如.L4,而是用的确定的地址,因为,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址;

(2)函数调用:在.s 文件中,函数调用之后直接跟着函数名称,而在反汇编程 序中,call的目标地址是当前下一条指令。这是因为 hello.c 中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执 行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text 节中为其添加重定。

4.5本章小结

        本章对hello.s进行了汇编,生成了hello.o可重定位目标文件,并且分析了可重定位文件的ELF头、节头部表、符号表和可重定位节,比较了hello.s和hello.o反汇编代码的不同之处,分析了从汇编语言到机器语言的一一映射关系。

第5章 链接

5.1链接的概念与作用

        链接是编译流程的最后阶段,由链接器(ld)完成,其核心作用是将多个可重定位目标文件(.o)与系统库文件(如libc.so)合并,生成可执行目标文件(.out或无后缀)。链接过程的核心操作包括符号解析(将未定义符号与库文件中的定义匹配)、重定位(修正代码段与数据段中的地址引用),最终形成一个地址连续、可被操作系统加载执行的文件。

作用:模块化编程支持:允许将程序拆分为多个源代码文件,分别编译后链接,提升开发效率;代码复用:通过链接系统库或自定义库,避免重复编写通用功能(如 printf、sleep)地址标准化:为程序分配统一的虚拟地址空间,确保指令与数据的地址引用合法。

5.2在Ubuntu下链接的命令

        使用 ld 链接器直接链接 hello.o 文件,需指定系统库路径与依赖库(如 libc.so、ld-linux.so),命令如下:

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 -h -S hello命令分析 hello 可执行文件的 ELF 格式,核心信息如下:

1.ELF Header:hello的文件头和hello.o文件头的不同之处如下图标记所示,Type类型为EXEC表明hello是一个可执行目标文件,有27个节;

2.节头部表Section Headers:Section Headers 对 hello中所有的节信息进行了声明,其 中包括大小 Size 以及在程序中的偏移量Offset,因此根据Section Headers 中的信息我们就可以用HexEdit定位各个节所占的区间(起始位置,大小)。其中Address是程序被载入到虚拟地址的起始地址。

3.重定位节:rela.text:

4.符号表symtab:

5.4Hello的虚拟地址空间

        使用gdb/edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

•代码段(.text)位于0x4010d0-0x401245;

•只读数据段(.rodata)位于0x402000-0x402049;

•动态链接相关节(.dynamic、.got、.plt)位于0x403e50之后。

        这验证了可执行文件的虚拟地址空间是由链接器在链接阶段分配的,操作系统加载时直接映射该虚拟地址。

5.5链接的重定位过程分析

1 .hello.o中的未解析符号修正:

        使用objdump -d -r hello对比 hello 与 hello.o 的反汇编结果,分析重定位过程:hello.o 中 printf 的调用指令为e8 00 00 00 00(临时占位地址),重定位后hello 中的对应指令变为:e8 51 ff ff ff。

        机器语言e8 51 ff ff ff对应的目标地址为 0x401090(__printf_chk@plt),即过程链接表(PLT)中printf 的入口地址;

        链接器通过符号解析找到libc.so中printf的定义,并重定位修正了callq指令的目标地址,解决了hello.o中未解析的符号引用。

2 .重定位类型分析

        hello.o中printf的重定位类型为R_X86_64_PLT32,链接时的地址计算方式为:修正后的指令偏移 = 符号的PLT入口地址 - 当前指令的地址 -  4

•符号的PLT入口地址(__printf_chk@plt):0x401090;

•当前指令地址:0x40113a;

•计算结果:0x401090 - 0x40113a - 4 = 0xFFFFFF51(补码表示为0x51 ff ff ff),对应机器语言中的51 ff ff ff,与反汇编结果一致。

5.6Hello的执行流程

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

程序名称(符号)

程序地址

核心说明(针对你的 hello 程序)

_start

0x4010d0

hello程序的真正入口(ELF 头Entry point),由C 运行时库crt1.o提供,负责启动整个程序

__libc_start_main

0x7ffff7de2fc0

libc库核心函数,由_start调用,负责初始化 argc/argv、堆/栈,最终调用main函数

main

0x401105

hello程序核心逻辑函数,接收4个命令行参数(学号/姓名/手机号/秒数),执行循环输出+等待输入

__printf_chk

0x401090

printf 的安全版本(PLT入口地址),对应R_X86_64_PLT32重定位的核心符号

sleep

0x7ffff7ea1df0

hello程序中调用的休眠函数(libc 库),实现每次循环的指定秒数暂停(如2秒)

getchar

0x7ffff7e4a590

hello程序循环结束后调用的函数,等待键盘输入,触发read系统调用读取键盘缓冲区

exit

0x7ffff7e05a70

程序终止函数,main返回0后由__libc_start_main 调用,释放进程资源并正常退出

__libc_csu_init

0x4010c0

C运行时库初始化函数,负责动态链接库的加载、全局构造函数执行,是main执行前的前置初始化

_dl_start

0x7ffff7de1630

动态链接器(ld-linux.so)的启动函数,负责解析hello的动态依赖(如 libc.so)并完成重定位

5.7Hello的动态链接分析

        动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。hello 程序采用动态链接方式(依赖 libc.so),使用ldd hello命令可查看动态依赖:

        验证 Linux 程序在运行时如何通过 PLT(过程链接表) 和 GOT(全局偏移表) 实现动态链接,理解延迟绑定(Lazy Binding)的工作原理:

1.查看PLT表:PLT 是动态链接的入口,每个外部函数有一个 PLT 项。

•PLT入口地址:0x401090;

•跳转目标:0x404020;

        输出中可以看到callq 401090 <__printf_chk@plt>:这是hello程序中调用printf时的汇编指令,说明程序会通过 PLT 入口触发动态链接。

2.查看GOT表:

        GOT 表保存函数的实际地址,由动态链接器在运行时填充。

        0x404020 是GOT表中对应 __printf_chk 的项,程序运行前,该地址内容是 PLT 入口的下一条指令地址。

3.用GDB观察动态链接过程:

        启动GDB,设置断点,运行程序,查看调用前的GOT表:

        此时GOT表项还没有实际函数地址。

        继续执行到PLT断点,再次查看GOT表:

        此时GOT表中已存储__printf_chk的实际地址(动态链接完成)。

4.验证延迟绑定:

        延迟绑定(Lazy Binding)是动态链接的优化机制,第一次调用时才解析地址。

        继续运行程序,会再次调用 printf,但这次不会触发动态链接器,而是直接跳转到 GOT 表中的地址,通过backtrace命令可以看到程序直接进入了 __printf_chk,而不是 PLT 解析流程。

5.查看共享库加载情况:

5.8本章小结

        本章阐述了链接的概念与核心作用,通过ld链接器生成了hello可执行文件,分析了ELF可执行文件的结构与虚拟地址空间分布,深入剖析了重定位过程与动态链接机制。链接的核心价值在于解决符号解析与地址修正问题,将可重定位文件与系统库合并为统一的可执行文件,为操作系统加载执行提供基础。动态链接的采用使得程序无需包含库函数的完整实现,减小了文件体积,同时支持库文件的共享与升级,体现了计算机系统 “模块化” 与 “复用性” 的设计思想。

第6章 Hello进程管理

6.1进程的概念与作用

1 进程的概念与作用

        进程是操作系统资源分配与调度的基本单位,是程序的执行实例。从本质上看,进程是“正在执行的程序”,包含程序代码、数据、进程控制块(PCB)、寄存器状态、栈空间等核心要素。进程的核心作用包括:隔离资源:每个进程拥有独立的虚拟地址空间,避免不同程序之间的资源冲突;并发执行:操作系统通过进程调度,使多个进程在CPU上交替执行,提升系统资源利用率;实现多任务:进程将程序的静态代码转化为动态执行的实体,支持用户同时运行多个程序(如浏览器、终端、编辑器)。

        对于hello程序而言,其对应的进程是操作系统为执行hello可执行文件而创建的实体,包含hello的代码、数据、运行时状态等,是hello从 “静态文件” 到“动态执行”的关键载体。

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

        Shell(命令行解释器)是用户与操作系统交互的桥梁,bash 是Linux系统默认的Shell。其核心作用是:解析用户输入的命令,创建子进程执行命令,等待命令执行完毕后回收子进程,并将执行结果反馈给用户。bash处理程序的核心流程如下:

(1)终端进程读取用户由键盘输入的命令行;

(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量;

(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令;

(4)如果不是内部命令,调用fork( )创建新进程/子进程;

(5)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序;

(6)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait…等待作业终止后返回;

(7)如果用户要求后台运行(如果命令末尾有&号),则shell返回。

6.3Hello的fork进程创建过程

        终端程序通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的 逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。

        以我们的hello为例,当我们输入./hello 2024112277 张越 18249727079 2的时候,首先shell对我们输入的命令进行解析,由于我们输入的命令不是一个内置的shell命令,因此shell会调用fork()创建一个子进程。

6.4Hello的execve过程

        当创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,加载并运行需要以下几个步骤:

(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构

(2)映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零

(3)映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域

(4)设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面

        除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据 复制。直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用 它的页面调度机制自动将页面从磁盘传送到内存。

6.5Hello的进程执行

        在进程调用execve函数之后,由上面分析可知,进程已经为hello程序分配了新的虚拟的地址空间,并且已经将hello的.txt和.data节分配虚拟地址空间的代码区和数据区。最初hello运行在用户模式下,输出hello 2024112277 张越 18249727079,然后hello调用sleep函数之后进程陷入内核模式,内核不会选择什么都不做等待sleep函数调用结束,而是处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。

        当hello调用getchar的时候,实际落脚到执行输入流是stdin的系统调用read,hello之前运行在用户模式,在进行read调用之后陷入内核,内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。此时进入内核模式,内核执行上下文切换,切换到其他进程。当完成键盘缓冲区到内存的数据传输时,引发一个中断信号,此时内核从其他进程进行上下文切换回hello进程。

6.6Hello的异常处理与信号

        hello进程执行过程中可能遇到的异常类型、信号及处理方式如下:

异常场景

触发方式

产生信号

信号含义

程序正常终止

main返回0

SIGCHLD

子进程终止,通知父进程

键盘中断

按下Ctrl+C

SIGINT

中断进程

暂停进程

按下Ctrl+Z

SIGTSTP

暂停进程(停止态)

非法指令

程序代码错误

SIGILL

非法指令

段错误

访问无效内存

SIGSEGV

段错误(内存访问违规)

        信号处理实验与结果:

        在 hello 进程执行过程中(循环输出信息),执行以下操作并观察结果:

1.按下Ctrl-C

        终端输入./hello 2024112277 张越 18249727079 4启动程序。按下Ctrl-C,进程立即终止,终端返回命令提示符。原因:SIGINT信号的默认处理方式是终止进程,内核向hello进程发送SIGINT后,进程执行终止操作并释放资源。

2.按下Ctrl-Z

        程序执行时按下Ctrl-Z,进程暂停,终端显示:[1]+已停止 ./hello 2024112277 张越 18249727079 4;执行jobs命令,显示暂停的进程:[1]+已停止 ./hello 2024112277 张越 18249727079 4;执行ps命令,查看进程状态;执行fg 1命令,进程恢复运行(继续循环输出);执行kill -9 2801命令(2801为PID),进程强制终止。

3.乱按键盘(包括回车)

        程序执行到 getchar () 时暂停,等待键盘输入,乱按键盘(如abc123)后按下回车,getchar () 读取输入缓冲区的字符,进程继续执行并终止。原因:键盘输入触发键盘中断,内核将扫描码转换为ASCII 码存入键盘缓冲区,getchar () 通过 read 系统调用读取缓冲区数据,直到检测到回车键返回。

6.7本章小结

        本章系统阐述了进程的概念与作用,分析了bash的工作流程、hello进程的创建(fork)、程序加载(execve)、执行调度及异常信号处理机制。hello进程的生命周期完整体现了操作系统进程管理的核心逻辑:从父进程fork创建新进程,到execve替换进程映像加载程序,再到调度器分配CPU资源执行指令,最后通过信号处理或正常终止回收资源。进程的用户态与核心态转换、写时复制、信号机制等设计,确保了程序执行的安全性、高效性与灵活性,是计算机系统多任务并发的基础

第7章 Hello的存储管理

7.1Hello的存储器地址空间

        逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。

        线性地址:也叫虚拟地址,和逻辑地址类似,也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件也是内存的转换前地址。

        虚拟地址:也就是线性地址。

        物理地址:用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应。可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上的抽像。

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

        段式管理是 Intel x86 架构的传统地址转换机制,核心作用是将逻辑地址(段选择符 + 段内偏移)转换为线性地址。x86-64 架构为兼容 32 位程序保留了段式管理,但进行了大幅简化:

1.段式管理核心结构:

•段选择符:16位寄存器(如 CS、DS),包含段描述符表索引、特权级等信息;

•段描述符表(GDT/LDT):存储段的基址、限长、权限等信息,x86-64 下 GDT(全局描述符表)是主要使用的段描述符表;

•段内偏移:逻辑地址中相对于段基址的偏移量(x86-64 下最大偏移量为 264-1)。

2.地址转换过程:

x86-64下,用户态进程的代码段(CS)、数据段(DS)等段描述符的基址均设置为0,限长设置为2^64-1,因此逻辑地址到线性地址的转换公式简化为:

线性地址 = 段基址(0)+ 段内偏移(逻辑地址)

        即逻辑地址与线性地址完全一致。对于hello程序,其代码中的地址(如main 函数地址0x401105)作为逻辑地址,经段式转换后直接成为线性地址 0x401105,段式管理在此过程中仅起到权限检查的作用(如代码段不可写、数据段不可执行)

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

        线性地址(也就是虚拟地址VA)到物理地址(PA)之间的转换通过分页机制完成。而分页机制是对虚拟地址内存空间进行分页。

        使用虚拟寻址,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址被送到内存之前首先转换为适当的物理地址。将一个虚拟地址转换为物理地址叫做地址翻译,需要CPU硬件和操作系统之间的紧密合作。CPU芯片上叫做内存管理单元(MMU)的住哪用硬件,利用主存中的查询表来动态翻译虚拟地址。

        虚拟地址作为到磁盘上存放字节的数组的索引,磁盘上的数组内容被缓存在主存中。同时,磁盘上的数据被分割成块,这些块作为磁盘和主存之间的传送单元。虚拟内存分割被成虚拟页。物理内存被分割为物理页,物理页和虚拟页的大小是相同的。任意时刻虚拟页都被分为三个不相交的子集:

•未分配的:VM系统还未分配的页

•缓存的:当前已经缓存在物理内存的已分配页

•未缓存的:当前未缓存在物理内存的已分配页

        每次将虚拟地址转换为物理地址,都会查询页表来判断一个虚拟页是否缓存在DRAM的某个地方,如果不在DRAM的某个地方,通过查询页表条目可以知道虚拟页在磁盘的位置。页表将虚拟页映射到物理页。如图7.3.1所示,页表就是一个页表条目的数组,每一个页表条目是由一个有效位和一个n位地址字段组成。有效位表明虚拟页是否缓存在DRAM中,n位地址字段是物理页的起始地址或者虚拟页在磁盘的起始地址。

        通过了解了上述虚拟地址转换为物理地址操作系统所提供的机制,现在我们来看一下到底是如何实现虚拟地址到物理地址的转换。

        n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,例如VPN 0选择PTE 0。根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。这里的VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。

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

        在Intel Core i7环境下研究 VA 到PA 的地址翻译问题。前提如下: 虚拟地址空间48位,物理地址空间52位,页表大小4KB,4级页表。TLB 4路16组相联。CR3指向第一级页表的起始位置(上下文一部分)。 解析前提条件:由一个页表大小4KB,一个PTE条目8B,共512个条目,使用9位二进制索引,一共4个页表共使用36位二进制索引,所以VPN共36位, 因为VA 48位,所以VPO 12位;因为TLB共16组,所以TLBI需4位,因为VPN 36位,所以TLBT 32位。

        CPU产生虚拟地址VA,VA传送给 MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)向TLB中匹配,如果命中,则得到PPN(40bit)与VPO(12bit)组合成PA(52bit)。如果TLB中没有命中,MMU向页表中查询,CR3确定第一级页表的起始地 址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成PA,并且向TLB中添加条目。如果查询PTE的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。

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

        讨论Cashe1的物理内存访问,Cashe2,Cashe3原理相同。

        由于L1Cashe有64组,所以组索引位s为6,每组有8个高速缓存行,由于每个块的大小为64B,所以块偏移为为6,因此标记位为52-6-6=40位。因此L1Cashe的物理访存大致过程如下:

(1)组选择取出虚拟地址的组索引位,将二进制组索引转化为一个无符号整数,找到相应的组;

(2)行匹配把虚拟地址的标记为拿去和相应的组中所有行的标记位进行比较,当虚拟地址的标记位和高速缓存行的标记位匹配时,而且高速缓存行的有效位是1,则高速缓存命中;

(3)字选择一旦高速缓存命中,我们就知道我们要找的字节在这个块的某个地方。因此块偏移位提供了第一个字节的偏移。把这个字节的内容取出返回给CPU即可;

(4)不命中如果高速缓存不命中,那么需要从存储层次结构中的下一层取出被请求的块,然后将新的块存储在组索引位所指示的组中的一个高速缓存行中。一种简单的 放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块, 产生冲突(evict),则采用最近最少使用策略LFU进行替换。

7.6Hello进程fork时的内存映射

        当fork函数被 shell 进程调用时,内核为新进程创建各种数据结构,并分配给 它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只 读,并将两个进程中的每个区域结构都标记为私有的写时复制。

7.7Hello进程execve时的内存映射

        execve () 系统调用加载 hello程序时,内核通过mmap () 系统调用完成虚拟地址空间的内存映射,核心流程如下:

1.虚拟地址分配:根据hello的ELF程序头表,为代码段、数据段、.got、.plt 等节分配连续的虚拟地址(如代码段0x4010d0-0x401245)。

2.文件映射:

•代码段(.text):将ELF文件中的代码段内容映射到虚拟地址0x4010d0-0x401245,权限设置为“只读+执行(R-E)”;

•只读数据段(.rodata):映射到虚拟地址0x402000-0x402049,权限设置为“只读(R--)”;

•动态链接相关节(.dynamic、.got、.plt):映射到虚拟地址0x403e50之后,权限根据需求设置(如.got为R-W)。

3.共享库映射:动态链接器(ld-linux.so)加载 libc.so 等共享库,为其分配独立的虚拟地址空间(如0x7f8b3a200000-0x7f8b3a420000),并建立映射。

4.栈与堆映射:为进程分配栈空间(默认大小8MB,如0x7ffd7b5d7000-0x7ffd7bd d7000)与堆空间(初始大小为0,动态增长),权限分别设置为“可读可写(R-W)”。

        内存映射完成后,hello进程的虚拟地址空间包含程序代码、数据、共享库、栈、堆等区域,各区域权限隔离,确保程序执行的安全性。

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

        缺页故障(Page Fault)是指CPU访问虚拟地址时,对应的页表项标记为“未映射”或“权限不足”,触发的中断(异常)。hello进程执行过程中常见的缺页故障场景及处理流程如下:

1.常见缺页故障场景:

(1)按需加载:execve () 映射内存时,仅建立虚拟地址与文件的映射关系,未将文件内容加载到物理内存;当CPU访问该虚拟地址时,触发缺页故障;

(2)写时复制:fork () 后子进程修改共享内存页时,触发缺页故障,内核执行COW复制;

(3)堆扩展:hello程序的printf 调用malloc动态分配内存时,堆空间不足,触发缺页故障,内核扩展堆空间。

2.缺页中断处理流程:

(1)触发中断:CPU访问虚拟地址时检测到缺页,陷入内核态,保存进程上下文;

(2)故障原因判断:内核检查页表项,判断缺页原因(未映射、权限不足、COW等);

(3)处理缺页:

•按需加载:内核分配物理页框,从ELF文件或共享库中读取数据到物理页,更新页表项(映射虚拟地址与物理地址);

•COW复制:分配新物理页框,复制原页数据,更新子进程页表项;

•权限不足:若进程无访问权限(如写代码段),则发送SIGSEGV信号终止进程;

(4)恢复执行:缺页处理完成后,内核恢复进程上下文,返回用户态,进程从缺页地址继续执行。

7.9动态存储分配管理

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

7.10本章小结

        本章主要介绍了hello的存储器的地址空间,介绍了四种地址空间的差别和地址的相互转换。同时介绍了hello的四级页表的虚拟地址空间到物理地址的转换。阐述了三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

第8章 Hello的IO管理

8.1Linux的IO设备管理方法

        一个Linux文件就是一个m字节的序列:B0,B1,B2……Bm

        所有的 IO设备(如网路、磁盘、终端)都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O,这使得所有的输入和输出都被当做相应文件的读和写来执行

        设备的模型化:文件

        设备管理:unix io接口

8.2简述Unix IO接口及其函数

Unix I/O接口:

(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。

(2)Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。 (3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。

(4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文 件,当k>=m 时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

(5)关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

Unix I/O函数:

(1)int open(char* filename,int flags,mode_t mode) ,进程通过调用open函 数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在 进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访 问这个文件,mode参数指定了新文件的访问权限位。

(2)int close(fd),fd 是需要关闭的文件的描述符,close 返回操作结果。

(3) ssize_t read(int fd,void *buf,size_t n),read函数从描述符为fd的当前文 件位置赋值最多n个字节到内存位置 buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。

4)ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。

8.3Printf的实现分析

int printf(const char *fmt, ...)
{
int i;
char buf[256];
   
     va_list arg = (va_list)((char*)(&fmt) + 4);
     i = vsprintf(buf, fmt, arg);
     write(buf, i);
   
     return i;
    }

        printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。

        接下来是write函数:

write:
     mov eax, _NR_write
     mov ebx, [esp + 4]
     mov ecx, [esp + 8]
     int INT_VECTOR_SYS_CALL

        在printf中调用系统函数write(buf,i)将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。

        查看syscall函数体:

sys_call:
     call save
   
     push dword [p_proc_ready]
   
     sti
   
     push ecx
     push ebx
     call [sys_call_table + eax * 4]
     add esp, 4 * 3
   
     mov [esi + EAXREG - P_STACKBASE], eax
   
     cli
   
     ret

        syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。于是我们的打印字符串就显示在了屏幕上。

        从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4Getchar的实现分析

        getchar的源代码为:

int getchar(void)  

{  

static char buf[BUFSIZ];  

static char *bb = buf;  

static int n = 0;  

if(n == 0)  

{  

n = read(0, buf, BUFSIZ);  

bb = buf;  

}  

return(--n >= 0)?(unsigned char) *bb++ : EOF;  

}         

        异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成ASCII 码,保存到系统的键盘缓冲区之中。

        getchar函数落实到底层调用了系统函数read,通过系统调用read读取存储在 键盘缓冲区中的ASCII码直到读到回车符然后返回整个字串,getchar进行封装, 大体逻辑是读取字符串的第一个字符然后返回。

8.5本章小结

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

结论

        用计算机系统的语言,逐条总结hello所经历的过程。

hello.c:编写c程序,hello.c诞生,它是一个二进制文本文件,hello.c中的每个字符都是用ascall编码表示;

hello.i:hello.c经过预处理阶段变为hello.i;

hello.s:hello.i经过编译阶段变为hello.s;

hello.o:hello.s经过汇编阶段变为hello.o;

hello:hello.o与可重定位目标文件和动态链接库链接成为可执行文件hello。至此可执行hello程序正式诞生;

运行:在终端输入2024112277 张越 18249727079 2;

创建子进程:由于终端输入的不是一个内置的shell命令,因此shell调用fork()函数创建一个子进程;

加载:shell调用 execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数;

上下文切换hello调用sleep函数之后进程陷入内核模式,处理休眠请求主动释放当前进程,内核进行上下文切换将当前进程的控制权交给其他进程,当sleep函数调用完成时,内核执行上下文切换将控制传递给当前进程;

动态申请内存:当hello程序执行printf函数是,会调用malloc向动态内存分配器申请堆中的内存;

信号管理:当程序在运行的时候我们输入Ctrl+C,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+Z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起;

终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构;

        通过全程剖析 hello 程序的“P2P”生命周期与“020”闭环流程,我深刻体会到计算机系统是软硬件协同的精密工程,其设计暗藏三大核心逻辑:一是 “分层抽象” 的智慧,从预处理、编译、汇编、链接的编译链路,到段式、页式、TLB、Cache的存储层次,每一层都通过抽象屏蔽底层复杂度,同时保留精准控制接口,既降低了开发门槛,又保障了执行效率;二是 “资源复用与隔离” 的平衡,动态链接通过共享库减少冗余存储,进程通过独立虚拟地址空间避免资源冲突,写时复制(COW)则在复用内存与隔离修改间找到最优解,体现了 “按需分配、能复不用” 的设计哲学;三是 “延迟处理与按需触发” 的优化思维,缺页加载、动态链接的延迟绑定(Lazy Binding)等机制,将资源消耗推迟到必要时刻,最大化提升了系统整体吞吐量。

附件

文件名

生成阶段

作用

hello.c

源代码阶段

程序原始文本文件,包含核心逻辑代码

hello.i

预处理阶段

预处理后的C语言文件,展开头文件、替换宏定义

hello.s

编译阶段

汇编语言文件,将预处理后的代码转化为汇编指令

hello.o

汇编阶段

可重定位目标文件(ELF格式),包含机器语言指令但未完成链接

hello

链接阶段

可执行目标文件(ELF格式),完成重定位与库链接,可被操作系统加载执行

hello_gdb.log

调试阶段

GDB调试日志,记录程序执行流程、断点信息及内存地址变化

readelf_hello_o.txt

分析阶段

hello.o的ELF格式分析结果,包含节信息、重定位表等

readelf_hello.txt

分析阶段

hello可执行文件的ELF格式分析结果,包含段信息、虚拟地址分布等

参考文献

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

[2]  深入理解计算机系统原书第3版-文字版.pdf

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

原文链接:https://blog.csdn.net/HITZY344177/article/details/156490332

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

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