← Back to Index

mmap在用户空间的处理

mmap是linux中的一个系统调用,它提供了一种进程通过地址映射直接访问内存/文件的机制

当我们调用了mmap,mmap经历了什么?

//头文件:glibc/misc/sys/mman.h
extern void *mmap (void *__addr, size_t __len, int __prot,
           int __flags, int __fd, __off_t __offset) __THROW;

//其定义在:glibc/malloc/memusage.c
void *
mmap (void *start, size_t len, int prot, int flags, int fd, off_t offset)
{
result = (*mmapp)(start, len, prot, flags, fd, offset);//调用了mmapp
}

mmapp = (void *(*)(void *, size_t, int, int, int, off_t))dlsym (RTLD_NEXT,
                                                                  "mmap");

这里的dlsym(RTLD_NEXT, “mmap”), RTLD_NEXT的意思是链接库中第一个(与链接顺序有关)出现的mmap符号,那就调用它(http://www.tecyle.com/2017/03/03/dlsym参数-rtld_next详解/)。看下我们写的一个mmap测试用例,它的链接的库有哪些:

[4/5% h4hu]$ldd a.out
        linux-vdso.so.1 =>  (0x00007ffca6f7c000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f5004edf000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f5004b12000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f50050fb000)

主要关注libc.so.6 和 ld-linux-x86-64.so.2→/lib64/ld-2.17.so。glibc的库是上面代码的本身,所以glibc中调用的就是ld-linux-x86-64.so.2中的mmap。

[4/6% h4hu]$nm /lib64/ld-2.17.so | grep mmap
0000000000019890 t mmap
0000000000019890 t __mmap
0000000000019890 t mmap64
0000000000019890 t __mmap64

mmap调用glibc中的 mmap

weak_alias (__mmap, mmap)

void *
__mmap (void *addr, size_t len, int prot, int flags, int fd, off_t offset)
{
  MMAP_CHECK_PAGE_UNIT ();

  if (offset & MMAP_OFF_LOW_MASK)
    return (void *) INLINE_SYSCALL_ERROR_RETURN_VALUE (EINVAL);

\#ifdef __NR_mmap2
  return (void *) MMAP_CALL (mmap2, addr, len, prot, flags, fd,
                 offset / (uint32_t) MMAP2_PAGE_UNIT);
\#else//x86走这个分支
  return (void *) MMAP_CALL (mmap, addr, len, prot, flags, fd,
                 MMAP_ADJUST_OFFSET (offset));
\#endif
}

# define MMAP_CALL(__nr, __addr, __len, __prot, __flags, __fd, __offset) \
  INLINE_SYSCALL_CALL (__nr, __addr, __len, __prot, __flags, __fd, __offset)

\#define INLINE_SYSCALL_CALL(...) \
  __INLINE_SYSCALL_DISP (__INLINE_SYSCALL, __VA_ARGS__)

\#define __INLINE_SYSCALL_DISP(b,...) \ /*这里的b=__INLINE_SYSCALL*/
  __SYSCALL_CONCAT (b,__INLINE_SYSCALL_NARGS(__VA_ARGS__))(__VA_ARGS__)/*__INLINE_SYSCALL_NARGS这个宏主要用来计算参数个数*/
//那么对于我们调用的__mmap(),它包含了6个参数,所以最终形式为:__INLINE_SYSCALL6(...)

\#define __INLINE_SYSCALL6(name, a1, a2, a3, a4, a5, a6) \
  INLINE_SYSCALL (name, 6, a1, a2, a3, a4, a5, a6)

\#define INLINE_SYSCALL(name, nr, args...)              \
  ({                                    \
    long int sc_ret = INTERNAL_SYSCALL (name, nr, args);        \
    __glibc_unlikely (INTERNAL_SYSCALL_ERROR_P (sc_ret))        \
    ? SYSCALL_ERROR_LABEL (INTERNAL_SYSCALL_ERRNO (sc_ret))     \
    : sc_ret;                               \
  })

\#define SYS_ify(syscall_name)  __NR_#\#syscall_name
\#define INTERNAL_SYSCALL(name, nr, args...)                \
    internal_syscall#\#nr (SYS_ify (name), args)
/*到这里为止的形式为internal_syscall6(__NR_mmap, addr, len, prot, flags, fd, offset)*/
/*__NR_mmap=9,定义在libc 库:sysdeps/unix/sysv/linux/x86_64/64/arch-syscall.h*/

//下面是internal_syscall6的定义,可以看到,参数被放到了寄存器中,然后调用了syscall指令
\#define internal_syscall6(number, arg1, arg2, arg3, arg4, arg5, arg6) \
({                                  \
    unsigned long int resultvar;                    \
    TYPEFY (arg6, __arg6) = ARGIFY (arg6);              \
    TYPEFY (arg5, __arg5) = ARGIFY (arg5);              \
    TYPEFY (arg4, __arg4) = ARGIFY (arg4);              \
    TYPEFY (arg3, __arg3) = ARGIFY (arg3);              \
    TYPEFY (arg2, __arg2) = ARGIFY (arg2);              \
    TYPEFY (arg1, __arg1) = ARGIFY (arg1);              \
    register TYPEFY (arg6, _a6) asm ("r9") = __arg6;            \
    register TYPEFY (arg5, _a5) asm ("r8") = __arg5;            \
    register TYPEFY (arg4, _a4) asm ("r10") = __arg4;           \
    register TYPEFY (arg3, _a3) asm ("rdx") = __arg3;           \
    register TYPEFY (arg2, _a2) asm ("rsi") = __arg2;           \
    register TYPEFY (arg1, _a1) asm ("rdi") = __arg1;           \
    asm volatile (                          \
    "syscall
\t"                         \
    : "=a" (resultvar)                          \
    : "0" (number), "r" (_a1), "r" (_a2), "r" (_a3), "r" (_a4),     \
      "r" (_a5), "r" (_a6)                      \
    : "memory", REGISTERS_CLOBBERED_BY_SYSCALL);            \
    (long int) resultvar;                       \
})

网络上对于这个syscall这个指令是这么解释的:

important

当我们执行syscall机器指令时,MSR_LSTAR寄存器中存放的对应方法就会被执行,即在user space,我们只要执行syscall机器指令,给它对应的系统调用编号和参数,kernel space里对应的系统调用就会被执行了。

我们只关心流程,先不管syscall具体的执行过程。

到syscall指令前为止,实际我们一直在用户程序和glibc中转悠。在看下面的流程前,我们需要建立一些基本概念:

important

1,syscall会把我们从用户空间带到内核空间,这其间会有一个中断

important

2,因为有中断产生,所以系统调用实际是通过中断向量表查找,最后调到内核中对应的系统调用

中断向量表

syscall

到这里,我们尝试来理解下syscall的执行流程。

对于一个中断,典形的处理流程是:中断信号→中断相关的寄存器产生中断信号→pc指针跳转到固定位置(这个固定位置是由中断类形决定的,不同的中断会跳到不同的位置)→找到中断处理函数。

对于系统调用而言,它占用了系统的128号中断,当我们在用户空间调用syscall这个指令时,就会产生这个中断:

# define SYSCALL_VECTOR         0x80

在内核中,中断向量表是由trap_init来完成初始化的(具体的以后再细究),这其中我们可以看到类似这样的代码:

set_system_trap_gate(SYSCALL_VECTOR, &system_call);

显然,这里是把中断号SYSCALL_VECTOR和中断处理函数system_call绑定在一起。再来看system_call,它位于entry_64.S这个文件。省略其他处理,在这个system_call中,会从sys_call_table(函数指针数组)中找到对应的处理。

ENTRY(system_call)
...
call *sys_call_table(,%rax,8)
...
END(system_call)

致此,syscall完成了它的使命。中断处理结束。

sys_call_table

可以看到内核中对sys_call_table初始化是在arch/x86/kernel/syscall_64.c中:

const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
    /*
     * Smells like a compiler bug -- it doesn't work
     * when the & below is removed.
     */
    [0 ... __NR_syscall_max] = &sys_ni_syscall,
\#include <asm/syscalls_64.h> /*注意这个有意思的写法*/
}

从字面意思来看,sys_call_table初始化成{&sys_ni_syscall, asm/syscalls_64.h}这一堆指针。这里的这个头文件比较trick。它实际的路径是arch/x86/include/generated/asm/,generate是编内核的时候生成的路径,其中generate/asm/下的h文件是给内核用的,而generated/uapi则是给用户空间的进程调用的。

__SYSCALL_COMMON(0, sys_read, sys_read)
__SYSCALL_COMMON(1, sys_write, sys_write)
__SYSCALL_COMMON(2, sys_open, sys_open)
__SYSCALL_COMMON(3, sys_close, sys_close)
__SYSCALL_COMMON(4, sys_newstat, sys_newstat)
__SYSCALL_COMMON(5, sys_newfstat, sys_newfstat)
__SYSCALL_COMMON(6, sys_newlstat, sys_newlstat)
__SYSCALL_COMMON(7, sys_poll, sys_poll)
__SYSCALL_COMMON(8, sys_lseek, sys_lseek)
__SYSCALL_COMMON(9, sys_mmap, sys_mmap)
__SYSCALL_COMMON(10, sys_mprotect, sys_mprotect)
__SYSCALL_COMMON(11, sys_munmap, sys_munmap)
__SYSCALL_COMMON(12, sys_brk, sys_brk)
__SYSCALL_64(13, sys_rt_sigaction, sys_rt_sigaction)
...

\#define __SYSCALL_COMMON(nr, sym, compat) __SYSCALL_64(nr, sym, compat)
\#define __SYSCALL_64(nr, sym, compat) [nr] = sym, //这个宏扩展开看,就会变成下面的形式:
========
const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
    /*
     * Smells like a compiler bug -- it doesn't work
     * when the & below is removed.
     */
    [0 ... __NR_syscall_max] = &sys_ni_syscall,
  [0] = sys_read,
  [1] = sys_write,
  [2] = sys_open,
//....
  [9] = sys_mmap,
//....
\#include <asm/syscalls_64.h> /*注意这个有意思的写法*/
}
========

可以看到,sys_call_table这张表中在各个位置放了相应内核中定义的函数指针。到这里,目标已经很明确了,我们要找到内核中定义sys_mmap的函数。

mmap内核处理

经过了一系列艰难的拔涉,我们终于到达了内核的处理。

SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,
        unsigned long, prot, unsigned long, flags,
        unsigned long, fd, unsigned long, off)
{
    long error;
    error = -EINVAL;
    if (off & ~PAGE_MASK)
        goto out;

    error = sys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
out:
    return error;
}

\#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _#\#name, __VA_ARGS__)
\#define SYSCALL_DEFINEx(x, sname, ...)             \
    SYSCALL_METADATA(sname, x, __VA_ARGS__)         \
    __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

//下面的这个宏定义实际上用来定义一个名为SyS#\#name->SyS_mmap的函数。我们通过下面的转换接口,又可以看出
//SyS_mmap其实就是sys_mmap (宏:SYSCALL_ALIAS)
\#define __SYSCALL_DEFINEx(x, name, ...)                    \
    asmlinkage long sys#\#name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
    static inline long SYSC#\#name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
    asmlinkage long SyS#\#name(__MAP(x,__SC_LONG,__VA_ARGS__))  \
    {                               \
        long ret = SYSC#\#name(__MAP(x,__SC_CAST,__VA_ARGS__)); \
        __MAP(x,__SC_TEST,__VA_ARGS__);             \
        __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));   \
        return ret;                     \
    }                               \
    SYSCALL_ALIAS(sys#\#name, SyS##name);               \
    static inline long SYSC#\#name(__MAP(x,__SC_DECL,__VA_ARGS__))

到些为止,我们终于找到了mmap的内核处理。

sys_mmap_pgoff 其实也是一个syscall,也是由SYSCALL_DEFINE6这个宏完成定义的。😎

SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,
        unsigned long, prot, unsigned long, flags,
        unsigned long, fd, unsigned long, pgoff)

参考资料

  1. https://cloud.tencent.com/developer/article/1439038