X86-64和ARM64用户栈的结构 ---进程用户栈的初始化

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了X86-64和ARM64用户栈的结构 ---进程用户栈的初始化相关的知识,希望对你有一定的参考价值。

用户进程栈的初始化

在进程刚开始运行的时候,需要知道运行的环境和用户传递给进程的参数,因此Linux在用户进程运行前,将系统的环境变量和用户给的参数保存到用户虚拟地址空间的栈中,从栈基地址处开始存放。若排除栈基地址随机化的影响,在Linux64bit系统上用户栈的基地址是固定的:
在x86_64一般设置为0x0000_7FFF_FFFF_F000:

#define STACK_TOP_MAX       TASK_SIZE_MAX
#define TASK_SIZE_MAX   ((1UL << __VIRTUAL_MASK_SHIFT) - PAGE_SIZE)
#define __VIRTUAL_MASK_SHIFT    47

在ARM64上是可以配置的,可以通过配置CONFIG_ARM64_VA_BITS的值决定栈的基地址:

#define STACK_TOP_MAX       TASK_SIZE_64
#define TASK_SIZE_64        (UL(1) << VA_BITS)
#define VA_BITS         (CONFIG_ARM64_VA_BITS)

为了防止利用缓冲区溢出,Linux会对栈的基地址做随机化处理,在开启地址空间布局随机化(Address Space Layout Randomization,ASLR)后, 栈的基地址不是一个固定值。
在介绍Linux如何初始化用户程序栈之前有必要介绍一下虚拟内存区域(Virtual Memory Area, VMA)(还有一篇不错的中文博客), 因为栈也是通过vma管理的,在初始化栈之前会初始化一个用于管理栈的vma,在Linux上,vma用struct vm_area_struct描述,它描述的是一段连续的、具有相同访问属性的虚存空间,该虚存空间的大小为物理内存页面的整数倍, vm_area_struct 中比较重要的成员是vm_start和vm_end,它们分别保存了该虚存空间的首地址和末地址后第一个字节的地址,以字节为单位,所以虚存空间范围可以用[vm_start, vm_end)表示。
由于不同虚拟内存区域的属性不一样,所以一个进程的虚存空间需要多个vm_area_struct结构来描述。在vm_area_struct结构的数目较少的时候,各个vm_area_struct按照升序排序,以单链表的形式组织数据(通过vm_next指针指向下一个vm_area_struct结构)。但是当vm_area_struct结构的数据较多的时候,仍然采用链表组织的化,势必会影响到它的搜索速度。针对这个问题,Linux还使用了红黑树组织vm_area_struct,以提高其搜索速度。
技术分享图片

Linux 对栈的初始化在系统调用execve中完成,其主要目的有两个:

  • 初始化用户栈
  • 将传递给main()函数的参数压栈
    用户栈的建立是伴随着可执行文件的加载建立的,Linux内核中使用linux_binprm管理加载的可执行文件,其定义如下:

    struct linux_binprm {
    char buf[BINPRM_BUF_SIZE];/*文件的头128字节,文件头*/
    struct vm_area_struct *vma;/*用于存储环境变量和参数的空间*/
    unsigned long vma_pages;/*vma中page的个数*/
    struct mm_struct *mm;
    unsigned long p; /* current top of mem,vma管理的内存的顶端 */
    unsigned int recursion_depth; /* only for search_binary_handler() */
    struct file * file;
    struct cred *cred;  /* new credentials */
    int unsafe;     /* how unsafe this exec is (mask of LSM_UNSAFE_*) */
    unsigned int per_clear; /* bits to clear in current->personality */
    int argc, envc; /*参数的数目和环境变量的数目*/
    const char * filename;  /* Name of binary as seen by procps */
    const char * interp;    /* Name of the binary really executed. Most
    of the time same as filename, but could be different for binfmt_{misc,script} */
    unsigned interp_flags;
    unsigned interp_data;
    unsigned long loader, exec;
    
    struct rlimit rlim_stack; /* Saved RLIMIT_STACK used during exec. */
    } __randomize_layout;
