评论

收藏

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

移动开发 移动开发 发布于:2022-04-19 15:52 | 阅读数:535 | 评论:0

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

内核汇编相关篇为:
      
  • v74.01 鸿蒙内核源码分析(编码方式) | 机器指令是如何编码的  
  • v75.03 鸿蒙内核源码分析(汇编基础) | CPU上班也要打卡  
  • v76.04 鸿蒙内核源码分析(汇编传参) | 如何传递复杂的参数  
  • v77.01 鸿蒙内核源码分析(可变参数) | 正在制作中 ...  
  • v78.01 鸿蒙内核源码分析(开机启动) | 正在制作中 ...  
  • v79.01 鸿蒙内核源码分析(进程切换) | 正在制作中 ...  
  • v80.03 鸿蒙内核源码分析(任务切换) | 看汇编如何切换任务  
  • v81.05 鸿蒙内核源码分析(中断切换) | 系统因中断活力四射  
  • v82.06 鸿蒙内核源码分析(异常接管) | 社会很单纯 复杂的是人  
  • v83.01 鸿蒙内核源码分析(缺页中断) | 正在制作中 ...
本篇说清楚 ARM指令是如何被编码的,机器指令由哪些部分构成,指令有哪些类型,每种类型的语法又是怎样的 ?
代码案例 | C -> 汇编 -> 机器指令
看一段C语言编译(clang)成的最后的机器指令(armv7)
int main(){
  int a = 0;
  if( a != 1) 
    a = 2*a + 1;
  return a;
}
 生成汇编代码如下:
  main:
60c: subsp, sp, #8
610: movr0, #0
614: strr0, [sp, #4]
618: strr0, [sp]
61c: ldrr0, [sp]
620: cmpr0, #1
624: beq640 <main+0x34>
628: b62c <main+0x20>
62c: ldrr1, [sp]
630: movr0, #1
634: orrr0, r0, r1, lsl #1
638: strr0, [sp]
63c: b640 <main+0x34>
640: ldrr0, [sp]
644: addsp, sp, #8
648: bxlr
汇编代码对应的机器指令如下图所示:
DSC0001.jpg

便于后续分析,将以上代码整理成如下表格
汇编代码    机器指令(十六进制表示)    机器指令(二进制表示)              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] 表达的含义。关于寄存器的详细介绍可翻看 系列篇的 (寄存器篇) DSC0002.jpg
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) DSC0003.png
解读 图为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
                            0000EQEqual(相等)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 | 数据处理类指令
DSC0004.png

      
  • 上图为涉及数据处理指令的对应编码,由 op[占5位]和op2[占2位]两项来确定指令的唯一性  
  • 一般情况下只需op指定唯一性,图中 SUB指令对应为 0010x,而代码案例中的第一句
    subsp, 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 | 加载存储指令
DSC0005.png

      
  • 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 | 多媒体指令
DSC0006.png

多媒体指令使用较少,但是它涉及指令却很多
10x | 跳转/分支/块数据处理 指令
DSC0007.png

      
  • 出现在代码案例中的
    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 | 软中断/协处理器 指令
DSC0008.png

      
  • 其中最有名的就是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
subsp, sp, #8  // 机器码 e24dd008 < = > 1110 0010 0100 1101 1101 0000 0000 1000
是减法操作指令,减法编码格式为 DSC0009.jpg
图中除了给出格式语法还有一段伪代码用于描述指令的使用条件
      
  • 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
DSC00010.png
mov r0, #0//e3a000001110 0011 1010 0000 0000 0000 0000 0000
bx lr
DSC00011.png
bx lre12fff1e1110 0001 0010 1111 1111 1111 0001 1110

      
  • Rm = 1110 对应 lr 寄存器 ,其相当于高级语言的 return,函数执行完了需切回到调用它的函数位置继续执行,lr保存的就是那个位置,从哪里来就回到哪里去。
百文说内核 | 抓住主脉络

      
  • 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。  
  • 与代码需不断debug一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。  
  • 百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,鸿蒙研究站 | weharmonyos 中回复 百文 可方便阅读。  
  • DSC00012.png
按功能模块:
      
  • 基础知识 >> 双向链表 | 内核概念 | 源码结构 | 地址空间 | 计时单位 | 宏的使用 | 钩子框架 | 位图管理 | POSIX | main函数 |  
  • 进程管理 >> 调度故事 | 进程控制块 | 进程空间 | 线性区 | 红黑树 | 进程管理 | Fork进程 | 进程回收 | Shell编辑 | Shell解析 |  
  • 任务管理 >> 任务控制块 | 并发并行 | 就绪队列 | 调度机制 | 任务管理 | 用栈方式 | 软件定时器 | 控制台 | 远程登录 | 协议栈 |  
  • 内存管理 >> 内存规则 | 物理内存 | 虚拟内存 | 虚实映射 | 页表管理 | 静态分配 | TLFS算法 | 内存池管理 | 原子操作 | 圆整对齐 |  
  • 通讯机制 >> 通讯总览 | 自旋锁 | 互斥锁 | 快锁使用 | 快锁实现 | 读写锁 | 信号量 | 事件机制 | 信号生产 | 信号消费 | 消息队列 | 消息封装 | 消息映射 | 共享内存 |  
  • 文件系统 >> 文件概念 | 文件故事 | 索引节点 | VFS | 文件句柄 | 根文件系统 | 挂载机制 | 管道文件 | 文件映射 | 写时拷贝 |  
  • 硬件架构 >> 芯片模式 | ARM架构 | 指令集 | 协处理器 | 工作模式 | 寄存器 | 多核管理 | 中断概念 | 中断管理 |  
  • 内核汇编 >> 编码方式 | 汇编基础 | 汇编传参 | 可变参数 | 开机启动 | 进程切换 | 任务切换 | 中断切换 | 异常接管 | 缺页中断 |  
  • 编译运行 >> 编译过程 | 编译构建 | GN语法 | 忍者无敌 | ELF格式 | ELF解析 | 静态链接 | 重定位 | 动态链接 | 进程映像 | 应用启动 | 系统调用 | VDSO |  
  • 调测工具 >> 模块监控 | 日志跟踪 | 系统安全 | 测试用例 |  
  • 前因后果 >> 总目录 | 源码注释 | 静态站点 | 参考手册 |
百万注源码 | 处处扣细节

      
  • 百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。
      
  • < gitee | github | coding | gitcode > 四大码仓推送 | 同步官方源码,鸿蒙研究站 | weharmonyos 中回复 百万 可方便阅读。


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