C++语法|进程虚拟地址空间和函数调用栈

本文来自施磊老师的课程,老师讲的非常不错,我的笔记也是囫囵吞枣全部记下,但是我在这里推荐一本书,真的真的建议初学C++或者想要进阶C++的同学们看看:《CPU眼里的C/C++》

文章目录

  • 进程的虚拟地址空间和布局
    • 进程虚拟地址空间
      • 1 不可访问区域
      • 2 .text代码段和.rodata只读数据段
      • 3 .data数据段和.bss数据段
      • 4 .heap段
      • 5 *.dll *so库
      • 6 stack段
      • 7 命令行参数和环境变量
      • 8 内核空间
    • 代码分析
  • 重点问题
    • 为什么局部变量一会儿说在栈上,一会儿又是在 .text段
    • 每一个进程的用户空间是私有的,但是内核空间是共享的!!!
  • 函数调用栈
    • 代码运行过程
      • int a = 10;
      • int b = 20
      • int ret = sum(a, b)
      • 回到main函数
    • 请回答本节开头的两个问题

进程的虚拟地址空间和布局

任何的编程语言,无非产生两种东西:指令和数据。
在任何操作系统,程序编译链接完成后,会生成可执行文件,并且该文件会存储在磁盘当中,在我们执行该文件,它就会加载到内存中。那么我们就有一个疑问:内存到底有没有区域的划分,划分之后又是什么样子呢?

不过就算我们加载到内存,也不可能加载到物理内存!!!

加载到内存中,首先linux系统(x86 32位)会给当前进程分配一个 2 3 2 2^32 232(4G)大小到一块空间。

需要注意的是,比较常见的画法是,低地址在下,高地址在上

这个空间叫做进程的虚拟地址空间,其实虚拟地址空间的本质不过是内核创建的一系列数据结构。

NOTE:
它存在,你能看见,它是物理的
它存在,你不能看见,它是透明的
它不存在,你能看见,它是虚拟的
它不存在,你也看不见,它被删除了

进程虚拟地址空间

该空间被默认分为两部分,一部分从0x00000000~0xC0000000一共3G到校被称为user space用户空间,剩下的空间为kernal space为1G。

每一个进程都有这么一个虚拟地址空间,在用户空间的划分情况又是什么样的呢?

1 不可访问区域

它并没有从零地址开始存储,而是从`0x08048000`开始存储,所以最顶部的空间是不能够访问的。有些情况下如果我们访问控制真: ```cpp char *p = nullptr; strlen(p); char *src = nullptr; strcpy(dest, src); ``` 这些都是零地址,其实就是在我们的`0x00000000`~`0x08048000`这部分地址不允许访问,不能读也不能写。如果访问的话程序会崩溃,系统要报异常(通过信号)。

2 .text代码段和.rodata只读数据段

0x08048000开始,首先是.text如果有人问指令在运行的时候放在哪块区域,我们不要说全局变量区或者静态区,直接说代码段或者.text段即可****。这一部分通常还有一块区域叫做.rodata叫做只读数据段放的是什么呢?

//在函数中定义一个局部变量指针
char *p = "hello world"; //会报warning
//只能写成
const chart *p = "hello world";

对于本例来说,p就在栈上,p指向的那个常量字符串就在.rodata段,那么如果我们想修改这个指针*p = 'a',这样编译是没有问题的,但是如果运行这个程序会直接挂掉,因为.rodata.text段落只能读不能写。其实在C++较新的编译器中,是不能使用普通指针指向常量字符串的(会报warning)。如果我们使用const修饰,所以就不会发生*p='a'这样不可预期的错误了。

3 .data数据段和.bss数据段

这两个段落都叫数据段,那么这两个有什么不同呢?

.data只存放初始化过的并且初始化数据不为0的

.bss存放未初始化的以及初始化为0的,程序运行的时候会把该段数据全部初始化为0。

那么,当我们全局作用域中,写一个全局变量但是没有初始化,当我们去打印它的值会发现它是一个0,程序运行的时候我们内核给当前进程分配地址空间,我们程序未初始化的数据放在.bss,我们的内核也就是操作系统会自己负责把.bss段的数据全部置为0,这就是为什么未初始化的全局变量是0。

#include <iostream>
using namespace std;
int gdata;
int main() {   
    cout << gdata << endl; //被内核初始化为0
    return 0;
}

4 .heap段

.bss段落再往下,暂时还没有,但是我们先把它画出来,这块空间就叫做堆heap!.heap只有在我们调用了newmallocalloc才被分配空间。

