NJU ICS2024 PA 作业心得(二)

RTFSC问题处理

这部分一定要很仔细很仔细的阅读RISCV的手册,否则后边会吃苦头的。

我们这里从框架的角度对取指之前和执行之后的操作进行一些简要的补充介绍,方便读者结合帮助文档了解NEMU的整体流程。

  • 取指之前

    1. risv32中每条指令的长度为4个字节,这些指令以二进制文件读写的形式被堆排到了bin文件,nemu中定义了位置参数获得需要的bin文件路径。

    2. 在交互控制台使用si或者c执行$NEMU_HOME/src/cpu/cpu-exec.c文件下的cpu_exec函数,该函数调用同文件下的execute函数执行若干次指令后。

    3. execute函数中创建了一个结构体Decodepc指代要被译码的指令地址,snpc指代pc之后的下一个地址,dnpc只带指令执行后,pc该前往的地址,inst只带了指令的二进制代码,logbuf则是用来存指令的反汇编结果。

      然后通过调用若干次exec_once函数执行顺序若干次指令。

    4. exec_once函数维护了结构体Decode的内容,调用了isa_exec_once以此进行取指,译码、执行

  • 执行(更新pc后)之后

    1. exec_once函数中记录指令信息和反汇编结果到Decode.logbuf中。
    2. execute函数中调用trace_and_difftest函数检查NEMU中我们的译码行为,并打印追踪日志(所有的XTRACE应该都在这里记录结果)。每次调用exec_once函数会检查NEMU的状态(出现异常则停止执行指令)
    3. cpu_exec函数中打印执行结果。

riscv32由于指令字长限制应该如何载入32位常数

两者都使用分段加载的方式,先加载高位,再使用立即数指令将低位部分合并到寄存器中,以此完成32位常数的加载操作。这种方法有效利用了32位指令的有限空间,避免了直接在一条指令中编码整个32位常数的限制,下面我们给出RISC-V32的解决方案。

  1. lui(Load Upper Immediate)指令

    • 使用lui指令将一个32位常数的高20位加载到寄存器的高位部分。例如,lui t0, 0x12345会将0x12345000加载到寄存器t0中。
  2. addi(Add Immediate)指令
    • 使用addi指令加载低12位的立即数,并将其与lui加载的高位相加。例如,addi t0, t0, 0x678会将寄存器t0中的值更新为0x12345678

riscv32指令如何实现

本部分是这一小节的重点,有许多容易踩的坑(需要你仔细研读官方文档),

  • 一定要注意运算数是有符号还是无符号!
  • 一定要按照文档描述按照制定的运算数类型(64位or32位)进行运算!
  • 一定要按照文档描述对异常情况进行实现!

在这一部分为了能通过所有指令测试集(如果指令测试集通过,证明某条指令实现应该没有问题了,之后如果遇到BUG,请对自己实现的指令有充足的信心,不用溯源到此处),我们需要总共实现6中类型的指令R、I、S、B、U、J,需要在decode_operand添加TYPE_*以及他们立即数获取方式(参照下图)。

为了获取立即数,NEMU框架给我们提供了一些很棒的宏定义:

  • BITS(x, hi, lo)获取二进制数\(x\)第\([lo,hi]\)的比特内容
  • SEXT(x, len)有符号拓展\(x\)到二进制下\(len\)位。这里的实现用到了位域,巧妙到通过限制有符号int的长度接受到\(x\)的符号位信息,从而再转化成无符号类型实现有符号位拓展。

接着的实现就是根据报错信息找到缺失的指令,根据opcode,funct*锁定具体指令实现即可。

程序, 运行时环境与AM问题处理

如何通过批处理模式运行NEMU

因为AM文件夹中Makefile肯定通过了某种方式编译了nemu,并传入了参数,我们需要找到这一步,然后想办法将批处理参数-b传入。

通过阅读$AM_HOME/Makefile line 97的内容-include $(AM_HOME)/scripts/$(ARCH).mk可以发现这里引入了$(AM_HOME)/scripts/$(ARCH).mk,由于我们的ARCH=riscv32-nemu,于是再查看$(AM_HOME)/scripts/riscv32-nemu.mk发现他引用了$AM_HOME/scripts/platform/nemu.mk

在这个文件的第31行$(MAKE) -C $(NEMU_HOME) ISA=$(ISA) run ARGS="$(NEMUFLAGS)" IMG=$(IMAGE).bin发现它传给nemu的参数是NEMUFLAGS这个变量。细心的你会发现,在我们的测试文件的Makefile中,我们会把命令行中的ARGS参数传给MakefileARGS变量,我们需要将这个变量传给Makefile中的NEMUFLAGS才对。

但很快你会发现如果改了这一步,会导致NEMU无法输出nemu.log了,于是我们尝试去寻找-l xxx/nemu.log参数定义的位置,发现他在$AM_HOME/scripts/platform/nemu.mk中,但是这个变量在声明的时候没有添加override关键字。这就会导致+=运算无法对命令行传来的变量生效,因为命令行同名变量会覆盖Makefile文件中定义的同名变量,需要加override专门在针对命令行同名变量进行运算。

如何实现sprintf

  • sprintf:将可变参数列表获取后提供给vsprintf,将结果输出到一个字符串即可。

  • printf:将可变参数列表获取后提供给vsprintf,将结果输出到一个字符串,然后再利用putch住个字符输出即可

所以如果要实现sprintf最关键的步骤是实现vsprintf

vsprintf 是一个格式化输出函数,用于将格式化的数据输出到字符串中。该函数的设计思想基于处理格式化字符串中的不同占位符,并根据可变参数的类型将其转换为相应的字符串形式,最后将结果拼接到输出缓冲区 out 中。以下是该函数的设计思路:

1. 输入参数

  • char *out:输出缓冲区指针,用于存储格式化后的字符串。
  • const char *fmt:格式化字符串,包含普通字符和格式化占位符(例如 %d%s 等)。
  • va_list ap:可变参数列表,包含需要格式化输出的数据。

2. 遍历格式化字符串

使用 while (*fmt != '\0') 遍历格式化字符串 fmt,逐个字符处理。处理分为两种情况:

  1. 如果当前字符不是 %,将其作为普通字符直接拷贝到输出缓冲区中,并更新缓冲区指针和总输出长度。
  2. 如果当前字符是 %,则需要进入格式化处理逻辑。

3. 格式化占位符解析

