2025哈工大计统PA-P2P程序人生

摘  要

作此论文的目的是为了了解程序从输入终端到在终端中显示运行的一系列过程。本文详细分析了计算机在生成hello可执行文件的预处理、编译、汇编、链接、进程管理等整个生命周期,解析了hello程序从初始状态输入到结束执行被回收的全部过程,查看并注释了在此过程中的各种系统语言与程序命令的本身意义,相互之间的转化与最终执行的方法。借由此,希望见微实著地解析计算机系统的工作原理和体系结构,帮助读者更深入地理解和掌握C语言程序的编译和执行过程。

关键词:P2P;计算机系统;Linuxshell

 

第1章 概述

1.1 Hello简介

P2P:即From Program to Process,指hello.c从程序(Program)变为运行时进程(Process),将其变成可执行文件的主要步骤流程如下:

020:即From Zero-0 to Zero-0,描述从程序启动到资源完全释放的全生命周期,其生命周期的主要流程如下所示:

1.2 环境与工具

除第五章之外其他章节使用泰山服务器。

第五章edb工具使用虚拟机,具体为:

硬件环境:

处理器:12th Gen Intel(R) Core(TM)i5-12500H   2.50 GHz

机带RAM20.0GB

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

软件环境:

Windows11 64位,VMwareUbuntu 20.04 LTS

   开发与调试:vim objump edb gcc readelf等命令工具

1.3 中间结果

hello.i

预处理后得到的文本文件

hello.s

编译后得到的汇编语言文件

hello.o

汇编后得到的可重定位目标文件

hello.asm

反汇编hello.o得到的反汇编文件

hello1.asm

反汇编hello可执行文件得到的反汇编文件

1.4 本章小结

介绍了hello的P2P,020流程,包括流程的设计思路和实现方法;然后,详细说明了本实验所需的硬件配置、软件平台、开发工具以及本实验生成的各个中间结果文件的名称和功能。

第2章 预处理

2.1 预处理的概念与作用

2.1.1概念:

预处理是编译过程的初始阶段,由预处理器对源代码进行文本级处理。它通过解析以#开头的指令(如#include、#define),执行宏替换、文件包含、条件编译等操作,生成经过修改的中间代码(.i文件),为后续编译阶段提供标准化输入。预处理不涉及语法分析,仅按规则直接操作文本内容。

2.1.2作用:

预处理的核心作用在于增强代码可维护性和环境适应性。通过宏定义可将常量、函数代码块抽象为符号,提升代码复用性;#include指令实现头文件嵌入,支持模块化开发;条件编译则允许针对不同平台或配置生成定制化代码版本,解决跨平台兼容问题。此外,特殊指令能优化编译行为,#error和#warning可辅助调试。预处理本质是代码文本的智能扩展机制,为编译器提供纯净、适配性强的中间代码。

2.2在Ubuntu下预处理的命令

命令:gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析

使用cat显示hello.i的内容,我们可以发现输出结果有几千行,基本上全是预处理指令,而main程序并没有发生变化:

最下方没有发生变化的main函数

前方大量预处理指令中的一部分

在预处理过程中,计算机主要对.c文件做了这几件事:

  1. 删掉所有注释,调整格式(如处理续行符号,删除空格等)
  2. 处理掉所有宏定义,将宏标识符替换为定义的值或代码片段
  3. 处理#include,从标准库中调用头文件对应的完整文件并插入代码
  4. 处理条件编译#ifdef/#if/#endif),根据预定义宏或表达式决定保留/删除代码块
  5. 处理特殊指令如#pragma,#error,#warning
  6. 生成hello.i并添加行号标记

2.4 本章小结

本章主要内容为在linux环境中如何用命令对C语言程序进行预处理,以及预处理的含义和作用。然后用一个简单的hello程序演示了从hello.c到hello.i的过程,并用具体的代码分析了预处理后的结果。通过分析,我们可以发现预处理后的文件hello.i包含了标准输入输出库stdio.h的内容,以及一些宏和常量的定义,还有一些行号信息和条件编译指令。

第3章 编译

3.1 编译的概念与作用

3.1.1概念:

编译的概念是指将用高级程序设计语言书写的源程序,翻译成等价的汇编语言格式程序的翻译过程。

3.1.2作用:

使高级语言源程序变为汇编语言,提高编程效率和可移植性。计算机程序编译的基本流程包括词法分析、语法分析、语义分析、中间代码生成、代码优化和目标代码生成等阶段。

3.2 在Ubuntu下编译的命令

命令:gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析

 

3.3.1文件的定义:

文件定义中包含两个部分:

.arch armv8-a:指定目标架构为ARMv8-A(64位ARM架构)。

.file "hello.c":表示该汇编代码由hello.c源文件生成。

其中,.file声明了源文件,接下来的.text表示代码节开始,从这里开始存放可执行指令。

3.3.2.只读数据段(.rodata)

这一部分的主要工作内容是定义一个不可被更改的数据段用来存放输出

.section .rodata定义只读数据段,存放常量字符串。

.LC0用于存储字符串:string后面部分,这里因为篇幅原因只截取了一部分,是中文字符的八进制转义内容,转换为UTF-8字符后为:"用法: Hello 学号 姓名 手机号 秒数!\n"

.LC1用于存储格式化字符串:"Hello %s %s %s\n",用于后续printf调用。

其中.text(接下来出现的所有都是这个意思)表示代码节,.string用于声明一个字符串,.type用于声明一个符号的类型,.globl用于声明全局变量,而.align用于声明对指令或者数据的存放地址进行对齐的方式,这里使用8 字节(2^3)对齐数据,确保访问效率。

3.3.3.main函数

这一部分主要用来处理主函数中栈帧的分配,并且进行函数中存在的参数检查。在进行处理之前做如下内容:

用于声明main为一个全局符号以作为程序入口,并且指定main为函数类型。

这一部分是main函数最开始用于分配栈的内容。stp x29, x30, [sp, -48]!用于保存帧指针 x29 和链接寄存器 x30 到栈顶,后面的大括号内容表示sp 向低地址移动 48 字节,分配栈空间(用于局部变量和参数传递),! 表示更新 sp 的值。

注意.cfi_* 指令,是一段调试信息,用于栈展开(如 .cfi_def_cfa_offset 48 表示当前栈帧偏移为 48)。这是一个结构内容。

接下来的mov x29, sp将当前栈指针 sp 设置为帧指针 x29,标记栈帧起始。