5 *.dll *so库

堆内存再往下就是我们当前程序在运行过程中会加载一些共享库,也就是我们的动态链接库,windows下是*.dll,linxu下是*.so库。再一个需要注意的是,这里也是我们堆栈共享区,栈会向低地址生长,堆则会向高地址生长。
比较常见的画法应该是下面是低地址,上面是高地址

6 stack段

现在就到我们的栈空间!程序运行每一个线程都独有的stack栈空间!栈空间跟其他地方不一样的是,栈空间是从下往上进行增长,堆被分配时是从低地址到高地址的增长。

7 命令行参数和环境变量

在这里存储命令行参数和环境变量的路径


8 内核空间

此处为内核空间,内核空间是进程共享的!!!!
以上就是我们用户空间内存划分的布局,在内核空间主要分为了ZONE_DMA和ZONE_NORMAL还有ZONE_HIGHMEM这三块地区。大概分别为16M、800M、剩下的就是我们的ZONE_HIGHMEM
在ZONE_NORMAL一般是放PCB块,以及内核空间的线程和内核空间运行的函数所在的栈空间都在这一部分。
最后ZONE_HIGHMEM是高端内存,它是映射我们高地址的物理内存的时候做地址映射用的。

代码分析

int gdata1 = 10;
int gdata1 = 0;
int gdata3;

static int gdata1 = 10;
static int gdata1 = 0;
static int gdata3;

int main() {    
    int a = 12;
    int b = 0;
    int c;

    static int e = 13;
    static int f = 0;
    static int g;
    return 0;
}

gdata1gdata4被初始化并且初始值不为零,被放在.data段。

gdata2gdata3gdata5gdata6未初始化或初始值为0,被放在.bss段。

至于abc他们并不产生符号,而是产生指令,比如说int a = 12;在x86指令集中为mov dword ptr[a], 0Ch。所以他们三个局部变量最终产生的是指令,被放在.text段。

然后关于efg这三个为静态局部变量,也是放在数据段(.data或.bss)的,但程序运行的时候是不会初始化的,只有第一次运行到他们才会进行初始化,分别放在数据段的.data.bss.bss

如果我们有如下操作:

//打印c
cout << c << endl;
//打印g
cout << g << endl;

打印c肯定不为0!因为它是栈上的无效值,但是如果打印g,肯定是0!因为他在.bss段。


综上所述,我们的红色部分都存储在.text部分,因为他们都会产生指令

但是我们一定要问自己一个问题,我们a、b、c已知那些数据都是放在栈上面的,有为什么说他们产生了指令呢

重点问题

为什么局部变量一会儿说在栈上,一会儿又是在 .text段

a, b, c编译后产生的指令是要放到.text段的,但是这个函数运行的时候,系统会在栈上面给该函数开辟一个栈帧,指令mov dword ptr[a], 0Ch就是把12放在a这块内存的4字节内存中,所以指令运行的时候会在栈空间上划分一块4字节的空间来存放12。也就是说a这个语句生成的时机是在函数运行时的,我们执行可执行文件后,先加载它的指令放在.text段,然后等到这条指令运行时,才会在栈空间开辟一个4字节的空间。

每一个进程的用户空间是私有的,但是内核空间是共享的!!!

如果我创建多个进程,QQ、酷狗音乐、VS。各自都有各自的用户空间,但是内核空间是共享的。

进程跟进程之间通信比较难的原因就是因为他们的用户空间是隔离的,谁也访问不到谁,但是内核空间是共享的。所以说进程之间的通信方式有哪些??

这样我们很容易理解了,进程间通信其实就是在内核空间划分了一块儿内存,这样一来进程1往内核共享的这块内存中写数据,进程2、3就都能看的见。
匿名管道通信

本模块推荐书籍:
《深入理解计算机系统》 尤其第七章 链接
《程序员的自我修养》尤其是

函数调用栈

给定一下代码:

int sum (int a, int b) {
    int tmp = 0;
    tmp = a + b;
    return tmp;
}
int main() {
    int a = 10;
    int b = 20;
    
    int ret = sum(a, b);
    cout << "ret: " << ret << endl;    
    return 0;
}

问两个问题:

  • 问题一:main函数调用sum,sum执行完之后,怎么知道回到哪个函数中
  • 问题二:sum函数执行完,回到main以后,怎么知道从哪一行指令继续运行

接下来我们会以此为例,讲解函数调用栈的使用过程

代码运行过程

