【Linux环境编程】内存管理:函数栈空间,虚拟内存及其分配

一.函数调用栈空间的分配与释放

函数执行时有自己的临时栈空间,c++成员函数有两个临时栈空间,一个是成员函数的还有一个是对象的。

函数的参数是压进临时栈中,传递的实参用来初始化临时栈中的形参。

函数属性:

int __attribute__((stdcall)) add(int a, int b)
{
    return a+b;
}

一共有3种属性(调用方式):stdcall,cdecl,fastcall,他们会影响编译:
函数栈压栈的参数顺序(这三个都是从右到左)
函数栈清空方式
函数的名字转换

前两点可反汇编看看:

 gcc XXX.c -S
 cat XXX.s

第三点可以:

nm 程序

可以看到c程序 add 函数名字就叫add c++因为有重载机制会 把名字 变成 列如 Z3addPis Z 是都有的,3表示函数名字的长度,后面的是函数形参类型——C++的重载其实是一个假象!编译之后函数的符号是不一样的。

二.虚拟内存

#include<stdio.h>
#include<malloc.h>
int main()
{

    int *p = malloc(4);

    printf("%p\n",p);
    while(1);

    return 0;
}

结果:0xb7786018

#include<stdio.h>
#include<malloc.h>

int main()
{
    int *p = (int*)0xb7786018;
    printf("%d\n",*p);
    return 0;
}

可能会出现段错误,或随机的值。

#include<stdio.h>
#include<malloc.h>

int main()
{
    int *p = malloc(0);
    *p = 999;
    printf("%d\n",*p);
    return 0;
}

输出:999

问题:一个程序不能访问另外一个程序的地址指向的空间。

理解:
1.每个程序的开始地址是相同的
2.程序中使用的地址不是物理地址,而是逻辑地址。
(逻辑地址仅仅是个编号,它使用int 4字节整数表示 (4G),每个程序提供了4G的访问能力。)

逻辑地址与物理地址之间存在映射,这个过程叫做内存映射。

strace  程序 
 #跟踪程序的执行过程

虚拟内存禁止了用户直接访问物理存储设备, 提高了系统的稳定性。

对于第一个程序,a分配了空间,即有了内存映射了,所以正确执行。

对于第二个程序,p指向了地址0xb7786018,而这个地址在本程序中并没有映射到物理内存中,所以访问会出错。

#include<stdio.h>
#include<malloc.h>
int main()
{

    int *p = malloc(4);
    *(p) = 1;
    *(p+1) = 2;
    *(p+1000) = 3; 
    printf("%p\n",p);
    while(1);

    return 0;
}

上面程序执行并不会发生段错误。

原因:内存映射的基本单位是 4k (即1000,16进制,叫内存页)。哪怕我们要1个字节,它映射的也是4k空间,malloc 第一次被调用的时候 不管你分不分配空间,它至少调用4k的空间。(所以上面的代码以及第三个代码不会出现段错误)

段错误:地址没有映射到一个物理空间,无效访问

合法访问:比如malloc分配的多余空间可以访问,但属于非法访问!

三.虚拟内存的分配

栈:编译器自动生成代码维护
堆:地址是否映射,映射的空间是否被管理

brk / sbrk 内存映射函数

man brk
#查看说明文档

int brk(void *end_data_segment);
分配空间,释放空间

void *sbrk(intptr_t increment);
返回空间地址

应用:
1.使用sbrk分配空间
2.使用sbrk得到没有映射的虚拟地址(参数为0)
3.使用brk分配空间
4.使用brk释放空间

sbrk与brk后台系统维护一个指针。
指针初始化为null。
调用sbrk时,判断指针是否为null,如果是,得到大块空闲空间的首地址来初始化指针,同时把指针+increment;否则,先返回当前指针,并且把指针位置+increment;

在映射的时候,都是映射一个页,以此来提高效率。

#include <stdio.h>
#include <unistd.h>

main()
{
    int *p = sbrk(4);//得到大块空闲空间的首地址来初始化指针并返回,然后把维护的指针+4
    *p = 8888;
    printf("p:%p\n", p);
    printf("*p:%d\n", *p);
}

结果:
p:0x93c3000
*p:8888
一切正常。

我们看看参数为0的情况:

#include <stdio.h>
#include <unistd.h>

main()
{
    int *p = sbrk(0);//初次调用,且参数为0,获得没有映射的虚拟地址
    *p = 8888;
    printf("p:%p\n", p);
    printf("*p:%d\n", *p);
}

结果:段错误 (核心已转储)
我们加一个死循环,进入/proc/${pid}/maps 看看:

#include <stdio.h>
#include <unistd.h>

main()
{
    int *p = sbrk(0);//获得没有映射的虚拟地址
    printf("p:%p\n", p);
    printf("pid:%d\n", getpid());
    while(1);
}

结果:
p:0x9482000
pid:2933

但是我们进入/proc/2933/maps中查看之后,并找不到p所指向的内存空间地址!说明该虚拟地址并没有映射到物理地址上,所以上一个程序当然会出现段错误了。

我们再看看下面一个例子:

#include <stdio.h>
#include <unistd.h>

main()
{
    int *p = sbrk(0);//获得没有映射的虚拟地址,得到大块空闲空间的首地址来初始化指针,同时把指针+increment
    brk(p+1);//分配空间,映射
    *p = 8888;
}

该程序执行没有出错,首先,sbrk(0)返回了一个虚拟地址,但是还没映射。之后调用brk,brk检测到p+1还未映射,于是分配空间,进行映射。

 brk(p);//释放空间 *p = 8888;

如果我们加上以上代码,又会出现段错误,因为空间被释放了!
sbrk:表示指针的相对移动;
brk:表示指针的绝对移动,如发现当前指针地址未映射,则进行映射。

为说明sbrk的相对移动以及空间的分配与释放,我们看下面的例子:

#include <stdio.h>
#include <unistd.h>

main()
{
    int *p1 = sbrk(4);
    int *p2 = sbrk(200);
    int *p3 = sbrk(400);
    int *p4 = sbrk(-4);
    int *p5 = sbrk(-4);
    printf("%p\n", p1);
    printf("%p\n", p2);
    printf("%p\n", p3);
    printf("%p\n", p4);
    printf("%p\n", p5);
}

结果:

//我们假设sbrk维护的指针叫做q
0x88b4000   //分配一个页的地址q并返回给p1,q = q+4
0x88b4004   //取得q,q = q+200
0x88b40cc   //取得q,q = q+400
0x88b425c   //取得q,q = q-4
0x88b4258   //取得q,q = q-4

我们可以看出,sbrk都是相对指针的当前位置进行移动的~通过参数的正负,可以实现空间的分配与释放。

异常处理:

如果成功,brk返回0,sbrk返回指针;
如果失败,brk返回-1,sbrk返回(void *)-1。

检测错误: 使用 perror(“err:”);函数。

你可能感兴趣的:(C语言,内存管理,虚拟内存,Linux编程,函数栈)