⚠️ 本文所使用的 QEMU 版本為:v4.2.0
在之前的文章中 (Part 1., Part 2.) 我們提到了如何使用 Decodetree 來定義指令的 decoder。本篇文章就實際使用 Decodetree 來定義一個 QEMU RISC-V 目前尚未支援的指令 - B(itmanip) Extension 中的 pcnt 指令,並實做其行為。
pcnt 指令#
pcnt 指令的定義如下:
This instruction counts the number of 1 bits in a register. This operations is known as population
count, popcount, sideways sum, bit summation, or Hamming weight.
其指令格式為:
1
2
3
| | 1 0 9 8 7 6 5 | 4 3 2 1 0 | 9 8 7 6 5 | 4 3 2 | 1 0 9 8 7 | 6 5 4 3 2 1 0 |
|===========================================================================|
| 0110000 | 00010 | rs1 | 001 | rd | 0010011 | PCNT
|
由於 B Extension 尚未正式定稿 (Draft),因此必須至 riscv-bitmanip repo 下載 toolchain,並依照該 repo 的指示安裝:
1
2
3
| sudo mkdir /opt/riscv64b
sudo chown $USER: /opt/riscv64b
bash build-all.sh
|
此安裝除了 toolchain 外,還會安裝支援 B Extension 的 Spike (riscv-isa-sim) 及 riscv-pk (P.S. 目前的 script 是寫死安裝路徑為:/opt/riscv64b)。
範例程式#
在安裝好後,我們可以寫一個範例程式,並搭配 Spike 來做測試:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| #include <stdio.h>
int count_set_bits(int num)
{
int count;
__asm__("pcnt %0, %1\n"
: "=r"(count)
: "r"(num)
:);
return count;
}
int main(void)
{
int num = 187;
int result = count_set_bits(num);
printf("num = %d\n", num);
printf("# of set bits = %d\n", result);
return 0;
}
|
此範例程式做的事情很簡單,透過 inline assembly:pcnt 指令,將 int num = 187 的 1 bits 個數給計算出來。
透過剛剛的 toolchain 編譯此程式:
1
| /opt/riscv64b/bin/riscv64-unknown-elf-gcc -Wall -march=rv64gb -Os -o pcnt_example pcnt_example.c
|
- -march=rv64gb:指定 target ISA 為
RISC-V 64-bit + g (IMAFD base) + b (B Extension)。
透過 objdump 觀看其反組譯碼:
1
| /opt/riscv64b/bin/riscv64-unknown-elf-objdump -S pcnt_example > pcnt_example.s
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| pcnt_example: file format elf64-littleriscv
Disassembly of section .text:
00000000000100b0 < main >:
100b0: 00017537 lui a0,0x17
100b4: ff010113 addi sp,sp,-16
100b8: 0bb00593 li a1,187
100bc: 8b850513 addi a0,a0,-1864 # 168b8 <__trunctfdf2+0x2ce>
100c0: 00113423 sd ra,8(sp)
100c4: 00813023 sd s0,0(sp)
100c8: 2ac000ef jal ra,10374 <printf>
100cc: 0bb00413 li s0,187
100d0: 00017537 lui a0,0x17
100d4: 60241413 pcnt s0,s0
100d8: 0004041b sext.w s0,s0
100dc: 00040593 mv a1,s0
100e0: 8c850513 addi a0,a0,-1848 # 168c8 <__trunctfdf2+0x2de>
100e4: 290000ef jal ra,10374 <printf>
100e8: 00813083 ld ra,8(sp)
100ec: 00013403 ld s0,0(sp)
100f0: 00000513 li a0,0
100f4: 01010113 addi sp,sp,16
100f8: 00008067 ret
|
可以看到 Line:15 呼叫了 pcnt 指令:pcnt s0, s0。
透過 Spike 執行程式:
1
| /opt/riscv64b/bin/spike --isa=RV64GCB pk pcnt_example
|
1
2
3
| bbl loader
num = 187
# of set bits = 6
|
187 的二進位為 10111011,1 bits 個數為 6,與程式輸出的結果一致。
同樣的程式,我們使用 QEMU 來執行:
1
| ./qemu/riscv64-linux-user/qemu-riscv64 pcnt_example
|
1
2
| num = 187
Illegal instruction
|
可以看到,目前 QEMU 尚未支援 pcnt 指令,因此當執行到 pcnt 指令時,便會噴 Illegal instruction 的錯誤訊息。
在 QEMU 中新增 pcnt 指令#
根據前述 B Extension spec. 所列的 pcnt 指令格式,參考目前 QEMU RISC-V 現有的 Decodetree:target/riscv/insn32.decode,pcnt 的 Pattern 可以搭配 @r2 的 Format (只有 rs1 及 rd 這兩個 Fields),其完整定義如下:
Field#
64
| @r2 ....... ..... ..... ... ..... ....... %rs1 %rd
|
Pattern#
我們可以定義 pcnt 的 Pattern 如下:
207
208
| # *** RV32B Standard Extension ***
pcnt 0110000 00010 ..... 001 ..... 0010011 @r2
|
所會產生的 decoder 如下:
38
39
40
41
| typedef struct {
int rd;
int rs1;
} arg_decode_insn3213;
|
491
492
493
494
495
| static void decode_insn32_extract_r2(DisasContext *ctx, arg_decode_insn3213 *a, uint32_t insn)
{
a->rs1 = extract32(insn, 15, 5);
a->rd = extract32(insn, 7, 5);
}
|
673
674
675
676
677
678
679
680
681
682
683
684
| case 0x1:
/* 01...... ........ .001.... .0010011 */
decode_insn32_extract_r2(ctx, &u.f_decode_insn3213, insn);
switch ((insn >> 20) & 0x3ff) {
case 0x202:
/* 01100000 0010.... .001.... .0010011 */
/* /home/frankchang/qemu/target/riscv/insn32.decode:208 */
if (trans_pcnt(ctx, &u.f_decode_insn3213)) return true;
return false;
}
return false;
}
|
由於 @r2 Format 並沒有參考任何的 Argument Set,因此 Decodetree 會自動根據 Format 所參考到的 Fields (rs1、rd) 動態產生 argument set struct: arg_decode_insn3213。
此外,由 pcnt Pattern 所產生的 decode function 會呼叫 decode_insn32_extract_r2() 這個 extract function 來解析指令中 rs2 及 rd 欄位的值,並更新所傳入 arg_decode_insn3213 對應的欄位,而後再呼叫 trans_pcnt() 來執行 pcnt 指令 (產生對應的 TCG ops)。因此,我們還必須定義 trans_pcnt() 來實作 pcnt 的指令行為。
參考 B Extension spec. 中,pcnt 指令的實作:
1
2
3
4
5
6
7
| uint_xlen_t pcnt(uint_xlen_t rs1)
{
int count = 0;
for (int index = 0; index < XLEN; index++)
count += (rs1 >> index) & 1;
return count;
}
|
及 Spike 中,pcnt 指令的實作:
1
2
3
4
5
| require_extension('B');
reg_t x = 0;
for (int i = 0; i < xlen; i++)
if (1 & (RS1 >> i)) x++;
WRITE_RD(sext_xlen(x));
|
實作很簡單,每次迴圈 right shift rs1 i 個 bits 並與 1 做 AND,若為 true 就將 count 加 1,最後回傳的 count 就是 1 bits 個數。
trans_pcnt() 實作了 pcnt 指令對應的 TCG ops。QEMU 在執行時,會將 target instructions (e.g. RISC-V instructions) 轉譯成 TCG ops,而 TCG ops 則會再轉譯為 host instructions (e.g. x86 instruction)。
1
2
3
4
| +---------------------+ +---------+ +-------------------+
| Target Instructions | ---> | TCG ops | ---> | Host instructions |
+---------------------+ +---------+ +-------------------+
(e.g. RISC-V) (e.g. x86)
|
關於 TCG 的說明,可以參考 QEMU 的 documentations:Translator Internals、TCG README。
新增一檔案:./target/riscv/insn_trans/trans_rvb.inc.c 來定義 B Extension 指令的實作 (當然,目前只有 pcnt 指令):
1
2
3
4
5
6
7
8
9
10
11
12
13
| /*
* RISC-V translation routines for the RVB Standard Extension.
*/
static bool trans_pcnt(DisasContext *ctx, arg_pcnt *a) {
if (a->rd != 0) {
TCGv t0 = tcg_temp_new();
gen_get_gpr(t0, a->rs1);
gen_helper_pcnt(cpu_gpr[a->rd], t0);
tcg_temp_free(t0);
}
return true;
}
|
由於對 x0 (zero register) 的寫入都會被忽略,因此首先判斷 rd 是否為 0,若為 0 則不做任何的事情。
再來宣告一 TCG variable:t0,並透過 gen_get_gpr() 將 rs1 暫存器的值 (如 pcnt_example 中 pcnt s0, s0 指令,rs1 即為 s0,也就是 x8),載入到 t0。
這邊還呼叫了我們所定義幫我們處理 pcnt 計算 1 bits 個數的 pcnt helper function:gen_helper_pcnt()。該 helper function 會在計算完後,將最後的結果存至 rd (i.e. cpu_gpr[a->rd]) 暫存器中。
最後別忘了要釋放之前所宣告的 TCG variable:t0。
P.S. 其實這邊可以更簡單的直接將 cpu_gpr[a->rs1] 傳入,省略 TCG variable:t0 的宣告:
1
2
3
4
5
6
7
8
9
10
| /*
* RISC-V translation routines for the RVB Standard Extension.
*/
static bool trans_pcnt(DisasContext *ctx, arg_pcnt *a) {
if (a->rd != 0) {
gen_helper_pcnt(cpu_gpr[a->rd], cpu_gpr[a->rs1]);
}
return true;
}
|
pcnt 的 helper function 定義如下:
1
2
| /* Bitmanip Extension */
DEF_HELPER_1(pcnt, tl, tl)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| /*
* RISC-V Bitmanip Extension Helpers for QEMU.
*/
#include "qemu/osdep.h"
#include "cpu.h"
#include "exec/exec-all.h"
#include "exec/helper-proto.h"
target_ulong HELPER(pcnt)(target_ulong rs1)
{
target_ulong count = 0;
for (int i = 0; i < TARGET_LONG_BITS; i++) {
count += (rs1 >> i) & 1;;
}
return count;
}
|
基本上就是實作先前在 B Extension spec. 及 Spike 中所看到的 1 bits 個數計算方式。由於 pcnt helper function 只需接收 rs1 暫存器的值,並回傳最後 1 bits 個數的結果,因此,我們定義 pcnt 的 helper function 為接收一 target_ulong 型態的 rs1 並回傳 target_ulong 型態的 1 bits 個數結果。
最後別忘了將我們新增的 bitmanip_helper.o 加入 compile objects 列表:
1
| obj-y += translate.o op_helper.o cpu_helper.o cpu.o csr.o fpu_helper.o bitmanip_helper.o gdbstub.o
|
重新編譯 QEMU,再次執行 pcnt_example:
1
| ./qemu/riscv64-linux-user/qemu-riscv64 pcnt_example
|
1
2
| num = 187
# of set bits = 6
|
這次 QEMU 就可以正確的 decode 並執行 pcnt 指令了。
在 QEMU 中新增指令的流程大致就如同本文所介紹,不過由於 pcnt 指令只是單純的 bit operation 指令,沒有像 csr 相關指令會涉及 CPURISCVState 的更新,以及像 jal 指令會涉及 DisasContext 的判斷,因此實作起來相對簡單。若欲讓 QEMU 支援不論是 B Extension 或是 V Extension 的其他指令,就是得好好 K spec. 並一個一個新增了。
另外最近剛好 C-Sky Microsystems 的 LIU Zhiwei <[email protected]> 在實作 V Extension 的 configure instructions:vsetvl 及 vsetvli,比起本文所介紹之 B Extension 的 pcnt 指令要來得複雜得多,patches 仍在被 reviewed 中,也可以做為參考。
本文所對 QEMU 做的修正,可以參考此 commit。