函数运行时,要在栈帧上开辟空间。描述一个栈结构有栈顶和栈底就可以了,所以在这里我们给这个main函数的栈帧表示出来。

(1)ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
(2)EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。

在这里,esp存储的是main函数栈帧栈顶的地址,所以说esp是可变的,随着栈的生产而逐渐变小。
ebp存放的是min函数栈帧栈底的地址,假如说栈底地址是0x0018ff40,ebp。
栈底是高地址,栈顶是低地址。

int a = 10;

汇编指令:mov dword ptr[a], 0Ah(真正的汇编指令是mov dword ptr[ebp-4], 0Ah,这里为了方便理解直接用了a)

我们来看,这个main函数的第一行代码是int a = 10;,执行的时候大家都知道a不产生符号,如果是汇编指令的话就是mov dword ptr[ebp-4], 0Ah,a是我们函数的第一个局部变量,所以他就出现在栈底,那为什么要用ebp减4呢,因为ebp是高地址,往上是低地址。
操作系统访问局部变量就是用栈底指针的偏移来访问

int b = 20

汇编指令:mov dword ptr[b], 14h (ptr[ebp-8])
图上画出来如图:

int ret = sum(a, b)

关于本条指令,ret是借助sum的返回值才完成初始化,所以我们先放到这里。

现在我们要开始调用函数了:一个函数的调用要先从右向左压参数,压栈往哪里压呢?往栈顶压!

  • 先压b,这块内存就是sum函数形参变量b的内存。所以形参内存开辟是由调用方函数来完成的。
  • 由于压栈操作push指令,所以esp也指向了栈顶。
  • 以上两个操作的汇编指令有两个,并且a也同理,所以一共四个汇编指令。完成a,b的压栈和相关指令如下:
mov eax, dword ptr[ebp-8]
push eax
mov eax, dword ptr[ebp-4]
push eax
  • 两个变量全部压完栈后,接下来就是函数调用指令call sum
    这个call指令会做两件事情,我们先展示call后面的汇编:
add esp, 8
move dword ptr[ebp-0Ch], eax

假设第一行指令的地址为08124458,call会把这个指令的地址入栈,因为我们后续等sum函数运行完,必须知道再继续运行哪一块代码。在这里我们就回答了上述的第二个问题
此时我们的内存情况如图:

  • 接下来我们要进入sum函数了
    其实我们需要首先执行我们的左括号,它对应三条指令,
push ebp
mov ebp, esp
sub esp, 4Ch

我们的push ebp会把ebp的地址压栈,还记得ebp是啥吗?没错,就是我们用来表示main函数的“栈帧”基地值,至此,main函数“栈帧”保护工作完成!这里也就回答了我们提出的第一个问题

紧接着mov ebp, esp,更新“栈帧”基准线,让他与栈顶平齐!现在他俩的地址相等了

再然后sub esp, 4Ch,也就是说我们的esp要往上走4Ch的空间,也就是给我们的sum函数开辟栈帧空间,主要是为了给我们的临时变量分配“栈”内存。

  • 接下来轮到我们sum函数中间的代码了,首先是int temp = 0;汇编指令如下:
mov dword ptr[ebp-4], 0

(这里的栈帧初始化只有windows的编译器才会做)

  • 接下来是temp = a + b,我们应该怎么取a和b呢?
    还记得之前我们的形参变量存到哪了吗?
    10=>int a 20=>int b这个位置。这里需要我们借助ebp来进行间接寻址。
mov eax, dword ptr[ebp+0Ch]
add ecx, dword ptr[ebp+8]		//这里计算a+b
mov dword ptr[ebp-4], eax		//把a+b的结果放到局部变量temp
  • 然后是return temp,注意temp是函数的局部变量,它是出不去的,temp是4个字节,返回他的时候不产生临时变量,而是直接通过eax寄存器带出去,所以汇编如下:
mov eax, dword ptr[ebp-4]
  • 最后到右括号了,我们先看汇编
mov esp, ebp
pop ebp
ret 0

第一行指令把ebp的值赋给esp,所以esp直接从上面跑到了sum函数栈帧的栈底,这里就是我们的回退栈空间

现在再看这段代码还安全吗?

int* func() {
	int data = 10;
	return &data;
}

我们的esp回退后,栈空间已经交还给系统了,这个地址返回之后还能用吗?肯定是不能,我们已经失去了对它的控制,成为了野指针。

第二行指令pop ebp,出栈,并把出栈元素的值赋给ebp,现在我们的栈顶放的是0x0018ff40,把它给ebp!我们的ebp又回到main函数栈帧的栈底了!

