1. 程式人生 > 其它 >v74.01 鴻蒙核心原始碼分析(編碼方式篇) | 機器指令是如何編碼的 | 百篇部落格分析OpenHarmony原始碼

v74.01 鴻蒙核心原始碼分析(編碼方式篇) | 機器指令是如何編碼的 | 百篇部落格分析OpenHarmony原始碼

核心彙編相關篇為:

本篇說清楚 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位是Z0標誌位。它記錄相關指令執行後,其結果是否為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說明都屬於這類指令,完成對記憶體的讀寫,包括 LDRLDRBLDRHSTRSTRBSTRH六條指令。 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說明都屬於這類指令
  • 聽得很多的poppush也屬於這類,成塊的資料操作,例如push常用於將函式的所有引數一次性入棧。
  • 記憶體 <> 暫存器 批量資料搬運指令 STMDA (STMED) LDMDA/LDMF

11x | 軟中斷/協處理器 指令

  • 其中最有名的就是svc 0,在系列篇中曾多次提及它,此處詳細說下 svc, svc全稱是 Supervisor Call, SupervisorCPU的管理模式,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 中回覆 百文 可方便閱讀。

按功能模組:

百萬注原始碼 | 處處扣細節

  • 百萬漢字註解核心目的是要看清楚其毛細血管,細胞結構,等於在拿放大鏡看核心。核心並不神祕,帶著問題去原始碼中找答案是很容易上癮的,你會發現很多文章對一些問題的解讀是錯誤的,或者說不深刻難以自圓其說,你會慢慢形成自己新的解讀,而新的解讀又會碰到新的問題,如此層層遞進,滾滾向前,拿著放大鏡根本不願意放手。

  • < gitee | github | coding | gitcode > 四大碼倉推送 | 同步官方原始碼,鴻蒙研究站 | weharmonyos 中回覆 百萬 可方便閱讀。