C语言内存分配模型->brk() sbrk()


   brk函数和sbrk的函数原型如下

    #include <unistd.h>  
    int brk(void *addr);  
    void *sbrk(intptr_t increment);  


   为了能够很好的理解这两个函数,我们需要先来理解一下uc程序的内存模型(需要有一定的虚拟内存的概念才能很好的理解!)

   32位的操作系统,会有4G的虚拟地址空间。   

                                                     高地址

  
                                                 低地址


1、程序代码区:存放函数体的二进制代码。  

2、全局区数据区:全局数据区划分为三个区域。全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。常量数据存放在另一个区域里。这些数据在程序结束后由系统释放。我们所说的BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。

3、堆区:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。

4、栈区:由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

5、命令行参数区:存放命令行参数和环境变量的值。

在man里是这么描述brk和sbrk的:

brk() 和 sbrk() 改变 "program brek" 的位置,这个位置定义了进程数据段的终止处(也就是说,program break 是在未初始化数据段终止处后的第一个位置)。
如此翻译过来,似乎会让人认为这个 program break 是和上图中矛盾的,上图中的 program break 是在堆的增长方向的第一个位置处(堆和栈的增长方向是相对的),而按照说明手册来理解,似乎是在 bss segment 结束那里(因为未初始化数据段一般认为是 bss segment)。


首先说明一点,一个程序一旦编译好后,text segment ,data segment 和 bss segment 是确定下来的,这也可以通过 objdump 观察到。下面通过一个程序来测试这个 program break 是不是在 bss segment 结束那里:


    #include <stdio.h>  
    #include <unistd.h>  
    #include <stdlib.h>  
    #include <sys/time.h>  
    #include <sys/resource.h>  
       
       
    int bssvar;    //声明一个味定义的变量,它会放在 bss segment 中  
       
       
    int main(void)  
    {  
        char *pmem;  
        long heap_gap_bss;  
       
       
        printf ("end of bss section:%p\n", (long)&bssvar + 4);  
       
       
        pmem = (char *)malloc(32);          //从堆中分配一块内存区,一般从堆的开始处获取  
        if (pmem == NULL) {  
            perror("malloc");  
            exit (EXIT_FAILURE);  
        }  
       
       
        printf ("pmem:%p\n", pmem);  
       
       
    //计算堆的开始地址和 bss segment 结束处得空隙大小,注意每次加载程序时这个空隙都是变化的,但是在同一次加载中
                                                         它不会改变  
        heap_gap_bss = (long)pmem - (long)&bssvar - 4;            
        printf ("1-gap between heap and bss:%lu\n", heap_gap_bss);  
       
       
        free (pmem);   //释放内存,归还给堆  
           
        sbrk(32);        //调整 program break 位置(假设现在不知道这个位置在堆头还是堆尾)  
         pmem = (char *)malloc(32);   //再一次获取内存区  
            if (pmem == NULL) {  
                    perror("malloc");  
                    exit (EXIT_FAILURE);  
            }  
       
       
            printf ("pmem:%p\n", pmem);   //检查和第一次获取的内存区的起始地址是否一样  
        heap_gap_bss = (long)pmem - (long)&bssvar - 4;  //计算调整 program break 后的空隙  
        printf ("2-gap between heap and bss:%lu\n", heap_gap_bss);  
       
       
        free(pmem);   //释放  
        return 0;  
    }  



从上面的输出中,可以发现几点:
1. bss 段一旦在在程序编译好后,它的地址就已经规定下来。
2. 一般及简单的情况下,使用 malloc() 申请的内存,释放后,仍然归还回原处,再次申请同样大小的内存区时,还是从第 1 次那里获得。
3. bss segment 结束处和堆的开始处的空隙大小,并不因为 sbrk() 的调整而改变,也就是说明了 program break 不是调整堆头部。

所以,man 手册里所说的  “program break 是在未初始化数据段终止处后的第一个位置” ,不能将这个位置理解为堆头部。这时,可以猜想应该是在堆尾部,也就是堆增长方向的最前方。下面用程序进行检验:

当 sbrk() 中的参数为 0 时,我们可以找到 program break 的位置。那么根据这一点,检查一下每次在程序加载时,系统给堆的分配是不是等同大小的:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/resource.h>
 
 
int main(void)
{
        void *tret;
        char *pmem;
 
 
 
        pmem = (char *)malloc(32);
        if (pmem == NULL) {
                perror("malloc");
                exit (EXIT_FAILURE);
        }
 
 
        printf ("pmem:%p\n", pmem);
 
        tret = sbrk(0);
        if (tret != (void *)-1)
                printf ("heap size on each load: %lu\n", (long)tret - (long)pmem);
 
 
    return 0;
}

从输出可以看到,虽然堆的头部地址在每次程序加载后都不一样,但是每次加载后,堆的大小默认分配是一致的。但是这不是不能改的,可以使用 sysctl 命令修改一下内核参数:

引用
#sysctl -w kernel/randomize_va_space=0
这么做之后,再运行 3 次这个程序看看:
引用
[beyes@localhost C]$ ./sbrk 
pmem:0x804a008
heap size on each load: 135160
[beyes@localhost C]$ ./sbrk 
pmem:0x804a008
heap size on each load: 135160
[beyes@localhost C]$ ./sbrk 
pmem:0x804a008
heap size on each load: 135160
从输出看到,每次加载后,堆头部的其实地址都一样了。但我们不需要这么做,每次堆都一样,容易带来缓冲区溢出攻击(以前老的 linux 内核就是特定地址加载的),所以还是需要保持 randomize_va_space 这个内核变量值为 1 。

下面就来验证 sbrk() 改变的 program break 位置在堆的增长方向处:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/resource.h>
 
 
int main(void)
{
        void *tret;
        char *pmem;
        int i;
        long sbrkret;
 
       pmem = (char *)malloc(32);
        if (pmem == NULL) {
                perror("malloc");
                exit (EXIT_FAILURE);
        }
 
 
        printf ("pmem:%p\n", pmem);
 
         for (i = 0; i < 65; i++) {
                sbrk(1);
                printf ("%d\n", sbrk(0) - (long)pmem - 0x20ff8);   //0x20ff8 就是堆和 bss段 之间的空隙常数;改变后要用 sbrk(0) 再次获取更新后的program break位置
        }
       free(pmem);
 
        
       return 0;
}

从输出看到,sbrk(1) 每次让堆往栈的方向增加 1 个字节的大小空间。

而 brk() 这个函数的参数是一个地址,假如你已经知道了堆的起始地址,还有堆的大小,那么你就可以据此修改 brk() 中的地址参数已达到调整堆的目的。

实际上,在应用程序中,基本不直接使用这两个函数,取而代之的是 malloc() 一类函数,这一类库函数的执行效率会更高。还需要注意一点,当使用 malloc() 分配过大的空间,比如超出 0x20ff8 这个常数(在我的系统(Fedora15)上是这样,别的系统可能会有变)时,malloc 不再从堆中分配空间,而是使用 mmap() 这个系统调用从映射区寻找可用的内存空间。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值