当遇到 % 时,函数进入解析模式,依次解析各类格式化修饰符:

  1. 标志位解析

    • 通过 switch 语句处理常见的标志位,如 -(左对齐)、+(显示正负号)、0(填充零)、#(特殊格式)等。每个标志位被映射为一个二进制标志位,并存储在 flags 变量中。
  2. 宽度解析
    • 如果格式说明符中指定了宽度(例如 %10d 表示输出宽度至少为 10),该宽度要么是直接给出的数字,要么是通过 *va_list 参数中动态获取。
  3. 精度解析
    • 如果存在精度修饰符(例如 %.2f 表示小数点后保留 2 位),解析精度值。同样,精度值可以是直接指定的数字或通过 *va_list 参数中获取。
  4. 类型说明符解析
    • 格式说明符的最后一个字符为类型说明符(如 d, s, x 等),用于确定当前参数的数据类型。根据类型说明符的不同,调用不同的处理函数来进行参数格式化。

4. 不同类型的格式化处理

  • 整数类型 (d, i, u, x, o):调用 int_to_str 函数,将整数值按照不同的进制转换为字符串(支持十进制、八进制、十六进制等),并根据指定的宽度和精度进行格式化输出。

  • 字符类型 (c):直接将 char 类型的参数输出到缓冲区。

  • 字符串类型 (s):将字符串按照指定的宽度和精度进行处理,支持左对齐或右对齐。

  • 指针类型 (p):将指针地址转换为十六进制格式输出,并加上 0x 前缀。

  • 特殊类型

    • %:直接输出 % 符号。
    • n:将当前输出字符的长度存储到相应的指针位置。

5. 输出结果拼接

在处理每个占位符后,格式化后的结果通过指针操作被拼接到输出缓冲区 out 中,并更新当前缓冲区指针 buf 以及总长度 total_len

6. 返回结果

当格式化字符串处理完毕后,在输出缓冲区末尾添加字符串终止符 \0,并返回输出缓冲区的长度。

如何实现string库

使用man 3 <function_name>仔细根据手册实现即可,否则可能存在问题,这里也有一点容易踩的坑,

  • strnpy如果\(n\)超出了\(det\)的长度,就不用添加字符串终止符 \0
  • strcmp要注意字符串中的字符一般为无符号char,在这里如果要对读入的字符进行无符号转换,否则对于在ASCII码在128及以后的字符的大小比较会出现问题。

如何编写klib库的测试程序

尽量用多层for循环嵌套,枚举给定两字符串的起始位置和结束为止,以及操作长度\(n\)(如果有的话),这样一般是可以覆盖所有情况的(包括前面坑点提到的边界情况)。

itrace的环形缓冲区如何实现

我的实现是新建了$NEMU_HOME/src/cpu/iringbuf.c文件,在里面维护了一个环形队列,对外暴露的接口是一个函数void record_inst(uint64_t pc, const char *asm_code)传入了当前记录指令的pc指针和反汇编字符串,以及void info_inst_records()函数打印当前队列的信息。

$NEMU_HOME/src/cpu/cpu-exec.ctrace_and_difftest函数中,将原本打印指令的位置更改成将指令记录到缓冲区。在同文件下的assert_fail_msg函数中,在NEMU框架ASSERT中断时输出调用info_inst_records

mtrace如何实现

$NEMU_HOME/src/memory/paddr.cpaddr_readpaddr_write函数成功调用后记录信息即可。

ftrace如何实现

首先我们需要了解elf文件的结构,这里就需要我们首先使用riscv64-linux-gnu-readelf -a获取elf文件的信息,再通过hd命令验证我们获取的elf文件信息,我们就可以很清晰的了解elf文件的架构了,下面我们进行一个简单的介绍。

ELF文件结构的整体组成

ELF文件由三个主要部分组成:

  1. ELF Header(ELF头):描述整个文件的组织结构,包括文件类型、目标架构、入口点等基本信息。

    ELF头位于文件的最前面,包含整个ELF文件的元数据信息。其结构固定,大小通常为64字节(32位格式为52字节),主要包含以下字段:

    • 魔数(Magic Number):标识这是一个ELF文件,前4个字节为固定值 0x7FELF

    • Class:文件的架构类型,32位(ELFCLASS32)或64位(ELFCLASS64)。

    • Data:数据编码,表示字节序的排列方式(小端LSB或大端MSB)。

    • Version:文件格式的版本,通常为1

    • OS ABI:目标操作系统接口的ABI(应用程序二进制接口)。

    • 文件类型:指定这是可执行文件(ET_EXEC)、可重定位文件(ET_REL)或共享库(ET_DYN)。

    • 目标架构:如x86(EM_386)或ARM(EM_ARM)等。

    • 入口点地址:程序开始执行的虚拟地址。

    • Program Header表偏移量:指向程序头表的偏移量。

    • Section Header表偏移量:指向节头表的偏移量。

    • 标志:与处理器相关的标志。

    • 头部的大小:ELF头的大小。

    • Program Header表的条目大小及数量。

    • Section Header表的条目大小及数量。

    • Section header string表在节头表中的Index

  2. Program Header Table(程序头表):描述如何将文件映射到内存中的不同段,用于执行时加载可执行文件。

    程序头表描述了如何将文件中的数据段映射到内存,以便程序执行。每个程序头条目描述了一个内存段的属性。程序头表在执行时用于决定哪些部分加载到内存中。

  3. Section Header Table(节头表):描述文件中的各个节(Section)的属性,用于链接和重定位。

    节头表的每个条目(节头)用于描述文件中的各个节(section),这些节在链接和重定位阶段起作用。每个节都有自己特定的作用,如存储代码、数据、符号表、重定位信息等。每个节头条目通常包含以下信息:

    • Section Name(节名称):通过节名称表(Section header string)来索引。

    • Section Type(节类型):描述该节的内容,如可重定位数据(SHT_RELA)、符号表(SHT_SYMTAB)、字符串表(SHT_STRTAB)等。

    • Section Flags(节标志):描述节的属性,如可写、可执行、分配到内存等。

    • Section Address(节地址):如果该节被加载到内存中,它的虚拟地址。

    • Section Offset(文件偏移):节在文件中的偏移量。

    • Section Size(节大小):节在文件中的大小。

    • Link:与其他节的关联,如符号表与字符串表的关联。

    • Info:附加信息,不同类型的节该字段含义不同。

    • Address Alignment(对齐):节在文件中或内存中的对齐要求。

    • Entry Size(条目大小):如果节中包含固定大小的条目(如符号表),该字段指定条目大小。

    典型的ELF文件会包含以下节:

    • .text:代码段。

    • .data:已初始化的数据。

    • .bss:未初始化的数据。

    • .symtab:符号表,包含所有符号及其地址信息。

    • .strtab:字符串表,存储符号名称和节名称。

    • .rel.text 或 .rela.text:重定位表,针对代码段的重定位信息。

