栈帧、ELF

news2024/11/14 20:30:29

前言

大家好我是jiantaoyab,这是我所总结作为学习的笔记第四篇,在这里分享给大家,还有一些书籍《深入理解计算机系统》《计算机组成:结构化方法》《计算机体系结构:量化研究方法》《程序员的自我修养》,今天我们来了解程序栈

为什么我们需要程序栈?

通过加入了程序栈,我们相当于在指令跳转的过程种,加入了一个“记忆”的功能,能在跳转去运行新的指令之后,再回到跳出去的位置,能够实现更加丰富和灵活的指令执行流程

Linux下

#include <stdio.h>
int static add(int a, int b)
{
    return a+b;
}
  
int main()
{
    int x = 1;
    int y = 2;
    int u = add(x, y);
}

执行

 gcc -g -c test.c
 objdump -d -M intel -S test.o   

结果

#include <stdio.h>
int static add(int a, int b)
{
   0:	55                   	push   rbp
   1:	48 89 e5             	mov    rbp,rsp
   4:	89 7d fc             	mov    DWORD PTR [rbp-0x4],edi
   7:	89 75 f8             	mov    DWORD PTR [rbp-0x8],esi
    return a+b;
   a:	8b 45 f8             	mov    eax,DWORD PTR [rbp-0x8]
   d:	8b 55 fc             	mov    edx,DWORD PTR [rbp-0x4]
  10:	01 d0                	add    eax,edx
}
  12:	5d                   	pop    rbp
  13:	c3                   	ret    
0000000000000014 <main>:
 
int main()
{
  14:	55                   	push   rbp
  15:	48 89 e5             	mov    rbp,rsp
  18:	48 83 ec 10          	sub    rsp,0x10
    int x = 1;
  1c:	c7 45 fc 01 00 00 00 	mov    DWORD PTR [rbp-0x4],0x1
    int y = 2;
  23:	c7 45 f8 02 00 00 00 	mov    DWORD PTR [rbp-0x8],0x2
    int u = add(x, y);
  2a:	8b 55 f8             	mov    edx,DWORD PTR [rbp-0x8]
  2d:	8b 45 fc             	mov    eax,DWORD PTR [rbp-0x4]
  30:	89 d6                	mov    esi,edx
  32:	89 c7                	mov    edi,eax
  34:	e8 c7 ff ff ff       	call   0 <add>
  39:	89 45 f4             	mov    DWORD PTR [rbp-0xc],eax
  3c:	c9                   	leave  
  3d:	c3                   	ret   
}

可以看出来,在这段代码里,main 函数和上一篇我们写的程序执行区别并不大,它主要是把 jump 指令换成了函数调用的 call 指令。call 指令后面跟着的,仍然是跳转后的程序地址。

我们来看 add 函数。可以看到,add 函数编译之后,代码先执行了一条 push 指令和一条 mov 指令;在函数执行结束的时候,又执行了一条 pop 和一条 ret 指令。这四条指令的执行,其实就是在进行我们接下来要讲压栈(Push)和出栈(Pop)操作。

函数调用和上一篇我们讲的 if…else 和 for/while 循环有点像。它们两个都是在原来顺序执行的指令过程里,执行了一个内存地址的跳转指令,让指令从原来顺序执行的过程里跳开,从新的跳转后的位置开始执行

但是,这两个跳转有个区别,if…else 和 for/while 的跳转,是跳转走了就不再回来了,就在跳转后的新地址开始顺序地执行指令,而函数调用的跳转是在对应函数的指令执行完了之后,还要再回到函数调用的地方,继续执行 call 之后的指令

栈帧(Stack Frame)

可以把调用的函数指令,直接插入在调用函数的地方,替换掉对应的 call 指令,然后在编译器编译代码的时候,直接就把函数调用变成对应的指令替换掉

但是如果函数 A 调用了函数 B,然后函数 B 再调用函数 A,我们就得面临在 A 里面插入 B 的指令,然后在 B 里面插入 A 的指令,这样就会产生无穷无尽地替换,这个做法是行不通的。

那假如我们把后面要跳回来指令的地址给记录下来呢?

专门设立一个“程序调用寄存器”,来存储接下来要跳转回来执行的指令地址。等到函数调用结束,从这个寄存器里取出地址,再跳转到这个记录的地址,继续执行就好了

