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

网友投稿 945 2022-11-28

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

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

文章目录

​​一、虚拟地址空间​​​​二、函数调用过程​​

​​一、参数入参​​

​​入参过程​​​​理解入栈顺序代码​​​​8字节数据入栈​​​​12字节数据入栈​​

​​二、函数栈帧开辟​​

一、虚拟地址空间

​​.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寄存器,指向当前运行进程的页表。

虚拟内存管理的作用:

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

#includeint g_data1 = 10; // .dataint g_data2 = 0; // .bssint g_data3; // .bssstatic int g_data4 = 11; // .datastatic int g_data5 = 0; // .bssstatic int g_data6; // .bssint 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字节数据入栈

#includestruct 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字节数据入栈

#includestruct 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字节

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

返回8字节

#includestruct 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);}

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:摩尔投票法
下一篇:socket编程常用函数笔记
相关文章

 发表评论

暂时没有评论,来抢沙发吧~