SYSCALL_DEFINE3(execve,
        const char __user *, filename,  //可执行文件
        const char __user *const __user *, argv,//命令行的参数
        const char __user *const __user *, envp)//环境变量
{
    return do_execve(getname(filename), argv, envp);
}
int do_execve(struct filename *filename,
    const char __user *const __user *__argv,
    const char __user *const __user *__envp)
{
    struct user_arg_ptr argv = { .ptr.native = __argv };
    struct user_arg_ptr envp = { .ptr.native = __envp };
    return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);
}
static int do_execveat_common(int fd, struct filename *filename,
                  struct user_arg_ptr argv,
                  struct user_arg_ptr envp,
                  int flags)
{
    char *pathbuf = NULL;
    struct linux_binprm *bprm;
    struct file *file;
    struct files_struct *displaced;
    int retval;
    bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
    bprm->interp = bprm->filename;
    retval = bprm_mm_init(bprm); //建立栈的vma
    bprm->argc = count(argv, MAX_ARG_STRINGS);//传给main()函数的argc
    if ((retval = bprm->argc) < 0)
        goto out;
    bprm->envc = count(envp, MAX_ARG_STRINGS); //envc
    if ((retval = bprm->envc) < 0)
        goto out;
    retval = prepare_binprm(bprm);
    if (retval < 0)
        goto out;
    retval = copy_strings_kernel(1, &bprm->filename, bprm);//复制文件名到vma
    if (retval < 0)
        goto out;
    bprm->exec = bprm->p;
    retval = copy_strings(bprm->envc, envp, bprm);//复制环境变量到vma
    if (retval < 0)
        goto out;
    retval = copy_strings(bprm->argc, argv, bprm);//复制参数到vma
    if (retval < 0)
        goto out;
    would_dump(bprm, bprm->file);
    retval = exec_binprm(bprm);  //执行可执行文件

}

通过对Linux代码的研究,用户进程栈的不是一步完成的,大致可以分为三步,一是需要linux建立一个vma用于管理用户栈,vma的建立主要是在bprm_mm_init中完成的,vma->vm_end设置为STACK_TOP_MAX,这时并没有栈随机化的参与,大小为一个PAGE_SIZE。
技术分享图片

接着通过以下三个函数的调用分别把文件名,环境变量、参数复制到栈vma中,

retval = copy_strings_kernel(1, &bprm->filename, bprm);
    if (retval < 0)
        goto out;
    bprm->exec = bprm->p;
    retval = copy_strings(bprm->envc, envp, bprm);
    if (retval < 0)
        goto out;
    retval = copy_strings(bprm->argc, argv, bprm);
    if (retval < 0)
        goto out;

技术分享图片

第三步主要是在exec_binprm->search_binary_handler->load_elf_binary->setup_arg_pages中完成的。这一步会对栈的基地址做随机化,并把已经建立起来vma栈复制到基地址随机化后的栈。
第四步 在函数create_elf_tables中完成,则是分别把argc,指向参数的指针,指向环境变量的指针,elf_info压栈。

比较重要的一步是start_thread(regs, elf_entry, bprm->p);启动用户进程,regs是当前CPU中寄存器的值,elf_entry是用户程序的进入点, bprm->p是用户程序的栈指针,根据这3个参数就可以运行一个新的用户进程了。
技术分享图片
start_thread的实现是体系结构相关的,在x86-64上:

static void
start_thread_common(struct pt_regs *regs, unsigned long new_ip,
            unsigned long new_sp,
            unsigned int _cs, unsigned int _ss, unsigned int _ds)
{
    WARN_ON_ONCE(regs != current_pt_regs());

    if (static_cpu_has(X86_BUG_NULL_SEG)) {
        /* Loading zero below won‘t clear the base. */
        loadsegment(fs, __USER_DS);
        load_gs_index(__USER_DS);
    }

    loadsegment(fs, 0);
    loadsegment(es, _ds);
    loadsegment(ds, _ds);
    load_gs_index(0);

    regs->ip        = new_ip;
    regs->sp        = new_sp;
    regs->cs        = _cs;
    regs->ss        = _ss;
    regs->flags     = X86_EFLAGS_IF;
    force_iret();
}

void
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
{
    start_thread_common(regs, new_ip, new_sp,
                __USER_CS, __USER_DS, 0);
}
在ARM64上:
static inline void start_thread_common(struct pt_regs *regs, unsigned long pc)
{
    memset(regs, 0, sizeof(*regs));
    forget_syscall(regs);
    regs->pc = pc;
}

static inline void start_thread(struct pt_regs *regs, unsigned long pc,
                unsigned long sp)
{
    start_thread_common(regs, pc);
    regs->pstate = PSR_MODE_EL0t;
    regs->sp = sp;
}

技术分享图片

不管是ARM64还是X86-64,都是将新的PC和SP复制给当前的current,然后一路路返回到do_execveat_common,从系统调用中断返回,因为current进程的pc和sp都已经被改变了,会从新的程序入口点elf_entry开始执行,栈也会从bprm->p开始,进程的全新的起点就开始了。新的起点一般不是我们常写的main函数,而是__start,__start就是elf_entry,其会执行一些初始化工作,最后才调用到main()函数。

以上是关于X86-64和ARM64用户栈的结构 ---进程用户栈的初始化的主要内容,如果未能解决你的问题,请参考以下文章

为arm64交叉编译googletest

X86/64架构中的总线 和 ARM架构中总线的理解

Playstore 会拒绝带有 armeabi-v7a、arm64-v8a、x86 但不支持 x86-64 的应用程序吗?

AMD64和X86_64

xecpu架构能装x86-64系统吗

linux查询静态库是arm平台还是x86平台