Skip to content

70_寄存器篇

kuangyufei edited this page Sep 5, 2022 · 1 revision

本篇关键词:、、、

下载 >> 离线文档.鸿蒙内核源码分析(百篇博客分析.挖透鸿蒙内核).pdf

硬件架构相关篇为:

本篇说清楚寄存器

本篇需结合 << ARM体系架构参考手册(ARMv7-A/R).pdf >> 阅读。

寄存器的本质

寄存器从大一的计算机组成原理就开始听到它,感觉很神秘,如梦如雾多年。揭开本质后才发现,寄存器就是一个32位的存储空间,一个int变量而已(其背后的硬件原理是D触发器),但它的厉害之处在于极高频率的使用,让人不敢相信是怎么做到的,不管再复杂再牛牛的应用程序,电商也好,游戏,直播也罢,到了它这里都变成了有限的十几个寄存器在玩,简直太神奇了。 本篇将清楚说明寄存器的数量和功能,至于它是如何把复杂的上层程序变成了这十几个寄存器来玩?这是编译器的事情,不在讨论范围之内。

在 32 位的 ARM 架构中,核心寄存器(core register)的数量一般有 37 个或者更多,视处理器实现的功能多少而定。所谓核心寄存器就是指 ARM 处理器内核执行常规指令时使用的寄存器,不包括用于浮点计算和 SIMD 技术的特殊寄存器,也可以理解为是 ARM 的核心处理器单元(PE)中的寄存器,不包括外围的协处理器中的寄存器。

ARM7的37个寄存器,具体看图说明:

这些寄存器不能同时显示,处理器指令状态和工作模式指定哪些寄存器可供使用,图中一一对应。

  • 其中31个通用32位寄存器,系统和用户模式全程复用寄存器,而其余5中异常(或叫特权)模式从R8_* ~ R14_* 的寄存器叫模式专属寄存器。这种特征的寄存器有个专门的称呼,叫 Banked register.Bank 本意是银行和存款的意思,在这里的意思是"有备份的"。
  • 注意 r8 和 r8_fiq是两个不同的寄存器,名字前缀是为了好记,管理方便,以示同级概念理解。如此凑成了31个寄存器。
  • 其中r13寄存器用于SP寄存器,始终指向栈顶,因为每种工作模式都有独立的运行栈,所以有独立的寄存器去记住各自的栈顶。
  • 同理r14寄存器用于LR寄存器,用于保存模式切换时的切换位置,也是独立存在,说明模式间回跳时并不需要重新给r14_*赋值,只需在跳出去的时候保存即可。
  • 系统和用户模式共用r13(sp)和r14(lr)寄存器,所以在每个子函数的栈帧中都要保存上一个调用它函数的SP和LR值,自己执行完成后要从栈帧中恢复这两个寄存器的值,否则无法界定回去后从哪里开始,从哪里计算偏移位置。
  • r15(pc)寄存器是指向代码段的,所有模式复用的原因是它是共用的,一份代码,你运行与不运行,代码段就在哪里,不增不减。
  • 6个状态寄存器,其中CPSR(1个)和SPSR_*(5个),它们主要用于自运行或发生模式切换后的各种状态保存。
  • CPSR:程序状态寄存器(current program status register) (当前程序状态寄存器),在任何处理器模式下被访问。
  • SPSR:程序状态保存寄存器(saved program status register),每一种处理器模式下都有一个状态寄存器SPSR,SPSR用于保存CPSR的状态,以便异常返回后恢复异常发生时的工作状态。当特定 的异常中断发生时,这个寄存器用于存放当前程序状态寄存器的内容。在异常中断退出时,可以用SPSR来恢复CPSR。

七种工作模式

关于工作模式在本文末尾对应篇中有详细介绍,可自行前往查看。此处只简单说明下。 本篇需结合 << ARM体系架构参考手册(ARMv7-A/R).pdf >> 阅读。

