文章目录
一、虚拟地址空间
.text
:代码段,存放代码,指令
.rodata
:只读数据区,存放常量
.data
:存放初始化且初始化值不为0的数据
.bss
:存放未初始化和初始化为0的数据(包括全局变量,static修饰的变量)
.heap
:堆区
.stack
:栈区(可存放函数形参和局部变量)
变量生存期
把程序运行时一个变量占有内存空间的时间段称为该变量的 生存期
C++把变量的生存期分为: 静态、自动和动态 三种。
静态生存期: 全局变量都具有静态生存期,它们的内存空间程序开始执行时就进行分配,直到程序结束才被收回。
自动生存期: 局部变量和函数形参一般都具有自动生存期,它们的内存空间在程序执行到定义它们的复合语句(包括函数体)时才分配,当定义它们的复合语句执行结束时内存被收回。
动态生存期: 具有动态生存期的变量的生存时间是由程序员自己控制的,其内存空间用new操作符分配,用delete回收。
在定义局部变量时,可以为它们加上存储类修饰符 auto、static 和 register 来指出它们的生存期。
定义为 static 存储类型的局部变量具有静态生存期,它们也被存放在静态数据区。
- Linux将每个进程的4GB的独立地址空间又划分为用户地址空间(0x00000000~0xBFFFFFFF)和内核地址空间(0xC000000~0xFFFFFFFF)两部分
- 操作系统内核代码和数据存放在内核地址空间,每个进程自己私有的代码和程序存放在用户地址空间
- 虽然linux的内核代码和数据被映射到了每个进程的地址空间中(所有进程看到的内容是相同的),但在物理内存中,只有内核代码和数据的一份拷贝
- 随着进程的切换,当前有效的用户地址空间也在切换
- CPU有一个CR3寄存器,指向当前运行进程的页表。
虚拟内存管理的作用:
- 为每个内存提供独立的内存空间
- 通过后台在物理内存和外存之间的数据换入换出,给进程创造“内存很大”的假象
- 通过物理内存页与虚拟内存页的一对多映射实现共享内存、共享函数库(动态链接库)、共享操作系统内核空间
- 利用延迟分配提高内存利用率以及动态内存分配速度
- 利用缺页中断实现内存保护
- 利用写时拷贝技术提高进程创建效率
- 通过“文件—虚拟空间”映射机制提供灵活的文件访问方式
#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;
}
每一个进程的用户空间是私有的,内核空间是共享的
二、函数调用过程
- 参数入参
- 函数栈帧开辟
- 返回值返回
- 栈帧回退
- 参数清除
形参内存空间不在被调用函数的栈空间,而在调用函数的栈帧
小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;
}
入参过程
- push数据:栈顶寄存器
esp
向上移动(低地址方向增长),将push的数据放到栈顶 - 入栈顺序:参数从右向左
- 入栈方式: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;
}
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);
}
主函数:
子函数:
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;
}
- 没有用push寄存器的方法,直接将栈顶指针向上偏移,然后将参数的值拷入
- 入参顺序:从右向左,结构体参数入栈时,从低地址数据到高地址数据入栈
二、函数栈帧开辟
返回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;
}
返回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;
}
返回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;
}
参数入参
参数a、b先入栈,然后入栈一个存放返回值的临时空间,入栈调用函数下一行的地址,保存ebp
到调用函数栈帧之上,并且开辟栈帧(call指令
)
返回值
读取入参时候入栈的参数,放在调用函数的栈空间
用eax
指向调用函数栈空间内用于接收返回值的地址(也即上面的[ebp-108h]
),此时的ebp
则指向被调用函数的栈空间,用eax
和 ebp
把返回值写入调用函数的栈空间。
返回值
栈帧回退
- pop恢复寄存器
-
esp=ebp
,pop ebp
两步退出被调用方的栈帧 - ret:将原来入栈的调用方下一条指令的地址出栈并恢复
参数清除
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);
}