但是在多层函数调用里,简单只记录一个地址也是不够的。我们在调用函数 A 之后,A 还可以调用函数 B,B 还能调用函数 C。这一层又一层的调用并没有数量上的限制。在所有函数调用返回之前,每一次调用的返回地址都要记录下来,但是我们 CPU 里的寄存器数量并不满足这个条件

最终,我们引入了栈这个数据结构,用兵乓球举个例子,栈就像一个乒乓球桶,每次程序调用函数之前,我们都把调用返回后的地址写在一个乒乓球上,然后塞进这个球桶。这个操作其实就是我们常说的压栈。如果函数执行完了,我们就从球桶里取出最上面的那个乒乓球,很显然,这就是出栈

拿到出栈的乒乓球,找到上面的地址,把程序跳转过去,就返回到了函数调用后的下一条指令了。如果函数 A 在执行完成之前又调用了函数 B,那么想要A执行完的结果,我们需要往球桶里塞一个乒乓球B。而我们从球桶最上面拿乒乓球的时候,拿的也一定是最近一次的球B,也就是最下面一层的函数调用完成后的地址。乒乓球桶的底部,就是栈底,最上面的乒乓球所在的位置,就是栈顶

在这里插入图片描述

在真实的程序里,压栈的不只有函数调用完成后的返回地址。比如函数 A 在调用 B 的时候,需要传输一些参数数据,这些参数数据在寄存器不够用的时候也会被压入栈中。整个函数 A 所占用的所有内存空间,就是函数 A 的栈帧(Stack Frame)

底在最上面,顶在最下面,这样的布局是因为栈底的内存地址是在一开始就固定的。而一层层压栈之后,栈顶的内存地址是在逐渐变小而不是变大。

汇编分析

在这里插入图片描述

对应上面函数 add 的汇编代码,我们来仔细看看,main 函数调用 add 函数时,add 函数入口在 0~1 行,add 函数结束之后在 12~13 行。

我们在调用第 34 行的 call 指令时,会把当前的 PC 寄存器里的下一条指令的地址压栈,保留函数调用结束后要执行的指令地址。而 add 函数的第 0 行,push rbp 这个指令,就是在进行压栈。这里的 rbp 又叫栈帧指针(Frame Pointer),是一个存放了当前栈帧位置的寄存器。push rbp 就把之前调用函数,也就是 main 函数的栈帧的栈底地址,压到栈顶

接着,第 1 行的一条命令 mov rbp, rsp 里,则是把 rsp 这个栈指针(Stack Pointer)的值复制到 rbp 里,而 rsp 始终会指向栈顶。这个命令意味着,rbp 这个栈帧指针指向的地址,变成当前最新的栈顶,也就是 add 函数的栈帧的栈底地址了

而在函数 add 执行完成之后,又会分别调用第 12 行的 pop rbp 来将当前的栈顶出栈,这部分操作维护好了我们整个栈帧。然后,我们可以调用第 13 行的 ret 指令,这时候同时要把 call 调用的时候压入的 PC 寄存器里的下一条指令出栈,更新到 PC 寄存器中,将程序的控制权返回到出栈后的栈顶。

stack overflow

无论有多少层的函数调用,或者在函数 A 里调用函数 B,再在函数 B 里调用 A,这样的递归调用,我们都只需要通过维持 rbp 和rsp,这两个维护栈顶所在地址的寄存器,就能管理好不同函数之间的跳转。不过,栈的大小也是有限的。如果函数调用层数太多,我们往栈里压入它存不下的内容,程序在执行的过程中就会遇到栈溢出的错误,这就是大名鼎鼎的“stack overflow”。

函数内联(Inline)

在定义函数的地方,加上 inline 的关键字,来提示编译器对函数进行内联内联带来的优化是,CPU
需要执行的指令数变少了,根据地址跳转的过程不需要了,压栈和出栈的过程也不用了

不过内联并不是没有代价,内联意味着,我们把可以复用的程序指令在调用它的地方完全展开了。如果一个函数在很多地方都被调用了,那么就会展开很多次,整个程序占用的空间就会变大了。

linux 的ELF

