文章目录

一、虚拟地址空间

虚拟地址空间和函数调用过程_调用函数


​.text​​:代码段,存放代码,指令

​.rodata​​:只读数据区,存放常量

​.data​​:存放初始化且初始化值不为0的数据

​.bss​​:存放未初始化和初始化为0的数据(包括全局变量,static修饰的变量)

​.heap​​:堆区

​.stack​​:栈区(可存放函数形参和局部变量)

变量生存期

把程序运行时一个变量占有内存空间的时间段称为该变量的 生存期
C++把变量的生存期分为: 静态、自动和动态 三种。

静态生存期: 全局变量都具有静态生存期,它们的内存空间程序开始执行时就进行分配,直到程序结束才被收回。

自动生存期: 局部变量和函数形参一般都具有自动生存期,它们的内存空间在程序执行到定义它们的复合语句(包括函数体)时才分配,当定义它们的复合语句执行结束时内存被收回。

动态生存期: 具有动态生存期的变量的生存时间是由程序员自己控制的,其内存空间用new操作符分配,用delete回收。

在定义局部变量时,可以为它们加上存储类修饰符 auto、static 和 register 来指出它们的生存期。

定义为 static 存储类型的局部变量具有静态生存期,它们也被存放在静态数据区。

虚拟地址空间和函数调用过程_调用函数_02

  1. Linux将每个进程的4GB的独立地址空间又划分为用户地址空间(0x00000000~0xBFFFFFFF)内核地址空间(0xC000000~0xFFFFFFFF)两部分
  2. 操作系统内核代码和数据存放在内核地址空间,每个进程自己私有的代码和程序存放在用户地址空间
  3. 虽然linux的内核代码和数据被映射到了每个进程的地址空间中(所有进程看到的内容是相同的),但在物理内存中,只有内核代码和数据的一份拷贝
  4. 随着进程的切换,当前有效的用户地址空间也在切换
  5. CPU有一个CR3寄存器,指向当前运行进程的页表。

虚拟内存管理的作用:

  1. 为每个内存提供独立的内存空间
  2. 通过后台在物理内存和外存之间的数据换入换出,给进程创造“内存很大”的假象
  3. 通过物理内存页与虚拟内存页的一对多映射实现共享内存、共享函数库(动态链接库)、共享操作系统内核空间
  4. 利用延迟分配提高内存利用率以及动态内存分配速度
  5. 利用缺页中断实现内存保护
  6. 利用写时拷贝技术提高进程创建效率
  7. 通过“文件—虚拟空间”映射机制提供灵活的文件访问方式

#include<stdio.h>

int g_data1 = 10; // .data
int g_data2 = 0; // .bss
int g_data3; // .bss

static int g_data4 = 11; // .data
static int g_data5 = 0; // .bss
static int g_data6; // .bss

int main() {
// 生成汇编指令,而不是产生数据
// 运行到该段代码时,才在函数栈帧上开辟4字节的空间存放数据
int a = 10;
int b = 0;
int c;

// 放在数据段,程序启动的时候不会初始化,运行到该代码后再初始化
// 程序运行起来后,.bss段被清0
static int e = 10; // .data
static int f = 0; // .bss
static int g; // .bss
return 0;
}

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

虚拟地址空间和函数调用过程_数据_03

二、函数调用过程

  1. 参数入参
  2. 函数栈帧开辟
  3. 返回值返回
  4. 栈帧回退
  5. 参数清除

虚拟地址空间和函数调用过程_入栈_04

形参内存空间不在被调用函数的栈空间,而在调用函数的栈帧

小Tips: 函数的参数都在ebp所指示的内存地址的正偏移处,函数内部的局部变量都在ebp所指示的内存地址的负偏移处。此时ebp指向当前函数栈的栈底。

一、参数入参

int fun(int a, int b) {
int c = a + b;
return c;
}

int main() {
int a = 10;
int b = 20;
int c = 0;
c = fun(a, b);
return 0;
}

虚拟地址空间和函数调用过程_数据_05

入参过程
  1. push数据:栈顶寄存器​​esp​​ 向上移动(低地址方向增长),将push的数据放到栈顶
  2. 入栈顺序:参数从右向左
  3. 入栈方式:push寄存器到栈顶