在ARM体系中,CPU工作在以下七种模式中:

  • 用户模式(usr):该模式是用户程序的工作模式,它运行在操作系统的用户态,它没有权限去操作其它硬件资源,只能执行处理自己的数据,也不能切换到其它模式下,要想访问硬件资源或切换到其它模式只能通过软中断或产生异常。

  • 快速中断模式(fiq):快速中断模式是相对一般中断模式而言的,用来处理高优先级中断的模式,处理对时间要求比较紧急的中断请求,主要用于高速数据传输及通道处理中。

  • 普通中断模式(irq):一般中断模式也叫普通中断模式,用于处理一般的中断请求,通常在硬件产生中断信号之后自动进入该模式,该模式可以自由访问系统硬件资源。

  • 管理模式(svc):操作系统保护模式,CPU上电复位和当应用程序执行 SVC 指令调用系统服务时也会进入此模式,操作系统内核的普通代码通常工作在这个模式下。

  • 终止模式(abt):当数据或指令预取终止时进入该模式,中止模式用于支持虚拟内存或存储器保护,当用户程序访问非法地址,没有权限读取的内存地址时,会进入该模式,

  • 系统模式(sys):供操作系统使用的高特权用户模式,与用户模式类似,但具有可以直接切换到其他模式等特权,用户模式与系统模式两者使用相同的寄存器,都没有SPSR(Saved Program Statement Register,已保存程序状态寄存器),但系统模式比用户模式有更高的权限,可以访问所有系统资源。

  • 未定义模式(und):未定义模式用于支持硬件协处理器的软件仿真,CPU在指令的译码阶段不能识别该指令操作时,会进入未定义模式。

除用户模式外,其余6种工作模式都属于特权模式

  • 特权模式中除了系统模式以外的其余5种模式称为异常模式
  • 大多数程序运行于用户模式
  • 进入特权模式是为了处理中断、异常、或者访问被保护的系统资源
  • 硬件权限级别:系统模式 > 异常模式 > 用户模式
  • 快中断(fiq)与慢中断(irq)区别:快中断处理时禁止中断

每种模式都有自己独立的入口和独立的运行栈空间。 系列篇之CPU篇 已介绍过只要提供了入口函数和运行空间,CPU就可以干活了。入口函数解决了指令来源问题,运行空间解决了指令的运行场地问题。 而且在多核情况下,每个CPU核的每种特权模式都有自己独立的栈空间。注意是特权模式下的栈空间,用户模式的栈空间是由用户(应用)程序提供的。

R0~R7 寄存器

这 8 个寄存器是最普通的,所有模式都可以访问和使用。 尤其是R0是寄存器中的王牌,被称为头号寄存器,通用寄存器中它用的最高频,随便翻段汇编代码都能看到它的影子。鸿蒙开机第一跳指令就是 r0 = 0

reset_vector: //鸿蒙开机代码
    /* clear register TPIDRPRW */
    mov     r0#0					@r0 = 0
    mcr     p150r0c13c04 	@c0c13 = 0C13为进程标识符 
    /* do some early cpu setup: i/d cache disable, mmu disabled */ @禁用MMUi/d缓存
    mrc     p150r0c1c00  	@r0 = c1c1寄存器详细解释见第64页
    bic     r0, #(1<<12) 			@位清除指令清除r0的第11位
    bic     r0, #(1<<2 | 1<<0)		@清除第0和2位禁止 MMU和缓存 0位:MMU enable/disable 2:Cache enable/disable
    mcr     p150r0c1c00 	@c1=r0 

再看拿自旋锁的汇编代码,这些代码都在系列篇中详细讲解过,可前往 v08.xx 鸿蒙内核源码分析(总目录) 自行查看。

FUNCTION(ArchSpinLock)	@非要拿到锁
	mov 	r1#1		@r1=1
1:						@循环的作用因SEV是广播事件不一定lock->rawLock的值已经改变了
	ldrex	r2, [r0]	@r0 = &lock->rawLock r2 = lock->rawLock
	cmp 	r2#0		@r2和0比较
	wfene				@不相等时说明资源被占用CPU核进入睡眠状态
	strexeq r2r1, [r0]@此时CPU被重新唤醒尝试令lock->rawLock=1成功写入则r2=0
	cmpeq	r2#0		@再来比较r2是否等于0,如果相等则获取到了锁
	bne 	1b			@如果不相等继续进入循环
	dmb 				@用DMB指令来隔离以保证缓冲中的数据已经落实到RAM中
	bx		lr			@此时是一定拿到锁了跳回调用ArchSpinLock函数

