分析過程基於Linux kernel 3.18.120
內核棧
Linux上進程的相關屬性在內核中表示為task_struct,該結構體中stack成員指向進程內核棧的棧底:
struct task_struct { |
我們知道Linux的子進程創建都是通過復制父進程的task_struct來進行的,所以可以從系統的0號進程着手分析進程內核棧的大小;0號進程為init_task:
struct task_struct init_task = INIT_TASK(init_task); |
來看看init_task的stack字段的值:
|
init_task的stack字段實際上指向thread_union聯合體中的thread_info,再來看一下thread_union的結構:
union thread_union { |
所以init_task進程的內核棧就是init_thread_union.stack,而thread_info位於內核棧的棧底;內核棧聲明為unsigned long類型的數組,其實際大小與平台相關,即為THREAD_SIZE的定義;對於arm32平台,它的定義為:
/* arch/arm/include/asm/thread_info.h */ |
而PAGE_SIZE的定義為
/* arch/arm/include/asm/page.h */ |
所以對於arm32平台,PAGE_SIZE大小為4k,THREAD_SIZE大小為8k;此時可以確定 init_task的內核棧大小為8k。
前面提到進程的創建是在內核中拷貝父進程的task_struct,來看一下這部分代碼:
static struct task_struct *dup_task_struct(struct task_struct *orig) |
在復制task_struct的時候,新的task_struct->stack通過alloc_thread_info_node來分配:
static struct thread_info *alloc_thread_info_node(struct task_struct *tsk, |
這里THREAD_SIZE_ORDER為1,所以分配了2個page,所以我們可以確定,進程的內核棧大小為8k。
用戶棧大小
用戶棧虛擬地址空間最大值
通過ulimit命令可以查看當前系統的進程用戶棧的虛擬地址空間上限,單位為kB;
~ # ulimit -s |
即當前系統中,用戶棧的虛擬地址空間上限為8M;為了確認這個值的出處,使用strace,確認ulimit執行過程中,使用了哪些系統調用:
-> % strace sh -c "ulimit -s" |
接着到內核中查找該系統調用的實現,函數名為SYSCALL_DEFINE4(prlimit64, .......)
/* kernel/sys.c */ |
函數的第一個參數為pid,第二個參數為資源的索引;這里可以理解為查找pid為0的進程中,RLIMIT_STACK的值;函數查找到pid對應的task_struct,然后調用do_prlimit
/* kernel/sys.c */ |
do_prlimit的實現為我們指明了到何處去查找RLIMIT_STACK的值,即tsk->signal->rlim + resource;我們知道0號進程為init_task,所以找到init_task->signal->rlim進行確認
/* include/linux/init_task.h */ |
接着找到INIT_RLIMITS宏的定義
/* include/asm-generic/resource.h */ |
_STK_LIM即為當前系統中,進程用戶棧的虛擬地址空間上限:
/* include/uapi/linux/resource.h */ |
當前用戶棧虛擬地址空間大小
可以從proc文件系統中,查看進程的虛擬地址空間分布;以init進程為例,其pid為1,可以通過以下命令查看init進程的虛擬地址空間分布,在arm32平台,內核版本3.18.120,init進程的用戶棧空間大小為132kB:
~ |
仔細觀察會發現,任意進程在啟動后,其棧空間大小基本都是132kB;在分析原因之前,我們先來看一下進程的虛擬地址空間分布:

進程的虛擬地址空間大小為4GB,其中內核空間1GB,用戶空間3GB,在arm32平台上,二者之間存在一個大小為16M的空隙;用戶空間的准確大小為TASK_SIZE:
/* arch/arm/include/asm/memory.h */ |
即用戶空間的地址范圍為0x00000000~0xBEFFFFFF。
上圖左側為用戶空間內的虛擬空間分布,分別為:用戶棧(向下增長),內存映射段(向下增長),堆(向上增長)以及BSS、Data和Text;我們關注的重點在用戶空間中的棧空間。
在Linux系統中,運行二進制需要通過exec族系統調用進行,例如execve、execl、execv等,而這些函數最終都會切換到kernel space,調用do_execve_common(),我們從這個函數開始分析:
static int do_execve_common(struct filename *filename, |
函數中的bprm是類型為struct linux_binprm的結構體,主要用來存儲運行可執行文件時所需要的參數,如虛擬內存空間vma、內存描述符mm、還有文件名和環境變量等信息:
struct linux_binprm { |
接着回到do_execve_common函數,在調用bprm_mm_init初始化內存空間描述符時,第一次為進程的棧空間分配了一個頁:
/* |
這里的vma就是進程的棧虛擬地址空間,這段vma區域的結束地址設置為STACK_TOP_MAX,大小為PAGE_SIZE;這兩個宏的定義如下:
/* arch/arm/include/asm/processor.h */ |
此時,進程的棧空間如下圖所示:

繼續回到do_execve_common()函數,到目前為止,內核還沒有識別到可執行文件的格式,也沒有解析可執行文件中各個段的數據;在exec_binprm()中,會遍歷在內核中注冊支持的可執行文件格式,並調用該格式的load_binary方法來處理對應格式的二進制文件:
/* |
search_binary_handler()會依次調用系統中注冊的可執行文件格式load_binary()方法;load_binary()方法中會自行識別當前二進制格式是否支持;以ELF格式為例,其注冊的load_binary方法為load_elf_binary():
/* fs/binfmt_elf.c */ |
該函數的實現比較復雜,這里我們重點關注setup_arg_pages()函數。
int setup_arg_pages(struct linux_binprm *bprm, |
前面我們已經初始化了一個頁的棧空間,用來存放二進制文件名、參數和環境變量等;在setup_arg_pages()中,我們把前面這一個頁的棧空間移動到stack_top的位置;在調用函數時,stack_top的值是randomize_stack_top(STACK_TOP),即一個隨機地址,這里是為了安全性而實現的棧地址隨機化;函數通過shift_arg_pages()將頁移動到新的地址,移動后的棧如下圖所示:

接着回到setup_arg_pages()函數,關注如下代碼:
stack_expand = 131072UL; /* randomly 32*4k (or 2*64k) pages */ |
expand_stack()函數用來擴展棧虛擬地址空間的大小,stack_base是新的棧基地址,這里的stack_expand是一個固定值,大小為128k,即此處將棧空間擴展128k的大小,擴展后棧空間如下:

所以擴展后的棧虛擬地址空間為4kB+128kB,剛剛好132kB.
棧頂地址隨機化
前面介紹setup_arg_pages()函數移動棧頂的時候提到,出於安全原因,會將棧頂移動到一個隨機的地址:
/* |
這里randomize_stack_top(STACK_TOP)就是將STACK_TOP進行隨機化處理,在我們的平台上。STACK_TOP與STACK_TOP_MAX的值相同,為0xBF000000;我們來分析一下randomize_stack_top()函數:
|
STACK_RND_MASK的值為0x7FF,PAGE_SHIFT為12;第一行將獲取的隨機值范圍限制在0~0x7FF的范圍內;第二行將該值左移12位,這樣得到的隨機數范圍就變成了0~0x7FF000,可以理解為棧頂地址是在一個8MB的范圍內取一個4kB對齊的隨機值。
線程的用戶棧
我們知道在Linux系統上,無論是進程還是線程,都是通過clone系統調用來創建,區別是傳入的參數不同;為了確認創建線程時使用的參數,我准備了一個測試程序,然后使用strace來確認:
|
該程序的strace部分輸出(在x86平台上運行):
clone(child_stack=0x7fd2500d0fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tid=[36747], tls=0x7fd2500d1700, child_tidptr=0x7fd2500d19d0) = 36747 |
我們可以看到調用clone的時候傳入的flags,其中與內存相關最重要的flags是CLONE_VM;接着我們來看內核部分的源碼,仍然從copy_process()函數開始:
/* kernel/fork.c */ |
在copy_mm中,檢查了clone_flags,如果設置了CLONE_VM,那么將當前task_struct->mm指針賦值給新的task_struct->mm;所以我們可以得到結論,通過pthread庫創建的線程,其內存是與主線程共享的。
