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

news2024/11/29 12:39:02

本文来自施磊老师的课程,老师讲的非常不错,我的笔记也是囫囵吞枣全部记下,但是我在这里推荐一本书,真的真的建议初学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.coloradmin.cn/o/1658883.html

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

相关文章

服务异步通讯MQ

同步调用存在的问题: 异步调用方案: RabbitMQ安装: 第一种:在线拉取 docker pull rabbitmq:3-management 第二种:将已有的安装包放入再用load加载 我这里放到tmp包里边 然后:cd /tmp docker load -i mq.tar 加载进去 然后运行mq容器 docker run \-e RABBITMQ_DEFAULT_USER…

tag-字符串:数组拆分I

题目 给定长度为 2n 的整数数组 nums &#xff0c;你的任务是将这些数分成 n 对, 例如 (a1, b1), (a2, b2), …, (an, bn) &#xff0c;使得从 1 到 n 的 min(ai, bi) 总和最大。 返回该 最大总和 。 示例 题解一 class Solution:def arrayPairSum(self, nums: List[int]) …

@Test测试Mapper接口报错java.lang.NullPointerException

Test测试Mapper接口报错java.lang.NullPointerException 报错原因&#xff1a;没有注入依赖 解决方法&#xff1a;在测试类上面添加SpringBootTest

windows 环境下安装《车辆动态监控系统》支持JT808、JT1078、苏标主动安全设备接入

《车辆动态监控系统》下载安装部署包 开放端口 80/443/8800&#xff0c;web后台端口&#xff0c;nginx代理服务&#xff0c;nginx默认为8800端口8808&#xff0c;JT808专用端口6802&#xff0c;视频播放推流端口6891-6898&#xff0c;FTP端口6821&#xff0c;苏标主动安全附件…

[GESP样题 四级] 绝对素数

B3939 [GESP样题 四级] 绝对素数 题目 如果一个两位数是素数&#xff0c;且它的数字位置经过对换后仍为素数&#xff0c;则称为绝对素数&#xff0c;例如 13。给定两个正整数 A, B&#xff0c;请求出大于等于 A&#xff0c;小于等于 B 的所有绝对素数。 输入 1 行&#xff0…

LPDDR5电路设计的新功能

最近因为需要使用到LPDDR5&#xff0c;快速地浏览了JEDEC标准文档&#xff0c;发现与前几代相比出现了一些新的电路设计功能&#xff0c;总结为如下三点&#xff1a; 1. CK/WCK/RDQS时钟方案&#xff1b; 2. 电源的PDN设计目标&#xff1b; 3. DQ, DMI和RDQS的Rx端DFE均衡技术。…

便宜的智能组网系统有哪些?

随着物联网的迅猛发展&#xff0c;智能设备的普及与应用也日益增多。不同地区的智能设备之间的互联通信仍然存在着很多困难和挑战。其中一个主要问题是如何实现便宜而高效的智能组网。在这篇文章中&#xff0c;我们将介绍一款名为【天联】的组网产品&#xff0c;它是北京金万维…

《TAM》论文笔记(上)

原文链接 [2005.06803] TAM: Temporal Adaptive Module for Video Recognition (arxiv.org) 原文代码 GitHub - liu-zhy/temporal-adaptive-module: TAM: Temporal Adaptive Module for Video Recognition 原文笔记 What&#xff1a; TAM: Temporal Adaptive Module for …

AXI4读时序在AXI Block RAM (BRAM) IP核中的应用

在本文中将展示描述了AXI从设备&#xff08;slave&#xff09;AXI BRAM Controller IP核与Xilinx AXI Interconnect之间的读时序关系。 1 Single Read 图1展示了一个从32位BRAM&#xff08;Block RAM&#xff09;进行AXI单次读取操作的时序示例。 图1 AXI 单次读时序图 在该…

555555

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起探讨和分享Linux C/C/Python/Shell编程、机器人技术、机器学习、机器视觉、嵌入式AI相关领域的知识和技术。 人工智能与机器学习 &#x1f4dd;人工智能相关概念☞什么是人工智能、机器学习、深度学习☞人工智能发…

【二次元MMORPG游戏开发】任务系统技术拆解