我们首先要明确以下信息:

  • 我们通过符号表获取函数的地址以及函数的名称在字符串表中的偏移量,通过字符串表译码偏移量获取函数名称。
  • 节名称表,符号表和字符串表,虽然中文叫表,但实际上他们是一个节(section)。
  • 节名称表,符号表和字符串表的节头信息存储在节头表中。节头表是一个数组,这个数组在内存中连续,具体位置(偏移量信息)和大小由ELF头给出。
  • ELF头信息位于文件最开头,偏移量为0。
  • 我们需要的信息已经在上述的结构组成中黑体加粗了。

获取函数名与函数起始地址的映射的流程

  1. 打开并读取ELF文件

    通过 fopen 打开传入的ELF文件。如果文件无法打开,输出错误信息并终止程序。

    使用 fread 读取ELF头部(Elf32_Ehdr),以获取文件的基本信息,如节头表的偏移和节的数量等。

  2. 验证文件是否是ELF格式

    检查ELF头部的魔数字段(e_ident),确保它是一个合法的ELF文件。如果魔数不匹配,输出错误信息并终止程序。

  3. 读取节头表(Section Headers)

    根据ELF头部中的节头表偏移量,节的数量和每一个节头表的大小,分配足够的内存来存储节头表(Elf32_Shdr),并使用 fseekfread 从文件中读取节头表。

  4. 读取节名称表

    节名称表是一个特殊的section,用于存储所有节的名称。在节头表中,通过索引找到节名称表(shstrtab),并读取其内容。

  5. 查找符号表和字符串表

    遍历所有节头条目,寻找 .symtab(符号表)和 .strtab(字符串表)的节头,分别用于存储符号和符号名称。(如果愿意的话,可以根据节头条目提供的节类型信息二次确认我们找到了正确的表)。

​ 如果没有找到这两个节,输出错误信息并终止程序。

  1. 读取符号表和字符串表

    根据 .symtab 节头中的偏移和大小,读取符号表。

    根据 .strtab 节头中的偏移和大小,读取符号名称字符串表。

  2. 将函数符号添加到符号表

    遍历符号表中的每个符号(Elf32_Sym),对于每个符号,检查它的类型是否是 STT_FUNC(即函数类型)。如果是函数符号,调用 add_symbol 将符号的名称、地址和大小添加到内存中的符号表中。

实现FTRACE

  1. 定义const char *find_symbol(uint64_t addr)函数从保存的映射表中找到内存地址对应的函数
  2. 涉及函数跳转的命令,jal是直接跳转到目标地址进行函数调用,他的跳转地址是一个立即数,所以是一个编译时已经知道的具体偏移量,因此是直接调用。jalr的目标地址是由寄存器提供的,这是一个运行时才能确定的偏移量,因此是间接调用。同时由于函数返回时,我们并不能在编译阶段就知道每个函数返回到具体哪个地址,因此我们需要在每次调用前将当前pc存入返回地址寄存器ra中,利用jalr ra, 0(ra)实现返回。
  3. 所以我们知道jal是直接调用,jalr在传入的立即数为0,且调用了返回地址寄存器ra时是在返回函数,其他情况下在间接调用,

如何实现DiffTest

检测ref_r中的寄存器信息是否和全局变量cpu中的一样即可,被忘了pc寄存器

输入输出问题处理

AM和NEMU的关系是,NEMU通过调用C函数库,模拟了一系列外设的接口,所以我们需要去阅读$NEMU_HOME/src/device下的文件,看NEMU是如何进行MMIO的,从而我们可以在AM中去读写正确的信息给NEMU处理。

volatile有什么用

比如,假设 p 指向一个硬件设备的寄存器。如果没有使用 volatile 修饰,编译器可能会认为这个寄存器的值在程序运行期间不会被外部改变,因此它会把读取 p 的操作优化为只读取一次,将值缓存在寄存器中,而不再频繁地从该地址重新读取。这对于普通变量来说是合理的优化,但对于设备寄存器,这种优化可能会导致问题。原因是设备寄存器的值可能随时发生变化(比如通过外部硬件设备更新状态),编译器缓存的值不再是最新的,导致程序读取到的设备状态并不正确,从而产生错误行为。

如何实现时钟

阅读NEMU中timer.c源码,可以看到NEMU的模拟实现是通过调用<time.h>获得当前的时间,存储到了rtc_port_base的16个字节中。同时通过其中的rtc_io_handler可以看到如果我们读取了高位字节,NEMU就会获得最新的时间。所以我们在实现的时候一定要先读高位字节,才能刷新设备缓存,获取最新时间。

如何实现malloc

注意好申请的地址要对齐,这里由于我的电脑是64位我就设置了8字节的对齐,即每次申请的空间大小必须向8的整数倍对齐。

如何实现DTRACE

通过阅读文档我们知道,NEMU通过map_readmap_write进行MMIO的操作,所以只要在这两个函数里记录log即可。

如何实现键盘

阅读NEMU中keyboard.c文件中的send_key函数可以看到键盘每一次按下的信息是将keymap中的键位信息与掩码通过按位或的方式记录。我们在AM中解析出这两段信息,写入kbd即可。

如何实现VGA

阅读NEMU中vga.c可以看到显示的长宽在vgactl_port_base中的存储顺序。在AM中我们需要提取出这些信息并记录在config中,因为通过阅读am-test中的测试程序,可以发现程序需要获取这些信息。

__am_gpu_fbdraw中需要将提供的ctl中的打印信息正确写入VGA的寄存器,这里就是简单的二维数组的映射,我们需要在x, y作为矩形的左上角端点,写入一个w*h的矩形图像信息。如果此时要求控制信息要求同步,我们也要将这个信息及时写入VGA的寄存器中。

如何实现声卡

NEMU中由于我们使用SDL库来模拟声卡,在每次我们调用声卡信息的时候,我们也需要使用刚才的信息初始化一个新的SDL(因为配置信息可能改变),并且设置好他的回调函数。SDL回调函数定义了我们如何处理寄存器中提供的音频信息,由于我们的缓存区有限,我们使用循环队列的方式向缓冲区中写入数据。因此我们需要正确的从缓存区中读出数据并写入stream中。