分配完堆栈后紧接着要做的就是使用它。从.c文件中,我们可以看到一共有两个参数argc和argv需要在函数中被传递,str w0, [sp, 28]将 main 的第一个参数 argc(被存在寄存器w0中,一共32位)存入栈偏移 28 字节处。str x1, [sp, 16]将 main 的第二个参数 argv(被存在寄存器x1中,是一个64位指针-main函数中定义为**argv)存入栈偏移 16 字节处。

以str开头,说明这两个参数都是字符串。

3.3.4参数检查与错误处理

这是main中的一部分,主要是由于.c函数中的这部分内容存在所以才要进行的:

进行的if循环中,要求如果argc不等于五就直接退出程序,所以必须进行参数检查。之前已经提到过argc这个数是被存在寄存器w0里的,所以用的时候直接调用。ldr w0, [sp, 28]这条命令就执行了这个调用,他从栈中加载 argc 到 w0。Cmp是一条比较用命令cmp w0, 5用于检查 argc 是否等于 5。beq .L2表示若 argc ==5,则跳转到 .L2(正常逻辑);否则继续执行错误处理。

错误处理的逻辑如下:

adrp x0, .LC0:加载 .LC0(错误信息)的页地址到 x0。

add x0, x0, :lo12::LC0:修正地址为 .LC0 的低 12 位偏移

bl puts调用 puts 输出错误信息。

mov w0, 1 和 bl exit设置返回值 1 并退出程序。

3.3.5循环

(1)L2循环

主要用于给i赋值和跳进i对应的L3循环,这两条指令也是这个意思,.L2: str wzr, [sp, 44]用于将 32 位零寄存器 wzr 的值(即 0)存入栈偏移 44 字节处,作为循环变量 i 的初始值;b .L3用于跳转到循环条件判断 .L3。

值得注意的是,这是.s文件中出现不一样的跳转符号,我们注意到错误判断中的跳转使用的是beq,而这里使用的是b,一个简化后的形态。

(2)L3循环

这一部分本来出现在L4之后,但是由于L2涉及到了所以就先写他,L3是一个循环条件判断,用于判断L4循环是否能进行:

想进行这个循环首先需要有一个i,所以先从栈中调出i。ldr w0, [sp, 44]加载循环变量 i。

接下来执行循环。从c代码中能看出这个循环所要表达的意思:

 

即i要从1循环到九。cmp w0, 9比较 i 是否小于等于 9,ble .L4用来执行循环,即若 i <= 9,跳转到 .L4 继续循环(执行if函数内的内容);否则退出循环。L3的循环总次数循环总次数是从 i=0 到 i=9,共执行 10 次。

这里出现了第三个跳转命令ble。

(3)L4循环

这个循环是循环体本身。

首先看一下循环需要执行的代码以方便理解:

从c代码中不难看出,这个循环总共要干三件事:首先是调用数据,把存在sp里的只读字符串hello一系列拿出来,然后再把分配好的argv数组拿出来;第二件是填数,把argv数组拿出来填进字符串里,这一步由于hello中已经指明,所以只要调出来三个数对应位置就会在print中填写;接着是调用函数,每次循环都调用一个print和一个sleep。

这一部分是argv的调用,由于它是个数组,虽然说是调用一个参数但是一共有三个数,系统是通过移动字节参数的方式来调出这三个在存储器中连着存放在一起的数字的:首先ldr加载指针到最开始的地方,即[sp, 16],读出八位argv[1],紧接着向后移动八位读出第二个,再移动八位读出第三个,存储方式为先都存进x0,然后再分别存进x1,x2,x3寄存器中。

用于加载.LC1(格式字符串)的页地址,并修正地址为低 12 位偏移,这是一段系统内容。

紧接着调用printf打印内容。

因为sleep中使用了argv[4],在调用这个函数之前需要先找到他,按照原本的调用1,2,3的过程,指针需要先被定位到x0,但因为print函数也用到了x0寄存器(hello等内容存在x0里),一顿操作之后现在x0里面到底放了什么很难保证了,所以首先需要在调用sleep之前重新加载指针,确保x0寄存器里村的东西是正确的argv[1],这样才方便通过加八找到argv4.

完成循环后,程序将从L3的判断中退回上级,此时更新i的值,ldr w0, [sp, 44]重新加载循环变量i,add令i++,str w0, [sp, 44]将i存回栈偏移 44 字节处

至此整个程序运行完毕。

3.3.6.清理与返回

bl getchar等待用户输入一个字符(可能用于暂停程序),ldp x29, x30, [sp], 48
恢复帧指针 x29 和链接寄存器 x30,并释放 48 字节栈空间,ret用于返回 0(mov w0, 0 隐含返回值)。

.cfi在本章开头已经介绍过,是一个系统内容。

3.4 本章小结

这一章介绍了C编译器如何把hello.i文件转换成hello.s文件的过程,简要说明了编译的含义和功能,演示了编译的指令,并通过分析生成的hello.s文件中的汇编代码,探讨了数据处理,函数调用,赋值、算术、关系等运算以及控制跳转和类型转换等方面,比较了源代码和汇编代码分别是怎样实现这些操作的。

第4章 汇编

4.1 汇编的概念与作用

4.1.1概念:

汇编是将汇编语言(.s文件)转换为机器语言二进制代码(.o目标文件)的过程,由汇编器(Assembler)完成,生成与硬件直接交互的指令,.o文件是一个二进制文件,包含main函数的指令编码。

4.1.2作用:

将人类可读的汇编代码翻译为计算机可执行的机器码。汇编器逐行解析指令,将符号(如标签、变量)转换为具体的内存地址,并处理伪指令(如数据定义、段声明)。它通过地址解析和重定位表生成可重定位的目标文件,为后续链接器合并多个模块提供基础。

此外,汇编过程会检查语法错误(如无效操作码或寄存器使用),并生成调试信息(如符号表),便于开发者分析程序结构。通过优化指令顺序或选择更高效的机器码变体,汇编器还能在一定程度上提升代码执行效率。最终,汇编将高级语言或手写汇编的逻辑转化为底层硬件可直接运行的二进制程序,是连接软件设计与硬件实现的关键环节。

4.2 在Ubuntu下汇编的命令

命令:gcc -c hello.s -o hello.o

4.3 可重定位目标elf格式

4.3.1生成ELF

获得可重定位目标文件的命令:readelf -a hello.o > hello.elf

   

4.3.2.ELF文件内容与解析