理解入栈顺序代码
void fun1(int a, int b) {
printf("a = %d, b = %d\n", a, b); // a = 1, b = 0
}

int main() {
int i = 0;
fun1(i++, i++); // 右边参数i=0入栈,i++,然后左边参数入栈
printf("i = %d", i); // i = 2
return 0;
}
8字节数据入栈
#include<stdio.h>

struct Node {
int d1;
int d2;
};

int fun(struct Node a, struct Node b) {
return 0;
}

int main() {
struct Node node1 = { 10, 20 };
struct Node node2 = { 30, 40 };
int c = fun(node1, node2);
return 0;
}

虚拟地址空间和函数调用过程_数据_06

struct Data
{
int d1;
int d2;
};

int fun(struct Data a, struct Data b)
{
int c = 0;
//c = a + b;
return c;
}

int main()
{
int c = 0;
struct Data a = { 20,30 };
struct Data b = { 40,50 };
c = fun(a, b);
}

虚拟地址空间和函数调用过程_数据_07


主函数:

虚拟地址空间和函数调用过程_调用函数_08

虚拟地址空间和函数调用过程_调用函数_09

子函数:

虚拟地址空间和函数调用过程_入栈_10

12字节数据入栈
#include<stdio.h>

struct Node {
int d1;
int d2;
int d3;
};

int fun(struct Node a, struct Node b) {
return 0;
}

int main() {
struct Node node1 = { 10, 20, 30 };
struct Node node2 = { 40, 50, 60 };
int c = fun(node1, node2);
return 0;
}

虚拟地址空间和函数调用过程_入栈_11

  1. 没有用push寄存器的方法,直接将栈顶指针向上偏移,然后将参数的值拷入
  2. 入参顺序:从右向左,结构体参数入栈时,从低地址数据到高地址数据入栈

二、函数栈帧开辟

虚拟地址空间和函数调用过程_调用函数_12


返回4字节

#include<stdio.h>

int fun(int a, int b) {
return 0;
}

int main() {
int a = 10;
int b = 20;
int c = fun(a, b);
return 0;
}

虚拟地址空间和函数调用过程_数据_13

返回8字节

#include<stdio.h>

struct Node {
int d1;
int d2;
};

struct Node fun(int a, int b) {
struct Node ret = { a, b };
return ret;
}

int main() {
int a = 10;
int b = 20;
struct Node node = fun(a, b);
return 0;
}

虚拟地址空间和函数调用过程_数据_14

返回12字节

struct Node {
int d1;
int d2;
int d3;
};

struct Node fun(int a, int b) {
struct Node d = { 10, 20, 30 };
return d;
}

int main() {
int a = 10;
int b = 20;
int c = 30;
struct Node node = fun(a, b);
return 0;
}

参数入参

虚拟地址空间和函数调用过程_C++_15

参数a、b先入栈,然后入栈一个存放返回值的临时空间,入栈调用函数下一行的地址,保存​​ebp​​​到调用函数栈帧之上,并且开辟栈帧(​​call指令​​)

返回值

读取入参时候入栈的参数,放在调用函数的栈空间

虚拟地址空间和函数调用过程_C++_16


用​​eax​​​指向调用函数栈空间内用于接收返回值的地址(也即上面的​​[ebp-108h]​​​),此时的​​ebp​​​则指向被调用函数的栈空间,用​​eax​​​和 ​​ebp​​把返回值写入调用函数的栈空间。

返回值

虚拟地址空间和函数调用过程_调用函数_17


栈帧回退

虚拟地址空间和函数调用过程_调用函数_18

  1. pop恢复寄存器
  2. ​esp=ebp​​​,​​pop ebp​​ 两步退出被调用方的栈帧
  3. ret:将原来入栈的调用方下一条指令的地址出栈并恢复

参数清除

虚拟地址空间和函数调用过程_数据_19

int func_B(int arg_B1, int arg_B2){
int var_B1, var_B2;
return 0;
}

int func_A(int arg_A1, int arg_A2){
int var_A1, var_A2;
func_B(4, 3);
return 0;
}

void main(){
int var_main;
func_A(2, 1);
}

虚拟地址空间和函数调用过程_数据_20