brk函数和sbrk的函数原型如下
#include <unistd.h>
int brk(void *addr);
void *sbrk(intptr_t increment);
为了能够很好的理解这两个函数,我们需要先来理解一下uc程序的内存模型(需要有一定的虚拟内存的概念才能很好的理解!)
高地址
低地址
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 命令修改一下内核参数:
这么做之后,再运行 3 次这个程序看看:引用#sysctl -w kernel/randomize_va_space=0
从输出看到,每次加载后,堆头部的其实地址都一样了。但我们不需要这么做,每次堆都一样,容易带来缓冲区溢出攻击(以前老的 linux 内核就是特定地址加载的),所以还是需要保持 randomize_va_space 这个内核变量值为 1 。引用[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
下面就来验证 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() 中的地址参数已达到调整堆的目的。