【精通内核】计算机程序的执行原理深度解析

简介: 深度解析ELF文件中将内存布局地址,CPU是如何执行指令的,C语言中方法的执行过程的内核调用。

前言

📫作者简介小明java问道之路,专注于研究计算机底层/Java/Liunx 内核,就职于大型金融公司后端高级工程师,擅长交易领域的高安全/可用/并发/性能的架构设计📫

🏆CSDN 专家博主/Java 领域优质创作者、阿里云专家博主、华为云享专家、51CTO 专家博主🏆

🔥如果此文还不错的话,还请👍关注、点赞、收藏三连支持👍一下博主~

本文导读

了解过ELF文件内容,我们知道程序由各种段组成,仅仅了解程序的组成还不够,本讲深入计算机程序(包含C/C++、Java、Python等等)所有语言的执行原理,同时了解在ELF文件中将内存布局地址,CPU是如何执行指令的,C语言中方法的执行过程的内核调用。

一、程序虚拟地址空间布局

在介绍ELF文件内容时,我们知道程由各种段组成,同时在LF件中将内存布地址都已经描述完成。程序读取到内存中后,根据 ELF的描述,决定是否执行动态链接。最后生成的程序布局图如图所示。

网络异常,图片无法展示
|

寻址空间为4GB的内存模型图,这里的地址空间是虚拟地址空间,底层的线性地址分段和物理地址分页,上层是无感知的。可以看到,每个程序的虚拟地址空间最高 1GB 处都是操作系统的内核映射,这是因为不管程序如何映射,都需要一段虚拟地址空间用于映射内核,这样我们才能通过系统调用访问内核。

整个程序包含如下部分

1、text segment 程序代码段

2、data segment 数据段

3、BSS segment 未初始化的数据段

4、 heap 堆区。由低地址往高地址扩张

5、memory mapping region 其实也属于堆区,只不过这一部分可以通过 mmap 来产生映射

6、stack程序运行时需要的栈内存,由高地址往低地址扩张

由于内部数据和函数,均在两个连接库中使用绝对地址所以我们将关注点放在全局数据和函数上。由于代码段 .text ,加载到内存中,OS不允许修改代码段的内容,他只读(保护程序)而对于数据段而言,非 .rodata,其他数据是可读可写的,所以维护表,来存储自己的程序的虚拟地址值。

网络异常,图片无法展示
|

二、CPU执行指令原理

我们了解到一个程序通过gcc编译,经过预处理里、编译、链接等步骤,最终生成了 ELF类型的二进制文件。通过反汇编,我们了解到这些文件其实就是之前学过的汇编语言 mov、sub 等,然后结合操作一系列的寄存器完成了整个执行过程。

本节我们就来探究以下两个问题:

CPU是如何执行指令的?C语言中方法的执行过程?

这里我们通过gdb来调试,并观察整个执行过程。读者需要寄存器。首先通过 gcc -g demo.c 编译源文件,生成 a.out ELF可执行文件。然后通过gdb的断点机制,对main函数打上断点,gdb a.out 。

程序成功地停止在断点处,观察此时的寄存器状态

#include<stdio.h> 
int sum(int a, int b) {
  int c = a + b; 
  return c;
}
int main() {
  int i = sum(3,3); 
  printf("%d",i); 
  return 1;
}

网络异常,图片无法展示
|

重点观察 RIP 和 CS 寄存器,此时的 RIP为0x40054f,CS为0x33。注意,OS 位数为64位,所以这里以R开头。前面介绍的16位为IP,32位为EIP,其他寄存器也是如此。

RIP为0x400537,CS为0x33,RSI和RDI为3,即我们传入的参数。接下来继续执行,此时,RIP为 0x400542,CS为0x33,RSI和RII为3,rax保存了返回值6,EFLAGS变为了 0x206。其实,就是增加了一个PF(奇偶校验位)。可以看到,RIP 的计数和编译后的 ELF 文件地址一样。这就意味着,编译时就确定了虚拟地址的信息,这也正是虚拟地址→线性地址→物理地址映射的魅力。每个程序都认为自己占有整个内存地址,事实上底层由 OS结合硬件来进行段表、页表映射。

从上面的分析过程,我们可以得出以下几点信息。

1、CPU 通过 RIP 来获取指令的地址。

2、C语言程序通过寄存器来传递参数。当然也可以通过栈,如参数太多、寄存器放不下等情况。

3、虚拟地址在 ELF 文件中就已经确定。

