# Overview * 高階語言(.c) -> (Compiler) -> 組合語言(.s) -> (Assembeler) -> 機器語言(.o) * 每一種處理器(Processer)都有自己的指令,多個指令形成程式 * 組合語言的種類 * ARM: 用於各種嵌入式系統 (本課程目標) * SIMD(x86): 用於大部分電腦系統中 * RISC-V * 使用組合語言的原因: * 速度更快 * 程式碼大小更小 * 有存取特殊硬體的需求 # Computer System ## 電腦基本架構 電腦系統主要可以分成三大區,並透過"bus"互相溝通 ![](https://hackmd.io/_uploads/rJ5N2r6Ah.png) ## Processor Processor可以被視為系統的控制中樞 執行流程 (以下三點不斷循環): 1. Fetch: 從記憶體(Memory)中取出指令 1. 透過address bus傳遞指令所在的記憶體位址訊息 2. 透過Control bus傳遞"讀指令"這個動作 3. 透過data bus放置讀出來的資料 3. Decode: 解讀指令 4. Execute: 執行指令 * 透過ALU(Arithmetic and Logic Unit)執行指令 ### Processor register * register是在Processor中,讀取速度非常快的記憶單元,但空間是有限的 * 一個register的大小通常是4 bytes * 必須先將資料要先放到register中才能做運算 觀念釐清: 1. 誰提供指令給Process執行? 使用者撰寫的程式 2. 誰把要執行的指令放到memory中供Processor Fetch? 作業系統會將使用者撰寫的程式從disk中轉移至memory 3. Processor 如何知道指令所在的記憶體位址? 作業系統再將程式從disk中轉移至memory的過程中,會順便告訴Processor記憶體位址相關資訊 ## Instruction 基本格式(一條指令就是一行) ![](https://hackmd.io/_uploads/SJ4YGt0Ch.png =50%x) * Function: 指定要做哪種運算 * OP1、OP2: 運算所需的參數 ### Number of address 在設計組合語言的指令時,跟據參數的不同,可以將組合語言的指令分成五大類 註: address可以被視作oprand(參數) * 4-address machine: * 包含一個result、兩個sourse、下個指令的位置 * ex: `ADD d,s1,s2,next` (等價於`d := s1+s2`) * 3-address machine: * 包含一個result、兩個sourse * 使用Progrem Counter(一種特別的register)來記錄下個指令的位置 * ex: `ADD d,s1,s2` (等價於`d := s1+s2`) * 2-address machine: * 包含一個"同時當result跟sourse"的參數、一個sourse * ex: `ADD d,s1` (等價於`d := d+s1`) * 1-address machine: * 包含一個sourse * 使用accumulator(一種特別的register)來同時當result跟sourse * ex: `ADD s1` (等價於`accumulator := accumulator+s1`) * 0-address machine: * 不需要任何參數 * 利用額外的Stack 來儲存參數,instruction只需要指定要對參數做甚麼事即可 * ex: `ADD` ![](https://hackmd.io/_uploads/S1h7dKC03.png =50%x) ## Memory ![](https://hackmd.io/_uploads/ry_DTAykT.png =50%x) * Memory 的每一格大小都是一個byte,且都有一個Address與之對應,故這種Memory 又被稱為**byte addressable** ### Read Operation ![](https://hackmd.io/_uploads/HkAf001Jp.png) 1. 先將CPU想要讀取的記憶體位置放到address bus中 2. 透過Control bus 通知Memory 要做"讀取"操作 3. Memory將資料讀出來,並放到data bus 中 4. 結束讀取操作 ### Write Operation ![](https://hackmd.io/_uploads/HkAf001Jp.png) 1. 先將CPU想要寫的記憶體位置放到address bus中 2. 把要寫進記憶體中的資料放到data bus 中 3. 透過Control bus 通知Memory 要做"寫入"操作 4. Memory成功寫入資料,並結束寫入操作 若是要寫入的資料不只一個byte,則會有兩種擺法(必考) ![](https://hackmd.io/_uploads/ByM4HylJT.png =80%x) * 將欲寫入資料**最右邊的bit**稱為Most significant bit(MSB) * 將欲寫入資料**最左邊的bit**稱為Least significant bit(LSB) * 將資料存進Memory時,都是從Address低的擺放到高的 * **若先擺LSB,則這種擺法稱為Little-endian byte ordering** (大部分processor的預設模式) * **若先擺MSB,則這種擺法稱為Big-endian byte ordering** ### Data aligment 系統在讀取記憶體時,基於性能考量,會一次讀取2、4、8... byte的資料,故一筆資料在記憶體中是否處於對齊的位置會直接影響到讀取速度 舉例來說,當資料跨區儲存時,Processor需要讀取兩次Memory才能得到想要的資料 ![](https://hackmd.io/_uploads/HJsjc1eya.png) 資料對齊的定義: * 1 byte 資料: 永遠都是對齊的 * 2 byte 資料: 需儲存在"2的倍數"的記憶體中才算對齊 * 4 byte 資料: 需儲存在"4的倍數"的記憶體中才算對齊 * 其他以此類推 針對資料是否要對齊,可以將Processor分成兩類 * soft alignment: Processor並不強迫對齊資料,但對齊能提高性能 * hard alignment: Processor強迫資料一定要對齊 ## Instruction Set Architecture(ISA) 從組合語言撰寫者的角度來看操作CPU的api ## Instruction types 可以分成三類: * Data processing instructions: 資料運算與處理相關的指令 (ex: add,sub) * Data movement instructions: 資料搬移相關的指令 (ex: ) * Program control flow instructions: # System software ## Assember 程式執行流程: ![](https://hackmd.io/_uploads/rJLbf9B16.png =70%x) * Assember: * 將組合語言的指令翻譯成機器語言 * 將翻譯好機器語言存成適當的格式(ex: a.out) * 給予symbolic labels正確的記憶體 * 將型態轉換成機器讀得懂的表示法 * Object code: 機器語言,由0跟1組成 (ex: a.out);分成三個區塊,分別存放程式、已初始化全域變數、未初始化全域變數 ![](https://hackmd.io/_uploads/S1fODOwe6.png =50%x) * Linker: 程式可能會寫在不同的檔案中,並分別被轉成Object code,Linker的工作便是負責合併不同的Object Code,使之變成一個執行檔 * Executable code: 隨時可執行的程式 * Loader: 隸屬作業系統,負責載入Executable code至memory,並告訴Processor這段Executable code可執行 關於symbolic labels: ![](https://hackmd.io/_uploads/ByUCHqSJp.png =70%x) * LABEL的語法就如同C語言的goto,當程式執行到0x108那行時,會跳回0x110那邊執行指令 * Assember其中之一的工作便是賦予label跳回去位置的記憶體位址 ### Assember Directives 負責提供Assember資訊的指令,不會被翻譯成machine code ex: ![](https://hackmd.io/_uploads/BJvfvcBkT.png =50%x) ### Pseudo Instruction 不是實際的指令,但Assember能看懂並轉成等價的實際指令 ex: 把r2的值搬到r1 ``` MOV r1, r2 (Pseudo Instruction) add r1, r2, #0 (等價實際指令,r1=r2+0) ``` ### Two Pass Assember 在Assember逐行翻譯指令的時候,可能會遇到Label要跳去的地方還沒讀到的問題 ![](https://hackmd.io/_uploads/rJUQpcB1T.png =50%x) 解決方法是使用Two Pass Assember: * 第一輪先掃過一次全部的指令,已得知Label的位置 * 第二輪再逐行翻譯指令 # 跨平台工具編譯 ![](https://hackmd.io/_uploads/B1hzc2BJ6.png =70%x) * Building machine: 負責編譯程式 * Host machine: 負責執行程式 * Target machine: 負責享受Host machine的執行結果 # ARM 指令介紹 學習目標: 32 bit ARM instruction set (ARM v4T) * Load-store 架構: 從記憶體中讀取資料至register,處理完後,儲存回記憶體 ## condition code flag觀念 ![](https://hackmd.io/_uploads/SJQ7iwvxa.png) * 電腦在運算的過程中,還存了32個bit的condition code flag,用來記錄一些必要的資訊 應用: 64 bit 加法 ![](https://hackmd.io/_uploads/Bkydivvgp.png) * ADDS指令中的S代表考慮進位,若ADD的運算過程中有進位,condition code flag C會變成1 * 並使用ADC來使用上一次進位的C ## Comparition Operation 這些指令只會對condition code bit造成影響 ![](https://hackmd.io/_uploads/Hy0oTBDlT.png) ## Immediate Operand 包含了Immediate value(常數) ![](https://hackmd.io/_uploads/BJt4CBPla.png =80%x) * 常數可放十進位或十六進位 ## Shift Register Operand * 包含了左移與右移運算等 (只會處理最後8個bit) * ARM 並沒有提供獨立的指令操作,只能可以**放在Data Processing instruction的第二參數後來使用** ![](https://hackmd.io/_uploads/Syw7gUDga.png =80%x) * LSL: 左移運算,不足的補0 * ASL: 跟LSL完全相同 ![](https://hackmd.io/_uploads/H1uUlLPl6.png =80%x) ![](https://hackmd.io/_uploads/Sk4wlUveT.png =80%x) * LSR: 右移運算,不足的補0 * ASR: 右移運算,**不足的補「Most significant bit」** (目的是處理負數) ![](https://hackmd.io/_uploads/SkDrfIPxT.png =80%x) * ROR: 右移運算,把被丟掉的bit補到最左邊,先丟的先補 ![](https://hackmd.io/_uploads/B1Ypz8wep.png) * RRX: 右移運算,**只右移一個bit**,將當前的C flag存到最左邊,接著把被丟掉的bit存至flag 左右移使用範例: ![](https://hackmd.io/_uploads/BJgVJIvea.png =80%x) * r3 = r2 + (r1左移3位) * 仍被視為一條指令,所需CPI不變 ## 其他語法 ![](https://hackmd.io/_uploads/ryNfDuvxp.png =70%x) * .section .text: 表示到 .end前的區塊都屬於object file 中的text區域 * .global main: 讓linker認識main這個名字 * .type main,%function: 告訴linker main這個名字的型態 (其他可能值為"object"及"common") * main: 屬於Label * 單行註解: 加@ * 多行註解: /* */ ## Data Processing instruction 專門處理register中的資料 ![](https://hackmd.io/_uploads/HkSSsHwlT.png) * ADD: 加法運算 * ADC: 加法運算,加上carry bit * SUB: 減法運算 * SBC: 減法運算,加上carry bit * RSB: 反向減法運算 * RSC: 反向減法運算,加上carry bit ![](https://hackmd.io/_uploads/SkMjiHPgp.png) * AND: bitwise AND 運算 * ORR: bitwise OR 運算 * EOR: bitwise XOR 運算 * BIC: 清除特定bit指令 (將r1中 「r2中為1中的那些位置」更新成0) ![](https://hackmd.io/_uploads/SJCD3Svep.png =50%x) ![](https://hackmd.io/_uploads/SkMcnSve6.png) * MOV: 將r2的值搬到r0 * MVN: 將 not r2的值搬到r0 ![](https://hackmd.io/_uploads/ryLuhwvl6.png) * MUL: 乘法運算 (速度慢,要約12個Clock Cycle) * result register(ex: r4) 不可以跟sourse register(r2,r3)是同一個register * 第二個參數不支援Immediate value(常數) * 無法處理進位問題 ## Data transfer instruction 這些指令負責協助資料在register跟memory間搬移 ### single register load and store instruction ![](https://hackmd.io/_uploads/SkOB4zhl6.png =90%x) * LDR: 將4 bytes的資料從r1所指向的記憶體中load至r0中 * STR: 將4 bytes資料store至r1所指向的記憶體中 * LDRB: 將1 bytes的資料從r1所指向的記憶體中load至r0中 * STRB: 將1 bytes資料store至r1所指向的記憶體中 * LDRH: 將2 bytes的資料從r1所指向的記憶體中load至r0中 * STRH: 將2 bytes資料store至r1所指向的記憶體中 * LDRSB: 將1 bytes的資料(有分正負)從r1所指向的記憶體中load至r0中 * LDRSH: 將2 bytes資料(有分正負)從r1所指向的記憶體中load至r0中 註: 之所以需要LDRSB跟LDRSH,是因為一個register的大小是4個bytes,當load的資料不足四個bytes時,剩餘的空間便需要補0(正數)或1(負數),取決於memory中most significant bit 其他特殊用法 pre-index addressing mode ![](https://hackmd.io/_uploads/SyCEVm2xp.png =70%x) * 執行LDR操作時,memory指標指向「r1向後位移四格」的位置(r1本身不變) auto indexing ![](https://hackmd.io/_uploads/HJECNQhlp.png =70%x) * 執行LDR操作時的,memory指標指向「r1向後位移四格」的位置;**執行完畢後,r1向後位移四格** (!便表示會更新r1指標的位置) post-index addressing mode ![](https://hackmd.io/_uploads/B1S4Bm3g6.png =70%x) * 執行LDR操作時,memory指標指向「r1」的位置;**執行完畢後,r1向後位移四格** ### Multiple register load and store instruction 一次便能處理一個區間內的資料,非常適用於進入跟離開function時搬移資料 ![](https://hackmd.io/_uploads/SkNos7hea.png =80%x) * 總共有2*4種指令組合 ex1: ![](https://hackmd.io/_uploads/SJnnKJTlT.png =80%x) * IA 是increase after,!代表會動到r0本身 ex2: ![](https://hackmd.io/_uploads/Sy2XnJ6gp.png =80%x) * Decrease系列,先讀到的數會被搬至後面的register (先讀到60 搬給r3) ### single register swap instruction * 該操作將一個register中的值跟一個memory中的值交換 * 操作過程中是atomic的 * 分成 * SWP: 一次交換4個byte * SWPB: 一次交換1個byte 範例: ![](https://hackmd.io/_uploads/B1fdu_pep.png =60%x) * 將r2所指向memory中的值轉移到r0,並將r1中的值轉移到r2所指向的memory中 ### 其他Pseudo instruction ![](https://hackmd.io/_uploads/ByC9pOTlp.png =50%x) * ADR指令可以將指定label代表的記憶體位址賦值給register * 將table1的記憶體位址賦值給r5 ## Control flow instruction 目的是滿足特定條件後,"跳到"指定的label ![](https://hackmd.io/_uploads/rJCopR9W6.png) * B: 無條件的跳到指定的label * BL: ![](https://hackmd.io/_uploads/ryG5XysZ6.png) * 跳到指定的label,並將return的位置(BL 指令的下一條指令位置)儲存於LR(r14 register)中 * 使用STMFD跟LDMFD來將return 位置儲存進Stack中跟從Stack中讀出,以因應多個function call 的情況 * 透過MOV指令,將return的位置賦值給pc(指向下一條要執行的指令位置) ## Conditional excution (predicate) 這類型的修飾字可以作用於大部分的指令上 原理是利用CMP 指令來改動condition code flag,並利用condition code flag來判斷要不要執行該指令 ex: ![](https://hackmd.io/_uploads/SylFLxoba.png) * ADD+NE 代表「當上一條CMP指令的結果是"Not equal時,才會執行這條指令"」 ## Supervisor Calls(SVC) 觸發 system interrupt, 交由作業系統執行某些事後再回來 ![](https://hackmd.io/_uploads/HJFkvMsZT.png) * 交由作業系統印出一個字元在螢幕上 ## Jump tables * 類似C語言中的switch語法,透過傳進來的值決定程式要執行哪個subroutine ![](https://hackmd.io/_uploads/HyPDtMiZa.png) * ADR那行: 將jump table的開頭load到r1 * CMP 那行: error handling (jump table 透過r0決定要執行哪個subroutine) * LDRLS那行: 取得下一條要執行的指令 * DCD: 預定義資料語法 # linkerscript and crt0 ## Object file format (.o) 一個Object file 可以依功能分成三個區塊 ![](https://hackmd.io/_uploads/Sk-AwgkGT.png =50%x) * TEXT: 存放指令 * DATA: 存放以初始化的全域變數 * BSS: 存放還未初始化的全域變數 ex: ![](https://hackmd.io/_uploads/SkzRWQyMa.png =80%x) * 可以把.matrix、.word等當作是指標(指向某個記憶體位址) * 整體概念如下 ![](https://hackmd.io/_uploads/BJ9hcQyGT.png =50%x) 回憶: ![](https://hackmd.io/_uploads/SytA3mkMa.png =50%x) * ADR: 將.matrix這個label的記憶體位址賦值給r0 * LDR: 取得 .matrix 指向的記憶體空間中的值 ## Linker Linker的目的是連結不同的Object file,使其變成執行檔 LMA(鏈結載入記憶體位址): Object file中的section被載入時,應該擺放的記憶體位址 VMA(虛擬記憶體位址): 程式在實際執行時,各個Object file中的section應該擺放的記憶體位址 ## crt0 * crt0 是程式被執行時最先被執行的部分(比main()還更早),負責初始化等等的工作 * 原理: linker在link程式時,會順便link crt0.o # ARM instruction set * 支援 [signed, unsigned] [32bits, 16bits, 8 bits] 共六種資料類型 * 一個instruction 大小為32 bits (a word) * 支援Little-endian(預設)、Big-endian * ARM 透過CPSR中的第0~4個bit來決定現在位於那個operating mode下 ![image](https://hackmd.io/_uploads/BkcHfH0Q6.png =70%x) * ARM 透過SPSR跳回原本的mode中 ## Register Organization ![image](https://hackmd.io/_uploads/B1TJdSAma.png) * ARM 在User mode以及System mode的情況下,共有以下register可被使用 * r0~r12: 一般register * r13(sp): stack pointer,追蹤runtime中top位置 * r14(lr): link register,在bl指令時,負責存要跳回去時的記憶體位址 * r15(pc): progrem counter,指向下一道指令的位置 * cpsr: ![image](https://hackmd.io/_uploads/r1LrpSCQ6.png) * 0~4個bit負責存現在位於那個operating mode * 29~31bit負責存當前的condition code flag * r0~r7又稱為Low register,其他的被稱為High register ## Exception (例外情況) 發生Exception後的標準流程: 1. 切換至對應的mode 2. 將回去的記憶體為址存至r14中 3. 將切換前的CPSR存至SPSR 4. 禁止發生其他Exception 5. 透過查表,使progrem counter指到中斷向量表(Exception Vector Address)中的固定位置(指定下一次要執行的指令) * 中斷向量表中的指令通常是branch instruction,會再跳到真正執行服務的地方 回復Exception的標準流程: 1. 回復修改的register 2. 使用SPSR回復CPSR 3. 使PC指向下一條該執行的指令位置 ``` MOVS pc, r14 @加了S代表在執行第三步時,同時執行第二步 ``` ## condition excution 細節 * 每一條ARM instruction的28~31個bit保留來處理condition;接著便可以根據CSPR中存的值判斷要不要執行 ``` ADDNE r0, r1, r2 @ 28~31個bit用來存"NE"這個資訊 ``` * 沒指定condition excution的情況下,預設條件為"always" * 共有16種可能的情況 ![image](https://hackmd.io/_uploads/S19rASAQT.png) ## 指令的encode方式 ### Branch instruction ![image](https://hackmd.io/_uploads/SyMEOL07a.png) * offset: 當前指令到下一條指令的"距離" (直接加到 PC 中,單位為 byte) * L: 該指令是否包含link(BL 中的"L") * Cond: 保留用來處理condition 格式: ![image](https://hackmd.io/_uploads/B1znDEzNa.png =50%x) 範例: ``` B LABEL LABEL: ``` ## SWI(Software interrupt) instruction (SVC) ![image](https://hackmd.io/_uploads/H1uZhI0Xa.png) * 0~23 bits: 表示特定的功能,例如輸出字元到螢幕上等等 * cond: 保留用來處理condition ## Data Processing instruction ![image](https://hackmd.io/_uploads/ByfkkezVa.png) * operand: 參數二 * 若 # = 1: immediate value * 若 # = 0: 分成 * Rm: 做為第二參數的register * 4 bit: 決定shift大小的來源 * Sh: shift的種類 * 7~11 bits: shift的大小,來源可以是register或immediate value * Rd: 目標 register,因為ARM的register總共有16顆,故需要4個bits * Rn: 參數一register,因為ARM的register總共有16顆,故需要4個bits * S: 決定是否要set condition (ADD**S**) * opcode: 決定指令種類 * \# : 決定參數二的定義 * cond: 保留用來處理condition 格式: ![image](https://hackmd.io/_uploads/r13ldNf4a.png) 範例: ``` ADD r2, r2, #3 ``` ``` @ 將r7 右移r2個bit後,r4=r5-r7 SUB r4, r5, r7, LSR r2 ``` ## 乘法及乘加指令 ![image](https://hackmd.io/_uploads/HkaoiWGET.png) * Rd,Rn,Rs,Rm: 乘法所需參數 格式: ![image](https://hackmd.io/_uploads/SyhetEzEp.png) * Rd= Rm*Rs中較低的32 bits * Rd= Rm*Rs+Rn中較低的32 bits ![image](https://hackmd.io/_uploads/BkhMFNGEp.png) * 進行特殊的64 bits乘法時 較高的32 bits會存至Rd中 (RdHi) 較低的32 bits會存至Rn中 (RdLo) * RdHi,RdLo = Rm*Rs (先寫lo再寫hi) 範例: ``` @ r1=r2*r3 較低的32個bits MUL r1, r2, r3 ``` ``` @ r4, r1 = r2*r3 UMULL r1, r4, r2, r3 @ r5,r1 = (r2*r3)+r5 UMLAL r1, r5, r2, r3 ``` ## Data Transfer instruction ### single word or unsigned byte 例如LDR、STRB ![image](https://hackmd.io/_uploads/B1pVeMG4p.png) * offset: 根據 # 決定是immediate還是Register * Rd: destination register * Rn: base register * L: 決定該指令是Load/store * W: 決定是否會動到base * B: 決定針對的資料型態是single word還是unsigned byte * U: 決定要往memory大還是小的地方走 * P: 決定是先做再動還是先動在做 * cond: 保留用來處理condition 格式: ![image](https://hackmd.io/_uploads/HJq1MGfVa.png) * condition要寫在 {B} 的前面,例如LDREQB 範例: ``` @ R2代表的address加上R4後,將R1中的值儲存至該位址 STR R1, [R2, R4] ``` ``` @ 將R1中的值儲存至R2代表的address後,R2代表的address加上R4 STR R1, [R2], R4 ``` ``` @ 將R1中的值儲存至PLACE這個label的address STR R1, PLACE ``` ### Half-word or Signed Byte 例如STRH、LDRSB ![image](https://hackmd.io/_uploads/rkT9fMGEa.png) * 與上面不同的是,offset儲存的方式被切成兩半,並由 # 決定存的是甚麼 * S、H: * 00: 進行SWP操作 * 進行Unsigned halfwords 操作 * 進行Signed byte操作 * 進行Signed halfword操作 格式: ![image](https://hackmd.io/_uploads/r19VjNMN6.png) 範例: ``` @ 將R3儲存至「R4向後移14格」的記憶體位址 STRH R3, [R4, #14] ``` ``` @ 將R2代表的address中的值給R8,結著R2向後位移12格 LDRSB R8, [R2], #12 ``` ### Multiple register Transfer instruction 例如STMIA、LDMFD ![image](https://hackmd.io/_uploads/SyN44Mf4T.png) * Register list: 用16個bit表示那些Register有參與操作 * 同樣先注意是先寫condition,再寫addressing mode (LDM**EQ**IA) 格式: ![image](https://hackmd.io/_uploads/HJOG3NzVp.png) 範例: ![image](https://hackmd.io/_uploads/ByjL24zNT.png) ### Swap instruction 回憶: ![image](https://hackmd.io/_uploads/rJhhrzGEp.png) ![image](https://hackmd.io/_uploads/rkqaSMf4T.png) 格式: ![image](https://hackmd.io/_uploads/HJ2O3Vf4a.png =50%x) 範例: ``` @ 將R2代表的address load至R0,並將R1中的值存入R2 @ 過程是atomic的 SWP R0, R1, [R2] ``` ### Status register to general register transfer instruction Status register 指的是SPSR或CPSR general register 指的是一般的register ![image](https://hackmd.io/_uploads/Sy45LzGVT.png) * R: 決定是SPSR還是CPSR 格式: ``` MRS{cond} Rd, CPSR @ 將CPSR的值給Rd @ CPSR可替換成SPSR ``` 範例: ``` @ 將CPSR的值移動至r0 MRS r0, CPSR ``` ### Status register to general register transfer instruction 上面指令的相反,但只能轉移特定區域的bit,不能全轉 (透過field控制) ![image](https://hackmd.io/_uploads/B1AzwGzNp.png) 用法: ``` MSR CPSR_<field> Rm @將Rm中的某部分搬到CPSR中 @ CPSR可替換成SPSR ``` * field可以為: * c: 將Rm的0~7 bits 搬到CPSR中 * x: 8~15 bits * s: 16~23 bits * f: 24~31 bits 範例: ``` @ 將r0中c field儲存進CPSR中 MSR CPSR_c, r0 ``` ### Coprecessor instruction * Coprecessor 可以視作輔助運算的處理器,專門用來執行一些特殊的運算 * ARM 架構最多支援16個Coprecessor,每個Coprecessor最多可以有16個private register * 分成三種: * Coprocessor data operations * Coprocessor data transfers * Coprocessor register transfers #### Coprecessor data operations ![image](https://hackmd.io/_uploads/SJ1x6XGEa.png) * CP#: 用來識別要使用哪個Coprecessor的運算 * CP Opc: 用來識別要使用某個Coprecessor的第幾號運算 * CRd, CRn, CRm: 可以想成就是Rd, Rn, Rm,代表參數跟回傳的結果 格式: ![image](https://hackmd.io/_uploads/HJRGCEG4T.png) 範例: ``` @ 對c2, c3執行p1 Coprecessor的第10號運算後,將結果存至c1 CDP p1, 10, c1, c2, c3 ``` #### Coprecessor data transfer ![image](https://hackmd.io/_uploads/Sk_n07GV6.png) * N: 用來決定要transfreeed多少word (因為只有一個bit,故只能決定兩種長度,根據不同的Coprecessor有不同長度定義) 格式: ![image](https://hackmd.io/_uploads/SJMqREM46.png) 範例: ``` @ 從label table的address中,載入資料至p1 Coprecessor的c2 register LDC p1, c2, table ``` ``` 將p2 Coprecessor 中的c3 register 儲存至「R5望後移24格」的位置 STC p2, c3, [R5, #24] ``` #### Coprecessor register transfer 讓ARM的register跟Coprecessor的register互相交換資料 ![image](https://hackmd.io/_uploads/HyfLmEzEa.png) 格式: ![image](https://hackmd.io/_uploads/B1aQSNM4a.png) 範例: ``` @ 對c5跟c6進行p2 Coprecessor的第5號運算後,將結果存至R3 @ (將資料從Coprecessor的register轉移至ARM的register) MRC p2, 5, R3, c5, c6 ``` ``` @ 對R4進行p6 Coprecessor的第0號運算後,將結果存至c6 MCR p6, 0, R4, c5, c6 ``` # SWI(SVC) 深入解析 * SWI指令存在的目的是將某些特定功能"外包"給其他人執行 (例如System call、I/O等等) * SWI會產生一個software interrupt SWI 也是一種exception,故處理流程很類似 觸發SWI後的標準流程: * User Progrem 至 Vector table 1. 切換至supervisor mode 2. 將回去的記憶體位址存至r14中 3. 將切換前的CPSR存至SPSR 4. 禁止發生其他Exception 5. 將PC設成0x8 * Vector table 至 SWI handler 1. 在 0x08 位置的指令通常是jump instruction,接著會跳到SWI Handler執行 * SWI handler 執行的邏輯: 1. 儲存當前的register值 2. 透過r14載入SWI指令 (這個例子是載入0x6) 3. 使用BIC清除不必要的部分,只保留0~23 bits (也就是服務的部分) 4. 呼叫相關服務 (這個例子是呼叫 0x6 代表的服務) ![image](https://hackmd.io/_uploads/BJpulUG46.png) ## Semihosting ![image](https://hackmd.io/_uploads/HJz5HLGET.png) * 問題: 當在開發嵌入式系統(target system)時,可能在debug期間會有某些SWI指令要求(例如Printf()),但這些功能是target system 不支援的 * 解法: 透過Semihosting的技術,將這個需求交給host machine(Debugger)代為執行 實作方法1: 1. Debugger在SWI指令這行設定一個break point,target system執行到那行時,將控制權轉交給Debugger 2. Debugger執行SWI指令要求 3. Debugger將控制權轉交給target system,繼續執行接下來的指令 實作方法2: 1. target system執行到SWI那行時,透過debug channel 通知debugger 2. debugger跟target system上的SWI handler 溝通,執行SWI指令要求 3. target system繼續執行接下來的指令 GNU Newlib的實作方法 ![image](https://hackmd.io/_uploads/r1LuP8MEp.png) * 針對那些target system無法執行的SWI要求,在設計程式時 1. 將要執行的SWI要求種類放入r0 register中 2. 將要執行的SWI要求所需參數陣列的第一個位置放入r1 register中 3. 呼叫特殊的SWI服務 4. 若這個SWI服務有回傳值,則會放入r0中 # 高階語言與ARM之間的轉換 ## 資料型態 * **字元(ASC2)**: ARM 本身支援Ubsigned byte load/store instruction * **字串**: 一次讀取一個字元即可 * **short integers, integer, long**: ARM 本身支援對應大小的 load/store instruction * **array**: 一次讀取一格即可 ``` ldr r0, [r1, #4]! (透過自動更新base來實現) ``` * Floating-Point Data type * 根據IEEE-754協議 ![image](https://hackmd.io/_uploads/BJY9QZINp.png =50%x) * exponent(指數): yyyy的部分 * fraction(小數): xxxx的部分 ![image](https://hackmd.io/_uploads/HJdUX-UVa.png) * 小數部分為0,指數部分為最大值時,根據S的值表示+-INF * 小數部分不為0,指數部分為最大值時,表示NaN * 小數部分不為0, 指數部分為0時,表示Denormalized number(小到無法表示的數) 補充: 資料存放的位置 * 區域變數: Stack中 * 全域變數: static area中 * 常數: Procedure's literal pool (常數池)中 ### Fixed Point(Q Format) Arithmetic * 若Processor硬體不支援浮點數運算,還可以透過Fixed Point的方式進行浮點數運算 * 概念: 計算7.5+2.1時,可以計算75+21,再補上小數點 * 實作:透過一組integer (n, e)表示小數 * ex: (0x00000001, 14)表示0x00000001*2$^{-14}$ * 備註: (n, e), (n, Q)兩種寫法都可 * 注意: 在Fixed Point架構下,兩數要計算時,要確保兩數的"e"一樣 * 優點: 快速 * 缺點: 準確度較低 ex: 將十進位小數a=9.216957436091198轉成Q5.11格式的二進位小數 1. Q5.11代表轉成二進位後,小數點後有11位 2. 先計算a*2$^{11}$ (因為a=(a * 2$^{11}$) * 2$^{-11}$) * a * 2$^{11}$ = 18876.3288 3. 省略小數部分,整數部分轉成二進位 18876 = (100100110111100)$_{2}$ 4. 補上小數點 * a = (1001.00110111100)$_{2}$ ex: 將Q2.2格式的**有號**Fixed Point a=(10.01)$_{2}$轉成十進位小數 1. 先轉成正的,最後再補上負號 * (10.01)$_{2}$ = 負的(01.11)$_{2}$ * (01.11)$_{2}$ = 2$^{0}$ + 2$^{-1}$ + 2$^{-2}$ =(1.75)$_{10}$ * 補上負號 * 答案為(-1.75)$_{10}$ ## conditional statement * 針對簡單的if-else判斷,可以使用conditional execution達成 ![image](https://hackmd.io/_uploads/SyU0nBwNT.png) * 針對if-else中比較多statement的邏輯,可以透過Branch instruction轉換 ![image](https://hackmd.io/_uploads/ryg_8hBDNT.png) 針對switch case語法 ![image](https://hackmd.io/_uploads/Sk4W0Hw4a.png) * 載入SUBTAB這個label代表的記憶體 (base) * 預先將要跳幾格的資訊存在r0,並檢查r0 是否合法 * 直接讓pc指向下一道指令執行的位置 ## 迴圈 for: ![image](https://hackmd.io/_uploads/Sy2rgUwN6.png) * 用CMP 控制條件 * 用B LOOP跳回去繼續循環 * 用BGE EXIT 跳出迴圈 while: ![image](https://hackmd.io/_uploads/HJdFZLP46.png =80%x) * 假設終止條件是CMP結果相等 * 檢查條件(evaltate exp)至於迴圈最前面執行 do-while: ![image](https://hackmd.io/_uploads/ByRNW8wNp.png) * 假設終止條件是CMP結果相等 * 類似while迴圈,但檢查條件(evaltate exp)置於迴圈最後面執行 ## function ### 名詞解釋 Subroutine: 被更高階routine所呼叫的routine Function: 有傳入參數跟回傳值的subroutine Procedure: 有傳入參數,但沒回傳值的subroutine Argument、parameter: ![image](https://hackmd.io/_uploads/BJz1BUDVa.png =50%x) ### Procedure call standard for ARM Architexture(AAPCS) 規範了關於函式互相呼叫時的細節,使不同人所寫的程式能互相呼叫 ![image](https://hackmd.io/_uploads/BkyZcIPVa.png) * Synonym指的是在AAPCS的架構下,各register的功能 * r0\~r3(a1~a4): * 負責Argument傳入: * 前四個Argument傳入會透過這四個register來傳,若Argument多於四個,剩餘的則會以逆序放入Stack中(為了確保順序能被還原) * 若參數包含了浮點數,則會先使用floating-point register(f0\~f3),再使用a1~a4,再使用Stack * 負責回傳值: * 若回傳值大小為1 word,則會使用a1 * 若回傳值大小為2 word,則會使用a1~a2,以此類推 * 若回傳值大小>4 word,則會放入Memory後回傳其位置 * 這四個參數是caller-saves register,caller需復原這幾個register作為Argument前的值 ![image](https://hackmd.io/_uploads/BJZMi8vVT.png =50%x) * r4\~r11(v1~v8): * 可被任意使用的register * 這八個參數是callee-saves register,callee需復原這幾個register作為回傳值前的值 ![image](https://hackmd.io/_uploads/B1gmh8wET.png =50%x) * 在不同的環境下,r9可能須扮演其他角色,例如SB等等 ### 在function間切換的細節 ![image](https://hackmd.io/_uploads/Hyl7oqPEa.png) * 在AAPCS的規範中,每個function都會占用Stack中的一段空間,並規定了每個變數存放的位置順序 * 為了符合AAPCS的規範,在每個function的一開始都要加上這段 ![image](https://hackmd.io/_uploads/H1-hDqDET.png=80%x) * 使用ip(Internal Pointer) 暫存sp(stack pointer的值) * ip的功能便是暫存 * 將母函式(caller)的相關資訊存起來 * 將fp(frame pointer)的位置往下移四個bits,以指向"該函式在Stack中所占用的記憶體位址"的第一個位置 * fp的功用便是指向"該函式在Stack中所占用的記憶體位址"的第一個位置,以透過位移取得caller的pc、lr等資訊 * 為了符合AAPCS的規範,在每個function結束前都要加上這段 ![image](https://hackmd.io/_uploads/r1Fh55wNa.png) * 復原caller的相關資訊 結論: 經過上述方法儲存後,整體架構如下: ![image](https://hackmd.io/_uploads/HkJf2cDEa.png) 補充: * Tail Continued Funcitons: 多層function在回傳時,不需要一層層慢慢回傳,而能直接跳回最外層的function以增加效率 * inline function: 在編譯時,直接將程式碼"嵌入"至呼叫的地方 * 優點: 不需要再執行函式呼叫的步驟,速度較快 * 缺點: 若函式被呼叫了很多次,程式碼大小會增加 ## Memory ARM C Compiler會將變數對齊 memory(Align),以提升效率 也可以指定某變數對齊的方式 ``` //指定變數x落在記憶體位址為4的倍數的地方 int x __attribute__ ((align(4))); ``` 這邊針對struct結構考慮 * 一般情況: ![image](https://hackmd.io/_uploads/B1Qi5iw46.png =50%x) * 使用__packed關鍵字,使各變數緊密對齊 ![image](https://hackmd.io/_uploads/H1LJjsw4T.png =50%x) # ARM 組合語言與C code ## 將ARM組合語言嵌入至C Code中 基本格式如下: ![image](https://hackmd.io/_uploads/r1wuaqdV6.png =80%x) * asm-qualifiers的值可以為 * volatile: 表示這段組合語言不可以被編譯優化動到 * inline: 表示這段組合語言以inline的格式嵌入 * goto: 允許使用Branch指令跳到C code中的某個Label (需要多一個參數指定Label) ![image](https://hackmd.io/_uploads/B1vEfsOVp.png) * %l2 表示的是 HERE這個Label (l表示Label),也可以寫成 %l[Here] * 邏輯為: 若a==b,跳至Label Here * 每個operand 的格式如下: ![image](https://hackmd.io/_uploads/HJUFJsOE6.png) * asmSymbolicName: 為對應的變數取別名以提升可讀性,而非%0, %1... * constraint: * "=r": 表示作為輸出 * "r": 表示作為輸入 * cvariablename: 表示對應的變數名稱 ex(本範例沒有asm-qualifiers): ![image](https://hackmd.io/_uploads/r19Jj9O4a.png) * 可以透過以下語法將ARM組合語言嵌入至C Code中 * asm(...) * \_\_asm\_\_(...) * 每一條ARM組合語言指令都使用 "" 括住,後面都加上 \n\t * 因為不知道C語言中的某變數實際上使用的是哪個register,故在組合語言的程式中先使用%0, %1等代替,之後再填入 * %0, %1, %2, %3 分別使用變數p, m, n, k填入 (由上至下、由左至右) * :"=r"(p\): p所代表的register是"作為輸出的register" * :"r"(m): m所代表的register是"作為輸入的register" * :"r2" 表示插入的組合語言指令會用到r2作為暫存變數,告訴編譯器不要使用到 ex: asmSymbolicName使用範例 ![image](https://hackmd.io/_uploads/HJ9Hyj_Vp.png) * [a]便代表變數a所使用的register ## 在ARM 語言中使用C function ex: 使用Printf ![image](https://hackmd.io/_uploads/SyxbOmK4p.png) * 同樣遵守AAPCS的規範,使r0, r1作為傳入的參數 補充: 在編譯時,compiler可能會將C code轉成不符合AAPCS規範但仍等價的程式碼;若希望編譯出的組合語言能嚴格遵守AAPCS規範,則須加上"-mapcs-frame"參數 ![image](https://hackmd.io/_uploads/S1cKOQY4p.png =70%x) ## 在C 語言中使用ARM function ex: call.c 內容 (作為caller呼叫用ARM定義的函式) ![image](https://hackmd.io/_uploads/rJpniQYV6.png) * extern關鍵字表示square這個function來自外部,須交由linker 連結 square.s 內容 (包含了被呼叫的函式定義) ![image](https://hackmd.io/_uploads/HksBnmYVT.png) * .global square: 告訴linker這個有square這個名字 * .type square, %function": 告訴linker square這個名字是function (就像main函式一樣) * 注意為了符合AAPCS的規範,函式的開頭跟結尾須為固定用法 編譯方法(將兩個檔案放在一起編譯): ![image](https://hackmd.io/_uploads/H1p6yNt46.png =50%x) ## 其他觀念 * Position Independent Code: * 程式的執行結果跟在memory中的擺放位置無關 ![image](https://hackmd.io/_uploads/S1KW-HqVp.png =50%x) * 透過紀錄PC與target的"相對位置"來進行branch的跳躍 ![image](https://hackmd.io/_uploads/BkKgWHqVT.png =50%x) * Position Independent Data: * 程式的執行結果與Data section的擺放位置無關 ![image](https://hackmd.io/_uploads/B17UZB5Vp.png =50%x) * 使用base (r5) + offset (Here)的方式來定位資料,而非直接使用絕對位置 ![image](https://hackmd.io/_uploads/SkjjZr54p.png =50%x) # Thumb instruction set * 為了避免浪費空間,Thumb instruction set在相同功能下,節省了一半的空間 * 即每一條Thumb instruction指占用16 bits的空間 * Thumb instruction set可以被視為壓縮後的ARM指令,屬於ARM instruction set的子集,每條Thumb instruction 都必定可以對應至一條 ARM instruction * 透過CPSR中的第五個bit來決定當前指令是不是Thumb instruction ![image](https://hackmd.io/_uploads/r1z8Sw9E6.png =50%x) * 透過BX或BLX指令切換ARM跟Thumb模式 (差異在於BLX會將回去的位置存在r14) * ![image](https://hackmd.io/_uploads/Bk-7_P54p.png =50%x) * Rm中放的是要跳至的記憶體位址 * 若Rm[0]=1,會跳到Thumb * 若Rm[0]=0會跳到ARM * Rm也可以改成Label,此時永遠會跳到Thumb * 為了進一步節省空間 * 一般的Thumb instruction只能使用register r0~r7,故每個register只需要占用3 bits * 大部分的Thumb instruction都是unconditionally * 許多Thumb instruction都是2-address format (某個register同時當參數跟結果) ![image](https://hackmd.io/_uploads/B1D3haoEp.png =50%x) * 一般ARM instruction: 1. 從記憶體中被讀出 2. 進入 instruction pipeline 3. 透過 mux 被選中 4. 交由 ARM instruction decoder 解讀 * Thumb instruction 1. 從記憶體中被讀出 2. 進入 instruction pipeline 3. 透過 mux 選擇上半還是下半解讀 (因為 load 進來是一個 word) 4. 透過 Thumb decompressor 轉成對應的 32-bits instruction 5. 透過 mux 被選中 6. 交由 ARM instruction decoder 解讀 ## Branch instruction 總共有這五種 ![image](https://hackmd.io/_uploads/ry-_Fu9N6.png =50%x) ### B 指令 ![image](https://hackmd.io/_uploads/B1KegO5VT.png =80%x) * 少數仍保留condition的指令 * 因為一條Thumb instruction大小為2 bytes,故Branch instruction只需存二的倍數的位置即可 * 將label所在的記憶體位址**右移一格**後存入指令中 * 讀取指令中的offset後,**左移一格**以對應至真實的記憶體位址 註: 針對unconditional Branch instruction,可以更進一步增加offset的可跳至的距離 ![image](https://hackmd.io/_uploads/BksVW_cNa.png =50%x) ### BL指令 * 因為該類指令可能會需要跳到比較遠的地方,故使用特殊的方法使兩條指令的offset能組合成更大的offset以滿足需求 ![image](https://hackmd.io/_uploads/rkHeYO94T.png =50%x) ![image](https://hackmd.io/_uploads/rkVlDucVT.png =80%x) * H: 透過這個bit決定這條指令的offset是屬於low還是high ![image](https://hackmd.io/_uploads/HkgVt_qNa.png =50%x) ## BX、BLX 如Thumb instruction set介紹,用於切換Thumb及ARM ## SWI指令 ![image](https://hackmd.io/_uploads/B1Mn3sqNa.png =80%x) * 透過8個bit表示想呼叫的服務 ## Data Processing instruction ### shift 因為Thumb instruction 可以使用的bit不夠,故shift指令獨立出來 ![image](https://hackmd.io/_uploads/Bysaps5Ea.png =80%x) * op: 負責決定指令的類型 * offset: 決定左右移幾個bit * 注意: 這類型的指令一定會set condition code flag (ex: MOV**S**) ### ADD、Substract (three address instruction format) ![image](https://hackmd.io/_uploads/H1ERAiqET.png =80%x) * Op: 決定指令類型是加還是減 * bit 10: 決定第二個輸入參數是immediate value還是register * 注意: 這類型的指令一定會set condition code flag (ex: MOV**S**) ### Move、Compare... (two address instruction format) * 比起上面介紹的指令,少了一個輸入參數 ![image](https://hackmd.io/_uploads/BkNoy3cET.png =80%x) * 注意這邊的Rd可以同時作為參數跟結果 * 這邊的CMP是拿一個register跟一個immediate value做比較 * 注意: 除了CMP外,這類型的指令一定會set condition code flag (ex: MOV**S**) ### ALU operation ![image](https://hackmd.io/_uploads/Byxqgn9Np.png =80%x) * 可用的op包含了AND、ADC等等 * 這邊的CMP是拿兩個register做比較 * 注意: 除了CMP外,這類型的指令一定會set condition code flag (ex: MOV**S**) ### ADD PC/SP 專門針對PC及SP位移的指令 ![image](https://hackmd.io/_uploads/By1J739E6.png =80%x) * SP用來決定要位移的是PC 還是 SP * 注意: 因為SP、PC在32 bit ARM 指令集架構中總是指向4的倍數的位置,故Branch instruction只需存四的倍數的位置即可 * 將label所在的記憶體位址**右移兩格**後存入指令中 * 讀取指令中的offset後,**左移兩格**以對應至真實的記憶體位址 * 注意: 這兩種指令不會影響到condition code flag ### Add offset to stack pointer 專門用來操作SP的指令 ![image](https://hackmd.io/_uploads/ryuarncVT.png =80%x) * bit 7: 控制offset 是正的還是負的 * 使用範例: ADD SP, #immediate * 相當於 ADD R13, R13, #immediate * 注意: 因為SP在32 bit ARM 指令集架構中總是指向4的倍數的位置,故Branch instruction只需存四的倍數的位置即可 * 將label所在的記憶體位址**右移兩格**後存入指令中 * 讀取指令中的offset後,**左移兩格**以對應至真實的記憶體位址 ### Hi register Operation 專門用來操作Hi register的operation ![image](https://hackmd.io/_uploads/r1MAq5jN6.png) * 透過H1, H2 來分別決定 destination 跟 sourse 是否為Hi register ex: ![image](https://hackmd.io/_uploads/BkrEics46.png) ### Data transder instruction * PC-Relative Load: 從 PC 指向的位置加上 offset 的地方 load 資料 * ![image](https://hackmd.io/_uploads/BJpX1ojVp.png) * 因為固定只有PC 作為 base,故不需要特別花 bit 存 * ex: LDR R3, [PC, #844] * offset只需存四的倍數的位置即可 * 將label所在的記憶體位址**右移兩格**後存入指令中 * 讀取指令中的offset後,**左移兩格**以對應至真實的記憶體位址 * Load/Store with Register offset: * ![image](https://hackmd.io/_uploads/ByBUn5i4a.png =50%x) * B: 決定要 load/store 一個 byte 還是 word * L: 決定要 load 還是 store * ex: STR R3, [R2, R6] * Load/Store sign-extended byte/halfword * ![image](https://hackmd.io/_uploads/ryhg65iVa.png =50%x) * S: 決定處理的資料類型是否是sign-extended byte * H: 決定處理的資料類型是否是Half word * LDSB R2, [R3, R0] * Load/Store with immediate offset * ![image](https://hackmd.io/_uploads/SkQFJjj46.png =50%x) * B: 決定要 load/store 一個 byte 還是 word * L: 決定要 load 還是 store * ex: LDR R2, [R5, 116] * 注意: offset 沒有特別對齊 * Load/Store Halfword * ![image](https://hackmd.io/_uploads/BJU-JsiNT.png =50%x) * L: 決定要 load 還是 store * ex: STRH R6, [R1, #56] * offset只需存二的倍數的位置即可 (halfword) * 將label所在的記憶體位址**右移一格**後存入指令中 * 讀取指令中的offset後,**左移一格**以對應至真實的記憶體位址 * SP-Relative Load/Store * ![image](https://hackmd.io/_uploads/B1N1ejj4p.png =50%x) * L: 決定要 load 還是 store * ex: STR R4, [SP, #492] * * offset只需存四的倍數的位置即可 * 將label所在的記憶體位址**右移兩格**後存入指令中 * 讀取指令中的offset後,**左移兩格**以對應至真實的記憶體位址 * Push/Pop register * Push負責在進入函式時,將 register 的值存起來,對應至 STMDB 指令 * Pop負責在離開函式時,還原 register 的值,對應至 LDMIA 指令 * ![image](https://hackmd.io/_uploads/S1hmZiiEp.png =50%x) * L: 決定要 load 還是 store * R: 是否要順便處理LR/PC register * ex: PUSH {R0-R4, LR} * Multiple load/store * 永遠會 update base * 只包含了LDMIA、STMIA 兩種指令 * ![image](https://hackmd.io/_uploads/HkQcbjiNa.png =50%x) * L: 決定要 load 還是 store * ex: STMIA R4!, {R1, R2, R3} ## Thumb2 instruction 在略為犧牲效能的情況下,最大幅度的降低 code size * 包含了所有的 16-bit Thumb instruction * 多了一些新的 16-bit instruction 跟32-bit instruction * 可以進行所有一般 ARM 指令可以進行的操作 # ARM Floating point instruction * 透過coprocessor,便可以使 ARM 支援浮點數運算 ![image](https://hackmd.io/_uploads/B1GGq0MHT.png =60%x) * 使用CP10、CP11 作為協助運算的coprocessor * 包含一個 floating-point status and control register (FPSCR) * 類似ARM 原生架構中的 CPSR * 儲存系統當前狀態 * ![image](https://hackmd.io/_uploads/SJrLeRbHp.png =60%x) * N、Z、C、V: condition code flag * RMODE: Rounding mode,用於決定將浮點數轉整數時的規則(共有往最近的取、往 +inf取、往 -inf 取、往0取四種) ![image](https://hackmd.io/_uploads/S1_uKCZST.png =60%x) * 包含一個 Coprocessor access control register(CPACR) * 控制那些功能不可以被一般的 application 存取 * 共有32個 32-bits register 可使用 (s0~s31) * s0~s15: caller saved register * s16~s31: callee saved register * 相鄰的 register 合併可以用來存 64-bits 的值 ## Moving data 補充: * R$_{i}$: 一般 register * S$_{i}$: 32-bits floating point register * D$_{i}$: 64-bits floating point register ### VMOV 指令 ![image](https://hackmd.io/_uploads/B1h2UAbH6.png =60%x) * 可以操作一般 register 跟 floating-point register * 可以做到 * 浮點數 register 搬到另一個浮點數 register * 浮點數 register 搬到一般 register * 一般 register 搬到浮點數 register * 浮點數 register 搬到一般 register (一次搬兩個) * 一般 register 搬到浮點數 register (一次搬兩個) * 只會照搬不誤,並不會做整數跟浮點數之間的轉換 補充: VMOV 指令只能拿有限的常數浮點數作為 immediate value ![image](https://hackmd.io/_uploads/SJJ-5hfSp.png) * 若一個浮點數不斷乘以二最多七次,有任一次乘出來的值是16~31間的整數,則該浮點數是合法的 immediate value * 若想要 load 任意的immediate value,須將該值暫時存在 data section 中,接著 * 將data section 中的 label 使用LDR載入至一般 register 中 * 從該 register 中使用VLDR 載入至 floating point register 中 * ![image](https://hackmd.io/_uploads/SyvUYCfrp.png =20%x) ### VCVT 指令 ![image](https://hackmd.io/_uploads/SyA2D0WBa.png =60%x) * 只能操作floating-point register (s0~s31) * 透過在 "VCVT" 後面加上關鍵字決定要執行哪種轉換 * .F32 * .S32 * .U32 * 可以做到 * 將無號整數轉成浮點數 * 將有號整數轉成浮點數 * 將浮點數轉成無號整數 (truncated,直接截斷) * 將浮點數轉成無號整數 (round,使用 RMODE 定義的方法省略) * 將浮點數轉成有號整數 (truncated) * 將浮點數轉成有號整數 (round) ### VLDR 指令 ![image](https://hackmd.io/_uploads/ryoE_sMST.png =60%x) * label必須在 text section 才可以直接使用 * 若 label 在 data section 則必須間接透過 register 來傳遞 * 都是將某個記憶體位址(只可能由一般 register 存) load 至一個 floating point register 中 * 可以做到 * 從 **register** 中 load 一個 32-bits 浮點數 * 從 **register+偏移量** 中 load 一個 32-bits 浮點數 * 從 label 代表的記憶體位址 中 load 一個 32-bits 浮點數 * 上述三種指令可以改成 load 64-bits 類似的有 LOAD multiple 指令,使用方法同一般 register ![image](https://hackmd.io/_uploads/SJW-iozra.png =60%x) ### VSTR 指令 ![image](https://hackmd.io/_uploads/Hk4qsifSa.pn =60%x) * 可以做到 * 將一個 32-bits 浮點數從 floating point register 中 存至一般**register** 代表的記憶體中 * 將一個 32-bits 浮點數從 floating point register 中 存至一般**register+偏移量** 代表的記憶體中 * 上述三種指令可以改成 store 64-bits * 注意不可直接將值存進一個 label 代表的記憶體中 類似的有 STORE multiple 指令,使用方法同一般 register ![image](https://hackmd.io/_uploads/r1mrhofHa.png =60%x) ## 函式 * input parameter * **S0~S15 拿來作為函式傳入"浮點數"的參數** * **若傳入的參數是指向浮點數的指標,仍然使用R0~R3** * 若 floating point register 還不夠用,則利用 **VPUSH 跟 VPOP 指令**(**只能操作floating point register)存進 stack 中** * return value * **浮點數的回傳結果存在S0** * **指向浮點數的指標或是其他資料型態的回傳結果存在R0** ex: ![image](https://hackmd.io/_uploads/S13pznMSa.png) * 將 R4(callee saved register) 跟 LR 存起來 * 利用 R4 暫存 S0 的值 * 呼叫function bar * 復原 s0 的值至 s1 * 計算回傳值,並放到s0中 * 復原暫存的 R4 跟 LR ## arithmetic instruction * 共有三種操作模式,硬體必須支援才能使用 * .F16 * .F32 * .F64 指令種類 ![image](https://hackmd.io/_uploads/r1xz6TzBT.png =60%x) * 輸入跟輸出一定要是floating point register * 多了除法跟開庚號兩種指令 ## Control flow instruction ![image](https://hackmd.io/_uploads/H1XCCTGSp.png =60%x) * 比較兩個浮點數 register 是否相等,並將結果存在 FPSCR 的condition code flag 中 * 比較一個浮點數 register 是否等於0,並將結果存在 FPSCR 的condition code flag 中 * 將FPSCR 的condition code flag 移動至 CPSR 中 (如此一來才能使用一般的 condition instruction 或是 conditional execution) ![image](https://hackmd.io/_uploads/Hknke0GSp.png =50%x) ex: ![image](https://hackmd.io/_uploads/SyA3X0MH6.png =60%x) * 使用VMRS 指令之後,便可以用VMOV**LT** 等 conditional execution # 其他觀念與指令 ## LDR sudo instruction (重要) 指令 `LDR r0, =0x12345678` (直接從0x12345678 load 資料至 r0 中) 相當於 ``` LDR r0, [pc, #offset] .word 0x12345678 ``` ## MOVW、MOVT 目的是將 immediate value 分段 load 進 32 bits register 中 ``` MOVW r3, #0xFACE @r3 = 0x0000FACE (載入 immediate value 至下半,上半清0) MOVT r3, #0xBEEF @r3 = 0xBEEFFACE (載入 immediate value 至上半) ``` ## CBZ、CBNZ 簡化跟0比較的指令 指令 `CBZ Rn, label` 相當於 ``` CMP Rn, #0 BEQ label ``` 指令 `CBNZ Rn, label` 相當於 ``` CMP Rn, #0 BNE label ``` ## IT instruction 目的是使 Thumb instruction 可以支援 conditional execution 格式: ![image](https://hackmd.io/_uploads/ry7bj6EHT.png =30%x) * 寫下該道指令後,最多可以使後面四行指令支援 conditional execution * 使用方法 * T: then,代表與 cond 相同的修飾字 * E: Else,代表與 cond 相反的修飾字 注意: 若 IT instruction 後面包含了 Branch instruction,則 Branch instruction 只能寫在該指令區塊的最後一行 這樣是不合法的 ![image](https://hackmd.io/_uploads/HJ4A364r6.png =50%x) 這樣是正確的 ![image](https://hackmd.io/_uploads/HkdZaaEra.png =50%x) ex: ``` ITTE NE ANDNE R0, R0, R1 ANDNE R2, R2, #1 MOVEQ R2, R3 ``` * 原本thumb instruction 不支援 conditional execution * 透過 `ITTE NE` 這行指令,便可使後面的指令支援 conditional execution * I 後面有三個字母,代表該指令可以使後面三行支援 conditional execution * TTE 分別對應 NE, NE, EQ * 雖然 `ITTE NE` 這行指令已經可以決定後面指令的 condition;但語法上,後面的三行指令仍要加上 conditional execution 的修飾字 ## Unified Assembly Language (UAL) * UAL 是一種整合不同種類 ARM 語言的語法,目的是讓使用者能以統一的語法撰寫組合語言,最後再決定要轉成 ARM 還是 Thumb instruction * 此時,稱之前學過、未被整合的程式語言為 "Pre-UAL" * 透過 assembly directive 決定該組合語言是哪種類型 * .syntax unified: 代表該組合語言是 UAL * .syntax divided: 代表該組合語言是 Pre-UAL (預設) * 若組合語言類型是 UAL,則可以透過 assembly directive 決定要轉成 ARM 還是 Thumb instruction * .code .arm: 轉成 ARM 組合語言 * .code .thumb: 轉成 Thumb 組合語言 ### UAL 與 Pre-UAL 的不同之處 1. LSL 指令獨立出來,不再依附在其他指令後 2. load/store multiple 指令只支援 "IA" 3. 可以用一行指令將一個 64-bits instruction 直接 load 至兩個 register 中 # SIMD 指令 * SIMD 指的是 Single instruction Multiple data,是一種由 processor(硬體) 提供的指令 * SIMD 指令 **同時對多筆資料進行同一種運算**,故只需一時間單位便能完成運算 ![image](https://hackmd.io/_uploads/SJ6EOQyIa.png =50%x) ## 使用 SIMD 指令 想要享受 SIMD 指令 帶來的加速效果,**可以透過以下方式** ### Assembly language 開發者直接寫對應的組合語言來使用 SIMD 指令 * 優點: 有彈性 * 缺點: 開發困難 ### Intrinsic function 使用編譯器內部提供的低階 function * 優點: 不需要處理 register 相關的問題,但卻能操作底層指令 * 缺點: 開發還是有點困難 低階 function api: _mm_add_epi32: 將兩個 \__m128i 型態的變數相加 _mm_sub_epi32: 將兩個 \__m128i 型態的變數相減 _mm_srli_si128: 將指定 \__m128i 型態的變數右移 k 個 bytes 後,回傳結果 (不動到變數本身) _mm_cvtsi128_si32: 將最後一個位置回傳 _mm_hadd_ps: ![image](https://hackmd.io/_uploads/rkOew5lIT.png =50%x) ex: **將陣列中的所有元素相加** (此範例重要,描述將一般邏輯轉成 SIMD 的流程) ![image](https://hackmd.io/_uploads/H1oQsEJL6.png =50%x) ![image](https://hackmd.io/_uploads/SkGUjEJLa.png) * 宣告 \__m128i 型態的變數 accum (可以看成 register 陣列) * 宣告\__m128i 型態的指標,指向傳入陣列的開頭 ![image](https://hackmd.io/_uploads/r1NTiVJIp.png =30%x) * \因為 __m128i 是 int 大小 32bits 的四倍,故這段連續的空間會被切成每 128bits 一個單位 * 清空 accum * _mm_add_epi32 可以一次處理四個 int (128 bits),故迴圈只需要原本的 1/4 次即可 * 註: 若陣列長度沒辦法整除四,可以將沒辦法整除的部分改成用一般的指令算;或視運算情況補0補到四的倍數, * [1,2,3,4] + [5,6,7,8] = [6,8,10,12] * 接著要將 [6,8,10,12] 四個數字相加 * [6,8,10,12] + [-,-,6,8] = [-,-,10+6,12+8] * [-,-,10+6,12+8] + [-,-,-,10+6] = [-,-,-,12+8+10+6] * 使用 _mm_cvtsi128_si32 將 最後一個位置的數字回傳 ### Language Extension 可以用接近高階語言的方式操作 SIMD 指令,例如透過 C++ class data type 優點: 可讀性高,容易開發因為交由 compiler 產生組合語言,故 performance 能受到保證 缺點: 彈性較差、可能沒辦法使用 SIMD 支援的所有功能 ex: 將陣列中的所有元素相加 ![image](https://hackmd.io/_uploads/H1EKRNkIT.png =50%x) ### Auto-Vectorization 透過 compile 原生的優化(-O2 等)來使用 SIMD 指令 優點: 很方便 缺點: 只能優化邏輯單純的部分 ## SIMD 指令與資料對齊 * 資料對齊能提升性能,而在 SSE/SSE2/SSE3 這三個SIMD指令集架構中,除了少數指令外,皆**強迫要求對齊資料** ### 資料對齊的方法 1. 之前提到的 \_\_attribute\_\_ (適用於linux 中的 靜態變數) ![image](https://hackmd.io/_uploads/HJky0tgLa.png =70%x) 2. _mm_malloc() (適用於動態配置的變數),可以直接指定 malloc 的記憶體要aligned 哪裡 * 概念等價於 * 假設要求 16-bytes aligned * 會先使用 malloc 配置"所需記憶體+15" 大小的空間 * 接著將指標移至16-bytes aligned的位置,並作為回傳值 ## SIMD 與 Data padding 若要分別計算 AW, BX, CY, DZ 方法一的資料排列 (需要 4 個 SIMD 乘法指令,12 個 SIMD 加法指令) ![image](https://hackmd.io/_uploads/SJ6Ni-M_6.png) 方法二的資料排列 (需要 4 個 SIMD 乘法指令,3 個 SIMD 加法指令,較佳) ![image](https://hackmd.io/_uploads/ByGrkix8a.png) ## ARM SIMD * 在 ARM v6 中,都是使用 32 bits register 來進行 SIMD 指令的 (可以同時做兩個 16 bits 或 4個 8 bits instruction) * 在 ARM v7 中,新增了 NEON 系列指令,多了 64-bits register (D register, ARM 本身提供)、128-bits register (Q register, 由兩個 D 合併而成) 格式範例: ![image](https://hackmd.io/_uploads/BJ4VM58Ia.png =50%x) * 一個 Q register 為 128 bits * 又指令為 "I16" 格式 (integer, 16 bits) * 故這條指令能一次處理 8 筆資料 ## X86 SIMD (GCC) * 想要在 X86 架構下使用 SIMD 系列指令,可以使用以下方法 1. 使用 Intrinsic function (編譯器內部提供的低階 function) 2. 定義新的資料型態,之後在使用時,compiler 會根據硬體是否支援來編譯成對應的組合語言指令 ![image](https://hackmd.io/_uploads/Sk38P3U8T.png) * 宣告 16 bytes, 每個元素是 int 的陣列 (共四個元素),作為新的形態 * 宣告 v4si 型態的三個變數 * 可以用類似 operator overloading 的方式直接運算