进行PA的正确姿势(鸡汤)

  • 多思考为什么
    • 从问题开始着手理解系统也是个不错的方法
  • 独立解决问题
    • 即使是调一个很弱智的bug, "顺带"能学到的东西也比你想象中多得多
    • 换句话说, 如果你选择抱大腿, 你失去的机会也比你想象中多得多
  • 尝试尽可能理解每一处细节
    • 将来调bug的时候, 这些细节就是你手中强有力的工具
    • 换句话说, 当你在调bug的时候感到无从下手, 一定是你不了解其中的细节
  • 用正确的工具做事情
    • 这才是节省时间的科学方法, 而不是偷懒
  • 多读讲义, 彩蛋很多
    • 讲义中特地设置了不少"不合时宜"的提示, 有的彩蛋要多次阅读才能明白其中的奥妙
    • 多看一道蓝框题, 也许能少调几天bug
  • 按时完成, 拒绝拖延
    • 这样S你才有时间做到上面几点

make多线程编译文件

  • 使用lscpu查看cpu的个数;
  • 在运行make时添加-j?参数,其中?为查询到的cpu的数量,如果是n,则可以创建n个进程;

ccache帮助提高编译速度

  • PATH变量的前面添加/usr/lib/ccaceh
  • 使用which gcc发现输出/usr/lib/ccache/gcc

看待程序的两个角度

  • 从源码的角度;
  • 从状态机的角度;

了解配置系统kconfig

目前只需要关心两点即可:
  • 根据配置,生成文件nemu/include/generated/autoconf.h,用于定义C宏;
  • 根据配置,生成文件nemu/include/config/auto.conf,用于定义makefile变量;

看懂makefile文件

  • 利用make -nB实现只输出命令但不执行的效果,根据输出的字符串,反推一些变量的值;

nemu-main.c

存在一个检测宏CONFIG_TARGET_AM的条件编译,该宏的含义是什么?

init_monitor

考虑没有定义该宏的情况,剖析函数init_monitor(argc, argv);
该函数执行了多个函数:
  • parse_args(argc, argv):该函数起到一个解析命令行参数的作用,可以先暂时不用关注;
    • 实例化了一个struct option数组