在AM的__am_audio_play中,我们需要正确维护好循环队列缓冲区,向其中写入数据。

源码

/* inst.c 粘一些代表性的,不全粘贴 */
INSTPAT_START();
INSTPAT("0000000 ????? ????? 000 ????? 01100 11", add , R, R(rd) = (int32_t)((int32_t)src1 + (int32_t)src2));
INSTPAT("0000001 ????? ????? 000 ????? 01100 11", mul , R, R(rd) = (int32_t)((int64_t)(int32_t)src1 * (int64_t)(int32_t)src2));
INSTPAT("0000001 ????? ????? 001 ????? 01100 11", mulh , R, R(rd) = (int32_t)(((int64_t)(int32_t)src1 * (int64_t)(int32_t)src2) >> 32));
INSTPAT("0000001 ????? ????? 010 ????? 01100 11", mulhsu , R, R(rd) = ((int64_t)(int32_t)src1 * (uint64_t)(uint32_t)src2) >> 32);
INSTPAT("0000001 ????? ????? 011 ????? 01100 11", mulhu , R, R(rd) = (uint32_t)(((uint64_t)(uint32_t)src1 * (uint64_t)(uint32_t)src2) >> 32));
INSTPAT("0100000 ????? ????? 000 ????? 01100 11", sub , R, R(rd) = src1 - src2);
INSTPAT("0000000 ????? ????? 010 ????? 01100 11", slt , R, R(rd) = (int32_t)src1 < (int32_t)src2);
INSTPAT("0000000 ????? ????? 011 ????? 01100 11", sltu , R, R(rd) = (uint32_t)src1 < (uint32_t)src2);
INSTPAT("0000000 ????? ????? 100 ????? 01100 11", xor , R, R(rd) = src1 ^ src2);
INSTPAT("0000001 ????? ????? 100 ????? 01100 11", div , R, R(rd) = src2 ? (src1 == (1 << 31) && src2 == -1) ? (1 << 31) : (int32_t)((int32_t)src1 / (int32_t)src2) : -1);
INSTPAT("0000001 ????? ????? 101 ????? 01100 11", divu , R, R(rd) = src2 ? (uint32_t)((uint32_t)src1 / (uint32_t)src2) : (uint32_t)((1ll << 32) - 1));
INSTPAT("0000000 ????? ????? 001 ????? 01100 11", sll , R, R(rd) = (uint32_t)src1 << BITS(src2, 4, 0));
INSTPAT("0000000 ????? ????? 101 ????? 01100 11", srl , R, R(rd) = (uint32_t)src1 >> BITS(src2, 4, 0));
INSTPAT("0100000 ????? ????? 101 ????? 01100 11", sra , R, R(rd) = (int32_t)src1 >> BITS(src2, 4, 0));
INSTPAT("0000000 ????? ????? 110 ????? 01100 11", or , R, R(rd) = src1 | src2);
INSTPAT("0000001 ????? ????? 110 ????? 01100 11", rem , R, R(rd) = src2 ? (src1 == (1 << 31) && src2 == -1) ? 0 : (int32_t)src1 % (int32_t)src2 : src1);
INSTPAT("0000001 ????? ????? 111 ????? 01100 11", remu , R, R(rd) = src2 ? (uint32_t)src1 % (uint32_t)src2 : src1);
INSTPAT("0000000 ????? ????? 111 ????? 01100 11", and , R, R(rd) = src1 & src2); INSTPAT("??????? ????? ????? 000 ????? 00000 11", lb , I, R(rd) = SEXT(Mr((uint32_t)((int32_t)src1 + (int32_t)imm), 1), 8));
INSTPAT("??????? ????? ????? 001 ????? 00000 11", lh , I, R(rd) = SEXT(Mr((uint32_t)((int32_t)src1 + (int32_t)imm), 2), 16));
INSTPAT("??????? ????? ????? 010 ????? 00000 11", lw , I, R(rd) = SEXT(Mr((uint32_t)((int32_t)src1 + (int32_t)imm), 4), 32));
INSTPAT("??????? ????? ????? 100 ????? 00000 11", lbu , I, R(rd) = Mr((uint32_t)((int32_t)src1 + (int32_t)imm), 1));
INSTPAT("??????? ????? ????? 101 ????? 00000 11", lhu , I, R(rd) = Mr((uint32_t)((int32_t)src1 + (int32_t)imm), 2));
INSTPAT("??????? ????? ????? 000 ????? 00100 11", addi , I, R(rd) = (int32_t)((int32_t)src1 + (int32_t)imm)); // mv
INSTPAT("??????? ????? ????? 010 ????? 00100 11", slti , I, R(rd) = (int32_t)src1 < (int32_t)imm);
INSTPAT("??????? ????? ????? 011 ????? 00100 11", sltiu , I, R(rd) = (uint32_t)src1 < (uint32_t)imm);
INSTPAT("??????? ????? ????? 100 ????? 00100 11", xori , I, R(rd) = src1 ^ imm);
INSTPAT("000000? ????? ????? 001 ????? 00100 11", slli , I, R(rd) = (uint32_t)src1 << BITS(imm, 4, 0));
INSTPAT("000000? ????? ????? 101 ????? 00100 11", srli , I, R(rd) = (uint32_t)src1 >> BITS(imm, 4, 0));
INSTPAT("010000? ????? ????? 101 ????? 00100 11", srai , I, R(rd) = (int32_t)src1 >> BITS(imm, 4, 0));
INSTPAT("??????? ????? ????? 110 ????? 00100 11", ori , I, R(rd) = src1 | imm);
INSTPAT("??????? ????? ????? 111 ????? 00100 11", andi , I, R(rd) = src1 & imm);
// INSTPAT("??????? ????? ????? 000 ????? 00011 11", fence , I, R(rd) = src1 & imm);
INSTPAT("??????? ????? ????? 000 ????? 11001 11", jalr , I, R(rd) = s->snpc; s->dnpc = (uint32_t)((int32_t)src1 + (int32_t)imm) & -2ull; check_jalr(rd, imm, s)); // ret INSTPAT("??????? ????? ????? 000 ????? 01000 11", sb , S, Mw((uint32_t)((int32_t)src1 + (int32_t)imm), 1, src2));
INSTPAT("??????? ????? ????? 001 ????? 01000 11", sh , S, Mw((uint32_t)((int32_t)src1 + (int32_t)imm), 2, src2));
INSTPAT("??????? ????? ????? 010 ????? 01000 11", sw , S, Mw((uint32_t)((int32_t)src1 + (int32_t)imm), 4, src2)); INSTPAT("??????? ????? ????? 000 ????? 11000 11", beq , B, s->dnpc = src1 == src2 ? (uint32_t)((int32_t)s->pc + (int32_t)imm) : s->dnpc);
INSTPAT("??????? ????? ????? 001 ????? 11000 11", bne , B, s->dnpc = src1 != src2 ? (uint32_t)((int32_t)s->pc + (int32_t)imm) : s->dnpc);
INSTPAT("??????? ????? ????? 100 ????? 11000 11", blt , B, s->dnpc = (int32_t)src1 < (int32_t)src2 ? (uint32_t)((int32_t)s->pc + (int32_t)imm) : s->dnpc);
INSTPAT("??????? ????? ????? 101 ????? 11000 11", bge , B, s->dnpc = (int32_t)src1 >= (int32_t)src2 ? (uint32_t)((int32_t)s->pc + (int32_t)imm) : s->dnpc);
INSTPAT("??????? ????? ????? 110 ????? 11000 11", bltu , B, s->dnpc = (uint32_t)src1 < (uint32_t)src2 ? (uint32_t)((int32_t)s->pc + (int32_t)imm) : s->dnpc);
INSTPAT("??????? ????? ????? 111 ????? 11000 11", bgeu , B, s->dnpc = (uint32_t)src1 >= (uint32_t)src2 ? (uint32_t)((int32_t)s->pc + (int32_t)imm) : s->dnpc); INSTPAT("??????? ????? ????? ??? ????? 00101 11", auipc , U, R(rd) = s->pc + imm);
INSTPAT("??????? ????? ????? ??? ????? 01101 11", lui , U, R(rd) = imm); INSTPAT("??????? ????? ????? ??? ????? 11011 11", jal , J, R(rd) = s->snpc; s->dnpc = s->pc + imm; check_jal(s)); INSTPAT("0000000 00001 00000 000 00000 11100 11", ebreak , N, NEMUTRAP(s->pc, R(10))); // R(10) is $a0
INSTPAT("??????? ????? ????? ??? ????? ????? ??", inv , N, INV(s->pc)); void check_jal(Decode *s) {
if (elf_loaded) {
const char *func_name = find_symbol(s->dnpc);
if (func_name != NULL) {
call_depth++;
log_write("F\t%#010x\t%*sCall to %s @ %#010x\n", s->pc, call_depth * 2, "", func_name, s->dnpc);
}
}
} void check_jalr(word_t rd, word_t imm, Decode *s) {
uint32_t i = s->isa.inst;
int rs1 = BITS(i, 19, 15);
if (elf_loaded) {
if (rd == 0 && rs1 == 1 && imm == 0) {
// Function return
const char *func_name = find_symbol(s->pc);
if (func_name != NULL) {
log_write("F\t%#010x\t%*sReturn from %s\n", s->pc, call_depth * 2, "", func_name);
}
if (call_depth > 0) call_depth--;
}
else {
// Indirect function call
const char *func_name = find_symbol(s->dnpc);
if (func_name != NULL) {
call_depth++;
log_write("F\t%#010x\t%*sIndirect call to %s @ %#010x\n", s->pc, call_depth * 2, "", func_name, s->dnpc);
}
}
}
}
/* stdio.c 粘一些代表性的,不全粘贴 */ 

