整理一些雜亂的內容。以下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
...