Linux: ARM32 各 CPU 模式下栈配置

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 背景

本文基于 ARM32 架构 + Linux 4.14 内核源码 进行分析。

3. ARM32 中断向量表 和 中断处理流程

看到这里,读者可能产生了疑问,栈的使用和中断有什么关系?这里先给出答案:应用的 各CPU 的内核模式栈(即 CPU SVC 模式栈),是在系统启动阶段进行配置;应用的用户栈(即 CPU User 模式栈),是在系统创建应用进程时进行配置;而除 CPU SVC/User 模式的栈外,其它异常模式的栈,在系统启动阶段为每个CPU配置了一个很小空间的栈,但是这个小空间,对于处理异常是不够的,于是在进入各异常模式向量后、在正式处理异常之前,将 CPU 模式切换到 SVC 模式,进而借用 CPU SVC 模式栈(即各 CPU 的内核栈)进行异常处理。有一个例外的是 ARM32 CPU 的 System 模式不被 Linux 使用,所以也不涉及到栈的配置。
由于在 CPU 异常模式下进行了栈配置,自然就涉及到中断处理流程,所以在这里先简单介绍下 ARM32 中断向量的组织结构,以及中断的处理简要流程。

3.1 ARM32 中断向量表

在 ARM32 架构的 Linux 内核代码,中断向量组织可以认为是一个二维数组 vectors[8][16]vectors[8][16] 数组的第一维索引是如下CPU 异常模式
在这里插入图片描述
vectors[8][16] 数组的第二维索引是如下CPU 模式的低4位
在这里插入图片描述
注意,第二维索引 是以上图中 Mode number ,即以 ARM32 CPU 模式的低4位为索引,ARM32 CPU 没有实现所有 5 位 CPU 模式的所有位模式组合,因此向量表的有些第二维的入口是非法的,定义为 __irq_invalid 。来看 ARM32 CPU 中断向量表 的具体定义,先看向量表第一维的组织形式:

/* arch/arm/kernel/entry-armv.S */

	/* 中断向量表第一维:按 CPU 异常模式 组织 */
	.section .vectors, "ax", %progbits
.L__vectors_start:
	W(b)	vector_rst
	W(b)	vector_und
	W(ldr)	pc, .L__vectors_start + 0x1000
	W(b)	vector_pabt
	W(b)	vector_dabt
	W(b)	vector_addrexcptn
	W(b)	vector_irq
	W(b)	vector_fiq

再看向量表的第二维组织形式:

/* arch/arm/kernel/entry-armv.S */