4、调用方法的过程中,CS 代表代码段寄存器。启动保护模式后,CS 为段选择子 0x33,可以确定main 函数和sum函数处于同一个段,且变为二进制为110011。段选择子后面的两位代表着4个特权级--0、3。其中,0 代表 OS 特权级,3 用于用户程序,这里正好 11(二进制)为3。同时,第3位表明段信息是 LDT 和 GDT,这里为 0,表明在 GDT 中。其余高13位用于在段描述符表中,作为索引查询段基址。

5、执行方法后,发生了改变,从原来的IF 增加到了 PF、IF。

通过这些观察和结论,我们可以总结CPU如何执行程序的:

首先通过 CS 和 IP 寄存器定位到需要执行的指令,然后执行指令,接着根据执行的结果设置 EFLAGS 寄存器,最后在调用方法时通过寄存器或者栈来传递参数,并且在ELF文件生成时就已经确定了程序的虚拟地址。

三、C语言中方法的执行原理

接下来我们研究C语言方法的执行过程,我们继续用上面的代码,文字解释过于隐晦,我们用一组图,来描述从 main 函数到 sum 函数的调用过程。

#include<stdio.h> 
int sum(int a, int b) {
  int c = a + b; 
  return c;
}
int main() {
  int i = sum(3,3); 
  printf("%d",i); 
  return 1;
}

1、下图,为进入 main 函数、开辟空间并调用月call sum 指令后的状态,因为 call 指令会自动将 call指令的下一条指令压入栈中,所以才通过 rsp 开辟空间。这里一定要注意,push 指令和 pop 指令都是显示操作栈指针rsp的。esi 和 edi 分别是从C函数的右到左开始保存参数,edi为第1个参数3,esi为第2参数3。

网络异常,图片无法展示
|

2、下图,为sum 函数通过操作 rsp 和 rbp 开辟了 sum 函数栈帧的状态。我们可以看到通过将 rsp 和 rbp  设置相等,就得到了一个新的栈,其中 rbp 指向栈底,rsp 指向栈顶。

网络异常,图片无法展示
|

3、下图,为 sum 函数执行完成但没有返回的状态。可以看到,操作栈时并没有通过 rsp 栈指针,而是直接通过 rbp 来执行存取运算的。这是可行的,因为我们不需要开辟栈帧,直接操作rbp即可,通过rbp 将局部变量 3和运算结果放入 sum 函数的栈帧中。其实根本没必要将这些局部变量压栈、出栈,因为我们只是加两个数然后直接返回,但编译器为何这么不聪明呢?答案是没有开启优化。通过gcc-O开启优化后,会发现这些都没了。编译器并不是不聪明,而是太听话,完全按照程序写的逻辑来生成对应代码。例如,看到代码“int c=a+b;”,c是个局部变量量,编译器一定会保存 a+b 的结果。同时,我们看到 eax寄存器 中保存了返回值6

网络异常,图片无法展示
|

4、下图,为sum 函数执行完成后的状态,可以看到,之前 sum 函数栈帧的数据还存在,但这并不响结果。读者可能会想到一个问题,就是野指针。如果我们其他程序用一个未初始化的指针读取值,就有可能读到这些脏值。需要注意的是,函数返回后,我们需要恢复 main 函数的栈帧,该怎么做呢?必然是将之前保存的rbp 地址恢复到 rbp 寄存器中,这时 main 函数栈帧的栈底就设置了。然后将 call指令保存的指令弹出到 rip 寄存中,执行流程就顺利地进入了 call 指令的下一个指令中,即返回 main函数中。

网络异常,图片无法展示
|

5、为main 函数执行后的状态。因为 printf 函数也是 call 指令调用的,所以可以看到之前为什么开辟了 16byte 空间,分别用于保存两个call 指令的下一条指令地址,并且我们复用了 8byte的地址用于临时保存变量6。从汇编代码中可以发现,编译器做了很多我们认为没必要的动作,如返回值入栈、出栈、给 esi 等。再次强调,这不是因为编译器不聪明,而是因为它太听话。读者可以推理,main 函数执行完毕后,通过保存的 rbp 和返回地址,也可以退出 main 的栈帧。

网络异常,图片无法展示
|

小结:这个过程很烦琐,读者只需要知道以下几点即可

一、C语言的方法调用,就是保存返回地址,通过操作 rsp 和 rbp开辟栈帧。

二、参数可以通过 esi和edi 寄存器传递。

三、返回值放在 eax寄存器中。

四、Call指令会自动地将下一条指令压入程序栈中。

五、Ret指令自动将 call 压入的返回地址弹出放入 rip 寄存器中,从而达到函数的 return。

总结

