文章目录
- 前言
- 一、内核栈
- 二、struct pt_regs
- 2.1 简介
- 2.2 获取pt_regs
- 参考资料
前言
Linux内核栈是用于存储内核执行期间的函数调用和临时数据的一块内存区域。每个运行的进程都有自己的内核栈,用于处理中断、异常、系统调用以及内核函数的执行。
每个进程的内核栈:在Linux中,每个进程都有自己的内核栈。当进程从用户空间切换到内核空间时,它将使用自己的内核栈来执行内核代码。
栈空间分配:内核栈在进程创建时被分配和初始化。在进程切换时,内核会自动切换到相应进程的内核栈。
大小限制:Linux内核栈的大小通常是固定的,取决于架构和编译时的配置。它通常较小,以节省内存空间。
栈溢出:由于内核栈的大小有限,如果在内核执行期间使用过多的栈空间,会导致栈溢出。栈溢出可能会导致系统崩溃或不可预测的行为。因此,内核开发人员需要注意避免在内核代码中使用过多的栈空间。
中断和上下文切换:当发生中断或系统调用时,当前进程的上下文会被保存到其内核栈中,然后切换到内核中断处理程序或系统调用处理程序的上下文。完成处理后,内核将恢复进程的上下文,并继续执行原来的任务。
一、内核栈
linux程序通过系统调用、中断、异常等手段从用户态切换到内核态时,内核中会有各种各样的函数调用:
对于x86_64架构函数调用,前六个参数使用rdi、rsi、rdx、rcx、r8、r9 这 6 个寄存器,用于传递存储函数调用时的 6 个参数。如果超过 6 的时候,需要放到栈里面。然而,前 6 个参数有时候需要进行寻址,但是如果在寄存器里面,是没有地址的,因而还是会放到栈里面,只不过放到栈里面的操作是被调用函数做的。
这样就需要栈来保存函数调用过程的局部变量,函数参数等,这些局部变量,函数参数就是保存在进程的内核栈中 struct task_struct -> stack。
struct task_struct {
......
void *stack;
......
}
Linux 给每个 task 都分配了内核栈,内核栈大小:
union thread_union {
......
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
THREAD_SIZE就表示内核栈的大小。
// linux-5.4.18/arch/x86/include/asm/page_types.h
/* PAGE_SHIFT determines the page size */
#define PAGE_SHIFT 12
// linux-5.4.18/arch/x86/include/asm/page_64_types.h
#ifdef CONFIG_KASAN
#define KASAN_STACK_ORDER 1
#else
#define KASAN_STACK_ORDER 0
#endif
#define THREAD_SIZE_ORDER (2 + KASAN_STACK_ORDER)
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
当没有配置CONFIG_KASAN选项,KASAN_STACK_ORDER = 0,在 PAGE_SIZE 的基础上左移两位,那么内核栈的大小就是4个页面的大小:4 * 4096,即 16 KB.
内核栈是一个非常特殊的结构,如下图所示:
在内核栈的最高地址端,存放的是另一个结构 pt_regs,这个结构体保存着进程从应用层进入到内核层时,用户态寄存器的状态。
我们可以看到在结构 pt_regs上面还有一个内核栈预留空间,这在x86_32位架构的一个遗留问题,在x86_64架构和arm64架构都没有该内核栈预留空间,因此:
获取内核栈:
/*
* When accessing the stack of a non-current task that might exit, use
* try_get_task_stack() instead. task_stack_page will return a pointer
* that could get freed out from under you.
*/
static inline void *task_stack_page(const struct task_struct *task)
{
return task->stack;
}
#define setup_thread_stack(new,old) do { } while(0)
static inline unsigned long *end_of_stack(const struct task_struct *task)
{
return task->stack;
}
task_stack_page 函数用于获取任务的栈页指针,end_of_stack 函数用于获取栈的末尾地址。
二、struct pt_regs
2.1 简介
对于内核栈上面的struct pt_regs结构体,linux程序通过系统调用、中断、异常等手段从用户态切换到内核态时,内核态需要保存用户态的寄存器上下文,通常内核态会在内核态堆栈的最顶端保留一段空间来存储用户态的寄存器上下文,这段空间的存储格式为pt_regs,当进程用内核态切换到用户态时,就会获取pt_regs结构体中的成员,这样就可以获取当进程用户态运行的寄存器上下文状态了。
当系统调用从用户态到内核态的时候,首先要做的第一件事情,就是将用户态运行过程中的 CPU 上下文保存起来,其实主要就是保存在这个结构的寄存器变量里。这样当从内核系统调用返回的时候,才能让进程在刚才的地方接着运行下去。
对于x86_64:
struct pt_regs {
/*
* C ABI says these regs are callee-preserved. They aren't saved on kernel entry
* unless syscall needs a complete, fully filled "struct pt_regs".
*/
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long bp;
unsigned long bx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long ax;
unsigned long cx;
unsigned long dx;
unsigned long si;
unsigned long di;
/*
* On syscall entry, this is syscall#. On CPU exception, this is error code.
* On hw interrupt, it's IRQ number:
*/
unsigned long orig_ax;
/* Return frame for iretq */
unsigned long ip;
unsigned long cs;
unsigned long flags;
unsigned long sp;
unsigned long ss;
/* top of stack page */
};
对于arm64:
/*
* This struct defines the way the registers are stored on the stack during an
* exception. Note that sizeof(struct pt_regs) has to be a multiple of 16 (for
* stack alignment). struct user_pt_regs must form a prefix of struct pt_regs.
*/
struct pt_regs {
union {
struct user_pt_regs user_regs;
struct {
u64 regs[31];
u64 sp;
u64 pc;
u64 pstate;
};
};
u64 orig_x0;
#ifdef __AARCH64EB__
u32 unused2;
s32 syscallno;
#else
s32 syscallno;
u32 unused2;
#endif
u64 orig_addr_limit;
/* Only valid when ARM64_HAS_IRQ_PRIO_MASKING is enabled. */
u64 pmr_save;
u64 stackframe[2];
};
2.2 获取pt_regs
内核通过task_pt_regs宏根据一个task来获取该task的struct pt_regs:
对于x86_64:
#ifdef CONFIG_X86_32
# ifdef CONFIG_VM86
# define TOP_OF_KERNEL_STACK_PADDING 16
# else
# define TOP_OF_KERNEL_STACK_PADDING 8
# endif
#else
# define TOP_OF_KERNEL_STACK_PADDING 0
#endif
TOP_OF_KERNEL_STACK_PADDING为内核栈预留空间,对于x86_64架构该预留空间为0。
#define task_pt_regs(task) \
({ \
unsigned long __ptr = (unsigned long)task_stack_page(task); \
__ptr += THREAD_SIZE - TOP_OF_KERNEL_STACK_PADDING; \
((struct pt_regs *)__ptr) - 1; \
})
宏定义用于获取给定任务(进程)的 pt_regs 结构体指针。
从 task_struct 找到内核栈的开始位置(栈顶)。然后这个位置加上 内核栈的大小THREAD_SIZE ,然后减去这个内核栈的预留空间,对于x86_64内核栈的预留空间为0,就到内核栈的最后的位置(栈顶),此时是struct pt_regs结构体的最后一个成员的ss的地址,而我们要获取的是struct pt_regs结构的首地址,所以要减去一个 struct pt_regs结构体大小,然后转换为 struct pt_regs,再减一,就相当于减少了一个 pt_regs 结构体大小的位置,就到了这个结构的首地址,也就是 struct pt_regs结构体r15成员的位置,r15是struct pt_regs结构的第一个成员。
从这个图我们也可以知道struct pt_regs结构的成员入栈时,是ss最先入栈,r15最后入栈。
我们可以看一下linux程序通过系统调用从用户态切换到内核态时,内核态保存用户态的寄存器上下文时候的压栈顺序:
// linux-5.4.18/arch/x86/entry/entry_64.S
ENTRY(entry_SYSCALL_64)
......
/* Construct struct pt_regs on stack */
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* pt_regs->sp */
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
GLOBAL(entry_SYSCALL_64_after_hwframe)
pushq %rax /* pt_regs->orig_ax */
.......
可以看到是pt_regs->ss成员先压入栈中。
对于arm64获取pt_regs:
#define task_pt_regs(p) \
((struct pt_regs *)(THREAD_SIZE + task_stack_page(p)) - 1)
参考资料
Linux 5.4.18
极客时间:趣谈操作系统