/* reset 异常向量表 */
vector_rst:
 ARM(	swi	SYS_ERROR0	)
 THUMB(	svc	#0		)
 THUMB(	nop			)
	b	vector_und

/* Undefined 异常向量表 */
vector_stub	und, UND_MODE /* vector_und */
	.long	__und_usr			@  0 (USR_26 / USR_32) /* User 模式 未定义指令异常 入口 */
	.long	__und_invalid			@  1 (FIQ_26 / FIQ_32)
	.long	__und_invalid			@  2 (IRQ_26 / IRQ_32)
	.long	__und_svc			@  3 (SVC_26 / SVC_32) /* SVC 模式 未定义指令异常 入口 */
	.long	__und_invalid			@  4
	.long	__und_invalid			@  5
	.long	__und_invalid			@  6
	.long	__und_invalid			@  7
	.long	__und_invalid			@  8
	.long	__und_invalid			@  9
	.long	__und_invalid			@  a
	.long	__und_invalid			@  b
	.long	__und_invalid			@  c
	.long	__und_invalid			@  d
	.long	__und_invalid			@  e
	.long	__und_invalid			@  f

/* 预取(Prefetch)异常向量表 */
vector_stub	pabt, ABT_MODE, 4 /* vector_pabt */
	.long	__pabt_usr			@  0 (USR_26 / USR_32)
	.long	__pabt_invalid			@  1 (FIQ_26 / FIQ_32)
	.long	__pabt_invalid			@  2 (IRQ_26 / IRQ_32)
	.long	__pabt_svc			@  3 (SVC_26 / SVC_32)
	.long	__pabt_invalid			@  4
	.long	__pabt_invalid			@  5
	.long	__pabt_invalid			@  6
	.long	__pabt_invalid			@  7
	.long	__pabt_invalid			@  8
	.long	__pabt_invalid			@  9
	.long	__pabt_invalid			@  a
	.long	__pabt_invalid			@  b
	.long	__pabt_invalid			@  c
	.long	__pabt_invalid			@  d
	.long	__pabt_invalid			@  e
	.long	__pabt_invalid			@  f

/* 数据异常(Data Abort)向量表 */
vector_stub	dabt, ABT_MODE, 8 /* vector_dabt */
	.long	__dabt_usr			@  0  (USR_26 / USR_32)
	.long	__dabt_invalid			@  1  (FIQ_26 / FIQ_32)
	.long	__dabt_invalid			@  2  (IRQ_26 / IRQ_32)
	.long	__dabt_svc			@  3  (SVC_26 / SVC_32)
	.long	__dabt_invalid			@  4
	.long	__dabt_invalid			@  5
	.long	__dabt_invalid			@  6
	.long	__dabt_invalid			@  7
	.long	__dabt_invalid			@  8
	.long	__dabt_invalid			@  9
	.long	__dabt_invalid			@  a
	.long	__dabt_invalid			@  b
	.long	__dabt_invalid			@  c
	.long	__dabt_invalid			@  d
	.long	__dabt_invalid			@  e
	.long	__dabt_invalid			@  f

/* 地址异常(Address Exception)向量表 */
vector_addrexcptn:
	b	vector_addrexcptn
	
/* IRQ 异常向量表 */
vector_stub	irq, IRQ_MODE, 4 /* vector_irq */
	.long	__irq_usr			@  0  (USR_26 / USR_32)
	.long	__irq_invalid			@  1  (FIQ_26 / FIQ_32)
	.long	__irq_invalid			@  2  (IRQ_26 / IRQ_32)
	.long	__irq_svc			@  3  (SVC_26 / SVC_32)
	.long	__irq_invalid			@  4
	.long	__irq_invalid			@  5
	.long	__irq_invalid			@  6
	.long	__irq_invalid			@  7
	.long	__irq_invalid			@  8
	.long	__irq_invalid			@  9
	.long	__irq_invalid			@  a
	.long	__irq_invalid			@  b
	.long	__irq_invalid			@  c
	.long	__irq_invalid			@  d
	.long	__irq_invalid			@  e
	.long	__irq_invalid			@  f

/* FIQ 异常向量表 */
vector_stub	fiq, FIQ_MODE, 4 /* vector_fiq */
	.long	__fiq_usr			@  0  (USR_26 / USR_32)
	.long	__fiq_svc			@  1  (FIQ_26 / FIQ_32)
	.long	__fiq_svc			@  2  (IRQ_26 / IRQ_32)
	.long	__fiq_svc			@  3  (SVC_26 / SVC_32)
	.long	__fiq_svc			@  4
	.long	__fiq_svc			@  5
	.long	__fiq_svc			@  6
	.long	__fiq_abt			@  7
	.long	__fiq_svc			@  8
	.long	__fiq_svc			@  9
	.long	__fiq_svc			@  a
	.long	__fiq_svc			@  b
	.long	__fiq_svc			@  c
	.long	__fiq_svc			@  d
	.long	__fiq_svc			@  e
	.long	__fiq_svc			@  f

3.2 ARM32 中断处理流程

发生中断异常后,系统自动将执行流程跳转到中断上述向量表 .L__vectors_start 中,对应异常模式的向量表入口,如发生了 IRQ 中断,则会进入到向量表 .L__vectors_start 中 vector_irq 向量表项;然后再根据 CPU 的模式是 SVC 还是 User ,分别跳转到第二维中断向量表 vector_stub irq, IRQ_MODE, 4__irq_svc(SVC模式)__irq_usr(User 模式) 入口进行执行;在处理完中断后,再返回到被中断的程序继续执行。这就是中断执行的主干流程,更多关于中断处理流程的细节,可参考 Linux: 中断实现简析

4. ARM32 各 CPU 模式下的栈配置

4.1 SVC 模式下各 CPU 栈配置(内核栈配置)

4.1.1 BOOT CPU SVC 模式栈配置(内核栈配置)

/* arch/arm/include/asm/thread_info.h */

#define THREAD_SIZE_ORDER 1
#define THREAD_SIZE  (PAGE_SIZE << THREAD_SIZE_ORDER) /* 如 PAGE_SIZE=4KB, 则 THREAD_SIZE=8KB */
#define THREAD_START_SP  (THREAD_SIZE - 8) /* 8字节 Guard Magic */

/* arch/arm/kernel/head-common.S */

	__INIT
__mmap_switched: /* 此处代码运行于 MMU 开启状况 */
	adr	r3, __mmap_switched_data /* r3 = __mmap_switched_data 虚拟地址 */
	...

	/*
	 * r4 = &processor_id (arch/arm/kernel/setup.c)
	 * r5 = &__machine_arch_type (arch/arm/kernel/setup.c)
	 * r6 = &__atags_pointer (arch/arm/kernel/setup.c)
	 * r7 = &cr_alignment (arch/arm/kernel/entry-armv.S)
	 * sp = 当前 CPU 的 swapper 进程内核栈指针
	 */
	 ARM(	ldmia	r3, {r4, r5, r6, r7, sp})

	...
	b	start_kernel /* 跳转到 start_kernel() 执行 */

	align	2
	.type	__mmap_switched_data, %object
__mmap_switched_data:
	...
	/* 当前 CPU 的 swapper 进程内核栈指针 */
	.long	init_thread_union + THREAD_START_SP @ sp
	.size	__mmap_switched_data, . - __mmap_switched_data
/* include/linux/sched.h */

union thread_union {
#ifndef CONFIG_THREAD_INFO_IN_TASK
	struct thread_info thread_info;
#endif
	unsigned long stack[THREAD_SIZE/sizeof(long)];
};

/* init/init_task.c */

/*
 * Initial thread structure. Alignment of this is handled by a special
 * linker map entry.
 */
union thread_union init_thread_union __init_task_data = {
#ifndef CONFIG_THREAD_INFO_IN_TASK
	INIT_THREAD_INFO(init_task)
#endif
};

可以看到,BOOT CPU SVC 模式下的内核栈(即 BOOT CPU swapper/idle 进程的内核栈),来自 init_taskunion thread_union 数据 init_thread_union

4.1.2 非 BOOT CPU SVC 模式栈配置(内核栈配置)

/*
 * 非 BOOT CPU SVC 模式栈创建,是在它们各自的 swapper/idle 进程创建时建立的。
 */

kernel_init()
	kernel_init_freeable()
		smp_init() /*  启动其它 非 BOOT CPU */
			idle_threads_init();

/* kernel/smpboot.c */
void __init idle_threads_init(void)
{
	unsigned int cpu, boot_cpu;

	boot_cpu = smp_processor_id();

	for_each_possible_cpu(cpu) {
		if (cpu != boot_cpu)
			idle_init(cpu);
	}
}

static inline void idle_init(unsigned int cpu)
{
	struct task_struct *tsk = per_cpu(idle_threads, cpu);

	if (!tsk) {
		tsk = fork_idle(cpu); /* 为 非 BOOT CPU 创建 idle/swapper 进程 */
		if (IS_ERR(tsk))
			pr_err("SMP: fork_idle() failed for CPU %u\n", cpu);
		else
			/* 设定 非 BOOT CPU 的 idle/swapper 进程对象 (struct task_struct) */
			per_cpu(idle_threads, cpu) = tsk; 
		}
}

/* kernel/fork.c */
fork_idle()
	struct task_struct *task;
	task = copy_process(CLONE_VM, 0, 0, NULL, &init_struct_pid, 0, 0, cpu_to_node(cpu));
		...
		struct task_struct *p;
		...
		p = dup_task_struct(current, node);
			struct task_struct *tsk;
			...
			tsk = alloc_task_struct_node(node);
			...
			stack = alloc_thread_stack_node(tsk, node); /* 分配进程 内核栈空间(含 struct thread_info) */
			...
			tsk->stack = stack; /* 设置进程 内核栈空间(含 struct thread_info) */
			...
	...

	return task;
/* arch/arm/kernel/smp.c */

struct secondary_data secondary_data;

...

/* 非 BOOT CPU 启动流程中 */
__cpu_up(cpu, idle)
	...
	
	/* 配置 @cpu (非 BOOT CPU) 的首进程 idle/swapper 的内核栈空间,即前面 fork_idle() 时分配的栈空间 */
	secondary_data.stack = task_stack_page(idle) + THREAD_START_SP;

	...

之后的 非 BOOT CPU 启动流程__cpu_up() -> ... -> secondary_startup

/* arch/arm/kernel/head.S */

ENTRY(secondary_startup)
	...
	
	/*
	 * Use the page tables supplied from  __cpu_up.
	 */
	adr	r4, __secondary_data /* r4 = __secondary_data 的当前物理地址 */
	/*
	 * r5 = __secondary_data 的链接虚拟地址
	 * r7 = secondary_data 的链接虚拟地址
	 * r12 = __secondary_switched 的链接虚拟地址
	 */
	ldmia	r4, {r5, r7, r12}		@ address to jump to after
	...
	/* r13 = __secondary_switched 的链接虚拟地址, __enable_mmu 后跳转到此处执行 */
	mov	r13, r12			@ __secondary_switched address
	...
ENDPROC(secondary_startup)

之后会经历 secondary_startup -> __turn_mmu_on

/* arch/arm/kernel/head.S */

ENTRY(__turn_mmu_on)
	...
	/*
	 * BOOT CPU: r13 = __mmap_switched
	 * 非 BOOT CPU: r13 = __secondary_switched 的链接虚拟地址
	 */
	mov	r3, r13
	/*
	 * BOOT CPU: 返回到 __mmap_switched 处
	 * 非 BOOT CPU: 返回到 __secondary_switched 处
	 */
	ret	r3
__turn_mmu_on_end:

ENTRY(__secondary_switched)
	/* 配置非 BOOT CPU SVC 模式栈(即内核栈),即前面 fork_idle() 时分配的栈空间 */
	ldr	sp, [r7, #12]			@ get secondary_data.stack
	mov	fp, #0
	b	secondary_start_kernel
ENDPROC(__secondary_switched)

4.2 中断异常模式下各 CPU 栈配置

4.2.1 系统启动阶段的中断异常模式下各 CPU 栈配置

start_kernel()
	setup_arch()
		setup_processor()
			cpu_init()
/* arch/arm/kernel/setup.c */

void notrace cpu_init(void)
{
#ifndef CONFIG_CPU_V7M
	unsigned int cpu = smp_processor_id();
	struct stack *stk = &stacks[cpu];

	if (cpu >= NR_CPUS) {
		pr_crit("CPU%u: bad primary CPU number\n", cpu);
		BUG();
	}

	/*
	 * This only works on resume and secondary cores. For booting on the
	 * boot cpu, smp_prepare_boot_cpu is called after percpu area setup.
	 */
	set_my_cpu_offset(per_cpu_offset(cpu)); // TODO: per CPU 相关

	/*
	 * arch/arm/mm/proc-v7.S, cpu_v7_proc_init
	 * ...
	 */
	cpu_proc_init();

	/*
	 * Define the placement constraint for the inline asm directive below.
	 * In Thumb-2, msr with an immediate value is not allowed.
	 */
#ifdef CONFIG_THUMB2_KERNEL
#define PLC	"r"
#else
#define PLC	"I"
#endif

	/*
	 * setup stacks for re-entrant exception handlers
	 */
	__asm__ (
	"msr	cpsr_c, %1\n\t" /* CPU 切换到 IRQ 模式 */
	"add	r14, %0, %2\n\t"
	"mov	sp, r14\n\t" /* 设置 IRQ 模式堆栈 */
	"msr	cpsr_c, %3\n\t" /* CPU 切换到 Abort 模式 */
	"add	r14, %0, %4\n\t"
	"mov	sp, r14\n\t" /* 设置 ABT 模式堆栈 */
	"msr	cpsr_c, %5\n\t" /* CPU 切换到 Undefined 模式 */
	"add	r14, %0, %6\n\t"
	"mov	sp, r14\n\t" /* 设置 UND 模式堆栈 */
	"msr	cpsr_c, %7\n\t" /* CPU 切换到 FIQ 模式 */
	"add	r14, %0, %8\n\t"
	"mov	sp, r14\n\t" /* 设置 FIQ 模式堆栈 */
	"msr	cpsr_c, %9" /* CPU 切回 SVC 模式 */
	    :
	    : "r" (stk), /* %0 */
	      PLC (PSR_F_BIT | PSR_I_BIT | IRQ_MODE), /* %1 */
	      "I" (offsetof(struct stack, irq[0])), /* %2 */
	      PLC (PSR_F_BIT | PSR_I_BIT | ABT_MODE), /* %3 */
	      "I" (offsetof(struct stack, abt[0])), /* %4 */
	      PLC (PSR_F_BIT | PSR_I_BIT | UND_MODE), /* %5 */
	      "I" (offsetof(struct stack, und[0])), /* %6 */
	      PLC (PSR_F_BIT | PSR_I_BIT | FIQ_MODE), /* %7 */
	      "I" (offsetof(struct stack, fiq[0])), /* %8 */
	      PLC (PSR_F_BIT | PSR_I_BIT | SVC_MODE) /* %9 */
	    : "r14");
#endif
}

上面的代码,为每个 CPU 的各异常模式 IRQ, Abort, Undefined, FIQ ,配置了定义在 stacks[] 中的栈空间,看一下 stacks[] 定义:

/*
 * 除 User,System,SVC 3个模式外的堆栈. 
 * User,SVC 3个模式的堆栈,分别由应用程序,或 各 CPU 启动阶段各自设置,
 * 而 Linux 内核模式不适用 System 模式。
 */
struct stack {
	u32 irq[3];
	u32 abt[3];
	u32 und[3];
	u32 fiq[3];
} ____cacheline_aligned;

#ifndef CONFIG_CPU_V7M
static struct stack stacks[NR_CPUS]; /* 每 CPU、每模式(除 SVC/System/User 模式外)的 堆栈 */
#endif

看到了吧,每个 CPU 的各异常模式的栈空间大小3 个 u32 大小,总共 12 字节,对于异常处理流程,这个真是太小了。我们继续看下一小节,看 ARM32 Linux 内核是怎么处理这个问题的。

4.2.2 中断异常发生时各异常模式 CPU 栈配置

异常发生是,会跳转到中断向量表中去执行。假设发生了一个 IRQ 中断,将跳转到 vector_irq 处执行:

vector_stub	irq, IRQ_MODE, 4 /* vector_irq */
	.long	__irq_usr			@  0  (USR_26 / USR_32)
	.long	__irq_invalid			@  1  (FIQ_26 / FIQ_32)
	.long	__irq_invalid			@  2  (IRQ_26 / IRQ_32)
	.long	__irq_svc			@  3  (SVC_26 / SVC_32)
	...

汇编宏 vector_stub 定义了 vector_irq 中断向量,但关于 vector_stub 一些至关重要的细节,我们在前面没有展开,这里补充一下:

	.macro	vector_stub, name, mode, correction=0
	.align	5

vector_\name:
	.if \correction
	sub	lr, lr, #\correction
	.endif

	/*
	 * 刚进入中断异常时,使用的是各 CPU 模式独立的栈,除 User/SVC/System
	 * 模式的栈外,其它各 CPU 模式的栈在启动阶段配置:
	 * start_kernel()
	 *		setup_arch()
	 *			setup_processor()
	 *				cpu_init()
	 *					// static struct stack stacks[NR_CPUS]; 
	 * User 模式的栈,由 C 启动代码初始化;
	 * SVC 模式的栈是各 CPU 在启动阶段,或进程切换时,设置为 进程内核栈;
	 * 另外,Linux 不使用 System 模式。
	 * 可以看到,各 CPU 模式启动初始化时在 cpu_init() 中设置的栈:
	 * struct stack {
	 *		u32 irq[3];
	 *		u32 abt[3];
	 *		u32 und[3];
	 *		u32 fiq[3];
	 * } ____cacheline_aligned;
	 * CPU 各模式(除 User/System/SVC 模式外)仅有 3 个 u32 的空间,从
	 * 下面的代码可以看到,这 3 个 u32 用来存储 r0(r0 被所有模式共用)
	 * 和 CPU 异常模式的 {lr_<exception>, spsr_<exception>} 。
	 * 如进入 IRQ 中断模式,则:
	 * stack::irq[0] = r0 (被 IRQ 中断时 r0 的值, r0 所有 CPU 模式共用)
	 * stack::irq[1] = lr_irq
	 * stack::irq[2] = spsr_irq (被 IRQ 中断的 CPU 模式的 cpsr, 如 cpsr_svc,cpsr_usr 等)
	 * 就这样,当前异常模式下的 3 个 u32 栈空间,已经被消耗光了,接下
	 * 来处理异常过程中还要使用栈空间,该怎么办? 答案是,代码将 CPU 切
	 * 换到 CPU SVC 模式,复用 CPU SVC 模式的栈空间。
	 * 
	 * 这里的 sp 为 sp_<exception>.
	 */
	stmia	sp, {r0, lr}		@ save r0, lr
	/*
	 * lr_<exception> = spsr_<exception>
	 * 
	 * 如 IRQ 中断了 SVC 或 User 模式, 则为:
	 * lr_irq = spsr_irq (cpsr_svc/cpsr_usr) 
	 */
	mrs	lr, spsr
	/*
	 * 如 IRQ 中断了 SVC 或 User 模式, 则为:
	 * stack::irq[2] = spsr_irq (cpsr_svc/cpsr_usr)
	 */
	str	lr, [sp, #8]		@ save spsr

	/*
	 * 为从 异常模式 切换到 SVC 模式 做准备.
	 * 异常模式的 3 个 u32 栈空间已经用完了, 在跳入异常处理接口时,
	 * 切换到 SVC 模式,然后复用 SVC 模式的栈空间 (即被中断进程的
	 * 内核栈空间), 以做进一步的异常处理.
	 *
	 * PSR_ISETSTATE 为指令集模式: ARM 内核为 0, Thumb 内核为 PSR_T_BIT.
	 */
	mrs	r0, cpsr
	eor	r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)
	msr	spsr_cxsf, r0 /* 切换到 SVC 模式 */

	/*
	 * 析取 被异常中断的 CPU 模式, 如 SVC / User.
	 * 提取 spsr_<exception> 低 4 位,spsr_<exception> 存有
	 * 被异常中断的 CPU 模式 的 cpsr 寄存器的拷贝, 如 cpsr_svc, cpsr_usr.
	 * 析取的 被异常中断的 CPU 模式, 用来索引异常模式向量表 .vectors, 
	 * 获得异常模式处理程序入口指针.
	 */
	and	lr, lr, #0x0f /* 提取 被中断的 CPU 模式 (如 SVC / User) */

	/*
	 * 将 r0 传给 <exception> handler, <exception> handler
	 * 会用到保存在 <exception> 栈上保存的 r0, lr, spsr 寄存器.
	 */
	mov	r0, sp /* r0 = sp_<exception> */
	/* 
	 * 定位异常向量表入口, 根据前面的代码:
	 * and	lr, lr, #0x0f // 被中断的 CPU 模式
	 * 以及此处使用
	 * pc + (lr << 2) // 每个向量入口占 1<<2=4 字节
	 * 来作为异常向量入口,因为此时 pc 指向下一条指令
	 * movs	pc, lr
	 * 所以这要求当前异常模式的向量表,必须紧跟着
	 * movs	pc, lr
	 * 指令,中间不能存在任何空隙。
	 */
	/* lr = CPU 当前异常模式向量入口 (如 __und_svc / __und_usr / __irq_usr / __irq_svc) */
 ARM(	ldr	lr, [pc, lr, lsl #2]	)
 	/*
	 * 切换到 SVC 模式, 并跳转到异常向量入口执行. 
	 * 如 IRQ 中断了 User / SVC 模式, 则跳转到
	 * __und_svc / __und_usr / __irq_usr / __irq_svc
	 * 切换到 SVC 模式, CPU 的 IRQ 中断仍然保持禁用状态.
	 *
	 * 带 s 后缀 的 mov 指令, 会附带的将当前异常模式的 spsr_<exception>
	 * 拷贝到被异常中断模式的 cpsr (如 cpsr_svc, cpsr_usr), 即这里不仅会
	 * 发生跳转到异常处理指针地址执行, 还会发生 CPU 模式切换, 即这条指令
	 * 效果相当于:
	 * cpsr_svc = spsr_<exception>
	 * pc = <exception> handler
	 */
	movs	pc, lr			@ branch to handler in SVC mode
ENDPROC(vector_\name)

看到了吧,异常处理的代码,会先保存 r0,lr,spsr_xxx 到 cpu_init() 配置的异常模式栈上,然后将 CPU 模式切换到 SVC 模式,然后使用 SVC 模式的栈(内核栈),进行异常处理流程。上述分析中,涉及到的多核 CPU 启动过程,可参考 Linux: 多核CPU启动流程简析

4.3 User 模式栈配置(用户空间栈配置)

4.3.1 启动新程序时的堆栈配置流程

bash 通过 fork + exec() 系统调用来启动一个新程序:系统调用返回用户空间前,配置了堆栈空间;系统调用返回用户空间时,将设置配置好的堆栈指针值到 sp 寄存器。来看细节:

/* fs/exec.c */

/* 只看 exec() 系统调用过程,这是重点 */
static int do_execveat_common(int fd, struct filename *filename,
         struct user_arg_ptr argv,
         struct user_arg_ptr envp,
         int flags)
{
	...
	struct linux_binprm *bprm;
	...

	...

	bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);

	...

	/*
	  * 为可执行程序创建新的进程地址空间管理数据并初始化
	  * . 为新程序创建并初始化内存管理对象 mm_struct: 创建页目录表等
	  * . 为新程序初始化栈空间: [STACK_TOP_MAX - PAGE_SIZE, STACK_TOP_MAX]
	  *                          @bprm->p 指向 栈空间第1个可用位置 STACK_TOP_MAX - sizeof(void *)
	  */
	retval = bprm_mm_init(bprm);

	...

	/* 将可执行程序文件名压入程序栈 bprm->p (相应的移动栈指针 bprm->p 到下一个可用位置) */
 	retval = copy_strings_kernel(1, &bprm->filename, bprm);

	bprm->exec = bprm->p; /* 指向可执行程序文件名 */
	retval = copy_strings(bprm->envc, envp, bprm); /* 拷贝环境变量 */
	
	/* 将程序用户参数压入程序栈 (相应的移动栈指针 bprm->p 到下一个可用位置) */
 	retval = copy_strings(bprm->argc, argv, bprm);

	...

	/* 调用具体类型的程序加载器, 来加载新程序 */
 	retval = exec_binprm(bprm); /* 以 ELF 格式程序加载为例: -> load_elf_binary() */
 	...

	return retval;
	...
}

ELF 程序加载过程中,重新设定堆栈空间位置和大小:

/* fs/binfmt_elf.c */

static int load_elf_binary(struct linux_binprm *bprm)
{
	...
	
	/* 
	  * 随机化栈顶后, 重新设定程序栈空间位置和大小. 
	  * 如果栈空间位置和大小发生了变化:
	  * . 将之前压入到栈上的程序名和参数移动到新栈空间的相应位置;
	  * . 相应的调整栈空间 VMA (bprm->vma) 和 栈指针 bprm->p
	  */
	 retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
     			executable_stack);

	...

	/* N.B. passed_fileno might not be initialized? */
	 current->mm->end_code = end_code;
	 current->mm->start_code = start_code;
	 current->mm->start_data = start_data;
	 current->mm->end_data = end_data;
	 current->mm->start_stack = bprm->p; /* 程序启动前, 栈指针位置 */

	...

	/* 
	  * 启动 程序 或 解释器程序: 
	  * 程序 或 解释器程序初始化后, 然后进入程序代码开始运行。
	  *
	  * 注意到,这里最主要的动作是赋值了用户模式下的 PC SP 等寄存器,
	  * 程序代码真正运行起来是在系统调用 sys_execve*() 返回用户空间之后。
	  */
	 start_thread(regs, elf_entry, bprm->p);
	 retval = 0;
	 ...
out_ret:
 	return retval;
}

看一下 start_thread() 到底做了什么工作:

/* arch/arm/include/asm/processor.h */

#define start_thread(regs,pc,sp)     \
({         \
	memset(regs->uregs, 0, sizeof(regs->uregs));   \
	if (current->personality & ADDR_LIMIT_32BIT)   \
		regs->ARM_cpsr = USR_MODE;    \
	else        \
		regs->ARM_cpsr = USR26_MODE;    \
	if (elf_hwcap & HWCAP_THUMB && pc & 1)    \
		regs->ARM_cpsr |= PSR_T_BIT;    \
	regs->ARM_cpsr |= PSR_ENDSTATE;     \
	regs->ARM_pc = pc & ~1;  /* pc */   \
	/* 设置程序用户态堆栈指针 */        \
	regs->ARM_sp = sp;  /* sp */   \
	nommu_start_thread(regs);     \
})

start_thread() 并不会像它的名字一样,将新进程调度起来。新程序真正调度起来,是在系统调用返回用户空间时完成的,此时会配置用户空间的堆栈指针 sp 寄存器:

/* arch/arm/kernel/entry-armv.S */

/*
 * Register switch for ARMv3 and ARMv4 processors
 * r0 = previous task_struct, r1 = previous thread_info, r2 = next thread_info
 * previous and next are guaranteed not to be the same.
 */
ENTRY(__switch_to)
	add ip, r1, #TI_CPU_SAVE
 ARM( stmia ip!, {r4 - sl, fp, sp, lr} ) @ Store most regs on stack
 	ldr r4, [r2, #TI_TP_VALUE]
 	ldr r5, [r2, #TI_TP_VALUE + 4]
 	......	
	mov r0, r5
	/* 加载新进程的寄存器(包括 sp, pc),然后切换到新进程运行 */
 ARM( ldmia r4, {r4 - sl, fp, sp, pc}  ) @ Load all regs saved previously	
ENDPROC(__switch_to)

应该了解的是,前面只是配置了进程堆栈的虚拟地址空间,而真正的物理内存分配,是在写入时触发缺页中断完成的。

4.3.2 子进程堆栈配置流程

可以通过 fork() 系统调用来创建子进程,这些进程会在创建时复制父进程的地址空间(包含堆栈);然后在写入时发生写时拷贝(COW: Copy-On-Write),进而建立进程自己独立的内存空间。来看细节:

/* kernel/fork.c */

sys_fork()
	_do_fork()
		copy_process()
			copy_mm()
			copy_thread_tls()
				copy_thread() /* 细节见 4.3.3 */

static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
	struct mm_struct *mm, *oldmm;
	
	...

	oldmm = current->mm;

	...

	/* initialize the new vmacache entries */
	vmacache_flush(tsk);

	mm = dup_mm(tsk);
	...

good_mm:
	tsk->mm = mm;
	tsk->active_mm = mm;
	return 0;
}

static struct mm_struct *dup_mm(struct task_struct *tsk)
{
	struct mm_struct *mm, *oldmm = current->mm;

	mm = allocate_mm();

	memcpy(mm, oldmm, sizeof(*mm));

	if (!mm_init(mm, tsk, mm->user_ns))
		goto fail_nomem;

	err = dup_mmap(mm, oldmm);
	
	...

	return mm;
}

4.3.3 线程堆栈配置流程

不同于上小节通过 fork() 系统调用创建的进程,应用编程中,我们常见到使用 pthread_create() 来创建线程(传递 CLONE_VM 标志位给系统调用 clone()),这些线程会共享主线程(线程组leader)的地址空间:

/* kernel/fork.c */

sys_clone()
	_do_fork()
		copy_process()
			copy_mm()

static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
	struct mm_struct *mm, *oldmm;
	
	...
	
	oldmm = current->mm;
	
	...
	
	if (clone_flags & CLONE_VM) { /* clone() / vfork(): 共享主线程(线程组leader)的地址空间 */
		mmget(oldmm); /* 增加地址空间 mm_struct 引用计数 */
		mm = oldmm;
		goto good_mm;
	}

	...

good_mm:
	tsk->mm = mm;
	tsk->active_mm = mm;
	return 0;
}

新线程共享了主线程(线程组leader)的地址空间,也就意味着共享了堆栈空间,这显然是不行的:线程组内线程 A 将堆栈指针 sp 放置了位置 P1,这时候切换到线程 B 执行,线程 B 将堆栈指针 sp 切换到比 P1 地址更大的 P2(假设堆栈由高地址向低地址增长),然后一通写,当再切回到线程 A 的时候,线程 A 发现自己栈上数据已经被写乱了,因为线程 A 和 B 共享了堆栈空间。 这时候该怎么办?Linux 内核为这种情形预留了方案:可以通过预先分配线程栈空间,然后将分配的栈空间地址和大小传递给 clone() 系统调用 来解决。来看具体细节,首先 glibcpthread 在创建线程时,用 mmap() 调用预分配一段进程虚拟地址空间,作为新线程的栈空间:

pthread_create()
	/* 用 mmap() 从进程地址空间,划分一段虚拟地址空间,用作新线程栈空间 */
	allocate_stack (iattr, &pd, &stackaddr, &stacksize)
		mem = __mmap (NULL, size, (guardsize == 0) ? prot : PROT_NONE,
				MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
		...
		pd->stackblock = mem;
		...
		*stacksize = stacktop - pd->stackblock;
		*stack = pd->stackblock;
	/* 将栈空间范围传递给 clone() 系统调用 */
	create_thread (pd, iattr, &stopped_start, stackaddr, stacksize, &thread_ran);
		const int clone_flags = (CLONE_VM | CLONE_SETTLS | ...);
		...
		struct clone_args args =
		{
			...
			/* 指定新线程的栈空间虚拟地址范围 */
			.stack = (uintptr_t) stackaddr,
			.stack_size = stacksize,
			...
		};
		__clone_internal (&args, &start_thread, pd); /* 调用系统调用 clone() */

接着看系统调用 clone() 是怎么处理线程栈空间的:

/* kernel/fork.c */

sys_clone()
	_do_fork()
		copy_process()
			copy_mm()
			copy_thread_tls(clone_flags, stack_start, stack_size, p, tls)
				copy_thread(clone_flags, sp, arg, p)
/* arch/arm/kernel/process.c */

int
copy_thread(unsigned long clone_flags, unsigned long stack_start,
	unsigned long stk_sz, struct task_struct *p)
{
	struct thread_info *thread = task_thread_info(p);
	struct pt_regs *childregs = task_pt_regs(p);

	memset(&thread->cpu_context, 0, sizeof(struct cpu_context_save));
	...

	if (likely(!(p->flags & PF_KTHREAD))) { /* 非内核线程 */
		*childregs = *current_pt_regs(); /* 复制父进程的寄存器 */
		childregs->ARM_r0 = 0; /* 子进程 fork()/vfork()/clone() 返回值为 0 */
		if (stack_start) /* 有线程独立的栈空间吗 */
			childregs->ARM_sp = stack_start; /* 设置线程独立的栈空间: sp 指针指向该栈空间 */
	} else {
		...
	}

	/*
	 * 子进程从 fork()/vfork()/clone() 系列系统调用返回到 ret_from_fork,
	 * 然后从 ret_from_fork 返回用户空间。如:
	 * sys_fork() -> ... -> wake_up_new_task() -> ... -> ret_from_fork -> 用户空间
	 * 进入内核空间时,会将用户空间寄存器保存到内核栈上;从内核返回用户空间时,会进行
	 * 对应的出栈操作,所以这里的 sp 指向将要出栈的寄存器空间。
	 */
	thread->cpu_context.pc = (unsigned long)ret_from_fork;
	thread->cpu_context.sp = (unsigned long)childregs;

	...

	return 0;
}

更多关于系统调用的实现细节,可参考 Linux系统调用实现简析

4.3.4 其它情形的栈配置

还没说到的情形就只有 vfork() 了,虽然 vfork() 父子进程共享地址空间,但由于父进程在子进程退出之前都不会运行,所以情形就简单了,反正父子进程不存在冲突访问。更多关于 vfork() 的细节可参考 Linux: vfork() 程序异常退出问题分析

5. 观察和调整进程栈空间

5.1 观察进程栈空间

可以通过命令 ulimit -s 来观察系统中全局默认的进程栈大小,同样也可以通过该命令配置系统中全局默认的进程栈大小。进程自身的资源限制状况(包括堆栈)也可以通过 /proc/<PID>/limits 进行观察。getrlimit(), pctrl() 接口可以用来获取系统资源的配置状况。

5.2 调整进程栈空间

setrlimit() 可以用来配置系统全局资源限制,进而可以影响进程栈空间大小,pctrl(PR_SET_MM) 也是类似的接口。

6. 参考资料

《ARM Architecture Reference Manual.pdf》
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值