X86-64和ARM64用戶棧的結構 (2) ---進程用戶棧的初始化
在進程剛開始運行的時候,需要知道運行的環境和用戶傳遞給進程的參數,因此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)表示。
Linux 對棧的初始化在系統調用execve中完成,其主要目的有兩個:
- 初始化用戶棧
-
將傳遞給main()函數的參數壓棧
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用戶棧的結構 (2) ---進程用戶棧的初始化