分析過程基於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
庫創建的線程,其內存是與主線程共享的。