使用了未初始化的内存a_Linux Kernel Map之内存管理(1) sys_brk

7b704c6524addb63f75e60d0567c00bf.png


https://makelinux.github.io/kernel/map/

内存是啥?下图就是:

ffcdc8614a2cb5f5bcd073b29500a53e.png

072b113c51365ac043d05d5d9717f2d8.png

24c027d9e8a9d889b0e0a7e79fbbc73c.png

7e1e3a8bbcf7f91c5a325fdd81ae393a.png

c1a7c882040e6f77b7577bbe9e2293cb.png

sys_brk


内存管理代码在内核源码“/mm”下。

7ac5c0299d709922da1420b757abd151.png

/* Below are the C functions used to declare the raw syscalls. They try to be * architecture-agnostic, and return either a success or -errno. Declaring them * static will lead to them being inlined in most cases, but it's still possible * to reference them by a pointer if needed. */static __attribute__((unused))void *sys_brk(void *addr){  return (void *)my_syscall1(__NR_brk, addr);}

首先看一下用户态的brk函数:

#include int brk(void *addr); void *sbrk(intptr_t increment);

为了能够很好的理解这两个函数,我们需要先来理解一下uc程序的内存模型(需要有一定的虚拟内存的概念才能很好的理解!)32位的操作系统,会有4G的虚拟地址空间。   

aef9a72f63259c88b549a4c46e03b6c6.png

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

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

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

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

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

brk() 和 sbrk() 改变 "program brek" 的位置,这个位置定义了进程数据段的终止处(也就是说,program break 是在未初始化数据段终止处后的第一个位置)。如此翻译过来,似乎会让人认为这个 program break 是和上图中矛盾的,上图中的 program break 是在堆的增长方向的第一个位置处(堆和栈的增长方向是相对的),而按照说明手册来理解,似乎是在 bss segment 结束那里(因为未初始化数据段一般认为是 bss segment)。
#include #include #include #include #include   //声明一个味定义的变量,它会放在 bss segment 中int bssvar;    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);         //调整 program break 位置(假设现在不知道这个位置在堆头还是堆尾)    sbrk(32);          //再一次获取内存区      pmem = (char *)malloc(32);       if (pmem == NULL) {              perror("malloc");              exit (EXIT_FAILURE);      }      //检查和第一次获取的内存区的起始地址是否一样    printf ("pmem:%p\n", pmem);             //计算调整 program break 后的空隙    heap_gap_bss = (long)pmem - (long)&bssvar - 4;        printf ("2-gap between heap and bss:%lu\n", heap_gap_bss);      //释放     free(pmem);        return 0;  }

运行结果:

$ ./a.exeend of bss section:0x1004071a4pmem:0x6000005301-gap between heap and bss:21470614412pmem:0x6000005302-gap between heap and bss:21470614412

从上面的输出中,可以发现几点:

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

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

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

运行结果:

$ ./a.exepmem:0x6000004e0heap size on each load: 326432

这么做之后,再运行 3 次这个程序看看:

$ ./a.exepmem:0x6000004e0heap size on each load: 326432Toa@DESKTOP-ASO4FBT /cygdrive/g/test/c/glibc/unistd$ ./a.exepmem:0x6000004e0heap size on each load: 326432Toa@DESKTOP-ASO4FBT /cygdrive/g/test/c/glibc/unistd$ ./a.exepmem:0x6000004e0heap size on each load: 326432

从输出看到,每次加载后,堆头部的其实地址都一样了。但我们不需要这么做,每次堆都一样,容易带来缓冲区溢出攻击(以前老的 linux 内核就是特定地址加载的),所以还是需要保持 randomize_va_space 这个内核变量值为 1 。

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

#include #include #include #include #include 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);        //0x20ff8 就是堆和 bss段 之间的空隙常数;        //改变后要用 sbrk(0) 再次获取更新后的program break位置        printf ("%d\n", sbrk(0) - (long)pmem - 0x20ff8);       }    free(pmem);        return 0;}

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

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

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


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

https://blog.csdn.net/hipilee/article/details/8075251

--推荐阅读--


《Slab allocators in the Linux Kernel: SLAB, SLOB, SLUB》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值