從src/isa/riscv32/inst.c
出發。
向上搜索,理解宏定義的含義。
R(i)
#define R(i) gpr(i)
R(i)
:訪問第i號通用寄存器
會被替換為:
#define gpr(idx) (cpu.gpr[check_reg_idx(idx)])
分為兩個部分:
cpu.gpr
check_reg_idx
cpu.gpr
的每個含義,在預學習的時候已經接觸過了。
對于check_reg_idx
,可見參數為一個int
,那么宏定義gpr
和R
的參數也是int
。
static inline int check_reg_idx(int idx) {IFDEF(CONFIG_RT_CHECK, assert(idx >= 0 && idx < MUXDEF(CONFIG_RVE, 16, 32)));return idx;
}
先看IFDEF
:
#define IFDEF(macro, ...) MUXDEF(macro, __KEEP, __IGNORE)(__VA_ARGS__)
又冒出來新的宏定義。
需要找MUXDEF
:
#define MUXDEF(macro, X, Y) MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)
又又冒出來新的宏定義。
如此遞歸,整理得到:
#define IFDEF(macro, ...) MUXDEF(macro, __KEEP, __IGNORE)(__VA_ARGS__)
#define MUXDEF(macro, X, Y) MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)
#define MUX_MACRO_PROPERTY(p, macro, a, b) MUX_WITH_COMMA(concat(p, macro), a, b)
#define MUX_WITH_COMMA(contain_comma, a, b) CHOOSE2nd(contain_comma a, b)
#define CHOOSE2nd(a, b, ...) b
CHOOSE2nd
遞推的終點是:#define CHOOSE2nd(a, b, ...) b
從宏的名字和定義可以看出,這個宏的作用是:從可變參數中選擇第二個參數。
測試一下,如果參數小于2
怎么辦。
error: macro "CHOOSE2nd" requires 3 arguments, but only 1 given6 | cout << CHOOSE2nd(1) << endl;
報錯信息雖然顯示的是三個參數,但其實兩個就夠了。
MUX_WITH_COMMA
非常細節的逗號:
#define MUX_WITH_COMMA(contain_comma, a, b) CHOOSE2nd(contain_comma a, b)
無論如何都會選中b
,意義何在?接著看看。
MUX_MACRO_PROPERTY
#define concat_temp(x, y) x ## y
#define concat(x, y) concat_temp(x, y)
#define CHOOSE2nd(a, b, ...) b
#define MUX_WITH_COMMA(contain_comma, a, b) CHOOSE2nd(contain_comma a, b)
#define MUX_MACRO_PROPERTY(p, macro, a, b) MUX_WITH_COMMA(concat(p, macro), a, b)
經測試,無論前兩個參數是啥,結果都是第四個參數。
- 這個宏的作用是?接著看看
MUXDEF
#define MUXDEF(macro, X, Y) MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)
#include <iostream>
using namespace std;
#define concat_temp(x, y) x ## y
#define concat(x, y) concat_temp(x, y)
#define CHOOSE2nd(a, b, ...) b
#define MUX_WITH_COMMA(contain_comma, a, b) CHOOSE2nd(contain_comma a, b)
#define MUX_MACRO_PROPERTY(p, macro, a, b) MUX_WITH_COMMA(concat(p, macro), a, b)
#define __P_DEF_0 X,
#define __P_DEF_1 X,
#define __P_ONE_1 X,
#define __P_ZERO_0 X,
#define MUXDEF(macro, X, Y) MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)
#define A
#define B 1
#define C 2int main() {cout << MUXDEF(A, 1, 2) << endl;cout << MUXDEF(B, 1, 2) << endl;cout << MUXDEF(C, 1, 2) << endl;cout << MUXDEF(1, 1, 2) << endl;cout << MUXDEF(0, 1, 2) << endl;
}
經測試,當拼接后的__P_DEF_macro
有定義時,會返回X
,否則返回Y
。
到這里,輸出的結果就不再是固定的了。
回頭看一下,依次展開:
MUXDEF(macro, X, Y)
會展開為:CHOOSE2nd(__P_DEF_macro X, Y)
但似乎還是只返回Y
,為什么會返回X
?看下面的函數:
#define concat_temp(x, y) x ## y
#define concat(x, y) concat_temp(x, y)
調用兩個函數,結果是不一樣的:
concat_temp(__P_DEF_, A)
:__P_DEF_A
concat(__P_DEF_, A)
:1,
哪來的逗號?
#define __P_DEF_1 X,
- 非常細節的宏定義,
X
后有一個逗號。
- 非常細節的宏定義,
concat(__P_DEF_, A)
的展開結果為:
concat(__P_DEF_, A)
concat_temp(__P_DEF_, 1)
__P_DEF_1
X,
這個的X,
與MUX_WITH_COMMA
省略的逗號結合。
如果A
被定義為0
或1
,那么展開后,contain_comma a
會變成X,a
,使a
成為第二個元素。
實際效果為宏定義下的?:
三元運算符。
再回頭看,那個流程圖展開是有問題的。
宏定義不會遞推到最后一層再展開,參考concat(__P_DEF_, A)
的展開過程,A
在第一步就展開了,它的展開結果會影響下一步展開。
對于整條鏈路的入口:MUXDEF(CONFIG_RVE, 16, 32))
- 如果定義了
CONFIG_RVE
為1
或0
,那么編譯16
,否則32
IFDEF
還有一個很費勁的宏定義,出現了三層括號。
#define __IGNORE(...)
#define __KEEP(...) __VA_ARGS__
#define IFDEF(macro, ...) MUXDEF(macro, __KEEP, __IGNORE)(__VA_ARGS__)
有一個非常關鍵的關鍵字:__VA_ARGS__
會取出可變參數的值,也就是...
的部分。
比如IFDEF(A, cout<<1<<endl;)
,會先展開為:
MUXDEF(A, __KEEP, __IGNORE)(cout<<1<<endl;)
前文已經知道,MUXDEF
在第一個參數定義為1
或0
時,會編譯為第二個參數。
那么就變成了:
__KEEP(cout<<1<<endl;)
__KEEP
會編譯為參數列表,也就是:cout<<1<<endl;
第三個括號等第二個括號解析完成后作為參數傳入。
總結
IFDEF(CONFIG_RT_CHECK, assert(idx >= 0 && idx < MUXDEF(CONFIG_RVE, 16, 32)));
- 作用是判斷,是否檢查寄存器越界訪問
R(i)
- 作用是取出第
i
個寄存器的值。
一串宏定義的作用是判斷取值的時候要不要檢查。
Mr/Mw
#define Mr vaddr_read
這個函數在預學習的時候也用到過,現在順著這個函數把宏定義捋一下。
首先是Mr
后面沒帶括號,是給vaddr_read
這個函數起了個別名。
vaddr_read
是調用了paddr_read
這個函數。
word_t paddr_read(paddr_t addr, int len) {if (likely(in_pmem(addr))) return pmem_read(addr, len);IFDEF(CONFIG_DEVICE, return mmio_read(addr, len));out_of_bound(addr);return 0;
}
現在又出現了多個宏定義。
likely
#define likely(cond) __builtin_expect(cond, 1)
告訴編譯器,cond
的值期望為1
。
__builtin_expect(expr, expected) 的返回值就是 expr 的值本身。
它的作用不是改變值,而是告訴編譯器你“預期這個值通常為 expected(通常是 0 或 1)”,以便編譯器做出更好的分支預測和優化。
static inline bool in_pmem(paddr_t addr) {return addr - CONFIG_MBASE < CONFIG_MSIZE;
}
in_pmem
的作用是判斷地址是否合法。通過與地址偏移量運算得到。
static word_t pmem_read(paddr_t addr, int len) {word_t ret = host_read(guest_to_host(addr), len);return ret;
}
pmem_read
的作用是從客戶機的物理內存地址addr
開始,讀取len
字節的數據,并返回對應的值。
static inline word_t host_read(void *addr, int len) {switch (len) {case 1: return *(uint8_t *)addr;case 2: return *(uint16_t *)addr;case 4: return *(uint32_t *)addr;IFDEF(CONFIG_ISA64, case 8: return *(uint64_t *)addr);default: MUXDEF(CONFIG_RT_CHECK, assert(0), return 0);}
}
len
只有1,2,4,8
四種取值。也就是取出addr
開始的1,2,4,8
個字節的數據。
default: MUXDEF(CONFIG_RT_CHECK, assert(0), return 0);
- 如果定義了
CONFIG_RT_CHECK
,那么非法的len
會觸發斷言 - 如果未定義
CONFIG_RT_CHECK
,那么非法的len
會被忽略,返回0
host_write
的函數體與host_read
邏輯類似。
static inline void host_write(void *addr, int len, word_t data) {switch (len) {case 1: *(uint8_t *)addr = data; return;case 2: *(uint16_t *)addr = data; return;case 4: *(uint32_t *)addr = data; return;IFDEF(CONFIG_ISA64, case 8: *(uint64_t *)addr = data; return);IFDEF(CONFIG_RT_CHECK, default: assert(0));}
}
imm*
這段宏定義在下面的decode_operand()
中使用。
BITS
#define BITS(x, hi, lo) (((x) >> (lo)) & BITMASK((hi) - (lo) + 1)) // similar to x[hi:lo] in verilog
- 提取
x
的第hi
到lo
位(閉區間)
運算分為兩個部分:((x) >> (lo))
與BITMASK((hi) - (lo) + 1)
先把低位干掉,然后取出新的地位。
BITMASK
#define BITMASK(bits) ((1ull << (bits)) - 1)
生成低bits
位全是1
的掩碼。
ull
避免溢出。
SEXT
#define SEXT(x, len) ({ struct { int64_t n : len; } __x = { .n = x }; (uint64_t)__x.n; })
寫個程序測試一下功能。
({ ... })
- 這是
GCC
和Clang
支持的一種語法糖,用于將一個代碼塊作為一個表達式返回值。不能在標準C
中使用。
在語法糖內部,有兩條語句:
struct { int64_t n : len; } __x = { .n = x };
struct { int64_t n : len; }
定義了一個匿名結構體,變量n
只取第n
位。__x = { .n = x }
創建了一個結構體變量__x
,.n
被賦值為x
,高位會被截斷。
(uint64_t)__x.n;
- 把階段的位域強轉為
uint64_t
,并作為表達式結果。
- 把階段的位域強轉為
那么SEXT
的作用就是:
- 將
x
看作一個len
位的有符號整數,對其進行“符號擴展”為64
位整數,并以uint64_t
類型返回其值。
immI
#define immI() do { *imm = SEXT(BITS(i, 31, 20), 12); } while(0)
- 取出
32
位指令中的位段[31:20]
- 將它作為
12
位 有符號立即數 符號擴展成64
位 - 然后賦值給
*imm
immu
#define immU() do { *imm = SEXT(BITS(i, 31, 12), 20) << 12; } while(0)
- 取出
32
位指令中的位段[31:12]
- 然后左移
12
位形成最終的32
位立即數
imms
#define immS() do { *imm = (SEXT(BITS(i, 31, 25), 7) << 5) | BITS(i, 11, 7); } while(0)
- 取出:高
7
位:i[31:25]
和低5
位:i[11:7]
- 將高
7
位符號擴展,再左移5
位 - 與低
5
位做按位或,合并成完整的12
位立即數
文獻來源
- https://drive.google.com/file/d/1uviu1nH-tScFfgrovvFCrj7Omv8tFtkp/view?usp=drive_link
- Page26
decode_exec
函數里面嵌套宏定義的寫法暫時看不懂。
向上搜索調用鏈:
int isa_exec_once(Decode *s) {s->isa.inst = inst_fetch(&s->snpc, 4);return decode_exec(s);
}
inst_fetch
調用到vaddr_ifetch
時,可以發現,與vaddr_read
接下來的走向如出一轍。
int isa_exec_once(Decode *s) {s->isa.inst = inst_fetch(&s->snpc, 4);return decode_exec(s);
}
static inline uint32_t inst_fetch(vaddr_t *pc, int len) {uint32_t inst = vaddr_ifetch(*pc, len);(*pc) += len;return inst;
}
isa_exec_once
的作用是,取出從&s->snpc
處,長為4
字節的指令。
- 也就是
32
位指令。
并更新snpc
為下一個位置。
snpc
在PA2
手冊中有提到:
snpc是下一條靜態指令, 而dnpc是下一條動態指令. 對于順序執行的指令, 它們的snpc和dnpc是一樣的; 但對于跳轉指令, snpc和dnpc就會有所不同, dnpc應該指向跳轉目標的指令. 顯然, 我們應該使用s->dnpc來更新PC, 并且在指令執行的過程中正確地維護s->dnpc
decode_exec
static int decode_exec(Decode *s) {s->dnpc = s->snpc;#define INSTPAT_INST(s) ((s)->isa.inst)
#define INSTPAT_MATCH(s, name, type, ... /* execute body */ ) { \int rd = 0; \word_t src1 = 0, src2 = 0, imm = 0; \decode_operand(s, &rd, &src1, &src2, &imm, concat(TYPE_, type)); \__VA_ARGS__ ; \
}INSTPAT_START();INSTPAT("??????? ????? ????? ??? ????? 00101 11", auipc , U, R(rd) = s->pc + imm);INSTPAT("??????? ????? ????? 100 ????? 00000 11", lbu , I, R(rd) = Mr(src1 + imm, 1));INSTPAT("??????? ????? ????? 000 ????? 01000 11", sb , S, Mw(src1 + imm, 1, src2));INSTPAT("0000000 00001 00000 000 00000 11100 11", ebreak , N, NEMUTRAP(s->pc, R(10))); // R(10) is $a0INSTPAT("??????? ????? ????? ??? ????? ????? ??", inv , N, INV(s->pc));INSTPAT_END();R(0) = 0; // reset $zero to 0return 0;
}
在decode_exec
的頭部,把dnpc
賦值為snpc
。表示默認下一條指令就在下一個字節的位置。
中間兩端宏定義暫時看不懂,但是好在暫時沒有調用,只是定義:
#define INSTPAT_INST(s) ((s)->isa.inst)
#define INSTPAT_MATCH(s, name, type, ... /* execute body */ ) { \int rd = 0; \word_t src1 = 0, src2 = 0, imm = 0; \decode_operand(s, &rd, &src1, &src2, &imm, concat(TYPE_, type)); \__VA_ARGS__ ; \
}
可以接著往下看:
INSTPAT_START
第二條要執行的語句是:INSTPAT_START();
#define INSTPAT_START(name) { const void * __instpat_end = &&concat(__instpat_end_, name);
&&label
是標簽地址,官方文檔鏈接:https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html
具體的功能可以寫一個函數測試一下:
int main() {void *ptr = &&label1;goto *ptr;printf("hello world\n");
label1:printf("Jumped to label1!\n");return 0;
}
INSTPAT_START()
傳入的是空參數,展開的結果為:
{const void *__instpat_end = &&__instpat_end_;
非常細節的大括號,作用需要搭配INSTPAT_END()
來理解:
__instpat_end_ :;
}
強制地提示,INSTPAT_START
應與INSTPAT_END
成對出現。
并且限制了作用域。
INSTPAT_END
放置在函數體結尾。
INSTPAT
#define INSTPAT(pattern, ...) do { \uint64_t key, mask, shift; \pattern_decode(pattern, STRLEN(pattern), &key, &mask, &shift); \if ((((uint64_t)INSTPAT_INST(s) >> shift) & mask) == key) { \INSTPAT_MATCH(s, ##__VA_ARGS__); \goto *(__instpat_end); \} \
} while (0)
這段宏定義的內容是定義了一段代碼,do...wihile
保證按期望運行。
uint64_t key, mask, shift;
聲明了一些變量
pattern_decode(pattern, STRLEN(pattern), &key, &mask, &shift);
進到這個函數看下是如何運作的。
pattern_decode
定義了一堆宏定義,看起來比較復雜。
macro
#define macro(i) \if ((i) >= len) goto finish; \else { \char c = str[i]; \if (c != ' ') { \Assert(c == '0' || c == '1' || c == '?', \"invalid character '%c' in pattern string", c); \__key = (__key << 1) | (c == '1' ? 1 : 0); \__mask = (__mask << 1) | (c == '?' ? 0 : 1); \__shift = (c == '?' ? __shift + 1 : 0); \} \}
if ((i) >= len) goto finish;
len
定義自:pattern_decode(pattern, STRLEN(pattern), &key, &mask, &shift);
也就是str
的長度。
思考一下,macro64
展開后能覆蓋0-63
,但字符串長度是64
,支持的最大長度是63
還是64
?
可以寫個程序測試下。
字符串長度為64
時輸出了pattern too long
。
pattern_decode
函數的作用是,從一個長度為len
的字符串str
中解析出三種信息:
key
:把所有'0'
和'1'
字符組成一個位串,表示匹配值mask
:每一位如果是'0'
或'1'
則為1
,如果是'?'
則為0
,表示哪些位需要匹配shift
:表示尾部連續'?'
的數量,這些位會被右移舍棄掉
回到INSTPAT
if ((((uint64_t)INSTPAT_INST(s) >> shift) & mask) == key)
這里為什么key
不用右移?
因為pattern_decode
中已經右移過了:
finish:*key = __key >> __shift;*mask = __mask >> __shift;*shift = __shift;
指令匹配成功之后,會執行INSTPAT_MATCH
,然后goto
到結尾的位置。
類似一堆if-else
。
INSTPAT_MATCH(s, ##__VA_ARGS__); \goto *(__instpat_end); \
INSTPAT_MATCH
INSTPAT_MATCH
的入參為##__VA_ARGS__
,在參數為空時,會自動去掉前面的逗號,避免編譯報錯。
#define INSTPAT_MATCH(s, name, type, ... /* execute body */ ) { \int rd = 0; \word_t src1 = 0, src2 = 0, imm = 0; \decode_operand(s, &rd, &src1, &src2, &imm, concat(TYPE_, type)); \__VA_ARGS__ ; \
}
發現name
和type
即使傳入空置也不會影響目前的編譯。
decode_operand
static void decode_operand(Decode *s, int *rd, word_t *src1, word_t *src2, word_t *imm, int type) {uint32_t i = s->isa.inst;int rs1 = BITS(i, 19, 15);int rs2 = BITS(i, 24, 20);*rd = BITS(i, 11, 7);switch (type) {case TYPE_I: src1R(); immI(); break;case TYPE_U: immU(); break;case TYPE_S: src1R(); src2R(); immS(); break;case TYPE_N: break;default: panic("unsupported type = %d", type);}
}
第一個參數就是當前正在解碼的指令上下文,封裝了機器碼值、指令地址等參數。
uint32_t i = s->isa.inst;
int rs1 = BITS(i, 19, 15);
int rs2 = BITS(i, 24, 20);
*rd = BITS(i, 11, 7);
分別取出源寄存器1、源寄存器2和目的寄存器。
這三個寄存器的位置是固定的,在RSICV
官方手冊中的出處:
還是這張圖。
每個寄存器不一定都能用到。但是每種類型的指令,只要用到了,位置就是固定的。
有個細節,上面的代碼取寄存器的時候,只有rd
是指針解引用賦值,其他參數是局部變量,對函數外暫時沒有產生影響。
對于I
型指令,需要immI
、rs1
和rd
。
對于U
型指令,需要immU
和rd
。
對于S
型指令,需要immS
、rs2
、rs1
和rd
。
rd
對應手冊中的imm[4:0]
,可以發現位置完全一樣。
對于R
型指令,看手冊定義,格式與S
型一致,猜測后續執行時會復用S
型指令的邏輯。
到這里,decode_operand
函數的意義已經非常明確了:
- 根據不同的指令類型,取出操作數。
VA_ARGS
把可變參數展開。
結合已有代碼:INSTPAT("??????? ????? ????? ??? ????? 00101 11", auipc , U, R(rd) = s->pc + imm);
首先會嘗試與字符串模板匹配:"??????? ????? ????? ??? ????? 00101 11"
如果匹配成功,會展開INSTPAT_MATCH
:
s
,在decode_exec
函數入參中傳入name
,INSTPAT
的第二個參數auipc
type
,INSTPAT
的第三個參數U
...
,INSTPAT
的第四個參數R(rd) = s->pc + imm)
name
目前來看無關緊要。
展開后會先根據type
取出操作數。
然后展開...
,操作取出的操作數。
總結
INSTPAT_START
和INSTPAT_END
成對出現。
中間處理指令,某條規則匹配成功后,會立即執行并不再繼續向下匹配。
INSTPAT
的參數是:
- 匹配規則
- 指令名字
- 指令類型
- 執行語義,傳入的應該是一系列函數。
參考
- https://ysyx.oscc.cc/docs/ics-pa/2.2.html#rtfsc-2