在linux下,如果将一个.o文件通过 chmod 命令赋予文件可执行的权限,运行的时候仍然只会得到一条 cannot execute binary file: Exec format error 的错误

这是为什么呢

.o文件并不是一个可以执行的文件,而是目标文件,只有通过链接器把多个目标文件以及调用的各种函数库链接起来,我们才能得到一个可执行文件

实际上,“C 语言代码 - 汇编代码 - 机器码” 这个过程,在我们的计算机上进行的时候是由两部分组成的。

  1. 由编译(Compile)、汇编(Assemble)以及链接(Link)三个阶段组成。在这三个阶段完成之后,我们就生成了一个可执行文件
  2. 我们通过装载器(Loader)把可执行文件装载(Load)到内存中。CPU 从内存中读取指令和数据,来开始真正执行程序。

在这里插入图片描述

ELF 格式和链接

程序最终是通过装载器变成指令和数据的,所以其实我们生成的可执行代码也并不仅仅是一条条的指令,在 Linux 下,可执行文件和目标文件所使用的都是一种叫ELF(Execuatable and Linkable File Format)的文件格式,中文名字叫可执行与可链接文件格式,这里面不仅存放了编译成的汇编指令,还保留了很多别的数据

比如我上几篇所有 objdump 出来的代码里,你都可以看到对应的函数名称,像 add、main 等等,乃至你自己定义的全局可以访问的变量名称,都存放在这个 ELF 格式文件里。这些名字和它们对应的地址,在 ELF 文件里面,存储在一个叫作符号表(Symbols Table)的位置里。符号表相当于一个地址簿,把名字和地址关联了起来。

在这里插入图片描述

ELF 文件格式把各种信息,分成一个一个的 Section 保存起来。ELF 有一个基本的文件头(File Header),用来表示这个文件的基本属性,比如是否是可执行文件,对应的 CPU、操作系统等等。除了这些基本属性之外,大部分程序还有这么一些 Section:

  1. .text Section,也叫作代码段或者指令段(Code Section),用来保存程序的代码和指令;
  2. .data Section,也叫作数据段(Data Section),用来保存程序里面设置好的初始化数据信息;
  3. .rel.text Secion,叫作重定位表(Relocation Table)。重定位表里,保留的是当前的文件里面,哪些跳转地址其实是我们不知道的。在链接发生之前,我们并不知道该跳转到哪里,这些信息就会存储在重定位表里;
  4. .symtab Section,叫作符号表(Symbol Table)。符号表保留了我们所说的当前文件里面定义的函数名称和对应地址的地址簿。

链接器会扫描所有输入的目标文件,然后把所有符号表里的信息收集起来,构成一个全局的符号表。然后再根据重定位表,把所有不确定要跳转地址的代码,根据符号表里面存储的地址,进行一次修正。最后,把所有的目标文件的对应段进行一次合并,变成了最终的可执行代码。这也是为什么,可执行文件里面的函数调用的地址都是正确的

在链接器把程序变成可执行文件之后,要装载器去执行程序就容易多了。装载器不再需要考虑地址跳转的问题,只需要解析 ELF 文件,把对应的指令和数据,加载到内存里面供 CPU 执行就可以了

linux的程序在window下不能跑

是因为两个操作系统下可执行文件的格式不一样,Linux 下是 ELF 文件格式,而 Windows
的可执行文件格式是一种叫作PE(Portable Executable Format)的文件格式。Linux 下的装载器只能解析
ELF 格式而不能解析 PE 格式。

如果我们有一个可以能够解析 PE 格式的装载器,我们就有可能在 Linux 下运行 Windows
程序了。这样的程序真的存在吗?没错,Linux 下著名的开源项目 Wine,就是通过兼容 PE 格式的装载器,使得我们能直接在 Linux
下运行 Windows 程序的。而现在微软的 Windows 里面也提供了 WSL,也就是 Windows Subsystem for
Linux,可以解析和加载 ELF 格式的文件。 E 格式。

如果我们有一个可以能够解析 PE 格式的装载器,我们就有可能在 Linux 下运行 Windows
程序了。这样的程序真的存在吗?没错,Linux 下著名的开源项目 Wine,就是通过兼容 PE 格式的装载器,使得我们能直接在 Linux
下运行 Windows 程序的。而现在微软的 Windows 里面也提供了 WSL,也就是 Windows Subsystem for
Linux,可以解析和加载 ELF 格式的文件。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1490388.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux下du命令和df命令的使用

