Skip to the content.

kernel stack

关键文档

https://www.kernel.org/doc/html/latest/arch/x86/kernel-stacks.html

x86_64 also has a feature which is not available on i386, the ability to automatically switch to a new stack for designated events such as double fault or NMI, which makes it easier to handle these unusual events on x86_64. This feature is called the Interrupt Stack Table (IST). There can be up to 7 IST entries per CPU. The IST code is an index into the Task State Segment (TSS). The IST entries in the TSS point to dedicated stacks; each stack can be a different size.

stack 的使用

软中断

[!NOTE] 参考 Deepseeek ,有待验证

软中断(Softirq)是中断处理的“下半部”(Bottom Half),它的栈使用情况比较特殊,分为两种情况: 情况A(最常见):当一个硬件中断处理程序(Hard IRQ handler)执行完毕返回时,内核会检查是否有待处理的软中断。如果有,它会立即在同一个CPU上、继续使用当前的中断栈来执行软中断处理函数。这样做效率很高,避免了不必要的栈切换。 情况B(高负载时):如果软中断的负载非常高,不能在中断返回路径上处理完,那么剩余的软中断任务会被一个名为 ksoftirqd 的内核线程接管。ksoftirqd 是一个普通的内核线程,所以当它运行时,它使用的是它自己的进程内核栈。 所以,软中断要么使用中断栈,要么使用ksoftirqd 线程的内核栈

  1. invoke_softirq 中,会根据 CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK 分析到底是使用

config HAVE_IRQ_EXIT_ON_IRQ_STACK

现在,可以买的到硬件都是 CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK ,不去考虑那么多, 也就是 invoke_softirq 继续调用 __do_softirq 就是在 irq stack 上的。

config HAVE_IRQ_EXIT_ON_IRQ_STACK
	bool
	help
	  Architecture doesn't only execute the irq handler on the irq stack
	  but also irq_exit(). This way we can process softirqs on this irq
	  stack instead of switching to a new one when we call __do_softirq()
	  in the end of an hardirq.
	  This spares a stack switch and improves cache usage on softirq
	  processing.
static inline void invoke_softirq(void)
{
	if (!force_irqthreads() || !__this_cpu_read(ksoftirqd)) {
#ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK
		/*
		 * We can safely execute softirq on the current stack if
		 * it is the irq stack, because it should be near empty
		 * at this stage.
		 */
		__do_softirq();
#else
		/*
		 * Otherwise, irq_exit() is called on the task stack that can
		 * be potentially deep already. So call softirq in its own stack
		 * to prevent from any overrun.
		 */
		do_softirq_own_stack();
#endif
	} else {
		wakeup_softirqd();
	}
}

do_softirq_own_stack

do_softirq -> do_softirq_own_stack ,之前一直没有理解 do_softirq_own_stack 的注释的意思, 这个也是 softirq 导致什么 stack 上工作的干扰,其实原理很简单。

#endif


- [ ] 还是和 preempt 有关的,而且是关闭的时候才是如此。

### 硬中断
percpu 一个的

#### 初始化
aarch64 中 CONFIG_IRQSTACKS 默认打开的:

