当前位置: 首页 > news >正文

一生一芯 PA2 RTFSC

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,那么宏定义gprR的参数也是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)
MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)
MUX_WITH_COMMA(concat(__P_DEF_, macro), X, Y)
CHOOSE2nd(__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被定义为01,那么展开后,contain_comma a会变成X,a,使a成为第二个元素。

实际效果为宏定义下的?:三元运算符。

再回头看,那个流程图展开是有问题的。

宏定义不会递推到最后一层再展开,参考concat(__P_DEF_, A)的展开过程,A在第一步就展开了,它的展开结果会影响下一步展开。

对于整条链路的入口:MUXDEF(CONFIG_RVE, 16, 32))

  • 如果定义了CONFIG_RVE10,那么编译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在第一个参数定义为10时,会编译为第二个参数。

那么就变成了:

  • __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的第hilo位(闭区间)

运算分为两个部分:((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; })

写个程序测试一下功能。

({ ... })

  • 这是GCCClang支持的一种语法糖,用于将一个代码块作为一个表达式返回值。不能在标准C中使用。

在语法糖内部,有两条语句:

  1. struct { int64_t n : len; } __x = { .n = x };
    1. struct { int64_t n : len; }定义了一个匿名结构体,变量n只取第n位。
    2. __x = { .n = x }创建了一个结构体变量__x.n被赋值为x,高位会被截断。
  2. (uint64_t)__x.n;
    1. 把阶段的位域强转为uint64_t,并作为表达式结果。

那么SEXT的作用就是:

  • x看作一个len位的有符号整数,对其进行“符号扩展”为64位整数,并以uint64_t类型返回其值。

immI

#define immI() do { *imm = SEXT(BITS(i, 31, 20), 12); } while(0)

  1. 取出32位指令中的位段 [31:20]
  2. 将它作为12位 有符号立即数 符号扩展成64
  3. 然后赋值给*imm

immu

#define immU() do { *imm = SEXT(BITS(i, 31, 12), 20) << 12; } while(0)

  1. 取出32位指令中的位段 [31:12]
  2. 然后左移12位形成最终的32位立即数

imms

#define immS() do { *imm = (SEXT(BITS(i, 31, 25), 7) << 5) | BITS(i, 11, 7); } while(0)

  1. 取出:高7位:i[31:25]和低5位:i[11:7]
  2. 将高7位符号扩展,再左移5
  3. 与低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为下一个位置。

snpcPA2手册中有提到:

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__ ; \
}

发现nametype即使传入空置也不会影响目前的编译。

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型指令,需要immIrs1rd

对于U型指令,需要immUrd

对于S型指令,需要immSrs2rs1rd

  • 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函数入参中传入
  • nameINSTPAT的第二个参数auipc
  • typeINSTPAT的第三个参数U
  • ...INSTPAT的第四个参数R(rd) = s->pc + imm)

name目前来看无关紧要。

展开后会先根据type取出操作数。

然后展开...,操作取出的操作数。

总结

INSTPAT_STARTINSTPAT_END成对出现。

中间处理指令,某条规则匹配成功后,会立即执行并不再继续向下匹配。

INSTPAT的参数是:

  • 匹配规则
  • 指令名字
  • 指令类型
  • 执行语义,传入的应该是一系列函数。

参考

  • https://ysyx.oscc.cc/docs/ics-pa/2.2.html#rtfsc-2

相关文章:

  • 20250620在Ubuntu20.04.6下编译KickPi的K7的Android14系统解决缺少libril.so.toc的问题
  • websocket入门到实战(详解websocket,实战聊天室,消息推送,springboot+vue)
  • C#上位机实现报警语音播报
  • 信任再造:跌倒检测算法如何让善意不再“自证”
  • MySQL之事务深度解析
  • 免费音频视频语音识别转文字软件SenseVoice整合包下载,支持批量操作可生成字幕
  • Linux下nginx访问路径页面
  • XCUITest + Swift 详细示例
  • Apache Doris 3.0.6 版本正式发布
  • 深入解析BERT:语言分类任务的革命性引擎
  • 大数据治理域——计算管理
  • Unity2D 街机风太空射击游戏 学习记录 #12环射道具的引入
  • React Native +Taro创建项目,开发Android
  • Lombok常用注解总结
  • HW蓝队工作流程
  • 为什么你的vue项目连接不到后端
  • 【机器学习实战笔记 12】集成学习:AdaBoost算法
  • Odoo 18 固定资产管理自动化指南
  • 基于深度学习的智能图像超分辨率技术:技术与实践
  • 【Python进阶系列】第10篇:Python 项目的结构设计与目录规范 —— 从脚本到模块,从混乱到整洁
  • 手机与pc的网站开发/乔拓云智能建站
  • 基于web的旅游网站建设/网站宣传费用
  • 长葛做网站/网站优化排名方案
  • 网站建设内容的重点/公司企业网站建设
  • 丹东网站建设/深圳百度地图
  • 好资源源码网站/百度会员登录入口