NJU-PA1

PA1

要开始受苦咯,我选择riscv32/64.

记录一些有趣的东西和相应的问题.

我大概花了30-40小时完成,好累.

记一下教训和学习到的:

  • make多线程和ccache

  • 当发现自己一个一个举例时,要抽象出模板来实现

  • 在直接判断时要注意能否复用

  • 判断中黑名单和白名单一样重要

  • sanitizer

    -fsanitize= 是 GCC 和 Clang 编译器提供的一组强大的编译时检测工具,用于在运行时发现程序中的各类错误,无需手动调试。以下是其主要类型及作用:

    1. AddressSanitizer (ASan)

      gcc -fsanitize=address -g program.c -o program

      • 作用:检测内存访问越界、使用已释放内存(野指针)、栈 / 堆 / 全局变量缓冲区溢出等。
      • 原理:在编译时插入检测代码,运行时使用红区(RedZone)和影子内存(Shadow Memory)跟踪内存状态。
      • 适用场景:C/C++ 程序的内存错误排查。
    2. MemorySanitizer (MSan)

      gcc -fsanitize=memory -g program.c -o program

      • 作用:检测未初始化内存的使用(如读取未初始化变量)。
      • 原理:通过影子内存标记未初始化内存,在访问时触发警告。
      • 适用场景:C/C++ 程序中难以追踪的未初始化值问题。
    3. LeakSanitizer (LSan)

      gcc -fsanitize=leak -g program.c -o program

      • 作用:检测内存泄漏(未释放的动态分配内存)。
      • 原理:在程序结束时扫描堆内存,标记未释放的内存块。
      • 适用场景:长期运行的程序或资源敏感的应用。
    4. 线程安全检测ThreadSanitizer (TSan)

      gcc -fsanitize=thread -g -pthread program.c -o program

      • 作用:检测数据竞争(Race Condition)、死锁和线程同步问题。
      • 原理:记录内存访问的线程信息和锁状态,在冲突时报告。
      • 适用场景:多线程程序(如并行计算、服务器应用)。
    5. 整数运算错误检测IntegerSanitizer (ISan)

      gcc -fsanitize=integer -g program.c -o program

      • 作用:检测整数溢出(如整数加法、乘法溢出)、除零错误等。
      • 原理:在编译时插入检查代码,运行时捕获异常运算。
      • 适用场景:对数值精度要求高的程序(如金融、科学计算)。
    6. 未定义行为检测**UndefinedBehaviorSanitizer (UBSan)

      gcc -fsanitize=undefined -g program.c -o program

      • 作用:

        检测各种未定义行为,如:

        • 空指针解引用
        • 有符号整数溢出
        • 未定义的移位操作
        • 越界访问数组
      • 原理:在编译时插入运行时检查,发现问题时触发断言。

      • 适用场景:提升代码健壮性,捕捉潜在漏洞。

    7. BoundsChecker

      gcc -fsanitize=bounds -g program.c -o program

      • 作用:检测数组越界访问(比 ASan 更轻量)。
      • 限制:仅支持静态已知大小的数组。
    8. SafeStack

      gcc -fsanitize=safestack -g program.c -o program

      • 作用:检测栈缓冲区溢出(尤其是递归深度大的程序)。
      • 原理:将函数局部变量存储在单独的栈中,防止溢出影响其他变量。
    9. CFI (Control Flow Integrity)

      gcc -fsanitize=cfi -g program.c -o program

      • 作用:检测控制流劫持(如函数指针被篡改),增强安全性。
      • 适用场景:防范缓冲区溢出攻击。

    可同时启用多个 sanitizer, 但需谨慎,主要原因是不同 sanitizer 的实现机制、内存模型和运行时行为可能存在冲突

  • gcc -Wall -Werror

PA1.1 简易调试器

在开始愉快的PA之旅之前

首先学习的是一堆fuck,RTFW,RTFSC,STFM…

Make小知识

多线程运行:可以用-j??=你想要的线程数量

查看效果:time

清理编译结果:clean

还可以用ccache存储中间文件,然后能够用链接把原本的gcc改为它的位置来默认启用.

What’s ISA?

ISA本质是规范手册.

What’s TRM

为了表达对图灵的敬仰, 我们也把上面这个最简单的计算机称为"图灵机"(Turing Machine, TRM).

开天辟地的篇章

选做题:计算机可以没有寄存器吗? (建议二周目思考)