引言 各位同学大家好。在今天的分享当中&#xff0c;我将对任务系统去做一个拆解。也许你见过很多任务系统&#xff0c;但是今天我要分享的是我们经过一个框架迭代以后的任务系统。我会结合客户端的功能演示给大家去讲解。 跟着演示学开发 基本操作 好&#xff0c;首先我们点…

STM32 ADC学习

ADC Analog-to-Digital Converter&#xff0c;即模拟/数字转换器 常见ADC类型 分辨率和采样速度相互矛盾&#xff0c;分辨率越高&#xff0c;采样速率越低。 ADC的特性参数 分辨率&#xff1a;表示ADC能辨别的最小模拟量&#xff0c;用二进制位数表示&#xff0c;比如8,10…

clickhouse学习笔记06

ClickHouse的建表和引擎选择思路讲解 ClickHouse的常见注意事项和异常问题排查 ClickHouse高性能查询原因剖析-稀疏索引 ClickHouse高性能写入剖析-LSM-Tree存储结构

【每日力扣】98. 验证二叉搜索树 与 108. 将有序数组转换为二叉搜索树

&#x1f525; 个人主页: 黑洞晓威 &#x1f600;你不必等到非常厉害&#xff0c;才敢开始&#xff0c;你需要开始&#xff0c;才会变的非常厉害 98. 验证二叉搜索树 给你一个二叉树的根节点 root &#xff0c;判断其是否是一个有效的二叉搜索树。 有效 二叉搜索树定义如下&a…

Centos7使用kubeadm搭建k8s集群(一主两从)----(mac版)

一、环境准备 1、下载centos7镜像 阿里巴巴开源镜像站-OPSX镜像站-阿里云开发者社区 下载地址: centos安装包下载_开源镜像站-阿里云 选择对应的版本即可&#xff0c;我下载的&#xff1a;CentOS-7-x86_64-DVD-2207-02.iso 2、使用VirtualBox安装centos 选择新建&#xff0c…

【读论文】Gaussian Grouping: Segment and Edit Anything in 3D Scenes

Gaussian Grouping: Segment and Edit Anything in 3D Scenes 文章目录 Gaussian Grouping: Segment and Edit Anything in 3D Scenes1. What2. Why3. How3.1 Anything Mask Input and Consistency3.2 3D Gaussian Rendering and Grouping3.3 Downstream: Local Gaussian Editi…

用手势掌控PPT,玩转演示新姿势

推荐运行环境 使用anaconda创建环境&#xff0c;以免污染原来的python开发环境conda install python3.9pip install -q mediapipe0.10.0pip install pyautoguiPython: version 3.8 - 3.11PIP: version 20.3 请注意以下的坑 以下为我测试过程中的大坑&#xff0c;请及时避开&am…

【嵌入式开发实验】外部中断 按键 LED灯;定时中断;检测直流电机的转速 液晶屏显示;(附实验代码+注释+实验简析)| 附:步进电机/循迹小车相关

“永远不要停止思考,永远不认输。” 🎯作者主页: 追光者♂🔥 🌸个人简介: 💖[1] 计算机专业硕士研究生💖 🌿[2] 2023年城市之星领跑者TOP1(哈尔滨)🌿 🌟[3] 2022年度博客之星人工智能领域TOP4🌟 🏅[4] 阿里云社区特邀专家博主🏅 �

4. Python的深拷贝、浅拷贝

文章目录 0、先说结论1、浅拷贝修改元素值2、深拷贝修改元素值学习链接 0、先说结论 无论深拷贝还是浅拷贝都会为新对象分配一块新的内存&#xff0c;因此新老对象id不相同。 对于浅拷贝&#xff0c;新老对象内部的可变and不可变元素id都是相同的(在没修改元素值之前)。 对于深…

【限时免费,手慢无】Unity 怪物资源包,MONSTER 动作超丰富,不领后悔!

Unity 怪物资源包&#xff0c;MONSTER 动作超丰富 前言资源包内容领取兑换码 前言 &#x1f47e; 突破想象&#xff01;惊艳众人的怪物模型登场 &#x1f47e; 今天要向大家介绍一款令人瞩目的游戏怪物模型&#xff01;这个看似丑陋的小怪物&#xff0c;却有着巨大的潜力&…