int vsprintf(char *out, const char *fmt, va_list ap) {
char* buf = out;
int total_len = 0; while (*fmt != '\0') {
if (*fmt != '%') {
*buf++ = *fmt++;
total_len++;
} else {
fmt++;
// Parse flags
int flags = 0;
int width = 0;
int precision = -1;
// int length = 0;
int specifier = 0; while (*fmt == '-' || *fmt == '+' || *fmt == ' ' || *fmt == '#' || *fmt == '0') {
switch (*fmt) {
case '#': flags |= 0x02; break; // Hash
case '+': flags |= 0x04; break; // Plus sign
case ' ': flags |= 0x08; break; // Space
case '-': flags |= 0x10; break; // Left align
case '0': flags |= 0x20; break; // Zero padding
}
fmt++;
} // Parse width
if (*fmt == '*') {
width = va_arg(ap, int);
fmt++;
} else {
while (*fmt >= '0' && *fmt <= '9') {
width = width * 10 + (*fmt++ - '0');
}
} // Parse precision
if (*fmt == '.') {
fmt++;
precision = 0;
if (*fmt == '*') {
precision = va_arg(ap, int);
fmt++;
} else {
while (*fmt >= '0' && *fmt <= '9') {
precision = precision * 10 + (*fmt++ - '0');
}
}
} // // Parse length modifier
// if (*fmt == 'h' || *fmt == 'l') {
// length = *fmt++;
// } // Parse specifier
specifier = *fmt++; // Handle specifier
if (specifier == 'd' || specifier == 'i') {
int value = va_arg(ap, int);
char temp[65];
int len = int_to_str(value, temp, 10, 0, width, precision > 0 ? precision : 0, flags);
for (int i = 0; i < len; i++) {
*buf++ = temp[i];
}
total_len += len;
} else if (specifier == 'o') {
unsigned int value = va_arg(ap, unsigned int);
char temp[65];
int len = int_to_str(value, temp, 8, 1, width, precision > 0 ? precision : 0, flags);
for (int i = 0; i < len; i++) {
*buf++ = temp[i];
}
total_len += len;
} else if (specifier == 'u') {
unsigned int value = va_arg(ap, unsigned int);
char temp[65];
int len = int_to_str(value, temp, 10, 1, width, precision > 0 ? precision : 0, flags);
for (int i = 0; i < len; i++) {
*buf++ = temp[i];
}
total_len += len;
} else if (specifier == 'x' || specifier == 'X') {
unsigned int value = va_arg(ap, unsigned int);
char temp[65];
if (specifier == 'X') flags |= 0x01; // Uppercase
int len = int_to_str(value, temp, 16, 1, width, precision > 0 ? precision : 0, flags);
for (int i = 0; i < len; i++) {
*buf++ = temp[i];
}
total_len += len;
} else if (specifier == 'c') {
char c = (char)va_arg(ap, int);
*buf++ = c;
total_len++;
} else if (specifier == 's') {
char *str = va_arg(ap, char*);
int len = precision >= 0 ? precision : strlen(str);
if (width > len && !(flags & 0x10)) { // Right align
for (int i = 0; i < width - len; i++) {
*buf++ = ' ';
total_len++;
}
}
for (int i = 0; i < len && str[i] != '\0'; i++) {
*buf++ = str[i];
total_len++;
}
if (width > len && (flags & 0x10)) { // Left align
for (int i = 0; i < width - len; i++) {
*buf++ = ' ';
total_len++;
}
}
} else if (specifier == 'p') {
void *ptr = va_arg(ap, void*);
unsigned long value = (unsigned long)ptr;
char temp[65];
flags |= 0x02; // Force '0x'
int len = int_to_str(value, temp, 16, 1, width, precision > 0 ? precision : 0, flags);
for (int i = 0; i < len; i++) {
*buf++ = temp[i];
}
total_len += len;
} else if (specifier == 'f' || specifier == 'F' || specifier == 'e' || specifier == 'E' || specifier == 'g' || specifier == 'G') {
assert(0);
// double value = va_arg(args, double);
// char temp[320];
// int len = double_to_str(value, temp, precision >= 0 ? precision : 6, specifier, flags, width);
// for (int i = 0; i < len; i++) {
// /*buf++ = temp[i];
// }
// total_len += len;
} else if (specifier == '%') {
*buf++ = '%';
total_len++;
} else if (specifier == 'n') {
int *ptr = va_arg(ap, int*);
*ptr = total_len;
} else {
// Invalid specifier
assert(0);
}
}
} *buf = '\0';
return buf - out;
} int sprintf(char *out, const char *fmt, ...) {
va_list args;
va_start(args, fmt);
int total_len = vsprintf(out, fmt, args);
va_end(args);
return total_len;
} int snprintf(char *out, size_t n, const char *fmt, ...) {
panic("Not implemented");
} int vsnprintf(char *out, size_t n, const char *fmt, va_list ap) {
panic("Not implemented");
} static int int_to_str(int value, char *buffer, int base, int is_unsigned, int width, int precision, int flags) {
char temp[65];
int i = 0;
unsigned int uvalue = value;
int is_negative = 0; if (!is_unsigned && value < 0) {
is_negative = 1;
uvalue = -value;
} if (uvalue == 0) {
temp[i++] = '0';
} else {
while (uvalue != 0) {
int digit = uvalue % base;
if (digit < 10) {
temp[i++] = digit + '0';
} else {
if (flags & 0x01) { // Uppercase for X
temp[i++] = digit - 10 + 'A';
} else {
temp[i++] = digit - 10 + 'a';
}
}
uvalue /= base;
}
} // Handle precision
while (i < precision) {
temp[i++] = '0';
} // Add prefix for '#' flag
if (flags & 0x02) {
if (base == 8 && temp[i-1] != '0') {
temp[i++] = '0';
} else if (base == 16) {
temp[i++] = flags & 0x01 ? 'X' : 'x';
temp[i++] = '0';
}
else {
assert(0);
}
} int len = i;
// Handle zero-padding and width
int padding = width - len - 1; // 1-width for the sign if (padding > 0 && (flags & 0x20) && !(flags & 0x10)) { // Right align
while (padding-- > 0) {
temp[i++] = '0';
}
} if (is_negative) {
temp[i++] = '-';
} else if (flags & 0x04) { // '+' flag
temp[i++] = '+';
} else if (flags & 0x08) { // ' ' flag
temp[i++] = ' ';
} if (padding > 0 && !(flags & 0x20) && !(flags & 0x10)) { // Right align
while (padding-- > 0) {
temp[i++] = ' ';
}
} reverse_str(temp, i); // Copy to buffer
for (int j = 0; j < i; j++) {
*buffer++ = temp[j];
} // Right padding if left align
if (padding > 0 && (flags & 0x10)) {
while (padding-- > 0) {
*buffer++ = ' ';
}
} return i;
}
/* iringbuf.c 粘一些代表性的,不全粘贴 */ 

