在進程剛開始運行的時候,須要知道運行的環境和用戶傳遞給進程的參數,所以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