ELF(Executable and Linkable Format)文件是一种常见的可执行文件和可链接文件格式,用于在Unix和类Unix系统上存储程序、库和其他相关数据。包括文件头、程序头表、节表、节、重定位表、符号表。

由于此文件格式是一个完全由十六进制组成内容组成的表,因此内容非常多,直接cat只会输出一部分,并且也看不懂。使用readelf -h或者反汇编语言可以查看elf的可读模式。

首先调出文件格式:

(1)头表

可以看到调出的是ELF头(head)。泰山服务器是英文版的,如果在自己的xunijineijinxing运行的话可以看到汉译版本,会更好理解。这个表头包含了ELF的类别,存储的数据,ABI版本,系统架构,入口点地址和程序头起点等描述ELF文件整体结构和属性的信息。

(2)程序头

可以看到本程序没有程序头,可能是因为.c文件比较简单的缘故。不过在庞大的代码体系中,一般出现的程序头作用是ELF 文件中 连接静态文件与动态执行 的桥梁。它定义了程序在内存中的布局、权限和依赖关系,是操作系统加载可执行文件的核心依据。

(3)节表

节表描述了ELF文件中各个节的信息,包括节的名称、类型、偏移、大小等。ELF文件的数据和代码通常存储在各个节中,比如.text节存储代码段、.data节存储数据段等。其中.text和.data是我们在汇编程序中声明的Section,而其它Section是汇编器自动添加的。

这个节表包含十三个节头:

名称

类型

作用

NULL

——

占位节,无实际内容,表示节头表的起始

