菜单

kaoqi
kaoqi
发布于 2026-09-20 / 17 阅读
0
0

kernel第2个函数第1段

smp_setup_processor_id是kernel的第2个函数,其主要作用是:

  • 读取CPU寄存器中唯一标识符MPIDR_EL1

  • 建立逻辑与物理CPU的映射关系

  • 初始化Per-CPU偏移量

  • 输出启动信息

这个函数的具体实例化在/arch/arm64/kernel/setup.h:

void __init smp_setup_processor_id(void)
{
	u64 mpidr = read_cpuid_mpidr() & MPIDR_HWID_BITMASK;
	set_cpu_logical_map(0, mpidr);

	/*
	 * clear __my_cpu_offset on boot CPU to avoid hang caused by
	 * using percpu variable early, for example, lockdep will
	 * access percpu variable inside lock_release
	 */
	set_my_cpu_offset(0);
	pr_info("Booting Linux on physical CPU 0x%010lx [0x%08x]\n",
		(unsigned long)mpidr, read_cpuid_id());
}

首先是第一个部分,read_cpuid_mpidr,这个函数的本质是进行一次MPIDR_EL1寄存器的read_cpuid。

// 位于/arch/arm64/include/asm/cputype.h
#define __mrs_s(v, r)						\
	DEFINE_MRS_S						\
"	mrs_s " v ", " __stringify(r) "\n"			\
	UNDEFINE_MRS_S

#define read_sysreg_s(r) ({						\
	u64 __val;							\
	asm volatile(__mrs_s("%0", r) : "=r" (__val));			\
	__val;								\
})

#define read_cpuid(reg)			read_sysreg_s(SYS_ ## reg)

static inline u64 __attribute_const__ read_cpuid_mpidr(void)
{
	return read_cpuid(MPIDR_EL1);
}

这里进行读取的函数是由asm volatile这个函数配合对应的寄存器宏展开,手搓出来的一个arm64架构下的MRS (Move Register from System) 指令。

首先,GCC在进行宏展开的时候,({ ... })块会将最后一个变量作为函数的返回值。

volatile函数是属于asm的一种GCC内联汇编的功能,它直接由GCC内联展开。不如由下面的这个定义,就可以引用:

#define __asm__ asm

#define __volatile__ volatile

这个volatile的函数结构为:

asm volatile (
    "汇编指令模板"
    : 输出操作数列表 (Output Operands)
    : 输入操作数列表 (Input Operands)
    : 破坏描述列表 (Clobber List)
);

这里,有关具体的指令的展开为:

#define __DEFINE_MRS_MSR_S_REGNUM				\
"	.irp	num,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30\n" \
"	.equ	.L__reg_num_x\\num, \\num\n"			\
"	.endr\n"						\
"	.equ	.L__reg_num_xzr, 31\n"

#define DEFINE_MRS_S						\
	__DEFINE_MRS_MSR_S_REGNUM				\
"	.macro	mrs_s, rt, sreg\n"				\
	__emit_inst(0xd5200000|(\\sreg)|(.L__reg_num_\\rt))	\
"	.endm\n"

#define UNDEFINE_MRS_S						\
"	.purgem	mrs_s\n"

#define __mrs_s(v, r)						\
	DEFINE_MRS_S						\
"	mrs_s " v ", " __stringify(r) "\n"			\
	UNDEFINE_MRS_S

// 这里拼出MPIDR_EL1寄存器
#define sys_reg(op0, op1, crn, crm, op2) \
	(((op0) << Op0_shift) | ((op1) << Op1_shift) | \
	 ((crn) << CRn_shift) | ((crm) << CRm_shift) | \
	 ((op2) << Op2_shift))
#define SYS_MPIDR_EL1			sys_reg(3, 0, 0, 0, 5)
#define read_cpuid(reg)			read_sysreg_s(SYS_ ## reg)

// 这里最后返回指令执行的读取值
#define read_sysreg_s(r) ({						\
	u64 __val;							\
	asm volatile(__mrs_s("%0", r) : "=r" (__val));			\
	__val;								\
})

这里相当于由read_sysreg_s输入寄存器MPIDR_EL1,拼出操作码MRS所需的指令:

  • 3 << 19 = 0x60000 (二进制: 110 0000 0000 0000 0000)

  • 0 << 16 = 0x0

  • 0 << 12 = 0x0

  • 0 << 8 = 0x0

  • 5 << 5 = 160 = 0xA0 (二进制: 101 00000)