并且随着出栈,esp也往下走了,所以指向`0x08124458`

第三行指令ret ,也就是出栈操作,把出栈的内容放入CPU的PC寄存器(该寄存器存放下一行要执行的指令)中,我们现在出栈的是0x08124458,这个地址是什么还记得吗,就是我们的main函数中,call sum指令后面的add esp, 8这个指令的地址!

现在正式回到main函数调用完sum之后的指令位置了!

回到main函数

call sum
add esp, 8      //0x08124458
mov dword ptr[ebp-0Ch], eax

最后这两个指令就是完成ret的赋值操作。结束!

请回答本节开头的两个问题

看文本节后,能回答出这两个问题吗?

int sum (int a, int b) {
    int tmp = 0;
    tmp = a + b;
    return tmp;
}
int main() {
    int a = 10;
    int b = 20;
    
    int ret = sum(a, b);
    cout << "ret: " << ret << endl;    
    return 0;
}

回答两个问题:

  • 问题一:main函数调用sum,sum执行完之后,怎么知道回到哪个函数中
  • 问题二:sum函数执行完,回到main以后,怎么知道从哪一行指令继续运行

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

布隆过滤器和黑名单,解决Redis缓存穿透

目录 1.什么是布隆过滤器&#xff1f; 2.布隆过滤器的原理 3.空间计算 4.布隆过滤器的视线场景&#xff1a; 5.在Spring Boot中集成Redisson实现布隆过滤器 6、Redisson实现布隆过滤器 6.1导入依赖 6.2使用 布隆过滤器&#xff08;Bloom Filter&#xff09;是1970年由布…

邮件大附件系统如何进行安全、高效的大附件发送?

邮件大附件系统是一套解决传统电子邮件系统&#xff0c;在发送大文件时遇到限制的解决方案。由于传统电子邮件系统通常对附件大小有限制&#xff0c;这使得发送大文件变得困难。邮件大附件系统通过各种技术手段&#xff0c;允许用户发送超过传统限制的大文件&#xff0c;通常在…

修改latex中block中公式与block标题间隔过大的问题

修改block中公式与block间隔过大的问题 如图的block中公式出现了空白:代码见下方 \begin{proof}[证明]\begin{align*}&Z\alpha \beta _XX\beta _YY\varepsilon \rightarrow XZ\alpha X\beta _XX^2\beta _YXY\varepsilon X&\\&E\left( Z \right) \alpha \beta _XE\…

【Java】Java中栈溢出的常见情况及解决方法

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 目录 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌…

同创优配正规炒股A股三大指数集体收涨 创指重回1900点关口

查查配5月9日电 周四,A股三大指数震荡上扬。截至收盘,上证指数涨0.83%,报3154.32点;深证成指涨1.55%,报9788.07点;创业板指涨1.87%,报1900.01点。总体上个股涨多跌少,全市场超4200只个股上涨。沪深两市今日成交额9011亿元,较上个交易日放量367亿元。 同创优配是AAA 级诚信经营…

Spring JdbcTemplate实现自定义动态sql拼接功能

需求描述&#xff1a; sql 需要能满足支持动态拼接&#xff0c;包含 查询字段、查询表、关联表、查询条件、关联表的查询条件、排序、分组、去重等 实现步骤&#xff1a; 1&#xff0c;创建表及导入测试数据 CREATE TABLE YES_DEV.T11 (ID BINARY_BIGINT NOT NULL,NAME VARCH…

栈结构(c语言)

1.栈的概念 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶&#xff0c;另一端称为栈底。栈中的数据元素遵守后进先出LIFO&#xff08;Last In First Out&#xff09;的原则。 压栈&am…

【c++线程】condition_variable的简单使用

尝试用两个线程交替打印1-100的数字&#xff0c;要求一个线程打印奇数&#xff0c;另一个线程打印偶数&#xff0c;并且打印数字从小到大依次递增。 #include <iostream> using namespace std; #include <thread> #include <mutex> #include <condition_…

前端技术交流群

欢迎来到前端筱园用户交流&#xff01;这是一个专注于前端编程技术、学习资源和行业动态的讨论平台。在这里&#xff0c;你可以分享经验、提问、回答问题&#xff0c;与其他前端开发者一起学习和成长。 &#x1f31f;亲爱的朋友们&#x1f31f; 大家好&#xff01;感谢你们一直…

SSC369G 双4K高性价比AI IPC方案