如果没有寄存器, 计算机还可以工作吗? 如果可以, 这会对硬件提供的编程模型有什么影响呢?

就算你是二周目来思考这个问题, 你也有可能是第一次听到"编程模型"这个概念. 不过如果一周目的时候你已经仔细地阅读过ISA手册, 你会记得确实有这么个概念. 所以, 如果想知道什么是编程模型, RTFM吧.

可以,只是要一个用来存储数字的东西罢了.内存这种反复刷新的硬件也可以实现.

必做题:尝试理解计算机如何计算

在看到上述例子之前, 你可能会觉得指令是一个既神秘又难以理解的概念. 不过当你看到对应的C代码时, 你就会发现指令做的事情竟然这么简单! 而且看上去还有点蠢, 你随手写一个for循环都要比这段C代码看上去更高级.
不过你也不妨站在计算机的角度来理解一下, 计算机究竟是怎么通过这种既简单又笨拙的方式来计算1+2+...+100的. 这种理解会使你建立"程序如何在计算机上运行"的最初原的认识
这个不想写,其实能理解,就是以rip寄存器指向的每个指令为一次状态.
所以 计算机就是状态机,程序就是状态机

RTFSC

代码框架

ics2024
├── abstract-machine # 抽象计算机
├── am-kernels # 基于抽象计算机开发的应用程序
├── fceux-am # 红白机模拟器
├── init.sh # 初始化脚本
├── Makefile # 用于工程打包提交
├── nemu # NEMU
└── README.md

nemu
├── configs # 预先提供的一些配置文件
├── include # 存放全局使用的头文件
│ ├── common.h # 公用的头文件
│ ├── config # 配置系统生成的头文件, 用于维护配置选项更新的时间戳
│ ├── cpu
│ │ ├── cpu.h
│ │ ├── decode.h # 译码相关
│ │ ├── difftest.h
│ │ └── ifetch.h # 取指相关
│ ├── debug.h # 一些方便调试用的宏
│ ├── device # 设备相关
│ ├── difftest-def.h
│ ├── generated
│ │ └── autoconf.h # 配置系统生成的头文件, 用于根据配置信息定义相关的宏
│ ├── isa.h # ISA相关
│ ├── macro.h # 一些方便的宏定义
│ ├── memory # 访问内存相关
│ └── utils.h
├── Kconfig # 配置信息管理的规则
├── Makefile # Makefile构建脚本
├── README.md
├── resource # 一些辅助资源
├── scripts # Makefile构建脚本
│ ├── build.mk
│ ├── config.mk
│ ├── git.mk # git版本控制相关
│ └── native.mk
├── src # 源文件
│ ├── cpu
│ │ └── cpu-exec.c # 指令执行的主循环
│ ├── device # 设备相关
│ ├── engine
│ │ └── interpreter # 解释器的实现
│ ├── filelist.mk
│ ├── isa # ISA相关的实现
│ │ ├── mips32
│ │ ├── riscv32
│ │ ├── riscv64
│ │ └── x86
│ ├── memory # 内存访问的实现
│ ├── monitor
│ │ ├── monitor.c
│ │ └── sdb # 简易调试器
│ │ ├── expr.c # 表达式求值的实现
│ │ ├── sdb.c # 简易调试器的命令处理
│ │ └── watchpoint.c # 监视点的实现
│ ├── nemu-main.c # 你知道的...
│ └── utils # 一些公共的功能
│ ├── log.c # 日志文件相关
│ ├── rand.c
│ ├── state.c
│ └── timer.c
└── tools # 一些工具
├── fixdep # 依赖修复, 配合配置系统进行使用
├── gen-expr
├── kconfig # 配置系统
├── kvm-diff
├── qemu-diff
└── spike-diff

这里还介绍了nemu有个自己设计的Kconfig,是仿造linux的Kconfig,主要关心配置文件生成的autoconf.h(C)和conf.h(make).

选做题: kconfig生成的宏与条件编译

