在进程刚开始运行的时候,须要知道运行的环境和用户传递给进程的参数,所以Linux在用户进程运行前,将系统的环境变量和用户给的参数保存到用户虚拟地址空间的栈中,从栈基地址处开始存放。若排除栈基地址随机化的影响,在Linux64bit系统上用户栈的基地址是固定的:
在x86_64通常设置为0x0000_7FFF_FFFF_F000:html
#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的值决定栈的基地址:linux
#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,以提升其搜索速度。dom
Linux 对栈的初始化在系统调用execve中完成,其主要目的有两个:ide
将传递给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压栈。post
比较重要的一步是start_thread(regs, elf_entry, bprm->p);启动用户进程,regs是当前CPU中寄存器的值,elf_entry是用户程序的进入点, bprm->p是用户程序的栈指针,根据这3个参数就能够运行一个新的用户进程了。
start_thread的实现是体系结构相关的,在x86-64上:this
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()函数。.net