2023.11.17
  • while循环条件中调用了函数getopt_long():用于解析命令行参数;

    11.18
    • init_rand():播种随机数;
    • void init_log(const char *log_file): 实现了一个log打印的功能,变量log_fp为目的地,默认为stdout,否则为log_file指定的文件指针;其中FILE *log_fp = NULL;在log.c中被定义;

    • void init_mem():内存初始化,分配一段内存,如果定义了某个宏,则对其进行随机初始化并最终输出内存的范围:
      • void init_isa() : 初始化isa相关:
        • load_img(): Load the image to memory.
          • init_difftest();
          • init_sdb();

          第一项工作,将客户程序读入到内存当中

          • 客户程序是什么:内置在nemu/src/isa/$ISA/init.c文件中的一段ricv-V指令;
          • 内存如何模拟:使用一个uint8_t类型的数组进行模拟,其定义在nemu/src/memory/paddr.c中;
          • 将客户程序读入到内存中的什么位置?约定让monitor直接将程序读入到一个固定的位置: RESET_VECTOR。其值在nemu/include/memory/paddr.h中定义;
            • 对RESET_VECTOR的定义:#define RESET_VECTOR (PMEM_LEFT + CONFIG_PC_RESET_OFFSET)
            • 对PMEM_LEFT的定义:#define PMEM_LEFT ((paddr_t)CONFIG_MBASE)
            • 对CONFIG_MBASE的定义如下:#define CONFIG_MBASE 0x8000000
            • 对CONFIG_PC_RESET_OFFSET的定义如下:#define CONFIG_PC_RESET_OFFSET 0x0

          11.19
          初始化完毕后内存布局如下:
          PS:什么是镜像文件?disk image

          11.19

          engine_start

          如果定义了CONFIG_TARGET_AM变量,则会执行cpu_exec(-1); 否则执行sdb_mainloop(); 函数;
          • sdb_mainloop()函数:
            • 首先判断是否为批处理模式
            • 其次,进入一个for循环,并且每次for循环都会先获取一个str字符串,如果不是NULL则对其进行处理,否则退出;
            • 在执行q命令时,makefile会报错,猜测可能时程序检测异常退出;
            • 在执行cmd_q时,改变nemu_state.state = NEMU_QUIT,问题解决;
          PS: paddr代表物理地址 vaddr代表虚拟地址

          min 11.19

          engine_start

          如果定义了CONFIG_TARGET_AM变量,则会执行cpu_exec(-1); 否则执行sdb_mainloop(); 函数;
          • sdb_mainloop()函数:
            • 首先判断是否为批处理模式
            • 其次,进入一个for循环,并且每次for循环都会先获取一个str字符串,如果不是NULL则对其进行处理,否则退出;
            • 在执行q命令时,makefile会报错,猜测可能时程序检测异常退出;
            • 在执行cmd_q时,改变nemu_state.state = NEMU_QUIT,问题解决;
          PS: paddr代表物理地址 vaddr代表虚拟地址

          11.19

          cpu_exec

          函数原型:
          • 第一行是:g_print_step = (n < MAX_INST_TO_PRINT);
          • 首先检验nemu_state.state的状态:
            • 如果是NEMU_END、NEMU_ABORT:打印说明程序已经执行完毕,并退出该函数;
            • 否则,令nemu_state.state = NEMU_RUNNING,然后模拟cpu运行;
            • uint64_t timer_start = get_time();应该是获取开始时间;
            • execute(n);
            • 该函数接受了一个uint64_t类型的参数n,是什么意思?n是程序指令的条数;
            • 为了弄清楚上个问题,先查看execute()函数的意思;

          execute

          • 声明Decode类型的变量s;
          • 随后以n作为循环索引变量进行循环,可以猜测n是指令的条数;
          • 随后调用了函数exec_once执行一条指令;
          • 随后对全局变量g_nr_guest_inst加一;
          • 调用trace_and_difftest函数;

          exec_once

          该函数就涵盖了一条指令完整的执行过程;
          • 好像需要利用Decode类型的变量s进行译码;先查看Decode;
          • Decode类型:
            • 将 s中的pc, snpc都 赋值为传入的参数pc,随后调用isa_exec_once(s), 然后将cpu.pc更新为s->dnpc
            • 先不管对s->logbuff变量的处理,查看isa_exec_once函数的用法;
            • CONFIG_ITRACE条件编译代码:
              • s->logbuf赋给p;
              • 查看snprintf函数原型:int snprintf(char *str, size_t size, const char *format, ...);将格式化字符创写入str中;
              • 明白了,p仅仅是一个索引,将当前执行的pc值格式化打印到logbuf中,再将所执行的命令逐个字节打印到logbuf中;
              • 添加一个空格,再最后添加一个\0方便打印输出;

            11.21

            isa_exec_once

            这个函数好简单:取指令,并返回decode_exec(s)的值;

            inst_fetch

            vaddr_ifetch

            paddr_read

            likely

            先放放,应该是一种条件判断;

            in_pmem

            PS: 妈的这些的代码真逆天啊,日,今天就到此为止吧11.20


            11.19

            decode_exec

            该函数的功能是译码并执行;
            这个还是比较复杂的,它牵扯到很多的宏定义和其他函数的调用,共同实现了译码的功能,要想看懂真的好难啊感觉,打开指令集手册,开干;
            • 声明了rd、src1、src2、imm来作为源寄存器地址、目的寄存器地址以及立即数;
            • 随后进行模式匹配!
            • 调用了INSTPAT_START()宏,该宏有些奇怪之处:
              • 调用了多个INSTPAT宏:
                • 执行U类型代码时使用了gpr()宏如下(为什么用小写啊!!!!)
                  • 基本就是一个译码+执行的过程,暂且一放吧;
                  代码:

                  trace_and_difftest

                  将当前执行了的指令的logbuf打印:

                  基础设施:简易调试器


                  11.19

                  common.h

                  • stdint.h;
                    • stdint.h是C标准函数库中的头文件,定义了具有特定位宽的整型,以及对应的宏;还列出了在其他标准头文件中定义的整型的极限。
                  • inttypes.h;

                  宏定义语法

                  • #define MAC(x,y) x##y: 将x与y两个字符串连接起来;
                  • 带多个参数的宏...与__VA_ARGS__;
                  • 基本理解了macro.h文件中MUXDEF、IFDEF等宏的实现原理

                  11.20

                  函数cmd_help(sdb.c)

                  • char *arg = strtok(NULL, " ");这一句话难道不会报段错误吗?
                  • 对于上述问题,我又查了以下strtok函数的具体用法:

                    11.20

                    cmd_c

                    • 执行了cpu_exec(-1);跑去查看该函数;
                    • 这里的-1被解释为无符号数,因此非常大,所以可以执行很多指令,直至cpu trap;

                    11.20
                     

                    11.21 不会用gdb,光眼瞪法实在是太难受了!!!!!根本不知道nemu_state.state的状态是在哪里被改变的啊啊啊!!!!我要恶心坏了!!!!!
                    通过使用gdb进行调试,终于明白了原来nemu_state是被指令所改变的,在decode_exec函数中!!!!

                    关于INSTPAT_START宏的解释如下

                    notion image

                    实现单步执行

                    这个好容易

                    实现打印寄存器的值

                    更容易了

                    扫描内存

                    只要实现了将一个十六进制数字符串转换为整数,就可以了

                    11.21
                    研究pmem的特点:
                    • 调用虚拟内存访问接口函数,即可访问静态变量pmem;
                    成功实现!!!

                    11.21

                    表达式求值

                    学会使用正则表达式识别token

                    • 要正确的识别token:
                      • 必须要有一个正确的正则表达式与其相匹配;
                      • 要有一个数据类型代表这个token;
                      • 以上两条就构成了一条规则,用于匹配识别所有token;
                    • init_regex():该函数在模拟器初始化时会将所有的规则编译为内部的pattern匹配信息;

                    init_regex

                    • 查看regcomp函数的用法:

                      Token结构体

                      该结构体用于记录每一个token的信息,空格串不需要记录;

                      make_token

                      该函数用于将表达式中的所有token识别、存储起来;

                      11.22
                      1.regex等调用、正则表达式元字符

                      实现算数表达式的词法分析

                      成功实现!
                      notion image

                      11.23

                      实现递归求值

                      • 首先更改了上一问题的bug,将空格字符丢弃;
                      • 尝试实现递归求值,未果。

                      11.24
                      解决check_parentheses函数括号匹配的bug,成功实现!

                      11.24

                      实现表达式生成器,并对自己的代码进行测试

                      gen-expr.c

                      • 播种随机数生成函数;
                      • 将第二个参数赋值为一个整数,作为获取测试样例的个数;
                      • 开始循环,逐个生成测试样例:
                        • 调用gen_rand_expr()获取生成的测试样例到buf中;
                        • 将buf输入到code_buf中;
                        • 打开/tmp/.code.c文件到fp;
                        • 将code_buf中的内容输出到fp中,关闭fp;
                        • 调用system执行gcc编译.code.c文件,这里利用system的返回值进行了判断;
                        • 打开/tmp/.expr文件,读入其中的内容到fp中;
                        • 将fp中的内容输入到result变量当中,关闭fp;
                        • 打印result和buf变量;

                        • 11.25基本实现表达式生成和自动求值

                      尝试解决表达式过长导致buf溢出的问题

                      初步的想法是,当检测到buf溢出的时候,直接舍弃掉当前表达式。
                      • 在main函数中,会判断system调用的gcc命令的返回状态,如果不为0,即编译失败,则直接continue,并不会基础处理该结果;
                      • 因此,只要检测表达式是否溢出就可以了,可以在判断buf满后不再做任何事情;

                      尝试解决分母为0的bug问题

                      这个bug解决了一天,都没有解决掉!!!
                      1. 在计算器中会报除0错误;
                      1. 用C去算则不会,有点逆天;
                      C不会产生core dump:
                      精简以下,C不会产生除0错误,:
                      排了一天的错误,结果发现了一个逆天bug:
                      • 当b作为一个变量嵌入到a中时,程序会产生浮点数错误,即除0错误;
                      • 当将b换成一个宏,粘贴到a中,一块计算a时,则不会产生除0错误;

                      发现一个有趣的现象

                      同一个表达式,在不同的编译器下进行编译计算,所得到的结果不同:
                      • 用gcc进行编译:会产生Floating point exception (core dumped)
                      • 用clang进行编译,则可以计算

                      终于解决了上面这个现象有关的bug

                      原因:C语言整数运算默认为有符号型!!!!!
                      这个bug调了两天,问了下老师终于明白了;

                      11.26

                      检查源码,尝试排除浮点异常错误

                      • 解决表达式生成器缓冲区溢出bug;
                      • 尝试解决除0bug,未果,手足无措

                      11.27
                      历时三四天终于决定进行第三阶段,第二阶段所语调的除0错误,我怀疑应该是gcc编译器或者c语言本身的一些特性所导致的,我的算法实现应该没有问题,因此现将这个bug放一放,不要影响了自己的心态,看看能不能在C语言手册中找到答案;

                      监视点

                      扩展表达式求值的功能


                      11.28 基本实现扩展功能,通过测试,但是有一个bug未能解决

                      修复十进制和十六进制匹配冲突bug

                      如何保证十六进制前面的0不会被十进制规则匹配?

                      11.28 实现监视点链表的管理功能(新建和释放), 解决上述bug(通过将十进制和十六进制的匹配规则的顺序置换以下,提高十六进制数匹配的优先级)

                      11.29 基本实现监测点暂停程序。

                      11.29 实现监测点暂停,进行部分测试。

                      1h 11.29 pa1基本完工。