我们已经在上文提到过, kconfig会根据配置选项的结果在 nemu/include/generated/autoconf.h中定义一些形如CONFIG_xxx的宏, 我们可以在C代码中通过条件编译的功能对这些宏进行测试, 来判断是否编译某些代码. 例如, 当CONFIG_DEVICE这个宏没有定义时, 设备相关的代码就无需进行编译.
为了编写更紧凑的代码, 我们在nemu/include/macro.h中定义了一些专门用来对宏进行测试的宏. 例如IFDEF(CONFIG_DEVICE, init_device());表示, 如果定义了CONFIG_DEVICE, 才会调用init_device()函数; 而MUXDEF(CONFIG_TRACE, "ON", "OFF")则表示, 如果定义了CONFIG_TRACE, 则预处理结果为"ON"("OFF"在预处理后会消失), 否则预处理结果为"OFF".
这些宏的功能非常神奇, 你知道这些宏是如何工作的吗?

不知道.
其实就是用编译器来实现

选做题:为什么全部都是函数?

阅读init_monitor()函数的代码, 你会发现里面全部都是函数调用. 按道理, 把相应的函数体在init_monitor()中展开也不影响代码的正确性.
相比之下, 在这里使用函数有什么好处呢?

模块化

选做题:参数的处理过程

另外的一个问题是, 这些参数是从哪里来的呢?

压栈

选做题:究竟要执行多久?

cmd_c()函数中, 调用cpu_exec()的时候传入了参数-1, 你知道这是什么意思吗?

void cpu_exec(uint64_t n) {
  g_print_step = (n < MAX_INST_TO_PRINT);
  switch (nemu_state.state) {
  case NEMU_END:
  case NEMU_ABORT:
  case NEMU_QUIT:
    printf("Program execution has ended. To restart the program, exit NEMU and "
           "run again.\n");
    return;
  default:
    nemu_state.state = NEMU_RUNNING;
  }

  uint64_t timer_start = get_time();

  execute(n);// 这个就是代表运行几次.

  uint64_t timer_end = get_time();
  g_timer += timer_end - timer_start;

  switch (nemu_state.state) {
  case NEMU_RUNNING:
    nemu_state.state = NEMU_STOP;
    break;

  case NEMU_END:
  case NEMU_ABORT:
    Log("nemu: %s at pc = " FMT_WORD,
        (nemu_state.state == NEMU_ABORT
             ? ANSI_FMT("ABORT", ANSI_FG_RED)
             : (nemu_state.halt_ret == 0
                    ? ANSI_FMT("HIT GOOD TRAP", ANSI_FG_GREEN)
                    : ANSI_FMT("HIT BAD TRAP", ANSI_FG_RED))),
        nemu_state.halt_pc);
    // fall through
  case NEMU_QUIT:
    statistic();
  }
}

根据源码来看是一直运行.

选做题:潜在的威胁 (建议二周目思考)

"调用cpu_exec()的时候传入了参数-1", 这一做法属于未定义行为吗? 请查阅C99手册确认你的想法.

希腊奶,不查

选做题:谁来指示程序的结束?

在程序设计课上老师告诉你, 当程序执行到main()函数返回处的时候, 程序就退出了, 你对此深信不疑. 但你是否怀疑过, 凭什么程序执行到main()函数的返回处就结束了? 如果有人告诉你, 程序设计课上老师的说法是错的, 你有办法来证明/反驳吗? 如果你对此感兴趣, 请在互联网上搜索相关内容.

作为逆向手,不查.

选做题:有始有终 (建议二周目思考)-TODO

对于GNU/Linux上的一个程序, 怎么样才算开始? 怎么样才算是结束? 对于在NEMU中运行的程序, 问题的答案又是什么呢?

与此相关的问题还有: NEMU中为什么要有nemu_trap? 为什么要有monitor?

那应该是fini段?有点忘记linux下的CRT了.

对于NEMU,这个问题放以后来看看吧 TODO

trap用来模拟中断,monitor不过是监视用的,并非必须.

必做题:理解框架代码

你需要结合上述文字理解NEMU的框架代码.

如果你不知道"怎么才算是看懂了框架代码", 你可以先尝试进行后面的任务. 如果发现不知道如何下手, 再回来仔细阅读这一页面. 理解框架代码是一个螺旋上升的过程, 不同的阶段有不同的重点. 你不必因为看不懂某些细节而感到沮丧, 更不要试图一次把所有代码全部看明白.

必做题:优雅地退出

为了测试大家是否已经理解框架代码, 我们给大家设置一个练习: 如果在运行NEMU之后直接键入q退出, 你会发现终端输出了一些错误信息. 请分析这个错误信息是什么原因造成的, 然后尝试在NEMU中修复它.

作为第一个真正意义上的编程题(PA0有一个但应该不算),我是看别人的,因为我原本以为cpu_exec()只有两行,因为helix一直显示报错(哎哎,lsp),没办法了只能去看了…

