1. 程式人生 > >X86-64和ARM64用戶棧的結構 (2) ---進程用戶棧的初始化

X86-64和ARM64用戶棧的結構 (2) ---進程用戶棧的初始化

.com roc mis goto war dump images thread prepare

用戶進程棧的初始化

在進程剛開始運行的時候,需要知道運行的環境和用戶傳遞給進程的參數,因此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用戶棧的結構 (2) ---進程用戶棧的初始化