du命令作用是估计文件系统的磁盘已使用量&#xff0c;常用于查看文件或目录所占磁盘容量。df命令是统计磁盘使用情况&#xff0c;可以用来查看磁盘已被使用多少空间和还剩余多少空间。du命令语法du [选项] [文件或目录名称]参数&#xff1a;-a&#xff1a;--all&#xff0c; 列…

Ethersacn的交易数据是什么样的(2)

分析 Raw Transanction RLP&#xff08;Recursive Length Prefix&#xff09;是一种以太坊中用于序列化数据的编码方式。它被用于将各种数据结构转换为二进制格式&#xff0c;以便在以太坊中传输和存储。RLP 是一种递归的编码方式&#xff0c;允许对复杂的数据结构进行编码。所…

5G工业智能网关保障煤矿安全生产

随着物联网技术发展与煤矿需求的持续激增&#xff0c;矿山矿井的分布范围广泛、户外环境恶劣等管理问题急需解决&#xff0c;而物联网网关工业级设计能够无惧恶劣环境干扰&#xff0c;轻松解决户外网络部署问题。 工业网关通过采集矿井内的各类传感器数据对矿井进行远程监控&a…

【图论】Dijkstra 算法求最短路 - 构建邻接矩阵(带权无向图)

文章目录 例题&#xff1a;到达目的地的方案数题目描述代码与解题思路构建带权无向图的邻接矩阵 例题&#xff1a;到达目的地的方案数 题目链接&#xff1a;1976. 到达目的地的方案数 题目描述 代码与解题思路 func countPaths(n int, roads [][]int) int {g : make([][]int…

使用API有效率地管理Dynadot域名,进行DNS域名解析

关于Dynadot Dynadot是通过ICANN认证的域名注册商&#xff0c;自2002年成立以来&#xff0c;服务于全球108个国家和地区的客户&#xff0c;为数以万计的客户提供简洁&#xff0c;优惠&#xff0c;安全的域名注册以及管理服务。 Dynadot平台操作教程索引&#xff08;包括域名邮…

文件多线程读取下载+异步上传云存储

文章目录 1. 前言2. 多线程读取下载异步上传2-1. 多线程读取下载2-2. 异步上传异步更新cosUrl 3. 线程池单个任务提交4. 关于异步上传的文件oosUrl地址返回问题 1. 前言 实际开发中经常遇到&#xff0c;文件上传或者文件批量下载的任务。单线程下载大批量文件&#xff0c;排队处…

灰狼算法优化elman神经网络回归分析,GWO-ELMAN回归分析

目录 背影 ELMAN神经网络的原理 ELMAN神经网络的定义 受限玻尔兹曼机(RBM) 灰狼算法原理 灰狼算法优化elman神经网络回归分析 基本结构 主要参数 数据 MATALB代码 结果图 展望 完整代码下载:灰狼算法优化ELMAN回归分析,GWO-ELMAN回归分析资源-CSDN文库 https://download.c…

Electron 多显示器渲染

Electron打出的包&#xff0c;如果当前有俩个显示器&#xff0c;则可以配置当前显示倒哪个显示器上&#xff0c;或者可以配置不同的显示器&#xff0c;启动不同的项目&#xff0c;只在Windows和Linux下测试过&#xff0c;Mac没有真机&#xff0c;可以利用docker安装MacOS环境&a…

苹果电脑免费释放磁盘空间软件CleanMyMac X2024

CleanMyMac X通过以下方式帮助用户释放磁盘空间&#xff1a; 智能扫描和清理&#xff1a;CleanMyMac X拥有强大的智能扫描功能&#xff0c;可以深入系统底层&#xff0c;快速识别并清理各类无用文件和垃圾&#xff0c;如缓存、日志、临时文件等。这些文件通常会占用大量的磁盘…

『运维备忘录』之 RegEx 正则表达式实例汇总

运维人员不仅要熟悉操作系统、服务器、网络等知识&#xff0c;甚至对于开发相关的也要有所了解。很多运维工作者可能一时半会记不住那么多命令、代码、方法、原理或者用法等等。这里我将结合自身工作&#xff0c;持续给大家更新运维工作所需要接触到的知识点&#xff0c;希望大…

