用brk实现sbrk,关于brk的返回值

好久不见. 提交于 2020-03-01 03:58:32

    首先我们已经知道linux下,malloc最后调用的是sbrk函数,而sbrk是对brk的简单封装。

    用sbrk模仿malloc很简单,sbrk(0)得到当前breakpoint,再调用sbrk(size)即可。(PS:breakpoint表示堆结束地址)

    一直以来让我困惑的是,怎么用brk去实现sbrk,换句话说,就是只有brk系统调用,如何能得知当前的breakpoint...难道就没有人想过这个问题嘛?搜索了各种关键字,来来回回都围绕着sbrk讲,算了,自己动手,丰衣足食,咱求人不如求己,还是自己分析分析好了,glibc中brk的wrapper如下:

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

man手册中对此函数的描述:

       brk() sets the end of the data segment to the value specified by addr, when that value is reasonable, the system has enough memory, and the process does not exceed  its  maximum data size (see setrlimit(2)).

RETURN VALUE

       On success, brk() returns zero.  On error, -1 is returned, and errno is set to ENOMEM.  (But see Linux Notes below.)

可以看见,这个函数的功能是直接设置breakpoint指针为addr,成功返回0,失败-1,并没有返回当前breakpoint的功能,难道glibc是自己初始化了一个自以为正确的起始地址然后以此为基准分配内存的么?这也太不靠谱了吧,而且如果有其他不依赖于glibc的基础库如果直接调用brk分配内存了,再调用malloc岂不是天大的杯具?所以肯定不是这么搞的,又或者是还有一个咱不知道的系统调用,作用就是返回当前breakpoint位置?算了,先看看glibc是怎么实现的再说,遂去官网下了个最新的2.17的glibc,解压后通过grep找到sbrk定义位于glibc-2.17/misc/sbrk.c中


void *
__sbrk (intptr_t increment)
{
  void *oldbrk;

  /* If this is not part of the dynamic library or the library is used
     via dynamic loading in a statically linked program update
     __curbrk from the kernel's brk value.  That way two separate
     instances of __brk and __sbrk can share the heap, returning
     interleaved pieces of it.  */
  if (__curbrk == NULL || __libc_multiple_libcs)
    if (__brk (0) < 0)      /* Initialize the break.  */
      return (void *) -1;

  if (increment == 0)
    return __curbrk;

  oldbrk = __curbrk;
  if ((increment > 0
       ? ((uintptr_t) oldbrk + (uintptr_t) increment < (uintptr_t) oldbrk)
       : ((uintptr_t) oldbrk < (uintptr_t) -increment))
      || __brk (oldbrk + increment) < 0)
    return (void *) -1;

  return oldbrk;
}
libc_hidden_def (__sbrk)
weak_alias (__sbrk, sbrk)

可以看见,当传入参数为0的时候,直接返回__curbrk,而__curbrk的初始化就是前一个if语句里面执行的,因此在在前面的__brk(0)调用过程中肯定设置了__curbrk的值。为了保证没有别的地方更新__curbrk什么的,再次祭出grep