最终计算出SYS_MPIDR_EL1为:(可以直接对比ARM64指令集找对应指令格式)

SYS_MPIDR_EL1 = 0x60000 | 0xA0 = 0x600A0

同时,还需要拼接一个通用寄存器、一个内存中的u64地址用于保存寄存器的返回值。

mrs本身是一个宏函数:

mrs_s %0, SYS_MPIDR_EL1

它在文件中被宏展开定义为:

.macro mrs_s, rt, sreg

__emit_inst(0xd5200000 | (\sreg) | (.L__reg_num_\rt))

.endm

其中,sreg为:0x600A0(就是SYS_MPIDR_EL1)

其中,L__reg_num_为:%0所找到的某个具体的可用寄存器位置

GCC 使用 %0, %1, %2... 等数字作为占位符,分别对应内联汇编语句中“输出操作数”和“输入操作数”列表中定义的 C 语言变量。

可以看到,在这个指令中,MPIDR_EL1的指令层面地址是确定的,这是属于ARM的一种硬编码地址。同时,MPIDR_EL1寄存器本身又具备一定的格式,可以用其中的Aff0~Aff3寄存器来推断当前物理拓扑的层级信息(核心、集群、插槽)。

ARM硬编码的MPIDR_EL1

旧事重提,这里先贴一个ARM手册链接:A-Profile Architecture

在文档中,有关这个寄存器的描述如下:D24.2.137章节

这玩意儿其实是从32位的版本演进而来的,主要内容包括Aff参数与MT参数,具体如下:

  • 当MT参数为1时,代表启用逻辑线程(超线程);为0时代表启用物理线程

  • AFF0,最内层亲和性ID,最小的核心或超线程的ID

  • AFF1,族内核心ID,一般用于同一个Cluster内的核心分组

  • AFF2,片内核心ID,一般用于区分不同的Cluster,即CPU簇

  • AFF3,Die/Socket ID,一般用于区分多Die的ID

有关MRS指令,手册里的描述原文如下:C6.2.286章节

这个指令的具体含义是,将一个系统寄存器搬移到一个通用寄存器中去。

这里规定了MRS的格式为:

0b110101010011    <oD>    <op1>    <CRn>    <CRm>    <op2>    <Rt>

其中Xt就是通用寄存器,Systemreg是一个由<op0>_<op1>_<Cn>_<Cm>_<op2>编码的系统寄存器地址,可以在D23章节“AArch64 System Register Emcoding”中找到所有的系统寄存器编码方式。

OP0:为0b10时代表这是系统寄存器地址空间

OP1:用于区分同一组Cn/Cm下的不同寄存器或不同异常级别

Cn:协处理器n,一般标识作为系统寄存器编码的一部分,通常表示寄存器的大类或组

Cm:协处理器m,配合Cn做进一步分细分

OP2:用于在同一组Cn/Cm下做寄存器区分用

这里还提供了一个系统寄存器的大表,用来查询可以基于硬编码+MRS指令访问的各类寄存器:

MRS命令本身的操作码开头是固定的0b110101010011,这意味着刚好为0xd5开头的

同时,有关MPIDR_EL1这个寄存器的Systemreg编码,在其章节内有如下明确定义:

所以确实地址就是上面给定的sys_reg(3, 0, 0, 0, 5)映射出来的0x600A0

asm volatile方法

这个方法的本质其实就是在C文件中写汇编。为了达到上述的MRS的效果(包含两个方面,第一个是要执行MRS,第二个是要找到并放入一个内存中),就可以使用这个方法来跑汇编。

可以使用如下:

uint64_t mpidr;  // 内存中定义一个变量
register uint64_t x0 asm("x0");  // 定义
asm volatile(
    ".inst 0xD5384100\n\t"  // 手动插入机器码
    "mov %0, x0"            // 把结果移出来
    : "=r"(mpidr)
    :
    : "x0"
);

来进行MRS并把数据搬到mpidr中去。当然,这里的汇编会绕开编译器的寄存器分配,很容易导致bug。

注意,这里的编码是可以由如下算出来的

__emit_inst(0xd5200000|(\\sreg)|(.L__reg_num_\\rt))

算出来后,其中的L__reg_num_0会直接对应到%0或者x0,进而完成最后的挪出操作。


评论