void record_inst(uint64_t pc, const char *asm_code) {
inst_buf[inst_buf_pos].pc = pc;
// Safely copy the assembly code to prevent buffer overflow
strncpy(inst_buf[inst_buf_pos].asm_code, asm_code, sizeof(inst_buf[inst_buf_pos].asm_code) - 1);
inst_buf[inst_buf_pos].asm_code[sizeof(inst_buf[inst_buf_pos].asm_code) - 1] = '\0'; // Ensure null-termination
inst_buf_pos = (inst_buf_pos + 1) % INST_BUF_SIZE; // Update write position to create a ring buffer
if (inst_buf_count < INST_BUF_SIZE) {
inst_buf_count++; // Increment the count of recorded instructions
}
}
/* elf.c */ 

#include <elf.h>

typedef struct {
char *name;
uint64_t addr;
uint64_t size;
} FunctionSymbol; static FunctionSymbol *symbol_table = NULL;
static int symbol_count = 0;
static int symbol_capacity = 0;
bool elf_loaded = false; void add_symbol(const char *name, uint64_t addr, uint64_t size) {
if (symbol_count >= symbol_capacity) {
symbol_capacity = symbol_capacity == 0 ? 1024 : symbol_capacity * 2;
symbol_table = realloc(symbol_table, symbol_capacity * sizeof(FunctionSymbol));
}
symbol_table[symbol_count].name = strdup(name);
symbol_table[symbol_count].addr = addr;
symbol_table[symbol_count].size = size;
symbol_count++;
} const char *find_symbol(uint64_t addr) {
if (!elf_loaded) return NULL;
for (int i = 0; i < symbol_count; i++) {
if (addr >= symbol_table[i].addr && addr < symbol_table[i].addr + symbol_table[i].size) {
return symbol_table[i].name;
}
}
return NULL;
} void init_elf(const char *elf_file) {
if (elf_file == NULL) {
elf_loaded = false;
return;
} FILE *fp = fopen(elf_file, "rb");
Assert(fp, "Cannot open '%s'", elf_file); elf_loaded = true; // Read ELF header
Elf32_Ehdr ehdr;
if (fread(&ehdr, sizeof(Elf32_Ehdr), 1, fp) != 1) {
printf("Failed to read ELF header\n");
exit(1);
} // Verify ELF magic number
if (memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0) {
printf("Not an ELF file\n");
exit(1);
} // Read section headers
Elf32_Shdr *shdrs = malloc(ehdr.e_shentsize * ehdr.e_shnum);
fseek(fp, ehdr.e_shoff, SEEK_SET);
if (fread(shdrs, ehdr.e_shentsize, ehdr.e_shnum, fp) != ehdr.e_shnum) {
printf("Failed to read section headers\n");
exit(1);
} // Read section header string table
Elf32_Shdr shstr_shdr = shdrs[ehdr.e_shstrndx];
char *shstrtab = malloc(shstr_shdr.sh_size);
fseek(fp, shstr_shdr.sh_offset, SEEK_SET);
if (fread(shstrtab, shstr_shdr.sh_size, 1, fp) != 1) {
printf("Failed to read section header string table\n");
exit(1);
} // Find .symtab and .strtab sections
Elf32_Shdr *symtab_shdr = NULL;
Elf32_Shdr *strtab_shdr = NULL;
for (int i = 0; i < ehdr.e_shnum; i++) {
char *section_name = &shstrtab[shdrs[i].sh_name];
if (shdrs[i].sh_type == SHT_SYMTAB && strcmp(section_name, ".symtab") == 0) {
symtab_shdr = &shdrs[i];
} else if (shdrs[i].sh_type == SHT_STRTAB && strcmp(section_name, ".strtab") == 0) {
strtab_shdr = &shdrs[i];
}
} if (symtab_shdr == NULL || strtab_shdr == NULL) {
printf("Failed to find .symtab or .strtab in ELF file\n");
exit(1);
} // Read symbol table
int sym_count = symtab_shdr->sh_size / symtab_shdr->sh_entsize;
Elf32_Sym *symtab = malloc(symtab_shdr->sh_size);
fseek(fp, symtab_shdr->sh_offset, SEEK_SET);
if (fread(symtab, symtab_shdr->sh_entsize, sym_count, fp) != sym_count) {
printf("Failed to read symbol table\n");
exit(1);
} // Read string table
char *strtab = malloc(strtab_shdr->sh_size);
fseek(fp, strtab_shdr->sh_offset, SEEK_SET);
if (fread(strtab, strtab_shdr->sh_size, 1, fp) != 1) {
printf("Failed to read string table\n");
exit(1);
} // Store function symbols
for (int i = 0; i < sym_count; i++) {
Elf32_Sym sym = symtab[i];
char *name = &strtab[sym.st_name];
if (ELF32_ST_TYPE(sym.st_info) == STT_FUNC) {
add_symbol(name, sym.st_value, sym.st_size);
}
} // Free allocated memory
free(shdrs);
free(shstrtab);
free(symtab);
free(strtab);
fclose(fp);
}
/* gpu.c 粘一些代表性的,不全粘贴 */ 

