轉自:https://www.cnblogs.com/gm-201705/p/9863960.html
整理一些雜亂的內容。以下x86架構。
Linux 內核棧大小
內核棧大小是固定的,默認為8k,曾經有選項可以設置為4k棧。由於大小固定,申請過大的棧內存,或者函數調用層次過深,都可能導致棧溢出。
關注默認4k還是8k棧,社區曾有過長時間討論。
其中8k棧的缺點如下:
-
浪費內存。
-
由於內核4k分頁,要創建一個內核棧就需要申請2塊連續的4k頁。當內存碎片嚴重,尤其內存緊張的時候,申請8k的連續內存,要比4k困難的多。
但貌似4k棧帶來的麻煩更大,內核中許多bug都由4k棧太小,發生溢出導致的。
因此內核從 2.6.37 版本開始,便移除了對4k棧的支持,見 commit : dcfa726280116dd31adad37da940f542663567d0
Linux內核棧布局
棧地址是逆增長的,thread_info 結構位於棧的底部,即低地址處。
top +----------------+
| return vals | | & local vars | | ... | | | | | | 0's | | thread_info | bottom +----------------+
從內核棧布局可以的到,如果在棧上申請內存過多,則會下溢破壞 thread_info 結構。在繞過 pxn 的時候,有一個辦法是修改進程的 addr_limit 值,這個值在 thread_info 中。由於內核棧固定8k的特性,要計算 thread_info 位置,只需要將 sp 指針的后13位清0,即 sp & ~(THREAD_SIZE-1) 即可。
thread_info_addr = sp & ~(THREAD_SIZE-1)
struct thread_info { struct task_struct *task; struct exec_domain *exec_domain; __u32 flags; __u32 status; __u32 cpu; int preempt_count; mm_segment_t addr_limit; struct restart_block restart_block; void __user *sysenter_return; #ifdef CONFIG_X86_32 unsigned long previous_esp; __u8 supervisor_stack[0]; #endif int uaccess_err; };
Stack 使用安全
由申請棧內存過多、過大,或函數調用層次太深導致的溢出問題非常隱蔽,因此這是內核編碼中需注意的地方。同時有許多工具來檢查這類BUG:
1. CONFIG_FRAME_WARN
這是一個內核配置選項,默認為1024,在內核編譯時傳遞給gcc的“-Wframe-larger-than=xxx”選項,當編譯器檢測到棧使用大於闕值時,會產生一條編譯告警:
...
CC ipc/msg.o
CC ipc/sem.o
.../linux-3.0.y/ipc/sem.c: In function 'semctl_main.clone.7': .../linux-3.0.y/ipc/sem.c:1021:1: warning: the frame size of 520 bytes is larger than 256 bytes .../linux-3.0.y/ipc/sem.c: In function 'sys_semtimedop': .../linux-3.0.y/ipc/sem.c:1514:1: warning: the frame size of 472 bytes is larger than 256 bytes CC ipc/shm.o CC ipc/ipcns_notifier.o
2. checkstack.pl
checkstack.pl是內核源碼中的一個Perl腳本,用於執行靜態的棧分析,使用方法如下:
$(CROSS_COMPILE)objdump -d vmlinux | scripts/checkstack.pl [arch]
其中arch支持arm, mips and x86等架構。注意其參數,是一個.S的匯編代碼通過pipe輸入checkstack.pl的
$ arm-eabi-objdummp -d vmlinux -o vmlinux-arm.S $ cat vmlinux-arm.S | scripts/checkstack.pl arm 0x0012c858 nlmclnt_reclaim [vmlinux-arm.o]: 720 0x0025748c do_tcp_getsockopt.clone.11 [vmlinux-arm.o]: 552 0x00258d04 do_tcp_setsockopt.clone.14 [vmlinux-arm.o]: 544 ...
3.CONFIG_DEBUG_STACK_USAGE
同樣是一個內核選項,用於輸出每個進程的棧使用情況。它的原理是在內核棧創建時使用’0’初始化,再通過計算thread_info結構到第一個非0位置的大小,獲取棧使用情況。
可以通過 dmesg 查看棧使用情況:
# dmesg | grep greatest kworker/u:0 used greatest stack depth: 10564 bytes left busybox used greatest stack depth: 9512 bytes left busybox used greatest stack depth: 9504 bytes left grep used greatest stack depth: 9372 bytes left init used greatest stack depth: 9028 bytes left
為什么dmesg中會有棧使用情況,看下CONFIG_DEBUG_STACK_USAGE的具體功能:
- 首先在進程創建時,將進程棧填充為0(kernel/fork.c)
- sysrq ‘t’時,顯示空閑內存大小,這是通過 stack_not_used()調用實現(kernel/sched.c)
- 定義check_stack_usage(),每次low-water時,進行printks打印
- low-water是所有棧全局共享的
- check_stack_usage()只有在進程退出時調用,因此只有在進程退出時才會發現棧使用的問題
- stack_not_used()在include/linux/sched.h文件中定義,他輸出從thread_info到第一個非0位置的內存大小
也可以通過 ‘t’ sysrq,得到當前運行進程棧實時的使用情況:
$ echo t >/proc/sysrq-trigger $ dmesg | grep -v [[] task PC stack pid father init S 802af8b0 932 1 0 0x00000000 kthreadd S 802af8b0 2496 2 0 0x00000000 ksoftirqd/0 S 802af8b0 2840 3 2 0x00000000 kworker/0:0 S 802af8b0 2776 4 2 0x00000000 kworker/u:0 S 802af8b0 2548 5 2 0x00000000 ...