kimo@ubuntu4710:~/gnu/glibc-2.17$ grep -n -r "__curbrk.*=" `find ./ -name "*.c"`
./ports/sysdeps/unix/sysv/linux/am33/brk.c:26:void *__curbrk = 0;
./ports/sysdeps/unix/sysv/linux/am33/brk.c:35:  __curbrk = newbrk;
./ports/sysdeps/unix/sysv/linux/arm/brk.c:24:void *__curbrk = 0;
./ports/sysdeps/unix/sysv/linux/arm/brk.c:31:  __curbrk = newbrk = (void *) INLINE_SYSCALL (brk, 1, addr);
./ports/sysdeps/unix/sysv/linux/m68k/brk.c:23:void *__curbrk = 0;
./ports/sysdeps/unix/sysv/linux/m68k/brk.c:37:  __curbrk = newbrk;
./ports/sysdeps/unix/sysv/linux/hppa/brk.c:24:void *__curbrk = 0;
./ports/sysdeps/unix/sysv/linux/hppa/brk.c:31:  __curbrk = newbrk = (void *) INLINE_SYSCALL (brk, 1, addr);
./ports/sysdeps/unix/sysv/linux/mips/brk.c:23:void *__curbrk = 0;
./ports/sysdeps/unix/sysv/linux/mips/brk.c:46:  __curbrk = newbrk;
./ports/sysdeps/unix/sysv/linux/generic/brk.c:24:void *__curbrk = 0;
./ports/sysdeps/unix/sysv/linux/generic/brk.c:36:  __curbrk = (void *) INTERNAL_SYSCALL (brk, err, 1, addr);
./misc/sbrk.c:42:  if (__curbrk == NULL || __libc_multiple_libcs)
./sysdeps/unix/sysv/linux/s390/brk.c:24:void *__curbrk = 0;
./sysdeps/unix/sysv/linux/s390/brk.c:45:  __curbrk = newbrk;
./sysdeps/unix/sysv/linux/sparc/sparc32/brk.c:25:void *__curbrk = 0;
./sysdeps/unix/sysv/linux/sparc/sparc32/brk.c:44:  __curbrk = newbrk;
./sysdeps/unix/sysv/linux/i386/brk.c:26:void *__curbrk = 0;
./sysdeps/unix/sysv/linux/i386/brk.c:42:  __curbrk = newbrk;
./sysdeps/unix/sysv/linux/x86_64/brk.c:24:void *__curbrk = 0;
./sysdeps/unix/sysv/linux/x86_64/brk.c:31:  __curbrk = newbrk = (void *) INLINE_SYSCALL (brk, 1, addr);
./sysdeps/unix/sysv/linux/sh/brk.c:24:void *__curbrk = 0;
./sysdeps/unix/sysv/linux/sh/brk.c:37:  __curbrk = newbrk;

果然,只有对应体系结构下的brk.c更新了这个值。我装的ubuntu64,默认应该就是./sysdeps/unix/sysv/linux/x86_64/brk.c

这个文件的代码不长,去掉前面那些gnu相关的注释也就这点内容

#include <errno.h>
#include <unistd.h>
#include <sysdep.h>

/* This must be initialized data because commons can't have aliases.  */
void *__curbrk = 0;

int
__brk (void *addr)
{
  void *newbrk;

  __curbrk = newbrk = (void *) INLINE_SYSCALL (brk, 1, addr);

  if (newbrk < addr)
    {
      __set_errno (ENOMEM);
      return -1;
    }

  return 0;
}
weak_alias (__brk, brk)

    这里的INLINE_SYSCALL(brk,1,addr)居然直接就能返回  当前的breakpoint,其实到这里我已经隐约感觉到了就是brk(0)系统调用返回了当前的breakpoint,但是...用linus 大神的话说,talk is cheap,show me the code....于是还是沿着这个宏一路向西追踪 陆续找到了如下定义, 在./sysdeps/unix/sysv/linux/x86_64/sysdep.h中

/* Define a macro which expands inline into the wrapper code for a system
   call.  */
# undef INLINE_SYSCALL
# define INLINE_SYSCALL(name, nr, args...) \
  ({                                          \
    unsigned long int resultvar = INTERNAL_SYSCALL (name, , nr, args);        \
    if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (resultvar, ), 0))         \
      {                                       \
    __set_errno (INTERNAL_SYSCALL_ERRNO (resultvar, ));           \
    resultvar = (unsigned long int) -1;                   \
      }                                       \
    (long int) resultvar; })