R0被潜规则的干了两件事,突出了它的重要性:

  • 第一个参数 由R0保管,当然第二个参数就给R1保管
  • 函数的返回值统一交给R0保管, 例如 a -> b ,b执行完会把返回值给r0,回到a后,a从r0取值,不管取到什么,它就认为这是b的返回值,默认都只认r0保存了返回值,这就是规定。

具体看一个C函数和它的汇编,在系列篇也已经讲过,可自行翻看。

//++++++++++++ square(c -> 汇编)++++++++++++++++++++++++
int square(int aint b){
    return a*b;
}
square(intint):
        sub     spsp#8     @sp减去8,意思为给square分配栈空间,只用2个栈空间完成计算
        str     r0, [sp#4]   @第一个参数入栈
        str     r1, [sp]       @第二个参数入栈
        ldr     r1, [sp#4]   @取出第一个参数给r1
        ldr     r2, [sp]       @取出第二个参数给r2
        mul     r0r1r2     @执行a*b给R0返回值的工作一直是交给R0的
        add     spsp#8     @函数执行完了要释放申请的栈空间
        bx      lr             @子程序返回等同于mov pclr即跳到调用处
//++++++++++++ fp(c -> 汇编)++++++++++++++++++++++++
int fp(int b)
{
    int a = 1;
    return square(a+ba+b);
}
fp(int):
        push    {r11lr}      @r11(fp)/lr入栈保存调用者main的位置
        mov     r11sp        @r11用于保存sp值函数栈开始位置 
        sub     spsp#8     @sp减去8,意思为给fp分配栈空间,只用2个栈空间完成计算
        str     r0, [sp#4]   @先保存参数值,放在SP+4,此时r0中存放的是参数
        mov     r0#1         @r0=1
        str     r0, [sp]       @再把1也保存在SP的位置
        ldr     r0, [sp]       @把SP的值给R0
        ldr     r1, [sp#4]   @把SP+4的值给R1
        add     r1r0r1     @执行r1=a+b
        mov     r0r1         @r0=r1用r0r1传参
        bl      square(intint)@先mov lrpc 再mov pc square(intint)   
        mov     spr11        @函数执行完了要释放申请的栈空间 
        pop     {r11lr}      @弹出r11和lrlr是专用标签弹出就自动复制给lr寄存器
        bx      lr             @子程序返回等同于mov pclr即跳到调用处

这段代码同样适用于理解以下的各个寄存器。R0的作用相当于 x86 的 EAX

R7 寄存器

为啥要单独讲R7寄存器,因为它偶尔作为特殊寄存器在使用。内核对上层应用提供了数百个系统调用功能,当发生系统调用时,在CPU工作模式切换过程中,系统调用号是一直保存在R7寄存器中的,通过系统调用号就能查询到对应的注册函数。具体在 系统调用篇中有详细的过程说明,这里只列出部分代码

//4个参数的系统调用时底层处理
static inline long __syscall4(long n, long along b, long clong d)
{
	register long a7 __asm__("a7") = n; //将系统调用号保存在R7寄存器
	register long a0 __asm__("a0") = a; //R0
	register long a1 __asm__("a1") = b; //R1
	register long a2 __asm__("a2") = c; //R2
	register long a3 __asm__("a3") = d; //R3
	__asm_syscall("r"(a7), "0"(a0), "r"(a1), "r"(a2), "r"(a3))
}
//切换到SVC模式后,由汇编代码调用由C语言实现的系统调用统一入口
LITE_OS_SEC_TEXT UINT32 *OsArmA32SyscallHandle(UINT32 *regs)
{
    UINT32 ret;
    UINT8 nArgs;
    UINTPTR handle;
    UINT32 cmd = regs[REG_R7];// 从R7寄存器中取出系统调用号
    handle = g_syscallHandle[cmd];//查询系统调用的注册函数 
    //...
}

fp(R11) 寄存器

R11:可以用作通用寄存器,在开启特定编译选项时可以用作帧指针寄存器FP,用来实现栈回溯功能。 GNU编译器(gcc)默认将R11作为存储变量的通用寄存器,因而默认情况下无法使用FP的栈回溯功能。为支持调用栈解析功能,需要在编译参数中添加 -fno-omit-frame-pointer 选项,提示编译器将R11作为FP使用。

FP寄存器(Frame Point),帧指针寄存器,指向当前函数的父函数的栈帧起始地址。利用该寄存器可以得到父函数的栈帧,从栈帧中获取父函数的FP,就可以得到祖父函数的栈帧,以此类推,可以追溯程序调用栈,得到函数间的调用关系。

在鸿蒙内核R11是当FP寄存器使用。

SP(R13) 寄存器

SP:栈指针寄存器(stack pointer),它也是 banked register,而且所有模式都有一份,总共有 6 个(有虚拟化支持时再多一个),分别用于用户、IRQFIQ、 未定义、中止和管理员模式。在 ARM 手册,有时用 SP_usrSP_svc 这样的写法来表示不同模式下的 SP 寄存器。

SP指向函数栈的栈顶,如此 fpsp 就划定了函数栈的范围,函数在运行期间除了动态申请的内存要跑出去玩,其余就在这块空间里玩。

在鸿蒙内核R13是当SP寄存器使用。

LR(R14) 寄存器

又叫 Link Register,简称 LR,在主动调用子函数时,ARM 处理器会自动将子函数的返回地址放到这个寄存器中。 另外在异常发生的被动阶段,会导致程序正常运行的被打断, 并将控制流转移到相应的异常处理(异常响应),有些异常(fiqirq)事件处理后,系统还希望能回 到当初异常发生时被打断的源程序断点处继续完成源程序的执行(异常返回),这就需要一种解决方案, 用于记录源程序的断点位置,以便正确的异常返回。 类似的还有子程序的调用和 返回。在主程序中(通过子程序调用指令)调用子程序时,也需要记录下主程序中的调用点位置,以便将来的子程序的返回。

LR:链接寄存器(linked pointer),就是用来解决上述问题的,ARM处理器中使用 R14实现对断点和调用点的记录,即R14用作返回连接寄存器(LR),确保回来知道自己从哪个位置中断,以便继续执行。

在鸿蒙内核R14是当LR寄存器使用。

PC(R15) 寄存器

简称 PC(Program Counter)。当执行 ARM 指令(每条指令 4 字节),它的值为当前指令的地址加 8,当执行 Thumb 指令时,它的值为当前指令的地址加 4,其设计原则是让 PC 指向当前指令后面的第二条指令。

PC寄存器涉及到arm的流水线结构设计,具体在后续流水线篇中详细说明,敬请关注。

在鸿蒙内核R15是当PC寄存器使用。

CPSR 寄存器

CPSR(current program status register)当前程序的状态寄存器 CPSR有4个8位区域:标志域(F)、状态域(S)、扩展域(X)、控制域(C) 32 位的程序状态寄存器可分为4 个域:

    1. 位[31:24]为条件标志位域,用f 表示;
    1. 位[23:16]为状态位域,用s 表示;
    1. 位[15:8]为扩展位域,用x 表示;
    1. 位[7:0]为控制位域,用c 表示;

CPSR和其他寄存器不一样,其他寄存器是用来存放数据的,都是整个寄存器具有一个含义。 而CPSR寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息。

CPSR的低8位(包括I、F、T和M[4:0])称为控制位,程序无法修改, 除非CPU运行于特权模式下,程序才能修改控制位

N、Z、C、V均为条件码标志位。它们的内容可被算术或逻辑运算的结果所改变,并且可以决定某条指令是否被执行!意义重大!

  • CPSR的第31位是 N,符号标志位。它记录相关指令执行后,其结果是否为负。 如果为负 N = 1,如果是非负数 N = 0。
  • CPSR的第30位是Z,0标志位。它记录相关指令执行后,其结果是否为0。 如果结果为0。那么Z = 1。如果结果不为0,那么Z = 0。
  • CPSR的第29位是C,进位标志位(Carry)。一般情况下,进行无符号数的运算。 加法运算:当运算结果产生了进位时(无符号数溢出),C=1,否则C=0。 减法运算(包括CMP):当运算时产生了借位时(无符号数溢出),C=0,否则C=1。
  • CPSR的第28位是V,溢出标志位(Overflow)。在进行有符号数运算的时候, 如果超过了机器所能标识的范围,称为溢出。

MSR{条件} 程序状态寄存器(CPSR 或SPSR)_<域>,操作数 MSR 指令用于将操作数的内容传送到程序状态寄存器的特定域中 示例如下:

	MSR CPSRR0   @传送R0 的内容到CPSR
	MSR SPSRR0   @传送R0 的内容到SPSR
	MSR CPSR_cR0 @传送R0 的内容到CPSR但仅仅修改CPSR中的控制位域

MRS{条件} 通用寄存器,程序状态寄存器(CPSR 或SPSR) MRS 指令用于将程序状态寄存器的内容传送到通用寄存器中。该指令一般用在以下两种情况: 1) 当需要改变程序状态寄存器的内容时,可用MRS 将程序状态寄存器的内容读入通用寄存器,修改后再写回程序状态寄存器。 2) 当在异常处理或进程切换时,需要保存程序状态寄存器的值,可先用该指令读出程序状态寄存器的值,然后保存。 示例如下:

MRS R0CPSR   @传送CPSR 的内容到R0
MRS R0SPSR   @传送SPSR 的内容到R0
               @MRS指令是唯一可以直接读取CPSR和SPSR寄存器的指令

SPSR 寄存器

SPSR(saved program status register)程序状态保存寄存器。五种异常模式下一个状态寄存器SPSR,用于保存CPSR的状态,以便异常返回后恢复异常发生时的工作状态。

  • 1、SPSR 为 CPSR 中断时刻的副本,退出中断后,将SPSR中数据恢复到CPSR中。
  • 2、用户模式和系统模式下SPSR不可用,所以SPSR寄存器只有5个

留个问题

从R11 ~ R15 寄存器除了R12都用着专用寄存器,用作为特殊用途,单独独R12夹在中间不上不下的,这又是为什么呢?

百文说内核 | 抓住主脉络

  • 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。
  • 与代码需不断debug一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。
  • 百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,百篇博客系列目录如下。

按功能模块:

基础知识 进程管理 任务管理 内存管理
双向链表 内核概念 源码结构 地址空间 计时单位 优雅的宏 钩子框架 位图管理 POSIX main函数 调度故事 进程控制块 进程空间 线性区 红黑树 进程管理 Fork进程 进程回收 Shell编辑 Shell解析 任务控制块 并发并行 就绪队列 调度机制 任务管理 用栈方式 软件定时器 控制台 远程登录 协议栈 内存规则 物理内存 内存概念 虚实映射 页表管理 静态分配 TLFS算法 内存池管理 原子操作 圆整对齐
通讯机制 文件系统 硬件架构 内核汇编
通讯总览 自旋锁 互斥锁 快锁使用 快锁实现 读写锁 信号量 事件机制 信号生产 信号消费 消息队列 消息封装 消息映射 共享内存 文件概念 文件故事 索引节点 VFS 文件句柄 根文件系统 挂载机制 管道文件 文件映射 写时拷贝 芯片模式 ARM架构 指令集 协处理器 工作模式 寄存器 多核管理 中断概念 中断管理 编码方式 汇编基础 汇编传参 链接脚本 内核启动 进程切换 任务切换 中断切换 异常接管 缺页中断
编译运行 调测工具
编译过程 编译构建 GN语法 忍者无敌 ELF格式 ELF解析 静态链接 重定位 动态链接 进程映像 应用启动 系统调用 VDSO 模块监控 日志跟踪 系统安全 测试用例

百万注源码 | 处处扣细节

  • 百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。

  • < gitee | github | coding | gitcode > 四大码仓推送 | 同步官方源码。

关注不迷路 | 代码即人生

期间不断得到小伙伴的支持,有学生,有职场新人,也有老江湖,在此一并感谢,大家的支持是前进的动力。尤其每次收到学生的赞助很感慨,后生可敬。 >> 查看捐助名单

据说喜欢 点赞 + 分享 的,后来都成了大神。:)

Clone this wiki locally