用brk實現sbrk,關於brk的返回值

    首先咱們已經知道linux下,malloc最後調用的是sbrk函數,而sbrk是對brk的簡單封裝。 html

    用sbrk模仿malloc很簡單,sbrk(0)獲得當前breakpoint,再調用sbrk(size)便可。(PS:breakpoint表示堆結束地址)linux

    一直以來讓我困惑的是,怎麼用brk去實現sbrk,換句話說,就是隻有brk系統調用,如何能得知當前的breakpoint...難道就沒有人想過這個問題嘛?搜索了各類關鍵字,來來回回都圍繞着sbrk講,算了,本身動手,豐衣足食,咱求人不如求己,仍是本身分析分析好了,glibc中brk的wrapper以下: shell

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

man手冊中對此函數的描述:ubuntu

       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)). vim

RETURN VALUE app

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

能夠看見,這個函數的功能是直接設置breakpoint指針爲addr,成功返回0,失敗-1,並無返回當前breakpoint的功能,難道glibc是本身初始化了一個自覺得正確的起始地址而後以此爲基準分配內存的麼?這也太不靠譜了吧,並且若是有其餘不依賴於glibc的基礎庫若是直接調用brk分配內存了,再調用malloc豈不是天大的杯具?因此確定不是這麼搞的,又或者是還有一個咱不知道的系統調用,做用就是返回當前breakpoint位置?算了,先看看glibc是怎麼實現的再說,遂去官網下了個最新的2.17的glibc,解壓後經過grep找到sbrk定義位於glibc-2.17/misc/sbrk.c中dom


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什麼的,再次祭出grepide

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

相關文章
相關標籤/搜索