单片机入门:LED数码管

LED数码管 LED数码管&#xff1a;由多个发光二极管封装在一起组成的“8”字型的器件。如下图所示&#xff1a; 数码管引脚定义 一位数码管 内部由八个LED组成。器件有十个引脚。 对于数码管内的8个LED有共阴和共阳两种连接方法。 共阴&#xff1a;将8个LED的阴极都连接到一…

羊大师揭秘羊奶,古今皆宜的滋养佳品

羊大师揭秘羊奶&#xff0c;古今皆宜的滋养佳品 羊奶&#xff0c;这一被誉为“奶中之王”的天然饮品&#xff0c;自古以来就因其独特的营养价值和健康益处而备受推崇。在古代&#xff0c;羊奶就已经被用作滋补身体和增强免疫力的饮品。随着现代营养学研究的深入&#xff0c;羊…

聊聊最近成交的一个小外贸订单

聊聊最近的一个小订单的故事吧&#xff0c;这个客户是个新手买家&#xff0c;也属于第一次在网上购物&#xff0c;客户在年前开始询问产品&#xff0c;而且当时正好是假期。 其实按照正常的处理思路来说&#xff0c;应该告诉客户现在是假期&#xff0c;大概是在什么时候恢复工…

JavaWeb Request:获取请求数据

Request是请求对象&#xff0c;Response是响应对象。 浏览器会发送HTTP请求到后台服务器[Tomcat]&#xff0c;请求中会包含很多请求数据 [请求行请求头请求体] &#xff0c;后台服务器[Tomcat]会对HTTP请求中的数据进行解析并把解析结果存入到Request对象&#xff0c;可以从Req…

STM32 | STM32时钟分析、GPIO分析、寄存器地址查找、LED灯开发(第二天)

STM32 第二天 一、 STM32时钟分析 寄存器&#xff1a;寄存器的功能是存储二进制代码&#xff0c;它是由具有存储功能的触发器组合起来构成的。一个触发器可以存储1位二进制代码&#xff0c;故存放n位二进制代码的寄存器&#xff0c;需用n个触发器来构成 在计算机领域&#x…

面试经典150题【51-60】

文章目录 面试经典150题【51-60】71.简化路径155.最小栈150.逆波兰表达式求值224.基本计算器141.环形链表2.两数相加21.合并两个有序链表138.随机链表的复制19.删除链表的倒数第N个节点82.删除链表中的重复元素II 面试经典150题【51-60】 71.简化路径 先用split(“/”)分开。然…

HTML极速入门

HTML基础 什么是HTML HTML(Hyper Text Markup Language),超文本标记语言. 超文本:比文本更强大.通过链接和交互式方式来组织和呈现信息的文本形式.不仅仅有文本,还可能包括图片,音频,或者自己经审阅过它的学者所加的评注,补充或脚注等. 标记语言:由标签构成的语言 HTML的标…

【JavaEE进阶】部署Web项目到Linux服务器

文章目录 &#x1f343;前言&#x1f340;什么是部署&#x1f332;环境配置&#x1f6a9;数据准备&#x1f6a9;程序配置⽂件修改 &#x1f384;构建项目并打包&#x1f38b;上传Jar包到服务器,并运行&#x1f6a9;上传Jar包&#x1f6a9;运行程序&#x1f6a9;开放端口号 &…

Material UI 5 学习02-其它按钮组件

Material UI 5 学习02-其它按钮组件 一、IconButton按钮二、 ButtonGroup按钮组1、最基本的实例2、垂直按钮组 一、IconButton按钮 图标按钮通常适用于切换按钮&#xff0c;允许选择或选择单个选项 取消选择&#xff0c;例如在项目中添加或删除星号。 <IconButton aria-lab…

Java多线程卖包子问题(附多种实现方式)

前提条件&#xff1a;生产者生产包子&#xff0c;但是包子总数不得超过10个&#xff0c;消费者有包子就购买 第一种&#xff1a;通过wait()和notify()来实现 在这个实例中&#xff0c;生产者和消费者通过wait()和notify()来进行通讯&#xff0c;再通过synchronized 块来确保线…