void __am_gpu_config(AM_GPU_CONFIG_T *cfg) {
uint32_t data = inl(VGACTL_ADDR);
int width = (data >> 16) & 0xffff;
int height = data & 0xffff;
int vmemsz = width * height * sizeof(uint32_t);
*cfg = (AM_GPU_CONFIG_T) {
.present = true, .has_accel = false,
.width = width, .height = height,
.vmemsz = vmemsz
};
} void __am_gpu_fbdraw(AM_GPU_FBDRAW_T *ctl) {
int x = ctl->x, y = ctl->y, w = ctl->w, h = ctl->h;
int width = inl(VGACTL_ADDR) >> 16, height = inl(VGACTL_ADDR) & 0xffff; uint32_t *pixels = ctl->pixels;
uint32_t *fb = (uint32_t *)(uintptr_t)FB_ADDR; for (int i = y; i < y + h && i < height; i++) {
for (int j = x; j < x + w && j < width; j++) {
fb[i * width + j] = pixels[(i - y) * w + (j - x)];
}
} if (ctl->sync) {
outl(SYNC_ADDR, 1);
}
}
/* audio.c 粘一些代表性的,不全粘贴 */ 

void __am_audio_play(AM_AUDIO_PLAY_T *ctl) {
uint8_t *audio_data = (ctl->buf).start;
uint32_t sbuf_size = inl(AUDIO_SBUF_SIZE_ADDR);
uint32_t len = (ctl->buf).end - (ctl->buf).start; uint8_t *ab = (uint8_t *)(uintptr_t)AUDIO_SBUF_ADDR;
for(int i = 0; i < len; i++){
ab[sbuf_pos] = audio_data[i];
sbuf_pos = (sbuf_pos + 1) % sbuf_size;
}
outl(AUDIO_COUNT_ADDR, inl(AUDIO_COUNT_ADDR) + len);
}
/* am/audio.c 粘一些代表性的,不全粘贴 */ 

void __am_audio_play(AM_AUDIO_PLAY_T *ctl) {
uint8_t *audio_data = (ctl->buf).start;
uint32_t sbuf_size = inl(AUDIO_SBUF_SIZE_ADDR);
uint32_t len = (ctl->buf).end - (ctl->buf).start; uint8_t *ab = (uint8_t *)(uintptr_t)AUDIO_SBUF_ADDR;
for(int i = 0; i < len; i++){
ab[sbuf_pos] = audio_data[i];
sbuf_pos = (sbuf_pos + 1) % sbuf_size;
}
outl(AUDIO_COUNT_ADDR, inl(AUDIO_COUNT_ADDR) + len);
}
/* nemu/audio.c 粘一些代表性的,不全粘贴 */ 

void init_sound();

static void audio_io_handler(uint32_t offset, int len, bool is_write) {
if(audio_base[reg_init] == 1){
init_sound();
audio_base[reg_init] = 0;
}
} void sdl_audio_callback(void *userdata, uint8_t *stream, int len){
SDL_memset(stream, 0, len);
uint32_t used_cnt = audio_base[reg_count];
len = len > used_cnt ? used_cnt : len; uint32_t sbuf_size = audio_base[reg_sbuf_size];
if( (sbuf_pos + len) > sbuf_size ){
SDL_MixAudio(stream, sbuf + sbuf_pos, sbuf_size - sbuf_pos , SDL_MIX_MAXVOLUME);
SDL_MixAudio(stream + (sbuf_size - sbuf_pos), sbuf, len - (sbuf_size - sbuf_pos), SDL_MIX_MAXVOLUME);
}
else
SDL_MixAudio(stream, sbuf + sbuf_pos, len , SDL_MIX_MAXVOLUME);
sbuf_pos = (sbuf_pos + len) % sbuf_size;
audio_base[reg_count] -= len;
} void init_sound() {
SDL_AudioSpec s = {};
s.format = AUDIO_S16SYS;
s.userdata = NULL;
s.freq = audio_base[reg_freq];
s.channels = audio_base[reg_channels];
s.samples = audio_base[reg_samples];
s.callback = sdl_audio_callback;
SDL_InitSubSystem(SDL_INIT_AUDIO);
SDL_OpenAudio(&s, NULL);
SDL_PauseAudio(0);
}