我们知道程序由各种段组成,了解在ELF文件中将内存布局地址,CPU通过寄存器执行指令的,C语言中方法的执行过程,就是CPU操作寄存器的过程。同时我们还需要理解C语言的方法调用,就是保存返回地址,通过操作 rsp 和 rbp开辟栈帧。 参数可以通过 esi和edi 寄存器传递。返回值放在 eax寄存器中。 Call指令会自动地将下一条指令压入程序栈中。 Ret指令自动将 call 压入的返回地址弹出放入 rip 寄存器中,从而达到函数的 return。

相关文章
|
10月前
|
安全 算法 网络协议
解析:HTTPS通过SSL/TLS证书加密的原理与逻辑
HTTPS通过SSL/TLS证书加密,结合对称与非对称加密及数字证书验证实现安全通信。首先,服务器发送含公钥的数字证书,客户端验证其合法性后生成随机数并用公钥加密发送给服务器,双方据此生成相同的对称密钥。后续通信使用对称加密确保高效性和安全性。同时,数字证书验证服务器身份,防止中间人攻击;哈希算法和数字签名确保数据完整性,防止篡改。整个流程保障了身份认证、数据加密和完整性保护。
|
9月前
|
机器学习/深度学习 数据可视化 PyTorch
深入解析图神经网络注意力机制:数学原理与可视化实现
本文深入解析了图神经网络(GNNs)中自注意力机制的内部运作原理,通过可视化和数学推导揭示其工作机制。文章采用“位置-转移图”概念框架,并使用NumPy实现代码示例,逐步拆解自注意力层的计算过程。文中详细展示了从节点特征矩阵、邻接矩阵到生成注意力权重的具体步骤,并通过四个类(GAL1至GAL4)模拟了整个计算流程。最终,结合实际PyTorch Geometric库中的代码,对比分析了核心逻辑,为理解GNN自注意力机制提供了清晰的学习路径。
655 7
深入解析图神经网络注意力机制:数学原理与可视化实现
|
9月前
|
机器学习/深度学习 缓存 自然语言处理
深入解析Tiktokenizer:大语言模型中核心分词技术的原理与架构
Tiktokenizer 是一款现代分词工具,旨在高效、智能地将文本转换为机器可处理的离散单元(token)。它不仅超越了传统的空格分割和正则表达式匹配方法,还结合了上下文感知能力,适应复杂语言结构。Tiktokenizer 的核心特性包括自适应 token 分割、高效编码能力和出色的可扩展性,使其适用于从聊天机器人到大规模文本分析等多种应用场景。通过模块化设计,Tiktokenizer 确保了代码的可重用性和维护性,并在分词精度、处理效率和灵活性方面表现出色。此外,它支持多语言处理、表情符号识别和领域特定文本处理,能够应对各种复杂的文本输入需求。
1158 6
深入解析Tiktokenizer:大语言模型中核心分词技术的原理与架构
|
9月前
|
传感器 人工智能 监控
反向寻车系统怎么做?基本原理与系统组成解析
本文通过反向寻车系统的核心组成部分与技术分析,阐述反向寻车系统的工作原理,适用于适用于商场停车场、医院停车场及火车站停车场等。如需获取智慧停车场反向寻车技术方案前往文章最下方获取,如有项目合作及技术交流欢迎私信作者。
707 2
|
9月前
|
Java 关系型数据库 数据库连接
Javaweb之Mybatis入门程序的详细解析
本文详细介绍了一个MyBatis入门程序的创建过程,从环境准备、Maven项目创建、MyBatis配置、实体类和Mapper接口的定义,到工具类和测试类的编写。通过这个示例,读者可以了解MyBatis的基本使用方法,并在实际项目中应用这些知识。
227 11
|
9月前
|
负载均衡 JavaScript 前端开发
分片上传技术全解析:原理、优势与应用(含简单实现源码)
分片上传通过将大文件分割成多个小的片段或块,然后并行或顺序地上传这些片段,从而提高上传效率和可靠性,特别适用于大文件的上传场景,尤其是在网络环境不佳时,分片上传能有效提高上传体验。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
364 2
|
9月前
|
算法 测试技术 C语言
深入理解HTTP/2:nghttp2库源码解析及客户端实现示例
通过解析nghttp2库的源码和实现一个简单的HTTP/2客户端示例,本文详细介绍了HTTP/2的关键特性和nghttp2的核心实现。了解这些内容可以帮助开发者更好地理解HTTP/2协议,提高Web应用的性能和用户体验。对于实际开发中的应用,可以根据需要进一步优化和扩展代码,以满足具体需求。
903 29
|
9月前
|
前端开发 数据安全/隐私保护 CDN
二次元聚合短视频解析去水印系统源码
二次元聚合短视频解析去水印系统源码
381 4
|
9月前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~

推荐镜像

更多
  • DNS