只要在cmd_q()那里加上nemu_state.state = NEMU_QUIT;

基础设施

string库

因为这个实验,所以去碰了下从来没有认真研究过的string库,了解了这些函数

snprintf(sprintf的高级版,可以指定写入数量,返回预期写入数量,不包括终止符),sprintf,strtol(字符串转数字),strtok(这个有点坑爹,不知道为什么,我在ubuntu中要在函数里运行两遍,才是下一个值,很奇怪,windows下都不会这样)

必做题:实现单步执行, 打印寄存器, 扫描内存

熟悉了NEMU的框架之后, 这些功能实现起来都很简单, 同时我们对输出的格式不作硬性规定, 就当做是熟悉GNU/Linux编程的一次练习吧.

NEMU默认会把单步执行的指令打印出来(这里面埋了一些坑, 你需要RTFSC看看指令是在哪里被打印的), 这样你就可以验证单步执行的效果了.

不知道如何下手? 嗯, 看来你需要再阅读一遍RTFSC小节的内容了. 如果你已经忘记了某些注意事项, 重新去阅读一遍也是应该的.

实现成功,详情看源码,不贴上来了.一个是丑,另一个是学术诚信(虽然我没有这玩意,也不赞成,不过他们说有这必要,那就听吧)

这部分主要是单步运行,打印,扫描内存

单步还好,修改状态即可

打印已经在cpu.c给api了,实现即可.

扫描内存在vaddr.c给api了,运行即可.这里有个坑,我原本以为vaddr_read(uint32_t addr, int len)这个len是指扫描内存的长度,结果是字节长度,debug了一会.

PA1.2 表达式求值

必做题:实现算术表达式的词法分析

你需要完成以下的内容:

  • 为算术表达式中的各种token类型添加规则, 你需要注意C语言字符串中转义字符的存在和正则表达式中元字符的功能.
  • 在成功识别出token后, 将token的信息依次记录到tokens数组中.

学了会正则,大概会用了,这里其实有看别人代码,因为实现不知道如何实现寄存器,结果发现是没学到\b这个东西,用来定位字符开头和结尾.

而且发现自己好傻逼,一个一个case,别人直接用default来赋值,当时给自己气笑了.

当发现自己一个一个举例时,要抽象出模板来实现

选做题:为什么printf()的输出要换行?

如果不换行, 可能会发生什么? 你可以在代码中尝试一下, 并思考原因, 然后STFW对比你的想法.

如果不换行,那打印就不换行了.

必做题:实现算术表达式的递归求值(我直接把其他表达式也实现了)

由于ICS不是算法课, 我们已经把递归求值的思路和框架都列出来了. 你需要做的是理解这一思路, 然后在框架中填充相应的内容. 实现表达式求值的功能之后, p命令也就不难实现了

这里debug最久了,因为我还把下一节的任务做了.主要就是前面讲的strtok的坑,还有一些判断没搞好.

特别是优先级,我没有把他们划分优先级(其实这样普适性更强),而是单纯if,所以搞了一会.

注意,判断中黑名单和白名单一样重要.

选做题:实现带有负数的算术表达式的求值 (选做)

在上述实现中, 我们并没有考虑负数的问题, 例如

"1 + -1"
"--1"    /* 我们不实现自减运算, 这里应该解释成 -(-1) = 1 */

它们会被判定为不合法的表达式. 为了实现负数的功能, 你需要考虑两个问题:

  • 负号和减号都是-, 如何区分它们?
  • 负号是个单目运算符, 分裂的时候需要注意什么?

你可以选择不实现负数的功能, 但你很快就要面临类似的问题了.

这里只能通过前后判断了.原本打算直接判断的,结果看下一节是用一个新的枚举表示,太对了.

在直接判断时要注意能否复用

必做题:表达式生成器

做的差不多了,然后鸽了.实在懒得搞.

PA1.3 观察点

必做题:实现监视点池的管理

为了使用监视点池, 你需要编写以下两个函数(你可以根据你的需要修改函数的参数和返回值):

WP* new_wp();
void free_wp(WP *wp);

其中new_wp()free_链表中返回一个空闲的监视点结构, free_wp()wp归还到free_链表中, 这两个函数会作为监视点池的接口被其它函数调用. 需要注意的是, 调用new_wp()时可能会出现没有空闲监视点结构的情况, 为了简单起见, 此时可以通过assert(0)马上终止程序. 框架代码中定义了32个监视点结构, 一般情况下应该足够使用, 如果你需要更多的监视点结构, 你可以修改NR_WP宏的值.