arch/arm64/kernel/irq.c
```c
static void __init init_irq_stacks(void)
{
	int cpu;
	unsigned long *p;

	for_each_possible_cpu(cpu) {
		p = arch_alloc_vmap_stack(IRQ_STACK_SIZE, early_cpu_to_node(cpu));
		per_cpu(irq_stack_ptr, cpu) = p;
	}
}

在 x86 中

arch/x86/kernel/irq.c

DEFINE_PER_CPU_CACHE_HOT(struct irq_stack *, hardirq_stack_ptr);

在用户态触发中断会切换两次 stack ?

从 call_on_irqstack_cond 注释看,如果从用户态进入,task stack 本来就是空的, 所以直接使用 task stack 就可以了

#define call_on_irqstack(func, asm_call, argconstr...)			\
	call_on_stack(__this_cpu_read(pcpu_hot.hardirq_stack_ptr),	\
		      func, asm_call, argconstr)
/*
 * Macro to invoke system vector and device interrupt C handlers.
 */
#define call_on_irqstack_cond(func, regs, asm_call, constr, c_args...)	\
{									\
	/*								\
	 * User mode entry and interrupt on the irq stack do not	\
	 * switch stacks. If from user mode the task stack is empty.	\
	 */								\
	if (user_mode(regs) || __this_cpu_read(pcpu_hot.hardirq_stack_inuse)) { \
		irq_enter_rcu();					\
		func(c_args); 						\
		irq_exit_rcu();						\
	} else {							\
		/*							\
		 * Mark the irq stack inuse _before_ and unmark _after_	\
		 * switching stacks. Interrupts are disabled in both	\
		 * places. Invoke the stack switch macro with the call	\
		 * sequence which matches the above direct invocation.	\
		 */							\
		__this_cpu_write(pcpu_hot.hardirq_stack_inuse, true);	\
		call_on_irqstack(func, asm_call, constr);		\
		__this_cpu_write(pcpu_hot.hardirq_stack_inuse, false);	\
	}								\
}

似乎可以通过看 backtrace 中是否有 call_on_irqstack 来确定是个中断当时 是在内核态触发的还是用户态的触发的:

不过需要注意,这个 call_on_irq_stack 是 arm 环境的函数:

@[
        md_wakeup_thread+0
        end_sync_read+152
        bio_endio+388
        blk_mq_end_request_batch+608
        nvme_pci_complete_batch+100
        nvme_irq+128
        __handle_irq_event_percpu+148
        handle_irq_event+84
        handle_fasteoi_irq+168
        handle_irq_desc+60
        generic_handle_domain_irq+36
        gic_handle_irq+84
        call_on_irq_stack+36 (call_on_irqstack)
        do_interrupt_handler+136
        el1_interrupt+52
        el1h_64_irq_handler+24
        el1h_64_irq+108
        __pi_memcmp+116
        md_thread+196
        kthread+316
        ret_from_fork+16
]: 184

这个是 x86 的,从这里可以看到中断的时候,当时正好是用户态还是内核态:

[
        handle_irq_event+5
        handle_edge_irq+159
        __common_interrupt+77
        common_interrupt+128
        asm_common_interrupt+38
        pv_native_safe_halt+15
        default_idle+19
        default_idle_call+122
        do_idle+480
        cpu_startup_entry+41
        rest_init+348
        start_kernel+1753
        x86_64_start_reservations+36
        x86_64_start_kernel+196
        common_startup_64+318
]: 4305
@[
        handle_irq_event+5
        handle_edge_irq+159
        __common_interrupt+77
        common_interrupt+66
        asm_common_interrupt+38
]: 9764

可以解释 backtrace 为什么可以将中断的打印出来了。

差不多了,也是清楚的。

exception

当前进程上下文中

用户态的 stack

和 context switch 的关系

  1. 程序启动的参数放在哪里?什么栈 环境变量存放的位置在什么地方(CSAPP 提到过,PA 试验, 进程初始化的过程是什么)
  2. 进程切换的时候,stack 是如何切换的
  3. 当 syscall 的时候,stack 如何切换,和上面的切换有什么区别和不同之处
    • 到底切换 stack 麻烦的在于,函数执行需要 stack 的支持,但是切换 stack 是通过调用函数实现的,实现切换的过程真空期如何处理。

问题

vma 的名称

cat /proc/self/maps : vma 有名字吗,他怎么知道是 stack

7ffc83f45000-7ffc83f67000 rw-p 00000000 00:00 0                          [stack]

stack overflow attack

缓冲区在 stack 中间,如果让 stack 不可执行,那么不就结束了。

程序中发生函数调用时,计算机做如下操作:

提高成功率的两种方法 :

  1. 在 shellcode 前面添加填充代码
  2. 在返回值附近重复跳转地址

task_struct 是放到 stack 上的吗?

不是

从 thread_union 看,似乎 task_struct 在 stack 上,检查代码,这个可能只是 init 线程临时使用的:

union thread_union {
	struct task_struct task;
	unsigned long stack[THREAD_SIZE/sizeof(long)];
};

通过 mm/stack.c 中,可以看到 stack 和 task_struct 是可以分离的

struct task_struct {
	/*
	 * For reasons of header soup (see current_thread_info()), this
	 * must be the first element of task_struct.
	 */
	struct thread_info		thread_info;
  // ...
  void				*stack;
  // ...
  /* CPU-specific state of this task: */
	struct thread_struct		thread;
}

从 alloc_thread_stack_node 中 stack 是这样分配的:

	/*
	 * Allocated stacks are cached and later reused by new threads,
	 * so memcg accounting is performed manually on assigning/releasing
	 * stacks to tasks. Drop __GFP_ACCOUNT.
	 */
	stack = __vmalloc_node(THREAD_SIZE, THREAD_ALIGN,
				     THREADINFO_GFP & ~__GFP_ACCOUNT,
				     node, __builtin_return_address(0));

而 task_struct 是通过 slab 机制分配的

static inline struct task_struct *alloc_task_struct_node(int node)
{
	return kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node);
}

所以,可以通过 /sys/kernel/slab/task_struct 获取 task_struct 分配的详细信息。

获取 task_struct

之前一些误导,似乎 task_struct 需要借助其他的方法才可以获取到,但是实际上,很简单:

static __always_inline struct task_struct *get_current(void)
{
	if (IS_ENABLED(CONFIG_USE_X86_SEG_SUPPORT))
		return this_cpu_read_const(const_current_task);

	return this_cpu_read_stable(current_task);
}

#define current get_current()

VMAP_STACK

config VMAP_STACK
	default y
	bool "Use a virtually-mapped stack"
	depends on HAVE_ARCH_VMAP_STACK
	depends on !KASAN || KASAN_HW_TAGS || KASAN_VMALLOC
	help
	  Enable this if you want the use virtually-mapped kernel stacks
	  with guard pages.  This causes kernel stack overflows to be
	  caught immediately rather than causing difficult-to-diagnose
	  corruption.

	  To use this with software KASAN modes, the architecture must support
	  backing virtual mappings with real shadow memory, and KASAN_VMALLOC
	  must be enabled.

shadow stack

-cpu host 直通的时候 guest os 还是没有

但是 host 上是有这个的:

CET_SS: CET shadow stack                 = false

似乎是 2023 才进入的 feature

History:        #0
Commit:         18e66b695e787374ca762ecdeaa1ab5e3772af94
Author:         Rick Edgecombe <rick.p.edgecombe@intel.com>
Author Date:    Tue 13 Jun 2023 08:10:32 AM CST
Committer Date: Wed 12 Jul 2023 05:12:18 AM CST

x86/shstk: Add Kconfig option for shadow stack

Shadow stack provides protection for applications against function return
address corruption. It is active when the processor supports it, the
kernel has CONFIG_X86_SHADOW_STACK enabled, and the application is built
for the feature. This is only implemented for the 64-bit kernel. When it
is enabled, legacy non-shadow stack applications continue to work, but
without protection.

Since there is another feature that utilizes CET (Kernel IBT) that will
share implementation with shadow stacks, create CONFIG_CET to signify
that at least one CET feature is configured.

Co-developed-by: Yu-cheng Yu <yu-cheng.yu@intel.com>
Signed-off-by: Yu-cheng Yu <yu-cheng.yu@intel.com>
Signed-off-by: Rick Edgecombe <rick.p.edgecombe@intel.com>
Signed-off-by: Dave Hansen <dave.hansen@linux.intel.com>
Reviewed-by: Borislav Petkov (AMD) <bp@alien8.de>
Reviewed-by: Kees Cook <keescook@chromium.org>
Acked-by: Mike Rapoport (IBM) <rppt@kernel.org>
Tested-by: Pengfei Xu <pengfei.xu@intel.com>
Tested-by: John Allen <john.allen@amd.com>
Tested-by: Kees Cook <keescook@chromium.org>
Link: https://lore.kernel.org/all/20230613001108.3040476-7-rick.p.edgecombe%40intel.com

原来,VM_SHADOW_STACK 是这个含义啊:

pte_t pte_mkwrite(pte_t pte, struct vm_area_struct *vma)
{
	if (vma->vm_flags & VM_SHADOW_STACK)
		return pte_mkwrite_shstk(pte);

	pte = pte_mkwrite_novma(pte);

	return pte_clear_saveddirty(pte);
}

https://docs.kernel.org/next/x86/shstk.html

SDM vol3 26.4.3 Shadow-Stack Updates

非常奇怪,即便是将 -cpu host ,l1 中也是看不到 CET 的了

CONFIG_DEBUG_STACK_USAGE

观察到虚拟机才有这个,在 nixos 的物理机中从来没有这个问题

[   10.053291] 10.0.0.2-manage (2288) used greatest stack depth: 12176 bytes left
[   10.107265] mount.nfs (2281) used greatest stack depth: 10576 bytes left
[  260.502203] rg (4669) used greatest stack depth: 10552 bytes left

原因是打开了 CONFIG_DEBUG_STACK_USAGE

TODO

call_on_irqstack_cond 中的 stack 测试一下

控制 virtio dummy 的中断亲和性,似乎不难,但是到此为止

应该是无论是在用户态还是内核态,中断的时候 stack 都是 CPU 对应的 stack

map_irq_stack 中设置 stack 的位置是:

$1 = (void *) 0xffffc90000000000

0xffffc900000c8f58 <—- interrupt 0xffffc90000d63d30 <—- exception

0xffffc90001253f18 <—– syscall 的

对于 double fault 之类的,stack 在其他的位置: https://www.kernel.org/doc/Documentation/x86/kernel-stacks

Like the split thread and interrupt stacks on i386, this gives more room for kernel interrupt processing without having to increase the size of every per thread stack.

用户态中断存在两次转换?

https://unix.stackexchange.com/questions/491437/how-does-linux-kernel-switches-from-kernel-stack-to-interrupt-stack 这个回答应该是误导人的吧? 分析 asm_common_interrupt 以及 asm_exc_page_fault 中,都是直接就开始上下文保存

https://stackoverflow.com/questions/38360312/how-does-linux-kernel-switch-between-user-mode-and-kernel-mode-stack

总结:

signal handler 的 stack

man signal(7)

A process can change the disposition of a signal using sigaction(2) or signal(2).

本站所有文章转发 CSDN 将按侵权追究法律责任,其它情况随意。