Linux Kernel Stack


整理一些雜亂的內容。以下x86架構。

Linux 內核棧大小

內核棧大小是固定的,默認為8k,曾經有選項可以設置為4k棧。由於大小固定,申請過大的棧內存,或者函數調用層次過深,都可能導致棧溢出。

關注默認4k還是8k棧,社區曾有過長時間討論。

其中8k棧的缺點如下:

  1. 浪費內存。

  2. 由於內核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
...


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM