diff --git a/articles/20230730-riscv-linux-extension-and-alternative.md b/articles/20230730-riscv-linux-extension-and-alternative.md new file mode 100755 index 0000000000000000000000000000000000000000..5b0f9aea81b8d24cce6bcf5888a0a9c7d8c28f66 --- /dev/null +++ b/articles/20230730-riscv-linux-extension-and-alternative.md @@ -0,0 +1,509 @@ +> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces codeinline pangu]
+> Author: Tan Yuan
+> Date: 20230730
+> Revisor: Falcon
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
+> Proposal: [通过编译器解决因链接过程 KEEP 操作引起的 Section GC 失败问题][004]
+> Sponsor: PLCT Lab, ISCAS + +# Linux 的 RISC-V 拓展支持与 alternative 运行时代码段修改 + +## 概述 + +RISC-V 拥有许多拓展指令集,CPU 可以选择性地支持这些拓展指令集。其中,RISC-V ISA Zbb 是一种基本位操作拓展指令集,可应用于字符串相关操作。近期,Linux 内核开始采用 Zbb 中的新指令来优化字符串操作,从而提升系统性能。 + +然而,内核必须同时考虑不支持 Zbb 的设备。使用不同的函数实现可能会引起多个问题,包括: + +- 如果 CPU 不支持新指令该如何处理? +- 如何将旧指令实现的函数替换成新指令实现的函数? +- 启动的最初阶段内核不知道 CPU 支持哪些拓展。 + +为了解决这些问题,内核采用了"alternative"功能来实现函数替换。该功能可以在内核运行时将原有代码内容动态替换为新的代码内容,修改代码段以更改函数的执行路径。 + +"alternative"根据 CPU 的型号来决定是否替代代码。如果 CPU 不支持 Zbb,那么在内核启动时,就不会更改函数的执行路径,避免运行到 Zbb 专属指令的部分。 + +接下来,我们将研究 Linux Kernel v6.5-rc2 如何将原有的 `strcmp` 替换为 Zbb 中的新指令,并详细阐述"alternative"的实现原理。 + +## 准备工作 + +我们需要准备好测试环境,包括支持 Zbb 的工具链、支持 Zbb 的 CPU、并开启内核关于 Zbb 的选项。 + +### 工具链支持和内核选项 + +在内核代码中搜索 Zbb,可以找到如下配置选项: + +``` +// arch/riscv/Kconfig:507 + +config TOOLCHAIN_HAS_ZBB + bool + default y + depends on !64BIT || $(cc-option,-mabi=lp64 -march=rv64ima_zbb) + depends on !32BIT || $(cc-option,-mabi=ilp32 -march=rv32ima_zbb) + depends on LLD_VERSION >= 150000 || LD_VERSION >= 23900 + depends on AS_HAS_OPTION_ARCH + +config RISCV_ISA_ZBB + bool "Zbb extension support for bit manipulation instructions" + depends on TOOLCHAIN_HAS_ZBB + depends on MMU + depends on RISCV_ALTERNATIVE + default y + help + Adds support to dynamically detect the presence of the ZBB + extension (basic bit manipulation) and enable its usage. + + The Zbb extension provides instructions to accelerate a number + of bit-specific operations (count bit population, sign extending, + bitrotation, etc). + + If you don't know what to do here, say Y. +``` + +我们需要开启这两个选项。`RISCV_ISA_ZBB` 依赖于 `TOOLCHAIN_HAS_ZBB`,`TOOLCHAIN_HAS_ZBB` 需要 `LD_VERSION >= 23900`。 + +我们可以使用 Binutils 2.40。如果发行版仓库中 Binutils 低于该版本,可以在 [交叉编译工具链镜像][002] 上下载该版本的 Binutils。 + +在正确的环境变量配置下,我们可以打开 menuconfig 来开启 `RISCV_ISA_ZBB` 选项。可以使用 `/` 来搜索选项,并且使用数字 1、2 来快速跳转到目标选项位置。 + +```bash +make ARCH=riscv CROSS_COMPILE=riscv64-linux- menuconfig +``` + +### QEMU 支持 + +我们需要使用支持 Zbb 的 CPU 来运行内核。 + +QEMU 关于这方面的文档并不完整。通过在其 [代码][005] 中搜索,发现 `v8.1.0-rc1` 版本中的 `veyron-v1` CPU 支持 Zbb。 + +使用 QEMU 运行 Linux Kernel: + +```bash +qemu-system-riscv64 \ + -nographic \ + -machine virt \ + -cpu veyron-v1 \ + -kernel arch/riscv/boot/Image \ + -append "console=ttyS0" +``` + +如果我们想使用不支持 Zbb 的 CPU 来调试,可以使用 `sifive-u54` CPU。 + +```bash +qemu-system-riscv64 \ + -nographic \ + -machine virt \ + -cpu sifive-u54 \ + -kernel arch/riscv/boot/Image \ + -append "console=ttyS0" +``` + +### 调试配置 + +#### 内核开启调试选项 + +需要开启内核中的 `CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT` 选项来为内核增加调试信息。 + +#### VSCode 调试配置 + +我们可以使用 VSCode 来让调试更加便利。 + +只需要在项目根目录下创建两个文件 `.vscode/launch.json` 和 `.vscode/task.json`,即可使用 VSCode 来调试内核。 + +```json +// .vscode/launch.json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Kernel Debug", + "type": "cppdbg", + "request": "launch", + "miDebuggerServerAddress": "localhost:1234", + "program": "${workspaceFolder:}/vmlinux", + "sourceFileMap": { + "kernel.map": "${workspaceFolder:}/System.map" + }, + "MIMode": "gdb", + "externalConsole": false, + "miDebuggerPath": "gdb-multiarch", + "internalConsoleOptions": "openOnSessionStart", + "preLaunchTask": "qemu-debug", + "cwd": "${workspaceFolder:}", + "setupCommands": [ + { + "text": "-enable-pretty-printing", + "description": "Enable GDB pretty printing", + "ignoreFailures": true + }, + { + "text": "set architecture riscv:rv64", + "description": "Set target architecture", + "ignoreFailures": true + } + ], + "miDebuggerArgs": "-q" + } + ] +} +``` + +```json +// .vscode/task.json +{ + "version": "2.0.0", + "tasks": [ + { + "type": "shell", + "label": "qemu-debug", + "command": "echo starting qemu... 1>&2 && qemu-system-riscv64 -nographic -machine virt -cpu veyron-v1 -kernel arch/riscv/boot/Image -append console=ttyS0 -s -S", + "isBackground": true, + "presentation": { + "echo": true, + "reveal": "always", + "focus": true, + "panel": "shared", + "showReuseMessage": false + }, + "options": { + "cwd": "${workspaceFolder:}" + }, + "problemMatcher": { + "pattern": { + "regexp": "." + }, + "background": { + "activeOnStart": true, + "beginsPattern": ".", + "endsPattern": "." + } + } + } + ] +} +``` + +通过更改 `.vscode/task.json` 中的 `command` 项,可以修改 QEMU 的参数。 + +创建好这两个文件后,即可使用 VSCode 调试功能。 + +## alternative 原理 + +### 动态替换代码 + +首先我们来看一下 `arch/riscv/lib/strcmp.S`,可以 [在线][003] 查看代码。 + +`strcmp.S` 的结构为: + +``` +strcmp: +ALTERNATIVE("nop", "j strcmp_zbb", 0, RISCV_ISA_EXT_Zbb, CONFIG_RISCV_ISA_ZBB) +< 通用的 strcmp 汇编代码 > +ret +strcmp_zbb: +< 使用 Zbb 拓展指令的 strcmp 汇编代码 > +ret +``` + +`ALTERNATIVE` 是 [汇编宏][008],在汇编转换为机器码的阶段展开。编译后,这部分代码段在运行时实际上会变为: + +``` +strcmp: +nop +< 通用的 strcmp 汇编代码 > +ret +strcmp_zbb: +< 使用 Zbb 拓展指令的 strcmp 汇编代码 > +ret +``` + +原来的 `ALTERNATIVE` 展开成 `nop` 指令。 + +我们可以推测,在内核启动时,如果 CPU 支持并且内核配置了 ZBB 选项,`ALTERNATIVE` 将把 `nop` 指令修改为跳转指令,跳转到 `strcmp_zbb` 处执行;相反,不支持 ZBB 的 CPU 或者没有配置 ZBB 选项,只会执行通用的 `strcmp` 汇编代码。 + +### 运行时的代码段修改 + +先在 `arch/riscv/kernel/alternative.c` 的 `riscv_alternative_fix_offsets()` 函数打断点。 + +![image-20230729150151420](images/riscv-linux-extension-and-alternative/image-20230729150151420.png) + +这里的 `alt_ptr` 是 `strcmp`,有点像我们要找的地方。我们又在调用栈的上一层 `riscv_cpufeature_patch_func()` 打断点并重新运行,看执行 `riscv_alternative_fix_offsets()` 函数之前是什么样子。 + +调用栈的更上一层,即 `riscv_alternative_fix_offsets()` 的调用者 `_apply_alternatives()` 函数我们暂时不讨论。 + +我们暂停在执行 `riscv_cpufeature_patch_func()` 函数的 `patch_text_nosync()` 语句前: + +![image-20230729165650943](images/riscv-linux-extension-and-alternative/image-20230729165650943.png) + +`oldptr` 指向代码段 `strcmp`,`altptr` 指向代码段 `strcmp+130`,`strcmp+130` 的指令内容为 `j strcmp+34`,即跳转到 Zbb 拓展实现的 `strcmp_zbb` 处。 + +为什么突然出现 `strcmp+130` 这样的地址呢? + +刚刚的 `strcmp.S` 代码中是这样的: + +```C +ALTERNATIVE("nop", "j strcmp_zbb", 0, RISCV_ISA_EXT_ZBB, CONFIG_RISCV_ISA_ZBB) +``` + +我们对 `vmlinux` 进行反汇编。`strcmp+130` 是整个的 `strcmp.S` 的结束地址,再往后是 `strlen`, + +```bash +$ riscv64-linux-objdump -D -j .text vmlinux | grep -A "35" ":" +ffffffff800ab18a : +ffffffff800ab18a: 00b563b3 or t2,a0,a1 +ffffffff800ab18e: 5efd li t4,-1 +ffffffff800ab190: 0073f393 and t2,t2,7 +ffffffff800ab194: 02039e63 bnez t2,ffffffff800ab1d0 +ffffffff800ab198: 00053283 ld t0,0(a0) +ffffffff800ab19c: 0005b303 ld t1,0(a1) +ffffffff800ab1a0: 2872de13 orc.b t3,t0 +ffffffff800ab1a4: 03de1163 bne t3,t4,ffffffff800ab1c6 +ffffffff800ab1a8: 0521 add a0,a0,8 +ffffffff800ab1aa: 05a1 add a1,a1,8 +ffffffff800ab1ac: fe6286e3 beq t0,t1,ffffffff800ab198 +ffffffff800ab1b0: 6b82d293 rev8 t0,t0 +ffffffff800ab1b4: 6b835313 rev8 t1,t1 +ffffffff800ab1b8: 0062b533 sltu a0,t0,t1 +ffffffff800ab1bc: 40a00533 neg a0,a0 +ffffffff800ab1c0: 00156513 or a0,a0,1 +ffffffff800ab1c4: 8082 ret +ffffffff800ab1c6: 00629563 bne t0,t1,ffffffff800ab1d0 +ffffffff800ab1ca: 4501 li a0,0 +ffffffff800ab1cc: 8082 ret +ffffffff800ab1ce: 0001 nop +ffffffff800ab1d0: 00054283 lbu t0,0(a0) +ffffffff800ab1d4: 0005c303 lbu t1,0(a1) +ffffffff800ab1d8: 0505 add a0,a0,1 +ffffffff800ab1da: 0585 add a1,a1,1 +ffffffff800ab1dc: 00629463 bne t0,t1,ffffffff800ab1e4 +ffffffff800ab1e0: fe0298e3 bnez t0,ffffffff800ab1d0 +ffffffff800ab1e4: 40628533 sub a0,t0,t1 +ffffffff800ab1e8: 8082 ret +ffffffff800ab1ea: fa1ff06f j ffffffff800ab18a + ... + +ffffffff800ab1f8 <__pi_strlen>: +ffffffff800ab1f8: 00000013 nop +ffffffff800ab1fc: 832a mv t1,a0 +``` + +可以发现新指令 `j strcmp_zbb` 实际上是存放在 `strcmp.S` 的结束处的。说明 `ALTERNATIVE` 搞了一些魔法,定位到了该节的末尾,插入了新代码 `j strcmp_zbb`。我们将在文章的下个小节讨论这个魔法。 + +继续分析后续代码。在执行了 `patch_text_nosync()` 后,刚刚查看的 `strcmp` 内存处的 `nop` 指令已经变成了 `j 0xffffffff800ab108`,如下图: + +![image-20230729171229044](images/riscv-linux-extension-and-alternative/image-20230729171229044.png) + +这个跳转地址很奇怪,看起来没有任何作用。我们将在文章的下个小节讨论它。 + +执行了 `riscv_alternative_fix_offsets()` 后,原来的 `nop` 变成了 `j strcmp+34`。 + +![image-20230729173407736](images/riscv-linux-extension-and-alternative/image-20230729173407736.png) + +查看 `strcmp+34` 处的指令,可以发现该处即为使用 Zbb 拓展指令实现的高性能 `strcmp_zbb`。 + +![image-20230729152123586](images/riscv-linux-extension-and-alternative/image-20230729152123586.png) + +在此之后,内核代码段的内容已经被修改。原本调用 `strcmp()` 执行的第一条指令为 `nop` 指令,现在 `nop` 指令被修改为无条件跳转指令。调用 `strcmp()` 会立即跳转到 34 字节外的新指令 `strcmp_zbb` 上。 + +原有的: + +``` +strcmp: +nop +< 通用的 strcmp 汇编代码 > +ret +strcmp_zbb: +< 使用 Zbb 拓展指令的 strcmp_zbb 汇编代码 > +ret +``` + +修改后: + +``` +strcmp: +j strcmp_zbb +< 通用的 strcmp 汇编代码 > +ret +strcmp_zbb: +< 使用 Zbb 拓展指令的 strcmp_zbb 汇编代码 > +ret +``` + +代码段 `strcmp` label 处的指令的变化过程: + +| 语句 | 代码段 `strcmp` label 处的指令 | +|----------------------------------------|-----------------------------------------------| +| `patch_text_nosync()` 执行前 | nop | +| `patch_text_nosync()` 执行后 | j 0xffffffff800ab108 <__memset+156> | +| `riscv_alternative_fix_offsets()` 执行后 | j 0xffffffff800ab18a | + +经过两次代码段修改,`nop` 指令被修改为跳转到 `strcmp_zbb` 的指令 + +### ELF 的 .alternative 节与魔法 + +```bash +$ riscv64-linux-readelf -S vmlinux.o | grep -A 1 alternative + [21332] .alternative PROGBITS 0000000000000000 0017df18 + 0000000000000030 0000000000000000 A 0 0 1 +``` + +`ALTERNATIVE` 汇编宏展开时,会调用 `ALT_NEW_CONTENT` 汇编宏。 + +```C +// arch/riscv/include/asm/alternative-macros.h:61 + +#define ALT_ENTRY(oldptr, newptr, vendor_id, patch_id, newlen) \ + ".4byte ((" oldptr ") - .) \n" \ + ".4byte ((" newptr ") - .) \n" \ + ".2byte " vendor_id "\n" \ + ".2byte " newlen "\n" \ + ".4byte " patch_id "\n" + +#define ALT_NEW_CONTENT(vendor_id, patch_id, enable, new_c) \ + ".if " __stringify(enable) " == 1\n" \ + ".pushsection .alternative, \"a\"\n" \ + ALT_ENTRY("886b", "888f", __stringify(vendor_id), __stringify(patch_id), "889f - 888f") \ + ".popsection\n" \ + ".subsection 1\n" \ + "888 :\n" \ + ".option push\n" \ + ".option norvc\n" \ + ".option norelax\n" \ + new_c "\n" \ + ".option pop\n" \ + "889 :\n" \ + ".org . - (887b - 886b) + (889b - 888b)\n" \ + ".org . - (889b - 888b) + (887b - 886b)\n" \ + ".previous\n" \ + ".endif\n" +``` + +其中,`".4byte ((" oldptr ") -.) \n"` 将 `oldptr` label 到当前位置的距离存储到了这个 4byte 大小空间中。有了偏移量,在应用 alternative 时(即刚刚打断点的 `riscv_cpufeature_patch_func()` 函数的调用者 `_apply_alternatives()`),就能计算出新代码和旧代码的位置: +![image-20230729191712546](images/riscv-linux-extension-and-alternative/image-20230729191712546.png) + +保存 offset 这个行为是由 [该 patch][007] 提供的,在这之前是直接保存的绝对地址,占用了更多的空间。 + +该汇编宏使用 `.pushsection` 在 ELF 文件中建立了 `.alternative` 节,将偏移量、`vendor_id` 和 `patch_id` 一起放在该节中。新代码并没有存储在这里,应该是为了缓存命中率的考虑,把新代码放在了所属代码节末尾。 + +存在于代码最前端的 `ALTERNATIVE` 展开后,能够把新代码放到代码段末尾,是通过 `.subsection` 和 `.previous` 实现的。 + +因此,后续 `_apply_alternatives()` 可以遍历该节每个 entry 来替换旧代码。 + +### 替换了两次旧代码? + +在刚刚的分析中,我们 `strcmp` 处的代码被替换了两次,第一次替换的新代码跳转到 `__memset+156` 处,在 `riscv_alternative_fix_offsets()` 执行后,新代码才跳转到 `strcmp_zbb` 处。 + +首先我们来看看 `patch_text_nosync()` 的定义: + +```C +// arch/riscv/kernel/patch.c:99 + +int patch_text_nosync(void *addr, const void *insns, size_t len) +{ + u32 *tp = addr; + int ret; + + ret = patch_insn_write(tp, insns, len); + + if (!ret) + flush_icache_range((uintptr_t) tp, (uintptr_t) tp + len); + + return ret; +} +``` + +这个函数在 `addr` 上写入长度为 `len` 的 `insns`。更改代码段需要刷新 `icache` 来确保缓存一致性。 + +```C +// arch/riscv/kernel/alternative.c:103 + +void riscv_alternative_fix_offsets(void *alt_ptr, unsigned int len, + int patch_offset) +{ + int num_insn = len / sizeof(u32); + int i; + + for (i = 0; i < num_insn; i++) { + u32 insn = riscv_instruction_at(alt_ptr + i * sizeof(u32)); + + /* + * May be the start of an auipc + jalr pair + * Needs to check that at least one more instruction + * is in the list. + */ + if (riscv_insn_is_auipc(insn) && i < num_insn - 1) { + u32 insn2 = riscv_instruction_at(alt_ptr + (i + 1) * sizeof(u32)); + + if (!riscv_insn_is_jalr(insn2)) + continue; + + /* if instruction pair is a call, it will use the ra register */ + if (RV_EXTRACT_RD_REG(insn) != 1) + continue; + + riscv_alternative_fix_auipc_jalr(alt_ptr + i * sizeof(u32), + insn, insn2, patch_offset); + i++; + } + + if (riscv_insn_is_jal(insn)) { + s32 imm = riscv_insn_extract_jtype_imm(insn); + + /* Don't modify jumps inside the alternative block */ + if ((alt_ptr + i * sizeof(u32) + imm) >= alt_ptr && + (alt_ptr + i * sizeof(u32) + imm) < (alt_ptr + len)) + continue; + + riscv_alternative_fix_jal(alt_ptr + i * sizeof(u32), + insn, patch_offset); + } + } +} +``` + +`riscv_alternative_fix_offset()` 函数根据偏移量计算出真正需要跳转的距离,按情况指派给 `riscv_alternative_fix_auipc_jalr()`、`riscv_alternative_fix_jal()`、`riscv_alternative_fix_offsets()` 函数,这些函数生成对应的指令后,再次使用 `patch_text_nosync()` 来修改目标地址代码。 + +经过 `git blame` 和搜索邮件列表,我们发现有一 [patch][006] 做了以下改动: + +```diff + patch_text_nosync(alt->old_ptr, alt->alt_ptr, alt->alt_len); ++ riscv_alternative_fix_offsets(alt->old_ptr, alt->alt_len, ++ alt->old_ptr - alt->alt_ptr); +``` + +邮件中写到: + +``` +Alternatives live in a different section, so addresses used by call +functions will point to wrong locations after the patch got applied. + +Similar to arm64, adjust the location to consider that offset. +``` + +原来是因为单独的一行 `patch_text_nosync()` 会有 bug,offset 计算不正确,所以才使用一个单独的函数来修补错误。 + +## 总结 + +内核的启动过程中,会检测 CPU 的型号,遍历 `.alternative` 节中的 entry,符合条件的 patch 会被 apply。apply 会修改目标地址的旧代码为新代码,同时确保 icache 一致性。 + +针对 RISC-V 架构设备而言,其模块化的架构设计意味着不同的扩展可以拥有各自的优化指令。CPU 的指令集只能在运行时才能得知,在此之前只能使用兼容性最高的指令。“alternative” 在这种情况下是一种非常好的解决方案,既保证了兼容性,也提高了性能。 + +## 参考资料 + +- [RISC-V Bit-manipulation A, B, C and S Extensions | Five EmbedDev (five-embeddev.com)][001] +- [Latest corss-compilers][002] +- [Linux kernel source tree][003] +- [通过编译器解决因链接过程 KEEP 操作引起的 Section GC 失败问题][004] +- [QEMU source tree][005] +- [Macro (Using as)][008] +- [[PATCH v5 09/13] riscv: switch to relative alternative entries][007] +- [[PATCH v5 12/12] RISC-V: fix auipc-jalr addresses in patched alternatives][006] + +[001]: https://five-embeddev.com/riscv-bitmanip/draft/bitmanip.html +[002]: https://ftp.sjtu.edu.cn/sites/ftp.kernel.org/pub/tools/crosstool/files/bin/x86_64/13.1.0/x86_64-gcc-13.1.0-nolibc-riscv64-linux.tar.xz +[003]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/riscv/lib/strcmp.S?h=v6.5-rc3 +[004]: https://gitee.com/tinylab/riscv-linux/issues/I79PO6 +[005]: https://github.com/qemu/qemu/blob/6cb2011fedf8c4e7b66b4a3abd6b42c1bae99ce6/target/riscv/cpu.c#L482 +[006]: https://lore.kernel.org/all/20221223221332.4127602-13-heiko@sntech.de/ +[007]: https://lore.kernel.org/all/20230128172856.3814-10-jszhang@kernel.org/ +[008]: https://sourceware.org/binutils/docs/as/Macro.html diff --git a/articles/images/riscv-linux-extension-and-alternative/image-20230729150151420.png b/articles/images/riscv-linux-extension-and-alternative/image-20230729150151420.png new file mode 100755 index 0000000000000000000000000000000000000000..7ebc30cd92aa6c5b79b6f3b9436c0b205e207894 Binary files /dev/null and b/articles/images/riscv-linux-extension-and-alternative/image-20230729150151420.png differ diff --git a/articles/images/riscv-linux-extension-and-alternative/image-20230729152123586.png b/articles/images/riscv-linux-extension-and-alternative/image-20230729152123586.png new file mode 100755 index 0000000000000000000000000000000000000000..7e9ad3add82893be307ebaca067899866d1c0bc0 Binary files /dev/null and b/articles/images/riscv-linux-extension-and-alternative/image-20230729152123586.png differ diff --git a/articles/images/riscv-linux-extension-and-alternative/image-20230729165650943.png b/articles/images/riscv-linux-extension-and-alternative/image-20230729165650943.png new file mode 100755 index 0000000000000000000000000000000000000000..84648a4c9e9d56a00b1c40b644d48af638947375 Binary files /dev/null and b/articles/images/riscv-linux-extension-and-alternative/image-20230729165650943.png differ diff --git a/articles/images/riscv-linux-extension-and-alternative/image-20230729171229044.png b/articles/images/riscv-linux-extension-and-alternative/image-20230729171229044.png new file mode 100755 index 0000000000000000000000000000000000000000..0ebe8e7c8591daaa03bc53cb927456f91769aaaa Binary files /dev/null and b/articles/images/riscv-linux-extension-and-alternative/image-20230729171229044.png differ diff --git a/articles/images/riscv-linux-extension-and-alternative/image-20230729173407736.png b/articles/images/riscv-linux-extension-and-alternative/image-20230729173407736.png new file mode 100755 index 0000000000000000000000000000000000000000..6c2db76a8b420c08e115768446481a290cb965f3 Binary files /dev/null and b/articles/images/riscv-linux-extension-and-alternative/image-20230729173407736.png differ diff --git a/articles/images/riscv-linux-extension-and-alternative/image-20230729191712546.png b/articles/images/riscv-linux-extension-and-alternative/image-20230729191712546.png new file mode 100644 index 0000000000000000000000000000000000000000..76f2b5bca131e933317a1bd7a6a22292df5344a5 Binary files /dev/null and b/articles/images/riscv-linux-extension-and-alternative/image-20230729191712546.png differ