Text(AX

PROGBITS(程序数据,如代码或初始化数据)

存放程序的机器指令(如函数代码),该节需加载到内存并可执行

rela.text(I-info

RELA(带加数的重定位表)

记录.text节中需要重定位的指令地址(如外部函数调用)

data(WA-Write + Alloc)

PROGBITS

存放已初始化的全局变量和静态变量。此处大小为0,表示无此类数据

Bss(WA)

NOBITS(该节在文件中不占空间)

存放未初始化的全局变量和静态变量。运行时分配内存,但文件中无实际内容。

Rodata(A-Alloc)

PROGBITS

存放只读数据(如字符串常量),需加载到内存.

Comment(MS-Merge+Strings

PROGBITS

存放编译器或链接器的注释信息, 内容为可合并的字符串,GCC就存在这里

note.GNU-stack

PROGBITS

指示栈的执行权限。若存在且非空,可能包含GNU_STACK标志(如禁止栈执行)。

eh_frame(A

PROGBITS

存放异常处理框架信息(如C++异常展开表)。

rela.eh_frame

RELA

记录.eh_frame节的重定位信息。

symtab

SYMTAB(符号表)

记录所有符号(如函数名、全局变量)的名称、类型和地址。

strtab

STRTAB(字符串表)

存放符号名称字符串(如main、printf)。

shstrtab

STRTAB

存放节名称字符串(如.text、.data)

由于有些内容是系统内容,有些内容比如NULL是个空的,所以这个节表并没办法直接使用cat查看全部内容,不过使用readelf -a hello.o可以查看部分这些表的具体条目中的一部分。

比如:以下是symtab表,记录了这个.c文件中的各种符号内容,包括这个文件本身的名字,各种定义参数的符号,以及使用的函数如print,sleep,main甚至exit也在这里。

还有一些其他的,比如以下是rela.text的内容,定义了一些需要重定位的内容的对应地址,一般而言,任何调用外部函数或者引用全局变量的指令都需要修改,而调用本地函数的指令不需修改。

4.4 Hello.o的结果解析

objdump -d -r hello.o  可以输出.o二进制文件的反汇编。

4.4.1.函数入口与栈分配

保存x29(帧指针)和x30(返回地址)到栈,分配48字节栈空间并设置帧指针x29 = 当前栈指针sp

从最开始的代码中,我们可以看到反汇编代码的格式,除了常见的stp,mov等在.s文件中可见的内容,前面还多了一串数字,这是对应指令的二进制编码。

4.4.2.参数保存

将argc(w0)(str)存入栈偏移28字节处,将argv指针(str)(x1)存入栈偏移16字节处。

和汇编语言中提到的一样,这里的调用依然遵循ARM64调用约定,即w0 存储 argc(32位),x1 存储 argv(64位指针)。

4.4.3.参数检查与错误处理

从栈加载(ldr)argc到w0,比较(cmp)w0与5,若相等(argc=5),跳转到地址0x30(分支到正常逻辑)。这段要注意的是,反汇编的跳转指令中,所有跳转的位置被表示为主函数+段内偏移量这样确定的地址,而不再是段名称。如此处的跳转到L3变成了main+0x30.

1c:加载.rodata段的页地址到x0(重定位标记:R_AARCH64_ADR_PREL_PG_HI21)

20:修正.rodata段内偏移(重定位标记:R_AARCH64_ADD_ABS_LO12_NC)

24: 调用puts输出错误信息(重定位标记:R_AARCH64_CALL26)

28: 设置返回值w0 = 1

2c: 调用exit(1)(重定位标记:R_AARCH64_CALL26)

从这里开始,反汇编语言进行了大量的重定位。查relatext表的时候,已经介绍过有哪些内容需要进行重定位,一般来说,在反汇编过程中处理 重定位(Relocation) 的核心目的是为了 解决地址不确定性,确保程序在链接或加载时能够正确解析符号和内存地址。有一些临时分配的代码和数据地址,外部引用标准库,跨段地址引用这些内容都需要重定义,以便后续填入合适的内容并将分散的代码块合成一个完整的执行文件。

在汇编文件中,这段代码内容被表示为:

可以看到反汇编文件加入重定位内容,并为指令添加机器数。相关命令与汇编语言中的相同,这其中的跳转采用了b.这个格式,在汇编语言中也出现过。但是跳转内容变成了一个具体地址,这个地址是原先以L开头的循环初始化。

4.4.4循环与取数操作

这段内容对应汇编文件中的取argv[i]至print的过程,基本流程和汇编文件中一样,也是每次都加载基极地址,然后通过加数取数,最后调用printf时,由于这是一个标准库,因此加入了重定位内容。这一段需要着重说明的是,汇编代码中原本的数字被改编成了十六进制。以取argv[0]为例:

反汇编中将8表示为#0x8,将操作数改成了十六进制。

其他的内容改变不多,而且思路和汇编语言基本一样,这里就不再赘述分析了。

4.4.5总结

汇编语言

反汇编语言

函数调用

正常使用bl命令调用数据与函数

使用call调用命令,加入重定位

操作数进制

正常使用十进制数

使用十六进制数

命令

正常命令格式

命令对应的机器语言+命令

分支与跳转

使用Lx表示分支

使用主函数+段内偏移量表示地址

4.5 本章小结

这一章介绍了汇编的含义和功能。以Ubuntu系统下的hello.s文件为例,说明了如何把它汇编成hello.o文件,并生成ELF格式的可执行文件hello.elf。将可重定位目标文件改为ELF格式观察文件内容,对文件中的每个节进行简单解析。通过分析hello.o的反汇编代码(保存在hello.asm中)和hello.s的区别和相同点,让人清楚地理解了汇编语言到机器语言的转换过程,以及机器为了链接而做的准备工作。

5章 链接

5.1 链接的概念与作用

5.1.1.概念

  链接(linkng)是将各种代码和数据片段收集并组合为一个单一文件的过程。

5.1.2.作用:

 在现代系统中,链接是由叫做链接器(1iker)的程序自动执行的,它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用。

5.2 在Ubuntu下链接的命令

命令:gcc hello.o -o hello.out

特别提一嘴:这个题在模板里特意强调了用ld链接,不止要链接hello.o。除了这个之外还能链接的文件有:crt1.o C 运行时启动文件,crti.o C 运行时初始化文件,libc.so C 标准库的共享库(动态链接版本),crtn.o C 运行时终止文件(C Runtime Termination)等一系列内容,但是我用的是学校服务器,找这些链接根本找不到地址,shell一个劲地显示no command no file的,把这一整个报告里能报的错都报了一遍,所以也没办法了,直接用gcc自动连接吧。

使用gcc自动连接的弊端是:没办法显示出全部的节表,因为大部分中间的链接过程都由gcc自动完成了,因此节表中会多出一个保存gcc的表,但少了如.got,.plt之类用来链接的表。因此,报告中的动态链接部分必须转到虚拟机中进行,这也是报告中使用自己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

要注意的一点是启动代码顺序必须按 crt1.o → crti.o → 用户代码 → 库 → crtn.o 的顺序链接,以确保初始化代码正确执行。

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

   这一步在反汇编过程中已经提到过,有关表的具体内容不再赘述:

5.3.1头表:

5.3.2:程序表(无):

5.3.3.节表

5.3.4节表内容(能调出的部分)

Rela.text

Rela.eh_frame

symtab

5.4 hello的虚拟地址空间

泰山服务器没有edb操作,以我现有知识无法查看虚拟地址,此处转移到虚拟机操作,调出edb DataDump窗口如下:

可以看到,程序从0x40100开始载入完成,在0x4000000x400f0结束。

从edb中,我们可以找到在elf文件节表中所表示的信息

我们在虚拟机中重新加载并调出elf表节表头,将表头中内容后面的虚拟地址与虚拟地址表中一一对应,即可找到对应信息。如.text对应的4010f0就可以在edb中找到。

5.5 链接的重定位过程分析

5.5.1.链接前后反汇编文件的比较

使用命令objdump -d -r hello > hello1.asm生成反汇编文件hello1.asm

与之前objdump -d -r hello.o > hello.asm生成的文件进行比较:

(1)函数数量增加:

如puts@plt,printf@plt等函数代码都被添加进反汇编中,因为此时重定位和链接已经完成,在hello.o反汇编中只是用地址表示的头文件都已经被找出并纳入系统执行范围等待被调用。

Hello.o反汇编中的exit重定位内容:

Hello.2链接完毕后对exit调出的函数:

Hello.o反汇编中的print重定位内容:

Hello.2链接完毕后对print调出的函数:

(2)调用指令参数改变:

函数调用之后的字节代码被链接器直接修改为目标地址与下一条指令的地址之差,指向相应的代码段:

(3)跳转指令参数改变:

在链接过程中,链接器解析了重定位条目,并计算相对距离,修改了对应位置的字节代码为PLT 中相应函数与下条指令的相对地址,从而得到完整的反汇编代码。如此处main代码后的地址就不再是虚拟地址,而是函数需要跳转到的实际地址

5.5.2重定位简介

计算机在链接时进行重定位的过程是将多个目标文件(.o 文件)和库合并为可执行文件的关键步骤。其核心目的是 解决代码和数据的地址不确定性,确保程序在内存中正确运行。

重定位主要由这几步组成:

(1) 符号解析:

确定所有符号(函数、变量)的最终地址。在这个过程中,链接器遍历所有目标文件,收集符号定义(如 main、printf),对于未定义的符号(如 printf),在链接的库(如 libc.a 或 libc.so)中查找定义。若符号未定义或重复定义,报错(如 undefined reference)。

(2) 段合并:

将相同类型的段合并为连续的内存区域,如将所有 .text 段合并为可执行文件的代码段,所有 .data 段合并为数据段。合并后,为每个段分配 虚拟内存地址(VMA

(3) 地址计算:

计算每个符号的最终地址。如:假设代码段起始地址为 0x400000,则 main 的地址可能是 0x400100;数据段起始地址为 0x600000,全局变量地址可能是 0x600020。

(4) 重定位修正:

根据符号的最终地址,修改指令或数据中的占位符。在此过程中,遍历重定位表(如 .rel.text),找到需要修正的位置,将指令中的临时地址(如 0x00000000)替换为实际地址。

可以看到,机器在重定位过程中对代码的修改全部都体现在了反汇编后的文件中,比如头文件的源文件并入,各种参数的改变等等。

5.6 hello的执行流程

使用gdb执行hello:

在main函数处设置断点并单步执行至程序结束。过程中只需重复输入next即可,图片太繁杂,此处只列出开始执行时到第一个断点的过程:

运行过程中,一共有如下函数参与了执行:

(I)开始执行:_start、_libe_start_main

(2)执行main:_main、printf、_exit、_sleep、getchar

(3)退出:exit

运行过程中,有如下地址:

程序名

地址

_start

0x4010f0

_libc_start_main

0x2f12271d

main

0x401125

_printf

0x4010a0

_sleep

0x4010e0

_getchar

0x4010b0

_exit

0x4010d0

5.7 Hello的动态链接分析

 在汇编文件中,计算机将整个程序切分成main,L1,L2,L3等小块,在后续的汇编,链接过程中再汇总到一起形成一个完整的程序库,这就是hello的动态链接过程。动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。

链接的过程由于使用了gcc链接,所以大部分连接过程是自动的。这部分必须转到虚拟机中进行,在5.1也已经说明。

调出虚拟机elf文件并查表:

可以看到表的数量比使用gcc更多。查看用于延迟绑定的got和plt,链接器采用延迟绑定的策略解决。动态链接器使用过程链接表PLT + 全局变量偏移表GOT实现函数的动态链接,GOT中存放目标函数的地址,PLT使用该地址跳转到目标位置,其中GOT[1]指向重定位表,GOT[2]指向动态链接器ld-linux.so运行地址。并查看他们的地址:

调用前

调用后

可以看到发生了变化。

5.8 本章小结

阐述了链接的基本概念和作用,展示了使用命令链接生成hello可执行文件,观察了hello文件ELF格式下的内容,利用edb观察了hello文件的虚拟地址空间使用情况,使用gdb对函数进程过程中的步骤做说明,最后以hello程序为例对重定位过程、执行过程和动态链接进行分析。

第6章 hello进程管理

6.1 进程的概念与作用

6.1.1概念

进程是计算机中程序执行的实例,拥有独立的内存空间和系统资源。操作系统通过进程管理程序运行,实现任务调度和资源分配,确保多任务并发执行时隔离与协调。在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

6.1.2作用
进程是操作系统资源分配的基本单位,负责隔离不同程序的执行环境,防止相互干扰。它通过分配独立内存和CPU时间片,支持多任务并发运行,提升系统效率。进程间通信机制允许数据交换与协作,满足复杂任务需求。进程为程序提供了一种假象,程序好像是独占的使用处理器和内存,处理器好像是无间断地一条接一条地执行我们程序中的指令。进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中。

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

Shell(Bash)的作用
Shell是用户与操作系统内核交互的命令行接口,负责解析用户输入的命令,协调进程执行、管理文件操作和环境配置,并提供脚本编程能力以实现自动化任务。

Shell(Bash)的处理流程
用户输入命令后,Bash首先解析命令中的特殊符号(如变量$VAR、通配符*、管道|),进行词法分割与语法分析。若命令涉及别名或函数,优先替换或调用。接着,检查是否为内置命令(如cd),直接执行;否则在PATH路径中查找外部可执行文件,通过fork创建子进程,exec加载程序,并处理输入/输出重定向(如>、<)或管道传递数据。命令执行后,父进程(Shell)捕获子进程退出状态,返回提示符等待新指令。同时,Bash管理作业控制(如后台运行&)、信号处理(如Ctrl+C中断),并维护环境变量与历史记录,实现高效的人机交互与脚本自动化。

6.3 Hello的fork进程创建过程

在运行Hello程序时,进程的创建过程如下:

1.Shell解析命令:用户在终端输入./hello后,Shell解析命令,准备执行目标程序。

2.Shell调用fork创建子进程:Shell进程通过fork()系统调用创建一个子进程。此时,子进程是Shell的副本,包含相同的代码、数据和环境变量。

3.子进程调用exec加载Hello程序:子进程调用execvp()(或类似函数)加载并执行hello程序。exec系列函数会替换当前进程的映像为hello的代码,但保留进程ID(PID)。

4.Hello程序执行(若内部有fork:若hello程序内部调用fork()(例如创建子进程处理任务),则会再次触发以下步骤:

fork创建孙子进程:hello进程调用fork(),生成一个子进程(孙子进程)。此时:

父进程(hello:继续执行原代码,根据fork()返回的子进程PID执行父进程逻辑。

子进程(孙子进程):从fork()返回0,执行子进程逻辑(如打印特定消息)。

写时复制(Copy-On-Write:父子进程共享内存空间,直到任一进程尝试修改内存时,系统才复制相关内存页,确保高效资源使用。

5.进程执行与结束Shell等待子进程:父进程(Shell)通过wait()等待子进程(hello)结束,回收资源并显示终端提示符。Hello及其子进程退出:hello程序及其可能的子进程完成代码执行后,通过exit()终止,释放资源。

6.4 Hello的execve过程

当调用execve运行hello程序时,依然是使用shell解析命令并由fork创建子进程。在子进程中进行execve的调用时,内核将遵循以下的处理步骤:

  1. 权限检查:检查文件是否存在、是否可执行,以及用户是否有权限。
  2. 加载可执行文件与映射内存段:解析 ELF 格式:读取 hello 的 ELF 文件头,验证魔数(0x7F ELF)和架构兼容性;将.text映射到内存,权限为 r-x;将data和 .bss映射到内存,权限为 rw-。将其他段(如 .rodata)按需加载。
  3. 设置堆栈:

构建用户态堆栈:压入命令行参数(argv)、环境变量,添加辅助向量,包含程序入口地址、平台信息等。

  1. 动态链接:若程序依赖动态库(如 libc.so),则加载动态链接器(ld-linux-x86-6so.2),动态链接器解析库依赖,重定位符号地址,完成延迟绑定(Lazy Binding。
  2. 跳转到入口点:将控制权转移到程序的入口地址(e_entry,在 ELF 头中定义),通常为 _start 符号,start 初始化运行环境后调用 main 函数。

6.5 Hello的进程执行

当我们在 Linux 终端输入hello程序并按下回车时,整个进程执行与调度的幕后过程涉及进程创建、上下文切换、用户态 / 核心态转换及调度器的复杂协作:

6.5.1从用户输入到进程创建

用户在 Shell(用户态)中输入命令后,Shell 通过系统调用(如fork()和execve())创建新进程。此时内核(核心态)会为hello进程分配独立的进程控制块(PCB),记录其唯一标识符(PID)、内存映射、打开文件描述符等进程上下文信息(包括通用寄存器值、程序计数器 PC、栈指针等)。同时,内核为其分配初始的时间片(通常由调度算法如 CFS 确定,默认约 10-100 毫秒),并将进程状态设为 “可运行”(TASK_RUNNING),加入调度器的可运行队列。

6.5.2 进程调度器的决策

Linux 内核的调度器(如 CFS,完全公平调度器)基于进程的优先级虚拟运行时间(vruntime)管理可运行队列。hello作为普通用户进程,初始优先级较低(nice 值默认 0),但调度器会为其分配公平的时间片。当 CPU 空闲或当前运行进程阻塞(如等待 I/O)时,调度器触发上下文切换(Context Switch

保存当前进程上下文:内核将正在运行进程的寄存器值、PC 指针等状态存入其 PCB。

选择hello进程:从可运行队列中选取 vruntime 最小的进程(即 “最需要运行” 的进程),此处假设为hello。

加载hello进程上下文:将其 PCB 中的寄存器值、程序地址等恢复到 CPU,使hello从上次暂停的位置继续执行。

6.5.3 用户态与核心态的切换

hello程序的代码属于用户态,只能访问受限的内存区域和 CPU 指令。当需要执行特权操作(如输出到终端)时,必须通过系统调用进入核心态:

用户态执行:hello的主函数开始运行,CPU 执行用户空间的机器指令,例如初始化变量、计算字符串长度等。此时 CPU 处于用户模式,权限级别低,无法直接操作硬件。

系统调用进入核心态:当执行到printf("hello\n")时,程序通过write()系统调用向内核请求写入终端。此时 CPU 切换至核心模式,内核根据系统调用号找到对应的处理函数(如sys_write),验证参数合法性后,操作硬件设备(如通过驱动程序控制显示器)。

核心态返回用户态:系统调用完成后,内核将结果存入寄存器,并触发模式切换,CPU 回到用户模式,hello继续执行后续代码(如退出前的清理操作)。

6.5.4时间片耗尽与调度

若hello的时间片未耗尽就完成执行(如简单的输出操作),则内核将其状态设为 “僵尸进程”(TASK_ZOMBIE),等待父进程(Shell)通过wait()系统调用回收资源。但若时间片耗尽时hello仍在运行(如陷入死循环),调度器会强制触发上下文切换:

保存hello上下文:内核记录其当前执行位置(PC 值)、栈状态等,以便下次恢复运行。

重新调度:hello被放回可运行队列,等待下一次被调度器选中。由于 CFS 的公平性,长时间运行的进程会因 vruntime 增加而降低调度优先级,确保其他进程(如交互程序)获得及时响应。

6.5.5进程终结

当hello执行完main函数或调用exit()时,会通过系统调用exit_group()进入内核态。内核释放其占用的内存、文件句柄等资源,将 PCB 标记为 “僵尸” 状态(保留少量信息供父进程读取),最终由 init 进程(PID=1)统一回收。至此,hello进程的生命周期结束。

6.6 hello的异常与信号处理

6.6.1.异常的种类与处理

程序异常可以发生在程序运行过程中也可以发生在程序运行之外。运行时异常都是 RuntimeException 类及其子类异常,如 NullPointerException、IndexOutOfBoundsException 等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般由程序逻辑错误引起,程序应该从逻辑角度尽可能避免这类异常的发生。这类异常情况是我们需要讨论的内容。常见异常种类如下:

对这几种运行异常,处理方式如下:

6.6.2程序运行

(1)正常运行

当输入信息足够时,根据输入的信息和输入的秒数打印八次信息并以回车键返回。

(2)运行中按ctrl+c:

shell收到信号,结束并回收进程

(3)运行按ctrl+z:

进程收到SIGSTP信号,Shell显示屏幕提示信息并挂起hello进程。

通过ps和jobs命令,可以看到hello这一进程被挂起的信息:

(4)挂起后续:

使用kill可以将此信息挂起进程杀死。首先查询被挂起的程序pid:

接着使用kill强制杀死程序,再调用ps时即可发现程序已被杀死

也可以使用fg 1命令把挂起的程序调回前台执行。重新执行时,hello将从挂起初运行,打印剩下的语句并且正常结束,完成回收。在此程序中,可以看到最后一回车为结尾正常退出。

(5)随便按键:

运行中随便按的键都会以字符形式显示在shell上,hello结束后,stdin中的其他字串会当做Shell的命令行输入。

6.7本章小结

探讨了计算机系统中的进程和shell,首先通过一个简单的hello程序,简要介绍了进程的概念和作用、shell的作用和处理流程,还详细分析了hello程序的进程创建、启动和执行过程,最后,本章对hello程序可能出现的异常情况,以及运行结果中的各种输入进行了解释和说明。

7章 hello的存储管理

7.1 hello的存储器地址空间

7.1.1逻辑地址

当程序员编写hello.c代码并编译时,编译器生成的地址是逻辑地址。这些地址是相对于程序自身起始点的偏移量,不考虑程序实际运行时在物理内存中的位置。例如,hello程序的main函数可能被编译到逻辑地址0x1000,全局变量message可能位于0x2000。逻辑地址是程序内部使用的相对地址,用于简化代码编写和模块间的引用。

7.1.2线性地址

在 x86 架构中,虚拟地址首先通过分段机制转换为线性地址。分段机制将虚拟地址空间划分为多个段,每个段有基址和限长。然而,现代 Linux 系统通常禁用分段(所有段的基址设为 0),因此线性地址通常直接等于虚拟地址。例如,hello程序的虚拟地址0x401000会直接作为线性地址传递给分页机制处理。线性地址是分段和分页之间的中间抽象层。

7.1.3虚拟地址

当hello程序被加载到内存运行时,Linux 内核为其创建独立的虚拟地址空间。虚拟地址是进程可见的地址,每个进程都认为自己拥有整个内存空间(如 64 位系统中的 47 位地址空间)。hello的代码段、数据段等被映射到这个虚拟地址空间的不同区域,例如代码段可能位于0x400000起始处。虚拟地址提供了内存隔离和保护,使得多个进程可以同时运行而互不干扰。

7.1.4物理地址

物理地址是实际内存(DRAM)中的地址。线性地址通过分页机制(页表)转换为物理地址。例如,hello程序中的线性地址0x401000经过页表查找,可能映射到物理地址0x900000。这种映射关系由操作系统动态维护,并通过硬件 MMU(内存管理单元)快速转换。物理地址最终用于 CPU 访问实际的内存单元,完成数据读取或指令执行。

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

在 Intel x86 架构中,逻辑地址到线性地址的变换通过段式管理实现。逻辑地址由段选择子(16 位,含段索引、表指示位 TI、请求特权级 RPL)和偏移量(32 位)组成。系统维护全局描述符表(GDT)和局部描述符表(LDT),每个段描述符(8 字节)包含段基址、段限长和访问权限。地址转换时,先根据段选择子的段索引和 TI 在 GDT/LDT 中定位段描述符,提取其段基址,再将段基址与偏移量相加得到线性地址,同时检查偏移量是否超出段限长。但值得一提的是,现代 Linux 采用平坦内存模型,将所有段基址设为 0,使线性地址直接等于偏移量,简化了转换过程,对内存的管理大部分使用分页管理而不是分段。

通常用以下方式表示一段:

段式管理的流程如下:

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

在 Linux 系统中,hello程序的线性地址到物理地址的变换通过页式管理实现。线性地址被拆分为页全局目录索引、页上级目录索引、页中间目录索引、页表索引和页内偏移。CPU 通过 CR3 寄存器找到hello进程的页全局目录,再经多级页表(PGD→PUD→PMD→PT)逐级查找,最终由页表项的物理页框号结合页内偏移生成物理地址。为加速转换,CPU 使用 TLB 缓存近期映射,若 TLB 未命中则访问内存页表。若页表项无效则触发缺页异常,内核将对应页面从磁盘加载到内存并更新页表。这一过程实现了内存离散分配、虚拟内存和进程隔离,确保hello程序高效安全运行

页式管理的流程如下所示:

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

在 TLB 与四级页表(PGD→PUD→PMD→PT)支持下,Linux 系统将hello程序的虚拟地址(VA)转换为物理地址(PA)的过程如下:VA 被拆分为 PGD 索引(最高 12 位)、PUD 索引(9 位)、PMD 索引(9 位)、PT 索引(9 位)和页内偏移(最低 12 位)。CPU 首先检查 TLB 是否缓存该 VA 的映射,若命中则直接获取 PA;未命中时,通过 CR3 寄存器找到 PGD 基址,依序查询 PGD→PUD→PMD→PT,每级索引定位下一级页表的物理地址,最终由 PT 表项的物理页框号结合页内偏移生成 PA,并将该映射存入 TLB。若页表项无效则触发缺页异常,内核加载对应页面并更新页表。这一机制通过 TLB 缓存加速访问,利用多级页表实现内存离散分配与虚拟内存,确保hello程序的地址转换高效且安全。

工作原理如下:

多级页表的工作原理展示如下:

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

在三级 Cache(L1、L2、L3)支持下,CPU 访问物理内存时通过分层缓存机制提升效率,具体过程如下:

CPU 首先根据物理地址中的标签(Tag组索引(Index)和块偏移(Offset,按 L1→L2→L3 的顺序逐级查找缓存:


 

L1 Cache:速度最快(纳秒级),容量最小(通常 32KB-64KB / 核),分为指令缓存(I-Cache)和数据缓存(D-Cache)。CPU 先检查 L1 D-Cache(数据访问时)或 I-Cache(指令访问时)是否命中:

若命中,直接通过块偏移读取缓存行中的数据,无需访问内存。

若未命中,进入 L2 Cache 查找。

L2 Cache:容量较大(数百 KB 到数 MB),速度稍慢于 L1。同样通过组索引定位缓存组,对比标签确认是否命中:

命中则读取数据并传递给 L1,同时可能更新 L1 缓存(依写策略而定)。

未命中则继续访问 L3 Cache。

L3 Cache:容量更大(数 MB 到数十 MB),通常为多核共享。查找方式与 L2 类似:

命中后将数据传递给 L2/L1,并可能更新下级缓存。

若三级 Cache 均未命中(缓存不命中),CPU 才会访问物理内存,从内存中读取数据块(通常 64 字节 / 缓存行),依次写入 L3→L2→L1 Cache,并最终返回数据。

7.6 hello进程fork时的内存映射

当hello进程调用fork()时,Linux 通过写时复制(COW)机制处理内存映射:内核为子进程创建独立虚拟地址空间并复制父进程页表,但所有页面标记为只读,父子进程共享同一物理内存。当任一进程尝试写入时触发页错误,内核分配新物理页、复制原内容、更新写入方页表为可写,原物理页仍由另一进程使用。若子进程立即执行新程序则跳过 COW。这一机制避免不必要复制,仅在写入时分配物理页,实现高效内存利用与快速进程创建。以下是fork创建一个进程的步骤图:

7.7 hello进程execve时的内存映射

hello进程调用execve()执行新程序(如ls)时,Linux 通过替换内存映射机制重新构建地址空间。

内核首先解析新程序的可执行文件(如 ELF 格式),根据文件头信息(如PT_LOAD段)确定代码段、数据段等的虚拟地址范围和属性(可读 / 写 / 执行)。然后释放hello进程原有的页表和物理内存映射(除共享库等保留部分),为新程序创建全新的页表结构:

代码段:从 ELF 文件加载指令到虚拟地址空间,标记为可读可执行,对应物理页通过按需调页(Demand Paging)机制在首次访问时从磁盘加载。

数据段:初始化全局变量和静态变量,若为零初始化(BSS 段)则不占用磁盘空间,仅在内存中分配零填充页,标记为可读可写

堆与栈:堆用于动态内存分配(brk系统调用),栈从高地址向下增长,用于函数调用和局部变量,栈初始内容包含命令行参数和环境变量,标记为可读可写

同时,若新程序依赖共享库(如libc.so),内核通过动态链接器将库的代码段和数据段映射到进程地址空间的共享区域,共享库的物理页由多个进程共享。

最终,hello进程的虚拟地址空间被新程序完全覆盖,原有的内存映射被销毁,CPU 从新程序的入口地址(如 ELF 头的e_entry)开始执行,实现进程功能的彻底切换。这一过程通过直接替换而非复制内存,确保高效加载新程序并隔离新旧地址空间。

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

当 CPU 访问虚拟地址对应的物理页未加载到内存时,触发缺页故障(Page Fault,操作系统通过缺页中断处理程序完成内存加载,具体流程如下:

1.CPU 在地址转换过程中发现页表项的 存在位”为 0,触发缺页异常并暂停当前指令执行。内核接管后:

2.判断页面合法性:检查页表项的其他标志位(如用户 / 内核态权限、读写权限),若为非法访问(如用户态访问内核页),触发段错误终止进程。

此时做合法缺页处理以处理错误

合法存在的页:

文件映射页(如可执行文件、共享库):从磁盘的可执行文件或共享库中读取对应数据块,分配物理页并建立页表映射(按需调页)。

匿名页(如堆、栈、未初始化变量)

若为堆 / 栈扩展(如brk或mmap分配内存),分配空闲物理页并清零,更新页表为可读写;若为写时复制页(如fork后的共享页),若属于写操作触发缺页,分配新物理页并复制原页内容(COW 机制)。

3.更新页表与 TLB:将物理页帧号填入页表项,设置 “存在位” 为 1(若为写操作还需设置 “脏位”),并刷新 TLB(转换后援缓冲器)以确保后续地址转换使用新映射。

4.恢复指令执行:CPU 重新执行触发缺页的指令,此时虚拟地址已映射到合法物理页,访问正常完成。

总体流程如下图所示:

7.9动态存储分配管理

printf调用可能涉及动态内存分配(如处理变长参数或大缓冲区),这依赖于操作系统的动态内存管理机制。以下是其基本方法与策略的核心要点:

1.内存分配单位(堆):堆是进程地址空间中用于动态分配的区域,由低地址向高地址增长(通过brk或mmap系统调用扩展),堆管理器(如 glibc 的malloc)负责维护堆空间的分配与回收。

2. 分配算法:

显式空闲链表:将空闲块用链表连接,每个块包含头部(记录大小、是否空闲)和数据区。分配时遍历链表寻找合适块(如首次适应、最佳适应、最坏适应)。

隐式空闲链表:仅维护已分配块,通过块头部的大小字段推算下一个块的位置,分配时需遍历所有块,效率较低。

伙伴系统(Buddy System:将内存按 2 的幂次划分,分配时向上取整到最近的 2 的幂(如请求 21 字节→分配 32 字节),合并时检查相邻的 “伙伴块” 是否空闲以减少碎片。

3. 优化策略:

内存池(Memory Pool:预分配大块内存,按固定大小切割成 “池”,用于频繁分配的小对象(如printf的临时缓冲区),减少系统调用开销。

分离适配(Segregated Free Lists:按大小范围维护多个空闲链表(如 < 32 字节、33-64 字节等),分配时快速定位合适链表,提升效率。

延迟合并:释放内存时暂不合并相邻块,待后续需要时再合并,减少遍历开销。

4. 内存回收与碎片处理:释放内存时,若相邻块为空闲则合并为更大块,减少外部碎片;通过移动已分配块,将所有空闲块合并为连续区域,但需暂停程序执行,仅在极端情况下使用;分配的内存块大小通常为字长的整数倍(如 8/16 字节),确保高效访问。

5. 系统调用接口

brk/sbrk:通过调整堆顶指针(break)扩展或收缩堆,适用于小内存分配。

mmap:直接从文件映射或匿名映射分配内存(如大对象 > 128KB),独立于堆,避免碎片,但释放时需通过munmap显式回收。

动态内存管理通过分层抽象(用户接口→分配算法→系统调用)和优化策略(池化、分离适配、延迟合并),在效率(减少系统调用)和利用率(降低碎片)之间取得平衡,确保如printf等函数能高效处理临时内存需求。

7.10本章小结

本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以intel Core i7在指定环境下介绍了虚拟地址VA到物理地址PA的转换、物理内存访问,分析了hello进程fork时的内存映射、hello进程、execve时的内存映射、缺页故障与缺页中断处理。

结论

Hello从c文件代码到被机器执行的过程是一个很漫长的过程,从自shell输入到输出过程中,此次作业所涉及到的相关知识点如下:

1. 预处理:

   预处理器处理hello.c中的#指令(如#include <stdio.h>),展开头文件内容

   替换宏定义(如#define),处理条件编译指令(如#ifdef)

   生成扩展后的源代码文件(如hello.i)

2. 编译:

   编译器将hello.i翻译成汇编代码(如hello.s)

   进行词法分析、语法分析、语义分析,生成中间代码(如三地址码)

   优化中间代码(如常量传播、死代码消除),生成目标机器指令

3. 汇编:

   汇编器将hello.s翻译成机器码,生成可重定位目标文件(如hello.o)

   为每个指令和全局变量分配临时地址(如.text段、.data段)

   创建符号表,记录全局变量和函数的名称及地址

4. 链接:

   链接器将hello.o与标准库(如libc.a)和其他目标文件链接

   解析外部符号引用(如printf),确定最终地址

   合并代码段、数据段,调整重定位条目,生成可执行文件hello

5. 加载:

   shell调用fork创建子进程,execve加载hello程序

   将hello的代码段、数据段映射到虚拟地址空间(如0x400000)

   初始化栈和堆,设置程序入口点(如_start)

6. 地址转换:

   逻辑地址通过段式管理(基址0)转换为线性地址(如0x401000)

   线性地址拆分为页表索引(PGD/PUD/PMD/PT)和页内偏移

   通过多级页表映射到物理地址(如0x900000)

7. 缓存访问:

   CPU通过TLB加速地址转换,未命中时访问内存页表

   物理内存访问经三级Cache(L1→L2→L3),命中时直接读取缓存数据

   未命中时从内存加载数据块到Cache,更新相应缓存行

8. 执行:

   CPU执行hello指令,遇到printf调用,触发动态内存分配

   malloc向堆申请空间,可能调用sbrk/brk或mmap扩展堆

   printf格式化字符串,调用write系统调用,触发用户态到核心态的切换

   内核处理write请求,将数据写入终端驱动程序

9. 进程管理:

   若hello被Ctrl+Z挂起,进程状态变为TASK_STOPPED,PCB信息被保留

   使用kill命令时,通过PID或作业号找到对应进程,发送SIGTERM/SIGKILL信号

   信号处理程序响应信号,终止进程并回收资源

10. 内存管理:

    fork创建子进程时通过写时复制(COW)共享物理内存,修改时复制页面

    execve加载新程序时替换原有内存映射,重新分配页表

    缺页中断时,内核从磁盘加载页面到内存,更新页表和TLB

附件

文件名

功能

hello.c

源程序

hello.i

文本文件

hello.s

汇编语言文件

hello.o

可重定位目标文件

hello.elf

readelf读取hello.o得到的ELF格式信息

hello.asm

反汇编hello.o得到的反汇编文件

hello1.asm

反汇编hello可执行文件得到的反汇编文件

hello

可执行文件

参考文献

[1]   Randal E.Bryant David R.O'Hallaron.深入理解计算机系统(第三版).机械工业出版社,2016.

[2]   256-Linux虚拟内存映射和fork的写时拷贝_linux fork 内存拷贝-CSDN博客

[3]   [转]printf 函数实现的深入剖析 - Pianistx - 博客园

[4]   printf背后的故事 - Florian - 博客园.

[5]   linux2.6 内存管理——逻辑地址转换为线性地址(逻辑地址、线性地址、物理地址、虚拟地址) - 刁海威 - 博客园

[6]   解析Linux进程的创建函数fork()及其fork内核实现 - 知乎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值