摘 要
作此论文的目标是为了相识程序从输入终端到在终端中显示运行的一系列过程。本文具体分析了计算机在天生hello可执行文件的预处理、编译、汇编、链接、历程管理等整个生命周期,解析了hello程序从初始状态输入到竣事执行被接纳的全部过程,查看并注释了在此过程中的各种体系语言与程序命令的本身意义,相互之间的转化与最终执行的方法。借由此,希望见微实著地解析计算机体系的工作原理和体系布局,资助读者更深入地理解和把握C语言程序的编译和执行过程。
关键词:P2P;计算机体系;Linux ;shell
第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
机带RAM:20.0GB
体系类型:64位操作体系,基于x64的处理器
软件环境:
Windows11 64位,VMware,Ubuntu 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文件做了这几件事:
- 删掉所有注释,调整格式(如处理续行符号,删除空格等)
- 处理掉所有宏界说,将宏标识符更换为界说的值或代码片断
- 处理#include,从标准库中调用头文件对应的完备文件并插入代码
- 处理条件编译(#ifdef/#if/#endif),根据预界说宏或表达式决定保留/删除代码块
- 处理特别指令如#pragma,#error,#warning等
- 天生hello.i并添加行号标记
本章重要内容为在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:
C0:修正地址为 .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.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) |
比如:以下是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至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
(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的调用时,内核将遵循以下的处理步调:
- 权限查抄:查抄文件是否存在、是否可执行,以及用户是否有权限。
- 加载可执行文件与映射内存段:解析 ELF 格式:读取 hello 的 ELF 文件头,验证魔数(0x7F ELF)和架构兼容性;将.text映射到内存,权限为 r-x;将data和 .bss映射到内存,权限为 rw-。将其他段(如 .rodata)按需加载。
- 设置堆栈:
- 动态链接:若程序依赖动态库(如 libc.so),则加载动态链接器(ld-linux-x86-6so.2),动态链接器解析库依赖,重定位符号地址,完成延长绑定(Lazy Binding。
- 跳转到入口点:将控制权转移到程序的入口地址(e_entry,在 ELF 头中界说),通常为 _start 符号,start 初始化运行环境后调用 main 函数。
当我们在 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内核实现 - 知乎
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
104阅读
0回复
暂无评论,点我抢沙发吧
使用浏览器的分享按钮
分享给好友哦
点我复制链接