NJU ICS2024 PA 作业心得(二)的更多相关文章

  1. 团队作业(二):ASB

    团队作业(二):团队选题 题目四:基于Android的文件加密系统 系统名称:ASB 一.引言 1.1编写目的 (1)学习并熟悉掌握AES/DES加密算法的原理以及算法 (2)学习并熟悉Android ...

  2. 2017-2018-2 1723《程序设计与数据结构》第八周作业 & 实验二 & 第一周结对编程 总结

    作业地址 第八周作业:https://edu.cnblogs.com/campus/besti/CS-IMIS-1723/homework/1847 (作业界面已评分,可随时查看,如果对自己的评分有意 ...

  3. 软件工程课堂作业(二)续——升级完整版随机产生四则运算题目(C++)

    一.设计思想: 1.根据题目新设要求,我将它们分为两类:一类是用户输入数目,根据这个数目改变一系列后续问题:另一类是用户输入0或1,分情况解决问题. 2.针对这两类要求,具体设计思路已在上篇博文中写出 ...

  4. Android studio使用心得(二)— 打包签名apk发布

    1.—–Android Studio菜单   Build->Generate Signed APK 2.——Create new.. 3.——-跟eclipse里面一样,添加keystore 信 ...

  5. BUAA2020软工作业(二)——对软件工程的初步理解

    项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 个人博客作业 我在这个课程的目标是 进一步提高自己的编码能力,工程能力 这个作业在哪个具体方面帮助 ...

  6. 关于学习JavaScript 的 高三编程 一些心得(二)

    今天在看高三的过程中,解决了之前我在 面试过程中遇到的一个问题. 就是将一段英文 颠倒过来. 下面就是我的代码: var zhang = "my name is zhangge !" ...

  7. 深入理解Java虚拟机阅读心得(二)

    垃圾收集 程序计数器.虚拟机栈.本地方法栈三个区域随线程而生,随线程而灭:这几个区域的内存分配和回收都具备稳定性,不需要过多的考虑回收的问题.而Java堆和方法区则不一样. Java堆中存储了几乎所有 ...

  8. python作业(二)实现注册功能和登陆功能

    #1.实现注册功能 输入:username.passowrd,cpassowrd #最多可以输错3次 #3个都不能为空 #用户名长度最少6位, 最长20位,用户名不能重复 #密码长度最少8位,最长15 ...

  9. oo作业总结(二)

    概述 和前三次作业相比,这几次作业最大的不同是难度的飞跃.遗憾的是在这难度的变化面前,我自己却没有做好充分的准备,错误的低估了作业难度导致给自己带来了很多不必要麻烦和损失.接下来我将对它们进行说明(度 ...

  10. 老男孩Day1作业(二):三级菜单

    作业需求: 1. 运行程序输出第一级菜单 2. 选择一级菜单某项,输出二级菜单,同理输出三级菜单 3. 菜单数据保存在文件中 4. 让用户选择是否要退出 5. 有返回上一级菜单的功能 1)编写思路 编 ...

随机推荐

  1. 【C3】02 操作总览

    在这篇文章中,我们将会拿一个简单的HTML文档做例子,并且在上边使用CSS样式,期待你能在此过程中学会更多有关CSS的实战性知识. 前置知识 在开始本单元之前,您应该: 基本熟悉计算机操作. 基本工作 ...

  2. 关于工业AI辅助制造(模具设计、模样生产制造环节)

    关于工业AI辅助制造(模具设计.模样生产制造环节) AI技术的具体使用场景: AI辅助模具设计: AI辅助模具安装工艺参数调整. 具体方案设想: AI辅助模具设计: 使用AI大模型对历史已有的设计方案 ...

  3. vue 识别\n \t 等字符(转载)

    使用.text(str)时,str中有"\n",但是页面显示却没有换行,需要在所在的 div添加属性 style="white-space: pre-line" ...

  4. 成为Apache SeaTunnel贡献者的N种方式

    如何参与开源贡献 参与开源贡献的常见方法有多种: 1)参与解答 在社区中, 帮助使用过程中遇到困难的人,帮他们解释框架的用法也算是一种贡献. 2)文档贡献 帮助框架来完善文档,比如说将英文文档翻译为中 ...

  5. 为了落地DDD,我是这样“PUA”大家的

    本文书接上回<先有鸡还是先有蛋?这是领域驱动设计落地最大的困局> https://mp.weixin.qq.com/s/lzAZXgchCg_VyLmyo2N18Q   故事背景 2023 ...

  6. 初探 Rust 语言与环境搭建

    1. Rust 简介 Rust 的历史 起源:Rust 语言最初由 Mozilla 研究员 Graydon Hoare 于 2006 年开始设计,并于 2009 年首次公开. 开发:Rust 是 Mo ...

  7. 这才是java对象正解

    这才是 Java 对象正解 在深入讨论对象之前,让我们先明确对对象和实例的理解. 什么是对象? 对象(Object)是内存中分配的实际数据结构,它包含了数据和方法.在 Java 中,对象是类的一个实例 ...

  8. 折腾 Quickwit,Rust 编写的分布式搜索引擎(专为从对象存储中实现亚秒级搜索而设计)

    什么是 Quickwit? Quickwit 是首个能在云端存储上直接执行复杂的搜索与分析查询的引擎,并且具有亚秒级延迟.它借助 Rust 语言和分离计算与存储的架构设计,旨在实现资源高效利用.易于操 ...

  9. MATLAB 使用

    MATLAB CLI 启动 MATLAB CLI 交互式界面(需要已安装 MATLAB): matlab -nodesktop -nosplash # 无桌面环境,无启动动画 不启动 MATLAB 直 ...

  10. 利用分布式锁在ASP.NET Core中实现防抖

    前言 在 Web 应用开发过程中,防抖(Debounce) 是确保同一操作在短时间内不会被重复触发的一种有效手段.常见的场景包括防止用户在短时间内重复提交表单,或者避免多次点击按钮导致后台服务执行多次 ...