一、方案描述 SSC369G 双4K高性价比AI IPC方案采用主芯片SSC369G&#xff0c;内核为CA55四核最高主频为1.5Ghz处理器。SOC内置集成一个64位的四核RISC处理器&#xff0c;先进的图像信号处理器&#xff08;ISP&#xff09;&#xff0c;高性能的H.265/H.264/MJPEG视频编解码器&a…

Open CASCADE学习|BRepFill_Edge3DLaw

BRepFill_Edge3DLaw类继承自BRepFill_LocationLaw&#xff0c;用于在3D空间中定义边缘的几何法则。 下面是对代码中关键部分的解释&#xff1a; 文件头部&#xff1a;包含了版权信息&#xff0c;指出这个文件是OCCT软件库的一部分&#xff0c;并且根据GNU Lesser General Publi…

驾驶证OCR识别接口如何对接

驾驶证OCR识别接口也叫驾驶证文字识别OCR接口&#xff0c;指的是传入驾驶证照片&#xff0c;精准识别静态驾驶证图像上的文字信息。那么驾驶证OCR文字识别接口如何对接呢&#xff1f; 首先我们找到一家有驾驶证OCR识别接口的服务商&#xff0c;数脉API,然后注册账户&#xff0…

WPF容器控件之dockpanel、布局控件

dockpanel 容器控件&#xff0c;对其子元素进行或者水平垂直排布&#xff0c;也可以叫停靠面板,也可以让子元素停靠到容器某一个边上&#xff0c;拉伸元素拾起充满全部的高度或者宽度&#xff0c;也可以使最后一个子元素是否铺满剩余的空间。 参数 LastChildFill最后一个子元素…

人工智能应用正在改变我们的生活

在这个AI蓬勃发展的时代&#xff0c;你如何使用人工智能&#xff1f;如果您认为还没有&#xff0c;请再想一想。人工智能已经为我们的许多日常活动提供了动力&#xff0c;尽管您可能还没有有意将其用作工具&#xff0c;但这种情况可能会在不久的将来发生变化。随着顶尖科技公司…

政务服务电子文件归档和电子档案管理系统,帮助组织收、管、存、用一体化

作为数字政府建设的重要抓手&#xff0c;政务服务改革经过多年发展&#xff0c;截至 2022 年底&#xff0c;全国一体化在线政务服务平台实名用户超过10亿人&#xff0c;在政务服务、办件过程中出现了大量需要归档的电子文件&#xff0c;对于电子档案、电子证照的需求愈加强烈。…

如何高效解决渠道问题

品牌渠道会围绕销售做一系列活动&#xff0c;定价也会影响渠道的发展&#xff0c;同样的维护好价格&#xff0c;对渠道来说同样重要&#xff0c;渠道中常见的问题包含低价、窜货等&#xff0c;当低价问题不及时解决&#xff0c;会波及影响更多链接&#xff0c;使其他店铺为了流…

数据可视化训练第二天(对比Python与numpy中的ndarray的效率并且可视化表示)

绪论 千里之行始于足下&#xff1b;继续坚持 1.对比Python和numpy的性能 使用魔法指令%timeit进行对比 需求&#xff1a; 实现两个数组的加法数组 A 是 0 到 N-1 数字的平方数组 B 是 0 到 N-1 数字的立方 import numpy as np def numpy_sum(text_num):"""…

环保用电解决方案--企业污染治理设施用电监管系统/分表计电

★环保解决方案 通过对污染防治设施用电实时监控&#xff0c;实现对企业生产运行无死角、全流程、差别化、精细化管理&#xff0c;达到变人防为信息化技防&#xff0c;从事后处罚到介入式执法&#xff0c;彻底扭转传统依靠人力、经验及部分排污在线数据进行现场核查的状态&…

python使用opencv对图像的基本操作(4)

19.调整图片强度 19.1.调整强度 import numpy as np from skimage import exposure img np.array([51, 102, 153], dtypenp.uint8) matexposure.rescale_intensity(img) print(mat)注&#xff1a;skimage.exposure.rescale_intensity函数来调整img数组的亮度范围。这个函数会…

Unreal Engine(虚幻引擎)的版本特点

Unreal Engine&#xff08;虚幻引擎&#xff09;是Epic Games开发的游戏引擎&#xff0c;广泛应用于游戏开发、影视制作、建筑设计、虚拟现实等领域。Unreal Engine版本指的是该引擎的发布版本&#xff0c;不同版本之间在功能、性能和稳定性等方面存在差异。北京木奇移动技术有…
最新文章