# define INTERNAL_SYSCALL(name, err, nr, args...) \
  INTERNAL_SYSCALL_NCS (__NR_##name, err, nr, ##args)

# define INTERNAL_SYSCALL_NCS(name, err, nr, args...) \
  ({                                          \
    unsigned long int resultvar;                          \
    LOAD_ARGS_##nr (args)                             \
    LOAD_REGS_##nr                                \
    asm volatile (                                \
    "syscall\n\t"                                 \
    : "=a" (resultvar)                                \
    : "0" (name) ASM_ARGS_##nr : "memory", "cc", "r11", "cx");            \
    (long int) resultvar; })

    呵呵,又是asm内嵌的汇编,奇怪的是没有看见int 0x80或者 sysenter 指令,google了一下才知道64位系统中的syscall就是对应32位的sysenter,到此用户空间执行完毕,完全可以肯定是系统调用brk(0)返回了当前的breakpoint值但是这个结果跟man手册描述的返回值不相同啊,brk不是返回0 or -1么。反正都折腾到这里了,不如在看看内核代码吧,在kernel.org上下载了个最新的稳定版linux-3.10.2。马上搜索系统调用的实现

kimo@ubuntu4710:~/gnu/linux-3.10.2$ grep -n -r "SYSCALL_DEFINE1.*brk" ./
./mm/mmap.c:261:SYSCALL_DEFINE1(brk, unsigned long, brk)
./mm/nommu.c:502:SYSCALL_DEFINE1(brk, unsigned long, brk)
./arch/alpha/kernel/osf_sys.c:54:SYSCALL_DEFINE1(osf_brk, unsigned long, brk)

    看了下mm下的Makefile,只有在编译内核的时候没有选择mmu为y,才会使用nommu.c,所以果断vim mm/mmap.c +261,对应代码如下

SYSCALL_DEFINE1(brk, unsigned long, brk)
{
    unsigned long rlim, retval;
    unsigned long newbrk, oldbrk;
    struct mm_struct *mm = current->mm;
    unsigned long min_brk;
    bool populate;

    down_write(&mm->mmap_sem);

#ifdef CONFIG_COMPAT_BRK
    /*
     * CONFIG_COMPAT_BRK can still be overridden by setting
     * randomize_va_space to 2, which will still cause mm->start_brk
     * to be arbitrarily shifted
     */
    if (current->brk_randomized)
        min_brk = mm->start_brk;
    else
        min_brk = mm->end_data;
#else
    min_brk = mm->start_brk;
#endif
    if (brk < min_brk)
        goto out;
    /*
     * Check against rlimit here. If this check is done later after the test
     * of oldbrk with newbrk then it can escape the test and let the data
     * segment grow beyond its set limit the in case where the limit is
     * not page aligned -Ram Gupta
     */
    rlim = rlimit(RLIMIT_DATA);
    if (rlim < RLIM_INFINITY && (brk - mm->start_brk) +
            (mm->end_data - mm->start_data) > rlim)
        goto out;

    newbrk = PAGE_ALIGN(brk);
    oldbrk = PAGE_ALIGN(mm->brk);
    if (oldbrk == newbrk)
        goto set_brk;

    /* Always allow shrinking brk. */
    if (brk <= mm->brk) {
        if (!do_munmap(mm, newbrk, oldbrk-newbrk))
            goto set_brk;
        goto out;
    }

    /* Check against existing mmap mappings. */
    if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))
        goto out;

    /* Ok, looks good - let it rip. */
    if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)
        goto out;

set_brk:
    mm->brk = brk;
    populate = newbrk > oldbrk && (mm->def_flags & VM_LOCKED) != 0;
    up_write(&mm->mmap_sem);
    if (populate)
        mm_populate(oldbrk, newbrk - oldbrk);
    return brk;

out:
    retval = mm->brk;
    up_write(&mm->mmap_sem);
    return retval;
}

    原来只要传入的brk < min_brk,就会返回当前的breakpoint,而min_brk就是mm->start_brk,即堆首地址。看来是man手册搞错了,这种错误一定要狠狠地改掉!!!于是我开始细细品味man brk的内容,然后,我开始为这过去几个小时浪费的时间忏悔。。。因为那个关于返回值的说明,它....它居然后面还有  But see Linux Notes below.  我去年买了个表!!!

Linux Notes
       The return value described above for brk() is the behavior provided by the glibc wrapper function for the Linux brk() system call.  (On most other  implementations,  the  return
       value  from  brk() is the same; this return value was also specified in SUSv2.)  However, the actual Linux system call returns the new program break on success.  On failure, the
       system call returns the current break.  The glibc wrapper function does some work (i.e., checks whether the new break is less than addr) to provide the 0 and  -1  return  values
       described above.

       On Linux, sbrk() is implemented as a library function that uses the brk() system call, and does some internal bookkeeping so that it can return the old break value.

心得:glibc封装的wrapper与linux的原生系统调用并不是一一对应的,这是因为glibc的wrapper要兼容很多*nix系统,而这些系统的系统调用之间是有一定差异的。所以,如果确定代码以后不会移植到非linux平台,最好还是使用syscall+参数的方式来使用系统调用。


参考链接

http://www.mouseos.com/arch/syscall_sysret.html

http://blog.csdn.net/sadamoo/article/details/8622917#t6

http://gcc.gnu.org/onlinedocs/gcc-4.8.1/gcc/C-Extensions.html#C-Extensions

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!