这两个函数里面都需要执行一些链表插入, 删除的操作, 对链表操作不熟悉的同学来说, 这可以作为一次链表的练习.

这个顺利多了

就是搞两个链表管理,没什么好讲的

后续为了看看有没有更好的思路,用ai,发现自己空指针了,修改了一下,并且直接插入头节点.

选做题:温故而知新

框架代码中定义wp_pool等变量的时候使用了关键字static, static在此处的含义是什么? 为什么要在此处使用它?

为了避免外部文件访问.

必做题:实现监视点

你需要实现上文描述的监视点相关功能, 实现了表达式求值之后, 监视点实现的重点就落在了链表操作上.

由于监视点的功能需要在cpu_exec()的每次循环中都进行检查, 这会对NEMU的性能带来较为明显的开销. 我们可以把监视点的检查放在trace_and_difftest()中, 并用一个新的宏 CONFIG_WATCHPOINT把检查监视点的代码包起来; 然后在nemu/Kconfig中为监视点添加一个开关选项, 最后通过menuconfig打开这个选项, 从而激活监视点的功能. 当你不需要使用监视点时, 可以在menuconfig中关闭这个开关选项来提高NEMU的性能.

在同一时刻触发两个以上的监视点也是有可能的, 你可以自由决定如何处理这些特殊情况, 我们对此不作硬性规定.

这里主要是Kconfig我没看懂,我就仿照实现了一下,结果成功了.

choice就是相应的大选项,config就是设置,这些设置都是宏.

选做题:你会如何测试你的监视点实现?

我们没有提供监视点相关的测试, 思考一下, 你会如何测试?

当然, 对于实验来说, 将来边用边测也是一种说得过去的方法, 就看你对自己代码的信心了.

bro懂我.

sanitizer - 一种底层的assert

段错误一般是由于非法访存造成的, 一种简单的想法是, 如果我们能在每一次访存之前都用assert()检查一下地址是否越界, 就可以在段错误发生之前捕捉到error了!

虽然我们只需要重点关注指针和数组的访问, 但这样的代码在项目中有很多, 如果要我们手动在这些访问之前添加assert(), 就太麻烦了. 事实上, 最适合做这件事情的是编译器, 因为它能知道指针和数组的访问都在哪里. 而让编译器支持这个功能的是一个叫Address Sanitizer的工具, 它可以自动地在指针和数组的访问之前插入用来检查是否越界的代码. GCC提供了一个-fsanitize=address的编译选项来启用它. menuconfig已经为大家准备好相应选项了, 你只需要打开它:

Build Options
  [*] Enable address sanitizer

然后清除编译结果并重新编译即可.

你可以尝试故意触发一个段错误, 然后阅读一下Address Sanitizer的报错信息. 不过你可能会发现程序的性能有所下降, 这是因为对每一次访存进行检查会带来额外的性能开销. 但作为一个可以帮助你诊断bug的工具, 付出这一点代价还是很值得的, 而且你还是可以在无需调试的时候将其关闭.

事实上, 除了地址越界的错误之外, Address Sanitizer还能检查use-after-free的错误 (即"释放从堆区申请的空间后仍然继续使用"的错误), 你知道它是如何实现这一功能的吗?

如果在添加GDB调试信息的情况下打开Address Sanitizer, 其报错信息还会指出发生错误的具体代码位置, 为问题的定位提供便利.

事实上, GCC还支持更多的sanitizer, 它们可以检查各种不同的错误, 你可以在man gcc中查阅-fsanitize相关的选项. 如果你的程序在各种sanitizer开启的情况下仍然能正确工作, 就说明你的程序还是有一定质量的.

学到了,以后多用.

选做题:如何提高断点的效率 (建议二周目思考)

如果你在运行稍大一些的程序(如microbench)的时候使用断点, 你会发现设置断点之后会明显地降低NEMU执行程序的效率. 思考一下这是为什么? 有什么方法解决这个问题吗?

大一点的话,每次都要检查吧.至于怎么处理,还真不知道.硬件断点?

选做题:一点也不能长?

