您的位置:首页 > 运维架构

v83.01 鸿蒙内核源码分析 (编码方式篇) | 机器指令是如何编码的 | 百篇博客分析 OpenHarmony 源码

2022-05-10 11:37 411 查看

本篇关键词:指令格式、条件域、类型域、操作域、数据指令、访存指令、跳转指令、SVC(软件中断)

内核汇编相关篇为:

本篇说清楚

ARM
指令是如何被编码的,机器指令由哪些部分构成,指令有哪些类型,每种类型的语法又是怎样的 ?

代码案例 | C -> 汇编 -> 机器指令

看一段C语言编译(clang)成的最后的机器指令(armv7)

int main(){
int a = 0;
if( a != 1)
a = 2*a + 1;
return a;
}

生成汇编代码如下:

main:
60c: sub	sp, sp, #8
610: mov	r0, #0
614: str	r0, [sp, #4]
618: str	r0, [sp]
61c: ldr	r0, [sp]
620: cmp	r0, #1
624: beq	640 <main+0x34>
628: b	62c <main+0x20>
62c: ldr	r1, [sp]
630: mov	r0, #1
634: orr	r0, r0, r1, lsl #1
638: str	r0, [sp]
63c: b	640 <main+0x34>
640: ldr	r0, [sp]
644: add	sp, sp, #8
648: bx	lr

汇编代码对应的机器指令如下图所示:

便于后续分析,将以上代码整理成如下表格

汇编代码 机器指令(十六进制表示) 机器指令(二进制表示)
sub sp, sp, #8 e24dd008 1110 0010 0100 1101 1101 0000 0000 1000
mov r0, #0 e3a00000 1110 0011 1010 0000 0000 0000 0000 0000
str r0, [sp, #4] e58d0004 1110 0101 1000 1101 0000 0000 0000 0100
str r0, [sp] e58d0000 1110 0101 1000 1101 0000 0000 0000 0000
ldr r0, [sp] e59d0000 1110 0101 1001 1101 0000 0000 0000 0000
cmp r0, #1 e3500001 1110 0011 0101 0000 0000 0000 0000 0001
beq 640 <main+0x34> 0a000005 0000 1010 0000 0000 0000 0000 0000 0101
b 62c <main+0x20> eaffffff 1110 1010 1111 1111 1111 1111 1111 1111
ldr r1, [sp] e59d1000 1110 0101 1001 1101 0001 0000 0000 0010
mov r0, #1 e3a00002 1110 0011 1010 0000 0000 0000 0000 0001
orr r0, r0, r1, lsl #1 e1800081 1110 0001 1000 0000 0000 0000 1000 0001
str r0, [sp] e58d0000 1110 0101 1000 1101 0000 0000 0000 0000
b 640 <main+0x34> eaffffff 1110 1010 1111 1111 1111 1111 1111 1111
ldr r0, [sp] e59d1000 1110 0101 1001 1101 0001 0000 0000 0000
add sp, sp, #8 e28dd008 1110 0010 1000 1101 1101 0000 0000 1000
bx lr e12fff1e 1110 0001 0010 1111 1111 1111 0001 1110

CPSR寄存器

在理解本篇之前需了解下

CPSR
寄存器的高
4
[31,28]
表达的含义。关于寄存器的详细介绍可翻看 系列篇的 (寄存器篇)

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
    )。在进行有符号数运算的时候, 如果超过了机器所能标识的范围,称为溢出。

指令格式

ARM
指令流是一连串的字对齐的四字节指令流。每个 ARM 指令是一个单一的
32
位字(
4
字节),如图(3)

解读 图为

ARM
指令的编码一级格式,所有的指令都必须符合一级格式,分成三部分:

  • 条件域:
    cond[31:28]
    表示,条件域会影响
    CPSR
    的条件码
    N、Z、C、V
    标志位。
  • 类型域:
    op1[27:25]
    op[4]
    arm
    将指令分成了六大类型 。
  • 操作域: 剩下的
    [24:5]
    [4:0]
    即图中的空白位/保留位,这是留给下级自由发挥的,不同的类型对这些保留位有不同的定义。可以理解为因类型变化而变化的二级格式。
  • 那有了二级格式会不会有三级格式 ? 答案是必须有, 二级格式只会对保留位定义部分位,会留一部分给具体的指令格式自由发挥。
  • 一定要理解这种层次结构才能理解
    ARM
    指令集的设计总思路,因为RISC(精简指令集) 的指令长度是固定的
    16/32/64
    位,以
    32
    位为例,所有的指令设计必须全用
    32
    位来表示,如果只有一层结构是难以满足众多的指令设计需求的,要灵活有包容就得给适当的空间发挥。

条件域

cond
为条件域,每一条可条件执行的条件指令都有
4
位的条件位域,
2^4
能表示
16
种条件

cond 助记符 含义(整型) 含义(浮点型) 条件标志
0000 EQ 相等 相等 Z == 1
0001 NE 不等 不等或无序 Z == 0
0010 CS 进位 大于等于或无序 C == 1
0011 CC 进位清除 小于 C == 0
0100 MI 减、负数 小于 N == 1
0101 PL 加、正数或 0 大于等于或无序 N == 0
0110 VS 溢出 无序 V == 1
0111 VC 未溢出 有序 V == 0
1000 HI 无符号大于 大于或无序 C == 1 and Z == 0
1001 LS 无符号小于或等于 小于或等于 C == 0 or Z == 1
1010 GE 有符号大于或等于 大于或等于 N == V
1011 LT 有符号小于 小于或无序 N != V
1100 GT 有符号大于 大于 Z == 0 and N ==V
1101 LE 有符号大于或等于 小于等于或无序 Z == 1 or N != V
1110 无条件 无条件 任何
  • 大部分的指令都是
    1110 = e
    ,无条件执行指令,只要看到
    e
    开头的机器指令都属于这类
  • beq 640 <main+0x34>	// 机器码 0a000005 <=>	0000 1010 0000 0000 0000 0000 0000 0101
    0000	EQ	Equal(相等)	Z == 1

类型域

图(3)

op1
域位于
bits[27:25]
,占三位;
op
域位于
bit[4]
,占一位。它们的取值组合在一起,决定指令所属的分类(Instruction Class),其值对应的关系如下

op1    op    指令类型
00x    -     数据处理以及杂项指令
010    -     load/store word类型 或者 unsigned byte
011    0     同上
011    1     媒体接口指令
10x    -     跳转指令和块数据操作指令,块数据操作指令指 STMDA 这类,连续内存操作。
11x    -     协处理器指令和 svc 指令,包括高级的 SIMD 和浮点指令。

操作域

操作域是因类型变化而变化的二级格式 ,作用于保留位。包含

00x | 数据处理类指令

  • 上图为涉及数据处理指令的对应编码,由
    op[占5位]
    op2[占2位]
    两项来确定指令的唯一性
  • 一般情况下只需
    op
    指定唯一性,图中
    SUB
    指令对应为
    0010x
    ,而代码案例中的第一句
    sub	sp, sp, #8  // 机器码 e24dd008 <=> 1110 001`0 0100` 1101 1101 0000 0000 1000
    对应
    [24:20]
    位就是
    0 0100
    ,从而
    CPU
    在译码阶段将其解析为
    SUB
    指令执行
  • 需要用到
    op2
    的是
    MOV
    系列指令,包括逻辑/算术左移右移,例如:
    mov r0, #0	//e3a00000 <=> 1110 0011 1010 0000 0000 0000 0000 0000
    中的
    op = 1 1010
    op2 = 00
    对应 MOV(register,ARM) on page A8-489
    00x
    中的
    x
    表示数据处理分两种情况
    000
    无立即数参与(寄存器之间) ,图A5.2.1 表示了这种情况
    [27:25]= 000
  • 001
    有立即参与的运算,例如
    mov r0, #0
    中的
    [27:25]= 001
    ,此处未展示图,可前往 ARM体系架构参考手册.pdf 翻看

010 | 加载存储指令

  • Load/store
    是一组内存访问指令,用来在
    ARM
    寄存器和内存之间进行数据传送,
    ARM
    指令中有
    3
    种基本的数据传送指令

    单寄存器
    Load/Store
    内存访问指令(
    single register
    ):这些指令为ARM寄存器和存储器提供了更灵活的单数据项传送方式。数据可以使字节,16位半字或32位字
  • 多寄存器
    Load/Store
    内存访问指令:可以实现大量数据的同时传送,主要用于进程的进入和退出、保存和恢复工作寄存器以及复制寄存器中的一片(一块)数据
  • 寄存器交换指令(
    single register swap
    ): 实现寄存器数据和内存数据进行交换,而且是在一条指令中完成,执行过程中不会受到中断干扰
  • 出现在代码案例中的

    str r0, [sp, #4] //  机器码 e58d0004 <=>	1110 0101 1000 1101 0000 0000 0000 0100
    str r0, [sp]	 //  机器码 e58d0000 <=>	1110 0101 1000 1101 0000 0000 0000 0000
    将r0中的字数据写入以SP为地址的存储器中
    ldr r0, [sp]	 //  机器码 e59d0000 <=>	1110 0101 1001 1101 0000 0000 0000 0000
    存储器地址地址为SP的数据读入r0 寄存器

    [27:25] = 010
    说明都属于这类指令,完成对内存的读写,包括
    LDR
    LDRB
    LDRH
    STR
    STRB
    STRH
    六条指令。
    ldr
    为加载指令,但是加载到内存还是寄存器,这该怎么记 ? 因为主角是
    CPU
    ,加载有进来的意思,将内容加载至寄存器中。
    STR
    有出去的意思,将内容保存到内存里。
    [sp]
    相当于
    C
    语言的
    *sp
    sp
    指向程序运行栈当前位置

  • 具体可看 >> ARM的六条访存指令集---LDR、LDRB、LDRH、STR、STRB、STRH

  • 010 | 多媒体指令

    多媒体指令使用较少,但是它涉及指令却很多

    10x | 跳转/分支/块数据处理 指令

    • 出现在代码案例中的
      beq 640 <main+0x34>	// 机器码 0a000005 <=> 0000 1010 0000 0000 0000 0000 0000 0101
      b 62c <main+0x20>	// 机器码 eaffffff <=> 1110 1010 1111 1111 1111 1111 1111 1111
      [27:25] = 101
      说明都属于这类指令
    • 听得很多的
      pop
      push
      也属于这类,成块的数据操作,例如
      push
      常用于将函数的所有参数一次性入栈。
    • 内存 <> 寄存器 批量数据搬运指令
      STMDA (STMED)
      LDMDA/LDMF

    11x | 软中断/协处理器 指令

    • 其中最有名的就是
      svc 0
      ,在系列篇中曾多次提及它,此处详细说下
      svc
      svc
      全称是
      Supervisor Call
      Supervisor
      CPU
      的管理模式,
      svc
      导致处理器进入管理模式,很多人问的系统调用底层是怎么实现的?
      svc
      就是答案。
    • 例如
      printf
      是个标准库函数,在标准库的底层代码中会调用
      svc 0
      ,导致用户态的
      ARM
      程序通常将系统调用号传入
      R7
      寄存器(也被鸿蒙内核使用),然后用
      SVC
      指令调用
      0
      号中断来直接执行系统调用,
    • 在以前的ARM架构版本中,
      SVC
      指令被称为
      SWI
      ,软件中断。
    • 描述
      svc
      功能的详细伪代码如下,请尝试读懂它
      The TakeSVCException() pseudocode procedure describes how the processor takes the exception:
      // TakeSVCException()
      // ==================
      TakeSVCException()
      // Determine return information. SPSR is to be the current CPSR, after changing the IT[]
      // bits to give them the correct values for the following instruction, and LR is to be
      // the current PC minus 2 for Thumb or 4 for ARM, to change the PC offsets of 4 or 8
      // respectively from the address of the current instruction into the required address of
      // the next instruction, the SVC instruction having size 2bytes for Thumb or 4 bytes for ARM.
      ITAdvance();
      new_lr_value = if CPSR.T == '1' then PC-2 else PC-4;
      new_spsr_value = CPSR;
      vect_offset = 8;
      // Check whether to take exception to Hyp mode
      // if in Hyp mode then stay in Hyp mode
      take_to_hyp = (HaveVirtExt() && HaveSecurityExt() && SCR.NS == '1' && CPSR.M == '11010');
      // if HCR.TGE is set to 1, take to Hyp mode through Hyp Trap vector
      route_to_hyp = (HaveVirtExt() && HaveSecurityExt() && !IsSecure() && HCR.TGE == '1'
      && CPSR.M == '10000'); // User mode
      // if HCR.TGE == '1' and in a Non-secure PL1 mode, the effect is UNPREDICTABLE
      
      preferred_exceptn_return = new_lr_value;
      if take_to_hyp then
      EnterHypMode(new_spsr_value, preferred_exceptn_return, vect_offset);
      elsif route_to_hyp then
      EnterHypMode(new_spsr_value, preferred_exceptn_return, 20);
      else
      // Enter Supervisor ('10011') mode, and ensure Secure state if initially in Monitor
      // ('10110') mode. This affects the Banked versions of various registers accessed later
      // in the code.
      if CPSR.M == '10110' then SCR.NS = '0';
      CPSR.M = '10011';
      // Write return information to registers, and make further CPSR changes: IRQs disabled,
      // IT state reset, instruction set and endianness set to SCTLR-configured values.
      SPSR[] = new_spsr_value;
      R[14] = new_lr_value;
      CPSR.I = '1';
      CPSR.IT = '00000000';
      CPSR.J = '0'; CPSR.T = SCTLR.TE; // TE=0: ARM, TE=1: Thumb
      CPSR.E = SCTLR.EE; // EE=0: little-endian, EE=1: big-endian
      // Branch to SVC vector.
      BranchTo(ExcVectorBase() + vect_offset);
    • 这部分内容在系列篇 (寄存器篇)(系统调用篇)(标准库篇) 中都有提及。

    具体指令

    细看几条代码案例出现的常用指令

    sub sp, sp, #8

    sub	sp, sp, #8  // 机器码 e24dd008 < = > 1110 0010 0100 1101 1101 0000 0000 1000

    是减法操作指令,减法编码格式为

    图中除了给出格式语法还有一段伪代码用于描述指令的使用条件

    • sp
      13
      号寄存器,
      lr
      14
      号寄存器 ,
      pc
      15
      号寄存器。

    • 如果是

      PC
      寄存器
      (Rn = 15)
      S
      等于
      0
      查看
      ADR
      指令。。

    • 如果是

      SP
      寄存器
      (Rn = 13)
      SUB
      (申请栈空间)。

    • 如果是

      PC
      寄存器
      (Rd = 15)
      S
      等于
      1
      。查看
      subs
      pc
      lr
      相关指令

    • 套用格式结合源码

      cond op1 操作码 S Rn Rd imm12(立即数)
      1110 001 0010 0 1101 1101 0000 0000 1000
      无条件执行 表示数据处理 SUB sp sp 8

    mov r0, #0

    mov r0, #0	//e3a00000	1110 0011 1010 0000 0000 0000 0000 0000

    bx lr

    bx lr	e12fff1e	1110 0001 0010 1111 1111 1111 0001 1110
    • Rm = 1110
      对应
      lr
      寄存器 ,其相当于高级语言的
      return
      ,函数执行完了需切回到调用它的函数位置继续执行,
      lr
      保存的就是那个位置,从哪里来就回到哪里去。

    百文说内核 | 抓住主脉络

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

    按功能模块:

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

    百万注源码 | 处处扣细节

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

    • < gitee | github | coding | gitcode > 四大码仓推送 | 同步官方源码,鸿蒙研究站 | weharmonyos 中回复 百万 可方便阅读。

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

    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: 
    相关文章推荐