x86的int3指令不带任何操作数, 操作码为1个字节, 因此指令的长度是1个字节. 这是必须的吗? 假设有一种x86体系结构的变种my-x86, 除了int3指令的长度变成了2个字节之外, 其余指令和x86相同. 在my-x86中, 上述文章中的断点机制还可以正常工作吗? 为什么?

不是必须的.

哪个文章?我找半天了.

选做题:随心所欲的断点

如果把断点设置在指令的非首字节(中间或末尾), 会发生什么? 你可以在GDB中尝试一下, 然后思考并解释其中的缘由.

[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[Inferior 1 (process 3798) exited normally]

看来是报错了,应该截断了opcode导致的.

选做题:NEMU的前世今生

你已经对NEMU的工作方式有所了解了. 事实上在NEMU诞生之前, NEMU曾经有一段时间并不叫NEMU, 而是叫NDB(NJU Debugger), 后来由于某种原因才改名为NEMU. 如果你想知道这一段史前的秘密, 你首先需要了解这样一个问题: 模拟器(Emulator)和调试器(Debugger)有什么不同? 更具体地, 和NEMU相比, GDB到底是如何调试程序的?

插入int3.

报告题

你需要在实验报告中回答下列问题:

  • 程序是个状态机 画出计算1+2+...+100的程序的状态机, 具体请参考这里.

    A:nope.

  • 理解基础设施 我们通过一些简单的计算来体会简易调试器的作用. 首先作以下假设:

    • 假设你需要编译500次NEMU才能完成PA.
    • 假设这500次编译当中, 有90%的次数是用于调试.
    • 假设你没有实现简易调试器, 只能通过GDB对运行在NEMU上的客户程序进行调试. 在每一次调试中, 由于GDB不能直接观测客户程序, 你需要花费30秒的时间来从GDB中获取并分析一个信息.
    • 假设你需要获取并分析20个信息才能排除一个bug.

    那么这个学期下来, 你将会在调试上花费多少时间?

    由于简易调试器可以直接观测客户程序, 假设通过简易调试器只需要花费10秒的时间从中获取并分析相同的信息. 那么这个学期下来, 简易调试器可以帮助你节省多少调试的时间?

    事实上, 这些数字也许还是有点乐观, 例如就算使用GDB来直接调试客户程序, 这些数字假设你能通过10分钟的时间排除一个bug. 如果实际上你需要在调试过程中获取并分析更多的信息, 简易调试器这一基础设施能带来的好处就更大.

    A:这是对的.

  • RTFM

    理解了科学查阅手册的方法之后, 请你尝试在你选择的ISA手册中查阅以下问题所在的位置, 把需要阅读的范围写到你的实验报告里面:

    • x86
      • EFLAGS寄存器中的CF位是什么意思?
      • ModR/M字节是什么?
      • mov指令的具体格式是怎么样的?
    • mips32
      • mips32有哪几种指令格式?
      • CP0寄存器是什么?
      • 若除法指令的除数为0, 结果会怎样?
    • riscv32
      • riscv32有哪几种指令格式?
      • LUI指令的行为是什么?
      • mstatus寄存器的结构是怎么样的?

    A:

  • shell命令 完成PA1的内容之后, nemu/目录下的所有.c和.h和文件总共有多少行代码? 你是使用什么命令得到这个结果的? 和框架代码相比, 你在PA1中编写了多少行代码? (Hint: 目前pa0分支中记录的正好是做PA1之前的状态, 思考一下应该如何回到"过去"?) 你可以把这条命令写入Makefile中, 随着实验进度的推进, 你可以很方便地统计工程的代码行数, 例如敲入make count就会自动运行统计代码行数的命令. 再来个难一点的, 除去空行之外, nemu/目录下的所有.c.h文件总共有多少行代码?

    A:

  • RTFM 打开nemu/scripters/build.mk文件, 你会在CFLAGS变量中看到gcc的一些编译选项. 请解释gcc中的-Wall-Werror有什么作用? 为什么要使用-Wall-Werror?

    A:-Wall是 "warn all" 的缩写,启用该选项后,gcc 会显示几乎所有常见的编译警告信息。这些警告通常指出代码中可能存在的逻辑错误、未使用的变量、隐式类型转换等问题。虽然这些问题不会直接导致编译失败,但可能在运行时引发错误。

    -Werror的作用是将所有警告视为错误(error)。当启用该选项后,如果代码中存在任何警告,编译过程将被终止.

posted @ 2025-08-30 15:26  T0fV404  阅读(27)  评论(0)    收藏  举报